diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/bidding/service.ts | 28 | ||||
| -rw-r--r-- | lib/bidding/vendor/partners-bidding-list-columns.tsx | 18 | ||||
| -rw-r--r-- | lib/rfq-last/contract-actions.ts | 2 | ||||
| -rw-r--r-- | lib/rfq-last/quotation-compare-view.tsx | 6 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/bidding-and-pr-mapper.ts | 158 | ||||
| -rw-r--r-- | lib/techsales-rfq/approval-handlers.ts | 10 | ||||
| -rw-r--r-- | lib/techsales-rfq/service.ts | 76 | ||||
| -rw-r--r-- | lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx | 62 |
8 files changed, 222 insertions, 138 deletions
diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts index 2474d464..0261ad57 100644 --- a/lib/bidding/service.ts +++ b/lib/bidding/service.ts @@ -2294,14 +2294,22 @@ export async function updateBiddingSchedule( try { const userName = await getUserNameById(userId) - // 날짜 문자열을 Date 객체로 변환 + // 날짜 문자열을 Date 객체로 수동 변환 const parseDate = (dateStr?: string) => { if (!dateStr) return undefined - try { - return new Date(dateStr) - } catch { - return undefined - } + // 'YYYY-MM-DDTHH:mm' 또는 'YYYY-MM-DD HH:mm' 등을 허용 + // 잘못된 포맷이면 undefined 반환 + const m = dateStr.match( + /^(\d{4})-(\d{2})-(\d{2})[T ]?(\d{2}):(\d{2})(?::(\d{2}))?$/ + ) + if (!m) return undefined + const year = parseInt(m[1], 10) + const month = parseInt(m[2], 10) - 1 // JS month는 0부터 + const day = parseInt(m[3], 10) + const hour = parseInt(m[4], 10) + const min = parseInt(m[5], 10) + const sec = m[6] ? parseInt(m[6], 10) : 0 + return new Date(Date.UTC(year, month, day, hour, min, sec)) } return await db.transaction(async (tx) => { @@ -2310,8 +2318,8 @@ export async function updateBiddingSchedule( updatedBy: userName, } - if (schedule.submissionStartDate !== undefined) updateData.submissionStartDate = parseDate(schedule.submissionStartDate) - if (schedule.submissionEndDate !== undefined) updateData.submissionEndDate = parseDate(schedule.submissionEndDate) + if (schedule.submissionStartDate !== undefined) updateData.submissionStartDate = parseDate(schedule.submissionStartDate) || null + if (schedule.submissionEndDate !== undefined) updateData.submissionEndDate = parseDate(schedule.submissionEndDate) || null if (schedule.remarks !== undefined) updateData.remarks = schedule.remarks if (schedule.isUrgent !== undefined) updateData.isUrgent = schedule.isUrgent if (schedule.hasSpecificationMeeting !== undefined) updateData.hasSpecificationMeeting = schedule.hasSpecificationMeeting @@ -2335,7 +2343,7 @@ export async function updateBiddingSchedule( await tx .update(specificationMeetings) .set({ - meetingDate: new Date(specificationMeeting.meetingDate), + meetingDate: parseDate(specificationMeeting.meetingDate) || null, meetingTime: specificationMeeting.meetingTime || null, location: specificationMeeting.location, address: specificationMeeting.address || null, @@ -2355,7 +2363,7 @@ export async function updateBiddingSchedule( .insert(specificationMeetings) .values({ biddingId, - meetingDate: new Date(specificationMeeting.meetingDate), + meetingDate: specificationMeeting.meetingDate, meetingTime: specificationMeeting.meetingTime || null, location: specificationMeeting.location, address: specificationMeeting.address || null, diff --git a/lib/bidding/vendor/partners-bidding-list-columns.tsx b/lib/bidding/vendor/partners-bidding-list-columns.tsx index ba47ce50..63d097c0 100644 --- a/lib/bidding/vendor/partners-bidding-list-columns.tsx +++ b/lib/bidding/vendor/partners-bidding-list-columns.tsx @@ -204,15 +204,15 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL }, }), - // 품목명 - columnHelper.accessor('itemName', { - header: '품목명', - cell: ({ row }) => ( - <div className="max-w-32 truncate" title={row.original.itemName}> - {row.original.itemName} - </div> - ), - }), + // // 품목명 + // columnHelper.accessor('itemName', { + // header: '품목명', + // cell: ({ row }) => ( + // <div className="max-w-32 truncate" title={row.original.itemName}> + // {row.original.itemName} + // </div> + // ), + // }), // 입찰명 columnHelper.accessor('title', { diff --git a/lib/rfq-last/contract-actions.ts b/lib/rfq-last/contract-actions.ts index 1323ba8a..4098f1bf 100644 --- a/lib/rfq-last/contract-actions.ts +++ b/lib/rfq-last/contract-actions.ts @@ -541,7 +541,7 @@ interface CreateBiddingParams { currency: string; contractType: "unit_price" | "general" | "sale"; // 계약구분 biddingType: string; // 입찰유형 (equipment, construction 등) - awardCount: "single" | "multiple"; // 낙찰수 + awardCount: "single" | "multiple"; // 낙찰업체 수 biddingStartDate: Date; biddingEndDate: Date; biddingRequirements?: string; diff --git a/lib/rfq-last/quotation-compare-view.tsx b/lib/rfq-last/quotation-compare-view.tsx index 40ea676c..c6710d7b 100644 --- a/lib/rfq-last/quotation-compare-view.tsx +++ b/lib/rfq-last/quotation-compare-view.tsx @@ -432,7 +432,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { <Button variant="destructive" onClick={() => setShowCancelDialog(true)} - disabled={selectedContractType !== ""} + disabled={selectedContractType !== "" || data.rfqInfo.rfqCode?.startsWith("R")} className="gap-2" > <X className="h-4 w-4" /> @@ -1547,10 +1547,10 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { </select> </div> - {/* 낙찰수 선택 */} + {/* 낙찰업체 수 선택 */} <div className="space-y-2"> <label htmlFor="award-count" className="text-sm font-medium"> - 낙찰수 + 낙찰업체 수 </label> <select id="award-count" diff --git a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts index adbb3e1e..1a5089eb 100644 --- a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts +++ b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts @@ -21,7 +21,8 @@ import { PR_INFORMATION_T_BID_HEADER, PR_INFORMATION_T_BID_ITEM, } from '@/db/schema/ECC/ecc'; -import { inArray, max, sql, eq } from 'drizzle-orm'; +import { inArray, max, sql, eq, like } from 'drizzle-orm'; +import { users } from '@/db/schema/users'; import { findUserInfoByEKGRP, findProjectInfoByPSPID, @@ -48,22 +49,22 @@ export type PrItemForBiddingData = typeof prItemsForBidding.$inferInsert; /** * Bidding 코드 생성 함수 (배치 처리용) - * 형식: BID{EKGRP}{00001} + * 형식: E/F/G{담당자코드3자리}{년도2자리}{시퀀스4자리}-01 * 기존 ANFNR은 기존 biddingNumber 사용, 새로운 ANFNR만 새 코드 생성 */ async function generateBiddingCodes(eccHeaders: ECCBidHeader[]): Promise<Map<string, string>> { try { debugLog('Bidding 코드 배치 생성 시작', { headerCount: eccHeaders.length }); - + const biddingCodeMap = new Map<string, string>(); - + // 1) 먼저 기존 ANFNR들의 biddingNumber 조회 const anfnrList = eccHeaders.map(h => h.ANFNR).filter(Boolean); if (anfnrList.length > 0) { const existingResult = await db - .select({ - ANFNR: biddings.ANFNR, - biddingNumber: biddings.biddingNumber + .select({ + ANFNR: biddings.ANFNR, + biddingNumber: biddings.biddingNumber }) .from(biddings) .where(inArray(biddings.ANFNR, anfnrList)); @@ -75,62 +76,108 @@ async function generateBiddingCodes(eccHeaders: ECCBidHeader[]): Promise<Map<str } } } - + // 2) 새로운 ANFNR들만 필터링 (기존에 없는 것들) - const newHeaders = eccHeaders.filter(header => + const newHeaders = eccHeaders.filter(header => header.ANFNR && !biddingCodeMap.has(header.ANFNR) ); - + if (newHeaders.length === 0) { - debugSuccess('모든 ANFNR이 기존에 존재함', { - totalCodes: biddingCodeMap.size + debugSuccess('모든 ANFNR이 기존에 존재함', { + totalCodes: biddingCodeMap.size }); return biddingCodeMap; } - - // 3) 새로운 ANFNR들을 EKGRP별로 그룹핑 - const ekgrpGroups = new Map<string, ECCBidHeader[]>(); + + // 3) 새로운 ANFNR들을 담당자별로 그룹핑 (EKGRP 기반) + const managerGroups = new Map<string, ECCBidHeader[]>(); for (const header of newHeaders) { - const ekgrp = header.EKGRP || 'UNKNOWN'; - if (!ekgrpGroups.has(ekgrp)) { - ekgrpGroups.set(ekgrp, []); + // EKGRP를 직접 사용하여 3자리 담당자 코드 생성 + let managerCode = '000'; + const ekgrp = header.EKGRP || ''; + + if (ekgrp.length >= 3) { + managerCode = ekgrp.substring(0, 3).toUpperCase(); + } else if (ekgrp.length > 0) { + managerCode = ekgrp.padEnd(3, '0').toUpperCase(); } - ekgrpGroups.get(ekgrp)!.push(header); + + if (!managerGroups.has(managerCode)) { + managerGroups.set(managerCode, []); + } + managerGroups.get(managerCode)!.push(header); } - - // 4) EKGRP별로 새 코드 생성 - for (const [ekgrp, headers] of ekgrpGroups) { - // 해당 EKGRP의 현재 최대 시퀀스 조회 - const maxResult = await db - .select({ - maxBiddingNumber: max(biddings.biddingNumber) + + // 계약 타입별 접두사 설정 (ECC에서는 기본적으로 일반계약으로 가정) + const contractType = 'general'; + const typePrefix = { + 'general': 'E', // 일반계약 + 'unit_price': 'F', // 단가계약 + 'sale': 'G', // 매각계약 + }; + const prefix = typePrefix[contractType as keyof typeof typePrefix] || 'E'; + + // 현재 년도 2자리 + const currentYear = new Date().getFullYear().toString().slice(-2); + + // 4) 담당자별로 새 코드 생성 + for (const [managerCode, headers] of managerGroups) { + const yearPrefix = `${prefix}${managerCode}${currentYear}`; + + // 해당 담당자의 현재 최대 시퀀스 조회 + const prefixLength = yearPrefix.length + 4; + const result = await db + .select({ + maxNumber: sql<string>`MAX(LEFT(${biddings.biddingNumber}, ${prefixLength}))` }) .from(biddings) - .where(sql`${biddings.biddingNumber} LIKE ${`B${ekgrp}%`}`); - - let nextSeq = 1; - if (maxResult[0]?.maxBiddingNumber) { - const prefix = `B${ekgrp}`; - const currentCode = maxResult[0].maxBiddingNumber; - if (currentCode.startsWith(prefix)) { - const seqPart = currentCode.substring(prefix.length); - const currentSeq = parseInt(seqPart, 10); - if (!isNaN(currentSeq)) { - nextSeq = currentSeq + 1; + .where(like(biddings.biddingNumber, `${yearPrefix}%`)); + + // 시퀀스 생성 헬퍼 함수 + const generateNextSequence = (lastSequence: string | null): string => { + if (!lastSequence) return '0001'; + const seqNum = parseInt(lastSequence, 10) || 0; + return (seqNum + 1).toString().padStart(4, '0'); + }; + + const nextSequence = generateNextSequence(result[0]?.maxNumber?.slice(-4) || null); + + // 동일 담당자 내에서 순차적으로 새 코드 생성 + for (const header of headers) { + const biddingNumber = `${yearPrefix}${nextSequence}-01`; + + // 중복 확인 및 재시도 (동시성 문제 방지) + let finalBiddingNumber = biddingNumber; + let attempts = 0; + const maxRetries = 5; + + while (attempts < maxRetries) { + const existing = await db + .select({ id: biddings.id }) + .from(biddings) + .where(eq(biddings.biddingNumber, finalBiddingNumber)) + .limit(1); + + if (existing.length === 0) { + break; // 중복 없음 + } + + // 중복이 발견되면 시퀀스 증가 + const currentSeq = parseInt(finalBiddingNumber.slice(-6, -3), 10); + const newSeq = (currentSeq + 1).toString().padStart(4, '0'); + finalBiddingNumber = `${yearPrefix}${newSeq}-01`; + attempts++; + + if (attempts >= maxRetries) { + throw new Error(`Failed to generate unique bidding number after ${maxRetries} attempts`); } } - } - - // 동일 EKGRP 내에서 순차적으로 새 코드 생성 - for (const header of headers) { - const seqString = nextSeq.toString().padStart(5, '0'); - const biddingCode = `B${ekgrp}${seqString}`; - biddingCodeMap.set(header.ANFNR || '', biddingCode); - nextSeq++; // 다음 시퀀스로 증가 + + biddingCodeMap.set(header.ANFNR || '', finalBiddingNumber); } } - - debugSuccess('Bidding 코드 배치 생성 완료', { + + debugSuccess('Bidding 코드 배치 생성 완료', { totalCodes: biddingCodeMap.size, newCodes: newHeaders.length, existingCodes: eccHeaders.length - newHeaders.length @@ -138,13 +185,15 @@ async function generateBiddingCodes(eccHeaders: ECCBidHeader[]): Promise<Map<str return biddingCodeMap; } catch (error) { debugError('Bidding 코드 배치 생성 중 오류 발생', { error }); - - // 오류 발생시 폴백: ANFNR 기반 코드 생성 + + // 오류 발생시 폴백: 기본 형식으로 코드 생성 const fallbackMap = new Map<string, string>(); + const currentYear = new Date().getFullYear().toString().slice(-2); + eccHeaders.forEach((header, index) => { - const ekgrp = header.EKGRP || 'UNKNOWN'; - const seqString = (index + 1).toString().padStart(5, '0'); - fallbackMap.set(header.ANFNR, `B${ekgrp}${seqString}`); + const seqString = (index + 1).toString().padStart(4, '0'); + const ekgrp = header.EKGRP || '000'; + fallbackMap.set(header.ANFNR, `E${ekgrp.substring(0, 3).toUpperCase()}${currentYear}${seqString}-01`); }); return fallbackMap; } @@ -195,6 +244,7 @@ export async function mapECCBiddingHeaderToBidding( // 매핑 const mappedData: BiddingData = { biddingNumber, // 생성된 Bidding 코드 + originalBiddingNumber: eccHeader.ANFNR || null, // 원입찰번호 revision: 0, // 기본 리비전 0 (I/F 해서 가져온 건 보낸 적 없으므로 0 고정) projectName, // 첫번째 PR Item의 PSPID로 찾은 프로젝트 이름 itemName, // 첫번째 PR Item의 MATNR로 조회한 자재명 @@ -204,13 +254,13 @@ export async function mapECCBiddingHeaderToBidding( // 계약 정보 - ECC에서 제공되지 않으므로 기본값 설정 contractType: 'general', // 일반계약 기본값 (notNull) biddingType: 'equipment', // 입찰유형 기본값 (notNull) - awardCount: 'single', // 낙찰수 기본값 (notNull) + awardCount: 'single', // 낙찰업체 수 기본값 (notNull) contractStartDate: null, // ECC에서 제공 X contractEndDate: null, // ECC에서 제공 X // 일정 관리 - ECC에서 제공되지 않음 preQuoteDate: null, - biddingRegistrationDate: null, + biddingRegistrationDate: new Date().toISOString(), // 입찰등록일 I/F 시점 등록(1120 이시원 프로 요청) submissionStartDate: null, submissionEndDate: null, evaluationDate: null, diff --git a/lib/techsales-rfq/approval-handlers.ts b/lib/techsales-rfq/approval-handlers.ts index 6ffd9fb4..0d4629e4 100644 --- a/lib/techsales-rfq/approval-handlers.ts +++ b/lib/techsales-rfq/approval-handlers.ts @@ -98,15 +98,7 @@ export async function sendTechSalesRfqWithApprovalInternal(payload: { } } - // 2. RFQ 상태를 "RFQ Sent"로 변경 - await db.update(techSalesRfqs) - .set({ - status: TECH_SALES_RFQ_STATUSES.RFQ_SENT, - updatedAt: new Date(), - }) - .where(eq(techSalesRfqs.id, payload.rfqId)); - - // 3. 실제 RFQ 발송 실행 + // 2. 실제 RFQ 발송 실행 (상태 변경과 이메일 발송은 sendTechSalesRfqToVendors에서 처리) const sendResult = await sendTechSalesRfqToVendors({ rfqId: payload.rfqId, vendorIds: payload.vendorIds, diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts index dc5950e0..ed3472b1 100644 --- a/lib/techsales-rfq/service.ts +++ b/lib/techsales-rfq/service.ts @@ -502,7 +502,45 @@ export async function getTechSalesRfqVendors(rfqId: number) { }
/**
+ * 기술영업 RFQ 첨부파일 중 DRM 해제 여부 확인
+ */
+export async function checkTechSalesRfqHasDrmAttachments(rfqId: number) {
+ unstable_noStore();
+ try {
+ const attachments = await db.query.techSalesAttachments.findMany({
+ where: eq(techSalesAttachments.techSalesRfqId, rfqId),
+ columns: {
+ id: true,
+ drmEncrypted: true,
+ fileName: true,
+ fileSize: true,
+ originalFileName: true,
+ }
+ });
+
+ const drmAttachments = attachments.filter(att => att.drmEncrypted === true);
+
+ return {
+ hasDrm: drmAttachments.length > 0,
+ drmAttachmentIds: drmAttachments.map(att => att.id),
+ drmAttachments: drmAttachments.map(att => ({
+ fileName: att.originalFileName || att.fileName,
+ fileSize: att.fileSize,
+ })),
+ };
+ } catch (err) {
+ console.error("기술영업 RFQ DRM 첨부파일 확인 오류:", err);
+ return {
+ hasDrm: false,
+ drmAttachmentIds: [],
+ drmAttachments: [],
+ };
+ }
+}
+
+/**
* 기술영업 RFQ 발송 (선택된 벤더들의 선택된 contact들에게)
+ * DRM 해제가 필요없는 경우에만 사용 (결재 프로세스 없이 바로 발송)
*/
export async function sendTechSalesRfqToVendors(input: {
rfqId: number;
@@ -559,7 +597,7 @@ export async function sendTechSalesRfqToVendors(input: { };
}
- // 발송 가능한 상태인지 확인 (결재 진행중 상태는 제외)
+ // 발송 가능한 상태인지 확인 (결재 진행중 상태 포함 - 결재 승인 후 발송 가능)
if (rfq.status !== TECH_SALES_RFQ_STATUSES.RFQ_VENDOR_ASSIGNED &&
rfq.status !== TECH_SALES_RFQ_STATUSES.RFQ_SENT &&
rfq.status !== TECH_SALES_RFQ_STATUSES.APPROVAL_IN_PROGRESS) {
@@ -568,14 +606,6 @@ export async function sendTechSalesRfqToVendors(input: { message: "벤더가 할당된 RFQ 또는 이미 전송된 RFQ만 다시 전송할 수 있습니다",
};
}
-
- // 결재 진행중 상태에서는 결재 승인 후처리 핸들러에서만 발송 가능
- if (rfq.status === TECH_SALES_RFQ_STATUSES.APPROVAL_IN_PROGRESS) {
- return {
- success: false,
- message: "결재 진행 중인 RFQ는 결재 승인 후 자동으로 발송됩니다",
- };
- }
const isResend = rfq.status === TECH_SALES_RFQ_STATUSES.RFQ_SENT;
@@ -626,34 +656,6 @@ export async function sendTechSalesRfqToVendors(input: { };
}
- // RFQ 첨부파일 중 DRM 파일 확인
- const attachments = await db.query.techSalesAttachments.findMany({
- where: eq(techSalesAttachments.techSalesRfqId, input.rfqId),
- columns: {
- id: true,
- drmEncrypted: true,
- fileName: true,
- fileSize: true,
- originalFileName: true,
- }
- });
-
- const drmAttachments = attachments.filter(att => att.drmEncrypted === true);
-
- // DRM 파일이 있으면 결재 프로세스 필요 (이 함수는 직접 발송하지 않고 결재 필요 신호 반환)
- if (drmAttachments.length > 0) {
- return {
- success: false,
- requiresApproval: true,
- message: "DRM 파일이 포함되어 있어 결재가 필요합니다",
- drmAttachmentIds: drmAttachments.map(att => att.id),
- drmAttachments: drmAttachments.map(att => ({
- fileName: att.originalFileName || att.fileName,
- fileSize: att.fileSize,
- })),
- };
- }
-
// 트랜잭션 시작
await db.transaction(async (tx) => {
// 1. RFQ 상태 업데이트 (최초 발송인 경우 rfqSendDate 설정)
diff --git a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx index 52758412..db2331af 100644 --- a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx +++ b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx @@ -259,26 +259,20 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps try {
setIsSendingRfq(true);
- // 기술영업 RFQ 발송 서비스 함수 호출 (contact 정보 포함)
- const vendorIds = selectedRows.map(row => row.vendorId).filter(Boolean);
- const { sendTechSalesRfqToVendors } = await import("@/lib/techsales-rfq/service");
+ // DRM 해제 여부 확인
+ const { checkTechSalesRfqHasDrmAttachments, sendTechSalesRfqToVendors } = await import("@/lib/techsales-rfq/service");
+ const drmCheck = await checkTechSalesRfqHasDrmAttachments(selectedRfqId);
- const result = await sendTechSalesRfqToVendors({
- rfqId: selectedRfqId,
- vendorIds: vendorIds as number[],
- selectedContacts: selectedContacts
- });
-
- // DRM 파일이 있어서 결재가 필요한 경우
- if (!result.success && result.requiresApproval) {
+ // DRM 파일이 걸려있으면 결재 프로세스 진행
+ if (drmCheck.hasDrm) {
// 결재 데이터 저장
setApprovalPreviewData({
vendors: selectedRows.map(row => ({
vendorId: row.vendorId!,
vendorName: row.vendorName || "",
})),
- drmAttachments: result.drmAttachments || [],
- drmAttachmentIds: result.drmAttachmentIds || [],
+ drmAttachments: drmCheck.drmAttachments,
+ drmAttachmentIds: drmCheck.drmAttachmentIds,
selectedContacts: selectedContacts,
});
@@ -288,6 +282,14 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps return;
}
+ // DRM 해제가 안 걸려있으면 바로 발송
+ const vendorIds = selectedRows.map(row => row.vendorId).filter(Boolean);
+ const result = await sendTechSalesRfqToVendors({
+ rfqId: selectedRfqId,
+ vendorIds: vendorIds as number[],
+ selectedContacts: selectedContacts
+ });
+
if (result.success) {
toast.success(result.message || `${selectedContacts.length}명의 연락처에게 RFQ가 발송되었습니다.`);
} else {
@@ -483,8 +485,7 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps applicationReason: reason,
});
- // 신청사유 다이얼로그 닫고 결재 미리보기 열기
- setShowApplicationReasonDialog(false);
+ // 결재 미리보기 열기
setShowApprovalPreview(true);
} catch (error) {
console.error("템플릿 변수 생성 실패:", error);
@@ -936,6 +937,37 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps quotationId={selectedQuotationForContacts?.id || null}
vendorName={selectedQuotationForContacts?.vendorName}
/>
+
+ {/* 신청사유 입력 다이얼로그 */}
+ {approvalPreviewData && (
+ <ApplicationReasonDialog
+ open={showApplicationReasonDialog}
+ onOpenChange={setShowApplicationReasonDialog}
+ onConfirm={handleApplicationReasonConfirm}
+ vendorCount={approvalPreviewData.vendors.length}
+ attachmentCount={approvalPreviewData.drmAttachmentIds.length}
+ />
+ )}
+
+ {/* 결재 미리보기 다이얼로그 */}
+ {approvalPreviewData && approvalPreviewData.templateVariables && (
+ <ApprovalPreviewDialog
+ open={showApprovalPreview}
+ onOpenChange={setShowApprovalPreview}
+ templateName="암호화해제 신청"
+ variables={approvalPreviewData.templateVariables}
+ title={`암호화해제 신청 - ${selectedRfq?.rfqCode || 'RFQ'}`}
+ currentUser={{
+ id: Number(session.data.user.id),
+ epId: session.data.user.epId,
+ name: session.data.user.name || undefined,
+ email: session.data.user.email || undefined,
+ }}
+ onConfirm={handleApprovalConfirm}
+ allowTitleEdit={false}
+ allowDescriptionEdit={false}
+ />
+ )}
</div>
)
}
\ No newline at end of file |
