diff options
Diffstat (limited to 'lib/evaluation-target-list/table')
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} |
