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.ts480
1 files changed, 478 insertions, 2 deletions
diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts
index 67cb901f..0c75e72f 100644
--- a/lib/rfq-last/service.ts
+++ b/lib/rfq-last/service.ts
@@ -571,9 +571,9 @@ export async function getRfqItemsAction(rfqId: number) {
materialDescription: item.materialDescription,
size: item.size,
deliveryDate: item.deliveryDate,
- quantity: item.quantity,
+ quantity: Number(item.quantity) || 0, // 여기서 숫자로 변환
uom: item.uom,
- grossWeight: item.grossWeight,
+ grossWeight: Number(item.grossWeight) || 0, // 여기서 숫자로 변환
gwUom: item.gwUom,
specNo: item.specNo,
specUrl: item.specUrl,
@@ -1835,4 +1835,480 @@ export async function getRfqWithDetails(rfqId: number) {
console.error("Get RFQ with details error:", error);
return { success: false, error: "데이터 조회 중 오류가 발생했습니다." };
}
+}
+
+
+// RFQ 정보 타입
+export interface RfqFullInfo {
+ // 기본 RFQ 정보
+ id: number;
+ rfqCode: string;
+ rfqType: string | null;
+ rfqTitle: string | null;
+ series: string | null;
+ rfqSealedYn: boolean | null;
+
+ // ITB 관련
+ projectCompany: string | null;
+ projectFlag: string | null;
+ projectSite: string | null;
+ smCode: string | null;
+
+ // RFQ 추가 필드
+ prNumber: string | null;
+ prIssueDate: Date | null;
+
+ // 프로젝트 정보
+ projectId: number | null;
+ projectCode: string | null;
+ projectName: string | null;
+
+ // 아이템 정보
+ itemCode: string | null;
+ itemName: string | null;
+
+ // 패키지 정보
+ packageNo: string | null;
+ packageName: string | null;
+
+ // 날짜 정보
+ dueDate: Date | null;
+ rfqSendDate: Date | null;
+
+ // 상태
+ status: string;
+
+ // 담당자 정보
+ picId: number | null;
+ picCode: string | null;
+ picName: string | null;
+ picUserName: string | null;
+ picTeam: string | null;
+
+ // 설계담당자
+ engPicName: string | null;
+ designTeam: string | null;
+
+ // 자재그룹 정보 (PR Items에서)
+ materialGroup: string | null;
+ materialGroupDesc: string | null;
+
+ // 카운트 정보
+ vendorCount: number;
+ shortListedVendorCount: number;
+ quotationReceivedCount: number;
+ prItemsCount: number;
+ majorItemsCount: number;
+
+ // 감사 정보
+ createdBy: number;
+ createdByUserName: string | null;
+ createdAt: Date;
+ updatedBy: number;
+ updatedByUserName: string | null;
+ updatedAt: Date;
+
+ sentBy: number | null;
+ sentByUserName: string | null;
+
+ remark: string | null;
+
+ // 평가 적용 여부 (추가 필드)
+ evaluationApply?: boolean;
+ quotationType?: string;
+ contractType?: string;
+
+ // 연관 데이터
+ vendors: VendorDetail[];
+ attachments: AttachmentInfo[];
+}
+
+// 벤더 상세 정보
+export interface VendorDetail {
+ detailId: number;
+ vendorId: number | null;
+ vendorName: string | null;
+ vendorCode: string | null;
+ vendorCountry: string | null;
+ vendorEmail?: string | null;
+ vendorCategory?: string | null;
+ vendorGrade?: string | null;
+ basicContract?: string | null;
+
+ // RFQ 조건
+ currency: string | null;
+ paymentTermsCode: string | null;
+ paymentTermsDescription: string | null;
+ incotermsCode: string | null;
+ incotermsDescription: string | null;
+ incotermsDetail: string | null;
+ deliveryDate: Date | null;
+ contractDuration: string | null;
+ taxCode: string | null;
+ placeOfShipping: string | null;
+ placeOfDestination: string | null;
+
+ // 상태
+ shortList: boolean;
+ returnYn: boolean;
+ returnedAt: Date | null;
+
+ // GTC/NDA
+ prjectGtcYn: boolean;
+ generalGtcYn: boolean;
+ ndaYn: boolean;
+ agreementYn: boolean;
+
+ // 추가 조건
+ materialPriceRelatedYn: boolean | null;
+ sparepartYn: boolean | null;
+ firstYn: boolean | null;
+ firstDescription: string | null;
+ sparepartDescription: string | null;
+
+ remark: string | null;
+ cancelReason: string | null;
+
+ // 회신 상태
+ quotationStatus?: string | null;
+ quotationSubmittedAt?: Date | null;
+
+ // 업데이트 정보
+ updatedBy: number;
+ updatedByUserName: string | null;
+ updatedAt: Date | null;
+}
+
+// 첨부파일 정보
+export interface AttachmentInfo {
+ id: number;
+ attachmentType: string;
+ serialNo: string;
+ currentRevision: string;
+ description: string | null;
+
+ // 최신 리비전 정보
+ fileName: string | null;
+ originalFileName: string | null;
+ filePath: string | null;
+ fileSize: number | null;
+ fileType: string | null;
+
+ createdBy: number;
+ createdByUserName: string | null;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+/**
+ * RFQ 전체 정보 조회
+ */
+export async function getRfqFullInfo(rfqId: number): Promise<RfqFullInfo> {
+ try {
+ // 1. RFQ 기본 정보 조회
+ const rfqData = await db
+ .select({
+ rfq: rfqsLast,
+ picUser: users,
+ })
+ .from(rfqsLast)
+ .leftJoin(users, eq(rfqsLast.pic, users.id))
+ .where(eq(rfqsLast.id, rfqId))
+ .limit(1);
+
+ if (!rfqData.length) {
+ throw new Error(`RFQ ID ${rfqId}를 찾을 수 없습니다.`);
+ }
+
+ const rfq = rfqData[0].rfq;
+ const picUser = rfqData[0].picUser;
+
+ // 2. PR Items에서 자재그룹 정보 조회 (Major Item)
+ const prItemsData = await db
+ .select({
+ materialCategory: rfqPrItems.materialCategory,
+ materialDescription: rfqPrItems.materialDescription,
+ prItemsCount: eq(rfqPrItems.majorYn, true),
+ })
+ .from(rfqPrItems)
+ .where(and(
+ eq(rfqPrItems.rfqsLastId, rfqId),
+ eq(rfqPrItems.majorYn, true)
+ ))
+ .limit(1);
+
+ const majorItem = prItemsData[0];
+
+ // 3. 벤더 정보 조회
+ const vendorsData = await db
+ .select({
+ detail: rfqLastDetails,
+ vendor: vendors,
+ paymentTerms: paymentTerms,
+ incoterms: incoterms,
+ updatedByUser: users,
+ })
+ .from(rfqLastDetails)
+ .leftJoin(vendors, eq(rfqLastDetails.vendorsId, vendors.id))
+ .leftJoin(paymentTerms, eq(rfqLastDetails.paymentTermsCode, paymentTerms.code))
+ .leftJoin(incoterms, eq(rfqLastDetails.incotermsCode, incoterms.code))
+ .leftJoin(users, eq(rfqLastDetails.updatedBy, users.id))
+ .where(eq(rfqLastDetails.rfqsLastId, rfqId));
+
+ const vendorDetails: VendorDetail[] = vendorsData.map(v => ({
+ detailId: v.detail.id,
+ vendorId: v.vendor?.id ?? null,
+ vendorName: v.vendor?.vendorName ?? null,
+ vendorCode: v.vendor?.vendorCode ?? null,
+ vendorCountry: v.vendor?.country ?? null,
+ vendorEmail: v.vendor?.email ?? null,
+ vendorCategory: v.vendor?.vendorCategory ?? null,
+ vendorGrade: v.vendor?.vendorGrade ?? null,
+ basicContract: v.vendor?.basicContract ?? null,
+
+ currency: v.detail.currency,
+ paymentTermsCode: v.detail.paymentTermsCode,
+ paymentTermsDescription: v.paymentTerms?.description ?? null,
+ incotermsCode: v.detail.incotermsCode,
+ incotermsDescription: v.incoterms?.description ?? null,
+ incotermsDetail: v.detail.incotermsDetail,
+ deliveryDate: v.detail.deliveryDate,
+ contractDuration: v.detail.contractDuration,
+ taxCode: v.detail.taxCode,
+ placeOfShipping: v.detail.placeOfShipping,
+ placeOfDestination: v.detail.placeOfDestination,
+
+ shortList: v.detail.shortList,
+ returnYn: v.detail.returnYn,
+ returnedAt: v.detail.returnedAt,
+
+ prjectGtcYn: v.detail.prjectGtcYn,
+ generalGtcYn: v.detail.generalGtcYn,
+ ndaYn: v.detail.ndaYn,
+ agreementYn: v.detail.agreementYn,
+
+ materialPriceRelatedYn: v.detail.materialPriceRelatedYn,
+ sparepartYn: v.detail.sparepartYn,
+ firstYn: v.detail.firstYn,
+ firstDescription: v.detail.firstDescription,
+ sparepartDescription: v.detail.sparepartDescription,
+
+ remark: v.detail.remark,
+ cancelReason: v.detail.cancelReason,
+
+ updatedBy: v.detail.updatedBy,
+ updatedByUserName: v.updatedByUser?.name ?? null,
+ updatedAt: v.detail.updatedAt,
+ }));
+
+ // 4. 첨부파일 정보 조회
+ const attachmentsData = await db
+ .select({
+ attachment: rfqLastAttachments,
+ revision: rfqLastAttachmentRevisions,
+ createdByUser: users,
+ })
+ .from(rfqLastAttachments)
+ .leftJoin(
+ rfqLastAttachmentRevisions,
+ and(
+ eq(rfqLastAttachments.latestRevisionId, rfqLastAttachmentRevisions.id),
+ eq(rfqLastAttachmentRevisions.isLatest, true)
+ )
+ )
+ .leftJoin(users, eq(rfqLastAttachments.createdBy, users.id))
+ .where(eq(rfqLastAttachments.rfqId, rfqId));
+
+ const attachments: AttachmentInfo[] = 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?.fileName ?? null,
+ originalFileName: a.revision?.originalFileName ?? null,
+ filePath: a.revision?.filePath ?? null,
+ fileSize: a.revision?.fileSize ?? null,
+ fileType: a.revision?.fileType ?? null,
+
+ createdBy: a.attachment.createdBy,
+ createdByUserName: a.createdByUser?.name ?? null,
+ createdAt: a.attachment.createdAt,
+ updatedAt: a.attachment.updatedAt,
+ }));
+
+ // 5. 카운트 정보 계산
+ const vendorCount = vendorDetails.length;
+ const shortListedVendorCount = vendorDetails.filter(v => v.shortList).length;
+ const quotationReceivedCount = vendorDetails.filter(v => v.quotationSubmittedAt).length;
+
+ // PR Items 카운트 (별도 쿼리 필요)
+ const prItemsCount = await db
+ .select({ count: sql<number>`COUNT(*)` })
+ .from(rfqPrItems)
+ .where(eq(rfqPrItems.rfqsLastId, rfqId));
+
+ const majorItemsCount = await db
+ .select({ count: sql<number>`COUNT(*)` })
+ .from(rfqPrItems)
+ .where(and(
+ eq(rfqPrItems.rfqsLastId, rfqId),
+ eq(rfqPrItems.majorYn, true)
+ ));
+
+ // 6. 사용자 정보 조회 (createdBy, updatedBy, sentBy)
+ const [createdByUser] = await db
+ .select({ name: users.name })
+ .from(users)
+ .where(eq(users.id, rfq.createdBy))
+ .limit(1);
+
+ const [updatedByUser] = await db
+ .select({ name: users.name })
+ .from(users)
+ .where(eq(users.id, rfq.updatedBy))
+ .limit(1);
+
+ const [sentByUser] = rfq.sentBy
+ ? await db
+ .select({ name: users.name })
+ .from(users)
+ .where(eq(users.id, rfq.sentBy))
+ .limit(1)
+ : [null];
+
+ // 7. 전체 정보 조합
+ const rfqFullInfo: RfqFullInfo = {
+ // 기본 정보
+ id: rfq.id,
+ rfqCode: rfq.rfqCode ?? '',
+ rfqType: rfq.rfqType,
+ rfqTitle: rfq.rfqTitle,
+ series: rfq.series,
+ rfqSealedYn: rfq.rfqSealedYn,
+
+ // ITB 관련
+ projectCompany: rfq.projectCompany,
+ projectFlag: rfq.projectFlag,
+ projectSite: rfq.projectSite,
+ smCode: rfq.smCode,
+
+ // RFQ 추가 필드
+ prNumber: rfq.prNumber,
+ prIssueDate: rfq.prIssueDate,
+
+ // 프로젝트
+ projectId: rfq.projectId,
+ projectCode: null, // 프로젝트 조인 필요시 추가
+ projectName: null, // 프로젝트 조인 필요시 추가
+
+ // 아이템
+ itemCode: rfq.itemCode,
+ itemName: rfq.itemName,
+
+ // 패키지
+ packageNo: rfq.packageNo,
+ packageName: rfq.packageName,
+
+ // 날짜
+ dueDate: rfq.dueDate,
+ rfqSendDate: rfq.rfqSendDate,
+
+ // 상태
+ status: rfq.status,
+
+ // 구매 담당자
+ picId: rfq.pic,
+ picCode: rfq.picCode,
+ picName: rfq.picName,
+ picUserName: picUser?.name ?? null,
+ picTeam: picUser?.department ?? null, // users 테이블에 department 필드가 있다고 가정
+
+ // 설계 담당자
+ engPicName: rfq.EngPicName,
+ designTeam: null, // 추가 정보 필요시 입력
+
+ // 자재그룹 (PR Items에서)
+ materialGroup: majorItem?.materialCategory ?? null,
+ materialGroupDesc: majorItem?.materialDescription ?? null,
+
+ // 카운트
+ vendorCount,
+ shortListedVendorCount,
+ quotationReceivedCount,
+ prItemsCount: prItemsCount[0]?.count ?? 0,
+ majorItemsCount: majorItemsCount[0]?.count ?? 0,
+
+ // 감사 정보
+ createdBy: rfq.createdBy,
+ createdByUserName: createdByUser?.name ?? null,
+ createdAt: rfq.createdAt,
+ updatedBy: rfq.updatedBy,
+ updatedByUserName: updatedByUser?.name ?? null,
+ updatedAt: rfq.updatedAt,
+ sentBy: rfq.sentBy,
+ sentByUserName: sentByUser?.name ?? null,
+
+ remark: rfq.remark,
+
+ // 추가 필드 (필요시)
+ evaluationApply: true, // 기본값 또는 별도 로직
+ quotationType: rfq.rfqType ?? undefined,
+ contractType: undefined, // 별도 필드 필요
+
+ // 연관 데이터
+ vendors: vendorDetails,
+ attachments: attachments,
+ };
+
+ return rfqFullInfo;
+ } catch (error) {
+ console.error("RFQ 정보 조회 실패:", error);
+ throw error;
+ }
+}
+
+/**
+ * SendRfqDialog용 간단한 정보 조회
+ */
+export async function getRfqInfoForSend(rfqId: number) {
+ const fullInfo = await getRfqFullInfo(rfqId);
+
+ return {
+ rfqCode: fullInfo.rfqCode,
+ rfqTitle: fullInfo.rfqTitle || '',
+ rfqType: fullInfo.rfqType || '',
+ projectCode: fullInfo.projectCode,
+ projectName: fullInfo.projectName,
+ picName: fullInfo.picName,
+ picCode: fullInfo.picCode,
+ picTeam: fullInfo.picTeam,
+ packageNo: fullInfo.packageNo,
+ packageName: fullInfo.packageName,
+ designPicName: fullInfo.engPicName, // EngPicName이 설계담당자
+ designTeam: fullInfo.designTeam,
+ materialGroup: fullInfo.materialGroup,
+ materialGroupDesc: fullInfo.materialGroupDesc,
+ dueDate: fullInfo.dueDate || new Date(),
+ quotationType: fullInfo.quotationType,
+ evaluationApply: fullInfo.evaluationApply,
+ contractType: fullInfo.contractType,
+ };
+}
+
+/**
+ * 벤더 정보만 조회
+ */
+export async function getRfqVendors(rfqId: number) {
+ const fullInfo = await getRfqFullInfo(rfqId);
+ return fullInfo.vendors;
+}
+
+/**
+ * 첨부파일 정보만 조회
+ */
+export async function getRfqAttachments(rfqId: number) {
+ const fullInfo = await getRfqFullInfo(rfqId);
+ return fullInfo.attachments;
} \ No newline at end of file