"use client" import * as React from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm, useFieldArray } from "react-hook-form" import { useRouter, useSearchParams, useParams } from "next/navigation" 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 { Button } from "@/components/ui/button" import { Separator } from "@/components/ui/separator" import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" 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 { Check, ChevronsUpDown, Loader2, Plus, X } from "lucide-react" import { cn } from "@/lib/utils" import { useTranslation } from "@/i18n/client" import { createVendor } from "@/lib/vendors/service" import { createVendorSchema, CreateVendorSchema } from "@/lib/vendors/validations" 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 { Badge } from "@/components/ui/badge" import { ScrollArea } from "@/components/ui/scroll-area" import prettyBytes from "pretty-bytes" 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, })) // Example agencies + rating scales const creditAgencies = [ { value: "NICE", label: "NICE평가정보" }, { value: "KIS", label: "KIS (한국신용평가)" }, { value: "KED", label: "KED (한국기업데이터)" }, { value: "SCI", label: "SCI평가정보" }, ] const creditRatingScaleMap: Record = { NICE: ["AAA", "AA", "A", "BBB", "BB", "B", "C", "D"], KIS: ["AAA", "AA+", "AA", "A+", "A", "BBB+", "BBB", "BB", "B", "C"], KED: ["AAA", "AA", "A", "BBB", "BB", "B", "CCC", "CC", "C", "D"], SCI: ["AAA", "AA+", "AA", "AA-", "A+", "A", "A-", "BBB+", "BBB-", "B"], } const MAX_FILE_SIZE = 3e9 export function JoinForm() { const params = useParams() const lng = (params.lng as string) || "ko" const { t } = useTranslation(lng, "translation") const router = useRouter() const searchParams = useSearchParams() const defaultTaxId = searchParams.get("taxID") ?? "" // File states const [selectedFiles, setSelectedFiles] = React.useState([]) const [creditRatingFile, setCreditRatingFile] = React.useState([]) const [cashFlowRatingFile, setCashFlowRatingFile] = React.useState([]) const [isSubmitting, setIsSubmitting] = React.useState(false) // React Hook Form const form = useForm({ resolver: zodResolver(createVendorSchema), defaultValues: { vendorName: "", taxId: defaultTaxId, address: "", email: "", phone: "", country: "", representativeName: "", representativeBirth: "", representativeEmail: "", representativePhone: "", corporateRegistrationNumber: "", creditAgency: "", creditRating: "", cashFlowRating: "", attachedFiles: undefined, creditRatingAttachment: undefined, cashFlowRatingAttachment: undefined, // contacts (no isPrimary) contacts: [ { contactName: "", contactPosition: "", contactEmail: "", contactPhone: "", }, ], }, mode: "onChange", }) const isFormValid = form.formState.isValid // Field array for contacts const { fields: contactFields, append: addContact, remove: removeContact } = useFieldArray({ control: form.control, name: "contacts", }) // Dropzone handlers (same as before)... const handleDropAccepted = (acceptedFiles: File[]) => { const newFiles = [...selectedFiles, ...acceptedFiles] setSelectedFiles(newFiles) form.setValue("attachedFiles", newFiles, { shouldValidate: true }) } const handleDropRejected = (fileRejections: any[]) => { fileRejections.forEach((rej) => { toast({ variant: "destructive", title: "File Error", description: `${rej.file.name}: ${rej.errors[0]?.message || "Upload failed"}`, }) }) } const removeFile = (index: number) => { const updated = [...selectedFiles] updated.splice(index, 1) setSelectedFiles(updated) form.setValue("attachedFiles", updated, { shouldValidate: true }) } const handleCreditDropAccepted = (acceptedFiles: File[]) => { const newFiles = [...creditRatingFile, ...acceptedFiles] setCreditRatingFile(newFiles) form.setValue("creditRatingAttachment", newFiles, { shouldValidate: true }) } const handleCreditDropRejected = (fileRejections: any[]) => { fileRejections.forEach((rej) => { toast({ variant: "destructive", title: "File Error", description: `${rej.file.name}: ${rej.errors[0]?.message || "Upload failed"}`, }) }) } const removeCreditFile = (index: number) => { const updated = [...creditRatingFile] updated.splice(index, 1) setCreditRatingFile(updated) form.setValue("creditRatingAttachment", updated, { shouldValidate: true }) } const handleCashFlowDropAccepted = (acceptedFiles: File[]) => { const newFiles = [...cashFlowRatingFile, ...acceptedFiles] setCashFlowRatingFile(newFiles) form.setValue("cashFlowRatingAttachment", newFiles, { shouldValidate: true }) } const handleCashFlowDropRejected = (fileRejections: any[]) => { fileRejections.forEach((rej) => { toast({ variant: "destructive", title: "File Error", description: `${rej.file.name}: ${rej.errors[0]?.message || "Upload failed"}`, }) }) } const removeCashFlowFile = (index: number) => { const updated = [...cashFlowRatingFile] updated.splice(index, 1) setCashFlowRatingFile(updated) form.setValue("cashFlowRatingAttachment", updated, { shouldValidate: true }) } // Submit async function onSubmit(values: CreateVendorSchema) { setIsSubmitting(true) try { const mainFiles = values.attachedFiles ? Array.from(values.attachedFiles as FileList) : [] const creditRatingFiles = values.creditRatingAttachment ? Array.from(values.creditRatingAttachment as FileList) : [] const cashFlowRatingFiles = values.cashFlowRatingAttachment ? Array.from(values.cashFlowRatingAttachment as FileList) : [] const vendorData = { vendorName: values.vendorName, vendorCode: values.vendorCode, website: values.website, taxId: values.taxId, address: values.address, email: values.email, phone: values.phone, country: values.country, status: "PENDING_REVIEW" as const, representativeName: values.representativeName || "", representativeBirth: values.representativeBirth || "", representativeEmail: values.representativeEmail || "", representativePhone: values.representativePhone || "", corporateRegistrationNumber: values.corporateRegistrationNumber || "", creditAgency: values.creditAgency || "", creditRating: values.creditRating || "", cashFlowRating: values.cashFlowRating || "", } const result = await createVendor({ vendorData, files: mainFiles, creditRatingFiles, cashFlowRatingFiles, contacts: values.contacts, }) if (!result.error) { toast({ title: "등록 완료", description: "회사 등록이 완료되었습니다. (status=PENDING_REVIEW)", }) router.push("/") } else { toast({ variant: "destructive", title: "오류", description: result.error || "등록에 실패했습니다.", }) } } catch (error: any) { console.error(error) toast({ variant: "destructive", title: "서버 에러", description: error.message || "에러가 발생했습니다.", }) } finally { setIsSubmitting(false) } } // Render return (

{defaultTaxId}{" "} {t("joinForm.title", { defaultValue: "Vendor Administrator Creation", })}

{t("joinForm.description", { defaultValue: "Please provide basic company information and attach any required documents (e.g., business registration). We will review and approve as soon as possible.", })}

{/* ───────────────────────────────────────── Basic Info ───────────────────────────────────────── */}

기본 정보

{/* vendorName is required in the schema → show * */} ( 업체명 )} /> {/* Address (optional, no * here) */} ( 주소 )} /> ( 대표 전화 )} /> {/* email is required → show * */} ( 대표 이메일 회사 대표 이메일(관리자 로그인에 사용될 수 있음) )} /> {/* website optional */} ( 웹사이트 )} /> { const selectedCountry = countryArray.find( (c) => c.code === field.value ) return ( Country No country found. {countryArray.map((country) => ( field.onChange(country.code) } > {country.label} ))} ) }} />
{/* ───────────────────────────────────────── 담당자 정보 (contacts) ───────────────────────────────────────── */}

담당자 정보 (최소 1명)

{contactFields.map((contact, index) => (
{/* contactName → required */} ( 담당자명 )} /> {/* contactPosition → optional */} ( 직급 / 부서 )} /> {/* contactEmail → required */} ( 이메일 )} /> {/* contactPhone → optional */} ( 전화번호 )} />
{/* Remove contact button row */} {contactFields.length > 1 && (
)}
))}
{/* ───────────────────────────────────────── 한국 사업자 (country === "KR") ───────────────────────────────────────── */} {form.watch("country") === "KR" && (

한국 사업자 정보

{/* 대표자 등... all optional or whichever you want * for */}
( 대표자 이름 )} /> ( 대표자 생년월일 )} /> ( 대표자 이메일 )} /> ( 대표자 전화번호 )} /> ( 법인등록번호 )} />
{/* 신용/현금 흐름 */}
{ const agencyValue = field.value return ( 평가사 신용평가 및 현금흐름등급에 사용할 평가사 ) }} /> {form.watch("creditAgency") && (
{/* 신용평가등급 */} { const selectedAgency = form.watch("creditAgency") const ratingScale = creditRatingScaleMap[ selectedAgency as keyof typeof creditRatingScaleMap ] || [] return ( 신용평가등급 ) }} /> {/* 현금흐름등급 */} { const selectedAgency = form.watch("creditAgency") const ratingScale = creditRatingScaleMap[ selectedAgency as keyof typeof creditRatingScaleMap ] || [] return ( 현금흐름등급 ) }} />
)}
{/* Credit/CashFlow Attachments */} {form.watch("creditAgency") && (
( 신용평가등급 첨부 {({ maxSize }) => (
드래그 또는 클릭 최대: {maxSize ? prettyBytes(maxSize) : "무제한"}
)}
{creditRatingFile.length > 0 && (
{creditRatingFile.map((file, i) => ( {file.name} {prettyBytes(file.size)} removeCreditFile(i)} > ))}
)}
)} /> {/* Cash Flow Attachment */} ( 현금흐름등급 첨부 {({ maxSize }) => (
드래그 또는 클릭 최대: {maxSize ? prettyBytes(maxSize) : "무제한"}
)}
{cashFlowRatingFile.length > 0 && (
{cashFlowRatingFile.map((file, i) => ( {file.name} {prettyBytes(file.size)} removeCashFlowFile(i)} > ))}
)}
)} />
)}
)} {/* ───────────────────────────────────────── 첨부파일 (사업자등록증 등) ───────────────────────────────────────── */}

기타 첨부파일

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