"use client" import * as React from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm, useFieldArray } from "react-hook-form" import { useRouter, useParams } from "next/navigation" import { useSession } from "next-auth/react" 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, Download, Loader2, Plus, X, FileText, Eye, Upload, CheckCircle } from "lucide-react" import { cn } from "@/lib/utils" import { useTranslation } from "@/i18n/client" import { getVendorDetailById, downloadVendorAttachments, updateVendorInfo } from "@/lib/vendors/service" import { updateVendorSchema, updateVendorSchemaWithConditions, type UpdateVendorInfoSchema } from "@/lib/vendors/validations" import { fetchVendorRegistrationStatus } from "@/lib/vendor-regular-registrations/service" import { DocumentStatusDialog } from "@/components/vendor-regular-registrations/document-status-dialog" import { AdditionalInfoDialog } from "@/components/vendor-regular-registrations/additional-info-dialog" import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig" 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" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" import { InformationButton } from "@/components/information/information-button" // 보안 파일 다운로드 유틸리티 import import { quickDownload, smartFileAction, getFileInfo, formatFileSize, getSecurityInfo } from "@/lib/file-download" 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 cashFlowRatingScaleMap: Record = { NICE: ["우수", "양호", "보통", "미흡", "불량"], KIS: ["A+", "A", "B+", "B", "C", "D"], KED: ["1등급", "2등급", "3등급", "4등급", "5등급"], SCI: ["Level 1", "Level 2", "Level 3", "Level 4"], } const MAX_FILE_SIZE = 3e9 const IMAGE_FILE_SIZE = 5e6 // 5MB // 첨부파일 타입 정의 const ATTACHMENT_TYPES = [ { value: "BUSINESS_REGISTRATION", label: "사업자등록증" }, { value: "CREDIT_REPORT", label: "신용평가보고서" }, { value: "BANK_ACCOUNT_COPY", label: "통장사본" }, { value: "ISO_CERTIFICATION", label: "ISO인증서" }, { value: "GENERAL", label: "일반 문서" }, ] as const type AttachmentType = typeof ATTACHMENT_TYPES[number]['value'] // 파일 타입 정의 interface AttachmentFile { id: number fileName: string filePath: string attachmentType: string fileSize?: number } export function InfoForm() { const params = useParams() || {} const lng = params.lng ? String(params.lng) : "ko" const { t } = useTranslation(lng, "translation") const router = useRouter() const { data: session } = useSession() const companyId = session?.user?.companyId || "17" // 협력업체 데이터 상태 const [vendor, setVendor] = React.useState(null) const [isLoading, setIsLoading] = React.useState(true) const [isSubmitting, setIsSubmitting] = React.useState(false) // 첨부파일 상태 const [existingFiles, setExistingFiles] = React.useState([]) const [existingCreditFiles, setExistingCreditFiles] = React.useState([]) const [existingCashFlowFiles, setExistingCashFlowFiles] = React.useState([]) const [existingSignatureFiles, setExistingSignatureFiles] = React.useState([]) const [selectedFiles, setSelectedFiles] = React.useState([]) const [creditRatingFile, setCreditRatingFile] = React.useState([]) const [cashFlowRatingFile, setCashFlowRatingFile] = React.useState([]) const [isDownloading, setIsDownloading] = React.useState(false); // 정규등록 관련 상태 const [registrationData, setRegistrationData] = React.useState(null) const [documentDialogOpen, setDocumentDialogOpen] = React.useState(false) const [additionalInfoDialogOpen, setAdditionalInfoDialogOpen] = React.useState(false) // 첨부파일 타입 선택 상태 const [selectedAttachmentType, setSelectedAttachmentType] = React.useState("GENERAL") // 서명/직인 업로드 관련 상태 const [signatureFiles, setSignatureFiles] = React.useState([]) const [hasSignature, setHasSignature] = React.useState(false) const [signaturePreviewUrl, setSignaturePreviewUrl] = React.useState(null) // React Hook Form const form = useForm({ resolver: zodResolver(updateVendorSchemaWithConditions), defaultValues: { vendorName: "", taxId: "", address: "", addressDetail: "", postalCode: "", email: "", phone: "", country: "", website: "", representativeName: "", representativeBirth: "", representativeEmail: "", representativePhone: "", corporateRegistrationNumber: "", creditAgency: "", creditRating: "", cashFlowRating: "", attachedFiles: undefined, creditRatingAttachment: undefined, cashFlowRatingAttachment: undefined, contacts: [ { contactName: "", contactPosition: "", contactEmail: "", contactPhone: "", }, ], }, mode: "onChange", }) const isFormValid = form.formState.isValid // Field array for contacts const { fields: contactFields, append: addContact, remove: removeContact, replace: replaceContacts } = useFieldArray({ control: form.control, name: "contacts", }) // 협력업체 정보 가져오기 React.useEffect(() => { async function fetchVendorData() { if (!companyId) return try { setIsLoading(true) // 협력업체 상세 정보 가져오기 (view 사용) const vendorData = await getVendorDetailById(Number(companyId)) if (!vendorData) { toast({ variant: "destructive", title: "오류", description: "협력업체 정보를 찾을 수 없습니다.", }) return } setVendor(vendorData) // 첨부파일 정보 분류 (view에서 이미 파싱된 attachments 배열 사용) if (vendorData.attachments && Array.isArray(vendorData.attachments)) { // 신용평가 관련 파일들만 따로 분리 const creditFiles = vendorData.attachments.filter( (file: AttachmentFile) => file.attachmentType === "CREDIT_RATING" ) const cashFlowFiles = vendorData.attachments.filter( (file: AttachmentFile) => file.attachmentType === "CASH_FLOW_RATING" ) // 서명/직인 파일들 분리 const signatureFiles = vendorData.attachments.filter( (file: AttachmentFile) => file.attachmentType === "SIGNATURE" || file.attachmentType === "SEAL" ) // 나머지 모든 파일들 (사업자등록증, 신용평가보고서, 통장사본, ISO인증서, 일반문서 등) const otherFiles = vendorData.attachments.filter( (file: AttachmentFile) => file.attachmentType !== "CREDIT_RATING" && file.attachmentType !== "CASH_FLOW_RATING" && file.attachmentType !== "SIGNATURE" && file.attachmentType !== "SEAL" ) setExistingFiles(otherFiles) // 모든 기타 파일들을 일반 첨부파일 섹션에 표시 setExistingCreditFiles(creditFiles) setExistingCashFlowFiles(cashFlowFiles) setExistingSignatureFiles(signatureFiles) // 서명/직인 파일들 // 기존 서명/직인 파일이 이미지인 경우 미리보기 URL 생성 if (signatureFiles.length > 0) { const signatureFile = signatureFiles[0] const fileName = signatureFile.fileName.toLowerCase() const isImage = fileName.includes('.jpg') || fileName.includes('.jpeg') || fileName.includes('.png') || fileName.includes('.gif') || fileName.includes('.webp') if (isImage) { // 실제 파일 경로 사용 (DB에 저장된 filePath) // filePath는 이미 /vendors/{vendorId}/{hashedFileName} 형태 setSignaturePreviewUrl(signatureFile.filePath) } } } // 폼 기본값 설정 (연락처 포함) const formValues = { vendorName: vendorData.vendorName || "", taxId: vendorData.taxId || "", address: vendorData.address || "", addressDetail: vendorData.addressDetail || "", postalCode: vendorData.postalCode || "", email: vendorData.email || "", phone: vendorData.phone || "", country: vendorData.country || "", website: vendorData.website || "", representativeName: vendorData.representativeName || "", representativeBirth: vendorData.representativeBirth || "", representativeEmail: vendorData.representativeEmail || "", representativePhone: vendorData.representativePhone || "", corporateRegistrationNumber: vendorData.corporateRegistrationNumber || "", creditAgency: vendorData.creditAgency || "", creditRating: vendorData.creditRating || "", cashFlowRating: vendorData.cashFlowRating || "", } form.reset(formValues) // 연락처 필드 업데이트 if (vendorData.contacts && Array.isArray(vendorData.contacts) && vendorData.contacts.length > 0) { const formattedContacts = vendorData.contacts.map((contact: any) => ({ id: contact.id, contactName: contact.contactName || "", contactPosition: contact.contactPosition || "", contactEmail: contact.contactEmail || "", contactPhone: contact.contactPhone || "", isPrimary: contact.isPrimary || false, })) replaceContacts(formattedContacts) } // 정규등록 상태 데이터 로드 (없는 경우 에러가 아님) try { const registrationResult = await fetchVendorRegistrationStatus(Number(companyId)) if (registrationResult.success) { setRegistrationData(registrationResult.data) } else if (registrationResult.noRegistration) { // 정규등록 데이터가 없는 경우는 정상적인 상황 (기존 정규업체 등) console.log("정규등록 데이터 없음 - 기존 정규업체이거나 아직 등록 진행하지 않음") setRegistrationData(null) } else { // 실제 에러인 경우 console.error("정규등록 상태 조회 오류:", registrationResult.error) setRegistrationData(null) } } catch (error) { console.error("정규등록 상태 조회 중 예외 발생:", error) setRegistrationData(null) } } catch (error) { console.error("Error fetching vendor data:", error) toast({ variant: "destructive", title: "데이터 로드 오류", description: "협력업체 정보를 불러오는 중 오류가 발생했습니다.", }) } finally { setIsLoading(false) } } fetchVendorData() }, [companyId, form, replaceContacts]) // 컴포넌트 언마운트 시 미리보기 URL 정리 (blob URL만) React.useEffect(() => { return () => { if (signaturePreviewUrl && signaturePreviewUrl.startsWith('blob:')) { URL.revokeObjectURL(signaturePreviewUrl) } } }, [signaturePreviewUrl]) // 보안 다운로드 유틸리티를 사용한 개별 파일 다운로드 const handleDownloadFile = async (file: AttachmentFile) => { try { setIsDownloading(true); // 파일이 객체인지 ID인지 확인하고 처리 const fileId = typeof file === 'object' ? file.id : file; const fileName = typeof file === 'object' ? file.fileName : `file-${fileId}`; // API 엔드포인트 URL 구성 const downloadUrl = `/api/vendors/attachments/download?id=${fileId}&vendorId=${Number(companyId)}`; // 보안 다운로드 유틸리티 사용 const { downloadFile } = await import('@/lib/file-download') const result = await downloadFile(downloadUrl, fileName, { action: 'download', showToast: false, // 우리가 직접 토스트 관리 onSuccess: (fileName, fileSize) => { const sizeText = fileSize ? ` (${formatFileSize(fileSize)})` : ''; toast({ title: "다운로드 완료", description: `파일 다운로드가 완료되었습니다: ${fileName}${sizeText}`, }); }, onError: (error) => { console.error("Download error:", error); toast({ variant: "destructive", title: "다운로드 오류", description: error || "파일 다운로드 중 오류가 발생했습니다.", }); } }); if (!result.success && result.error) { // 오류 처리는 onError 콜백에서 이미 처리됨 console.error("Download failed:", result.error); } } catch (error) { console.error("Error downloading file:", error); toast({ variant: "destructive", title: "다운로드 오류", description: "파일 다운로드 중 예상치 못한 오류가 발생했습니다.", }); } finally { setIsDownloading(false); } }; // 보안 다운로드 유틸리티를 사용한 전체 파일 다운로드 const handleDownloadAllFiles = async () => { try { setIsDownloading(true); // 전체 파일 다운로드 API 엔드포인트 const downloadUrl = `/api/vendors/attachments/download-all?vendorId=${Number(companyId)}`; const fileName = `vendor-${companyId}-files.zip`; // 보안 다운로드 유틸리티 사용 const { downloadFile } = await import('@/lib/file-download') const result = await downloadFile(downloadUrl, fileName, { action: 'download', showToast: false, // 우리가 직접 토스트 관리 onSuccess: (fileName, fileSize) => { const sizeText = fileSize ? ` (${formatFileSize(fileSize)})` : ''; toast({ title: "전체 다운로드 완료", description: `전체 파일 다운로드가 완료되었습니다: ${fileName}${sizeText}`, }); }, onError: (error) => { console.error("Download all error:", error); toast({ variant: "destructive", title: "다운로드 오류", description: error || "전체 파일 다운로드 중 오류가 발생했습니다.", }); } }); if (!result.success && result.error) { // 오류 처리는 onError 콜백에서 이미 처리됨 console.error("Download all failed:", result.error); } } catch (error) { console.error("Error downloading files:", error); toast({ variant: "destructive", title: "다운로드 오류", description: "전체 파일 다운로드 중 예상치 못한 오류가 발생했습니다.", }); } finally { setIsDownloading(false); } }; // 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"}`, }) }) } 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 }) } // 기존 파일 삭제 (ID 목록 관리) const [filesToDelete, setFilesToDelete] = React.useState([]) const handleDeleteExistingFile = (fileId: number) => { // 삭제할 ID 목록에 추가 setFilesToDelete([...filesToDelete, fileId]) // 서명/직인 파일을 삭제하는 경우 미리보기도 정리 const isSignatureFile = existingSignatureFiles.some(file => file.id === fileId) if (isSignatureFile && signaturePreviewUrl) { // 서버 URL인 경우 revokeObjectURL을 호출하지 않음 (blob URL이 아니므로) setSignaturePreviewUrl(null) } // UI에서 제거 setExistingFiles(existingFiles.filter(file => file.id !== fileId)) setExistingCreditFiles(existingCreditFiles.filter(file => file.id !== fileId)) setExistingCashFlowFiles(existingCashFlowFiles.filter(file => file.id !== fileId)) setExistingSignatureFiles(existingSignatureFiles.filter(file => file.id !== fileId)) toast({ title: "파일 삭제 표시됨", description: "저장 시 파일이 영구적으로 삭제됩니다.", }) } // 이미지 파일 검증 함수 (서명용) const isImageFile = (file: File): boolean => { const imageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'] return imageTypes.includes(file.type) } // 서명/직인 업로드 핸들러들 (한 개만 허용) const handleSignatureDropAccepted = (acceptedFiles: File[]) => { // 첫 번째 파일만 사용 (한 개만 허용) const newFile = acceptedFiles[0] if (newFile) { // 기존 서명/직인 파일이 있으면 삭제 목록에 추가 if (existingSignatureFiles.length > 0) { const existingFileId = existingSignatureFiles[0].id setFilesToDelete([...filesToDelete, existingFileId]) setExistingSignatureFiles([]) // UI에서 제거 toast({ title: "서명/직인 교체", description: "기존 서명/직인이 새 파일로 교체됩니다.", }) } // 기존 미리보기 URL 정리 (blob URL만) if (signaturePreviewUrl && signaturePreviewUrl.startsWith('blob:')) { URL.revokeObjectURL(signaturePreviewUrl) } setSignatureFiles([newFile]) // 새 파일 설정 setHasSignature(true) // 이미지 파일인 경우 미리보기 생성 if (isImageFile(newFile)) { const previewUrl = URL.createObjectURL(newFile) setSignaturePreviewUrl(previewUrl) } else { setSignaturePreviewUrl(null) } if (acceptedFiles.length > 1) { toast({ title: "파일 제한", description: "서명/직인은 한 개만 등록할 수 있습니다. 첫 번째 파일만 선택되었습니다.", }) } } } const handleSignatureDropRejected = (fileRejections: any[]) => { fileRejections.forEach((rej) => { toast({ variant: "destructive", title: "파일 오류", description: `${rej.file.name}: ${rej.errors[0]?.message || "업로드 실패"}`, }) }) } const removeSignatureFile = () => { setSignatureFiles([]) setHasSignature(false) if (signaturePreviewUrl && signaturePreviewUrl.startsWith('blob:')) { URL.revokeObjectURL(signaturePreviewUrl) } setSignaturePreviewUrl(null) } // 파일 타입 라벨 가져오기 const getAttachmentTypeLabel = (type: string) => { const attachmentType = ATTACHMENT_TYPES.find(t => t.value === type) return attachmentType?.label || type } const handleAdditionalInfoSave = async () => { // 데이터 새로고침 try { const registrationResult = await fetchVendorRegistrationStatus(Number(companyId)) if (registrationResult.success) { setRegistrationData(registrationResult.data) toast({ title: "데이터 새로고침", description: "등록 현황 데이터가 새로고침되었습니다.", }) } else if (registrationResult.noRegistration) { // 정규등록 데이터가 없는 경우는 정상적인 상황 setRegistrationData(null) } else { // 실제 에러인 경우 console.error("정규등록 상태 새로고침 오류:", registrationResult.error) setRegistrationData(null) } } catch (error) { console.error("정규등록 상태 새로고침 중 예외 발생:", error) setRegistrationData(null) } } // Submit async function onSubmit(values: UpdateVendorInfoSchema) { if (!companyId) { toast({ variant: "destructive", title: "오류", description: "회사 ID를 찾을 수 없습니다.", }) return } 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 = { id: Number(companyId), vendorName: values.vendorName, website: values.website, address: values.address, addressDetail: values.addressDetail, postalCode: values.postalCode, email: values.email, phone: values.phone, country: values.country, representativeName: values.representativeName || "", representativeBirth: values.representativeBirth || "", representativeEmail: values.representativeEmail || "", representativePhone: values.representativePhone || "", corporateRegistrationNumber: values.corporateRegistrationNumber || "", creditAgency: values.creditAgency || "", creditRating: values.creditRating || "", cashFlowRating: values.cashFlowRating || "", } // 서버 액션 직접 호출 (기존 fetch API 호출 대신) const result = await updateVendorInfo({ vendorData, files: mainFiles, creditRatingFiles, cashFlowRatingFiles, signatureFiles, // 서명/직인 파일들 contacts: values.contacts, filesToDelete, // 삭제할 파일 ID 목록 selectedAttachmentType, // 선택된 첨부파일 타입 }) if (!result.error) { toast({ title: "업데이트 완료", description: "회사 정보가 성공적으로 업데이트되었습니다.", }) // 삭제할 파일 목록 초기화 setFilesToDelete([]) // 페이지 새로고침하여 업데이트된 정보 표시 router.refresh() } else { toast({ variant: "destructive", title: "오류", description: result.error || "업데이트에 실패했습니다.", }) } } catch (error: any) { console.error(error) toast({ variant: "destructive", title: "서버 에러", description: error.message || "에러가 발생했습니다.", }) } finally { setIsSubmitting(false) } } if (isLoading) { return (
협력업체 정보를 불러오는 중입니다...
) } // 보안 정보 가져오기 (선택적으로 사용자에게 표시) // const securityInfo = getSecurityInfo(); // Render return (

{t("infoForm.title", { defaultValue: "Update Vendor Information", })}

{t("infoForm.description", { defaultValue: "Here you can view and update your company information and attachments.", })}

{vendor?.status && (
{vendor.status}
)} {/* 보안 정보 표시 (선택적) */} {/*

📁 허용 파일 크기: {securityInfo.maxFileSizeFormatted} | 남은 다운로드: {securityInfo.remainingDownloads}/분

*/}
{/* 서명/직인 등록과 첨부파일 요약을 하나의 행으로 배치 */}
{/* 서명/직인 등록 섹션 */} 회사 서명/직인 등록 (선택사항) 회사의 공식 서명이나 직인을 등록하여 계약서 및 공식 문서에 사용할 수 있습니다. {/* 현재 등록된 서명/직인 파일 표시 (한 개만) */} {(existingSignatureFiles.length > 0 || signatureFiles.length > 0) && (
{/* 기존 등록된 서명/직인 (첫 번째만 표시) */} {existingSignatureFiles.length > 0 && signatureFiles.length === 0 && (
{(() => { const file = existingSignatureFiles[0]; const fileInfo = getFileInfo(file.fileName); return (
{fileInfo.icon} {file.fileName}
{getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'}
handleDownloadFile(file)} disabled={isDownloading} > {isDownloading ? : } handleDeleteExistingFile(file.id)}>
); })()}
{/* 기존 서명 미리보기 (이미지인 경우만) */} {signaturePreviewUrl && !signatureFiles.length && (
등록된 서명/직인
등록된 서명/직인 { console.error("기존 서명 이미지 로드 실패") setSignaturePreviewUrl(null) }} // 추가 보안: referrer policy 설정 referrerPolicy="no-referrer" />
)}
)} {/* 새로 업로드된 서명/직인 */} {signatureFiles.length > 0 && (
{(() => { const file = signatureFiles[0]; return (
{file.name}
서명/직인 (새 파일) | {prettyBytes(file.size)}
); })()}
{/* 서명 미리보기 (이미지인 경우만) */} {signaturePreviewUrl && (
미리보기
서명/직인 미리보기 { toast({ variant: "destructive", title: "미리보기 오류", description: "이미지를 불러올 수 없습니다.", }) setSignaturePreviewUrl(null) }} // 추가 보안: referrer policy 설정 referrerPolicy="no-referrer" />
)}
)}
)} {/* 서명/직인 업로드 드롭존 */} {({ maxSize }) => (
{existingSignatureFiles.length > 0 || signatureFiles.length > 0 ? "서명/직인 교체" : "서명/직인 업로드" } 한 개 파일만 업로드 가능 {maxSize ? ` | 최대: ${prettyBytes(maxSize)}` : ""}
)}
{/* 첨부파일 요약 카드 - 기존 파일 있는 경우만 표시 */} {(existingFiles.length > 0 || existingCreditFiles.length > 0 || existingCashFlowFiles.length > 0) && ( 첨부파일 요약 현재 등록된 파일 목록입니다. 전체 다운로드하거나 개별 파일을 다운로드할 수 있습니다.
{existingFiles.length > 0 && (

첨부파일

{existingFiles.map((file) => { const fileInfo = getFileInfo(file.fileName); return ( {fileInfo.icon} {file.fileName} {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'}
handleDownloadFile(file)} disabled={isDownloading} > {isDownloading ? : } handleDeleteExistingFile(file.id)}>
); })}
)} {existingCreditFiles.length > 0 && (

신용평가 첨부파일

{existingCreditFiles.map((file) => { const fileInfo = getFileInfo(file.fileName); return ( {fileInfo.icon} {file.fileName} {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'}
handleDownloadFile(file)} disabled={isDownloading} > {isDownloading ? : } handleDeleteExistingFile(file.id)}>
); })}
)} {existingCashFlowFiles.length > 0 && (

현금흐름 첨부파일

{existingCashFlowFiles.map((file) => { const fileInfo = getFileInfo(file.fileName); return ( {fileInfo.icon} {file.fileName} {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'}
handleDownloadFile(file)} disabled={isDownloading} > {isDownloading ? : } handleDeleteExistingFile(file.id)}>
); })}
)}
{(existingFiles.length + existingCreditFiles.length + existingCashFlowFiles.length) > 1 && ( )}
)}
{/* 정규업체 등록 현황 섹션 */} {registrationData ? ( 정규업체 등록 현황 정규업체 등록을 위한 현황을 확인하고 관리하세요. {/* 현재 상태 표시 */} {registrationData.registration && (
{registrationData.registration.status === 'under_review' && '검토중'} {registrationData.registration.status === 'approval_ready' && '조건충족'} {registrationData.registration.status === 'in_review' && '정규등록검토'} {registrationData.registration.status === 'completed' && '등록완료'} {registrationData.registration.status === 'pending_approval' && '장기미등록'}
)} {/* 액션 버튼들 */}
) : ( // 정규업체 등록 데이터가 없는 경우 (기존 정규업체이거나 아직 등록 진행 안함) 정규업체 등록 현황 현재 정규업체 등록 진행 상황이 없습니다. {/*

이미 정규업체로 등록되어 있거나, 아직 정규업체 등록을 진행하지 않았습니다.

정규업체 등록이 필요한 경우 담당자에게 문의하세요.

*/}
)}

기본 정보

{/* vendorName is required in the schema → show * */} ( 업체명 )} /> {/* taxId - 읽기 전용으로 표시 */} ( 사업자등록번호 )} /> {/* Address */} ( 주소 )} /> {/* Address Detail */} ( 상세주소 )} /> {/* Postal Code */} ( 우편번호 )} /> ( 대표 전화 )} /> {/* email */} ( 대표 이메일 회사 대표 이메일(관리자 로그인에 사용될 수 있음) )} /> {/* website */} ( 웹사이트 )} /> { 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 = cashFlowRatingScaleMap[ selectedAgency as keyof typeof cashFlowRatingScaleMap ] || [] 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)} > ))}
)}
)} />
)}
)} {/* ───────────────────────────────────────── 첨부파일 (사업자등록증 등) - 추가 파일 ───────────────────────────────────────── */}

기타 첨부파일 추가

{/* 첨부파일 타입 선택 */}
( 첨부 파일 (추가) - {getAttachmentTypeLabel(selectedAttachmentType)} {({ maxSize }) => (
파일 업로드 드래그 또는 클릭 {maxSize ? ` (최대: ${prettyBytes(maxSize)})` : null}
)}
{selectedFiles.length > 0 && (
{selectedFiles.map((file, i) => ( {file.name} {getAttachmentTypeLabel(selectedAttachmentType)} | {prettyBytes(file.size)} removeFile(i)}> ))}
)}
)} />
{/* Submit 버튼 */}
{/* 정규등록 관련 다이얼로그들 - 정규등록 데이터가 있을 때만 표시 */} {registrationData && ( <> {/* 문서 현황 Dialog */} {/* 추가정보 입력 Dialog */} )}
) }