Skip to content

Conversation

@Bangdayeon
Copy link
Member

관련 이슈

PR 설명

safeFetch 정리

  • request 검사 및 에러 처리 사용을 더 쉽게 하도록 코드 추가 및 수정

error/

  • errors.ts
    • 기존 safeFetch의 에러 관련 로직(FetchError, TimeoutError, ParseError)을 errors.ts로 분리, 각 로직에 대한 생성함수 작성
  • handleApiError.ts
    • 에러를 HTTP Response로 변환하는 공통 핸들러

request/ : 요청 중 에러를 다룸

  • parseJsonBody.ts

    • NextRequest에서 JSON body를 안전하게 파싱 (요청 형식 오류 감지)
    • <T>로 기대하는 request 타입을 전달
  • requestError.ts

    • zodError를 감싸는 도메인 에러 (에러 분류 래퍼)
  • validateRequest.ts

    • 파싱된 payload를 Zod 스키마로 검증 (도메인/스키마 전용)
    • <T>로 기대하는 zod 스키마 타입을 전달
  • zodError

    • zodError를 HTTP Response로 변환 (API 응답 포맷 표준화)
[NextRequest]
     ↓
parseJsonBody
 (JSON 파싱)
     ↓
validateRequest
 (Zod 검증)
     ↓
RequestValidationError
 (에러 타입 명확화)
     ↓
handleApiError
     ↓
zodErrorToResponse
 (HTTP Response 생성)

임시 회원가입 페이지 및 로직 작성

(route)/signup/SignupPage.tsx

  • 회원가입 페이지
  • 폼 작성

useSignup.ts

  • const signup...
    • SignupResponse의 형태로 결과를 safeFetch 받음
  • useSignupSubmit
    • useMutation으로 회원가입 비동기 작업 진행
    • 결과 UI는 Toast로 임의 처리

useSignupForm.ts

  • zodResolver, react-hook-form을 사용하여 입력값 검증 및 상태 관리 수행
    • resolver : 폼 검증 로직을 SignupSchema와 연동해 자동 검증
    • mode 입력값이 변경될때마다 검증 수행
    • defaultValues : 폼 초기 렌더링 기본 입력값 정의

authApi.ts

  • 회원가입에서 사용하는 데이터(Email, Password) 스키마 정의
  • 회원가입 request, response 타입 정의

공유사항

  • hooks/util/api/index.ts의 네가지를 import하여 api에 사용할 수 있습니다.
  • signup/route.ts에서 사용한 예시를 확인할 수 있습니다.
    • body = await parseJsonBody(NextRequest) : 요청 바디를 안전하게 파싱
    • data = validateRequest(RequestSchema, 검사한 body) : 파싱한 바디가 스키마형식에 맞는지 검증
    • result = safeFetch<Response타입> ( ... ) : 검증한 request data로 safeFetch 진행하여 정의된 Response타입에 맞는 검증된 result를 받아옴
    • handleApiError(err) : 에러 발생 시, HTTP Response로 변환하여 리턴

@Bangdayeon Bangdayeon self-assigned this Jan 11, 2026
@Bangdayeon Bangdayeon linked an issue Jan 11, 2026 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Jan 11, 2026

Walkthrough

이 PR은 이메일/비밀번호 기반 일반 회원가입 기능을 추가합니다. 클라이언트 측 SignupPage, react-hook-form + Zod 기반 useSignupForm 훅, 제출 처리용 useSignupSubmit 훅, 서버측 /api/member/signup POST 라우트와 타입·검증 스키마를 도입했습니다. 또한 안전한 API 호출용 safeFetch 경로 및 내부 오류 타입을 정리하고(에러 클래스 중앙화, parseJsonBody·validateRequest 등 유틸 추가), 레이아웃에서 /signup 경로에 SideNavigation을 숨기며 og-thumbnail API 라우트는 제거합니다.

Possibly related PRs

  • 서버 전용 safeFetch 유틸 구현 #274: safeFetch와 관련된 에러 클래스(FetchError, TimeoutError, ParseError) 및 팩토리 함수들을 리팩토링·중앙화한 변경과 직접적으로 연결됩니다.
  • 링크 관련 api 함수 구현 #277 #278: src/apis/linkApi.ts (및 chatApi 관련)에서 safeFetch의 임포트 경로를 동일하게 변경한 코드 수준 연관성이 있습니다.
  • 구글 로그인 추가 #293: layout-client.tsx와 LandingPage의 네비게이션 표시/라우팅 조건 변경을 함께 다루어 UI 라우팅 로직과 연동됩니다.
🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning og-thumbnail 라우트 제거와 Input 컴포넌트의 value prop을 선택사항으로 변경한 부분이 연결된 이슈의 범위를 벗어나 보입니다. og-thumbnail 라우트 제거와 Input 컴포넌트 수정이 회원가입 기능 구현과 무관하므로 별도의 PR로 분리하거나 관련성을 설명해주세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 safeFetch 정리와 회원가입 기능 추가라는 두 가지 주요 변경사항을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 요구되는 템플릿의 모든 필수 섹션을 포함하고 있으며, safeFetch 정리 및 회원가입 기능에 대한 상세한 설명을 제공합니다.
Linked Issues check ✅ Passed 연결된 이슈 #336의 목표인 '이메일과 비밀번호로 회원가입하는 로직 구현'과 'Zod 스키마를 활용한 요청 데이터 검증'이 모두 충족되었습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/util/api/fetch/safeFetch.ts (1)

82-97: res.json() 실패 후 res.text() 호출은 실패합니다.

Line 83에서 res.json()을 호출하면 응답 본문 스트림이 소비됩니다. JSON 파싱이 실패한 후 line 89에서 res.text()를 호출해도 스트림이 이미 소비되어 빈 문자열이 반환되거나 에러가 발생합니다.

🐛 본문을 먼저 텍스트로 읽은 후 JSON 파싱 제안
    // JSON 파싱
    try {
-     const json = (await res.json()) as T;
+     const text = await res.text();
+     const json = JSON.parse(text) as T;
      return json;
    } catch (err) {
      const msg = err instanceof Error ? err.message : 'Unknown JSON parse error';
-     let raw: string | undefined;
-     try {
-       raw = await res.text();
-     } catch {
-       raw = undefined;
-     }
      throw createParseError(
        `Failed to parse JSON response: ${msg}`,
-       raw?.slice(0, maxErrorBodyBytes)
+       text?.slice(0, maxErrorBodyBytes)
      );
    }

이 방식은 텍스트를 먼저 읽고 JSON.parse로 파싱하므로, 실패 시에도 원본 텍스트를 에러에 포함할 수 있습니다. 단, 변수 스코프 조정이 필요합니다.

🤖 Fix all issues with AI agents
In @src/app/(route)/signup/SignupPage.tsx:
- Around line 20-48: The Input components for email and password use a mistyped
Tailwind class and a boolean short-circuit that can pass false into className;
change the className expressions on the inputs (where register('email') and
register('password') are used) to use a ternary so they return undefined when no
error, and fix the Tailwind class from "border-red500" to "border-red-500" (also
update "focus:border-red-500" if present) so the error styling applies
correctly.
- Around line 1-60: The password Input in the SignupPage component is missing
type="password", which exposes the password in plain text; update the Input used
for register('password') in the SignupPage function to include type="password"
(keeping autoComplete="new-password" and other props) so the entered password is
masked in the UI.

In @src/app/api/member/signup/route.ts:
- Around line 1-22: The BASE_URL constant is built from
process.env.NEXT_PUBLIC_BASE_API_URL which may be undefined; add a module-level
guard that validates NEXT_PUBLIC_BASE_API_URL on import (before POST runs) and
throw or return a clear error if it's missing, then construct BASE_URL only
after the check; update the POST handler to rely on the validated BASE_URL
(symbols: BASE_URL, POST) so requests are never made to "undefined/..." and
errors surface early and consistently with other API modules.

In @src/components/basics/Input/Input.tsx:
- Line 10: The Input component made its value prop optional but still passes
value={value} to the native input, risking React controlled/uncontrolled
warnings; fix by ensuring the rendered <input> always receives a string (e.g.,
use value ?? '' or default the prop to '' in the Input component) so the
component remains controlled, or alternatively revert value to required in the
InputProps type; update the code paths in the Input component where value is
forwarded (the value prop and the place using value={value}) to apply the chosen
fix.

In @src/hooks/util/api/error/handleApiError.ts:
- Around line 21-25: The current response includes upstream response body via
"detail: err.body", which can leak sensitive info; update the handleApiError
logic to stop returning err.body to clients — instead log the full err.body
internally (use your existing logger) and return a generic, non-sensitive detail
message (or omit detail) in the API response; if you need dev visibility, gate
returning err.body behind a clear environment flag (e.g., only include it when
NODE_ENV === 'development') so production never exposes upstream bodies.

In @src/hooks/util/api/request/parseJsonBody.ts:
- Around line 1-13: parseJsonBody currently throws a plain Error and is publicly
exported which risks client-side imports; change it to throw the
project-standard custom error (e.g., ParseError or FetchError used by other API
utils) inside parseJsonBody<T>(req: NextRequest) so caller can distinguish
INVALID_JSON_BODY, and mark the function as server-only by either adding a clear
comment/annotation and moving/removing it from the public barrel in
src/hooks/util/api/index.ts so it is not re-exported to client bundles; do not
use the { cause } option here because the TS target is ES2017.

In @src/types/api/authApi.ts:
- Around line 5-14: SignupSchema and SignupRequestSchema use different
validation rules causing potential mismatches; consolidate validations by
extracting shared base schemas (e.g., BaseEmailSchema, BasePasswordSchema) and
use those in both SignupSchema (frontend) and SignupRequestSchema (backend),
then apply frontend-only custom error messages or trims on top of the base
schemas (referencing Email, Password, SignupSchema, SignupRequestSchema) so the
underlying rules remain identical across client and server.
🧹 Nitpick comments (9)
src/hooks/util/zodError.ts (1)

9-22: LGTM! Zod 에러를 HTTP Response로 변환하는 로직이 적절합니다.

함수 시그니처가 400 | 422로 제한되어 있어 타입 안전성이 보장되며, error.flatten()을 사용한 에러 구조화도 올바릅니다.

📝 선택사항: JSDoc 추가로 문서화 개선
+/**
+ * Zod 검증 에러를 표준화된 HTTP Response로 변환합니다.
+ * @param error - Zod 검증 에러 객체
+ * @param status - HTTP 상태 코드 (400 또는 422, 기본값: 400)
+ * @returns 검증 에러 정보를 포함한 JSON Response
+ */
 export function zodErrorToResponse(error: ZodError, status: 400 | 422 = 400) {
src/types/api/authApi.ts (1)

17-27: 선택사항: LoginSchema와 SignupSchema 중복 제거 고려

LoginSchemaSignupSchema가 현재 동일한 구조(email, password)를 가지고 있습니다. 향후 회원가입 시 추가 필드(예: 이름, 약관 동의 등)가 필요한 경우를 대비해 분리된 것이 아니라면, 중복을 제거하는 것을 고려해볼 수 있습니다.

♻️ 중복 제거 방안
-export const LoginSchema = z.object({
+export const AuthCredentialsSchema = z.object({
   email: Email,
   password: Password,
 });
-export type LoginFormValues = z.infer<typeof LoginSchema>;
+export type LoginFormValues = z.infer<typeof AuthCredentialsSchema>;

-export const SignupSchema = z.object({
-  email: Email,
-  password: Password,
-});
-export type SignupFormValues = z.infer<typeof SignupSchema>;
+export type SignupFormValues = z.infer<typeof AuthCredentialsSchema>;

단, 향후 회원가입에만 필요한 추가 필드가 예상된다면 현재 구조를 유지하는 것이 더 나을 수 있습니다.

src/hooks/util/api/error/errors.ts (1)

1-16: 에러 중앙화 방향 좋습니다. 다만 contentType는 “optional + null”이 섞여 타입이 애매해요.

지금은 contentType?: string | null인데, 생성자에서 항상 null로 세팅하니 contentType: string | null(non-optional)로 두는 편이 사용처가 단순해집니다.

제안 diff
 export class FetchError extends Error {
   status?: number;
   body?: string;
-  contentType?: string | null;
+  contentType: string | null;

   constructor(
     message: string,
     opts?: { status?: number; body?: string; contentType?: string | null }
   ) {
     super(message);
     this.name = 'FetchError';
     this.status = opts?.status;
     this.body = opts?.body;
     this.contentType = opts?.contentType ?? null;
   }
 }
src/app/(route)/signup/page.tsx (1)

1-5: 동작은 문제없고, 라우트 wrapper로 분리한 것도 깔끔합니다.

(옵션) 함수명을 Page로 두면 컴포넌트/린트 관점에서 더 관례적입니다.

제안 diff
 import SignupPage from './SignupPage';

-export default function page() {
+export default function Page() {
   return <SignupPage />;
 }
src/hooks/util/api/request/validateRequest.ts (1)

1-17: @serverOnly를 실제로 강제할지 결정 필요 (Next.js) + Zod 타입 확인 권장

주석상 server-only 의도라면, 실수로 클라이언트에서 import 되는 걸 막기 위해 import 'server-only'; 같은 강제 장치를 추가하는 편이 안전합니다. 또한 Zod v4.1.13에서 ZodSchema 타입 export/권장 타입이 맞는지 확인해 주세요.

가능한 수정안
+import 'server-only';
-import { ZodSchema } from 'zod';
+import type { ZodSchema } from 'zod';
 import { RequestValidationError } from './requestError';
src/app/(route)/signup/SignupPage.tsx (1)

17-55: 제출 중 disabled 조건이 mutation.isPending와 싱크가 안 맞을 수 있음

isSubmitting은 handler가 sync로 끝나면 빨리 false가 될 수 있어, 요청이 진행 중인데 버튼이 다시 활성화될 수 있습니다. useSignupSubmit()이 pending 상태도 함께 노출해서(disabled에 반영) UX를 맞추는 걸 권장합니다.

src/hooks/util/api/request/requestError.ts (1)

1-11: Error 상속 시 instanceof 신뢰도를 위해 prototype 보정 권장

환경에 따라 err instanceof RequestValidationError가 깨질 수 있어, 생성자에서 prototype을 보정해 두는 패턴을 권장합니다.

수정안
 export class RequestValidationError extends Error {
   readonly zodError: ZodError;

   constructor(error: ZodError) {
     super('Request validation failed');
+    Object.setPrototypeOf(this, new.target.prototype);
     this.name = 'RequestValidationError';
     this.zodError = error;
   }
 }
src/hooks/server/useSignup.ts (2)

26-31: 동일한 toast id 'alert' 사용에 대한 고려

모든 토스트에 동일한 id: 'alert'를 사용하고 있습니다. 이전 토스트가 아직 표시 중일 때 새 토스트가 동일한 id로 추가되면 의도치 않은 동작이 발생할 수 있습니다. 중복 토스트 방지가 의도된 것이라면 괜찮지만, 그렇지 않다면 고유 id 생성을 고려해 보세요.


75-78: mutation 상태를 호출자에게 노출하는 것을 고려해 보세요.

현재 반환된 함수는 isPending 상태를 내부적으로만 사용합니다. 호출하는 컴포넌트에서 로딩 상태를 표시하거나 버튼을 비활성화하려면 mutation.isPending 또는 전체 mutation 객체를 함께 반환하는 것이 유용할 수 있습니다.

-  return (data: SignupRequest) => {
-    if (mutation.isPending) return;
-    mutation.mutate(data);
-  };
+  return {
+    submit: (data: SignupRequest) => {
+      if (mutation.isPending) return;
+      mutation.mutate(data);
+    },
+    isPending: mutation.isPending,
+  };
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ded98db and 00c8066.

📒 Files selected for processing (20)
  • src/apis/chatApi.ts
  • src/apis/linkApi.ts
  • src/app/(route)/signup/SignupPage.tsx
  • src/app/(route)/signup/page.tsx
  • src/app/(route)/signup/useSignupForm.ts
  • src/app/LandingPage.tsx
  • src/app/api/member/signup/route.ts
  • src/app/api/og-thumbnail/route.ts
  • src/app/layout-client.tsx
  • src/components/basics/Input/Input.tsx
  • src/hooks/server/useSignup.ts
  • src/hooks/util/api/error/errors.ts
  • src/hooks/util/api/error/handleApiError.ts
  • src/hooks/util/api/fetch/safeFetch.ts
  • src/hooks/util/api/index.ts
  • src/hooks/util/api/request/parseJsonBody.ts
  • src/hooks/util/api/request/requestError.ts
  • src/hooks/util/api/request/validateRequest.ts
  • src/hooks/util/zodError.ts
  • src/types/api/authApi.ts
💤 Files with no reviewable changes (1)
  • src/app/api/og-thumbnail/route.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-23T14:52:20.769Z
Learnt from: Bangdayeon
Repo: Team-SoFa/linkiving PR: 102
File: src/components/layout/SideNavigation/components/AddLinkModal/AddLinkModal.tsx:23-23
Timestamp: 2025-11-23T14:52:20.769Z
Learning: In src/components/layout/SideNavigation/components/AddLinkModal/AddLinkModal.tsx, the hardcoded '/file.svg' thumbnail is intentional as a placeholder because the backend API for fetching link metadata/thumbnails is still in preparation. The actual thumbnail fetch logic will be implemented after the backend is ready.

Applied to files:

  • src/app/LandingPage.tsx
  • src/app/layout-client.tsx
📚 Learning: 2025-11-23T12:03:33.890Z
Learnt from: Bangdayeon
Repo: Team-SoFa/linkiving PR: 97
File: src/components/basics/LinkCard/LinkCard.tsx:12-19
Timestamp: 2025-11-23T12:03:33.890Z
Learning: In src/components/basics/LinkCard/LinkCard.tsx, the summary prop should remain required (string type) because the backend always provides it as a string value. The isHaveSummary flag controls whether to display the summary text or show the AddSummaryButton, not whether the data exists.

Applied to files:

  • src/components/basics/Input/Input.tsx
🧬 Code graph analysis (13)
src/hooks/util/api/request/parseJsonBody.ts (2)
src/hooks/util/api/index.ts (1)
  • parseJsonBody (1-1)
src/hooks/util/server/safeFetch.ts (1)
  • safeFetch (37-125)
src/app/LandingPage.tsx (4)
src/components/basics/LinkCard/components/AddSummaryButton.tsx (2)
  • onSummaryGenerate (3-18)
  • console (4-6)
src/components/layout/SideNavigation/components/NewChatButton.tsx (1)
  • LinkNavItem (3-5)
src/components/layout/SideNavigation/components/AddLinkButton.tsx (1)
  • AddLinkButtonProps (7-11)
src/components/layout/SideNavigation/components/MypageButton.tsx (1)
  • LinkNavItem (3-12)
src/hooks/util/api/index.ts (1)
src/hooks/util/server/safeFetch.ts (1)
  • SafeFetchOptions (31-36)
src/types/api/authApi.ts (1)
src/types/api/linkApi.ts (1)
  • ApiResponseBase (3-9)
src/apis/linkApi.ts (1)
src/hooks/util/server/safeFetch.ts (4)
  • FetchError (1-15)
  • SafeFetchOptions (31-36)
  • safeFetch (37-125)
  • constructor (5-14)
src/components/basics/Input/Input.tsx (1)
src/stories/Input.stories.tsx (3)
  • ControlledInput (29-32)
  • e (31-31)
  • args (35-35)
src/hooks/util/api/error/errors.ts (1)
src/hooks/util/server/safeFetch.ts (2)
  • safeFetch (37-125)
  • SafeFetchOptions (31-36)
src/app/(route)/signup/useSignupForm.ts (1)
src/types/api/authApi.ts (2)
  • SignupFormValues (29-29)
  • SignupSchema (24-27)
src/apis/chatApi.ts (1)
src/hooks/util/server/safeFetch.ts (4)
  • FetchError (1-15)
  • SafeFetchOptions (31-36)
  • safeFetch (37-125)
  • constructor (5-14)
src/hooks/server/useSignup.ts (5)
src/types/api/authApi.ts (2)
  • SignupRequest (42-42)
  • SignupResponse (36-36)
src/hooks/util/api/fetch/safeFetch.ts (1)
  • safeFetch (21-109)
src/hooks/util/api/index.ts (1)
  • safeFetch (3-3)
src/stores/toastStore.ts (2)
  • useToastStore (31-40)
  • showToast (42-43)
src/hooks/util/api/error/errors.ts (2)
  • FetchError (1-16)
  • TimeoutError (18-23)
src/hooks/util/api/error/handleApiError.ts (3)
src/hooks/util/api/request/requestError.ts (1)
  • RequestValidationError (3-11)
src/hooks/util/zodError.ts (1)
  • zodErrorToResponse (9-22)
src/hooks/util/api/error/errors.ts (3)
  • TimeoutError (18-23)
  • FetchError (1-16)
  • ParseError (25-33)
src/hooks/util/api/request/validateRequest.ts (2)
src/hooks/util/api/index.ts (1)
  • validateRequest (2-2)
src/hooks/util/api/request/requestError.ts (1)
  • RequestValidationError (3-11)
src/app/(route)/signup/SignupPage.tsx (2)
src/app/(route)/signup/useSignupForm.ts (1)
  • useSignupForm (7-15)
src/hooks/server/useSignup.ts (1)
  • useSignupSubmit (18-79)
🔇 Additional comments (12)
src/app/layout-client.tsx (1)

11-11: LGTM! 회원가입 페이지에서 사이드 네비게이션 숨김 처리가 적절합니다.

랜딩 페이지와 동일한 패턴으로 회원가입 페이지에서도 사이드 네비게이션을 숨기도록 구현되어 있습니다.

src/app/LandingPage.tsx (1)

63-68: LGTM! 회원가입 버튼이 적절하게 추가되었습니다.

버튼 스타일이 Google 로그인 버튼과 일관성 있게 구현되었으며, /signup 경로로의 네비게이션 로직도 올바릅니다.

src/apis/chatApi.ts (1)

1-1: 여기도 safeFetch 이동에 따른 “클라이언트 안전성”만 추가 확인하면 됩니다.

(위 src/apis/linkApi.ts 코멘트의 동일 스크립트로 함께 확인 가능)

src/hooks/util/api/error/errors.ts (3)

18-23: TimeoutError 기본 메시지 포함 깔끔합니다.


25-33: ParseError.raw 필드 추가는 디버깅에 유용합니다.


37-44: 생성 함수 제공으로 호출부 일관성 좋아졌습니다.

src/apis/linkApi.ts (1)

1-1: 경로 변경은 안전합니다 — 클라이언트 번들 가능함.

새 경로의 safeFetch 모듈(및 의존하는 errors 모듈)은 server-only, next/server, Node 전용 API를 사용하지 않습니다. fetch, AbortController, setTimeout 등 표준 Web API만 사용하므로 NEXT_PUBLIC_* 환경변수를 사용하는 linkApi.ts에서 클라이언트 번들로 포함되어도 문제없습니다. SafeFetchOptions 타입도 정상적으로 export되고 있습니다.

src/app/(route)/signup/useSignupForm.ts (1)

1-15: 구성 단순하고 일관적입니다 (RHF + Zod)
mode: 'onChange'defaultValues 포함해서 SignupPage 사용 흐름에 잘 맞습니다.

src/hooks/util/api/error/handleApiError.ts (2)

19-28: FetchError의 status 전달 동작을 확인해 주세요.

err.status ?? 502로 upstream의 HTTP 상태 코드를 그대로 클라이언트에 전달합니다. upstream에서 401/403 같은 인증 관련 에러가 발생하면 클라이언트에게 혼란을 줄 수 있습니다. 의도된 동작인지 확인이 필요합니다.

예를 들어, upstream 서버가 401을 반환하면 클라이언트는 자신의 인증 문제로 오해할 수 있습니다. 4xx 에러도 502(Bad Gateway)로 통일하거나, 특정 상태 코드만 전달하는 것을 고려해 보세요.


10-35: 에러 핸들링 구조가 잘 설계되었습니다.

에러 타입별로 적절한 HTTP 상태 코드로 매핑하는 중앙화된 핸들러 패턴이 좋습니다. @serverOnly 주석으로 사용 컨텍스트를 명시한 점도 좋습니다.

src/hooks/util/api/fetch/safeFetch.ts (1)

1-8: 에러 팩토리 패턴으로 리팩터링이 잘 되었습니다.

에러 클래스와 팩토리 함수를 별도 모듈로 분리하여 재사용성과 일관성이 향상되었습니다. @shared 주석으로 클라이언트/서버 양쪽에서 사용 가능함을 명시한 점도 좋습니다.

src/hooks/util/api/index.ts (1)

1-4: ✅ Barrel export 구조가 깔끔합니다.

API 유틸리티를 중앙에서 re-export하여 import 경로를 단순화한 좋은 패턴입니다. SafeFetchOptions와 safeFetch 함수 모두 올바른 위치(src/hooks/util/api/fetch/safeFetch.ts)에서 정의되고 있으며, 중복 파일은 없습니다.

Comment on lines +21 to +25
{
success: false,
message: 'Upstream request failed',
detail: err.body,
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

upstream 응답 본문(err.body) 노출에 주의하세요.

detail: err.body로 upstream 응답 본문을 클라이언트에 그대로 전달합니다. upstream 서버가 내부 정보나 스택 트레이스를 포함한 에러를 반환하면 민감한 정보가 노출될 수 있습니다. 프로덕션 환경에서는 상세 정보 노출을 제한하거나, 로깅만 하고 클라이언트에는 일반적인 메시지만 반환하는 것을 권장합니다.

🤖 Prompt for AI Agents
In @src/hooks/util/api/error/handleApiError.ts around lines 21 - 25, The current
response includes upstream response body via "detail: err.body", which can leak
sensitive info; update the handleApiError logic to stop returning err.body to
clients — instead log the full err.body internally (use your existing logger)
and return a generic, non-sensitive detail message (or omit detail) in the API
response; if you need dev visibility, gate returning err.body behind a clear
environment flag (e.g., only include it when NODE_ENV === 'development') so
production never exposes upstream bodies.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/hooks/server/useSignup.ts:
- Around line 34-62: The onError handler early-returns before the "기타 에러" toast,
so non-409 4xx errors (e.g., 400/401/403) never show feedback; update the
control flow in onError (the FetchError branch) to remove the stray return after
the server-error check and instead branch: if status === 409 -> show "이미 사용 중인
이메일입니다." and return; else if !status || status >= 500 -> show server-error toast
and return; otherwise (all other 4xx) call showToast with the "요청이 올바르지 않습니다.
입력값을 확인해 주세요." message. Ensure these branches are inside the FetchError check in
useSignup's onError so the correct toast is displayed per status.
🧹 Nitpick comments (4)
src/app/layout-client.tsx (1)

11-11: SideNavigation 숨김 조건 업데이트 - 승인

현재 로직은 올바르게 동작합니다. 향후 인증 관련 페이지(예: /login, /forgot-password)가 추가될 경우, 배열 기반 접근 방식이 더 확장성 있을 수 있습니다.

♻️ 선택적 리팩터링 제안
+ const AUTH_ROUTES = ['/', '/signup'];
+
  // 랜딩에서는 SideNavigation 숨김
- const showSideNav = pathname !== '/' && pathname !== '/signup';
+ const showSideNav = !AUTH_ROUTES.includes(pathname);
src/app/(route)/signup/page.tsx (1)

3-5: React 컴포넌트 네이밍 컨벤션

컴포넌트 함수명 page가 소문자로 시작합니다. React 컨벤션에 따라 PascalCase(Page 또는 SignupPageWrapper)를 사용하는 것이 권장됩니다.

♻️ 제안된 수정
- export default function page() {
+ export default function Page() {
    return <SignupPage />;
  }
src/app/api/member/signup/route.ts (1)

5-9: 환경 변수 검증이 모듈 로드 시점에 올바르게 수행됩니다.

서버 시작 시 필수 환경 변수 누락을 조기에 감지할 수 있어 좋습니다. 다만, NEXT_PUBLIC_ 접두사는 클라이언트에 노출되는 변수에 사용하는 컨벤션입니다. 서버 측에서만 사용한다면 BASE_API_URL로 변경하는 것을 고려해 보세요.

src/hooks/server/useSignup.ts (1)

26-31: 토스트 ID 중복 사용에 대한 고려 사항

모든 토스트에 동일한 id: 'alert'를 사용하고 있습니다. 이는 의도적으로 중복 토스트를 방지하려는 것으로 보이나, 빠른 연속 동작 시 중요한 메시지가 덮어씌워질 수 있습니다. 고유 ID를 사용하거나 ID를 생략하여 자동 생성되도록 하는 것도 고려해 보세요.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00c8066 and 02f29d6.

📒 Files selected for processing (20)
  • src/apis/chatApi.ts
  • src/apis/linkApi.ts
  • src/app/(route)/signup/SignupPage.tsx
  • src/app/(route)/signup/page.tsx
  • src/app/(route)/signup/useSignupForm.ts
  • src/app/LandingPage.tsx
  • src/app/api/member/signup/route.ts
  • src/app/api/og-thumbnail/route.ts
  • src/app/layout-client.tsx
  • src/components/basics/Input/Input.tsx
  • src/hooks/server/useSignup.ts
  • src/hooks/util/api/error/errors.ts
  • src/hooks/util/api/error/handleApiError.ts
  • src/hooks/util/api/fetch/safeFetch.ts
  • src/hooks/util/api/index.ts
  • src/hooks/util/api/request/parseJsonBody.ts
  • src/hooks/util/api/request/requestError.ts
  • src/hooks/util/api/request/validateRequest.ts
  • src/hooks/util/zodError.ts
  • src/types/api/authApi.ts
💤 Files with no reviewable changes (1)
  • src/app/api/og-thumbnail/route.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/apis/linkApi.ts
  • src/app/(route)/signup/SignupPage.tsx
  • src/hooks/util/api/index.ts
  • src/app/(route)/signup/useSignupForm.ts
  • src/components/basics/Input/Input.tsx
  • src/hooks/util/api/error/errors.ts
  • src/apis/chatApi.ts
  • src/types/api/authApi.ts
  • src/hooks/util/api/request/requestError.ts
  • src/hooks/util/api/error/handleApiError.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-23T14:52:20.769Z
Learnt from: Bangdayeon
Repo: Team-SoFa/linkiving PR: 102
File: src/components/layout/SideNavigation/components/AddLinkModal/AddLinkModal.tsx:23-23
Timestamp: 2025-11-23T14:52:20.769Z
Learning: In src/components/layout/SideNavigation/components/AddLinkModal/AddLinkModal.tsx, the hardcoded '/file.svg' thumbnail is intentional as a placeholder because the backend API for fetching link metadata/thumbnails is still in preparation. The actual thumbnail fetch logic will be implemented after the backend is ready.

Applied to files:

  • src/app/layout-client.tsx
🧬 Code graph analysis (4)
src/hooks/util/api/request/parseJsonBody.ts (1)
src/hooks/util/api/error/errors.ts (1)
  • ParseError (25-33)
src/hooks/util/api/request/validateRequest.ts (2)
src/hooks/util/api/index.ts (1)
  • validateRequest (2-2)
src/hooks/util/api/request/requestError.ts (1)
  • RequestValidationError (3-11)
src/app/api/member/signup/route.ts (5)
src/hooks/util/api/request/parseJsonBody.ts (1)
  • parseJsonBody (9-15)
src/hooks/util/api/request/validateRequest.ts (1)
  • validateRequest (9-17)
src/types/api/authApi.ts (2)
  • SignupRequestSchema (37-40)
  • SignupResponse (35-35)
src/hooks/util/api/fetch/safeFetch.ts (1)
  • safeFetch (21-109)
src/hooks/util/api/error/handleApiError.ts (1)
  • handleApiError (10-35)
src/hooks/server/useSignup.ts (5)
src/types/api/authApi.ts (2)
  • SignupRequest (41-41)
  • SignupResponse (35-35)
src/hooks/util/api/fetch/safeFetch.ts (1)
  • safeFetch (21-109)
src/hooks/util/api/index.ts (1)
  • safeFetch (3-3)
src/stores/toastStore.ts (2)
  • useToastStore (31-40)
  • showToast (42-43)
src/hooks/util/api/error/errors.ts (2)
  • FetchError (1-16)
  • TimeoutError (18-23)
🪛 Biome (2.1.2)
src/hooks/server/useSignup.ts

[error] 59-67: This code will never be reached ...

... because this statement will return from the function beforehand

(lint/correctness/noUnreachable)

🔇 Additional comments (8)
src/app/LandingPage.tsx (1)

63-68: 회원가입 버튼 추가 - LGTM!

회원가입 버튼이 기존 Google 로그인 버튼과 일관된 스타일로 잘 추가되었습니다.

src/hooks/util/api/request/validateRequest.ts (1)

1-17: 유효성 검증 유틸리티 구현 - LGTM!

safeParse를 사용하여 예외를 제어된 방식으로 처리하고, 제네릭 타입을 활용하여 타입 안전성을 확보한 깔끔한 구현입니다. RequestValidationError로 래핑하여 API 에러 핸들링과 잘 통합됩니다.

src/hooks/util/zodError.ts (1)

1-22: Zod 에러 응답 변환 유틸리티 - LGTM!

ZodError.flatten()을 사용하여 에러 세부 정보를 구조화하고, 상태 코드별 메시지 매핑으로 일관된 API 응답 형식을 제공합니다. 타입 안전한 상태 코드 제한(400 | 422)도 적절합니다.

src/hooks/util/api/request/parseJsonBody.ts (1)

9-14: LGTM! 서버 측 JSON 파싱 유틸리티가 잘 구현되었습니다.

ParseError를 사용한 에러 처리가 적절하며, 이 유틸리티는 validateRequest와 함께 사용되어 런타임 타입 검증이 이루어지므로 as T 캐스팅은 문제가 없습니다.

src/hooks/util/api/fetch/safeFetch.ts (2)

1-8: LGTM! 에러 처리의 중앙화 리팩터링이 잘 되었습니다.

에러 클래스와 팩토리 함수를 외부 모듈에서 import하여 사용하도록 변경한 것이 코드 일관성과 유지보수성을 높입니다.


98-105: 에러 재throw 로직이 올바르게 구현되었습니다.

AbortErrorTimeoutError로 변환하고, 이미 알려진 에러 타입은 그대로 재throw하며, 알 수 없는 에러는 FetchError로 래핑하는 패턴이 적절합니다.

src/app/api/member/signup/route.ts (1)

11-25: LGTM! 회원가입 API 라우트가 문서화된 흐름대로 잘 구현되었습니다.

parseJsonBodyvalidateRequestsafeFetchhandleApiError 패턴이 일관되게 적용되어 있으며, 타입 안전성과 에러 처리가 적절합니다.

src/hooks/server/useSignup.ts (1)

82-85: 중복 제출 방지 로직이 적절합니다.

mutation.isPending 체크를 통해 중복 요청을 방지하는 패턴이 잘 적용되었습니다.

Comment on lines +34 to +62
onError: err => {
if (err instanceof FetchError) {
if (err.status === 409) {
showToast({
id: 'alert',
message: '이미 사용 중인 이메일입니다.',
variant: 'error',
duration: 2000,
});
return;
}
if (!err.status || err.status >= 500) {
showToast({
id: 'alert',
message: '회원가입에 실패했습니다. 잠시 후 다시 시도해 주세요.',
variant: 'error',
duration: 2000,
});
return;
}
return;
// 기타 에러
showToast({
id: 'alert',
message: '요청이 올바르지 않습니다. 입력값을 확인해 주세요.',
variant: 'error',
duration: 2000,
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

도달 불가능한 코드로 인해 4xx 에러 처리가 누락됩니다.

Line 54의 return; 문으로 인해 Lines 56-61의 "기타 에러" 토스트가 절대 실행되지 않습니다. 이로 인해 400, 401, 403 등 409와 500 이외의 클라이언트 에러에 대해 사용자에게 아무런 피드백이 표시되지 않습니다.

🐛 수정 제안
       if (!err.status || err.status >= 500) {
         showToast({
           id: 'alert',
           message: '회원가입에 실패했습니다. 잠시 후 다시 시도해 주세요.',
           variant: 'error',
           duration: 2000,
         });
         return;
       }
-      return;
-      // 기타 에러
+      // 기타 4xx 에러
       showToast({
         id: 'alert',
         message: '요청이 올바르지 않습니다. 입력값을 확인해 주세요.',
         variant: 'error',
         duration: 2000,
       });
+      return;
     }
🤖 Prompt for AI Agents
In @src/hooks/server/useSignup.ts around lines 34 - 62, The onError handler
early-returns before the "기타 에러" toast, so non-409 4xx errors (e.g.,
400/401/403) never show feedback; update the control flow in onError (the
FetchError branch) to remove the stray return after the server-error check and
instead branch: if status === 409 -> show "이미 사용 중인 이메일입니다." and return; else if
!status || status >= 500 -> show server-error toast and return; otherwise (all
other 4xx) call showToast with the "요청이 올바르지 않습니다. 입력값을 확인해 주세요." message.
Ensure these branches are inside the FetchError check in useSignup's onError so
the correct toast is displayed per status.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

일반 회원가입 기능 추가

2 participants