summaryrefslogtreecommitdiff
path: root/lib/evaluation-submit/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/evaluation-submit/service.ts')
-rw-r--r--lib/evaluation-submit/service.ts385
1 files changed, 341 insertions, 44 deletions
diff --git a/lib/evaluation-submit/service.ts b/lib/evaluation-submit/service.ts
index 84d356e7..99c5cb5e 100644
--- a/lib/evaluation-submit/service.ts
+++ b/lib/evaluation-submit/service.ts
@@ -12,11 +12,14 @@ import {
evaluationTargetReviewers,
evaluationTargets,
regEvalCriteria,
- periodicEvaluations
+ periodicEvaluations,
+ reviewerEvaluationAttachments,
+ users
} from "@/db/schema";
import { and, asc, desc, eq, ilike, or, SQL, count , sql, avg, isNotNull} from "drizzle-orm";
import { filterColumns } from "@/lib/filter-columns";
-import { DEPARTMENT_CATEGORY_MAPPING, EvaluationFormData, EvaluationQuestionItem, GetSHIEvaluationsSubmitSchema, REVIEWER_TYPES, ReviewerType } from "./validation";
+import { DEPARTMENT_CATEGORY_MAPPING, EvaluationFormData, GetSHIEvaluationsSubmitSchema, REVIEWER_TYPES, ReviewerType } from "./validation";
+import { AttachmentInfo, EvaluationQuestionItem } from "@/types/evaluation-form";
// ===============================================================================
@@ -128,7 +131,6 @@ export async function getEvaluationFormData(reviewerEvaluationId: number): Promi
const reviewerType = calculateReviewerType(evaluation.division, evaluation.materialType);
// 2. 부서에 따른 카테고리 필터링 로직
- // const categoryFilter = getCategoryFilterByDepartment("admin");
const categoryFilter = getCategoryFilterByDepartment(evaluation.departmentCode);
// 3. 해당 부서에 맞는 평가 기준들과 답변 옵션들 조회
@@ -179,15 +181,90 @@ export async function getEvaluationFormData(reviewerEvaluationId: number): Promi
updatedAt: reviewerEvaluationDetails.updatedAt,
})
.from(reviewerEvaluationDetails)
- .where(
- and(
- eq(reviewerEvaluationDetails.reviewerEvaluationId, reviewerEvaluationId),
- // ✅ null이 아닌 실제 응답만 조회
- isNotNull(reviewerEvaluationDetails.regEvalCriteriaDetailsId)
- )
- );
+ .where(eq(reviewerEvaluationDetails.reviewerEvaluationId, reviewerEvaluationId));
+
+ // 📎 5. 첨부파일 정보 조회
+ const attachmentsData = await db
+ .select({
+ // 첨부파일 정보
+ attachmentId: reviewerEvaluationAttachments.id,
+ originalFileName: reviewerEvaluationAttachments.originalFileName,
+ storedFileName: reviewerEvaluationAttachments.storedFileName,
+ publicPath: reviewerEvaluationAttachments.publicPath,
+ fileSize: reviewerEvaluationAttachments.fileSize,
+ mimeType: reviewerEvaluationAttachments.mimeType,
+ fileExtension: reviewerEvaluationAttachments.fileExtension,
+ description: reviewerEvaluationAttachments.description,
+ uploadedBy: reviewerEvaluationAttachments.uploadedBy,
+ attachmentCreatedAt: reviewerEvaluationAttachments.createdAt,
+ attachmentUpdatedAt: reviewerEvaluationAttachments.updatedAt,
+
+ // 업로드한 사용자 정보
+ uploadedByName: users.name,
+
+ // 평가 세부사항 정보 (어떤 질문에 대한 첨부파일인지 확인)
+ evaluationDetailId: reviewerEvaluationDetails.id,
+ regEvalCriteriaDetailsId: reviewerEvaluationDetails.regEvalCriteriaDetailsId,
+
+ // 평가 기준 정보 (질문 식별용)
+ criteriaId: regEvalCriteriaDetails.criteriaId,
+ category: regEvalCriteria.category,
+ })
+ .from(reviewerEvaluationAttachments)
+ .innerJoin(
+ reviewerEvaluationDetails,
+ eq(reviewerEvaluationAttachments.reviewerEvaluationDetailId, reviewerEvaluationDetails.id)
+ )
+ .leftJoin(
+ regEvalCriteriaDetails,
+ eq(reviewerEvaluationDetails.regEvalCriteriaDetailsId, regEvalCriteriaDetails.id)
+ )
+ .leftJoin(
+ regEvalCriteria,
+ eq(regEvalCriteriaDetails.criteriaId, regEvalCriteria.id)
+ )
+ .leftJoin(
+ users,
+ eq(reviewerEvaluationAttachments.uploadedBy, users.id)
+ )
+ .where(eq(reviewerEvaluationDetails.reviewerEvaluationId, reviewerEvaluationId))
+ .orderBy(desc(reviewerEvaluationAttachments.createdAt));
+
+ // 📎 6. 첨부파일을 질문별로 그룹화
+ const attachmentsByQuestion = new Map<number, AttachmentInfo[]>();
+ const attachmentsByCategory = new Map<string, number>();
+
+ attachmentsData.forEach(attachment => {
+ // Variable 타입 질문의 경우 criteriaId가 null일 수 있으므로 처리
+ const questionKey = attachment.criteriaId || attachment.evaluationDetailId;
+
+ if (!attachmentsByQuestion.has(questionKey)) {
+ attachmentsByQuestion.set(questionKey, []);
+ }
+
+ const attachmentInfo: AttachmentInfo = {
+ id: attachment.attachmentId,
+ originalFileName: attachment.originalFileName,
+ storedFileName: attachment.storedFileName,
+ publicPath: attachment.publicPath,
+ fileSize: attachment.fileSize,
+ mimeType: attachment.mimeType || undefined,
+ fileExtension: attachment.fileExtension || undefined,
+ description: attachment.description || undefined,
+ uploadedBy: attachment.uploadedBy,
+ uploadedByName: attachment.uploadedByName || undefined,
+ createdAt: new Date(attachment.attachmentCreatedAt),
+ updatedAt: new Date(attachment.attachmentUpdatedAt),
+ };
+
+ attachmentsByQuestion.get(questionKey)!.push(attachmentInfo);
+
+ // 카테고리별 파일 수 집계
+ const category = attachment.category || '기타';
+ attachmentsByCategory.set(category, (attachmentsByCategory.get(category) || 0) + 1);
+ });
- // 5. 질문별로 그룹화하고 답변 옵션들 정리
+ // 7. 질문별로 그룹화하고 답변 옵션들 정리
const questionsMap = new Map<number, EvaluationQuestionItem>();
criteriaWithDetails.forEach(record => {
@@ -201,6 +278,8 @@ export async function getEvaluationFormData(reviewerEvaluationId: number): Promi
// 질문이 이미 존재하는지 확인
if (!questionsMap.has(criteriaId)) {
+ const questionAttachments = attachmentsByQuestion.get(criteriaId) || [];
+
questionsMap.set(criteriaId, {
criteriaId: record.criteriaId,
category: record.category,
@@ -215,6 +294,11 @@ export async function getEvaluationFormData(reviewerEvaluationId: number): Promi
selectedDetailId: null, // ✅ 초기값은 null (아직 선택하지 않음)
currentScore: null,
currentComment: null,
+
+ // 📎 첨부파일 정보 추가
+ attachments: questionAttachments,
+ attachmentCount: questionAttachments.length,
+ attachmentTotalSize: questionAttachments.reduce((sum, att) => sum + att.fileSize, 0),
});
}
@@ -228,14 +312,45 @@ export async function getEvaluationFormData(reviewerEvaluationId: number): Promi
});
});
- // 6. ✅ 초기 응답 생성하지 않음 - 사용자가 실제로 답변할 때만 생성
+ // 8. ✅ Variable 타입 질문 처리 (첨부파일은 있지만 criteriaId가 null인 경우)
+ const variableTypeAttachments = new Map<number, AttachmentInfo[]>();
+ attachmentsData.forEach(attachment => {
+ if (!attachment.criteriaId && attachment.regEvalCriteriaDetailsId === null) {
+ // Variable 타입 질문의 첨부파일
+ if (!variableTypeAttachments.has(attachment.evaluationDetailId)) {
+ variableTypeAttachments.set(attachment.evaluationDetailId, []);
+ }
+ variableTypeAttachments.get(attachment.evaluationDetailId)!.push({
+ id: attachment.attachmentId,
+ originalFileName: attachment.originalFileName,
+ storedFileName: attachment.storedFileName,
+ publicPath: attachment.publicPath,
+ fileSize: attachment.fileSize,
+ mimeType: attachment.mimeType || undefined,
+ fileExtension: attachment.fileExtension || undefined,
+ description: attachment.description || undefined,
+ uploadedBy: attachment.uploadedBy,
+ uploadedByName: attachment.uploadedByName || undefined,
+ createdAt: new Date(attachment.attachmentCreatedAt),
+ updatedAt: new Date(attachment.attachmentUpdatedAt),
+ });
+ }
+ });
- // 7. 기존 응답 데이터를 질문에 매핑
- const existingResponsesMap = new Map(
- existingResponses.map(r => [r.regEvalCriteriaDetailsId, r])
- );
+ // 9. 기존 응답 데이터를 질문에 매핑
+ const existingResponsesMap = new Map<number | null, typeof existingResponses[0]>();
+ const responseDetailMap = new Map<number, typeof existingResponses[0]>();
- // 8. 각 질문에 현재 응답 정보 매핑
+ existingResponses.forEach(response => {
+ if (response.regEvalCriteriaDetailsId) {
+ existingResponsesMap.set(response.regEvalCriteriaDetailsId, response);
+ } else {
+ // Variable 타입 응답 (regEvalCriteriaDetailsId가 null)
+ responseDetailMap.set(response.id, response);
+ }
+ });
+
+ // 10. 각 질문에 현재 응답 정보 매핑
const questions: EvaluationQuestionItem[] = [];
questionsMap.forEach(question => {
// 현재 선택된 답변 찾기 (실제 응답이 있는 경우에만)
@@ -251,7 +366,7 @@ export async function getEvaluationFormData(reviewerEvaluationId: number): Promi
if (selectedResponse) {
question.responseId = selectedResponse.id;
- question.currentScore = selectedResponse.score;
+ question.currentScore = selectedResponse.score ? Number(selectedResponse.score) : null;
question.currentComment = selectedResponse.comment;
}
// ✅ else 케이스: 아직 답변하지 않은 상태 (모든 값이 null)
@@ -259,12 +374,21 @@ export async function getEvaluationFormData(reviewerEvaluationId: number): Promi
questions.push(question);
});
+ // 📎 11. 전체 첨부파일 통계 계산
+ const attachmentStats = {
+ totalFiles: attachmentsData.length,
+ totalSize: attachmentsData.reduce((sum, att) => sum + att.fileSize, 0),
+ questionsWithAttachments: attachmentsByQuestion.size + variableTypeAttachments.size,
+ filesByCategory: Object.fromEntries(attachmentsByCategory),
+ };
+
return {
evaluationInfo: {
...evaluation,
reviewerType
},
questions,
+ attachmentStats,
};
} catch (err) {
@@ -274,7 +398,6 @@ export async function getEvaluationFormData(reviewerEvaluationId: number): Promi
}
-
/**
* 평가 제출 목록을 조회합니다
*/
@@ -395,22 +518,33 @@ export async function getSHIEvaluationSubmissionById(id: number) {
/**
* 평가 응답을 업데이트합니다
*/
+// 기존 updateEvaluationResponse 함수를 확장하여 첨부파일 처리 지원
+
export async function updateEvaluationResponse(
reviewerEvaluationId: number,
selectedDetailId: number,
- comment?: string
+ comment?: string,
+ customScore?: number
) {
try {
- await db.transaction(async (tx) => {
- // 1. 선택된 답변 옵션의 정보 조회
- const selectedDetail = await tx
- .select()
- .from(regEvalCriteriaDetails)
- .where(eq(regEvalCriteriaDetails.id, selectedDetailId))
- .limit(1);
+ let reviewerEvaluationDetailId: number | null = null;
- if (selectedDetail.length === 0) {
- throw new Error('Selected detail not found');
+ await db.transaction(async (tx) => {
+ // 1. 선택된 답변 옵션의 정보 조회 (variable 타입이 아닌 경우)
+ let selectedDetail = null;
+ let score: number;
+
+ if (selectedDetailId !== -1) {
+ const detailResult = await tx
+ .select()
+ .from(regEvalCriteriaDetails)
+ .where(eq(regEvalCriteriaDetails.id, selectedDetailId))
+ .limit(1);
+
+ if (detailResult.length === 0) {
+ throw new Error('Selected detail not found');
+ }
+ selectedDetail = detailResult[0];
}
// 2. reviewerEvaluation 정보 조회 (periodicEvaluationId 포함)
@@ -447,20 +581,37 @@ export async function updateEvaluationResponse(
.where(eq(periodicEvaluations.id, periodicEvaluationId));
}
- // 4. 리뷰어 타입 정보 조회
- const evaluationInfo = await getEvaluationFormData(reviewerEvaluationId);
- if (!evaluationInfo) {
- throw new Error('Evaluation not found');
+ // 4. 점수 결정
+ if (selectedDetailId === -1) {
+ // variable 타입인 경우 customScore 사용
+ if (customScore === undefined) {
+ throw new Error('Custom score is required for variable type');
+ }
+ score = customScore;
+ } else {
+ // 일반 타입인 경우 리뷰어 타입에 맞는 점수 가져오기
+ const evaluationInfo = await getEvaluationFormData(reviewerEvaluationId);
+ if (!evaluationInfo) {
+ throw new Error('Evaluation not found');
+ }
+
+ const calculatedScore = getScoreByReviewerType(selectedDetail!, evaluationInfo.evaluationInfo.reviewerType);
+ if (calculatedScore === null) {
+ throw new Error('Score not found for this reviewer type');
+ }
+ score = calculatedScore;
}
- // 5. 해당 리뷰어 타입에 맞는 점수 가져오기
- const score = getScoreByReviewerType(selectedDetail[0], evaluationInfo.evaluationInfo.reviewerType);
- if (score === null) {
- throw new Error('Score not found for this reviewer type');
+ // 5. 해당 평가 기준에 대한 기존 응답들 삭제
+ let criteriaId: number;
+ if (selectedDetailId === -1) {
+ // variable 타입인 경우, criteriaId를 별도로 조회해야 함
+ // 이 부분은 실제 데이터 구조에 따라 조정 필요
+ throw new Error('Variable type criteria ID lookup not implemented');
+ } else {
+ criteriaId = selectedDetail!.criteriaId;
}
- // 6. 같은 질문에 대한 기존 응답들 삭제
- const criteriaId = selectedDetail[0].criteriaId;
await tx
.delete(reviewerEvaluationDetails)
.where(
@@ -472,27 +623,173 @@ export async function updateEvaluationResponse(
)
);
- // 7. 새로운 응답 생성
- await tx
+ // 6. 새로운 응답 생성
+ const [newDetail] = await tx
.insert(reviewerEvaluationDetails)
.values({
reviewerEvaluationId,
- regEvalCriteriaDetailsId: selectedDetailId,
+ regEvalCriteriaDetailsId: selectedDetailId === -1 ? null : selectedDetailId,
score: score.toString(),
comment,
+ })
+ .returning({
+ id: reviewerEvaluationDetails.id,
});
- // 8. 카테고리별 점수 계산 및 총점 업데이트
+ reviewerEvaluationDetailId = newDetail.id;
+
+ // 7. 카테고리별 점수 계산 및 총점 업데이트
await recalculateEvaluationScores(tx, reviewerEvaluationId);
});
- return { success: true };
+ return {
+ success: true,
+ reviewerEvaluationDetailId
+ };
} catch (err) {
console.error('Error in updateEvaluationResponse:', err);
throw err;
}
}
+// variable 타입을 위한 별도 함수
+export async function updateVariableEvaluationResponse(
+ reviewerEvaluationId: number,
+ criteriaId: number,
+ score: number,
+ comment?: string
+) {
+ try {
+ let reviewerEvaluationDetailId: number | null = null;
+
+ await db.transaction(async (tx) => {
+ // 1. reviewerEvaluation 정보 조회
+ const reviewerEvaluationInfo = await tx
+ .select({
+ periodicEvaluationId: reviewerEvaluations.periodicEvaluationId,
+ })
+ .from(reviewerEvaluations)
+ .where(eq(reviewerEvaluations.id, reviewerEvaluationId))
+ .limit(1);
+
+ if (reviewerEvaluationInfo.length === 0) {
+ throw new Error('Reviewer evaluation not found');
+ }
+
+ const { periodicEvaluationId } = reviewerEvaluationInfo[0];
+
+ // 2. 상태 업데이트
+ const currentStatus = await tx
+ .select({
+ status: periodicEvaluations.status,
+ })
+ .from(periodicEvaluations)
+ .where(eq(periodicEvaluations.id, periodicEvaluationId))
+ .limit(1);
+
+ if (currentStatus.length > 0 && currentStatus[0].status !== "IN_REVIEW") {
+ await tx
+ .update(periodicEvaluations)
+ .set({
+ status: "IN_REVIEW",
+ updatedAt: new Date(),
+ })
+ .where(eq(periodicEvaluations.id, periodicEvaluationId));
+ }
+
+ // 3. 해당 평가 기준에 대한 기존 응답들 삭제
+ await tx
+ .delete(reviewerEvaluationDetails)
+ .where(
+ and(
+ eq(reviewerEvaluationDetails.reviewerEvaluationId, reviewerEvaluationId),
+ sql`${reviewerEvaluationDetails.regEvalCriteriaDetailsId} IN (
+ SELECT id FROM reg_eval_criteria_details WHERE criteria_id = ${criteriaId}
+ )`
+ )
+ );
+
+ // 4. 새로운 응답 생성 (variable 타입은 regEvalCriteriaDetailsId가 null)
+ const [newDetail] = await tx
+ .insert(reviewerEvaluationDetails)
+ .values({
+ reviewerEvaluationId,
+ regEvalCriteriaDetailsId: null, // variable 타입은 null
+ score: score.toString(),
+ comment,
+ })
+ .returning({
+ id: reviewerEvaluationDetails.id,
+ });
+
+ reviewerEvaluationDetailId = newDetail.id;
+
+ // 5. 점수 재계산
+ await recalculateEvaluationScores(tx, reviewerEvaluationId);
+ });
+
+ return {
+ success: true,
+ reviewerEvaluationDetailId
+ };
+ } catch (err) {
+ console.error('Error in updateVariableEvaluationResponse:', err);
+ throw err;
+ }
+}
+
+// 첨부파일과 함께 평가 응답 업데이트
+// export async function updateEvaluationResponseWithAttachment(
+// reviewerEvaluationId: number,
+// selectedDetailId: number,
+// comment?: string,
+// customScore?: number,
+// attachmentFile?: File,
+// attachmentDescription?: string
+// ) {
+// try {
+// // 1. 먼저 평가 응답 업데이트
+// const updateResult = selectedDetailId === -1 && customScore !== undefined ?
+// await updateVariableEvaluationResponse(reviewerEvaluationId, /* criteriaId 필요 */, customScore, comment) :
+// await updateEvaluationResponse(reviewerEvaluationId, selectedDetailId, comment, customScore);
+
+// if (!updateResult.success || !updateResult.reviewerEvaluationDetailId) {
+// throw new Error('Failed to update evaluation response');
+// }
+
+// // 2. 첨부파일이 있으면 저장
+// if (attachmentFile) {
+// const fileResult = await saveFile({
+// file: attachmentFile,
+// directory: "evaluation-attachments",
+// originalName: attachmentFile.name,
+// });
+
+// if (!fileResult.success) {
+// throw new Error(fileResult.error || "파일 저장에 실패했습니다.");
+// }
+
+// // 3. DB에 첨부파일 정보 저장
+// await db.insert(reviewerEvaluationAttachments).values({
+// reviewerEvaluationDetailId: updateResult.reviewerEvaluationDetailId,
+// originalFileName: attachmentFile.name,
+// storedFileName: fileResult.fileName!,
+// filePath: fileResult.filePath!,
+// publicPath: fileResult.publicPath!,
+// fileSize: attachmentFile.size,
+// mimeType: attachmentFile.type,
+// fileExtension: attachmentFile.name.split('.').pop()?.toLowerCase() || '',
+// description: attachmentDescription || null,
+// uploadedBy: /* session.user.id 필요 */,
+// });
+// }
+
+// return { success: true };
+// } catch (err) {
+// console.error('Error in updateEvaluationResponseWithAttachment:', err);
+// throw err;
+// }
+// }
/**
* 평가 점수 재계산