summaryrefslogtreecommitdiff
path: root/lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx')
-rw-r--r--lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx1312
1 files changed, 0 insertions, 1312 deletions
diff --git a/lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx b/lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx
deleted file mode 100644
index f05fe9ef..00000000
--- a/lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx
+++ /dev/null
@@ -1,1312 +0,0 @@
-'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,
- getCommunicationChannel,
- getLocation,
- getFieldServiceRate,
- getOffsetDetails,
- getSubcontractChecklist,
- uploadContractApprovalFile,
- sendContractApprovalRequest
-} from '../service'
-
-interface ContractApprovalRequestDialogProps {
- contract: Record<string, unknown>
- open: boolean
- onOpenChange: (open: boolean) => void
-}
-
-interface ContractSummary {
- basicInfo: Record<string, unknown>
- items: Record<string, unknown>[]
- communicationChannel: Record<string, unknown> | null
- location: Record<string, unknown> | null
- fieldServiceRate: Record<string, unknown> | null
- offsetDetails: Record<string, unknown> | null
- subcontractChecklist: Record<string, unknown> | null
-}
-
-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 contractId = contract.id as number
- const userId = session?.user?.id || ''
-
- // LOI 템플릿용 변수 매핑 함수
- const mapContractSummaryToLOITemplate = (contractSummary: ContractSummary) => {
- const { basicInfo, items } = contractSummary
- const firstItem = items && items.length > 0 ? items[0] : {}
-
- // 날짜 포맷팅 헬퍼 함수
- const formatDate = (date: any) => {
- if (!date) return ''
- try {
- const d = new Date(date)
- return d.toLocaleDateString('ko-KR')
- } catch {
- return ''
- }
- }
-
- return {
- // 날짜 관련 (템플릿에서 {{todayDate}} 형식으로 사용)
- todayDate: new Date().toLocaleDateString('ko-KR'),
-
- // 벤더 정보
- vendorName: basicInfo?.vendorName || '',
- representativeName: '', // 벤더 대표자 이름 - 현재 데이터에 없음, 향후 확장 가능
-
- // 계약 기본 정보
- contractNumber: basicInfo?.contractNumber || '',
-
- // 프로젝트 정보
- projectNumber: '', // 프로젝트 코드 - 현재 데이터에 없음, 향후 확장 가능
- projectName: basicInfo?.projectName || '',
- project: basicInfo?.projectName || '',
-
- // 아이템 정보
- item: firstItem?.itemInfo || '',
-
- // 무역 조건
- incoterms: basicInfo?.deliveryTerm || '', // Incoterms 대신 deliveryTerm 사용
- shippingLocation: basicInfo?.shippingLocation || '',
-
- // 금액 및 통화
- contractCurrency: basicInfo?.currency || '',
- contractAmount: basicInfo?.contractAmount || '',
- totalAmount: basicInfo?.contractAmount || '', // totalAmount가 없으면 contractAmount 사용
-
- // 수량
- quantity: firstItem?.quantity || '',
-
- // 납기일
- contractDeliveryDate: formatDate(basicInfo?.contractDeliveryDate),
-
- // 지급 조건
- paymentTerm: basicInfo?.paymentTerm || '',
-
- // 유효기간
- validityEndDate: formatDate(basicInfo?.endDate), // validityEndDate 대신 endDate 사용
- }
- }
-
- // 기본계약 생성 함수 (최종 전송 시점에 호출)
- 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.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: [],
- communicationChannel: null,
- location: null,
- fieldServiceRate: null,
- offsetDetails: null,
- subcontractChecklist: null
- }
-
- // Basic Info 확인 (항상 활성화)
- try {
- const basicInfoData = await getBasicInfo(contractId)
- if (basicInfoData && basicInfoData.success) {
- summary.basicInfo = basicInfoData.data || {}
- }
- } catch {
- console.log('Basic Info 데이터 없음')
- }
-
- // 품목 정보 확인
- try {
- const itemsData = await getContractItems(contractId)
- if (itemsData && itemsData.length > 0) {
- summary.items = itemsData
- }
- } catch {
- console.log('품목 정보 데이터 없음')
- }
-
- // 각 컴포넌트의 활성화 상태 및 데이터 확인
- try {
- // Communication Channel 확인
- const commData = await getCommunicationChannel(contractId)
- if (commData && commData.enabled) {
- summary.communicationChannel = commData
- }
- } catch {
- console.log('Communication Channel 데이터 없음')
- }
-
- try {
- // Location 확인
- const locationData = await getLocation(contractId)
- if (locationData && locationData.enabled) {
- summary.location = locationData
- }
- } catch {
- console.log('Location 데이터 없음')
- }
-
- try {
- // Field Service Rate 확인
- const fieldServiceData = await getFieldServiceRate(contractId)
- if (fieldServiceData && fieldServiceData.enabled) {
- summary.fieldServiceRate = fieldServiceData
- }
- } catch {
- console.log('Field Service Rate 데이터 없음')
- }
-
- try {
- // Offset Details 확인
- const offsetData = await getOffsetDetails(contractId)
- if (offsetData && offsetData.enabled) {
- summary.offsetDetails = offsetData
- }
- } catch {
- console.log('Offset Details 데이터 없음')
- }
-
- try {
- // Subcontract Checklist 확인
- const subcontractData = await getSubcontractChecklist(contractId)
- if (subcontractData && subcontractData.success && subcontractData.enabled) {
- summary.subcontractChecklist = subcontractData.data
- }
- } catch {
- console.log('Subcontract Checklist 데이터 없음')
- }
-
- 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단계: 파일 업로드 처리
- const handleFileUpload = async (file: File) => {
- // 파일 확장자 검증
- const allowedExtensions = ['.doc', '.docx']
- const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'))
-
- if (!allowedExtensions.includes(fileExtension)) {
- toast.error('Word 문서(.doc, .docx) 파일만 업로드 가능합니다.')
- return
- }
-
- if (!userId) {
- toast.error('로그인이 필요합니다.')
- return
- }
-
- setIsLoading(true)
- try {
- // 서버액션을 사용하여 파일 저장 (본 계약문서로 고정)
- const result = await uploadContractApprovalFile(
- contractId,
- file,
- userId
- )
-
- if (result.success) {
- setUploadedFile(file)
- toast.success('파일이 업로드되었습니다.')
- } else {
- throw new Error(result.error || '파일 업로드 실패')
- }
- } catch (error) {
- console.error('Error uploading file:', error)
- toast.error('파일 업로드 중 오류가 발생했습니다.')
- } finally {
- setIsLoading(false)
- }
- }
-
- // 4단계: PDF 생성 및 미리보기 (PDFTron 사용)
- const generatePdf = async () => {
- if (!uploadedFile || !contractSummary) {
- toast.error('업로드된 파일과 계약 정보가 필요합니다.')
- return
- }
-
- setIsLoading(true)
- try {
- // 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(uploadedFile, {
- filename: uploadedFile.name,
- extension: 'docx',
- })
-
- // LOI 템플릿용 변수 매핑
- const mappedTemplateData = mapContractSummaryToLOITemplate(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 변환 완료: ${uploadedFile.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) {
- console.error('❌ PDF 생성 실패:', error)
- toast.error('PDF 생성 중 오류가 발생했습니다.')
- } 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], { type: 'application/pdf' })
- const pdfUrl = URL.createObjectURL(pdfBlob)
- console.log("🔄 PDF Blob URL 생성:", pdfUrl)
-
- // 문서 로드
- console.log("🔄 문서 로드 시작")
- const { documentViewer } = instance.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], { 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 미리보기 닫기 완료")
- }
-
- // 최종 전송
- 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}개의 기본계약서가 생성되었습니다.`);
- }
- }
- }
-
- // 서버액션을 사용하여 계약승인요청 전송
- const result = await sendContractApprovalRequest(
- contractSummary,
- generatedPdfBuffer,
- 'contractDocument',
- userId,
- generatedBasicContractPdfs
- )
-
- if (result.success) {
- toast.success('계약승인요청이 전송되었습니다.')
- onOpenChange(false)
- } else {
- // 서버에서 이미 처리된 에러 메시지 표시
- toast.error(result.error || '계약승인요청 전송 실패')
- return
- }
- } catch (error: any) {
- console.error('Error submitting approval request:', error)
-
- // 데이터베이스 중복 키 오류 처리
- if (error.message && error.message.includes('duplicate key value violates unique constraint')) {
- toast.error('이미 존재하는 계약번호입니다. 다른 계약번호를 사용해주세요.')
- return
- }
-
- // 다른 오류에 대한 일반적인 처리
- toast.error('계약승인요청 전송 중 오류가 발생했습니다.')
- } 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-4">
- <TabsTrigger value="1" disabled={currentStep < 1}>
- 1. 계약 현황 정리
- </TabsTrigger>
- <TabsTrigger value="2" disabled={currentStep < 2}>
- 2. 기본계약 체크
- </TabsTrigger>
- <TabsTrigger value="3" disabled={currentStep < 3}>
- 3. 문서 업로드
- </TabsTrigger>
- <TabsTrigger value="4" disabled={currentStep < 4}>
- 4. 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)
- .filter(([, value]) => value === true)
- .map(([key]) => key)
- .join(', ') || '없음'}
- </div>
- <div>
- <span className="font-medium">계약해지조건:</span>
- {contractSummary?.basicInfo?.contractTerminationConditions &&
- Object.entries(contractSummary.basicInfo.contractTerminationConditions)
- .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">{item.itemInfo || item.description || `품목 ${index + 1}`}</div>
- <div className="text-muted-foreground">
- 수량: {item.quantity || 0} | 단가: {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="communication-enabled"
- checked={!!contractSummary?.communicationChannel}
- disabled
- />
- <Label htmlFor="communication-enabled" className="font-medium">
- 커뮤니케이션 채널
- </Label>
- <Badge variant="outline">선택</Badge>
- </div>
- <p className="text-sm text-muted-foreground">
- {contractSummary?.communicationChannel
- ? '정보가 입력되어 있습니다.'
- : '정보가 입력되지 않았습니다.'}
- </p>
- </div>
-
- {/* 위치 정보 */}
- <div className="border rounded-lg p-4">
- <div className="flex items-center gap-2 mb-3">
- <Checkbox
- id="location-enabled"
- checked={!!contractSummary?.location}
- disabled
- />
- <Label htmlFor="location-enabled" className="font-medium">
- 위치 정보
- </Label>
- <Badge variant="outline">선택</Badge>
- </div>
- <p className="text-sm text-muted-foreground">
- {contractSummary?.location
- ? '정보가 입력되어 있습니다.'
- : '정보가 입력되지 않았습니다.'}
- </p>
- </div>
-
- {/* 현장 서비스 요율 */}
- <div className="border rounded-lg p-4">
- <div className="flex items-center gap-2 mb-3">
- <Checkbox
- id="fieldService-enabled"
- checked={!!contractSummary?.fieldServiceRate}
- disabled
- />
- <Label htmlFor="fieldService-enabled" className="font-medium">
- 현장 서비스 요율
- </Label>
- <Badge variant="outline">선택</Badge>
- </div>
- <p className="text-sm text-muted-foreground">
- {contractSummary?.fieldServiceRate
- ? '정보가 입력되어 있습니다.'
- : '정보가 입력되지 않았습니다.'}
- </p>
- </div>
-
- {/* 오프셋 세부사항 */}
- <div className="border rounded-lg p-4">
- <div className="flex items-center gap-2 mb-3">
- <Checkbox
- id="offset-enabled"
- checked={!!contractSummary?.offsetDetails}
- disabled
- />
- <Label htmlFor="offset-enabled" className="font-medium">
- 오프셋 세부사항
- </Label>
- <Badge variant="outline">선택</Badge>
- </div>
- <p className="text-sm text-muted-foreground">
- {contractSummary?.offsetDetails
- ? '정보가 입력되어 있습니다.'
- : '정보가 입력되지 않았습니다.'}
- </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단계: 문서 업로드 */}
- <TabsContent value="3" className="space-y-4">
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <Upload className="h-5 w-5 text-blue-600" />
- 계약서 업로드
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- <div className="space-y-4">
- <p className="text-lg text-muted-foreground">일반계약 표준문서 관리 페이지에 접속하여, 원하는 양식의 계약서를 다운받아 수정 후 업로드하세요.</p>
- <div>
- <Label htmlFor="file-upload">파일 업로드</Label>
- <Input
- id="file-upload"
- type="file"
- accept=".doc,.docx"
- onChange={(e) => {
- const file = e.target.files?.[0]
- if (file) handleFileUpload(file)
- }}
- />
- <p className="text-sm text-muted-foreground mt-1">
- Word 문서(.doc, .docx) 파일만 업로드 가능합니다.
- </p>
- </div>
-
- {/* ContractDocuments 컴포넌트 사용 */}
- {/* <div className="mt-4">
- <Label>업로드된 문서</Label>
- <ContractDocuments
- contractId={contractId}
- userId={userId}
- readOnly={false}
- />
- </div> */}
-
- {uploadedFile && (
- <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">업로드 완료</span>
- </div>
- <p className="text-sm text-green-800 mt-1">{uploadedFile.name}</p>
- </div>
- )}
- </div>
- </CardContent>
- </Card>
-
- <div className="flex justify-between">
- <Button variant="outline" onClick={() => setCurrentStep(2)}>
- 이전 단계
- </Button>
- <Button
- onClick={() => setCurrentStep(4)}
- disabled={!uploadedFile}
- >
- 다음 단계
- </Button>
- </div>
- </TabsContent>
-
- {/* 4단계: PDF 미리보기 */}
- <TabsContent value="4" 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(3)}>
- 이전 단계
- </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>
- )} \ No newline at end of file