From c4f5472b961afb237dc819f9dd3f42a7b8f71075 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Tue, 18 Nov 2025 10:30:31 +0000
Subject: (최겸) 구매 입찰 수정, 입찰초대 결재 등록, 재입찰, 차수증가, 폐찰,
 유찰취소 로직 수정, readonly 추가 등
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../bidding/manage/bidding-schedule-editor.tsx     | 225 +++++++++++++--------
 1 file changed, 141 insertions(+), 84 deletions(-)

(limited to 'components/bidding/manage/bidding-schedule-editor.tsx')

diff --git a/components/bidding/manage/bidding-schedule-editor.tsx b/components/bidding/manage/bidding-schedule-editor.tsx
index ce03c742..f2978f95 100644
--- a/components/bidding/manage/bidding-schedule-editor.tsx
+++ b/components/bidding/manage/bidding-schedule-editor.tsx
@@ -12,6 +12,9 @@ import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Textarea } from '@/components/ui/textarea'
 import { Switch } from '@/components/ui/switch'
+import { ApprovalPreviewDialog } from '@/lib/approval/approval-preview-dialog'
+import { requestBiddingInvitationWithApproval } from '@/lib/bidding/approval-actions'
+import { prepareBiddingApprovalData } from '@/lib/bidding/approval-actions'
 import { BiddingInvitationDialog } from '@/lib/bidding/detail/table/bidding-invitation-dialog'
 import { sendBiddingBasicContracts, getSelectedVendorsForBidding } from '@/lib/bidding/pre-quote/service'
 import { registerBidding } from '@/lib/bidding/detail/service'
@@ -41,16 +44,17 @@ interface SpecificationMeetingInfo {
 
 interface BiddingScheduleEditorProps {
   biddingId: number
+  readonly?: boolean
 }
 
 interface VendorContractRequirement {
   vendorId: number
   vendorName: string
-  vendorCode?: string | null
+  vendorCode?: string
   vendorCountry?: string
-  vendorEmail?: string | null
-  contactPerson?: string | null
-  contactEmail?: string | null
+  vendorEmail?: string
+  contactPerson?: string
+  contactEmail?: string
   ndaYn?: boolean
   generalGtcYn?: boolean
   projectGtcYn?: boolean
@@ -88,7 +92,7 @@ interface BiddingInvitationData {
   message?: string
 }
 
-export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps) {
+export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingScheduleEditorProps) {
   const { data: session } = useSession()
   const router = useRouter()
   const { toast } = useToast()
@@ -110,7 +114,11 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps)
   const [isSubmitting, setIsSubmitting] = React.useState(false)
   const [biddingInfo, setBiddingInfo] = React.useState<{ title: string; projectName?: string; status: string } | null>(null)
   const [isBiddingInvitationDialogOpen, setIsBiddingInvitationDialogOpen] = React.useState(false)
+  const [isApprovalDialogOpen, setIsApprovalDialogOpen] = React.useState(false)
   const [selectedVendors, setSelectedVendors] = React.useState<VendorContractRequirement[]>([])
+  const [approvalVariables, setApprovalVariables] = React.useState<Record<string, string>>({})
+  const [approvalTitle, setApprovalTitle] = React.useState('')
+  const [invitationData, setInvitationData] = React.useState<BiddingInvitationData | null>(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)
       </Card>
 
       {/* 액션 버튼 */}
-      <div className="flex justify-between gap-4">
-        <Button
-          variant="default"
-          onClick={() => setIsBiddingInvitationDialogOpen(true)}
-          disabled={!biddingInfo || biddingInfo.status !== 'bidding_generated'}
-          className="min-w-[120px]"
-        >
-          <Send className="w-4 h-4 mr-2" />
-          입찰공고
-        </Button>
-        <div className="flex gap-4">
+      {!readonly && (
+        <div className="flex justify-between gap-4">
           <Button
-            onClick={handleSave}
-            disabled={isSubmitting || !biddingInfo || biddingInfo.status !== 'bidding_generated'}
+            variant="default"
+            onClick={handleBiddingInvitationClick}
+            disabled={!biddingInfo || biddingInfo.status !== 'bidding_generated'}
             className="min-w-[120px]"
           >
-            {isSubmitting ? (
-              <>
-                <RefreshCw className="w-4 h-4 mr-2 animate-spin" />
-                저장 중...
-              </>
-            ) : (
-              <>
-                <Save className="w-4 h-4 mr-2" />
-                저장
-              </>
-            )}
+            <Send className="w-4 h-4 mr-2" />
+            입찰공고
           </Button>
+          <div className="flex gap-4">
+            <Button
+              onClick={handleSave}
+              disabled={isSubmitting || !biddingInfo || biddingInfo.status !== 'bidding_generated'}
+              className="min-w-[120px]"
+            >
+              {isSubmitting ? (
+                <>
+                  <RefreshCw className="w-4 h-4 mr-2 animate-spin" />
+                  저장 중...
+                </>
+              ) : (
+                <>
+                  <Save className="w-4 h-4 mr-2" />
+                  저장
+                </>
+              )}
+            </Button>
+          </div>
         </div>
-      </div>
+      )}
 
       {/* 입찰 초대 다이얼로그 */}
       {biddingInfo && (
@@ -656,6 +687,32 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps)
           onSend={handleBiddingInvitationSend}
         />
       )}
+
+      {/* 입찰초대 결재 다이얼로그 */}
+      {session?.user && session.user.epId && biddingInfo && invitationData && Object.keys(approvalVariables).length > 0 && (
+        <ApprovalPreviewDialog
+          open={isApprovalDialogOpen}
+          onOpenChange={(open) => {
+            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}
+        />
+      )}
     </div>
   )
 }
-- 
cgit v1.2.3

