From 969c25b56f6d29d7ffa4bc2ce04c5fb4e5846b34 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 14 Aug 2025 11:54:47 +0000 Subject: (대표님) 정규벤더등록, 벤더문서관리, 벤더데이터입력, 첨부파일관리 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/vendor-regular-registrations/service.ts | 400 ++++++++++++++++++++++++++-- 1 file changed, 377 insertions(+), 23 deletions(-) (limited to 'lib/vendor-regular-registrations/service.ts') diff --git a/lib/vendor-regular-registrations/service.ts b/lib/vendor-regular-registrations/service.ts index b587ec23..51f4e82b 100644 --- a/lib/vendor-regular-registrations/service.ts +++ b/lib/vendor-regular-registrations/service.ts @@ -11,6 +11,7 @@ import { getServerSession } from "next-auth"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import { headers } from "next/headers"; import { sendEmail } from "@/lib/mail/sendEmail"; +import type { RegistrationRequestData } from "@/components/vendor-regular-registrations/registration-request-dialog"; import { vendors, vendorRegularRegistrations, @@ -20,7 +21,8 @@ import { basicContract, vendorPQSubmissions, vendorBusinessContacts, - vendorAdditionalInfo + vendorAdditionalInfo, + basicContractTemplates } from "@/db/schema"; import db from "@/db/db"; import { inArray, eq, desc } from "drizzle-orm"; @@ -437,7 +439,7 @@ export async function skipLegalReview(vendorIds: number[], skipReason: string) { const newRegistration = await createVendorRegularRegistration({ vendorId: vendorId, status: "cp_finished", // CP완료로 변경 - remarks: `법무검토 Skip: ${skipReason}`, + remarks: `GTC Skip: ${skipReason}`, }); registrationId = newRegistration.id; } else { @@ -445,11 +447,12 @@ export async function skipLegalReview(vendorIds: number[], skipReason: string) { registrationId = existingRegistrations[0].id; const currentRemarks = existingRegistrations[0].remarks || ""; const newRemarks = currentRemarks - ? `${currentRemarks}\n법무검토 Skip: ${skipReason}` - : `법무검토 Skip: ${skipReason}`; + ? `${currentRemarks}\nGTC Skip: ${skipReason}` + : `GTC Skip: ${skipReason}`; await updateVendorRegularRegistration(registrationId, { status: "cp_finished", // CP완료로 변경 + gtcSkipped: true, // GTC Skip 여부 설정 remarks: newRemarks, }); } @@ -470,18 +473,19 @@ export async function skipLegalReview(vendorIds: number[], skipReason: string) { return { success: true, - message: `${successCount}개 업체의 법무검토를 Skip 처리했습니다.`, + message: `${successCount}개 업체의 GTC를 Skip 처리했습니다.`, }; } catch (error) { console.error("Error skipping legal review:", error); return { success: false, - error: error instanceof Error ? error.message : "법무검토 Skip 처리 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "GTC Skip 처리 중 오류가 발생했습니다.", }; } } -// 안전적격성평가 Skip 기능 +// 안전적격성평가 Skip 기능 (삭제됨 - 개별 입력으로 대체) +/* export async function skipSafetyQualification(vendorIds: number[], skipReason: string) { try { const session = await getServerSession(authOptions); @@ -562,6 +566,42 @@ export async function skipSafetyQualification(vendorIds: number[], skipReason: s }; } } +*/ + +// 주요품목 업데이트 +export async function updateMajorItems( + registrationId: number, + majorItems: string +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return { success: false, error: "로그인이 필요합니다." }; + } + + const result = await updateVendorRegularRegistration(registrationId, { + majorItems: majorItems, + }); + + if (!result) { + return { success: false, error: "등록 정보를 찾을 수 없습니다." }; + } + + // 캐시 무효화 + revalidateTag("vendor-regular-registrations"); + + return { + success: true, + message: "주요품목이 업데이트되었습니다.", + }; + } catch (error) { + console.error("Error updating major items:", error); + return { + success: false, + error: error instanceof Error ? error.message : "주요품목 업데이트 중 오류가 발생했습니다.", + }; + } +} // 벤더용 현황 조회 함수들 export async function fetchVendorRegistrationStatus(vendorId: number) { @@ -570,7 +610,15 @@ export async function fetchVendorRegistrationStatus(vendorId: number) { try { // 벤더 기본 정보 const vendor = await db - .select() + .select({ + id: vendors.id, + vendorName: vendors.vendorName, + taxId: vendors.taxId, + representativeName: vendors.representativeName, + country: vendors.country, + createdAt: vendors.createdAt, + updatedAt: vendors.updatedAt, + }) .from(vendors) .where(eq(vendors.id, vendorId)) .limit(1) @@ -584,7 +632,23 @@ export async function fetchVendorRegistrationStatus(vendorId: number) { // 정규업체 등록 정보 const registration = await db - .select() + .select({ + id: vendorRegularRegistrations.id, + vendorId: vendorRegularRegistrations.vendorId, + potentialCode: vendorRegularRegistrations.potentialCode, + status: vendorRegularRegistrations.status, + majorItems: vendorRegularRegistrations.majorItems, + registrationRequestDate: vendorRegularRegistrations.registrationRequestDate, + assignedDepartment: vendorRegularRegistrations.assignedDepartment, + assignedDepartmentCode: vendorRegularRegistrations.assignedDepartmentCode, + assignedUser: vendorRegularRegistrations.assignedUser, + assignedUserCode: vendorRegularRegistrations.assignedUserCode, + remarks: vendorRegularRegistrations.remarks, + safetyQualificationContent: vendorRegularRegistrations.safetyQualificationContent, + gtcSkipped: vendorRegularRegistrations.gtcSkipped, + createdAt: vendorRegularRegistrations.createdAt, + updatedAt: vendorRegularRegistrations.updatedAt, + }) .from(vendorRegularRegistrations) .where(eq(vendorRegularRegistrations.vendorId, vendorId)) .limit(1) @@ -613,12 +677,45 @@ export async function fetchVendorRegistrationStatus(vendorId: number) { .orderBy(desc(vendorPQSubmissions.createdAt)) .limit(1) - // 기본계약 정보 - const contractInfo = await db - .select() + // 기본계약 정보 - 템플릿 정보와 함께 조회 + const allVendorContracts = await db + .select({ + templateId: basicContract.templateId, + templateName: basicContractTemplates.templateName, + status: basicContract.status, + createdAt: basicContract.createdAt, + }) .from(basicContract) + .leftJoin(basicContractTemplates, eq(basicContract.templateId, basicContractTemplates.id)) .where(eq(basicContract.vendorId, vendorId)) - .limit(1) + .orderBy(desc(basicContract.createdAt)) + + // 계약 필터링 (기술자료, 비밀유지 제외) + const filteredContracts = allVendorContracts.filter(contract => + contract.templateName && + !contract.templateName.includes("기술자료") && + !contract.templateName.includes("비밀유지") + ) + + // 템플릿 이름별로 가장 최신 계약만 유지 + const vendorContracts = filteredContracts.reduce((acc: typeof filteredContracts, contract) => { + const existing = acc.find((c: typeof contract) => c.templateName === contract.templateName) + if (!existing || (contract.createdAt && existing.createdAt && contract.createdAt > existing.createdAt)) { + return acc.filter((c: typeof contract) => c.templateName !== contract.templateName).concat(contract) + } + return acc + }, [] as typeof filteredContracts) + + console.log(`🏢 Partners 벤더 ID ${vendorId} 기본계약 정보:`, { + allContractsCount: allVendorContracts.length, + filteredContractsCount: filteredContracts.length, + finalContractsCount: vendorContracts.length, + vendorContracts: vendorContracts.map((c: any) => ({ + templateName: c.templateName, + status: c.status, + createdAt: c.createdAt + })) + }) // 업무담당자 정보 const businessContacts = await db @@ -636,15 +733,15 @@ export async function fetchVendorRegistrationStatus(vendorId: number) { // 문서 제출 현황 계산 const documentStatus = { businessRegistration: vendorFiles.some(f => f.attachmentType === "BUSINESS_REGISTRATION"), - creditEvaluation: vendorFiles.some(f => f.attachmentType === "CREDIT_EVALUATION"), - bankCopy: vendorFiles.some(f => f.attachmentType === "BANK_COPY"), + creditEvaluation: vendorFiles.some(f => f.attachmentType === "CREDIT_REPORT"), // CREDIT_EVALUATION -> CREDIT_REPORT + bankCopy: vendorFiles.some(f => f.attachmentType === "BANK_ACCOUNT_COPY"), // BANK_COPY -> BANK_ACCOUNT_COPY auditResult: investigationFiles.length > 0, // DocumentStatusDialog에서 사용하는 키 - cpDocument: !!contractInfo[0]?.status && contractInfo[0].status === "completed", - gtc: !!contractInfo[0]?.status && contractInfo[0].status === "completed", - standardSubcontract: !!contractInfo[0]?.status && contractInfo[0].status === "completed", - safetyHealth: !!contractInfo[0]?.status && contractInfo[0].status === "completed", - ethics: !!contractInfo[0]?.status && contractInfo[0].status === "completed", - domesticCredit: !!contractInfo[0]?.status && contractInfo[0].status === "completed", + cpDocument: vendorContracts.some(c => c.status === "COMPLETED"), + gtc: vendorContracts.some(c => c.templateName?.includes("GTC") && c.status === "COMPLETED"), + standardSubcontract: vendorContracts.some(c => c.templateName?.includes("표준하도급") && c.status === "COMPLETED"), + safetyHealth: vendorContracts.some(c => c.templateName?.includes("안전보건") && c.status === "COMPLETED"), + ethics: vendorContracts.some(c => c.templateName?.includes("윤리") && c.status === "COMPLETED"), + domesticCredit: vendorContracts.some(c => c.templateName?.includes("신용") && c.status === "COMPLETED"), safetyQualification: investigationFiles.length > 0, } @@ -656,6 +753,26 @@ export async function fetchVendorRegistrationStatus(vendorId: number) { const requiredContactTypes = ["sales", "design", "delivery", "quality", "tax_invoice"] const existingContactTypes = businessContacts.map(contact => contact.contactType) const missingContactTypes = requiredContactTypes.filter(type => !existingContactTypes.includes(type)) + + // 추가정보 완료 여부 (업무담당자 + 추가정보 테이블 모두 필요) + const contactsCompleted = missingContactTypes.length === 0 + const additionalInfoTableCompleted = !!additionalInfo[0] + const additionalInfoCompleted = contactsCompleted && additionalInfoTableCompleted + + console.log(`🔍 Partners 벤더 ID ${vendorId} 전체 데이터:`, { + vendor: vendor[0], + registration: registration[0], + safetyQualificationContent: registration[0]?.safetyQualificationContent, + gtcSkipped: registration[0]?.gtcSkipped, + requiredContactTypes, + existingContactTypes, + missingContactTypes, + contactsCompleted, + additionalInfoTableCompleted, + additionalInfoData: additionalInfo[0], + finalAdditionalInfoCompleted: additionalInfoCompleted, + basicContractsCount: vendorContracts.length + }) return { success: true, @@ -666,10 +783,10 @@ export async function fetchVendorRegistrationStatus(vendorId: number) { missingDocuments, businessContacts, missingContactTypes, - additionalInfo: additionalInfo[0] || null, + additionalInfo: additionalInfoCompleted, // boolean 값으로 변경 pqSubmission: pqSubmission[0] || null, auditPassed: investigationFiles.length > 0, - contractInfo: contractInfo[0] || null, + basicContracts: vendorContracts, // 기본계약 정보 추가 incompleteItemsCount: { documents: missingDocuments.length, contacts: missingContactTypes.length, @@ -823,3 +940,240 @@ export async function saveVendorAdditionalInfo( } } } + +// 안전적격성 평가 업데이트 +export async function updateSafetyQualification( + registrationId: number, + safetyQualificationContent: string +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return { success: false, error: "로그인이 필요합니다." }; + } + + const result = await updateVendorRegularRegistration(registrationId, { + safetyQualificationContent: safetyQualificationContent.trim(), + }); + + if (!result) { + return { success: false, error: "등록 정보를 찾을 수 없습니다." }; + } + + // 캐시 무효화 + revalidateTag("vendor-regular-registrations"); + + return { + success: true, + message: "안전적격성 평가가 등록되었습니다.", + }; + } catch (error) { + console.error("Error updating safety qualification:", error); + return { + success: false, + error: error instanceof Error ? error.message : "안전적격성 평가 등록 중 오류가 발생했습니다.", + }; + } +} + +// GTC Skip 처리 +export async function updateGtcSkip( + registrationId: number, + skipReason: string +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return { success: false, error: "로그인이 필요합니다." }; + } + + // 현재 비고 가져오기 + const existingRegistration = await getVendorRegularRegistrationById(registrationId); + if (!existingRegistration) { + return { success: false, error: "등록 정보를 찾을 수 없습니다." }; + } + + const currentRemarks = existingRegistration.remarks || ""; + const newRemarks = currentRemarks + ? `${currentRemarks}\nGTC Skip: ${skipReason}` + : `GTC Skip: ${skipReason}`; + + const result = await updateVendorRegularRegistration(registrationId, { + gtcSkipped: true, + remarks: newRemarks, + }); + + if (!result) { + return { success: false, error: "등록 정보를 찾을 수 없습니다." }; + } + + // 캐시 무효화 + revalidateTag("vendor-regular-registrations"); + + return { + success: true, + message: "GTC Skip이 처리되었습니다.", + }; + } catch (error) { + console.error("Error updating GTC skip:", error); + return { + success: false, + error: error instanceof Error ? error.message : "GTC Skip 처리 중 오류가 발생했습니다.", + }; + } +} + +// 정규업체 등록 요청을 위한 상세 데이터 조회 +export async function fetchRegistrationRequestData(registrationId: number) { + try { + // 등록 정보 조회 + const registration = await db + .select() + .from(vendorRegularRegistrations) + .where(eq(vendorRegularRegistrations.id, registrationId)) + .limit(1); + + if (!registration[0]) { + return { success: false, error: "등록 정보를 찾을 수 없습니다." }; + } + + // 벤더 정보 조회 + const vendor = await db + .select({ + id: vendors.id, + vendorName: vendors.vendorName, + taxId: vendors.taxId, + representativeName: vendors.representativeName, + representativeBirth: vendors.representativeBirth, + representativeEmail: vendors.representativeEmail, + representativePhone: vendors.representativePhone, + representativeWorkExpirence: vendors.representativeWorkExpirence, + country: vendors.country, + corporateRegistrationNumber: vendors.corporateRegistrationNumber, + address: vendors.address, + phone: vendors.phone, + email: vendors.email, + createdAt: vendors.createdAt, + updatedAt: vendors.updatedAt, + }) + .from(vendors) + .where(eq(vendors.id, registration[0].vendorId)) + .limit(1); + + if (!vendor[0]) { + return { success: false, error: "벤더 정보를 찾을 수 없습니다." }; + } + + // 업무담당자 정보 조회 + const businessContacts = await db + .select() + .from(vendorBusinessContacts) + .where(eq(vendorBusinessContacts.vendorId, vendor[0].id)); + + // 추가정보 조회 + const additionalInfo = await db + .select() + .from(vendorAdditionalInfo) + .where(eq(vendorAdditionalInfo.vendorId, vendor[0].id)) + .limit(1); + + return { + success: true, + data: { + registration: registration[0], + vendor: vendor[0], + businessContacts, + additionalInfo: additionalInfo[0] || null, + } + }; + + } catch (error) { + console.error("정규업체 등록 요청 데이터 조회 오류:", error); + return { + success: false, + error: error instanceof Error ? error.message : "데이터 조회 중 오류가 발생했습니다." + }; + } +} + +// 정규업체 등록 요청 서버 액션 +export async function submitRegistrationRequest( + registrationId: number, + requestData: RegistrationRequestData +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return { success: false, error: "인증이 필요합니다." }; + } + + // 현재 등록 정보 조회 + const registration = await db + .select() + .from(vendorRegularRegistrations) + .where(eq(vendorRegularRegistrations.id, registrationId)) + .limit(1); + + if (!registration[0]) { + return { success: false, error: "등록 정보를 찾을 수 없습니다." }; + } + + // 조건충족 상태인지 확인 + if (registration[0].status !== "approval_ready") { + return { success: false, error: "조건충족 상태가 아닙니다." }; + } + + // 정규업체 등록 요청 데이터를 JSON으로 저장 + const registrationRequestData = { + requestDate: new Date(), + requestedBy: session.user.id, + requestedByName: session.user.name, + requestData: requestData, + status: "requested" // 요청됨 + }; + + // 상태를 '등록요청됨'으로 변경하고 요청 데이터 저장 + await db + .update(vendorRegularRegistrations) + .set({ + status: "registration_requested", + remarks: `정규업체 등록 요청됨 - ${new Date().toISOString()}\n요청자: ${session.user.name}`, + updatedAt: new Date(), + }) + .where(eq(vendorRegularRegistrations.id, registrationId)); + + // TODO: MDG 인터페이스 연동 + // await sendToMDG(registrationRequestData); + + // TODO: Knox 결재 연동 + // - 사업자등록증, 신용평가보고서, 개인정보동의서, 통장사본 + // - 실사결과 보고서 + // - CP문서, GTC문서, 비밀유지계약서 + // await initiateKnoxApproval(registrationRequestData); + + console.log("✅ 정규업체 등록 요청 데이터:", { + registrationId, + companyName: requestData.companyNameKor, + businessNumber: requestData.businessNumber, + representative: requestData.representativeNameKor, + requestedBy: session.user.name, + requestDate: new Date().toISOString() + }); + + // 캐시 무효화 + revalidateTag("vendor-regular-registrations"); + revalidateTag(`vendor-regular-registration-${registrationId}`); + + return { + success: true, + message: "정규업체 등록 요청이 성공적으로 제출되었습니다.\nKnox 결재 시스템과 MDG 인터페이스 연동은 추후 구현 예정입니다." + }; + + } catch (error) { + console.error("정규업체 등록 요청 오류:", error); + return { + success: false, + error: error instanceof Error ? error.message : "정규업체 등록 요청 중 오류가 발생했습니다." + }; + } +} \ No newline at end of file -- cgit v1.2.3