Conversation
Walkthrough이 PR은 홈페이지와 나의CD 페이지의 UI/UX 개선을 반영한 광범위한 변경사항을 포함한다. 좋아요 기능 추가(API, 훅, 컴포넌트), 나의CD 페이지에 탭 네비게이션 구현, 홈페이지 캐러셀 컴포넌트 리팩토링, 플레이리스트 타입 변경(isRepresentative→isPublic), 새로운 아이콘/이미지 export 추가를 포함한다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as LikeButton
participant Hook as useLike
participant API as Like API
participant Auth as Auth Check
participant Query as React Query
participant Cache as Query Cache
User->>UI: 좋아요 버튼 클릭
UI->>Hook: toggleLike()
alt 로그인 미확인
Hook->>Auth: 인증 체크
Auth-->>Hook: 미인증
Hook->>UI: /login 리다이렉트
else 로그인 됨
alt 현재 좋아요 상태 = false
Hook->>API: postLike(playlistId)
API-->>Hook: 성공
Hook->>UI: liked = true (로컬 상태 업데이트)
else 현재 좋아요 상태 = true
Hook->>API: deleteLike(playlistId)
API-->>Hook: 성공
Hook->>UI: liked = false (로컬 상태 업데이트)
end
Hook->>Cache: invalidate queryKey: ['playlistDetail', playlistId]
Cache-->>Query: 캐시 초기화
Query-->>UI: 재페칭
end
sequenceDiagram
participant User
participant MyCd as MyCd Page
participant HeaderTab as HeaderTab
participant Carousel as LoopCarousel
participant API as Playlist API
participant Detail as PlaylistDetail Hook
User->>HeaderTab: "나의 CD" 또는 "좋아요한 CD" 탭 선택
HeaderTab->>MyCd: onSelect(tab) 콜백
MyCd->>MyCd: selectedTab 상태 업데이트
alt selectedTab = "MY"
MyCd->>API: useMyCdList('RECENT')
else selectedTab = "LIKE"
MyCd->>API: useMyLikedCdList('RECENT')
end
API-->>MyCd: 리스트 데이터
MyCd->>MyCd: centerPlaylist 초기화 (첫 항목)
MyCd->>Carousel: data, onCenterChange 전달
User->>Carousel: 캐러셀에서 플레이리스트 선택
Carousel->>MyCd: onCenterChange({playlistId, playlistName})
MyCd->>MyCd: centerPlaylist 업데이트
MyCd->>Detail: usePlaylistDetail(centerPlaylist.playlistId)
Detail-->>MyCd: 플레이리스트 상세 데이터
MyCd->>MyCd: convertedPlaylist 구성
MyCd->>UI: 플레이어 및 UI 렌더링
sequenceDiagram
participant Home as Home Page
participant LoopCarousel as LoopCarousel
participant EmblaSensor as Embla Carousel
participant Callback as onCenterChange
Home->>LoopCarousel: data: CarouselPlaylist[], onCenterChange 전달
LoopCarousel->>EmblaSensor: 캐러셀 초기화
User->>EmblaSensor: 슬라이드 이동
EmblaSensor->>LoopCarousel: select 이벤트 발생
LoopCarousel->>LoopCarousel: handleSelect() 실행<br/>activeIndex 업데이트
alt onCenterChange 콜백 제공됨
LoopCarousel->>Callback: onCenterChange({<br/>playlistId,<br/>playlistName<br/>})
Callback-->>Home: 중심 플레이리스트 정보 전달
end
LoopCarousel->>LoopCarousel: 스티커 렌더링<br/>(activeIndex 기반)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 변경 범위가 광범위하며, 새로운 기능 3개(좋아요, 탭 네비게이션, 캐러셀 리팩토링)를 포함하고 있고, 기존 타입 변경(isRepresentative→isPublic)으로 인한 영향 범위가 큽니다. 각 기능별로 API, 훅, UI 컴포넌트가 조화를 이루어야 하고, 플레이리스트 관련 타입 변경이 여러 파일에 흩어져 있어 일관성 확인이 필요합니다. Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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 @maylh, 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은 사용자에게 더욱 직관적이고 풍부한 경험을 제공하기 위해 홈페이지와 '나의 CD' 페이지의 UI를 대대적으로 개선했습니다. 특히, '좋아요' 기능을 도입하여 사용자가 선호하는 플레이리스트를 쉽게 관리하고 탐색할 수 있도록 하였으며, 이를 위한 백엔드 API 연동과 프론트엔드 상태 관리 로직을 구현했습니다. 전반적인 시각적 요소와 기능적 흐름이 사용자 중심적으로 재편되었습니다. 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
|
|
🎵 Storybook Link 🎵 |
There was a problem hiding this comment.
Code Review
이 PR은 홈페이지와 '나의 CD' 페이지의 UI를 대대적으로 개편하고, 좋아요 기능 및 관련 API를 연동하는 변경 사항을 담고 있습니다. 전반적으로 새로운 UI 구조가 잘 적용되었고, like 기능 관련 훅과 컴포넌트들이 FSD 아키텍처에 맞게 잘 추가되었습니다. 몇 가지 개선점과 버그 수정이 필요한 부분을 아래 리뷰 댓글로 남겼습니다. 특히 mycd/index.tsx에서 ActionBar 컴포넌트로 전달되는 props 값에 오류가 있어 수정이 필요해 보입니다. 또한, useLike 훅에서 좋아요 목록 데이터의 일관성을 유지하기 위한 쿼리 무효화 로직 추가를 제안합니다.
| <ActionBar | ||
| playlistId={centerPlaylist.playlistId ?? 0} | ||
| creatorId="currentPlaylist.creator.creatorId" | ||
| stickers={[]} | ||
| type="MY" | ||
| /> |
There was a problem hiding this comment.
ActionBar 컴포넌트에 전달되는 props에 몇 가지 오류가 있습니다.
creatorId가{currentPlaylist.creator.creatorId}대신 문자열 리터럴"currentPlaylist.creator.creatorId"로 전달되고 있습니다.stickers가 빈 배열[]로 하드코딩되어 있습니다.playlistDetail에서 가져온 스티커 데이터를 전달해야 합니다. (playlistDetail?.onlyCdResponse?.cdItems ?? [])
이 부분은 컴포넌트가 올바르게 동작하지 않게 만드는 버그이므로 수정이 필요합니다.1
<ActionBar
playlistId={centerPlaylist.playlistId ?? 0}
creatorId={currentPlaylist.creator.creatorId}
stickers={playlistDetail?.onlyCdResponse?.cdItems ?? []}
type="MY"
/>
Style Guide References
Footnotes
-
컴포넌트에 props를 전달할 때, 올바른 타입과 값을 전달해야 합니다. 문자열 리터럴을 변수 대신 사용하거나, 필요한 데이터를 전달하지 않으면 런타임 에러나 비정상적인 동작으로 이어질 수 있습니다. ↩
| onSuccess: () => { | ||
| setIsLiked(true) | ||
| queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] }) | ||
| }, |
There was a problem hiding this comment.
좋아요 액션이 성공했을 때, playlistDetail 쿼리만 무효화하고 있습니다. '좋아요한 CD' 목록의 데이터 일관성을 위해 myLikedCdList 쿼리도 함께 무효화하는 것이 좋습니다. useMyLikedCdList 훅의 queryKey가 ['myLikedCdList', sort]이므로, queryClient.invalidateQueries({ queryKey: ['myLikedCdList'] })를 추가하면 관련 목록이 모두 업데이트될 것입니다.1
onSuccess: () => {
setIsLiked(true)
queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] })
queryClient.invalidateQueries({ queryKey: ['myLikedCdList'] })
},Style Guide References
Footnotes
-
Tanstack Query를 사용하여 서버 상태를 관리할 때, 데이터 변경(mutation) 후 관련된 쿼리를 무효화하여 데이터 동기화를 유지하는 것이 좋습니다. '좋아요' 상태가 변경되면 '좋아요한 CD 목록'도 업데이트되어야 합니다. ↩
| onSuccess: () => { | ||
| setIsLiked(false) | ||
| queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] }) | ||
| }, |
There was a problem hiding this comment.
좋아요 취소 액션이 성공했을 때, playlistDetail 쿼리만 무효화하고 있습니다. '좋아요한 CD' 목록의 데이터 일관성을 위해 myLikedCdList 쿼리도 함께 무효화하는 것이 좋습니다. useMyLikedCdList 훅의 queryKey가 ['myLikedCdList', sort]이므로, queryClient.invalidateQueries({ queryKey: ['myLikedCdList'] })를 추가하면 관련 목록이 모두 업데이트될 것입니다.1
onSuccess: () => {
setIsLiked(false)
queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] })
queryClient.invalidateQueries({ queryKey: ['myLikedCdList'] })
},Style Guide References
Footnotes
-
Tanstack Query를 사용하여 서버 상태를 관리할 때, 데이터 변경(mutation) 후 관련된 쿼리를 무효화하여 데이터 동기화를 유지하는 것이 좋습니다. '좋아요' 상태가 변경되면 '좋아요한 CD 목록'도 업데이트되어야 합니다. ↩
| const CharacterBg = styled.img` | ||
| position: absolute; | ||
| right: -48px; | ||
| width: 285px; | ||
| object-fit: contain; | ||
| object-position: center; | ||
| ` |
There was a problem hiding this comment.
CharacterBg 컴포넌트에 position: absolute와 right: -48px가 적용되어 있습니다. 부모 컴포넌트인 TopSection이나 PageLayout에 overflow: hidden 속성이 없어 일부 화면 크기에서 가로 스크롤이 발생할 수 있습니다. 의도된 디자인이라면 괜찮지만, 아니라면 부모 요소에서 overflow를 제어하거나 다른 방식으로 위치를 조정하는 것을 고려해 보세요.1
Style Guide References
Footnotes
-
CSS 작성 시 의도치 않은 사이드 이펙트(예: overflow로 인한 레이아웃 깨짐)가 발생하지 않도록 주의해야 합니다.
position: absolute와 음수 좌표값 사용 시 부모 요소의overflow속성을 확인하는 것이 좋습니다. ↩
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/widgets/playlist/Playlist.tsx (1)
42-47: 클릭 가능한 카드에서 커서 포인터가 사라졌습니다.
Wrapper가 여전히 전체 카드를 클릭 처리하지만cursor: pointer가 제거되어 마우스 오버 시 클릭 가능 여부를 직관적으로 파악하기 어렵습니다.cursor: pointer를 복원해 주세요.src/pages/home/index.tsx (1)
142-203: 배경 이미지가 뷰포트 기준으로 절대 배치되어 레이아웃이 깨집니다.
CharacterBg는position: absolute인데 상위 컨테이너(FirstSection/TopSection)가position: relative가 아니라서, 이미지가 의도한 섹션이 아니라 페이지 전체 기준으로 고정됩니다.FirstSection등에position: relative를 추가해 배경이 해당 영역 안에서만 정렬되도록 해 주세요.
🧹 Nitpick comments (5)
src/entities/playlist/api/playlist.ts (1)
74-75: 쿼리 파라미터 처리 방식 일관성 개선을 권장합니다.현재 템플릿 리터럴로 쿼리스트링을 직접 연결하고 있습니다. 파일 내 다른 함수들(예:
getShufflePlaylists)처럼params옵션을 사용하는 것이 더 안전하고 일관성 있습니다.다음과 같이 리팩터링할 수 있습니다:
export const getMyLikedCdList = (sort: string) => { - return api.get<MyCdListResponse>(`/main/playlist/mypage/me/likes?sort=${sort}`) + return api.get<MyCdListResponse>('/main/playlist/mypage/me/likes', { params: { sort } }) }src/pages/myPage/ui/playlist/index.tsx (1)
55-76: 코드 중복 해소 권장TODO 주석에서 언급한 대로
copyToClipboard함수가 여러 곳에서 중복 사용되고 있습니다. 이 함수를@/shared/lib/clipboard.ts같은 공통 유틸리티로 추출하여 재사용성을 높이는 것을 권장합니다.예시:
// src/shared/lib/clipboard.ts export const copyToClipboard = async (text: string): Promise<void> => { if (navigator.clipboard?.writeText) { return navigator.clipboard.writeText(text) } // 사파리 or 모바일 브라우저 fallback const textarea = document.createElement('textarea') textarea.value = text textarea.style.position = 'fixed' textarea.style.opacity = '0' document.body.appendChild(textarea) textarea.focus() textarea.select() try { document.execCommand('copy') } catch (e) { console.error(e) } document.body.removeChild(textarea) }src/features/like/ui/LikeButton.tsx (1)
22-55: 접근성 개선 권장현재 구현은 마우스 클릭만 처리하고 있습니다. 키보드 접근성을 위해 다음 개선을 권장합니다:
SvgButton이 이미 버튼 역할을 하므로 문제없지만,Wrapper가div인 경우 키보드 포커스가 불가능합니다.type === 'MY'일 때 전체가 버튼처럼 동작해야 한다면,Wrapper를button요소로 변경하거나 적절한 ARIA 속성을 추가해야 합니다.예시:
- return ( - <Wrapper $opacity={opacity} $isMy={type === 'MY'}> - <SvgButton - icon={Icon} - onClick={handleClick} + return type === 'MY' ? ( + <ButtonWrapper $opacity={opacity} onClick={handleClick}> + <Icon width={size} height={size} fill={...} stroke={...} /> + <p>좋아요</p> + </ButtonWrapper> + ) : ( + <Wrapper $opacity={opacity}> + <SvgButton + icon={Icon} + onClick={handleClick} ... /> - {type === 'MY' && <p>좋아요</p>} - </Wrapper> + </Wrapper> )src/pages/mycd/ui/HeaderTab.tsx (1)
17-31: 키보드 네비게이션 개선 권장탭 컴포넌트의 기본 구현은 좋으나, 키보드 접근성을 위해 다음 개선을 권장합니다:
- 탭 간 화살표 키 네비게이션 (ArrowLeft/ArrowRight)
- 적절한 ARIA 속성 (
role="tablist",role="tab",aria-selected)예시:
<TabContainer role="tablist"> {TABS.map((tab) => ( <TabButton key={tab.key} role="tab" aria-selected={selectedTab === tab.key} $active={selectedTab === tab.key} onClick={() => onSelect(tab.key)} onKeyDown={(e) => { if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { const currentIndex = TABS.findIndex((t) => t.key === selectedTab) const nextIndex = e.key === 'ArrowLeft' ? (currentIndex - 1 + TABS.length) % TABS.length : (currentIndex + 1) % TABS.length onSelect(TABS[nextIndex].key) } }} > {tab.label} </TabButton> ))} </TabContainer>src/pages/home/ui/LoopCarousel.tsx (1)
52-59: React key를 인덱스로 쓰면 리스트 동기화가 깨집니다.데이터 재정렬/탭 전환 시 슬라이드가 잘못 매핑될 수 있으니 고유한
playlistId로 교체해 주세요.- {data.map((slide, index: Key) => ( - <EmblaSlide key={index}> - <Slide $active={activeIndex === index}> + {data.map((slide) => ( + <EmblaSlide key={slide.playlistId}> + <Slide $active={activeIndex === data.findIndex(({ playlistId }) => playlistId === slide.playlistId)}>필요하다면
$active계산은 별도 변수로 캐싱해도 됩니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (6)
src/assets/icons/icn_cta_arrow.svgis excluded by!**/*.svgsrc/assets/icons/icn_like_stroke.svgis excluded by!**/*.svgsrc/assets/icons/icn_next.svgis excluded by!**/*.svgsrc/assets/icons/icn_prev.svgis excluded by!**/*.svgsrc/assets/icons/icn_search.svgis excluded by!**/*.svgsrc/assets/images/img_character_home.pngis excluded by!**/*.png
📒 Files selected for processing (33)
src/assets/icons/index.ts(1 hunks)src/assets/images/index.ts(1 hunks)src/entities/playlist/api/playlist.ts(1 hunks)src/entities/playlist/model/useMyPlaylist.ts(2 hunks)src/entities/playlist/model/usePlaylists.ts(1 hunks)src/entities/playlist/types/playlist.ts(1 hunks)src/features/like/api/like.ts(1 hunks)src/features/like/index.ts(1 hunks)src/features/like/model/useLike.ts(1 hunks)src/features/like/ui/LikeButton.tsx(1 hunks)src/features/share/ui/ShareButton.tsx(3 hunks)src/pages/home/config/messages.ts(1 hunks)src/pages/home/index.tsx(5 hunks)src/pages/home/ui/LoopCarousel.tsx(1 hunks)src/pages/myPage/ui/components/CdGrid.tsx(1 hunks)src/pages/myPage/ui/customize/step1/index.tsx(1 hunks)src/pages/myPage/ui/playlist/index.tsx(1 hunks)src/pages/mycd/index.tsx(5 hunks)src/pages/mycd/ui/HeaderTab.tsx(1 hunks)src/pages/mycd/ui/index.ts(1 hunks)src/shared/styles/mixins.ts(1 hunks)src/shared/ui/Cd.tsx(1 hunks)src/shared/ui/Header.tsx(0 hunks)src/shared/ui/SvgButton.tsx(1 hunks)src/stories/LoopCarousel.stories.tsx(2 hunks)src/widgets/chat/ChatButton.tsx(2 hunks)src/widgets/playlist/ActionBar.tsx(1 hunks)src/widgets/playlist/ControlBar.tsx(1 hunks)src/widgets/playlist/Playlist.tsx(3 hunks)src/widgets/playlist/PlaylistLayout.tsx(2 hunks)src/widgets/playlist/ProgressBar.tsx(0 hunks)src/widgets/playlist/YoutubePlayer.tsx(1 hunks)src/widgets/playlist/index.ts(1 hunks)
💤 Files with no reviewable changes (2)
- src/shared/ui/Header.tsx
- src/widgets/playlist/ProgressBar.tsx
🧰 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/like/index.tssrc/features/like/api/like.tssrc/pages/mycd/ui/index.tssrc/shared/ui/SvgButton.tsxsrc/widgets/playlist/index.tssrc/entities/playlist/api/playlist.tssrc/pages/mycd/ui/HeaderTab.tsxsrc/widgets/playlist/ControlBar.tsxsrc/shared/styles/mixins.tssrc/entities/playlist/model/usePlaylists.tssrc/pages/myPage/ui/playlist/index.tsxsrc/pages/myPage/ui/components/CdGrid.tsxsrc/entities/playlist/model/useMyPlaylist.tssrc/pages/myPage/ui/customize/step1/index.tsxsrc/widgets/playlist/ActionBar.tsxsrc/pages/home/index.tsxsrc/assets/icons/index.tssrc/features/share/ui/ShareButton.tsxsrc/assets/images/index.tssrc/features/like/ui/LikeButton.tsxsrc/features/like/model/useLike.tssrc/stories/LoopCarousel.stories.tsxsrc/pages/home/config/messages.tssrc/pages/mycd/index.tsxsrc/entities/playlist/types/playlist.tssrc/shared/ui/Cd.tsxsrc/widgets/playlist/Playlist.tsxsrc/widgets/playlist/YoutubePlayer.tsxsrc/widgets/playlist/PlaylistLayout.tsxsrc/pages/home/ui/LoopCarousel.tsxsrc/widgets/chat/ChatButton.tsx
🧬 Code graph analysis (17)
src/features/like/api/like.ts (1)
src/shared/api/httpClient.ts (1)
api(5-19)
src/entities/playlist/api/playlist.ts (2)
src/shared/api/httpClient.ts (1)
api(5-19)src/entities/playlist/types/playlist.ts (1)
MyCdListResponse(9-9)
src/pages/mycd/ui/HeaderTab.tsx (2)
src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)src/shared/styles/theme.ts (1)
theme(3-122)
src/widgets/playlist/ControlBar.tsx (2)
src/shared/styles/theme.ts (1)
theme(3-122)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)
src/shared/styles/mixins.ts (1)
src/shared/styles/theme.ts (1)
theme(3-122)
src/entities/playlist/model/usePlaylists.ts (1)
src/entities/playlist/api/playlist.ts (1)
getPlaylistDetail(49-51)
src/entities/playlist/model/useMyPlaylist.ts (1)
src/entities/playlist/api/playlist.ts (1)
getMyLikedCdList(74-76)
src/widgets/playlist/ActionBar.tsx (2)
src/entities/playlist/types/playlist.ts (1)
CdCustomData(23-34)src/shared/styles/mixins.ts (3)
flexRowCenter(3-8)flexColCenter(10-15)myCdButton(17-25)
src/pages/home/index.tsx (3)
src/pages/home/config/messages.ts (1)
TITLE_TEXT(6-9)src/shared/styles/theme.ts (1)
theme(3-122)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)
src/features/share/ui/ShareButton.tsx (2)
src/entities/playlist/types/playlist.ts (1)
CdCustomData(23-34)src/shared/styles/mixins.ts (2)
flexRowCenter(3-8)myCdButton(17-25)
src/features/like/ui/LikeButton.tsx (2)
src/shared/styles/theme.ts (1)
theme(3-122)src/shared/styles/mixins.ts (2)
flexRowCenter(3-8)myCdButton(17-25)
src/features/like/model/useLike.ts (2)
src/features/auth/store/authStore.ts (1)
useAuthStore(7-52)src/features/like/api/like.ts (3)
postLike(3-5)deleteLike(7-9)getLikeStatus(11-13)
src/stories/LoopCarousel.stories.tsx (1)
src/entities/playlist/types/playlist.ts (1)
CdCustomData(23-34)
src/pages/mycd/index.tsx (6)
src/features/auth/store/authStore.ts (1)
useAuthStore(7-52)src/shared/lib/useDevice.ts (1)
useDevice(14-17)src/entities/playlist/model/useMyPlaylist.ts (2)
useMyCdList(14-20)useMyLikedCdList(80-86)src/entities/playlist/model/usePlaylists.ts (1)
usePlaylistDetail(45-51)src/features/chat/model/sendMessage.ts (1)
useChatSocket(17-80)src/shared/styles/theme.ts (1)
theme(3-122)
src/widgets/playlist/Playlist.tsx (1)
src/entities/playlist/types/playlist.ts (1)
CdCustomData(23-34)
src/pages/home/ui/LoopCarousel.tsx (2)
src/entities/playlist/types/playlist.ts (1)
CdCustomData(23-34)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)
src/widgets/chat/ChatButton.tsx (1)
src/shared/styles/mixins.ts (2)
flexRowCenter(3-8)myCdButton(17-25)
🪛 GitHub Check: Build and Lint
src/pages/home/ui/LoopCarousel.tsx
[warning] 53-53:
Do not use Array index in keys
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Chromatic
🔇 Additional comments (21)
src/pages/myPage/ui/components/CdGrid.tsx (1)
42-42: 필드명 변경이 올바르게 적용되었습니다.
isRepresentative에서isPublic으로의 필드명 변경이 타입 정의와 일관성 있게 적용되었습니다. Optional chaining을 사용하여 안전하게 처리하고 있습니다.src/widgets/playlist/index.ts (1)
11-11: 새로운 ControlBar 컴포넌트 export가 추가되었습니다.모듈 구조에 맞게 올바르게 추가되었습니다.
src/shared/ui/Cd.tsx (1)
100-100: 캐러셀 variant 크기가 증가되었습니다.
carouselvariant의 크기가 180에서 260으로 약 44% 증가했습니다. 이 변경사항이 캐러셀이 사용되는 모든 화면에서 레이아웃에 문제가 없는지 확인해주세요.src/pages/mycd/ui/index.ts (1)
1-1: HeaderTab 컴포넌트가 올바르게 export되었습니다.새로운 탭 기능을 위한 컴포넌트가 모듈 구조에 맞게 추가되었습니다.
src/pages/home/config/messages.ts (1)
7-7: 줄바꿈 위치가 조정되었습니다.줄바꿈 위치가 변경되고
\n뒤에 공백이 추가되었습니다. 줄바꿈 후의 공백(오늘의)이 의도된 들여쓰기인지 확인해주세요. 일반적으로 줄바꿈 후에는 공백을 넣지 않습니다.src/entities/playlist/api/playlist.ts (1)
73-76: 좋아요한 플레이리스트 조회 API가 추가되었습니다.새로운 API 엔드포인트가 올바르게 추가되었습니다.
src/features/share/ui/ShareButton.tsx (3)
18-21: type prop이 추가되어 버튼 스타일을 구분할 수 있습니다.'MY'와 'DISCOVER' 타입에 따라 다른 UI를 렌더링하도록 개선되었습니다. 기본값이 'DISCOVER'로 설정되어 있어 기존 사용처에서는 변경 없이 동작합니다.
87-90: 조건부 렌더링이 올바르게 구현되었습니다.type prop에 따라 버튼 크기와 레이블 표시를 적절히 제어하고 있습니다.
148-151: ButtonWrapper 스타일 컴포넌트가 적절히 구현되었습니다.조건부로
myCdButtonmixin을 적용하는 구조가 명확하고 재사용 가능합니다.src/pages/myPage/ui/customize/step1/index.tsx (2)
40-40: 필드명 변경이 올바르게 반영되었습니다.
isRepresentative에서isPublic으로 변경된 데이터 모델을 올바르게 사용하고 있습니다.
83-83: API 페이로드 필드명 일관성 확인이 필요합니다.프론트엔드에서는
isPublic으로 변경되었지만, API 페이로드에는 여전히isRepresentative를 사용하고 있습니다. 백엔드 API의 필드명 변경 계획이 있는지 확인하고, 향후 일관성을 위해 백엔드와 동일한 필드명(isPublic)을 사용하도록 조율해주세요.src/pages/myPage/ui/playlist/index.tsx (1)
126-142: LGTM!
isRepresentative에서isPublic으로의 필드명 변경이 타입 정의와 일관되게 적용되었습니다. 조건부 렌더링 로직도 올바르게 동작합니다.src/entities/playlist/types/playlist.ts (2)
6-6: LGTM!
MyCdInfo인터페이스의 필드명 변경이 올바르게 적용되었습니다. 이 변경사항은MyPlaylistResponse등 다른 타입들에도 자동으로 반영됩니다.
115-124: 검증 결과: 리뷰 코멘트는 부정확합니다.
MyRepresentResponse의isRepresentative필드와MyCdInfo의isPublic필드는 서로 다른 개념을 나타내기 때문에 일관성 문제가 아닙니다:
MyCdInfo(isPublic): CD 플레이리스트의 공개/비공개 여부MyRepresentResponse(isRepresentative): 대표 플레이리스트 여부를 나타내는/main/playlist/mypage/me/representative엔드포인트 응답두 타입은 서로 다른 API 엔드포인트에서 반환되며, 각각 다른 목적의 필드입니다. 또한
PlaylistDetail에서 사용하는representative필드와도 구분되어 있습니다.코드 상태는 정상입니다.
src/features/like/index.ts (1)
1-3: LGTM!Like 기능의 공개 API를 명확하게 정의한 barrel 파일입니다. 구조가 깔끔하고 표준적인 패턴을 따릅니다.
src/entities/playlist/model/useMyPlaylist.ts (1)
80-86: LGTM!새로운
useMyLikedCdList훅이 기존 패턴을 잘 따르고 있으며,refetchOnMount: 'always'설정으로 항상 최신 데이터를 보장합니다.src/features/like/ui/LikeButton.tsx (1)
31-32: LGTM!
ICON_STYLE객체를 통한 타입별 아이콘 스타일 관리가 명확하고, opacity 로직도 적절하게 구현되었습니다.src/entities/playlist/model/usePlaylists.ts (1)
45-51: LGTM!
playlistId를 nullable로 변경하고enabled옵션을 추가한 것은 좋은 방어적 프로그래밍입니다.playlistId가 null일 때 불필요한 API 호출을 방지합니다.타입 단언(
as number)은enabled조건에서 이미 null 체크를 했기 때문에 안전합니다.src/pages/mycd/ui/HeaderTab.tsx (1)
35-46: LGTM!스타일링이 테마 시스템을 잘 활용하고 있으며, 활성 상태에 따른 색상과 폰트 웨이트 변경이 적절합니다.
src/widgets/playlist/PlaylistLayout.tsx (2)
82-88: LGTM!
ActionBar에type="DISCOVER"prop을 추가하여 컨텍스트별 UI를 제공하는 것이 적절합니다. props 구조가 명확해졌습니다.
164-168: LGTM!
padding-top: 32px추가로 진행 바의 레이아웃 간격이 개선되었습니다.
| import { useState } from 'react' | ||
| import { useNavigate } from 'react-router-dom' | ||
|
|
||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' | ||
|
|
||
| import { useAuthStore } from '@/features/auth/store/authStore' | ||
| import { postLike, deleteLike, getLikeStatus } from '@/features/like/api/like' | ||
|
|
||
| const useLike = (playlistId: number, initialIsLiked: boolean) => { | ||
| const queryClient = useQueryClient() | ||
| const [isLiked, setIsLiked] = useState(initialIsLiked) | ||
| const navigate = useNavigate() | ||
| const { isLogin } = useAuthStore() | ||
|
|
||
| const likeMutation = useMutation({ | ||
| mutationFn: (playlistId: number) => postLike(playlistId), | ||
| onSuccess: () => { | ||
| setIsLiked(true) | ||
| queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] }) | ||
| }, | ||
| }) | ||
|
|
||
| const unlikeMutation = useMutation({ | ||
| mutationFn: (playlistId: number) => deleteLike(playlistId), | ||
| onSuccess: () => { | ||
| setIsLiked(false) | ||
| queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] }) | ||
| }, | ||
| }) | ||
|
|
||
| const toggleLike = () => { | ||
| if (!isLogin) { | ||
| navigate('/login') | ||
| return | ||
| } | ||
|
|
||
| if (likeMutation.isPending || unlikeMutation.isPending) return | ||
|
|
||
| if (isLiked) { | ||
| unlikeMutation.mutate(playlistId) | ||
| } else { | ||
| likeMutation.mutate(playlistId) | ||
| } | ||
| } |
There was a problem hiding this comment.
플레이리스트 전환 시 좋아요 상태가 갱신되도록 보완이 필요합니다.
현재 로컬 상태를 initialIsLiked로 한 번만 초기화해서, 같은 컴포넌트 인스턴스에서 다른 플레이리스트로 이동하면 이전 곡의 좋아요 상태가 그대로 남습니다. 특히 라우트 파라미터만 바뀌고 컴포넌트가 유지되는 케이스에서 바로 노출됩니다. 플레이리스트 ID나 초기값이 바뀔 때 상태를 다시 맞춰 주세요.
적용 예시는 아래와 같습니다:
-import { useState } from 'react'
+import { useEffect, useState } from 'react'
...
const [isLiked, setIsLiked] = useState(initialIsLiked)
...
+ useEffect(() => {
+ setIsLiked(initialIsLiked)
+ }, [initialIsLiked, playlistId])🤖 Prompt for AI Agents
In src/features/like/model/useLike.ts around lines 1-44, the hook only
initializes isLiked from initialIsLiked once so when the component stays mounted
and playlistId or initialIsLiked change the local state is stale; add an effect
that watches playlistId and initialIsLiked and calls setIsLiked(initialIsLiked)
(i.e. useEffect(() => setIsLiked(initialIsLiked), [playlistId, initialIsLiked]))
so the local like state is resynchronized whenever the playlist or its initial
status changes.
| onSuccess: () => { | ||
| setIsLiked(true) | ||
| queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] }) | ||
| }, | ||
| }) | ||
|
|
||
| const unlikeMutation = useMutation({ | ||
| mutationFn: (playlistId: number) => deleteLike(playlistId), | ||
| onSuccess: () => { | ||
| setIsLiked(false) | ||
| queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] }) | ||
| }, |
There was a problem hiding this comment.
좋아요 상태 쿼리도 함께 무효화해야 합니다.
useLikeStatus 훅이 ['likeStatus', playlistId] 키에 의존하는데, 토글 후 이 쿼리를 무효화하지 않아 좋아요 상태가 갱신되지 않습니다. 동일 화면에서 상태 표시가 어긋나므로, 성공 콜백에서 likeStatus 쿼리도 무효화해 주세요.
아래와 같이 수정할 수 있습니다:
onSuccess: () => {
setIsLiked(true)
queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] })
+ queryClient.invalidateQueries({ queryKey: ['likeStatus', playlistId] })
},
...
onSuccess: () => {
setIsLiked(false)
queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] })
+ queryClient.invalidateQueries({ queryKey: ['likeStatus', playlistId] })
},🤖 Prompt for AI Agents
In src/features/like/model/useLike.ts around lines 17 to 28, the onSuccess
handlers for the like and unlike mutations invalidate only ['playlistDetail',
playlistId] but not the like status query, so the UI's like state (which depends
on ['likeStatus', playlistId]) doesn't update; update both onSuccess callbacks
to also call queryClient.invalidateQueries({ queryKey: ['likeStatus',
playlistId] }) (in addition to the existing playlistDetail invalidation) so the
likeStatus hook is refreshed after a toggle.
| creator: { | ||
| creatorId: userInfo.userId, | ||
| creatorNickname: userInfo.username, | ||
| }, | ||
| playlistId: playlistData.playlistId, | ||
| playlistName: playlistData.playlistName, | ||
| genre: playlistData.genre, | ||
| songs: playlistData.songs, | ||
| playlistId: playlistDetail.playlistId, | ||
| playlistName: playlistDetail.playlistName, | ||
| genre: playlistDetail.genre, | ||
| songs: playlistDetail.songs, | ||
| representative: false, | ||
| cdItems: playlistData.onlyCdResponse?.cdItems || [], | ||
| cdItems: playlistDetail.onlyCdResponse?.cdItems || [], | ||
| } |
There was a problem hiding this comment.
좋아요 탭에서 재생 시 생성자 정보가 잘못 주입됩니다.
LIKE 탭에서 타인의 플레이리스트를 재생하면 convertedPlaylist.creator가 항상 로그인 유저로 고정돼 ActionBar/권한 판단이 깨집니다. playlistDetail이 내려주는 실제 생성자 정보를 그대로 사용하도록 교체해 주세요. 필드명은 API 응답에 맞춰 조정 바랍니다.
- const convertedPlaylist = {
- creator: {
- creatorId: userInfo.userId,
- creatorNickname: userInfo.username,
- },
+ const convertedPlaylist = {
+ creator: {
+ creatorId: playlistDetail.creatorId,
+ creatorNickname: playlistDetail.creatorNickname,
+ },📝 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.
| creator: { | |
| creatorId: userInfo.userId, | |
| creatorNickname: userInfo.username, | |
| }, | |
| playlistId: playlistData.playlistId, | |
| playlistName: playlistData.playlistName, | |
| genre: playlistData.genre, | |
| songs: playlistData.songs, | |
| playlistId: playlistDetail.playlistId, | |
| playlistName: playlistDetail.playlistName, | |
| genre: playlistDetail.genre, | |
| songs: playlistDetail.songs, | |
| representative: false, | |
| cdItems: playlistData.onlyCdResponse?.cdItems || [], | |
| cdItems: playlistDetail.onlyCdResponse?.cdItems || [], | |
| } | |
| creator: { | |
| creatorId: playlistDetail.creatorId, | |
| creatorNickname: playlistDetail.creatorNickname, | |
| }, | |
| playlistId: playlistDetail.playlistId, | |
| playlistName: playlistDetail.playlistName, | |
| genre: playlistDetail.genre, | |
| songs: playlistDetail.songs, | |
| representative: false, | |
| cdItems: playlistDetail.onlyCdResponse?.cdItems || [], |
🤖 Prompt for AI Agents
In src/pages/mycd/index.tsx around lines 88 to 98, convertedPlaylist is being
assigned creator info from the current logged-in user (userInfo), which breaks
permission/UI logic when playing others' playlists from the LIKE tab; replace
the hardcoded creator block with the creator object coming from playlistDetail
(use the exact API response fields, e.g. playlistDetail.creator.creatorId /
playlistDetail.creator.creatorNickname or the API's naming) so
convertedPlaylist.creator mirrors the playlist owner; ensure you don't overwrite
other creator subfields and fallback only if playlistDetail.creator is missing.
| <ActionBar | ||
| playlistId={centerPlaylist.playlistId ?? 0} | ||
| creatorId="currentPlaylist.creator.creatorId" | ||
| stickers={[]} | ||
| type="MY" | ||
| /> |
There was a problem hiding this comment.
ActionBar에 전달되는 creatorId가 문자열 리터럴입니다.
현재 "currentPlaylist.creator.creatorId"라는 문자열이 넘어가 실제 생성자 정보가 전달되지 않아 소유자 판단/액션이 전부 동작하지 않습니다. 실값을 넘기도록 수정이 필요합니다.
<ActionBar
playlistId={centerPlaylist.playlistId ?? 0}
- creatorId="currentPlaylist.creator.creatorId"
+ creatorId={currentPlaylist.creator.creatorId}
stickers={[]}
type="MY"
/>📝 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.
| <ActionBar | |
| playlistId={centerPlaylist.playlistId ?? 0} | |
| creatorId="currentPlaylist.creator.creatorId" | |
| stickers={[]} | |
| type="MY" | |
| /> | |
| <ActionBar | |
| playlistId={centerPlaylist.playlistId ?? 0} | |
| creatorId={currentPlaylist.creator.creatorId} | |
| stickers={[]} | |
| type="MY" | |
| /> |
🤖 Prompt for AI Agents
In src/pages/mycd/index.tsx around lines 158 to 163, the creatorId prop is being
passed as the literal string "currentPlaylist.creator.creatorId" instead of the
actual value; replace that string with the real property (e.g.
centerPlaylist?.creator?.creatorId) and provide a safe fallback (empty string or
undefined) to avoid runtime errors — for example change
creatorId="currentPlaylist.creator.creatorId" to
creatorId={centerPlaylist?.creator?.creatorId ?? ''} (or another appropriate
default).
| {type === 'MY' && <p>28</p>} | ||
| </ButtonWrapper> |
There was a problem hiding this comment.
하드코딩된 채팅 개수로 인해 잘못된 정보가 노출됩니다.
type === 'MY'일 때 항상 28을 노출하고 있는데, 실제 채팅 수와 무관하게 고정값이 표시되어 사용자에게 오정보를 주게 됩니다. 최소한 API 연동 전까지는 표시를 숨기거나 명확한 플레이스홀더(예: '--')로 처리해 주세요.
🤖 Prompt for AI Agents
In src/widgets/chat/ChatButton.tsx around lines 28-29, the button renders a
hardcoded "28" when type === 'MY', which leaks incorrect info; remove the fixed
literal and instead render the real chat count if available (use a prop like
chatCount or state) and fall back to a placeholder such as '--' or render
nothing while the API is not connected; update the conditional to check for a
defined numeric count (e.g., typeof chatCount === 'number') and render
chatCount, otherwise render the placeholder or hide the element.
| <LikeButton playlistId={playlistId} isLiked={false} type={type} /> | ||
| <ChatButton roomId={playlistId} creatorId={creatorId} type={type} /> |
There was a problem hiding this comment.
좋아요 초기 상태가 항상 해제되어 실제 상태와 불일치합니다.
LikeButton에 isLiked={false}를 고정으로 넘기면 사용자가 이미 좋아요를 누른 플레이리스트도 항상 해제된 상태로 보이게 됩니다. API가 아직 준비되지 않았다면 표시를 잠시 숨기거나, 적어도 TODO 주석과 함께 실제 데이터 연동 이전에는 true/false를 구분해 받을 수 있도록 대비해 주세요.
🤖 Prompt for AI Agents
In src/widgets/playlist/ActionBar.tsx around lines 33-34 the LikeButton is
hardcoded with isLiked={false}, causing UI to always show unliked even when the
playlist is liked; remove the hardcoded value and instead pass a real boolean
from props/state (e.g., a prop like initialIsLiked or derived from playlist
data) or conditionally hide the LikeButton until the API provides the value, and
add a TODO comment documenting the temporary behavior and where to wire the API
response when ready.
hansololiviakim
left a comment
There was a problem hiding this comment.
감사합니다! 백엔드 API 리팩토링 어제 밤에 완료되었다고 합니다ㅎㅎ 참고차 공유드려요!
🛰️ 관련 이슈
✨ 주요 변경 사항
🔍 테스트 방법 / 체크리스트
🗯️ PR 포인트
🚀 알게된 점
📖 참고 자료 (선택)
Summary by CodeRabbit
Release Notes
New Features
UI/UX Improvements