From d47334639bd717aa860563ec1020a29827524fd4 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Fri, 5 Dec 2025 06:29:23 +0000
Subject: (최겸)구매 결재일 기준 공고 수정
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/bidding/receive/biddings-receive-columns.tsx | 808 +++++++++++------------
 1 file changed, 404 insertions(+), 404 deletions(-)

(limited to 'lib/bidding/receive/biddings-receive-columns.tsx')

diff --git a/lib/bidding/receive/biddings-receive-columns.tsx b/lib/bidding/receive/biddings-receive-columns.tsx
index 9650574a..f2e2df17 100644
--- a/lib/bidding/receive/biddings-receive-columns.tsx
+++ b/lib/bidding/receive/biddings-receive-columns.tsx
@@ -1,404 +1,404 @@
-"use client"
-
-import * as React from "react"
-import { type ColumnDef } from "@tanstack/react-table"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import { Checkbox } from "@/components/ui/checkbox"
-import {
-  Eye, Calendar, Users, CheckCircle, XCircle, Clock, AlertTriangle
-} from "lucide-react"
-import {
-  Tooltip,
-  TooltipContent,
-  TooltipProvider,
-  TooltipTrigger,
-} from "@/components/ui/tooltip"
-import {
-  DropdownMenu,
-  DropdownMenuContent,
-  DropdownMenuItem,
-  DropdownMenuSeparator,
-  DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
-import {
-  biddingStatusLabels,
-  contractTypeLabels,
-} from "@/db/schema"
-import { formatDate } from "@/lib/utils"
-import { DataTableRowAction } from "@/types/table"
-
-type BiddingReceiveItem = {
-  id: number
-  biddingNumber: string
-  originalBiddingNumber: string | null
-  title: string
-  status: string
-  contractType: string
-  prNumber: string | null
-  submissionStartDate: Date | null
-  submissionEndDate: Date | null
-  bidPicName: string | null
-  supplyPicName: string | null
-  createdBy: string | null
-  createdAt: Date | null
-  updatedAt: Date | null
-
-  // 참여 현황
-  participantExpected: number
-  participantParticipated: number
-  participantDeclined: number
-  participantPending: number
-
-  // 개찰 정보
-  openedAt: Date | null
-  openedBy: string | null
-}
-
-interface GetColumnsProps {
-  setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<BiddingReceiveItem> | null>>
-  onParticipantClick?: (biddingId: number, participantType: 'expected' | 'participated' | 'declined' | 'pending') => void
-}
-
-// 상태별 배지 색상
-const getStatusBadgeVariant = (status: string) => {
-  switch (status) {
-    case 'received_quotation':
-      return 'secondary'
-    case 'bidding_opened':
-      return 'default'
-    case 'bidding_closed':
-      return 'outline'
-    default:
-      return 'outline'
-  }
-}
-
-// 금액 포맷팅
-const formatCurrency = (amount: string | number | null, currency = 'KRW') => {
-  if (!amount) return '-'
-
-  const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount
-  if (isNaN(numAmount)) return '-'
-
-  return new Intl.NumberFormat('ko-KR', {
-    style: 'currency',
-    currency: currency,
-    minimumFractionDigits: 0,
-    maximumFractionDigits: 0,
-  }).format(numAmount)
-}
-
-export function getBiddingsReceiveColumns({ setRowAction, onParticipantClick }: GetColumnsProps): ColumnDef<BiddingReceiveItem>[] {
-
-  return [
-    // ░░░ 선택 ░░░
-    {
-      id: "select",
-      header: "",
-      cell: ({ row }) => (
-        <Checkbox
-          checked={row.getIsSelected()}
-          onCheckedChange={(value) => {
-            // single select 모드에서는 다른 행들의 선택을 해제
-            row.toggleSelected(!!value)
-          }}
-          aria-label="행 선택"
-        />
-      ),
-      size: 50,
-      enableSorting: false,
-      enableHiding: false,
-    },
-
-    // ░░░ 입찰번호 ░░░
-    {
-      accessorKey: "biddingNumber",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰번호" />,
-      cell: ({ row }) => (
-        <div className="font-mono text-sm">
-          {row.original.biddingNumber}
-        </div>
-      ),
-      size: 120,
-      meta: { excelHeader: "입찰번호" },
-    },
-
-    // ░░░ 입찰명 ░░░
-    {
-      accessorKey: "title",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰명" />,
-      cell: ({ row }) => (
-        <div className="truncate max-w-[200px]" title={row.original.title}>
-          {/* <Button
-            variant="link"
-            className="p-0 h-auto text-left justify-start font-bold underline"
-            onClick={() => setRowAction({ row, type: "view" })}
-          >
-            <div className="whitespace-pre-line">
-              {row.original.title}
-            </div>
-          </Button> */}
-          {row.original.title}
-        </div>
-      ),
-      size: 200,
-      meta: { excelHeader: "입찰명" },
-    },
-
-    // ░░░ 원입찰번호 ░░░
-    {
-      accessorKey: "originalBiddingNumber",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="원입찰번호" />,
-      cell: ({ row }) => (
-        <div className="font-mono text-sm">
-          {row.original.originalBiddingNumber || '-'}
-        </div>
-      ),
-      size: 120,
-      meta: { excelHeader: "원입찰번호" },
-    },
-
-    // ░░░ 진행상태 ░░░
-    {
-      accessorKey: "status",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="진행상태" />,
-      cell: ({ row }) => (
-        <Badge variant={getStatusBadgeVariant(row.original.status)}>
-          {biddingStatusLabels[row.original.status]}
-        </Badge>
-      ),
-      size: 120,
-      meta: { excelHeader: "진행상태" },
-    },
-
-    // ░░░ 계약구분 ░░░
-    {
-      accessorKey: "contractType",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약구분" />,
-      cell: ({ row }) => (
-        <Badge variant="outline">
-          {contractTypeLabels[row.original.contractType]}
-        </Badge>
-      ),
-      size: 100,
-      meta: { excelHeader: "계약구분" },
-    },
-
-    // ░░░ 입찰서제출기간 ░░░
-    {
-      id: "submissionPeriod",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰서제출기간" />,
-      cell: ({ row }) => {
-        const startDate = row.original.submissionStartDate
-        const endDate = row.original.submissionEndDate
-        
-        if (!startDate || !endDate) return <span className="text-muted-foreground">-</span>
-        
-        const startObj = new Date(startDate)
-        const endObj = new Date(endDate)
-
-        // UI 표시용 KST 변환
-        const formatKst = (d: Date) => new Date(d.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
-        
-        return (
-          <div className="text-xs">
-            <div>
-              {formatKst(startObj)} ~ {formatKst(endObj)}
-            </div>
-          </div>
-        )
-      },
-      size: 140,
-      meta: { excelHeader: "입찰서제출기간" },
-    },
-
-    // ░░░ P/R번호 ░░░
-    {
-      accessorKey: "prNumber",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="P/R번호" />,
-      cell: ({ row }) => (
-        <span className="font-mono text-sm">{row.original.prNumber || '-'}</span>
-      ),
-      size: 100,
-      meta: { excelHeader: "P/R번호" },
-    },
-
-    // ░░░ 입찰담당자 ░░░
-    {
-      accessorKey: "bidPicName",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰담당자" />,
-      cell: ({ row }) => {
-        const bidPic = row.original.bidPicName
-        const supplyPic = row.original.supplyPicName
-
-        const displayName = bidPic || supplyPic || "-"
-        return <span className="text-sm">{displayName}</span>
-      },
-      size: 100,
-      meta: { excelHeader: "입찰담당자" },
-    },
-
-    // ░░░ 참여예정협력사 ░░░
-    {
-      id: "participantExpected",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="참여예정협력사" />,
-      cell: ({ row }) => (
-        <Button
-          variant="ghost"
-          size="sm"
-          className="h-auto p-1 hover:bg-blue-50"
-          onClick={() => onParticipantClick?.(row.original.id, 'expected')}
-          disabled={row.original.participantExpected === 0}
-        >
-          <div className="flex items-center gap-1">
-            <Users className="h-4 w-4 text-blue-500" />
-            <span className="text-sm font-medium">{row.original.participantExpected}</span>
-          </div>
-        </Button>
-      ),
-      size: 100,
-      meta: { excelHeader: "참여예정협력사" },
-    },
-
-    // ░░░ 참여협력사 ░░░
-    {
-      id: "participantParticipated",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="참여협력사" />,
-      cell: ({ row }) => (
-        <Button
-          variant="ghost"
-          size="sm"
-          className="h-auto p-1 hover:bg-green-50"
-          onClick={() => onParticipantClick?.(row.original.id, 'participated')}
-          disabled={row.original.participantParticipated === 0}
-        >
-          <div className="flex items-center gap-1">
-            <CheckCircle className="h-4 w-4 text-green-500" />
-            <span className="text-sm font-medium">{row.original.participantParticipated}</span>
-          </div>
-        </Button>
-      ),
-      size: 100,
-      meta: { excelHeader: "참여협력사" },
-    },
-
-    // ░░░ 포기협력사 ░░░
-    {
-      id: "participantDeclined",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="포기협력사" />,
-      cell: ({ row }) => (
-        <Button
-          variant="ghost"
-          size="sm"
-          className="h-auto p-1 hover:bg-red-50"
-          onClick={() => onParticipantClick?.(row.original.id, 'declined')}
-          disabled={row.original.participantDeclined === 0}
-        >
-          <div className="flex items-center gap-1">
-            <XCircle className="h-4 w-4 text-red-500" />
-            <span className="text-sm font-medium">{row.original.participantDeclined}</span>
-          </div>
-        </Button>
-      ),
-      size: 100,
-      meta: { excelHeader: "포기협력사" },
-    },
-
-    // ░░░ 미제출협력사 ░░░
-    {
-      id: "participantPending",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="미제출협력사" />,
-      cell: ({ row }) => (
-        <Button
-          variant="ghost"
-          size="sm"
-          className="h-auto p-1 hover:bg-yellow-50"
-          onClick={() => onParticipantClick?.(row.original.id, 'pending')}
-          disabled={row.original.participantPending === 0}
-        >
-          <div className="flex items-center gap-1">
-            <Clock className="h-4 w-4 text-yellow-500" />
-            <span className="text-sm font-medium">{row.original.participantPending}</span>
-          </div>
-        </Button>
-      ),
-      size: 100,
-      meta: { excelHeader: "미제출협력사" },
-    },
-
-    // ░░░ 개찰자명 ░░░
-    {
-      id: "openedBy",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="개찰자명" />,
-      cell: ({ row }) => {
-        const openedBy = row.original.openedBy
-        return <span className="text-sm">{openedBy || '-'}</span>
-      },
-      size: 100,
-      meta: { excelHeader: "개찰자명" },
-    },
-
-    // ░░░ 개찰일 ░░░
-    {
-      id: "openedAt",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="개찰일" />,
-      cell: ({ row }) => {
-        const openedAt = row.original.openedAt
-        return <span className="text-sm">{openedAt ? formatDate(openedAt, "KR") : '-'}</span>
-      },
-      size: 100,
-      meta: { excelHeader: "개찰일" },
-    },
-
-    // ░░░ 등록자 ░░░
-    {
-      accessorKey: "createdBy",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="등록자" />,
-      cell: ({ row }) => (
-        <span className="text-sm">{row.original.createdBy || '-'}</span>
-      ),
-      size: 100,
-      meta: { excelHeader: "등록자" },
-    },
-
-    // ░░░ 등록일시 ░░░
-    {
-      accessorKey: "createdAt",
-      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="등록일시" />,
-      cell: ({ row }) => (
-        <span className="text-sm">{row.original.createdAt ? formatDate(row.original.createdAt, "KR") : '-'}</span>
-      ),
-      size: 100,
-      meta: { excelHeader: "등록일시" },
-    },
-
-    // ═══════════════════════════════════════════════════════════════
-    // 액션
-    // ═══════════════════════════════════════════════════════════════
-    // {
-    //   id: "actions",
-    //   header: "액션",
-    //   cell: ({ row }) => (
-    //     <DropdownMenu>
-    //       <DropdownMenuTrigger asChild>
-    //         <Button variant="ghost" className="h-8 w-8 p-0">
-    //           <span className="sr-only">메뉴 열기</span>
-    //           <AlertTriangle className="h-4 w-4" />
-    //         </Button>
-    //       </DropdownMenuTrigger>
-    //       <DropdownMenuContent align="end">
-    //         <DropdownMenuItem onClick={() => setRowAction({ row, type: "view" })}>
-    //           <Eye className="mr-2 h-4 w-4" />
-    //           상세보기
-    //         </DropdownMenuItem>
-    //       </DropdownMenuContent>
-    //     </DropdownMenu>
-    //   ),
-    //   size: 50,
-    //   enableSorting: false,
-    //   enableHiding: false,
-    // },
-  ]
-}
+"use client"
+
+import * as React from "react"
+import { type ColumnDef } from "@tanstack/react-table"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Checkbox } from "@/components/ui/checkbox"
+import {
+  Eye, Calendar, Users, CheckCircle, XCircle, Clock, AlertTriangle
+} from "lucide-react"
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipProvider,
+  TooltipTrigger,
+} from "@/components/ui/tooltip"
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuSeparator,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+import {
+  biddingStatusLabels,
+  contractTypeLabels,
+} from "@/db/schema"
+import { formatDate } from "@/lib/utils"
+import { DataTableRowAction } from "@/types/table"
+
+type BiddingReceiveItem = {
+  id: number
+  biddingNumber: string
+  originalBiddingNumber: string | null
+  title: string
+  status: string
+  contractType: string
+  prNumber: string | null
+  submissionStartDate: Date | null
+  submissionEndDate: Date | null
+  bidPicName: string | null
+  supplyPicName: string | null
+  createdBy: string | null
+  createdAt: Date | null
+  updatedAt: Date | null
+
+  // 참여 현황
+  participantExpected: number
+  participantParticipated: number
+  participantDeclined: number
+  participantPending: number
+
+  // 개찰 정보
+  openedAt: Date | null
+  openedBy: string | null
+}
+
+interface GetColumnsProps {
+  setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<BiddingReceiveItem> | null>>
+  onParticipantClick?: (biddingId: number, participantType: 'expected' | 'participated' | 'declined' | 'pending') => void
+}
+
+// 상태별 배지 색상
+const getStatusBadgeVariant = (status: string) => {
+  switch (status) {
+    case 'received_quotation':
+      return 'secondary'
+    case 'bidding_opened':
+      return 'default'
+    case 'bidding_closed':
+      return 'outline'
+    default:
+      return 'outline'
+  }
+}
+
+// 금액 포맷팅
+const formatCurrency = (amount: string | number | null, currency = 'KRW') => {
+  if (!amount) return '-'
+
+  const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount
+  if (isNaN(numAmount)) return '-'
+
+  return new Intl.NumberFormat('ko-KR', {
+    style: 'currency',
+    currency: currency,
+    minimumFractionDigits: 0,
+    maximumFractionDigits: 0,
+  }).format(numAmount)
+}
+
+export function getBiddingsReceiveColumns({ setRowAction, onParticipantClick }: GetColumnsProps): ColumnDef<BiddingReceiveItem>[] {
+
+  return [
+    // ░░░ 선택 ░░░
+    {
+      id: "select",
+      header: "",
+      cell: ({ row }) => (
+        <Checkbox
+          checked={row.getIsSelected()}
+          onCheckedChange={(value) => {
+            // single select 모드에서는 다른 행들의 선택을 해제
+            row.toggleSelected(!!value)
+          }}
+          aria-label="행 선택"
+        />
+      ),
+      size: 50,
+      enableSorting: false,
+      enableHiding: false,
+    },
+
+    // ░░░ 입찰번호 ░░░
+    {
+      accessorKey: "biddingNumber",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰번호" />,
+      cell: ({ row }) => (
+        <div className="font-mono text-sm">
+          {row.original.biddingNumber}
+        </div>
+      ),
+      size: 120,
+      meta: { excelHeader: "입찰번호" },
+    },
+
+    // ░░░ 입찰명 ░░░
+    {
+      accessorKey: "title",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰명" />,
+      cell: ({ row }) => (
+        <div className="truncate max-w-[200px]" title={row.original.title}>
+          {/* <Button
+            variant="link"
+            className="p-0 h-auto text-left justify-start font-bold underline"
+            onClick={() => setRowAction({ row, type: "view" })}
+          >
+            <div className="whitespace-pre-line">
+              {row.original.title}
+            </div>
+          </Button> */}
+          {row.original.title}
+        </div>
+      ),
+      size: 200,
+      meta: { excelHeader: "입찰명" },
+    },
+
+    // ░░░ 원입찰번호 ░░░
+    {
+      accessorKey: "originalBiddingNumber",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="원입찰번호" />,
+      cell: ({ row }) => (
+        <div className="font-mono text-sm">
+          {row.original.originalBiddingNumber || '-'}
+        </div>
+      ),
+      size: 120,
+      meta: { excelHeader: "원입찰번호" },
+    },
+
+    // ░░░ 진행상태 ░░░
+    {
+      accessorKey: "status",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="진행상태" />,
+      cell: ({ row }) => (
+        <Badge variant={getStatusBadgeVariant(row.original.status)}>
+          {biddingStatusLabels[row.original.status]}
+        </Badge>
+      ),
+      size: 120,
+      meta: { excelHeader: "진행상태" },
+    },
+
+    // ░░░ 계약구분 ░░░
+    {
+      accessorKey: "contractType",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약구분" />,
+      cell: ({ row }) => (
+        <Badge variant="outline">
+          {contractTypeLabels[row.original.contractType]}
+        </Badge>
+      ),
+      size: 100,
+      meta: { excelHeader: "계약구분" },
+    },
+
+    // ░░░ 입찰서제출기간 ░░░
+    {
+      id: "submissionPeriod",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰서제출기간" />,
+      cell: ({ row }) => {
+        const startDate = row.original.submissionStartDate
+        const endDate = row.original.submissionEndDate
+        
+        if (!startDate || !endDate) return <span className="text-muted-foreground">-</span>
+        
+        const startObj = new Date(startDate)
+        const endObj = new Date(endDate)
+
+        // UI 표시용 KST 변환
+        const formatKst = (d: Date) => new Date(d.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
+        
+        return (
+          <div className="text-xs">
+            <div>
+              {formatKst(startObj)} ~ {formatKst(endObj)}
+            </div>
+          </div>
+        )
+      },
+      size: 140,
+      meta: { excelHeader: "입찰서제출기간" },
+    },
+
+    // ░░░ P/R번호 ░░░
+    {
+      accessorKey: "prNumber",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="P/R번호" />,
+      cell: ({ row }) => (
+        <span className="font-mono text-sm">{row.original.prNumber || '-'}</span>
+      ),
+      size: 100,
+      meta: { excelHeader: "P/R번호" },
+    },
+
+    // ░░░ 입찰담당자 ░░░
+    {
+      accessorKey: "bidPicName",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰담당자" />,
+      cell: ({ row }) => {
+        const bidPic = row.original.bidPicName
+        const supplyPic = row.original.supplyPicName
+
+        const displayName = bidPic || supplyPic || "-"
+        return <span className="text-sm">{displayName}</span>
+      },
+      size: 100,
+      meta: { excelHeader: "입찰담당자" },
+    },
+
+    // ░░░ 참여예정협력사 ░░░
+    {
+      id: "participantExpected",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="참여예정협력사" />,
+      cell: ({ row }) => (
+        <Button
+          variant="ghost"
+          size="sm"
+          className="h-auto p-1 hover:bg-blue-50"
+          onClick={() => onParticipantClick?.(row.original.id, 'expected')}
+          disabled={row.original.participantExpected === 0}
+        >
+          <div className="flex items-center gap-1">
+            <Users className="h-4 w-4 text-blue-500" />
+            <span className="text-sm font-medium">{row.original.participantExpected}</span>
+          </div>
+        </Button>
+      ),
+      size: 100,
+      meta: { excelHeader: "참여예정협력사" },
+    },
+
+    // ░░░ 참여협력사 ░░░
+    {
+      id: "participantParticipated",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="참여협력사" />,
+      cell: ({ row }) => (
+        <Button
+          variant="ghost"
+          size="sm"
+          className="h-auto p-1 hover:bg-green-50"
+          onClick={() => onParticipantClick?.(row.original.id, 'participated')}
+          disabled={row.original.participantParticipated === 0}
+        >
+          <div className="flex items-center gap-1">
+            <CheckCircle className="h-4 w-4 text-green-500" />
+            <span className="text-sm font-medium">{row.original.participantParticipated}</span>
+          </div>
+        </Button>
+      ),
+      size: 100,
+      meta: { excelHeader: "참여협력사" },
+    },
+
+    // ░░░ 포기협력사 ░░░
+    {
+      id: "participantDeclined",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="포기협력사" />,
+      cell: ({ row }) => (
+        <Button
+          variant="ghost"
+          size="sm"
+          className="h-auto p-1 hover:bg-red-50"
+          onClick={() => onParticipantClick?.(row.original.id, 'declined')}
+          disabled={row.original.participantDeclined === 0}
+        >
+          <div className="flex items-center gap-1">
+            <XCircle className="h-4 w-4 text-red-500" />
+            <span className="text-sm font-medium">{row.original.participantDeclined}</span>
+          </div>
+        </Button>
+      ),
+      size: 100,
+      meta: { excelHeader: "포기협력사" },
+    },
+
+    // ░░░ 미제출협력사 ░░░
+    {
+      id: "participantPending",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="미제출협력사" />,
+      cell: ({ row }) => (
+        <Button
+          variant="ghost"
+          size="sm"
+          className="h-auto p-1 hover:bg-yellow-50"
+          onClick={() => onParticipantClick?.(row.original.id, 'pending')}
+          disabled={row.original.participantPending === 0}
+        >
+          <div className="flex items-center gap-1">
+            <Clock className="h-4 w-4 text-yellow-500" />
+            <span className="text-sm font-medium">{row.original.participantPending}</span>
+          </div>
+        </Button>
+      ),
+      size: 100,
+      meta: { excelHeader: "미제출협력사" },
+    },
+
+    // ░░░ 개찰자명 ░░░
+    {
+      id: "openedBy",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="개찰자명" />,
+      cell: ({ row }) => {
+        const openedBy = row.original.openedBy
+        return <span className="text-sm">{openedBy || '-'}</span>
+      },
+      size: 100,
+      meta: { excelHeader: "개찰자명" },
+    },
+
+    // ░░░ 개찰일 ░░░
+    {
+      id: "openedAt",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="개찰일" />,
+      cell: ({ row }) => {
+        const openedAt = row.original.openedAt
+        return <span className="text-sm">{openedAt ? formatDate(openedAt, "KR") : '-'}</span>
+      },
+      size: 100,
+      meta: { excelHeader: "개찰일" },
+    },
+
+    // ░░░ 등록자 ░░░
+    {
+      accessorKey: "createdBy",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="등록자" />,
+      cell: ({ row }) => (
+        <span className="text-sm">{row.original.createdBy || '-'}</span>
+      ),
+      size: 100,
+      meta: { excelHeader: "등록자" },
+    },
+
+    // ░░░ 등록일시 ░░░
+    {
+      accessorKey: "createdAt",
+      header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="등록일시" />,
+      cell: ({ row }) => (
+        <span className="text-sm">{row.original.createdAt ? formatDate(row.original.createdAt, "KR") : '-'}</span>
+      ),
+      size: 100,
+      meta: { excelHeader: "등록일시" },
+    },
+
+    // ═══════════════════════════════════════════════════════════════
+    // 액션
+    // ═══════════════════════════════════════════════════════════════
+    // {
+    //   id: "actions",
+    //   header: "액션",
+    //   cell: ({ row }) => (
+    //     <DropdownMenu>
+    //       <DropdownMenuTrigger asChild>
+    //         <Button variant="ghost" className="h-8 w-8 p-0">
+    //           <span className="sr-only">메뉴 열기</span>
+    //           <AlertTriangle className="h-4 w-4" />
+    //         </Button>
+    //       </DropdownMenuTrigger>
+    //       <DropdownMenuContent align="end">
+    //         <DropdownMenuItem onClick={() => setRowAction({ row, type: "view" })}>
+    //           <Eye className="mr-2 h-4 w-4" />
+    //           상세보기
+    //         </DropdownMenuItem>
+    //       </DropdownMenuContent>
+    //     </DropdownMenu>
+    //   ),
+    //   size: 50,
+    //   enableSorting: false,
+    //   enableHiding: false,
+    // },
+  ]
+}
-- 
cgit v1.2.3


From c0e1cc06e0c67cb4a941889a3d63d312d1fb8fce Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 8 Dec 2025 03:02:42 +0000
Subject: (최겸) 구매 입찰계약 수정, 입찰기간수정
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../bidding/manage/bidding-schedule-editor.tsx     |   15 +-
 db/schema/bidding.ts                               |    1 +
 lib/bidding/detail/service.ts                      |   45 +-
 .../bidding-detail-vendor-toolbar-actions.tsx      |    8 +-
 lib/bidding/list/biddings-table-columns.tsx        |   31 +-
 lib/bidding/list/export-biddings-to-excel.ts       |    9 +-
 lib/bidding/receive/biddings-receive-columns.tsx   |    6 +-
 .../selection/biddings-selection-columns.tsx       |    7 +-
 lib/bidding/service.ts                             |   39 +-
 .../vendor/export-partners-biddings-to-excel.ts    |    9 +-
 lib/bidding/vendor/partners-bidding-detail.tsx     |   11 +-
 .../vendor/partners-bidding-list-columns.tsx       |    8 +-
 .../approval-template-variables.ts                 |  270 +--
 .../general-contract-approval-request-dialog.tsx   | 2381 ++++++++++----------
 .../detail/general-contract-items-table.tsx        |    2 +-
 15 files changed, 1449 insertions(+), 1393 deletions(-)

(limited to 'lib/bidding/receive/biddings-receive-columns.tsx')

diff --git a/components/bidding/manage/bidding-schedule-editor.tsx b/components/bidding/manage/bidding-schedule-editor.tsx
index 32ce6940..72961c3d 100644
--- a/components/bidding/manage/bidding-schedule-editor.tsx
+++ b/components/bidding/manage/bidding-schedule-editor.tsx
@@ -151,15 +151,18 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc
     return new Date(kstTime).toISOString().slice(0, 16)
   }
 
-  // timestamp에서 시간(HH:MM) 추출 (KST 기준)
+  // timestamp에서 시간(HH:MM) 추출
+  // 수정: Backend에서 setUTCHours로 저장했으므로, 읽을 때도 getUTCHours로 읽어야
+  // 브라우저 타임존(KST)의 간섭 없이 원래 저장한 숫자를 가져올 수 있습니다.
   const extractTimeFromTimestamp = (date: string | Date | undefined | null): string => {
     if (!date) return ''
     const d = new Date(date)
-    // UTC 시간에 9시간을 더함
-    const kstTime = d.getTime() + (9 * 60 * 60 * 1000)
-    const kstDate = new Date(kstTime)
-    const hours = kstDate.getUTCHours().toString().padStart(2, '0')
-    const minutes = kstDate.getUTCMinutes().toString().padStart(2, '0')
+
+    // 중요: Backend에서 setUTCHours로 저장했으므로, 읽을 때도 getUTCHours로 읽어야
+    // 브라우저 타임존(KST)의 간섭 없이 원래 저장한 숫자(09:00)를 가져올 수 있습니다.
+    const hours = d.getUTCHours().toString().padStart(2, '0')
+    const minutes = d.getUTCMinutes().toString().padStart(2, '0')
+
     return `${hours}:${minutes}`
   }
 
diff --git a/db/schema/bidding.ts b/db/schema/bidding.ts
index c5370174..8e5fe823 100644
--- a/db/schema/bidding.ts
+++ b/db/schema/bidding.ts
@@ -196,6 +196,7 @@ export const biddings = pgTable('biddings', {
   // PR 정보
   prNumber: varchar('pr_number', { length: 50 }), // PR No.
   hasPrDocument: boolean('has_pr_document').default(false), // PR 문서 여부
+  plant: varchar('plant', { length: 10 }), // 플랜트 코드(WERKS), ECC 연동 시 설정
   
   // 상태 및 설정
   status: biddingStatusEnum('status').default('bidding_generated').notNull(),
diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts
index 99591e3b..eec3f253 100644
--- a/lib/bidding/detail/service.ts
+++ b/lib/bidding/detail/service.ts
@@ -854,15 +854,14 @@ export async function registerBidding(biddingId: number, userId: string) {
     await db.transaction(async (tx) => {
       debugLog('registerBidding: Transaction started')
       
-      // 0. 입찰서 제출기간 계산 (오프셋 기반)
+      // 0. 입찰서 제출기간 계산 (입력값 절대 기준)
       const { submissionStartOffset, submissionDurationDays, submissionStartDate, submissionEndDate } = bidding
       
       let calculatedStartDate = bidding.submissionStartDate
       let calculatedEndDate = bidding.submissionEndDate
 
-      // 오프셋 값이 있으면 날짜 계산
       if (submissionStartOffset !== null && submissionDurationDays !== null) {
-        // 시간 추출 (기본값: 시작 09:00, 마감 18:00)
+        // DB에 저장된 시간을 숫자 그대로 가져옴 (예: 10:00 저장 → 10 반환)
         const startTime = submissionStartDate 
           ? { hours: submissionStartDate.getUTCHours(), minutes: submissionStartDate.getUTCMinutes() }
           : { hours: 9, minutes: 0 }
@@ -870,22 +869,30 @@ export async function registerBidding(biddingId: number, userId: string) {
           ? { hours: submissionEndDate.getUTCHours(), minutes: submissionEndDate.getUTCMinutes() }
           : { hours: 18, minutes: 0 }
 
-        // baseDate = 현재일 날짜만 (00:00:00)
-        const baseDate = new Date()
-        baseDate.setHours(0, 0, 0, 0)
-
-        // 시작일 = baseDate + offset일 + 시작시간
-        calculatedStartDate = new Date(baseDate)
-        calculatedStartDate.setDate(calculatedStartDate.getDate() + submissionStartOffset)
-        calculatedStartDate.setHours(startTime.hours, startTime.minutes, 0, 0)
-
-        // 마감일 = 시작일(날짜만) + duration일 + 마감시간
-        calculatedEndDate = new Date(calculatedStartDate)
-        calculatedEndDate.setHours(0, 0, 0, 0)
-        calculatedEndDate.setDate(calculatedEndDate.getDate() + submissionDurationDays)
-        calculatedEndDate.setHours(endTime.hours, endTime.minutes, 0, 0)
-
-        debugLog('registerBidding: Submission dates calculated', {
+        // 서버의 오늘 날짜(년/월/일)를 그대로 사용해 00:00 UTC 시점 생성
+        const now = new Date()
+        const baseDate = new Date(Date.UTC(
+          now.getFullYear(),
+          now.getMonth(),
+          now.getDate(),
+          0, 0, 0
+        ))
+
+        // 시작일 = baseDate + offset일 + 입력 시간(숫자 그대로)
+        const tempStartDate = new Date(baseDate)
+        tempStartDate.setUTCDate(tempStartDate.getUTCDate() + submissionStartOffset)
+        tempStartDate.setUTCHours(startTime.hours, startTime.minutes, 0, 0)
+
+        // 마감일 = 시작일 날짜만 기준 + duration일 + 입력 마감 시간
+        const tempEndDate = new Date(tempStartDate)
+        tempEndDate.setUTCHours(0, 0, 0, 0)
+        tempEndDate.setUTCDate(tempEndDate.getUTCDate() + submissionDurationDays)
+        tempEndDate.setUTCHours(endTime.hours, endTime.minutes, 0, 0)
+
+        calculatedStartDate = tempStartDate
+        calculatedEndDate = tempEndDate
+
+        debugLog('registerBidding: Submission dates calculated (Input Value Based)', {
           baseDate: baseDate.toISOString(),
           calculatedStartDate: calculatedStartDate.toISOString(),
           calculatedEndDate: calculatedEndDate.toISOString(),
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 d3df141a..e934a5fe 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
@@ -154,8 +154,12 @@ export function BiddingDetailVendorToolbarActions({
           title: "성공",
           description: '차수증가가 완료되었습니다.',
         })
-        router.push(`/evcp/bid`)
-        onSuccess()
+        if (result.biddingId) {
+          router.push(`/evcp/bid/${result.biddingId}/info`)
+        } else {
+          router.push(`/evcp/bid`)
+        }
+        // onSuccess()
       } else {
         toast({
           title: "오류",
diff --git a/lib/bidding/list/biddings-table-columns.tsx b/lib/bidding/list/biddings-table-columns.tsx
index 62d4dbe7..602bcbb9 100644
--- a/lib/bidding/list/biddings-table-columns.tsx
+++ b/lib/bidding/list/biddings-table-columns.tsx
@@ -257,21 +257,40 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef
       id: "submissionPeriod",
       header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰서제출기간" />,
       cell: ({ row }) => {
+        const status = row.original.status
+
+        // 입찰생성 또는 결재진행중 상태일 때는 특별 메시지 표시
+        if (status === 'bidding_generated') {
+          return (
+            <div className="text-xs text-orange-600 font-medium">
+              입찰 등록중입니다
+            </div>
+          )
+        }
+
+        if (status === 'approval_pending') {
+          return (
+            <div className="text-xs text-blue-600 font-medium">
+              결재 진행중입니다
+            </div>
+          )
+        }
+
         const startDate = row.original.submissionStartDate
         const endDate = row.original.submissionEndDate
-        
+
         if (!startDate || !endDate) return <span className="text-muted-foreground">-</span>
-        
+
         const startObj = new Date(startDate)
         const endObj = new Date(endDate)
 
-        // UI 표시용 KST 변환
-        const formatKst = (d: Date) => new Date(d.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
-        
+        // 입력값 기반: 저장된 UTC 값 그대로 표시 (타임존 가감 없음)
+        const formatValue = (d: Date) => d.toISOString().slice(0, 16).replace('T', ' ')
+
         return (
           <div className="text-xs">
             <div>
-              {formatKst(startObj)} ~ {formatKst(endObj)}
+              {formatValue(startObj)} ~ {formatValue(endObj)}
             </div>
           </div>
         )
diff --git a/lib/bidding/list/export-biddings-to-excel.ts b/lib/bidding/list/export-biddings-to-excel.ts
index 8b13e38d..64d98399 100644
--- a/lib/bidding/list/export-biddings-to-excel.ts
+++ b/lib/bidding/list/export-biddings-to-excel.ts
@@ -83,13 +83,10 @@ export async function exportBiddingsToExcel(
             const startObj = new Date(startDate)
             const endObj = new Date(endDate)
             
-            // KST 변환 (UTC+9)
-            const formatKst = (d: Date) => {
-              const kstDate = new Date(d.getTime() + 9 * 60 * 60 * 1000)
-              return kstDate.toISOString().slice(0, 16).replace('T', ' ')
-            }
+            // 입력값 기반: 저장된 UTC 값을 그대로 표시 (타임존 가감 없음)
+            const formatValue = (d: Date) => d.toISOString().slice(0, 16).replace('T', ' ')
             
-            value = `${formatKst(startObj)} ~ ${formatKst(endObj)}`
+            value = `${formatValue(startObj)} ~ ${formatValue(endObj)}`
           }
           break
 
diff --git a/lib/bidding/receive/biddings-receive-columns.tsx b/lib/bidding/receive/biddings-receive-columns.tsx
index f2e2df17..6847d9d5 100644
--- a/lib/bidding/receive/biddings-receive-columns.tsx
+++ b/lib/bidding/receive/biddings-receive-columns.tsx
@@ -199,13 +199,13 @@ export function getBiddingsReceiveColumns({ setRowAction, onParticipantClick }:
         const startObj = new Date(startDate)
         const endObj = new Date(endDate)
 
-        // UI 표시용 KST 변환
-        const formatKst = (d: Date) => new Date(d.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
+        // 입력값 기반: 저장된 UTC 값 그대로 표시 (타임존 가감 없음)
+        const formatValue = (d: Date) => d.toISOString().slice(0, 16).replace('T', ' ')
         
         return (
           <div className="text-xs">
             <div>
-              {formatKst(startObj)} ~ {formatKst(endObj)}
+              {formatValue(startObj)} ~ {formatValue(endObj)}
             </div>
           </div>
         )
diff --git a/lib/bidding/selection/biddings-selection-columns.tsx b/lib/bidding/selection/biddings-selection-columns.tsx
index 87c489e3..030fc05b 100644
--- a/lib/bidding/selection/biddings-selection-columns.tsx
+++ b/lib/bidding/selection/biddings-selection-columns.tsx
@@ -177,14 +177,13 @@ export function getBiddingsSelectionColumns({ setRowAction }: GetColumnsProps):
 
         const startObj = new Date(startDate)
         const endObj = new Date(endDate)
-        // 비교로직만 유지, 색상표기/마감뱃지 제거
-        // UI 표시용 KST 변환
-        const formatKst = (d: Date) => new Date(d.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
+        // 입력값 기반: 저장된 UTC 값 그대로 표시 (타임존 가감 없음)
+        const formatValue = (d: Date) => d.toISOString().slice(0, 16).replace('T', ' ')
 
         return (
           <div className="text-xs">
             <div>
-              {formatKst(startObj)} ~ {formatKst(endObj)}
+              {formatValue(startObj)} ~ {formatValue(endObj)}
             </div>
           </div>
         )
diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts
index 71ee01ab..ed20ad0c 100644
--- a/lib/bidding/service.ts
+++ b/lib/bidding/service.ts
@@ -3006,6 +3006,7 @@ export async function increaseRoundOrRebid(biddingId: number, userId: string | u
           
           // 구매조직
           purchasingOrganization: existingBidding.purchasingOrganization,
+          plant: existingBidding.plant,
           
           // 담당자 정보 복제
           bidPicId: existingBidding.bidPicId,
@@ -3323,7 +3324,6 @@ export async function increaseRoundOrRebid(biddingId: number, userId: string | u
       revalidatePath('/evcp/bid-receive')
       revalidatePath('/evcp/bid')
       revalidatePath(`/bid-receive/${biddingId}`) // 기존 입찰 페이지도 갱신
-      revalidatePath(`/bid-receive/${newBidding.id}`)
 
       return {
         success: true,
@@ -4139,6 +4139,41 @@ export async function getBiddingSelectionItemsAndPrices(biddingId: number) {
  */
 export async function checkAndSaveChemicalSubstancesForBidding(biddingId: number) {
   try {
+    const [biddingInfo] = await db
+      .select({
+        id: biddings.id,
+        ANFNR: biddings.ANFNR,
+        plant: biddings.plant,
+      })
+      .from(biddings)
+      .where(eq(biddings.id, biddingId))
+      .limit(1)
+
+    if (!biddingInfo) {
+      return {
+        success: false,
+        message: '입찰 정보를 찾을 수 없습니다.',
+        results: []
+      }
+    }
+
+    if (!biddingInfo.ANFNR) {
+      return {
+        success: true,
+        message: 'SAP PR 연동 입찰이 아니므로 화학물질 검사를 건너뜁니다.',
+        results: []
+      }
+    }
+
+    const biddingWerks = biddingInfo.plant?.trim()
+    if (!biddingWerks) {
+      return {
+        success: false,
+        message: '입찰의 플랜트(WERKS) 정보가 없어 화학물질 검사를 진행할 수 없습니다.',
+        results: []
+      }
+    }
+
     // 입찰의 모든 참여업체 조회 (벤더 코드 있는 것만)
     const biddingCompaniesList = await db
       .select({
@@ -4222,7 +4257,7 @@ export async function checkAndSaveChemicalSubstancesForBidding(biddingId: number
           try {
             const checkResult = await checkChemicalSubstance({
               bukrs: 'H100', // 회사코드는 H100 고정
-              werks: 'PM11', // WERKS는 PM11 고정
+              werks: biddingWerks,
               lifnr: biddingCompany.vendors!.vendorCode!,
               matnr: materialNumber
             })
diff --git a/lib/bidding/vendor/export-partners-biddings-to-excel.ts b/lib/bidding/vendor/export-partners-biddings-to-excel.ts
index 9e99eeec..e1d985fe 100644
--- a/lib/bidding/vendor/export-partners-biddings-to-excel.ts
+++ b/lib/bidding/vendor/export-partners-biddings-to-excel.ts
@@ -124,13 +124,10 @@ export async function exportPartnersBiddingsToExcel(
             const startObj = new Date(startDate)
             const endObj = new Date(endDate)
             
-            // KST 변환 (UTC+9)
-            const formatKst = (d: Date) => {
-              const kstDate = new Date(d.getTime() + 9 * 60 * 60 * 1000)
-              return kstDate.toISOString().slice(0, 16).replace('T', ' ')
-            }
+            // 입력값 기반: 저장된 UTC 값을 그대로 표시 (타임존 가감 없음)
+            const formatValue = (d: Date) => d.toISOString().slice(0, 16).replace('T', ' ')
             
-            value = `${formatKst(startObj)} ~ ${formatKst(endObj)}`
+            value = `${formatValue(startObj)} ~ ${formatValue(endObj)}`
           }
           break
 
diff --git a/lib/bidding/vendor/partners-bidding-detail.tsx b/lib/bidding/vendor/partners-bidding-detail.tsx
index 087648ab..bf33cef5 100644
--- a/lib/bidding/vendor/partners-bidding-detail.tsx
+++ b/lib/bidding/vendor/partners-bidding-detail.tsx
@@ -868,7 +868,8 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
                   const timeLeft = deadline.getTime() - now.getTime()
                   const daysLeft = Math.floor(timeLeft / (1000 * 60 * 60 * 24))
                   const hoursLeft = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
-                  const kstDeadline = new Date(deadline.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
+                  // 입력값 기반: 저장된 UTC 값을 그대로 표시
+                  const displayDeadline = deadline.toISOString().slice(0, 16).replace('T', ' ')
 
                   return (
                     <div className={`p-3 rounded-lg border-2 ${
@@ -883,7 +884,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
                           <Calendar className="w-5 h-5" />
                           <span className="font-medium">제출 마감일:</span>
                           <span className="text-lg font-semibold">
-                            {kstDeadline}
+                            {displayDeadline}
                           </span>
                         </div>
                         {isExpired ? (
@@ -920,9 +921,9 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
                   <span className="font-medium">입찰서 제출기간:</span> {(() => {
                     const start = new Date(biddingDetail.submissionStartDate!)
                     const end = new Date(biddingDetail.submissionEndDate!)
-                    const kstStart = new Date(start.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
-                    const kstEnd = new Date(end.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
-                    return `${kstStart} ~ ${kstEnd}`
+                    const displayStart = start.toISOString().slice(0, 16).replace('T', ' ')
+                    const displayEnd = end.toISOString().slice(0, 16).replace('T', ' ')
+                    return `${displayStart} ~ ${displayEnd}`
                   })()}
                 </div>
               )}
diff --git a/lib/bidding/vendor/partners-bidding-list-columns.tsx b/lib/bidding/vendor/partners-bidding-list-columns.tsx
index 6276d1b7..09c3caad 100644
--- a/lib/bidding/vendor/partners-bidding-list-columns.tsx
+++ b/lib/bidding/vendor/partners-bidding-list-columns.tsx
@@ -352,14 +352,14 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
         const startObj = new Date(startDate)
         const endObj = new Date(endDate)
 
-        // UI 표시용 KST 변환
-        const formatKst = (d: Date) => new Date(d.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
+        // 입력값 기반: 저장된 UTC 값 그대로 표시 (타임존 가감 없음)
+        const formatValue = (d: Date) => d.toISOString().slice(0, 16).replace('T', ' ')
 
         return (
           <div className="text-sm">
-            <div>{formatKst(startObj)}</div>
+            <div>{formatValue(startObj)}</div>
             <div className="text-muted-foreground">~</div>
-            <div>{formatKst(endObj)}</div>
+            <div>{formatValue(endObj)}</div>
           </div>
         )
       },
diff --git a/lib/general-contracts/approval-template-variables.ts b/lib/general-contracts/approval-template-variables.ts
index 6924694e..710e6101 100644
--- a/lib/general-contracts/approval-template-variables.ts
+++ b/lib/general-contracts/approval-template-variables.ts
@@ -144,129 +144,75 @@ export async function mapContractToApprovalTemplateVariables(
     issuer: string;
   }> = [];
 
-  // 계약보증
-  if (basicInfo.contractBond) {
-    const bond = typeof basicInfo.contractBond === 'string'
-      ? JSON.parse(basicInfo.contractBond)
-      : basicInfo.contractBond;
-    
-    if (bond && Array.isArray(bond)) {
-      bond.forEach((b: any, idx: number) => {
-        guarantees.push({
-          type: '계약보증',
-          order: idx + 1,
-          bondNumber: b.bondNumber || '',
-          rate: b.rate ? `${b.rate}%` : '',
-          amount: formatCurrency(b.amount),
-          period: b.period || '',
-          startDate: formatDate(b.startDate),
-          endDate: formatDate(b.endDate),
-          issuer: b.issuer || '',
-        });
-      });
-    }
-  }
-
-  // 지급보증
-  if (basicInfo.paymentBond) {
-    const bond = typeof basicInfo.paymentBond === 'string'
-      ? JSON.parse(basicInfo.paymentBond)
-      : basicInfo.paymentBond;
-    
-    if (bond && Array.isArray(bond)) {
-      bond.forEach((b: any, idx: number) => {
-        guarantees.push({
-          type: '지급보증',
-          order: idx + 1,
-          bondNumber: b.bondNumber || '',
-          rate: b.rate ? `${b.rate}%` : '',
-          amount: formatCurrency(b.amount),
-          period: b.period || '',
-          startDate: formatDate(b.startDate),
-          endDate: formatDate(b.endDate),
-          issuer: b.issuer || '',
-        });
-      });
-    }
-  }
-
-  // 하자보증
-  if (basicInfo.defectBond) {
-    const bond = typeof basicInfo.defectBond === 'string'
-      ? JSON.parse(basicInfo.defectBond)
-      : basicInfo.defectBond;
-    
-    if (bond && Array.isArray(bond)) {
-      bond.forEach((b: any, idx: number) => {
-        guarantees.push({
-          type: '하자보증',
-          order: idx + 1,
-          bondNumber: b.bondNumber || '',
-          rate: b.rate ? `${b.rate}%` : '',
-          amount: formatCurrency(b.amount),
-          period: b.period || '',
-          startDate: formatDate(b.startDate),
-          endDate: formatDate(b.endDate),
-          issuer: b.issuer || '',
-        });
-      });
-    }
-  }
-
-  // 보증 전체 비고
-  const guaranteeNote = basicInfo.guaranteeNote || '';
-
-  // 하도급 체크리스트
-  const checklistItems: Array<{
-    category: string;
-    item1: string;
-    item2: string;
-    result: string;
-    department: string;
-    cause: string;
-    measure: string;
-  }> = [];
+  // // 계약보증 (첫 번째 항목만 사용)
+  // if (basicInfo.contractBond) {
+  //   const bond = typeof basicInfo.contractBond === 'string'
+  //     ? JSON.parse(basicInfo.contractBond)
+  //     : basicInfo.contractBond;
+
+  //   if (bond && Array.isArray(bond) && bond.length > 0) {
+  //     const b = bond[0];
+  //     guarantees.push({
+  //       type: '계약보증',
+  //       order: 1,
+  //       bondNumber: b.bondNumber || '',
+  //       rate: b.rate ? `${b.rate}%` : '',
+  //       amount: formatCurrency(b.amount),
+  //       period: b.period || '',
+  //       startDate: formatDate(b.startDate),
+  //       endDate: formatDate(b.endDate),
+  //       issuer: b.issuer || '',
+  //     });
+  //   }
+  // }
+
+  // // 지급보증 (첫 번째 항목만 사용)
+  // if (basicInfo.paymentBond) {
+  //   const bond = typeof basicInfo.paymentBond === 'string'
+  //     ? JSON.parse(basicInfo.paymentBond)
+  //     : basicInfo.paymentBond;
+
+  //   if (bond && Array.isArray(bond) && bond.length > 0) {
+  //     const b = bond[0];
+  //     guarantees.push({
+  //       type: '지급보증',
+  //       order: 1,
+  //       bondNumber: b.bondNumber || '',
+  //       rate: b.rate ? `${b.rate}%` : '',
+  //       amount: formatCurrency(b.amount),
+  //       period: b.period || '',
+  //       startDate: formatDate(b.startDate),
+  //       endDate: formatDate(b.endDate),
+  //       issuer: b.issuer || '',
+  //     });
+  //   }
+  // }
+
+  // // 하자보증 (첫 번째 항목만 사용)
+  // if (basicInfo.defectBond) {
+  //   const bond = typeof basicInfo.defectBond === 'string'
+  //     ? JSON.parse(basicInfo.defectBond)
+  //     : basicInfo.defectBond;
+
+  //   if (bond && Array.isArray(bond) && bond.length > 0) {
+  //     const b = bond[0];
+  //     guarantees.push({
+  //       type: '하자보증',
+  //       order: 1,
+  //       bondNumber: b.bondNumber || '',
+  //       rate: b.rate ? `${b.rate}%` : '',
+  //       amount: formatCurrency(b.amount),
+  //       period: b.period || '',
+  //       startDate: formatDate(b.startDate),
+  //       endDate: formatDate(b.endDate),
+  //       issuer: b.issuer || '',
+  //     });
+  //   }
+  // }
+
+  // // 보증 전체 비고
+  // const guaranteeNote = basicInfo.guaranteeNote || '';
 
-  if (subcontractChecklist) {
-    // 1-1. 작업 시 서면 발급
-    checklistItems.push({
-      category: '계약 시 [계약 체결 단계]',
-      item1: '1-1. 작업 시 서면 발급',
-      item2: '-',
-      result: subcontractChecklist.workDocumentIssued === '준수' ? '준수' : 
-              subcontractChecklist.workDocumentIssued === '위반' ? '위반' : 
-              subcontractChecklist.workDocumentIssued === '위반의심' ? '위반의심' : '',
-      department: subcontractChecklist.workDocumentIssuedDepartment || '',
-      cause: subcontractChecklist.workDocumentIssuedCause || '',
-      measure: subcontractChecklist.workDocumentIssuedMeasure || '',
-    });
-
-    // 1-2. 6대 법정 기재사항 명기 여부
-    checklistItems.push({
-      category: '계약 시 [계약 체결 단계]',
-      item1: '1-2. 6대 법정 기재사항 명기 여부',
-      item2: '-',
-      result: subcontractChecklist.sixLegalItems === '준수' ? '준수' : 
-              subcontractChecklist.sixLegalItems === '위반' ? '위반' : 
-              subcontractChecklist.sixLegalItems === '위반의심' ? '위반의심' : '',
-      department: subcontractChecklist.sixLegalItemsDepartment || '',
-      cause: subcontractChecklist.sixLegalItemsCause || '',
-      measure: subcontractChecklist.sixLegalItemsMeasure || '',
-    });
-
-    // 2. 부당 하도급 대금 결정 행위
-    checklistItems.push({
-      category: '계약 시 [계약 체결 단계]',
-      item1: '-',
-      item2: '2. 부당 하도급 대금 결정 행위 (대금 결정 방법)',
-      result: subcontractChecklist.unfairSubcontractPrice === '준수' ? '준수' : 
-              subcontractChecklist.unfairSubcontractPrice === '위반' ? '위반' : 
-              subcontractChecklist.unfairSubcontractPrice === '위반의심' ? '위반의심' : '',
-      department: subcontractChecklist.unfairSubcontractPriceDepartment || '',
-      cause: subcontractChecklist.unfairSubcontractPriceCause || '',
-      measure: subcontractChecklist.unfairSubcontractPriceMeasure || '',
-    });
-  }
 
   // 총 계약 금액 계산
   const totalContractAmount = items.reduce((sum, item) => {
@@ -338,31 +284,61 @@ export async function mapContractToApprovalTemplateVariables(
   // 총 계약 금액
   variables['총_계약금액'] = formatCurrency(totalContractAmount);
 
-  // 보증 정보 변수
-  guarantees.forEach((guarantee, index) => {
-    const idx = index + 1;
-    const typeKey = String(guarantee.type);
-    variables[`${typeKey}_차수_${idx}`] = String(guarantee.order);
-    variables[`${typeKey}_증권번호_${idx}`] = String(guarantee.bondNumber || '');
-    variables[`${typeKey}_보증금율_${idx}`] = String(guarantee.rate || '');
-    variables[`${typeKey}_보증금액_${idx}`] = String(guarantee.amount || '');
-    variables[`${typeKey}_보증기간_${idx}`] = String(guarantee.period || '');
-    variables[`${typeKey}_시작일_${idx}`] = String(guarantee.startDate || '');
-    variables[`${typeKey}_종료일_${idx}`] = String(guarantee.endDate || '');
-    variables[`${typeKey}_발행기관_${idx}`] = String(guarantee.issuer || '');
-  });
-
-  // 보증 전체 비고
-  variables['보증_전체_비고'] = String(guaranteeNote);
-
-  // 하도급 체크리스트 변수
-  checklistItems.forEach((item, index) => {
-    const idx = index + 1;
-    variables[`점검결과_${idx === 1 ? '1_1' : idx === 2 ? '1_2' : '2'}`] = String(item.result);
-    variables[`귀책부서_${idx === 1 ? '1_1' : idx === 2 ? '1_2' : '2'}`] = String(item.department);
-    variables[`원인_${idx === 1 ? '1_1' : idx === 2 ? '1_2' : '2'}`] = String(item.cause);
-    variables[`대책_${idx === 1 ? '1_1' : idx === 2 ? '1_2' : '2'}`] = String(item.measure);
-  });
+  // // 보증 정보 변수 (첫 번째 항목만 사용)
+  // const contractGuarantee = guarantees.find(g => g.type === '계약보증');
+  // if (contractGuarantee) {
+  //   variables['계약보증_차수_1'] = String(contractGuarantee.order);
+  //   variables['계약보증_증권번호_1'] = String(contractGuarantee.bondNumber || '');
+  //   variables['계약보증_보증금율_1'] = String(contractGuarantee.rate || '');
+  //   variables['계약보증_보증금액_1'] = String(contractGuarantee.amount || '');
+  //   variables['계약보증_보증기간_1'] = String(contractGuarantee.period || '');
+  //   variables['계약보증_시작일_1'] = String(contractGuarantee.startDate || '');
+  //   variables['계약보증_종료일_1'] = String(contractGuarantee.endDate || '');
+  //   variables['계약보증_발행기관_1'] = String(contractGuarantee.issuer || '');
+  //   variables['계약보증_비고_1'] = String(guaranteeNote); // 기존 보증 전체 비고를 계약보증 비고로 사용
+  // }
+
+  // const paymentGuarantee = guarantees.find(g => g.type === '지급보증');
+  // if (paymentGuarantee) {
+  //   variables['지급보증_차수_1'] = String(paymentGuarantee.order);
+  //   variables['지급보증_증권번호_1'] = String(paymentGuarantee.bondNumber || '');
+  //   variables['지급보증_보증금율_1'] = String(paymentGuarantee.rate || '');
+  //   variables['지급보증_보증금액_1'] = String(paymentGuarantee.amount || '');
+  //   variables['지급보증_보증기간_1'] = String(paymentGuarantee.period || '');
+  //   variables['지급보증_시작일_1'] = String(paymentGuarantee.startDate || '');
+  //   variables['지급보증_종료일_1'] = String(paymentGuarantee.endDate || '');
+  //   variables['지급보증_발행기관_1'] = String(paymentGuarantee.issuer || '');
+  //   variables['지급보증_비고_1'] = String(guaranteeNote); // 기존 보증 전체 비고를 지급보증 비고로 사용
+  // }
+
+  // const defectGuarantee = guarantees.find(g => g.type === '하자보증');
+  // if (defectGuarantee) {
+  //   variables['하자보증_차수_1'] = String(defectGuarantee.order);
+  //   variables['하자보증_증권번호_1'] = String(defectGuarantee.bondNumber || '');
+  //   variables['하자보증_보증금율_1'] = String(defectGuarantee.rate || '');
+  //   variables['하자보증_보증금액_1'] = String(defectGuarantee.amount || '');
+  //   variables['하자보증_보증기간_1'] = String(defectGuarantee.period || '');
+  //   variables['하자보증_시작일_1'] = String(defectGuarantee.startDate || '');
+  //   variables['하자보증_종료일_1'] = String(defectGuarantee.endDate || '');
+  //   variables['하자보증_발행기관_1'] = String(defectGuarantee.issuer || '');
+  //   variables['하자보증_비고_1'] = String(guaranteeNote); // 기존 보증 전체 비고를 하자보증 비고로 사용
+  // }
+
+  // 하도급 체크리스트 변수 (새로운 템플릿 구조에 맞춤)
+  if (subcontractChecklist) {
+    variables['작업전_서면발급_체크'] = String(subcontractChecklist.workDocumentIssuedCheck || subcontractChecklist.workDocumentIssued || '');
+    variables['기재사항_1'] = String(subcontractChecklist.legalItem1 || subcontractChecklist.sixLegalItems1 || '');
+    variables['기재사항_2'] = String(subcontractChecklist.legalItem2 || subcontractChecklist.sixLegalItems2 || '');
+    variables['기재사항_3'] = String(subcontractChecklist.legalItem3 || subcontractChecklist.sixLegalItems3 || '');
+    variables['기재사항_4'] = String(subcontractChecklist.legalItem4 || subcontractChecklist.sixLegalItems4 || '');
+    variables['기재사항_5'] = String(subcontractChecklist.legalItem5 || subcontractChecklist.sixLegalItems5 || '');
+    variables['기재사항_6'] = String(subcontractChecklist.legalItem6 || subcontractChecklist.sixLegalItems6 || '');
+    variables['부당대금_결정'] = String(subcontractChecklist.unfairPriceDecision || subcontractChecklist.unfairSubcontractPrice || '');
+    variables['점검결과'] = String(subcontractChecklist.inspectionResult || subcontractChecklist.overallResult || '');
+    variables['귀책부서'] = String(subcontractChecklist.responsibleDepartment || subcontractChecklist.overallDepartment || '');
+    variables['원인'] = String(subcontractChecklist.cause || subcontractChecklist.overallCause || '');
+    variables['대책'] = String(subcontractChecklist.countermeasure || subcontractChecklist.overallMeasure || '');
+  }
 
   return variables;
 }
diff --git a/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx b/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx
index d44f4290..db0901cb 100644
--- a/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx
+++ b/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx
@@ -1,1183 +1,1200 @@
-'use client'
-
-import React, { useState, useEffect } from 'react'
-import { useSession } from 'next-auth/react'
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
-import { Button } from '@/components/ui/button'
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
-import { Badge } from '@/components/ui/badge'
-import { Checkbox } from '@/components/ui/checkbox'
-import { Label } from '@/components/ui/label'
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
-import { Input } from '@/components/ui/input'
-import { toast } from 'sonner'
-import { 
-  FileText, 
-  Upload, 
-  Eye, 
-  Send, 
-  CheckCircle, 
-  Download,
-  AlertCircle
-} from 'lucide-react'
-import { ContractDocuments } from './general-contract-documents'
-import { getActiveContractTemplates } from '@/lib/bidding/service'
-import { type BasicContractTemplate } from '@/db/schema'
-import { 
-  getBasicInfo,
-  getContractItems,
-  getSubcontractChecklist,
-  uploadContractApprovalFile,
-  sendContractApprovalRequest,
-  getContractById,
-  getContractTemplateByContractType,
-  getStorageInfo
-} from '../service'
-import { mapContractDataToTemplateVariables } from '../utils'
-import { ApprovalPreviewDialog } from '@/lib/approval/client'
-import { requestContractApprovalWithApproval } from '../approval-actions'
-import { mapContractToApprovalTemplateVariables } from '../approval-template-variables'
-
-interface ContractApprovalRequestDialogProps {
-  contract: Record<string, unknown>
-  open: boolean
-  onOpenChange: (open: boolean) => void
-}
-
-interface ContractSummary {
-  basicInfo: Record<string, unknown>
-  items: Record<string, unknown>[]
-  subcontractChecklist: Record<string, unknown> | null
-  storageInfo?: Record<string, unknown>[]
-  pdfPath?: string
-  basicContractPdfs?: Array<{ key: string; buffer: number[]; fileName: string }>
-}
-
-export function ContractApprovalRequestDialog({ 
-  contract, 
-  open, 
-  onOpenChange 
-}: ContractApprovalRequestDialogProps) {
-  const { data: session } = useSession()
-  const [currentStep, setCurrentStep] = useState(1)
-  const [contractSummary, setContractSummary] = useState<ContractSummary | null>(null)
-  const [uploadedFile, setUploadedFile] = useState<File | null>(null)
-  const [generatedPdfUrl, setGeneratedPdfUrl] = useState<string | null>(null)
-  const [generatedPdfBuffer, setGeneratedPdfBuffer] = useState<Uint8Array | null>(null)
-  const [isLoading, setIsLoading] = useState(false)
-  const [pdfViewerInstance, setPdfViewerInstance] = useState<any>(null)
-  const [isPdfPreviewVisible, setIsPdfPreviewVisible] = useState(false)
-
-  // 기본계약 관련 상태
-  const [selectedBasicContracts, setSelectedBasicContracts] = useState<Array<{
-    type: string;
-    templateName: string;
-    checked: boolean;
-  }>>([])
-  const [isLoadingBasicContracts, setIsLoadingBasicContracts] = useState(false)
-
-  // 결재 관련 상태
-  const [approvalDialogOpen, setApprovalDialogOpen] = useState(false)
-  const [approvalVariables, setApprovalVariables] = useState<Record<string, string>>({})
-  const [savedPdfPath, setSavedPdfPath] = useState<string | null>(null)
-  const [savedBasicContractPdfs, setSavedBasicContractPdfs] = useState<Array<{ key: string; buffer: number[]; fileName: string }>>([])
-
-  const contractId = contract.id as number
-  const userId = session?.user?.id || ''
-
-
-  // 기본계약 생성 함수 (최종 전송 시점에 호출)
-  const generateBasicContractPdf = async (
-    vendorId: number,
-    contractType: string,
-    templateName: string
-  ): Promise<{ buffer: number[], fileName: string }> => {
-    try {
-      // 1. 템플릿 데이터 준비 (서버 액션 호출)
-      const prepareResponse = await fetch("/api/contracts/prepare-template", {
-        method: "POST",
-        headers: { "Content-Type": "application/json" },
-        body: JSON.stringify({
-          templateName,
-          vendorId,
-        }),
-      });
-
-      if (!prepareResponse.ok) {
-        const errorText = await prepareResponse.text();
-        throw new Error(`템플릿 준비 실패 (${prepareResponse.status}): ${errorText}`);
-      }
-
-      const { template, templateData } = await prepareResponse.json();
-
-      // 2. 템플릿 파일 다운로드
-      const templateResponse = await fetch("/api/contracts/get-template", {
-        method: "POST",
-        headers: { "Content-Type": "application/json" },
-        body: JSON.stringify({ templatePath: template.filePath }),
-      });
-
-      const templateBlob = await templateResponse.blob();
-      const templateFile = new window.File([templateBlob], "template.docx", {
-        type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
-      });
-
-      // 3. PDFTron WebViewer로 PDF 변환
-      const { default: WebViewer } = await import("@pdftron/webviewer");
-
-      const tempDiv = document.createElement('div');
-      tempDiv.style.display = 'none';
-      document.body.appendChild(tempDiv);
-
-      try {
-        const instance = await WebViewer(
-          {
-            path: "/pdftronWeb",
-            licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
-            fullAPI: true,
-            enableOfficeEditing: true,
-          },
-          tempDiv
-        );
-
-        const { Core } = instance;
-        const { createDocument } = Core;
-
-        const templateDoc = await createDocument(templateFile, {
-          filename: templateFile.name,
-          extension: 'docx',
-        });
-
-        // 변수 치환 적용
-        await templateDoc.applyTemplateValues(templateData);
-        await new Promise(resolve => setTimeout(resolve, 3000));
-
-        const fileData = await templateDoc.getFileData();
-        const pdfBuffer = await (Core as any).officeToPDFBuffer(fileData, { extension: 'docx' });
-
-        const fileName = `${contractType}_${contractSummary?.basicInfo?.vendorCode || vendorId}_${Date.now()}.pdf`;
-
-        instance.UI.dispose();
-        return {
-          buffer: Array.from(pdfBuffer),
-          fileName
-        };
-
-      } finally {
-        if (tempDiv.parentNode) {
-          document.body.removeChild(tempDiv);
-        }
-      }
-
-    } catch (error) {
-      console.error(`기본계약 PDF 생성 실패 (${contractType}):`, error);
-      throw error;
-    }
-  };
-
-  // 기본계약 생성 및 선택 초기화
-  const initializeBasicContracts = React.useCallback(async () => {
-    if (!contractSummary?.basicInfo) return;
-
-    setIsLoadingBasicContracts(true);
-    try {
-      // 기본적으로 사용할 수 있는 계약서 타입들
-      const availableContracts: Array<{
-        type: string;
-        templateName: string;
-        checked: boolean;
-      }> = [
-        { type: "NDA", templateName: "비밀", checked: false },
-        { type: "General_GTC", templateName: "General GTC", checked: false },
-        { type: "기술자료", templateName: "기술", checked: false }
-      ];
-
-      // 프로젝트 코드가 있으면 Project GTC도 추가
-      if (contractSummary.basicInfo.projectCode) {
-        availableContracts.push({
-          type: "Project_GTC",
-          templateName: contractSummary.basicInfo.projectCode as string,
-          checked: false
-        });
-      }
-
-      setSelectedBasicContracts(availableContracts);
-    } catch (error) {
-      console.error('기본계약 초기화 실패:', error);
-      toast.error('기본계약 초기화에 실패했습니다.');
-    } finally {
-      setIsLoadingBasicContracts(false);
-    }
-  }, [contractSummary]);
-
-  // 기본계약 선택 토글
-  const toggleBasicContract = (type: string) => {
-    setSelectedBasicContracts(prev =>
-      prev.map(contract =>
-        contract.type === type
-          ? { ...contract, checked: !contract.checked }
-          : contract
-      )
-    );
-  };
-
-
-  // 1단계: 계약 현황 수집
-  const collectContractSummary = React.useCallback(async () => {
-    setIsLoading(true)
-    try {
-      // 각 컴포넌트에서 활성화된 데이터만 수집
-      const summary: ContractSummary = {
-        basicInfo: {},
-        items: [],
-        subcontractChecklist: null
-      }
-
-      // Basic Info 확인 (항상 활성화)
-      try {
-        const basicInfoData = await getBasicInfo(contractId)
-        if (basicInfoData && basicInfoData.success) {
-          summary.basicInfo = basicInfoData.data || {}
-        }
-        // externalYardEntry 정보도 추가로 가져오기
-        const contractData = await getContractById(contractId)
-        if (contractData) {
-          summary.basicInfo = {
-            ...summary.basicInfo,
-            externalYardEntry: contractData.externalYardEntry || 'N'
-          }
-        }
-      } catch {
-        console.log('Basic Info 데이터 없음')
-      }
-
-      // 품목 정보 확인
-      try {
-        const itemsData = await getContractItems(contractId)
-        if (itemsData && itemsData.length > 0) {
-          summary.items = itemsData
-        }
-      } catch {
-        console.log('품목 정보 데이터 없음')
-      }
-
-      try {
-        // Subcontract Checklist 확인
-        const subcontractData = await getSubcontractChecklist(contractId)
-        if (subcontractData && subcontractData.success && subcontractData.enabled) {
-          summary.subcontractChecklist = subcontractData.data
-        }
-      } catch {
-        console.log('Subcontract Checklist 데이터 없음')
-      }
-
-      // 임치(물품보관) 계약 정보 확인 (SG)
-      try {
-        if (summary.basicInfo?.contractType === 'SG') {
-          const storageData = await getStorageInfo(contractId)
-          if (storageData && storageData.length > 0) {
-            summary.storageInfo = storageData
-          }
-        }
-      } catch {
-        console.log('임치계약 정보 없음')
-      }
-      
-      console.log('contractSummary 구조:', summary)
-      console.log('basicInfo 내용:', summary.basicInfo)
-      setContractSummary(summary)
-    } catch (error) {
-      console.error('Error collecting contract summary:', error)
-      toast.error('계약 정보를 수집하는 중 오류가 발생했습니다.')
-    } finally {
-      setIsLoading(false)
-    }
-  }, [contractId])
-
-  // 3단계: PDF 생성 및 미리보기 (PDFTron 사용) - 템플릿 자동 로드
-  const generatePdf = async () => {
-    if (!contractSummary) {
-      toast.error('계약 정보가 필요합니다.')
-      return
-    }
-
-    setIsLoading(true)
-    try {
-      // 1. 계약 유형에 맞는 템플릿 조회
-      const contractType = contractSummary.basicInfo.contractType as string
-      const templateResult = await getContractTemplateByContractType(contractType)
-
-      if (!templateResult.success || !templateResult.template) {
-        throw new Error(templateResult.error || '템플릿을 찾을 수 없습니다.')
-      }
-
-      const template = templateResult.template
-
-      // 2. 템플릿 파일 다운로드
-      const templateResponse = await fetch("/api/contracts/get-template", {
-        method: "POST",
-        headers: { "Content-Type": "application/json" },
-        body: JSON.stringify({ templatePath: template.filePath }),
-      })
-
-      if (!templateResponse.ok) {
-        throw new Error("템플릿 파일을 다운로드할 수 없습니다.")
-      }
-
-      const templateBlob = await templateResponse.blob()
-      const templateFile = new File([templateBlob], template.fileName || "template.docx", {
-        type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
-      })
-
-      // 3. PDFTron을 사용해서 변수 치환 및 PDF 변환
-      // @ts-ignore
-      const WebViewer = await import("@pdftron/webviewer").then(({ default: WebViewer }) => WebViewer)
-      
-      // 임시 WebViewer 인스턴스 생성 (DOM에 추가하지 않음)
-      const tempDiv = document.createElement('div')
-      tempDiv.style.display = 'none'
-      document.body.appendChild(tempDiv)
-
-      const instance = await WebViewer(
-        {
-          path: "/pdftronWeb",
-          licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
-          fullAPI: true,
-        },
-        tempDiv
-      )
-
-      try {
-        const { Core } = instance
-        const { createDocument } = Core
-
-        // 템플릿 문서 생성 및 변수 치환
-        const templateDoc = await createDocument(templateFile, {
-          filename: templateFile.name,
-          extension: 'docx',
-        })
-
-        // 템플릿 변수 매핑
-        const mappedTemplateData = mapContractDataToTemplateVariables(contractSummary)
-
-        console.log("🔄 변수 치환 시작:", mappedTemplateData)
-        await templateDoc.applyTemplateValues(mappedTemplateData as any)
-        console.log("✅ 변수 치환 완료")
-
-        // PDF 변환
-        const fileData = await templateDoc.getFileData()
-        const pdfBuffer = await (Core as any).officeToPDFBuffer(fileData, { extension: 'docx' })
-
-        console.log(`✅ PDF 변환 완료: ${templateFile.name}`, `크기: ${pdfBuffer.byteLength} bytes`)
-
-        // PDF 버퍼를 Blob URL로 변환하여 미리보기
-        const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' })
-        const pdfUrl = URL.createObjectURL(pdfBlob)
-        setGeneratedPdfUrl(pdfUrl)
-        
-        // PDF 버퍼를 상태에 저장 (최종 전송 시 사용)
-        setGeneratedPdfBuffer(new Uint8Array(pdfBuffer))
-        
-        toast.success('PDF가 생성되었습니다.')
-
-      } finally {
-        // 임시 WebViewer 정리
-        instance.UI.dispose()
-        document.body.removeChild(tempDiv)
-      }
-
-    } catch (error: any) {
-      console.error('❌ PDF 생성 실패:', error)
-      const errorMessage = error instanceof Error ? error.message : (error?.message || '알 수 없는 오류')
-      toast.error(`PDF 생성 중 오류가 발생했습니다: ${errorMessage}`)
-    } finally {
-      setIsLoading(false)
-    }
-  }
-
-  // PDF 미리보기 기능
-  const openPdfPreview = async () => {
-    if (!generatedPdfBuffer) {
-      toast.error('생성된 PDF가 없습니다.')
-      return
-    }
-
-    setIsLoading(true)
-    try {
-      // @ts-ignore
-      const WebViewer = await import("@pdftron/webviewer").then(({ default: WebViewer }) => WebViewer)
-      
-      // 기존 인스턴스가 있다면 정리
-      if (pdfViewerInstance) {
-        console.log("🔄 기존 WebViewer 인스턴스 정리")
-        try {
-          pdfViewerInstance.UI.dispose()
-        } catch (error) {
-          console.warn('기존 WebViewer 정리 중 오류:', error)
-        }
-        setPdfViewerInstance(null)
-      }
-
-      // 미리보기용 컨테이너 확인
-      let previewDiv = document.getElementById('pdf-preview-container')
-      if (!previewDiv) {
-        console.log("🔄 컨테이너 생성")
-        previewDiv = document.createElement('div')
-        previewDiv.id = 'pdf-preview-container'
-        previewDiv.className = 'w-full h-full'
-        previewDiv.style.width = '100%'
-        previewDiv.style.height = '100%'
-        
-        // 실제 컨테이너에 추가
-        const actualContainer = document.querySelector('[data-pdf-container]')
-        if (actualContainer) {
-          actualContainer.appendChild(previewDiv)
-        }
-      }
-
-      console.log("🔄 WebViewer 인스턴스 생성 시작")
-
-      // WebViewer 인스턴스 생성 (문서 없이)
-      const instance = await Promise.race([
-        WebViewer(
-          {
-            path: "/pdftronWeb",
-            licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
-            fullAPI: true,
-          },
-          previewDiv
-        ),
-        new Promise((_, reject) => 
-          setTimeout(() => reject(new Error('WebViewer 초기화 타임아웃')), 30000)
-        )
-      ])
-
-      console.log("🔄 WebViewer 인스턴스 생성 완료")
-      setPdfViewerInstance(instance)
-
-      // PDF 버퍼를 Blob으로 변환
-      const pdfBlob = new Blob([generatedPdfBuffer as any], { type: 'application/pdf' })
-      const pdfUrl = URL.createObjectURL(pdfBlob)
-      console.log("🔄 PDF Blob URL 생성:", pdfUrl)
-
-      // 문서 로드
-      console.log("🔄 문서 로드 시작")
-      const { documentViewer } = (instance as any).Core
-      
-      // 문서 로드 이벤트 대기
-      await new Promise((resolve, reject) => {
-        const timeout = setTimeout(() => {
-          reject(new Error('문서 로드 타임아웃'))
-        }, 20000)
-
-        const onDocumentLoaded = () => {
-          clearTimeout(timeout)
-          documentViewer.removeEventListener('documentLoaded', onDocumentLoaded)
-          documentViewer.removeEventListener('documentError', onDocumentError)
-          console.log("🔄 문서 로드 완료")
-          resolve(true)
-        }
-
-        const onDocumentError = (error: any) => {
-          clearTimeout(timeout)
-          documentViewer.removeEventListener('documentLoaded', onDocumentLoaded)
-          documentViewer.removeEventListener('documentError', onDocumentError)
-          console.error('문서 로드 오류:', error)
-          reject(error)
-        }
-
-        documentViewer.addEventListener('documentLoaded', onDocumentLoaded)
-        documentViewer.addEventListener('documentError', onDocumentError)
-        
-        // 문서 로드 시작
-        documentViewer.loadDocument(pdfUrl)
-      })
-
-      setIsPdfPreviewVisible(true)
-      toast.success('PDF 미리보기가 준비되었습니다.')
-
-    } catch (error) {
-      console.error('PDF 미리보기 실패:', error)
-      toast.error(`PDF 미리보기 중 오류가 발생했습니다: ${error.message}`)
-    } finally {
-      setIsLoading(false)
-    }
-  }
-
-  // PDF 다운로드 기능
-  const downloadPdf = () => {
-    if (!generatedPdfBuffer) {
-      toast.error('다운로드할 PDF가 없습니다.')
-      return
-    }
-
-    const pdfBlob = new Blob([generatedPdfBuffer as any], { type: 'application/pdf' })
-    const pdfUrl = URL.createObjectURL(pdfBlob)
-    
-    const link = document.createElement('a')
-    link.href = pdfUrl
-    link.download = `contract_${contractId}_${Date.now()}.pdf`
-    document.body.appendChild(link)
-    link.click()
-    document.body.removeChild(link)
-    
-    URL.revokeObjectURL(pdfUrl)
-    toast.success('PDF가 다운로드되었습니다.')
-  }
-
-  // PDF 미리보기 닫기
-  const closePdfPreview = () => {
-    console.log("🔄 PDF 미리보기 닫기 시작")
-    if (pdfViewerInstance) {
-      try {
-        console.log("🔄 WebViewer 인스턴스 정리")
-        pdfViewerInstance.UI.dispose()
-      } catch (error) {
-        console.warn('WebViewer 정리 중 오류:', error)
-      }
-      setPdfViewerInstance(null)
-    }
-    
-    // 컨테이너 정리
-    const previewDiv = document.getElementById('pdf-preview-container')
-    if (previewDiv) {
-      try {
-        previewDiv.innerHTML = ''
-      } catch (error) {
-        console.warn('컨테이너 정리 중 오류:', error)
-      }
-    }
-    
-    setIsPdfPreviewVisible(false)
-    console.log("🔄 PDF 미리보기 닫기 완료")
-  }
-
-  // PDF를 서버에 저장하는 함수 (API route 사용)
-  const savePdfToServer = async (pdfBuffer: Uint8Array, fileName: string): Promise<string | null> => {
-    try {
-      // PDF 버퍼를 Blob으로 변환
-      const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' });
-      
-      // FormData 생성
-      const formData = new FormData();
-      formData.append('file', pdfBlob, fileName);
-      formData.append('contractId', String(contractId));
-
-      // API route로 업로드
-      const response = await fetch('/api/general-contracts/upload-pdf', {
-        method: 'POST',
-        body: formData,
-      });
-
-      if (!response.ok) {
-        const errorData = await response.json();
-        throw new Error(errorData.error || 'PDF 파일 저장에 실패했습니다.');
-      }
-
-      const result = await response.json();
-      
-      if (!result.success) {
-        throw new Error(result.error || 'PDF 파일 저장에 실패했습니다.');
-      }
-
-      return result.filePath;
-    } catch (error) {
-      console.error('PDF 저장 실패:', error);
-      return null;
-    }
-  };
-
-  // 최종 전송 - 결재 프로세스 시작
-  const handleFinalSubmit = async () => {
-    if (!generatedPdfUrl || !contractSummary || !generatedPdfBuffer) {
-      toast.error('생성된 PDF가 필요합니다.')
-      return
-    }
-
-    if (!userId) {
-      toast.error('로그인이 필요합니다.')
-      return
-    }
-
-    setIsLoading(true)
-    try {
-      // 기본계약서 생성 (최종 전송 시점에)
-      let generatedBasicContractPdfs: Array<{ key: string; buffer: number[]; fileName: string }> = [];
-
-      const contractsToGenerate = selectedBasicContracts.filter(c => c.checked);
-      if (contractsToGenerate.length > 0) {
-        // vendorId 조회
-        let vendorId: number | undefined;
-        try {
-          const basicInfoData = await getBasicInfo(contractId);
-          if (basicInfoData && basicInfoData.success && basicInfoData.data) {
-            vendorId = basicInfoData.data.vendorId;
-          }
-        } catch (error) {
-          console.error('vendorId 조회 실패:', error);
-        }
-
-        if (vendorId) {
-          toast.info('기본계약서를 생성하는 중입니다...');
-
-          for (const contract of contractsToGenerate) {
-            try {
-              const pdf = await generateBasicContractPdf(vendorId, contract.type, contract.templateName);
-              generatedBasicContractPdfs.push({
-                key: `${vendorId}_${contract.type}_${contract.templateName}`,
-                ...pdf
-              });
-            } catch (error) {
-              console.error(`${contract.type} 계약서 생성 실패:`, error);
-              // 개별 실패는 전체를 중단하지 않음
-            }
-          }
-
-          if (generatedBasicContractPdfs.length > 0) {
-            toast.success(`${generatedBasicContractPdfs.length}개의 기본계약서가 생성되었습니다.`);
-          }
-        }
-      }
-
-      // PDF를 서버에 저장
-      toast.info('PDF를 서버에 저장하는 중입니다...');
-      const pdfPath = await savePdfToServer(
-        generatedPdfBuffer,
-        `contract_${contractId}_${Date.now()}.pdf`
-      );
-
-      if (!pdfPath) {
-        toast.error('PDF 저장에 실패했습니다.');
-        return;
-      }
-
-      setSavedPdfPath(pdfPath);
-      setSavedBasicContractPdfs(generatedBasicContractPdfs);
-
-      // 결재 템플릿 변수 매핑
-      const approvalVars = await mapContractToApprovalTemplateVariables(contractSummary);
-      setApprovalVariables(approvalVars);
-
-      // 계약승인요청 dialog close
-      onOpenChange(false);
-
-      // 결재 템플릿 dialog open
-      setApprovalDialogOpen(true);
-    } catch (error: any) {
-      console.error('Error preparing approval:', error);
-      toast.error('결재 준비 중 오류가 발생했습니다.')
-    } finally {
-      setIsLoading(false)
-    }
-  }
-
-  // 결재 등록 처리
-  const handleApprovalSubmit = async (data: {
-    approvers: string[];
-    title: string;
-    attachments?: File[];
-  }) => {
-    if (!contractSummary || !savedPdfPath) {
-      toast.error('계약 정보가 필요합니다.')
-      return
-    }
-
-    setIsLoading(true)
-    try {
-      const result = await requestContractApprovalWithApproval({
-        contractId,
-        contractSummary: {
-          ...contractSummary,
-          // PDF 경로를 contractSummary에 추가
-          pdfPath: savedPdfPath || undefined,
-          basicContractPdfs: savedBasicContractPdfs.length > 0 ? savedBasicContractPdfs : undefined,
-        } as ContractSummary,
-        currentUser: {
-          id: Number(userId),
-          epId: session?.user?.epId || null,
-          email: session?.user?.email || undefined,
-        },
-        approvers: data.approvers,
-        title: data.title,
-      });
-
-      if (result.status === 'pending_approval') {
-        toast.success('결재가 등록되었습니다.')
-        setApprovalDialogOpen(false);
-      } else {
-        toast.error('결재 등록에 실패했습니다.')
-      }
-    } catch (error: any) {
-      console.error('Error submitting approval:', error);
-      toast.error(`결재 등록 중 오류가 발생했습니다: ${error.message || '알 수 없는 오류'}`);
-    } finally {
-      setIsLoading(false)
-    }
-  }
-
-  // 다이얼로그가 열릴 때 1단계 데이터 수집
-  useEffect(() => {
-    if (open && currentStep === 1) {
-      collectContractSummary()
-    }
-  }, [open, currentStep, collectContractSummary])
-
-  // 계약 요약이 준비되면 기본계약 초기화
-  useEffect(() => {
-    if (contractSummary && currentStep === 2) {
-      const loadBasicContracts = async () => {
-        await initializeBasicContracts()
-      }
-      loadBasicContracts()
-    }
-  }, [contractSummary, currentStep, initializeBasicContracts])
-
-  // 다이얼로그가 닫힐 때 PDF 뷰어 정리
-  useEffect(() => {
-    if (!open) {
-      closePdfPreview()
-    }
-  }, [open])
-
-
-  return (
-    <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
-        <DialogHeader>
-          <DialogTitle className="flex items-center gap-2">
-            <FileText className="h-5 w-5" />
-            계약승인요청
-          </DialogTitle>
-        </DialogHeader>
-
-        <Tabs value={currentStep.toString()} className="w-full">
-          <TabsList className="grid w-full grid-cols-3">
-            <TabsTrigger value="1" disabled={currentStep < 1}>
-              1. 계약 현황 정리
-            </TabsTrigger>
-            <TabsTrigger value="2" disabled={currentStep < 2}>
-              2. 기본계약 체크
-            </TabsTrigger>
-            <TabsTrigger value="3" disabled={currentStep < 3}>
-              3. PDF 미리보기
-            </TabsTrigger>
-          </TabsList>
-
-          {/* 1단계: 계약 현황 정리 */}
-          <TabsContent value="1" className="space-y-4">
-            <Card>
-              <CardHeader>
-                <CardTitle className="flex items-center gap-2">
-                  <CheckCircle className="h-5 w-5 text-green-600" />
-                  작성된 계약 현황
-                </CardTitle>
-              </CardHeader>
-              <CardContent className="space-y-4">
-                {isLoading ? (
-                  <div className="text-center py-4">
-                    <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
-                    <p className="mt-2 text-sm text-muted-foreground">계약 정보를 수집하는 중...</p>
-                  </div>
-                ) : (
-                  <div className="space-y-4">
-                    {/* 기본 정보 (필수) */}
-                    <div className="border rounded-lg p-4">
-                      <div className="flex items-center gap-2 mb-3">
-                        <CheckCircle className="h-4 w-4 text-green-600" />
-                        <Label className="font-medium">기본 정보</Label>
-                        <Badge variant="secondary">필수</Badge>
-                      </div>
-                      <div className="grid grid-cols-2 gap-4 text-sm">
-                        <div>
-                          <span className="font-medium">계약번호:</span> {String(contractSummary?.basicInfo?.contractNumber || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">계약명:</span> {String(contractSummary?.basicInfo?.contractName || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">벤더:</span> {String(contractSummary?.basicInfo?.vendorName || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">프로젝트:</span> {String(contractSummary?.basicInfo?.projectName || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">계약유형:</span> {String(contractSummary?.basicInfo?.contractType || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">계약상태:</span> {String(contractSummary?.basicInfo?.contractStatus || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">계약금액:</span> {String(contractSummary?.basicInfo?.contractAmount || '')} {String(contractSummary?.basicInfo?.currency || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">계약기간:</span> {String(contractSummary?.basicInfo?.startDate || '')} ~ {String(contractSummary?.basicInfo?.endDate || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">사양서 유형:</span> {String(contractSummary?.basicInfo?.specificationType || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">단가 유형:</span> {String(contractSummary?.basicInfo?.unitPriceType || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">연결 PO번호:</span> {String(contractSummary?.basicInfo?.linkedPoNumber || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">연결 입찰번호:</span> {String(contractSummary?.basicInfo?.linkedBidNumber || '')}
-                        </div>
-                      </div>
-                    </div>
-
-                    {/* 지급/인도 조건 */}
-                    <div className="border rounded-lg p-4">
-                      <div className="flex items-center gap-2 mb-3">
-                        <CheckCircle className="h-4 w-4 text-green-600" />
-                        <Label className="font-medium">지급/인도 조건</Label>
-                        <Badge variant="secondary">필수</Badge>
-                      </div>
-                      <div className="grid grid-cols-2 gap-4 text-sm">
-                        <div>
-                          <span className="font-medium">지급조건:</span> {String(contractSummary?.basicInfo?.paymentTerm || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">세금 유형:</span> {String(contractSummary?.basicInfo?.taxType || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">인도조건:</span> {String(contractSummary?.basicInfo?.deliveryTerm || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">인도유형:</span> {String(contractSummary?.basicInfo?.deliveryType || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">선적지:</span> {String(contractSummary?.basicInfo?.shippingLocation || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">하역지:</span> {String(contractSummary?.basicInfo?.dischargeLocation || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">계약납기:</span> {String(contractSummary?.basicInfo?.contractDeliveryDate || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">위약금:</span> {contractSummary?.basicInfo?.liquidatedDamages ? '적용' : '미적용'}
-                        </div>
-                      </div>
-                    </div>
-
-                    {/* 추가 조건 */}
-                    <div className="border rounded-lg p-4">
-                      <div className="flex items-center gap-2 mb-3">
-                        <CheckCircle className="h-4 w-4 text-green-600" />
-                        <Label className="font-medium">추가 조건</Label>
-                        <Badge variant="secondary">필수</Badge>
-                      </div>
-                      <div className="grid grid-cols-2 gap-4 text-sm">
-                        <div>
-                          <span className="font-medium">연동제 정보:</span> {String(contractSummary?.basicInfo?.interlockingSystem || '')}
-                        </div>
-                        <div>
-                          <span className="font-medium">계약성립조건:</span> 
-                          {contractSummary?.basicInfo?.contractEstablishmentConditions && 
-                            Object.entries(contractSummary.basicInfo.contractEstablishmentConditions as Record<string, unknown>)
-                              .filter(([, value]) => value === true)
-                              .map(([key]) => key)
-                              .join(', ') || '없음'}
-                        </div>
-                        <div>
-                          <span className="font-medium">계약해지조건:</span>
-                          {contractSummary?.basicInfo?.contractTerminationConditions && 
-                            Object.entries(contractSummary.basicInfo.contractTerminationConditions as Record<string, unknown>)
-                              .filter(([, value]) => value === true)
-                              .map(([key]) => key)
-                              .join(', ') || '없음'}
-                        </div>
-                      </div>
-                    </div>
-
-                    {/* 품목 정보 */}
-                    <div className="border rounded-lg p-4">
-                      <div className="flex items-center gap-2 mb-3">
-                        <Checkbox 
-                          id="items-enabled" 
-                          checked={contractSummary?.items && contractSummary.items.length > 0}
-                          disabled
-                        />
-                        <Label htmlFor="items-enabled" className="font-medium">품목 정보</Label>
-                        <Badge variant="outline">선택</Badge>
-                      </div>
-                      {contractSummary?.items && contractSummary.items.length > 0 ? (
-                        <div className="space-y-2">
-                          <p className="text-sm text-muted-foreground">
-                            총 {contractSummary.items.length}개 품목이 입력되어 있습니다.
-                          </p>
-                          <div className="max-h-32 overflow-y-auto">
-                            {contractSummary.items.slice(0, 3).map((item: Record<string, unknown>, index: number) => (
-                              <div key={index} className="text-xs bg-gray-50 p-2 rounded">
-                                <div className="font-medium">{String(item.itemInfo || item.description || `품목 ${index + 1}`)}</div>
-                                <div className="text-muted-foreground">
-                                  수량: {String(item.quantity || 0)} | 단가: {String(item.contractUnitPrice || item.unitPrice || 0)}
-                                </div>
-                              </div>
-                            ))}
-                            {contractSummary.items.length > 3 && (
-                              <div className="text-xs text-muted-foreground text-center">
-                                ... 외 {contractSummary.items.length - 3}개 품목
-                              </div>
-                            )}
-                          </div>
-                        </div>
-                      ) : (
-                        <p className="text-sm text-muted-foreground">
-                          품목 정보가 입력되지 않았습니다.
-                        </p>
-                      )}
-                    </div>
-
-                    {/* 하도급 체크리스트 */}
-                    <div className="border rounded-lg p-4">
-                      <div className="flex items-center gap-2 mb-3">
-                        <Checkbox 
-                          id="subcontract-enabled" 
-                          checked={!!contractSummary?.subcontractChecklist}
-                          disabled
-                        />
-                        <Label htmlFor="subcontract-enabled" className="font-medium">
-                          하도급 체크리스트
-                        </Label>
-                        <Badge variant="outline">선택</Badge>
-                      </div>
-                      <p className="text-sm text-muted-foreground">
-                        {contractSummary?.subcontractChecklist 
-                          ? '정보가 입력되어 있습니다.' 
-                          : '정보가 입력되지 않았습니다.'}
-                      </p>
-                    </div>
-                  </div>
-                )}
-              </CardContent>
-            </Card>
-
-            <div className="flex justify-end">
-              <Button 
-                onClick={() => setCurrentStep(2)}
-                disabled={isLoading}
-              >
-                다음 단계
-              </Button>
-            </div>
-          </TabsContent>
-
-          {/* 2단계: 기본계약 체크 */}
-          <TabsContent value="2" className="space-y-4">
-            <Card>
-              <CardHeader>
-                <CardTitle className="flex items-center gap-2">
-                  <FileText className="h-5 w-5 text-blue-600" />
-                  기본계약서 선택
-                </CardTitle>
-                <p className="text-sm text-muted-foreground">
-                  벤더에게 발송할 기본계약서를 선택해주세요. (템플릿이 있는 계약서만 선택 가능합니다.)
-                </p>
-              </CardHeader>
-              <CardContent className="space-y-4">
-                {isLoadingBasicContracts ? (
-                  <div className="text-center py-8">
-                    <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
-                    <p className="mt-2 text-sm text-muted-foreground">기본계약 템플릿을 불러오는 중...</p>
-                  </div>
-                ) : (
-                  <div className="space-y-4">
-                    {selectedBasicContracts.length > 0 ? (
-                      <div className="space-y-3">
-                        <div className="flex items-center justify-between">
-                          <h4 className="font-medium">필요한 기본계약서</h4>
-                          <Badge variant="outline">
-                            {selectedBasicContracts.filter(c => c.checked).length}개 선택됨
-                          </Badge>
-                        </div>
-
-                        <div className="grid gap-3">
-                          {selectedBasicContracts.map((contract) => (
-                            <div
-                              key={contract.type}
-                              className="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50"
-                            >
-                              <div className="flex items-center gap-3">
-                                <Checkbox
-                                  id={`contract-${contract.type}`}
-                                  checked={contract.checked}
-                                  onCheckedChange={() => toggleBasicContract(contract.type)}
-                                />
-                                <div>
-                                  <Label
-                                    htmlFor={`contract-${contract.type}`}
-                                    className="font-medium cursor-pointer"
-                                  >
-                                    {contract.type}
-                                  </Label>
-                                  <p className="text-sm text-muted-foreground">
-                                    템플릿: {contract.templateName}
-                                  </p>
-                                </div>
-                              </div>
-                              <Badge
-                                variant="secondary"
-                                className="text-xs"
-                              >
-                                {contract.checked ? "선택됨" : "미선택"}
-                              </Badge>
-                            </div>
-                          ))}
-                        </div>
-
-                      </div>
-                    ) : (
-                      <div className="text-center py-8 text-muted-foreground">
-                        <FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
-                        <p>기본계약서 목록을 불러올 수 없습니다.</p>
-                        <p className="text-sm">잠시 후 다시 시도해주세요.</p>
-                      </div>
-                    )}
-
-                  </div>
-                )}
-              </CardContent>
-            </Card>
-
-            <div className="flex justify-between">
-              <Button variant="outline" onClick={() => setCurrentStep(1)}>
-                이전 단계
-              </Button>
-              <Button
-                onClick={() => setCurrentStep(3)}
-                disabled={isLoadingBasicContracts}
-              >
-                다음 단계
-              </Button>
-            </div>
-          </TabsContent>
-
-          {/* 3단계: PDF 미리보기 */}
-          <TabsContent value="3" className="space-y-4">
-            <Card>
-              <CardHeader>
-                <CardTitle className="flex items-center gap-2">
-                  <Eye className="h-5 w-5 text-purple-600" />
-                  PDF 미리보기
-                </CardTitle>
-              </CardHeader>
-              <CardContent className="space-y-4">
-                {!generatedPdfUrl ? (
-                  <div className="text-center py-8">
-                    <Button onClick={generatePdf} disabled={isLoading}>
-                      {isLoading ? 'PDF 생성 중...' : 'PDF 생성하기'}
-                    </Button>
-                  </div>
-                ) : (
-                  <div className="space-y-4">
-                    <div className="border rounded-lg p-4 bg-green-50">
-                      <div className="flex items-center gap-2">
-                        <CheckCircle className="h-4 w-4 text-green-600" />
-                        <span className="font-medium text-green-900">PDF 생성 완료</span>
-                      </div>
-                    </div>
-
-                    <div className="border rounded-lg p-4">
-                      <div className="flex items-center justify-between mb-4">
-                        <h4 className="font-medium">생성된 PDF</h4>
-                        <div className="flex gap-2">
-                          <Button 
-                            variant="outline" 
-                            size="sm"
-                            onClick={downloadPdf}
-                            disabled={isLoading}
-                          >
-                            <Download className="h-4 w-4 mr-2" />
-                            다운로드
-                          </Button>
-                          <Button 
-                            variant="outline" 
-                            size="sm"
-                            onClick={openPdfPreview}
-                            disabled={isLoading}
-                          >
-                            <Eye className="h-4 w-4 mr-2" />
-                            미리보기
-                          </Button>
-                        </div>
-                      </div>
-                      
-                      {/* PDF 미리보기 영역 */}
-                      <div className="border rounded-lg h-96 bg-gray-50 relative" data-pdf-container>
-                        {isPdfPreviewVisible ? (
-                          <>
-                            <div className="absolute top-2 right-2 z-10">
-                              <Button 
-                                variant="outline" 
-                                size="sm"
-                                onClick={closePdfPreview}
-                                className="bg-white/90 hover:bg-white"
-                              >
-                                ✕ 닫기
-                              </Button>
-                            </div>
-                            <div id="pdf-preview-container" className="w-full h-full" />
-                          </>
-                        ) : (
-                          <div className="flex items-center justify-center h-full">
-                            <div className="text-center text-muted-foreground">
-                              <FileText className="h-12 w-12 mx-auto mb-2" />
-                              <p>미리보기 버튼을 클릭하여 PDF를 확인하세요</p>
-                            </div>
-                          </div>
-                        )}
-                      </div>
-                    </div>
-                  </div>
-                )}
-              </CardContent>
-            </Card>
-
-            <div className="flex justify-between">
-              <Button variant="outline" onClick={() => setCurrentStep(2)}>
-                이전 단계
-              </Button>
-              <Button
-                onClick={handleFinalSubmit}
-                disabled={!generatedPdfUrl || isLoading}
-                className="bg-green-600 hover:bg-green-700"
-              >
-                <Send className="h-4 w-4 mr-2" />
-                {isLoading ? '전송 중...' : '최종 전송'}
-              </Button>
-            </div>
-          </TabsContent>
-        </Tabs>
-      </DialogContent>
-
-      {/* 결재 미리보기 Dialog */}
-      {session?.user && session.user.epId && contractSummary && (
-        <ApprovalPreviewDialog
-          open={approvalDialogOpen}
-          onOpenChange={(open) => {
-            setApprovalDialogOpen(open);
-            if (!open) {
-              setApprovalVariables({});
-              setSavedPdfPath(null);
-              setSavedBasicContractPdfs([]);
-            }
-          }}
-          templateName="일반계약 결재"
-          variables={approvalVariables}
-          title={`계약 체결 진행 품의 요청서 - ${contractSummary.basicInfo?.contractNumber || contractId}`}
-          currentUser={{
-            id: Number(session.user.id),
-            epId: session.user.epId,
-            name: session.user.name || undefined,
-            email: session.user.email || undefined,
-          }}
-          onConfirm={handleApprovalSubmit}
-          enableAttachments={false}
-        />
-      )}
-    </Dialog>
+'use client'
+
+import React, { useState, useEffect } from 'react'
+import { useSession } from 'next-auth/react'
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import { Button } from '@/components/ui/button'
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Badge } from '@/components/ui/badge'
+import { Checkbox } from '@/components/ui/checkbox'
+import { Label } from '@/components/ui/label'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { Input } from '@/components/ui/input'
+import { toast } from 'sonner'
+import { 
+  FileText, 
+  Upload, 
+  Eye, 
+  Send, 
+  CheckCircle, 
+  Download,
+  AlertCircle
+} from 'lucide-react'
+import { ContractDocuments } from './general-contract-documents'
+import { getActiveContractTemplates } from '@/lib/bidding/service'
+import { type BasicContractTemplate } from '@/db/schema'
+import { 
+  getBasicInfo,
+  getContractItems,
+  getSubcontractChecklist,
+  uploadContractApprovalFile,
+  sendContractApprovalRequest,
+  getContractById,
+  getContractTemplateByContractType,
+  getStorageInfo
+} from '../service'
+import { mapContractDataToTemplateVariables } from '../utils'
+import { ApprovalPreviewDialog } from '@/lib/approval/client'
+import { requestContractApprovalWithApproval } from '../approval-actions'
+import { mapContractToApprovalTemplateVariables } from '../approval-template-variables'
+
+interface ContractApprovalRequestDialogProps {
+  contract: Record<string, unknown>
+  open: boolean
+  onOpenChange: (open: boolean) => void
+}
+
+interface ContractSummary {
+  basicInfo: Record<string, unknown>
+  items: Record<string, unknown>[]
+  subcontractChecklist: Record<string, unknown> | null
+  storageInfo?: Record<string, unknown>[]
+  pdfPath?: string
+  basicContractPdfs?: Array<{ key: string; buffer: number[]; fileName: string }>
+}
+
+export function ContractApprovalRequestDialog({ 
+  contract, 
+  open, 
+  onOpenChange 
+}: ContractApprovalRequestDialogProps) {
+  const { data: session } = useSession()
+  const [currentStep, setCurrentStep] = useState(1)
+  const [contractSummary, setContractSummary] = useState<ContractSummary | null>(null)
+  const [uploadedFile, setUploadedFile] = useState<File | null>(null)
+  const [generatedPdfUrl, setGeneratedPdfUrl] = useState<string | null>(null)
+  const [generatedPdfBuffer, setGeneratedPdfBuffer] = useState<Uint8Array | null>(null)
+  const [isLoading, setIsLoading] = useState(false)
+  const [pdfViewerInstance, setPdfViewerInstance] = useState<any>(null)
+  const [isPdfPreviewVisible, setIsPdfPreviewVisible] = useState(false)
+
+  // 기본계약 관련 상태
+  const [selectedBasicContracts, setSelectedBasicContracts] = useState<Array<{
+    type: string;
+    templateName: string;
+    checked: boolean;
+  }>>([])
+  const [isLoadingBasicContracts, setIsLoadingBasicContracts] = useState(false)
+
+  // 결재 관련 상태
+  const [approvalDialogOpen, setApprovalDialogOpen] = useState(false)
+  const [approvalVariables, setApprovalVariables] = useState<Record<string, string>>({})
+  const [savedPdfPath, setSavedPdfPath] = useState<string | null>(null)
+  const [savedBasicContractPdfs, setSavedBasicContractPdfs] = useState<Array<{ key: string; buffer: number[]; fileName: string }>>([])
+
+  const contractId = contract.id as number
+  const userId = session?.user?.id || ''
+
+
+  // 기본계약 생성 함수 (최종 전송 시점에 호출)
+  const generateBasicContractPdf = async (
+    vendorId: number,
+    contractType: string,
+    templateName: string
+  ): Promise<{ buffer: number[], fileName: string }> => {
+    try {
+      // 1. 템플릿 데이터 준비 (서버 액션 호출)
+      const prepareResponse = await fetch("/api/contracts/prepare-template", {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({
+          templateName,
+          vendorId,
+        }),
+      });
+
+      if (!prepareResponse.ok) {
+        const errorText = await prepareResponse.text();
+        throw new Error(`템플릿 준비 실패 (${prepareResponse.status}): ${errorText}`);
+      }
+
+      const { template, templateData } = await prepareResponse.json();
+
+      // 2. 템플릿 파일 다운로드
+      const templateResponse = await fetch("/api/contracts/get-template", {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({ templatePath: template.filePath }),
+      });
+
+      const templateBlob = await templateResponse.blob();
+      const templateFile = new window.File([templateBlob], "template.docx", {
+        type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+      });
+
+      // 3. PDFTron WebViewer로 PDF 변환
+      const { default: WebViewer } = await import("@pdftron/webviewer");
+
+      const tempDiv = document.createElement('div');
+      tempDiv.style.display = 'none';
+      document.body.appendChild(tempDiv);
+
+      try {
+        const instance = await WebViewer(
+          {
+            path: "/pdftronWeb",
+            licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
+            fullAPI: true,
+            enableOfficeEditing: true,
+          },
+          tempDiv
+        );
+
+        const { Core } = instance;
+        const { createDocument } = Core;
+
+        const templateDoc = await createDocument(templateFile, {
+          filename: templateFile.name,
+          extension: 'docx',
+        });
+
+        // 변수 치환 적용
+        await templateDoc.applyTemplateValues(templateData);
+        await new Promise(resolve => setTimeout(resolve, 3000));
+
+        const fileData = await templateDoc.getFileData();
+        const pdfBuffer = await (Core as any).officeToPDFBuffer(fileData, { extension: 'docx' });
+
+        const fileName = `${contractType}_${contractSummary?.basicInfo?.vendorCode || vendorId}_${Date.now()}.pdf`;
+
+        instance.UI.dispose();
+        return {
+          buffer: Array.from(pdfBuffer),
+          fileName
+        };
+
+      } finally {
+        if (tempDiv.parentNode) {
+          document.body.removeChild(tempDiv);
+        }
+      }
+
+    } catch (error) {
+      console.error(`기본계약 PDF 생성 실패 (${contractType}):`, error);
+      throw error;
+    }
+  };
+
+  // 기본계약 생성 및 선택 초기화
+  const initializeBasicContracts = React.useCallback(async () => {
+    if (!contractSummary?.basicInfo) return;
+
+    setIsLoadingBasicContracts(true);
+    try {
+      // 기본적으로 사용할 수 있는 계약서 타입들
+      const availableContracts: Array<{
+        type: string;
+        templateName: string;
+        checked: boolean;
+      }> = [
+        { type: "NDA", templateName: "비밀", checked: false },
+        { type: "General_GTC", templateName: "General GTC", checked: false },
+        { type: "기술자료", templateName: "기술", checked: false }
+      ];
+
+      // 프로젝트 코드가 있으면 Project GTC도 추가
+      if (contractSummary.basicInfo.projectCode) {
+        availableContracts.push({
+          type: "Project_GTC",
+          templateName: contractSummary.basicInfo.projectCode as string,
+          checked: false
+        });
+      }
+
+      setSelectedBasicContracts(availableContracts);
+    } catch (error) {
+      console.error('기본계약 초기화 실패:', error);
+      toast.error('기본계약 초기화에 실패했습니다.');
+    } finally {
+      setIsLoadingBasicContracts(false);
+    }
+  }, [contractSummary]);
+
+  // 기본계약 선택 토글
+  const toggleBasicContract = (type: string) => {
+    setSelectedBasicContracts(prev =>
+      prev.map(contract =>
+        contract.type === type
+          ? { ...contract, checked: !contract.checked }
+          : contract
+      )
+    );
+  };
+
+
+  // 1단계: 계약 현황 수집
+  const collectContractSummary = React.useCallback(async () => {
+    setIsLoading(true)
+    try {
+      // 각 컴포넌트에서 활성화된 데이터만 수집
+      const summary: ContractSummary = {
+        basicInfo: {},
+        items: [],
+        subcontractChecklist: null
+      }
+
+      // Basic Info 확인 (항상 활성화)
+      try {
+        const basicInfoData = await getBasicInfo(contractId)
+        if (basicInfoData && basicInfoData.success) {
+          summary.basicInfo = basicInfoData.data || {}
+        }
+        // externalYardEntry 정보도 추가로 가져오기
+        const contractData = await getContractById(contractId)
+        if (contractData) {
+          summary.basicInfo = {
+            ...summary.basicInfo,
+            externalYardEntry: contractData.externalYardEntry || 'N'
+          }
+        }
+      } catch {
+        console.log('Basic Info 데이터 없음')
+      }
+
+      // 품목 정보 확인
+      try {
+        const itemsData = await getContractItems(contractId)
+        if (itemsData && itemsData.length > 0) {
+          summary.items = itemsData
+        }
+      } catch {
+        console.log('품목 정보 데이터 없음')
+      }
+
+      try {
+        // Subcontract Checklist 확인
+        const subcontractData = await getSubcontractChecklist(contractId)
+        if (subcontractData && subcontractData.success && subcontractData.enabled) {
+          summary.subcontractChecklist = subcontractData.data
+        }
+      } catch {
+        console.log('Subcontract Checklist 데이터 없음')
+      }
+
+      // 임치(물품보관) 계약 정보 확인 (SG)
+      try {
+        if (summary.basicInfo?.contractType === 'SG') {
+          const storageData = await getStorageInfo(contractId)
+          if (storageData && storageData.length > 0) {
+            summary.storageInfo = storageData
+          }
+        }
+      } catch {
+        console.log('임치계약 정보 없음')
+      }
+      
+      console.log('contractSummary 구조:', summary)
+      console.log('basicInfo 내용:', summary.basicInfo)
+      setContractSummary(summary)
+    } catch (error) {
+      console.error('Error collecting contract summary:', error)
+      toast.error('계약 정보를 수집하는 중 오류가 발생했습니다.')
+    } finally {
+      setIsLoading(false)
+    }
+  }, [contractId])
+
+  // 3단계: PDF 생성 및 미리보기 (PDFTron 사용) - 템플릿 자동 로드
+  const generatePdf = async () => {
+    if (!contractSummary) {
+      toast.error('계약 정보가 필요합니다.')
+      return
+    }
+
+    setIsLoading(true)
+    try {
+      // 1. 계약 유형에 맞는 템플릿 조회
+      const contractType = contractSummary.basicInfo.contractType as string
+      const templateResult = await getContractTemplateByContractType(contractType)
+
+      if (!templateResult.success || !templateResult.template) {
+        throw new Error(templateResult.error || '템플릿을 찾을 수 없습니다.')
+      }
+
+      const template = templateResult.template
+
+      // 2. 템플릿 파일 다운로드
+      const templateResponse = await fetch("/api/contracts/get-template", {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({ templatePath: template.filePath }),
+      })
+
+      if (!templateResponse.ok) {
+        throw new Error("템플릿 파일을 다운로드할 수 없습니다.")
+      }
+
+      const templateBlob = await templateResponse.blob()
+      const templateFile = new File([templateBlob], template.fileName || "template.docx", {
+        type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+      })
+
+      // 3. PDFTron을 사용해서 변수 치환 및 PDF 변환
+      // @ts-ignore
+      const WebViewer = await import("@pdftron/webviewer").then(({ default: WebViewer }) => WebViewer)
+      
+      // 임시 WebViewer 인스턴스 생성 (DOM에 추가하지 않음)
+      const tempDiv = document.createElement('div')
+      tempDiv.style.display = 'none'
+      document.body.appendChild(tempDiv)
+
+      const instance = await WebViewer(
+        {
+          path: "/pdftronWeb",
+          licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
+          fullAPI: true,
+        },
+        tempDiv
+      )
+
+      try {
+        const { Core } = instance
+        const { createDocument } = Core
+
+        // 템플릿 문서 생성 및 변수 치환
+        const templateDoc = await createDocument(templateFile, {
+          filename: templateFile.name,
+          extension: 'docx',
+        })
+
+        // 템플릿 변수 매핑
+        const mappedTemplateData = mapContractDataToTemplateVariables(contractSummary)
+
+        console.log("🔄 변수 치환 시작:", mappedTemplateData)
+        await templateDoc.applyTemplateValues(mappedTemplateData as any)
+        console.log("✅ 변수 치환 완료")
+
+        // PDF 변환
+        const fileData = await templateDoc.getFileData()
+        const pdfBuffer = await (Core as any).officeToPDFBuffer(fileData, { extension: 'docx' })
+
+        console.log(`✅ PDF 변환 완료: ${templateFile.name}`, `크기: ${pdfBuffer.byteLength} bytes`)
+
+        // PDF 버퍼를 Blob URL로 변환하여 미리보기
+        const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' })
+        const pdfUrl = URL.createObjectURL(pdfBlob)
+        setGeneratedPdfUrl(pdfUrl)
+        
+        // PDF 버퍼를 상태에 저장 (최종 전송 시 사용)
+        setGeneratedPdfBuffer(new Uint8Array(pdfBuffer))
+        
+        toast.success('PDF가 생성되었습니다.')
+
+      } finally {
+        // 임시 WebViewer 정리
+        instance.UI.dispose()
+        document.body.removeChild(tempDiv)
+      }
+
+    } catch (error: any) {
+      console.error('❌ PDF 생성 실패:', error)
+      const errorMessage = error instanceof Error ? error.message : (error?.message || '알 수 없는 오류')
+      toast.error(`PDF 생성 중 오류가 발생했습니다: ${errorMessage}`)
+    } finally {
+      setIsLoading(false)
+    }
+  }
+
+  // PDF 미리보기 기능
+  const openPdfPreview = async () => {
+    if (!generatedPdfBuffer) {
+      toast.error('생성된 PDF가 없습니다.')
+      return
+    }
+
+    setIsLoading(true)
+    try {
+      // @ts-ignore
+      const WebViewer = await import("@pdftron/webviewer").then(({ default: WebViewer }) => WebViewer)
+      
+      // 기존 인스턴스가 있다면 정리
+      if (pdfViewerInstance) {
+        console.log("🔄 기존 WebViewer 인스턴스 정리")
+        try {
+          pdfViewerInstance.UI.dispose()
+        } catch (error) {
+          console.warn('기존 WebViewer 정리 중 오류:', error)
+        }
+        setPdfViewerInstance(null)
+      }
+
+      // 미리보기용 컨테이너 확인
+      let previewDiv = document.getElementById('pdf-preview-container')
+      if (!previewDiv) {
+        console.log("🔄 컨테이너 생성")
+        previewDiv = document.createElement('div')
+        previewDiv.id = 'pdf-preview-container'
+        previewDiv.className = 'w-full h-full'
+        previewDiv.style.width = '100%'
+        previewDiv.style.height = '100%'
+        
+        // 실제 컨테이너에 추가
+        const actualContainer = document.querySelector('[data-pdf-container]')
+        if (actualContainer) {
+          actualContainer.appendChild(previewDiv)
+        }
+      }
+
+      console.log("🔄 WebViewer 인스턴스 생성 시작")
+
+      // WebViewer 인스턴스 생성 (문서 없이)
+      const instance = await Promise.race([
+        WebViewer(
+          {
+            path: "/pdftronWeb",
+            licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
+            fullAPI: true,
+          },
+          previewDiv
+        ),
+        new Promise((_, reject) => 
+          setTimeout(() => reject(new Error('WebViewer 초기화 타임아웃')), 30000)
+        )
+      ])
+
+      console.log("🔄 WebViewer 인스턴스 생성 완료")
+      setPdfViewerInstance(instance)
+
+      // PDF 버퍼를 Blob으로 변환
+      const pdfBlob = new Blob([generatedPdfBuffer as any], { type: 'application/pdf' })
+      const pdfUrl = URL.createObjectURL(pdfBlob)
+      console.log("🔄 PDF Blob URL 생성:", pdfUrl)
+
+      // 문서 로드
+      console.log("🔄 문서 로드 시작")
+      const { documentViewer } = (instance as any).Core
+      
+      // 문서 로드 이벤트 대기
+      await new Promise((resolve, reject) => {
+        const timeout = setTimeout(() => {
+          reject(new Error('문서 로드 타임아웃'))
+        }, 20000)
+
+        const onDocumentLoaded = () => {
+          clearTimeout(timeout)
+          documentViewer.removeEventListener('documentLoaded', onDocumentLoaded)
+          documentViewer.removeEventListener('documentError', onDocumentError)
+          console.log("🔄 문서 로드 완료")
+          resolve(true)
+        }
+
+        const onDocumentError = (error: any) => {
+          clearTimeout(timeout)
+          documentViewer.removeEventListener('documentLoaded', onDocumentLoaded)
+          documentViewer.removeEventListener('documentError', onDocumentError)
+          console.error('문서 로드 오류:', error)
+          reject(error)
+        }
+
+        documentViewer.addEventListener('documentLoaded', onDocumentLoaded)
+        documentViewer.addEventListener('documentError', onDocumentError)
+        
+        // 문서 로드 시작
+        documentViewer.loadDocument(pdfUrl)
+      })
+
+      setIsPdfPreviewVisible(true)
+      toast.success('PDF 미리보기가 준비되었습니다.')
+
+    } catch (error) {
+      console.error('PDF 미리보기 실패:', error)
+      toast.error(`PDF 미리보기 중 오류가 발생했습니다: ${error.message}`)
+    } finally {
+      setIsLoading(false)
+    }
+  }
+
+  // PDF 다운로드 기능
+  const downloadPdf = () => {
+    if (!generatedPdfBuffer) {
+      toast.error('다운로드할 PDF가 없습니다.')
+      return
+    }
+
+    const pdfBlob = new Blob([generatedPdfBuffer as any], { type: 'application/pdf' })
+    const pdfUrl = URL.createObjectURL(pdfBlob)
+    
+    const link = document.createElement('a')
+    link.href = pdfUrl
+    link.download = `contract_${contractId}_${Date.now()}.pdf`
+    document.body.appendChild(link)
+    link.click()
+    document.body.removeChild(link)
+    
+    URL.revokeObjectURL(pdfUrl)
+    toast.success('PDF가 다운로드되었습니다.')
+  }
+
+  // PDF 미리보기 닫기
+  const closePdfPreview = () => {
+    console.log("🔄 PDF 미리보기 닫기 시작")
+    if (pdfViewerInstance) {
+      try {
+        console.log("🔄 WebViewer 인스턴스 정리")
+        pdfViewerInstance.UI.dispose()
+      } catch (error) {
+        console.warn('WebViewer 정리 중 오류:', error)
+      }
+      setPdfViewerInstance(null)
+    }
+    
+    // 컨테이너 정리
+    const previewDiv = document.getElementById('pdf-preview-container')
+    if (previewDiv) {
+      try {
+        previewDiv.innerHTML = ''
+      } catch (error) {
+        console.warn('컨테이너 정리 중 오류:', error)
+      }
+    }
+    
+    setIsPdfPreviewVisible(false)
+    console.log("🔄 PDF 미리보기 닫기 완료")
+  }
+
+  // PDF를 서버에 저장하는 함수 (API route 사용)
+  const savePdfToServer = async (pdfBuffer: Uint8Array, fileName: string): Promise<string | null> => {
+    try {
+      // PDF 버퍼를 Blob으로 변환
+      const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' });
+      
+      // FormData 생성
+      const formData = new FormData();
+      formData.append('file', pdfBlob, fileName);
+      formData.append('contractId', String(contractId));
+
+      // API route로 업로드
+      const response = await fetch('/api/general-contracts/upload-pdf', {
+        method: 'POST',
+        body: formData,
+      });
+
+      if (!response.ok) {
+        const errorData = await response.json();
+        throw new Error(errorData.error || 'PDF 파일 저장에 실패했습니다.');
+      }
+
+      const result = await response.json();
+      
+      if (!result.success) {
+        throw new Error(result.error || 'PDF 파일 저장에 실패했습니다.');
+      }
+
+      return result.filePath;
+    } catch (error) {
+      console.error('PDF 저장 실패:', error);
+      return null;
+    }
+  };
+
+  // 최종 전송 - 결재 프로세스 시작
+  const handleFinalSubmit = async () => {
+    if (!generatedPdfUrl || !contractSummary || !generatedPdfBuffer) {
+      toast.error('생성된 PDF가 필요합니다.')
+      return
+    }
+
+    if (!userId) {
+      toast.error('로그인이 필요합니다.')
+      return
+    }
+
+    setIsLoading(true)
+    try {
+      // 기본계약서 생성 (최종 전송 시점에)
+      let generatedBasicContractPdfs: Array<{ key: string; buffer: number[]; fileName: string }> = [];
+
+      const contractsToGenerate = selectedBasicContracts.filter(c => c.checked);
+      if (contractsToGenerate.length > 0) {
+        // vendorId 조회
+        let vendorId: number | undefined;
+        try {
+          const basicInfoData = await getBasicInfo(contractId);
+          if (basicInfoData && basicInfoData.success && basicInfoData.data) {
+            vendorId = basicInfoData.data.vendorId;
+          }
+        } catch (error) {
+          console.error('vendorId 조회 실패:', error);
+        }
+
+        if (vendorId) {
+          toast.info('기본계약서를 생성하는 중입니다...');
+
+          for (const contract of contractsToGenerate) {
+            try {
+              const pdf = await generateBasicContractPdf(vendorId, contract.type, contract.templateName);
+              generatedBasicContractPdfs.push({
+                key: `${vendorId}_${contract.type}_${contract.templateName}`,
+                ...pdf
+              });
+            } catch (error) {
+              console.error(`${contract.type} 계약서 생성 실패:`, error);
+              // 개별 실패는 전체를 중단하지 않음
+            }
+          }
+
+          if (generatedBasicContractPdfs.length > 0) {
+            toast.success(`${generatedBasicContractPdfs.length}개의 기본계약서가 생성되었습니다.`);
+          }
+        }
+      }
+
+      // PDF를 서버에 저장
+      toast.info('PDF를 서버에 저장하는 중입니다...');
+      const pdfPath = await savePdfToServer(
+        generatedPdfBuffer,
+        `contract_${contractId}_${Date.now()}.pdf`
+      );
+
+      if (!pdfPath) {
+        toast.error('PDF 저장에 실패했습니다.');
+        return;
+      }
+
+      setSavedPdfPath(pdfPath);
+      setSavedBasicContractPdfs(generatedBasicContractPdfs);
+
+      // 결재 템플릿 변수 매핑
+      const approvalVars = await mapContractToApprovalTemplateVariables(contractSummary);
+      setApprovalVariables(approvalVars);
+
+      // 계약승인요청 dialog close
+      onOpenChange(false);
+
+      // 결재 템플릿 dialog open
+      setApprovalDialogOpen(true);
+    } catch (error: any) {
+      console.error('Error preparing approval:', error);
+      toast.error('결재 준비 중 오류가 발생했습니다.')
+    } finally {
+      setIsLoading(false)
+    }
+  }
+
+  // 결재 등록 처리
+  const handleApprovalSubmit = async (data: {
+    approvers: string[];
+    title: string;
+    attachments?: File[];
+  }) => {
+    if (!contractSummary || !savedPdfPath) {
+      toast.error('계약 정보가 필요합니다.')
+      return
+    }
+
+    setIsLoading(true)
+    try {
+      const result = await requestContractApprovalWithApproval({
+        contractId,
+        contractSummary: {
+          ...contractSummary,
+          // PDF 경로를 contractSummary에 추가
+          pdfPath: savedPdfPath || undefined,
+          basicContractPdfs: savedBasicContractPdfs.length > 0 ? savedBasicContractPdfs : undefined,
+        } as ContractSummary,
+        currentUser: {
+          id: Number(userId),
+          epId: session?.user?.epId || null,
+          email: session?.user?.email || undefined,
+        },
+        approvers: data.approvers,
+        title: data.title,
+      });
+
+      if (result.status === 'pending_approval') {
+        toast.success('결재가 등록되었습니다.')
+        setApprovalDialogOpen(false);
+      } else {
+        toast.error('결재 등록에 실패했습니다.')
+      }
+    } catch (error: any) {
+      console.error('Error submitting approval:', error);
+      toast.error(`결재 등록 중 오류가 발생했습니다: ${error.message || '알 수 없는 오류'}`);
+    } finally {
+      setIsLoading(false)
+    }
+  }
+
+  // 다이얼로그가 열릴 때 1단계 데이터 수집
+  useEffect(() => {
+    if (open && currentStep === 1) {
+      collectContractSummary()
+    }
+  }, [open, currentStep, collectContractSummary])
+
+  // 계약 요약이 준비되면 기본계약 초기화
+  useEffect(() => {
+    if (contractSummary && currentStep === 2) {
+      const loadBasicContracts = async () => {
+        await initializeBasicContracts()
+      }
+      loadBasicContracts()
+    }
+  }, [contractSummary, currentStep, initializeBasicContracts])
+
+  // 다이얼로그가 닫힐 때 PDF 뷰어 정리
+  useEffect(() => {
+    if (!open) {
+      closePdfPreview()
+    }
+  }, [open])
+
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
+        <DialogHeader>
+          <DialogTitle className="flex items-center gap-2">
+            <FileText className="h-5 w-5" />
+            계약승인요청
+          </DialogTitle>
+        </DialogHeader>
+
+        <Tabs value={currentStep.toString()} className="w-full">
+          <TabsList className="grid w-full grid-cols-3">
+            <TabsTrigger value="1" disabled={currentStep < 1}>
+              1. 계약 현황 정리
+            </TabsTrigger>
+            <TabsTrigger value="2" disabled={currentStep < 2}>
+              2. 기본계약 체크
+            </TabsTrigger>
+            <TabsTrigger value="3" disabled={currentStep < 3}>
+              3. PDF 미리보기
+            </TabsTrigger>
+          </TabsList>
+
+          {/* 1단계: 계약 현황 정리 */}
+          <TabsContent value="1" className="space-y-4">
+            <Card>
+              <CardHeader>
+                <CardTitle className="flex items-center gap-2">
+                  <CheckCircle className="h-5 w-5 text-green-600" />
+                  작성된 계약 현황
+                </CardTitle>
+              </CardHeader>
+              <CardContent className="space-y-4">
+                {isLoading ? (
+                  <div className="text-center py-4">
+                    <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
+                    <p className="mt-2 text-sm text-muted-foreground">계약 정보를 수집하는 중...</p>
+                  </div>
+                ) : (
+                  <div className="space-y-4">
+                    {/* 기본 정보 (필수) */}
+                    <div className="border rounded-lg p-4">
+                      <div className="flex items-center gap-2 mb-3">
+                        <CheckCircle className="h-4 w-4 text-green-600" />
+                        <Label className="font-medium">기본 정보</Label>
+                        <Badge variant="secondary">필수</Badge>
+                      </div>
+                      <div className="grid grid-cols-2 gap-4 text-sm">
+                        <div>
+                          <span className="font-medium">계약번호:</span> {String(contractSummary?.basicInfo?.contractNumber || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">계약명:</span> {String(contractSummary?.basicInfo?.contractName || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">벤더:</span> {String(contractSummary?.basicInfo?.vendorName || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">프로젝트:</span> {String(contractSummary?.basicInfo?.projectName || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">계약유형:</span> {String(contractSummary?.basicInfo?.contractType || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">계약상태:</span> {String(contractSummary?.basicInfo?.contractStatus || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">계약금액:</span> {String(contractSummary?.basicInfo?.contractAmount || '')} {String(contractSummary?.basicInfo?.currency || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">계약기간:</span> {String(contractSummary?.basicInfo?.startDate || '')} ~ {String(contractSummary?.basicInfo?.endDate || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">사양서 유형:</span> {String(contractSummary?.basicInfo?.specificationType || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">단가 유형:</span> {String(contractSummary?.basicInfo?.unitPriceType || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">연결 PO번호:</span> {String(contractSummary?.basicInfo?.linkedPoNumber || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">연결 입찰번호:</span> {String(contractSummary?.basicInfo?.linkedBidNumber || '')}
+                        </div>
+                      </div>
+                    </div>
+
+                    {/* 지급/인도 조건 */}
+                    <div className="border rounded-lg p-4">
+                      <div className="flex items-center gap-2 mb-3">
+                        <CheckCircle className="h-4 w-4 text-green-600" />
+                        <Label className="font-medium">지급/인도 조건</Label>
+                        <Badge variant="secondary">필수</Badge>
+                      </div>
+                      <div className="grid grid-cols-2 gap-4 text-sm">
+                        <div>
+                          <span className="font-medium">지급조건:</span> {String(contractSummary?.basicInfo?.paymentTerm || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">세금 유형:</span> {String(contractSummary?.basicInfo?.taxType || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">인도조건:</span> {String(contractSummary?.basicInfo?.deliveryTerm || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">인도유형:</span> {String(contractSummary?.basicInfo?.deliveryType || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">선적지:</span> {String(contractSummary?.basicInfo?.shippingLocation || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">하역지:</span> {String(contractSummary?.basicInfo?.dischargeLocation || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">계약납기:</span> {String(contractSummary?.basicInfo?.contractDeliveryDate || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">위약금:</span> {contractSummary?.basicInfo?.liquidatedDamages ? '적용' : '미적용'}
+                        </div>
+                      </div>
+                    </div>
+
+                    {/* 추가 조건 */}
+                    <div className="border rounded-lg p-4">
+                      <div className="flex items-center gap-2 mb-3">
+                        <CheckCircle className="h-4 w-4 text-green-600" />
+                        <Label className="font-medium">추가 조건</Label>
+                        <Badge variant="secondary">필수</Badge>
+                      </div>
+                      <div className="grid grid-cols-2 gap-4 text-sm">
+                        <div>
+                          <span className="font-medium">연동제 정보:</span> {String(contractSummary?.basicInfo?.interlockingSystem || '')}
+                        </div>
+                        <div>
+                          <span className="font-medium">계약성립조건:</span>
+                          {contractSummary?.basicInfo?.contractEstablishmentConditions ? (() => {
+                            const conditions = Object.entries(contractSummary.basicInfo.contractEstablishmentConditions as Record<string, boolean>)
+                              .filter(([, value]) => value === true)
+                              .map(([key]) => {
+                                const conditionMap: Record<string, string> = {
+                                  'ownerApproval': '정규업체 등록(실사 포함) 시',
+                                  'regularVendorRegistration': '프로젝트 수주 시',
+                                  'shipOwnerApproval': '선주 승인 시',
+                                  'other': '기타'
+                                };
+                                return conditionMap[key] || key;
+                              });
+                            return conditions.length > 0 ? conditions.join(', ') : '없음';
+                          })() : '없음'}
+                        </div>
+                        <div>
+                          <span className="font-medium">계약해지조건:</span>
+                          {contractSummary?.basicInfo?.contractTerminationConditions ? (() => {
+                            const conditions = Object.entries(contractSummary.basicInfo.contractTerminationConditions as Record<string, boolean>)
+                              .filter(([, value]) => value === true)
+                              .map(([key]) => {
+                                const conditionMap: Record<string, string> = {
+                                  'standardTermination': '표준 계약해지조건',
+                                  'projectNotAwarded': '프로젝트 미수주 시',
+                                  'other': '기타'
+                                };
+                                return conditionMap[key] || key;
+                              });
+                            return conditions.length > 0 ? conditions.join(', ') : '없음';
+                          })() : '없음'}
+                        </div>
+                      </div>
+                    </div>
+
+                    {/* 품목 정보 */}
+                    <div className="border rounded-lg p-4">
+                      <div className="flex items-center gap-2 mb-3">
+                        <Checkbox 
+                          id="items-enabled" 
+                          checked={contractSummary?.items && contractSummary.items.length > 0}
+                          disabled
+                        />
+                        <Label htmlFor="items-enabled" className="font-medium">품목 정보</Label>
+                        <Badge variant="outline">선택</Badge>
+                      </div>
+                      {contractSummary?.items && contractSummary.items.length > 0 ? (
+                        <div className="space-y-2">
+                          <p className="text-sm text-muted-foreground">
+                            총 {contractSummary.items.length}개 품목이 입력되어 있습니다.
+                          </p>
+                          <div className="max-h-32 overflow-y-auto">
+                            {contractSummary.items.slice(0, 3).map((item: Record<string, unknown>, index: number) => (
+                              <div key={index} className="text-xs bg-gray-50 p-2 rounded">
+                                <div className="font-medium">{String(item.itemInfo || item.description || `품목 ${index + 1}`)}</div>
+                                <div className="text-muted-foreground">
+                                  수량: {String(item.quantity || 0)} | 단가: {String(item.contractUnitPrice || item.unitPrice || 0)}
+                                </div>
+                              </div>
+                            ))}
+                            {contractSummary.items.length > 3 && (
+                              <div className="text-xs text-muted-foreground text-center">
+                                ... 외 {contractSummary.items.length - 3}개 품목
+                              </div>
+                            )}
+                          </div>
+                        </div>
+                      ) : (
+                        <p className="text-sm text-muted-foreground">
+                          품목 정보가 입력되지 않았습니다.
+                        </p>
+                      )}
+                    </div>
+
+                    {/* 하도급 체크리스트 */}
+                    <div className="border rounded-lg p-4">
+                      <div className="flex items-center gap-2 mb-3">
+                        <Checkbox 
+                          id="subcontract-enabled" 
+                          checked={!!contractSummary?.subcontractChecklist}
+                          disabled
+                        />
+                        <Label htmlFor="subcontract-enabled" className="font-medium">
+                          하도급 체크리스트
+                        </Label>
+                        <Badge variant="outline">선택</Badge>
+                      </div>
+                      <p className="text-sm text-muted-foreground">
+                        {contractSummary?.subcontractChecklist 
+                          ? '정보가 입력되어 있습니다.' 
+                          : '정보가 입력되지 않았습니다.'}
+                      </p>
+                    </div>
+                  </div>
+                )}
+              </CardContent>
+            </Card>
+
+            <div className="flex justify-end">
+              <Button 
+                onClick={() => setCurrentStep(2)}
+                disabled={isLoading}
+              >
+                다음 단계
+              </Button>
+            </div>
+          </TabsContent>
+
+          {/* 2단계: 기본계약 체크 */}
+          <TabsContent value="2" className="space-y-4">
+            <Card>
+              <CardHeader>
+                <CardTitle className="flex items-center gap-2">
+                  <FileText className="h-5 w-5 text-blue-600" />
+                  기본계약서 선택
+                </CardTitle>
+                <p className="text-sm text-muted-foreground">
+                  벤더에게 발송할 기본계약서를 선택해주세요. (템플릿이 있는 계약서만 선택 가능합니다.)
+                </p>
+              </CardHeader>
+              <CardContent className="space-y-4">
+                {isLoadingBasicContracts ? (
+                  <div className="text-center py-8">
+                    <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
+                    <p className="mt-2 text-sm text-muted-foreground">기본계약 템플릿을 불러오는 중...</p>
+                  </div>
+                ) : (
+                  <div className="space-y-4">
+                    {selectedBasicContracts.length > 0 ? (
+                      <div className="space-y-3">
+                        <div className="flex items-center justify-between">
+                          <h4 className="font-medium">필요한 기본계약서</h4>
+                          <Badge variant="outline">
+                            {selectedBasicContracts.filter(c => c.checked).length}개 선택됨
+                          </Badge>
+                        </div>
+
+                        <div className="grid gap-3">
+                          {selectedBasicContracts.map((contract) => (
+                            <div
+                              key={contract.type}
+                              className="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50"
+                            >
+                              <div className="flex items-center gap-3">
+                                <Checkbox
+                                  id={`contract-${contract.type}`}
+                                  checked={contract.checked}
+                                  onCheckedChange={() => toggleBasicContract(contract.type)}
+                                />
+                                <div>
+                                  <Label
+                                    htmlFor={`contract-${contract.type}`}
+                                    className="font-medium cursor-pointer"
+                                  >
+                                    {contract.type}
+                                  </Label>
+                                  <p className="text-sm text-muted-foreground">
+                                    템플릿: {contract.templateName}
+                                  </p>
+                                </div>
+                              </div>
+                              <Badge
+                                variant="secondary"
+                                className="text-xs"
+                              >
+                                {contract.checked ? "선택됨" : "미선택"}
+                              </Badge>
+                            </div>
+                          ))}
+                        </div>
+
+                      </div>
+                    ) : (
+                      <div className="text-center py-8 text-muted-foreground">
+                        <FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
+                        <p>기본계약서 목록을 불러올 수 없습니다.</p>
+                        <p className="text-sm">잠시 후 다시 시도해주세요.</p>
+                      </div>
+                    )}
+
+                  </div>
+                )}
+              </CardContent>
+            </Card>
+
+            <div className="flex justify-between">
+              <Button variant="outline" onClick={() => setCurrentStep(1)}>
+                이전 단계
+              </Button>
+              <Button
+                onClick={() => setCurrentStep(3)}
+                disabled={isLoadingBasicContracts}
+              >
+                다음 단계
+              </Button>
+            </div>
+          </TabsContent>
+
+          {/* 3단계: PDF 미리보기 */}
+          <TabsContent value="3" className="space-y-4">
+            <Card>
+              <CardHeader>
+                <CardTitle className="flex items-center gap-2">
+                  <Eye className="h-5 w-5 text-purple-600" />
+                  PDF 미리보기
+                </CardTitle>
+              </CardHeader>
+              <CardContent className="space-y-4">
+                {!generatedPdfUrl ? (
+                  <div className="text-center py-8">
+                    <Button onClick={generatePdf} disabled={isLoading}>
+                      {isLoading ? 'PDF 생성 중...' : 'PDF 생성하기'}
+                    </Button>
+                  </div>
+                ) : (
+                  <div className="space-y-4">
+                    <div className="border rounded-lg p-4 bg-green-50">
+                      <div className="flex items-center gap-2">
+                        <CheckCircle className="h-4 w-4 text-green-600" />
+                        <span className="font-medium text-green-900">PDF 생성 완료</span>
+                      </div>
+                    </div>
+
+                    <div className="border rounded-lg p-4">
+                      <div className="flex items-center justify-between mb-4">
+                        <h4 className="font-medium">생성된 PDF</h4>
+                        <div className="flex gap-2">
+                          <Button 
+                            variant="outline" 
+                            size="sm"
+                            onClick={downloadPdf}
+                            disabled={isLoading}
+                          >
+                            <Download className="h-4 w-4 mr-2" />
+                            다운로드
+                          </Button>
+                          <Button 
+                            variant="outline" 
+                            size="sm"
+                            onClick={openPdfPreview}
+                            disabled={isLoading}
+                          >
+                            <Eye className="h-4 w-4 mr-2" />
+                            미리보기
+                          </Button>
+                        </div>
+                      </div>
+                      
+                      {/* PDF 미리보기 영역 */}
+                      <div className="border rounded-lg h-96 bg-gray-50 relative" data-pdf-container>
+                        {isPdfPreviewVisible ? (
+                          <>
+                            <div className="absolute top-2 right-2 z-10">
+                              <Button 
+                                variant="outline" 
+                                size="sm"
+                                onClick={closePdfPreview}
+                                className="bg-white/90 hover:bg-white"
+                              >
+                                ✕ 닫기
+                              </Button>
+                            </div>
+                            <div id="pdf-preview-container" className="w-full h-full" />
+                          </>
+                        ) : (
+                          <div className="flex items-center justify-center h-full">
+                            <div className="text-center text-muted-foreground">
+                              <FileText className="h-12 w-12 mx-auto mb-2" />
+                              <p>미리보기 버튼을 클릭하여 PDF를 확인하세요</p>
+                            </div>
+                          </div>
+                        )}
+                      </div>
+                    </div>
+                  </div>
+                )}
+              </CardContent>
+            </Card>
+
+            <div className="flex justify-between">
+              <Button variant="outline" onClick={() => setCurrentStep(2)}>
+                이전 단계
+              </Button>
+              <Button
+                onClick={handleFinalSubmit}
+                disabled={!generatedPdfUrl || isLoading}
+                className="bg-green-600 hover:bg-green-700"
+              >
+                <Send className="h-4 w-4 mr-2" />
+                {isLoading ? '전송 중...' : '최종 전송'}
+              </Button>
+            </div>
+          </TabsContent>
+        </Tabs>
+      </DialogContent>
+
+      {/* 결재 미리보기 Dialog */}
+      {session?.user && session.user.epId && contractSummary && (
+        <ApprovalPreviewDialog
+          open={approvalDialogOpen}
+          onOpenChange={(open) => {
+            setApprovalDialogOpen(open);
+            if (!open) {
+              setApprovalVariables({});
+              setSavedPdfPath(null);
+              setSavedBasicContractPdfs([]);
+            }
+          }}
+          templateName="일반계약 결재"
+          variables={approvalVariables}
+          title={`계약 체결 진행 품의 요청서 - ${contractSummary.basicInfo?.contractNumber || contractId}`}
+          currentUser={{
+            id: Number(session.user.id),
+            epId: session.user.epId,
+            name: session.user.name || undefined,
+            email: session.user.email || undefined,
+          }}
+          onConfirm={handleApprovalSubmit}
+          enableAttachments={false}
+        />
+      )}
+    </Dialog>
   )}
\ No newline at end of file
diff --git a/lib/general-contracts/detail/general-contract-items-table.tsx b/lib/general-contracts/detail/general-contract-items-table.tsx
index 4f74cfbb..be174417 100644
--- a/lib/general-contracts/detail/general-contract-items-table.tsx
+++ b/lib/general-contracts/detail/general-contract-items-table.tsx
@@ -218,7 +218,7 @@ export function ContractItemsTable({
         const item = itemsToSave[index]
         // if (!item.itemCode) errors.push(`${index + 1}번째 품목의 품목코드`)
         if (!item.itemInfo) errors.push(`${index + 1}번째 품목의 Item 정보`)
-        if (!item.quantity || item.quantity <= 0) errors.push(`${index + 1}번째 품목의 수량`)
+        // if (!item.quantity || item.quantity <= 0) errors.push(`${index + 1}번째 품목의 수량`)
         if (!item.contractUnitPrice || item.contractUnitPrice <= 0) errors.push(`${index + 1}번째 품목의 단가`)
         if (!item.contractDeliveryDate) errors.push(`${index + 1}번째 품목의 납기일`)
       }
-- 
cgit v1.2.3

