Skip to content

[feat] 마이페이지 드롭다운 구현 (#36)#40

Merged
mgYang53 merged 8 commits intomainfrom
feat/mypage-36
Feb 1, 2026
Merged

[feat] 마이페이지 드롭다운 구현 (#36)#40
mgYang53 merged 8 commits intomainfrom
feat/mypage-36

Conversation

@mgYang53
Copy link
Contributor

@mgYang53 mgYang53 commented Feb 1, 2026

🚀 풀 리퀘스트 제안

📋 작업 내용

Header 컴포넌트의 프로필 아바타 클릭 시 나타나는 마이페이지 드롭다운 구현 및 메인페이지 사용자 닉네임 연동

주요 기능

  • 프로필 이미지 변경/제거 (즉시 반영)
  • 닉네임 수정 (중복 체크 → 저장)
  • 로그아웃
  • 회원 탈퇴 (확인 모달)
  • 메인페이지 "안녕하세요, {닉네임}님!" 인사말 표시
  • 이미지 업로드 5MB 제한

🔧 변경 사항

Phase 1: API & Hooks

  • user.endpoints: PROFILE_IMAGE 엔드포인트 추가
  • user.api: updateNickname, updateProfileImage, deleteProfileImage 함수 추가
  • hooks: useUserProfile, useUpdateNickname, useUpdateProfileImage, useDeleteProfileImage, useDeleteUser 추가

Phase 2: 공통 컴포넌트

  • ProfileImagePicker: 프로필 이미지 선택 UI (variant: onboarding / mypage)
  • NicknameInput: 닉네임 입력 필드 (검증 상태 표시)
  • useProfileForm: 프로필 폼 상태 관리 hook
  • MAX_IMAGE_SIZE: 이미지 5MB 제한 상수 추가

Phase 3: 마이페이지 드롭다운

  • MyPageDropdown: Header Popover에 연동
  • 탈퇴 확인 모달: useGlobalModalStore.openConfirm 활용

Phase 4: 리팩토링 & 통합

  • OnboardingPage: 공통 컴포넌트 적용으로 리팩토링
  • Header: MyPageDropdown Popover 연동

Phase 5: 메인페이지

  • HomePage: useUserProfile 연동, 닉네임 인사말 표시

📸 스크린샷

image image image

📄 기타

  • 파비콘 추가 및 .gitignore 설정
  • 탈퇴 기능은 탈퇴 후 같은 계정으로 가입하려면 DB 값을 지워달라고 백엔드에 문의해야 해서 테스트 못해봤습니다.

Summary by CodeRabbit

  • 새로운 기능

    • 프로필 이미지 선택·변경·제거 UI 추가 (온보딩 및 내 페이지)
    • 닉네임 편집 및 실시간 유효성/중복 검사 추가
    • 헤더에 프로필 드롭다운 메뉴 추가(저장·로그아웃·탈퇴 포함)
    • 홈 화면에 사용자 닉네임 기반 개인화 인사 표시
  • 기타 개선

    • 프로필 이미지 허용 형식 및 최대 5MB 제한 적용
    • 사용자 프로필 데이터 및 관련 라우트·캐시 관리 개선
  • 자산

    • 다양한 파비콘 및 애플 터치 아이콘 추가

✏️ Tip: You can customize this high-level summary in your review settings.

@mgYang53 mgYang53 linked an issue Feb 1, 2026 that may be closed by this pull request
19 tasks
@coderabbitai
Copy link

coderabbitai bot commented Feb 1, 2026

Walkthrough

문제 → 영향 → 대안
문제: 마이페이지(닉네임·프로필 이미지) 편집과 관련된 훅·컴포넌트·API가 부재하거나 중복 관리되어 있었습니다.
영향: 온보딩·헤더·홈에 사용자 프로필 흐름이 산발적으로 구현되어 유지보수·캐시 동기화가 어려웠습니다.
대안: 일관된 쿼리 키, 타입, 이미지 상수와 함께 useProfileForm, 프로필 훅/뮤테이션, ProfileImagePicker, MyPageDropdown, NicknameInput 및 새로운 API(프로필 이미지 엔드포인트 포함)를 추가하여 통합했습니다.

Changes

Cohort / File(s) Summary
API, Endpoints & Types
src/features/user/user.api.ts, src/features/user/user.endpoints.ts, src/features/user/user.types.ts
updateNickname, updateProfileImage, deleteProfileImage 추가; updateUser·UserUpdateInput 제거. User 타입 변경(id→userId, nickname nullable, email 추가). PROFILE_IMAGE 엔드포인트와 NicknameCheckResult, OnboardingInput 타입 추가.
Query Key Factories
src/features/auth/hooks/authQueryKeys.ts, src/features/user/hooks/userQueryKeys.ts
인증/유저 관련 쿼리 키 팩토리(authQueryKeys, userQueryKeys) 추가로 쿼리 키 중앙화 및 타입 안전성 확보.
User Hooks
src/features/user/hooks/...
useUserProfile.ts, useUpdateNickname.ts, useUpdateProfileImage.ts, useDeleteProfileImage.ts, useDeleteUser.ts, useProfileForm.ts, useOnboarding.ts, useCheckNickname.ts, index.ts
프로필 조회·업데이트·삭제 훅 추가 및 기존 훅 타입 강화와 캐시 동기화 개선. useProfileForm은 닉네임 검증(중복 체크 포함), 이미지 업로드/미리보기, 즉시/지연 업로드 모드 등을 캡슐화.
User Components
src/features/user/components/...
MyPageDropdown.tsx, NicknameInput.tsx, ProfileImagePicker.tsx, index.ts
마이페이지 드롭다운, 닉네임 입력, 이미지 픽커(온보딩/마이페이지 변형) 추가. 이미지 크기/포맷 검사 및 글로벌 모달 연동 포함. 바렐에 재수출.
Integration (Pages & Layout)
src/pages/Auth/OnboardingPage.tsx, src/pages/Home/HomePage.tsx, src/shared/layout/components/Header.tsx
OnboardingPage가 useProfileForm과 새 컴포넌트를 사용하도록 리팩터. Header에 Popover 기반 MyPageDropdown 통합(조건부 프로필 조회). HomePage는 useUserProfile로 닉네임 표시.
Constants
src/shared/constants/image.ts, src/shared/constants/index.ts, src/shared/constants/routes.ts
이미지 관련 상수(ALLOWED_IMAGE_*, MAX_IMAGE_SIZE) 추가 및 재수출. ROUTES에 INVITE_BASE 및 INVITE(inviteCode) 경로 추가.
Config & Assets
.gitignore, index.html
.env.development, .env.production, .env.test.local, /docs 등 .gitignore 확장. favicon 및 apple-touch-icon 링크 추가(기존 svg 제거).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Header/Dropdown
    participant Form as useProfileForm
    participant Query as useUserProfile
    participant MutImg as useUpdateProfileImage
    participant MutName as useUpdateNickname
    participant API as Backend
    participant Cache as ReactQueryCache

    User->>UI: 아바타 클릭 -> 드롭다운 오픈
    UI->>Query: 프로필 요청 (enabled)
    Query->>API: fetchCurrentUserProfile()
    API-->>Cache: 프로필 응답 캐시
    Cache-->>UI: 프로필 데이터 제공
    User->>UI: 이미지 선택 / 닉네임 입력
    UI->>Form: handleImageChange / setNickname
    alt immediate image upload
        Form->>MutImg: mutate(file)
        MutImg->>API: updateProfileImage(file)
        API-->>MutImg: User 응답
        MutImg->>Cache: setQueryData(profile)
    end
    User->>UI: 저장 클릭
    UI->>MutName: mutate(nickname)
    MutName->>API: updateNickname(nickname)
    API-->>MutName: User 응답
    MutName->>Cache: invalidateQueries(profile)
    Cache-->>UI: 최신 프로필 반영
Loading

🎯 Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • choiyoungae
  • haruyam15
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 주요 변경사항인 마이페이지 드롭다운 구현을 명확하게 설명하고 있으며, 구현된 기능(프로필 이미지 변경/삭제, 닉네임 편집, 로그아웃, 탈퇴)과 정확히 일치합니다.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/mypage-36

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.

@mgYang53 mgYang53 self-assigned this Feb 1, 2026
@mgYang53 mgYang53 added feat 새로운 기능 추가 refactor 코드 리팩토링 chore 설정과 기타 작업들을 포함한 추가 및 수정 labels Feb 1, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/features/user/components/MyPageDropdown.tsx`:
- Around line 29-42: The form's nickname state in useProfileForm isn't updating
when the initialNickname/initialImageUrl props change, causing stale values; fix
this by adding a useEffect inside useProfileForm that watches initialNickname
and initialImageUrl and calls setNickname and sets profileImagePreview (or
equivalent state) when those props change, ensuring the component's nickname and
profileImagePreview sync with incoming user updates; alternatively, as an
option, update MyPageDropdown to only call useProfileForm after user is loaded
(conditional render) to avoid passing empty initial values.

In `@src/features/user/hooks/useOnboarding.ts`:
- Around line 38-41: The build fails because authQueryKeys is referenced but not
defined; fix by either adding an authQueryKeys factory in src/features/auth that
mirrors the userQueryKeys pattern (exporting e.g. authQueryKeys.me() returning
['auth','me']), or by replacing authQueryKeys.me() in useOnboarding (the
Promise.all call that calls queryClient.invalidateQueries) with the same literal
key used in useAuth (['auth','me']); update imports accordingly so
useOnboarding.ts no longer references an undefined symbol.

In `@src/shared/layout/components/Header.tsx`:
- Around line 31-38: Header.tsx references ROUTES.INVITE_BASE when computing
isInvitePage and enabling useUserProfile, but that constant is missing; add a
new ROUTES.INVITE_BASE entry to your ROUTES export (in the routes constants
file) with the invite base path or replace the reference in Header.tsx (the
isInvitePage calculation) with the actual invite path string; ensure the ROUTES
symbol name matches other usages and update imports if needed so isInvitePage
and useUserProfile({ enabled: !isInvitePage || isLoggedIn }) evaluate correctly.
🧹 Nitpick comments (5)
src/pages/Home/HomePage.tsx (1)

4-8: 로딩 상태 처리 고려.

useUserProfile()isLoading 상태를 활용하지 않아, 초기 로딩 시 빈 인사말(안녕하세요, 님!)이 잠깐 노출될 수 있습니다.

간단한 스켈레톤이나 조건부 렌더링으로 UX 개선 가능합니다.

💡 선택적 개선안
 export default function HomePage() {
-  const { data: user } = useUserProfile()
+  const { data: user, isLoading } = useUserProfile()

   return (
     <div className="flex flex-col gap-xtiny pt-xlarge">
-      <h1 className="text-black typo-heading2">안녕하세요, {user?.nickname ?? ''}님!</h1>
+      <h1 className="text-black typo-heading2">
+        {isLoading ? '안녕하세요!' : `안녕하세요, ${user?.nickname ?? ''}님!`}
+      </h1>
       <p className="text-grey-600 typo-heading2">읽고 있는 책과 생각을 기록해보세요</p>
     </div>
   )
 }
src/features/user/hooks/useUpdateNickname.ts (1)

18-23: 캐시 업데이트 전략 불일치 (참고)

useUpdateProfileImagesetQueryData로 즉시 캐시 갱신하고, 이 훅은 invalidateQueries로 refetch를 트리거합니다. updateNickname API도 User를 반환하므로 setQueryData 사용이 가능합니다.

현재 구현도 정상 동작하지만, 일관성을 위해 동일한 패턴 적용을 고려해볼 수 있습니다.

♻️ setQueryData 패턴 적용 예시
   return useMutation<User, ApiError, string>({
     mutationFn: (nickname) => updateNickname(nickname),
-    onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: userQueryKeys.profile() })
+    onSuccess: (data) => {
+      queryClient.setQueryData<User>(userQueryKeys.profile(), data)
     },
   })
src/features/user/components/ProfileImagePicker.tsx (1)

114-118: showRemoveButtononRemove 간의 암묵적 의존성

showRemoveButton={true}일 때 onRemoveundefined면 버튼이 보이지만 클릭해도 아무 동작이 없습니다.

의도적 설계라면 무시해도 되지만, 방어적으로 조건을 강화할 수 있습니다.

♻️ 제안: 조건 강화
-        {showRemoveButton && (
+        {showRemoveButton && onRemove && (
           <TextButton onClick={onRemove} disabled={disabled}>
             제거하기
           </TextButton>
         )}
src/features/user/hooks/useProfileForm.ts (2)

77-96: 닉네임 변경 여부 중복 체크

isNicknameChanged (Line 84)와 debouncedNickname !== initialNickname (Line 90) 조건이 checkNicknameChange=true일 때 중복됩니다.

현재 동작에 문제는 없지만, 조건을 단순화하면 가독성이 향상됩니다.

♻️ 제안: 조건 단순화
  const shouldCheckNickname =
    debouncedNickname.length >= 2 &&
-   isNicknameChanged &&
-   debouncedNickname !== initialNickname &&
+   (!checkNicknameChange || debouncedNickname !== initialNickname) &&
    NICKNAME_REGEX.test(debouncedNickname)

138-157: FileReader 오류 처리 누락

FileReader.onerror가 처리되지 않아 파일 읽기 실패 시 사용자에게 피드백이 없습니다.

실제 발생 가능성이 낮지만, 방어적 처리를 권장합니다.

♻️ 제안: 에러 핸들러 추가
        const reader = new FileReader()
        reader.onloadend = () => {
          setProfileImagePreview(reader.result as string)
        }
+       reader.onerror = () => {
+         console.error('Failed to read file')
+         setProfileImageFile(null)
+       }
        reader.readAsDataURL(file)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/features/user/hooks/useProfileForm.ts`:
- Around line 103-126: The nicknameStatus memo incorrectly treats nicknameCheck
=== undefined as 'checking' unconditionally; update the branching in
useProfileForm's nicknameStatus so that the 'checking' branch requires
shouldCheckNickname (use shouldCheckNickname && (isCheckingNickname ||
nicknameCheck === undefined)); when shouldCheckNickname is false, decide between
'unchanged' or 'available' according to the checkNicknameChange option and the
relationship between debouncedNickname and initialNickname (so canSubmit isn't
blocked when no duplicate check is needed). Ensure you reference and adjust the
nicknameStatus computation and any logic using nicknameCheck,
isCheckingNickname, shouldCheckNickname, checkNicknameChange, debouncedNickname,
and initialNickname.
🧹 Nitpick comments (2)
src/shared/constants/routes.ts (1)

19-21: INVITE 경로 파라미터 인코딩 필요

문제: invitationCode를 그대로 경로에 삽입해 특수문자(/, ?, #, 공백 등) 포함 시 라우팅이 깨질 수 있어요.
영향: 초대 코드에 따라 링크 이동 실패/잘못된 경로 매칭이 발생할 수 있습니다.
대안: 경로 생성 시 encodeURIComponent로 안전하게 인코딩하세요.

🔧 수정 제안
-  INVITE: (invitationCode: string) => `/invite/${invitationCode}`,
+  INVITE: (invitationCode: string) =>
+    `/invite/${encodeURIComponent(invitationCode)}`,
src/features/auth/hooks/index.ts (1)

1-1: useAuth에서 authQueryKeys factory 사용으로 일관성 확보

authQueryKeys factory를 도입했으나 useAuth.ts line 24에서 여전히 하드코딩된 ['auth', 'me']를 사용 중입니다. 동일한 키 값이지만, query key의 단일 진실 공급원(single source of truth)이 손상되어 나중에 키 구조 변경 시 여러 곳을 수정해야 합니다.

✅ 수정 제안
// src/features/auth/hooks/useAuth.ts
+import { authQueryKeys } from './authQueryKeys'

 export function useAuth() {
   return useQuery({
-    queryKey: ['auth', 'me'],
+    queryKey: authQueryKeys.me(),
     queryFn: fetchCurrentUser,
     retry: false,
     staleTime: 5 * 60 * 1000,
   })
 }

@mgYang53 mgYang53 merged commit 62e59a9 into main Feb 1, 2026
1 check passed
@mgYang53 mgYang53 deleted the feat/mypage-36 branch February 1, 2026 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore 설정과 기타 작업들을 포함한 추가 및 수정 feat 새로운 기능 추가 refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 마이페이지 드롭다운 구현

2 participants