summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/bidding/service.ts28
-rw-r--r--lib/bidding/vendor/partners-bidding-list-columns.tsx18
-rw-r--r--lib/rfq-last/contract-actions.ts2
-rw-r--r--lib/rfq-last/quotation-compare-view.tsx6
-rw-r--r--lib/soap/ecc/mapper/bidding-and-pr-mapper.ts158
-rw-r--r--lib/techsales-rfq/approval-handlers.ts10
-rw-r--r--lib/techsales-rfq/service.ts76
-rw-r--r--lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx62
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