diff --git a/apps/timo-web/api/axios.ts b/apps/timo-web/api/axios.ts deleted file mode 100644 index 158e3d7..0000000 --- a/apps/timo-web/api/axios.ts +++ /dev/null @@ -1,13 +0,0 @@ -import axios from "axios"; - -export const instance = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, -}); - -instance.interceptors.response.use( - (response) => response, - (error) => { - // TODO: 추후 인증 방식 확정 후 error.response.status 기반 에러 핸들링 추가 - return Promise.reject(error); - }, -); diff --git a/apps/timo-web/api/client/axios.ts b/apps/timo-web/api/client/axios.ts new file mode 100644 index 0000000..f2d892a --- /dev/null +++ b/apps/timo-web/api/client/axios.ts @@ -0,0 +1,15 @@ +import axios from "axios"; + +import { parseApiError } from "@/api/error/api-error"; + +export const instance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, +}); + +instance.interceptors.response.use( + (response) => response, + (error) => { + // TODO: 추후 인증 방식 확정 후 status(401 등) 기반 사이드 이펙트(리다이렉트 등) 추가 + return Promise.reject(parseApiError(error)); + }, +); diff --git a/apps/timo-web/api/query-client.ts b/apps/timo-web/api/client/query-client.ts similarity index 100% rename from apps/timo-web/api/query-client.ts rename to apps/timo-web/api/client/query-client.ts diff --git a/apps/timo-web/api/error/api-error.ts b/apps/timo-web/api/error/api-error.ts new file mode 100644 index 0000000..7b87428 --- /dev/null +++ b/apps/timo-web/api/error/api-error.ts @@ -0,0 +1,40 @@ +import axios from "axios"; + +import { ApiErrorResponse, apiErrorSchema } from "@/api/schema/response"; + +export class ApiError extends Error { + readonly status: number; + readonly errorCode: string; + readonly path: string; + readonly timestamp: string; + + constructor(response: ApiErrorResponse) { + super(response.message); + this.name = "ApiError"; + this.status = response.status; + this.errorCode = response.errorCode; + this.path = response.path; + this.timestamp = response.timestamp; + } +} + +export function parseApiError(error: unknown): ApiError { + if (axios.isAxiosError(error)) { + const parsed = apiErrorSchema.safeParse(error.response?.data); + + if (parsed.success) { + return new ApiError(parsed.data); + } + } + + return new ApiError({ + timestamp: new Date().toISOString(), + status: 0, + errorCode: "UNKNOWN_ERROR", + message: + error instanceof Error + ? error.message + : "알 수 없는 오류가 발생했습니다.", + path: "", + }); +} diff --git a/apps/timo-web/api/schema/response.ts b/apps/timo-web/api/schema/response.ts new file mode 100644 index 0000000..985f1bc --- /dev/null +++ b/apps/timo-web/api/schema/response.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +export function apiResponseSchema(dataSchema: T) { + return z.object({ + status: z.number(), + message: z.string(), + data: dataSchema, + }); +} + +export const apiErrorSchema = z.object({ + timestamp: z.string(), + status: z.number(), + errorCode: z.string(), + message: z.string(), + path: z.string(), +}); + +export type ApiErrorResponse = z.infer; diff --git a/apps/timo-web/package.json b/apps/timo-web/package.json index 52c33af..e8bf086 100644 --- a/apps/timo-web/package.json +++ b/apps/timo-web/package.json @@ -18,7 +18,8 @@ "axios": "^1.18.1", "next": "16.2.0", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "zod": "^4.4.3" }, "devDependencies": { "@repo/eslint-config": "workspace:*", diff --git a/apps/timo-web/providers/QueryProvider.tsx b/apps/timo-web/providers/QueryProvider.tsx index 430398a..8c8dc38 100644 --- a/apps/timo-web/providers/QueryProvider.tsx +++ b/apps/timo-web/providers/QueryProvider.tsx @@ -3,7 +3,7 @@ import { QueryClientProvider } from "@tanstack/react-query"; import dynamic from "next/dynamic"; -import { queryClient } from "@/api/query-client"; +import { queryClient } from "@/api/client/query-client"; const ReactQueryDevtools = process.env.NODE_ENV === "development" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 387cd7a..4f3610c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: react-dom: specifier: ^19.2.0 version: 19.2.0(react@19.2.0) + zod: + specifier: ^4.4.3 + version: 4.4.3 devDependencies: '@repo/eslint-config': specifier: workspace:* @@ -4199,6 +4202,9 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + snapshots: '@adobe/css-tools@4.5.0': {} @@ -8326,3 +8332,5 @@ snapshots: yocto-queue@0.1.0: {} yoctocolors-cjs@2.1.3: {} + + zod@4.4.3: {}