From 4e63d8427d26d0d1b366ddc53650e15f3481fc75 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 24 Jun 2025 01:44:03 +0000 Subject: (대표님/최겸) 20250624 작업사항 10시43분 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/evaluation-target-list/service.ts | 372 +++++++++++--- .../table/evaluation-target-table.tsx | 543 +++++++++------------ .../table/evaluation-targets-columns.tsx | 351 +++++-------- .../table/evaluation-targets-filter-sheet.tsx | 2 +- .../table/evaluation-targets-toolbar-actions.tsx | 4 +- .../table/update-evaluation-target.tsx | 4 +- 6 files changed, 676 insertions(+), 600 deletions(-) (limited to 'lib/evaluation-target-list') diff --git a/lib/evaluation-target-list/service.ts b/lib/evaluation-target-list/service.ts index 572b468d..0da50fa2 100644 --- a/lib/evaluation-target-list/service.ts +++ b/lib/evaluation-target-list/service.ts @@ -17,13 +17,16 @@ import { type DomesticForeign, EVALUATION_DEPARTMENT_CODES, EvaluationTargetWithDepartments, - evaluationTargetsWithDepartments + evaluationTargetsWithDepartments, + periodicEvaluations, + reviewerEvaluations } 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"; +import type { SQL } from "drizzle-orm" export async function selectEvaluationTargetsFromView( tx: PgTransaction, @@ -60,76 +63,122 @@ export async function countEvaluationTargetsFromView( // ============= 메인 서버 액션도 함께 수정 ============= + export async function getEvaluationTargets(input: GetEvaluationTargetsSchema) { try { const offset = (input.page - 1) * input.perPage; - // 고급 필터링 (View 테이블 기준) - const advancedWhere = filterColumns({ - table: evaluationTargetsWithDepartments, - filters: input.filters, - joinOperator: input.joinOperator, - }); + // ✅ getRFQ 방식과 동일한 필터링 처리 + // 1) 고급 필터 조건 + let advancedWhere: SQL | undefined = undefined; + if (input.filters && input.filters.length > 0) { + advancedWhere = filterColumns({ + table: evaluationTargetsWithDepartments, + filters: input.filters, + joinOperator: input.joinOperator || 'and', + }); + } - // 베이직 필터링 (커스텀 필터) - let basicWhere; + // 2) 기본 필터 조건 + let basicWhere: SQL | undefined = undefined; if (input.basicFilters && input.basicFilters.length > 0) { basicWhere = filterColumns({ table: evaluationTargetsWithDepartments, filters: input.basicFilters, - joinOperator: input.basicJoinOperator || "and", + joinOperator: input.basicJoinOperator || 'and', }); } - // 전역 검색 (View 테이블 기준) - let globalWhere; + // 3) 글로벌 검색 조건 + let globalWhere: SQL | undefined = undefined; if (input.search) { const s = `%${input.search}%`; - globalWhere = or( - ilike(evaluationTargetsWithDepartments.vendorCode, s), - ilike(evaluationTargetsWithDepartments.vendorName, s), - ilike(evaluationTargetsWithDepartments.adminComment, s), - ilike(evaluationTargetsWithDepartments.consolidatedComment, s), - // 담당자 이름으로도 검색 가능 - ilike(evaluationTargetsWithDepartments.orderReviewerName, s), - ilike(evaluationTargetsWithDepartments.procurementReviewerName, s), - ilike(evaluationTargetsWithDepartments.qualityReviewerName, s), - ilike(evaluationTargetsWithDepartments.designReviewerName, s), - ilike(evaluationTargetsWithDepartments.csReviewerName, s) - ); + + const validSearchConditions: SQL[] = []; + + const vendorCodeCondition = ilike(evaluationTargetsWithDepartments.vendorCode, s); + if (vendorCodeCondition) validSearchConditions.push(vendorCodeCondition); + + const vendorNameCondition = ilike(evaluationTargetsWithDepartments.vendorName, s); + if (vendorNameCondition) validSearchConditions.push(vendorNameCondition); + + const adminCommentCondition = ilike(evaluationTargetsWithDepartments.adminComment, s); + if (adminCommentCondition) validSearchConditions.push(adminCommentCondition); + + const consolidatedCommentCondition = ilike(evaluationTargetsWithDepartments.consolidatedComment, s); + if (consolidatedCommentCondition) validSearchConditions.push(consolidatedCommentCondition); + + // 담당자 이름으로도 검색 + const orderReviewerCondition = ilike(evaluationTargetsWithDepartments.orderReviewerName, s); + if (orderReviewerCondition) validSearchConditions.push(orderReviewerCondition); + + const procurementReviewerCondition = ilike(evaluationTargetsWithDepartments.procurementReviewerName, s); + if (procurementReviewerCondition) validSearchConditions.push(procurementReviewerCondition); + + const qualityReviewerCondition = ilike(evaluationTargetsWithDepartments.qualityReviewerName, s); + if (qualityReviewerCondition) validSearchConditions.push(qualityReviewerCondition); + + const designReviewerCondition = ilike(evaluationTargetsWithDepartments.designReviewerName, s); + if (designReviewerCondition) validSearchConditions.push(designReviewerCondition); + + const csReviewerCondition = ilike(evaluationTargetsWithDepartments.csReviewerName, s); + if (csReviewerCondition) validSearchConditions.push(csReviewerCondition); + + if (validSearchConditions.length > 0) { + globalWhere = or(...validSearchConditions); + } } - const finalWhere = and(advancedWhere, basicWhere, globalWhere); + // ✅ getRFQ 방식과 동일한 WHERE 조건 생성 + const whereConditions: SQL[] = []; - // 정렬 (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); - }) - : [desc(evaluationTargetsWithDepartments.createdAt)]; - - // 데이터 조회 - View 테이블 사용 - const { data, total } = await db.transaction(async (tx) => { - const data = await selectEvaluationTargetsFromView(tx, { - where: finalWhere, - orderBy, - offset, - limit: input.perPage, - }); + if (advancedWhere) whereConditions.push(advancedWhere); + if (basicWhere) whereConditions.push(basicWhere); + if (globalWhere) whereConditions.push(globalWhere); + + const finalWhere = whereConditions.length > 0 ? and(...whereConditions) : undefined; - const total = await countEvaluationTargetsFromView(tx, finalWhere); - return { data, total }; + // ✅ getRFQ 방식과 동일한 전체 데이터 수 조회 (Transaction 제거) + const totalResult = await db + .select({ count: count() }) + .from(evaluationTargetsWithDepartments) + .where(finalWhere); + + const total = totalResult[0]?.count || 0; + + if (total === 0) { + return { data: [], pageCount: 0, total: 0 }; + } + + console.log("Total evaluation targets:", total); + + // ✅ getRFQ 방식과 동일한 정렬 및 페이징 처리된 데이터 조회 + const orderByColumns = input.sort.map((sort) => { + const column = sort.id as keyof typeof evaluationTargetsWithDepartments.$inferSelect; + return sort.desc ? desc(evaluationTargetsWithDepartments[column]) : asc(evaluationTargetsWithDepartments[column]); }); + if (orderByColumns.length === 0) { + orderByColumns.push(desc(evaluationTargetsWithDepartments.createdAt)); + } + + const evaluationData = await db + .select() + .from(evaluationTargetsWithDepartments) + .where(finalWhere) + .orderBy(...orderByColumns) + .limit(input.perPage) + .offset(offset); + const pageCount = Math.ceil(total / input.perPage); - return { data, pageCount, total }; + + return { data: evaluationData, pageCount, total }; } catch (err) { console.error("Error in getEvaluationTargets:", err); - return { data: [], pageCount: 0 }; + // ✅ getRFQ 방식과 동일한 에러 반환 (total 포함) + return { data: [], pageCount: 0, total: 0 }; } } - // ============= 개별 조회 함수도 업데이트 ============= export async function getEvaluationTargetById(id: number): Promise { @@ -377,7 +426,8 @@ export async function updateEvaluationTarget(input: UpdateEvaluationTargetInput) console.log(input, "update input") try { - const session = await auth() + const session = await getServerSession(authOptions) + if (!session?.user) { throw new Error("인증이 필요합니다.") } @@ -462,8 +512,8 @@ export async function updateEvaluationTarget(input: UpdateEvaluationTargetInput) .values({ evaluationTargetId: input.id, departmentCode: update.departmentCode, - reviewerUserId: user[0].id, - assignedBy: session.user.id, + reviewerUserId: Number(user[0].id), + assignedBy:Number( session.user.id), }) } } @@ -553,7 +603,7 @@ export async function updateEvaluationTarget(input: UpdateEvaluationTargetInput) consensusStatus: hasConsensus, status: newStatus, confirmedAt: hasConsensus ? new Date() : null, - confirmedBy: hasConsensus ? session.user.id : null, + confirmedBy: hasConsensus ? Number(session.user.id) : null, updatedAt: new Date() }) .where(eq(evaluationTargets.id, input.id)) @@ -649,20 +699,27 @@ export async function getDepartmentInfo() { } -export async function confirmEvaluationTargets(targetIds: number[]) { +export async function confirmEvaluationTargets( + 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 ="연간" + // 트랜잭션으로 처리 - await db.transaction(async (tx) => { + const result = await db.transaction(async (tx) => { // 확정 가능한 대상들 확인 (PENDING 상태이면서 consensusStatus가 true인 것들) const eligibleTargets = await tx .select() @@ -674,13 +731,14 @@ export async function confirmEvaluationTargets(targetIds: number[]) { eq(evaluationTargets.consensusStatus, true) ) ) - + if (eligibleTargets.length === 0) { throw new Error("확정 가능한 평가 대상이 없습니다. (의견 일치 상태인 대기중 항목만 확정 가능)") } - - // 상태를 CONFIRMED로 변경 + const confirmedTargetIds = eligibleTargets.map(target => target.id) + + // 1. 평가 대상 상태를 CONFIRMED로 변경 await tx .update(evaluationTargets) .set({ @@ -690,26 +748,201 @@ export async function confirmEvaluationTargets(targetIds: number[]) { updatedAt: new Date() }) .where(inArray(evaluationTargets.id, confirmedTargetIds)) + + // 2. 각 확정된 평가 대상에 대해 periodicEvaluations 레코드 생성 + const periodicEvaluationsToCreate = [] + + for (const target of eligibleTargets) { + // 이미 해당 기간에 평가가 존재하는지 확인 + const existingEvaluation = await tx + .select({ id: periodicEvaluations.id }) + .from(periodicEvaluations) + .where( + and( + eq(periodicEvaluations.evaluationTargetId, target.id), + // eq(periodicEvaluations.evaluationPeriod, currentPeriod) + ) + ) + .limit(1) + + // 없으면 생성 목록에 추가 + if (existingEvaluation.length === 0) { + periodicEvaluationsToCreate.push({ + evaluationTargetId: target.id, + evaluationPeriod: currentPeriod, + // 평가년도에 따른 제출 마감일 설정 (예: 상반기는 7월 말, 하반기는 1월 말) + submissionDeadline: getSubmissionDeadline(target.evaluationYear, currentPeriod), + status: "PENDING_SUBMISSION" as const, + createdAt: new Date(), + updatedAt: new Date() + }) + } + } + + // 3. periodicEvaluations 레코드들 일괄 생성 + let createdEvaluationsCount = 0 + if (periodicEvaluationsToCreate.length > 0) { + const createdEvaluations = await tx + .insert(periodicEvaluations) + .values(periodicEvaluationsToCreate) + .returning({ id: periodicEvaluations.id }) + + createdEvaluationsCount = createdEvaluations.length + } + + // 4. 평가 항목 수 조회 (evaluationSubmissions 생성을 위해) + const [generalItemsCount, esgItemsCount] = await Promise.all([ + // 활성화된 일반평가 항목 수 + 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 - return confirmedTargetIds + // 5. 각 periodicEvaluation에 대해 담당자별 reviewerEvaluations도 생성 + if (periodicEvaluationsToCreate.length > 0) { + // 새로 생성된 periodicEvaluations 조회 + const newPeriodicEvaluations = await tx + .select({ + id: periodicEvaluations.id, + evaluationTargetId: periodicEvaluations.evaluationTargetId + }) + .from(periodicEvaluations) + .where( + and( + inArray(periodicEvaluations.evaluationTargetId, confirmedTargetIds), + eq(periodicEvaluations.evaluationPeriod, currentPeriod) + ) + ) + + // 각 평가에 대해 담당자별 reviewerEvaluations 생성 + for (const periodicEval of newPeriodicEvaluations) { + // 해당 evaluationTarget의 담당자들 조회 + const reviewers = await tx + .select() + .from(evaluationTargetReviewers) + .where(eq(evaluationTargetReviewers.evaluationTargetId, periodicEval.evaluationTargetId)) + + if (reviewers.length > 0) { + const reviewerEvaluationsToCreate = reviewers.map(reviewer => ({ + periodicEvaluationId: periodicEval.id, + evaluationTargetReviewerId: reviewer.id, + isCompleted: false, + createdAt: new Date(), + updatedAt: new Date() + })) + + await tx + .insert(reviewerEvaluations) + .values(reviewerEvaluationsToCreate) + } + } + } + + // 6. 벤더별 evaluationSubmissions 레코드 생성 + const evaluationSubmissionsToCreate = [] + + 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) { + evaluationSubmissionsToCreate.push({ + companyId: target.vendorId, + 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, + totalConfirmed: confirmedTargetIds.length + } }) - - - return { - success: true, - message: `${targetIds.length}개 평가 대상이 확정되었습니다.`, - confirmedCount: targetIds.length + + return { + success: true, + message: `${result.totalConfirmed}개 평가 대상이 확정되었습니다. ${result.createdEvaluationsCount}개의 정기평가와 ${result.createdSubmissionsCount}개의 제출 요청이 생성되었습니다.`, + confirmedCount: result.totalConfirmed, + createdEvaluationsCount: result.createdEvaluationsCount, + createdSubmissionsCount: result.createdSubmissionsCount } - + } catch (error) { console.error("Error confirming evaluation targets:", error) - return { - success: false, - error: error instanceof Error ? error.message : "확정 처리 중 오류가 발생했습니다." + return { + success: false, + error: error instanceof Error ? error.message : "확정 처리 중 오류가 발생했습니다." } } } + +// 현재 날짜 기준으로 평가 기간 결정하는 헬퍼 함수 +function getCurrentEvaluationPeriod(): string { + const now = new Date() + const month = now.getMonth() + 1 // 0-based이므로 +1 + + // 1~6월: 상반기, 7~12월: 하반기 + return month <= 6 ? "상반기" : "하반기" +} + +// 평가년도와 기간에 따른 제출 마감일 설정하는 헬퍼 함수 +function getSubmissionDeadline(evaluationYear: number, period: string): Date { + const year = evaluationYear + + if (period === "상반기") { + // 상반기 평가는 다음 해 6월 말까지 + return new Date(year, 5, 31) // 7월은 6 (0-based) + } else if (period === "하반기") { + // 하반기 평가는 다음 올해 12월 말까지 + return new Date(year, 11, 31) // 1월은 0 (0-based) + } else { + // 연간 평가는 올해 6월 말까지 + return new Date(year, 5, 31) // 3월은 2 (0-based) + } +} + export async function excludeEvaluationTargets(targetIds: number[]) { try { const session = await getServerSession(authOptions) @@ -769,7 +1002,8 @@ export async function excludeEvaluationTargets(targetIds: number[]) { export async function requestEvaluationReview(targetIds: number[], message?: string) { try { - const session = await auth() + const session = await getServerSession(authOptions) + if (!session?.user) { return { success: false, error: "인증이 필요합니다." } } diff --git a/lib/evaluation-target-list/table/evaluation-target-table.tsx b/lib/evaluation-target-list/table/evaluation-target-table.tsx index fe0b3188..87be3589 100644 --- a/lib/evaluation-target-list/table/evaluation-target-table.tsx +++ b/lib/evaluation-target-list/table/evaluation-target-table.tsx @@ -1,76 +1,61 @@ -"use client" - -import * as React from "react" -import { useRouter, useSearchParams } from "next/navigation" -import { Button } from "@/components/ui/button" -import { PanelLeftClose, PanelLeftOpen } from "lucide-react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Skeleton } from "@/components/ui/skeleton" +// ============================================================================ +// components/evaluation-targets-table.tsx (CLIENT COMPONENT) +// ─ 완전본 ─ evaluation-targets-columns.tsx 는 별도 +// ============================================================================ +"use client"; + +import * as React from "react"; +import { useSearchParams } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { PanelLeftClose, PanelLeftOpen } from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; import type { DataTableAdvancedFilterField, DataTableFilterField, DataTableRowAction, -} from "@/types/table" - -import { useDataTable } from "@/hooks/use-data-table" -import { DataTable } from "@/components/data-table/data-table" -import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" -import { getEvaluationTargets, getEvaluationTargetsStats } from "../service" -import { cn } from "@/lib/utils" -import { useTablePresets } from "@/components/data-table/use-table-presets" -import { TablePresetManager } from "@/components/data-table/data-table-preset" -import { useMemo } from "react" -import { getEvaluationTargetsColumns } from "./evaluation-targets-columns" -import { EvaluationTargetsTableToolbarActions } from "./evaluation-targets-toolbar-actions" -import { EvaluationTargetFilterSheet } from "./evaluation-targets-filter-sheet" -import { EvaluationTargetWithDepartments } from "@/db/schema" -import { EditEvaluationTargetSheet } from "./update-evaluation-target" - -interface EvaluationTargetsTableProps { - promises: Promise<[Awaited>]> - evaluationYear: number - className?: string -} - -// 통계 카드 컴포넌트 (클라이언트 컴포넌트용) +} from "@/types/table"; +import { useDataTable } from "@/hooks/use-data-table"; +import { DataTable } from "@/components/data-table/data-table"; +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"; +import { getEvaluationTargets, getEvaluationTargetsStats } from "../service"; +import { cn } from "@/lib/utils"; +import { useTablePresets } from "@/components/data-table/use-table-presets"; +import { TablePresetManager } from "@/components/data-table/data-table-preset"; +import { getEvaluationTargetsColumns } from "./evaluation-targets-columns"; +import { EvaluationTargetsTableToolbarActions } from "./evaluation-targets-toolbar-actions"; +import { EvaluationTargetFilterSheet } from "./evaluation-targets-filter-sheet"; +import { EvaluationTargetWithDepartments } from "@/db/schema"; +import { EditEvaluationTargetSheet } from "./update-evaluation-target"; + +/* -------------------------------------------------------------------------- */ +/* Stats Card */ +/* -------------------------------------------------------------------------- */ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number }) { - const [stats, setStats] = React.useState(null) - const [isLoading, setIsLoading] = React.useState(true) - const [error, setError] = React.useState(null) + const [stats, setStats] = React.useState(null); + const [isLoading, setIsLoading] = React.useState(true); + const [error, setError] = React.useState(null); React.useEffect(() => { - let isMounted = true - - async function fetchStats() { + let mounted = true; + (async () => { try { - setIsLoading(true) - setError(null) - const statsData = await getEvaluationTargetsStats(evaluationYear) - - if (isMounted) { - setStats(statsData) - } - } catch (err) { - if (isMounted) { - setError(err instanceof Error ? err.message : 'Failed to fetch stats') - console.error('Error fetching evaluation targets stats:', err) - } + setIsLoading(true); + const data = await getEvaluationTargetsStats(evaluationYear); + mounted && setStats(data); + } catch (e) { + mounted && setError(e instanceof Error ? e.message : "failed"); } finally { - if (isMounted) { - setIsLoading(false) - } + mounted && setIsLoading(false); } - } - - fetchStats() - + })(); return () => { - isMounted = false - } - }, []) + mounted = false; + }; + }, [evaluationYear]); - if (isLoading) { + if (isLoading) return (
{Array.from({ length: 4 }).map((_, i) => ( @@ -84,42 +69,32 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number }) ))}
- ) - } - - if (error) { + ); + if (error) return (
- -
- 통계 데이터를 불러올 수 없습니다: {error} -
+ + 통계 데이터를 불러올 수 없습니다: {error}
- ) - } - - if (!stats) { + ); + if (!stats) return (
- -
- 통계 데이터가 없습니다. -
+ + 통계 데이터가 없습니다.
- ) - } + ); - const totalTargets = stats.total || 0 - const pendingTargets = stats.pending || 0 - const confirmedTargets = stats.confirmed || 0 - const excludedTargets = stats.excluded || 0 - const consensusRate = totalTargets > 0 ? Math.round(((stats.consensusTrue || 0) / totalTargets) * 100) : 0 + const total = stats.total || 0; + const pending = stats.pending || 0; + const confirmed = stats.confirmed || 0; + const consensusRate = total ? Math.round(((stats.consensusTrue || 0) / total) * 100) : 0; return (
@@ -130,7 +105,7 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number }) {evaluationYear}년 -
{totalTargets.toLocaleString()}
+
{total.toLocaleString()}
해양 {stats.oceanDivision || 0}개 | 조선 {stats.shipyardDivision || 0}개
@@ -144,9 +119,9 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number }) 대기 -
{pendingTargets.toLocaleString()}
+
{pending.toLocaleString()}
- {totalTargets > 0 ? Math.round((pendingTargets / totalTargets) * 100) : 0}% of total + {total ? Math.round((pending / total) * 100) : 0}% of total
@@ -155,12 +130,12 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number }) 확정 - 완료 + 완료 -
{confirmedTargets.toLocaleString()}
+
{confirmed.toLocaleString()}
- {totalTargets > 0 ? Math.round((confirmedTargets / totalTargets) * 100) : 0}% of total + {total ? Math.round((confirmed / total) * 100) : 0}% of total
@@ -169,9 +144,7 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number }) 의견 일치율 - = 80 ? "default" : consensusRate >= 60 ? "secondary" : "destructive"}> - {consensusRate}% - + = 80 ? "default" : consensusRate >= 60 ? "secondary" : "destructive"}>{consensusRate}%
{consensusRate}%
@@ -181,83 +154,92 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number })
- ) + ); +} + +/* -------------------------------------------------------------------------- */ +/* EvaluationTargetsTable */ +/* -------------------------------------------------------------------------- */ +interface EvaluationTargetsTableProps { + promises: Promise<[Awaited>]>; + evaluationYear: number; + className?: string; } export function EvaluationTargetsTable({ promises, evaluationYear, className }: EvaluationTargetsTableProps) { - const [rowAction, setRowAction] = React.useState | null>(null) - const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false) - const router = useRouter() - const searchParams = useSearchParams() + const [rowAction, setRowAction] = React.useState | null>(null); + const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false); + const searchParams = useSearchParams(); - const containerRef = React.useRef(null) - const [containerTop, setContainerTop] = React.useState(0) + /* --------------------------- layout refs --------------------------- */ + const containerRef = React.useRef(null); + const [containerTop, setContainerTop] = React.useState(0); - // ✅ 스크롤 이벤트 throttling으로 성능 최적화 + // RFQ 패턴으로 변경: State를 통한 위치 관리 const updateContainerBounds = React.useCallback(() => { if (containerRef.current) { - const rect = containerRef.current.getBoundingClientRect() - const newTop = rect.top - - // ✅ 값이 실제로 변경될 때만 상태 업데이트 - setContainerTop(prevTop => { - if (Math.abs(prevTop - newTop) > 1) { // 1px 이상 차이날 때만 업데이트 - return newTop - } - return prevTop - }) + const rect = containerRef.current.getBoundingClientRect(); + setContainerTop(rect.top); } - }, []) - - // ✅ throttle 함수 추가 - const throttledUpdateBounds = React.useCallback(() => { - let timeoutId: NodeJS.Timeout - return () => { - clearTimeout(timeoutId) - timeoutId = setTimeout(updateContainerBounds, 16) // ~60fps - } - }, [updateContainerBounds]) + }, []); React.useEffect(() => { - updateContainerBounds() - - const throttledHandler = throttledUpdateBounds() - + updateContainerBounds(); + const handleResize = () => { - updateContainerBounds() - } - - window.addEventListener('resize', handleResize) - window.addEventListener('scroll', throttledHandler) // ✅ throttled 함수 사용 - + updateContainerBounds(); + }; + + window.addEventListener('resize', handleResize); + window.addEventListener('scroll', updateContainerBounds); + return () => { - window.removeEventListener('resize', handleResize) - window.removeEventListener('scroll', throttledHandler) + window.removeEventListener('resize', handleResize); + window.removeEventListener('scroll', updateContainerBounds); + }; + }, [updateContainerBounds]); + + /* ---------------------- 데이터 프리패치 ---------------------- */ + const [promiseData] = React.use(promises); + const tableData = promiseData; + + /* ---------------------- 검색 파라미터 안전 처리 ---------------------- */ + const getSearchParam = React.useCallback((key: string, defaultValue?: string): string => { + return searchParams?.get(key) ?? defaultValue ?? ""; + }, [searchParams]); + + // 제네릭 함수는 useCallback 밖에서 정의 + const parseSearchParamHelper = React.useCallback((key: string, defaultValue: any): any => { + try { + const value = getSearchParam(key); + return value ? JSON.parse(value) : defaultValue; + } catch { + return defaultValue; } - }, [updateContainerBounds, throttledUpdateBounds]) + }, [getSearchParam]); - const [promiseData] = React.use(promises) - const tableData = promiseData - - console.log(tableData) +const parseSearchParam = (key: string, defaultValue: T): T => { + return parseSearchParamHelper(key, defaultValue); +}; + /* ---------------------- 초기 설정 ---------------------------- */ const initialSettings = React.useMemo(() => ({ - page: parseInt(searchParams.get('page') || '1'), - perPage: parseInt(searchParams.get('perPage') || '10'), - sort: searchParams.get('sort') ? JSON.parse(searchParams.get('sort')!) : [{ id: "createdAt", desc: true }], - filters: searchParams.get('filters') ? JSON.parse(searchParams.get('filters')!) : [], - joinOperator: (searchParams.get('joinOperator') as "and" | "or") || "and", - basicFilters: searchParams.get('basicFilters') ? - JSON.parse(searchParams.get('basicFilters')!) : [], - basicJoinOperator: (searchParams.get('basicJoinOperator') as "and" | "or") || "and", - search: searchParams.get('search') || '', + page: parseInt(getSearchParam("page", "1")), + perPage: parseInt(getSearchParam("perPage", "10")), + sort: parseSearchParam("sort", [{ id: "createdAt", desc: true }]), + filters: parseSearchParam("filters", []), + joinOperator: (getSearchParam("joinOperator") as "and" | "or") || "and", + basicFilters: parseSearchParam("basicFilters", []), + basicJoinOperator: (getSearchParam("basicJoinOperator") as "and" | "or") || "and", + search: getSearchParam("search", ""), columnVisibility: {}, columnOrder: [], pinnedColumns: { left: [], right: ["actions"] }, groupBy: [], - expandedRows: [] - }), [searchParams]) + expandedRows: [], + }), [getSearchParam, parseSearchParamHelper]); + /* --------------------- 프리셋 훅 ------------------------------ */ const { presets, activePresetId, @@ -269,83 +251,59 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: deletePreset, setDefaultPreset, renamePreset, - updateClientState, getCurrentSettings, - } = useTablePresets('evaluation-targets-table', initialSettings) - - const columns = React.useMemo( - () => getEvaluationTargetsColumns({ setRowAction }), - [setRowAction] - ) - + } = useTablePresets( + "evaluation-targets-table", + initialSettings + ); + + /* --------------------- 컬럼 ------------------------------ */ + const columns = React.useMemo(() => getEvaluationTargetsColumns({ setRowAction }), [setRowAction]); +// const columns =[ +// { accessorKey: "vendorCode", header: "벤더 코드" }, +// { accessorKey: "vendorName", header: "벤더명" }, +// { accessorKey: "status", header: "상태" }, +// { accessorKey: "evaluationYear", header: "평가년도" }, +// { accessorKey: "division", header: "구분" } +// ]; + +window.addEventListener('beforeunload', () => { + console.trace('[beforeunload] 문서가 통째로 사라지려 합니다!'); +}); + + /* 기본 필터 */ const filterFields: DataTableFilterField[] = [ { id: "vendorCode", label: "벤더 코드" }, { id: "vendorName", label: "벤더명" }, { id: "status", label: "상태" }, - ] + ]; + /* 고급 필터 */ const advancedFilterFields: DataTableAdvancedFilterField[] = [ { id: "evaluationYear", label: "평가년도", type: "number" }, - { - id: "division", label: "구분", type: "select", options: [ - { label: "해양", value: "OCEAN" }, - { label: "조선", value: "SHIPYARD" }, - ] - }, + { id: "division", label: "구분", type: "select", options: [ { label: "해양", value: "OCEAN" }, { label: "조선", value: "SHIPYARD" } ] }, { id: "vendorCode", label: "벤더 코드", type: "text" }, { id: "vendorName", label: "벤더명", type: "text" }, - { - id: "domesticForeign", label: "내외자", type: "select", options: [ - { label: "내자", value: "DOMESTIC" }, - { label: "외자", value: "FOREIGN" }, - ] - }, - { - id: "materialType", label: "자재구분", type: "select", options: [ - { label: "기자재", value: "EQUIPMENT" }, - { label: "벌크", value: "BULK" }, - { label: "기자재/벌크", value: "EQUIPMENT_BULK" }, - ] - }, - { - id: "status", label: "상태", type: "select", options: [ - { label: "검토 중", value: "PENDING" }, - { label: "확정", value: "CONFIRMED" }, - { label: "제외", value: "EXCLUDED" }, - ] - }, - { - id: "consensusStatus", label: "의견 일치", type: "select", options: [ - { label: "의견 일치", value: "true" }, - { label: "의견 불일치", value: "false" }, - { label: "검토 중", value: "null" }, - ] - }, + { id: "domesticForeign", label: "내외자", type: "select", options: [ { label: "내자", value: "DOMESTIC" }, { label: "외자", value: "FOREIGN" } ] }, + { id: "materialType", label: "자재구분", type: "select", options: [ { label: "기자재", value: "EQUIPMENT" }, { label: "벌크", value: "BULK" }, { label: "기/벌", value: "EQUIPMENT_BULK" } ] }, + { id: "status", label: "상태", type: "select", options: [ { label: "검토 중", value: "PENDING" }, { label: "확정", value: "CONFIRMED" }, { label: "제외", value: "EXCLUDED" } ] }, + { id: "consensusStatus", label: "의견 일치", type: "select", options: [ { label: "일치", value: "true" }, { label: "불일치", value: "false" }, { label: "검토 중", value: "null" } ] }, { id: "adminComment", label: "관리자 의견", type: "text" }, { id: "consolidatedComment", label: "종합 의견", type: "text" }, { id: "confirmedAt", label: "확정일", type: "date" }, { id: "createdAt", label: "생성일", type: "date" }, - ] - - const currentSettings = useMemo(() => { - return getCurrentSettings() - }, [getCurrentSettings]) - - function getColKey(c: ColumnDef): string | undefined { - if ("accessorKey" in c && c.accessorKey) return c.accessorKey as string - if ("id" in c && c.id) return c.id as string - return undefined - } - - const initialState = useMemo(() => { - return { - sorting: initialSettings.sort.filter(s => - columns.some(c => getColKey(c) === s.id)), - columnVisibility: currentSettings.columnVisibility, - columnPinning: currentSettings.pinnedColumns, - } - }, [columns, currentSettings, initialSettings.sort]) + ]; + + /* current settings */ + const currentSettings = React.useMemo(() => getCurrentSettings(), [getCurrentSettings]); + + const initialState = React.useMemo(() => ({ + sorting: initialSettings.sort.filter((s: any) => columns.some((c: any) => ("accessorKey" in c ? c.accessorKey : c.id) === s.id)), + columnVisibility: currentSettings.columnVisibility, + columnPinning: currentSettings.pinnedColumns, + }), [columns, currentSettings, initialSettings.sort]); + /* ----------------------- useDataTable ------------------------ */ const { table } = useDataTable({ data: tableData.data, columns, @@ -355,134 +313,119 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: enablePinning: true, enableAdvancedFilter: true, initialState, - getRowId: (originalRow) => String(originalRow.id), + getRowId: (row) => String(row.id), shallow: false, clearOnDefault: true, - }) + }); - const handleSearch = () => { - setIsFilterPanelOpen(false) - } - - const getActiveBasicFilterCount = () => { + /* ---------------------- helper ------------------------------ */ + const getActiveBasicFilterCount = React.useCallback(() => { try { - const basicFilters = searchParams.get('basicFilters') - return basicFilters ? JSON.parse(basicFilters).length : 0 - } catch (e) { - return 0 + const f = getSearchParam("basicFilters"); + return f ? JSON.parse(f).length : 0; + } catch { + return 0; } - } + }, [getSearchParam]); const FILTER_PANEL_WIDTH = 400; + /* ---------------------------- JSX ---------------------------- */ return ( - <> + <> {/* Filter Panel */}
-
- setIsFilterPanelOpen(false)} - onSearch={handleSearch} - isLoading={false} - /> -
+ setIsFilterPanelOpen(false)} + onSearch={() => setIsFilterPanelOpen(false)} + isLoading={false} + />
- {/* Main Content Container */} -
+ {/* Main Container */} +
- {/* Header Bar */} + {/* Header */}
-
- -
- -
- {tableData && ( - 총 {tableData.total || tableData.data.length}건 + +
+ 총 {tableData.total || tableData.data.length}건
- {/* 통계 카드들 */} + {/* Stats */}
- {/* Table Content Area */} -
-
- - -
- - presets={presets} - activePresetId={activePresetId} - currentSettings={currentSettings} - hasUnsavedChanges={hasUnsavedChanges} - isLoading={presetsLoading} - onCreatePreset={createPreset} - onUpdatePreset={updatePreset} - onDeletePreset={deletePreset} - onApplyPreset={applyPreset} - onSetDefaultPreset={setDefaultPreset} - onRenamePreset={renamePreset} - /> - - -
-
-
- - setRowAction(null)} - evaluationTarget={rowAction?.row.original ?? null} - /> - -
+ {/* Table */} +
+ + +
+ + presets={presets} + activePresetId={activePresetId} + currentSettings={currentSettings} + hasUnsavedChanges={hasUnsavedChanges} + isLoading={presetsLoading} + onCreatePreset={createPreset} + onUpdatePreset={updatePreset} + onDeletePreset={deletePreset} + onApplyPreset={applyPreset} + onSetDefaultPreset={setDefaultPreset} + onRenamePreset={renamePreset} + /> + + +
+
+
+ + {/* 편집 다이얼로그 */} + setRowAction(null)} + evaluationTarget={rowAction?.row.original ?? null} + />
- ) + ); } \ No newline at end of file diff --git a/lib/evaluation-target-list/table/evaluation-targets-columns.tsx b/lib/evaluation-target-list/table/evaluation-targets-columns.tsx index 93807ef9..e2163cad 100644 --- a/lib/evaluation-target-list/table/evaluation-targets-columns.tsx +++ b/lib/evaluation-target-list/table/evaluation-targets-columns.tsx @@ -4,16 +4,17 @@ import { type ColumnDef } from "@tanstack/react-table"; import { Checkbox } from "@/components/ui/checkbox"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Pencil, Eye, MessageSquare, Check, X } from "lucide-react"; +import { Pencil, Check, X } from "lucide-react"; import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"; import { EvaluationTargetWithDepartments } from "@/db/schema"; -import { EditEvaluationTargetSheet } from "./update-evaluation-target"; +import type { DataTableRowAction } from "@/types/table"; +import { formatDate } from "@/lib/utils"; interface GetColumnsProps { setRowAction: React.Dispatch | null>>; } -// 상태별 색상 매핑 +// ✅ 모든 헬퍼 함수들을 컴포넌트 외부로 이동 (매번 재생성 방지) const getStatusBadgeVariant = (status: string) => { switch (status) { case "PENDING": @@ -27,7 +28,15 @@ const getStatusBadgeVariant = (status: string) => { } }; -// 의견 일치 여부 배지 +const getStatusText = (status: string) => { + const statusMap = { + PENDING: "검토 중", + CONFIRMED: "확정", + EXCLUDED: "제외" + }; + return statusMap[status] || status; +}; + const getConsensusBadge = (consensusStatus: boolean | null) => { if (consensusStatus === null) { return 검토 중; @@ -38,16 +47,14 @@ const getConsensusBadge = (consensusStatus: boolean | null) => { return 의견 불일치; }; -// 구분 배지 const getDivisionBadge = (division: string) => { return ( - - {division === "PLANT" ? "해양" : "조선"} + + {division === "OCEAN" ? "해양" : "조선"} ); }; -// 자재구분 배지 const getMaterialTypeBadge = (materialType: string) => { const typeMap = { EQUIPMENT: "기자재", @@ -57,7 +64,6 @@ const getMaterialTypeBadge = (materialType: string) => { return {typeMap[materialType] || materialType}; }; -// 내외자 배지 const getDomesticForeignBadge = (domesticForeign: string) => { return ( @@ -66,24 +72,27 @@ const getDomesticForeignBadge = (domesticForeign: string) => { ); }; -// 평가 상태 배지 -const getApprovalBadge = (isApproved: boolean | null) => { - if (isApproved === null) { - return 대기중; - } - if (isApproved === true) { - return 승인; +// ✅ 평가 대상 여부 표시 함수 +const getEvaluationTargetBadge = (isTarget: boolean | null) => { + if (isTarget === null) { + return 미정; } - return 거부; + return isTarget ? ( + + + 평가 대상 + + ) : ( + + + 평가 제외 + + ); }; -export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): ColumnDef[] { +export function getEvaluationTargetsColumns({ setRowAction }: GetColumnsProps): ColumnDef[] { return [ - // ═══════════════════════════════════════════════════════════════ - // 기본 정보 - // ═══════════════════════════════════════════════════════════════ - - // Checkbox + // ✅ Checkbox { id: "select", header: ({ table }) => ( @@ -107,15 +116,13 @@ export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): Col enableHiding: false, }, - // ░░░ 평가년도 ░░░ + // ✅ 기본 정보 { accessorKey: "evaluationYear", header: ({ column }) => , cell: ({ row }) => {row.getValue("evaluationYear")}, size: 100, }, - - // ░░░ 구분 ░░░ { accessorKey: "division", header: ({ column }) => , @@ -127,24 +134,25 @@ export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): Col header: ({ column }) => , cell: ({ row }) => { const status = row.getValue("status"); - const statusMap = { - PENDING: "검토 중", - CONFIRMED: "확정", - EXCLUDED: "제외" - }; return ( - {statusMap[status] || status} + {getStatusText(status)} ); }, size: 100, }, + { + accessorKey: "consensusStatus", + header: ({ column }) => , + cell: ({ row }) => getConsensusBadge(row.getValue("consensusStatus")), + size: 100, + }, - // ░░░ 벤더 코드 ░░░ - + // ✅ 벤더 정보 그룹 { - header: "협력업체 정보", + id: "vendorInfo", + header: "벤더 정보", columns: [ { accessorKey: "vendorCode", @@ -154,8 +162,6 @@ export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): Col ), size: 120, }, - - // ░░░ 벤더명 ░░░ { accessorKey: "vendorName", header: ({ column }) => , @@ -166,267 +172,182 @@ export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): Col ), size: 200, }, - - // ░░░ 내외자 ░░░ { accessorKey: "domesticForeign", header: ({ column }) => , cell: ({ row }) => getDomesticForeignBadge(row.getValue("domesticForeign")), size: 80, }, - + { + accessorKey: "materialType", + header: ({ column }) => , + cell: ({ row }) => getMaterialTypeBadge(row.getValue("materialType")), + size: 120, + }, ] }, - // ░░░ 자재구분 ░░░ - { - accessorKey: "materialType", - header: ({ column }) => , - cell: ({ row }) => getMaterialTypeBadge(row.getValue("materialType")), - size: 120, - }, - - // ░░░ 상태 ░░░ - - - // ░░░ 의견 일치 여부 ░░░ - { - accessorKey: "consensusStatus", - header: ({ column }) => , - cell: ({ row }) => getConsensusBadge(row.getValue("consensusStatus")), - size: 100, - }, - - // ═══════════════════════════════════════════════════════════════ - // 주문 부서 그룹 - // ═══════════════════════════════════════════════════════════════ + // ✅ 발주 담당자 { - header: "발주 평가 담당자", + id: "orderReviewer", + header: "발주 담당자", columns: [ - { - accessorKey: "orderDepartmentName", - header: ({ column }) => , - cell: ({ row }) => { - const departmentName = row.getValue("orderDepartmentName"); - return departmentName ? ( -
- {departmentName} -
- ) : ( - - - ); - }, - size: 120, - }, { accessorKey: "orderReviewerName", - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { const reviewerName = row.getValue("orderReviewerName"); return reviewerName ? ( -
+
{reviewerName}
) : ( - ); }, - size: 100, + size: 120, }, { accessorKey: "orderIsApproved", - header: ({ column }) => , - cell: ({ row }) => getApprovalBadge(row.getValue("orderIsApproved")), - size: 80, + header: ({ column }) => , + cell: ({ row }) => { + const isApproved = row.getValue("orderIsApproved"); + return getEvaluationTargetBadge(isApproved); + }, + size: 120, }, - ], + ] }, - // ═══════════════════════════════════════════════════════════════ - // 조달 부서 그룹 - // ═══════════════════════════════════════════════════════════════ + // ✅ 조달 담당자 { - header: "조달 평가 담당자", + id: "procurementReviewer", + header: "조달 담당자", columns: [ - { - accessorKey: "procurementDepartmentName", - header: ({ column }) => , - cell: ({ row }) => { - const departmentName = row.getValue("procurementDepartmentName"); - return departmentName ? ( -
- {departmentName} -
- ) : ( - - - ); - }, - size: 120, - }, { accessorKey: "procurementReviewerName", - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { const reviewerName = row.getValue("procurementReviewerName"); return reviewerName ? ( -
+
{reviewerName}
) : ( - ); }, - size: 100, + size: 120, }, { accessorKey: "procurementIsApproved", - header: ({ column }) => , - cell: ({ row }) => getApprovalBadge(row.getValue("procurementIsApproved")), - size: 80, + header: ({ column }) => , + cell: ({ row }) => { + const isApproved = row.getValue("procurementIsApproved"); + return getEvaluationTargetBadge(isApproved); + }, + size: 120, }, - ], + ] }, - // ═══════════════════════════════════════════════════════════════ - // 품질 부서 그룹 - // ═══════════════════════════════════════════════════════════════ + // ✅ 품질 담당자 { - header: "품질 평가 담당자", + id: "qualityReviewer", + header: "품질 담당자", columns: [ - { - accessorKey: "qualityDepartmentName", - header: ({ column }) => , - cell: ({ row }) => { - const departmentName = row.getValue("qualityDepartmentName"); - return departmentName ? ( -
- {departmentName} -
- ) : ( - - - ); - }, - size: 120, - }, { accessorKey: "qualityReviewerName", - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { const reviewerName = row.getValue("qualityReviewerName"); return reviewerName ? ( -
+
{reviewerName}
) : ( - ); }, - size: 100, + size: 120, }, { accessorKey: "qualityIsApproved", - header: ({ column }) => , - cell: ({ row }) => getApprovalBadge(row.getValue("qualityIsApproved")), - size: 80, + header: ({ column }) => , + cell: ({ row }) => { + const isApproved = row.getValue("qualityIsApproved"); + return getEvaluationTargetBadge(isApproved); + }, + size: 120, }, - ], + ] }, - // ═══════════════════════════════════════════════════════════════ - // 설계 부서 그룹 - // ═══════════════════════════════════════════════════════════════ + // ✅ 설계 담당자 { - header: "설계 평가 담당자", + id: "designReviewer", + header: "설계 담당자", columns: [ - { - accessorKey: "designDepartmentName", - header: ({ column }) => , - cell: ({ row }) => { - const departmentName = row.getValue("designDepartmentName"); - return departmentName ? ( -
- {departmentName} -
- ) : ( - - - ); - }, - size: 120, - }, { accessorKey: "designReviewerName", - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { const reviewerName = row.getValue("designReviewerName"); return reviewerName ? ( -
+
{reviewerName}
) : ( - ); }, - size: 100, + size: 120, }, { accessorKey: "designIsApproved", - header: ({ column }) => , - cell: ({ row }) => getApprovalBadge(row.getValue("designIsApproved")), - size: 80, + header: ({ column }) => , + cell: ({ row }) => { + const isApproved = row.getValue("designIsApproved"); + return getEvaluationTargetBadge(isApproved); + }, + size: 120, }, - ], + ] }, - // ═══════════════════════════════════════════════════════════════ - // CS 부서 그룹 - // ═══════════════════════════════════════════════════════════════ + // ✅ CS 담당자 { - header: "CS 평가 담당자", + id: "csReviewer", + header: "CS 담당자", columns: [ - { - accessorKey: "csDepartmentName", - header: ({ column }) => , - cell: ({ row }) => { - const departmentName = row.getValue("csDepartmentName"); - return departmentName ? ( -
- {departmentName} -
- ) : ( - - - ); - }, - size: 120, - }, { accessorKey: "csReviewerName", - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { const reviewerName = row.getValue("csReviewerName"); return reviewerName ? ( -
+
{reviewerName}
) : ( - ); }, - size: 100, + size: 120, }, { accessorKey: "csIsApproved", - header: ({ column }) => , - cell: ({ row }) => getApprovalBadge(row.getValue("csIsApproved")), - size: 80, + header: ({ column }) => , + cell: ({ row }) => { + const isApproved = row.getValue("csIsApproved"); + return getEvaluationTargetBadge(isApproved); + }, + size: 120, }, - ], + ] }, - // ═══════════════════════════════════════════════════════════════ - // 관리 정보 - // ═══════════════════════════════════════════════════════════════ - - // ░░░ 관리자 의견 ░░░ + // ✅ 의견 및 결과 { accessorKey: "adminComment", header: ({ column }) => , @@ -442,8 +363,6 @@ export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): Col }, size: 150, }, - - // ░░░ 종합 의견 ░░░ { accessorKey: "consolidatedComment", header: ({ column }) => , @@ -459,69 +378,49 @@ export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): Col }, size: 150, }, - - // ░░░ 확정일 ░░░ { accessorKey: "confirmedAt", header: ({ column }) => , cell: ({ row }) => { const confirmedAt = row.getValue("confirmedAt"); - return confirmedAt ? ( - - {new Intl.DateTimeFormat("ko-KR", { - year: "numeric", - month: "2-digit", - day: "2-digit", - }).format(new Date(confirmedAt))} - - ) : ( - - - ); + return {formatDate(confirmedAt, "KR")}; }, size: 100, }, - - // ░░░ 생성일 ░░░ { accessorKey: "createdAt", header: ({ column }) => , cell: ({ row }) => { const createdAt = row.getValue("createdAt"); - return createdAt ? ( - - {new Intl.DateTimeFormat("ko-KR", { - year: "numeric", - month: "2-digit", - day: "2-digit", - }).format(new Date(createdAt))} - - ) : ( - - - ); + return {formatDate(createdAt, "KR")}; }, size: 100, }, - // ░░░ Actions ░░░ + // ✅ Actions - 가장 안전하게 처리 { id: "actions", enableHiding: false, size: 40, minSize: 40, cell: ({ row }) => { - return ( + // ✅ 함수를 직접 정의해서 매번 새로 생성되지 않도록 처리 + const handleEdit = () => { + setRowAction({ row, type: "update" }); + }; + + return (
-
); }, diff --git a/lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx b/lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx index 502ee974..c37258ae 100644 --- a/lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx +++ b/lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx @@ -365,7 +365,7 @@ export function EvaluationTargetFilterSheet({ setIsInitializing(true); form.reset({ - evaluationYear: new Date().getFullYear().toString(), + evaluationYear: "", division: "", status: "", domesticForeign: "", diff --git a/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx b/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx index 9043c588..7ea2e0ec 100644 --- a/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx +++ b/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx @@ -239,7 +239,7 @@ export function EvaluationTargetsTableToolbarActions({ /> {/* 선택 정보 표시 */} - {hasSelection && ( + {/* {hasSelection && (
선택된 {selectedRows.length}개 항목: 대기중 {selectedStats.pending}개, @@ -248,7 +248,7 @@ export function EvaluationTargetsTableToolbarActions({ {selectedStats.consensusTrue > 0 && ` | 의견일치 ${selectedStats.consensusTrue}개`} {selectedStats.consensusFalse > 0 && ` | 의견불일치 ${selectedStats.consensusFalse}개`}
- )} + )} */} ) } \ No newline at end of file diff --git a/lib/evaluation-target-list/table/update-evaluation-target.tsx b/lib/evaluation-target-list/table/update-evaluation-target.tsx index 0d56addb..9f9b7af4 100644 --- a/lib/evaluation-target-list/table/update-evaluation-target.tsx +++ b/lib/evaluation-target-list/table/update-evaluation-target.tsx @@ -498,7 +498,7 @@ export function EditEvaluationTargetSheet({ {/* 각 부서별 평가 */}
{[ - { key: "orderIsApproved", label: "주문 부서 평가", email: evaluationTarget.orderReviewerEmail }, + { key: "orderIsApproved", label: "발주 부서 평가", email: evaluationTarget.orderReviewerEmail }, { key: "procurementIsApproved", label: "조달 부서 평가", email: evaluationTarget.procurementReviewerEmail }, { key: "qualityIsApproved", label: "품질 부서 평가", email: evaluationTarget.qualityReviewerEmail }, { key: "designIsApproved", label: "설계 부서 평가", email: evaluationTarget.designReviewerEmail }, @@ -608,7 +608,7 @@ export function EditEvaluationTargetSheet({ {[ - { key: "orderReviewerEmail", label: "주문 부서 담당자", current: evaluationTarget.orderReviewerEmail }, + { key: "orderReviewerEmail", label: "발주 부서 담당자", current: evaluationTarget.orderReviewerEmail }, { key: "procurementReviewerEmail", label: "조달 부서 담당자", current: evaluationTarget.procurementReviewerEmail }, { key: "qualityReviewerEmail", label: "품질 부서 담당자", current: evaluationTarget.qualityReviewerEmail }, { key: "designReviewerEmail", label: "설계 부서 담당자", current: evaluationTarget.designReviewerEmail }, -- cgit v1.2.3