diff options
Diffstat (limited to 'components')
6 files changed, 565 insertions, 470 deletions
diff --git a/components/common/selectors/procurement-item/procurement-item-selector-dialog-single.tsx b/components/common/selectors/procurement-item/procurement-item-selector-dialog-single.tsx index dab65780..90d4975b 100644 --- a/components/common/selectors/procurement-item/procurement-item-selector-dialog-single.tsx +++ b/components/common/selectors/procurement-item/procurement-item-selector-dialog-single.tsx @@ -70,13 +70,13 @@ export interface ProcurementItemSelectorDialogSingleProps { * ``` */ export function ProcurementItemSelectorDialogSingle({ - triggerLabel = "품목 선택", + triggerLabel = "1회성 품목 선택", triggerVariant = "outline", triggerSize = "default", selectedProcurementItem = null, onProcurementItemSelect, - title = "품목 선택", - description = "품목을 검색하고 선택해주세요.", + title = "1회성 품목 선택", + description = "1회성 품목을 검색하고 선택해주세요.", showConfirmButtons = false, }: ProcurementItemSelectorDialogSingleProps) { const [open, setOpen] = useState(false); diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx index 5fe6ab51..ee84add2 100644 --- a/components/login/login-form.tsx +++ b/components/login/login-form.tsx @@ -950,15 +950,16 @@ export function LoginForm() { {/* Terms - MFA 화면에서는 숨김 */} {!showMfaForm && ( - <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary"> - {t("agreement")}{" "} - <Link - href={`/${lng}/privacy`} - className="underline underline-offset-4 hover:text-primary" - > - {t("privacyPolicy")} - </Link> - </div> + // 1118 구매 파워유저 요구사항에 따라 삭제 + // <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary"> + // {t("agreement")}{" "} + // <Link + // href={`/${lng}/privacy`} + // className="underline underline-offset-4 hover:text-primary" + // > + // {t("privacyPolicy")} + // </Link> + // </div> )} </div> </div> diff --git a/components/login/partner-auth-form.tsx b/components/login/partner-auth-form.tsx index 22917997..ebd2219c 100644 --- a/components/login/partner-auth-form.tsx +++ b/components/login/partner-auth-form.tsx @@ -300,13 +300,14 @@ export function CompanyAuthForm({ className, ...props }: React.HTMLAttributes<HT </form> </div> <p className="px-8 text-center text-sm text-muted-foreground"> - {t("agreement")}{" "} + {/* 1118 구매 파워유저 요구사항에 따라 삭제 */} + {/* {t("agreement")}{" "} <Link href={`/${lng}/privacy`} // 개인정보처리방침만 남김 className="underline underline-offset-4 hover:text-primary" > {t("privacyPolicy")} - </Link> + </Link> */} {/* {t("privacyAgreement")}. */} </p> </div> diff --git a/components/pq-input/pq-input-tabs.tsx b/components/pq-input/pq-input-tabs.tsx index f0d44d04..df911d5e 100644 --- a/components/pq-input/pq-input-tabs.tsx +++ b/components/pq-input/pq-input-tabs.tsx @@ -276,6 +276,55 @@ export function PQInputTabs({ setAllSaved(allItemsSaved) }, [form.watch()]) + // ---------------------------------------------------------------------- + // C-1) Calculate item counts for display + // ---------------------------------------------------------------------- + + // ---------------------------------------------------------------------- + // C-2) Tab color mapping for better visual distinction + // ---------------------------------------------------------------------- + const getTabColorClasses = (groupName: string) => { + switch (groupName.toLowerCase()) { + case 'general': + return { + tab: 'data-[state=active]:bg-blue-50 data-[state=active]:text-blue-700 data-[state=active]:border-blue-200', + badge: 'bg-blue-100 text-blue-800 border-blue-200' + } + case 'hsg': + return { + tab: 'data-[state=active]:bg-green-50 data-[state=active]:text-green-700 data-[state=active]:border-green-200', + badge: 'bg-green-100 text-green-800 border-green-200' + } + case 'qms': + return { + tab: 'data-[state=active]:bg-orange-50 data-[state=active]:text-orange-700 data-[state=active]:border-orange-200', + badge: 'bg-orange-100 text-orange-800 border-orange-200' + } + case 'warranty': + return { + tab: 'data-[state=active]:bg-red-50 data-[state=active]:text-red-700 data-[state=active]:border-red-200', + badge: 'bg-red-100 text-red-800 border-red-200' + } + default: + return { + tab: 'data-[state=active]:bg-gray-50 data-[state=active]:text-gray-700 data-[state=active]:border-gray-200', + badge: 'bg-gray-100 text-gray-800 border-gray-200' + } + } + } + const getItemCounts = () => { + const values = form.getValues() + const totalItems = values.answers.length + const savedItems = values.answers.filter( + (answer) => answer.saved && (answer.answer || answer.uploadedFiles.length > 0) + ).length + const notSavedItems = totalItems - savedItems + + return { totalItems, savedItems, notSavedItems } + } + + const { totalItems, savedItems, notSavedItems } = getItemCounts() + // Helper to find the array index by criteriaId const getAnswerIndex = (criteriaId: number): number => { return form.getValues().answers.findIndex((a) => a.criteriaId === criteriaId) @@ -677,6 +726,30 @@ export function PQInputTabs({ <Tabs defaultValue={data[0]?.groupName || ""} className="w-full"> {/* Top Controls - Sticky Header */} <div className="sticky top-0 z-10 bg-background border-b border-border mb-4 pb-4"> + {/* Item Count Display */} + <div className="mb-3 flex items-center gap-6 text-sm"> + <div className="flex items-center gap-4"> + <span className="font-medium">총 항목:</span> + <Badge variant="outline" className="text-xs"> + {totalItems} + </Badge> + </div> + <div className="flex items-center gap-2"> + <CheckCircle2 className="h-4 w-4 text-green-600" /> + <span className="text-green-600 font-medium">Saved:</span> + <Badge variant="outline" className="bg-green-50 text-green-700 border-green-200 text-xs"> + {savedItems} + </Badge> + </div> + <div className="flex items-center gap-2"> + <AlertTriangle className="h-4 w-4 text-amber-600" /> + <span className="text-amber-600 font-medium">Not Saved:</span> + <Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-200 text-xs"> + {notSavedItems} + </Badge> + </div> + </div> + {/* Filter Controls */} <div className="mb-3 flex items-center gap-4"> <span className="text-sm font-medium">필터:</span> @@ -702,8 +775,11 @@ export function PQInputTabs({ checked={filterOptions.showSaved} onCheckedChange={(checked) => { const newOptions = { ...filterOptions, showSaved: !!checked }; - if (!checked && !filterOptions.showAll && !filterOptions.showNotSaved) { - // 최소 하나는 체크되어 있어야 함 + // Save 항목이나 Not Save 항목을 선택하면 전체 항목 자동 해제 + if (checked) { + newOptions.showAll = false; + } else if (!filterOptions.showNotSaved && !filterOptions.showAll) { + // 최소 하나는 체크되어 있어야 함 - 모두 해제되면 전체 항목 체크 newOptions.showAll = true; } setFilterOptions(newOptions); @@ -717,8 +793,11 @@ export function PQInputTabs({ checked={filterOptions.showNotSaved} onCheckedChange={(checked) => { const newOptions = { ...filterOptions, showNotSaved: !!checked }; - if (!checked && !filterOptions.showAll && !filterOptions.showSaved) { - // 최소 하나는 체크되어 있어야 함 + // Save 항목이나 Not Save 항목을 선택하면 전체 항목 자동 해제 + if (checked) { + newOptions.showAll = false; + } else if (!filterOptions.showSaved && !filterOptions.showAll) { + // 최소 하나는 체크되어 있어야 함 - 모두 해제되면 전체 항목 체크 newOptions.showAll = true; } setFilterOptions(newOptions); @@ -731,27 +810,30 @@ export function PQInputTabs({ <div className="flex justify-between items-center"> <TabsList className="grid grid-cols-4"> - {data.map((group) => ( - <TabsTrigger - key={group.groupName} - value={group.groupName} - className="truncate" - > - <div className="flex items-center gap-2"> - {/* Mobile: truncated version */} - <span className="block sm:hidden"> - {group.groupName.length > 5 - ? group.groupName.slice(0, 5) + "..." - : group.groupName} - </span> - {/* Desktop: full text */} - <span className="hidden sm:block">{group.groupName}</span> - <span className="inline-flex items-center justify-center h-5 min-w-5 px-1 rounded-full bg-muted text-xs font-medium"> - {group.items.length} - </span> - </div> - </TabsTrigger> - ))} + {data.map((group) => { + const colorClasses = getTabColorClasses(group.groupName) + return ( + <TabsTrigger + key={group.groupName} + value={group.groupName} + className={`truncate ${colorClasses.tab}`} + > + <div className="flex items-center gap-2"> + {/* Mobile: truncated version */} + <span className="block sm:hidden"> + {group.groupName.length > 5 + ? group.groupName.slice(0, 5) + "..." + : group.groupName} + </span> + {/* Desktop: full text */} + <span className="hidden sm:block">{group.groupName}</span> + <span className={`inline-flex items-center justify-center h-5 min-w-5 px-1 rounded-full text-xs font-medium ${colorClasses.badge}`}> + {group.items.length} + </span> + </div> + </TabsTrigger> + ) + })} </TabsList> <div className="flex gap-2"> @@ -849,13 +931,13 @@ export function PQInputTabs({ {/* Save Status & Button */} <div className="flex items-center gap-2"> {!isSaved && canSave && ( - <span className="text-amber-600 text-xs flex items-center"> + <span className="text-amber-600 text-sm font-medium flex items-center"> <AlertTriangle className="h-4 w-4 mr-1" /> Not Saved </span> )} {isSaved && ( - <span className="text-green-600 text-xs flex items-center"> + <span className="text-green-600 text-sm font-medium flex items-center"> <CheckCircle2 className="h-4 w-4 mr-1" /> Saved </span> 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} |
