From 41ad2455ac47d8e2da331d7240ded1354df9a784 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 18 Nov 2025 10:33:20 +0000 Subject: (최겸) 구매 피드백 반영 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../document-status-dialog.tsx | 852 ++++++++++----------- .../registration-request-dialog.tsx | 17 +- 2 files changed, 440 insertions(+), 429 deletions(-) (limited to 'components/vendor-regular-registrations') diff --git a/components/vendor-regular-registrations/document-status-dialog.tsx b/components/vendor-regular-registrations/document-status-dialog.tsx index 5efee64e..02da19bf 100644 --- a/components/vendor-regular-registrations/document-status-dialog.tsx +++ b/components/vendor-regular-registrations/document-status-dialog.tsx @@ -1,426 +1,426 @@ -"use client"; - -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { FileText, Download, CheckCircle, XCircle, Clock, RefreshCw } from "lucide-react"; -import { toast } from "sonner"; - -import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig"; -import { - documentStatusColumns, -} from "@/config/vendorRegularRegistrationsColumnsConfig"; - -interface DocumentStatusDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - registration: VendorRegularRegistration | null; - onRefresh?: () => void; - isVendorUser?: boolean; -} - -const StatusIcon = ({ status }: { status: string | boolean }) => { - if (typeof status === "boolean") { - return status ? ( - - ) : ( - - ); - } - - switch (status) { - case "completed": - return ; - case "reviewing": - return ; - case "not_submitted": - default: - return ; - } -}; - -const StatusBadge = ({ status }: { status: string | boolean }) => { - if (typeof status === "boolean") { - return ( - - {status ? "제출완료" : "미제출"} - - ); - } - - const statusConfig = { - completed: { label: "완료", variant: "default" as const }, - reviewing: { label: "검토중", variant: "secondary" as const }, - not_submitted: { label: "미제출", variant: "destructive" as const }, - }; - - const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.not_submitted; - - return {config.label}; -}; - -export function DocumentStatusDialog({ - open, - onOpenChange, - registration, - onRefresh, - isVendorUser = false, -}: DocumentStatusDialogProps) { - if (!registration) return null; - - // 파일 다운로드 핸들러 - const handleFileDownload = async (docKey: string, fileIndex: number = 0) => { - try { - console.log(`🔍 파일 다운로드 시도:`, { - docKey, - fileIndex, - allDocumentFiles: registration.documentFiles, - registrationId: registration.id, - registrationKeys: Object.keys(registration), - fullRegistration: registration - }); - //isvendoruser인 경우는 실사 결과 파일 다운로드 불가능 - if (isVendorUser && docKey === "auditResult") { - toast.error("실사 결과 파일은 다운로드할 수 없습니다."); - return; - } - - // documentFiles가 없는 경우 처리 - if (!registration.documentFiles) { - console.error(`❌ documentFiles가 없음:`, { - registration, - hasDocumentFiles: !!registration.documentFiles, - registrationKeys: Object.keys(registration) - }); - toast.error("문서 파일 정보를 찾을 수 없습니다. 페이지를 새로고침 해주세요."); - return; - } - - const files = registration.documentFiles[docKey as keyof typeof registration.documentFiles]; - console.log(`📂 ${docKey} 파일 목록:`, files); - - if (!files || files.length === 0) { - console.warn(`❌ ${docKey}에 파일이 없음:`, { files, length: files?.length }); - toast.error("다운로드할 파일이 없습니다."); - return; - } - - const file = files[fileIndex]; - console.log(`📄 선택된 파일 (index ${fileIndex}):`, file); - - if (!file) { - console.warn(`❌ 파일 인덱스 ${fileIndex}에 파일이 없음`); - toast.error("파일을 찾을 수 없습니다."); - return; - } - - // 파일 객체의 모든 속성 확인 - console.log(`🔍 파일 객체 전체 속성:`, Object.keys(file)); - console.log(`🔍 파일 상세 정보:`, { - filePath: file.filePath, - path: file.path, - originalFileName: file.originalFileName, - fileName: file.fileName, - name: file.name, - fullObject: file - }); - - // filePath와 fileName 추출 - const filePath = file.filePath || file.path; - const fileName = file.originalFileName || file.fileName || file.name; - - console.log(`📝 추출된 파일 정보:`, { filePath, fileName }); - - if (!filePath || !fileName) { - console.error(`❌ 파일 정보 누락:`, { - filePath, - fileName, - fileObject: file, - availableKeys: Object.keys(file) - }); - toast.error("파일 정보가 올바르지 않습니다."); - return; - } - - console.log(`📥 파일 다운로드 시작:`, { filePath, fileName, docKey }); - - // downloadFile 함수를 동적으로 import하여 파일 다운로드 - const { downloadFile } = await import('@/lib/file-download'); - const result = await downloadFile(filePath, fileName, { - showToast: true, - onError: (error: any) => { - console.error("파일 다운로드 오류:", error); - toast.error(`파일 다운로드 실패: ${error}`); - }, - onSuccess: (fileName: string, fileSize?: number) => { - console.log(`✅ 파일 다운로드 성공:`, { fileName, fileSize }); - } - }); - - if (!result.success) { - console.error("파일 다운로드 실패:", result.error); - } - } catch (error) { - console.error("파일 다운로드 중 오류 발생:", error); - toast.error("파일 다운로드 중 오류가 발생했습니다."); - } - }; - - // 기본계약 파일 다운로드 핸들러 - const handleContractDownload = async (contractIndex: number) => { - try { - if (!registration.basicContracts || registration.basicContracts.length === 0) { - toast.error("다운로드할 계약이 없습니다."); - return; - } - - const contract = registration.basicContracts[contractIndex]; - if (!contract) { - toast.error("계약을 찾을 수 없습니다."); - return; - } - - if (contract.status !== "VENDOR_SIGNED" && contract.status !== "COMPLETED") { - toast.error("완료된 계약서만 다운로드할 수 있습니다."); - return; - } - - // 서명된 계약서 파일 정보 확인 - const filePath = contract.filePath; - const fileName = contract.fileName || `${contract.templateName || '기본계약'}_${registration.companyName}.docx`; - - if (!filePath) { - toast.error("계약서 파일을 찾을 수 없습니다."); - return; - } - - console.log(`📥 기본계약 다운로드 시작:`, { - filePath, - fileName, - templateName: contract.templateName - }); - - // downloadFile 함수를 사용하여 서명된 계약서 다운로드 - const { downloadFile } = await import('@/lib/file-download'); - const result = await downloadFile(filePath, fileName, { - showToast: true, - onError: (error: any) => { - console.error("기본계약 다운로드 오류:", error); - toast.error(`기본계약 다운로드 실패: ${error}`); - }, - onSuccess: (fileName: string, fileSize?: number) => { - console.log(`✅ 기본계약 다운로드 성공:`, { fileName, fileSize }); - } - }); - - if (!result.success) { - console.error("기본계약 다운로드 실패:", result.error); - } - } catch (error) { - console.error("기본계약 다운로드 중 오류 발생:", error); - toast.error("기본계약 다운로드 중 오류가 발생했습니다."); - } - }; - - return ( - - - - -
- - 문서/자료 접수 현황 - {registration.companyName} -
- {onRefresh && ( - - )} -
-
- -
- {/* 기본 정보 */} -
-
- 업체명: - {registration.companyName} -
-
- 사업자번호: - {registration.businessNumber} -
-
- 대표자: - {registration.representative || "-"} -
-
- 현재상태: - {registration.status} -
-
- - {/* 문서 제출 현황 */} -
-
-

문서 제출 현황

-
-
-
-
문서유형
-
상태
-
제출일자
-
액션
-
- {documentStatusColumns.map((doc) => { - const isSubmitted = registration.documentSubmissions?.[ - doc.key as keyof typeof registration.documentSubmissions - ] as boolean || false; - - // 내자인 경우 통장사본은 표시하지 않음 - const isForeign = registration.country !== 'KR'; - if (doc.key === 'bankCopy' && !isForeign) { - return null; - } - - return ( -
-
- - {doc.label} - {doc.key === 'bankCopy' && isForeign && ( - (외자 필수) - )} -
-
- -
-
- {isSubmitted ? "2024.01.01" : "-"} -
-
- {isSubmitted && ( - - )} -
-
- ); - })} -
-
- - {/* 계약 동의 현황 */} -
-
-

계약 동의 현황

-
-
-
-
계약유형
-
상태
-
서약일자
-
액션
-
- {!registration.basicContracts || registration.basicContracts.length === 0 ? ( -
- 요청된 기본계약이 없습니다. -
- ) : ( - registration.basicContracts.map((contract, index) => { - const isCompleted = contract.status === "VENDOR_SIGNED" || contract.status === "COMPLETED"; - - return ( -
-
- - {contract.templateName || "템플릿명 없음"} -
-
- -
-
- {isCompleted && contract.createdAt - ? new Intl.DateTimeFormat('ko-KR').format(new Date(contract.createdAt)) - : "-" - } -
-
- {isCompleted && ( - - )} -
-
- ); - }) - )} -
-
- - {/* 안전적격성 평가 */} -
-

안전적격성 평가

-
-
-
- - 안전적격성 평가 -
- -
- {registration.safetyQualificationContent && ( -
-

{registration.safetyQualificationContent}

-
- )} -
-
- - {/* 추가 정보 */} -
-

추가 정보

-
-
-
- - 추가 정보 등록 -
- -
-
-
-
-
-
- ); -} +"use client"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { FileText, Download, CheckCircle, XCircle, Clock, RefreshCw } from "lucide-react"; +import { toast } from "sonner"; + +import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig"; +import { + documentStatusColumns, +} from "@/config/vendorRegularRegistrationsColumnsConfig"; + +interface DocumentStatusDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + registration: VendorRegularRegistration | null; + onRefresh?: () => void; + isVendorUser?: boolean; +} + +const StatusIcon = ({ status }: { status: string | boolean }) => { + if (typeof status === "boolean") { + return status ? ( + + ) : ( + + ); + } + + switch (status) { + case "completed": + return ; + case "reviewing": + return ; + case "not_submitted": + default: + return ; + } +}; + +const StatusBadge = ({ status }: { status: string | boolean }) => { + if (typeof status === "boolean") { + return ( + + {status ? "제출완료" : "미제출"} + + ); + } + + const statusConfig = { + completed: { label: "완료", variant: "default" as const }, + reviewing: { label: "검토중", variant: "secondary" as const }, + not_submitted: { label: "미제출", variant: "destructive" as const }, + }; + + const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.not_submitted; + + return {config.label}; +}; + +export function DocumentStatusDialog({ + open, + onOpenChange, + registration, + onRefresh, + isVendorUser = false, +}: DocumentStatusDialogProps) { + if (!registration) return null; + + // 파일 다운로드 핸들러 + const handleFileDownload = async (docKey: string, fileIndex: number = 0) => { + try { + console.log(`🔍 파일 다운로드 시도:`, { + docKey, + fileIndex, + allDocumentFiles: registration.documentFiles, + registrationId: registration.id, + registrationKeys: Object.keys(registration), + fullRegistration: registration + }); + //isvendoruser인 경우는 실사 결과 파일 다운로드 불가능 + if (isVendorUser && docKey === "auditResult") { + toast.error("실사 결과 파일은 다운로드할 수 없습니다."); + return; + } + + // documentFiles가 없는 경우 처리 + if (!registration.documentFiles) { + console.error(`❌ documentFiles가 없음:`, { + registration, + hasDocumentFiles: !!registration.documentFiles, + registrationKeys: Object.keys(registration) + }); + toast.error("문서 파일 정보를 찾을 수 없습니다. 페이지를 새로고침 해주세요."); + return; + } + + const files = registration.documentFiles[docKey as keyof typeof registration.documentFiles]; + console.log(`📂 ${docKey} 파일 목록:`, files); + + if (!files || files.length === 0) { + console.warn(`❌ ${docKey}에 파일이 없음:`, { files, length: files?.length }); + toast.error("다운로드할 파일이 없습니다."); + return; + } + + const file = files[fileIndex]; + console.log(`📄 선택된 파일 (index ${fileIndex}):`, file); + + if (!file) { + console.warn(`❌ 파일 인덱스 ${fileIndex}에 파일이 없음`); + toast.error("파일을 찾을 수 없습니다."); + return; + } + + // 파일 객체의 모든 속성 확인 + console.log(`🔍 파일 객체 전체 속성:`, Object.keys(file)); + console.log(`🔍 파일 상세 정보:`, { + filePath: file.filePath, + path: file.path, + originalFileName: file.originalFileName, + fileName: file.fileName, + name: file.name, + fullObject: file + }); + + // filePath와 fileName 추출 + const filePath = file.filePath || file.path; + const fileName = file.originalFileName || file.fileName || file.name; + + console.log(`📝 추출된 파일 정보:`, { filePath, fileName }); + + if (!filePath || !fileName) { + console.error(`❌ 파일 정보 누락:`, { + filePath, + fileName, + fileObject: file, + availableKeys: Object.keys(file) + }); + toast.error("파일 정보가 올바르지 않습니다."); + return; + } + + console.log(`📥 파일 다운로드 시작:`, { filePath, fileName, docKey }); + + // downloadFile 함수를 동적으로 import하여 파일 다운로드 + const { downloadFile } = await import('@/lib/file-download'); + const result = await downloadFile(filePath, fileName, { + showToast: true, + onError: (error: any) => { + console.error("파일 다운로드 오류:", error); + toast.error(`파일 다운로드 실패: ${error}`); + }, + onSuccess: (fileName: string, fileSize?: number) => { + console.log(`✅ 파일 다운로드 성공:`, { fileName, fileSize }); + } + }); + + if (!result.success) { + console.error("파일 다운로드 실패:", result.error); + } + } catch (error) { + console.error("파일 다운로드 중 오류 발생:", error); + toast.error("파일 다운로드 중 오류가 발생했습니다."); + } + }; + + // 기본계약 파일 다운로드 핸들러 + const handleContractDownload = async (contractIndex: number) => { + try { + if (!registration.basicContracts || registration.basicContracts.length === 0) { + toast.error("다운로드할 계약이 없습니다."); + return; + } + + const contract = registration.basicContracts[contractIndex]; + if (!contract) { + toast.error("계약을 찾을 수 없습니다."); + return; + } + + if (contract.status !== "VENDOR_SIGNED" && contract.status !== "COMPLETED") { + toast.error("완료된 계약서만 다운로드할 수 있습니다."); + return; + } + + // 서명된 계약서 파일 정보 확인 + const filePath = contract.filePath; + const fileName = contract.fileName || `${contract.templateName || '기본계약'}_${registration.companyName}.docx`; + + if (!filePath) { + toast.error("계약서 파일을 찾을 수 없습니다."); + return; + } + + console.log(`📥 기본계약 다운로드 시작:`, { + filePath, + fileName, + templateName: contract.templateName + }); + + // downloadFile 함수를 사용하여 서명된 계약서 다운로드 + const { downloadFile } = await import('@/lib/file-download'); + const result = await downloadFile(filePath, fileName, { + showToast: true, + onError: (error: any) => { + console.error("기본계약 다운로드 오류:", error); + toast.error(`기본계약 다운로드 실패: ${error}`); + }, + onSuccess: (fileName: string, fileSize?: number) => { + console.log(`✅ 기본계약 다운로드 성공:`, { fileName, fileSize }); + } + }); + + if (!result.success) { + console.error("기본계약 다운로드 실패:", result.error); + } + } catch (error) { + console.error("기본계약 다운로드 중 오류 발생:", error); + toast.error("기본계약 다운로드 중 오류가 발생했습니다."); + } + }; + + return ( + + + + +
+ + 문서/자료 접수 현황 - {registration.companyName} +
+ {onRefresh && ( + + )} +
+
+ +
+ {/* 기본 정보 */} +
+
+ 업체명: + {registration.companyName} +
+
+ 사업자번호: + {registration.businessNumber} +
+
+ 대표자: + {registration.representative || "-"} +
+
+ 현재상태: + {registration.status} +
+
+ + {/* 문서 제출 현황 */} +
+
+

문서 제출 현황

+
+
+
+
문서유형
+
상태
+
제출일자
+
액션
+
+ {documentStatusColumns.map((doc) => { + const isSubmitted = registration.documentSubmissions?.[ + doc.key as keyof typeof registration.documentSubmissions + ] as boolean || false; + + // 내자인 경우 통장사본은 표시하지 않음 + const isForeign = registration.country !== 'KR'; + if (doc.key === 'bankCopy' && !isForeign) { + return null; + } + + return ( +
+
+ + {doc.label} + {doc.key === 'bankCopy' && isForeign && ( + (외자 필수) + )} +
+
+ +
+
+ {isSubmitted ? "2024.01.01" : "-"} +
+
+ {isSubmitted && ( + + )} +
+
+ ); + })} +
+
+ + {/* 계약 동의 현황 */} +
+
+

계약 동의 현황

+
+
+
+
계약유형
+
상태
+
서약일자
+
액션
+
+ {!registration.basicContracts || registration.basicContracts.length === 0 ? ( +
+ 요청된 기본계약이 없습니다. +
+ ) : ( + registration.basicContracts.map((contract, index) => { + const isCompleted = contract.status === "VENDOR_SIGNED" || contract.status === "COMPLETED"; + + return ( +
+
+ + {contract.templateName || "템플릿명 없음"} +
+
+ +
+
+ {isCompleted && contract.createdAt + ? new Intl.DateTimeFormat('ko-KR').format(new Date(contract.createdAt)) + : "-" + } +
+
+ {isCompleted && ( + + )} +
+
+ ); + }) + )} +
+
+ + {/* 안전적격성 평가 */} +
+

안전적격성 평가

+
+
+
+ + 안전적격성 평가 +
+ +
+ {registration.safetyQualificationContent && ( +
+

{registration.safetyQualificationContent}

+
+ )} +
+
+ + {/* 추가 정보 */} +
+

추가 정보

+
+
+
+ + 추가 정보 등록 +
+ +
+
+
+
+
+
+ ); +} diff --git a/components/vendor-regular-registrations/registration-request-dialog.tsx b/components/vendor-regular-registrations/registration-request-dialog.tsx index 99599ce5..d3aeb812 100644 --- a/components/vendor-regular-registrations/registration-request-dialog.tsx +++ b/components/vendor-regular-registrations/registration-request-dialog.tsx @@ -313,6 +313,16 @@ export function RegistrationRequestDialog({ return; } + // 업무담당자 검증 (최소 하나의 담당자라도 이름과 이메일이 있어야 함) + const hasValidBusinessContact = Object.values(formData.businessContacts).some(contact => + contact.name?.trim() && contact.email?.trim() + ); + + if (!hasValidBusinessContact) { + toast.error("업무담당자 정보를 최소 하나 이상 입력해주세요. (담당자명과 이메일 필수)"); + return; + } + if (onSubmit) { await onSubmit(formData); } @@ -599,7 +609,8 @@ export function RegistrationRequestDialog({ {/* 업무담당자 */}
-

업무담당자

+

업무담당자 *

+

최소 하나의 업무담당자 정보를 입력해주세요.

{Object.entries(formData.businessContacts).map(([type, contact]) => { const labels = { @@ -615,7 +626,7 @@ export function RegistrationRequestDialog({
{labels[type as keyof typeof labels]} 담당자
- + handleBusinessContactChange(type as keyof typeof formData.businessContacts, 'name', e.target.value)} @@ -646,7 +657,7 @@ export function RegistrationRequestDialog({ />
- +