Conversation
📝 WalkthroughWalkthrough인증 콜백 처리, 온보딩 마법사 및 닉네임 단계, 프로필 업데이트 API/훅과 ApiError 타입을 추가하고 Google 로그인·로그아웃 리디렉션 및 에러 처리를 개선합니다. useMe의 staleTime이 0으로 변경됩니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant Browser as 클라이언트
participant AuthProvider as 인증 제공자
participant AppAPI as 앱 API
participant QueryClient as 쿼리 클라이언트
rect rgba(200,220,255,0.6)
Note over User,Browser: 1) Google 로그인 → 콜백으로 리디렉트
User->>Browser: Google 로그인 클릭
Browser->>AuthProvider: 인증 요청 (callbackUrl includes next)
AuthProvider-->>Browser: /auth/callback?next=/원래경로 리다이렉트
end
rect rgba(220,240,220,0.6)
Note over Browser,AppAPI: 2) 콜백 페이지 처리 (useMe)
Browser->>AppAPI: GET /me (useMe)
alt ApiError 401/403
AppAPI-->>Browser: ApiError(401/403)
Browser->>Browser: /onboarding?next=/원래경로로 리다이렉트
else 성공
AppAPI-->>Browser: user
Browser->>QueryClient: me 쿼리 갱신
Browser->>Browser: /원래경로로 리다이렉트
end
end
rect rgba(255,240,200,0.6)
Note over User,AppAPI: 3) 온보딩 완료 흐름
User->>Browser: 닉네임 입력 및 제출
Browser->>AppAPI: PATCH /user-profiles/me/onboarding (completeOnboarding)
AppAPI-->>Browser: { result: true }
Browser->>QueryClient: invalidateQueries(["me"])
QueryClient->>AppAPI: refetch /me
Browser->>Browser: next 경로로 리다이렉트
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✨ 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.
Actionable comments posted: 7
🧹 Nitpick comments (12)
src/app/onboarding/page.tsx (1)
3-11: 불필요한 Fragment 래퍼를 제거하세요.현재
<><div>...</div></>로 감싸고 있는데, Fragment가 불필요합니다. 또한OnboardingWizard는useRouter,useState,useMemo등 클라이언트 훅을 사용하고 있으며, 파일 최상단에"use client"지시자가 올바르게 설정되어 있어 서버 컴포넌트인 이 페이지와의 경계가 적절합니다.🔎 불필요한 Fragment 제거 제안
export default function OnboardingPage() { - return ( - <> - <div> - <OnboardingWizard /> - </div> - </> - ); + return ( + <div> + <OnboardingWizard /> + </div> + ); }src/entities/user/api/me.ts (1)
17-17: 타입 캐스팅 대신 런타임 검증 또는 명시적 매핑 고려
as User캐스팅은 API 응답의 실제 형태를 검증하지 않아 런타임 타입 불일치 위험이 있습니다.코딩 가이드라인에 따르면 API 모듈은 "DTO → Domain 매핑까지 책임"져야 하며, raw DTO를 직접 다루지 않도록 권장됩니다.
🔎 개선 방안
방안 1: 간단한 런타임 검증 (추천)
- return (await res.json()) as User; + const data = await res.json(); + if (!data.nickname || !data.slug) { + throw new ApiError(500, "Invalid user data structure"); + } + return data as User;방안 2: toDomain 매핑 함수 도입
// 별도 파일에 DTO 타입과 매핑 함수 정의 type UserDTO = { nickname: string; slug: string; profileImageUrl: string | null; }; function toUser(dto: UserDTO): User { return { nickname: dto.nickname, slug: dto.slug, profileImageUrl: dto.profileImageUrl, }; } // API 함수에서 사용 const dto = await res.json(); return toUser(dto);코딩 가이드라인 기준 적용
src/entities/user/hooks/useCompleteOnboarding.ts (1)
10-10: queryKey를 상수로 중앙 관리 권장
["me"]문자열 리터럴을 직접 사용하면 다른 곳에서 동일 키를 사용할 때 오타 위험과 타입 안전성 저하가 발생합니다.🔎 개선 방안
queryKey를 중앙 관리하는 상수 파일 생성
// src/entities/user/api/queryKeys.ts export const userQueryKeys = { me: ["me"] as const, // 향후 추가 키들... } as const;hook에서 사용
+import { userQueryKeys } from "../api/queryKeys"; + export function useCompleteOnboarding() { const qc = useQueryClient(); return useMutation({ mutationFn: completeOnboarding, onSuccess: async () => { - await qc.invalidateQueries({ queryKey: ["me"] }); + await qc.invalidateQueries({ queryKey: userQueryKeys.me }); }, }); }이렇게 하면 타입 안전성이 향상되고 queryKey 변경 시 한 곳만 수정하면 됩니다.
src/features/user-profile/api/updateMe.ts (2)
23-23: 타입 캐스팅 대신 런타임 검증 고려
me.ts의 Line 17과 동일한 이슈입니다.as User캐스팅은 API 응답의 실제 타입을 검증하지 않아 런타임 타입 불일치 위험이 있습니다.코딩 가이드라인의 "DTO → Domain 매핑" 권장사항에 따라 명시적 변환 계층을 고려하세요.
코딩 가이드라인 기준 적용
7-24: API 호출 로직의 공통 패턴 추상화 고려
me.ts,onboarding.ts,logout.ts등 여러 API 파일에서 동일한 fetch → 에러 핸들링 패턴이 반복됩니다. 공통 fetch wrapper를 도입하면 코드 중복을 줄이고 유지보수성을 높일 수 있습니다.🔎 개선 방안
공통 API 유틸리티 함수 생성
// src/shared/api/fetcher.ts export async function apiFetch<T>( url: string, options?: RequestInit ): Promise<T> { const res = await fetch(`${API_BASE_URL}${url}`, { credentials: "include", headers: { Accept: "application/json", ...options?.headers, }, ...options, }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new ApiError(res.status, text || res.statusText); } return res.json(); }사용 예시
export async function updateMe(payload: UpdateMePayload): Promise<User> { return apiFetch<User>("/api/v1/user-profiles/me", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); }src/shared/api/api-error.ts (1)
1-5: 에러 클래스 구조 양호, Error.name 설정 고려기본 구현은 적절합니다. 디버깅 편의성을 위해
Error.name을 명시적으로 설정하는 것을 고려하세요.🔎 개선 제안
export class ApiError extends Error { constructor(public status: number, message: string) { super(message); + this.name = "ApiError"; } }이렇게 하면 콘솔이나 로그에서
ApiError: ...형태로 표시되어 일반Error와 구분하기 쉽습니다.src/features/user-profile/ui/NicknameEditForm.tsx (1)
35-35: 에러 메시지를 더 구체적으로 표시 권장현재 고정된 "변경 실패" 메시지만 표시되어 사용자가 실패 원인을 파악하기 어렵습니다.
error객체의 메시지를 활용하여 더 구체적인 정보를 제공하는 것을 고려해보세요.🔎 개선안
- {error && <p className="text-sm text-red-600">변경 실패</p>} + {error && ( + <p className="text-sm text-red-600"> + 변경 실패: {error.message || "알 수 없는 오류"} + </p> + )}src/app/auth/callback/page.tsx (1)
19-37: useEffect 의존성 배열 정리 권장현재 의존성 배열에
router가 포함되어 있으나, Next.js의useRouter훅이 반환하는 router 객체는 안정적인 참조이므로 의존성에서 제거할 수 있습니다.또한
next값이 매 렌더링마다sp.get()으로 재계산되므로,useMemo로 메모이제이션하는 것을 고려해보세요.🔎 개선안
+ import { useEffect, useMemo } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useMe } from "@/src/entities/user/hooks/useMe"; import { ApiError } from "@/src/shared/api/api-error"; export default function AuthCallbackPage() { const router = useRouter(); const sp = useSearchParams(); - const next = sp.get("next") || "/"; + const next = useMemo(() => sp.get("next") || "/", [sp]); const { data: me, isError, error, isSuccess } = useMe(); useEffect(() => { // 리다이렉션 로직... - }, [isError, error, isSuccess, me, next, router]); + }, [isError, error, isSuccess, me, next]); return null; }src/features/onboarding/ui/OnboardingWizard.tsx (2)
22-22: 불필요한 useMemo 제거 필요
draft.nickname ?? ""는 단순한 null 병합 연산이므로 useMemo로 감쌀 필요가 없습니다. 계산 비용이 매우 낮아 메모이제이션의 이점이 없고, 오히려 코드만 복잡하게 만듭니다.🔎 개선안
- const defaultNickname = useMemo(() => draft.nickname ?? "", [draft.nickname]); + const defaultNickname = draft.nickname ?? "";
31-39: 타입 안정성 개선 필요: as 캐스팅 대신 명시적 검증Line 32에서
nickname존재 여부만 검증하고 Line 34에서as OnboardingPayload로 강제 캐스팅하는 것은 향후 필드가 추가될 때 타입 안정성을 보장하지 못합니다.현재는 nickname만 필수이므로 문제없지만, 확장성 관점에서 타입 가드 함수를 사용하거나 명시적으로 모든 필수 필드를 검증하는 것이 안전합니다.
🔎 개선안 1: 타입 가드 사용
별도 파일(예:
src/entities/user/model/guards.ts)에 타입 가드 추가:export function isOnboardingPayload( payload: Partial<OnboardingPayload> ): payload is OnboardingPayload { return typeof payload.nickname === "string" && payload.nickname.trim() !== ""; // 향후 필드 추가 시 여기에 검증 로직 추가 }그리고 이 파일에서 사용:
+import { isOnboardingPayload } from "@/src/entities/user/model/guards"; const submitAll = (payload: Partial<OnboardingPayload>) => { - if (!payload.nickname) return; + if (!isOnboardingPayload(payload)) return; - mutate(payload as OnboardingPayload, { + mutate(payload, { onSuccess: () => { router.replace(next); }, }); };🔎 개선안 2: 명시적 객체 생성
const submitAll = (payload: Partial<OnboardingPayload>) => { if (!payload.nickname) return; - mutate(payload as OnboardingPayload, { + const completePayload: OnboardingPayload = { + nickname: payload.nickname, + // 향후 필드 추가 시 여기에 명시적으로 추가 + }; + + mutate(completePayload, { onSuccess: () => { router.replace(next); }, }); };src/features/onboarding/ui/steps/NicknameStep.tsx (2)
37-37: 매 렌더링마다 trim() 계산 제거 (파생 상태)버튼의
disabled속성에서!nickname.trim()를 매번 계산하는 대신, 파생 값으로 한 번만 계산하는 것이 좋습니다. 성능 영향은 미미하지만 코드 명확성과 일관성 측면에서 개선할 수 있습니다.🔎 개선안
export default function NicknameStep({ defaultValue = "", submitting = false, onNext, }: Props) { const [nickname, setNickname] = useState(defaultValue); + const isValid = nickname.trim() !== ""; const handleSubmit = (e: FormEvent) => { e.preventDefault(); const v = nickname.trim(); if (!v) return; onNext(v); }; return ( <form onSubmit={handleSubmit}> <label> 닉네임 <input value={nickname} onChange={(e) => setNickname(e.target.value)} placeholder="닉네임을 입력하세요" disabled={submitting} /> </label> - <button type="submit" disabled={submitting || !nickname.trim()}> + <button type="submit" disabled={submitting || !isValid}> 다음 </button> </form> ); }
27-35: 접근성 개선: label 구조 권장사항현재
<label>안에 텍스트와<input>을 함께 배치했는데, 명시적으로htmlFor와id를 사용하는 것이 접근성 측면에서 더 명확합니다.🔎 개선안
- <label> - 닉네임 - <input + <label htmlFor="nickname-input">닉네임</label> + <input + id="nickname-input" value={nickname} onChange={(e) => setNickname(e.target.value)} placeholder="닉네임을 입력하세요" disabled={submitting} /> - </label>
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
src/app/auth/callback/page.tsxsrc/app/onboarding/page.tsxsrc/app/test/page.tsxsrc/entities/user/api/me.tssrc/entities/user/api/onboarding.tssrc/entities/user/hooks/useCompleteOnboarding.tssrc/entities/user/model/types.tssrc/features/auth/login-with-google/api/login.tssrc/features/auth/logout/api/logout.tssrc/features/auth/logout/model/useLogout.tssrc/features/onboarding/ui/OnboardingWizard.tsxsrc/features/onboarding/ui/steps/NicknameStep.tsxsrc/features/user-profile/api/updateMe.tssrc/features/user-profile/hooks/useUpdateMe.tssrc/features/user-profile/model/types.tssrc/features/user-profile/ui/NicknameEditForm.tsxsrc/shared/api/api-error.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*
⚙️ CodeRabbit configuration file
**/*: - 리뷰는 한국어로 작성.
- 유지보수/확장성 관점에서 모듈 경계(의존성 방향, 책임 분리)가 적절한지 최우선으로 확인.
- 파일/함수 책임이 과도하면 응집도/결합도 기준으로 분리/통합 개선안을 제시.
- TypeScript에서 any/과도한 as 캐스팅을 지양하고, 도메인 타입(예: ProfileId 같은 ID 타입)으로 의미를 드러내는지 확인.
- React에서 상태 위치 미스, 불필요한 리렌더링, derived state 남발, useEffect 남용을 지적하고 개선안을 제시.
Files:
src/features/user-profile/hooks/useUpdateMe.tssrc/features/user-profile/model/types.tssrc/entities/user/hooks/useCompleteOnboarding.tssrc/features/onboarding/ui/OnboardingWizard.tsxsrc/features/auth/login-with-google/api/login.tssrc/entities/user/api/onboarding.tssrc/shared/api/api-error.tssrc/features/user-profile/api/updateMe.tssrc/features/onboarding/ui/steps/NicknameStep.tsxsrc/app/auth/callback/page.tsxsrc/entities/user/api/me.tssrc/entities/user/model/types.tssrc/app/test/page.tsxsrc/features/auth/logout/api/logout.tssrc/app/onboarding/page.tsxsrc/features/user-profile/ui/NicknameEditForm.tsxsrc/features/auth/logout/model/useLogout.ts
**/ui/**/*.tsx
⚙️ CodeRabbit configuration file
**/ui/**/*.tsx: - 상태 위치가 적절한지(상/하위 이동 필요성) 지적하고 개선안 제시.
- 파생 값은 state로 두지 말고 계산(useMemo는 필요할 때만).
- 리스트 key 안정성, 불필요한 re-render 유발 props(익명 함수/객체) 지적.
Files:
src/features/onboarding/ui/OnboardingWizard.tsxsrc/features/onboarding/ui/steps/NicknameStep.tsxsrc/features/user-profile/ui/NicknameEditForm.tsx
**/api/**/*.ts*
⚙️ CodeRabbit configuration file
**/api/**/*.ts*: - API 모듈은 DTO 타입 + 네트워크 호출 + toDomain 매핑까지 책임지는 구조를 우선 권장.
- UI에서 raw DTO(res.json())를 직접 다루지 않도록, 반환 타입은 Domain(User 등)으로 정규화.
- fetch 옵션(credentials, headers 등) 규약이 일관적인지 확인.
Files:
src/features/auth/login-with-google/api/login.tssrc/entities/user/api/onboarding.tssrc/shared/api/api-error.tssrc/features/user-profile/api/updateMe.tssrc/entities/user/api/me.tssrc/features/auth/logout/api/logout.ts
src/app/**/*.ts*
⚙️ CodeRabbit configuration file
src/app/**/*.ts*: - Next.js App Router 규칙 준수: server/client 컴포넌트 경계가 적절한지 확인.
- 가능하면 데이터 패칭은 async 서버 컴포넌트/route handler에서 처리하고, 클라에서 불필요한 fetch/useEffect를 줄일 것.
- "use client"는 꼭 필요한 곳에만.
Files:
src/app/auth/callback/page.tsxsrc/app/test/page.tsxsrc/app/onboarding/page.tsx
🧬 Code graph analysis (13)
src/features/user-profile/hooks/useUpdateMe.ts (2)
src/features/user-profile/model/types.ts (1)
UpdateMePayload(1-4)src/features/user-profile/api/updateMe.ts (1)
updateMe(7-24)
src/entities/user/hooks/useCompleteOnboarding.ts (1)
src/entities/user/api/onboarding.ts (1)
completeOnboarding(5-25)
src/features/onboarding/ui/OnboardingWizard.tsx (3)
src/entities/user/model/types.ts (1)
OnboardingPayload(7-12)src/entities/user/hooks/useCompleteOnboarding.ts (1)
useCompleteOnboarding(4-13)src/features/onboarding/ui/steps/NicknameStep.tsx (1)
NicknameStep(11-42)
src/features/auth/login-with-google/api/login.ts (1)
src/shared/api/config.ts (1)
API_BASE_URL(1-1)
src/entities/user/api/onboarding.ts (3)
src/entities/user/model/types.ts (1)
OnboardingPayload(7-12)src/shared/api/config.ts (1)
API_BASE_URL(1-1)src/shared/api/api-error.ts (1)
ApiError(1-5)
src/features/user-profile/api/updateMe.ts (4)
src/features/user-profile/model/types.ts (1)
UpdateMePayload(1-4)src/entities/user/model/types.ts (1)
User(1-5)src/shared/api/config.ts (1)
API_BASE_URL(1-1)src/shared/api/api-error.ts (1)
ApiError(1-5)
src/app/auth/callback/page.tsx (2)
src/entities/user/hooks/useMe.ts (1)
useMe(7-14)src/shared/api/api-error.ts (1)
ApiError(1-5)
src/entities/user/api/me.ts (1)
src/shared/api/api-error.ts (1)
ApiError(1-5)
src/app/test/page.tsx (1)
src/features/user-profile/ui/NicknameEditForm.tsx (1)
NicknameEditForm(6-38)
src/features/auth/logout/api/logout.ts (2)
src/shared/api/config.ts (1)
API_BASE_URL(1-1)src/shared/api/api-error.ts (1)
ApiError(1-5)
src/app/onboarding/page.tsx (1)
src/features/onboarding/ui/OnboardingWizard.tsx (1)
OnboardingWizard(11-52)
src/features/user-profile/ui/NicknameEditForm.tsx (1)
src/features/user-profile/hooks/useUpdateMe.ts (1)
useUpdateMe(8-17)
src/features/auth/logout/model/useLogout.ts (1)
src/features/auth/logout/api/logout.ts (1)
logoutApi(4-14)
🔇 Additional comments (11)
src/features/user-profile/model/types.ts (1)
1-4: 타입 정의가 적절합니다.PATCH 페이로드에 적합하게 모든 필드를 optional로 정의했고,
profileImageUrl에null을 허용해 이미지 제거 시나리오를 명시적으로 지원하는 점이 좋습니다. 타입이 model 디렉토리에 분리되어 관심사가 명확히 구분되어 있습니다.src/app/test/page.tsx (1)
6-6: 테스트 페이지에 적절하게 추가되었습니다.
NicknameEditForm컴포넌트가 클라이언트 훅을 사용하고 있으며, 이미"use client"지시자가 있는 페이지에 추가되어 클라이언트/서버 경계가 적절합니다.Also applies to: 15-15
src/features/user-profile/hooks/useUpdateMe.ts (1)
8-17: 훅 구현이 React Query 모범 사례를 잘 따르고 있습니다.
useMutation을 활용한 표준 패턴- 성공 시
"me"쿼리를 무효화하여 UI 동기화 보장invalidateQueries를await로 처리해 리페치 완료를 보장- 단일 책임 원칙 준수: 훅은 mutation 로직만, API 호출은 별도 레이어에 위임
모듈 경계가 명확하고(hooks → api → model), 의존성 방향도 적절합니다.
src/features/auth/login-with-google/api/login.ts (1)
4-8: 콜백 페이지에서redirectToGoogleLogin호출이 없으므로 순환 리디렉션 위험은 없습니다.현재 페이지 URL을
next파라미터로 전달하고 인코딩을 올바르게 처리한 것은 적절합니다. 다만 검토 결과,redirectToGoogleLogin은googleLoginButton.tsx의 버튼 클릭 핸들러에서만 사용되며 콜백 페이지(auth/callback/page.tsx)에서는 호출되지 않습니다. 콜백 페이지는next파라미터를 받아router.replace(next)로 리디렉션할 뿐입니다. 따라서 정상적인 코드 흐름에서 순환 리디렉션은 발생하지 않습니다.다만
next파라미터가/auth/callback자체를 가리킬 수 있는 엣지 케이스는 존재하므로, 필요시 콜백 핸들러에서 리디렉션 대상이 인증 경로가 아닌지 검증하는 것을 고려해볼 수 있습니다.Likely an incorrect or invalid review comment.
src/features/auth/logout/api/logout.ts (1)
1-14: 변경사항 적절함함수명을
logoutApi로 변경하여 명확성을 높였고,ApiError를 사용하여 일관된 에러 핸들링을 구현했습니다. 다른 API 함수들과 구조적 일관성도 유지되고 있습니다.src/entities/user/api/onboarding.ts (1)
6-17: API 호출 옵션이 적절함
credentials: "include", JSON 헤더 설정, PATCH 메서드 사용이 온보딩 API 특성에 적합하며, 다른 API 모듈과의 일관성도 유지되고 있습니다.src/entities/user/model/types.ts (1)
7-12: 타입 정의가 명확함
OnboardingPayload타입이 현재 온보딩 흐름의 요구사항을 명확히 표현하고 있으며, 주석 처리된 필드를 통해 향후 확장 가능성도 고려되었습니다.src/features/user-profile/ui/NicknameEditForm.tsx (1)
6-8: 로컬 상태 관리가 적절함폼 입력을 위한 로컬 상태 관리는 이 컴포넌트의 책임 범위에 적절하며, 불필요한 상위 상태 끌어올리기를 피하고 있습니다.
src/app/auth/callback/page.tsx (2)
34-36: 방어적 타입 가드가 적절함
isSuccess && me조건을 통해 타입 안전성을 확보하는 방어적 코드 작성이 적절합니다.
1-1: 클라이언트 컴포넌트 사용이 적절함인증 콜백 페이지는 동적 리다이렉션 로직과 클라이언트 측 상태 관리가 필요하므로,
"use client"지시어 사용이 적절합니다.As per coding guidelines, "use client" is used only where necessary.
src/features/onboarding/ui/steps/NicknameStep.tsx (1)
16-16: defaultValue 변경 시 상태 동기화 미지원현재 구현은
defaultValue를 초기값으로만 사용하며, 부모에서defaultValueprop이 변경되어도 로컬 상태(nickname)가 업데이트되지 않습니다.현재
OnboardingWizard의 사용 패턴을 보면defaultValue는 마운트 시점에만 설정되고 이후 변경되지 않으므로 문제없지만, 향후 확장 시 주의가 필요합니다.만약 defaultValue가 동적으로 변경될 가능성이 있다면,
useEffect로 동기화하거나 상태를 부모로 올려 제어 컴포넌트로 만드는 것을 고려하세요.현재 사용 패턴에서는 문제없으나, 향후 확장 시 이 동작이 의도된 것인지 확인이 필요합니다.
| export async function completeOnboarding(payload: OnboardingPayload) { | ||
| const res = await fetch( | ||
| `${API_BASE_URL}/api/v1/user-profiles/me/onboarding`, | ||
| { | ||
| method: "PATCH", | ||
| credentials: "include", | ||
| headers: { | ||
| Accept: "application/json", | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify(payload), | ||
| } | ||
| ); | ||
|
|
||
| if (!res.ok) { | ||
| const text = await res.text().catch(() => ""); | ||
| throw new ApiError(res.status, text || res.statusText); | ||
| } | ||
|
|
||
| return res.json(); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
반환 타입 명시 및 Domain 변환 로직 추가 필요
현재 res.json()을 직접 반환하여 반환 타입이 Promise<any>로 추론되며, API 응답 구조가 불명확합니다.
코딩 가이드라인에 따르면 API 모듈은 DTO 타입 정의 + 네트워크 호출 + Domain 변환까지 책임져야 하며, UI에서 raw DTO를 직접 다루지 않아야 합니다.
🔎 개선안
- 응답 DTO 타입 정의:
// src/entities/user/api/onboarding.ts 상단에 추가
interface CompleteOnboardingResponse {
// 서버 응답 구조에 맞게 정의
// 예시:
success: boolean;
// 또는 User 객체 반환 시
// user: { nickname: string; slug: string; profileImageUrl: string | null; }
}- 반환 타입 명시 및 필요 시 Domain 변환:
-export async function completeOnboarding(payload: OnboardingPayload) {
+export async function completeOnboarding(
+ payload: OnboardingPayload
+): Promise<CompleteOnboardingResponse> {
const res = await fetch(
// ...
);
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new ApiError(res.status, text || res.statusText);
}
- return res.json();
+ const data: CompleteOnboardingResponse = await res.json();
+ return data;
// 또는 Domain 변환이 필요한 경우:
// return toDomainUser(data.user);
}As per coding guidelines, API modules should normalize return types to Domain models.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/entities/user/api/onboarding.ts around lines 5 to 25, the function
returns res.json() directly causing Promise<any> and leaking raw DTOs to the UI;
define a response DTO (e.g., CompleteOnboardingResponse) that matches the API,
change the function signature to return Promise<YourDomainUserType> (or a
normalized domain shape), await and type the parsed JSON into the DTO,
map/convert the DTO into the Domain model inside this module (normalizing field
names and optional values like profileImageUrl), and return that Domain object;
keep existing error handling but ensure the typed parsing happens before mapping
so callers get a strongly-typed Domain object instead of any.
| const onSubmit = (e: React.FormEvent) => { | ||
| e.preventDefault(); | ||
|
|
||
| const trimmed = nickname.trim(); | ||
| if (!trimmed) return; | ||
|
|
||
| mutate({ nickname: trimmed }); | ||
| }; |
There was a problem hiding this comment.
mutation 성공 시 입력 필드 초기화 필요
현재 닉네임 변경이 성공해도 입력 필드가 초기화되지 않아, 사용자가 변경 완료 여부를 명확히 인지하기 어렵습니다.
mutation 성공 시 입력 필드를 초기화하여 UX를 개선하는 것을 권장합니다.
🔎 개선안
export default function NicknameEditForm() {
- const { mutate, isPending, error } = useUpdateMe();
+ const { mutate, isPending, error, isSuccess } = useUpdateMe();
const [nickname, setNickname] = useState("");
+ useEffect(() => {
+ if (isSuccess) {
+ setNickname("");
+ }
+ }, [isSuccess]);
+
const onSubmit = (e: React.FormEvent) => {
// ...
};또는 mutate 호출 시 onSuccess 콜백 사용:
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
const trimmed = nickname.trim();
if (!trimmed) return;
- mutate({ nickname: trimmed });
+ mutate({ nickname: trimmed }, {
+ onSuccess: () => setNickname(""),
+ });
};📝 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.
| const onSubmit = (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| const trimmed = nickname.trim(); | |
| if (!trimmed) return; | |
| mutate({ nickname: trimmed }); | |
| }; | |
| const onSubmit = (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| const trimmed = nickname.trim(); | |
| if (!trimmed) return; | |
| mutate({ nickname: trimmed }, { | |
| onSuccess: () => setNickname(""), | |
| }); | |
| }; |
🤖 Prompt for AI Agents
In src/features/user-profile/ui/NicknameEditForm.tsx around lines 10 to 17, the
form does not clear the input after a successful nickname mutation; update the
mutate call to provide an onSuccess handler (or pass an options object) that
resets the local nickname state to an empty string and optionally shows success
feedback, ensuring the input is cleared when the server confirms the change.
|
Note Docstrings generation - SUCCESS |
Docstrings generation was requested by @aryu1217. * #9 (comment) The following files were modified: * `src/app/auth/callback/page.tsx` * `src/app/onboarding/page.tsx` * `src/app/test/page.tsx` * `src/entities/user/api/me.ts` * `src/entities/user/api/onboarding.ts` * `src/entities/user/hooks/useCompleteOnboarding.ts` * `src/features/auth/login-with-google/api/login.ts` * `src/features/auth/logout/api/logout.ts` * `src/features/auth/logout/model/useLogout.ts` * `src/features/onboarding/ui/OnboardingWizard.tsx` * `src/features/onboarding/ui/steps/NicknameStep.tsx` * `src/features/user-profile/api/updateMe.ts` * `src/features/user-profile/hooks/useUpdateMe.ts` * `src/features/user-profile/ui/NicknameEditForm.tsx`
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/features/user-profile/api/updateMe.ts (1)
22-22: DTO 타입과 toDomain 매핑이 필요합니다.현재
as User타입 단언을 사용하여 응답을 직접 캐스팅하고 있는데, 이는 API 모듈 패턴에서 권장하는 구조를 벗어납니다. 코딩 가이드라인에 따르면 "API 모듈은 DTO 타입 + 네트워크 호출 + toDomain 매핑까지 책임지는 구조를 우선 권장"하며, "UI에서 raw DTO(res.json())를 직접 다루지 않도록, 반환 타입은 Domain(User 등)으로 정규화"해야 합니다.문제점:
- 런타임 타입 검증 없이 컴파일 타임에만 타입 안전성 보장
- API 응답 구조 변경 시 오류를 조기에 감지하기 어려움
- 타입 불일치 에러가 UI 레이어에서 발생하여 디버깅 어려움
코딩 가이드라인 기준
🔎 DTO 타입과 매핑 함수를 추가하는 리팩토링 제안
import { ApiError } from "@/src/shared/api/api-error"; import { API_BASE_URL } from "@/src/shared/api/config"; import type { User } from "@/src/entities/user/model/types"; import { UpdateMePayload } from "../model/types"; + // API 응답 DTO 타입 정의 + type UpdateMeResponseDto = { + nickname: string; + slug: string; + profileImageUrl: string | null; + }; + + // DTO를 Domain 타입으로 변환 + function toUser(dto: UpdateMeResponseDto): User { + return { + nickname: dto.nickname, + slug: dto.slug, + profileImageUrl: dto.profileImageUrl, + }; + } export async function updateMe(payload: UpdateMePayload): Promise<User> { const res = await fetch(`${API_BASE_URL}/api/v1/user-profiles/me`, { method: "PATCH", credentials: "include", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify(payload), }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new ApiError(res.status, text || res.statusText); } - return (await res.json()) as User; + const dto: UpdateMeResponseDto = await res.json(); + return toUser(dto); }이 구조를 통해:
- API 응답 형태를 명시적으로 정의
- 매핑 로직을 한 곳에서 관리
- 향후 zod 등의 런타임 검증 라이브러리 추가 용이
🧹 Nitpick comments (3)
src/features/user-profile/api/updateMe.ts (1)
17-20: 에러 응답 파싱 개선을 고려해 보세요.현재는 에러 응답을 텍스트로만 읽고 있는데, 서버가 JSON 형식의 에러 응답을 반환한다면 구조화된 에러 정보를 활용할 수 있습니다.
🔎 JSON 에러 응답 처리 개선안
if (!res.ok) { - const text = await res.text().catch(() => ""); - throw new ApiError(res.status, text || res.statusText); + let message = res.statusText; + try { + const errorData = await res.json(); + message = errorData.message || errorData.error || message; + } catch { + const text = await res.text().catch(() => ""); + message = text || message; + } + throw new ApiError(res.status, message); }src/features/onboarding/ui/OnboardingWizard.tsx (2)
22-22: 불필요한 useMemo 제거 권장단순한 null coalescing 연산에는 useMemo가 불필요합니다. 직접 계산으로도 충분하며, 코드가 더 간결해집니다.
🔎 제안하는 수정안
- const defaultNickname = useMemo(() => draft.nickname ?? "", [draft.nickname]); + const defaultNickname = draft.nickname ?? "";
31-39: 타입 캐스팅 대신 타입 가드 사용 권장Line 34의
as OnboardingPayload캐스팅은 타입 안정성을 우회합니다. 현재는 nickname만 체크하지만, 향후 OnboardingPayload에 다른 필수 필드가 추가되면 런타임 에러가 발생할 수 있습니다.🔎 타입 가드를 사용한 개선안
+ // 파일 상단에 타입 가드 추가 + function isValidOnboardingPayload( + payload: Partial<OnboardingPayload> + ): payload is OnboardingPayload { + return !!payload.nickname; + // 향후 필드 추가 시: && !!payload.otherField + } + const submitAll = (payload: Partial<OnboardingPayload>) => { - if (!payload.nickname) return; + if (!isValidOnboardingPayload(payload)) return; - mutate(payload as OnboardingPayload, { + mutate(payload, { onSuccess: () => { router.replace(next); }, }); };이렇게 하면:
- 타입 시스템의 보호를 받으며 확장 가능
- 필수 필드 추가 시 타입 가드만 수정하면 됨
as캐스팅 불필요
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/app/auth/callback/page.tsxsrc/entities/user/api/onboarding.tssrc/entities/user/hooks/useMe.tssrc/features/auth/logout/model/useLogout.tssrc/features/onboarding/ui/OnboardingWizard.tsxsrc/features/user-profile/api/updateMe.tssrc/features/user-profile/hooks/useUpdateMe.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- src/features/user-profile/hooks/useUpdateMe.ts
- src/app/auth/callback/page.tsx
- src/entities/user/api/onboarding.ts
- src/features/auth/logout/model/useLogout.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*
⚙️ CodeRabbit configuration file
**/*: - 리뷰는 한국어로 작성.
- 유지보수/확장성 관점에서 모듈 경계(의존성 방향, 책임 분리)가 적절한지 최우선으로 확인.
- 파일/함수 책임이 과도하면 응집도/결합도 기준으로 분리/통합 개선안을 제시.
- TypeScript에서 any/과도한 as 캐스팅을 지양하고, 도메인 타입(예: ProfileId 같은 ID 타입)으로 의미를 드러내는지 확인.
- React에서 상태 위치 미스, 불필요한 리렌더링, derived state 남발, useEffect 남용을 지적하고 개선안을 제시.
Files:
src/features/onboarding/ui/OnboardingWizard.tsxsrc/features/user-profile/api/updateMe.tssrc/entities/user/hooks/useMe.ts
**/ui/**/*.tsx
⚙️ CodeRabbit configuration file
**/ui/**/*.tsx: - 상태 위치가 적절한지(상/하위 이동 필요성) 지적하고 개선안 제시.
- 파생 값은 state로 두지 말고 계산(useMemo는 필요할 때만).
- 리스트 key 안정성, 불필요한 re-render 유발 props(익명 함수/객체) 지적.
Files:
src/features/onboarding/ui/OnboardingWizard.tsx
**/api/**/*.ts*
⚙️ CodeRabbit configuration file
**/api/**/*.ts*: - API 모듈은 DTO 타입 + 네트워크 호출 + toDomain 매핑까지 책임지는 구조를 우선 권장.
- UI에서 raw DTO(res.json())를 직접 다루지 않도록, 반환 타입은 Domain(User 등)으로 정규화.
- fetch 옵션(credentials, headers 등) 규약이 일관적인지 확인.
Files:
src/features/user-profile/api/updateMe.ts
🧬 Code graph analysis (2)
src/features/onboarding/ui/OnboardingWizard.tsx (3)
src/entities/user/model/types.ts (1)
OnboardingPayload(7-12)src/entities/user/hooks/useCompleteOnboarding.ts (1)
useCompleteOnboarding(4-13)src/features/onboarding/ui/steps/NicknameStep.tsx (1)
NicknameStep(11-42)
src/features/user-profile/api/updateMe.ts (4)
src/features/user-profile/model/types.ts (1)
UpdateMePayload(1-4)src/entities/user/model/types.ts (1)
User(1-5)src/shared/api/config.ts (1)
API_BASE_URL(1-1)src/shared/api/api-error.ts (1)
ApiError(1-5)
🔇 Additional comments (5)
src/entities/user/hooks/useMe.ts (1)
11-11: 이미 올바른 패턴이 적용되어 있습니다.
staleTime: 0과invalidateQueries조합은 이미 구현되어 있습니다. 프로필 업데이트(useUpdateMe), 온보딩(useCompleteOnboarding), 로그아웃(useLogout) 시 모두queryClient.invalidateQueries({ queryKey: ["me"] })를 호출하고 있으므로, 권장되는 React Query 패턴을 따르고 있습니다.
staleTime: 0은 이 invalidateQueries 전략과 함께 사용되어 mutation 성공 후 데이터 신선도를 보장하는 의도적인 설정입니다.src/features/user-profile/api/updateMe.ts (2)
1-4: 임포트 구조가 적절합니다.의존성 방향(features → entities → shared)이 올바르고, 필요한 타입과 유틸리티를 명확하게 가져오고 있습니다.
6-15: fetch 옵션 일관성 확인 완료updateMe.ts의 fetch 옵션은 다른 API 모듈과 일관되게 구현되어 있습니다. credentials: "include"는 모든 인증이 필요한 API 호출에서 정상 사용 중이고, PATCH 메서드로 body를 전송하는 onboarding.ts의 패턴과 동일하게 Accept 및 Content-Type 헤더를 설정하고 있습니다.
다만 src/features/auth/logout/api/logout.ts의 logoutApi()는 다른 API 호출과 달리 headers를 지정하지 않고 있습니다. 일관성을 위해 logout 요청도 최소한 Accept 헤더를 포함하도록 수정하는 것을 권장합니다.
src/features/onboarding/ui/OnboardingWizard.tsx (2)
1-7: LGTM - 적절한 클라이언트 컴포넌트 구성"use client" 지시어와 필요한 의존성들이 올바르게 구성되어 있습니다.
24-29: LGTM - 클로저 문제 해결됨이전 리뷰에서 지적된 클로저 이슈가 올바르게 수정되었습니다.
updatedDraft를 명시적으로 생성하여 상태 업데이트와 제출에 동일한 값을 사용하고 있습니다.
| const submitAll = (payload: Partial<OnboardingPayload>) => { | ||
| if (!payload.nickname) return; | ||
|
|
||
| mutate(payload as OnboardingPayload, { | ||
| onSuccess: () => { | ||
| router.replace(next); | ||
| }, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# useCompleteOnboarding 훅의 에러 처리 구현 확인
fd -e ts -e tsx | xargs grep -l "useCompleteOnboarding" | head -5Repository: Queuing-org/front-end
Length of output: 161
🏁 Script executed:
# OnboardingWizard.tsx 파일 전체 확인
cat -n src/features/onboarding/ui/OnboardingWizard.tsxRepository: Queuing-org/front-end
Length of output: 1865
🏁 Script executed:
# 다른 mutation 사용 패턴에서 에러 처리 방법 확인 (onError 포함)
rg "mutate\(" -A 5 --type tsx src/ | grep -A 5 "onError"Repository: Queuing-org/front-end
Length of output: 92
🏁 Script executed:
# useCompleteOnboarding 훅 찾기
find src -name "*useCompleteOnboarding*" -o -name "*Complete*" | head -10Repository: Queuing-org/front-end
Length of output: 113
🏁 Script executed:
# useCompleteOnboarding 훅의 전체 구현 확인
cat -n src/entities/user/hooks/useCompleteOnboarding.tsRepository: Queuing-org/front-end
Length of output: 514
🏁 Script executed:
# 다른 useMutation 또는 mutation 패턴에서 onError 사용 현황 확인
rg "onError" -A 3 -B 2 src/ --type ts --type tsx | head -50Repository: Queuing-org/front-end
Length of output: 92
🏁 Script executed:
# 다른 mutation 패턴에서 onError 사용 현황 확인 (타입 옵션 수정)
rg "onError" -A 3 -B 2 src/ | head -80Repository: Queuing-org/front-end
Length of output: 47
🏁 Script executed:
# completeOnboarding API 함수 구현 확인
fd completeOnboarding -e ts -e tsxRepository: Queuing-org/front-end
Length of output: 47
🏁 Script executed:
# 다른 mutation 훅에서 에러 처리 방식 확인
rg "useMutation" -A 10 src/ | grep -A 10 "onError\|onSuccess" | head -60Repository: Queuing-org/front-end
Length of output: 1911
🏁 Script executed:
# mutate return type를 확인하기 위해 useCompleteOnboarding 호출처 확인
rg "const.*mutate.*=.*useCompleteOnboarding" -A 5 src/Repository: Queuing-org/front-end
Length of output: 590
🏁 Script executed:
# 컴포넌트에서 에러 상태 관리하는 패턴 확인
rg "error.*useState\|const.*error" src/features/ | grep -i "onboarding\|form" | head -20Repository: Queuing-org/front-end
Length of output: 47
🏁 Script executed:
# completeOnboarding API 함수 확인
find src -path "*api*onboarding*" -type fRepository: Queuing-org/front-end
Length of output: 100
🏁 Script executed:
# API 함수 구현 확인
cat -n src/entities/user/api/onboarding.tsRepository: Queuing-org/front-end
Length of output: 1084
🏁 Script executed:
# useCompleteOnboarding 훅에서 반환하는 전체 객체 확인
rg "useMutation" -A 15 src/entities/user/hooks/useCompleteOnboarding.tsRepository: Queuing-org/front-end
Length of output: 423
🏁 Script executed:
# React Query의 useMutation에서 error 상태를 받을 수 있는지 확인
# 다른 훅에서 error를 명시적으로 처리하는 패턴이 있는지 확인
rg "\.error\|error:" src/ --type ts --type tsx | head -20Repository: Queuing-org/front-end
Length of output: 92
뮤테이션 및 훅 수준에서 에러 처리 추가 필요
submitAll 함수에서 onError 핸들러가 없을 뿐만 아니라, useCompleteOnboarding 훅 자체도 에러 처리가 완전히 누락되어 있습니다.
API는 completeOnboarding (lines 23-26)에서 ApiError를 throw하지만, 훅에서 이를 처리하거나 노출하지 않아서 온보딩 실패 시 사용자 피드백이 전혀 제공되지 않습니다.
개선안:
useCompleteOnboarding훅에onError핸들러를 추가하거나- 컴포넌트에서 뮤테이션의
error상태를 추출하여 에러 메시지를 표시
현재 상황에서 네트워크 오류, 서버 에러 등이 발생해도 사용자는 아무 알림을 받지 못합니다.
🤖 Prompt for AI Agents
In src/features/onboarding/ui/OnboardingWizard.tsx around lines 31 to 39, the
submitAll mutation call lacks error handling and the useCompleteOnboarding hook
does not expose or handle errors, so failures produce no user feedback; update
the hook to forward mutation error state and accept an onError handler (or
return { mutate, error, isError } from the hook) and then add an onError
callback to the mutate options inside submitAll that captures the ApiError, sets
a local component error state (or uses a toast) and prevents silent failure,
ensuring network/server errors are surfaced to the user and optionally log them
for debugging.
Summary by CodeRabbit
릴리스 노트
새로운 기능
버그 수정
✏️ Tip: You can customize this high-level summary in your review settings.
Summary by CodeRabbit
새 기능
리팩토링
✏️ Tip: You can customize this high-level summary in your review settings.