From e4b2bef735e6aab6a5ecae9a017c5c618a6d3a4b Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 3 Jul 2025 02:48:24 +0000 Subject: (최겸) 기술영업 벤더 아이템 조회 아이콘 기능 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tech-vendors/repository.ts | 63 ++++++- lib/tech-vendors/service.ts | 101 +++++++---- .../tech-vendor-possible-items-view-dialog.tsx | 201 +++++++++++++++++++++ .../table/tech-vendors-table-columns.tsx | 50 ++++- lib/tech-vendors/table/tech-vendors-table.tsx | 22 ++- 5 files changed, 389 insertions(+), 48 deletions(-) create mode 100644 lib/tech-vendors/table/tech-vendor-possible-items-view-dialog.tsx (limited to 'lib') diff --git a/lib/tech-vendors/repository.ts b/lib/tech-vendors/repository.ts index 72c01a1c..d3c6671c 100644 --- a/lib/tech-vendors/repository.ts +++ b/lib/tech-vendors/repository.ts @@ -2,8 +2,9 @@ import { eq, inArray, count, desc } from "drizzle-orm"; import db from '@/db/db'; -import { sql, SQL } from "drizzle-orm"; +import { SQL } from "drizzle-orm"; import { techVendors, techVendorContacts, techVendorPossibleItems, techVendorItemsView, type TechVendor, type TechVendorContact, type TechVendorItem, type TechVendorWithAttachments, techVendorAttachments } from "@/db/schema/techVendors"; +import { itemShipbuilding, itemOffshoreTop, itemOffshoreHull } from "@/db/schema/items"; export type NewTechVendorContact = typeof techVendorContacts.$inferInsert export type NewTechVendorItem = typeof techVendorPossibleItems.$inferInsert @@ -79,10 +80,14 @@ export async function selectTechVendorsWithAttachments( .from(techVendorAttachments) .where(eq(techVendorAttachments.vendorId, vendor.id)); + // 벤더의 worktype 조회 + const workTypes = await getVendorWorkTypes(tx, vendor.id, vendor.techVendorType); + return { ...vendor, hasAttachments: attachments.length > 0, attachmentsList: attachments, + workTypes: workTypes.join(', '), // 콤마로 구분해서 저장 } as TechVendorWithAttachments; }) ); @@ -326,3 +331,59 @@ export async function insertTechVendorItem( .returning(); } +// 벤더의 worktype 조회 +export async function getVendorWorkTypes( + tx: any, + vendorId: number, + vendorType: string +): Promise { + try { + // 벤더의 possible items 조회 + const possibleItems = await tx + .select({ itemCode: techVendorPossibleItems.itemCode }) + .from(techVendorPossibleItems) + .where(eq(techVendorPossibleItems.vendorId, vendorId)); + + if (!possibleItems.length) { + return []; + } + + const itemCodes = possibleItems.map((item: { itemCode: string }) => item.itemCode); + const workTypes: string[] = []; + + // 벤더 타입에 따라 해당하는 아이템 테이블에서 worktype 조회 + if (vendorType.includes('조선')) { + const shipWorkTypes = await tx + .select({ workType: itemShipbuilding.workType }) + .from(itemShipbuilding) + .where(inArray(itemShipbuilding.itemCode, itemCodes)); + + workTypes.push(...shipWorkTypes.map((item: { workType: string | null }) => item.workType).filter(Boolean)); + } + + if (vendorType.includes('해양TOP')) { + const topWorkTypes = await tx + .select({ workType: itemOffshoreTop.workType }) + .from(itemOffshoreTop) + .where(inArray(itemOffshoreTop.itemCode, itemCodes)); + + workTypes.push(...topWorkTypes.map((item: { workType: string | null }) => item.workType).filter(Boolean)); + } + + if (vendorType.includes('해양HULL')) { + const hullWorkTypes = await tx + .select({ workType: itemOffshoreHull.workType }) + .from(itemOffshoreHull) + .where(inArray(itemOffshoreHull.itemCode, itemCodes)); + + workTypes.push(...hullWorkTypes.map((item: { workType: string | null }) => item.workType).filter(Boolean)); + } + + // 중복 제거 후 반환 + const uniqueWorkTypes = [...new Set(workTypes)]; + + return uniqueWorkTypes; + } catch (error) { + return []; + } +} diff --git a/lib/tech-vendors/service.ts b/lib/tech-vendors/service.ts index 15e7331b..71d47e05 100644 --- a/lib/tech-vendors/service.ts +++ b/lib/tech-vendors/service.ts @@ -846,13 +846,15 @@ export async function getItemsByVendorType(vendorType: string, itemCode: string) })); return { data: result, error: null }; - } catch (error) { + } catch (err) { + console.error("Error fetching items by vendor type:", err); return { data: [], error: "Failed to fetch items" }; } } /** * 벤더의 possible_items를 조회하고 해당 아이템 코드로 각 타입별 테이블을 조회 + * 벤더 타입이 콤마로 구분된 경우 (예: "조선,해양TOP,해양HULL") 모든 타입의 아이템을 조회 */ export async function getVendorItemsByType(vendorId: number, vendorType: string) { try { @@ -865,47 +867,61 @@ export async function getVendorItemsByType(vendorId: number, vendorType: string) }) const itemCodes = possibleItems.map(item => item.itemCode) + + if (itemCodes.length === 0) { + return { data: [] } + } - // 벤더 타입에 따라 해당하는 테이블에서 아이템 조회 - switch (vendorType) { - case "조선": - const shipbuildingItems = await db.query.itemShipbuilding.findMany({ - where: inArray(itemShipbuilding.itemCode, itemCodes) - }) - return { - data: shipbuildingItems.map(item => ({ + // 벤더 타입을 콤마로 분리 + const vendorTypes = vendorType.split(',').map(type => type.trim()) + const allItems: Array & { techVendorType: "조선" | "해양TOP" | "해양HULL" }> = [] + + // 각 벤더 타입에 따라 해당하는 테이블에서 아이템 조회 + for (const singleType of vendorTypes) { + switch (singleType) { + case "조선": + const shipbuildingItems = await db.query.itemShipbuilding.findMany({ + where: inArray(itemShipbuilding.itemCode, itemCodes) + }) + allItems.push(...shipbuildingItems.map(item => ({ ...item, - techVendorType: "조선" - })) - } + techVendorType: "조선" as const + }))) + break - case "해양TOP": - const offshoreTopItems = await db.query.itemOffshoreTop.findMany({ - where: inArray(itemOffshoreTop.itemCode, itemCodes) - }) - return { - data: offshoreTopItems.map(item => ({ + case "해양TOP": + const offshoreTopItems = await db.query.itemOffshoreTop.findMany({ + where: inArray(itemOffshoreTop.itemCode, itemCodes) + }) + allItems.push(...offshoreTopItems.map(item => ({ ...item, - techVendorType: "해양TOP" - })) - } + techVendorType: "해양TOP" as const + }))) + break - case "해양HULL": - const offshoreHullItems = await db.query.itemOffshoreHull.findMany({ - where: inArray(itemOffshoreHull.itemCode, itemCodes) - }) - return { - data: offshoreHullItems.map(item => ({ + case "해양HULL": + const offshoreHullItems = await db.query.itemOffshoreHull.findMany({ + where: inArray(itemOffshoreHull.itemCode, itemCodes) + }) + allItems.push(...offshoreHullItems.map(item => ({ ...item, - techVendorType: "해양HULL" - })) - } + techVendorType: "해양HULL" as const + }))) + break - default: - throw new Error(`Unsupported vendor type: ${vendorType}`) + default: + console.warn(`Unknown vendor type: ${singleType}`) + break + } } - } catch (error) { - throw error + + // 중복 허용 - 모든 아이템을 그대로 반환 + return { + data: allItems.sort((a, b) => a.itemCode.localeCompare(b.itemCode)) + } + } catch (err) { + console.error("Error getting vendor items by type:", err) + return { data: [] } } } @@ -1645,3 +1661,20 @@ export async function addTechVendor(input: { } } +/** + * 벤더의 possible items 개수 조회 + */ +export async function getTechVendorPossibleItemsCount(vendorId: number): Promise { + try { + const result = await db + .select({ count: sql`count(*)`.as("count") }) + .from(techVendorPossibleItems) + .where(eq(techVendorPossibleItems.vendorId, vendorId)); + + return result[0]?.count || 0; + } catch (err) { + console.error("Error getting tech vendor possible items count:", err); + return 0; + } +} + diff --git a/lib/tech-vendors/table/tech-vendor-possible-items-view-dialog.tsx b/lib/tech-vendors/table/tech-vendor-possible-items-view-dialog.tsx new file mode 100644 index 00000000..b2b9c990 --- /dev/null +++ b/lib/tech-vendors/table/tech-vendor-possible-items-view-dialog.tsx @@ -0,0 +1,201 @@ +"use client" + +import * as React from "react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Package, FileText, X } from "lucide-react" +import { getVendorItemsByType } from "../service" + +interface VendorPossibleItem { + id: number; + itemCode: string; + itemList: string; + workType: string | null; + shipTypes?: string | null; // 조선용 + subItemList?: string | null; // 해양용 + techVendorType: "조선" | "해양TOP" | "해양HULL"; +} + +interface TechVendorPossibleItemsViewDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + vendor: { + id: number; + vendorName?: string | null; + vendorCode?: string | null; + techVendorType?: string | null; + } | null; +} + +export function TechVendorPossibleItemsViewDialog({ + open, + onOpenChange, + vendor, +}: TechVendorPossibleItemsViewDialogProps) { + const [items, setItems] = React.useState([]); + const [loading, setLoading] = React.useState(false); + + console.log("TechVendorPossibleItemsViewDialog render:", { open, vendor }); + + React.useEffect(() => { + console.log("TechVendorPossibleItemsViewDialog useEffect:", { open, vendorId: vendor?.id }); + if (open && vendor?.id && vendor?.techVendorType) { + loadItems(); + } + }, [open, vendor?.id, vendor?.techVendorType]); + + const loadItems = async () => { + if (!vendor?.id || !vendor?.techVendorType) return; + + console.log("Loading items for vendor:", vendor.id, vendor.techVendorType); + setLoading(true); + try { + const result = await getVendorItemsByType(vendor.id, vendor.techVendorType); + console.log("Items loaded:", result); + if (result.data) { + setItems(result.data); + } + } catch (error) { + console.error("Failed to load items:", error); + } finally { + setLoading(false); + } + }; + + const getTypeLabel = (type: string) => { + switch (type) { + case "조선": + return "조선"; + case "해양TOP": + return "해양TOP"; + case "해양HULL": + return "해양HULL"; + default: + return type; + } + }; + + const getTypeColor = (type: string) => { + switch (type) { + case "조선": + return "bg-blue-100 text-blue-800"; + case "해양TOP": + return "bg-green-100 text-green-800"; + case "해양HULL": + return "bg-purple-100 text-purple-800"; + default: + return "bg-gray-100 text-gray-800"; + } + }; + + return ( + + + + + 벤더 Possible Items 조회 + + {vendor?.vendorName || `Vendor #${vendor?.id}`} + + {vendor?.techVendorType && ( + + {getTypeLabel(vendor.techVendorType)} + + )} + + + 해당 벤더가 공급 가능한 아이템 목록을 확인할 수 있습니다. + + + +
+
+ {loading ? ( +
+
+
+

아이템을 불러오는 중...

+
+
+ ) : items.length === 0 ? ( +
+ +

등록된 아이템이 없습니다

+

+ 이 벤더에 등록된 아이템이 없습니다. +

+
+ ) : ( + <> + {/* 헤더 행 (라벨) */} +
+
No.
+
타입
+
자재 그룹
+
공종
+
자재명
+
선종/자재명(상세)
+
+ + {/* 아이템 행들 */} +
+ {items.map((item, index) => ( +
+
+ {index + 1} +
+
+ + {getTypeLabel(item.techVendorType)} + +
+
+ {item.itemCode} +
+
+ {item.workType || '-'} +
+
+ {item.itemList} +
+
+ {item.techVendorType === '조선' ? item.shipTypes : item.subItemList} +
+
+ ))} +
+ +
+
+ + + 총 {items.length}개 아이템 + +
+
+ + )} +
+
+ + + + +
+
+ ) +} \ No newline at end of file diff --git a/lib/tech-vendors/table/tech-vendors-table-columns.tsx b/lib/tech-vendors/table/tech-vendors-table-columns.tsx index 22e89dd0..438fceac 100644 --- a/lib/tech-vendors/table/tech-vendors-table-columns.tsx +++ b/lib/tech-vendors/table/tech-vendors-table-columns.tsx @@ -3,7 +3,7 @@ import * as React from "react" import { type DataTableRowAction } from "@/types/table" import { type ColumnDef } from "@tanstack/react-table" -import { Ellipsis } from "lucide-react" +import { Ellipsis, Package } from "lucide-react" import { toast } from "sonner" import { getErrorMessage } from "@/lib/handle-error" @@ -47,6 +47,7 @@ type NextRouter = ReturnType; interface GetColumnsProps { setRowAction: React.Dispatch | null>>; router: NextRouter; + openItemsDialog: (vendor: TechVendor) => void; } @@ -55,7 +56,7 @@ interface GetColumnsProps { /** * tanstack table 컬럼 정의 (중첩 헤더 버전) */ -export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef[] { +export function getColumns({ setRowAction, router, openItemsDialog }: GetColumnsProps): ColumnDef[] { // ---------------------------------------------------------------- // 1) select 컬럼 (체크박스) // ---------------------------------------------------------------- @@ -230,12 +231,6 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef className: "bg-slate-800 text-white border-slate-900", iconColor: "text-white" }; - case "PENDING_REVIEW": - return { - variant: "default", - className: "bg-gray-100 text-gray-800 border-gray-300", - iconColor: "text-gray-600" - }; default: return { variant: "default", @@ -251,7 +246,7 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef "ACTIVE": "활성 상태", "INACTIVE": "비활성 상태", "BLACKLISTED": "거래 금지", - "PENDING_REVIEW": "비교 견적", + "PENDING_REVIEW": "비교 견적" }; return statusMap[status] || status; @@ -306,6 +301,43 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef } }); + // Possible Items 컬럼 추가 (액션 컬럼 직전에) + const possibleItemsColumn: ColumnDef = { + id: "possibleItems", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const vendor = row.original; + + const handleClick = () => { + openItemsDialog(vendor); + }; + + return ( + + ); + }, + enableSorting: false, + enableResizing: false, + size: 80, + meta: { + excelHeader: "Possible Items" + }, + }; + + columns.push(possibleItemsColumn); columns.push(actionsColumn); // 마지막에 액션 컬럼 추가 return columns; diff --git a/lib/tech-vendors/table/tech-vendors-table.tsx b/lib/tech-vendors/table/tech-vendors-table.tsx index 63ca8fcc..125e39dc 100644 --- a/lib/tech-vendors/table/tech-vendors-table.tsx +++ b/lib/tech-vendors/table/tech-vendors-table.tsx @@ -17,6 +17,7 @@ import { TechVendor, techVendors, TechVendorWithAttachments } from "@/db/schema/ import { TechVendorsTableToolbarActions } from "./tech-vendors-table-toolbar-actions" import { UpdateVendorSheet } from "./update-vendor-sheet" import { getVendorStatusIcon } from "../utils" +import { TechVendorPossibleItemsViewDialog } from "./tech-vendor-possible-items-view-dialog" // import { ViewTechVendorLogsDialog } from "./view-tech-vendors-logs-dialog" interface TechVendorsTableProps { @@ -34,14 +35,22 @@ export function TechVendorsTable({ promises }: TechVendorsTableProps) { const [isCompact, setIsCompact] = React.useState(false) const [rowAction, setRowAction] = React.useState | null>(null) + const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false) + const [selectedVendorForItems, setSelectedVendorForItems] = React.useState(null) // **router** 획득 const router = useRouter() - // getColumns() 호출 시, router를 주입 + // openItemsDialog 함수 정의 + const openItemsDialog = React.useCallback((vendor: TechVendor) => { + setSelectedVendorForItems(vendor) + setItemsDialogOpen(true) + }, []) + + // getColumns() 호출 시, router와 openItemsDialog를 주입 const columns = React.useMemo( - () => getColumns({ setRowAction, router }), - [setRowAction, router] + () => getColumns({ setRowAction, router, openItemsDialog }), + [setRowAction, router, openItemsDialog] ) // 상태 한글 변환 유틸리티 함수 @@ -133,7 +142,7 @@ export function TechVendorsTable({ promises }: TechVendorsTableProps) { enableAdvancedFilter: true, initialState: { sorting: [{ id: "createdAt", desc: true }], - columnPinning: { right: ["actions"] }, + columnPinning: { right: ["actions", "possibleItems"] }, }, getRowId: (originalRow) => String(originalRow.id), shallow: false, @@ -173,6 +182,11 @@ export function TechVendorsTable({ promises }: TechVendorsTableProps) { onOpenChange={() => setRowAction(null)} vendor={rowAction?.row.original ?? null} /> + ) } \ No newline at end of file -- cgit v1.2.3