From 4eb7532f822c821fb6b69bf103bd075fefba769b Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 15 Jul 2025 10:07:09 +0000 Subject: (대표님) 20250715 협력사 정기평가, spreadJS, roles 서비스에 함수 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/admin-users/table/ausers-table.tsx | 2 + .../add-basic-contract-template-dialog.tsx | 406 +++++++++++---------- lib/evaluation-submit/service.ts | 9 +- .../table/evaluation-submissions-table-columns.tsx | 4 +- lib/evaluation-submit/table/submit-table.tsx | 3 - lib/evaluation-target-list/service.ts | 301 +++++++-------- lib/evaluation/service.ts | 18 +- lib/evaluation/table/evaluation-columns.tsx | 1 + lib/evaluation/table/evaluation-filter-sheet.tsx | 14 +- lib/evaluation/table/evaluation-table.tsx | 1 - .../table/periodic-evaluation-action-dialogs.tsx | 2 +- .../table/periodic-evaluations-toolbar-actions.tsx | 2 +- lib/roles/services.ts | 104 +++++- lib/roles/table/add-role-dialog.tsx | 248 ++++++++----- lib/roles/table/assign-roles-sheet.tsx | 168 +++++++-- lib/roles/table/roles-table.tsx | 60 ++- lib/roles/table/update-roles-sheet.tsx | 2 +- lib/roles/userTable/assignedUsers-table.tsx | 127 ++++++- .../tech-sales-quotation-attachments-sheet.tsx | 4 + .../table/tech-sales-rfq-attachments-sheet.tsx | 6 +- lib/users/service.ts | 147 +++++++- lib/users/table/assign-roles-dialog.tsx | 353 +++++++++++++++--- lib/users/table/users-table-toolbar-actions.tsx | 6 +- lib/users/table/users-table.tsx | 5 +- .../ship/enhanced-doc-table-toolbar-actions.tsx | 32 +- .../ship/send-to-shi-button.tsx | 336 ++++++++++++----- lib/vendor-evaluation-submit/service.ts | 42 ++- .../table/esg-evaluation-form-sheet.tsx | 78 ++-- .../table/general-evaluation-form-sheet.tsx | 1 + 29 files changed, 1715 insertions(+), 767 deletions(-) (limited to 'lib') diff --git a/lib/admin-users/table/ausers-table.tsx b/lib/admin-users/table/ausers-table.tsx index ed575e75..1e254b5c 100644 --- a/lib/admin-users/table/ausers-table.tsx +++ b/lib/admin-users/table/ausers-table.tsx @@ -45,6 +45,8 @@ export function AdmUserTable({ promises }: UsersTableProps) { React.use(promises) + console.log(roles,"roles") + const [rowAction, setRowAction] = React.useState | null>(null) diff --git a/lib/basic-contract/template/add-basic-contract-template-dialog.tsx b/lib/basic-contract/template/add-basic-contract-template-dialog.tsx index 3a83d50f..c88819e4 100644 --- a/lib/basic-contract/template/add-basic-contract-template-dialog.tsx +++ b/lib/basic-contract/template/add-basic-contract-template-dialog.tsx @@ -73,9 +73,12 @@ const templateFormSchema = z.object({ status: z.enum(["ACTIVE", "DISPOSED"]).default("ACTIVE"), }).refine((data) => { // 적어도 하나의 적용 범위는 선택되어야 함 - const hasAnyScope = BUSINESS_UNITS.some(unit => - data[unit.key as keyof typeof data] as boolean - ); + const scopeFields = [ + 'shipBuildingApplicable', 'windApplicable', 'pcApplicable', 'nbApplicable', + 'rcApplicable', 'gyApplicable', 'sysApplicable', 'infraApplicable' + ]; + + const hasAnyScope = scopeFields.some(field => data[field as keyof typeof data] === true); return hasAnyScope; }, { message: "적어도 하나의 적용 범위를 선택해야 합니다.", @@ -274,42 +277,85 @@ export function AddTemplateDialog() { 템플릿 추가 - - + + {/* 고정된 헤더 */} + 새 기본계약서 템플릿 추가 템플릿 정보를 입력하고 계약서 파일을 업로드하세요. * 표시된 항목은 필수 입력사항입니다. -
- - {/* 기본 정보 */} - - - 기본 정보 - - -
+ + {/* 스크롤 가능한 컨텐츠 영역 */} +
+ + + {/* 기본 정보 */} + + + 기본 정보 + + +
+ ( + + + 템플릿 코드 * + + + field.onChange(e.target.value.toUpperCase())} + /> + + + 영문 대문자, 숫자, '_', '-'만 사용 가능 + + + + )} + /> + + ( + + 리비전 + + field.onChange(parseInt(e.target.value) || 1)} + /> + + + 템플릿 버전 (기본값: 1) + + + + )} + /> +
+ ( - 템플릿 코드 * + 템플릿 이름 * - field.onChange(e.target.value.toUpperCase())} - /> + - - 영문 대문자, 숫자, '_', '-'만 사용 가능 - )} @@ -317,191 +363,157 @@ export function AddTemplateDialog() { ( - - 리비전 + +
+ 법무검토 필요 + + 법무팀 검토가 필요한 템플릿인지 설정 + +
- field.onChange(parseInt(e.target.value) || 1)} + - - 템플릿 버전 (기본값: 1) - -
)} /> -
- - ( - - - 템플릿 이름 * - - - - - - - )} - /> + + - ( - -
- 법무검토 필요 - - 법무팀 검토가 필요한 템플릿인지 설정 - -
- - - -
+ {/* 적용 범위 */} + + + + 적용 범위 * + + + 이 템플릿이 적용될 사업부를 선택하세요. ({selectedScopesCount}개 선택됨) + + + +
+ + +
+ + + +
+ {BUSINESS_UNITS.map((unit) => ( + ( + + + + +
+ + {unit.label} + +
+
+ )} + /> + ))} +
+ + {form.formState.errors.shipBuildingApplicable && ( +

+ {form.formState.errors.shipBuildingApplicable.message} +

)} - /> -
-
+ + - {/* 적용 범위 */} - - - 적용 범위 - - 이 템플릿이 적용될 사업부를 선택하세요. ({selectedScopesCount}개 선택됨) - - - -
- + + 파일 업로드 + + + ( + + + 계약서 파일 * + + + + + + + {selectedFile ? selectedFile.name : "PDF 파일을 여기에 드래그하세요"} + + + {selectedFile + ? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB` + : "또는 클릭하여 PDF 파일을 선택하세요 (최대 100MB)"} + + + + + + + + )} /> - -
- - - -
- {BUSINESS_UNITS.map((unit) => ( - ( - - - - -
- - {unit.label} - -
-
- )} - /> - ))} -
- - {form.formState.errors.shipBuildingApplicable && ( -

- {form.formState.errors.shipBuildingApplicable.message} -

- )} -
-
- - {/* 파일 업로드 */} - - - 파일 업로드 - - - ( - - - 계약서 파일 * - - - - - - - {selectedFile ? selectedFile.name : "PDF 파일을 여기에 드래그하세요"} - - - {selectedFile - ? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB` - : "또는 클릭하여 PDF 파일을 선택하세요 (최대 100MB)"} - - - - - - - - )} - /> - - {showProgress && ( -
-
- 업로드 진행률 - {uploadProgress}% + + {showProgress && ( +
+
+ 업로드 진행률 + {uploadProgress}% +
+
- -
- )} - - + )} + + + + +
- - - - - - + {/* 고정된 푸터 */} + + + + ); diff --git a/lib/evaluation-submit/service.ts b/lib/evaluation-submit/service.ts index 99c5cb5e..21ceb36f 100644 --- a/lib/evaluation-submit/service.ts +++ b/lib/evaluation-submit/service.ts @@ -16,7 +16,7 @@ import { reviewerEvaluationAttachments, users } from "@/db/schema"; -import { and, asc, desc, eq, ilike, or, SQL, count , sql, avg, isNotNull} from "drizzle-orm"; +import { and, inArray, asc, desc, eq, ilike, or, SQL, count , sql, avg, isNotNull} from "drizzle-orm"; import { filterColumns } from "@/lib/filter-columns"; import { DEPARTMENT_CATEGORY_MAPPING, EvaluationFormData, GetSHIEvaluationsSubmitSchema, REVIEWER_TYPES, ReviewerType } from "./validation"; import { AttachmentInfo, EvaluationQuestionItem } from "@/types/evaluation-form"; @@ -421,18 +421,18 @@ export async function getSHIEvaluationSubmissions(input: GetSHIEvaluationsSubmit ); } - const existingReviewer = await db.query.evaluationTargetReviewers.findFirst({ + const existingReviewer = await db.query.evaluationTargetReviewers.findMany({ where: eq(evaluationTargetReviewers.reviewerUserId, userId), }); - const finalWhere = and( advancedWhere, globalWhere, - eq(reviewerEvaluationsView.evaluationTargetReviewerId, existingReviewer?.id), + inArray(reviewerEvaluationsView.evaluationTargetReviewerId, existingReviewer.map(e => e.id)), ); + // 정렬 const orderBy = input.sort.length > 0 ? input.sort.map((item) => { @@ -458,7 +458,6 @@ export async function getSHIEvaluationSubmissions(input: GetSHIEvaluationsSubmit .select({ count: count() }) .from(reviewerEvaluationsView) .where(finalWhere); - const total = totalResult[0]?.count || 0; return { data, total }; diff --git a/lib/evaluation-submit/table/evaluation-submissions-table-columns.tsx b/lib/evaluation-submit/table/evaluation-submissions-table-columns.tsx index 8d097aff..73c4f378 100644 --- a/lib/evaluation-submit/table/evaluation-submissions-table-columns.tsx +++ b/lib/evaluation-submit/table/evaluation-submissions-table-columns.tsx @@ -342,14 +342,14 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef
최종: {parseFloat(finalScore.toString()).toFixed(1)}점
- {finalGrade} + {/* {finalGrade} */}
) : evaluationScore && evaluationGrade ? (
{parseFloat(evaluationScore.toString()).toFixed(1)}점
- {evaluationGrade} + {/* {evaluationGrade} */}
) : ( 미산정 diff --git a/lib/evaluation-submit/table/submit-table.tsx b/lib/evaluation-submit/table/submit-table.tsx index 9000c48b..a1d917fd 100644 --- a/lib/evaluation-submit/table/submit-table.tsx +++ b/lib/evaluation-submit/table/submit-table.tsx @@ -33,9 +33,6 @@ export function SHIEvaluationSubmissionsTable({ promises }: EvaluationSubmission }>({ data: [], pageCount: 0 }) const router = useRouter() - console.log(tableData) - - // 2. 행 액션 상태 관리 const [rowAction, setRowAction] = React.useState | null>(null) diff --git a/lib/evaluation-target-list/service.ts b/lib/evaluation-target-list/service.ts index 4559374b..251561f9 100644 --- a/lib/evaluation-target-list/service.ts +++ b/lib/evaluation-target-list/service.ts @@ -345,7 +345,7 @@ export async function createEvaluationTarget( // 담당자들 지정 if (input.reviewers && input.reviewers.length > 0) { const reviewerIds = input.reviewers.map(r => r.reviewerUserId); - + // 🔧 수정: SQL 배열 처리 개선 const reviewerInfos = await tx .select({ @@ -354,26 +354,26 @@ export async function createEvaluationTarget( .from(users) .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, - }; - }), - // session user 추가 - { + const reviewerAssignments: typeof evaluationTargetReviewers.$inferInsert[] = [ + ...input.reviewers.map(r => { + const info = reviewerInfos.find(i => i.id === r.reviewerUserId); + return { evaluationTargetId, - departmentCode: "admin", - departmentNameFrom: "정기평가 관리자", - reviewerUserId: Number(session.user.id), + departmentCode: r.departmentCode, + departmentNameFrom: info?.departmentName ?? "TEST 부서", + reviewerUserId: r.reviewerUserId, assignedBy: createdBy, - } - ]; + }; + }), + // session user 추가 + { + evaluationTargetId, + departmentCode: "admin", + departmentNameFrom: "정기평가 관리자", + reviewerUserId: Number(session.user.id), + assignedBy: createdBy, + } + ]; await tx.insert(evaluationTargetReviewers).values(reviewerAssignments); } @@ -423,14 +423,14 @@ export interface UpdateEvaluationTargetInput { 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 @@ -441,7 +441,7 @@ export interface UpdateEvaluationTargetInput { export async function updateEvaluationTarget(input: UpdateEvaluationTargetInput) { console.log(input, "update input") - + try { const session = await getServerSession(authOptions) @@ -486,7 +486,7 @@ export async function updateEvaluationTarget(input: UpdateEvaluationTargetInput) // 기본 정보가 있으면 업데이트 if (Object.keys(updateFields).length > 0) { updateFields.updatedAt = new Date() - + await tx .update(evaluationTargets) .set(updateFields) @@ -530,7 +530,7 @@ export async function updateEvaluationTarget(input: UpdateEvaluationTargetInput) evaluationTargetId: input.id, departmentCode: update.departmentCode, reviewerUserId: Number(user[0].id), - assignedBy:Number( session.user.id), + assignedBy: Number(session.user.id), }) } } @@ -550,8 +550,8 @@ export async function updateEvaluationTarget(input: UpdateEvaluationTargetInput) if (review.isApproved !== undefined) { // 해당 부서의 담당자 조회 const reviewer = await tx - .select({ - reviewerUserId: evaluationTargetReviewers.reviewerUserId + .select({ + reviewerUserId: evaluationTargetReviewers.reviewerUserId }) .from(evaluationTargetReviewers) .where( @@ -598,10 +598,25 @@ export async function updateEvaluationTarget(input: UpdateEvaluationTargetInput) .from(evaluationTargetReviews) .where(eq(evaluationTargetReviews.evaluationTargetId, input.id)) - console.log("Current reviews:", currentReviews) + + const evaluationTargetForConcensus = await tx + .select({ + materialType: evaluationTargets.materialType, + }) + .from(evaluationTargets) + .where(eq(evaluationTargets.id, input.id)) + .limit(1) + + if (evaluationTargetForConcensus.length === 0) { + throw new Error("평가 대상을 찾을 수 없습니다.") + } + + const { materialType } = evaluationTargetForConcensus[0] + const minimumReviewsRequired = materialType === "BULK" ? 3 : 5 + // 최소 3개 부서에서 평가가 완료된 경우 의견 일치 상태 계산 - if (currentReviews.length >= 3) { + if (currentReviews.length >= minimumReviewsRequired) { const approvals = currentReviews.map(r => r.isApproved) const allApproved = approvals.every(approval => approval === true) const allRejected = approvals.every(approval => approval === false) @@ -617,7 +632,7 @@ export async function updateEvaluationTarget(input: UpdateEvaluationTargetInput) await tx .update(evaluationTargets) - .set({ + .set({ consensusStatus: hasConsensus, confirmedAt: hasConsensus ? new Date() : null, confirmedBy: hasConsensus ? Number(session.user.id) : null, @@ -710,24 +725,24 @@ export async function getDepartmentInfo() { export async function confirmEvaluationTargets( - targetIds: number[], + targetIds: number[], evaluationPeriod?: string // "상반기", "하반기", "연간" 등 ) { try { const session = await getServerSession(authOptions) - + if (!session?.user) { return { success: false, error: "인증이 필요합니다." } } - + if (targetIds.length === 0) { return { success: false, error: "선택된 평가 대상이 없습니다." } } // 평가 기간이 없으면 현재 날짜 기준으로 자동 결정 // const currentPeriod = evaluationPeriod || getCurrentEvaluationPeriod() - const currentPeriod ="연간" - + const currentPeriod = "연간" + // 트랜잭션으로 처리 const result = await db.transaction(async (tx) => { // 확정 가능한 대상들 확인 (PENDING 상태이면서 consensusStatus가 true인 것들) @@ -741,13 +756,13 @@ export async function confirmEvaluationTargets( eq(evaluationTargets.consensusStatus, true) ) ) - + if (eligibleTargets.length === 0) { throw new Error("확정 가능한 평가 대상이 없습니다. (의견 일치 상태인 대기중 항목만 확정 가능)") } - + const confirmedTargetIds = eligibleTargets.map(target => target.id) - + // 1. 평가 대상 상태를 CONFIRMED로 변경 await tx .update(evaluationTargets) @@ -758,10 +773,10 @@ export async function confirmEvaluationTargets( updatedAt: new Date() }) .where(inArray(evaluationTargets.id, confirmedTargetIds)) - + // 2. 각 확정된 평가 대상에 대해 periodicEvaluations 레코드 생성 const periodicEvaluationsToCreate = [] - + for (const target of eligibleTargets) { // 이미 해당 기간에 평가가 존재하는지 확인 const existingEvaluation = await tx @@ -774,7 +789,7 @@ export async function confirmEvaluationTargets( ) ) .limit(1) - + // 없으면 생성 목록에 추가 if (existingEvaluation.length === 0) { periodicEvaluationsToCreate.push({ @@ -782,14 +797,14 @@ export async function confirmEvaluationTargets( evaluationPeriod: currentPeriod, // 평가년도에 따른 제출 마감일 설정 (예: 상반기는 7월 말, 하반기는 1월 말) submissionDeadline: getSubmissionDeadline(target.evaluationYear, currentPeriod), - status: "PENDING_SUBMISSION" as const, + status: "PENDING" as const, createdAt: new Date(), updatedAt: new Date() }) } console.log("periodicEvaluationsToCreate", periodicEvaluationsToCreate) } - + // 3. periodicEvaluations 레코드들 일괄 생성 let createdEvaluationsCount = 0 if (periodicEvaluationsToCreate.length > 0) { @@ -797,7 +812,7 @@ export async function confirmEvaluationTargets( .insert(periodicEvaluations) .values(periodicEvaluationsToCreate) .returning({ id: periodicEvaluations.id }) - + createdEvaluationsCount = createdEvaluations.length } console.log("createdEvaluationsCount", createdEvaluationsCount) @@ -807,13 +822,13 @@ export async function confirmEvaluationTargets( tx.select({ count: count() }) .from(generalEvaluations) .where(eq(generalEvaluations.isActive, true)), - + // 활성화된 ESG 평가항목 수 tx.select({ count: count() }) .from(esgEvaluationItems) .where(eq(esgEvaluationItems.isActive, true)) ]) - + const totalGeneralItems = generalItemsCount[0]?.count || 0 const totalEsgItems = esgItemsCount[0]?.count || 0 @@ -832,7 +847,7 @@ export async function confirmEvaluationTargets( // eq(periodicEvaluations.evaluationPeriod, currentPeriod) // ) // ) - + // // 각 평가에 대해 담당자별 reviewerEvaluations 생성 // for (const periodicEval of newPeriodicEvaluations) { // // 해당 evaluationTarget의 담당자들 조회 @@ -840,7 +855,7 @@ export async function confirmEvaluationTargets( // .select() // .from(evaluationTargetReviewers) // .where(eq(evaluationTargetReviewers.evaluationTargetId, periodicEval.evaluationTargetId)) - + // if (reviewers.length > 0) { // const reviewerEvaluationsToCreate = reviewers.map(reviewer => ({ // periodicEvaluationId: periodicEval.id, @@ -849,102 +864,102 @@ export async function confirmEvaluationTargets( // createdAt: new Date(), // updatedAt: new Date() // })) - + // await tx // .insert(reviewerEvaluations) // .values(reviewerEvaluationsToCreate) // } // } // } - + // 6. 벤더별 evaluationSubmissions 레코드 생성 - const evaluationSubmissionsToCreate = [] - - // 생성된 periodicEvaluations의 ID를 매핑하기 위한 맵 생성 - const periodicEvaluationIdMap = new Map() - if (createdEvaluationsCount > 0) { - const createdEvaluations = await tx - .select({ - id: periodicEvaluations.id, - evaluationTargetId: periodicEvaluations.evaluationTargetId - }) - .from(periodicEvaluations) - .where( - and( - inArray(periodicEvaluations.evaluationTargetId, confirmedTargetIds), - eq(periodicEvaluations.evaluationPeriod, currentPeriod) - ) - ) - - // evaluationTargetId를 키로 하는 맵 생성 - createdEvaluations.forEach(periodicEval => { - periodicEvaluationIdMap.set(periodicEval.evaluationTargetId, periodicEval.id) - }) - } - console.log("periodicEvaluationIdMap", periodicEvaluationIdMap) - - for (const target of eligibleTargets) { - // 이미 해당 년도/기간에 제출 레코드가 있는지 확인 - const existingSubmission = await tx - .select({ id: evaluationSubmissions.id }) - .from(evaluationSubmissions) - .where( - and( - eq(evaluationSubmissions.companyId, target.vendorId), - eq(evaluationSubmissions.evaluationYear, target.evaluationYear), - // eq(evaluationSubmissions.evaluationRound, currentPeriod) - ) - ) - .limit(1) - - // 없으면 생성 목록에 추가 - if (existingSubmission.length === 0) { - const periodicEvaluationId = periodicEvaluationIdMap.get(target.id) - if (periodicEvaluationId) { - evaluationSubmissionsToCreate.push({ - companyId: target.vendorId, - periodicEvaluationId: periodicEvaluationId, - evaluationYear: target.evaluationYear, - evaluationRound: currentPeriod, - submissionStatus: "draft" as const, - totalGeneralItems: totalGeneralItems, - completedGeneralItems: 0, - totalEsgItems: totalEsgItems, - completedEsgItems: 0, - isActive: true, - createdAt: new Date(), - updatedAt: new Date() - }) - } - } - } - // 7. evaluationSubmissions 레코드들 일괄 생성 - let createdSubmissionsCount = 0 - if (evaluationSubmissionsToCreate.length > 0) { - const createdSubmissions = await tx - .insert(evaluationSubmissions) - .values(evaluationSubmissionsToCreate) - .returning({ id: evaluationSubmissions.id }) - - createdSubmissionsCount = createdSubmissions.length - } - + // const evaluationSubmissionsToCreate = [] + + // // 생성된 periodicEvaluations의 ID를 매핑하기 위한 맵 생성 + // const periodicEvaluationIdMap = new Map() + // if (createdEvaluationsCount > 0) { + // const createdEvaluations = await tx + // .select({ + // id: periodicEvaluations.id, + // evaluationTargetId: periodicEvaluations.evaluationTargetId + // }) + // .from(periodicEvaluations) + // .where( + // and( + // inArray(periodicEvaluations.evaluationTargetId, confirmedTargetIds), + // eq(periodicEvaluations.evaluationPeriod, currentPeriod) + // ) + // ) + + // // evaluationTargetId를 키로 하는 맵 생성 + // createdEvaluations.forEach(periodicEval => { + // periodicEvaluationIdMap.set(periodicEval.evaluationTargetId, periodicEval.id) + // }) + // } + // console.log("periodicEvaluationIdMap", periodicEvaluationIdMap) + + // for (const target of eligibleTargets) { + // // 이미 해당 년도/기간에 제출 레코드가 있는지 확인 + // const existingSubmission = await tx + // .select({ id: evaluationSubmissions.id }) + // .from(evaluationSubmissions) + // .where( + // and( + // eq(evaluationSubmissions.companyId, target.vendorId), + // eq(evaluationSubmissions.evaluationYear, target.evaluationYear), + // // eq(evaluationSubmissions.evaluationRound, currentPeriod) + // ) + // ) + // .limit(1) + + // // 없으면 생성 목록에 추가 + // if (existingSubmission.length === 0) { + // const periodicEvaluationId = periodicEvaluationIdMap.get(target.id) + // if (periodicEvaluationId) { + // evaluationSubmissionsToCreate.push({ + // companyId: target.vendorId, + // periodicEvaluationId: periodicEvaluationId, + // evaluationYear: target.evaluationYear, + // evaluationRound: currentPeriod, + // submissionStatus: "draft" as const, + // totalGeneralItems: totalGeneralItems, + // completedGeneralItems: 0, + // totalEsgItems: totalEsgItems, + // completedEsgItems: 0, + // isActive: true, + // createdAt: new Date(), + // updatedAt: new Date() + // }) + // } + // } + // } + // // 7. evaluationSubmissions 레코드들 일괄 생성 + // let createdSubmissionsCount = 0 + // if (evaluationSubmissionsToCreate.length > 0) { + // const createdSubmissions = await tx + // .insert(evaluationSubmissions) + // .values(evaluationSubmissionsToCreate) + // .returning({ id: evaluationSubmissions.id }) + + // createdSubmissionsCount = createdSubmissions.length + // } + return { confirmedTargetIds, createdEvaluationsCount, - createdSubmissionsCount, + // createdSubmissionsCount, totalConfirmed: confirmedTargetIds.length } }) - + return { success: true, message: `${result.totalConfirmed}개 평가 대상이 확정되었습니다. ${result.createdEvaluationsCount}개의 정기평가와 ${result.createdSubmissionsCount}개의 제출 요청이 생성되었습니다.`, confirmedCount: result.totalConfirmed, createdEvaluationsCount: result.createdEvaluationsCount, - createdSubmissionsCount: result.createdSubmissionsCount + // createdSubmissionsCount: result.createdSubmissionsCount } - + } catch (error) { console.error("Error confirming evaluation targets:", error) return { @@ -959,7 +974,7 @@ export async function confirmEvaluationTargets( function getCurrentEvaluationPeriod(): string { const now = new Date() const month = now.getMonth() + 1 // 0-based이므로 +1 - + // 1~6월: 상반기, 7~12월: 하반기 return month <= 6 ? "상반기" : "하반기" } @@ -967,7 +982,7 @@ function getCurrentEvaluationPeriod(): string { // 평가년도와 기간에 따른 제출 마감일 설정하는 헬퍼 함수 function getSubmissionDeadline(evaluationYear: number, period: string): Date { const year = evaluationYear - + if (period === "상반기") { // 상반기 평가는 다음 해 6월 말까지 return new Date(year, 5, 31) // 7월은 6 (0-based) @@ -1022,17 +1037,17 @@ export async function excludeEvaluationTargets(targetIds: number[]) { }) - return { - success: true, + 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 : "제외 처리 중 오류가 발생했습니다." + return { + success: false, + error: error instanceof Error ? error.message : "제외 처리 중 오류가 발생했습니다." } } } @@ -1095,7 +1110,7 @@ export async function requestEvaluationReview(targetIds: number[], message?: str reviewers: [] } } - + if (item.reviewerEmail) { acc[item.id].reviewers.push({ email: item.reviewerEmail, @@ -1104,7 +1119,7 @@ export async function requestEvaluationReview(targetIds: number[], message?: str departmentName: item.departmentName }) } - + return acc }, {} as Record) @@ -1118,14 +1133,14 @@ export async function requestEvaluationReview(targetIds: number[], message?: str 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) @@ -1141,7 +1156,7 @@ export async function requestEvaluationReview(targetIds: number[], message?: str // 각 담당자에게 이메일 발송 const emailPromises = Array.from(reviewerEmails).map(email => { const reviewer = reviewerInfo.get(email)! - + return sendEmail({ to: email, subject: `벤더 평가 의견 요청 - ${targets.length}건`, @@ -1165,17 +1180,17 @@ export async function requestEvaluationReview(targetIds: number[], message?: str await Promise.all(emailPromises) - return { - success: true, + 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 : "의견 요청 중 오류가 발생했습니다." + return { + success: false, + error: error instanceof Error ? error.message : "의견 요청 중 오류가 발생했습니다." } } } @@ -1220,7 +1235,7 @@ export async function autoGenerateEvaluationTargets( // vendor 정보 vendorCode: vendors.vendorCode, vendorName: vendors.vendorName, - vendorType: vendors.country ==="KR"? "DOMESTIC":"FOREIGN", // DOMESTIC | FOREIGN + vendorType: vendors.country === "KR" ? "DOMESTIC" : "FOREIGN", // DOMESTIC | FOREIGN // project 정보 projectType: projects.type, // ship | plant }) @@ -1258,7 +1273,7 @@ export async function autoGenerateEvaluationTargets( 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, diff --git a/lib/evaluation/service.ts b/lib/evaluation/service.ts index 8e394f88..3e85b4a2 100644 --- a/lib/evaluation/service.ts +++ b/lib/evaluation/service.ts @@ -136,7 +136,6 @@ export async function getPeriodicEvaluations(input: GetEvaluationTargetsSchema) const pageCount = Math.ceil(total / input.perPage); - console.log(periodicEvaluationsData, "periodicEvaluationsData") return { data: periodicEvaluationsData, pageCount, total }; } catch (err) { @@ -359,6 +358,20 @@ export async function requestDocumentsFromVendors(data: RequestDocumentsData[]) }) ) + // periodic_evaluations 테이블의 status를 PENDING_SUBMISSION으로 업데이트 + const periodicEvaluationIds = [...new Set(data.map(item => item.periodicEvaluationId))] + + await Promise.all( + periodicEvaluationIds.map(async (periodicEvaluationId) => { + await db + .update(periodicEvaluations) + .set({ + status: 'PENDING_SUBMISSION', + updatedAt: new Date() + }) + .where(eq(periodicEvaluations.id, periodicEvaluationId)) + }) + ) return { success: true, @@ -375,7 +388,6 @@ export async function requestDocumentsFromVendors(data: RequestDocumentsData[]) } } } - // 기존 요청 상태 확인 함수 추가 export async function checkExistingSubmissions(periodicEvaluationIds: number[]) { try { @@ -397,6 +409,8 @@ export async function checkExistingSubmissions(periodicEvaluationIds: number[]) } }) + console.log(existingSubmissions, "existingSubmissions") + return existingSubmissions } catch (error) { console.error("Error checking existing submissions:", error) diff --git a/lib/evaluation/table/evaluation-columns.tsx b/lib/evaluation/table/evaluation-columns.tsx index 315ec66b..dca19ddb 100644 --- a/lib/evaluation/table/evaluation-columns.tsx +++ b/lib/evaluation/table/evaluation-columns.tsx @@ -40,6 +40,7 @@ const getStatusBadgeVariant = (status: string) => { const getStatusLabel = (status: string) => { const statusMap = { + PENDING: "대상확정", PENDING_SUBMISSION: "자료접수중", SUBMITTED: "제출완료", IN_REVIEW: "평가중", diff --git a/lib/evaluation/table/evaluation-filter-sheet.tsx b/lib/evaluation/table/evaluation-filter-sheet.tsx index 7c1e93d8..7f4de6a6 100644 --- a/lib/evaluation/table/evaluation-filter-sheet.tsx +++ b/lib/evaluation/table/evaluation-filter-sheet.tsx @@ -67,11 +67,12 @@ const divisionOptions = [ ] const statusOptions = [ - { value: "PENDING_SUBMISSION", label: "제출대기" }, + { value: "PENDING", label: "대상확정" }, + { value: "PENDING_SUBMISSION", label: "자료접수중" }, { value: "SUBMITTED", label: "제출완료" }, - { value: "IN_REVIEW", label: "검토중" }, - { value: "REVIEW_COMPLETED", label: "검토완료" }, - { value: "FINALIZED", label: "최종확정" }, + { value: "IN_REVIEW", label: "평가중" }, + { value: "REVIEW_COMPLETED", label: "평가완료" }, + { value: "FINALIZED", label: "결과확정" }, ] const domesticForeignOptions = [ @@ -91,7 +92,6 @@ const documentsSubmittedOptions = [ ] const gradeOptions = [ - { value: "S", label: "S등급" }, { value: "A", label: "A등급" }, { value: "B", label: "B등급" }, { value: "C", label: "C등급" }, @@ -470,7 +470,7 @@ export function PeriodicEvaluationFilterSheet({ /> {/* 평가기간 */} - ( @@ -514,7 +514,7 @@ export function PeriodicEvaluationFilterSheet({ )} - /> + /> */} {/* 구분 */} { return searchParams?.get(key) ?? defaultValue ?? ""; diff --git a/lib/evaluation/table/periodic-evaluation-action-dialogs.tsx b/lib/evaluation/table/periodic-evaluation-action-dialogs.tsx index fc07aea1..e6eec53a 100644 --- a/lib/evaluation/table/periodic-evaluation-action-dialogs.tsx +++ b/lib/evaluation/table/periodic-evaluation-action-dialogs.tsx @@ -82,7 +82,7 @@ export function RequestDocumentsDialog({ // 제출대기 상태인 평가들만 필터링 const pendingEvaluations = React.useMemo(() => - evaluations.filter(e => e.status === "PENDING_SUBMISSION"), + evaluations.filter(e => e.status === "PENDING_SUBMISSION" ||e.status === "PENDING" ), [evaluations] ) diff --git a/lib/evaluation/table/periodic-evaluations-toolbar-actions.tsx b/lib/evaluation/table/periodic-evaluations-toolbar-actions.tsx index d910f916..38622af4 100644 --- a/lib/evaluation/table/periodic-evaluations-toolbar-actions.tsx +++ b/lib/evaluation/table/periodic-evaluations-toolbar-actions.tsx @@ -66,7 +66,7 @@ export function PeriodicEvaluationsTableToolbarActions({ .getFilteredSelectedRowModel() .rows .map(row => row.original) - .filter(e => e.status === "PENDING_SUBMISSION"); + .filter(e => e.status === "PENDING_SUBMISSION"||e.status === "PENDING"); }, [table.getFilteredSelectedRowModel().rows]); const submittedEvaluations = React.useMemo(() => { diff --git a/lib/roles/services.ts b/lib/roles/services.ts index 1a91d4fa..54c7d833 100644 --- a/lib/roles/services.ts +++ b/lib/roles/services.ts @@ -3,7 +3,7 @@ import { revalidateTag, unstable_cache, unstable_noStore } from "next/cache"; import db from "@/db/db"; import { permissions, Role, rolePermissions, roles, RoleView, roleView, userRoles } from "@/db/schema/users"; -import { and, or, asc, desc, ilike, eq, inArray } from "drizzle-orm"; +import { and, or, asc, desc, ilike, eq, inArray, sql } from "drizzle-orm"; import { filterColumns } from "@/lib/filter-columns"; import { selectRolesWithUserCount, @@ -297,4 +297,106 @@ export async function getMenuPermissions( .where(ilike(permissions.permissionKey, pattern)); return rows; +} + + +export async function checkRegularEvaluationRoleExists(): Promise { + try { + const existingRoles = await db + .select({ id: roles.id, name: roles.name }) + .from(roles) + .where(sql`${roles.name} ILIKE '%정기평가%'`) + .limit(1) + + return existingRoles.length > 0 + } catch (error) { + console.error("정기평가 role 체크 중 에러:", error) + throw new Error("정기평가 role 체크에 실패했습니다") + } +} + + + +/** + * 여러 정기평가 role들의 할당 상태를 한번에 체크 + */ +export async function checkMultipleRegularEvaluationRolesAssigned(roleIds: number[]): Promise<{[roleId: number]: boolean}> { + try { + // 정기평가 role들만 필터링 + const regularEvaluationRoles = await db + .select({ id: roles.id, name: roles.name }) + .from(roles) + .where( + and( + inArray(roles.id, roleIds), + sql`${roles.name} ILIKE '%정기평가%'` + ) + ) + + const regularEvaluationRoleIds = regularEvaluationRoles.map(r => r.id) + const result: {[roleId: number]: boolean} = {} + + // 모든 role ID에 대해 초기값 설정 + roleIds.forEach(roleId => { + result[roleId] = false + }) + + if (regularEvaluationRoleIds.length > 0) { + // 할당된 정기평가 role들 체크 + const assignedRoles = await db + .select({ roleId: userRoles.roleId }) + .from(userRoles) + .where(inArray(userRoles.roleId, regularEvaluationRoleIds)) + + // 할당된 role들을 true로 설정 + assignedRoles.forEach(assignment => { + result[assignment.roleId] = true + }) + } + + return result + } catch (error) { + console.error("여러 정기평가 role 할당 상태 체크 중 에러:", error) + throw new Error("정기평가 role 할당 상태 체크에 실패했습니다") + } +} + +/** + * 특정 유저가 이미 다른 정기평가 role을 가지고 있는지 체크 + */ +export async function checkUserHasRegularEvaluationRole(userId: string): Promise<{hasRole: boolean, roleName?: string}> { + try { + const userRegularEvaluationRoles = await db + .select({ + roleId: userRoles.roleId, + roleName: roles.name + }) + .from(userRoles) + .innerJoin(roles, eq(userRoles.roleId, roles.id)) + .where( + and( + eq(userRoles.userId, userId), + sql`${roles.name} ILIKE '%정기평가%'` + ) + ) + .limit(1) + + return { + hasRole: userRegularEvaluationRoles.length > 0, + roleName: userRegularEvaluationRoles[0]?.roleName + } + } catch (error) { + console.error(`유저 ${userId}의 정기평가 role 체크 중 에러:`, error) + throw new Error("유저 정기평가 role 체크에 실패했습니다") + } +} + + +export async function removeRolesFromUsers(roleIds: number[], userIds: number[]) { + try { + // userRoles 테이블에서 해당 역할들을 제거하는 로직 + // 구현 필요 + } catch (error) { + return { error: "역할 제거 실패" } + } } \ No newline at end of file diff --git a/lib/roles/table/add-role-dialog.tsx b/lib/roles/table/add-role-dialog.tsx index 365daf29..162aaa89 100644 --- a/lib/roles/table/add-role-dialog.tsx +++ b/lib/roles/table/add-role-dialog.tsx @@ -21,12 +21,13 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" -import { Check, ChevronsUpDown, Loader } from "lucide-react" +import { Check, ChevronsUpDown, Loader, AlertTriangle } from "lucide-react" import { cn } from "@/lib/utils" import { toast } from "sonner" +import { Alert, AlertDescription } from "@/components/ui/alert" import { createRoleSchema, type CreateRoleSchema } from "../validations" -import { createRole } from "../services" +import { createRole, checkRegularEvaluationRoleExists } from "../services" import { Textarea } from "@/components/ui/textarea" import { Company } from "@/db/schema/companies" import { getAllCompanies } from "@/lib/admin-users/service" @@ -44,8 +45,6 @@ import { CommandEmpty, } from "@/components/ui/command" - - const domainOptions = [ { value: "partners", label: "협력업체" }, { value: "evcp", label: "삼성중공업" }, @@ -54,7 +53,9 @@ const domainOptions = [ export function AddRoleDialog() { const [open, setOpen] = React.useState(false) const [isAddPending, startAddTransition] = React.useTransition() - const [companies, setCompanies] = React.useState([]) // 회사 목록 + const [companies, setCompanies] = React.useState([]) + const [regularEvaluationExists, setRegularEvaluationExists] = React.useState(false) + const [isCheckingRegularEvaluation, setIsCheckingRegularEvaluation] = React.useState(false) React.useEffect(() => { getAllCompanies().then((res) => { @@ -67,12 +68,39 @@ export function AddRoleDialog() { resolver: zodResolver(createRoleSchema), defaultValues: { name: "", - domain: "evcp", // 기본값 + domain: "evcp", description: "", - // companyId: null, // optional }, }) + // name 필드 watch + const watchedName = form.watch("name") + + // "정기평가"가 포함된 이름인지 체크 + const isRegularEvaluationRole = watchedName.includes("정기평가") + + // 정기평가 role 존재 여부 체크 (debounced) + React.useEffect(() => { + if (!isRegularEvaluationRole) { + setRegularEvaluationExists(false) + return + } + + const timeoutId = setTimeout(async () => { + setIsCheckingRegularEvaluation(true) + try { + const exists = await checkRegularEvaluationRoleExists() + setRegularEvaluationExists(exists) + } catch (error) { + console.error("정기평가 role 체크 실패:", error) + } finally { + setIsCheckingRegularEvaluation(false) + } + }, 500) // 500ms debounce + + return () => clearTimeout(timeoutId) + }, [isRegularEvaluationRole, watchedName]) + async function onSubmit(data: CreateRoleSchema) { startAddTransition(async () => { const result = await createRole(data) @@ -82,19 +110,21 @@ export function AddRoleDialog() { } form.reset() setOpen(false) - toast.success("Role added") + setRegularEvaluationExists(false) + toast.success("Role이 성공적으로 추가되었습니다") }) } function handleDialogOpenChange(nextOpen: boolean) { if (!nextOpen) { form.reset() + setRegularEvaluationExists(false) } setOpen(nextOpen) } - // domain이 partners일 경우 companyId 입력 필드 보이게 const selectedDomain = form.watch("domain") + const canSubmit = !isRegularEvaluationRole || !regularEvaluationExists return ( @@ -129,6 +159,35 @@ export function AddRoleDialog() { /> + + {/* 정기평가 관련 경고 메시지 */} + {isRegularEvaluationRole && ( +
+ {isCheckingRegularEvaluation ? ( + + + + 정기평가 role 존재 여부를 확인하고 있습니다... + + + ) : regularEvaluationExists ? ( + + + + 경고: "정기평가"가 포함된 role이 이미 존재합니다. + 정기평가 role은 시스템에서 하나만 허용됩니다. + + + ) : ( + + + + 정기평가 role을 생성할 수 있습니다. + + + )} +
+ )} )} /> @@ -161,7 +220,6 @@ export function AddRoleDialog() { Domain