Skip to content
56 changes: 34 additions & 22 deletions src/page/TimerPage/components/TimeBasedTimer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ type TimeBasedTimerInstance = {
totalTimer: number | null;
speakingTimer: number | null;
isRunning: boolean;
startTimer: () => void;
startTimer: (isOpponentDone: boolean) => void;
pauseTimer: () => void;
resetCurrentTimer: () => void;
denominator: number;
};

interface TimeBasedTimerProps {
timeBasedTimerInstance: TimeBasedTimerInstance;
isOpponentDone: boolean;
isSelected: boolean;
onActivate?: () => void;
prosCons: TimeBasedStance;
Expand All @@ -28,6 +30,7 @@ interface TimeBasedTimerProps {

export default function TimeBasedTimer({
timeBasedTimerInstance,
isOpponentDone,
isSelected,
prosCons,
teamName,
Expand All @@ -40,6 +43,7 @@ export default function TimeBasedTimer({
startTimer,
pauseTimer,
resetCurrentTimer,
denominator,
} = timeBasedTimerInstance;

const minute = Formatting.formatTwoDigits(
Expand All @@ -55,30 +59,38 @@ export default function TimeBasedTimer({
);

const initRawProgress = (): number => {
if (speakingTimer === null) {
// 1회당 발언 시간 X일 때...
if (item.timePerTeam && totalTimer && item.timePerTeam > 0) {
// 팀당 발언 시간 타이머가 정상 동작 중이고 남은 시간이 있을 경우, 진행도를 계산
if (totalTimer <= 0) {
return 100;
}
return ((item.timePerTeam - totalTimer) / item.timePerTeam) * 100;
if (isOpponentDone) {
if (speakingTimer === null) {
// 1회당 발언 시간 X일 때...
return ((denominator - (totalTimer ?? 0)) / denominator) * 100;
} else {
// 팀당 발언 시간 타이머가 멈추거나 완료된 경우,
// 완료(100%)에 해당하는 진행도를 반환
return 100;
// 1회당 발언 시간 O일 때...
return ((denominator - (speakingTimer ?? 0)) / denominator) * 100;
}
} else {
// 1회당 발언 시간 O일 때...
if (item.timePerSpeaking && speakingTimer && item.timePerSpeaking > 0) {
// 1회당 발언 시간 타이머가 정상 동작 중이고 남은 시간이 있을 경우, 진행도를 계산
return (
((item.timePerSpeaking - speakingTimer) / item.timePerSpeaking) * 100
);
if (speakingTimer === null) {
// 1회당 발언 시간 X일 때...
if (item.timePerTeam && totalTimer && item.timePerTeam > 0) {
// 팀당 발언 시간 타이머가 정상 동작 중이고 남은 시간이 있을 경우, 진행도를 계산
if (totalTimer <= 0) {
return 100;
}
return ((item.timePerTeam - totalTimer) / denominator) * 100;
} else {
// 팀당 발언 시간 타이머가 멈추거나 완료된 경우,
// 완료(100%)에 해당하는 진행도를 반환
return 100;
}
} else {
// 1회당 발언 시간 타이머가 멈추거나 완료된 경우,
// 완료(100%)에 해당하는 진행도를 반환
return 100;
// 1회당 발언 시간 O일 때...
if (item.timePerSpeaking && speakingTimer && item.timePerSpeaking > 0) {
// 1회당 발언 시간 타이머가 정상 동작 중이고 남은 시간이 있을 경우, 진행도를 계산
return ((item.timePerSpeaking - speakingTimer) / denominator) * 100;
} else {
// 1회당 발언 시간 타이머가 멈추거나 완료된 경우,
// 완료(100%)에 해당하는 진행도를 반환
return 100;
}
}
}
};
Expand Down Expand Up @@ -177,7 +189,7 @@ export default function TimeBasedTimer({
{/* 조작부 */}
<TimerController
isRunning={isRunning}
onStart={startTimer}
onStart={() => startTimer(isOpponentDone)}
onPause={pauseTimer}
onReset={resetCurrentTimer}
stance={prosCons}
Expand Down
4 changes: 4 additions & 0 deletions src/page/TimerPage/components/TimerView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ export default function TimerView({ state }: { state: TimerPageLogics }) {
isRunning: timer1.isRunning,
startTimer: timer1.startTimer,
pauseTimer: timer1.pauseTimer,
denominator: timer1.denominator,
resetCurrentTimer: () => timer1.resetCurrentTimer(timer2.isDone),
}}
item={data.table[index]}
isOpponentDone={timer2.isDone}
isSelected={prosConsSelected === 'PROS'}
onActivate={() => handleActivateTeam('PROS')}
prosCons="PROS"
Expand All @@ -79,9 +81,11 @@ export default function TimerView({ state }: { state: TimerPageLogics }) {
isRunning: timer2.isRunning,
startTimer: timer2.startTimer,
pauseTimer: timer2.pauseTimer,
denominator: timer2.denominator,
resetCurrentTimer: () => timer2.resetCurrentTimer(timer1.isDone),
}}
item={data.table[index]}
isOpponentDone={timer1.isDone}
isSelected={prosConsSelected === 'CONS'}
onActivate={() => handleActivateTeam('CONS')}
prosCons="CONS"
Expand Down
103 changes: 77 additions & 26 deletions src/page/TimerPage/hooks/useTimeBasedTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,33 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
const targetTimeRef = useRef<number | null>(null);
const speakingTargetTimeRef = useRef<number | null>(null);

// 진행률 계산용 변수
const [denominator, setDenominator] = useState(1);
const updateDenominator = useCallback(
(isOpponentDone: boolean) => {
if (isOpponentDone) {
if (isSpeakingTimerAvailable) {
setDenominator(speakingTimer ?? 0);
} else {
setDenominator(totalTimer ?? 0);
}
} else {
if (isSpeakingTimerAvailable) {
setDenominator(defaultTime.defaultSpeakingTimer ?? 0);
} else {
setDenominator(defaultTime.defaultTotalTimer ?? 0);
}
}
},
[
defaultTime.defaultSpeakingTimer,
defaultTime.defaultTotalTimer,
isSpeakingTimerAvailable,
speakingTimer,
totalTimer,
],
);

/**
* 타이머 시작을 위해 사용하는 저수준 함수
*/
Expand Down Expand Up @@ -84,30 +111,37 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
* - isDone(완료) 상태일 땐 시작X
* - 1초마다 totalTimer, speakingTimer(필요시) 감소
*/
const startTimer = useCallback(() => {
if (intervalRef.current !== null || totalTimer === null || isDone) {
return;
}
const startTimer = useCallback(
(isOpponentDone: boolean) => {
if (intervalRef.current !== null || totalTimer === null || isDone) {
return;
}

// 목표 시각을 실제 시각 기반으로 계산
// 예를 들어, 현재 시각이 오후 13시 00분 30초인데, 1회당 발언 시간이 30초라면,
// 1회당 발언 시간이 모두 끝나는 시간은 13시 01분 00초이므로,
// 해당 시간을 목표 시간으로 두는 식임
const startTime = Date.now();
targetTimeRef.current = startTime + totalTimer * 1000;
if (isSpeakingTimerAvailable) {
speakingTargetTimeRef.current = startTime + speakingTimer * 1000;
}
// 목표 시각을 실제 시각 기반으로 계산
// 예를 들어, 현재 시각이 오후 13시 00분 30초인데, 1회당 발언 시간이 30초라면,
// 1회당 발언 시간이 모두 끝나는 시간은 13시 01분 00초이므로,
// 해당 시간을 목표 시간으로 두는 식임
const startTime = Date.now();
targetTimeRef.current = startTime + totalTimer * 1000;
if (isSpeakingTimerAvailable) {
speakingTargetTimeRef.current = startTime + speakingTimer * 1000;
}

// 타이머 인터벌 시작
setTimerInterval();
}, [
isDone,
isSpeakingTimerAvailable,
setTimerInterval,
speakingTimer,
totalTimer,
]);
// 분모 갱신
updateDenominator(isOpponentDone);

// 타이머 인터벌 시작
setTimerInterval();
},
[
isDone,
isSpeakingTimerAvailable,
setTimerInterval,
speakingTimer,
totalTimer,
updateDenominator,
],
);

/**
* 타이머 일시정지
Expand Down Expand Up @@ -151,13 +185,17 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
} else {
setSpeakingTimer(defaultTime.defaultSpeakingTimer);
}

// 분모 갱신
updateDenominator(isOpponentDone);
},
[
isSpeakingTimerAvailable,
defaultTime.defaultSpeakingTimer,
defaultTime.defaultTotalTimer,
totalTimer,
pauseTimer,
updateDenominator,
],
);

Expand All @@ -184,18 +222,25 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {

// 계산한 시간을 1회당 발언 시간으로 설정
setSpeakingTimer(nextSpeakingTime);
updateDenominator(isOpponentDone);
return nextSpeakingTime;
} else {
// # 1회당 발언 시간을 사용하지 않을 경우

// 전체 발언 시간 타이머는 초기값으로 리셋
updateDenominator(isOpponentDone);
if (totalTimer === 0 || totalTimer === null) {
return 0;
}
return totalTimer;
}
},
[defaultTime.defaultSpeakingTimer, isSpeakingTimerAvailable, totalTimer],
[
defaultTime.defaultSpeakingTimer,
isSpeakingTimerAvailable,
totalTimer,
updateDenominator,
],
);

/**
Expand All @@ -205,7 +250,6 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
*/
const resetAndStartTimer = useCallback(
(isOpponentDone: boolean) => {
console.log(`# resetAndStartTimer 호출`);
const newTime = resetTimerForNextPhase(isOpponentDone);

if (intervalRef.current !== null || totalTimer === null || isDone) {
Expand All @@ -224,13 +268,17 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {

// 타이머 인터벌 시작
setTimerInterval();

// 분모 갱신
updateDenominator(isOpponentDone);
},
[
resetTimerForNextPhase,
setTimerInterval,
isDone,
isSpeakingTimerAvailable,
totalTimer,
updateDenominator,
],
);

Expand All @@ -257,8 +305,9 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
setTotalTimer(null);
setSpeakingTimer(null);
setIsDone(false);
updateDenominator(false);
intervalRef.current = null;
}, [pauseTimer]);
}, [pauseTimer, updateDenominator]);

useEffect(() => () => pauseTimer(), [pauseTimer]);

Expand All @@ -278,6 +327,7 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
setDefaultTime,
setIsDone,
clearTimer,
denominator,
};
}

Expand All @@ -291,7 +341,7 @@ export interface TimeBasedTimerLogics {
defaultSpeakingTimer: number | null;
};
isSpeakingTimerAvailable: boolean;
startTimer: () => void;
startTimer: (isOpponentDone: boolean) => void;
pauseTimer: () => void;
resetTimerForNextPhase: (isOpponentDone: boolean) => number;
resetAndStartTimer: (isOpponentDone: boolean) => void;
Expand All @@ -305,4 +355,5 @@ export interface TimeBasedTimerLogics {
>;
setIsDone: Dispatch<SetStateAction<boolean>>;
clearTimer: () => void;
denominator: number;
}
24 changes: 14 additions & 10 deletions src/page/TimerPage/hooks/useTimerHotkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,19 @@ export function useTimerHotkey(state: TimerPageLogics) {
}

// 찬/반 타이머 토글 (시작/정지)
const toggleTimer = (timer: typeof timer1 | typeof timer2) => {
if (timer.isRunning) {
timer.pauseTimer();
} else {
timer.startTimer();
const toggleTimeBasedTimer = () => {
if (prosConsSelected === 'PROS') {
if (timer1.isRunning) {
timer1.pauseTimer();
} else {
timer1.startTimer(timer2.isDone);
}
} else if (prosConsSelected === 'CONS') {
if (timer2.isRunning) {
timer2.pauseTimer();
} else {
timer2.startTimer(timer1.isDone);
}
}
};

Expand All @@ -68,11 +76,7 @@ export function useTimerHotkey(state: TimerPageLogics) {
normalTimer.startTimer();
}
} else {
if (prosConsSelected === 'PROS') {
toggleTimer(timer1);
} else if (prosConsSelected === 'CONS') {
toggleTimer(timer2);
}
toggleTimeBasedTimer();
}
break;
case 'KeyR':
Expand Down
1 change: 1 addition & 0 deletions src/page/TimerPage/stories/TimeBasedTimer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const mockTimerInstance = {
isDone: false,
defaultTime: { defaultTotalTimer: 150, defaultSpeakingTimer: 50 },
isSpeakingTimer: true,
denominator: 1,
startTimer: () => {},
pauseTimer: () => {},
resetTimerForNextPhase: () => {},
Expand Down