([])
const [isLoading, setIsLoading] = React.useState(false)
@@ -620,7 +621,7 @@ export function BiddingItemsEditor({ biddingId }: BiddingItemsEditorProps) {
/>
|
- {biddingType === 'equipment' ? (
+ {biddingType !== 'equipment' ? (
) : (
{/* 액션 버튼 */}
-
-
-
+ {!readonly && (
+
+
+
+ )}
{/* 사전견적용 일반견적 생성 다이얼로그 */}
(null)
const [isBiddingInvitationDialogOpen, setIsBiddingInvitationDialogOpen] = React.useState(false)
+ const [isApprovalDialogOpen, setIsApprovalDialogOpen] = React.useState(false)
const [selectedVendors, setSelectedVendors] = React.useState([])
+ const [approvalVariables, setApprovalVariables] = React.useState>({})
+ const [approvalTitle, setApprovalTitle] = React.useState('')
+ const [invitationData, setInvitationData] = React.useState(null)
// 데이터 로딩
React.useEffect(() => {
@@ -197,11 +205,11 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps)
return result.vendors.map((vendor): VendorContractRequirement => ({
vendorId: vendor.vendorId,
vendorName: vendor.vendorName,
- vendorCode: vendor.vendorCode ?? undefined,
+ vendorCode: vendor.vendorCode || undefined,
vendorCountry: vendor.vendorCountry,
- vendorEmail: vendor.vendorEmail ?? undefined,
- contactPerson: vendor.contactPerson ?? undefined,
- contactEmail: vendor.contactEmail ?? undefined,
+ vendorEmail: vendor.vendorEmail || undefined,
+ contactPerson: vendor.contactPerson || undefined,
+ contactEmail: vendor.contactEmail || undefined,
ndaYn: vendor.ndaYn,
generalGtcYn: vendor.generalGtcYn,
projectGtcYn: vendor.projectGtcYn,
@@ -219,7 +227,7 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps)
}
}, [biddingId])
- // 본입찰 초대 다이얼로그가 열릴 때 선정된 업체들 조회
+ // 입찰 초대 다이얼로그가 열릴 때 선정된 업체들 조회
React.useEffect(() => {
if (isBiddingInvitationDialogOpen) {
getSelectedVendors().then(vendors => {
@@ -228,75 +236,96 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps)
}
}, [isBiddingInvitationDialogOpen, getSelectedVendors])
- // 입찰 초대 발송 핸들러
- const handleBiddingInvitationSend = async (data: BiddingInvitationData) => {
- try {
- const userId = session?.user?.id?.toString() || '1'
-
- // 1. 기본계약 발송
- // sendBiddingBasicContracts에 필요한 형식으로 변환
- const vendorDataForContract = data.vendors.map(vendor => ({
- vendorId: vendor.vendorId,
- vendorName: vendor.vendorName,
- vendorCode: vendor.vendorCode || undefined,
- vendorCountry: vendor.vendorCountry,
- selectedMainEmail: vendor.selectedMainEmail,
- additionalEmails: vendor.additionalEmails,
- customEmails: vendor.customEmails,
- contractRequirements: {
- ndaYn: vendor.ndaYn || false,
- generalGtcYn: vendor.generalGtcYn || false,
- projectGtcYn: vendor.projectGtcYn || false,
- agreementYn: vendor.agreementYn || false,
- },
- biddingCompanyId: vendor.biddingCompanyId,
- biddingId: vendor.biddingId,
- hasExistingContracts: vendor.hasExistingContracts,
- }))
-
- const contractResult = await sendBiddingBasicContracts(
- biddingId,
- vendorDataForContract,
- data.generatedPdfs,
- data.message
- )
+ // 입찰공고 버튼 클릭 핸들러 - 입찰 초대 다이얼로그 열기
+ const handleBiddingInvitationClick = () => {
+ setIsBiddingInvitationDialogOpen(true)
+ }
- if (!contractResult.success) {
- const errorMessage = 'message' in contractResult
- ? contractResult.message
- : 'error' in contractResult
- ? contractResult.error
- : '기본계약 발송에 실패했습니다.'
+ // 결재 상신 핸들러 - 결재 완료 시 실제 입찰 등록 실행
+ const handleApprovalSubmit = async ({ approvers, title, attachments }: { approvers: string[], title: string, attachments?: File[] }) => {
+ try {
+ if (!session?.user?.id || !session.user.epId || !invitationData) {
toast({
- title: '기본계약 발송 실패',
- description: errorMessage,
+ title: '오류',
+ description: '필요한 정보가 없습니다.',
variant: 'destructive',
})
return
}
- // 2. 입찰 등록 진행
- const registerResult = await registerBidding(biddingId, userId)
+ // 결재 상신
+ const result = await requestBiddingInvitationWithApproval({
+ biddingId,
+ vendors: selectedVendors,
+ message: invitationData.message || '',
+ currentUser: {
+ id: session.user.id,
+ epId: session.user.epId,
+ email: session.user.email || undefined,
+ },
+ approvers,
+ })
- if (registerResult.success) {
+ if (result.status === 'pending_approval') {
toast({
- title: '본입찰 초대 완료',
- description: '기본계약 발송 및 본입찰 초대가 완료되었습니다.',
+ title: '입찰초대 결재 상신 완료',
+ description: `결재가 상신되었습니다. (ID: ${result.approvalId})`,
})
+ setIsApprovalDialogOpen(false)
setIsBiddingInvitationDialogOpen(false)
+ setInvitationData(null)
router.refresh()
- } else {
+ }
+ } catch (error) {
+ console.error('결재 상신 중 오류 발생:', error)
+ toast({
+ title: '오류',
+ description: '결재 상신 중 오류가 발생했습니다.',
+ variant: 'destructive',
+ })
+ }
+ }
+
+ // 입찰 초대 발송 핸들러 - 결재 준비 및 결재 다이얼로그 열기
+ const handleBiddingInvitationSend = async (data: BiddingInvitationData) => {
+ try {
+ if (!session?.user?.id || !session.user.epId) {
+ toast({
+ title: '오류',
+ description: '사용자 정보가 없습니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ // 선정된 업체들 조회
+ const vendors = await getSelectedVendors()
+ if (vendors.length === 0) {
toast({
title: '오류',
- description: 'error' in registerResult ? registerResult.error : '입찰 등록에 실패했습니다.',
+ description: '선정된 업체가 없습니다.',
variant: 'destructive',
})
+ return
}
+
+ // 결재 데이터 준비 (템플릿 변수, 제목 등)
+ const approvalData = await prepareBiddingApprovalData({
+ biddingId,
+ vendors,
+ message: data.message || '',
+ })
+
+ // 결재 준비 완료 - invitationData와 결재 데이터 저장 및 결재 다이얼로그 열기
+ setInvitationData(data)
+ setApprovalVariables(approvalData.variables)
+ setApprovalTitle(`입찰초대 - ${approvalData.bidding.title}`)
+ setIsApprovalDialogOpen(true)
} catch (error) {
- console.error('본입찰 초대 실패:', error)
+ console.error('결재 준비 중 오류 발생:', error)
toast({
title: '오류',
- description: '본입찰 초대에 실패했습니다.',
+ description: '결재 준비 중 오류가 발생했습니다.',
variant: 'destructive',
})
}
@@ -614,36 +643,38 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps)
{/* 액션 버튼 */}
-
-
-
+ {!readonly && (
+
+
+
+
-
+ )}
{/* 입찰 초대 다이얼로그 */}
{biddingInfo && (
@@ -656,6 +687,32 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps)
onSend={handleBiddingInvitationSend}
/>
)}
+
+ {/* 입찰초대 결재 다이얼로그 */}
+ {session?.user && session.user.epId && biddingInfo && invitationData && Object.keys(approvalVariables).length > 0 && (
+ {
+ setIsApprovalDialogOpen(open)
+ if (!open) {
+ // 다이얼로그가 닫히면 결재 변수 초기화
+ setApprovalVariables({})
+ setApprovalTitle('')
+ setInvitationData(null)
+ }
+ }}
+ templateName="입찰초대 결재"
+ variables={approvalVariables}
+ title={approvalTitle}
+ currentUser={{
+ id: Number(session.user.id),
+ epId: session.user.epId,
+ name: session.user.name || undefined,
+ email: session.user.email || undefined
+ }}
+ onConfirm={handleApprovalSubmit}
+ />
+ )}
)
}
diff --git a/lib/approval/handlers-registry.ts b/lib/approval/handlers-registry.ts
index a92c5ce5..7aec3ae5 100644
--- a/lib/approval/handlers-registry.ts
+++ b/lib/approval/handlers-registry.ts
@@ -49,6 +49,21 @@ export async function initializeApprovalHandlers() {
// RFQ 발송 핸들러 등록 (결재 승인 후 실행될 함수 sendRfqWithApprovalInternal)
registerActionHandler('rfq_send_with_attachments', sendRfqWithApprovalInternal);
+ // 7. 기술영업 RFQ 발송 핸들러 (DRM 파일이 있는 경우)
+ const {
+ sendTechSalesRfqWithApprovalInternal,
+ resendTechSalesRfqWithDrmInternal
+ } = await import('@/lib/techsales-rfq/approval-handlers');
+ // 기술영업 RFQ 발송 핸들러 등록 (결재 승인 후 실행될 함수 sendTechSalesRfqWithApprovalInternal)
+ registerActionHandler('tech_sales_rfq_send_with_drm', sendTechSalesRfqWithApprovalInternal);
+ // 기술영업 RFQ 재발송 핸들러 등록 (결재 승인 후 실행될 함수 resendTechSalesRfqWithDrmInternal)
+ registerActionHandler('tech_sales_rfq_resend_with_drm', resendTechSalesRfqWithDrmInternal);
+
+ // 8. 입찰초대 핸들러
+ const { requestBiddingInvitationInternal } = await import('@/lib/bidding/handlers');
+ // 입찰초대 핸들러 등록 (결재 승인 후 실행될 함수 requestBiddingInvitationInternal)
+ registerActionHandler('bidding_invitation', requestBiddingInvitationInternal);
+
// ... 추가 핸들러 등록
console.log('[Approval Handlers] All handlers registered successfully');
diff --git "a/lib/approval/templates/\354\236\205\354\260\260\354\264\210\353\214\200 \352\262\260\354\236\254.html" "b/lib/approval/templates/\354\236\205\354\260\260\354\264\210\353\214\200 \352\262\260\354\236\254.html"
new file mode 100644
index 00000000..d22b9322
--- /dev/null
+++ "b/lib/approval/templates/\354\236\205\354\260\260\354\264\210\353\214\200 \352\262\260\354\236\254.html"
@@ -0,0 +1,805 @@
+
+
+
+
+
+ |
+ 입찰 결재 요청서 ({{제목}})
+ |
+
+
+
+
+
+
+
+
+ |
+ ■ 입찰 기본 정보
+ |
+
+
+
+
+
+ |
+ 입찰명
+ |
+
+ {{입찰명}}
+ |
+
+ 입찰번호
+ |
+
+ {{입찰번호}}
+ |
+
+
+
+ |
+ 낙찰업체수
+ |
+
+ {{낙찰업체수}}
+ |
+
+ 계약구분
+ |
+
+ {{계약구분}}
+ |
+
+
+
+ |
+ P/R번호
+ |
+
+ {{P/R번호}}
+ |
+
+ 예산
+ |
+
+ {{예산}}
+ |
+
+
+
+ |
+ 내정가
+ |
+
+ {{내정가}}
+ |
+
+ 입찰요청 시스템
+ |
+
+ eVCP
+ |
+
+
+
+ |
+ 입찰담당자
+ |
+
+ {{입찰담당자}}
+ |
+
+ 내정가 산정 기준
+ |
+
+ {{내정가_산정_기준}}
+ |
+
+
+
+ |
+ 입찰 개요
+ |
+
+ {{입찰개요}}
+ |
+
+
+
+ |
+ 입찰 공고문
+ |
+
+ {{입찰공고문}}
+ |
+
+
+
+
+
+
+
+
+ |
+ ■ 입찰 대상 협력사
+ |
+
+
+ |
+ 순번
+ |
+
+ 협력사 코드
+ |
+
+ 협력사명
+ |
+
+ 담당자
+ |
+
+ 이메일
+ |
+
+ 전화번호
+ |
+
+
+
+
+
+ | 1 |
+ {{협력사_코드_1}} |
+ {{협력사명_1}} |
+ {{담당자_1}} |
+ {{이메일_1}} |
+ {{전화번호_1}} |
+
+
+
+ | 2 |
+ {{협력사_코드_2}} |
+ {{협력사명_2}} |
+ {{담당자_2}} |
+ {{이메일_2}} |
+ {{전화번호_2}} |
+
+
+
+
+
+
+
+
+
+ |
+ ■ 입찰 일정 계획
+ |
+
+
+ |
+ 구분
+ |
+
+ 실행 여부
+ |
+
+ 시작 예정 일시
+ |
+
+ 종료 예정 일시
+ |
+
+
+
+
+ |
+ 사양 설명회
+ |
+
+ {{사양설명회_실행여부}}
+ |
+
+ {{사양설명회_시작예정일시}}
+ |
+
+ {{사양설명회_종료예정일시}}
+ |
+
+
+ |
+ 입찰서 제출 기간
+ |
+
+ {{입찰서제출기간_실행여부}}
+ |
+
+ {{입찰서제출기간_시작예정일시}}
+ |
+
+ {{입찰서제출기간_종료예정일시}}
+ |
+
+
+
+
+
+
+
+
+ |
+ ■ 입찰 대상 자재 정보 (총 {{대상_자재_수}} 건)
+ |
+
+
+ |
+ 순번
+ |
+
+ 프로젝트
+ |
+
+ 자재그룹
+ |
+
+ 자재그룹명
+ |
+
+ 자재코드
+ |
+
+ 자재코드명
+ |
+
+ 수량
+ |
+
+ 구매단위
+ |
+
+ 내정단가
+ |
+
+ 수량단위
+ |
+
+ 총중량
+ |
+
+ 중량단위
+ |
+
+ 예산
+ |
+
+ 내정금액
+ |
+
+ 통화
+ |
+
+
+
+
+
+ | 1 |
+ {{프로젝트_1}} |
+ {{자재그룹_1}} |
+ {{자재그룹명_1}} |
+ {{자재코드_1}} |
+ {{자재코드명_1}} |
+ {{수량_1}} |
+ {{구매단위_1}} |
+ {{내정단가_1}} |
+ {{수량단위_1}} |
+ {{총중량_1}} |
+ {{중량단위_1}} |
+ {{예산_1}} |
+ {{내정금액_1}} |
+ {{통화_1}} |
+
+
+ | 2 |
+ {{프로젝트_2}} |
+ {{자재그룹_2}} |
+ {{자재그룹명_2}} |
+ {{자재코드_2}} |
+ {{자재코드명_2}} |
+ {{수량_2}} |
+ {{구매단위_2}} |
+ {{내정단가_2}} |
+ {{수량단위_2}} |
+ {{총중량_2}} |
+ {{중량단위_2}} |
+ {{예산_2}} |
+ {{내정금액_2}} |
+ {{통화_2}} |
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/bidding/approval-actions.ts b/lib/bidding/approval-actions.ts
new file mode 100644
index 00000000..3a82b08f
--- /dev/null
+++ b/lib/bidding/approval-actions.ts
@@ -0,0 +1,243 @@
+/**
+ * 입찰초대 관련 결재 서버 액션
+ *
+ * ✅ 베스트 프랙티스:
+ * - 'use server' 지시어 포함 (서버 액션)
+ * - UI에서 호출하는 진입점 함수들
+ * - withApproval()을 사용하여 결재 프로세스 시작
+ * - 템플릿 변수 준비 및 입력 검증
+ * - 핸들러(Internal)에는 최소 데이터만 전달
+ */
+
+'use server';
+
+import { ApprovalSubmissionSaga } from '@/lib/approval';
+import { mapBiddingInvitationToTemplateVariables } from './handlers';
+import { debugLog, debugError, debugSuccess } from '@/lib/debug-utils';
+
+/**
+ * 입찰초대 결재를 거쳐 입찰등록을 처리하는 서버 액션
+ *
+ * ✅ 사용법 (클라이언트 컴포넌트에서):
+ * ```typescript
+ * const result = await requestBiddingInvitationWithApproval({
+ * biddingId: 123,
+ * vendors: [...],
+ * message: "입찰 초대 메시지",
+ * currentUser: { id: 1, epId: 'EP001', email: 'user@example.com' },
+ * approvers: ['EP002', 'EP003']
+ * });
+ *
+ * if (result.status === 'pending_approval') {
+ * toast.success(`입찰초대 결재가 상신되었습니다. (ID: ${result.approvalId})`);
+ * }
+ * ```
+ */
+/**
+ * 입찰초대 결재를 위한 공통 데이터 준비 헬퍼 함수
+ */
+export async function prepareBiddingApprovalData(data: {
+ biddingId: number;
+ vendors: Array<{
+ vendorId: number;
+ vendorName: string;
+ vendorCode?: string | null;
+ vendorCountry?: string;
+ vendorEmail?: string | null;
+ contactPerson?: string | null;
+ contactEmail?: string | null;
+ ndaYn?: boolean;
+ generalGtcYn?: boolean;
+ projectGtcYn?: boolean;
+ agreementYn?: boolean;
+ biddingCompanyId: number;
+ biddingId: number;
+ }>;
+ message?: string;
+}) {
+ // 1. 입찰 정보 조회 (템플릿 변수용)
+ debugLog('[BiddingInvitationApproval] 입찰 정보 조회 시작');
+ const { default: db } = await import('@/db/db');
+ const { biddings, prItemsForBidding } = await import('@/db/schema');
+ const { eq } = await import('drizzle-orm');
+
+ const biddingInfo = await db
+ .select({
+ id: biddings.id,
+ title: biddings.title,
+ biddingNumber: biddings.biddingNumber,
+ projectName: biddings.projectName,
+ itemName: biddings.itemName,
+ biddingType: biddings.biddingType,
+ bidPicName: biddings.bidPicName,
+ supplyPicName: biddings.supplyPicName,
+ submissionStartDate: biddings.submissionStartDate,
+ submissionEndDate: biddings.submissionEndDate,
+ hasSpecificationMeeting: biddings.hasSpecificationMeeting,
+ isUrgent: biddings.isUrgent,
+ remarks: biddings.remarks,
+ targetPrice: biddings.targetPrice,
+ })
+ .from(biddings)
+ .where(eq(biddings.id, data.biddingId))
+ .limit(1);
+
+ if (biddingInfo.length === 0) {
+ debugError('[BiddingInvitationApproval] 입찰 정보를 찾을 수 없음');
+ throw new Error('입찰 정보를 찾을 수 없습니다');
+ }
+
+ const bidding = biddingInfo[0];
+
+ // 입찰 대상 자재 정보 조회
+ const biddingItemsInfo = await db
+ .select({
+ id: prItemsForBidding.id,
+ projectName: prItemsForBidding.projectInfo,
+ materialGroup: prItemsForBidding.materialGroupNumber,
+ materialGroupName: prItemsForBidding.materialGroupInfo,
+ materialCode: prItemsForBidding.materialNumber,
+ materialCodeName: prItemsForBidding.materialInfo,
+ quantity: prItemsForBidding.quantity,
+ purchasingUnit: prItemsForBidding.purchaseUnit,
+ targetUnitPrice: prItemsForBidding.targetUnitPrice,
+ quantityUnit: prItemsForBidding.quantityUnit,
+ totalWeight: prItemsForBidding.totalWeight,
+ weightUnit: prItemsForBidding.weightUnit,
+ budget: prItemsForBidding.budgetAmount,
+ targetAmount: prItemsForBidding.targetAmount,
+ currency: prItemsForBidding.targetCurrency,
+ })
+ .from(prItemsForBidding)
+ .where(eq(prItemsForBidding.biddingId, data.biddingId));
+
+ debugLog('[BiddingInvitationApproval] 입찰 정보 조회 완료', {
+ biddingId: bidding.id,
+ title: bidding.title,
+ itemCount: biddingItemsInfo.length,
+ });
+
+ // 2. 템플릿 변수 매핑
+ debugLog('[BiddingInvitationApproval] 템플릿 변수 매핑 시작');
+ const requestedAt = new Date();
+ const { mapBiddingInvitationToTemplateVariables } = await import('./handlers');
+ const variables = await mapBiddingInvitationToTemplateVariables({
+ bidding,
+ biddingItems: biddingItemsInfo,
+ vendors: data.vendors,
+ message: data.message,
+ requestedAt,
+ });
+ debugLog('[BiddingInvitationApproval] 템플릿 변수 매핑 완료', {
+ variableKeys: Object.keys(variables),
+ });
+
+ return {
+ bidding,
+ biddingItems: biddingItemsInfo,
+ variables,
+ };
+}
+
+export async function requestBiddingInvitationWithApproval(data: {
+ biddingId: number;
+ vendors: Array<{
+ vendorId: number;
+ vendorName: string;
+ vendorCode?: string | null;
+ vendorCountry?: string;
+ vendorEmail?: string | null;
+ contactPerson?: string | null;
+ contactEmail?: string | null;
+ ndaYn?: boolean;
+ generalGtcYn?: boolean;
+ projectGtcYn?: boolean;
+ agreementYn?: boolean;
+ biddingCompanyId: number;
+ biddingId: number;
+ }>;
+ message?: string;
+ currentUser: { id: number; epId: string | null; email?: string };
+ approvers?: string[]; // Knox EP ID 배열 (결재선)
+}) {
+ debugLog('[BiddingInvitationApproval] 입찰초대 결재 서버 액션 시작', {
+ biddingId: data.biddingId,
+ vendorCount: data.vendors.length,
+ userId: data.currentUser.id,
+ hasEpId: !!data.currentUser.epId,
+ });
+
+ // 1. 입력 검증
+ if (!data.currentUser.epId) {
+ debugError('[BiddingInvitationApproval] Knox EP ID 없음');
+ throw new Error('Knox EP ID가 필요합니다');
+ }
+
+ if (data.vendors.length === 0) {
+ debugError('[BiddingInvitationApproval] 선정된 업체 없음');
+ throw new Error('입찰 초대할 업체를 선택해주세요');
+ }
+
+ // 2. 입찰 상태를 결재 진행중으로 변경
+ debugLog('[BiddingInvitationApproval] 입찰 상태 변경 시작');
+ const { default: db } = await import('@/db/db');
+ const { biddings, biddingCompanies, prItemsForBidding } = await import('@/db/schema');
+ const { eq } = await import('drizzle-orm');
+
+ await db
+ .update(biddings)
+ .set({
+ status: 'approval_pending', // 결재 진행중 상태
+ updatedBy: data.currentUser.epId,
+ updatedAt: new Date()
+ })
+ .where(eq(biddings.id, data.biddingId));
+
+ debugLog('[BiddingInvitationApproval] 입찰 상태 변경 완료', {
+ biddingId: data.biddingId,
+ newStatus: 'approval_pending'
+ });
+
+ // 3. 결재 데이터 준비
+ const { bidding, biddingItems: biddingItemsInfo, variables } = await prepareBiddingApprovalData({
+ biddingId: data.biddingId,
+ vendors: data.vendors,
+ message: data.message,
+ });
+
+ // 4. 결재 워크플로우 시작 (Saga 패턴)
+ debugLog('[BiddingInvitationApproval] ApprovalSubmissionSaga 생성');
+ const saga = new ApprovalSubmissionSaga(
+ // actionType: 핸들러를 찾을 때 사용할 키
+ 'bidding_invitation',
+
+ // actionPayload: 결재 승인 후 핸들러에 전달될 데이터 (최소 데이터만)
+ {
+ biddingId: data.biddingId,
+ vendors: data.vendors,
+ message: data.message,
+ currentUserId: data.currentUser.id, // ✅ 결재 승인 후 핸들러 실행 시 필요
+ },
+
+ // approvalConfig: 결재 상신 정보 (템플릿 포함)
+ {
+ title: `입찰초대 - ${bidding.title}`,
+ description: `${bidding.title} 입찰 초대 결재`,
+ templateName: '입찰초대 결재', // 한국어 템플릿명
+ variables, // 치환할 변수들
+ approvers: data.approvers,
+ currentUser: data.currentUser,
+ }
+ );
+
+ debugLog('[BiddingInvitationApproval] Saga 실행 시작');
+ const result = await saga.execute();
+
+ debugSuccess('[BiddingInvitationApproval] 입찰초대 결재 워크플로우 완료', {
+ approvalId: result.approvalId,
+ pendingActionId: result.pendingActionId,
+ status: result.status,
+ });
+
+ return result;
+}
diff --git a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx
index 6f35405d..80e50119 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx
@@ -76,17 +76,21 @@ export function getBiddingDetailVendorColumns({
cell: ({ row }) => {
const hasAmount = row.original.quotationAmount && Number(row.original.quotationAmount) > 0
return (
-
+
{hasAmount ? (
-
+ <>
+
+ >
) : (
- - {row.original.currency}
+ - {row.original.currency}
)}
)
diff --git a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
index c1677ae7..f2c23de9 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
@@ -4,6 +4,7 @@ import * as React from "react"
import { useRouter } from "next/navigation"
import { useTransition } from "react"
import { Button } from "@/components/ui/button"
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Plus, Send, RotateCcw, XCircle, Trophy, FileText, DollarSign, RotateCw } from "lucide-react"
import { registerBidding, markAsDisposal, createRebidding } from "@/lib/bidding/detail/service"
import { sendBiddingBasicContracts, getSelectedVendorsForBidding } from "@/lib/bidding/pre-quote/service"
@@ -37,6 +38,7 @@ export function BiddingDetailVendorToolbarActions({
const [isPricesDialogOpen, setIsPricesDialogOpen] = React.useState(false)
const [isBiddingInvitationDialogOpen, setIsBiddingInvitationDialogOpen] = React.useState(false)
const [selectedVendors, setSelectedVendors] = React.useState ([])
+ const [isRoundIncreaseDialogOpen, setIsRoundIncreaseDialogOpen] = React.useState(false)
// 본입찰 초대 다이얼로그가 열릴 때 선정된 업체들 조회
React.useEffect(() => {
@@ -176,6 +178,32 @@ export function BiddingDetailVendorToolbarActions({
})
}
+ const handleRoundIncreaseWithNavigation = () => {
+ startTransition(async () => {
+ const result = await increaseRoundOrRebid(bidding.id, userId, 'round_increase')
+
+ if (result.success) {
+ toast({
+ title: "성공",
+ description: result.message,
+ })
+ // 새로 생성된 입찰의 상세 페이지로 이동
+ if (result.biddingId) {
+ router.push(`/evcp/bid/${result.biddingId}`)
+ } else {
+ router.push(`/evcp/bid`)
+ }
+ onSuccess()
+ } else {
+ toast({
+ title: "오류",
+ description: result.error || "차수증가 중 오류가 발생했습니다.",
+ variant: 'destructive',
+ })
+ }
+ })
+ }
+
return (
<>
@@ -185,7 +213,7 @@ export function BiddingDetailVendorToolbarActions({
|