'use client' import React, { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Progress } from '@/components/ui/progress'; import { Check, ChevronRight, User, Building, FileText, Plus, X, ChevronsUpDown, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useRouter, useParams, useSearchParams } from 'next/navigation'; import { useTranslation } from '@/i18n/client'; import { toast } from '@/hooks/use-toast'; import { Popover, PopoverTrigger, PopoverContent, } from '@/components/ui/popover'; import { Command, CommandList, CommandInput, CommandEmpty, CommandGroup, CommandItem, } from '@/components/ui/command'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Dropzone, DropzoneZone, DropzoneInput, DropzoneUploadIcon, DropzoneTitle, DropzoneDescription, } from '@/components/ui/dropzone'; import { FileList, FileListItem, FileListHeader, FileListIcon, FileListInfo, FileListName, FileListDescription, FileListAction, } from '@/components/ui/file-list'; import { ScrollArea } from '@/components/ui/scroll-area'; import prettyBytes from 'pretty-bytes'; // 기존 JoinForm에서 가져온 데이터들 import i18nIsoCountries from "i18n-iso-countries"; import enLocale from "i18n-iso-countries/langs/en.json"; import koLocale from "i18n-iso-countries/langs/ko.json"; import { getVendorTypes } from '@/lib/vendors/service'; import ConsentStep from './conset-step'; i18nIsoCountries.registerLocale(enLocale); i18nIsoCountries.registerLocale(koLocale); const locale = "ko"; const countryMap = i18nIsoCountries.getNames(locale, { select: "official" }); const countryArray = Object.entries(countryMap).map(([code, label]) => ({ code, label, })); const sortedCountryArray = [...countryArray].sort((a, b) => { if (a.code === "KR") return -1; if (b.code === "KR") return 1; return a.label.localeCompare(b.label); }); const enhancedCountryArray = sortedCountryArray.map(country => ({ ...country, label: locale === "ko" && country.code === "KR" ? "대한민국 (South Korea)" : country.label })); const contactTaskOptions = [ { value: "PRESIDENT_DIRECTOR", label: "회사대표 President/Director" }, { value: "SALES_MANAGEMENT", label: "영업관리 Sales Management" }, { value: "ENGINEERING_DESIGN", label: "설계/기술 Engineering/Design" }, { value: "PROCUREMENT", label: "구매 Procurement" }, { value: "DELIVERY_CONTROL", label: "납기/출하/운송 Delivery Control" }, { value: "PM_MANUFACTURING", label: "PM/생산관리 PM/Manufacturing" }, { value: "QUALITY_MANAGEMENT", label: "품질관리 Quality Management" }, { value: "SHIPPING_DOC_MANAGEMENT", label: "세금계산서/납품서관리 Shipping Doc. Management" }, { value: "AS_MANAGEMENT", label: "A/S 관리 A/S Management" }, { value: "FIELD_SERVICE_ENGINEER", label: "FSE(야드작업자) Field Service Engineer" } ]; export const countryDialCodes = { AF: "+93", AL: "+355", DZ: "+213", AS: "+1-684", AD: "+376", AO: "+244", AI: "+1-264", AG: "+1-268", AR: "+54", AM: "+374", AW: "+297", AU: "+61", AT: "+43", AZ: "+994", BS: "+1-242", BH: "+973", BD: "+880", BB: "+1-246", BY: "+375", BE: "+32", BZ: "+501", BJ: "+229", BM: "+1-441", BT: "+975", BO: "+591", BA: "+387", BW: "+267", BR: "+55", BN: "+673", BG: "+359", BF: "+226", BI: "+257", KH: "+855", CM: "+237", CA: "+1", CV: "+238", KY: "+1-345", CF: "+236", TD: "+235", CL: "+56", CN: "+86", CO: "+57", KM: "+269", CG: "+242", CD: "+243", CR: "+506", CI: "+225", HR: "+385", CU: "+53", CY: "+357", CZ: "+420", DK: "+45", DJ: "+253", DM: "+1-767", DO: "+1-809", EC: "+593", EG: "+20", SV: "+503", GQ: "+240", ER: "+291", EE: "+372", ET: "+251", FJ: "+679", FI: "+358", FR: "+33", GA: "+241", GM: "+220", GE: "+995", DE: "+49", GH: "+233", GR: "+30", GD: "+1-473", GT: "+502", GN: "+224", GW: "+245", GY: "+592", HT: "+509", HN: "+504", HK: "+852", HU: "+36", IS: "+354", IN: "+91", ID: "+62", IR: "+98", IQ: "+964", IE: "+353", IL: "+972", IT: "+39", JM: "+1-876", JP: "+81", JO: "+962", KZ: "+7", KE: "+254", KI: "+686", KR: "+82", KW: "+965", KG: "+996", LA: "+856", LV: "+371", LB: "+961", LS: "+266", LR: "+231", LY: "+218", LI: "+423", LT: "+370", LU: "+352", MK: "+389", MG: "+261", MW: "+265", MY: "+60", MV: "+960", ML: "+223", MT: "+356", MH: "+692", MR: "+222", MU: "+230", MX: "+52", FM: "+691", MD: "+373", MC: "+377", MN: "+976", ME: "+382", MA: "+212", MZ: "+258", MM: "+95", NA: "+264", NR: "+674", NP: "+977", NL: "+31", NZ: "+64", NI: "+505", NE: "+227", NG: "+234", NU: "+683", KP: "+850", NO: "+47", OM: "+968", PK: "+92", PW: "+680", PS: "+970", PA: "+507", PG: "+675", PY: "+595", PE: "+51", PH: "+63", PL: "+48", PT: "+351", PR: "+1-787", QA: "+974", RO: "+40", RU: "+7", RW: "+250", KN: "+1-869", LC: "+1-758", VC: "+1-784", WS: "+685", SM: "+378", ST: "+239", SA: "+966", SN: "+221", RS: "+381", SC: "+248", SL: "+232", SG: "+65", SK: "+421", SI: "+386", SB: "+677", SO: "+252", ZA: "+27", SS: "+211", ES: "+34", LK: "+94", SD: "+249", SR: "+597", SZ: "+268", SE: "+46", CH: "+41", SY: "+963", TW: "+886", TJ: "+992", TZ: "+255", TH: "+66", TL: "+670", TG: "+228", TK: "+690", TO: "+676", TT: "+1-868", TN: "+216", TR: "+90", TM: "+993", TV: "+688", UG: "+256", UA: "+380", AE: "+971", GB: "+44", US: "+1", UY: "+598", UZ: "+998", VU: "+678", VA: "+39-06", VE: "+58", VN: "+84", YE: "+967", ZM: "+260", ZW: "+263" }; const MAX_FILE_SIZE = 3e9; // 스텝 정의 const STEPS = [ { id: 1, title: '약관 동의', description: '서비스 이용 약관 동의', icon: FileText }, { id: 2, title: '계정 생성', description: '개인 계정 정보 입력', icon: User }, { id: 3, title: '업체 등록', description: '업체 정보 및 서류 제출', icon: Building } ]; export default function JoinForm() { const params = useParams() || {}; const lng = params.lng ? String(params.lng) : "ko"; const { t } = useTranslation(lng, "translation"); const router = useRouter(); const searchParams = useSearchParams() || new URLSearchParams(); const defaultTaxId = searchParams.get("taxID") ?? ""; const [currentStep, setCurrentStep] = useState(1); const [completedSteps, setCompletedSteps] = useState(new Set()); // 각 스텝별 데이터 const [consentData, setConsentData] = useState({ privacy: false, terms: false, marketing: false }); const [accountData, setAccountData] = useState({ name: '', email: '', password: '', confirmPassword: '', phone: '' }); const [vendorData, setVendorData] = useState({ vendorName: "", vendorTypeId: undefined, items: "", taxId: defaultTaxId, address: "", email: "", phone: "", country: "", website: "", representativeName: "", representativeBirth: "", representativeEmail: "", representativePhone: "", corporateRegistrationNumber: "", representativeWorkExpirence: false, contacts: [ { contactName: "", contactPosition: "", contactDepartment: "", contactTask: "", contactEmail: "", contactPhone: "", }, ], }); // 업체 타입 및 파일 상태 const [vendorTypes, setVendorTypes] = useState([]); const [isLoadingVendorTypes, setIsLoadingVendorTypes] = useState(true); const [businessRegistrationFiles, setBusinessRegistrationFiles] = useState([]); const [isoCertificationFiles, setIsoCertificationFiles] = useState([]); const [creditReportFiles, setCreditReportFiles] = useState([]); const [bankAccountFiles, setBankAccountFiles] = useState([]); const [policyVersions, setPolicyVersions] = useState({ privacy_policy: '1.0', terms_of_service: '1.0' }); const progress = ((currentStep - 1) / (STEPS.length - 1)) * 100; // 정책 버전 및 업체 타입 로드 useEffect(() => { fetchPolicyVersions(); loadVendorTypes(); }, []); const fetchPolicyVersions = async () => { try { const response = await fetch('/api/consent/policy-versions'); const versions = await response.json(); setPolicyVersions(versions); } catch (error) { console.error('Failed to fetch policy versions:', error); } }; const loadVendorTypes = async () => { setIsLoadingVendorTypes(true); try { const result = await getVendorTypes(); if (result.data) { setVendorTypes(result.data); } } catch (error) { console.error("Failed to load vendor types:", error); toast({ variant: "destructive", title: "Error", description: "Failed to load vendor types", }); } finally { setIsLoadingVendorTypes(false); } }; const handleStepComplete = (step) => { setCompletedSteps(prev => new Set([...prev, step])); if (step < STEPS.length) { setCurrentStep(step + 1); } }; const handleStepClick = (stepId) => { if (stepId <= Math.max(...completedSteps) + 1) { setCurrentStep(stepId); } }; // 전화번호 플레이스홀더 함수들 const getPhonePlaceholder = (countryCode) => { if (!countryCode || !countryDialCodes[countryCode]) return "+82 010-1234-5678"; const dialCode = countryDialCodes[countryCode]; switch (countryCode) { case 'KR': return `${dialCode} 010-1234-5678`; case 'US': case 'CA': return `${dialCode} 555-123-4567`; case 'JP': return `${dialCode} 90-1234-5678`; case 'CN': return `${dialCode} 138-0013-8000`; case 'GB': return `${dialCode} 20-7946-0958`; case 'DE': return `${dialCode} 30-12345678`; case 'FR': return `${dialCode} 1-42-86-83-16`; default: return `${dialCode} 전화번호`; } }; const getPhoneDescription = (countryCode) => { if (!countryCode) return "국가를 먼저 선택해주세요."; const dialCode = countryDialCodes[countryCode]; switch (countryCode) { case 'KR': return `${dialCode}로 시작하는 국제번호 또는 010으로 시작하는 국내번호를 입력하세요.`; case 'US': case 'CA': return `${dialCode}로 시작하는 10자리 번호를 입력하세요.`; case 'JP': return `${dialCode}로 시작하는 일본 전화번호를 입력하세요.`; case 'CN': return `${dialCode}로 시작하는 중국 전화번호를 입력하세요.`; default: return `${dialCode}로 시작하는 국제 전화번호를 입력하세요.`; } }; return (
{/* 진행률 표시 */}

파트너 등록

{currentStep} / {STEPS.length}
{/* 스텝 네비게이션 */}
{STEPS.map((step, index) => { const Icon = step.icon; const isCompleted = completedSteps.has(step.id); const isCurrent = currentStep === step.id; const isAccessible = step.id <= Math.max(...completedSteps) + 1; return (
handleStepClick(step.id)} >
{isCompleted ? ( ) : ( )}
{step.title}
{step.description}
{index < STEPS.length - 1 && ( )}
); })}
{/* 스텝 콘텐츠 */}
{currentStep === 1 && ( handleStepComplete(1)} policyVersions={policyVersions} /> )} {currentStep === 2 && ( handleStepComplete(2)} onBack={() => setCurrentStep(1)} /> )} {currentStep === 3 && ( setCurrentStep(2)} onComplete={() => { handleStepComplete(3); // 완료 후 대시보드로 이동 router.push(`/${lng}/partners/dashboard`); }} accountData={accountData} consentData={consentData} vendorTypes={vendorTypes} isLoadingVendorTypes={isLoadingVendorTypes} businessRegistrationFiles={businessRegistrationFiles} setBusinessRegistrationFiles={setBusinessRegistrationFiles} isoCertificationFiles={isoCertificationFiles} setIsoCertificationFiles={setIsoCertificationFiles} creditReportFiles={creditReportFiles} setCreditReportFiles={setCreditReportFiles} bankAccountFiles={bankAccountFiles} setBankAccountFiles={setBankAccountFiles} getPhonePlaceholder={getPhonePlaceholder} getPhoneDescription={getPhoneDescription} enhancedCountryArray={enhancedCountryArray} contactTaskOptions={contactTaskOptions} lng={lng} policyVersions={policyVersions} /> )}
); } // Step 2: 계정 생성 function AccountStep({ data, onChange, onNext, onBack }) { const [isLoading, setIsLoading] = useState(false); const isValid = data.name && data.email && data.password && data.confirmPassword && data.phone && data.password === data.confirmPassword; const handleInputChange = (field, value) => { onChange(prev => ({ ...prev, [field]: value })); }; const handleNext = async () => { if (!isValid) return; setIsLoading(true); try { // 이메일 중복 확인 const response = await fetch('/api/auth/check-email', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: data.email }) }); const result = await response.json(); if (!response.ok) { if (result.error === 'EMAIL_EXISTS') { alert('이미 사용 중인 이메일입니다.'); return; } throw new Error(result.error); } onNext(); } catch (error) { console.error('Email check error:', error); alert('이메일 확인 중 오류가 발생했습니다.'); } finally { setIsLoading(false); } }; return (

계정 정보 입력

서비스 이용을 위한 개인 계정을 생성합니다.

handleInputChange('name', e.target.value)} />
handleInputChange('email', e.target.value)} />
handleInputChange('password', e.target.value)} />
handleInputChange('confirmPassword', e.target.value)} /> {data.confirmPassword && data.password !== data.confirmPassword && (

비밀번호가 일치하지 않습니다.

)}
handleInputChange('phone', e.target.value)} />

SMS 인증에 사용됩니다. 국제번호 형식으로 입력해주세요.

); } // Step 3: 업체 등록 (기존 JoinForm 내용) function VendorStep(props) { return ; } // 완전한 업체 등록 폼 컴포넌트 (기존 JoinForm 내용) function CompleteVendorForm({ data, onChange, onBack, onComplete, accountData, consentData, vendorTypes, isLoadingVendorTypes, businessRegistrationFiles, setBusinessRegistrationFiles, isoCertificationFiles, setIsoCertificationFiles, creditReportFiles, setCreditReportFiles, bankAccountFiles, setBankAccountFiles, getPhonePlaceholder, getPhoneDescription, enhancedCountryArray, contactTaskOptions, lng, policyVersions }) { const [isSubmitting, setIsSubmitting] = useState(false); // 담당자 관리 함수들 const addContact = () => { onChange(prev => ({ ...prev, contacts: [...prev.contacts, { contactName: "", contactPosition: "", contactDepartment: "", contactTask: "", contactEmail: "", contactPhone: "", }] })); }; const removeContact = (index) => { onChange(prev => ({ ...prev, contacts: prev.contacts.filter((_, i) => i !== index) })); }; const updateContact = (index, field, value) => { onChange(prev => ({ ...prev, contacts: prev.contacts.map((contact, i) => i === index ? { ...contact, [field]: value } : contact ) })); }; // 폼 입력 변경 핸들러 const handleInputChange = (field, value) => { onChange(prev => ({ ...prev, [field]: value })); }; // 파일 업로드 핸들러들 const createFileUploadHandler = (setFiles, currentFiles) => ({ onDropAccepted: (acceptedFiles) => { const newFiles = [...currentFiles, ...acceptedFiles]; setFiles(newFiles); }, onDropRejected: (fileRejections) => { fileRejections.forEach((rej) => { toast({ variant: "destructive", title: "File Error", description: `${rej.file.name}: ${rej.errors[0]?.message || "Upload failed"}`, }); }); }, removeFile: (index) => { const updated = [...currentFiles]; updated.splice(index, 1); setFiles(updated); } }); const businessRegistrationHandler = createFileUploadHandler(setBusinessRegistrationFiles, businessRegistrationFiles); const isoCertificationHandler = createFileUploadHandler(setIsoCertificationFiles, isoCertificationFiles); const creditReportHandler = createFileUploadHandler(setCreditReportFiles, creditReportFiles); const bankAccountHandler = createFileUploadHandler(setBankAccountFiles, bankAccountFiles); // 유효성 검사 const validateRequiredFiles = () => { const errors = []; if (businessRegistrationFiles.length === 0) { errors.push("사업자등록증을 업로드해주세요."); } if (isoCertificationFiles.length === 0) { errors.push("ISO 인증서를 업로드해주세요."); } if (creditReportFiles.length === 0) { errors.push("신용평가보고서를 업로드해주세요."); } if (data.country !== "KR" && bankAccountFiles.length === 0) { errors.push("대금지급 통장사본을 업로드해주세요."); } return errors; }; const isFormValid = data.vendorName && data.vendorTypeId && data.items && data.country && data.phone && data.email && data.contacts.length > 0 && data.contacts[0].contactName && validateRequiredFiles().length === 0; // 최종 제출 const handleSubmit = async () => { const fileErrors = validateRequiredFiles(); if (fileErrors.length > 0) { toast({ variant: "destructive", title: "파일 업로드 필수", description: fileErrors.join("\n"), }); return; } setIsSubmitting(true); try { const formData = new FormData(); // 통합 데이터 준비 const completeData = { account: accountData, vendor: { ...data, email: data.email || accountData.email, // 업체 이메일이 없으면 계정 이메일 사용 }, consents: { privacy_policy: { agreed: consentData.privacy, version: policyVersions.privacy_policy }, terms_of_service: { agreed: consentData.terms, version: policyVersions.terms_of_service }, marketing: { agreed: consentData.marketing, version: policyVersions.privacy_policy } } }; formData.append('completeData', JSON.stringify(completeData)); // 파일들 추가 businessRegistrationFiles.forEach(file => { formData.append('businessRegistration', file); }); isoCertificationFiles.forEach(file => { formData.append('isoCertification', file); }); creditReportFiles.forEach(file => { formData.append('creditReport', file); }); if (data.country !== "KR") { bankAccountFiles.forEach(file => { formData.append('bankAccount', file); }); } const response = await fetch('/api/auth/signup-with-vendor', { method: 'POST', body: formData, }); const result = await response.json(); if (response.ok) { toast({ title: "등록 완료", description: "회원가입 및 업체 등록이 완료되었습니다. 관리자 승인 후 서비스를 이용하실 수 있습니다.", }); onComplete(); } else { toast({ variant: "destructive", title: "오류", description: result.error || "등록에 실패했습니다.", }); } } catch (error) { console.error(error); toast({ variant: "destructive", title: "서버 에러", description: error.message || "에러가 발생했습니다.", }); } finally { setIsSubmitting(false); } }; return (

업체 정보 등록

업체 정보와 필요한 서류를 등록해주세요. 모든 정보는 관리자 검토 후 승인됩니다.

{/* 기본 정보 */}

기본 정보

{/* 업체 유형 */}
No vendor type found. {vendorTypes.map((type) => ( handleInputChange('vendorTypeId', type.id)} > {lng === "ko" ? type.nameKo : type.nameEn} ))}
{/* 업체명 */}
handleInputChange('vendorName', e.target.value)} disabled={isSubmitting} />

{data.country === "KR" ? "사업자 등록증에 표기된 정확한 회사명을 입력하세요." : "해외 업체의 경우 영문 회사명을 입력하세요."}

{/* 공급품목 */}
handleInputChange('items', e.target.value)} disabled={isSubmitting} />

공급 가능한 제품/서비스를 입력하세요

{/* 사업자등록번호 */}
handleInputChange('taxId', e.target.value)} disabled={isSubmitting} placeholder="123-45-67890" />
{/* 주소 */}
handleInputChange('address', e.target.value)} disabled={isSubmitting} />
{/* 국가 */}
No country found. {enhancedCountryArray.map((country) => ( handleInputChange('country', country.code)} > {country.label} ))}
{/* 대표 전화 */}
handleInputChange('phone', e.target.value)} placeholder={getPhonePlaceholder(data.country)} disabled={isSubmitting} />

{getPhoneDescription(data.country)}

{/* 대표 이메일 */}
handleInputChange('email', e.target.value)} disabled={isSubmitting} placeholder={accountData.email} />

비워두면 계정 이메일({accountData.email})을 사용합니다.

{/* 웹사이트 */}
handleInputChange('website', e.target.value)} disabled={isSubmitting} />
{/* 담당자 정보 */}

담당자 정보 (최소 1명)

{data.contacts.map((contact, index) => (
updateContact(index, 'contactName', e.target.value)} disabled={isSubmitting} />
updateContact(index, 'contactPosition', e.target.value)} disabled={isSubmitting} />
updateContact(index, 'contactDepartment', e.target.value)} disabled={isSubmitting} />
updateContact(index, 'contactEmail', e.target.value)} disabled={isSubmitting} />
updateContact(index, 'contactPhone', e.target.value)} placeholder={getPhonePlaceholder(data.country)} disabled={isSubmitting} />
{data.contacts.length > 1 && (
)}
))}
{/* 한국 사업자 정보 */} {data.country === "KR" && (

한국 사업자 정보

handleInputChange('representativeName', e.target.value)} disabled={isSubmitting} />
handleInputChange('representativeBirth', e.target.value)} disabled={isSubmitting} />
handleInputChange('representativeEmail', e.target.value)} disabled={isSubmitting} />
handleInputChange('representativePhone', e.target.value)} disabled={isSubmitting} />
handleInputChange('corporateRegistrationNumber', e.target.value)} disabled={isSubmitting} />
handleInputChange('representativeWorkExpirence', e.target.checked)} disabled={isSubmitting} className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" />
)} {/* 필수 첨부 서류 */}

필수 첨부 서류

{data.country !== "KR" && ( )}
); } // 파일 업로드 섹션 컴포넌트 function FileUploadSection({ title, description, files, onDropAccepted, onDropRejected, removeFile, isSubmitting, required = true }) { return (
{title} {required && *}

{description}

{({ maxSize }) => (
파일 업로드 드래그 또는 클릭 {maxSize ? ` (최대: ${prettyBytes(maxSize)})` : null}
)}
{files.length > 0 && (
{files.map((file, i) => ( {file.name} {prettyBytes(file.size)} removeFile(i)}> ))}
)}
); }