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;