Conversation
- 백엔드 response 형식 변경에 맞춰 key값 수정 - 의미, 형식에 맞게 변수/함수/로직 개편
|
Caution Review failedThe pull request is closed. 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. Walkthrough마이페이지/플레이리스트 중심을 CD 모델로 전환: API·타입 재정의, 훅 마이그레이션, 커스터마이즈(CD) 흐름 추가, 공유 복사 훅 도입, 다수 UI 컴포넌트 생성/제거 및 네임스페이스(myPage → mypage) 정리. 접근성·글로벌 스타일·토스트 항목도 일부 추가/수정됨. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User
participant UI as CustomizeStep1 UI
participant Hook as useCdTempSave
participant API as postCdTempSave
participant Toast
User->>UI: YouTube URL 입력 및 트랙 추가
UI->>UI: 비디오 ID 추출·썸네일 검증
User->>UI: 다음 클릭
UI->>UI: 유효성 검사
alt 실패
UI-->>User: 모달 오류 표시
else 성공
UI->>Hook: temp save 호출 (payload)
Hook->>API: postCdTempSave(payload)
API-->>Hook: 성공 응답
Hook->>Toast: 'LINK' 토스트
Hook-->>UI: 성공 → Step2로 이동
end
sequenceDiagram
autonumber
participant User
participant Tracklist as MypageTracklist
participant Hook as useMyCdActions
participant API as getTracklist/deleteMyCd/patchMyCdPublic
participant Toast
User->>Tracklist: 페이지 진입
Tracklist->>Hook: getTracklist(cdId)
Hook->>API: getTracklist
API-->>Hook: PlaylistDetailResponse
Hook-->>Tracklist: 트랙리스트 반환
User->>Tracklist: 삭제 요청
Tracklist->>Hook: deleteMutation(cdId)
Hook->>API: deleteMyCd
API-->>Hook: 성공
Hook->>Toast: 'CD_DELETE'
Hook-->>Tracklist: 성공 -> /mypage로 이동
User->>Tracklist: 공개 토글
Tracklist->>Hook: togglePublicMutation
Hook->>API: patchMyCdPublic
API-->>Hook: 성공 -> 트랙리스트 쿼리 무효화
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes 근거: API/타입 재정의, 훅·컴포넌트 대규모 마이그레이션, UI 흐름 재설계 및 네임스페이스 변경 등 이질적 변경이 광범위하게 포함됨. Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (21)
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. Comment |
Summary of ChangesHello @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은 마이페이지의 사용자 경험을 향상시키기 위해 UI와 API를 대대적으로 업데이트합니다. '플레이리스트' 개념을 'CD'와 '트랙리스트'로 명확히 하고, CD 생성 및 수정 과정을 더욱 직관적이고 기능적으로 개선하는 데 중점을 두었습니다. 또한, '나의 팔로잉' 기능을 '나의 좋아요'로 변경하여 사용자에게 더 관련성 높은 정보를 제공하며, 알림 기능은 향후 스프린트에서 구현될 예정입니다. Highlights
Using Gemini Code AssistThe 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 by creating a comment using either
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 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
|
There was a problem hiding this comment.
Code Review
마이페이지 관련 UI 및 API 변경사항을 반영하는 대규모 리팩토링 PR이네요. 전반적으로 파일 구조 변경, API 연동 수정, 컴포넌트 및 훅 분리 등 코드의 구조와 재사용성을 크게 개선한 점이 인상적입니다. 특히, 다음과 같은 점들이 좋았습니다:
myPage를mypage로 변경하고 관련 컴포넌트들을 재구성하여 FSD 아키텍처에 더 부합하도록 구조를 개선했습니다.useCopyCdShareUrl훅을 새로 만들어 클립보드 복사 로직을 분리하여 재사용성을 높였습니다.- CD 생성/수정 플로우(step 1~3)를 리팩토링하면서 유효성 검사와 에러 핸들링을 강화하여 안정성을 높였습니다.
Cd컴포넌트에responsivevariant를 추가하고ResizeObserver를 활용하여 동적으로 크기를 조절하도록 개선한 점도 훌륭합니다.
몇 가지 추가 개선을 위해 아래에 리뷰 코멘트를 남겼습니다. 주로 네이밍 컨벤션과 타입 정의 일관성에 대한 내용으로, 코드의 명확성과 유지보수성을 더욱 향상시키는 데 도움이 될 것입니다. 전체적으로 훌륭한 작업이며, 꼼꼼하게 변경사항을 반영해주셔서 감사합니다.
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/features/customize/api/customize.ts (1)
49-53: multipart/form-data 헤더 수동 지정 제거FormData 전송 시 Content-Type을 직접 지정하면 boundary가 누락되어 서버에서 파싱 실패할 수 있습니다. 브라우저/axios가 자동으로 설정하게 두는 게 안전합니다.
- return api.post<UserEachStickerResponse>('/main/prop/upload', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }) + return api.post<UserEachStickerResponse>('/main/prop/upload', formData)src/pages/mypage/ui/customize/step2/index.tsx (2)
963-969: React key로 uuidv4() 사용 지양 — 매 렌더마다 재마운트됨고정 가능한 식별자(id)로 key를 지정하세요.
- {stickerList.map((id) => ( - <li key={uuidv4()}> + {stickerList.map((id) => ( + <li key={id}> <StickerButton type="button" onClick={() => onStickersAddClick(id)}> <img src={stickerUrls[id - 1]} alt={`${currentThemeId} 스티커`} /> </StickerButton> </li> ))}
389-399: z 계층(s.z) 값이 재정렬/삭제 시 갱신되지 않음 — 저장 시 레이어 불일치 위험클릭으로 최상단 이동하거나 삭제 후 stickers 배열 순서는 바뀌지만 s.z는 그대로라 서버 저장(zCoordinate)이 실제 레이어와 달라질 수 있습니다. 재정렬/삭제 시 z를 재계산하세요.
- const newStickers = [...stickers] + const newStickers = [...stickers] const clickedSticker = newStickers.splice(i, 1)[0] - newStickers.push(clickedSticker) - setStickers(newStickers) + newStickers.push(clickedSticker) + setStickers(newStickers.map((s, idx) => ({ ...s, z: idx + 1 }))) setSelectedSticker(newStickers.length - 1)- const deleteSelectedSticker = () => { + const deleteSelectedSticker = () => { if (selectedSticker !== null) { - const updatedStickers = stickers.filter((_, index) => index !== selectedSticker) - setStickers(updatedStickers) + const updatedStickers = stickers + .filter((_, index) => index !== selectedSticker) + .map((s, idx) => ({ ...s, z: idx + 1 })) + setStickers(updatedStickers) setSelectedSticker(null) } }Also applies to: 300-305
🧹 Nitpick comments (25)
src/pages/home/index.tsx (1)
24-25: TODO 주석 포맷을 코딩 컨벤션에 맞게 수정하세요.팀 코딩 가이드라인에 따르면 TODO 주석 형식은
// TODO: 설명 - 작성자이어야 합니다. 현재 작성자 정보가 누락되어 있습니다.다음과 같이 수정하는 것을 권장합니다:
- // TODO: 알림 기능 2차 스프린트 시 작업 예정 + // TODO: 알림 기능 2차 스프린트 시 작업 예정 - hansololiviakim // const handleNotiClick = () => navigate('/mypage/notification')- {/* TODO: 알림 기능 2차 스프린트 시 작업 예정 */} + {/* TODO: 알림 기능 2차 스프린트 시 작업 예정 - hansololiviakim */} {/* <SvgButton icon={Notification} onClick={handleNotiClick} /> */}As per coding guidelines
Also applies to: 50-51
src/pages/mypage/types/mypage.ts (1)
1-3: 타입 네이밍 컨벤션을 PascalCase로 변경하는 것을 권장합니다.TypeScript 컨벤션에 따르면 타입명은 PascalCase를 사용하는 것이 표준입니다. SCREAMING_SNAKE_CASE는 일반적으로 상수 값에 사용됩니다.
다음과 같이 수정하는 것을 권장합니다:
-export type CUSTOMIZE_STEP = 1 | 2 | 3 +export type CustomizeStep = 1 | 2 | 3 -export type MYPAGE_TAB_TYPE = 'cd' | 'like' +export type MypageTabType = 'cd' | 'like'코딩 가이드라인에 명시된 네이밍 컨벤션(PascalCase for types)을 준수합니다.
src/pages/mypage/ui/main/index.tsx (1)
19-22: TAB_LIST를 컴포넌트 외부로 이동하는 것을 권장합니다.현재 TAB_LIST가 컴포넌트 내부에 정의되어 있어 매 렌더링마다 새로 생성됩니다.
다음과 같이 컴포넌트 외부로 이동하면 불필요한 재생성을 방지할 수 있습니다:
+const TAB_LIST: { label: string; value: MYPAGE_TAB_TYPE }[] = [ + { label: '나의 CD', value: 'cd' }, + { label: '나의 좋아요', value: 'like' }, +] + const Mypage = () => { const navigate = useNavigate() const { selected: currentTab, onSelect: setCurrentTab } = useSingleSelect<MYPAGE_TAB_TYPE>('cd') - const TAB_LIST: { label: string; value: MYPAGE_TAB_TYPE }[] = [ - { label: '나의 CD', value: 'cd' }, - { label: '나의 좋아요', value: 'like' }, - ]src/shared/lib/useCopyCdShareUrl.ts (1)
23-27: 에러 처리 개선을 권장합니다.현재
console.error만 사용하고 있는데, 사용자에게 복사 실패를 알리는 것이 좋을 수 있습니다.다음과 같이 에러 토스트를 추가하는 것을 고려해보세요:
try { document.execCommand('copy') } catch (e) { console.error(e) + toast('ERROR') // 또는 적절한 에러 메시지 + return }src/pages/mypage/ui/main/components/MyCdList.tsx (1)
61-76: MyLikedCdList와 스타일 코드가 중복됩니다.
CD_LIST_GAP,CdListWrap,CdButton이 MyLikedCdList.tsx와 거의 동일하게 중복되어 있습니다. 공통 스타일을 별도 파일로 추출하거나 믹스인으로 관리하는 것을 권장합니다.예를 들어:
src/pages/mypage/ui/main/components/styles.ts생성- 공통 상수와 스타일 컴포넌트를 export하여 재사용
Also applies to: 96-101
src/pages/mypage/ui/tracklist/index.tsx (1)
162-166: 매 렌더링마다 songs를 정렬하고 있습니다.현재 구조에서는 컴포넌트가 리렌더링될 때마다
sort()가 실행됩니다. 정렬된 목록을useMemo로 메모이제이션하면 불필요한 계산을 방지할 수 있습니다.다음과 같이 개선할 수 있습니다:
+const sortedSongs = useMemo( + () => cdMetadata?.songs?.slice().sort((a, b) => a.orderIndex - b.orderIndex) ?? [], + [cdMetadata?.songs] +) <TracklistContainer> - {cdMetadata?.songs - .sort((a, b) => a.orderIndex - b.orderIndex) - .map((track) => ( + {sortedSongs.map((track) => ( <Link key={track.id} data={track} /> ))} </TracklistContainer>참고:
slice()를 사용하여 원본 배열을 변경하지 않도록 합니다.src/entities/playlist/types/playlist.ts (1)
28-28: CdMetaResponse 타입 정의를 확인하세요.
CdMetaResponse가 배열 타입((CdBasicInfo & OnlyCdResponse)[])으로 정의되어 있습니다. 이는 API 응답이 항상 배열임을 의미하는데, 타입 이름에 복수형이나 List/Array 같은 접미사가 없어 단일 객체로 오해할 수 있습니다.명확성을 위해
CdMetaListResponse또는CdMetaResponse[]형태의 사용을 권장합니다.src/pages/mypage/ui/customize/step1/components/TrackItem.tsx (3)
78-87: CSS :has 사용은 파이어폭스 등 일부 브라우저에서 미지원 — 폴백 추가 권장선택자 :has(input)은 지원이 불완전합니다. @supports로 가드하고 폴백을 제공해 주세요.
다음과 같이 적용을 권장합니다.
- & > div > div:has(input) { - border: 1px solid ${({ theme }) => theme.COLOR['gray-700']}; - background-color: ${({ theme }) => theme.COLOR['gray-800']}; - padding-right: 40px; - } + @supports selector(:has(*)) { + & > div > div:has(input) { + border: 1px solid ${({ theme }) => theme.COLOR['gray-700']}; + background-color: ${({ theme }) => theme.COLOR['gray-800']}; + padding-right: 40px; + } + } + @supports not selector(:has(*)) { + & input { + border: 1px solid ${({ theme }) => theme.COLOR['gray-700']}; + background-color: ${({ theme }) => theme.COLOR['gray-800']}; + padding-right: 40px; + } + }
41-44: alt 인덱스 off-by-one 가능성orderIndex가 0부터라면 접근성 문구가 0번째로 표시될 수 있습니다. +1 보정 권장.
- alt={`${track.orderIndex}번째 트랙 썸네일`} + alt={`${track.orderIndex + 1}번째 트랙 썸네일`}
31-31: 읽기 전용 입력 UXURL 복사를 고려하면 disabled 대신 readOnly가 낫습니다(포커싱/복사 가능). 드래그 충돌 우려 시 tabIndex={-1} 병행 가능.
- <Input type="url" value={track.link} disabled /> + <Input type="url" value={track.link} readOnly tabIndex={-1} />src/entities/playlist/model/useMyCd.ts (2)
44-47: CD 삭제 후 관련 쿼리 무효화 누락삭제 성공 시 내 목록/좋아요 목록/해당 트랙리스트 캐시를 무효화해야 UI가 즉시 반영됩니다.
const deleteMutation = useMutation({ mutationKey: ['deleteMyCd', cdId], mutationFn: () => deleteMyCd(cdId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['myCdList'] }) + queryClient.invalidateQueries({ queryKey: ['myLikeList'] }) + queryClient.invalidateQueries({ queryKey: ['getTracklist', cdId] }) + }, })
50-56: 공개 토글 후 목록 반영공개여부는 카드 메타에도 반영되므로 목록 쿼리도 무효화하세요.
const togglePublicMutation = useMutation({ mutationKey: ['patchMyCdPublic', cdId], mutationFn: () => patchMyCdPublic(cdId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['getTracklist', cdId] }) + queryClient.invalidateQueries({ queryKey: ['myCdList'] }) }, })src/pages/mypage/ui/customize/step2/index.tsx (2)
784-786: 터치 디폴트 방지 이벤트 리스너 add 누락cleanup만 있고 add가 없습니다. 실제 등록하거나, 불필요하면 cleanup을 제거하세요. 등록하는 쪽을 권장합니다(passive: false).
// 모바일 Safari 등에서 터치 스크롤 간섭 방지 const stopDefault = (ev: TouchEvent) => ev.preventDefault() + canvas.addEventListener('touchstart', stopDefault, { passive: false }) + canvas.addEventListener('touchmove', stopDefault, { passive: false }) + canvas.addEventListener('touchend', stopDefault, { passive: false }) return () => { canvas.removeEventListener('touchstart', stopDefault) canvas.removeEventListener('touchmove', stopDefault) canvas.removeEventListener('touchend', stopDefault) }Also applies to: 855-860
231-233: 중복 캐시 무효화 제거 제안useUserSticker의 onSuccess에서 이미 ['getUserStickers']를 invalidate하고 있어 여기서 한 번 더 호출할 필요가 없습니다.
- // 유저 스티커 리스트 재조회 - queryClient.invalidateQueries({ queryKey: ['getUserStickers'] })src/features/customize/types/customize.ts (1)
5-11: 타입 일관성 점검(선택)YoutubeVideoInfo.duration은 string, 응답 songs.youtubeLength는 number로 정의되어 있습니다. 양방향 변환 지점에서 혼동이 있을 수 있으니, 저장/표시 레이어에서 명확히 변환/주석을 남겨 두는 것을 권장합니다.
Also applies to: 26-37
src/features/customize/model/useCustomize.ts (1)
38-53: 유튜브 링크 검증 로직 강화 권장API가 valid 필드를 제공한다면, 유효하지 않은 링크가 포함될 때 즉시 에러로 처리하세요. 또한 링크 문자열 비교 대신 순서(index) 기반 매칭 폴백을 두면 안정성이 올라갑니다.
- const newVideoInfoList = tempSaveMap.youTubeVideoInfo.map((e) => { - const resMap = videoResArr.find((res) => e.link === res.link) + const invalid = videoResArr.filter((v) => v.valid === false) + if (invalid.length) throw new Error('유효하지 않은 유튜브 링크가 포함되어 있습니다.') + const newVideoInfoList = tempSaveMap.youTubeVideoInfo.map((e, i) => { + const resMap = videoResArr.find((res) => e.link === res.link) ?? videoResArr[i] if (!resMap) throw new Error('일치하는 유튜브 영상 정보를 찾지 못했습니다.') const { duration, thumbnailUrl, title } = resMap return { ...e, duration, thumbnailUrl, title } })API 문서 기준으로 valid 필드 의미/스키마가 확정인지 확인 부탁드립니다.
src/entities/playlist/api/playlist.ts (2)
11-18: query 파라미터는 config.params로 전달해 인코딩/타입 안정성을 확보해주세요문자열 보간은 공백/특수문자에서 취약합니다. axios의 params를 사용하면 자동 인코딩됩니다.
아래처럼 수정 제안:
-export const getMyCdList = (sort: string) => { - return api.get<CdMetaResponse>(`/main/playlist/mypage/me?sort=${sort}`) -} +export const getMyCdList = (sort: string) => { + return api.get<CdMetaResponse>('/main/playlist/mypage/me', { params: { sort } }) +} -export const getLikedCdList = (sort: string) => { - return api.get<CdMetaResponse>(`/main/playlist/mypage/me/likes?sort=${sort}`) -} +export const getLikedCdList = (sort: string) => { + return api.get<CdMetaResponse>('/main/playlist/mypage/me/likes', { params: { sort } }) +}또한 sort를 리터럴 유니온 타입(예: 'RECENT' | 'POPULAR')으로 좁히면 오타 방지에 도움이 됩니다. As per coding guidelines.
41-43: 네이밍 일관성: playlist ↔ tracklist 혼재 정리 제안주석은 “트랙리스트 상세 조회”인데 함수명은
getPlaylistDetail, 파라미터는cdId입니다. 추후 혼동을 줄이려면getTracklistDetail또는getCdDetail등으로 통일을 검토해주세요.변경 영향 범위가 넓을 수 있어 리네임 시 호출부 일괄 변경 여부 확인 부탁드립니다.
src/pages/mypage/ui/customize/step1/index.tsx (4)
31-35: setter 네이밍 일관성(setBasicInfo)으로 가독성 개선React 관례상 state setter는 PascalCase를 따릅니다.
setbasicInfo→setBasicInfo로 교체를 권장합니다.적용 예:
-const [basicInfoMap, setbasicInfo] = useState({ +const [basicInfoMap, setBasicInfo] = useState({ ... - setbasicInfo((prev) => ({ ...prev, genre })) + setBasicInfo((prev) => ({ ...prev, genre })) ... - onChange={(e) => setbasicInfo((prev) => ({ ...prev, name: e.target.value }))} + onChange={(e) => setBasicInfo((prev) => ({ ...prev, name: e.target.value }))} ... - setIsOn={(isOn) => setbasicInfo((prev) => ({ ...prev, isPublic: isOn }))} + setIsOn={(isOn) => setBasicInfo((prev) => ({ ...prev, isPublic: isOn }))}As per coding guidelines.
Also applies to: 96-98, 238-239, 248-250
180-187: 재정렬 시 안전 가드 추가
prev.find(...)!로 단언 중이라 예외 입력이 오면 런타임 오류 위험이 있습니다. 안전 가드를 추가해 주세요.예:
- const onReSort = (newOrderIds: string[]) => { - setTracklist((prev) => - newOrderIds.map((id, idx) => { - const track = prev.find((t) => t.id === id)! - return { ...track, orderIndex: idx + 1 } - }) - ) - } + const onReSort = (newOrderIds: string[]) => { + setTracklist((prev) => + newOrderIds + .map((id, idx) => { + const track = prev.find((t) => t.id === id) + return track ? { ...track, orderIndex: idx + 1 } : null + }) + .filter((v): v is NonNullable<typeof v> => !!v) + ) + }
43-49: 상수는 컴포넌트 외부로 이동하여 재생성 방지
MAX_LINK_COUNT,VALID_YOUTUBE_URLS는 불변이므로 컴포넌트 바깥으로 올리면 렌더마다 재생성되지 않습니다.
484-493::has()의존 CSS는 브라우저 호환성 점검 필요
:has()는 최신 브라우저에서만 안정적입니다. Input 컴포넌트가 변경되면 구조 의존성도 깨질 수 있습니다. 가능하면 wrapper에 명시적 클래스/prop을 부여해 스타일링하거나, 폴리필/대체 셀렉터 전략을 준비해주세요.호환성 타깃 브라우저에서 정상 동작 확인 부탁드립니다.
src/shared/ui/Cd.tsx (2)
29-35: responsive 초기 렌더에서 ratio=0로 스티커가 잠깐 사라질 수 있음
dynamicBase초기값 0 →ratio=0이라 스티커 위치/사이즈가 0으로 계산됩니다. 첫 페인트 깜빡임을 줄이려면 초기 fallback을 두세요.예:
-const [dynamicBase, setDynamicBase] = useState(0) -const baseSize = variant === 'responsive' ? dynamicBase : sizeMap[variant].base -const ratio = baseSize / 275 +const [dynamicBase, setDynamicBase] = useState<number | null>(null) +const baseSize = variant === 'responsive' ? (dynamicBase ?? sizeMap.md.base) : sizeMap[variant].base +const ratio = baseSize / 275또는
variant==='responsive' && dynamicBase===null인 동안 스티커 렌더를 잠시 스킵하는 방법도 있습니다.Also applies to: 68-79, 158-165
175-186: 비공개 오버레이는 absolute로 깔아 완전 덮도록현재
PrivateCover가 relative라 stacking 문맥에 따라 겹침이 보장되지 않을 수 있습니다. absolute + inset으로 확실히 덮어주세요.예:
-const PrivateCover = styled.div` - position: relative; +const PrivateCover = styled.div` + position: absolute; + inset: 0; ${flexColCenter} gap: 3px; width: 100%; height: 100%; background-color: rgba(42, 47, 57, 0.7);src/pages/mypage/ui/customize/index.tsx (1)
28-31: 파생 값은 state 대신 변수로 계산하여 단순화
isEditMode는cdId에서 파생되므로 state로 가질 필요가 없습니다. 또한useMyCdActions호출부에서 그대로 활용하면 의도가 명확해집니다.예:
-const [currentStep, setCurrentStep] = useState<CUSTOMIZE_STEP>(1) -const [currentCdId, setCurrentCdId] = useState<number | null>(null) -const [isEditMode] = useState<boolean>(!!cdId && Number(cdId) > 0) +const [currentStep, setCurrentStep] = useState<CUSTOMIZE_STEP>(1) +const [currentCdId, setCurrentCdId] = useState<number | null>(null) +const isEditMode = typeof cdId === 'number' && cdId > 0 -} = useMyCdActions(isEditMode ? Number(cdId) : -1) +} = useMyCdActions(isEditMode ? Number(cdId) : -1)렌더/재계산 시 불필요한 state 업데이트를 줄입니다. As per coding guidelines.
Also applies to: 48-53, 64-85
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (11)
src/assets/icons/icn_cancel_circle.svgis excluded by!**/*.svgsrc/assets/icons/icn_drag.svgis excluded by!**/*.svgsrc/assets/icons/icn_eye.svgis excluded by!**/*.svgsrc/assets/icons/icn_eye_slash.svgis excluded by!**/*.svgsrc/assets/icons/icn_eye_slash_white.svgis excluded by!**/*.svgsrc/assets/icons/icn_link_primary.svgis excluded by!**/*.svgsrc/assets/icons/icn_no_like.svgis excluded by!**/*.svgsrc/assets/icons/icn_pin.svgis excluded by!**/*.svgsrc/assets/icons/icn_pin_primary.svgis excluded by!**/*.svgsrc/assets/icons/icn_plus_black.svgis excluded by!**/*.svgsrc/assets/icons/icn_start_black.svgis excluded by!**/*.svg
📒 Files selected for processing (54)
src/App.tsx(2 hunks)src/app/providers/ToastProvider.tsx(1 hunks)src/assets/icons/index.ts(2 hunks)src/entities/playlist/api/playlist.ts(2 hunks)src/entities/playlist/model/useMyCd.ts(1 hunks)src/entities/playlist/model/useMyPlaylist.ts(0 hunks)src/entities/playlist/model/usePlaylists.ts(1 hunks)src/entities/playlist/types/playlist.ts(2 hunks)src/features/customize/api/customize.ts(1 hunks)src/features/customize/model/useCustomize.ts(1 hunks)src/features/customize/types/customize.ts(1 hunks)src/features/share/api/share.ts(0 hunks)src/features/share/model/useShare.ts(0 hunks)src/features/share/ui/ShareButton.tsx(3 hunks)src/pages/home/index.tsx(3 hunks)src/pages/myPage/ui/components/CdGrid.tsx(0 hunks)src/pages/myPage/ui/components/index.ts(0 hunks)src/pages/myPage/ui/customize/step1/components/InputLinkItem.tsx(0 hunks)src/pages/myPage/ui/customize/step1/components/index.ts(0 hunks)src/pages/myPage/ui/customize/step1/index.tsx(0 hunks)src/pages/myPage/ui/index.tsx(0 hunks)src/pages/myPage/ui/playlist/index.tsx(0 hunks)src/pages/myPage/ui/setting/components/index.tsx(0 hunks)src/pages/mycd/index.tsx(1 hunks)src/pages/mypage/lib/customizeTheme.ts(1 hunks)src/pages/mypage/types/mypage.ts(1 hunks)src/pages/mypage/ui/MypageLayout.tsx(1 hunks)src/pages/mypage/ui/components/StepHeader.tsx(3 hunks)src/pages/mypage/ui/components/ToggleSwitch.tsx(2 hunks)src/pages/mypage/ui/components/index.ts(1 hunks)src/pages/mypage/ui/customize/index.tsx(5 hunks)src/pages/mypage/ui/customize/step1/components/TrackItem.tsx(1 hunks)src/pages/mypage/ui/customize/step1/components/index.ts(1 hunks)src/pages/mypage/ui/customize/step1/index.tsx(1 hunks)src/pages/mypage/ui/customize/step2/index.tsx(11 hunks)src/pages/mypage/ui/customize/step3/index.tsx(2 hunks)src/pages/mypage/ui/main/components/CdNameInfo.tsx(1 hunks)src/pages/mypage/ui/main/components/MyCdList.tsx(1 hunks)src/pages/mypage/ui/main/components/MyLikedCdList.tsx(1 hunks)src/pages/mypage/ui/main/components/index.ts(1 hunks)src/pages/mypage/ui/main/index.tsx(1 hunks)src/pages/mypage/ui/notification/index.tsx(1 hunks)src/pages/mypage/ui/privacy/index.tsx(1 hunks)src/pages/mypage/ui/setting/index.tsx(3 hunks)src/pages/mypage/ui/terms/index.tsx(1 hunks)src/pages/mypage/ui/tracklist/index.tsx(1 hunks)src/pages/mypage/ui/unregister/index.tsx(1 hunks)src/shared/config/routesConfig.ts(2 hunks)src/shared/lib/useCopyCdShareUrl.ts(1 hunks)src/shared/styles/globalStyle.ts(2 hunks)src/shared/ui/Cd.tsx(7 hunks)src/shared/ui/Input.tsx(6 hunks)src/shared/ui/Modal.tsx(4 hunks)src/stories/ToggleSwitch.stories.tsx(1 hunks)
💤 Files with no reviewable changes (11)
- src/features/share/model/useShare.ts
- src/pages/myPage/ui/components/index.ts
- src/pages/myPage/ui/playlist/index.tsx
- src/pages/myPage/ui/setting/components/index.tsx
- src/pages/myPage/ui/index.tsx
- src/features/share/api/share.ts
- src/pages/myPage/ui/customize/step1/index.tsx
- src/pages/myPage/ui/components/CdGrid.tsx
- src/pages/myPage/ui/customize/step1/components/InputLinkItem.tsx
- src/pages/myPage/ui/customize/step1/components/index.ts
- src/entities/playlist/model/useMyPlaylist.ts
🧰 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/pages/mypage/ui/main/components/index.tssrc/pages/mypage/ui/customize/step1/components/index.tssrc/pages/mypage/ui/main/index.tsxsrc/app/providers/ToastProvider.tsxsrc/pages/mypage/types/mypage.tssrc/pages/mypage/ui/setting/index.tsxsrc/pages/mypage/ui/privacy/index.tsxsrc/entities/playlist/model/usePlaylists.tssrc/pages/mycd/index.tsxsrc/pages/mypage/ui/customize/step3/index.tsxsrc/pages/mypage/ui/notification/index.tsxsrc/assets/icons/index.tssrc/pages/mypage/ui/unregister/index.tsxsrc/pages/mypage/ui/main/components/MyLikedCdList.tsxsrc/pages/home/index.tsxsrc/stories/ToggleSwitch.stories.tsxsrc/shared/ui/Modal.tsxsrc/pages/mypage/ui/main/components/MyCdList.tsxsrc/pages/mypage/lib/customizeTheme.tssrc/entities/playlist/model/useMyCd.tssrc/pages/mypage/ui/customize/step1/index.tsxsrc/pages/mypage/ui/customize/step1/components/TrackItem.tsxsrc/pages/mypage/ui/components/ToggleSwitch.tsxsrc/pages/mypage/ui/tracklist/index.tsxsrc/shared/config/routesConfig.tssrc/pages/mypage/ui/terms/index.tsxsrc/pages/mypage/ui/components/StepHeader.tsxsrc/pages/mypage/ui/MypageLayout.tsxsrc/entities/playlist/types/playlist.tssrc/pages/mypage/ui/main/components/CdNameInfo.tsxsrc/shared/ui/Input.tsxsrc/shared/ui/Cd.tsxsrc/shared/styles/globalStyle.tssrc/App.tsxsrc/features/share/ui/ShareButton.tsxsrc/pages/mypage/ui/customize/index.tsxsrc/shared/lib/useCopyCdShareUrl.tssrc/features/customize/model/useCustomize.tssrc/pages/mypage/ui/customize/step2/index.tsxsrc/features/customize/api/customize.tssrc/features/customize/types/customize.tssrc/entities/playlist/api/playlist.tssrc/pages/mypage/ui/components/index.ts
🧠 Learnings (1)
📚 Learning: 2025-08-15T06:26:48.282Z
Learnt from: hansololiviakim
PR: dnd-side-project/dnd-13th-8-frontend#27
File: src/shared/styles/globalStyle.ts:34-50
Timestamp: 2025-08-15T06:26:48.282Z
Learning: In the dnd-13th-8-frontend project, the team uses a centralized approach with the custom Input component (src/shared/ui/Input.tsx) for all input elements, implementing custom focus styling with :focus-within instead of relying on native browser focus styles. This architectural decision allows them to safely use global input outline resets in globalStyle.ts.
Applied to files:
src/shared/ui/Input.tsx
🧬 Code graph analysis (22)
src/pages/mypage/ui/main/index.tsx (4)
src/shared/lib/useSingleSelect.ts (1)
useSingleSelect(3-11)src/pages/mypage/types/mypage.ts (1)
MYPAGE_TAB_TYPE(3-3)src/shared/styles/theme.ts (1)
theme(3-116)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)
src/pages/mypage/ui/setting/index.tsx (2)
src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)src/shared/styles/theme.ts (1)
theme(3-116)
src/entities/playlist/model/usePlaylists.ts (1)
src/entities/playlist/api/playlist.ts (1)
getPlaylistDetail(41-43)
src/pages/mypage/ui/customize/step3/index.tsx (1)
src/features/customize/model/useCustomize.ts (1)
useFinalCdCustom(19-26)
src/pages/mypage/ui/main/components/MyLikedCdList.tsx (5)
src/shared/lib/useSingleSelect.ts (1)
useSingleSelect(3-11)src/shared/ui/ContentHeader.tsx (1)
SortType(8-8)src/entities/playlist/model/useMyCd.ts (1)
useMyLikedCdList(20-26)src/shared/styles/mixins.ts (1)
flexColCenter(10-15)src/shared/styles/theme.ts (1)
theme(3-116)
src/shared/ui/Modal.tsx (2)
src/shared/styles/theme.ts (1)
theme(3-116)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)
src/pages/mypage/ui/main/components/MyCdList.tsx (5)
src/features/auth/store/authStore.ts (1)
useAuthStore(7-52)src/shared/lib/useSingleSelect.ts (1)
useSingleSelect(3-11)src/shared/ui/ContentHeader.tsx (1)
SortType(8-8)src/entities/playlist/model/useMyCd.ts (1)
useMyCdList(12-18)src/shared/styles/theme.ts (1)
theme(3-116)
src/entities/playlist/model/useMyCd.ts (1)
src/entities/playlist/api/playlist.ts (6)
getMyCdList(11-13)getLikedCdList(16-18)getTracklist(21-23)deleteMyCd(31-33)patchMyCdPublic(26-28)getMyRepresentativePlaylist(61-63)
src/pages/mypage/ui/customize/step1/index.tsx (6)
src/pages/mypage/ui/customize/index.tsx (1)
CustomizeStepProps(13-21)src/features/customize/model/useCustomize.ts (1)
useCdTempSave(28-62)src/shared/config/musicGenres.ts (2)
MUSIC_GENRES(1-12)MusicGenre(17-17)src/features/customize/types/customize.ts (1)
YoutubeVideoInfo(5-11)src/shared/ui/Modal.tsx (1)
ModalProps(9-19)src/shared/styles/mixins.ts (2)
flexColCenter(10-15)flexRowCenter(3-8)
src/pages/mypage/ui/customize/step1/components/TrackItem.tsx (3)
src/features/customize/types/customize.ts (1)
YoutubeVideoInfo(5-11)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)src/shared/styles/theme.ts (1)
theme(3-116)
src/pages/mypage/ui/tracklist/index.tsx (8)
src/shared/ui/Modal.tsx (1)
ModalProps(9-19)src/entities/playlist/model/usePlaylists.ts (1)
usePlaylistDetail(45-51)src/entities/playlist/model/useMyCd.ts (1)
useMyCdActions(28-65)src/shared/lib/useCopyCdShareUrl.ts (1)
useCopyCdShareUrl(5-37)src/features/auth/store/authStore.ts (1)
useAuthStore(7-52)src/app/providers/ToastProvider.tsx (1)
useToast(44-48)src/shared/config/musicGenres.ts (1)
MUSIC_GENRES(1-12)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)
src/pages/mypage/ui/components/StepHeader.tsx (2)
src/pages/mypage/types/mypage.ts (1)
CUSTOMIZE_STEP(1-1)src/shared/styles/theme.ts (1)
theme(3-116)
src/pages/mypage/ui/main/components/CdNameInfo.tsx (1)
src/shared/styles/theme.ts (1)
theme(3-116)
src/shared/ui/Input.tsx (1)
src/shared/styles/theme.ts (1)
theme(3-116)
src/shared/ui/Cd.tsx (3)
src/entities/playlist/types/playlist.ts (1)
CdCustomData(10-22)src/shared/styles/mixins.ts (1)
flexColCenter(10-15)src/shared/styles/theme.ts (1)
theme(3-116)
src/features/share/ui/ShareButton.tsx (1)
src/shared/lib/useCopyCdShareUrl.ts (1)
useCopyCdShareUrl(5-37)
src/pages/mypage/ui/customize/index.tsx (3)
src/pages/mypage/types/mypage.ts (1)
CUSTOMIZE_STEP(1-1)src/shared/ui/Modal.tsx (1)
ModalProps(9-19)src/entities/playlist/model/useMyCd.ts (1)
useMyCdActions(28-65)
src/features/customize/model/useCustomize.ts (2)
src/features/customize/api/customize.ts (5)
getFinalCdCustom(35-37)postYouTubeVideoInfo(15-17)postCdTempSave(20-22)postCdFinalCreate(25-27)postCdFinalUpdate(30-32)src/features/customize/types/customize.ts (3)
CdTempSavePayload(13-18)CdFinalCreatePayload(20-20)CdFinalUpdatePayload(22-22)
src/pages/mypage/ui/customize/step2/index.tsx (4)
src/pages/mypage/ui/customize/index.tsx (1)
CustomizeStepProps(13-21)src/features/customize/model/useCustomize.ts (2)
useUserSticker(87-116)useCdFinalSave(64-85)src/shared/ui/Modal.tsx (1)
ModalProps(9-19)src/pages/mypage/types/mypage.ts (2)
StickerInfoType(19-31)StickerThemeType(5-15)
src/features/customize/api/customize.ts (2)
src/shared/api/httpClient.ts (1)
api(5-19)src/features/customize/types/customize.ts (7)
YoutubeVideoInfo(5-11)CdTempSavePayload(13-18)CdFinalCreatePayload(20-20)CdFinalSaveResponse(26-37)CdFinalUpdatePayload(22-22)CdCustomResponse(24-24)UserStickerListResponse(45-47)
src/features/customize/types/customize.ts (2)
src/shared/config/musicGenres.ts (1)
MusicGenreId(15-15)src/entities/playlist/types/playlist.ts (1)
CdCoverInfo(24-26)
src/entities/playlist/api/playlist.ts (2)
src/shared/api/httpClient.ts (1)
api(5-19)src/entities/playlist/types/playlist.ts (4)
CdMetaResponse(28-28)PlaylistDetailResponse(58-62)PlaylistParams(91-95)PlaylistResponse(84-89)
🪛 Biome (2.1.2)
src/pages/mypage/ui/main/components/MyLikedCdList.tsx
[error] 10-10: Do not shadow the global "Error" property.
Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.
(lint/suspicious/noShadowRestrictedNames)
src/pages/mypage/ui/main/components/MyCdList.tsx
[error] 10-10: Do not shadow the global "Error" property.
Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.
(lint/suspicious/noShadowRestrictedNames)
🔇 Additional comments (52)
src/App.tsx (1)
98-137: 접근성 개선이 적절합니다.장식 요소들을
aria-hidden="true"속성을 가진 div로 감싼 것은 좋은 접근성 개선입니다. 이를 통해 스크린 리더 사용자에게 불필요한 장식 콘텐츠가 전달되지 않습니다.추가된 DOM 노드는 내부 요소들이 모두 absolute positioning을 사용하기 때문에 레이아웃에 영향을 주지 않으며, 접근성 향상이라는 명확한 목적이 있어 정당화됩니다.
src/pages/home/index.tsx (1)
7-7: LGTM! 불필요한 import 제거 완료알림 기능 제거에 따라 Notification import를 정리한 것이 적절합니다.
src/shared/styles/globalStyle.ts (1)
42-44: LGTM: 포맷팅 정리
all: unset포맷팅이 일관성 있게 수정되었습니다.src/pages/mycd/index.tsx (1)
8-8: LGTM!import 경로가 모듈 재구성에 따라 올바르게 업데이트되었습니다. 훅 사용 방식은 동일하게 유지되어 기존 동작이 보존됩니다.
src/pages/mypage/ui/notification/index.tsx (1)
4-4: LGTM!import 경로가 소문자 mypage 네임스페이스로 올바르게 업데이트되었습니다.
src/pages/mypage/ui/terms/index.tsx (1)
1-1: LGTM!import 경로가 소문자 mypage 네임스페이스로 올바르게 업데이트되었습니다.
src/pages/mypage/ui/privacy/index.tsx (1)
1-1: LGTM!import 경로가 소문자 mypage 네임스페이스로 올바르게 업데이트되었습니다.
src/pages/mypage/ui/unregister/index.tsx (1)
5-5: LGTM!import 경로가 소문자 mypage 네임스페이스로 올바르게 업데이트되었습니다.
src/pages/mypage/ui/MypageLayout.tsx (1)
3-7: LGTM!컴포넌트명이 파일명과 일치하도록 변경되었으며, 프로젝트 전반의 myPage → mypage 네이밍 통일 작업에 부합합니다.
src/app/providers/ToastProvider.tsx (2)
7-7: LGTM!CD 삭제 기능을 위한 새 토스트 타입이 추가되었습니다.
15-15: LGTM!토스트 메시지가 한국어로 명확하게 작성되었으며, 다른 메시지들과 형식이 일관됩니다.
src/pages/mypage/ui/setting/index.tsx (1)
8-9: LGTM!import 경로 변경이 프로젝트의 폴더 구조 재구성 목적에 부합하며, flexRowCenter mixin 추가도 적절합니다.
src/shared/ui/Input.tsx (2)
31-32: LGTM!
required와disabledprops 추가가 적절하며, HTML 표준 속성을 올바르게 전달하고 있습니다. 기본값을false로 설정한 것도 합리적입니다.Also applies to: 51-52, 72-73
84-84: LGTM!
InputWrap에position: relative를 추가하고,ErrorMessage를 절대 위치로 배치하는 방식은 에러 메시지를 input 하단에 일관되게 표시하는 표준 패턴입니다.Also applies to: 125-127
src/stories/ToggleSwitch.stories.tsx (1)
5-5: LGTM!import 경로 변경이 프로젝트의 폴더명 통일 목적에 부합합니다.
src/assets/icons/index.ts (1)
12-12: LGTM!새로운 아이콘 추가(CancelCircle, PlusBlack, StartBlack, Eye 관련, NoLike, LinkPrimary)가 PR의 UI 변경사항(비공개 CD UI, 나의 좋아요 등)을 적절히 반영하고 있습니다.
Also applies to: 20-20, 28-28, 45-45, 50-53
src/pages/mypage/lib/customizeTheme.ts (1)
1-1: LGTM!타입 import 경로 변경이 프로젝트의 폴더명 통일과 일치합니다.
src/pages/mypage/ui/customize/step1/components/index.ts (1)
1-1: LGTM!barrel export 패턴을 사용하여 TrackItem 컴포넌트를 외부에서 쉽게 import할 수 있도록 했습니다.
src/entities/playlist/model/usePlaylists.ts (1)
45-50: LGTM!
usePlaylistDetail에enabled옵션을 추가하여 쿼리 실행을 조건부로 제어할 수 있게 했습니다. 기본값을true로 설정하여 기존 동작을 유지하므로 breaking change가 없습니다. 이는 Tanstack Query의 표준 패턴을 따르는 좋은 개선사항입니다.src/features/share/ui/ShareButton.tsx (1)
12-12: LGTM!CD 공유 URL 복사 로직을
useCopyCdShareUrlhook으로 분리한 것은 훌륭한 리팩토링입니다. 코드 재사용성이 향상되고, 관심사의 분리 원칙을 잘 따르고 있습니다.Also applies to: 25-25, 88-90
src/pages/mypage/ui/components/index.ts (1)
1-4: LGTM! 깔끔한 barrel export 구조입니다.컴포넌트들을 중앙에서 관리하는 깔끔한 구조입니다. import 경로를 단순화하여 개발자 경험을 개선합니다.
src/shared/config/routesConfig.ts (2)
7-15: 일관된 네이밍 변경이 잘 적용되었습니다.
myPage→mypage로의 디렉토리명 변경이 모든 import 경로에 일관되게 반영되어 있습니다.
81-81: URL 네이밍 통일이 적절합니다.
:id/playlist→:id/tracklist로 변경되어 PR 목표인 URL 네이밍 통일이 달성되었습니다.src/shared/ui/Modal.tsx (3)
12-12: Optional prop 패턴이 올바르게 적용되었습니다.TypeScript의 optional property를 활용하여 하위 호환성을 유지하면서 새로운 기능을 추가했습니다.
Also applies to: 24-24
35-38: 레이아웃 구조 변경이 적절합니다.title과 description을 감싸는 wrapper div를 추가하여 레이아웃을 명확하게 구분했습니다. 조건부 렌더링도 간결하게 처리되었습니다.
83-89: 테마 시스템을 일관되게 활용했습니다.색상과 폰트 스타일이 테마 시스템을 통해 관리되고 있어 유지보수성이 좋습니다.
src/pages/mypage/ui/main/index.tsx (2)
30-36: TODO 주석이 명확합니다.알림 기능이 2차 스프린트로 이동한 사유가 명확하게 기록되어 있습니다.
79-96: Transient props 사용이 올바릅니다.Styled components의 transient props($isActive)를 사용하여 DOM에 불필요한 속성이 전달되지 않도록 했습니다.
src/pages/mypage/ui/main/components/CdNameInfo.tsx (2)
3-10: 간결한 Presentational 컴포넌트입니다.컴포넌트가 단일 책임 원칙을 잘 따르고 있습니다. inline 타입 정의도 이 정도 크기의 컴포넌트에는 적절합니다.
21-30: 텍스트 overflow 처리가 적절합니다.
-webkit-line-clamp를 사용한 2줄 말줄임 처리와word-break: break-word를 통한 긴 단어 처리가 잘 구현되었습니다.src/pages/mypage/ui/customize/step3/index.tsx (2)
21-24: 캐시 무효화 로직이 적절합니다.tracklist 페이지로 이동하기 전에 캐시를 무효화하여 최신 데이터를 보장합니다. React Query의 베스트 프랙티스를 잘 따르고 있습니다.
10-10: 훅 네이밍 변경이 일관되게 적용되었습니다.
useCdCustomData→useFinalCdCustom으로 변경되어 API 리팩토링이 반영되었습니다.src/shared/lib/useCopyCdShareUrl.ts (2)
12-29: 클립보드 API fallback 처리가 우수합니다.모던 브라우저의
navigator.clipboard.writeText와 레거시 브라우저를 위한execCommandfallback을 모두 구현했습니다. Safari와 모바일 브라우저 호환성을 고려한 좋은 접근입니다.
8-34: useCallback 사용이 적절합니다.
toast의존성을 명시하고 useCallback으로 함수를 메모이제이션하여 불필요한 재생성을 방지했습니다.src/pages/mypage/ui/components/ToggleSwitch.tsx (2)
23-26: 이벤트 핸들러 구현이 올바릅니다.클릭과 키보드(Enter, Space) 이벤트 모두 처리하여 접근성을 고려했습니다.
13-13: 검증 완료: ToggleSwitch 사용처 모두 새 시그니처와 호환ToggleSwitch 컴포넌트의 모든 사용처를 확인한 결과, 새로운 시그니처
setIsOn: (value: boolean) => void과 완전히 호환됩니다:
src/pages/mypage/ui/setting/index.tsx:24(주석 처리): 상태 setter 직접 전달src/pages/mypage/ui/customize/step1/index.tsx:249: boolean을 받아 void를 반환하는 함수 전달 ✓src/stories/ToggleSwitch.stories.tsx:31: 상태 setter 직접 전달기존 updater 패턴
(prev) => !prev형식을 사용하는 곳은 없으며, 모든 사용처가 이미 새 시그니처에 맞춰져 있습니다.src/pages/mypage/ui/main/components/index.ts (1)
1-4: 배럴 export 구조가 깔끔합니다.컴포넌트들을 단일 진입점으로 재export하는 표준 패턴을 잘 따르고 있습니다.
src/pages/mypage/ui/main/components/MyLikedCdList.tsx (4)
10-10: 정적 분석 경고는 무시해도 됩니다.Biome가
Error를 전역 객체 섀도잉으로 플래그했으나, 이는 명명된 import이므로 실제 문제가 없습니다.
13-21: 로딩 및 에러 처리 로직이 적절합니다.early return 패턴으로 로딩/에러 상태를 깔끔하게 처리하고 있습니다.
41-54: 하드코딩된 네비게이션 경로를 확인하세요.Line 44에서 모든 CD를 클릭 시 동일한
/mycd경로로 이동합니다. 각 CD의 고유 ID를 사용하지 않고 있는데, 이것이 의도된 동작인지 확인이 필요합니다. 일반적으로는/mycd/${item.playlistId}같은 형태가 예상됩니다.TODO 주석에서 병합 후 작업 예정이라고 명시되어 있으므로, 현재는 임시 구현일 수 있습니다.
65-77: 반응형 그리드 레이아웃 구현이 좋습니다.3열 그리드를 calc를 사용해 정확하게 계산하고 있으며, gap을 상수로 관리하는 것도 좋은 접근입니다.
src/pages/mypage/ui/main/components/MyCdList.tsx (2)
10-10: 정적 분석 경고는 무시해도 됩니다.MyLikedCdList와 동일하게,
Error는 명명된 import이므로 실제 문제가 없습니다.
38-53: 네비게이션 로직이 올바릅니다.각 CD 클릭 시 해당 playlistId를 사용하여 트랙리스트로 이동하고, state를 통해 출처를 전달하는 패턴이 적절합니다.
src/pages/mypage/ui/tracklist/index.tsx (2)
36-41: 조건부 데이터 페칭 패턴이 효율적입니다.
isFromMyCdList상태에 따라 적절한 데이터 소스를 사용하고,enabled옵션으로 불필요한 쿼리를 방지하고 있습니다.
52-82: CD 삭제 로직과 에러 처리가 잘 구현되어 있습니다.모달을 통한 확인 절차, mutation 성공/실패 처리, 토스트 메시지 표시까지 전체 플로우가 적절합니다.
src/pages/mypage/ui/components/StepHeader.tsx (2)
6-6: 타입 변경이 일관되게 적용되었습니다.
CustomizeStep에서CUSTOMIZE_STEP로의 마이그레이션이 props, 내부 로직, 타입 캐스팅에 모두 올바르게 반영되어 있습니다. Line 25의 가드 조건 덕분에 Line 29의 타입 캐스팅도 안전합니다.Also applies to: 16-17, 29-29
94-95: UI 스타일 개선이 적용되었습니다.버튼에 패딩과 레이블 폰트 스타일이 추가되어 일관된 디자인을 유지하고 있습니다.
src/entities/playlist/types/playlist.ts (3)
3-8: CdBasicInfo 인터페이스가 명확하게 정의되었습니다.
MyCdInfo에서 리네이밍되었고,isRepresentative필드가 제거되고isPublic으로 대체되어 PR 목표와 일치합니다.
10-22: CdCustomData의 선택적 필드들이 적절합니다.
cdItemId,theme,imageUrl을 선택적으로 만들어 생성/수정 시나리오에서 유연성을 제공합니다.zCoordinate추가로 3D 배치가 가능해졌습니다.
36-43: Track 인터페이스에 orderIndex가 추가되었습니다.트랙 순서를 명시적으로 관리할 수 있게 되어, 정렬 로직이 간단해졌습니다. 이는 tracklist 컴포넌트에서 활용되고 있습니다.
src/pages/mypage/ui/customize/step1/components/TrackItem.tsx (1)
19-25: Reorder.Item value 키 일치 여부 확인value={track.id}를 사용 중인데, 상위 Reorder.Group의 values 배열 원소와 동일 기준(문자열 id)인지 확인이 필요합니다. 객체를 재정렬한다면 value로 객체 자체를 넘기는 패턴이 더 안전합니다.
상위 컴포넌트에서 Reorder.Group의 values 정의와 onReorder 로직이 id 기준으로 동작하는지 확인 부탁드립니다.
src/features/customize/api/customize.ts (1)
15-17: 응답 타입 스키마 검증 필요:valid필드 불일치 확인됨검색 결과,
valid필드는 API 함수의 반환 타입에만 지정되어 있고 실제 코드에서는 전혀 사용되지 않고 있습니다.
postYouTubeVideoInfo호출 처(src/features/customize/model/useCustomize.ts:38)에서 응답을videoResArr로 받지만, 오직.length체크만 수행YoutubeVideoInfo기본 타입 정의(src/features/customize/types/customize.ts:5-9)에valid필드 없음- 다른 사용처(
src/pages/mypage/ui/customize/step1/index.tsx:36)에서도 상태 타입을(YoutubeVideoInfo & { id: string })[]로 정의 →valid미포함런타임 리스크:
- 서버가
valid필드를 반환하지 않으면 타입 에러 발생 가능valid: false항목에 대한 검증 로직 부재 → 잘못된 데이터가 처리될 수 있음권장 조치:
- 서버 응답 스키마 확인 후,
valid필드가 실제 반환되는지 검증valid필드가 필요하다면 응답 처리 시 검증 로직 추가 (예:invalid항목 필터링)- 또는 불필요한 타입 어노테이션 제거
maylh
left a comment
There was a problem hiding this comment.
수고하셨습니다 ~~~! 👍👍
svg 아이콘 중에 stroke나 fill 색 때문에 컬러별로 나눠둔 게 있는 것 같은데 (eye_slash),
아이콘 하나만 넣고, 내부 fill이나 stroke는 지운 뒤에 처럼 사용하면 컬러 별로 각각 아이콘 생성하지 않아도 될 거예요 !
| <li key={item.playlistId}> | ||
| {/* TODO: 나의CD 이동 후 좋아요한 CD 재생은 각 브랜치 병합 이후 작업 */} | ||
| <CdButton type="button" onClick={() => navigate('/mycd')}> | ||
| {/* TODO: CD 컴포넌트 좋아요는 각 브랜치 병합 이후 작업 (10/20~) */} | ||
| <Cd | ||
| variant="responsive" | ||
| stickers={item?.cdResponse?.cdItems} | ||
| isPublic={item?.isPublic} | ||
| /> | ||
| </CdButton> | ||
| <CdNameInfo title={item?.playlistName || ''} creator={item?.creatorNickname || ''} /> | ||
| </li> |
There was a problem hiding this comment.
이거 Playlist 컴포넌트로 만들어두었어요 ! 좋아요 기능도 구현되어 있습니다 Storybook
다만 해당 컴포넌트 사용하실 경우에는 Cd variant랑 onClick route, 일부 css가 조금 달라서, type 같은 props를 받아서 내부에서 조건부 처리는 필요해 보입니다 🥲 .....
감사합니다! 오늘 병합 먼저 처리하고 다음 이슈에서 작업해두겠습니다~! |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
src/pages/mypage/ui/customize/index.tsx (1)
48-52: cdId 타입 변환 로직 재확인 필요라인 52의
Number(cdId)는 cdId가 null일 때 0을 반환하여 의도하지 않은 동작을 유발할 수 있습니다. 라인 25-30에서 제안한 수정사항을 적용하면 이 문제도 함께 해결됩니다.src/pages/mypage/ui/customize/step1/components/TrackItem.tsx (1)
90-93: 중복 선언 제거 확인 — LGTM.position 중복 선언 이슈가 해소되었습니다. 스타일 정상입니다.
src/pages/mypage/ui/customize/step1/index.tsx (1)
145-152: Modal 닫기 로직 중복 — 동일하게 캐스팅 제거 권장.위와 동일 이슈이므로
closeModal재사용으로 통일하세요.
🧹 Nitpick comments (14)
src/pages/mypage/ui/setting/index.tsx (1)
15-20: 로그아웃 핸들러에 에러 처리 및 세션 정리를 추가하는 것을 권장합니다.현재 구현은 기본 기능을 수행하지만, 다음 개선 사항을 고려해보세요:
setLogin에서sessionStorage.removeItem('anonymous_token')을 수행하므로, 로그아웃 시에도 세션 스토리지 정리가 필요할 수 있습니다.- 에러 처리 및 사용자 피드백(로딩 상태, 성공/실패 메시지)을 추가하면 더 나은 UX를 제공할 수 있습니다.
다음과 같이 개선할 수 있습니다:
const onLogoutClick = () => { + try { + sessionStorage.clear() // 또는 특정 키만 제거 setLogout() navigate('/', { replace: true }) + } catch (error) { + console.error('로그아웃 실패:', error) + // 에러 토스트 메시지 표시 등 + } }src/pages/mypage/ui/unregister/index.tsx (3)
20-30: Optional: 초기 Modal 상태의 description 값 고려
description이ModalProps에서 optional로 정의되어 있으므로, 빈 문자열 대신undefined를 사용하는 것이 더 명확할 수 있습니다.const [modal, setModal] = useState<ModalProps>({ isOpen: false, title: '', - description: '', + description: undefined, ctaType: 'single',
50-51: 프로덕션 환경의 에러 로깅 개선 권장
console.error를 직접 사용하는 것보다 에러 모니터링 서비스(예: Sentry)나 중앙화된 로깅 시스템을 사용하는 것이 프로덕션 환경에서 더 적합합니다.
68-101: 약관 내용을 별도 상수로 분리 권장약관 텍스트가 컴포넌트 내부에 하드코딩되어 있어 향후 내용 수정이나 다국어 지원 시 관리가 어려울 수 있습니다. 별도 상수 파일이나 CMS로 분리하는 것을 권장합니다.
예시 구조:
// constants/terms.ts export const UNREGISTER_TERMS = { privacy: { title: '1. 개인정보처리방침', items: [ '이용자의 '동의를 기반으로...', // ... ] }, // ... }src/pages/mypage/ui/customize/step1/components/TrackItem.tsx (3)
27-29: 드래그 핸들 a11y 강화 (키보드/스크린리더 지원).pointer 기반만으로 드래그 시작은 키보드 접근이 어렵습니다. 최소한 핸들에 라벨과 포커스 스타일을 추가해주세요.
-<DragHandle as={motion.button} type="button" onPointerDown={(e) => controls.start(e)}> +<DragHandle + as={motion.button} + type="button" + aria-label="트랙 순서 변경" + onPointerDown={(e) => controls.start(e)} +>
31-31: URL 입력 필드: disabled 대신 readOnly 권장.disabled는 탭 포커스/복사 불가합니다. 읽기 전용이면 readOnly로 두고 접근성을 유지하세요.
-<Input type="url" value={track.link} disabled /> +<Input type="url" value={track.link} readOnly aria-label="트랙 링크" />
78-82: :has() 선택자 호환성 확인 및 결합도 낮추기.
:has(input)는 최신 브라우저에서만 동작합니다. 지원 브라우저 범위를 확인하고, 가능하면 Input 래퍼에 클래스/prop(예: $withRightPadding)로 스타일을 제어해 DOM 구조 결합도를 낮추세요.src/pages/mypage/ui/customize/step1/index.tsx (7)
31-35: Setter 오타(sestBasicInfoMap) — 일관된 네이밍으로 수정.카멜 케이스/가독성 측면에서 최소 오타 수정은 필수입니다. (추가로
basicInfoMap→basicInfo리네임은 후속 권장)-const [basicInfoMap, sestBasicInfoMap] = useState({ +const [basicInfoMap, setBasicInfoMap] = useState({아래 참조 사용처도 함께 변경 필요:
- Line 95, 241, 252 등
sestBasicInfoMap→setBasicInfoMap치환.
57-60: non‑null 단언(genre!) 제거로 타입 안정성 확보.실행 경로상 유효하더라도
!는 취약합니다. 로컬 변수로 안전하게 처리하세요.- tempSaveMap: { - ...basicInfoMap, - genre: basicInfoMap.genre!.id, - youTubeVideoInfo: tracklist.map(({ id: _id, ...rest }) => rest), - }, + const genreId = basicInfoMap.genre?.id + if (!genreId) return + tempSaveMap: { + ...basicInfoMap, + genre: genreId, + youTubeVideoInfo: tracklist.map(({ id: _id, ...rest }) => rest), + },
300-301: 입력 도중 trim은 UX 저하. 추가 시점에만 trim.onChange에서 trim하면 커서 점프/붙여넣기 이슈가 납니다. 값 추가 직전에만 trim 하세요.
-onChange={(e) => setCurrentTrackUrl(e.target.value.trim())} +onChange={(e) => setCurrentTrackUrl(e.target.value)}그리고 onTrackAddClick 시작부에:
const url = currentTrackUrl.trim() if (!url) return const thumbnailUrl = await getValidYoutubeThumbnail(url)
68-79: Modal 타입 강제 캐스팅 제거.
setModal({ isOpen: false } as ModalProps)는 런타임 불일치 숨김 위험. partial 허용이 아니라면 안전한 클로저/머지 방식으로 닫으세요.const closeModal = () => setModal((prev) => ({ ...prev, isOpen: false })) // 사용처: onClose/onConfirm에서 closeModal 전달
118-139: 썸네일 프리로드 중 언마운트/경합 처리.이미지 로드 완료 전에 언마운트되면 state 업데이트 경고가 날 수 있습니다. 취소 플래그나 Abort 시그널로 방어하세요.
- setIsThumbnailLoading(true) - const thumbnailUrl = `https://img.youtube.com/vi/${videoId}/default.jpg` - const isLoaded = await new Promise<boolean>((resolve) => { - const img = new Image() - img.onload = () => resolve(true) - img.onerror = () => resolve(false) - img.src = thumbnailUrl - }) + setIsThumbnailLoading(true) + let cancelled = false + const thumbnailUrl = `https://img.youtube.com/vi/${videoId}/default.jpg` + const isLoaded = await new Promise<boolean>((resolve) => { + const img = new Image() + img.onload = () => resolve(!cancelled) + img.onerror = () => resolve(false) + img.src = thumbnailUrl + }) + if (cancelled) return '' ... -} finally { - setIsThumbnailLoading(false) -} +} finally { + if (!cancelled) setIsThumbnailLoading(false) +}컴포넌트 외부에
useEffectcleanup에서cancelled = true설정하거나, 함수 내부에서AbortController패턴으로 개선 가능합니다.
44-48: YouTube 링크 검증: host 기반 파싱으로 오탐 줄이기.
startsWith대신 URL 파싱 후 host/path로 판별하면 견고합니다.const ALLOWED_HOSTS = new Set(['youtu.be', 'www.youtube.com', 'music.youtube.com']) const isAllowed = (() => { try { const u = new URL(link) return ALLOWED_HOSTS.has(u.host) && (!!u.searchParams.get('v') || u.pathname.split('/')[1]) } catch { return false } })()
186-188:find(... )!단언 대신 방어 코드.이상치로
id불일치가 들어오면 크래시합니다. 안전하게 처리하세요.-const track = prev.find((t) => t.id === id)! -return { ...track, orderIndex: idx + 1 } +const track = prev.find((t) => t.id === id) +return track ? { ...track, orderIndex: idx + 1 } : prev[idx]!
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/features/auth/api/auth.ts(1 hunks)src/features/auth/model/useAuth.ts(2 hunks)src/pages/mypage/ui/customize/index.tsx(5 hunks)src/pages/mypage/ui/customize/step1/components/TrackItem.tsx(1 hunks)src/pages/mypage/ui/customize/step1/index.tsx(1 hunks)src/pages/mypage/ui/setting/index.tsx(4 hunks)src/pages/mypage/ui/unregister/index.tsx(1 hunks)src/stories/Modal.stories.tsx(2 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/features/auth/api/auth.tssrc/pages/mypage/ui/customize/step1/components/TrackItem.tsxsrc/features/auth/model/useAuth.tssrc/stories/Modal.stories.tsxsrc/pages/mypage/ui/setting/index.tsxsrc/pages/mypage/ui/unregister/index.tsxsrc/pages/mypage/ui/customize/index.tsxsrc/pages/mypage/ui/customize/step1/index.tsx
🧬 Code graph analysis (7)
src/features/auth/api/auth.ts (1)
src/shared/api/httpClient.ts (1)
api(5-19)
src/pages/mypage/ui/customize/step1/components/TrackItem.tsx (3)
src/features/customize/types/customize.ts (1)
YoutubeVideoInfo(5-11)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)src/shared/styles/theme.ts (1)
theme(3-116)
src/features/auth/model/useAuth.ts (1)
src/features/auth/api/auth.ts (2)
getUserInfo(20-22)deleteAccount(15-17)
src/pages/mypage/ui/setting/index.tsx (2)
src/features/auth/store/authStore.ts (1)
useAuthStore(7-52)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)
src/pages/mypage/ui/unregister/index.tsx (5)
src/features/auth/model/useAuth.ts (1)
useDeleteAccount(27-32)src/features/auth/store/authStore.ts (1)
useAuthStore(7-52)src/shared/ui/Modal.tsx (1)
ModalProps(9-19)src/shared/styles/terms.ts (2)
TermsContainer(5-12)TermsItems(14-19)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)
src/pages/mypage/ui/customize/index.tsx (4)
src/pages/mypage/types/mypage.ts (1)
CUSTOMIZE_STEP(1-1)src/shared/ui/Modal.tsx (1)
ModalProps(9-19)src/entities/playlist/types/playlist.ts (1)
PlaylistDetailResponse(58-62)src/entities/playlist/model/useMyCd.ts (1)
useMyCdActions(28-65)
src/pages/mypage/ui/customize/step1/index.tsx (6)
src/pages/mypage/ui/customize/index.tsx (1)
CustomizeStepProps(13-21)src/features/customize/model/useCustomize.ts (1)
useCdTempSave(28-62)src/shared/config/musicGenres.ts (2)
MUSIC_GENRES(1-12)MusicGenre(17-17)src/features/customize/types/customize.ts (1)
YoutubeVideoInfo(5-11)src/shared/ui/Modal.tsx (1)
ModalProps(9-19)src/shared/styles/mixins.ts (2)
flexColCenter(10-15)flexRowCenter(3-8)
🔇 Additional comments (13)
src/pages/mypage/ui/setting/index.tsx (4)
8-10: LGTM! 임포트 경로가 올바르게 업데이트되었습니다.새로운 기능에 필요한 임포트가 추가되었고, 경로 네이밍 변경 사항(myPage → mypage)이 올바르게 반영되었습니다.
29-33: LGTM! 알림 기능이 적절하게 주석 처리되었습니다.PR 설명과 일치하게 2차 스프린트로 이관될 알림 기능이 TODO 주석과 함께 명확하게 표시되어 있습니다.
67-74: 이전 리뷰에서 지적된 로그아웃 버튼 이슈가 해결되었습니다.로그아웃 버튼에
onLogoutClick핸들러가 올바르게 연결되었고, 탈퇴하기 버튼도 정상적으로 동작합니다. 코드 변경 사항이 좋습니다!
113-130: LGTM! 스타일 컴포넌트가 코딩 가이드라인을 잘 따르고 있습니다.테마 시스템과 mixin을 적절히 활용하고 있으며, CSS 포맷팅이 가독성 있게 작성되었습니다.
src/features/auth/api/auth.ts (1)
14-17: LGTM!회원 탈퇴 API 함수가 올바르게 구현되었습니다. 타입 명시와 주석이 적절하며, 기존 API 함수들과 일관된 패턴을 따르고 있습니다.
src/features/auth/model/useAuth.ts (1)
3-3: LGTM!회원 탈퇴 mutation hook이 올바르게 구현되었습니다. 기존 hook들과 일관된 패턴을 따르고 있으며, 구조가 명확합니다.
Also applies to: 27-32
src/pages/mypage/ui/unregister/index.tsx (1)
126-133: 절대 위치 지정 시 반응형 동작 확인 권장
BottomCraWrap이 절대 위치(position: absolute)와 고정된bottom: 34px값을 사용합니다. 다양한 화면 크기나 모바일 키보드 표시 시 의도한 대로 동작하는지 확인해주세요.src/pages/mypage/ui/customize/index.tsx (4)
2-11: LGTM: import 구조가 명확합니다절대 경로를 사용한 import 구조가 깔끔하고, 필요한 타입과 훅이 모두 올바르게 import되었습니다.
13-21: LGTM: Props 타입 정의가 명확합니다CUSTOMIZE_STEP 타입과 prevTracklist 네이밍 변경이 일관성 있게 적용되었습니다.
32-46: LGTM: Modal 상태에 description 필드가 올바르게 추가되었습니다Modal 컴포넌트의 새로운 description prop과 일관되게 상태가 업데이트되었습니다.
54-100: LGTM: 에러/로딩 핸들링 및 렌더링 로직이 적절합니다에러 발생 시 /error로 이동하고, 로딩 중에는 Loading 컴포넌트를 표시하는 로직이 올바르게 구현되었습니다. 각 Step 컴포넌트에 prevTracklist가 일관되게 전달되고, Modal에도 description prop이 올바르게 전달되었습니다.
src/pages/mypage/ui/customize/step1/index.tsx (2)
59-60: ESLint disable 제거 대응 — 잘 하셨습니다.
{ id: _id, ...rest }로 미사용 필드 명시 제거가 컨벤션에 부합합니다. LGTM.
326-334: Reorder 제어 방식 LGTM.
values={ids}+Reorder.Item value={id}조합이 일관되고onReorder처리도 적절합니다.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (3)
src/pages/mypage/ui/customize/index.tsx (2)
25-30: cdId 추출 및 타입 안전성 문제 (이전 리뷰 미해결)이전 리뷰에서 지적된
location.state타입 안전성 문제가 여전히 남아있습니다:
location.state가 객체이지만cdId속성이 없는 경우cdId: undefined가 됩니다- Line 30의
Number(cdId)는cdId가null일 때0을 반환합니다- Line 52에서
Number(cdId)를 사용할 때null→0변환으로 인해 의도하지 않은 동작이 발생할 수 있습니다As per coding guidelines.
다음과 같이 수정하세요:
const navigate = useNavigate() const location = useLocation() - const { cdId } = location?.state ?? { cdId: null } + const cdId = (location.state as { cdId?: number } | null)?.cdId ?? null const [currentStep, setCurrentStep] = useState<CUSTOMIZE_STEP>(1) const [currentCdId, setCurrentCdId] = useState<number | null>(null) - const [isEditMode] = useState<boolean>(!!cdId && Number(cdId) > 0) + const [isEditMode] = useState<boolean>(typeof cdId === 'number' && cdId > 0)
48-52:Number(cdId)사용 시 null 처리 문제
cdId가null일 때Number(null)은0을 반환하여 의도하지 않은 동작을 일으킬 수 있습니다.useMyCdActions에 잘못된 ID가 전달될 수 있습니다.다음과 같이 수정하세요:
const { tracklist: prevTracklist, isLoading, isError, - } = useMyCdActions(isEditMode ? Number(cdId) : -1) + } = useMyCdActions(isEditMode && typeof cdId === 'number' ? cdId : -1)src/pages/mypage/ui/tracklist/index.tsx (1)
100-101: 에러 처리에서 return 문이 누락되었습니다.Line 101에서
/error로 이동한 후 return 문이 없어, 컴포넌트가 계속 렌더링을 시도할 수 있습니다. 이는 예상치 못한 동작이나 에러를 발생시킬 수 있습니다.다음과 같이 수정하세요:
if (isLoading) return <Loading isLoading={isLoading} /> - if (isError) return navigate('/error') + if (isError) { + navigate('/error') + return null + }
🧹 Nitpick comments (1)
src/pages/mypage/ui/tracklist/index.tsx (1)
36-41: 조건부 데이터 페칭 로직에 설명 주석을 추가하는 것을 권장합니다.
isFromMyCdList플래그에 따라 서로 다른 훅을 사용하는 이유가 명확하지 않을 수 있습니다. 향후 유지보수를 위해 간단한 주석을 추가하면 좋겠습니다.+ // 진입 경로에 따라 다른 데이터 소스 사용: 내 CD 목록에서 진입 시 useMyCdActions, 일반 플레이리스트 상세에서 진입 시 usePlaylistDetail const isFromMyCdList = state?.isFromMyCdList === true const { data, isLoading, isError } = usePlaylistDetail(Number(cdId), { enabled: !isFromMyCdList }) const { tracklist, deleteMutation, togglePublicMutation } = useMyCdActions(Number(cdId), { enabled: isFromMyCdList, })
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
index.html(1 hunks)src/pages/mypage/ui/customize/index.tsx(5 hunks)src/pages/mypage/ui/customize/step3/index.tsx(2 hunks)src/pages/mypage/ui/tracklist/index.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/pages/mypage/ui/customize/step3/index.tsxsrc/pages/mypage/ui/tracklist/index.tsxsrc/pages/mypage/ui/customize/index.tsx
🧬 Code graph analysis (3)
src/pages/mypage/ui/customize/step3/index.tsx (1)
src/features/customize/model/useCustomize.ts (1)
useFinalCdCustom(19-26)
src/pages/mypage/ui/tracklist/index.tsx (8)
src/shared/ui/Modal.tsx (1)
ModalProps(9-19)src/entities/playlist/model/usePlaylists.ts (1)
usePlaylistDetail(45-51)src/entities/playlist/model/useMyCd.ts (1)
useMyCdActions(28-65)src/shared/lib/useCopyCdShareUrl.ts (1)
useCopyCdShareUrl(5-37)src/features/auth/store/authStore.ts (1)
useAuthStore(7-52)src/app/providers/ToastProvider.tsx (1)
useToast(44-48)src/shared/config/musicGenres.ts (1)
MUSIC_GENRES(1-12)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)
src/pages/mypage/ui/customize/index.tsx (4)
src/pages/mypage/types/mypage.ts (1)
CUSTOMIZE_STEP(1-1)src/shared/ui/Modal.tsx (1)
ModalProps(9-19)src/entities/playlist/types/playlist.ts (1)
PlaylistDetailResponse(58-62)src/entities/playlist/model/useMyCd.ts (1)
useMyCdActions(28-65)
🔇 Additional comments (8)
index.html (1)
17-17: 형식 자동 감지 비활성화 메타 태그 추가 — 적절함전화번호, 이메일, 주소에 대한 브라우저의 자동 형식 감지를 명시적으로 비활성화하는 설정으로, 앱 전체의 형식 표현 제어를 위한 일관된 접근 방식입니다. PR에서 진행 중인 글로벌 스타일 업데이트와도 잘 정렬되어 있습니다.
src/pages/mypage/ui/customize/step3/index.tsx (2)
21-24: LGTM!캐시 무효화 후 페이지 이동하는 패턴이 적절하게 구현되었습니다.
playlistDetail쿼리 키는 트랙리스트 페이지에서 사용하는 캐시를 갱신하기 위한 것으로 보입니다.
51-51: LGTM!버튼 클릭 시 캐시 무효화와 네비게이션을 함께 처리하는
moveToTracklist함수를 적절하게 연결했습니다.src/pages/mypage/ui/customize/index.tsx (2)
13-21: LGTM!인터페이스 타입 정의가 새로운 데이터 구조와 타입에 맞게 올바르게 업데이트되었습니다.
CUSTOMIZE_STEP타입과PlaylistDetailResponse타입이 적절히 적용되었습니다.
32-46: LGTM!Modal 상태에
description필드가 추가되었고,ModalProps인터페이스의 선택적 필드로 올바르게 초기화되었습니다.src/pages/mypage/ui/tracklist/index.tsx (3)
52-98: 이벤트 핸들러 로직이 잘 구현되어 있습니다.
- 삭제 확인 모달 → mutation → 성공/실패 처리 흐름이 명확합니다
- 공개 토글은
useMyCdActions에서 자동으로 쿼리 무효화를 처리하므로 별도의 성공 핸들러가 없어도 적절합니다- 모달 상태 관리가 일관되게 함수형 업데이트를 사용하고 있습니다
126-129: TODO 주석이 존재하며, 모두 재생 기능이 구현 대기 중입니다.현재 버튼에 onClick 핸들러가 없지만, TODO 주석으로 향후 작업이 문서화되어 있습니다. 병합 후 관련 브랜치와 함께 작업 예정으로 보입니다.
병합 후 해당 기능 구현을 추적할 수 있도록 별도 이슈가 생성되어 있는지 확인해주세요.
184-225: 스타일 컴포넌트가 잘 구성되어 있습니다.
- 테마 시스템을 일관되게 활용하고 있습니다
flexRowCenter믹스인을 적절히 재사용하고 있습니다- 컴포넌트 네이밍이 명확하고 PascalCase를 따릅니다
- 불필요한 중복 스타일이 없습니다
Based on coding guidelines
|
🎵 Storybook Link 🎵 |

🛰️ 관련 이슈
✨ 주요 변경 사항
▪️ 기능
▪️ UI
🗯️ PR 포인트
🚀 알게된 점
📖 참고 자료 (선택)
Summary by CodeRabbit
릴리스 노트
New Features
Bug Fixes
Refactor
Style