Skip to content

Commit 9f581a7

Browse files
committed
fix: 더나은 오류 핸들
1 parent f6216d8 commit 9f581a7

File tree

4 files changed

+97
-29
lines changed

4 files changed

+97
-29
lines changed

src/services/authService.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
RefreshTokenResponse,
44
LoginRequest,
55
User,
6+
ApiResponse,
67
} from "../types/auth";
78

89
const API_BASE_URL =
@@ -19,8 +20,22 @@ export class AuthService {
1920
body: JSON.stringify(credentials),
2021
});
2122

22-
if (!res.ok) throw new Error("로그인 실패");
23-
return res.json();
23+
const data: ApiResponse<LoginResponse> = await res.json();
24+
25+
if (!data.success || !data.data) {
26+
const errorMsg = data.error?.message || "로그인 실패";
27+
const fields = data.error?.fields;
28+
29+
// 필드별 에러가 있으면 첫 번째 필드 에러 메시지 사용
30+
if (fields) {
31+
const firstFieldError = Object.values(fields)[0];
32+
throw new Error(firstFieldError || errorMsg);
33+
}
34+
35+
throw new Error(errorMsg);
36+
}
37+
38+
return data.data;
2439
}
2540

2641
static async refreshToken(): Promise<RefreshTokenResponse> {
@@ -30,30 +45,47 @@ export class AuthService {
3045
credentials: "include", // 🍪 리프레시 토큰은 쿠키에 있음
3146
});
3247

33-
if (!res.ok) throw new Error("토큰 갱신 실패");
34-
return res.json(); // { accessToken: string, user: User }
48+
const data: ApiResponse<RefreshTokenResponse> = await res.json();
49+
50+
if (!data.success || !data.data) {
51+
throw new Error(data.error?.message || "토큰 갱신 실패");
52+
}
53+
54+
return data.data;
3555
}
3656

3757
static async logout(accessToken: string): Promise<void> {
38-
await fetch(`${API_BASE_URL}/api/auth/logout`, {
58+
const res = await fetch(`${API_BASE_URL}/api/auth/logout`, {
3959
method: "POST",
4060
headers: {
4161
Authorization: `Bearer ${accessToken}`,
4262
"Content-Type": "application/json",
4363
},
4464
credentials: "include",
4565
});
66+
67+
const data: ApiResponse = await res.json();
68+
69+
if (!data.success && data.error) {
70+
throw new Error(data.error.message || "로그아웃 실패");
71+
}
4672
}
4773

4874
static async signOut(accessToken: string): Promise<void> {
49-
await fetch(`${API_BASE_URL}/api/auth/sign-out`, {
75+
const res = await fetch(`${API_BASE_URL}/api/auth/sign-out`, {
5076
method: "DELETE",
5177
headers: {
5278
Authorization: `Bearer ${accessToken}`,
5379
"Content-Type": "application/json",
5480
},
5581
credentials: "include",
5682
});
83+
84+
const data: ApiResponse = await res.json();
85+
86+
if (!data.success && data.error) {
87+
throw new Error(data.error.message || "계정 삭제 실패");
88+
}
5789
}
5890
}
5991

src/services/projectService.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,39 @@
11
// src/services/projectService.ts
22
import { useProjectStore } from "../stores/projectStore";
3+
import { TokenStorage } from "./authService";
4+
import type { ApiResponse } from "../types/auth";
35

46
const API_BASE_URL =
57
import.meta.env.VITE_API_BASE_URL || "http://localhost:8080";
68

79
export async function createProject(name: string, description: string) {
8-
const accessToken = localStorage.getItem("accessToken"); // ✅ 토큰 가져오기
10+
const accessToken = TokenStorage.getAccessToken();
911

1012
const response = await fetch(`${API_BASE_URL}/api/projects`, {
1113
method: "POST",
1214
headers: {
1315
"Content-Type": "application/json",
14-
...(accessToken && { Authorization: `Bearer ${accessToken}` }), // ✅ 토큰 헤더 추가
16+
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
1517
},
1618
body: JSON.stringify({ name, description }),
17-
credentials: "include", // refreshToken 쿠키도 함께 보냄
19+
credentials: "include",
1820
});
1921

20-
if (!response.ok) {
21-
throw new Error("프로젝트 생성 실패");
22-
}
22+
const data: ApiResponse = await response.json();
23+
24+
if (!data.success || !data.data) {
25+
const errorMsg = data.error?.message || "프로젝트 생성 실패";
26+
const fields = data.error?.fields;
2327

24-
const data = await response.json();
25-
if (data.success && data.data) {
26-
useProjectStore.getState().loadProject(data.data); // store에 저장
27-
return data.data;
28+
// 필드별 에러가 있으면 사용자에게 친절하게 표시
29+
if (fields) {
30+
const fieldErrors = Object.values(fields).join(", ");
31+
throw new Error(fieldErrors || errorMsg);
32+
}
33+
34+
throw new Error(errorMsg);
2835
}
29-
throw new Error(data.error?.message || "프로젝트 생성 실패");
36+
37+
useProjectStore.getState().loadProject(data.data);
38+
return data.data;
3039
}

src/types/auth.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,16 @@ export interface AuthState {
4646

4747
// API 에러 타입
4848
export interface ApiError {
49+
code: number;
4950
message: string;
50-
status: number;
51-
code?: string;
51+
fields?: Record<string, string>;
52+
}
53+
54+
// API 공통 응답 타입
55+
export interface ApiResponse<T = any> {
56+
success: boolean;
57+
data: T | null;
58+
error: ApiError | null;
5259
}
5360

5461
// 로그인 요청 타입 (테스트용)

src/utils/apiClients.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { TokenStorage } from "../services/authService";
2+
import type { ApiResponse } from "../types/auth";
23

34
const API_BASE_URL =
45
import.meta.env.VITE_API_BASE_URL || "http://localhost:8080";
@@ -47,7 +48,15 @@ export const apiFetch = async (
4748
throw new Error("로그인이 만료되었습니다.");
4849
}
4950

50-
const { accessToken: newAccessToken } = await refreshRes.json();
51+
const refreshData: ApiResponse = await refreshRes.json();
52+
53+
if (!refreshData.success || !refreshData.data) {
54+
console.error("❌ refresh token 갱신 실패");
55+
TokenStorage.clearAll();
56+
throw new Error(refreshData.error?.message || "로그인이 만료되었습니다.");
57+
}
58+
59+
const newAccessToken = refreshData.data.accessToken;
5160
TokenStorage.saveTokens(newAccessToken, TokenStorage.getRefreshToken() || "");
5261
accessToken = newAccessToken;
5362

@@ -64,12 +73,6 @@ export const apiFetch = async (
6473
}
6574

6675
// 3. 최종 응답 처리 (빈 응답도 대응)
67-
if (!res.ok) {
68-
const text = await res.text();
69-
throw new Error(text || "API 요청 실패");
70-
}
71-
72-
// 🔁 Content-Length가 0일 수도 있음
7376
const contentType = res.headers.get("Content-Type");
7477
if (
7578
res.status === 204 ||
@@ -79,7 +82,22 @@ export const apiFetch = async (
7982
return null;
8083
}
8184

82-
return res.json();
85+
const data: ApiResponse = await res.json();
86+
87+
// 4. success: false인 경우 에러 처리
88+
if (!data.success && data.error) {
89+
const errorMsg = data.error.message || "API 요청 실패";
90+
const fields = data.error.fields;
91+
92+
if (fields) {
93+
const fieldErrors = Object.values(fields).join(", ");
94+
throw new Error(fieldErrors || errorMsg);
95+
}
96+
97+
throw new Error(errorMsg);
98+
}
99+
100+
return data.data || data;
83101
};
84102

85103
export const logout = async () => {
@@ -88,7 +106,9 @@ export const logout = async () => {
88106
credentials: "include",
89107
});
90108

91-
if (!res.ok) {
92-
throw new Error("로그아웃 실패");
109+
const data: ApiResponse = await res.json();
110+
111+
if (!data.success && data.error) {
112+
throw new Error(data.error.message || "로그아웃 실패");
93113
}
94114
};

0 commit comments

Comments
 (0)