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 00fedd3f..39f06769 100644 --- a/frontend/app/(main)/COMPANY_16/design/change-management/page.tsx +++ b/frontend/app/(main)/COMPANY_16/design/change-management/page.tsx @@ -57,6 +57,7 @@ import { import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; // --- Types --- type ChangeType = "설계오류" | "원가절감" | "고객요청" | "공정개선" | "법규대응"; @@ -866,185 +867,52 @@ export default function DesignChangeManagementPage() {
{currentTab === "ecr" ? ( - - - - No - {tsEcr.visibleColumns.map((col) => ( - - {col.label} - - ))} - - - - {filteredEcr.length === 0 ? ( - - -
- - 조건에 맞는 ECR이 없어요 -
-
-
- ) : ( - filteredEcr.map((item, idx) => ( - handleRowClick(item.id)} - > - {idx + 1} - {tsEcr.isVisible("request_no") && {item.id}} - {tsEcr.isVisible("change_type") && ( - - - {item.changeType} - - - )} - {tsEcr.isVisible("status") && ( - - - {item.status} - - - )} - {tsEcr.isVisible("urgency") && ( - - {item.urgency === "긴급" ? ( - - 긴급 - - ) : ( - "-" - )} - - )} - {tsEcr.isVisible("target_name") && {item.target}} - {tsEcr.isVisible("drawing_no") && {item.drawingNo}} - {tsEcr.isVisible("req_dept") && {item.reqDept}} - {tsEcr.isVisible("requester") && {item.requester}} - {tsEcr.isVisible("request_date") && {item.date}} - {tsEcr.isVisible("ecn_no") && ( - - {item.ecnNo ? ( - - ) : ( - "-" - )} - - )} - - )) - )} -
-
+ {val} }, + { key: "changeType", label: "변경유형", width: "w-[90px]", align: "center" as const, render: (val: any) => {val} }, + { key: "status", label: "상태", width: "w-[90px]", align: "center" as const, render: (val: any) => {val} }, + { key: "urgency", label: "긴급", width: "w-[60px]", align: "center" as const, render: (val: any) => val === "긴급" ? 긴급 : - }, + { key: "target", label: "대상 품목/설비", width: "w-[200px]" }, + { key: "drawingNo", label: "도면번호", width: "w-[150px]" }, + { key: "reqDept", label: "요청부서", width: "w-[80px]" }, + { key: "requester", label: "요청자", width: "w-[70px]" }, + { key: "date", label: "요청일자", width: "w-[100px]" }, + { key: "ecnNo", label: "관련 ECN", width: "w-[130px]", render: (val: any) => val ? : - }, + ] as EDataTableColumn[]} + data={tsEcr.groupData(filteredEcr)} + rowKey={(row) => row.id} + selectedId={selectedId} + onSelect={(id) => { if (id) handleRowClick(id); }} + onRowClick={(row) => handleRowClick(row.id)} + emptyMessage="조건에 맞는 ECR이 없어요" + showRowNumber + showPagination={false} + draggableColumns={false} + /> ) : ( - - - - No - {tsEcn.visibleColumns.map((col) => ( - - {col.label} - - ))} - - - - {filteredEcn.length === 0 ? ( - - -
- - 조건에 맞는 ECN이 없어요 -
-
-
- ) : ( - filteredEcn.map((item, idx) => ( - handleRowClick(item.id)} - > - {idx + 1} - {tsEcn.isVisible("ecn_no") && {item.id}} - {tsEcn.isVisible("status") && ( - - - {item.status} - - - )} - {tsEcn.isVisible("target") && {item.target}} - {tsEcn.isVisible("drawing_after") && {item.drawingAfter}} - {tsEcn.isVisible("designer") && {item.designer}} - {tsEcn.isVisible("ecn_date") && {item.date}} - {tsEcn.isVisible("apply_date") && {item.applyDate}} - {tsEcn.isVisible("notify_depts") && {item.notifyDepts.join(", ")}} - {tsEcn.isVisible("ecr_id") && ( - - - - )} - - )) - )} -
-
+ {val} }, + { key: "status", label: "상태", width: "w-[90px]", align: "center" as const, render: (val: any) => {val} }, + { key: "target", label: "대상 품목/설비", width: "w-[200px]" }, + { key: "drawingAfter", label: "도면 (변경 후)", width: "w-[160px]", render: (val: any) => {val} }, + { key: "designer", label: "설계담당", width: "w-[80px]" }, + { key: "date", label: "발행일자", width: "w-[100px]" }, + { key: "applyDate", label: "적용일자", width: "w-[100px]" }, + { key: "notifyDepts", label: "통보 부서", width: "w-[140px]", render: (val: any) => {Array.isArray(val) ? val.join(", ") : val} }, + { key: "ecrNo", label: "관련 ECR", width: "w-[130px]", render: (val: any) => }, + ] as EDataTableColumn[]} + data={tsEcn.groupData(filteredEcn)} + rowKey={(row) => row.id} + selectedId={selectedId} + onSelect={(id) => { if (id) handleRowClick(id); }} + onRowClick={(row) => handleRowClick(row.id)} + emptyMessage="조건에 맞는 ECN이 없어요" + showRowNumber + showPagination={false} + draggableColumns={false} + /> )}
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 3e2965d7..e83c22b3 100644 --- a/frontend/app/(main)/COMPANY_16/design/design-request/page.tsx +++ b/frontend/app/(main)/COMPANY_16/design/design-request/page.tsx @@ -44,6 +44,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; import { getDesignRequestList, createDesignRequest, @@ -460,95 +461,42 @@ export default function DesignRequestPage() { {/* 테이블 영역 */}
-
- {loading ? ( -
- - 불러오는 중... -
- ) : ( - - - - {ts.visibleColumns.map((col) => ( - - {col.label} - - ))} - - - - {filteredRequests.length === 0 && ( - - -
- - 등록된 설계의뢰가 없어요 -
-
-
- )} - {filteredRequests.map((item) => { - const progress = getProgress(item.status); + + columns={ts.visibleColumns.map((col): EDataTableColumn => ({ + key: col.key, + label: col.label, + width: col.key === "request_no" ? "w-[100px]" : col.key === "design_type" ? "w-[70px]" : col.key === "status" ? "w-[70px]" : col.key === "priority" ? "w-[60px]" : col.key === "customer" ? "w-[90px]" : col.key === "designer" ? "w-[70px]" : col.key === "due_date" ? "w-[85px]" : col.key === "progress" ? "w-[65px]" : undefined, + align: (col.key === "design_type" || col.key === "status" || col.key === "priority" || col.key === "progress") ? "center" : undefined, + render: col.key === "request_no" + ? (val: any) => {val || "-"} + : col.key === "design_type" + ? (val: any) => val ? {val} : - + : col.key === "status" + ? (val: any) => {val} + : col.key === "priority" + ? (val: any) => {val} + : col.key === "progress" + ? (_val: any, row: DesignRequest) => { + const progress = STATUS_PROGRESS[row.status] ?? 0; return ( - handleRowClick(item.id)} - > - {ts.isVisible("request_no") && {item.request_no || "-"}} - {ts.isVisible("design_type") && ( - - {item.design_type ? ( - {item.design_type} - ) : "-"} - - )} - {ts.isVisible("status") && ( - - {item.status} - - )} - {ts.isVisible("priority") && ( - - {item.priority} - - )} - {ts.isVisible("target_name") && {item.target_name || "-"}} - {ts.isVisible("customer") && {item.customer || "-"}} - {ts.isVisible("designer") && {item.designer || "-"}} - {ts.isVisible("due_date") && {item.due_date || "-"}} - {ts.isVisible("progress") && ( - -
-
-
-
- {progress}% -
- - )} - +
+
+
+
+ {progress}% +
); - })} - -
- )} -
+ } + : undefined, + }))} + data={ts.groupData(filteredRequests)} + loading={loading} + emptyMessage="등록된 설계의뢰가 없어요" + selectedId={selectedId} + onSelect={(id) => setSelectedId(id)} + onRowClick={(row) => handleRowClick(row.id)} + draggableColumns={false} + />
{/* 상세 정보 다이얼로그 */} 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 e0092b21..8c136cb9 100644 --- a/frontend/app/(main)/COMPANY_16/design/my-work/page.tsx +++ b/frontend/app/(main)/COMPANY_16/design/my-work/page.tsx @@ -76,6 +76,7 @@ import { import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { toast } from "sonner"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; import { useAuth } from "@/hooks/useAuth"; import { getMyWork, @@ -1244,66 +1245,50 @@ export default function MyWorkPage() { )} {viewMode === "list" && ( - - - - 프로젝트 - 업무명 - 유형 - 상태 - 종료일 - 진행률 - - - - {filteredTasks - .sort((a, b) => { - const ad = a.status !== "완료" && new Date(a.end) < today; - const bd = b.status !== "완료" && new Date(b.end) < today; - if (ad && !bd) return -1; - if (!ad && bd) return 1; - const ord: Record = { 진행중: 0, 대기: 1, 검토중: 2, 완료: 3 }; - return (ord[a.status] ?? 9) - (ord[b.status] ?? 9); - }) - .map((t) => { - const isDelay = t.status !== "완료" && new Date(t.end) < today; - const displayStatus = isDelay ? "지연" : t.status; - const isSelected = selectedTaskKey === `${t.projectId}||${t.name}`; - return ( - handleSelectTask(t.projectId, t.name)} - > - - {t.projectId} -
- {t.projectName} -
- {t.name} - {t.category} - - {displayStatus} - - {t.end} - -
-
-
-
- {t.progress}% -
- - - ); - })} - {filteredTasks.length === 0 && ( - - 검색 결과가 없어요 - - )} - -
+ ( +
+ {row.projectId} +
+ {row.projectName} +
+ )}, + { key: "name", label: "업무명" }, + { key: "category", label: "유형", width: "w-[65px]" }, + { key: "status", label: "상태", width: "w-[55px]", align: "center", render: (_v, row) => { + const isDelay = row.status !== "완료" && new Date(row.end) < today; + const displayStatus = isDelay ? "지연" : row.status; + return {displayStatus}; + }}, + { key: "end", label: "종료일", width: "w-[80px]", render: (v, row) => { + const isDelay = row.status !== "완료" && new Date(row.end) < today; + return {v}; + }}, + { key: "progress", label: "진행률", width: "w-[70px]", sortable: true, render: (v) => ( +
+
+
+
+ {v}% +
+ )}, + ] as EDataTableColumn[]} + data={[...filteredTasks].sort((a, b) => { + const ad = a.status !== "완료" && new Date(a.end) < today; + const bd = b.status !== "완료" && new Date(b.end) < today; + if (ad && !bd) return -1; + if (!ad && bd) return 1; + const ord: Record = { 진행중: 0, 대기: 1, 검토중: 2, 완료: 3 }; + return (ord[a.status] ?? 9) - (ord[b.status] ?? 9); + })} + rowKey={(row) => `${row.projectId}||${row.name}`} + selectedId={selectedTaskKey} + onRowClick={(row) => handleSelectTask(row.projectId, row.name)} + emptyMessage="검색 결과가 없어요" + showPagination={false} + draggableColumns={false} + /> )} {viewMode === "timesheet" && ( diff --git a/frontend/app/(main)/COMPANY_16/design/project/page.tsx b/frontend/app/(main)/COMPANY_16/design/project/page.tsx index 1aaafc9f..7e66b696 100644 --- a/frontend/app/(main)/COMPANY_16/design/project/page.tsx +++ b/frontend/app/(main)/COMPANY_16/design/project/page.tsx @@ -63,6 +63,7 @@ import { import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; // --- Types --- type ProjectStatus = "진행중" | "계획" | "보류" | "완료"; @@ -728,133 +729,63 @@ export default function DesignProjectPage() {
- - - - {ts.visibleColumns.map((col) => ( - - {col.label} - - ))} - - - - {loading ? ( - - - -
로딩 중...
-
-
- ) : treeRows.length === 0 ? ( - - -
- - 조건에 맞는 프로젝트가 없어요 -
-
-
- ) : ( - treeRows.map(({ project: p, depth }) => { - const hasChildren = filteredProjects.some((c) => c.parentId === p.id); - const isExpanded = expandedIds[p.id] !== false; - const childCount = getAllDescendants(projects, p.id).length; - - return ( - = 2 && "bg-muted/15" - )} - onClick={() => { - setSelectedId(p.id); - setDetailTab("wbs"); - fetchTaskDetails(p.id); - }} - > - {ts.isVisible("project_no") && ( - -
- {hasChildren ? ( - - ) : ( - - )} - - {p.projectNo} - - {p.relation && ( - - {getRelationLabel(p.relation)} - - )} -
-
- )} - {ts.isVisible("status") && ( - - - {p.status} - - - )} - {ts.isVisible("name") && ( - - {p.name} - {childCount > 0 && ( - - {childCount} - - )} - - )} - {ts.isVisible("pm") && {p.pm}} - {ts.isVisible("customer") && {p.customer}} - {ts.isVisible("start_date") && {p.startDate}} - {ts.isVisible("end_date") && {p.endDate}} - {ts.isVisible("progress") && ( - -
-
-
-
- {p.progress}% -
- - )} - {ts.isVisible("source_no") && {p.sourceNo || "-"}} - - ); - }) - )} - -
+ { + const depth = row._depth ?? 0; + const hasChildren = filteredProjects.some((c) => c.parentId === row.id); + const isExpanded = expandedIds[row.id] !== false; + return ( +
+ {hasChildren ? ( + + ) : ()} + {row.projectNo} + {row.relation && ({getRelationLabel(row.relation)})} +
+ ); + }}, + { key: "status", label: "상태", width: "w-[80px]", align: "center" as const, render: (val: any) => {val} }, + { key: "name", label: "프로젝트명", width: "w-[200px]", render: (val: any, row: any) => { + const childCount = getAllDescendants(projects, row.id).length; + return (<>{val}{childCount > 0 && {childCount}}); + }}, + { key: "pm", label: "PM", width: "w-[70px]" }, + { key: "customer", label: "고객", width: "w-[80px]" }, + { key: "startDate", label: "시작일", width: "w-[90px]" }, + { key: "endDate", label: "종료예정", width: "w-[90px]" }, + { key: "progress", label: "진행률", width: "w-[100px]", align: "center" as const, render: (val: any) => ( +
+
+
+
+ {val}% +
+ )}, + { key: "sourceNo", label: "원접수번호", width: "w-[90px]", render: (val: any) => {val || "-"} }, + ] as EDataTableColumn[]} + data={ts.groupData(treeRows.map(({ project: p, depth }) => ({ ...p, _depth: depth })))} + rowKey={(row) => row.id} + loading={loading} + emptyMessage="조건에 맞는 프로젝트가 없어요" + selectedId={selectedId} + onSelect={(id) => { + if (id) { + setSelectedId(id); + setDetailTab("wbs"); + fetchTaskDetails(id); + } + }} + onRowClick={(row) => { + setSelectedId(row.id); + setDetailTab("wbs"); + fetchTaskDetails(row.id); + }} + showPagination={false} + draggableColumns={false} + />
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 de76142f..f5603e46 100644 --- a/frontend/app/(main)/COMPANY_16/design/task-management/page.tsx +++ b/frontend/app/(main)/COMPANY_16/design/task-management/page.tsx @@ -57,6 +57,7 @@ import { Users, Settings2, } from "lucide-react"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { @@ -749,108 +750,49 @@ export default function DesignTaskManagementPage() { -
- - - - {ts.visibleColumns.map((col) => ( - - {col.label} - - ))} - - - - {loading && allTasks.length === 0 ? ( - - -
- - 로딩 중... -
-
-
- ) : filteredData.length === 0 ? ( - - -
- - 조건에 맞는 업무가 없어요 -
-
-
- ) : ( - filteredData.map((item) => ( - handleSelectTask(item.dbId)} - > - {ts.isVisible("source_type") && ( - - - {item.sourceType === "dr" ? "DR" : "ECR"} - - - )} - {ts.isVisible("request_no") && ( - - {item.id} - - )} - {ts.isVisible("status") && ( - - - {item.status} - - - )} - {ts.isVisible("priority") && ( - - - {item.priority} - - - )} - {ts.isVisible("target_name") && {item.targetName}} - {ts.isVisible("req_dept") && {item.reqDept}} - {ts.isVisible("requester") && {item.requester}} - {ts.isVisible("request_date") && {item.date}} - {ts.isVisible("due_date") && {item.dueDate}} - {ts.isVisible("designer") && ( - - {item.designer || 미배정} - - )} - - )) - )} -
-
-
+ + columns={ts.visibleColumns.map((col): EDataTableColumn => ({ + key: col.key === "request_no" ? "id" : col.key === "target_name" ? "targetName" : col.key === "req_dept" ? "reqDept" : col.key === "request_date" ? "date" : col.key === "due_date" ? "dueDate" : col.key === "source_type" ? "sourceType" : col.key, + label: col.label, + width: col.key === "source_type" ? "w-[60px]" : col.key === "request_no" ? "w-[130px]" : col.key === "status" ? "w-[90px]" : col.key === "priority" ? "w-[80px]" : col.key === "target_name" ? "min-w-[180px]" : col.key === "req_dept" ? "w-[90px]" : col.key === "requester" ? "w-[80px]" : col.key === "request_date" ? "w-[100px]" : col.key === "due_date" ? "w-[100px]" : col.key === "designer" ? "w-[80px]" : undefined, + align: (col.key === "source_type" || col.key === "status" || col.key === "priority") ? "center" : undefined, + render: col.key === "source_type" + ? (val: any, row: TaskItem) => ( + + {row.sourceType === "dr" ? "DR" : "ECR"} + + ) + : col.key === "request_no" + ? (val: any, row: TaskItem) => ( + + {val} + + ) + : col.key === "status" + ? (val: any) => ( + + {val} + + ) + : col.key === "priority" + ? (val: any) => ( + + {val} + + ) + : col.key === "designer" + ? (val: any) => val ? {val} : 미배정 + : undefined, + }))} + data={ts.groupData(filteredData)} + loading={loading} + emptyMessage="조건에 맞는 업무가 없어요" + rowKey={(row) => row.dbId} + selectedId={selectedTaskId} + onSelect={(id) => handleSelectTask(id ?? "")} + draggableColumns={false} + showPagination={false} + /> diff --git a/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx index 314db047..553e441e 100644 --- a/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx @@ -8,7 +8,7 @@ * 점검항목 복사 기능 포함 */ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -34,6 +34,7 @@ import { ImageUpload } from "@/components/common/ImageUpload"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const EQUIP_TABLE = "equipment_mng"; const INSPECTION_TABLE = "equipment_inspection_item"; @@ -138,6 +139,17 @@ export default function EquipmentInfoPage() { return catOptions[col]?.find((o) => o.code === code)?.label || code; }; + const mainTableColumns = useMemo(() => { + const cols: EDataTableColumn[] = []; + if (ts.isVisible("equipment_code")) cols.push({ key: "equipment_code", label: "설비코드", width: "w-[110px]" }); + if (ts.isVisible("equipment_name")) cols.push({ key: "equipment_name", label: "설비명", minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }); + if (ts.isVisible("equipment_type")) cols.push({ key: "equipment_type", label: "설비유형", width: "w-[90px]", render: (v) => v || "-" }); + if (ts.isVisible("manufacturer")) cols.push({ key: "manufacturer", label: "제조사", width: "w-[100px]", render: (v) => v || "-" }); + if (ts.isVisible("installation_location")) cols.push({ key: "installation_location", label: "설치장소", width: "w-[100px]", render: (v) => v || "-" }); + if (ts.isVisible("operation_status")) cols.push({ key: "operation_status", label: "가동상태", width: "w-[80px]", render: (v) => v || "-" }); + return cols; + }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + // 설비 조회 const fetchEquipments = useCallback(async () => { setEquipLoading(true); @@ -431,48 +443,18 @@ export default function EquipmentInfoPage() { -
- {equipLoading ? ( -
- -
- ) : equipments.length === 0 ? ( -
- -

등록된 설비가 없어요

-
- ) : ( - - - - {ts.isVisible("equipment_code") && 설비코드} - {ts.isVisible("equipment_name") && 설비명} - {ts.isVisible("equipment_type") && 설비유형} - {ts.isVisible("manufacturer") && 제조사} - {ts.isVisible("installation_location") && 설치장소} - {ts.isVisible("operation_status") && 가동상태} - - - - {equipments.map((equip) => ( - setSelectedEquipId(equip.id)} - onDoubleClick={openEquipEdit} - > - {ts.isVisible("equipment_code") && {equip.equipment_code}} - {ts.isVisible("equipment_name") && {equip.equipment_name || "-"}} - {ts.isVisible("equipment_type") && {equip.equipment_type || "-"}} - {ts.isVisible("manufacturer") && {equip.manufacturer || "-"}} - {ts.isVisible("installation_location") && {equip.installation_location || "-"}} - {ts.isVisible("operation_status") && {equip.operation_status || "-"}} - - ))} - -
- )} -
+ setSelectedEquipId(id)} + onRowDoubleClick={() => openEquipEdit()} + showPagination={true} + draggableColumns={false} + columnOrderKey="c16-equipment-info-main" + /> 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 5a3cd862..4e94d293 100644 --- a/frontend/app/(main)/COMPANY_16/equipment/plc-settings/page.tsx +++ b/frontend/app/(main)/COMPANY_16/equipment/plc-settings/page.tsx @@ -19,6 +19,7 @@ import { useAuth } from "@/hooks/useAuth"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; import { toast } from "sonner"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; @@ -285,46 +286,25 @@ export default function PlcSettingsPage() {
- - - - - 0 && dtChecked.length === datatypes.length} - onCheckedChange={(v) => setDtChecked(v ? datatypes.map(r => r.id) : [])} - /> - - {ts.visibleColumns.map((col) => ( - {col.label} - ))} - - - - {dtLoading ? ( - - ) : datatypes.length === 0 ? ( -

등록된 PLC 데이터타입이 없어요

- ) : datatypes.map((row) => ( - setDtChecked(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id])} - onDoubleClick={() => openDtEdit(row)} - > - e.stopPropagation()}> - setDtChecked(prev => v ? [...prev, row.id] : prev.filter(id => id !== row.id))} /> - - {ts.visibleColumns.map((col) => ( - - {col.key === "is_active" - ? {row.is_active ? "사용" : "미사용"} - : row[col.key] ?? ""} - - ))} - - ))} -
-
+ ({ + key: col.key, + label: col.label, + align: col.key === "is_active" ? "center" : undefined, + render: col.key === "is_active" + ? (val: any) => {val ? "사용" : "미사용"} + : undefined, + }))} + data={ts.groupData(datatypes)} + loading={dtLoading} + emptyMessage="등록된 PLC 데이터타입이 없어요" + showCheckbox + checkedIds={dtChecked} + onCheckedChange={setDtChecked} + onRowDoubleClick={(row) => openDtEdit(row)} + showPagination={false} + draggableColumns={false} + />
@@ -360,52 +340,26 @@ export default function PlcSettingsPage() {
- - - - - 0 && cfgChecked.length === configs.length} - onCheckedChange={(v) => setCfgChecked(v ? configs.map(r => r.id) : [])} - /> - - 설정명 - 소스연결ID - 소스테이블 - 대상테이블 - 수집유형 - 스케줄(Cron) - 사용여부 - - - - {cfgLoading ? ( - - ) : configs.length === 0 ? ( -

등록된 수집 설정이 없어요

- ) : configs.map((row) => ( - setCfgChecked(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id])} - onDoubleClick={() => openCfgEdit(row)} - > - e.stopPropagation()}> - setCfgChecked(prev => v ? [...prev, row.id] : prev.filter(id => id !== row.id))} /> - - {row.config_name} - {row.source_connection_id} - {row.source_table} - {row.target_table} - {row.collection_type} - {row.schedule_cron} - - {row.is_active ? "사용" : "미사용"} - - - ))} -
-
+ {val} }, + { key: "is_active", label: "사용여부", width: "w-[80px]", align: "center" as const, render: (val: any) => {val ? "사용" : "미사용"} }, + ] as EDataTableColumn[]} + data={configs} + loading={cfgLoading} + emptyMessage="등록된 수집 설정이 없어요" + showCheckbox + checkedIds={cfgChecked} + onCheckedChange={setCfgChecked} + onRowDoubleClick={(row) => openCfgEdit(row)} + showPagination={false} + draggableColumns={false} + />
diff --git a/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx index 29be86d7..f3f5b91b 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx @@ -53,6 +53,7 @@ import { exportToExcel } from "@/lib/utils/excelExport"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; // ========== 타입 & 상수 ========== type TabKey = "carrier" | "cost" | "contract" | "route" | "vehicle"; @@ -757,95 +758,25 @@ export default function LogisticsInfoPage() { {/* 테이블 영역 */}
- {tabLoading[tab.key] ? ( -
- - 불러오는 중... -
- ) : displayData.length === 0 ? ( -
-
- -
- 등록된 {tab.label} 정보가 없어요 - - 등록 버튼을 눌러 새 항목을 추가해주세요 - -
- ) : ( - - - - - - toggleAllCheck(tab.key, !!checked) - } - /> - - {getVisibleColumns(tab.key).map((col) => ( - - {col.label} - - ))} - - - - {displayData.map((row: any, idx: number) => { - const rowId = String(row.id); - const isChecked = tabChecked[tab.key].includes(rowId); - return ( - toggleRowCheck(tab.key, rowId)} - onDoubleClick={() => handleOpenEdit(row)} - > - - toggleRowCheck(tab.key, rowId)} - onClick={(e) => e.stopPropagation()} - /> - - {getVisibleColumns(tab.key).map((col) => { - const val = row[col.key]; - const display = - col.formatNumber && val != null && val !== "" - ? Number(val).toLocaleString() - : val ?? ""; - return ( - - {display} - - ); - })} - - ); - })} - -
- )} + ({ + key: col.key, + label: col.label, + align: col.align, + formatNumber: col.formatNumber, + truncate: true, + }))} + data={tsMap[tab.key].groupData(displayData)} + rowKey={(row: any) => String(row.id)} + loading={tabLoading[tab.key]} + emptyMessage={`등록된 ${tab.label} 정보가 없어요`} + showCheckbox + checkedIds={tabChecked[tab.key]} + onCheckedChange={(ids) => setTabChecked((prev) => ({ ...prev, [tab.key]: ids }))} + onRowDoubleClick={(row) => handleOpenEdit(row)} + showPagination={false} + draggableColumns={false} + />
); diff --git a/frontend/app/(main)/COMPANY_16/logistics/inventory/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/inventory/page.tsx index ebbd812e..f91be557 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/inventory/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/inventory/page.tsx @@ -59,6 +59,7 @@ import { useAuth } from "@/hooks/useAuth"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; import { toast } from "sonner"; import { exportToExcel } from "@/lib/utils/excelExport"; @@ -303,6 +304,45 @@ export default function InventoryStatusPage() { } }; + // EDataTable 컬럼 정의 + const stockColumns: EDataTableColumn[] = ts.visibleColumns.map((col) => { + const base: EDataTableColumn = { key: col.key, label: col.label, align: col.align }; + if (col.key === "current_qty") { + return { + ...base, + align: "right" as const, + render: (val: any, row: any) => ( + + + {Number(row.current_qty || 0).toLocaleString()} + + {row._isLow && ( + + )} + + ), + }; + } + if (col.key === "safety_qty") { + return { + ...base, + align: "right" as const, + formatNumber: true, + }; + } + if (col.key === "status") { + return { + ...base, + render: (val: any) => ( + + {val} + + ), + }; + } + return base; + }); + // 엑셀 내보내기 const handleExcelExport = () => { if (stockItems.length === 0) { @@ -368,86 +408,19 @@ export default function InventoryStatusPage() { -
- {stockLoading ? ( -
- -
- ) : stockItems.length === 0 ? ( -
- 등록된 재고가 없어요 -
- ) : ( - - - - # - {ts.visibleColumns.map((col) => ( - - {col.label} - - ))} - - - - {stockItems.map((item, idx) => ( - setSelectedStockId(item.id)} - > - - {idx + 1} - - {ts.visibleColumns.map((col) => { - if (col.key === "current_qty") { - return ( - - - {Number(item.current_qty || 0).toLocaleString()} - - {item._isLow && ( - - )} - - ); - } - if (col.key === "safety_qty") { - return ( - - {Number(item.safety_qty || 0).toLocaleString()} - - ); - } - if (col.key === "status") { - return ( - - - {item.status} - - - ); - } - return ( - - {item[col.key] ?? ""} - - ); - })} - - ))} - -
- )} -
+ row.id} + loading={stockLoading} + emptyMessage="등록된 재고가 없어요" + selectedId={selectedStockId} + onSelect={(id) => setSelectedStockId(id)} + showRowNumber + showPagination={false} + draggableColumns={false} + columnOrderKey="c16-inventory" + /> diff --git a/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx index 6743e03e..e56c927c 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx @@ -47,6 +47,7 @@ import { toast } from "sonner"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; // API: /outbound/* import { getOutboundList, @@ -603,139 +604,40 @@ export default function OutboundPage() { -
- - - - - - - {ts.isVisible("outbound_number") && 출고번호} - {ts.isVisible("outbound_type") && 출고유형} - {ts.isVisible("outbound_date") && 출고일} - {ts.isVisible("reference_number") && 참조번호} - {ts.isVisible("source_type") && 데이터출처} - {ts.isVisible("customer_name") && 거래처} - {ts.isVisible("item_number") && 품목코드} - {ts.isVisible("item_name") && 품목명} - {ts.isVisible("spec") && 규격} - {ts.isVisible("outbound_qty") && 출고수량} - {ts.isVisible("unit_price") && 단가} - {ts.isVisible("total_amount") && 금액} - {ts.isVisible("warehouse_name") && 창고} - {ts.isVisible("outbound_status") && 출고상태} - {ts.isVisible("remark") && 비고} - - - - {loading ? ( - - - - - - ) : data.length === 0 ? ( - - -
- -

등록된 출고 내역이 없어요

-

- 출고 등록 버튼을 클릭하여 출고를 추가해주세요 -

-
-
-
- ) : ( - data.map((row) => ( - toggleCheck(row.id)} - onDoubleClick={() => openEditModal(row)} - > - e.stopPropagation()} - > - toggleCheck(row.id)} - /> - - {ts.isVisible("outbound_number") && - {row.outbound_number} - } - {ts.isVisible("outbound_type") && - - {row.outbound_type || "-"} - - } - {ts.isVisible("outbound_date") && - {row.outbound_date - ? new Date(row.outbound_date).toLocaleDateString("ko-KR") - : "-"} - } - {ts.isVisible("reference_number") && - {row.reference_number || "-"} - } - {ts.isVisible("source_type") && - {row.source_type - ? SOURCE_TYPE_LABEL[row.source_type] || row.source_type - : "-"} - } - {ts.isVisible("customer_name") && - {row.customer_name || "-"} - } - {ts.isVisible("item_number") && - {row.item_code || "-"} - } - {ts.isVisible("item_name") && {row.item_name || "-"}} - {ts.isVisible("spec") && {row.specification || "-"}} - {ts.isVisible("outbound_qty") && - {Number(row.outbound_qty || 0).toLocaleString()} - } - {ts.isVisible("unit_price") && - {Number(row.unit_price || 0).toLocaleString()} - } - {ts.isVisible("total_amount") && - {Number(row.total_amount || 0).toLocaleString()} - } - {ts.isVisible("warehouse_name") && - {row.warehouse_name || row.warehouse_code || "-"} - } - {ts.isVisible("outbound_status") && - - {row.outbound_status || "-"} - - } - {ts.isVisible("remark") && - {row.memo || "-"} - } - - )) - )} -
-
-
+ ( + {v || "-"} + )}, + { key: "outbound_date", label: "출고일", width: "w-[100px]", render: (v) => v ? new Date(v).toLocaleDateString("ko-KR") : "-" }, + { key: "reference_number", label: "참조번호", width: "w-[120px]" }, + { key: "source_type", label: "데이터출처", width: "w-[80px]", render: (v) => v ? SOURCE_TYPE_LABEL[v] || v : "-" }, + { key: "customer_name", label: "거래처", width: "w-[120px]" }, + { key: "item_code", label: "품목코드", width: "w-[100px]" }, + { key: "item_name", label: "품목명", minWidth: "min-w-[150px]" }, + { key: "specification", label: "규격", width: "w-[80px]" }, + { key: "outbound_qty", label: "출고수량", width: "w-[80px]", align: "right", formatNumber: true }, + { key: "unit_price", label: "단가", width: "w-[90px]", align: "right", formatNumber: true }, + { key: "total_amount", label: "금액", width: "w-[100px]", align: "right", formatNumber: true }, + { key: "warehouse_name", label: "창고", width: "w-[100px]", render: (_v, row) => row.warehouse_name || row.warehouse_code || "-" }, + { key: "outbound_status", label: "출고상태", width: "w-[90px]", align: "center", render: (v) => ( + {v || "-"} + )}, + { key: "memo", label: "비고", width: "w-[100px]" }, + ] as EDataTableColumn[]} + data={ts.groupData(data)} + rowKey={(row) => row.id} + loading={loading} + emptyMessage="등록된 출고 내역이 없어요" + showCheckbox + checkedIds={checkedIds} + onCheckedChange={setCheckedIds} + onRowDoubleClick={(row) => openEditModal(row)} + showPagination + draggableColumns + columnOrderKey="c16-outbound" + /> {/* 출고 등록 모달 */} diff --git a/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx index 1a04aa7f..5d4d5787 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx @@ -30,6 +30,7 @@ import { import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const GRID_COLUMNS = [ { key: "pkg_code", label: "품목코드" }, @@ -458,58 +459,32 @@ 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") && 상태} - - - - {pkgLoading ? ( - - - - - - ) : filteredPkgUnits.length === 0 ? ( - - -
-
- -
-

등록된 포장재가 없어요

-
-
-
- ) : filteredPkgUnits.map((p) => ( - selectPkg(p)} - > - {ts.isVisible("pkg_code") && {p.pkg_code}} - {ts.isVisible("pkg_name") && {p.pkg_name}} - {ts.isVisible("pkg_type") && {PKG_TYPE_LABEL[p.pkg_type] || p.pkg_type || "-"}} - {ts.isVisible("size") && {fmtSize(p.width_mm, p.length_mm, p.height_mm)}} - {ts.isVisible("max_weight") && {Number(p.max_load_kg || 0) > 0 ? `${p.max_load_kg}kg` : "-"}} - {ts.isVisible("status") && - - {STATUS_LABEL[p.status] || p.status} - - } - - ))} -
-
+ PKG_TYPE_LABEL[v] || v || "-" }, + { key: "size", label: "크기(mm)", width: "w-[100px]", render: (_v, row) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, + { key: "max_load_kg", label: "최대중량", width: "w-[80px]", align: "right", render: (v) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + { key: "status", label: "상태", width: "w-[60px]", align: "center", render: (v) => ( + + {STATUS_LABEL[v] || v} + + )}, + ] as EDataTableColumn[]} + data={ts.groupData(filteredPkgUnits)} + rowKey={(row) => String(row.id)} + loading={pkgLoading} + emptyMessage="등록된 포장재가 없어요" + selectedId={selectedPkg ? String(selectedPkg.id) : null} + onSelect={(id) => { + const pkg = filteredPkgUnits.find((p) => String(p.id) === id); + if (pkg) selectPkg(pkg); + }} + showPagination={false} + draggableColumns + columnOrderKey="c16-packaging-pkg" + />
{/* 매칭 품목 서브패널 */} diff --git a/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx index 0287cb14..42590754 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx @@ -53,6 +53,7 @@ import { apiClient } from "@/lib/api/client"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; // API: /receiving/* import { getReceivingList, @@ -574,135 +575,39 @@ 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") && 비고} - - - - {loading ? ( - - - - - - ) : data.length === 0 ? ( - - -
- -

등록된 입고 내역이 없어요

-

- 입고 등록 버튼을 클릭하여 입고를 추가해 보세요 -

-
-
-
- ) : ( - data.map((row) => ( - toggleCheck(row.id)} - > - e.stopPropagation()} - > - toggleCheck(row.id)} - /> - - {ts.isVisible("inbound_number") && - {row.inbound_number} - } - {ts.isVisible("inbound_type") && - - {row.inbound_type || "-"} - - } - {ts.isVisible("inbound_date") && - {row.inbound_date - ? new Date(row.inbound_date).toLocaleDateString("ko-KR") - : "-"} - } - {ts.isVisible("reference_number") && - {row.reference_number || "-"} - } - {ts.isVisible("source_type") && - {row.source_table - ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table - : "-"} - } - {ts.isVisible("supplier_name") && - {row.supplier_name || "-"} - } - {ts.isVisible("item_number") && - {row.item_number || "-"} - } - {ts.isVisible("item_name") && {row.item_name || "-"}} - {ts.isVisible("spec") && {row.spec || "-"}} - {ts.isVisible("inbound_qty") && - {Number(row.inbound_qty || 0).toLocaleString()} - } - {ts.isVisible("unit_price") && - {Number(row.unit_price || 0).toLocaleString()} - } - {ts.isVisible("total_amount") && - {Number(row.total_amount || 0).toLocaleString()} - } - {ts.isVisible("warehouse_name") && - {row.warehouse_name || row.warehouse_code || "-"} - } - {ts.isVisible("inbound_status") && - - {row.inbound_status || "-"} - - } - {ts.isVisible("remark") && - {row.memo || "-"} - } - - )) - )} -
-
-
+ ( + {v || "-"} + )}, + { key: "inbound_date", label: "입고일", width: "w-[100px]", render: (v) => v ? new Date(v).toLocaleDateString("ko-KR") : "-" }, + { key: "reference_number", label: "참조번호", width: "w-[120px]" }, + { key: "source_table", label: "데이터출처", width: "w-[80px]", render: (v) => v ? SOURCE_TABLE_LABEL[v] || v : "-" }, + { key: "supplier_name", label: "공급처", width: "w-[120px]" }, + { key: "item_number", label: "품목코드", width: "w-[100px]" }, + { key: "item_name", label: "품목명", minWidth: "min-w-[150px]" }, + { key: "spec", label: "규격", width: "w-[80px]" }, + { key: "inbound_qty", label: "입고수량", width: "w-[80px]", align: "right", formatNumber: true }, + { key: "unit_price", label: "단가", width: "w-[90px]", align: "right", formatNumber: true }, + { key: "total_amount", label: "금액", width: "w-[100px]", align: "right", formatNumber: true }, + { key: "warehouse_name", label: "창고", width: "w-[100px]", render: (_v, row) => row.warehouse_name || row.warehouse_code || "-" }, + { key: "inbound_status", label: "입고상태", width: "w-[90px]", align: "center", render: (v) => ( + {v || "-"} + )}, + { key: "memo", label: "비고", width: "w-[100px]" }, + ] as EDataTableColumn[]} + data={ts.groupData(data)} + rowKey={(row) => row.id} + loading={loading} + emptyMessage="등록된 입고 내역이 없어요" + showCheckbox + checkedIds={checkedIds} + onCheckedChange={setCheckedIds} + showPagination + draggableColumns + columnOrderKey="c16-receiving" + /> {/* 입고 등록 모달 */} diff --git a/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx index c95e0cbf..98c4fff0 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx @@ -64,6 +64,7 @@ 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 { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; import { exportToExcel } from "@/lib/utils/excelExport"; const WAREHOUSE_TABLE = "warehouse_info"; @@ -605,6 +606,32 @@ export default function WarehouseManagementPage() { maxLevels: rackConditions.reduce((acc, c) => Math.max(acc, c.levels || 0), 0), }; + // EDataTable 컬럼 정의 + const warehouseColumns: EDataTableColumn[] = ts.visibleColumns.map((col) => { + const base: EDataTableColumn = { key: col.key, label: col.label }; + if (col.key === "warehouse_type") { + return { + ...base, + render: (val: any) => ( + + {val} + + ), + }; + } + if (col.key === "status") { + return { + ...base, + render: (val: any) => ( + + {val} + + ), + }; + } + return base; + }); + // 엑셀 내보내기 const handleExcelExport = () => { if (warehouses.length === 0) { @@ -689,70 +716,20 @@ export default function WarehouseManagementPage() { -
- {warehouseLoading ? ( -
- -
- ) : warehouses.length === 0 ? ( -
- 등록된 창고가 없어요 -
- ) : ( - - - - # - {ts.visibleColumns.map((col) => ( - {col.label} - ))} - - - - {warehouses.map((w, idx) => ( - setSelectedWarehouseId(w.id)} - onDoubleClick={() => openWarehouseEditModal(w)} - > - - {idx + 1} - - {ts.visibleColumns.map((col) => { - if (col.key === "warehouse_type") { - return ( - - - {w.warehouse_type} - - - ); - } - if (col.key === "status") { - return ( - - - {w.status} - - - ); - } - return ( - - {w[col.key] ?? ""} - - ); - })} - - ))} - -
- )} -
+ row.id} + loading={warehouseLoading} + emptyMessage="등록된 창고가 없어요" + selectedId={selectedWarehouseId} + onSelect={(id) => setSelectedWarehouseId(id)} + onRowDoubleClick={(row) => openWarehouseEditModal(row)} + showRowNumber + showPagination={false} + draggableColumns={false} + columnOrderKey="c16-warehouse" + /> 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 6f48424b..dfd1b666 100644 --- a/frontend/app/(main)/COMPANY_16/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_16/master-data/company/page.tsx @@ -31,6 +31,7 @@ import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { formatField, validateField, validateForm } from "@/lib/utils/validation"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const COMPANY_TABLE = "company_mng"; const DEPT_TABLE = "dept_info"; @@ -376,6 +377,16 @@ export default function CompanyPage() { } }; + // EDataTable 컬럼 정의 (사원 목록) + const companyMemberColumns: EDataTableColumn[] = [ + { key: "sabun", label: "사번", width: "w-[80px]", render: (val: any) => {val || "-"} }, + { key: "user_name", label: "이름", width: "w-[90px]" }, + { key: "user_id", label: "사용자ID", width: "w-[100px]" }, + { key: "position_name", label: "직급", width: "w-[80px]", render: (val: any) => {val || "-"} }, + { key: "cell_phone", label: "휴대폰", width: "w-[120px]", render: (val: any) => {val || "-"} }, + { key: "email", label: "이메일" }, + ]; + /* ── 트리 렌더 ── */ const renderTree = (nodes: DeptNode[], depth = 0) => { return nodes.map((node) => { @@ -685,47 +696,17 @@ export default function CompanyPage() { )} {selectedDeptCode ? ( -
- {memberLoading ? ( -
- -
- ) : members.length === 0 ? ( -
- - 소속 사원이 없어요 -
- ) : ( - - - - 사번 - 이름 - 사용자ID - 직급 - 휴대폰 - 이메일 - - - - {members.map((row) => ( - openUserModal(row)} - > - {row.sabun || "-"} - {row.user_name} - {row.user_id} - {row.position_name || "-"} - {row.cell_phone || "-"} - {row.email || "-"} - - ))} - -
- )} -
+ row.user_id || row.id} + loading={memberLoading} + emptyMessage="소속 사원이 없어요" + emptyIcon={} + onRowDoubleClick={(row) => openUserModal(row)} + showPagination={false} + draggableColumns={false} + /> ) : (
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 a4cbb7ed..a2bbcba5 100644 --- a/frontend/app/(main)/COMPANY_16/master-data/department/page.tsx +++ b/frontend/app/(main)/COMPANY_16/master-data/department/page.tsx @@ -39,6 +39,7 @@ 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"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const DEPT_TABLE = "dept_info"; const USER_TABLE = "user_info"; @@ -313,6 +314,36 @@ export default function DepartmentPage() { const isColVisible = (key: string) => ts.isVisible(key); + // EDataTable 컬럼 정의 (부서 목록) + const deptColumns: EDataTableColumn[] = [ + { key: "dept_code", label: "부서코드", width: "w-[120px]" }, + { key: "dept_name", label: "부서명", minWidth: "min-w-[140px]" }, + ...(isColVisible("parent_dept_code") + ? [{ + key: "parent_dept_code", + label: "상위부서", + width: "w-[110px]", + render: (val: any) => {val || "\u2014"}, + }] + : []), + ...(isColVisible("status") + ? [{ + key: "status", + label: "상태", + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val === "active" ? "활성" : (val || "\u2014")} + + ) : null, + }] + : []), + ]; + return (
{/* 검색 필터 바 */} @@ -366,61 +397,20 @@ export default function DepartmentPage() {
{/* 부서 테이블 */} -
- - - - No - 부서코드 - 부서명 - {isColVisible("parent_dept_code") && 상위부서} - {isColVisible("status") && 상태} - - - - {deptLoading ? ( - - - - - - ) : depts.length === 0 ? ( - - - 등록된 부서가 없어요 - - - ) : depts.map((dept, idx) => ( - setSelectedDeptId((prev) => prev === dept.id ? null : dept.id)} - onDoubleClick={openDeptEdit} - > - {idx + 1} - {dept.dept_code} - {dept.dept_name} - {isColVisible("parent_dept_code") && {dept.parent_dept_code || "—"}} - {isColVisible("status") && ( - - {dept.status && ( - - {dept.status === "active" ? "활성" : (dept.status || "—")} - - )} - - )} - - ))} - -
-
+ row.id} + loading={deptLoading} + emptyMessage="등록된 부서가 없어요" + selectedId={selectedDeptId} + onSelect={(id) => setSelectedDeptId(id)} + onRowDoubleClick={() => openDeptEdit()} + showRowNumber + showPagination={false} + draggableColumns={false} + columnOrderKey="c16-department" + />
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 d818ada2..3318f70d 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 @@ -36,6 +36,7 @@ import { Pencil, Copy, Settings2, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; @@ -335,64 +336,21 @@ export default function ItemInfoPage() { {/* 메인 테이블 */} -
- {loading ? ( -
- -
- ) : items.length === 0 ? ( -
- 등록된 품목이 없어요 -
- ) : ( - - - - # - {ts.visibleColumns.map((col) => ( - - {col.label} - - ))} - - - - {items.map((item, idx) => ( - setSelectedId(item.id)} - onDoubleClick={() => openEditModal(item)} - > - {idx + 1} - {ts.visibleColumns.map((col) => ( - - {item[col.key] ?? ""} - - ))} - - ))} - -
- )} -
+ ({ + key: col.key, + label: col.label, + align: col.align as "left" | "center" | "right" | undefined, + }))} + data={ts.groupData(items)} + loading={loading} + emptyMessage="등록된 품목이 없어요" + selectedId={selectedId} + onSelect={(id) => setSelectedId(id)} + onRowDoubleClick={(row) => openEditModal(row)} + showRowNumber + draggableColumns={false} + /> {/* 등록/수정 모달 */} 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 c4349c90..929d35c1 100644 --- a/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor-item/page.tsx +++ b/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor-item/page.tsx @@ -9,7 +9,7 @@ * 외주업체관리와 양방향 연동 (같은 subcontractor_item_mapping 테이블) */ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -31,6 +31,7 @@ import { exportToExcel } from "@/lib/utils/excelExport"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const ITEM_TABLE = "item_info"; const MAPPING_TABLE = "subcontractor_item_mapping"; @@ -113,6 +114,19 @@ export default function SubcontractorItemPage() { return categoryOptions[col]?.find((o) => o.code === code)?.label || code; }; + const mainTableColumns = useMemo(() => { + const cols: EDataTableColumn[] = []; + if (ts.isVisible("item_number")) cols.push({ key: "item_number", label: "품번", width: "w-[110px]" }); + if (ts.isVisible("item_name")) cols.push({ key: "item_name", label: "품명", minWidth: "min-w-[130px]", render: (v) => v || "-" }); + if (ts.isVisible("size")) cols.push({ key: "size", label: "규격", width: "w-[90px]", render: (v) => v || "-" }); + if (ts.isVisible("unit")) cols.push({ key: "unit", label: "단위", width: "w-[60px]", render: (v) => v || "-" }); + if (ts.isVisible("standard_price")) cols.push({ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }); + if (ts.isVisible("selling_price")) cols.push({ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true }); + if (ts.isVisible("currency_code")) cols.push({ key: "currency_code", label: "통화", width: "w-[50px]", render: (v) => v || "-" }); + if (ts.isVisible("status")) cols.push({ key: "status", label: "상태", width: "w-[60px]", render: (v) => v || "-" }); + return cols; + }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + // 좌측: 품목 조회 (division이 "외주관리"인 품목만 필터링) const outsourcingDivisionCode = categoryOptions["division"]?.find( (o) => o.label === "외주관리" || o.label === "외주" || o.label.includes("외주") @@ -337,52 +351,18 @@ export default function SubcontractorItemPage() { -
- {itemLoading ? ( -
- -
- ) : items.length === 0 ? ( -
- -

등록된 외주품목이 없어요

-
- ) : ( - - - - {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") && 상태} - - - - {items.map((item) => ( - setSelectedItemId(item.id)} - onDoubleClick={openEditItem} - > - {ts.isVisible("item_number") && {item.item_number}} - {ts.isVisible("item_name") && {item.item_name || "-"}} - {ts.isVisible("size") && {item.size || "-"}} - {ts.isVisible("unit") && {item.unit || "-"}} - {ts.isVisible("standard_price") && {formatNum(item.standard_price)}} - {ts.isVisible("selling_price") && {formatNum(item.selling_price)}} - {ts.isVisible("currency_code") && {item.currency_code || "-"}} - {ts.isVisible("status") && {item.status || "-"}} - - ))} - -
- )} -
+ setSelectedItemId(id)} + onRowDoubleClick={() => openEditItem()} + showPagination={true} + draggableColumns={false} + columnOrderKey="c16-subcontractor-item-main" + /> diff --git a/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx b/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx index 82b9c7d6..b7a69213 100644 --- a/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx +++ b/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -31,6 +31,7 @@ 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"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const SUBCONTRACTOR_TABLE = "subcontractor_mng"; const MAPPING_TABLE = "subcontractor_item_mapping"; @@ -167,6 +168,14 @@ export default function SubcontractorManagementPage() { return val; }; + const mainTableColumns = useMemo(() => { + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + render: (value: any, row: any) => renderCellValue(row, col.key), + })); + }, [ts.visibleColumns, categoryOptions]); // eslint-disable-line react-hooks/exhaustive-deps + // 외주업체 목록 조회 const fetchSubcontractors = useCallback(async () => { setSubcontractorLoading(true); @@ -831,49 +840,18 @@ export default function SubcontractorManagementPage() { -
- {subcontractorLoading ? ( -
- -
- ) : subcontractors.length === 0 ? ( -
- -

등록된 외주업체가 없어요

-
- ) : ( - - - - {ts.visibleColumns.map((col) => ( - {col.label} - ))} - - - - {subcontractors.map((sub) => ( - setSelectedSubcontractorId(sub.id)} - onDoubleClick={openSubcontractorEdit} - > - {ts.visibleColumns.map((col) => ( - - {renderCellValue(sub, col.key)} - - ))} - - ))} - -
- )} -
+ setSelectedSubcontractorId(id)} + onRowDoubleClick={() => openSubcontractorEdit()} + showPagination={true} + draggableColumns={false} + columnOrderKey="c16-subcontractor-main" + /> diff --git a/frontend/app/(main)/COMPANY_16/production/bom/page.tsx b/frontend/app/(main)/COMPANY_16/production/bom/page.tsx index d3fb7be1..1cf6eb63 100644 --- a/frontend/app/(main)/COMPANY_16/production/bom/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/bom/page.tsx @@ -65,6 +65,7 @@ import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; import { exportToExcel } from "@/lib/utils/excelExport"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; // ─── 상수 ─────────────────────────────────────── const BOM_TABLE = "bom"; @@ -945,71 +946,31 @@ export default function BomManagementPage() { {/* BOM 목록 테이블 */}
- {loading ? ( -
- -
- ) : bomList.length === 0 ? ( -
- -

등록된 BOM이 없어요

-
- ) : ( - - - - - 0} - onCheckedChange={(checked) => - setCheckedIds(checked ? bomList.map((r) => r.id) : []) - } - /> - - {ts.visibleColumns.map((col) => ( - {col.label} - ))} - - - - {bomList.map((row) => ( - setSelectedBomId(row.id)} - > - e.stopPropagation()}> - - setCheckedIds((prev) => - checked ? [...prev, row.id] : prev.filter((id) => id !== row.id) - ) - } - /> - - {ts.visibleColumns.map((col) => { - if (col.key === "item_code") { - return {row.item_code || row.item_number || "-"}; - } - if (col.key === "bom_type") { - return {BOM_TYPE_OPTIONS.find((o) => o.code === row.bom_type)?.label || row.bom_type || "-"}; - } - if (col.key === "status") { - return {renderStatusBadge(row.status)}; - } - return {row[col.key] || "-"}; - })} - - ))} - -
- )} + ({ + key: col.key, + label: col.label, + render: col.key === "item_code" + ? (_val: any, row: any) => {row.item_code || row.item_number || "-"} + : col.key === "bom_type" + ? (_val: any, row: any) => {BOM_TYPE_OPTIONS.find((o) => o.code === row.bom_type)?.label || row.bom_type || "-"} + : col.key === "status" + ? (_val: any, row: any) => renderStatusBadge(row.status) + : undefined, + }))} + data={ts.groupData(bomList)} + loading={loading} + emptyMessage="등록된 BOM이 없어요" + showCheckbox + checkedIds={checkedIds} + onCheckedChange={setCheckedIds} + selectedId={selectedBomId} + onSelect={(id) => setSelectedBomId(id)} + onRowClick={(row) => setSelectedBomId(row.id)} + showPagination + draggableColumns={false} + columnOrderKey="c16-bom" + />
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 aeff278d..32f7b2cd 100644 --- a/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx @@ -1040,8 +1040,25 @@ export default function ProductionPlanManagementPage() { - {orderItems.map((item) => ( - + {ts.groupData(orderItems).map((item, rowIdx) => { + if (item._isGroupSummary) { + return ( + + + + {ts.visibleColumns.map((col) => { + const v = (item as any)[col.key]; + return ( + + {typeof v === "number" ? Number(v).toLocaleString() : (v || "")} + + ); + })} + + ); + } + return ( + e.stopPropagation()}> toggleItemGroupSelect(item.item_code)} className="h-4 w-4" /> @@ -1093,7 +1110,8 @@ export default function ProductionPlanManagementPage() { ))} - ))} + ); + })} diff --git a/frontend/app/(main)/COMPANY_16/production/process-info/ItemRoutingTab.tsx b/frontend/app/(main)/COMPANY_16/production/process-info/ItemRoutingTab.tsx index 9acf4e17..a5b136c2 100644 --- a/frontend/app/(main)/COMPANY_16/production/process-info/ItemRoutingTab.tsx +++ b/frontend/app/(main)/COMPANY_16/production/process-info/ItemRoutingTab.tsx @@ -13,6 +13,7 @@ import { Label } from "@/components/ui/label"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; import { Dialog, DialogContent, @@ -684,62 +685,27 @@ export function ItemRoutingTab() {
{!selectedVersionId ? (

버전을 선택해주세요

- ) : detailsLoading ? ( -
- -

불러오는 중...

-
- ) : detailsGridData.length === 0 ? ( -

등록된 공정이 없어요

) : ( - - - - - 0} - onCheckedChange={(checked) => { - if (checked) setSelectedDetailIds(new Set(detailsGridData.map((r) => r.id))); - else setSelectedDetailIds(new Set()); - }} - /> - - 순번 - 공정명 - 필수 - 순서고정 - 작업구분 - 표준시간 - 외주업체 - - - - {detailsGridData.map((row) => ( - - e.stopPropagation()}> - { - setSelectedDetailIds((prev) => { - const next = new Set(prev); - if (checked) next.add(row.id); - else next.delete(row.id); - return next; - }); - }} - /> - - {row.seq_no} - {row.process_display} - {row.is_required} - {row.is_fixed_order} - {row.work_type} - {row.standard_time} - {row.outsource_display} - - ))} - -
+ row.id} + loading={detailsLoading} + emptyMessage="등록된 공정이 없어요" + showCheckbox + checkedIds={Array.from(selectedDetailIds)} + onCheckedChange={(ids) => setSelectedDetailIds(new Set(ids))} + showPagination={false} + draggableColumns={false} + /> )}
diff --git a/frontend/app/(main)/COMPANY_16/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_16/production/process-info/ProcessMasterTab.tsx index 34244cc9..cfbee962 100644 --- a/frontend/app/(main)/COMPANY_16/production/process-info/ProcessMasterTab.tsx +++ b/frontend/app/(main)/COMPANY_16/production/process-info/ProcessMasterTab.tsx @@ -46,6 +46,7 @@ import { TableRow, } from "@/components/ui/table"; import { Checkbox } from "@/components/ui/checkbox"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; import { getProcessList, createProcess, @@ -435,69 +436,34 @@ export function ProcessMasterTab() { {/* 공정 목록 테이블 */}
- {listBusy ? ( -
- -

불러오는 중...

-
- ) : processGridData.length === 0 ? ( -

조회된 공정이 없어요

- ) : ( - - - - - 0} - onCheckedChange={(checked) => { - if (checked) setSelectedIds(new Set(processGridData.map((r) => r.id))); - else setSelectedIds(new Set()); - }} - /> - - 공정코드 - 공정명 - 공정유형 - 표준시간(분) - 작업인원 - 사용여부 - - - - {processGridData.map((row) => ( - { - const proc = processes.find((p) => p.id === row.id); - setSelectedProcess(proc || null); - }} - > - e.stopPropagation()}> - { - setSelectedIds((prev) => { - const next = new Set(prev); - if (checked) next.add(row.id); - else next.delete(row.id); - return next; - }); - }} - /> - - {row.process_code} - {row.process_name} - {row.process_type_display} - {row.standard_time} - {row.worker_count} - {row.use_yn_display} - - ))} - -
- )} + {val} }, + { key: "process_name", label: "공정명" }, + { key: "process_type_display", label: "공정유형", width: "w-[120px]" }, + { key: "standard_time", label: "표준시간(분)", width: "w-[110px]", align: "right" as const }, + { key: "worker_count", label: "작업인원", width: "w-[90px]", align: "right" as const }, + { key: "use_yn_display", label: "사용여부", width: "w-[90px]", align: "center" as const }, + ] as EDataTableColumn[]} + data={processGridData} + rowKey={(row) => row.id} + loading={listBusy} + emptyMessage="조회된 공정이 없어요" + selectedId={selectedProcess?.id ?? null} + onSelect={(id) => { + const proc = processes.find((p) => p.id === id); + setSelectedProcess(proc || null); + }} + onRowClick={(row) => { + const proc = processes.find((p) => p.id === row.id); + setSelectedProcess(proc || null); + }} + showCheckbox + checkedIds={Array.from(selectedIds)} + onCheckedChange={(ids) => setSelectedIds(new Set(ids))} + showPagination={false} + draggableColumns={false} + />
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 b70ab356..08ace7ec 100644 --- a/frontend/app/(main)/COMPANY_16/production/work-instruction/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/work-instruction/page.tsx @@ -23,6 +23,7 @@ import { WorkStandardEditModal } from "./WorkStandardEditModal"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const GRID_COLUMNS = [ { key: "work_instruction_no", label: "작업지시번호" }, @@ -445,104 +446,74 @@ 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") && 작업} - - - - {loading ? ( - - ) : orders.length === 0 ? ( - - -
-
- -
-

등록된 작업지시가 없어요

-

새로운 작업지시를 등록해주세요

-
-
-
- ) : orders.map((o, rowIdx) => { - const pct = getProgress(o); - const pLabel = getProgressLabel(o); - const pBadge = PROGRESS_BADGE[pLabel] || PROGRESS_BADGE["대기"]; - const sBadge = STATUS_BADGE[o.status] || STATUS_BADGE["일반"]; - const isFirstOfGroup = Number(o.detail_seq) === 1; + {getDisplayNo(row)} }, + { key: "status", label: "상태", width: "w-[70px]", align: "center", render: (v) => { + const sBadge = STATUS_BADGE[v] || STATUS_BADGE["일반"]; + return {sBadge.label}; + }}, + { key: "progress", label: "진행현황", width: "w-[100px]", align: "center", sortable: false, filterable: false, render: (_v, row) => { + const isFirstOfGroup = Number(row.detail_seq) === 1; + if (!isFirstOfGroup) return ; + const pct = getProgress(row); + const pLabel = getProgressLabel(row); + const pBadge = PROGRESS_BADGE[pLabel] || PROGRESS_BADGE["대기"]; + return ( +
+ {pBadge.label} +
+
= 100 ? "bg-success" : pct > 0 ? "bg-primary" : "bg-muted-foreground/30")} style={{ width: `${pct}%` }} /> +
+ {pct}% +
+ ); + }}, + { key: "item_name", label: "품목명", render: (_v, row) => row.item_name || row.item_number || "-" }, + { key: "item_spec", label: "규격", width: "w-[100px]" }, + { key: "detail_qty", label: "수량", width: "w-[80px]", align: "right", formatNumber: true }, + { key: "equipment_name", label: "설비", width: "w-[120px]", render: (v, row) => Number(row.detail_seq) === 1 ? (v || "-") : "" }, + { key: "routing", label: "라우팅", width: "w-[120px]", sortable: false, filterable: false, render: (_v, row) => { + const isFirstOfGroup = Number(row.detail_seq) === 1; + if (!isFirstOfGroup) return ""; + if (row.routing_version_id) { return ( - - {ts.isVisible("work_instruction_no") && {getDisplayNo(o)}} - {ts.isVisible("status") && {sBadge.label}} - {ts.isVisible("progress") && - {isFirstOfGroup ? ( -
- {pBadge.label} -
-
= 100 ? "bg-success" : pct > 0 ? "bg-primary" : "bg-muted-foreground/30")} style={{ width: `${pct}%` }} /> -
- {pct}% -
- ) : } - } - {ts.isVisible("item_name") && {o.item_name || o.item_number || "-"}} - {ts.isVisible("spec") && {o.item_spec || "-"}} - {ts.isVisible("qty") && {Number(o.detail_qty || 0).toLocaleString()}} - {ts.isVisible("equipment") && {isFirstOfGroup ? (o.equipment_name || "-") : ""}} - {ts.isVisible("routing") && - {isFirstOfGroup ? ( - o.routing_version_id ? ( - - ) : - - ) : ""} - } - {ts.isVisible("work_team") && {isFirstOfGroup ? (o.work_team || "-") : ""}} - {ts.isVisible("worker") && {isFirstOfGroup ? getWorkerName(o.worker) : ""}} - {ts.isVisible("start_date") && {isFirstOfGroup ? (o.start_date || "-") : ""}} - {ts.isVisible("end_date") && {isFirstOfGroup ? (o.end_date || "-") : ""}} - {ts.isVisible("actions") && - {isFirstOfGroup && ( -
- - -
- )} -
} - + ); - })} - -
-
+ } + return -; + }}, + { key: "work_team", label: "작업조", width: "w-[80px]", align: "center", render: (v, row) => Number(row.detail_seq) === 1 ? (v || "-") : "" }, + { key: "worker", label: "작업자", width: "w-[100px]", render: (v, row) => Number(row.detail_seq) === 1 ? getWorkerName(v) : "" }, + { key: "start_date", label: "시작일", width: "w-[100px]", align: "center", render: (v, row) => Number(row.detail_seq) === 1 ? (v || "-") : "" }, + { key: "end_date", label: "완료일", width: "w-[100px]", align: "center", render: (v, row) => Number(row.detail_seq) === 1 ? (v || "-") : "" }, + { key: "actions", label: "작업", width: "w-[150px]", align: "center", sortable: false, filterable: false, render: (_v, row) => { + const isFirstOfGroup = Number(row.detail_seq) === 1; + if (!isFirstOfGroup) return null; + return ( +
+ + +
+ ); + }}, + ] as EDataTableColumn[]} + data={ts.groupData(orders)} + rowKey={(row) => `${row.wi_id}-${row.detail_id}`} + loading={loading} + emptyMessage="등록된 작업지시가 없어요" + showPagination + draggableColumns + columnOrderKey="c16-work-instruction" + /> diff --git a/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx index 0f0fbfa1..8e5074c3 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx @@ -26,6 +26,7 @@ import { toast } from "sonner"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const MASTER_TABLE = "purchase_order_mng"; const DETAIL_TABLE = "purchase_detail"; @@ -588,8 +589,6 @@ export default function PurchaseOrderPage() { toast.success("다운로드 완료"); }; - const allChecked = orders.length > 0 && checkedIds.length === orders.length; - const someChecked = checkedIds.length > 0 && checkedIds.length < orders.length; return (
@@ -638,90 +637,32 @@ export default function PurchaseOrderPage() {
{/* 데이터 테이블 */} -
- - - - - { - setCheckedIds(checked ? orders.map((o) => o.id) : []); - }} - /> - - {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") && 메모} - - - - {loading ? ( - - - - - - ) : orders.length === 0 ? ( - - - 등록된 발주가 없어요 - - - ) : orders.map((row) => ( - openEditModal(row.purchase_no)} - > - e.stopPropagation()}> - { - setCheckedIds((prev) => - checked ? [...prev, row.id] : prev.filter((id) => id !== row.id) - ); - }} - /> - - {ts.isVisible("purchase_no") && {row.purchase_no}} - {ts.isVisible("order_date") && {row.order_date}} - {ts.isVisible("supplier_name") && {row.supplier_name}} - {ts.isVisible("item_code") && {row.item_code}} - {ts.isVisible("item_name") && {row.item_name}} - {ts.isVisible("spec") && {row.spec}} - {ts.isVisible("order_qty") && {row.order_qty ? Number(row.order_qty).toLocaleString() : ""}} - {ts.isVisible("received_qty") && {row.received_qty ? Number(row.received_qty).toLocaleString() : ""}} - {ts.isVisible("remain_qty") && {row.remain_qty ? Number(row.remain_qty).toLocaleString() : ""}} - {ts.isVisible("unit_price") && {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}} - {ts.isVisible("amount") && {row.amount ? Number(row.amount).toLocaleString() : ""}} - {ts.isVisible("due_date") && {row.due_date}} - {ts.isVisible("status") && ( - - {row.status && ( - - {row.status} - - )} - - )} - {ts.isVisible("memo") && {row.memo}} - - ))} - -
+
+ ({ + key: col.key, + label: col.label, + align: ["order_qty", "received_qty", "remain_qty", "unit_price", "amount"].includes(col.key) ? "right" : undefined, + formatNumber: ["order_qty", "received_qty", "remain_qty", "unit_price", "amount"].includes(col.key), + render: col.key === "status" + ? (val: any, row: any) => row.status ? ( + + {row.status} + + ) : null + : undefined, + }))} + data={ts.groupData(orders)} + loading={loading} + emptyMessage="등록된 발주가 없어요" + showCheckbox + checkedIds={checkedIds} + onCheckedChange={setCheckedIds} + onRowDoubleClick={(row) => openEditModal(row.purchase_no)} + showPagination + draggableColumns={false} + columnOrderKey="c16-purchase-order" + />
{/* 발주 등록/수정 모달 */} 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 b730842a..c031bc9e 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -20,6 +20,7 @@ import { exportToExcel } from "@/lib/utils/excelExport"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const ITEM_TABLE = "item_info"; const MAPPING_TABLE = "supplier_item_mapping"; @@ -128,6 +129,25 @@ export default function PurchaseItemPage() { const isColVisible = (key: string) => ts.isVisible(key); const itemColSpan = 2 + ITEM_COLUMNS.filter((c) => isColVisible(c.key)).length; + const mainTableColumns = useMemo(() => { + const cols: EDataTableColumn[] = [ + { key: "item_number", label: "품번", width: "w-[110px]" }, + { key: "item_name", label: "품명" }, + ]; + if (isColVisible("size")) cols.push({ key: "size", label: "규격", width: "w-[90px]", render: (v) => v || "-" }); + if (isColVisible("unit")) cols.push({ key: "unit", label: "단위", width: "w-[60px]", render: (v) => v || "-" }); + if (isColVisible("standard_price")) cols.push({ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }); + if (isColVisible("status")) cols.push({ + key: "status", label: "상태", width: "w-[60px]", align: "center", + render: (v) => ( + {v || "-"} + ), + }); + return cols; + }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + // 우측: 공급업체 매핑 조회 useEffect(() => { if (!selectedItem?.item_number) { setSupplierItems([]); setSupplierCheckedIds([]); return; } @@ -380,50 +400,18 @@ export default function PurchaseItemPage() {
-
- - - - 품번 - 품명 - {isColVisible("size") && 규격} - {isColVisible("unit") && 단위} - {isColVisible("standard_price") && 기준단가} - {isColVisible("status") && 상태} - - - - {itemLoading ? ( - - ) : items.length === 0 ? ( - 등록된 구매품목이 없어요 - ) : items.map((item) => ( - setSelectedItemId(item.id)} - onDoubleClick={openEditItem} - > - {item.item_number} - {item.item_name} - {isColVisible("size") && {item.size || "-"}} - {isColVisible("unit") && {item.unit || "-"}} - {isColVisible("standard_price") && {item.standard_price ? Number(item.standard_price).toLocaleString() : "-"}} - {isColVisible("status") && ( - - {item.status || "-"} - - )} - - ))} - -
-
+ setSelectedItemId(id)} + onRowDoubleClick={() => openEditItem()} + showPagination={true} + draggableColumns={false} + columnOrderKey="c16-purchase-item-main" + /> diff --git a/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx index 8c06cbb8..4eb3ed3f 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -20,6 +20,7 @@ import { exportToExcel } from "@/lib/utils/excelExport"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const SUPPLIER_TABLE = "supplier_mng"; const MAPPING_TABLE = "supplier_item_mapping"; @@ -101,6 +102,24 @@ export default function SupplierManagementPage() { const isColVisible = (key: string) => ts.isVisible(key); const supplierColSpan = 2 + SUPPLIER_COLUMNS.filter((c) => isColVisible(c.key)).length; + const mainTableColumns = useMemo(() => { + const cols: EDataTableColumn[] = [ + { key: "supplier_code", label: "공급업체코드", width: "w-[120px]" }, + { key: "supplier_name", label: "공급업체명" }, + ]; + if (isColVisible("contact_person")) cols.push({ key: "contact_person", label: "담당자", width: "w-[90px]", render: (v) => v || "-" }); + if (isColVisible("contact_phone")) cols.push({ key: "contact_phone", label: "연락처", width: "w-[120px]", render: (v) => v || "-" }); + if (isColVisible("status")) cols.push({ + key: "status", label: "상태", width: "w-[70px]", align: "center", + render: (v) => ( + {v || "-"} + ), + }); + return cols; + }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + // 우측: 품목 매핑 조회 useEffect(() => { if (!selectedSupplier?.supplier_code) { setMappingItems([]); setMappingCheckedIds([]); return; } @@ -369,48 +388,18 @@ export default function SupplierManagementPage() { -
- - - - 공급업체코드 - 공급업체명 - {isColVisible("contact_person") && 담당자} - {isColVisible("contact_phone") && 연락처} - {isColVisible("status") && 상태} - - - - {supplierLoading ? ( - - ) : suppliers.length === 0 ? ( - 등록된 공급업체가 없어요 - ) : suppliers.map((s) => ( - setSelectedSupplierId(s.id)} - onDoubleClick={openSupplierEdit} - > - {s.supplier_code} - {s.supplier_name} - {isColVisible("contact_person") && {s.contact_person || "-"}} - {isColVisible("contact_phone") && {s.contact_phone || "-"}} - {isColVisible("status") && ( - - {s.status || "-"} - - )} - - ))} - -
-
+ setSelectedSupplierId(id)} + onRowDoubleClick={() => openSupplierEdit()} + showPagination={true} + draggableColumns={false} + columnOrderKey="c16-supplier-main" + /> diff --git a/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx b/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx index 56226c40..f56f5582 100644 --- a/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -38,6 +38,7 @@ import { toast } from "sonner"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; /* ───── 테이블명 ───── */ const INSPECTION_TABLE = "inspection_standard"; @@ -176,6 +177,16 @@ export default function InspectionManagementPage() { return opts.find((o) => o.code === code)?.label || code; }; + const inspTableColumns = useMemo(() => { + return ts.visibleColumns.map((col) => { + const base: EDataTableColumn = { key: col.key, label: col.label }; + if (["inspection_type", "inspection_method", "judgment_criteria", "unit", "apply_type"].includes(col.key)) { + base.render = (v: any, row: any) => getCatLabel(INSPECTION_TABLE, col.key, row[col.key]); + } + return base; + }); + }, [ts.visibleColumns, catOptions]); // eslint-disable-line react-hooks/exhaustive-deps + /* ═══════════════════ 데이터 조회 ═══════════════════ */ // 다중값 컬럼 (쉼표 구분 저장) — 서버 equals 대신 contains 사용 const MULTI_VALUE_COLUMNS = ["inspection_type"]; @@ -574,99 +585,19 @@ export default function InspectionManagementPage() { />
- - - - - 0 && inspChecked.length === inspections.length} - onCheckedChange={(v) => setInspChecked(v ? inspections.map((r) => r.id) : [])} - /> - - {ts.visibleColumns.map((col) => ( - - {col.label} - - ))} - - - - {inspLoading ? ( - - - - - - ) : inspections.length === 0 ? ( - - - -

등록된 검사기준이 없어요

-
-
- ) : ( - inspections.map((row) => ( - - setInspChecked((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id], - ) - } - onDoubleClick={() => openInspEdit(row)} - > - e.stopPropagation()}> - - setInspChecked((prev) => (v ? [...prev, row.id] : prev.filter((id) => id !== row.id))) - } - /> - - {ts.visibleColumns.map((col) => { - if (col.key === "inspection_type") - return ( - - {getCatLabel(INSPECTION_TABLE, "inspection_type", row.inspection_type)} - - ); - if (col.key === "inspection_method") - return ( - - {getCatLabel(INSPECTION_TABLE, "inspection_method", row.inspection_method)} - - ); - if (col.key === "judgment_criteria") - return ( - - {getCatLabel(INSPECTION_TABLE, "judgment_criteria", row.judgment_criteria)} - - ); - if (col.key === "unit") - return ( - {getCatLabel(INSPECTION_TABLE, "unit", row.unit)} - ); - if (col.key === "apply_type") - return ( - - {getCatLabel(INSPECTION_TABLE, "apply_type", row.apply_type)} - - ); - return {row[col.key] ?? ""}; - })} - - )) - )} -
-
+ openInspEdit(row)} + showPagination={true} + draggableColumns={false} + columnOrderKey="c16-inspection-main" + />
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 dccae8da..d030846f 100644 --- a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx @@ -20,6 +20,7 @@ import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { toast } from "sonner"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const TABLE_NAME = "item_inspection_info"; @@ -302,48 +303,29 @@ export default function ItemInspectionInfoPage() { />
-
- - - - - 0 && checkedIds.length === data.length} - onCheckedChange={(v) => setCheckedIds(v ? data.map(r => r.id) : [])} - /> - - {ts.visibleColumns.map((col) => ( - {col.label} - ))} - - - - {loading ? ( - - ) : data.length === 0 ? ( -

등록된 품목검사정보가 없어요

- ) : data.map((row) => ( - setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id])} - onDoubleClick={() => openEdit(row)} - > - e.stopPropagation()}> - setCheckedIds(prev => v ? [...prev, row.id] : prev.filter(id => id !== row.id))} /> - - {ts.visibleColumns.map((col) => ( - - {col.key === "is_active" - ? {row.is_active ? "사용" : "미사용"} - : row[col.key] ?? ""} - - ))} - - ))} -
-
-
+ ({ + key: col.key, + label: col.label, + render: col.key === "is_active" + ? (val: any, row: any) => ( + + {row.is_active ? "사용" : "미사용"} + + ) + : undefined, + }))} + data={ts.groupData(data)} + loading={loading} + emptyMessage="등록된 품목검사정보가 없어요" + showCheckbox + checkedIds={checkedIds} + onCheckedChange={setCheckedIds} + onRowDoubleClick={(row) => openEdit(row)} + showPagination + draggableColumns={false} + columnOrderKey="c16-item-inspection" + />
diff --git a/frontend/app/(main)/COMPANY_16/sales/claim/page.tsx b/frontend/app/(main)/COMPANY_16/sales/claim/page.tsx index 78b0d1ca..a2c89599 100644 --- a/frontend/app/(main)/COMPANY_16/sales/claim/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/claim/page.tsx @@ -63,6 +63,7 @@ import { Wrench, Settings2, } from "lucide-react"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; @@ -463,91 +464,38 @@ export default function ClaimManagementPage() { {/* 테이블 */} -
- - - - # - {ts.visibleColumns.map((col) => ( - - {col.label} - - ))} - - - - {loading && data.length === 0 ? ( - - -
- - 불러오는 중... -
-
-
- ) : data.length === 0 ? ( - - -
- - 등록된 클레임이 없어요 -
-
-
- ) : ( - data.map((claim, idx) => ( - handleRowClick(claim.claim_no)} - onDoubleClick={() => openEditModal(claim.claim_no)} - > - - {idx + 1} - - {ts.visibleColumns.map((col) => { - if (col.key === "claim_type") { - return ( - - - {claim.claim_type} - - - ); - } - if (col.key === "claim_status") { - return ( - - - {claim.claim_status} - - - ); - } - if (col.key === "claim_content") { - return ( - - {claim.claim_content} - - ); - } - return ( - - {claim[col.key] ?? "-"} - - ); - })} - - )) - )} -
-
-
+ => ({ + key: col.key, + label: col.label, + align: col.key === "claim_type" || col.key === "claim_status" ? "center" : undefined, + render: col.key === "claim_type" + ? (val: any) => ( + + {val} + + ) + : col.key === "claim_status" + ? (val: any) => ( + + {val} + + ) + : undefined, + }))} + data={ts.groupData(data)} + loading={loading} + emptyMessage="등록된 클레임이 없어요" + rowKey={(row) => String(row.id)} + selectedId={selectedClaimNo ? String(data.find(c => c.claim_no === selectedClaimNo)?.id ?? "") : null} + onSelect={(id) => { + const claim = data.find(c => String(c.id) === id); + handleRowClick(claim?.claim_no ?? ""); + }} + onRowDoubleClick={(row) => openEditModal(row.claim_no)} + showRowNumber + draggableColumns={false} + /> diff --git a/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx b/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx index f888bddf..26a3fd94 100644 --- a/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx @@ -38,6 +38,7 @@ import { validateField, validateForm, formatField } from "@/lib/utils/validation import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { useTableSettings } from "@/hooks/useTableSettings"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const CUSTOMER_TABLE = "customer_mng"; const MAPPING_TABLE = "customer_item_mapping"; @@ -777,6 +778,39 @@ export default function CustomerManagementPage() { const customerColSpan = 1 + ["customer_code", "customer_name", "contact_person", "contact_phone", "division", "status"] .filter((k) => isColumnVisible(k)).length; + // EDataTable 컬럼 정의 (거래처 목록) + const customerColumns: EDataTableColumn[] = [ + ...(isColumnVisible("customer_code") ? [{ key: "customer_code", label: "거래처코드", width: "w-[120px]" }] : []), + ...(isColumnVisible("customer_name") ? [{ key: "customer_name", label: "거래처명", minWidth: "min-w-[160px]" }] : []), + ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "대표자", width: "w-[90px]" }] : []), + ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "연락처", width: "w-[120px]" }] : []), + ...(isColumnVisible("division") ? [{ + key: "division", + label: "유형", + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }] : []), + ...(isColumnVisible("status") ? [{ + key: "status", + label: "상태", + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }] : []), + ]; + // 엑셀 다운로드 const handleExcelDownload = async () => { if (customers.length === 0) return; @@ -914,73 +948,20 @@ export default function CustomerManagementPage() { {/* 거래처 테이블 */} -
- - - - No - {isColumnVisible("customer_code") && 거래처코드} - {isColumnVisible("customer_name") && 거래처명} - {isColumnVisible("contact_person") && 대표자} - {isColumnVisible("contact_phone") && 연락처} - {isColumnVisible("division") && 유형} - {isColumnVisible("status") && 상태} - - - - {customerLoading ? ( - - - - - - ) : customers.length === 0 ? ( - - - 등록된 거래처가 없어요 - - - ) : customers.map((c, idx) => ( - setSelectedCustomerId(c.id)} - onDoubleClick={() => { setSelectedCustomerId(c.id); openCustomerEdit(); }} - > - {idx + 1} - {isColumnVisible("customer_code") && {c.customer_code}} - {isColumnVisible("customer_name") && {c.customer_name}} - {isColumnVisible("contact_person") && {c.contact_person}} - {isColumnVisible("contact_phone") && {c.contact_phone}} - {isColumnVisible("division") && ( - - {c.division && ( - - {c.division} - - )} - - )} - {isColumnVisible("status") && ( - - {c.status && ( - - {c.status} - - )} - - )} - - ))} - -
-
+ row.id} + loading={customerLoading} + emptyMessage="등록된 거래처가 없어요" + selectedId={selectedCustomerId} + onSelect={(id) => setSelectedCustomerId(id)} + onRowDoubleClick={(row) => { setSelectedCustomerId(row.id); openCustomerEdit(); }} + showRowNumber + showPagination={false} + draggableColumns={false} + columnOrderKey="c16-customer" + /> diff --git a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx index f7a711a0..b9bb4683 100644 --- a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx @@ -26,6 +26,7 @@ import { ShippingPlanBatchModal } from "@/components/common/ShippingPlanBatchMod import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const DETAIL_TABLE = "sales_order_detail"; const MASTER_TABLE = "sales_order_mng"; @@ -560,13 +561,6 @@ export default function SalesOrderPage() { toast.success("다운로드 완료"); }; - // 전체 선택/해제 - const isAllChecked = orders.length > 0 && orders.every((o) => checkedIds.includes(o.id)); - const toggleAllChecked = () => { - if (isAllChecked) setCheckedIds([]); - else setCheckedIds(orders.map((o) => o.id).filter(Boolean)); - }; - return (
{/* 브레드크럼 */} @@ -634,99 +628,25 @@ export default function SalesOrderPage() { {/* 데이터 테이블 */}
-
- - - - - - - {ts.isVisible("order_no") && 수주번호} - {ts.isVisible("part_code") && 품번} - {ts.isVisible("part_name") && 품명} - {ts.isVisible("spec") && 규격} - {ts.isVisible("unit") && 단위} - {ts.isVisible("qty") && 수량} - {ts.isVisible("ship_qty") && 출하수량} - {ts.isVisible("balance_qty") && 잔량} - {ts.isVisible("unit_price") && 단가} - {ts.isVisible("amount") && 금액} - {ts.isVisible("currency_code") && 통화} - {ts.isVisible("due_date") && 납기일} - {ts.isVisible("memo") && 메모} - - - - {loading ? ( - - - - - - ) : orders.length === 0 ? ( - - -
- - 등록된 수주가 없어요 -
-
-
- ) : ( - orders.map((row) => { - const isChecked = checkedIds.includes(row.id); - return ( - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - )} - onDoubleClick={() => openEditModal(row.order_no)} - > - - - - {ts.isVisible("order_no") && {row.order_no}} - {ts.isVisible("part_code") && ( - - {row.part_code} - - )} - {ts.isVisible("part_name") && ( - - {row.part_name} - - )} - {ts.isVisible("spec") && {row.spec}} - {ts.isVisible("unit") && {row.unit}} - {ts.isVisible("qty") && {row.qty ? Number(row.qty).toLocaleString() : ""}} - {ts.isVisible("ship_qty") && {row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""}} - {ts.isVisible("balance_qty") && {row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""}} - {ts.isVisible("unit_price") && {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}} - {ts.isVisible("amount") && {row.amount ? Number(row.amount).toLocaleString() : ""}} - {ts.isVisible("currency_code") && {row.currency_code}} - {ts.isVisible("due_date") && {row.due_date}} - {ts.isVisible("memo") && ( - - {row.memo} - - )} - - ); - }) - )} -
-
-
+ ({ + key: col.key, + label: col.label, + align: ["qty", "ship_qty", "balance_qty", "unit_price", "amount"].includes(col.key) ? "right" : undefined, + formatNumber: ["qty", "ship_qty", "balance_qty", "unit_price", "amount"].includes(col.key), + }))} + data={ts.groupData(orders)} + loading={loading} + emptyMessage="등록된 수주가 없어요" + emptyIcon={} + showCheckbox + checkedIds={checkedIds} + onCheckedChange={setCheckedIds} + onRowDoubleClick={(row) => openEditModal(row.order_no)} + showPagination + draggableColumns={false} + columnOrderKey="c16-sales-order" + />
{/* 수주 등록/수정 모달 */} 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 1ccf6d2c..3c510588 100644 --- a/frontend/app/(main)/COMPANY_16/sales/sales-item/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/sales-item/page.tsx @@ -29,6 +29,7 @@ import { exportToExcel } from "@/lib/utils/excelExport"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { useTableSettings } from "@/hooks/useTableSettings"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const ITEM_TABLE = "item_info"; const MAPPING_TABLE = "customer_item_mapping"; @@ -605,6 +606,18 @@ export default function SalesItemPage() { toast.success("다운로드 완료"); }; + // EDataTable 컬럼 정의 (판매품목) + const itemColumns: EDataTableColumn[] = [ + { key: "item_number", label: "품번", width: "w-[110px]" }, + { key: "item_name", label: "품명", minWidth: "min-w-[130px]" }, + { key: "size", label: "규격", width: "w-[80px]" }, + { key: "unit", label: "단위", width: "w-[60px]" }, + { key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }, + { key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true }, + { key: "currency_code", label: "통화", width: "w-[50px]" }, + { key: "status", label: "상태", width: "w-[60px]" }, + ]; + return (
@@ -649,58 +662,20 @@ export default function SalesItemPage() {
{/* 테이블 영역 */} -
- {itemLoading ? ( -
- -
- ) : items.length === 0 ? ( -
- 등록된 판매품목이 없어요 -
- ) : ( - - - - # - 품번 - 품명 - 규격 - 단위 - 기준단가 - 판매가격 - 통화 - 상태 - - - - {items.map((item, idx) => ( - setSelectedItemId(item.id)} - onDoubleClick={() => openEditItem()} - > - {idx + 1} - {item.item_number} - {item.item_name} - {item.size} - {item.unit} - {formatNum(item.standard_price)} - {formatNum(item.selling_price)} - {item.currency_code} - {item.status} - - ))} - -
- )} -
+ row.id} + loading={itemLoading} + emptyMessage="등록된 판매품목이 없어요" + selectedId={selectedItemId} + onSelect={(id) => setSelectedItemId(id)} + onRowDoubleClick={() => openEditItem()} + showRowNumber + showPagination={false} + draggableColumns={false} + columnOrderKey="c16-sales-item" + />
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 86ab3f0d..4ab5a9ad 100644 --- a/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx @@ -25,6 +25,7 @@ import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const GRID_COLUMNS = [ { key: "instruction_no", label: "출하지시번호" }, @@ -201,10 +202,6 @@ export default function ShippingOrderPage() { } }, [isModalOpen, dataSource]); - const handleCheckAll = (checked: boolean) => { - setCheckedIds(checked ? orders.map((o: any) => o.id) : []); - }; - const handleDeleteSelected = async () => { if (checkedIds.length === 0) return; if (!confirm(`선택한 ${checkedIds.length}개의 출하지시를 삭제하시겠습니까?`)) return; @@ -392,6 +389,70 @@ export default function ShippingOrderPage() { const formatDate = (d: string) => d ? d.split("T")[0] : "-"; + // 출하지시 데이터를 플랫한 행 목록으로 변환 (EDataTable용) + const flattenedOrders = useMemo(() => { + const rows: any[] = []; + for (const order of orders) { + const items = Array.isArray(order.items) ? order.items.filter((it: any) => it.id) : []; + if (items.length === 0) { + rows.push({ + _rowId: String(order.id), + _orderId: order.id, + _order: order, + instruction_no: order.instruction_no, + ship_date: formatDate(order.instruction_date), + customer_name: order.customer_name || "-", + transport_company: order.carrier_name || "-", + vehicle_no: order.vehicle_no || "-", + driver_name: order.driver_name || "-", + status: order.status, + item_code: "-", + item_name: "-", + qty: 0, + source_type: "-", + remark: order.memo || "-", + }); + } else { + items.forEach((item: any, idx: number) => { + rows.push({ + _rowId: `${order.id}-${item.id}`, + _orderId: order.id, + _order: order, + instruction_no: idx === 0 ? order.instruction_no : "", + ship_date: idx === 0 ? formatDate(order.instruction_date) : "", + customer_name: idx === 0 ? (order.customer_name || "-") : "", + transport_company: idx === 0 ? (order.carrier_name || "-") : "", + vehicle_no: idx === 0 ? (order.vehicle_no || "-") : "", + driver_name: idx === 0 ? (order.driver_name || "-") : "", + status: idx === 0 ? order.status : "", + item_code: item.item_code || "", + item_name: item.item_name || "", + qty: Number(item.order_qty || 0), + source_type: item.source_type || "", + remark: idx === 0 ? (order.memo || "-") : "", + }); + }); + } + } + return rows; + }, [orders]); + + // checkedIds를 order.id 기준으로 관리하므로 _orderId로 매핑 + const flatCheckedRowIds = useMemo(() => { + return flattenedOrders + .filter((r) => checkedIds.includes(r._orderId)) + .map((r) => r._rowId); + }, [flattenedOrders, checkedIds]); + + const handleFlatCheckedChange = useCallback((rowIds: string[]) => { + const orderIds = new Set(); + for (const rowId of rowIds) { + const row = flattenedOrders.find((r) => r._rowId === rowId); + if (row) orderIds.add(row._orderId); + } + setCheckedIds(Array.from(orderIds)); + }, [flattenedOrders]); + const dataSourceTitle: Record = { shipmentPlan: "출하계획 목록", salesOrder: "수주정보 목록", @@ -454,138 +515,42 @@ export default function ShippingOrderPage() { {/* 메인 테이블 */}
-
- {loading ? ( -
- -
- ) : ( - - - - - 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") && 비고} - - - - {orders.length === 0 ? ( - - -
-
- -
-

등록된 출하지시가 없어요

-

출하지시 등록 버튼으로 등록해주세요

-
-
-
- ) : ( - orders.map((order: any) => { - const items = Array.isArray(order.items) ? order.items.filter((it: any) => it.id) : []; - if (items.length === 0) { - return ( - setSelectedOrderId(order.id)} - onDoubleClick={() => openModal(order)} - > - e.stopPropagation()}> - { - if (c) setCheckedIds(p => [...p, order.id]); - else setCheckedIds(p => p.filter(i => i !== order.id)); - }} - /> - - {ts.isVisible("instruction_no") && {order.instruction_no}} - {ts.isVisible("ship_date") && {formatDate(order.instruction_date)}} - {ts.isVisible("customer_name") && {order.customer_name || "-"}} - {ts.isVisible("transport_company") && {order.carrier_name || "-"}} - {ts.isVisible("vehicle_no") && {order.vehicle_no || "-"}} - {ts.isVisible("driver_name") && {order.driver_name || "-"}} - {ts.isVisible("status") && - - {getStatusLabel(order.status)} - - } - {ts.isVisible("item_code") && -} - {ts.isVisible("item_name") && -} - {ts.isVisible("qty") && 0} - {ts.isVisible("source_type") && -} - {ts.isVisible("remark") && {order.memo || "-"}} - - ); - } - return items.map((item: any, itemIdx: number) => ( - setSelectedOrderId(order.id)} - onDoubleClick={() => openModal(order)} - > - e.stopPropagation()}> - {itemIdx === 0 && ( - { - if (c) setCheckedIds(p => [...p, order.id]); - else setCheckedIds(p => p.filter(i => i !== order.id)); - }} - /> - )} - - {ts.isVisible("instruction_no") && {itemIdx === 0 ? order.instruction_no : ""}} - {ts.isVisible("ship_date") && {itemIdx === 0 ? formatDate(order.instruction_date) : ""}} - {ts.isVisible("customer_name") && {itemIdx === 0 ? (order.customer_name || "-") : ""}} - {ts.isVisible("transport_company") && {itemIdx === 0 ? (order.carrier_name || "-") : ""}} - {ts.isVisible("vehicle_no") && {itemIdx === 0 ? (order.vehicle_no || "-") : ""}} - {ts.isVisible("driver_name") && {itemIdx === 0 ? (order.driver_name || "-") : ""}} - {ts.isVisible("status") && - {itemIdx === 0 && ( - - {getStatusLabel(order.status)} - - )} - } - {ts.isVisible("item_code") && {item.item_code}} - {ts.isVisible("item_name") && {item.item_name}} - {ts.isVisible("qty") && {Number(item.order_qty || 0).toLocaleString()}} - {ts.isVisible("source_type") && - {(() => { - const b = getSourceBadge(item.source_type || ""); - return {b.label}; - })()} - } - {ts.isVisible("remark") && - {itemIdx === 0 ? (order.memo || "-") : ""} - } - - )); - }) - )} -
-
- )} -
+ ({ + key: col.key, + label: col.label, + align: col.key === "qty" ? "right" : col.key === "status" || col.key === "source_type" || col.key === "ship_date" ? "center" : undefined, + formatNumber: col.key === "qty", + sortable: false, + filterable: false, + render: col.key === "status" + ? (val: any) => val ? ( + + {getStatusLabel(val)} + + ) : null + : col.key === "source_type" + ? (val: any) => { + if (!val || val === "-") return -; + const b = getSourceBadge(val); + return {b.label}; + } + : undefined, + }))} + data={ts.groupData(flattenedOrders)} + rowKey={(row) => row._rowId} + loading={loading} + emptyMessage="등록된 출하지시가 없어요" + showCheckbox + checkedIds={flatCheckedRowIds} + onCheckedChange={handleFlatCheckedChange} + selectedId={selectedOrderId != null ? String(selectedOrderId) : null} + onRowClick={(row) => setSelectedOrderId(row._orderId)} + onRowDoubleClick={(row) => openModal(row._order)} + showPagination + draggableColumns={false} + columnOrderKey="c16-shipping-order" + />
{/* 등록/수정 모달 */} 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 1a47e986..747ac23d 100644 --- a/frontend/app/(main)/COMPANY_16/sales/shipping-plan/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/shipping-plan/page.tsx @@ -18,6 +18,7 @@ import { import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const GRID_COLUMNS = [ { key: "order_no", label: "수주번호" }, @@ -114,10 +115,11 @@ export default function ShippingPlanPage() { const selectedPlan = useMemo(() => data.find(p => p.id === selectedId), [data, selectedId]); const groupedData = useMemo(() => { - const orderMap = new Map(); + const grouped = ts.groupData(data); + const orderMap = new Map(); const orderKeys: string[] = []; - data.forEach(plan => { - const key = plan.order_no || `_no_order_${plan.id}`; + grouped.forEach(plan => { + const key = (plan as any)._isGroupSummary ? `_summary_${orderKeys.length}` : (plan.order_no || `_no_order_${plan.id}`); if (!orderMap.has(key)) { orderMap.set(key, []); orderKeys.push(key); @@ -128,7 +130,7 @@ export default function ShippingPlanPage() { orderNo: key, plans: orderMap.get(key)!, })); - }, [data]); + }, [data, ts.groupData]); const handleRowClick = (plan: ShipmentPlanListItem) => { if (isDetailChanged && selectedId !== plan.id) { @@ -233,91 +235,36 @@ 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") && 상태} - - - - {groupedData.length === 0 ? ( - - -
-
- -
-

출하계획이 없어요

-

조건을 변경해서 다시 조회해주세요

-
-
-
- ) : ( - groupedData.map(group => - group.plans.map((plan, planIdx) => ( - handleRowClick(plan)} - > - e.stopPropagation()}> - {planIdx === 0 && ( - checkedIds.includes(p.id))} - onCheckedChange={(c) => { - if (c) { - setCheckedIds(prev => [...new Set([...prev, ...group.plans.filter(p => p.status !== "CANCELLED").map(p => p.id)])]); - } else { - setCheckedIds(prev => prev.filter(id => !group.plans.some(p => p.id === id))); - } - }} - /> - )} - - {ts.isVisible("order_no") && - {planIdx === 0 ? (plan.order_no || "-") : ""} - } - {ts.isVisible("due_date") && - {planIdx === 0 ? formatDate(plan.due_date) : ""} - } - {ts.isVisible("customer_name") && - {planIdx === 0 ? (plan.customer_name || "-") : ""} - } - {ts.isVisible("part_code") && {plan.part_code || "-"}} - {ts.isVisible("part_name") && {plan.part_name || "-"}} - {ts.isVisible("order_qty") && {formatNumber(plan.order_qty)}} - {ts.isVisible("plan_qty") && {formatNumber(plan.plan_qty)}} - {ts.isVisible("plan_date") && {formatDate(plan.plan_date)}} - {ts.isVisible("status") && - - {getStatusLabel(plan.status)} - - } - - )) - ) - )} -
-
+ {val || "-"} }, + { key: "due_date", label: "납기일", align: "center" as const, render: (val: any) => {formatDate(val)} }, + { key: "customer_name", label: "거래처", render: (val: any) => {val || "-"} }, + { key: "part_code", label: "품목코드", render: (val: any) => {val || "-"} }, + { key: "part_name", label: "품목명", render: (val: any) => {val || "-"} }, + { key: "order_qty", label: "수주수량", align: "right" as const, formatNumber: true }, + { key: "plan_qty", label: "계획수량", align: "right" as const, render: (val: any) => {formatNumber(val)} }, + { key: "plan_date", label: "계획일", align: "center" as const, render: (val: any) => {formatDate(val)} }, + { key: "status", label: "상태", align: "center" as const, render: (val: any) => {getStatusLabel(val)} }, + ] as EDataTableColumn[]} + data={data} + rowKey={(row) => String(row.id)} + loading={loading} + emptyMessage="출하계획이 없어요" + selectedId={selectedId !== null ? String(selectedId) : null} + onSelect={(id) => { + if (id) { + const plan = data.find(p => String(p.id) === id); + if (plan) handleRowClick(plan); + } + }} + onRowClick={(row) => handleRowClick(row)} + showCheckbox + checkedIds={checkedIds.map(String)} + onCheckedChange={(ids) => setCheckedIds(ids.map(Number))} + showPagination={false} + draggableColumns={false} + />
diff --git a/frontend/components/common/DynamicSearchFilter.tsx b/frontend/components/common/DynamicSearchFilter.tsx index 7e3316d2..28a57567 100644 --- a/frontend/components/common/DynamicSearchFilter.tsx +++ b/frontend/components/common/DynamicSearchFilter.tsx @@ -147,7 +147,10 @@ export function DynamicSearchFilter({ } setAllColumns(merged); - setActiveFilters(merged.filter((c) => c.enabled)); + // externalFilterConfig가 있으면 외부 설정이 activeFilters를 관리하므로 건드리지 않음 + if (!externalFilterConfig) { + setActiveFilters(merged.filter((c) => c.enabled)); + } // 저장된 필터 값 복원 const savedValues = localStorage.getItem(STORAGE_KEY_VALUES); diff --git a/frontend/components/common/EDataTable.tsx b/frontend/components/common/EDataTable.tsx new file mode 100644 index 00000000..034de5ef --- /dev/null +++ b/frontend/components/common/EDataTable.tsx @@ -0,0 +1,795 @@ +"use client"; + +/** + * EDataTable — 직접 구현 페이지용 공통 데이터 테이블 컴포넌트 + * + * 프리셋 디자인 규격(Type A~F) 기반, shadcn/ui 위에 구축. + * 기능: 정렬, 헤더 필터, 컬럼 드래그 이동, 인라인 편집, 체크박스, 페이지네이션 + */ + +import React, { useState, useEffect, useRef, useCallback, useMemo } from "react"; +import { + DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent, +} from "@dnd-kit/core"; +import { SortableContext, horizontalListSortingStrategy, useSortable, arrayMove } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Badge } from "@/components/ui/badge"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Input } from "@/components/ui/input"; +import { + Filter, Check, Search, X, Loader2, Inbox, GripVertical, + ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, ArrowUp, ArrowDown, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { toast } from "sonner"; +import { apiClient } from "@/lib/api/client"; + +// ─── 타입 ─── + +export interface EDataTableColumn { + key: string; + label: string; + width?: string; + minWidth?: string; + align?: "left" | "center" | "right"; + sortable?: boolean; + filterable?: boolean; + editable?: boolean; + inputType?: "text" | "number" | "date" | "select"; + selectOptions?: { value: string; label: string }[]; + formatNumber?: boolean; + truncate?: boolean; + render?: (value: any, row: T, rowIndex: number) => React.ReactNode; +} + +export interface SortState { + key: string; + direction: "asc" | "desc"; +} + +export interface EDataTableProps = any> { + columns: EDataTableColumn[]; + data: T[]; + rowKey?: (row: T) => string; + + loading?: boolean; + emptyMessage?: string; + emptyIcon?: React.ReactNode; + + selectedId?: string | null; + onSelect?: (id: string | null) => void; + + showCheckbox?: boolean; + checkedIds?: string[]; + onCheckedChange?: (ids: string[]) => void; + + onRowClick?: (row: T, index: number) => void; + onRowDoubleClick?: (row: T, index: number) => void; + + onCellEdit?: (rowId: string, columnKey: string, newValue: any, row: T) => void; + tableName?: string; + + sort?: SortState | null; + onSortChange?: (sort: SortState | null) => void; + + draggableColumns?: boolean; + onColumnOrderChange?: (columns: EDataTableColumn[]) => void; + columnOrderKey?: string; + + showRowNumber?: boolean; + showPagination?: boolean; + defaultPageSize?: number; + + className?: string; +} + +// ─── 유틸 ─── + +const fmtNum = (val: any) => { + if (val == null || val === "") return ""; + const n = Number(String(val).replace(/,/g, "")); + if (isNaN(n)) return String(val); + return n.toLocaleString(); +}; + +const getRowId = (row: any, rowKey?: (row: any) => string) => { + if (rowKey) return rowKey(row); + return row.id ?? row._id ?? ""; +}; + +// ─── SortableHeaderCell ─── + +function SortableHeaderCell({ + col, sortKey, sortDir, onSort, + headerFilterValues, uniqueValues, onToggleFilter, onClearFilter, + draggable, +}: { + col: EDataTableColumn; + sortKey: string | null; + sortDir: "asc" | "desc"; + onSort: (key: string) => void; + headerFilterValues: Set; + uniqueValues: string[]; + onToggleFilter: (colKey: string, value: string) => void; + onClearFilter: (colKey: string) => void; + draggable: boolean; +}) { + const [filterSearch, setFilterSearch] = useState(""); + const { + attributes, listeners, setNodeRef, transform, transition, isDragging, + } = useSortable({ id: col.key, disabled: !draggable }); + + const style: React.CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + const isSorted = sortKey === col.key; + const hasFilter = headerFilterValues.size > 0; + const filteredUniqueValues = uniqueValues.filter( + (v) => !filterSearch || v.toLowerCase().includes(filterSearch.toLowerCase()) + ); + + return ( + +
+ {/* 드래그 핸들 */} + {draggable && ( +
+ +
+ )} + + {/* 컬럼 라벨 + 정렬 */} +
{ + e.stopPropagation(); + if (col.sortable !== false) onSort(col.key); + }} + > + {col.label} + {isSorted && ( + sortDir === "asc" + ? + : + )} +
+ + {/* 필터 아이콘 + Popover */} + {col.filterable !== false && uniqueValues.length > 0 && ( + + + + + e.stopPropagation()}> +
+
+ 필터: {col.label} + {hasFilter && ( + + )} +
+
+ + setFilterSearch(e.target.value)} + placeholder="검색..." + className="h-7 text-xs pl-7" + /> +
+
+ {filteredUniqueValues.slice(0, 100).map((val) => { + const isSelected = headerFilterValues.has(val); + return ( +
onToggleFilter(col.key, val)} + > +
+ {isSelected && } +
+ {val || "(빈 값)"} +
+ ); + })} + {filteredUniqueValues.length > 100 && ( +
+ ...외 {filteredUniqueValues.length - 100}개 +
+ )} +
+
+
+
+ )} +
+
+ ); +} + +// ─── EDataTable ─── + +export function EDataTable = any>({ + columns: initialColumns, + data, + rowKey, + loading = false, + emptyMessage = "데이터가 없어요", + emptyIcon, + selectedId, + onSelect, + showCheckbox = false, + checkedIds = [], + onCheckedChange, + onRowClick, + onRowDoubleClick, + onCellEdit, + tableName, + sort: externalSort, + onSortChange, + draggableColumns = true, + onColumnOrderChange, + columnOrderKey, + showRowNumber = false, + showPagination = true, + defaultPageSize = 50, + className, +}: EDataTableProps) { + const [columns, setColumns] = useState(initialColumns); + useEffect(() => { setColumns(initialColumns); }, [initialColumns]); + + // 정렬 + const [internalSort, setInternalSort] = useState(null); + const sortState = externalSort !== undefined ? externalSort : internalSort; + + // 헤더 필터 + const [headerFilters, setHeaderFilters] = useState>>({}); + + // 페이지네이션 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(defaultPageSize); + const [pageSizeInput, setPageSizeInput] = useState(String(defaultPageSize)); + + // 인라인 편집 + const [editingCell, setEditingCell] = useState<{ rowId: string; colKey: string } | null>(null); + const [editValue, setEditValue] = useState(""); + const editRef = useRef(null); + + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 8 } }) + ); + + // localStorage에서 컬럼 순서 복원 + useEffect(() => { + if (!columnOrderKey) return; + const saved = localStorage.getItem(`edatatable_col_order_${columnOrderKey}`); + if (saved) { + try { + const order = JSON.parse(saved) as string[]; + const reordered = order + .map((key) => initialColumns.find((c) => c.key === key)) + .filter(Boolean) as EDataTableColumn[]; + const remaining = initialColumns.filter((c) => !order.includes(c.key)); + setColumns([...reordered, ...remaining]); + } catch { /* skip */ } + } + }, [columnOrderKey]); // eslint-disable-line react-hooks/exhaustive-deps + + // 컬럼별 고유값 + const columnUniqueValues = useMemo(() => { + const result: Record = {}; + for (const col of columns) { + if (col.filterable === false) continue; + const values = new Set(); + data.forEach((row) => { + const val = row[col.key]; + if (val !== null && val !== undefined && val !== "") { + values.add(String(val)); + } + }); + result[col.key] = Array.from(values).sort(); + } + return result; + }, [data, columns]); + + // 드래그 완료 + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + setColumns((prev) => { + const oldIndex = prev.findIndex((c) => c.key === active.id); + const newIndex = prev.findIndex((c) => c.key === over.id); + const next = arrayMove(prev, oldIndex, newIndex); + if (columnOrderKey) { + localStorage.setItem(`edatatable_col_order_${columnOrderKey}`, JSON.stringify(next.map((c) => c.key))); + } + onColumnOrderChange?.(next); + return next; + }); + }; + + // 정렬 + const handleSort = (key: string) => { + const newSort: SortState | null = sortState?.key === key + ? sortState.direction === "asc" + ? { key, direction: "desc" } + : null + : { key, direction: "asc" }; + + if (onSortChange) { + onSortChange(newSort); + } else { + setInternalSort(newSort); + } + }; + + // 헤더 필터 + const toggleHeaderFilter = (colKey: string, value: string) => { + setHeaderFilters((prev) => { + const next = { ...prev }; + const set = new Set(next[colKey] || []); + if (set.has(value)) set.delete(value); else set.add(value); + if (set.size === 0) delete next[colKey]; else next[colKey] = set; + return next; + }); + }; + + const clearHeaderFilter = (colKey: string) => { + setHeaderFilters((prev) => { + const next = { ...prev }; + delete next[colKey]; + return next; + }); + }; + + // 필터 + 정렬 + const processedData = useMemo(() => { + let result = [...data]; + + // 헤더 필터 + if (Object.keys(headerFilters).length > 0) { + result = result.filter((row) => + Object.entries(headerFilters).every(([colKey, values]) => { + if (values.size === 0) return true; + const cellVal = row[colKey] != null ? String(row[colKey]) : ""; + return values.has(cellVal); + }) + ); + } + + // 정렬 (외부 정렬이 아닌 경우만) + if (sortState && !onSortChange) { + const { key, direction } = sortState; + result.sort((a, b) => { + const av = a[key] ?? ""; + const bv = b[key] ?? ""; + const na = Number(av); + const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" + ? String(av).localeCompare(String(bv)) + : String(bv).localeCompare(String(av)); + }); + } + + return result; + }, [data, headerFilters, sortState, onSortChange]); + + // 필터/데이터 변경 시 1페이지 리셋 + useEffect(() => { setCurrentPage(1); }, [data, headerFilters]); + + // 페이지네이션 + const totalItems = processedData.length; + const totalPages = Math.max(1, Math.ceil(totalItems / pageSize)); + const safePage = Math.min(currentPage, totalPages); + + useEffect(() => { + if (currentPage > totalPages) setCurrentPage(totalPages); + }, [currentPage, totalPages]); + + const pageOffset = (safePage - 1) * pageSize; + const paginatedData = showPagination + ? processedData.slice(pageOffset, pageOffset + pageSize) + : processedData; + + const applyPageSize = () => { + const n = parseInt(pageSizeInput, 10); + if (!isNaN(n) && n >= 1) { + setPageSize(n); + setCurrentPage(1); + setPageSizeInput(String(n)); + } else { + setPageSizeInput(String(pageSize)); + } + }; + + const getPageNumbers = () => { + const delta = 2; + let start = Math.max(1, safePage - delta); + let end = Math.min(totalPages, safePage + delta); + if (end - start < delta * 2) { + if (start === 1) end = Math.min(totalPages, start + delta * 2); + else if (end === totalPages) start = Math.max(1, end - delta * 2); + } + const pages: (number | "...")[] = []; + if (start > 1) { pages.push(1); if (start > 2) pages.push("..."); } + for (let i = start; i <= end; i++) pages.push(i); + if (end < totalPages) { if (end < totalPages - 1) pages.push("..."); pages.push(totalPages); } + return pages; + }; + + // 인라인 편집 + const startEdit = (rowId: string, colKey: string, currentVal: any) => { + const col = columns.find((c) => c.key === colKey); + if (!col?.editable) return; + setEditingCell({ rowId, colKey }); + setEditValue(currentVal != null ? String(currentVal) : ""); + }; + + const saveEdit = useCallback(async () => { + if (!editingCell) return; + const { rowId, colKey } = editingCell; + const row = paginatedData.find((r) => getRowId(r, rowKey) === rowId); + if (!row) { setEditingCell(null); return; } + + const originalVal = String(row[colKey] ?? ""); + if (originalVal === editValue) { setEditingCell(null); return; } + + if (tableName && row.id) { + try { + await apiClient.put(`/table-management/tables/${tableName}/edit`, { + originalData: { id: row.id }, + updatedData: { [colKey]: editValue || null }, + }); + (row as any)[colKey] = editValue; + toast.success("저장되었어요"); + } catch { + toast.error("저장에 실패했어요"); + setEditingCell(null); + return; + } + } + + onCellEdit?.(rowId, colKey, editValue, row as T); + setEditingCell(null); + }, [editingCell, editValue, paginatedData, tableName, onCellEdit, rowKey]); + + const cancelEdit = () => setEditingCell(null); + + const handleEditKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { e.preventDefault(); saveEdit(); } + else if (e.key === "Escape") { e.preventDefault(); cancelEdit(); } + else if (e.key === "Tab") { e.preventDefault(); saveEdit(); } + }; + + useEffect(() => { + if (editingCell && editRef.current) { + editRef.current.focus(); + if ("select" in editRef.current) editRef.current.select(); + } + }, [editingCell]); + + // 체크박스 + const allChecked = processedData.length > 0 && checkedIds.length === processedData.length; + + // colSpan 계산 + const colSpan = columns.length + (showCheckbox ? 1 : 0) + (showRowNumber ? 1 : 0); + + // 셀 렌더링 + const renderCell = (row: T, col: EDataTableColumn, rowIdx: number) => { + const id = getRowId(row, rowKey); + const isEditing = editingCell?.rowId === id && editingCell?.colKey === col.key; + const val = row[col.key]; + + // 편집 모드 + if (isEditing) { + if (col.inputType === "select" && col.selectOptions) { + return ( + + ); + } + return ( + setEditValue(e.target.value)} + onKeyDown={handleEditKeyDown} + onBlur={() => saveEdit()} + className={cn( + "h-8 w-full rounded border border-primary bg-background px-2 text-[13px] focus:ring-1 focus:ring-primary", + col.align === "right" && "text-right" + )} + /> + ); + } + + // 커스텀 렌더러 + if (col.render) { + return col.render(val, row, rowIdx); + } + + // 기본 렌더링 + let display: React.ReactNode = val ?? ""; + if (col.formatNumber || col.inputType === "number") display = fmtNum(val); + + return ( + + {display} + + ); + }; + + return ( +
+
+ + + + c.key)} strategy={horizontalListSortingStrategy}> + + {/* 체크박스 */} + {showCheckbox && ( + + { + onCheckedChange?.(checked ? processedData.map((r) => getRowId(r, rowKey)) : []); + }} + /> + + )} + {/* 행번호 */} + {showRowNumber && ( + + # + + )} + {/* 데이터 컬럼 */} + {columns.map((col) => ( + + ))} + + + + + + {loading ? ( + + + + + + ) : paginatedData.length === 0 ? ( + + +
+ {emptyIcon || } + {emptyMessage} +
+
+
+ ) : ( + paginatedData.map((row, rowIdx) => { + // 그룹 소계 행 처리 + if ((row as any)._isGroupSummary) { + return ( + + {showCheckbox && } + {showRowNumber && } + {columns.map((col) => ( + + {typeof row[col.key] === "number" ? Number(row[col.key]).toLocaleString() : (row[col.key] || "")} + + ))} + + ); + } + + const id = getRowId(row, rowKey); + const isSelected = selectedId === id; + const isChecked = checkedIds.includes(id); + const highlighted = isSelected || isChecked; + + return ( + { + onSelect?.(id); + onRowClick?.(row, pageOffset + rowIdx); + if (showCheckbox && onCheckedChange) { + const next = checkedIds.includes(id) + ? checkedIds.filter((cid) => cid !== id) + : [...checkedIds, id]; + onCheckedChange(next); + } + }} + onDoubleClick={() => onRowDoubleClick?.(row, pageOffset + rowIdx)} + > + {showCheckbox && ( + e.stopPropagation()}> + { + const next = checked + ? [...checkedIds, id] + : checkedIds.filter((cid) => cid !== id); + onCheckedChange?.(next); + }} + /> + + )} + {showRowNumber && ( + + {pageOffset + rowIdx + 1} + + )} + {columns.map((col) => ( + { + if (col.editable) { + e.stopPropagation(); + startEdit(id, col.key, row[col.key]); + } + }} + > + {renderCell(row, col, pageOffset + rowIdx)} + + ))} + + ); + }) + )} +
+
+
+
+ + {/* 페이지네이션 */} + {showPagination && ( +
+
+
+ 전체 + {totalItems.toLocaleString()} + +
+
+ setPageSizeInput(e.target.value)} + onBlur={applyPageSize} + onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }} + className="h-7 w-16 text-center text-xs" + /> + 건씩 보기 +
+
+ +
+ + + {getPageNumbers().map((page, idx) => + page === "..." ? ( + ... + ) : ( + + ) + )} + + +
+ +
+
+ )} +
+ ); +} diff --git a/frontend/components/common/TableSettingsModal.tsx b/frontend/components/common/TableSettingsModal.tsx index b3617a97..40db3a3a 100644 --- a/frontend/components/common/TableSettingsModal.tsx +++ b/frontend/components/common/TableSettingsModal.tsx @@ -652,20 +652,22 @@ export function TableSettingsModal({
- {/* 그룹별 합산 토글 */} -
-
-
그룹별 합산
-
같은 값끼리 그룹핑하여 합산
-
- -
{/* ===== 탭 3: 그룹 설정 ===== */} -
- 사용 가능한 컬럼 + {/* 헤더 + 합산 토글 */} +
+
+ 그룹 컬럼 + + {tempGroups.filter((g) => g.enabled).length}개 선택 + +
+
+ 소계 합산 + +
diff --git a/frontend/hooks/useTableSettings.ts b/frontend/hooks/useTableSettings.ts index 10956d0c..7155887c 100644 --- a/frontend/hooks/useTableSettings.ts +++ b/frontend/hooks/useTableSettings.ts @@ -46,6 +46,8 @@ export function useTableSettings( () => initialVisibleKeys || defaultColumns.map((c) => c.key), ); const [baseFilter, setBaseFilter] = useState(); + const [groupColumns, setGroupColumns] = useState([]); + const [groupSumEnabled, setGroupSumEnabled] = useState(false); // 초기 filterConfig: GRID_COLUMNS에 있는 컬럼만 필터 가능 (전부 비활성) const [filterConfig, setFilterConfig] = useState( @@ -96,6 +98,11 @@ export function useTableSettings( // 기본 데이터 필터 setBaseFilter(settings.baseFilter); + + // 그룹 설정 + const enabledGroups = (settings.groups || []).filter((g) => g.enabled).map((g) => g.columnName); + setGroupColumns(enabledGroups); + setGroupSumEnabled(settings.groupSumEnabled || false); }, [defaultColumns, initialVisibleKeys], ); @@ -148,6 +155,50 @@ export function useTableSettings( [columnWidths], ); + /** + * 데이터를 그룹핑하고 소계 행을 삽입한 배열을 반환합니다. + * groupColumns가 비어있으면 원본 배열을 그대로 반환합니다. + * 소계 행은 _isGroupSummary: true, _groupKey, _groupValue 속성을 가집니다. + */ + const groupData = useCallback( + >(rows: R[]): (R & { _isGroupSummary?: boolean; _groupKey?: string; _groupValue?: string })[] => { + if (groupColumns.length === 0) return rows; + + const groupCol = groupColumns[0]; // 첫 번째 그룹 컬럼 기준 + const groups = new Map(); + + for (const row of rows) { + const key = String(row[groupCol] ?? "(빈 값)"); + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(row); + } + + const result: (R & { _isGroupSummary?: boolean; _groupKey?: string; _groupValue?: string })[] = []; + + for (const [groupValue, groupRows] of groups) { + // 그룹 내 데이터 행 + result.push(...groupRows); + + // 소계 행 (groupSumEnabled일 때만) + if (groupSumEnabled) { + const summaryRow: any = { _isGroupSummary: true, _groupKey: groupCol, _groupValue: groupValue }; + // 숫자 컬럼 합산 + for (const col of defaultColumns) { + const values = groupRows.map((r) => Number(r[col.key])).filter((v) => !isNaN(v)); + if (values.length > 0 && values.some((v) => v !== 0)) { + summaryRow[col.key] = values.reduce((a, b) => a + b, 0); + } + } + summaryRow[groupCol] = `${groupValue} 소계 (${groupRows.length}건)`; + result.push(summaryRow); + } + } + + return result; + }, + [groupColumns, groupSumEnabled, defaultColumns], + ); + return { /** 모달 open 상태 */ open, @@ -171,6 +222,12 @@ export function useTableSettings( filterConfig, /** 기본 데이터 필터 (예: division = '판매') */ baseFilter, + /** 데이터 그룹핑 + 소계 삽입 함수 */ + groupData, + /** 그룹 컬럼 목록 */ + groupColumns, + /** 그룹별 합산 활성 여부 */ + groupSumEnabled, /** GRID_COLUMNS 기본 컬럼 키 목록 (TableSettingsModal defaultVisibleKeys용) */ defaultVisibleKeys: initialVisibleKeys || defaultColumns.map((c) => c.key), }; diff --git a/myfile.txt b/myfile.txt new file mode 100644 index 00000000..ce013625 --- /dev/null +++ b/myfile.txt @@ -0,0 +1 @@ +hello diff --git a/test.txt b/test.txt new file mode 100644 index 00000000..ce013625 --- /dev/null +++ b/test.txt @@ -0,0 +1 @@ +hello