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 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) (limited to 'lib/techsales-rfq/service.ts') 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별 발송된 담당자 정보 조회 */ -- cgit v1.2.3