diff options
Diffstat (limited to 'lib/site-visit/service.ts')
| -rw-r--r-- | lib/site-visit/service.ts | 668 |
1 files changed, 668 insertions, 0 deletions
diff --git a/lib/site-visit/service.ts b/lib/site-visit/service.ts new file mode 100644 index 00000000..3b9bcb91 --- /dev/null +++ b/lib/site-visit/service.ts @@ -0,0 +1,668 @@ +"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<string, boolean>;
+ shiAttendeeDetails?: string;
+ vendorRequests: Record<string, boolean>;
+ otherVendorRequests?: string;
+ additionalRequests?: string;
+ attachments?: Array<File>;
+ }) {
+ 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<string, { checked: boolean; count: number; details?: string }>;
+
+ // 메일 제목
+ 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<string, string> = {
+ 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<string, boolean>)
+ .filter(key => (siteVisitRequest.vendorRequests as Record<string, boolean>)[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<string, unknown> | null,
+ vendorRequests: item.vendorRequests as Record<string, unknown> | 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<File>;
+ }) {
+ 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: "협력업체 방문실사 정보 조회 중 오류가 발생했습니다."
+ };
+ }
+ }
\ No newline at end of file |
