diff options
Diffstat (limited to 'lib/techsales-rfq/service.ts')
| -rw-r--r-- | lib/techsales-rfq/service.ts | 350 |
1 files changed, 276 insertions, 74 deletions
diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts index 735fcf68..26117452 100644 --- a/lib/techsales-rfq/service.ts +++ b/lib/techsales-rfq/service.ts @@ -6,7 +6,7 @@ import { techSalesRfqs, techSalesVendorQuotations, techSalesAttachments, - items, + itemShipbuilding, users, techSalesRfqComments } from "@/db/schema"; @@ -186,7 +186,9 @@ async function generateRfqCodes(tx: any, count: number, year?: number): Promise< export async function createTechSalesRfq(input: { // 프로젝트 관련 biddingProjectId: number; - // 자재 관련 (자재그룹 코드들) + // 조선 아이템 관련 + itemShipbuildingId: number; + // 자재 관련 (자재그룹 코드들을 CSV로) materialGroupCodes: string[]; // 기본 정보 dueDate?: Date; @@ -194,8 +196,17 @@ export async function createTechSalesRfq(input: { createdBy: number; }) { unstable_noStore(); + console.log('🔍 createTechSalesRfq 호출됨:', { + biddingProjectId: input.biddingProjectId, + itemShipbuildingId: input.itemShipbuildingId, + materialGroupCodes: input.materialGroupCodes, + dueDate: input.dueDate, + remark: input.remark, + createdBy: input.createdBy + }); + try { - const results: typeof techSalesRfqs.$inferSelect[] = []; + let result: typeof techSalesRfqs.$inferSelect | undefined; // 트랜잭션으로 처리 await db.transaction(async (tx) => { @@ -250,58 +261,52 @@ export async function createTechSalesRfq(input: { post1: series.post1 || undefined, })); - // 각 자재그룹 코드별로 RFQ 생성 - for (const materialCode of input.materialGroupCodes) { - // RFQ 코드 생성 - const rfqCode = await generateRfqCodes(tx, 1); - - // 기본 due date 설정 (7일 후) - const dueDate = input.dueDate || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); - - // 기존 item 확인 또는 새로 생성 - let itemId: number; - const existingItem = await tx.query.items.findFirst({ - where: (items, { eq }) => eq(items.itemCode, materialCode), - columns: { id: true } - }); - - if (existingItem) { - // 기존 item 사용 - itemId = existingItem.id; - } else { - // 새 item 생성 - const [newItem] = await tx.insert(items).values({ - itemCode: materialCode, - itemName: `자재그룹 ${materialCode}`, - description: `기술영업 자재그룹`, - }).returning(); - itemId = newItem.id; - } - - // 새 기술영업 RFQ 작성 (스냅샷 포함) - const [newRfq] = await tx.insert(techSalesRfqs).values({ - rfqCode: rfqCode[0], - itemId: itemId, - biddingProjectId: input.biddingProjectId, - materialCode, - dueDate, - remark: input.remark, - createdBy: input.createdBy, - updatedBy: input.createdBy, - // 스냅샷 데이터 추가 - projectSnapshot, - seriesSnapshot, - }).returning(); - - results.push(newRfq); + // RFQ 코드 생성 + const rfqCode = await generateRfqCodes(tx, 1); + + // 기본 due date 설정 (7일 후) + const dueDate = input.dueDate || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); + + // itemShipbuildingId 유효성 검증 + console.log('🔍 itemShipbuildingId 검증:', input.itemShipbuildingId); + const existingItemShipbuilding = await tx.query.itemShipbuilding.findFirst({ + where: (itemShipbuilding, { eq }) => eq(itemShipbuilding.id, input.itemShipbuildingId), + columns: { id: true, itemCode: true, itemList: true } + }); + + if (!existingItemShipbuilding) { + throw new Error(`itemShipbuildingId ${input.itemShipbuildingId}에 해당하는 itemShipbuilding 레코드를 찾을 수 없습니다.`); } + + console.log('✅ itemShipbuilding 찾음:', existingItemShipbuilding); + + // 새 기술영업 RFQ 작성 (스냅샷 포함) + const [newRfq] = await tx.insert(techSalesRfqs).values({ + rfqCode: rfqCode[0], + itemShipbuildingId: input.itemShipbuildingId, + biddingProjectId: input.biddingProjectId, + materialCode: input.materialGroupCodes.join(','), // 모든 materialCode를 CSV로 저장 + dueDate, + remark: input.remark, + createdBy: input.createdBy, + updatedBy: input.createdBy, + // 스냅샷 데이터 추가 + projectSnapshot, + seriesSnapshot, + }).returning(); + + result = newRfq; }); // 캐시 무효화 revalidateTag("techSalesRfqs"); revalidatePath("/evcp/budgetary-tech-sales-ship"); - return { data: results, error: null }; + if (!result) { + throw new Error(`RFQ 생성에 실패했습니다. 입력값: ${JSON.stringify(input)}`); + } + + return { data: [result], error: null }; } catch (err) { console.error("Error creating RFQ:", err); return { data: null, error: getErrorMessage(err) }; @@ -715,13 +720,14 @@ export async function addVendorToTechSalesRfq(input: { vendorId: input.vendorId, status: "Draft", totalPrice: "0", - currency: "USD", + currency: null, createdBy: input.createdBy, updatedBy: input.createdBy, }) .returning(); // 캐시 무효화 + revalidateTag("techSalesRfqs"); revalidateTag("techSalesVendorQuotations"); revalidateTag(`techSalesRfq-${input.rfqId}`); revalidateTag(`vendor-${input.vendorId}-quotations`); @@ -816,6 +822,7 @@ export async function addVendorsToTechSalesRfq(input: { }); // 캐시 무효화 추가 + revalidateTag("techSalesRfqs"); revalidateTag("techSalesVendorQuotations"); revalidateTag(`techSalesRfq-${input.rfqId}`); revalidatePath("/evcp/budgetary-tech-sales-ship"); @@ -1033,11 +1040,11 @@ export async function sendTechSalesRfqToVendors(input: { seriesSnapshot: true, }, with: { - item: { + itemShipbuilding: { columns: { id: true, itemCode: true, - itemName: true, + itemList: true, } }, biddingProject: { @@ -1186,7 +1193,7 @@ export async function sendTechSalesRfqToVendors(input: { rfq: { id: rfq.id, code: rfq.rfqCode, - title: rfq.item?.itemName || '', + title: rfq.itemShipbuilding?.itemList || '', projectCode: rfq.biddingProject?.pspid || '', projectName: rfq.biddingProject?.projNm || '', description: rfq.remark || '', @@ -1237,8 +1244,8 @@ export async function sendTechSalesRfqToVendors(input: { await sendEmail({ to: vendorEmailsString, subject: isResend - ? `[기술영업 RFQ 재전송] ${rfq.rfqCode} - ${rfq.item?.itemName || '견적 요청'} ${emailContext.versionInfo}` - : `[기술영업 RFQ] ${rfq.rfqCode} - ${rfq.item?.itemName || '견적 요청'}`, + ? `[기술영업 RFQ 재전송] ${rfq.rfqCode} - ${rfq.itemShipbuilding?.itemList || '견적 요청'} ${emailContext.versionInfo}` + : `[기술영업 RFQ] ${rfq.rfqCode} - ${rfq.itemShipbuilding?.itemList || '견적 요청'}`, template: 'tech-sales-rfq-invite-ko', // 기술영업용 템플릿 context: emailContext, cc: sender.email, // 발신자를 CC에 추가 @@ -1278,7 +1285,7 @@ export async function getTechSalesVendorQuotation(quotationId: number) { with: { rfq: { with: { - item: true, + itemShipbuilding: true, biddingProject: true, createdByUser: { columns: { @@ -1466,6 +1473,32 @@ export async function getVendorQuotations(input: { return unstable_cache( async () => { try { + // 디버깅 로그 추가 + console.log('🔍 [getVendorQuotations] 받은 파라미터:'); + console.log(' 📊 기본 정보:', { + vendorId, + page: input.page, + perPage: input.perPage, + }); + console.log(' 🔧 정렬 정보:', { + sort: input.sort, + sortLength: input.sort?.length, + sortDetails: input.sort?.map(s => `${s.id}:${s.desc ? 'DESC' : 'ASC'}`) + }); + console.log(' 🔍 필터 정보:', { + filters: input.filters, + filtersLength: input.filters?.length, + joinOperator: input.joinOperator, + basicFilters: input.basicFilters, + basicFiltersLength: input.basicFilters?.length, + basicJoinOperator: input.basicJoinOperator + }); + console.log(' 🔎 검색 정보:', { + search: input.search, + from: input.from, + to: input.to + }); + const { page, perPage, sort, filters = [], search = "", from = "", to = "" } = input; const offset = (page - 1) * perPage; const limit = perPage; @@ -1495,13 +1528,125 @@ export async function getVendorQuotations(input: { // 고급 필터 처리 if (filters.length > 0) { - const filterWhere = filterColumns({ - table: techSalesVendorQuotations, - filters: filters as Filter<typeof techSalesVendorQuotations>[], - joinOperator: input.joinOperator || "and", - }); - if (filterWhere) { - baseConditions.push(filterWhere); + // 조인된 테이블의 컬럼들을 분리 + const joinedColumnFilters = []; + const baseTableFilters = []; + + for (const filter of filters) { + const filterId = filter.id as string; + + // 조인된 컬럼들인지 확인 + if (['rfqCode', 'materialCode', 'dueDate', 'rfqStatus', 'itemName', 'projNm', 'pspid', 'sector', 'kunnrNm'].includes(filterId)) { + joinedColumnFilters.push(filter); + } else { + baseTableFilters.push(filter); + } + } + + // 기본 테이블 필터 처리 + if (baseTableFilters.length > 0) { + const filterWhere = filterColumns({ + table: techSalesVendorQuotations, + filters: baseTableFilters as Filter<typeof techSalesVendorQuotations>[], + joinOperator: input.joinOperator || "and", + }); + if (filterWhere) { + baseConditions.push(filterWhere); + } + } + + // 조인된 컬럼 필터 처리 + if (joinedColumnFilters.length > 0) { + const joinedConditions = joinedColumnFilters.map(filter => { + const filterId = filter.id as string; + const value = filter.value; + const operator = filter.operator || 'eq'; + + switch (filterId) { + case 'rfqCode': + if (operator === 'iLike') { + return ilike(techSalesRfqs.rfqCode, `%${value}%`); + } else if (operator === 'eq') { + return eq(techSalesRfqs.rfqCode, value as string); + } + break; + + case 'materialCode': + if (operator === 'iLike') { + return ilike(techSalesRfqs.materialCode, `%${value}%`); + } else if (operator === 'eq') { + return eq(techSalesRfqs.materialCode, value as string); + } + break; + + case 'dueDate': + if (operator === 'eq') { + return eq(techSalesRfqs.dueDate, new Date(value as string)); + } else if (operator === 'gte') { + return sql`${techSalesRfqs.dueDate} >= ${new Date(value as string)}`; + } else if (operator === 'lte') { + return sql`${techSalesRfqs.dueDate} <= ${new Date(value as string)}`; + } + break; + + case 'rfqStatus': + if (Array.isArray(value) && value.length > 0) { + return sql`${techSalesRfqs.status} IN (${value.map(v => `'${v}'`).join(',')})`; + } else if (typeof value === 'string') { + return eq(techSalesRfqs.status, value); + } + break; + + case 'itemName': + if (operator === 'iLike') { + return ilike(itemShipbuilding.itemList, `%${value}%`); + } else if (operator === 'eq') { + return eq(itemShipbuilding.itemList, value as string); + } + break; + + case 'projNm': + if (operator === 'iLike') { + return sql`${techSalesRfqs.projectSnapshot}->>'projNm' ILIKE ${'%' + value + '%'}`; + } else if (operator === 'eq') { + return sql`${techSalesRfqs.projectSnapshot}->>'projNm' = ${value}`; + } + break; + + case 'pspid': + if (operator === 'iLike') { + return sql`${techSalesRfqs.projectSnapshot}->>'pspid' ILIKE ${'%' + value + '%'}`; + } else if (operator === 'eq') { + return sql`${techSalesRfqs.projectSnapshot}->>'pspid' = ${value}`; + } + break; + + case 'sector': + if (operator === 'iLike') { + return sql`${techSalesRfqs.projectSnapshot}->>'sector' ILIKE ${'%' + value + '%'}`; + } else if (operator === 'eq') { + return sql`${techSalesRfqs.projectSnapshot}->>'sector' = ${value}`; + } + break; + + case 'kunnrNm': + if (operator === 'iLike') { + return sql`${techSalesRfqs.projectSnapshot}->>'kunnrNm' ILIKE ${'%' + value + '%'}`; + } else if (operator === 'eq') { + return sql`${techSalesRfqs.projectSnapshot}->>'kunnrNm' = ${value}`; + } + break; + } + return undefined; + }).filter(Boolean); + + if (joinedConditions.length > 0) { + const joinOperator = input.joinOperator || "and"; + const combinedCondition = joinOperator === "and" + ? and(...joinedConditions) + : or(...joinedConditions); + baseConditions.push(combinedCondition); + } } } @@ -1532,6 +1677,59 @@ export async function getVendorQuotations(input: { return item.desc ? desc(techSalesVendorQuotations.createdAt) : techSalesVendorQuotations.createdAt; case 'updatedAt': return item.desc ? desc(techSalesVendorQuotations.updatedAt) : techSalesVendorQuotations.updatedAt; + case 'createdBy': + return item.desc ? desc(techSalesVendorQuotations.createdBy) : techSalesVendorQuotations.createdBy; + case 'updatedBy': + return item.desc ? desc(techSalesVendorQuotations.updatedBy) : techSalesVendorQuotations.updatedBy; + case 'quotationCode': + return item.desc ? desc(techSalesVendorQuotations.quotationCode) : techSalesVendorQuotations.quotationCode; + case 'quotationVersion': + return item.desc ? desc(techSalesVendorQuotations.quotationVersion) : techSalesVendorQuotations.quotationVersion; + case 'rejectionReason': + return item.desc ? desc(techSalesVendorQuotations.rejectionReason) : techSalesVendorQuotations.rejectionReason; + case 'acceptedAt': + return item.desc ? desc(techSalesVendorQuotations.acceptedAt) : techSalesVendorQuotations.acceptedAt; + // 조인된 RFQ 정보 정렬 + case 'rfqCode': + return item.desc ? desc(techSalesRfqs.rfqCode) : techSalesRfqs.rfqCode; + case 'materialCode': + return item.desc ? desc(techSalesRfqs.materialCode) : techSalesRfqs.materialCode; + case 'dueDate': + return item.desc ? desc(techSalesRfqs.dueDate) : techSalesRfqs.dueDate; + case 'rfqStatus': + return item.desc ? desc(techSalesRfqs.status) : techSalesRfqs.status; + // 조인된 아이템 정보 정렬 + case 'itemName': + return item.desc ? desc(itemShipbuilding.itemList) : itemShipbuilding.itemList; + // JSON 필드에서 추출된 프로젝트 정보 정렬 + case 'projNm': + return item.desc ? desc(sql`${techSalesRfqs.projectSnapshot}->>'projNm'`) : sql`${techSalesRfqs.projectSnapshot}->>'projNm'`; + case 'projMsrm': + // 척수 (정수 캐스팅) + return item.desc ? desc(sql`(${techSalesRfqs.projectSnapshot}->>'projMsrm')::int`) : sql`(${techSalesRfqs.projectSnapshot}->>'projMsrm')::int`; + case 'ptypeNm': + // 선종명 + return item.desc ? desc(sql`${techSalesRfqs.projectSnapshot}->>'ptypeNm'`) : sql`${techSalesRfqs.projectSnapshot}->>'ptypeNm'`; + case 'pspid': + // 프로젝트 ID + return item.desc ? desc(sql`${techSalesRfqs.projectSnapshot}->>'pspid'`) : sql`${techSalesRfqs.projectSnapshot}->>'pspid'`; + case 'sector': + // 섹터 + return item.desc ? desc(sql`${techSalesRfqs.projectSnapshot}->>'sector'`) : sql`${techSalesRfqs.projectSnapshot}->>'sector'`; + case 'kunnrNm': + // 고객명 + return item.desc ? desc(sql`${techSalesRfqs.projectSnapshot}->>'kunnrNm'`) : sql`${techSalesRfqs.projectSnapshot}->>'kunnrNm'`; + // 계산된 필드 정렬 + case 'attachmentCount': + return item.desc ? desc(sql`( + SELECT COUNT(*) + FROM tech_sales_attachments + WHERE tech_sales_attachments.tech_sales_rfq_id = ${techSalesRfqs.id} + )`) : sql`( + SELECT COUNT(*) + FROM tech_sales_attachments + WHERE tech_sales_attachments.tech_sales_rfq_id = ${techSalesRfqs.id} + )`; default: return item.desc ? desc(techSalesVendorQuotations.updatedAt) : techSalesVendorQuotations.updatedAt; } @@ -1554,13 +1752,17 @@ export async function getVendorQuotations(input: { updatedAt: techSalesVendorQuotations.updatedAt, createdBy: techSalesVendorQuotations.createdBy, updatedBy: techSalesVendorQuotations.updatedBy, + quotationCode: techSalesVendorQuotations.quotationCode, + quotationVersion: techSalesVendorQuotations.quotationVersion, + rejectionReason: techSalesVendorQuotations.rejectionReason, + acceptedAt: techSalesVendorQuotations.acceptedAt, // RFQ 정보 rfqCode: techSalesRfqs.rfqCode, materialCode: techSalesRfqs.materialCode, dueDate: techSalesRfqs.dueDate, rfqStatus: techSalesRfqs.status, // 아이템 정보 - itemName: items.itemName, + itemName: itemShipbuilding.itemList, // 프로젝트 정보 (JSON에서 추출) projNm: sql<string>`${techSalesRfqs.projectSnapshot}->>'projNm'`, // 첨부파일 개수 @@ -1572,7 +1774,7 @@ export async function getVendorQuotations(input: { }) .from(techSalesVendorQuotations) .leftJoin(techSalesRfqs, eq(techSalesVendorQuotations.rfqId, techSalesRfqs.id)) - .leftJoin(items, eq(techSalesRfqs.itemId, items.id)) + .leftJoin(itemShipbuilding, eq(techSalesRfqs.itemShipbuildingId, itemShipbuilding.id)) .where(finalWhere) .orderBy(...orderBy) .limit(limit) @@ -1583,7 +1785,7 @@ export async function getVendorQuotations(input: { .select({ count: sql<number>`count(*)` }) .from(techSalesVendorQuotations) .leftJoin(techSalesRfqs, eq(techSalesVendorQuotations.rfqId, techSalesRfqs.id)) - .leftJoin(items, eq(techSalesRfqs.itemId, items.id)) + .leftJoin(itemShipbuilding, eq(techSalesRfqs.itemShipbuildingId, itemShipbuilding.id)) .where(finalWhere); const total = totalResult[0]?.count || 0; @@ -2092,7 +2294,7 @@ export async function sendQuotationSubmittedNotificationToVendor(quotationId: nu with: { rfq: { with: { - item: true, + itemShipbuilding: true, biddingProject: true, createdByUser: { columns: { @@ -2169,7 +2371,7 @@ export async function sendQuotationSubmittedNotificationToVendor(quotationId: nu rfq: { id: quotation.rfq.id, code: quotation.rfq.rfqCode, - title: quotation.rfq.item?.itemName || '', + title: quotation.rfq.itemShipbuilding?.itemList || '', projectCode: projectInfo.pspid || quotation.rfq.biddingProject?.pspid || '', projectName: projectInfo.projNm || quotation.rfq.biddingProject?.projNm || '', dueDate: quotation.rfq.dueDate, @@ -2202,7 +2404,7 @@ export async function sendQuotationSubmittedNotificationToVendor(quotationId: nu // 이메일 발송 await sendEmail({ to: vendorEmails, - subject: `[견적 제출 확인] ${quotation.rfq.rfqCode} - ${quotation.rfq.item?.itemName || '견적 요청'}`, + subject: `[견적 제출 확인] ${quotation.rfq.rfqCode} - ${quotation.rfq.itemShipbuilding?.itemList || '견적 요청'}`, template: 'tech-sales-quotation-submitted-vendor-ko', context: emailContext, }); @@ -2226,7 +2428,7 @@ export async function sendQuotationSubmittedNotificationToManager(quotationId: n with: { rfq: { with: { - item: true, + itemShipbuilding: true, biddingProject: true, createdByUser: { columns: { @@ -2288,7 +2490,7 @@ export async function sendQuotationSubmittedNotificationToManager(quotationId: n rfq: { id: quotation.rfq.id, code: quotation.rfq.rfqCode, - title: quotation.rfq.item?.itemName || '', + title: quotation.rfq.itemShipbuilding?.itemList || '', projectCode: projectInfo.pspid || quotation.rfq.biddingProject?.pspid || '', projectName: projectInfo.projNm || quotation.rfq.biddingProject?.projNm || '', dueDate: quotation.rfq.dueDate, @@ -2345,7 +2547,7 @@ export async function sendQuotationAcceptedNotification(quotationId: number) { with: { rfq: { with: { - item: true, + itemShipbuilding: true, biddingProject: true, createdByUser: { columns: { @@ -2422,7 +2624,7 @@ export async function sendQuotationAcceptedNotification(quotationId: number) { rfq: { id: quotation.rfq.id, code: quotation.rfq.rfqCode, - title: quotation.rfq.item?.itemName || '', + title: quotation.rfq.itemShipbuilding?.itemList || '', projectCode: projectInfo.pspid || quotation.rfq.biddingProject?.pspid || '', projectName: projectInfo.projNm || quotation.rfq.biddingProject?.projNm || '', dueDate: quotation.rfq.dueDate, @@ -2479,7 +2681,7 @@ export async function sendQuotationRejectedNotification(quotationId: number) { with: { rfq: { with: { - item: true, + itemShipbuilding: true, biddingProject: true, createdByUser: { columns: { @@ -2556,7 +2758,7 @@ export async function sendQuotationRejectedNotification(quotationId: number) { rfq: { id: quotation.rfq.id, code: quotation.rfq.rfqCode, - title: quotation.rfq.item?.itemName || '', + title: quotation.rfq.itemShipbuilding?.itemList || '', projectCode: projectInfo.pspid || quotation.rfq.biddingProject?.pspid || '', projectName: projectInfo.projNm || quotation.rfq.biddingProject?.projNm || '', dueDate: quotation.rfq.dueDate, |
