Skip to content
Open
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
13 changes: 0 additions & 13 deletions apps/timo-web/api/axios.ts

This file was deleted.

15 changes: 15 additions & 0 deletions apps/timo-web/api/client/axios.ts
Original file line number Diff line number Diff line change
@@ -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,
});
Comment on lines +5 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

axios 인스턴스에 timeout이 설정되어 있지 않아요.

응답이 지연되거나 네트워크가 멈추면 요청이 무한정 대기하게 되고, 이 상태에서는 parseApiError가 실행될 기회조차 없어 사용자에게 로딩 상태만 남게 됩니다. 인스턴스 레벨에 기본 timeout을 설정해두면 이런 행잉을 방지할 수 있어요.

🛡️ 제안 수정
export const instance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
+ timeout: 10000,
});

작은 설정 한 줄로 사용자 경험이 확 달라질 수 있는 포인트라 놓치지 않았으면 해요!

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
});
export const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
timeout: 10000,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/timo-web/api/client/axios.ts` around lines 5 - 7, The axios instance
created in axios.ts is missing a default timeout, so requests can hang
indefinitely before parseApiError ever runs. Update the axios.create
configuration in instance to include a sensible timeout value at the instance
level, alongside baseURL, so all API calls fail fast on stalled network
responses.


instance.interceptors.response.use(
(response) => response,
(error) => {
// TODO: 추후 인증 방식 확정 후 status(401 등) 기반 사이드 이펙트(리다이렉트 등) 추가
return Promise.reject(parseApiError(error));
},
);
40 changes: 40 additions & 0 deletions apps/timo-web/api/error/api-error.ts
Original file line number Diff line number Diff line change
@@ -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: "",
});
}
Comment on lines +21 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

axios 에러인데 body가 스키마와 안 맞으면 실제 status/path 정보가 유실돼요.

error.response?.dataapiErrorSchema를 통과하지 못하면(예: 게이트웨이 500 HTML 응답, 백엔드가 다른 포맷으로 에러를 내려주는 경우) 바로 UNKNOWN_ERROR + status: 0 분기로 빠지는데, 이때 axios 에러 객체에는 이미 error.response.status, error.config?.url 같은 유용한 정보가 있음에도 버려지고 있습니다. 향후 401 리다이렉트 같은 상태 코드 기반 처리를 붙일 때(axios.ts의 TODO) 이 정보 유실이 바로 발목을 잡을 수 있어요.

🐛 제안 수정
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: error.response?.status ?? 0,
+     errorCode: "UNKNOWN_ERROR",
+     message: error.message,
+     path: error.config?.url ?? "",
+   });
  }

  return new ApiError({
    timestamp: new Date().toISOString(),
    status: 0,
    errorCode: "UNKNOWN_ERROR",
    message:
      error instanceof Error
        ? error.message
        : "알 수 없는 오류가 발생했습니다.",
    path: "",
  });
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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: "",
});
}
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: error.response?.status ?? 0,
errorCode: "UNKNOWN_ERROR",
message: error.message,
path: error.config?.url ?? "",
});
}
return new ApiError({
timestamp: new Date().toISOString(),
status: 0,
errorCode: "UNKNOWN_ERROR",
message:
error instanceof Error
? error.message
: "알 수 없는 오류가 발생했습니다.",
path: "",
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/timo-web/api/error/api-error.ts` around lines 21 - 40, parseApiError
currently drops useful axios metadata when error.response?.data fails
apiErrorSchema validation, causing status/path information to be lost. Update
parseApiError to preserve axios-derived fields from the axios.isAxiosError
branch by falling back to error.response.status and error.config?.url (or
equivalent request URL) when building the ApiError, while still using the
schema-parsed body when available. Keep the UNKNOWN_ERROR fallback only for
non-axios cases, and ensure ApiError construction in parseApiError always
carries the best available status/path context.

19 changes: 19 additions & 0 deletions apps/timo-web/api/schema/response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { z } from "zod";

export function apiResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

z.ZodTypeAny는 Zod 4에서 deprecated 되었습니다.

Zod 공식 마이그레이션 가이드에 따르면 z.ZodTypeAny는 제거되었고 z.ZodType을 대신 사용하도록 권장하고 있습니다. (참고: https://zod.dev/v4/changelog)

♻️ 제안 수정
-export function apiResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {
+export function apiResponseSchema<T extends z.ZodType>(dataSchema: T) {

지금 당장 동작에는 문제 없지만, 향후 zod 메이저 업그레이드 시 제거될 수 있는 API라 미리 정리해두면 좋을 것 같아요. 작은 수고로 미래의 골칫거리를 예방하는 셈이죠! 😄

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function apiResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {
export function apiResponseSchema<T extends z.ZodType>(dataSchema: T) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/timo-web/api/schema/response.ts` at line 3, `apiResponseSchema`에서 사용 중인
`z.ZodTypeAny`가 Zod 4에서 deprecated 되었으므로, 제네릭 타입 제약을 `z.ZodType`으로 바꿔 호환성을
맞춰주세요. 이 변경은 `apiResponseSchema` 시그니처에만 적용하면 되며, 관련 타입 사용처가 있다면 함께 정리해
`z.ZodTypeAny` 참조가 남지 않도록 확인하세요.

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<typeof apiErrorSchema>;
3 changes: 2 additions & 1 deletion apps/timo-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
2 changes: 1 addition & 1 deletion apps/timo-web/providers/QueryProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading