From 8a19a6fa336768d8b6712752c9d713360067ecb0 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 8 Dec 2025 08:45:20 +0000 Subject: (최겸) 구매 피드백 수정, 안전담당자, pq항목 내 첨부, 내외자 구분, 도로명주소 api 반영(운영기준) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/vendors/table/request-pq-dialog.tsx | 2275 ++++++++++++++++--------------- 1 file changed, 1145 insertions(+), 1130 deletions(-) (limited to 'lib/vendors/table/request-pq-dialog.tsx') diff --git a/lib/vendors/table/request-pq-dialog.tsx b/lib/vendors/table/request-pq-dialog.tsx index 83eab201..4197b879 100644 --- a/lib/vendors/table/request-pq-dialog.tsx +++ b/lib/vendors/table/request-pq-dialog.tsx @@ -1,1130 +1,1145 @@ -"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 { Progress } from "@/components/ui/progress" -import { Vendor } from "@/db/schema/vendors" -import { requestBasicContractInfo, requestPQVendors, sendBasicContractEmail } from "../service" -import { getProjectsWithPQList, getNonInspectionPQLists } 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 { saveNdaAttachments, getVendorPQHistory } from "../service" -import { useRouter } from "next/navigation" -import { createGtcVendorDocuments, createProjectGtcVendorDocuments, getStandardGtcDocumentId, getProjectGtcDocumentId } from "@/lib/gtc-contract/service" -import { MaterialGroupSelectorDialogMulti } from "@/components/common/material/material-group-selector-dialog-multi" -import type { MaterialSearchItem } from "@/lib/material/material-group-service" -// import { PQContractViewer } from "../pq-contract-viewer" // 더 이상 사용하지 않음 - -interface RequestPQDialogProps extends React.ComponentPropsWithoutRef { - vendors: Row["original"][] - showTrigger?: boolean - onSuccess?: () => void -} - -// const AGREEMENT_LIST = [ -// "준법서약", -// "표준하도급계약", -// "안전보건관리계약", -// "윤리규범 준수 서약", -// "동반성장협약", -// "내국신용장 미개설 합의", -// "기술자료 제출 기본 동의", -// "GTC 합의", -// ] - -// PQ 대상 품목 타입 정의 (Material Group 기반) - MaterialSearchItem 사용 - -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([]) - - // PQ 품목 선택 관련 상태는 MaterialGroupSelectorDialogMulti에서 관리됨 - const [isLoadingProjects, setIsLoadingProjects] = React.useState(false) - const [basicContractTemplates, setBasicContractTemplates] = React.useState([]) - const [selectedTemplateIds, setSelectedTemplateIds] = React.useState([]) - const [isLoadingTemplates, setIsLoadingTemplates] = React.useState(false) - - // 미실사 PQ 관련 상태 - const [activeNonInspectionPQList, setActiveNonInspectionPQList] = React.useState(null) - const [isLoadingNonInspectionPQ, setIsLoadingNonInspectionPQ] = React.useState(false) - - // 비밀유지 계약서 첨부파일 관련 상태 - const [ndaAttachments, setNdaAttachments] = React.useState([]) - const [isUploadingNdaFiles, setIsUploadingNdaFiles] = React.useState(false) - - // 프로그레스 관련 상태 - const [progressValue, setProgressValue] = React.useState(0) - const [currentStep, setCurrentStep] = React.useState("") - const [showProgress, setShowProgress] = React.useState(false) - - // PQ 히스토리 관련 상태 - const [pqHistory, setPqHistory] = React.useState>({}) - const [isLoadingHistory, setIsLoadingHistory] = React.useState(false) - - - React.useEffect(() => { - if (type === "PROJECT") { - setIsLoadingProjects(true) - getProjectsWithPQList().then(setProjects).catch(() => toast.error("프로젝트 로딩 실패")) - .finally(() => setIsLoadingProjects(false)) - } else if (type === "NON_INSPECTION") { - setIsLoadingNonInspectionPQ(true) - // 활성화된 미실사 PQ 리스트 조회 - getNonInspectionPQLists().then(result => { - if (result.success) { - setActiveNonInspectionPQList(result.data) - } else { - setActiveNonInspectionPQList(null) - } - }).catch(() => { - toast.error("미실사 PQ 리스트 로딩 실패") - setActiveNonInspectionPQList(null) - }).finally(() => setIsLoadingNonInspectionPQ(false)) - } - }, [type]) - - // 기본계약서 템플릿 로딩 및 자동 선택, PQ 히스토리 로딩 - React.useEffect(() => { - setIsLoadingTemplates(true) - const loadPQHistory = async () => { - if (vendors.length === 0) return - - setIsLoadingHistory(true) - try { - const historyPromises = vendors.map(async (vendor) => { - console.log("vendor.id", vendor.id) - const result = await getVendorPQHistory(vendor.id) - console.log("result", result) - return { vendorId: vendor.id, history: result.success ? result.data : [] } - }) - - const results = await Promise.all(historyPromises) - const historyMap: Record = {} - - results.forEach(({ vendorId, history }) => { - historyMap[vendorId] = history - }) - - setPqHistory(historyMap) - } catch (error) { - console.error('PQ 히스토리 로딩 실패:', error) - toast.error('PQ 히스토리 로딩 중 오류가 발생했습니다') - } finally { - setIsLoadingHistory(false) - } - } - loadPQHistory() - getALLBasicContractTemplates() - .then((templates) => { - - // 벤더 국가별 자동 선택 로직 - if (vendors.length > 0) { - const isAllForeign = vendors.every(vendor => vendor.country !== 'KR') - const isAllDomestic = vendors.every(vendor => vendor.country === 'KR') - //외자면 리스트에 비밀유지계약, 기술자료, 내국신용장, 한글 제외 - if(isAllForeign) { - const foreignTemplates = templates.filter(template => { - const name = template.templateName?.toLowerCase() || '' - return !name.includes('비밀유지') && !name.includes('기술자료') && !name.includes('내국신용장') && !name.includes('한글') - }) - setBasicContractTemplates(foreignTemplates) - } - //내자면 리스트에 GTC 제외, 비밀유지, 기술자료, 영문서약 제외 - if(isAllDomestic) { - const domesticTemplates = templates.filter(template => { - const name = template.templateName?.toLowerCase() || '' - return !name.includes('gtc') && !name.includes('비밀유지') && !name.includes('기술자료') && !name.includes('영문') - }) - setBasicContractTemplates(domesticTemplates) - } - if (isAllForeign) { - // 외자: 준법서약 (영문), GTC 선택 (GTC는 1개만 선택하도록) - const foreignTemplates = templates.filter(template => { - const name = template.templateName?.toLowerCase() || '' - return ( - (template.templateName?.includes('준법서약') && template.templateName?.includes('영문')) || - template.templateName?.includes('gtc') - ) - }) - // GTC 템플릿 중 최신 리비전의 것만 선택 - const gtcTemplates = foreignTemplates.filter(t => t.templateName?.includes('gtc')) - const nonGtcTemplates = foreignTemplates.filter(t => !t.templateName?.includes('gtc')) - - if (gtcTemplates.length > 0) { - // GTC 템플릿 중 이름이 가장 긴 것 (프로젝트 GTC 우선) 선택 - const selectedGtcTemplate = gtcTemplates.reduce((prev, current) => - (prev.templateName?.length || 0) > (current.templateName?.length || 0) ? prev : current - ) - setSelectedTemplateIds([...nonGtcTemplates.map(t => t.id), selectedGtcTemplate.id]) - } else { - setSelectedTemplateIds(nonGtcTemplates.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([]) - setNdaAttachments([]) - setIsUploadingNdaFiles(false) - setProgressValue(0) - setCurrentStep("") - setShowProgress(false) - setPqHistory({}) - setIsLoadingHistory(false) - setActiveNonInspectionPQList(null) - setIsLoadingNonInspectionPQ(false) - } - }, [props.open]) - - // PQ 품목 선택 함수 (MaterialGroupSelectorDialogMulti에서 호출됨) - const handlePQItemsChange = (items: MaterialSearchItem[]) => { - setPqItems(items) - } - - // PQ 품목 제거 함수 - const handleRemovePQItem = (materialGroupCode: string) => { - setPqItems(prev => prev.filter(item => item.materialGroupCode !== materialGroupCode)) - } - - // 비밀유지 계약서 첨부파일 추가 함수 - 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 (type === "NON_INSPECTION" && !activeNonInspectionPQList) return toast.error("활성화된 미실사 PQ 리스트가 없습니다.") - if (!dueDate) return toast.error("마감일을 선택하세요.") - if (pqItems.length === 0) return toast.error("PQ 대상 품목을 선택하세요.") - if (!session?.user?.id) return toast.error("인증 실패") - - // GTC 템플릿 선택 검증 - const selectedGtcTemplates = basicContractTemplates.filter(template => - selectedTemplateIds.includes(template.id) && - template.templateName?.toLowerCase().includes('gtc') - ) - - if (selectedGtcTemplates.length > 1) { - return toast.error("GTC 템플릿은 하나만 선택할 수 있습니다.") - } - - // 프로그레스 바를 즉시 표시 - setShowProgress(true) - setProgressValue(0) - setCurrentStep("시작 중...") - - startApproveTransition(async () => { - try { - - // 전체 단계 수 계산 - const gtcTemplates = basicContractTemplates.filter(template => - selectedTemplateIds.includes(template.id) && - template.templateName?.toLowerCase().includes('gtc') - ) - - const totalSteps = 1 + - (selectedTemplateIds.length > 0 ? 1 : 0) + - (isNdaTemplateSelected() && ndaAttachments.length > 0 ? 1 : 0) + - (gtcTemplates.length > 0 ? 1 : 0) - let completedSteps = 0 - - // 1단계: PQ 생성 - setCurrentStep("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.map(item => ({ - materialGroupCode: item.materialGroupCode, - materialGroupDescription: item.materialGroupDescription - }))), - templateId: selectedTemplateIds.length > 0 ? selectedTemplateIds[0] : null, - }) - - if (pqError) { - setShowProgress(false) - toast.error(`PQ 생성 실패: ${pqError}`) - return - } - - completedSteps++ - setProgressValue((completedSteps / totalSteps) * 100) - console.log("✅ PQ 생성 완료") - toast.success("PQ가 성공적으로 요청되었습니다") - - // 2단계: 기본계약서 템플릿이 선택된 경우 백그라운드에서 처리 - if (selectedTemplateIds.length > 0) { - const templates = basicContractTemplates.filter(t => - selectedTemplateIds.includes(t.id) - ) - - setCurrentStep(`기본계약서 생성 중... (${templates.length}개 템플릿)`) - console.log("📋 기본계약서 백그라운드 처리 시작", templates.length, "개 템플릿") - await processBasicContractsInBackground(templates, vendors) - - completedSteps++ - setProgressValue((completedSteps / totalSteps) * 100) - } - - // 3단계: 비밀유지 계약서 첨부파일이 있는 경우 저장 - if (isNdaTemplateSelected() && ndaAttachments.length > 0) { - setCurrentStep(`비밀유지 계약서 첨부파일 저장 중... (${ndaAttachments.length}개 파일)`) - 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}`) - } - - completedSteps++ - setProgressValue((completedSteps / totalSteps) * 100) - } - //4단계: GTC 템플릿 처리 - if (selectedGtcTemplates.length > 0) { - setCurrentStep(`GTC 문서 생성 중... (${selectedGtcTemplates.length}개 템플릿)`) - console.log("📋 GTC 문서 생성 시작", selectedGtcTemplates.length, "개 템플릿") - - try { - await processGtcTemplates(selectedGtcTemplates, vendors) - completedSteps++ - setProgressValue((completedSteps / totalSteps) * 100) - } catch (error) { - console.error("GTC 템플릿 처리 중 오류:", error) - toast.error(`GTC 템플릿 처리 중 오류가 발생했습니다`) - // GTC 처리 실패해도 PQ 생성은 성공으로 간주 - completedSteps++ - setProgressValue((completedSteps / totalSteps) * 100) - } - } - //5단계: 각 협력업체들에게 기본계약서 이메일 발송 - if (selectedTemplateIds.length > 0) { - setCurrentStep(`기본계약서 이메일 발송 중... (${selectedTemplateIds.length}개 템플릿)`) - console.log("📋 기본계약서 이메일 발송 시작", selectedTemplateIds.length, "개 템플릿") - await processBasicContractsEmail(selectedTemplateIds, vendors) - completedSteps++ - setProgressValue((completedSteps / totalSteps) * 100) - } - - setCurrentStep("완료!") - setProgressValue(100) - - // 잠시 완료 상태를 보여준 후 다이얼로그 닫기 - setTimeout(() => { - setShowProgress(false) - props.onOpenChange?.(false) - onSuccess?.() - }, 1000) - - } catch (error) { - console.error('PQ 생성 오류:', error) - setShowProgress(false) - 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++ - - // 진행률 업데이트 (2단계 범위 내에서) - const baseProgress = 33.33 // 1단계 완료 후 - const contractProgress = (processedCount / totalContracts) * 33.33 // 2단계는 33.33% 차지 - const newProgress = baseProgress + contractProgress - setProgressValue(newProgress) - setCurrentStep(`기본계약서 생성 중... (${processedCount}/${totalContracts})`) - - 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 - } - } - - // GTC 템플릿 처리 함수 - const processGtcTemplates = async (gtcTemplates: BasicContractTemplate[], vendors: any[]) => { - if (!session?.user?.id) { - toast.error("인증 정보가 없습니다") - return - } - - try { - const vendorIds = vendors.map(v => v.id) - const userId = Number(session.user.id) - - for (const template of gtcTemplates) { - const templateName = template.templateName?.toLowerCase() || '' - - if (templateName.includes('general gtc') || (templateName.includes('gtc') && !templateName.includes(' '))) { - // General GTC 처리 - console.log(`📄 General GTC 템플릿 처리: ${template.templateName}`) - - const gtcDocument = await getStandardGtcDocumentId() - if (!gtcDocument) { - toast.error(`표준 GTC 문서를 찾을 수 없습니다.`) - continue - } - - const result = await createGtcVendorDocuments({ - baseDocumentId: gtcDocument.id, - vendorIds, - createdById: userId, - documentTitle: gtcDocument.title - }) - - if (result.success) { - console.log(`✅ General GTC 문서 생성 완료: ${result.count}개`) - } else { - toast.error(`General GTC 문서 생성 실패: ${result.error}`) - } - - } else if (templateName.includes('gtc') && templateName.includes(' ')) { - // 프로젝트 GTC 처리 (프로젝트 코드 추출) - const projectCodeMatch = template.templateName?.match(/^([A-Z0-9]+)\s+GTC/) - console.log("🔄 프로젝트 GTC 템플릿 처리: ", template.templateName, projectCodeMatch) - console.log(` - 템플릿 이름 분석: "${template.templateName}"`) - console.log(` - 소문자 변환: "${templateName}"`) - if (projectCodeMatch) { - const projectCode = projectCodeMatch[1] - console.log(`📄 프로젝트 GTC 템플릿 처리: ${template.templateName} (프로젝트: ${projectCode})`) - - // const gtcDocument = await getProjectGtcDocumentId(projectCode) - // if (!gtcDocument) { - // toast.error(`프로젝트 "${projectCode}"의 GTC 문서를 찾을 수 없습니다.`) - // continue - // } - // console.log("🔄 getProjectGtcDocumentId", gtcDocument) - - const result = await createProjectGtcVendorDocuments({ - projectCode, - vendorIds, - createdById: userId, - documentTitle: template.templateName - }) - - if (result.success) { - console.log(`✅ 프로젝트 GTC 문서 생성 완료: ${result.count}개`) - } else { - toast.error(`프로젝트 GTC 문서 생성 실패: ${result.error}`) - } - } else { - toast.error(`프로젝트 GTC 템플릿 이름 형식이 올바르지 않습니다: ${template.templateName}`) - } - } else { - console.log(`⚠️ GTC 템플릿이지만 처리할 수 없는 형식: ${template.templateName}`) - } - } - - toast.success(`GTC 문서가 ${gtcTemplates.length}개 템플릿에 대해 생성되었습니다`) - - } catch (error) { - console.error('GTC 템플릿 처리 중 오류:', error) - toast.error(`GTC 템플릿 처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`) - } - } - - const processBasicContractsEmail = async (templateIds: number[], vendors: any[]) => { - if (!session?.user?.id) { - toast.error("인증 정보가 없습니다") - return - } - try { - const vendorIds = vendors.map(v => v.id) - const userId = Number(session.user.id) - - // 2. 성공한 템플릿이 있으면 이메일 발송 - if (templateIds.length > 0) { - const emailResult = await sendBasicContractEmail({ - vendorIds, - templateIds, - requestedBy: userId - }) - - if (emailResult.success) { - toast.success(`${vendorIds.length}개 협력업체에 이메일이 발송되었습니다`) - } else { - toast.warning(`계약서는 생성되었으나 일부 이메일 발송 실패: ${emailResult.error}`) - } - } else { - toast.error("기본계약서 생성에 실패했습니다") - } - - } catch (error) { - console.error('기본계약서 이메일 발송 중 오류:', error) - toast.error(`기본계약서 이메일 발송 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`) - } -} - - // PQ 히스토리 컴포넌트 - const PQHistorySection = () => { - if (isLoadingHistory) { - return ( -
-
- - PQ 히스토리 로딩 중... -
-
- ) - } - - const hasAnyHistory = Object.values(pqHistory).some(history => history.length > 0) - - if (!hasAnyHistory) { - return ( -
-
- 최근 PQ 요청 내역이 없습니다. -
-
- ) - } - - return ( -
-
-
- 최근 PQ 요청 내역 -
- {vendors.map((vendor) => { - const vendorHistory = pqHistory[vendor.id] || [] - if (vendorHistory.length === 0) return null - - return ( -
-
- {vendor.vendorName} -
-
- {vendorHistory.slice(0, 3).map((pq) => { - const createdDate = new Date(pq.createdAt).toLocaleDateString('ko-KR') - const statusText = - pq.status === 'REQUESTED' ? '요청됨' : - pq.status === 'APPROVED' ? '승인됨' : - pq.status === 'SUBMITTED' ? '제출됨' : - pq.status === 'REJECTED' ? '거절됨' : - pq.status - - return ( -
-
- - - {statusText} - -
-
-
- {pq.type === 'GENERAL' ? '일반' : pq.type === 'PROJECT' ? '프로젝트' : '미실사'} -
-
- {createdDate} -
-
-
- ) - })} - {vendorHistory.length > 3 && ( -
- 외 {vendorHistory.length - 3}건 더 있음 -
- )} -
-
- ) - })} -
-
- ) - } - - - - const dialogContent = ( -
- {/* 선택된 협력업체 정보 */} -
- -
- {vendors.map((vendor) => ( -
-
-
{vendor.vendorName}
-
- {vendor.vendorCode} • {vendor.email || "이메일 없음"} -
-
-
- ))} -
-
- -
- - -
- - {type === "PROJECT" && ( -
- - -
- )} - - {type === "NON_INSPECTION" && ( -
- - {isLoadingNonInspectionPQ ? ( -
로딩 중...
- ) : activeNonInspectionPQList ? ( -
-
- 미실사 PQ - {activeNonInspectionPQList.name} -
-
- 활성화된 미실사 PQ 리스트를 기준으로 요청합니다. -
-
- ) : ( -
-
- 활성화된 미실사 PQ 리스트가 없습니다. 먼저 PQ 관리에서 미실사 PQ 리스트를 생성하고 활성화해주세요. -
-
- )} -
- )} - - {/* 마감일 입력 */} -
- - { - if (date) { - // 한국 시간대로 날짜 변환 (UTC 변환으로 인한 날짜 변경 방지) - const kstDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000) - setDueDate(kstDate.toISOString().slice(0, 10)) - } else { - setDueDate("") - } - }} - placeholder="마감일 선택" - minDate={new Date()} - /> -
- - {/* PQ 대상품목 */} -
- -
- - - {pqItems.length > 0 && ( -
- {pqItems.length}개 자재 그룹이 선택되었습니다. -
- )} -
- - {/* 추가 안내사항 */} -
- -