Skip to content

Commit 4e5a77a

Browse files
authored
[FRONTEND] layered 패턴 적용 (#86)
layered 패턴 적용 api 패턴 최적화 (최종) 가이드 api 연결 예정
1 parent a23b242 commit 4e5a77a

36 files changed

+495
-992
lines changed

frontend/.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"kakao",
66
"lineheigh",
77
"lineheight",
8+
"mainboard",
89
"VITE"
910
],
1011
"cSpell.customDictionaries": {

frontend/src/App.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BrowserRouter, Route, Routes, Navigate } from "react-router";
1+
import { BrowserRouter, Route, Routes } from "react-router";
22
import { QueryClientProvider } from "@tanstack/react-query";
33
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
44
import "./App.css";
@@ -14,6 +14,7 @@ import SecondSetting from "./pages/secondSetting";
1414
import RedirectPage from "./pages/oauth/kakao/RedirectPage";
1515
import ExpertVerifyLayout from "./components/layout/expertVerifyLayout";
1616
import { ProtectedRoute } from "./routes/ProtectedRoute";
17+
import RootRedirect from "./routes/RootRedirect";
1718
import {
1819
ErrorBoundary,
1920
GlobalLoader,
@@ -25,7 +26,7 @@ const Router = () => {
2526
return (
2627
<BrowserRouter>
2728
<Routes>
28-
<Route path={"/"} element={<Navigate to="/signIn" replace />} />
29+
<Route path={"/"} element={<RootRedirect />} />
2930
<Route
3031
path={"/home"}
3132
element={

frontend/src/api/core/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export const API_ENDPOINTS = {
134134

135135
// 사용자 관련
136136
USERS: {
137-
PROFILE: '/users/profile',
137+
PROFILE: '/users/me',
138138
INITIALIZE: '/users/initialize',
139139
DELETE: '/users',
140140
EXPERT_VERIFY: '/users/expert-verify',

frontend/src/api/guide/useGuideData.ts

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,39 +39,30 @@ const transformApiGuideData = (apiData: ApiGuideData, id: number): GuideData =>
3939
* 가이드 데이터 조회 훅 옵션
4040
*/
4141
interface UseGuideDataOptions {
42-
category?: string;
4342
enabled?: boolean;
44-
useDummy?: boolean;
4543
}
4644

45+
/**
46+
* 모든 가이드 카테고리의 데이터를 한 번에 조회
47+
*/
4748
export const useGuideData = (
4849
options: UseGuideDataOptions = {}
4950
) => {
50-
const { category, enabled = true } = options;
51+
const { enabled = true } = options;
5152

5253
return useQuery({
53-
queryKey: category
54-
? QUERY_KEYS.GUIDE.BY_CATEGORY(category)
55-
: QUERY_KEYS.GUIDE.ALL,
54+
queryKey: QUERY_KEYS.GUIDE.ALL,
5655

5756
queryFn: async () => {
58-
// 실제 API 호출
59-
if (category) {
60-
// 카테고리를 소문자로 변환하여 API 호출
61-
const apiData = await guideService.getGuideByCategory(category.toLowerCase());
62-
// API 응답을 GuideData 형식으로 변환
63-
return transformApiGuideData(apiData, 1);
64-
}
65-
66-
// 모든 카테고리 데이터를 가져오기
67-
// 백엔드가 모든 카테고리를 반환하는 API가 없으므로 각 카테고리별로 호출
57+
// 카테고리 목록 하드코딩
6858
const categories = ['cpu', 'mainboard', 'ram', 'gpu', 'storage', 'power', 'case', 'cooler'];
6959

7060
const allGuides = await Promise.all(
7161
categories.map(async (cat, index) => {
7262
try {
7363
const apiData = await guideService.getGuideByCategory(cat);
74-
return transformApiGuideData(apiData, index + 1);
64+
const transformed = transformApiGuideData(apiData, index + 1);
65+
return transformed;
7566
} catch (error) {
7667
console.warn(`카테고리 ${cat} 데이터를 불러오는데 실패했습니다:`, error);
7768
return null;
@@ -80,12 +71,14 @@ export const useGuideData = (
8071
);
8172

8273
// null이 아닌 데이터만 필터링하여 반환
83-
return allGuides.filter((guide): guide is GuideData => guide !== null);
74+
const validGuides = allGuides.filter((guide): guide is GuideData => guide !== null);
75+
76+
return validGuides;
8477
},
8578

8679
enabled,
8780
staleTime: 1000 * 60 * 5, // 5분
88-
gcTime: 1000 * 60 * 30, // 30분 (cacheTime에서 변경됨)
81+
gcTime: 1000 * 60 * 30, // 30분
8982
});
9083
};
9184

frontend/src/api/services/guideService.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@ class GuideService {
2222

2323
/**
2424
* 카테고리별 가이드 데이터 조회
25-
* @param category - 가이드 카테고리 (CPU, 메인보드, RAM 등)
25+
* @param category - 가이드 카테고리 (cpu, mainboard, ram 등)
2626
*/
2727
async getGuideByCategory(category: string): Promise<ApiGuideData> {
28-
const response = await apiClient.get<ApiResponse<ApiGuideData>>(
29-
API_ENDPOINTS.GUIDE.BY_CATEGORY(category)
28+
const response = await apiClient.get<ApiGuideData>(
29+
API_ENDPOINTS.GUIDE.BASE,
30+
{
31+
params: { category }
32+
}
3033
);
31-
return response.data.data;
34+
return response.data;
3235
}
3336

3437
/**
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useMutation } from '@tanstack/react-query';
2+
import { useAuthStore } from '../../stores/useAuthStore';
3+
import { apiClient } from '../core/client';
4+
import type { CommonMutationOptions } from '../core/query-config';
5+
import { API_ENDPOINTS } from '../core/types';
6+
7+
// 계정 삭제
8+
export function useDeleteUser(
9+
options?: CommonMutationOptions<void, void>
10+
) {
11+
const { clearAuthState } = useAuthStore();
12+
13+
return useMutation({
14+
mutationFn: async () => {
15+
const response = await apiClient.delete<void>(API_ENDPOINTS.USERS.DELETE);
16+
return response;
17+
},
18+
onSuccess: () => {
19+
clearAuthState();
20+
window.location.href = '/signIn';
21+
},
22+
...options,
23+
});
24+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useMutation } from '@tanstack/react-query';
2+
import { apiClient } from '../core/client';
3+
import {
4+
invalidateQueries
5+
} from '../core/query-config';
6+
import type { CommonMutationOptions } from '../core/query-config';
7+
import { API_ENDPOINTS } from '../core/types';
8+
9+
// 전문가 인증
10+
interface ExpertVerifyRequest {
11+
certification?: string;
12+
portfolio_url?: string;
13+
certification_file?: File;
14+
}
15+
16+
export function useExpertVerify(
17+
options?: CommonMutationOptions<void, ExpertVerifyRequest>
18+
) {
19+
return useMutation({
20+
mutationFn: async (data: ExpertVerifyRequest) => {
21+
if (data.certification_file) {
22+
// 파일이 있는 경우 FormData 사용
23+
const formData = new FormData();
24+
formData.append('certification_file', data.certification_file);
25+
26+
if (data.certification) {
27+
formData.append('certification', data.certification);
28+
}
29+
if (data.portfolio_url) {
30+
formData.append('portfolio_url', data.portfolio_url);
31+
}
32+
33+
return apiClient.uploadFile<void>(API_ENDPOINTS.USERS.EXPERT_VERIFY, formData);
34+
} else {
35+
// 파일이 없는 경우 JSON 사용
36+
return apiClient.post<void>(API_ENDPOINTS.USERS.EXPERT_VERIFY, {
37+
certification: data.certification,
38+
portfolio_url: data.portfolio_url,
39+
});
40+
}
41+
},
42+
onSuccess: () => {
43+
// 프로필 정보 갱신
44+
invalidateQueries.userProfile();
45+
},
46+
...options,
47+
});
48+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useMutation } from '@tanstack/react-query';
2+
import { useAuthStore } from '../../stores/useAuthStore';
3+
import { apiClient } from '../core/client';
4+
import {
5+
invalidateQueries
6+
} from '../core/query-config';
7+
import type { CommonMutationOptions } from '../core/query-config';
8+
import { API_ENDPOINTS } from '../core/types';
9+
import type { UserData } from '../../types/user';
10+
11+
// 사용자 초기화
12+
interface InitializeUserRequest {
13+
role: string;
14+
interest_ids: number[];
15+
custom_interests?: string[];
16+
}
17+
18+
export function useInitializeUser(
19+
options?: CommonMutationOptions<UserData, InitializeUserRequest>
20+
) {
21+
const { setUser } = useAuthStore();
22+
23+
return useMutation({
24+
mutationFn: async (data: InitializeUserRequest) => {
25+
const response = await apiClient.put<UserData>(
26+
API_ENDPOINTS.USERS.INITIALIZE,
27+
data
28+
);
29+
return response;
30+
},
31+
onSuccess: (response) => {
32+
if (response.success && response.data) {
33+
setUser(response.data);
34+
invalidateQueries.user();
35+
36+
// 홈으로 이동
37+
window.location.href = '/home';
38+
}
39+
},
40+
...options,
41+
});
42+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { useMutation } from '@tanstack/react-query';
2+
import { useAuthStore } from '../../stores/useAuthStore';
3+
import { useTokenStore } from '../../stores/useTokenStore';
4+
import { apiClient } from '../core/client';
5+
import { API_ENDPOINTS } from '../core/types';
6+
import { getRedirectUri } from '../../config/api';
7+
import type { UserData } from '../../types/user';
8+
9+
// 카카오 로그인
10+
interface KakaoLoginRequest {
11+
code: string;
12+
redirect_uri: string;
13+
}
14+
15+
interface KakaoLoginResponse {
16+
access_token: string;
17+
refresh_token: string;
18+
user?: UserData;
19+
}
20+
21+
export function useKakaoLogin() {
22+
const { setTokens } = useTokenStore();
23+
const { setAuthenticated, setUser, clearAuthState } = useAuthStore();
24+
const { clearTokens } = useTokenStore();
25+
26+
// 카카오 로그인 페이지로 이동하는 함수
27+
const initiateKakaoLogin = () => {
28+
// 로그인 시작 전 기존 인증 정보 완전 제거
29+
clearTokens();
30+
clearAuthState();
31+
localStorage.removeItem("token-store");
32+
localStorage.removeItem("auth-store");
33+
34+
const KAKAO_AUTH_URL = "https://kauth.kakao.com/oauth/authorize";
35+
const KAKAO_CLIENT_ID = import.meta.env.VITE_KAKAO_CLIENT_ID;
36+
const REDIRECT_URI = getRedirectUri();
37+
38+
if (!KAKAO_CLIENT_ID) {
39+
console.error("KAKAO_CLIENT_ID가 설정되지 않았습니다.");
40+
alert("카카오 클라이언트 ID가 설정되지 않았습니다. 환경 변수를 확인해주세요.");
41+
return;
42+
}
43+
44+
const kakaoAuthUrl = `${KAKAO_AUTH_URL}?client_id=${KAKAO_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&prompt=login`;
45+
46+
// 카카오 로그인 페이지로 이동
47+
window.location.href = kakaoAuthUrl;
48+
};
49+
50+
const mutation = useMutation({
51+
mutationFn: async (data: KakaoLoginRequest) => {
52+
const response = await apiClient.post<KakaoLoginResponse>(
53+
API_ENDPOINTS.AUTH.KAKAO_LOGIN,
54+
data
55+
);
56+
57+
return response;
58+
},
59+
onSuccess: (response) => {
60+
if (response.success && response.data) {
61+
const { access_token, refresh_token, user } = response.data;
62+
63+
// 토큰 저장
64+
setTokens(access_token, refresh_token);
65+
66+
// 사용자 정보가 있으면 저장
67+
if (user) {
68+
setUser(user);
69+
}
70+
71+
// 인증 상태 업데이트
72+
setAuthenticated(true);
73+
}
74+
},
75+
onError: (error: Error) => {
76+
console.error("카카오 로그인 실패:", error);
77+
},
78+
});
79+
80+
return {
81+
...mutation,
82+
initiateKakaoLogin, // 카카오 로그인 시작 함수 추가
83+
};
84+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useMutation } from '@tanstack/react-query';
2+
import { useAuthStore } from '../../stores/useAuthStore';
3+
import { useTokenStore } from '../../stores/useTokenStore';
4+
import { apiClient } from '../core/client';
5+
import { API_ENDPOINTS } from '../core/types';
6+
7+
// 로그아웃
8+
export function useLogout() {
9+
const { clearAuthState } = useAuthStore();
10+
const { clearTokens } = useTokenStore();
11+
12+
return useMutation({
13+
mutationFn: async () => {
14+
try {
15+
// 서버에 로그아웃 요청
16+
await apiClient.post<void>(API_ENDPOINTS.AUTH.LOGOUT);
17+
} catch (error) {
18+
// 서버 요청 실패해도 로컬 정리는 진행
19+
console.error('서버 로그아웃 실패:', error);
20+
}
21+
},
22+
onMutate: () => {
23+
// mutation 시작 전에 먼저 정리 (가장 빠른 시점)
24+
clearTokens();
25+
clearAuthState();
26+
},
27+
onSettled: () => {
28+
// 한 번 더 확실하게 정리
29+
clearTokens();
30+
clearAuthState();
31+
32+
// localStorage 인증 관련만 삭제
33+
localStorage.removeItem('token-store');
34+
localStorage.removeItem('auth-store');
35+
sessionStorage.clear();
36+
37+
// 즉시 리로드 (모든 메모리 상태 초기화)
38+
setTimeout(() => {
39+
window.location.href = '/signIn';
40+
}, 0);
41+
},
42+
});
43+
}

0 commit comments

Comments
 (0)