summaryrefslogtreecommitdiff
path: root/lib/rfq-last/shared/rfq-items-dialog.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-04 17:30:27 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-04 17:30:27 +0900
commita1710296dbc1881a7ed86093872904a529901430 (patch)
tree62e4be5dabba4bf7c337769f391d11ce59154cc4 /lib/rfq-last/shared/rfq-items-dialog.tsx
parent680da9b323db8b8d7cf27c674ab0016ec87bfe81 (diff)
(김준회) RFQ 테이블 POS 다운로드할 수 있도록 변경, 벤더측은 다운로드시 암호화 해제 추가, item dialog 통일처리
Diffstat (limited to 'lib/rfq-last/shared/rfq-items-dialog.tsx')
-rw-r--r--lib/rfq-last/shared/rfq-items-dialog.tsx489
1 files changed, 489 insertions, 0 deletions
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<RfqItem[]>([])
+ const [statistics, setStatistics] = React.useState<ItemStatistics | null>(null)
+ const [isLoading, setIsLoading] = React.useState(false)
+ // POS 파일 선택 다이얼로그 상태
+ const [posDialogOpen, setPosDialogOpen] = React.useState(false)
+ const [selectedMaterialCode, setSelectedMaterialCode] = React.useState<string>("")
+ const [posFiles, setPosFiles] = React.useState<Array<{
+ fileName: string
+ dcmtmId: string
+ projNo: string
+ posNo: string
+ posRevNo: string
+ fileSer: string
+ }>>([])
+ const [loadingPosFiles, setLoadingPosFiles] = React.useState(false)
+ const [downloadingFileIndex, setDownloadingFileIndex] = React.useState<number | null>(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 (
+ <Dialog open={isOpen} onOpenChange={onClose}>
+ <DialogContent className="max-w-7xl h-[90vh] flex flex-col">
+ <DialogHeader>
+ <DialogTitle>견적 품목 목록</DialogTitle>
+ <DialogDescription>
+ {rfqData.rfqCode} - {rfqData.rfqTitle || rfqData.itemName || "품목 정보"}
+ </DialogDescription>
+ </DialogHeader>
+
+ {/* 통계 정보 */}
+ {statistics && !isLoading && (
+ <>
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 py-3">
+ <div className="text-center">
+ <div className="text-2xl font-bold text-primary">{statistics.total}</div>
+ <div className="text-xs text-muted-foreground">전체 품목</div>
+ </div>
+ <div className="text-center">
+ <div className="text-2xl font-bold text-gray-600">{statistics.regular}</div>
+ <div className="text-xs text-muted-foreground">일반 품목</div>
+ </div>
+ <div className="text-center">
+ <div className="text-2xl font-bold text-green-600">{statistics.totalQuantity.toLocaleString()}</div>
+ <div className="text-xs text-muted-foreground">총 수량</div>
+ </div>
+ <div className="text-center">
+ <div className="text-2xl font-bold text-orange-600">{statistics.totalWeight.toLocaleString()}</div>
+ <div className="text-xs text-muted-foreground">총 중량 (KG)</div>
+ </div>
+ </div>
+ <Separator />
+ </>
+ )}
+
+ <ScrollArea className="flex-1">
+ {isLoading ? (
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-[60px]">아이템</TableHead>
+ <TableHead className="w-[120px]">자재코드</TableHead>
+ <TableHead>자재명</TableHead>
+ <TableHead className="w-[80px]">수량</TableHead>
+ <TableHead className="w-[60px]">수량단위</TableHead>
+ <TableHead className="w-[80px]">중량</TableHead>
+ <TableHead className="w-[60px]">중량단위</TableHead>
+ <TableHead className="w-[100px]">납기일</TableHead>
+ <TableHead className="w-[100px]">PR번호</TableHead>
+ <TableHead className="w-[120px]">사양/설계문서</TableHead>
+ <TableHead>비고</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {[...Array(3)].map((_, i) => (
+ <TableRow key={i}>
+ <TableCell><Skeleton className="h-8 w-full" /></TableCell>
+ <TableCell><Skeleton className="h-8 w-full" /></TableCell>
+ <TableCell><Skeleton className="h-8 w-full" /></TableCell>
+ <TableCell><Skeleton className="h-8 w-full" /></TableCell>
+ <TableCell><Skeleton className="h-8 w-full" /></TableCell>
+ <TableCell><Skeleton className="h-8 w-full" /></TableCell>
+ <TableCell><Skeleton className="h-8 w-full" /></TableCell>
+ <TableCell><Skeleton className="h-8 w-full" /></TableCell>
+ <TableCell><Skeleton className="h-8 w-full" /></TableCell>
+ <TableCell><Skeleton className="h-8 w-full" /></TableCell>
+ <TableCell><Skeleton className="h-8 w-full" /></TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ ) : items.length === 0 ? (
+ <div className="text-center text-muted-foreground py-12">
+ <Package className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
+ <p>품목이 없습니다.</p>
+ </div>
+ ) : (
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-[60px]">아이템</TableHead>
+ <TableHead className="w-[120px]">자재코드</TableHead>
+ <TableHead>자재명</TableHead>
+ <TableHead className="w-[80px]">수량</TableHead>
+ <TableHead className="w-[60px]">수량단위</TableHead>
+ <TableHead className="w-[80px]">중량</TableHead>
+ <TableHead className="w-[60px]">중량단위</TableHead>
+ <TableHead className="w-[100px]">납기일</TableHead>
+ <TableHead className="w-[100px]">PR번호</TableHead>
+ <TableHead className="w-[100px]">PR 아이템 번호</TableHead>
+ <TableHead className="w-[120px]">사양/설계문서</TableHead>
+ <TableHead className="w-[100px]">프로젝트</TableHead>
+ <TableHead>비고</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {items.map((item, index) => (
+ <TableRow key={item.id} className={item.majorYn ? "bg-blue-50 border-l-4 border-l-blue-500" : ""}>
+ <TableCell>
+ <div className="flex flex-col items-center gap-1">
+ <span className="text-xs font-mono">#{index + 1}</span>
+ {item.majorYn && (
+ <Badge variant="default" className="text-xs px-1 py-0">
+ 주요
+ </Badge>
+ )}
+ </div>
+ </TableCell>
+ <TableCell>
+ <div className="flex flex-col">
+ <span className="font-mono text-sm font-medium">{item.materialCode || "-"}</span>
+ {item.acc && (
+ <span className="text-xs text-muted-foreground font-mono">
+ ACC: {item.acc}
+ </span>
+ )}
+ </div>
+ </TableCell>
+ <TableCell>
+ <div className="flex flex-col">
+ <span className="text-sm font-medium" title={item.materialDescription || ""}>
+ {item.materialDescription || "-"}
+ </span>
+ {item.materialCategory && (
+ <span className="text-xs text-muted-foreground">
+ {item.materialCategory}
+ </span>
+ )}
+ {item.size && (
+ <span className="text-xs text-muted-foreground">
+ 크기: {item.size}
+ </span>
+ )}
+ </div>
+ </TableCell>
+ <TableCell>
+ <span className="text-sm font-medium">
+ {item.quantity ? item.quantity.toLocaleString() : "-"}
+ </span>
+ </TableCell>
+ <TableCell>
+ <span className="text-sm text-muted-foreground">
+ {item.uom || "-"}
+ </span>
+ </TableCell>
+ <TableCell>
+ <span className="text-sm font-medium">
+ {item.grossWeight ? item.grossWeight.toLocaleString() : "-"}
+ </span>
+ </TableCell>
+ <TableCell>
+ <span className="text-sm text-muted-foreground">
+ {item.gwUom || "-"}
+ </span>
+ </TableCell>
+ <TableCell>
+ <span className="text-sm">
+ {item.deliveryDate ? format(new Date(item.deliveryDate), "yyyy-MM-dd") : "-"}
+ </span>
+ </TableCell>
+ <TableCell>
+ <span className="text-xs font-mono">{item.prNo || "-"}</span>
+ </TableCell>
+ <TableCell>
+ <span className="text-xs font-mono">{item.prItem || "-"}</span>
+ </TableCell>
+ <TableCell>
+ <div className="flex flex-col gap-1">
+ {/* 기존 스펙 정보 */}
+ <div className="flex items-center gap-1">
+ {item.specNo && (
+ <span className="text-xs font-mono">{item.specNo}</span>
+ )}
+ {item.specUrl && (
+ <Button
+ variant="ghost"
+ size="sm"
+ className="h-5 w-5 p-0"
+ onClick={() => handleOpenSpec(item.specUrl!)}
+ title="사양서 열기"
+ >
+ <ExternalLink className="h-3 w-3" />
+ </Button>
+ )}
+ </div>
+
+ {/* POS 파일 다운로드 */}
+ {item.materialCode && (
+ <div className="flex items-center gap-1">
+ <FileText className="h-3 w-3 text-green-500" />
+ <Button
+ variant="ghost"
+ size="sm"
+ className="h-5 p-1 text-xs text-green-600 hover:text-green-800"
+ onClick={() => handleOpenPosDialog(item.materialCode!)}
+ disabled={loadingPosFiles && selectedMaterialCode === item.materialCode}
+ title={`POS 파일 다운로드 (자재코드: ${item.materialCode})`}
+ >
+ <Download className="h-3 w-3 mr-1" />
+ {loadingPosFiles && selectedMaterialCode === item.materialCode ? '조회중...' : 'POS 다운로드'}
+ </Button>
+ </div>
+ )}
+
+ {/* 트래킹 번호 */}
+ {item.trackingNo && (
+ <div className="text-xs text-muted-foreground">
+ TRK: {item.trackingNo}
+ </div>
+ )}
+ </div>
+ </TableCell>
+ <TableCell>
+ <div className="text-xs">
+ {[
+ 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(" | ") || "-"}
+ </div>
+ </TableCell>
+ <TableCell>
+ <span className="text-xs" title={item.remark || ""}>
+ {item.remark ? (item.remark.length > 30 ? `${item.remark.slice(0, 30)}...` : item.remark) : "-"}
+ </span>
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ )}
+ </ScrollArea>
+
+ {/* 하단 통계 정보 */}
+ {statistics && !isLoading && (
+ <div className="border-t pt-3 text-xs text-muted-foreground">
+ <div className="flex justify-between items-center">
+ <span>
+ 총 {statistics.total}개 품목
+ </span>
+ <span>
+ 전체 수량: {statistics.totalQuantity.toLocaleString()} |
+ 전체 중량: {statistics.totalWeight.toLocaleString()} KG
+ </span>
+ </div>
+ </div>
+ )}
+
+ {/* POS 파일 선택 다이얼로그 */}
+ <PosFileSelectionDialog
+ isOpen={posDialogOpen}
+ onClose={handleClosePosDialog}
+ materialCode={selectedMaterialCode}
+ files={posFiles}
+ onDownload={handleDownloadPosFile}
+ downloadingIndex={downloadingFileIndex}
+ />
+ </DialogContent>
+ </Dialog>
+ )
+}
+