From 89274bffa596ffdfc4275fb8d11cdb02ff9a2d02 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 13 Oct 2025 00:22:54 +0000 Subject: (최겸) 기술영업 import 수정 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/techsales-rfq/service.ts | 240 +++++++++++++++++++++ lib/techsales-rfq/table/create-rfq-hull-dialog.tsx | 22 ++ lib/techsales-rfq/table/create-rfq-ship-dialog.tsx | 27 ++- lib/techsales-rfq/table/create-rfq-top-dialog.tsx | 22 ++ .../table/detail-table/rfq-detail-table.tsx | 1 + .../vendor-contact-selection-dialog.tsx | 46 ++-- lib/techsales-rfq/table/rfq-table-column.tsx | 31 +++ lib/techsales-rfq/table/rfq-table.tsx | 76 ++++++- .../table/vendor-quotations-table-columns.tsx | 38 +++- .../table/vendor-quotations-table.tsx | 1 + 10 files changed, 478 insertions(+), 26 deletions(-) (limited to 'lib/techsales-rfq') diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts index 3736bf76..deb2981a 100644 --- a/lib/techsales-rfq/service.ts +++ b/lib/techsales-rfq/service.ts @@ -1719,6 +1719,68 @@ export async function deleteTechSalesRfqAttachment(attachmentId: number) { } } +/** + * 기술영업 RFQ 삭제 (벤더 추가 이전에만 가능) + */ +export async function deleteTechSalesRfq(rfqId: number) { + unstable_noStore(); + try { + return await db.transaction(async (tx) => { + // RFQ 정보 조회 및 상태 확인 + const rfq = await tx.query.techSalesRfqs.findFirst({ + where: eq(techSalesRfqs.id, rfqId), + columns: { id: true, status: true, rfqType: true } + }); + + if (!rfq) { + throw new Error("RFQ를 찾을 수 없습니다."); + } + + // 벤더 추가 이전 상태에서만 삭제 가능 + if (rfq.status !== "RFQ Created") { + throw new Error("벤더가 추가된 RFQ는 삭제할 수 없습니다."); + } + + // 관련 RFQ 아이템들 삭제 + await tx.delete(techSalesRfqItems) + .where(eq(techSalesRfqItems.rfqId, rfqId)); + + // 관련 첨부파일들 삭제 (파일 시스템에서도 삭제) + const attachments = await tx.query.techSalesAttachments.findMany({ + where: eq(techSalesAttachments.techSalesRfqId, rfqId), + columns: { id: true, filePath: true } + }); + + for (const attachment of attachments) { + await tx.delete(techSalesAttachments) + .where(eq(techSalesAttachments.id, attachment.id)); + + // 파일 시스템에서 파일 삭제 + try { + deleteFile(attachment.filePath); + } catch (fileError) { + console.warn("파일 삭제 실패:", fileError); + } + } + + // RFQ 삭제 + const deletedRfq = await tx.delete(techSalesRfqs) + .where(eq(techSalesRfqs.id, rfqId)) + .returning(); + + // 캐시 무효화 + revalidateTag("techSalesRfqs"); + revalidateTag(`techSalesRfq-${rfqId}`); + revalidatePath(getTechSalesRevalidationPath(rfq.rfqType || "SHIP")); + + return { data: deletedRfq[0], error: null }; + }); + } catch (err) { + console.error("기술영업 RFQ 삭제 오류:", err); + return { data: null, error: getErrorMessage(err) }; + } +} + /** * 기술영업 RFQ 첨부파일 일괄 처리 (업로드 + 삭제) */ @@ -2377,6 +2439,7 @@ export async function createTechSalesShipRfq(input: { itemIds: number[]; // 조선 아이템 ID 배열 dueDate: Date; description?: string; + remark?: string; createdBy: number; }) { unstable_noStore(); @@ -2401,6 +2464,7 @@ export async function createTechSalesShipRfq(input: { rfqCode: rfqCode[0], biddingProjectId: input.biddingProjectId, description: input.description, + remark: input.remark, dueDate: input.dueDate, status: "RFQ Created", rfqType: "SHIP", @@ -2440,6 +2504,7 @@ export async function createTechSalesHullRfq(input: { itemIds: number[]; // Hull 아이템 ID 배열 dueDate: Date; description?: string; + remark?: string; createdBy: number; }) { unstable_noStore(); @@ -2466,6 +2531,7 @@ export async function createTechSalesHullRfq(input: { rfqCode: hullRfqCode[0], biddingProjectId: input.biddingProjectId, description: input.description, + remark: input.remark, dueDate: input.dueDate, status: "RFQ Created", rfqType: "HULL", @@ -2505,6 +2571,7 @@ export async function createTechSalesTopRfq(input: { itemIds: number[]; // TOP 아이템 ID 배열 dueDate: Date; description?: string; + remark?: string; createdBy: number; }) { unstable_noStore(); @@ -2531,6 +2598,7 @@ export async function createTechSalesTopRfq(input: { rfqCode: topRfqCode[0], biddingProjectId: input.biddingProjectId, description: input.description, + remark: input.remark, dueDate: input.dueDate, status: "RFQ Created", rfqType: "TOP", @@ -3958,6 +4026,178 @@ export async function getTechVendorsContacts(vendorIds: number[]) { } } +/** + * RFQ와 연결된 벤더의 contact 정보 조회 (techSalesContactPossibleItems 기준) + */ +export async function getTechVendorsContactsWithPossibleItems(vendorIds: number[], rfqId?: number) { + unstable_noStore(); + try { + // RFQ ID가 있으면 해당 RFQ의 아이템들을 먼저 조회 + let rfqItems: number[] = []; + if (rfqId) { + const rfqItemResults = await db + .select({ + id: techSalesRfqItems.id, + }) + .from(techSalesRfqItems) + .where(eq(techSalesRfqItems.rfqId, rfqId)); + + rfqItems = rfqItemResults.map(item => item.id); + } + + // 벤더와 contact 정보 조회 (기존과 동일) + const contactsWithVendor = await db + .select({ + contactId: techVendorContacts.id, + contactName: techVendorContacts.contactName, + contactPosition: techVendorContacts.contactPosition, + contactTitle: techVendorContacts.contactTitle, + contactEmail: techVendorContacts.contactEmail, + contactPhone: techVendorContacts.contactPhone, + isPrimary: techVendorContacts.isPrimary, + vendorId: techVendorContacts.vendorId, + vendorName: techVendors.vendorName, + vendorCode: techVendors.vendorCode + }) + .from(techVendorContacts) + .leftJoin(techVendors, eq(techVendorContacts.vendorId, techVendors.id)) + .where(inArray(techVendorContacts.vendorId, vendorIds)) + .orderBy( + asc(techVendorContacts.vendorId), + desc(techVendorContacts.isPrimary), + asc(techVendorContacts.contactName) + ); + + // techSalesContactPossibleItems 테이블에서 RFQ 아이템과 연결된 담당자들 조회 + let selectedContactIds: Set = new Set(); + if (rfqId && vendorIds.length > 0) { + console.log(`[DEBUG] RFQ ID: ${rfqId}, Vendor IDs: ${vendorIds.join(', ')}`); + + // 선택된 벤더들이 가진 possible items 중 현재 RFQ의 아이템들과 매칭되는 것들을 찾기 + // 1. 먼저 현재 RFQ의 아이템들을 조회 + const rfqItems = await db + .select({ + id: techSalesRfqItems.id, + itemShipbuildingId: techSalesRfqItems.itemShipbuildingId, + itemOffshoreTopId: techSalesRfqItems.itemOffshoreTopId, + itemOffshoreHullId: techSalesRfqItems.itemOffshoreHullId, + itemType: techSalesRfqItems.itemType, + }) + .from(techSalesRfqItems) + .where(eq(techSalesRfqItems.rfqId, rfqId)); + + console.log(`[DEBUG] RFQ Items count: ${rfqItems.length}`); + rfqItems.forEach(item => { + console.log(`[DEBUG] RFQ Item: ${item.itemType} - ${item.itemShipbuildingId || item.itemOffshoreTopId || item.itemOffshoreHullId}`); + }); + + if (rfqItems.length > 0) { + // 2. 선택된 벤더들이 가진 possible items 조회 + const vendorPossibleItems = await db + .select({ + id: techVendorPossibleItems.id, + vendorId: techVendorPossibleItems.vendorId, + shipbuildingItemId: techVendorPossibleItems.shipbuildingItemId, + offshoreTopItemId: techVendorPossibleItems.offshoreTopItemId, + offshoreHullItemId: techVendorPossibleItems.offshoreHullItemId, + }) + .from(techVendorPossibleItems) + .where(inArray(techVendorPossibleItems.vendorId, vendorIds)); + + console.log(`[DEBUG] Vendor Possible Items count: ${vendorPossibleItems.length}`); + vendorPossibleItems.forEach(item => { + console.log(`[DEBUG] Vendor Item ${item.id}: ${item.shipbuildingItemId || item.offshoreTopItemId || item.offshoreHullItemId} (Vendor: ${item.vendorId})`); + }); + + // 3. RFQ 아이템과 벤더 possible items 간 매칭 + const matchedPossibleItemIds: number[] = []; + + for (const rfqItem of rfqItems) { + for (const vendorItem of vendorPossibleItems) { + // RFQ 아이템 타입별로 매칭 확인 + if (rfqItem.itemType === "SHIP" && rfqItem.itemShipbuildingId === vendorItem.shipbuildingItemId) { + matchedPossibleItemIds.push(vendorItem.id); + console.log(`[DEBUG] Matched SHIP: RFQ Item ${rfqItem.id} -> Vendor Item ${vendorItem.id}`); + } else if (rfqItem.itemType === "TOP" && rfqItem.itemOffshoreTopId === vendorItem.offshoreTopItemId) { + matchedPossibleItemIds.push(vendorItem.id); + console.log(`[DEBUG] Matched TOP: RFQ Item ${rfqItem.id} -> Vendor Item ${vendorItem.id}`); + } else if (rfqItem.itemType === "HULL" && rfqItem.itemOffshoreHullId === vendorItem.offshoreHullItemId) { + matchedPossibleItemIds.push(vendorItem.id); + console.log(`[DEBUG] Matched HULL: RFQ Item ${rfqItem.id} -> Vendor Item ${vendorItem.id}`); + } + } + } + + console.log(`[DEBUG] Matched Possible Item IDs: ${matchedPossibleItemIds.join(', ')}`); + + if (matchedPossibleItemIds.length > 0) { + // 4. 매칭된 possible items와 연결된 contact들 조회 + const selectedContacts = await db + .select({ + contactId: techSalesContactPossibleItems.contactId, + }) + .from(techSalesContactPossibleItems) + .where(inArray(techSalesContactPossibleItems.vendorPossibleItemId, matchedPossibleItemIds)); + + console.log(`[DEBUG] Selected Contacts count: ${selectedContacts.length}`); + selectedContacts.forEach(contact => { + console.log(`[DEBUG] Selected Contact ID: ${contact.contactId}`); + }); + + selectedContactIds = new Set(selectedContacts.map(sc => sc.contactId)); + } + } + } + + // 벤더별로 그룹화하고 선택 상태 추가 + const contactsByVendor = contactsWithVendor.reduce((acc, row) => { + const vendorId = row.vendorId; + if (!acc[vendorId]) { + acc[vendorId] = { + vendor: { + id: vendorId, + vendorName: row.vendorName || '', + vendorCode: row.vendorCode || '' + }, + contacts: [] + }; + } + acc[vendorId].contacts.push({ + id: row.contactId, + contactName: row.contactName, + contactPosition: row.contactPosition, + contactTitle: row.contactTitle, + contactEmail: row.contactEmail, + contactPhone: row.contactPhone, + isPrimary: row.isPrimary, + isSelectedForRfq: selectedContactIds.has(row.contactId) // RFQ 아이템과 연결되어 있는지 여부 + }); + return acc; + }, {} as Record; + }>); + + return { data: contactsByVendor, error: null }; + } catch (err) { + console.error("벤더 contact 조회 오류:", err); + return { data: {}, error: getErrorMessage(err) }; + } +} + /** * quotation별 발송된 담당자 정보 조회 */ diff --git a/lib/techsales-rfq/table/create-rfq-hull-dialog.tsx b/lib/techsales-rfq/table/create-rfq-hull-dialog.tsx index 5870c785..d79205b6 100644 --- a/lib/techsales-rfq/table/create-rfq-hull-dialog.tsx +++ b/lib/techsales-rfq/table/create-rfq-hull-dialog.tsx @@ -74,6 +74,7 @@ const createHullRfqSchema = z.object({ required_error: "마감일을 선택해주세요.", }), description: z.string().optional(), + remark: z.string().optional(), }) // 폼 데이터 타입 @@ -194,6 +195,7 @@ export function CreateHullRfqDialog({ onCreated }: CreateHullRfqDialogProps) { itemIds: [], dueDate: undefined, description: "", + remark: "", } }) @@ -261,6 +263,7 @@ export function CreateHullRfqDialog({ onCreated }: CreateHullRfqDialogProps) { itemIds: data.itemIds, dueDate: data.dueDate, description: data.description, + remark: data.remark, createdBy: Number(session.user.id), }) @@ -379,6 +382,25 @@ export function CreateHullRfqDialog({ onCreated }: CreateHullRfqDialogProps) { )} /> + + {/* 비고 */} + ( + + RFQ Context + + + + + + )} + /> + {/* 마감일 설정 */} { - return shipTypes + return shipTypes.filter(shipType => shipType !== "OPTION") }, [shipTypes]) // 프로젝트 선택 처리 @@ -274,6 +276,7 @@ export function CreateShipRfqDialog({ onCreated }: CreateShipRfqDialogProps) { itemIds: data.itemIds, dueDate: data.dueDate, description: data.description, + remark: data.remark, createdBy: Number(session.user.id), }) @@ -396,7 +399,25 @@ export function CreateShipRfqDialog({ onCreated }: CreateShipRfqDialogProps) { )} /> - + + {/* 비고 */} + ( + + RFQ Context + + + + + + )} + /> + {/* 선종 선택 */} diff --git a/lib/techsales-rfq/table/create-rfq-top-dialog.tsx b/lib/techsales-rfq/table/create-rfq-top-dialog.tsx index 49fb35ca..d7171c09 100644 --- a/lib/techsales-rfq/table/create-rfq-top-dialog.tsx +++ b/lib/techsales-rfq/table/create-rfq-top-dialog.tsx @@ -66,6 +66,7 @@ const createTopRfqSchema = z.object({ required_error: "마감일을 선택해주세요.", }), description: z.string().optional(), + remark: z.string().optional(), }) // 폼 데이터 타입 @@ -185,6 +186,7 @@ export function CreateTopRfqDialog({ onCreated }: CreateTopRfqDialogProps) { itemIds: [], dueDate: undefined, description: "", + remark: "", } }) @@ -252,6 +254,7 @@ export function CreateTopRfqDialog({ onCreated }: CreateTopRfqDialogProps) { itemIds: data.itemIds, dueDate: data.dueDate, description: data.description, + remark: data.remark, createdBy: Number(session.user.id), }) @@ -371,6 +374,25 @@ export function CreateTopRfqDialog({ onCreated }: CreateTopRfqDialogProps) { )} /> + + {/* 비고 */} + ( + + RFQ Context + + + + + + )} + /> + {/* 마감일 설정 */} row.vendorId).filter(Boolean) as number[]} + rfqId={selectedRfqId} onSendRfq={handleSendRfqWithContacts} /> diff --git a/lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx b/lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx index 031f4aa2..d83394bb 100644 --- a/lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx +++ b/lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx @@ -25,6 +25,7 @@ interface VendorContact { contactEmail: string contactPhone: string | null isPrimary: boolean + isSelectedForRfq?: boolean // RFQ의 아이템과 연결되어 있는지 여부 } interface VendorWithContacts { @@ -47,6 +48,7 @@ interface VendorContactSelectionDialogProps { open: boolean onOpenChange: (open: boolean) => void vendorIds: number[] + rfqId?: number // RFQ ID 추가 onSendRfq: (selectedContacts: SelectedContact[]) => Promise } @@ -54,6 +56,7 @@ export function VendorContactSelectionDialog({ open, onOpenChange, vendorIds, + rfqId, onSendRfq }: VendorContactSelectionDialogProps) { const [vendorsWithContacts, setVendorsWithContacts] = useState>({}) @@ -80,38 +83,41 @@ export function VendorContactSelectionDialog({ const loadVendorsContacts = useCallback(async () => { try { setIsLoading(true) - const { getTechVendorsContacts } = await import("@/lib/techsales-rfq/service") - - const result = await getTechVendorsContacts(vendorIds) - + const { getTechVendorsContactsWithPossibleItems } = await import("@/lib/techsales-rfq/service") + + const result = await getTechVendorsContactsWithPossibleItems(vendorIds, rfqId) + if (result.error) { toast.error(result.error) return } - + setVendorsWithContacts(result.data) - - // 기본 선택: 모든 contact 선택 + + // 기본 선택: techSalesContactPossibleItems 테이블을 기준으로 선택 const defaultSelected: SelectedContact[] = [] Object.values(result.data).forEach(vendorData => { vendorData.contacts.forEach(contact => { - defaultSelected.push({ - vendorId: vendorData.vendor.id, - contactId: contact.id, - contactEmail: contact.contactEmail, - contactName: contact.contactName - }) + // 해당 담당자가 선택된 아이템과 연결되어 있다면 우선 선택 + if (contact.isSelectedForRfq) { + defaultSelected.push({ + vendorId: vendorData.vendor.id, + contactId: contact.id, + contactEmail: contact.contactEmail, + contactName: contact.contactName + }) + } }) }) setSelectedContacts(defaultSelected) - + } catch (error) { console.error("벤더 contact 조회 오류:", error) toast.error("벤더 연락처를 불러오는 중 오류가 발생했습니다") } finally { setIsLoading(false) } - }, [vendorIds]) + }, [vendorIds, rfqId]) // contact 선택/해제 핸들러 const handleContactToggle = (vendorId: number, contact: VendorContact) => { @@ -201,7 +207,7 @@ export function VendorContactSelectionDialog({ RFQ 발송 대상 선택 - 각 벤더의 연락처를 선택하여 RFQ를 발송하세요. 기본적으로 모든 연락처가 선택되어 있습니다. + RFQ에 포함된 자재와 연결된 담당자들이 우선 선택됩니다. 해당 벤더에 연결된 담당자가 없다면 수동으로 선택해주세요. @@ -227,7 +233,8 @@ export function VendorContactSelectionDialog({ ) : ( Object.entries(vendorsWithContacts).map(([vendorId, vendorData]) => { const selectionState = getVendorSelectionState(Number(vendorId), vendorData) - + const hasSelectedContacts = vendorData.contacts.some(contact => contact.isSelectedForRfq) + return (
@@ -251,6 +258,11 @@ export function VendorContactSelectionDialog({ 코드: {vendorData.vendor.vendorCode}

)} + {!hasSelectedContacts && ( +

+ ※ 해당 벤더의 담당자가 RFQ 자재와 연결되지 않았습니다. 수동으로 선택해주세요. +

+ )}
diff --git a/lib/techsales-rfq/table/rfq-table-column.tsx b/lib/techsales-rfq/table/rfq-table-column.tsx index 2bc5b5b4..1ac59d8b 100644 --- a/lib/techsales-rfq/table/rfq-table-column.tsx +++ b/lib/techsales-rfq/table/rfq-table-column.tsx @@ -95,6 +95,31 @@ export function getColumns({ enableResizing: true, size: 200, }, + { + accessorKey: "remark", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const remark = row.getValue("remark") as string | null; + return ( +
+ {remark ? ( + + {remark} + + ) : ( + - + )} +
+ ); + }, + meta: { + excelHeader: "비고" + }, + enableResizing: true, + size: 200, + }, { accessorKey: "projNm", header: ({ column }) => ( @@ -434,6 +459,12 @@ export function getColumns({ setRowAction({ row, type: "update" })}> 수정하기 + setRowAction({ row, type: "delete" })} + className="text-destructive focus:text-destructive" + > + 삭제하기 + ) diff --git a/lib/techsales-rfq/table/rfq-table.tsx b/lib/techsales-rfq/table/rfq-table.tsx index e1e511c8..889b87b3 100644 --- a/lib/techsales-rfq/table/rfq-table.tsx +++ b/lib/techsales-rfq/table/rfq-table.tsx @@ -30,6 +30,17 @@ import { RFQFilterSheet } from "./rfq-filter-sheet" import { TechSalesRfqAttachmentsSheet, ExistingTechSalesAttachment } from "./tech-sales-rfq-attachments-sheet" import { RfqItemsViewDialog } from "./rfq-items-view-dialog" import UpdateSheet from "./update-rfq-sheet" +import { deleteTechSalesRfq } from "@/lib/techsales-rfq/service" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" // 기본적인 RFQ 타입 정의 (repository selectTechSalesRfqsWithJoin 반환 타입에 맞춤) export interface TechSalesRfq { id: number @@ -101,6 +112,11 @@ export function RFQListTable({ // 아이템 다이얼로그 상태 const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false) const [selectedRfqForItems, setSelectedRfqForItems] = React.useState(null) + + // 삭제 다이얼로그 상태 + const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false) + const [rfqToDelete, setRfqToDelete] = React.useState(null) + const [isDeleting, setIsDeleting] = React.useState(false) // 패널 collapse 상태 const [panelHeight, setPanelHeight] = React.useState(55) @@ -255,7 +271,9 @@ export function RFQListTable({ setUpdateSheetOpen(true); break; case "delete": - console.log("Delete rfq:", rowAction.row.original) + // 삭제 다이얼로그 열기 + setRfqToDelete(rowAction.row.original as TechSalesRfq) + setDeleteDialogOpen(true) break; } setRowAction(null) @@ -334,6 +352,38 @@ export function RFQListTable({ setItemsDialogOpen(true) }, []) + // RFQ 삭제 처리 함수 + const handleDeleteRfq = React.useCallback(async () => { + if (!rfqToDelete) return + + try { + setIsDeleting(true) + + const result = await deleteTechSalesRfq(rfqToDelete.id) + + if (result.error) { + toast.error(`삭제 실패: ${result.error}`) + return + } + + toast.success("RFQ가 성공적으로 삭제되었습니다.") + + // 선택된 RFQ 초기화 + setSelectedRfq(null) + setRfqToDelete(null) + setDeleteDialogOpen(false) + + // 테이블 새로고침을 위해 페이지 리로드 또는 데이터 재요청 필요 + // 현재는 캐시 무효화가 되어 있으므로 자연스럽게 업데이트됨 + + } catch (error) { + console.error("RFQ 삭제 오류:", error) + toast.error("삭제 중 오류가 발생했습니다.") + } finally { + setIsDeleting(false) + } + }, [rfqToDelete]) + const columns = React.useMemo( () => getColumns({ setRowAction, @@ -647,6 +697,30 @@ export function RFQListTable({ }} /> )} + + {/* RFQ 삭제 다이얼로그 */} + + + + RFQ 삭제 확인 + + 정말로 "{rfqToDelete?.rfqCode || rfqToDelete?.description}" RFQ를 삭제하시겠습니까? +
+ 주의: 이 작업은 되돌릴 수 없습니다. RFQ와 관련된 모든 데이터가 삭제됩니다. +
+
+ + 취소 + + {isDeleting ? "삭제 중..." : "삭제하기"} + + +
+
) } \ No newline at end of file diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx index 46b14f46..aabe7a64 100644 --- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx +++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx @@ -27,23 +27,24 @@ interface QuotationWithRfqCode extends TechSalesVendorQuotations { materialCode?: string; dueDate?: Date; rfqStatus?: string; - + // 아이템 정보 itemName?: string; itemCount?: number; - + // 프로젝트 정보 projNm?: string; pspid?: string; sector?: string; - + // RFQ 정보 description?: string; - + remark?: string; + // 벤더 정보 vendorName?: string; vendorCode?: string; - + // 사용자 정보 createdByName?: string | null; updatedByName?: string | null; @@ -223,6 +224,33 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog, open enableSorting: true, enableHiding: true, }, + { + accessorKey: "remark", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const remark = row.getValue("remark") as string; + return ( +
+ + + + + {remark || "N/A"} + + + +

{remark || "N/A"}

+
+
+
+
+ ); + }, + enableSorting: true, + enableHiding: true, + }, { accessorKey: "projNm", header: ({ column }) => ( diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx index 5bb219bf..214e2b89 100644 --- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx +++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx @@ -40,6 +40,7 @@ interface QuotationWithRfqCode extends TechSalesVendorQuotations { itemName?: string | null; projNm?: string | null; description?: string | null; + remark?: string | null; attachmentCount?: number; itemCount?: number; pspid?: string | null; -- cgit v1.2.3