diff --git a/frontend/src/features/songs/components/IntervalInput.stories.tsx b/frontend/src/features/songs/components/IntervalInput.stories.tsx deleted file mode 100644 index 3e9c6f78f..000000000 --- a/frontend/src/features/songs/components/IntervalInput.stories.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useState } from 'react'; -import { VideoPlayerProvider } from '@/features/youtube/components/VideoPlayerProvider'; -import IntervalInput from './IntervalInput'; -import { VoteInterfaceProvider } from './VoteInterfaceProvider'; -import type { Meta, StoryObj } from '@storybook/react'; - -const meta = { - component: IntervalInput, - title: 'IntervalInput', - decorators: [ - (Story) => ( - - - - - - ), - ], -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -const TestIntervalInput = () => { - const [errorMessage, setErrorMessage] = useState(''); - - const onChangeErrorMessage = (message: string) => { - setErrorMessage(message); - }; - - return ; -}; - -export const Default = { - render: () => , -} satisfies Story; diff --git a/frontend/src/features/songs/components/IntervalInput.tsx b/frontend/src/features/songs/components/IntervalInput.tsx deleted file mode 100644 index 7a3a22f56..000000000 --- a/frontend/src/features/songs/components/IntervalInput.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { useState } from 'react'; -import { css, styled } from 'styled-components'; -import { secondsToMinSec } from '@/shared/utils/convertTime'; -import { isValidMinSec } from '@/shared/utils/validateTime'; -import ERROR_MESSAGE from '../constants/errorMessage'; -import useVoteInterfaceContext from '../hooks/useVoteInterfaceContext'; -import { isInputName } from '../types/IntervalInput.type'; -import type { IntervalInputType } from '../types/IntervalInput.type'; - -export interface IntervalInputProps { - errorMessage: string; - onChangeErrorMessage: (message: string) => void; -} - -const IntervalInput = ({ errorMessage, onChangeErrorMessage }: IntervalInputProps) => { - const { interval, partStartTime, videoLength, updatePartStartTime } = useVoteInterfaceContext(); - - const [activeInput, setActiveInput] = useState(null); - - const partEndTime = partStartTime + interval; - const { minute: startMinute, second: startSecond } = secondsToMinSec(partStartTime); - const { minute: endMinute, second: endSecond } = secondsToMinSec(partEndTime); - - const onChangeIntervalStart: React.ChangeEventHandler = ({ - currentTarget: { name: timeUnit, value, valueAsNumber }, - }) => { - if (!isValidMinSec(value)) { - onChangeErrorMessage(ERROR_MESSAGE.MIN_SEC); - - return; - } - - onChangeErrorMessage(''); - updatePartStartTime(timeUnit, valueAsNumber); - }; - - const onFocusIntervalStart: React.FocusEventHandler = ({ - currentTarget: { name }, - }) => { - if (isInputName(name)) { - setActiveInput(name); - } - }; - - const onBlurIntervalStart = () => { - if (partStartTime + interval > videoLength) { - const { minute: songMin, second: songSec } = secondsToMinSec(videoLength - interval); - - onChangeErrorMessage(ERROR_MESSAGE.SONG_RANGE(songMin, songSec)); - return; - } - - onChangeErrorMessage(''); - setActiveInput(null); - }; - - return ( - - - - : - - ~ - - : - - - {errorMessage} - - ); -}; - -export default IntervalInput; - -const IntervalContainer = styled.div` - display: flex; - flex-direction: column; - justify-content: space-between; - - padding: 0 24px; - - font-size: 16px; - color: ${({ theme: { color } }) => color.white}; -`; - -const Flex = styled.div` - display: flex; -`; - -const ErrorMessage = styled.p` - margin: 8px 0; - font-size: 12px; - color: ${({ theme: { color } }) => color.error}; -`; - -const Separator = styled.span<{ $inactive?: boolean }>` - flex: none; - - margin: 0 8px; - padding-bottom: 8px; - - color: ${({ $inactive, theme: { color } }) => $inactive && color.subText}; - text-align: center; -`; - -const inputBase = css` - flex: 1; - - width: 16px; - margin: 0 8px; - margin: 0; - padding: 0; - - text-align: center; - - background-color: transparent; - border: none; - border-bottom: 1px solid white; - outline: none; - -webkit-box-shadow: none; - box-shadow: none; -`; - -const InputStart = styled.input<{ $active: boolean }>` - ${inputBase} - color: ${({ theme: { color } }) => color.white}; - border-bottom: 1px solid - ${({ $active, theme: { color } }) => ($active ? color.primary : color.white)}; -`; - -const InputEnd = styled.input` - ${inputBase} - color: ${({ theme: { color } }) => color.subText}; - border-bottom: 1px solid ${({ theme: { color } }) => color.subText}; -`; diff --git a/frontend/src/features/songs/components/VoteInterface.tsx b/frontend/src/features/songs/components/VoteInterface.tsx index 8550ba630..7a418fd6d 100644 --- a/frontend/src/features/songs/components/VoteInterface.tsx +++ b/frontend/src/features/songs/components/VoteInterface.tsx @@ -16,16 +16,16 @@ const VoteInterface = () => { const { showToast } = useToastContext(); const { interval, partStartTime, songId, songVideoId } = useVoteInterfaceContext(); const { videoPlayer } = useVideoPlayerContext(); - const { createKillingPart } = usePostKillingPart(); const { isOpen, openModal, closeModal } = useModal(); - const { user } = useAuthContext(); - const voteTimeText = toPlayingTimeText(partStartTime, partStartTime + interval); - + const voteTimeText = interval ? toPlayingTimeText(partStartTime, partStartTime + interval) : ''; + const isDisabledSummit = interval === null; const submitKillingPart = async () => { + if (!interval) return; videoPlayer.current?.pauseVideo(); + await createKillingPart(songId, { startSecond: partStartTime, length: interval }); openModal(); }; @@ -46,9 +46,15 @@ const VoteInterface = () => { - + 등록 + {isDisabledSummit && ( + <> + + 킬링파트 구간 선택 후 등록이 가능합니다. + + )} @@ -90,15 +96,16 @@ const RegisterTitle = styled.p` `; const Register = styled.button` - cursor: pointer; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; width: 100%; height: 36px; font-weight: 700; - color: ${({ theme: { color } }) => color.white}; + color: ${({ theme: { color }, disabled }) => (disabled ? color.disabled : color.white)}; - background-color: ${({ theme: { color } }) => color.primary}; + background-color: ${({ theme: { color }, disabled }) => + disabled ? color.disabledBackground : color.primary}; border: none; border-radius: 10px; `; @@ -150,3 +157,7 @@ const ButtonContainer = styled.div` const Warning = styled.div` color: ${({ theme: { color } }) => color.subText}; `; + +const Information = styled.p` + color: ${({ theme: { color } }) => color.primary}; +`; diff --git a/frontend/src/features/songs/components/VoteInterfaceProvider.tsx b/frontend/src/features/songs/components/VoteInterfaceProvider.tsx index 7f1972355..481de8188 100644 --- a/frontend/src/features/songs/components/VoteInterfaceProvider.tsx +++ b/frontend/src/features/songs/components/VoteInterfaceProvider.tsx @@ -5,7 +5,7 @@ import type { PropsWithChildren } from 'react'; interface VoteInterfaceContextProps extends VoteInterfaceProviderProps { partStartTime: number; - interval: KillingPartInterval; + interval: KillingPartInterval | null; // NOTE: Why both setState and eventHandler have same naming convention? updatePartStartTime: (timeUnit: string, value: number) => void; updateKillingPartInterval: React.MouseEventHandler; @@ -25,12 +25,17 @@ export const VoteInterfaceProvider = ({ songId, songVideoId, }: PropsWithChildren) => { - const [interval, setInterval] = useState(10); + const [interval, setInterval] = useState(10); const [partStartTime, setPartStartTime] = useState(0); const { videoPlayer } = useVideoPlayerContext(); const updateKillingPartInterval: React.MouseEventHandler = (e) => { const newInterval = Number(e.currentTarget.dataset['interval']) as KillingPartInterval; + if (newInterval === interval) { + setInterval(null); + return; + } + const partEndTime = partStartTime + newInterval; if (partEndTime > videoLength) { @@ -38,6 +43,7 @@ export const VoteInterfaceProvider = ({ setPartStartTime(partStartTime - overflowedSeconds); } + videoPlayer.current?.seekTo(partStartTime, true); setInterval(newInterval); }; @@ -62,6 +68,7 @@ export const VoteInterfaceProvider = ({ }; useEffect(() => { + if (!interval) return; const timer = window.setInterval(() => { videoPlayer.current?.seekTo(partStartTime, true); }, interval * 1000); diff --git a/frontend/src/features/youtube/components/VideoSlider.tsx b/frontend/src/features/youtube/components/VideoSlider.tsx index e69279bd1..eb62ec6cf 100644 --- a/frontend/src/features/youtube/components/VideoSlider.tsx +++ b/frontend/src/features/youtube/components/VideoSlider.tsx @@ -8,9 +8,12 @@ const VideoSlider = () => { const { interval, partStartTime, videoLength, updatePartStartTime } = useVoteInterfaceContext(); const { videoPlayer } = useVideoPlayerContext(); - const partEndTime = partStartTime + interval; - const partStartTimeText = toMinSecText(partStartTime); - const partEndTimeText = toMinSecText(partEndTime); + const partStartTimeText = interval ? toMinSecText(partStartTime) : toMinSecText(0); + const partEndTimeText = interval + ? toMinSecText(partStartTime + interval) + : toMinSecText(videoLength); + + const maxPlayingTime = interval ? videoLength - interval : videoLength; const changeTime: ChangeEventHandler = ({ currentTarget: { valueAsNumber: currentSelectedTime }, @@ -40,7 +43,7 @@ const VideoSlider = () => { onTouchEnd={seekToTime} onMouseUp={seekToTime} min={0} - max={videoLength - interval} + max={maxPlayingTime} step={1} interval={interval} /> @@ -86,7 +89,7 @@ export const PartEndTime = styled.span` font-weight: 700; `; -const Slider = styled.input<{ interval: number }>` +const Slider = styled.input<{ interval: number | null }>` cursor: pointer; width: 100%; @@ -99,7 +102,7 @@ const Slider = styled.input<{ interval: number }>` position: relative; top: -4px; - width: ${({ interval }) => interval * 6}px; + width: ${({ interval }) => (interval ? interval * 6 : 2)}px; height: 16px; -webkit-appearance: none; diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 68e9aefe8..2b18b499a 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -109,11 +109,12 @@ const PlatformName = styled.div` display: flex; align-items: center; justify-content: center; - text-align: center; width: 400px; height: 60px; + text-align: center; + border-radius: 12px; @media (max-width: ${({ theme }) => theme.breakPoints.xs}) {