Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/#513, #523 검색완료, 가수 상세 페이지 구현 및 메인페이지 케러셀 정책 변경 반영 #525

Merged
merged 41 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
9c0abec
feat: 가수 상세 페이지 구현
Creative-Lee Oct 17, 2023
f7ebde1
refactor: 핸들러 로직 수정
Creative-Lee Oct 17, 2023
f8eb5c5
design: 반응형 디자인 추가
Creative-Lee Oct 17, 2023
4ed3b2b
chore: type 파일 suffix 추가
Creative-Lee Oct 18, 2023
9e40f33
refactor: 가수 디테일과 검색결과 type 분리
Creative-Lee Oct 18, 2023
111c2e5
design: 미디어 쿼리 호버 적용
Creative-Lee Oct 18, 2023
b44dbab
chore: type 파일 이동
Creative-Lee Oct 18, 2023
3873123
refactor: SingersSong type 추가
Creative-Lee Oct 18, 2023
954d871
refactor: 컴포넌트 분리
Creative-Lee Oct 18, 2023
65d0f1a
test: 스토리 추가
Creative-Lee Oct 18, 2023
91a580a
design: 분리된 컴포넌트 색상 추가
Creative-Lee Oct 18, 2023
749c9db
refactor: Spacing 컴포넌트 반응형 디자인 props 추가
Creative-Lee Oct 18, 2023
217b9f4
feat: 노래 클릭 시 듣기페이지 이동 기능 구현
Creative-Lee Oct 18, 2023
3e72fab
refactor: 컴포넌트 분리
Creative-Lee Oct 18, 2023
edc9df0
refactor: 하위 컴포넌트가 title을 가지도록 변경
Creative-Lee Oct 18, 2023
ca30a5f
feat: 검색 결과 페이지 구현
Creative-Lee Oct 18, 2023
c200e1f
chore: 노래 검색결과 픽스쳐 데이터 수정
Creative-Lee Oct 18, 2023
41d756c
feat: 배너 클릭 시 가수페이지 이동 기능 구현
Creative-Lee Oct 18, 2023
9db9be2
chore: 사용하지 않는 import 삭제 및 픽스쳐 데이터 삭제
Creative-Lee Oct 18, 2023
2433293
feat: 배너 호버 시 아이콘 디테일 추가
Creative-Lee Oct 18, 2023
56e7ab5
design: 아이콘 사이즈 축소
Creative-Lee Oct 18, 2023
558733f
chore: 사용하지 않는 훅 삭제
Creative-Lee Oct 19, 2023
5306d46
feat: 메인 케러셀 최근 추가된 노래 리스트로 대체
Creative-Lee Oct 19, 2023
d170a1a
feat: 메인 케러셀 최근 추가된 노래 리스트로 대체
Creative-Lee Oct 19, 2023
dd78f3c
feat: 최근 들은 노래 핸들러 및 fixture 구현
Creative-Lee Oct 19, 2023
5afc6d2
design: 글로벌 스타일 수정
Creative-Lee Oct 19, 2023
6417256
test: singer관련 스토리 추가
Creative-Lee Oct 19, 2023
19518c8
design: title color 추가
Creative-Lee Oct 19, 2023
16fd725
test: 스토리 title prefix 추가로 디렉터리 구분
Creative-Lee Oct 19, 2023
0c108bd
design: 기본 배경색 지정
Creative-Lee Oct 19, 2023
7da9bb3
Merge branch 'main' into feat/#513
Creative-Lee Oct 19, 2023
1597a23
fix: lint 에러 해결
Creative-Lee Oct 19, 2023
537b832
refactor: spacing 컴포넌트 변수명 변경으로 가독성 개선
Creative-Lee Oct 19, 2023
5527cf6
refactor: null 명시
Creative-Lee Oct 19, 2023
fae6725
chore: singer 리모트 함수 파일 위치 변경
Creative-Lee Oct 19, 2023
6afd8b0
refactor: api 명세 변경으로 의미 개선
Creative-Lee Oct 19, 2023
db32c8a
refactor: api 명세 변경 msw 반영 및 singer 핸들러 분리
Creative-Lee Oct 19, 2023
29eae71
refactor: 함수 불리언 화 방식 변경
Creative-Lee Oct 19, 2023
5dbd153
design: 페이지 별 title을 다르게 하여 디자인 구분 되로록 개선
Creative-Lee Oct 19, 2023
0b989a1
refactor: params가 변경되면 데이터를 refetch하도록 변경
Creative-Lee Oct 19, 2023
cd49437
Merge branch 'main' into feat/#513
Creative-Lee Oct 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/src/assets/icon/right-long-arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Meta, StoryObj } from '@storybook/react';

const meta = {
component: SearchBar,
title: 'SearchBar',
title: 'search/SearchBar',
} satisfies Meta<typeof SearchBar>;

export default meta;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof SearchPreviewSheet>;

export default meta;
type Story = StoryObj<typeof SearchPreviewSheet>;

export const Default: Story = {
render: () => <SearchPreviewSheet result={searchedSingerPreview} endSearch={() => {}} />,
};
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
13 changes: 5 additions & 8 deletions frontend/src/features/search/remotes/search.ts
Original file line number Diff line number Diff line change
@@ -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<SingerSearchPreview[]> => {
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<SingerSearchResult[]> => {
export const getSingerSearch = async (query: string): Promise<SingerDetail[]> => {
const encodedQuery = encodeURIComponent(query);
return await fetcher(`/singers?name=${encodedQuery}&search=singer&search=song`, 'GET');
};

export const getSingerDetail = async (singerId: number): Promise<SingerSearchResult> => {
return await fetcher(`/singers/${singerId}`, 'GET');
return await fetcher(`/search?keyword=${encodedQuery}&type=singer&type=song`, 'GET');
};
5 changes: 5 additions & 0 deletions frontend/src/features/search/types/search.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface SingerSearchPreview {
id: number;
singer: string;
profileImageUrl: string;
}
Original file line number Diff line number Diff line change
@@ -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<typeof SingerBanner>;

export default meta;
type Story = StoryObj<typeof SingerBanner>;

export const Default: Story = {
render: () => <SingerBanner {...searchedSingers[0]} />,
};
107 changes: 107 additions & 0 deletions frontend/src/features/singer/components/SingerBanner.tsx
Original file line number Diff line number Diff line change
@@ -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<SingerDetail, 'profileImageUrl' | 'singer' | 'totalSongCount'> {
onClick?: () => void;
}
Comment on lines +6 to +9
Copy link
Collaborator

Choose a reason for hiding this comment

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

💬 type alias 과 비교해 어떤 것이 이해하기 쉬운가용?
저는 여러 타입 연산을 사용하거나 유틸리티 타입을 이용하는 경우 type alias가 해석하기 쉽긴 합니다.

Suggested change
interface SingerBannerProps
extends Pick<SingerDetail, 'profileImageUrl' | 'singer' | 'totalSongCount'> {
onClick?: () => void;
}
type SingerBannerProps = Pick<SingerDetail, 'profileImageUrl' | 'singer' | 'totalSongCount'> & {
onClick?: () => void;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

저도 동감합니다~ 자동 개행되는 부분이 별로 안이쁘긴 하더라고요
개인적으로는 이제 type을 더 선호하게 된것 ? 같기도 합니다 ㅋㅋ
다만, 저희 컨벤션대로 interface를 사용할 수 밖에 없었습니다 ㅎ...


const SingerBanner = ({ profileImageUrl, singer, totalSongCount, onClick }: SingerBannerProps) => {
const clickable = typeof onClick === 'function';

return (
<Container>
<Title>아티스트</Title>
<SingerInfoContainer $align="center" $gap={24} onClick={onClick} $clickable={clickable}>
<ProfileImage src={profileImageUrl} alt="" />
<Flex $direction="column" $gap={16}>
<Name>{singer}</Name>
<SongCount>{`등록된 노래 ${totalSongCount}개`}</SongCount>
</Flex>
</SingerInfoContainer>
</Container>
);
};

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;
}
`;
Original file line number Diff line number Diff line change
@@ -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<typeof SingerSongItem>;

export default meta;
type Story = StoryObj<typeof SingerSongItem>;

export const Default: Story = {
render: () => <SingerSongItem {...song} />,
};
116 changes: 116 additions & 0 deletions frontend/src/features/singer/components/SingerSongItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<SongListItem as="li">
<FlexLink to={`/${ROUTE_PATH.SONG_DETAILS}/${id}/ALL`}>
<AlbumCoverWrapper>
<AlbumCover src={albumCoverUrl} />
</AlbumCoverWrapper>
<FlexInfo $direction="column" $gap={8} $xs={{ $gap: 4 }}>
<SongTitle>{title}</SongTitle>
<Singer>{singer}</Singer>
</FlexInfo>
<VideoLength as="span" $align="center" $justify="flex-end">
{toMinSecText(videoLength)}
</VideoLength>
</FlexLink>
</SongListItem>
);
};

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;
}
`;
Original file line number Diff line number Diff line change
@@ -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<typeof SingerSongList>;

export default meta;
type Story = StoryObj<typeof SingerSongList>;

export const Default: Story = {
render: () => <SingerSongList songs={singerSong.songs} title="곡" />,
};
44 changes: 44 additions & 0 deletions frontend/src/features/singer/components/SingerSongList.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Container>
<Title>{title}</Title>
<SongsItemList as="ol" $direction="column" $gap={12} $align="center">
{songs.map((song) => (
<SingerSongItem key={song.id} {...song} />
))}
</SongsItemList>
</Container>
);
};

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;
}
`;
6 changes: 6 additions & 0 deletions frontend/src/features/singer/remotes/singer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import fetcher from '@/shared/remotes';
import type { SingerDetail } from '../types/singer.type';

export const getSingerDetail = async (singerId: number): Promise<SingerDetail> => {
return await fetcher(`/singers/${singerId}`, 'GET');
};
Loading
Loading