summaryrefslogtreecommitdiff
path: root/components/vendor-regular-registrations
diff options
context:
space:
mode:
Diffstat (limited to 'components/vendor-regular-registrations')
-rw-r--r--components/vendor-regular-registrations/document-status-dialog.tsx852
-rw-r--r--components/vendor-regular-registrations/registration-request-dialog.tsx17
2 files changed, 440 insertions, 429 deletions
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 ? (
- <CheckCircle className="w-4 h-4 text-green-600" />
- ) : (
- <XCircle className="w-4 h-4 text-red-500" />
- );
- }
-
- switch (status) {
- case "completed":
- return <CheckCircle className="w-4 h-4 text-green-600" />;
- case "reviewing":
- return <Clock className="w-4 h-4 text-yellow-600" />;
- case "not_submitted":
- default:
- return <XCircle className="w-4 h-4 text-red-500" />;
- }
-};
-
-const StatusBadge = ({ status }: { status: string | boolean }) => {
- if (typeof status === "boolean") {
- return (
- <Badge variant={status ? "default" : "destructive"}>
- {status ? "제출완료" : "미제출"}
- </Badge>
- );
- }
-
- 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 <Badge variant={config.variant}>{config.label}</Badge>;
-};
-
-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 (
- <Dialog open={open} onOpenChange={onOpenChange}>
- <DialogContent className="max-w-4xl max-h-[80vh]">
- <DialogHeader className="sticky top-0 z-10 border-b pr-4 pb-4 mb-4">
- <DialogTitle className="flex items-center justify-between">
- <div className="flex items-center gap-2">
- <FileText className="w-5 h-5" />
- 문서/자료 접수 현황 - {registration.companyName}
- </div>
- {onRefresh && (
- <Button
- variant="outline"
- size="sm"
- onClick={onRefresh}
- className="flex items-center gap-2"
- >
- <RefreshCw className="w-4 h-4" />
- 새로고침
- </Button>
- )}
- </DialogTitle>
- </DialogHeader>
-
- <div className="overflow-y-auto max-h-[calc(80vh-120px)] space-y-6">
- {/* 기본 정보 */}
- <div className="grid grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg">
- <div>
- <span className="text-sm font-medium text-gray-600">업체명:</span>
- <span className="ml-2">{registration.companyName}</span>
- </div>
- <div>
- <span className="text-sm font-medium text-gray-600">사업자번호:</span>
- <span className="ml-2">{registration.businessNumber}</span>
- </div>
- <div>
- <span className="text-sm font-medium text-gray-600">대표자:</span>
- <span className="ml-2">{registration.representative || "-"}</span>
- </div>
- <div>
- <span className="text-sm font-medium text-gray-600">현재상태:</span>
- <Badge className="ml-2">{registration.status}</Badge>
- </div>
- </div>
-
- {/* 문서 제출 현황 */}
- <div>
- <div className="flex items-center justify-between mb-4">
- <h3 className="text-lg font-semibold">문서 제출 현황</h3>
- </div>
- <div className="border rounded-lg">
- <div className="grid grid-cols-4 gap-4 p-4 bg-gray-50 font-medium text-sm">
- <div>문서유형</div>
- <div>상태</div>
- <div>제출일자</div>
- <div>액션</div>
- </div>
- {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 (
- <div
- key={doc.key}
- className="grid grid-cols-4 gap-4 p-4 border-t items-center"
- >
- <div className="flex items-center gap-2">
- <StatusIcon status={isSubmitted} />
- {doc.label}
- {doc.key === 'bankCopy' && isForeign && (
- <span className="text-xs text-blue-600">(외자 필수)</span>
- )}
- </div>
- <div>
- <StatusBadge status={isSubmitted} />
- </div>
- <div className="text-sm text-gray-600">
- {isSubmitted ? "2024.01.01" : "-"}
- </div>
- <div>
- {isSubmitted && (
- <Button
- size="sm"
- variant="outline"
- onClick={() => handleFileDownload(doc.key)}
- >
- <Download className="w-4 h-4 mr-1" />
- 다운로드
- </Button>
- )}
- </div>
- </div>
- );
- })}
- </div>
- </div>
-
- {/* 계약 동의 현황 */}
- <div>
- <div className="flex items-center justify-between mb-4">
- <h3 className="text-lg font-semibold">계약 동의 현황</h3>
- </div>
- <div className="border rounded-lg">
- <div className="grid grid-cols-4 gap-4 p-4 bg-gray-50 font-medium text-sm">
- <div>계약유형</div>
- <div>상태</div>
- <div>서약일자</div>
- <div>액션</div>
- </div>
- {!registration.basicContracts || registration.basicContracts.length === 0 ? (
- <div className="p-4 border-t text-center text-gray-500">
- 요청된 기본계약이 없습니다.
- </div>
- ) : (
- registration.basicContracts.map((contract, index) => {
- const isCompleted = contract.status === "VENDOR_SIGNED" || contract.status === "COMPLETED";
-
- return (
- <div
- key={`${contract.templateId}-${index}`}
- className="grid grid-cols-4 gap-4 p-4 border-t items-center"
- >
- <div className="flex items-center gap-2">
- <StatusIcon status={isCompleted} />
- {contract.templateName || "템플릿명 없음"}
- </div>
- <div>
- <StatusBadge status={isCompleted} />
- </div>
- <div className="text-sm text-gray-600">
- {isCompleted && contract.createdAt
- ? new Intl.DateTimeFormat('ko-KR').format(new Date(contract.createdAt))
- : "-"
- }
- </div>
- <div>
- {isCompleted && (
- <Button
- size="sm"
- variant="outline"
- onClick={() => handleContractDownload(index)}
- >
- <Download className="w-4 h-4 mr-1" />
- 다운로드
- </Button>
- )}
- </div>
- </div>
- );
- })
- )}
- </div>
- </div>
-
- {/* 안전적격성 평가 */}
- <div>
- <h3 className="text-lg font-semibold mb-4">안전적격성 평가</h3>
- <div className="p-4 border rounded-lg">
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-2">
- <StatusIcon status={!!registration.safetyQualificationContent} />
- <span>안전적격성 평가</span>
- </div>
- <StatusBadge status={!!registration.safetyQualificationContent} />
- </div>
- {registration.safetyQualificationContent && (
- <div className="mt-3 p-3 bg-gray-50 rounded">
- <p className="text-sm">{registration.safetyQualificationContent}</p>
- </div>
- )}
- </div>
- </div>
-
- {/* 추가 정보 */}
- <div>
- <h3 className="text-lg font-semibold mb-4">추가 정보</h3>
- <div className="p-4 border rounded-lg">
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-2">
- <StatusIcon status={registration.additionalInfo} />
- <span>추가 정보 등록</span>
- </div>
- <StatusBadge status={registration.additionalInfo} />
- </div>
- </div>
- </div>
- </div>
- </DialogContent>
- </Dialog>
- );
-}
+"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 ? (
+ <CheckCircle className="w-4 h-4 text-green-600" />
+ ) : (
+ <XCircle className="w-4 h-4 text-red-500" />
+ );
+ }
+
+ switch (status) {
+ case "completed":
+ return <CheckCircle className="w-4 h-4 text-green-600" />;
+ case "reviewing":
+ return <Clock className="w-4 h-4 text-yellow-600" />;
+ case "not_submitted":
+ default:
+ return <XCircle className="w-4 h-4 text-red-500" />;
+ }
+};
+
+const StatusBadge = ({ status }: { status: string | boolean }) => {
+ if (typeof status === "boolean") {
+ return (
+ <Badge variant={status ? "default" : "destructive"}>
+ {status ? "제출완료" : "미제출"}
+ </Badge>
+ );
+ }
+
+ 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 <Badge variant={config.variant}>{config.label}</Badge>;
+};
+
+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 (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-4xl max-h-[80vh]">
+ <DialogHeader className="sticky top-0 z-10 border-b pr-4 pb-4 mb-4">
+ <DialogTitle className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <FileText className="w-5 h-5" />
+ 문서/자료 접수 현황 - {registration.companyName}
+ </div>
+ {onRefresh && (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={onRefresh}
+ className="flex items-center gap-2"
+ >
+ <RefreshCw className="w-4 h-4" />
+ 새로고침
+ </Button>
+ )}
+ </DialogTitle>
+ </DialogHeader>
+
+ <div className="overflow-y-auto max-h-[calc(80vh-120px)] space-y-6">
+ {/* 기본 정보 */}
+ <div className="grid grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg">
+ <div>
+ <span className="text-sm font-medium text-gray-600">업체명:</span>
+ <span className="ml-2">{registration.companyName}</span>
+ </div>
+ <div>
+ <span className="text-sm font-medium text-gray-600">사업자번호:</span>
+ <span className="ml-2">{registration.businessNumber}</span>
+ </div>
+ <div>
+ <span className="text-sm font-medium text-gray-600">대표자:</span>
+ <span className="ml-2">{registration.representative || "-"}</span>
+ </div>
+ <div>
+ <span className="text-sm font-medium text-gray-600">현재상태:</span>
+ <Badge className="ml-2">{registration.status}</Badge>
+ </div>
+ </div>
+
+ {/* 문서 제출 현황 */}
+ <div>
+ <div className="flex items-center justify-between mb-4">
+ <h3 className="text-lg font-semibold">문서 제출 현황</h3>
+ </div>
+ <div className="border rounded-lg">
+ <div className="grid grid-cols-4 gap-4 p-4 bg-gray-50 font-medium text-sm">
+ <div>문서유형</div>
+ <div>상태</div>
+ <div>제출일자</div>
+ <div>액션</div>
+ </div>
+ {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 (
+ <div
+ key={doc.key}
+ className="grid grid-cols-4 gap-4 p-4 border-t items-center"
+ >
+ <div className="flex items-center gap-2">
+ <StatusIcon status={isSubmitted} />
+ {doc.label}
+ {doc.key === 'bankCopy' && isForeign && (
+ <span className="text-xs text-blue-600">(외자 필수)</span>
+ )}
+ </div>
+ <div>
+ <StatusBadge status={isSubmitted} />
+ </div>
+ <div className="text-sm text-gray-600">
+ {isSubmitted ? "2024.01.01" : "-"}
+ </div>
+ <div>
+ {isSubmitted && (
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => handleFileDownload(doc.key)}
+ >
+ <Download className="w-4 h-4 mr-1" />
+ 다운로드
+ </Button>
+ )}
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </div>
+
+ {/* 계약 동의 현황 */}
+ <div>
+ <div className="flex items-center justify-between mb-4">
+ <h3 className="text-lg font-semibold">계약 동의 현황</h3>
+ </div>
+ <div className="border rounded-lg">
+ <div className="grid grid-cols-4 gap-4 p-4 bg-gray-50 font-medium text-sm">
+ <div>계약유형</div>
+ <div>상태</div>
+ <div>서약일자</div>
+ <div>액션</div>
+ </div>
+ {!registration.basicContracts || registration.basicContracts.length === 0 ? (
+ <div className="p-4 border-t text-center text-gray-500">
+ 요청된 기본계약이 없습니다.
+ </div>
+ ) : (
+ registration.basicContracts.map((contract, index) => {
+ const isCompleted = contract.status === "VENDOR_SIGNED" || contract.status === "COMPLETED";
+
+ return (
+ <div
+ key={`${contract.templateId}-${index}`}
+ className="grid grid-cols-4 gap-4 p-4 border-t items-center"
+ >
+ <div className="flex items-center gap-2">
+ <StatusIcon status={isCompleted} />
+ {contract.templateName || "템플릿명 없음"}
+ </div>
+ <div>
+ <StatusBadge status={isCompleted} />
+ </div>
+ <div className="text-sm text-gray-600">
+ {isCompleted && contract.createdAt
+ ? new Intl.DateTimeFormat('ko-KR').format(new Date(contract.createdAt))
+ : "-"
+ }
+ </div>
+ <div>
+ {isCompleted && (
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => handleContractDownload(index)}
+ >
+ <Download className="w-4 h-4 mr-1" />
+ 다운로드
+ </Button>
+ )}
+ </div>
+ </div>
+ );
+ })
+ )}
+ </div>
+ </div>
+
+ {/* 안전적격성 평가 */}
+ <div>
+ <h3 className="text-lg font-semibold mb-4">안전적격성 평가</h3>
+ <div className="p-4 border rounded-lg">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <StatusIcon status={!!registration.safetyQualificationContent} />
+ <span>안전적격성 평가</span>
+ </div>
+ <StatusBadge status={!!registration.safetyQualificationContent} />
+ </div>
+ {registration.safetyQualificationContent && (
+ <div className="mt-3 p-3 bg-gray-50 rounded">
+ <p className="text-sm">{registration.safetyQualificationContent}</p>
+ </div>
+ )}
+ </div>
+ </div>
+
+ {/* 추가 정보 */}
+ <div>
+ <h3 className="text-lg font-semibold mb-4">추가 정보</h3>
+ <div className="p-4 border rounded-lg">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <StatusIcon status={registration.additionalInfo} />
+ <span>추가 정보 등록</span>
+ </div>
+ <StatusBadge status={registration.additionalInfo} />
+ </div>
+ </div>
+ </div>
+ </div>
+ </DialogContent>
+ </Dialog>
+ );
+}
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({
{/* 업무담당자 */}
<div>
- <h4 className="font-semibold mb-3">업무담당자</h4>
+ <h4 className="font-semibold mb-3">업무담당자 <span className="text-red-500">*</span></h4>
+ <p className="text-sm text-muted-foreground mb-4">최소 하나의 업무담당자 정보를 입력해주세요.</p>
<div className="space-y-4">
{Object.entries(formData.businessContacts).map(([type, contact]) => {
const labels = {
@@ -615,7 +626,7 @@ export function RegistrationRequestDialog({
<h5 className="font-medium text-sm">{labels[type as keyof typeof labels]} 담당자</h5>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
- <Label>담당자명</Label>
+ <Label>담당자명 <span className="text-red-500">*</span></Label>
<Input
value={contact.name}
onChange={(e) => handleBusinessContactChange(type as keyof typeof formData.businessContacts, 'name', e.target.value)}
@@ -646,7 +657,7 @@ export function RegistrationRequestDialog({
/>
</div>
<div>
- <Label>이메일</Label>
+ <Label>이메일 <span className="text-red-500">*</span></Label>
<Input
type="email"
value={contact.email}