feat: Implement dynamic search filter in Shipping Plan page

- Integrated DynamicSearchFilter component to manage search filters.
- Removed individual search state variables and replaced with a single searchFilters state.
- Updated fetchData function to handle new filter structure.
- Refactored search filter UI to utilize DynamicSearchFilter.
- Adjusted table header styles for better visibility and consistency.

style: Update global styles for improved UI consistency

- Unified font size across the application to 16px, excluding buttons.
- Adjusted header padding and font size for better readability.
- Enhanced dark mode styles for checkboxes to ensure visibility.

feat: Add Options Setting page for category and numbering configurations

- Created a new OptionsSettingPage component with tabs for category and numbering settings.
- Implemented drag-to-resize functionality for the category column list.
- Integrated CategoryColumnList and CategoryValueManager components for managing categories.

feat: Introduce useTableSettings hook for table configuration management

- Developed useTableSettings hook to manage column visibility, order, and width.
- Implemented localStorage persistence for table settings.
- Enhanced TableSettingsModal to accept defaultVisibleKeys for initial column visibility.

chore: Update AdminPageRenderer to include new COMPANY_16 routes

- Added new routes for COMPANY_16 master-data options and other pages.
This commit is contained in:
DDD1542
2026-04-03 09:28:59 +09:00
parent f179a575ab
commit d03f92947d
40 changed files with 1863 additions and 2671 deletions
@@ -31,7 +31,6 @@ import {
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
RotateCcw,
Plus,
Save,
Inbox,
@@ -57,6 +56,7 @@ import {
} from "@/lib/api/design";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
// --- Types ---
type ChangeType = "설계오류" | "원가절감" | "고객요청" | "공정개선" | "법규대응";
@@ -363,12 +363,8 @@ export default function DesignChangeManagementPage() {
const [selectedId, setSelectedId] = useState<string | null>(null);
const [detailOpen, setDetailOpen] = useState(false);
// 검색 상태
const [searchDateFrom, setSearchDateFrom] = useState("");
const [searchDateTo, setSearchDateTo] = useState("");
const [searchStatus, setSearchStatus] = useState<string>("all");
const [searchChangeType, setSearchChangeType] = useState<string>("all");
const [searchKeyword, setSearchKeyword] = useState("");
// 검색 필터 (DynamicSearchFilter)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// ECR 모달
const [isEcrModalOpen, setIsEcrModalOpen] = useState(false);
@@ -386,13 +382,6 @@ export default function DesignChangeManagementPage() {
const [rejectReason, setRejectReason] = useState("");
const [rejectTargetId, setRejectTargetId] = useState("");
useEffect(() => {
const today = new Date();
const threeMonthsAgo = new Date(today);
threeMonthsAgo.setMonth(today.getMonth() - 3);
setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]);
setSearchDateTo(today.toISOString().split("T")[0]);
}, []);
const fetchData = useCallback(async () => {
setLoading(true);
@@ -419,39 +408,66 @@ export default function DesignChangeManagementPage() {
fetchData();
}, [fetchData]);
// snake_case → camelCase 매핑 (ECR)
const ecrFieldMap: Record<string, string> = {
request_no: "id",
request_date: "date",
change_type: "changeType",
target_name: "target",
drawing_no: "drawingNo",
req_dept: "reqDept",
ecn_no: "ecnNo",
apply_timing: "applyTiming",
};
// snake_case → camelCase 매핑 (ECN)
const ecnFieldMap: Record<string, string> = {
ecn_no: "id",
ecn_date: "date",
apply_date: "applyDate",
drawing_before: "drawingBefore",
drawing_after: "drawingAfter",
ecr_id: "ecrNo",
notify_depts: "notifyDepts",
};
const getFieldValue = (obj: any, colName: string, map: Record<string, string>): string => {
const key = map[colName] || colName;
const val = obj[key];
if (Array.isArray(val)) return val.join(",");
return val !== undefined && val !== null ? String(val) : "";
};
const applyFilters = (items: any[], map: Record<string, string>) => {
if (searchFilters.length === 0) return items;
return items.filter((item) => {
for (const f of searchFilters) {
const val = getFieldValue(item, f.columnName, map);
if (f.operator === "contains") {
if (!val.toLowerCase().includes(f.value.toLowerCase())) return false;
} else if (f.operator === "equals") {
if (val !== f.value) return false;
} else if (f.operator === "in") {
const allowed = f.value.split("|");
if (!allowed.includes(val)) return false;
} else if (f.operator === "between") {
const [from, to] = f.value.split("|");
if (from && val < from) return false;
if (to && val > to) return false;
}
}
return true;
});
};
// --- Filtered Data ---
const filteredEcr = useMemo(() => {
return ecrData
.filter((item) => {
if (searchDateFrom && item.date < searchDateFrom) return false;
if (searchDateTo && item.date > searchDateTo) return false;
if (searchStatus !== "all" && item.status !== searchStatus) return false;
if (searchChangeType !== "all" && item.changeType !== searchChangeType) return false;
if (searchKeyword) {
const kw = searchKeyword.toLowerCase();
const str = [item.id, item.target, item.requester, item.drawingNo].join(" ").toLowerCase();
if (!str.includes(kw)) return false;
}
return true;
})
.sort((a, b) => b.date.localeCompare(a.date));
}, [ecrData, searchDateFrom, searchDateTo, searchStatus, searchChangeType, searchKeyword]);
return applyFilters(ecrData, ecrFieldMap)
.sort((a: EcrItem, b: EcrItem) => b.date.localeCompare(a.date));
}, [ecrData, searchFilters]);
const filteredEcn = useMemo(() => {
return ecnData
.filter((item) => {
if (searchDateFrom && item.date < searchDateFrom) return false;
if (searchDateTo && item.date > searchDateTo) return false;
if (searchStatus !== "all" && item.status !== searchStatus) return false;
if (searchKeyword) {
const kw = searchKeyword.toLowerCase();
const str = [item.id, item.target, item.designer, item.ecrNo].join(" ").toLowerCase();
if (!str.includes(kw)) return false;
}
return true;
})
.sort((a, b) => b.date.localeCompare(a.date));
}, [ecnData, searchDateFrom, searchDateTo, searchStatus, searchKeyword]);
return applyFilters(ecnData, ecnFieldMap)
.sort((a: EcnItem, b: EcnItem) => b.date.localeCompare(a.date));
}, [ecnData, searchFilters]);
// --- Status Counts ---
const ecrStatusCounts = useMemo(() => {
@@ -480,23 +496,10 @@ export default function DesignChangeManagementPage() {
const handleTabSwitch = (tab: TabType) => {
setCurrentTab(tab);
setSelectedId(null);
setSearchStatus("all");
};
// --- Search ---
const handleResetSearch = () => {
const today = new Date();
const threeMonthsAgo = new Date(today);
threeMonthsAgo.setMonth(today.getMonth() - 3);
setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]);
setSearchDateTo(today.toISOString().split("T")[0]);
setSearchStatus("all");
setSearchChangeType("all");
setSearchKeyword("");
};
const handleFilterByStatus = (status: string) => {
setSearchStatus(status);
const handleFilterByStatus = (_status: string) => {
// Status filter now handled by DynamicSearchFilter
};
// --- ECR/ECN Navigation ---
@@ -505,11 +508,9 @@ export default function DesignChangeManagementPage() {
if (targetId.startsWith("ECN")) {
setCurrentTab("ecn");
setSelectedId(targetId);
setSearchStatus("all");
} else if (targetId.startsWith("ECR")) {
setCurrentTab("ecr");
setSelectedId(targetId);
setSearchStatus("all");
}
};
@@ -781,7 +782,6 @@ export default function DesignChangeManagementPage() {
const currentStatCards = currentTab === "ecr" ? ecrStatCards : ecnStatCards;
const currentList = currentTab === "ecr" ? filteredEcr : filteredEcn;
const currentStatuses = currentTab === "ecr" ? ECR_STATUSES : ECN_STATUSES;
const handleRowClick = (id: string) => {
setSelectedId(id);
@@ -796,95 +796,36 @@ export default function DesignChangeManagementPage() {
</div>
)}
{/* 검색 필터 */}
<div className="shrink-0 border rounded-lg bg-card p-4">
<div className="flex flex-wrap items-end gap-3">
<div className="space-y-1.5">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"></Label>
<div className="flex items-center gap-2">
<Input
type="date"
className="w-[140px] h-9"
value={searchDateFrom}
onChange={(e) => setSearchDateFrom(e.target.value)}
/>
<span className="text-muted-foreground">~</span>
<Input
type="date"
className="w-[140px] h-9"
value={searchDateTo}
onChange={(e) => setSearchDateTo(e.target.value)}
/>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"></Label>
<Select value={currentTab} onValueChange={(v) => handleTabSwitch(v as TabType)}>
<SelectTrigger className="w-[150px] h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ecr">ECR ()</SelectItem>
<SelectItem value="ecn">ECN ()</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"></Label>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="w-[130px] h-9">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{currentStatuses.map((s) => (
<SelectItem key={s} value={s}>
{s}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{currentTab === "ecr" && (
<div className="space-y-1.5">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"></Label>
<Select value={searchChangeType} onValueChange={setSearchChangeType}>
<SelectTrigger className="w-[140px] h-9">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{CHANGE_TYPES.map((t) => (
<SelectItem key={t} value={t}>
{t}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="space-y-1.5">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"></Label>
<Input
placeholder="ECR/ECN번호 / 품목 / 요청자"
className="w-[260px] h-9"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
</div>
<div className="flex-1" />
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="h-9" onClick={handleResetSearch}>
<RotateCcw className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 탭 선택 + 검색 필터 */}
<div className="shrink-0 flex flex-col gap-2">
<div className="flex items-center gap-3 px-1">
<Select value={currentTab} onValueChange={(v) => handleTabSwitch(v as TabType)}>
<SelectTrigger className="w-[170px] h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ecr">ECR ()</SelectItem>
<SelectItem value="ecn">ECN ()</SelectItem>
</SelectContent>
</Select>
</div>
{currentTab === "ecr" ? (
<DynamicSearchFilter
tableName="dsn_design_request"
filterId="c16-change-management-ecr"
onFilterChange={setSearchFilters}
externalFilterConfig={tsEcr.filterConfig}
dataCount={filteredEcr.length}
/>
) : (
<DynamicSearchFilter
tableName="dsn_ecn"
filterId="c16-change-management-ecn"
onFilterChange={setSearchFilters}
externalFilterConfig={tsEcn.filterConfig}
dataCount={filteredEcn.length}
/>
)}
</div>
{/* 현황 카드 */}
@@ -926,9 +867,9 @@ export default function DesignChangeManagementPage() {
<div className="flex-1 overflow-auto">
{currentTab === "ecr" ? (
<Table>
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
<TableRow>
<TableHead className="w-[50px] text-center">No</TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[50px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
{tsEcr.visibleColumns.map((col) => (
<TableHead
key={col.key}
@@ -1027,9 +968,9 @@ export default function DesignChangeManagementPage() {
</Table>
) : (
<Table>
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
<TableRow>
<TableHead className="w-[50px] text-center">No</TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[50px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
{tsEcn.visibleColumns.map((col) => (
<TableHead
key={col.key}
@@ -1699,6 +1640,7 @@ export default function DesignChangeManagementPage() {
onOpenChange={tsEcr.setOpen}
tableName={tsEcr.tableName}
settingsId={tsEcr.settingsId}
defaultVisibleKeys={tsEcr.defaultVisibleKeys}
onSave={tsEcr.applySettings}
/>
<TableSettingsModal
@@ -1706,6 +1648,7 @@ export default function DesignChangeManagementPage() {
onOpenChange={tsEcn.setOpen}
tableName={tsEcn.tableName}
settingsId={tsEcn.settingsId}
defaultVisibleKeys={tsEcn.defaultVisibleKeys}
onSave={tsEcn.applySettings}
/>
</div>
@@ -2,8 +2,6 @@
import React, { useState, useMemo, useCallback, useEffect } from "react";
import {
Search,
RotateCcw,
Plus,
Pencil,
Trash2,
@@ -54,6 +52,7 @@ import {
} from "@/lib/api/design";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
// ========== 타입 ==========
interface HistoryItem {
@@ -187,10 +186,8 @@ export default function DesignRequestPage() {
const [saving, setSaving] = useState(false);
const [detailOpen, setDetailOpen] = useState(false);
const [filterStatus, setFilterStatus] = useState("");
const [filterType, setFilterType] = useState("");
const [filterPriority, setFilterPriority] = useState("");
const [filterKeyword, setFilterKeyword] = useState("");
// 검색 필터 (DynamicSearchFilter)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
const [modalOpen, setModalOpen] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
@@ -204,13 +201,6 @@ export default function DesignRequestPage() {
setLoading(true);
try {
const params: Record<string, string> = { source_type: "dr" };
if (filterStatus && filterStatus !== "__all__") params.status = filterStatus;
if (filterType && filterType !== "__all__") {
// design_type은 서버에서 직접 필터링하지 않으므로 클라이언트에서 처리
}
if (filterPriority && filterPriority !== "__all__") params.priority = filterPriority;
if (filterKeyword) params.search = filterKeyword;
const res = await getDesignRequestList(params);
if (res.success && res.data) {
setRequests(res.data);
@@ -222,20 +212,35 @@ export default function DesignRequestPage() {
} finally {
setLoading(false);
}
}, [filterStatus, filterPriority, filterKeyword]);
}, []);
useEffect(() => {
fetchRequests();
}, [fetchRequests]);
// 클라이언트 사이드 필터링 (design_type은 서버에서 지원하지 않으므로)
// 클라이언트 사이드 필터링 (DynamicSearchFilter)
const filteredRequests = useMemo(() => {
let list = requests;
if (filterType && filterType !== "__all__") {
list = list.filter((item) => item.design_type === filterType);
}
return list;
}, [requests, filterType]);
if (searchFilters.length === 0) return requests;
return requests.filter((item) => {
for (const f of searchFilters) {
const val = item[f.columnName as keyof DesignRequest];
const strVal = val !== undefined && val !== null ? (Array.isArray(val) ? val.join(",") : String(val)) : "";
if (f.operator === "contains") {
if (!strVal.toLowerCase().includes(f.value.toLowerCase())) return false;
} else if (f.operator === "equals") {
if (strVal !== f.value) return false;
} else if (f.operator === "in") {
const allowed = f.value.split("|");
if (!allowed.includes(strVal)) return false;
} else if (f.operator === "between") {
const [from, to] = f.value.split("|");
if (from && strVal < from) return false;
if (to && strVal > to) return false;
}
}
return true;
});
}, [requests, searchFilters]);
const selectedItem = useMemo(() => {
if (!selectedId) return null;
@@ -250,12 +255,6 @@ export default function DesignRequestPage() {
};
}, [requests]);
const handleResetFilter = useCallback(() => {
setFilterStatus("");
setFilterType("");
setFilterPriority("");
setFilterKeyword("");
}, []);
// 채번: 기존 데이터 기반으로 다음 번호 생성
const generateNextNo = useCallback(() => {
@@ -410,85 +409,30 @@ export default function DesignRequestPage() {
return (
<div className="flex h-full flex-col gap-3 p-3">
{/* 검색 필터 */}
<div className="shrink-0 border rounded-lg bg-card p-4">
<div className="flex flex-wrap items-end gap-3">
<div className="space-y-1.5">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"></Label>
<Select value={filterStatus} onValueChange={setFilterStatus}>
<SelectTrigger className="w-[130px] h-9"><SelectValue placeholder="상태 전체" /></SelectTrigger>
<SelectContent>
<SelectItem value="__all__"> </SelectItem>
{["신규접수", "접수대기", "검토중", "설계진행", "설계검토", "출도완료", "반려", "종료"].map((s) => (
<SelectItem key={s} value={s}>{s}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"></Label>
<Select value={filterType} onValueChange={setFilterType}>
<SelectTrigger className="w-[130px] h-9"><SelectValue placeholder="유형 전체" /></SelectTrigger>
<SelectContent>
<SelectItem value="__all__"> </SelectItem>
{["신규설계", "유사설계", "개조설계"].map((s) => (
<SelectItem key={s} value={s}>{s}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"></Label>
<Select value={filterPriority} onValueChange={setFilterPriority}>
<SelectTrigger className="w-[130px] h-9"><SelectValue placeholder="우선순위 전체" /></SelectTrigger>
<SelectContent>
<SelectItem value="__all__"> </SelectItem>
{["긴급", "높음", "보통", "낮음"].map((s) => (
<SelectItem key={s} value={s}>{s}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"></Label>
<Input
value={filterKeyword}
onChange={(e) => setFilterKeyword(e.target.value)}
placeholder="의뢰번호 / 설비명 / 고객명"
className="w-[240px] h-9"
/>
</div>
<div className="flex-1" />
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handleResetFilter}>
<RotateCcw className="w-4 h-4 mr-1.5" />
</Button>
</div>
</div>
<div className="shrink-0">
<DynamicSearchFilter
tableName="dsn_design_request"
filterId="c16-design-request"
onFilterChange={setSearchFilters}
externalFilterConfig={ts.filterConfig}
dataCount={filteredRequests.length}
/>
</div>
{/* 현황 카드 */}
<div className="grid grid-cols-3 gap-3 shrink-0">
<button
className="rounded-lg border bg-card px-3 py-2 text-left transition-colors hover:bg-accent/50"
onClick={() => setFilterStatus("접수대기")}
>
<div className="rounded-lg border bg-card px-3 py-2 text-left">
<div className="text-[10px] text-muted-foreground"></div>
<div className="text-xl font-bold text-info">{statusCounts.}</div>
</button>
<button
className="rounded-lg border bg-card px-3 py-2 text-left transition-colors hover:bg-accent/50"
onClick={() => setFilterStatus("설계진행")}
>
</div>
<div className="rounded-lg border bg-card px-3 py-2 text-left">
<div className="text-[10px] text-muted-foreground"></div>
<div className="text-xl font-bold text-warning">{statusCounts.}</div>
</button>
<button
className="rounded-lg border bg-card px-3 py-2 text-left transition-colors hover:bg-accent/50"
onClick={() => setFilterStatus("출도완료")}
>
</div>
<div className="rounded-lg border bg-card px-3 py-2 text-left">
<div className="text-[10px] text-muted-foreground"></div>
<div className="text-xl font-bold text-success">{statusCounts.}</div>
</button>
</div>
</div>
{/* 액션 바 */}
@@ -524,8 +468,8 @@ export default function DesignRequestPage() {
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
{ts.visibleColumns.map((col) => (
<TableHead
key={col.key}
@@ -871,6 +815,7 @@ export default function DesignRequestPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -1245,14 +1245,14 @@ export default function MyWorkPage() {
{viewMode === "list" && (
<Table>
<TableHeader>
<TableRow className="bg-muted">
<TableHead className="w-[90px] text-[11px] font-semibold uppercase tracking-wide"></TableHead>
<TableHead className="text-[11px] font-semibold uppercase tracking-wide"></TableHead>
<TableHead className="w-[65px] text-[11px] font-semibold uppercase tracking-wide"></TableHead>
<TableHead className="w-[55px] text-center text-[11px] font-semibold uppercase tracking-wide"></TableHead>
<TableHead className="w-[80px] text-[11px] font-semibold uppercase tracking-wide"></TableHead>
<TableHead className="w-[70px] text-[11px] font-semibold uppercase tracking-wide"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide"></TableHead>
<TableHead className="w-[65px] text-[11px] font-bold uppercase tracking-wide"></TableHead>
<TableHead className="w-[55px] text-center text-[11px] font-bold uppercase tracking-wide"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide"></TableHead>
<TableHead className="w-[70px] text-[11px] font-bold uppercase tracking-wide"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1327,9 +1327,9 @@ export default function MyWorkPage() {
</span>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[120px] text-[11px]">/</TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="min-w-[120px] text-[11px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
{timesheetData.wds.map((d, i) => (
<TableHead
key={i}
@@ -1344,7 +1344,7 @@ export default function MyWorkPage() {
<span className="text-[9px]">{d.getMonth() + 1}/{d.getDate()}</span>
</TableHead>
))}
<TableHead className="text-center text-[11px] font-bold"></TableHead>
<TableHead className="text-center text-[11px] font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -28,7 +28,6 @@ import {
} from "@/components/ui/resizable";
import {
Plus,
RotateCcw,
Save,
Pencil,
Trash2,
@@ -63,6 +62,7 @@ import {
} from "@/lib/api/design";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
// --- Types ---
type ProjectStatus = "진행중" | "계획" | "보류" | "완료";
@@ -290,10 +290,8 @@ export default function DesignProjectPage() {
const [selectedId, setSelectedId] = useState<string | null>(null);
const [expandedIds, setExpandedIds] = useState<Record<string, boolean>>({});
// 검색
const [searchStatus, setSearchStatus] = useState("all");
const [searchPM, setSearchPM] = useState("all");
const [searchKeyword, setSearchKeyword] = useState("");
// 검색 필터 (DynamicSearchFilter)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 상세 탭
const [detailTab, setDetailTab] = useState("wbs");
@@ -365,18 +363,40 @@ export default function DesignProjectPage() {
}
}, []);
// snake_case → camelCase 매핑 (DynamicSearchFilter의 columnName은 snake_case)
const fieldMap: Record<string, string> = {
project_no: "projectNo",
start_date: "startDate",
end_date: "endDate",
source_no: "sourceNo",
};
const getFieldValue = (obj: any, colName: string): string => {
const key = fieldMap[colName] || colName;
const val = obj[key];
return val !== undefined && val !== null ? String(val) : "";
};
// 필터링
const filteredProjects = useMemo(() => {
if (searchStatus === "all" && searchPM === "all" && !searchKeyword) return projects;
if (searchFilters.length === 0) return projects;
const matched = new Set<string>();
projects.forEach((p) => {
let pass = true;
if (searchStatus !== "all" && p.status !== searchStatus) pass = false;
if (searchPM !== "all" && p.pm !== searchPM) pass = false;
if (searchKeyword) {
const str = [p.projectNo, p.name, p.customer, p.pm, p.sourceNo].join(" ").toLowerCase();
if (!str.includes(searchKeyword.toLowerCase())) pass = false;
for (const f of searchFilters) {
const val = getFieldValue(p, f.columnName);
if (f.operator === "contains") {
if (!val.toLowerCase().includes(f.value.toLowerCase())) { pass = false; break; }
} else if (f.operator === "equals") {
if (val !== f.value) { pass = false; break; }
} else if (f.operator === "in") {
const allowed = f.value.split("|");
if (!allowed.includes(val)) { pass = false; break; }
} else if (f.operator === "between") {
const [from, to] = f.value.split("|");
if (from && val < from) { pass = false; break; }
if (to && val > to) { pass = false; break; }
}
}
if (pass) matched.add(p.id);
});
@@ -394,7 +414,7 @@ export default function DesignProjectPage() {
});
return projects.filter((p) => result.has(p.id));
}, [projects, searchStatus, searchPM, searchKeyword]);
}, [projects, searchFilters]);
const selectedProject = useMemo(
() => projects.find((p) => p.id === selectedId),
@@ -426,11 +446,6 @@ export default function DesignProjectPage() {
}));
};
const handleResetSearch = () => {
setSearchStatus("all");
setSearchPM("all");
setSearchKeyword("");
};
// --- 프로젝트 모달 ---
const openProjectModal = (editProject?: Project, presetParentId?: string) => {
@@ -683,46 +698,14 @@ export default function DesignProjectPage() {
return (
<div className="flex flex-col h-[calc(100vh-4rem)] gap-3 p-3">
{/* 검색 필터 */}
<div className="shrink-0 flex flex-wrap items-center gap-3 border rounded-lg bg-card px-4 py-3">
<div className="flex flex-col gap-0.5">
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></span>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="w-[110px] h-8 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="진행중"></SelectItem>
<SelectItem value="계획"></SelectItem>
<SelectItem value="보류"></SelectItem>
<SelectItem value="완료"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-0.5">
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">PM</span>
<Select value={searchPM} onValueChange={setSearchPM}>
<SelectTrigger className="w-[110px] h-8 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="이설계"></SelectItem>
<SelectItem value="박도면"></SelectItem>
<SelectItem value="최기구"></SelectItem>
<SelectItem value="김전장"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-0.5">
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"> / </span>
<Input
placeholder="프로젝트번호 / 프로젝트명 / 고객명"
className="w-[280px] h-8 text-xs"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
</div>
<div className="flex-1" />
<Button variant="outline" size="sm" className="h-8 text-xs" onClick={handleResetSearch}>
<RotateCcw className="w-3.5 h-3.5 mr-1.5" />
</Button>
<div className="shrink-0">
<DynamicSearchFilter
tableName="dsn_project"
filterId="c16-design-project"
onFilterChange={setSearchFilters}
externalFilterConfig={ts.filterConfig}
dataCount={filteredProjects.length}
/>
</div>
{/* 좌우 분할 메인 */}
@@ -746,8 +729,8 @@ export default function DesignProjectPage() {
<div className="flex-1 overflow-auto">
<Table>
<TableHeader className="sticky top-0 bg-muted z-10">
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
{ts.visibleColumns.map((col) => (
<TableHead
key={col.key}
@@ -980,8 +963,8 @@ export default function DesignProjectPage() {
</div>
) : (
<Table>
<TableHeader className="bg-muted/50">
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[180px] text-[11px] uppercase tracking-wide"></TableHead>
<TableHead className="w-[70px] text-[11px] uppercase tracking-wide"></TableHead>
<TableHead className="w-[85px] text-[11px] uppercase tracking-wide"></TableHead>
@@ -1465,6 +1448,7 @@ export default function DesignProjectPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -3,13 +3,6 @@
import React, { useState, useMemo, useCallback, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
@@ -47,8 +40,6 @@ import {
CommandList,
} from "@/components/ui/command";
import {
Search,
RotateCcw,
RefreshCw,
ClipboardList,
Pencil,
@@ -78,6 +69,7 @@ import { getUserList } from "@/lib/api/user";
import { useAuth } from "@/hooks/useAuth";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
// --- Types ---
type SourceType = "dr" | "ecr";
@@ -287,11 +279,8 @@ export default function DesignTaskManagementPage() {
fetchEmployees();
}, [fetchTasks, fetchEmployees]);
// 검색 필터
const [searchStatus, setSearchStatus] = useState<string>("all");
const [searchPriority, setSearchPriority] = useState<string>("all");
const [searchReqDept, setSearchReqDept] = useState<string>("all");
const [searchKeyword, setSearchKeyword] = useState("");
// 검색 필터 (DynamicSearchFilter)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 담당자 선택 모달 상태
const [designerModalOpen, setDesignerModalOpen] = useState(false);
@@ -351,23 +340,51 @@ export default function DesignTaskManagementPage() {
};
}, [myRelatedTasks]);
// snake_case → camelCase 매핑 (DynamicSearchFilter의 columnName은 snake_case)
const taskFieldMap: Record<string, string> = {
source_type: "sourceType",
request_no: "id",
target_name: "targetName",
req_dept: "reqDept",
request_date: "date",
due_date: "dueDate",
approval_step: "approvalStep",
design_type: "designType",
drawing_no: "drawingNo",
change_type: "changeType",
review_memo: "reviewMemo",
project_id: "projectNo",
};
const getTaskFieldValue = (obj: any, colName: string): string => {
const key = taskFieldMap[colName] || colName;
const val = obj[key];
if (Array.isArray(val)) return val.join(",");
return val !== undefined && val !== null ? String(val) : "";
};
// 필터링된 데이터
const filteredData = useMemo(() => {
return myRelatedTasks.filter((item) => {
if (currentTab === "dr" && item.sourceType !== "dr") return false;
if (currentTab === "ecr" && item.sourceType !== "ecr") return false;
if (searchStatus !== "all" && item.status !== searchStatus) return false;
if (searchPriority !== "all" && item.priority !== searchPriority) return false;
if (searchReqDept !== "all" && item.reqDept !== searchReqDept) return false;
if (searchKeyword) {
const str = [item.id, item.targetName, item.customer, item.requester, item.designer, item.reqDept]
.join(" ")
.toLowerCase();
if (!str.includes(searchKeyword.toLowerCase())) return false;
for (const f of searchFilters) {
const val = getTaskFieldValue(item, f.columnName);
if (f.operator === "contains") {
if (!val.toLowerCase().includes(f.value.toLowerCase())) return false;
} else if (f.operator === "equals") {
if (val !== f.value) return false;
} else if (f.operator === "in") {
const allowed = f.value.split("|");
if (!allowed.includes(val)) return false;
} else if (f.operator === "between") {
const [from, to] = f.value.split("|");
if (from && val < from) return false;
if (to && val > to) return false;
}
}
return true;
});
}, [myRelatedTasks, currentTab, searchStatus, searchPriority, searchReqDept, searchKeyword]);
}, [myRelatedTasks, currentTab, searchFilters]);
// 현황 통계
const stats = useMemo(() => {
@@ -607,15 +624,8 @@ export default function DesignTaskManagementPage() {
fetchTasks();
}, [selectedTaskId, reviewMemoText, fetchTasks]);
const handleResetSearch = useCallback(() => {
setSearchStatus("all");
setSearchPriority("all");
setSearchReqDept("all");
setSearchKeyword("");
}, []);
const handleFilterByStatus = useCallback((status: TaskStatus) => {
setSearchStatus(status);
const handleFilterByStatus = useCallback((_status: TaskStatus) => {
// Status filter now handled by DynamicSearchFilter
}, []);
// 납기 남은 일수 계산
@@ -706,62 +716,13 @@ export default function DesignTaskManagementPage() {
{/* 검색 필터 */}
<div className="border-b border-border bg-card px-4 py-3">
<div className="flex flex-wrap items-center gap-3">
<div className="flex flex-1 flex-wrap items-center gap-2">
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="h-8 w-[120px] text-xs">
<SelectValue placeholder="상태 전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
<SelectItem value="신규접수"></SelectItem>
<SelectItem value="검토중"></SelectItem>
<SelectItem value="승인완료"></SelectItem>
<SelectItem value="반려"></SelectItem>
<SelectItem value="프로젝트생성"></SelectItem>
</SelectContent>
</Select>
<Select value={searchPriority} onValueChange={setSearchPriority}>
<SelectTrigger className="h-8 w-[120px] text-xs">
<SelectValue placeholder="우선순위 전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
<SelectItem value="긴급"></SelectItem>
<SelectItem value="높음"></SelectItem>
<SelectItem value="보통"></SelectItem>
<SelectItem value="낮음"></SelectItem>
</SelectContent>
</Select>
<Select value={searchReqDept} onValueChange={setSearchReqDept}>
<SelectTrigger className="h-8 w-[120px] text-xs">
<SelectValue placeholder="의뢰부서 전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
<SelectItem value="영업팀"></SelectItem>
<SelectItem value="생산팀"></SelectItem>
<SelectItem value="품질팀"></SelectItem>
<SelectItem value="구매팀"></SelectItem>
<SelectItem value="기획팀"></SelectItem>
<SelectItem value="설계팀"></SelectItem>
</SelectContent>
</Select>
<div className="relative min-w-[260px]">
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
placeholder="접수번호 / 설비명 / 품목명 / 고객명 검색"
className="h-8 pl-8 text-xs"
/>
</div>
</div>
<Button size="sm" variant="outline" className="h-8 text-xs shrink-0" onClick={handleResetSearch}>
<RotateCcw className="mr-1 h-3.5 w-3.5" />
</Button>
</div>
<DynamicSearchFilter
tableName="dsn_design_request"
filterId="c16-task-management"
onFilterChange={setSearchFilters}
externalFilterConfig={ts.filterConfig}
dataCount={filteredData.length}
/>
</div>
{/* 좌우 분할 패널 */}
@@ -790,8 +751,8 @@ export default function DesignTaskManagementPage() {
</div>
<div className="flex-1 overflow-auto">
<Table>
<TableHeader className="sticky top-0 z-10 bg-muted">
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
{ts.visibleColumns.map((col) => (
<TableHead
key={col.key}
@@ -1377,6 +1338,7 @@ export default function DesignTaskManagementPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -20,7 +20,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import {
Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil,
Inbox, ClipboardCheck, Package, Copy, Info, Search, RotateCcw, Settings2,
Inbox, ClipboardCheck, Package, Copy, Info, Settings2,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
@@ -33,6 +33,7 @@ import { MultiTableExcelUploadModal } from "@/components/common/MultiTableExcelU
import { ImageUpload } from "@/components/common/ImageUpload";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
const EQUIP_TABLE = "equipment_mng";
const INSPECTION_TABLE = "equipment_inspection_item";
@@ -54,8 +55,7 @@ export default function EquipmentInfoPage() {
const [equipments, setEquipments] = useState<any[]>([]);
const [equipLoading, setEquipLoading] = useState(false);
const [equipCount, setEquipCount] = useState(0);
const [inputKeyword, setInputKeyword] = useState("");
const [searchKeyword, setSearchKeyword] = useState("");
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
const [selectedEquipId, setSelectedEquipId] = useState<string | null>(null);
// 우측 탭
@@ -142,10 +142,7 @@ export default function EquipmentInfoPage() {
const fetchEquipments = useCallback(async () => {
setEquipLoading(true);
try {
const filters: any[] = [];
if (searchKeyword) {
filters.push({ columnName: "equipment_name", operator: "contains", value: searchKeyword });
}
const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
const res = await apiClient.post(`/table-management/tables/${EQUIP_TABLE}/data`, {
page: 1, size: 500,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
@@ -159,7 +156,7 @@ export default function EquipmentInfoPage() {
})));
setEquipCount(res.data?.data?.total || raw.length);
} catch { toast.error("설비 목록 조회 실패"); } finally { setEquipLoading(false); }
}, [searchKeyword, catOptions]);
}, [searchFilters, catOptions]);
useEffect(() => { fetchEquipments(); }, [fetchEquipments]);
@@ -377,8 +374,6 @@ export default function EquipmentInfoPage() {
</Select>
);
const handleSearch = () => setSearchKeyword(inputKeyword);
return (
<div className="flex h-full flex-col gap-3 p-3">
{/* 브레드크럼 */}
@@ -389,38 +384,31 @@ export default function EquipmentInfoPage() {
</div>
{/* 검색 바 */}
<div className="flex items-center gap-2 px-4 py-3 bg-card border rounded-lg shrink-0">
<span className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap"></span>
<Input
className="h-9 w-[200px]"
placeholder="설비명 검색"
value={inputKeyword}
onChange={(e) => setInputKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
/>
<Button variant="outline" size="sm" className="h-9" onClick={() => { setInputKeyword(""); setSearchKeyword(""); }}>
<RotateCcw className="w-3.5 h-3.5 mr-1" />
</Button>
<Button size="sm" className="h-9" onClick={handleSearch}>
<Search className="w-3.5 h-3.5 mr-1" />
</Button>
<div className="ml-auto flex gap-1.5">
<Button variant="outline" size="sm" className="h-9" disabled={excelDetecting}
onClick={async () => {
setExcelDetecting(true);
try {
const r = await autoDetectMultiTableConfig(EQUIP_TABLE);
if (r.success && r.data) { setExcelChainConfig(r.data); setExcelUploadOpen(true); }
else toast.error("테이블 구조 분석 실패");
} catch { toast.error("오류"); } finally { setExcelDetecting(false); }
}}>
{excelDetecting ? <Loader2 className="w-3.5 h-3.5 mr-1 animate-spin" /> : <FileSpreadsheet className="w-3.5 h-3.5 mr-1" />}
</Button>
<Button variant="outline" size="sm" className="h-9" onClick={handleExcelDownload}>
<Download className="w-3.5 h-3.5 mr-1" />
</Button>
</div>
</div>
<DynamicSearchFilter
tableName={EQUIP_TABLE}
filterId="c16-equipment-info"
onFilterChange={setSearchFilters}
dataCount={equipCount}
externalFilterConfig={ts.filterConfig}
extraActions={
<div className="flex gap-1.5">
<Button variant="outline" size="sm" className="h-9" disabled={excelDetecting}
onClick={async () => {
setExcelDetecting(true);
try {
const r = await autoDetectMultiTableConfig(EQUIP_TABLE);
if (r.success && r.data) { setExcelChainConfig(r.data); setExcelUploadOpen(true); }
else toast.error("테이블 구조 분석 실패");
} catch { toast.error("오류"); } finally { setExcelDetecting(false); }
}}>
{excelDetecting ? <Loader2 className="w-3.5 h-3.5 mr-1 animate-spin" /> : <FileSpreadsheet className="w-3.5 h-3.5 mr-1" />}
</Button>
<Button variant="outline" size="sm" className="h-9" onClick={handleExcelDownload}>
<Download className="w-3.5 h-3.5 mr-1" />
</Button>
</div>
}
/>
<div className="flex-1 overflow-hidden border rounded-lg bg-card">
<ResizablePanelGroup direction="horizontal">
@@ -457,12 +445,12 @@ export default function EquipmentInfoPage() {
<Table noWrapper>
<thead className="sticky top-0 z-10 bg-card">
<TableRow>
{ts.isVisible("equipment_code") && <TableHead className="w-[110px]"></TableHead>}
{ts.isVisible("equipment_name") && <TableHead className="min-w-[130px]"></TableHead>}
{ts.isVisible("equipment_type") && <TableHead className="w-[90px]"></TableHead>}
{ts.isVisible("manufacturer") && <TableHead className="w-[100px]"></TableHead>}
{ts.isVisible("installation_location") && <TableHead className="w-[100px]"></TableHead>}
{ts.isVisible("operation_status") && <TableHead className="w-[80px]"></TableHead>}
{ts.isVisible("equipment_code") && <TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("equipment_name") && <TableHead className="min-w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("equipment_type") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("manufacturer") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("installation_location") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("operation_status") && <TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
</TableRow>
</thead>
<TableBody>
@@ -599,13 +587,13 @@ export default function EquipmentInfoPage() {
<Table noWrapper>
<thead className="sticky top-0 z-10 bg-card">
<TableRow>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[70px]"></TableHead>
<TableHead className="w-[70px]"></TableHead>
<TableHead className="w-[60px]"></TableHead>
<TableHead className="min-w-[150px]"></TableHead>
<TableHead className="min-w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</thead>
<TableBody>
@@ -639,11 +627,11 @@ export default function EquipmentInfoPage() {
<Table noWrapper>
<thead className="sticky top-0 z-10 bg-card">
<TableRow>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="w-[90px]"></TableHead>
<TableHead className="w-[60px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="min-w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</thead>
<TableBody>
@@ -828,9 +816,9 @@ export default function EquipmentInfoPage() {
<input type="checkbox" checked={copyItems.length > 0 && copyChecked.size === copyItems.length}
onChange={(e) => { if (e.target.checked) setCopyChecked(new Set(copyItems.map((i) => i.id))); else setCopyChecked(new Set()); }} />
</TableHead>
<TableHead></TableHead><TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[80px]"></TableHead><TableHead className="w-[70px]"></TableHead>
<TableHead className="w-[70px]"></TableHead><TableHead className="w-[60px]"></TableHead>
<TableHead></TableHead><TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead><TableHead className="w-[70px]"></TableHead>
<TableHead className="w-[70px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead><TableHead className="w-[60px]"></TableHead>
</TableRow>
</thead>
<TableBody>
@@ -878,6 +866,7 @@ export default function EquipmentInfoPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
@@ -286,8 +286,8 @@ export default function PlcSettingsPage() {
</div>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10">
<Checkbox
checked={datatypes.length > 0 && dtChecked.length === datatypes.length}
@@ -295,7 +295,7 @@ export default function PlcSettingsPage() {
/>
</TableHead>
{ts.visibleColumns.map((col) => (
<TableHead key={col.key}>{col.label}</TableHead>
<TableHead key={col.key} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">{col.label}</TableHead>
))}
</TableRow>
</TableHeader>
@@ -361,21 +361,21 @@ export default function PlcSettingsPage() {
</div>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10">
<Checkbox
checked={configs.length > 0 && cfgChecked.length === configs.length}
onCheckedChange={(v) => setCfgChecked(v ? configs.map(r => r.id) : [])}
/>
</TableHead>
<TableHead></TableHead>
<TableHead className="w-[110px]">ID</TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[90px]"></TableHead>
<TableHead className="w-[120px]">(Cron)</TableHead>
<TableHead className="w-[80px] text-center"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">ID</TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">(Cron)</TableHead>
<TableHead className="w-[80px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -539,6 +539,7 @@ export default function PlcSettingsPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -12,10 +12,8 @@ import {
Download,
Pencil,
RefreshCw,
Search,
Inbox,
Loader2,
RotateCcw,
Settings2,
} from "lucide-react";
import { cn } from "@/lib/utils";
@@ -54,6 +52,7 @@ import { toast } from "sonner";
import { exportToExcel } from "@/lib/utils/excelExport";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
// ========== 타입 & 상수 ==========
type TabKey = "carrier" | "cost" | "contract" | "route" | "vehicle";
@@ -247,9 +246,8 @@ export default function LogisticsInfoPage() {
// 탭 상태
const [activeTab, setActiveTab] = useState<TabKey>("carrier");
// 검색 키워드 (탭 전환 시 초기화)
const [searchInput, setSearchInput] = useState("");
const [keyword, setKeyword] = useState("");
// 검색 필터
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 탭별 독립 상태
const [tabData, setTabData] = useState<Record<TabKey, any[]>>({
@@ -293,15 +291,21 @@ export default function LogisticsInfoPage() {
// 컬럼 가시성 헬퍼
const getVisibleColumns = (tabKey: TabKey) => tsMap[tabKey].visibleColumns;
// 클라이언트 사이드 키워드 필터링
// 클라이언트 사이드 필터링
const filteredData = useMemo(() => {
const data = tabData[activeTab];
if (!keyword.trim()) return data;
const kw = keyword.toLowerCase();
if (searchFilters.length === 0) return data;
return data.filter((row) =>
Object.values(row).some((v) => String(v ?? "").toLowerCase().includes(kw))
searchFilters.every((f) => {
if (!f.value) return true;
const kw = f.value.toLowerCase();
if (f.columnName) {
return String(row[f.columnName] ?? "").toLowerCase().includes(kw);
}
return Object.values(row).some((v) => String(v ?? "").toLowerCase().includes(kw));
})
);
}, [tabData, activeTab, keyword]);
}, [tabData, activeTab, searchFilters]);
// FK 참조 데이터 로드
const loadReferences = useCallback(async () => {
@@ -393,22 +397,10 @@ export default function LogisticsInfoPage() {
fetchTabData(activeTab);
}, [activeTab, fetchTabData]);
// 검색 실행
const handleSearch = useCallback(() => {
setKeyword(searchInput);
}, [searchInput]);
// 검색 초기화
const handleReset = useCallback(() => {
setSearchInput("");
setKeyword("");
}, []);
// 탭 변경
const handleTabChange = useCallback((tab: string) => {
setActiveTab(tab as TabKey);
setSearchInput("");
setKeyword("");
setSearchFilters([]);
}, []);
// 등록 모달 열기
@@ -637,35 +629,13 @@ export default function LogisticsInfoPage() {
return (
<div className="flex h-[calc(100vh-4rem)] flex-col gap-3 p-3">
{/* 검색 필터 바 */}
<div className="flex shrink-0 flex-wrap items-end gap-3 rounded-lg border bg-card px-4 py-3">
<div className="flex flex-col gap-1">
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
</span>
<Input
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
placeholder={`${activeConfig.label} 검색...`}
className="h-9 w-[260px] text-sm"
/>
</div>
<Button size="sm" className="h-9" onClick={handleSearch}>
<Search className="mr-1.5 h-4 w-4" />
</Button>
<Button size="sm" variant="outline" className="h-9" onClick={handleReset}>
<RotateCcw className="mr-1.5 h-4 w-4" />
</Button>
<div className="ml-auto flex items-center gap-1.5">
<span className="font-mono text-xs text-muted-foreground">
{keyword.trim() && filteredData.length !== tabData[activeTab].length
? `${filteredData.length} / ${tabData[activeTab].length}`
: `${tabData[activeTab].length}`}
</span>
</div>
</div>
<DynamicSearchFilter
tableName={activeConfig.tableName}
filterId="c16-logistics-info"
onFilterChange={setSearchFilters}
externalFilterConfig={activeTs.filterConfig}
dataCount={filteredData.length}
/>
{/* 탭 + 콘텐츠 영역 */}
<Tabs
@@ -795,8 +765,8 @@ export default function LogisticsInfoPage() {
</div>
) : (
<Table>
<TableHeader>
<TableRow className="bg-muted/50 hover:bg-muted/50">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] p-2">
<Checkbox
checked={isAllChecked}
@@ -923,6 +893,7 @@ export default function LogisticsInfoPage() {
onOpenChange={activeTs.setOpen}
tableName={activeTs.tableName}
settingsId={activeTs.settingsId}
defaultVisibleKeys={activeTs.defaultVisibleKeys}
onSave={activeTs.applySettings}
/>
@@ -9,7 +9,7 @@
* ★ 재고 직접 등록/삭제 불가 — 입출고를 통해서만 변동, 조정만 가능
*/
import React, { useState, useEffect, useCallback, useMemo } from "react";
import React, { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -51,8 +51,6 @@ import {
History,
AlertTriangle,
RefreshCw,
Search,
RotateCcw,
Settings2,
} from "lucide-react";
import { cn } from "@/lib/utils";
@@ -60,6 +58,7 @@ import { apiClient } from "@/lib/api/client";
import { useAuth } from "@/hooks/useAuth";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { toast } from "sonner";
import { exportToExcel } from "@/lib/utils/excelExport";
@@ -118,9 +117,8 @@ export default function InventoryStatusPage() {
const [stockLoading, setStockLoading] = useState(false);
const [selectedStockId, setSelectedStockId] = useState<string | null>(null);
// 검색 상태
const [searchKeyword, setSearchKeyword] = useState("");
const [searchStatus, setSearchStatus] = useState("all");
// 검색 필터
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 우측: 이동 이력
const [historyItems, setHistoryItems] = useState<any[]>([]);
@@ -171,11 +169,13 @@ export default function InventoryStatusPage() {
const fetchStock = useCallback(async () => {
setStockLoading(true);
try {
const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
const res = await apiClient.post(
`/table-management/tables/${STOCK_TABLE}/data`,
{
page: 1,
size: 500,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
sort: { columnName: "item_number", order: "asc" },
}
@@ -197,28 +197,12 @@ export default function InventoryStatusPage() {
} finally {
setStockLoading(false);
}
}, [categoryOptions]);
}, [categoryOptions, searchFilters]);
useEffect(() => {
fetchStock();
}, [fetchStock]);
// 클라이언트 사이드 필터링
const filteredStock = useMemo(() => {
return stockItems.filter((item) => {
const kw = searchKeyword.trim().toLowerCase();
if (
kw &&
!item.item_number?.toLowerCase().includes(kw) &&
!item.item_name?.toLowerCase().includes(kw) &&
!item.warehouse_name?.toLowerCase().includes(kw)
)
return false;
if (searchStatus !== "all" && item.status !== searchStatus) return false;
return true;
});
}, [stockItems, searchKeyword, searchStatus]);
// 선택된 재고
const selectedStock = stockItems.find((s) => s.id === selectedStockId);
@@ -321,12 +305,12 @@ export default function InventoryStatusPage() {
// 엑셀 내보내기
const handleExcelExport = () => {
if (filteredStock.length === 0) {
if (stockItems.length === 0) {
toast.error("내보낼 데이터가 없어요");
return;
}
exportToExcel(
filteredStock.map((r) => ({
stockItems.map((r) => ({
품목코드: r.item_number,
품명: r.item_name,
창고: r.warehouse_name,
@@ -340,63 +324,32 @@ export default function InventoryStatusPage() {
);
};
const statusOptions = categoryOptions["status"] || [];
return (
<div className="flex flex-col h-full gap-3 p-3">
{/* 검색 바 */}
<div className="shrink-0 flex items-center gap-2 px-1">
<div className="relative flex-1 max-w-xs">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
className="pl-8 h-8 text-xs"
placeholder="품목코드, 품명, 창고 검색"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
</div>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="h-8 w-[130px] text-xs">
<SelectValue placeholder="상태 전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{statusOptions.map((o) => (
<SelectItem key={o.code} value={o.label}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="ghost"
size="sm"
className="h-8 px-2"
onClick={() => {
setSearchKeyword("");
setSearchStatus("all");
}}
>
<RotateCcw className="h-3.5 w-3.5" />
</Button>
<div className="ml-auto flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{filteredStock.length}
</span>
<Button
variant="outline"
size="sm"
className="h-8 gap-1 text-xs"
onClick={handleExcelExport}
>
<Download className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="sm" className="h-8 px-2" onClick={() => ts.setOpen(true)} title="테이블 설정">
<Settings2 className="h-3.5 w-3.5" />
</Button>
</div>
</div>
<DynamicSearchFilter
tableName={STOCK_TABLE}
filterId="c16-inventory"
onFilterChange={setSearchFilters}
externalFilterConfig={ts.filterConfig}
dataCount={stockItems.length}
extraActions={
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="h-8 gap-1 text-xs"
onClick={handleExcelExport}
>
<Download className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="sm" className="h-8 px-2" onClick={() => ts.setOpen(true)} title="테이블 설정">
<Settings2 className="h-3.5 w-3.5" />
</Button>
</div>
}
/>
{/* 마스터-디테일 패널 */}
<ResizablePanelGroup
@@ -410,7 +363,7 @@ export default function InventoryStatusPage() {
<div className="flex items-center gap-2">
<span className="text-[13px] font-bold"> </span>
<Badge variant="default" className="rounded-full text-[11px]">
{filteredStock.length}
{stockItems.length}
</Badge>
</div>
</div>
@@ -420,15 +373,15 @@ export default function InventoryStatusPage() {
<div className="flex items-center justify-center h-32">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : filteredStock.length === 0 ? (
) : stockItems.length === 0 ? (
<div className="flex items-center justify-center h-32 text-sm text-muted-foreground">
</div>
) : (
<Table>
<TableHeader>
<TableRow className="text-[11px]">
<TableHead className="w-8 text-center">#</TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-8 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">#</TableHead>
{ts.visibleColumns.map((col) => (
<TableHead
key={col.key}
@@ -440,7 +393,7 @@ export default function InventoryStatusPage() {
</TableRow>
</TableHeader>
<TableBody>
{filteredStock.map((item, idx) => (
{stockItems.map((item, idx) => (
<TableRow
key={item.id}
className={cn(
@@ -628,16 +581,16 @@ export default function InventoryStatusPage() {
</div>
) : (
<Table>
<TableHeader>
<TableRow className="text-[11px]">
<TableHead className="w-8 text-center">#</TableHead>
<TableHead className="w-[110px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[90px] text-right"></TableHead>
<TableHead className="w-[90px] text-right"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-8 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">#</TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -696,6 +649,7 @@ export default function InventoryStatusPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
@@ -600,6 +600,7 @@ export default function MaterialStatusPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -31,7 +31,6 @@ import {
Search,
Plus,
Trash2,
RotateCcw,
Loader2,
PackageOpen,
X,
@@ -46,6 +45,7 @@ import {
import { cn } from "@/lib/utils";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
// API: /outbound/*
import {
getOutboundList,
@@ -132,11 +132,7 @@ export default function OutboundPage() {
const [checkedIds, setCheckedIds] = useState<string[]>([]);
// 검색 필터
const [searchType, setSearchType] = useState("all");
const [searchStatus, setSearchStatus] = useState("all");
const [searchKeyword, setSearchKeyword] = useState("");
const [searchDateFrom, setSearchDateFrom] = useState("");
const [searchDateTo, setSearchDateTo] = useState("");
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 등록 모달
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -162,33 +158,31 @@ export default function OutboundPage() {
const [sourcePage, setSourcePage] = useState(1);
const [sourcePageSize, setSourcePageSize] = useState(20);
// 날짜 초기화
useEffect(() => {
const today = new Date();
const threeMonthsAgo = new Date(today);
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]);
setSearchDateTo(today.toISOString().split("T")[0]);
}, []);
// 목록 조회
const fetchList = useCallback(async () => {
setLoading(true);
try {
const res = await getOutboundList({
outbound_type: searchType !== "all" ? searchType : undefined,
outbound_status: searchStatus !== "all" ? searchStatus : undefined,
search_keyword: searchKeyword || undefined,
date_from: searchDateFrom || undefined,
date_to: searchDateTo || undefined,
});
const params: Record<string, string | undefined> = {};
for (const f of searchFilters) {
if (!f.value) continue;
if (f.columnName === "outbound_type") params.outbound_type = f.value;
else if (f.columnName === "outbound_status") params.outbound_status = f.value;
else if (f.columnName === "outbound_date" && f.operator === "between") {
const [from, to] = f.value.split("~").map((s) => s.trim());
if (from) params.date_from = from;
if (to) params.date_to = to;
} else {
params.search_keyword = f.value;
}
}
const res = await getOutboundList(params);
if (res.success) setData(res.data);
} catch {
// ignore
} finally {
setLoading(false);
}
}, [searchType, searchStatus, searchKeyword, searchDateFrom, searchDateTo]);
}, [searchFilters]);
useEffect(() => {
fetchList();
@@ -206,18 +200,6 @@ export default function OutboundPage() {
})();
}, []);
// 검색 초기화
const handleReset = () => {
setSearchType("all");
setSearchStatus("all");
setSearchKeyword("");
const today = new Date();
const threeMonthsAgo = new Date(today);
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]);
setSearchDateTo(today.toISOString().split("T")[0]);
};
// 체크박스
const allChecked = data.length > 0 && checkedIds.length === data.length;
const toggleCheckAll = () => {
@@ -513,82 +495,13 @@ export default function OutboundPage() {
return (
<div className="flex h-[calc(100vh-4rem)] flex-col gap-3 p-3">
{/* 검색 영역 */}
<div className="shrink-0 flex flex-wrap items-end gap-3 rounded-lg border bg-card p-3">
<div className="flex flex-col gap-1">
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></span>
<Select value={searchType} onValueChange={setSearchType}>
<SelectTrigger className="h-9 w-full text-xs">
<SelectValue placeholder="출고유형" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{OUTBOUND_TYPES.map((t) => (
<SelectItem key={t.value} value={t.value}>
{t.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></span>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="h-9 w-full text-xs">
<SelectValue placeholder="출고상태" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{OUTBOUND_STATUS_OPTIONS.map((s) => (
<SelectItem key={s.value} value={s.value}>
{s.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></span>
<Input
placeholder="출고번호 / 품목명 / 참조번호"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && fetchList()}
className="h-9 text-xs"
/>
</div>
<div className="flex flex-col gap-1">
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></span>
<div className="flex items-center gap-1.5">
<Input
type="date"
value={searchDateFrom}
onChange={(e) => setSearchDateFrom(e.target.value)}
className="h-9 flex-1 text-xs"
/>
<span className="text-muted-foreground/50 text-xs">~</span>
<Input
type="date"
value={searchDateTo}
onChange={(e) => setSearchDateTo(e.target.value)}
className="h-9 flex-1 text-xs"
/>
</div>
</div>
<div className="flex items-end justify-end gap-2">
<Button size="sm" variant="outline" onClick={handleReset} className="h-9">
<RotateCcw className="mr-1 h-4 w-4" />
</Button>
<Button size="sm" onClick={fetchList} className="h-9">
<Search className="mr-1 h-4 w-4" />
</Button>
</div>
</div>
<DynamicSearchFilter
tableName="outbound_mng"
filterId="c16-outbound"
onFilterChange={setSearchFilters}
externalFilterConfig={ts.filterConfig}
dataCount={data.length}
/>
{/* 출고 목록 테이블 */}
<div className="flex flex-1 flex-col overflow-hidden rounded-lg border bg-card">
@@ -624,8 +537,8 @@ export default function OutboundPage() {
<div className="flex-1 overflow-auto">
<Table>
<TableHeader>
<TableRow className="bg-muted">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center text-[11px] uppercase tracking-wide">
<Checkbox
checked={allChecked}
@@ -985,14 +898,14 @@ export default function OutboundPage() {
</div>
) : (
<Table>
<TableHeader>
<TableRow className="text-[11px]">
<TableHead className="w-[30px] p-2">No</TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="w-[80px] p-2 text-right"></TableHead>
<TableHead className="w-[80px] p-2 text-right"></TableHead>
<TableHead className="w-[90px] p-2 text-right"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[30px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[30px] p-2" />
</TableRow>
</TableHeader>
@@ -1098,6 +1011,7 @@ export default function OutboundPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -1126,15 +1040,15 @@ function SourceShipmentInstructionTable({
return (
<Table>
<TableHeader>
<TableRow className="text-[11px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] p-2" />
<TableHead className="p-2"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="w-[70px] p-2 text-right"></TableHead>
<TableHead className="w-[70px] p-2 text-right"></TableHead>
<TableHead className="w-[70px] p-2 text-right"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1208,14 +1122,14 @@ function SourcePurchaseOrderTable({
return (
<Table>
<TableHeader>
<TableRow className="text-[11px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] p-2" />
<TableHead className="p-2"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="w-[70px] p-2 text-right"></TableHead>
<TableHead className="w-[70px] p-2 text-right"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1282,14 +1196,14 @@ function SourceItemTable({
return (
<Table>
<TableHeader>
<TableRow className="text-[11px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] p-2" />
<TableHead className="p-2"></TableHead>
<TableHead className="w-[80px] p-2"></TableHead>
<TableHead className="w-[80px] p-2"></TableHead>
<TableHead className="w-[60px] p-2"></TableHead>
<TableHead className="w-[80px] p-2 text-right"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -15,7 +15,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { Command, CommandInput, CommandList, CommandEmpty, CommandItem } from "@/components/ui/command";
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
import {
Search, Plus, Trash2, RotateCcw, Loader2, Package, Box, X, Save, Edit2, Download, ChevronsUpDown, Check, Inbox, Settings2,
Plus, Trash2, Loader2, Package, Box, X, Save, Edit2, Download, ChevronsUpDown, Check, Inbox, Settings2,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
@@ -29,6 +29,7 @@ import {
} from "@/lib/api/packaging";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
const GRID_COLUMNS = [
{ key: "pkg_code", label: "품목코드" },
@@ -72,8 +73,8 @@ export default function PackagingPage() {
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
const [activeTab, setActiveTab] = useState<"packing" | "loading">("packing");
// 검색
const [searchKeyword, setSearchKeyword] = useState("");
// 검색 필터
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 포장재 데이터
const [pkgUnits, setPkgUnits] = useState<PkgUnit[]>([]);
@@ -157,15 +158,21 @@ export default function PackagingPage() {
// 검색 필터 적용
const filteredPkgUnits = pkgUnits.filter((p) => {
if (!searchKeyword) return true;
const kw = searchKeyword.toLowerCase();
return (p.pkg_code?.toLowerCase().includes(kw) || p.pkg_name?.toLowerCase().includes(kw));
if (searchFilters.length === 0) return true;
return searchFilters.every((f) => {
if (!f.value) return true;
const kw = f.value.toLowerCase();
return Object.values(p).some((v) => String(v ?? "").toLowerCase().includes(kw));
});
});
const filteredLoadingUnits = loadingUnits.filter((l) => {
if (!searchKeyword) return true;
const kw = searchKeyword.toLowerCase();
return (l.loading_code?.toLowerCase().includes(kw) || l.loading_name?.toLowerCase().includes(kw));
if (searchFilters.length === 0) return true;
return searchFilters.every((f) => {
if (!f.value) return true;
const kw = f.value.toLowerCase();
return Object.values(l).some((v) => String(v ?? "").toLowerCase().includes(kw));
});
});
// --- 포장재 등록/수정 모달 ---
@@ -354,25 +361,13 @@ export default function PackagingPage() {
<div className="flex h-[calc(100vh-4rem)] flex-col gap-3 p-4">
{/* 1. 필터 바 */}
<div className="flex shrink-0 items-center gap-4 rounded-lg border bg-card px-4 py-3">
<div className="flex items-center gap-2">
<span className="whitespace-nowrap text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></span>
<Input
placeholder="포장코드 / 포장명 / 적재함명"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
className="h-8 w-[260px] text-xs"
/>
{searchKeyword && (
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => setSearchKeyword("")}>
<X className="h-3.5 w-3.5" />
</Button>
)}
</div>
<Button variant="ghost" size="sm" className="ml-auto" onClick={() => setSearchKeyword("")}>
<RotateCcw className="mr-1.5 h-3.5 w-3.5" />
</Button>
</div>
<DynamicSearchFilter
tableName="pkg_unit"
filterId="c16-packaging"
onFilterChange={setSearchFilters}
externalFilterConfig={ts.filterConfig}
dataCount={activeTab === "packing" ? filteredPkgUnits.length : filteredLoadingUnits.length}
/>
{/* 2. 탭 바 */}
<div className="flex shrink-0 gap-1 border-b">
@@ -464,14 +459,14 @@ export default function PackagingPage() {
{/* 포장재 목록 테이블 */}
<div className={cn("overflow-auto", selectedPkg ? "flex-[0_0_50%] border-b" : "flex-1")}>
<Table>
<TableHeader className="sticky top-0 z-10 bg-muted/80 backdrop-blur-sm">
<TableRow className="text-[11px]">
{ts.isVisible("pkg_code") && <TableHead className="p-2"></TableHead>}
{ts.isVisible("pkg_name") && <TableHead className="p-2"></TableHead>}
{ts.isVisible("pkg_type") && <TableHead className="p-2 w-[80px]"></TableHead>}
{ts.isVisible("size") && <TableHead className="p-2 w-[100px]">(mm)</TableHead>}
{ts.isVisible("max_weight") && <TableHead className="p-2 w-[80px] text-right"></TableHead>}
{ts.isVisible("status") && <TableHead className="p-2 w-[60px] text-center"></TableHead>}
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
{ts.isVisible("pkg_code") && <TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("pkg_name") && <TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("pkg_type") && <TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("size") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">(mm)</TableHead>}
{ts.isVisible("max_weight") && <TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("status") && <TableHead className="w-[60px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
@@ -554,13 +549,13 @@ export default function PackagingPage() {
</div>
) : (
<Table>
<TableHeader className="sticky top-0 z-10 bg-muted/80 backdrop-blur-sm">
<TableRow className="text-[11px]">
<TableHead className="p-2"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2 w-[80px]"></TableHead>
<TableHead className="p-2 w-[50px]"></TableHead>
<TableHead className="p-2 w-[80px] text-right"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 w-[40px]" />
</TableRow>
</TableHeader>
@@ -592,14 +587,14 @@ export default function PackagingPage() {
{/* 적재함 목록 테이블 */}
<div className={cn("overflow-auto", selectedLoading ? "flex-[0_0_50%] border-b" : "flex-1")}>
<Table>
<TableHeader className="sticky top-0 z-10 bg-muted/80 backdrop-blur-sm">
<TableRow className="text-[11px]">
<TableHead className="p-2"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2 w-[90px]"></TableHead>
<TableHead className="p-2 w-[100px]">(mm)</TableHead>
<TableHead className="p-2 w-[80px] text-right"></TableHead>
<TableHead className="p-2 w-[60px] text-center"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">(mm)</TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[60px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -682,13 +677,13 @@ export default function PackagingPage() {
</div>
) : (
<Table>
<TableHeader className="sticky top-0 z-10 bg-muted/80 backdrop-blur-sm">
<TableRow className="text-[11px]">
<TableHead className="p-2"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2 w-[80px]"></TableHead>
<TableHead className="p-2 w-[90px] text-right"></TableHead>
<TableHead className="p-2 w-[80px]"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 w-[40px]" />
</TableRow>
</TableHeader>
@@ -906,14 +901,14 @@ export default function PackagingPage() {
/>
<div className="max-h-[300px] overflow-auto rounded border">
<Table>
<TableHeader>
<TableRow className="text-[11px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="p-2 w-[30px]" />
<TableHead className="p-2 w-[130px]"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2 w-[100px]"></TableHead>
<TableHead className="p-2 w-[80px]"></TableHead>
<TableHead className="p-2 w-[50px]"></TableHead>
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -979,14 +974,14 @@ export default function PackagingPage() {
/>
<div className="max-h-[300px] overflow-auto rounded border">
<Table>
<TableHeader>
<TableRow className="text-[11px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="p-2 w-[30px]" />
<TableHead className="p-2 w-[120px]"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2 w-[70px]"></TableHead>
<TableHead className="p-2 w-[100px]">(mm)</TableHead>
<TableHead className="p-2 w-[80px] text-right"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">(mm)</TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1053,6 +1048,7 @@ export default function PackagingPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -38,7 +38,6 @@ import {
Search,
Plus,
Trash2,
RotateCcw,
Loader2,
Inbox,
X,
@@ -53,6 +52,7 @@ import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
// API: /receiving/*
import {
getReceivingList,
@@ -153,11 +153,7 @@ export default function ReceivingPage() {
const [checkedIds, setCheckedIds] = useState<string[]>([]);
// 검색 필터
const [searchType, setSearchType] = useState("all");
const [searchStatus, setSearchStatus] = useState("all");
const [searchKeyword, setSearchKeyword] = useState("");
const [searchDateFrom, setSearchDateFrom] = useState("");
const [searchDateTo, setSearchDateTo] = useState("");
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 등록 모달
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -188,14 +184,8 @@ export default function ReceivingPage() {
// 구매관리 division 코드 (라벨 기준 조회)
const [purchaseDivisionCode, setPurchaseDivisionCode] = useState<string>("");
// 날짜 초기화 + 구매관리 division 코드 로드
// 구매관리 division 코드 로드
useEffect(() => {
const today = new Date();
const threeMonthsAgo = new Date(today);
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]);
setSearchDateTo(today.toISOString().split("T")[0]);
// division 카테고리에서 "구매관리" 라벨의 코드 조회
apiClient.get("/table-categories/item_info/division/values").then((res) => {
const vals = res.data?.data || [];
@@ -208,20 +198,27 @@ export default function ReceivingPage() {
const fetchList = useCallback(async () => {
setLoading(true);
try {
const res = await getReceivingList({
inbound_type: searchType !== "all" ? searchType : undefined,
inbound_status: searchStatus !== "all" ? searchStatus : undefined,
search_keyword: searchKeyword || undefined,
date_from: searchDateFrom || undefined,
date_to: searchDateTo || undefined,
});
const params: Record<string, string | undefined> = {};
for (const f of searchFilters) {
if (!f.value) continue;
if (f.columnName === "inbound_type") params.inbound_type = f.value;
else if (f.columnName === "inbound_status") params.inbound_status = f.value;
else if (f.columnName === "inbound_date" && f.operator === "between") {
const [from, to] = f.value.split("~").map((s) => s.trim());
if (from) params.date_from = from;
if (to) params.date_to = to;
} else {
params.search_keyword = f.value;
}
}
const res = await getReceivingList(params);
if (res.success) setData(res.data);
} catch {
// 에러 무시
} finally {
setLoading(false);
}
}, [searchType, searchStatus, searchKeyword, searchDateFrom, searchDateTo]);
}, [searchFilters]);
useEffect(() => {
fetchList();
@@ -239,18 +236,6 @@ export default function ReceivingPage() {
})();
}, []);
// 검색 초기화
const handleReset = () => {
setSearchType("all");
setSearchStatus("all");
setSearchKeyword("");
const today = new Date();
const threeMonthsAgo = new Date(today);
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]);
setSearchDateTo(today.toISOString().split("T")[0]);
};
// 체크박스
const allChecked = data.length > 0 && checkedIds.length === data.length;
const toggleCheckAll = () => {
@@ -548,86 +533,32 @@ export default function ReceivingPage() {
</div>
{/* 검색 영역 */}
<div className="flex flex-wrap items-center gap-3 rounded-lg border bg-card p-3 shrink-0">
<Select value={searchType} onValueChange={setSearchType}>
<SelectTrigger className="h-9 w-[130px] text-xs">
<SelectValue placeholder="입고유형" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{INBOUND_TYPES.map((t) => (
<SelectItem key={t.value} value={t.value}>
{t.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="h-9 w-[130px] text-xs">
<SelectValue placeholder="입고상태" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{INBOUND_STATUS_OPTIONS.map((s) => (
<SelectItem key={s.value} value={s.value}>
{s.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Input
placeholder="입고번호 / 품목명 / 참조번호 검색"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && fetchList()}
className="h-9 w-[240px] text-xs"
/>
<div className="flex items-center gap-1">
<Input
type="date"
value={searchDateFrom}
onChange={(e) => setSearchDateFrom(e.target.value)}
className="h-9 w-[140px] text-xs"
/>
<span className="text-muted-foreground text-xs">~</span>
<Input
type="date"
value={searchDateTo}
onChange={(e) => setSearchDateTo(e.target.value)}
className="h-9 w-[140px] text-xs"
/>
</div>
<Button size="sm" onClick={fetchList} className="h-9">
<Search className="mr-1 h-4 w-4" />
</Button>
<Button size="sm" variant="outline" onClick={handleReset} className="h-9">
<RotateCcw className="mr-1 h-4 w-4" />
</Button>
<div className="ml-auto flex items-center gap-2">
<Button size="sm" onClick={openRegisterModal} className="h-9">
<Plus className="mr-1 h-4 w-4" />
</Button>
<div className="h-4 w-px bg-border" />
<Button
size="sm"
variant="outline"
onClick={handleDelete}
disabled={checkedIds.length === 0}
className="h-9 text-destructive border-destructive/20 bg-destructive/5 hover:bg-destructive/10"
>
<Trash2 className="mr-1 h-4 w-4" />
({checkedIds.length})
</Button>
</div>
</div>
<DynamicSearchFilter
tableName="inbound_mng"
filterId="c16-receiving"
onFilterChange={setSearchFilters}
externalFilterConfig={ts.filterConfig}
dataCount={data.length}
extraActions={
<div className="flex items-center gap-2">
<Button size="sm" onClick={openRegisterModal} className="h-9">
<Plus className="mr-1 h-4 w-4" />
</Button>
<div className="h-4 w-px bg-border" />
<Button
size="sm"
variant="outline"
onClick={handleDelete}
disabled={checkedIds.length === 0}
className="h-9 text-destructive border-destructive/20 bg-destructive/5 hover:bg-destructive/10"
>
<Trash2 className="mr-1 h-4 w-4" />
({checkedIds.length})
</Button>
</div>
}
/>
{/* 입고 목록 테이블 */}
<div className="flex-1 overflow-hidden rounded-lg border bg-card">
@@ -645,29 +576,29 @@ export default function ReceivingPage() {
<div className="flex-1 overflow-auto">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center">
<Checkbox
checked={allChecked}
onCheckedChange={toggleCheckAll}
/>
</TableHead>
{ts.isVisible("inbound_number") && <TableHead className="w-[130px]"></TableHead>}
{ts.isVisible("inbound_type") && <TableHead className="w-[90px]"></TableHead>}
{ts.isVisible("inbound_date") && <TableHead className="w-[100px]"></TableHead>}
{ts.isVisible("reference_number") && <TableHead className="w-[120px]"></TableHead>}
{ts.isVisible("source_type") && <TableHead className="w-[80px]"></TableHead>}
{ts.isVisible("supplier_name") && <TableHead className="w-[120px]"></TableHead>}
{ts.isVisible("item_number") && <TableHead className="w-[100px]"></TableHead>}
{ts.isVisible("item_name") && <TableHead className="min-w-[150px]"></TableHead>}
{ts.isVisible("spec") && <TableHead className="w-[80px]"></TableHead>}
{ts.isVisible("inbound_qty") && <TableHead className="w-[80px] text-right"></TableHead>}
{ts.isVisible("unit_price") && <TableHead className="w-[90px] text-right"></TableHead>}
{ts.isVisible("total_amount") && <TableHead className="w-[100px] text-right"></TableHead>}
{ts.isVisible("warehouse_name") && <TableHead className="w-[100px]"></TableHead>}
{ts.isVisible("inbound_status") && <TableHead className="w-[90px] text-center"></TableHead>}
{ts.isVisible("remark") && <TableHead className="w-[100px]"></TableHead>}
{ts.isVisible("inbound_number") && <TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("inbound_type") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("inbound_date") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("reference_number") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("source_type") && <TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("supplier_name") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("item_number") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("item_name") && <TableHead className="min-w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("spec") && <TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("inbound_qty") && <TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("unit_price") && <TableHead className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("total_amount") && <TableHead className="w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("warehouse_name") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("inbound_status") && <TableHead className="w-[90px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("remark") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
@@ -1022,11 +953,11 @@ export default function ReceivingPage() {
</div>
) : (
<Table>
<TableHeader>
<TableRow className="text-[11px]">
<TableHead className="w-[30px] p-2">No</TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[30px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] p-2 text-right">
</TableHead>
@@ -1156,6 +1087,7 @@ export default function ReceivingPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -1184,15 +1116,15 @@ function SourcePurchaseOrderTable({
return (
<Table>
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow className="text-[11px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] p-2" />
<TableHead className="p-2"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="w-[70px] p-2 text-right"></TableHead>
<TableHead className="w-[70px] p-2 text-right"></TableHead>
<TableHead className="w-[70px] p-2 text-right"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1264,14 +1196,14 @@ function SourceShipmentTable({
return (
<Table>
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow className="text-[11px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] p-2" />
<TableHead className="p-2"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="w-[70px] p-2 text-right"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1342,14 +1274,14 @@ function SourceItemTable({
return (
<Table>
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow className="text-[11px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] p-2" />
<TableHead className="p-2"></TableHead>
<TableHead className="w-[80px] p-2"></TableHead>
<TableHead className="w-[80px] p-2"></TableHead>
<TableHead className="w-[60px] p-2"></TableHead>
<TableHead className="w-[80px] p-2 text-right"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -9,7 +9,7 @@
*
*/
import React, { useState, useEffect, useCallback, useMemo } from "react";
import React, { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -50,8 +50,6 @@ import {
Download,
MapPin,
Building2,
Search,
RotateCcw,
Settings2,
} from "lucide-react";
import { cn } from "@/lib/utils";
@@ -61,6 +59,7 @@ import { toast } from "sonner";
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { exportToExcel } from "@/lib/utils/excelExport";
const WAREHOUSE_TABLE = "warehouse_info";
@@ -116,10 +115,8 @@ export default function WarehouseManagementPage() {
const [warehouseCount, setWarehouseCount] = useState(0);
const [selectedWarehouseId, setSelectedWarehouseId] = useState<string | null>(null);
// 검색 상태
const [searchKeyword, setSearchKeyword] = useState("");
const [searchWarehouseType, setSearchWarehouseType] = useState("all");
const [searchStatus, setSearchStatus] = useState("all");
// 검색 필터
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 우측: 로케이션 목록
const [locations, setLocations] = useState<any[]>([]);
@@ -204,11 +201,13 @@ export default function WarehouseManagementPage() {
const fetchWarehouses = useCallback(async () => {
setWarehouseLoading(true);
try {
const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
const res = await apiClient.post(
`/table-management/tables/${WAREHOUSE_TABLE}/data`,
{
page: 1,
size: 500,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
sort: { columnName: "warehouse_code", order: "asc" },
}
@@ -226,28 +225,12 @@ export default function WarehouseManagementPage() {
} finally {
setWarehouseLoading(false);
}
}, [categoryOptions, resolveCategory]);
}, [categoryOptions, resolveCategory, searchFilters]);
useEffect(() => {
fetchWarehouses();
}, [fetchWarehouses]);
// 클라이언트 사이드 필터링
const filteredWarehouses = useMemo(() => {
return warehouses.filter((w) => {
const kw = searchKeyword.trim().toLowerCase();
if (
kw &&
!w.warehouse_code?.toLowerCase().includes(kw) &&
!w.warehouse_name?.toLowerCase().includes(kw)
)
return false;
if (searchWarehouseType !== "all" && w.warehouse_type !== searchWarehouseType) return false;
if (searchStatus !== "all" && w.status !== searchStatus) return false;
return true;
});
}, [warehouses, searchKeyword, searchWarehouseType, searchStatus]);
// 선택된 창고
const selectedWarehouse = warehouses.find((w) => w.id === selectedWarehouseId);
@@ -496,80 +479,34 @@ export default function WarehouseManagementPage() {
);
};
const warehouseTypeOptions = categoryOptions["warehouse_type"] || [];
const statusOptions = categoryOptions["status"] || [];
return (
<div className="flex flex-col h-full gap-3 p-3">
{ConfirmDialogComponent}
{/* 검색 바 */}
<div className="shrink-0 flex items-center gap-2 px-1">
<div className="relative flex-1 max-w-xs">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
className="pl-8 h-8 text-xs"
placeholder="창고코드, 창고명 검색"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
</div>
<Select value={searchWarehouseType} onValueChange={setSearchWarehouseType}>
<SelectTrigger className="h-8 w-[130px] text-xs">
<SelectValue placeholder="유형 전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{warehouseTypeOptions.map((o) => (
<SelectItem key={o.code} value={o.label}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="h-8 w-[130px] text-xs">
<SelectValue placeholder="상태 전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{statusOptions.map((o) => (
<SelectItem key={o.code} value={o.label}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="ghost"
size="sm"
className="h-8 px-2"
onClick={() => {
setSearchKeyword("");
setSearchWarehouseType("all");
setSearchStatus("all");
}}
>
<RotateCcw className="h-3.5 w-3.5" />
</Button>
<div className="ml-auto flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{filteredWarehouses.length}
</span>
<Button
variant="outline"
size="sm"
className="h-8 gap-1 text-xs"
onClick={handleExcelExport}
>
<Download className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="sm" className="h-8 px-2" onClick={() => ts.setOpen(true)} title="테이블 설정">
<Settings2 className="h-3.5 w-3.5" />
</Button>
</div>
</div>
<DynamicSearchFilter
tableName={WAREHOUSE_TABLE}
filterId="c16-warehouse"
onFilterChange={setSearchFilters}
externalFilterConfig={ts.filterConfig}
dataCount={warehouses.length}
extraActions={
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="h-8 gap-1 text-xs"
onClick={handleExcelExport}
>
<Download className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="sm" className="h-8 px-2" onClick={() => ts.setOpen(true)} title="테이블 설정">
<Settings2 className="h-3.5 w-3.5" />
</Button>
</div>
}
/>
{/* 마스터-디테일 패널 */}
<ResizablePanelGroup
@@ -583,7 +520,7 @@ export default function WarehouseManagementPage() {
<div className="flex items-center gap-2">
<span className="text-[13px] font-bold"> </span>
<Badge variant="default" className="rounded-full text-[11px]">
{filteredWarehouses.length}
{warehouses.length}
</Badge>
</div>
<div className="flex items-center gap-1.5">
@@ -613,22 +550,22 @@ export default function WarehouseManagementPage() {
<div className="flex items-center justify-center h-32">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : filteredWarehouses.length === 0 ? (
) : warehouses.length === 0 ? (
<div className="flex items-center justify-center h-32 text-sm text-muted-foreground">
</div>
) : (
<Table>
<TableHeader>
<TableRow className="text-[11px]">
<TableHead className="w-8 text-center">#</TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-8 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">#</TableHead>
{ts.visibleColumns.map((col) => (
<TableHead key={col.key}>{col.label}</TableHead>
<TableHead key={col.key} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">{col.label}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{filteredWarehouses.map((w, idx) => (
{warehouses.map((w, idx) => (
<TableRow
key={w.id}
className={cn(
@@ -743,8 +680,8 @@ export default function WarehouseManagementPage() {
</div>
) : (
<Table>
<TableHeader>
<TableRow className="text-[11px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 text-center">
<Checkbox
checked={
@@ -758,15 +695,15 @@ export default function WarehouseManagementPage() {
aria-label="전체 선택"
/>
</TableHead>
<TableHead className="w-8 text-center">#</TableHead>
<TableHead className="w-[110px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[50px] text-center"></TableHead>
<TableHead className="w-[70px]"></TableHead>
<TableHead className="w-[50px] text-center"></TableHead>
<TableHead className="w-[50px] text-center"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[70px]"></TableHead>
<TableHead className="w-8 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">#</TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[50px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[50px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[50px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1116,6 +1053,7 @@ export default function WarehouseManagementPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -697,14 +697,14 @@ export default function CompanyPage() {
</div>
) : (
<Table>
<TableHeader className="sticky top-0 bg-muted z-10">
<TableRow>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[90px]"></TableHead>
<TableHead className="w-[100px]">ID</TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">ID</TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -24,7 +24,7 @@ import {
import { Checkbox } from "@/components/ui/checkbox";
import {
Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil,
Users, Search, Settings2,
Users, Settings2,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
@@ -38,11 +38,10 @@ import { exportToExcel } from "@/lib/utils/excelExport";
import { formatField, validateField, validateForm } from "@/lib/utils/validation";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
const DEPT_TABLE = "dept_info";
const USER_TABLE = "user_info";
const ALL_VALUE = "__all__";
const DEPT_COLUMNS = [
{ key: "parent_dept_code", label: "상위부서" },
{ key: "status", label: "상태" },
@@ -52,10 +51,8 @@ export default function DepartmentPage() {
const { user } = useAuth();
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
// 필터
const [filterDeptCode, setFilterDeptCode] = useState("");
const [filterDeptName, setFilterDeptName] = useState("");
const [filterStatus, setFilterStatus] = useState<string>(ALL_VALUE);
// 검색 필터 (DynamicSearchFilter)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 좌측: 부서
const [depts, setDepts] = useState<any[]>([]);
@@ -96,10 +93,7 @@ export default function DepartmentPage() {
const fetchDepts = useCallback(async () => {
setDeptLoading(true);
try {
const filters: { columnName: string; operator: string; value: string }[] = [];
if (filterDeptCode.trim()) filters.push({ columnName: "dept_code", operator: "contains", value: filterDeptCode.trim() });
if (filterDeptName.trim()) filters.push({ columnName: "dept_name", operator: "contains", value: filterDeptName.trim() });
if (filterStatus !== ALL_VALUE) filters.push({ columnName: "status", operator: "equals", value: filterStatus });
const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
const res = await apiClient.post(`/table-management/tables/${DEPT_TABLE}/data`, {
page: 1, size: 500,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
@@ -115,7 +109,7 @@ export default function DepartmentPage() {
} finally {
setDeptLoading(false);
}
}, [filterDeptCode, filterDeptName, filterStatus]);
}, [searchFilters]);
useEffect(() => { fetchDepts(); }, [fetchDepts]);
@@ -312,12 +306,6 @@ export default function DepartmentPage() {
toast.success("다운로드 완료");
};
const handleResetFilters = () => {
setFilterDeptCode("");
setFilterDeptName("");
setFilterStatus(ALL_VALUE);
};
// 퇴사일 기반 재직/퇴사 분리
const today = new Date().toISOString().split("T")[0];
const activeMembers = members.filter((m) => !m.end_date || m.end_date.substring(0, 10) >= today);
@@ -328,53 +316,24 @@ export default function DepartmentPage() {
return (
<div className="flex h-full flex-col gap-3 p-4">
{/* 검색 필터 바 */}
<div className="flex items-center gap-2 px-4 py-3 rounded-lg border bg-muted flex-wrap shrink-0">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-muted-foreground whitespace-nowrap"></span>
<Input
value={filterDeptCode}
onChange={(e) => setFilterDeptCode(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") void fetchDepts(); }}
placeholder="코드"
className="h-8 w-32 text-sm"
/>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-muted-foreground whitespace-nowrap"></span>
<Input
value={filterDeptName}
onChange={(e) => setFilterDeptName(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") void fetchDepts(); }}
placeholder="부서명"
className="h-8 w-36 text-sm"
/>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-muted-foreground"></span>
<Select value={filterStatus} onValueChange={setFilterStatus}>
<SelectTrigger className="h-8 w-24 text-xs">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL_VALUE}></SelectItem>
<SelectItem value="active"></SelectItem>
<SelectItem value="inactive"></SelectItem>
</SelectContent>
</Select>
</div>
<Button size="sm" className="h-8" onClick={() => void fetchDepts()} disabled={deptLoading}>
<Search className="w-3.5 h-3.5 mr-1.5" />
</Button>
<div className="flex gap-1.5 ml-auto">
<Button variant="outline" size="sm" className="h-8" onClick={() => setExcelUploadOpen(true)}>
<FileSpreadsheet className="w-3.5 h-3.5 mr-1" />
</Button>
<Button variant="outline" size="sm" className="h-8" onClick={() => void handleExcelDownload()}>
<Download className="w-3.5 h-3.5 mr-1" />
</Button>
</div>
</div>
<DynamicSearchFilter
tableName={DEPT_TABLE}
filterId="c16-department"
onFilterChange={setSearchFilters}
dataCount={deptCount}
externalFilterConfig={ts.filterConfig}
extraActions={
<div className="flex gap-1.5">
<Button variant="outline" size="sm" className="h-8" onClick={() => setExcelUploadOpen(true)}>
<FileSpreadsheet className="w-3.5 h-3.5 mr-1" />
</Button>
<Button variant="outline" size="sm" className="h-8" onClick={() => void handleExcelDownload()}>
<Download className="w-3.5 h-3.5 mr-1" />
</Button>
</div>
}
/>
{/* 마스터-디테일 분할 패널 */}
<div className="flex-1 overflow-hidden border rounded-lg bg-background shadow-sm">
@@ -409,13 +368,13 @@ export default function DepartmentPage() {
{/* 부서 테이블 */}
<div className="flex-1 overflow-auto">
<Table noWrapper>
<TableHeader className="sticky top-0 bg-muted z-10">
<TableRow>
<TableHead className="w-[40px] text-center text-xs px-2">No</TableHead>
<TableHead className="w-[120px] text-xs"></TableHead>
<TableHead className="min-w-[140px] text-xs"></TableHead>
{isColVisible("parent_dept_code") && <TableHead className="w-[110px] text-xs"></TableHead>}
{isColVisible("status") && <TableHead className="w-[70px] text-xs"></TableHead>}
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center px-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
{isColVisible("parent_dept_code") && <TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColVisible("status") && <TableHead className="w-[70px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
@@ -531,15 +490,15 @@ export default function DepartmentPage() {
<div className="text-center py-8 text-muted-foreground text-sm"> </div>
) : (
<Table noWrapper>
<TableHeader className="sticky top-0 bg-muted z-10">
<TableRow>
<TableHead className="w-[40px] text-center text-xs px-2">No</TableHead>
<TableHead className="w-[80px] text-xs"></TableHead>
<TableHead className="w-[90px] text-xs"></TableHead>
<TableHead className="w-[110px] text-xs">ID</TableHead>
<TableHead className="w-[80px] text-xs"></TableHead>
<TableHead className="w-[120px] text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center px-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">ID</TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -566,16 +525,16 @@ export default function DepartmentPage() {
<div className="text-center py-8 text-muted-foreground text-sm"> </div>
) : (
<Table noWrapper>
<TableHeader className="sticky top-0 bg-muted z-10">
<TableRow>
<TableHead className="w-[40px] text-center text-xs px-2">No</TableHead>
<TableHead className="w-[80px] text-xs"></TableHead>
<TableHead className="w-[90px] text-xs"></TableHead>
<TableHead className="w-[110px] text-xs">ID</TableHead>
<TableHead className="w-[80px] text-xs"></TableHead>
<TableHead className="w-[120px] text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="w-[110px] text-xs"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center px-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">ID</TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -799,6 +758,7 @@ export default function DepartmentPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
@@ -1,6 +1,6 @@
"use client";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import React, { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -29,10 +29,11 @@ import {
SelectValue,
} from "@/components/ui/select";
import { ExcelUploadModal } from "@/components/common/ExcelUploadModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { exportToExcel } from "@/lib/utils/excelExport";
import {
Plus, Trash2, Save, Loader2, FileSpreadsheet, Download,
Pencil, Copy, Search, RotateCcw, Settings2,
Pencil, Copy, Settings2,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
@@ -93,10 +94,8 @@ export default function ItemInfoPage() {
const [items, setItems] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// 로컬 검색 필터
const [searchKeyword, setSearchKeyword] = useState("");
const [searchDivision, setSearchDivision] = useState("all");
const [searchStatus, setSearchStatus] = useState("all");
// 검색 필터 (DynamicSearchFilter)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 모달
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -150,9 +149,11 @@ export default function ItemInfoPage() {
const fetchItems = useCallback(async () => {
setLoading(true);
try {
const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, {
page: 1,
size: 500,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
@@ -175,23 +176,12 @@ export default function ItemInfoPage() {
} finally {
setLoading(false);
}
}, [categoryOptions]);
}, [categoryOptions, searchFilters]);
useEffect(() => {
fetchItems();
}, [fetchItems]);
// 클라이언트 필터링
const filteredData = useMemo(() => {
return items.filter((item) => {
const kw = searchKeyword.trim().toLowerCase();
if (kw && !item.item_number?.toLowerCase().includes(kw) && !item.item_name?.toLowerCase().includes(kw)) return false;
if (searchDivision !== "all" && item.division !== searchDivision) return false;
if (searchStatus !== "all" && item.status !== searchStatus) return false;
return true;
});
}, [items, searchKeyword, searchDivision, searchStatus]);
// 등록 모달 열기
const openRegisterModal = () => {
setFormData({});
@@ -286,59 +276,21 @@ export default function ItemInfoPage() {
toast.success("엑셀 다운로드 완료");
};
const divisionOptions = useMemo(() => categoryOptions["division"] || [], [categoryOptions]);
const statusOptions = useMemo(() => categoryOptions["status"] || [], [categoryOptions]);
return (
<div className="flex h-full flex-col gap-0">
{/* 검색 필터 바 */}
<div className="flex items-center gap-3 border-b px-4 py-3">
<div className="relative flex-1 max-w-xs">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
<Input
className="pl-8 h-9"
placeholder="품목코드 / 품명 검색"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
</div>
<Select value={searchDivision} onValueChange={setSearchDivision}>
<SelectTrigger className="w-36 h-9">
<SelectValue placeholder="관리품목 전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{divisionOptions.map((opt) => (
<SelectItem key={opt.code} value={opt.label}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="w-32 h-9">
<SelectValue placeholder="상태 전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{statusOptions.map((opt) => (
<SelectItem key={opt.code} value={opt.label}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="ghost"
size="icon"
className="h-9 w-9 shrink-0"
onClick={() => { setSearchKeyword(""); setSearchDivision("all"); setSearchStatus("all"); }}
>
<RotateCcw className="h-4 w-4" />
</Button>
</div>
<DynamicSearchFilter
tableName={TABLE_NAME}
filterId="c16-item-info"
onFilterChange={setSearchFilters}
externalFilterConfig={ts.filterConfig}
/>
{/* 액션 바 */}
<div className="flex items-center justify-between px-4 py-2.5 border-b">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold"> </span>
<Badge variant="secondary" className="font-mono text-xs">{filteredData.length}</Badge>
<Badge variant="secondary" className="font-mono text-xs">{items.length}</Badge>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setExcelUploadOpen(true)}>
@@ -388,15 +340,15 @@ export default function ItemInfoPage() {
<div className="flex h-full items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : filteredData.length === 0 ? (
) : items.length === 0 ? (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
</div>
) : (
<Table>
<TableHeader className="sticky top-0 z-10 bg-muted/60">
<TableRow>
<TableHead className="w-10 text-center text-xs">#</TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">#</TableHead>
{ts.visibleColumns.map((col) => (
<TableHead
key={col.key}
@@ -411,7 +363,7 @@ export default function ItemInfoPage() {
</TableRow>
</TableHeader>
<TableBody>
{filteredData.map((item, idx) => (
{items.map((item, idx) => (
<TableRow
key={item.id ?? idx}
className={cn(
@@ -517,6 +469,7 @@ export default function ItemInfoPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
@@ -0,0 +1,136 @@
"use client";
import React, { useState, useCallback, useRef, useEffect } from "react";
import { Settings2, Tags, Hash } from "lucide-react";
import { cn } from "@/lib/utils";
import { CategoryColumnList } from "@/components/table-category/CategoryColumnList";
import { CategoryValueManager } from "@/components/table-category/CategoryValueManager";
import { NumberingRuleDesigner } from "@/components/numbering-rule/NumberingRuleDesigner";
const TABS = [
{ id: "category", label: "카테고리 설정", icon: Tags },
{ id: "numbering", label: "코드 설정", icon: Hash },
] as const;
type TabId = (typeof TABS)[number]["id"];
export default function OptionsSettingPage() {
const [activeTab, setActiveTab] = useState<TabId>("category");
const [selectedColumn, setSelectedColumn] = useState<string | null>(null);
const [selectedColumnLabel, setSelectedColumnLabel] = useState("");
const [selectedTableName, setSelectedTableName] = useState("");
const [leftWidth, setLeftWidth] = useState(340);
const [isDragging, setIsDragging] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const handleMouseDown = useCallback(() => {
setIsDragging(true);
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
}, []);
useEffect(() => {
if (!isDragging) return;
const handleMouseMove = (e: MouseEvent) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
setLeftWidth(Math.max(260, Math.min(500, e.clientX - rect.left)));
};
const handleMouseUp = () => {
setIsDragging(false);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [isDragging]);
return (
<div className="flex h-full flex-col p-3 gap-3">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Settings2 className="h-4 w-4 text-primary" />
<h1 className="text-sm font-semibold"> </h1>
</div>
<div className="flex bg-muted rounded-md p-0.5 gap-0.5">
{TABS.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={cn(
"flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-all",
activeTab === tab.id
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
>
<Icon className="h-3.5 w-3.5" />
{tab.label}
</button>
);
})}
</div>
</div>
<div className="flex-1 min-h-0">
{activeTab === "category" && (
<div ref={containerRef} className="flex h-full">
<div
style={{ width: leftWidth }}
className="shrink-0 border rounded-lg bg-card overflow-hidden"
>
<CategoryColumnList
tableName=""
selectedColumn={selectedColumn}
onColumnSelect={(uniqueKey, label, tableName) => {
setSelectedColumn(uniqueKey);
setSelectedColumnLabel(label);
setSelectedTableName(tableName);
}}
/>
</div>
<div
onMouseDown={handleMouseDown}
className={cn(
"w-1.5 mx-0.5 cursor-col-resize rounded-full transition-colors shrink-0",
isDragging ? "bg-primary" : "bg-border hover:bg-primary/50"
)}
/>
<div className="flex-1 min-w-0 border rounded-lg bg-card overflow-hidden">
{selectedColumn && selectedTableName ? (
<CategoryValueManager
tableName={selectedTableName}
columnName={selectedColumn.includes("__") ? selectedColumn.split("__").pop()! : selectedColumn}
columnLabel={selectedColumnLabel}
/>
) : (
<div className="flex h-full items-center justify-center">
<div className="text-center space-y-2">
<Tags className="h-8 w-8 mx-auto text-muted-foreground/30" />
<p className="text-sm text-muted-foreground"> </p>
</div>
</div>
)}
</div>
</div>
)}
{activeTab === "numbering" && (
<div className="h-full border rounded-lg bg-card overflow-auto">
<NumberingRuleDesigner />
</div>
)}
</div>
</div>
);
}
@@ -744,12 +744,12 @@ export default function MoldInfoPage() {
) : (
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs w-[60px]" />
</TableRow>
</TableHeader>
@@ -804,14 +804,14 @@ export default function MoldInfoPage() {
) : (
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs w-[60px]" />
</TableRow>
</TableHeader>
@@ -863,14 +863,14 @@ export default function MoldInfoPage() {
) : (
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs w-[60px]" />
</TableRow>
</TableHeader>
@@ -351,14 +351,14 @@ export default function SubcontractorItemPage() {
<Table noWrapper>
<thead className="sticky top-0 z-10 bg-card">
<TableRow>
{ts.isVisible("item_number") && <TableHead className="w-[110px]"></TableHead>}
{ts.isVisible("item_name") && <TableHead className="min-w-[130px]"></TableHead>}
{ts.isVisible("size") && <TableHead className="w-[90px]"></TableHead>}
{ts.isVisible("unit") && <TableHead className="w-[60px]"></TableHead>}
{ts.isVisible("standard_price") && <TableHead className="w-[90px] text-right"></TableHead>}
{ts.isVisible("selling_price") && <TableHead className="w-[90px] text-right"></TableHead>}
{ts.isVisible("currency_code") && <TableHead className="w-[50px]"></TableHead>}
{ts.isVisible("status") && <TableHead className="w-[60px]"></TableHead>}
{ts.isVisible("item_number") && <TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("item_name") && <TableHead className="min-w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("size") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("unit") && <TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("standard_price") && <TableHead className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("selling_price") && <TableHead className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("currency_code") && <TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("status") && <TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
</TableRow>
</thead>
<TableBody>
@@ -423,13 +423,13 @@ export default function SubcontractorItemPage() {
<Table noWrapper>
<thead className="sticky top-0 z-10 bg-card">
<TableRow>
<TableHead className="w-[110px]"></TableHead>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[80px] text-right"></TableHead>
<TableHead className="w-[80px] text-right"></TableHead>
<TableHead className="w-[50px]"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</thead>
<TableBody>
@@ -532,10 +532,10 @@ export default function SubcontractorItemPage() {
else setSubCheckedIds(new Set());
}} />
</TableHead>
<TableHead className="w-[110px]"></TableHead>
<TableHead className="min-w-[130px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</thead>
<TableBody>
@@ -587,6 +587,7 @@ export default function SubcontractorItemPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
@@ -30,6 +30,7 @@ import { exportToExcel } from "@/lib/utils/excelExport";
import { validateField, validateForm, formatField } from "@/lib/utils/validation";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
const SUBCONTRACTOR_TABLE = "subcontractor_mng";
const MAPPING_TABLE = "subcontractor_item_mapping";
@@ -46,10 +47,8 @@ const GRID_COLUMNS_CONFIG = [
export default function SubcontractorManagementPage() {
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
// 검색 필터
const [searchKeyword, setSearchKeyword] = useState("");
const [searchDivision, setSearchDivision] = useState("all");
const [searchStatus, setSearchStatus] = useState("all");
// 검색 필터 (DynamicSearchFilter)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 좌측: 외주업체 목록
const [subcontractors, setSubcontractors] = useState<any[]>([]);
@@ -144,10 +143,7 @@ export default function SubcontractorManagementPage() {
const fetchSubcontractors = useCallback(async () => {
setSubcontractorLoading(true);
try {
const filters: any[] = [];
if (searchKeyword) filters.push({ columnName: "subcontractor_name", operator: "contains", value: searchKeyword });
if (searchDivision !== "all") filters.push({ columnName: "division", operator: "equals", value: searchDivision });
if (searchStatus !== "all") filters.push({ columnName: "status", operator: "equals", value: searchStatus });
const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
const res = await apiClient.post(`/table-management/tables/${SUBCONTRACTOR_TABLE}/data`, {
page: 1, size: 500,
@@ -171,7 +167,7 @@ export default function SubcontractorManagementPage() {
} finally {
setSubcontractorLoading(false);
}
}, [searchKeyword, searchDivision, searchStatus, categoryOptions]);
}, [searchFilters, categoryOptions]);
useEffect(() => { fetchSubcontractors(); }, [fetchSubcontractors]);
@@ -682,44 +678,14 @@ export default function SubcontractorManagementPage() {
</nav>
{/* 검색 필터 바 */}
<div className="shrink-0 rounded-lg border bg-card p-3">
<div className="flex items-center gap-2 flex-wrap">
<Input
placeholder="외주업체명 검색"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && fetchSubcontractors()}
className="h-9 w-48 text-sm"
/>
<Select value={searchDivision} onValueChange={setSearchDivision}>
<SelectTrigger className="h-9 w-32 text-sm">
<SelectValue placeholder="거래유형" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{(categoryOptions["division"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="h-9 w-28 text-sm">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{(categoryOptions["status"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Button size="sm" onClick={fetchSubcontractors} disabled={subcontractorLoading}>
{subcontractorLoading
? <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
: <Search className="mr-1.5 h-4 w-4" />}
</Button>
<div className="ml-auto flex items-center gap-2">
<DynamicSearchFilter
tableName={SUBCONTRACTOR_TABLE}
filterId="c16-subcontractor"
onFilterChange={setSearchFilters}
dataCount={subcontractorCount}
externalFilterConfig={ts.filterConfig}
extraActions={
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
@@ -747,8 +713,8 @@ export default function SubcontractorManagementPage() {
</Button>
</div>
</div>
</div>
}
/>
{/* 마스터-디테일 분할 패널 */}
<ResizablePanelGroup direction="horizontal" className="flex-1 overflow-hidden rounded-lg">
@@ -799,14 +765,14 @@ export default function SubcontractorManagementPage() {
</div>
) : (
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
{ts.isVisible("subcontractor_code") && <TableHead className="text-xs font-semibold"></TableHead>}
{ts.isVisible("subcontractor_name") && <TableHead className="text-xs font-semibold"></TableHead>}
{ts.isVisible("contact_person") && <TableHead className="text-xs font-semibold"></TableHead>}
{ts.isVisible("contact_phone") && <TableHead className="text-xs font-semibold"></TableHead>}
{ts.isVisible("division_label") && <TableHead className="text-xs font-semibold"></TableHead>}
{ts.isVisible("status_label") && <TableHead className="text-xs font-semibold"></TableHead>}
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
{ts.isVisible("subcontractor_code") && <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("subcontractor_name") && <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("contact_person") && <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("contact_phone") && <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("division_label") && <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("status_label") && <TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
@@ -896,18 +862,18 @@ export default function SubcontractorManagementPage() {
</div>
) : (
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold text-right"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold text-right"></TableHead>
<TableHead className="text-xs font-semibold text-right"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1059,8 +1025,8 @@ export default function SubcontractorManagementPage() {
</div>
<div className="overflow-auto max-h-[350px] rounded-md border">
<Table>
<TableHeader className="sticky top-0 bg-muted/80 z-10">
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center">
<input
type="checkbox"
@@ -1072,11 +1038,11 @@ export default function SubcontractorManagementPage() {
className="h-4 w-4"
/>
</TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1325,6 +1291,7 @@ export default function SubcontractorManagementPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
@@ -63,6 +63,7 @@ import { toast } from "sonner";
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { exportToExcel } from "@/lib/utils/excelExport";
// ─── 상수 ───────────────────────────────────────
@@ -221,9 +222,7 @@ export default function BomManagementPage() {
const [bomList, setBomList] = useState<BomRow[]>([]);
const [loading, setLoading] = useState(false);
const [totalCount, setTotalCount] = useState(0);
const [searchKeyword, setSearchKeyword] = useState("");
const [searchType, setSearchType] = useState("all");
const [searchStatus, setSearchStatus] = useState("all");
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
const [selectedBomId, setSelectedBomId] = useState<string | null>(null);
const [checkedIds, setCheckedIds] = useState<string[]>([]);
@@ -291,16 +290,7 @@ export default function BomManagementPage() {
const fetchBomList = useCallback(async () => {
setLoading(true);
try {
const filters: any[] = [];
if (searchKeyword.trim()) {
filters.push({ columnName: "item_name", operator: "contains", value: searchKeyword.trim() });
}
if (searchType !== "all") {
filters.push({ columnName: "bom_type", operator: "equals", value: searchType });
}
if (searchStatus !== "all") {
filters.push({ columnName: "status", operator: "equals", value: searchStatus });
}
const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
const res = await apiClient.post(`/table-management/tables/${BOM_TABLE}/data`, {
page: 1,
@@ -318,7 +308,7 @@ export default function BomManagementPage() {
} finally {
setLoading(false);
}
}, [searchKeyword, searchType, searchStatus]);
}, [searchFilters]);
useEffect(() => {
fetchBomList();
@@ -894,60 +884,26 @@ export default function BomManagementPage() {
</nav>
{/* 검색 필터 */}
<div className="flex items-center gap-2 px-4 py-2.5 border-b bg-muted/50 shrink-0 flex-wrap">
<Input
className="h-8 w-[180px] text-xs"
placeholder="품명 검색"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && fetchBomList()}
<div className="px-4 py-2.5 border-b bg-muted/50 shrink-0">
<DynamicSearchFilter
tableName={BOM_TABLE}
filterId="c16-bom"
onFilterChange={setSearchFilters}
externalFilterConfig={ts.filterConfig}
dataCount={totalCount}
extraActions={
<div className="flex items-center gap-2">
<Button size="sm" variant="outline" className="h-8 text-xs" onClick={() => setShowExcelUpload(true)}>
<Upload className="w-3.5 h-3.5 mr-1.5" />
</Button>
<Button size="sm" variant="outline" className="h-8 text-xs" onClick={handleExcelDownload} disabled={!selectedBomId}>
<Download className="w-3.5 h-3.5 mr-1.5" />
</Button>
</div>
}
/>
<Select value={searchType} onValueChange={setSearchType}>
<SelectTrigger className="h-8 w-[130px] text-xs">
<SelectValue placeholder="BOM 유형" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
<SelectItem value="production"> BOM</SelectItem>
<SelectItem value="design"> BOM</SelectItem>
<SelectItem value="cost"> BOM</SelectItem>
</SelectContent>
</Select>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="h-8 w-[110px] text-xs">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
<SelectItem value="active"></SelectItem>
<SelectItem value="draft"></SelectItem>
<SelectItem value="expired"></SelectItem>
<SelectItem value="inactive"></SelectItem>
</SelectContent>
</Select>
<Button size="sm" className="h-8 text-xs" onClick={fetchBomList}>
<Search className="w-3.5 h-3.5 mr-1" />
</Button>
<Button
size="sm"
variant="outline"
className="h-8 text-xs"
onClick={() => { setSearchKeyword(""); setSearchType("all"); setSearchStatus("all"); }}
>
<RotateCcw className="w-3.5 h-3.5 mr-1" />
</Button>
<div className="ml-auto flex items-center gap-2">
<Button size="sm" variant="outline" className="h-8 text-xs" onClick={() => setShowExcelUpload(true)}>
<Upload className="w-3.5 h-3.5 mr-1.5" />
</Button>
<Button size="sm" variant="outline" className="h-8 text-xs" onClick={handleExcelDownload} disabled={!selectedBomId}>
<Download className="w-3.5 h-3.5 mr-1.5" />
</Button>
</div>
</div>
{/* 메인 콘텐츠: 좌우 분할 */}
@@ -1000,8 +956,8 @@ export default function BomManagementPage() {
</div>
) : (
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[36px] text-center">
<Checkbox
checked={checkedIds.length === bomList.length && bomList.length > 0}
@@ -1011,7 +967,7 @@ export default function BomManagementPage() {
/>
</TableHead>
{ts.visibleColumns.map((col) => (
<TableHead key={col.key} className="text-xs">{col.label}</TableHead>
<TableHead key={col.key} className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground">{col.label}</TableHead>
))}
</TableRow>
</TableHeader>
@@ -1529,15 +1485,15 @@ export default function BomManagementPage() {
</div>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="w-[40px] text-center">#</TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[70px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[80px]">(%)</TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">#</TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">(%)</TableHead>
<TableHead className="w-[40px]" />
</TableRow>
</TableHeader>
@@ -1798,6 +1754,7 @@ export default function BomManagementPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -86,6 +86,7 @@ import { exportToExcel } from "@/lib/utils/excelExport";
import { apiClient } from "@/lib/api/client";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
// ─── 상수 ───
@@ -146,6 +147,7 @@ export default function ProductionPlanManagementPage() {
const [filterUnplannedOrdersOnly, setFilterUnplannedOrdersOnly] = useState(false);
const [selectedStockItems, setSelectedStockItems] = useState<Set<string>>(new Set());
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
const [searchItemCode, setSearchItemCode] = useState("");
const [searchStatus, setSearchStatus] = useState("all");
const [searchStartDate, setSearchStartDate] = useState("");
@@ -265,6 +267,36 @@ export default function ProductionPlanManagementPage() {
fetchEquipmentList();
}, []);
// ========== DynamicSearchFilter 콜백 ==========
const handleSearchFilterChange = useCallback((filters: FilterValue[]) => {
setSearchFilters(filters);
let itemCode = "";
let status = "all";
let startDate = "";
let endDate = "";
for (const f of filters) {
if (f.columnName === "item_code" && f.value) itemCode = f.value;
if (f.columnName === "status" && f.value) status = f.value;
if (f.columnName === "start_date" && f.operator === "between" && f.value) {
const [s, e] = f.value.split("|");
if (s) startDate = s;
if (e) endDate = e;
}
}
setSearchItemCode(itemCode);
setSearchStatus(status || "all");
setSearchStartDate(startDate);
setSearchEndDate(endDate);
}, []);
useEffect(() => {
if (searchFilters.length > 0) {
fetchOrderSummary();
fetchPlans();
}
}, [searchItemCode, searchStatus, searchStartDate, searchEndDate]);
// ========== 토글/선택 핸들러 ==========
const toggleItemExpand = useCallback((itemCode: string) => {
@@ -901,45 +933,13 @@ export default function ProductionPlanManagementPage() {
</nav>
{/* 상단 바 */}
<div className="shrink-0 rounded-lg border bg-card p-3">
<div className="flex items-center gap-2 flex-wrap">
<Input
placeholder="품목코드 검색"
value={searchItemCode}
onChange={(e) => setSearchItemCode(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
className="h-9 w-44 text-sm"
/>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="h-9 w-32 text-sm">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="planned"></SelectItem>
<SelectItem value="work-order"></SelectItem>
<SelectItem value="in-progress"></SelectItem>
<SelectItem value="completed"></SelectItem>
</SelectContent>
</Select>
<Input
type="date"
value={searchStartDate}
onChange={(e) => setSearchStartDate(e.target.value)}
className="h-9 w-40 text-sm"
/>
<span className="text-muted-foreground text-sm">~</span>
<Input
type="date"
value={searchEndDate}
onChange={(e) => setSearchEndDate(e.target.value)}
className="h-9 w-40 text-sm"
/>
<Button size="sm" onClick={handleSearch}>
<RefreshCw className="mr-1.5 h-4 w-4" />
</Button>
<div className="ml-auto flex items-center gap-2">
<DynamicSearchFilter
tableName="sales_order_mng"
filterId="c16-plan-management"
onFilterChange={handleSearchFilterChange}
externalFilterConfig={ts.filterConfig}
extraActions={
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="text-success border-success/30 hover:bg-success/10" onClick={() => setExcelUploadOpen(true)}>
<Upload className="mr-1.5 h-4 w-4" />
@@ -952,8 +952,8 @@ export default function ProductionPlanManagementPage() {
{isFullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
}
/>
{/* 데이터 섹션 */}
<ResizablePanelGroup direction="horizontal" className="flex-1 overflow-hidden rounded-lg">
@@ -1020,23 +1020,23 @@ export default function ProductionPlanManagementPage() {
) : (
<div className="overflow-x-auto rounded-md border">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[30px]">
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
</TableHead>
<TableHead className="w-[40px]" />
<TableHead className="text-xs font-semibold whitespace-nowrap"></TableHead>
<TableHead className="text-xs font-semibold whitespace-nowrap"></TableHead>
{isColVisible("total_order_qty") && <TableHead className="text-xs font-semibold whitespace-nowrap text-right"></TableHead>}
{isColVisible("total_ship_qty") && <TableHead className="text-xs font-semibold whitespace-nowrap text-right"></TableHead>}
{isColVisible("total_balance_qty") && <TableHead className="text-xs font-semibold whitespace-nowrap text-right"></TableHead>}
{isColVisible("current_stock") && <TableHead className="text-xs font-semibold whitespace-nowrap text-right"></TableHead>}
{isColVisible("safety_stock") && <TableHead className="text-xs font-semibold whitespace-nowrap text-right"></TableHead>}
{isColVisible("existing_plan_qty") && <TableHead className="text-xs font-semibold whitespace-nowrap text-right"></TableHead>}
{isColVisible("in_progress_qty") && <TableHead className="text-xs font-semibold whitespace-nowrap text-right"></TableHead>}
{isColVisible("required_plan_qty") && <TableHead className="text-xs font-semibold whitespace-nowrap text-right"></TableHead>}
{isColVisible("lead_time") && <TableHead className="text-xs font-semibold whitespace-nowrap text-right">()</TableHead>}
<TableHead className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
{isColVisible("total_order_qty") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColVisible("total_ship_qty") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColVisible("total_balance_qty") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColVisible("current_stock") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColVisible("safety_stock") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColVisible("existing_plan_qty") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColVisible("in_progress_qty") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColVisible("required_plan_qty") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColVisible("lead_time") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">()</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
@@ -1133,18 +1133,18 @@ export default function ProductionPlanManagementPage() {
) : (
<div className="overflow-x-auto rounded-md border">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[30px]">
<Checkbox checked={selectedStockItems.size === stockItems.length && stockItems.length > 0} onCheckedChange={(c) => toggleAllStockItems(!!c)} className="h-4 w-4" />
</TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold text-right"></TableHead>
<TableHead className="text-xs font-semibold text-right"></TableHead>
<TableHead className="text-xs font-semibold text-right"></TableHead>
<TableHead className="text-xs font-semibold text-right"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1672,13 +1672,13 @@ export default function ProductionPlanManagementPage() {
</p>
<div className="rounded-md border border-success/20 overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="bg-success/5">
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold text-right"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1705,14 +1705,14 @@ export default function ProductionPlanManagementPage() {
</p>
<div className="rounded-md border border-destructive/20 overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="bg-destructive/5">
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold text-right"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1740,13 +1740,13 @@ export default function ProductionPlanManagementPage() {
</p>
<div className="rounded-md border border-warning/20 overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="bg-warning/5">
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHead className="text-xs font-semibold text-right"></TableHead>
<TableHead className="text-xs font-semibold"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-xs font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1796,6 +1796,7 @@ export default function ProductionPlanManagementPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
@@ -215,6 +215,7 @@ export default function ProcessInfoPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -9,7 +9,7 @@ import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Plus, Trash2, RotateCcw, Save, X, ChevronLeft, ChevronRight, Search, Loader2, Wrench, Pencil, CheckCircle2, ArrowRight, Check, ChevronsUpDown, ClipboardCheck, Inbox, Settings2 } from "lucide-react";
import { Plus, Trash2, Save, X, ChevronLeft, ChevronRight, Search, Loader2, Wrench, Pencil, CheckCircle2, ArrowRight, Check, ChevronsUpDown, ClipboardCheck, Inbox, Settings2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
@@ -22,6 +22,7 @@ import {
import { WorkStandardEditModal } from "./WorkStandardEditModal";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
const GRID_COLUMNS = [
{ key: "work_instruction_no", label: "작업지시번호" },
@@ -67,12 +68,7 @@ export default function WorkInstructionPage() {
const [employeeOptions, setEmployeeOptions] = useState<EmployeeOption[]>([]);
// 검색
const [searchKeyword, setSearchKeyword] = useState("");
const [debouncedKeyword, setDebouncedKeyword] = useState("");
const [searchStatus, setSearchStatus] = useState("all");
const [searchProgress, setSearchProgress] = useState("all");
const [searchDateFrom, setSearchDateFrom] = useState("");
const [searchDateTo, setSearchDateTo] = useState("");
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 1단계: 등록 모달
const [isRegModalOpen, setIsRegModalOpen] = useState(false);
@@ -138,9 +134,6 @@ export default function WorkInstructionPage() {
const [wsModalItemName, setWsModalItemName] = useState("");
const [wsModalItemCode, setWsModalItemCode] = useState("");
useEffect(() => { const t = setTimeout(() => setDebouncedKeyword(searchKeyword), 500); return () => clearTimeout(t); }, [searchKeyword]);
useEffect(() => {
getEquipmentList().then(r => { if (r.success) setEquipmentOptions(r.data || []); });
getEmployeeList().then(r => { if (r.success) setEmployeeOptions(r.data || []); });
@@ -150,23 +143,28 @@ export default function WorkInstructionPage() {
setLoading(true);
try {
const params: any = {};
if (searchDateFrom) params.dateFrom = searchDateFrom;
if (searchDateTo) params.dateTo = searchDateTo;
if (searchStatus !== "all") params.status = searchStatus;
if (searchProgress !== "all") params.progressStatus = searchProgress;
if (debouncedKeyword.trim()) params.keyword = debouncedKeyword.trim();
for (const f of searchFilters) {
if (f.columnName === "start_date" && f.operator === "between" && f.value) {
const [from, to] = f.value.split("|");
if (from) params.dateFrom = from;
if (to) params.dateTo = to;
} else if (f.columnName === "status" && f.value) {
params.status = f.value;
} else if (f.columnName === "progress" && f.value) {
params.progressStatus = f.value;
} else if (f.columnName === "work_instruction_no" && f.value) {
params.keyword = f.value;
} else if (f.columnName === "item_name" && f.value) {
params.keyword = f.value;
}
}
const r = await getWorkInstructionList(params);
if (r.success) setOrders(r.data || []);
} catch {} finally { setLoading(false); }
}, [searchDateFrom, searchDateTo, searchStatus, searchProgress, debouncedKeyword]);
}, [searchFilters]);
useEffect(() => { fetchOrders(); }, [fetchOrders]);
const handleResetSearch = () => {
setSearchKeyword(""); setDebouncedKeyword(""); setSearchStatus("all"); setSearchProgress("all");
setSearchDateFrom(""); setSearchDateTo("");
};
// ─── 1단계 등록 ───
const openRegModal = () => {
setRegSourceType("production"); setRegSourceData([]); setRegKeyword(""); setRegCheckedIds(new Set());
@@ -415,31 +413,13 @@ export default function WorkInstructionPage() {
return (
<div className="flex flex-col h-[calc(100vh-4rem)] p-3 gap-3">
{/* 검색 필터 바 */}
<div className="flex flex-wrap items-center gap-2 rounded-lg border bg-card px-3 py-2">
<div className="flex items-center gap-1.5 shrink-0">
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></span>
<Input type="date" value={searchDateFrom} onChange={(e) => setSearchDateFrom(e.target.value)} className="h-8 w-[130px] text-xs" />
<span className="text-[11px] text-muted-foreground/40">~</span>
<Input type="date" value={searchDateTo} onChange={(e) => setSearchDateTo(e.target.value)} className="h-8 w-[130px] text-xs" />
</div>
<Input placeholder="작업지시번호/품목명 검색" value={searchKeyword} onChange={e => setSearchKeyword(e.target.value)} className="h-8 w-[195px] text-xs" />
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="h-8 w-[100px] text-xs"><SelectValue /></SelectTrigger>
<SelectContent><SelectItem value="all"></SelectItem><SelectItem value="일반"></SelectItem><SelectItem value="긴급"></SelectItem></SelectContent>
</Select>
<Select value={searchProgress} onValueChange={setSearchProgress}>
<SelectTrigger className="h-8 w-[110px] text-xs"><SelectValue /></SelectTrigger>
<SelectContent><SelectItem value="all"></SelectItem><SelectItem value="대기"></SelectItem><SelectItem value="진행중"></SelectItem><SelectItem value="완료"></SelectItem></SelectContent>
</Select>
<div className="flex-1" />
{loading && <Loader2 className="w-3.5 h-3.5 animate-spin text-muted-foreground" />}
<Button variant="default" size="sm" className="h-8 text-xs gap-1" onClick={fetchOrders}>
<Search className="w-3 h-3" />
</Button>
<Button variant="ghost" size="sm" className="h-8 text-xs gap-1 text-muted-foreground" onClick={handleResetSearch}>
<RotateCcw className="w-3 h-3" />
</Button>
</div>
<DynamicSearchFilter
tableName="work_instruction"
filterId="c16-work-instruction"
onFilterChange={setSearchFilters}
externalFilterConfig={ts.filterConfig}
dataCount={new Set(orders.map(o => o.work_instruction_no)).size}
/>
{/* 메인 테이블 */}
<div className="flex-1 overflow-hidden border bg-card rounded-lg flex flex-col">
@@ -467,21 +447,21 @@ export default function WorkInstructionPage() {
{/* 테이블 */}
<div className="flex-1 overflow-auto">
<Table>
<TableHeader className="sticky top-0 bg-muted/90 z-10">
<TableRow>
{ts.isVisible("work_instruction_no") && <TableHead className="w-[150px] text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("status") && <TableHead className="w-[70px] text-center text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("progress") && <TableHead className="w-[100px] text-center text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("item_name") && <TableHead className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("spec") && <TableHead className="w-[100px] text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("qty") && <TableHead className="w-[80px] text-right text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("equipment") && <TableHead className="w-[120px] text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("routing") && <TableHead className="w-[120px] text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("work_team") && <TableHead className="w-[80px] text-center text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("worker") && <TableHead className="w-[100px] text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("start_date") && <TableHead className="w-[100px] text-center text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("end_date") && <TableHead className="w-[100px] text-center text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("actions") && <TableHead className="w-[150px] text-center text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></TableHead>}
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
{ts.isVisible("work_instruction_no") && <TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("status") && <TableHead className="w-[70px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("progress") && <TableHead className="w-[100px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("item_name") && <TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("spec") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("qty") && <TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("equipment") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("routing") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("work_team") && <TableHead className="w-[80px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("worker") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("start_date") && <TableHead className="w-[100px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("end_date") && <TableHead className="w-[100px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("actions") && <TableHead className="w-[150px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
@@ -615,12 +595,12 @@ export default function WorkInstructionPage() {
</div>
) : (
<Table>
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow>
<TableHead className="w-[50px] text-center"><Checkbox checked={regSourceData.length > 0 && regCheckedIds.size === regSourceData.length} onCheckedChange={toggleRegAll} /></TableHead>
{regSourceType === "item" && <><TableHead className="w-[120px]"></TableHead><TableHead></TableHead><TableHead className="w-[120px]"></TableHead></>}
{regSourceType === "order" && <><TableHead className="w-[110px]"></TableHead><TableHead className="w-[100px]"></TableHead><TableHead></TableHead><TableHead className="w-[100px]"></TableHead><TableHead className="w-[80px] text-right"></TableHead><TableHead className="w-[100px]"></TableHead></>}
{regSourceType === "production" && <><TableHead className="w-[110px]"></TableHead><TableHead className="w-[100px]"></TableHead><TableHead></TableHead><TableHead className="w-[80px] text-right"></TableHead><TableHead className="w-[90px]"></TableHead><TableHead className="w-[90px]"></TableHead><TableHead className="w-[100px]"></TableHead></>}
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[50px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"><Checkbox checked={regSourceData.length > 0 && regCheckedIds.size === regSourceData.length} onCheckedChange={toggleRegAll} /></TableHead>
{regSourceType === "item" && <><TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead><TableHead></TableHead><TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead></>}
{regSourceType === "order" && <><TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead><TableHead className="w-[100px]"></TableHead><TableHead></TableHead><TableHead className="w-[100px]"></TableHead><TableHead className="w-[80px] text-right"></TableHead><TableHead className="w-[100px]"></TableHead></>}
{regSourceType === "production" && <><TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead><TableHead className="w-[100px]"></TableHead><TableHead></TableHead><TableHead className="w-[80px] text-right"></TableHead><TableHead className="w-[90px]"></TableHead><TableHead className="w-[90px]"></TableHead><TableHead className="w-[100px]"></TableHead></>}
</TableRow>
</TableHeader>
<TableBody>
@@ -693,15 +673,15 @@ export default function WorkInstructionPage() {
<h4 className="text-[13px] font-bold pb-2 border-b text-foreground mb-3"> </h4>
<div className="overflow-auto">
<Table>
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow>
<TableHead className="w-[50px]"></TableHead>
<TableHead className="w-[110px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[90px]"></TableHead>
<TableHead className="w-[180px]"></TableHead>
<TableHead></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[180px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[40px]" />
</TableRow>
</TableHeader>
@@ -782,16 +762,16 @@ export default function WorkInstructionPage() {
</div>
<div className="overflow-auto">
<Table>
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow>
<TableHead className="w-[50px]"></TableHead>
<TableHead className="w-[110px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[90px] text-right"></TableHead>
<TableHead className="w-[180px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[180px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[40px]" />
</TableRow>
</TableHeader>
@@ -884,6 +864,7 @@ export default function WorkInstructionPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -13,7 +13,7 @@ import { Label } from "@/components/ui/label";
import {
Plus, Trash2, Save, Loader2, FileSpreadsheet, Download,
ClipboardList, Pencil, Search, X, Package,
ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, RotateCcw,
ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight,
Settings2,
} from "lucide-react";
import { cn } from "@/lib/utils";
@@ -25,6 +25,7 @@ import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
const MASTER_TABLE = "purchase_order_mng";
const DETAIL_TABLE = "purchase_detail";
@@ -88,12 +89,8 @@ export default function PurchaseOrderPage() {
const [loading, setLoading] = useState(false);
const [totalCount, setTotalCount] = useState(0);
// 인라인 검색 필터
const [searchOrderNo, setSearchOrderNo] = useState("");
const [searchSupplier, setSearchSupplier] = useState("");
const [searchStatus, setSearchStatus] = useState("all");
const [searchDateFrom, setSearchDateFrom] = useState("");
const [searchDateTo, setSearchDateTo] = useState("");
// 검색 필터 (DynamicSearchFilter)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
@@ -193,17 +190,28 @@ export default function PurchaseOrderPage() {
loadCategories();
}, []);
// 마스터 테이블 컬럼 (supplier_name, order_date 등)
const MASTER_COLUMNS = new Set(["supplier_name", "supplier_code", "order_date", "status"]);
// 데이터 조회
const fetchOrders = useCallback(async () => {
setLoading(true);
try {
const filters: any[] = [];
if (searchOrderNo) filters.push({ columnName: "purchase_no", operator: "contains", value: searchOrderNo });
if (searchStatus && searchStatus !== "all") filters.push({ columnName: "status", operator: "equals", value: searchStatus });
// searchFilters를 detail / master로 분리
const detailFilters: any[] = [];
const masterExtraFilters: any[] = [];
for (const f of searchFilters) {
const filter = { columnName: f.columnName, operator: f.operator, value: f.value };
if (MASTER_COLUMNS.has(f.columnName)) {
masterExtraFilters.push(filter);
} else {
detailFilters.push(filter);
}
}
const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, {
page: 1, size: 500,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined,
autoFilter: true,
sort: { columnName: "purchase_no", order: "desc" },
});
@@ -213,10 +221,7 @@ export default function PurchaseOrderPage() {
let masterMap: Record<string, any> = {};
if (purchaseNos.length > 0) {
try {
const masterFilters: any[] = [{ columnName: "purchase_no", operator: "in", value: purchaseNos }];
if (searchSupplier) masterFilters.push({ columnName: "supplier_name", operator: "contains", value: searchSupplier });
if (searchDateFrom) masterFilters.push({ columnName: "order_date", operator: "gte", value: searchDateFrom });
if (searchDateTo) masterFilters.push({ columnName: "order_date", operator: "lte", value: searchDateTo });
const masterFilters: any[] = [{ columnName: "purchase_no", operator: "in", value: purchaseNos }, ...masterExtraFilters];
const masterRes = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, {
page: 1, size: purchaseNos.length + 10,
dataFilter: { enabled: true, filters: masterFilters },
@@ -248,11 +253,11 @@ export default function PurchaseOrderPage() {
return opts.find((o) => o.code === code)?.label || code;
};
const hasMasterFilters = masterExtraFilters.length > 0;
const data = rows
.filter((row: any) => {
const master = masterMap[row.purchase_no];
// master 필터가 있으면 masterMap에 없는 것 제외
if ((searchSupplier || searchDateFrom || searchDateTo) && !master) return false;
if (hasMasterFilters && !master) return false;
return true;
})
.map((row: any) => {
@@ -278,7 +283,7 @@ export default function PurchaseOrderPage() {
} finally {
setLoading(false);
}
}, [searchOrderNo, searchSupplier, searchStatus, searchDateFrom, searchDateTo, categoryOptions]);
}, [searchFilters, categoryOptions]);
useEffect(() => { fetchOrders(); }, [fetchOrders]);
@@ -288,14 +293,6 @@ export default function PurchaseOrderPage() {
return found?.label || code;
};
const handleResetSearch = () => {
setSearchOrderNo("");
setSearchSupplier("");
setSearchStatus("all");
setSearchDateFrom("");
setSearchDateTo("");
};
// 등록 모달 열기
const openRegisterModal = () => {
const defaultInputMode = categoryOptions["input_mode"]?.[0]?.code || "";
@@ -596,57 +593,14 @@ export default function PurchaseOrderPage() {
return (
<div className="flex h-full flex-col gap-3 p-3">
{/* 인라인 검색 바 */}
<div className="flex flex-wrap items-end gap-2 rounded-lg border bg-card p-3">
<div className="flex flex-col gap-1 min-w-[160px]">
<Label className="text-[11px] font-semibold text-muted-foreground tracking-wide"></Label>
<Input
placeholder="발주번호 검색"
value={searchOrderNo}
onChange={(e) => setSearchOrderNo(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && fetchOrders()}
className="h-8 text-sm"
/>
</div>
<div className="flex flex-col gap-1 min-w-[160px]">
<Label className="text-[11px] font-semibold text-muted-foreground tracking-wide"></Label>
<Input
placeholder="공급업체 검색"
value={searchSupplier}
onChange={(e) => setSearchSupplier(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && fetchOrders()}
className="h-8 text-sm"
/>
</div>
<div className="flex flex-col gap-1 min-w-[130px]">
<Label className="text-[11px] font-semibold text-muted-foreground tracking-wide"></Label>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="h-8 text-sm"><SelectValue placeholder="전체" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{STATUS_OPTIONS.map((s) => (
<SelectItem key={s.code} value={s.code}>{s.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1 min-w-[140px]">
<Label className="text-[11px] font-semibold text-muted-foreground tracking-wide"> ()</Label>
<Input type="date" value={searchDateFrom} onChange={(e) => setSearchDateFrom(e.target.value)} className="h-8 text-sm" />
</div>
<div className="flex flex-col gap-1 min-w-[140px]">
<Label className="text-[11px] font-semibold text-muted-foreground tracking-wide"> ()</Label>
<Input type="date" value={searchDateTo} onChange={(e) => setSearchDateTo(e.target.value)} className="h-8 text-sm" />
</div>
<div className="flex items-end gap-1.5 pb-0.5">
<Button size="sm" className="h-8" onClick={fetchOrders} disabled={loading}>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /></>}
</Button>
<Button size="sm" variant="outline" className="h-8" onClick={handleResetSearch}>
<RotateCcw className="w-4 h-4 mr-1" />
</Button>
</div>
</div>
{/* 검색 필터 바 */}
<DynamicSearchFilter
tableName={DETAIL_TABLE}
filterId="c16-purchase-order"
onFilterChange={setSearchFilters}
dataCount={totalCount}
externalFilterConfig={ts.filterConfig}
/>
{/* 액션 바 */}
<div className="flex items-center justify-between flex-wrap gap-3">
@@ -686,8 +640,8 @@ export default function PurchaseOrderPage() {
{/* 데이터 테이블 */}
<div className="flex-1 overflow-auto border rounded-lg bg-card">
<Table>
<TableHeader className="sticky top-0 bg-muted z-10">
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[44px] text-center">
<Checkbox
checked={allChecked}
@@ -697,20 +651,20 @@ export default function PurchaseOrderPage() {
}}
/>
</TableHead>
{ts.isVisible("purchase_no") && <TableHead className="w-[120px]"></TableHead>}
{ts.isVisible("order_date") && <TableHead className="w-[110px]"></TableHead>}
{ts.isVisible("supplier_name") && <TableHead className="w-[140px]"></TableHead>}
{ts.isVisible("item_code") && <TableHead className="w-[120px]"></TableHead>}
{ts.isVisible("item_name") && <TableHead className="w-[140px]"></TableHead>}
{ts.isVisible("spec") && <TableHead className="w-[100px]"></TableHead>}
{ts.isVisible("order_qty") && <TableHead className="w-[80px] text-right"></TableHead>}
{ts.isVisible("received_qty") && <TableHead className="w-[80px] text-right"></TableHead>}
{ts.isVisible("remain_qty") && <TableHead className="w-[70px] text-right"></TableHead>}
{ts.isVisible("unit_price") && <TableHead className="w-[100px] text-right"></TableHead>}
{ts.isVisible("amount") && <TableHead className="w-[110px] text-right"></TableHead>}
{ts.isVisible("due_date") && <TableHead className="w-[110px]"></TableHead>}
{ts.isVisible("status") && <TableHead className="w-[90px]"></TableHead>}
{ts.isVisible("memo") && <TableHead className="w-[120px]"></TableHead>}
{ts.isVisible("purchase_no") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("order_date") && <TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("supplier_name") && <TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("item_code") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("item_name") && <TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("spec") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("order_qty") && <TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("received_qty") && <TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("remain_qty") && <TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("unit_price") && <TableHead className="w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("amount") && <TableHead className="w-[110px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("due_date") && <TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("status") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("memo") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
@@ -923,20 +877,20 @@ export default function PurchaseOrderPage() {
) : (
<div className="border rounded-lg overflow-x-auto">
<Table className="table-fixed">
<TableHeader className="sticky top-0 bg-muted z-10">
<TableRow>
{!isReadOnly && <TableHead className="w-[40px]"></TableHead>}
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[60px]"></TableHead>
<TableHead className="w-[90px]"></TableHead>
<TableHead className="w-[90px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[160px]"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
{!isReadOnly && <TableHead className="w-[40px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1052,8 +1006,8 @@ export default function PurchaseOrderPage() {
</div>
<div className="flex-1 overflow-auto border rounded-lg">
<Table>
<TableHeader className="sticky top-0 bg-muted z-10">
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center">
<input type="checkbox"
checked={itemSearchResults.length > 0 && itemSearchResults.every((i) => itemSelectedMap.has(i.id))}
@@ -1066,11 +1020,11 @@ export default function PurchaseOrderPage() {
});
}} />
</TableHead>
<TableHead className="w-[130px]"></TableHead>
<TableHead className="min-w-[150px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[60px]"></TableHead>
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1163,6 +1117,7 @@ export default function PurchaseOrderPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
@@ -9,7 +9,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogD
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Truck, Search, RotateCcw, Settings2 } from "lucide-react";
import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Truck, Search, Settings2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
import { useAuth } from "@/hooks/useAuth";
@@ -19,6 +19,7 @@ import { ExcelUploadModal } from "@/components/common/ExcelUploadModal";
import { exportToExcel } from "@/lib/utils/excelExport";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
const ITEM_TABLE = "item_info";
const MAPPING_TABLE = "supplier_item_mapping";
@@ -35,9 +36,8 @@ export default function PurchaseItemPage() {
const { user } = useAuth();
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
// 검색
const [inputKeyword, setInputKeyword] = useState("");
const [searchKeyword, setSearchKeyword] = useState("");
// 검색 필터 (DynamicSearchFilter)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 좌측: 품목
const [items, setItems] = useState<any[]>([]);
@@ -108,10 +108,7 @@ export default function PurchaseItemPage() {
const fetchItems = useCallback(async () => {
setItemLoading(true);
try {
const filters: any[] = [];
if (searchKeyword) {
filters.push({ columnName: "item_name", operator: "contains", value: searchKeyword });
}
const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
const res = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, {
page: 1, size: 500,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
@@ -123,7 +120,7 @@ export default function PurchaseItemPage() {
} finally {
setItemLoading(false);
}
}, [searchKeyword]);
}, [searchFilters]);
useEffect(() => { fetchItems(); }, [fetchItems]);
@@ -349,33 +346,23 @@ export default function PurchaseItemPage() {
return (
<div className="flex h-full flex-col gap-3 p-3">
{/* 검색 바 */}
<div className="flex items-center gap-3 rounded-lg border bg-card p-3 shrink-0">
<div className="flex items-center gap-1.5">
<Label className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap">/</Label>
<Input
placeholder="품번 또는 품명 검색"
value={inputKeyword}
onChange={(e) => setInputKeyword(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") setSearchKeyword(inputKeyword); }}
className="w-[220px] h-9"
/>
</div>
<div className="flex items-center gap-1.5 ml-auto">
<Button variant="ghost" size="sm" onClick={() => { setInputKeyword(""); setSearchKeyword(""); }}>
<RotateCcw className="w-3.5 h-3.5" />
</Button>
<Button size="sm" onClick={() => setSearchKeyword(inputKeyword)}>
<Search className="w-3.5 h-3.5" />
</Button>
<div className="h-4 w-px bg-border mx-1" />
<Button variant="outline" size="sm" onClick={() => setExcelUploadOpen(true)}>
<FileSpreadsheet className="w-3.5 h-3.5" />
</Button>
<Button variant="outline" size="sm" onClick={handleExcelDownload}>
<Download className="w-3.5 h-3.5" />
</Button>
</div>
</div>
<DynamicSearchFilter
tableName={ITEM_TABLE}
filterId="c16-purchase-item"
onFilterChange={setSearchFilters}
dataCount={items.length}
externalFilterConfig={ts.filterConfig}
extraActions={
<div className="flex items-center gap-1.5">
<Button variant="outline" size="sm" onClick={() => setExcelUploadOpen(true)}>
<FileSpreadsheet className="w-3.5 h-3.5" />
</Button>
<Button variant="outline" size="sm" onClick={handleExcelDownload}>
<Download className="w-3.5 h-3.5" />
</Button>
</div>
}
/>
{/* 분할 패널 */}
<div className="flex-1 overflow-hidden">
@@ -395,14 +382,14 @@ export default function PurchaseItemPage() {
</div>
<div className="flex-1 overflow-auto">
<Table>
<TableHeader className="sticky top-0 bg-muted/50 z-10">
<TableRow className="text-[11px]">
<TableHead className="p-2 w-[110px]"></TableHead>
<TableHead className="p-2"></TableHead>
{isColVisible("size") && <TableHead className="p-2 w-[90px]"></TableHead>}
{isColVisible("unit") && <TableHead className="p-2 w-[60px]"></TableHead>}
{isColVisible("standard_price") && <TableHead className="p-2 w-[90px] text-right"></TableHead>}
{isColVisible("status") && <TableHead className="p-2 w-[60px] text-center"></TableHead>}
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
{isColVisible("size") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColVisible("unit") && <TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColVisible("standard_price") && <TableHead className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColVisible("status") && <TableHead className="w-[60px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
@@ -480,8 +467,8 @@ export default function PurchaseItemPage() {
</div>
<div className="flex-1 overflow-auto">
<Table>
<TableHeader className="sticky top-0 bg-muted/50 z-10">
<TableRow className="text-[11px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="p-2 w-10">
<Checkbox
checked={supplierItems.length > 0 && supplierCheckedIds.length === supplierItems.length}
@@ -491,13 +478,13 @@ export default function PurchaseItemPage() {
}}
/>
</TableHead>
<TableHead className="p-2 w-[110px]"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2 w-[100px]"></TableHead>
<TableHead className="p-2 w-[80px] text-right"></TableHead>
<TableHead className="p-2 w-[80px] text-right"></TableHead>
<TableHead className="p-2 w-[50px]"></TableHead>
<TableHead className="p-2 w-[70px] text-right"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -604,8 +591,8 @@ export default function PurchaseItemPage() {
</div>
<div className="overflow-auto max-h-[350px] border rounded-lg">
<Table>
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 text-center">
<Checkbox
checked={suppSearchResults.length > 0 && suppCheckedIds.size === suppSearchResults.length}
@@ -615,10 +602,10 @@ export default function PurchaseItemPage() {
}}
/>
</TableHead>
<TableHead className="w-[110px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -758,6 +745,7 @@ export default function PurchaseItemPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
@@ -9,7 +9,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogD
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Truck, Search, RotateCcw, Settings2 } from "lucide-react";
import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Truck, Search, Settings2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
import { useAuth } from "@/hooks/useAuth";
@@ -19,6 +19,7 @@ import { ExcelUploadModal } from "@/components/common/ExcelUploadModal";
import { exportToExcel } from "@/lib/utils/excelExport";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
const SUPPLIER_TABLE = "supplier_mng";
const MAPPING_TABLE = "supplier_item_mapping";
@@ -33,9 +34,8 @@ export default function SupplierManagementPage() {
const { user } = useAuth();
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
// 검색
const [inputKeyword, setInputKeyword] = useState("");
const [searchKeyword, setSearchKeyword] = useState("");
// 검색 필터 (DynamicSearchFilter)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 좌측: 공급업체 목록
const [suppliers, setSuppliers] = useState<any[]>([]);
@@ -81,8 +81,7 @@ export default function SupplierManagementPage() {
const fetchSuppliers = useCallback(async () => {
setSupplierLoading(true);
try {
const filters: any[] = [];
if (searchKeyword) filters.push({ columnName: "supplier_name", operator: "contains", value: searchKeyword });
const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
const res = await apiClient.post(`/table-management/tables/${SUPPLIER_TABLE}/data`, {
page: 1, size: 500,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
@@ -94,7 +93,7 @@ export default function SupplierManagementPage() {
} finally {
setSupplierLoading(false);
}
}, [searchKeyword]);
}, [searchFilters]);
useEffect(() => { fetchSuppliers(); }, [fetchSuppliers]);
@@ -331,33 +330,23 @@ export default function SupplierManagementPage() {
return (
<div className="flex h-full flex-col gap-3 p-3">
{/* 검색 바 */}
<div className="flex items-center gap-3 rounded-lg border bg-card p-3 shrink-0">
<div className="flex items-center gap-1.5">
<Label className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap"></Label>
<Input
placeholder="공급업체명 검색"
value={inputKeyword}
onChange={(e) => setInputKeyword(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") setSearchKeyword(inputKeyword); }}
className="w-[220px] h-9"
/>
</div>
<div className="flex items-center gap-1.5 ml-auto">
<Button variant="ghost" size="sm" onClick={() => { setInputKeyword(""); setSearchKeyword(""); }}>
<RotateCcw className="w-3.5 h-3.5" />
</Button>
<Button size="sm" onClick={() => setSearchKeyword(inputKeyword)}>
<Search className="w-3.5 h-3.5" />
</Button>
<div className="h-4 w-px bg-border mx-1" />
<Button variant="outline" size="sm" onClick={() => setExcelUploadOpen(true)}>
<FileSpreadsheet className="w-3.5 h-3.5" />
</Button>
<Button variant="outline" size="sm" onClick={handleExcelDownload}>
<Download className="w-3.5 h-3.5" />
</Button>
</div>
</div>
<DynamicSearchFilter
tableName={SUPPLIER_TABLE}
filterId="c16-supplier"
onFilterChange={setSearchFilters}
dataCount={suppliers.length}
externalFilterConfig={ts.filterConfig}
extraActions={
<div className="flex items-center gap-1.5">
<Button variant="outline" size="sm" onClick={() => setExcelUploadOpen(true)}>
<FileSpreadsheet className="w-3.5 h-3.5" />
</Button>
<Button variant="outline" size="sm" onClick={handleExcelDownload}>
<Download className="w-3.5 h-3.5" />
</Button>
</div>
}
/>
{/* 분할 패널 */}
<div className="flex-1 overflow-hidden">
@@ -382,13 +371,13 @@ export default function SupplierManagementPage() {
</div>
<div className="flex-1 overflow-auto">
<Table>
<TableHeader className="sticky top-0 bg-muted/50 z-10">
<TableRow className="text-[11px]">
<TableHead className="p-2 w-[120px]"></TableHead>
<TableHead className="p-2"></TableHead>
{isColVisible("contact_person") && <TableHead className="p-2 w-[90px]"></TableHead>}
{isColVisible("contact_phone") && <TableHead className="p-2 w-[120px]"></TableHead>}
{isColVisible("status") && <TableHead className="p-2 w-[70px] text-center"></TableHead>}
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
{isColVisible("contact_person") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColVisible("contact_phone") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColVisible("status") && <TableHead className="w-[70px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
@@ -462,8 +451,8 @@ export default function SupplierManagementPage() {
</div>
<div className="flex-1 overflow-auto">
<Table>
<TableHeader className="sticky top-0 bg-muted/50 z-10">
<TableRow className="text-[11px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="p-2 w-10">
<Checkbox
checked={mappingItems.length > 0 && mappingCheckedIds.length === mappingItems.length}
@@ -473,13 +462,13 @@ export default function SupplierManagementPage() {
}}
/>
</TableHead>
<TableHead className="p-2 w-[100px]"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2 w-[100px]"></TableHead>
<TableHead className="p-2 w-[80px] text-right"></TableHead>
<TableHead className="p-2 w-[80px] text-right"></TableHead>
<TableHead className="p-2 w-[50px]"></TableHead>
<TableHead className="p-2 w-[70px] text-right"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -590,8 +579,8 @@ export default function SupplierManagementPage() {
</div>
<div className="overflow-auto max-h-[350px] border rounded-lg">
<Table>
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 text-center">
<Checkbox
checked={itemSearchResults.length > 0 && itemCheckedIds.size === itemSearchResults.length}
@@ -601,11 +590,11 @@ export default function SupplierManagementPage() {
}}
/>
</TableHead>
<TableHead className="w-[110px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[90px]"></TableHead>
<TableHead className="w-[60px]"></TableHead>
<TableHead className="w-[90px] text-right"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -746,6 +735,7 @@ export default function SupplierManagementPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
@@ -14,6 +14,7 @@ import {
Plus, Trash2, Save, Loader2, Pencil,
ClipboardCheck, AlertTriangle, Wrench, Search, Inbox, Settings2,
} from "lucide-react";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
import { useAuth } from "@/hooks/useAuth";
@@ -63,7 +64,7 @@ export default function InspectionManagementPage() {
const [inspEditMode, setInspEditMode] = useState(false);
const [inspForm, setInspForm] = useState<Record<string, any>>({});
const [inspSaving, setInspSaving] = useState(false);
const [inspKeyword, setInspKeyword] = useState("");
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
/* ───── 불량관리 ───── */
const [defects, setDefects] = useState<any[]>([]);
@@ -122,12 +123,10 @@ export default function InspectionManagementPage() {
};
/* ═══════════════════ 데이터 조회 ═══════════════════ */
const fetchInspections = useCallback(async (keyword?: string) => {
const fetchInspections = useCallback(async () => {
setInspLoading(true);
try {
const kw = keyword !== undefined ? keyword : inspKeyword;
const filters: any[] = [];
if (kw.trim()) filters.push({ columnName: "inspection_standard", operator: "contains", value: kw.trim() });
const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
const res = await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/data`, {
page: 1, size: 500,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
@@ -141,7 +140,7 @@ export default function InspectionManagementPage() {
} finally {
setInspLoading(false);
}
}, [inspKeyword]);
}, [searchFilters]);
const fetchDefects = useCallback(async () => {
setDefLoading(true);
@@ -175,11 +174,8 @@ export default function InspectionManagementPage() {
}
}, []);
useEffect(() => {
fetchInspections();
fetchDefects();
fetchEquipments();
}, []);
useEffect(() => { fetchInspections(); }, [fetchInspections]);
useEffect(() => { fetchDefects(); fetchEquipments(); }, []);
/* ───── 클라이언트 필터 ───── */
const filteredDefects = defKeyword.trim()
@@ -333,43 +329,33 @@ export default function InspectionManagementPage() {
{/* ──── 검사기준 탭 ──── */}
<TabsContent value="inspection" className="p-3 mt-0">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-2.5 top-2 h-3.5 w-3.5 text-muted-foreground" />
<Input
className="h-8 pl-8 w-56 text-sm"
placeholder="검사기준 검색..."
value={inspKeyword}
onChange={(e) => setInspKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && fetchInspections(inspKeyword)}
/>
</div>
<Button size="sm" variant="outline" className="h-8" onClick={() => fetchInspections(inspKeyword)}>
<Search className="w-3.5 h-3.5 mr-1" />
</Button>
<Button size="sm" variant="ghost" className="h-8" onClick={() => { setInspKeyword(""); fetchInspections(""); }}>
</Button>
<Badge variant="secondary" className="bg-primary/10 text-primary">{inspCount}</Badge>
</div>
<div className="flex items-center gap-2">
<Button size="sm" onClick={openInspCreate}><Plus className="w-4 h-4 mr-1" /></Button>
<Button size="sm" variant="outline" onClick={() => {
const sel = inspections.find(r => inspChecked.includes(r.id));
if (sel) openInspEdit(sel);
else toast.error("수정할 항목을 선택해주세요");
}}><Pencil className="w-4 h-4 mr-1" /></Button>
<Button size="sm" variant="destructive" onClick={deleteInspections}><Trash2 className="w-4 h-4 mr-1" /></Button>
<Button variant="ghost" size="sm" onClick={() => ts.setOpen(true)} title="테이블 설정">
<Settings2 className="h-4 w-4" />
</Button>
</div>
<div className="mb-3">
<DynamicSearchFilter
tableName={INSPECTION_TABLE}
filterId="c16-inspection"
onFilterChange={setSearchFilters}
externalFilterConfig={ts.filterConfig}
dataCount={inspCount}
extraActions={
<div className="flex items-center gap-2">
<Button size="sm" onClick={openInspCreate}><Plus className="w-4 h-4 mr-1" /></Button>
<Button size="sm" variant="outline" onClick={() => {
const sel = inspections.find(r => inspChecked.includes(r.id));
if (sel) openInspEdit(sel);
else toast.error("수정할 항목을 선택해주세요");
}}><Pencil className="w-4 h-4 mr-1" /></Button>
<Button size="sm" variant="destructive" onClick={deleteInspections}><Trash2 className="w-4 h-4 mr-1" /></Button>
<Button variant="ghost" size="sm" onClick={() => ts.setOpen(true)} title="테이블 설정">
<Settings2 className="h-4 w-4" />
</Button>
</div>
}
/>
</div>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10">
<Checkbox
checked={inspections.length > 0 && inspChecked.length === inspections.length}
@@ -377,7 +363,7 @@ export default function InspectionManagementPage() {
/>
</TableHead>
{ts.visibleColumns.map((col) => (
<TableHead key={col.key}>{col.label}</TableHead>
<TableHead key={col.key} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">{col.label}</TableHead>
))}
</TableRow>
</TableHeader>
@@ -436,18 +422,18 @@ export default function InspectionManagementPage() {
</div>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10">
<Checkbox
checked={filteredDefects.length > 0 && defChecked.length === filteredDefects.length}
onCheckedChange={(v) => setDefChecked(v ? filteredDefects.map(r => r.id) : [])}
/>
</TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[80px] text-center"></TableHead>
<TableHead className="w-[80px] text-center"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -507,20 +493,20 @@ export default function InspectionManagementPage() {
</div>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10">
<Checkbox
checked={filteredEquipments.length > 0 && eqChecked.length === filteredEquipments.length}
onCheckedChange={(v) => setEqChecked(v ? filteredEquipments.map(r => r.id) : [])}
/>
</TableHead>
<TableHead></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[110px]"></TableHead>
<TableHead className="w-[90px]"></TableHead>
<TableHead className="w-[110px]"></TableHead>
<TableHead className="w-[90px]"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -722,6 +708,7 @@ export default function InspectionManagementPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -10,8 +10,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
import {
Plus, Trash2, Save, Loader2, Pencil, Search, Inbox, Settings2,
Plus, Trash2, Save, Loader2, Pencil, Inbox, Settings2,
} from "lucide-react";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
import { useAuth } from "@/hooks/useAuth";
@@ -42,7 +43,7 @@ export default function ItemInspectionInfoPage() {
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [totalCount, setTotalCount] = useState(0);
const [searchKeyword, setSearchKeyword] = useState("");
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
const [checkedIds, setCheckedIds] = useState<string[]>([]);
const [modalOpen, setModalOpen] = useState(false);
@@ -73,12 +74,10 @@ export default function ItemInspectionInfoPage() {
}, []);
/* ═══════════════════ 데이터 조회 ═══════════════════ */
const fetchData = useCallback(async (keyword?: string) => {
const fetchData = useCallback(async () => {
setLoading(true);
try {
const kw = keyword !== undefined ? keyword : searchKeyword;
const filters: any[] = [];
if (kw.trim()) filters.push({ columnName: "item_name", operator: "contains", value: kw.trim() });
const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, {
page: 1, size: 500,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
@@ -92,9 +91,9 @@ export default function ItemInspectionInfoPage() {
} finally {
setLoading(false);
}
}, [searchKeyword]);
}, [searchFilters]);
useEffect(() => { fetchData(); }, []);
useEffect(() => { fetchData(); }, [fetchData]);
/* ═══════════════════ CRUD ═══════════════════ */
const openCreate = () => { setForm({}); setEditMode(false); setModalOpen(true); };
@@ -141,44 +140,34 @@ export default function ItemInspectionInfoPage() {
{ConfirmDialogComponent}
<div className="rounded-lg border bg-card">
<div className="px-3 py-2.5 border-b bg-muted/50 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-2.5 top-2 h-3.5 w-3.5 text-muted-foreground" />
<Input
className="h-8 pl-8 w-56 text-sm"
placeholder="품목명 검색..."
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && fetchData(searchKeyword)}
/>
</div>
<Button size="sm" variant="outline" className="h-8" onClick={() => fetchData(searchKeyword)}>
<Search className="w-3.5 h-3.5 mr-1" />
</Button>
<Button size="sm" variant="ghost" className="h-8" onClick={() => { setSearchKeyword(""); fetchData(""); }}>
</Button>
<Badge variant="secondary" className="bg-primary/10 text-primary">{totalCount}</Badge>
</div>
<div className="flex items-center gap-2">
<Button size="sm" onClick={openCreate}><Plus className="w-4 h-4 mr-1" /></Button>
<Button size="sm" variant="outline" onClick={() => {
const sel = data.find(r => checkedIds.includes(r.id));
if (sel) openEdit(sel);
else toast.error("수정할 항목을 선택해주세요");
}}><Pencil className="w-4 h-4 mr-1" /></Button>
<Button size="sm" variant="destructive" onClick={handleDelete}><Trash2 className="w-4 h-4 mr-1" /></Button>
<Button variant="ghost" size="sm" onClick={() => ts.setOpen(true)} title="테이블 설정">
<Settings2 className="h-4 w-4" />
</Button>
</div>
<div className="px-3 py-2.5 border-b bg-muted/50">
<DynamicSearchFilter
tableName={TABLE_NAME}
filterId="c16-item-inspection"
onFilterChange={setSearchFilters}
externalFilterConfig={ts.filterConfig}
dataCount={totalCount}
extraActions={
<div className="flex items-center gap-2">
<Button size="sm" onClick={openCreate}><Plus className="w-4 h-4 mr-1" /></Button>
<Button size="sm" variant="outline" onClick={() => {
const sel = data.find(r => checkedIds.includes(r.id));
if (sel) openEdit(sel);
else toast.error("수정할 항목을 선택해주세요");
}}><Pencil className="w-4 h-4 mr-1" /></Button>
<Button size="sm" variant="destructive" onClick={handleDelete}><Trash2 className="w-4 h-4 mr-1" /></Button>
<Button variant="ghost" size="sm" onClick={() => ts.setOpen(true)} title="테이블 설정">
<Settings2 className="h-4 w-4" />
</Button>
</div>
}
/>
</div>
<div className="p-3">
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10">
<Checkbox
checked={data.length > 0 && checkedIds.length === data.length}
@@ -186,7 +175,7 @@ export default function ItemInspectionInfoPage() {
/>
</TableHead>
{ts.visibleColumns.map((col) => (
<TableHead key={col.key}>{col.label}</TableHead>
<TableHead key={col.key} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">{col.label}</TableHead>
))}
</TableRow>
</TableHeader>
@@ -290,6 +279,7 @@ export default function ItemInspectionInfoPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -61,8 +61,6 @@ import {
Pencil,
FileText,
Wrench,
Search,
RotateCcw,
Settings2,
} from "lucide-react";
import { cn } from "@/lib/utils";
@@ -72,6 +70,7 @@ import { toast } from "sonner";
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { ExcelUploadModal } from "@/components/common/ExcelUploadModal";
import { exportToExcel } from "@/lib/utils/excelExport";
@@ -156,12 +155,8 @@ export default function ClaimManagementPage() {
const [totalCount, setTotalCount] = useState(0);
const [selectedClaimNo, setSelectedClaimNo] = useState<string | null>(null);
// 로컬 검색 필터 상태
const [searchKeyword, setSearchKeyword] = useState("");
const [searchType, setSearchType] = useState<string>("all");
const [searchStatus, setSearchStatus] = useState<string>("all");
const [dateFrom, setDateFrom] = useState("");
const [dateTo, setDateTo] = useState("");
// 검색 필터 (DynamicSearchFilter에서 관리)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 엑셀 업로드
const [excelUploadOpen, setExcelUploadOpen] = useState(false);
@@ -186,11 +181,11 @@ export default function ClaimManagementPage() {
const fetchData = useCallback(async () => {
setLoading(true);
try {
const filters: any[] = [];
if (searchType !== "all") filters.push({ columnName: "claim_type", operator: "equals", value: searchType });
if (searchStatus !== "all") filters.push({ columnName: "claim_status", operator: "equals", value: searchStatus });
if (dateFrom) filters.push({ columnName: "claim_date", operator: "gte", value: dateFrom });
if (dateTo) filters.push({ columnName: "claim_date", operator: "lte", value: dateTo });
const filters = searchFilters.map(f => ({
columnName: f.columnName,
operator: f.operator,
value: f.value,
}));
const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, {
page: 1,
@@ -208,22 +203,12 @@ export default function ClaimManagementPage() {
} finally {
setLoading(false);
}
}, [searchType, searchStatus, dateFrom, dateTo]);
}, [searchFilters]);
useEffect(() => {
fetchData();
}, [fetchData]);
// 클라이언트 사이드 필터링 (키워드 다중컬럼 검색만)
const filteredData = useMemo(() => {
if (!searchKeyword) return data;
const kw = searchKeyword.toLowerCase();
return data.filter((claim) =>
claim.claim_no?.toLowerCase().includes(kw) ||
claim.customer_name?.toLowerCase().includes(kw) ||
claim.manager_name?.toLowerCase().includes(kw)
);
}, [data, searchKeyword]);
// 거래처 목록 조회 (autoFilter로 멀티테넌시 적용)
const fetchCustomers = useCallback(async (force = false) => {
@@ -431,96 +416,16 @@ export default function ClaimManagementPage() {
[data, selectedClaimNo]
);
// 검색 초기화
const handleResetSearch = () => {
setSearchKeyword("");
setSearchType("all");
setSearchStatus("all");
setDateFrom("");
setDateTo("");
};
return (
<div className="flex flex-col h-[calc(100vh-4rem)] overflow-hidden">
{/* ───── 검색 필터 바 ───── */}
<div className="flex items-center gap-2 flex-wrap px-4 py-2.5 bg-card border-b shrink-0">
{/* 접수일자 범위 */}
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap"></span>
<Input
type="date"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
className="h-8 w-[130px] text-sm"
/>
<span className="text-muted-foreground/40 text-xs">~</span>
<Input
type="date"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
className="h-8 w-[130px] text-sm"
/>
</div>
{/* 클레임 유형 */}
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap"></span>
<Select value={searchType} onValueChange={setSearchType}>
<SelectTrigger className="h-8 w-[110px] text-sm">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{CLAIM_TYPES.map((t) => (
<SelectItem key={t} value={t}>{t}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 처리 상태 */}
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap"></span>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="h-8 w-[100px] text-sm">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{CLAIM_STATUSES.map((s) => (
<SelectItem key={s} value={s}>{s}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 검색어 */}
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap"></span>
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none" />
<Input
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
placeholder="클레임번호/거래처명"
className="h-8 pl-8 w-[180px] text-sm"
onKeyDown={(e) => e.key === "Enter" && fetchData()}
/>
</div>
</div>
{/* 버튼 영역 */}
<div className="flex items-center gap-1.5 ml-auto">
<Button variant="outline" size="sm" onClick={handleResetSearch} className="h-8">
<RotateCcw className="w-3.5 h-3.5" />
</Button>
<Button variant="outline" size="sm" onClick={fetchData} className="h-8">
<Search className="w-3.5 h-3.5" />
</Button>
</div>
</div>
{/* 검색 필터 (DynamicSearchFilter) */}
<DynamicSearchFilter
tableName={TABLE_NAME}
filterId="c16-claim"
onFilterChange={setSearchFilters}
dataCount={data.length}
externalFilterConfig={ts.filterConfig}
/>
{/* ───── 메인 분할 레이아웃 ───── */}
<div className="flex-1 overflow-hidden">
@@ -534,7 +439,7 @@ export default function ClaimManagementPage() {
<ClipboardList className="w-4 h-4 text-muted-foreground" />
<span className="text-[13px] font-bold text-foreground"> </span>
<span className="text-[11px] font-semibold text-primary bg-primary/8 border border-primary/15 px-2 py-0.5 rounded-full font-mono">
{filteredData.length}
{data.length}
</span>
{loading && <Loader2 className="w-3.5 h-3.5 animate-spin text-muted-foreground" />}
</div>
@@ -560,8 +465,8 @@ export default function ClaimManagementPage() {
{/* 테이블 */}
<div className="flex-1 overflow-auto">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center text-[11px] font-bold uppercase tracking-wide">#</TableHead>
{ts.visibleColumns.map((col) => (
<TableHead key={col.key} className="text-[11px] font-bold uppercase tracking-wide">
@@ -580,7 +485,7 @@ export default function ClaimManagementPage() {
</div>
</TableCell>
</TableRow>
) : filteredData.length === 0 ? (
) : data.length === 0 ? (
<TableRow>
<TableCell colSpan={ts.visibleColumns.length + 1} className="h-32 text-center">
<div className="flex flex-col items-center gap-2 text-muted-foreground">
@@ -590,7 +495,7 @@ export default function ClaimManagementPage() {
</TableCell>
</TableRow>
) : (
filteredData.map((claim, idx) => (
data.map((claim, idx) => (
<TableRow
key={claim.id}
className={cn(
@@ -1071,6 +976,7 @@ export default function ClaimManagementPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -35,21 +35,31 @@ import { MultiTableExcelUploadModal } from "@/components/common/MultiTableExcelU
import { autoDetectMultiTableConfig, TableChainConfig } from "@/lib/api/multiTableExcel";
import { exportToExcel } from "@/lib/utils/excelExport";
import { validateField, validateForm, formatField } from "@/lib/utils/validation";
import { TableSettingsModal, TableSettings, loadTableSettings } from "@/components/common/TableSettingsModal";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { useTableSettings } from "@/hooks/useTableSettings";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
const CUSTOMER_TABLE = "customer_mng";
const MAPPING_TABLE = "customer_item_mapping";
const PRICE_TABLE = "customer_item_prices";
const DELIVERY_TABLE = "delivery_destination";
const CUSTOMER_GRID_COLUMNS = [
{ key: "customer_code", label: "거래처코드" },
{ key: "customer_name", label: "거래처명" },
{ key: "contact_person", label: "대표자" },
{ key: "contact_phone", label: "연락처" },
{ key: "division", label: "유형" },
{ key: "status", label: "상태" },
];
export default function CustomerManagementPage() {
const { user } = useAuth();
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
const ts = useTableSettings("c16-customer", CUSTOMER_TABLE, CUSTOMER_GRID_COLUMNS);
// 검색 필터
const [searchKeyword, setSearchKeyword] = useState("");
const [searchDivision, setSearchDivision] = useState("__all__");
const [searchStatus, setSearchStatus] = useState("__all__");
// 검색 필터 (DynamicSearchFilter에서 관리)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 좌측: 거래처 목록
const [customers, setCustomers] = useState<any[]>([]);
@@ -112,14 +122,6 @@ export default function CustomerManagementPage() {
const [categoryOptions, setCategoryOptions] = useState<Record<string, { code: string; label: string }[]>>({});
const [employeeOptions, setEmployeeOptions] = useState<{ user_id: string; user_name: string; position_name?: string }[]>([]);
// 테이블 설정
const [tableSettingsOpen, setTableSettingsOpen] = useState(false);
const [gridColumns, setGridColumns] = useState<TableSettings["columns"]>(
() => loadTableSettings("customer-mng")?.columns || []
);
const [filterConfig, setFilterConfig] = useState<TableSettings["filters"] | undefined>(
() => loadTableSettings("customer-mng")?.filters
);
// 카테고리 로드
useEffect(() => {
@@ -164,10 +166,11 @@ export default function CustomerManagementPage() {
const fetchCustomers = useCallback(async () => {
setCustomerLoading(true);
try {
const filters: any[] = [];
if (searchKeyword) filters.push({ columnName: "customer_name", operator: "contains", value: searchKeyword });
if (searchDivision !== "__all__") filters.push({ columnName: "division", operator: "equals", value: searchDivision });
if (searchStatus !== "__all__") filters.push({ columnName: "status", operator: "equals", value: searchStatus });
const filters = searchFilters.map(f => ({
columnName: f.columnName,
operator: f.operator,
value: f.value,
}));
const res = await apiClient.post(`/table-management/tables/${CUSTOMER_TABLE}/data`, {
page: 1, size: 500,
@@ -196,7 +199,7 @@ export default function CustomerManagementPage() {
} finally {
setCustomerLoading(false);
}
}, [searchKeyword, searchDivision, searchStatus, categoryOptions, employeeOptions]);
}, [searchFilters, categoryOptions, employeeOptions]);
useEffect(() => { fetchCustomers(); }, [fetchCustomers]);
@@ -769,11 +772,7 @@ export default function CustomerManagementPage() {
};
// 컬럼 가시성 헬퍼
const isColumnVisible = (key: string) => {
if (gridColumns.length === 0) return true;
const col = gridColumns.find((c) => c.columnName === key);
return col ? col.visible : true;
};
const isColumnVisible = (key: string) => ts.isVisible(key);
const customerColSpan = 1 + ["customer_code", "customer_name", "contact_person", "contact_phone", "division", "status"]
.filter((k) => isColumnVisible(k)).length;
@@ -848,49 +847,17 @@ export default function CustomerManagementPage() {
return (
<div className="flex h-full flex-col gap-3 p-4">
{/* 검색 필터 */}
<div className="flex items-center gap-2 px-4 py-3 rounded-lg border bg-muted flex-wrap shrink-0">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-muted-foreground whitespace-nowrap"></span>
<Input
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && fetchCustomers()}
placeholder="거래처명 검색"
className="h-8 w-40 text-sm"
/>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-muted-foreground"></span>
<Select value={searchDivision} onValueChange={setSearchDivision}>
<SelectTrigger className="h-8 w-28 text-xs">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__"></SelectItem>
{(categoryOptions["division"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-muted-foreground"></span>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="h-8 w-24 text-xs">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__"></SelectItem>
{(categoryOptions["status"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button size="sm" className="h-8" onClick={fetchCustomers}>
<Search className="w-3.5 h-3.5 mr-1.5" />
</Button>
{/* 검색 필터 (DynamicSearchFilter) */}
<DynamicSearchFilter
tableName={CUSTOMER_TABLE}
filterId="c16-customer"
onFilterChange={setSearchFilters}
dataCount={customers.length}
externalFilterConfig={ts.filterConfig}
/>
{/* 액션 버튼 영역 */}
<div className="flex items-center gap-2 px-4 shrink-0">
<div className="flex gap-1.5 ml-auto">
<Button
variant="outline" size="sm" className="h-8" disabled={excelDetecting}
@@ -940,7 +907,7 @@ export default function CustomerManagementPage() {
<Button variant="destructive" size="sm" disabled={!selectedCustomerId} onClick={handleCustomerDelete}>
<Trash2 className="w-3.5 h-3.5 mr-1" />
</Button>
<Button variant="outline" size="sm" onClick={() => setTableSettingsOpen(true)}>
<Button variant="outline" size="sm" onClick={() => ts.setOpen(true)}>
<Settings2 className="w-3.5 h-3.5" />
</Button>
</div>
@@ -949,15 +916,15 @@ export default function CustomerManagementPage() {
{/* 거래처 테이블 */}
<div className="flex-1 overflow-auto">
<Table noWrapper>
<TableHeader className="sticky top-0 bg-muted z-10">
<TableRow>
<TableHead className="w-[40px] text-center text-xs px-2">No</TableHead>
{isColumnVisible("customer_code") && <TableHead className="w-[120px] text-xs"></TableHead>}
{isColumnVisible("customer_name") && <TableHead className="min-w-[160px] text-xs"></TableHead>}
{isColumnVisible("contact_person") && <TableHead className="w-[90px] text-xs"></TableHead>}
{isColumnVisible("contact_phone") && <TableHead className="w-[120px] text-xs"></TableHead>}
{isColumnVisible("division") && <TableHead className="w-[80px] text-xs"></TableHead>}
{isColumnVisible("status") && <TableHead className="w-[70px] text-xs"></TableHead>}
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center px-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
{isColumnVisible("customer_code") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColumnVisible("customer_name") && <TableHead className="min-w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColumnVisible("contact_person") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColumnVisible("contact_phone") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColumnVisible("division") && <TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{isColumnVisible("status") && <TableHead className="w-[70px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
@@ -1085,8 +1052,8 @@ export default function CustomerManagementPage() {
</div>
<div className="flex-1 overflow-auto">
<Table noWrapper>
<TableHeader className="sticky top-0 bg-muted z-10">
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center px-2">
<input
type="checkbox"
@@ -1095,16 +1062,16 @@ export default function CustomerManagementPage() {
onChange={(e) => setPriceCheckedIds(e.target.checked ? priceItems.map((p) => p.id) : [])}
/>
</TableHead>
<TableHead className="w-[100px] text-xs"></TableHead>
<TableHead className="min-w-[100px] text-xs"></TableHead>
<TableHead className="w-[100px] text-xs"></TableHead>
<TableHead className="w-[100px] text-xs"></TableHead>
<TableHead className="w-[80px] text-xs"></TableHead>
<TableHead className="w-[80px] text-xs text-right"></TableHead>
<TableHead className="w-[70px] text-xs"></TableHead>
<TableHead className="w-[60px] text-xs text-right"></TableHead>
<TableHead className="w-[80px] text-xs text-right"></TableHead>
<TableHead className="w-[50px] text-xs"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[60px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1176,8 +1143,8 @@ export default function CustomerManagementPage() {
</div>
<div className="flex-1 overflow-auto">
<Table noWrapper>
<TableHeader className="sticky top-0 bg-muted z-10">
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center px-2">
<input
type="checkbox"
@@ -1186,13 +1153,13 @@ export default function CustomerManagementPage() {
onChange={(e) => setDeliveryCheckedIds(e.target.checked ? deliveryItems.map((d) => d.id) : [])}
/>
</TableHead>
<TableHead className="w-[110px] text-xs"></TableHead>
<TableHead className="min-w-[130px] text-xs"></TableHead>
<TableHead className="min-w-[150px] text-xs"></TableHead>
<TableHead className="w-[80px] text-xs"></TableHead>
<TableHead className="w-[110px] text-xs"></TableHead>
<TableHead className="w-[100px] text-xs"></TableHead>
<TableHead className="w-[50px] text-xs text-center"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[50px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1406,8 +1373,8 @@ export default function CustomerManagementPage() {
</div>
<div className="overflow-auto max-h-[350px] border rounded-lg">
<Table noWrapper>
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center px-2">
<input
type="checkbox"
@@ -1419,11 +1386,11 @@ export default function CustomerManagementPage() {
}}
/>
</TableHead>
<TableHead className="w-[120px] text-xs"></TableHead>
<TableHead className="min-w-[140px] text-xs"></TableHead>
<TableHead className="w-[100px] text-xs"></TableHead>
<TableHead className="w-[100px] text-xs"></TableHead>
<TableHead className="w-[60px] text-xs"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1790,14 +1757,12 @@ export default function CustomerManagementPage() {
{/* 테이블 설정 모달 */}
<TableSettingsModal
open={tableSettingsOpen}
onOpenChange={setTableSettingsOpen}
tableName={CUSTOMER_TABLE}
settingsId="customer-mng"
onSave={(settings) => {
setGridColumns(settings.columns);
setFilterConfig(settings.filters);
}}
open={ts.open}
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
{ConfirmDialogComponent}
@@ -12,7 +12,7 @@ import { Label } from "@/components/ui/label";
import {
Plus, Trash2, Save, Loader2, FileSpreadsheet, Download,
ClipboardList, Pencil, Search, X, Truck, Package,
ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, RotateCcw,
ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight,
Settings2,
} from "lucide-react";
import { cn } from "@/lib/utils";
@@ -25,6 +25,7 @@ import { toast } from "sonner";
import { ShippingPlanBatchModal } from "@/components/common/ShippingPlanBatchModal";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
const DETAIL_TABLE = "sales_order_detail";
const MASTER_TABLE = "sales_order_mng";
@@ -62,12 +63,8 @@ export default function SalesOrderPage() {
const [loading, setLoading] = useState(false);
const [totalCount, setTotalCount] = useState(0);
// 검색 필터 상태
const [searchOrderNo, setSearchOrderNo] = useState("");
const [searchPartnerCode, setSearchPartnerCode] = useState("all");
const [searchDateFrom, setSearchDateFrom] = useState("");
const [searchDateTo, setSearchDateTo] = useState("");
const [searchSellMode, setSearchSellMode] = useState("all");
// 검색 필터 (DynamicSearchFilter에서 관리)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 모달
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -188,12 +185,11 @@ export default function SalesOrderPage() {
const fetchOrders = useCallback(async () => {
setLoading(true);
try {
const filters: any[] = [];
if (searchOrderNo) filters.push({ columnName: "order_no", operator: "contains", value: searchOrderNo });
if (searchPartnerCode && searchPartnerCode !== "all") filters.push({ columnName: "partner_id", operator: "equals", value: searchPartnerCode });
if (searchDateFrom) filters.push({ columnName: "order_date", operator: "gte", value: searchDateFrom });
if (searchDateTo) filters.push({ columnName: "order_date", operator: "lte", value: searchDateTo });
if (searchSellMode && searchSellMode !== "all") filters.push({ columnName: "sell_mode", operator: "equals", value: searchSellMode });
const filters = searchFilters.map(f => ({
columnName: f.columnName,
operator: f.operator,
value: f.value,
}));
const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, {
page: 1, size: 500,
@@ -260,7 +256,7 @@ export default function SalesOrderPage() {
} finally {
setLoading(false);
}
}, [searchOrderNo, searchPartnerCode, searchDateFrom, searchDateTo, searchSellMode, categoryOptions]);
}, [searchFilters]);
useEffect(() => { fetchOrders(); }, [fetchOrders]);
@@ -580,75 +576,14 @@ export default function SalesOrderPage() {
<span className="font-medium text-foreground"></span>
</nav>
{/* 검색 필터 */}
<div className="rounded-lg border border-border bg-card px-5 py-4">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-5 items-end">
{/* 수주번호 */}
<div className="flex flex-col gap-1">
<label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></label>
<Input
value={searchOrderNo}
onChange={(e) => setSearchOrderNo(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && fetchOrders()}
placeholder="SO-2026-0001"
className="h-9"
/>
</div>
{/* 거래처 */}
<div className="flex flex-col gap-1">
<label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></label>
<Select value={searchPartnerCode} onValueChange={setSearchPartnerCode}>
<SelectTrigger className="h-9"><SelectValue placeholder="전체" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{(categoryOptions["partner_id"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 기간 */}
<div className="flex flex-col gap-1">
<label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></label>
<div className="flex items-center gap-1.5">
<Input type="date" value={searchDateFrom} onChange={(e) => setSearchDateFrom(e.target.value)} className="h-9 flex-1" />
<span className="text-xs text-muted-foreground/50">~</span>
<Input type="date" value={searchDateTo} onChange={(e) => setSearchDateTo(e.target.value)} className="h-9 flex-1" />
</div>
</div>
{/* 판매유형 */}
<div className="flex flex-col gap-1">
<label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></label>
<Select value={searchSellMode} onValueChange={setSearchSellMode}>
<SelectTrigger className="h-9"><SelectValue placeholder="전체" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{(categoryOptions["sell_mode"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 조회/초기화 버튼 */}
<div className="flex items-end gap-2">
<Button
variant="ghost" size="sm" className="h-9"
onClick={() => {
setSearchOrderNo("");
setSearchPartnerCode("all");
setSearchDateFrom("");
setSearchDateTo("");
setSearchSellMode("all");
}}
>
<RotateCcw className="w-4 h-4" />
</Button>
<Button size="sm" className="h-9 flex-1" onClick={fetchOrders} disabled={loading}>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Search className="w-4 h-4" />}
</Button>
</div>
</div>
</div>
{/* 검색 필터 (DynamicSearchFilter) */}
<DynamicSearchFilter
tableName={DETAIL_TABLE}
filterId="c16-sales-order"
onFilterChange={setSearchFilters}
dataCount={orders.length}
externalFilterConfig={ts.filterConfig}
/>
{/* 액션 바 */}
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
@@ -702,7 +637,7 @@ export default function SalesOrderPage() {
<div className="overflow-auto" style={{ maxHeight: "calc(100vh - 290px)" }}>
<Table noWrapper className="w-full">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted border-b border-border">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 pl-4">
<input
type="checkbox"
@@ -1024,19 +959,19 @@ export default function SalesOrderPage() {
) : (
<div className="overflow-x-auto rounded-lg border border-border">
<Table noWrapper className="w-full">
<TableHeader>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 text-center text-[11px] font-bold">#</TableHead>
<TableHead className="w-28 text-[11px] font-bold"></TableHead>
<TableHead className="w-32 text-[11px] font-bold"></TableHead>
<TableHead className="w-20 text-[11px] font-bold"></TableHead>
<TableHead className="w-16 text-[11px] font-bold"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold"></TableHead>
<TableHead className="w-24 text-[11px] font-bold"></TableHead>
<TableHead className="w-24 text-[11px] font-bold"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold"></TableHead>
<TableHead className="w-16 text-[11px] font-bold"></TableHead>
<TableHead className="w-36 text-[11px] font-bold"></TableHead>
<TableHead className="w-10 text-center text-[11px] font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground">#</TableHead>
<TableHead className="w-28 text-[11px] font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-16 text-[11px] font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-[11px] font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-[11px] font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-16 text-[11px] font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-36 text-[11px] font-bold text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-10" />
</TableRow>
</TableHeader>
@@ -1157,8 +1092,8 @@ export default function SalesOrderPage() {
</div>
<div className="overflow-auto rounded-lg border border-border" style={{ maxHeight: "320px" }}>
<Table noWrapper>
<TableHeader className="sticky top-0 bg-muted z-10">
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 text-center">
<input
type="checkbox"
@@ -1173,11 +1108,11 @@ export default function SalesOrderPage() {
}}
/>
</TableHead>
<TableHead className="w-32"></TableHead>
<TableHead className="min-w-[150px]"></TableHead>
<TableHead className="w-24"></TableHead>
<TableHead className="w-24"></TableHead>
<TableHead className="w-16"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-16 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1299,6 +1234,7 @@ export default function SalesOrderPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
@@ -26,7 +26,9 @@ import { toast } from "sonner";
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
import { ExcelUploadModal } from "@/components/common/ExcelUploadModal";
import { exportToExcel } from "@/lib/utils/excelExport";
import { TableSettingsModal, TableSettings, loadTableSettings } from "@/components/common/TableSettingsModal";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { useTableSettings } from "@/hooks/useTableSettings";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
const ITEM_TABLE = "item_info";
const MAPPING_TABLE = "customer_item_mapping";
@@ -39,9 +41,21 @@ const formatNum = (val: any): string => {
return isNaN(n) ? String(val) : n.toLocaleString();
};
const ITEM_GRID_COLUMNS = [
{ key: "item_number", label: "품번" },
{ key: "item_name", label: "품명" },
{ key: "size", label: "규격" },
{ key: "unit", label: "단위" },
{ key: "standard_price", label: "기준단가" },
{ key: "selling_price", label: "판매가격" },
{ key: "currency_code", label: "통화" },
{ key: "status", label: "상태" },
];
export default function SalesItemPage() {
const { user } = useAuth();
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
const ts = useTableSettings("c16-sales-item", ITEM_TABLE, ITEM_GRID_COLUMNS);
// 좌측: 품목
const [items, setItems] = useState<any[]>([]);
@@ -49,9 +63,8 @@ export default function SalesItemPage() {
const [itemCount, setItemCount] = useState(0);
const [selectedItemId, setSelectedItemId] = useState<string | null>(null);
// 검색
const [inputKeyword, setInputKeyword] = useState("");
const [searchKeyword, setSearchKeyword] = useState("");
// 검색 필터 (DynamicSearchFilter에서 관리)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 우측: 거래처
const [customerItems, setCustomerItems] = useState<any[]>([]);
@@ -77,9 +90,6 @@ export default function SalesItemPage() {
// 엑셀
const [excelUploadOpen, setExcelUploadOpen] = useState(false);
// 테이블 설정
const [tableSettingsOpen, setTableSettingsOpen] = useState(false);
const [filterConfig, setFilterConfig] = useState<TableSettings["filters"] | undefined>();
// 거래처 상세 입력 모달 (거래처 품번/품명 + 단가)
const [custDetailOpen, setCustDetailOpen] = useState(false);
@@ -93,16 +103,6 @@ export default function SalesItemPage() {
}>>>({});
const [editCustData, setEditCustData] = useState<any>(null);
// 테이블 설정 적용
const applyTableSettings = useCallback((settings: TableSettings) => {
setFilterConfig(settings.filters);
}, []);
// 마운트 시 저장된 설정 복원
useEffect(() => {
const saved = loadTableSettings("sales-item");
if (saved) applyTableSettings(saved);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// 카테고리 로드
useEffect(() => {
@@ -151,8 +151,9 @@ export default function SalesItemPage() {
// 판매품목/영업관리 division 필터 (다중값 컬럼이므로 contains로 매칭)
filters.push({ columnName: "division", operator: "contains", value: "CAT_ML8ZFVEL_1TOR" });
if (searchKeyword) {
filters.push({ columnName: "item_name", operator: "contains", value: searchKeyword });
// DynamicSearchFilter에서 전달된 필터 추가
for (const f of searchFilters) {
filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
}
const res = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, {
@@ -177,14 +178,10 @@ export default function SalesItemPage() {
} finally {
setItemLoading(false);
}
}, [searchKeyword, categoryOptions]); // eslint-disable-line react-hooks/exhaustive-deps
}, [searchFilters, categoryOptions]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { fetchItems(); }, [fetchItems]);
const handleSearch = () => {
setSearchKeyword(inputKeyword);
};
// 선택된 품목
const selectedItem = items.find((i) => i.id === selectedItemId);
@@ -611,33 +608,26 @@ export default function SalesItemPage() {
return (
<div className="flex h-full flex-col gap-3 p-3">
{/* ── 검색 필터 바 ── */}
<div className="flex items-center gap-2 flex-wrap border rounded-lg px-4 py-2.5 bg-card shrink-0">
<div className="flex items-center gap-1.5">
<span className="text-xs font-semibold text-muted-foreground whitespace-nowrap">/</span>
<Input
value={inputKeyword}
onChange={(e) => setInputKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
placeholder="검색어를 입력해주세요"
className="h-8 w-[180px] text-sm"
/>
</div>
<Button size="sm" className="h-8" onClick={handleSearch}>
<Search className="w-3.5 h-3.5" />
</Button>
<div className="flex gap-1.5 ml-auto">
<Button variant="outline" size="sm" className="h-8" onClick={() => setExcelUploadOpen(true)}>
<FileSpreadsheet className="w-3.5 h-3.5" />
</Button>
<Button variant="outline" size="sm" className="h-8" onClick={handleExcelDownload}>
<Download className="w-3.5 h-3.5" />
</Button>
</div>
</div>
{/* 검색 필터 (DynamicSearchFilter) */}
<DynamicSearchFilter
tableName={ITEM_TABLE}
filterId="c16-sales-item"
onFilterChange={setSearchFilters}
dataCount={items.length}
externalFilterConfig={ts.filterConfig}
extraActions={
<div className="flex gap-1.5">
<Button variant="outline" size="sm" className="h-8" onClick={() => setExcelUploadOpen(true)}>
<FileSpreadsheet className="w-3.5 h-3.5" />
</Button>
<Button variant="outline" size="sm" className="h-8" onClick={handleExcelDownload}>
<Download className="w-3.5 h-3.5" />
</Button>
</div>
}
/>
{/* ── 마스터-디테일 분할 패널 ── */}
<div className="flex-1 overflow-hidden border rounded-lg bg-background">
@@ -654,7 +644,7 @@ export default function SalesItemPage() {
{itemCount}
</span>
</div>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => setTableSettingsOpen(true)}>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => ts.setOpen(true)}>
<Settings2 className="w-3.5 h-3.5" />
</Button>
</div>
@@ -670,8 +660,8 @@ export default function SalesItemPage() {
</div>
) : (
<Table noWrapper>
<TableHeader>
<TableRow className="border-b">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground w-[32px] text-center">#</TableHead>
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground w-[110px]"></TableHead>
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground min-w-[130px]"></TableHead>
@@ -789,8 +779,8 @@ export default function SalesItemPage() {
</div>
) : (
<Table noWrapper>
<TableHeader>
<TableRow className="border-b">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="sticky top-0 z-10 bg-card w-[36px] text-center">
<Checkbox
checked={customerItems.length > 0 && customerCheckedIds.length === customerItems.length}
@@ -945,8 +935,8 @@ export default function SalesItemPage() {
{/* 검색 결과 테이블 */}
<div className="overflow-auto max-h-[350px] border rounded-lg">
<Table noWrapper>
<TableHeader>
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="sticky top-0 bg-card w-[40px] text-center">
<Checkbox
checked={custSearchResults.length > 0 && custCheckedIds.size === custSearchResults.length}
@@ -1238,14 +1228,12 @@ export default function SalesItemPage() {
{/* 테이블 설정 모달 */}
<TableSettingsModal
open={tableSettingsOpen}
onOpenChange={setTableSettingsOpen}
tableName={ITEM_TABLE}
settingsId="sales-item"
onSave={(settings) => {
applyTableSettings(settings);
setTableSettingsOpen(false);
}}
open={ts.open}
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
{/* 엑셀 업로드 */}
@@ -10,7 +10,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { Plus, Trash2, RotateCcw, Save, X, ChevronDown, ChevronRight, ChevronLeft, ChevronsLeft, ChevronsRight, Search, Loader2, FileSpreadsheet, Inbox, Settings2 } from "lucide-react";
import { Plus, Trash2, Save, X, ChevronDown, ChevronRight, ChevronLeft, ChevronsLeft, ChevronsRight, Search, Loader2, FileSpreadsheet, Inbox, Settings2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
getShippingOrderList,
@@ -24,6 +24,7 @@ import {
import { ExcelUploadModal } from "@/components/common/ExcelUploadModal";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
const GRID_COLUMNS = [
{ key: "instruction_no", label: "출하지시번호" },
@@ -92,14 +93,8 @@ export default function ShippingOrderPage() {
const [checkedIds, setCheckedIds] = useState<number[]>([]);
const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null);
// 검색
const [searchKeyword, setSearchKeyword] = useState("");
const [searchCustomer, setSearchCustomer] = useState("");
const [debouncedKeyword, setDebouncedKeyword] = useState("");
const [debouncedCustomer, setDebouncedCustomer] = useState("");
const [searchStatus, setSearchStatus] = useState("all");
const [searchDateFrom, setSearchDateFrom] = useState("");
const [searchDateTo, setSearchDateTo] = useState("");
// 검색 필터 (DynamicSearchFilter에서 관리)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 엑셀 업로드
const [excelUploadOpen, setExcelUploadOpen] = useState(false);
@@ -135,36 +130,25 @@ export default function ShippingOrderPage() {
const [sourcePageSize, setSourcePageSize] = useState(20);
const [sourceTotalCount, setSourceTotalCount] = useState(0);
// 텍스트 입력 debounce (500ms)
useEffect(() => {
const t = setTimeout(() => setDebouncedKeyword(searchKeyword), 500);
return () => clearTimeout(t);
}, [searchKeyword]);
useEffect(() => {
const t = setTimeout(() => setDebouncedCustomer(searchCustomer), 500);
return () => clearTimeout(t);
}, [searchCustomer]);
// 초기 날짜
useEffect(() => {
const today = new Date();
const from = new Date(today);
from.setMonth(from.getMonth() - 1);
setSearchDateFrom(from.toISOString().split("T")[0]);
setSearchDateTo(today.toISOString().split("T")[0]);
}, []);
// 데이터 조회
const fetchOrders = useCallback(async () => {
setLoading(true);
try {
const params: any = {};
if (searchDateFrom) params.dateFrom = searchDateFrom;
if (searchDateTo) params.dateTo = searchDateTo;
if (searchStatus !== "all") params.status = searchStatus;
if (debouncedCustomer.trim()) params.customer = debouncedCustomer.trim();
if (debouncedKeyword.trim()) params.keyword = debouncedKeyword.trim();
for (const f of searchFilters) {
if (f.columnName === "ship_date" && f.operator === "between" && f.value) {
const [from, to] = f.value.split(",");
if (from) params.dateFrom = from;
if (to) params.dateTo = to;
} else if (f.columnName === "status") {
params.status = f.value;
} else if (f.columnName === "customer_name") {
params.customer = f.value;
} else {
params.keyword = f.value;
}
}
const result = await getShippingOrderList(params);
if (result.success) setOrders(result.data || []);
@@ -173,10 +157,10 @@ export default function ShippingOrderPage() {
} finally {
setLoading(false);
}
}, [searchDateFrom, searchDateTo, searchStatus, debouncedCustomer, debouncedKeyword]);
}, [searchFilters]);
useEffect(() => {
if (searchDateFrom && searchDateTo) fetchOrders();
fetchOrders();
}, [fetchOrders]);
// 소스 데이터 조회
@@ -217,22 +201,6 @@ export default function ShippingOrderPage() {
}
}, [isModalOpen, dataSource]);
// 핸들러
const handleSearch = () => fetchOrders();
const handleResetSearch = () => {
setSearchKeyword("");
setSearchCustomer("");
setDebouncedKeyword("");
setDebouncedCustomer("");
setSearchStatus("all");
const today = new Date();
const from = new Date(today);
from.setMonth(from.getMonth() - 1);
setSearchDateFrom(from.toISOString().split("T")[0]);
setSearchDateTo(today.toISOString().split("T")[0]);
};
const handleCheckAll = (checked: boolean) => {
setCheckedIds(checked ? orders.map((o: any) => o.id) : []);
};
@@ -439,66 +407,14 @@ export default function ShippingOrderPage() {
<span className="font-semibold text-foreground"></span>
</nav>
{/* 검색 필터 */}
<div className="bg-card border rounded-lg p-3 shrink-0">
<div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-2">
<Label className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap"></Label>
<Input
placeholder="검색"
className="w-[130px] h-9"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
/>
</div>
<div className="flex items-center gap-2">
<Label className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap"></Label>
<Input
placeholder="거래처 검색"
className="w-[130px] h-9"
value={searchCustomer}
onChange={(e) => setSearchCustomer(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
/>
</div>
<div className="flex items-center gap-2">
<Label className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap"></Label>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="w-[100px] h-9"><SelectValue placeholder="전체" /></SelectTrigger>
<SelectContent>
{STATUS_OPTIONS.map(o => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Label className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap"></Label>
<Input
type="date"
value={searchDateFrom}
onChange={(e) => setSearchDateFrom(e.target.value)}
className="w-[130px] h-9"
/>
<span className="text-muted-foreground/40 text-xs">~</span>
<Input
type="date"
value={searchDateTo}
onChange={(e) => setSearchDateTo(e.target.value)}
className="w-[130px] h-9"
/>
</div>
<div className="flex items-center gap-2 ml-auto">
<Button variant="ghost" size="sm" onClick={handleResetSearch}>
<RotateCcw className="w-3.5 h-3.5" />
</Button>
<Button size="sm" onClick={handleSearch} disabled={loading}>
{loading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Search className="w-3.5 h-3.5" />}
</Button>
</div>
</div>
</div>
{/* 검색 필터 (DynamicSearchFilter) */}
<DynamicSearchFilter
tableName={ts.tableName}
filterId="c16-shipping-order"
onFilterChange={setSearchFilters}
dataCount={orders.length}
externalFilterConfig={ts.filterConfig}
/>
{/* 액션 바 */}
<div className="flex items-center justify-between flex-wrap gap-3 shrink-0">
@@ -545,26 +461,26 @@ export default function ShippingOrderPage() {
</div>
) : (
<Table>
<TableHeader className="sticky top-0 bg-muted/30 z-10">
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center">
<Checkbox
checked={orders.length > 0 && checkedIds.length === orders.length}
onCheckedChange={handleCheckAll}
/>
</TableHead>
{ts.isVisible("instruction_no") && <TableHead className="w-[140px]"></TableHead>}
{ts.isVisible("ship_date") && <TableHead className="w-[100px] text-center"></TableHead>}
{ts.isVisible("customer_name") && <TableHead className="w-[120px]"></TableHead>}
{ts.isVisible("transport_company") && <TableHead className="w-[100px]"></TableHead>}
{ts.isVisible("vehicle_no") && <TableHead className="w-[90px]"></TableHead>}
{ts.isVisible("driver_name") && <TableHead className="w-[80px]"></TableHead>}
{ts.isVisible("status") && <TableHead className="w-[80px] text-center"></TableHead>}
{ts.isVisible("item_code") && <TableHead className="w-[100px]"></TableHead>}
{ts.isVisible("item_name") && <TableHead className="w-[130px]"></TableHead>}
{ts.isVisible("qty") && <TableHead className="w-[70px] text-right"></TableHead>}
{ts.isVisible("source_type") && <TableHead className="w-[80px] text-center"></TableHead>}
{ts.isVisible("remark") && <TableHead></TableHead>}
{ts.isVisible("instruction_no") && <TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("ship_date") && <TableHead className="w-[100px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("customer_name") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("transport_company") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("vehicle_no") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("driver_name") && <TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("status") && <TableHead className="w-[80px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("item_code") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("item_name") && <TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("qty") && <TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("source_type") && <TableHead className="w-[80px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("remark") && <TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
@@ -742,15 +658,15 @@ export default function ShippingOrderPage() {
</div>
) : (
<Table>
<TableHeader className="sticky top-0 bg-muted/30 z-10">
<TableRow>
<TableHead className="w-[40px] text-center"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[70px] text-right"></TableHead>
{dataSource === "shipmentPlan" && <TableHead className="w-[70px] text-center"></TableHead>}
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
{dataSource === "shipmentPlan" && <TableHead className="w-[70px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
@@ -951,14 +867,14 @@ export default function ShippingOrderPage() {
</div>
) : (
<Table>
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow>
<TableHead className="w-[40px] text-center"></TableHead>
<TableHead className="w-[90px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[90px] text-center"></TableHead>
<TableHead className="w-[70px] text-right"></TableHead>
<TableHead className="w-[40px] text-center"></TableHead>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[40px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1039,6 +955,7 @@ export default function ShippingOrderPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
@@ -3,13 +3,12 @@
import React, { useState, useMemo, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { Search, X, Save, RotateCcw, Loader2, Inbox, Settings2 } from "lucide-react";
import { X, Save, Loader2, Inbox, Settings2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
getShipmentPlanList,
@@ -18,6 +17,7 @@ import {
} from "@/lib/api/shipping";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
const GRID_COLUMNS = [
{ key: "order_no", label: "수주번호" },
@@ -65,12 +65,8 @@ export default function ShippingPlanPage() {
const [selectedId, setSelectedId] = useState<number | null>(null);
const [checkedIds, setCheckedIds] = useState<number[]>([]);
// 검색
const [searchDateFrom, setSearchDateFrom] = useState("");
const [searchDateTo, setSearchDateTo] = useState("");
const [searchStatus, setSearchStatus] = useState("all");
const [searchCustomer, setSearchCustomer] = useState("");
const [searchKeyword, setSearchKeyword] = useState("");
// 검색 필터 (DynamicSearchFilter에서 관리)
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
// 상세 패널 편집
const [editPlanQty, setEditPlanQty] = useState("");
@@ -79,27 +75,25 @@ export default function ShippingPlanPage() {
const [isDetailChanged, setIsDetailChanged] = useState(false);
const [saving, setSaving] = useState(false);
// 날짜 초기화
useEffect(() => {
const today = new Date();
const threeMonthsAgo = new Date(today);
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
const oneMonthLater = new Date(today);
oneMonthLater.setMonth(oneMonthLater.getMonth() + 1);
setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]);
setSearchDateTo(oneMonthLater.toISOString().split("T")[0]);
}, []);
// 데이터 조회
const fetchData = useCallback(async () => {
setLoading(true);
try {
const params: any = {};
if (searchDateFrom) params.dateFrom = searchDateFrom;
if (searchDateTo) params.dateTo = searchDateTo;
if (searchStatus !== "all") params.status = searchStatus;
if (searchCustomer.trim()) params.customer = searchCustomer.trim();
if (searchKeyword.trim()) params.keyword = searchKeyword.trim();
for (const f of searchFilters) {
if (f.columnName === "plan_date" && f.operator === "between" && f.value) {
const [from, to] = f.value.split(",");
if (from) params.dateFrom = from;
if (to) params.dateTo = to;
} else if (f.columnName === "status") {
params.status = f.value;
} else if (f.columnName === "customer_name") {
params.customer = f.value;
} else {
params.keyword = f.value;
}
}
const result = await getShipmentPlanList(params);
if (result.success) {
@@ -110,29 +104,12 @@ export default function ShippingPlanPage() {
} finally {
setLoading(false);
}
}, [searchDateFrom, searchDateTo, searchStatus, searchCustomer, searchKeyword]);
}, [searchFilters]);
// 초기 로드 + 검색 시 자동 조회
// searchFilters 변경 시 자동 조회
useEffect(() => {
if (searchDateFrom && searchDateTo) {
fetchData();
}
}, [searchDateFrom, searchDateTo]);
const handleSearch = () => fetchData();
const handleResetSearch = () => {
setSearchStatus("all");
setSearchCustomer("");
setSearchKeyword("");
const today = new Date();
const threeMonthsAgo = new Date(today);
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
const oneMonthLater = new Date(today);
oneMonthLater.setMonth(oneMonthLater.getMonth() + 1);
setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]);
setSearchDateTo(oneMonthLater.toISOString().split("T")[0]);
};
fetchData();
}, [fetchData]);
const selectedPlan = useMemo(() => data.find(p => p.id === selectedId), [data, selectedId]);
@@ -225,74 +202,14 @@ export default function ShippingPlanPage() {
<span className="font-semibold text-foreground"></span>
</nav>
{/* 검색 필터 */}
<div className="bg-card border rounded-lg p-3 shrink-0">
<div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-2">
<Label className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap"></Label>
<Input
type="date"
value={searchDateFrom}
onChange={(e) => setSearchDateFrom(e.target.value)}
className="w-[130px] h-9"
/>
<span className="text-muted-foreground/40 text-xs">~</span>
<Input
type="date"
value={searchDateTo}
onChange={(e) => setSearchDateTo(e.target.value)}
className="w-[130px] h-9"
/>
</div>
<div className="flex items-center gap-2">
<Label className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap"></Label>
<Select value={searchStatus} onValueChange={setSearchStatus}>
<SelectTrigger className="w-[100px] h-9">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
{STATUS_OPTIONS.map(o => (
<SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Label className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap"></Label>
<Input
placeholder="거래처 검색"
className="w-[130px] h-9"
value={searchCustomer}
onChange={(e) => setSearchCustomer(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
/>
</div>
<div className="flex items-center gap-2">
<Label className="text-[11px] font-semibold text-muted-foreground whitespace-nowrap">/</Label>
<Input
placeholder="수주번호 / 품목 검색"
className="w-[180px] h-9"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
/>
</div>
<div className="flex items-center gap-2 ml-auto">
<Button variant="ghost" size="sm" onClick={handleResetSearch}>
<RotateCcw className="w-3.5 h-3.5" />
</Button>
<Button size="sm" onClick={handleSearch} disabled={loading}>
{loading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Search className="w-3.5 h-3.5" />}
</Button>
</div>
</div>
</div>
{/* 검색 필터 (DynamicSearchFilter) */}
<DynamicSearchFilter
tableName="shipment_plan"
filterId="c16-shipping-plan"
onFilterChange={setSearchFilters}
dataCount={data.length}
externalFilterConfig={ts.filterConfig}
/>
{/* 마스터-디테일 */}
<div className="flex-1 overflow-hidden rounded-lg border bg-card">
@@ -317,23 +234,23 @@ export default function ShippingPlanPage() {
{/* 테이블 */}
<div className="flex-1 overflow-auto">
<Table>
<TableHeader className="sticky top-0 bg-muted/30 z-10">
<TableRow>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[40px] text-center">
<Checkbox
checked={data.length > 0 && checkedIds.length === data.filter(p => p.status !== "CANCELLED").length}
onCheckedChange={handleCheckAll}
/>
</TableHead>
{ts.isVisible("order_no") && <TableHead className="w-[10%]"></TableHead>}
{ts.isVisible("due_date") && <TableHead className="w-[8%] text-center"></TableHead>}
{ts.isVisible("customer_name") && <TableHead className="w-[12%]"></TableHead>}
{ts.isVisible("part_code") && <TableHead className="w-[18%]"></TableHead>}
{ts.isVisible("part_name") && <TableHead className="w-[18%]"></TableHead>}
{ts.isVisible("order_qty") && <TableHead className="w-[7%] text-right"></TableHead>}
{ts.isVisible("plan_qty") && <TableHead className="w-[7%] text-right"></TableHead>}
{ts.isVisible("plan_date") && <TableHead className="w-[8%] text-center"></TableHead>}
{ts.isVisible("status") && <TableHead className="w-[6%] text-center"></TableHead>}
{ts.isVisible("order_no") && <TableHead className="w-[10%] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("due_date") && <TableHead className="w-[8%] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("customer_name") && <TableHead className="w-[12%] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("part_code") && <TableHead className="w-[18%] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("part_name") && <TableHead className="w-[18%] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("order_qty") && <TableHead className="w-[7%] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("plan_qty") && <TableHead className="w-[7%] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("plan_date") && <TableHead className="w-[8%] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
{ts.isVisible("status") && <TableHead className="w-[6%] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
@@ -582,6 +499,7 @@ export default function ShippingPlanPage() {
onOpenChange={ts.setOpen}
tableName={ts.tableName}
settingsId={ts.settingsId}
defaultVisibleKeys={ts.defaultVisibleKeys}
onSave={ts.applySettings}
/>
</div>
+22 -4
View File
@@ -508,10 +508,10 @@ select {
font-family: "Gaegu", cursive;
}
/* ===== 전체 폰트 사이즈 16px 통일 (버튼 제외) ===== */
/* ===== 전체 폰트 사이즈 16px 통일 (버튼 제외) 작업자 김주석 =====
body *:not(button, [role="button"], .kpi-dynamic-font) {
font-size: 16px !important;
}
} */
body button *,
body [role="button"] * {
@@ -782,14 +782,32 @@ body [role="button"] * {
line-height: 1.5 !important;
}
/* 헤더 패딩 맞춤 */
/* 헤더 */
[data-slot="table-head"] {
padding: 12px 16px !important;
font-size: 12px !important;
font-size: 14px !important;
font-weight: 700 !important;
letter-spacing: 0.03em !important;
}
/* 다크모드 체크박스 — input[type=checkbox] + shadcn Checkbox 모두 커버 */
.dark [data-slot="table-cell"] input[type="checkbox"],
.dark [data-slot="table-head"] input[type="checkbox"] {
background-color: transparent !important;
border: 2px solid hsl(0 0% 85%) !important;
accent-color: hsl(var(--primary)) !important;
}
.dark [data-slot="table-cell"] button[role="checkbox"],
.dark [data-slot="table-head"] button[role="checkbox"] {
border: 2px solid hsl(0 0% 85%) !important;
background-color: transparent !important;
}
.dark [data-slot="table-cell"] button[role="checkbox"][data-state="checked"],
.dark [data-slot="table-head"] button[role="checkbox"][data-state="checked"] {
border-color: hsl(var(--primary)) !important;
background-color: hsl(var(--primary)) !important;
}
/* 짝수 행 stripe — 행 구분 */
[data-slot="table-body"] [data-slot="table-row"]:nth-child(even) {
background-color: hsl(var(--muted) / 0.35);
@@ -76,6 +76,8 @@ export interface TableSettingsModalProps {
onSave?: (settings: TableSettings) => void;
/** 초기 탭 */
initialTab?: "columns" | "filters" | "groups";
/** 기본 표시 컬럼 키 목록 (GRID_COLUMNS 기준). 미지정 시 전체 표시 */
defaultVisibleKeys?: string[];
}
// ===== 상수 =====
@@ -204,6 +206,7 @@ export function TableSettingsModal({
settingsId,
onSave,
initialTab = "columns",
defaultVisibleKeys,
}: TableSettingsModalProps) {
const [activeTab, setActiveTab] = useState(initialTab);
const [loading, setLoading] = useState(false);
@@ -241,7 +244,7 @@ export function TableSettingsModal({
.map((t) => ({
columnName: t.columnName,
displayName: t.displayName || t.columnLabel || t.columnName,
visible: true,
visible: defaultVisibleKeys ? defaultVisibleKeys.includes(t.columnName) : true,
width: 120,
}));
@@ -116,22 +116,10 @@ const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
"/COMPANY_7/design/my-work": dynamic(() => import("@/app/(main)/COMPANY_7/design/my-work/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/design/design-request": dynamic(() => import("@/app/(main)/COMPANY_7/design/design-request/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/design/task-management": dynamic(() => import("@/app/(main)/COMPANY_7/design/task-management/page"), { ssr: false, loading: LoadingFallback }),
// === COMPANY_7 신규 개발 화면 ===
"/COMPANY_7/purchase/order": dynamic(() => import("@/app/(main)/COMPANY_7/purchase/order/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/purchase/purchase-item": dynamic(() => import("@/app/(main)/COMPANY_7/purchase/purchase-item/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/purchase/supplier": dynamic(() => import("@/app/(main)/COMPANY_7/purchase/supplier/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/logistics/inventory": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/inventory/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/logistics/warehouse": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/warehouse/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/logistics/info": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/info/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/quality/inspection": dynamic(() => import("@/app/(main)/COMPANY_7/quality/inspection/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/quality/item-inspection": dynamic(() => import("@/app/(main)/COMPANY_7/quality/item-inspection/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/equipment/plc-settings": dynamic(() => import("@/app/(main)/COMPANY_7/equipment/plc-settings/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/master-data/company": dynamic(() => import("@/app/(main)/COMPANY_7/master-data/company/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/mold/info": dynamic(() => import("@/app/(main)/COMPANY_7/mold/info/page"), { ssr: false, loading: LoadingFallback }),
// === COMPANY_16 (하이큐마그) ===
"/COMPANY_16/master-data/options": dynamic(() => import("@/app/(main)/COMPANY_16/master-data/options/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_16/master-data/item-info": dynamic(() => import("@/app/(main)/COMPANY_16/master-data/item-info/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_16/master-data/department": dynamic(() => import("@/app/(main)/COMPANY_16/master-data/department/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_16/master-data/options": dynamic(() => import("@/app/(main)/COMPANY_16/master-data/options/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_16/master-data/company": dynamic(() => import("@/app/(main)/COMPANY_16/master-data/company/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_16/sales/order": dynamic(() => import("@/app/(main)/COMPANY_16/sales/order/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_16/sales/customer": dynamic(() => import("@/app/(main)/COMPANY_16/sales/customer/page"), { ssr: false, loading: LoadingFallback }),
@@ -439,6 +427,9 @@ const COMPANY_PAGE_PREFIXES = [
"/logistics/",
"/outsourcing/",
"/design/",
"/purchase/",
"/quality/",
"/mold/",
];
function isCompanyPage(url: string): boolean {
+153
View File
@@ -0,0 +1,153 @@
"use client";
/**
* useTableSettings
*
* TableSettingsModal과 /, , .
* localStorage에 /.
*
* @example
* const ts = useTableSettings("item-info", TABLE_NAME, GRID_COLUMNS);
*
* // 툴바 버튼
* <Button variant="ghost" size="sm" onClick={() => ts.setOpen(true)}>
* <Settings2 className="h-4 w-4" />
* </Button>
*
* // 테이블 헤더 — GRID_COLUMNS 대신 ts.visibleColumns 사용
* {ts.visibleColumns.map(col => <TableHead key={col.key}>{col.label}</TableHead>)}
*
* // 모달 (JSX 하단)
* <TableSettingsModal
* open={ts.open}
* onOpenChange={ts.setOpen}
* tableName={ts.tableName}
* settingsId={ts.settingsId}
* onSave={ts.applySettings}
* />
*/
import { useState, useEffect, useCallback, useMemo } from "react";
import { loadTableSettings, type TableSettings } from "@/components/common/TableSettingsModal";
export function useTableSettings<T extends { key: string }>(
settingsId: string,
tableName: string,
defaultColumns: T[],
) {
const [open, setOpen] = useState(false);
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(
() => new Set(defaultColumns.map((c) => c.key)),
);
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
const [orderedKeys, setOrderedKeys] = useState<string[]>(
() => defaultColumns.map((c) => c.key),
);
// 초기 filterConfig: GRID_COLUMNS에 있는 컬럼만 필터 가능 (전부 비활성)
const [filterConfig, setFilterConfig] = useState<TableSettings["filters"]>(
() =>
defaultColumns.map((c) => ({
columnName: c.key,
displayName: (c as any).label || c.key,
enabled: false,
filterType: "text" as const,
width: 25,
})),
);
/** TableSettingsModal onSave에 전달할 콜백 */
const applySettings = useCallback(
(settings: TableSettings) => {
const visible = new Set<string>();
const widths: Record<string, number> = {};
const order: string[] = [];
for (const cs of settings.columns) {
if (cs.visible) {
visible.add(cs.columnName);
widths[cs.columnName] = cs.width;
order.push(cs.columnName);
}
}
// settings에 없는 새 컬럼은 보이도록 추가
for (const col of defaultColumns) {
if (!settings.columns.find((c) => c.columnName === col.key)) {
visible.add(col.key);
order.push(col.key);
}
}
setVisibleKeys(visible);
setColumnWidths(widths);
setOrderedKeys(order);
// 화면에 표시된 컬럼만 필터 가능하도록 제한
setFilterConfig(
settings.filters?.filter((f) => visible.has(f.columnName)),
);
},
[defaultColumns],
);
// 마운트 시 저장된 설정 복원
useEffect(() => {
const saved = loadTableSettings(settingsId);
if (saved) applySettings(saved);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
/** 설정이 적용된 컬럼 목록 (순서 + 표시 필터 적용) */
const visibleColumns = useMemo((): T[] => {
const colMap = new Map(defaultColumns.map((c) => [c.key, c]));
const result: T[] = [];
// 저장된 순서대로
for (const key of orderedKeys) {
if (visibleKeys.has(key)) {
const col = colMap.get(key);
if (col) result.push(col);
}
}
// orderedKeys에 없는 컬럼 (새로 추가된 것)
for (const col of defaultColumns) {
if (!orderedKeys.includes(col.key) && visibleKeys.has(col.key)) {
result.push(col);
}
}
return result.length > 0 ? result : defaultColumns;
}, [defaultColumns, orderedKeys, visibleKeys]);
/** 컬럼 표시 여부 확인 */
const isVisible = useCallback((key: string) => visibleKeys.has(key), [visibleKeys]);
/** 컬럼 너비 가져오기 (설정값 or undefined) */
const getWidth = useCallback(
(key: string): number | undefined => columnWidths[key],
[columnWidths],
);
return {
/** 모달 open 상태 */
open,
/** 모달 open 상태 setter */
setOpen,
/** web-types API 호출용 테이블명 */
tableName,
/** localStorage 키 */
settingsId,
/** TableSettingsModal onSave 콜백 */
applySettings,
/** 설정 적용된 컬럼 배열 (순서 + 표시 필터) */
visibleColumns,
/** 특정 컬럼 표시 여부 */
isVisible,
/** 특정 컬럼 너비 (px) */
getWidth,
/** 필터 설정 */
filterConfig,
/** GRID_COLUMNS 기본 컬럼 키 목록 (TableSettingsModal defaultVisibleKeys용) */
defaultVisibleKeys: defaultColumns.map((c) => c.key),
};
}