Skip to content

[FEAT] 사용자, 기사 프로필 생성 및 수정 추가 - #62

Merged
9g-g9 merged 23 commits into
devfrom
feature/create-profile
Aug 5, 2026
Merged

[FEAT] 사용자, 기사 프로필 생성 및 수정 추가#62
9g-g9 merged 23 commits into
devfrom
feature/create-profile

Conversation

@9g-g9

@9g-g9 9g-g9 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📋 작업 내용

고객/기사 프로필 생성·수정 플로우를 구현하고, Header 반응형(사이드 네비)과 프로필 관련 API·에러 처리를 BE 명세에 맞게 정리했습니다.

  • 고객/기사 프로필 생성·수정 페이지 및 폼 구현
  • OAuth 계정(hasPhone / hasPassword)에 따른 전화번호·비밀번호 필드 분기
  • 고객 프로필 수정: 기본정보(PATCH .../me/basic) + 프로필(PATCH .../me) 변경분 이중 호출
  • Header tablet/mobile 햄버거·사이드 네비 및 compact GNB(알림·프로필·햄버거)
  • VALIDATION_ERRORerror.data 메시지 추출, 프로필 로딩 스켈레톤·loadingFallback 분리

🔥 변경 사항

프로필

  • 고객: /profile(생성), /profile/edit(수정 — 한 페이지에서 basic/profile 분리 호출)
  • 기사: /mover/profile(생성), /mover/profile/edit, /mover/basic/edit
  • status.hasPhone === false일 때만 생성 폼에 전화번호 입력 노출 (OAuth 부분)
  • hasPassword === false일 때 수정 폼에서 비밀번호 변경 필드 숨김 (OAuth 부분)
  • 고객 수정: 변경분만 전송, basic → profile 순서, 부분 실패 시 안내 + 비밀번호 필드 초기화
  • 수정 성공 시 Toast, basic 수정 성공 시 establishSession으로 Header 이름 동기화
  • 공용 phoneSchema(BE 01[016789] 규칙), 고객 edit의 disabled phone은 형식 검증 완화 (phone은 유니크하고 수정이 불가하므로 edit 쪽은 완화, 생성쪽은 BE와 규칙 맞춤)

Header

  • 햄버거 메뉴 + 오른쪽 SideNav 추가
  • focus trap, desktop(1024px) 리사이즈 시 자동 닫기, 햄버거 메뉴로 focus 복귀

API / Auth / Loading

  • ctx.addIssue 의 custom 에러 메시지에 대응하게끔 만드는 과정에서 ApiErrorData의 path/method/timestamp 를 data 값 하나로 합침
  • getValidationDataMessage / getApiErrorMessageVALIDATION_ERROR 상세 메시지 표시
  • useMoverAuthReady를 customer와 동일 allowlist 방식으로 정렬
  • RoleGuard 의 loadingFallback 을 한 파일로 분리하여 관리하게끔 정리

✅ 체크리스트

  • 로컬에서 정상 동작을 확인했습니다.
  • 기존 기능에 영향을 주지 않는지 확인했습니다.
  • 불필요한 console.log를 제거했습니다.
  • lint를 통과했습니다.
  • README 또는 문서를 수정했습니다. (필요 시)
  • API 명세와 일치하는지 확인했습니다.

📷 스크린샷 (선택)

  • 수정 완료 시 toast 예제 (생성은 그대로 redirect)
image
  • 고객 프로필 생성 / 수정
    생성 (일반)
image

생성 (OAuth)
image

수정 (일반)
image

수정 (OAuth)
image

  • 기사 프로필 생성 / 프로필 수정 / 기본정보 수정
    생성 (일반)
image

생성 (OAuth)
image

프로필 수정 (일반, OAuth)
image

기본정보 수정 (일반)
image

기본정보 수정 (OAuth)
image

  • 헤더 반응형 처리 (비로그인·로그인)
    desktop
image

tablet, mobile
image

side nav
image

알람 및 프로필은 dektop과 동일하게 열림
image


💬 To Reviewer

  • 고객과 기사의 프로필 생성 및 수정을 한 번에 작업하여 작업한 파일이 많습니다 죄송합니다...! 아래에 대칭되게끔 작업된 파일들 및 해당 파일의 역할을 묶어서 정리해뒀습니다. 참고 부탁드려요!

프로필 생성 (고객 ↔ 기사 대칭)

  • View: CustomerProfileCreateView / MoverProfileCreateView
  • Form: CustomerProfileForm / MoverProfileForm
  • Hook: useCreateCustomerProfile / useCreateMoverProfile
  • Status: useCustomerProfileStatus / useMoverProfileStatus
  • Schema: customerProfileSchema / moverProfileSchema
  • Mapper: toCustomerProfileFormValues / toMoverProfileFormValues
  • Page: app/.../profile/page.tsx (customer·mover 각각)

프로필 수정

  • 고객(한 페이지, basic+profile 이중 호출):

    • CustomerProfileEditView / CustomerProfileEditForm
    • useUpdateCustomerBasicInfo / useUpdateCustomerProfile
    • buildCustomerProfileEditPayloads / customerProfileEditSchema
  • 기사(페이지 분리):

    • 프로필: MoverProfileEditView / MoverProfileEditForm / useUpdateMoverProfile
    • 기본정보: MoverBasicInfoEditView / MoverBasicInfoEditForm / useUpdateMoverBasicInfo / moverBasicInfoEditSchema

공통 UI·유틸 (customer/mover 공용)

  • ProfilePageHeader, ProfileFormActions, ProfileFormSkeleton
  • ProfileImageUploader, ProfileChipGroup, ProfileSelectableChip
  • uploadProfileImage, phoneSchema, passwordChangeFields
  • API/타입: lib/api/profile.ts, types/profile.ts

Header 반응형

  • Header.tsx, HeaderSideNav.tsx, ProfileMenuTrigger.tsx

API 에러 / Loading

  • getValidationDataMessage, getApiErrorMessage, getApiError, types/api.ts
  • getCustomerProtectedLoadingFallback / getMoverProtectedLoadingFallback

다른 참고 사항

  • 고객 프로필 수정은 Figma 한 페이지 기준이라 FE에서 basic/profile을 순차 호출합니다. profile 수정 실패 시 부분 저장 안내 + 비밀번호 필드 clear로 재시도 UX를 보완했습니다.
  • 프로필 이미지 업로드(IS_PROFILE_IMAGE_UPLOAD_ENABLED = false)는 S3 연동 전이라 UI 미리보기만 동작합니다.
  • 생성 시에는 지정한 홈 페이지 또는 router history 로 이동하게 되지만, 수정 시에는 toast 문구(수정이 완료되었습니다!) 가 뜨며 별개로 이동은 하지 않도록 하였습니다.

Summary by CodeRabbit

  • 새 기능
    • 고객과 기사님의 프로필 등록·수정 페이지를 추가했습니다.
    • 기본정보, 비밀번호, 서비스 지역, 제공 서비스, 경력, 소개, 프로필 이미지를 관리할 수 있습니다.
    • 모바일·태블릿 사이드 내비게이션과 로그인 메뉴를 제공합니다.
    • 프로필 이미지 업로드와 역할별 맞춤 로딩 화면을 지원합니다.
  • 개선
    • 필수 입력 표시, 설명, 오류 메시지 등 폼 접근성을 강화했습니다.
    • 인증 및 프로필 상태에 따른 안내와 이동 처리를 개선했습니다.
  • 버그 수정
    • 검증 오류 메시지가 더욱 정확하게 표시됩니다.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 78054870-2465-4ca6-ad1e-40c1871bd260

📥 Commits

Reviewing files that changed from the base of the PR and between 38bfe5b and 4634639.

📒 Files selected for processing (3)
  • src/lib/api/fetchInstance.ts
  • src/lib/constants/apiRoutes.ts
  • src/lib/constants/queryKeys.ts

📝 Walkthrough

Walkthrough

고객과 기사 프로필 생성·수정 페이지와 API 흐름을 추가했습니다. 인증 로딩 fallback, 프로필 검증, 이미지 업로드, 오류 메시지 처리, 반응형 헤더 사이드 내비게이션도 변경했습니다.

Changes

프로필 관리 흐름

Layer / File(s) Summary
프로필 계약과 API
src/types/*, src/lib/api/*, src/lib/schemas/*
프로필 타입, 생성·수정 API, 이미지 업로드 API, 검증 스키마와 상세 오류 메시지 처리를 추가했습니다.
프로필 데이터 흐름
src/hooks/profile/*, src/lib/profile/*
프로필 조회·변경 훅, 폼 값 변환, 변경 payload 생성과 이미지 업로드 흐름을 추가했습니다.
프로필 폼과 보호 라우트
src/components/profile/*, src/app/(customer)/(protected)/*, src/app/(mover)/(protected)/*
고객과 기사 프로필 생성·수정 폼, 상태 화면, 페이지 연결과 역할별 loading fallback을 추가했습니다.
반응형 헤더와 공통 UI
src/components/common/Header/*, src/components/common/FormField/FormField.tsx, src/components/common/Chip/SelectableChip.tsx, src/icons/index.ts, src/styles/tokens.theme.css
모바일 사이드 내비게이션, 접근성 라벨 처리, 칩 스타일, 아이콘과 spacing 토큰을 변경했습니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels: ♻️ refactor

Suggested reviewers: wndnjs2037

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 고객과 기사 프로필 생성 및 수정 기능을 추가한 주요 변경 사항을 정확하고 간결하게 설명합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/create-profile

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

@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: 15

🧹 Nitpick comments (8)
src/lib/profile/uploadProfileImage.ts (1)

15-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

네트워크 부수효과를 src/lib/profile에서 분리하세요.

uploadProfileImageIfNeeded는 presigned URL 요청과 S3 PUT를 수행합니다. 이 동작은 src/lib의 순수 함수 규칙을 위반합니다.

업로드 orchestration은 src/hooks/profile의 전용 Hook 또는 API 계층으로 이동하세요. isProfileImageContentTypebuildProfileImagePublicUrl만 순수 유틸로 유지하세요.

As per path instructions, "src/lib/**/*.ts: 순수 함수로 작성하고 부수효과를 두지 않습니다."

🤖 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/lib/profile/uploadProfileImage.ts` around lines 15 - 54, Move the network
orchestration out of uploadProfileImageIfNeeded in src/lib/profile into a
dedicated hook under src/hooks/profile or an API-layer function, including
requestProfileImageUploadUrl and uploadFileToPresignedUrl calls. Keep
src/lib/profile limited to the pure utilities isProfileImageContentType and
buildProfileImagePublicUrl, and update callers to use the moved upload flow
while preserving the feature-flag and validation behavior.

Source: Path instructions

src/components/profile/ProfileChipGroup.tsx (1)

68-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

role="group" 에 접근 이름과 오류 연결을 추가해 주세요.

현재 role="group" 에는 접근 이름이 없습니다. 스크린리더는 그룹의 목적을 읽지 못합니다. 또한 오류 문구가 chip 과 프로그램적으로 연결되지 않습니다. aria-label(또는 FormField label id 를 가리키는 aria-labelledby)과 aria-describedby 를 받도록 확장하면 두 문제가 함께 해결됩니다.

♿ 제안 diff
 interface ProfileChipGroupBaseProps<T extends string | number> {
   options: ProfileChipOption<T>[];
   className?: string;
   chipClassName?: string;
   error?: string;
+  /** 그룹 접근 이름 (FormField label 과 연결 권장) */
+  ariaLabel?: string;
+  ariaLabelledBy?: string;
 }
-  const { options, className, chipClassName, error } = props;
+  const { options, className, chipClassName, error, ariaLabel, ariaLabelledBy } = props;
+  const errorId = useId();
 
   return (
     <div className="flex w-full flex-col gap-4">
-      <div className={cn("flex flex-wrap gap-8 md:gap-12", className)} role="group">
+      <div
+        className={cn("flex flex-wrap gap-8 md:gap-12", className)}
+        role="group"
+        aria-label={ariaLabelledBy ? undefined : ariaLabel}
+        aria-labelledby={ariaLabelledBy}
+        aria-describedby={error ? errorId : undefined}
+      >
-        <Text as="p" role="alert" variant="xs-regular" className="text-text-error">
+        <Text as="p" id={errorId} role="alert" variant="xs-regular" className="text-text-error">
🤖 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/components/profile/ProfileChipGroup.tsx` around lines 68 - 84, Update the
group element in ProfileChipGroup to expose an accessible name via aria-label or
the associated FormField label id through aria-labelledby, and connect the
rendered error message through aria-describedby. Ensure the relevant label and
error ids are available and applied to the role="group" element while preserving
the existing chip selection behavior.

Source: Path instructions

src/components/profile/CustomerProfileForm.tsx (1)

68-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

regionId 필수 검증이 두 폼의 submit 핸들러에 중복되어 있습니다. 근본 원인은 검증 책임이 zod 스키마 밖에 있는 점입니다. 그래서 오류가 필드 대신 폼 전체 문구로 표시되고, isValid 는 지역 미선택 상태에서도 true 가 됩니다. 스키마에서 regionId 를 필수로 정의하면 두 폼의 분기 코드가 사라집니다.

  • src/components/profile/CustomerProfileForm.tsx#L68-L71: createCustomerProfileSchema 에서 regionId 를 필수로 만들고 이 분기를 제거해 주세요.
  • src/components/profile/CustomerProfileEditForm.tsx#L88-L91: customerProfileEditSchema 에 동일 규칙을 적용하고 이 분기를 제거해 주세요.
🤖 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/components/profile/CustomerProfileForm.tsx` around lines 68 - 71, Make
regionId required in both createCustomerProfileSchema and
customerProfileEditSchema, then remove the duplicated submit-handler null checks
in src/components/profile/CustomerProfileForm.tsx lines 68-71 and
src/components/profile/CustomerProfileEditForm.tsx lines 88-91 so schema
validation drives field-level errors and isValid.
src/components/common/Header/ProfileMenuTrigger.tsx (1)

135-142: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

모바일 터치 영역을 확인해 주세요.

lg 미만에서 아이콘은 size-24 이고 닉네임은 max-lg:hidden 으로 숨겨집니다. 따라서 트리거 버튼의 실제 클릭 영역이 24×24px로 축소됩니다. 이 값은 일반적인 최소 터치 영역 권장치보다 작습니다.

시각 크기는 유지하고 패딩으로 터치 영역만 확장하는 방식을 권장합니다. Figma 디자인이 24px를 명시했다면 현재 상태를 유지해도 됩니다.

코딩 가이드라인의 "키보드 접근성, focus·disabled·hover·active 상태, aria 속성, 모달 focus 관리, 색상 대비, 모바일 터치 영역을 확인한다" 항목을 근거로 지적했습니다.

🤖 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/components/common/Header/ProfileMenuTrigger.tsx` around lines 135 - 142,
Update the ProfileMenuTrigger mobile trigger styling around the profile Image so
its visual size remains 24px while the clickable/focusable area meets the
minimum touch-target size, using padding or equivalent spacing; preserve the
existing desktop sizing and hidden nickname behavior.

Source: Coding guidelines

src/components/common/Header/Header.tsx (1)

189-217: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

메뉴 열기 버튼이 두 분기에 중복 정의되어 있습니다.

189-198행과 208-217행의 버튼은 ref, className, aria-label, aria-expanded, onClick, 아이콘까지 동일합니다. 클래스 문자열이 길어 한쪽만 수정되면 어긋납니다.

파일 내부 서브 컴포넌트로 추출하는 방식을 권장합니다.

♻️ 제안 리팩터
+const SideNavOpenButton = ({
+  buttonRef,
+  isOpen,
+  onOpen,
+}: {
+  buttonRef: RefObject<HTMLButtonElement | null>;
+  isOpen: boolean;
+  onOpen: () => void;
+}) => (
+  <button
+    ref={buttonRef}
+    type="button"
+    className="text-icon-default focus-visible:ring-border-brand rounded-4 flex size-24 items-center justify-center focus-visible:ring-2 focus-visible:outline-none lg:hidden"
+    aria-label="메뉴 열기"
+    aria-expanded={isOpen}
+    onClick={onOpen}
+  >
+    <MenuIcon aria-hidden="true" className="size-24" />
+  </button>
+);
🤖 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/components/common/Header/Header.tsx` around lines 189 - 217, Extract the
duplicated menu-open button markup into a file-local subcomponent, preserving
its ref, accessibility attributes, styling, click handler, and MenuIcon. Replace
both conditional branches in the Header component with this shared subcomponent
so future changes remain consistent.
src/components/common/Header/HeaderSideNav.tsx (2)

141-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

배경 오버레이 버튼의 접근 가능한 이름이 닫기 버튼과 중복됩니다.

오버레이 <button> 과 헤더의 닫기 <button> 이 모두 "메뉴 닫기" 이름을 가집니다. 스크린리더 사용자는 동일한 이름의 버튼을 두 개 만납니다. 키보드 사용자에게도 오버레이는 불필요한 Tab 정지점입니다.

이미 Escape 키와 닫기 버튼으로 닫기 동작을 제공하므로, 오버레이는 마우스 전용으로 두고 접근성 트리에서 제외하는 방식을 권장합니다.

♿ 제안 수정
       <button
         type="button"
         className="bg-overlay-scrim absolute inset-0"
-        aria-label="메뉴 닫기"
+        aria-hidden="true"
+        tabIndex={-1}
         onClick={onClose}
       />
🤖 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/components/common/Header/HeaderSideNav.tsx` around lines 141 - 146,
Update the background overlay button in HeaderSideNav so it is excluded from the
accessibility tree and removed from keyboard tab order, while preserving its
mouse click behavior through onClose. Keep the header’s existing close button
and Escape-key behavior unchanged.

24-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

데스크톱 브레이크포인트 값이 하드코딩되어 있습니다.

DESKTOP_MEDIA_QUERY1024px 는 Tailwind lg 브레이크포인트와 같은 값입니다. 이 컴포넌트의 lg:hidden 클래스와 값이 동기화되어야 합니다. 프로젝트가 브레이크포인트를 변경하면 이 상수만 남아 어긋납니다.

브레이크포인트 상수를 공용 모듈에 두고 두 곳에서 참조하는 방식을 권장합니다. 최소한 lg 와 동일해야 함을 주석으로 명시해 주세요.

🤖 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/components/common/Header/HeaderSideNav.tsx` around lines 24 - 27, Update
DESKTOP_MEDIA_QUERY in HeaderSideNav to avoid an untracked hardcoded breakpoint
by reusing the project’s shared breakpoint constant or configuration value for
Tailwind lg; if no shared symbol exists, add a comment explicitly requiring the
value to remain synchronized with lg. Keep the media query behavior aligned with
the component’s lg:hidden class.
src/styles/tokens.theme.css (1)

261-264: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

size-25size-100으로 변경하고 중복 토큰을 제거하세요.

size-25 사용처는 프로필 이미지 두 곳이며 모두 100×100 크기를 의도합니다. 두 사용처를 size-100으로 변경한 뒤 --spacing-25를 제거하세요. --spacing-100--spacing-104에는 각각 용도 주석을 추가하세요.

🤖 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/styles/tokens.theme.css` around lines 261 - 264, 프로필 이미지의 두 size-25 사용처를
size-100으로 변경하고 중복된 --spacing-25 토큰을 제거하세요. --spacing-100과 --spacing-104에는 각각의
용도를 설명하는 주석을 추가하며, 기존 토큰 값과 다른 사용처는 유지하세요.

Source: Path instructions

🤖 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/components/common/FormField/FormField.tsx`:
- Around line 34-43: FormField의 labelContent에서 시각적 별표는 유지하되, aria-hidden 별표와 함께
sr-only 텍스트로 “필수”를 제공해 필수 상태를 접근성 트리에 노출하세요. 또한 FormField가 렌더링하거나 전달하는 실제 input
요소에 required 또는 aria-required를 설정해 스크린 리더가 필수 필드로 인식하도록 업데이트하세요.

In `@src/components/common/Header/Header.tsx`:
- Around line 127-128: Header의 openSideNav와 closeSideNav를 useCallback으로 감싸 렌더 간
함수 참조를 안정화하세요. 두 콜백의 의존성 배열은 setIsSideNavOpen의 안정적인 setter에 맞게 설정하고, 필요한
useCallback import도 추가하여 HeaderSideNav의 effect가 불필요하게 재실행되지 않도록 하세요.

In `@src/components/common/Header/HeaderSideNav.tsx`:
- Around line 29-46: Extract the duplicated isNavLinkActive logic into a shared
utility module, preserving the exact pathname, href, and
APP_ROUTES.MOVERS.FAVORITES handling. Remove the local implementations from
Header.tsx and HeaderSideNav.tsx, and import the shared function in both
components so desktop and mobile navigation use one active-state rule.
- Around line 148-154: Separate the dialog container from the navigation
landmark in the HeaderSideNav component: move role="dialog", aria-modal,
aria-labelledby, and panelRef to a div typed via useRef<HTMLDivElement>, then
wrap only the link list after the close-button block in a nav without
role="dialog" and close it at the list’s end.

In `@src/components/profile/CustomerProfileEditForm.tsx`:
- Around line 134-158: The three profile forms branch on localized error.message
text instead of stable ApiError.code values. Create a shared utility that maps
backend error codes, verified against /docs, to the affected field names, then
reuse it in src/components/profile/CustomerProfileEditForm.tsx lines 134-158 to
replace the phone and current-password message checks; in
src/components/profile/CustomerProfileForm.tsx lines 83-93 to replace the phone
check and retain setFocus("phone"); and in
src/components/profile/MoverProfileForm.tsx lines 88-106 to replace the phone,
nickname, and alias checks, preserving each form’s existing field error handling
and focus behavior.
- Around line 196-205: CustomerProfileEditForm의 전화번호 서버 오류 처리를 setError("phone")
및 setFocus("phone") 대신 setSubmitError로 변경하세요. 비활성화된 customer-edit-phone 입력은 계속
disabled 상태와 기존 error 렌더링을 유지하고, 전화번호를 수정할 수 없는 경우 제출 오류로 표시되도록 하세요.

In `@src/components/profile/CustomerProfileEditView.tsx`:
- Around line 18-20: Update src/components/profile/CustomerProfileEditView.tsx
lines 18-20 in the loading condition around isAuthPending, isProfilePending, and
canFetch to use isAuthPending || (canFetch && isProfilePending), then add a
!canFetch branch showing access guidance or redirecting. Apply the same
canFetch-aware loading logic in
src/components/profile/CustomerProfileCreateView.tsx lines 13-21, and handle
missing user?.id there because useCustomerProfileStatus requires it.

In `@src/components/profile/MoverBasicInfoEditForm.tsx`:
- Around line 144-152: Update the PasswordInput for currentPassword in
MoverBasicInfoEditForm to use autoComplete="current-password" instead of
"new-password". Keep the new-password value unchanged for the new password and
confirmation fields.

In `@src/components/profile/ProfileImageUploader.tsx`:
- Around line 51-59: Update src/components/profile/ProfileImageUploader.tsx
lines 51-59 and its props to accept an optional id, apply id ?? inputId to the
file input, and connect the trigger button to the error message with
aria-describedby. Update src/components/profile/CustomerProfileEditForm.tsx
lines 246-259 to pass id="customer-edit-profile-image", and
src/components/profile/CustomerProfileForm.tsx lines 128-141 to pass
id="customer-profile-image", matching each FormField labelFor; verify FormField
renders labelFor as label htmlFor.
- Around line 29-40: Move object URL creation out of the render path: replace
the useMemo-based objectUrl in the component with useState and create/revoke the
URL inside a useEffect tied to value, ensuring cleanup handles each created URL.
Remove the useMemo import and add useState while preserving the existing null
behavior when value is absent.

In `@src/hooks/profile/useCustomerProfileMe.ts`:
- Around line 5-10: Update useCustomerProfileMe in
src/hooks/profile/useCustomerProfileMe.ts at lines 5-10 to read userId from
useAuthStore, include it in the CUSTOMER_ME query key, and enable the query only
when userId exists. Apply the same change to useMoverProfileMe in
src/hooks/profile/useMoverProfileMe.ts at lines 5-10, using the MOVER_ME key.

In `@src/lib/api/profile.ts`:
- Around line 69-85: Update createCustomerProfile, updateCustomerBasicInfo, and
updateCustomerProfile in src/lib/api/profile.ts lines 69-85 to use axiosInstance
instead of fetchInstance while preserving their request payloads and response
types. Apply the same axiosInstance client and error-handling contract to the
mover profile creation and update functions in src/lib/api/profile.ts lines
132-148.

In `@src/lib/profile/uploadProfileImage.ts`:
- Around line 9-54: Update the profile-image flow around
IS_PROFILE_IMAGE_UPLOAD_ENABLED and uploadProfileImageIfNeeded so a selected
image is uploaded and its returned public URL reaches the
CustomerProfileEditForm PATCH payload before showing success. Before release,
enable the upload path; if upload infrastructure is unavailable, disable the
image-selection UI or clearly mark it unsupported instead of silently returning
undefined.

In `@src/lib/schemas/moverProfileSchema.ts`:
- Around line 31-35: career 스키마의 숫자 문자열 검증에 안전한 정수 범위 제한을 추가하세요.
Number(formValues.career)로 변환하기 전에 Number.isSafeInteger에 해당하는 값만 허용하고, 범위를 벗어나거나
Infinity가 될 수 있는 값은 기존 검증 오류 방식으로 거부되도록 수정하세요.

In `@src/lib/schemas/phoneSchema.ts`:
- Around line 7-17: phoneSchema에서 하이픈을 제거하는 transform보다 먼저 원본 입력값을 BE와 동일한 전화번호
정규식으로 검증하도록 순서를 변경하세요. 검증을 통과한 값만 하이픈을 제거해 후속 숫자 및 전화번호 검증으로 전달하고, 잘못된 하이픈 위치는
클라이언트에서도 거부되도록 하세요.

---

Nitpick comments:
In `@src/components/common/Header/Header.tsx`:
- Around line 189-217: Extract the duplicated menu-open button markup into a
file-local subcomponent, preserving its ref, accessibility attributes, styling,
click handler, and MenuIcon. Replace both conditional branches in the Header
component with this shared subcomponent so future changes remain consistent.

In `@src/components/common/Header/HeaderSideNav.tsx`:
- Around line 141-146: Update the background overlay button in HeaderSideNav so
it is excluded from the accessibility tree and removed from keyboard tab order,
while preserving its mouse click behavior through onClose. Keep the header’s
existing close button and Escape-key behavior unchanged.
- Around line 24-27: Update DESKTOP_MEDIA_QUERY in HeaderSideNav to avoid an
untracked hardcoded breakpoint by reusing the project’s shared breakpoint
constant or configuration value for Tailwind lg; if no shared symbol exists, add
a comment explicitly requiring the value to remain synchronized with lg. Keep
the media query behavior aligned with the component’s lg:hidden class.

In `@src/components/common/Header/ProfileMenuTrigger.tsx`:
- Around line 135-142: Update the ProfileMenuTrigger mobile trigger styling
around the profile Image so its visual size remains 24px while the
clickable/focusable area meets the minimum touch-target size, using padding or
equivalent spacing; preserve the existing desktop sizing and hidden nickname
behavior.

In `@src/components/profile/CustomerProfileForm.tsx`:
- Around line 68-71: Make regionId required in both createCustomerProfileSchema
and customerProfileEditSchema, then remove the duplicated submit-handler null
checks in src/components/profile/CustomerProfileForm.tsx lines 68-71 and
src/components/profile/CustomerProfileEditForm.tsx lines 88-91 so schema
validation drives field-level errors and isValid.

In `@src/components/profile/ProfileChipGroup.tsx`:
- Around line 68-84: Update the group element in ProfileChipGroup to expose an
accessible name via aria-label or the associated FormField label id through
aria-labelledby, and connect the rendered error message through
aria-describedby. Ensure the relevant label and error ids are available and
applied to the role="group" element while preserving the existing chip selection
behavior.

In `@src/lib/profile/uploadProfileImage.ts`:
- Around line 15-54: Move the network orchestration out of
uploadProfileImageIfNeeded in src/lib/profile into a dedicated hook under
src/hooks/profile or an API-layer function, including
requestProfileImageUploadUrl and uploadFileToPresignedUrl calls. Keep
src/lib/profile limited to the pure utilities isProfileImageContentType and
buildProfileImagePublicUrl, and update callers to use the moved upload flow
while preserving the feature-flag and validation behavior.

In `@src/styles/tokens.theme.css`:
- Around line 261-264: 프로필 이미지의 두 size-25 사용처를 size-100으로 변경하고 중복된 --spacing-25
토큰을 제거하세요. --spacing-100과 --spacing-104에는 각각의 용도를 설명하는 주석을 추가하며, 기존 토큰 값과 다른
사용처는 유지하세요.
🪄 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: 29f168fa-7511-4df8-b157-3938e97ab127

📥 Commits

Reviewing files that changed from the base of the PR and between ed77eba and f6dc473.

⛔ Files ignored due to path filters (1)
  • src/icons/gallery.svg is excluded by !**/*.svg
📒 Files selected for processing (67)
  • src/app/(customer)/(protected)/layout.tsx
  • src/app/(customer)/(protected)/profile/edit/page.tsx
  • src/app/(customer)/(protected)/profile/page.tsx
  • src/app/(mover)/(protected)/layout.tsx
  • src/app/(mover)/(protected)/mover/basic/edit/page.tsx
  • src/app/(mover)/(protected)/mover/profile/edit/page.tsx
  • src/app/(mover)/(protected)/mover/profile/page.tsx
  • src/components/auth/MoverAuthGate.tsx
  • src/components/common/FormField/FormField.tsx
  • src/components/common/Header/Header.tsx
  • src/components/common/Header/HeaderSideNav.tsx
  • src/components/common/Header/ProfileMenuTrigger.tsx
  • src/components/common/Header/notification/NotificationTrigger.tsx
  • src/components/profile/CustomerProfileCreateView.tsx
  • src/components/profile/CustomerProfileEditForm.tsx
  • src/components/profile/CustomerProfileEditView.tsx
  • src/components/profile/CustomerProfileForm.tsx
  • src/components/profile/MoverBasicInfoEditForm.tsx
  • src/components/profile/MoverBasicInfoEditView.tsx
  • src/components/profile/MoverProfileCreateView.tsx
  • src/components/profile/MoverProfileEditForm.tsx
  • src/components/profile/MoverProfileEditView.tsx
  • src/components/profile/MoverProfileForm.tsx
  • src/components/profile/ProfileChipGroup.tsx
  • src/components/profile/ProfileFormActions.tsx
  • src/components/profile/ProfileFormSkeleton.tsx
  • src/components/profile/ProfileImageUploader.tsx
  • src/components/profile/ProfilePageHeader.tsx
  • src/components/profile/ProfileSelectableChip.tsx
  • src/hooks/profile/useCreateCustomerProfile.ts
  • src/hooks/profile/useCreateMoverProfile.ts
  • src/hooks/profile/useCustomerProfileMe.ts
  • src/hooks/profile/useCustomerProfileStatus.ts
  • src/hooks/profile/useMoverProfileMe.ts
  • src/hooks/profile/useMoverProfileStatus.ts
  • src/hooks/profile/useUpdateCustomerBasicInfo.ts
  • src/hooks/profile/useUpdateCustomerProfile.ts
  • src/hooks/profile/useUpdateMoverBasicInfo.ts
  • src/hooks/profile/useUpdateMoverProfile.ts
  • src/hooks/useMoverAuthReady.ts
  • src/icons/index.ts
  • src/lib/api/fetchInstance.ts
  • src/lib/api/getApiError.ts
  • src/lib/api/getApiErrorMessage.ts
  • src/lib/api/getValidationDataMessage.ts
  • src/lib/api/profile.ts
  • src/lib/api/profileImage.ts
  • src/lib/constants/apiRoutes.ts
  • src/lib/constants/appRoutes.ts
  • src/lib/constants/queryKeys.ts
  • src/lib/loading/getCustomerProtectedLoadingFallback.tsx
  • src/lib/loading/getMoverProtectedLoadingFallback.tsx
  • src/lib/profile/buildCustomerProfileEditPayloads.ts
  • src/lib/profile/toCustomerProfileEditFormValues.ts
  • src/lib/profile/toCustomerProfileFormValues.ts
  • src/lib/profile/toMoverBasicInfoEditFormValues.ts
  • src/lib/profile/toMoverProfileFormValues.ts
  • src/lib/profile/uploadProfileImage.ts
  • src/lib/schemas/customerProfileEditSchema.ts
  • src/lib/schemas/customerProfileSchema.ts
  • src/lib/schemas/moverBasicInfoEditSchema.ts
  • src/lib/schemas/moverProfileSchema.ts
  • src/lib/schemas/passwordChangeFields.ts
  • src/lib/schemas/phoneSchema.ts
  • src/styles/tokens.theme.css
  • src/types/api.ts
  • src/types/profile.ts

Comment thread src/components/common/FormField/FormField.tsx
Comment thread src/components/common/Header/Header.tsx Outdated
Comment thread src/components/common/Header/HeaderSideNav.tsx Outdated
Comment thread src/components/common/Header/HeaderSideNav.tsx Outdated
Comment thread src/components/profile/CustomerProfileEditForm.tsx
Comment thread src/hooks/profile/useCustomerProfileMe.ts
Comment thread src/lib/api/profile.ts
Comment thread src/lib/profile/uploadProfileImage.ts
Comment thread src/lib/schemas/moverProfileSchema.ts Outdated
Comment thread src/lib/schemas/phoneSchema.ts Outdated

@juengseulki juengseulki 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.

📋 PR 리뷰

👍 좋았던 점

  • 고객과 기사 프로필 생성·수정 흐름을 View, Form, Hook, Schema, Mapper로 분리해 역할별 구조를 대칭적으로 구성했습니다.
  • 고객 프로필 수정 화면은 하나로 유지하면서 BE의 기본정보 API와 프로필 API에 맞춰 변경 Payload를 나누도록 설계했습니다.
  • 초기값과 현재값을 비교해 실제 변경된 필드만 전송하고, 변경 사항이 없으면 요청을 막도록 처리했습니다.
  • hasPhonehasPassword를 실제 API 응답 기준으로 사용해 OAuth 계정의 전화번호·비밀번호 필드를 분기했습니다.
  • 기본정보 수정 성공 시 Auth Store의 세션도 갱신해 Header 이름이 즉시 변경되도록 했습니다.
  • Query invalidation을 통해 고객·기사 프로필과 프로필 완료 상태를 최신 응답으로 다시 맞추도록 구성했습니다.
  • VALIDATION_ERRORerror.data를 해석하는 유틸을 분리해 서버 검증 메시지를 사용자에게 구체적으로 표시할 수 있도록 했습니다.
  • 고객과 기사 보호 화면의 Loading Fallback을 별도 유틸로 분리해 Role Guard 사용처의 중복을 줄였습니다.
  • SideNav에 포커스 트랩, Escape 닫기, Scrim, Body Scroll Lock, 데스크톱 전환 시 자동 닫기 및 포커스 복원을 적용했습니다.
  • Presigned URL 발급, S3 PUT, Public URL 생성 책임을 각각 분리해 향후 이미지 업로드 활성화 시 연결하기 좋은 구조를 마련했습니다.
  • 고객과 기사 프로필 응답 타입에 hasPassword를 반영하고, 기사 응답에는 닉네임·경력·소개·평점 정보를 포함해 BE 계약에 맞게 매핑했습니다.

🚨 수정이 필요한 부분

  1. 고객 수정이 부분 성공한 뒤 재시도하면 이미 저장된 기본정보가 다시 변경분으로 계산될 수 있습니다.
  • 기본정보 성공 후에도 비교 기준인 defaultValues는 최초 값으로 유지됩니다.
  • 프로필 저장 실패 후 재시도하면 이름 등의 기본정보 요청이 다시 전송될 수 있습니다.
  • 성공한 필드를 초기 기준값에 반영하거나 최신 Query 값으로 Form을 reset해야 합니다.
  1. Header에서 렌더링 중 상태를 변경하고 있습니다.
  • pathname !== sideNavPathname 조건문 안에서 Setter를 직접 호출합니다.
  • 경로 변경에 따른 SideNav 닫기는 useEffect([pathname])로 이동하고 sideNavPathname 상태를 제거하는 편이 안전합니다.

🔍 확인 및 제안

  • 프로필 이미지 업로드가 비활성화된 상태에서도 사용자는 이미지를 선택하고 미리보기를 볼 수 있습니다. 현재 저장되지 않는 기능임을 UI에서 분명히 알려주는 것이 좋아 보여요.
  • 고객 기본정보와 프로필 요청을 순차적으로 실행하는 현재 구조에서는 원자적 저장이 불가능합니다. 이번 PR처럼 부분 저장을 명확하게 안내하는 방향은 현실적이지만, 성공한 영역을 재시도 대상에서 제외하는 기준값 갱신은 꼭 필요합니다.

전체적으로 BE 프로필 API 분리와 OAuth 상태 계약을 FE에 잘 반영했고, 고객·기사 구조도 비교적 일관되게 정리됐습니다. 다만 부분 저장 후 재시도 기준과 Header의 렌더 중 상태 변경은 실제 동작 안정성에 영향을 주므로 병합 전에 보완하는 게 안전해 보입니다!😀


To Reviewer 요청 기준으로 고객·기사 프로필 생성·수정 구조,
고객의 basic/profile 이중 호출, OAuth 필드 분기와 Header SideNav를
중점적으로 확인했습니다!

고객과 기사 프로필 생성 구조를 View, Form, Hook, Status, Schema와 Mapper로
대칭되게 구성한 방향은 적절해 보입니다.

고객 수정 화면에서 기본정보와 프로필 정보를 한 번에 입력받되,
실제 요청은 BE 계약에 맞춰 PATCH /customer/me/basic
PATCH /customer/me로 나눠 변경분만 순차 전송한 방식도 자연스럽습니다.

두 API를 하나의 트랜잭션으로 처리할 수 없으므로
기본정보는 성공하고 프로필 수정만 실패할 수 있는데,
이 경우 부분 저장 사실을 안내하고 비밀번호 필드를 초기화한 방향도 좋았습니다.

다만 기본정보 성공 이후에도 Form이 변경분을 비교하는 기준은
최초 defaultValues로 유지됩니다.

따라서 프로필 수정 실패 후 다시 제출하면 이미 저장된 이름 등의 기본정보가
다시 요청 Payload에 포함될 수 있어, 성공한 기본정보를 새로운 초기값으로 반영하거나
최신 Query 값을 기준으로 Form을 reset하는 보완이 필요해 보입니다.

OAuth 계정 분기도 API 응답의 hasPhone, hasPassword를 사용하고 있어
프론트에서 로그인 제공자를 추측하지 않고 실제 데이터 상태를 기준으로
입력 필드를 제공하는 방식이 적절해 보였습니다.

Header SideNav도 다음 접근성 동작이 잘 반영되어 있었습니다.

  • SideNav 진입 시 닫기 버튼으로 포커스 이동
  • 내부 Tab 순환
  • Escape 및 Scrim 닫기
  • Body Scroll Lock과 스크롤바 폭 보정
  • 데스크톱 Breakpoint 진입 시 자동 닫기
  • 닫힌 후 햄버거 버튼으로 포커스 복귀

다만 Header.tsx에서 pathname 변경을 확인하며 렌더링 중
상태 Setter를 직접 호출하고 있으므로, 이 부분은 useEffect([pathname])
이동하는 것이 React 렌더링 규칙상 더 안전해 보입니다.

이미지 업로드는 현재 기능 플래그가 꺼져 있어 선택과 미리보기는 가능하지만
실제 저장에는 반영되지 않습니다. S3 연동 전 임시 정책이라면
사용자가 저장되는 것으로 오해하지 않도록 안내 문구를 추가하는 것도
함께 검토하면 좋을 것 같습니다.

Comment thread src/components/profile/CustomerProfileEditForm.tsx
Comment thread src/components/common/Header/Header.tsx
Comment thread src/lib/profile/uploadProfileImage.ts

@yooseohyeon yooseohyeon 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.

작업하느라 수고 많으셨습니다!
프로필 생성·수정 폼에서 공통으로 사용되는 UI들을 잘 분리해주셔서 폼 사이의 중복을 줄이면서도 각 화면의 요구사항은 명확하게 유지된 것 같습니다!

이미 리뷰가 달린 부분들은 제외하고 인라인 코멘트를 추가했습니다.
추가로 한 가지 질문이 있습니다! 지금은 회원가입 후 프로필 등록을 하지 않아도 헤더의 메뉴가 감춰지지 않고 헤더의 메뉴들에 접근하면 프로필 등록을 해야 한다는 안내가 없습니다. 기존 에러 ui(불러올 수 없다는 문구 같은)만 표시되는 것으로 확인됩니다. 이는 추후에 작업하실 예정이신 걸까요??

Comment thread src/components/profile/MoverProfileCreateView.tsx
Comment thread src/components/profile/CustomerProfileCreateView.tsx
Comment thread src/components/profile/CustomerProfileEditForm.tsx

@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: 1

🤖 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/components/profile/CustomerProfileEditForm.tsx`:
- Around line 107-114: Update the reset call in CustomerProfileEditForm so it
resets only successfully saved basic-information fields and clears the password
fields, rather than spreading all values from getValues(). Preserve dirty state
for regionId, serviceTypes, and image changes until the profile PATCH succeeds,
so buildCustomerProfileEditPayloads() still produces a retry payload after
failure.
🪄 Autofix

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: ab9ed5a1-9f8e-40ab-8a02-612aacc5cdaf

📥 Commits

Reviewing files that changed from the base of the PR and between f6dc473 and c32c0a9.

📒 Files selected for processing (29)
  • src/components/common/FormField/FormField.tsx
  • src/components/common/Header/Header.tsx
  • src/components/common/Header/HeaderSideNav.tsx
  • src/components/common/Header/ProfileMenuTrigger.tsx
  • src/components/common/Header/notification/NotificationTrigger.tsx
  • src/components/estimate/request/EstimateRequestForm.tsx
  • src/components/profile/CustomerProfileCreateView.tsx
  • src/components/profile/CustomerProfileEditForm.tsx
  • src/components/profile/CustomerProfileEditView.tsx
  • src/components/profile/CustomerProfileForm.tsx
  • src/components/profile/MoverBasicInfoEditForm.tsx
  • src/components/profile/MoverBasicInfoEditView.tsx
  • src/components/profile/MoverProfileCreateView.tsx
  • src/components/profile/MoverProfileEditForm.tsx
  • src/components/profile/MoverProfileEditView.tsx
  • src/components/profile/MoverProfileForm.tsx
  • src/components/profile/ProfileChipGroup.tsx
  • src/components/profile/ProfileEmptyState.tsx
  • src/components/profile/ProfileImageUploader.tsx
  • src/hooks/profile/useCustomerProfileMe.ts
  • src/hooks/profile/useMoverProfileMe.ts
  • src/icons/index.ts
  • src/lib/constants/apiRoutes.ts
  • src/lib/constants/queryKeys.ts
  • src/lib/profile/buildCustomerProfileEditPayloads.ts
  • src/lib/schemas/moverProfileSchema.ts
  • src/lib/schemas/phoneSchema.ts
  • src/lib/schemas/signUpSchema.ts
  • src/styles/tokens.theme.css
🚧 Files skipped from review as they are similar to previous changes (17)
  • src/icons/index.ts
  • src/lib/schemas/phoneSchema.ts
  • src/lib/constants/queryKeys.ts
  • src/components/common/Header/notification/NotificationTrigger.tsx
  • src/styles/tokens.theme.css
  • src/hooks/profile/useCustomerProfileMe.ts
  • src/components/common/Header/ProfileMenuTrigger.tsx
  • src/components/profile/CustomerProfileForm.tsx
  • src/components/profile/MoverProfileEditView.tsx
  • src/components/profile/CustomerProfileEditView.tsx
  • src/components/profile/MoverProfileForm.tsx
  • src/lib/schemas/moverProfileSchema.ts
  • src/components/common/Header/Header.tsx
  • src/components/profile/MoverBasicInfoEditForm.tsx
  • src/components/common/Header/HeaderSideNav.tsx
  • src/components/profile/ProfileChipGroup.tsx
  • src/lib/constants/apiRoutes.ts

Comment thread src/components/profile/CustomerProfileEditForm.tsx Outdated

@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: 1

Caution

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

⚠️ Outside diff range comments (2)
src/components/common/Header/HeaderSideNav.tsx (1)

44-67: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

데스크톱 전환 시 shouldRender를 직접 해제하세요.

사이드 내비게이션이 열린 상태에서 화면이 xl 이상으로 전환되면 xl:hidden이 패널을 즉시 숨깁니다. 이 경우 transformtransitionend가 발생하지 않아 Line 87의 setShouldRender(false)가 실행되지 않습니다.

그 결과 Line 74-80의 body scroll lock이 계속 유지됩니다. 사용자는 데스크톱 화면에서 페이지를 스크롤할 수 없습니다.

shouldRendertrue인 동안에는 미디어 쿼리를 감시하세요. xl 전환 시 onClose()setShouldRender(false)를 함께 호출하세요.

수정 예시
-  useEffect(() => {
-    if (!isOpen) {
+  useEffect(() => {
+    if (!shouldRender) {
       return;
     }
 
     const mediaQuery = window.matchMedia(MEDIA_QUERY.xl);
 
     const handleBreakpointChange = (event: MediaQueryListEvent) => {
       if (event.matches) {
         onClose();
+        setShouldRender(false);
       }
     };
 
     if (mediaQuery.matches) {
       onClose();
+      setShouldRender(false);
       return;
     }
 
     mediaQuery.addEventListener("change", handleBreakpointChange);
 
     return () => {
       mediaQuery.removeEventListener("change", handleBreakpointChange);
     };
-  }, [isOpen, onClose]);
+  }, [onClose, shouldRender]);
🤖 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/components/common/Header/HeaderSideNav.tsx` around lines 44 - 67, Update
the HeaderSideNav media-query effect to also run while shouldRender is true, not
only when isOpen is true. When the xl breakpoint matches initially or changes to
matched, call both onClose() and setShouldRender(false) so desktop transitions
release rendering and body scroll locking even without a transitionend event;
retain cleanup for the media-query listener.

Source: Coding guidelines

src/components/common/FormField/FormField.tsx (1)

17-19: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

FormFieldProps에서 라벨 연결을 필수화하세요.

labelForlabelId가 모두 선택적이므로 두 값을 생략하거나 동시에 전달하는 잘못된 호출을 허용합니다. labelFor 또는 labelId 중 정확히 하나만 허용하는 union 타입으로 정의하세요.

🤖 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/components/common/FormField/FormField.tsx` around lines 17 - 19, Update
FormFieldProps so labelFor and labelId form an exclusive union: require exactly
one of the two properties while disallowing both together or omitting both.
Preserve the existing string types and use the union directly in the prop
definition.

Sources: Coding guidelines, Path instructions

🤖 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/components/common/Header/isNavLinkActive.ts`:
- Around line 13-23: Update the root-link handling in isNavLinkActive so
APP_ROUTES.MOVER_ESTIMATES.ROOT returns inactive for
APP_ROUTES.MOVER_ESTIMATES.SENT and any path beneath it, while preserving
activation for other root paths. Mirror the existing APP_ROUTES.MOVERS.FAVORITES
exclusion pattern and leave exact matching for the SENT menu unchanged.

---

Outside diff comments:
In `@src/components/common/FormField/FormField.tsx`:
- Around line 17-19: Update FormFieldProps so labelFor and labelId form an
exclusive union: require exactly one of the two properties while disallowing
both together or omitting both. Preserve the existing string types and use the
union directly in the prop definition.

In `@src/components/common/Header/HeaderSideNav.tsx`:
- Around line 44-67: Update the HeaderSideNav media-query effect to also run
while shouldRender is true, not only when isOpen is true. When the xl breakpoint
matches initially or changes to matched, call both onClose() and
setShouldRender(false) so desktop transitions release rendering and body scroll
locking even without a transitionend event; retain cleanup for the media-query
listener.
🪄 Autofix

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: b67a4849-f870-46e7-a31c-d23cdcf263f8

📥 Commits

Reviewing files that changed from the base of the PR and between c32c0a9 and 060c8af.

📒 Files selected for processing (10)
  • src/components/common/Chip/SelectableChip.tsx
  • src/components/common/FormField/FormField.tsx
  • src/components/common/Header/Header.tsx
  • src/components/common/Header/HeaderSideNav.tsx
  • src/components/common/Header/isNavLinkActive.ts
  • src/components/estimate/request/EstimateRequestForm.tsx
  • src/components/profile/CustomerProfileEditForm.tsx
  • src/components/profile/ProfileChipGroup.tsx
  • src/icons/index.ts
  • src/lib/constants/appRoutes.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/components/estimate/request/EstimateRequestForm.tsx
  • src/lib/constants/appRoutes.ts
  • src/icons/index.ts
  • src/components/profile/ProfileChipGroup.tsx
  • src/components/profile/CustomerProfileEditForm.tsx

Comment thread src/components/common/Header/isNavLinkActive.ts
@9g-g9
9g-g9 merged commit 7f87273 into dev Aug 5, 2026
1 check was pending
Comment on lines +69 to +72
useEffect(() => {
if (!shouldRender) {
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

로직을 보면 스크롤 잠금 해제가 shouldRender가 false로 바뀔 때 실행되고, shouldRender는 패널의 transition이 끝나야(transitionend) false가 되게 되어 있는데,
데스크톱 폭에서는 패널이 xl:hidden으로 사라지면서 transition 자체가 실행되지 않아서, shouldRender가 true로 남고 overflow: hidden이 body에 그대로 남는 문제가 있을 수 있을 것 같습니다.
사이드바를 연 상태에서 화면을 데스크톱 폭으로 늘리면 body 스크롤 잠금이 안 풀리는 경우가 있는지 체크해보시고, 혹시 문제가 된다면 스크롤 잠금 effect가 shouldRender 대신 isOpen을 보고 잠그고 해제하도록 바꿔보시면 좋을 것 같아요.

@wndnjs2037

Copy link
Copy Markdown
Collaborator

파일이 많은데도 고객–기사가 1:1 대칭으로 읽히도록 잘 구현해주신 것 같아요.
basic 수정 성공 후 resetField로 basic 필드만 기준값으로 승격하고 profile의 변경 상태는 유지하도록 처리해주셔서, profile 수정이 실패해도 재시도 시 이미 저장된 값이 다시 전송되지 않는 장점이 생긴 것 같습니다.
useMoverAuthReady도 "CUSTOMER 제외"가 아니라 "MOVER 명시 확인" 방식으로 처리
해주셔서, 역할이 확정되기 전에 보호 API가 호출되는 경우를 구조적으로 잘 막아줄 것 같네요!
고생 많으셨습니다 👍

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants