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