summaryrefslogtreecommitdiff
path: root/lib/evaluation/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/evaluation/service.ts')
-rw-r--r--lib/evaluation/service.ts1348
1 files changed, 1003 insertions, 345 deletions
diff --git a/lib/evaluation/service.ts b/lib/evaluation/service.ts
index 19e41dff..67a692ab 100644
--- a/lib/evaluation/service.ts
+++ b/lib/evaluation/service.ts
@@ -1,389 +1,1047 @@
'use server'
import db from "@/db/db"
-import {
+import {
evaluationSubmissions,
- periodicEvaluationsView,
- type PeriodicEvaluationView
+ evaluationTargetReviewers,
+ evaluationTargets,
+ periodicEvaluations,
+ periodicEvaluationsView,
+ regEvalCriteria,
+ regEvalCriteriaDetails,
+ reviewerEvaluationDetails,
+ reviewerEvaluations,
+ users,
+ type PeriodicEvaluationView
} from "@/db/schema"
-import {
- and,
- asc,
- count,
- desc,
- ilike,
- or, sql , eq, avg,
- type SQL
+import {
+ and,
+ asc,
+ count,
+ desc,
+ ilike,
+ or, sql, eq, avg, inArray,
+ type SQL
} from "drizzle-orm"
import { filterColumns } from "@/lib/filter-columns"
import { GetEvaluationTargetsSchema } from "../evaluation-target-list/validation";
+import { sendEmail } from "../mail/sendEmail"
+import { revalidatePath } from "next/cache"
+import { DEPARTMENT_CODE_LABELS } from "@/types/evaluation"
export async function getPeriodicEvaluations(input: GetEvaluationTargetsSchema) {
- try {
+ try {
- const offset = (input.page - 1) * input.perPage;
-
- // ✅ getEvaluationTargets 방식과 동일한 필터링 처리
- // 1) 고급 필터 조건
- let advancedWhere: SQL<unknown> | undefined = undefined;
- if (input.filters && input.filters.length > 0) {
- advancedWhere = filterColumns({
- table: periodicEvaluationsView,
- filters: input.filters,
- joinOperator: input.joinOperator || 'and',
- });
- }
-
- // 2) 기본 필터 조건
- let basicWhere: SQL<unknown> | undefined = undefined;
- if (input.basicFilters && input.basicFilters.length > 0) {
- basicWhere = filterColumns({
- table: periodicEvaluationsView,
- filters: input.basicFilters,
- joinOperator: input.basicJoinOperator || 'and',
- });
- }
-
- // 3) 글로벌 검색 조건
- let globalWhere: SQL<unknown> | undefined = undefined;
- if (input.search) {
- const s = `%${input.search}%`;
-
- const validSearchConditions: SQL<unknown>[] = [];
-
- // 벤더 정보로 검색
- const vendorCodeCondition = ilike(periodicEvaluationsView.vendorCode, s);
- if (vendorCodeCondition) validSearchConditions.push(vendorCodeCondition);
-
- const vendorNameCondition = ilike(periodicEvaluationsView.vendorName, s);
- if (vendorNameCondition) validSearchConditions.push(vendorNameCondition);
-
- // 평가 관련 코멘트로 검색
- const evaluationNoteCondition = ilike(periodicEvaluationsView.evaluationNote, s);
- if (evaluationNoteCondition) validSearchConditions.push(evaluationNoteCondition);
-
- const adminCommentCondition = ilike(periodicEvaluationsView.evaluationTargetAdminComment, s);
- if (adminCommentCondition) validSearchConditions.push(adminCommentCondition);
-
- const consolidatedCommentCondition = ilike(periodicEvaluationsView.evaluationTargetConsolidatedComment, s);
- if (consolidatedCommentCondition) validSearchConditions.push(consolidatedCommentCondition);
-
- // 최종 확정자 이름으로 검색
- const finalizedByUserNameCondition = ilike(periodicEvaluationsView.finalizedByUserName, s);
- if (finalizedByUserNameCondition) validSearchConditions.push(finalizedByUserNameCondition);
-
- if (validSearchConditions.length > 0) {
- globalWhere = or(...validSearchConditions);
- }
- }
-
- // ✅ getEvaluationTargets 방식과 동일한 WHERE 조건 생성
- const whereConditions: SQL<unknown>[] = [];
-
- if (advancedWhere) whereConditions.push(advancedWhere);
- if (basicWhere) whereConditions.push(basicWhere);
- if (globalWhere) whereConditions.push(globalWhere);
-
- const finalWhere = whereConditions.length > 0 ? and(...whereConditions) : undefined;
-
- // ✅ getEvaluationTargets 방식과 동일한 전체 데이터 수 조회
- const totalResult = await db
- .select({ count: count() })
- .from(periodicEvaluationsView)
- .where(finalWhere);
-
- const total = totalResult[0]?.count || 0;
-
- if (total === 0) {
- return { data: [], pageCount: 0, total: 0 };
- }
-
- console.log("Total periodic evaluations:", total);
-
- // ✅ getEvaluationTargets 방식과 동일한 정렬 및 페이징 처리된 데이터 조회
- const orderByColumns = input.sort.map((sort) => {
- const column = sort.id as keyof typeof periodicEvaluationsView.$inferSelect;
- return sort.desc ? desc(periodicEvaluationsView[column]) : asc(periodicEvaluationsView[column]);
+ const offset = (input.page - 1) * input.perPage;
+
+ // ✅ getEvaluationTargets 방식과 동일한 필터링 처리
+ // 1) 고급 필터 조건
+ let advancedWhere: SQL<unknown> | undefined = undefined;
+ if (input.filters && input.filters.length > 0) {
+ advancedWhere = filterColumns({
+ table: periodicEvaluationsView,
+ filters: input.filters,
+ joinOperator: input.joinOperator || 'and',
});
-
- if (orderByColumns.length === 0) {
- orderByColumns.push(desc(periodicEvaluationsView.createdAt));
+ }
+
+ // 2) 기본 필터 조건
+ let basicWhere: SQL<unknown> | undefined = undefined;
+ if (input.basicFilters && input.basicFilters.length > 0) {
+ basicWhere = filterColumns({
+ table: periodicEvaluationsView,
+ filters: input.basicFilters,
+ joinOperator: input.basicJoinOperator || 'and',
+ });
+ }
+
+ // 3) 글로벌 검색 조건
+ let globalWhere: SQL<unknown> | undefined = undefined;
+ if (input.search) {
+ const s = `%${input.search}%`;
+
+ const validSearchConditions: SQL<unknown>[] = [];
+
+ // 벤더 정보로 검색
+ const vendorCodeCondition = ilike(periodicEvaluationsView.vendorCode, s);
+ if (vendorCodeCondition) validSearchConditions.push(vendorCodeCondition);
+
+ const vendorNameCondition = ilike(periodicEvaluationsView.vendorName, s);
+ if (vendorNameCondition) validSearchConditions.push(vendorNameCondition);
+
+ // 평가 관련 코멘트로 검색
+ const evaluationNoteCondition = ilike(periodicEvaluationsView.evaluationNote, s);
+ if (evaluationNoteCondition) validSearchConditions.push(evaluationNoteCondition);
+
+ const adminCommentCondition = ilike(periodicEvaluationsView.evaluationTargetAdminComment, s);
+ if (adminCommentCondition) validSearchConditions.push(adminCommentCondition);
+
+ const consolidatedCommentCondition = ilike(periodicEvaluationsView.evaluationTargetConsolidatedComment, s);
+ if (consolidatedCommentCondition) validSearchConditions.push(consolidatedCommentCondition);
+
+ // 최종 확정자 이름으로 검색
+ const finalizedByUserNameCondition = ilike(periodicEvaluationsView.finalizedByUserName, s);
+ if (finalizedByUserNameCondition) validSearchConditions.push(finalizedByUserNameCondition);
+
+ if (validSearchConditions.length > 0) {
+ globalWhere = or(...validSearchConditions);
}
-
- const periodicEvaluationsData = await db
- .select()
- .from(periodicEvaluationsView)
- .where(finalWhere)
- .orderBy(...orderByColumns)
- .limit(input.perPage)
- .offset(offset);
-
- const pageCount = Math.ceil(total / input.perPage);
+ }
- console.log(periodicEvaluationsData,"periodicEvaluationsData")
-
- return { data: periodicEvaluationsData, pageCount, total };
- } catch (err) {
- console.error("Error in getPeriodicEvaluations:", err);
- // ✅ getEvaluationTargets 방식과 동일한 에러 반환 (total 포함)
+ // ✅ getEvaluationTargets 방식과 동일한 WHERE 조건 생성
+ const whereConditions: SQL<unknown>[] = [];
+
+ if (advancedWhere) whereConditions.push(advancedWhere);
+ if (basicWhere) whereConditions.push(basicWhere);
+ if (globalWhere) whereConditions.push(globalWhere);
+
+ const finalWhere = whereConditions.length > 0 ? and(...whereConditions) : undefined;
+
+ // ✅ getEvaluationTargets 방식과 동일한 전체 데이터 수 조회
+ const totalResult = await db
+ .select({ count: count() })
+ .from(periodicEvaluationsView)
+ .where(finalWhere);
+
+ const total = totalResult[0]?.count || 0;
+
+ if (total === 0) {
return { data: [], pageCount: 0, total: 0 };
}
- }
- export interface PeriodicEvaluationsStats {
- total: number
- pendingSubmission: number
- submitted: number
- inReview: number
- reviewCompleted: number
- finalized: number
- averageScore: number | null
- completionRate: number
- averageFinalScore: number | null
- documentsSubmittedCount: number
- documentsNotSubmittedCount: number
- reviewProgress: {
- totalReviewers: number
- completedReviewers: number
- pendingReviewers: number
- reviewCompletionRate: number
+ console.log("Total periodic evaluations:", total);
+
+ // ✅ getEvaluationTargets 방식과 동일한 정렬 및 페이징 처리된 데이터 조회
+ const orderByColumns = input.sort.map((sort) => {
+ const column = sort.id as keyof typeof periodicEvaluationsView.$inferSelect;
+ return sort.desc ? desc(periodicEvaluationsView[column]) : asc(periodicEvaluationsView[column]);
+ });
+
+ if (orderByColumns.length === 0) {
+ orderByColumns.push(desc(periodicEvaluationsView.createdAt));
}
+
+ const periodicEvaluationsData = await db
+ .select()
+ .from(periodicEvaluationsView)
+ .where(finalWhere)
+ .orderBy(...orderByColumns)
+ .limit(input.perPage)
+ .offset(offset);
+
+ const pageCount = Math.ceil(total / input.perPage);
+
+ console.log(periodicEvaluationsData, "periodicEvaluationsData")
+
+ return { data: periodicEvaluationsData, pageCount, total };
+ } catch (err) {
+ console.error("Error in getPeriodicEvaluations:", err);
+ // ✅ getEvaluationTargets 방식과 동일한 에러 반환 (total 포함)
+ return { data: [], pageCount: 0, total: 0 };
}
-
- export async function getPeriodicEvaluationsStats(evaluationYear: number): Promise<PeriodicEvaluationsStats> {
- try {
- // 기본 WHERE 조건: 해당 연도의 평가만
- const baseWhere = eq(periodicEvaluationsView.evaluationYear, evaluationYear)
-
- // 1. 전체 통계 조회
- const totalStatsResult = await db
- .select({
- total: count(),
- averageScore: avg(periodicEvaluationsView.totalScore),
- averageFinalScore: avg(periodicEvaluationsView.finalScore),
- })
- .from(periodicEvaluationsView)
- .where(baseWhere)
-
- const totalStats = totalStatsResult[0] || {
- total: 0,
- averageScore: null,
- averageFinalScore: null
+}
+
+export interface PeriodicEvaluationsStats {
+ total: number
+ pendingSubmission: number
+ submitted: number
+ inReview: number
+ reviewCompleted: number
+ finalized: number
+ averageScore: number | null
+ completionRate: number
+ averageFinalScore: number | null
+ documentsSubmittedCount: number
+ documentsNotSubmittedCount: number
+ reviewProgress: {
+ totalReviewers: number
+ completedReviewers: number
+ pendingReviewers: number
+ reviewCompletionRate: number
+ }
+}
+
+export async function getPeriodicEvaluationsStats(evaluationYear: number): Promise<PeriodicEvaluationsStats> {
+ try {
+ // 기본 WHERE 조건: 해당 연도의 평가만
+ const baseWhere = eq(periodicEvaluationsView.evaluationYear, evaluationYear)
+
+ // 1. 전체 통계 조회
+ const totalStatsResult = await db
+ .select({
+ total: count(),
+ averageScore: avg(periodicEvaluationsView.totalScore),
+ averageFinalScore: avg(periodicEvaluationsView.finalScore),
+ })
+ .from(periodicEvaluationsView)
+ .where(baseWhere)
+
+ const totalStats = totalStatsResult[0] || {
+ total: 0,
+ averageScore: null,
+ averageFinalScore: null
+ }
+
+ // 2. 상태별 카운트 조회
+ const statusStatsResult = await db
+ .select({
+ status: periodicEvaluationsView.status,
+ count: count(),
+ })
+ .from(periodicEvaluationsView)
+ .where(baseWhere)
+ .groupBy(periodicEvaluationsView.status)
+
+ // 상태별 카운트를 객체로 변환
+ const statusCounts = statusStatsResult.reduce((acc, item) => {
+ acc[item.status] = item.count
+ return acc
+ }, {} as Record<string, number>)
+
+ // 3. 문서 제출 상태 통계
+ const documentStatsResult = await db
+ .select({
+ documentsSubmitted: periodicEvaluationsView.documentsSubmitted,
+ count: count(),
+ })
+ .from(periodicEvaluationsView)
+ .where(baseWhere)
+ .groupBy(periodicEvaluationsView.documentsSubmitted)
+
+ const documentCounts = documentStatsResult.reduce((acc, item) => {
+ if (item.documentsSubmitted) {
+ acc.submitted = item.count
+ } else {
+ acc.notSubmitted = item.count
}
-
- // 2. 상태별 카운트 조회
- const statusStatsResult = await db
- .select({
- status: periodicEvaluationsView.status,
- count: count(),
- })
- .from(periodicEvaluationsView)
- .where(baseWhere)
- .groupBy(periodicEvaluationsView.status)
-
- // 상태별 카운트를 객체로 변환
- const statusCounts = statusStatsResult.reduce((acc, item) => {
- acc[item.status] = item.count
- return acc
- }, {} as Record<string, number>)
-
- // 3. 문서 제출 상태 통계
- const documentStatsResult = await db
- .select({
- documentsSubmitted: periodicEvaluationsView.documentsSubmitted,
- count: count(),
+ return acc
+ }, { submitted: 0, notSubmitted: 0 })
+
+ // 4. 리뷰어 진행 상황 통계
+ const reviewProgressResult = await db
+ .select({
+ totalReviewers: sql<number>`SUM(${periodicEvaluationsView.totalReviewers})`.as('total_reviewers'),
+ completedReviewers: sql<number>`SUM(${periodicEvaluationsView.completedReviewers})`.as('completed_reviewers'),
+ pendingReviewers: sql<number>`SUM(${periodicEvaluationsView.pendingReviewers})`.as('pending_reviewers'),
+ })
+ .from(periodicEvaluationsView)
+ .where(baseWhere)
+
+ const reviewProgress = reviewProgressResult[0] || {
+ totalReviewers: 0,
+ completedReviewers: 0,
+ pendingReviewers: 0,
+ }
+
+ // 5. 완료율 계산
+ const finalizedCount = statusCounts['FINALIZED'] || 0
+ const totalCount = totalStats.total
+ const completionRate = totalCount > 0 ? Math.round((finalizedCount / totalCount) * 100) : 0
+
+ // 6. 리뷰 완료율 계산
+ const reviewCompletionRate = reviewProgress.totalReviewers > 0
+ ? Math.round((reviewProgress.completedReviewers / reviewProgress.totalReviewers) * 100)
+ : 0
+
+ // 7. 평균 점수 포맷팅 (소수점 1자리)
+ const formatScore = (score: string | number | null): number | null => {
+ if (score === null || score === undefined) return null
+ return Math.round(Number(score) * 10) / 10
+ }
+
+ return {
+ total: totalCount,
+ pendingSubmission: statusCounts['PENDING_SUBMISSION'] || 0,
+ submitted: statusCounts['SUBMITTED'] || 0,
+ inReview: statusCounts['IN_REVIEW'] || 0,
+ reviewCompleted: statusCounts['REVIEW_COMPLETED'] || 0,
+ finalized: finalizedCount,
+ averageScore: formatScore(totalStats.averageScore),
+ averageFinalScore: formatScore(totalStats.averageFinalScore),
+ completionRate,
+ documentsSubmittedCount: documentCounts.submitted,
+ documentsNotSubmittedCount: documentCounts.notSubmitted,
+ reviewProgress: {
+ totalReviewers: reviewProgress.totalReviewers,
+ completedReviewers: reviewProgress.completedReviewers,
+ pendingReviewers: reviewProgress.pendingReviewers,
+ reviewCompletionRate,
+ },
+ }
+
+ } catch (error) {
+ console.error('Error in getPeriodicEvaluationsStats:', error)
+ // 에러 발생 시 기본값 반환
+ return {
+ total: 0,
+ pendingSubmission: 0,
+ submitted: 0,
+ inReview: 0,
+ reviewCompleted: 0,
+ finalized: 0,
+ averageScore: null,
+ averageFinalScore: null,
+ completionRate: 0,
+ documentsSubmittedCount: 0,
+ documentsNotSubmittedCount: 0,
+ reviewProgress: {
+ totalReviewers: 0,
+ completedReviewers: 0,
+ pendingReviewers: 0,
+ reviewCompletionRate: 0,
+ },
+ }
+ }
+}
+
+
+
+interface RequestDocumentsData {
+ periodicEvaluationId: number
+ companyId: number
+ evaluationYear: number
+ evaluationRound: string
+ message: string
+}
+
+export async function requestDocumentsFromVendors(data: RequestDocumentsData[]) {
+ try {
+ // 각 평가에 대해 evaluationSubmissions 레코드 생성
+ const submissions = await Promise.all(
+ data.map(async (item) => {
+ // 이미 해당 periodicEvaluationId와 companyId로 생성된 submission이 있는지 확인
+ const existingSubmission = await db.query.evaluationSubmissions.findFirst({
+ where: and(
+ eq(evaluationSubmissions.periodicEvaluationId, item.periodicEvaluationId),
+ eq(evaluationSubmissions.companyId, item.companyId)
+ )
})
- .from(periodicEvaluationsView)
- .where(baseWhere)
- .groupBy(periodicEvaluationsView.documentsSubmitted)
-
- const documentCounts = documentStatsResult.reduce((acc, item) => {
- if (item.documentsSubmitted) {
- acc.submitted = item.count
+
+ if (existingSubmission) {
+ // 이미 존재하면 reviewComments만 업데이트
+ const [updated] = await db
+ .update(evaluationSubmissions)
+ .set({
+ reviewComments: item.message,
+ updatedAt: new Date()
+ })
+ .where(eq(evaluationSubmissions.id, existingSubmission.id))
+ .returning()
+
+ return updated
} else {
- acc.notSubmitted = item.count
+ // 새로 생성
+ const [created] = await db
+ .insert(evaluationSubmissions)
+ .values({
+ periodicEvaluationId: item.periodicEvaluationId,
+ companyId: item.companyId,
+ evaluationYear: item.evaluationYear,
+ evaluationRound: item.evaluationRound,
+ submissionStatus: 'draft', // 기본값
+ reviewComments: item.message,
+ // 진행률 관련 필드들은 기본값 0으로 설정됨
+ totalGeneralItems: 0,
+ completedGeneralItems: 0,
+ totalEsgItems: 0,
+ completedEsgItems: 0,
+ isActive: true
+ })
+ .returning()
+
+ return created
}
- return acc
- }, { submitted: 0, notSubmitted: 0 })
-
- // 4. 리뷰어 진행 상황 통계
- const reviewProgressResult = await db
+ })
+ )
+
+
+ return {
+ success: true,
+ message: `${submissions.length}개 업체에 자료 요청이 완료되었습니다.`,
+ submissions
+ }
+
+ } catch (error) {
+ console.error("Error requesting documents from vendors:", error)
+ return {
+ success: false,
+ message: "자료 요청 중 오류가 발생했습니다.",
+ error: error instanceof Error ? error.message : "Unknown error"
+ }
+ }
+}
+
+// 기존 요청 상태 확인 함수 추가
+export async function checkExistingSubmissions(periodicEvaluationIds: number[]) {
+ try {
+ const existingSubmissions = await db.query.evaluationSubmissions.findMany({
+ where: (submissions) => {
+ // periodicEvaluationIds 배열에 포함된 ID들을 확인
+ return periodicEvaluationIds.length === 1
+ ? eq(submissions.periodicEvaluationId, periodicEvaluationIds[0])
+ : periodicEvaluationIds.length > 1
+ ? or(...periodicEvaluationIds.map(id => eq(submissions.periodicEvaluationId, id)))
+ : eq(submissions.id, -1) // 빈 배열인 경우 결과 없음
+ },
+ columns: {
+ id: true,
+ periodicEvaluationId: true,
+ companyId: true,
+ createdAt: true,
+ reviewComments: true
+ }
+ })
+
+ return existingSubmissions
+ } catch (error) {
+ console.error("Error checking existing submissions:", error)
+ return []
+ }
+}
+
+
+// ================================================================
+// 타입 정의
+// ================================================================
+interface ReviewerInfo {
+ id: number
+ name: string
+ email: string
+ deptName: string | null
+ departmentCode: string
+ evaluationTargetId: number
+ evaluationTargetReviewerId: number
+}
+
+interface ReviewerEvaluationRequestData {
+ periodicEvaluationId: number
+ evaluationTargetReviewerId: number
+ message: string
+}
+
+// ================================================================
+// 1. 평가 대상별 리뷰어 정보 가져오기
+// ================================================================
+export async function getReviewersForEvaluations(
+ evaluationTargetIds: number[]
+): Promise<ReviewerInfo[]> {
+ try {
+ if (evaluationTargetIds.length === 0) {
+ return []
+ }
+
+ // evaluation_target_reviewers와 users 테이블 조인
+ const reviewers = await db
+ .select({
+ id: users.id,
+ name: users.name,
+ email: users.email,
+ deptName: users.deptName,
+ departmentCode: evaluationTargetReviewers.departmentCode,
+ evaluationTargetId: evaluationTargetReviewers.evaluationTargetId,
+ evaluationTargetReviewerId: evaluationTargetReviewers.id,
+ })
+ .from(evaluationTargetReviewers)
+ .innerJoin(users, eq(evaluationTargetReviewers.reviewerUserId, users.id))
+ .where(
+ and(
+ inArray(evaluationTargetReviewers.evaluationTargetId, evaluationTargetIds),
+ eq(users.isActive, true) // 활성 사용자만
+ )
+ )
+ .orderBy(evaluationTargetReviewers.evaluationTargetId, users.name)
+
+ return reviewers
+ } catch (error) {
+ console.error('Error fetching reviewers for evaluations:', error)
+ throw new Error('평가자 정보를 가져오는데 실패했습니다.')
+ }
+}
+// ================================================================
+// 2. 리뷰어 평가 요청 생성 및 알림 발송
+// ================================================================
+export async function createReviewerEvaluationsRequest(
+ requestData: ReviewerEvaluationRequestData[]
+): Promise<{ success: boolean; message: string }> {
+ try {
+ if (requestData.length === 0) {
+ return {
+ success: false,
+ message: "요청할 평가 데이터가 없습니다."
+ }
+ }
+
+ console.log('평가 요청 데이터:', requestData)
+
+ // 트랜잭션으로 처리
+ await db.transaction(async (tx) => {
+ // 1. 기존 reviewerEvaluations 확인 (중복 방지)
+ const existingEvaluations = await tx
.select({
- totalReviewers: sql<number>`SUM(${periodicEvaluationsView.totalReviewers})`.as('total_reviewers'),
- completedReviewers: sql<number>`SUM(${periodicEvaluationsView.completedReviewers})`.as('completed_reviewers'),
- pendingReviewers: sql<number>`SUM(${periodicEvaluationsView.pendingReviewers})`.as('pending_reviewers'),
+ periodicEvaluationId: reviewerEvaluations.periodicEvaluationId,
+ evaluationTargetReviewerId: reviewerEvaluations.evaluationTargetReviewerId,
})
- .from(periodicEvaluationsView)
- .where(baseWhere)
-
- const reviewProgress = reviewProgressResult[0] || {
- totalReviewers: 0,
- completedReviewers: 0,
- pendingReviewers: 0,
+ .from(reviewerEvaluations)
+ .where(
+ and(
+ inArray(
+ reviewerEvaluations.periodicEvaluationId,
+ requestData.map(r => r.periodicEvaluationId)
+ ),
+ inArray(
+ reviewerEvaluations.evaluationTargetReviewerId,
+ requestData.map(r => r.evaluationTargetReviewerId)
+ )
+ )
+ )
+
+ // 2. 중복되지 않는 새로운 평가 요청만 필터링
+ const newRequestData = requestData.filter(request =>
+ !existingEvaluations.some(existing =>
+ existing.periodicEvaluationId === request.periodicEvaluationId &&
+ existing.evaluationTargetReviewerId === request.evaluationTargetReviewerId
+ )
+ )
+
+ if (newRequestData.length === 0) {
+ throw new Error("모든 평가 요청이 이미 생성되어 있습니다.")
}
-
- // 5. 완료율 계산
- const finalizedCount = statusCounts['FINALIZED'] || 0
- const totalCount = totalStats.total
- const completionRate = totalCount > 0 ? Math.round((finalizedCount / totalCount) * 100) : 0
-
- // 6. 리뷰 완료율 계산
- const reviewCompletionRate = reviewProgress.totalReviewers > 0
- ? Math.round((reviewProgress.completedReviewers / reviewProgress.totalReviewers) * 100)
- : 0
-
- // 7. 평균 점수 포맷팅 (소수점 1자리)
- const formatScore = (score: string | number | null): number | null => {
- if (score === null || score === undefined) return null
- return Math.round(Number(score) * 10) / 10
+
+ console.log(`새로 생성할 평가 요청: ${newRequestData.length}개`)
+
+ // 3. reviewerEvaluations 테이블에 레코드 생성
+ const reviewerEvaluationInsertData = newRequestData.map(request => ({
+ periodicEvaluationId: request.periodicEvaluationId,
+ evaluationTargetReviewerId: request.evaluationTargetReviewerId,
+ isCompleted: false,
+ // 기본값들
+ processScore: null,
+ priceScore: null,
+ deliveryScore: null,
+ selfEvaluationScore: null,
+ participationBonus: "0",
+ qualityDeduction: "0",
+ totalScore: null,
+ grade: null,
+ completedAt: null,
+ reviewerComment: null,
+ }))
+
+ const insertedEvaluations = await tx.insert(reviewerEvaluations).values(reviewerEvaluationInsertData).returning({ id: reviewerEvaluations.id })
+ console.log(`reviewerEvaluations 레코드 생성 완료: ${insertedEvaluations.length}개`)
+
+ // 4. 이메일 발송을 위한 상세 정보 수집
+ try {
+ await sendEvaluationRequestEmails(tx, newRequestData, requestData[0]?.message || "")
+ } catch (emailError) {
+ console.error('이메일 발송 중 오류:', emailError)
+ // 이메일 발송 실패해도 전체 트랜잭션은 성공으로 처리
}
-
+ })
+
+ const totalReviewers = [...new Set(requestData.map(r => r.evaluationTargetReviewerId))].length
+ const totalEvaluations = [...new Set(requestData.map(r => r.periodicEvaluationId))].length
+
+ return {
+ success: true,
+ message: `${totalEvaluations}개 평가에 대해 ${totalReviewers}명의 평가자에게 요청이 발송되었습니다.`
+ }
+
+ } catch (error) {
+ console.error('Error creating reviewer evaluation requests:', error)
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : "평가 요청 생성 중 오류가 발생했습니다."
+ }
+ }
+}
+
+
+const getDepartmentLabel = (code: string): string => {
+ return DEPARTMENT_CODE_LABELS[code as keyof typeof DEPARTMENT_CODE_LABELS] || code
+}
+
+// ================================================================
+// 이메일 발송 헬퍼 함수 (완전 새로 작성)
+// ================================================================
+async function sendEvaluationRequestEmails(
+ tx: any,
+ requestData: ReviewerEvaluationRequestData[],
+ message: string
+) {
+ try {
+
+ // 1. 평가 정보 수집 (periodicEvaluations + evaluationTargets 조인)
+ const evaluationIds = [...new Set(requestData.map(r => r.periodicEvaluationId))]
+
+ const evaluationDetails = await tx
+ .select({
+ periodicEvaluationId: periodicEvaluations.id,
+ evaluationTargetId: periodicEvaluations.evaluationTargetId,
+ evaluationYear: evaluationTargets.evaluationYear,
+ evaluationPeriod: periodicEvaluations.evaluationPeriod,
+ vendorCode: evaluationTargets.vendorCode,
+ vendorName: evaluationTargets.vendorName,
+ division: evaluationTargets.division,
+ materialType: evaluationTargets.materialType,
+ domesticForeign: evaluationTargets.domesticForeign,
+ submissionDeadline: periodicEvaluations.submissionDeadline,
+ })
+ .from(periodicEvaluations)
+ .innerJoin(evaluationTargets, eq(periodicEvaluations.evaluationTargetId, evaluationTargets.id))
+ .where(inArray(periodicEvaluations.id, evaluationIds))
+
+ console.log('평가 상세 정보:', evaluationDetails)
+
+ // 2. 리뷰어 정보 수집
+ const reviewerIds = [...new Set(requestData.map(r => r.evaluationTargetReviewerId))]
+ console.log('리뷰어 ID들:', reviewerIds)
+
+ const reviewerDetails = await tx
+ .select({
+ evaluationTargetReviewerId: evaluationTargetReviewers.id,
+ evaluationTargetId: evaluationTargetReviewers.evaluationTargetId,
+ departmentCode: evaluationTargetReviewers.departmentCode,
+ reviewerUserId: evaluationTargetReviewers.reviewerUserId,
+ userName: users.name,
+ userEmail: users.email,
+ deptName: users.deptName,
+ })
+ .from(evaluationTargetReviewers)
+ .innerJoin(users, eq(evaluationTargetReviewers.reviewerUserId, users.id))
+ .where(inArray(evaluationTargetReviewers.id, reviewerIds))
+
+ console.log('리뷰어 상세 정보:', reviewerDetails)
+
+ // 3. 평가별로 그룹핑 (각 평가에 대한 리뷰어들)
+ const evaluationGroups = evaluationDetails.map(evaluation => {
+ const relatedRequests = requestData.filter(req => req.periodicEvaluationId === evaluation.periodicEvaluationId)
+ const evaluationReviewers = relatedRequests.map(req => {
+ const reviewer = reviewerDetails.find(r => r.evaluationTargetReviewerId === req.evaluationTargetReviewerId)
+ return {
+ ...reviewer,
+ departmentLabel: getDepartmentLabel(reviewer?.departmentCode || ''),
+ }
+ }).filter(Boolean)
+
return {
- total: totalCount,
- pendingSubmission: statusCounts['PENDING_SUBMISSION'] || 0,
- submitted: statusCounts['SUBMITTED'] || 0,
- inReview: statusCounts['IN_REVIEW'] || 0,
- reviewCompleted: statusCounts['REVIEW_COMPLETED'] || 0,
- finalized: finalizedCount,
- averageScore: formatScore(totalStats.averageScore),
- averageFinalScore: formatScore(totalStats.averageFinalScore),
- completionRate,
- documentsSubmittedCount: documentCounts.submitted,
- documentsNotSubmittedCount: documentCounts.notSubmitted,
- reviewProgress: {
- totalReviewers: reviewProgress.totalReviewers,
- completedReviewers: reviewProgress.completedReviewers,
- pendingReviewers: reviewProgress.pendingReviewers,
- reviewCompletionRate,
- },
+ ...evaluation,
+ reviewers: evaluationReviewers,
+ relatedRequests
}
-
- } catch (error) {
- console.error('Error in getPeriodicEvaluationsStats:', error)
- // 에러 발생 시 기본값 반환
- return {
- total: 0,
- pendingSubmission: 0,
- submitted: 0,
- inReview: 0,
- reviewCompleted: 0,
- finalized: 0,
- averageScore: null,
- averageFinalScore: null,
- completionRate: 0,
- documentsSubmittedCount: 0,
- documentsNotSubmittedCount: 0,
- reviewProgress: {
- totalReviewers: 0,
- completedReviewers: 0,
- pendingReviewers: 0,
- reviewCompletionRate: 0,
- },
+ })
+
+ console.log('평가 그룹:', evaluationGroups)
+
+ // 4. 각 리뷰어에게 개별 이메일 발송
+ const emailPromises = []
+
+ for (const group of evaluationGroups) {
+ for (const reviewer of group.reviewers) {
+ if (!reviewer?.userEmail) {
+ console.log(`이메일 주소가 없는 리뷰어 스킵: ${reviewer?.userName}`)
+ continue
+ }
+
+ // 해당 리뷰어를 제외한 다른 리뷰어들
+ const otherReviewers = group.reviewers.filter(r => r?.evaluationTargetReviewerId !== reviewer.evaluationTargetReviewerId)
+
+ console.log(`${reviewer.userName}(${reviewer.userEmail})에게 이메일 발송 준비`)
+
+ const emailPromise = sendEmail({
+ to: reviewer.userEmail,
+ subject: `[평가 요청] ${group.vendorName} - ${group.evaluationYear}년 ${group.evaluationPeriod} 정기평가`,
+ template: "evaluation-request",
+ context: {
+ language: "ko",
+ reviewerName: reviewer.userName,
+ departmentLabel: reviewer.departmentLabel,
+ evaluation: {
+ vendorName: group.vendorName,
+ vendorCode: group.vendorCode,
+ evaluationYear: group.evaluationYear,
+ evaluationPeriod: group.evaluationPeriod,
+ division: group.division,
+ materialType: group.materialType,
+ domesticForeign: group.domesticForeign,
+ submissionDeadline: group.submissionDeadline ? new Date(group.submissionDeadline).toLocaleDateString('ko-KR') : null,
+ },
+ otherReviewers: otherReviewers.map(r => ({
+ name: r?.userName,
+ department: r?.departmentLabel,
+ email: r?.userEmail
+ })).filter(r => r.name),
+ message: message || "협력업체 정기평가를 진행해 주시기 바랍니다.",
+ evaluationUrl: `${process.env.NEXT_PUBLIC_APP_URL}/evaluations/${group.periodicEvaluationId}/review`
+ },
+ }).catch(error => {
+ console.error(`${reviewer.userEmail}에게 이메일 발송 실패:`, error)
+ return null
+ })
+
+ emailPromises.push(emailPromise)
}
}
+
+ // 5. 모든 이메일 발송 대기
+ const emailResults = await Promise.allSettled(emailPromises)
+ const successCount = emailResults.filter(result => result.status === 'fulfilled').length
+ const failureCount = emailResults.filter(result => result.status === 'rejected').length
+
+ console.log(`이메일 발송 완료: 성공 ${successCount}개, 실패 ${failureCount}개`)
+
+ if (failureCount > 0) {
+ console.error('실패한 이메일들:', emailResults.filter(r => r.status === 'rejected').map(r => r.reason))
+ }
+
+ } catch (error) {
+ console.error('Error sending evaluation request emails:', error)
+ throw error // 이메일 발송 실패도 에러로 처리하려면 throw, 아니면 console.error만
}
+}
+// ================================================================
+// 3. 리뷰어별 평가 완료 상태 확인 (선택적 기능)
+// ================================================================
+export async function getReviewerEvaluationStatus(
+ periodicEvaluationIds: number[]
+): Promise<Array<{
+ periodicEvaluationId: number
+ totalReviewers: number
+ completedReviewers: number
+ completionRate: number
+}>> {
+ try {
+ if (periodicEvaluationIds.length === 0) {
+ return []
+ }
+ const evaluationStatus = await db
+ .select({
+ periodicEvaluationId: reviewerEvaluations.periodicEvaluationId,
+ totalReviewers: db.$count(reviewerEvaluations.id),
+ completedReviewers: db.$count(
+ reviewerEvaluations.id,
+ eq(reviewerEvaluations.isCompleted, true)
+ ),
+ })
+ .from(reviewerEvaluations)
+ .where(inArray(reviewerEvaluations.periodicEvaluationId, periodicEvaluationIds))
+ .groupBy(reviewerEvaluations.periodicEvaluationId)
+ return evaluationStatus.map(status => ({
+ ...status,
+ completionRate: status.totalReviewers > 0
+ ? Math.round((status.completedReviewers / status.totalReviewers) * 100)
+ : 0
+ }))
- interface RequestDocumentsData {
- periodicEvaluationId: number
- companyId: number
- evaluationYear: number
- evaluationRound: string
- message: string
+ } catch (error) {
+ console.error('Error fetching reviewer evaluation status:', error)
+ throw new Error('평가 완료 상태를 가져오는데 실패했습니다.')
}
-
- export async function requestDocumentsFromVendors(data: RequestDocumentsData[]) {
- try {
- // 각 평가에 대해 evaluationSubmissions 레코드 생성
- const submissions = await Promise.all(
- data.map(async (item) => {
- // 이미 해당 periodicEvaluationId와 companyId로 생성된 submission이 있는지 확인
- const existingSubmission = await db.query.evaluationSubmissions.findFirst({
- where: and(
- eq(evaluationSubmissions.periodicEvaluationId, item.periodicEvaluationId),
- eq(evaluationSubmissions.companyId, item.companyId)
- )
+}
+
+// 평가 확정 데이터 타입
+interface FinalizeEvaluationData {
+ id: number
+ finalScore: number
+ finalGrade: "S" | "A" | "B" | "C" | "D"
+}
+
+/**
+ * 평가를 최종 확정합니다
+ */
+export async function finalizeEvaluations(
+ evaluationData: FinalizeEvaluationData[]
+) {
+ try {
+ // 현재 사용자 정보 가져오기
+ const currentUser = await getCurrentUser()
+ if (!currentUser) {
+ throw new Error("인증이 필요합니다")
+ }
+
+ // 트랜잭션으로 여러 평가를 한번에 처리
+ await db.transaction(async (tx) => {
+ const now = new Date()
+
+ // 각 평가를 순차적으로 처리
+ for (const evaluation of evaluationData) {
+ // 1. 평가 상태가 REVIEW_COMPLETED인지 확인
+ const existingEvaluation = await tx
+ .select({
+ id: periodicEvaluations.id,
+ status: periodicEvaluations.status,
})
-
- if (existingSubmission) {
- // 이미 존재하면 reviewComments만 업데이트
- const [updated] = await db
- .update(evaluationSubmissions)
- .set({
- reviewComments: item.message,
- updatedAt: new Date()
- })
- .where(eq(evaluationSubmissions.id, existingSubmission.id))
- .returning()
-
- return updated
- } else {
- // 새로 생성
- const [created] = await db
- .insert(evaluationSubmissions)
- .values({
- periodicEvaluationId: item.periodicEvaluationId,
- companyId: item.companyId,
- evaluationYear: item.evaluationYear,
- evaluationRound: item.evaluationRound,
- submissionStatus: 'draft', // 기본값
- reviewComments: item.message,
- // 진행률 관련 필드들은 기본값 0으로 설정됨
- totalGeneralItems: 0,
- completedGeneralItems: 0,
- totalEsgItems: 0,
- completedEsgItems: 0,
- isActive: true
- })
- .returning()
-
- return created
- }
- })
- )
-
-
- return {
- success: true,
- message: `${submissions.length}개 업체에 자료 요청이 완료되었습니다.`,
- submissions
+ .from(periodicEvaluations)
+ .where(eq(periodicEvaluations.id, evaluation.id))
+ .limit(1)
+
+ if (existingEvaluation.length === 0) {
+ throw new Error(`평가를 찾을 수 없습니다: ID ${evaluation.id}`)
+ }
+
+ if (existingEvaluation[0].status !== "REVIEW_COMPLETED") {
+ throw new Error(
+ `평가 ${evaluation.id}는 검토 완료 상태가 아닙니다. 현재 상태: ${existingEvaluation[0].status}`
+ )
+ }
+
+ // 2. 평가를 최종 확정으로 업데이트
+ await tx
+ .update(periodicEvaluations)
+ .set({
+ finalScore: evaluation.finalScore.toString(),
+ finalGrade: evaluation.finalGrade,
+ status: "FINALIZED",
+ finalizedAt: now,
+ finalizedBy: currentUser.id,
+ updatedAt: now,
+ })
+ .where(eq(periodicEvaluations.id, evaluation.id))
}
-
- } catch (error) {
- console.error("Error requesting documents from vendors:", error)
- return {
- success: false,
- message: "자료 요청 중 오류가 발생했습니다.",
- error: error instanceof Error ? error.message : "Unknown error"
+ })
+
+ revalidatePath("/evcp/evaluation")
+ revalidatePath("/procurement/evaluation")
+
+ return {
+ success: true,
+ message: `${evaluationData.length}건의 평가가 성공적으로 확정되었습니다`,
+ }
+ } catch (error) {
+ console.error("Error finalizing evaluations:", error)
+ throw new Error(
+ error instanceof Error
+ ? error.message
+ : "평가 확정 중 오류가 발생했습니다"
+ )
+ }
+}
+
+/**
+ * 평가 확정을 취소합니다 (필요시 추가)
+ */
+export async function unfinalizeEvaluations(evaluationIds: number[]) {
+ try {
+ const currentUser = await getCurrentUser()
+ if (!currentUser) {
+ throw new Error("인증이 필요합니다")
+ }
+
+ await db.transaction(async (tx) => {
+ for (const evaluationId of evaluationIds) {
+ // 1. 평가 상태가 FINALIZED인지 확인
+ const existingEvaluation = await tx
+ .select({
+ id: periodicEvaluations.id,
+ status: periodicEvaluations.status,
+ })
+ .from(periodicEvaluations)
+ .where(eq(periodicEvaluations.id, evaluationId))
+ .limit(1)
+
+ if (existingEvaluation.length === 0) {
+ throw new Error(`평가를 찾을 수 없습니다: ID ${evaluationId}`)
+ }
+
+ if (existingEvaluation[0].status !== "FINALIZED") {
+ throw new Error(
+ `평가 ${evaluationId}는 확정 상태가 아닙니다. 현재 상태: ${existingEvaluation[0].status}`
+ )
+ }
+
+ // 2. 확정 해제 - 검토 완료 상태로 되돌림
+ await tx
+ .update(periodicEvaluations)
+ .set({
+ finalScore: null,
+ finalGrade: null,
+ status: "REVIEW_COMPLETED",
+ finalizedAt: null,
+ finalizedBy: null,
+ updatedAt: new Date(),
+ })
+ .where(eq(periodicEvaluations.id, evaluationId))
}
+ })
+
+ revalidatePath("/evcp/evaluation")
+ revalidatePath("/procurement/evaluation")
+
+ return {
+ success: true,
+ message: `${evaluationIds.length}건의 평가 확정이 취소되었습니다`,
}
+ } catch (error) {
+ console.error("Error unfinalizing evaluations:", error)
+ throw new Error(
+ error instanceof Error
+ ? error.message
+ : "평가 확정 취소 중 오류가 발생했습니다"
+ )
}
+}
+
+
+// 평가 상세 정보 타입
+export interface EvaluationDetailData {
+ // 리뷰어 정보
+ reviewerEvaluationId: number
+ reviewerName: string
+ reviewerEmail: string
+ departmentCode: string
+ departmentName: string
+ isCompleted: boolean
+ completedAt: Date | null
+ reviewerComment: string | null
- // 기존 요청 상태 확인 함수 추가
- export async function checkExistingSubmissions(periodicEvaluationIds: number[]) {
- try {
- const existingSubmissions = await db.query.evaluationSubmissions.findMany({
- where: (submissions) => {
- // periodicEvaluationIds 배열에 포함된 ID들을 확인
- return periodicEvaluationIds.length === 1
- ? eq(submissions.periodicEvaluationId, periodicEvaluationIds[0])
- : periodicEvaluationIds.length > 1
- ? or(...periodicEvaluationIds.map(id => eq(submissions.periodicEvaluationId, id)))
- : eq(submissions.id, -1) // 빈 배열인 경우 결과 없음
- },
- columns: {
- id: true,
- periodicEvaluationId: true,
- companyId: true,
- createdAt: true,
- reviewComments: true
- }
+ // 평가 항목별 상세
+ evaluationItems: {
+ // 평가 기준 정보
+ criteriaId: number
+ category: string
+ category2: string
+ item: string
+ classification: string
+ range: string | null
+ remarks: string | null
+ scoreType: string
+
+ // 선택된 옵션 정보 (fixed 타입인 경우)
+ selectedDetailId: number | null
+ selectedDetail: string | null
+
+ // 점수 및 의견
+ score: number | null
+ comment: string | null
+ }[]
+}
+
+
+/**
+ * 특정 정기평가의 상세 정보를 조회합니다
+ */
+export async function getEvaluationDetails(periodicEvaluationId: number): Promise<{
+ evaluationInfo: {
+ id: number
+ vendorName: string
+ vendorCode: string
+ evaluationYear: number
+ division: string
+ status: string
+ }
+ reviewerDetails: EvaluationDetailData[]
+}> {
+ try {
+ // 1. 평가 기본 정보 조회
+ const evaluationInfo = await db
+ .select({
+ id: periodicEvaluations.id,
+ vendorName: evaluationTargets.vendorName,
+ vendorCode: evaluationTargets.vendorCode,
+ evaluationYear: evaluationTargets.evaluationYear,
+ division: evaluationTargets.division,
+ status: periodicEvaluations.status,
})
-
- return existingSubmissions
- } catch (error) {
- console.error("Error checking existing submissions:", error)
- return []
+ .from(periodicEvaluations)
+ .leftJoin(evaluationTargets, eq(periodicEvaluations.evaluationTargetId, evaluationTargets.id))
+ .where(eq(periodicEvaluations.id, periodicEvaluationId))
+ .limit(1)
+
+ if (evaluationInfo.length === 0) {
+ throw new Error("평가를 찾을 수 없습니다")
}
- } \ No newline at end of file
+
+ // 2. 리뷰어별 평가 상세 정보 조회
+ const reviewerDetailsRaw = await db
+ .select({
+ // 리뷰어 평가 기본 정보
+ reviewerEvaluationId: reviewerEvaluations.id,
+ reviewerName: users.name,
+ reviewerEmail: users.email,
+ departmentCode: evaluationTargetReviewers.departmentCode,
+ isCompleted: reviewerEvaluations.isCompleted,
+ completedAt: reviewerEvaluations.completedAt,
+ reviewerComment: reviewerEvaluations.reviewerComment,
+
+ // 평가 항목 상세
+ detailId: reviewerEvaluationDetails.id,
+ criteriaId: regEvalCriteria.id,
+ category: regEvalCriteria.category,
+ category2: regEvalCriteria.category2,
+ item: regEvalCriteria.item,
+ classification: regEvalCriteria.classification,
+ range: regEvalCriteria.range,
+ remarks: regEvalCriteria.remarks,
+ scoreType: regEvalCriteria.scoreType,
+
+ // 선택된 옵션 정보
+ selectedDetailId: reviewerEvaluationDetails.regEvalCriteriaDetailsId,
+ selectedDetail: regEvalCriteriaDetails.detail,
+
+ // 점수 및 의견
+ score: reviewerEvaluationDetails.score,
+ comment: reviewerEvaluationDetails.comment,
+ })
+ .from(reviewerEvaluations)
+ .leftJoin(evaluationTargetReviewers, eq(reviewerEvaluations.evaluationTargetReviewerId, evaluationTargetReviewers.id))
+ .leftJoin(users, eq(evaluationTargetReviewers.reviewerUserId, users.id))
+ .leftJoin(reviewerEvaluationDetails, eq(reviewerEvaluations.id, reviewerEvaluationDetails.reviewerEvaluationId))
+ .leftJoin(regEvalCriteriaDetails, eq(reviewerEvaluationDetails.regEvalCriteriaDetailsId, regEvalCriteriaDetails.id))
+ .leftJoin(regEvalCriteria, eq(regEvalCriteriaDetails.criteriaId, regEvalCriteria.id))
+ .where(eq(reviewerEvaluations.periodicEvaluationId, periodicEvaluationId))
+ .orderBy(evaluationTargetReviewers.departmentCode, regEvalCriteria.category, regEvalCriteria.classification)
+
+ // 3. 리뷰어별로 그룹화
+ const reviewerDetailsMap = new Map<number, EvaluationDetailData>()
+
+ reviewerDetailsRaw.forEach(row => {
+ if (!reviewerDetailsMap.has(row.reviewerEvaluationId)) {
+ reviewerDetailsMap.set(row.reviewerEvaluationId, {
+ reviewerEvaluationId: row.reviewerEvaluationId,
+ reviewerName: row.reviewerName || "",
+ reviewerEmail: row.reviewerEmail || "",
+ departmentCode: row.departmentCode || "",
+ departmentName: DEPARTMENT_CODE_LABELS[row.departmentCode as keyof typeof DEPARTMENT_CODE_LABELS] || row.departmentCode || "",
+ isCompleted: row.isCompleted || false,
+ completedAt: row.completedAt,
+ reviewerComment: row.reviewerComment,
+ evaluationItems: []
+ })
+ }
+
+ // 평가 항목이 있는 경우에만 추가
+ if (row.criteriaId && row.detailId) {
+ const reviewer = reviewerDetailsMap.get(row.reviewerEvaluationId)!
+ reviewer.evaluationItems.push({
+ criteriaId: row.criteriaId,
+ category: row.category || "",
+ category2: row.category2 || "",
+ item: row.item || "",
+ classification: row.classification || "",
+ range: row.range,
+ remarks: row.remarks,
+ scoreType: row.scoreType || "fixed",
+ selectedDetailId: row.selectedDetailId,
+ selectedDetail: row.selectedDetail,
+ score: row.score ? Number(row.score) : null,
+ comment: row.comment
+ })
+ }
+ })
+
+ return {
+ evaluationInfo: evaluationInfo[0],
+ reviewerDetails: Array.from(reviewerDetailsMap.values())
+ }
+
+ } catch (error) {
+ console.error("Error fetching evaluation details:", error)
+ throw new Error(
+ error instanceof Error
+ ? error.message
+ : "평가 상세 정보 조회 중 오류가 발생했습니다"
+ )
+ }
+} \ No newline at end of file