diff options
| author | joonhoekim <26rote@gmail.com> | 2025-12-08 14:19:37 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-12-08 14:19:37 +0900 |
| commit | 2ac7deb8494cf4123f0cff3321860585a44f157c (patch) | |
| tree | 789b6980c8f863a0f675fad38c4a17d91ba28bf3 /lib/techsales-rfq/service.ts | |
| parent | 71c0ba1f01b98770ec2c60cdb935ffb36c1830a9 (diff) | |
| parent | e37cce51ccfa3dcb91904b2492df3a29970fadf7 (diff) | |
Merge remote-tracking branch 'origin/sec-patch' into table-v2
Diffstat (limited to 'lib/techsales-rfq/service.ts')
| -rw-r--r-- | lib/techsales-rfq/service.ts | 197 |
1 files changed, 194 insertions, 3 deletions
diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts index cf4d02e2..8ce41cba 100644 --- a/lib/techsales-rfq/service.ts +++ b/lib/techsales-rfq/service.ts @@ -557,6 +557,7 @@ export async function sendTechSalesRfqToVendors(input: { email?: string | null;
epId?: string | null;
};
+ hideProjectInfoForVendors?: boolean;
}) {
unstable_noStore();
try {
@@ -573,6 +574,7 @@ export async function sendTechSalesRfqToVendors(input: { materialCode: true,
description: true,
rfqType: true,
+ hideProjectInfoForVendors: true,
},
with: {
biddingProject: true,
@@ -604,6 +606,23 @@ export async function sendTechSalesRfqToVendors(input: { }
const isResend = rfq.status === TECH_SALES_RFQ_STATUSES.RFQ_SENT;
+ const effectiveHideProjectInfo =
+ typeof input.hideProjectInfoForVendors === "boolean"
+ ? input.hideProjectInfoForVendors
+ : rfq.hideProjectInfoForVendors ?? false;
+
+ if (
+ typeof input.hideProjectInfoForVendors === "boolean" &&
+ input.hideProjectInfoForVendors !== rfq.hideProjectInfoForVendors
+ ) {
+ await db
+ .update(techSalesRfqs)
+ .set({
+ hideProjectInfoForVendors: input.hideProjectInfoForVendors,
+ updatedAt: new Date(),
+ })
+ .where(eq(techSalesRfqs.id, input.rfqId));
+ }
// 현재 사용자 정보 조회
const sender = await db.query.users.findFirst({
@@ -728,6 +747,9 @@ export async function sendTechSalesRfqToVendors(input: { const rfqItemsResult = await getTechSalesRfqItems(rfq.id);
const rfqItems = rfqItemsResult.data || [];
+ const projectNameForVendor = effectiveHideProjectInfo ? "" : rfq.biddingProject?.projNm || "";
+ const projectCodeForVendor = effectiveHideProjectInfo ? "" : rfq.biddingProject?.pspid || "";
+
// 이메일 컨텍스트 구성
const emailContext = {
language: language,
@@ -735,8 +757,8 @@ export async function sendTechSalesRfqToVendors(input: { id: rfq.id,
code: rfq.rfqCode,
title: rfqItems.length > 0 ? rfqItems.map(item => item.itemList).join(', ') : '',
- projectCode: rfq.biddingProject?.pspid || '',
- projectName: rfq.biddingProject?.projNm || '',
+ projectCode: projectCodeForVendor,
+ projectName: projectNameForVendor,
description: rfq.remark || '',
dueDate: rfq.dueDate ? formatDate(rfq.dueDate, "KR") : 'N/A',
materialCode: rfq.materialCode || '',
@@ -990,6 +1012,7 @@ export async function getTechSalesVendorQuotation(quotationId: number) { projMsrm: quotation.projMsrm,
ptypeNm: quotation.ptypeNm,
} : null,
+ hideProjectInfoForVendors: quotation.hideProjectInfoForVendors ?? false,
},
// 벤더 정보
@@ -1414,6 +1437,7 @@ export async function getVendorQuotations(input: { dueDate: techSalesRfqs.dueDate,
rfqStatus: techSalesRfqs.status,
description: techSalesRfqs.description,
+ hideProjectInfoForVendors: techSalesRfqs.hideProjectInfoForVendors,
// 프로젝트 정보 (직접 조인)
projNm: biddingProjects.projNm,
// 아이템 개수
@@ -3662,7 +3686,8 @@ export async function getTechSalesVendorQuotationAttachments(quotationId: number updatedAt: techSalesVendorQuotationAttachments.updatedAt,
})
.from(techSalesVendorQuotationAttachments)
- .where(eq(techSalesVendorQuotationAttachments.quotationId, quotationId))
+ .where(and(eq(techSalesVendorQuotationAttachments.quotationId, quotationId),
+ eq(techSalesVendorQuotationAttachments.isVendorUpload, true)))
.orderBy(desc(techSalesVendorQuotationAttachments.createdAt));
return { data: attachments };
@@ -3680,6 +3705,172 @@ export async function getTechSalesVendorQuotationAttachments(quotationId: number }
/**
+ * 기술영업 RFQ 기준 벤더 견적서 요약 목록 조회 (eml 첨부 전용)
+ */
+export async function getTechSalesVendorQuotationsForRfq(rfqId: number) {
+ unstable_noStore();
+ try {
+ const quotations = await db
+ .select({
+ id: techSalesVendorQuotations.id,
+ vendorId: techSalesVendorQuotations.vendorId,
+ vendorName: techVendors.vendorName,
+ vendorCode: techVendors.vendorCode,
+ quotationVersion: techSalesVendorQuotations.quotationVersion,
+ status: techSalesVendorQuotations.status,
+ })
+ .from(techSalesVendorQuotations)
+ .leftJoin(techVendors, eq(techSalesVendorQuotations.vendorId, techVendors.id))
+ .where(eq(techSalesVendorQuotations.rfqId, rfqId))
+ .orderBy(
+ asc(techVendors.vendorName),
+ asc(techSalesVendorQuotations.id)
+ );
+
+ return { data: quotations, error: null };
+ } catch (error) {
+ console.error("기술영업 RFQ 벤더 견적서 목록 조회 오류:", error);
+ return { data: [], error: getErrorMessage(error) };
+ }
+}
+
+/**
+ * 기술영업 벤더 견적서 eml 첨부파일 조회 (isVendorUpload = false)
+ */
+export async function getTechSalesVendorQuotationEmlAttachments(quotationId: number) {
+ unstable_noStore();
+ try {
+ const attachments = await db
+ .select({
+ id: techSalesVendorQuotationAttachments.id,
+ quotationId: techSalesVendorQuotationAttachments.quotationId,
+ revisionId: techSalesVendorQuotationAttachments.revisionId,
+ fileName: techSalesVendorQuotationAttachments.fileName,
+ originalFileName: techSalesVendorQuotationAttachments.originalFileName,
+ fileSize: techSalesVendorQuotationAttachments.fileSize,
+ fileType: techSalesVendorQuotationAttachments.fileType,
+ filePath: techSalesVendorQuotationAttachments.filePath,
+ description: techSalesVendorQuotationAttachments.description,
+ uploadedBy: techSalesVendorQuotationAttachments.uploadedBy,
+ vendorId: techSalesVendorQuotationAttachments.vendorId,
+ isVendorUpload: techSalesVendorQuotationAttachments.isVendorUpload,
+ createdAt: techSalesVendorQuotationAttachments.createdAt,
+ updatedAt: techSalesVendorQuotationAttachments.updatedAt,
+ })
+ .from(techSalesVendorQuotationAttachments)
+ .where(
+ and(
+ eq(techSalesVendorQuotationAttachments.quotationId, quotationId),
+ eq(techSalesVendorQuotationAttachments.isVendorUpload, false)
+ )
+ )
+ .orderBy(desc(techSalesVendorQuotationAttachments.createdAt));
+
+ return { data: attachments, error: null };
+ } catch (error) {
+ console.error("기술영업 벤더 견적서 eml 첨부파일 조회 오류:", error);
+ return { data: [], error: getErrorMessage(error) };
+ }
+}
+
+/**
+ * 기술영업 벤더 견적서 eml 첨부파일 업로드/삭제 처리
+ * - isVendorUpload = false 로 저장 (메일 등 별도 전달 문서 보관용)
+ */
+export async function processTechSalesVendorQuotationEmlAttachments(params: {
+ quotationId: number;
+ newFiles?: { file: File; description?: string }[];
+ deleteAttachmentIds?: number[];
+ uploadedBy: number;
+ revisionId?: number;
+}) {
+ unstable_noStore();
+ const { quotationId, newFiles = [], deleteAttachmentIds = [], uploadedBy, revisionId } = params;
+
+ try {
+ // 견적서 확인
+ const quotation = await db.query.techSalesVendorQuotations.findFirst({
+ where: eq(techSalesVendorQuotations.id, quotationId),
+ columns: { id: true, rfqId: true, quotationVersion: true },
+ });
+
+ if (!quotation) {
+ return { data: null, error: "견적서를 찾을 수 없습니다." };
+ }
+
+ const targetRevisionId = revisionId ?? quotation.quotationVersion ?? 0;
+
+ await db.transaction(async (tx) => {
+ // 삭제 처리 (벤더 업로드 파일은 삭제하지 않음)
+ if (deleteAttachmentIds.length > 0) {
+ const deletable = await tx.query.techSalesVendorQuotationAttachments.findMany({
+ where: inArray(techSalesVendorQuotationAttachments.id, deleteAttachmentIds),
+ });
+
+ for (const attachment of deletable) {
+ if (attachment.isVendorUpload) {
+ throw new Error("벤더가 업로드한 파일은 여기서 삭제할 수 없습니다.");
+ }
+
+ await tx
+ .delete(techSalesVendorQuotationAttachments)
+ .where(eq(techSalesVendorQuotationAttachments.id, attachment.id));
+
+ try {
+ deleteFile(attachment.filePath);
+ } catch (fileError) {
+ console.warn("eml 첨부파일 삭제 중 파일 시스템 오류:", fileError);
+ }
+ }
+ }
+
+ // 업로드 처리
+ if (newFiles.length > 0) {
+ for (const { file, description } of newFiles) {
+ const saveResult = await saveFile({
+ file,
+ directory: `techsales-quotations/${quotationId}/eml`,
+ userId: uploadedBy.toString(),
+ });
+
+ if (!saveResult.success) {
+ throw new Error(saveResult.error || "파일 저장에 실패했습니다.");
+ }
+
+ await tx.insert(techSalesVendorQuotationAttachments).values({
+ quotationId,
+ revisionId: targetRevisionId,
+ fileName: saveResult.fileName!,
+ originalFileName: saveResult.originalName || file.name,
+ fileSize: file.size,
+ fileType: file.type || undefined,
+ filePath: saveResult.publicPath!,
+ description: description || null,
+ uploadedBy,
+ isVendorUpload: false,
+ });
+ }
+ }
+ });
+
+ // 캐시 무효화
+ revalidateTag(`quotation-${quotationId}`);
+ revalidateTag("quotation-attachments");
+ revalidateTag("techSalesVendorQuotations");
+ if (quotation.rfqId) {
+ revalidateTag(`techSalesRfq-${quotation.rfqId}`);
+ }
+ revalidateTag("techSalesRfqs");
+
+ const refreshed = await getTechSalesVendorQuotationEmlAttachments(quotationId);
+ return { data: refreshed.data, error: refreshed.error };
+ } catch (error) {
+ console.error("기술영업 벤더 견적서 eml 첨부파일 처리 오류:", error);
+ return { data: null, error: getErrorMessage(error) };
+ }
+}
+
+/**
* 특정 리비전의 견적서 첨부파일 조회
*/
export async function getTechSalesVendorQuotationAttachmentsByRevision(quotationId: number, revisionId: number) {
|
