Skip to content

Commit 00b0173

Browse files
authored
Merge pull request #75 from IT-Pick/feat-clicelee/vote
[FEAT/#43] 투표 컴포넌트 중간 구현
2 parents 75e890d + f96cc20 commit 00b0173

16 files changed

+265
-5
lines changed
Loading
Loading
Loading
Loading

src/assets/images/ico_empty_photo.svg

+3
Loading

src/main.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import UploadedPage from './pages/UploadedVotePage/UploadedPage';
2222
import DebateCreatePage from './pages/WritePostPage/DebateCreatePage';
2323
import InterestPage from './pages/InterestPage/InterestPage';
2424
import { SignUpProvider } from './context/SignUpContext';
25+
import MakeVote from './pages/MakeVote/MakeVote';
2526

2627
ReactDOM.createRoot(document.getElementById('root')!).render(
2728
// <React.StrictMode>
@@ -48,6 +49,8 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
4849
<Route path="/create" element={<DebateCreatePage />} />
4950
<Route path='/interest' element={<InterestPage/>} />
5051
<Route path='/uploaded-debate' element={<UploadedPage/>} />
52+
{/* <Route path="/vote-test" element={<VoteComponentTestPage />} /> */}
53+
<Route path='/make-vote' element={<MakeVote/>} />
5154
<Route path='*' element={<ErrorPage />} />
5255
</Routes>
5356
</BrowserRouter>

src/pages/KeywordPage/KeywordPage.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const KeywordPage: React.FC = () => {
1010
const navigate = useNavigate();
1111

1212
const handleButtonClick = () => {
13-
navigate('/debate-create');
13+
navigate('/create'); //라우팅 링크 typo 수정
1414
};
1515

1616
return(

src/pages/MakeVote/MakeVote.tsx

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React, { useState } from 'react';
2+
import { useNavigate } from 'react-router-dom'; // 리디렉션을 위한 useNavigate
3+
import ItemInput from './component/ItemInput';
4+
import MakeVoteBar from './component/MakeVoteBar';
5+
6+
const MakeVote: React.FC = () => {
7+
const [items, setItems] = useState<number[]>([1, 2]);
8+
const [itemNames, setItemNames] = useState<{ [key: number]: string }>({});
9+
const navigate = useNavigate(); // useNavigate 훅 사용
10+
11+
// 항목 추가 함수
12+
const addItem = () => {
13+
if (items.length < 5) {
14+
setItems((prevItems) => [...prevItems, prevItems.length + 1]);
15+
}
16+
};
17+
18+
// 항목 삭제 함수
19+
const removeItem = (id: number) => {
20+
if (items.length > 2) {
21+
setItems((prevItems) => prevItems.filter((item) => item !== id));
22+
const updatedNames = { ...itemNames };
23+
delete updatedNames[id];
24+
setItemNames(updatedNames);
25+
}
26+
};
27+
28+
// 항목 이름 변경 함수
29+
const handleNameChange = (id: number, name: string) => {
30+
setItemNames({ ...itemNames, [id]: name });
31+
};
32+
33+
// 완료 버튼 클릭 시 호출되는 함수
34+
const handleComplete = () => {
35+
const voteItems = items.map((id) => itemNames[id] || `항목 ${id}`);
36+
navigate('/create', { state: { voteItems } }); // 글쓰기 페이지로 리디렉션하며 투표 항목 데이터 전달
37+
};
38+
39+
return (
40+
<div className="w-[390px] h-screen mx-auto flex flex-col items-center bg-background">
41+
<div className="w-full flex flex-col justify-start items-center">
42+
<div className="w-full flex justify-between items-center mb-6 py-4 px-6 bg-white">
43+
<span className="text-point500 text-lg font-bold font-pretendard">투표 만들기</span>
44+
<span
45+
className="text-gray3 text-sm font-medium font-pretendard cursor-pointer"
46+
onClick={handleComplete} // 클릭 시 handleComplete 함수 실행
47+
>
48+
완료
49+
</span>
50+
</div>
51+
52+
<div className="w-full flex flex-col justify-start items-center gap-4">
53+
{items.map((item) => (
54+
<ItemInput
55+
key={item}
56+
id={item}
57+
name={itemNames[item] || ''}
58+
onRemove={removeItem}
59+
onNameChange={handleNameChange}
60+
canRemove={items.length > 2}
61+
/>
62+
))}
63+
</div>
64+
65+
<div className="w-full flex justify-center mt-4">
66+
<div
67+
className={`w-[350px] h-[52px] rounded-lg flex justify-center items-center ${
68+
items.length >= 5 ? 'bg-gray-300 cursor-not-allowed' : 'bg-[#7620e4] cursor-pointer'
69+
}`}
70+
onClick={addItem}
71+
style={{ pointerEvents: items.length >= 5 ? 'none' : 'auto' }}
72+
>
73+
<div className="text-center text-lg font-semibold font-pretendard text-white">
74+
항목 추가
75+
</div>
76+
</div>
77+
</div>
78+
<div className='w-[390px]'>
79+
<MakeVoteBar />
80+
</div>
81+
</div>
82+
</div>
83+
);
84+
};
85+
86+
export default MakeVote;
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React, { useState } from "react";
2+
import ico_vote_add_photo from "../../../assets/images/etc/ico_vote_add_photo.svg";
3+
import ico_vote_delete_item_active from "../../../assets/images/etc/ico_vote_delete_item_active.svg";
4+
import ico_vote_delete_item_unactive from "../../../assets/images/etc/ico_vote_delete_item_unactive.svg";
5+
6+
interface ItemInputProps {
7+
id: number;
8+
onRemove: (id: number) => void;
9+
canRemove: boolean;
10+
}
11+
12+
const ItemInput: React.FC<ItemInputProps> = ({ id, onRemove, canRemove }) => {
13+
const [text, setText] = useState(""); // 항목 입력 텍스트를 관리하는 상태
14+
15+
return (
16+
<div className="w-[350px] h-[47px] flex flex-col justify-center items-start">
17+
<div className="w-full flex justify-between items-center">
18+
<div className="flex justify-start items-center gap-3">
19+
<button
20+
className={`w-[18px] h-[18px] relative ${
21+
canRemove ? 'cursor-pointer' : 'cursor-not-allowed'
22+
}`}
23+
onClick={() => canRemove && onRemove(id)}
24+
disabled={!canRemove}
25+
aria-label="항목 삭제"
26+
>
27+
<img
28+
src={canRemove ? ico_vote_delete_item_active : ico_vote_delete_item_unactive}
29+
alt="삭제 아이콘"
30+
className="w-full h-full"
31+
/>
32+
</button>
33+
<input
34+
type="text"
35+
value={text}
36+
onChange={(e) => setText(e.target.value)}
37+
placeholder="항목 입력"
38+
className="text-black text-base font-medium font-['Pretendard'] bg-transparent focus:outline-none w-[250px]"
39+
/>
40+
</div>
41+
<div className="w-[35px] h-[35px]">
42+
<img src={ico_vote_add_photo} alt="사진 추가" className="w-full h-full" />
43+
</div>
44+
</div>
45+
<div className="w-full h-[0px] border border-[#edf0f3] mt-2"></div>
46+
</div>
47+
);
48+
};
49+
50+
export default ItemInput;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React, { useEffect, useState } from 'react';
2+
3+
const MakeVoteBar: React.FC = () => {
4+
const [isMultipleChoice, setIsMultipleChoice] = useState(false);
5+
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
6+
const [viewportHeight, setViewportHeight] = useState(window.innerHeight);
7+
8+
useEffect(() => {
9+
const handleResize = () => {
10+
const currentViewportHeight = visualViewport.height;
11+
setViewportHeight(currentViewportHeight);
12+
13+
if (window.visualViewport.height < window.innerHeight) {
14+
setIsKeyboardVisible(true);
15+
} else {
16+
setIsKeyboardVisible(false);
17+
}
18+
};
19+
20+
window.visualViewport.addEventListener('resize', handleResize);
21+
handleResize(); // 초기화
22+
return () => {
23+
window.visualViewport.removeEventListener('resize', handleResize);
24+
};
25+
}, []);
26+
27+
const handleToggle = () => {
28+
setIsMultipleChoice(!isMultipleChoice);
29+
};
30+
31+
return (
32+
<div
33+
className="w-full px-5 py-3 bg-white fixed left-0 right-0 border-t border-gray2 flex justify-between items-center"
34+
style={{ bottom: isKeyboardVisible ? `${window.innerHeight - viewportHeight}px` : '0' }}
35+
>
36+
<span className="font-pretendard text-base text-gray-800">복수 선택 가능</span>
37+
<div
38+
onClick={handleToggle} // 클릭 시 토글
39+
className={`relative w-11 h-6 flex items-center rounded-full p-1 cursor-pointer transition-colors ${
40+
isMultipleChoice ? 'bg-[#7620e4]' : 'bg-gray-300'
41+
}`}
42+
>
43+
<div
44+
className={`bg-white w-4 h-4 rounded-full shadow-md transform transition-transform ${
45+
isMultipleChoice ? 'translate-x-5' : ''
46+
}`}
47+
/>
48+
</div>
49+
</div>
50+
);
51+
};
52+
53+
export default MakeVoteBar;

src/pages/WritePostPage/.gitkeep

Whitespace-only changes.

src/pages/WritePostPage/DebateCreatePage.tsx

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import React, { useState, useEffect } from 'react';
2+
import { useLocation } from 'react-router-dom';
23
import DebateIconBar from './components/DebateIconBar';
4+
import VoteResult from './components/VoteResult';
35

46
const DebateCreatePage: React.FC = () => {
57
const [title, setTitle] = useState('');
68
const [content, setContent] = useState('');
79
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
8-
const [viewportHeight, setViewportHeight] = useState(window.innerHeight);
10+
const [viewportHeight, setViewportHeight] = useState(window.innerHeight);
11+
const location = useLocation<{ voteItems: string[] }>();
12+
const voteItems = location.state?.voteItems || []; // 투표 데이터 가져오기
913

1014
useEffect(() => {
1115
const handleResize = () => {
1216
const currentViewportHeight = visualViewport.height;
1317
setViewportHeight(currentViewportHeight);
14-
18+
1519
if (window.visualViewport.height < window.innerHeight) {
1620
setIsKeyboardVisible(true);
1721
} else {
@@ -20,7 +24,7 @@ const DebateCreatePage: React.FC = () => {
2024
};
2125

2226
window.visualViewport.addEventListener('resize', handleResize);
23-
handleResize(); //초기화
27+
handleResize(); // 초기화
2428
return () => {
2529
window.visualViewport.removeEventListener('resize', handleResize);
2630
};
@@ -50,6 +54,13 @@ const DebateCreatePage: React.FC = () => {
5054
onChange={(e) => setContent(e.target.value)}
5155
className="w-[335px] flex-grow px-5 font-pretendard font-medium text-[16px] text-gray5 placeholder-gray3 border-none focus:outline-none resize-none bg-background"
5256
/>
57+
58+
{/* 투표 결과 표시 */}
59+
{voteItems.length > 0 && (
60+
<div className="mt-4">
61+
<VoteResult items={voteItems} />
62+
</div>
63+
)}
5364
</div>
5465
<div className={`w-[390px] flex justify-center py-3 bg-white ${isKeyboardVisible ? 'fixed bottom-0' : 'absolute bottom-0'}`}
5566
style={{ bottom: isKeyboardVisible ? `${window.innerHeight - viewportHeight}px` : '0' }}>

src/pages/WritePostPage/components/DebateIconBar.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useState } from 'react';
2+
import { useNavigate } from 'react-router-dom'; // useNavigate 훅을 가져옵니다.
23
import tagIcoVoteGray from '../../../assets/images/16x16/tag_ico_vote_gray.svg';
34
import tagIcoPictureGray from '../../../assets/images/16x16/tag_ico_pic_gray.svg';
45
import tagIcoVotePoint from '../../../assets/images/16x16/tag_ico_vote.svg';
@@ -7,9 +8,11 @@ import tagIcoPicturePoint from '../../../assets/images/16x16/tag_ico_pic.svg';
78
const DebateIconBar: React.FC = () => {
89
const [voteActive, setVoteActive] = useState(false);
910
const [pictureActive, setPictureActive] = useState(false);
11+
const navigate = useNavigate(); // useNavigate 훅을 사용하여 navigate 함수를 생성합니다.
1012

1113
const handleVoteClick = () => {
1214
setVoteActive(!voteActive);
15+
navigate('/make-vote'); // 버튼 클릭 시 /make-vote로 이동합니다.
1316
};
1417

1518
const handlePictureClick = () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from "react";
2+
3+
interface VoteResultProps {
4+
items: string[];
5+
}
6+
7+
const VoteResult: React.FC<VoteResultProps> = ({ items }) => {
8+
return (
9+
<div className="w-[350px] h-auto px-5 pt-4 pb-5 bg-white rounded-xl flex flex-col gap-2">
10+
<div className="flex justify-between items-center">
11+
<div className="text-[#1d2228] text-sm font-semibold font-['Pretendard']">투표</div>
12+
</div>
13+
{items.map((item, index) => (
14+
<div
15+
key={index}
16+
className="self-stretch h-14 p-3 bg-[#edf0f3] rounded-lg flex items-center gap-2"
17+
>
18+
<div className="w-5 h-5 bg-white rounded-sm" />
19+
<div className="text-[#1d2228] text-base font-semibold font-['Pretendard']">
20+
{item}
21+
</div>
22+
</div>
23+
))}
24+
</div>
25+
);
26+
};
27+
28+
export default VoteResult;

tsconfig.node.json

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
{
22
"compilerOptions": {
3+
"baseUrl": "./src",
4+
"paths": {
5+
"@images/*": ["assets/images/*"],
6+
"@components/*": ["components/*"],
7+
"@utils/*": ["utils/*"],
8+
},
39
"composite": true,
410
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
511
"skipLibCheck": true,

vite.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default defineConfig({
1010
'@components': '/src/components',
1111
'@utils': '/src/utils',
1212
'@apis': '/src/apis',
13+
'@pages': '/src/pages',
1314
}
1415
},
1516
server: {

0 commit comments

Comments
 (0)