Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -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;
}

Expand Down Expand Up @@ -186,6 +188,7 @@ const ApplicationEditTab = () => {
</Styled.HeaderContainer>
<Styled.FormTitle
type='text'
maxLength={50}
value={formData.title}
onChange={(e) => handleFormTitleChange(e.target.value)}
placeholder='지원서 제목을 입력하세요'
Expand Down
71 changes: 71 additions & 0 deletions frontend/src/pages/AdminPage/validation/validateApplicationForm.ts
Original file line number Diff line number Diff line change
@@ -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<number, string>;
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<number, string> = {};

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;
Loading