Skip to content

[FEAT] 온보딩 페이지 API 연동 #36

Merged
KOJ50 merged 9 commits into
mainfrom
feature/#34-onboarding-api
Jun 22, 2026
Merged

[FEAT] 온보딩 페이지 API 연동 #36
KOJ50 merged 9 commits into
mainfrom
feature/#34-onboarding-api

Conversation

@KOJ50

@KOJ50 KOJ50 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

📢 PR 유형

어떤 변경 사항이 있었나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

📌 관련 이슈번호


✅ Key Changes

  • 로그인 API를 연동 및 로그인 성공 시 access token과 userRole을 쿠키에 저장
  • 로그인 응답의 userType에 따라 자동 이동하는 라우팅 흐름
  • 공통 API 클라이언트를 추가, 인증 요청에 Authorization 헤더를 자동 주입
  • 401 응답 발생 시 access token 재발급을 시도, 실패 시 클라이언트 인증 정보를 초기화
  • 디자이너 회원가입 시, 포트폴리오 파일 presigned URL 발급 및 업로드 후 회원가입 요청 전송
  • 아이디 중복 확인, 이메일 인증번호 요청/검증 API 실제 서버 연동 방식으로 교체
  • mock 데이터 및 mock 인증 로직을 제거 및 zod/react-hook-form 기반 유효성 검증
  • 회원가입 단계 이동 시 입력한 정보 유지
  • 로그인된 사용자가 /login, /signup에 접근 제한
  • 로그아웃 API를 연동 및 사이드바 로그아웃 버튼에서 쿠키 초기화 및 홈 이동

📸 스크린샷 or 실행영상

2026-06-22.18.15.25.mov

🎸 기타 사항 or 추가 코멘트

기존에 요청했던 플로우는 계속 api를 호출해서 문제가 발생할 가능성이 있어 확인 버튼으로 반영(디자인 팀 노티 필요)
image

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 로그인 페이지 개선 및 입력 검증 강화
    • 이메일 인증을 포함한 새로운 회원가입 프로세스
    • 디자이너 회원가입 시 포트폴리오 파일 업로드 기능 추가
    • 로그아웃 기능 구현
    • 사용자 역할에 따른 자동 페이지 리디렉션
  • Refactor

    • API 클라이언트 인증 토큰 자동 갱신 시스템

@KOJ50 KOJ50 self-assigned this Jun 22, 2026
@KOJ50 KOJ50 added 📢 API 서버 API 연동 🍀 오진 labels Jun 22, 2026
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e66a81ec-bfc0-4c24-8409-56663b90ceac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

ky, zod, react-hook-form 기반의 공유 API 클라이언트와 클라이언트 쿠키 인증 유틸을 신규 구축하고, 로그인·회원가입(강사/디자이너)·로그아웃 기능을 실제 API 호출로 전환했습니다. Next.js 미들웨어에 게스트 전용 경로 리다이렉트 로직이 추가되었습니다.

Changes

온보딩 API 연동 (로그인·회원가입·로그아웃)

Layer / File(s) Summary
공유 API 클라이언트 및 인증 유틸 구축
src/shared/api/types.ts, src/shared/api/client.ts, src/shared/lib/auth/client.ts, package.json
ApiResponse<T>, ApiError, toApiError, getApiResponseMessage를 정의하고 ky 기반 publicApi/api 인스턴스를 추가했습니다. apibeforeRequest에서 Bearer 토큰을 주입하고 afterResponse에서 401 시 토큰 재발급 후 재시도합니다. 클라이언트 쿠키 기반 JWT 유틸(setClientAuth, clearClientAuth, normalizeClientUserRole, getClientUserHomePath)도 함께 추가되었습니다.
미들웨어 게스트 경로 리다이렉트 및 역할 파싱 확장
src/proxy.ts
GUEST_ONLY_PATHS를 추가하고, 인증된 사용자가 /login·/signup에 접근하면 역할 홈 경로로 리다이렉트합니다. JWT 역할 파싱이 payload.role 또는 payload.userType을 모두 지원하도록 확장되었고, config.matcher에 두 경로가 추가되었습니다.
로그인 스키마·훅·API·페이지 연동
src/features/login/model/loginSchemas.ts, src/features/login/model/useLoginForm.ts, src/features/login/api/login.ts, src/features/login/index.ts, src/app/login/page.tsx
loginFormSchema(Zod), loginResultSchema, login API 함수, useLoginForm 훅을 신규 구현했습니다. 로그인 페이지는 <form onSubmit>type="submit" 버튼으로 재구성되고, 훅이 제출 중 상태·에러 메시지·역할 기반 라우팅을 처리합니다.
로그아웃 API·훅·UI 및 SidebarMenu 확장
src/features/auth/api/logout.ts, src/features/auth/model/useLogout.ts, src/features/auth/ui/LogoutSidebarMenu.tsx, src/features/auth/index.ts, src/shared/ui/SidebarMenu.tsx, src/app/designer/layout.tsx, src/app/instructor/layout.tsx
logout API, useLogout 훅, LogoutSidebarMenu 컴포넌트를 신규 추가했습니다. SidebarMenudisabled·onClick props가 추가되어 href 없이 버튼으로 렌더링할 수 있게 되었습니다. 디자이너·강사 레이아웃의 사이드바 하단이 LogoutSidebarMenu로 교체되었습니다.
회원가입 Zod 스키마 및 약관 ID 상수 정비
src/features/signup/model/signupSchemas.ts, src/features/signup/model/signup.ts, src/features/signup/config/signup.ts
약관·프로필·계정·디자이너 추가 입력 스키마를 Zod로 정의하고 인퍼드 타입을 내보냈습니다. 약관 id"1"~"4"에서 "SERVICE", "USERINFO", "SETTLEMENT", "DISINTERMEDIATION" 등 의미 있는 값으로 교체되고 목(mock) 상수가 제거되었습니다.
회원가입 API 함수 구현
src/features/signup/api/signup.ts, src/features/signup/index.ts
이메일 인증 코드 발급/검증, 사용자명 중복 체크, 인스트럭터 가입, 디자이너 가입(presigned URL 포트폴리오 업로드 포함) API 함수를 추가했습니다. unwrapApiResponse 헬퍼로 성공 여부를 검사하고 Zod로 결과를 파싱합니다.
useSignupStep2Form react-hook-form 전환
src/features/signup/model/useSignupStep2Form.ts
로컬 useState 기반 폼에서 react-hook-form + zodResolver 기반으로 전환했습니다. 아이디 중복 확인·이메일 인증 요청·인증코드 검증이 실제 API 호출 기반 비동기 상태 머신으로 교체되었습니다. 이메일 인증 상태가 idle → sending → sent → verifying → verified 흐름으로 확장되었습니다.
회원가입 UI 컴포넌트 props 및 SignupFunnel 데이터 흐름
src/features/signup/ui/TermsProfileStep.tsx, src/features/signup/ui/AccountStep.tsx, src/features/signup/ui/DesignerAdditionalStep.tsx, src/widgets/signup/ui/SignupFunnel.tsx
TermsProfileStep·AccountStep·DesignerAdditionalSteponNext/onSubmit이 검증된 데이터를 전달하도록 시그니처가 변경되었습니다. SignupFunnel은 각 단계 데이터를 수집하고 디자이너 최종 제출 시 signupDesigner 호출 후 setClientAuth로 인증을 설정하고 역할 홈 경로로 이동합니다.
Header 인증 상태 조건 조정
src/shared/ui/Header.tsx
isLoggedIn 판단 조건이 accessToken != null만으로 단순화되고, role이 null일 때 accountHref/login에서 /로 변경되었습니다.

Sequence Diagram(s)

sequenceDiagram
  participant User as 사용자
  participant LoginPage as LoginPage
  participant useLoginForm
  participant loginAPI as login()
  participant APIServer as API 서버
  participant AuthCookie as 클라이언트 쿠키

  User->>LoginPage: 아이디/비밀번호 입력 후 제출
  LoginPage->>useLoginForm: onSubmit(formValues)
  useLoginForm->>loginAPI: login({ username, password })
  loginAPI->>APIServer: POST /api/v1/auth/login
  APIServer-->>loginAPI: { success, result: { accessToken, userType, ... } }
  loginAPI-->>useLoginForm: LoginResult
  useLoginForm->>AuthCookie: setClientAuth({ accessToken, role })
  useLoginForm->>LoginPage: router.push(getClientUserHomePath(role))
Loading
sequenceDiagram
  participant SignupFunnel
  participant TermsProfileStep
  participant AccountStep
  participant DesignerAdditionalStep
  participant signupDesigner
  participant AuthCookie as 클라이언트 쿠키

  SignupFunnel->>TermsProfileStep: terms, initialData
  TermsProfileStep-->>SignupFunnel: handleProfileNext(SignupProfileData)
  SignupFunnel->>AccountStep: role, profileData, initialData
  AccountStep-->>SignupFunnel: handleAccountNext(SignupAccountData)
  SignupFunnel->>DesignerAdditionalStep: initialData
  DesignerAdditionalStep-->>SignupFunnel: handleDesignerSubmit(SignupDesignerAdditionalData)
  SignupFunnel->>signupDesigner: { profile, account, additional }
  signupDesigner-->>SignupFunnel: DesignerSignupResult
  SignupFunnel->>AuthCookie: setClientAuth({ accessToken, role })
  SignupFunnel->>SignupFunnel: router.push(getClientUserHomePath(role))
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~100 minutes

Possibly related PRs

  • Ditda-Official/Ditda-Frontend#22: 로그인 페이지 UI를 구현한 PR로, 이번 PR에서 해당 페이지를 useLoginForm 훅 기반의 실제 API 제출 흐름으로 전환했습니다.
  • Ditda-Official/Ditda-Frontend#35: 회원가입 퍼널 컴포넌트(AccountStep, DesignerAdditionalStep, TermsProfileStep, SignupFunnel)와 src/proxy.ts, src/shared/ui/Header.tsx를 동일하게 수정하는 PR로, 이번 PR의 API 연동 변경과 직접적으로 연관됩니다.

Suggested labels

✨ Feature UI

Suggested reviewers

  • waldls
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목 '[FEAT] 온보딩 페이지 API 연동'은 로그인/회원가입 API 통합이라는 변경 사항의 주요 목적을 명확하게 요약합니다.
Linked Issues check ✅ Passed 모든 링크된 이슈 요구사항이 충족되었습니다: 로그인 페이지 API 연동, 강사 회원가입 API 연동, 디자이너 회원가입 API 연동이 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 링크된 이슈 #34의 온보딩 API 연동 범위 내에 있으며, 이와 무관한 변경은 발견되지 않았습니다.
Description check ✅ Passed PR에 명확한 설명이 포함되어 있으며, 로그인/회원가입 API 연동의 주요 변경사항, 구현 세부사항, 토큰 관리, 파일 업로드 처리 등이 잘 문서화되어 있습니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/#34-onboarding-api

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.

@github-actions

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (8)
src/features/signup/api/signup.ts (2)

18-34: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

instructorSignupResultSchemadesignerSignupResultSchema가 동일합니다.

두 스키마의 구조가 완전히 같으므로 공통 스키마로 추출하여 중복을 제거할 수 있습니다.

♻️ 스키마 통합 제안
-const instructorSignupResultSchema = z.object({
-  userId: z.number(),
-  userType: z.string(),
-  name: z.string(),
-  profileImageUrl: z.string(),
-  accessToken: z.string(),
-});
-
-export type InstructorSignupResult = z.infer<typeof instructorSignupResultSchema>;
-
-const designerSignupResultSchema = z.object({
-  userId: z.number(),
-  userType: z.string(),
-  name: z.string(),
-  profileImageUrl: z.string(),
-  accessToken: z.string(),
-});
+const signupResultSchema = z.object({
+  userId: z.number(),
+  userType: z.string(),
+  name: z.string(),
+  profileImageUrl: z.string(),
+  accessToken: z.string(),
+});
+
+export type InstructorSignupResult = z.infer<typeof signupResultSchema>;
+export type DesignerSignupResult = z.infer<typeof signupResultSchema>;
🤖 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 `@src/features/signup/api/signup.ts` around lines 18 - 34, The
instructorSignupResultSchema and designerSignupResultSchema have identical field
definitions which creates unnecessary code duplication. Extract a single shared
schema containing the common fields (userId, userType, name, profileImageUrl,
accessToken), then use this shared schema to define both InstructorSignupResult
and DesignerSignupResult types. This eliminates the duplicate schema definitions
while maintaining the same type exports.

141-159: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

presigned URL 업로드 실패 시 재시도 로직이 없습니다.

S3 presigned URL 업로드는 네트워크 일시적 오류로 실패할 수 있습니다. 현재 구현은 단일 시도 후 바로 실패합니다. 중요한 파일 업로드이므로 재시도 로직 추가를 고려해 보세요.

🤖 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 `@src/features/signup/api/signup.ts` around lines 141 - 159, The
uploadPortfolioFile function lacks retry logic for handling transient network
failures during the S3 presigned URL upload. Implement exponential backoff retry
logic in the uploadPortfolioFile function to automatically retry the fetch
request when network errors occur or when the response is not ok. Define a
reasonable retry count (typically 2-3 attempts) and only throw the ApiError
after all retry attempts have been exhausted. This ensures the upload has a
better chance of succeeding despite temporary network hiccups without changing
the function signature or error handling contract.
src/features/signup/model/useSignupStep2Form.ts (2)

240-246: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

validateAndGetAccountData가 validation 실패 원인을 구분하지 않습니다.

isFormValid, isUserIdAvailable, isEmailVerified 중 어느 조건이 실패했는지 알 수 없어 호출자가 적절한 피드백을 제공하기 어렵습니다. 현재 UI에서는 버튼 활성화 상태로 미리 제어하므로 문제없지만, 향후 개선 시 고려해 보세요.

🤖 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 `@src/features/signup/model/useSignupStep2Form.ts` around lines 240 - 246, The
validateAndGetAccountData function returns null without distinguishing which
validation condition failed among isFormValid, isUserIdAvailable, or
isEmailVerified, making it difficult for callers to provide specific feedback.
Refactor the return type of validateAndGetAccountData to return an object that
includes both the account data and validation status details (such as which
condition failed), rather than just returning null on any failure, so the caller
can determine the specific reason for validation failure.

71-71: 🧹 Nitpick | 🔵 Trivial

useWatch 호출 시 명시적 defaultValue 지정 권장

useWatch({ control })에서 타입 단언을 통해 SignupAccountFormValues로 처리하고 있습니다. useFormdefaultValues가 모든 필드를 제공하므로 실무에서는 안전하지만, 스키마 변경 시 타입 안전성을 보장하기 위해 다음과 같이 개선하는 것이 좋습니다:

const values = useWatch({ 
  control,
  defaultValue: {
    email: "",
    password: "",
    passwordConfirm: "",
    username: "",
    verificationCode: "",
  }
}) as SignupAccountFormValues;

또는 타입 단언 없이 더 안전하게:

const values = useWatch<SignupAccountFormValues>({ control });
🤖 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 `@src/features/signup/model/useSignupStep2Form.ts` at line 71, The useWatch
hook call on the values variable is using a type assertion as
SignupAccountFormValues without explicitly specifying a defaultValue parameter.
To improve type safety and handle potential schema changes, replace the type
assertion with either an explicit defaultValue object containing all required
fields (email, password, passwordConfirm, username, verificationCode) in the
useWatch configuration, or use a generic type parameter approach by specifying
useWatch<SignupAccountFormValues>({ control }) without the type assertion.
Choose the generic type parameter approach for cleaner code and better type
inference.
src/features/signup/model/signupSchemas.ts (2)

29-33: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

phone 스키마의 .max() 제약이 중복됩니다.

regex /^\d{10,11}$/가 이미 10-11자리로 길이를 제한하므로 .max(SIGNUP_MAX_PHONE_NUMBER_LENGTH)는 불필요합니다. 단, 가독성 또는 명시적 문서화 목적이라면 유지해도 무방합니다.

🤖 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 `@src/features/signup/model/signupSchemas.ts` around lines 29 - 33, The phone
schema in the signupSchemas has a redundant `.max()` constraint that duplicates
validation already performed by the regex pattern. The regex `/^\d{10,11}$/`
already restricts the phone number to 10-11 digits, making the
`.max(SIGNUP_MAX_PHONE_NUMBER_LENGTH)` call unnecessary. Remove the redundant
`.max()` method call from the phone field validation chain to clean up the
schema, unless explicit documentation or readability justifies keeping it.

58-67: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

z.custom 사용이 적절하나, 에러 메시지 커스터마이징을 고려해 보세요.

z.custom은 validation 실패 시 기본 에러 메시지가 불명확합니다. Zod 4에서는 error 옵션으로 명확한 메시지를 제공할 수 있습니다.

♻️ 에러 메시지 추가 예시
 export const signupDesignerAdditionalSchema = z.object({
   bankCode: z.custom<(typeof BANK_OPTIONS)[number]["code"]>(
     value => typeof value === "string" && BANK_OPTIONS.some(({ code }) => code === value),
+    { error: "유효한 은행을 선택해주세요" },
   ),
   accountNumber: z.string().trim().min(1),
   accountHolder: z.string().trim().min(1),
   portfolioFiles: z
     .array(z.custom<File>(value => typeof File !== "undefined" && value instanceof File))
     .max(3),
 });
🤖 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 `@src/features/signup/model/signupSchemas.ts` around lines 58 - 67, The
z.custom validators in the signupDesignerAdditionalSchema schema lack clear
error messages which makes validation failures confusing. Add the error option
parameter to both z.custom calls in the bankCode field and the portfolioFiles
array validation to provide descriptive error messages. For the bankCode
validation, specify an error message indicating it must be a valid bank code
from the available options. For the portfolioFiles File instance check, specify
an error message indicating that each item must be a valid File object. This
will help users understand validation failures more clearly.
src/widgets/signup/ui/SignupFunnel.tsx (1)

67-73: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

디자이너 회원가입 완료 시 moveNext/login 경로가 도달하지 않습니다.

handleDesignerSubmit에서 router.push(getClientUserHomePath(userRole))로 직접 이동하므로, moveNext/login 폴백은 실행되지 않습니다. 이 코드가 의도된 것인지 확인이 필요합니다.

현재 흐름:

  • 강사: AccountStephandleInstructorSignup 성공 → onNextmoveNext/instructor
  • 디자이너: DesignerAdditionalStephandleDesignerSubmitrouter.push(homePath) (moveNext 호출 안함)

만약 moveNext의 디자이너 경로가 불필요하다면 제거하거나, 의도된 폴백이라면 주석을 추가해주세요.

🤖 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 `@src/widgets/signup/ui/SignupFunnel.tsx` around lines 67 - 73, The routing
logic in the `moveNext` function currently defaults to `/login` for
non-instructor roles, but the designer signup flow bypasses this by calling
`router.push(homePath)` directly in `handleDesignerSubmit`, making the `/login`
fallback unreachable for designers. Update the condition in `moveNext` where
`router.push` is called to explicitly handle all roles (instructor should
navigate to "/instructor" and designer should navigate to its appropriate home
path using `getClientUserHomePath`), or alternatively add a clear comment
explaining why designers intentionally skip this `moveNext` logic. This ensures
the routing behavior is explicit and intentional for all user roles.
src/shared/ui/SidebarMenu.tsx (1)

39-40: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

조건부 className 결합은 cn() 유틸로 통일해주세요.

현재 문자열 덧붙이기 방식은 Tailwind 조건 확장 시 가독성과 유지보수성이 떨어집니다. 팀 규칙대로 cn() 기반 조합으로 맞추는 게 좋습니다.

리팩터링 예시
-  const className =
-    "bg-gray-5 rounded-8 hover:bg-gray-20 group block w-58 cursor-pointer px-5 py-3 text-left transition-colors duration-150 disabled:cursor-not-allowed disabled:opacity-60" +
-    (isSelected ? " bg-gray-20" : "");
+  const className = cn(
+    "bg-gray-5 rounded-8 hover:bg-gray-20 group block w-58 cursor-pointer px-5 py-3 text-left transition-colors duration-150 disabled:cursor-not-allowed disabled:opacity-60",
+    isSelected && "bg-gray-20",
+  );

As per coding guidelines, "TailwindCSS - 조건부 클래스 문자열을 직접 이어붙이는 경우 cn() 유틸리티 사용을 제안해주세요."

🤖 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 `@src/shared/ui/SidebarMenu.tsx` around lines 39 - 40, The className assignment
in SidebarMenu.tsx uses string concatenation with the + operator to combine base
Tailwind classes with a conditional class based on the isSelected state. Replace
this string concatenation approach with the cn() utility function by passing the
base class string and the conditional class (using a ternary operator or object
syntax) as separate arguments to cn(). This improves code readability and
maintainability by following the team's coding guidelines for conditional
className combining.

Source: Coding guidelines

🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/features/signup/api/signup.ts`:
- Around line 43-62: The `fallbackMessage` parameter in the `unwrapApiResponse`
function is declared but never used in the function body, making it an unused
parameter. Either remove the `fallbackMessage` parameter from the function
signature and all call sites where it is passed, or utilize it by passing it to
the `getApiResponseMessage` call or using it as a fallback in error message
construction when an API response error occurs.

In `@src/features/signup/config/signup.ts`:
- Around line 100-104: The USERINFO configuration object in the signup
configuration has an inconsistency between the label property (which is "강사 약관
2") and the modalTitle property (which is "강사 약관 제목 3"). Update the modalTitle
value from "강사 약관 제목 3" to "강사 약관 제목 2" to match the corresponding label and
maintain consistency across the configuration.

In `@src/features/signup/ui/AccountStep.tsx`:
- Around line 34-54: The handleNext function silently returns without user
feedback when profileData is null or when userRole normalization fails (when
role is "instructor"). Add error messaging or toast notifications to inform the
user when these validation failures occur. Specifically, display an appropriate
error message when profileData is null before calling
form.handleInstructorSignup and when normalizeClientUserRole returns null, so
users understand why the signup flow is not proceeding.

In `@src/features/signup/ui/DesignerAdditionalStep.tsx`:
- Around line 40-43: The useUploadedFiles hook on line 43 is not being passed
the initialData to restore previously uploaded portfolio files, while other form
fields like bankCode, accountNumber, and accountHolder are properly initialized
from initialData on lines 40-42. Modify the useUploadedFiles hook invocation to
pass initialData?.portfolioFiles as a parameter to restore the files when users
navigate back to this step, ensuring consistency with how other form fields are
being initialized.

In `@src/shared/api/client.ts`:
- Around line 76-86: The reissueAccessToken function and related token refresh
logic has a race condition where multiple simultaneous 401 requests can trigger
duplicate token reissue operations, and one failed reissue can call
clearClientAuth() to invalidate a successful reissue from a parallel request.
Implement a single-flight pattern by storing the token reissue Promise in a
module-level variable. When reissueAccessToken is called, check if a reissue is
already in progress and return the existing Promise instead of starting a new
one. Clear the stored Promise only after the reissue completes (either
successfully or with error). This ensures all concurrent requests share a single
reissue operation, preventing the clearClientAuth() call from invalidating a
successful reissue completed by another request.
- Around line 171-174: The fallback case in the error handler is returning an
empty string as the ApiError message, which provides no guidance to users when
an unknown exception occurs. Replace the empty string with a meaningful fallback
message such as a common error phrase like "An unknown error occurred" or
similar contextual text. This ensures that even when the error is not an
instance of Error, users receive helpful error guidance instead of seeing
nothing.

In `@src/shared/ui/Header.tsx`:
- Line 56: The header's login authentication check is inconsistent with the
middleware authentication logic in src/proxy.ts. Currently, the isLoggedIn
property only verifies that accessToken is not null, but the middleware also
validates that the user's role can be successfully interpreted. When role
interpretation fails in the middleware, the user is treated as unauthenticated,
which causes a mismatch where the header shows the user as logged in while the
middleware rejects the request, breaking the recovery flow. Update the
isLoggedIn check in the Header component to include the same role/permissions
validation logic that the proxy middleware uses, ensuring both checks use
identical authentication criteria. This applies to all authentication checks in
the Header component.

---

Nitpick comments:
In `@src/features/signup/api/signup.ts`:
- Around line 18-34: The instructorSignupResultSchema and
designerSignupResultSchema have identical field definitions which creates
unnecessary code duplication. Extract a single shared schema containing the
common fields (userId, userType, name, profileImageUrl, accessToken), then use
this shared schema to define both InstructorSignupResult and
DesignerSignupResult types. This eliminates the duplicate schema definitions
while maintaining the same type exports.
- Around line 141-159: The uploadPortfolioFile function lacks retry logic for
handling transient network failures during the S3 presigned URL upload.
Implement exponential backoff retry logic in the uploadPortfolioFile function to
automatically retry the fetch request when network errors occur or when the
response is not ok. Define a reasonable retry count (typically 2-3 attempts) and
only throw the ApiError after all retry attempts have been exhausted. This
ensures the upload has a better chance of succeeding despite temporary network
hiccups without changing the function signature or error handling contract.

In `@src/features/signup/model/signupSchemas.ts`:
- Around line 29-33: The phone schema in the signupSchemas has a redundant
`.max()` constraint that duplicates validation already performed by the regex
pattern. The regex `/^\d{10,11}$/` already restricts the phone number to 10-11
digits, making the `.max(SIGNUP_MAX_PHONE_NUMBER_LENGTH)` call unnecessary.
Remove the redundant `.max()` method call from the phone field validation chain
to clean up the schema, unless explicit documentation or readability justifies
keeping it.
- Around line 58-67: The z.custom validators in the
signupDesignerAdditionalSchema schema lack clear error messages which makes
validation failures confusing. Add the error option parameter to both z.custom
calls in the bankCode field and the portfolioFiles array validation to provide
descriptive error messages. For the bankCode validation, specify an error
message indicating it must be a valid bank code from the available options. For
the portfolioFiles File instance check, specify an error message indicating that
each item must be a valid File object. This will help users understand
validation failures more clearly.

In `@src/features/signup/model/useSignupStep2Form.ts`:
- Around line 240-246: The validateAndGetAccountData function returns null
without distinguishing which validation condition failed among isFormValid,
isUserIdAvailable, or isEmailVerified, making it difficult for callers to
provide specific feedback. Refactor the return type of validateAndGetAccountData
to return an object that includes both the account data and validation status
details (such as which condition failed), rather than just returning null on any
failure, so the caller can determine the specific reason for validation failure.
- Line 71: The useWatch hook call on the values variable is using a type
assertion as SignupAccountFormValues without explicitly specifying a
defaultValue parameter. To improve type safety and handle potential schema
changes, replace the type assertion with either an explicit defaultValue object
containing all required fields (email, password, passwordConfirm, username,
verificationCode) in the useWatch configuration, or use a generic type parameter
approach by specifying useWatch<SignupAccountFormValues>({ control }) without
the type assertion. Choose the generic type parameter approach for cleaner code
and better type inference.

In `@src/shared/ui/SidebarMenu.tsx`:
- Around line 39-40: The className assignment in SidebarMenu.tsx uses string
concatenation with the + operator to combine base Tailwind classes with a
conditional class based on the isSelected state. Replace this string
concatenation approach with the cn() utility function by passing the base class
string and the conditional class (using a ternary operator or object syntax) as
separate arguments to cn(). This improves code readability and maintainability
by following the team's coding guidelines for conditional className combining.

In `@src/widgets/signup/ui/SignupFunnel.tsx`:
- Around line 67-73: The routing logic in the `moveNext` function currently
defaults to `/login` for non-instructor roles, but the designer signup flow
bypasses this by calling `router.push(homePath)` directly in
`handleDesignerSubmit`, making the `/login` fallback unreachable for designers.
Update the condition in `moveNext` where `router.push` is called to explicitly
handle all roles (instructor should navigate to "/instructor" and designer
should navigate to its appropriate home path using `getClientUserHomePath`), or
alternatively add a clear comment explaining why designers intentionally skip
this `moveNext` logic. This ensures the routing behavior is explicit and
intentional for all user roles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: de0c4fdc-c731-4954-989a-fa5e5808528c

📥 Commits

Reviewing files that changed from the base of the PR and between 9c4101c and 307d721.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • package.json
  • src/app/designer/layout.tsx
  • src/app/instructor/layout.tsx
  • src/app/login/page.tsx
  • src/features/auth/api/logout.ts
  • src/features/auth/index.ts
  • src/features/auth/model/useLogout.ts
  • src/features/auth/ui/LogoutSidebarMenu.tsx
  • src/features/login/api/login.ts
  • src/features/login/index.ts
  • src/features/login/model/loginSchemas.ts
  • src/features/login/model/useLoginForm.ts
  • src/features/signup/api/signup.ts
  • src/features/signup/config/signup.ts
  • src/features/signup/index.ts
  • src/features/signup/model/signup.ts
  • src/features/signup/model/signupSchemas.ts
  • src/features/signup/model/useSignupStep2Form.ts
  • src/features/signup/ui/AccountStep.tsx
  • src/features/signup/ui/DesignerAdditionalStep.tsx
  • src/features/signup/ui/TermsProfileStep.tsx
  • src/proxy.ts
  • src/shared/api/client.ts
  • src/shared/api/types.ts
  • src/shared/lib/auth/client.ts
  • src/shared/ui/Header.tsx
  • src/shared/ui/SidebarMenu.tsx
  • src/widgets/signup/ui/SignupFunnel.tsx

Comment on lines +43 to +62
const unwrapApiResponse = async <T>(
request: Promise<ApiResponse<unknown>>,
resultSchema: z.ZodType<T>,
fallbackMessage?: string,
) => {
try {
const response = await request;

if (!response.success) {
throw new ApiError(getApiResponseMessage(response), {
code: response.code,
response,
});
}

return resultSchema.parse(response.result);
} catch (error) {
throw await toApiError(error);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

fallbackMessage 매개변수가 선언되었지만 사용되지 않습니다.

unwrapApiResponse의 세 번째 매개변수 fallbackMessage가 함수 내부에서 사용되지 않아 Line 207에서 전달된 값이 무시됩니다.

🐛 fallbackMessage 활용 제안
 const unwrapApiResponse = async <T>(
   request: Promise<ApiResponse<unknown>>,
   resultSchema: z.ZodType<T>,
   fallbackMessage?: string,
 ) => {
   try {
     const response = await request;

     if (!response.success) {
       throw new ApiError(getApiResponseMessage(response), {
         code: response.code,
         response,
       });
     }

     return resultSchema.parse(response.result);
   } catch (error) {
-    throw await toApiError(error);
+    throw await toApiError(error, fallbackMessage);
   }
 };

또는 fallbackMessage를 사용하지 않는다면 매개변수와 Line 207의 인자를 제거하세요.

🤖 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 `@src/features/signup/api/signup.ts` around lines 43 - 62, The
`fallbackMessage` parameter in the `unwrapApiResponse` function is declared but
never used in the function body, making it an unused parameter. Either remove
the `fallbackMessage` parameter from the function signature and all call sites
where it is passed, or utilize it by passing it to the `getApiResponseMessage`
call or using it as a fallback in error message construction when an API
response error occurs.

Comment on lines +100 to +104
id: "USERINFO",
label: "강사 약관 2",
modalTitle: "강사 약관 제목 3",
content: "강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2",
version: "V1.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

USERINFO 항목의 modalTitle이 "강사 약관 제목 3"으로 되어 있습니다.

label은 "강사 약관 2"인데 modalTitle은 "강사 약관 제목 3"으로 되어 있어 불일치합니다. "강사 약관 제목 2"가 되어야 할 것 같습니다.

🔧 수정 제안
   {
     id: "USERINFO",
     label: "강사 약관 2",
-    modalTitle: "강사 약관 제목 3",
+    modalTitle: "강사 약관 제목 2",
     content: "강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2",
     version: "V1.0",
   },
📝 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
id: "USERINFO",
label: "강사 약관 2",
modalTitle: "강사 약관 제목 3",
content: "강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2",
version: "V1.0",
id: "USERINFO",
label: "강사 약관 2",
modalTitle: "강사 약관 제목 2",
content: "강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2",
version: "V1.0",
🤖 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 `@src/features/signup/config/signup.ts` around lines 100 - 104, The USERINFO
configuration object in the signup configuration has an inconsistency between
the label property (which is "강사 약관 2") and the modalTitle property (which is
"강사 약관 제목 3"). Update the modalTitle value from "강사 약관 제목 3" to "강사 약관 제목 2" to
match the corresponding label and maintain consistency across the configuration.

Comment on lines +34 to +54
const handleNext = async () => {
const accountData = await form.validateAndGetAccountData();

if (accountData == null) return;

if (role === "instructor") {
if (profileData == null) return;

const result = await form.handleInstructorSignup(profileData, accountData);

if (result == null) return;

const userRole = normalizeClientUserRole(result.userType);

if (userRole == null) return;

setClientAuth({ accessToken: result.accessToken, role: userRole });
}

onNext(accountData);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

profileData가 없거나 userRole 정규화 실패 시 사용자에게 피드백이 없습니다.

role === "instructor"일 때 profileData == null이거나 userRole == null인 경우 조용히 반환되어 사용자가 왜 진행되지 않는지 알 수 없습니다. 에러 메시지를 표시하는 것이 좋겠습니다.

🛡️ 개선 제안
   const handleNext = async () => {
     const accountData = await form.validateAndGetAccountData();
 
     if (accountData == null) return;
 
     if (role === "instructor") {
-      if (profileData == null) return;
+      if (profileData == null) {
+        form.setSubmitErrorMessage?.("프로필 정보를 확인할 수 없습니다");
+        return;
+      }
 
       const result = await form.handleInstructorSignup(profileData, accountData);
 
       if (result == null) return;
 
       const userRole = normalizeClientUserRole(result.userType);
 
-      if (userRole == null) return;
+      if (userRole == null) {
+        form.setSubmitErrorMessage?.("사용자 유형을 확인할 수 없습니다");
+        return;
+      }
 
       setClientAuth({ accessToken: result.accessToken, role: userRole });
     }
 
     onNext(accountData);
   };
📝 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
const handleNext = async () => {
const accountData = await form.validateAndGetAccountData();
if (accountData == null) return;
if (role === "instructor") {
if (profileData == null) return;
const result = await form.handleInstructorSignup(profileData, accountData);
if (result == null) return;
const userRole = normalizeClientUserRole(result.userType);
if (userRole == null) return;
setClientAuth({ accessToken: result.accessToken, role: userRole });
}
onNext(accountData);
};
const handleNext = async () => {
const accountData = await form.validateAndGetAccountData();
if (accountData == null) return;
if (role === "instructor") {
if (profileData == null) {
form.setSubmitErrorMessage?.("프로필 정보를 확인할 수 없습니다");
return;
}
const result = await form.handleInstructorSignup(profileData, accountData);
if (result == null) return;
const userRole = normalizeClientUserRole(result.userType);
if (userRole == null) {
form.setSubmitErrorMessage?.("사용자 유형을 확인할 수 없습니다");
return;
}
setClientAuth({ accessToken: result.accessToken, role: userRole });
}
onNext(accountData);
};
🤖 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 `@src/features/signup/ui/AccountStep.tsx` around lines 34 - 54, The handleNext
function silently returns without user feedback when profileData is null or when
userRole normalization fails (when role is "instructor"). Add error messaging or
toast notifications to inform the user when these validation failures occur.
Specifically, display an appropriate error message when profileData is null
before calling form.handleInstructorSignup and when normalizeClientUserRole
returns null, so users understand why the signup flow is not proceeding.

Comment on lines +40 to 43
const [selectedBank, setSelectedBank] = useState<BankCode | null>(initialData?.bankCode ?? null);
const [accountNumber, setAccountNumber] = useState(initialData?.accountNumber ?? "");
const [accountHolder, setAccountHolder] = useState(initialData?.accountHolder ?? "");
const { uploadedFiles, handleFilesAdded, handleRemove } = useUploadedFiles();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# useUploadedFiles 훅의 초기값 지원 여부 확인
ast-grep --pattern $'const useUploadedFiles = ($$$) => {
  $$$
}'

Repository: Ditda-Official/Ditda-Frontend

Length of output: 3364


🏁 Script executed:

# Check the DesignerAdditionalStep.tsx file to see initialData structure and hook call
head -60 src/features/signup/ui/DesignerAdditionalStep.tsx

Repository: Ditda-Official/Ditda-Frontend

Length of output: 2128


🏁 Script executed:

# Find and check the SignupDesignerAdditionalData type definition
rg "type SignupDesignerAdditionalData" -A 15

Repository: Ditda-Official/Ditda-Frontend

Length of output: 2488


🏁 Script executed:

# Check the signupDesignerAdditionalSchema definition
rg "signupDesignerAdditionalSchema" -B 5 -A 15 src/features/signup/model/signupSchemas.ts

Repository: Ditda-Official/Ditda-Frontend

Length of output: 1047


🏁 Script executed:

# Check the handleSubmit and how portfolioFiles are handled in submission
sed -n '60,120p' src/features/signup/ui/DesignerAdditionalStep.tsx

Repository: Ditda-Official/Ditda-Frontend

Length of output: 2159


useUploadedFiles() 호출 시 initialData?.portfolioFiles를 전달하지 않아 파일 상태가 복원되지 않습니다.

다른 필드들(bankCode, accountNumber, accountHolder)은 initialData로부터 초기값이 설정되지만(40-42줄), 포트폴리오 파일은 복원되지 않습니다. useUploadedFiles 훅이 externalFiles 매개변수를 지원하므로, 다음과 같이 수정하세요:

const { uploadedFiles, handleFilesAdded, handleRemove } = useUploadedFiles(initialData?.portfolioFiles);

사용자가 "이전" 버튼으로 돌아갔다가 다시 이 단계로 오면 업로드한 파일이 사라집니다.

🤖 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 `@src/features/signup/ui/DesignerAdditionalStep.tsx` around lines 40 - 43, The
useUploadedFiles hook on line 43 is not being passed the initialData to restore
previously uploaded portfolio files, while other form fields like bankCode,
accountNumber, and accountHolder are properly initialized from initialData on
lines 40-42. Modify the useUploadedFiles hook invocation to pass
initialData?.portfolioFiles as a parameter to restore the files when users
navigate back to this step, ensuring consistency with how other form fields are
being initialized.

Comment thread src/shared/api/client.ts
Comment on lines +76 to +86
const reissueAccessToken = async () => {
try {
const response = await tokenRefreshApi
.post(createApiPath(TOKEN_REISSUE_PATH))
.json<ApiResponse<unknown>>();

return extractAccessToken(response);
} catch (error) {
throw await toApiError(error);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

토큰 재발급 경쟁 상태로 정상 세션이 강제 로그아웃될 수 있습니다.

동시에 여러 요청이 401을 받으면 재발급이 중복 실행되고, 한 요청의 실패가 clearClientAuth()를 호출해 이미 갱신된 세션까지 지울 수 있습니다. 재발급 Promise를 공유해 단일 비행으로 직렬화해 주세요.

🔧 제안 수정안
+let refreshPromise: Promise<string> | null = null;
+
+const getOrRefreshAccessToken = () => {
+  if (refreshPromise == null) {
+    refreshPromise = reissueAccessToken().finally(() => {
+      refreshPromise = null;
+    });
+  }
+
+  return refreshPromise;
+};
+
 export const api = ky.create({
@@
     afterResponse: [
       async ({ request, response, retryCount }) => {
@@
         try {
-          const accessToken = await reissueAccessToken();
+          const accessToken = await getOrRefreshAccessToken();
           setClientAccessToken(accessToken);
@@
         } catch {
           clearClientAuth();
         }
       },
     ],
   },
 });

Also applies to: 101-126

🤖 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 `@src/shared/api/client.ts` around lines 76 - 86, The reissueAccessToken
function and related token refresh logic has a race condition where multiple
simultaneous 401 requests can trigger duplicate token reissue operations, and
one failed reissue can call clearClientAuth() to invalidate a successful reissue
from a parallel request. Implement a single-flight pattern by storing the token
reissue Promise in a module-level variable. When reissueAccessToken is called,
check if a reissue is already in progress and return the existing Promise
instead of starting a new one. Clear the stored Promise only after the reissue
completes (either successfully or with error). This ensures all concurrent
requests share a single reissue operation, preventing the clearClientAuth() call
from invalidating a successful reissue completed by another request.

Comment thread src/shared/api/client.ts
Comment on lines +171 to +174
if (error instanceof Error) return new ApiError(error.message);

return new ApiError("");
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

알 수 없는 예외에서 빈 메시지를 반환하면 에러 안내가 사라집니다.

기본 메시지를 빈 문자열로 두지 말고 공통 fallback 문구를 사용해 주세요.

🔧 제안 수정안
-  return new ApiError("");
+  return new ApiError("요청 처리 중 문제가 발생했습니다");
🤖 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 `@src/shared/api/client.ts` around lines 171 - 174, The fallback case in the
error handler is returning an empty string as the ApiError message, which
provides no guidance to users when an unknown exception occurs. Replace the
empty string with a meaningful fallback message such as a common error phrase
like "An unknown error occurred" or similar contextual text. This ensures that
even when the error is not an instance of Error, users receive helpful error
guidance instead of seeing nothing.

Comment thread src/shared/ui/Header.tsx Outdated

setAuthState({
isLoggedIn: accessToken != null && role != null,
isLoggedIn: accessToken != null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

헤더의 로그인 판별 기준이 미들웨어 권한 판별과 불일치합니다.

현재는 accessToken만 있으면 로그인 UI를 노출하지만, src/proxy.ts는 역할 해석 실패 시 비인증으로 처리합니다. 이 상태에선 로그인/회원가입 CTA가 숨겨지고 내 계정/로 고정되어 복구 동선이 끊깁니다. 역할 해석 기준을 동일하게 맞춰 주세요.

🔧 제안 수정안
+import { getClientUserRoleFromAccessToken } from "`@/shared/lib/auth/client`";
@@
     const syncAuthState = () => {
       const accessToken = getCookieValue(ACCESS_TOKEN_COOKIE_NAME);
-      const role = normalizeRole(getCookieValue(USER_ROLE_COOKIE_NAME));
+      const roleFromCookie = normalizeRole(getCookieValue(USER_ROLE_COOKIE_NAME));
+      const role =
+        roleFromCookie ?? (accessToken != null ? getClientUserRoleFromAccessToken(accessToken) : null);
 
       setAuthState({
-        isLoggedIn: accessToken != null,
+        isLoggedIn: accessToken != null && role != null,
         role,
       });
     };
@@
-    if (authState.role == null) return "/";
+    if (authState.role == null) return "/login";

Also applies to: 68-68

🤖 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 `@src/shared/ui/Header.tsx` at line 56, The header's login authentication check
is inconsistent with the middleware authentication logic in src/proxy.ts.
Currently, the isLoggedIn property only verifies that accessToken is not null,
but the middleware also validates that the user's role can be successfully
interpreted. When role interpretation fails in the middleware, the user is
treated as unauthenticated, which causes a mismatch where the header shows the
user as logged in while the middleware rejects the request, breaking the
recovery flow. Update the isLoggedIn check in the Header component to include
the same role/permissions validation logic that the proxy middleware uses,
ensuring both checks use identical authentication criteria. This applies to all
authentication checks in the Header component.

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

@waldls waldls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

수고 너무 많으셨습니다 🙇🏻🙇🏻짱!!

@KOJ50 KOJ50 merged commit 1ab86e4 into main Jun 22, 2026
2 checks passed
@KOJ50 KOJ50 deleted the feature/#34-onboarding-api branch June 22, 2026 17:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍀 오진 📢 API 서버 API 연동

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 온보딩 API 연동

2 participants