From 9a490d8f0915eb460434eec2fae27729cfebfc5a Mon Sep 17 00:00:00 2001 From: ukkodeveloper Date: Thu, 19 Oct 2023 04:11:10 +0900 Subject: [PATCH 1/9] =?UTF-8?q?chore:=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EB=82=B4=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=EB=90=A0=20svg?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/icon/pin.svg | 2 ++ frontend/src/assets/icon/play-stream.svg | 3 +++ frontend/src/assets/icon/remove.svg | 3 +++ 3 files changed, 8 insertions(+) create mode 100644 frontend/src/assets/icon/pin.svg create mode 100644 frontend/src/assets/icon/play-stream.svg create mode 100644 frontend/src/assets/icon/remove.svg diff --git a/frontend/src/assets/icon/pin.svg b/frontend/src/assets/icon/pin.svg new file mode 100644 index 000000000..60dfa4183 --- /dev/null +++ b/frontend/src/assets/icon/pin.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icon/play-stream.svg b/frontend/src/assets/icon/play-stream.svg new file mode 100644 index 000000000..090a75f92 --- /dev/null +++ b/frontend/src/assets/icon/play-stream.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icon/remove.svg b/frontend/src/assets/icon/remove.svg new file mode 100644 index 000000000..802ca7781 --- /dev/null +++ b/frontend/src/assets/icon/remove.svg @@ -0,0 +1,3 @@ + \ No newline at end of file From dd52fc7506ef37d5451f71e732eef2b6d1b19b70 Mon Sep 17 00:00:00 2001 From: ukkodeveloper Date: Thu, 19 Oct 2023 04:11:41 +0900 Subject: [PATCH 2/9] =?UTF-8?q?fix:=20=EC=98=81=EC=83=81=20=EB=81=8A?= =?UTF-8?q?=EA=B9=80=20=ED=98=84=EC=83=81=EC=9C=BC=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=B4=20debounce=20=EA=B8=B8=EC=9D=B4=20=EC=97=B0=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/features/killingParts/hooks/useWave.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/killingParts/hooks/useWave.ts b/frontend/src/features/killingParts/hooks/useWave.ts index d92fdd09c..89a8fe0ec 100644 --- a/frontend/src/features/killingParts/hooks/useWave.ts +++ b/frontend/src/features/killingParts/hooks/useWave.ts @@ -68,7 +68,7 @@ const useWave = () => { setXPos(null); }; - useDebounceEffect(() => video.seekTo(partStartTime), [partStartTime], 150); + useDebounceEffect(() => video.seekTo(partStartTime), [partStartTime], 300); useDebounceEffect( () => { if (boxRef.current) { From 92afe469889ceead2f48bb04ba86f1a0ca866ff8 Mon Sep 17 00:00:00 2001 From: ukkodeveloper Date: Thu, 19 Oct 2023 04:14:46 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=EB=93=B1=EB=A1=9D=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=9E=84=EC=8B=9C=20=EC=A0=80=EC=9E=A5=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20pin=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../killingParts/components/VideoBadges.tsx | 178 ++++++++++++++++-- .../components/VideoIntervalStepper.tsx | 5 +- .../components/CollectingPartProvider.tsx | 2 + 3 files changed, 173 insertions(+), 12 deletions(-) diff --git a/frontend/src/features/killingParts/components/VideoBadges.tsx b/frontend/src/features/killingParts/components/VideoBadges.tsx index 107212d49..5164b9cbb 100644 --- a/frontend/src/features/killingParts/components/VideoBadges.tsx +++ b/frontend/src/features/killingParts/components/VideoBadges.tsx @@ -1,18 +1,73 @@ -import styled from 'styled-components'; +import { useRef, useState } from 'react'; +import styled, { css, keyframes } from 'styled-components'; import playIcon from '@/assets/icon/fill-play.svg'; import pauseIcon from '@/assets/icon/pause.svg'; +import pinIcon from '@/assets/icon/pin.svg'; +import playStreamIcon from '@/assets/icon/play-stream.svg'; +import removeIcon from '@/assets/icon/remove.svg'; import useCollectingPartContext from '@/features/killingParts/hooks/useCollectingPartContext'; import useVideoPlayerContext from '@/features/youtube/hooks/useVideoPlayerContext'; import Flex from '@/shared/components/Flex/Flex'; +import Spacing from '@/shared/components/Spacing'; import { toMinSecText } from '@/shared/utils/convertTime'; const VideoBadges = () => { - const { partStartTime, isPlayingEntire, toggleEntirePlaying } = useCollectingPartContext(); + const { + partStartTime, + isPlayingEntire, + interval, + setPartStartTime, + setInterval, + toggleEntirePlaying, + } = useCollectingPartContext(); const video = useVideoPlayerContext(); const partStartTimeText = toMinSecText(partStartTime); + const ref = useRef(null); + + // state + const [timeListPinned, setTimeListPinned] = useState< + { partStartTime: number; interval: number; text: string }[] + >([]); + const [activePinnedIndex, setActivePinnedIndex] = useState(null); + + const animationKeyRef = useRef(1); + + const saveTime = () => { + const text = `${toMinSecText(partStartTime)} ~ ${toMinSecText(partStartTime + interval)}`; + if (timeListPinned.find((timePinned) => timePinned.text === text)) return; + + setTimeListPinned((prevTimeList) => [ + { + partStartTime, + interval, + text, + }, + ...prevTimeList, + ]); + + setActivePinnedIndex(0); + if (ref.current) { + ref.current.scrollTo({ + left: 0, + behavior: 'smooth', + }); + } + + animationKeyRef.current += 1; + }; const isPaused = video.playerState === YT.PlayerState.PAUSED; + const deletePin = () => { + if (activePinnedIndex) { + setTimeListPinned(timeListPinned.filter((_, index) => index !== activePinnedIndex)); + } else { + setTimeListPinned(timeListPinned.slice(1)); + } + + setActivePinnedIndex(null); + }; + const videoPlay = () => { if (isPlayingEntire) { video.play(); @@ -25,15 +80,48 @@ const VideoBadges = () => { }; return ( - - {partStartTimeText} - - {'재생 - - - 전체 듣기 - - + <> + + + + {partStartTimeText} + + + 나만의 파트 임시 저장 + + + {'재생 + + + 전체 듣기 + + + + + + {timeListPinned.length !== 0 && ( + + 나만의 파트 임시 저장 삭제하기 + + )} + {timeListPinned.map(({ partStartTime, interval, text }, index) => ( + { + setPartStartTime(partStartTime); + setInterval(interval); + + setActivePinnedIndex(index); + }} + $isActive={index === activePinnedIndex} + $isNew={index === 0 && index === activePinnedIndex} + > + {text} + + ))} + + ); }; export default VideoBadges; @@ -61,3 +149,71 @@ const Badge = styled.span<{ $isActive?: boolean }>` font-size: 16px; } `; + +const StartBadge = styled(Badge)` + margin-right: auto; + letter-spacing: 1px; +`; + +const PinFlex = styled(Flex)` + overflow-x: scroll; + position: relative; +`; + +const slideLeft = keyframes` + from { + opacity: 0; + transform: translateX(-30px); + + } + to { + opacity: 1; + transform: translateX(0); + } +`; + +const slideRight = keyframes` + from { + transform: translateX(-10px); + } + to { + transform: translateX(0); + } +`; + +const PinBadge = styled(Badge)<{ $isActive?: boolean; $isNew?: boolean }>` + background-color: ${({ theme: { color }, $isActive }) => + $isActive ? color.magenta700 : color.disabledBackground}; + + z-index: ${({ $isActive }) => ($isActive ? 1 : 0)}; + opacity: ${({ $isActive }) => ($isActive ? 1 : 0.5)} + + border: none; + width: 100px; + white-space: nowrap; + + color: ${({ theme: { color }, $isActive }) => ($isActive ? color.white : color.black)}; + font-size: 12px; + margin-right: 4px; + border-radius: 4px; + + transition: background-color 0.3s ease-in-out; + animation: ${({ $isNew }) => + $isNew + ? css` + ${slideLeft} 1s forwards + ` + : css` + ${slideRight} 0.5s forwards + `}; + + +`; + +const DeleteBadge = styled(Badge)` + border-radius: 50%; + height: 30px; + min-width: 30px; + padding: 0; + margin-right: 10px; +`; diff --git a/frontend/src/features/killingParts/components/VideoIntervalStepper.tsx b/frontend/src/features/killingParts/components/VideoIntervalStepper.tsx index 923a3dcb3..0e7c721c6 100644 --- a/frontend/src/features/killingParts/components/VideoIntervalStepper.tsx +++ b/frontend/src/features/killingParts/components/VideoIntervalStepper.tsx @@ -21,11 +21,11 @@ const StepperElementStyle = css` min-width: 50px; margin: 0; - padding: 4px 11px; font-weight: 700; text-align: center; + height: 30px; border: none; border-radius: 10px; `; @@ -36,6 +36,9 @@ const ControlButton = styled.button` background-color: ${({ theme: { color } }) => color.secondary}; font-size: 24px; + display: flex; + justify-content: center; + align-items: center; &:active { background-color: ${({ theme: { color } }) => color.disabled}; diff --git a/frontend/src/features/songs/components/CollectingPartProvider.tsx b/frontend/src/features/songs/components/CollectingPartProvider.tsx index e98eeca3d..0448bd43f 100644 --- a/frontend/src/features/songs/components/CollectingPartProvider.tsx +++ b/frontend/src/features/songs/components/CollectingPartProvider.tsx @@ -14,6 +14,7 @@ interface CollectingPartContextProps extends CollectingPartProviderProps { interval: number; isPlayingEntire: boolean; setPartStartTime: React.Dispatch>; + setInterval: React.Dispatch>; increasePartInterval: () => void; decreasePartInterval: () => void; toggleEntirePlaying: () => void; @@ -72,6 +73,7 @@ export const CollectingPartProvider = ({ songVideoId, isPlayingEntire, setPartStartTime, + setInterval, increasePartInterval, decreasePartInterval, toggleEntirePlaying, From c3711ba7cbbf1074c2950cfe450f564e5312210d Mon Sep 17 00:00:00 2001 From: ukkodeveloper Date: Thu, 19 Oct 2023 04:38:22 +0900 Subject: [PATCH 4/9] =?UTF-8?q?refactor:=20pin=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../killingParts/components/VideoBadges.tsx | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/frontend/src/features/killingParts/components/VideoBadges.tsx b/frontend/src/features/killingParts/components/VideoBadges.tsx index 5164b9cbb..a27854e34 100644 --- a/frontend/src/features/killingParts/components/VideoBadges.tsx +++ b/frontend/src/features/killingParts/components/VideoBadges.tsx @@ -25,18 +25,21 @@ const VideoBadges = () => { const ref = useRef(null); // state - const [timeListPinned, setTimeListPinned] = useState< + const [pinList, setPinList] = useState< { partStartTime: number; interval: number; text: string }[] >([]); - const [activePinnedIndex, setActivePinnedIndex] = useState(null); + const [activePinIndex, setActivePinIndex] = useState(null); - const animationKeyRef = useRef(1); + const pinAnimationRef = useRef(1); + const refreshPinAnimation = () => { + pinAnimationRef.current += 1; + }; - const saveTime = () => { + const addPin = () => { const text = `${toMinSecText(partStartTime)} ~ ${toMinSecText(partStartTime + interval)}`; - if (timeListPinned.find((timePinned) => timePinned.text === text)) return; + if (pinList.find((pin) => pin.text === text)) return; - setTimeListPinned((prevTimeList) => [ + setPinList((prevTimeList) => [ { partStartTime, interval, @@ -45,7 +48,7 @@ const VideoBadges = () => { ...prevTimeList, ]); - setActivePinnedIndex(0); + setActivePinIndex(0); if (ref.current) { ref.current.scrollTo({ left: 0, @@ -53,21 +56,27 @@ const VideoBadges = () => { }); } - animationKeyRef.current += 1; + refreshPinAnimation(); }; - const isPaused = video.playerState === YT.PlayerState.PAUSED; - const deletePin = () => { - if (activePinnedIndex) { - setTimeListPinned(timeListPinned.filter((_, index) => index !== activePinnedIndex)); + if (activePinIndex) { + setPinList(pinList.filter((_, index) => index !== activePinIndex)); } else { - setTimeListPinned(timeListPinned.slice(1)); + setPinList(pinList.slice(1)); } - setActivePinnedIndex(null); + setActivePinIndex(null); }; + const playPin = (start: number, interval: number, index: number) => () => { + setPartStartTime(start); + setInterval(interval); + setActivePinIndex(index); + }; + + const isPaused = video.playerState === YT.PlayerState.PAUSED; + const videoPlay = () => { if (isPlayingEntire) { video.play(); @@ -86,7 +95,7 @@ const VideoBadges = () => { {partStartTimeText} - + 나만의 파트 임시 저장 @@ -99,25 +108,20 @@ const VideoBadges = () => { - {timeListPinned.length !== 0 && ( + {pinList.length !== 0 && ( 나만의 파트 임시 저장 삭제하기 )} - {timeListPinned.map(({ partStartTime, interval, text }, index) => ( + {pinList.map((pin, index) => ( { - setPartStartTime(partStartTime); - setInterval(interval); - - setActivePinnedIndex(index); - }} - $isActive={index === activePinnedIndex} - $isNew={index === 0 && index === activePinnedIndex} + onClick={playPin(pin.partStartTime, pin.interval, index)} + $isActive={index === activePinIndex} + $isNew={index === 0 && index === activePinIndex} > - {text} + {pin.text} ))} @@ -206,8 +210,6 @@ const PinBadge = styled(Badge)<{ $isActive?: boolean; $isNew?: boolean }>` : css` ${slideRight} 0.5s forwards `}; - - `; const DeleteBadge = styled(Badge)` From 89b2b9cc3be6f51af5bfbf12e93182242d00b518 Mon Sep 17 00:00:00 2001 From: ukkodeveloper Date: Thu, 19 Oct 2023 06:06:06 +0900 Subject: [PATCH 5/9] =?UTF-8?q?design:=20=EC=83=81=ED=95=98=20=EA=B0=84?= =?UTF-8?q?=EA=B2=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/killingParts/components/VideoController.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/killingParts/components/VideoController.tsx b/frontend/src/features/killingParts/components/VideoController.tsx index 24726625d..eba8bd419 100644 --- a/frontend/src/features/killingParts/components/VideoController.tsx +++ b/frontend/src/features/killingParts/components/VideoController.tsx @@ -6,7 +6,7 @@ import Flex from '@/shared/components/Flex/Flex'; const VideoController = () => { return ( - + 길이 선택 From 69e4614dac99ae56ee03aa0d5a3ab148de42ece6 Mon Sep 17 00:00:00 2001 From: ukkodeveloper Date: Thu, 19 Oct 2023 06:09:59 +0900 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20=ED=95=80=20active=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A5=BC=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A4=91?= =?UTF-8?q?=EC=8B=AC=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../killingParts/components/VideoBadges.tsx | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/frontend/src/features/killingParts/components/VideoBadges.tsx b/frontend/src/features/killingParts/components/VideoBadges.tsx index a27854e34..5d590de8b 100644 --- a/frontend/src/features/killingParts/components/VideoBadges.tsx +++ b/frontend/src/features/killingParts/components/VideoBadges.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import styled, { css, keyframes } from 'styled-components'; import playIcon from '@/assets/icon/fill-play.svg'; import pauseIcon from '@/assets/icon/pause.svg'; @@ -8,9 +8,13 @@ import removeIcon from '@/assets/icon/remove.svg'; import useCollectingPartContext from '@/features/killingParts/hooks/useCollectingPartContext'; import useVideoPlayerContext from '@/features/youtube/hooks/useVideoPlayerContext'; import Flex from '@/shared/components/Flex/Flex'; -import Spacing from '@/shared/components/Spacing'; import { toMinSecText } from '@/shared/utils/convertTime'; +interface Pin { + partStartTime: number; + interval: number; + text: string; +} const VideoBadges = () => { const { partStartTime, @@ -24,11 +28,15 @@ const VideoBadges = () => { const partStartTimeText = toMinSecText(partStartTime); const ref = useRef(null); - // state - const [pinList, setPinList] = useState< - { partStartTime: number; interval: number; text: string }[] - >([]); - const [activePinIndex, setActivePinIndex] = useState(null); + const [pinList, setPinList] = useState([]); + + const activePinIndex = useMemo( + () => + pinList.findIndex((pin) => pin.partStartTime === partStartTime && pin.interval === interval), + [pinList, partStartTime, interval] + ); + + const isPinListEmpty = pinList.length === 0; const pinAnimationRef = useRef(1); const refreshPinAnimation = () => { @@ -36,8 +44,7 @@ const VideoBadges = () => { }; const addPin = () => { - const text = `${toMinSecText(partStartTime)} ~ ${toMinSecText(partStartTime + interval)}`; - if (pinList.find((pin) => pin.text === text)) return; + const text = `${toMinSecText(partStartTime)}`; setPinList((prevTimeList) => [ { @@ -45,10 +52,9 @@ const VideoBadges = () => { interval, text, }, - ...prevTimeList, + ...prevTimeList.filter((pin) => pin.text !== text), ]); - setActivePinIndex(0); if (ref.current) { ref.current.scrollTo({ left: 0, @@ -60,19 +66,16 @@ const VideoBadges = () => { }; const deletePin = () => { - if (activePinIndex) { + if (activePinIndex >= 0) { setPinList(pinList.filter((_, index) => index !== activePinIndex)); } else { setPinList(pinList.slice(1)); } - - setActivePinIndex(null); }; - const playPin = (start: number, interval: number, index: number) => () => { + const playPin = (start: number, interval: number) => () => { setPartStartTime(start); setInterval(interval); - setActivePinIndex(index); }; const isPaused = video.playerState === YT.PlayerState.PAUSED; @@ -95,7 +98,7 @@ const VideoBadges = () => { {partStartTimeText} - + 나만의 파트 임시 저장 @@ -105,10 +108,8 @@ const VideoBadges = () => { 전체 듣기 - - - {pinList.length !== 0 && ( + {!isPinListEmpty && ( 나만의 파트 임시 저장 삭제하기 @@ -117,7 +118,7 @@ const VideoBadges = () => { @@ -140,14 +141,20 @@ const Badge = styled.span<{ $isActive?: boolean }>` padding: 0 10px; font-size: 14px; - color: ${({ theme: { color }, $isActive }) => ($isActive ? color.black : color.white)}; + color: ${({ theme: { color } }) => color.white}; text-align: center; background-color: ${({ theme: { color }, $isActive }) => - $isActive ? color.magenta200 : color.disabled}; + $isActive ? color.magenta700 : color.disabled}; border-radius: 40px; - transition: background-color 0.2s ease-in; + transition: + background-color 0.2s ease-in, + box-shadow 0.2s ease; + + &:active { + box-shadow: 0 0 0 1px inset white; + } @media (min-width: ${({ theme }) => theme.breakPoints.md}) { font-size: 16px; @@ -178,7 +185,7 @@ const slideLeft = keyframes` const slideRight = keyframes` from { - transform: translateX(-10px); + transform: translateX(-15px); } to { transform: translateX(0); @@ -193,7 +200,7 @@ const PinBadge = styled(Badge)<{ $isActive?: boolean; $isNew?: boolean }>` opacity: ${({ $isActive }) => ($isActive ? 1 : 0.5)} border: none; - width: 100px; + width: 50px; white-space: nowrap; color: ${({ theme: { color }, $isActive }) => ($isActive ? color.white : color.black)}; From ad9a4073270beca57fe27f3a4b2ea66f21a01d66 Mon Sep 17 00:00:00 2001 From: ukkodeveloper Date: Thu, 19 Oct 2023 06:21:53 +0900 Subject: [PATCH 7/9] =?UTF-8?q?refactor:=20usePin=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../killingParts/components/VideoBadges.tsx | 76 +++---------------- .../src/features/killingParts/hooks/usePin.ts | 76 +++++++++++++++++++ 2 files changed, 88 insertions(+), 64 deletions(-) create mode 100644 frontend/src/features/killingParts/hooks/usePin.ts diff --git a/frontend/src/features/killingParts/components/VideoBadges.tsx b/frontend/src/features/killingParts/components/VideoBadges.tsx index 5d590de8b..2b96cc587 100644 --- a/frontend/src/features/killingParts/components/VideoBadges.tsx +++ b/frontend/src/features/killingParts/components/VideoBadges.tsx @@ -1,4 +1,3 @@ -import { useMemo, useRef, useState } from 'react'; import styled, { css, keyframes } from 'styled-components'; import playIcon from '@/assets/icon/fill-play.svg'; import pauseIcon from '@/assets/icon/pause.svg'; @@ -6,77 +5,26 @@ import pinIcon from '@/assets/icon/pin.svg'; import playStreamIcon from '@/assets/icon/play-stream.svg'; import removeIcon from '@/assets/icon/remove.svg'; import useCollectingPartContext from '@/features/killingParts/hooks/useCollectingPartContext'; +import usePin from '@/features/killingParts/hooks/usePin'; import useVideoPlayerContext from '@/features/youtube/hooks/useVideoPlayerContext'; import Flex from '@/shared/components/Flex/Flex'; import { toMinSecText } from '@/shared/utils/convertTime'; -interface Pin { - partStartTime: number; - interval: number; - text: string; -} const VideoBadges = () => { + const { partStartTime, isPlayingEntire, toggleEntirePlaying } = useCollectingPartContext(); const { - partStartTime, - isPlayingEntire, - interval, - setPartStartTime, - setInterval, - toggleEntirePlaying, - } = useCollectingPartContext(); + pinList, + isPinListEmpty, + activePinIndex, + ref, + pinAnimationRef, + addPin, + deletePin, + playPin, + } = usePin(); + const video = useVideoPlayerContext(); const partStartTimeText = toMinSecText(partStartTime); - const ref = useRef(null); - - const [pinList, setPinList] = useState([]); - - const activePinIndex = useMemo( - () => - pinList.findIndex((pin) => pin.partStartTime === partStartTime && pin.interval === interval), - [pinList, partStartTime, interval] - ); - - const isPinListEmpty = pinList.length === 0; - - const pinAnimationRef = useRef(1); - const refreshPinAnimation = () => { - pinAnimationRef.current += 1; - }; - - const addPin = () => { - const text = `${toMinSecText(partStartTime)}`; - - setPinList((prevTimeList) => [ - { - partStartTime, - interval, - text, - }, - ...prevTimeList.filter((pin) => pin.text !== text), - ]); - - if (ref.current) { - ref.current.scrollTo({ - left: 0, - behavior: 'smooth', - }); - } - - refreshPinAnimation(); - }; - - const deletePin = () => { - if (activePinIndex >= 0) { - setPinList(pinList.filter((_, index) => index !== activePinIndex)); - } else { - setPinList(pinList.slice(1)); - } - }; - - const playPin = (start: number, interval: number) => () => { - setPartStartTime(start); - setInterval(interval); - }; const isPaused = video.playerState === YT.PlayerState.PAUSED; diff --git a/frontend/src/features/killingParts/hooks/usePin.ts b/frontend/src/features/killingParts/hooks/usePin.ts new file mode 100644 index 000000000..f20332692 --- /dev/null +++ b/frontend/src/features/killingParts/hooks/usePin.ts @@ -0,0 +1,76 @@ +import { useMemo, useRef, useState } from 'react'; +import useCollectingPartContext from '@/features/killingParts/hooks/useCollectingPartContext'; +import { toMinSecText } from '@/shared/utils/convertTime'; + +interface Pin { + partStartTime: number; + interval: number; + text: string; +} + +const usePin = () => { + const { partStartTime, interval, setPartStartTime, setInterval } = useCollectingPartContext(); + const [pinList, setPinList] = useState([]); + const ref = useRef(null); + + const activePinIndex = useMemo( + () => + pinList.findIndex((pin) => pin.partStartTime === partStartTime && pin.interval === interval), + [pinList, partStartTime, interval] + ); + + const isPinListEmpty = pinList.length === 0; + + const pinAnimationRef = useRef(1); + const refreshPinAnimation = () => { + pinAnimationRef.current += 1; + }; + + const addPin = () => { + const text = `${toMinSecText(partStartTime)}`; + + setPinList((prevTimeList) => [ + { + partStartTime, + interval, + text, + }, + ...prevTimeList.filter((pin) => pin.text !== text), + ]); + + if (ref.current) { + ref.current.scrollTo({ + left: 0, + behavior: 'smooth', + }); + } + + refreshPinAnimation(); + }; + + const deletePin = () => { + if (activePinIndex >= 0) { + setPinList(pinList.filter((_, index) => index !== activePinIndex)); + } else { + setPinList(pinList.slice(1)); + } + }; + + const playPin = (start: number, interval: number) => () => { + setPartStartTime(start); + setInterval(interval); + }; + + return { + pinList, + isPinListEmpty, + activePinIndex, + pinAnimationRef, + ref, + addPin, + deletePin, + playPin, + }; +}; + +export default usePin; From 44fd17a86750feeae751c9df6c986837693ea5b6 Mon Sep 17 00:00:00 2001 From: ukkodeveloper Date: Thu, 19 Oct 2023 06:44:31 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=EB=93=B1=EB=A1=9D=20=EC=8B=9C?= =?UTF-8?q?=EC=97=90=20=EC=9D=B4=EC=A0=84=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EB=B0=8F=20=EB=AA=A8=EB=8B=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../killingParts/components/RegisterPart.tsx | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/frontend/src/features/killingParts/components/RegisterPart.tsx b/frontend/src/features/killingParts/components/RegisterPart.tsx index dbe469708..0d8359b1e 100644 --- a/frontend/src/features/killingParts/components/RegisterPart.tsx +++ b/frontend/src/features/killingParts/components/RegisterPart.tsx @@ -1,3 +1,4 @@ +import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import { useAuthContext } from '@/features/auth/components/AuthProvider'; import useCollectingPartContext from '@/features/killingParts/hooks/useCollectingPartContext'; @@ -5,55 +6,50 @@ import { usePostKillingPart } from '@/features/killingParts/remotes/usePostKilli import useVideoPlayerContext from '@/features/youtube/hooks/useVideoPlayerContext'; import useModal from '@/shared/components/Modal/hooks/useModal'; import Modal from '@/shared/components/Modal/Modal'; -import useToastContext from '@/shared/components/Toast/hooks/useToastContext'; +import Spacing from '@/shared/components/Spacing'; import { toPlayingTimeText } from '@/shared/utils/convertTime'; -import copyClipboard from '@/shared/utils/copyClipBoard'; const RegisterPart = () => { const { isOpen, openModal, closeModal } = useModal(); - const { showToast } = useToastContext(); const { user } = useAuthContext(); - const { interval, partStartTime, songId, songVideoId } = useCollectingPartContext(); + const { interval, partStartTime, songId } = useCollectingPartContext(); const video = useVideoPlayerContext(); const { createKillingPart } = usePostKillingPart(); + const navigate = useNavigate(); // 현재 useMutation 훅이 response 객체를 리턴하지 않고 내부적으로 처리합니다. // 때문에 컴포넌트 단에서 createKillingPart 성공 여부에 따라 등록 완료 만료를 처리를 할 수 없어요! - // 현재 비로그인 시에 등록을 누르면 두 개의 모달이 뜹니다. + // 현재 비로그인 시에 등록을 누르면 두 개의 모달이 뜹니다.정 const submitKillingPart = async () => { video.pause(); await createKillingPart(songId, { startSecond: partStartTime, length: interval }); - openModal(); + navigate(-1); }; const voteTimeText = toPlayingTimeText(partStartTime, partStartTime + interval); - const copyPartVideoUrl = async () => { - await copyClipboard(`https://www.youtube.com/watch?v=${songVideoId}&t=${partStartTime}s`); - closeModal(); - showToast('클립보드에 영상링크가 복사되었습니다.'); - }; - return ( <> - + 등록 - {user?.nickname}님의 - 킬링파트 등록을 완료했습니다. + {user?.nickname}님의 파트 저장 - {voteTimeText} - 파트를 공유해 보세요😀 + + {voteTimeText} + + + 나만의 파트로 등록하시겠습니까? - 확인 + 취소 - - 공유하기 + + 등록 @@ -125,3 +121,11 @@ const ButtonContainer = styled.div` gap: 16px; width: 100%; `; + +const Part = styled.span` + background-color: ${({ theme: { color } }) => color.disabled}; + border-radius: 10px; + padding: 6px 11px; + letter-spacing: 1px; + color: white; +`; From 1f22ecaca04d73b45c4d830ead04e44d710522ba Mon Sep 17 00:00:00 2001 From: ukkodeveloper Date: Thu, 19 Oct 2023 07:02:23 +0900 Subject: [PATCH 9/9] =?UTF-8?q?design:=20pin=20=EB=94=94=EC=9E=90=EC=9D=B8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../killingParts/components/VideoBadges.tsx | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/frontend/src/features/killingParts/components/VideoBadges.tsx b/frontend/src/features/killingParts/components/VideoBadges.tsx index 2b96cc587..3baba8cce 100644 --- a/frontend/src/features/killingParts/components/VideoBadges.tsx +++ b/frontend/src/features/killingParts/components/VideoBadges.tsx @@ -22,12 +22,10 @@ const VideoBadges = () => { deletePin, playPin, } = usePin(); - const video = useVideoPlayerContext(); const partStartTimeText = toMinSecText(partStartTime); const isPaused = video.playerState === YT.PlayerState.PAUSED; - const videoPlay = () => { if (isPlayingEntire) { video.play(); @@ -62,23 +60,36 @@ const VideoBadges = () => { 나만의 파트 임시 저장 삭제하기 )} - {pinList.map((pin, index) => ( - - {pin.text} - - ))} + + {pinList.map((pin, index) => ( + + {pin.text} + + ))} + ); }; export default VideoBadges; +const PinFlex = styled(Flex)` + //overflow-x: scroll; + //position: relative; + width: 100%; +`; + +const PinInner = styled(Flex)` + overflow-x: scroll; + width: calc(100% - 44px); +`; + const Badge = styled.span<{ $isActive?: boolean }>` display: flex; align-items: center; @@ -114,12 +125,7 @@ const StartBadge = styled(Badge)` letter-spacing: 1px; `; -const PinFlex = styled(Flex)` - overflow-x: scroll; - position: relative; -`; - -const slideLeft = keyframes` +const slideFirstItem = keyframes` from { opacity: 0; transform: translateX(-30px); @@ -131,7 +137,7 @@ const slideLeft = keyframes` } `; -const slideRight = keyframes` +const slideRestItems = keyframes` from { transform: translateX(-15px); } @@ -151,19 +157,19 @@ const PinBadge = styled(Badge)<{ $isActive?: boolean; $isNew?: boolean }>` width: 50px; white-space: nowrap; - color: ${({ theme: { color }, $isActive }) => ($isActive ? color.white : color.black)}; + color: black; font-size: 12px; margin-right: 4px; border-radius: 4px; - transition: background-color 0.3s ease-in-out; + transition: background-color 0.5s ease-in-out; animation: ${({ $isNew }) => $isNew ? css` - ${slideLeft} 1s forwards + ${slideFirstItem} 1s forwards ` : css` - ${slideRight} 0.5s forwards + ${slideRestItems} 0.5s forwards `}; `;