summaryrefslogtreecommitdiff
path: root/lib/evaluation-target-list/table
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-24 11:06:32 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-24 11:06:32 +0000
commit1dc24d48e52f2e490f5603ceb02842586ecae533 (patch)
tree8fca2c5b5b52cc10557b5ba6e55b937ae3c57cf6 /lib/evaluation-target-list/table
parented0d6fcc98f671280c2ccde797b50693da88152e (diff)
(대표님) 정기평가 피드백 반영, 설계 피드백 반영, (최겸) 기술영업 피드백 반영
Diffstat (limited to 'lib/evaluation-target-list/table')
-rw-r--r--lib/evaluation-target-list/table/delete-targets-dialog.tsx181
-rw-r--r--lib/evaluation-target-list/table/evaluation-target-table.tsx199
-rw-r--r--lib/evaluation-target-list/table/evaluation-targets-columns.tsx87
-rw-r--r--lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx304
-rw-r--r--lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx32
-rw-r--r--lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx2
-rw-r--r--lib/evaluation-target-list/table/update-evaluation-target.tsx4
7 files changed, 538 insertions, 271 deletions
diff --git a/lib/evaluation-target-list/table/delete-targets-dialog.tsx b/lib/evaluation-target-list/table/delete-targets-dialog.tsx
new file mode 100644
index 00000000..5414d281
--- /dev/null
+++ b/lib/evaluation-target-list/table/delete-targets-dialog.tsx
@@ -0,0 +1,181 @@
+"use client"
+
+import * as React from "react"
+import { Trash2, AlertTriangle } from "lucide-react"
+import { toast } from "sonner"
+
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import { Badge } from "@/components/ui/badge"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { EvaluationTargetWithDepartments } from "@/db/schema"
+import { deleteEvaluationTargets } from "../service"
+interface DeleteTargetsDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ targets: EvaluationTargetWithDepartments[]
+ onSuccess?: () => void
+}
+
+export function DeleteTargetsDialog({
+ open,
+ onOpenChange,
+ targets,
+ onSuccess
+}: DeleteTargetsDialogProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
+
+ // PENDING 상태인 타겟들만 필터링 (추가 안전장치)
+ const pendingTargets = React.useMemo(() => {
+ return targets.filter(target => target.status === "PENDING")
+ }, [targets])
+
+ console.log(pendingTargets,"pendingTargets")
+
+ const handleDelete = async () => {
+ if (pendingTargets.length === 0) {
+ toast.error("삭제할 수 있는 평가 대상이 없습니다.")
+ return
+ }
+
+ setIsLoading(true)
+ try {
+ const targetIds = pendingTargets.map(target => target.id)
+ const result = await deleteEvaluationTargets(targetIds)
+
+ if (result.success) {
+ toast.success(result.message || "평가 대상이 성공적으로 삭제되었습니다.", {
+ description: `${result.deletedCount || pendingTargets.length}개의 항목이 삭제되었습니다.`
+ })
+ onSuccess?.()
+ onOpenChange(false)
+ } else {
+ toast.error(result.error || "삭제 중 오류가 발생했습니다.")
+ }
+ } catch (error) {
+ console.error('Error deleting targets:', error)
+ toast.error("삭제 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleCancel = () => {
+ if (!isLoading) {
+ onOpenChange(false)
+ }
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-2xl">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <Trash2 className="size-5 text-destructive" />
+ 평가 대상 삭제
+ </DialogTitle>
+ <DialogDescription>
+ 선택한 평가 대상을 영구적으로 삭제합니다. 이 작업은 되돌릴 수 없습니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ {pendingTargets.length > 0 ? (
+ <div className="space-y-4">
+ {/* 경고 메시지 */}
+ <div className="flex items-start gap-3 p-4 bg-destructive/10 rounded-lg border border-destructive/20">
+ <AlertTriangle className="size-5 text-destructive mt-0.5 flex-shrink-0" />
+ <div className="space-y-1">
+ <p className="font-medium text-destructive">
+ 주의: 삭제된 데이터는 복구할 수 없습니다
+ </p>
+ <p className="text-sm text-muted-foreground">
+ PENDING 상태의 평가 대상만 삭제할 수 있습니다.
+ 확정(CONFIRMED)되거나 제외(EXCLUDED)된 대상은 삭제할 수 없습니다.
+ </p>
+ </div>
+ </div>
+
+ {/* 삭제 대상 목록 */}
+ <div className="space-y-2">
+ <div className="flex items-center justify-between">
+ <h4 className="font-medium">삭제될 평가 대상 ({pendingTargets.length}개)</h4>
+ <Badge variant="destructive" className="gap-1">
+ <Trash2 className="size-3" />
+ 삭제 예정
+ </Badge>
+ </div>
+
+ <ScrollArea className="h-40 w-full border rounded-md">
+ <div className="p-4 space-y-2">
+ {pendingTargets.map((target) => (
+ <div
+ key={target.id}
+ className="flex items-center justify-between p-2 bg-muted/50 rounded text-sm"
+ >
+ <div className="space-y-1">
+ <div className="font-medium">
+ {target.vendorName || '알 수 없는 업체'}
+ </div>
+ <div className="text-xs text-muted-foreground">
+ • {target.evaluationYear}년
+ </div>
+ </div>
+ <div className="flex items-center gap-2">
+ <Badge variant="secondary" className="text-xs">
+ {target.status}
+ </Badge>
+ </div>
+ </div>
+ ))}
+ </div>
+ </ScrollArea>
+ </div>
+ </div>
+ ) : (
+ <div className="flex items-center justify-center py-8 text-muted-foreground">
+ <div className="text-center space-y-2">
+ <Trash2 className="size-8 mx-auto opacity-50" />
+ <p>삭제할 수 있는 평가 대상이 없습니다.</p>
+ <p className="text-xs">PENDING 상태의 대상만 삭제할 수 있습니다.</p>
+ </div>
+ </div>
+ )}
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={handleCancel}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button
+ variant="destructive"
+ onClick={handleDelete}
+ disabled={isLoading || pendingTargets.length === 0}
+ className="gap-2"
+ >
+ {isLoading ? (
+ <>
+ <div className="size-4 border-2 border-current border-r-transparent rounded-full animate-spin" />
+ 삭제 중...
+ </>
+ ) : (
+ <>
+ <Trash2 className="size-4" />
+ {pendingTargets.length}개 삭제
+ </>
+ )}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file
diff --git a/lib/evaluation-target-list/table/evaluation-target-table.tsx b/lib/evaluation-target-list/table/evaluation-target-table.tsx
index 5560d3ff..c65a7815 100644
--- a/lib/evaluation-target-list/table/evaluation-target-table.tsx
+++ b/lib/evaluation-target-list/table/evaluation-target-table.tsx
@@ -1,6 +1,6 @@
// ============================================================================
// components/evaluation-targets-table.tsx (CLIENT COMPONENT)
-// ─ 완전본 ─ evaluation-targets-columns.tsx 는 별도
+// ─ 정리된 버전 ─
// ============================================================================
"use client";
@@ -18,14 +18,14 @@ import type {
} 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 { 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 { EvaluationTargetFilterSheet } from "./evaluation-targets-filter-sheet"; // ✅ 폼 기반 필터 시트
import { EvaluationTargetWithDepartments } from "@/db/schema";
import { EditEvaluationTargetSheet } from "./update-evaluation-target";
import {
@@ -33,6 +33,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
+import { useRouter } from "next/navigation"; // ✅ 라우터 추가
/* -------------------------------------------------------------------------- */
/* Process Guide Popover */
@@ -239,11 +240,93 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false);
const searchParams = useSearchParams();
+ // ✅ 외부 필터 상태 (폼에서 전달받은 필터)
+ const [externalFilters, setExternalFilters] = React.useState<any[]>([]);
+ const [externalJoinOperator, setExternalJoinOperator] = React.useState<"and" | "or">("and");
+
+ // ✅ 폼에서 전달받은 필터를 처리하는 핸들러
+ const handleFiltersApply = React.useCallback((filters: any[], joinOperator: "and" | "or") => {
+ console.log("=== 폼에서 필터 전달받음 ===", filters, joinOperator);
+ setExternalFilters(filters);
+ setExternalJoinOperator(joinOperator);
+ // 필터 적용 후 패널 닫기
+ setIsFilterPanelOpen(false);
+ }, []);
+
+
+ const searchString = React.useMemo(
+ () => searchParams.toString(),
+ [searchParams]
+ );
+
+ const getSearchParam = React.useCallback(
+ (key: string, def = "") =>
+ new URLSearchParams(searchString).get(key) ?? def,
+ [searchString]
+ );
+
+
+ // ✅ URL 필터 변경 감지 및 데이터 새로고침
+ React.useEffect(() => {
+ const refetchData = async () => {
+ try {
+ setIsDataLoading(true);
+
+ // 현재 URL 파라미터 기반으로 새 검색 파라미터 생성
+ const currentFilters = getSearchParam("filters");
+ const currentJoinOperator = getSearchParam("joinOperator", "and");
+ const currentPage = parseInt(getSearchParam("page", "1"));
+ const currentPerPage = parseInt(getSearchParam("perPage", "10"));
+ const currentSort = getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }];
+ const currentSearch = getSearchParam("search", "");
+
+ const searchParams = {
+ filters: currentFilters ? JSON.parse(currentFilters) : [],
+ joinOperator: currentJoinOperator as "and" | "or",
+ page: currentPage,
+ perPage: currentPerPage,
+ sort: currentSort,
+ search: currentSearch,
+ evaluationYear: evaluationYear
+ };
+
+ console.log("=== 새 데이터 요청 ===", searchParams);
+
+ // 서버 액션 직접 호출
+ const newData = await getEvaluationTargets(searchParams);
+ setTableData(newData);
+
+ console.log("=== 데이터 업데이트 완료 ===", newData.data.length, "건");
+ } catch (error) {
+ console.error("데이터 새로고침 오류:", error);
+ } finally {
+ setIsDataLoading(false);
+ }
+ };
+
+ /* ---------------------- 검색 파라미터 안전 처리 ---------------------- */
+
+ // 필터나 검색 파라미터가 변경되면 데이터 새로고침 (디바운스 적용)
+ const timeoutId = setTimeout(() => {
+ // 필터, 검색, 페이지네이션, 정렬 중 하나라도 변경되면 새로고침
+ const hasChanges = getSearchParam("filters") ||
+ getSearchParam("search") ||
+ getSearchParam("page") !== "1" ||
+ getSearchParam("perPage") !== "10" ||
+ getSearchParam("sort");
+
+ if (hasChanges) {
+ refetchData();
+ }
+ }, 300); // 디바운스 시간 단축
+
+ return () => clearTimeout(timeoutId);
+ }, [searchString, evaluationYear, getSearchParam]);
+
/* --------------------------- layout refs --------------------------- */
const containerRef = React.useRef<HTMLDivElement>(null);
const [containerTop, setContainerTop] = React.useState(0);
- // RFQ 패턴으로 변경: State를 통한 위치 관리
const updateContainerBounds = React.useCallback(() => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
@@ -267,25 +350,16 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
};
}, [updateContainerBounds]);
- /* ---------------------- 데이터 프리패치 ---------------------- */
- const [promiseData] = React.use(promises);
- const tableData = promiseData;
+ /* ---------------------- 데이터 상태 관리 ---------------------- */
+ // 초기 데이터 설정
+ const [initialPromiseData] = React.use(promises);
+
+ // ✅ 테이블 데이터 상태 추가
+ const [tableData, setTableData] = React.useState(initialPromiseData);
+ const [isDataLoading, setIsDataLoading] = React.useState(false);
- console.log(tableData)
- /* ---------------------- 검색 파라미터 안전 처리 ---------------------- */
- const searchString = React.useMemo(
- () => searchParams.toString(), // query가 바뀔 때만 새로 계산
- [searchParams]
- );
- const getSearchParam = React.useCallback(
- (key: string, def = "") =>
- new URLSearchParams(searchString).get(key) ?? def,
- [searchString]
- );
-
- // 제네릭 함수는 useCallback 밖에서 정의
const parseSearchParamHelper = React.useCallback((key: string, defaultValue: any): any => {
try {
const value = getSearchParam(key);
@@ -295,9 +369,9 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
}
}, [getSearchParam]);
-const parseSearchParam = <T,>(key: string, defaultValue: T): T => {
- return parseSearchParamHelper(key, defaultValue);
-};
+ const parseSearchParam = <T,>(key: string, defaultValue: T): T => {
+ return parseSearchParamHelper(key, defaultValue);
+ };
/* ---------------------- 초기 설정 ---------------------------- */
const initialSettings = React.useMemo(() => ({
@@ -306,15 +380,13 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => {
sort: getSearchParam('sort') ? JSON.parse(getSearchParam('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: [],
- }), [getSearchParam]);
+ }), [getSearchParam, parseSearchParam]);
/* --------------------- 프리셋 훅 ------------------------------ */
const {
@@ -336,14 +408,6 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => {
/* --------------------- 컬럼 ------------------------------ */
const columns = React.useMemo(() => getEvaluationTargetsColumns({ setRowAction }), [setRowAction]);
-// const columns =[
-// { accessorKey: "vendorCode", header: "벤더 코드" },
-// { accessorKey: "vendorName", header: "벤더명" },
-// { accessorKey: "status", header: "상태" },
-// { accessorKey: "evaluationYear", header: "평가년도" },
-// { accessorKey: "division", header: "구분" }
-// ];
-
/* 기본 필터 */
const filterFields: DataTableFilterField<EvaluationTargetWithDepartments>[] = [
@@ -355,13 +419,36 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => {
/* 고급 필터 */
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: "PLANT" },
+ { label: "조선", value: "SHIP" }
+ ]},
{ 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: "orderReviewerName", label: "발주 담당자명", type: "text" },
+ { id: "procurementReviewerName", label: "조달 담당자명", type: "text" },
+ { id: "qualityReviewerName", label: "품질 담당자명", type: "text" },
+ { id: "designReviewerName", label: "설계 담당자명", type: "text" },
+ { id: "csReviewerName", label: "CS 담당자명", type: "text" },
{ id: "adminComment", label: "관리자 의견", type: "text" },
{ id: "consolidatedComment", label: "종합 의견", type: "text" },
{ id: "confirmedAt", label: "확정일", type: "date" },
@@ -398,10 +485,15 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => {
});
/* ---------------------- helper ------------------------------ */
- const getActiveBasicFilterCount = React.useCallback(() => {
+ const getActiveFilterCount = React.useCallback(() => {
try {
- const f = getSearchParam("basicFilters");
- return f ? JSON.parse(f).length : 0;
+ // URL에서 현재 필터 수 확인
+ const filtersParam = getSearchParam("filters");
+ if (filtersParam) {
+ const filters = JSON.parse(filtersParam);
+ return Array.isArray(filters) ? filters.length : 0;
+ }
+ return 0;
} catch {
return 0;
}
@@ -427,7 +519,7 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => {
<EvaluationTargetFilterSheet
isOpen={isFilterPanelOpen}
onClose={() => setIsFilterPanelOpen(false)}
- onSearch={() => setIsFilterPanelOpen(false)}
+ onFiltersApply={handleFiltersApply} // ✅ 필터 적용 콜백 전달
isLoading={false}
/>
</div>
@@ -451,9 +543,9 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => {
className="flex items-center shadow-sm"
>
{isFilterPanelOpen ? <PanelLeftClose className="size-4" /> : <PanelLeftOpen className="size-4" />}
- {getActiveBasicFilterCount() > 0 && (
+ {getActiveFilterCount() > 0 && (
<span className="ml-2 bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs">
- {getActiveBasicFilterCount()}
+ {getActiveFilterCount()}
</span>
)}
</Button>
@@ -468,12 +560,27 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => {
</div>
{/* Table */}
- <div className="flex-1 overflow-hidden" style={{ height: "calc(100vh - 500px)" }}>
+ <div className="flex-1 overflow-hidden relative" style={{ height: "calc(100vh - 500px)" }}>
+ {isDataLoading && (
+ <div className="absolute inset-0 bg-background/50 backdrop-blur-sm z-10 flex items-center justify-center">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
+ 필터링 중...
+ </div>
+ </div>
+ )}
<DataTable table={table} className="h-full">
+ {/* ✅ 확장된 DataTableAdvancedToolbar 사용 */}
<DataTableAdvancedToolbar
table={table}
filterFields={advancedFilterFields}
+ debounceMs={300}
shallow={false}
+ externalFilters={externalFilters}
+ externalJoinOperator={externalJoinOperator}
+ onFiltersChange={(filters, joinOperator) => {
+ console.log("=== 필터 변경 감지 ===", filters, joinOperator);
+ }}
>
<div className="flex items-center gap-2">
<TablePresetManager<EvaluationTargetWithDepartments>
diff --git a/lib/evaluation-target-list/table/evaluation-targets-columns.tsx b/lib/evaluation-target-list/table/evaluation-targets-columns.tsx
index c3aa9d71..7b6754c1 100644
--- a/lib/evaluation-target-list/table/evaluation-targets-columns.tsx
+++ b/lib/evaluation-target-list/table/evaluation-targets-columns.tsx
@@ -210,18 +210,27 @@ function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): Col
header: createHeaderRenderer("평가년도"),
cell: renderEvaluationYear,
size: 100,
+ meta: {
+ excelHeader: "평가년도",
+ },
},
{
accessorKey: "division",
header: createHeaderRenderer("구분"),
cell: renderDivision,
size: 80,
+ meta: {
+ excelHeader: "구분",
+ },
},
{
accessorKey: "status",
header: createHeaderRenderer("상태"),
cell: renderStatus,
size: 100,
+ meta: {
+ excelHeader: "상태",
+ },
},
@@ -235,24 +244,36 @@ function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): Col
header: createHeaderRenderer("벤더 코드"),
cell: renderVendorCode,
size: 120,
+ meta: {
+ excelHeader: "벤더 코드",
+ },
},
{
accessorKey: "vendorName",
header: createHeaderRenderer("벤더명"),
cell: renderVendorName,
size: 200,
+ meta: {
+ excelHeader: "벤더명",
+ },
},
{
accessorKey: "domesticForeign",
header: createHeaderRenderer("내외자"),
cell: renderDomesticForeign,
size: 80,
+ meta: {
+ excelHeader: "내외자",
+ },
},
{
accessorKey: "materialType",
header: createHeaderRenderer("자재구분"),
cell: renderMaterialType,
size: 120,
+ meta: {
+ excelHeader: "자재구분",
+ },
},
]
},
@@ -262,6 +283,9 @@ function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): Col
header: createHeaderRenderer("의견 일치"),
cell: renderConsensusStatus,
size: 100,
+ meta: {
+ excelHeader: "의견 일치",
+ },
},
{
@@ -275,6 +299,9 @@ function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): Col
<span className="text-sm">{row.original.ldClaimCount}</span>
),
size: 80,
+ meta: {
+ excelHeader: "LD 건수",
+ },
},
{
@@ -284,6 +311,9 @@ function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): Col
<span className="font-mono text-sm">{(Number(row.original.ldClaimAmount).toLocaleString())}</span>
),
size: 80,
+ meta: {
+ excelHeader: "LD 금액",
+ },
},
{
accessorKey: "ldClaimCurrency",
@@ -293,6 +323,9 @@ function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): Col
<span className="text-sm">{row.original.ldClaimCurrency}</span>
),
size: 80,
+ meta: {
+ excelHeader: "LD 금액 단위",
+ },
},
]
@@ -308,12 +341,18 @@ function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): Col
header: createHeaderRenderer("담당자명"),
cell: renderReviewerName("orderReviewerName"),
size: 120,
+ meta: {
+ excelHeader: "발주 담당자명",
+ },
},
{
accessorKey: "orderIsApproved",
header: createHeaderRenderer("평가 대상"),
cell: renderIsApproved("orderIsApproved"),
size: 120,
+ meta: {
+ excelHeader: "발주 평가 대상",
+ },
},
]
},
@@ -328,12 +367,18 @@ function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): Col
header: createHeaderRenderer("담당자명"),
cell: renderReviewerName("procurementReviewerName"),
size: 120,
+ meta: {
+ excelHeader: "조달 담당자명",
+ },
},
{
accessorKey: "procurementIsApproved",
header: createHeaderRenderer("평가 대상"),
cell: renderIsApproved("procurementIsApproved"),
size: 120,
+ meta: {
+ excelHeader: "조달 평가 대상",
+ },
},
]
},
@@ -348,12 +393,18 @@ function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): Col
header: createHeaderRenderer("담당자명"),
cell: renderReviewerName("qualityReviewerName"),
size: 120,
+ meta: {
+ excelHeader: "품질 담당자명",
+ },
},
{
accessorKey: "qualityIsApproved",
header: createHeaderRenderer("평가 대상"),
cell: renderIsApproved("qualityIsApproved"),
size: 120,
+ meta: {
+ excelHeader: "품질 평가 대상",
+ },
},
]
},
@@ -369,12 +420,12 @@ function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): Col
cell: renderReviewerName("designReviewerName"),
size: 120,
},
- {
- accessorKey: "designIsApproved",
- header: createHeaderRenderer("평가 대상"),
- cell: renderIsApproved("designIsApproved"),
- size: 120,
- },
+ // {
+ // accessorKey: "designIsApproved",
+ // header: createHeaderRenderer("평가 대상"),
+ // cell: renderIsApproved("designIsApproved"),
+ // size: 120,
+ // },
]
},
@@ -389,12 +440,12 @@ function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): Col
cell: renderReviewerName("csReviewerName"),
size: 120,
},
- {
- accessorKey: "csIsApproved",
- header: createHeaderRenderer("평가 대상"),
- cell: renderIsApproved("csIsApproved"),
- size: 120,
- },
+ // {
+ // accessorKey: "csIsApproved",
+ // header: createHeaderRenderer("평가 대상"),
+ // cell: renderIsApproved("csIsApproved"),
+ // size: 120,
+ // },
]
},
@@ -404,24 +455,36 @@ function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): Col
header: createHeaderRenderer("관리자 의견"),
cell: renderComment("max-w-[150px]"),
size: 150,
+ meta: {
+ excelHeader: "관리자 의견",
+ },
},
{
accessorKey: "consolidatedComment",
header: createHeaderRenderer("종합 의견"),
cell: renderComment("max-w-[150px]"),
size: 150,
+ meta: {
+ excelHeader: "종합 의견",
+ },
},
{
accessorKey: "confirmedAt",
header: createHeaderRenderer("확정일"),
cell: renderConfirmedAt,
size: 100,
+ meta: {
+ excelHeader: "확정일",
+ },
},
{
accessorKey: "createdAt",
header: createHeaderRenderer("생성일"),
cell: renderCreatedAt,
size: 100,
+ meta: {
+ excelHeader: "생성일",
+ },
},
// Actions
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 c37258ae..3b6f9fa1 100644
--- a/lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx
+++ b/lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx
@@ -7,7 +7,6 @@ import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { Search, X } from "lucide-react"
import { customAlphabet } from "nanoid"
-import { parseAsStringEnum, useQueryState } from "nuqs"
import { Button } from "@/components/ui/button"
import {
@@ -28,7 +27,7 @@ import {
SelectValue,
} from "@/components/ui/select"
import { cn } from "@/lib/utils"
-import { getFiltersStateParser } from "@/lib/parsers"
+import { EVALUATION_TARGET_FILTER_OPTIONS } from "../validation"
// nanoid 생성기
const generateId = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 6)
@@ -43,56 +42,28 @@ const evaluationTargetFilterSchema = z.object({
consensusStatus: z.string().optional(),
vendorCode: z.string().optional(),
vendorName: z.string().optional(),
- reviewerUserId: z.string().optional(), // 담당자 ID로 필터링
- orderReviewerName: z.string().optional(), // 주문 검토자명
- procurementReviewerName: z.string().optional(), // 조달 검토자명
- qualityReviewerName: z.string().optional(), // 품질 검토자명
- designReviewerName: z.string().optional(), // 설계 검토자명
- csReviewerName: z.string().optional(), // CS 검토자명
+ reviewerUserId: z.string().optional(),
+ orderReviewerName: z.string().optional(),
+ procurementReviewerName: z.string().optional(),
+ qualityReviewerName: z.string().optional(),
+ designReviewerName: z.string().optional(),
+ csReviewerName: z.string().optional(),
})
-// 옵션 정의
-const divisionOptions = [
- { value: "PLANT", label: "해양" },
- { value: "SHIP", label: "조선" },
-]
-
-const statusOptions = [
- { value: "PENDING", label: "검토 중" },
- { value: "CONFIRMED", label: "확정" },
- { value: "EXCLUDED", label: "제외" },
-]
-
-const domesticForeignOptions = [
- { value: "DOMESTIC", label: "내자" },
- { value: "FOREIGN", label: "외자" },
-]
-
-const materialTypeOptions = [
- { value: "EQUIPMENT", label: "기자재" },
- { value: "BULK", label: "벌크" },
- { value: "EQUIPMENT_BULK", label: "기자재/벌크" },
-]
-
-const consensusStatusOptions = [
- { value: "true", label: "의견 일치" },
- { value: "false", label: "의견 불일치" },
- { value: "null", label: "검토 중" },
-]
type EvaluationTargetFilterFormValues = z.infer<typeof evaluationTargetFilterSchema>
interface EvaluationTargetFilterSheetProps {
isOpen: boolean;
onClose: () => void;
- onSearch?: () => void;
+ onFiltersApply: (filters: any[], joinOperator: "and" | "or") => void; // ✅ 필터 전달 콜백
isLoading?: boolean;
}
export function EvaluationTargetFilterSheet({
isOpen,
onClose,
- onSearch,
+ onFiltersApply,
isLoading = false
}: EvaluationTargetFilterSheetProps) {
const router = useRouter()
@@ -100,25 +71,7 @@ export function EvaluationTargetFilterSheet({
const lng = params ? (params.lng as string) : 'ko';
const [isPending, startTransition] = useTransition()
-
- // 초기화 상태 추가
- const [isInitializing, setIsInitializing] = useState(false)
- const lastAppliedFilters = useRef<string>("")
-
- // nuqs로 URL 상태 관리
- const [filters, setFilters] = useQueryState(
- "basicFilters",
- getFiltersStateParser().withDefault([])
- )
-
- // joinOperator 설정
- const [joinOperator, setJoinOperator] = useQueryState(
- "basicJoinOperator",
- parseAsStringEnum(["and", "or"]).withDefault("and")
- )
-
- // 현재 URL의 페이지 파라미터도 가져옴
- const [page, setPage] = useQueryState("page", { defaultValue: "1" })
+ const [joinOperator, setJoinOperator] = useState<"and" | "or">("and")
// 폼 상태 초기화
const form = useForm<EvaluationTargetFilterFormValues>({
@@ -141,46 +94,13 @@ export function EvaluationTargetFilterSheet({
},
})
- // URL 필터에서 초기 폼 상태 설정
- useEffect(() => {
- const currentFiltersString = JSON.stringify(filters);
-
- if (isOpen && filters && filters.length > 0 && currentFiltersString !== lastAppliedFilters.current) {
- setIsInitializing(true);
-
- const formValues = { ...form.getValues() };
- let formUpdated = false;
-
- filters.forEach(filter => {
- if (filter.id in formValues) {
- // @ts-ignore - 동적 필드 접근
- formValues[filter.id] = filter.value;
- formUpdated = true;
- }
- });
-
- if (formUpdated) {
- form.reset(formValues);
- lastAppliedFilters.current = currentFiltersString;
- }
-
- setIsInitializing(false);
- }
- }, [filters, isOpen])
-
- // 현재 적용된 필터 카운트
- const getActiveFilterCount = () => {
- return filters?.length || 0
- }
-
- // 폼 제출 핸들러
+ // ✅ 폼 제출 핸들러 - 필터 배열 생성 및 전달
async function onSubmit(data: EvaluationTargetFilterFormValues) {
- if (isInitializing) return;
-
startTransition(async () => {
try {
const newFilters = []
+ // 필터 생성 로직
if (data.evaluationYear?.trim()) {
newFilters.push({
id: "evaluationYear",
@@ -271,7 +191,6 @@ export function EvaluationTargetFilterSheet({
})
}
- // 새로 추가된 검토자명 필터들
if (data.orderReviewerName?.trim()) {
newFilters.push({
id: "orderReviewerName",
@@ -322,82 +241,41 @@ export function EvaluationTargetFilterSheet({
})
}
- // URL 업데이트
- const currentUrl = new URL(window.location.href);
- const params = new URLSearchParams(currentUrl.search);
-
- // 기존 필터 관련 파라미터 제거
- params.delete('basicFilters');
- params.delete('basicJoinOperator');
- params.delete('page');
-
- // 새로운 필터 추가
- if (newFilters.length > 0) {
- params.set('basicFilters', JSON.stringify(newFilters));
- params.set('basicJoinOperator', joinOperator);
- }
-
- params.set('page', '1');
-
- const newUrl = `${currentUrl.pathname}?${params.toString()}`;
- console.log("New Evaluation Target Filter URL:", newUrl);
-
- // 페이지 완전 새로고침
- window.location.href = newUrl;
-
- lastAppliedFilters.current = JSON.stringify(newFilters);
-
- if (onSearch) {
- console.log("Calling evaluation target onSearch...");
- onSearch();
- }
+ console.log("=== 생성된 필터들 ===", newFilters);
+ console.log("=== 조인 연산자 ===", joinOperator);
- console.log("=== Evaluation Target Filter Submit Complete ===");
+ // ✅ 부모 컴포넌트에 필터 전달
+ onFiltersApply(newFilters, joinOperator);
+
+ console.log("=== 필터 적용 완료 ===");
} catch (error) {
console.error("평가 대상 필터 적용 오류:", error);
}
})
}
- // 필터 초기화 핸들러
- async function handleReset() {
- try {
- setIsInitializing(true);
-
- form.reset({
- evaluationYear: "",
- division: "",
- status: "",
- domesticForeign: "",
- materialType: "",
- consensusStatus: "",
- vendorCode: "",
- vendorName: "",
- reviewerUserId: "",
- orderReviewerName: "",
- procurementReviewerName: "",
- qualityReviewerName: "",
- designReviewerName: "",
- csReviewerName: "",
- });
-
- // URL 초기화
- const currentUrl = new URL(window.location.href);
- const params = new URLSearchParams(currentUrl.search);
-
- params.delete('basicFilters');
- params.delete('basicJoinOperator');
- params.set('page', '1');
-
- const newUrl = `${currentUrl.pathname}?${params.toString()}`;
- window.location.href = newUrl;
-
- lastAppliedFilters.current = "";
- setIsInitializing(false);
- } catch (error) {
- console.error("평가 대상 필터 초기화 오류:", error);
- setIsInitializing(false);
- }
+ // ✅ 필터 초기화 핸들러
+ function handleReset() {
+ form.reset({
+ evaluationYear: "",
+ division: "",
+ status: "",
+ domesticForeign: "",
+ materialType: "",
+ consensusStatus: "",
+ vendorCode: "",
+ vendorName: "",
+ reviewerUserId: "",
+ orderReviewerName: "",
+ procurementReviewerName: "",
+ qualityReviewerName: "",
+ designReviewerName: "",
+ csReviewerName: "",
+ });
+
+ // 빈 필터 배열 전달
+ onFiltersApply([], "and");
+ setJoinOperator("and");
}
if (!isOpen) {
@@ -409,13 +287,14 @@ export function EvaluationTargetFilterSheet({
{/* 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>
- <div className="flex items-center gap-2">
- {getActiveFilterCount() > 0 && (
- <Badge variant="secondary" className="px-2 py-1">
- {getActiveFilterCount()}개 필터 적용됨
- </Badge>
- )}
- </div>
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={onClose}
+ className="h-8 w-8"
+ >
+ <X className="size-4" />
+ </Button>
</div>
{/* Join Operator Selection */}
@@ -424,7 +303,6 @@ export function EvaluationTargetFilterSheet({
<Select
value={joinOperator}
onValueChange={(value: "and" | "or") => setJoinOperator(value)}
- disabled={isInitializing}
>
<SelectTrigger className="h-8 w-[180px] mt-2 bg-white">
<SelectValue placeholder="조건 결합 방식" />
@@ -456,7 +334,6 @@ export function EvaluationTargetFilterSheet({
placeholder="평가년도 입력"
{...field}
className={cn(field.value && "pr-8", "bg-white")}
- disabled={isInitializing}
/>
{field.value && (
<Button
@@ -468,7 +345,6 @@ export function EvaluationTargetFilterSheet({
e.stopPropagation();
form.setValue("evaluationYear", "");
}}
- disabled={isInitializing}
>
<X className="size-3.5" />
</Button>
@@ -490,7 +366,6 @@ export function EvaluationTargetFilterSheet({
<Select
value={field.value}
onValueChange={field.onChange}
- disabled={isInitializing}
>
<FormControl>
<SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
@@ -506,7 +381,6 @@ export function EvaluationTargetFilterSheet({
e.stopPropagation();
form.setValue("division", "");
}}
- disabled={isInitializing}
>
<X className="size-3" />
</Button>
@@ -515,7 +389,7 @@ export function EvaluationTargetFilterSheet({
</SelectTrigger>
</FormControl>
<SelectContent>
- {divisionOptions.map(option => (
+ {EVALUATION_TARGET_FILTER_OPTIONS.DIVISIONS.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
@@ -537,7 +411,6 @@ export function EvaluationTargetFilterSheet({
<Select
value={field.value}
onValueChange={field.onChange}
- disabled={isInitializing}
>
<FormControl>
<SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
@@ -553,7 +426,6 @@ export function EvaluationTargetFilterSheet({
e.stopPropagation();
form.setValue("status", "");
}}
- disabled={isInitializing}
>
<X className="size-3" />
</Button>
@@ -562,7 +434,7 @@ export function EvaluationTargetFilterSheet({
</SelectTrigger>
</FormControl>
<SelectContent>
- {statusOptions.map(option => (
+ {EVALUATION_TARGET_FILTER_OPTIONS.STATUSES.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
@@ -584,7 +456,6 @@ export function EvaluationTargetFilterSheet({
<Select
value={field.value}
onValueChange={field.onChange}
- disabled={isInitializing}
>
<FormControl>
<SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
@@ -600,7 +471,6 @@ export function EvaluationTargetFilterSheet({
e.stopPropagation();
form.setValue("domesticForeign", "");
}}
- disabled={isInitializing}
>
<X className="size-3" />
</Button>
@@ -609,7 +479,7 @@ export function EvaluationTargetFilterSheet({
</SelectTrigger>
</FormControl>
<SelectContent>
- {domesticForeignOptions.map(option => (
+ {EVALUATION_TARGET_FILTER_OPTIONS.DOMESTIC_FOREIGN.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
@@ -631,7 +501,6 @@ export function EvaluationTargetFilterSheet({
<Select
value={field.value}
onValueChange={field.onChange}
- disabled={isInitializing}
>
<FormControl>
<SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
@@ -647,7 +516,6 @@ export function EvaluationTargetFilterSheet({
e.stopPropagation();
form.setValue("materialType", "");
}}
- disabled={isInitializing}
>
<X className="size-3" />
</Button>
@@ -656,7 +524,7 @@ export function EvaluationTargetFilterSheet({
</SelectTrigger>
</FormControl>
<SelectContent>
- {materialTypeOptions.map(option => (
+ {EVALUATION_TARGET_FILTER_OPTIONS.MATERIAL_TYPES.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
@@ -678,7 +546,6 @@ export function EvaluationTargetFilterSheet({
<Select
value={field.value}
onValueChange={field.onChange}
- disabled={isInitializing}
>
<FormControl>
<SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
@@ -694,7 +561,6 @@ export function EvaluationTargetFilterSheet({
e.stopPropagation();
form.setValue("consensusStatus", "");
}}
- disabled={isInitializing}
>
<X className="size-3" />
</Button>
@@ -703,7 +569,7 @@ export function EvaluationTargetFilterSheet({
</SelectTrigger>
</FormControl>
<SelectContent>
- {consensusStatusOptions.map(option => (
+ {EVALUATION_TARGET_FILTER_OPTIONS.CONSENSUS_STATUS.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
@@ -728,7 +594,6 @@ export function EvaluationTargetFilterSheet({
placeholder="벤더 코드 입력"
{...field}
className={cn(field.value && "pr-8", "bg-white")}
- disabled={isInitializing}
/>
{field.value && (
<Button
@@ -740,7 +605,6 @@ export function EvaluationTargetFilterSheet({
e.stopPropagation();
form.setValue("vendorCode", "");
}}
- disabled={isInitializing}
>
<X className="size-3.5" />
</Button>
@@ -765,7 +629,6 @@ export function EvaluationTargetFilterSheet({
placeholder="벤더명 입력"
{...field}
className={cn(field.value && "pr-8", "bg-white")}
- disabled={isInitializing}
/>
{field.value && (
<Button
@@ -777,7 +640,6 @@ export function EvaluationTargetFilterSheet({
e.stopPropagation();
form.setValue("vendorName", "");
}}
- disabled={isInitializing}
>
<X className="size-3.5" />
</Button>
@@ -789,7 +651,43 @@ export function EvaluationTargetFilterSheet({
)}
/>
- {/* 주문 검토자명 */}
+ {/* 담당자 ID */}
+ <FormField
+ control={form.control}
+ name="reviewerUserId"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>담당자 ID</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ type="number"
+ placeholder="담당자 ID 입력"
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("reviewerUserId", "");
+ }}
+ >
+ <X className="size-3.5" />
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 발주 담당자명 */}
<FormField
control={form.control}
name="orderReviewerName"
@@ -802,7 +700,6 @@ export function EvaluationTargetFilterSheet({
placeholder="발주 담당자명 입력"
{...field}
className={cn(field.value && "pr-8", "bg-white")}
- disabled={isInitializing}
/>
{field.value && (
<Button
@@ -814,7 +711,6 @@ export function EvaluationTargetFilterSheet({
e.stopPropagation();
form.setValue("orderReviewerName", "");
}}
- disabled={isInitializing}
>
<X className="size-3.5" />
</Button>
@@ -826,7 +722,7 @@ export function EvaluationTargetFilterSheet({
)}
/>
- {/* 조달 검토자명 */}
+ {/* 조달 담당자명 */}
<FormField
control={form.control}
name="procurementReviewerName"
@@ -839,7 +735,6 @@ export function EvaluationTargetFilterSheet({
placeholder="조달 담당자명 입력"
{...field}
className={cn(field.value && "pr-8", "bg-white")}
- disabled={isInitializing}
/>
{field.value && (
<Button
@@ -851,7 +746,6 @@ export function EvaluationTargetFilterSheet({
e.stopPropagation();
form.setValue("procurementReviewerName", "");
}}
- disabled={isInitializing}
>
<X className="size-3.5" />
</Button>
@@ -863,7 +757,7 @@ export function EvaluationTargetFilterSheet({
)}
/>
- {/* 품질 검토자명 */}
+ {/* 품질 담당자명 */}
<FormField
control={form.control}
name="qualityReviewerName"
@@ -876,7 +770,6 @@ export function EvaluationTargetFilterSheet({
placeholder="품질 담당자명 입력"
{...field}
className={cn(field.value && "pr-8", "bg-white")}
- disabled={isInitializing}
/>
{field.value && (
<Button
@@ -888,7 +781,6 @@ export function EvaluationTargetFilterSheet({
e.stopPropagation();
form.setValue("qualityReviewerName", "");
}}
- disabled={isInitializing}
>
<X className="size-3.5" />
</Button>
@@ -900,7 +792,7 @@ export function EvaluationTargetFilterSheet({
)}
/>
- {/* 설계 검토자명 */}
+ {/* 설계 담당자명 */}
<FormField
control={form.control}
name="designReviewerName"
@@ -913,7 +805,6 @@ export function EvaluationTargetFilterSheet({
placeholder="설계 담당자명 입력"
{...field}
className={cn(field.value && "pr-8", "bg-white")}
- disabled={isInitializing}
/>
{field.value && (
<Button
@@ -925,7 +816,6 @@ export function EvaluationTargetFilterSheet({
e.stopPropagation();
form.setValue("designReviewerName", "");
}}
- disabled={isInitializing}
>
<X className="size-3.5" />
</Button>
@@ -937,7 +827,7 @@ export function EvaluationTargetFilterSheet({
)}
/>
- {/* CS 검토자명 */}
+ {/* CS 담당자명 */}
<FormField
control={form.control}
name="csReviewerName"
@@ -950,7 +840,6 @@ export function EvaluationTargetFilterSheet({
placeholder="CS 담당자명 입력"
{...field}
className={cn(field.value && "pr-8", "bg-white")}
- disabled={isInitializing}
/>
{field.value && (
<Button
@@ -962,7 +851,6 @@ export function EvaluationTargetFilterSheet({
e.stopPropagation();
form.setValue("csReviewerName", "");
}}
- disabled={isInitializing}
>
<X className="size-3.5" />
</Button>
@@ -984,7 +872,7 @@ export function EvaluationTargetFilterSheet({
type="button"
variant="outline"
onClick={handleReset}
- disabled={isPending || getActiveFilterCount() === 0 || isInitializing}
+ disabled={isPending}
className="px-4"
>
초기화
@@ -992,7 +880,7 @@ export function EvaluationTargetFilterSheet({
<Button
type="submit"
variant="samsung"
- disabled={isPending || isLoading || isInitializing}
+ disabled={isPending || isLoading}
className="px-4"
>
<Search className="size-4 mr-2" />
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 d1c7e500..6a493d8e 100644
--- a/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx
+++ b/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx
@@ -10,7 +10,8 @@ import {
Download,
Upload,
RefreshCw,
- Settings
+ Settings,
+ Trash2
} from "lucide-react"
import { toast } from "sonner"
import { useRouter } from "next/navigation"
@@ -30,6 +31,7 @@ import {
ExcludeTargetsDialog,
RequestReviewDialog
} from "./evaluation-target-action-dialogs"
+import { DeleteTargetsDialog } from "./delete-targets-dialog"
import { EvaluationTargetWithDepartments } from "@/db/schema"
import { exportTableToExcel } from "@/lib/export"
import { autoGenerateEvaluationTargets } from "../service" // 서버 액션 import
@@ -49,6 +51,7 @@ export function EvaluationTargetsTableToolbarActions({
const [confirmDialogOpen, setConfirmDialogOpen] = React.useState(false)
const [excludeDialogOpen, setExcludeDialogOpen] = React.useState(false)
const [reviewDialogOpen, setReviewDialogOpen] = React.useState(false)
+ const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
const router = useRouter()
const { data: session } = useSession()
@@ -137,7 +140,8 @@ export function EvaluationTargetsTableToolbarActions({
consensusNull,
canConfirm: pending > 0 && consensusTrue > 0,
canExclude: pending > 0,
- canRequestReview: pending > 0
+ canRequestReview: pending > 0,
+ canDelete: pending > 0 // 삭제는 PENDING 상태인 것만 가능
}
}, [
pendingTargets.length,
@@ -308,6 +312,22 @@ export function EvaluationTargetsTableToolbarActions({
</Button>
)}
+ {/* 삭제 버튼 */}
+ {selectedStats.canDelete && (
+ <Button
+ variant="destructive"
+ size="sm"
+ className="gap-2"
+ onClick={() => setDeleteDialogOpen(true)}
+ disabled={isLoading}
+ >
+ <Trash2 className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">
+ 삭제 ({selectedStats.pending})
+ </span>
+ </Button>
+ )}
+
{/* 의견 요청 버튼 */}
{selectedStats.canRequestReview && (
<Button
@@ -369,6 +389,14 @@ export function EvaluationTargetsTableToolbarActions({
targets={selectedTargets}
onSuccess={handleActionSuccess}
/>
+
+ {/* 삭제 컨펌 다이얼로그 */}
+ <DeleteTargetsDialog
+ open={deleteDialogOpen}
+ onOpenChange={setDeleteDialogOpen}
+ targets={pendingTargets} // PENDING 상태인 타겟들만 전달
+ onSuccess={handleActionSuccess}
+ />
</>
)}
</>
diff --git a/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx b/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx
index af369ea6..44497cdb 100644
--- a/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx
+++ b/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx
@@ -100,7 +100,7 @@ const createEvaluationTargetSchema = z.object({
evaluationYear: z.number().min(2020).max(2030),
division: z.enum(["PLANT", "SHIP"]),
vendorId: z.number().min(0), // 0도 허용, 클라이언트에서 검증
- materialType: z.enum(["EQUIPMENT", "BULK", "EQUIPMENT_BULK"]),
+ materialType: z.enum(["EQUIPMENT", "BULK"]),
adminComment: z.string().optional(),
// L/D 클레임 정보
ldClaimCount: z.number().min(0).optional(),
diff --git a/lib/evaluation-target-list/table/update-evaluation-target.tsx b/lib/evaluation-target-list/table/update-evaluation-target.tsx
index 8ea63a1a..ef24aa9f 100644
--- a/lib/evaluation-target-list/table/update-evaluation-target.tsx
+++ b/lib/evaluation-target-list/table/update-evaluation-target.tsx
@@ -503,8 +503,8 @@ export function EditEvaluationTargetSheet({
{ key: "orderIsApproved", label: "발주 부서 평가", email: evaluationTarget.orderReviewerEmail },
{ key: "procurementIsApproved", label: "조달 부서 평가", email: evaluationTarget.procurementReviewerEmail },
{ key: "qualityIsApproved", label: "품질 부서 평가", email: evaluationTarget.qualityReviewerEmail },
- { key: "designIsApproved", label: "설계 부서 평가", email: evaluationTarget.designReviewerEmail },
- { key: "csIsApproved", label: "CS 부서 평가", email: evaluationTarget.csReviewerEmail },
+ // { key: "designIsApproved", label: "설계 부서 평가", email: evaluationTarget.designReviewerEmail },
+ // { key: "csIsApproved", label: "CS 부서 평가", email: evaluationTarget.csReviewerEmail },
].map(({ key, label, email }) => (
<FormField
key={key}