summaryrefslogtreecommitdiff
path: root/lib/vendor-investigation/service.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-10-29 07:43:44 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-10-29 07:43:44 +0000
commit2eb717eb2bbfd97a5f149d13049aa336c26c393b (patch)
tree274283b7759bfba619e6d143edccf3845ba45ed6 /lib/vendor-investigation/service.ts
parentbfc26491991997b5b109af6ea6bc75a8be138e9a (diff)
(최겸) 구매 실사 개발(진행중)
Diffstat (limited to 'lib/vendor-investigation/service.ts')
-rw-r--r--lib/vendor-investigation/service.ts516
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 : "알 수 없는 오류"
+ };
+ }
+}