diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-09 10:32:34 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-09 10:32:34 +0000 |
| commit | c62ec046327fd388ebce04571b55910747e69a3b (patch) | |
| tree | 41ccdc4a8dea99808622f6d5d52014ac59a2d7ab /lib/rfq-last/service.ts | |
| parent | ebcec3f296d1d27943caf8a3aed26efef117cdc5 (diff) | |
(정희성, 최겸, 대표님) formatDate 변경 등
Diffstat (limited to 'lib/rfq-last/service.ts')
| -rw-r--r-- | lib/rfq-last/service.ts | 310 |
1 files changed, 306 insertions, 4 deletions
diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts index 0c75e72f..ac7104df 100644 --- a/lib/rfq-last/service.ts +++ b/lib/rfq-last/service.ts @@ -3,7 +3,7 @@ import { revalidatePath, unstable_cache, unstable_noStore } from "next/cache"; import db from "@/db/db"; -import {paymentTerms,incoterms, rfqLastVendorQuotationItems,rfqLastVendorAttachments,rfqLastVendorResponses, RfqsLastView, rfqLastAttachmentRevisions, rfqLastAttachments, rfqsLast, rfqsLastView, users, rfqPrItems, prItemsLastView ,vendors, rfqLastDetails, rfqLastVendorResponseHistory, rfqLastDetailsView} from "@/db/schema"; +import {paymentTerms,incoterms, rfqLastVendorQuotationItems,rfqLastVendorAttachments,rfqLastVendorResponses, RfqsLastView, rfqLastAttachmentRevisions, rfqLastAttachments, rfqsLast, rfqsLastView, users, rfqPrItems, prItemsLastView ,vendors, rfqLastDetails, rfqLastVendorResponseHistory, rfqLastDetailsView, vendorContacts,projects} from "@/db/schema"; import { sql, and, desc, asc, like, ilike, or, eq, SQL, count, gte, lte, isNotNull, ne, inArray } from "drizzle-orm"; import { filterColumns } from "@/lib/filter-columns"; import { GetRfqLastAttachmentsSchema, GetRfqsSchema } from "./validations"; @@ -1570,18 +1570,27 @@ export async function getRfqVendorResponses(rfqId: number) { ) .orderBy(desc(rfqLastVendorResponses.createdAt)); + if (!vendorResponsesData || vendorResponsesData.length === 0) { + return { + success: true, + data: [], + rfq: rfqData[0], + details: details, + }; + } + // 4. 각 벤더 응답별 견적 아이템 수와 첨부파일 수 계산 const vendorResponsesWithCounts = await Promise.all( vendorResponsesData.map(async (response) => { // 견적 아이템 수 조회 const itemCount = await db - .select({ count: sql`COUNT(*)::int` }) + .select({ count: count()}) .from(rfqLastVendorQuotationItems) .where(eq(rfqLastVendorQuotationItems.vendorResponseId, response.id)); // 첨부파일 수 조회 const attachmentCount = await db - .select({ count: sql`COUNT(*)::int` }) + .select({ count: count()}) .from(rfqLastVendorAttachments) .where(eq(rfqLastVendorAttachments.vendorResponseId, response.id)); @@ -1594,7 +1603,8 @@ export async function getRfqVendorResponses(rfqId: number) { ); // 5. 응답 데이터 정리 - const formattedResponses = vendorResponsesWithCounts.map(response => ({ + const formattedResponses = vendorResponsesWithCounts + .filter(response => response && response.id).map(response => ({ id: response.id, rfqsLastId: response.rfqsLastId, rfqLastDetailsId: response.rfqLastDetailsId, @@ -2311,4 +2321,296 @@ export async function getRfqVendors(rfqId: number) { export async function getRfqAttachments(rfqId: number) { const fullInfo = await getRfqFullInfo(rfqId); return fullInfo.attachments; +} + + +// RFQ 발송용 데이터 타입 +export interface RfqSendData { + rfqInfo: { + rfqCode: string; + rfqTitle: string; + rfqType: string; + projectCode?: string; + projectName?: string; + picName?: string; + picCode?: string; + picTeam?: string; + packageNo?: string; + packageName?: string; + designPicName?: string; + designTeam?: string; + materialGroup?: string; + materialGroupDesc?: string; + dueDate: Date; + quotationType?: string; + evaluationApply?: boolean; + contractType?: string; + }; + attachments: Array<{ + id: number; + attachmentType: string; + serialNo: string; + currentRevision: string; + description?: string | null; + fileName?: string | null; + fileSize?: number | null; + uploadedAt?: Date; + }>; +} + +// 선택된 벤더의 이메일 정보 조회 +export interface VendorEmailInfo { + vendorId: number; + vendorName: string; + vendorCode?: string | null; + vendorCountry?: string | null; + vendorEmail?: string | null; // vendors 테이블의 기본 이메일 + representativeEmail?: string | null; // 대표자 이메일 + contactEmails: string[]; // 영업/대표 담당자 이메일들 + primaryEmail?: string | null; // 최종 선택된 주 이메일 + currency?: string | null; +} + +/** + * RFQ 발송 다이얼로그용 데이터 조회 + */ +export async function getRfqSendData(rfqId: number): Promise<RfqSendData> { + try { + // 1. RFQ 기본 정보 조회 + const [rfqData] = await db + .select({ + rfq: rfqsLast, + project: projects, + picUser: users, + }) + .from(rfqsLast) + .leftJoin(projects, eq(rfqsLast.projectId, projects.id)) + .leftJoin(users, eq(rfqsLast.pic, users.id)) + .where(eq(rfqsLast.id, rfqId)) + .limit(1); + + if (!rfqData) { + throw new Error(`RFQ ID ${rfqId}를 찾을 수 없습니다.`); + } + + const { rfq, project, picUser } = rfqData; + + // 2. PR Items에서 자재그룹 정보 조회 (Major Item) + const [majorItem] = await db + .select({ + materialCategory: rfqPrItems.materialCategory, + materialDescription: rfqPrItems.materialDescription, + }) + .from(rfqPrItems) + .where(and( + eq(rfqPrItems.rfqsLastId, rfqId), + eq(rfqPrItems.majorYn, true) + )) + .limit(1); + + // 3. 첨부파일 정보 조회 + const attachmentsData = await db + .select({ + attachment: rfqLastAttachments, + revision: rfqLastAttachmentRevisions, + }) + .from(rfqLastAttachments) + .leftJoin( + rfqLastAttachmentRevisions, + and( + eq(rfqLastAttachments.latestRevisionId, rfqLastAttachmentRevisions.id), + eq(rfqLastAttachmentRevisions.isLatest, true) + ) + ) + .where(eq(rfqLastAttachments.rfqId, rfqId)); + + const attachments = attachmentsData.map(a => ({ + id: a.attachment.id, + attachmentType: a.attachment.attachmentType, + serialNo: a.attachment.serialNo, + currentRevision: a.attachment.currentRevision, + description: a.attachment.description, + fileName: a.revision?.originalFileName ?? null, + fileSize: a.revision?.fileSize ?? null, + uploadedAt: a.attachment.createdAt, + })); + + // 4. RFQ 정보 조합 + const rfqInfo = { + rfqCode: rfq.rfqCode || '', + rfqTitle: rfq.rfqTitle || '', + rfqType: rfq.rfqType || '', + projectCode: project?.code || undefined, + projectName: project?.name || undefined, + picName: rfq.picName || undefined, + picCode: rfq.picCode || undefined, + picTeam: picUser?.deptName || undefined, + packageNo: rfq.packageNo || undefined, + packageName: rfq.packageName || undefined, + designPicName: rfq.EngPicName || undefined, + rfqTitle: rfq.rfqTitle || undefined, + rfqType: rfq.rfqType || undefined, + designTeam: undefined, // 필요시 추가 조회 + materialGroup: majorItem?.materialCategory || undefined, + materialGroupDesc: majorItem?.materialDescription || undefined, + dueDate: rfq.dueDate || new Date(), + quotationType: rfq.rfqType || undefined, + evaluationApply: true, // 기본값 또는 별도 필드 + contractType: undefined, // 필요시 추가 + }; + + return { + rfqInfo, + attachments, + }; + } catch (error) { + console.error("RFQ 발송 데이터 조회 실패:", error); + throw error; + } +} + +interface ContactDetail { + id: number; + name: string; + position?: string | null; + department?: string | null; + email: string; + phone?: string | null; + isPrimary: boolean; +} + +/** + * 벤더 이메일 정보 조회 + */ +export async function getVendorEmailInfo(vendorIds: number[]): Promise<VendorEmailInfo[]> { + try { + // 1. 벤더 기본 정보 조회 + const vendorsData = await db + .select({ + id: vendors.id, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + country: vendors.country, + email: vendors.email, + representativeEmail: vendors.representativeEmail, + }) + .from(vendors) + .where(sql`${vendors.id} IN ${vendorIds}`); + + // 2. 각 벤더의 모든 담당자 정보 조회 + const contactsData = await db + .select({ + id: vendorContacts.id, + vendorId: vendorContacts.vendorId, + contactName: vendorContacts.contactName, + contactPosition: vendorContacts.contactPosition, + contactDepartment: vendorContacts.contactDepartment, + contactEmail: vendorContacts.contactEmail, + contactPhone: vendorContacts.contactPhone, + isPrimary: vendorContacts.isPrimary, + }) + .from(vendorContacts) + .where(sql`${vendorContacts.vendorId} IN ${vendorIds}`); + + // 3. 데이터 조합 + const vendorEmailInfos: VendorEmailInfo[] = vendorsData.map(vendor => { + const vendorContacts = contactsData.filter(c => c.vendorId === vendor.id); + + // ContactDetail 형식으로 변환 + const contacts: ContactDetail[] = vendorContacts.map(c => ({ + id: c.id, + name: c.contactName, + position: c.contactPosition, + department: c.contactDepartment, + email: c.contactEmail, + phone: c.contactPhone, + isPrimary: c.isPrimary, + })); + + // 포지션별로 그룹화 + const contactsByPosition: Record<string, ContactDetail[]> = {}; + contacts.forEach(contact => { + const position = contact.position || '기타'; + if (!contactsByPosition[position]) { + contactsByPosition[position] = []; + } + contactsByPosition[position].push(contact); + }); + + // 주 이메일 선택 우선순위: + // 1. isPrimary가 true인 담당자 이메일 + // 2. 대표자 이메일 + // 3. vendors 테이블의 기본 이메일 + // 4. 영업 담당자 이메일 + // 5. 첫번째 담당자 이메일 + const primaryContact = contacts.find(c => c.isPrimary); + const salesContact = contacts.find(c => c.position === '영업'); + const primaryEmail = + primaryContact?.email || + vendor.representativeEmail || + vendor.email || + salesContact?.email || + contacts[0]?.email || + null; + + return { + vendorId: vendor.id, + vendorName: vendor.vendorName, + vendorCode: vendor.vendorCode, + vendorCountry: vendor.country, + vendorEmail: vendor.email, + representativeEmail: vendor.representativeEmail, + contacts, + contactsByPosition, + primaryEmail, + currency: 'KRW', // 기본값, 필요시 별도 조회 + }; + }); + + return vendorEmailInfos; + } catch (error) { + console.error("벤더 이메일 정보 조회 실패:", error); + throw error; + } +} + +/** + * 선택된 벤더들의 상세 정보 조회 (RFQ Detail 포함) + */ +export async function getSelectedVendorsWithEmails( + rfqId: number, + vendorIds: number[] +): Promise<Array<VendorEmailInfo & { currency?: string | null }>> { + try { + // 1. 벤더 이메일 정보 조회 + const vendorEmailInfos = await getVendorEmailInfo(vendorIds); + + // 2. RFQ Detail에서 통화 정보 조회 (옵션) + const rfqDetailsData = await db + .select({ + vendorId: rfqLastDetails.vendorsId, + currency: rfqLastDetails.currency, + }) + .from(rfqLastDetails) + .where( + and( + eq(rfqLastDetails.rfqsLastId, rfqId), + sql`${rfqLastDetails.vendorsId} IN ${vendorIds}` + ) + ); + + // 3. 통화 정보 병합 + const result = vendorEmailInfos.map(vendor => { + const detail = rfqDetailsData.find(d => d.vendorId === vendor.vendorId); + return { + ...vendor, + currency: detail?.currency || vendor.currency || 'KRW', + }; + }); + + return result; + } catch (error) { + console.error("선택된 벤더 정보 조회 실패:", error); + throw error; + } }
\ No newline at end of file |
