Skip to content

[feat] 홈페이지, 나의 CD UI 변경사항 반영#112

Merged
maylh merged 14 commits intodevelopfrom
feat/#111/home-ui
Oct 21, 2025
Merged

[feat] 홈페이지, 나의 CD UI 변경사항 반영#112
maylh merged 14 commits intodevelopfrom
feat/#111/home-ui

Conversation

@maylh
Copy link
Collaborator

@maylh maylh commented Oct 16, 2025

🛰️ 관련 이슈


✨ 주요 변경 사항

  • 홈페이지, 나의 CD UI 변경사항 반영
  • 좋아요 한 플레이리스트 조회 API 연결

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

  • 없음

🗯️ PR 포인트

  • MyCdResponse 인터페이스에서 기존 isRepresentative 필드를 isPublic으로 변경했습니다. 기존 코드에서 사용 중이던 isRepresentative는 우선 모두 isPublic으로 대체해 두었습니다 a6346b8
  • 아래는 추후 업데이트 예정인 부분입니다 ! 따로 이슈 파서 진행하겠습니다
    • 좋아요 : 여러 플레이리스트에 대한 좋아요 여부 받아오는 API 작업 완료되면 실제 값으로 반영 예쩡
    • 채팅 총 갯수: API 수정 후 반영 예정
    • 홈페이지 상단부 캐러셀 문구

🚀 알게된 점


📖 참고 자료 (선택)

Summary by CodeRabbit

Release Notes

  • New Features

    • Like/favorite playlists with dedicated like button and status tracking
    • MyCD page with tabs to separate "My CD" and "Liked CD" views
    • Home page call-to-action button and decorative character background
    • Playlist action bar with like, chat, and share buttons
    • New media player control bar with play, pause, and skip controls
  • UI/UX Improvements

    • Improved home page layout with better visual hierarchy
    • Enhanced MyCD page navigation with tab-based organization
    • Updated header styling and button components
    • Better responsive design for mobile and desktop views

@coderabbitai
Copy link

coderabbitai bot commented Oct 16, 2025

Walkthrough

이 PR은 홈페이지와 나의CD 페이지의 UI/UX 개선을 반영한 광범위한 변경사항을 포함한다. 좋아요 기능 추가(API, 훅, 컴포넌트), 나의CD 페이지에 탭 네비게이션 구현, 홈페이지 캐러셀 컴포넌트 리팩토링, 플레이리스트 타입 변경(isRepresentative→isPublic), 새로운 아이콘/이미지 export 추가를 포함한다.

Changes

Cohort / File(s) 변경 요약
아이콘 및 이미지 asset export
src/assets/icons/index.ts, src/assets/images/index.ts
CtaArrow, LikeStroke 아이콘과 HomeCharacter 이미지를 새로 export
플레이리스트 타입 및 API 확장
src/entities/playlist/types/playlist.ts, src/entities/playlist/api/playlist.ts, src/entities/playlist/model/useMyPlaylist.ts
MyCdInfo 인터페이스의 isRepresentative를 isPublic으로 변경하고, 좋아요한 CD 목록 조회 API(getMyLikedCdList) 및 훅(useMyLikedCdList) 추가
플레이리스트 상세 조회 훅 개선
src/entities/playlist/model/usePlaylists.ts
usePlaylistDetail이 null을 허용하도록 확장하고 enabled 조건 추가
좋아요 기능 구현
src/features/like/api/like.ts, src/features/like/model/useLike.ts, src/features/like/ui/LikeButton.tsx, src/features/like/index.ts
좋아요 API(postLike, deleteLike, getLikeStatus), 좋아요 상태 관리 훅(useLike, useLikeStatus), 좋아요 버튼 UI 컴포넌트 신규 구현
홈페이지 UI 개선
src/pages/home/index.tsx, src/pages/home/ui/LoopCarousel.tsx, src/pages/home/config/messages.ts
캐러셀 컴포넌트 리팩토링(CarouselPlaylist 타입 도입, onCenterChange 콜백 추가), CTA 버튼 및 배경 이미지 추가, 멤버 인사말 문구 개선
나의CD 페이지 탭 네비게이션
src/pages/mycd/index.tsx, src/pages/mycd/ui/HeaderTab.tsx, src/pages/mycd/ui/index.ts
탭 기반 "나의 CD"/"좋아요한 CD" 전환, 캐러셀 중심 추적, 상세 데이터 페칭 로직 개선
플레이리스트 레이아웃 및 제어 컴포넌트
src/widgets/playlist/ActionBar.tsx, src/widgets/playlist/ControlBar.tsx, src/widgets/playlist/PlaylistLayout.tsx, src/widgets/playlist/Playlist.tsx, src/widgets/playlist/index.ts
ActionBar 타입별 렌더링 개선, ControlBar 컴포넌트 신규 구현, PlaylistLayout 팔로우 기능 제거, Playlist에 isLiked prop 추가
공유 버튼 개선
src/features/share/ui/ShareButton.tsx, src/widgets/chat/ChatButton.tsx
타입(MY/DISCOVER) 기반 렌더링 조건 추가
마이페이지 플레이리스트 표시
src/pages/myPage/ui/components/CdGrid.tsx, src/pages/myPage/ui/customize/step1/index.tsx, src/pages/myPage/ui/playlist/index.tsx
isRepresentative를 isPublic으로 변경하여 배지/공유 버튼/대표 표시 조건 반영
UI 컴포넌트 업데이트
src/shared/ui/Cd.tsx, src/shared/ui/Header.tsx, src/shared/ui/SvgButton.tsx, src/shared/styles/mixins.ts
캐러셀 Cd 크기 조정(180→260), Header 배경색 제거, SvgButton 클릭 핸들러 타입 표준화, myCdButton 스타일 믹신 추가
스토리북 및 기타
src/stories/LoopCarousel.stories.tsx, src/widgets/playlist/YoutubePlayer.tsx, src/widgets/playlist/ProgressBar.tsx
LoopCarousel 스토리 목업 및 prop 업데이트, YouTubePlayer useEffect 제거, ProgressBar 패딩 조정

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
Loading
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 렌더링
Loading
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 기반)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

변경 범위가 광범위하며, 새로운 기능 3개(좋아요, 탭 네비게이션, 캐러셀 리팩토링)를 포함하고 있고, 기존 타입 변경(isRepresentative→isPublic)으로 인한 영향 범위가 큽니다. 각 기능별로 API, 훅, UI 컴포넌트가 조화를 이루어야 하고, 플레이리스트 관련 타입 변경이 여러 파일에 흩어져 있어 일관성 확인이 필요합니다.

Possibly related PRs

Suggested labels

HIGH, feat, UI, home, mycd

Suggested reviewers

  • hansololiviakim

Poem

🐰 새 기능들이 콩닥콩닥,
좋아요 버튼은 쿵닥쿵닥!
탭을 누르면 화면 팅팅,
캐러셀 춤을 추며 링링~
토끼가 축하해 이 변화를! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "[feat] 홈페이지, 나의 CD UI 변경사항 반영"은 간결하고 명확하며, 변경사항을 정확하게 요약하고 있습니다. 제목은 홈페이지와 나의 CD 페이지의 UI 변경이라는 주요 내용을 명시하고 있으며, 팀원이 변경사항을 빠르게 파악할 수 있는 수준의 구체성을 갖추고 있습니다. 불필요한 이모지나 자세한 파일 목록 없이 간단하면서도 변경사항의 핵심을 전달합니다.
Linked Issues Check ✅ Passed 연결된 이슈 #111의 모든 작업 목표가 PR에서 충족되었습니다. 상단부 캐러셀 디자인 변경사항은 LoopCarousel 리팩토링, TopSection 추가, 크기 조정 등으로 반영되었고, 좋아요 기능은 postLike/deleteLike/getLikeStatus API와 useLike/useLikeStatus 훅, LikeButton 컴포넌트로 완전히 구현되었으며, 나의CD UI 변경사항은 탭 기반 네비게이션 추가, HeaderTab 컴포넌트, isRepresentative → isPublic 필드 변경으로 반영되었습니다.
Out of Scope Changes Check ✅ Passed 모든 코드 변경사항이 이슈 #111의 범위 내에 있습니다. 아이콘 및 이미지 내보내기 추가, 좋아요 기능 구현, 홈페이지 UI 리팩토링, 나의 CD 탭 네비게이션, isRepresentative → isPublic 필드 변경, SvgButton 타입 개선 등 모든 변경사항은 지정된 UI 변경사항 반영 및 좋아요 기능 연결이라는 이슈 목표와 직접 관련이 있습니다. 범위를 벗어난 변경사항은 없습니다.
Description Check ✅ Passed PR 설명은 저장소의 템플릿 구조를 따르고 있으며 필수 섹션들이 대부분 채워져 있습니다. 관련 이슈(close #111), 주요 변경 사항, 테스트 방법, PR 포인트가 모두 명시되어 있습니다. 특히 PR 포인트 섹션에서 MyCdResponse 인터페이스의 isRepresentative → isPublic 변경과 추후 업데이트 예정 항목들을 명확히 설명하고 있어 리뷰어의 이해를 돕습니다. 주요 변경사항 섹션이 다소 간단하지만 전반적으로 충분한 정보를 제공합니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#111/home-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @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

  • 홈페이지 및 나의 CD UI 전면 개편: 홈페이지 상단 UI가 새로운 CTA 버튼과 캐릭터 이미지로 개편되었으며, '나의 CD' 페이지는 '나의 CD'와 '좋아요한 CD' 탭을 통해 플레이리스트를 분류하고, CD 캐러셀, 액션바, 재생 컨트롤러 등 개선된 사용자 경험을 제공합니다.
  • 플레이리스트 '좋아요' 기능 추가: 플레이리스트에 '좋아요'를 누르거나 취소할 수 있는 기능이 추가되었으며, '좋아요한 플레이리스트 조회' API와 연동되어 사용자가 좋아요한 CD 목록을 확인할 수 있습니다. 홈페이지와 '나의 CD' 페이지에 '좋아요' 버튼 UI가 통합되었습니다.
  • 인터페이스 필드명 변경 및 API/훅 추가: MyCdResponse 인터페이스의 isRepresentative 필드가 isPublic으로 변경되어 명확성을 높였습니다. 또한, '좋아요한 플레이리스트 조회'를 위한 getMyLikedCdList API 및 useMyLikedCdList 쿼리 훅, 그리고 '좋아요' 기능 (postLike, deleteLike, getLikeStatus)을 위한 새로운 API와 훅 (useLike, useLikeStatus)이 추가되었습니다.
  • 새로운 아이콘 및 이미지 에셋 추가: 새로운 SVG 아이콘 (icn_cta_arrow.svg, icn_like_stroke.svg)과 이미지 (img_character_home.png)가 추가되어 UI 개선에 활용되었습니다. 기존 아이콘 (icn_next.svg, icn_prev.svg, icn_search.svg)의 색상 및 크기도 조정되었습니다.
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 by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

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 pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

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

Limitations & Feedback

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

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

Footnotes

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

@github-actions
Copy link

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

Copy link

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

Choose a reason for hiding this comment

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

Code Review

이 PR은 홈페이지와 '나의 CD' 페이지의 UI를 대대적으로 개편하고, 좋아요 기능 및 관련 API를 연동하는 변경 사항을 담고 있습니다. 전반적으로 새로운 UI 구조가 잘 적용되었고, like 기능 관련 훅과 컴포넌트들이 FSD 아키텍처에 맞게 잘 추가되었습니다. 몇 가지 개선점과 버그 수정이 필요한 부분을 아래 리뷰 댓글로 남겼습니다. 특히 mycd/index.tsx에서 ActionBar 컴포넌트로 전달되는 props 값에 오류가 있어 수정이 필요해 보입니다. 또한, useLike 훅에서 좋아요 목록 데이터의 일관성을 유지하기 위한 쿼리 무효화 로직 추가를 제안합니다.

Comment on lines +158 to +163
<ActionBar
playlistId={centerPlaylist.playlistId ?? 0}
creatorId="currentPlaylist.creator.creatorId"
stickers={[]}
type="MY"
/>

Choose a reason for hiding this comment

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

critical

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

  1. 컴포넌트에 props를 전달할 때, 올바른 타입과 값을 전달해야 합니다. 문자열 리터럴을 변수 대신 사용하거나, 필요한 데이터를 전달하지 않으면 런타임 에러나 비정상적인 동작으로 이어질 수 있습니다.

Comment on lines +17 to +20
onSuccess: () => {
setIsLiked(true)
queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] })
},

Choose a reason for hiding this comment

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

high

좋아요 액션이 성공했을 때, 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

  1. Tanstack Query를 사용하여 서버 상태를 관리할 때, 데이터 변경(mutation) 후 관련된 쿼리를 무효화하여 데이터 동기화를 유지하는 것이 좋습니다. '좋아요' 상태가 변경되면 '좋아요한 CD 목록'도 업데이트되어야 합니다.

Comment on lines +25 to +28
onSuccess: () => {
setIsLiked(false)
queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] })
},

Choose a reason for hiding this comment

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

high

좋아요 취소 액션이 성공했을 때, 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

  1. Tanstack Query를 사용하여 서버 상태를 관리할 때, 데이터 변경(mutation) 후 관련된 쿼리를 무효화하여 데이터 동기화를 유지하는 것이 좋습니다. '좋아요' 상태가 변경되면 '좋아요한 CD 목록'도 업데이트되어야 합니다.

Comment on lines +197 to +203
const CharacterBg = styled.img`
position: absolute;
right: -48px;
width: 285px;
object-fit: contain;
object-position: center;
`

Choose a reason for hiding this comment

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

medium

CharacterBg 컴포넌트에 position: absoluteright: -48px가 적용되어 있습니다. 부모 컴포넌트인 TopSection이나 PageLayoutoverflow: hidden 속성이 없어 일부 화면 크기에서 가로 스크롤이 발생할 수 있습니다. 의도된 디자인이라면 괜찮지만, 아니라면 부모 요소에서 overflow를 제어하거나 다른 방식으로 위치를 조정하는 것을 고려해 보세요.1

Style Guide References

Footnotes

  1. CSS 작성 시 의도치 않은 사이드 이펙트(예: overflow로 인한 레이아웃 깨짐)가 발생하지 않도록 주의해야 합니다. position: absolute와 음수 좌표값 사용 시 부모 요소의 overflow 속성을 확인하는 것이 좋습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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: 배경 이미지가 뷰포트 기준으로 절대 배치되어 레이아웃이 깨집니다.

CharacterBgposition: 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: 접근성 개선 권장

현재 구현은 마우스 클릭만 처리하고 있습니다. 키보드 접근성을 위해 다음 개선을 권장합니다:

  1. SvgButton이 이미 버튼 역할을 하므로 문제없지만, Wrapperdiv인 경우 키보드 포커스가 불가능합니다.
  2. type === 'MY'일 때 전체가 버튼처럼 동작해야 한다면, Wrapperbutton 요소로 변경하거나 적절한 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: 키보드 네비게이션 개선 권장

탭 컴포넌트의 기본 구현은 좋으나, 키보드 접근성을 위해 다음 개선을 권장합니다:

  1. 탭 간 화살표 키 네비게이션 (ArrowLeft/ArrowRight)
  2. 적절한 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2dbe948 and b948ab1.

⛔ Files ignored due to path filters (6)
  • src/assets/icons/icn_cta_arrow.svg is excluded by !**/*.svg
  • src/assets/icons/icn_like_stroke.svg is excluded by !**/*.svg
  • src/assets/icons/icn_next.svg is excluded by !**/*.svg
  • src/assets/icons/icn_prev.svg is excluded by !**/*.svg
  • src/assets/icons/icn_search.svg is excluded by !**/*.svg
  • src/assets/images/img_character_home.png is 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.ts
  • src/features/like/api/like.ts
  • src/pages/mycd/ui/index.ts
  • src/shared/ui/SvgButton.tsx
  • src/widgets/playlist/index.ts
  • src/entities/playlist/api/playlist.ts
  • src/pages/mycd/ui/HeaderTab.tsx
  • src/widgets/playlist/ControlBar.tsx
  • src/shared/styles/mixins.ts
  • src/entities/playlist/model/usePlaylists.ts
  • src/pages/myPage/ui/playlist/index.tsx
  • src/pages/myPage/ui/components/CdGrid.tsx
  • src/entities/playlist/model/useMyPlaylist.ts
  • src/pages/myPage/ui/customize/step1/index.tsx
  • src/widgets/playlist/ActionBar.tsx
  • src/pages/home/index.tsx
  • src/assets/icons/index.ts
  • src/features/share/ui/ShareButton.tsx
  • src/assets/images/index.ts
  • src/features/like/ui/LikeButton.tsx
  • src/features/like/model/useLike.ts
  • src/stories/LoopCarousel.stories.tsx
  • src/pages/home/config/messages.ts
  • src/pages/mycd/index.tsx
  • src/entities/playlist/types/playlist.ts
  • src/shared/ui/Cd.tsx
  • src/widgets/playlist/Playlist.tsx
  • src/widgets/playlist/YoutubePlayer.tsx
  • src/widgets/playlist/PlaylistLayout.tsx
  • src/pages/home/ui/LoopCarousel.tsx
  • src/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 크기가 증가되었습니다.

carousel variant의 크기가 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 스타일 컴포넌트가 적절히 구현되었습니다.

조건부로 myCdButton mixin을 적용하는 구조가 명확하고 재사용 가능합니다.

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: 검증 결과: 리뷰 코멘트는 부정확합니다.

MyRepresentResponseisRepresentative 필드와 MyCdInfoisPublic 필드는 서로 다른 개념을 나타내기 때문에 일관성 문제가 아닙니다:

  • 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!

ActionBartype="DISCOVER" prop을 추가하여 컨텍스트별 UI를 제공하는 것이 적절합니다. props 구조가 명확해졌습니다.


164-168: LGTM!

padding-top: 32px 추가로 진행 바의 레이아웃 간격이 개선되었습니다.

Comment on lines +1 to +44
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)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

플레이리스트 전환 시 좋아요 상태가 갱신되도록 보완이 필요합니다.

현재 로컬 상태를 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.

Comment on lines +17 to +28
onSuccess: () => {
setIsLiked(true)
queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] })
},
})

const unlikeMutation = useMutation({
mutationFn: (playlistId: number) => deleteLike(playlistId),
onSuccess: () => {
setIsLiked(false)
queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] })
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

좋아요 상태 쿼리도 함께 무효화해야 합니다.

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.

Comment on lines 88 to 98
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 || [],
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

좋아요 탭에서 재생 시 생성자 정보가 잘못 주입됩니다.

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.

Suggested change
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.

Comment on lines +158 to +163
<ActionBar
playlistId={centerPlaylist.playlistId ?? 0}
creatorId="currentPlaylist.creator.creatorId"
stickers={[]}
type="MY"
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
<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).

Comment on lines +28 to +29
{type === 'MY' && <p>28</p>}
</ButtonWrapper>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

하드코딩된 채팅 개수로 인해 잘못된 정보가 노출됩니다.

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.

Comment on lines +33 to +34
<LikeButton playlistId={playlistId} isLiked={false} type={type} />
<ChatButton roomId={playlistId} creatorId={creatorId} type={type} />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

좋아요 초기 상태가 항상 해제되어 실제 상태와 불일치합니다.

LikeButtonisLiked={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.

Copy link
Member

@hansololiviakim hansololiviakim left a comment

Choose a reason for hiding this comment

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

감사합니다! 백엔드 API 리팩토링 어제 밤에 완료되었다고 합니다ㅎㅎ 참고차 공유드려요!

@maylh maylh merged commit 2ab2a4e into develop Oct 21, 2025
4 checks passed
@hansololiviakim hansololiviakim deleted the feat/#111/home-ui branch November 7, 2025 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 홈페이지, 나의CD UI 변경사항 반영

2 participants