diff --git a/src/apis/requiredFields.ts b/src/apis/requiredFields.ts new file mode 100644 index 0000000..d5f833b --- /dev/null +++ b/src/apis/requiredFields.ts @@ -0,0 +1,12 @@ +import { RequiredFieldsDto } from 'types/DTO/requiredFieldsDto'; +import apiClient from './apiClient'; + +export const getRequiredFields = async (contestId: number): Promise => { + const { data } = await apiClient.get(`/contests/${contestId}/team-detail-template`); + return data; +}; + +export const putRequiredFields = async (contestId: number, payload: RequiredFieldsDto) => { + const { data } = await apiClient.put(`/contests/${contestId}/team-detail-template`, payload); + return data; +}; diff --git a/src/components/ui/Checkbox.tsx b/src/components/ui/Checkbox.tsx new file mode 100644 index 0000000..8c9d8c7 --- /dev/null +++ b/src/components/ui/Checkbox.tsx @@ -0,0 +1,32 @@ +import { FaCheck } from 'react-icons/fa'; + +interface CheckboxProps { + checked: boolean; + onChange?: (checked: boolean) => void; + disabled?: boolean; + ariaLabel?: string; +} + +const Checkbox = ({ checked, onChange, disabled = false, ariaLabel }: CheckboxProps) => { + return ( +
+ onChange?.(e.target.checked)} + disabled={disabled} + className="peer sr-only" + /> + +
+ ); +}; + +export default Checkbox; diff --git a/src/constants/requiredFields.ts b/src/constants/requiredFields.ts new file mode 100644 index 0000000..ebfb013 --- /dev/null +++ b/src/constants/requiredFields.ts @@ -0,0 +1,31 @@ +import { RequiredFieldsDto } from 'types/DTO/requiredFieldsDto'; + +export const defaultRequiredFields: RequiredFieldsDto = { + division: 'OPTIONAL', + projectName: 'OPTIONAL', + teamName: 'OPTIONAL', + leader: 'OPTIONAL', + teamMembers: 'OPTIONAL', + professor: 'OPTIONAL', + githubPath: 'OPTIONAL', + youtubePath: 'OPTIONAL', + productionPath: 'OPTIONAL', + overview: 'OPTIONAL', + poster: 'OPTIONAL', + images: 'OPTIONAL', +}; + +export const labelByField: Record = { + division: '분과', + projectName: '프로젝트명', + teamName: '팀명', + leader: '팀장', + teamMembers: '팀원', + professor: '지도교수', + githubPath: 'GitHub 링크', + youtubePath: 'Youtube 링크', + productionPath: '배포 링크', + overview: '프로젝트 개요', + poster: '포스터', + images: '이미지', +}; diff --git a/src/hooks/useRequiredFields.ts b/src/hooks/useRequiredFields.ts new file mode 100644 index 0000000..e0da4c2 --- /dev/null +++ b/src/hooks/useRequiredFields.ts @@ -0,0 +1,48 @@ +import { useEffect, useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { defaultRequiredFields } from 'constants/requiredFields'; +import { RequiredFieldsDto, RequiredFieldsType } from 'types/DTO/requiredFieldsDto'; +import { useToast } from './useToast'; +import { getRequiredFields, putRequiredFields } from 'apis/requiredFields'; + +export const useRequiredFields = (contestId: number) => { + const [fieldsSetting, setFieldsSetting] = useState(defaultRequiredFields); + const toast = useToast(); + const queryClient = useQueryClient(); + + const { data: requiredFields } = useQuery({ + queryKey: ['requiredFields', contestId], + queryFn: () => getRequiredFields(contestId), + enabled: !!contestId, + }); + const updateRequiredFields = useMutation({ + mutationKey: [`updateRequiredFields`], + mutationFn: (payload: RequiredFieldsDto) => putRequiredFields(contestId, payload), + }); + + useEffect(() => { + if (requiredFields) { + setFieldsSetting(requiredFields); + } + }, [requiredFields]); + + const toggleField = (key: string, value: RequiredFieldsType) => { + setFieldsSetting((prev) => ({ ...prev, [key]: value })); + }; + + const handleSave = async () => { + if (!fieldsSetting) return; + + updateRequiredFields.mutate(fieldsSetting, { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['requiredFields', contestId] }); + toast('필수 항목 설정이 저장되었습니다.', 'success'); + }, + onError: () => { + toast('필수 항목 설정 저장에 실패했습니다.', 'error'); + }, + }); + }; + + return { fieldsSetting, isLoading: updateRequiredFields.isPending, toggleField, handleSave }; +}; diff --git a/src/pages/admin/required-field/RequiredFields.tsx b/src/pages/admin/required-field/RequiredFields.tsx new file mode 100644 index 0000000..7f394db --- /dev/null +++ b/src/pages/admin/required-field/RequiredFields.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import Checkbox from '@components/ui/Checkbox'; +import { RequiredFieldsDto, RequiredFieldsType } from 'types/DTO/requiredFieldsDto'; +import { labelByField } from 'constants/requiredFields'; +import { cn } from 'utils/classname'; + +interface RequiredFieldsProps { + fields: RequiredFieldsDto; + onToggle: (key: string, value: RequiredFieldsType) => void; + className?: string; +} + +const RequiredFields: React.FC = ({ fields, onToggle, className = '' }) => { + return ( +
+
+
+
필드명
+
필수
+
+
+ {Object.entries(fields).map(([key, value]) => ( +
+
{labelByField[key]}
+
+ onToggle(key, checked ? 'REQUIRED' : 'OPTIONAL')} + ariaLabel={`${labelByField[key]} 필수 여부`} + /> +
+
+ ))} +
+
+
+ ); +}; + +export default RequiredFields; diff --git a/src/pages/admin/required-field/RequiredFieldsPage.tsx b/src/pages/admin/required-field/RequiredFieldsPage.tsx new file mode 100644 index 0000000..3845be4 --- /dev/null +++ b/src/pages/admin/required-field/RequiredFieldsPage.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import RequiredFields from '@pages/admin/required-field/RequiredFields'; +import { Button } from '@components/ui/button'; +import { useRequiredFields } from 'hooks/useRequiredFields'; +import { useParams } from 'react-router-dom'; + +const RequiredFieldsPage: React.FC = () => { + const { contestId } = useParams(); + const { fieldsSetting, isLoading, toggleField, handleSave } = useRequiredFields(Number(contestId ?? 0)); + + return ( +
+
+
+

필수 항목 설정

+

프로젝트 생성/수정 폼의 필수 항목을 설정합니다.

+
+
+ +
+
+ +
+ ); +}; + +export default RequiredFieldsPage; diff --git a/src/route/AppRoutes.tsx b/src/route/AppRoutes.tsx index 3921ce3..c075efe 100644 --- a/src/route/AppRoutes.tsx +++ b/src/route/AppRoutes.tsx @@ -18,6 +18,7 @@ import AdminLayout from '@layout/admin/AdminLayout'; import AdminContestLayout from '@layout/admin/contest/AdminContestLayout'; import FullContainer from '@layout/FullContainer'; import TeamOrderAdminPage from '@pages/admin/team-order/TeamOrderAdminPage'; +import RequiredFieldsPage from '@pages/admin/required-field/RequiredFieldsPage'; const AppRoutes = () => createBrowserRouter([ @@ -61,7 +62,7 @@ const AppRoutes = () => { path: 'projects', element:
프로젝트 관리
}, { path: 'team-order', element: }, { path: 'awards', element:
수상 관리
}, - { path: 'required-fields', element:
필수 항목 설정
}, + { path: 'required-fields', element: }, // 대회 { path: 'settings', element:
대회 관리
}, { path: 'departments', element:
분과 관리
}, diff --git a/src/types/DTO/requiredFieldsDto.ts b/src/types/DTO/requiredFieldsDto.ts new file mode 100644 index 0000000..eafb694 --- /dev/null +++ b/src/types/DTO/requiredFieldsDto.ts @@ -0,0 +1,16 @@ +export type RequiredFieldsType = 'REQUIRED' | 'OPTIONAL'; + +export interface RequiredFieldsDto { + division: RequiredFieldsType; + projectName: RequiredFieldsType; + teamName: RequiredFieldsType; + leader: RequiredFieldsType; + teamMembers: RequiredFieldsType; + professor: RequiredFieldsType; + githubPath: RequiredFieldsType; + youtubePath: RequiredFieldsType; + productionPath: RequiredFieldsType; + overview: RequiredFieldsType; + poster: RequiredFieldsType; + images: RequiredFieldsType; +}