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}) {