diff options
Diffstat (limited to 'components/signup/join-form.tsx')
| -rw-r--r-- | components/signup/join-form.tsx | 379 |
1 files changed, 265 insertions, 114 deletions
diff --git a/components/signup/join-form.tsx b/components/signup/join-form.tsx index 30449a63..ecaf6bc3 100644 --- a/components/signup/join-form.tsx +++ b/components/signup/join-form.tsx @@ -39,7 +39,7 @@ import { Check, ChevronsUpDown, Loader2, Plus, X } from "lucide-react" import { cn } from "@/lib/utils" import { useTranslation } from "@/i18n/client" -import { createVendor, getVendorTypes } from "@/lib/vendors/service" +import { getVendorTypes } from "@/lib/vendors/service" import { createVendorSchema, CreateVendorSchema } from "@/lib/vendors/validations" import { Select, @@ -70,6 +70,7 @@ import { import { Badge } from "@/components/ui/badge" import { ScrollArea } from "@/components/ui/scroll-area" import prettyBytes from "pretty-bytes" +import { Checkbox } from "../ui/checkbox" i18nIsoCountries.registerLocale(enLocale) i18nIsoCountries.registerLocale(koLocale) @@ -161,8 +162,11 @@ export function JoinForm() { const [vendorTypes, setVendorTypes] = React.useState<VendorType[]>([]) const [isLoadingVendorTypes, setIsLoadingVendorTypes] = React.useState(true) - // File states - const [selectedFiles, setSelectedFiles] = React.useState<File[]>([]) + // Individual file states + const [businessRegistrationFiles, setBusinessRegistrationFiles] = React.useState<File[]>([]) + const [isoCertificationFiles, setIsoCertificationFiles] = React.useState<File[]>([]) + const [creditReportFiles, setCreditReportFiles] = React.useState<File[]>([]) + const [bankAccountFiles, setBankAccountFiles] = React.useState<File[]>([]) const [isSubmitting, setIsSubmitting] = React.useState(false) @@ -207,7 +211,7 @@ export function JoinForm() { representativeEmail: "", representativePhone: "", corporateRegistrationNumber: "", - attachedFiles: undefined, + representativeWorkExpirence: false, // contacts (no isPrimary) contacts: [ { @@ -220,11 +224,31 @@ export function JoinForm() { }, mode: "onChange", }) - 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); + // Custom validation for file uploads + const validateRequiredFiles = () => { + const errors = [] + + if (businessRegistrationFiles.length === 0) { + errors.push("사업자등록증을 업로드해주세요.") + } + + if (isoCertificationFiles.length === 0) { + errors.push("ISO 인증서를 업로드해주세요.") + } + + if (creditReportFiles.length === 0) { + errors.push("신용평가보고서를 업로드해주세요.") + } + + if (form.watch("country") !== "KR" && bankAccountFiles.length === 0) { + errors.push("대금지급 통장사본을 업로드해주세요.") + } + + return errors + } + + const isFormValid = form.formState.isValid && validateRequiredFiles().length === 0 // Field array for contacts const { fields: contactFields, append: addContact, remove: removeContact } = @@ -233,36 +257,53 @@ export function JoinForm() { name: "contacts", }) - // Dropzone handlers - 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"}`, + // File upload handlers + const createFileUploadHandler = ( + setFiles: React.Dispatch<React.SetStateAction<File[]>>, + currentFiles: File[] + ) => ({ + onDropAccepted: (acceptedFiles: File[]) => { + const newFiles = [...currentFiles, ...acceptedFiles] + setFiles(newFiles) + }, + onDropRejected: (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 }) - } + }, + removeFile: (index: number) => { + 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) // Submit async function onSubmit(values: CreateVendorSchema) { + const fileErrors = validateRequiredFiles() + if (fileErrors.length > 0) { + toast({ + variant: "destructive", + title: "파일 업로드 필수", + description: fileErrors.join("\n"), + }) + return + } + setIsSubmitting(true) try { - const mainFiles = values.attachedFiles - ? Array.from(values.attachedFiles as FileList) - : [] + const formData = new FormData() + // Add vendor data const vendorData = { vendorName: values.vendorName, vendorTypeId: values.vendorTypeId, @@ -279,16 +320,40 @@ export function JoinForm() { representativeBirth: values.representativeBirth || "", representativeEmail: values.representativeEmail || "", representativePhone: values.representativePhone || "", - corporateRegistrationNumber: values.corporateRegistrationNumber || "" + corporateRegistrationNumber: values.corporateRegistrationNumber || "", + representativeWorkExpirence: values.representativeWorkExpirence || false + } + + formData.append('vendorData', JSON.stringify(vendorData)) + formData.append('contacts', JSON.stringify(values.contacts)) + + // Add files with specific types + businessRegistrationFiles.forEach(file => { + formData.append('businessRegistration', file) + }) + + isoCertificationFiles.forEach(file => { + formData.append('isoCertification', file) + }) + + creditReportFiles.forEach(file => { + formData.append('creditReport', file) + }) + + if (values.country !== "KR") { + bankAccountFiles.forEach(file => { + formData.append('bankAccount', file) + }) } - const result = await createVendor({ - vendorData, - files: mainFiles, - contacts: values.contacts, + const response = await fetch('/api/vendors', { + method: 'POST', + body: formData, }) - if (!result.error) { + const result = await response.json() + + if (response.ok) { toast({ title: "등록 완료", description: "회사 등록이 완료되었습니다. (status=PENDING_REVIEW)", @@ -340,7 +405,7 @@ export function JoinForm() { } }; - const getPhoneDescription = (countryCode: string) => { + const getPhoneDescription = (countryCode: string) => { if (!countryCode) return "국가를 먼저 선택해주세요."; const dialCode = countryDialCodes[countryCode]; @@ -359,7 +424,84 @@ export function JoinForm() { return `${dialCode}로 시작하는 국제 전화번호를 입력하세요.`; } }; - + + // File display component + const FileUploadSection = ({ + title, + description, + files, + onDropAccepted, + onDropRejected, + removeFile, + required = true + }: { + title: string; + description: string; + files: File[]; + onDropAccepted: (files: File[]) => void; + onDropRejected: (rejections: any[]) => void; + removeFile: (index: number) => void; + required?: boolean; + }) => ( + <div className="space-y-4"> + <div> + <h5 className="text-sm font-medium"> + {title} + {required && <span className="text-red-500 ml-1">*</span>} + </h5> + <p className="text-xs text-muted-foreground mt-1">{description}</p> + </div> + + <Dropzone + maxSize={MAX_FILE_SIZE} + multiple + onDropAccepted={onDropAccepted} + onDropRejected={onDropRejected} + disabled={isSubmitting} + > + {({ maxSize }) => ( + <DropzoneZone className="flex justify-center"> + <DropzoneInput /> + <div className="flex items-center gap-4"> + <DropzoneUploadIcon /> + <div className="grid gap-1"> + <DropzoneTitle>파일 업로드</DropzoneTitle> + <DropzoneDescription> + 드래그 또는 클릭 + {maxSize ? ` (최대: ${prettyBytes(maxSize)})` : null} + </DropzoneDescription> + </div> + </div> + </DropzoneZone> + )} + </Dropzone> + + {files.length > 0 && ( + <div className="mt-2"> + <ScrollArea className="max-h-32"> + <FileList className="gap-2"> + {files.map((file, i) => ( + <FileListItem key={file.name + i}> + <FileListHeader> + <FileListIcon /> + <FileListInfo> + <FileListName>{file.name}</FileListName> + <FileListDescription> + {prettyBytes(file.size)} + </FileListDescription> + </FileListInfo> + <FileListAction onClick={() => removeFile(i)}> + <X className="h-4 w-4" /> + </FileListAction> + </FileListHeader> + </FileListItem> + ))} + </FileList> + </ScrollArea> + </div> + )} + </div> + ) // Render return ( @@ -391,7 +533,7 @@ export function JoinForm() { <div className="rounded-md border p-4 space-y-4"> <h4 className="text-md font-semibold">기본 정보</h4> <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> - {/* Vendor Type - New Field */} + {/* Vendor Type */} <FormField control={form.control} name="vendorTypeId" @@ -481,7 +623,7 @@ export function JoinForm() { )} /> - {/* Items - New Field */} + {/* Items */} <FormField control={form.control} name="items" @@ -516,7 +658,7 @@ export function JoinForm() { )} /> - {/* Country - Updated with enhanced list */} + {/* Country */} <FormField control={form.control} name="country" @@ -583,8 +725,7 @@ export function JoinForm() { ) }} /> - - {/* Phone - Updated with country code hint */} + {/* Phone */} <FormField control={form.control} name="phone" @@ -611,7 +752,7 @@ export function JoinForm() { )} /> - {/* Email - Updated with company domain guidance */} + {/* Email */} <FormField control={form.control} name="email" @@ -679,7 +820,7 @@ export function JoinForm() { className="bg-muted/10 rounded-md p-4 space-y-4" > <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> - {/* contactName - All required now */} + {/* contactName */} <FormField control={form.control} name={`contacts.${index}.contactName`} @@ -696,7 +837,7 @@ export function JoinForm() { )} /> - {/* contactPosition - Now required */} + {/* contactPosition */} <FormField control={form.control} name={`contacts.${index}.contactPosition`} @@ -730,7 +871,7 @@ export function JoinForm() { )} /> - {/* contactPhone - Now required */} + {/* contactPhone */} <FormField control={form.control} name={`contacts.${index}.contactPhone`} @@ -777,7 +918,6 @@ export function JoinForm() { <div className="rounded-md border p-4 space-y-4"> <h4 className="text-md font-semibold">한국 사업자 정보</h4> - {/* 대표자 등... all now required for Korean companies */} <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <FormField control={form.control} @@ -858,78 +998,89 @@ export function JoinForm() { </FormItem> )} /> + +<FormField + control={form.control} + name="representativeWorkExpirence" + render={({ field }) => ( + <FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + disabled={isSubmitting} + /> + </FormControl> + <div className="space-y-1 leading-none"> + <FormLabel> + 대표자 삼성중공업 근무이력 + </FormLabel> + <FormDescription> + 대표자가 삼성중공업에서 근무한 경험이 있는 경우 체크해주세요. + </FormDescription> + </div> + </FormItem> + )} + /> + </div> </div> )} {/* ───────────────────────────────────────── - 첨부파일 (사업자등록증 등) + Required Document Uploads ───────────────────────────────────────── */} - <div className="rounded-md border p-4 space-y-4"> - <h4 className="text-md font-semibold">기타 첨부파일</h4> - <FormField - control={form.control} - name="attachedFiles" - render={() => ( - <FormItem> - <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500"> - 첨부 파일 - </FormLabel> - <FormDescription> - 사업자등록증, ISO 9001 인증서, 회사 브로셔, 기본 소개자료 등을 첨부해주세요. - </FormDescription> - <Dropzone - maxSize={MAX_FILE_SIZE} - multiple - onDropAccepted={handleDropAccepted} - onDropRejected={handleDropRejected} - disabled={isSubmitting} - > - {({ maxSize }) => ( - <DropzoneZone className="flex justify-center"> - <DropzoneInput /> - <div className="flex items-center gap-4"> - <DropzoneUploadIcon /> - <div className="grid gap-1"> - <DropzoneTitle>파일 업로드</DropzoneTitle> - <DropzoneDescription> - 드래그 또는 클릭 - {maxSize - ? ` (최대: ${prettyBytes(maxSize)})` - : null} - </DropzoneDescription> - </div> - </div> - </DropzoneZone> - )} - </Dropzone> - {selectedFiles.length > 0 && ( - <div className="mt-2"> - <ScrollArea className="max-h-32"> - <FileList className="gap-2"> - {selectedFiles.map((file, i) => ( - <FileListItem key={file.name + i}> - <FileListHeader> - <FileListIcon /> - <FileListInfo> - <FileListName>{file.name}</FileListName> - <FileListDescription> - {prettyBytes(file.size)} - </FileListDescription> - </FileListInfo> - <FileListAction onClick={() => removeFile(i)}> - <X className="h-4 w-4" /> - </FileListAction> - </FileListHeader> - </FileListItem> - ))} - </FileList> - </ScrollArea> - </div> - )} - </FormItem> - )} + <div className="rounded-md border p-4 space-y-6"> + <h4 className="text-md font-semibold">필수 첨부 서류</h4> + + {/* Business Registration */} + <FileUploadSection + title="사업자등록증" + description="사업자등록증 스캔본 또는 사진을 업로드해주세요. 모든 내용이 선명하게 보여야 합니다." + files={businessRegistrationFiles} + onDropAccepted={businessRegistrationHandler.onDropAccepted} + onDropRejected={businessRegistrationHandler.onDropRejected} + removeFile={businessRegistrationHandler.removeFile} /> + + <Separator /> + + {/* ISO Certification */} + <FileUploadSection + title="ISO 인증서" + description="ISO 9001, ISO 14001 등 품질/환경 관리 인증서를 업로드해주세요. 유효기간이 확인 가능해야 합니다." + files={isoCertificationFiles} + onDropAccepted={isoCertificationHandler.onDropAccepted} + onDropRejected={isoCertificationHandler.onDropRejected} + removeFile={isoCertificationHandler.removeFile} + /> + + <Separator /> + + {/* Credit Report */} + <FileUploadSection + title="신용평가보고서" + description="신용평가기관(KIS, NICE 등)에서 발급한 발행 1년 이내의 신용평가보고서를 업로드해주세요. 전년도 재무제표 필수표시. 신규업체, 영세업체로 재무제표 및 신용평가 결과가 없을 경우는 국세, 지방세 납입 증명으로 신용평가를 갈음할 수 있음" + files={creditReportFiles} + onDropAccepted={creditReportHandler.onDropAccepted} + onDropRejected={creditReportHandler.onDropRejected} + removeFile={creditReportHandler.removeFile} + /> + + {/* Bank Account Copy - Only for non-Korean companies */} + {form.watch("country") !== "KR" && ( + <> + <Separator /> + <FileUploadSection + title="대금지급 통장사본" + description="대금 지급용 은행 계좌의 통장 사본 또는 계좌증명서를 업로드해주세요. 계좌번호와 예금주명이 명확히 보여야 합니다." + files={bankAccountFiles} + onDropAccepted={bankAccountHandler.onDropAccepted} + onDropRejected={bankAccountHandler.onDropRejected} + removeFile={bankAccountHandler.removeFile} + /> + </> + )} </div> {/* ───────────────────────────────────────── |
