Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit 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 failedThe pull request is closed. Walkthrough프로필 PATCH API·타입·훅 추가 및 마이페이지 프로필 UI 연동, 인증 스토어에 사용자 정보 갱신 액션 추가, axios 응답 인터셉터의 401/익명 토큰 재시도 로직 확장, 로그인 페이지 만료 토스트 표시 및 게스트 경로 변경, 플레이리스트 엔드포인트/프롭/정렬 및 CD 오버레이 렌더링 방식 변경, 라우트 import 경로 정리, 일부 쿼리의 enabled 조건 추가. Changes
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
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: 프리뷰/닉네임 갱신, 로딩 해제
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (4)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
There was a problem hiding this comment.
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
-
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. ↩
|
🎵 Storybook Link 🎵 |
There was a problem hiding this comment.
Code Review
프로필 수정 API 연동, 인터셉터를 통한 토큰 만료 처리 개선, CD 오버레이 UI 수정 등 전반적인 기능 구현이 잘 이루어졌습니다. 코드 구조도 좋습니다. 리뷰 과정에서 비동기 처리에서의 상태 관리, 캐시 데이터의 불변성 유지 등 몇 가지 개선점을 발견했습니다. 특히 프로필 수정 핸들러의 로직과 쿼리 캐시에서 가져온 목록을 정렬하는 부분에서 잠재적인 문제가 있어 수정을 제안합니다. 또한, 반응성이 보장되지 않는 상태 접근 방식과 불필요한 console.log에 대한 의견도 남겼습니다.
| // 초기화 | ||
| setIsEditMode(false) | ||
| setIsFileError(false) | ||
| setHasErrorMsg('') | ||
| setUpdatedProfile({ | ||
| nickname: userInfo.username, | ||
| profileImg: userInfo?.userProfileImageUrl || null, | ||
| profileImage: userInfo?.userProfileImageUrl || null, | ||
| file: null, | ||
| }) | ||
| setPreviewImage(userInfo?.userProfileImageUrl || null) |
There was a problem hiding this comment.
mutate 함수는 비동기적으로 동작하는데, 바로 다음에 상태를 초기화하는 로직이 있어 API 요청이 완료되기 전에 UI가 초기화되는 문제가 발생합니다. 이는 사용자 경험을 해치고 의도치 않은 동작을 유발할 수 있습니다. 1 초기화 로직은 mutate 함수의 onSuccess나 onError 콜백 안에서 처리되어야 합니다. 현재 onSuccess 콜백에 이미 성공 시 상태를 업데이트하는 로직이 있으므로, 이 중복되고 잘못된 위치에 있는 초기화 코드는 제거하는 것이 좋습니다.
Style Guide References
Footnotes
-
비동기 작업(API 호출 등)에서 발생하는 에러를 적절히 처리하도록 권장합니다. Tanstack Query 사용 시 onError 콜백 등을 활용합니다. ↩
| {playlistData?.songs | ||
| .sort((a, b) => a.id - b.id) | ||
| .map((song) => ( | ||
| <Link key={song.id} data={song} /> | ||
| ))} |
There was a problem hiding this comment.
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
-
서버 상태 관리에 Tanstack Query를 적극 활용하여 데이터 fetching, 캐싱, 동기화, 에러 처리 등을 효율적으로 관리하도록 권장합니다. ↩
| } | ||
| return undefined | ||
| }, | ||
| enabled: !!useAuthStore.getState().accessToken || !!localStorage.getItem('anonymous_token'), |
There was a problem hiding this comment.
useAuthStore.getState()를 사용하는 것은 Zustand 상태 변경에 반응하지 않습니다. 이로 인해 enabled 옵션은 훅이 처음 호출될 때 한 번만 계산되고, 이후 로그인 상태가 변경되어도 쿼리가 자동으로 활성화/비활성화되지 않을 수 있습니다. 1 상태 변경에 반응하도록 하려면 useAuthStore 훅을 사용하여 상태 값을 구독하는 것이 좋습니다.
Style Guide References
Footnotes
-
Zustand, Tanstack-Query 등 상태 관리 패턴이 일관성 있게 사용되었는지 확인하고, 복잡한 상태 로직은 커스텀 훅으로 분리하도록 제안해주세요. ↩
| isEditMode ? Number(playlistId) : -1 | ||
| ) | ||
|
|
||
| console.log(playlistData) |
maylh
left a comment
There was a problem hiding this comment.
백엔드 토큰이 35분이어서 언제 만료됐는지 파악이 어려웠는데요..
로그인 토큰 만료로 응답 오면 로그아웃해서 데이터 날리고 /login 으로 넘어가도록 interceptor 수정했습니다
요 부분 토큰 재발급이 아니라 아예 날리고 다시 로그인 하게 하는 걸로 정해진건가용 ??
나머지는 확인했습니다 ~~~~~~~~~
수고하셨어요 !!!!!! 🪄
재발급 하는 api 가 없는 것 같기도 하고 원래 일주일로 늘려주신다고 하셔서 일단 그거에 맞춰보려구요! |
There was a problem hiding this comment.
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에 키가 없거나 값이 잘못되면localIndex가NaN이 되어 매칭 실패가 발생합니다. 가드 추가를 권장합니다.적용 제안:
- 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 - offsetsrc/pages/myPage/ui/playlist/index.tsx (1)
98-100: 렌더 중 네비게이션 호출은 경고 유발 가능 → useEffect로 이전
render중navigate호출 대신 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_MESSAGESsrc/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: 서로 배타적인 파일/이미지 입력을 타입으로 명시
file과profileImage가 동시에 존재/부재 가능한 상태를 막기 위해 분기형 타입으로 제약을 걸면 안전합니다.-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.
📒 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.tssrc/app/providers/ToastProvider.tsxsrc/pages/myPage/ui/customize/index.tsxsrc/widgets/playlist/PlaylistHorizontal.tsxsrc/features/auth/types/auth.tssrc/features/profile/api/profile.tssrc/pages/login/index.tsxsrc/entities/playlist/model/usePlaylists.tssrc/features/profile/model/useProfile.tssrc/features/auth/store/authStore.tssrc/shared/ui/Cd.tsxsrc/widgets/authGuard/PrivateRoute.tsxsrc/pages/myPage/ui/components/UserProfile.tsxsrc/shared/config/routesConfig.tssrc/features/profile/types/profile.tssrc/pages/myPage/ui/playlist/index.tsxsrc/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 접근 사례 없음
호출부에서 반환된 값의status나headers를 참조하는 코드가 발견되지 않아,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 전달 연동 OKUI 합성에 필요한 데이터 연결이 적절합니다.
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 CdProps에stickers?: CdCustomData[]가 선언되어 있습니다.const Cd = ({ stickers }: CdProps)로 디스트럭처링만 되어 있어,stickers가 undefined일 경우 내부에서.map등으로 바로 사용 시 런타임 에러가 발생할 수 있습니다.
→ 옵셔널 체이닝(stickers?.map) 또는 디폴트값(stickers = []) 할당 여부를 확인해주세요.src/pages/myPage/ui/components/UserProfile.tsx (3)
96-101: 파일 선택 후 미리보기/상태 업데이트 로직 LGTM에러 메시지 초기화, blob URL 생성, 상태 분리(
filevsprofileImage: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헤더로 올바르게 전송되고 있습니다.
| if (payload.file instanceof File) { | ||
| formData.append('profileImage', payload.file) | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| return api.patch<ProfileResponse>('/main/mypage/playlists/me', formData, { | ||
| headers: { | ||
| 'Content-Type': 'multipart/form-data', | ||
| }, | ||
| }) |
There was a problem hiding this comment.
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.
| 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.
| import { useMutation } from '@tanstack/react-query' | ||
|
|
||
| import { patchProfile } from '@/features/profile/api/profile' | ||
|
|
||
| export const useProfile = () => { | ||
| return useMutation({ | ||
| mutationKey: ['patchProfile'], | ||
| mutationFn: patchProfile, | ||
| }) | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| 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) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
저장 로직에서 조기 초기화로 인한 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.
| 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.
| const [isEditMode] = useState<boolean>(!!playlistId && Number(playlistId) > 0) | ||
|
|
There was a problem hiding this comment.
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.
| 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.
| {playlistData?.songs | ||
| .sort((a, b) => a.id - b.id) | ||
| .map((song) => ( | ||
| <Link key={song.id} data={song} /> | ||
| ))} |
There was a problem hiding this comment.
옵셔널 체이닝 누락 + sort의 원본 배열 변이
playlistData?.songs가 undefined일 때 .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.
| {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.
| 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 | ||
| } | ||
|
|
There was a problem hiding this comment.
로그인 사용자 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.
| 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.
| 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) | ||
| })() | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
익명 토큰 재발급 무한 재시도 가능성 및 헤더 설정 방식 개선
- 재시도 플래그가 없어 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.
| 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.
🛰️ 관련 이슈
✨ 주요 변경 사항
1️⃣ 프로필 api 연동
2️⃣ interceptor에서 토큰 만료 시 /login 랜딩 추가
3️⃣ cd sticker와 함께 보여지도록 overlay 출력 방식 수정
🔍 테스트 방법 / 체크리스트
🗯️ PR 포인트
로그인 토큰 만료로 응답 오면 로그아웃해서 데이터 날리고 /login 으로 넘어가도록 interceptor 수정했습니다
🚀 알게된 점
📖 참고 자료 (선택)
Summary by CodeRabbit
New Features
Bug Fixes