"use server" import db from "@/db/db" import { and, eq, isNull, desc, sql} from "drizzle-orm"; import { revalidatePath} from "next/cache"; import { format } from "date-fns" import { vendorInvestigations, vendorPQSubmissions, siteVisitRequests, vendorSiteVisitInfo, siteVisitRequestAttachments } from "@/db/schema/pq" import { sendEmail } from "../mail/sendEmail"; import { decryptWithServerAction } from '@/components/drm/drmUtils' import { vendors } from "@/db/schema/vendors"; import { saveFile, saveDRMFile } from "@/lib/file-stroage"; import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" import { users } from "@/db/schema" // 방문실사 요청 서버 액션 export async function createSiteVisitRequestAction(input: { investigationId: number; inspectionDuration: number; requestedStartDate: Date; requestedEndDate: Date; shiAttendees: Record; shiAttendeeDetails?: string; vendorRequests: Record; otherVendorRequests?: string; additionalRequests?: string; attachments?: Array; }) { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { throw new Error("Unauthorized"); } const investigationId = Number(input.investigationId) // 기존 방문실사 요청이 있는지 확인 const existingRequest = await db .select() .from(siteVisitRequests) .where(eq(siteVisitRequests.investigationId, investigationId)) .limit(1); if (existingRequest.length > 0) { return { success: false, error: "이미 방문실사 요청이 존재합니다. 추가 요청은 불가능합니다." }; } // 방문실사 요청 생성 const [siteVisitRequest] = await db .insert(siteVisitRequests) .values({ investigationId: investigationId, requesterId: session.user.id, inspectionDuration: input.inspectionDuration, requestedStartDate: input.requestedStartDate, requestedEndDate: input.requestedEndDate, shiAttendees: input.shiAttendees, vendorRequests: input.vendorRequests, additionalRequests: input.additionalRequests, status: "REQUESTED", }) .returning(); // SHI 첨부파일 처리 if (input.attachments && input.attachments.length > 0) { console.log(`📎 첨부파일 처리 시작: ${input.attachments.length}개 파일`); const attachmentValues = []; for (const file of input.attachments) { try { console.log(`📁 파일 처리 중: ${file.name} (${file.size} bytes)`); // saveDRMFile을 사용하여 파일 저장 const saveResult = await saveDRMFile( file, decryptWithServerAction, `site-visit-requests/${siteVisitRequest.id}`, session.user.id.toString() ); if (!saveResult.success) { console.error(`❌ 파일 저장 실패: ${file.name}`, saveResult.error); throw new Error(`파일 저장 실패: ${file.name} - ${saveResult.error}`); } console.log(`✅ 파일 저장 완료: ${file.name} -> ${saveResult.fileName}`); // DB에 첨부파일 레코드 생성 const attachmentValue = { siteVisitRequestId: siteVisitRequest.id, vendorSiteVisitInfoId: null, // SHI 첨부파일은 vendorSiteVisitInfoId가 null fileName: saveResult.fileName!, originalFileName: file.name, filePath: saveResult.publicPath!, fileSize: file.size, mimeType: file.type || 'application/octet-stream', createdAt: new Date(), updatedAt: new Date(), }; attachmentValues.push(attachmentValue); } catch (error) { console.error(`❌ 첨부파일 처리 오류: ${file.name}`, error); throw new Error(`첨부파일 처리 중 오류가 발생했습니다: ${file.name}`); } } if (attachmentValues.length > 0) { await db.insert(siteVisitRequestAttachments).values(attachmentValues); console.log(`✅ 첨부파일 DB 저장 완료: ${attachmentValues.length}개`); } } // 이메일 발송 try { // 실사, 협력업체, 발송자 정보 조회 const investigationResult = await db .select() .from(vendorInvestigations) .where(eq(vendorInvestigations.id, siteVisitRequest.investigationId)) .limit(1); const investigation = investigationResult[0]; if (!investigation) { throw new Error('실사 정보를 찾을 수 없습니다.'); } const vendorResult = await db .select() .from(vendors) .where(eq(vendors.id, investigation.vendorId)) .limit(1); const vendor = vendorResult[0]; if (!vendor) { throw new Error('협력업체 정보를 찾을 수 없습니다.'); } const senderResult = await db .select() .from(users) .where(eq(users.id, siteVisitRequest.requesterId)) .limit(1); const sender = senderResult[0]; if (!sender) { throw new Error('발송자 정보를 찾을 수 없습니다.'); } // 평가 유형 라벨 및 설명 const getEvaluationTypeInfo = (type: string) => { switch (type) { case 'PRODUCT_INSPECTION': return { label: '제품검사평가', description: '제품의 품질, 성능, 안전성 등을 직접 검사하는 평가' }; case 'SITE_VISIT_EVAL': return { label: '방문실사평가', description: '공장 시설, 생산능력, 품질관리체계 등을 현장에서 점검하는 평가' }; default: return { label: type, description: '' }; } }; const evaluationTypeInfo = getEvaluationTypeInfo(investigation.evaluationType || ''); // 마감일 계산 (발송일 + 7일) const deadlineDate = format(new Date(), 'yyyy.MM.dd'); // SHI 참석자 정보 파싱 (새로운 구조에 맞게) const shiAttendees = input.shiAttendees as Record; // 메일 제목 const subject = `[SHI Audit] 방문실사 시행 안내 및 실사 관련 추가정보 요청 _ ${vendor.vendorName} (${vendor.vendorCode}, 사업자번호: ${vendor.taxId})`; // 메일 컨텍스트 const context = { // 기본 정보 vendorName: vendor.vendorName, vendorContactName: vendor.vendorName || '', requesterName: sender.name, requesterTitle: 'Procurement Manager', requesterEmail: sender.email, // 실사 정보 evaluationType: evaluationTypeInfo.label, evaluationTypeDescription: evaluationTypeInfo.description, requestedStartDate: format(siteVisitRequest.requestedStartDate!, 'yyyy.MM.dd'), requestedEndDate: format(siteVisitRequest.requestedEndDate!, 'yyyy.MM.dd'), inspectionDuration: siteVisitRequest.inspectionDuration, // 마감일 deadlineDate, // SHI 참석자 정보 (새로운 구조) shiAttendees: Object.entries(shiAttendees) .filter(([, value]) => value.checked) .map(([key, value]) => { const departmentLabels: Record = { technicalSales: "기술영업", design: "설계", procurement: "구매", quality: "품질", production: "생산", commissioning: "시운전", other: "기타" }; const departmentName = departmentLabels[key] || key; const details = value.details ? ` (${value.details})` : ''; return `${departmentName} ${value.count}명${details}`; }), shiAttendeeDetails: input.shiAttendeeDetails || null, // 협력업체 요청 정보 vendorRequests: Object.keys(siteVisitRequest.vendorRequests as Record) .filter(key => (siteVisitRequest.vendorRequests as Record)[key]) .map(key => { const requestLabels = { 'factoryName': '○ 실사공장명', 'factoryLocation': '○ 실사공장 주소', 'factoryDirections': '○ 실사공장 가는 방법', 'factoryPicName': '○ 실사공장 Contact Point', 'factoryPicPhone': '○ 실사공장 연락처', 'factoryPicEmail': '○ 실사공장 이메일', 'attendees': '○ 실사 참석 예정인력', 'accessProcedure': '○ 공장 출입절차 및 준비물' }; return requestLabels[key as keyof typeof requestLabels] || key; }), otherVendorRequests: input.otherVendorRequests, // 추가 요청사항 additionalRequests: siteVisitRequest.additionalRequests, // 포털 URL portalUrl: `${process.env.NEXTAUTH_URL}/ko/partners/site-visit`, // 현재 연도 currentYear: new Date().getFullYear() }; // 메일 발송 (벤더 이메일로 직접 발송) await sendEmail({ to: vendor.email, subject, template: 'site-visit-request' as string, context, cc: vendor.email !== sender.email ? sender.email : undefined }); console.log('방문실사 요청 메일 발송 완료:', { to: vendor.email, subject, vendorName: vendor.vendorName }); // 메일 발송 성공 시 상태 업데이트 await db .update(siteVisitRequests) .set({ status: "SENT", sentAt: new Date() }) .where(eq(siteVisitRequests.id, siteVisitRequest.id)); } catch (emailError) { console.error('방문실사 요청 메일 발송 실패:', emailError); } revalidatePath("/evcp/pq_new"); return { success: true, data: siteVisitRequest, message: "방문실사 요청이 성공적으로 생성되었습니다." }; } catch (error) { console.error("방문실사 요청 생성 오류:", error); return { success: false, error: "방문실사 요청 생성 중 오류가 발생했습니다." }; } } // 방문실사 요청 조회 서버 액션 export async function getSiteVisitRequestAction(investigationId: number) { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { throw new Error("Unauthorized"); } const siteVisitRequest = await db .select() .from(siteVisitRequests) .where(eq(siteVisitRequests.investigationId, investigationId)) .limit(1); if (!siteVisitRequest[0]) { return { success: true, data: null }; } // SHI 첨부파일 조회 (vendorSiteVisitInfoId가 null인 것들) const shiAttachments = await db .select() .from(siteVisitRequestAttachments) .where( and( eq(siteVisitRequestAttachments.siteVisitRequestId, siteVisitRequest[0].id), isNull(siteVisitRequestAttachments.vendorSiteVisitInfoId) ) ); return { success: true, data: { ...siteVisitRequest[0], shiAttachments } }; } catch (error) { console.error("방문실사 요청 조회 오류:", error); return { success: false, error: "방문실사 요청 조회 중 오류가 발생했습니다." }; } } // 협력업체용 방문실사 요청 조회 export async function getSiteVisitRequestsByVendorId(vendorId: number) { try { const result = await db .select({ id: siteVisitRequests.id, investigationId: siteVisitRequests.investigationId, requesterId: siteVisitRequests.requesterId, inspectionDuration: siteVisitRequests.inspectionDuration, requestedStartDate: siteVisitRequests.requestedStartDate, requestedEndDate: siteVisitRequests.requestedEndDate, shiAttendees: siteVisitRequests.shiAttendees, vendorRequests: siteVisitRequests.vendorRequests, additionalRequests: siteVisitRequests.additionalRequests, status: siteVisitRequests.status, sentAt: siteVisitRequests.sentAt, createdAt: siteVisitRequests.createdAt, updatedAt: siteVisitRequests.updatedAt, // 실사 정보 evaluationType: vendorInvestigations.evaluationType, investigationMethod: vendorInvestigations.investigationMethod, investigationAddress: vendorInvestigations.investigationAddress, investigationNotes: vendorInvestigations.investigationNotes, forecastedAt: vendorInvestigations.forecastedAt, actualAt: vendorInvestigations.completedAt, result: vendorInvestigations.evaluationResult, resultNotes: vendorInvestigations.purchaseComment, // PQ 정보 pqItems: vendorPQSubmissions.pqItems, // 협력업체 정보 vendorName: vendors.vendorName, vendorCode: vendors.vendorCode, vendorEmail: vendors.email, }) .from(siteVisitRequests) .leftJoin( vendorInvestigations, eq(siteVisitRequests.investigationId, vendorInvestigations.id) ) .leftJoin( sql`users AS requester`, eq(siteVisitRequests.requesterId, sql`requester.id`) ) .leftJoin( vendors, eq(vendorInvestigations.vendorId, vendors.id) ) .leftJoin( vendorPQSubmissions, eq(vendorInvestigations.pqSubmissionId, vendorPQSubmissions.id) ) .where(eq(vendorInvestigations.vendorId, vendorId)) .orderBy(desc(siteVisitRequests.createdAt)); // 각 방문실사 요청에 대해 협력업체 정보 조회 const resultWithVendorInfo = await Promise.all( result.map(async (item) => { const vendorInfoResult = await db .select({ id: vendorSiteVisitInfo.id, siteVisitRequestId: vendorSiteVisitInfo.siteVisitRequestId, factoryName: vendorSiteVisitInfo.factoryName, factoryLocation: vendorSiteVisitInfo.factoryLocation, factoryAddress: vendorSiteVisitInfo.factoryAddress, factoryPicName: vendorSiteVisitInfo.factoryPicName, factoryPicPhone: vendorSiteVisitInfo.factoryPicPhone, factoryPicEmail: vendorSiteVisitInfo.factoryPicEmail, factoryDirections: vendorSiteVisitInfo.factoryDirections, accessProcedure: vendorSiteVisitInfo.accessProcedure, hasAttachments: vendorSiteVisitInfo.hasAttachments, otherInfo: vendorSiteVisitInfo.otherInfo, submittedAt: vendorSiteVisitInfo.submittedAt, submittedBy: vendorSiteVisitInfo.submittedBy, createdAt: vendorSiteVisitInfo.createdAt, updatedAt: vendorSiteVisitInfo.updatedAt, }) .from(vendorSiteVisitInfo) .where(eq(vendorSiteVisitInfo.siteVisitRequestId, item.id)) .limit(1); const vendorInfo = vendorInfoResult.length > 0 ? vendorInfoResult[0] : null; // SHI 첨부파일 조회 (vendorSiteVisitInfoId가 null인 것들) const shiAttachments = await db .select() .from(siteVisitRequestAttachments) .where( and( eq(siteVisitRequestAttachments.siteVisitRequestId, item.id), isNull(siteVisitRequestAttachments.vendorSiteVisitInfoId) ) ); return { ...item, shiAttendees: item.shiAttendees as Record | null, vendorRequests: item.vendorRequests as Record | null, vendorInfo, shiAttachments, }; }) ); console.log(`📊 방문실사 요청 조회 완료 - 총 ${resultWithVendorInfo.length}개 요청`) console.log(`🔍 실제실사일/실사결과 데이터 확인:`, resultWithVendorInfo.map(item => ({ id: item.id, actualAt: item.actualAt, result: item.result, investigationId: item.investigationId }))) return resultWithVendorInfo; } catch (error) { console.error("방문실사 요청 조회 오류:", error); return []; } } // 협력업체 정보 제출 서버 액션 export async function submitVendorInfoAction(input: { siteVisitRequestId: number; factoryName: string; factoryLocation: string; factoryAddress: string; factoryPicName: string; factoryPicPhone: string; factoryPicEmail: string; factoryDirections: string; accessProcedure: string; hasAttachments: boolean; otherInfo?: string; attachments?: Array; }) { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { throw new Error("Unauthorized"); } // 기존 협력업체 정보가 있는지 확인 const existingInfo = await db .select() .from(vendorSiteVisitInfo) .where(eq(vendorSiteVisitInfo.siteVisitRequestId, input.siteVisitRequestId)) .limit(1); if (existingInfo.length > 0) { // 기존 정보 업데이트 await db .update(vendorSiteVisitInfo) .set({ factoryName: input.factoryName, factoryLocation: input.factoryLocation, factoryAddress: input.factoryAddress, factoryPicName: input.factoryPicName, factoryPicPhone: input.factoryPicPhone, factoryPicEmail: input.factoryPicEmail, factoryDirections: input.factoryDirections, accessProcedure: input.accessProcedure, hasAttachments: input.hasAttachments, otherInfo: input.otherInfo, // submittedBy: session.user.id, submittedAt: new Date(), }) .where(eq(vendorSiteVisitInfo.siteVisitRequestId, input.siteVisitRequestId)); } else { // 새로운 정보 삽입 await db .insert(vendorSiteVisitInfo) .values({ siteVisitRequestId: input.siteVisitRequestId, factoryName: input.factoryName, factoryLocation: input.factoryLocation, factoryAddress: input.factoryAddress, factoryPicName: input.factoryPicName, factoryPicPhone: input.factoryPicPhone, factoryPicEmail: input.factoryPicEmail, factoryDirections: input.factoryDirections, accessProcedure: input.accessProcedure, hasAttachments: input.hasAttachments, otherInfo: input.otherInfo, submittedBy: session.user.id, }); } // 첨부파일 처리 if (input.attachments && input.attachments.length > 0) { console.log(`📎 협력업체 첨부파일 처리 시작: ${input.attachments.length}개 파일`); // 기존 첨부파일 삭제 (업데이트 시) if (existingInfo.length > 0) { console.log(`🗑️ 기존 첨부파일 삭제: vendorSiteVisitInfoId ${existingInfo[0].id}`); await db .delete(siteVisitRequestAttachments) .where(eq(siteVisitRequestAttachments.vendorSiteVisitInfoId, existingInfo[0].id)); } const attachmentValues = []; for (const file of input.attachments) { try { console.log(`📁 협력업체 파일 처리 중: ${file.name} (${file.size} bytes)`); // saveFile을 사용하여 파일 저장 (협력업체 첨부파일은 일반 파일로 처리) const saveResult = await saveFile({ file, directory: `site-visit-vendor-info/${input.siteVisitRequestId}`, originalName: file.name, userId: session.user.id.toString() }); if (!saveResult.success) { console.error(`❌ 협력업체 파일 저장 실패: ${file.name}`, saveResult.error); throw new Error(`파일 저장 실패: ${file.name} - ${saveResult.error}`); } console.log(`✅ 협력업체 파일 저장 완료: ${file.name} -> ${saveResult.fileName}`); // DB에 첨부파일 레코드 생성 const attachmentValue = { siteVisitRequestId: input.siteVisitRequestId, vendorSiteVisitInfoId: existingInfo.length > 0 ? existingInfo[0].id : undefined, fileName: saveResult.fileName!, originalFileName: file.name, filePath: saveResult.publicPath!, fileSize: file.size, mimeType: file.type || 'application/octet-stream', createdAt: new Date(), updatedAt: new Date(), }; attachmentValues.push(attachmentValue); } catch (error) { console.error(`❌ 협력업체 첨부파일 처리 오류: ${file.name}`, error); throw new Error(`첨부파일 처리 중 오류가 발생했습니다: ${file.name}`); } } if (attachmentValues.length > 0) { await db.insert(siteVisitRequestAttachments).values(attachmentValues); console.log(`✅ 협력업체 첨부파일 DB 저장 완료: ${attachmentValues.length}개`); } } // 방문실사 요청 상태 업데이트 await db .update(siteVisitRequests) .set({ status: "VENDOR_SUBMITTED" }) .where(eq(siteVisitRequests.id, input.siteVisitRequestId)); revalidatePath("/partners/site-visit"); return { success: true, message: "협력업체 정보가 성공적으로 제출되었습니다." }; } catch (error) { console.error("협력업체 정보 제출 오류:", error); return { success: false, error: "협력업체 정보 제출 중 오류가 발생했습니다." }; } } // SHI eVCP에서 협력업체 방문실사 정보 조회 export async function getVendorSiteVisitInfoAction(siteVisitRequestId: number) { try { // 새로운 테이블에서 협력업체 정보 조회 const vendorInfoResult = await db .select() .from(vendorSiteVisitInfo) .where(eq(vendorSiteVisitInfo.siteVisitRequestId, siteVisitRequestId)) .limit(1); const vendorInfo = vendorInfoResult.length > 0 ? vendorInfoResult[0] : null; if (!vendorInfo) { return { success: false, error: "해당 방문실사 요청에 대한 협력업체 정보가 없습니다." }; } // 첨부파일 조회 const attachments = await db .select() .from(siteVisitRequestAttachments) .where(eq(siteVisitRequestAttachments.vendorSiteVisitInfoId, vendorInfo.id)); return { success: true, data: { vendorInfo, attachments } }; } catch (error) { console.error("협력업체 방문실사 정보 조회 오류:", error); return { success: false, error: "협력업체 방문실사 정보 조회 중 오류가 발생했습니다." }; } }