From 69544e16e2b9013d5fdb508c0365149d8189489b Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 7 Apr 2026 14:28:06 +0900 Subject: [PATCH] Merge branch 'jskim-node' of https://g.wace.me/jskim/vexplor_dev into jskim-node --- .../design/change-management/page.tsx | 1231 ++++---- .../COMPANY_10/design/design-request/page.tsx | 882 +++--- .../(main)/COMPANY_10/design/my-work/page.tsx | 507 +-- .../(main)/COMPANY_10/design/project/page.tsx | 669 ++-- .../design/task-management/page.tsx | 722 ++--- .../(main)/COMPANY_10/equipment/info/page.tsx | 505 ++- .../equipment/plc-settings/page.tsx | 501 +++ .../(main)/COMPANY_10/logistics/info/page.tsx | 905 ++++++ .../COMPANY_10/logistics/inventory/page.tsx | 738 +++++ .../logistics/material-status/page.tsx | 145 +- .../COMPANY_10/logistics/outbound/page.tsx | 1317 +++++--- .../COMPANY_10/logistics/packaging/page.tsx | 1035 +++--- .../COMPANY_10/logistics/receiving/page.tsx | 1164 ++++--- .../COMPANY_10/logistics/warehouse/page.tsx | 1608 ++++++++++ .../COMPANY_10/master-data/company/page.tsx | 848 +++++ .../master-data/department/page.tsx | 495 ++- .../COMPANY_10/master-data/item-info/page.tsx | 560 ++-- .../COMPANY_10/master-data/options/page.tsx | 136 + .../app/(main)/COMPANY_10/mold/info/page.tsx | 1450 +++++++++ .../COMPANY_10/monitoring/equipment/page.tsx | 575 ++++ .../COMPANY_10/monitoring/production/page.tsx | 504 +++ .../COMPANY_10/monitoring/quality/page.tsx | 512 +++ .../outsourcing/subcontractor-item/page.tsx | 363 ++- .../outsourcing/subcontractor/page.tsx | 1010 +++--- .../(main)/COMPANY_10/production/bom/page.tsx | 2521 +++++++++++++++ .../production/plan-management/page.tsx | 406 +-- .../process-info/ItemRoutingTab.tsx | 905 +++--- .../process-info/ProcessMasterTab.tsx | 589 ++-- .../production/process-info/page.tsx | 223 +- .../production/work-instruction/page.tsx | 537 ++-- .../(main)/COMPANY_10/purchase/order/page.tsx | 1267 ++++++++ .../purchase/purchase-item/page.tsx | 1240 ++++++++ .../COMPANY_10/purchase/supplier/page.tsx | 2762 +++++++++++++++++ .../COMPANY_10/quality/inspection/page.tsx | 1703 ++++++++++ .../quality/item-inspection/page.tsx | 721 +++++ .../(main)/COMPANY_10/sales/claim/page.tsx | 1103 +++---- .../(main)/COMPANY_10/sales/customer/page.tsx | 2600 ++++++++++++---- .../(main)/COMPANY_10/sales/order/page.tsx | 1625 +++++++--- .../COMPANY_10/sales/sales-item/page.tsx | 935 +++--- .../COMPANY_10/sales/shipping-order/page.tsx | 748 +++-- .../COMPANY_10/sales/shipping-plan/page.tsx | 661 ++-- .../design/change-management/page.tsx | 1231 ++++---- .../COMPANY_7/design/design-request/page.tsx | 882 +++--- .../(main)/COMPANY_7/design/my-work/page.tsx | 507 +-- .../(main)/COMPANY_7/design/project/page.tsx | 669 ++-- .../COMPANY_7/design/task-management/page.tsx | 722 ++--- .../(main)/COMPANY_7/equipment/info/page.tsx | 505 ++- .../COMPANY_7/equipment/plc-settings/page.tsx | 501 +++ .../(main)/COMPANY_7/logistics/info/page.tsx | 905 ++++++ .../COMPANY_7/logistics/inventory/page.tsx | 738 +++++ .../logistics/material-status/page.tsx | 145 +- .../COMPANY_7/logistics/outbound/page.tsx | 1317 +++++--- .../COMPANY_7/logistics/packaging/page.tsx | 1035 +++--- .../COMPANY_7/logistics/receiving/page.tsx | 1164 ++++--- .../COMPANY_7/logistics/warehouse/page.tsx | 1608 ++++++++++ .../COMPANY_7/master-data/company/page.tsx | 848 +++++ .../COMPANY_7/master-data/department/page.tsx | 495 ++- .../COMPANY_7/master-data/item-info/page.tsx | 560 ++-- .../COMPANY_7/master-data/options/page.tsx | 136 + .../COMPANY_7/master-data/sample/page.tsx | 299 -- .../app/(main)/COMPANY_7/mold/info/page.tsx | 1450 +++++++++ .../outsourcing/subcontractor-item/page.tsx | 363 ++- .../outsourcing/subcontractor/page.tsx | 1010 +++--- .../(main)/COMPANY_7/production/bom/page.tsx | 2521 +++++++++++++++ .../production/plan-management/page.tsx | 406 +-- .../process-info/ItemRoutingTab.tsx | 905 +++--- .../process-info/ProcessMasterTab.tsx | 589 ++-- .../production/process-info/page.tsx | 223 +- .../production/work-instruction/page.tsx | 537 ++-- .../(main)/COMPANY_7/purchase/order/page.tsx | 1267 ++++++++ .../COMPANY_7/purchase/purchase-item/page.tsx | 1240 ++++++++ .../COMPANY_7/purchase/supplier/page.tsx | 2762 +++++++++++++++++ .../COMPANY_7/quality/inspection/page.tsx | 1703 ++++++++++ .../quality/item-inspection/page.tsx | 721 +++++ .../app/(main)/COMPANY_7/sales/claim/page.tsx | 1103 +++---- .../(main)/COMPANY_7/sales/customer/page.tsx | 2600 ++++++++++++---- .../app/(main)/COMPANY_7/sales/order/page.tsx | 1625 +++++++--- .../app/(main)/COMPANY_7/sales/quote/page.tsx | 995 ------ .../COMPANY_7/sales/sales-item/page.tsx | 935 +++--- .../COMPANY_7/sales/shipping-order/page.tsx | 748 +++-- .../COMPANY_7/sales/shipping-plan/page.tsx | 661 ++-- .../design/change-management/page.tsx | 1524 +++++++++ .../COMPANY_8/design/design-request/page.tsx | 781 +++++ .../(main)/COMPANY_8/design/my-work/page.tsx | 1958 ++++++++++++ .../(main)/COMPANY_8/design/project/page.tsx | 1501 +++++++++ .../COMPANY_8/design/task-management/page.tsx | 1288 ++++++++ .../(main)/COMPANY_8/equipment/info/page.tsx | 945 ++++++ .../COMPANY_8/equipment/plc-settings/page.tsx | 501 +++ .../(main)/COMPANY_8/logistics/info/page.tsx | 905 ++++++ .../COMPANY_8/logistics/inventory/page.tsx | 738 +++++ .../logistics/material-status/page.tsx | 608 ++++ .../COMPANY_8/logistics/outbound/page.tsx | 1789 +++++++++++ .../COMPANY_8/logistics/packaging/page.tsx | 1031 ++++++ .../COMPANY_8/logistics/receiving/page.tsx | 1757 +++++++++++ .../COMPANY_8/logistics/warehouse/page.tsx | 1608 ++++++++++ .../COMPANY_8/master-data/company/page.tsx | 848 +++++ .../COMPANY_8/master-data/department/page.tsx | 767 +++++ .../COMPANY_8/master-data/item-info/page.tsx | 535 ++++ .../COMPANY_8/master-data/options/page.tsx | 136 + .../app/(main)/COMPANY_8/mold/info/page.tsx | 1450 +++++++++ .../COMPANY_8/monitoring/equipment/page.tsx | 575 ++++ .../COMPANY_8/monitoring/production/page.tsx | 504 +++ .../COMPANY_8/monitoring/quality/page.tsx | 512 +++ .../outsourcing/subcontractor-item/page.tsx | 577 ++++ .../outsourcing/subcontractor/page.tsx | 1410 +++++++++ .../(main)/COMPANY_8/production/bom/page.tsx | 2521 +++++++++++++++ .../production/plan-management/page.tsx | 1825 +++++++++++ .../process-info/ItemRoutingTab.tsx | 994 ++++++ .../process-info/ProcessMasterTab.tsx | 664 ++++ .../process-info/ProcessWorkStandardTab.tsx | 17 + .../production/process-info/page.tsx | 223 ++ .../WorkStandardEditModal.tsx | 1007 ++++++ .../production/work-instruction/page.tsx | 843 +++++ .../(main)/COMPANY_8/purchase/order/page.tsx | 1267 ++++++++ .../COMPANY_8/purchase/purchase-item/page.tsx | 1240 ++++++++ .../COMPANY_8/purchase/supplier/page.tsx | 2762 +++++++++++++++++ .../COMPANY_8/quality/inspection/page.tsx | 1703 ++++++++++ .../quality/item-inspection/page.tsx | 721 +++++ .../app/(main)/COMPANY_8/sales/claim/page.tsx | 932 ++++++ .../(main)/COMPANY_8/sales/customer/page.tsx | 2760 ++++++++++++++++ .../app/(main)/COMPANY_8/sales/order/page.tsx | 1860 +++++++++++ .../COMPANY_8/sales/sales-item/page.tsx | 1242 ++++++++ .../COMPANY_8/sales/shipping-order/page.tsx | 928 ++++++ .../COMPANY_8/sales/shipping-plan/page.tsx | 454 +++ .../components/layout/AdminPageRenderer.tsx | 62 + 125 files changed, 107224 insertions(+), 18608 deletions(-) create mode 100644 frontend/app/(main)/COMPANY_10/equipment/plc-settings/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/logistics/info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/logistics/inventory/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/logistics/warehouse/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/master-data/company/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/master-data/options/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/mold/info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/monitoring/equipment/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/monitoring/production/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/monitoring/quality/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/production/bom/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/purchase/order/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/purchase/purchase-item/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/purchase/supplier/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/quality/inspection/page.tsx create mode 100644 frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx create mode 100644 frontend/app/(main)/COMPANY_7/equipment/plc-settings/page.tsx create mode 100644 frontend/app/(main)/COMPANY_7/logistics/info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_7/logistics/inventory/page.tsx create mode 100644 frontend/app/(main)/COMPANY_7/logistics/warehouse/page.tsx create mode 100644 frontend/app/(main)/COMPANY_7/master-data/company/page.tsx create mode 100644 frontend/app/(main)/COMPANY_7/master-data/options/page.tsx delete mode 100644 frontend/app/(main)/COMPANY_7/master-data/sample/page.tsx create mode 100644 frontend/app/(main)/COMPANY_7/mold/info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_7/production/bom/page.tsx create mode 100644 frontend/app/(main)/COMPANY_7/purchase/order/page.tsx create mode 100644 frontend/app/(main)/COMPANY_7/purchase/purchase-item/page.tsx create mode 100644 frontend/app/(main)/COMPANY_7/purchase/supplier/page.tsx create mode 100644 frontend/app/(main)/COMPANY_7/quality/inspection/page.tsx create mode 100644 frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx delete mode 100644 frontend/app/(main)/COMPANY_7/sales/quote/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/design/change-management/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/design/design-request/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/design/my-work/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/design/project/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/design/task-management/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/equipment/info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/equipment/plc-settings/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/logistics/info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/logistics/inventory/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/logistics/material-status/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/logistics/outbound/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/logistics/packaging/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/logistics/receiving/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/logistics/warehouse/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/master-data/company/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/master-data/department/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/master-data/item-info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/master-data/options/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/mold/info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/monitoring/equipment/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/monitoring/production/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/monitoring/quality/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/outsourcing/subcontractor-item/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/outsourcing/subcontractor/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/production/bom/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/production/process-info/ItemRoutingTab.tsx create mode 100644 frontend/app/(main)/COMPANY_8/production/process-info/ProcessMasterTab.tsx create mode 100644 frontend/app/(main)/COMPANY_8/production/process-info/ProcessWorkStandardTab.tsx create mode 100644 frontend/app/(main)/COMPANY_8/production/process-info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/production/work-instruction/WorkStandardEditModal.tsx create mode 100644 frontend/app/(main)/COMPANY_8/production/work-instruction/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/purchase/order/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/purchase/purchase-item/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/purchase/supplier/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/quality/inspection/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/sales/claim/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/sales/customer/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/sales/order/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/sales/sales-item/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/sales/shipping-order/page.tsx create mode 100644 frontend/app/(main)/COMPANY_8/sales/shipping-plan/page.tsx diff --git a/frontend/app/(main)/COMPANY_10/design/change-management/page.tsx b/frontend/app/(main)/COMPANY_10/design/change-management/page.tsx index 8879ba8a..39f06769 100644 --- a/frontend/app/(main)/COMPANY_10/design/change-management/page.tsx +++ b/frontend/app/(main)/COMPANY_10/design/change-management/page.tsx @@ -18,7 +18,6 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { @@ -32,16 +31,8 @@ import { import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from "@/components/ui/resizable"; -import { - Search, - RotateCcw, Plus, Save, - ClipboardList, Inbox, Pencil, FileText, @@ -50,6 +41,7 @@ import { Paperclip, Upload, Loader2, + Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; @@ -62,6 +54,10 @@ import { createEcn, updateEcn, } from "@/lib/api/design"; +import { useTableSettings } from "@/hooks/useTableSettings"; +import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; // --- Types --- type ChangeType = "설계오류" | "원가절감" | "고객요청" | "공정개선" | "법규대응"; @@ -119,65 +115,83 @@ interface EcnItem { const getChangeTypeStyle = (type: ChangeType) => { switch (type) { case "설계오류": - return "bg-rose-100 text-rose-800 border-rose-200"; + return "bg-destructive/10 text-destructive border-destructive/20"; case "원가절감": - return "bg-emerald-100 text-emerald-800 border-emerald-200"; + return "bg-success/10 text-success border-success/20"; case "고객요청": - return "bg-blue-100 text-blue-800 border-blue-200"; + return "bg-info/10 text-info border-info/20"; case "공정개선": - return "bg-amber-100 text-amber-800 border-amber-200"; + return "bg-warning/10 text-warning border-warning/20"; case "법규대응": - return "bg-purple-100 text-purple-800 border-purple-200"; + return "bg-primary/10 text-primary border-primary/20"; default: - return "bg-gray-100 text-gray-800 border-gray-200"; + return "bg-muted text-muted-foreground border-border"; } }; const getEcrStatusStyle = (status: EcrStatus) => { switch (status) { case "요청접수": - return "bg-blue-100 text-blue-800 border-blue-200"; + return "bg-info/10 text-info border-info/20"; case "영향도분석": - return "bg-amber-100 text-amber-800 border-amber-200"; + return "bg-warning/10 text-warning border-warning/20"; case "ECN발행": - return "bg-emerald-100 text-emerald-800 border-emerald-200"; + return "bg-success/10 text-success border-success/20"; case "기각": - return "bg-slate-100 text-slate-800 border-slate-200"; + return "bg-muted text-muted-foreground border-border"; default: - return "bg-gray-100 text-gray-800 border-gray-200"; + return "bg-muted text-muted-foreground border-border"; } }; const getEcnStatusStyle = (status: EcnStatus) => { switch (status) { case "ECN발행": - return "bg-blue-100 text-blue-800 border-blue-200"; + return "bg-info/10 text-info border-info/20"; case "도면변경": - return "bg-purple-100 text-purple-800 border-purple-200"; + return "bg-primary/10 text-primary border-primary/20"; case "통보완료": - return "bg-teal-100 text-teal-800 border-teal-200"; + return "bg-warning/10 text-warning border-warning/20"; case "적용완료": - return "bg-emerald-100 text-emerald-800 border-emerald-200"; + return "bg-success/10 text-success border-success/20"; default: - return "bg-gray-100 text-gray-800 border-gray-200"; + return "bg-muted text-muted-foreground border-border"; } }; const getImpactBadgeStyle = (impact: string) => { switch (impact) { case "BOM": - return "bg-blue-100 text-blue-800 border-blue-200"; + return "bg-info/10 text-info border-info/20"; case "공정": - return "bg-amber-100 text-amber-800 border-amber-200"; + return "bg-warning/10 text-warning border-warning/20"; case "금형": - return "bg-rose-100 text-rose-800 border-rose-200"; + return "bg-destructive/10 text-destructive border-destructive/20"; case "검사기준": - return "bg-purple-100 text-purple-800 border-purple-200"; + return "bg-primary/10 text-primary border-primary/20"; case "구매": case "원가": - return "bg-emerald-100 text-emerald-800 border-emerald-200"; + return "bg-success/10 text-success border-success/20"; default: - return "bg-gray-100 text-gray-800 border-gray-200"; + return "bg-muted text-muted-foreground border-border"; + } +}; + +const getTimelineStatusStyle = (status: string) => { + switch (status) { + case "기각": + return "bg-muted text-muted-foreground border-border"; + case "적용완료": + case "ECN발행": + return "bg-success/10 text-success border-success/20"; + case "영향도분석": + return "bg-warning/10 text-warning border-warning/20"; + case "도면변경": + return "bg-primary/10 text-primary border-primary/20"; + case "통보완료": + return "bg-info/10 text-info border-info/20"; + default: + return "bg-info/10 text-info border-info/20"; } }; @@ -278,12 +292,12 @@ function Timeline({ history }: { history: EcrHistory[] }) { className={cn( "w-3 h-3 rounded-full border-2 mt-1.5 shrink-0", isLast && isRejected - ? "bg-rose-500 border-rose-300" + ? "bg-destructive border-destructive/60" : isLast && isCompleted - ? "bg-emerald-500 border-emerald-300" + ? "bg-success border-success/60" : isLast ? "bg-primary border-primary/50 ring-4 ring-primary/10" - : "bg-emerald-500 border-emerald-300" + : "bg-success border-success/60" )} /> {!isLast && ( @@ -295,19 +309,7 @@ function Timeline({ history }: { history: EcrHistory[] }) { {h.status} @@ -325,20 +327,45 @@ function Timeline({ history }: { history: EcrHistory[] }) { ); } +// --- Grid Columns --- +const ECR_GRID_COLUMNS = [ + { key: "request_no", label: "ECR번호" }, + { key: "change_type", label: "변경유형" }, + { key: "status", label: "상태" }, + { key: "urgency", label: "긴급" }, + { key: "target_name", label: "대상 품목/설비" }, + { key: "drawing_no", label: "도면번호" }, + { key: "req_dept", label: "요청부서" }, + { key: "requester", label: "요청자" }, + { key: "request_date", label: "요청일자" }, + { key: "ecn_no", label: "관련 ECN" }, +]; + +const ECN_GRID_COLUMNS = [ + { key: "ecn_no", label: "ECN번호" }, + { key: "status", label: "상태" }, + { key: "target", label: "대상 품목/설비" }, + { key: "drawing_after", label: "도면 (변경 후)" }, + { key: "designer", label: "설계담당" }, + { key: "ecn_date", label: "발행일자" }, + { key: "apply_date", label: "적용일자" }, + { key: "notify_depts", label: "통보 부서" }, + { key: "ecr_id", label: "관련 ECR" }, +]; + // --- Main Component --- export default function DesignChangeManagementPage() { + const tsEcr = useTableSettings("c16-change-management-ecr", "dsn_design_request", ECR_GRID_COLUMNS); + const tsEcn = useTableSettings("c16-change-management-ecn", "dsn_ecn", ECN_GRID_COLUMNS); const [currentTab, setCurrentTab] = useState("ecr"); const [ecrData, setEcrData] = useState([]); const [ecnData, setEcnData] = useState([]); const [loading, setLoading] = useState(true); const [selectedId, setSelectedId] = useState(null); + const [detailOpen, setDetailOpen] = useState(false); - // 검색 상태 - const [searchDateFrom, setSearchDateFrom] = useState(""); - const [searchDateTo, setSearchDateTo] = useState(""); - const [searchStatus, setSearchStatus] = useState("all"); - const [searchChangeType, setSearchChangeType] = useState("all"); - const [searchKeyword, setSearchKeyword] = useState(""); + // 검색 필터 (DynamicSearchFilter) + const [searchFilters, setSearchFilters] = useState([]); // ECR 모달 const [isEcrModalOpen, setIsEcrModalOpen] = useState(false); @@ -356,13 +383,6 @@ export default function DesignChangeManagementPage() { const [rejectReason, setRejectReason] = useState(""); const [rejectTargetId, setRejectTargetId] = useState(""); - useEffect(() => { - const today = new Date(); - const threeMonthsAgo = new Date(today); - threeMonthsAgo.setMonth(today.getMonth() - 3); - setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); - setSearchDateTo(today.toISOString().split("T")[0]); - }, []); const fetchData = useCallback(async () => { setLoading(true); @@ -379,7 +399,7 @@ export default function DesignChangeManagementPage() { setEcnData((ecnRes.data as any[]).map((r) => mapEcnFromApi(r, ecrList))); } } catch { - toast.error("데이터를 불러오는데 실패했습니다."); + toast.error("데이터를 불러오는데 실패했어요."); } finally { setLoading(false); } @@ -389,39 +409,66 @@ export default function DesignChangeManagementPage() { fetchData(); }, [fetchData]); + // snake_case → camelCase 매핑 (ECR) + const ecrFieldMap: Record = { + request_no: "id", + request_date: "date", + change_type: "changeType", + target_name: "target", + drawing_no: "drawingNo", + req_dept: "reqDept", + ecn_no: "ecnNo", + apply_timing: "applyTiming", + }; + // snake_case → camelCase 매핑 (ECN) + const ecnFieldMap: Record = { + ecn_no: "id", + ecn_date: "date", + apply_date: "applyDate", + drawing_before: "drawingBefore", + drawing_after: "drawingAfter", + ecr_id: "ecrNo", + notify_depts: "notifyDepts", + }; + const getFieldValue = (obj: any, colName: string, map: Record): string => { + const key = map[colName] || colName; + const val = obj[key]; + if (Array.isArray(val)) return val.join(","); + return val !== undefined && val !== null ? String(val) : ""; + }; + + const applyFilters = (items: any[], map: Record) => { + if (searchFilters.length === 0) return items; + return items.filter((item) => { + for (const f of searchFilters) { + const val = getFieldValue(item, f.columnName, map); + if (f.operator === "contains") { + if (!val.toLowerCase().includes(f.value.toLowerCase())) return false; + } else if (f.operator === "equals") { + if (val !== f.value) return false; + } else if (f.operator === "in") { + const allowed = f.value.split("|"); + if (!allowed.includes(val)) return false; + } else if (f.operator === "between") { + const [from, to] = f.value.split("|"); + if (from && val < from) return false; + if (to && val > to) return false; + } + } + return true; + }); + }; + // --- Filtered Data --- const filteredEcr = useMemo(() => { - return ecrData - .filter((item) => { - if (searchDateFrom && item.date < searchDateFrom) return false; - if (searchDateTo && item.date > searchDateTo) return false; - if (searchStatus !== "all" && item.status !== searchStatus) return false; - if (searchChangeType !== "all" && item.changeType !== searchChangeType) return false; - if (searchKeyword) { - const kw = searchKeyword.toLowerCase(); - const str = [item.id, item.target, item.requester, item.drawingNo].join(" ").toLowerCase(); - if (!str.includes(kw)) return false; - } - return true; - }) - .sort((a, b) => b.date.localeCompare(a.date)); - }, [ecrData, searchDateFrom, searchDateTo, searchStatus, searchChangeType, searchKeyword]); + return applyFilters(ecrData, ecrFieldMap) + .sort((a: EcrItem, b: EcrItem) => b.date.localeCompare(a.date)); + }, [ecrData, searchFilters]); const filteredEcn = useMemo(() => { - return ecnData - .filter((item) => { - if (searchDateFrom && item.date < searchDateFrom) return false; - if (searchDateTo && item.date > searchDateTo) return false; - if (searchStatus !== "all" && item.status !== searchStatus) return false; - if (searchKeyword) { - const kw = searchKeyword.toLowerCase(); - const str = [item.id, item.target, item.designer, item.ecrNo].join(" ").toLowerCase(); - if (!str.includes(kw)) return false; - } - return true; - }) - .sort((a, b) => b.date.localeCompare(a.date)); - }, [ecnData, searchDateFrom, searchDateTo, searchStatus, searchKeyword]); + return applyFilters(ecnData, ecnFieldMap) + .sort((a: EcnItem, b: EcnItem) => b.date.localeCompare(a.date)); + }, [ecnData, searchFilters]); // --- Status Counts --- const ecrStatusCounts = useMemo(() => { @@ -450,35 +497,21 @@ export default function DesignChangeManagementPage() { const handleTabSwitch = (tab: TabType) => { setCurrentTab(tab); setSelectedId(null); - setSearchStatus("all"); }; - // --- Search --- - const handleResetSearch = () => { - const today = new Date(); - const threeMonthsAgo = new Date(today); - threeMonthsAgo.setMonth(today.getMonth() - 3); - setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); - setSearchDateTo(today.toISOString().split("T")[0]); - setSearchStatus("all"); - setSearchChangeType("all"); - setSearchKeyword(""); - }; - - const handleFilterByStatus = (status: string) => { - setSearchStatus(status); + const handleFilterByStatus = (_status: string) => { + // Status filter now handled by DynamicSearchFilter }; // --- ECR/ECN Navigation --- const navigateToLink = (targetId: string) => { + setDetailOpen(false); if (targetId.startsWith("ECN")) { setCurrentTab("ecn"); setSelectedId(targetId); - setSearchStatus("all"); } else if (targetId.startsWith("ECR")) { setCurrentTab("ecr"); setSelectedId(targetId); - setSearchStatus("all"); } }; @@ -540,19 +573,19 @@ export default function DesignChangeManagementPage() { const handleSaveEcr = async () => { if (!ecrForm.changeType) { - toast.error("변경 유형을 선택하세요."); + toast.error("변경 유형을 선택해 주세요."); return; } if (!ecrForm.target?.trim()) { - toast.error("대상 품목/설비를 입력하세요."); + toast.error("대상 품목/설비를 입력해 주세요."); return; } if (!ecrForm.reason?.trim()) { - toast.error("변경 사유를 입력하세요."); + toast.error("변경 사유를 입력해 주세요."); return; } if (!ecrForm.content?.trim()) { - toast.error("변경 요구 내용을 입력하세요."); + toast.error("변경 요구 내용을 입력해 주세요."); return; } @@ -581,11 +614,11 @@ export default function DesignChangeManagementPage() { apply_timing: ecrForm.applyTiming || "즉시", }); if (res.success) { - toast.success("ECR이 수정되었습니다."); + toast.success("ECR이 수정되었어요."); setIsEcrModalOpen(false); fetchData(); } else { - toast.error(res.message || "ECR 수정에 실패했습니다."); + toast.error(res.message || "ECR 수정에 실패했어요."); } } else { const res = await createDesignRequest({ @@ -606,11 +639,11 @@ export default function DesignChangeManagementPage() { history: [historyEntry], }); if (res.success) { - toast.success("ECR이 등록되었습니다."); + toast.success("ECR이 등록되었어요."); setIsEcrModalOpen(false); fetchData(); } else { - toast.error(res.message || "ECR 등록에 실패했습니다."); + toast.error(res.message || "ECR 등록에 실패했어요."); } } }; @@ -641,15 +674,15 @@ export default function DesignChangeManagementPage() { const handleSaveEcn = async () => { if (!ecnForm.after?.trim()) { - toast.error("변경 후(TO-BE) 내용을 입력하세요."); + toast.error("변경 후(TO-BE) 내용을 입력해 주세요."); return; } if (!ecnForm.applyDate) { - toast.error("적용일자를 입력하세요."); + toast.error("적용일자를 입력해 주세요."); return; } if (!ecnForm.ecrId) { - toast.error("관련 ECR 정보가 없습니다."); + toast.error("관련 ECR 정보가 없어요."); return; } @@ -692,11 +725,11 @@ export default function DesignChangeManagementPage() { user_name: ecnForm.designer || "시스템", description: `${ecnNo} 발행`, }); - toast.success("ECN이 발행되었습니다."); + toast.success("ECN이 발행되었어요."); setIsEcnModalOpen(false); fetchData(); } else { - toast.error(res.message || "ECN 발행에 실패했습니다."); + toast.error(res.message || "ECN 발행에 실패했어요."); } }; @@ -709,19 +742,19 @@ export default function DesignChangeManagementPage() { const handleRejectSubmit = async () => { if (!rejectReason.trim()) { - toast.error("기각 사유를 입력하세요."); + toast.error("기각 사유를 입력해 주세요."); return; } const ecr = ecrData.find((r) => r.id === rejectTargetId); if (!ecr?._id) { - toast.error("ECR 정보를 찾을 수 없습니다."); + toast.error("ECR 정보를 찾을 수 없어요."); return; } const updateRes = await updateDesignRequest(ecr._id, { status: "기각", review_memo: rejectReason }); if (!updateRes.success) { - toast.error(updateRes.message || "ECR 기각에 실패했습니다."); + toast.error(updateRes.message || "ECR 기각에 실패했어요."); return; } await addRequestHistory(ecr._id, { @@ -730,571 +763,397 @@ export default function DesignChangeManagementPage() { user_name: "설계팀", description: rejectReason, }); - toast.success("ECR이 기각되었습니다."); + toast.success("ECR이 기각되었어요."); setIsRejectModalOpen(false); fetchData(); }; // --- Stat Cards --- const ecrStatCards = [ - { label: "요청접수", value: ecrStatusCounts["요청접수"] || 0, gradient: "from-indigo-500 to-blue-600", textColor: "text-white" }, - { label: "영향도분석", value: ecrStatusCounts["영향도분석"] || 0, gradient: "from-amber-400 to-orange-500", textColor: "text-white" }, - { label: "ECN발행", value: ecrStatusCounts["ECN발행"] || 0, gradient: "from-emerald-400 to-green-600", textColor: "text-white" }, + { label: "요청접수", value: ecrStatusCounts["요청접수"] || 0, color: "text-info" }, + { label: "영향도분석", value: ecrStatusCounts["영향도분석"] || 0, color: "text-warning" }, + { label: "ECN발행", value: ecrStatusCounts["ECN발행"] || 0, color: "text-success" }, ]; const ecnStatCards = [ - { label: "도면변경", value: ecnStatusCounts["도면변경"] || 0, gradient: "from-purple-400 to-violet-600", textColor: "text-white" }, - { label: "통보완료", value: ecnStatusCounts["통보완료"] || 0, gradient: "from-teal-400 to-cyan-600", textColor: "text-white" }, - { label: "적용완료", value: ecnStatusCounts["적용완료"] || 0, gradient: "from-emerald-400 to-green-600", textColor: "text-white" }, + { label: "도면변경", value: ecnStatusCounts["도면변경"] || 0, color: "text-primary" }, + { label: "통보완료", value: ecnStatusCounts["통보완료"] || 0, color: "text-info" }, + { label: "적용완료", value: ecnStatusCounts["적용완료"] || 0, color: "text-success" }, ]; const currentStatCards = currentTab === "ecr" ? ecrStatCards : ecnStatCards; const currentList = currentTab === "ecr" ? filteredEcr : filteredEcn; - const currentStatuses = currentTab === "ecr" ? ECR_STATUSES : ECN_STATUSES; + + const handleRowClick = (id: string) => { + setSelectedId(id); + setDetailOpen(true); + }; return ( -
+
{loading && (
)} - {/* 검색 섹션 */} - - -
- -
- setSearchDateFrom(e.target.value)} - /> - ~ - setSearchDateTo(e.target.value)} - /> -
-
-
- - -
+ {/* 탭 선택 + 검색 필터 */} +
+
+ +
+ {currentTab === "ecr" ? ( + + ) : ( + + )} +
-
- - -
+ {/* 현황 카드 */} +
+ {currentStatCards.map((card) => ( + + ))} +
+ {/* 액션 바 */} +
+
+

+ {currentTab === "ecr" ? "설계변경요청(ECR) 목록" : "설계변경통지(ECN) 목록"} +

+ {currentList.length}건 +
+
{currentTab === "ecr" && ( -
- - -
- )} - -
- - setSearchKeyword(e.target.value)} - /> -
- -
- -
- -
- - + )} + +
+
- {/* 메인 분할 레이아웃 */} -
- - {/* 왼쪽: 목록 */} - -
-
-
- - {currentTab === "ecr" ? "설계변경요청(ECR) 목록" : "설계변경통지(ECN) 목록"} - - {currentList.length}건 - -
- {currentTab === "ecr" && ( - - )} -
+ {/* 테이블 영역 */} +
+
+ {currentTab === "ecr" ? ( + {val} }, + { key: "changeType", label: "변경유형", width: "w-[90px]", align: "center" as const, render: (val: any) => {val} }, + { key: "status", label: "상태", width: "w-[90px]", align: "center" as const, render: (val: any) => {val} }, + { key: "urgency", label: "긴급", width: "w-[60px]", align: "center" as const, render: (val: any) => val === "긴급" ? 긴급 : - }, + { key: "target", label: "대상 품목/설비", width: "w-[200px]" }, + { key: "drawingNo", label: "도면번호", width: "w-[150px]" }, + { key: "reqDept", label: "요청부서", width: "w-[80px]" }, + { key: "requester", label: "요청자", width: "w-[70px]" }, + { key: "date", label: "요청일자", width: "w-[100px]" }, + { key: "ecnNo", label: "관련 ECN", width: "w-[130px]", render: (val: any) => val ? : - }, + ] as EDataTableColumn[]} + data={tsEcr.groupData(filteredEcr)} + rowKey={(row) => row.id} + selectedId={selectedId} + onSelect={(id) => { if (id) handleRowClick(id); }} + onRowClick={(row) => handleRowClick(row.id)} + emptyMessage="조건에 맞는 ECR이 없어요" + showRowNumber + showPagination={false} + draggableColumns={false} + /> + ) : ( + {val} }, + { key: "status", label: "상태", width: "w-[90px]", align: "center" as const, render: (val: any) => {val} }, + { key: "target", label: "대상 품목/설비", width: "w-[200px]" }, + { key: "drawingAfter", label: "도면 (변경 후)", width: "w-[160px]", render: (val: any) => {val} }, + { key: "designer", label: "설계담당", width: "w-[80px]" }, + { key: "date", label: "발행일자", width: "w-[100px]" }, + { key: "applyDate", label: "적용일자", width: "w-[100px]" }, + { key: "notifyDepts", label: "통보 부서", width: "w-[140px]", render: (val: any) => {Array.isArray(val) ? val.join(", ") : val} }, + { key: "ecrNo", label: "관련 ECR", width: "w-[130px]", render: (val: any) => }, + ] as EDataTableColumn[]} + data={tsEcn.groupData(filteredEcn)} + rowKey={(row) => row.id} + selectedId={selectedId} + onSelect={(id) => { if (id) handleRowClick(id); }} + onRowClick={(row) => handleRowClick(row.id)} + emptyMessage="조건에 맞는 ECN이 없어요" + showRowNumber + showPagination={false} + draggableColumns={false} + /> + )} +
+
-
- {currentTab === "ecr" ? ( - - - - No - ECR번호 - 변경유형 - 상태 - 긴급 - 대상 품목/설비 - 도면번호 - 요청부서 - 요청자 - 요청일자 - 관련 ECN - - - - {filteredEcr.length === 0 ? ( - - -
- - 조건에 맞는 ECR이 없습니다 -
-
-
+ {/* 상세 정보 다이얼로그 */} + + + + {currentTab === "ecr" ? `ECR 상세 — ${selectedEcr?.id || ""}` : `ECN 상세 — ${selectedEcn?.id || ""}`} + {currentTab === "ecr" ? "설계변경요청의 상세 정보를 확인해요." : "설계변경통지의 상세 정보를 확인해요."} + +
+
+ {/* ECR 상세 */} + {selectedEcr ? ( + <> +
+

+ 기본 정보 +

+
+
+ ECR번호 + {selectedEcr.id} +
+
+ 상태 + + {selectedEcr.status} + +
+
+ 변경 유형 + + {selectedEcr.changeType} + +
+
+ 긴급도 + + {selectedEcr.urgency === "긴급" ? ( + 긴급 ) : ( - filteredEcr.map((item, idx) => ( - setSelectedId(item.id)} - > - {idx + 1} - {item.id} - - - {item.changeType} - - - - - {item.status} - - - - {item.urgency === "긴급" ? ( - - 긴급 - - ) : ( - "-" - )} - - {item.target} - {item.drawingNo} - {item.reqDept} - {item.requester} - {item.date} - - {item.ecnNo ? ( - - ) : ( - "-" - )} - - - )) + "보통" )} - -
- ) : ( - - - - No - ECN번호 - 상태 - 대상 품목/설비 - 도면 (변경 후) - 설계담당 - 발행일자 - 적용일자 - 통보 부서 - 관련 ECR - - - - {filteredEcn.length === 0 ? ( - - -
- - 조건에 맞는 ECN이 없습니다 -
-
-
- ) : ( - filteredEcn.map((item, idx) => ( - setSelectedId(item.id)} - > - {idx + 1} - {item.id} - - - {item.status} - - - {item.target} - {item.drawingAfter} - {item.designer} - {item.date} - {item.applyDate} - {item.notifyDepts.join(", ")} - - - - - )) - )} -
-
- )} -
-
-
- - - - {/* 오른쪽: 상세 */} - -
-
- - - 상세 정보 - - {selectedEcr && ( -
- - {selectedEcr.status === "영향도분석" && ( - <> - - - + +
+
+ 대상 품목/설비 + {selectedEcr.target} +
+
+ 도면번호 + {selectedEcr.drawingNo} +
+
+ 요청부서 / 요청자 + {selectedEcr.reqDept} / {selectedEcr.requester} +
+
+ 요청일자 + {selectedEcr.date} +
+
+ 희망 적용시점 + {selectedEcr.applyTiming} +
+
+ 관련 ECN + {selectedEcr.ecnNo ? ( + + ) : ( + 미발행 )}
- )} -
+
+ -
- {/* 현황 카드 */} -
- {currentStatCards.map((card) => ( - +
+

변경 사유

+
+ {selectedEcr.reason} +
+
+ +
+

변경 요구 내용

+
+ {selectedEcr.content} +
+
+ +
+

영향 범위

+
+ {selectedEcr.impact.map((imp) => ( + + {imp} + ))}
+
- {/* ECR 상세 */} - {selectedEcr ? ( -
-
-

- 기본 정보 -

-
-
- ECR번호 - {selectedEcr.id} -
-
- 상태 - - {selectedEcr.status} - -
-
- 변경 유형 - - {selectedEcr.changeType} - -
-
- 긴급도 - - {selectedEcr.urgency === "긴급" ? ( - 긴급 - ) : ( - "보통" - )} - -
-
- 대상 품목/설비 - {selectedEcr.target} -
-
- 도면번호 - {selectedEcr.drawingNo} -
-
- 요청부서 / 요청자 - {selectedEcr.reqDept} / {selectedEcr.requester} -
-
- 요청일자 - {selectedEcr.date} -
-
- 희망 적용시점 - {selectedEcr.applyTiming} -
-
- 관련 ECN - {selectedEcr.ecnNo ? ( - - ) : ( - 미발행 - )} -
-
-
- -
-

변경 사유

-
- {selectedEcr.reason} -
-
- -
-

변경 요구 내용

-
- {selectedEcr.content} -
-
- -
-

영향 범위

-
- {selectedEcr.impact.map((imp) => ( - - {imp} - - ))} -
-
- -
-

처리 이력

- -
+
+

처리 이력

+ +
+ + ) : selectedEcn ? ( + <> +
+

+ ECN 기본 정보 +

+
+
+ ECN번호 + {selectedEcn.id}
- ) : selectedEcn ? ( -
-
-

- ECN 기본 정보 -

-
-
- ECN번호 - {selectedEcn.id} -
-
- 상태 - - {selectedEcn.status} - -
-
- 대상 품목/설비 - {selectedEcn.target} -
-
- 설계담당 - {selectedEcn.designer} -
-
- 발행일자 - {selectedEcn.date} -
-
- 적용일자 - {selectedEcn.applyDate} -
-
- 관련 ECR - -
-
- 통보 부서 - {selectedEcn.notifyDepts.join(", ")} -
-
-
- -
-

변경 전/후 비교

-
-
-
- 변경 전 ({selectedEcn.drawingBefore}) -
-
{selectedEcn.before}
-
-
-
- 변경 후 ({selectedEcn.drawingAfter}) -
-
{selectedEcn.after}
-
-
-
- -
-

변경 사유

-
- {selectedEcn.reason} -
- {selectedEcn.remark && ( -

비고: {selectedEcn.remark}

- )} -
- -
-

처리 이력

- -
+
+ 상태 + + {selectedEcn.status} +
- ) : ( -
-
- +
+ 대상 품목/설비 + {selectedEcn.target} +
+
+ 설계담당 + {selectedEcn.designer} +
+
+ 발행일자 + {selectedEcn.date} +
+
+ 적용일자 + {selectedEcn.applyDate} +
+
+ 관련 ECR + +
+
+ 통보 부서 + {selectedEcn.notifyDepts.join(", ")} +
+
+
+ +
+

변경 전/후 비교

+
+
+
+ 변경 전 ({selectedEcn.drawingBefore})
-

좌측 목록에서 항목을 선택하세요

+
{selectedEcn.before}
+
+
+ 변경 후 ({selectedEcn.drawingAfter}) +
+
{selectedEcn.after}
+
+
+
+ +
+

변경 사유

+
+ {selectedEcn.reason} +
+ {selectedEcn.remark && ( +

비고: {selectedEcn.remark}

)} -
-
- - -
+ + +
+

처리 이력

+ +
+ + ) : null} +
+
+ + {selectedEcr && ( + <> + + {selectedEcr.status === "영향도분석" && ( + <> + + + + )} + + )} + + + {/* ECR 등록/수정 모달 */} - + - - {isEcrEditMode ? "설계변경요청(ECR) 수정" : "설계변경요청(ECR) 등록"} - - - {isEcrEditMode ? "ECR 정보를 수정합니다." : "새로운 설계변경요청을 등록합니다."} - + {isEcrEditMode ? "설계변경요청(ECR) 수정" : "설계변경요청(ECR) 등록"} + {isEcrEditMode ? "ECR 정보를 수정해요." : "새로운 설계변경요청을 등록해요."} - -
-
+
+
{/* 좌측: 요청 정보 */}

변경 요청 정보

- +
- +
- + setEcrForm((p) => ({ ...p, changeType: v as ChangeType }))}> @@ -1332,7 +1191,7 @@ export default function DesignChangeManagementPage() {
- + setEcrForm((p) => ({ ...p, target: e.target.value }))} @@ -1356,7 +1215,7 @@ export default function DesignChangeManagementPage() {
- + setEcrForm((p) => ({ ...p, drawingNo: e.target.value }))} @@ -1367,7 +1226,7 @@ export default function DesignChangeManagementPage() {
- +
- + setEcrForm((p) => ({ ...p, requester: e.target.value }))} @@ -1397,27 +1256,27 @@ export default function DesignChangeManagementPage() {

변경 내용

- +