diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-29 07:43:44 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-29 07:43:44 +0000 |
| commit | 2eb717eb2bbfd97a5f149d13049aa336c26c393b (patch) | |
| tree | 274283b7759bfba619e6d143edccf3845ba45ed6 /lib/vendor-investigation/service.ts | |
| parent | bfc26491991997b5b109af6ea6bc75a8be138e9a (diff) | |
(최겸) 구매 실사 개발(진행중)
Diffstat (limited to 'lib/vendor-investigation/service.ts')
| -rw-r--r-- | lib/vendor-investigation/service.ts | 516 |
1 files changed, 512 insertions, 4 deletions
diff --git a/lib/vendor-investigation/service.ts b/lib/vendor-investigation/service.ts index 9395a5de..f81f78f6 100644 --- a/lib/vendor-investigation/service.ts +++ b/lib/vendor-investigation/service.ts @@ -1,7 +1,7 @@ "use server"; // Next.js 서버 액션에서 직접 import하려면 (선택) -import { items, vendorInvestigationAttachments, vendorInvestigations, vendorInvestigationsView, vendorPossibleItems, vendors } from "@/db/schema/" -import { GetVendorsInvestigationSchema, updateVendorInvestigationSchema } from "./validations" +import { items, vendorInvestigationAttachments, vendorInvestigations, vendorInvestigationsView, vendorPossibleItems, vendors, siteVisitRequests } from "@/db/schema/" +import { GetVendorsInvestigationSchema, updateVendorInvestigationSchema, updateVendorInvestigationProgressSchema, updateVendorInvestigationResultSchema } from "./validations" import { asc, desc, ilike, inArray, and, or, gte, lte, eq, isNull, count } from "drizzle-orm"; import { revalidateTag, unstable_noStore, revalidatePath } from "next/cache"; import { filterColumns } from "@/lib/filter-columns"; @@ -193,7 +193,213 @@ export async function requestInvestigateVendors({ } -// 개선된 서버 액션 - 텍스트 데이터만 처리 +// 실사 진행 관리 업데이트 액션 (PLANNED -> IN_PROGRESS) +export async function updateVendorInvestigationProgressAction(formData: FormData) { + try { + // 1) 텍스트 필드만 추출 + const textEntries: Record<string, string> = {} + for (const [key, value] of formData.entries()) { + if (typeof value === "string") { + textEntries[key] = value + } + } + + // 2) 적절한 타입으로 변환 + const processedEntries: any = {} + + // 필수 필드 + if (textEntries.investigationId) { + processedEntries.investigationId = Number(textEntries.investigationId) + } + + // 선택적 필드들 + if (textEntries.investigationAddress) { + processedEntries.investigationAddress = textEntries.investigationAddress + } + if (textEntries.investigationMethod) { + processedEntries.investigationMethod = textEntries.investigationMethod + } + + // 선택적 날짜 필드 + if (textEntries.forecastedAt) { + processedEntries.forecastedAt = new Date(textEntries.forecastedAt) + } + if (textEntries.confirmedAt) { + processedEntries.confirmedAt = new Date(textEntries.confirmedAt) + } + + // 3) Zod로 파싱/검증 + const parsed = updateVendorInvestigationProgressSchema.parse(processedEntries) + + // 4) 업데이트 데이터 준비 + const updateData: any = { + updatedAt: new Date(), + } + + // 선택적 필드들은 존재할 때만 추가 + if (parsed.investigationAddress !== undefined) { + updateData.investigationAddress = parsed.investigationAddress + } + if (parsed.investigationMethod !== undefined) { + updateData.investigationMethod = parsed.investigationMethod + } + if (parsed.forecastedAt !== undefined) { + updateData.forecastedAt = parsed.forecastedAt + } + if (parsed.confirmedAt !== undefined) { + updateData.confirmedAt = parsed.confirmedAt + } + + // 실사 방법이 설정되면 PLANNED -> IN_PROGRESS로 상태 변경 + if (parsed.investigationMethod) { + updateData.investigationStatus = "IN_PROGRESS" + } + + // 5) vendor_investigations 테이블 업데이트 + await db + .update(vendorInvestigations) + .set(updateData) + .where(eq(vendorInvestigations.id, parsed.investigationId)) + + // 6) 캐시 무효화 + revalidateTag("vendor-investigations") + revalidatePath("/evcp/vendor-investigation") + + return { success: true } + } catch (error) { + console.error("실사 진행 관리 업데이트 오류:", error) + return { + success: false, + error: error instanceof Error ? error.message : "알 수 없는 오류" + } + } +} + +// 실사 결과 입력 액션 (IN_PROGRESS -> COMPLETED/CANCELED/SUPPLEMENT_REQUIRED) +export async function updateVendorInvestigationResultAction(formData: FormData) { + try { + // 1) 텍스트 필드만 추출 + const textEntries: Record<string, string> = {} + for (const [key, value] of formData.entries()) { + if (typeof value === "string") { + textEntries[key] = value + } + } + + // 2) 적절한 타입으로 변환 + const processedEntries: any = {} + + // 필수 필드 + if (textEntries.investigationId) { + processedEntries.investigationId = Number(textEntries.investigationId) + } + + // 선택적 필드들 + if (textEntries.completedAt) { + processedEntries.completedAt = new Date(textEntries.completedAt) + } + if (textEntries.evaluationScore) { + processedEntries.evaluationScore = Number(textEntries.evaluationScore) + } + if (textEntries.evaluationResult) { + processedEntries.evaluationResult = textEntries.evaluationResult + } + if (textEntries.investigationNotes) { + processedEntries.investigationNotes = textEntries.investigationNotes + } + + // 3) Zod로 파싱/검증 + const parsed = updateVendorInvestigationResultSchema.parse(processedEntries) + + // 4) 업데이트 데이터 준비 + const updateData: any = { + updatedAt: new Date(), + } + + // 선택적 필드들은 존재할 때만 추가 + if (parsed.completedAt !== undefined) { + updateData.completedAt = parsed.completedAt + } + if (parsed.evaluationScore !== undefined) { + updateData.evaluationScore = parsed.evaluationScore + } + if (parsed.evaluationResult !== undefined) { + updateData.evaluationResult = parsed.evaluationResult + } + if (parsed.investigationNotes !== undefined) { + updateData.investigationNotes = parsed.investigationNotes + } + + // 평가 결과에 따라 상태 자동 변경 + if (parsed.evaluationResult) { + if (parsed.evaluationResult === "REJECTED") { + updateData.investigationStatus = "CANCELED" + } else if (parsed.evaluationResult === "SUPPLEMENT" || + parsed.evaluationResult === "SUPPLEMENT_REINSPECT" || + parsed.evaluationResult === "SUPPLEMENT_DOCUMENT") { + updateData.investigationStatus = "SUPPLEMENT_REQUIRED" + } else if (parsed.evaluationResult === "APPROVED") { + updateData.investigationStatus = "COMPLETED" + } + } + + // 5) vendor_investigations 테이블 업데이트 + await db + .update(vendorInvestigations) + .set(updateData) + .where(eq(vendorInvestigations.id, parsed.investigationId)) + /* + 현재 보완 프로세스는 자동으로 처리됨. 만약 dialog 필요하면 아래 서버액션 분기 필요.(1029/최겸) + */ + // 5-1) 보완 프로세스 자동 처리 (TO-BE) + if (parsed.evaluationResult === "SUPPLEMENT_REINSPECT" || parsed.evaluationResult === "SUPPLEMENT_DOCUMENT") { + // 실사 방법 확인 + const investigation = await db + .select({ + investigationMethod: vendorInvestigations.investigationMethod, + }) + .from(vendorInvestigations) + .where(eq(vendorInvestigations.id, parsed.investigationId)) + .then(rows => rows[0]); + + if (investigation?.investigationMethod === "PRODUCT_INSPECTION" || investigation?.investigationMethod === "SITE_VISIT_EVAL") { + if (parsed.evaluationResult === "SUPPLEMENT_REINSPECT") { + // 보완-재실사 요청 자동 생성 + await requestSupplementReinspectionAction({ + investigationId: parsed.investigationId, + siteVisitData: { + inspectionDuration: 1.0, // 기본 1일 + additionalRequests: "보완을 위한 재실사 요청입니다.", + } + }); + } else if (parsed.evaluationResult === "SUPPLEMENT_DOCUMENT") { + // 보완-서류제출 요청 자동 생성 + await requestSupplementDocumentAction({ + investigationId: parsed.investigationId, + documentRequests: { + requiredDocuments: ["보완 서류"], + additionalRequests: "보완을 위한 서류 제출 요청입니다.", + } + }); + } + } + } + + // 6) 캐시 무효화 + revalidateTag("vendor-investigations") + revalidatePath("/evcp/vendor-investigation") + + return { success: true } + } catch (error) { + console.error("실사 결과 업데이트 오류:", error) + return { + success: false, + error: error instanceof Error ? error.message : "알 수 없는 오류" + } + } +} + +// 기존 함수 (호환성을 위해 유지) export async function updateVendorInvestigationAction(formData: FormData) { try { // 1) 텍스트 필드만 추출 @@ -300,12 +506,51 @@ export async function updateVendorInvestigationAction(formData: FormData) { updateData.investigationStatus = "IN_PROGRESS"; } + // 보완 프로세스 분기 로직 (TO-BE) + if (parsed.evaluationResult === "SUPPLEMENT_REINSPECT" || parsed.evaluationResult === "SUPPLEMENT_DOCUMENT") { + updateData.investigationStatus = "SUPPLEMENT_REQUIRED"; + } + // 5) vendor_investigations 테이블 업데이트 await db .update(vendorInvestigations) .set(updateData) .where(eq(vendorInvestigations.id, parsed.investigationId)) + // 5-1) 보완 프로세스 자동 처리 (TO-BE) + if (parsed.evaluationResult === "SUPPLEMENT_REINSPECT" || parsed.evaluationResult === "SUPPLEMENT_DOCUMENT") { + // 실사 방법 확인 + const investigation = await db + .select({ + investigationMethod: vendorInvestigations.investigationMethod, + }) + .from(vendorInvestigations) + .where(eq(vendorInvestigations.id, parsed.investigationId)) + .then(rows => rows[0]); + + if (investigation?.investigationMethod === "PRODUCT_INSPECTION" || investigation?.investigationMethod === "SITE_VISIT_EVAL") { + if (parsed.evaluationResult === "SUPPLEMENT_REINSPECT") { + // 보완-재실사 요청 자동 생성 + await requestSupplementReinspectionAction({ + investigationId: parsed.investigationId, + siteVisitData: { + inspectionDuration: 1.0, // 기본 1일 + additionalRequests: "보완을 위한 재실사 요청입니다.", + } + }); + } else if (parsed.evaluationResult === "SUPPLEMENT_DOCUMENT") { + // 보완-서류제출 요청 자동 생성 + await requestSupplementDocumentAction({ + investigationId: parsed.investigationId, + documentRequests: { + requiredDocuments: ["보완 서류"], + additionalRequests: "보완을 위한 서류 제출 요청입니다.", + } + }); + } + } + } + // 6) 캐시 무효화 revalidateTag("vendor-investigations") revalidateTag("pq-submissions") @@ -693,4 +938,267 @@ export async function createVendorInvestigationAttachmentAction(input: { error: error instanceof Error ? error.message : "알 수 없는 오류", }; } -}
\ No newline at end of file +} + +// 보완-재실사 요청 액션 +export async function requestSupplementReinspectionAction({ + investigationId, + siteVisitData +}: { + investigationId: number; + siteVisitData: { + inspectionDuration?: number; + requestedStartDate?: Date; + requestedEndDate?: Date; + shiAttendees?: any; + vendorRequests?: any; + additionalRequests?: string; + }; +}) { + try { + // 1. 실사 상태를 SUPPLEMENT_REQUIRED로 변경 + await db + .update(vendorInvestigations) + .set({ + investigationStatus: "SUPPLEMENT_REQUIRED", + updatedAt: new Date(), + }) + .where(eq(vendorInvestigations.id, investigationId)); + + // 2. 새로운 방문실사 요청 생성 + const [newSiteVisitRequest] = await db + .insert(siteVisitRequests) + .values({ + investigationId: investigationId, + inspectionDuration: siteVisitData.inspectionDuration, + requestedStartDate: siteVisitData.requestedStartDate, + requestedEndDate: siteVisitData.requestedEndDate, + shiAttendees: siteVisitData.shiAttendees || {}, + vendorRequests: siteVisitData.vendorRequests || {}, + additionalRequests: siteVisitData.additionalRequests, + status: "REQUESTED", + }) + .returning(); + + // 3. 캐시 무효화 + revalidateTag("vendor-investigations"); + revalidateTag("site-visit-requests"); + + return { success: true, siteVisitRequestId: newSiteVisitRequest.id }; + } catch (error) { + console.error("보완-재실사 요청 실패:", error); + return { + success: false, + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } +} + +// 보완-서류제출 요청 액션 +export async function requestSupplementDocumentAction({ + investigationId, + documentRequests +}: { + investigationId: number; + documentRequests: { + requiredDocuments: string[]; + additionalRequests?: string; + }; +}) { + try { + // 1. 실사 상태를 SUPPLEMENT_REQUIRED로 변경 + await db + .update(vendorInvestigations) + .set({ + investigationStatus: "SUPPLEMENT_REQUIRED", + updatedAt: new Date(), + }) + .where(eq(vendorInvestigations.id, investigationId)); + + // 2. 서류제출 요청을 위한 방문실사 요청 생성 (서류제출용) + const [newSiteVisitRequest] = await db + .insert(siteVisitRequests) + .values({ + investigationId: investigationId, + inspectionDuration: 0, // 서류제출은 방문 시간 0 + shiAttendees: {}, // 서류제출은 참석자 없음 + vendorRequests: { + requiredDocuments: documentRequests.requiredDocuments, + documentSubmissionOnly: true, // 서류제출 전용 플래그 + }, + additionalRequests: documentRequests.additionalRequests, + status: "REQUESTED", + }) + .returning(); + + // 3. 캐시 무효화 + revalidateTag("vendor-investigations"); + revalidateTag("site-visit-requests"); + + return { success: true, siteVisitRequestId: newSiteVisitRequest.id }; + } catch (error) { + console.error("보완-서류제출 요청 실패:", error); + return { + success: false, + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } +} + +// 보완 서류 제출 완료 액션 (벤더가 서류 제출 완료) +export async function completeSupplementDocumentAction({ + investigationId, + siteVisitRequestId, + submittedBy +}: { + investigationId: number; + siteVisitRequestId: number; + submittedBy: number; +}) { + try { + // 1. 방문실사 요청 상태를 COMPLETED로 변경 + await db + .update(siteVisitRequests) + .set({ + status: "COMPLETED", + sentAt: new Date(), + updatedAt: new Date(), + }) + .where(eq(siteVisitRequests.id, siteVisitRequestId)); + + // 2. 실사 상태를 IN_PROGRESS로 변경 (재검토 대기) + await db + .update(vendorInvestigations) + .set({ + investigationStatus: "IN_PROGRESS", + updatedAt: new Date(), + }) + .where(eq(vendorInvestigations.id, investigationId)); + + // 3. 캐시 무효화 + revalidateTag("vendor-investigations"); + revalidateTag("site-visit-requests"); + + return { success: true }; + } catch (error) { + console.error("보완 서류 제출 완료 처리 실패:", error); + return { + success: false, + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } +} + +// 보완 재실사 완료 액션 (재실사 완료 후) +export async function completeSupplementReinspectionAction({ + investigationId, + siteVisitRequestId, + evaluationResult, + evaluationScore, + investigationNotes +}: { + investigationId: number; + siteVisitRequestId: number; + evaluationResult: "APPROVED" | "SUPPLEMENT" | "REJECTED"; + evaluationScore?: number; + investigationNotes?: string; +}) { + try { + // 1. 방문실사 요청 상태를 COMPLETED로 변경 + await db + .update(siteVisitRequests) + .set({ + status: "COMPLETED", + sentAt: new Date(), + updatedAt: new Date(), + }) + .where(eq(siteVisitRequests.id, siteVisitRequestId)); + + // 2. 실사 상태 및 평가 결과 업데이트 + const updateData: any = { + investigationStatus: evaluationResult === "APPROVED" ? "COMPLETED" : "SUPPLEMENT_REQUIRED", + evaluationResult: evaluationResult, + updatedAt: new Date(), + }; + + if (evaluationScore !== undefined) { + updateData.evaluationScore = evaluationScore; + } + if (investigationNotes) { + updateData.investigationNotes = investigationNotes; + } + if (evaluationResult === "COMPLETED") { + updateData.completedAt = new Date(); + } + + await db + .update(vendorInvestigations) + .set(updateData) + .where(eq(vendorInvestigations.id, investigationId)); + + // 3. 캐시 무효화 + revalidateTag("vendor-investigations"); + revalidateTag("site-visit-requests"); + + return { success: true }; + } catch (error) { + console.error("보완 재실사 완료 처리 실패:", error); + return { + success: false, + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } +} + +// 보완 서류제출 응답 제출 액션 +export async function submitSupplementDocumentResponseAction({ + investigationId, + responseData +}: { + investigationId: number + responseData: { + responseText: string + attachments: Array<{ + fileName: string + url: string + size?: number + }> + } +}) { + try { + // 1. 실사 상태를 SUPPLEMENT_REQUIRED로 변경 + await db + .update(vendorInvestigations) + .set({ + investigationStatus: "SUPPLEMENT_REQUIRED", + investigationNotes: responseData.responseText, + updatedAt: new Date(), + }) + .where(eq(vendorInvestigations.id, investigationId)); + + // 2. 첨부 파일 저장 + if (responseData.attachments.length > 0) { + const attachmentData = responseData.attachments.map(attachment => ({ + investigationId, + fileName: attachment.fileName, + filePath: attachment.url, + fileSize: attachment.size || 0, + uploadedAt: new Date(), + })); + + await db.insert(vendorInvestigationAttachments).values(attachmentData); + } + + // 3. 캐시 무효화 + revalidateTag("vendor-investigations"); + revalidateTag("vendor-investigation-attachments"); + + return { success: true }; + } catch (error) { + console.error("보완 서류제출 응답 처리 실패:", error); + return { + success: false, + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } +} |
