summaryrefslogtreecommitdiff
path: root/lib/evaluation
diff options
context:
space:
mode:
Diffstat (limited to 'lib/evaluation')
-rw-r--r--lib/evaluation/service.ts125
-rw-r--r--lib/evaluation/table/evaluation-columns.tsx35
-rw-r--r--lib/evaluation/table/evaluation-filter-sheet.tsx2
-rw-r--r--lib/evaluation/table/evaluation-table.tsx72
4 files changed, 184 insertions, 50 deletions
diff --git a/lib/evaluation/service.ts b/lib/evaluation/service.ts
index e69de29b..3cc4ca7d 100644
--- a/lib/evaluation/service.ts
+++ b/lib/evaluation/service.ts
@@ -0,0 +1,125 @@
+import db from "@/db/db"
+import {
+ periodicEvaluationsView,
+ type PeriodicEvaluationView
+} from "@/db/schema"
+import {
+ and,
+ asc,
+ count,
+ desc,
+ ilike,
+ or,
+ type SQL
+} from "drizzle-orm"
+import { filterColumns } from "@/lib/filter-columns"
+import { GetEvaluationTargetsSchema } from "../evaluation-target-list/validation";
+
+export async function getPeriodicEvaluations(input: GetEvaluationTargetsSchema) {
+ try {
+ const offset = (input.page - 1) * input.perPage;
+
+ // ✅ getEvaluationTargets 방식과 동일한 필터링 처리
+ // 1) 고급 필터 조건
+ let advancedWhere: SQL<unknown> | undefined = undefined;
+ if (input.filters && input.filters.length > 0) {
+ advancedWhere = filterColumns({
+ table: periodicEvaluationsView,
+ filters: input.filters,
+ joinOperator: input.joinOperator || 'and',
+ });
+ }
+
+ // 2) 기본 필터 조건
+ let basicWhere: SQL<unknown> | undefined = undefined;
+ if (input.basicFilters && input.basicFilters.length > 0) {
+ basicWhere = filterColumns({
+ table: periodicEvaluationsView,
+ filters: input.basicFilters,
+ joinOperator: input.basicJoinOperator || 'and',
+ });
+ }
+
+ // 3) 글로벌 검색 조건
+ let globalWhere: SQL<unknown> | undefined = undefined;
+ if (input.search) {
+ const s = `%${input.search}%`;
+
+ const validSearchConditions: SQL<unknown>[] = [];
+
+ // 벤더 정보로 검색
+ const vendorCodeCondition = ilike(periodicEvaluationsView.vendorCode, s);
+ if (vendorCodeCondition) validSearchConditions.push(vendorCodeCondition);
+
+ const vendorNameCondition = ilike(periodicEvaluationsView.vendorName, s);
+ if (vendorNameCondition) validSearchConditions.push(vendorNameCondition);
+
+ // 평가 관련 코멘트로 검색
+ const evaluationNoteCondition = ilike(periodicEvaluationsView.evaluationNote, s);
+ if (evaluationNoteCondition) validSearchConditions.push(evaluationNoteCondition);
+
+ const adminCommentCondition = ilike(periodicEvaluationsView.evaluationTargetAdminComment, s);
+ if (adminCommentCondition) validSearchConditions.push(adminCommentCondition);
+
+ const consolidatedCommentCondition = ilike(periodicEvaluationsView.evaluationTargetConsolidatedComment, s);
+ if (consolidatedCommentCondition) validSearchConditions.push(consolidatedCommentCondition);
+
+ // 최종 확정자 이름으로 검색
+ const finalizedByUserNameCondition = ilike(periodicEvaluationsView.finalizedByUserName, s);
+ if (finalizedByUserNameCondition) validSearchConditions.push(finalizedByUserNameCondition);
+
+ if (validSearchConditions.length > 0) {
+ globalWhere = or(...validSearchConditions);
+ }
+ }
+
+ // ✅ getEvaluationTargets 방식과 동일한 WHERE 조건 생성
+ const whereConditions: SQL<unknown>[] = [];
+
+ if (advancedWhere) whereConditions.push(advancedWhere);
+ if (basicWhere) whereConditions.push(basicWhere);
+ if (globalWhere) whereConditions.push(globalWhere);
+
+ const finalWhere = whereConditions.length > 0 ? and(...whereConditions) : undefined;
+
+ // ✅ getEvaluationTargets 방식과 동일한 전체 데이터 수 조회
+ const totalResult = await db
+ .select({ count: count() })
+ .from(periodicEvaluationsView)
+ .where(finalWhere);
+
+ const total = totalResult[0]?.count || 0;
+
+ if (total === 0) {
+ return { data: [], pageCount: 0, total: 0 };
+ }
+
+ console.log("Total periodic evaluations:", total);
+
+ // ✅ getEvaluationTargets 방식과 동일한 정렬 및 페이징 처리된 데이터 조회
+ const orderByColumns = input.sort.map((sort) => {
+ const column = sort.id as keyof typeof periodicEvaluationsView.$inferSelect;
+ return sort.desc ? desc(periodicEvaluationsView[column]) : asc(periodicEvaluationsView[column]);
+ });
+
+ if (orderByColumns.length === 0) {
+ orderByColumns.push(desc(periodicEvaluationsView.createdAt));
+ }
+
+ const periodicEvaluationsData = await db
+ .select()
+ .from(periodicEvaluationsView)
+ .where(finalWhere)
+ .orderBy(...orderByColumns)
+ .limit(input.perPage)
+ .offset(offset);
+
+ const pageCount = Math.ceil(total / input.perPage);
+
+ return { data: periodicEvaluationsData, pageCount, total };
+ } catch (err) {
+ console.error("Error in getPeriodicEvaluations:", err);
+ // ✅ getEvaluationTargets 방식과 동일한 에러 반환 (total 포함)
+ return { data: [], pageCount: 0, total: 0 };
+ }
+ } \ No newline at end of file
diff --git a/lib/evaluation/table/evaluation-columns.tsx b/lib/evaluation/table/evaluation-columns.tsx
index 0c207a53..821e8182 100644
--- a/lib/evaluation/table/evaluation-columns.tsx
+++ b/lib/evaluation/table/evaluation-columns.tsx
@@ -11,6 +11,7 @@ import { Button } from "@/components/ui/button";
import { Pencil, Eye, MessageSquare, Check, X, Clock, FileText } from "lucide-react";
import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header";
import { PeriodicEvaluationView } from "@/db/schema";
+import { DataTableRowAction } from "@/types/table";
@@ -104,7 +105,7 @@ const getProgressBadge = (completed: number, total: number) => {
return <Badge variant={variant}>{completed}/{total} ({percentage}%)</Badge>;
};
-export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps): ColumnDef<PeriodicEvaluationWithRelations>[] {
+export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps): ColumnDef<PeriodicEvaluationView>[] {
return [
// ═══════════════════════════════════════════════════════════════
// 선택 및 기본 정보
@@ -136,9 +137,9 @@ export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps):
// ░░░ 평가년도 ░░░
{
- accessorKey: "evaluationTarget.evaluationYear",
+ accessorKey: "evaluationYear",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가년도" />,
- cell: ({ row }) => <span className="font-medium">{row.original.evaluationTarget?.evaluationYear}</span>,
+ cell: ({ row }) => <span className="font-medium">{row.original.evaluationYear}</span>,
size: 100,
},
@@ -154,9 +155,9 @@ export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps):
// ░░░ 구분 ░░░
{
- accessorKey: "evaluationTarget.division",
+ accessorKey: "division",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="구분" />,
- cell: ({ row }) => getDivisionBadge(row.original.evaluationTarget?.division || ""),
+ cell: ({ row }) => getDivisionBadge(row.original.division || ""),
size: 80,
},
@@ -167,36 +168,36 @@ export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps):
header: "협력업체 정보",
columns: [
{
- accessorKey: "evaluationTarget.vendorCode",
+ accessorKey: "vendorCode",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더 코드" />,
cell: ({ row }) => (
- <span className="font-mono text-sm">{row.original.evaluationTarget?.vendorCode}</span>
+ <span className="font-mono text-sm">{row.original.vendorCode}</span>
),
size: 120,
},
{
- accessorKey: "evaluationTarget.vendorName",
+ accessorKey: "vendorName",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더명" />,
cell: ({ row }) => (
- <div className="truncate max-w-[200px]" title={row.original.evaluationTarget?.vendorName}>
- {row.original.evaluationTarget?.vendorName}
+ <div className="truncate max-w-[200px]" title={row.original.vendorName}>
+ {row.original.vendorName}
</div>
),
size: 200,
},
{
- accessorKey: "evaluationTarget.domesticForeign",
+ accessorKey: "domesticForeign",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="내외자" />,
- cell: ({ row }) => getDomesticForeignBadge(row.original.evaluationTarget?.domesticForeign || ""),
+ cell: ({ row }) => getDomesticForeignBadge(row.original.domesticForeign || ""),
size: 80,
},
{
- accessorKey: "evaluationTarget.materialType",
+ accessorKey: "materialType",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재구분" />,
- cell: ({ row }) => getMaterialTypeBadge(row.original.evaluationTarget?.materialType || ""),
+ cell: ({ row }) => getMaterialTypeBadge(row.original.materialType || ""),
size: 120,
},
]
@@ -355,10 +356,10 @@ export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps):
id: "reviewProgress",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="리뷰진행" />,
cell: ({ row }) => {
- const stats = row.original.reviewerStats;
- if (!stats) return <span className="text-muted-foreground">-</span>;
+ const totalReviewers = row.original.totalReviewers || 0;
+ const completedReviewers = row.original.completedReviewers || 0;
- return getProgressBadge(stats.completedReviewers, stats.totalReviewers);
+ return getProgressBadge(completedReviewers, totalReviewers);
},
size: 120,
},
diff --git a/lib/evaluation/table/evaluation-filter-sheet.tsx b/lib/evaluation/table/evaluation-filter-sheet.tsx
index 7cda4989..7c1e93d8 100644
--- a/lib/evaluation/table/evaluation-filter-sheet.tsx
+++ b/lib/evaluation/table/evaluation-filter-sheet.tsx
@@ -394,7 +394,7 @@ export function PeriodicEvaluationFilterSheet({
}
return (
- <div className="flex flex-col h-full max-h-full bg-[#F5F7FB] px-6 sm:px-8">
+ <div className="flex flex-col h-full max-h-full bg-[#F5F7FB] px-6 sm:px-8" style={{backgroundColor:"#F5F7FB", paddingLeft:"2rem", paddingRight:"2rem"}}>
{/* Filter Panel Header */}
<div className="flex items-center justify-between px-6 min-h-[60px] shrink-0">
<h3 className="text-lg font-semibold whitespace-nowrap">정기평가 검색 필터</h3>
diff --git a/lib/evaluation/table/evaluation-table.tsx b/lib/evaluation/table/evaluation-table.tsx
index 16f70592..a628475d 100644
--- a/lib/evaluation/table/evaluation-table.tsx
+++ b/lib/evaluation/table/evaluation-table.tsx
@@ -23,6 +23,7 @@ import { useMemo } from "react"
import { PeriodicEvaluationFilterSheet } from "./evaluation-filter-sheet"
import { getPeriodicEvaluationsColumns } from "./evaluation-columns"
import { PeriodicEvaluationView } from "@/db/schema"
+import { getPeriodicEvaluations } from "../service"
interface PeriodicEvaluationsTableProps {
promises: Promise<[Awaited<ReturnType<typeof getPeriodicEvaluations>>]>
@@ -229,16 +230,33 @@ export function PeriodicEvaluationsTable({ promises, evaluationYear, className }
const [promiseData] = React.use(promises)
const tableData = promiseData
+ const getSearchParam = React.useCallback((key: string, defaultValue?: string): string => {
+ return searchParams?.get(key) ?? defaultValue ?? "";
+ }, [searchParams]);
+
+ const parseSearchParamHelper = React.useCallback((key: string, defaultValue: any): any => {
+ try {
+ const value = getSearchParam(key);
+ return value ? JSON.parse(value) : defaultValue;
+ } catch {
+ return defaultValue;
+ }
+ }, [getSearchParam]);
+
+ const parseSearchParam = <T,>(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: getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }],
+ filters: getSearchParam('filters') ? JSON.parse(getSearchParam('filters')!) : [],
+ joinOperator: (getSearchParam('joinOperator') as "and" | "or") || "and",
+ basicFilters: getSearchParam('basicFilters') ?
+ JSON.parse(getSearchParam('basicFilters')!) : [],
+ basicJoinOperator: (getSearchParam('basicJoinOperator') as "and" | "or") || "and",
+ search: getSearchParam('search') || '',
columnVisibility: {},
columnOrder: [],
pinnedColumns: { left: [], right: ["actions"] },
@@ -267,22 +285,22 @@ export function PeriodicEvaluationsTable({ promises, evaluationYear, className }
)
const filterFields: DataTableFilterField<PeriodicEvaluationView>[] = [
- { id: "evaluationTarget.vendorCode", label: "벤더 코드" },
- { id: "evaluationTarget.vendorName", label: "벤더명" },
+ { id: "vendorCode", label: "벤더 코드" },
+ { id: "vendorName", label: "벤더명" },
{ id: "status", label: "진행상태" },
]
const advancedFilterFields: DataTableAdvancedFilterField<PeriodicEvaluationView>[] = [
- { id: "evaluationTarget.evaluationYear", label: "평가년도", type: "number" },
+ { id: "evaluationYear", label: "평가년도", type: "number" },
{ id: "evaluationPeriod", label: "평가기간", type: "text" },
{
- id: "evaluationTarget.division", label: "구분", type: "select", options: [
+ id: "division", label: "구분", type: "select", options: [
{ label: "해양", value: "PLANT" },
{ label: "조선", value: "SHIP" },
]
},
- { id: "evaluationTarget.vendorCode", label: "벤더 코드", type: "text" },
- { id: "evaluationTarget.vendorName", label: "벤더명", type: "text" },
+ { id: "vendorCode", label: "벤더 코드", type: "text" },
+ { id: "vendorName", label: "벤더명", type: "text" },
{
id: "status", label: "진행상태", type: "select", options: [
{ label: "제출대기", value: "PENDING_SUBMISSION" },
@@ -305,24 +323,14 @@ export function PeriodicEvaluationsTable({ promises, evaluationYear, className }
{ id: "finalizedAt", label: "최종확정일", type: "date" },
]
- const currentSettings = useMemo(() => {
- return getCurrentSettings()
- }, [getCurrentSettings])
+ const currentSettings = React.useMemo(() => getCurrentSettings(), [getCurrentSettings]);
- function getColKey<T>(c: ColumnDef<T>): 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 = 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]);
- 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])
const { table } = useDataTable({
data: tableData.data,
@@ -344,7 +352,7 @@ export function PeriodicEvaluationsTable({ promises, evaluationYear, className }
const getActiveBasicFilterCount = () => {
try {
- const basicFilters = searchParams.get('basicFilters')
+ const basicFilters = getSearchParam('basicFilters')
return basicFilters ? JSON.parse(basicFilters).length : 0
} catch (e) {
return 0