diff --git a/index.html b/index.html index 24a65e6..bf8bd93 100644 --- a/index.html +++ b/index.html @@ -10,6 +10,11 @@ as="image" href="/src/assets/image/bg-vote-promise.png" /> + diff --git a/package.json b/package.json index 07b5b07..72443ad 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@lottiefiles/dotlottie-react": "^0.14.2", "@stackflow/core": "^1.2.0", "@stackflow/plugin-basic-ui": "^1.14.1", "@stackflow/plugin-renderer-basic": "^1.1.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dff9fb0..a2a65f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@lottiefiles/dotlottie-react': + specifier: ^0.14.2 + version: 0.14.2(react@19.1.0) '@stackflow/core': specifier: ^1.2.0 version: 1.2.0 @@ -361,6 +364,14 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@lottiefiles/dotlottie-react@0.14.2': + resolution: {integrity: sha512-RR4r0HrKQbOAw6iS6C3mRARS2iu+yI+G1vICoUsRMHzlUUk1/26l3WyAjhcG+KoaGoKmORx8FgHjTNr4Sr/2Ug==} + peerDependencies: + react: ^17 || ^18 || ^19 + + '@lottiefiles/dotlottie-web@0.47.0': + resolution: {integrity: sha512-YN6wSB4iYZBYEAFKEs/taufrPH3rfNlUA632Ib61WoR58TALAJ1ZX8yDIGUBT28byMJhZR4+xdpRX4v7X8OeBQ==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1970,6 +1981,13 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@lottiefiles/dotlottie-react@0.14.2(react@19.1.0)': + dependencies: + '@lottiefiles/dotlottie-web': 0.47.0 + react: 19.1.0 + + '@lottiefiles/dotlottie-web@0.47.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 diff --git a/src/app/stackflow/Stack.tsx b/src/app/stackflow/Stack.tsx index 62e29c3..b2dec08 100644 --- a/src/app/stackflow/Stack.tsx +++ b/src/app/stackflow/Stack.tsx @@ -14,6 +14,7 @@ import { NoticeCreateScreen } from '@/screen/notice-create/ui'; import { NoticeScreen } from '@/screen/notice/ui'; import { VoteCompleteScreen } from '@/screen/vote-complete/ui'; import { VoteCreateCompleteScreen } from '@/screen/vote-create-complete/ui'; +import { VoteCreateLoadingScreen } from '@/screen/vote-create-loading/ui'; import { VoteCreateScreen } from '@/screen/vote-create/ui'; import { VoteEditScreen } from '@/screen/vote-edit/ui'; import { VotePromiseScreen } from '@/screen/vote-promise/ui'; @@ -37,6 +38,7 @@ export const { Stack, useFlow } = stackflow({ VoteEditScreen, VoteScreen, VoteCreateScreen, + VoteCreateLoadingScreen, VoteCreateCompleteScreen, VoteCompleteScreen, VotePromiseScreen, diff --git a/src/assets/icon/icon-user-square.svg b/src/assets/icon/icon-user-square.svg new file mode 100644 index 0000000..7c81cc3 --- /dev/null +++ b/src/assets/icon/icon-user-square.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icon/index.ts b/src/assets/icon/index.ts index e022a39..d25f58e 100644 --- a/src/assets/icon/index.ts +++ b/src/assets/icon/index.ts @@ -16,6 +16,7 @@ import NotificationIcon from './icon-notification.png'; import ResultIcon from './icon-result.png'; import TextBoxIcon from './icon-textbox.svg'; import UserIcon from './icon-user.svg'; +import UserSquareIcon from './icon-user-square.svg'; import VoteCompleteIcon from './icon-vote-complete.png'; import VoteIcon from './icon-vote.svg'; import WriteIcon from './icon-write.svg'; @@ -38,6 +39,7 @@ export { ResultIcon, TextBoxIcon, UserIcon, + UserSquareIcon, VerifiedCheckIcon, VoteCompleteIcon, VoteIcon, diff --git a/src/features/admin-dashboard/api/election.ts b/src/features/admin-dashboard/api/election.ts new file mode 100644 index 0000000..c9672f7 --- /dev/null +++ b/src/features/admin-dashboard/api/election.ts @@ -0,0 +1,19 @@ +import { REQUEST, userGet } from '@/shared/api'; +import type { Election, VoteStatus } from '@/shared/types'; +import { useQuery } from '@tanstack/react-query'; + +const fetchElectionByStatus = async (status: VoteStatus) => { + const response = await userGet({ + request: REQUEST.ELECTION_STATUS, + params: { status: status }, + }); + return response.data; +}; + +export const useFetchElectionByStatus = (status: VoteStatus) => { + return useQuery({ + queryKey: ['election-status', `${status}`], + queryFn: () => fetchElectionByStatus(status), + staleTime: 60 * 5, + }); +}; diff --git a/src/features/admin-dashboard/api/index.ts b/src/features/admin-dashboard/api/index.ts new file mode 100644 index 0000000..71fe4e1 --- /dev/null +++ b/src/features/admin-dashboard/api/index.ts @@ -0,0 +1 @@ +export * from './election'; diff --git a/src/features/admin-dashboard/ui/CardList.tsx b/src/features/admin-dashboard/ui/CardList.tsx index 09adf57..1315a35 100644 --- a/src/features/admin-dashboard/ui/CardList.tsx +++ b/src/features/admin-dashboard/ui/CardList.tsx @@ -1,25 +1,52 @@ -import type { VoteStatus } from '@/shared/types'; -// import { Card } from '@/shared/ui'; -import { VOTE_MOCK } from '@/features/admin-dashboard/mock'; +import type { Election, VoteStatus } from '@/shared/types'; +import { Card } from '@/shared/ui'; +import { getDate } from '@/shared/utils'; -export default function CardList({ status }: { status: VoteStatus }) { - const data = VOTE_MOCK.filter( - ({ status: voteStatus }) => voteStatus === status, +import { useFetchElectionByStatus } from '@/features/admin-dashboard/api'; + +export default function CardList({ status }: { status: string }) { + const VOTE_STATUS: Record = { + 진행중: 'ongoing', + 예정: 'upcoming', + 종료: 'ended', + }; + const { data, isError, isFetching } = useFetchElectionByStatus( + VOTE_STATUS[status], ); - console.log(data); - + + const renderElection = (data: Election[]) => { + if (isError) + return ( +
+ 투표를 가져오던 중 오류가 발생했어요! +
+ ); + if (!isFetching && data.length === 0) + return ( +
투표가 없어요
+ ); + if (data && data.length > 0) + return ( + <> + {data.map(({ id, campus, title, startAt, endAt }, index) => ( + + ))} + + ); + return <>; + }; + return (
-{/* {data.map(({ id, campus, status, title, date }, index) => ( - - ))} */} + {renderElection(data || [])}
); } diff --git a/src/features/notice-create/api/create.ts b/src/features/notice-create/api/create.ts new file mode 100644 index 0000000..5f9149a --- /dev/null +++ b/src/features/notice-create/api/create.ts @@ -0,0 +1,26 @@ +import { useFlow } from '@/app/stackflow'; +import { useMutation } from '@tanstack/react-query'; + +import { REQUEST, userPost } from '@/shared/api'; +import type { Replace, WholeCampus, Notice } from '@/shared/types'; + +const submitNotice = async (data: Replace) => { + const response = await userPost({ + request: REQUEST.NOTICE.slice(0, -1), + data: data, + }); + return response; +}; + +export const useSubmitNotice = () => { + const { pop } = useFlow(); + + return useMutation({ + mutationFn: (data: Replace) => + submitNotice(data), + onSuccess: () => { + alert('공지 등록 성공!'); + pop({ animate: false }); + }, + }); +}; diff --git a/src/features/notice-create/api/index.ts b/src/features/notice-create/api/index.ts new file mode 100644 index 0000000..1e03cce --- /dev/null +++ b/src/features/notice-create/api/index.ts @@ -0,0 +1 @@ +export * from './create'; diff --git a/src/features/notice-create/ui/NoticeForm.tsx b/src/features/notice-create/ui/NoticeForm.tsx index fcc492d..500805b 100644 --- a/src/features/notice-create/ui/NoticeForm.tsx +++ b/src/features/notice-create/ui/NoticeForm.tsx @@ -1,35 +1,108 @@ +import { useForm } from 'react-hook-form'; +import { useEffect, useState } from 'react'; + +import type { + Campus, + Replace, + WholeCampus, + Notice, + NoticeType, +} from '@/shared/types'; +import { CAMPUS, NOTICE_TYPE } from '@/shared/constants'; +import { keys } from '@/shared/utils'; +import { Button } from '@/shared/ui'; + +import { useSubmitNotice } from '@/features/notice-create/api'; + import NoticeFormButton from './NoticeFormButton'; import NoticeFormInput from './NoticeFormInput'; import NoticeFormItem from './NoticeFormItem'; export default function NoticeForm() { + const { mutate } = useSubmitNotice(); + const [noticeType, setNoticeType] = useState(); + const [campus, setCampus] = useState([]); + const { register, handleSubmit, watch, setValue } = useForm< + Replace + >({ + defaultValues: { + startAt: '2025-06-27', + endAt: '2025-06-28', + }, + }); + const isFormValid = + watch('title') && + watch('campus') && + watch('content') && + watch('noticeType'); + + useEffect(() => { + const value = campus.length > 1 ? ('ALL' as const) : campus[0]; + setValue('campus', value); + }, [campus, setValue]); + return ( -
- -
- - -
-
- -
- - -
-
- -
-
- ~ -
-
- - - - - -