-
Notifications
You must be signed in to change notification settings - Fork 0
회원가입 기능 추가, safeFetch 정리 #337
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this 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 중복 제거 고려
LoginSchema와SignupSchema가 현재 동일한 구조(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
📒 Files selected for processing (20)
src/apis/chatApi.tssrc/apis/linkApi.tssrc/app/(route)/signup/SignupPage.tsxsrc/app/(route)/signup/page.tsxsrc/app/(route)/signup/useSignupForm.tssrc/app/LandingPage.tsxsrc/app/api/member/signup/route.tssrc/app/api/og-thumbnail/route.tssrc/app/layout-client.tsxsrc/components/basics/Input/Input.tsxsrc/hooks/server/useSignup.tssrc/hooks/util/api/error/errors.tssrc/hooks/util/api/error/handleApiError.tssrc/hooks/util/api/fetch/safeFetch.tssrc/hooks/util/api/index.tssrc/hooks/util/api/request/parseJsonBody.tssrc/hooks/util/api/request/requestError.tssrc/hooks/util/api/request/validateRequest.tssrc/hooks/util/zodError.tssrc/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.tsxsrc/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)에서 정의되고 있으며, 중복 파일은 없습니다.
| { | ||
| success: false, | ||
| message: 'Upstream request failed', | ||
| detail: err.body, | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
00c8066 to
02f29d6
Compare
There was a problem hiding this 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
📒 Files selected for processing (20)
src/apis/chatApi.tssrc/apis/linkApi.tssrc/app/(route)/signup/SignupPage.tsxsrc/app/(route)/signup/page.tsxsrc/app/(route)/signup/useSignupForm.tssrc/app/LandingPage.tsxsrc/app/api/member/signup/route.tssrc/app/api/og-thumbnail/route.tssrc/app/layout-client.tsxsrc/components/basics/Input/Input.tsxsrc/hooks/server/useSignup.tssrc/hooks/util/api/error/errors.tssrc/hooks/util/api/error/handleApiError.tssrc/hooks/util/api/fetch/safeFetch.tssrc/hooks/util/api/index.tssrc/hooks/util/api/request/parseJsonBody.tssrc/hooks/util/api/request/requestError.tssrc/hooks/util/api/request/validateRequest.tssrc/hooks/util/zodError.tssrc/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 로직이 올바르게 구현되었습니다.
AbortError를TimeoutError로 변환하고, 이미 알려진 에러 타입은 그대로 재throw하며, 알 수 없는 에러는FetchError로 래핑하는 패턴이 적절합니다.src/app/api/member/signup/route.ts (1)
11-25: LGTM! 회원가입 API 라우트가 문서화된 흐름대로 잘 구현되었습니다.
parseJsonBody→validateRequest→safeFetch→handleApiError패턴이 일관되게 적용되어 있으며, 타입 안전성과 에러 처리가 적절합니다.src/hooks/server/useSignup.ts (1)
82-85: 중복 제출 방지 로직이 적절합니다.
mutation.isPending체크를 통해 중복 요청을 방지하는 패턴이 잘 적용되었습니다.
| 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, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
도달 불가능한 코드로 인해 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.
관련 이슈
PR 설명
safeFetch정리error/errors.tssafeFetch의 에러 관련 로직(FetchError,TimeoutError,ParseError)을errors.ts로 분리, 각 로직에 대한 생성함수 작성handleApiError.tsrequest/: 요청 중 에러를 다룸parseJsonBody.tsNextRequest에서 JSON body를 안전하게 파싱 (요청 형식 오류 감지)<T>로 기대하는 request 타입을 전달requestError.tsvalidateRequest.ts<T>로 기대하는 zod 스키마 타입을 전달zodError임시 회원가입 페이지 및 로직 작성
(route)/signup/SignupPage.tsxuseSignup.tsconst signup...SignupResponse의 형태로 결과를safeFetch받음useSignupSubmituseMutation으로 회원가입 비동기 작업 진행Toast로 임의 처리useSignupForm.tszodResolver,react-hook-form을 사용하여 입력값 검증 및 상태 관리 수행resolver: 폼 검증 로직을SignupSchema와 연동해 자동 검증mode입력값이 변경될때마다 검증 수행defaultValues: 폼 초기 렌더링 기본 입력값 정의authApi.ts공유사항
hooks/util/api/index.ts의 네가지를 import하여 api에 사용할 수 있습니다.signup/route.ts에서 사용한 예시를 확인할 수 있습니다.body = await parseJsonBody(NextRequest): 요청 바디를 안전하게 파싱data = validateRequest(RequestSchema, 검사한 body): 파싱한 바디가 스키마형식에 맞는지 검증handleApiError(err): 에러 발생 시, HTTP Response로 변환하여 리턴