diff --git a/package.json b/package.json
index 64d8829..07b5b07 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"axios": "^1.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "dayjs": "^1.11.13",
"framer-motion": "^12.12.1",
"jotai": "^2.12.4",
"lodash": "^4.17.21",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4788923..dff9fb0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -35,6 +35,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ dayjs:
+ specifier: ^1.11.13
+ version: 1.11.13
framer-motion:
specifier: ^12.12.1
version: 12.12.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -991,6 +994,9 @@ packages:
resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
engines: {node: '>=12'}
+ dayjs@1.11.13:
+ resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
+
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
@@ -2579,6 +2585,8 @@ snapshots:
d3-transition: 3.0.1(d3-selection@3.0.0)
d3-zoom: 3.0.0
+ dayjs@1.11.13: {}
+
debug@4.4.1:
dependencies:
ms: 2.1.3
diff --git a/src/app/stackflow/Stack.tsx b/src/app/stackflow/Stack.tsx
index 9ca2207..62e29c3 100644
--- a/src/app/stackflow/Stack.tsx
+++ b/src/app/stackflow/Stack.tsx
@@ -52,12 +52,21 @@ export const { Stack, useFlow } = stackflow({
],
initialActivity: () => {
if (fetchLoginStatus()) {
- if (sessionStorage.getItem('userMode')) {
- const mode = JSON.parse(sessionStorage.getItem('userMode')!).mode;
+ const userModeRaw = sessionStorage.getItem('userMode');
+ if (userModeRaw) {
+ const mode = JSON.parse(userModeRaw).mode;
if (mode === 'STUDENT') return 'HomeScreen';
- else if (mode === 'ADMIN') return 'AdminHomeScreen';
- else return 'LoginScreen';
- } else return 'LoginScreen';
- } else return 'LoginScreen';
+ if (mode === 'ADMIN') return 'AdminHomeScreen';
+ }
+
+ const userInfoRaw = sessionStorage.getItem('userInfo');
+ if (userInfoRaw) {
+ const role = JSON.parse(userInfoRaw).role;
+ if (role === 'ROLE_USER') return 'HomeScreen';
+ if (role === 'ROLE_ADMIN') return 'AdminHomeScreen';
+ }
+ }
+
+ return 'LoginScreen';
},
});
diff --git a/src/assets/icon/icon-alert.png b/src/assets/icon/icon-alert.png
new file mode 100644
index 0000000..e1a5112
Binary files /dev/null and b/src/assets/icon/icon-alert.png differ
diff --git a/src/assets/icon/index.ts b/src/assets/icon/index.ts
index 00c47ad..e022a39 100644
--- a/src/assets/icon/index.ts
+++ b/src/assets/icon/index.ts
@@ -1,3 +1,4 @@
+import Alert from './icon-alert.png';
import Character from './character.svg';
import CharacterComplete from './character-complete.png';
import CharacterFlat from './character-flat.png';
@@ -20,6 +21,7 @@ import VoteIcon from './icon-vote.svg';
import WriteIcon from './icon-write.svg';
export {
+ Alert,
BackButton,
Character,
Character2,
diff --git a/src/features/admin-dashboard/ui/CardList.tsx b/src/features/admin-dashboard/ui/CardList.tsx
index 93a8f70..09adf57 100644
--- a/src/features/admin-dashboard/ui/CardList.tsx
+++ b/src/features/admin-dashboard/ui/CardList.tsx
@@ -1,14 +1,16 @@
import type { VoteStatus } from '@/shared/types';
-import { Card } from '@/shared/ui';
-import { VOTE_MOCK } from '../mock';
+// import { Card } from '@/shared/ui';
+import { VOTE_MOCK } from '@/features/admin-dashboard/mock';
export default function CardList({ status }: { status: VoteStatus }) {
const data = VOTE_MOCK.filter(
({ status: voteStatus }) => voteStatus === status,
);
+ console.log(data);
+
return (
- {data.map(({ id, campus, status, title, date }, index) => (
+{/* {data.map(({ id, campus, status, title, date }, index) => (
- ))}
+ ))} */}
);
}
diff --git a/src/features/home/api/election.ts b/src/features/home/api/election.ts
new file mode 100644
index 0000000..ce72875
--- /dev/null
+++ b/src/features/home/api/election.ts
@@ -0,0 +1,29 @@
+import { REQUEST, userGet } from '@/shared/api';
+import type { Election } from '@/shared/types';
+import { useQuery } from '@tanstack/react-query';
+
+interface ElectionResponse {
+ status: {
+ code: number;
+ message: string;
+ };
+ metadata: {
+ resultCount: number;
+ };
+ results: Election[];
+}
+
+const fetchAllElections = async () => {
+ const response = await userGet({
+ request: REQUEST.ELECTION_ALL,
+ });
+ if (response.data.metadata.resultCount === 0) return [];
+ return response.data.results;
+};
+
+export const useFetchAllElections = () => {
+ return useQuery({
+ queryKey: ['elections'],
+ queryFn: fetchAllElections,
+ });
+};
diff --git a/src/features/home/api/index.ts b/src/features/home/api/index.ts
new file mode 100644
index 0000000..71fe4e1
--- /dev/null
+++ b/src/features/home/api/index.ts
@@ -0,0 +1 @@
+export * from './election';
diff --git a/src/features/home/mock/card.ts b/src/features/home/mock/card.ts
index 51cf3bc..d885edb 100644
--- a/src/features/home/mock/card.ts
+++ b/src/features/home/mock/card.ts
@@ -1,4 +1,10 @@
-import type { CardProps } from '@/features/home/types';
+type CardProps = {
+ id: number;
+ title: string;
+ campus: string;
+ status: string;
+ date: string;
+}
export const CARD_MOCK: CardProps[] = [
{
diff --git a/src/features/home/types/card.ts b/src/features/home/types/card.ts
deleted file mode 100644
index d4271cc..0000000
--- a/src/features/home/types/card.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export type CardProps = {
- id: number;
- title: string;
- campus: string;
- status: string;
- date: string;
-};
diff --git a/src/features/home/types/index.ts b/src/features/home/types/index.ts
deleted file mode 100644
index cb5809f..0000000
--- a/src/features/home/types/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './card';
diff --git a/src/features/home/ui/CardSection.tsx b/src/features/home/ui/CardSection.tsx
new file mode 100644
index 0000000..22bc9a2
--- /dev/null
+++ b/src/features/home/ui/CardSection.tsx
@@ -0,0 +1,26 @@
+import { useFetchAllElections } from '../api';
+import CardStack from './CardStack';
+
+export default function CardSection() {
+ const { data, isFetching, isError } = useFetchAllElections();
+ if (isFetching) return <>>;
+ if (isError) return <>>;
+ if (data && data.length === 0)
+ return (
+ <>
+ 진행중인 선거가 없어요
+
+ >
+ );
+ if (data)
+ return (
+ <>
+ 진행중인 선거가 있어요!
+
+ >
+ );
+
+ return <>>;
+}
diff --git a/src/features/home/ui/CardStack.tsx b/src/features/home/ui/CardStack.tsx
index b1ce8fd..82a9adc 100644
--- a/src/features/home/ui/CardStack.tsx
+++ b/src/features/home/ui/CardStack.tsx
@@ -2,14 +2,14 @@ import { useRef, useState } from 'react';
import { animate, motion } from 'framer-motion';
import { Card } from '@/shared/ui';
-import type { CardProps } from '@/features/home/types';
-import { CARD_MOCK } from '@/features/home/mock';
+import type { Election } from '@/shared/types';
+import { getDate } from '@/shared/utils';
-export default function CardStack() {
- const [stack, setStack] = useState(CARD_MOCK);
- const [passedCards, setPassedCards] = useState([]);
+export default function CardStack({ data }: { data: Election[] }) {
+ const [stack, setStack] = useState(data);
+ const [passedCards, setPassedCards] = useState([]);
const cardRefs = useRef<{ [key: number]: HTMLDivElement | null }>({});
- const currentIndex = CARD_MOCK.length - stack.length;
+ const currentIndex = data.length - stack.length;
return (
<>
@@ -47,7 +47,7 @@ export default function CardStack() {
{ x: 0 },
{ type: 'spring', stiffness: 300 },
);
- setStack(CARD_MOCK);
+ setStack(data);
setPassedCards([]);
} else if (stack.length !== 0) {
setPassedCards(prev => [...prev, stack[0]]);
@@ -94,17 +94,17 @@ export default function CardStack() {
>
);
})}
- {CARD_MOCK.map((_, idx) => (
+ {data.map((_, idx) => (
{
+ const response = await userGet
({
+ request: REQUEST.NOTICE + `${id}`,
+ });
+ return response.data;
+};
+
+export const useFetchNoticeContent = (id: number) => {
+ return useQuery({
+ queryKey: [`${id}`, 'notice-content'],
+ queryFn: () => fetchNoticeContent(id),
+ });
+};
diff --git a/src/features/notice-content/api/index.ts b/src/features/notice-content/api/index.ts
new file mode 100644
index 0000000..7b367d1
--- /dev/null
+++ b/src/features/notice-content/api/index.ts
@@ -0,0 +1 @@
+export * from './content';
diff --git a/src/features/notice-content/ui/NoticeContentSection.tsx b/src/features/notice-content/ui/NoticeContentSection.tsx
new file mode 100644
index 0000000..e1fca77
--- /dev/null
+++ b/src/features/notice-content/ui/NoticeContentSection.tsx
@@ -0,0 +1,17 @@
+import { useFetchNoticeContent } from '../api';
+
+export default function NoticeContentSection({ id }: { id: number }) {
+ const { data } = useFetchNoticeContent(id);
+ if (data)
+ return (
+
+ {data.content.split('\n').map((line, index) => (
+
+ {line}
+
+ ))}
+
+ );
+
+ return <>>;
+}
diff --git a/src/features/notice-content/ui/index.ts b/src/features/notice-content/ui/index.ts
new file mode 100644
index 0000000..45b01fe
--- /dev/null
+++ b/src/features/notice-content/ui/index.ts
@@ -0,0 +1 @@
+export { default as NoticeContentSection } from './NoticeContentSection';
diff --git a/src/features/notice/mock/index.ts b/src/features/notice/api/index.ts
similarity index 100%
rename from src/features/notice/mock/index.ts
rename to src/features/notice/api/index.ts
diff --git a/src/features/notice/api/notice.ts b/src/features/notice/api/notice.ts
new file mode 100644
index 0000000..43089a9
--- /dev/null
+++ b/src/features/notice/api/notice.ts
@@ -0,0 +1,21 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { REQUEST, userGet } from '@/shared/api';
+import type { Campus } from '@/shared/types';
+import type { NoticeList } from '../types';
+
+const fetchNoticeByCampus = async (campus: Campus) => {
+ const response = await userGet({
+ request: REQUEST.NOTICE_CAMPUS + campus,
+ });
+ return response.data;
+};
+
+export const useFetchNoticeByCampus = (campus: Campus) => {
+ return useQuery({
+ queryKey: ['notices', campus],
+ queryFn: () => fetchNoticeByCampus(campus),
+ staleTime: 1000 * 60 * 5,
+ retry: 1,
+ });
+};
diff --git a/src/features/notice/constants/index.ts b/src/features/notice/constants/index.ts
new file mode 100644
index 0000000..420cc02
--- /dev/null
+++ b/src/features/notice/constants/index.ts
@@ -0,0 +1 @@
+export * from './status';
diff --git a/src/features/notice/constants/status.ts b/src/features/notice/constants/status.ts
new file mode 100644
index 0000000..dd5561b
--- /dev/null
+++ b/src/features/notice/constants/status.ts
@@ -0,0 +1,22 @@
+import type { NoticeListType } from '../types';
+
+type StatusStyle = {
+ label: string;
+ bgColor: string;
+ color: string;
+};
+
+export const NOTICE_STATUS: Record = {
+ COMPLETED: { label: '종료', bgColor: '#F5F5F5', color: '#999' },
+ NOTIFY: {
+ label: '알림',
+ bgColor: 'rgba(255, 95, 95, 0.10)',
+ color: '#FA4545',
+ },
+ ONGOING: {
+ label: '진행',
+ bgColor: 'rgba(50, 205, 50, 0.10)',
+ color: '#32CD32',
+ },
+ UPCOMING: { label: '예정', bgColor: '#E9F6FF', color: '#377FF8' },
+};
diff --git a/src/features/notice/mock/notice.ts b/src/features/notice/mock/notice.ts
deleted file mode 100644
index 9fb973b..0000000
--- a/src/features/notice/mock/notice.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { Notice } from '../types';
-
-export const NOTICE_MOCK: Notice[] = [
- {
- id: 1,
- title: '2025년 봄학기 개강 안내',
- type: 'NOTIFICATION',
- date: '2025-03-01',
- },
- {
- id: 2,
- title: '홈페이지 점검 일정 안내',
- type: 'NOTIFICATION',
- date: '2025-03-05',
- },
- {
- id: 3,
- title: '중간고사 일정 공지',
- type: 'NOTIFICATION',
- date: '2025-04-10',
- },
- {
- id: 4,
- title: '신입생 오리엔테이션 안내',
- type: 'NOTIFICATION',
- date: '2025-02-25',
- },
- {
- id: 5,
- title: '장학금 신청 기간 안내',
- type: 'MESSAGE',
- date: '2025-03-15',
- },
- {
- id: 6,
- title: '건물 전기 점검에 따른 이용 제한 안내',
- type: 'MESSAGE',
- date: '2025-03-20',
- },
-];
diff --git a/src/features/notice/types/notice.ts b/src/features/notice/types/notice.ts
index c72e46b..0ea5c2c 100644
--- a/src/features/notice/types/notice.ts
+++ b/src/features/notice/types/notice.ts
@@ -1,8 +1,15 @@
-export type NoticeType = 'MESSAGE' | 'NOTIFICATION';
+export type NoticeType = 'UPCOMING' | 'NOTIFY';
+
+export type NoticeListType = NoticeType | 'ONGOING' | 'COMPLETED';
export type Notice = {
id: number;
title: string;
- type: NoticeType;
- date: string;
+ startAt: string;
+ endAt: string;
+ noticeType: NoticeType;
+};
+
+export type NoticeList = Omit & {
+ noticeStatus: NoticeListType;
};
diff --git a/src/features/notice/ui/NoticeItem.tsx b/src/features/notice/ui/NoticeItem.tsx
index 901e71f..f8e4d6c 100644
--- a/src/features/notice/ui/NoticeItem.tsx
+++ b/src/features/notice/ui/NoticeItem.tsx
@@ -1,25 +1,33 @@
import { MessageIcon, NotificationIcon } from '@/assets/icon';
-import type { Notice } from '../types';
+import type { NoticeList } from '../types';
-import { cn } from '@/shared/utils';
import { useFlow } from '@/app/stackflow';
import { PATH } from '@/shared/constants';
+import { NOTICE_STATUS } from '../constants';
-export default function NoticeItem({ id, title, type, date }: Notice) {
- const typeLabel = type === 'NOTIFICATION' ? '알림' : '예정';
+export default function NoticeItem({
+ id,
+ title,
+ noticeStatus,
+ startAt,
+ endAt,
+}: NoticeList) {
const { push } = useFlow();
+ const { label, bgColor, color } = NOTICE_STATUS[noticeStatus];
return (
-
{date}
+
{startAt}
);
diff --git a/src/features/notice/ui/NoticeList.tsx b/src/features/notice/ui/NoticeList.tsx
index e9f9e39..0f53cfc 100644
--- a/src/features/notice/ui/NoticeList.tsx
+++ b/src/features/notice/ui/NoticeList.tsx
@@ -1,12 +1,49 @@
-import { NOTICE_MOCK } from '../mock';
+import type { Campus } from '@/shared/types';
+import { Alert, CharacterFlat } from '@/assets/icon';
+
+import { useFetchNoticeByCampus } from '@/features/notice/api';
+import type { NoticeList } from '@/features/notice/types';
+
import NoticeItem from './NoticeItem';
-export default function NoticeList() {
+export default function NoticeList({ campus }: { campus: Campus }) {
+ const { data, isError, isFetching } = useFetchNoticeByCampus(campus);
+
+ const renderDataView = (data: NoticeList[]) => {
+ if (isFetching) return <>>;
+ if (data.length === 0 && isError) return ;
+ if (!isFetching && !isError && data.length === 0) return ;
+ return (
+ <>
+ {data.map(notice => (
+
+ ))}
+ >
+ );
+ };
+
return (
- {NOTICE_MOCK.map(({ id, title, type, date }) => (
-
- ))}
+ {renderDataView(data || [])}
);
}
+
+const Error = () => (
+
+

+
+ 공지사항을 불러오지 못했어요
+
+
인터넷 연결을 확인해주세요
+
+);
+
+const NoData = () => (
+
+

+
+ 아직 공지사항이 없어요!
+
+
+);
diff --git a/src/screen/admin-vote-edit/ui/AdminVoteEditScreen.tsx b/src/screen/admin-vote-edit/ui/AdminVoteEditScreen.tsx
index 5d5ce09..e18733d 100644
--- a/src/screen/admin-vote-edit/ui/AdminVoteEditScreen.tsx
+++ b/src/screen/admin-vote-edit/ui/AdminVoteEditScreen.tsx
@@ -18,7 +18,8 @@ export default function AdminVoteEditScreen() {
선거운동본부를 선택해주세요.
{
- if (data) {
- console.log('로그인 성공', data);
+ const handleLogin = async () => {
+ if (!data) return;
if (data.signedUp) {
- const kakaoAccessToken = data.tokenDto.accessToken;
- setUserToken({
- accessToken: kakaoAccessToken,
- });
- replace(RAW_PATH.HOME);
+ try {
+ const kakaoAccessToken = data.tokenDto.accessToken;
+ await setUserToken({ accessToken: kakaoAccessToken });
+ const userData = await fetchUserData();
+ const userInfo = userData.data?.results?.[0];
+ if (userInfo) {
+ sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
+ }
+ replace(RAW_PATH.HOME);
+ } catch (error) {
+ console.error('로그인 후 처리 중 오류', error);
+ alert('사용자 정보를 불러오는 데 실패했어요. 다시 시도해 주세요.');
+ replace(RAW_PATH.HOME);
+ }
} else {
- const kakaoEmail = data.kakaoEmail;
- setUserEmail({
- kakaoEmail: kakaoEmail,
- });
+ setUserEmail({ kakaoEmail: data.kakaoEmail });
alert('회원가입 화면으로 이동합니다!');
replace(RAW_PATH.SIGNUP);
}
- }
+ };
+
+ if (data) handleLogin();
if (isError) {
alert('로그인에 실패했어요. 다시 시도해 주세요!');
replace(RAW_PATH.HOME);
}
- }, [data, isError, setUserToken, setUserEmail]);
+ }, [data, isError, setUserToken, setUserEmail, fetchUserData]);
return (
diff --git a/src/screen/notice-content/ui/NoticeContentScreen.tsx b/src/screen/notice-content/ui/NoticeContentScreen.tsx
index 6624d4c..61e230e 100644
--- a/src/screen/notice-content/ui/NoticeContentScreen.tsx
+++ b/src/screen/notice-content/ui/NoticeContentScreen.tsx
@@ -5,14 +5,17 @@ import type { Notice } from '@/features/notice/types';
import type { ActivityComponentType } from '@stackflow/react';
import { NoticeContentContainer } from '@/widgets/notice-content/ui';
-const NoticeContentScreen: ActivityComponentType<{ notice: Notice }> = ({
- params,
-}: {
- params: { notice: Notice };
-}) => {
+const NoticeContentScreen: ActivityComponentType<{
+ notice: Omit
;
+}> = ({ params }: { params: { notice: Omit } }) => {
+ const { title, id, startAt, endAt } = params.notice;
return (
-
+
);
};
diff --git a/src/screen/vote-promise/ui/VotePromiseScreen.tsx b/src/screen/vote-promise/ui/VotePromiseScreen.tsx
index 2644a6d..3219217 100644
--- a/src/screen/vote-promise/ui/VotePromiseScreen.tsx
+++ b/src/screen/vote-promise/ui/VotePromiseScreen.tsx
@@ -1,18 +1,30 @@
+import type { ActivityComponentType } from '@stackflow/react';
import { AppScreen } from '@stackflow/plugin-basic-ui';
import { TitleAppBar } from '@/shared/ui';
import { VotePromiseBg } from '@/assets/image';
import { VotePromiseContainer } from '@/widgets/vote-promise/ui';
+import type { Candidate, Nominee } from '@/shared/types';
-export default function VotePromiseScreen() {
+const VotePledgeScreen: ActivityComponentType<{
+ nominees: Array;
+ candidates: Array;
+}> = ({
+ params,
+}: {
+ params: { nominees: Array; candidates: Array };
+}) => {
return (
- <>
-
-
-
- >
+
+
+
);
-}
+};
+
+export default VotePledgeScreen;
diff --git a/src/screen/vote/ui/VoteScreen.tsx b/src/screen/vote/ui/VoteScreen.tsx
index b7d3bed..ac44508 100644
--- a/src/screen/vote/ui/VoteScreen.tsx
+++ b/src/screen/vote/ui/VoteScreen.tsx
@@ -1,18 +1,22 @@
import { AppScreen } from '@stackflow/plugin-basic-ui';
+import type { ActivityComponentType } from '@stackflow/react';
import { TitleAppBar } from '@/shared/ui';
import { VoteBg } from '@/assets/image';
import { VoteContainer } from '@/widgets/vote/ui';
-export default function VoteScreen() {
+const VoteScreen: ActivityComponentType<{
+ id: number;
+ title: string;
+}> = ({ params }: { params: { id: number; title: string } }) => {
return (
- <>
-
-
-
- >
+
+
+
);
-}
+};
+
+export default VoteScreen;
diff --git a/src/shared/api/requests.ts b/src/shared/api/requests.ts
index d78273c..acf0a63 100644
--- a/src/shared/api/requests.ts
+++ b/src/shared/api/requests.ts
@@ -1,5 +1,12 @@
export const REQUEST = {
+ USER: '/api/users/',
LOGIN: '/auth/kakao',
JOIN: '/auth/kakao/signup',
REFRESH: '/auth/refresh',
+ ELECTION_ALL: '/api/elections/all',
+ NOTICE_CAMPUS: '/api/notices/notices/campus/',
+ NOTICE: '/api/notices/',
+ CANDIDATE: '/api/candidates/all/',
+ NOMINEE: '/api/nominees/{candidateId}/all',
+ PLEDGE: '/api/pledges/{candidateId}/all',
};
diff --git a/src/shared/api/user.ts b/src/shared/api/user.ts
index a23b461..c2f618e 100644
--- a/src/shared/api/user.ts
+++ b/src/shared/api/user.ts
@@ -27,7 +27,7 @@ type RefreshTokenResponse = {
};
const instance = axios.create({
- baseURL: 'https://www.festamate.shop/api',
+ baseURL: 'http://localhost:8080',
});
instance.interceptors.request.use(async config => {
diff --git a/src/shared/constants/campus.ts b/src/shared/constants/campus.ts
new file mode 100644
index 0000000..47cc9b2
--- /dev/null
+++ b/src/shared/constants/campus.ts
@@ -0,0 +1,6 @@
+import type { Campus } from '../types';
+
+export const CAMPUS: Record = {
+ SUWON: '수원캠',
+ SEOUL: '서울캠',
+};
diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts
index 395e34a..fbd4605 100644
--- a/src/shared/constants/index.ts
+++ b/src/shared/constants/index.ts
@@ -1,2 +1,3 @@
export * from './path';
export * from './bottomSheet';
+export * from './campus';
diff --git a/src/shared/hook/index.ts b/src/shared/hook/index.ts
index 9004b34..42c0013 100644
--- a/src/shared/hook/index.ts
+++ b/src/shared/hook/index.ts
@@ -1 +1,2 @@
export { default as useBottomSheet } from './useBottomSheet';
+export * from './useFetchUserInfo';
diff --git a/src/shared/hook/useFetchUserInfo.ts b/src/shared/hook/useFetchUserInfo.ts
new file mode 100644
index 0000000..c45c5a7
--- /dev/null
+++ b/src/shared/hook/useFetchUserInfo.ts
@@ -0,0 +1,43 @@
+import { useQuery } from '@tanstack/react-query';
+import { REQUEST, userGet } from '../api';
+import { fetchLoginStatus } from '../utils';
+import type { Role } from '../types';
+
+interface UserInfoResponse {
+ status: {
+ code: number;
+ message: string;
+ };
+ metadata: {
+ resultCount: number;
+ };
+ results: [
+ {
+ id: 1;
+ name: string;
+ collegeMajorName: string;
+ kakaoEmail: string;
+ refreshToken: string;
+ role: Role;
+ studentVerified: boolean;
+ studentEmail: string;
+ walletAddress: string;
+ },
+ ];
+}
+
+export const fetchUserInfo = async () => {
+ const response = await userGet({
+ request: REQUEST.USER,
+ });
+ return response.data;
+};
+
+export const useFetchUserInfo = () => {
+ return useQuery({
+ queryKey: ['user-info'],
+ queryFn: fetchUserInfo,
+ retry: 1,
+ enabled: fetchLoginStatus(),
+ });
+};
diff --git a/src/shared/types/campus.ts b/src/shared/types/campus.ts
new file mode 100644
index 0000000..40d9c74
--- /dev/null
+++ b/src/shared/types/campus.ts
@@ -0,0 +1 @@
+export type Campus = 'SUWON' | 'SEOUL';
diff --git a/src/shared/types/election.ts b/src/shared/types/election.ts
new file mode 100644
index 0000000..96d169e
--- /dev/null
+++ b/src/shared/types/election.ts
@@ -0,0 +1,37 @@
+import type { Campus } from './campus';
+
+export type Election = {
+ id: number;
+ title: string;
+ description: string;
+ startAt: string;
+ endAt: string;
+ isActive: boolean;
+ campus: Campus;
+ ownerId: number;
+ collageMajorName: string;
+};
+
+export type Candidate = {
+ id: number;
+ name: string;
+ electionId: number;
+ voteCount: number;
+};
+
+export type Nominee = {
+ id: number;
+ name: string;
+ studentId: string;
+ college: string;
+ department: string;
+ description: string;
+ candidateId: number;
+ main: boolean;
+};
+
+export type Pledge = {
+ id: number;
+ description: string;
+ candidateId: number;
+};
diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts
index 0433d04..eda329a 100644
--- a/src/shared/types/index.ts
+++ b/src/shared/types/index.ts
@@ -1,4 +1,6 @@
-export * from './path';
export * from './bottomSheet';
-export * from './vote';
+export * from './campus';
+export * from './election';
+export * from './path';
export * from './user';
+export * from './vote';
diff --git a/src/shared/types/user.ts b/src/shared/types/user.ts
index c5ea37a..c5bf0e7 100644
--- a/src/shared/types/user.ts
+++ b/src/shared/types/user.ts
@@ -14,3 +14,5 @@ export type Token = {
accessTokenExpiresIn: number;
refreshToken: string;
};
+
+export type Role = 'ROLE_ADMIN' | 'ROLE_USER';
diff --git a/src/shared/ui/Card.tsx b/src/shared/ui/Card.tsx
index 653c228..8e634fb 100644
--- a/src/shared/ui/Card.tsx
+++ b/src/shared/ui/Card.tsx
@@ -1,17 +1,19 @@
+import dayjs from 'dayjs';
import { useFlow } from '@/app/stackflow';
-import { PATH } from '@/shared/constants';
+import { CAMPUS, PATH } from '@/shared/constants';
import { cn, hexToRgba } from '@/shared/utils';
import { Button, DateBadge } from '@/shared/ui';
-import type { PathItem } from '@/shared/types';
+import type { Campus, PathItem } from '@/shared/types';
interface CardProps {
isStackCard?: boolean;
className?: string;
- campus: string;
- status: string;
+ campus: Campus;
+ status?: string;
title: string;
date: string;
+ id: number;
customTo?: PathItem;
}
@@ -19,10 +21,11 @@ export default function Card({
isStackCard = false,
className,
campus,
- status,
+ status = '진행중',
title,
date,
customTo,
+ id,
}: CardProps) {
const { push } = useFlow();
@@ -51,7 +54,7 @@ export default function Card({
- {campus}
+ {CAMPUS[campus]}
- {status === '진행중' &&
}
+ {status === '진행중' && (
+
+ )}
{title}
{date}
diff --git a/src/shared/utils/getDate.ts b/src/shared/utils/getDate.ts
new file mode 100644
index 0000000..df175cb
--- /dev/null
+++ b/src/shared/utils/getDate.ts
@@ -0,0 +1,11 @@
+import dayjs from 'dayjs';
+import localizedFormat from 'dayjs/plugin/localizedFormat';
+import 'dayjs/locale/ko';
+
+dayjs.extend(localizedFormat);
+dayjs.locale('ko');
+
+export const getDate = (date: Date | string, format: string) => {
+ const target = dayjs(date).locale('ko');
+ return target.format(format);
+};
diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts
index 4c27d5d..ead9d8c 100644
--- a/src/shared/utils/index.ts
+++ b/src/shared/utils/index.ts
@@ -4,3 +4,4 @@ export * from './fetchSessionData';
export * from './logout';
export * from './navigate';
export * from './string';
+export * from './getDate';
diff --git a/src/widgets/home/ui/HomeContainer.tsx b/src/widgets/home/ui/HomeContainer.tsx
index 299e5b7..32aec40 100644
--- a/src/widgets/home/ui/HomeContainer.tsx
+++ b/src/widgets/home/ui/HomeContainer.tsx
@@ -2,7 +2,7 @@ import { NoticeIcon, ResultIcon } from '@/assets/icon';
import { useFlow } from '@/app/stackflow';
import type { PathItem } from '@/shared/types';
import { PATH } from '@/shared/constants';
-import { CardStack } from '@/features/home/ui';
+import { CardSection } from '@/features/home/ui';
interface ServiceButtonProps {
title: string;
@@ -14,8 +14,7 @@ interface ServiceButtonProps {
export default function HomeContainer() {
return (
-
진행중인 선거가 있어요!
-
+
이런 서비스도 있어요
{title}
- 2025.03.19
+ {date}
-
- {data.split('\n').map((line, index) => (
-
- {line}
-
- ))}
-
+
);
}
diff --git a/src/widgets/notice/ui/NoticeContainer.tsx b/src/widgets/notice/ui/NoticeContainer.tsx
index 4925910..4f2d452 100644
--- a/src/widgets/notice/ui/NoticeContainer.tsx
+++ b/src/widgets/notice/ui/NoticeContainer.tsx
@@ -2,14 +2,18 @@ import { useState } from 'react';
import { cn } from '@/shared/utils';
import { NoticeList } from '@/features/notice/ui';
+import type { Campus } from '@/shared/types';
export default function NoticeContainer() {
const [selected, setSelected] = useState(0);
- const campus = ['수원캠', '서울캠'];
+ const CAMPUS: Record
= {
+ 수원캠: 'SUWON',
+ 서울캠: 'SEOUL',
+ };
return (
- {campus.map((campusName, index) => (
+ {Object.keys(CAMPUS).map((campusName, index) => (
-
+
);
}
diff --git a/src/widgets/vote-promise/api/index.ts b/src/widgets/vote-promise/api/index.ts
new file mode 100644
index 0000000..d6b79c1
--- /dev/null
+++ b/src/widgets/vote-promise/api/index.ts
@@ -0,0 +1 @@
+export * from './pledge';
diff --git a/src/widgets/vote-promise/api/pledge.ts b/src/widgets/vote-promise/api/pledge.ts
new file mode 100644
index 0000000..c947566
--- /dev/null
+++ b/src/widgets/vote-promise/api/pledge.ts
@@ -0,0 +1,32 @@
+import { REQUEST, userGet } from '@/shared/api';
+import type { Pledge } from '@/shared/types';
+import { useQuery } from '@tanstack/react-query';
+
+interface PledgeResponse {
+ status: {
+ code: number;
+ message: string;
+ };
+ metadata: {
+ resultCount: number;
+ };
+ results: Pledge[];
+}
+
+const fetchPledgeByCandidate = async (id: number) => {
+ const response = await userGet({
+ request: REQUEST.PLEDGE.split('{candidateId}').join(`${id}`),
+ });
+ return response.data;
+};
+
+export const useFetchPledgeData = (id: number) => {
+ return useQuery({
+ queryKey: [`candidate-${id}-pledge`],
+ queryFn: () => fetchPledgeByCandidate(id),
+ staleTime: Infinity,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ refetchInterval: false,
+ });
+};
diff --git a/src/widgets/vote-promise/ui/VoteCandidate.tsx b/src/widgets/vote-promise/ui/VoteCandidate.tsx
index af4125b..185af77 100644
--- a/src/widgets/vote-promise/ui/VoteCandidate.tsx
+++ b/src/widgets/vote-promise/ui/VoteCandidate.tsx
@@ -1,13 +1,20 @@
-interface VoteCandidateProps {
- name: string;
-}
-export default function VoteCandidate({ name }: VoteCandidateProps) {
+import type { Nominee } from '@/shared/types';
+
+export default function VoteCandidate({
+ name,
+ main,
+ college,
+ department,
+ studentId,
+ description,
+}: Nominee) {
return (
- <>
- 정후보자 {name}
- 컴퓨터공학부 04
- 기아기아 최강기아 역임
- 기아의 해결사 역임
- >
+
+
+ {main ? '정후보자' : '부후보자'} {name}
+
+
{`${college} ${department} ${studentId.slice(2, 4)}`}
+
{description}
+
);
}
diff --git a/src/widgets/vote-promise/ui/VotePromiseCard.tsx b/src/widgets/vote-promise/ui/VotePromiseCard.tsx
index fbbac6a..ac25661 100644
--- a/src/widgets/vote-promise/ui/VotePromiseCard.tsx
+++ b/src/widgets/vote-promise/ui/VotePromiseCard.tsx
@@ -2,15 +2,70 @@ import { useState } from 'react';
import { motion } from 'framer-motion';
import { VerifiedCheckIcon } from '@/assets/icon';
-import VoteCandidate from './VoteCandidate';
+import type { Candidate, Nominee, Pledge } from '@/shared/types';
import { cn } from '@/shared/utils';
-export default function VotePromiseCard() {
+import VoteCandidate from './VoteCandidate';
+import { useFetchPledgeData } from '../api';
+
+interface VotePromiseCardProps {
+ nominees: Nominee[];
+ candidateName: string;
+ candidate: Candidate;
+}
+
+export default function VotePromiseCard({
+ nominees,
+ candidateName,
+ candidate,
+}: VotePromiseCardProps) {
const [flipped, setFlipped] = useState(false);
+ const { data: pledge } = useFetchPledgeData(candidate.id);
+
+ const CardFront = () => (
+
+
+
+ 1/2
+
+
+ {nominees.map(nominee => (
+
+ ))}
+
+
+ );
+
+ const CardBack = () => (
+
+
+
+ 2/2
+
+
+
+ '{candidateName}'의 핵심공약
+
+ {renderPledge(pledge?.results || [])}
+
+
+ );
+
+ const renderPledge = (data: Pledge[]) => {
+ if (data)
+ return (
+
+ {data.map(({ id, description }) => (
+
+ ))}
+
+ );
+ else return <>>;
+ };
return (
setFlipped(!flipped)}
>
(
-
-
-
- 1/2
-
-
-
-
-
-
-
-
-);
-
-const CardBack = () => (
-
-
-
- 2/2
-
-
-
- '기아'의 핵심공약
-
-
-
-
-
-
-
-
-
-
-);
-
const TeamBadge = ({
team,
flipped = false,
diff --git a/src/widgets/vote-promise/ui/VotePromiseContainer.tsx b/src/widgets/vote-promise/ui/VotePromiseContainer.tsx
index 8a45c2a..d6de9a1 100644
--- a/src/widgets/vote-promise/ui/VotePromiseContainer.tsx
+++ b/src/widgets/vote-promise/ui/VotePromiseContainer.tsx
@@ -3,18 +3,32 @@ import VotePromiseCard from './VotePromiseCard';
import { cn } from '@/shared/utils';
import { Button } from '@/shared/ui';
import { useFlow } from '@/app/stackflow';
+import type { Candidate, Nominee } from '@/shared/types';
-export default function VotePromiseContainer() {
+interface VotePromiseContainerProps {
+ nomineeData: Array;
+ candidateData: Array;
+}
+
+export default function VotePromiseContainer({
+ nomineeData,
+ candidateData,
+}: VotePromiseContainerProps) {
const [selectedCard, setSelectedCard] = useState(0);
const { pop } = useFlow();
const promises = Array.from({ length: 2 });
+ const candidates = candidateData.map(candidate => candidate.name);
return (
카드를 탭해서 뒤집어보세요!
-
+
{promises.map((_, i) => (
@@ -24,7 +38,7 @@ export default function VotePromiseContainer() {
selectedCard === i
? 'border-m bg-mxl text-m border-[1px]'
: 'text-sl bg-white',
- 'grid size-15 place-items-center rounded-lg text-3xl font-bold',
+ 'grid size-15 cursor-pointer place-items-center rounded-lg text-3xl font-bold',
)}
onClick={() => setSelectedCard(i)}
>
diff --git a/src/widgets/vote/api/candidate.ts b/src/widgets/vote/api/candidate.ts
new file mode 100644
index 0000000..4ecc77e
--- /dev/null
+++ b/src/widgets/vote/api/candidate.ts
@@ -0,0 +1,29 @@
+import { REQUEST, userGet } from '@/shared/api';
+import type { Candidate, Nominee } from '@/shared/types';
+import { useQuery } from '@tanstack/react-query';
+
+interface CandidateDetailResponse {
+ status: {
+ code: number;
+ message: string;
+ };
+ metadata: {
+ resultCount: number;
+ };
+ results: Nominee[];
+}
+
+const fetchCandidateDetail = async (id: number) => {
+ const response = await userGet
({
+ request: REQUEST.NOMINEE.split('{candidateId}').join(`${id}`).toString(),
+ });
+ return response.data;
+};
+
+export const useFetchCandidateDetail = (id?: number, candidate?: Candidate) => {
+ return useQuery({
+ queryKey: ['candidate-detail', `${id}`],
+ queryFn: () => fetchCandidateDetail(id!),
+ enabled: typeof candidate !== 'undefined',
+ });
+};
diff --git a/src/widgets/vote/api/election.ts b/src/widgets/vote/api/election.ts
new file mode 100644
index 0000000..20f6af0
--- /dev/null
+++ b/src/widgets/vote/api/election.ts
@@ -0,0 +1,28 @@
+import { useQuery } from '@tanstack/react-query';
+import { REQUEST, userGet } from '@/shared/api';
+import type { Candidate } from '@/shared/types';
+
+interface ElectionDetailResponse {
+ status: {
+ code: number;
+ message: string;
+ };
+ metadata: {
+ resultCount: number;
+ };
+ results: Candidate[];
+}
+
+const fetchElectionDetail = async (id: number) => {
+ const response = await userGet({
+ request: REQUEST.CANDIDATE + `${id}`,
+ });
+ return response.data;
+};
+
+export const useFetchElectionDetail = (id: number) => {
+ return useQuery({
+ queryKey: ['election-detail', `${id}`],
+ queryFn: () => fetchElectionDetail(id),
+ });
+};
diff --git a/src/widgets/vote/ui/VoteContainer.tsx b/src/widgets/vote/ui/VoteContainer.tsx
index 8af7a9a..20b6e78 100644
--- a/src/widgets/vote/ui/VoteContainer.tsx
+++ b/src/widgets/vote/ui/VoteContainer.tsx
@@ -7,47 +7,81 @@ import { Button } from '@/shared/ui';
import { PATH } from '@/shared/constants';
import { VoteItem } from '@/widgets/vote/ui';
-import { VOTE_MOCK } from '@/widgets/vote/mock';
+import { useFetchElectionDetail } from '../api/election';
+import { useFetchCandidateDetail } from '../api/candidate';
-export default function VoteContainer() {
+interface VoteContainerProps {
+ electionId: number;
+ title: string;
+}
+
+export default function VoteContainer({
+ electionId,
+ title,
+}: VoteContainerProps) {
const [selected, setSelected] = useState(-1);
const { push, replace } = useFlow();
+ const { data: candidates } = useFetchElectionDetail(electionId);
+
+ const { data: nominee1 } = useFetchCandidateDetail(
+ candidates?.results[0].id,
+ candidates?.results[0],
+ );
+ const { data: nominee2 } = useFetchCandidateDetail(
+ candidates?.results[1].id,
+ candidates?.results[1],
+ );
const handleClick = (index: number) => {
if (selected === index) setSelected(-1);
else setSelected(index);
};
+ const renderCandidates = () => {
+ if (candidates) {
+ return (
+
+ {candidates.results.map(({ name, id }, index) => (
+
handleClick(index)}
+ />
+ ))}
+
+ VS
+
+
+ );
+ } else return <>>;
+ };
+
return (
-
-
2025학년도
-
총학생회 선거 후보
+
+ {title.split(' ')[0]}
+
+ {title.split(' ').slice(1).join(' ') + ' 후보'}
공약을 살펴보고 신중하게 투표해주세요
-
- {VOTE_MOCK.map(({ title, candidates }, index) => (
-
handleClick(index)}
- />
- ))}
-
- VS
-
-
+ {renderCandidates()}
);