summaryrefslogtreecommitdiff
path: root/lib/rfq-last/table
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/table
parent680da9b323db8b8d7cf27c674ab0016ec87bfe81 (diff)
(김준회) RFQ 테이블 POS 다운로드할 수 있도록 변경, 벤더측은 다운로드시 암호화 해제 추가, item dialog 통일처리
Diffstat (limited to 'lib/rfq-last/table')
-rw-r--r--lib/rfq-last/table/rfq-items-dialog.tsx479
-rw-r--r--lib/rfq-last/table/rfq-table.tsx3
2 files changed, 2 insertions, 480 deletions
diff --git a/lib/rfq-last/table/rfq-items-dialog.tsx b/lib/rfq-last/table/rfq-items-dialog.tsx
deleted file mode 100644
index 5f8e4382..00000000
--- a/lib/rfq-last/table/rfq-items-dialog.tsx
+++ /dev/null
@@ -1,479 +0,0 @@
-"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 } 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
-}
-
-export function RfqItemsDialog({ isOpen, onClose, rfqData }: 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}` })
-
- const downloadUrl = `/api/pos/download-on-demand?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-blue-600">{statistics.major}</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>
- <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}개 품목
- {/* (주요: {statistics.major}개, 일반: {statistics.regular}개) */}
- </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>
- )
-} \ No newline at end of file
diff --git a/lib/rfq-last/table/rfq-table.tsx b/lib/rfq-last/table/rfq-table.tsx
index 162dd343..46bb4670 100644
--- a/lib/rfq-last/table/rfq-table.tsx
+++ b/lib/rfq-last/table/rfq-table.tsx
@@ -25,7 +25,7 @@ import { RfqsLastView } from "@/db/schema";
import { getRfqs } from "../service";
import { RfqTableToolbarActions } from "./rfq-table-toolbar-actions";
import { RfqAttachmentsDialog } from "./rfq-attachments-dialog";
-import { RfqItemsDialog } from "./rfq-items-dialog";
+import { RfqItemsDialog } from "../shared/rfq-items-dialog";
interface RfqTableProps {
data: Awaited<ReturnType<typeof getRfqs>>;
@@ -494,6 +494,7 @@ export function RfqTable({
isOpen={true}
onClose={() => setRowAction(null)}
rfqData={rowAction.row.original}
+ viewerType="evcp"
/>
)}
</>