'use client'; import { useState, useEffect, useRef, useCallback } from 'react'; import Link from 'next/link'; import { createClient } from '@/lib/supabase/client'; import { trackEvent } from '@/lib/gtag'; // 외주 의뢰용 4단계 폼. // ① 프로젝트 유형 → ② 예산·일정 → ③ 상세 내용 → ④ 연락처 // 각 단계 검증을 통과해야 다음으로 진행한다. 마지막에 POST /api/contact. // 마운트 시 로그인 사용자면 이메일을 자동 채운다(수정 가능). // 기존 ContactForm.tsx는 보존하고, 이 폼이 /outsourcing #contact에서 대체한다. // 디자인: --jsm-* 토큰만 사용. gradient/blur/보라/이모지 금지. const KOR_TIGHT = { letterSpacing: '-0.02em' } as const; const KOR_BODY = { letterSpacing: '-0.01em' } as const; const INPUT_STYLE = { background: 'var(--jsm-surface)', border: '1px solid var(--jsm-line)', color: 'var(--jsm-ink)', } as const; const PROJECT_TYPES = [ '웹 서비스', '웹사이트', '업무 자동화', 'API·백엔드', '봇 개발', 'AI 연동', '기타', ] as const; const BUDGETS = [ '100만원 미만', '100~300만원', '300~1,000만원', '1,000만원 이상', '미정', ] as const; const TIMELINES = ['1개월 내', '1~3개월', '3개월 이상', '미정'] as const; const STEPS = [ { n: 1, label: '프로젝트 유형' }, { n: 2, label: '예산·일정' }, { n: 3, label: '상세 내용' }, { n: 4, label: '연락처' }, ] as const; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; interface SuccessInfo { trackUrl: string | null; } export default function OutsourcingRequestForm() { const [step, setStep] = useState(1); const [projectType, setProjectType] = useState(''); const [budget, setBudget] = useState(''); const [timeline, setTimeline] = useState(''); const [message, setMessage] = useState(''); const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [phone, setPhone] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(null); const headingRef = useRef(null); const setHeadingRef = useCallback((el: HTMLElement | null) => { headingRef.current = el; }, []); const firstRender = useRef(true); // 로그인 사용자 이메일 자동 채움 (BankTransferModal 세션 확인 패턴) useEffect(() => { let mounted = true; const supabase = createClient(); supabase.auth .getUser() .then(({ data }) => { const userEmail = data?.user?.email; if (mounted && userEmail) { setEmail((prev) => (prev ? prev : userEmail)); } }) .catch(() => { /* 비로그인 — 무시 */ }); return () => { mounted = false; }; }, []); // 단계 전환 시 헤딩으로 포커스 이동 (초기 마운트는 제외) useEffect(() => { if (firstRender.current) { firstRender.current = false; return; } headingRef.current?.focus(); }, [step, success]); const trimmedMessage = message.trim(); const trimmedName = name.trim(); const trimmedEmail = email.trim(); const stepValid = (s: number): boolean => { switch (s) { case 1: return projectType !== ''; case 2: return budget !== '' && timeline !== ''; case 3: return trimmedMessage.length >= 10; case 4: return trimmedName !== '' && EMAIL_RE.test(trimmedEmail); default: return false; } }; const goNext = useCallback(() => { if (!stepValid(step)) return; setError(''); setStep((s) => Math.min(s + 1, STEPS.length)); }, [step]); const goPrev = useCallback(() => { setError(''); setStep((s) => Math.max(s - 1, 1)); }, []); const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); if (!stepValid(4) || submitting) return; setSubmitting(true); setError(''); try { const res = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: trimmedName, phone: phone.trim(), email: trimmedEmail, service: `외주 개발 문의 — ${projectType}`, message: trimmedMessage, projectType, budget, timeline, }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { setError( data?.error || '의뢰 전송 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' ); setSubmitting(false); return; } trackEvent('generate_lead', { event_category: 'contact', event_label: `외주 개발 문의 — ${projectType}`, }); setSuccess({ trackUrl: typeof data?.trackUrl === 'string' ? data.trackUrl : null }); } catch { setError('네트워크 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'); setSubmitting(false); } }, // eslint-disable-next-line react-hooks/exhaustive-deps [ submitting, trimmedName, trimmedEmail, trimmedMessage, phone, projectType, budget, timeline, ] ); // ── 완료 화면 ────────────────────────────────────────────── if (success) { return (

의뢰가 접수되었습니다

영업일 2일 내 회신드립니다.

{success.trackUrl ? (
진행 상태 확인하기

추적 링크를 이메일로도 보내드렸습니다.

) : null}
); } const isLast = step === STEPS.length; const canAdvance = stepValid(step); return (
{/* 진행 표시기 */}
    {STEPS.map((s, i) => { const state = s.n < step ? 'done' : s.n === step ? 'current' : 'upcoming'; return (
  1. {s.n} {s.label} {i < STEPS.length - 1 && ( )}
  2. ); })}
{/* ── 단계 ① 프로젝트 유형 ── */} {step === 1 && (
어떤 프로젝트인가요?

가장 가까운 유형을 하나 선택해주세요.

{PROJECT_TYPES.map((t) => { const selected = projectType === t; return ( ); })}
)} {/* ── 단계 ② 예산·일정 ── */} {step === 2 && (

예산과 일정을 알려주세요

대략적인 범위면 충분합니다. 정해지지 않았다면 미정을 선택하세요.

예산
{BUDGETS.map((b) => ( setBudget(b)} /> ))}
희망 일정
{TIMELINES.map((t) => ( setTimeline(t)} /> ))}
)} {/* ── 단계 ③ 상세 내용 ── */} {step === 3 && (

자세히 들려주세요

구체적일수록 정확한 견적이 가능합니다. 최소 10자 이상 작성해주세요.