From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 7 Nov 2025 08:39:04 +0000 Subject: (최겸) 입찰/견적 수정사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/bidding-pre-quote-invitation-dialog.tsx | 770 --------------------- 1 file changed, 770 deletions(-) delete mode 100644 lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx (limited to 'lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx') diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx deleted file mode 100644 index 3205df08..00000000 --- a/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx +++ /dev/null @@ -1,770 +0,0 @@ -'use client' - -import * as React from 'react' -import { Button } from '@/components/ui/button' -import { Checkbox } from '@/components/ui/checkbox' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' -import { Badge } from '@/components/ui/badge' -import { BiddingCompany } from './bidding-pre-quote-vendor-columns' -import { sendPreQuoteInvitations, sendBiddingBasicContracts, getExistingBasicContractsForBidding } from '../service' -import { getActiveContractTemplates } from '../../service' -import { useToast } from '@/hooks/use-toast' -import { useTransition } from 'react' -import { Mail, Building2, Calendar, FileText, CheckCircle, Info, RefreshCw } from 'lucide-react' -import { Progress } from '@/components/ui/progress' -import { Separator } from '@/components/ui/separator' -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { cn } from '@/lib/utils' - -interface BiddingPreQuoteInvitationDialogProps { - open: boolean - onOpenChange: (open: boolean) => void - companies: BiddingCompany[] - biddingId: number - biddingTitle: string - projectName?: string - onSuccess: () => void -} - -interface BasicContractTemplate { - id: number - templateName: string - revision: number - status: string - filePath: string | null - validityPeriod: number | null - legalReviewRequired: boolean - createdAt: Date | null -} - -interface SelectedContract { - templateId: number - templateName: string - contractType: string // templateName을 contractType으로 사용 - checked: boolean -} - -// PDF 생성 유틸리티 함수 -const generateBasicContractPdf = async ( - template: BasicContractTemplate, - vendorId: number -): Promise<{ buffer: number[]; fileName: string }> => { - try { - // 1. 템플릿 데이터 준비 (서버 API 호출) - const prepareResponse = await fetch("/api/contracts/prepare-template", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - templateName: template.templateName, - vendorId, - }), - }); - - if (!prepareResponse.ok) { - throw new Error("템플릿 준비 실패"); - } - - const { template: preparedTemplate, templateData } = await prepareResponse.json(); - - // 2. 템플릿 파일 다운로드 - const templateResponse = await fetch("/api/contracts/get-template", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ templatePath: preparedTemplate.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, - }, - tempDiv - ); - - const { Core } = instance; - const { createDocument } = Core; - - const templateDoc = await createDocument(templateFile, { - filename: templateFile.name, - extension: 'docx', - }); - - // 변수 치환 적용 - await templateDoc.applyTemplateValues(templateData); - - // PDF 변환 - const fileData = await templateDoc.getFileData(); - const pdfBuffer = await Core.officeToPDFBuffer(fileData, { extension: 'docx' }); - - const fileName = `${template.templateName}_${Date.now()}.pdf`; - - return { - buffer: Array.from(pdfBuffer), // Uint8Array를 일반 배열로 변환 - fileName - }; - - } finally { - if (tempDiv.parentNode) { - document.body.removeChild(tempDiv); - } - } - } catch (error) { - console.error(`기본계약 PDF 생성 실패 (${template.templateName}):`, error); - throw error; - } -}; - -export function BiddingPreQuoteInvitationDialog({ - open, - onOpenChange, - companies, - biddingId, - biddingTitle, - projectName, - onSuccess -}: BiddingPreQuoteInvitationDialogProps) { - const { toast } = useToast() - const [isPending, startTransition] = useTransition() - const [selectedCompanyIds, setSelectedCompanyIds] = React.useState([]) - const [preQuoteDeadline, setPreQuoteDeadline] = React.useState('') - const [additionalMessage, setAdditionalMessage] = React.useState('') - - // 기본계약 관련 상태 - const [existingContracts, setExistingContracts] = React.useState([]) - const [isGeneratingPdfs, setIsGeneratingPdfs] = React.useState(false) - const [pdfGenerationProgress, setPdfGenerationProgress] = React.useState(0) - const [currentGeneratingContract, setCurrentGeneratingContract] = React.useState('') - - // 기본계약서 템플릿 관련 상태 - const [availableTemplates, setAvailableTemplates] = React.useState([]) - const [selectedContracts, setSelectedContracts] = React.useState([]) - const [isLoadingTemplates, setIsLoadingTemplates] = React.useState(false) - - // 초대 가능한 업체들 (pending 상태인 업체들) - const invitableCompanies = React.useMemo(() => companies.filter(company => - company.invitationStatus === 'pending' && company.companyName - ), [companies]) - - // 다이얼로그가 열릴 때 기존 계약 조회 및 템플릿 로드 - React.useEffect(() => { - if (open) { - const fetchInitialData = async () => { - setIsLoadingTemplates(true); - try { - const [contractsResult, templatesData] = await Promise.all([ - getExistingBasicContractsForBidding(biddingId), - getActiveContractTemplates() - ]); - - // 기존 계약 조회 - 서버 액션 사용 - const existingContractsResult = await getExistingBasicContractsForBidding(biddingId); - setExistingContracts(existingContractsResult.success ? existingContractsResult.contracts || [] : []); - - // 템플릿 로드 (4개 타입만 필터링) - // 4개 템플릿 타입만 필터링: 비밀, General, Project, 기술자료 - const allowedTemplateNames = ['비밀', 'General GTC', '기술', '기술자료']; - const filteredTemplates = (templatesData.templates || []).filter((template: any) => - allowedTemplateNames.some(allowedName => - template.templateName.includes(allowedName) || - allowedName.includes(template.templateName) - ) - ); - setAvailableTemplates(filteredTemplates as BasicContractTemplate[]); - const initialSelected = filteredTemplates.map((template: any) => ({ - templateId: template.id, - templateName: template.templateName, - contractType: template.templateName, - checked: false - })); - setSelectedContracts(initialSelected); - - } catch (error) { - console.error('초기 데이터 로드 실패:', error); - toast({ - title: '오류', - description: '기본 정보를 불러오는 데 실패했습니다.', - variant: 'destructive', - }); - setExistingContracts([]); - setAvailableTemplates([]); - setSelectedContracts([]); - } finally { - setIsLoadingTemplates(false); - } - } - fetchInitialData(); - } - }, [open, biddingId, toast]); - - const handleSelectAll = (checked: boolean | 'indeterminate') => { - if (checked) { - // 기존 계약이 없는 업체만 선택 - const availableCompanies = invitableCompanies.filter(company => - !existingContracts.some(ec => ec.vendorId === company.companyId) - ) - setSelectedCompanyIds(availableCompanies.map(company => company.id)) - } else { - setSelectedCompanyIds([]) - } - } - - const handleSelectCompany = (companyId: number, checked: boolean) => { - const company = invitableCompanies.find(c => c.id === companyId) - const hasExistingContract = company ? existingContracts.some(ec => ec.vendorId === company.companyId) : false - - if (hasExistingContract) { - toast({ - title: '선택 불가', - description: '이미 기본계약서를 받은 업체는 다시 선택할 수 없습니다.', - variant: 'default', - }) - return - } - - if (checked) { - setSelectedCompanyIds(prev => [...prev, companyId]) - } else { - setSelectedCompanyIds(prev => prev.filter(id => id !== companyId)) - } - } - - // 기본계약서 선택 토글 - const toggleContractSelection = (templateId: number) => { - setSelectedContracts(prev => - prev.map(contract => - contract.templateId === templateId - ? { ...contract, checked: !contract.checked } - : contract - ) - ) - } - - // 모든 기본계약서 선택/해제 - const toggleAllContractSelection = (checked: boolean | 'indeterminate') => { - setSelectedContracts(prev => - prev.map(contract => ({ ...contract, checked: !!checked })) - ) - } - - const handleSendInvitations = () => { - if (selectedCompanyIds.length === 0) { - toast({ - title: '알림', - description: '초대를 발송할 업체를 선택해주세요.', - variant: 'default', - }) - return - } - - const selectedContractTemplates = selectedContracts.filter(c => c.checked); - const companiesForContracts = invitableCompanies.filter(company => selectedCompanyIds.includes(company.id)); - - const vendorsToGenerateContracts = companiesForContracts.filter(company => - !existingContracts.some(ec => - ec.vendorId === company.companyId && ec.biddingCompanyId === company.id - ) - ); - - startTransition(async () => { - try { - // 1. 사전견적 초대 발송 - const invitationResponse = await sendPreQuoteInvitations( - selectedCompanyIds, - preQuoteDeadline || undefined - ) - - if (!invitationResponse.success) { - toast({ - title: '초대 발송 실패', - description: invitationResponse.error, - variant: 'destructive', - }) - return - } - - // 2. 기본계약 발송 (선택된 템플릿과 업체가 있는 경우) - let contractResponse: Awaited> | null = null - if (selectedContractTemplates.length > 0 && selectedCompanyIds.length > 0) { - setIsGeneratingPdfs(true) - setPdfGenerationProgress(0) - - const generatedPdfsMap = new Map() - - let generatedCount = 0; - for (const vendor of vendorsToGenerateContracts) { - for (const contract of selectedContractTemplates) { - setCurrentGeneratingContract(`${vendor.companyName} - ${contract.templateName}`); - const templateDetails = availableTemplates.find(t => t.id === contract.templateId); - - if (templateDetails) { - const pdfData = await generateBasicContractPdf(templateDetails, vendor.companyId); - // sendBiddingBasicContracts와 동일한 키 형식 사용 - let contractType = ''; - if (contract.templateName.includes('비밀')) { - contractType = 'NDA'; - } else if (contract.templateName.includes('General GTC')) { - contractType = 'General_GTC'; - } else if (contract.templateName.includes('기술') && !contract.templateName.includes('기술자료')) { - contractType = 'Project_GTC'; - } else if (contract.templateName.includes('기술자료')) { - contractType = '기술자료'; - } - const key = `${vendor.companyId}_${contractType}_${contract.templateName}`; - generatedPdfsMap.set(key, pdfData); - } - } - generatedCount++; - setPdfGenerationProgress((generatedCount / vendorsToGenerateContracts.length) * 100); - } - - setIsGeneratingPdfs(false); - - const vendorData = companiesForContracts.map(company => { - // 선택된 템플릿에 따라 contractRequirements 동적으로 설정 - const contractRequirements = { - ndaYn: selectedContractTemplates.some(c => c.templateName.includes('비밀')), - generalGtcYn: selectedContractTemplates.some(c => c.templateName.includes('General GTC')), - projectGtcYn: selectedContractTemplates.some(c => c.templateName.includes('기술') && !c.templateName.includes('기술자료')), - agreementYn: selectedContractTemplates.some(c => c.templateName.includes('기술자료')) - }; - - return { - vendorId: company.companyId, - vendorName: company.companyName || '', - vendorCode: company.companyCode, - vendorCountry: '대한민국', - selectedMainEmail: company.contactEmail || '', - contactPerson: company.contactPerson, - contactEmail: company.contactEmail, - biddingCompanyId: company.id, - biddingId: biddingId, - hasExistingContracts: existingContracts.some(ec => - ec.vendorId === company.companyId && ec.biddingCompanyId === company.id - ), - contractRequirements, - additionalEmails: [], - customEmails: [] - }; - }); - - const pdfsArray = Array.from(generatedPdfsMap.entries()).map(([key, data]) => ({ - key, - buffer: data.buffer, - fileName: data.fileName, - })); - - console.log("Calling sendBiddingBasicContracts with biddingId:", biddingId); - console.log("vendorData:", vendorData.map(v => ({ vendorId: v.vendorId, biddingCompanyId: v.biddingCompanyId, biddingId: v.biddingId }))); - - contractResponse = await sendBiddingBasicContracts( - biddingId, - vendorData, - pdfsArray, - additionalMessage - ); - } - - let successMessage = '사전견적 초대가 성공적으로 발송되었습니다.'; - if (contractResponse && contractResponse.success) { - successMessage += `\n${contractResponse.message}`; - } - - toast({ - title: '성공', - description: successMessage, - }) - - // 상태 초기화 - setSelectedCompanyIds([]); - setPreQuoteDeadline(''); - setAdditionalMessage(''); - setExistingContracts([]); - setIsGeneratingPdfs(false); - setPdfGenerationProgress(0); - setCurrentGeneratingContract(''); - setSelectedContracts(prev => prev.map(c => ({ ...c, checked: false }))); - - onOpenChange(false); - onSuccess(); - - } catch (error) { - console.error('발송 실패:', error); - toast({ - title: '오류', - description: '발송 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', - variant: 'destructive', - }); - setIsGeneratingPdfs(false); - } - }) - } - - const handleOpenChange = (open: boolean) => { - onOpenChange(open) - if (!open) { - setSelectedCompanyIds([]) - setPreQuoteDeadline('') - setAdditionalMessage('') - setExistingContracts([]) - setIsGeneratingPdfs(false) - setPdfGenerationProgress(0) - setCurrentGeneratingContract('') - setSelectedContracts([]) - } - } - - const selectedContractCount = selectedContracts.filter(c => c.checked).length; - const selectedCompanyCount = selectedCompanyIds.length; - const companiesToReceiveContracts = invitableCompanies.filter(company => selectedCompanyIds.includes(company.id)); - - // 기존 계약이 없는 업체들만 계산 - const availableCompanies = invitableCompanies.filter(company => - !existingContracts.some(ec => ec.vendorId === company.companyId) - ); - const selectedAvailableCompanyCount = selectedCompanyIds.filter(id => - availableCompanies.some(company => company.id === id) - ).length; - - // 선택된 업체들 중 기존 계약이 있는 업체들 - const selectedCompaniesWithExistingContracts = invitableCompanies.filter(company => - selectedCompanyIds.includes(company.id) && - existingContracts.some(ec => ec.vendorId === company.companyId) - ); - - return ( - - - - - - 사전견적 초대 및 기본계약 발송 - - - 선택한 업체들에게 사전견적 요청과 기본계약서를 발송합니다. - - - -
-
- {/* 견적 마감일 설정 */} -
- - setPreQuoteDeadline(e.target.value)} - className="w-full" - /> -
- - {/* 기존 계약 정보 알림 */} - {existingContracts.length > 0 && ( - - - 기존 계약 정보 - - 이미 기본계약을 받은 업체가 있습니다. - 해당 업체들은 초대 대상에서 제외되며, 계약서 재생성도 건너뜁니다. - - - )} - - {/* 업체 선택 섹션 */} - - - - - 초대 대상 업체 - - - - {invitableCompanies.length === 0 ? ( -
- 초대 가능한 업체가 없습니다. -
- ) : ( - <> -
-
- 0} - onCheckedChange={handleSelectAll} - /> - -
- - {selectedCompanyCount}개 선택됨 - -
- -
- {invitableCompanies.map((company) => { - const hasExistingContract = existingContracts.some(ec => ec.vendorId === company.companyId); - return ( -
- handleSelectCompany(company.id, !!checked)} - /> -
-
- - {company.companyName} - - - {company.companyCode} - - {hasExistingContract && ( - - - 계약 체결됨 - - )} -
- {hasExistingContract && ( -

- 이미 기본계약서를 받은 업체입니다. 선택에서 제외됩니다. -

- )} -
-
- ) - })} -
- - )} -
-
- - {/* 선택된 업체 중 기존 계약이 있는 경우 경고 */} - {selectedCompaniesWithExistingContracts.length > 0 && ( - - - 선택한 업체 중 제외될 업체 - - 선택한 {selectedCompaniesWithExistingContracts.length}개 업체가 이미 기본계약서를 받았습니다. - 이 업체들은 초대 발송 및 계약서 생성에서 제외됩니다. -
- 실제 발송 대상: {selectedCompanyCount - selectedCompaniesWithExistingContracts.length}개 업체 -
-
- )} - - {/* 기본계약서 선택 섹션 */} - - - - - - 기본계약서 선택 (선택된 업체에만 발송) - - - - {isLoadingTemplates ? ( -
- -

기본계약서 템플릿을 불러오는 중...

-
- ) : ( -
- {selectedCompanyCount === 0 && ( - - - 알림 - - 기본계약서를 발송할 업체를 먼저 선택해주세요. - - - )} - {availableTemplates.length === 0 ? ( -
- -

사용 가능한 기본계약서 템플릿이 없습니다.

-
- ) : ( - <> -
-
- 0 && selectedContracts.every(c => c.checked)} - onCheckedChange={toggleAllContractSelection} - /> - -
- - {selectedContractCount}개 선택됨 - -
-
- {selectedContracts.map((contract) => ( -
toggleContractSelection(contract.templateId)} - > -
- toggleContractSelection(contract.templateId)} - /> -
- -

- {contract.contractType} -

-
-
-
- ))} -
- - )} - {selectedContractCount > 0 && ( -
-
- - - 선택된 기본계약서 ({selectedContractCount}개) - -
-
    - {selectedContracts.filter(c => c.checked).map((contract) => ( -
  • - {contract.templateName} -
  • - ))} -
-
- )} -
- )} -
-
- - {/* 추가 메시지 */} -
- -