summaryrefslogtreecommitdiff
path: root/lib/evaluation-target-list/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/evaluation-target-list/service.ts')
-rw-r--r--lib/evaluation-target-list/service.ts616
1 files changed, 566 insertions, 50 deletions
diff --git a/lib/evaluation-target-list/service.ts b/lib/evaluation-target-list/service.ts
index 62f0f0ef..572b468d 100644
--- a/lib/evaluation-target-list/service.ts
+++ b/lib/evaluation-target-list/service.ts
@@ -1,13 +1,13 @@
'use server'
-import { and, or, desc, asc, ilike, eq, isNull, sql, count } from "drizzle-orm";
+import { and, or, desc, asc, ilike, eq, isNull, sql, count, inArray } from "drizzle-orm";
import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers";
import { filterColumns } from "@/lib/filter-columns";
import db from "@/db/db";
-import {
- evaluationTargets,
- evaluationTargetReviewers,
+import {
+ evaluationTargets,
+ evaluationTargetReviewers,
evaluationTargetReviews,
users,
vendors,
@@ -21,7 +21,9 @@ import {
} from "@/db/schema";
import { GetEvaluationTargetsSchema } from "./validation";
import { PgTransaction } from "drizzle-orm/pg-core";
-
+import { getServerSession } from "next-auth/next"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
+import { sendEmail } from "../mail/sendEmail";
export async function selectEvaluationTargetsFromView(
tx: PgTransaction<any, any, any>,
@@ -52,7 +54,7 @@ export async function countEvaluationTargetsFromView(
.select({ count: count() })
.from(evaluationTargetsWithDepartments)
.where(where);
-
+
return res[0]?.count ?? 0;
}
@@ -102,9 +104,9 @@ export async function getEvaluationTargets(input: GetEvaluationTargetsSchema) {
// 정렬 (View 테이블 기준)
const orderBy = input.sort.length > 0
? input.sort.map((item) => {
- const column = evaluationTargetsWithDepartments[item.id as keyof typeof evaluationTargetsWithDepartments];
- return item.desc ? desc(column) : asc(column);
- })
+ const column = evaluationTargetsWithDepartments[item.id as keyof typeof evaluationTargetsWithDepartments];
+ return item.desc ? desc(column) : asc(column);
+ })
: [desc(evaluationTargetsWithDepartments.createdAt)];
// 데이터 조회 - View 테이블 사용
@@ -196,7 +198,7 @@ export interface CreateEvaluationTargetInput {
// service.ts 파일의 CreateEvaluationTargetInput 타입 수정
export interface CreateEvaluationTargetInput {
evaluationYear: number
- division: "OCEAN" | "SHIPYARD"
+ division: "PLANT" | "SHIP"
vendorId: number
materialType: "EQUIPMENT" | "BULK" | "EQUIPMENT_BULK"
adminComment?: string
@@ -216,17 +218,16 @@ export async function createEvaluationTarget(
input: CreateEvaluationTargetInput,
createdBy: number
) {
-
- console.log(input,"input")
+ console.log(input, "input")
try {
return await db.transaction(async (tx) => {
- // 벤더 정보 조회 (기존과 동일)
+ // 벤더 정보 조회
const vendor = await tx
.select({
id: vendors.id,
vendorCode: vendors.vendorCode,
vendorName: vendors.vendorName,
- country: vendors.country,
+ country: vendors.country,
})
.from(vendors)
.where(eq(vendors.id, input.vendorId))
@@ -238,7 +239,7 @@ export async function createEvaluationTarget(
const vendorInfo = vendor[0];
- // 중복 체크 (기존과 동일)
+ // 중복 체크
const existing = await tx
.select({ id: evaluationTargets.id })
.from(evaluationTargets)
@@ -255,51 +256,57 @@ export async function createEvaluationTarget(
throw new Error("이미 동일한 평가 대상이 존재합니다.");
}
- // 평가 대상 생성 (기존과 동일)
+ // 🔧 수정: 타입 추론 문제 해결
+ const targetValues: typeof evaluationTargets.$inferInsert = {
+ evaluationYear: input.evaluationYear,
+ division: input.division,
+ vendorId: input.vendorId,
+ vendorCode: vendorInfo.vendorCode ?? '',
+ vendorName: vendorInfo.vendorName,
+ domesticForeign: vendorInfo.country === 'KR' ? 'DOMESTIC' : 'FOREIGN',
+ materialType: input.materialType,
+ status: 'PENDING',
+ adminComment: input.adminComment,
+ adminUserId: createdBy,
+ ldClaimCount: input.ldClaimCount ?? 0,
+ // 🔧 수정: decimal 타입은 숫자로 처리
+ ldClaimAmount: input.ldClaimAmount?.toString() ?? '0',
+ ldClaimCurrency: input.ldClaimCurrency ?? 'KRW',
+ }
+
+ console.log(targetValues)
+
+ // 평가 대상 생성
const newEvaluationTarget = await tx
.insert(evaluationTargets)
- .values({
- evaluationYear: input.evaluationYear,
- division: input.division,
- vendorId: input.vendorId,
- vendorCode: vendorInfo.vendorCode || "",
- vendorName: vendorInfo.vendorName,
- domesticForeign: vendorInfo.country === "KR" ? "DOMESTIC" : "FOREIGN",
- materialType: input.materialType,
- status: "PENDING",
- adminComment: input.adminComment,
- adminUserId: createdBy,
- ldClaimCount: input.ldClaimCount || 0,
- ldClaimAmount: input.ldClaimAmount?.toString() || "0",
- ldClaimCurrency: input.ldClaimCurrency || "KRW",
- })
+ .values(targetValues)
.returning({ id: evaluationTargets.id });
const evaluationTargetId = newEvaluationTarget[0].id;
- // ✅ 담당자들 지정 (departmentNameFrom 추가)
+ // 담당자들 지정
if (input.reviewers && input.reviewers.length > 0) {
- // 담당자들의 부서 정보 조회
const reviewerIds = input.reviewers.map(r => r.reviewerUserId);
+
+ // 🔧 수정: SQL 배열 처리 개선
const reviewerInfos = await tx
.select({
id: users.id,
- departmentName: users.departmentName, // users 테이블에 부서명 필드가 있다고 가정
})
.from(users)
- .where(sql`${users.id} = ANY(${reviewerIds})`);
-
- const reviewerAssignments = input.reviewers.map((reviewer) => {
- const reviewerInfo = reviewerInfos.find(info => info.id === reviewer.reviewerUserId);
-
- return {
- evaluationTargetId,
- departmentCode: reviewer.departmentCode,
- departmentNameFrom: reviewerInfo?.departmentName || null, // ✅ 실제 부서명 저장
- reviewerUserId: reviewer.reviewerUserId,
- assignedBy: createdBy,
- };
- });
+ .where(inArray(users.id, reviewerIds)); // sql 대신 inArray 사용
+
+ const reviewerAssignments: typeof evaluationTargetReviewers.$inferInsert[] =
+ input.reviewers.map(r => {
+ const info = reviewerInfos.find(i => i.id === r.reviewerUserId);
+ return {
+ evaluationTargetId,
+ departmentCode: r.departmentCode,
+ departmentNameFrom: info?.departmentName ?? "TEST 부서",
+ reviewerUserId: r.reviewerUserId,
+ assignedBy: createdBy,
+ };
+ });
await tx.insert(evaluationTargetReviewers).values(reviewerAssignments);
}
@@ -319,6 +326,253 @@ export async function createEvaluationTarget(
}
}
+//업데이트 입력 타입 정의
+export interface UpdateEvaluationTargetInput {
+ id: number
+ adminComment?: string
+ consolidatedComment?: string
+ ldClaimCount?: number
+ ldClaimAmount?: number
+ ldClaimCurrency?: "KRW" | "USD" | "EUR" | "JPY"
+ consensusStatus?: boolean | null
+ orderIsApproved?: boolean | null
+ procurementIsApproved?: boolean | null
+ qualityIsApproved?: boolean | null
+ designIsApproved?: boolean | null
+ csIsApproved?: boolean | null
+ // 담당자 이메일 변경
+ orderReviewerEmail?: string
+ procurementReviewerEmail?: string
+ qualityReviewerEmail?: string
+ designReviewerEmail?: string
+ csReviewerEmail?: string
+}
+
+export interface UpdateEvaluationTargetInput {
+ id: number
+ // 기본 정보
+ adminComment?: string
+ consolidatedComment?: string
+ ldClaimCount?: number
+ ldClaimAmount?: number
+ ldClaimCurrency?: "KRW" | "USD" | "EUR" | "JPY"
+ consensusStatus?: boolean | null
+
+ // 각 부서별 평가 결과
+ orderIsApproved?: boolean | null
+ procurementIsApproved?: boolean | null
+ qualityIsApproved?: boolean | null
+ designIsApproved?: boolean | null
+ csIsApproved?: boolean | null
+
+ // 담당자 이메일 (사용자 ID로 변환됨)
+ orderReviewerEmail?: string
+ procurementReviewerEmail?: string
+ qualityReviewerEmail?: string
+ designReviewerEmail?: string
+ csReviewerEmail?: string
+}
+
+export async function updateEvaluationTarget(input: UpdateEvaluationTargetInput) {
+ console.log(input, "update input")
+
+ try {
+ const session = await auth()
+ if (!session?.user) {
+ throw new Error("인증이 필요합니다.")
+ }
+
+ return await db.transaction(async (tx) => {
+ // 평가 대상 존재 확인
+ const existing = await tx
+ .select({ id: evaluationTargets.id })
+ .from(evaluationTargets)
+ .where(eq(evaluationTargets.id, input.id))
+ .limit(1)
+
+ if (!existing.length) {
+ throw new Error("평가 대상을 찾을 수 없습니다.")
+ }
+
+ // 1. 기본 정보 업데이트
+ const updateFields: Partial<typeof evaluationTargets.$inferInsert> = {}
+
+ if (input.adminComment !== undefined) {
+ updateFields.adminComment = input.adminComment
+ }
+ if (input.consolidatedComment !== undefined) {
+ updateFields.consolidatedComment = input.consolidatedComment
+ }
+ if (input.ldClaimCount !== undefined) {
+ updateFields.ldClaimCount = input.ldClaimCount
+ }
+ if (input.ldClaimAmount !== undefined) {
+ updateFields.ldClaimAmount = input.ldClaimAmount.toString()
+ }
+ if (input.ldClaimCurrency !== undefined) {
+ updateFields.ldClaimCurrency = input.ldClaimCurrency
+ }
+ if (input.consensusStatus !== undefined) {
+ updateFields.consensusStatus = input.consensusStatus
+ }
+
+ // 기본 정보가 있으면 업데이트
+ if (Object.keys(updateFields).length > 0) {
+ updateFields.updatedAt = new Date()
+
+ await tx
+ .update(evaluationTargets)
+ .set(updateFields)
+ .where(eq(evaluationTargets.id, input.id))
+ }
+
+ // 2. 담당자 정보 업데이트
+ const reviewerUpdates = [
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.ORDER_EVAL, email: input.orderReviewerEmail },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.PROCUREMENT_EVAL, email: input.procurementReviewerEmail },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.QUALITY_EVAL, email: input.qualityReviewerEmail },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.DESIGN_EVAL, email: input.designReviewerEmail },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.CS_EVAL, email: input.csReviewerEmail },
+ ]
+
+ for (const update of reviewerUpdates) {
+ if (update.email !== undefined) {
+ // 기존 담당자 제거
+ await tx
+ .delete(evaluationTargetReviewers)
+ .where(
+ and(
+ eq(evaluationTargetReviewers.evaluationTargetId, input.id),
+ eq(evaluationTargetReviewers.departmentCode, update.departmentCode)
+ )
+ )
+
+ // 새 담당자 추가 (이메일이 있는 경우만)
+ if (update.email) {
+ // 이메일로 사용자 ID 조회
+ const user = await tx
+ .select({ id: users.id })
+ .from(users)
+ .where(eq(users.email, update.email))
+ .limit(1)
+
+ if (user.length > 0) {
+ await tx
+ .insert(evaluationTargetReviewers)
+ .values({
+ evaluationTargetId: input.id,
+ departmentCode: update.departmentCode,
+ reviewerUserId: user[0].id,
+ assignedBy: session.user.id,
+ })
+ }
+ }
+ }
+ }
+
+ // 3. 평가 결과 업데이트
+ const reviewUpdates = [
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.ORDER_EVAL, isApproved: input.orderIsApproved },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.PROCUREMENT_EVAL, isApproved: input.procurementIsApproved },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.QUALITY_EVAL, isApproved: input.qualityIsApproved },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.DESIGN_EVAL, isApproved: input.designIsApproved },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.CS_EVAL, isApproved: input.csIsApproved },
+ ]
+
+ for (const review of reviewUpdates) {
+ if (review.isApproved !== undefined) {
+ // 해당 부서의 담당자 조회
+ const reviewer = await tx
+ .select({
+ reviewerUserId: evaluationTargetReviewers.reviewerUserId
+ })
+ .from(evaluationTargetReviewers)
+ .where(
+ and(
+ eq(evaluationTargetReviewers.evaluationTargetId, input.id),
+ eq(evaluationTargetReviewers.departmentCode, review.departmentCode)
+ )
+ )
+ .limit(1)
+
+ if (reviewer.length > 0) {
+ // 기존 평가 결과 삭제
+ await tx
+ .delete(evaluationTargetReviews)
+ .where(
+ and(
+ eq(evaluationTargetReviews.evaluationTargetId, input.id),
+ eq(evaluationTargetReviews.reviewerUserId, reviewer[0].reviewerUserId)
+ )
+ )
+
+ // 새 평가 결과 추가 (null이 아닌 경우만)
+ if (review.isApproved !== null) {
+ await tx
+ .insert(evaluationTargetReviews)
+ .values({
+ evaluationTargetId: input.id,
+ reviewerUserId: reviewer[0].reviewerUserId,
+ departmentCode: review.departmentCode,
+ isApproved: review.isApproved,
+ reviewedAt: new Date(),
+ })
+ }
+ }
+ }
+ }
+
+ // 4. 의견 일치 상태 및 전체 상태 자동 계산
+ const currentReviews = await tx
+ .select({
+ isApproved: evaluationTargetReviews.isApproved,
+ departmentCode: evaluationTargetReviews.departmentCode,
+ })
+ .from(evaluationTargetReviews)
+ .where(eq(evaluationTargetReviews.evaluationTargetId, input.id))
+
+ console.log("Current reviews:", currentReviews)
+
+ // 최소 3개 부서에서 평가가 완료된 경우 의견 일치 상태 계산
+ if (currentReviews.length >= 3) {
+ const approvals = currentReviews.map(r => r.isApproved)
+ const allApproved = approvals.every(approval => approval === true)
+ const allRejected = approvals.every(approval => approval === false)
+ const hasConsensus = allApproved || allRejected
+
+ let newStatus: "PENDING" | "CONFIRMED" | "EXCLUDED" = "PENDING"
+ if (hasConsensus) {
+ newStatus = allApproved ? "CONFIRMED" : "EXCLUDED"
+ }
+
+ console.log("Auto-updating status:", { hasConsensus, newStatus, approvals })
+
+ await tx
+ .update(evaluationTargets)
+ .set({
+ consensusStatus: hasConsensus,
+ status: newStatus,
+ confirmedAt: hasConsensus ? new Date() : null,
+ confirmedBy: hasConsensus ? session.user.id : null,
+ updatedAt: new Date()
+ })
+ .where(eq(evaluationTargets.id, input.id))
+ }
+
+ return {
+ success: true,
+ message: "평가 대상이 성공적으로 수정되었습니다.",
+ }
+ })
+ } catch (error) {
+ console.error("Error updating evaluation target:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "평가 대상 수정 중 오류가 발생했습니다.",
+ }
+ }
+}
+
// 담당자 목록 조회 시 부서 정보도 함께 반환
export async function getAvailableReviewers(departmentCode?: string) {
try {
@@ -358,9 +612,9 @@ export async function getAvailableVendors(search?: string) {
// 검색어가 있으면 적용
search
? or(
- ilike(vendors.vendorCode, `%${search}%`),
- ilike(vendors.vendorName, `%${search}%`)
- )
+ ilike(vendors.vendorCode, `%${search}%`),
+ ilike(vendors.vendorName, `%${search}%`)
+ )
: undefined
)
)
@@ -392,4 +646,266 @@ export async function getDepartmentInfo() {
key,
};
});
+}
+
+
+export async function confirmEvaluationTargets(targetIds: number[]) {
+ try {
+ const session = await getServerSession(authOptions)
+
+ if (!session?.user) {
+ return { success: false, error: "인증이 필요합니다." }
+ }
+
+ if (targetIds.length === 0) {
+ return { success: false, error: "선택된 평가 대상이 없습니다." }
+ }
+
+ // 트랜잭션으로 처리
+ await db.transaction(async (tx) => {
+ // 확정 가능한 대상들 확인 (PENDING 상태이면서 consensusStatus가 true인 것들)
+ const eligibleTargets = await tx
+ .select()
+ .from(evaluationTargets)
+ .where(
+ and(
+ inArray(evaluationTargets.id, targetIds),
+ eq(evaluationTargets.status, "PENDING"),
+ eq(evaluationTargets.consensusStatus, true)
+ )
+ )
+
+ if (eligibleTargets.length === 0) {
+ throw new Error("확정 가능한 평가 대상이 없습니다. (의견 일치 상태인 대기중 항목만 확정 가능)")
+ }
+
+ // 상태를 CONFIRMED로 변경
+ const confirmedTargetIds = eligibleTargets.map(target => target.id)
+ await tx
+ .update(evaluationTargets)
+ .set({
+ status: "CONFIRMED",
+ confirmedAt: new Date(),
+ confirmedBy: Number(session.user.id),
+ updatedAt: new Date()
+ })
+ .where(inArray(evaluationTargets.id, confirmedTargetIds))
+
+ return confirmedTargetIds
+ })
+
+
+ return {
+ success: true,
+ message: `${targetIds.length}개 평가 대상이 확정되었습니다.`,
+ confirmedCount: targetIds.length
+ }
+
+ } catch (error) {
+ console.error("Error confirming evaluation targets:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "확정 처리 중 오류가 발생했습니다."
+ }
+ }
+}
+
+export async function excludeEvaluationTargets(targetIds: number[]) {
+ try {
+ const session = await getServerSession(authOptions)
+ if (!session?.user) {
+ return { success: false, error: "인증이 필요합니다." }
+ }
+
+ if (targetIds.length === 0) {
+ return { success: false, error: "선택된 평가 대상이 없습니다." }
+ }
+
+ // 트랜잭션으로 처리
+ await db.transaction(async (tx) => {
+ // 제외 가능한 대상들 확인 (PENDING 상태인 것들)
+ const eligibleTargets = await tx
+ .select()
+ .from(evaluationTargets)
+ .where(
+ and(
+ inArray(evaluationTargets.id, targetIds),
+ eq(evaluationTargets.status, "PENDING")
+ )
+ )
+
+ if (eligibleTargets.length === 0) {
+ throw new Error("제외 가능한 평가 대상이 없습니다. (대기중 상태인 항목만 제외 가능)")
+ }
+
+ // 상태를 EXCLUDED로 변경
+ const excludedTargetIds = eligibleTargets.map(target => target.id)
+ await tx
+ .update(evaluationTargets)
+ .set({
+ status: "EXCLUDED",
+ updatedAt: new Date()
+ })
+ .where(inArray(evaluationTargets.id, excludedTargetIds))
+
+ return excludedTargetIds
+ })
+
+
+ return {
+ success: true,
+ message: `${targetIds.length}개 평가 대상이 제외되었습니다.`,
+ excludedCount: targetIds.length
+ }
+
+ } catch (error) {
+ console.error("Error excluding evaluation targets:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "제외 처리 중 오류가 발생했습니다."
+ }
+ }
+}
+
+export async function requestEvaluationReview(targetIds: number[], message?: string) {
+ try {
+ const session = await auth()
+ if (!session?.user) {
+ return { success: false, error: "인증이 필요합니다." }
+ }
+
+ if (targetIds.length === 0) {
+ return { success: false, error: "선택된 평가 대상이 없습니다." }
+ }
+
+ // 선택된 평가 대상들과 담당자 정보 조회
+ const targetsWithReviewers = await db
+ .select({
+ id: evaluationTargets.id,
+ vendorCode: evaluationTargets.vendorCode,
+ vendorName: evaluationTargets.vendorName,
+ materialType: evaluationTargets.materialType,
+ evaluationYear: evaluationTargets.evaluationYear,
+ status: evaluationTargets.status,
+ reviewerEmail: users.email,
+ reviewerName: users.name,
+ departmentCode: evaluationTargetReviewers.departmentCode,
+ departmentName: evaluationTargetReviewers.departmentNameFrom,
+ })
+ .from(evaluationTargets)
+ .leftJoin(
+ evaluationTargetReviewers,
+ eq(evaluationTargets.id, evaluationTargetReviewers.evaluationTargetId)
+ )
+ .leftJoin(
+ users,
+ eq(evaluationTargetReviewers.reviewerUserId, users.id)
+ )
+ .where(
+ and(
+ inArray(evaluationTargets.id, targetIds),
+ eq(evaluationTargets.status, "PENDING")
+ )
+ )
+
+ if (targetsWithReviewers.length === 0) {
+ return { success: false, error: "의견 요청 가능한 평가 대상이 없습니다." }
+ }
+
+ // 평가 대상별로 그룹화
+ const targetGroups = targetsWithReviewers.reduce((acc, item) => {
+ if (!acc[item.id]) {
+ acc[item.id] = {
+ id: item.id,
+ vendorCode: item.vendorCode,
+ vendorName: item.vendorName,
+ materialType: item.materialType,
+ evaluationYear: item.evaluationYear,
+ reviewers: []
+ }
+ }
+
+ if (item.reviewerEmail) {
+ acc[item.id].reviewers.push({
+ email: item.reviewerEmail,
+ name: item.reviewerName,
+ departmentCode: item.departmentCode,
+ departmentName: item.departmentName
+ })
+ }
+
+ return acc
+ }, {} as Record<number, any>)
+
+ const targets = Object.values(targetGroups)
+
+ // 모든 담당자 이메일 수집 (중복 제거)
+ const reviewerEmails = new Set<string>()
+ const reviewerInfo = new Map<string, { name: string; departments: string[] }>()
+
+ targets.forEach(target => {
+ target.reviewers.forEach((reviewer: any) => {
+ if (reviewer.email) {
+ reviewerEmails.add(reviewer.email)
+
+ if (!reviewerInfo.has(reviewer.email)) {
+ reviewerInfo.set(reviewer.email, {
+ name: reviewer.name || reviewer.email,
+ departments: []
+ })
+ }
+
+ const info = reviewerInfo.get(reviewer.email)!
+ if (reviewer.departmentName && !info.departments.includes(reviewer.departmentName)) {
+ info.departments.push(reviewer.departmentName)
+ }
+ }
+ })
+ })
+
+ if (reviewerEmails.size === 0) {
+ return { success: false, error: "담당자가 지정되지 않은 평가 대상입니다." }
+ }
+
+ // 각 담당자에게 이메일 발송
+ const emailPromises = Array.from(reviewerEmails).map(email => {
+ const reviewer = reviewerInfo.get(email)!
+
+ return sendEmail({
+ to: email,
+ subject: `벤더 평가 의견 요청 - ${targets.length}건`,
+ template: "evaluation-review-request",
+ context: {
+ requesterName: session.user.name || session.user.email,
+ reviewerName: reviewer.name,
+ targetCount: targets.length,
+ targets: targets.map(target => ({
+ vendorCode: target.vendorCode,
+ vendorName: target.vendorName,
+ materialType: target.materialType,
+ evaluationYear: target.evaluationYear
+ })),
+ message: message || "",
+ reviewUrl: `${process.env.NEXTAUTH_URL}/evaluation-targets`,
+ requestDate: new Date().toLocaleString('ko-KR')
+ }
+ })
+ })
+
+ await Promise.all(emailPromises)
+
+ revalidatePath("/evaluation-targets")
+ return {
+ success: true,
+ message: `${reviewerEmails.size}명의 담당자에게 의견 요청 이메일이 발송되었습니다.`,
+ emailCount: reviewerEmails.size
+ }
+
+ } catch (error) {
+ console.error("Error requesting evaluation review:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "의견 요청 중 오류가 발생했습니다."
+ }
+ }
} \ No newline at end of file