"use client" import * as React from "react" import { type Row } from "@tanstack/react-table" import { Loader, SendHorizonal } from "lucide-react" import { toast } from "sonner" import { useMediaQuery } from "@/hooks/use-media-query" import { Button } from "@/components/ui/button" import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Checkbox } from "@/components/ui/checkbox" import { Label } from "@/components/ui/label" import { Vendor } from "@/db/schema/vendors" import { requestBasicContractInfo, requestPQVendors } from "../service" import { getProjectsWithPQList } from "@/lib/pq/service" import type { Project } from "@/lib/pq/service" import { useSession } from "next-auth/react" import { DatePicker } from "@/components/ui/date-picker" import { getALLBasicContractTemplates } from "@/lib/basic-contract/service" import type { BasicContractTemplate } from "@/db/schema" // import { PQContractViewer } from "../pq-contract-viewer" // 더 이상 사용하지 않음 interface RequestPQDialogProps extends React.ComponentPropsWithoutRef { vendors: Row["original"][] showTrigger?: boolean onSuccess?: () => void } const AGREEMENT_LIST = [ "준법서약", "표준하도급계약", "안전보건관리계약", "윤리규범 준수 서약", "동반성장협약", "내국신용장 미개설 합의", "기술자료 제출 기본 동의", "GTC 합의", ] export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...props }: RequestPQDialogProps) { const [isApprovePending, startApproveTransition] = React.useTransition() const isDesktop = useMediaQuery("(min-width: 640px)") const { data: session } = useSession() const [type, setType] = React.useState<"GENERAL" | "PROJECT" | "NON_INSPECTION" | null>(null) const [dueDate, setDueDate] = React.useState(null) const [projects, setProjects] = React.useState([]) const [selectedProjectId, setSelectedProjectId] = React.useState(null) const [agreements, setAgreements] = React.useState>({}) const [extraNote, setExtraNote] = React.useState("") const [pqItems, setPqItems] = React.useState("") const [isLoadingProjects, setIsLoadingProjects] = React.useState(false) const [basicContractTemplates, setBasicContractTemplates] = React.useState([]) const [selectedTemplateIds, setSelectedTemplateIds] = React.useState([]) const [isLoadingTemplates, setIsLoadingTemplates] = React.useState(false) React.useEffect(() => { if (type === "PROJECT") { setIsLoadingProjects(true) getProjectsWithPQList().then(setProjects).catch(() => toast.error("프로젝트 로딩 실패")) .finally(() => setIsLoadingProjects(false)) } }, [type]) // 기본계약서 템플릿 로딩 React.useEffect(() => { setIsLoadingTemplates(true) getALLBasicContractTemplates() .then(setBasicContractTemplates) .catch(() => toast.error("기본계약서 템플릿 로딩 실패")) .finally(() => setIsLoadingTemplates(false)) }, []) React.useEffect(() => { if (!props.open) { setType(null) setSelectedProjectId(null) setAgreements({}) setDueDate(null) setPqItems("") setExtraNote("") setSelectedTemplateIds([]) } }, [props.open]) const onApprove = () => { if (!type) return toast.error("PQ 유형을 선택하세요.") if (type === "PROJECT" && !selectedProjectId) return toast.error("프로젝트를 선택하세요.") if (!dueDate) return toast.error("마감일을 선택하세요.") if (!session?.user?.id) return toast.error("인증 실패") startApproveTransition(async () => { try { // 1단계: PQ 생성 console.log("🚀 PQ 생성 시작") const { error: pqError } = await requestPQVendors({ ids: vendors.map((v) => v.id), userId: Number(session.user.id), agreements, dueDate, projectId: type === "PROJECT" ? selectedProjectId : null, type: type || "GENERAL", extraNote, pqItems, templateId: selectedTemplateIds.length > 0 ? selectedTemplateIds[0] : null, }) if (pqError) { toast.error(`PQ 생성 실패: ${pqError}`) return } console.log("✅ PQ 생성 완료") toast.success("PQ가 성공적으로 요청되었습니다") // 2단계: 기본계약서 템플릿이 선택된 경우 백그라운드에서 처리 if (selectedTemplateIds.length > 0) { const templates = basicContractTemplates.filter(t => selectedTemplateIds.includes(t.id) ) console.log("📋 기본계약서 백그라운드 처리 시작", templates.length, "개 템플릿") await processBasicContractsInBackground(templates, vendors) } // 완료 후 다이얼로그 닫기 props.onOpenChange?.(false) onSuccess?.() } catch (error) { console.error('PQ 생성 오류:', error) toast.error(`처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`) } }) } // 백그라운드에서 기본계약서 처리 const processBasicContractsInBackground = async (templates: BasicContractTemplate[], vendors: any[]) => { if (!session?.user?.id) { toast.error("인증 정보가 없습니다") return } try { const totalContracts = templates.length * vendors.length let processedCount = 0 // 각 벤더별로, 각 템플릿을 처리 for (let vendorIndex = 0; vendorIndex < vendors.length; vendorIndex++) { const vendor = vendors[vendorIndex] // 벤더별 템플릿 데이터 생성 const templateData = { vendor_name: vendor.vendorName || '협력업체명', address: vendor.address || '주소', representative_name: vendor.representativeName || '대표자명', today_date: new Date().toLocaleDateString('ko-KR'), } console.log(`🔄 벤더 ${vendorIndex + 1}/${vendors.length} 템플릿 데이터:`, templateData) // 해당 벤더에 대해 각 템플릿을 순차적으로 처리 for (let templateIndex = 0; templateIndex < templates.length; templateIndex++) { const template = templates[templateIndex] processedCount++ console.log(`📄 처리 중: ${vendor.vendorName} - ${template.templateName} (${processedCount}/${totalContracts})`) // 개별 벤더에 대한 기본계약 생성 await processTemplate(template, templateData, [vendor]) console.log(`✅ 완료: ${vendor.vendorName} - ${template.templateName}`) } } toast.success(`총 ${totalContracts}개 기본계약이 모두 생성되었습니다`) } catch (error) { console.error('기본계약 처리 중 오류:', error) toast.error(`기본계약 처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`) } } const processTemplate = async (template: BasicContractTemplate, templateData: any, vendors: any[]) => { try { // 1. 템플릿 파일 가져오기 const templateResponse = await fetch('/api/basic-contract/get-template', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ templateId: template.id }) }) if (!templateResponse.ok) { throw new Error(`템플릿 파일을 가져올 수 없습니다: ${template.templateName}`) } const templateBlob = await templateResponse.blob() // 2. 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 // 3. 템플릿 문서 생성 및 변수 치환 const templateDoc = await createDocument(templateBlob, { filename: template.fileName || 'template.docx', extension: 'docx', }) console.log("🔄 변수 치환 시작:", templateData) await templateDoc.applyTemplateValues(templateData) console.log("✅ 변수 치환 완료") // 4. PDF 변환 const fileData = await templateDoc.getFileData() const pdfBuffer = await Core.officeToPDFBuffer(fileData, { extension: 'docx' }) console.log(`✅ PDF 변환 완료: ${template.templateName}`, `크기: ${pdfBuffer.byteLength} bytes`) // 5. 기본계약 생성 요청 const { error: contractError } = await requestBasicContractInfo({ vendorIds: vendors.map((v) => v.id), requestedBy: Number(session!.user.id), templateId: template.id, pdfBuffer: new Uint8Array(pdfBuffer), }) if (contractError) { throw new Error(contractError) } console.log(`✅ 기본계약 생성 완료: ${template.templateName}`) } finally { // 임시 WebViewer 정리 instance.UI.dispose() document.body.removeChild(tempDiv) } } catch (error) { console.error(`❌ 템플릿 처리 실패: ${template.templateName}`, error) throw error } } const dialogContent = (
{/* 선택된 협력업체 정보 */}
{vendors.map((vendor) => (
{vendor.vendorName}
{vendor.vendorCode} • {vendor.email || "이메일 없음"}
))}
{type === "PROJECT" && (
)} {/* 마감일 입력 */}
{ if (date) { // 한국 시간대로 날짜 변환 (UTC 변환으로 인한 날짜 변경 방지) const kstDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000) setDueDate(kstDate.toISOString().slice(0, 10)) } else { setDueDate("") } }} placeholder="마감일 선택" />
{/* PQ 대상품목 */}