summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/techsales-rfq/service.ts')
-rw-r--r--lib/techsales-rfq/service.ts350
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,