From a1710296dbc1881a7ed86093872904a529901430 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Tue, 4 Nov 2025 17:30:27 +0900 Subject: (김준회) RFQ 테이블 POS 다운로드할 수 있도록 변경, 벤더측은 다운로드시 암호화 해제 추가, item dialog 통일처리 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/rfq-last/shared/rfq-items-dialog.tsx | 489 +++++++++++++++++++++++++++++++ 1 file changed, 489 insertions(+) create mode 100644 lib/rfq-last/shared/rfq-items-dialog.tsx (limited to 'lib/rfq-last/shared/rfq-items-dialog.tsx') diff --git a/lib/rfq-last/shared/rfq-items-dialog.tsx b/lib/rfq-last/shared/rfq-items-dialog.tsx new file mode 100644 index 00000000..c25670fc --- /dev/null +++ b/lib/rfq-last/shared/rfq-items-dialog.tsx @@ -0,0 +1,489 @@ +"use client" + +import * as React from "react" +import { format } from "date-fns" +import { Package, ExternalLink, Download, FileText } from "lucide-react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Skeleton } from "@/components/ui/skeleton" +import { Separator } from "@/components/ui/separator" +import { toast } from "sonner" +import { RfqsLastView, VendorQuotationView } from "@/db/schema" +import { getRfqItemsAction } from "../service" +import { getDownloadUrlByMaterialCode, checkPosFileExists } from "@/lib/pos" +import { PosFileSelectionDialog } from "@/lib/pos/components/pos-file-selection-dialog" + +// 품목 타입 +interface RfqItem { + id: number + rfqsLastId: number | null + rfqItem: string | null + prItem: string | null + prNo: string | null + materialCode: string | null + materialCategory: string | null + acc: string | null + materialDescription: string | null + size: string | null + deliveryDate: Date | null + quantity: number | null + uom: string | null + grossWeight: number | null + gwUom: string | null + specNo: string | null + specUrl: string | null + trackingNo: string | null + majorYn: boolean | null + remark: string | null + projectDef: string | null + projectSc: string | null + projectKl: string | null + projectLc: string | null + projectDl: string | null + // RFQ 관련 정보 + rfqCode: string | null + rfqType: string | null + rfqTitle: string | null + itemCode: string | null + itemName: string | null + projectCode: string | null + projectName: string | null +} + +interface ItemStatistics { + total: number + major: number + regular: number + totalQuantity: number + totalWeight: number +} + +interface RfqItemsDialogProps { + isOpen: boolean + onClose: () => void + rfqData: RfqsLastView | VendorQuotationView + /** + * 뷰어 타입 + * - 'evcp': EVCP 사용자 (암호화된 파일 직접 다운로드) + * - 'partners': 협력사 사용자 (복호화된 파일 다운로드) + */ + viewerType?: 'evcp' | 'partners' +} + +export function RfqItemsDialog({ + isOpen, + onClose, + rfqData, + viewerType = 'evcp' +}: RfqItemsDialogProps) { + const [items, setItems] = React.useState([]) + const [statistics, setStatistics] = React.useState(null) + const [isLoading, setIsLoading] = React.useState(false) + // POS 파일 선택 다이얼로그 상태 + const [posDialogOpen, setPosDialogOpen] = React.useState(false) + const [selectedMaterialCode, setSelectedMaterialCode] = React.useState("") + const [posFiles, setPosFiles] = React.useState>([]) + const [loadingPosFiles, setLoadingPosFiles] = React.useState(false) + const [downloadingFileIndex, setDownloadingFileIndex] = React.useState(null) + + // 품목 목록 로드 + React.useEffect(() => { + if (!isOpen || !rfqData.id) return + + const loadData = async () => { + setIsLoading(true) + + try { + // 품목 목록 로드 + const itemsResult = await getRfqItemsAction(rfqData.id) + + if (itemsResult.success) { + setItems(itemsResult.data) + setStatistics(itemsResult.statistics || null) + } else { + toast.error(itemsResult.error || "품목을 불러오는데 실패했습니다") + setItems([]) + setStatistics(null) + } + + } catch (error) { + console.error("데이터 로드 오류:", error) + toast.error("데이터를 불러오는데 실패했습니다") + setItems([]) + setStatistics(null) + } finally { + setIsLoading(false) + } + } + + loadData() + }, [isOpen, rfqData.id]) + + // 사양서 링크 열기 + const handleOpenSpec = (specUrl: string) => { + window.open(specUrl, '_blank', 'noopener,noreferrer') + } + + // POS 파일 목록 조회 및 다이얼로그 열기 + const handleOpenPosDialog = async (materialCode: string) => { + if (!materialCode) { + toast.error("자재코드가 없습니다") + return + } + + setLoadingPosFiles(true) + setSelectedMaterialCode(materialCode) + + try { + toast.loading(`POS 파일 목록 조회 중... (${materialCode})`, { id: `pos-check-${materialCode}` }) + + const result = await checkPosFileExists(materialCode) + + if (result.exists && result.files && result.files.length > 0) { + // 파일 정보를 상세하게 가져오기 위해 getDownloadUrlByMaterialCode 사용 + const detailResult = await getDownloadUrlByMaterialCode(materialCode) + + if (detailResult.success && detailResult.availableFiles) { + setPosFiles(detailResult.availableFiles) + setPosDialogOpen(true) + toast.success(`${result.fileCount}개의 POS 파일을 찾았습니다`, { id: `pos-check-${materialCode}` }) + } else { + toast.error('POS 파일 정보를 가져올 수 없습니다', { id: `pos-check-${materialCode}` }) + } + } else { + toast.error(result.error || 'POS 파일을 찾을 수 없습니다', { id: `pos-check-${materialCode}` }) + } + } catch (error) { + console.error("POS 파일 조회 오류:", error) + toast.error("POS 파일 조회에 실패했습니다", { id: `pos-check-${materialCode}` }) + } finally { + setLoadingPosFiles(false) + } + } + + // POS 파일 다운로드 실행 + const handleDownloadPosFile = async (fileIndex: number, fileName: string) => { + if (!selectedMaterialCode) return + + setDownloadingFileIndex(fileIndex) + + try { + toast.loading(`POS 파일 다운로드 준비 중...`, { id: `download-${fileIndex}` }) + + // viewerType에 따라 다른 엔드포인트 사용 + const endpoint = viewerType === 'partners' + ? `/api/pos/download-on-demand-partners` // 복호화 포함 + : `/api/pos/download-on-demand` // 암호화 파일 그대로 + + const downloadUrl = `${endpoint}?materialCode=${encodeURIComponent(selectedMaterialCode)}&fileIndex=${fileIndex}` + + toast.success(`POS 파일 다운로드 시작: ${fileName}`, { id: `download-${fileIndex}` }) + window.open(downloadUrl, '_blank', 'noopener,noreferrer') + + // 다운로드 시작 후 잠시 대기 후 상태 초기화 + setTimeout(() => { + setDownloadingFileIndex(null) + }, 1000) + } catch (error) { + console.error("POS 파일 다운로드 오류:", error) + toast.error("POS 파일 다운로드에 실패했습니다", { id: `download-${fileIndex}` }) + setDownloadingFileIndex(null) + } + } + + // POS 다이얼로그 닫기 + const handleClosePosDialog = () => { + setPosDialogOpen(false) + setSelectedMaterialCode("") + setPosFiles([]) + setDownloadingFileIndex(null) + } + + + return ( + + + + 견적 품목 목록 + + {rfqData.rfqCode} - {rfqData.rfqTitle || rfqData.itemName || "품목 정보"} + + + + {/* 통계 정보 */} + {statistics && !isLoading && ( + <> +
+
+
{statistics.total}
+
전체 품목
+
+
+
{statistics.regular}
+
일반 품목
+
+
+
{statistics.totalQuantity.toLocaleString()}
+
총 수량
+
+
+
{statistics.totalWeight.toLocaleString()}
+
총 중량 (KG)
+
+
+ + + )} + + + {isLoading ? ( + + + + 아이템 + 자재코드 + 자재명 + 수량 + 수량단위 + 중량 + 중량단위 + 납기일 + PR번호 + 사양/설계문서 + 비고 + + + + {[...Array(3)].map((_, i) => ( + + + + + + + + + + + + + + ))} + +
+ ) : items.length === 0 ? ( +
+ +

품목이 없습니다.

+
+ ) : ( + + + + 아이템 + 자재코드 + 자재명 + 수량 + 수량단위 + 중량 + 중량단위 + 납기일 + PR번호 + PR 아이템 번호 + 사양/설계문서 + 프로젝트 + 비고 + + + + {items.map((item, index) => ( + + +
+ #{index + 1} + {item.majorYn && ( + + 주요 + + )} +
+
+ +
+ {item.materialCode || "-"} + {item.acc && ( + + ACC: {item.acc} + + )} +
+
+ +
+ + {item.materialDescription || "-"} + + {item.materialCategory && ( + + {item.materialCategory} + + )} + {item.size && ( + + 크기: {item.size} + + )} +
+
+ + + {item.quantity ? item.quantity.toLocaleString() : "-"} + + + + + {item.uom || "-"} + + + + + {item.grossWeight ? item.grossWeight.toLocaleString() : "-"} + + + + + {item.gwUom || "-"} + + + + + {item.deliveryDate ? format(new Date(item.deliveryDate), "yyyy-MM-dd") : "-"} + + + + {item.prNo || "-"} + + + {item.prItem || "-"} + + +
+ {/* 기존 스펙 정보 */} +
+ {item.specNo && ( + {item.specNo} + )} + {item.specUrl && ( + + )} +
+ + {/* POS 파일 다운로드 */} + {item.materialCode && ( +
+ + +
+ )} + + {/* 트래킹 번호 */} + {item.trackingNo && ( +
+ TRK: {item.trackingNo} +
+ )} +
+
+ +
+ {[ + item.projectDef && `${item.projectDef}`, + item.projectSc && `SC: ${item.projectSc}`, + item.projectKl && `KL: ${item.projectKl}`, + item.projectLc && `LC: ${item.projectLc}`, + item.projectDl && `DL: ${item.projectDl}` + ].filter(Boolean).join(" | ") || "-"} +
+
+ + + {item.remark ? (item.remark.length > 30 ? `${item.remark.slice(0, 30)}...` : item.remark) : "-"} + + +
+ ))} +
+
+ )} +
+ + {/* 하단 통계 정보 */} + {statistics && !isLoading && ( +
+
+ + 총 {statistics.total}개 품목 + + + 전체 수량: {statistics.totalQuantity.toLocaleString()} | + 전체 중량: {statistics.totalWeight.toLocaleString()} KG + +
+
+ )} + + {/* POS 파일 선택 다이얼로그 */} + +
+
+ ) +} + -- cgit v1.2.3