Skip to content

[feat] 프로필 수정 api 연동#78

Merged
hansololiviakim merged 6 commits intodevelopfrom
feature/#76/mycd-edit
Aug 29, 2025
Merged

[feat] 프로필 수정 api 연동#78
hansololiviakim merged 6 commits intodevelopfrom
feature/#76/mycd-edit

Conversation

@hansololiviakim
Copy link
Member

@hansololiviakim hansololiviakim commented Aug 29, 2025

🛰️ 관련 이슈


✨ 주요 변경 사항

1️⃣ 프로필 api 연동
2️⃣ interceptor에서 토큰 만료 시 /login 랜딩 추가
3️⃣ cd sticker와 함께 보여지도록 overlay 출력 방식 수정


🔍 테스트 방법 / 체크리스트

  • 없음

🗯️ PR 포인트

  • 백엔드 토큰이 35분이어서 언제 만료됐는지 파악이 어려웠는데요..
    로그인 토큰 만료로 응답 오면 로그아웃해서 데이터 날리고 /login 으로 넘어가도록 interceptor 수정했습니다
  • cd sticker가 레이어 순번에 맞춰서 z-index를 먹는데 cd overlay 이미지가 거기에 묻혀서, 오버레이 렌더링 하는 방식을 바꿨습니다!
  • 프로필 수정 api 를 연동했습니다. 백엔드 수정사항이 있을 것 같은데 만약 있으면 다른 티켓에서 같이 진행하겠습니다!

🚀 알게된 점

  • 없음

📖 참고 자료 (선택)

  • 없음

Summary by CodeRabbit

  • New Features

    • 로그인 만료 토스트(AUTH_EXPIRED) 추가 및 로그인 페이지에서 자동 표시
    • 프로필 편집 기능 개선: 이미지 미리보기, 5MB 용량 검사, 닉네임 검증 및 저장 중 로딩 표시
    • 편집 모드 지원: 마이페이지 커스터마이즈에서 편집용 로딩/에러 처리 및 데이터 로드
    • 플레이리스트에서 편집 버튼으로 커스터마이즈 페이지로 이동, 스티커 표시 지원
  • Bug Fixes

    • 인증 만료 시 자동 로그아웃/재인증 처리로 실패 감소
    • 비회원 둘러보기 시 홈으로 이동하도록 수정
    • 플레이리스트 곡 목록 정렬 일관성 개선

@hansololiviakim hansololiviakim self-assigned this Aug 29, 2025
@hansololiviakim hansololiviakim requested a review from maylh as a code owner August 29, 2025 08:14
@hansololiviakim hansololiviakim added the HIGH 빠르게 처리해야 하는 높은 우선순위 label Aug 29, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 29, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Caution

Review failed

The pull request is closed.

Walkthrough

프로필 PATCH API·타입·훅 추가 및 마이페이지 프로필 UI 연동, 인증 스토어에 사용자 정보 갱신 액션 추가, axios 응답 인터셉터의 401/익명 토큰 재시도 로직 확장, 로그인 페이지 만료 토스트 표시 및 게스트 경로 변경, 플레이리스트 엔드포인트/프롭/정렬 및 CD 오버레이 렌더링 방식 변경, 라우트 import 경로 정리, 일부 쿼리의 enabled 조건 추가.

Changes

Cohort / File(s) Summary
Auth & token handling
src/shared/api/instance.ts, src/features/auth/store/authStore.ts, src/features/auth/types/auth.ts, src/pages/login/index.tsx, src/widgets/authGuard/PrivateRoute.tsx, src/app/providers/ToastProvider.tsx
axios 응답 인터셉터에서 COMMON-401/401 처리 및 익명 토큰 재발급·요청 재시도 추가, 로그인 사용자 강제 로그아웃·리다이렉트 처리, useAuthStoreupdateUserInfo 액션 추가·타입 확장, 로그인 페이지에 AUTH_EXPIRED 토스트 표시 로직 및 게스트 네비게이션 대상 변경, PrivateRoute 네비게이션 동작 변경, 토스트 타입/메시지에 AUTH_EXPIRED 추가.
Profile feature
src/features/profile/api/profile.ts, src/features/profile/model/useProfile.ts, src/features/profile/types/profile.ts, src/pages/myPage/ui/components/UserProfile.tsx
프로필 PATCH API(FormData, multipart) 추가, useProfile mutation 훅 추가, ProfilePayload/ProfileResponse 타입 추가, UserProfile 컴포넌트에 파일 검증(최대 5MB), 프리뷰 URL 관리, 로딩 표시 및 성공 시 전역 스토어 updateUserInfo 호출로 동기화 구현.
Playlist adjustments
src/entities/playlist/api/playlist.ts, src/widgets/playlist/PlaylistHorizontal.tsx, src/pages/myPage/ui/playlist/index.tsx
대표 플레이리스트 엔드포인트 경로 변경, PlaylistHorizontalstickers?: CdCustomData[] prop 추가 및 전달, 곡 리스트를 id 기준 정렬, 편집 버튼이 커스터마이즈 페이지로 네비게이션.
Customize edit mode
src/pages/myPage/ui/customize/index.tsx
URL playlistId 기반 편집 모드 분기 도입, 편집 시 단건 데이터 fetch 적용, 로딩/에러 처리 및 에러시 /error 네비게이션.
Query gating
src/entities/playlist/model/usePlaylists.ts
무한쿼리(useInfiniteQuery)에 enabled 조건 추가: accessToken 또는 anonymous_token 존재 시에만 실행.
Routing config
src/shared/config/routesConfig.ts
지연 로드(import) 경로 접두사 @pages/...@/pages/...로 변경.
UI: CD overlay
src/shared/ui/Cd.tsx
Overlay 아이콘 컴포넌트 제거, SVG URL(?url) 기반 CSS 오버레이로 교체(포인터 이벤트 비허용, blend 적용).
Misc formatting
src/pages/mycd/playlist/index.tsx
import-컴포넌트 사이 공백 제거(무기능 변경).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant App
  participant Axios as axiosInstance
  participant Server

  User->>App: API 요청
  App->>Axios: 요청 전달
  Axios->>Server: HTTP 요청
  Server-->>Axios: 401 / COMMON-401 응답
  alt 로그인 상태
    Axios->>App: logout() 호출 → navigate('/login')
  else 익명 상태
    Axios->>Server: 익명 토큰 요청
    Server-->>Axios: anonymous_token
    Axios->>Server: 원요청 재시도(새 토큰)
    Server-->>Axios: 200 OK
    Axios-->>App: response.data 반환
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant UI as UserProfile
  participant MQ as useProfile
  participant API as patchProfile
  participant Server
  participant Store as useAuthStore

  User->>UI: 프로필 저장 클릭
  UI->>MQ: mutate(FormData)
  MQ->>API: patchProfile 호출
  API->>Server: PATCH /main/mypage/playlists/me
  Server-->>API: ProfileResponse
  API-->>MQ: 응답 전달
  MQ-->>UI: onSuccess(ProfileResponse)
  UI->>Store: updateUserInfo(ProfileResponse)
  UI-->>User: 프리뷰/닉네임 갱신, 로딩 해제
Loading
sequenceDiagram
  autonumber
  actor User
  participant LoginPage
  participant LS as localStorage
  participant Toast as ToastProvider

  User->>LoginPage: 페이지 진입
  LoginPage->>LS: getItem('show_expired_toast')
  alt 'true'
    LoginPage->>Toast: toast('AUTH_EXPIRED')
    LoginPage->>LS: removeItem('show_expired_toast')
  else
    Note right of LoginPage: 토스트 없음
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
프로필 수정 API 추가 및 연동 (#76)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
axios 인터셉터의 401 처리·익명 토큰 재발급 로직 변경 (src/shared/api/instance.ts) 프로필 PATCH API 연동(#76) 범위와 직접 관련 없음; 전역 인증/네트워크 로직 수정임.
토스트 타입/로그인 페이지 만료 토스트 표시 및 게스트 네비게이션 변경 (src/app/providers/ToastProvider.tsx, src/pages/login/index.tsx) 프로필 수정 API 요구사항에 포함되지 않은 UI/네비게이션 변경임.
CD 오버레이 렌더링 방식 변경 (src/shared/ui/Cd.tsx) 프로필 API 연동과 무관한 UI 구현 변경임.

Possibly related PRs

Suggested reviewers

  • maylh

Poem

(\_/) 깡총, 코드가 바뀌었네요 🐇
(•ᴗ•) 닉네임과 사진, 프리뷰가 반짝
(⌒▽⌒) 토큰은 새로, 요청은 다시 시도
(✿◠‿◠) CD 위엔 새 오버레이 반짝
(づ。◕‿‿◕。)づ 머지 토끼가 축하 춤을 춰요 🎉


📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4c1f5c6 and c64d86f.

📒 Files selected for processing (4)
  • src/entities/playlist/api/playlist.ts (1 hunks)
  • src/entities/playlist/model/usePlaylists.ts (2 hunks)
  • src/pages/mycd/playlist/index.tsx (0 hunks)
  • src/shared/ui/Cd.tsx (3 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#76/mycd-edit

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @hansololiviakim, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 사용자 프로필 수정 기능을 백엔드 API와 연동하고 관련 UI/UX를 개선하는 데 중점을 둡니다. 또한, 인증 토큰 만료 시 사용자 경험을 향상시키기 위한 인터셉터 로직을 추가했으며, CD 컴포넌트의 시각적 요소를 조정하여 스티커와 오버레이가 자연스럽게 어우러지도록 했습니다.

Highlights

  • 프로필 수정 기능: 사용자 프로필 수정 API를 연동하고 관련 UI/UX를 개선했습니다.
  • 인증 토큰 만료 처리: 토큰 만료 시 자동 로그아웃 및 로그인 페이지로 리다이렉트되는 인터셉터 로직을 추가했습니다.
  • CD 컴포넌트 렌더링 개선: CD 스티커와 오버레이가 올바르게 표시되도록 렌더링 방식을 개선했습니다.
  • 플레이리스트 편집 모드: 플레이리스트 편집 모드 진입 및 데이터 로딩 기능을 추가했습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions
Copy link

github-actions bot commented Aug 29, 2025

🎵 Storybook Link 🎵
🔗 https://689dbb45f8d09aea7832eeb1-nfetvutlcf.chromatic.com/

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

프로필 수정 API 연동, 인터셉터를 통한 토큰 만료 처리 개선, CD 오버레이 UI 수정 등 전반적인 기능 구현이 잘 이루어졌습니다. 코드 구조도 좋습니다. 리뷰 과정에서 비동기 처리에서의 상태 관리, 캐시 데이터의 불변성 유지 등 몇 가지 개선점을 발견했습니다. 특히 프로필 수정 핸들러의 로직과 쿼리 캐시에서 가져온 목록을 정렬하는 부분에서 잠재적인 문제가 있어 수정을 제안합니다. 또한, 반응성이 보장되지 않는 상태 접근 방식과 불필요한 console.log에 대한 의견도 남겼습니다.

Comment on lines +66 to +74
// 초기화
setIsEditMode(false)
setIsFileError(false)
setHasErrorMsg('')
setUpdatedProfile({
nickname: userInfo.username,
profileImg: userInfo?.userProfileImageUrl || null,
profileImage: userInfo?.userProfileImageUrl || null,
file: null,
})
setPreviewImage(userInfo?.userProfileImageUrl || null)

Choose a reason for hiding this comment

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

high

mutate 함수는 비동기적으로 동작하는데, 바로 다음에 상태를 초기화하는 로직이 있어 API 요청이 완료되기 전에 UI가 초기화되는 문제가 발생합니다. 이는 사용자 경험을 해치고 의도치 않은 동작을 유발할 수 있습니다. 1 초기화 로직은 mutate 함수의 onSuccessonError 콜백 안에서 처리되어야 합니다. 현재 onSuccess 콜백에 이미 성공 시 상태를 업데이트하는 로직이 있으므로, 이 중복되고 잘못된 위치에 있는 초기화 코드는 제거하는 것이 좋습니다.

Style Guide References

Footnotes

  1. 비동기 작업(API 호출 등)에서 발생하는 에러를 적절히 처리하도록 권장합니다. Tanstack Query 사용 시 onError 콜백 등을 활용합니다.

Comment on lines +146 to +150
{playlistData?.songs
.sort((a, b) => a.id - b.id)
.map((song) => (
<Link key={song.id} data={song} />
))}

Choose a reason for hiding this comment

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

high

Array.prototype.sort()는 원본 배열을 직접 수정(mutate)합니다. playlistData.songs는 React Query 캐시에서 온 데이터이므로 직접 수정하는 것은 안티패턴이며, 다른 컴포넌트에서 예기치 않은 동작을 유발할 수 있습니다. 1 정렬하기 전에 배열의 복사본을 만들어 불변성을 유지해야 합니다.

        {playlistData?.songs
          .slice()
          .sort((a, b) => a.id - b.id)
          .map((song) => (
            <Link key={song.id} data={song} />
          ))}

Style Guide References

Footnotes

  1. 서버 상태 관리에 Tanstack Query를 적극 활용하여 데이터 fetching, 캐싱, 동기화, 에러 처리 등을 효율적으로 관리하도록 권장합니다.

}
return undefined
},
enabled: !!useAuthStore.getState().accessToken || !!localStorage.getItem('anonymous_token'),

Choose a reason for hiding this comment

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

medium

useAuthStore.getState()를 사용하는 것은 Zustand 상태 변경에 반응하지 않습니다. 이로 인해 enabled 옵션은 훅이 처음 호출될 때 한 번만 계산되고, 이후 로그인 상태가 변경되어도 쿼리가 자동으로 활성화/비활성화되지 않을 수 있습니다. 1 상태 변경에 반응하도록 하려면 useAuthStore 훅을 사용하여 상태 값을 구독하는 것이 좋습니다.

Style Guide References

Footnotes

  1. Zustand, Tanstack-Query 등 상태 관리 패턴이 일관성 있게 사용되었는지 확인하고, 복잡한 상태 로직은 커스텀 훅으로 분리하도록 제안해주세요.

isEditMode ? Number(playlistId) : -1
)

console.log(playlistData)

Choose a reason for hiding this comment

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

medium

디버깅 목적으로 사용된 console.log 문이 남아있습니다. 프로덕션 코드에 포함되지 않도록 병합 전에 제거해주세요. 1

Style Guide References

Footnotes

  1. 불필요하거나 자명한 주석은 피하도록 안내해주세요.

Copy link
Collaborator

@maylh maylh left a comment

Choose a reason for hiding this comment

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

백엔드 토큰이 35분이어서 언제 만료됐는지 파악이 어려웠는데요..
로그인 토큰 만료로 응답 오면 로그아웃해서 데이터 날리고 /login 으로 넘어가도록 interceptor 수정했습니다

요 부분 토큰 재발급이 아니라 아예 날리고 다시 로그인 하게 하는 걸로 정해진건가용 ??
나머지는 확인했습니다 ~~~~~~~~~
수고하셨어요 !!!!!! 🪄

@hansololiviakim
Copy link
Member Author

요 부분 토큰 재발급이 아니라 아예 날리고 다시 로그인 하게 하는 걸로 정해진건가용 ??

재발급 하는 api 가 없는 것 같기도 하고 원래 일주일로 늘려주신다고 하셔서 일단 그거에 맞춰보려구요!
qa 때 백엔드에 다시 여쭤보겠습니다 ㅎ.ㅎ

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: 8

Caution

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

⚠️ Outside diff range comments (3)
src/shared/ui/Cd.tsx (1)

33-36: propId 오프셋 미정의 시 NaN 및 잘못된 파일명 생성 가능성

THEME_PROP_ID_OFFSET에 키가 없거나 값이 잘못되면 localIndexNaN이 되어 매칭 실패가 발생합니다. 가드 추가를 권장합니다.

적용 제안:

-    const offset = THEME_PROP_ID_OFFSET[convertThemeName as keyof typeof THEME_PROP_ID_OFFSET]
-    const localIndex = propId - offset
+    const offset = THEME_PROP_ID_OFFSET[convertThemeName as keyof typeof THEME_PROP_ID_OFFSET]
+    if (offset == null || Number.isNaN(offset)) {
+      console.warn(`Unknown offset for theme=${convertThemeName}`)
+      return ''
+    }
+    const localIndex = propId - offset
src/pages/myPage/ui/playlist/index.tsx (1)

98-100: 렌더 중 네비게이션 호출은 경고 유발 가능 → useEffect로 이전

rendernavigate 호출 대신 effect에서 처리하세요.

적용 제안:

-  if (isError) {
-    navigate('/error')
-  }
+  useEffect(() => {
+    if (isError) {
+      navigate('/error', { replace: true })
+    }
+  }, [isError, navigate])

그리고 상단 import를 다음과 같이 수정하세요:

-import { useState } from 'react'
+import { useEffect, useState } from 'react'
src/entities/playlist/model/usePlaylists.ts (1)

17-42: Zustand getState() 직접 호출 및 localStorage 접근 통일 필요

  • src/entities/playlist/model/usePlaylists.ts: getState() 대신 selector 기반 구독으로 변경, queryKey에 auth 스코프(accessToken / userId 또는 ‘anonymous’) 추가, enabled를 selector 기반 accessToken / isAnonymous로 설정
  • src/pages/login/index.tsx: useEffect 내 useAuthStore.getState() → const isLogin = useAuthStore(s => s.isLogin)로 구독해 로그인 상태 변경 시 redirect 트리거
  • src/shared/api/instance.ts: Axios 인터셉터 내 getState()는 매 요청 시 최신 상태를 반환하므로 유지 가능하지만, localStorage.getItem('anonymous_token') 호출을 별도 토큰 조회 유틸(getToken())로 추출해 중복 제거 및 테스트·캐시 일관성 확보
🧹 Nitpick comments (13)
src/app/providers/ToastProvider.tsx (1)

7-15: ToastType/TOAST_MESSAGES 중복 정의 제거로 타입-값 동기화

키 목록을 단일 소스로 관리하면 신규 타입 추가 시 누락 가능성을 줄일 수 있습니다.

다음과 같이 정리하면 됩니다:

-type ToastType = 'LINK' | 'IMAGE' | 'REPORT' | 'COMMENT' | 'AUTH_EXPIRED'
-
-const TOAST_MESSAGES: Record<ToastType, string> = {
+const TOAST_MESSAGES = {
   LINK: '링크가 복사됐어요',
   IMAGE: '이미지가 저장됐어요',
   REPORT: '신고가 접수됐어요',
   COMMENT: '댓글이 삭제됐어요',
   AUTH_EXPIRED: '로그인 정보가 만료되었어요',
-}
+} as const
+
+type ToastType = keyof typeof TOAST_MESSAGES
src/pages/login/index.tsx (1)

57-57: 게스트 이동 시 히스토리 정리 제안

홈으로 이동은 replace: true를 사용해 로그인 페이지가 히스토리에 남지 않도록 하는 편이 UX에 유리합니다.

-          <GuestButton type="button" onClick={() => navigate('/')}>
+          <GuestButton type="button" onClick={() => navigate('/', { replace: true })}>
src/features/profile/types/profile.ts (1)

1-5: 서로 배타적인 파일/이미지 입력을 타입으로 명시

fileprofileImage가 동시에 존재/부재 가능한 상태를 막기 위해 분기형 타입으로 제약을 걸면 안전합니다.

-export interface ProfilePayload {
-  nickname: string
-  file: File | null
-  profileImage: string | null
-}
+export type ProfilePayload =
+  | { nickname: string; file: File; profileImage?: never }
+  | { nickname: string; file?: null; profileImage: string | null }
src/shared/ui/Cd.tsx (2)

58-66: 장식용 이미지 접근성: alt 비우고 aria-hidden 추가

스티커 이미지는 장식용으로 보이며 반복적 대체 텍스트가 스크린리더에 불필요한 노이즈를 줄 수 있습니다. alt=""aria-hidden 적용을 권장합니다.

적용 제안:

-            alt="cd-sticker"
+            alt=""
+            aria-hidden="true"

127-133: 혼합 모드 렌더 순서 보장: z-index 명시 권장

현재는 DOM 순서에 의존합니다. 명시적인 z-index로 레이어링을 고정하면 예기치 않은 스태킹 컨텍스트 변화에 견고해집니다.

적용 제안:

 const Overlay = styled.div`
   position: absolute;
   inset: 0;
   background: url(${overlayUrl}) no-repeat center/cover;
   mix-blend-mode: multiply;
   pointer-events: none;
+  z-index: 1;
 `
src/pages/myPage/ui/customize/index.tsx (2)

50-50: 디버그 콘솔 제거

불필요한 console.log는 제거하세요.

적용 제안:

-  console.log(playlistData)

52-56: 에러 페이지 이동 시 히스토리 정리 권장

뒤로가기로 에러 페이지 루프를 방지하려면 replace 사용이 안전합니다.

적용 제안:

-    if (isError) {
-      navigate('/error')
-    }
+    if (isError) {
+      navigate('/error', { replace: true })
+    }
src/features/profile/model/useProfile.ts (1)

5-10: 훅 명확성: 업데이트 전용이면 이름을 usePatchProfile로 변경 제안

해당 훅이 프로필 “조회”가 아닌 “수정(PATCH)”만 담당하므로 의도를 드러내는 네이밍이 유지보수에 유리합니다.

src/widgets/authGuard/PrivateRoute.tsx (1)

12-21: isLogin 변경 시 모달 상태 동기화

로그인/로그아웃 전환 때 모달 열림 상태가 초기값에 고정될 수 있습니다. isLogin을 의존성으로 동기화하세요.

-import { useState } from 'react'
+import { useEffect, useState } from 'react'
...
-  const [isModalOpen, setIsModalOpen] = useState(!isLogin)
+  const [isModalOpen, setIsModalOpen] = useState(!isLogin)
+  useEffect(() => {
+    setIsModalOpen(!isLogin)
+  }, [isLogin])
src/pages/myPage/ui/components/UserProfile.tsx (4)

151-152: 입력 단계에서 trim 적용은 사용성 저하

타이핑 중 실시간 trim()은 공백 입력을 막아 UX가 떨어집니다. 저장 시에만 trim하도록 변경 권장.

-            onChange={(e) =>
-              setUpdatedProfile({ ...updatedProfile, nickname: e.target.value.trim() })
-            }
+            onChange={(e) =>
+              setUpdatedProfile({ ...updatedProfile, nickname: e.target.value })
+            }

121-134: 업로드 중 편집/파일 선택 비활성화 필요

isPending 동안 버튼/파일 입력을 막아 중복 제출과 레이스 컨디션을 방지하세요.

               <ProfileImgEditBtn
                 type="button"
                 aria-label="프로필 이미지 수정"
                 onClick={() => fileInputRef.current?.click()}
+                disabled={isPending}
+                aria-disabled={isPending}
               >
@@
               <input
                 type="file"
                 ref={fileInputRef}
                 accept="image/*"
                 onChange={onFileChange}
                 hidden
+                disabled={isPending}
               />

156-158: 저장 버튼 비활성화로 중복 제출 방지

로딩 중 버튼을 비활성화하고 접근성 속성을 추가하세요.

-        <ProfileEditBtn type="button" onClick={onProfileEditClick}>
+        <ProfileEditBtn
+          type="button"
+          onClick={onProfileEditClick}
+          disabled={isPending}
+          aria-busy={isPending}
+        >
           {isEditMode ? '저장하기' : '프로필 편집'}
         </ProfileEditBtn>

81-95: 파일 MIME 타입 검증 추가 권장

accept는 신뢰할 수 없습니다. 실제 file.type으로 이미지인지 확인해 주세요.

   const file = e.target.files[0]
+  if (!file.type.startsWith('image/')) {
+    setHasErrorMsg('이미지 파일만 업로드 가능해요')
+    if (fileInputRef.current) fileInputRef.current.value = ''
+    return
+  }
   if (file.size > MAX_FILE_SIZE) {
     setHasErrorMsg('5MB 이하의 파일만 업로드 가능해요')
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 498506e and 4c1f5c6.

📒 Files selected for processing (17)
  • src/app/providers/ToastProvider.tsx (1 hunks)
  • src/entities/playlist/api/playlist.ts (1 hunks)
  • src/entities/playlist/model/usePlaylists.ts (2 hunks)
  • src/features/auth/store/authStore.ts (2 hunks)
  • src/features/auth/types/auth.ts (2 hunks)
  • src/features/profile/api/profile.ts (1 hunks)
  • src/features/profile/model/useProfile.ts (1 hunks)
  • src/features/profile/types/profile.ts (1 hunks)
  • src/pages/login/index.tsx (4 hunks)
  • src/pages/myPage/ui/components/UserProfile.tsx (1 hunks)
  • src/pages/myPage/ui/customize/index.tsx (3 hunks)
  • src/pages/myPage/ui/playlist/index.tsx (2 hunks)
  • src/shared/api/instance.ts (1 hunks)
  • src/shared/config/routesConfig.ts (1 hunks)
  • src/shared/ui/Cd.tsx (3 hunks)
  • src/widgets/authGuard/PrivateRoute.tsx (2 hunks)
  • src/widgets/playlist/PlaylistHorizontal.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

**/*.{ts,tsx,js,jsx}: ## 1. 일반적인 코딩 컨벤션

포맷팅

  • .prettierrc 설정에 따라 포맷팅 확인
  • 들여쓰기: 2칸 스페이스
  • 최대 줄 길이: 100자
  • 세미콜론 사용 안함
  • 따옴표: 작은따옴표 사용
  • 괄호 안 공백: 있음
  • 화살표 함수 괄호: 항상 사용
  • 줄바꿈: LF 사용

네이밍 컨벤션

  • 컴포넌트: PascalCase (예: UserProfile)
  • 유틸리티/훅/변수: camelCase (예: getUserData, useUserInfo)
  • 상수: UPPER_SNAKE_CASE (예: API_BASE_URL)
  • 이미지 파일: kebab-case (예: user-profile-icon.png)

주석 사용

  • 복잡한 로직에만 주석 추가
  • 불필요한 주석 지양 (코드로 설명 가능한 것)
  • TODO/FIXME 형식: // TODO: 설명 - 작성자

가독성

  • 매직 넘버 지양, 의미있는 상수 사용
  • 함수는 하나의 책임만 가지도록 작성 (최대 20줄 권장)
  • 중첩 깊이 최소화 (3단계 이하 권장)

2. React 모범 사례

컴포넌트 작성

  • 최신 React hooks 사용 권장
  • 컴포넌트는 단일 책임 원칙 준수
  • Presentational/Container 컴포넌트 분리
  • 성능 최적화: memo, useCallback, useMemo 적절히 사용
  • 대용량 리스트는 가상화 라이브러리 사용 고려

상태 관리

  • Zustand와 Tanstack Query를 일관되게 사용
  • 상태 구조는 정규화된 형태로 관리
  • 에러 처리: Error Boundary와 try-catch 또는 onError 콜백 활용

3. 스타일링

Styled Components

  • Styled Components 일관되게 사용
  • 스타일드 컴포넌트명은 의미있게 작성
  • 동적 스타일링은 props나 CSS 변수 활용
  • 테마 시스템 활용하여 글로벌 스타일 관리
  • 재사용 가능한 스타일은 mixin이나 확장으로 관리
  • CSS 포맷팅 가독성 유지
  • 사용하지 않는 스타일이나 중복 스타일 제거

4. Vite 및 빌드 최적화

  • 모듈 import 최적화 (tree-shaking 고려)
  • 환경변수는 .env 파일로 관리
  • vite.config.ts에서 빌드 성능 튜닝 (sourcemap 설정, 플러그인 최적화 등)

5. 아키텍처 및 개발 환경

폴더 구조

  • Feature-Sliced Design (FSD) 구조 준수
  • 레이어별 참조 규칙 엄격히 적용

타입스크립트

  • strict 모드 사용
  • 타입 명시적으로 작성 (any 사용 지양)
  • path alias (@/_) 절대 경로 import 사용

Git 훅

  • Husky 설정으로 pre-commit, commit-msg 린팅 확인

6. 기타 가이드라인

  • 충분한 근거와 함께 리뷰 제공
  • 정보 검증 후 답변
  • 간결하고 명확한 응답
  • 필요시 추가 컨텍스트 요청
  • 검증되지 않은 주장 지양
  • 가능한 경우 출처 명시
  • 별도 언급 없으면 JavaScript 기준
  • 한국어로 응답
  • 대부분 브라우저에서 지원하는 ES6+ 기능 활용
  • 코드 리뷰를 통한 유지보수성 향상에 적극 활용

Files:

  • src/entities/playlist/api/playlist.ts
  • src/app/providers/ToastProvider.tsx
  • src/pages/myPage/ui/customize/index.tsx
  • src/widgets/playlist/PlaylistHorizontal.tsx
  • src/features/auth/types/auth.ts
  • src/features/profile/api/profile.ts
  • src/pages/login/index.tsx
  • src/entities/playlist/model/usePlaylists.ts
  • src/features/profile/model/useProfile.ts
  • src/features/auth/store/authStore.ts
  • src/shared/ui/Cd.tsx
  • src/widgets/authGuard/PrivateRoute.tsx
  • src/pages/myPage/ui/components/UserProfile.tsx
  • src/shared/config/routesConfig.ts
  • src/features/profile/types/profile.ts
  • src/pages/myPage/ui/playlist/index.tsx
  • src/shared/api/instance.ts
🧬 Code graph analysis (13)
src/entities/playlist/api/playlist.ts (1)
src/shared/api/httpClient.ts (1)
  • api (5-19)
src/pages/myPage/ui/customize/index.tsx (1)
src/entities/playlist/model/useMyPlaylist.ts (1)
  • useMyPagePlaylist (34-66)
src/widgets/playlist/PlaylistHorizontal.tsx (1)
src/entities/playlist/types/playlist.ts (1)
  • CdCustomData (23-34)
src/features/auth/types/auth.ts (1)
src/features/profile/types/profile.ts (1)
  • ProfileResponse (7-11)
src/features/profile/api/profile.ts (2)
src/features/profile/types/profile.ts (2)
  • ProfilePayload (1-5)
  • ProfileResponse (7-11)
src/shared/api/httpClient.ts (1)
  • api (5-19)
src/pages/login/index.tsx (1)
src/app/providers/ToastProvider.tsx (1)
  • useToast (43-47)
src/entities/playlist/model/usePlaylists.ts (1)
src/features/auth/store/authStore.ts (1)
  • useAuthStore (7-52)
src/features/profile/model/useProfile.ts (1)
src/features/profile/api/profile.ts (1)
  • patchProfile (5-18)
src/features/auth/store/authStore.ts (1)
src/features/profile/types/profile.ts (1)
  • ProfileResponse (7-11)
src/widgets/authGuard/PrivateRoute.tsx (1)
src/features/auth/store/authStore.ts (1)
  • useAuthStore (7-52)
src/pages/myPage/ui/components/UserProfile.tsx (3)
src/features/auth/store/authStore.ts (1)
  • useAuthStore (7-52)
src/features/profile/model/useProfile.ts (1)
  • useProfile (5-10)
src/shared/ui/Profile.tsx (1)
  • ProfileUrl (8-8)
src/pages/myPage/ui/playlist/index.tsx (1)
src/shared/config/musicGenres.ts (1)
  • MUSIC_GENRES (1-12)
src/shared/api/instance.ts (1)
src/features/auth/store/authStore.ts (1)
  • useAuthStore (7-52)
🔇 Additional comments (21)
src/shared/config/routesConfig.ts (1)

4-11: alias/모듈 경로 검증 완료
vite.config.ts·tsconfig.app.json의 @ alias 설정과 src/pages/... 디렉토리 인덱스(index.tsx) 경로 모두 정상 존재 확인됨.

src/entities/playlist/api/playlist.ts (1)

39-39: 경로 상수화 및 반환 타입 검토

  • API가 204(No Content)를 반환한다면 반환 타입을 Promise<void>로 단순화하는 방안을 검토해주세요.
  • 중복되는 경로 문자열 방지를 위해 base path 상수화를 추천합니다.
src/pages/login/index.tsx (2)

40-45: 만료 토스트 표시 로직 LGTM

리다이렉트 후 1회성 토스트 표시 및 플래그 제거 흐름이 명확합니다.


6-6: ToastProvider 래핑 확인 완료
현재 App.tsx에서 <ToastProvider>AppRoutes(및 그 하위의 LoginPage)를 감싸고 있어 useToast 호출 시 예외가 발생하지 않습니다.

src/features/profile/types/profile.ts (1)

7-11: userId 반환 타입 API 명세 확인 필요
프로필 조회 API(/main/mypage/playlists/me)가 userId를 number로 반환하는지 string으로 반환하는지 백엔드 OpenAPI/Swagger 명세나 담당자에게 확인해 주세요. 반환값이 number라면 ProfileResponse.userId를 number로 변경하거나, 프론트에서 string으로 변환하는 로직을 추가해야 합니다.

src/shared/ui/Cd.tsx (1)

75-75: 오버레이 DOM 순서로 레이어링 해결된 점 좋습니다

오버레이를 마지막에 렌더링해 스티커 위에 자연스럽게 올라오도록 한 전략 적절합니다.

src/shared/api/instance.ts (2)

75-76: 에러 로깅은 유지하되, 최종적으로 항상 reject 보장

현재 분기 외 경로에서만 Promise.reject가 보장됩니다. 위 제안 적용 시 모든 분기에서 일관된 reject 흐름이 보장됩니다.


30-30: 문제 없음: 호출부에서 response.status·headers 접근 사례 없음
호출부에서 반환된 값의 statusheaders를 참조하는 코드가 발견되지 않아, response.data만 반환하도록 변경해도 영향이 없습니다.

src/pages/myPage/ui/customize/index.tsx (2)

46-49: 편집 모드에서만 쿼리 활성화하는 조건 적절합니다

무효 ID에 대해 enabled:false로 불필요한 호출을 막는 전략 좋습니다.


58-60: 로딩 상태 분기 적절

비동기 페치 전 화면 깜빡임을 줄이는 로딩 가드가 잘 들어갔습니다.

src/features/auth/types/auth.ts (2)

1-1: ProfileResponse 타입 의존성 추가 적절

교차 모듈 타입 연결이 명확합니다.


24-24: AuthState에 updateUserInfo 추가 적합

스토어 구현과 일치하며 프로필 수정 후 사용자 정보 동기화에 필요합니다.

src/pages/myPage/ui/playlist/index.tsx (2)

108-115: 편집 버튼 라우팅 추가 적절

플리 편집 플로우 진입 경로가 명확합니다.


121-122: 스티커 Prop 전달 연동 OK

UI 합성에 필요한 데이터 연결이 적절합니다.

src/features/profile/api/profile.ts (1)

1-18: payload.profileImage 미사용 및 이미지 삭제 시나리오 확인 필요

ProfilePayload에 있는 profileImage(string|null)가 전송되지 않습니다. 이미지 제거(파일 삭제) 플로우가 필요하다면 백엔드 계약에 맞는 플래그/필드를 추가해야 합니다. 확인 부탁드립니다.

src/features/auth/store/authStore.ts (1)

33-41: LGTM: 프로필 수정 응답으로 userInfo 동기화 적절

닉네임/프로필 이미지 매핑이 명확하고 partialize 정책과도 일치합니다.

src/widgets/playlist/PlaylistHorizontal.tsx (2)

11-15: 스티커 prop 추가 및 시그니처 확장 LGTM

stickers?: CdCustomData[]를 공개 API로 추가하고 컴포넌트 시그니처에 반영한 방향 좋습니다. 타입 alias와 경로도 가이드라인에 부합합니다.


18-18: Cd 컴포넌트 Prop 타입에 stickers?: CdCustomData[] 선언 확인 — undefined 처리 검증 필요

  • src/shared/ui/Cd.tsx의 interface CdPropsstickers?: CdCustomData[]가 선언되어 있습니다.
  • const Cd = ({ stickers }: CdProps)로 디스트럭처링만 되어 있어, stickers가 undefined일 경우 내부에서 .map 등으로 바로 사용 시 런타임 에러가 발생할 수 있습니다.
    → 옵셔널 체이닝(stickers?.map) 또는 디폴트값(stickers = []) 할당 여부를 확인해주세요.
src/pages/myPage/ui/components/UserProfile.tsx (3)

96-101: 파일 선택 후 미리보기/상태 업데이트 로직 LGTM

에러 메시지 초기화, blob URL 생성, 상태 분리(file vs profileImage:null)가 명확합니다.


103-111: blob URL 정리 로직 LGTM

의존성 기반 cleanup으로 직전 blob URL을 적시에 해제하고 있어 누수 우려 낮습니다.


24-32: 타입 및 전송 포맷 일치 확인 완료
updatedProfile의 구조가 ProfilePayload(nickname: string, file: File | null, profileImage: string | null)와 완벽히 일치하며, patchProfile에서 FormData를 사용해 multipart/form-data 헤더로 올바르게 전송되고 있습니다.

Comment on lines +9 to +11
if (payload.file instanceof File) {
formData.append('profileImage', payload.file)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

SSR 안전성/런타임 가드: File 전역 의존 제거

SSR 환경에서 File 전역이 없을 수 있습니다. 런타임 가드를 단순화해 안전하게 처리하세요.

-  if (payload.file instanceof File) {
-    formData.append('profileImage', payload.file)
-  }
+  if (payload.file) {
+    formData.append('profileImage', payload.file)
+  }
📝 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
if (payload.file instanceof File) {
formData.append('profileImage', payload.file)
}
if (payload.file) {
formData.append('profileImage', payload.file)
}
🤖 Prompt for AI Agents
In src/features/profile/api/profile.ts around lines 9 to 11, the current check
uses the global File constructor which can be undefined in SSR; replace the
direct instanceof File check with a runtime-safe guard such as first verifying
typeof File !== 'undefined' before using instanceof, or alternatively allow Blob
checks (e.g. typeof File !== 'undefined' && payload.file instanceof File) or
(payload.file && (payload.file instanceof Blob || (typeof payload.file ===
'object' && payload.file.constructor?.name === 'File'))), then append to
formData only when the guarded check passes.

Comment on lines +13 to +17
return api.patch<ProfileResponse>('/main/mypage/playlists/me', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

FormData 업로드 시 Content-Type 수동 지정 금지

브라우저가 boundary를 포함해 적절한 Content-Type을 설정합니다. 수동 지정 시 boundary 누락으로 업로드 실패 가능성이 있습니다.

-  return api.patch<ProfileResponse>('/main/mypage/playlists/me', formData, {
-    headers: {
-      'Content-Type': 'multipart/form-data',
-    },
-  })
+  return api.patch<ProfileResponse>('/main/mypage/playlists/me', formData)
📝 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
return api.patch<ProfileResponse>('/main/mypage/playlists/me', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return api.patch<ProfileResponse>('/main/mypage/playlists/me', formData)
🤖 Prompt for AI Agents
In src/features/profile/api/profile.ts around lines 13 to 17 the request is
manually setting 'Content-Type': 'multipart/form-data' when uploading FormData;
remove that manual header so the browser/axios can set the Content-Type with the
required boundary. Modify the call to either omit the headers option entirely or
keep other headers but delete the Content-Type property so the request succeeds
with the correct boundary.

Comment on lines +1 to +10
import { useMutation } from '@tanstack/react-query'

import { patchProfile } from '@/features/profile/api/profile'

export const useProfile = () => {
return useMutation({
mutationKey: ['patchProfile'],
mutationFn: patchProfile,
})
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

useMutation 제네릭 명시로 타입 안전성 강화

변수/결과/에러 타입을 명시하면 호출부에서 안전성이 올라가고 자동완성도 개선됩니다.

 import { useMutation } from '@tanstack/react-query'

 import { patchProfile } from '@/features/profile/api/profile'
+import type { ProfilePayload, ProfileResponse } from '@/features/profile/types/profile'

 export const useProfile = () => {
-  return useMutation({
+  return useMutation<ProfileResponse, Error, ProfilePayload>({
     mutationKey: ['patchProfile'],
     mutationFn: patchProfile,
   })
 }
📝 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
import { useMutation } from '@tanstack/react-query'
import { patchProfile } from '@/features/profile/api/profile'
export const useProfile = () => {
return useMutation({
mutationKey: ['patchProfile'],
mutationFn: patchProfile,
})
}
import { useMutation } from '@tanstack/react-query'
import { patchProfile } from '@/features/profile/api/profile'
import type { ProfilePayload, ProfileResponse } from '@/features/profile/types/profile'
export const useProfile = () => {
return useMutation<ProfileResponse, Error, ProfilePayload>({
mutationKey: ['patchProfile'],
mutationFn: patchProfile,
})
}
🤖 Prompt for AI Agents
In src/features/profile/model/useProfile.ts around lines 1 to 10, the
useMutation call is missing generic type parameters which reduces type safety
and autocomplete; update it to include the mutation result, error and variables
types that match patchProfile (e.g. useMutation<ReturnType<typeof patchProfile>,
Error, Parameters<typeof patchProfile>[0]>), or import explicit types from the
profile API (e.g. ProfileResponse, PatchProfileInput) and use
useMutation<ProfileResponse, Error, PatchProfileInput> so callers get proper
typings and autocompletion.

Comment on lines +46 to 75
if (updatedProfile.nickname.length === 0) {
setHasErrorMsg('1자 이상 입력해주세요')
return
}

mutate(updatedProfile, {
onSuccess: (response) => {
updateUserInfo(response)

setIsEditMode(false)
setHasErrorMsg('')
setUpdatedProfile({
nickname: response.nickname,
profileImage: response.profileImageUrl,
file: null,
})
setPreviewImage(response.profileImageUrl)
},
})

// 초기화
setIsEditMode(false)
setIsFileError(false)
setHasErrorMsg('')
setUpdatedProfile({
nickname: userInfo.username,
profileImg: userInfo?.userProfileImageUrl || null,
profileImage: userInfo?.userProfileImageUrl || null,
file: null,
})
setPreviewImage(userInfo?.userProfileImageUrl || null)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

저장 로직에서 조기 초기화로 인한 UX/상태 불일치 문제

mutate 호출 직후(라인 66-75) 편집 상태와 폼을 즉시 초기화하고 있어 실패 시에도 입력이 사라지고, 성공 처리(onSuccess)와 중복으로 상태가 흔들립니다. 검증은 trim 기준으로 하고, 초기화는 onSuccess/onError에서 분기하세요.

다음 diff를 적용해 주세요:

-    if (updatedProfile.nickname.length === 0) {
-      setHasErrorMsg('1자 이상 입력해주세요')
-      return
-    }
+    const nickname = updatedProfile.nickname.trim()
+    if (!nickname) {
+      setHasErrorMsg('1자 이상 입력해주세요')
+      return
+    }

-    mutate(updatedProfile, {
+    mutate({ ...updatedProfile, nickname }, {
       onSuccess: (response) => {
         updateUserInfo(response)

         setIsEditMode(false)
         setHasErrorMsg('')
         setUpdatedProfile({
           nickname: response.nickname,
           profileImage: response.profileImageUrl,
           file: null,
         })
         setPreviewImage(response.profileImageUrl)
       },
+      onError: () => {
+        setHasErrorMsg('프로필 저장에 실패했어요. 잠시 후 다시 시도해주세요.')
+      },
     })

-    // 초기화
-    setIsEditMode(false)
-    setHasErrorMsg('')
-    setUpdatedProfile({
-      nickname: userInfo.username,
-      profileImage: userInfo?.userProfileImageUrl || null,
-      file: null,
-    })
-    setPreviewImage(userInfo?.userProfileImageUrl || null)
📝 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
if (updatedProfile.nickname.length === 0) {
setHasErrorMsg('1자 이상 입력해주세요')
return
}
mutate(updatedProfile, {
onSuccess: (response) => {
updateUserInfo(response)
setIsEditMode(false)
setHasErrorMsg('')
setUpdatedProfile({
nickname: response.nickname,
profileImage: response.profileImageUrl,
file: null,
})
setPreviewImage(response.profileImageUrl)
},
})
// 초기화
setIsEditMode(false)
setIsFileError(false)
setHasErrorMsg('')
setUpdatedProfile({
nickname: userInfo.username,
profileImg: userInfo?.userProfileImageUrl || null,
profileImage: userInfo?.userProfileImageUrl || null,
file: null,
})
setPreviewImage(userInfo?.userProfileImageUrl || null)
}
const nickname = updatedProfile.nickname.trim()
if (!nickname) {
setHasErrorMsg('1자 이상 입력해주세요')
return
}
mutate({ ...updatedProfile, nickname }, {
onSuccess: (response) => {
updateUserInfo(response)
setIsEditMode(false)
setHasErrorMsg('')
setUpdatedProfile({
nickname: response.nickname,
profileImage: response.profileImageUrl,
file: null,
})
setPreviewImage(response.profileImageUrl)
},
onError: () => {
setHasErrorMsg('프로필 저장에 실패했어요. 잠시 후 다시 시도해주세요.')
},
})
🤖 Prompt for AI Agents
In src/pages/myPage/ui/components/UserProfile.tsx around lines 46 to 75, the
save handler currently trims validation incorrectly and immediately resets edit
state and form right after calling mutate which causes input to disappear on
failure and duplicates state changes in onSuccess; change validation to use
updatedProfile.nickname.trim().length === 0 and return early with
setHasErrorMsg, remove the unconditional post-mutate "초기화" block, and instead
perform form/state reset only inside mutate onSuccess (setIsEditMode(false),
clear error, update updatedProfile and previewImage from response) and add an
onError handler to preserve the current form state and surface an error message
so failed saves do not clear the user input.

Comment on lines +29 to +30
const [isEditMode] = useState<boolean>(!!playlistId && Number(playlistId) > 0)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

isEditMode를 state로 보관하면 쿼리 파라미터 변경 시 값이 갱신되지 않습니다

초기값만 반영됩니다. 파라미터 변화에 반응하도록 파생 값으로 두세요.

적용 제안:

-  const [isEditMode] = useState<boolean>(!!playlistId && Number(playlistId) > 0)
+  const isEditMode = !!playlistId && Number(playlistId) > 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
const [isEditMode] = useState<boolean>(!!playlistId && Number(playlistId) > 0)
// 변경 전
- const [isEditMode] = useState<boolean>(!!playlistId && Number(playlistId) > 0)
// 변경 후
const isEditMode = !!playlistId && Number(playlistId) > 0
🤖 Prompt for AI Agents
In src/pages/myPage/ui/customize/index.tsx around lines 29-30, isEditMode is
currently created with useState using the initial value derived from playlistId
which means it won't update when the query parameter changes; remove the state
and make isEditMode a derived value (e.g. const isEditMode = !!playlistId &&
Number(playlistId) > 0 or use useMemo based on playlistId) so it recalculates on
playlistId updates, and ensure playlistId is safely parsed before the check.

Comment on lines +146 to +150
{playlistData?.songs
.sort((a, b) => a.id - b.id)
.map((song) => (
<Link key={song.id} data={song} />
))}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

옵셔널 체이닝 누락 + sort의 원본 배열 변이

playlistData?.songsundefined일 때 .sort 접근에서 런타임 오류가 납니다. 또한 sort는 원본을 변이하므로 slice() 후 정렬하세요.

적용 제안:

-        {playlistData?.songs
-          .sort((a, b) => a.id - b.id)
-          .map((song) => (
-            <Link key={song.id} data={song} />
-          ))}
+        {(playlistData?.songs ?? [])
+          .slice()
+          .sort((a, b) => a.id - b.id)
+          .map((song) => <Link key={song.id} data={song} />)}
📝 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
{playlistData?.songs
.sort((a, b) => a.id - b.id)
.map((song) => (
<Link key={song.id} data={song} />
))}
{(playlistData?.songs ?? [])
.slice()
.sort((a, b) => a.id - b.id)
.map((song) => <Link key={song.id} data={song} />)}
🤖 Prompt for AI Agents
In src/pages/myPage/ui/playlist/index.tsx around lines 146 to 150, the code
calls .sort on playlistData?.songs which can be undefined and also mutates the
original array; change it to operate on a safe array copy by providing a
fallback when songs is undefined (e.g., use an empty array if undefined) and
call .slice() (or spread) before .sort so you sort a copy instead of mutating
the original, then map over that result.

Comment on lines +37 to +47
if (code === 'COMMON-401' || status === 401) {
if (isLogin) {
// 회원 로그인 토큰 만료 만료 → 로그아웃 + 로그인 페이지 이동
if (!['/login', '/login/callback'].includes(currentPath)) {
setLogout()
localStorage.setItem('show_expired_toast', 'true')
window.location.replace('/login')
}
return
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

로그인 사용자 401 처리에서 Promise 미반환 → 체인 끊김

return만 하면 호출부는 성공으로 인식하거나 undefined를 받습니다. 반드시 reject/throw 하세요.

적용 제안:

-        // 회원 로그인 토큰 만료 만료 → 로그아웃 + 로그인 페이지 이동
+        // 회원 로그인 토큰 만료 → 로그아웃 + 로그인 페이지 이동
         if (!['/login', '/login/callback'].includes(currentPath)) {
           setLogout()
           localStorage.setItem('show_expired_toast', 'true')
           window.location.replace('/login')
         }
-        return
+        return Promise.reject(error)
📝 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
if (code === 'COMMON-401' || status === 401) {
if (isLogin) {
// 회원 로그인 토큰 만료 만료 → 로그아웃 + 로그인 페이지 이동
if (!['/login', '/login/callback'].includes(currentPath)) {
setLogout()
localStorage.setItem('show_expired_toast', 'true')
window.location.replace('/login')
}
return
}
if (code === 'COMMON-401' || status === 401) {
if (isLogin) {
// 회원 로그인 토큰 만료 → 로그아웃 + 로그인 페이지 이동
if (!['/login', '/login/callback'].includes(currentPath)) {
setLogout()
localStorage.setItem('show_expired_toast', 'true')
window.location.replace('/login')
}
return Promise.reject(error)
}
🤖 Prompt for AI Agents
In src/shared/api/instance.ts around lines 37 to 47, the 401 branch for
logged-in users currently uses a bare `return`, which resolves to undefined and
breaks the Promise chain; change this to propagate an error by either throwing
an Error or returning a rejected Promise after performing logout/navigation so
the caller receives a rejected promise (include a descriptive message with
status/code/context) and ensure the rejection is actually returned from the
handler.

Comment on lines +49 to +72
if (!['/login', '/login/callback'].includes(currentPath)) {
return (async () => {
try {
const res = await axios.post(
`${import.meta.env.VITE_API_URL}/auth/anonymous`,
{},
{ withCredentials: true }
)

// 서버 응답 형태 확인 (string vs object)
const token = typeof res.data === 'string' ? res.data : res.data.token
localStorage.setItem('anonymous_token', token)

// 원래 요청 재시도
if (error.config) {
error.config.headers.Authorization = `Bearer ${token}`
return axiosInstance.request(error.config)
}
} catch (e) {
console.error('익명 토큰 재발급 실패: ', e)
}
return Promise.reject(error)
})()
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

익명 토큰 재발급 무한 재시도 가능성 및 헤더 설정 방식 개선

  • 재시도 플래그가 없어 401 반복 시 무한 루프 위험이 있습니다.
  • headers.Authorization 직접 대입은 Axios v1 헤더 타입과 충돌할 수 있습니다. 병합 할당으로 안전하게 설정하세요.
  • 토큰 응답 검증 누락 시 undefined 저장 가능성이 있습니다.

적용 제안:

       if (!['/login', '/login/callback'].includes(currentPath)) {
         return (async () => {
           try {
             const res = await axios.post(
               `${import.meta.env.VITE_API_URL}/auth/anonymous`,
               {},
               { withCredentials: true }
             )

-            // 서버 응답 형태 확인 (string vs object)
-            const token = typeof res.data === 'string' ? res.data : res.data.token
+            // 서버 응답 형태 확인 (string vs object)
+            const token = typeof res.data === 'string' ? res.data : (res.data as any)?.token
+            if (!token) {
+              throw new Error('Invalid anonymous token response')
+            }
             localStorage.setItem('anonymous_token', token)

             // 원래 요청 재시도
             if (error.config) {
-              error.config.headers.Authorization = `Bearer ${token}`
-              return axiosInstance.request(error.config)
+              // 무한 루프 방지: 1회만 재시도
+              if ((error.config as any)._retry) {
+                return Promise.reject(error)
+              }
+              ;(error.config as any)._retry = true
+              error.config.headers = {
+                ...(error.config.headers as any),
+                Authorization: `Bearer ${token}`,
+              }
+              return axiosInstance.request(error.config)
             }
           } catch (e) {
             console.error('익명 토큰 재발급 실패: ', e)
           }
           return Promise.reject(error)
         })()
       }
📝 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
if (!['/login', '/login/callback'].includes(currentPath)) {
return (async () => {
try {
const res = await axios.post(
`${import.meta.env.VITE_API_URL}/auth/anonymous`,
{},
{ withCredentials: true }
)
// 서버 응답 형태 확인 (string vs object)
const token = typeof res.data === 'string' ? res.data : res.data.token
localStorage.setItem('anonymous_token', token)
// 원래 요청 재시도
if (error.config) {
error.config.headers.Authorization = `Bearer ${token}`
return axiosInstance.request(error.config)
}
} catch (e) {
console.error('익명 토큰 재발급 실패: ', e)
}
return Promise.reject(error)
})()
}
if (!['/login', '/login/callback'].includes(currentPath)) {
return (async () => {
try {
const res = await axios.post(
`${import.meta.env.VITE_API_URL}/auth/anonymous`,
{},
{ withCredentials: true }
)
// 서버 응답 형태 확인 (string vs object)
const token = typeof res.data === 'string'
? res.data
: (res.data as any)?.token
if (!token) {
throw new Error('Invalid anonymous token response')
}
localStorage.setItem('anonymous_token', token)
// 원래 요청 재시도
if (error.config) {
// 무한 루프 방지: 1회만 재시도
if ((error.config as any)._retry) {
return Promise.reject(error)
}
;(error.config as any)._retry = true
error.config.headers = {
...(error.config.headers as any),
Authorization: `Bearer ${token}`,
}
return axiosInstance.request(error.config)
}
} catch (e) {
console.error('익명 토큰 재발급 실패: ', e)
}
return Promise.reject(error)
})()
}
🤖 Prompt for AI Agents
In src/shared/api/instance.ts around lines 49 to 72, the anonymous-token refresh
flow can cause infinite retries (no retry flag), may store undefined tokens, and
directly assigns headers in a way that can conflict with Axios v1; fix by
validating the token before saving (only call localStorage.setItem if token is a
non-empty string), add a retry guard on the retried request (e.g. set a boolean
flag like config._retry = true and bail out if already set), merge headers
safely instead of direct assignment (set config.headers = { ...config.headers,
Authorization: `Bearer ${token}` }) and ensure error.config exists before
mutating, then call axiosInstance.request(error.config); also surface or rethrow
the inner error after logging so caller sees failure.

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

Labels

HIGH 빠르게 처리해야 하는 높은 우선순위

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 프로필 수정 api 연동

2 participants