diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index be74dc2d9..16933b390 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -15,6 +15,8 @@ import AccountEditTab from '@/pages/AdminPage/tabs/AccountEditTab/AccountEditTab import LoginTab from '@/pages/AdminPage/auth/LoginTab/LoginTab'; import PrivateRoute from '@/pages/AdminPage/auth/PrivateRoute/PrivateRoute'; import PhotoEditTab from '@/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab'; +import AnswerApplicationForm from './pages/AdminPage/application/answer/AnswerApplicationForm'; +import CreateApplicationForm from './pages/AdminPage/application/CreateApplicationForm'; // TODO: 지원서 개발 완료 후 활성화 // import AnswerApplicationForm from '@/pages/AdminPage/application/answer/AnswerApplicationForm'; // import CreateApplicationForm from '@/pages/AdminPage/application/CreateApplicationForm'; @@ -73,10 +75,10 @@ const App = () => { /> {/*🔒 메인 브랜치에서는 접근 차단 (배포용 차단 목적)*/} {/*develop-fe 브랜치에서는 접근 가능하도록 풀고 개발 예정*/} - {/*}*/} - {/*/>*/} + } + /> @@ -85,10 +87,10 @@ const App = () => { /> {/*🔒 사용자용 지원서 작성 페이지도 메인에서는 비활성화 처리 */} {/*🛠 develop-fe에서는 다시 노출 예정*/} - {/*}*/} - {/*/>*/} + } + /> } /> diff --git a/frontend/src/apis/application/applyToClub.ts b/frontend/src/apis/application/applyToClub.ts new file mode 100644 index 000000000..3d50d6ed0 --- /dev/null +++ b/frontend/src/apis/application/applyToClub.ts @@ -0,0 +1,36 @@ +import API_BASE_URL from '@/constants/api'; +import { AnswerItem } from '@/types/application'; + +export const applyToClub = async ( + clubId: string, + answers: AnswerItem[], +) => { + try { + const response = await fetch( + `${API_BASE_URL}/api/club/${clubId}/apply`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + questions: [ + ...answers + ] + }), + }, + ); + + if (!response.ok) { + throw new Error('답변 제출에 실패했습니다.'); + } + + const result = await response.json(); + return result.data; + } catch (error) { + console.error('답변 제출 중 오류 발생:', error); + throw error; + } +}; + +export default applyToClub; \ No newline at end of file diff --git a/frontend/src/apis/application/getApplication.ts b/frontend/src/apis/application/getApplication.ts index a9ec0e0bc..366d3339b 100644 --- a/frontend/src/apis/application/getApplication.ts +++ b/frontend/src/apis/application/getApplication.ts @@ -4,7 +4,8 @@ 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}`); + console.error(`Failed to fetch: ${response.statusText}`) + throw new Error((await response.json()).message); } const result = await response.json(); diff --git a/frontend/src/constants/INITIAL_FORM_DATA.ts b/frontend/src/constants/INITIAL_FORM_DATA.ts index e97cf76f1..2e7e82aaa 100644 --- a/frontend/src/constants/INITIAL_FORM_DATA.ts +++ b/frontend/src/constants/INITIAL_FORM_DATA.ts @@ -3,8 +3,17 @@ import { ApplicationFormData } from '@/types/application'; const INITIAL_FORM_DATA: ApplicationFormData = { title: '', questions: [ + //맨 처음은 이름 { id: 1, + title: '이름을 입력해주세요', + description: '지원자의 이름을 입력해주세요. (예: 홍길동)', + type: 'SHORT_TEXT', + options: { required: true }, + items: [], + }, + { + id: 2, title: '', description: '', type: 'SHORT_TEXT', @@ -12,7 +21,7 @@ const INITIAL_FORM_DATA: ApplicationFormData = { items: [], }, { - id: 2, + id: 3, title: '', description: '', type: 'CHOICE', diff --git a/frontend/src/hooks/useAnswers.ts b/frontend/src/hooks/useAnswers.ts index 6cf5dfead..51c096c4b 100644 --- a/frontend/src/hooks/useAnswers.ts +++ b/frontend/src/hooks/useAnswers.ts @@ -7,14 +7,14 @@ export const useAnswers = () => { const updateSingleAnswer = (id: number, value: string) => { setAnswers((prev) => [ ...prev.filter((a) => a.id !== id), - { id, answer: value }, + { id, value: value }, ]); }; const updateMultiAnswer = (id: number, values: string[]) => { setAnswers((prev) => [ ...prev.filter((a) => a.id !== id), - ...values.map((v) => ({ id, answer: v })), + ...values.map((v) => ({ id, value: v })), ]); }; @@ -27,7 +27,7 @@ export const useAnswers = () => { }; const getAnswersById = (id: number) => - answers.filter((a) => a.id === id).map((a) => a.answer); + answers.filter((a) => a.id === id).map((a) => a.value); - return { onAnswerChange, getAnswersById }; + return { onAnswerChange, getAnswersById, answers }; }; diff --git a/frontend/src/pages/AdminPage/application/CreateApplicationForm.tsx b/frontend/src/pages/AdminPage/application/CreateApplicationForm.tsx index bc3be5bcc..2ae388853 100644 --- a/frontend/src/pages/AdminPage/application/CreateApplicationForm.tsx +++ b/frontend/src/pages/AdminPage/application/CreateApplicationForm.tsx @@ -158,6 +158,7 @@ const CreateApplicationForm = () => { options={question.options} items={question.items} type={question.type} + readOnly={index === 0} //인덱스 0번은 이름을 위한 고정 부분이므로 수정 불가 onTitleChange={handleTitleChange(question.id)} onDescriptionChange={handleDescriptionChange(question.id)} onItemsChange={handleItemsChange(question.id)} diff --git a/frontend/src/pages/AdminPage/application/answer/AnswerApplicationForm.tsx b/frontend/src/pages/AdminPage/application/answer/AnswerApplicationForm.tsx index dd9fe6f9d..80336e013 100644 --- a/frontend/src/pages/AdminPage/application/answer/AnswerApplicationForm.tsx +++ b/frontend/src/pages/AdminPage/application/answer/AnswerApplicationForm.tsx @@ -1,7 +1,7 @@ import { PageContainer } from '@/styles/PageContainer.styles'; import * as Styled from './AnswerApplicationForm.styles'; import Header from '@/components/common/Header/Header'; -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useGetClubDetail } from '@/hooks/queries/club/useGetClubDetail'; import ClubProfile from '@/pages/ClubDetailPage/components/ClubProfile/ClubProfile'; import { useAnswers } from '@/hooks/useAnswers'; @@ -9,19 +9,27 @@ import QuestionAnswerer from '@/pages/AdminPage/application/components/QuestionA import { useGetApplication } from '@/hooks/queries/application/useGetApplication'; import { Question } from '@/types/application'; import Spinner from '@/components/common/Spinner/Spinner'; +import applyToClub from '@/apis/application/applyToClub'; const AnswerApplicationForm = () => { const { clubId } = useParams<{ clubId: string }>(); + const navigate = useNavigate(); if (!clubId) return null; const { data: clubDetail, error } = useGetClubDetail(clubId); - const { data: formData, isLoading, isError } = useGetApplication(clubId); + const { data: formData, isLoading, isError, error: applicationError } = useGetApplication(clubId); - const { onAnswerChange, getAnswersById } = useAnswers(); + const { onAnswerChange, getAnswersById, answers } = useAnswers(); if (isLoading) return ; + + if (isError) { + alert(applicationError.message) + navigate(`/club/${clubId}`) + return
문제가 발생했어요. 잠시 후 다시 시도해 주세요.
; + } - if (error || isError) { + if (error) { return
문제가 발생했어요. 잠시 후 다시 시도해 주세요.
; } @@ -34,6 +42,16 @@ const AnswerApplicationForm = () => { ); } + const handleSubmit = async () => { + try { + await applyToClub(clubId, answers); + alert('답변이 성공적으로 제출되었습니다.'); + // TODO: 필요시 페이지 이동 등 추가 + } catch (e) { + alert('답변 제출에 실패했습니다. 잠시 후 다시 시도해 주세요.'); + } + }; + return ( <>
@@ -57,7 +75,7 @@ const AnswerApplicationForm = () => { ))} - 제출하기 + 제출하기 diff --git a/frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts b/frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts index 8da426be7..05d359afe 100644 --- a/frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts +++ b/frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts @@ -75,7 +75,9 @@ export const SelectionToggleButton = styled.button<{ active: boolean }>` color 0.2s ease; `; -export const QuestionWrapper = styled.div` +export const QuestionWrapper = styled.div<{readOnly?: boolean}>` display: flex; gap: 36px; + pointer-events: ${({ readOnly }) => (readOnly ? 'none' : 'auto')}; + cursor: ${({ readOnly }) => (readOnly ? 'not-allowed' : 'auto')}; `; diff --git a/frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.tsx b/frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.tsx index c84c57d03..e52d10117 100644 --- a/frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.tsx +++ b/frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.tsx @@ -16,6 +16,7 @@ const QuestionBuilder = ({ options, items, type, + readOnly, onTitleChange, onItemsChange, onDescriptionChange, @@ -117,7 +118,7 @@ const QuestionBuilder = ({ }; return ( - + onRequiredChange?.(!options?.required)} @@ -133,7 +134,9 @@ const QuestionBuilder = ({ }} /> {renderSelectionToggle()} - + {!readOnly && ( + + )} {renderFieldByQuestionType()} diff --git a/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx b/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx index d6b3b305d..f160cd25a 100644 --- a/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx +++ b/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx @@ -31,9 +31,6 @@ const SideBar = ({ clubLogo, clubName }: SideBarProps) => { if (tab.label === '계정 관리') { alert('계정 관리 기능은 아직 준비 중이에요. ☺️'); return; - } else if (tab.label === '지원 관리') { - alert('동아리 지원 관리 기능은 곧 오픈돼요!\n조금만 기다려주세요 🚀'); - return; } navigate(tab.path); }; diff --git a/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx b/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx index bb0d43a94..415c4366e 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx @@ -4,8 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { useGetClubDetail } from '@/hooks/queries/club/useGetClubDetail'; interface ButtonProps { - recruitmentForm?: string; - presidentPhoneNumber?: string; + isRecruiting: boolean; } const Button = styled.button` @@ -38,8 +37,7 @@ const Button = styled.button` `; const ClubApplyButton = ({ - recruitmentForm, - presidentPhoneNumber, + isRecruiting }: ButtonProps) => { const { clubId } = useParams<{ clubId: string }>(); const trackEvent = useMixpanelTrack(); @@ -49,14 +47,11 @@ const ClubApplyButton = ({ trackEvent('Club Apply Button Clicked'); //TODO: 지원서를 작성한 동아리의 경우에만 리다이렉트 - //navigate(`/application/${clubId}`); - - // [x] FIXME: recruitmentForm 있을 때는 리다이렉트 - if (presidentPhoneNumber) { - alert(`${presidentPhoneNumber} 으로 연락하여 지원해 주세요.`); - } else { - alert('모집이 마감되었습니다. 다음에 지원해 주세요.'); + if (!isRecruiting) { + alert('지원모집이 마감되었습니다. 다음에 지원해 주세요.') + return; } + navigate(`/application/${clubId}`); }; return ; diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx b/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx index 3aca35fae..307af9b5c 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx @@ -28,10 +28,7 @@ const ClubDetailFooter = ({ ); diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx b/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx index 028c5744f..e56432559 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx @@ -43,10 +43,7 @@ const ClubDetailHeader = ({ logo={logo} /> ); diff --git a/frontend/src/types/application.ts b/frontend/src/types/application.ts index 7a8375e19..e880eab61 100644 --- a/frontend/src/types/application.ts +++ b/frontend/src/types/application.ts @@ -14,6 +14,7 @@ export interface Question { } export interface QuestionBuilderProps extends Question { + readOnly: boolean; onTitleChange: (value: string) => void; onDescriptionChange: (value: string) => void; onItemsChange?: (newItems: { value: string }[]) => void; @@ -52,5 +53,5 @@ export interface ApplicationFormData { export interface AnswerItem { id: number; - answer: string; + value: string; }