summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-25 05:00:10 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-25 05:00:10 +0000
commitca6545ad76c548a3202e0deee1e2b1dde51bc413 (patch)
treee18ed085c205b602d228b52a289487bea716f442 /lib/techsales-rfq
parent6824e097d768f724cf439b410ccfb1ab9685ac98 (diff)
(최겸) 기술영업 rfq 수정(리비전 오류 및 첨부파일 오류 수정)+스키마 일부 변경
Diffstat (limited to 'lib/techsales-rfq')
-rw-r--r--lib/techsales-rfq/service.ts53
-rw-r--r--lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx8
-rw-r--r--lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet.tsx15
-rw-r--r--lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx38
4 files changed, 63 insertions, 51 deletions
diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts
index 25e1f379..ffa29acd 100644
--- a/lib/techsales-rfq/service.ts
+++ b/lib/techsales-rfq/service.ts
@@ -955,27 +955,32 @@ export async function submitTechSalesVendorQuotation(data: {
// });
// }
- // 항상 revision 저장 (변경사항 여부와 관계없이)
- await tx.insert(techSalesVendorQuotationRevisions).values({
- quotationId: data.id,
- version: currentQuotation.quotationVersion || 1,
- snapshot: {
- currency: currentQuotation.currency,
- totalPrice: currentQuotation.totalPrice,
- validUntil: currentQuotation.validUntil,
- remark: currentQuotation.remark,
- status: currentQuotation.status,
- quotationVersion: currentQuotation.quotationVersion,
- submittedAt: currentQuotation.submittedAt,
- acceptedAt: currentQuotation.acceptedAt,
- updatedAt: currentQuotation.updatedAt,
- },
- changeReason: "견적서 제출",
- revisedBy: data.updatedBy,
- });
+ // 첫 제출인지 확인 (quotationVersion이 null인 경우)
+ const isFirstSubmission = currentQuotation.quotationVersion === null;
+
+ // 첫 제출이 아닌 경우에만 revision 저장 (변경사항 이력 관리)
+ if (!isFirstSubmission) {
+ await tx.insert(techSalesVendorQuotationRevisions).values({
+ quotationId: data.id,
+ version: currentQuotation.quotationVersion || 1,
+ snapshot: {
+ currency: currentQuotation.currency,
+ totalPrice: currentQuotation.totalPrice,
+ validUntil: currentQuotation.validUntil,
+ remark: currentQuotation.remark,
+ status: currentQuotation.status,
+ quotationVersion: currentQuotation.quotationVersion,
+ submittedAt: currentQuotation.submittedAt,
+ acceptedAt: currentQuotation.acceptedAt,
+ updatedAt: currentQuotation.updatedAt,
+ },
+ changeReason: "견적서 제출",
+ revisedBy: data.updatedBy,
+ });
+ }
- // 새로운 버전 번호 계산 (항상 1 증가)
- const newRevisionId = (currentQuotation.quotationVersion || 1) + 1;
+ // 새로운 버전 번호 계산 (첫 제출은 1, 재제출은 1 증가)
+ const newRevisionId = isFirstSubmission ? 1 : (currentQuotation.quotationVersion || 1) + 1;
// 새로운 버전으로 업데이트
const result = await tx
@@ -1177,7 +1182,7 @@ export async function getVendorQuotations(input: {
});
}
- // 조인을 포함한 데이터 조회
+ // 조인을 포함한 데이터 조회 (중복 제거를 위해 techSalesAttachments JOIN 제거)
const data = await db
.select({
id: techSalesVendorQuotations.id,
@@ -1211,16 +1216,16 @@ export async function getVendorQuotations(input: {
FROM tech_sales_rfq_items
WHERE tech_sales_rfq_items.rfq_id = ${techSalesRfqs.id}
)`,
- // RFQ 첨부파일 개수
+ // RFQ 첨부파일 개수 (RFQ_COMMON 타입만 카운트)
attachmentCount: sql<number>`(
SELECT COUNT(*)
FROM tech_sales_attachments
WHERE tech_sales_attachments.tech_sales_rfq_id = ${techSalesRfqs.id}
+ AND tech_sales_attachments.attachment_type = 'RFQ_COMMON'
)`,
})
.from(techSalesVendorQuotations)
.leftJoin(techSalesRfqs, eq(techSalesVendorQuotations.rfqId, techSalesRfqs.id))
- .leftJoin(techSalesAttachments, eq(techSalesRfqs.id, techSalesAttachments.techSalesRfqId))
.leftJoin(biddingProjects, eq(techSalesRfqs.biddingProjectId, biddingProjects.id))
.where(finalWhere)
.orderBy(...orderBy)
@@ -2727,12 +2732,14 @@ export async function addTechVendorsToTechSalesRfq(input: {
}
// 🔥 중요: 벤더 추가 시에는 견적서를 생성하지 않고, "Assigned" 상태로만 생성
+ // quotation_version은 null로 설정하여 벤더가 실제 견적 제출 시에만 리비전 생성
const [quotation] = await tx
.insert(techSalesVendorQuotations)
.values({
rfqId: input.rfqId,
vendorId: vendorId,
status: "Assigned", // Draft가 아닌 Assigned 상태로 생성
+ quotationVersion: null, // 리비전은 견적 제출 시에만 생성
createdBy: input.createdBy,
updatedBy: input.createdBy,
})
diff --git a/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx b/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx
index 7832fa2b..0195b10c 100644
--- a/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx
+++ b/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx
@@ -119,12 +119,12 @@ function QuotationCard({
{statusInfo.label}
</Badge>
</div>
- {changeReason && (
+ {/* {changeReason && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<FileText className="size-4" />
<span>{changeReason}</span>
</div>
- )}
+ )} */}
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-4">
@@ -250,7 +250,7 @@ export function QuotationHistoryDialog({
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
- <DialogContent className=" max-h-[80vh] overflow-y-auto">
+ <DialogContent className="w-[80vw] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>견적서 수정 히스토리</DialogTitle>
<DialogDescription>
@@ -258,7 +258,7 @@ export function QuotationHistoryDialog({
</DialogDescription>
</DialogHeader>
- <div className="space-y-4">
+ <div className="space-y-4 overflow-x-auto">
{isLoading ? (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
diff --git a/lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet.tsx b/lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet.tsx
index a7b487e1..3b0fd38d 100644
--- a/lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet.tsx
+++ b/lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet.tsx
@@ -49,7 +49,7 @@ import {
import prettyBytes from "pretty-bytes"
import { formatDate } from "@/lib/utils"
-import { processTechSalesRfqAttachments, getTechSalesRfqAttachments } from "@/lib/techsales-rfq/service"
+import { processTechSalesRfqAttachments } from "@/lib/techsales-rfq/service"
const MAX_FILE_SIZE = 6e8 // 600MB
@@ -113,6 +113,8 @@ interface TechSalesRfqAttachmentsSheetProps
rfq: TechSalesRfq | null
/** 첨부파일 타입 */
attachmentType?: "RFQ_COMMON" | "TBE_RESULT" | "CBE_RESULT"
+ /** 읽기 전용 모드 (벤더용) */
+ readOnly?: boolean
/** 업로드/삭제 후 상위 테이블에 attachmentCount 등을 업데이트하기 위한 콜백 */
// onAttachmentsUpdated?: (rfqId: number, newAttachmentCount: number) => void
@@ -123,6 +125,7 @@ export function TechSalesRfqAttachmentsSheet({
// onAttachmentsUpdated,
rfq,
attachmentType = "RFQ_COMMON",
+ readOnly = false,
...props
}: TechSalesRfqAttachmentsSheetProps) {
const [isPending, setIsPending] = React.useState(false)
@@ -135,24 +138,24 @@ export function TechSalesRfqAttachmentsSheet({
title: "TBE 결과 첨부파일",
description: "기술 평가(TBE) 결과 파일을 관리합니다.",
fileTypeLabel: "TBE 결과",
- canEdit: true
+ canEdit: !readOnly
}
case "CBE_RESULT":
return {
title: "CBE 결과 첨부파일",
description: "상업성 평가(CBE) 결과 파일을 관리합니다.",
fileTypeLabel: "CBE 결과",
- canEdit: true
+ canEdit: !readOnly
}
default: // RFQ_COMMON
return {
title: "RFQ 첨부파일",
- description: "RFQ 공통 첨부파일을 관리합니다.",
+ description: readOnly ? "RFQ 공통 첨부파일을 조회합니다." : "RFQ 공통 첨부파일을 관리합니다.",
fileTypeLabel: "공통",
- canEdit: true
+ canEdit: !readOnly
}
}
- }, [attachmentType, rfq?.status])
+ }, [attachmentType, readOnly])
// // RFQ 상태에 따른 편집 가능 여부 결정 (readOnly prop이 true면 항상 false)
// const isEditable = React.useMemo(() => {
diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx
index 4c5cdf8e..e79d7c4d 100644
--- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx
+++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx
@@ -218,7 +218,7 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab
return data;
}, [data.length, data.map(item => `${item.id}-${item.status}-${item.updatedAt}`).join(',')]);
- // 첨부파일 시트 열기 함수
+ // 첨부파일 시트 열기 함수 (벤더는 RFQ_COMMON 타입만 조회)
const openAttachmentsSheet = React.useCallback(async (rfqId: number) => {
try {
// RFQ 정보 조회 (data에서 rfqId에 해당하는 데이터 찾기)
@@ -236,20 +236,22 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab
return
}
- // API 응답을 ExistingTechSalesAttachment 형식으로 변환
- const attachments: ExistingTechSalesAttachment[] = result.data.map(att => ({
- id: att.id,
- techSalesRfqId: att.techSalesRfqId || rfqId,
- fileName: att.fileName,
- originalFileName: att.originalFileName,
- filePath: att.filePath,
- fileSize: att.fileSize || undefined,
- fileType: att.fileType || undefined,
- attachmentType: att.attachmentType as "RFQ_COMMON" | "VENDOR_SPECIFIC",
- description: att.description || undefined,
- createdBy: att.createdBy,
- createdAt: att.createdAt,
- }))
+ // API 응답을 ExistingTechSalesAttachment 형식으로 변환하고 RFQ_COMMON 타입만 필터링
+ const attachments: ExistingTechSalesAttachment[] = result.data
+ .filter(att => att.attachmentType === "RFQ_COMMON") // 벤더는 RFQ_COMMON 타입만 조회
+ .map(att => ({
+ id: att.id,
+ techSalesRfqId: att.techSalesRfqId || rfqId,
+ fileName: att.fileName,
+ originalFileName: att.originalFileName,
+ filePath: att.filePath,
+ fileSize: att.fileSize || undefined,
+ fileType: att.fileType || undefined,
+ attachmentType: att.attachmentType as "RFQ_COMMON" | "TBE_RESULT" | "CBE_RESULT",
+ description: att.description || undefined,
+ createdBy: att.createdBy,
+ createdAt: att.createdAt,
+ }))
setAttachmentsDefault(attachments)
setSelectedRfqForAttachments({
@@ -383,7 +385,7 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab
// useDataTable 훅 사용
const { table } = useDataTable({
data: stableData,
- columns: columns as any,
+ columns: columns as any, // 타입 오류 임시 해결
pageCount,
rowCount: total,
filterFields,
@@ -488,8 +490,8 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab
onOpenChange={setAttachmentsOpen}
defaultAttachments={attachmentsDefault}
rfq={selectedRfqForAttachments}
- onAttachmentsUpdated={() => {}} // 읽기 전용이므로 빈 함수
- readOnly={true} // 벤더 쪽에서는 항상 읽기 전용
+ attachmentType="RFQ_COMMON" // 벤더는 RFQ_COMMON 타입만 조회
+ readOnly={true} // 벤더는 항상 읽기 전용
/>
{/* 아이템 보기 다이얼로그 */}