From dbd4c8bebf96c90aa6e11cb6901efb23a119c2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=84=ED=98=84?= <77152650+Creative-Lee@users.noreply.github.com> Date: Thu, 21 Sep 2023 19:51:34 +0900 Subject: [PATCH] =?UTF-8?q?Refactor/#433=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EB=B3=84=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=EC=9D=84=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.=20(#440)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * design: 노래 리스트 Flex direction 수정 * refactor: 컴포넌트 재사용을 위한 파일명, 컴포넌트 명 변경 * refactor: MainPage의 좋아요순 노래 목록 SongItemList 컴포넌트로 분리 * refactor: 정책 변경으로 type 명 변경 각 카테고리의 모든 노래가 좋아요 순으로 정렬되므로 변경하였음. 범용적으로 사용할 수 있도록 popular prefix 제거 * refactor: 컴포넌트에서 remote 함수 분리 * feat: 장르 type 필드 추가 * refactor: 변경된 api 명세 반영 장르 쿼리파람 추가 * chore: 파일명, 상수명 단수 복수 오류 수정 * refactor: 썸네일 컴포넌트 사이즈 추가 및 radius 수정 * feat: 전체장르 추가 및 remote 함수 수정 * feat: 장르별 top10 fetch 기능 구현 * refactor: Songitem의 순위, 좋아요 수 삭제 정책 변경으로 인한 디자인 수정 * refactor: msw fixture 데이터 수정 앨범 커버 resize 옵션 수정 * style: 스타일린트 적용 * design: 케러셀 border radius 통일 * feat: useValidParams 커스텀 훅 추가 params의 undefined 검증 + 에러를 해결해주고 원하는 type이 추론되도록 하는 훅 * refactor: 변경된 api 정책 반영 api url 경로 수정 및 쿼리파람 추가 * fix: useExtraFetch type 에러로 제네릭 수정 2개이상의 인자를 허용하도록 수정 * refactor: 스와이프 페이지 route 경로에 genre path parameter 추가 * fix: 라우트 경로 오류 수정 * refactor: 메인 페이지 아이템 첫 로드시에도 랜더되지 않도록 분기 추가 --- .../songs/components/CarouselItem.tsx | 15 ++-- .../songs/components/PopularSongItem.tsx | 80 ------------------ ...gItem.stories.tsx => SongItem.stories.tsx} | 8 +- .../features/songs/components/SongItem.tsx | 49 +++++++++++ .../songs/components/SongItemList.tsx | 81 +++++++++++++++++++ .../features/songs/components/Thumbnail.tsx | 12 ++- .../src/features/songs/constants/genres.ts | 20 +++++ frontend/src/features/songs/remotes/song.ts | 7 ++ frontend/src/features/songs/remotes/songs.ts | 25 ++++-- .../src/features/songs/types/Song.type.ts | 7 +- frontend/src/mocks/fixtures/popularSongs.json | 80 +++++++++--------- frontend/src/mocks/handlers/songsHandlers.ts | 11 ++- frontend/src/pages/MainPage.tsx | 57 +++---------- frontend/src/pages/SongDetailListPage.tsx | 21 +++-- frontend/src/router.tsx | 2 +- frontend/src/shared/hooks/useExtraFetch.ts | 6 +- frontend/src/shared/hooks/useValidParams.ts | 12 +++ 17 files changed, 289 insertions(+), 204 deletions(-) delete mode 100644 frontend/src/features/songs/components/PopularSongItem.tsx rename frontend/src/features/songs/components/{PopularSongItem.stories.tsx => SongItem.stories.tsx} (66%) create mode 100644 frontend/src/features/songs/components/SongItem.tsx create mode 100644 frontend/src/features/songs/components/SongItemList.tsx create mode 100644 frontend/src/features/songs/constants/genres.ts create mode 100644 frontend/src/shared/hooks/useValidParams.ts diff --git a/frontend/src/features/songs/components/CarouselItem.tsx b/frontend/src/features/songs/components/CarouselItem.tsx index e3c2a7274..e3d21da34 100644 --- a/frontend/src/features/songs/components/CarouselItem.tsx +++ b/frontend/src/features/songs/components/CarouselItem.tsx @@ -41,41 +41,44 @@ const Wrapper = styled.li` const CollectingLink = styled(Link)` display: flex; justify-content: center; - padding: 10px; `; const Album = styled.img` max-width: 120px; background-color: white; + border-radius: 4px; `; const Contents = styled.div` display: flex; flex-direction: column; align-items: start; + justify-content: space-evenly; width: 150px; + white-space: nowrap; - justify-content: space-evenly; `; const Title = styled.p` overflow: hidden; + + max-width: 150px; margin-left: 0; + font-size: 18px; font-weight: 700; - max-width: 150px; - text-overflow: ellipsis; `; const Singer = styled.p` overflow: hidden; - margin-left: 0; - font-size: 14px; + max-width: 150px; + margin-left: 0; + font-size: 14px; text-overflow: ellipsis; `; diff --git a/frontend/src/features/songs/components/PopularSongItem.tsx b/frontend/src/features/songs/components/PopularSongItem.tsx deleted file mode 100644 index 09603ac63..000000000 --- a/frontend/src/features/songs/components/PopularSongItem.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { styled } from 'styled-components'; -import Thumbnail from './Thumbnail'; - -interface PopularSongItemProps { - rank: number; - title: string; - singer: string; - albumCoverUrl: string; - totalLikeCount: number; -} - -const PopularSongItem = ({ - rank, - albumCoverUrl, - title, - singer, - totalLikeCount, -}: PopularSongItemProps) => { - return ( - - {rank} - - {title} - {singer} - - {new Intl.NumberFormat('ko-KR').format(totalLikeCount)} likes - - - ); -}; - -export default PopularSongItem; - -const Grid = styled.div` - display: grid; - grid-template: - 'rank thumbnail title' 26px - 'rank thumbnail singer' 26px - 'rank thumbnail info' 18px - / 14px 70px; - column-gap: 8px; - - padding: 6px 0; - - color: ${({ theme: { color } }) => color.white}; -`; - -const Rank = styled.div` - display: flex; - grid-area: rank; - align-items: center; - justify-content: center; - - font-weight: 800; -`; - -const SongTitle = styled.div` - overflow: hidden; - grid-area: title; - - font-size: 16px; - font-weight: 800; - text-overflow: ellipsis; - white-space: nowrap; -`; - -const Singer = styled.div` - overflow: hidden; - grid-area: singer; - - font-size: 12px; - text-overflow: ellipsis; - white-space: nowrap; -`; - -const Info = styled.div` - grid-area: info; - font-size: 12px; - color: #808191; -`; diff --git a/frontend/src/features/songs/components/PopularSongItem.stories.tsx b/frontend/src/features/songs/components/SongItem.stories.tsx similarity index 66% rename from frontend/src/features/songs/components/PopularSongItem.stories.tsx rename to frontend/src/features/songs/components/SongItem.stories.tsx index 6720f0a84..7cbfaace6 100644 --- a/frontend/src/features/songs/components/PopularSongItem.stories.tsx +++ b/frontend/src/features/songs/components/SongItem.stories.tsx @@ -1,14 +1,14 @@ import popularSongs from '@/mocks/fixtures/popularSongs.json'; -import PopularSongItem from './PopularSongItem'; +import SongItem from './SongItem'; import type { Meta, StoryObj } from '@storybook/react'; -const meta: Meta = { - component: PopularSongItem, +const meta: Meta = { + component: SongItem, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; const { title, singer, albumCoverUrl, totalLikeCount } = popularSongs[0]; diff --git a/frontend/src/features/songs/components/SongItem.tsx b/frontend/src/features/songs/components/SongItem.tsx new file mode 100644 index 000000000..b1595a54f --- /dev/null +++ b/frontend/src/features/songs/components/SongItem.tsx @@ -0,0 +1,49 @@ +import { styled } from 'styled-components'; +import Spacing from '@/shared/components/Spacing'; +import Thumbnail from './Thumbnail'; + +interface SongItemProps { + rank: number; + title: string; + singer: string; + albumCoverUrl: string; + totalLikeCount: number; +} + +const SongItem = ({ albumCoverUrl, title, singer }: SongItemProps) => { + return ( + + + + {title} + {singer} + + ); +}; + +export default SongItem; + +const Flex = styled.div` + display: flex; + flex-direction: column; + color: ${({ theme: { color } }) => color.white}; +`; + +const SongTitle = styled.div` + overflow: hidden; + grid-area: title; + + font-size: 14px; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const Singer = styled.div` + overflow: hidden; + grid-area: singer; + + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +`; diff --git a/frontend/src/features/songs/components/SongItemList.tsx b/frontend/src/features/songs/components/SongItemList.tsx new file mode 100644 index 000000000..1959a34d4 --- /dev/null +++ b/frontend/src/features/songs/components/SongItemList.tsx @@ -0,0 +1,81 @@ +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; +import SongItem from '@/features/songs/components/SongItem'; +import Spacing from '@/shared/components/Spacing'; +import ROUTE_PATH from '@/shared/constants/path'; +import useFetch from '@/shared/hooks/useFetch'; +import GENRES from '../constants/genres'; +import { getHighLikedSongs } from '../remotes/song'; +import type { Genre, Song } from '../types/Song.type'; + +interface SongItemListProps { + genre: Genre; +} + +const SongItemList = ({ genre }: SongItemListProps) => { + const { data: songs } = useFetch(() => getHighLikedSongs(genre)); + + if (songs === null || songs?.length === 0) return; + + return ( + <> + {`${GENRES[genre]} Top 10`} + + + {songs?.map(({ id, albumCoverUrl, title, singer, totalLikeCount }, i) => ( +
  • + + + +
  • + ))} +
    + + + ); +}; + +export default SongItemList; + +const SongList = styled.ol` + scroll-snap-type: x mandatory; + + overflow-x: scroll; + display: flex; + flex-direction: row; + gap: 12px; + align-items: flex-start; + + width: 100%; +`; + +const Li = styled.li` + scroll-snap-align: center; + scroll-snap-stop: normal; + max-width: 130px; +`; + +const StyledLink = styled(Link)` + width: 100%; + + &:hover, + &:focus { + background-color: ${({ theme }) => theme.color.secondary}; + } +`; + +const Title = styled.h2` + align-self: flex-start; + font-size: 18px; + font-weight: 700; + color: white; +`; diff --git a/frontend/src/features/songs/components/Thumbnail.tsx b/frontend/src/features/songs/components/Thumbnail.tsx index 311090657..a313b33d3 100644 --- a/frontend/src/features/songs/components/Thumbnail.tsx +++ b/frontend/src/features/songs/components/Thumbnail.tsx @@ -12,7 +12,7 @@ const Thumbnail = ({ size = 'lg', ...props }: ThumbnailProps) => { }; return ( - + 노래 앨범 ); @@ -20,10 +20,10 @@ const Thumbnail = ({ size = 'lg', ...props }: ThumbnailProps) => { export default Thumbnail; -const Wrapper = styled.div<{ size: Size }>` +const Wrapper = styled.div<{ $size: Size }>` overflow: hidden; - ${({ size }) => SIZE_VARIANTS[size]}; - border-radius: 8px; + ${({ $size }) => SIZE_VARIANTS[$size]}; + border-radius: 4px; `; const SIZE_VARIANTS = { @@ -35,6 +35,10 @@ const SIZE_VARIANTS = { width: 70px; height: 70px; `, + xl: css` + width: 130px; + height: 130px; + `, } as const; type Size = keyof typeof SIZE_VARIANTS; diff --git a/frontend/src/features/songs/constants/genres.ts b/frontend/src/features/songs/constants/genres.ts new file mode 100644 index 000000000..04b09b261 --- /dev/null +++ b/frontend/src/features/songs/constants/genres.ts @@ -0,0 +1,20 @@ +const GENRES = { + ALL: '전체', + DANCE: '댄스', + HIPHOP: '힙합', + BALLAD: '발라드', + POP: '팝', + RHYTHM_AND_BLUES: 'R&B/Soul', + INDIE: '인디', + ROCK_METAL: '락/메탈', + TROT: '트로트', + FOLK_BLUES: '포크/블루스', + JAZZ: '재즈', + CLASSIC: '클래식', + J_POP: 'J-POP', + EDM: 'EDM', + ETC: '기타', +} as const; +` `; + +export default GENRES; diff --git a/frontend/src/features/songs/remotes/song.ts b/frontend/src/features/songs/remotes/song.ts index b9931c1b9..c1b0159de 100644 --- a/frontend/src/features/songs/remotes/song.ts +++ b/frontend/src/features/songs/remotes/song.ts @@ -1,6 +1,13 @@ import fetcher from '@/shared/remotes'; +import type { Genre, Song } from '../types/Song.type'; import type { SongDetail } from '@/shared/types/song'; export const getSongDetail = async (songId: number): Promise => { return await fetcher(`/songs/${songId}`, 'GET'); }; + +export const getHighLikedSongs = async (genre: Genre): Promise => { + const query = genre === 'ALL' ? '' : `?genre=${genre}`; + + return await fetcher(`/songs/high-liked${query}`, 'GET'); +}; diff --git a/frontend/src/features/songs/remotes/songs.ts b/frontend/src/features/songs/remotes/songs.ts index 0a2ef1a01..004ff60eb 100644 --- a/frontend/src/features/songs/remotes/songs.ts +++ b/frontend/src/features/songs/remotes/songs.ts @@ -1,14 +1,27 @@ import fetcher from '@/shared/remotes'; +import type { Genre } from '../types/Song.type'; import type { SongDetail, SongDetailEntries } from '@/shared/types/song'; -export const getSongDetailEntries = async (songId: number): Promise => { - return await fetcher(`/songs/${songId}`, 'GET'); +export const getSongDetailEntries = async ( + songId: number, + genre: Genre +): Promise => { + const query = genre === 'ALL' ? '' : `?genre=${genre}`; + return await fetcher(`/songs/high-liked/${songId}${query}`, 'GET'); }; -export const getExtraPrevSongDetails = async (songId: number): Promise => { - return await fetcher(`/songs/${songId}/prev`, 'GET'); +export const getExtraPrevSongDetails = async ( + songId: number, + genre: Genre +): Promise => { + const query = genre === 'ALL' ? '' : `?genre=${genre}`; + return await fetcher(`/songs/high-liked/${songId}/prev${query}`, 'GET'); }; -export const getExtraNextSongDetails = async (songId: number): Promise => { - return await fetcher(`/songs/${songId}/next`, 'GET'); +export const getExtraNextSongDetails = async ( + songId: number, + genre: Genre +): Promise => { + const query = genre === 'ALL' ? '' : `?genre=${genre}`; + return await fetcher(`/songs/high-liked/${songId}/next${query}`, 'GET'); }; diff --git a/frontend/src/features/songs/types/Song.type.ts b/frontend/src/features/songs/types/Song.type.ts index 0e99bce69..24df22d2b 100644 --- a/frontend/src/features/songs/types/Song.type.ts +++ b/frontend/src/features/songs/types/Song.type.ts @@ -1,9 +1,14 @@ -export interface PopularSong { +import type GENRES from '@/features/songs/constants/genres'; + +export type Genre = keyof typeof GENRES; + +export interface Song { id: number; title: string; singer: string; albumCoverUrl: string; totalLikeCount: number; + genre: Genre; } export interface VotingSong { diff --git a/frontend/src/mocks/fixtures/popularSongs.json b/frontend/src/mocks/fixtures/popularSongs.json index 0645eaee9..d0667b641 100644 --- a/frontend/src/mocks/fixtures/popularSongs.json +++ b/frontend/src/mocks/fixtures/popularSongs.json @@ -3,280 +3,280 @@ "id": 1, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 258200 }, { "id": 2, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 121312 }, { "id": 3, "title": "노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요.", "singer": "가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요.", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 9000 }, { "id": 4, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 8000 }, { "id": 5, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 6, "title": "Seven (feat. Latto) - Clean Ver.", "singer": "정국", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 7, "title": "노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요.", "singer": "가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요.", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 8, "title": "Seven (feat. Latto) - Clean Ver.", "singer": "정국", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 9, "title": "Seven (feat. Latto) - Clean Ver.", "singer": "정국", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 10, "title": "Seven (feat. Latto) - Clean Ver.", "singer": "정국", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 11, "title": "I AM", "singer": "IVE (아이브)", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 12, "title": "I AM", "singer": "IVE (아이브)", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 13, "title": "I AM", "singer": "IVE (아이브)", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 14, "title": "I AM", "singer": "IVE (아이브)", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 15, "title": "I AM", "singer": "IVE (아이브)", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 16, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 17, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 18, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 19, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 20, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 21, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 22, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 23, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 24, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 25, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 26, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 27, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 28, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 29, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 30, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 31, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 32, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 33, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 34, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 35, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 36, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 37, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 38, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 39, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 }, { "id": 40, "title": "Super Shy", "singer": "New Jeans", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", "totalLikeCount": 1 } ] diff --git a/frontend/src/mocks/handlers/songsHandlers.ts b/frontend/src/mocks/handlers/songsHandlers.ts index f8eb8209c..882f13936 100644 --- a/frontend/src/mocks/handlers/songsHandlers.ts +++ b/frontend/src/mocks/handlers/songsHandlers.ts @@ -11,6 +11,7 @@ const { BASE_URL } = process.env; export const songsHandlers = [ rest.get(`${BASE_URL}/songs/high-liked`, (req, res, ctx) => { + // const genre = req.url.searchParams.get('genre') return res(ctx.status(200), ctx.json(popularSongs)); }), @@ -39,15 +40,17 @@ export const songsHandlers = [ return res(ctx.status(201)); }), - rest.get(`${BASE_URL}/songs/:songId`, (req, res, ctx) => { + rest.get(`${BASE_URL}/songs/high-liked/:songId`, (req, res, ctx) => { + // const genre = req.url.searchParams.get('genre') return res(ctx.status(200), ctx.json(songEntries)); }), - - rest.get(`${BASE_URL}/songs/:songId/prev`, (req, res, ctx) => { + rest.get(`${BASE_URL}/songs/high-liked/:songId/prev`, (req, res, ctx) => { + // const genre = req.url.searchParams.get('genre') return res(ctx.status(200), ctx.json(extraPrevSongDetails)); }), - rest.get(`${BASE_URL}/songs/:songId/next`, (req, res, ctx) => { + rest.get(`${BASE_URL}/songs/high-liked/:songId/next`, (req, res, ctx) => { + // const genre = req.url.searchParams.get('genre') return res(ctx.status(200), ctx.json(extraNextSongDetails)); }), diff --git a/frontend/src/pages/MainPage.tsx b/frontend/src/pages/MainPage.tsx index bc404ba46..008b7be2c 100644 --- a/frontend/src/pages/MainPage.tsx +++ b/frontend/src/pages/MainPage.tsx @@ -1,20 +1,20 @@ -import { Link } from 'react-router-dom'; import { styled } from 'styled-components'; import CarouselItem from '@/features/songs/components/CarouselItem'; import CollectionCarousel from '@/features/songs/components/CollectionCarousel'; -import PopularSongItem from '@/features/songs/components/PopularSongItem'; +import SongItemList from '@/features/songs/components/SongItemList'; +import GENRES from '@/features/songs/constants/genres'; import Spacing from '@/shared/components/Spacing'; import SRHeading from '@/shared/components/SRHeading'; -import ROUTE_PATH from '@/shared/constants/path'; import useFetch from '@/shared/hooks/useFetch'; import fetcher from '@/shared/remotes'; -import type { PopularSong, VotingSong } from '@/features/songs/types/Song.type'; +import type { Genre, VotingSong } from '@/features/songs/types/Song.type'; + +const genres = Object.keys(GENRES) as Genre[]; const MainPage = () => { - const { data: popularSongs } = useFetch(() => fetcher('/songs/high-liked', 'GET')); const { data: votingSongs } = useFetch(() => fetcher('/voting-songs', 'GET')); - if (!popularSongs || !votingSongs) return null; + if (!votingSongs) return null; const isEmptyVotingSongs = votingSongs.length === 0; @@ -35,26 +35,9 @@ const MainPage = () => { )} - 킬링파트 좋아요 많은순 - - - {popularSongs.map(({ id, albumCoverUrl, title, singer, totalLikeCount }, i) => ( -
  • - - - -
  • - ))} -
    + {genres.map((genre) => ( + + ))} ); }; @@ -76,19 +59,6 @@ const Container = styled.div` } `; -const Li = styled.li` - width: 100%; -`; - -const StyledLink = styled(Link)` - width: 100%; - - &:hover, - &:focus { - background-color: ${({ theme }) => theme.color.secondary}; - } -`; - const Title = styled.h2` align-self: flex-start; font-size: 20px; @@ -96,15 +66,6 @@ const Title = styled.h2` color: white; `; -const PopularSongList = styled.ol` - display: flex; - flex-direction: column; - gap: 12px; - align-items: flex-start; - - width: 100%; -`; - const EmptyMessage = styled.li` display: flex; align-items: center; diff --git a/frontend/src/pages/SongDetailListPage.tsx b/frontend/src/pages/SongDetailListPage.tsx index a649198c4..ec582fd7f 100644 --- a/frontend/src/pages/SongDetailListPage.tsx +++ b/frontend/src/pages/SongDetailListPage.tsx @@ -1,5 +1,4 @@ import { useEffect, useLayoutEffect, useRef } from 'react'; -import { useParams } from 'react-router-dom'; import { styled } from 'styled-components'; import SongDetailItem from '@/features/songs/components/SongDetailItem'; import { @@ -9,11 +8,15 @@ import { } from '@/features/songs/remotes/songs'; import useExtraFetch from '@/shared/hooks/useExtraFetch'; import useFetch from '@/shared/hooks/useFetch'; +import useValidParams from '@/shared/hooks/useValidParams'; import createObserver from '@/shared/utils/createObserver'; +import type { Genre } from '@/features/songs/types/Song.type'; const SongDetailListPage = () => { - const { id: songIdParams } = useParams(); - const { data: songDetailEntries } = useFetch(() => getSongDetailEntries(Number(songIdParams))); + const { id: songIdParams, genre: genreParams } = useValidParams(); + const { data: songDetailEntries } = useFetch(() => + getSongDetailEntries(Number(songIdParams), genreParams as Genre) + ); const { data: extraPrevSongDetails, fetchData: fetchExtraPrevSongDetails } = useExtraFetch( getExtraPrevSongDetails, @@ -49,20 +52,24 @@ const SongDetailListPage = () => { useEffect(() => { if (!prevTargetRef.current) return; - const prevObserver = createObserver(() => fetchExtraPrevSongDetails(getFirstSongId())); + const prevObserver = createObserver(() => + fetchExtraPrevSongDetails(getFirstSongId(), genreParams as Genre) + ); prevObserver.observe(prevTargetRef.current); return () => prevObserver.disconnect(); - }, [fetchExtraPrevSongDetails, songDetailEntries]); + }, [fetchExtraPrevSongDetails, songDetailEntries, genreParams]); useEffect(() => { if (!nextTargetRef.current) return; - const nextObserver = createObserver(() => fetchExtraNextSongDetails(getLastSongId())); + const nextObserver = createObserver(() => + fetchExtraNextSongDetails(getLastSongId(), genreParams as Genre) + ); nextObserver.observe(nextTargetRef.current); return () => nextObserver.disconnect(); - }, [fetchExtraNextSongDetails, songDetailEntries]); + }, [fetchExtraNextSongDetails, songDetailEntries, genreParams]); useLayoutEffect(() => { itemRef.current?.scrollIntoView({ behavior: 'instant', block: 'start' }); diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index d465496e9..2140daff0 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -24,7 +24,7 @@ const router = createBrowserRouter([ element: , }, { - path: `${ROUTE_PATH.SONG_DETAILS}/:id`, + path: `${ROUTE_PATH.SONG_DETAILS}/:id/:genre`, element: , }, { diff --git a/frontend/src/shared/hooks/useExtraFetch.ts b/frontend/src/shared/hooks/useExtraFetch.ts index cf9213389..758cc5783 100644 --- a/frontend/src/shared/hooks/useExtraFetch.ts +++ b/frontend/src/shared/hooks/useExtraFetch.ts @@ -3,8 +3,8 @@ import type { ErrorResponse } from '@/shared/remotes'; type FetchDirection = 'prev' | 'next'; -const useExtraFetch = ( - extraFetcher: (...params: P[]) => Promise, +const useExtraFetch = ( + extraFetcher: (...params: P) => Promise, fetchDirection: FetchDirection ) => { const [data, setData] = useState([]); @@ -12,7 +12,7 @@ const useExtraFetch = ( const [error, setError] = useState(null); const fetchData = useCallback( - async (...params: P[]) => { + async (...params: P) => { setError(null); setIsLoading(true); diff --git a/frontend/src/shared/hooks/useValidParams.ts b/frontend/src/shared/hooks/useValidParams.ts new file mode 100644 index 000000000..30ee78028 --- /dev/null +++ b/frontend/src/shared/hooks/useValidParams.ts @@ -0,0 +1,12 @@ +import { useParams } from 'react-router-dom'; + +const useValidParams = >() => { + const params = useParams(); + if (!params || Object.values(params).some((value) => value === undefined)) { + throw new Error('Invalid parameters'); + } + + return params as Record; +}; + +export default useValidParams;