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:
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user