-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* test: 마이페이지에서 마이 파트 msw 추가 및 좋아요한 파트 엔드포인트 수정 * feat: LikePartItem 컴포넌트를 분리 - 컴포넌트 이름 변경: PartItem (좋아요와 마이파트 모두 사용) * feat: PartList 구현 - 탭 전환시 스크롤이 상단으로 이동하도록 구현 * feat: PartList가 포함된 탭을 설정하는 MyPartList 컴포넌트 구현 * feat: 좋아요한 킬링파트에서 좋아요와 마이파트가 포함된 MyPartList를 보여주도록 변경 * test: songEntries mock 데이터에 myPart 추가 및 songHandler에 마이 파트를 삭제하는 msw 추가 * feat: 마이파트 삭제를 확인하는 MyPartModal 컴포넌트 구현 * feat: 마이파트 등록과 제거 구현 * test: 스토리북에 myPart 추가 및 TimerProvider 추가 - 재생되지 않는 버그를 주석으로 남김 * style: 불필요한 코드 삭제 * refactor: 마이페이지에서 기본 자기소개 부분 분리 및 함수 구현 * design: 자기소개가 3줄 정도만 보이도록 수정 - 모바일 기준 대략(72자) * refactor: 좋아요한 파트와 마이파트를 가져오는 fetch 로직 분리 * refactor: 마이파트 제거 fetch 로직 분리 * design: 좋아요한 킬링파트와 나의 킬링파트를 포함하는 탭의 색상을 더 진하게 변경 * refactor: PartItem의 props를 스프레드로 간단하게 표현 * refactor: PartItem에서 상수 사용 및 변수 분리 * fix: fetch의 method를 대문자로 수정 * design: PartList의 scrollTop을 일정하게 유지시키기 위한 스타일 수정 * refactor: PartList의 li 태그를 PartItem으로 이동 - 스타일드 컴포넌트 이름 변경 - 스타일 및 태그 수정 * refactor: 마이페이지의 탭 타입 분리 * style: 함수명 및 스타일드 컴포넌트 명 변경 * design: font-weight 700으로 수정 * refactor: 상수 const assertion * refactor: PartItem에 rank prop 삭제 * refactor: 탭과 보여줄 컨텐츠를 동적으로 늘릴 수 있도록 수정 * feat: 킬링파트 트랙과 마이페이지에서 탭의 접근성 향상 * fix: 킬링파트 id와 마이파트 id가 겹쳐 재생할 수 없는 현상 수정 * fix 마이 페이지의 내 킬링파트 조회 api 엔드포인트 수정 * test: msw 데이터 fixture로 분리 * fix: 반복 듣기가 되지 않는 현상 수정 * fix: 잘못된 경로 수정 * fix: myPart를 memberPart로 변경 * test: msw 데이터 필드 수정 * feat: 마이파트에 공유하는 버튼 추가 * test: 마이페이지에서 마이 파트 msw에 songVideoId 필드 추가 * feat: 마이페이지에서 나의 킬링파트는 유튜브 링크 공유로 변경 * fix: 마이페이지의 좋아요한 파트 공유 링크 수정
- Loading branch information
1 parent
280182e
commit 749ec05
Showing
25 changed files
with
950 additions
and
335 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { useState } from 'react'; | ||
import styled from 'styled-components'; | ||
import Spacing from '@/shared/components/Spacing'; | ||
import useFetch from '@/shared/hooks/useFetch'; | ||
import { getLikeParts, getMyParts } from '../remotes/myPage'; | ||
import PartList from './PartList'; | ||
import type { MyPageTab } from '../types/myPage'; | ||
import type { KillingPart, SongDetail } from '@/shared/types/song'; | ||
|
||
export type LikeKillingPart = Pick< | ||
SongDetail, | ||
'title' | 'singer' | 'albumCoverUrl' | 'songVideoId' | ||
> & | ||
Pick<KillingPart, 'start' | 'end'> & { | ||
songId: number; | ||
partId: number; | ||
}; | ||
|
||
const MyPartList = () => { | ||
const [tab, setTab] = useState<MyPageTab>('Like'); | ||
|
||
const { data: likes } = useFetch<LikeKillingPart[]>(getLikeParts); | ||
const { data: myParts } = useFetch<LikeKillingPart[]>(getMyParts); | ||
|
||
if (!likes || !myParts) { | ||
return null; | ||
} | ||
|
||
const partTabItems: { tab: MyPageTab; title: string; parts: LikeKillingPart[] }[] = [ | ||
{ tab: 'Like', title: '좋아요 한 킬링파트', parts: likes }, | ||
{ tab: 'MyKillingPart', title: '내 킬링파트', parts: myParts }, | ||
]; | ||
|
||
const pressEnterChangeTab = (tab: MyPageTab) => (event: React.KeyboardEvent<HTMLLIElement>) => { | ||
if (event.key === 'Enter') { | ||
setTab(tab); | ||
} | ||
}; | ||
|
||
return ( | ||
<> | ||
<Tabs role="tablist"> | ||
{partTabItems.map((option) => ( | ||
<TabItem | ||
key={option.tab} | ||
role="tab" | ||
aria-selected={tab === option.tab} | ||
$isActive={tab === option.tab} | ||
onClick={() => setTab(option.tab)} | ||
onKeyDown={pressEnterChangeTab(option.tab)} | ||
tabIndex={0} | ||
> | ||
{option.title} | ||
</TabItem> | ||
))} | ||
</Tabs> | ||
|
||
<Spacing direction="vertical" size={24} /> | ||
|
||
{partTabItems.map((option) => ( | ||
<PartList | ||
key={option.tab} | ||
parts={option.parts} | ||
isShow={tab === option.tab} | ||
tab={option.tab} | ||
/> | ||
))} | ||
</> | ||
); | ||
}; | ||
|
||
export default MyPartList; | ||
|
||
const Tabs = styled.ul` | ||
position: sticky; | ||
top: ${({ theme }) => theme.headerHeight.desktop}; | ||
display: flex; | ||
background-color: ${({ theme: { color } }) => color.black}; | ||
@media (max-width: ${({ theme }) => theme.breakPoints.xs}) { | ||
top: ${({ theme }) => theme.headerHeight.mobile}; | ||
} | ||
@media (max-width: ${({ theme }) => theme.breakPoints.xxs}) { | ||
top: ${({ theme }) => theme.headerHeight.xxs}; | ||
} | ||
`; | ||
|
||
const TabItem = styled.li<{ $isActive?: boolean }>` | ||
cursor: pointer; | ||
flex-shrink: 1; | ||
width: 100%; | ||
padding: 15px 20px; | ||
color: ${({ $isActive, theme: { color } }) => | ||
$isActive ? color.white : color.disabledBackground}; | ||
text-align: center; | ||
border-bottom: 2px solid | ||
${({ $isActive, theme: { color } }) => ($isActive ? color.white : color.disabled)}; | ||
transition: | ||
color 0.3s, | ||
border 0.3s; | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import { useNavigate } from 'react-router-dom'; | ||
import styled from 'styled-components'; | ||
import link from '@/assets/icon/link.svg'; | ||
import shook from '@/assets/icon/shook.svg'; | ||
import { useAuthContext } from '@/features/auth/components/AuthProvider'; | ||
import Thumbnail from '@/features/songs/components/Thumbnail'; | ||
import Spacing from '@/shared/components/Spacing'; | ||
import useToastContext from '@/shared/components/Toast/hooks/useToastContext'; | ||
import { GA_ACTIONS, GA_CATEGORIES } from '@/shared/constants/GAEventName'; | ||
import ROUTE_PATH from '@/shared/constants/path'; | ||
import sendGAEvent from '@/shared/googleAnalytics/sendGAEvent'; | ||
import { secondsToMinSec, toPlayingTimeText } from '@/shared/utils/convertTime'; | ||
import copyClipboard from '@/shared/utils/copyClipBoard'; | ||
import type { LikeKillingPart } from './MyPartList'; | ||
import type { MyPageTab } from '../types/myPage'; | ||
|
||
const { BASE_URL } = process.env; | ||
|
||
type PartItemProps = LikeKillingPart & { | ||
tab: MyPageTab; | ||
}; | ||
|
||
const PartItem = ({ | ||
songId, | ||
albumCoverUrl, | ||
title, | ||
singer, | ||
start, | ||
end, | ||
songVideoId, | ||
tab, | ||
}: PartItemProps) => { | ||
const { showToast } = useToastContext(); | ||
const { user } = useAuthContext(); | ||
const navigate = useNavigate(); | ||
|
||
const shareUrl: React.MouseEventHandler = (e) => { | ||
e.stopPropagation(); | ||
sendGAEvent({ | ||
action: GA_ACTIONS.COPY_URL, | ||
category: GA_CATEGORIES.MY_PAGE, | ||
memberId: user?.memberId, | ||
}); | ||
|
||
const shareLink = | ||
tab === 'Like' | ||
? `${BASE_URL?.replace('/api', '')}/${ROUTE_PATH.SONG_DETAILS}/${songId}/ALL` | ||
: `https://youtu.be/${songVideoId}?start=${start}`; | ||
|
||
copyClipboard(shareLink); | ||
showToast('클립보드에 영상링크가 복사되었습니다.'); | ||
}; | ||
|
||
const goToSongDetailListPage = () => { | ||
navigate(`/${ROUTE_PATH.SONG_DETAILS}/${songId}/ALL`); | ||
}; | ||
|
||
const { minute: startMin, second: startSec } = secondsToMinSec(start); | ||
const { minute: endMin, second: endSec } = secondsToMinSec(end); | ||
|
||
return ( | ||
<PartItemGrid onClick={goToSongDetailListPage}> | ||
<Thumbnail src={albumCoverUrl} alt={`${title}-${singer}`} /> | ||
<SongTitle>{title}</SongTitle> | ||
<Singer>{singer}</Singer> | ||
<TimeWrapper> | ||
<Shook src={shook} alt="" /> | ||
<Spacing direction="horizontal" size={4} /> | ||
<p | ||
tabIndex={0} | ||
aria-label={`킬링파트 구간 ${startMin}분 ${startSec}초부터 ${endMin}분 ${endSec}초`} | ||
> | ||
{toPlayingTimeText(start, end)} | ||
</p> | ||
<Spacing direction="horizontal" size={10} /> | ||
</TimeWrapper> | ||
<ShareButton onClick={shareUrl}> | ||
<Share src={link} alt="영상 링크 공유하기" /> | ||
</ShareButton> | ||
</PartItemGrid> | ||
); | ||
}; | ||
|
||
const PartItemGrid = styled.li` | ||
cursor: pointer; | ||
display: grid; | ||
grid-template: | ||
'thumbnail title _' 26px | ||
'thumbnail singer share' 26px | ||
'thumbnail info share' 18px | ||
/ 70px auto 26px; | ||
column-gap: 8px; | ||
width: 100%; | ||
padding: 6px 10px; | ||
color: ${({ theme: { color } }) => color.white}; | ||
text-align: start; | ||
&:hover, | ||
&:focus { | ||
background-color: ${({ theme }) => theme.color.secondary}; | ||
} | ||
`; | ||
|
||
const SongTitle = styled.div` | ||
overflow: hidden; | ||
grid-area: title; | ||
font-size: 16px; | ||
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; | ||
`; | ||
|
||
const TimeWrapper = styled.div` | ||
display: flex; | ||
grid-area: info; | ||
font-size: 14px; | ||
font-weight: 700; | ||
color: ${({ theme: { color } }) => color.primary}; | ||
letter-spacing: 1px; | ||
`; | ||
|
||
const ShareButton = styled.button` | ||
grid-area: share; | ||
width: 26px; | ||
height: 26px; | ||
`; | ||
|
||
const Share = styled.img` | ||
padding: 2px; | ||
background-color: white; | ||
border-radius: 50%; | ||
`; | ||
|
||
const Shook = styled.img` | ||
width: 16px; | ||
height: 18px; | ||
border-radius: 50%; | ||
`; | ||
|
||
export default PartItem; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { useEffect } from 'react'; | ||
import styled from 'styled-components'; | ||
import PartItem from './PartItem'; | ||
import type { LikeKillingPart } from './MyPartList'; | ||
import type { MyPageTab } from '../types/myPage'; | ||
|
||
interface PartListProps { | ||
parts: LikeKillingPart[]; | ||
isShow: boolean; | ||
tab: MyPageTab; | ||
} | ||
|
||
const PART_LIST_SCROLL_TOP = 180; | ||
|
||
const PartList = ({ parts, isShow, tab }: PartListProps) => { | ||
useEffect(() => { | ||
if (window.scrollY > PART_LIST_SCROLL_TOP) { | ||
window.scrollTo({ top: PART_LIST_SCROLL_TOP, behavior: 'smooth' }); | ||
} | ||
}, [isShow]); | ||
|
||
if (!isShow) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<PartListContainer> | ||
{parts.map((part) => ( | ||
<PartItem key={part.partId} tab={tab} {...part} /> | ||
))} | ||
</PartListContainer> | ||
); | ||
}; | ||
|
||
export default PartList; | ||
|
||
const PartListContainer = styled.ol` | ||
display: flex; | ||
flex-direction: column; | ||
gap: 12px; | ||
align-items: flex-start; | ||
width: 100%; | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export const introductions = [ | ||
'아무 노래나 일단 틀어', | ||
'또 물보라를 일으켜', | ||
'난 내가 말야, 스무살쯤엔 요절할 천재일줄만 알고', | ||
'You make me feel special', | ||
'우린 참 별나고 이상한 사이야', | ||
'난 차라리 꽉 눌러붙을래, 날 재촉한다면', | ||
'So lovely day so lovely Errday with you so lovely', | ||
'Weight of the world on your shoulders', | ||
] as const; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import fetcher from '@/shared/remotes'; | ||
|
||
export const deleteMemberParts = (partId: number) => fetcher(`/member-parts/${partId}`, 'DELETE'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import fetcher from '@/shared/remotes'; | ||
|
||
export const getLikeParts = () => fetcher('/my-page/like-parts', 'GET'); | ||
|
||
export const getMyParts = () => fetcher('/my-page/my-parts', 'GET'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export type MyPageTab = 'Like' | 'MyKillingPart'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { introductions } from '../constants/introductions'; | ||
|
||
const getRandomIntroduction = (index = Math.floor(Math.random() * introductions.length)) => { | ||
return introductions[index]; | ||
}; | ||
|
||
export default getRandomIntroduction; |
Oops, something went wrong.