From 754af7526796b6eef5906d79df36253796415eca Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sat, 21 Feb 2026 15:54:38 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=A7=80=EC=9B=90=EC=84=9C=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=8B=9C=20=EC=9E=85=EB=A0=A5=EA=B0=92=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - validateApplicationForm 함수로 제목/설명 필수 및 최대 길이, 질문 제목, 외부 URL 검증 - 검증 실패 시 항목별 에러 메시지를 alert으로 표시 - FormTitle에 maxLength={50} 추가 - 기존 인라인 URL 검증 로직을 validateApplicationForm으로 통합 --- .../ApplicationEditTab/ApplicationEditTab.tsx | 43 +++++------ .../validation/validateApplicationForm.ts | 71 +++++++++++++++++++ 2 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 frontend/src/pages/AdminPage/validation/validateApplicationForm.ts diff --git a/frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx index 90f1d3008..12a73b6e1 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { createApplication, updateApplication } from '@/apis/application'; @@ -10,6 +10,10 @@ import { queryKeys } from '@/constants/queryKeys'; import { useAdminClubContext } from '@/context/AdminClubContext'; import { useGetApplication } from '@/hooks/Queries/useApplication'; import QuestionBuilder from '@/pages/AdminPage/components/QuestionBuilder/QuestionBuilder'; +import { + hasErrors, + validateApplicationForm, +} from '@/pages/AdminPage/validation/validateApplicationForm'; import { PageContainer } from '@/styles/PageContainer.styles'; import { ApplicationFormData, @@ -20,14 +24,6 @@ import { import * as Styled from './ApplicationEditTab.styles'; import { QuestionDivider } from './ApplicationEditTab.styles'; -const externalApplicationUrlAllowed = [ - 'https://forms.gle', - 'https://docs.google.com/forms', - 'https://form.naver.com', - 'https://naver.me', - 'https://everytime.kr', -]; - const ApplicationEditTab = () => { const queryClient = useQueryClient(); const navigate = useNavigate(); @@ -120,6 +116,23 @@ const ApplicationEditTab = () => { const handleSubmit = async () => { if (!clubId) return; + + const validationErrors = validateApplicationForm( + formData, + applicationFormMode, + externalApplicationUrl, + ); + if (hasErrors(validationErrors)) { + const messages: string[] = [ + ...(validationErrors.title ? [validationErrors.title] : []), + ...(validationErrors.description ? [validationErrors.description] : []), + ...Object.values(validationErrors.questions ?? {}), + ...(validationErrors.externalUrl ? [validationErrors.externalUrl] : []), + ]; + alert(messages.join('\n')); + return; + } + const reorderedQuestions = formData.questions?.map((q, idx) => ({ ...q, id: idx + 1, @@ -137,17 +150,6 @@ const ApplicationEditTab = () => { if (applicationFormMode === ApplicationFormMode.INTERNAL) { payload.questions = reorderedQuestions; } else if (applicationFormMode === ApplicationFormMode.EXTERNAL) { - const isValidUrl = externalApplicationUrlAllowed.some((url) => - externalApplicationUrl.startsWith(url), - ); - - if (!isValidUrl) { - alert( - '외부 지원서 링크는 Google Forms, Naver Form 또는 Everytime 링크여야 합니다.', - ); - return; - } - payload.externalApplicationUrl = externalApplicationUrl; } @@ -186,6 +188,7 @@ const ApplicationEditTab = () => { handleFormTitleChange(e.target.value)} placeholder='지원서 제목을 입력하세요' diff --git a/frontend/src/pages/AdminPage/validation/validateApplicationForm.ts b/frontend/src/pages/AdminPage/validation/validateApplicationForm.ts new file mode 100644 index 000000000..ad6274f65 --- /dev/null +++ b/frontend/src/pages/AdminPage/validation/validateApplicationForm.ts @@ -0,0 +1,71 @@ +import { ApplicationFormData, ApplicationFormMode } from '@/types/application'; + +const ALLOWED_EXTERNAL_URLS = [ + 'https://forms.gle', + 'https://docs.google.com/forms', + 'https://form.naver.com', + 'https://naver.me', + 'https://everytime.kr', +]; + +export interface ApplicationFormErrors { + title?: string; + description?: string; + questions?: Record; + externalUrl?: string; +} + +export const validateApplicationForm = ( + formData: ApplicationFormData, + mode: ApplicationFormMode, + externalUrl: string, +): ApplicationFormErrors => { + const errors: ApplicationFormErrors = {}; + + if (!formData.title?.trim()) { + errors.title = '지원서 제목을 입력해주세요.'; + } else if (formData.title.length > 50) { + errors.title = '지원서 제목은 최대 50자까지 입력할 수 있습니다.'; + } + + if (!formData.description?.trim()) { + errors.description = '지원서 설명을 입력해주세요.'; + } else if (formData.description.length > 3000) { + errors.description = '지원서 설명은 최대 3000자까지 입력할 수 있습니다.'; + } + + if (mode === ApplicationFormMode.INTERNAL) { + const questionErrors: Record = {}; + + formData.questions?.forEach((q) => { + if (!q.title.trim()) { + questionErrors[q.id] = '질문 제목을 입력해주세요.'; + } else if ( + (q.type === 'CHOICE' || q.type === 'MULTI_CHOICE') && + q.items.some((item) => !item.value.trim()) + ) { + questionErrors[q.id] = '선택지에 빈 항목이 있습니다.'; + } + }); + + if (Object.keys(questionErrors).length > 0) { + errors.questions = questionErrors; + } + } + + if (mode === ApplicationFormMode.EXTERNAL) { + if (!externalUrl.trim()) { + errors.externalUrl = '외부 지원서 링크를 입력해주세요.'; + } else if ( + !ALLOWED_EXTERNAL_URLS.some((url) => externalUrl.startsWith(url)) + ) { + errors.externalUrl = + 'Google Forms, Naver Form 또는 Everytime 링크여야 합니다.'; + } + } + + return errors; +}; + +export const hasErrors = (errors: ApplicationFormErrors): boolean => + Object.keys(errors).length > 0; From 84111f637d40d145ca85ef988ed7ed48915f8503 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sat, 21 Feb 2026 16:02:07 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EC=99=B8=EB=B6=80=EC=A7=80?= =?UTF-8?q?=EC=9B=90=EC=84=9C=EB=A7=81=ED=81=AC=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=EC=97=90=20=EC=8A=AC=EB=9E=98=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/AdminPage/validation/validateApplicationForm.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/AdminPage/validation/validateApplicationForm.ts b/frontend/src/pages/AdminPage/validation/validateApplicationForm.ts index ad6274f65..34a725a80 100644 --- a/frontend/src/pages/AdminPage/validation/validateApplicationForm.ts +++ b/frontend/src/pages/AdminPage/validation/validateApplicationForm.ts @@ -1,11 +1,11 @@ import { ApplicationFormData, ApplicationFormMode } from '@/types/application'; const ALLOWED_EXTERNAL_URLS = [ - 'https://forms.gle', + 'https://forms.gle/', 'https://docs.google.com/forms', - 'https://form.naver.com', - 'https://naver.me', - 'https://everytime.kr', + 'https://form.naver.com/', + 'https://naver.me/', + 'https://everytime.kr/', ]; export interface ApplicationFormErrors {