-
+
+
{speakingMinute}
:
-
+
{speakingSecond}
@@ -189,7 +177,7 @@ export default function TimeBasedTimer({
{/* 조작부 */}
startTimer(isOpponentDone)}
+ onStart={startTimer}
onPause={pauseTimer}
onReset={resetCurrentTimer}
stance={prosCons}
diff --git a/src/page/TimerPage/components/TimerView.tsx b/src/page/TimerPage/components/TimerView.tsx
index 478d3a8c..933ee49d 100644
--- a/src/page/TimerPage/components/TimerView.tsx
+++ b/src/page/TimerPage/components/TimerView.tsx
@@ -53,11 +53,9 @@ 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"
@@ -81,11 +79,9 @@ 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"
diff --git a/src/page/TimerPage/hooks/useCircularTimerAnimation.ts b/src/page/TimerPage/hooks/useCircularTimerAnimation.ts
index 228fd692..40459e34 100644
--- a/src/page/TimerPage/hooks/useCircularTimerAnimation.ts
+++ b/src/page/TimerPage/hooks/useCircularTimerAnimation.ts
@@ -1,18 +1,21 @@
import { animate, clamp, useMotionValue } from 'framer-motion';
import { useEffect } from 'react';
-export default function useCircularTimerAnimation(rawProgress: number) {
+export default function useCircularTimerAnimation(
+ rawProgress: number,
+ isRunning: boolean,
+) {
const progress = clamp(0, 100, rawProgress);
const progressMotionValue = useMotionValue(0);
useEffect(() => {
const controls = animate(progressMotionValue, progress, {
- duration: 0.7,
+ duration: isRunning ? 0.7 : 0,
ease: 'easeOut',
});
return () => controls.stop();
- }, [progress, progressMotionValue]);
+ }, [progress, progressMotionValue, isRunning]);
return progressMotionValue;
}
diff --git a/src/page/TimerPage/hooks/useFeedbackTimer.ts b/src/page/TimerPage/hooks/useFeedbackTimer.ts
index c073317e..d3df0cc3 100644
--- a/src/page/TimerPage/hooks/useFeedbackTimer.ts
+++ b/src/page/TimerPage/hooks/useFeedbackTimer.ts
@@ -82,16 +82,19 @@ export function useFeedbackTimer(): FeedbackTimerLogics {
*/
const adjustTime = useCallback(
(amount: number) => {
- if (isRunning) return;
+ if (isRunning) {
+ return;
+ }
+
+ let newTime: number;
setTimer((prevTimer) => {
- const newTime = (prevTimer ?? 0) + amount;
+ newTime = (prevTimer ?? 0) + amount;
return newTime < 0 ? 0 : newTime;
});
- setDefaultTimer((prevDefault) => {
- const newDefault = prevDefault + amount;
- return newDefault < 0 ? 0 : newDefault;
+ setDefaultTimer(() => {
+ return newTime < 0 ? 0 : newTime;
});
},
[isRunning],
diff --git a/src/page/TimerPage/hooks/useTimeBasedTimer.ts b/src/page/TimerPage/hooks/useTimeBasedTimer.ts
index 8f4c8641..d9957547 100644
--- a/src/page/TimerPage/hooks/useTimeBasedTimer.ts
+++ b/src/page/TimerPage/hooks/useTimeBasedTimer.ts
@@ -38,33 +38,6 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
const targetTimeRef = useRef(null);
const speakingTargetTimeRef = useRef(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,
- ],
- );
-
/**
* 타이머 시작을 위해 사용하는 저수준 함수
*/
@@ -111,37 +84,30 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
* - isDone(완료) 상태일 땐 시작X
* - 1초마다 totalTimer, speakingTimer(필요시) 감소
*/
- 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;
- }
+ const startTimer = useCallback(() => {
+ if (intervalRef.current !== null || totalTimer === null || isDone) {
+ return;
+ }
- // 분모 갱신
- updateDenominator(isOpponentDone);
+ // 목표 시각을 실제 시각 기반으로 계산
+ // 예를 들어, 현재 시각이 오후 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,
- ],
- );
+ // 타이머 인터벌 시작
+ setTimerInterval();
+ }, [
+ isDone,
+ isSpeakingTimerAvailable,
+ setTimerInterval,
+ speakingTimer,
+ totalTimer,
+ ]);
/**
* 타이머 일시정지
@@ -185,9 +151,6 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
} else {
setSpeakingTimer(defaultTime.defaultSpeakingTimer);
}
-
- // 분모 갱신
- updateDenominator(isOpponentDone);
},
[
isSpeakingTimerAvailable,
@@ -195,7 +158,6 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
defaultTime.defaultTotalTimer,
totalTimer,
pauseTimer,
- updateDenominator,
],
);
@@ -222,25 +184,18 @@ 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,
- updateDenominator,
- ],
+ [defaultTime.defaultSpeakingTimer, isSpeakingTimerAvailable, totalTimer],
);
/**
@@ -250,6 +205,7 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
*/
const resetAndStartTimer = useCallback(
(isOpponentDone: boolean) => {
+ console.log(`# resetAndStartTimer 호출`);
const newTime = resetTimerForNextPhase(isOpponentDone);
if (intervalRef.current !== null || totalTimer === null || isDone) {
@@ -268,9 +224,6 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
// 타이머 인터벌 시작
setTimerInterval();
-
- // 분모 갱신
- updateDenominator(isOpponentDone);
},
[
resetTimerForNextPhase,
@@ -278,7 +231,6 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
isDone,
isSpeakingTimerAvailable,
totalTimer,
- updateDenominator,
],
);
@@ -305,9 +257,8 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
setTotalTimer(null);
setSpeakingTimer(null);
setIsDone(false);
- updateDenominator(false);
intervalRef.current = null;
- }, [pauseTimer, updateDenominator]);
+ }, [pauseTimer]);
useEffect(() => () => pauseTimer(), [pauseTimer]);
@@ -327,7 +278,6 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics {
setDefaultTime,
setIsDone,
clearTimer,
- denominator,
};
}
@@ -341,7 +291,7 @@ export interface TimeBasedTimerLogics {
defaultSpeakingTimer: number | null;
};
isSpeakingTimerAvailable: boolean;
- startTimer: (isOpponentDone: boolean) => void;
+ startTimer: () => void;
pauseTimer: () => void;
resetTimerForNextPhase: (isOpponentDone: boolean) => number;
resetAndStartTimer: (isOpponentDone: boolean) => void;
@@ -355,5 +305,4 @@ export interface TimeBasedTimerLogics {
>;
setIsDone: Dispatch>;
clearTimer: () => void;
- denominator: number;
}
diff --git a/src/page/TimerPage/hooks/useTimerHotkey.ts b/src/page/TimerPage/hooks/useTimerHotkey.ts
index 169f706d..cf066907 100644
--- a/src/page/TimerPage/hooks/useTimerHotkey.ts
+++ b/src/page/TimerPage/hooks/useTimerHotkey.ts
@@ -50,19 +50,11 @@ export function useTimerHotkey(state: TimerPageLogics) {
}
// 찬/반 타이머 토글 (시작/정지)
- 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);
- }
+ const toggleTimer = (timer: typeof timer1 | typeof timer2) => {
+ if (timer.isRunning) {
+ timer.pauseTimer();
+ } else {
+ timer.startTimer();
}
};
@@ -76,7 +68,11 @@ export function useTimerHotkey(state: TimerPageLogics) {
normalTimer.startTimer();
}
} else {
- toggleTimeBasedTimer();
+ if (prosConsSelected === 'PROS') {
+ toggleTimer(timer1);
+ } else if (prosConsSelected === 'CONS') {
+ toggleTimer(timer2);
+ }
}
break;
case 'KeyR':
diff --git a/src/page/TimerPage/stories/TimeBasedTimer.stories.tsx b/src/page/TimerPage/stories/TimeBasedTimer.stories.tsx
index 690f6378..e6ae3cf5 100644
--- a/src/page/TimerPage/stories/TimeBasedTimer.stories.tsx
+++ b/src/page/TimerPage/stories/TimeBasedTimer.stories.tsx
@@ -10,7 +10,6 @@ const mockTimerInstance = {
isDone: false,
defaultTime: { defaultTotalTimer: 150, defaultSpeakingTimer: 50 },
isSpeakingTimer: true,
- denominator: 1,
startTimer: () => {},
pauseTimer: () => {},
resetTimerForNextPhase: () => {},
From 544e99db90f0b516d827d28daabd74fa2773a034 Mon Sep 17 00:00:00 2001
From: jaeml06
Date: Sun, 12 Oct 2025 23:21:31 +0900
Subject: [PATCH 4/5] =?UTF-8?q?[FEAT]=20=EC=8A=B9=ED=8C=A8=20=ED=88=AC?=
=?UTF-8?q?=ED=91=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#387)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: 토론 시작 화면 구현
* feat: 투표 관련 api 함수 구현
* feat: 투표 관련 api 호출 리액트 훅 함수 구현
* feat: backgroundImage에 브랜드 색상 추가
* style: footer레이아웃 정렬 기준 변경
* feat: 승패 투표 생성 api 연결
* feat 승패 투표 결과 api DebateVotePage페이지에 연결
* feat: 투표자 투표 참여 페이지 구현
* feat: 투표자 투표 완료 페이지 구현
* feat: 투표 결과 페이지 구현
* feat: 투표 관련 구현 페이지 라우터에 연결
* test: storybook 테스트 코드 추가
* refactor: Create, Post 중복 사용 제거
* refactor: 파일명 컨벤션에 맞게 변경
* refactor: GetPollResponseType을 타입을 수정
* refactor: CheckBox 구현 방식 변경
* fix: pollId 유효성 검사 추가
* fix: msw 연관 핸들러에 맞게 네이밍 수정
* test: storybook이 url에서 제대로 param를 가져오도록 수정
* fix: msw 메서드를 올바르게 변경
* refactor: 유사한 디자인 버튼을 컴포넌트로 분리
* refactor: 불필요한 검증 로직 제거
* style: 애니메이션 시간 감소
* refactor: 컴포넌트 명 변경
* refactor: 타입 재활용
* refactor: 함수 선언식을 함수 표현식으로 변경
* refactor: 색상을 지정 컨벤션 통일
* refactor: 색상을 지정 컨벤션 통일
* fix: pollId 검증의 되어야 api훅이 호출되도록 변경
* refactor: 파일 이름과 컴포넌트명 일치
* refactor: 파일 이름과 컴포넌트명 일치
* refactor: 파일 이름과 컴포넌트명 일치
* refactor: layout 중복 제거
* fix: pollId 조건문 오류 수정
* refactor: 불필요한 export문 제거
* refactor: 불필요한 상태변경 로직 수정
* fix: 삭제된 export 복구
* fix: 파일명 대소문자 구분
* fix: 중복파일 삭제
---
src/apis/apis/poll.ts | 99 ++++++++++
src/apis/endpoints.ts | 1 +
src/apis/responses/poll.ts | 28 +++
src/assets/debateEnd/crown.svg | 9 +
src/components/icons/CheckBox.tsx | 37 ++++
src/hooks/mutations/useCreatePoll.ts | 12 ++
src/hooks/mutations/useFetchEndPoll.ts | 12 ++
src/hooks/mutations/usePostVoterPollInfo.ts | 18 ++
src/hooks/query/useGetPollInfo.ts | 15 ++
src/hooks/query/useGetVoterPollInfo.ts | 14 ++
.../components/footer/StickyFooterWrapper.tsx | 2 +-
src/mocks/handlers/global.ts | 7 +-
src/mocks/handlers/poll.ts | 93 ++++++++++
.../DebateEndPage/DebateEndPage.stories.tsx | 25 +++
src/page/DebateEndPage/DebateEndPage.tsx | 65 +++----
.../DebateEndPage/components/MenuCard.tsx | 58 ++++++
.../DebateVotePage/DebateVotePage.stories.tsx | 25 +++
src/page/DebateVotePage/DebateVotePage.tsx | 132 +++++++++++++
.../DebateVoteResultPage.stories.tsx | 25 +++
.../DebateVoteResultPage.tsx | 134 ++++++++++++++
.../components/AnimatedCounter.tsx | 31 ++++
.../components/VoteBar.tsx | 60 ++++++
.../components/VoteDetailResult.tsx | 58 ++++++
.../components/WinnerCard.tsx | 54 ++++++
.../VoteCompletePage.stories.tsx | 23 +++
.../VoteCompletePage/VoteCompletePage.tsx | 25 +++
.../VoteParticipationPage.stories.tsx | 24 +++
.../VoteParticipationPage.tsx | 173 ++++++++++++++++++
.../components/VoteTeamButton.tsx | 57 ++++++
src/routes/routes.tsx | 24 +++
src/type/type.ts | 41 ++++-
tailwind.config.js | 3 +
32 files changed, 1340 insertions(+), 44 deletions(-)
create mode 100644 src/apis/apis/poll.ts
create mode 100644 src/apis/responses/poll.ts
create mode 100644 src/assets/debateEnd/crown.svg
create mode 100644 src/components/icons/CheckBox.tsx
create mode 100644 src/hooks/mutations/useCreatePoll.ts
create mode 100644 src/hooks/mutations/useFetchEndPoll.ts
create mode 100644 src/hooks/mutations/usePostVoterPollInfo.ts
create mode 100644 src/hooks/query/useGetPollInfo.ts
create mode 100644 src/hooks/query/useGetVoterPollInfo.ts
create mode 100644 src/mocks/handlers/poll.ts
create mode 100644 src/page/DebateEndPage/DebateEndPage.stories.tsx
create mode 100644 src/page/DebateEndPage/components/MenuCard.tsx
create mode 100644 src/page/DebateVotePage/DebateVotePage.stories.tsx
create mode 100644 src/page/DebateVotePage/DebateVotePage.tsx
create mode 100644 src/page/DebateVoteResultPage/DebateVoteResultPage.stories.tsx
create mode 100644 src/page/DebateVoteResultPage/DebateVoteResultPage.tsx
create mode 100644 src/page/DebateVoteResultPage/components/AnimatedCounter.tsx
create mode 100644 src/page/DebateVoteResultPage/components/VoteBar.tsx
create mode 100644 src/page/DebateVoteResultPage/components/VoteDetailResult.tsx
create mode 100644 src/page/DebateVoteResultPage/components/WinnerCard.tsx
create mode 100644 src/page/VoteCompletePage/VoteCompletePage.stories.tsx
create mode 100644 src/page/VoteCompletePage/VoteCompletePage.tsx
create mode 100644 src/page/VoteParticipationPage/VoteParticipationPage.stories.tsx
create mode 100644 src/page/VoteParticipationPage/VoteParticipationPage.tsx
create mode 100644 src/page/VoteParticipationPage/components/VoteTeamButton.tsx
diff --git a/src/apis/apis/poll.ts b/src/apis/apis/poll.ts
new file mode 100644
index 00000000..d66d31dd
--- /dev/null
+++ b/src/apis/apis/poll.ts
@@ -0,0 +1,99 @@
+import { VoterPollInfo } from '../../type/type';
+import { ApiUrl } from '../endpoints';
+import { request } from '../primitives';
+import {
+ GetPollResponseType,
+ GetVoterPollInfoResponseType,
+ PatchPollResponseType,
+ PostPollResponseType,
+ PostVoterPollInfoResponseType,
+} from '../responses/poll';
+
+// Template
+/*
+export async function apiFunc(
+
+): Promise {
+ const requestUrl: string = ApiUrl.
+ const response = await request(
+ method,
+ requestUrl,
+ data,
+ params,
+ );
+
+ return response.data;
+}
+*/
+
+// POST /api/polls/{tableId}
+export async function postPoll(tableId: number): Promise {
+ const requestUrl: string = ApiUrl.poll;
+ const response = await request(
+ 'POST',
+ requestUrl + `/${tableId}`,
+ null,
+ null,
+ );
+
+ return response.data;
+}
+
+// GET /api/polls/{pollId}
+export async function getPollInfo(
+ pollId: number,
+): Promise {
+ const requestUrl: string = ApiUrl.poll;
+ const response = await request(
+ 'GET',
+ requestUrl + `/${pollId}`,
+ null,
+ null,
+ );
+ return response.data;
+}
+
+// PATCH /api/polls/{pollId}
+export async function patchEndPoll(
+ pollId: number,
+): Promise {
+ const requestUrl: string = ApiUrl.poll;
+ const response = await request(
+ 'PATCH',
+ requestUrl + `/${pollId}`,
+ null,
+ null,
+ );
+ return response.data;
+}
+
+// GET /api/polls/{pollId}/votes
+export async function getVoterPollInfo(
+ pollId: number,
+): Promise {
+ const requestUrl: string = ApiUrl.poll;
+ const response = await request(
+ 'GET',
+ requestUrl + `/${pollId}/votes`,
+ null,
+ null,
+ );
+
+ return response.data;
+}
+
+// POST /api/polls/{pollId}/votes
+export async function postVoterPollInfo(
+ pollId: number,
+ voterInfo: VoterPollInfo,
+): Promise {
+ const requestUrl: string = ApiUrl.poll;
+ const response = await request(
+ 'POST',
+ requestUrl + `/${pollId}/votes`,
+ voterInfo,
+ null,
+ );
+
+ return response.data;
+}
diff --git a/src/apis/endpoints.ts b/src/apis/endpoints.ts
index 95241a7c..7589d94c 100644
--- a/src/apis/endpoints.ts
+++ b/src/apis/endpoints.ts
@@ -10,4 +10,5 @@ export const ApiUrl = {
table: makeUrl('/table'),
parliamentary: makeUrl('/table/parliamentary'),
customize: makeUrl('/table/customize'),
+ poll: makeUrl('/polls'),
};
diff --git a/src/apis/responses/poll.ts b/src/apis/responses/poll.ts
new file mode 100644
index 00000000..01295050
--- /dev/null
+++ b/src/apis/responses/poll.ts
@@ -0,0 +1,28 @@
+import { BasePollInfo, PollInfo, VoterPollInfo } from '../../type/type';
+
+// POST /api/polls/{tableId}
+export interface PostPollResponseType extends BasePollInfo {
+ id: number;
+}
+
+// GET /api/polls/{pollId}
+export interface GetPollResponseType extends PollInfo {
+ id: number;
+}
+
+// PATCH /api/polls/{pollId}
+export interface PatchPollResponseType extends PollInfo {
+ id: number;
+}
+
+// GET /api/polls/{pollId}/votes
+export interface GetVoterPollInfoResponseType extends PollInfo {
+ id: number;
+ participateCode: string;
+}
+
+// POST /api/polls/{pollId}/votes
+
+export interface PostVoterPollInfoResponseType extends VoterPollInfo {
+ id: number;
+}
diff --git a/src/assets/debateEnd/crown.svg b/src/assets/debateEnd/crown.svg
new file mode 100644
index 00000000..4c356156
--- /dev/null
+++ b/src/assets/debateEnd/crown.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/components/icons/CheckBox.tsx b/src/components/icons/CheckBox.tsx
new file mode 100644
index 00000000..14fb5712
--- /dev/null
+++ b/src/components/icons/CheckBox.tsx
@@ -0,0 +1,37 @@
+import DTCheck from './Check';
+import clsx from 'clsx';
+
+interface CheckBoxProps {
+ checked?: boolean;
+ size?: number | string;
+ className?: string;
+}
+
+/**
+ *text-* 클래스가 포함되면 자동으로 DTCheck로 전달
+ */
+export default function CheckBox({
+ checked = false,
+ size = 24,
+ className = '',
+}: CheckBoxProps) {
+ // text-로 시작하는 클래스만 추출
+ const textClass =
+ className.split(' ').find((c) => c.startsWith('text-')) ?? '';
+
+ return (
+
+ {checked && }
+
+ );
+}
diff --git a/src/hooks/mutations/useCreatePoll.ts b/src/hooks/mutations/useCreatePoll.ts
new file mode 100644
index 00000000..173adbee
--- /dev/null
+++ b/src/hooks/mutations/useCreatePoll.ts
@@ -0,0 +1,12 @@
+import { postPoll } from '../../apis/apis/poll';
+import { PostPollResponseType } from '../../apis/responses/poll';
+import { usePreventDuplicateMutation } from './usePreventDuplicateMutation';
+
+export default function usePostPoll(onSuccess: (id: number) => void) {
+ return usePreventDuplicateMutation({
+ mutationFn: (id: number) => postPoll(id),
+ onSuccess: (response: PostPollResponseType) => {
+ onSuccess(response.id);
+ },
+ });
+}
diff --git a/src/hooks/mutations/useFetchEndPoll.ts b/src/hooks/mutations/useFetchEndPoll.ts
new file mode 100644
index 00000000..53574760
--- /dev/null
+++ b/src/hooks/mutations/useFetchEndPoll.ts
@@ -0,0 +1,12 @@
+import { patchEndPoll } from '../../apis/apis/poll';
+import { PatchPollResponseType } from '../../apis/responses/poll';
+import { usePreventDuplicateMutation } from './usePreventDuplicateMutation';
+
+export default function useFetchEndPoll(onSuccess: (id: number) => void) {
+ return usePreventDuplicateMutation({
+ mutationFn: (pollId: number) => patchEndPoll(pollId),
+ onSuccess: (response: PatchPollResponseType) => {
+ onSuccess(response.id);
+ },
+ });
+}
diff --git a/src/hooks/mutations/usePostVoterPollInfo.ts b/src/hooks/mutations/usePostVoterPollInfo.ts
new file mode 100644
index 00000000..3e19e5e6
--- /dev/null
+++ b/src/hooks/mutations/usePostVoterPollInfo.ts
@@ -0,0 +1,18 @@
+import { postVoterPollInfo } from '../../apis/apis/poll';
+import { VoterPollInfo } from '../../type/type';
+import { usePreventDuplicateMutation } from './usePreventDuplicateMutation';
+
+export default function usePostVoterPollInfo(onSuccess: () => void) {
+ return usePreventDuplicateMutation({
+ mutationFn: ({
+ pollId,
+ voterInfo,
+ }: {
+ pollId: number;
+ voterInfo: VoterPollInfo;
+ }) => postVoterPollInfo(pollId, voterInfo),
+ onSuccess: () => {
+ onSuccess();
+ },
+ });
+}
diff --git a/src/hooks/query/useGetPollInfo.ts b/src/hooks/query/useGetPollInfo.ts
new file mode 100644
index 00000000..ab5948c1
--- /dev/null
+++ b/src/hooks/query/useGetPollInfo.ts
@@ -0,0 +1,15 @@
+import { useQuery } from '@tanstack/react-query';
+import { getPollInfo } from '../../apis/apis/poll';
+import { GetPollResponseType } from '../../apis/responses/poll';
+
+export function useGetPollInfo(
+ pollId: number,
+ options?: { refetchInterval?: number | false; enabled?: boolean },
+) {
+ return useQuery({
+ queryKey: ['Poll', pollId],
+ queryFn: () => getPollInfo(pollId),
+ refetchInterval: options?.refetchInterval ?? false,
+ enabled: options?.enabled,
+ });
+}
diff --git a/src/hooks/query/useGetVoterPollInfo.ts b/src/hooks/query/useGetVoterPollInfo.ts
new file mode 100644
index 00000000..d10efbd5
--- /dev/null
+++ b/src/hooks/query/useGetVoterPollInfo.ts
@@ -0,0 +1,14 @@
+import { useQuery } from '@tanstack/react-query';
+import { getVoterPollInfo } from '../../apis/apis/poll';
+import { GetVoterPollInfoResponseType } from '../../apis/responses/poll';
+
+export function useGetVoterPollInfo(
+ pollId: number,
+ options?: { enabled?: boolean },
+) {
+ return useQuery({
+ queryKey: ['VoterPoll', pollId],
+ queryFn: () => getVoterPollInfo(pollId),
+ enabled: options?.enabled,
+ });
+}
diff --git a/src/layout/components/footer/StickyFooterWrapper.tsx b/src/layout/components/footer/StickyFooterWrapper.tsx
index 3a52d507..4fdbb240 100644
--- a/src/layout/components/footer/StickyFooterWrapper.tsx
+++ b/src/layout/components/footer/StickyFooterWrapper.tsx
@@ -4,7 +4,7 @@ export default function StickyFooterWrapper(props: PropsWithChildren) {
const { children } = props;
return (
-