From bab208bb7e766165e552047e93846451652a98a0 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, 19 Oct 2023 22:50:04 +0900 Subject: [PATCH] =?UTF-8?q?Feat/#513,=20#523=20=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EC=99=84=EB=A3=8C,=20=EA=B0=80=EC=88=98=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84=20=EB=B0=8F?= =?UTF-8?q?=20=EB=A9=94=EC=9D=B8=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=BC=80?= =?UTF-8?q?=EB=9F=AC=EC=85=80=20=EC=A0=95=EC=B1=85=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20(#525)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 가수 상세 페이지 구현 * refactor: 핸들러 로직 수정 * design: 반응형 디자인 추가 * chore: type 파일 suffix 추가 * refactor: 가수 디테일과 검색결과 type 분리 * design: 미디어 쿼리 호버 적용 * chore: type 파일 이동 * refactor: SingersSong type 추가 * refactor: 컴포넌트 분리 * test: 스토리 추가 * design: 분리된 컴포넌트 색상 추가 * refactor: Spacing 컴포넌트 반응형 디자인 props 추가 * feat: 노래 클릭 시 듣기페이지 이동 기능 구현 * refactor: 컴포넌트 분리 리스트 컴포넌트 분리 * refactor: 하위 컴포넌트가 title을 가지도록 변경 * feat: 검색 결과 페이지 구현 * chore: 노래 검색결과 픽스쳐 데이터 수정 * feat: 배너 클릭 시 가수페이지 이동 기능 구현 * chore: 사용하지 않는 import 삭제 및 픽스쳐 데이터 삭제 * feat: 배너 호버 시 아이콘 디테일 추가 * design: 아이콘 사이즈 축소 * chore: 사용하지 않는 훅 삭제 * feat: 메인 케러셀 최근 추가된 노래 리스트로 대체 * feat: 메인 케러셀 최근 추가된 노래 리스트로 대체 * feat: 최근 들은 노래 핸들러 및 fixture 구현 * design: 글로벌 스타일 수정 스토리북에서 li요소 단일 랜더 시 ::marker 표시 이슈로 li list styled none 추가 * test: singer관련 스토리 추가 * design: title color 추가 * test: 스토리 title prefix 추가로 디렉터리 구분 * design: 기본 배경색 지정 * fix: lint 에러 해결 * refactor: spacing 컴포넌트 변수명 변경으로 가독성 개선 * refactor: null 명시 * chore: singer 리모트 함수 파일 위치 변경 * refactor: api 명세 변경으로 의미 개선 검색 api 엔드포인트 변경 singer -> search / 검색 의미 강조하고자 함 name -> keyword / 범용적인 검색어의 의미를 주고자 함 search -> type / 검색 타입의 의미를 주고자함 * refactor: api 명세 변경 msw 반영 및 singer 핸들러 분리 * refactor: 함수 불리언 화 방식 변경 * design: 페이지 별 title을 다르게 하여 디자인 구분 되로록 개선 * refactor: params가 변경되면 데이터를 refetch하도록 변경 --- frontend/src/assets/icon/right-long-arrow.svg | 3 + .../search/components/SearchBar.stories.tsx | 2 +- .../components/SearchPreviewSheet.stories.tsx | 15 +++ .../search/components/SearchPreviewSheet.tsx | 2 +- .../src/features/search/remotes/search.ts | 13 +- .../src/features/search/types/search.type.ts | 5 + .../components/SingerBanner.stories.tsx | 15 +++ .../singer/components/SingerBanner.tsx | 107 ++++++++++++++++ .../components/SingerSongItem.stories.tsx | 17 +++ .../singer/components/SingerSongItem.tsx | 116 ++++++++++++++++++ .../components/SingerSongList.stories.tsx | 17 +++ .../singer/components/SingerSongList.tsx | 44 +++++++ .../src/features/singer/remotes/singer.ts | 6 + .../search.ts => singer/types/singer.type.ts} | 11 +- .../songs/components/CarouselItem.tsx | 35 ++---- .../components/CollectionCarousel.stories.tsx | 2 +- .../components/KillingPartTrack.stories.tsx | 2 +- .../KillingPartTrackList.stories.tsx | 3 +- .../songs/components/SongItem.stories.tsx | 1 + frontend/src/features/songs/remotes/song.ts | 9 +- .../songs/remotes/useGetSongDetail.ts | 14 --- frontend/src/mocks/fixtures/recentSongs.json | 44 +++++++ .../src/mocks/fixtures/searchedSingers.json | 33 +++-- frontend/src/mocks/fixtures/songEntries.json | 8 +- frontend/src/mocks/handlers/index.ts | 3 +- frontend/src/mocks/handlers/searchHandlers.ts | 20 +-- frontend/src/mocks/handlers/singerHandlers.ts | 22 ++++ frontend/src/mocks/handlers/songsHandlers.ts | 10 +- frontend/src/pages/MainPage.tsx | 31 ++--- frontend/src/pages/SearchResultPage.tsx | 75 ++++++++++- frontend/src/pages/SingerDetailPage.tsx | 47 ++++++- .../BottomSheet/BottomSheet.stories.tsx | 1 + .../shared/components/Modal/Modal.stories.tsx | 1 + frontend/src/shared/components/Spacing.tsx | 58 ++++++++- .../shared/components/Toast/Toast.stories.tsx | 2 +- .../ToggleSwitch/ToggleSwitch.stories.tsx | 2 +- frontend/src/shared/styles/GlobalStyles.ts | 2 +- frontend/src/shared/types/song.ts | 8 ++ 38 files changed, 673 insertions(+), 133 deletions(-) create mode 100644 frontend/src/assets/icon/right-long-arrow.svg create mode 100644 frontend/src/features/search/components/SearchPreviewSheet.stories.tsx create mode 100644 frontend/src/features/search/types/search.type.ts create mode 100644 frontend/src/features/singer/components/SingerBanner.stories.tsx create mode 100644 frontend/src/features/singer/components/SingerBanner.tsx create mode 100644 frontend/src/features/singer/components/SingerSongItem.stories.tsx create mode 100644 frontend/src/features/singer/components/SingerSongItem.tsx create mode 100644 frontend/src/features/singer/components/SingerSongList.stories.tsx create mode 100644 frontend/src/features/singer/components/SingerSongList.tsx create mode 100644 frontend/src/features/singer/remotes/singer.ts rename frontend/src/features/{search/types/search.ts => singer/types/singer.type.ts} (59%) delete mode 100644 frontend/src/features/songs/remotes/useGetSongDetail.ts create mode 100644 frontend/src/mocks/fixtures/recentSongs.json create mode 100644 frontend/src/mocks/handlers/singerHandlers.ts diff --git a/frontend/src/assets/icon/right-long-arrow.svg b/frontend/src/assets/icon/right-long-arrow.svg new file mode 100644 index 000000000..3f7e2f49a --- /dev/null +++ b/frontend/src/assets/icon/right-long-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/features/search/components/SearchBar.stories.tsx b/frontend/src/features/search/components/SearchBar.stories.tsx index 3b22a0237..bbd27eada 100644 --- a/frontend/src/features/search/components/SearchBar.stories.tsx +++ b/frontend/src/features/search/components/SearchBar.stories.tsx @@ -4,7 +4,7 @@ import type { Meta, StoryObj } from '@storybook/react'; const meta = { component: SearchBar, - title: 'SearchBar', + title: 'search/SearchBar', } satisfies Meta; export default meta; diff --git a/frontend/src/features/search/components/SearchPreviewSheet.stories.tsx b/frontend/src/features/search/components/SearchPreviewSheet.stories.tsx new file mode 100644 index 000000000..e2c2951e2 --- /dev/null +++ b/frontend/src/features/search/components/SearchPreviewSheet.stories.tsx @@ -0,0 +1,15 @@ +import searchedSingerPreview from '@/mocks/fixtures/searchedSingerPreview.json'; +import SearchPreviewSheet from './SearchPreviewSheet'; +import type { Meta, StoryObj } from '@storybook/react'; + +const meta = { + component: SearchPreviewSheet, + title: 'search/SearchPreviewSheet', +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => {}} />, +}; diff --git a/frontend/src/features/search/components/SearchPreviewSheet.tsx b/frontend/src/features/search/components/SearchPreviewSheet.tsx index 4c515738a..fca5deba5 100644 --- a/frontend/src/features/search/components/SearchPreviewSheet.tsx +++ b/frontend/src/features/search/components/SearchPreviewSheet.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import Thumbnail from '@/features/songs/components/Thumbnail'; import Flex from '@/shared/components/Flex/Flex'; import ROUTE_PATH from '@/shared/constants/path'; -import type { SingerSearchPreview } from '../types/search'; +import type { SingerSearchPreview } from '../types/search.type'; interface ResultSheetProps { result: SingerSearchPreview[]; diff --git a/frontend/src/features/search/remotes/search.ts b/frontend/src/features/search/remotes/search.ts index 486a71ec8..86208433c 100644 --- a/frontend/src/features/search/remotes/search.ts +++ b/frontend/src/features/search/remotes/search.ts @@ -1,16 +1,13 @@ import fetcher from '@/shared/remotes'; -import type { SingerSearchPreview, SingerSearchResult } from '../types/search'; +import type { SingerDetail } from '../../singer/types/singer.type'; +import type { SingerSearchPreview } from '../types/search.type'; export const getSingerSearchPreview = async (query: string): Promise => { const encodedQuery = encodeURIComponent(query); - return await fetcher(`/singers?name=${encodedQuery}&search=singer`, 'GET'); + return await fetcher(`/search?keyword=${encodedQuery}&type=singer`, 'GET'); }; -export const getSingerSearch = async (query: string): Promise => { +export const getSingerSearch = async (query: string): Promise => { const encodedQuery = encodeURIComponent(query); - return await fetcher(`/singers?name=${encodedQuery}&search=singer&search=song`, 'GET'); -}; - -export const getSingerDetail = async (singerId: number): Promise => { - return await fetcher(`/singers/${singerId}`, 'GET'); + return await fetcher(`/search?keyword=${encodedQuery}&type=singer&type=song`, 'GET'); }; diff --git a/frontend/src/features/search/types/search.type.ts b/frontend/src/features/search/types/search.type.ts new file mode 100644 index 000000000..cf045db54 --- /dev/null +++ b/frontend/src/features/search/types/search.type.ts @@ -0,0 +1,5 @@ +export interface SingerSearchPreview { + id: number; + singer: string; + profileImageUrl: string; +} diff --git a/frontend/src/features/singer/components/SingerBanner.stories.tsx b/frontend/src/features/singer/components/SingerBanner.stories.tsx new file mode 100644 index 000000000..0d826adf0 --- /dev/null +++ b/frontend/src/features/singer/components/SingerBanner.stories.tsx @@ -0,0 +1,15 @@ +import searchedSingers from '@/mocks/fixtures/searchedSingers.json'; +import SingerBanner from './SingerBanner'; +import type { Meta, StoryObj } from '@storybook/react'; + +const meta = { + component: SingerBanner, + title: 'singer/SingerBanner', +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => , +}; diff --git a/frontend/src/features/singer/components/SingerBanner.tsx b/frontend/src/features/singer/components/SingerBanner.tsx new file mode 100644 index 000000000..fd30fe56d --- /dev/null +++ b/frontend/src/features/singer/components/SingerBanner.tsx @@ -0,0 +1,107 @@ +import styled from 'styled-components'; +import rightArrow from '@/assets/icon/right-long-arrow.svg'; +import Flex from '@/shared/components/Flex/Flex'; +import type { SingerDetail } from '@/features/singer/types/singer.type'; + +interface SingerBannerProps + extends Pick { + onClick?: () => void; +} + +const SingerBanner = ({ profileImageUrl, singer, totalSongCount, onClick }: SingerBannerProps) => { + const clickable = typeof onClick === 'function'; + + return ( + + 아티스트 + + + + {singer} + {`등록된 노래 ${totalSongCount}개`} + + + + ); +}; + +export default SingerBanner; + +const Container = styled.div``; + +const SingerInfoContainer = styled(Flex)<{ $clickable: boolean }>` + cursor: ${({ $clickable }) => ($clickable ? 'pointer' : 'default')}; + + position: relative; + + width: 370px; + height: 240px; + padding: 20px; + + color: ${({ theme: { color } }) => color.white}; + + background-color: ${({ theme: { color } }) => color.black500}; + border-radius: 8px; + + transition: background-color 0.3s ease; + + @media (hover: hover) { + &:hover { + background-color: ${({ $clickable, theme: { color } }) => + $clickable ? color.secondary : ''}; + + &::after { + content: ''; + + position: absolute; + right: 10px; + bottom: 4px; + + width: 30px; + height: 30px; + + background: ${({ $clickable }) => + $clickable ? `no-repeat center url(${rightArrow})` : ''}; + } + } + } + + @media (max-width: ${({ theme }) => theme.breakPoints.md}) { + width: 100%; + height: 180px; + } +`; + +const ProfileImage = styled.img` + width: 100px; + height: 100px; + border-radius: 50%; +`; + +const Name = styled.div` + font-size: 36px; + font-weight: 700; + + @media (max-width: ${({ theme }) => theme.breakPoints.md}) { + font-size: 20px; + } +`; + +const SongCount = styled.p` + font-size: 18px; + + @media (max-width: ${({ theme }) => theme.breakPoints.md}) { + font-size: 14px; + } +`; + +const Title = styled.h1` + margin-bottom: 18px; + font-size: 28px; + font-weight: 700; + color: ${({ theme: { color } }) => color.white}; + + @media (max-width: ${({ theme }) => theme.breakPoints.md}) { + font-size: 24px; + } +`; diff --git a/frontend/src/features/singer/components/SingerSongItem.stories.tsx b/frontend/src/features/singer/components/SingerSongItem.stories.tsx new file mode 100644 index 000000000..62b68aeed --- /dev/null +++ b/frontend/src/features/singer/components/SingerSongItem.stories.tsx @@ -0,0 +1,17 @@ +import searchedSingers from '@/mocks/fixtures/searchedSingers.json'; +import SingerSongItem from './SingerSongItem'; +import type { Meta, StoryObj } from '@storybook/react'; + +const song = searchedSingers[0].songs[0]; + +const meta = { + component: SingerSongItem, + title: 'singer/SingerSongItem', +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => , +}; diff --git a/frontend/src/features/singer/components/SingerSongItem.tsx b/frontend/src/features/singer/components/SingerSongItem.tsx new file mode 100644 index 000000000..d9d24beaa --- /dev/null +++ b/frontend/src/features/singer/components/SingerSongItem.tsx @@ -0,0 +1,116 @@ +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; +import Flex from '@/shared/components/Flex/Flex'; +import ROUTE_PATH from '@/shared/constants/path'; +import { toMinSecText } from '@/shared/utils/convertTime'; +import type { SingersSong } from '@/features/singer/types/singer.type'; + +interface SingerSongItemProps extends SingersSong {} + +const SingerSongItem = ({ id, singer, albumCoverUrl, title, videoLength }: SingerSongItemProps) => { + return ( + + + + + + + {title} + {singer} + + + {toMinSecText(videoLength)} + + + + ); +}; + +export default SingerSongItem; + +const FlexLink = styled(Link)` + display: flex; + gap: 16px; + justify-content: center; + width: 100%; + + @media (max-width: ${({ theme }) => theme.breakPoints.xs}) { + align-items: center; + } +`; + +const SongListItem = styled.li` + width: 100%; + padding: 8px; + + color: ${({ theme: { color } }) => color.white}; + + background-color: ${({ theme: { color } }) => color.black}; + border-radius: 4px; + + transition: background-color 0.3s ease; + + @media (hover: hover) { + &:hover { + background-color: ${({ theme: { color } }) => color.secondary}; + } + } + + @media (max-width: ${({ theme }) => theme.breakPoints.xs}) { + border-radius: 0; + } +`; + +const AlbumCoverWrapper = styled.div` + aspect-ratio: 1 / 1; + width: 100px; + + @media (max-width: ${({ theme }) => theme.breakPoints.md}) { + width: 70px; + } +`; + +const AlbumCover = styled.img` + width: 100%; +`; + +const FlexInfo = styled(Flex)` + overflow: hidden; + flex: 3 1 0; + + padding: 8px 0; + + text-overflow: ellipsis; + white-space: nowrap; + + @media (max-width: ${({ theme }) => theme.breakPoints.md}) { + padding: 6px 0; + } +`; + +const SongTitle = styled.div` + overflow: hidden; + + font-size: 16px; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const Singer = styled.div` + overflow: hidden; + + font-size: 14px; + color: ${({ theme: { color } }) => color.subText}; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const VideoLength = styled(Flex)` + flex: 1 0 0; + font-size: 16px; + + @media (max-width: ${({ theme }) => theme.breakPoints.md}) { + font-size: 14px; + } +`; diff --git a/frontend/src/features/singer/components/SingerSongList.stories.tsx b/frontend/src/features/singer/components/SingerSongList.stories.tsx new file mode 100644 index 000000000..c46d7d1c6 --- /dev/null +++ b/frontend/src/features/singer/components/SingerSongList.stories.tsx @@ -0,0 +1,17 @@ +import singerSongs from '@/mocks/fixtures/searchedSingers.json'; +import SingerSongList from './SingerSongList'; +import type { Meta, StoryObj } from '@storybook/react'; + +const singerSong = singerSongs[0]; + +const meta = { + component: SingerSongList, + title: 'singer/SingerSongList', +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => , +}; diff --git a/frontend/src/features/singer/components/SingerSongList.tsx b/frontend/src/features/singer/components/SingerSongList.tsx new file mode 100644 index 000000000..4dc2d7b8d --- /dev/null +++ b/frontend/src/features/singer/components/SingerSongList.tsx @@ -0,0 +1,44 @@ +import styled from 'styled-components'; +import Flex from '@/shared/components/Flex/Flex'; +import SingerSongItem from './SingerSongItem'; +import type { SingersSong } from '../types/singer.type'; + +interface SingerSongListProps { + songs: SingersSong[]; + title: string; +} + +const SingerSongList = ({ songs, title }: SingerSongListProps) => { + return ( + + {title} + + {songs.map((song) => ( + + ))} + + + ); +}; + +export default SingerSongList; + +const Container = styled.div` + flex: 1; +`; + +const SongsItemList = styled(Flex)` + overflow-y: scroll; + width: 100%; +`; + +const Title = styled.h2` + margin-bottom: 18px; + font-size: 28px; + font-weight: 700; + color: ${({ theme: { color } }) => color.white}; + + @media (max-width: ${({ theme }) => theme.breakPoints.xs}) { + font-size: 24px; + } +`; diff --git a/frontend/src/features/singer/remotes/singer.ts b/frontend/src/features/singer/remotes/singer.ts new file mode 100644 index 000000000..bea1d670b --- /dev/null +++ b/frontend/src/features/singer/remotes/singer.ts @@ -0,0 +1,6 @@ +import fetcher from '@/shared/remotes'; +import type { SingerDetail } from '../types/singer.type'; + +export const getSingerDetail = async (singerId: number): Promise => { + return await fetcher(`/singers/${singerId}`, 'GET'); +}; diff --git a/frontend/src/features/search/types/search.ts b/frontend/src/features/singer/types/singer.type.ts similarity index 59% rename from frontend/src/features/search/types/search.ts rename to frontend/src/features/singer/types/singer.type.ts index 133dad1b7..fb7e1365b 100644 --- a/frontend/src/features/search/types/search.ts +++ b/frontend/src/features/singer/types/singer.type.ts @@ -1,17 +1,12 @@ -interface SingersSong { +export interface SingersSong { id: number; + singer: string; title: string; albumCoverUrl: string; videoLength: number; } -export interface SingerSearchPreview { - id: number; - singer: string; - profileImageUrl: string; -} - -export interface SingerSearchResult { +export interface SingerDetail { id: number; singer: string; profileImageUrl: string; diff --git a/frontend/src/features/songs/components/CarouselItem.tsx b/frontend/src/features/songs/components/CarouselItem.tsx index 4221331c1..9992762c2 100644 --- a/frontend/src/features/songs/components/CarouselItem.tsx +++ b/frontend/src/features/songs/components/CarouselItem.tsx @@ -1,41 +1,22 @@ -import { useNavigate } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { styled } from 'styled-components'; import emptyPlay from '@/assets/icon/empty-play.svg'; -import { useAuthContext } from '@/features/auth/components/AuthProvider'; -import LoginModal from '@/features/auth/components/LoginModal'; import Thumbnail from '@/features/songs/components/Thumbnail'; -import useModal from '@/shared/components/Modal/hooks/useModal'; import Spacing from '@/shared/components/Spacing'; import ROUTE_PATH from '@/shared/constants/path'; import { toMinSecText } from '@/shared/utils/convertTime'; -import type { VotingSong } from '../types/Song.type'; +import type { RecentSong } from '@/shared/types/song'; interface CarouselItemProps { - votingSong: VotingSong; + recentSong: RecentSong; } -const CarouselItem = ({ votingSong }: CarouselItemProps) => { - const { id, singer, title, videoLength, albumCoverUrl } = votingSong; - - const { isOpen, openModal, closeModal } = useModal(); - - const { user } = useAuthContext(); - const isLoggedIn = !!user; - - const navigate = useNavigate(); - const goToPartCollectingPage = () => navigate(`${ROUTE_PATH.COLLECT}/${id}`); +const CarouselItem = ({ recentSong }: CarouselItemProps) => { + const { id, singer, title, videoLength, albumCoverUrl } = recentSong; return ( - - - + @@ -46,7 +27,7 @@ const CarouselItem = ({ votingSong }: CarouselItemProps) => { {toMinSecText(videoLength)} - + ); }; @@ -58,7 +39,7 @@ const Wrapper = styled.li` min-width: 350px; `; -const CollectingLink = styled.a` +const FlexLink = styled(Link)` display: flex; justify-content: center; padding: 10px; diff --git a/frontend/src/features/songs/components/CollectionCarousel.stories.tsx b/frontend/src/features/songs/components/CollectionCarousel.stories.tsx index 3e9126acb..7f20350e0 100644 --- a/frontend/src/features/songs/components/CollectionCarousel.stories.tsx +++ b/frontend/src/features/songs/components/CollectionCarousel.stories.tsx @@ -6,7 +6,7 @@ import type { Meta, StoryObj } from '@storybook/react'; const meta: Meta = { component: CollectionCarousel, - title: 'CollectionCarousel', + title: 'songs/CollectionCarousel', decorators: [ (Story) => ( diff --git a/frontend/src/features/songs/components/KillingPartTrack.stories.tsx b/frontend/src/features/songs/components/KillingPartTrack.stories.tsx index 625d62858..a7ef50472 100644 --- a/frontend/src/features/songs/components/KillingPartTrack.stories.tsx +++ b/frontend/src/features/songs/components/KillingPartTrack.stories.tsx @@ -9,7 +9,7 @@ import type { Meta, StoryObj } from '@storybook/react'; // FIXME: 재생시 `YT is not defined` 에러 발생 const meta = { component: KillingPartTrack, - title: 'KillingPartTrack', + title: 'killingPart/KillingPartTrack', decorators: [ (Story) => { return ( diff --git a/frontend/src/features/songs/components/KillingPartTrackList.stories.tsx b/frontend/src/features/songs/components/KillingPartTrackList.stories.tsx index 15e625634..979087a2c 100644 --- a/frontend/src/features/songs/components/KillingPartTrackList.stories.tsx +++ b/frontend/src/features/songs/components/KillingPartTrackList.stories.tsx @@ -9,13 +9,12 @@ import type { Meta, StoryObj } from '@storybook/react'; // FIXME: 재생시 `YT is not defined` 에러 발생 const meta = { component: KillingPartTrackList, - title: 'KillingPartTrackList', + title: 'killingPart/KillingPartTrackList', decorators: [ (Story) => { return ( - {/* FIXME: time prop을 KillingPartTrack 스토리북 컴포넌트를 보고 임의로 똑같이 넣었습니다. */} diff --git a/frontend/src/features/songs/components/SongItem.stories.tsx b/frontend/src/features/songs/components/SongItem.stories.tsx index 7cbfaace6..eb59ac905 100644 --- a/frontend/src/features/songs/components/SongItem.stories.tsx +++ b/frontend/src/features/songs/components/SongItem.stories.tsx @@ -4,6 +4,7 @@ import type { Meta, StoryObj } from '@storybook/react'; const meta: Meta = { component: SongItem, + title: 'songs/SongItem', }; export default meta; diff --git a/frontend/src/features/songs/remotes/song.ts b/frontend/src/features/songs/remotes/song.ts index c1b0159de..9039cc549 100644 --- a/frontend/src/features/songs/remotes/song.ts +++ b/frontend/src/features/songs/remotes/song.ts @@ -1,9 +1,12 @@ import fetcher from '@/shared/remotes'; import type { Genre, Song } from '../types/Song.type'; -import type { SongDetail } from '@/shared/types/song'; +import type { RecentSong } from '@/shared/types/song'; -export const getSongDetail = async (songId: number): Promise => { - return await fetcher(`/songs/${songId}`, 'GET'); +// 메인 케러셀 최신순 노래 n개 조회 api - 쿼리파람 없는경우, 응답 기본값은 5개입니다. +export const getRecentSongs = async (songCount?: number): Promise => { + const query = songCount ? `?size=${songCount}` : ''; + + return await fetcher(`/songs/recent${query}`, 'GET'); }; export const getHighLikedSongs = async (genre: Genre): Promise => { diff --git a/frontend/src/features/songs/remotes/useGetSongDetail.ts b/frontend/src/features/songs/remotes/useGetSongDetail.ts deleted file mode 100644 index 6c5dbf111..000000000 --- a/frontend/src/features/songs/remotes/useGetSongDetail.ts +++ /dev/null @@ -1,14 +0,0 @@ -import useFetch from '@/shared/hooks/useFetch'; -import { getSongDetail } from './song'; -import type { SongDetail } from '@/shared/types/song'; - -export const useGetSongDetail = (songId: number) => { - const { - data: songDetail, - isLoading, - error, - fetchData, - } = useFetch(() => getSongDetail(songId)); - - return { songDetail, isLoading, error, fetchData }; -}; diff --git a/frontend/src/mocks/fixtures/recentSongs.json b/frontend/src/mocks/fixtures/recentSongs.json new file mode 100644 index 000000000..2b0191128 --- /dev/null +++ b/frontend/src/mocks/fixtures/recentSongs.json @@ -0,0 +1,44 @@ +[ + { + "id": 1, + "title": "Ditto", + "singer": "NewJeans", + "videoLength": 187, + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize" + }, + { + "id": 2, + "title": "Still With You", + "singer": "정국", + "videoLength": 239, + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize" + }, + { + "id": 3, + "title": "Broken Melodies", + "singer": "NCT DREAM", + "videoLength": 227, + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize" + }, + { + "id": 4, + "title": "Candy", + "singer": "NCT DREAM", + "videoLength": 220, + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize" + }, + { + "id": 5, + "title": "Kitsch", + "singer": "IVE (아이브)", + "videoLength": 195, + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize" + }, + { + "id": 6, + "title": "UNFORGIVEN (feat. Nile Rodgers)", + "singer": "LE SSERAFIM (르세라핌)", + "videoLength": 181, + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize" + } +] diff --git a/frontend/src/mocks/fixtures/searchedSingers.json b/frontend/src/mocks/fixtures/searchedSingers.json index 73583d623..d8749784d 100644 --- a/frontend/src/mocks/fixtures/searchedSingers.json +++ b/frontend/src/mocks/fixtures/searchedSingers.json @@ -2,25 +2,28 @@ { "id": 1, "singer": "악동뮤지션", - "profileImageUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/416/quality/80/optimize", + "profileImageUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/240/quality/100/optimize", "totalSongCount": 6, "songs": [ { "id": 1, + "singer": "악동뮤지션", "title": "계란찜의 꿈", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/416/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/240/quality/100/optimize", "videoLength": 111 }, { "id": 2, + "singer": "악동뮤지션", "title": "수란의 꿈", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/416/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/240/quality/100/optimize", "videoLength": 222 }, { "id": 3, + "singer": "악동뮤지션", "title": "서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 꿈", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/416/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/240/quality/100/optimize", "videoLength": 333 } ] @@ -28,25 +31,28 @@ { "id": 2, "singer": "악동", - "profileImageUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/416/quality/80/optimize", + "profileImageUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/240/quality/100/optimize", "totalSongCount": 12, "songs": [ { "id": 4, + "singer": "악동뮤지션", "title": "계란찜의 꿈", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/416/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/240/quality/100/optimize", "videoLength": 111 }, { "id": 5, + "singer": "악동뮤지션", "title": "수란의 꿈", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/416/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/240/quality/100/optimize", "videoLength": 222 }, { "id": 6, + "singer": "악동뮤지션", "title": "서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 꿈", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/416/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/240/quality/100/optimize", "videoLength": 333 } ] @@ -54,25 +60,28 @@ { "id": 3, "singer": "뮤지션", - "profileImageUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/416/quality/80/optimize", + "profileImageUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/240/quality/100/optimize", "totalSongCount": 18, "songs": [ { "id": 7, + "singer": "악동", "title": "계란찜의 꿈", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/416/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/240/quality/100/optimize", "videoLength": 111 }, { "id": 8, + "singer": "악동", "title": "수란의 꿈", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/416/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/240/quality/100/optimize", "videoLength": 222 }, { "id": 9, + "singer": "악동", "title": "서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 서니사이드업의 꿈", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/416/quality/80/optimize", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/artistcrop/images/007/12/452/712452_20230821182358_500.jpg?f9ab949ec336274795e07b2719addab6/melon/resize/240/quality/100/optimize", "videoLength": 333 } ] diff --git a/frontend/src/mocks/fixtures/songEntries.json b/frontend/src/mocks/fixtures/songEntries.json index 44c78a6e2..29001cb51 100644 --- a/frontend/src/mocks/fixtures/songEntries.json +++ b/frontend/src/mocks/fixtures/songEntries.json @@ -403,11 +403,11 @@ ], "currentSong": { "id": 11, - "title": "모래 알갱이", - "singer": "임영웅", + "title": "후라이의 꿈", + "singer": "악동뮤지션", "videoLength": 221, - "songVideoId": "3_wOZrzmQ1o", - "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", + "songVideoId": "3kGAlp_PNUg", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/113/09/190/11309190_20230818161008_500.jpg?93148adc9d3b5622f4c4dca1e429650e/melon/resize/282/quality/80/optimize", "killingParts": [ { "id": 33, diff --git a/frontend/src/mocks/handlers/index.ts b/frontend/src/mocks/handlers/index.ts index 4189b9d25..f416102a7 100644 --- a/frontend/src/mocks/handlers/index.ts +++ b/frontend/src/mocks/handlers/index.ts @@ -1,7 +1,8 @@ import memberHandlers from './memberHandlers'; import searchHandlers from './searchHandlers'; +import singerHandlers from './singerHandlers'; import songsHandlers from './songsHandlers'; -const handlers = [...memberHandlers, ...searchHandlers, ...songsHandlers]; +const handlers = [...memberHandlers, ...searchHandlers, ...singerHandlers, ...songsHandlers]; export default handlers; diff --git a/frontend/src/mocks/handlers/searchHandlers.ts b/frontend/src/mocks/handlers/searchHandlers.ts index 56d1fdf4b..2cd8dce4d 100644 --- a/frontend/src/mocks/handlers/searchHandlers.ts +++ b/frontend/src/mocks/handlers/searchHandlers.ts @@ -5,9 +5,10 @@ import searchedSingers from '@/mocks/fixtures/searchedSingers.json'; const { BASE_URL } = process.env; const searchHandlers = [ - rest.get(`${BASE_URL}/singers`, (req, res, ctx) => { - const query = req.url.searchParams.get('name') ?? ''; - const [singer, song] = req.url.searchParams.getAll('search'); + // 검색 미리보기, 검색완료 페이지 + rest.get(`${BASE_URL}/search`, (req, res, ctx) => { + const query = req.url.searchParams.get('keyword') ?? ''; + const [singer, song] = req.url.searchParams.getAll('type'); const testQueries = ['악동뮤지션', '악동', '뮤지션']; const isPreviewRequest = singer !== undefined && song === undefined; @@ -29,19 +30,6 @@ const searchHandlers = [ return res(ctx.status(200), ctx.json([])); } }), - - rest.get(`${BASE_URL}/singers/:singerId`, (req, res, ctx) => { - const { singerId } = req.params; - - const numberSingerId = Number(singerId as string); - const searchedSinger = searchedSingers[numberSingerId]; - - if (searchedSinger !== undefined) { - return res(ctx.status(200), ctx.json(searchedSinger)); - } - - return res(ctx.status(400), ctx.json({})); - }), ]; export default searchHandlers; diff --git a/frontend/src/mocks/handlers/singerHandlers.ts b/frontend/src/mocks/handlers/singerHandlers.ts new file mode 100644 index 000000000..c119dd812 --- /dev/null +++ b/frontend/src/mocks/handlers/singerHandlers.ts @@ -0,0 +1,22 @@ +import { rest } from 'msw'; +import searchedSingers from '@/mocks/fixtures/searchedSingers.json'; + +const { BASE_URL } = process.env; + +const singerHandlers = [ + // 가수 상세페이지 + rest.get(`${BASE_URL}/singers/:singerId`, (req, res, ctx) => { + const { singerId } = req.params; + + const numberSingerId = Number(singerId as string); + const searchedSinger = searchedSingers[numberSingerId - 1]; + + if (searchedSinger !== undefined) { + return res(ctx.status(200), ctx.json(searchedSinger)); + } + + return res(ctx.status(400), ctx.json({})); + }), +]; + +export default singerHandlers; diff --git a/frontend/src/mocks/handlers/songsHandlers.ts b/frontend/src/mocks/handlers/songsHandlers.ts index 8b4363840..01a793b45 100644 --- a/frontend/src/mocks/handlers/songsHandlers.ts +++ b/frontend/src/mocks/handlers/songsHandlers.ts @@ -3,8 +3,8 @@ import comments from '../fixtures/comments.json'; import extraNextSongDetails from '../fixtures/extraNextSongDetails.json'; import extraPrevSongDetails from '../fixtures/extraPrevSongDetails.json'; import popularSongs from '../fixtures/popularSongs.json'; +import recentSongs from '../fixtures/recentSongs.json'; import songEntries from '../fixtures/songEntries.json'; -import votingSongs from '../fixtures/votingSongs.json'; import type { KillingPartPostRequest } from '@/shared/types/killingPart'; const { BASE_URL } = process.env; @@ -54,8 +54,12 @@ const songsHandlers = [ return res(ctx.status(200), ctx.json(extraNextSongDetails)); }), - rest.get(`${BASE_URL}/voting-songs`, (req, res, ctx) => { - return res(ctx.status(200), ctx.json(votingSongs)); + rest.get(`${BASE_URL}/songs/recent`, (req, res, ctx) => { + const size = req.url.searchParams.get('size'); + + const slicedRecentSongs = size ? recentSongs.slice(0, Number(size)) : recentSongs.slice(0, 5); + + return res(ctx.status(200), ctx.json(slicedRecentSongs)); }), rest.delete(`${BASE_URL}/member-parts/:partId`, (req, res, ctx) => { diff --git a/frontend/src/pages/MainPage.tsx b/frontend/src/pages/MainPage.tsx index 008b7be2c..7d93ba7ea 100644 --- a/frontend/src/pages/MainPage.tsx +++ b/frontend/src/pages/MainPage.tsx @@ -3,20 +3,18 @@ import CarouselItem from '@/features/songs/components/CarouselItem'; import CollectionCarousel from '@/features/songs/components/CollectionCarousel'; import SongItemList from '@/features/songs/components/SongItemList'; import GENRES from '@/features/songs/constants/genres'; +import { getRecentSongs } from '@/features/songs/remotes/song'; import Spacing from '@/shared/components/Spacing'; import SRHeading from '@/shared/components/SRHeading'; import useFetch from '@/shared/hooks/useFetch'; -import fetcher from '@/shared/remotes'; -import type { Genre, VotingSong } from '@/features/songs/types/Song.type'; +import type { Genre } from '@/features/songs/types/Song.type'; const genres = Object.keys(GENRES) as Genre[]; const MainPage = () => { - const { data: votingSongs } = useFetch(() => fetcher('/voting-songs', 'GET')); + const { data: recentSongs } = useFetch(() => getRecentSongs()); - if (!votingSongs) return null; - - const isEmptyVotingSongs = votingSongs.length === 0; + if (!recentSongs) return null; return ( @@ -24,15 +22,9 @@ const MainPage = () => { 현재 킬링파트 등록중인 노래 - {isEmptyVotingSongs ? ( - - 수집중인 노래가 곧 등록될 예정입니다. - - ) : ( - votingSongs.map((votingSong) => { - return ; - }) - )} + {recentSongs.map((recentSong) => ( + + ))} {genres.map((genre) => ( @@ -65,12 +57,3 @@ const Title = styled.h2` font-weight: 700; color: white; `; - -const EmptyMessage = styled.li` - display: flex; - align-items: center; - justify-content: center; - - width: 100%; - min-width: 350px; -`; diff --git a/frontend/src/pages/SearchResultPage.tsx b/frontend/src/pages/SearchResultPage.tsx index 4a9084166..054185c17 100644 --- a/frontend/src/pages/SearchResultPage.tsx +++ b/frontend/src/pages/SearchResultPage.tsx @@ -1,9 +1,82 @@ +import React, { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; +import { getSingerSearch } from '@/features/search/remotes/search'; +import SingerBanner from '@/features/singer/components/SingerBanner'; +import SingerSongList from '@/features/singer/components/SingerSongList'; +import Flex from '@/shared/components/Flex/Flex'; +import Spacing from '@/shared/components/Spacing'; +import ROUTE_PATH from '@/shared/constants/path'; +import useFetch from '@/shared/hooks/useFetch'; import useValidSearchParams from '@/shared/hooks/useValidSearchParams'; const SearchResultPage = () => { const { name } = useValidSearchParams('name'); + const navigate = useNavigate(); - return
{name}
; + const { data: singerDetailList, fetchData: refetchSingerSearch } = useFetch(() => + getSingerSearch(name) + ); + + useEffect(() => { + refetchSingerSearch(); + }, [name]); + + if (!singerDetailList) return; + + const goToSingerDetailPage = (singerId: number) => + navigate(`/${ROUTE_PATH.SINGER_DETAIL}/${singerId}`); + + return ( + + 검색 결과 + + {singerDetailList.map(({ id: singerId, profileImageUrl, singer, totalSongCount, songs }) => ( + + + goToSingerDetailPage(singerId)} + /> + + + + + + ))} + + ); }; export default SearchResultPage; + +const Container = styled(Flex)` + width: 100%; + padding-top: ${({ theme: { headerHeight } }) => headerHeight.desktop}; + + @media (max-width: ${({ theme }) => theme.breakPoints.xs}) { + padding-top: ${({ theme: { headerHeight } }) => headerHeight.mobile}; + } + + @media (max-width: ${({ theme }) => theme.breakPoints.xxs}) { + padding-top: ${({ theme: { headerHeight } }) => headerHeight.xxs}; + } +`; + +const FlexSearchResultContainer = styled(Flex)` + width: 100%; +`; + +const Title = styled.h1` + font-size: 28px; + font-weight: 700; +`; + +const UnderLine = styled.div` + width: 100%; + margin: 32px 0; + border-bottom: 1px solid ${({ theme: { color } }) => color.black200}; + border-radius: 1px; +`; diff --git a/frontend/src/pages/SingerDetailPage.tsx b/frontend/src/pages/SingerDetailPage.tsx index 3c977f7e0..95d1bfc4a 100644 --- a/frontend/src/pages/SingerDetailPage.tsx +++ b/frontend/src/pages/SingerDetailPage.tsx @@ -1,9 +1,54 @@ +import { useEffect } from 'react'; +import styled from 'styled-components'; +import SingerBanner from '@/features/singer/components/SingerBanner'; +import SingerSongList from '@/features/singer/components/SingerSongList'; +import { getSingerDetail } from '@/features/singer/remotes/singer'; +import Flex from '@/shared/components/Flex/Flex'; +import Spacing from '@/shared/components/Spacing'; +import useFetch from '@/shared/hooks/useFetch'; import useValidParams from '@/shared/hooks/useValidParams'; const SingerDetailPage = () => { const { singerId } = useValidParams(); - return
{singerId}
; + const { data: singerDetail, fetchData: refetchSingerDetail } = useFetch(() => + getSingerDetail(Number(singerId)) + ); + + useEffect(() => { + refetchSingerDetail(); + }, [singerId]); + + if (!singerDetail) return null; + + const { profileImageUrl, singer, songs, totalSongCount } = singerDetail; + + return ( + + + + + + + + ); }; export default SingerDetailPage; + +const Container = styled(Flex)` + width: 100%; + padding-top: ${({ theme: { headerHeight } }) => headerHeight.desktop}; + + @media (max-width: ${({ theme }) => theme.breakPoints.xs}) { + padding-top: ${({ theme: { headerHeight } }) => headerHeight.mobile}; + } + + @media (max-width: ${({ theme }) => theme.breakPoints.xxs}) { + padding-top: ${({ theme: { headerHeight } }) => headerHeight.xxs}; + } +`; diff --git a/frontend/src/shared/components/BottomSheet/BottomSheet.stories.tsx b/frontend/src/shared/components/BottomSheet/BottomSheet.stories.tsx index 5680adccc..332e94a0f 100644 --- a/frontend/src/shared/components/BottomSheet/BottomSheet.stories.tsx +++ b/frontend/src/shared/components/BottomSheet/BottomSheet.stories.tsx @@ -4,6 +4,7 @@ import BottomSheet from './BottomSheet'; import type { Meta, StoryObj } from '@storybook/react'; const meta: Meta = { + title: 'shared/BottomSheet', component: BottomSheet, }; diff --git a/frontend/src/shared/components/Modal/Modal.stories.tsx b/frontend/src/shared/components/Modal/Modal.stories.tsx index ca520da1f..7c6671a5c 100644 --- a/frontend/src/shared/components/Modal/Modal.stories.tsx +++ b/frontend/src/shared/components/Modal/Modal.stories.tsx @@ -3,6 +3,7 @@ import Modal from './Modal'; import type { Meta, StoryObj } from '@storybook/react'; const meta: Meta = { + title: 'shared/Modal', component: Modal, }; diff --git a/frontend/src/shared/components/Spacing.tsx b/frontend/src/shared/components/Spacing.tsx index f0049c56e..28f825472 100644 --- a/frontend/src/shared/components/Spacing.tsx +++ b/frontend/src/shared/components/Spacing.tsx @@ -1,9 +1,63 @@ -import { styled } from 'styled-components'; +import styled, { css } from 'styled-components'; +import type { BreakPoints } from '@/shared/types/theme'; -const Spacing = styled.div<{ direction: 'horizontal' | 'vertical'; size: number }>` +interface Spacing { + direction: 'horizontal' | 'vertical'; + size: number; +} + +interface ResponsiveSpacing extends Partial>> {} + +interface SpacingProps extends Spacing, ResponsiveSpacing {} + +const Spacing = styled.div` flex: none; min-width: ${({ direction, size }) => (direction === 'horizontal' ? `${size}px` : '0')}; min-height: ${({ direction, size }) => (direction === 'vertical' ? `${size}px` : '0')}; + + @media (max-width: ${({ theme }) => theme.breakPoints.xxl}) { + ${({ $xxl, direction, size }) => spacingCss($xxl, direction, size)} + } + + @media (max-width: ${({ theme }) => theme.breakPoints.xl}) { + ${({ $xl, direction, size }) => spacingCss($xl, direction, size)} + } + + @media (max-width: ${({ theme }) => theme.breakPoints.lg}) { + ${({ $lg, direction, size }) => spacingCss($lg, direction, size)} + } + + @media (max-width: ${({ theme }) => theme.breakPoints.md}) { + ${({ $md, direction, size }) => spacingCss($md, direction, size)} + } + + @media (max-width: ${({ theme }) => theme.breakPoints.sm}) { + ${({ $sm, direction, size }) => spacingCss($sm, direction, size)} + } + + @media (max-width: ${({ theme }) => theme.breakPoints.xs}) { + ${({ $xs, direction, size }) => spacingCss($xs, direction, size)} + } + + @media (max-width: ${({ theme }) => theme.breakPoints.xxs}) { + ${({ $xxs, direction, size }) => spacingCss($xxs, direction, size)} + } `; +const spacingCss = ( + responsiveSpacing?: Partial, + originalDir?: Spacing['direction'], + originalSize?: Spacing['size'] +) => { + if (!responsiveSpacing) return; + + const direction = responsiveSpacing.direction ?? originalDir; + const size = responsiveSpacing.size ?? originalSize; + + return css` + min-width: ${direction === 'horizontal' ? `${size}px` : '0'}; + min-height: ${direction === 'vertical' ? `${size}px` : '0'}; + `; +}; + export default Spacing; diff --git a/frontend/src/shared/components/Toast/Toast.stories.tsx b/frontend/src/shared/components/Toast/Toast.stories.tsx index 956e8b55c..fcf5a672d 100644 --- a/frontend/src/shared/components/Toast/Toast.stories.tsx +++ b/frontend/src/shared/components/Toast/Toast.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; const meta = { component: Toast, - title: 'Toast', + title: 'shared/Toast', args: { message: `I'm toast`, }, diff --git a/frontend/src/shared/components/ToggleSwitch/ToggleSwitch.stories.tsx b/frontend/src/shared/components/ToggleSwitch/ToggleSwitch.stories.tsx index 82dc353c1..313a2d174 100644 --- a/frontend/src/shared/components/ToggleSwitch/ToggleSwitch.stories.tsx +++ b/frontend/src/shared/components/ToggleSwitch/ToggleSwitch.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; const meta = { component: ToggleSwitch, - title: 'ToggleSwitch', + title: 'shared/ToggleSwitch', } satisfies Meta; export default meta; diff --git a/frontend/src/shared/styles/GlobalStyles.ts b/frontend/src/shared/styles/GlobalStyles.ts index 50122fffc..fbce5564b 100644 --- a/frontend/src/shared/styles/GlobalStyles.ts +++ b/frontend/src/shared/styles/GlobalStyles.ts @@ -61,7 +61,7 @@ const GlobalStyles = createGlobalStyle` border-collapse: collapse; } - ol, ul { + ol, ul, li { list-style: none; } diff --git a/frontend/src/shared/types/song.ts b/frontend/src/shared/types/song.ts index d7ecd88e2..fb0085669 100644 --- a/frontend/src/shared/types/song.ts +++ b/frontend/src/shared/types/song.ts @@ -40,3 +40,11 @@ export interface SongInfo { genre: Genre; killingParts: KillingPart[]; } + +export interface RecentSong { + id: number; + title: string; + singer: string; + videoLength: number; + albumCoverUrl: string; +}