diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-04 17:30:27 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-04 17:30:27 +0900 |
| commit | a1710296dbc1881a7ed86093872904a529901430 (patch) | |
| tree | 62e4be5dabba4bf7c337769f391d11ce59154cc4 /lib/rfq-last/table | |
| parent | 680da9b323db8b8d7cf27c674ab0016ec87bfe81 (diff) | |
(김준회) RFQ 테이블 POS 다운로드할 수 있도록 변경, 벤더측은 다운로드시 암호화 해제 추가, item dialog 통일처리
Diffstat (limited to 'lib/rfq-last/table')
| -rw-r--r-- | lib/rfq-last/table/rfq-items-dialog.tsx | 479 | ||||
| -rw-r--r-- | lib/rfq-last/table/rfq-table.tsx | 3 |
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" /> )} </> |
