From 1e6d30c9f649dcaa0c1d24561af35d7a77fd51b2 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 18 Nov 2025 10:31:47 +0000 Subject: (최겸) 구매 법무검토 조회 dialog 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/legal/sslvw-pur-inq-req-dialog.tsx | 333 +++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 components/common/legal/sslvw-pur-inq-req-dialog.tsx (limited to 'components/common') diff --git a/components/common/legal/sslvw-pur-inq-req-dialog.tsx b/components/common/legal/sslvw-pur-inq-req-dialog.tsx new file mode 100644 index 00000000..438b6582 --- /dev/null +++ b/components/common/legal/sslvw-pur-inq-req-dialog.tsx @@ -0,0 +1,333 @@ +"use client" + +import * as React from "react" +import { Loader, Database, Check } from "lucide-react" +import { toast } from "sonner" +import { + useReactTable, + getCoreRowModel, + getPaginationRowModel, + getFilteredRowModel, + ColumnDef, + flexRender, +} from "@tanstack/react-table" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Checkbox } from "@/components/ui/checkbox" +import { ScrollArea } from "@/components/ui/scroll-area" + +import { getSSLVWPurInqReqData } from "@/lib/basic-contract/sslvw-service" +import { SSLVWPurInqReq } from "@/lib/basic-contract/sslvw-service" + +interface SSLVWPurInqReqDialogProps { + onConfirm?: (selectedRows: SSLVWPurInqReq[]) => void +} + +export function SSLVWPurInqReqDialog({ onConfirm }: SSLVWPurInqReqDialogProps) { + const [open, setOpen] = React.useState(false) + const [isLoading, setIsLoading] = React.useState(false) + const [data, setData] = React.useState([]) + const [error, setError] = React.useState(null) + const [rowSelection, setRowSelection] = React.useState>({}) + + const loadData = async () => { + setIsLoading(true) + setError(null) + try { + const result = await getSSLVWPurInqReqData() + if (result.success) { + setData(result.data) + if (result.isUsingFallback) { + toast.info("테스트 데이터를 표시합니다.") + } + } else { + setError(result.error || "데이터 로딩 실패") + toast.error(result.error || "데이터 로딩 실패") + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "알 수 없는 오류" + setError(errorMessage) + toast.error(errorMessage) + } finally { + setIsLoading(false) + } + } + + React.useEffect(() => { + if (open) { + loadData() + } else { + // 다이얼로그 닫힐 때 데이터 초기화 + setData([]) + setError(null) + setRowSelection({}) + } + }, [open]) + + // 테이블 컬럼 정의 (동적 생성) + const columns = React.useMemo[]>(() => { + if (data.length === 0) return [] + + const dataKeys = Object.keys(data[0]) + + return [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="모든 행 선택" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="행 선택" + /> + ), + enableSorting: false, + enableHiding: false, + }, + ...dataKeys.map((key) => ({ + accessorKey: key, + header: key, + cell: ({ getValue }: any) => { + const value = getValue() + return value !== null && value !== undefined ? String(value) : "" + }, + })), + ] + }, [data]) + + // 테이블 인스턴스 생성 + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onRowSelectionChange: setRowSelection, + state: { + rowSelection, + }, + }) + + // 선택된 행들 가져오기 + const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original) + + // 확인 버튼 핸들러 + const handleConfirm = () => { + if (selectedRows.length === 0) { + toast.error("행을 선택해주세요.") + return + } + + if (onConfirm) { + onConfirm(selectedRows) + toast.success(`${selectedRows.length}개의 행을 선택했습니다.`) + } else { + // 임시로 선택된 데이터 콘솔 출력 + console.log("선택된 행들:", selectedRows) + toast.success(`${selectedRows.length}개의 행이 선택되었습니다. (콘솔 확인)`) + } + + setOpen(false) + } + + return ( + + + + + + + 법무검토 요청 데이터 + + 법무검토 요청 데이터를 조회합니다. + {data.length > 0 && ` (${data.length}건, ${selectedRows.length}개 선택됨)`} + + + +
+ {isLoading ? ( +
+ + 데이터 로딩 중... +
+ ) : error ? ( +
+ 오류: {error} +
+ ) : data.length === 0 ? ( +
+ 데이터가 없습니다. +
+ ) : ( + <> + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + 데이터가 없습니다. + + + )} + +
+
+ + {/* 페이지네이션 컨트롤 */} +
+
+ {table.getFilteredSelectedRowModel().rows.length}개 행 선택됨 +
+
+
+

페이지당 행 수

+ +
+
+ {table.getState().pagination.pageIndex + 1} /{" "} + {table.getPageCount()} +
+
+ + + + +
+
+
+ + )} +
+ + + + {/* */} + + +
+
+ ) +} -- cgit v1.2.3 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 --- .../procurement-item-selector-dialog-single.tsx | 6 +- components/login/login-form.tsx | 19 +- components/login/partner-auth-form.tsx | 5 +- components/pq-input/pq-input-tabs.tsx | 136 +++- .../document-status-dialog.tsx | 852 ++++++++++----------- .../registration-request-dialog.tsx | 17 +- db/schema/bidding.ts | 1 + lib/rfq-last/shared/rfq-items-dialog.tsx | 4 +- .../editor/vendor-response-editor.tsx | 2 +- lib/rfq-last/vendor/rfq-vendor-table.tsx | 10 +- lib/vendor-regular-registrations/repository.ts | 614 +++++++-------- 11 files changed, 880 insertions(+), 786 deletions(-) (limited to 'components/common') 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 && ( -
- {t("agreement")}{" "} - - {t("privacyPolicy")} - -
+ // 1118 구매 파워유저 요구사항에 따라 삭제 + //
+ // {t("agreement")}{" "} + // + // {t("privacyPolicy")} + // + //
)} 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

- {t("agreement")}{" "} + {/* 1118 구매 파워유저 요구사항에 따라 삭제 */} + {/* {t("agreement")}{" "} {t("privacyPolicy")} - + */} {/* {t("privacyAgreement")}. */}

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({ {/* Top Controls - Sticky Header */}
+ {/* Item Count Display */} +
+
+ 총 항목: + + {totalItems} + +
+
+ + Saved: + + {savedItems} + +
+
+ + Not Saved: + + {notSavedItems} + +
+
+ {/* Filter Controls */}
필터: @@ -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({
- {data.map((group) => ( - -
- {/* Mobile: truncated version */} - - {group.groupName.length > 5 - ? group.groupName.slice(0, 5) + "..." - : group.groupName} - - {/* Desktop: full text */} - {group.groupName} - - {group.items.length} - -
-
- ))} + {data.map((group) => { + const colorClasses = getTabColorClasses(group.groupName) + return ( + +
+ {/* Mobile: truncated version */} + + {group.groupName.length > 5 + ? group.groupName.slice(0, 5) + "..." + : group.groupName} + + {/* Desktop: full text */} + {group.groupName} + + {group.items.length} + +
+
+ ) + })}
@@ -849,13 +931,13 @@ export function PQInputTabs({ {/* Save Status & Button */}
{!isSaved && canSave && ( - + Not Saved )} {isSaved && ( - + Saved 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({ />
- +
#{index + 1} - {item.majorYn && ( + {/* {item.majorYn && ( 주요 - )} + )} */}
diff --git a/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx b/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx index abd2b516..8c70b8dd 100644 --- a/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx +++ b/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx @@ -322,7 +322,7 @@ export default function VendorResponseEditor({ if (errors.quotationItems) { toast.error("견적 품목 정보를 확인해주세요. 모든 품목의 단가와 총액을 입력해야 합니다.") } else { - toast.error("입력 정보를 확인해주세요.") + toast.error("기본계약 또는 상업조건 정보를 확인해주세요.") } } } diff --git a/lib/rfq-last/vendor/rfq-vendor-table.tsx b/lib/rfq-last/vendor/rfq-vendor-table.tsx index 2ee2cb73..577ae492 100644 --- a/lib/rfq-last/vendor/rfq-vendor-table.tsx +++ b/lib/rfq-last/vendor/rfq-vendor-table.tsx @@ -1539,10 +1539,10 @@ export function RfqVendorTable({ )} {/* 기본계약 수정 메뉴 추가 */} - handleAction("edit-contract", vendor)}> + {/* handleAction("edit-contract", vendor)}> 기본계약 수정 - + */} {emailSentAt && ( <> @@ -1824,9 +1824,6 @@ export function RfqVendorTable({ 벤더 추가 - - {selectedRows.length > 0 && ( - <> {/* 정보 일괄 입력 버튼 - 취소되지 않은 벤더만 */} - + {selectedRows.length > 0 && ( + <> {/* RFQ 발송 버튼 - 취소되지 않은 벤더만 */}