summaryrefslogtreecommitdiff
path: root/lib/evaluation-target-list/table
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-06-24 01:51:59 +0000
committerjoonhoekim <26rote@gmail.com>2025-06-24 01:51:59 +0000
commit6824e097d768f724cf439b410ccfb1ab9685ac98 (patch)
tree1f297313637878e7a4ad6c89b84d5a2c3e9eb650 /lib/evaluation-target-list/table
parentf4825dd3853188de4688fb4a56c0f4e847da314b (diff)
parent4e63d8427d26d0d1b366ddc53650e15f3481fc75 (diff)
(merge) 대표님/최겸 작업사항 머지
Diffstat (limited to 'lib/evaluation-target-list/table')
-rw-r--r--lib/evaluation-target-list/table/evaluation-target-table.tsx543
-rw-r--r--lib/evaluation-target-list/table/evaluation-targets-columns.tsx351
-rw-r--r--lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx2
-rw-r--r--lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx4
-rw-r--r--lib/evaluation-target-list/table/update-evaluation-target.tsx4
5 files changed, 373 insertions, 531 deletions
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<ReturnType<typeof getEvaluationTargets>>]>
- 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<any>(null)
- const [isLoading, setIsLoading] = React.useState(true)
- const [error, setError] = React.useState<string | null>(null)
+ const [stats, setStats] = React.useState<any>(null);
+ const [isLoading, setIsLoading] = React.useState(true);
+ const [error, setError] = React.useState<string | null>(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 (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6">
{Array.from({ length: 4 }).map((_, i) => (
@@ -84,42 +69,32 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number })
</Card>
))}
</div>
- )
- }
-
- if (error) {
+ );
+ if (error)
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6">
<Card className="col-span-full">
- <CardContent className="pt-6">
- <div className="text-center text-sm text-muted-foreground">
- 통계 데이터를 불러올 수 없습니다: {error}
- </div>
+ <CardContent className="pt-6 text-center text-sm text-muted-foreground">
+ 통계 데이터를 불러올 수 없습니다: {error}
</CardContent>
</Card>
</div>
- )
- }
-
- if (!stats) {
+ );
+ if (!stats)
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6">
<Card className="col-span-full">
- <CardContent className="pt-6">
- <div className="text-center text-sm text-muted-foreground">
- 통계 데이터가 없습니다.
- </div>
+ <CardContent className="pt-6 text-center text-sm text-muted-foreground">
+ 통계 데이터가 없습니다.
</CardContent>
</Card>
</div>
- )
- }
+ );
- 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 (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6">
@@ -130,7 +105,7 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number })
<Badge variant="outline">{evaluationYear}년</Badge>
</CardHeader>
<CardContent>
- <div className="text-2xl font-bold">{totalTargets.toLocaleString()}</div>
+ <div className="text-2xl font-bold">{total.toLocaleString()}</div>
<div className="text-xs text-muted-foreground mt-1">
해양 {stats.oceanDivision || 0}개 | 조선 {stats.shipyardDivision || 0}개
</div>
@@ -144,9 +119,9 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number })
<Badge variant="secondary">대기</Badge>
</CardHeader>
<CardContent>
- <div className="text-2xl font-bold text-orange-600">{pendingTargets.toLocaleString()}</div>
+ <div className="text-2xl font-bold text-orange-600">{pending.toLocaleString()}</div>
<div className="text-xs text-muted-foreground mt-1">
- {totalTargets > 0 ? Math.round((pendingTargets / totalTargets) * 100) : 0}% of total
+ {total ? Math.round((pending / total) * 100) : 0}% of total
</div>
</CardContent>
</Card>
@@ -155,12 +130,12 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number })
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">확정</CardTitle>
- <Badge variant="default" className="bg-green-600">완료</Badge>
+ <Badge variant="default">완료</Badge>
</CardHeader>
<CardContent>
- <div className="text-2xl font-bold text-green-600">{confirmedTargets.toLocaleString()}</div>
+ <div className="text-2xl font-bold text-green-600">{confirmed.toLocaleString()}</div>
<div className="text-xs text-muted-foreground mt-1">
- {totalTargets > 0 ? Math.round((confirmedTargets / totalTargets) * 100) : 0}% of total
+ {total ? Math.round((confirmed / total) * 100) : 0}% of total
</div>
</CardContent>
</Card>
@@ -169,9 +144,7 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number })
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">의견 일치율</CardTitle>
- <Badge variant={consensusRate >= 80 ? "default" : consensusRate >= 60 ? "secondary" : "destructive"}>
- {consensusRate}%
- </Badge>
+ <Badge variant={consensusRate >= 80 ? "default" : consensusRate >= 60 ? "secondary" : "destructive"}>{consensusRate}%</Badge>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{consensusRate}%</div>
@@ -181,83 +154,92 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number })
</CardContent>
</Card>
</div>
- )
+ );
+}
+
+/* -------------------------------------------------------------------------- */
+/* EvaluationTargetsTable */
+/* -------------------------------------------------------------------------- */
+interface EvaluationTargetsTableProps {
+ promises: Promise<[Awaited<ReturnType<typeof getEvaluationTargets>>]>;
+ evaluationYear: number;
+ className?: string;
}
export function EvaluationTargetsTable({ promises, evaluationYear, className }: EvaluationTargetsTableProps) {
- const [rowAction, setRowAction] = React.useState<DataTableRowAction<EvaluationTargetWithDepartments> | null>(null)
- const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false)
- const router = useRouter()
- const searchParams = useSearchParams()
+ const [rowAction, setRowAction] = React.useState<DataTableRowAction<EvaluationTargetWithDepartments> | null>(null);
+ const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false);
+ const searchParams = useSearchParams();
- const containerRef = React.useRef<HTMLDivElement>(null)
- const [containerTop, setContainerTop] = React.useState(0)
+ /* --------------------------- layout refs --------------------------- */
+ const containerRef = React.useRef<HTMLDivElement>(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 = <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: 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<EvaluationTargetWithDepartments>('evaluation-targets-table', initialSettings)
-
- const columns = React.useMemo(
- () => getEvaluationTargetsColumns({ setRowAction }),
- [setRowAction]
- )
-
+ } = useTablePresets<EvaluationTargetWithDepartments>(
+ "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<EvaluationTargetWithDepartments>[] = [
{ id: "vendorCode", label: "벤더 코드" },
{ id: "vendorName", label: "벤더명" },
{ id: "status", label: "상태" },
- ]
+ ];
+ /* 고급 필터 */
const advancedFilterFields: DataTableAdvancedFilterField<EvaluationTargetWithDepartments>[] = [
{ 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<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 = 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 */}
<div
className={cn(
"fixed left-0 bg-background border-r z-50 flex flex-col transition-all duration-300 ease-in-out overflow-hidden",
isFilterPanelOpen ? "border-r shadow-lg" : "border-r-0"
)}
- style={{
- width: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px',
+ style={{
+ width: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : "0px",
top: `${containerTop}px`,
height: `calc(100vh - ${containerTop}px)`
}}
>
- <div className="h-full">
- <EvaluationTargetFilterSheet
- isOpen={isFilterPanelOpen}
- onClose={() => setIsFilterPanelOpen(false)}
- onSearch={handleSearch}
- isLoading={false}
- />
- </div>
+ <EvaluationTargetFilterSheet
+ isOpen={isFilterPanelOpen}
+ onClose={() => setIsFilterPanelOpen(false)}
+ onSearch={() => setIsFilterPanelOpen(false)}
+ isLoading={false}
+ />
</div>
- {/* Main Content Container */}
- <div
- ref={containerRef}
- className={cn("relative w-full overflow-hidden", className)}
- >
+ {/* Main Container */}
+ <div ref={containerRef} className={cn("relative w-full overflow-hidden", className)}>
<div className="flex w-full h-full">
<div
className="flex flex-col min-w-0 overflow-hidden transition-all duration-300 ease-in-out"
style={{
- width: isFilterPanelOpen ? `calc(100% - ${FILTER_PANEL_WIDTH}px)` : '100%',
- marginLeft: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px'
+ width: isFilterPanelOpen ? `calc(100% - ${FILTER_PANEL_WIDTH}px)` : "100%",
+ marginLeft: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : "0px",
}}
>
- {/* Header Bar */}
+ {/* Header */}
<div className="flex items-center justify-between p-4 bg-background shrink-0">
- <div className="flex items-center gap-3">
- <Button
- variant="outline"
- size="sm"
- type='button'
- onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)}
- className="flex items-center shadow-sm"
- >
- {isFilterPanelOpen ? <PanelLeftClose className="size-4" /> : <PanelLeftOpen className="size-4" />}
- {getActiveBasicFilterCount() > 0 && (
- <span className="ml-2 bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs">
- {getActiveBasicFilterCount()}
- </span>
- )}
- </Button>
- </div>
-
- <div className="text-sm text-muted-foreground">
- {tableData && (
- <span>총 {tableData.total || tableData.data.length}건</span>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)}
+ className="flex items-center shadow-sm"
+ >
+ {isFilterPanelOpen ? <PanelLeftClose className="size-4" /> : <PanelLeftOpen className="size-4" />}
+ {getActiveBasicFilterCount() > 0 && (
+ <span className="ml-2 bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs">
+ {getActiveBasicFilterCount()}
+ </span>
)}
+ </Button>
+ <div className="text-sm text-muted-foreground">
+ 총 {tableData.total || tableData.data.length}건
</div>
</div>
- {/* 통계 카드들 */}
+ {/* Stats */}
<div className="px-4">
<EvaluationTargetsStats evaluationYear={evaluationYear} />
</div>
- {/* Table Content Area */}
- <div className="flex-1 overflow-hidden" style={{ height: 'calc(100vh - 500px)' }}>
- <div className="h-full w-full">
- <DataTable table={table} className="h-full">
- <DataTableAdvancedToolbar
- table={table}
- filterFields={advancedFilterFields}
- shallow={false}
- >
- <div className="flex items-center gap-2">
- <TablePresetManager<EvaluationTargetWithDepartments>
- presets={presets}
- activePresetId={activePresetId}
- currentSettings={currentSettings}
- hasUnsavedChanges={hasUnsavedChanges}
- isLoading={presetsLoading}
- onCreatePreset={createPreset}
- onUpdatePreset={updatePreset}
- onDeletePreset={deletePreset}
- onApplyPreset={applyPreset}
- onSetDefaultPreset={setDefaultPreset}
- onRenamePreset={renamePreset}
- />
-
- <EvaluationTargetsTableToolbarActions table={table} />
- </div>
- </DataTableAdvancedToolbar>
- </DataTable>
-
- <EditEvaluationTargetSheet
- open={rowAction?.type === "update"}
- onOpenChange={() => setRowAction(null)}
- evaluationTarget={rowAction?.row.original ?? null}
- />
-
- </div>
+ {/* Table */}
+ <div className="flex-1 overflow-hidden" style={{ height: "calc(100vh - 500px)" }}>
+ <DataTable table={table} className="h-full">
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ shallow={false}
+ >
+ <div className="flex items-center gap-2">
+ <TablePresetManager<EvaluationTargetWithDepartments>
+ presets={presets}
+ activePresetId={activePresetId}
+ currentSettings={currentSettings}
+ hasUnsavedChanges={hasUnsavedChanges}
+ isLoading={presetsLoading}
+ onCreatePreset={createPreset}
+ onUpdatePreset={updatePreset}
+ onDeletePreset={deletePreset}
+ onApplyPreset={applyPreset}
+ onSetDefaultPreset={setDefaultPreset}
+ onRenamePreset={renamePreset}
+ />
+
+ <EvaluationTargetsTableToolbarActions table={table} />
+ </div>
+ </DataTableAdvancedToolbar>
+ </DataTable>
+
+ {/* 편집 다이얼로그 */}
+ <EditEvaluationTargetSheet
+ open={rowAction?.type === "update"}
+ onOpenChange={() => setRowAction(null)}
+ evaluationTarget={rowAction?.row.original ?? null}
+ />
</div>
</div>
</div>
</div>
</>
- )
+ );
} \ 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<React.SetStateAction<DataTableRowAction<EvaluationTargetWithDepartments> | 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 <Badge variant="outline">검토 중</Badge>;
@@ -38,16 +47,14 @@ const getConsensusBadge = (consensusStatus: boolean | null) => {
return <Badge variant="destructive">의견 불일치</Badge>;
};
-// 구분 배지
const getDivisionBadge = (division: string) => {
return (
- <Badge variant={division === "PLANT" ? "default" : "secondary"}>
- {division === "PLANT" ? "해양" : "조선"}
+ <Badge variant={division === "OCEAN" ? "default" : "secondary"}>
+ {division === "OCEAN" ? "해양" : "조선"}
</Badge>
);
};
-// 자재구분 배지
const getMaterialTypeBadge = (materialType: string) => {
const typeMap = {
EQUIPMENT: "기자재",
@@ -57,7 +64,6 @@ const getMaterialTypeBadge = (materialType: string) => {
return <Badge variant="outline">{typeMap[materialType] || materialType}</Badge>;
};
-// 내외자 배지
const getDomesticForeignBadge = (domesticForeign: string) => {
return (
<Badge variant={domesticForeign === "DOMESTIC" ? "default" : "secondary"}>
@@ -66,24 +72,27 @@ const getDomesticForeignBadge = (domesticForeign: string) => {
);
};
-// 평가 상태 배지
-const getApprovalBadge = (isApproved: boolean | null) => {
- if (isApproved === null) {
- return <Badge variant="outline" className="text-xs">대기중</Badge>;
- }
- if (isApproved === true) {
- return <Badge variant="default" className="bg-green-600 text-xs">승인</Badge>;
+// ✅ 평가 대상 여부 표시 함수
+const getEvaluationTargetBadge = (isTarget: boolean | null) => {
+ if (isTarget === null) {
+ return <Badge variant="outline">미정</Badge>;
}
- return <Badge variant="destructive" className="text-xs">거부</Badge>;
+ return isTarget ? (
+ <Badge variant="default" className="bg-blue-600">
+ <Check className="size-3 mr-1" />
+ 평가 대상
+ </Badge>
+ ) : (
+ <Badge variant="secondary">
+ <X className="size-3 mr-1" />
+ 평가 제외
+ </Badge>
+ );
};
-export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): ColumnDef<EvaluationTargetWithDepartments>[] {
+export function getEvaluationTargetsColumns({ setRowAction }: GetColumnsProps): ColumnDef<EvaluationTargetWithDepartments>[] {
return [
- // ═══════════════════════════════════════════════════════════════
- // 기본 정보
- // ═══════════════════════════════════════════════════════════════
-
- // Checkbox
+ // ✅ Checkbox
{
id: "select",
header: ({ table }) => (
@@ -107,15 +116,13 @@ export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): Col
enableHiding: false,
},
- // ░░░ 평가년도 ░░░
+ // ✅ 기본 정보
{
accessorKey: "evaluationYear",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가년도" />,
cell: ({ row }) => <span className="font-medium">{row.getValue("evaluationYear")}</span>,
size: 100,
},
-
- // ░░░ 구분 ░░░
{
accessorKey: "division",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="구분" />,
@@ -127,24 +134,25 @@ export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): Col
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="상태" />,
cell: ({ row }) => {
const status = row.getValue<string>("status");
- const statusMap = {
- PENDING: "검토 중",
- CONFIRMED: "확정",
- EXCLUDED: "제외"
- };
return (
<Badge variant={getStatusBadgeVariant(status)}>
- {statusMap[status] || status}
+ {getStatusText(status)}
</Badge>
);
},
size: 100,
},
+ {
+ accessorKey: "consensusStatus",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="의견 일치" />,
+ 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 }) => <DataTableColumnHeaderSimple column={column} title="벤더명" />,
@@ -166,267 +172,182 @@ export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): Col
),
size: 200,
},
-
- // ░░░ 내외자 ░░░
{
accessorKey: "domesticForeign",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="내외자" />,
cell: ({ row }) => getDomesticForeignBadge(row.getValue("domesticForeign")),
size: 80,
},
-
+ {
+ accessorKey: "materialType",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재구분" />,
+ cell: ({ row }) => getMaterialTypeBadge(row.getValue("materialType")),
+ size: 120,
+ },
]
},
- // ░░░ 자재구분 ░░░
- {
- accessorKey: "materialType",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재구분" />,
- cell: ({ row }) => getMaterialTypeBadge(row.getValue("materialType")),
- size: 120,
- },
-
- // ░░░ 상태 ░░░
-
-
- // ░░░ 의견 일치 여부 ░░░
- {
- accessorKey: "consensusStatus",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="의견 일치" />,
- cell: ({ row }) => getConsensusBadge(row.getValue("consensusStatus")),
- size: 100,
- },
-
- // ═══════════════════════════════════════════════════════════════
- // 주문 부서 그룹
- // ═══════════════════════════════════════════════════════════════
+ // ✅ 발주 담당자
{
- header: "발주 평가 담당자",
+ id: "orderReviewer",
+ header: "발주 담당자",
columns: [
{
- accessorKey: "orderDepartmentName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />,
- cell: ({ row }) => {
- const departmentName = row.getValue<string>("orderDepartmentName");
- return departmentName ? (
- <div className="truncate max-w-[120px]" title={departmentName}>
- {departmentName}
- </div>
- ) : (
- <span className="text-muted-foreground">-</span>
- );
- },
- size: 120,
- },
- {
accessorKey: "orderReviewerName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />,
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자명" />,
cell: ({ row }) => {
const reviewerName = row.getValue<string>("orderReviewerName");
return reviewerName ? (
- <div className="truncate max-w-[100px]" title={reviewerName}>
+ <div className="truncate max-w-[120px]" title={reviewerName}>
{reviewerName}
</div>
) : (
<span className="text-muted-foreground">-</span>
);
},
- size: 100,
+ size: 120,
},
{
accessorKey: "orderIsApproved",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />,
- cell: ({ row }) => getApprovalBadge(row.getValue("orderIsApproved")),
- size: 80,
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가 대상" />,
+ cell: ({ row }) => {
+ const isApproved = row.getValue<boolean>("orderIsApproved");
+ return getEvaluationTargetBadge(isApproved);
+ },
+ size: 120,
},
- ],
+ ]
},
- // ═══════════════════════════════════════════════════════════════
- // 조달 부서 그룹
- // ═══════════════════════════════════════════════════════════════
+ // ✅ 조달 담당자
{
- header: "조달 평가 담당자",
+ id: "procurementReviewer",
+ header: "조달 담당자",
columns: [
{
- accessorKey: "procurementDepartmentName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />,
- cell: ({ row }) => {
- const departmentName = row.getValue<string>("procurementDepartmentName");
- return departmentName ? (
- <div className="truncate max-w-[120px]" title={departmentName}>
- {departmentName}
- </div>
- ) : (
- <span className="text-muted-foreground">-</span>
- );
- },
- size: 120,
- },
- {
accessorKey: "procurementReviewerName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />,
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자명" />,
cell: ({ row }) => {
const reviewerName = row.getValue<string>("procurementReviewerName");
return reviewerName ? (
- <div className="truncate max-w-[100px]" title={reviewerName}>
+ <div className="truncate max-w-[120px]" title={reviewerName}>
{reviewerName}
</div>
) : (
<span className="text-muted-foreground">-</span>
);
},
- size: 100,
+ size: 120,
},
{
accessorKey: "procurementIsApproved",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />,
- cell: ({ row }) => getApprovalBadge(row.getValue("procurementIsApproved")),
- size: 80,
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가 대상" />,
+ cell: ({ row }) => {
+ const isApproved = row.getValue<boolean>("procurementIsApproved");
+ return getEvaluationTargetBadge(isApproved);
+ },
+ size: 120,
},
- ],
+ ]
},
- // ═══════════════════════════════════════════════════════════════
- // 품질 부서 그룹
- // ═══════════════════════════════════════════════════════════════
+ // ✅ 품질 담당자
{
- header: "품질 평가 담당자",
+ id: "qualityReviewer",
+ header: "품질 담당자",
columns: [
{
- accessorKey: "qualityDepartmentName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />,
- cell: ({ row }) => {
- const departmentName = row.getValue<string>("qualityDepartmentName");
- return departmentName ? (
- <div className="truncate max-w-[120px]" title={departmentName}>
- {departmentName}
- </div>
- ) : (
- <span className="text-muted-foreground">-</span>
- );
- },
- size: 120,
- },
- {
accessorKey: "qualityReviewerName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />,
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자명" />,
cell: ({ row }) => {
const reviewerName = row.getValue<string>("qualityReviewerName");
return reviewerName ? (
- <div className="truncate max-w-[100px]" title={reviewerName}>
+ <div className="truncate max-w-[120px]" title={reviewerName}>
{reviewerName}
</div>
) : (
<span className="text-muted-foreground">-</span>
);
},
- size: 100,
+ size: 120,
},
{
accessorKey: "qualityIsApproved",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />,
- cell: ({ row }) => getApprovalBadge(row.getValue("qualityIsApproved")),
- size: 80,
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가 대상" />,
+ cell: ({ row }) => {
+ const isApproved = row.getValue<boolean>("qualityIsApproved");
+ return getEvaluationTargetBadge(isApproved);
+ },
+ size: 120,
},
- ],
+ ]
},
- // ═══════════════════════════════════════════════════════════════
- // 설계 부서 그룹
- // ═══════════════════════════════════════════════════════════════
+ // ✅ 설계 담당자
{
- header: "설계 평가 담당자",
+ id: "designReviewer",
+ header: "설계 담당자",
columns: [
{
- accessorKey: "designDepartmentName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />,
- cell: ({ row }) => {
- const departmentName = row.getValue<string>("designDepartmentName");
- return departmentName ? (
- <div className="truncate max-w-[120px]" title={departmentName}>
- {departmentName}
- </div>
- ) : (
- <span className="text-muted-foreground">-</span>
- );
- },
- size: 120,
- },
- {
accessorKey: "designReviewerName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />,
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자명" />,
cell: ({ row }) => {
const reviewerName = row.getValue<string>("designReviewerName");
return reviewerName ? (
- <div className="truncate max-w-[100px]" title={reviewerName}>
+ <div className="truncate max-w-[120px]" title={reviewerName}>
{reviewerName}
</div>
) : (
<span className="text-muted-foreground">-</span>
);
},
- size: 100,
+ size: 120,
},
{
accessorKey: "designIsApproved",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />,
- cell: ({ row }) => getApprovalBadge(row.getValue("designIsApproved")),
- size: 80,
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가 대상" />,
+ cell: ({ row }) => {
+ const isApproved = row.getValue<boolean>("designIsApproved");
+ return getEvaluationTargetBadge(isApproved);
+ },
+ size: 120,
},
- ],
+ ]
},
- // ═══════════════════════════════════════════════════════════════
- // CS 부서 그룹
- // ═══════════════════════════════════════════════════════════════
+ // ✅ CS 담당자
{
- header: "CS 평가 담당자",
+ id: "csReviewer",
+ header: "CS 담당자",
columns: [
{
- accessorKey: "csDepartmentName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />,
- cell: ({ row }) => {
- const departmentName = row.getValue<string>("csDepartmentName");
- return departmentName ? (
- <div className="truncate max-w-[120px]" title={departmentName}>
- {departmentName}
- </div>
- ) : (
- <span className="text-muted-foreground">-</span>
- );
- },
- size: 120,
- },
- {
accessorKey: "csReviewerName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />,
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자명" />,
cell: ({ row }) => {
const reviewerName = row.getValue<string>("csReviewerName");
return reviewerName ? (
- <div className="truncate max-w-[100px]" title={reviewerName}>
+ <div className="truncate max-w-[120px]" title={reviewerName}>
{reviewerName}
</div>
) : (
<span className="text-muted-foreground">-</span>
);
},
- size: 100,
+ size: 120,
},
{
accessorKey: "csIsApproved",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />,
- cell: ({ row }) => getApprovalBadge(row.getValue("csIsApproved")),
- size: 80,
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가 대상" />,
+ cell: ({ row }) => {
+ const isApproved = row.getValue<boolean>("csIsApproved");
+ return getEvaluationTargetBadge(isApproved);
+ },
+ size: 120,
},
- ],
+ ]
},
- // ═══════════════════════════════════════════════════════════════
- // 관리 정보
- // ═══════════════════════════════════════════════════════════════
-
- // ░░░ 관리자 의견 ░░░
+ // ✅ 의견 및 결과
{
accessorKey: "adminComment",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="관리자 의견" />,
@@ -442,8 +363,6 @@ export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): Col
},
size: 150,
},
-
- // ░░░ 종합 의견 ░░░
{
accessorKey: "consolidatedComment",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="종합 의견" />,
@@ -459,69 +378,49 @@ export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): Col
},
size: 150,
},
-
- // ░░░ 확정일 ░░░
{
accessorKey: "confirmedAt",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="확정일" />,
cell: ({ row }) => {
const confirmedAt = row.getValue<Date>("confirmedAt");
- return confirmedAt ? (
- <span className="text-sm">
- {new Intl.DateTimeFormat("ko-KR", {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- }).format(new Date(confirmedAt))}
- </span>
- ) : (
- <span className="text-muted-foreground">-</span>
- );
+ return <span className="text-sm">{formatDate(confirmedAt, "KR")}</span>;
},
size: 100,
},
-
- // ░░░ 생성일 ░░░
{
accessorKey: "createdAt",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="생성일" />,
cell: ({ row }) => {
const createdAt = row.getValue<Date>("createdAt");
- return createdAt ? (
- <span className="text-sm">
- {new Intl.DateTimeFormat("ko-KR", {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- }).format(new Date(createdAt))}
- </span>
- ) : (
- <span className="text-muted-foreground">-</span>
- );
+ return <span className="text-sm">{formatDate(createdAt, "KR")}</span>;
},
size: 100,
},
- // ░░░ Actions ░░░
+ // ✅ Actions - 가장 안전하게 처리
{
id: "actions",
enableHiding: false,
size: 40,
minSize: 40,
cell: ({ row }) => {
- return (
+ // ✅ 함수를 직접 정의해서 매번 새로 생성되지 않도록 처리
+ const handleEdit = () => {
+ setRowAction({ row, type: "update" });
+ };
+
+ return (
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="size-8"
- onClick={() => setRowAction({ row, type: "update" })}
+ onClick={handleEdit}
aria-label="수정"
title="수정"
>
<Pencil className="size-4" />
</Button>
-
</div>
);
},
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 && (
<div className="text-xs text-muted-foreground">
선택된 {selectedRows.length}개 항목:
대기중 {selectedStats.pending}개,
@@ -248,7 +248,7 @@ export function EvaluationTargetsTableToolbarActions({
{selectedStats.consensusTrue > 0 && ` | 의견일치 ${selectedStats.consensusTrue}개`}
{selectedStats.consensusFalse > 0 && ` | 의견불일치 ${selectedStats.consensusFalse}개`}
</div>
- )}
+ )} */}
</>
)
} \ 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({
{/* 각 부서별 평가 */}
<div className="grid grid-cols-1 gap-4">
{[
- { 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({
</CardHeader>
<CardContent className="space-y-4">
{[
- { 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 },