From 35a33ce488766ab9bbf03920eef01134c763b14c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9C=A4=EC=84=9C?= Date: Sat, 30 May 2026 17:27:15 +0900 Subject: [PATCH] =?UTF-8?q?Fix:=20#78=20=20=EB=A6=AC=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EB=A0=89=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jobdri/src/lib/api/client.ts | 86 +++++++++++++++++++++++++++++++ jobdri/src/lib/api/credit.ts | 17 +++--- jobdri/src/lib/api/jobPostings.ts | 66 +++--------------------- jobdri/src/lib/api/mockApplies.ts | 38 +------------- jobdri/src/lib/api/questions.ts | 28 +--------- jobdri/src/lib/api/result.ts | 24 +-------- 6 files changed, 107 insertions(+), 152 deletions(-) create mode 100644 jobdri/src/lib/api/client.ts diff --git a/jobdri/src/lib/api/client.ts b/jobdri/src/lib/api/client.ts new file mode 100644 index 0000000..cd66836 --- /dev/null +++ b/jobdri/src/lib/api/client.ts @@ -0,0 +1,86 @@ +import { clearAuthTokens, getAuthHeaders, API_BASE_URL } from "@/lib/auth"; +import { ROUTES } from "@/constants/routes"; + +export { API_BASE_URL, getAuthHeaders }; + +export interface ApiResponse { + isSuccess: boolean; + code: string; + message: string; + result: T | null; + error: string | null; +} + +export class UnauthorizedError extends Error { + constructor() { + super("인증이 만료되었습니다. 다시 로그인해주세요."); + this.name = "UnauthorizedError"; + } +} + +export function handleUnauthorized(): never { + clearAuthTokens(); + + if (typeof window !== "undefined") { + const redirectPath = encodeURIComponent(window.location.pathname); + window.location.replace(`${ROUTES.LOGIN}?redirect=${redirectPath}`); + } + + throw new UnauthorizedError(); +} + +export async function parseApiResponse( + response: Response, + fallbackMessage: string, +): Promise { + if (response.status === 401) { + handleUnauthorized(); + } + + let data: ApiResponse | null = null; + + try { + data = (await response.json()) as ApiResponse; + } catch { + throw new Error(`${fallbackMessage} 응답을 확인할 수 없습니다.`); + } + + if (!response.ok || !data.isSuccess || !data.result) { + throw new Error(data?.error || data?.message || fallbackMessage); + } + + return data.result; +} + +export async function parseApiResponseAllowNull( + response: Response, + fallbackMessage: string, +): Promise { + if (response.status === 401) { + handleUnauthorized(); + } + + const responseText = await response.text(); + + if (!responseText.trim()) { + if (!response.ok) { + throw new Error(fallbackMessage); + } + + return null; + } + + let data: ApiResponse | null = null; + + try { + data = JSON.parse(responseText) as ApiResponse; + } catch { + throw new Error(`${fallbackMessage} 응답을 확인할 수 없습니다.`); + } + + if (!response.ok || !data.isSuccess) { + throw new Error(data?.error || data?.message || fallbackMessage); + } + + return data.result; +} diff --git a/jobdri/src/lib/api/credit.ts b/jobdri/src/lib/api/credit.ts index 7932663..b33ea62 100644 --- a/jobdri/src/lib/api/credit.ts +++ b/jobdri/src/lib/api/credit.ts @@ -1,4 +1,4 @@ -import { getAuthHeaders } from "../auth"; +import { getAuthHeaders, handleUnauthorized } from "@/lib/api/client"; const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; @@ -19,11 +19,16 @@ interface ApiResponse { error: string | null; } +function checkResponse(response: Response, fallbackMessage: string): void { + if (response.status === 401) handleUnauthorized(); + if (!response.ok) throw new Error(fallbackMessage); +} + export async function fetchCreditBalance(): Promise { const response = await fetch(`${BASE_URL}/api/payments/credits/me`, { headers: getAuthHeaders(), }); - if (!response.ok) throw new Error("크레딧 잔액 조회에 실패했습니다."); + checkResponse(response, "크레딧 잔액 조회에 실패했습니다."); const { result }: ApiResponse<{ creditBalance: number }> = await response.json(); return result.creditBalance; @@ -38,7 +43,7 @@ export async function fetchCreditTransactions( const response = await fetch(url.toString(), { headers: getAuthHeaders(), }); - if (!response.ok) throw new Error("크레딧 거래 내역 조회에 실패했습니다."); + checkResponse(response, "크레딧 거래 내역 조회에 실패했습니다."); const { result }: ApiResponse = await response.json(); return result; } @@ -57,7 +62,7 @@ export async function fetchCreditPlans(): Promise { const response = await fetch(`${BASE_URL}/api/payments/plans`, { headers: getAuthHeaders(), }); - if (!response.ok) throw new Error("크레딧 플랜 조회에 실패했습니다."); + checkResponse(response, "크레딧 플랜 조회에 실패했습니다."); const { result }: ApiResponse = await response.json(); return result; } @@ -83,7 +88,7 @@ export async function preparePurchase( }, body: JSON.stringify({ planCode }), }); - if (!response.ok) throw new Error("결제 준비에 실패했습니다."); + checkResponse(response, "결제 준비에 실패했습니다."); const { result }: ApiResponse = await response.json(); return result; } @@ -101,5 +106,5 @@ export async function confirmPurchase( }, body: JSON.stringify({ paymentKey, orderId, amount }), }); - if (!response.ok) throw new Error("결제 승인에 실패했습니다."); + checkResponse(response, "결제 승인에 실패했습니다."); } diff --git a/jobdri/src/lib/api/jobPostings.ts b/jobdri/src/lib/api/jobPostings.ts index 55ca5a0..fdead38 100644 --- a/jobdri/src/lib/api/jobPostings.ts +++ b/jobdri/src/lib/api/jobPostings.ts @@ -1,12 +1,9 @@ -import { API_BASE_URL, AUTH_STORAGE_KEYS } from "@/lib/auth"; - -interface ApiResponse { - isSuccess: boolean; - code: string; - message: string; - result: T | null; - error: string | null; -} +import { + API_BASE_URL, + getAuthHeaders, + parseApiResponse, + parseApiResponseAllowNull, +} from "@/lib/api/client"; export interface JobPostingExtracted { companyName: string; @@ -117,14 +114,6 @@ interface TimedRequestInit extends RequestInit { const DEFAULT_REQUEST_TIMEOUT_MS = 30000; -function getAuthHeaders(): Record { - const token = - typeof window !== "undefined" - ? window.localStorage.getItem(AUTH_STORAGE_KEYS.accessToken) - : null; - - return token ? { Authorization: `Bearer ${token}` } : {}; -} function getImageContentType(file: File) { if (file.type) { @@ -202,49 +191,6 @@ async function fetchWithTimeout( } } -async function parseApiResponse(response: Response, fallbackMessage: string) { - let data: ApiResponse | null = null; - - try { - data = (await response.json()) as ApiResponse; - } catch { - throw new Error(`${fallbackMessage} 응답을 확인할 수 없습니다.`); - } - - if (!response.ok || !data.isSuccess || !data.result) { - throw new Error(data?.error || data?.message || fallbackMessage); - } - - return data.result; -} - -async function parseApiResponseAllowNull( - response: Response, - fallbackMessage: string, -) { - let data: ApiResponse | null = null; - const responseText = await response.text(); - - if (!responseText.trim()) { - if (!response.ok) { - throw new Error(fallbackMessage); - } - - return null; - } - - try { - data = JSON.parse(responseText) as ApiResponse; - } catch { - throw new Error(`${fallbackMessage} 응답을 확인할 수 없습니다.`); - } - - if (!response.ok || !data.isSuccess) { - throw new Error(data?.error || data?.message || fallbackMessage); - } - - return data.result; -} async function postJobPosting( path: string, diff --git a/jobdri/src/lib/api/mockApplies.ts b/jobdri/src/lib/api/mockApplies.ts index 85b77a2..2c7f83f 100644 --- a/jobdri/src/lib/api/mockApplies.ts +++ b/jobdri/src/lib/api/mockApplies.ts @@ -1,12 +1,4 @@ -import { API_BASE_URL, AUTH_STORAGE_KEYS } from "@/lib/auth"; - -interface ApiResponse { - isSuccess: boolean; - code: string; - message: string; - result: T | null; - error: string | null; -} +import { API_BASE_URL, getAuthHeaders, parseApiResponse } from "@/lib/api/client"; export type JobPostingApplyType = "MOCK" | "ACTUAL"; export type MockApplyProgressStatus = @@ -58,34 +50,6 @@ export interface MockApplyHomeList { export const APPLY_TYPE_STORAGE_KEY = "jobdri.applyType"; const MOCK_APPLY_RESUME_STORAGE_KEY = "jobdri.mockApplyResumeRecords"; -function getAuthHeaders(): Record { - const token = - typeof window !== "undefined" - ? window.localStorage.getItem(AUTH_STORAGE_KEYS.accessToken) - : null; - - return token ? { Authorization: `Bearer ${token}` } : {}; -} - -async function parseApiResponse( - response: Response, - fallbackMessage: string, -) { - let data: ApiResponse | null = null; - - try { - data = (await response.json()) as ApiResponse; - } catch { - throw new Error(`${fallbackMessage} 응답을 확인할 수 없습니다.`); - } - - if (!response.ok || !data.isSuccess || !data.result) { - throw new Error(data?.error || data?.message || fallbackMessage); - } - - return data.result; -} - async function postMockApplyFromJobPosting( path: string, jobPostingId: number, diff --git a/jobdri/src/lib/api/questions.ts b/jobdri/src/lib/api/questions.ts index 42f8c0f..4d6e1ae 100644 --- a/jobdri/src/lib/api/questions.ts +++ b/jobdri/src/lib/api/questions.ts @@ -1,4 +1,4 @@ -import { API_BASE_URL, getAuthHeaders } from "@/lib/auth"; +import { API_BASE_URL, getAuthHeaders, parseApiResponse } from "@/lib/api/client"; export interface QuestionItem { id: string; @@ -20,14 +20,6 @@ interface QuestionApiItem { answer?: string; } -interface ApiResponse { - isSuccess: boolean; - code: string; - message: string; - result: T | null; - error: string | null; -} - interface SelectedQuestionsApiResponse { mockApplyId: number; status: string; @@ -39,24 +31,6 @@ export interface AnswerItem { answer: string; } -async function parseApiResponse( - response: Response, - fallbackMessage: string, -) { - let data: ApiResponse | null = null; - - try { - data = (await response.json()) as ApiResponse; - } catch { - throw new Error(`${fallbackMessage} 응답을 확인할 수 없습니다.`); - } - - if (!response.ok || !data.isSuccess) { - throw new Error(data?.error || data?.message || fallbackMessage); - } - - return data.result; -} export async function fetchQuestions( mockApplyId: number, diff --git a/jobdri/src/lib/api/result.ts b/jobdri/src/lib/api/result.ts index 64a96a2..653141e 100644 --- a/jobdri/src/lib/api/result.ts +++ b/jobdri/src/lib/api/result.ts @@ -1,4 +1,4 @@ -import { API_BASE_URL, getAuthHeaders } from "@/lib/auth"; +import { API_BASE_URL, getAuthHeaders, parseApiResponse as parseApiResponseBase } from "@/lib/api/client"; export class CreditInsufficientError extends Error { constructor() { @@ -45,14 +45,6 @@ export interface AnalysisResult { questions: AnalysisQuestion[]; } -interface ApiResponse { - isSuccess: boolean; - code: string; - message: string; - result: T | null; - error: string | null; -} - async function parseApiResponse( response: Response, fallbackMessage: string, @@ -61,19 +53,7 @@ async function parseApiResponse( throw new CreditInsufficientError(); } - let data: ApiResponse | null = null; - - try { - data = (await response.json()) as ApiResponse; - } catch { - throw new Error(`${fallbackMessage} 응답을 확인할 수 없습니다.`); - } - - if (!response.ok || !data.isSuccess || !data.result) { - throw new Error(data?.error || data?.message || fallbackMessage); - } - - return data.result; + return parseApiResponseBase(response, fallbackMessage); } export async function fetchSequence(