From d03f92947dac8ec807fc8f7d8489f1c21b98413f Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 3 Apr 2026 09:28:59 +0900 Subject: [PATCH] feat: Implement dynamic search filter in Shipping Plan page - Integrated DynamicSearchFilter component to manage search filters. - Removed individual search state variables and replaced with a single searchFilters state. - Updated fetchData function to handle new filter structure. - Refactored search filter UI to utilize DynamicSearchFilter. - Adjusted table header styles for better visibility and consistency. style: Update global styles for improved UI consistency - Unified font size across the application to 16px, excluding buttons. - Adjusted header padding and font size for better readability. - Enhanced dark mode styles for checkboxes to ensure visibility. feat: Add Options Setting page for category and numbering configurations - Created a new OptionsSettingPage component with tabs for category and numbering settings. - Implemented drag-to-resize functionality for the category column list. - Integrated CategoryColumnList and CategoryValueManager components for managing categories. feat: Introduce useTableSettings hook for table configuration management - Developed useTableSettings hook to manage column visibility, order, and width. - Implemented localStorage persistence for table settings. - Enhanced TableSettingsModal to accept defaultVisibleKeys for initial column visibility. chore: Update AdminPageRenderer to include new COMPANY_16 routes - Added new routes for COMPANY_16 master-data options and other pages. --- .../design/change-management/page.tsx | 253 +++++++----------- .../COMPANY_16/design/design-request/page.tsx | 141 +++------- .../(main)/COMPANY_16/design/my-work/page.tsx | 24 +- .../(main)/COMPANY_16/design/project/page.tsx | 106 ++++---- .../design/task-management/page.tsx | 142 ++++------ .../(main)/COMPANY_16/equipment/info/page.tsx | 115 ++++---- .../equipment/plc-settings/page.tsx | 25 +- .../(main)/COMPANY_16/logistics/info/page.tsx | 79 ++---- .../COMPANY_16/logistics/inventory/page.tsx | 144 ++++------ .../logistics/material-status/page.tsx | 1 + .../COMPANY_16/logistics/outbound/page.tsx | 200 ++++---------- .../COMPANY_16/logistics/packaging/page.tsx | 140 +++++----- .../COMPANY_16/logistics/receiving/page.tsx | 246 ++++++----------- .../COMPANY_16/logistics/warehouse/page.tsx | 160 ++++------- .../COMPANY_16/master-data/company/page.tsx | 16 +- .../master-data/department/page.tsx | 142 ++++------ .../COMPANY_16/master-data/item-info/page.tsx | 89 ++---- .../COMPANY_16/master-data/options/page.tsx | 136 ++++++++++ .../app/(main)/COMPANY_16/mold/info/page.tsx | 44 +-- .../outsourcing/subcontractor-item/page.tsx | 39 +-- .../outsourcing/subcontractor/page.tsx | 119 +++----- .../(main)/COMPANY_16/production/bom/page.tsx | 115 +++----- .../production/plan-management/page.tsx | 171 ++++++------ .../production/process-info/page.tsx | 1 + .../production/work-instruction/page.tsx | 153 +++++------ .../(main)/COMPANY_16/purchase/order/page.tsx | 183 +++++-------- .../purchase/purchase-item/page.tsx | 106 ++++---- .../COMPANY_16/purchase/supplier/page.tsx | 104 ++++--- .../COMPANY_16/quality/inspection/page.tsx | 107 ++++---- .../quality/item-inspection/page.tsx | 76 +++--- .../(main)/COMPANY_16/sales/claim/page.tsx | 140 ++-------- .../(main)/COMPANY_16/sales/customer/page.tsx | 189 ++++++------- .../(main)/COMPANY_16/sales/order/page.tsx | 142 +++------- .../COMPANY_16/sales/sales-item/page.tsx | 120 ++++----- .../COMPANY_16/sales/shipping-order/page.tsx | 201 ++++---------- .../COMPANY_16/sales/shipping-plan/page.tsx | 164 +++--------- frontend/app/globals.css | 26 +- .../components/common/TableSettingsModal.tsx | 5 +- .../components/layout/AdminPageRenderer.tsx | 17 +- frontend/hooks/useTableSettings.ts | 153 +++++++++++ 40 files changed, 1863 insertions(+), 2671 deletions(-) create mode 100644 frontend/app/(main)/COMPANY_16/master-data/options/page.tsx create mode 100644 frontend/hooks/useTableSettings.ts diff --git a/frontend/app/(main)/COMPANY_16/design/change-management/page.tsx b/frontend/app/(main)/COMPANY_16/design/change-management/page.tsx index 42fb528f..5ceedc05 100644 --- a/frontend/app/(main)/COMPANY_16/design/change-management/page.tsx +++ b/frontend/app/(main)/COMPANY_16/design/change-management/page.tsx @@ -31,7 +31,6 @@ import { import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { - RotateCcw, Plus, Save, Inbox, @@ -57,6 +56,7 @@ import { } from "@/lib/api/design"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; // --- Types --- type ChangeType = "설계오류" | "원가절감" | "고객요청" | "공정개선" | "법규대응"; @@ -363,12 +363,8 @@ export default function DesignChangeManagementPage() { 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); @@ -386,13 +382,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); @@ -419,39 +408,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(() => { @@ -480,23 +496,10 @@ 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 --- @@ -505,11 +508,9 @@ export default function DesignChangeManagementPage() { if (targetId.startsWith("ECN")) { setCurrentTab("ecn"); setSelectedId(targetId); - setSearchStatus("all"); } else if (targetId.startsWith("ECR")) { setCurrentTab("ecr"); setSelectedId(targetId); - setSearchStatus("all"); } }; @@ -781,7 +782,6 @@ export default function DesignChangeManagementPage() { 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); @@ -796,95 +796,36 @@ export default function DesignChangeManagementPage() { )} - {/* 검색 필터 */} -
-
-
- -
- setSearchDateFrom(e.target.value)} - /> - ~ - setSearchDateTo(e.target.value)} - /> -
-
- -
- - -
- -
- - -
- - {currentTab === "ecr" && ( -
- - -
- )} - -
- - setSearchKeyword(e.target.value)} - /> -
- -
- -
- -
+ {/* 탭 선택 + 검색 필터 */} +
+
+
+ {currentTab === "ecr" ? ( + + ) : ( + + )}
{/* 현황 카드 */} @@ -926,9 +867,9 @@ export default function DesignChangeManagementPage() {
{currentTab === "ecr" ? ( - - - No + + + No {tsEcr.visibleColumns.map((col) => ( ) : (
- - - No + + + No {tsEcn.visibleColumns.map((col) => ( diff --git a/frontend/app/(main)/COMPANY_16/design/design-request/page.tsx b/frontend/app/(main)/COMPANY_16/design/design-request/page.tsx index e16ebfb3..fdb63357 100644 --- a/frontend/app/(main)/COMPANY_16/design/design-request/page.tsx +++ b/frontend/app/(main)/COMPANY_16/design/design-request/page.tsx @@ -2,8 +2,6 @@ import React, { useState, useMemo, useCallback, useEffect } from "react"; import { - Search, - RotateCcw, Plus, Pencil, Trash2, @@ -54,6 +52,7 @@ import { } from "@/lib/api/design"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; // ========== 타입 ========== interface HistoryItem { @@ -187,10 +186,8 @@ export default function DesignRequestPage() { const [saving, setSaving] = useState(false); const [detailOpen, setDetailOpen] = useState(false); - const [filterStatus, setFilterStatus] = useState(""); - const [filterType, setFilterType] = useState(""); - const [filterPriority, setFilterPriority] = useState(""); - const [filterKeyword, setFilterKeyword] = useState(""); + // 검색 필터 (DynamicSearchFilter) + const [searchFilters, setSearchFilters] = useState([]); const [modalOpen, setModalOpen] = useState(false); const [isEditMode, setIsEditMode] = useState(false); @@ -204,13 +201,6 @@ export default function DesignRequestPage() { setLoading(true); try { const params: Record = { source_type: "dr" }; - if (filterStatus && filterStatus !== "__all__") params.status = filterStatus; - if (filterType && filterType !== "__all__") { - // design_type은 서버에서 직접 필터링하지 않으므로 클라이언트에서 처리 - } - if (filterPriority && filterPriority !== "__all__") params.priority = filterPriority; - if (filterKeyword) params.search = filterKeyword; - const res = await getDesignRequestList(params); if (res.success && res.data) { setRequests(res.data); @@ -222,20 +212,35 @@ export default function DesignRequestPage() { } finally { setLoading(false); } - }, [filterStatus, filterPriority, filterKeyword]); + }, []); useEffect(() => { fetchRequests(); }, [fetchRequests]); - // 클라이언트 사이드 필터링 (design_type은 서버에서 지원하지 않으므로) + // 클라이언트 사이드 필터링 (DynamicSearchFilter) const filteredRequests = useMemo(() => { - let list = requests; - if (filterType && filterType !== "__all__") { - list = list.filter((item) => item.design_type === filterType); - } - return list; - }, [requests, filterType]); + if (searchFilters.length === 0) return requests; + return requests.filter((item) => { + for (const f of searchFilters) { + const val = item[f.columnName as keyof DesignRequest]; + const strVal = val !== undefined && val !== null ? (Array.isArray(val) ? val.join(",") : String(val)) : ""; + if (f.operator === "contains") { + if (!strVal.toLowerCase().includes(f.value.toLowerCase())) return false; + } else if (f.operator === "equals") { + if (strVal !== f.value) return false; + } else if (f.operator === "in") { + const allowed = f.value.split("|"); + if (!allowed.includes(strVal)) return false; + } else if (f.operator === "between") { + const [from, to] = f.value.split("|"); + if (from && strVal < from) return false; + if (to && strVal > to) return false; + } + } + return true; + }); + }, [requests, searchFilters]); const selectedItem = useMemo(() => { if (!selectedId) return null; @@ -250,12 +255,6 @@ export default function DesignRequestPage() { }; }, [requests]); - const handleResetFilter = useCallback(() => { - setFilterStatus(""); - setFilterType(""); - setFilterPriority(""); - setFilterKeyword(""); - }, []); // 채번: 기존 데이터 기반으로 다음 번호 생성 const generateNextNo = useCallback(() => { @@ -410,85 +409,30 @@ export default function DesignRequestPage() { return (
{/* 검색 필터 */} -
-
-
- - -
-
- - -
-
- - -
-
- - setFilterKeyword(e.target.value)} - placeholder="의뢰번호 / 설비명 / 고객명" - className="w-[240px] h-9" - /> -
-
-
- -
-
+
+
{/* 현황 카드 */}
- -
+
설계진행
{statusCounts.설계진행}
- -
+
출도완료
{statusCounts.출도완료}
- +
{/* 액션 바 */} @@ -524,8 +468,8 @@ export default function DesignRequestPage() {
) : (
- - + + {ts.visibleColumns.map((col) => ( diff --git a/frontend/app/(main)/COMPANY_16/design/my-work/page.tsx b/frontend/app/(main)/COMPANY_16/design/my-work/page.tsx index 471451b5..e0092b21 100644 --- a/frontend/app/(main)/COMPANY_16/design/my-work/page.tsx +++ b/frontend/app/(main)/COMPANY_16/design/my-work/page.tsx @@ -1245,14 +1245,14 @@ export default function MyWorkPage() { {viewMode === "list" && (
- - - 프로젝트 - 업무명 - 유형 - 상태 - 종료일 - 진행률 + + + 프로젝트 + 업무명 + 유형 + 상태 + 종료일 + 진행률 @@ -1327,9 +1327,9 @@ export default function MyWorkPage() {
- - - 프로젝트/업무 + + + 프로젝트/업무 {timesheetData.wds.map((d, i) => ( {d.getMonth() + 1}/{d.getDate()} ))} - 합계 + 합계 diff --git a/frontend/app/(main)/COMPANY_16/design/project/page.tsx b/frontend/app/(main)/COMPANY_16/design/project/page.tsx index 4785c6ae..89d06ae8 100644 --- a/frontend/app/(main)/COMPANY_16/design/project/page.tsx +++ b/frontend/app/(main)/COMPANY_16/design/project/page.tsx @@ -28,7 +28,6 @@ import { } from "@/components/ui/resizable"; import { Plus, - RotateCcw, Save, Pencil, Trash2, @@ -63,6 +62,7 @@ import { } from "@/lib/api/design"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; // --- Types --- type ProjectStatus = "진행중" | "계획" | "보류" | "완료"; @@ -290,10 +290,8 @@ export default function DesignProjectPage() { const [selectedId, setSelectedId] = useState(null); const [expandedIds, setExpandedIds] = useState>({}); - // 검색 - const [searchStatus, setSearchStatus] = useState("all"); - const [searchPM, setSearchPM] = useState("all"); - const [searchKeyword, setSearchKeyword] = useState(""); + // 검색 필터 (DynamicSearchFilter) + const [searchFilters, setSearchFilters] = useState([]); // 상세 탭 const [detailTab, setDetailTab] = useState("wbs"); @@ -365,18 +363,40 @@ export default function DesignProjectPage() { } }, []); + // snake_case → camelCase 매핑 (DynamicSearchFilter의 columnName은 snake_case) + const fieldMap: Record = { + project_no: "projectNo", + start_date: "startDate", + end_date: "endDate", + source_no: "sourceNo", + }; + const getFieldValue = (obj: any, colName: string): string => { + const key = fieldMap[colName] || colName; + const val = obj[key]; + return val !== undefined && val !== null ? String(val) : ""; + }; + // 필터링 const filteredProjects = useMemo(() => { - if (searchStatus === "all" && searchPM === "all" && !searchKeyword) return projects; + if (searchFilters.length === 0) return projects; const matched = new Set(); projects.forEach((p) => { let pass = true; - if (searchStatus !== "all" && p.status !== searchStatus) pass = false; - if (searchPM !== "all" && p.pm !== searchPM) pass = false; - if (searchKeyword) { - const str = [p.projectNo, p.name, p.customer, p.pm, p.sourceNo].join(" ").toLowerCase(); - if (!str.includes(searchKeyword.toLowerCase())) pass = false; + for (const f of searchFilters) { + const val = getFieldValue(p, f.columnName); + if (f.operator === "contains") { + if (!val.toLowerCase().includes(f.value.toLowerCase())) { pass = false; break; } + } else if (f.operator === "equals") { + if (val !== f.value) { pass = false; break; } + } else if (f.operator === "in") { + const allowed = f.value.split("|"); + if (!allowed.includes(val)) { pass = false; break; } + } else if (f.operator === "between") { + const [from, to] = f.value.split("|"); + if (from && val < from) { pass = false; break; } + if (to && val > to) { pass = false; break; } + } } if (pass) matched.add(p.id); }); @@ -394,7 +414,7 @@ export default function DesignProjectPage() { }); return projects.filter((p) => result.has(p.id)); - }, [projects, searchStatus, searchPM, searchKeyword]); + }, [projects, searchFilters]); const selectedProject = useMemo( () => projects.find((p) => p.id === selectedId), @@ -426,11 +446,6 @@ export default function DesignProjectPage() { })); }; - const handleResetSearch = () => { - setSearchStatus("all"); - setSearchPM("all"); - setSearchKeyword(""); - }; // --- 프로젝트 모달 --- const openProjectModal = (editProject?: Project, presetParentId?: string) => { @@ -683,46 +698,14 @@ export default function DesignProjectPage() { return (
{/* 검색 필터 */} -
-
- 상태 - -
-
- PM - -
-
- 프로젝트 / 고객 검색 - setSearchKeyword(e.target.value)} - /> -
-
- +
+
{/* 좌우 분할 메인 */} @@ -746,8 +729,8 @@ export default function DesignProjectPage() {
- - + + {ts.visibleColumns.map((col) => ( ) : (
- - + + 업무명 담당자 시작일 @@ -1465,6 +1448,7 @@ export default function DesignProjectPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/design/task-management/page.tsx b/frontend/app/(main)/COMPANY_16/design/task-management/page.tsx index 3d4aef55..9dbd1164 100644 --- a/frontend/app/(main)/COMPANY_16/design/task-management/page.tsx +++ b/frontend/app/(main)/COMPANY_16/design/task-management/page.tsx @@ -3,13 +3,6 @@ import React, { useState, useMemo, useCallback, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { Table, TableBody, @@ -47,8 +40,6 @@ import { CommandList, } from "@/components/ui/command"; import { - Search, - RotateCcw, RefreshCw, ClipboardList, Pencil, @@ -78,6 +69,7 @@ import { getUserList } from "@/lib/api/user"; import { useAuth } from "@/hooks/useAuth"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; // --- Types --- type SourceType = "dr" | "ecr"; @@ -287,11 +279,8 @@ export default function DesignTaskManagementPage() { fetchEmployees(); }, [fetchTasks, fetchEmployees]); - // 검색 필터 - const [searchStatus, setSearchStatus] = useState("all"); - const [searchPriority, setSearchPriority] = useState("all"); - const [searchReqDept, setSearchReqDept] = useState("all"); - const [searchKeyword, setSearchKeyword] = useState(""); + // 검색 필터 (DynamicSearchFilter) + const [searchFilters, setSearchFilters] = useState([]); // 담당자 선택 모달 상태 const [designerModalOpen, setDesignerModalOpen] = useState(false); @@ -351,23 +340,51 @@ export default function DesignTaskManagementPage() { }; }, [myRelatedTasks]); + // snake_case → camelCase 매핑 (DynamicSearchFilter의 columnName은 snake_case) + const taskFieldMap: Record = { + source_type: "sourceType", + request_no: "id", + target_name: "targetName", + req_dept: "reqDept", + request_date: "date", + due_date: "dueDate", + approval_step: "approvalStep", + design_type: "designType", + drawing_no: "drawingNo", + change_type: "changeType", + review_memo: "reviewMemo", + project_id: "projectNo", + }; + const getTaskFieldValue = (obj: any, colName: string): string => { + const key = taskFieldMap[colName] || colName; + const val = obj[key]; + if (Array.isArray(val)) return val.join(","); + return val !== undefined && val !== null ? String(val) : ""; + }; + // 필터링된 데이터 const filteredData = useMemo(() => { return myRelatedTasks.filter((item) => { if (currentTab === "dr" && item.sourceType !== "dr") return false; if (currentTab === "ecr" && item.sourceType !== "ecr") return false; - if (searchStatus !== "all" && item.status !== searchStatus) return false; - if (searchPriority !== "all" && item.priority !== searchPriority) return false; - if (searchReqDept !== "all" && item.reqDept !== searchReqDept) return false; - if (searchKeyword) { - const str = [item.id, item.targetName, item.customer, item.requester, item.designer, item.reqDept] - .join(" ") - .toLowerCase(); - if (!str.includes(searchKeyword.toLowerCase())) return false; + for (const f of searchFilters) { + const val = getTaskFieldValue(item, f.columnName); + 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; }); - }, [myRelatedTasks, currentTab, searchStatus, searchPriority, searchReqDept, searchKeyword]); + }, [myRelatedTasks, currentTab, searchFilters]); // 현황 통계 const stats = useMemo(() => { @@ -607,15 +624,8 @@ export default function DesignTaskManagementPage() { fetchTasks(); }, [selectedTaskId, reviewMemoText, fetchTasks]); - const handleResetSearch = useCallback(() => { - setSearchStatus("all"); - setSearchPriority("all"); - setSearchReqDept("all"); - setSearchKeyword(""); - }, []); - - const handleFilterByStatus = useCallback((status: TaskStatus) => { - setSearchStatus(status); + const handleFilterByStatus = useCallback((_status: TaskStatus) => { + // Status filter now handled by DynamicSearchFilter }, []); // 납기 남은 일수 계산 @@ -706,62 +716,13 @@ export default function DesignTaskManagementPage() { {/* 검색 필터 */}
-
-
- - - -
- - setSearchKeyword(e.target.value)} - placeholder="접수번호 / 설비명 / 품목명 / 고객명 검색" - className="h-8 pl-8 text-xs" - /> -
-
- -
+
{/* 좌우 분할 패널 */} @@ -790,8 +751,8 @@ export default function DesignTaskManagementPage() {
- - + + {ts.visibleColumns.map((col) => ( diff --git a/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx index 2625a2c5..88034b88 100644 --- a/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx @@ -20,7 +20,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, - Inbox, ClipboardCheck, Package, Copy, Info, Search, RotateCcw, Settings2, + Inbox, ClipboardCheck, Package, Copy, Info, Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; @@ -33,6 +33,7 @@ import { MultiTableExcelUploadModal } from "@/components/common/MultiTableExcelU import { ImageUpload } from "@/components/common/ImageUpload"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; const EQUIP_TABLE = "equipment_mng"; const INSPECTION_TABLE = "equipment_inspection_item"; @@ -54,8 +55,7 @@ export default function EquipmentInfoPage() { const [equipments, setEquipments] = useState([]); const [equipLoading, setEquipLoading] = useState(false); const [equipCount, setEquipCount] = useState(0); - const [inputKeyword, setInputKeyword] = useState(""); - const [searchKeyword, setSearchKeyword] = useState(""); + const [searchFilters, setSearchFilters] = useState([]); const [selectedEquipId, setSelectedEquipId] = useState(null); // 우측 탭 @@ -142,10 +142,7 @@ export default function EquipmentInfoPage() { const fetchEquipments = useCallback(async () => { setEquipLoading(true); try { - const filters: any[] = []; - if (searchKeyword) { - filters.push({ columnName: "equipment_name", operator: "contains", value: searchKeyword }); - } + const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); const res = await apiClient.post(`/table-management/tables/${EQUIP_TABLE}/data`, { page: 1, size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, @@ -159,7 +156,7 @@ export default function EquipmentInfoPage() { }))); setEquipCount(res.data?.data?.total || raw.length); } catch { toast.error("설비 목록 조회 실패"); } finally { setEquipLoading(false); } - }, [searchKeyword, catOptions]); + }, [searchFilters, catOptions]); useEffect(() => { fetchEquipments(); }, [fetchEquipments]); @@ -377,8 +374,6 @@ export default function EquipmentInfoPage() { ); - const handleSearch = () => setSearchKeyword(inputKeyword); - return (
{/* 브레드크럼 */} @@ -389,38 +384,31 @@ export default function EquipmentInfoPage() {
{/* 검색 바 */} -
- 설비명 - setInputKeyword(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSearch()} - /> - - -
- - -
-
+ + + + + } + />
@@ -457,12 +445,12 @@ export default function EquipmentInfoPage() {
- {ts.isVisible("equipment_code") && 설비코드} - {ts.isVisible("equipment_name") && 설비명} - {ts.isVisible("equipment_type") && 설비유형} - {ts.isVisible("manufacturer") && 제조사} - {ts.isVisible("installation_location") && 설치장소} - {ts.isVisible("operation_status") && 가동상태} + {ts.isVisible("equipment_code") && 설비코드} + {ts.isVisible("equipment_name") && 설비명} + {ts.isVisible("equipment_type") && 설비유형} + {ts.isVisible("manufacturer") && 제조사} + {ts.isVisible("installation_location") && 설치장소} + {ts.isVisible("operation_status") && 가동상태} @@ -599,13 +587,13 @@ export default function EquipmentInfoPage() {
- 점검항목 - 점검주기 - 점검방법 - 하한치 - 상한치 - 단위 - 점검내용 + 점검항목 + 점검주기 + 점검방법 + 하한치 + 상한치 + 단위 + 점검내용 @@ -639,11 +627,11 @@ export default function EquipmentInfoPage() {
- 소모품명 - 교체주기 - 단위 - 규격 - 제조사 + 소모품명 + 교체주기 + 단위 + 규격 + 제조사 @@ -828,9 +816,9 @@ export default function EquipmentInfoPage() { 0 && copyChecked.size === copyItems.length} onChange={(e) => { if (e.target.checked) setCopyChecked(new Set(copyItems.map((i) => i.id))); else setCopyChecked(new Set()); }} /> - 점검항목점검주기 - 점검방법하한 - 상한단위 + 점검항목점검주기 + 점검방법하한 + 상한단위 @@ -878,6 +866,7 @@ export default function EquipmentInfoPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/equipment/plc-settings/page.tsx b/frontend/app/(main)/COMPANY_16/equipment/plc-settings/page.tsx index c2eac9e3..36fcf00a 100644 --- a/frontend/app/(main)/COMPANY_16/equipment/plc-settings/page.tsx +++ b/frontend/app/(main)/COMPANY_16/equipment/plc-settings/page.tsx @@ -286,8 +286,8 @@ export default function PlcSettingsPage() {
- - + + 0 && dtChecked.length === datatypes.length} @@ -295,7 +295,7 @@ export default function PlcSettingsPage() { /> {ts.visibleColumns.map((col) => ( - {col.label} + {col.label} ))} @@ -361,21 +361,21 @@ export default function PlcSettingsPage() {
- - + + 0 && cfgChecked.length === configs.length} onCheckedChange={(v) => setCfgChecked(v ? configs.map(r => r.id) : [])} /> - 설정명 - 소스연결ID - 소스테이블 - 대상테이블 - 수집유형 - 스케줄(Cron) - 사용여부 + 설정명 + 소스연결ID + 소스테이블 + 대상테이블 + 수집유형 + 스케줄(Cron) + 사용여부 @@ -539,6 +539,7 @@ export default function PlcSettingsPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx index 247a5d30..e291f00e 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx @@ -12,10 +12,8 @@ import { Download, Pencil, RefreshCw, - Search, Inbox, Loader2, - RotateCcw, Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -54,6 +52,7 @@ import { toast } from "sonner"; import { exportToExcel } from "@/lib/utils/excelExport"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; // ========== 타입 & 상수 ========== type TabKey = "carrier" | "cost" | "contract" | "route" | "vehicle"; @@ -247,9 +246,8 @@ export default function LogisticsInfoPage() { // 탭 상태 const [activeTab, setActiveTab] = useState("carrier"); - // 검색 키워드 (탭 전환 시 초기화) - const [searchInput, setSearchInput] = useState(""); - const [keyword, setKeyword] = useState(""); + // 검색 필터 + const [searchFilters, setSearchFilters] = useState([]); // 탭별 독립 상태 const [tabData, setTabData] = useState>({ @@ -293,15 +291,21 @@ export default function LogisticsInfoPage() { // 컬럼 가시성 헬퍼 const getVisibleColumns = (tabKey: TabKey) => tsMap[tabKey].visibleColumns; - // 클라이언트 사이드 키워드 필터링 + // 클라이언트 사이드 필터링 const filteredData = useMemo(() => { const data = tabData[activeTab]; - if (!keyword.trim()) return data; - const kw = keyword.toLowerCase(); + if (searchFilters.length === 0) return data; return data.filter((row) => - Object.values(row).some((v) => String(v ?? "").toLowerCase().includes(kw)) + searchFilters.every((f) => { + if (!f.value) return true; + const kw = f.value.toLowerCase(); + if (f.columnName) { + return String(row[f.columnName] ?? "").toLowerCase().includes(kw); + } + return Object.values(row).some((v) => String(v ?? "").toLowerCase().includes(kw)); + }) ); - }, [tabData, activeTab, keyword]); + }, [tabData, activeTab, searchFilters]); // FK 참조 데이터 로드 const loadReferences = useCallback(async () => { @@ -393,22 +397,10 @@ export default function LogisticsInfoPage() { fetchTabData(activeTab); }, [activeTab, fetchTabData]); - // 검색 실행 - const handleSearch = useCallback(() => { - setKeyword(searchInput); - }, [searchInput]); - - // 검색 초기화 - const handleReset = useCallback(() => { - setSearchInput(""); - setKeyword(""); - }, []); - // 탭 변경 const handleTabChange = useCallback((tab: string) => { setActiveTab(tab as TabKey); - setSearchInput(""); - setKeyword(""); + setSearchFilters([]); }, []); // 등록 모달 열기 @@ -637,35 +629,13 @@ export default function LogisticsInfoPage() { return (
{/* 검색 필터 바 */} -
-
- - 검색 - - setSearchInput(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSearch()} - placeholder={`${activeConfig.label} 검색...`} - className="h-9 w-[260px] text-sm" - /> -
- - -
- - {keyword.trim() && filteredData.length !== tabData[activeTab].length - ? `${filteredData.length} / ${tabData[activeTab].length}건` - : `${tabData[activeTab].length}건`} - -
-
+ {/* 탭 + 콘텐츠 영역 */} ) : (
- - + + diff --git a/frontend/app/(main)/COMPANY_16/logistics/inventory/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/inventory/page.tsx index 85e5010e..940dc89d 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/inventory/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/inventory/page.tsx @@ -9,7 +9,7 @@ * ★ 재고 직접 등록/삭제 불가 — 입출고를 통해서만 변동, 조정만 가능 */ -import React, { useState, useEffect, useCallback, useMemo } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; @@ -51,8 +51,6 @@ import { History, AlertTriangle, RefreshCw, - Search, - RotateCcw, Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -60,6 +58,7 @@ import { apiClient } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; import { toast } from "sonner"; import { exportToExcel } from "@/lib/utils/excelExport"; @@ -118,9 +117,8 @@ export default function InventoryStatusPage() { const [stockLoading, setStockLoading] = useState(false); const [selectedStockId, setSelectedStockId] = useState(null); - // 검색 상태 - const [searchKeyword, setSearchKeyword] = useState(""); - const [searchStatus, setSearchStatus] = useState("all"); + // 검색 필터 + const [searchFilters, setSearchFilters] = useState([]); // 우측: 이동 이력 const [historyItems, setHistoryItems] = useState([]); @@ -171,11 +169,13 @@ export default function InventoryStatusPage() { const fetchStock = useCallback(async () => { setStockLoading(true); try { + const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); const res = await apiClient.post( `/table-management/tables/${STOCK_TABLE}/data`, { page: 1, size: 500, + dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, sort: { columnName: "item_number", order: "asc" }, } @@ -197,28 +197,12 @@ export default function InventoryStatusPage() { } finally { setStockLoading(false); } - }, [categoryOptions]); + }, [categoryOptions, searchFilters]); useEffect(() => { fetchStock(); }, [fetchStock]); - // 클라이언트 사이드 필터링 - const filteredStock = useMemo(() => { - return stockItems.filter((item) => { - const kw = searchKeyword.trim().toLowerCase(); - if ( - kw && - !item.item_number?.toLowerCase().includes(kw) && - !item.item_name?.toLowerCase().includes(kw) && - !item.warehouse_name?.toLowerCase().includes(kw) - ) - return false; - if (searchStatus !== "all" && item.status !== searchStatus) return false; - return true; - }); - }, [stockItems, searchKeyword, searchStatus]); - // 선택된 재고 const selectedStock = stockItems.find((s) => s.id === selectedStockId); @@ -321,12 +305,12 @@ export default function InventoryStatusPage() { // 엑셀 내보내기 const handleExcelExport = () => { - if (filteredStock.length === 0) { + if (stockItems.length === 0) { toast.error("내보낼 데이터가 없어요"); return; } exportToExcel( - filteredStock.map((r) => ({ + stockItems.map((r) => ({ 품목코드: r.item_number, 품명: r.item_name, 창고: r.warehouse_name, @@ -340,63 +324,32 @@ export default function InventoryStatusPage() { ); }; - const statusOptions = categoryOptions["status"] || []; - return (
{/* 검색 바 */} -
-
- - setSearchKeyword(e.target.value)} - /> -
- - -
- - {filteredStock.length}건 - - - -
-
+ + + +
+ } + /> {/* 마스터-디테일 패널 */} 재고 목록 - {filteredStock.length}건 + {stockItems.length}건 @@ -420,15 +373,15 @@ export default function InventoryStatusPage() {
- ) : filteredStock.length === 0 ? ( + ) : stockItems.length === 0 ? (
등록된 재고가 없어요
) : (
- - - # + + + # {ts.visibleColumns.map((col) => ( - {filteredStock.map((item, idx) => ( + {stockItems.map((item, idx) => ( ) : (
- - - # - 일자 - 유형 - 변동수량 - 이후수량 - 참조번호 - 사유 - 처리자 + + + # + 일자 + 유형 + 변동수량 + 이후수량 + 참조번호 + 사유 + 처리자 @@ -696,6 +649,7 @@ export default function InventoryStatusPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/logistics/material-status/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/material-status/page.tsx index ef6859ff..46de306c 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/material-status/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/material-status/page.tsx @@ -600,6 +600,7 @@ export default function MaterialStatusPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx index 4ecdf414..df51c4d6 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx @@ -31,7 +31,6 @@ import { Search, Plus, Trash2, - RotateCcw, Loader2, PackageOpen, X, @@ -46,6 +45,7 @@ import { import { cn } from "@/lib/utils"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; // API: /outbound/* import { getOutboundList, @@ -132,11 +132,7 @@ export default function OutboundPage() { const [checkedIds, setCheckedIds] = useState([]); // 검색 필터 - const [searchType, setSearchType] = useState("all"); - const [searchStatus, setSearchStatus] = useState("all"); - const [searchKeyword, setSearchKeyword] = useState(""); - const [searchDateFrom, setSearchDateFrom] = useState(""); - const [searchDateTo, setSearchDateTo] = useState(""); + const [searchFilters, setSearchFilters] = useState([]); // 등록 모달 const [isModalOpen, setIsModalOpen] = useState(false); @@ -162,33 +158,31 @@ export default function OutboundPage() { const [sourcePage, setSourcePage] = useState(1); const [sourcePageSize, setSourcePageSize] = useState(20); - // 날짜 초기화 - useEffect(() => { - const today = new Date(); - const threeMonthsAgo = new Date(today); - threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); - setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); - setSearchDateTo(today.toISOString().split("T")[0]); - }, []); - // 목록 조회 const fetchList = useCallback(async () => { setLoading(true); try { - const res = await getOutboundList({ - outbound_type: searchType !== "all" ? searchType : undefined, - outbound_status: searchStatus !== "all" ? searchStatus : undefined, - search_keyword: searchKeyword || undefined, - date_from: searchDateFrom || undefined, - date_to: searchDateTo || undefined, - }); + const params: Record = {}; + for (const f of searchFilters) { + if (!f.value) continue; + if (f.columnName === "outbound_type") params.outbound_type = f.value; + else if (f.columnName === "outbound_status") params.outbound_status = f.value; + else if (f.columnName === "outbound_date" && f.operator === "between") { + const [from, to] = f.value.split("~").map((s) => s.trim()); + if (from) params.date_from = from; + if (to) params.date_to = to; + } else { + params.search_keyword = f.value; + } + } + const res = await getOutboundList(params); if (res.success) setData(res.data); } catch { // ignore } finally { setLoading(false); } - }, [searchType, searchStatus, searchKeyword, searchDateFrom, searchDateTo]); + }, [searchFilters]); useEffect(() => { fetchList(); @@ -206,18 +200,6 @@ export default function OutboundPage() { })(); }, []); - // 검색 초기화 - const handleReset = () => { - setSearchType("all"); - setSearchStatus("all"); - setSearchKeyword(""); - const today = new Date(); - const threeMonthsAgo = new Date(today); - threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); - setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); - setSearchDateTo(today.toISOString().split("T")[0]); - }; - // 체크박스 const allChecked = data.length > 0 && checkedIds.length === data.length; const toggleCheckAll = () => { @@ -513,82 +495,13 @@ export default function OutboundPage() { return (
{/* 검색 영역 */} -
-
- 출고유형 - -
- -
- 출고상태 - -
- -
- 검색어 - setSearchKeyword(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && fetchList()} - className="h-9 text-xs" - /> -
- -
- 기간 -
- setSearchDateFrom(e.target.value)} - className="h-9 flex-1 text-xs" - /> - ~ - setSearchDateTo(e.target.value)} - className="h-9 flex-1 text-xs" - /> -
-
- -
- - -
-
+ {/* 출고 목록 테이블 */}
@@ -624,8 +537,8 @@ export default function OutboundPage() {
- - + + ) : (
- - - No - 품목명 - 참조번호 - 수량 - 단가 - 금액 + + + No + 품목명 + 참조번호 + 수량 + 단가 + 금액 @@ -1098,6 +1011,7 @@ export default function OutboundPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> @@ -1126,15 +1040,15 @@ function SourceShipmentInstructionTable({ return (
- - + + - 출하지시번호 - 출하일 - 품목 - 계획수량 - 출고수량 - 미출고 + 출하지시번호 + 출하일 + 품목 + 계획수량 + 출고수량 + 미출고 @@ -1208,14 +1122,14 @@ function SourcePurchaseOrderTable({ return (
- - + + - 발주번호 - 공급처 - 품목 - 발주수량 - 입고수량 + 발주번호 + 공급처 + 품목 + 발주수량 + 입고수량 @@ -1282,14 +1196,14 @@ function SourceItemTable({ return (
- - + + - 품목 - 규격 - 재질 - 단위 - 기준가 + 품목 + 규격 + 재질 + 단위 + 기준가 diff --git a/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx index e9aaf231..f97b8f1f 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx @@ -15,7 +15,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Command, CommandInput, CommandList, CommandEmpty, CommandItem } from "@/components/ui/command"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { - Search, Plus, Trash2, RotateCcw, Loader2, Package, Box, X, Save, Edit2, Download, ChevronsUpDown, Check, Inbox, Settings2, + Plus, Trash2, Loader2, Package, Box, X, Save, Edit2, Download, ChevronsUpDown, Check, Inbox, Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; @@ -29,6 +29,7 @@ import { } from "@/lib/api/packaging"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; const GRID_COLUMNS = [ { key: "pkg_code", label: "품목코드" }, @@ -72,8 +73,8 @@ export default function PackagingPage() { const { confirm, ConfirmDialogComponent } = useConfirmDialog(); const [activeTab, setActiveTab] = useState<"packing" | "loading">("packing"); - // 검색 - const [searchKeyword, setSearchKeyword] = useState(""); + // 검색 필터 + const [searchFilters, setSearchFilters] = useState([]); // 포장재 데이터 const [pkgUnits, setPkgUnits] = useState([]); @@ -157,15 +158,21 @@ export default function PackagingPage() { // 검색 필터 적용 const filteredPkgUnits = pkgUnits.filter((p) => { - if (!searchKeyword) return true; - const kw = searchKeyword.toLowerCase(); - return (p.pkg_code?.toLowerCase().includes(kw) || p.pkg_name?.toLowerCase().includes(kw)); + if (searchFilters.length === 0) return true; + return searchFilters.every((f) => { + if (!f.value) return true; + const kw = f.value.toLowerCase(); + return Object.values(p).some((v) => String(v ?? "").toLowerCase().includes(kw)); + }); }); const filteredLoadingUnits = loadingUnits.filter((l) => { - if (!searchKeyword) return true; - const kw = searchKeyword.toLowerCase(); - return (l.loading_code?.toLowerCase().includes(kw) || l.loading_name?.toLowerCase().includes(kw)); + if (searchFilters.length === 0) return true; + return searchFilters.every((f) => { + if (!f.value) return true; + const kw = f.value.toLowerCase(); + return Object.values(l).some((v) => String(v ?? "").toLowerCase().includes(kw)); + }); }); // --- 포장재 등록/수정 모달 --- @@ -354,25 +361,13 @@ export default function PackagingPage() {
{/* 1. 필터 바 */} -
-
- 검색 - setSearchKeyword(e.target.value)} - className="h-8 w-[260px] text-xs" - /> - {searchKeyword && ( - - )} -
- -
+ {/* 2. 탭 바 */}
@@ -464,14 +459,14 @@ export default function PackagingPage() { {/* 포장재 목록 테이블 */}
- - - {ts.isVisible("pkg_code") && 품목코드} - {ts.isVisible("pkg_name") && 포장명} - {ts.isVisible("pkg_type") && 유형} - {ts.isVisible("size") && 크기(mm)} - {ts.isVisible("max_weight") && 최대중량} - {ts.isVisible("status") && 상태} + + + {ts.isVisible("pkg_code") && 품목코드} + {ts.isVisible("pkg_name") && 포장명} + {ts.isVisible("pkg_type") && 유형} + {ts.isVisible("size") && 크기(mm)} + {ts.isVisible("max_weight") && 최대중량} + {ts.isVisible("status") && 상태} @@ -554,13 +549,13 @@ export default function PackagingPage() { ) : (
- - - 품목코드 - 품목명 - 규격 - 단위 - 포장수량 + + + 품목코드 + 품목명 + 규격 + 단위 + 포장수량 @@ -592,14 +587,14 @@ export default function PackagingPage() { {/* 적재함 목록 테이블 */}
- - - 품목코드 - 적재함명 - 유형 - 크기(mm) - 최대적재 - 상태 + + + 품목코드 + 적재함명 + 유형 + 크기(mm) + 최대적재 + 상태 @@ -682,13 +677,13 @@ export default function PackagingPage() { ) : (
- - - 포장코드 - 포장명 - 유형 - 최대수량 - 적재방향 + + + 포장코드 + 포장명 + 유형 + 최대수량 + 적재방향 @@ -906,14 +901,14 @@ export default function PackagingPage() { />
- - + + - 품목코드 - 품목명 - 규격 - 재질 - 단위 + 품목코드 + 품목명 + 규격 + 재질 + 단위 @@ -979,14 +974,14 @@ export default function PackagingPage() { />
- - + + - 포장코드 - 포장명 - 유형 - 크기(mm) - 최대중량 + 포장코드 + 포장명 + 유형 + 크기(mm) + 최대중량 @@ -1053,6 +1048,7 @@ export default function PackagingPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx index a902aaff..620b1feb 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx @@ -38,7 +38,6 @@ import { Search, Plus, Trash2, - RotateCcw, Loader2, Inbox, X, @@ -53,6 +52,7 @@ import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; // API: /receiving/* import { getReceivingList, @@ -153,11 +153,7 @@ export default function ReceivingPage() { const [checkedIds, setCheckedIds] = useState([]); // 검색 필터 - const [searchType, setSearchType] = useState("all"); - const [searchStatus, setSearchStatus] = useState("all"); - const [searchKeyword, setSearchKeyword] = useState(""); - const [searchDateFrom, setSearchDateFrom] = useState(""); - const [searchDateTo, setSearchDateTo] = useState(""); + const [searchFilters, setSearchFilters] = useState([]); // 등록 모달 const [isModalOpen, setIsModalOpen] = useState(false); @@ -188,14 +184,8 @@ export default function ReceivingPage() { // 구매관리 division 코드 (라벨 기준 조회) const [purchaseDivisionCode, setPurchaseDivisionCode] = useState(""); - // 날짜 초기화 + 구매관리 division 코드 로드 + // 구매관리 division 코드 로드 useEffect(() => { - const today = new Date(); - const threeMonthsAgo = new Date(today); - threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); - setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); - setSearchDateTo(today.toISOString().split("T")[0]); - // division 카테고리에서 "구매관리" 라벨의 코드 조회 apiClient.get("/table-categories/item_info/division/values").then((res) => { const vals = res.data?.data || []; @@ -208,20 +198,27 @@ export default function ReceivingPage() { const fetchList = useCallback(async () => { setLoading(true); try { - const res = await getReceivingList({ - inbound_type: searchType !== "all" ? searchType : undefined, - inbound_status: searchStatus !== "all" ? searchStatus : undefined, - search_keyword: searchKeyword || undefined, - date_from: searchDateFrom || undefined, - date_to: searchDateTo || undefined, - }); + const params: Record = {}; + for (const f of searchFilters) { + if (!f.value) continue; + if (f.columnName === "inbound_type") params.inbound_type = f.value; + else if (f.columnName === "inbound_status") params.inbound_status = f.value; + else if (f.columnName === "inbound_date" && f.operator === "between") { + const [from, to] = f.value.split("~").map((s) => s.trim()); + if (from) params.date_from = from; + if (to) params.date_to = to; + } else { + params.search_keyword = f.value; + } + } + const res = await getReceivingList(params); if (res.success) setData(res.data); } catch { // 에러 무시 } finally { setLoading(false); } - }, [searchType, searchStatus, searchKeyword, searchDateFrom, searchDateTo]); + }, [searchFilters]); useEffect(() => { fetchList(); @@ -239,18 +236,6 @@ export default function ReceivingPage() { })(); }, []); - // 검색 초기화 - const handleReset = () => { - setSearchType("all"); - setSearchStatus("all"); - setSearchKeyword(""); - const today = new Date(); - const threeMonthsAgo = new Date(today); - threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); - setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); - setSearchDateTo(today.toISOString().split("T")[0]); - }; - // 체크박스 const allChecked = data.length > 0 && checkedIds.length === data.length; const toggleCheckAll = () => { @@ -548,86 +533,32 @@ export default function ReceivingPage() { {/* 검색 영역 */} -
- - - - - setSearchKeyword(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && fetchList()} - className="h-9 w-[240px] text-xs" - /> - -
- setSearchDateFrom(e.target.value)} - className="h-9 w-[140px] text-xs" - /> - ~ - setSearchDateTo(e.target.value)} - className="h-9 w-[140px] text-xs" - /> -
- - - - -
- -
- -
-
+ + +
+ +
+ } + /> {/* 입고 목록 테이블 */}
@@ -645,29 +576,29 @@ export default function ReceivingPage() {
- - + + - {ts.isVisible("inbound_number") && 입고번호} - {ts.isVisible("inbound_type") && 입고유형} - {ts.isVisible("inbound_date") && 입고일} - {ts.isVisible("reference_number") && 참조번호} - {ts.isVisible("source_type") && 데이터출처} - {ts.isVisible("supplier_name") && 공급처} - {ts.isVisible("item_number") && 품목코드} - {ts.isVisible("item_name") && 품목명} - {ts.isVisible("spec") && 규격} - {ts.isVisible("inbound_qty") && 입고수량} - {ts.isVisible("unit_price") && 단가} - {ts.isVisible("total_amount") && 금액} - {ts.isVisible("warehouse_name") && 창고} - {ts.isVisible("inbound_status") && 입고상태} - {ts.isVisible("remark") && 비고} + {ts.isVisible("inbound_number") && 입고번호} + {ts.isVisible("inbound_type") && 입고유형} + {ts.isVisible("inbound_date") && 입고일} + {ts.isVisible("reference_number") && 참조번호} + {ts.isVisible("source_type") && 데이터출처} + {ts.isVisible("supplier_name") && 공급처} + {ts.isVisible("item_number") && 품목코드} + {ts.isVisible("item_name") && 품목명} + {ts.isVisible("spec") && 규격} + {ts.isVisible("inbound_qty") && 입고수량} + {ts.isVisible("unit_price") && 단가} + {ts.isVisible("total_amount") && 금액} + {ts.isVisible("warehouse_name") && 창고} + {ts.isVisible("inbound_status") && 입고상태} + {ts.isVisible("remark") && 비고} @@ -1022,11 +953,11 @@ export default function ReceivingPage() { ) : (
- - - No - 품목명 - 참조번호 + + + No + 품목명 + 참조번호 수량 @@ -1156,6 +1087,7 @@ export default function ReceivingPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> @@ -1184,15 +1116,15 @@ function SourcePurchaseOrderTable({ return (
- - + + - 발주번호 - 공급처 - 품목 - 발주수량 - 입고수량 - 미입고 + 발주번호 + 공급처 + 품목 + 발주수량 + 입고수량 + 미입고 @@ -1264,14 +1196,14 @@ function SourceShipmentTable({ return (
- - + + - 출하번호 - 출하일 - 거래처 - 품목 - 출하수량 + 출하번호 + 출하일 + 거래처 + 품목 + 출하수량 @@ -1342,14 +1274,14 @@ function SourceItemTable({ return (
- - + + - 품목 - 규격 - 재질 - 단위 - 기준가 + 품목 + 규격 + 재질 + 단위 + 기준가 diff --git a/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx index 26293d61..8e59c67b 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx @@ -9,7 +9,7 @@ * ★ 하위 위치가 있으면 창고 삭제 불가 */ -import React, { useState, useEffect, useCallback, useMemo } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; @@ -50,8 +50,6 @@ import { Download, MapPin, Building2, - Search, - RotateCcw, Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -61,6 +59,7 @@ import { toast } from "sonner"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; import { exportToExcel } from "@/lib/utils/excelExport"; const WAREHOUSE_TABLE = "warehouse_info"; @@ -116,10 +115,8 @@ export default function WarehouseManagementPage() { const [warehouseCount, setWarehouseCount] = useState(0); const [selectedWarehouseId, setSelectedWarehouseId] = useState(null); - // 검색 상태 - const [searchKeyword, setSearchKeyword] = useState(""); - const [searchWarehouseType, setSearchWarehouseType] = useState("all"); - const [searchStatus, setSearchStatus] = useState("all"); + // 검색 필터 + const [searchFilters, setSearchFilters] = useState([]); // 우측: 로케이션 목록 const [locations, setLocations] = useState([]); @@ -204,11 +201,13 @@ export default function WarehouseManagementPage() { const fetchWarehouses = useCallback(async () => { setWarehouseLoading(true); try { + const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); const res = await apiClient.post( `/table-management/tables/${WAREHOUSE_TABLE}/data`, { page: 1, size: 500, + dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, sort: { columnName: "warehouse_code", order: "asc" }, } @@ -226,28 +225,12 @@ export default function WarehouseManagementPage() { } finally { setWarehouseLoading(false); } - }, [categoryOptions, resolveCategory]); + }, [categoryOptions, resolveCategory, searchFilters]); useEffect(() => { fetchWarehouses(); }, [fetchWarehouses]); - // 클라이언트 사이드 필터링 - const filteredWarehouses = useMemo(() => { - return warehouses.filter((w) => { - const kw = searchKeyword.trim().toLowerCase(); - if ( - kw && - !w.warehouse_code?.toLowerCase().includes(kw) && - !w.warehouse_name?.toLowerCase().includes(kw) - ) - return false; - if (searchWarehouseType !== "all" && w.warehouse_type !== searchWarehouseType) return false; - if (searchStatus !== "all" && w.status !== searchStatus) return false; - return true; - }); - }, [warehouses, searchKeyword, searchWarehouseType, searchStatus]); - // 선택된 창고 const selectedWarehouse = warehouses.find((w) => w.id === selectedWarehouseId); @@ -496,80 +479,34 @@ export default function WarehouseManagementPage() { ); }; - const warehouseTypeOptions = categoryOptions["warehouse_type"] || []; - const statusOptions = categoryOptions["status"] || []; - return (
{ConfirmDialogComponent} {/* 검색 바 */} -
-
- - setSearchKeyword(e.target.value)} - /> -
- - - -
- - {filteredWarehouses.length}건 - - - -
-
+ + + +
+ } + /> {/* 마스터-디테일 패널 */} 창고 목록 - {filteredWarehouses.length}건 + {warehouses.length}건
@@ -613,22 +550,22 @@ export default function WarehouseManagementPage() {
- ) : filteredWarehouses.length === 0 ? ( + ) : warehouses.length === 0 ? (
등록된 창고가 없어요
) : (
- - - # + + + # {ts.visibleColumns.map((col) => ( - {col.label} + {col.label} ))} - {filteredWarehouses.map((w, idx) => ( + {warehouses.map((w, idx) => ( ) : (
- - + + - # - 위치코드 - 위치명 - - 구역 - - - 유형 - 상태 + # + 위치코드 + 위치명 + + 구역 + + + 유형 + 상태 @@ -1116,6 +1053,7 @@ export default function WarehouseManagementPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/master-data/company/page.tsx b/frontend/app/(main)/COMPANY_16/master-data/company/page.tsx index 9351c50c..6f48424b 100644 --- a/frontend/app/(main)/COMPANY_16/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_16/master-data/company/page.tsx @@ -697,14 +697,14 @@ export default function CompanyPage() { ) : (
- - - 사번 - 이름 - 사용자ID - 직급 - 휴대폰 - 이메일 + + + 사번 + 이름 + 사용자ID + 직급 + 휴대폰 + 이메일 diff --git a/frontend/app/(main)/COMPANY_16/master-data/department/page.tsx b/frontend/app/(main)/COMPANY_16/master-data/department/page.tsx index e94ddb3d..fd8138e2 100644 --- a/frontend/app/(main)/COMPANY_16/master-data/department/page.tsx +++ b/frontend/app/(main)/COMPANY_16/master-data/department/page.tsx @@ -24,7 +24,7 @@ import { import { Checkbox } from "@/components/ui/checkbox"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, - Users, Search, Settings2, + Users, Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; @@ -38,11 +38,10 @@ import { exportToExcel } from "@/lib/utils/excelExport"; import { formatField, validateField, validateForm } from "@/lib/utils/validation"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; const DEPT_TABLE = "dept_info"; const USER_TABLE = "user_info"; -const ALL_VALUE = "__all__"; - const DEPT_COLUMNS = [ { key: "parent_dept_code", label: "상위부서" }, { key: "status", label: "상태" }, @@ -52,10 +51,8 @@ export default function DepartmentPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); - // 필터 - const [filterDeptCode, setFilterDeptCode] = useState(""); - const [filterDeptName, setFilterDeptName] = useState(""); - const [filterStatus, setFilterStatus] = useState(ALL_VALUE); + // 검색 필터 (DynamicSearchFilter) + const [searchFilters, setSearchFilters] = useState([]); // 좌측: 부서 const [depts, setDepts] = useState([]); @@ -96,10 +93,7 @@ export default function DepartmentPage() { const fetchDepts = useCallback(async () => { setDeptLoading(true); try { - const filters: { columnName: string; operator: string; value: string }[] = []; - if (filterDeptCode.trim()) filters.push({ columnName: "dept_code", operator: "contains", value: filterDeptCode.trim() }); - if (filterDeptName.trim()) filters.push({ columnName: "dept_name", operator: "contains", value: filterDeptName.trim() }); - if (filterStatus !== ALL_VALUE) filters.push({ columnName: "status", operator: "equals", value: filterStatus }); + const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); const res = await apiClient.post(`/table-management/tables/${DEPT_TABLE}/data`, { page: 1, size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, @@ -115,7 +109,7 @@ export default function DepartmentPage() { } finally { setDeptLoading(false); } - }, [filterDeptCode, filterDeptName, filterStatus]); + }, [searchFilters]); useEffect(() => { fetchDepts(); }, [fetchDepts]); @@ -312,12 +306,6 @@ export default function DepartmentPage() { toast.success("다운로드 완료"); }; - const handleResetFilters = () => { - setFilterDeptCode(""); - setFilterDeptName(""); - setFilterStatus(ALL_VALUE); - }; - // 퇴사일 기반 재직/퇴사 분리 const today = new Date().toISOString().split("T")[0]; const activeMembers = members.filter((m) => !m.end_date || m.end_date.substring(0, 10) >= today); @@ -328,53 +316,24 @@ export default function DepartmentPage() { return (
{/* 검색 필터 바 */} -
-
- 부서코드 - setFilterDeptCode(e.target.value)} - onKeyDown={(e) => { if (e.key === "Enter") void fetchDepts(); }} - placeholder="코드" - className="h-8 w-32 text-sm" - /> -
-
- 부서명 - setFilterDeptName(e.target.value)} - onKeyDown={(e) => { if (e.key === "Enter") void fetchDepts(); }} - placeholder="부서명" - className="h-8 w-36 text-sm" - /> -
-
- 상태 - -
- -
- - -
-
+ + + +
+ } + /> {/* 마스터-디테일 분할 패널 */}
@@ -409,13 +368,13 @@ export default function DepartmentPage() { {/* 부서 테이블 */}
- - - No - 부서코드 - 부서명 - {isColVisible("parent_dept_code") && 상위부서} - {isColVisible("status") && 상태} + + + No + 부서코드 + 부서명 + {isColVisible("parent_dept_code") && 상위부서} + {isColVisible("status") && 상태} @@ -531,15 +490,15 @@ export default function DepartmentPage() {
재직중인 사원이 없어요
) : (
- - - No - 사번 - 이름 - 사용자ID - 직급 - 휴대폰 - 이메일 + + + No + 사번 + 이름 + 사용자ID + 직급 + 휴대폰 + 이메일 @@ -566,16 +525,16 @@ export default function DepartmentPage() {
퇴사한 사원이 없어요
) : (
- - - No - 사번 - 이름 - 사용자ID - 직급 - 휴대폰 - 이메일 - 퇴사일 + + + No + 사번 + 이름 + 사용자ID + 직급 + 휴대폰 + 이메일 + 퇴사일 @@ -799,6 +758,7 @@ export default function DepartmentPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx index 034b4f2c..2c1f5c1b 100644 --- a/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useCallback, useMemo } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -29,10 +29,11 @@ import { SelectValue, } from "@/components/ui/select"; import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; import { exportToExcel } from "@/lib/utils/excelExport"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, - Pencil, Copy, Search, RotateCcw, Settings2, + Pencil, Copy, Settings2, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; @@ -93,10 +94,8 @@ export default function ItemInfoPage() { const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); - // 로컬 검색 필터 - const [searchKeyword, setSearchKeyword] = useState(""); - const [searchDivision, setSearchDivision] = useState("all"); - const [searchStatus, setSearchStatus] = useState("all"); + // 검색 필터 (DynamicSearchFilter) + const [searchFilters, setSearchFilters] = useState([]); // 모달 const [isModalOpen, setIsModalOpen] = useState(false); @@ -150,9 +149,11 @@ export default function ItemInfoPage() { const fetchItems = useCallback(async () => { setLoading(true); try { + const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, { page: 1, size: 500, + dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); @@ -175,23 +176,12 @@ export default function ItemInfoPage() { } finally { setLoading(false); } - }, [categoryOptions]); + }, [categoryOptions, searchFilters]); useEffect(() => { fetchItems(); }, [fetchItems]); - // 클라이언트 필터링 - const filteredData = useMemo(() => { - return items.filter((item) => { - const kw = searchKeyword.trim().toLowerCase(); - if (kw && !item.item_number?.toLowerCase().includes(kw) && !item.item_name?.toLowerCase().includes(kw)) return false; - if (searchDivision !== "all" && item.division !== searchDivision) return false; - if (searchStatus !== "all" && item.status !== searchStatus) return false; - return true; - }); - }, [items, searchKeyword, searchDivision, searchStatus]); - // 등록 모달 열기 const openRegisterModal = () => { setFormData({}); @@ -286,59 +276,21 @@ export default function ItemInfoPage() { toast.success("엑셀 다운로드 완료"); }; - const divisionOptions = useMemo(() => categoryOptions["division"] || [], [categoryOptions]); - const statusOptions = useMemo(() => categoryOptions["status"] || [], [categoryOptions]); - return (
{/* 검색 필터 바 */} -
-
- - setSearchKeyword(e.target.value)} - /> -
- - - -
+ {/* 액션 바 */}
품목 관리 - {filteredData.length}건 + {items.length}건
- - - # + + + # {ts.visibleColumns.map((col) => ( - {filteredData.map((item, idx) => ( + {items.map((item, idx) => ( diff --git a/frontend/app/(main)/COMPANY_16/master-data/options/page.tsx b/frontend/app/(main)/COMPANY_16/master-data/options/page.tsx new file mode 100644 index 00000000..91aa75ef --- /dev/null +++ b/frontend/app/(main)/COMPANY_16/master-data/options/page.tsx @@ -0,0 +1,136 @@ +"use client"; + +import React, { useState, useCallback, useRef, useEffect } from "react"; +import { Settings2, Tags, Hash } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { CategoryColumnList } from "@/components/table-category/CategoryColumnList"; +import { CategoryValueManager } from "@/components/table-category/CategoryValueManager"; +import { NumberingRuleDesigner } from "@/components/numbering-rule/NumberingRuleDesigner"; + +const TABS = [ + { id: "category", label: "카테고리 설정", icon: Tags }, + { id: "numbering", label: "코드 설정", icon: Hash }, +] as const; + +type TabId = (typeof TABS)[number]["id"]; + +export default function OptionsSettingPage() { + const [activeTab, setActiveTab] = useState("category"); + + const [selectedColumn, setSelectedColumn] = useState(null); + const [selectedColumnLabel, setSelectedColumnLabel] = useState(""); + const [selectedTableName, setSelectedTableName] = useState(""); + + const [leftWidth, setLeftWidth] = useState(340); + const [isDragging, setIsDragging] = useState(false); + const containerRef = useRef(null); + + const handleMouseDown = useCallback(() => { + setIsDragging(true); + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + }, []); + + useEffect(() => { + if (!isDragging) return; + const handleMouseMove = (e: MouseEvent) => { + if (!containerRef.current) return; + const rect = containerRef.current.getBoundingClientRect(); + setLeftWidth(Math.max(260, Math.min(500, e.clientX - rect.left))); + }; + const handleMouseUp = () => { + setIsDragging(false); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + }, [isDragging]); + + return ( +
+
+
+ +

옵션 설정

+
+
+ {TABS.map((tab) => { + const Icon = tab.icon; + return ( + + ); + })} +
+
+ +
+ {activeTab === "category" && ( +
+
+ { + setSelectedColumn(uniqueKey); + setSelectedColumnLabel(label); + setSelectedTableName(tableName); + }} + /> +
+ +
+ +
+ {selectedColumn && selectedTableName ? ( + + ) : ( +
+
+ +

좌측에서 카테고리 컬럼을 선택해주세요

+
+
+ )} +
+
+ )} + + {activeTab === "numbering" && ( +
+ +
+ )} +
+
+ ); +} diff --git a/frontend/app/(main)/COMPANY_16/mold/info/page.tsx b/frontend/app/(main)/COMPANY_16/mold/info/page.tsx index 24314e0b..5db95d13 100644 --- a/frontend/app/(main)/COMPANY_16/mold/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/mold/info/page.tsx @@ -744,12 +744,12 @@ export default function MoldInfoPage() { ) : (
- - - 일련번호 - 상태 - 보관위치 - 비고 + + + 일련번호 + 상태 + 보관위치 + 비고 @@ -804,14 +804,14 @@ export default function MoldInfoPage() { ) : (
- - - 점검항목 - 점검주기 - 점검방법 - 하한치 - 상한치 - 단위 + + + 점검항목 + 점검주기 + 점검방법 + 하한치 + 상한치 + 단위 @@ -863,14 +863,14 @@ export default function MoldInfoPage() { ) : (
- - - 부품명 - 교체주기 - 단위 - 규격 - 제조사 - 비고 + + + 부품명 + 교체주기 + 단위 + 규격 + 제조사 + 비고 diff --git a/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor-item/page.tsx b/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor-item/page.tsx index 95ec7313..52b253ec 100644 --- a/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor-item/page.tsx +++ b/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor-item/page.tsx @@ -351,14 +351,14 @@ export default function SubcontractorItemPage() {
- {ts.isVisible("item_number") && 품번} - {ts.isVisible("item_name") && 품명} - {ts.isVisible("size") && 규격} - {ts.isVisible("unit") && 단위} - {ts.isVisible("standard_price") && 기준단가} - {ts.isVisible("selling_price") && 판매가격} - {ts.isVisible("currency_code") && 통화} - {ts.isVisible("status") && 상태} + {ts.isVisible("item_number") && 품번} + {ts.isVisible("item_name") && 품명} + {ts.isVisible("size") && 규격} + {ts.isVisible("unit") && 단위} + {ts.isVisible("standard_price") && 기준단가} + {ts.isVisible("selling_price") && 판매가격} + {ts.isVisible("currency_code") && 통화} + {ts.isVisible("status") && 상태} @@ -423,13 +423,13 @@ export default function SubcontractorItemPage() {
- 외주업체코드 - 외주업체명 - 외주품번 - 외주품명 - 기준가 - 단가 - 통화 + 외주업체코드 + 외주업체명 + 외주품번 + 외주품명 + 기준가 + 단가 + 통화 @@ -532,10 +532,10 @@ export default function SubcontractorItemPage() { else setSubCheckedIds(new Set()); }} /> - 외주업체코드 - 외주업체명 - 거래유형 - 담당자 + 외주업체코드 + 외주업체명 + 거래유형 + 담당자 @@ -587,6 +587,7 @@ export default function SubcontractorItemPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx b/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx index 9ee170d1..5b806f86 100644 --- a/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx +++ b/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx @@ -30,6 +30,7 @@ import { exportToExcel } from "@/lib/utils/excelExport"; import { validateField, validateForm, formatField } from "@/lib/utils/validation"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; const SUBCONTRACTOR_TABLE = "subcontractor_mng"; const MAPPING_TABLE = "subcontractor_item_mapping"; @@ -46,10 +47,8 @@ const GRID_COLUMNS_CONFIG = [ export default function SubcontractorManagementPage() { const { confirm, ConfirmDialogComponent } = useConfirmDialog(); - // 검색 필터 - const [searchKeyword, setSearchKeyword] = useState(""); - const [searchDivision, setSearchDivision] = useState("all"); - const [searchStatus, setSearchStatus] = useState("all"); + // 검색 필터 (DynamicSearchFilter) + const [searchFilters, setSearchFilters] = useState([]); // 좌측: 외주업체 목록 const [subcontractors, setSubcontractors] = useState([]); @@ -144,10 +143,7 @@ export default function SubcontractorManagementPage() { const fetchSubcontractors = useCallback(async () => { setSubcontractorLoading(true); try { - const filters: any[] = []; - if (searchKeyword) filters.push({ columnName: "subcontractor_name", operator: "contains", value: searchKeyword }); - if (searchDivision !== "all") filters.push({ columnName: "division", operator: "equals", value: searchDivision }); - if (searchStatus !== "all") filters.push({ columnName: "status", operator: "equals", value: searchStatus }); + const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); const res = await apiClient.post(`/table-management/tables/${SUBCONTRACTOR_TABLE}/data`, { page: 1, size: 500, @@ -171,7 +167,7 @@ export default function SubcontractorManagementPage() { } finally { setSubcontractorLoading(false); } - }, [searchKeyword, searchDivision, searchStatus, categoryOptions]); + }, [searchFilters, categoryOptions]); useEffect(() => { fetchSubcontractors(); }, [fetchSubcontractors]); @@ -682,44 +678,14 @@ export default function SubcontractorManagementPage() { {/* 검색 필터 바 */} -
-
- setSearchKeyword(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && fetchSubcontractors()} - className="h-9 w-48 text-sm" - /> - - - -
+
-
-
+ } + /> {/* 마스터-디테일 분할 패널 */} @@ -799,14 +765,14 @@ export default function SubcontractorManagementPage() { ) : (
- - - {ts.isVisible("subcontractor_code") && 외주업체코드} - {ts.isVisible("subcontractor_name") && 외주업체명} - {ts.isVisible("contact_person") && 담당자} - {ts.isVisible("contact_phone") && 연락처} - {ts.isVisible("division_label") && 유형} - {ts.isVisible("status_label") && 상태} + + + {ts.isVisible("subcontractor_code") && 외주업체코드} + {ts.isVisible("subcontractor_name") && 외주업체명} + {ts.isVisible("contact_person") && 담당자} + {ts.isVisible("contact_phone") && 연락처} + {ts.isVisible("division_label") && 유형} + {ts.isVisible("status_label") && 상태} @@ -896,18 +862,18 @@ export default function SubcontractorManagementPage() { ) : (
- - - 품목코드 - 품명 - 외주품번 - 외주품명 - 기준유형 - 기준가 - 할인유형 - 할인값 - 단가 - 통화 + + + 품목코드 + 품명 + 외주품번 + 외주품명 + 기준유형 + 기준가 + 할인유형 + 할인값 + 단가 + 통화 @@ -1059,8 +1025,8 @@ export default function SubcontractorManagementPage() {
- - + + - 품목코드 - 품명 - 규격 - 재질 - 단위 + 품목코드 + 품명 + 규격 + 재질 + 단위 @@ -1325,6 +1291,7 @@ export default function SubcontractorManagementPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/production/bom/page.tsx b/frontend/app/(main)/COMPANY_16/production/bom/page.tsx index 0da915df..5a27642d 100644 --- a/frontend/app/(main)/COMPANY_16/production/bom/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/bom/page.tsx @@ -63,6 +63,7 @@ import { toast } from "sonner"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; import { exportToExcel } from "@/lib/utils/excelExport"; // ─── 상수 ─────────────────────────────────────── @@ -221,9 +222,7 @@ export default function BomManagementPage() { const [bomList, setBomList] = useState([]); const [loading, setLoading] = useState(false); const [totalCount, setTotalCount] = useState(0); - const [searchKeyword, setSearchKeyword] = useState(""); - const [searchType, setSearchType] = useState("all"); - const [searchStatus, setSearchStatus] = useState("all"); + const [searchFilters, setSearchFilters] = useState([]); const [selectedBomId, setSelectedBomId] = useState(null); const [checkedIds, setCheckedIds] = useState([]); @@ -291,16 +290,7 @@ export default function BomManagementPage() { const fetchBomList = useCallback(async () => { setLoading(true); try { - const filters: any[] = []; - if (searchKeyword.trim()) { - filters.push({ columnName: "item_name", operator: "contains", value: searchKeyword.trim() }); - } - if (searchType !== "all") { - filters.push({ columnName: "bom_type", operator: "equals", value: searchType }); - } - if (searchStatus !== "all") { - filters.push({ columnName: "status", operator: "equals", value: searchStatus }); - } + const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); const res = await apiClient.post(`/table-management/tables/${BOM_TABLE}/data`, { page: 1, @@ -318,7 +308,7 @@ export default function BomManagementPage() { } finally { setLoading(false); } - }, [searchKeyword, searchType, searchStatus]); + }, [searchFilters]); useEffect(() => { fetchBomList(); @@ -894,60 +884,26 @@ export default function BomManagementPage() { {/* 검색 필터 */} -
- setSearchKeyword(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && fetchBomList()} +
+ + + +
+ } /> - - - - -
- - -
{/* 메인 콘텐츠: 좌우 분할 */} @@ -1000,8 +956,8 @@ export default function BomManagementPage() { ) : (
- - + + 0} @@ -1011,7 +967,7 @@ export default function BomManagementPage() { /> {ts.visibleColumns.map((col) => ( - {col.label} + {col.label} ))} @@ -1529,15 +1485,15 @@ export default function BomManagementPage() {
- - - # - 품목코드 - 품명 - 수량 - 단위 - 공정유형 - 손실율(%) + + + # + 품목코드 + 품명 + 수량 + 단위 + 공정유형 + 손실율(%) @@ -1798,6 +1754,7 @@ export default function BomManagementPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx index 54b5daa3..49640fe7 100644 --- a/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx @@ -86,6 +86,7 @@ import { exportToExcel } from "@/lib/utils/excelExport"; import { apiClient } from "@/lib/api/client"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; // ─── 상수 ─── @@ -146,6 +147,7 @@ export default function ProductionPlanManagementPage() { const [filterUnplannedOrdersOnly, setFilterUnplannedOrdersOnly] = useState(false); const [selectedStockItems, setSelectedStockItems] = useState>(new Set()); + const [searchFilters, setSearchFilters] = useState([]); const [searchItemCode, setSearchItemCode] = useState(""); const [searchStatus, setSearchStatus] = useState("all"); const [searchStartDate, setSearchStartDate] = useState(""); @@ -265,6 +267,36 @@ export default function ProductionPlanManagementPage() { fetchEquipmentList(); }, []); + // ========== DynamicSearchFilter 콜백 ========== + + const handleSearchFilterChange = useCallback((filters: FilterValue[]) => { + setSearchFilters(filters); + let itemCode = ""; + let status = "all"; + let startDate = ""; + let endDate = ""; + for (const f of filters) { + if (f.columnName === "item_code" && f.value) itemCode = f.value; + if (f.columnName === "status" && f.value) status = f.value; + if (f.columnName === "start_date" && f.operator === "between" && f.value) { + const [s, e] = f.value.split("|"); + if (s) startDate = s; + if (e) endDate = e; + } + } + setSearchItemCode(itemCode); + setSearchStatus(status || "all"); + setSearchStartDate(startDate); + setSearchEndDate(endDate); + }, []); + + useEffect(() => { + if (searchFilters.length > 0) { + fetchOrderSummary(); + fetchPlans(); + } + }, [searchItemCode, searchStatus, searchStartDate, searchEndDate]); + // ========== 토글/선택 핸들러 ========== const toggleItemExpand = useCallback((itemCode: string) => { @@ -901,45 +933,13 @@ export default function ProductionPlanManagementPage() { {/* 상단 바 */} -
-
- setSearchItemCode(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSearch()} - className="h-9 w-44 text-sm" - /> - - setSearchStartDate(e.target.value)} - className="h-9 w-40 text-sm" - /> - ~ - setSearchEndDate(e.target.value)} - className="h-9 w-40 text-sm" - /> - -
+
-
-
+ } + /> {/* 데이터 섹션 */} @@ -1020,23 +1020,23 @@ export default function ProductionPlanManagementPage() { ) : (
- - + + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - 품목코드 - 품목명 - {isColVisible("total_order_qty") && 총수주량} - {isColVisible("total_ship_qty") && 출고량} - {isColVisible("total_balance_qty") && 잔량} - {isColVisible("current_stock") && 현재고} - {isColVisible("safety_stock") && 안전재고} - {isColVisible("existing_plan_qty") && 기생산계획량} - {isColVisible("in_progress_qty") && 생산진행} - {isColVisible("required_plan_qty") && 필요생산계획} - {isColVisible("lead_time") && 리드타임(일)} + 품목코드 + 품목명 + {isColVisible("total_order_qty") && 총수주량} + {isColVisible("total_ship_qty") && 출고량} + {isColVisible("total_balance_qty") && 잔량} + {isColVisible("current_stock") && 현재고} + {isColVisible("safety_stock") && 안전재고} + {isColVisible("existing_plan_qty") && 기생산계획량} + {isColVisible("in_progress_qty") && 생산진행} + {isColVisible("required_plan_qty") && 필요생산계획} + {isColVisible("lead_time") && 리드타임(일)} @@ -1133,18 +1133,18 @@ export default function ProductionPlanManagementPage() { ) : (
- - + + 0} onCheckedChange={(c) => toggleAllStockItems(!!c)} className="h-4 w-4" /> - 품목코드 - 품목명 - 현재고 - 안전재고 - 부족수량 - 권장생산량 - 최종입고일 + 품목코드 + 품목명 + 현재고 + 안전재고 + 부족수량 + 권장생산량 + 최종입고일 @@ -1672,13 +1672,13 @@ export default function ProductionPlanManagementPage() {

- - - 품목코드 - 품목명 - 수량 - 시작일 - 종료일 + + + 품목코드 + 품목명 + 수량 + 시작일 + 종료일 @@ -1705,14 +1705,14 @@ export default function ProductionPlanManagementPage() {

- - - 계획번호 - 품목코드 - 품목명 - 수량 - 시작일 - 종료일 + + + 계획번호 + 품목코드 + 품목명 + 수량 + 시작일 + 종료일 @@ -1740,13 +1740,13 @@ export default function ProductionPlanManagementPage() {

- - - 계획번호 - 품목코드 - 품목명 - 수량 - 상태 + + + 계획번호 + 품목코드 + 품목명 + 수량 + 상태 @@ -1796,6 +1796,7 @@ export default function ProductionPlanManagementPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/production/process-info/page.tsx b/frontend/app/(main)/COMPANY_16/production/process-info/page.tsx index 653eba7a..a7b81550 100644 --- a/frontend/app/(main)/COMPANY_16/production/process-info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/process-info/page.tsx @@ -215,6 +215,7 @@ export default function ProcessInfoPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/production/work-instruction/page.tsx b/frontend/app/(main)/COMPANY_16/production/work-instruction/page.tsx index aa3131da..7bbf3870 100644 --- a/frontend/app/(main)/COMPANY_16/production/work-instruction/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/work-instruction/page.tsx @@ -9,7 +9,7 @@ import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; -import { Plus, Trash2, RotateCcw, Save, X, ChevronLeft, ChevronRight, Search, Loader2, Wrench, Pencil, CheckCircle2, ArrowRight, Check, ChevronsUpDown, ClipboardCheck, Inbox, Settings2 } from "lucide-react"; +import { Plus, Trash2, Save, X, ChevronLeft, ChevronRight, Search, Loader2, Wrench, Pencil, CheckCircle2, ArrowRight, Check, ChevronsUpDown, ClipboardCheck, Inbox, Settings2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; @@ -22,6 +22,7 @@ import { import { WorkStandardEditModal } from "./WorkStandardEditModal"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; const GRID_COLUMNS = [ { key: "work_instruction_no", label: "작업지시번호" }, @@ -67,12 +68,7 @@ export default function WorkInstructionPage() { const [employeeOptions, setEmployeeOptions] = useState([]); // 검색 - const [searchKeyword, setSearchKeyword] = useState(""); - const [debouncedKeyword, setDebouncedKeyword] = useState(""); - const [searchStatus, setSearchStatus] = useState("all"); - const [searchProgress, setSearchProgress] = useState("all"); - const [searchDateFrom, setSearchDateFrom] = useState(""); - const [searchDateTo, setSearchDateTo] = useState(""); + const [searchFilters, setSearchFilters] = useState([]); // 1단계: 등록 모달 const [isRegModalOpen, setIsRegModalOpen] = useState(false); @@ -138,9 +134,6 @@ export default function WorkInstructionPage() { const [wsModalItemName, setWsModalItemName] = useState(""); const [wsModalItemCode, setWsModalItemCode] = useState(""); - useEffect(() => { const t = setTimeout(() => setDebouncedKeyword(searchKeyword), 500); return () => clearTimeout(t); }, [searchKeyword]); - - useEffect(() => { getEquipmentList().then(r => { if (r.success) setEquipmentOptions(r.data || []); }); getEmployeeList().then(r => { if (r.success) setEmployeeOptions(r.data || []); }); @@ -150,23 +143,28 @@ export default function WorkInstructionPage() { setLoading(true); try { const params: any = {}; - if (searchDateFrom) params.dateFrom = searchDateFrom; - if (searchDateTo) params.dateTo = searchDateTo; - if (searchStatus !== "all") params.status = searchStatus; - if (searchProgress !== "all") params.progressStatus = searchProgress; - if (debouncedKeyword.trim()) params.keyword = debouncedKeyword.trim(); + for (const f of searchFilters) { + if (f.columnName === "start_date" && f.operator === "between" && f.value) { + const [from, to] = f.value.split("|"); + if (from) params.dateFrom = from; + if (to) params.dateTo = to; + } else if (f.columnName === "status" && f.value) { + params.status = f.value; + } else if (f.columnName === "progress" && f.value) { + params.progressStatus = f.value; + } else if (f.columnName === "work_instruction_no" && f.value) { + params.keyword = f.value; + } else if (f.columnName === "item_name" && f.value) { + params.keyword = f.value; + } + } const r = await getWorkInstructionList(params); if (r.success) setOrders(r.data || []); } catch {} finally { setLoading(false); } - }, [searchDateFrom, searchDateTo, searchStatus, searchProgress, debouncedKeyword]); + }, [searchFilters]); useEffect(() => { fetchOrders(); }, [fetchOrders]); - const handleResetSearch = () => { - setSearchKeyword(""); setDebouncedKeyword(""); setSearchStatus("all"); setSearchProgress("all"); - setSearchDateFrom(""); setSearchDateTo(""); - }; - // ─── 1단계 등록 ─── const openRegModal = () => { setRegSourceType("production"); setRegSourceData([]); setRegKeyword(""); setRegCheckedIds(new Set()); @@ -415,31 +413,13 @@ export default function WorkInstructionPage() { return (
{/* 검색 필터 바 */} -
-
- 작업기간 - setSearchDateFrom(e.target.value)} className="h-8 w-[130px] text-xs" /> - ~ - setSearchDateTo(e.target.value)} className="h-8 w-[130px] text-xs" /> -
- setSearchKeyword(e.target.value)} className="h-8 w-[195px] text-xs" /> - - -
- {loading && } - - -
+ o.work_instruction_no)).size} + /> {/* 메인 테이블 */}
@@ -467,21 +447,21 @@ export default function WorkInstructionPage() { {/* 테이블 */}
- - - {ts.isVisible("work_instruction_no") && 작업지시번호} - {ts.isVisible("status") && 상태} - {ts.isVisible("progress") && 진행현황} - {ts.isVisible("item_name") && 품목명} - {ts.isVisible("spec") && 규격} - {ts.isVisible("qty") && 수량} - {ts.isVisible("equipment") && 설비} - {ts.isVisible("routing") && 라우팅} - {ts.isVisible("work_team") && 작업조} - {ts.isVisible("worker") && 작업자} - {ts.isVisible("start_date") && 시작일} - {ts.isVisible("end_date") && 완료일} - {ts.isVisible("actions") && 작업} + + + {ts.isVisible("work_instruction_no") && 작업지시번호} + {ts.isVisible("status") && 상태} + {ts.isVisible("progress") && 진행현황} + {ts.isVisible("item_name") && 품목명} + {ts.isVisible("spec") && 규격} + {ts.isVisible("qty") && 수량} + {ts.isVisible("equipment") && 설비} + {ts.isVisible("routing") && 라우팅} + {ts.isVisible("work_team") && 작업조} + {ts.isVisible("worker") && 작업자} + {ts.isVisible("start_date") && 시작일} + {ts.isVisible("end_date") && 완료일} + {ts.isVisible("actions") && 작업} @@ -615,12 +595,12 @@ export default function WorkInstructionPage() { ) : (
- - - 0 && regCheckedIds.size === regSourceData.length} onCheckedChange={toggleRegAll} /> - {regSourceType === "item" && <>품목코드품목명규격} - {regSourceType === "order" && <>수주번호품번품목명규격수량납기일} - {regSourceType === "production" && <>계획번호품번품목명계획수량시작일완료일설비} + + + 0 && regCheckedIds.size === regSourceData.length} onCheckedChange={toggleRegAll} /> + {regSourceType === "item" && <>품목코드품목명규격} + {regSourceType === "order" && <>수주번호품번품목명규격수량납기일} + {regSourceType === "production" && <>계획번호품번품목명계획수량시작일완료일설비} @@ -693,15 +673,15 @@ export default function WorkInstructionPage() {

품목 목록

- - - 순번 - 품목코드 - 품목명 - 규격 - 수량 - 라우팅 - 비고 + + + 순번 + 품목코드 + 품목명 + 규격 + 수량 + 라우팅 + 비고 @@ -782,16 +762,16 @@ export default function WorkInstructionPage() {
- - - 순번 - 품목코드 - 품목명 - 규격 - 수량 - 라우팅 - 공정작업기준 - 비고 + + + 순번 + 품목코드 + 품목명 + 규격 + 수량 + 라우팅 + 공정작업기준 + 비고 @@ -884,6 +864,7 @@ export default function WorkInstructionPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx index ddb368ad..f746596f 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx @@ -13,7 +13,7 @@ import { Label } from "@/components/ui/label"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, ClipboardList, Pencil, Search, X, Package, - ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, RotateCcw, + ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -25,6 +25,7 @@ import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; const MASTER_TABLE = "purchase_order_mng"; const DETAIL_TABLE = "purchase_detail"; @@ -88,12 +89,8 @@ export default function PurchaseOrderPage() { const [loading, setLoading] = useState(false); const [totalCount, setTotalCount] = useState(0); - // 인라인 검색 필터 - const [searchOrderNo, setSearchOrderNo] = useState(""); - const [searchSupplier, setSearchSupplier] = useState(""); - const [searchStatus, setSearchStatus] = useState("all"); - const [searchDateFrom, setSearchDateFrom] = useState(""); - const [searchDateTo, setSearchDateTo] = useState(""); + // 검색 필터 (DynamicSearchFilter) + const [searchFilters, setSearchFilters] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const [isEditMode, setIsEditMode] = useState(false); @@ -193,17 +190,28 @@ export default function PurchaseOrderPage() { loadCategories(); }, []); + // 마스터 테이블 컬럼 (supplier_name, order_date 등) + const MASTER_COLUMNS = new Set(["supplier_name", "supplier_code", "order_date", "status"]); + // 데이터 조회 const fetchOrders = useCallback(async () => { setLoading(true); try { - const filters: any[] = []; - if (searchOrderNo) filters.push({ columnName: "purchase_no", operator: "contains", value: searchOrderNo }); - if (searchStatus && searchStatus !== "all") filters.push({ columnName: "status", operator: "equals", value: searchStatus }); + // searchFilters를 detail / master로 분리 + const detailFilters: any[] = []; + const masterExtraFilters: any[] = []; + for (const f of searchFilters) { + const filter = { columnName: f.columnName, operator: f.operator, value: f.value }; + if (MASTER_COLUMNS.has(f.columnName)) { + masterExtraFilters.push(filter); + } else { + detailFilters.push(filter); + } + } const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { page: 1, size: 500, - dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, + dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined, autoFilter: true, sort: { columnName: "purchase_no", order: "desc" }, }); @@ -213,10 +221,7 @@ export default function PurchaseOrderPage() { let masterMap: Record = {}; if (purchaseNos.length > 0) { try { - const masterFilters: any[] = [{ columnName: "purchase_no", operator: "in", value: purchaseNos }]; - if (searchSupplier) masterFilters.push({ columnName: "supplier_name", operator: "contains", value: searchSupplier }); - if (searchDateFrom) masterFilters.push({ columnName: "order_date", operator: "gte", value: searchDateFrom }); - if (searchDateTo) masterFilters.push({ columnName: "order_date", operator: "lte", value: searchDateTo }); + const masterFilters: any[] = [{ columnName: "purchase_no", operator: "in", value: purchaseNos }, ...masterExtraFilters]; const masterRes = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, { page: 1, size: purchaseNos.length + 10, dataFilter: { enabled: true, filters: masterFilters }, @@ -248,11 +253,11 @@ export default function PurchaseOrderPage() { return opts.find((o) => o.code === code)?.label || code; }; + const hasMasterFilters = masterExtraFilters.length > 0; const data = rows .filter((row: any) => { const master = masterMap[row.purchase_no]; - // master 필터가 있으면 masterMap에 없는 것 제외 - if ((searchSupplier || searchDateFrom || searchDateTo) && !master) return false; + if (hasMasterFilters && !master) return false; return true; }) .map((row: any) => { @@ -278,7 +283,7 @@ export default function PurchaseOrderPage() { } finally { setLoading(false); } - }, [searchOrderNo, searchSupplier, searchStatus, searchDateFrom, searchDateTo, categoryOptions]); + }, [searchFilters, categoryOptions]); useEffect(() => { fetchOrders(); }, [fetchOrders]); @@ -288,14 +293,6 @@ export default function PurchaseOrderPage() { return found?.label || code; }; - const handleResetSearch = () => { - setSearchOrderNo(""); - setSearchSupplier(""); - setSearchStatus("all"); - setSearchDateFrom(""); - setSearchDateTo(""); - }; - // 등록 모달 열기 const openRegisterModal = () => { const defaultInputMode = categoryOptions["input_mode"]?.[0]?.code || ""; @@ -596,57 +593,14 @@ export default function PurchaseOrderPage() { return (
- {/* 인라인 검색 바 */} -
-
- - setSearchOrderNo(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && fetchOrders()} - className="h-8 text-sm" - /> -
-
- - setSearchSupplier(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && fetchOrders()} - className="h-8 text-sm" - /> -
-
- - -
-
- - setSearchDateFrom(e.target.value)} className="h-8 text-sm" /> -
-
- - setSearchDateTo(e.target.value)} className="h-8 text-sm" /> -
-
- - -
-
+ {/* 검색 필터 바 */} + {/* 액션 바 */}
@@ -686,8 +640,8 @@ export default function PurchaseOrderPage() { {/* 데이터 테이블 */}
- - + + - {ts.isVisible("purchase_no") && 발주번호} - {ts.isVisible("order_date") && 발주일} - {ts.isVisible("supplier_name") && 공급업체} - {ts.isVisible("item_code") && 품번} - {ts.isVisible("item_name") && 품명} - {ts.isVisible("spec") && 규격} - {ts.isVisible("order_qty") && 발주수량} - {ts.isVisible("received_qty") && 입고수량} - {ts.isVisible("remain_qty") && 잔량} - {ts.isVisible("unit_price") && 단가} - {ts.isVisible("amount") && 금액} - {ts.isVisible("due_date") && 납기일} - {ts.isVisible("status") && 상태} - {ts.isVisible("memo") && 메모} + {ts.isVisible("purchase_no") && 발주번호} + {ts.isVisible("order_date") && 발주일} + {ts.isVisible("supplier_name") && 공급업체} + {ts.isVisible("item_code") && 품번} + {ts.isVisible("item_name") && 품명} + {ts.isVisible("spec") && 규격} + {ts.isVisible("order_qty") && 발주수량} + {ts.isVisible("received_qty") && 입고수량} + {ts.isVisible("remain_qty") && 잔량} + {ts.isVisible("unit_price") && 단가} + {ts.isVisible("amount") && 금액} + {ts.isVisible("due_date") && 납기일} + {ts.isVisible("status") && 상태} + {ts.isVisible("memo") && 메모} @@ -923,20 +877,20 @@ export default function PurchaseOrderPage() { ) : (
- - - {!isReadOnly && } - 품번 - 품명 - 규격 - 단위 - 발주수량 - 입고수량 - 잔량 - 단가 - 금액 - 납기일 - 메모 + + + {!isReadOnly && } + 품번 + 품명 + 규격 + 단위 + 발주수량 + 입고수량 + 잔량 + 단가 + 금액 + 납기일 + 메모 @@ -1052,8 +1006,8 @@ export default function PurchaseOrderPage() {
- - + + 0 && itemSearchResults.every((i) => itemSelectedMap.has(i.id))} @@ -1066,11 +1020,11 @@ export default function PurchaseOrderPage() { }); }} /> - 품목코드 - 품명 - 규격 - 재질 - 단위 + 품목코드 + 품명 + 규격 + 재질 + 단위 @@ -1163,6 +1117,7 @@ export default function PurchaseOrderPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx index e57085a0..a46de4d0 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx @@ -9,7 +9,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogD import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; -import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Truck, Search, RotateCcw, Settings2 } from "lucide-react"; +import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Truck, Search, Settings2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; @@ -19,6 +19,7 @@ import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; import { exportToExcel } from "@/lib/utils/excelExport"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; const ITEM_TABLE = "item_info"; const MAPPING_TABLE = "supplier_item_mapping"; @@ -35,9 +36,8 @@ export default function PurchaseItemPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); - // 검색 - const [inputKeyword, setInputKeyword] = useState(""); - const [searchKeyword, setSearchKeyword] = useState(""); + // 검색 필터 (DynamicSearchFilter) + const [searchFilters, setSearchFilters] = useState([]); // 좌측: 품목 const [items, setItems] = useState([]); @@ -108,10 +108,7 @@ export default function PurchaseItemPage() { const fetchItems = useCallback(async () => { setItemLoading(true); try { - const filters: any[] = []; - if (searchKeyword) { - filters.push({ columnName: "item_name", operator: "contains", value: searchKeyword }); - } + const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); const res = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, { page: 1, size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, @@ -123,7 +120,7 @@ export default function PurchaseItemPage() { } finally { setItemLoading(false); } - }, [searchKeyword]); + }, [searchFilters]); useEffect(() => { fetchItems(); }, [fetchItems]); @@ -349,33 +346,23 @@ export default function PurchaseItemPage() { return (
{/* 검색 바 */} -
-
- - setInputKeyword(e.target.value)} - onKeyDown={(e) => { if (e.key === "Enter") setSearchKeyword(inputKeyword); }} - className="w-[220px] h-9" - /> -
-
- - -
- - -
-
+ + + +
+ } + /> {/* 분할 패널 */}
@@ -395,14 +382,14 @@ export default function PurchaseItemPage() {
- - - 품번 - 품명 - {isColVisible("size") && 규격} - {isColVisible("unit") && 단위} - {isColVisible("standard_price") && 기준단가} - {isColVisible("status") && 상태} + + + 품번 + 품명 + {isColVisible("size") && 규격} + {isColVisible("unit") && 단위} + {isColVisible("standard_price") && 기준단가} + {isColVisible("status") && 상태} @@ -480,8 +467,8 @@ export default function PurchaseItemPage() {
- - + + 0 && supplierCheckedIds.length === supplierItems.length} @@ -491,13 +478,13 @@ export default function PurchaseItemPage() { }} /> - 공급업체코드 - 공급업체명 - 공급업체품번 - 기준가 - 단가 - 통화 - 리드타임 + 공급업체코드 + 공급업체명 + 공급업체품번 + 기준가 + 단가 + 통화 + 리드타임 @@ -604,8 +591,8 @@ export default function PurchaseItemPage() {
- - + + 0 && suppCheckedIds.size === suppSearchResults.length} @@ -615,10 +602,10 @@ export default function PurchaseItemPage() { }} /> - 공급업체코드 - 공급업체명 - 담당자 - 연락처 + 공급업체코드 + 공급업체명 + 담당자 + 연락처 @@ -758,6 +745,7 @@ export default function PurchaseItemPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx index e66eadaf..1a45c38c 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx @@ -9,7 +9,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogD import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; -import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Truck, Search, RotateCcw, Settings2 } from "lucide-react"; +import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Truck, Search, Settings2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; @@ -19,6 +19,7 @@ import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; import { exportToExcel } from "@/lib/utils/excelExport"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; const SUPPLIER_TABLE = "supplier_mng"; const MAPPING_TABLE = "supplier_item_mapping"; @@ -33,9 +34,8 @@ export default function SupplierManagementPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); - // 검색 - const [inputKeyword, setInputKeyword] = useState(""); - const [searchKeyword, setSearchKeyword] = useState(""); + // 검색 필터 (DynamicSearchFilter) + const [searchFilters, setSearchFilters] = useState([]); // 좌측: 공급업체 목록 const [suppliers, setSuppliers] = useState([]); @@ -81,8 +81,7 @@ export default function SupplierManagementPage() { const fetchSuppliers = useCallback(async () => { setSupplierLoading(true); try { - const filters: any[] = []; - if (searchKeyword) filters.push({ columnName: "supplier_name", operator: "contains", value: searchKeyword }); + const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); const res = await apiClient.post(`/table-management/tables/${SUPPLIER_TABLE}/data`, { page: 1, size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, @@ -94,7 +93,7 @@ export default function SupplierManagementPage() { } finally { setSupplierLoading(false); } - }, [searchKeyword]); + }, [searchFilters]); useEffect(() => { fetchSuppliers(); }, [fetchSuppliers]); @@ -331,33 +330,23 @@ export default function SupplierManagementPage() { return (
{/* 검색 바 */} -
-
- - setInputKeyword(e.target.value)} - onKeyDown={(e) => { if (e.key === "Enter") setSearchKeyword(inputKeyword); }} - className="w-[220px] h-9" - /> -
-
- - -
- - -
-
+ + + +
+ } + /> {/* 분할 패널 */}
@@ -382,13 +371,13 @@ export default function SupplierManagementPage() {
- - - 공급업체코드 - 공급업체명 - {isColVisible("contact_person") && 담당자} - {isColVisible("contact_phone") && 연락처} - {isColVisible("status") && 상태} + + + 공급업체코드 + 공급업체명 + {isColVisible("contact_person") && 담당자} + {isColVisible("contact_phone") && 연락처} + {isColVisible("status") && 상태} @@ -462,8 +451,8 @@ export default function SupplierManagementPage() {
- - + + 0 && mappingCheckedIds.length === mappingItems.length} @@ -473,13 +462,13 @@ export default function SupplierManagementPage() { }} /> - 품목코드 - 품명 - 공급업체품번 - 기준가 - 단가 - 통화 - 리드타임 + 품목코드 + 품명 + 공급업체품번 + 기준가 + 단가 + 통화 + 리드타임 @@ -590,8 +579,8 @@ export default function SupplierManagementPage() {
- - + + 0 && itemCheckedIds.size === itemSearchResults.length} @@ -601,11 +590,11 @@ export default function SupplierManagementPage() { }} /> - 품목코드 - 품명 - 규격 - 단위 - 기준단가 + 품목코드 + 품명 + 규격 + 단위 + 기준단가 @@ -746,6 +735,7 @@ export default function SupplierManagementPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx b/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx index c5ae6ae8..be480fcc 100644 --- a/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx @@ -14,6 +14,7 @@ import { Plus, Trash2, Save, Loader2, Pencil, ClipboardCheck, AlertTriangle, Wrench, Search, Inbox, Settings2, } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; @@ -63,7 +64,7 @@ export default function InspectionManagementPage() { const [inspEditMode, setInspEditMode] = useState(false); const [inspForm, setInspForm] = useState>({}); const [inspSaving, setInspSaving] = useState(false); - const [inspKeyword, setInspKeyword] = useState(""); + const [searchFilters, setSearchFilters] = useState([]); /* ───── 불량관리 ───── */ const [defects, setDefects] = useState([]); @@ -122,12 +123,10 @@ export default function InspectionManagementPage() { }; /* ═══════════════════ 데이터 조회 ═══════════════════ */ - const fetchInspections = useCallback(async (keyword?: string) => { + const fetchInspections = useCallback(async () => { setInspLoading(true); try { - const kw = keyword !== undefined ? keyword : inspKeyword; - const filters: any[] = []; - if (kw.trim()) filters.push({ columnName: "inspection_standard", operator: "contains", value: kw.trim() }); + const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); const res = await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/data`, { page: 1, size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, @@ -141,7 +140,7 @@ export default function InspectionManagementPage() { } finally { setInspLoading(false); } - }, [inspKeyword]); + }, [searchFilters]); const fetchDefects = useCallback(async () => { setDefLoading(true); @@ -175,11 +174,8 @@ export default function InspectionManagementPage() { } }, []); - useEffect(() => { - fetchInspections(); - fetchDefects(); - fetchEquipments(); - }, []); + useEffect(() => { fetchInspections(); }, [fetchInspections]); + useEffect(() => { fetchDefects(); fetchEquipments(); }, []); /* ───── 클라이언트 필터 ───── */ const filteredDefects = defKeyword.trim() @@ -333,43 +329,33 @@ export default function InspectionManagementPage() { {/* ──── 검사기준 탭 ──── */} -
-
-
- - setInspKeyword(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && fetchInspections(inspKeyword)} - /> -
- - - {inspCount}건 -
-
- - - - -
+
+ + + + + +
+ } + />
- - + + 0 && inspChecked.length === inspections.length} @@ -377,7 +363,7 @@ export default function InspectionManagementPage() { /> {ts.visibleColumns.map((col) => ( - {col.label} + {col.label} ))} @@ -436,18 +422,18 @@ export default function InspectionManagementPage() {
- - + + 0 && defChecked.length === filteredDefects.length} onCheckedChange={(v) => setDefChecked(v ? filteredDefects.map(r => r.id) : [])} /> - 불량유형 - 불량명 - 심각도 - 사용여부 + 불량유형 + 불량명 + 심각도 + 사용여부 @@ -507,20 +493,20 @@ export default function InspectionManagementPage() {
- - + + 0 && eqChecked.length === filteredEquipments.length} onCheckedChange={(v) => setEqChecked(v ? filteredEquipments.map(r => r.id) : [])} /> - 장비명 - 모델명 - 제조사 - 교정주기 - 최종교정일 - 장비상태 + 장비명 + 모델명 + 제조사 + 교정주기 + 최종교정일 + 장비상태 @@ -722,6 +708,7 @@ export default function InspectionManagementPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx index 9b9f1ea2..1a62164d 100644 --- a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx @@ -10,8 +10,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import { - Plus, Trash2, Save, Loader2, Pencil, Search, Inbox, Settings2, + Plus, Trash2, Save, Loader2, Pencil, Inbox, Settings2, } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; @@ -42,7 +43,7 @@ export default function ItemInspectionInfoPage() { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [totalCount, setTotalCount] = useState(0); - const [searchKeyword, setSearchKeyword] = useState(""); + const [searchFilters, setSearchFilters] = useState([]); const [checkedIds, setCheckedIds] = useState([]); const [modalOpen, setModalOpen] = useState(false); @@ -73,12 +74,10 @@ export default function ItemInspectionInfoPage() { }, []); /* ═══════════════════ 데이터 조회 ═══════════════════ */ - const fetchData = useCallback(async (keyword?: string) => { + const fetchData = useCallback(async () => { setLoading(true); try { - const kw = keyword !== undefined ? keyword : searchKeyword; - const filters: any[] = []; - if (kw.trim()) filters.push({ columnName: "item_name", operator: "contains", value: kw.trim() }); + const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, { page: 1, size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, @@ -92,9 +91,9 @@ export default function ItemInspectionInfoPage() { } finally { setLoading(false); } - }, [searchKeyword]); + }, [searchFilters]); - useEffect(() => { fetchData(); }, []); + useEffect(() => { fetchData(); }, [fetchData]); /* ═══════════════════ CRUD ═══════════════════ */ const openCreate = () => { setForm({}); setEditMode(false); setModalOpen(true); }; @@ -141,44 +140,34 @@ export default function ItemInspectionInfoPage() { {ConfirmDialogComponent}
-
-
-
- - setSearchKeyword(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && fetchData(searchKeyword)} - /> -
- - - {totalCount}건 -
-
- - - - -
+
+ + + + + +
+ } + />
- - + + 0 && checkedIds.length === data.length} @@ -186,7 +175,7 @@ export default function ItemInspectionInfoPage() { /> {ts.visibleColumns.map((col) => ( - {col.label} + {col.label} ))} @@ -290,6 +279,7 @@ export default function ItemInspectionInfoPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/sales/claim/page.tsx b/frontend/app/(main)/COMPANY_16/sales/claim/page.tsx index a2711fd5..a7f3763e 100644 --- a/frontend/app/(main)/COMPANY_16/sales/claim/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/claim/page.tsx @@ -61,8 +61,6 @@ import { Pencil, FileText, Wrench, - Search, - RotateCcw, Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -72,6 +70,7 @@ import { toast } from "sonner"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; import { exportToExcel } from "@/lib/utils/excelExport"; @@ -156,12 +155,8 @@ export default function ClaimManagementPage() { const [totalCount, setTotalCount] = useState(0); const [selectedClaimNo, setSelectedClaimNo] = useState(null); - // 로컬 검색 필터 상태 - const [searchKeyword, setSearchKeyword] = useState(""); - const [searchType, setSearchType] = useState("all"); - const [searchStatus, setSearchStatus] = useState("all"); - const [dateFrom, setDateFrom] = useState(""); - const [dateTo, setDateTo] = useState(""); + // 검색 필터 (DynamicSearchFilter에서 관리) + const [searchFilters, setSearchFilters] = useState([]); // 엑셀 업로드 const [excelUploadOpen, setExcelUploadOpen] = useState(false); @@ -186,11 +181,11 @@ export default function ClaimManagementPage() { const fetchData = useCallback(async () => { setLoading(true); try { - const filters: any[] = []; - if (searchType !== "all") filters.push({ columnName: "claim_type", operator: "equals", value: searchType }); - if (searchStatus !== "all") filters.push({ columnName: "claim_status", operator: "equals", value: searchStatus }); - if (dateFrom) filters.push({ columnName: "claim_date", operator: "gte", value: dateFrom }); - if (dateTo) filters.push({ columnName: "claim_date", operator: "lte", value: dateTo }); + const filters = searchFilters.map(f => ({ + columnName: f.columnName, + operator: f.operator, + value: f.value, + })); const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, { page: 1, @@ -208,22 +203,12 @@ export default function ClaimManagementPage() { } finally { setLoading(false); } - }, [searchType, searchStatus, dateFrom, dateTo]); + }, [searchFilters]); useEffect(() => { fetchData(); }, [fetchData]); - // 클라이언트 사이드 필터링 (키워드 다중컬럼 검색만) - const filteredData = useMemo(() => { - if (!searchKeyword) return data; - const kw = searchKeyword.toLowerCase(); - return data.filter((claim) => - claim.claim_no?.toLowerCase().includes(kw) || - claim.customer_name?.toLowerCase().includes(kw) || - claim.manager_name?.toLowerCase().includes(kw) - ); - }, [data, searchKeyword]); // 거래처 목록 조회 (autoFilter로 멀티테넌시 적용) const fetchCustomers = useCallback(async (force = false) => { @@ -431,96 +416,16 @@ export default function ClaimManagementPage() { [data, selectedClaimNo] ); - // 검색 초기화 - const handleResetSearch = () => { - setSearchKeyword(""); - setSearchType("all"); - setSearchStatus("all"); - setDateFrom(""); - setDateTo(""); - }; - return (
- {/* ───── 검색 필터 바 ───── */} -
- {/* 접수일자 범위 */} -
- 접수일자 - setDateFrom(e.target.value)} - className="h-8 w-[130px] text-sm" - /> - ~ - setDateTo(e.target.value)} - className="h-8 w-[130px] text-sm" - /> -
- - {/* 클레임 유형 */} -
- 유형 - -
- - {/* 처리 상태 */} -
- 상태 - -
- - {/* 검색어 */} -
- 검색어 -
- - setSearchKeyword(e.target.value)} - placeholder="클레임번호/거래처명" - className="h-8 pl-8 w-[180px] text-sm" - onKeyDown={(e) => e.key === "Enter" && fetchData()} - /> -
-
- - {/* 버튼 영역 */} -
- - -
-
+ {/* 검색 필터 (DynamicSearchFilter) */} + {/* ───── 메인 분할 레이아웃 ───── */}
@@ -534,7 +439,7 @@ export default function ClaimManagementPage() { 클레임 목록 - {filteredData.length}건 + {data.length}건 {loading && }
@@ -560,8 +465,8 @@ export default function ClaimManagementPage() { {/* 테이블 */}
- - + + # {ts.visibleColumns.map((col) => ( @@ -580,7 +485,7 @@ export default function ClaimManagementPage() { - ) : filteredData.length === 0 ? ( + ) : data.length === 0 ? (
@@ -590,7 +495,7 @@ export default function ClaimManagementPage() { ) : ( - filteredData.map((claim, idx) => ( + data.map((claim, idx) => (
diff --git a/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx b/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx index ae4938bc..f5a4061f 100644 --- a/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx @@ -35,21 +35,31 @@ import { MultiTableExcelUploadModal } from "@/components/common/MultiTableExcelU import { autoDetectMultiTableConfig, TableChainConfig } from "@/lib/api/multiTableExcel"; import { exportToExcel } from "@/lib/utils/excelExport"; import { validateField, validateForm, formatField } from "@/lib/utils/validation"; -import { TableSettingsModal, TableSettings, loadTableSettings } from "@/components/common/TableSettingsModal"; +import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { useTableSettings } from "@/hooks/useTableSettings"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; const CUSTOMER_TABLE = "customer_mng"; const MAPPING_TABLE = "customer_item_mapping"; const PRICE_TABLE = "customer_item_prices"; const DELIVERY_TABLE = "delivery_destination"; +const CUSTOMER_GRID_COLUMNS = [ + { key: "customer_code", label: "거래처코드" }, + { key: "customer_name", label: "거래처명" }, + { key: "contact_person", label: "대표자" }, + { key: "contact_phone", label: "연락처" }, + { key: "division", label: "유형" }, + { key: "status", label: "상태" }, +]; + export default function CustomerManagementPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); + const ts = useTableSettings("c16-customer", CUSTOMER_TABLE, CUSTOMER_GRID_COLUMNS); - // 검색 필터 - const [searchKeyword, setSearchKeyword] = useState(""); - const [searchDivision, setSearchDivision] = useState("__all__"); - const [searchStatus, setSearchStatus] = useState("__all__"); + // 검색 필터 (DynamicSearchFilter에서 관리) + const [searchFilters, setSearchFilters] = useState([]); // 좌측: 거래처 목록 const [customers, setCustomers] = useState([]); @@ -112,14 +122,6 @@ export default function CustomerManagementPage() { const [categoryOptions, setCategoryOptions] = useState>({}); const [employeeOptions, setEmployeeOptions] = useState<{ user_id: string; user_name: string; position_name?: string }[]>([]); - // 테이블 설정 - const [tableSettingsOpen, setTableSettingsOpen] = useState(false); - const [gridColumns, setGridColumns] = useState( - () => loadTableSettings("customer-mng")?.columns || [] - ); - const [filterConfig, setFilterConfig] = useState( - () => loadTableSettings("customer-mng")?.filters - ); // 카테고리 로드 useEffect(() => { @@ -164,10 +166,11 @@ export default function CustomerManagementPage() { const fetchCustomers = useCallback(async () => { setCustomerLoading(true); try { - const filters: any[] = []; - if (searchKeyword) filters.push({ columnName: "customer_name", operator: "contains", value: searchKeyword }); - if (searchDivision !== "__all__") filters.push({ columnName: "division", operator: "equals", value: searchDivision }); - if (searchStatus !== "__all__") filters.push({ columnName: "status", operator: "equals", value: searchStatus }); + const filters = searchFilters.map(f => ({ + columnName: f.columnName, + operator: f.operator, + value: f.value, + })); const res = await apiClient.post(`/table-management/tables/${CUSTOMER_TABLE}/data`, { page: 1, size: 500, @@ -196,7 +199,7 @@ export default function CustomerManagementPage() { } finally { setCustomerLoading(false); } - }, [searchKeyword, searchDivision, searchStatus, categoryOptions, employeeOptions]); + }, [searchFilters, categoryOptions, employeeOptions]); useEffect(() => { fetchCustomers(); }, [fetchCustomers]); @@ -769,11 +772,7 @@ export default function CustomerManagementPage() { }; // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => { - if (gridColumns.length === 0) return true; - const col = gridColumns.find((c) => c.columnName === key); - return col ? col.visible : true; - }; + const isColumnVisible = (key: string) => ts.isVisible(key); const customerColSpan = 1 + ["customer_code", "customer_name", "contact_person", "contact_phone", "division", "status"] .filter((k) => isColumnVisible(k)).length; @@ -848,49 +847,17 @@ export default function CustomerManagementPage() { return (
- {/* 검색 필터 바 */} -
-
- 거래처명 - setSearchKeyword(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && fetchCustomers()} - placeholder="거래처명 검색" - className="h-8 w-40 text-sm" - /> -
-
- 유형 - -
-
- 상태 - -
- + {/* 검색 필터 (DynamicSearchFilter) */} + + + {/* 액션 버튼 영역 */} +
-
@@ -949,15 +916,15 @@ export default function CustomerManagementPage() { {/* 거래처 테이블 */}
- - - No - {isColumnVisible("customer_code") && 거래처코드} - {isColumnVisible("customer_name") && 거래처명} - {isColumnVisible("contact_person") && 대표자} - {isColumnVisible("contact_phone") && 연락처} - {isColumnVisible("division") && 유형} - {isColumnVisible("status") && 상태} + + + No + {isColumnVisible("customer_code") && 거래처코드} + {isColumnVisible("customer_name") && 거래처명} + {isColumnVisible("contact_person") && 대표자} + {isColumnVisible("contact_phone") && 연락처} + {isColumnVisible("division") && 유형} + {isColumnVisible("status") && 상태} @@ -1085,8 +1052,8 @@ export default function CustomerManagementPage() {
- - + + setPriceCheckedIds(e.target.checked ? priceItems.map((p) => p.id) : [])} /> - 품목코드 - 품명 - 거래처품번 - 거래처품명 - 기준유형 - 기준가 - 할인유형 - 할인값 - 단가 - 통화 + 품목코드 + 품명 + 거래처품번 + 거래처품명 + 기준유형 + 기준가 + 할인유형 + 할인값 + 단가 + 통화 @@ -1176,8 +1143,8 @@ export default function CustomerManagementPage() {
- - + + setDeliveryCheckedIds(e.target.checked ? deliveryItems.map((d) => d.id) : [])} /> - 납품처코드 - 납품처명 - 주소 - 담당자 - 전화번호 - 메모 - 기본 + 납품처코드 + 납품처명 + 주소 + 담당자 + 전화번호 + 메모 + 기본 @@ -1406,8 +1373,8 @@ export default function CustomerManagementPage() {
- - + + - 품목코드 - 품명 - 규격 - 재질 - 단위 + 품목코드 + 품명 + 규격 + 재질 + 단위 @@ -1790,14 +1757,12 @@ export default function CustomerManagementPage() { {/* 테이블 설정 모달 */} { - setGridColumns(settings.columns); - setFilterConfig(settings.filters); - }} + open={ts.open} + onOpenChange={ts.setOpen} + tableName={ts.tableName} + settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} + onSave={ts.applySettings} /> {ConfirmDialogComponent} diff --git a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx index defe971c..475facb1 100644 --- a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx @@ -12,7 +12,7 @@ import { Label } from "@/components/ui/label"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, ClipboardList, Pencil, Search, X, Truck, Package, - ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, RotateCcw, + ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -25,6 +25,7 @@ import { toast } from "sonner"; import { ShippingPlanBatchModal } from "@/components/common/ShippingPlanBatchModal"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; const DETAIL_TABLE = "sales_order_detail"; const MASTER_TABLE = "sales_order_mng"; @@ -62,12 +63,8 @@ export default function SalesOrderPage() { const [loading, setLoading] = useState(false); const [totalCount, setTotalCount] = useState(0); - // 검색 필터 상태 - const [searchOrderNo, setSearchOrderNo] = useState(""); - const [searchPartnerCode, setSearchPartnerCode] = useState("all"); - const [searchDateFrom, setSearchDateFrom] = useState(""); - const [searchDateTo, setSearchDateTo] = useState(""); - const [searchSellMode, setSearchSellMode] = useState("all"); + // 검색 필터 (DynamicSearchFilter에서 관리) + const [searchFilters, setSearchFilters] = useState([]); // 모달 const [isModalOpen, setIsModalOpen] = useState(false); @@ -188,12 +185,11 @@ export default function SalesOrderPage() { const fetchOrders = useCallback(async () => { setLoading(true); try { - const filters: any[] = []; - if (searchOrderNo) filters.push({ columnName: "order_no", operator: "contains", value: searchOrderNo }); - if (searchPartnerCode && searchPartnerCode !== "all") filters.push({ columnName: "partner_id", operator: "equals", value: searchPartnerCode }); - if (searchDateFrom) filters.push({ columnName: "order_date", operator: "gte", value: searchDateFrom }); - if (searchDateTo) filters.push({ columnName: "order_date", operator: "lte", value: searchDateTo }); - if (searchSellMode && searchSellMode !== "all") filters.push({ columnName: "sell_mode", operator: "equals", value: searchSellMode }); + const filters = searchFilters.map(f => ({ + columnName: f.columnName, + operator: f.operator, + value: f.value, + })); const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { page: 1, size: 500, @@ -260,7 +256,7 @@ export default function SalesOrderPage() { } finally { setLoading(false); } - }, [searchOrderNo, searchPartnerCode, searchDateFrom, searchDateTo, searchSellMode, categoryOptions]); + }, [searchFilters]); useEffect(() => { fetchOrders(); }, [fetchOrders]); @@ -580,75 +576,14 @@ export default function SalesOrderPage() { 수주관리 - {/* 검색 필터 바 */} -
-
- {/* 수주번호 */} -
- - setSearchOrderNo(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && fetchOrders()} - placeholder="SO-2026-0001" - className="h-9" - /> -
- {/* 거래처 */} -
- - -
- {/* 기간 */} -
- -
- setSearchDateFrom(e.target.value)} className="h-9 flex-1" /> - ~ - setSearchDateTo(e.target.value)} className="h-9 flex-1" /> -
-
- {/* 판매유형 */} -
- - -
- {/* 조회/초기화 버튼 */} -
- - -
-
-
+ {/* 검색 필터 (DynamicSearchFilter) */} + {/* 액션 바 */}
@@ -702,7 +637,7 @@ export default function SalesOrderPage() {
- +
- + - # - 품번 - 품명 - 규격 - 단위 - 기준단가 - 수량 - 단가 - 금액 - 통화 - 납기일 + # + 품번 + 품명 + 규격 + 단위 + 기준단가 + 수량 + 단가 + 금액 + 통화 + 납기일 @@ -1157,8 +1092,8 @@ export default function SalesOrderPage() {
- - + + - 품목코드 - 품명 - 규격 - 재질 - 단위 + 품목코드 + 품명 + 규격 + 재질 + 단위 @@ -1299,6 +1234,7 @@ export default function SalesOrderPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/sales/sales-item/page.tsx b/frontend/app/(main)/COMPANY_16/sales/sales-item/page.tsx index d95a79b6..1ccf6d2c 100644 --- a/frontend/app/(main)/COMPANY_16/sales/sales-item/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/sales-item/page.tsx @@ -26,7 +26,9 @@ import { toast } from "sonner"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; import { exportToExcel } from "@/lib/utils/excelExport"; -import { TableSettingsModal, TableSettings, loadTableSettings } from "@/components/common/TableSettingsModal"; +import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { useTableSettings } from "@/hooks/useTableSettings"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; const ITEM_TABLE = "item_info"; const MAPPING_TABLE = "customer_item_mapping"; @@ -39,9 +41,21 @@ const formatNum = (val: any): string => { return isNaN(n) ? String(val) : n.toLocaleString(); }; +const ITEM_GRID_COLUMNS = [ + { key: "item_number", label: "품번" }, + { key: "item_name", label: "품명" }, + { key: "size", label: "규격" }, + { key: "unit", label: "단위" }, + { key: "standard_price", label: "기준단가" }, + { key: "selling_price", label: "판매가격" }, + { key: "currency_code", label: "통화" }, + { key: "status", label: "상태" }, +]; + export default function SalesItemPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); + const ts = useTableSettings("c16-sales-item", ITEM_TABLE, ITEM_GRID_COLUMNS); // 좌측: 품목 const [items, setItems] = useState([]); @@ -49,9 +63,8 @@ export default function SalesItemPage() { const [itemCount, setItemCount] = useState(0); const [selectedItemId, setSelectedItemId] = useState(null); - // 검색 - const [inputKeyword, setInputKeyword] = useState(""); - const [searchKeyword, setSearchKeyword] = useState(""); + // 검색 필터 (DynamicSearchFilter에서 관리) + const [searchFilters, setSearchFilters] = useState([]); // 우측: 거래처 const [customerItems, setCustomerItems] = useState([]); @@ -77,9 +90,6 @@ export default function SalesItemPage() { // 엑셀 const [excelUploadOpen, setExcelUploadOpen] = useState(false); - // 테이블 설정 - const [tableSettingsOpen, setTableSettingsOpen] = useState(false); - const [filterConfig, setFilterConfig] = useState(); // 거래처 상세 입력 모달 (거래처 품번/품명 + 단가) const [custDetailOpen, setCustDetailOpen] = useState(false); @@ -93,16 +103,6 @@ export default function SalesItemPage() { }>>>({}); const [editCustData, setEditCustData] = useState(null); - // 테이블 설정 적용 - const applyTableSettings = useCallback((settings: TableSettings) => { - setFilterConfig(settings.filters); - }, []); - - // 마운트 시 저장된 설정 복원 - useEffect(() => { - const saved = loadTableSettings("sales-item"); - if (saved) applyTableSettings(saved); - }, []); // eslint-disable-line react-hooks/exhaustive-deps // 카테고리 로드 useEffect(() => { @@ -151,8 +151,9 @@ export default function SalesItemPage() { // 판매품목/영업관리 division 필터 (다중값 컬럼이므로 contains로 매칭) filters.push({ columnName: "division", operator: "contains", value: "CAT_ML8ZFVEL_1TOR" }); - if (searchKeyword) { - filters.push({ columnName: "item_name", operator: "contains", value: searchKeyword }); + // DynamicSearchFilter에서 전달된 필터 추가 + for (const f of searchFilters) { + filters.push({ columnName: f.columnName, operator: f.operator, value: f.value }); } const res = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, { @@ -177,14 +178,10 @@ export default function SalesItemPage() { } finally { setItemLoading(false); } - }, [searchKeyword, categoryOptions]); // eslint-disable-line react-hooks/exhaustive-deps + }, [searchFilters, categoryOptions]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { fetchItems(); }, [fetchItems]); - const handleSearch = () => { - setSearchKeyword(inputKeyword); - }; - // 선택된 품목 const selectedItem = items.find((i) => i.id === selectedItemId); @@ -611,33 +608,26 @@ export default function SalesItemPage() { return (
- {/* ── 검색 필터 바 ── */} -
-
- 품번/품명 - setInputKeyword(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSearch()} - placeholder="검색어를 입력해주세요" - className="h-8 w-[180px] text-sm" - /> -
- -
- - -
-
+ {/* 검색 필터 (DynamicSearchFilter) */} + + + +
+ } + /> {/* ── 마스터-디테일 분할 패널 ── */}
@@ -654,7 +644,7 @@ export default function SalesItemPage() { {itemCount}건
- @@ -670,8 +660,8 @@ export default function SalesItemPage() { ) : (
- - + + # 품번 품명 @@ -789,8 +779,8 @@ export default function SalesItemPage() { ) : (
- - + + 0 && customerCheckedIds.length === customerItems.length} @@ -945,8 +935,8 @@ export default function SalesItemPage() { {/* 검색 결과 테이블 */}
- - + + 0 && custCheckedIds.size === custSearchResults.length} @@ -1238,14 +1228,12 @@ export default function SalesItemPage() { {/* 테이블 설정 모달 */} { - applyTableSettings(settings); - setTableSettingsOpen(false); - }} + open={ts.open} + onOpenChange={ts.setOpen} + tableName={ts.tableName} + settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} + onSave={ts.applySettings} /> {/* 엑셀 업로드 */} diff --git a/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx index 5996525f..05c536f8 100644 --- a/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx @@ -10,7 +10,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; -import { Plus, Trash2, RotateCcw, Save, X, ChevronDown, ChevronRight, ChevronLeft, ChevronsLeft, ChevronsRight, Search, Loader2, FileSpreadsheet, Inbox, Settings2 } from "lucide-react"; +import { Plus, Trash2, Save, X, ChevronDown, ChevronRight, ChevronLeft, ChevronsLeft, ChevronsRight, Search, Loader2, FileSpreadsheet, Inbox, Settings2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { getShippingOrderList, @@ -24,6 +24,7 @@ import { import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; const GRID_COLUMNS = [ { key: "instruction_no", label: "출하지시번호" }, @@ -92,14 +93,8 @@ export default function ShippingOrderPage() { const [checkedIds, setCheckedIds] = useState([]); const [selectedOrderId, setSelectedOrderId] = useState(null); - // 검색 - const [searchKeyword, setSearchKeyword] = useState(""); - const [searchCustomer, setSearchCustomer] = useState(""); - const [debouncedKeyword, setDebouncedKeyword] = useState(""); - const [debouncedCustomer, setDebouncedCustomer] = useState(""); - const [searchStatus, setSearchStatus] = useState("all"); - const [searchDateFrom, setSearchDateFrom] = useState(""); - const [searchDateTo, setSearchDateTo] = useState(""); + // 검색 필터 (DynamicSearchFilter에서 관리) + const [searchFilters, setSearchFilters] = useState([]); // 엑셀 업로드 const [excelUploadOpen, setExcelUploadOpen] = useState(false); @@ -135,36 +130,25 @@ export default function ShippingOrderPage() { const [sourcePageSize, setSourcePageSize] = useState(20); const [sourceTotalCount, setSourceTotalCount] = useState(0); - // 텍스트 입력 debounce (500ms) - useEffect(() => { - const t = setTimeout(() => setDebouncedKeyword(searchKeyword), 500); - return () => clearTimeout(t); - }, [searchKeyword]); - - useEffect(() => { - const t = setTimeout(() => setDebouncedCustomer(searchCustomer), 500); - return () => clearTimeout(t); - }, [searchCustomer]); - - // 초기 날짜 - useEffect(() => { - const today = new Date(); - const from = new Date(today); - from.setMonth(from.getMonth() - 1); - setSearchDateFrom(from.toISOString().split("T")[0]); - setSearchDateTo(today.toISOString().split("T")[0]); - }, []); // 데이터 조회 const fetchOrders = useCallback(async () => { setLoading(true); try { const params: any = {}; - if (searchDateFrom) params.dateFrom = searchDateFrom; - if (searchDateTo) params.dateTo = searchDateTo; - if (searchStatus !== "all") params.status = searchStatus; - if (debouncedCustomer.trim()) params.customer = debouncedCustomer.trim(); - if (debouncedKeyword.trim()) params.keyword = debouncedKeyword.trim(); + for (const f of searchFilters) { + if (f.columnName === "ship_date" && f.operator === "between" && f.value) { + const [from, to] = f.value.split(","); + if (from) params.dateFrom = from; + if (to) params.dateTo = to; + } else if (f.columnName === "status") { + params.status = f.value; + } else if (f.columnName === "customer_name") { + params.customer = f.value; + } else { + params.keyword = f.value; + } + } const result = await getShippingOrderList(params); if (result.success) setOrders(result.data || []); @@ -173,10 +157,10 @@ export default function ShippingOrderPage() { } finally { setLoading(false); } - }, [searchDateFrom, searchDateTo, searchStatus, debouncedCustomer, debouncedKeyword]); + }, [searchFilters]); useEffect(() => { - if (searchDateFrom && searchDateTo) fetchOrders(); + fetchOrders(); }, [fetchOrders]); // 소스 데이터 조회 @@ -217,22 +201,6 @@ export default function ShippingOrderPage() { } }, [isModalOpen, dataSource]); - // 핸들러 - const handleSearch = () => fetchOrders(); - - const handleResetSearch = () => { - setSearchKeyword(""); - setSearchCustomer(""); - setDebouncedKeyword(""); - setDebouncedCustomer(""); - setSearchStatus("all"); - const today = new Date(); - const from = new Date(today); - from.setMonth(from.getMonth() - 1); - setSearchDateFrom(from.toISOString().split("T")[0]); - setSearchDateTo(today.toISOString().split("T")[0]); - }; - const handleCheckAll = (checked: boolean) => { setCheckedIds(checked ? orders.map((o: any) => o.id) : []); }; @@ -439,66 +407,14 @@ export default function ShippingOrderPage() { 출하지시 - {/* 검색 필터 */} -
-
-
- - setSearchKeyword(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSearch()} - /> -
-
- - setSearchCustomer(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSearch()} - /> -
-
- - -
-
- - setSearchDateFrom(e.target.value)} - className="w-[130px] h-9" - /> - ~ - setSearchDateTo(e.target.value)} - className="w-[130px] h-9" - /> -
-
- - -
-
-
+ {/* 검색 필터 (DynamicSearchFilter) */} + {/* 액션 바 */}
@@ -545,26 +461,26 @@ export default function ShippingOrderPage() {
) : (
- - + + 0 && checkedIds.length === orders.length} onCheckedChange={handleCheckAll} /> - {ts.isVisible("instruction_no") && 출하지시번호} - {ts.isVisible("ship_date") && 출하일자} - {ts.isVisible("customer_name") && 거래처명} - {ts.isVisible("transport_company") && 운송업체} - {ts.isVisible("vehicle_no") && 차량번호} - {ts.isVisible("driver_name") && 기사명} - {ts.isVisible("status") && 상태} - {ts.isVisible("item_code") && 품번} - {ts.isVisible("item_name") && 품명} - {ts.isVisible("qty") && 수량} - {ts.isVisible("source_type") && 소스} - {ts.isVisible("remark") && 비고} + {ts.isVisible("instruction_no") && 출하지시번호} + {ts.isVisible("ship_date") && 출하일자} + {ts.isVisible("customer_name") && 거래처명} + {ts.isVisible("transport_company") && 운송업체} + {ts.isVisible("vehicle_no") && 차량번호} + {ts.isVisible("driver_name") && 기사명} + {ts.isVisible("status") && 상태} + {ts.isVisible("item_code") && 품번} + {ts.isVisible("item_name") && 품명} + {ts.isVisible("qty") && 수량} + {ts.isVisible("source_type") && 소스} + {ts.isVisible("remark") && 비고} @@ -742,15 +658,15 @@ export default function ShippingOrderPage() { ) : (
- - - 선택 - 품번 - 품명 - 규격 - 거래처 - 수량 - {dataSource === "shipmentPlan" && 상태} + + + 선택 + 품번 + 품명 + 규격 + 거래처 + 수량 + {dataSource === "shipmentPlan" && 상태} @@ -951,14 +867,14 @@ export default function ShippingOrderPage() { ) : (
- - - 소스 - 품번 - 품명 - 출하수량 - 계획수량 - 삭제 + + + 소스 + 품번 + 품명 + 출하수량 + 계획수량 + 삭제 @@ -1039,6 +955,7 @@ export default function ShippingOrderPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/COMPANY_16/sales/shipping-plan/page.tsx b/frontend/app/(main)/COMPANY_16/sales/shipping-plan/page.tsx index f6088613..f9089bc9 100644 --- a/frontend/app/(main)/COMPANY_16/sales/shipping-plan/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/shipping-plan/page.tsx @@ -3,13 +3,12 @@ import React, { useState, useMemo, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; -import { Search, X, Save, RotateCcw, Loader2, Inbox, Settings2 } from "lucide-react"; +import { X, Save, Loader2, Inbox, Settings2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { getShipmentPlanList, @@ -18,6 +17,7 @@ import { } from "@/lib/api/shipping"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; const GRID_COLUMNS = [ { key: "order_no", label: "수주번호" }, @@ -65,12 +65,8 @@ export default function ShippingPlanPage() { const [selectedId, setSelectedId] = useState(null); const [checkedIds, setCheckedIds] = useState([]); - // 검색 - const [searchDateFrom, setSearchDateFrom] = useState(""); - const [searchDateTo, setSearchDateTo] = useState(""); - const [searchStatus, setSearchStatus] = useState("all"); - const [searchCustomer, setSearchCustomer] = useState(""); - const [searchKeyword, setSearchKeyword] = useState(""); + // 검색 필터 (DynamicSearchFilter에서 관리) + const [searchFilters, setSearchFilters] = useState([]); // 상세 패널 편집 const [editPlanQty, setEditPlanQty] = useState(""); @@ -79,27 +75,25 @@ export default function ShippingPlanPage() { const [isDetailChanged, setIsDetailChanged] = useState(false); const [saving, setSaving] = useState(false); - // 날짜 초기화 - useEffect(() => { - const today = new Date(); - const threeMonthsAgo = new Date(today); - threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); - const oneMonthLater = new Date(today); - oneMonthLater.setMonth(oneMonthLater.getMonth() + 1); - setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); - setSearchDateTo(oneMonthLater.toISOString().split("T")[0]); - }, []); // 데이터 조회 const fetchData = useCallback(async () => { setLoading(true); try { const params: any = {}; - if (searchDateFrom) params.dateFrom = searchDateFrom; - if (searchDateTo) params.dateTo = searchDateTo; - if (searchStatus !== "all") params.status = searchStatus; - if (searchCustomer.trim()) params.customer = searchCustomer.trim(); - if (searchKeyword.trim()) params.keyword = searchKeyword.trim(); + for (const f of searchFilters) { + if (f.columnName === "plan_date" && f.operator === "between" && f.value) { + const [from, to] = f.value.split(","); + if (from) params.dateFrom = from; + if (to) params.dateTo = to; + } else if (f.columnName === "status") { + params.status = f.value; + } else if (f.columnName === "customer_name") { + params.customer = f.value; + } else { + params.keyword = f.value; + } + } const result = await getShipmentPlanList(params); if (result.success) { @@ -110,29 +104,12 @@ export default function ShippingPlanPage() { } finally { setLoading(false); } - }, [searchDateFrom, searchDateTo, searchStatus, searchCustomer, searchKeyword]); + }, [searchFilters]); - // 초기 로드 + 검색 시 자동 조회 + // searchFilters 변경 시 자동 조회 useEffect(() => { - if (searchDateFrom && searchDateTo) { - fetchData(); - } - }, [searchDateFrom, searchDateTo]); - - const handleSearch = () => fetchData(); - - const handleResetSearch = () => { - setSearchStatus("all"); - setSearchCustomer(""); - setSearchKeyword(""); - const today = new Date(); - const threeMonthsAgo = new Date(today); - threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); - const oneMonthLater = new Date(today); - oneMonthLater.setMonth(oneMonthLater.getMonth() + 1); - setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); - setSearchDateTo(oneMonthLater.toISOString().split("T")[0]); - }; + fetchData(); + }, [fetchData]); const selectedPlan = useMemo(() => data.find(p => p.id === selectedId), [data, selectedId]); @@ -225,74 +202,14 @@ export default function ShippingPlanPage() { 출하계획 - {/* 검색 필터 */} -
-
-
- - setSearchDateFrom(e.target.value)} - className="w-[130px] h-9" - /> - ~ - setSearchDateTo(e.target.value)} - className="w-[130px] h-9" - /> -
- -
- - -
- -
- - setSearchCustomer(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSearch()} - /> -
- -
- - setSearchKeyword(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSearch()} - /> -
- -
- - -
-
-
+ {/* 검색 필터 (DynamicSearchFilter) */} + {/* 마스터-디테일 */}
@@ -317,23 +234,23 @@ export default function ShippingPlanPage() { {/* 테이블 */}
- - + + 0 && checkedIds.length === data.filter(p => p.status !== "CANCELLED").length} onCheckedChange={handleCheckAll} /> - {ts.isVisible("order_no") && 수주번호} - {ts.isVisible("due_date") && 납기일} - {ts.isVisible("customer_name") && 거래처} - {ts.isVisible("part_code") && 품목코드} - {ts.isVisible("part_name") && 품목명} - {ts.isVisible("order_qty") && 수주수량} - {ts.isVisible("plan_qty") && 계획수량} - {ts.isVisible("plan_date") && 계획일} - {ts.isVisible("status") && 상태} + {ts.isVisible("order_no") && 수주번호} + {ts.isVisible("due_date") && 납기일} + {ts.isVisible("customer_name") && 거래처} + {ts.isVisible("part_code") && 품목코드} + {ts.isVisible("part_name") && 품목명} + {ts.isVisible("order_qty") && 수주수량} + {ts.isVisible("plan_qty") && 계획수량} + {ts.isVisible("plan_date") && 계획일} + {ts.isVisible("status") && 상태} @@ -582,6 +499,7 @@ export default function ShippingPlanPage() { onOpenChange={ts.setOpen} tableName={ts.tableName} settingsId={ts.settingsId} + defaultVisibleKeys={ts.defaultVisibleKeys} onSave={ts.applySettings} /> diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 799c7d4f..9cfb0667 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -508,10 +508,10 @@ select { font-family: "Gaegu", cursive; } -/* ===== 전체 폰트 사이즈 16px 통일 (버튼 제외) ===== */ +/* ===== 전체 폰트 사이즈 16px 통일 (버튼 제외) 작업자 김주석 ===== body *:not(button, [role="button"], .kpi-dynamic-font) { font-size: 16px !important; -} +} */ body button *, body [role="button"] * { @@ -782,14 +782,32 @@ body [role="button"] * { line-height: 1.5 !important; } -/* 헤더 패딩 맞춤 */ +/* 헤더 */ [data-slot="table-head"] { padding: 12px 16px !important; - font-size: 12px !important; + font-size: 14px !important; font-weight: 700 !important; letter-spacing: 0.03em !important; } +/* 다크모드 체크박스 — input[type=checkbox] + shadcn Checkbox 모두 커버 */ +.dark [data-slot="table-cell"] input[type="checkbox"], +.dark [data-slot="table-head"] input[type="checkbox"] { + background-color: transparent !important; + border: 2px solid hsl(0 0% 85%) !important; + accent-color: hsl(var(--primary)) !important; +} +.dark [data-slot="table-cell"] button[role="checkbox"], +.dark [data-slot="table-head"] button[role="checkbox"] { + border: 2px solid hsl(0 0% 85%) !important; + background-color: transparent !important; +} +.dark [data-slot="table-cell"] button[role="checkbox"][data-state="checked"], +.dark [data-slot="table-head"] button[role="checkbox"][data-state="checked"] { + border-color: hsl(var(--primary)) !important; + background-color: hsl(var(--primary)) !important; +} + /* 짝수 행 stripe — 행 구분 */ [data-slot="table-body"] [data-slot="table-row"]:nth-child(even) { background-color: hsl(var(--muted) / 0.35); diff --git a/frontend/components/common/TableSettingsModal.tsx b/frontend/components/common/TableSettingsModal.tsx index 7b7eabe0..084cfd76 100644 --- a/frontend/components/common/TableSettingsModal.tsx +++ b/frontend/components/common/TableSettingsModal.tsx @@ -76,6 +76,8 @@ export interface TableSettingsModalProps { onSave?: (settings: TableSettings) => void; /** 초기 탭 */ initialTab?: "columns" | "filters" | "groups"; + /** 기본 표시 컬럼 키 목록 (GRID_COLUMNS 기준). 미지정 시 전체 표시 */ + defaultVisibleKeys?: string[]; } // ===== 상수 ===== @@ -204,6 +206,7 @@ export function TableSettingsModal({ settingsId, onSave, initialTab = "columns", + defaultVisibleKeys, }: TableSettingsModalProps) { const [activeTab, setActiveTab] = useState(initialTab); const [loading, setLoading] = useState(false); @@ -241,7 +244,7 @@ export function TableSettingsModal({ .map((t) => ({ columnName: t.columnName, displayName: t.displayName || t.columnLabel || t.columnName, - visible: true, + visible: defaultVisibleKeys ? defaultVisibleKeys.includes(t.columnName) : true, width: 120, })); diff --git a/frontend/components/layout/AdminPageRenderer.tsx b/frontend/components/layout/AdminPageRenderer.tsx index db0ef561..b9392659 100644 --- a/frontend/components/layout/AdminPageRenderer.tsx +++ b/frontend/components/layout/AdminPageRenderer.tsx @@ -116,22 +116,10 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_7/design/my-work": dynamic(() => import("@/app/(main)/COMPANY_7/design/my-work/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/design/design-request": dynamic(() => import("@/app/(main)/COMPANY_7/design/design-request/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/design/task-management": dynamic(() => import("@/app/(main)/COMPANY_7/design/task-management/page"), { ssr: false, loading: LoadingFallback }), - // === COMPANY_7 신규 개발 화면 === - "/COMPANY_7/purchase/order": dynamic(() => import("@/app/(main)/COMPANY_7/purchase/order/page"), { ssr: false, loading: LoadingFallback }), - "/COMPANY_7/purchase/purchase-item": dynamic(() => import("@/app/(main)/COMPANY_7/purchase/purchase-item/page"), { ssr: false, loading: LoadingFallback }), - "/COMPANY_7/purchase/supplier": dynamic(() => import("@/app/(main)/COMPANY_7/purchase/supplier/page"), { ssr: false, loading: LoadingFallback }), - "/COMPANY_7/logistics/inventory": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/inventory/page"), { ssr: false, loading: LoadingFallback }), - "/COMPANY_7/logistics/warehouse": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/warehouse/page"), { ssr: false, loading: LoadingFallback }), - "/COMPANY_7/logistics/info": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/info/page"), { ssr: false, loading: LoadingFallback }), - "/COMPANY_7/quality/inspection": dynamic(() => import("@/app/(main)/COMPANY_7/quality/inspection/page"), { ssr: false, loading: LoadingFallback }), - "/COMPANY_7/quality/item-inspection": dynamic(() => import("@/app/(main)/COMPANY_7/quality/item-inspection/page"), { ssr: false, loading: LoadingFallback }), - "/COMPANY_7/equipment/plc-settings": dynamic(() => import("@/app/(main)/COMPANY_7/equipment/plc-settings/page"), { ssr: false, loading: LoadingFallback }), - "/COMPANY_7/master-data/company": dynamic(() => import("@/app/(main)/COMPANY_7/master-data/company/page"), { ssr: false, loading: LoadingFallback }), - "/COMPANY_7/mold/info": dynamic(() => import("@/app/(main)/COMPANY_7/mold/info/page"), { ssr: false, loading: LoadingFallback }), // === COMPANY_16 (하이큐마그) === + "/COMPANY_16/master-data/options": dynamic(() => import("@/app/(main)/COMPANY_16/master-data/options/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/master-data/item-info": dynamic(() => import("@/app/(main)/COMPANY_16/master-data/item-info/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/master-data/department": dynamic(() => import("@/app/(main)/COMPANY_16/master-data/department/page"), { ssr: false, loading: LoadingFallback }), - "/COMPANY_16/master-data/options": dynamic(() => import("@/app/(main)/COMPANY_16/master-data/options/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/master-data/company": dynamic(() => import("@/app/(main)/COMPANY_16/master-data/company/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/sales/order": dynamic(() => import("@/app/(main)/COMPANY_16/sales/order/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/sales/customer": dynamic(() => import("@/app/(main)/COMPANY_16/sales/customer/page"), { ssr: false, loading: LoadingFallback }), @@ -439,6 +427,9 @@ const COMPANY_PAGE_PREFIXES = [ "/logistics/", "/outsourcing/", "/design/", + "/purchase/", + "/quality/", + "/mold/", ]; function isCompanyPage(url: string): boolean { diff --git a/frontend/hooks/useTableSettings.ts b/frontend/hooks/useTableSettings.ts new file mode 100644 index 00000000..ef4371de --- /dev/null +++ b/frontend/hooks/useTableSettings.ts @@ -0,0 +1,153 @@ +"use client"; + +/** + * useTableSettings — 날코딩 페이지용 테이블 설정 훅 + * + * TableSettingsModal과 함께 사용하여 컬럼 표시/숨김, 순서, 너비를 관리합니다. + * 설정은 localStorage에 자동 저장/복원됩니다. + * + * @example + * const ts = useTableSettings("item-info", TABLE_NAME, GRID_COLUMNS); + * + * // 툴바 버튼 + * + * + * // 테이블 헤더 — GRID_COLUMNS 대신 ts.visibleColumns 사용 + * {ts.visibleColumns.map(col => {col.label})} + * + * // 모달 (JSX 하단) + * + */ + +import { useState, useEffect, useCallback, useMemo } from "react"; +import { loadTableSettings, type TableSettings } from "@/components/common/TableSettingsModal"; + +export function useTableSettings( + settingsId: string, + tableName: string, + defaultColumns: T[], +) { + const [open, setOpen] = useState(false); + const [visibleKeys, setVisibleKeys] = useState>( + () => new Set(defaultColumns.map((c) => c.key)), + ); + const [columnWidths, setColumnWidths] = useState>({}); + const [orderedKeys, setOrderedKeys] = useState( + () => defaultColumns.map((c) => c.key), + ); + // 초기 filterConfig: GRID_COLUMNS에 있는 컬럼만 필터 가능 (전부 비활성) + const [filterConfig, setFilterConfig] = useState( + () => + defaultColumns.map((c) => ({ + columnName: c.key, + displayName: (c as any).label || c.key, + enabled: false, + filterType: "text" as const, + width: 25, + })), + ); + + /** TableSettingsModal onSave에 전달할 콜백 */ + const applySettings = useCallback( + (settings: TableSettings) => { + const visible = new Set(); + const widths: Record = {}; + const order: string[] = []; + + for (const cs of settings.columns) { + if (cs.visible) { + visible.add(cs.columnName); + widths[cs.columnName] = cs.width; + order.push(cs.columnName); + } + } + + // settings에 없는 새 컬럼은 보이도록 추가 + for (const col of defaultColumns) { + if (!settings.columns.find((c) => c.columnName === col.key)) { + visible.add(col.key); + order.push(col.key); + } + } + + setVisibleKeys(visible); + setColumnWidths(widths); + setOrderedKeys(order); + + // 화면에 표시된 컬럼만 필터 가능하도록 제한 + setFilterConfig( + settings.filters?.filter((f) => visible.has(f.columnName)), + ); + }, + [defaultColumns], + ); + + // 마운트 시 저장된 설정 복원 + useEffect(() => { + const saved = loadTableSettings(settingsId); + if (saved) applySettings(saved); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + /** 설정이 적용된 컬럼 목록 (순서 + 표시 필터 적용) */ + const visibleColumns = useMemo((): T[] => { + const colMap = new Map(defaultColumns.map((c) => [c.key, c])); + const result: T[] = []; + + // 저장된 순서대로 + for (const key of orderedKeys) { + if (visibleKeys.has(key)) { + const col = colMap.get(key); + if (col) result.push(col); + } + } + + // orderedKeys에 없는 컬럼 (새로 추가된 것) + for (const col of defaultColumns) { + if (!orderedKeys.includes(col.key) && visibleKeys.has(col.key)) { + result.push(col); + } + } + + return result.length > 0 ? result : defaultColumns; + }, [defaultColumns, orderedKeys, visibleKeys]); + + /** 컬럼 표시 여부 확인 */ + const isVisible = useCallback((key: string) => visibleKeys.has(key), [visibleKeys]); + + /** 컬럼 너비 가져오기 (설정값 or undefined) */ + const getWidth = useCallback( + (key: string): number | undefined => columnWidths[key], + [columnWidths], + ); + + return { + /** 모달 open 상태 */ + open, + /** 모달 open 상태 setter */ + setOpen, + /** web-types API 호출용 테이블명 */ + tableName, + /** localStorage 키 */ + settingsId, + /** TableSettingsModal onSave 콜백 */ + applySettings, + /** 설정 적용된 컬럼 배열 (순서 + 표시 필터) */ + visibleColumns, + /** 특정 컬럼 표시 여부 */ + isVisible, + /** 특정 컬럼 너비 (px) */ + getWidth, + /** 필터 설정 */ + filterConfig, + /** GRID_COLUMNS 기본 컬럼 키 목록 (TableSettingsModal defaultVisibleKeys용) */ + defaultVisibleKeys: defaultColumns.map((c) => c.key), + }; +}