diff --git a/frontend/src/apis/application/createApplication.ts b/frontend/src/apis/application/createApplication.ts new file mode 100644 index 000000000..a2ad1fadc --- /dev/null +++ b/frontend/src/apis/application/createApplication.ts @@ -0,0 +1,33 @@ +import API_BASE_URL from '@/constants/api'; +import { secureFetch } from '@/apis/auth/secureFetch'; +import { ApplicationFormData } from '@/types/application'; + +export const createApplication = async ( + data: ApplicationFormData, + clubId: string, +) => { + try { + const response = await secureFetch( + `${API_BASE_URL}/api/club/${clubId}/application`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }, + ); + + if (!response.ok) { + throw new Error('지원서 제출에 실패했습니다.'); + } + + const result = await response.json(); + return result.data; + } catch (error) { + console.error('지원서 제출 중 오류 발생:', error); + throw error; + } +}; + +export default createApplication; diff --git a/frontend/src/apis/application/getApplication.ts b/frontend/src/apis/application/getApplication.ts new file mode 100644 index 000000000..a9ec0e0bc --- /dev/null +++ b/frontend/src/apis/application/getApplication.ts @@ -0,0 +1,20 @@ +import API_BASE_URL from '@/constants/api'; + +const getApplication = async (clubId: string) => { + try { + const response = await fetch(`${API_BASE_URL}/api/club/${clubId}/apply`); + if (!response.ok) { + throw new Error(`Failed to fetch: ${response.statusText}`); + } + + const result = await response.json(); + return result.data; + } catch (error) { + // [x] FIXME: + // {"statuscode":"800-1","message":"지원서가 존재하지 않습니다.","data":null} + console.error('Error fetching club details', error); + throw error; + } +}; + +export default getApplication; diff --git a/frontend/src/apis/application/updateApplication.ts b/frontend/src/apis/application/updateApplication.ts new file mode 100644 index 000000000..6111af261 --- /dev/null +++ b/frontend/src/apis/application/updateApplication.ts @@ -0,0 +1,33 @@ +import API_BASE_URL from '@/constants/api'; +import { secureFetch } from '@/apis/auth/secureFetch'; +import { ApplicationFormData } from '@/types/application'; + +export const updateApplication = async ( + data: ApplicationFormData, + clubId: string, +) => { + try { + const response = await secureFetch( + `${API_BASE_URL}/api/club/${clubId}/application`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }, + ); + + if (!response.ok) { + throw new Error('지원서 수정에 실패했습니다.'); + } + + const result = await response.json(); + return result.data; + } catch (error) { + console.error('지원서 수정 중 오류 발생:', error); + throw error; + } +}; + +export default updateApplication; diff --git a/frontend/src/constants/INITIAL_FORM_DATA.ts b/frontend/src/constants/INITIAL_FORM_DATA.ts index c19fedd56..e97cf76f1 100644 --- a/frontend/src/constants/INITIAL_FORM_DATA.ts +++ b/frontend/src/constants/INITIAL_FORM_DATA.ts @@ -1,7 +1,7 @@ import { ApplicationFormData } from '@/types/application'; const INITIAL_FORM_DATA: ApplicationFormData = { - form_title: '', + title: '', questions: [ { id: 1, @@ -9,6 +9,7 @@ const INITIAL_FORM_DATA: ApplicationFormData = { description: '', type: 'SHORT_TEXT', options: { required: true }, + items: [], }, { id: 2, diff --git a/frontend/src/hooks/queries/application/useGetApplication.ts b/frontend/src/hooks/queries/application/useGetApplication.ts new file mode 100644 index 000000000..28d8865c2 --- /dev/null +++ b/frontend/src/hooks/queries/application/useGetApplication.ts @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query'; +import getApplication from '@/apis/application/getApplication'; + +export const useGetApplication = (clubId: string) => { + return useQuery({ + queryKey: ['applicationForm', clubId], + queryFn: () => getApplication(clubId), + retry: false, + }); +}; diff --git a/frontend/src/mocks/api/apply.ts b/frontend/src/mocks/api/apply.ts index 039920ad9..56f6a4eee 100644 --- a/frontend/src/mocks/api/apply.ts +++ b/frontend/src/mocks/api/apply.ts @@ -25,7 +25,7 @@ export const applyHandlers = [ return HttpResponse.json( { clubId, - form_title: mockData.form_title, + form_title: mockData.title, questions: mockData.questions, }, { status: 200 }, diff --git a/frontend/src/mocks/data/mockData.ts b/frontend/src/mocks/data/mockData.ts index e81e8ea99..45c93df72 100644 --- a/frontend/src/mocks/data/mockData.ts +++ b/frontend/src/mocks/data/mockData.ts @@ -23,7 +23,7 @@ export interface Question { } export const mockData: ApplicationFormData = { - form_title: '2025_2_지원서', + title: '2025_2_지원서', questions: [ { id: 1, diff --git a/frontend/src/pages/AdminPage/application/CreateApplicationForm.tsx b/frontend/src/pages/AdminPage/application/CreateApplicationForm.tsx index a37828f2d..bc3be5bcc 100644 --- a/frontend/src/pages/AdminPage/application/CreateApplicationForm.tsx +++ b/frontend/src/pages/AdminPage/application/CreateApplicationForm.tsx @@ -1,25 +1,36 @@ -// 지원서 제작하기 : 지원서 제작 컴포넌트 -// 지원서 수정과 제작을 맡을 컴포넌트 -// Todo: 질문 삭제 및 질문 추가 기능 구현 -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import QuestionBuilder from '@/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder'; import { QuestionType } from '@/types/application'; import { Question } from '@/types/application'; -import { mockData } from '@/mocks/data/mockData'; import { ApplicationFormData } from '@/types/application'; import { PageContainer } from '@/styles/PageContainer.styles'; import * as Styled from './CreateApplicationForm.styles'; import INITIAL_FORM_DATA from '@/constants/INITIAL_FORM_DATA'; import { QuestionDivider } from './CreateApplicationForm.styles'; +import { useAdminClubContext } from '@/context/AdminClubContext'; +import { useGetApplication } from '@/hooks/queries/application/useGetApplication'; +import createApplication from '@/apis/application/createApplication'; +import updateApplication from '@/apis/application/updateApplication'; const CreateApplicationForm = () => { - const [formData, setFormData] = useState( - mockData ?? INITIAL_FORM_DATA, - ); + const { clubId } = useAdminClubContext(); + if (!clubId) return null; + + const { data, isLoading, isError } = useGetApplication(clubId); + + const [formData, setFormData] = + useState(INITIAL_FORM_DATA); + + useEffect(() => { + if (data) { + setFormData(data); + } + }, [data]); + const [nextId, setNextId] = useState(() => { - const questions = mockData?.questions ?? INITIAL_FORM_DATA.questions; + const questions = data?.questions ?? INITIAL_FORM_DATA.questions; if (questions.length === 0) return 1; - const maxId = Math.max(...questions.map((q) => q.id)); + const maxId = Math.max(...questions.map((q: Question) => q.id)); return maxId + 1; }); @@ -29,6 +40,7 @@ const CreateApplicationForm = () => { title: '', description: '', type: 'SHORT_TEXT', + items: [], options: { required: false }, }; setFormData((prev) => ({ @@ -61,7 +73,7 @@ const CreateApplicationForm = () => { const handleFormTitleChange = (value: string) => { setFormData((prev) => ({ ...prev, - form_title: value, + title: value, })); }; @@ -87,7 +99,7 @@ const CreateApplicationForm = () => { ? q.items && q.items.length >= 2 ? q.items : [{ value: '' }, { value: '' }] - : undefined, + : [], }; }), })); @@ -102,12 +114,37 @@ const CreateApplicationForm = () => { })); }; + const handleSubmit = async () => { + if (!clubId) return; + const reorderedQuestions = formData.questions.map((q, idx) => ({ + ...q, + id: idx + 1, + })); + + const payload: ApplicationFormData = { + ...formData, + questions: reorderedQuestions, + }; + try { + if (data) { + await updateApplication(payload, clubId); + alert('지원서가 성공적으로 수정되었습니다.'); + } else { + await createApplication(payload, clubId); + alert('지원서가 성공적으로 생성되었습니다.'); + } + } catch (error) { + alert('지원서 저장에 실패했습니다.'); + console.error(error); + } + }; + return ( <> handleFormTitleChange(e.target.value)} placeholder='지원서 제목을 입력하세요' > @@ -135,7 +172,9 @@ const CreateApplicationForm = () => { 질문 추가 + - 제출하기 + + 저장하기 + diff --git a/frontend/src/pages/AdminPage/application/answer/AnswerApplicationForm.tsx b/frontend/src/pages/AdminPage/application/answer/AnswerApplicationForm.tsx index b728f844a..4b0b3cf51 100644 --- a/frontend/src/pages/AdminPage/application/answer/AnswerApplicationForm.tsx +++ b/frontend/src/pages/AdminPage/application/answer/AnswerApplicationForm.tsx @@ -1,4 +1,3 @@ -import { mockData } from '@/mocks/data/mockData'; import { PageContainer } from '@/styles/PageContainer.styles'; import * as Styled from './AnswerApplicationForm.styles'; import Header from '@/components/common/Header/Header'; @@ -7,13 +6,23 @@ import { useGetClubDetail } from '@/hooks/queries/club/useGetClubDetail'; import ClubProfile from '@/pages/ClubDetailPage/components/ClubProfile/ClubProfile'; import { useAnswers } from '@/hooks/useAnswers'; import QuestionAnswerer from '@/pages/AdminPage/application/components/QuestionAnswerer/QuestionAnswerer'; +import { useGetApplication } from '@/hooks/queries/application/useGetApplication'; +import { Question } from '@/types/application'; const AnswerApplicationForm = () => { const { clubId } = useParams<{ clubId: string }>(); const { data: clubDetail, error } = useGetClubDetail(clubId || ''); + const { + data: formData, + isLoading, + isError, + } = useGetApplication(clubId || ''); + if (!clubId) return null; + if (!clubDetail) return null; const { onAnswerChange, getAnswersById } = useAnswers(); - if (!clubDetail) { - return null; + + if (!clubId || isLoading || !formData || !clubDetail) { + return
로딩 중...
; } if (error) { @@ -31,9 +40,9 @@ const AnswerApplicationForm = () => { category={clubDetail.category} tags={clubDetail.tags} /> - {mockData.form_title} + {formData.title} - {mockData.questions.map((q) => ( + {formData.questions.map((q: Question) => (