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/#506 마이파트를 등록하는 기능을 추가한다 #507

Merged
merged 39 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
21657ab
test: 마이페이지에서 마이 파트 msw 추가 및 좋아요한 파트 엔드포인트 수정
cruelladevil Oct 15, 2023
ef5effb
feat: LikePartItem 컴포넌트를 분리
cruelladevil Oct 15, 2023
1e6ca39
feat: PartList 구현
cruelladevil Oct 15, 2023
1903827
feat: PartList가 포함된 탭을 설정하는 MyPartList 컴포넌트 구현
cruelladevil Oct 15, 2023
3ee7f92
feat: 좋아요한 킬링파트에서 좋아요와 마이파트가 포함된 MyPartList를 보여주도록 변경
cruelladevil Oct 15, 2023
4909fe9
test: songEntries mock 데이터에 myPart 추가 및 songHandler에 마이 파트를 삭제하는 msw 추가
cruelladevil Oct 15, 2023
160dfae
feat: 마이파트 삭제를 확인하는 MyPartModal 컴포넌트 구현
cruelladevil Oct 15, 2023
70c242c
feat: 마이파트 등록과 제거 구현
cruelladevil Oct 15, 2023
f4e6bcc
test: 스토리북에 myPart 추가 및 TimerProvider 추가
cruelladevil Oct 15, 2023
755cbf9
style: 불필요한 코드 삭제
cruelladevil Oct 15, 2023
78e77fb
refactor: 마이페이지에서 기본 자기소개 부분 분리 및 함수 구현
cruelladevil Oct 15, 2023
6a19f5d
design: 자기소개가 3줄 정도만 보이도록 수정
cruelladevil Oct 15, 2023
9a2b2e7
refactor: 좋아요한 파트와 마이파트를 가져오는 fetch 로직 분리
cruelladevil Oct 15, 2023
7445fe0
refactor: 마이파트 제거 fetch 로직 분리
cruelladevil Oct 15, 2023
dea67f7
design: 좋아요한 킬링파트와 나의 킬링파트를 포함하는 탭의 색상을 더 진하게 변경
cruelladevil Oct 17, 2023
7933937
refactor: PartItem의 props를 스프레드로 간단하게 표현
cruelladevil Oct 17, 2023
28956d8
refactor: PartItem에서 상수 사용 및 변수 분리
cruelladevil Oct 17, 2023
726937b
fix: fetch의 method를 대문자로 수정
cruelladevil Oct 17, 2023
0534145
design: PartList의 scrollTop을 일정하게 유지시키기 위한 스타일 수정
cruelladevil Oct 17, 2023
2538e6d
refactor: PartList의 li 태그를 PartItem으로 이동
cruelladevil Oct 17, 2023
dabb0e2
refactor: 마이페이지의 탭 타입 분리
cruelladevil Oct 17, 2023
1eb6d78
style: 함수명 및 스타일드 컴포넌트 명 변경
cruelladevil Oct 17, 2023
4be4363
design: font-weight 700으로 수정
cruelladevil Oct 17, 2023
bc88c8b
refactor: 상수 const assertion
cruelladevil Oct 17, 2023
f9b85f1
refactor: PartItem에 rank prop 삭제
cruelladevil Oct 17, 2023
c907719
refactor: 탭과 보여줄 컨텐츠를 동적으로 늘릴 수 있도록 수정
cruelladevil Oct 17, 2023
59c967d
feat: 킬링파트 트랙과 마이페이지에서 탭의 접근성 향상
cruelladevil Oct 17, 2023
12208bc
fix: 킬링파트 id와 마이파트 id가 겹쳐 재생할 수 없는 현상 수정
cruelladevil Oct 18, 2023
15449dd
fix 마이 페이지의 내 킬링파트 조회 api 엔드포인트 수정
cruelladevil Oct 18, 2023
9b35f7d
test: msw 데이터 fixture로 분리
cruelladevil Oct 18, 2023
b18023b
fix: 반복 듣기가 되지 않는 현상 수정
cruelladevil Oct 18, 2023
b8622c0
fix: 잘못된 경로 수정
cruelladevil Oct 18, 2023
abba1df
fix: myPart를 memberPart로 변경
cruelladevil Oct 18, 2023
505d557
test: msw 데이터 필드 수정
cruelladevil Oct 18, 2023
908d681
feat: 마이파트에 공유하는 버튼 추가
cruelladevil Oct 18, 2023
bebe44f
test: 마이페이지에서 마이 파트 msw에 songVideoId 필드 추가
cruelladevil Oct 18, 2023
eb9d592
feat: 마이페이지에서 나의 킬링파트는 유튜브 링크 공유로 변경
cruelladevil Oct 18, 2023
725ea97
fix: 마이페이지의 좋아요한 파트 공유 링크 수정
cruelladevil Oct 19, 2023
ce494ef
Merge branch 'main' into feat/#506
cruelladevil 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
59 changes: 59 additions & 0 deletions frontend/src/assets/icon/trash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
107 changes: 107 additions & 0 deletions frontend/src/features/member/components/MyPartList.tsx
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;
cruelladevil marked this conversation as resolved.
Show resolved Hide resolved
top: ${({ theme }) => theme.headerHeight.desktop};
Comment on lines +75 to +76
Copy link
Collaborator

Choose a reason for hiding this comment

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

저 이거 갑자기 생각났는데,
모바일에서 sticky 적용한 컴포넌트가 1px만큼 가느다랗게 뒷 배경이 보이는 이슈가 있거든요?

top: 49px or margin-top: -1px; 이거 주면 사라지긴 합니다 ㅋㅋ
저도 적용해야겠는데용

Copy link
Collaborator

Choose a reason for hiding this comment

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

image
보이시나요? 대략 이런 이슈입니당ㅋㅋㅋ
구글에 쳐도 쫌 많이 나오더라고여

Copy link
Collaborator Author

@cruelladevil cruelladevil Oct 17, 2023

Choose a reason for hiding this comment

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

💬 해당 이슈가 fixed라고 하는 것 같은데 1px 올려야될까요?
저는 일단 개발자 도구의 device toolbar를 켰을 때만 일어나는 것 같아요.
혹시 실사용하면서 해당 버그가 발생하는가용?

https://bugs.chromium.org/p/chromium/issues/detail?id=1076036

Copy link
Collaborator

Choose a reason for hiding this comment

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

모바일 개발자 도구에서 확인되는 문제였던걸로! 대화를 통해 해결!

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;
`;
154 changes: 154 additions & 0 deletions frontend/src/features/member/components/PartItem.tsx
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;
44 changes: 44 additions & 0 deletions frontend/src/features/member/components/PartList.tsx
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%;
`;
10 changes: 10 additions & 0 deletions frontend/src/features/member/constants/introductions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const introductions = [
'아무 노래나 일단 틀어',
'또 물보라를 일으켜',
'난 내가 말야, 스무살쯤엔 요절할 천재일줄만 알고',
Copy link
Collaborator

Choose a reason for hiding this comment

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

분리 굿이용~
그런데 가끔 2줄이 되는 노랫말이 있네요. 그래서 테스트를 한번 해봤습니당

image


+++ 로그인 했다고 가정하고 html로 직접 돔을 추가해서 위에 닉네임이 뜨도록 해봤는데용
코난의 탭 선택시, 스무스 스크롤 148 이동시키는게 살짝 의도와 달라지네요


닉네임 없을때: 스무스 하게 딱 탭 경계선에 걸쳐져서, 스크롤 하면 탭고정 상태로 바로 내려감

스크롤 정상


닉네임 두고 테스트 해봤을 때: 스무스하게 탭 경계선 살짝 위로 더 올라감
다시 스크롤 하면 탭이 살짝 내려가고 다시 내려감

스크롤


말로하면 이해가 안될듯해서 영상 남깁니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

커밋된 내용의 ProfileFlex 컴포넌트의 높이를 일정하게 유지시켜서
scrollTop을 고정적으로 운용할 수 있도록 스타일을 수정했습니다!
높이의 기준은 자기 소개 3줄 기준입니다.

✍️ 수정 커밋 - 0534145

'You make me feel special',
'우린 참 별나고 이상한 사이야',
'난 차라리 꽉 눌러붙을래, 날 재촉한다면',
'So lovely day so lovely Errday with you so lovely',
'Weight of the world on your shoulders',
] as const;
3 changes: 3 additions & 0 deletions frontend/src/features/member/remotes/memberParts.ts
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');
5 changes: 5 additions & 0 deletions frontend/src/features/member/remotes/myPage.ts
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');
1 change: 1 addition & 0 deletions frontend/src/features/member/types/myPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type MyPageTab = 'Like' | 'MyKillingPart';
7 changes: 7 additions & 0 deletions frontend/src/features/member/utils/getRandomIntroduction.ts
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;
Loading