"use client" import * as React from "react" import { type Row } from "@tanstack/react-table" import { Loader, SendHorizonal, Search, X, Plus, Router } 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 { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" 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 { searchItemsForPQ } from "@/lib/items/service" import { saveNdaAttachments } from "../service" import { useRouter } from "next/navigation" // import { PQContractViewer } from "../pq-contract-viewer" // 더 이상 사용하지 않음 interface RequestPQDialogProps extends React.ComponentPropsWithoutRef { vendors: Row["original"][] showTrigger?: boolean onSuccess?: () => void } // const AGREEMENT_LIST = [ // "준법서약", // "표준하도급계약", // "안전보건관리계약", // "윤리규범 준수 서약", // "동반성장협약", // "내국신용장 미개설 합의", // "기술자료 제출 기본 동의", // "GTC 합의", // ] // PQ 대상 품목 타입 정의 interface PQItem { itemCode: string itemName: string } 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 router = useRouter() 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 [itemSearchQuery, setItemSearchQuery] = React.useState("") const [filteredItems, setFilteredItems] = React.useState([]) const [showItemDropdown, setShowItemDropdown] = React.useState(false) const [isLoadingProjects, setIsLoadingProjects] = React.useState(false) const [basicContractTemplates, setBasicContractTemplates] = React.useState([]) const [selectedTemplateIds, setSelectedTemplateIds] = React.useState([]) const [isLoadingTemplates, setIsLoadingTemplates] = React.useState(false) // 비밀유지 계약서 첨부파일 관련 상태 const [ndaAttachments, setNdaAttachments] = React.useState([]) const [isUploadingNdaFiles, setIsUploadingNdaFiles] = React.useState(false) // 아이템 검색 필터링 React.useEffect(() => { if (itemSearchQuery.trim() === "") { setFilteredItems([]) setShowItemDropdown(false) return } const searchItems = async () => { try { const results = await searchItemsForPQ(itemSearchQuery) setFilteredItems(results) setShowItemDropdown(true) } catch (error) { console.error("아이템 검색 오류:", error) toast.error("아이템 검색 중 오류가 발생했습니다.") setFilteredItems([]) setShowItemDropdown(false) } } // 디바운싱: 300ms 후에 검색 실행 const timeoutId = setTimeout(searchItems, 300) return () => clearTimeout(timeoutId) }, [itemSearchQuery]) React.useEffect(() => { if (type === "PROJECT") { setIsLoadingProjects(true) getProjectsWithPQList().then(setProjects).catch(() => toast.error("프로젝트 로딩 실패")) .finally(() => setIsLoadingProjects(false)) } }, [type]) // 기본계약서 템플릿 로딩 및 자동 선택 React.useEffect(() => { setIsLoadingTemplates(true) getALLBasicContractTemplates() .then((templates) => { setBasicContractTemplates(templates) // 벤더 국가별 자동 선택 로직 if (vendors.length > 0) { const isAllForeign = vendors.every(vendor => vendor.country !== 'KR') const isAllDomestic = vendors.every(vendor => vendor.country === 'KR') if (isAllForeign) { // 외자: 준법서약 (영문), GTC만 선택 const foreignTemplates = templates.filter(template => template.templateName?.includes('준법서약') && template.templateName?.includes('영문') || template.templateName?.includes('GTC') ) setSelectedTemplateIds(foreignTemplates.map(t => t.id)) } else if (isAllDomestic) { // 내자: 준법서약 (영문), GTC 제외한 모든 템플릿 선택 const domesticTemplates = templates.filter(template => { const name = template.templateName?.toLowerCase() || '' return !(name.includes('준법서약') && name.includes('영문')) && !name.includes('gtc') }) setSelectedTemplateIds(domesticTemplates.map(t => t.id)) } } }) .catch(() => toast.error("기본계약서 템플릿 로딩 실패")) .finally(() => setIsLoadingTemplates(false)) }, [vendors]) React.useEffect(() => { if (!props.open) { setType(null) setSelectedProjectId(null) setAgreements({}) setDueDate(null) setPqItems([]) setExtraNote("") setSelectedTemplateIds([]) setItemSearchQuery("") setFilteredItems([]) setShowItemDropdown(false) setNdaAttachments([]) setIsUploadingNdaFiles(false) } }, [props.open]) // 아이템 선택 함수 const handleSelectItem = (item: PQItem) => { // 이미 선택된 아이템인지 확인 const isAlreadySelected = pqItems.some(selectedItem => selectedItem.itemCode === item.itemCode ) if (!isAlreadySelected) { setPqItems(prev => [...prev, item]) } // 검색 초기화 setItemSearchQuery("") setFilteredItems([]) setShowItemDropdown(false) } // 아이템 제거 함수 const handleRemoveItem = (itemCode: string) => { setPqItems(prev => prev.filter(item => item.itemCode !== itemCode)) } // 비밀유지 계약서 첨부파일 추가 함수 const handleAddNdaAttachment = (event: React.ChangeEvent) => { const files = event.target.files if (files) { const newFiles = Array.from(files) setNdaAttachments(prev => [...prev, ...newFiles]) } // input 초기화 event.target.value = '' } // 비밀유지 계약서 첨부파일 제거 함수 const handleRemoveNdaAttachment = (fileIndex: number) => { setNdaAttachments(prev => prev.filter((_, index) => index !== fileIndex)) } // 비밀유지 계약서가 선택되었는지 확인하는 함수 const isNdaTemplateSelected = () => { return basicContractTemplates.some(template => selectedTemplateIds.includes(template.id) && template.templateName?.includes("비밀유지") ) } 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: JSON.stringify(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) } // 3단계: 비밀유지 계약서 첨부파일이 있는 경우 저장 if (isNdaTemplateSelected() && ndaAttachments.length > 0) { console.log("📎 비밀유지 계약서 첨부파일 처리 시작", ndaAttachments.length, "개 파일") const ndaResult = await saveNdaAttachments({ vendorIds: vendors.map((v) => v.id), files: ndaAttachments, userId: session.user.id.toString() }) if (ndaResult.success) { toast.success(`비밀유지 계약서 첨부파일이 모두 저장되었습니다 (${ndaResult.summary?.success}/${ndaResult.summary?.total})`) } else { toast.error(`첨부파일 처리 중 일부 오류가 발생했습니다: ${ndaResult.error}`) } } // 완료 후 다이얼로그 닫기 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 = { company_name: vendor.vendorName || '협력업체명', company_address: vendor.address || '주소', representative_name: vendor.representativeName || '대표자명', signature_date: new Date().toLocaleDateString('ko-KR'), tax_id: vendor.taxId || '사업자번호', phone_number: vendor.phone || '전화번호', } 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}개 기본계약이 모두 생성되었습니다`) router.refresh(); } 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 대상품목 */}
{/* 선택된 아이템들 표시 */} {pqItems.length > 0 && (
{pqItems.map((item) => ( {item.itemCode} - {item.itemName} ))}
)} {/* 검색 입력 */}
setItemSearchQuery(e.target.value)} className="pl-9" />
{/* 검색 결과 드롭다운 */} {showItemDropdown && (
{filteredItems.length > 0 ? ( filteredItems.map((item) => ( )) ) : (
검색 결과가 없습니다.
)}
)}
아이템 코드나 이름을 입력하여 검색하고 선택하세요. (선택사항)
{/* 추가 안내사항 */}