"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, CommandInput, CommandEmpty, CommandGroup, CommandItem, } from "@/components/ui/command" import { Check, ChevronsUpDown, Loader2, Plus, X } from "lucide-react" import { cn } from "@/lib/utils" import { createTechVendorFromSignup } from "@/lib/tech-vendors/service" import { TechVendorItemSelectorDialog } from "./tech-vendor-item-selector-dialog" import { createTechVendorSchema, CreateTechVendorSchema } from "@/lib/tech-vendors/validations" import { VENDOR_TYPES } from "@/db/schema/techVendors" import { verifyTechVendorInvitationToken } from "@/lib/tech-vendor-invitation-token" 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, })) // Sort countries to put Korea first, then alphabetically const sortedCountryArray = [...countryArray].sort((a, b) => { if (a.code === "KR") return -1; if (b.code === "KR") return 1; return a.label.localeCompare(b.label); }); // Add English names for Korean locale const enhancedCountryArray = sortedCountryArray.map(country => ({ ...country, label: locale === "ko" && country.code === "KR" ? "대한민국 (South Korea)" : country.label })); // Comprehensive list of country dial codes const countryDialCodes: { [key: string]: string } = { 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 export function TechVendorJoinForm() { const params = useParams() || {}; const lng = params.lng ? String(params.lng) : "ko"; const router = useRouter() const searchParams = useSearchParams() || new URLSearchParams(); const defaultTaxId = searchParams.get("taxID") ?? "" const invitationToken = searchParams.get("token") ?? "" // States const [selectedFiles, setSelectedFiles] = React.useState([]) const [isSubmitting, setIsSubmitting] = React.useState(false) const [isLoading, setIsLoading] = React.useState(false) const [hasValidToken, setHasValidToken] = React.useState(null) const [isItemSelectorOpen, setIsItemSelectorOpen] = React.useState(false) const [selectedItemCodes, setSelectedItemCodes] = React.useState([]) // React Hook Form (항상 최상위에서 호출) const form = useForm({ resolver: zodResolver(createTechVendorSchema), defaultValues: { vendorName: "", vendorCode: "", items: "", taxId: defaultTaxId, address: "", email: "", phone: "", country: "", website: "", techVendorType: [], representativeName: "", representativeBirth: "", representativeEmail: "", representativePhone: "", files: undefined, contacts: [ { contactName: "", contactPosition: "", contactEmail: "", contactPhone: "", }, ], }, mode: "onChange", }) // Field array for contacts (항상 최상위에서 호출) const { fields: contactFields, append: addContact, remove: removeContact } = useFieldArray({ control: form.control, name: "contacts", }) // 토큰 검증 로직 React.useEffect(() => { // 토큰이 없으면 유효하지 않음 if (!invitationToken) { setHasValidToken(false); return; } // 토큰이 있으면 검증 수행 const validateToken = async () => { setIsLoading(true); try { const tokenPayload = await verifyTechVendorInvitationToken(invitationToken); if (tokenPayload) { setHasValidToken(true); console.log("tokenPayload", tokenPayload); // 토큰에서 가져온 정보로 폼 미리 채우기 form.setValue("vendorName", tokenPayload.vendorName); form.setValue("email", tokenPayload.email); form.setValue("techVendorType", tokenPayload.vendorType as "조선" | "해양TOP" | "해양HULL" | ("조선" | "해양TOP" | "해양HULL")[]); // // 연락처 정보도 미리 채우기 // form.setValue("contacts.0.contactName", tokenPayload.vendorName); // form.setValue("contacts.0.contactEmail", tokenPayload.email); toast({ title: "초대 정보 로드 완료", description: "기존 정보가 자동으로 입력되었습니다. 추가 정보를 입력해주세요.", }); } else { setHasValidToken(false); toast({ variant: "destructive", title: "유효하지 않은 초대 링크", description: "초대 링크가 만료되었거나 유효하지 않습니다.", }); } } catch (error) { console.error("Token verification error:", error); setHasValidToken(false); toast({ variant: "destructive", title: "오류 발생", description: "초대 정보를 불러오는 중 오류가 발생했습니다.", }); } finally { setIsLoading(false); } }; validateToken(); }, [invitationToken, form, router]); // 토큰이 유효하지 않으면 에러 페이지 표시 if (hasValidToken === false) { return (

유효하지 않은 접근

기술영업 협력업체 등록은 초대를 통해서만 가능합니다.
올바른 초대 링크를 통해 접근해주세요.

); } const isFormValid = form.formState.isValid // console.log("Form errors:", form.formState.errors); // console.log("Form values:", form.getValues()); // console.log("Form valid:", form.formState.isValid); // Dropzone handlers const handleDropAccepted = (acceptedFiles: File[]) => { const newFiles = [...selectedFiles, ...acceptedFiles] setSelectedFiles(newFiles) form.setValue("files", 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("files", updated, { shouldValidate: true }) } const handleItemsSelected = (itemCodes: string[]) => { setSelectedItemCodes(itemCodes) // 선택된 아이템 코드들을 콤마로 구분하여 items 필드에 설정 const itemsString = itemCodes.join(", ") form.setValue("items", itemsString) } // Submit async function onSubmit(values: CreateTechVendorSchema) { setIsSubmitting(true) try { const mainFiles = values.files ? Array.from(values.files as FileList) : [] const techVendorData = { vendorName: values.vendorName, vendorCode: values.vendorCode, items: values.items, website: values.website, taxId: values.taxId, address: values.address, email: values.email, phone: values.phone, country: values.country, techVendorType: values.techVendorType as "조선" | "해양TOP" | "해양HULL" | ("조선" | "해양TOP" | "해양HULL")[], representativeName: values.representativeName || "", representativeBirth: values.representativeBirth || "", representativeEmail: values.representativeEmail || "", representativePhone: values.representativePhone || "", contacts: values.contacts, files: mainFiles, } const result = await createTechVendorFromSignup({ vendorData: techVendorData, files: mainFiles, contacts: values.contacts, selectedItemCodes: selectedItemCodes, invitationToken: invitationToken || undefined, }) if (!result.error) { toast({ title: "등록 완료", description: invitationToken ? "기술영업 업체 정보가 성공적으로 업데이트되었습니다." : "기술영업 업체 등록이 완료되었습니다.", }) router.push("/ko/partners") } else { toast({ variant: "destructive", title: "오류", description: result.error || "등록에 실패했습니다.", }) } } catch (error: any) { console.error(error) toast({ variant: "destructive", title: "서버 에러", description: error.message || "에러가 발생했습니다.", }) } finally { setIsSubmitting(false) } } // Get country code for phone number placeholder const getPhonePlaceholder = (countryCode: string) => { if (!countryCode || !countryDialCodes[countryCode]) return "전화번호"; return `${countryDialCodes[countryCode]} 전화번호`; }; if (isLoading) { return (
기존 정보를 불러오는 중...
); } return (

{invitationToken ? "기술영업 업체 정보 업데이트" : "기술영업 업체 등록"}

{invitationToken ? "기술영업 업체 정보를 업데이트하고 필요한 서류를 첨부해주세요." : "기술영업 업체 기본 정보를 입력하고 필요한 서류를 첨부해주세요. 검토 후 빠른 시일 내에 승인처리 해드리겠습니다." }

{/* ───────────────────────────────────────── 기본 정보 ───────────────────────────────────────── */}

기본 정보

{/* 업체 유형 */} ( 업체 유형
{VENDOR_TYPES.map((type) => (
{ const currentValues = Array.isArray(field.value) ? field.value : []; if (e.target.checked) { field.onChange([...currentValues, type]); } else { field.onChange(currentValues.filter((v: string) => v !== type)); } }} className="rounded border-input" />
))}
)} /> {/* 업체명 */} ( 업체명 )} /> {/* 업체 코드 */} ( 업체 코드 )} /> {/* 사업자등록번호 */} ( 사업자등록번호 )} /> {/* 국가 */} ( 국가 국가를 찾을 수 없습니다. {Object.entries(i18nIsoCountries.getNames("ko")).map(([code, name]) => ( { form.setValue("country", code); }} > {name} ))} )} /> {/* 주소 */} ( 주소 )} /> {/* 이메일 (수정 불가, 뷰 전용) */} ( 이메일 )} /> {/* 전화번호 */} ( 전화번호 )} /> {/* 웹사이트 */} ( 웹사이트 )} /> {/* 주요 품목 */} ( 주요 품목
{selectedItemCodes.length > 0 && ( {selectedItemCodes.length}개 아이템 선택됨 )}
공급가능품목 선택 버튼을 클릭하여 아이템을 선택하세요. 원하는 아이템이 없다면 텍스트로 입력하세요.
)} />
{/* ───────────────────────────────────────── 대표자 정보 ───────────────────────────────────────── */}

대표자 정보

{/* 대표자명 */} ( 대표자명 )} /> {/* 대표자 생년월일 */} ( 대표자 생년월일 )} /> {/* 대표자 이메일 */} ( 대표자 이메일 )} /> {/* 대표자 전화번호 */} ( 대표자 전화번호 )} />
{/* ───────────────────────────────────────── 연락처 정보 ───────────────────────────────────────── */}

연락처 정보

{contactFields.map((field, index) => (
연락처 {index + 1}
{contactFields.length > 1 && ( )}
( 연락처명 )} /> ( 직책 )} /> ( 이메일 )} /> ( 전화번호 )} />
))}
{/* ───────────────────────────────────────── 첨부파일 ───────────────────────────────────────── */}

첨부파일

( 파일을 드래그하거나 클릭하여 업로드 PDF, Word, Excel, 이미지 파일 (최대 3GB) )} /> {selectedFiles.length > 0 && (
업로드된 파일 {selectedFiles.length}개 파일 {selectedFiles.map((file, index) => ( {file.name} {prettyBytes(file.size)} ))}
)}
{/* 공급가능품목 선택 다이얼로그 */}
) }