"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" 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("🚀 1단계: 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("✅ 1단계: PQ 생성 완료") // 2단계 & 3단계: 기본계약서 템플릿이 선택된 경우에만 실행 (여러 템플릿 처리) if (selectedTemplateIds.length > 0) { console.log(`🚀 2단계 & 3단계: ${selectedTemplateIds.length}개 템플릿 처리 시작`) let successCount = 0 let errorCount = 0 const errors: string[] = [] // 템플릿별로 반복 처리 for (let i = 0; i < selectedTemplateIds.length; i++) { const templateId = selectedTemplateIds[i] const selectedTemplate = basicContractTemplates.find(t => t.id === templateId) if (!selectedTemplate) { console.error(`템플릿 ID ${templateId}를 찾을 수 없습니다`) errorCount++ errors.push(`템플릿 ID ${templateId}를 찾을 수 없습니다`) continue } try { console.log(`📄 [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 2단계: DOCX to PDF 변환 시작`) // 템플릿 파일을 가져와서 PDF로 변환 const formData = new FormData() // 템플릿 파일 가져오기 (서버에서 파일 읽기) const templateResponse = await fetch('/api/basic-contract/get-template', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ templateId }) }) if (!templateResponse.ok) { throw new Error(`템플릿 파일을 가져올 수 없습니다: ${selectedTemplate.templateName}`) } console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 템플릿 파일 가져오기 완료`) const templateBlob = await templateResponse.blob() const templateFile = new File([templateBlob], selectedTemplate.fileName || 'template.docx') // 템플릿 데이터 생성 (첫 번째 협력업체 정보 기반) const firstVendor = vendors[0] const templateData = { // 영문 변수명으로 변경 (PDFTron이 한글 변수명을 지원하지 않음) vendor_name: firstVendor?.vendorName || '협력업체명', address: firstVendor?.address || '주소', representative_name: firstVendor?.representativeName || '대표자명', today_date: new Date().toLocaleDateString('ko-KR'), } console.log(`📝 [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 생성된 템플릿 데이터:`, templateData) formData.append('templateFile', templateFile) formData.append('outputFileName', `${selectedTemplate.templateName}_converted.pdf`) formData.append('templateData', JSON.stringify(templateData)) // PDF 변환 호출 const pdfResponse = await fetch('/api/pdftron/createBasicContractPdf', { method: 'POST', body: formData, }) console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - PDF 변환 호출 완료`) if (!pdfResponse.ok) { const errorText = await pdfResponse.text() throw new Error(`PDF 변환 실패 (${selectedTemplate.templateName}): ${errorText}`) } const pdfBuffer = await pdfResponse.arrayBuffer() console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - PDF 변환 완료`) // 3단계: 변환된 PDF로 기본계약 생성 console.log(`📋 [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 3단계: 기본계약 생성 시작`) const { error: contractError } = await requestBasicContractInfo({ vendorIds: vendors.map((v) => v.id), requestedBy: Number(session.user.id), templateId, pdfBuffer: new Uint8Array(pdfBuffer), // ArrayBuffer를 Uint8Array로 변환하여 전달 }) if (contractError) { console.error(`기본계약 생성 오류 (${selectedTemplate.templateName}):`, contractError) errorCount++ errors.push(`${selectedTemplate.templateName}: ${contractError}`) } else { console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 3단계: 기본계약 생성 완료`) successCount++ } } catch (templateError) { console.error(`템플릿 처리 오류 (${selectedTemplate.templateName}):`, templateError) errorCount++ errors.push(`${selectedTemplate.templateName}: ${templateError instanceof Error ? templateError.message : '알 수 없는 오류'}`) } } // 결과 토스트 메시지 if (successCount > 0 && errorCount === 0) { toast.success(`PQ 요청 및 ${successCount}개 기본계약서 생성이 모두 완료되었습니다!`) } else if (successCount > 0 && errorCount > 0) { toast.success(`PQ는 성공적으로 요청되었습니다. ${successCount}개 기본계약서 성공, ${errorCount}개 실패`) console.error('기본계약서 생성 오류들:', errors) } else if (errorCount > 0) { toast.error(`PQ는 성공적으로 요청되었지만, 모든 기본계약서 생성이 실패했습니다`) console.error('기본계약서 생성 오류들:', errors) } } else { // 기본계약서 템플릿이 선택되지 않은 경우 toast.success("PQ가 성공적으로 요청되었습니다") } props.onOpenChange?.(false) onSuccess?.() } catch (error) { console.error('전체 프로세스 오류:', error) toast.error(`처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`) } }) } const dialogContent = (
{/* 선택된 협력업체 정보 */}
{vendors.map((vendor) => (
{vendor.vendorName}
{vendor.vendorCode} • {vendor.email || "이메일 없음"}
))}
{type === "PROJECT" && (
)} {/* 마감일 입력 */}
setDueDate(date ? date.toISOString().slice(0, 10) : "")} placeholder="마감일 선택" />
{/* PQ 대상품목 */}