diff options
Diffstat (limited to 'lib/evaluation-target-list/service.ts')
| -rw-r--r-- | lib/evaluation-target-list/service.ts | 195 |
1 files changed, 191 insertions, 4 deletions
diff --git a/lib/evaluation-target-list/service.ts b/lib/evaluation-target-list/service.ts index 9e21dc51..6de00329 100644 --- a/lib/evaluation-target-list/service.ts +++ b/lib/evaluation-target-list/service.ts @@ -1,6 +1,6 @@ 'use server' -import { and, or, desc, asc, ilike, eq, isNull, sql, count, inArray } from "drizzle-orm"; +import { and, or, desc, asc, ilike, eq, isNull, sql, count, inArray, gte, lte } from "drizzle-orm"; import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"; import { filterColumns } from "@/lib/filter-columns"; @@ -22,7 +22,9 @@ import { reviewerEvaluations, evaluationSubmissions, generalEvaluations, - esgEvaluationItems + esgEvaluationItems, + contracts, + projects } from "@/db/schema"; @@ -33,6 +35,7 @@ import { authOptions } from "@/app/api/auth/[...nextauth]/route" import { sendEmail } from "../mail/sendEmail"; import type { SQL } from "drizzle-orm" import { DEPARTMENT_CODE_LABELS } from "@/types/evaluation"; +import { revalidatePath } from "next/cache"; export async function selectEvaluationTargetsFromView( tx: PgTransaction<any, any, any>, @@ -214,8 +217,8 @@ export async function getEvaluationTargetsStats(evaluationYear: number) { consensusTrue: sql<number>`sum(case when consensus_status = true then 1 else 0 end)`, consensusFalse: sql<number>`sum(case when consensus_status = false then 1 else 0 end)`, consensusNull: sql<number>`sum(case when consensus_status is null then 1 else 0 end)`, - oceanDivision: sql<number>`sum(case when division = 'OCEAN' then 1 else 0 end)`, - shipyardDivision: sql<number>`sum(case when division = 'SHIPYARD' then 1 else 0 end)`, + oceanDivision: sql<number>`sum(case when division = 'PLANT' then 1 else 0 end)`, + shipyardDivision: sql<number>`sum(case when division = 'SHIP' then 1 else 0 end)`, }) .from(evaluationTargetsWithDepartments) .where(eq(evaluationTargetsWithDepartments.evaluationYear, evaluationYear)); @@ -1165,4 +1168,188 @@ export async function requestEvaluationReview(targetIds: number[], message?: str error: error instanceof Error ? error.message : "의견 요청 중 오류가 발생했습니다." } } +} + + + +interface AutoGenerateResult { + success: boolean + message: string + error?: string + generatedCount?: number + skippedCount?: number + details?: { + shipTargets: number + plantTargets: number + duplicateSkipped: number + } +} + +/** + * 자동으로 평가 대상을 생성하는 서버 액션 + * 전년도 10월부터 현재년도 9월까지의 계약을 기준으로 평가 대상을 생성 + */ +export async function autoGenerateEvaluationTargets( + evaluationYear: number, + adminUserId: number +): Promise<AutoGenerateResult> { + try { + // 평가 기간 계산 (전년도 10월 ~ 현재년도 9월) + const startDate = `${evaluationYear - 1}-10-01` + const endDate = `${evaluationYear}-09-30` + + console.log(`Generating evaluation targets for period: ${startDate} to ${endDate}`) + + // 1. 해당 기간의 계약들과 관련 정보를 조회 + const contractsWithDetails = await db + .select({ + contractId: contracts.id, + vendorId: contracts.vendorId, + projectId: contracts.projectId, + startDate: contracts.startDate, + // vendor 정보 + vendorCode: vendors.vendorCode, + vendorName: vendors.vendorName, + vendorType: vendors.country ==="KR"? "DOMESTIC":"FOREIGN", // DOMESTIC | FOREIGN + // project 정보 + projectType: projects.type, // ship | plant + }) + .from(contracts) + .innerJoin(vendors, eq(contracts.vendorId, vendors.id)) + .innerJoin(projects, eq(contracts.projectId, projects.id)) + .where( + and( + gte(contracts.startDate, startDate), + lte(contracts.startDate, endDate) + ) + ) + + if (contractsWithDetails.length === 0) { + return { + success: true, + message: "해당 기간에 생성할 평가 대상이 없습니다.", + generatedCount: 0, + skippedCount: 0 + } + } + + console.log(`Found ${contractsWithDetails.length} contracts in the period`) + + // 2. 벤더별, 구분별로 그룹화하여 중복 제거 + const targetGroups = new Map<string, { + vendorId: number + vendorCode: string + vendorName: string + domesticForeign: "DOMESTIC" | "FOREIGN" + division: "SHIP" | "PLANT" + materialType: "EQUIPMENT" | "BULK" | "EQUIPMENT_BULK" + }>() + + contractsWithDetails.forEach(contract => { + const division = contract.projectType === "ship" ? "SHIP" : "PLANT" + const key = `${contract.vendorId}-${division}` + + if (!targetGroups.has(key)) { + targetGroups.set(key, { + vendorId: contract.vendorId, + vendorCode: contract.vendorCode, + vendorName: contract.vendorName, + domesticForeign: contract.vendorType === "DOMESTIC" ? "DOMESTIC" : "FOREIGN", + division: division as "SHIP" | "PLANT", + // 기본값으로 EQUIPMENT 설정 (추후 더 정교한 로직 필요시 수정) + materialType: "EQUIPMENT" as const + }) + } + }) + + console.log(`Created ${targetGroups.size} unique vendor-division combinations`) + + // 3. 이미 존재하는 평가 대상 확인 + const existingTargetsKeys = new Set<string>() + if (targetGroups.size > 0) { + const vendorIds = Array.from(targetGroups.values()).map(t => t.vendorId) + const existingTargets = await db + .select({ + vendorId: evaluationTargets.vendorId, + division: evaluationTargets.division + }) + .from(evaluationTargets) + .where( + and( + eq(evaluationTargets.evaluationYear, evaluationYear), + inArray(evaluationTargets.vendorId, vendorIds) + ) + ) + + existingTargets.forEach(target => { + existingTargetsKeys.add(`${target.vendorId}-${target.division}`) + }) + } + + console.log(`Found ${existingTargetsKeys.size} existing targets`) + + // 4. 새로운 평가 대상만 필터링 + const newTargets = Array.from(targetGroups.entries()) + .filter(([key]) => !existingTargetsKeys.has(key)) + .map(([_, target]) => target) + + if (newTargets.length === 0) { + return { + success: true, + message: "이미 모든 평가 대상이 생성되어 있습니다.", + generatedCount: 0, + skippedCount: targetGroups.size + } + } + + // 5. 평가 대상 생성 + const evaluationTargetsToInsert = newTargets.map(target => ({ + evaluationYear, + division: target.division, + vendorId: target.vendorId, + vendorCode: target.vendorCode, + vendorName: target.vendorName, + domesticForeign: target.domesticForeign, + materialType: target.materialType, + status: "PENDING" as const, + adminUserId, + ldClaimCount: 0, + ldClaimAmount: "0", + ldClaimCurrency: "KRW" as const + })) + + // 배치로 삽입 + await db.insert(evaluationTargets).values(evaluationTargetsToInsert) + + // 통계 계산 + const shipTargets = newTargets.filter(t => t.division === "SHIP").length + const plantTargets = newTargets.filter(t => t.division === "PLANT").length + const duplicateSkipped = existingTargetsKeys.size + + console.log(`Successfully created ${newTargets.length} evaluation targets`) + + // 캐시 무효화 + revalidatePath("/evcp/evaluation-target-list") + revalidatePath("/procurement/evaluation-target-list") + + return { + success: true, + message: `${newTargets.length}개의 평가 대상이 성공적으로 생성되었습니다.`, + generatedCount: newTargets.length, + skippedCount: duplicateSkipped, + details: { + shipTargets, + plantTargets, + duplicateSkipped + } + } + + } catch (error) { + console.error("Error auto generating evaluation targets:", error) + return { + success: false, + error: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.", + message: "평가 대상 자동 생성에 실패했습니다." + } + } }
\ No newline at end of file |
