diff options
Diffstat (limited to 'lib/techsales-rfq/service.ts')
| -rw-r--r-- | lib/techsales-rfq/service.ts | 240 |
1 files changed, 240 insertions, 0 deletions
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 @@ -1720,6 +1720,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 첨부파일 일괄 처리 (업로드 + 삭제)
*/
export async function processTechSalesRfqAttachments(params: {
@@ -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",
@@ -3959,6 +4027,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<number> = 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<number, {
+ vendor: {
+ id: number;
+ vendorName: string;
+ vendorCode: string | null;
+ };
+ contacts: Array<{
+ id: number;
+ contactName: string;
+ contactPosition: string | null;
+ contactTitle: string | null;
+ contactEmail: string;
+ contactPhone: string | null;
+ isPrimary: boolean;
+ isSelectedForRfq?: boolean;
+ }>;
+ }>);
+
+ return { data: contactsByVendor, error: null };
+ } catch (err) {
+ console.error("벤더 contact 조회 오류:", err);
+ return { data: {}, error: getErrorMessage(err) };
+ }
+}
+
+/**
* quotation별 발송된 담당자 정보 조회
*/
export async function getQuotationContacts(quotationId: number) {
|
