summaryrefslogtreecommitdiff
path: root/components/signup/join-form.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-07 01:43:36 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-07 01:43:36 +0000
commitfbb3b7f05737f9571b04b0a8f4f15c0928de8545 (patch)
tree343247117a7587b8ef5c418c9528d1cf2e0b6f1c /components/signup/join-form.tsx
parent9945ad119686a4c3a66f7b57782750f78a366cfb (diff)
(대표님) 변경사항 20250707 10시 43분
Diffstat (limited to 'components/signup/join-form.tsx')
-rw-r--r--components/signup/join-form.tsx379
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>
{/* ─────────────────────────────────────────