Skip to content

Commit 06d49a1

Browse files
committed
Merge branch 'main' into dev
2 parents eaed03f + cb2851b commit 06d49a1

File tree

5 files changed

+177
-137
lines changed

5 files changed

+177
-137
lines changed

react-app/src/components/Candidate/CandidateGroup.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import React from 'react';
12
import Candidate from './Candidate';
23
import { CandidateGroupProps } from './Candidate.types';
34

@@ -19,6 +20,6 @@ const CandidateGroup = ({ candidateArr, selectedCandidateId, onSelect }: Candida
1920
);
2021
};
2122

22-
export default CandidateGroup;
23+
export default React.memo(CandidateGroup);
2324

2425

react-app/src/hook/useVote.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useState} from 'react';
2+
import useVoteData from './useVoteData';
3+
import useVoteSubmit from './useVoteSubmit';
4+
5+
export const useVote = () => {
6+
const { pollData, pollIds } = useVoteData();
7+
const { isSubmitting, isModalOpen, setIsModalOpen, handleSubmit } = useVoteSubmit();
8+
const [pageIndex, setPageIndex] = useState(0);
9+
const [selectedCandidates, setSelectedCandidates] = useState<{ [pollId: number]: number | null }>(
10+
{},
11+
);
12+
13+
const handleSelect = (pollId: number, candidateId: number) => {
14+
setSelectedCandidates((prev) => ({ ...prev, [pollId]: candidateId }));
15+
};
16+
17+
return {
18+
pollData,
19+
pollIds,
20+
pageIndex,
21+
setPageIndex,
22+
selectedCandidates,
23+
handleSelect,
24+
isSubmitting,
25+
isModalOpen,
26+
setIsModalOpen,
27+
handleSubmit: () => handleSubmit(selectedCandidates),
28+
};
29+
};

react-app/src/hook/useVoteData.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useNavigate } from "react-router-dom";
2+
import { useToast } from "./useToast";
3+
import { useEffect, useState } from "react";
4+
import { getCandidateLatestResponse, getCandidateLatests } from "@/apis/candidate/getCandidateLatest";
5+
import { AxiosError } from "axios";
6+
7+
const useVoteData = () => {
8+
const navigate = useNavigate();
9+
const { showToast } = useToast();
10+
const [pollData, setPollData] = useState<{ [pollId: number]: getCandidateLatestResponse[] }>({});
11+
const [pollIds, setPollIds] = useState<number[]>([]);
12+
13+
useEffect(() => {
14+
async function fetchData() {
15+
try {
16+
const data = await getCandidateLatests();
17+
const groupedData: { [pollId: number]: getCandidateLatestResponse[] } = {};
18+
19+
data.forEach((item) => {
20+
if (!groupedData[item.pollId]) {
21+
groupedData[item.pollId] = [];
22+
}
23+
groupedData[item.pollId].push(item);
24+
});
25+
26+
const ids = Object.keys(groupedData)
27+
.map(Number)
28+
.sort((a, b) => a - b);
29+
setPollData(groupedData);
30+
setPollIds(ids);
31+
} catch (error) {
32+
navigate('/main');
33+
if (error instanceof AxiosError) {
34+
const message =
35+
typeof error.response?.data === 'string'
36+
? error.response.data
37+
: JSON.stringify(error.response?.data);
38+
showToast(message, 'warning');
39+
} else {
40+
showToast(String(error), 'warning');
41+
}
42+
}
43+
}
44+
45+
fetchData();
46+
}, []);
47+
return { pollData , pollIds};
48+
};
49+
50+
export default useVoteData;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useCallback, useState } from 'react';
2+
import { useNavigate } from 'react-router-dom';
3+
import { useToast } from './useToast';
4+
import { updateVoteCount } from '@/apis/vote/updateVoteCount';
5+
import { postVoteResult } from '@/apis/vote/postVoteResult';
6+
import { AxiosError } from 'axios';
7+
8+
const useVoteSubmit = () => {
9+
const navigate = useNavigate();
10+
const { showToast } = useToast();
11+
const [isSubmitting, setIsSubmitting] = useState(false);
12+
const [isModalOpen, setIsModalOpen] = useState(false);
13+
14+
const handleSubmit = useCallback(async (selectedCandidates: {[pollId: number]: number | null}) => {
15+
const userId = Number(localStorage.getItem('userId'));
16+
if (!userId) {
17+
navigate('/');
18+
showToast('로그인이 필요합니다.', 'warning');
19+
return;
20+
}
21+
22+
const voteResults = Object.entries(selectedCandidates).map(([pollId, candidateId]) => ({
23+
pollId: Number(pollId),
24+
candidateId: candidateId!,
25+
}));
26+
27+
try {
28+
setIsSubmitting(true);
29+
await Promise.all(voteResults.map(updateVoteCount));
30+
await postVoteResult({ userId });
31+
localStorage.setItem('voted', 'true');
32+
navigate('/main');
33+
showToast('투표가 성공적으로 완료되었습니다.', 'success');
34+
} catch (error) {
35+
navigate('/main');
36+
const message =
37+
error instanceof AxiosError
38+
? typeof error.response?.data === 'string'
39+
? error.response.data
40+
: JSON.stringify(error.response?.data)
41+
: String(error);
42+
showToast(message, 'warning');
43+
} finally {
44+
setIsSubmitting(false);
45+
}
46+
}, [showToast, navigate]);
47+
48+
return {
49+
isSubmitting,
50+
isModalOpen,
51+
setIsModalOpen,
52+
handleSubmit,
53+
};
54+
};
55+
56+
export default useVoteSubmit;

react-app/src/pages/Vote.tsx

Lines changed: 40 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -3,149 +3,52 @@ import Button from '@/components/Button/Button';
33
import CandidateGroup from '@/components/Candidate/CandidateGroup';
44
import Modal from '@/components/Modal/Modal';
55
import Loading from '@/components/Loading/Loading';
6-
import { useToast } from '@/hook/useToast';
7-
import { useEffect, useState } from 'react';
8-
import useIsMobile from '@/hook/useIsMobile';
9-
import { useNavigate } from 'react-router-dom';
10-
import {
11-
getCandidateLatests,
12-
getCandidateLatestResponse,
13-
} from '@/apis/candidate/getCandidateLatest';
6+
import { useMediaQuery } from 'react-responsive';
147
import { ICONS } from '@/constants/iconPath';
15-
import { updateVoteCount } from '@/apis/vote/updateVoteCount';
16-
import { postVoteResult } from '@/apis/vote/postVoteResult';
17-
import { AxiosError } from 'axios';
8+
import { useVote } from '@/hook/useVote';
9+
import { useCallback, useMemo } from 'react';
1810

1911
const Vote = () => {
20-
const userId = Number(localStorage.getItem('userId'));
21-
const [isSubmitting, setIsSubmitting] = useState(false);
22-
const [pollData, setPollData] = useState<{ [pollId: number]: getCandidateLatestResponse[] }>({});
23-
const [pollIds, setPollIds] = useState<number[]>([]);
24-
const [pageIndex, setPageIndex] = useState(0);
25-
const [isModalOpen, setIsModalOpen] = useState(false);
26-
const [selectedCandidates, setSelectedCandidates] = useState<{ [pollId: number]: number | null }>(
27-
{},
12+
const isMobile = useMediaQuery({ query: '(max-width: 768px)' });
13+
const {
14+
pollData,
15+
pollIds,
16+
pageIndex,
17+
setPageIndex,
18+
isSubmitting,
19+
isModalOpen,
20+
setIsModalOpen,
21+
selectedCandidates,
22+
handleSelect,
23+
handleSubmit,
24+
} = useVote();
25+
26+
const currentPollId = useMemo(() => pollIds[pageIndex], [pollIds, pageIndex]);
27+
28+
const currentCandidates = useMemo(() => pollData[currentPollId] || [], [pollData, currentPollId]);
29+
30+
const selectedCandidateId = useMemo(
31+
() => selectedCandidates[currentPollId] ?? null,
32+
[selectedCandidates, currentPollId],
2833
);
29-
const navigate = useNavigate();
30-
const isMobile = useIsMobile();
31-
const { showToast } = useToast();
3234

33-
useEffect(() => {
34-
async function fetchData() {
35-
try {
36-
const data = await getCandidateLatests();
37-
const groupedData: { [pollId: number]: getCandidateLatestResponse[] } = {};
38-
39-
data.forEach((item) => {
40-
if (!groupedData[item.pollId]) {
41-
groupedData[item.pollId] = [];
42-
}
43-
groupedData[item.pollId].push(item);
44-
});
45-
46-
const ids: number[] = Object.keys(groupedData)
47-
.map(Number)
48-
.sort((a, b) => a - b);
49-
50-
setPollData(groupedData);
51-
setPollIds(ids);
52-
setPageIndex(0);
53-
} catch (error) {
54-
navigate('/main');
55-
if (error instanceof AxiosError) {
56-
if (error.status === 404) {
57-
showToast(error.message, 'warning');
58-
} else {
59-
const message =
60-
typeof error.response?.data === 'string'
61-
? error.response.data
62-
: JSON.stringify(error.response?.data);
63-
64-
showToast(message, 'warning');
65-
}
66-
} else {
67-
showToast(String(error), 'warning');
68-
}
69-
console.error(error);
70-
}
71-
}
72-
73-
fetchData();
74-
}, []);
75-
76-
const handlePrev = () => {
77-
setPageIndex((prev) => Math.max(prev - 1, 0));
78-
};
79-
80-
const handleMain = () => {
81-
navigate('/main');
82-
};
83-
84-
const handleNext = () => {
85-
setPageIndex((prev) => Math.min(prev + 1, pollIds.length - 1));
86-
};
87-
88-
const handleConfirm = async () => {
89-
setIsModalOpen(false);
90-
91-
if (!userId) {
92-
navigate('/');
93-
showToast('로그인이 필요합니다.', 'warning');
94-
return;
95-
}
96-
97-
const voteResults = Object.entries(selectedCandidates).map(([pollId, candidateId]) => ({
98-
pollId: Number(pollId),
99-
candidateId: candidateId!,
100-
}));
101-
102-
try {
103-
setIsSubmitting(true);
104-
await Promise.all(voteResults.map(updateVoteCount));
105-
await postVoteResult({ userId });
106-
localStorage.setItem('voted', 'true');
107-
navigate('/main');
108-
showToast('투표가 성공적으로 완료되었습니다.', 'success');
109-
} catch (error) {
110-
navigate('/main');
111-
if (error instanceof AxiosError) {
112-
if (error.status === 404) {
113-
showToast(error.message, 'warning');
114-
} else {
115-
const message =
116-
typeof error.response?.data === 'string'
117-
? error.response.data
118-
: JSON.stringify(error.response?.data);
119-
120-
showToast(message, 'warning');
121-
}
122-
} else {
123-
showToast(String(error), 'warning');
124-
}
125-
console.error(error);
126-
} finally {
127-
setIsSubmitting(false);
128-
}
129-
};
35+
const questionText = currentCandidates[0]?.questionText;
36+
const icon = currentCandidates[0]?.icon;
37+
const isCandidateSelected = selectedCandidateId !== null;
13038

131-
const handleCandidateSelect = (pollId: number, candidateId: number) => {
132-
setSelectedCandidates((prev) => ({ ...prev, [pollId]: candidateId }));
133-
};
39+
const handlePrev = useCallback(() => setPageIndex((prev) => Math.max(prev - 1, 0)), []);
40+
const handleNext = useCallback(
41+
() => setPageIndex((prev) => Math.min(prev + 1, pollIds.length - 1)),
42+
[pollIds.length],
43+
);
13444

135-
if (pollIds.length === 0) {
45+
if (!pollIds.length) {
13646
return (
13747
<div className="h-full flex items-center justify-center">
13848
<Loading />
13949
</div>
14050
);
14151
}
142-
const currentPollId = pollIds[pageIndex];
143-
const currentCandidates = pollData[currentPollId];
144-
const questionText = currentCandidates[0]?.questionText;
145-
const icon = currentCandidates[0]?.icon;
146-
147-
const selectedCandidateId = selectedCandidates[currentPollId] ?? null;
148-
const isCandidateSelected = selectedCandidateId !== null;
14952

15053
return (
15154
<div className="h-full overflow-hidden bg-white flex items-center flex-col justify-center p-5">
@@ -170,15 +73,15 @@ const Vote = () => {
17073
userName: c.userName,
17174
}))}
17275
selectedCandidateId={selectedCandidateId}
173-
onSelect={(candidateId) => handleCandidateSelect(currentPollId, candidateId)}
76+
onSelect={(candidateId) => handleSelect(currentPollId, candidateId)}
17477
/>
17578
</div>
17679
</div>
17780
<div className="flex justify-between items-center w-full p-5">
17881
{pageIndex > 0 ? (
17982
<Button label="이전" onClick={handlePrev} type="outline" size="sm" />
18083
) : (
181-
<Button label="이전" onClick={handleMain} type="outline" size="sm" />
84+
<Button label="이전" onClick={() => setPageIndex(0)} type="outline" size="sm" />
18285
)}
18386

18487
{pageIndex < pollIds.length - 1 ? (
@@ -197,7 +100,7 @@ const Vote = () => {
197100
<div className="p-5 ">
198101
<div className="flex flex-col items-center justify-center bg-gray-50 p-5 rounded-[30px] md:flex-row min-h-[400px] min-w-[1030px]">
199102
<div className="flex flex-1 p-13 min-h-[400px] min-w-[500px] items-center justify-center">
200-
<div className="flex flex-col items-center justify-center gap-10 font-ps text-xl text-center ">
103+
<div className="flex flex-col items-center justify-center gap-10 font-ps text-xl text-center">
201104
<p className="break-all">{questionText}</p>
202105
<img
203106
src={ICONS.QUESTION_ICON(icon)}
@@ -213,15 +116,15 @@ const Vote = () => {
213116
userName: c.userName,
214117
}))}
215118
selectedCandidateId={selectedCandidateId}
216-
onSelect={(candidateId) => handleCandidateSelect(currentPollId, candidateId)}
119+
onSelect={(id) => handleSelect(currentPollId, id)}
217120
/>
218121
</div>
219122
</div>
220123
<div className="flex justify-between items-center w-full mt-5 mb-5">
221124
{pageIndex > 0 ? (
222125
<Button label="이전" onClick={handlePrev} type="outline" size="lg" />
223126
) : (
224-
<Button label="이전" onClick={handleMain} type="outline" size="lg" />
127+
<Button label="이전" onClick={() => setPageIndex(0)} type="outline" size="lg" />
225128
)}
226129

227130
{pageIndex < pollIds.length - 1 ? (
@@ -241,9 +144,10 @@ const Vote = () => {
241144
<Modal
242145
isOpen={isModalOpen}
243146
setIsOpen={setIsModalOpen}
244-
onConfirm={handleConfirm}
147+
onConfirm={handleSubmit}
245148
text2="한번 완료하면 다시 투표할 수 없어요"
246149
/>
150+
247151
{isSubmitting && (
248152
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-30">
249153
<Loading />

0 commit comments

Comments
 (0)