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 | |
| parent | 680da9b323db8b8d7cf27c674ab0016ec87bfe81 (diff) | |
(김준회) RFQ 테이블 POS 다운로드할 수 있도록 변경, 벤더측은 다운로드시 암호화 해제 추가, item dialog 통일처리
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/pos/components/pos-file-selection-dialog.tsx | 7 | ||||
| -rw-r--r-- | lib/rfq-last/shared/rfq-items-dialog.tsx (renamed from lib/rfq-last/table/rfq-items-dialog.tsx) | 40 | ||||
| -rw-r--r-- | lib/rfq-last/table/rfq-table.tsx | 3 | ||||
| -rw-r--r-- | lib/rfq-last/vendor-response/editor/quotation-items-table.tsx | 32 | ||||
| -rw-r--r-- | lib/rfq-last/vendor-response/rfq-items-dialog.tsx | 360 | ||||
| -rw-r--r-- | lib/rfq-last/vendor-response/vendor-quotations-table.tsx | 3 | ||||
| -rw-r--r-- | lib/swp/table/swp-table-columns.tsx | 2 |
7 files changed, 33 insertions, 414 deletions
diff --git a/lib/pos/components/pos-file-selection-dialog.tsx b/lib/pos/components/pos-file-selection-dialog.tsx index 29936d21..0553754d 100644 --- a/lib/pos/components/pos-file-selection-dialog.tsx +++ b/lib/pos/components/pos-file-selection-dialog.tsx @@ -18,7 +18,6 @@ import { TableRow, } from "@/components/ui/table" import { Button } from "@/components/ui/button" -import { ScrollArea } from "@/components/ui/scroll-area" import { Badge } from "@/components/ui/badge" interface PosFileInfo { @@ -49,7 +48,7 @@ export function PosFileSelectionDialog({ }: PosFileSelectionDialogProps) { return ( <Dialog open={isOpen} onOpenChange={onClose}> - <DialogContent className="max-w-4xl"> + <DialogContent className="max-w-7xl max-h-[90vh]"> <DialogHeader> <DialogTitle className="flex items-center gap-2"> <FileText className="h-5 w-5 text-green-600" /> @@ -62,7 +61,7 @@ export function PosFileSelectionDialog({ </DialogDescription> </DialogHeader> - <ScrollArea className="max-h-[60vh]"> + <div className="overflow-auto max-h-[70vh]"> <Table> <TableHeader> <TableRow> @@ -119,7 +118,7 @@ export function PosFileSelectionDialog({ ))} </TableBody> </Table> - </ScrollArea> + </div> {files.length === 0 && ( <div className="text-center py-8 text-muted-foreground"> diff --git a/lib/rfq-last/table/rfq-items-dialog.tsx b/lib/rfq-last/shared/rfq-items-dialog.tsx index 5f8e4382..c25670fc 100644 --- a/lib/rfq-last/table/rfq-items-dialog.tsx +++ b/lib/rfq-last/shared/rfq-items-dialog.tsx @@ -24,7 +24,7 @@ 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 { 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" @@ -77,10 +77,21 @@ interface ItemStatistics { interface RfqItemsDialogProps { isOpen: boolean onClose: () => void - rfqData: RfqsLastView + rfqData: RfqsLastView | VendorQuotationView + /** + * 뷰어 타입 + * - 'evcp': EVCP 사용자 (암호화된 파일 직접 다운로드) + * - 'partners': 협력사 사용자 (복호화된 파일 다운로드) + */ + viewerType?: 'evcp' | 'partners' } -export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps) { +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) @@ -182,7 +193,12 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps try { toast.loading(`POS 파일 다운로드 준비 중...`, { id: `download-${fileIndex}` }) - const downloadUrl = `/api/pos/download-on-demand?materialCode=${encodeURIComponent(selectedMaterialCode)}&fileIndex=${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') @@ -220,15 +236,11 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps {/* 통계 정보 */} {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="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> @@ -278,8 +290,6 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps <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> @@ -453,8 +463,7 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps <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}개) */} + 총 {statistics.total}개 품목 </span> <span> 전체 수량: {statistics.totalQuantity.toLocaleString()} | @@ -476,4 +485,5 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps </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" /> )} </> diff --git a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx index 23ddc924..b0c1488a 100644 --- a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx +++ b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx @@ -64,15 +64,6 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp console.log(prItems,"prItems") - // 통계 정보 계산 - const statistics = { - total: prItems.length, - regular: prItems.filter(item => !item.majorYn).length, - major: prItems.filter(item => item.majorYn).length, - totalQuantity: prItems.reduce((sum, item) => sum + (item.quantity || 0), 0), - totalWeight: prItems.reduce((sum, item) => sum + (item.grossWeight || 0), 0), - } - // PR 아이템 정보를 quotationItems에 초기화 useEffect(() => { if (prItems && prItems.length > 0) { @@ -474,29 +465,6 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp </div> </div> </div> - - {/* 통계 정보 */} - <div className="mt-4"> - <div className="grid grid-cols-2 sm:grid-cols-4 gap-3"> - <div className="text-center p-3 border rounded-lg bg-muted/50"> - <div className="text-2xl font-bold text-primary">{statistics.total}</div> - <div className="text-xs text-muted-foreground">전체 품목</div> - </div> - <div className="text-center p-3 border rounded-lg bg-muted/50"> - <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 p-3 border rounded-lg bg-muted/50"> - <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 p-3 border rounded-lg bg-muted/50"> - <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 className="mt-4" /> - </div> </CardHeader> <CardContent> <ScrollArea className="h-[600px]"> diff --git a/lib/rfq-last/vendor-response/rfq-items-dialog.tsx b/lib/rfq-last/vendor-response/rfq-items-dialog.tsx deleted file mode 100644 index 9790a1bd..00000000 --- a/lib/rfq-last/vendor-response/rfq-items-dialog.tsx +++ /dev/null @@ -1,360 +0,0 @@ -"use client" - -import * as React from "react" -import { format } from "date-fns" -import { Package, ExternalLink } 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" - -// 품목 타입 -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) - - // 품목 목록 로드 - React.useEffect(() => { - if (!isOpen || !rfqData.id) return - - const loadItems = async () => { - setIsLoading(true) - try { - const result = await getRfqItemsAction(rfqData.id) - - if (result.success) { - setItems(result.data) - setStatistics(result.statistics ?? null) - } else { - toast.error(result.error || "품목을 불러오는데 실패했습니다") - setItems([]) - setStatistics(null) - } - } catch (error) { - console.error("품목 로드 오류:", error) - toast.error("품목을 불러오는데 실패했습니다") - setItems([]) - setStatistics(null) - } finally { - setIsLoading(false) - } - } - - loadItems() - }, [isOpen, rfqData.id]) - - // 사양서 링크 열기 - const handleOpenSpec = (specUrl: string) => { - window.open(specUrl, '_blank', 'noopener,noreferrer') - } - - - 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-5 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-[80px]">사양</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> - </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]">사양</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> - <div className="flex flex-col"> - <span className="text-xs font-mono">{item.prNo || "-"}</span> - {item.prItem && item.prItem !== item.prNo && ( - <span className="text-xs text-muted-foreground font-mono"> - {item.prItem} - </span> - )} - </div> - </TableCell> - <TableCell> - <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> - )} - {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> - )} - </DialogContent> - </Dialog> - ) -}
\ No newline at end of file diff --git a/lib/rfq-last/vendor-response/vendor-quotations-table.tsx b/lib/rfq-last/vendor-response/vendor-quotations-table.tsx index 2e4975f1..dccd9b6c 100644 --- a/lib/rfq-last/vendor-response/vendor-quotations-table.tsx +++ b/lib/rfq-last/vendor-response/vendor-quotations-table.tsx @@ -13,7 +13,7 @@ import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-adv import { useRouter } from "next/navigation" import { getColumns } from "./vendor-quotations-table-columns" import { RfqAttachmentsDialog } from "./rfq-attachments-dialog"; -import { RfqItemsDialog } from "./rfq-items-dialog"; +import { RfqItemsDialog } from "../shared/rfq-items-dialog"; import { VendorQuotationView } from "@/db/schema" interface VendorQuotationsTableLastProps { @@ -163,6 +163,7 @@ export function VendorQuotationsTableLast({ promises }: VendorQuotationsTableLas isOpen={true} onClose={() => setRowAction(null)} rfqData={rowAction.row.original} + viewerType="partners" /> )} </> diff --git a/lib/swp/table/swp-table-columns.tsx b/lib/swp/table/swp-table-columns.tsx index 09207be8..f42e7a7a 100644 --- a/lib/swp/table/swp-table-columns.tsx +++ b/lib/swp/table/swp-table-columns.tsx @@ -131,7 +131,7 @@ export const swpDocumentColumns: ColumnDef<DocumentListItem>[] = [ }, { id: "actions", - header: "액션", + header: "커버페이지 다운로드", cell: function ActionCell({ row }) { const [isDownloading, setIsDownloading] = React.useState(false); |
