Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0415e54
feat: 토론 시작 화면 구현
jaeml06 Sep 21, 2025
beca8d1
feat: 투표 관련 api 함수 구현
jaeml06 Oct 6, 2025
702cdd8
feat: 투표 관련 api 호출 리액트 훅 함수 구현
jaeml06 Oct 6, 2025
4a892fa
feat: backgroundImage에 브랜드 색상 추가
jaeml06 Oct 6, 2025
cd8cc58
style: footer레이아웃 정렬 기준 변경
jaeml06 Oct 6, 2025
2b6518c
feat: 승패 투표 생성 api 연결
jaeml06 Oct 6, 2025
755e196
feat 승패 투표 결과 api DebateVotePage페이지에 연결
jaeml06 Oct 6, 2025
433b783
feat: 투표자 투표 참여 페이지 구현
jaeml06 Oct 6, 2025
fb7fd34
feat: 투표자 투표 완료 페이지 구현
jaeml06 Oct 6, 2025
6351b37
feat: 투표 결과 페이지 구현
jaeml06 Oct 6, 2025
9030215
feat: 투표 관련 구현 페이지 라우터에 연결
jaeml06 Oct 6, 2025
731d70d
test: storybook 테스트 코드 추가
jaeml06 Oct 7, 2025
ee9983a
refactor: Create, Post 중복 사용 제거
jaeml06 Oct 7, 2025
fed2a0e
refactor: 파일명 컨벤션에 맞게 변경
jaeml06 Oct 7, 2025
662bff0
refactor: GetPollResponseType을 타입을 수정
jaeml06 Oct 7, 2025
66066e5
refactor: CheckBox 구현 방식 변경
jaeml06 Oct 8, 2025
ca7c048
fix: pollId 유효성 검사 추가
jaeml06 Oct 8, 2025
314facb
fix: msw 연관 핸들러에 맞게 네이밍 수정
jaeml06 Oct 8, 2025
cf7cf7a
test: storybook이 url에서 제대로 param를 가져오도록 수정
jaeml06 Oct 8, 2025
d5e7202
fix: msw 메서드를 올바르게 변경
jaeml06 Oct 8, 2025
3a52de5
refactor: 유사한 디자인 버튼을 컴포넌트로 분리
jaeml06 Oct 8, 2025
3c74696
refactor: 불필요한 검증 로직 제거
jaeml06 Oct 8, 2025
1062c7c
style: 애니메이션 시간 감소
jaeml06 Oct 8, 2025
e6d8b9c
refactor: 컴포넌트 명 변경
jaeml06 Oct 8, 2025
744da7b
refactor: 타입 재활용
jaeml06 Oct 8, 2025
50437c1
refactor: 함수 선언식을 함수 표현식으로 변경
jaeml06 Oct 8, 2025
a33ce25
refactor: 색상을 지정 컨벤션 통일
jaeml06 Oct 8, 2025
6211fdf
refactor: 색상을 지정 컨벤션 통일
jaeml06 Oct 8, 2025
d9258e9
fix: pollId 검증의 되어야 api훅이 호출되도록 변경
jaeml06 Oct 8, 2025
6dfae79
refactor: 파일 이름과 컴포넌트명 일치
jaeml06 Oct 11, 2025
c951c52
refactor: 파일 이름과 컴포넌트명 일치
jaeml06 Oct 11, 2025
7e94cb0
refactor: 파일 이름과 컴포넌트명 일치
jaeml06 Oct 11, 2025
2588e21
refactor: layout 중복 제거
jaeml06 Oct 11, 2025
ba9b815
fix: pollId 조건문 오류 수정
jaeml06 Oct 11, 2025
7cd3101
refactor: 불필요한 export문 제거
jaeml06 Oct 11, 2025
bd4cb28
refactor: 불필요한 상태변경 로직 수정
jaeml06 Oct 11, 2025
452e103
Merge branch 'develop' into feat/#381
jaeml06 Oct 12, 2025
d2a24aa
fix: 삭제된 export 복구
jaeml06 Oct 12, 2025
2b6c228
Merge branch 'feat/#381' of https://github.com/debate-timer/debate-ti…
jaeml06 Oct 12, 2025
0dfed86
fix: 파일명 대소문자 구분
jaeml06 Oct 12, 2025
a08add3
fix: 중복파일 삭제
jaeml06 Oct 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions src/apis/apis/poll.ts
Original file line number Diff line number Diff line change
@@ -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<ReturnType> {
const requestUrl: string = ApiUrl.
const response = await request<ReturnType>(
method,
requestUrl,
data,
params,
);

return response.data;
}
*/

// POST /api/polls/{tableId}
export async function postPoll(tableId: number): Promise<PostPollResponseType> {
const requestUrl: string = ApiUrl.poll;
const response = await request<PostPollResponseType>(
'POST',
requestUrl + `/${tableId}`,
null,
null,
);

return response.data;
}

// GET /api/polls/{pollId}
export async function getPollInfo(
pollId: number,
): Promise<GetPollResponseType> {
const requestUrl: string = ApiUrl.poll;
const response = await request<GetPollResponseType>(
'GET',
requestUrl + `/${pollId}`,
null,
null,
);
return response.data;
}

// PATCH /api/polls/{pollId}
export async function patchEndPoll(
pollId: number,
): Promise<PatchPollResponseType> {
const requestUrl: string = ApiUrl.poll;
const response = await request<PatchPollResponseType>(
'PATCH',
requestUrl + `/${pollId}`,
null,
null,
);
return response.data;
}

// GET /api/polls/{pollId}/votes
export async function getVoterPollInfo(
pollId: number,
): Promise<GetVoterPollInfoResponseType> {
const requestUrl: string = ApiUrl.poll;
const response = await request<GetVoterPollInfoResponseType>(
'GET',
requestUrl + `/${pollId}/votes`,
null,
null,
);

return response.data;
}

// POST /api/polls/{pollId}/votes
export async function postVoterPollInfo(
pollId: number,
voterInfo: VoterPollInfo,
): Promise<PostVoterPollInfoResponseType> {
const requestUrl: string = ApiUrl.poll;
const response = await request<PostVoterPollInfoResponseType>(
'POST',
requestUrl + `/${pollId}/votes`,
voterInfo,
null,
);

return response.data;
}
1 change: 1 addition & 0 deletions src/apis/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export const ApiUrl = {
table: makeUrl('/table'),
parliamentary: makeUrl('/table/parliamentary'),
customize: makeUrl('/table/customize'),
poll: makeUrl('/polls'),
};
28 changes: 28 additions & 0 deletions src/apis/responses/poll.ts
Original file line number Diff line number Diff line change
@@ -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;
}
9 changes: 9 additions & 0 deletions src/assets/debateEnd/crown.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions src/components/icons/CheckBox.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={clsx(
'flex items-center justify-center rounded-md border',
checked ? 'border-transparent' : 'border-neutral-400',
className,
)}
style={{
width: size,
height: size,
}}
>
{checked && <DTCheck className={clsx('w-2/3', textClass)} />}
</div>
);
}
12 changes: 12 additions & 0 deletions src/hooks/mutations/useCreatePoll.ts
Original file line number Diff line number Diff line change
@@ -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({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

중복호출방지 훅 사용까지 !!! 🥹

mutationFn: (id: number) => postPoll(id),
onSuccess: (response: PostPollResponseType) => {
onSuccess(response.id);
},
});
}
12 changes: 12 additions & 0 deletions src/hooks/mutations/useFetchEndPoll.ts
Original file line number Diff line number Diff line change
@@ -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);
},
});
}
18 changes: 18 additions & 0 deletions src/hooks/mutations/usePostVoterPollInfo.ts
Original file line number Diff line number Diff line change
@@ -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();
},
});
}
15 changes: 15 additions & 0 deletions src/hooks/query/useGetPollInfo.ts
Original file line number Diff line number Diff line change
@@ -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<GetPollResponseType>({
queryKey: ['Poll', pollId],
queryFn: () => getPollInfo(pollId),
refetchInterval: options?.refetchInterval ?? false,
enabled: options?.enabled,
});
}
14 changes: 14 additions & 0 deletions src/hooks/query/useGetVoterPollInfo.ts
Original file line number Diff line number Diff line change
@@ -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<GetVoterPollInfoResponseType>({
queryKey: ['VoterPoll', pollId],
queryFn: () => getVoterPollInfo(pollId),
enabled: options?.enabled,
});
}
2 changes: 1 addition & 1 deletion src/layout/components/footer/StickyFooterWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function StickyFooterWrapper(props: PropsWithChildren) {
const { children } = props;

return (
<footer className="sticky bottom-0 flex w-full flex-shrink-0 content-center items-center gap-1 px-8">
<footer className="sticky bottom-0 flex w-full flex-shrink-0 content-center items-center justify-center gap-1 px-8">
{children}
</footer>
);
Expand Down
7 changes: 6 additions & 1 deletion src/mocks/handlers/global.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { customizeHandlers } from './customize';
import { memberHandlers } from './member';
import { pollHandlers } from './poll';

export const allHandlers = [...memberHandlers, ...customizeHandlers];
export const allHandlers = [
...memberHandlers,
...customizeHandlers,
...pollHandlers,
];
93 changes: 93 additions & 0 deletions src/mocks/handlers/poll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { http, HttpResponse } from 'msw';
import { ApiUrl } from '../../apis/endpoints';
import {
PatchPollResponseType,
PostPollResponseType,
PostVoterPollInfoResponseType,
} from '../../apis/responses/poll';

export const pollHandlers = [
// GET /api/polls/:pollId
http.get(ApiUrl.poll + '/:pollId', ({ params }) => {
const { pollId } = params;
console.log(`# pollId = ${pollId}`);

return HttpResponse.json({
id: 7,
status: 'PROGRESS',
prosTeamName: '찬성',
consTeamName: '반대',
totalCount: 1,
prosCount: 1,
consCount: 0,
voterNames: ['ㅇㄹㄴ', 'ㅁㄴㅇ'],
});
}),

// POST /api/polls/{tableId}
http.post(ApiUrl.poll + '/:tableId', async ({ request }) => {
const result = (await request.json()) as PostPollResponseType;
console.log(
`# pollId = ${result?.id}, prosTeamName = ${result?.prosTeamName}, consTeamName = ${result?.consTeamName}`,
);
return HttpResponse.json({
id: 7,
status: 'PROGRESS',
prosTeamName: '찬성',
consTeamName: '반대',
});
}),

// PATCH /api/polls/{pollId}
http.patch(ApiUrl.poll + '/:pollId', async ({ request, params }) => {
const result = (await request.json()) as PatchPollResponseType;
const { pollId } = params;
console.log(
`pollId = ${pollId}, status = ${result?.status}, prosTeamName = ${result?.prosTeamName}, consTeamName = ${result?.consTeamName}`,
);

return HttpResponse.json({
id: 7,
status: 'DONE',
prosTeamName: '찬성',
consTeamName: '반대',
totalCount: 1,
prosCount: 1,
consCount: 0,
voterNames: ['ㅇㄹㄴ', 'ㅁㄴㅇ'],
});
}),

// GET /api/polls/{pollId}/votes
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

주석의 HTTP 메서드 수정 필요.

주석에 "GET"으로 표기되어 있지만 실제로는 DELETE 핸들러입니다.

다음 diff를 적용하여 수정하세요:

-  // GET /api/polls/{pollId}/votes
+  // DELETE /api/polls/{pollId}/votes
   http.delete(ApiUrl.poll + `/:pollId/votes`, ({ params }) => {
🤖 Prompt for AI Agents
In src/mocks/handlers/poll.ts around line 60, the comment above the handler
incorrectly states "GET /api/polls/{pollId}/votes" while the implemented handler
is a DELETE; update the comment to read "DELETE /api/polls/{pollId}/votes" (or
the exact DELETE route signature used) so the comment matches the actual HTTP
method and endpoint.

http.get(ApiUrl.poll + `/:pollId/votes`, ({ params }) => {
const { pollId } = params;
console.log(`# pollId = ${pollId}`);

return HttpResponse.json({
id: 7,
status: 'PROGRESS',
prosTeamName: '찬성',
consTeamName: '반대',
participateCode: '494bcec7-f8e8-4e96-8922-511cd7114a07',
totalCount: 1,
prosCount: 1,
consCount: 0,
});
}),

// POST /api/polls/{pollId}/votes
http.post(ApiUrl.poll + '/:pollId/votes', async ({ request, params }) => {
const { pollId } = params;
const result = (await request.json()) as PostVoterPollInfoResponseType;
console.log(
`# pollId = ${pollId}, name = ${result?.name}, participateCode = ${result?.participateCode}, team = ${result?.team}`,
);

return HttpResponse.json({
id: 7,
name: 'ㅇㄹㄴ',
participateCode: 'string',
team: 'PROS',
});
}),
];
25 changes: 25 additions & 0 deletions src/page/DebateEndPage/DebateEndPage.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Meta, StoryObj } from '@storybook/react';
import DebateEndPage from './DebateEndPage';

const meta: Meta<typeof DebateEndPage> = {
title: 'page/DebateEndPage',
component: DebateEndPage,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen', // Storybook에서 전체 화면으로 표시
route: '/table/customize/123/end/vote',
routePattern: '/table/customize/:id/end/vote',
Comment on lines +10 to +11
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

라우트 설정이 잘못되었습니다.

현재 route가 /table/customize/123/end/vote로 설정되어 있는데, 이는 투표 결과 페이지로 이동할 때 사용하는 경로입니다. DebateEndPage는 /table/customize/:id/end에 위치해야 합니다.

다음 diff를 적용하세요:

   parameters: {
     layout: 'fullscreen', // Storybook에서 전체 화면으로 표시
-    route: '/table/customize/123/end/vote',
-    routePattern: '/table/customize/:id/end/vote',
+    route: '/table/customize/123/end',
+    routePattern: '/table/customize/:id/end',
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
route: '/table/customize/123/end/vote',
routePattern: '/table/customize/:id/end/vote',
parameters: {
layout: 'fullscreen', // Storybook에서 전체 화면으로 표시
route: '/table/customize/123/end',
routePattern: '/table/customize/:id/end',
},
🤖 Prompt for AI Agents
In src/page/DebateEndPage/DebateEndPage.stories.tsx around lines 10 to 11, the
route and routePattern are incorrect for DebateEndPage; update route from
'/table/customize/123/end/vote' to '/table/customize/123/end' and routePattern
from '/table/customize/:id/end/vote' to '/table/customize/:id/end' so the story
uses the correct end page path.

},
};

export default meta;

type Story = StoryObj<typeof DebateEndPage>;

export const Default: Story = {
render: () => (
<div style={{ height: '100vh', width: '100vw', overflow: 'hidden' }}>
<DebateEndPage />
</div>
),
};
Loading