import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { bypassAccountApi, type BypassRequestSubmitRequest, } from '../api/bypassAccountApi'; import { useToastContext } from '../contexts/ToastContext'; interface FormState { applicantName: string; organization: string; purpose: string; email: string; requestedAccessPeriod: string; } interface ErrorState { applicantName?: string; email?: string; requestedAccessPeriod?: string; } interface TouchedState { applicantName: boolean; email: boolean; requestedAccessPeriod: boolean; } type PeriodPreset = '3개월' | '6개월' | '9개월' | '1년'; const INITIAL_FORM: FormState = { applicantName: '', organization: '', purpose: '', email: '', requestedAccessPeriod: '', }; const INITIAL_TOUCHED: TouchedState = { applicantName: false, email: false, requestedAccessPeriod: false, }; const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; function toDateString(date: Date): string { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${y}-${m}-${d}`; } function addMonths(date: Date, months: number): Date { const result = new Date(date); result.setMonth(result.getMonth() + months); return result; } function addYears(date: Date, years: number): Date { const result = new Date(date); result.setFullYear(result.getFullYear() + years); return result; } function calcPresetPeriod(preset: PeriodPreset): string { const today = new Date(); const from = toDateString(today); let to: string; switch (preset) { case '3개월': to = toDateString(addMonths(today, 3)); break; case '6개월': to = toDateString(addMonths(today, 6)); break; case '9개월': to = toDateString(addMonths(today, 9)); break; case '1년': to = toDateString(addYears(today, 1)); break; } return `${from} ~ ${to}`; } function validateForm(form: FormState): ErrorState { const errors: ErrorState = {}; if (!form.applicantName.trim()) { errors.applicantName = '신청자명을 입력해주세요.'; } if (!form.email.trim()) { errors.email = '이메일을 입력해주세요.'; } else if (!EMAIL_REGEX.test(form.email.trim())) { errors.email = '올바른 이메일 형식을 입력해주세요.'; } if (!form.requestedAccessPeriod.trim()) { errors.requestedAccessPeriod = '사용 기간을 선택해주세요.'; } return errors; } export default function BypassAccessRequest() { const { showToast } = useToastContext(); const navigate = useNavigate(); const [form, setForm] = useState(INITIAL_FORM); const [submitting, setSubmitting] = useState(false); const [submittedId, setSubmittedId] = useState(null); // Validation state const [errors, setErrors] = useState({}); const [touched, setTouched] = useState(INITIAL_TOUCHED); const [submitAttempted, setSubmitAttempted] = useState(false); // Period mode const [periodManual, setPeriodManual] = useState(false); const [selectedPreset, setSelectedPreset] = useState(null); const [periodFrom, setPeriodFrom] = useState(''); const [periodTo, setPeriodTo] = useState(''); const [periodRangeError, setPeriodRangeError] = useState(''); const handleChange = (field: keyof FormState, value: string) => { setForm((prev) => ({ ...prev, [field]: value })); }; const handleBlur = (field: keyof TouchedState) => { setTouched((prev) => ({ ...prev, [field]: true })); const currentErrors = validateForm(form); setErrors(currentErrors); }; const handlePresetClick = (preset: PeriodPreset) => { setSelectedPreset(preset); const period = calcPresetPeriod(preset); handleChange('requestedAccessPeriod', period); }; const handlePeriodFromChange = (value: string) => { setPeriodFrom(value); setPeriodRangeError(''); if (value && periodTo) { if (value >= periodTo) { setPeriodRangeError('시작일은 종료일보다 이전이어야 합니다.'); } else { handleChange('requestedAccessPeriod', `${value} ~ ${periodTo}`); } } }; const handlePeriodToChange = (value: string) => { setPeriodTo(value); setPeriodRangeError(''); if (periodFrom && value) { if (periodFrom >= value) { setPeriodRangeError('시작일은 종료일보다 이전이어야 합니다.'); } else { handleChange('requestedAccessPeriod', `${periodFrom} ~ ${value}`); } } }; const handleToggleManual = () => { setPeriodManual((prev) => { const next = !prev; if (!next) { // Switching back to preset: clear manual inputs setPeriodFrom(''); setPeriodTo(''); setPeriodRangeError(''); setSelectedPreset(null); handleChange('requestedAccessPeriod', ''); } else { // Switching to manual: clear preset selection setSelectedPreset(null); handleChange('requestedAccessPeriod', ''); } return next; }); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setSubmitAttempted(true); setTouched({ applicantName: true, email: true, requestedAccessPeriod: true }); const currentErrors = validateForm(form); if (periodManual && periodRangeError) { return; } if (Object.keys(currentErrors).length > 0) { setErrors(currentErrors); // 첫 번째 에러 필드로 스크롤 const firstErrorField = Object.keys(currentErrors)[0]; const el = document.getElementById(`field-${firstErrorField}`); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }); return; } setSubmitting(true); try { const requestData: BypassRequestSubmitRequest = { applicantName: form.applicantName, organization: form.organization, purpose: form.purpose, email: form.email, phone: '', requestedAccessPeriod: form.requestedAccessPeriod, }; const res = await bypassAccountApi.submitRequest(requestData); setSubmittedId(res.data.id); showToast('신청이 완료되었습니다.', 'success'); } catch (err) { showToast('신청 제출 실패. 다시 시도해주세요.', 'error'); console.error(err); } finally { setSubmitting(false); } }; const showError = (field: keyof TouchedState): boolean => (touched[field] || submitAttempted) && Boolean(errors[field]); const inputClass = (field: keyof TouchedState) => `w-full px-3 py-2 text-sm rounded-lg border ${ showError(field) ? 'border-red-400' : 'border-wing-border' } bg-wing-card text-wing-text placeholder:text-wing-muted focus:outline-none focus:ring-2 ${ showError(field) ? 'focus:ring-red-400/50' : 'focus:ring-wing-accent/50' }`; const PRESETS: PeriodPreset[] = ['3개월', '6개월', '9개월', '1년']; return (
{/* 헤더 */}

S&P API 계정 신청

S&P API 계정을 신청합니다. 검토 후 계정 정보가 이메일로 발송됩니다.

{submittedId !== null ? ( /* 제출 완료 화면 */

신청이 완료되었습니다

신청 번호: #{submittedId}

검토 후 입력하신 이메일로 안내 드리겠습니다.

) : ( /* 신청 폼 */
{/* 신청자명 + 기관 */}
handleChange('applicantName', e.target.value)} onBlur={() => handleBlur('applicantName')} placeholder="홍길동" disabled={submitting} className={inputClass('applicantName')} /> {showError('applicantName') && (

{errors.applicantName}

)}
handleChange('organization', e.target.value)} placeholder="소속 기관명" disabled={submitting} className="w-full px-3 py-2 text-sm rounded-lg border border-wing-border bg-wing-card text-wing-text placeholder:text-wing-muted focus:outline-none focus:ring-2 focus:ring-wing-accent/50" />
{/* 이메일 + 사용 기간 */}
handleChange('email', e.target.value)} onBlur={() => handleBlur('email')} placeholder="example@domain.com" disabled={submitting} className={inputClass('email')} /> {showError('email') && (

{errors.email}

)}
{!periodManual ? (
{PRESETS.map((preset) => ( ))}
) : (
handlePeriodFromChange(e.target.value)} disabled={submitting} className="w-full px-3 py-2 text-sm rounded-lg border border-wing-border bg-wing-card text-wing-text focus:outline-none focus:ring-2 focus:ring-wing-accent/50" />
~
handlePeriodToChange(e.target.value)} disabled={submitting} className="w-full px-3 py-2 text-sm rounded-lg border border-wing-border bg-wing-card text-wing-text focus:outline-none focus:ring-2 focus:ring-wing-accent/50" />
)} {form.requestedAccessPeriod && !periodRangeError && (

선택된 기간: {form.requestedAccessPeriod}

)} {periodRangeError && (

{periodRangeError}

)} {showError('requestedAccessPeriod') && !periodRangeError && (

{errors.requestedAccessPeriod}

)}
{/* 사용 목적 */}