Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"@tanstack/react-query": "^5.69.0",
"axios": "^1.7.9",
"clsx": "^2.1.1",
"event-source-polyfill": "^1.0.31",
"firebase": "^11.5.0",
"framer-motion": "^12.6.2",
"idb": "^8.0.2",
Expand Down
12 changes: 7 additions & 5 deletions src/app/store.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import { configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { authReducer } from '@service/feature/auth/store/auth/authSlice.ts';
import { authReducer } from '@service/feature/auth/store/auth/authSlice';
import teamReducer from '@service/feature/team/store/teamSlice';

const persistConfig = {
key: 'auth',
storage,
whitelist: ['user', 'isAuthenticated'],
whitelist: ['user', 'isAuthenticated', 'profile'],
};

const persistedAuthReducer = persistReducer(persistConfig, authReducer);

export const store = configureStore({
reducer: {
auth: persistedAuthReducer,
teams: teamReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ serializableCheck: false }),
});

export const persistor = persistStore(store);

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export type AppDispatch = typeof store.dispatch;

export const persistor = persistStore(store);
11 changes: 10 additions & 1 deletion src/service/feature/auth/api/profileApi.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import createAxiosInstance from '@service/feature/common/axios/axiosInstance.ts';
import { UserProfile } from '@service/feature/auth/types/profile.ts';
import {MemberState, UserProfile} from '@service/feature/auth/types/profile.ts';
import { ApiResponse } from '@service/feature/common/axios/apiType.ts';
import {UpdateProfileRequest} from "@service/feature/auth/schema/profileSchema.ts";

const axios = createAxiosInstance();

export const getProfile = async (): Promise<UserProfile> => {
const response = await axios.get<ApiResponse<UserProfile>>('/members');
return response.data.data;
};

export const updateStatus = async (memberState: MemberState): Promise<void> => {
await axios.patch('/members/status', {memberState});
};

export const updateProfile = async (profileData: UpdateProfileRequest): Promise<void> => {
await axios.put('/members', profileData);
};
10 changes: 5 additions & 5 deletions src/service/feature/auth/context/useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useContext } from 'react';
import { AuthContext } from './AuthContext';
import { AuthContext, AuthContextType } from './AuthContext';

export const useAuth = () => {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
return ctx;
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
};
28 changes: 25 additions & 3 deletions src/service/feature/auth/hook/auth/useLogin.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useMutation } from '@tanstack/react-query';
import { login } from '../../api/authApi';
import { useDispatch } from 'react-redux';
import { setUser } from '@service/feature/auth';
import {logout, setUser} from '@service/feature/auth';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { loginSchema } from '../../schema/authSchema.ts';
import {getProfile} from "@service/feature/auth/api/profileApi.ts";
import {setProfile} from "@service/feature/auth/store/profile/userSlice.ts";

export const useLogin = () => {
const dispatch = useDispatch();
Expand All @@ -21,7 +23,7 @@ export const useLogin = () => {
}
return login(result.data);
},
onSuccess: (data) => {
onSuccess: async (data) => {
if (!data.id || !data.token) {
toast.error('로그인에 성공했지만 사용자 정보가 올바르지 않습니다.');
return;
Expand All @@ -32,11 +34,19 @@ export const useLogin = () => {
dispatch(
setUser({
userId: data.id,
nickname: data.name,
email: '',
name: data.name,
}),
);

try {
const profile = await getProfile();
dispatch(setProfile(profile));
} catch (error) {
console.error('프로필 정보를 불러오지 못했습니다:', error);
toast.error('프로필 정보를 업데이트하지 못했습니다.');
}

toast.success('로그인 성공!');
navigate('/channels/@me');
},
Expand All @@ -47,3 +57,15 @@ export const useLogin = () => {
},
});
};

export const useLogout = () => {
const dispatch = useDispatch();

return () => {
dispatch(logout());
toast.success('로그아웃 되었습니다!');
document.cookie = 'accessToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;';
window.location.href = '/';
};
};

11 changes: 11 additions & 0 deletions src/service/feature/auth/schema/profileSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {z} from 'zod';

export const updateProfileSchema = z.object({
birth: z.string(),
name: z.string(),
newPassword: z.string().optional(),
password: z.string(),
avatarUrl: z.string().optional()
});

export type UpdateProfileRequest = z.infer<typeof updateProfileSchema>;
11 changes: 9 additions & 2 deletions src/service/feature/auth/store/auth/authSlice.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import {UserProfile} from "@service/feature/auth/types/profile.ts";

interface User {
userId: string;
email: string;
nickname: string;
name: string;
}

interface AuthState {
user: User | null;
profile: UserProfile | null;
isAuthenticated: boolean;
}

const initialState: AuthState = {
user: null,
profile: null,
isAuthenticated: false,
};

Expand All @@ -24,8 +27,12 @@ const authSlice = createSlice({
state.user = action.payload;
state.isAuthenticated = true;
},
setProfile(state, action: PayloadAction<UserProfile>) {
state.profile = action.payload;
},
logout: (state) => {
state.user = null;
state.profile = null;
state.isAuthenticated = false;
},
},
Expand All @@ -34,4 +41,4 @@ const authSlice = createSlice({
export const authReducer = authSlice.reducer;
export const authActions = authSlice.actions;

export const { setUser, logout } = authSlice.actions;
export const { setUser, setProfile, logout } = authSlice.actions;
3 changes: 2 additions & 1 deletion src/service/feature/auth/types/profile.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
export type MemberState = 'ONLINE' | 'OFFLINE' | 'IDLE' | 'DO_NOT_DISTURB';
export type MemberType = 'MEMBER' | 'ADMIN'

export interface UserProfile {
id: string;
email: string;
nickname: string;
name: string;
birth: string;
type: 'MEMBER' | 'ADMIN' | string;
type: MemberType;
avatarUrl: string | null;
state: MemberState;
createdAt: string;
Expand Down
16 changes: 2 additions & 14 deletions src/service/feature/channel/api/categorieAPI.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createAxiosInstance } from '@service/feature/common/axios/axiosInstance';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import {CreateCategoryResponse} from "@service/feature/channel/types/category.ts";

const axios = createAxiosInstance();

export const createCategory = async (teamId: string, name: string) => {
export const createCategory = async (teamId: string, name: string): Promise<CreateCategoryResponse> => {
const res = await axios.post(`/teams/${teamId}/categories`, { name });
return res.data;
};
Expand All @@ -20,17 +21,4 @@ export const moveCategory = async (teamId: string, body: {
}) => {
const res = await axios.patch(`/teams/${teamId}/categories/${body.prevCategoryId}`, body);
return res.data;
};


export const useCreateCategoryMutation = (teamId: string) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (name: string) => createCategory(teamId, name),
onSuccess: () => {
toast.success('카테고리 생성 완료!');
queryClient.invalidateQueries({ queryKey: ['teamStructure', teamId] });
},
});
};
98 changes: 39 additions & 59 deletions src/service/feature/channel/api/channelAPI.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,69 @@
import { createAxiosInstance } from '@service/feature/common/axios/axiosInstance';
import { DMDetail, DMList } from '../types/channel';
import { CreateChannelRequest, DMDetail, ChannelResponse } from '../types/channel';
import { MoveChannelRequest } from '@service/feature/channel/types/category';

const axios = createAxiosInstance();

export const getChannelList = async (teamId: string) => {
const res = await axios.get(`/teams/${teamId}`);
export const endpoints = {
teams: (teamId: string) => `/teams/${teamId}`,
categories: (teamId: string, categoryId: number) => `/teams/${teamId}/categories/${categoryId}/channels`,
channel: (channelId: number) => `/channels/${channelId}`,
dm: '/channels/me',
};

export const getChannelList = async (teamId: string): Promise<ChannelResponse> => {
const res = await axios.get(endpoints.teams(teamId));
return res.data.data;
};

export const createChannel = async ({
teamId,
categoryId,
name,
channelType,
}: {
teamId: string;
categoryId: number;
name: string;
channelType: 'TEXT' | 'VOICE';
}) => {
const res = await axios.post(
`/teams/${teamId}/categories/${categoryId}/channels`,
{ name, channelType },
);
export const createChannel = async (
teamId: string,
categoryId: number,
request: CreateChannelRequest
): Promise<void> => {
const res = await axios.post(endpoints.categories(teamId, categoryId), request);
return res.data;
};

export const deleteChannel = async ({
teamId,
categoryId,
channelId,
}: {
teamId: string;
categoryId: number;
channelId: number;
}) => {
const res = await axios.delete(
`/teams/${teamId}/categories/${categoryId}/channels/${channelId}`,
);
export const deleteChannel = async (
teamId: string,
categoryId: number,
channelId: number
): Promise<void> => {
const res = await axios.delete(`${endpoints.categories(teamId, categoryId)}/${channelId}`);
return res.data;
};

export const moveChannel = async (
teamId: string,
categoryId: number,
channelId: number,
body: {
destCategoryId: number;
prevChannelId: number;
nextChannelId: number;
},
) => {
const res = await axios.patch(
`/teams/${teamId}/categories/${categoryId}/channels/${channelId}`,
body,
);
teamId: string,
categoryId: number,
channelId: number,
body: MoveChannelRequest
): Promise<void> => {
const res = await axios.patch(`${endpoints.categories(teamId, categoryId)}/${channelId}`, body);
return res.data;
};

export const editChannel = async ({
teamId,
categoryId,
channelId,
}: {
teamId: string;
categoryId: number;
channelId: number;
}) => {
const res = await axios.patch(
`/teams/${teamId}/categories/${categoryId}/channels/${channelId}`,
);
export const editChannel = async (
teamId: string,
categoryId: number,
channelId: number
): Promise<void> => {
const res = await axios.patch(`${endpoints.categories(teamId, categoryId)}/${channelId}`);
return res.data;
};

export const getDMDetail = async (channelId: number): Promise<DMDetail> => {
const res = await axios.get(`/channels/${channelId}`);
const res = await axios.get(endpoints.channel(channelId));
return res.data.data;
};

export const getDMList = async (): Promise<DMDetail[]> => {
const res = await axios.get(`/channels/me`);
const res = await axios.get(endpoints.dm);
return res.data.data;
};

export const createDM = async (memberIds: string[]): Promise<DMDetail> => {
const res = await axios.post(`/channels/members`, { memberIds });
const res = await axios.post('/channels/members', { memberIds });
return res.data.data;
};
};
15 changes: 15 additions & 0 deletions src/service/feature/channel/hook/mutation/useCategoryMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {useMutation, useQueryClient} from "@tanstack/react-query";
import {toast} from "sonner";
import {createCategory} from "@service/feature/channel/api/categorieAPI.ts";

export const useCreateCategoryMutation = (teamId: string) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: ({ name }: { name: string }) => createCategory(teamId, name),
onSuccess: () => {
toast.success("카테고리 생성 완료!");
queryClient.invalidateQueries({ queryKey: ["teamStructure", teamId] });
},
});
};
Loading
Loading