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/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 +++++++++++++++++++++- 7 files changed, 204 insertions(+), 21 deletions(-) (limited to 'lib/techsales-rfq/table') 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 -- cgit v1.2.3