diff --git a/frontend/app/(main)/COMPANY_10/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_10/logistics/outbound/page.tsx index 824e2e86..457e2723 100644 --- a/frontend/app/(main)/COMPANY_10/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_10/logistics/outbound/page.tsx @@ -38,7 +38,6 @@ import { Save, ChevronRight, ChevronLeft, - ChevronDown, ChevronsLeft, ChevronsRight, Inbox, @@ -116,41 +115,8 @@ const GRID_COLUMNS = [ { key: "remark", label: "비고" }, ]; -// 마스터 헤더 레이아웃 (출고번호 뒤) -const MASTER_BODY_LAYOUT = [ - { key: "outbound_type", label: "출고유형", colSpan: 1 }, - { key: "outbound_date", label: "출고일", colSpan: 1 }, - { key: "reference_number", label: "참조번호", colSpan: 1 }, - { key: "customer_name", label: "거래처", colSpan: 1 }, - { key: "warehouse_name", label: "창고", colSpan: 1 }, - { key: "outbound_status", label: "출고상태", colSpan: 1 }, - { key: "memo", label: "비고", colSpan: 1 }, -]; - -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "source_type", label: "출처" }, - { key: "item_code", label: "품목코드" }, - { key: "item_name", label: "품목명" }, - { key: "specification", label: "규격" }, - { key: "outbound_qty", label: "출고수량" }, - { key: "unit_price", label: "단가" }, - { key: "total_amount", label: "금액" }, -]; - -// 마스터 필드 키 목록 (필터 분류용) -const MASTER_KEYS = new Set(["outbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); - -// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) -const DETAIL_KEY_MAP: Record = { - source_type: "source_type", - item_number: "item_code", - item_name: "item_name", - spec: "specification", - outbound_qty: "outbound_qty", - unit_price: "unit_price", - total_amount: "total_amount", -}; +// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16 +const TOTAL_COLS = 16; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -258,30 +224,6 @@ interface SelectedSourceItem { export default function OutboundPage() { const ts = useTableSettings("c16-outbound", "outbound_mng", GRID_COLUMNS); - // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 - const visibleMasterLayout = useMemo(() => { - const ordered: typeof MASTER_BODY_LAYOUT = []; - for (const vc of ts.visibleColumns) { - const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); - if (m) ordered.push(m); - } - return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; - }, [ts.visibleColumns]); - - const visibleDetailCols = useMemo(() => { - const ordered: typeof DETAIL_HEADER_COLS = []; - for (const vc of ts.visibleColumns) { - const detailKey = DETAIL_KEY_MAP[vc.key]; - if (detailKey) { - const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); - if (d) ordered.push(d); - } - } - return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; - }, [ts.visibleColumns]); - - const TOTAL_COLS = 3 + visibleMasterLayout.length; - // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -290,9 +232,7 @@ export default function OutboundPage() { // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); - // 마스터-디테일 그룹 테이블 state - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); + // 헤더 필터 & 정렬 const [headerFilters, setHeaderFilters] = useState>>({}); const [sortState, setSortState] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -403,121 +343,59 @@ export default function OutboundPage() { })(); }, []); - // --- 마스터-디테일 그룹핑, 필터, 정렬 --- + // 플랫 행 생성 (출고유형 코드→라벨 변환, source_type→한글 등) + const flatRows = useMemo(() => { + return data.map((row) => ({ + ...row, + _raw_outbound_type: row.outbound_type, + outbound_type: resolveCat("outbound_type", row.outbound_type) || row.outbound_type || "", + source_type: row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "", + item_number: row.item_code || (row as any).item_number || "", + spec: row.specification || (row as any).spec || "", + })); + }, [data, resolveCat]); - // outbound_number 기준 그룹핑 + 필터 + 정렬 - const filteredGroups = useMemo(() => { - // 1차: outbound_number 기준 그룹핑 - const allGroups: Record = {}; - for (const row of data) { - const key = row.outbound_number || "_no_number"; - if (!allGroups[key]) { - allGroups[key] = { master: row, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - const raw = (group.master as any)?.[colKey] ?? ""; - return values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([outNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - let cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; - if (colKey === "source_type") cellVal = SOURCE_TYPE_LABEL[cellVal] || cellVal; - return values.has(cellVal); - }) - ); - return [outNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - entries.sort(([, a], [, b]) => { - const av = (a.master as any)?.[key] ?? ""; - const bv = (b.master as any)?.[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - let av: any = (a as any)[key] ?? ""; - let bv: any = (b as any)[key] ?? ""; - if (key === "source_type") { av = SOURCE_TYPE_LABEL[av] || av; bv = SOURCE_TYPE_LABEL[bv] || bv; } - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [data, headerFilters, sortState]); - - // 마스터 컬럼별 고유값 (헤더 필터용) - const masterUniqueValues = useMemo(() => { + // 컬럼별 고유값 (헤더 필터용) + const columnUniqueValues = useMemo(() => { const result: Record = {}; - const seenMasters = new Map(); - data.forEach((row) => { - if (row.outbound_number && !seenMasters.has(row.outbound_number)) { - seenMasters.set(row.outbound_number, row); - } - }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "outbound_number", label: "출고번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label }))]) { + for (const col of GRID_COLUMNS) { const values = new Set(); - masters.forEach((m) => { - const val = (m as any)?.[col.key]; + flatRows.forEach((row) => { + const val = (row as any)[col.key]; if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [data]); + }, [flatRows]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) - const columnUniqueValues = useMemo(() => { - const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { - const values = new Set(); - data.forEach((row) => { - let val = (row as any)[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "source_type") val = SOURCE_TYPE_LABEL[val] || val; - values.add(String(val)); - } + // 필터 + 정렬 적용된 플랫 데이터 + const filteredRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; + return values.has(cellVal); }); - result[col.key] = Array.from(values).sort(); } - return result; - }, [data]); + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = (a as any)[key] ?? ""; + const bv = (b as any)[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -938,7 +816,7 @@ export default function OutboundPage() { dataCount={data.length} /> - {/* 출고 목록 테이블 */} + {/* 출고 목록 테이블 (플랫 리스트) */}
{/* 패널 헤더 */}
@@ -946,7 +824,7 @@ export default function OutboundPage() { 출고 목록 - {data.length}건 + {filteredRows.length}건
@@ -971,78 +849,68 @@ export default function OutboundPage() {
- +
- - {visibleMasterLayout.map((col) => ( - - ))} + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 출고번호 */} - -
-
handleSort("outbound_number")}> - 출고번호 - {sortState?.key === "outbound_number" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["outbound_number"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {GRID_COLUMNS.map((col) => { + const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} + + ); + })} @@ -1052,7 +920,7 @@ export default function OutboundPage() { - ) : Object.keys(filteredGroups).length === 0 ? ( + ) : filteredRows.length === 0 ? (
@@ -1062,214 +930,59 @@ export default function OutboundPage() { ) : ( - Object.entries(filteredGroups).map(([outboundNo, group]) => { - const isExpanded = expandedOrders.has(outboundNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 */} - { - if (expandedOrders.has(outboundNo)) { - setClosingOrders((prev) => new Set(prev).add(outboundNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(outboundNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(outboundNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(outboundNo)); - } - }} - onDoubleClick={() => openEditModal(group.details[0])} - > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 출고번호 */} - - {outboundNo} - ({group.details.length}) - - {/* 마스터 필드 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => { - switch (col.key) { - case "outbound_type": return ( - - - {resolveCat("outbound_type", master.outbound_type) || "-"} - - - ); - case "outbound_date": return ( - - {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} - - ); - case "reference_number": return ( - - {master.reference_number || ""} - - ); - case "customer_name": return ( - - {master.customer_name || ""} - - ); - case "warehouse_name": return ( - - {master.warehouse_name || master.warehouse_code || ""} - - ); - case "outbound_status": return ( - - - {master.outbound_status || "-"} - - - ); - case "memo": return ( - - {master.memo || ""} - - ); - default: return {(master as any)[col.key] ?? ""}; - } - })} - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - - {visibleDetailCols.map((col) => { - const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => { - let v = (d as any)[col.key]; - if (col.key === "source_type") v = SOURCE_TYPE_LABEL[v] || v; - return v; - }).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} -
+ { - const isClosing = closingOrders.has(outboundNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - onDoubleClick={() => openEditModal(row)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - - {visibleDetailCols.map((col) => { - switch (col.key) { - case "source_type": return {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"}; - case "item_code": return {row.item_code || ""}; - case "item_name": return {row.item_name || ""}; - case "specification": return {row.specification || ""}; - case "outbound_qty": return {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}; - case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; - case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; - default: return {(row as any)[col.key] ?? ""}; - } - })} - + onClick={() => { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] ); - })} - + }} + onDoubleClick={() => openEditModal(row as any)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + > + {}} /> + + {row.outbound_number || ""} + + + {row.outbound_type || "-"} + + + + {row.outbound_date ? new Date(row.outbound_date).toLocaleDateString("ko-KR") : ""} + + {row.reference_number || ""} + {row.source_type || ""} + {row.customer_name || ""} + {row.item_number || ""} + {row.item_name || ""} + {row.spec || ""} + {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {row.warehouse_name || row.warehouse_code || ""} + + + {row.outbound_status || "-"} + + + {row.remark || row.memo || ""} + ); }) )} diff --git a/frontend/app/(main)/COMPANY_10/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_10/logistics/receiving/page.tsx index ac25af56..77d21e7b 100644 --- a/frontend/app/(main)/COMPANY_10/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_10/logistics/receiving/page.tsx @@ -43,7 +43,6 @@ import { X, Save, ChevronRight, - ChevronDown, ChevronLeft, ChevronsLeft, ChevronsRight, @@ -64,6 +63,7 @@ import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSea import { getReceivingList, createReceiving, + updateReceiving, deleteReceiving, generateReceivingNumber, getReceivingWarehouses, @@ -95,41 +95,8 @@ const GRID_COLUMNS = [ { key: "remark", label: "비고" }, ]; -// 마스터 헤더 레이아웃 (입고번호 뒤, 디테일 7컬럼 위에 colSpan으로 맵핑) -const MASTER_BODY_LAYOUT = [ - { key: "inbound_type", label: "입고유형", colSpan: 1 }, - { key: "inbound_date", label: "입고일", colSpan: 1 }, - { key: "reference_number", label: "참조번호", colSpan: 1 }, - { key: "supplier_name", label: "공급처", colSpan: 1 }, - { key: "warehouse_name", label: "창고", colSpan: 1 }, - { key: "inbound_status", label: "입고상태", colSpan: 1 }, - { key: "memo", label: "비고", colSpan: 1 }, -]; - -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "source_table", label: "출처" }, - { key: "item_number", label: "품목코드" }, - { key: "item_name", label: "품목명" }, - { key: "spec", label: "규격" }, - { key: "inbound_qty", label: "입고수량" }, - { key: "unit_price", label: "단가" }, - { key: "total_amount", label: "금액" }, -]; - -// 마스터 필드 키 목록 (필터 분류용) -const MASTER_KEYS = new Set(["inbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); - -// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) -const DETAIL_KEY_MAP: Record = { - source_type: "source_table", - item_number: "item_number", - item_name: "item_name", - spec: "spec", - inbound_qty: "inbound_qty", - unit_price: "unit_price", - total_amount: "total_amount", -}; +// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16 +const TOTAL_COLS = 16; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -287,30 +254,6 @@ interface SelectedSourceItem { export default function ReceivingPage() { const ts = useTableSettings("c16-receiving", "inbound_mng", GRID_COLUMNS); - // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 - const visibleMasterLayout = useMemo(() => { - const ordered: typeof MASTER_BODY_LAYOUT = []; - for (const vc of ts.visibleColumns) { - const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); - if (m) ordered.push(m); - } - return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; - }, [ts.visibleColumns]); - - const visibleDetailCols = useMemo(() => { - const ordered: typeof DETAIL_HEADER_COLS = []; - for (const vc of ts.visibleColumns) { - const detailKey = DETAIL_KEY_MAP[vc.key]; - if (detailKey) { - const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); - if (d) ordered.push(d); - } - } - return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; - }, [ts.visibleColumns]); - - const TOTAL_COLS = 3 + visibleMasterLayout.length; - // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -319,9 +262,7 @@ export default function ReceivingPage() { // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); - // 마스터-디테일 그룹 테이블 - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); + // 헤더 필터 & 정렬 const [headerFilters, setHeaderFilters] = useState>>({}); const [sortState, setSortState] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -338,6 +279,10 @@ export default function ReceivingPage() { const [selectedItems, setSelectedItems] = useState([]); const [saving, setSaving] = useState(false); + // 수정 모드 + const [editMode, setEditMode] = useState(false); + const [editItemIds, setEditItemIds] = useState([]); + // 소스 데이터 const [sourceKeyword, setSourceKeyword] = useState(""); const [sourceLoading, setSourceLoading] = useState(false); @@ -377,7 +322,7 @@ export default function ReceivingPage() { Promise.all( ["material", "unit"].map(async (col) => { try { - const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_10`); + const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`); const items = flatten(res.data?.data || []); map[col] = {}; for (const item of items) map[col][item.code] = item.label; @@ -434,124 +379,56 @@ export default function ReceivingPage() { })(); }, []); - // 필터 + 정렬 적용된 데이터 -> 그룹핑 - const filteredGroups = useMemo(() => { - // 1차: inbound_number 기준 그룹핑 - const allGroups: Record = {}; - for (const row of data) { - const key = row.inbound_number || "_no_inbound"; - if (!allGroups[key]) { - allGroups[key] = { master: row, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - let raw = (group.master as any)?.[colKey] ?? ""; - // 입고유형은 코드→라벨 변환된 값으로 비교 - if (colKey === "inbound_type") raw = resolveInboundType(String(raw)); - return values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([inboundNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - let cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; - if (colKey === "source_table") cellVal = SOURCE_TABLE_LABEL[cellVal] || cellVal; - return values.has(cellVal); - }) - ); - return [inboundNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - entries.sort(([, a], [, b]) => { - let av: any = (a.master as any)?.[key] ?? ""; - let bv: any = (b.master as any)?.[key] ?? ""; - if (key === "inbound_type") { av = resolveInboundType(String(av)); bv = resolveInboundType(String(bv)); } - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - const av = (a as any)[key] ?? ""; - const bv = (b as any)[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [data, headerFilters, sortState]); - - // 마스터 컬럼별 고유값 (마스터 헤더 필터용) - const masterUniqueValues = useMemo(() => { - const result: Record = {}; - const seenMasters = new Map(); - data.forEach((row) => { - if (row.inbound_number && !seenMasters.has(row.inbound_number)) { - seenMasters.set(row.inbound_number, row); - } - }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "inbound_number", label: "입고번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label }))]) { - const values = new Set(); - masters.forEach((m) => { - let val = (m as any)?.[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "inbound_type") val = resolveInboundType(String(val)); - values.add(String(val)); - } - }); - result[col.key] = Array.from(values).sort(); - } - return result; + // 플랫 행 생성 (입고유형 코드→라벨 변환, source_table→한글 등) + const flatRows = useMemo(() => { + return data.map((row) => ({ + ...row, + inbound_type: resolveInboundType(row.inbound_type), + source_type: row.source_table ? (SOURCE_TABLE_LABEL[row.source_table] || row.source_table) : (row as any).source_type || "", + })); }, [data]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) + // 컬럼별 고유값 (헤더 필터용) const columnUniqueValues = useMemo(() => { const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { + for (const col of GRID_COLUMNS) { const values = new Set(); - data.forEach((row) => { - let val = (row as any)[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "source_table") val = SOURCE_TABLE_LABEL[val] || val; - values.add(String(val)); - } + flatRows.forEach((row) => { + const val = (row as any)[col.key]; + if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [data]); + }, [flatRows]); + + // 필터 + 정렬 적용된 플랫 데이터 + const filteredRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; + return values.has(cellVal); + }); + } + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = (a as any)[key] ?? ""; + const bv = (b as any)[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -633,6 +510,8 @@ export default function ReceivingPage() { const openRegisterModal = async () => { const defaultType = "구매입고"; + setEditMode(false); + setEditItemIds([]); setModalInboundType(defaultType); setModalInboundDate(new Date().toISOString().split("T")[0]); setModalWarehouse(""); @@ -661,6 +540,51 @@ export default function ReceivingPage() { } }; + // 수정 모달 열기 + const openEditModal = (row: InboundItem) => { + const inNo = row.inbound_number; + const grouped = data.filter((d) => d.inbound_number === inNo); + const first = grouped[0] || row; + + setEditMode(true); + setEditItemIds(grouped.map((g) => g.id)); + setModalInboundNo(inNo); + setModalInboundType(first.inbound_type || "구매입고"); + setModalInboundDate(first.inbound_date ? String(first.inbound_date).slice(0, 10) : ""); + setModalWarehouse(first.warehouse_code || ""); + setModalLocation(first.location_code || ""); + setModalInspector((first as any).inspector || ""); + setModalManager((first as any).manager || ""); + setModalMemo(first.memo || ""); + setSelectedItems( + grouped.map((g) => ({ + key: g.id, + inbound_type: g.inbound_type || "", + reference_number: g.reference_number || "", + supplier_code: (g as any).supplier_code || "", + supplier_name: g.supplier_name || "", + item_number: g.item_number || "", + item_name: g.item_name || "", + spec: g.spec || "", + material: (g as any).material || "", + unit: (g as any).unit || "", + inbound_qty: Number(g.inbound_qty) || 0, + unit_price: Number(g.unit_price) || 0, + total_amount: Number(g.total_amount) || 0, + source_table: (g as any).source_table || "", + source_id: (g as any).source_id || "", + })) + ); + setSourceKeyword(""); + setPurchaseOrders([]); + setShipments([]); + setItems([]); + setSourcePage(1); + setSourceTotalCount(0); + setIsModalOpen(true); + loadSourceData(first.inbound_type || "구매입고", undefined, 1); + }; + // 검색 버튼 클릭 시 const searchSourceData = useCallback(async () => { setSourcePage(1); @@ -811,41 +735,97 @@ export default function ReceivingPage() { } setSaving(true); try { - const res = await createReceiving({ - inbound_number: modalInboundNo, - inbound_date: modalInboundDate, - warehouse_code: modalWarehouse, - location_code: modalLocation || undefined, - inspector: modalInspector || undefined, - manager: modalManager || undefined, - memo: modalMemo || undefined, - items: selectedItems.map((item) => ({ - inbound_type: item.inbound_type, - reference_number: item.reference_number, - supplier_code: item.supplier_code, - supplier_name: item.supplier_name, - item_number: item.item_number, - item_name: item.item_name, - spec: item.spec, - material: item.material, - unit: item.unit, - inbound_qty: item.inbound_qty, - unit_price: item.unit_price, - total_amount: item.total_amount, - source_table: item.source_table, - source_id: item.source_id, - inbound_status: "입고완료", - inspection_status: "대기", - })), - }); + if (editMode) { + // 기존 item 수정 + 삭제된 item 삭제 + 새 item 추가 + const currentKeys = new Set(selectedItems.map((i) => i.key)); + const toDelete = editItemIds.filter((id) => !currentKeys.has(id)); + const toUpdate = selectedItems.filter((i) => editItemIds.includes(i.key)); + const toCreate = selectedItems.filter((i) => !editItemIds.includes(i.key)); - if (res.success) { - alert(res.message || "입고 등록 완료"); + await Promise.all([ + ...toDelete.map((id) => deleteReceiving(id)), + ...toUpdate.map((item) => + updateReceiving(item.key, { + inbound_date: modalInboundDate, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + warehouse_code: modalWarehouse || undefined, + location_code: modalLocation || undefined, + memo: modalMemo || undefined, + } as any) + ), + ...(toCreate.length > 0 + ? [createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: toCreate.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + })] + : []), + ]); + toast.success("입고 정보를 수정했어요"); setIsModalOpen(false); fetchList(); + } else { + const res = await createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: selectedItems.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + }); + + if (res.success) { + toast.success(res.message || "입고 등록 완료"); + setIsModalOpen(false); + fetchList(); + } } - } catch { - alert("입고 등록 중 오류가 발생했습니다."); + } catch (err: any) { + const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "입고 등록 중 오류가 발생했습니다."); + toast.error(msg); } finally { setSaving(false); } @@ -897,13 +877,13 @@ export default function ReceivingPage() { } /> - {/* 입고 목록 테이블 (마스터-디테일 그룹) */} + {/* 입고 목록 테이블 (플랫 리스트) */}

입고 목록

- {Object.keys(filteredGroups).length}건 + {filteredRows.length}건
-
+
- - {visibleMasterLayout.map((col) => ( - - ))} + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 입고번호 (별도 컬럼) */} - -
-
handleSort("inbound_number")}> - 입고번호 - {sortState?.key === "inbound_number" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["inbound_number"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {GRID_COLUMNS.map((col) => { + const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} + + ); + })} @@ -993,7 +963,7 @@ export default function ReceivingPage() { - ) : Object.keys(filteredGroups).length === 0 ? ( + ) : filteredRows.length === 0 ? (
@@ -1003,212 +973,59 @@ export default function ReceivingPage() { ) : ( - Object.entries(filteredGroups).map(([inboundNo, group]) => { - const isExpanded = expandedOrders.has(inboundNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 */} - { - if (expandedOrders.has(inboundNo)) { - setClosingOrders((prev) => new Set(prev).add(inboundNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(inboundNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(inboundNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(inboundNo)); - } + { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + onDoubleClick={() => openEditModal(row as any)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); }} > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 입고번호 */} - - {inboundNo} - ({group.details.length}) - - {/* 마스터 필드 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => { - switch (col.key) { - case "inbound_type": return ( - - - {resolveInboundType(master.inbound_type)} - - - ); - case "inbound_date": return ( - - {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} - - ); - case "reference_number": return ( - - {master.reference_number || ""} - - ); - case "supplier_name": return ( - - {master.supplier_name || ""} - - ); - case "warehouse_name": return ( - - {master.warehouse_name || master.warehouse_code || ""} - - ); - case "inbound_status": return ( - - - {master.inbound_status || "-"} - - - ); - case "memo": return ( - - {master.memo || ""} - - ); - default: return {(master as any)[col.key] ?? ""}; - } - })} - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - - {visibleDetailCols.map((col) => { - const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => { - let v = (d as any)[col.key]; - if (col.key === "source_table") v = SOURCE_TABLE_LABEL[v] || v; - return v; - }).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} -
- )} - - {/* 디테일 행 (펼쳤을 때만) */} - {isExpanded && group.details.map((row, detailIdx) => { - const isClosing = closingOrders.has(inboundNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - - {visibleDetailCols.map((col) => { - switch (col.key) { - case "source_table": return {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"}; - case "item_number": return {row.item_number || ""}; - case "item_name": return {row.item_name || ""}; - case "spec": return {row.spec || ""}; - case "inbound_qty": return {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}; - case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; - case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; - default: return {(row as any)[col.key] ?? ""}; - } - })} - - ); - })} - + {}} /> + + {row.inbound_number || ""} + + + {row.inbound_type || "-"} + + + + {row.inbound_date ? new Date(row.inbound_date).toLocaleDateString("ko-KR") : ""} + + {row.reference_number || ""} + {row.source_type || ""} + {row.supplier_name || ""} + {row.item_number || ""} + {row.item_name || ""} + {row.spec || ""} + {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {row.warehouse_name || (row as any).warehouse_code || ""} + + + {row.inbound_status || "-"} + + + {row.remark || row.memo || ""} + ); }) )} @@ -1221,9 +1038,9 @@ export default function ReceivingPage() { - 입고 등록 + {editMode ? "입고 수정" : "입고 등록"} - 입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해 주세요. + {editMode ? "입고 정보를 수정해 주세요." : "입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해 주세요."}
diff --git a/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx index 38b01658..534582f9 100644 --- a/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx @@ -1053,14 +1053,14 @@ export default function ProductionPlanManagementPage() { return (
- + - + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - - 품목코드 - 품목명 + + 품목코드 + 품목명 {ts.visibleColumns.map((col) => ( {col.label} @@ -1073,9 +1073,9 @@ export default function ProductionPlanManagementPage() { if (item._isGroupSummary) { return ( - - - + + + {ts.visibleColumns.map((col) => { const v = (item as any)[col.key]; return ( @@ -1090,14 +1090,14 @@ export default function ProductionPlanManagementPage() { return ( - e.stopPropagation()}> + e.stopPropagation()}> toggleItemGroupSelect(item.item_code)} className="h-4 w-4" /> - toggleItemExpand(item.item_code)}> + toggleItemExpand(item.item_code)}> - toggleItemExpand(item.item_code)}>{item.item_code} - toggleItemExpand(item.item_code)}>{item.item_name} + toggleItemExpand(item.item_code)}>{item.item_code} + toggleItemExpand(item.item_code)}>{item.item_name} {ts.visibleColumns.map((col) => renderGroupCell(col, item))} {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => { @@ -1107,9 +1107,9 @@ export default function ProductionPlanManagementPage() { } return ( - - - + + +
수주번호: {detail.order_no} diff --git a/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx index 143a84a9..f712d3ed 100644 --- a/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx @@ -13,7 +13,7 @@ import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, - ClipboardList, Pencil, Search, X, Package, ChevronDown, + ClipboardList, Pencil, Search, X, Package, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, GripVertical, } from "lucide-react"; @@ -88,18 +88,18 @@ const GRID_COLUMNS_CONFIG = [ ]; const MODAL_DETAIL_COLUMNS = [ - { key: "item_code", label: "품번", width: "w-[120px]" }, - { key: "item_name", label: "품명", width: "w-[120px]" }, - { key: "supplier", label: "공급업체", width: "w-[150px]" }, - { key: "spec", label: "규격", width: "w-[80px]" }, - { key: "unit", label: "단위", width: "w-[60px]" }, - { key: "order_qty", label: "발주수량", width: "w-[90px]" }, - { key: "received_qty", label: "입고수량", width: "w-[90px]" }, - { key: "remain_qty", label: "잔량", width: "w-[80px]" }, - { key: "unit_price", label: "단가", width: "w-[100px]" }, - { key: "amount", label: "금액", width: "w-[100px]" }, - { key: "due_date", label: "납기일", width: "w-[160px]" }, - { key: "memo", label: "메모", width: "w-[120px]" }, + { key: "item_code", label: "품번", width: "min-w-[120px]" }, + { key: "item_name", label: "품명", width: "min-w-[150px]" }, + { key: "supplier", label: "공급업체", width: "min-w-[150px]" }, + { key: "spec", label: "규격", width: "min-w-[80px]" }, + { key: "unit", label: "단위", width: "min-w-[90px]" }, + { key: "order_qty", label: "발주수량", width: "min-w-[90px]" }, + { key: "received_qty", label: "입고수량", width: "min-w-[90px]" }, + { key: "remain_qty", label: "잔량", width: "min-w-[80px]" }, + { key: "unit_price", label: "단가", width: "min-w-[100px]" }, + { key: "amount", label: "금액", width: "min-w-[100px]" }, + { key: "due_date", label: "납기일", width: "min-w-[160px]" }, + { key: "memo", label: "메모", width: "min-w-[120px]" }, ]; const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16"; @@ -162,7 +162,6 @@ export default function PurchaseOrderPage() { const [excelUploadOpen, setExcelUploadOpen] = useState(false); const [categoryOptions, setCategoryOptions] = useState>({}); const [checkedIds, setCheckedIds] = useState([]); - const [expandedOrders, setExpandedOrders] = useState>(new Set()); // 테이블 설정 const ts = useTableSettings("c16-purchase-order", DETAIL_TABLE, GRID_COLUMNS_CONFIG); @@ -238,7 +237,7 @@ export default function PurchaseOrderPage() { ); try { const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, size: 5000, autoFilter: true, }); const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || []; optMap["supplier_code"] = supps.map((s: any) => ({ @@ -248,7 +247,7 @@ export default function PurchaseOrderPage() { } catch { /* skip */ } try { const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, size: 5000, autoFilter: true, }); const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; optMap["manager"] = users.map((u: any) => ({ @@ -294,7 +293,7 @@ export default function PurchaseOrderPage() { } const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined, autoFilter: true, sort: { columnName: "purchase_no", order: "desc" }, @@ -372,20 +371,6 @@ export default function PurchaseOrderPage() { useEffect(() => { fetchOrders(); }, [fetchOrders]); // purchase_no 기준 그룹핑 - const orderGroups = useMemo(() => { - const map: Record = {}; - for (const row of orders) { - const key = row.purchase_no || row.id; - if (!map[key]) { - map[key] = { - master: { purchase_no: row.purchase_no, order_date: row.order_date, supplier_name: row.supplier_name, status: row.status, memo: row.memo }, - details: [], - }; - } - map[key].details.push(row); - } - return map; - }, [orders]); const getCategoryLabel = (col: string, code: string) => { if (!code) return ""; @@ -553,7 +538,7 @@ export default function PurchaseOrderPage() { } }; - // 품목 검색 + // 품목 검색 (수주관리와 동일한 서버 페이징 방식) const searchItems = async (page?: number, size?: number) => { const p = page ?? itemPage; const s = size ?? itemPageSize; @@ -563,25 +548,24 @@ export default function PurchaseOrderPage() { if (itemSearchKeyword) { filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); } + // 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응) + if (itemSearchDivision !== "all") { + const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; + const divValues = [itemSearchDivision]; + if (divLabel) divValues.push(divLabel); + filters.push({ columnName: "division", operator: "in", value: divValues }); + } const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: p, size: s, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const resData = res.data?.data; - let allRows = resData?.data || resData?.rows || []; - if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - allRows = allRows.filter((item: any) => { - const div = item.division || ""; - return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel)); - }); - } - const total = allRows.length; - const start = (p - 1) * s; - setItemSearchResults(allRows.slice(start, start + s)); - setItemTotal(total); - setItemTotalPages(Math.max(1, Math.ceil(total / s))); + const rows = resData?.data || resData?.rows || []; + const serverTotal = resData?.total || resData?.totalCount || rows.length; + setItemSearchResults(rows); + setItemTotal(serverTotal); + setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); } catch { /* skip */ } finally { setItemSearchLoading(false); } @@ -623,7 +607,7 @@ export default function PurchaseOrderPage() { try { const itemIds = selected.map((item) => item.item_number || item.id); const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [ @@ -686,7 +670,7 @@ export default function PurchaseOrderPage() { if (itemCodes.length === 0) return; try { const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, autoFilter: true, }); @@ -708,7 +692,7 @@ export default function PurchaseOrderPage() { if (itemCodes.length === 0) return; try { const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [ { columnName: "supplier_id", operator: "equals", value: supplierCode }, { columnName: "item_id", operator: "in", value: itemCodes }, @@ -811,125 +795,83 @@ export default function PurchaseOrderPage() {
- {/* 데이터 테이블 — 아코디언 그룹핑 */} + {/* 데이터 테이블 — 플랫 리스트 */}
{loading ? (
- ) : Object.keys(orderGroups).length === 0 ? ( + ) : orders.length === 0 ? (

등록된 발주가 없어요

) : ( (() => { - const MASTER_KEYS = new Set(["purchase_no", "order_date", "supplier_name", "status", "memo"]); const numCols = new Set(["order_qty", "received_qty", "remain_qty", "unit_price", "amount"]); - - // ts.visibleColumns 순서를 따르되, 마스터/디테일 컬럼을 분리 - // 고정 컬럼(품목수)은 마스터 선행 컬럼 뒤에 배치 - const leadingMaster: typeof ts.visibleColumns = []; - const detailCols: typeof ts.visibleColumns = []; - const trailingMaster: typeof ts.visibleColumns = []; - let passedFirstDetail = false; - for (const col of ts.visibleColumns) { - if (MASTER_KEYS.has(col.key)) { - if (passedFirstDetail) trailingMaster.push(col); - else leadingMaster.push(col); - } else { - passedFirstDetail = true; - detailCols.push(col); - } - } - - const renderDetailCell = (row: any, key: string) => { + const renderCell = (row: any, key: string) => { const val = row[key]; if (key === "status") return val ? {val} : "-"; - if (numCols.has(key)) return {val ? Number(val).toLocaleString() : "0"}; - return val || "-"; + if (key === "order_date" || key === "due_date") return val ? new Date(val).toLocaleDateString("ko-KR") : "-"; + if (numCols.has(key)) return {val ? Number(val).toLocaleString() : ""}; + return val || ""; }; - - const renderMasterHead = (col: { key: string; label: string }) => ( - - {col.label} - - ); - - const renderMasterCell = (col: { key: string }, m: any, purchaseNo: string) => { - if (col.key === "purchase_no") return {purchaseNo}; - if (col.key === "order_date") return {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}; - if (col.key === "supplier_name") return {m.supplier_name || "-"}; - if (col.key === "status") return {m.status && {m.status}}; - if (col.key === "memo") return {m.memo || ""}; - return ; - }; - return (
- - - {leadingMaster.map(renderMasterHead)} - 품목수 - {detailCols.map(col => ( - - {col.label}{col.key === "order_qty" || col.key === "amount" ? " 합계" : ""} + { + const allIds = orders.map((r) => r.id); + const allChecked = allIds.length > 0 && allIds.every((id) => checkedIds.includes(id)); + setCheckedIds(allChecked ? [] : allIds); + }} + > + 0 && orders.every((r) => checkedIds.includes(r.id))} + onCheckedChange={() => {}} + /> + + {ts.visibleColumns.map((col) => ( + + {col.label} ))} - {trailingMaster.map(renderMasterHead)} - {Object.entries(orderGroups).map(([purchaseNo, group]) => { - const isExpanded = expandedOrders.has(purchaseNo); - const detailIds = group.details.map(d => d.id); - const allChecked = detailIds.length > 0 && detailIds.every(id => checkedIds.includes(id)); - const someChecked = detailIds.some(id => checkedIds.includes(id)); - const m = group.master; - const totalQty = group.details.reduce((s, d) => s + (Number(d.order_qty) || 0), 0); - const totalAmt = group.details.reduce((s, d) => s + (Number(d.amount) || 0), 0); + {orders.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - setExpandedOrders(prev => { const next = new Set(prev); if (next.has(purchaseNo)) next.delete(purchaseNo); else next.add(purchaseNo); return next; })} - onDoubleClick={() => openEditModal(purchaseNo)} + { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + onDoubleClick={() => openEditModal(row.purchase_no)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} > - - + {}} /> + + {ts.visibleColumns.map((col) => ( + + {renderCell(row, col.key)} - { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !detailIds.includes(id)) : [...new Set([...prev, ...detailIds])]); }}> - {}} /> - - {leadingMaster.map(col => renderMasterCell(col, m, purchaseNo))} - {group.details.length}건 - {detailCols.map(col => ( - - {col.key === "order_qty" ? totalQty.toLocaleString() - : col.key === "amount" ? totalAmt.toLocaleString() - : ""} - - ))} - {trailingMaster.map(col => renderMasterCell(col, m, purchaseNo))} - - {isExpanded && group.details.map((row) => ( - - - { e.stopPropagation(); setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id]); }}> - {}} /> - - {leadingMaster.map(col => )} - - {detailCols.map(col => ( - - {renderDetailCell(row, col.key)} - - ))} - {trailingMaster.map(col => )} - ))} - + ); })} @@ -938,7 +880,7 @@ export default function PurchaseOrderPage() { })() )}
- 전체 {Object.keys(orderGroups).length}건 (발주 기준) / {orders.length}건 (품목 기준) + 전체 {orders.length}건
@@ -1094,7 +1036,7 @@ export default function PurchaseOrderPage() { ) : (
-
+
c.key)} strategy={horizontalListSortingStrategy}> @@ -1118,12 +1060,12 @@ export default function PurchaseOrderPage() { {visibleModalColumns.map((col) => { switch (col.key) { case "item_code": - return {row.item_code}; + return {row.item_code}; case "item_name": - return {row.item_name}; + return {row.item_name}; case "supplier": return ( - + {isReadOnly ? ( {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} ) : ( @@ -1133,7 +1075,7 @@ export default function PurchaseOrderPage() { updateDetailRow(idx, "supplier_code", v); updateDetailRow(idx, "supplier_name", name); }}> - + {(categoryOptions["supplier_code"] || []).map(o => ( {o.label} @@ -1149,11 +1091,11 @@ export default function PurchaseOrderPage() { return {row.unit}; case "order_qty": return ( - + {isReadOnly ? ( {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} ) : ( - updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" /> )} ); @@ -1163,11 +1105,11 @@ export default function PurchaseOrderPage() { return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; case "unit_price": return ( - + {isReadOnly ? ( {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} ) : ( - updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" /> )} ); @@ -1175,21 +1117,21 @@ export default function PurchaseOrderPage() { return {row.amount ? Number(row.amount).toLocaleString() : ""}; case "due_date": return ( - + {isReadOnly ? ( {row.due_date} ) : ( - updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> + updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs w-full" /> )} ); case "memo": return ( - + {isReadOnly ? ( {row.memo} ) : ( - updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> + updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs w-full" /> )} ); diff --git a/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx index 05facbbc..863c0aef 100644 --- a/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx @@ -424,9 +424,10 @@ export default function ItemInspectionInfoPage() { case "inspection_type": return (
- {group.types.map((t: string) => ( - {t} - ))} + {group.types.map((t: string) => { + const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t; + return {label}; + })}
); @@ -494,7 +495,7 @@ export default function ItemInspectionInfoPage() { : "bg-muted/50 text-muted-foreground border-border hover:bg-muted" )} > - {type} + {inspTypeCatOptions.find((o) => o.code === type)?.label || type} o.code === itemSearchDivision)?.label || ""; + // 코드 또는 라벨이 저장된 경우 모두 조회하기 위해 in 연산자 사용 + const divValues = [itemSearchDivision]; + if (divLabel) divValues.push(divLabel); + filters.push({ columnName: "division", operator: "in", value: divValues }); + } + // 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링 const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI"; const partnerId = masterForm.partner_id; @@ -632,7 +641,7 @@ export default function SalesOrderPage() { if (isCustomerPrice && partnerId) { try { const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] }, autoFilter: true, }); @@ -642,31 +651,22 @@ export default function SalesOrderPage() { } const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: p, size: s, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const resData = res.data?.data; - let allRows = resData?.data || resData?.rows || []; + let rows = resData?.data || resData?.rows || []; + const serverTotal = resData?.total || resData?.totalCount || rows.length; - // 거래처우선일 때 연결된 품목만 표시 + // 거래처우선일 때 연결된 품목만 표시 (클라이언트 필터) if (customerItemIds) { - allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); + rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); } - // 관리품목 필터 (코드/라벨 혼재 대응) - if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - allRows = allRows.filter((item: any) => { - const div = item.division || ""; - return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel)); - }); - } - const total = allRows.length; - const start = (p - 1) * s; - setItemSearchResults(allRows.slice(start, start + s)); - setItemTotal(total); - setItemTotalPages(Math.max(1, Math.ceil(total / s))); + setItemSearchResults(rows); + setItemTotal(serverTotal); + setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); } catch { /* skip */ } finally { setItemSearchLoading(false); } @@ -1320,44 +1320,44 @@ export default function SalesOrderPage() {
- No - 품번 - 품명 - 규격 - 재질 - 포장재 - 단위 - 수량 - 포장수량 - 단가 - 금액 - 납기일 - 분할/삭제 + No + 품번 + 품명 + 규격 + 재질 + 포장재 + 단위 + 수량 + 포장수량 + 단가 + 금액 + 납기일 + 분할/삭제 {detailRows.map((row, idx) => ( {idx + 1} - - {row.part_code} + + {row.part_code} - - {row.part_name} + + {row.part_name} - {row.spec} - {row.material} + {row.spec} + {row.material} updateDetailRow(idx, "packing_material", e.target.value)} placeholder="포장재" - className="h-8 text-xs" + className="h-8 text-xs w-full" /> - - - 전체 - {(categoryOptions["item_division"] || []).map((o) => ( - {o.label} - ))} - - +
영업관리
diff --git a/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx index 824e2e86..457e2723 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx @@ -38,7 +38,6 @@ import { Save, ChevronRight, ChevronLeft, - ChevronDown, ChevronsLeft, ChevronsRight, Inbox, @@ -116,41 +115,8 @@ const GRID_COLUMNS = [ { key: "remark", label: "비고" }, ]; -// 마스터 헤더 레이아웃 (출고번호 뒤) -const MASTER_BODY_LAYOUT = [ - { key: "outbound_type", label: "출고유형", colSpan: 1 }, - { key: "outbound_date", label: "출고일", colSpan: 1 }, - { key: "reference_number", label: "참조번호", colSpan: 1 }, - { key: "customer_name", label: "거래처", colSpan: 1 }, - { key: "warehouse_name", label: "창고", colSpan: 1 }, - { key: "outbound_status", label: "출고상태", colSpan: 1 }, - { key: "memo", label: "비고", colSpan: 1 }, -]; - -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "source_type", label: "출처" }, - { key: "item_code", label: "품목코드" }, - { key: "item_name", label: "품목명" }, - { key: "specification", label: "규격" }, - { key: "outbound_qty", label: "출고수량" }, - { key: "unit_price", label: "단가" }, - { key: "total_amount", label: "금액" }, -]; - -// 마스터 필드 키 목록 (필터 분류용) -const MASTER_KEYS = new Set(["outbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); - -// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) -const DETAIL_KEY_MAP: Record = { - source_type: "source_type", - item_number: "item_code", - item_name: "item_name", - spec: "specification", - outbound_qty: "outbound_qty", - unit_price: "unit_price", - total_amount: "total_amount", -}; +// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16 +const TOTAL_COLS = 16; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -258,30 +224,6 @@ interface SelectedSourceItem { export default function OutboundPage() { const ts = useTableSettings("c16-outbound", "outbound_mng", GRID_COLUMNS); - // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 - const visibleMasterLayout = useMemo(() => { - const ordered: typeof MASTER_BODY_LAYOUT = []; - for (const vc of ts.visibleColumns) { - const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); - if (m) ordered.push(m); - } - return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; - }, [ts.visibleColumns]); - - const visibleDetailCols = useMemo(() => { - const ordered: typeof DETAIL_HEADER_COLS = []; - for (const vc of ts.visibleColumns) { - const detailKey = DETAIL_KEY_MAP[vc.key]; - if (detailKey) { - const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); - if (d) ordered.push(d); - } - } - return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; - }, [ts.visibleColumns]); - - const TOTAL_COLS = 3 + visibleMasterLayout.length; - // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -290,9 +232,7 @@ export default function OutboundPage() { // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); - // 마스터-디테일 그룹 테이블 state - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); + // 헤더 필터 & 정렬 const [headerFilters, setHeaderFilters] = useState>>({}); const [sortState, setSortState] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -403,121 +343,59 @@ export default function OutboundPage() { })(); }, []); - // --- 마스터-디테일 그룹핑, 필터, 정렬 --- + // 플랫 행 생성 (출고유형 코드→라벨 변환, source_type→한글 등) + const flatRows = useMemo(() => { + return data.map((row) => ({ + ...row, + _raw_outbound_type: row.outbound_type, + outbound_type: resolveCat("outbound_type", row.outbound_type) || row.outbound_type || "", + source_type: row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "", + item_number: row.item_code || (row as any).item_number || "", + spec: row.specification || (row as any).spec || "", + })); + }, [data, resolveCat]); - // outbound_number 기준 그룹핑 + 필터 + 정렬 - const filteredGroups = useMemo(() => { - // 1차: outbound_number 기준 그룹핑 - const allGroups: Record = {}; - for (const row of data) { - const key = row.outbound_number || "_no_number"; - if (!allGroups[key]) { - allGroups[key] = { master: row, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - const raw = (group.master as any)?.[colKey] ?? ""; - return values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([outNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - let cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; - if (colKey === "source_type") cellVal = SOURCE_TYPE_LABEL[cellVal] || cellVal; - return values.has(cellVal); - }) - ); - return [outNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - entries.sort(([, a], [, b]) => { - const av = (a.master as any)?.[key] ?? ""; - const bv = (b.master as any)?.[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - let av: any = (a as any)[key] ?? ""; - let bv: any = (b as any)[key] ?? ""; - if (key === "source_type") { av = SOURCE_TYPE_LABEL[av] || av; bv = SOURCE_TYPE_LABEL[bv] || bv; } - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [data, headerFilters, sortState]); - - // 마스터 컬럼별 고유값 (헤더 필터용) - const masterUniqueValues = useMemo(() => { + // 컬럼별 고유값 (헤더 필터용) + const columnUniqueValues = useMemo(() => { const result: Record = {}; - const seenMasters = new Map(); - data.forEach((row) => { - if (row.outbound_number && !seenMasters.has(row.outbound_number)) { - seenMasters.set(row.outbound_number, row); - } - }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "outbound_number", label: "출고번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label }))]) { + for (const col of GRID_COLUMNS) { const values = new Set(); - masters.forEach((m) => { - const val = (m as any)?.[col.key]; + flatRows.forEach((row) => { + const val = (row as any)[col.key]; if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [data]); + }, [flatRows]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) - const columnUniqueValues = useMemo(() => { - const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { - const values = new Set(); - data.forEach((row) => { - let val = (row as any)[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "source_type") val = SOURCE_TYPE_LABEL[val] || val; - values.add(String(val)); - } + // 필터 + 정렬 적용된 플랫 데이터 + const filteredRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; + return values.has(cellVal); }); - result[col.key] = Array.from(values).sort(); } - return result; - }, [data]); + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = (a as any)[key] ?? ""; + const bv = (b as any)[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -938,7 +816,7 @@ export default function OutboundPage() { dataCount={data.length} /> - {/* 출고 목록 테이블 */} + {/* 출고 목록 테이블 (플랫 리스트) */}
{/* 패널 헤더 */}
@@ -946,7 +824,7 @@ export default function OutboundPage() { 출고 목록 - {data.length}건 + {filteredRows.length}건
@@ -971,78 +849,68 @@ export default function OutboundPage() {
-
+
- - {visibleMasterLayout.map((col) => ( - - ))} + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 출고번호 */} - -
-
handleSort("outbound_number")}> - 출고번호 - {sortState?.key === "outbound_number" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["outbound_number"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {GRID_COLUMNS.map((col) => { + const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} + + ); + })} @@ -1052,7 +920,7 @@ export default function OutboundPage() { - ) : Object.keys(filteredGroups).length === 0 ? ( + ) : filteredRows.length === 0 ? (
@@ -1062,214 +930,59 @@ export default function OutboundPage() { ) : ( - Object.entries(filteredGroups).map(([outboundNo, group]) => { - const isExpanded = expandedOrders.has(outboundNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 */} - { - if (expandedOrders.has(outboundNo)) { - setClosingOrders((prev) => new Set(prev).add(outboundNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(outboundNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(outboundNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(outboundNo)); - } - }} - onDoubleClick={() => openEditModal(group.details[0])} - > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 출고번호 */} - - {outboundNo} - ({group.details.length}) - - {/* 마스터 필드 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => { - switch (col.key) { - case "outbound_type": return ( - - - {resolveCat("outbound_type", master.outbound_type) || "-"} - - - ); - case "outbound_date": return ( - - {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} - - ); - case "reference_number": return ( - - {master.reference_number || ""} - - ); - case "customer_name": return ( - - {master.customer_name || ""} - - ); - case "warehouse_name": return ( - - {master.warehouse_name || master.warehouse_code || ""} - - ); - case "outbound_status": return ( - - - {master.outbound_status || "-"} - - - ); - case "memo": return ( - - {master.memo || ""} - - ); - default: return {(master as any)[col.key] ?? ""}; - } - })} - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - - {visibleDetailCols.map((col) => { - const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => { - let v = (d as any)[col.key]; - if (col.key === "source_type") v = SOURCE_TYPE_LABEL[v] || v; - return v; - }).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} -
+ { - const isClosing = closingOrders.has(outboundNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - onDoubleClick={() => openEditModal(row)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - - {visibleDetailCols.map((col) => { - switch (col.key) { - case "source_type": return {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"}; - case "item_code": return {row.item_code || ""}; - case "item_name": return {row.item_name || ""}; - case "specification": return {row.specification || ""}; - case "outbound_qty": return {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}; - case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; - case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; - default: return {(row as any)[col.key] ?? ""}; - } - })} - + onClick={() => { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] ); - })} - + }} + onDoubleClick={() => openEditModal(row as any)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + > + {}} /> + + {row.outbound_number || ""} + + + {row.outbound_type || "-"} + + + + {row.outbound_date ? new Date(row.outbound_date).toLocaleDateString("ko-KR") : ""} + + {row.reference_number || ""} + {row.source_type || ""} + {row.customer_name || ""} + {row.item_number || ""} + {row.item_name || ""} + {row.spec || ""} + {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {row.warehouse_name || row.warehouse_code || ""} + + + {row.outbound_status || "-"} + + + {row.remark || row.memo || ""} + ); }) )} diff --git a/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx index 2933d64b..77d21e7b 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx @@ -43,7 +43,6 @@ import { X, Save, ChevronRight, - ChevronDown, ChevronLeft, ChevronsLeft, ChevronsRight, @@ -64,6 +63,7 @@ import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSea import { getReceivingList, createReceiving, + updateReceiving, deleteReceiving, generateReceivingNumber, getReceivingWarehouses, @@ -95,41 +95,8 @@ const GRID_COLUMNS = [ { key: "remark", label: "비고" }, ]; -// 마스터 헤더 레이아웃 (입고번호 뒤, 디테일 7컬럼 위에 colSpan으로 맵핑) -const MASTER_BODY_LAYOUT = [ - { key: "inbound_type", label: "입고유형", colSpan: 1 }, - { key: "inbound_date", label: "입고일", colSpan: 1 }, - { key: "reference_number", label: "참조번호", colSpan: 1 }, - { key: "supplier_name", label: "공급처", colSpan: 1 }, - { key: "warehouse_name", label: "창고", colSpan: 1 }, - { key: "inbound_status", label: "입고상태", colSpan: 1 }, - { key: "memo", label: "비고", colSpan: 1 }, -]; - -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "source_table", label: "출처" }, - { key: "item_number", label: "품목코드" }, - { key: "item_name", label: "품목명" }, - { key: "spec", label: "규격" }, - { key: "inbound_qty", label: "입고수량" }, - { key: "unit_price", label: "단가" }, - { key: "total_amount", label: "금액" }, -]; - -// 마스터 필드 키 목록 (필터 분류용) -const MASTER_KEYS = new Set(["inbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); - -// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) -const DETAIL_KEY_MAP: Record = { - source_type: "source_table", - item_number: "item_number", - item_name: "item_name", - spec: "spec", - inbound_qty: "inbound_qty", - unit_price: "unit_price", - total_amount: "total_amount", -}; +// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16 +const TOTAL_COLS = 16; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -287,30 +254,6 @@ interface SelectedSourceItem { export default function ReceivingPage() { const ts = useTableSettings("c16-receiving", "inbound_mng", GRID_COLUMNS); - // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 - const visibleMasterLayout = useMemo(() => { - const ordered: typeof MASTER_BODY_LAYOUT = []; - for (const vc of ts.visibleColumns) { - const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); - if (m) ordered.push(m); - } - return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; - }, [ts.visibleColumns]); - - const visibleDetailCols = useMemo(() => { - const ordered: typeof DETAIL_HEADER_COLS = []; - for (const vc of ts.visibleColumns) { - const detailKey = DETAIL_KEY_MAP[vc.key]; - if (detailKey) { - const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); - if (d) ordered.push(d); - } - } - return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; - }, [ts.visibleColumns]); - - const TOTAL_COLS = 3 + visibleMasterLayout.length; - // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -319,9 +262,7 @@ export default function ReceivingPage() { // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); - // 마스터-디테일 그룹 테이블 - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); + // 헤더 필터 & 정렬 const [headerFilters, setHeaderFilters] = useState>>({}); const [sortState, setSortState] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -338,6 +279,10 @@ export default function ReceivingPage() { const [selectedItems, setSelectedItems] = useState([]); const [saving, setSaving] = useState(false); + // 수정 모드 + const [editMode, setEditMode] = useState(false); + const [editItemIds, setEditItemIds] = useState([]); + // 소스 데이터 const [sourceKeyword, setSourceKeyword] = useState(""); const [sourceLoading, setSourceLoading] = useState(false); @@ -434,124 +379,56 @@ export default function ReceivingPage() { })(); }, []); - // 필터 + 정렬 적용된 데이터 -> 그룹핑 - const filteredGroups = useMemo(() => { - // 1차: inbound_number 기준 그룹핑 - const allGroups: Record = {}; - for (const row of data) { - const key = row.inbound_number || "_no_inbound"; - if (!allGroups[key]) { - allGroups[key] = { master: row, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - let raw = (group.master as any)?.[colKey] ?? ""; - // 입고유형은 코드→라벨 변환된 값으로 비교 - if (colKey === "inbound_type") raw = resolveInboundType(String(raw)); - return values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([inboundNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - let cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; - if (colKey === "source_table") cellVal = SOURCE_TABLE_LABEL[cellVal] || cellVal; - return values.has(cellVal); - }) - ); - return [inboundNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - entries.sort(([, a], [, b]) => { - let av: any = (a.master as any)?.[key] ?? ""; - let bv: any = (b.master as any)?.[key] ?? ""; - if (key === "inbound_type") { av = resolveInboundType(String(av)); bv = resolveInboundType(String(bv)); } - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - const av = (a as any)[key] ?? ""; - const bv = (b as any)[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [data, headerFilters, sortState]); - - // 마스터 컬럼별 고유값 (마스터 헤더 필터용) - const masterUniqueValues = useMemo(() => { - const result: Record = {}; - const seenMasters = new Map(); - data.forEach((row) => { - if (row.inbound_number && !seenMasters.has(row.inbound_number)) { - seenMasters.set(row.inbound_number, row); - } - }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "inbound_number", label: "입고번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label }))]) { - const values = new Set(); - masters.forEach((m) => { - let val = (m as any)?.[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "inbound_type") val = resolveInboundType(String(val)); - values.add(String(val)); - } - }); - result[col.key] = Array.from(values).sort(); - } - return result; + // 플랫 행 생성 (입고유형 코드→라벨 변환, source_table→한글 등) + const flatRows = useMemo(() => { + return data.map((row) => ({ + ...row, + inbound_type: resolveInboundType(row.inbound_type), + source_type: row.source_table ? (SOURCE_TABLE_LABEL[row.source_table] || row.source_table) : (row as any).source_type || "", + })); }, [data]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) + // 컬럼별 고유값 (헤더 필터용) const columnUniqueValues = useMemo(() => { const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { + for (const col of GRID_COLUMNS) { const values = new Set(); - data.forEach((row) => { - let val = (row as any)[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "source_table") val = SOURCE_TABLE_LABEL[val] || val; - values.add(String(val)); - } + flatRows.forEach((row) => { + const val = (row as any)[col.key]; + if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [data]); + }, [flatRows]); + + // 필터 + 정렬 적용된 플랫 데이터 + const filteredRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; + return values.has(cellVal); + }); + } + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = (a as any)[key] ?? ""; + const bv = (b as any)[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -633,6 +510,8 @@ export default function ReceivingPage() { const openRegisterModal = async () => { const defaultType = "구매입고"; + setEditMode(false); + setEditItemIds([]); setModalInboundType(defaultType); setModalInboundDate(new Date().toISOString().split("T")[0]); setModalWarehouse(""); @@ -661,6 +540,51 @@ export default function ReceivingPage() { } }; + // 수정 모달 열기 + const openEditModal = (row: InboundItem) => { + const inNo = row.inbound_number; + const grouped = data.filter((d) => d.inbound_number === inNo); + const first = grouped[0] || row; + + setEditMode(true); + setEditItemIds(grouped.map((g) => g.id)); + setModalInboundNo(inNo); + setModalInboundType(first.inbound_type || "구매입고"); + setModalInboundDate(first.inbound_date ? String(first.inbound_date).slice(0, 10) : ""); + setModalWarehouse(first.warehouse_code || ""); + setModalLocation(first.location_code || ""); + setModalInspector((first as any).inspector || ""); + setModalManager((first as any).manager || ""); + setModalMemo(first.memo || ""); + setSelectedItems( + grouped.map((g) => ({ + key: g.id, + inbound_type: g.inbound_type || "", + reference_number: g.reference_number || "", + supplier_code: (g as any).supplier_code || "", + supplier_name: g.supplier_name || "", + item_number: g.item_number || "", + item_name: g.item_name || "", + spec: g.spec || "", + material: (g as any).material || "", + unit: (g as any).unit || "", + inbound_qty: Number(g.inbound_qty) || 0, + unit_price: Number(g.unit_price) || 0, + total_amount: Number(g.total_amount) || 0, + source_table: (g as any).source_table || "", + source_id: (g as any).source_id || "", + })) + ); + setSourceKeyword(""); + setPurchaseOrders([]); + setShipments([]); + setItems([]); + setSourcePage(1); + setSourceTotalCount(0); + setIsModalOpen(true); + loadSourceData(first.inbound_type || "구매입고", undefined, 1); + }; + // 검색 버튼 클릭 시 const searchSourceData = useCallback(async () => { setSourcePage(1); @@ -811,41 +735,97 @@ export default function ReceivingPage() { } setSaving(true); try { - const res = await createReceiving({ - inbound_number: modalInboundNo, - inbound_date: modalInboundDate, - warehouse_code: modalWarehouse, - location_code: modalLocation || undefined, - inspector: modalInspector || undefined, - manager: modalManager || undefined, - memo: modalMemo || undefined, - items: selectedItems.map((item) => ({ - inbound_type: item.inbound_type, - reference_number: item.reference_number, - supplier_code: item.supplier_code, - supplier_name: item.supplier_name, - item_number: item.item_number, - item_name: item.item_name, - spec: item.spec, - material: item.material, - unit: item.unit, - inbound_qty: item.inbound_qty, - unit_price: item.unit_price, - total_amount: item.total_amount, - source_table: item.source_table, - source_id: item.source_id, - inbound_status: "입고완료", - inspection_status: "대기", - })), - }); + if (editMode) { + // 기존 item 수정 + 삭제된 item 삭제 + 새 item 추가 + const currentKeys = new Set(selectedItems.map((i) => i.key)); + const toDelete = editItemIds.filter((id) => !currentKeys.has(id)); + const toUpdate = selectedItems.filter((i) => editItemIds.includes(i.key)); + const toCreate = selectedItems.filter((i) => !editItemIds.includes(i.key)); - if (res.success) { - alert(res.message || "입고 등록 완료"); + await Promise.all([ + ...toDelete.map((id) => deleteReceiving(id)), + ...toUpdate.map((item) => + updateReceiving(item.key, { + inbound_date: modalInboundDate, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + warehouse_code: modalWarehouse || undefined, + location_code: modalLocation || undefined, + memo: modalMemo || undefined, + } as any) + ), + ...(toCreate.length > 0 + ? [createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: toCreate.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + })] + : []), + ]); + toast.success("입고 정보를 수정했어요"); setIsModalOpen(false); fetchList(); + } else { + const res = await createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: selectedItems.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + }); + + if (res.success) { + toast.success(res.message || "입고 등록 완료"); + setIsModalOpen(false); + fetchList(); + } } - } catch { - alert("입고 등록 중 오류가 발생했습니다."); + } catch (err: any) { + const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "입고 등록 중 오류가 발생했습니다."); + toast.error(msg); } finally { setSaving(false); } @@ -897,13 +877,13 @@ export default function ReceivingPage() { } /> - {/* 입고 목록 테이블 (마스터-디테일 그룹) */} + {/* 입고 목록 테이블 (플랫 리스트) */}

입고 목록

- {Object.keys(filteredGroups).length}건 + {filteredRows.length}건
-
+
- - {visibleMasterLayout.map((col) => ( - - ))} + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 입고번호 (별도 컬럼) */} - -
-
handleSort("inbound_number")}> - 입고번호 - {sortState?.key === "inbound_number" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["inbound_number"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {GRID_COLUMNS.map((col) => { + const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} + + ); + })} @@ -993,7 +963,7 @@ export default function ReceivingPage() { - ) : Object.keys(filteredGroups).length === 0 ? ( + ) : filteredRows.length === 0 ? (
@@ -1003,212 +973,59 @@ export default function ReceivingPage() { ) : ( - Object.entries(filteredGroups).map(([inboundNo, group]) => { - const isExpanded = expandedOrders.has(inboundNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 */} - { - if (expandedOrders.has(inboundNo)) { - setClosingOrders((prev) => new Set(prev).add(inboundNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(inboundNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(inboundNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(inboundNo)); - } + { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + onDoubleClick={() => openEditModal(row as any)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); }} > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 입고번호 */} - - {inboundNo} - ({group.details.length}) - - {/* 마스터 필드 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => { - switch (col.key) { - case "inbound_type": return ( - - - {resolveInboundType(master.inbound_type)} - - - ); - case "inbound_date": return ( - - {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} - - ); - case "reference_number": return ( - - {master.reference_number || ""} - - ); - case "supplier_name": return ( - - {master.supplier_name || ""} - - ); - case "warehouse_name": return ( - - {master.warehouse_name || master.warehouse_code || ""} - - ); - case "inbound_status": return ( - - - {master.inbound_status || "-"} - - - ); - case "memo": return ( - - {master.memo || ""} - - ); - default: return {(master as any)[col.key] ?? ""}; - } - })} - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - - {visibleDetailCols.map((col) => { - const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => { - let v = (d as any)[col.key]; - if (col.key === "source_table") v = SOURCE_TABLE_LABEL[v] || v; - return v; - }).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} -
- )} - - {/* 디테일 행 (펼쳤을 때만) */} - {isExpanded && group.details.map((row, detailIdx) => { - const isClosing = closingOrders.has(inboundNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - - {visibleDetailCols.map((col) => { - switch (col.key) { - case "source_table": return {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"}; - case "item_number": return {row.item_number || ""}; - case "item_name": return {row.item_name || ""}; - case "spec": return {row.spec || ""}; - case "inbound_qty": return {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}; - case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; - case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; - default: return {(row as any)[col.key] ?? ""}; - } - })} - - ); - })} - + {}} /> + + {row.inbound_number || ""} + + + {row.inbound_type || "-"} + + + + {row.inbound_date ? new Date(row.inbound_date).toLocaleDateString("ko-KR") : ""} + + {row.reference_number || ""} + {row.source_type || ""} + {row.supplier_name || ""} + {row.item_number || ""} + {row.item_name || ""} + {row.spec || ""} + {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {row.warehouse_name || (row as any).warehouse_code || ""} + + + {row.inbound_status || "-"} + + + {row.remark || row.memo || ""} + ); }) )} @@ -1221,9 +1038,9 @@ export default function ReceivingPage() { - 입고 등록 + {editMode ? "입고 수정" : "입고 등록"} - 입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해 주세요. + {editMode ? "입고 정보를 수정해 주세요." : "입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해 주세요."}
diff --git a/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx index 38b01658..534582f9 100644 --- a/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx @@ -1053,14 +1053,14 @@ export default function ProductionPlanManagementPage() { return (
- + - + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - - 품목코드 - 품목명 + + 품목코드 + 품목명 {ts.visibleColumns.map((col) => ( {col.label} @@ -1073,9 +1073,9 @@ export default function ProductionPlanManagementPage() { if (item._isGroupSummary) { return ( - - - + + + {ts.visibleColumns.map((col) => { const v = (item as any)[col.key]; return ( @@ -1090,14 +1090,14 @@ export default function ProductionPlanManagementPage() { return ( - e.stopPropagation()}> + e.stopPropagation()}> toggleItemGroupSelect(item.item_code)} className="h-4 w-4" /> - toggleItemExpand(item.item_code)}> + toggleItemExpand(item.item_code)}> - toggleItemExpand(item.item_code)}>{item.item_code} - toggleItemExpand(item.item_code)}>{item.item_name} + toggleItemExpand(item.item_code)}>{item.item_code} + toggleItemExpand(item.item_code)}>{item.item_name} {ts.visibleColumns.map((col) => renderGroupCell(col, item))} {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => { @@ -1107,9 +1107,9 @@ export default function ProductionPlanManagementPage() { } return ( - - - + + +
수주번호: {detail.order_no} diff --git a/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx index 143a84a9..f712d3ed 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx @@ -13,7 +13,7 @@ import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, - ClipboardList, Pencil, Search, X, Package, ChevronDown, + ClipboardList, Pencil, Search, X, Package, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, GripVertical, } from "lucide-react"; @@ -88,18 +88,18 @@ const GRID_COLUMNS_CONFIG = [ ]; const MODAL_DETAIL_COLUMNS = [ - { key: "item_code", label: "품번", width: "w-[120px]" }, - { key: "item_name", label: "품명", width: "w-[120px]" }, - { key: "supplier", label: "공급업체", width: "w-[150px]" }, - { key: "spec", label: "규격", width: "w-[80px]" }, - { key: "unit", label: "단위", width: "w-[60px]" }, - { key: "order_qty", label: "발주수량", width: "w-[90px]" }, - { key: "received_qty", label: "입고수량", width: "w-[90px]" }, - { key: "remain_qty", label: "잔량", width: "w-[80px]" }, - { key: "unit_price", label: "단가", width: "w-[100px]" }, - { key: "amount", label: "금액", width: "w-[100px]" }, - { key: "due_date", label: "납기일", width: "w-[160px]" }, - { key: "memo", label: "메모", width: "w-[120px]" }, + { key: "item_code", label: "품번", width: "min-w-[120px]" }, + { key: "item_name", label: "품명", width: "min-w-[150px]" }, + { key: "supplier", label: "공급업체", width: "min-w-[150px]" }, + { key: "spec", label: "규격", width: "min-w-[80px]" }, + { key: "unit", label: "단위", width: "min-w-[90px]" }, + { key: "order_qty", label: "발주수량", width: "min-w-[90px]" }, + { key: "received_qty", label: "입고수량", width: "min-w-[90px]" }, + { key: "remain_qty", label: "잔량", width: "min-w-[80px]" }, + { key: "unit_price", label: "단가", width: "min-w-[100px]" }, + { key: "amount", label: "금액", width: "min-w-[100px]" }, + { key: "due_date", label: "납기일", width: "min-w-[160px]" }, + { key: "memo", label: "메모", width: "min-w-[120px]" }, ]; const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16"; @@ -162,7 +162,6 @@ export default function PurchaseOrderPage() { const [excelUploadOpen, setExcelUploadOpen] = useState(false); const [categoryOptions, setCategoryOptions] = useState>({}); const [checkedIds, setCheckedIds] = useState([]); - const [expandedOrders, setExpandedOrders] = useState>(new Set()); // 테이블 설정 const ts = useTableSettings("c16-purchase-order", DETAIL_TABLE, GRID_COLUMNS_CONFIG); @@ -238,7 +237,7 @@ export default function PurchaseOrderPage() { ); try { const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, size: 5000, autoFilter: true, }); const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || []; optMap["supplier_code"] = supps.map((s: any) => ({ @@ -248,7 +247,7 @@ export default function PurchaseOrderPage() { } catch { /* skip */ } try { const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, size: 5000, autoFilter: true, }); const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; optMap["manager"] = users.map((u: any) => ({ @@ -294,7 +293,7 @@ export default function PurchaseOrderPage() { } const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined, autoFilter: true, sort: { columnName: "purchase_no", order: "desc" }, @@ -372,20 +371,6 @@ export default function PurchaseOrderPage() { useEffect(() => { fetchOrders(); }, [fetchOrders]); // purchase_no 기준 그룹핑 - const orderGroups = useMemo(() => { - const map: Record = {}; - for (const row of orders) { - const key = row.purchase_no || row.id; - if (!map[key]) { - map[key] = { - master: { purchase_no: row.purchase_no, order_date: row.order_date, supplier_name: row.supplier_name, status: row.status, memo: row.memo }, - details: [], - }; - } - map[key].details.push(row); - } - return map; - }, [orders]); const getCategoryLabel = (col: string, code: string) => { if (!code) return ""; @@ -553,7 +538,7 @@ export default function PurchaseOrderPage() { } }; - // 품목 검색 + // 품목 검색 (수주관리와 동일한 서버 페이징 방식) const searchItems = async (page?: number, size?: number) => { const p = page ?? itemPage; const s = size ?? itemPageSize; @@ -563,25 +548,24 @@ export default function PurchaseOrderPage() { if (itemSearchKeyword) { filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); } + // 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응) + if (itemSearchDivision !== "all") { + const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; + const divValues = [itemSearchDivision]; + if (divLabel) divValues.push(divLabel); + filters.push({ columnName: "division", operator: "in", value: divValues }); + } const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: p, size: s, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const resData = res.data?.data; - let allRows = resData?.data || resData?.rows || []; - if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - allRows = allRows.filter((item: any) => { - const div = item.division || ""; - return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel)); - }); - } - const total = allRows.length; - const start = (p - 1) * s; - setItemSearchResults(allRows.slice(start, start + s)); - setItemTotal(total); - setItemTotalPages(Math.max(1, Math.ceil(total / s))); + const rows = resData?.data || resData?.rows || []; + const serverTotal = resData?.total || resData?.totalCount || rows.length; + setItemSearchResults(rows); + setItemTotal(serverTotal); + setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); } catch { /* skip */ } finally { setItemSearchLoading(false); } @@ -623,7 +607,7 @@ export default function PurchaseOrderPage() { try { const itemIds = selected.map((item) => item.item_number || item.id); const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [ @@ -686,7 +670,7 @@ export default function PurchaseOrderPage() { if (itemCodes.length === 0) return; try { const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, autoFilter: true, }); @@ -708,7 +692,7 @@ export default function PurchaseOrderPage() { if (itemCodes.length === 0) return; try { const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [ { columnName: "supplier_id", operator: "equals", value: supplierCode }, { columnName: "item_id", operator: "in", value: itemCodes }, @@ -811,125 +795,83 @@ export default function PurchaseOrderPage() {
- {/* 데이터 테이블 — 아코디언 그룹핑 */} + {/* 데이터 테이블 — 플랫 리스트 */}
{loading ? (
- ) : Object.keys(orderGroups).length === 0 ? ( + ) : orders.length === 0 ? (

등록된 발주가 없어요

) : ( (() => { - const MASTER_KEYS = new Set(["purchase_no", "order_date", "supplier_name", "status", "memo"]); const numCols = new Set(["order_qty", "received_qty", "remain_qty", "unit_price", "amount"]); - - // ts.visibleColumns 순서를 따르되, 마스터/디테일 컬럼을 분리 - // 고정 컬럼(품목수)은 마스터 선행 컬럼 뒤에 배치 - const leadingMaster: typeof ts.visibleColumns = []; - const detailCols: typeof ts.visibleColumns = []; - const trailingMaster: typeof ts.visibleColumns = []; - let passedFirstDetail = false; - for (const col of ts.visibleColumns) { - if (MASTER_KEYS.has(col.key)) { - if (passedFirstDetail) trailingMaster.push(col); - else leadingMaster.push(col); - } else { - passedFirstDetail = true; - detailCols.push(col); - } - } - - const renderDetailCell = (row: any, key: string) => { + const renderCell = (row: any, key: string) => { const val = row[key]; if (key === "status") return val ? {val} : "-"; - if (numCols.has(key)) return {val ? Number(val).toLocaleString() : "0"}; - return val || "-"; + if (key === "order_date" || key === "due_date") return val ? new Date(val).toLocaleDateString("ko-KR") : "-"; + if (numCols.has(key)) return {val ? Number(val).toLocaleString() : ""}; + return val || ""; }; - - const renderMasterHead = (col: { key: string; label: string }) => ( - - {col.label} - - ); - - const renderMasterCell = (col: { key: string }, m: any, purchaseNo: string) => { - if (col.key === "purchase_no") return {purchaseNo}; - if (col.key === "order_date") return {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}; - if (col.key === "supplier_name") return {m.supplier_name || "-"}; - if (col.key === "status") return {m.status && {m.status}}; - if (col.key === "memo") return {m.memo || ""}; - return ; - }; - return (
- - - {leadingMaster.map(renderMasterHead)} - 품목수 - {detailCols.map(col => ( - - {col.label}{col.key === "order_qty" || col.key === "amount" ? " 합계" : ""} + { + const allIds = orders.map((r) => r.id); + const allChecked = allIds.length > 0 && allIds.every((id) => checkedIds.includes(id)); + setCheckedIds(allChecked ? [] : allIds); + }} + > + 0 && orders.every((r) => checkedIds.includes(r.id))} + onCheckedChange={() => {}} + /> + + {ts.visibleColumns.map((col) => ( + + {col.label} ))} - {trailingMaster.map(renderMasterHead)} - {Object.entries(orderGroups).map(([purchaseNo, group]) => { - const isExpanded = expandedOrders.has(purchaseNo); - const detailIds = group.details.map(d => d.id); - const allChecked = detailIds.length > 0 && detailIds.every(id => checkedIds.includes(id)); - const someChecked = detailIds.some(id => checkedIds.includes(id)); - const m = group.master; - const totalQty = group.details.reduce((s, d) => s + (Number(d.order_qty) || 0), 0); - const totalAmt = group.details.reduce((s, d) => s + (Number(d.amount) || 0), 0); + {orders.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - setExpandedOrders(prev => { const next = new Set(prev); if (next.has(purchaseNo)) next.delete(purchaseNo); else next.add(purchaseNo); return next; })} - onDoubleClick={() => openEditModal(purchaseNo)} + { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + onDoubleClick={() => openEditModal(row.purchase_no)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} > - - + {}} /> + + {ts.visibleColumns.map((col) => ( + + {renderCell(row, col.key)} - { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !detailIds.includes(id)) : [...new Set([...prev, ...detailIds])]); }}> - {}} /> - - {leadingMaster.map(col => renderMasterCell(col, m, purchaseNo))} - {group.details.length}건 - {detailCols.map(col => ( - - {col.key === "order_qty" ? totalQty.toLocaleString() - : col.key === "amount" ? totalAmt.toLocaleString() - : ""} - - ))} - {trailingMaster.map(col => renderMasterCell(col, m, purchaseNo))} - - {isExpanded && group.details.map((row) => ( - - - { e.stopPropagation(); setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id]); }}> - {}} /> - - {leadingMaster.map(col => )} - - {detailCols.map(col => ( - - {renderDetailCell(row, col.key)} - - ))} - {trailingMaster.map(col => )} - ))} - + ); })} @@ -938,7 +880,7 @@ export default function PurchaseOrderPage() { })() )}
- 전체 {Object.keys(orderGroups).length}건 (발주 기준) / {orders.length}건 (품목 기준) + 전체 {orders.length}건
@@ -1094,7 +1036,7 @@ export default function PurchaseOrderPage() { ) : (
-
+
c.key)} strategy={horizontalListSortingStrategy}> @@ -1118,12 +1060,12 @@ export default function PurchaseOrderPage() { {visibleModalColumns.map((col) => { switch (col.key) { case "item_code": - return {row.item_code}; + return {row.item_code}; case "item_name": - return {row.item_name}; + return {row.item_name}; case "supplier": return ( - + {isReadOnly ? ( {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} ) : ( @@ -1133,7 +1075,7 @@ export default function PurchaseOrderPage() { updateDetailRow(idx, "supplier_code", v); updateDetailRow(idx, "supplier_name", name); }}> - + {(categoryOptions["supplier_code"] || []).map(o => ( {o.label} @@ -1149,11 +1091,11 @@ export default function PurchaseOrderPage() { return {row.unit}; case "order_qty": return ( - + {isReadOnly ? ( {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} ) : ( - updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" /> )} ); @@ -1163,11 +1105,11 @@ export default function PurchaseOrderPage() { return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; case "unit_price": return ( - + {isReadOnly ? ( {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} ) : ( - updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" /> )} ); @@ -1175,21 +1117,21 @@ export default function PurchaseOrderPage() { return {row.amount ? Number(row.amount).toLocaleString() : ""}; case "due_date": return ( - + {isReadOnly ? ( {row.due_date} ) : ( - updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> + updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs w-full" /> )} ); case "memo": return ( - + {isReadOnly ? ( {row.memo} ) : ( - updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> + updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs w-full" /> )} ); diff --git a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx index 05facbbc..863c0aef 100644 --- a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx @@ -424,9 +424,10 @@ export default function ItemInspectionInfoPage() { case "inspection_type": return (
- {group.types.map((t: string) => ( - {t} - ))} + {group.types.map((t: string) => { + const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t; + return {label}; + })}
); @@ -494,7 +495,7 @@ export default function ItemInspectionInfoPage() { : "bg-muted/50 text-muted-foreground border-border hover:bg-muted" )} > - {type} + {inspTypeCatOptions.find((o) => o.code === type)?.label || type} o.code === itemSearchDivision)?.label || ""; + // 코드 또는 라벨이 저장된 경우 모두 조회하기 위해 in 연산자 사용 + const divValues = [itemSearchDivision]; + if (divLabel) divValues.push(divLabel); + filters.push({ columnName: "division", operator: "in", value: divValues }); + } + // 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링 const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI"; const partnerId = masterForm.partner_id; @@ -632,7 +641,7 @@ export default function SalesOrderPage() { if (isCustomerPrice && partnerId) { try { const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] }, autoFilter: true, }); @@ -642,31 +651,22 @@ export default function SalesOrderPage() { } const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: p, size: s, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const resData = res.data?.data; - let allRows = resData?.data || resData?.rows || []; + let rows = resData?.data || resData?.rows || []; + const serverTotal = resData?.total || resData?.totalCount || rows.length; - // 거래처우선일 때 연결된 품목만 표시 + // 거래처우선일 때 연결된 품목만 표시 (클라이언트 필터) if (customerItemIds) { - allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); + rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); } - // 관리품목 필터 (코드/라벨 혼재 대응) - if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - allRows = allRows.filter((item: any) => { - const div = item.division || ""; - return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel)); - }); - } - const total = allRows.length; - const start = (p - 1) * s; - setItemSearchResults(allRows.slice(start, start + s)); - setItemTotal(total); - setItemTotalPages(Math.max(1, Math.ceil(total / s))); + setItemSearchResults(rows); + setItemTotal(serverTotal); + setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); } catch { /* skip */ } finally { setItemSearchLoading(false); } @@ -1320,44 +1320,44 @@ export default function SalesOrderPage() {
- No - 품번 - 품명 - 규격 - 재질 - 포장재 - 단위 - 수량 - 포장수량 - 단가 - 금액 - 납기일 - 분할/삭제 + No + 품번 + 품명 + 규격 + 재질 + 포장재 + 단위 + 수량 + 포장수량 + 단가 + 금액 + 납기일 + 분할/삭제 {detailRows.map((row, idx) => ( {idx + 1} - - {row.part_code} + + {row.part_code} - - {row.part_name} + + {row.part_name} - {row.spec} - {row.material} + {row.spec} + {row.material} updateDetailRow(idx, "packing_material", e.target.value)} placeholder="포장재" - className="h-8 text-xs" + className="h-8 text-xs w-full" /> - - - 전체 - {(categoryOptions["item_division"] || []).map((o) => ( - {o.label} - ))} - - +
영업관리
diff --git a/frontend/app/(main)/COMPANY_29/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_29/logistics/outbound/page.tsx index 824e2e86..457e2723 100644 --- a/frontend/app/(main)/COMPANY_29/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_29/logistics/outbound/page.tsx @@ -38,7 +38,6 @@ import { Save, ChevronRight, ChevronLeft, - ChevronDown, ChevronsLeft, ChevronsRight, Inbox, @@ -116,41 +115,8 @@ const GRID_COLUMNS = [ { key: "remark", label: "비고" }, ]; -// 마스터 헤더 레이아웃 (출고번호 뒤) -const MASTER_BODY_LAYOUT = [ - { key: "outbound_type", label: "출고유형", colSpan: 1 }, - { key: "outbound_date", label: "출고일", colSpan: 1 }, - { key: "reference_number", label: "참조번호", colSpan: 1 }, - { key: "customer_name", label: "거래처", colSpan: 1 }, - { key: "warehouse_name", label: "창고", colSpan: 1 }, - { key: "outbound_status", label: "출고상태", colSpan: 1 }, - { key: "memo", label: "비고", colSpan: 1 }, -]; - -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "source_type", label: "출처" }, - { key: "item_code", label: "품목코드" }, - { key: "item_name", label: "품목명" }, - { key: "specification", label: "규격" }, - { key: "outbound_qty", label: "출고수량" }, - { key: "unit_price", label: "단가" }, - { key: "total_amount", label: "금액" }, -]; - -// 마스터 필드 키 목록 (필터 분류용) -const MASTER_KEYS = new Set(["outbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); - -// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) -const DETAIL_KEY_MAP: Record = { - source_type: "source_type", - item_number: "item_code", - item_name: "item_name", - spec: "specification", - outbound_qty: "outbound_qty", - unit_price: "unit_price", - total_amount: "total_amount", -}; +// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16 +const TOTAL_COLS = 16; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -258,30 +224,6 @@ interface SelectedSourceItem { export default function OutboundPage() { const ts = useTableSettings("c16-outbound", "outbound_mng", GRID_COLUMNS); - // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 - const visibleMasterLayout = useMemo(() => { - const ordered: typeof MASTER_BODY_LAYOUT = []; - for (const vc of ts.visibleColumns) { - const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); - if (m) ordered.push(m); - } - return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; - }, [ts.visibleColumns]); - - const visibleDetailCols = useMemo(() => { - const ordered: typeof DETAIL_HEADER_COLS = []; - for (const vc of ts.visibleColumns) { - const detailKey = DETAIL_KEY_MAP[vc.key]; - if (detailKey) { - const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); - if (d) ordered.push(d); - } - } - return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; - }, [ts.visibleColumns]); - - const TOTAL_COLS = 3 + visibleMasterLayout.length; - // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -290,9 +232,7 @@ export default function OutboundPage() { // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); - // 마스터-디테일 그룹 테이블 state - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); + // 헤더 필터 & 정렬 const [headerFilters, setHeaderFilters] = useState>>({}); const [sortState, setSortState] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -403,121 +343,59 @@ export default function OutboundPage() { })(); }, []); - // --- 마스터-디테일 그룹핑, 필터, 정렬 --- + // 플랫 행 생성 (출고유형 코드→라벨 변환, source_type→한글 등) + const flatRows = useMemo(() => { + return data.map((row) => ({ + ...row, + _raw_outbound_type: row.outbound_type, + outbound_type: resolveCat("outbound_type", row.outbound_type) || row.outbound_type || "", + source_type: row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "", + item_number: row.item_code || (row as any).item_number || "", + spec: row.specification || (row as any).spec || "", + })); + }, [data, resolveCat]); - // outbound_number 기준 그룹핑 + 필터 + 정렬 - const filteredGroups = useMemo(() => { - // 1차: outbound_number 기준 그룹핑 - const allGroups: Record = {}; - for (const row of data) { - const key = row.outbound_number || "_no_number"; - if (!allGroups[key]) { - allGroups[key] = { master: row, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - const raw = (group.master as any)?.[colKey] ?? ""; - return values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([outNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - let cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; - if (colKey === "source_type") cellVal = SOURCE_TYPE_LABEL[cellVal] || cellVal; - return values.has(cellVal); - }) - ); - return [outNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - entries.sort(([, a], [, b]) => { - const av = (a.master as any)?.[key] ?? ""; - const bv = (b.master as any)?.[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - let av: any = (a as any)[key] ?? ""; - let bv: any = (b as any)[key] ?? ""; - if (key === "source_type") { av = SOURCE_TYPE_LABEL[av] || av; bv = SOURCE_TYPE_LABEL[bv] || bv; } - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [data, headerFilters, sortState]); - - // 마스터 컬럼별 고유값 (헤더 필터용) - const masterUniqueValues = useMemo(() => { + // 컬럼별 고유값 (헤더 필터용) + const columnUniqueValues = useMemo(() => { const result: Record = {}; - const seenMasters = new Map(); - data.forEach((row) => { - if (row.outbound_number && !seenMasters.has(row.outbound_number)) { - seenMasters.set(row.outbound_number, row); - } - }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "outbound_number", label: "출고번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label }))]) { + for (const col of GRID_COLUMNS) { const values = new Set(); - masters.forEach((m) => { - const val = (m as any)?.[col.key]; + flatRows.forEach((row) => { + const val = (row as any)[col.key]; if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [data]); + }, [flatRows]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) - const columnUniqueValues = useMemo(() => { - const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { - const values = new Set(); - data.forEach((row) => { - let val = (row as any)[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "source_type") val = SOURCE_TYPE_LABEL[val] || val; - values.add(String(val)); - } + // 필터 + 정렬 적용된 플랫 데이터 + const filteredRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; + return values.has(cellVal); }); - result[col.key] = Array.from(values).sort(); } - return result; - }, [data]); + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = (a as any)[key] ?? ""; + const bv = (b as any)[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -938,7 +816,7 @@ export default function OutboundPage() { dataCount={data.length} /> - {/* 출고 목록 테이블 */} + {/* 출고 목록 테이블 (플랫 리스트) */}
{/* 패널 헤더 */}
@@ -946,7 +824,7 @@ export default function OutboundPage() { 출고 목록 - {data.length}건 + {filteredRows.length}건
@@ -971,78 +849,68 @@ export default function OutboundPage() {
-
+
- - {visibleMasterLayout.map((col) => ( - - ))} + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 출고번호 */} - -
-
handleSort("outbound_number")}> - 출고번호 - {sortState?.key === "outbound_number" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["outbound_number"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {GRID_COLUMNS.map((col) => { + const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} + + ); + })} @@ -1052,7 +920,7 @@ export default function OutboundPage() { - ) : Object.keys(filteredGroups).length === 0 ? ( + ) : filteredRows.length === 0 ? (
@@ -1062,214 +930,59 @@ export default function OutboundPage() { ) : ( - Object.entries(filteredGroups).map(([outboundNo, group]) => { - const isExpanded = expandedOrders.has(outboundNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 */} - { - if (expandedOrders.has(outboundNo)) { - setClosingOrders((prev) => new Set(prev).add(outboundNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(outboundNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(outboundNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(outboundNo)); - } - }} - onDoubleClick={() => openEditModal(group.details[0])} - > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 출고번호 */} - - {outboundNo} - ({group.details.length}) - - {/* 마스터 필드 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => { - switch (col.key) { - case "outbound_type": return ( - - - {resolveCat("outbound_type", master.outbound_type) || "-"} - - - ); - case "outbound_date": return ( - - {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} - - ); - case "reference_number": return ( - - {master.reference_number || ""} - - ); - case "customer_name": return ( - - {master.customer_name || ""} - - ); - case "warehouse_name": return ( - - {master.warehouse_name || master.warehouse_code || ""} - - ); - case "outbound_status": return ( - - - {master.outbound_status || "-"} - - - ); - case "memo": return ( - - {master.memo || ""} - - ); - default: return {(master as any)[col.key] ?? ""}; - } - })} - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - - {visibleDetailCols.map((col) => { - const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => { - let v = (d as any)[col.key]; - if (col.key === "source_type") v = SOURCE_TYPE_LABEL[v] || v; - return v; - }).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} -
+ { - const isClosing = closingOrders.has(outboundNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - onDoubleClick={() => openEditModal(row)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - - {visibleDetailCols.map((col) => { - switch (col.key) { - case "source_type": return {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"}; - case "item_code": return {row.item_code || ""}; - case "item_name": return {row.item_name || ""}; - case "specification": return {row.specification || ""}; - case "outbound_qty": return {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}; - case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; - case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; - default: return {(row as any)[col.key] ?? ""}; - } - })} - + onClick={() => { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] ); - })} - + }} + onDoubleClick={() => openEditModal(row as any)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + > + {}} /> + + {row.outbound_number || ""} + + + {row.outbound_type || "-"} + + + + {row.outbound_date ? new Date(row.outbound_date).toLocaleDateString("ko-KR") : ""} + + {row.reference_number || ""} + {row.source_type || ""} + {row.customer_name || ""} + {row.item_number || ""} + {row.item_name || ""} + {row.spec || ""} + {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {row.warehouse_name || row.warehouse_code || ""} + + + {row.outbound_status || "-"} + + + {row.remark || row.memo || ""} + ); }) )} diff --git a/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx index 4ec40482..77d21e7b 100644 --- a/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx @@ -43,7 +43,6 @@ import { X, Save, ChevronRight, - ChevronDown, ChevronLeft, ChevronsLeft, ChevronsRight, @@ -64,6 +63,7 @@ import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSea import { getReceivingList, createReceiving, + updateReceiving, deleteReceiving, generateReceivingNumber, getReceivingWarehouses, @@ -95,41 +95,8 @@ const GRID_COLUMNS = [ { key: "remark", label: "비고" }, ]; -// 마스터 헤더 레이아웃 (입고번호 뒤, 디테일 7컬럼 위에 colSpan으로 맵핑) -const MASTER_BODY_LAYOUT = [ - { key: "inbound_type", label: "입고유형", colSpan: 1 }, - { key: "inbound_date", label: "입고일", colSpan: 1 }, - { key: "reference_number", label: "참조번호", colSpan: 1 }, - { key: "supplier_name", label: "공급처", colSpan: 1 }, - { key: "warehouse_name", label: "창고", colSpan: 1 }, - { key: "inbound_status", label: "입고상태", colSpan: 1 }, - { key: "memo", label: "비고", colSpan: 1 }, -]; - -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "source_table", label: "출처" }, - { key: "item_number", label: "품목코드" }, - { key: "item_name", label: "품목명" }, - { key: "spec", label: "규격" }, - { key: "inbound_qty", label: "입고수량" }, - { key: "unit_price", label: "단가" }, - { key: "total_amount", label: "금액" }, -]; - -// 마스터 필드 키 목록 (필터 분류용) -const MASTER_KEYS = new Set(["inbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); - -// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) -const DETAIL_KEY_MAP: Record = { - source_type: "source_table", - item_number: "item_number", - item_name: "item_name", - spec: "spec", - inbound_qty: "inbound_qty", - unit_price: "unit_price", - total_amount: "total_amount", -}; +// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16 +const TOTAL_COLS = 16; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -287,30 +254,6 @@ interface SelectedSourceItem { export default function ReceivingPage() { const ts = useTableSettings("c16-receiving", "inbound_mng", GRID_COLUMNS); - // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 - const visibleMasterLayout = useMemo(() => { - const ordered: typeof MASTER_BODY_LAYOUT = []; - for (const vc of ts.visibleColumns) { - const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); - if (m) ordered.push(m); - } - return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; - }, [ts.visibleColumns]); - - const visibleDetailCols = useMemo(() => { - const ordered: typeof DETAIL_HEADER_COLS = []; - for (const vc of ts.visibleColumns) { - const detailKey = DETAIL_KEY_MAP[vc.key]; - if (detailKey) { - const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); - if (d) ordered.push(d); - } - } - return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; - }, [ts.visibleColumns]); - - const TOTAL_COLS = 3 + visibleMasterLayout.length; - // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -319,9 +262,7 @@ export default function ReceivingPage() { // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); - // 마스터-디테일 그룹 테이블 - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); + // 헤더 필터 & 정렬 const [headerFilters, setHeaderFilters] = useState>>({}); const [sortState, setSortState] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -338,6 +279,10 @@ export default function ReceivingPage() { const [selectedItems, setSelectedItems] = useState([]); const [saving, setSaving] = useState(false); + // 수정 모드 + const [editMode, setEditMode] = useState(false); + const [editItemIds, setEditItemIds] = useState([]); + // 소스 데이터 const [sourceKeyword, setSourceKeyword] = useState(""); const [sourceLoading, setSourceLoading] = useState(false); @@ -377,7 +322,7 @@ export default function ReceivingPage() { Promise.all( ["material", "unit"].map(async (col) => { try { - const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_29`); + const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`); const items = flatten(res.data?.data || []); map[col] = {}; for (const item of items) map[col][item.code] = item.label; @@ -434,124 +379,56 @@ export default function ReceivingPage() { })(); }, []); - // 필터 + 정렬 적용된 데이터 -> 그룹핑 - const filteredGroups = useMemo(() => { - // 1차: inbound_number 기준 그룹핑 - const allGroups: Record = {}; - for (const row of data) { - const key = row.inbound_number || "_no_inbound"; - if (!allGroups[key]) { - allGroups[key] = { master: row, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - let raw = (group.master as any)?.[colKey] ?? ""; - // 입고유형은 코드→라벨 변환된 값으로 비교 - if (colKey === "inbound_type") raw = resolveInboundType(String(raw)); - return values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([inboundNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - let cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; - if (colKey === "source_table") cellVal = SOURCE_TABLE_LABEL[cellVal] || cellVal; - return values.has(cellVal); - }) - ); - return [inboundNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - entries.sort(([, a], [, b]) => { - let av: any = (a.master as any)?.[key] ?? ""; - let bv: any = (b.master as any)?.[key] ?? ""; - if (key === "inbound_type") { av = resolveInboundType(String(av)); bv = resolveInboundType(String(bv)); } - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - const av = (a as any)[key] ?? ""; - const bv = (b as any)[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [data, headerFilters, sortState]); - - // 마스터 컬럼별 고유값 (마스터 헤더 필터용) - const masterUniqueValues = useMemo(() => { - const result: Record = {}; - const seenMasters = new Map(); - data.forEach((row) => { - if (row.inbound_number && !seenMasters.has(row.inbound_number)) { - seenMasters.set(row.inbound_number, row); - } - }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "inbound_number", label: "입고번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label }))]) { - const values = new Set(); - masters.forEach((m) => { - let val = (m as any)?.[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "inbound_type") val = resolveInboundType(String(val)); - values.add(String(val)); - } - }); - result[col.key] = Array.from(values).sort(); - } - return result; + // 플랫 행 생성 (입고유형 코드→라벨 변환, source_table→한글 등) + const flatRows = useMemo(() => { + return data.map((row) => ({ + ...row, + inbound_type: resolveInboundType(row.inbound_type), + source_type: row.source_table ? (SOURCE_TABLE_LABEL[row.source_table] || row.source_table) : (row as any).source_type || "", + })); }, [data]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) + // 컬럼별 고유값 (헤더 필터용) const columnUniqueValues = useMemo(() => { const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { + for (const col of GRID_COLUMNS) { const values = new Set(); - data.forEach((row) => { - let val = (row as any)[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "source_table") val = SOURCE_TABLE_LABEL[val] || val; - values.add(String(val)); - } + flatRows.forEach((row) => { + const val = (row as any)[col.key]; + if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [data]); + }, [flatRows]); + + // 필터 + 정렬 적용된 플랫 데이터 + const filteredRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; + return values.has(cellVal); + }); + } + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = (a as any)[key] ?? ""; + const bv = (b as any)[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -633,6 +510,8 @@ export default function ReceivingPage() { const openRegisterModal = async () => { const defaultType = "구매입고"; + setEditMode(false); + setEditItemIds([]); setModalInboundType(defaultType); setModalInboundDate(new Date().toISOString().split("T")[0]); setModalWarehouse(""); @@ -661,6 +540,51 @@ export default function ReceivingPage() { } }; + // 수정 모달 열기 + const openEditModal = (row: InboundItem) => { + const inNo = row.inbound_number; + const grouped = data.filter((d) => d.inbound_number === inNo); + const first = grouped[0] || row; + + setEditMode(true); + setEditItemIds(grouped.map((g) => g.id)); + setModalInboundNo(inNo); + setModalInboundType(first.inbound_type || "구매입고"); + setModalInboundDate(first.inbound_date ? String(first.inbound_date).slice(0, 10) : ""); + setModalWarehouse(first.warehouse_code || ""); + setModalLocation(first.location_code || ""); + setModalInspector((first as any).inspector || ""); + setModalManager((first as any).manager || ""); + setModalMemo(first.memo || ""); + setSelectedItems( + grouped.map((g) => ({ + key: g.id, + inbound_type: g.inbound_type || "", + reference_number: g.reference_number || "", + supplier_code: (g as any).supplier_code || "", + supplier_name: g.supplier_name || "", + item_number: g.item_number || "", + item_name: g.item_name || "", + spec: g.spec || "", + material: (g as any).material || "", + unit: (g as any).unit || "", + inbound_qty: Number(g.inbound_qty) || 0, + unit_price: Number(g.unit_price) || 0, + total_amount: Number(g.total_amount) || 0, + source_table: (g as any).source_table || "", + source_id: (g as any).source_id || "", + })) + ); + setSourceKeyword(""); + setPurchaseOrders([]); + setShipments([]); + setItems([]); + setSourcePage(1); + setSourceTotalCount(0); + setIsModalOpen(true); + loadSourceData(first.inbound_type || "구매입고", undefined, 1); + }; + // 검색 버튼 클릭 시 const searchSourceData = useCallback(async () => { setSourcePage(1); @@ -811,41 +735,97 @@ export default function ReceivingPage() { } setSaving(true); try { - const res = await createReceiving({ - inbound_number: modalInboundNo, - inbound_date: modalInboundDate, - warehouse_code: modalWarehouse, - location_code: modalLocation || undefined, - inspector: modalInspector || undefined, - manager: modalManager || undefined, - memo: modalMemo || undefined, - items: selectedItems.map((item) => ({ - inbound_type: item.inbound_type, - reference_number: item.reference_number, - supplier_code: item.supplier_code, - supplier_name: item.supplier_name, - item_number: item.item_number, - item_name: item.item_name, - spec: item.spec, - material: item.material, - unit: item.unit, - inbound_qty: item.inbound_qty, - unit_price: item.unit_price, - total_amount: item.total_amount, - source_table: item.source_table, - source_id: item.source_id, - inbound_status: "입고완료", - inspection_status: "대기", - })), - }); + if (editMode) { + // 기존 item 수정 + 삭제된 item 삭제 + 새 item 추가 + const currentKeys = new Set(selectedItems.map((i) => i.key)); + const toDelete = editItemIds.filter((id) => !currentKeys.has(id)); + const toUpdate = selectedItems.filter((i) => editItemIds.includes(i.key)); + const toCreate = selectedItems.filter((i) => !editItemIds.includes(i.key)); - if (res.success) { - alert(res.message || "입고 등록 완료"); + await Promise.all([ + ...toDelete.map((id) => deleteReceiving(id)), + ...toUpdate.map((item) => + updateReceiving(item.key, { + inbound_date: modalInboundDate, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + warehouse_code: modalWarehouse || undefined, + location_code: modalLocation || undefined, + memo: modalMemo || undefined, + } as any) + ), + ...(toCreate.length > 0 + ? [createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: toCreate.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + })] + : []), + ]); + toast.success("입고 정보를 수정했어요"); setIsModalOpen(false); fetchList(); + } else { + const res = await createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: selectedItems.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + }); + + if (res.success) { + toast.success(res.message || "입고 등록 완료"); + setIsModalOpen(false); + fetchList(); + } } - } catch { - alert("입고 등록 중 오류가 발생했습니다."); + } catch (err: any) { + const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "입고 등록 중 오류가 발생했습니다."); + toast.error(msg); } finally { setSaving(false); } @@ -897,13 +877,13 @@ export default function ReceivingPage() { } /> - {/* 입고 목록 테이블 (마스터-디테일 그룹) */} + {/* 입고 목록 테이블 (플랫 리스트) */}

입고 목록

- {Object.keys(filteredGroups).length}건 + {filteredRows.length}건
-
+
- - {visibleMasterLayout.map((col) => ( - - ))} + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 입고번호 (별도 컬럼) */} - -
-
handleSort("inbound_number")}> - 입고번호 - {sortState?.key === "inbound_number" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["inbound_number"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {GRID_COLUMNS.map((col) => { + const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} + + ); + })} @@ -993,7 +963,7 @@ export default function ReceivingPage() { - ) : Object.keys(filteredGroups).length === 0 ? ( + ) : filteredRows.length === 0 ? (
@@ -1003,212 +973,59 @@ export default function ReceivingPage() { ) : ( - Object.entries(filteredGroups).map(([inboundNo, group]) => { - const isExpanded = expandedOrders.has(inboundNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 */} - { - if (expandedOrders.has(inboundNo)) { - setClosingOrders((prev) => new Set(prev).add(inboundNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(inboundNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(inboundNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(inboundNo)); - } + { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + onDoubleClick={() => openEditModal(row as any)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); }} > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 입고번호 */} - - {inboundNo} - ({group.details.length}) - - {/* 마스터 필드 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => { - switch (col.key) { - case "inbound_type": return ( - - - {resolveInboundType(master.inbound_type)} - - - ); - case "inbound_date": return ( - - {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} - - ); - case "reference_number": return ( - - {master.reference_number || ""} - - ); - case "supplier_name": return ( - - {master.supplier_name || ""} - - ); - case "warehouse_name": return ( - - {master.warehouse_name || master.warehouse_code || ""} - - ); - case "inbound_status": return ( - - - {master.inbound_status || "-"} - - - ); - case "memo": return ( - - {master.memo || ""} - - ); - default: return {(master as any)[col.key] ?? ""}; - } - })} - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - - {visibleDetailCols.map((col) => { - const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => { - let v = (d as any)[col.key]; - if (col.key === "source_table") v = SOURCE_TABLE_LABEL[v] || v; - return v; - }).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} -
- )} - - {/* 디테일 행 (펼쳤을 때만) */} - {isExpanded && group.details.map((row, detailIdx) => { - const isClosing = closingOrders.has(inboundNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - - {visibleDetailCols.map((col) => { - switch (col.key) { - case "source_table": return {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"}; - case "item_number": return {row.item_number || ""}; - case "item_name": return {row.item_name || ""}; - case "spec": return {row.spec || ""}; - case "inbound_qty": return {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}; - case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; - case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; - default: return {(row as any)[col.key] ?? ""}; - } - })} - - ); - })} - + {}} /> + + {row.inbound_number || ""} + + + {row.inbound_type || "-"} + + + + {row.inbound_date ? new Date(row.inbound_date).toLocaleDateString("ko-KR") : ""} + + {row.reference_number || ""} + {row.source_type || ""} + {row.supplier_name || ""} + {row.item_number || ""} + {row.item_name || ""} + {row.spec || ""} + {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {row.warehouse_name || (row as any).warehouse_code || ""} + + + {row.inbound_status || "-"} + + + {row.remark || row.memo || ""} + ); }) )} @@ -1221,9 +1038,9 @@ export default function ReceivingPage() { - 입고 등록 + {editMode ? "입고 수정" : "입고 등록"} - 입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해 주세요. + {editMode ? "입고 정보를 수정해 주세요." : "입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해 주세요."}
diff --git a/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx index 38b01658..534582f9 100644 --- a/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx @@ -1053,14 +1053,14 @@ export default function ProductionPlanManagementPage() { return (
- + - + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - - 품목코드 - 품목명 + + 품목코드 + 품목명 {ts.visibleColumns.map((col) => ( {col.label} @@ -1073,9 +1073,9 @@ export default function ProductionPlanManagementPage() { if (item._isGroupSummary) { return ( - - - + + + {ts.visibleColumns.map((col) => { const v = (item as any)[col.key]; return ( @@ -1090,14 +1090,14 @@ export default function ProductionPlanManagementPage() { return ( - e.stopPropagation()}> + e.stopPropagation()}> toggleItemGroupSelect(item.item_code)} className="h-4 w-4" /> - toggleItemExpand(item.item_code)}> + toggleItemExpand(item.item_code)}> - toggleItemExpand(item.item_code)}>{item.item_code} - toggleItemExpand(item.item_code)}>{item.item_name} + toggleItemExpand(item.item_code)}>{item.item_code} + toggleItemExpand(item.item_code)}>{item.item_name} {ts.visibleColumns.map((col) => renderGroupCell(col, item))} {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => { @@ -1107,9 +1107,9 @@ export default function ProductionPlanManagementPage() { } return ( - - - + + +
수주번호: {detail.order_no} diff --git a/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx index 143a84a9..f712d3ed 100644 --- a/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx @@ -13,7 +13,7 @@ import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, - ClipboardList, Pencil, Search, X, Package, ChevronDown, + ClipboardList, Pencil, Search, X, Package, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, GripVertical, } from "lucide-react"; @@ -88,18 +88,18 @@ const GRID_COLUMNS_CONFIG = [ ]; const MODAL_DETAIL_COLUMNS = [ - { key: "item_code", label: "품번", width: "w-[120px]" }, - { key: "item_name", label: "품명", width: "w-[120px]" }, - { key: "supplier", label: "공급업체", width: "w-[150px]" }, - { key: "spec", label: "규격", width: "w-[80px]" }, - { key: "unit", label: "단위", width: "w-[60px]" }, - { key: "order_qty", label: "발주수량", width: "w-[90px]" }, - { key: "received_qty", label: "입고수량", width: "w-[90px]" }, - { key: "remain_qty", label: "잔량", width: "w-[80px]" }, - { key: "unit_price", label: "단가", width: "w-[100px]" }, - { key: "amount", label: "금액", width: "w-[100px]" }, - { key: "due_date", label: "납기일", width: "w-[160px]" }, - { key: "memo", label: "메모", width: "w-[120px]" }, + { key: "item_code", label: "품번", width: "min-w-[120px]" }, + { key: "item_name", label: "품명", width: "min-w-[150px]" }, + { key: "supplier", label: "공급업체", width: "min-w-[150px]" }, + { key: "spec", label: "규격", width: "min-w-[80px]" }, + { key: "unit", label: "단위", width: "min-w-[90px]" }, + { key: "order_qty", label: "발주수량", width: "min-w-[90px]" }, + { key: "received_qty", label: "입고수량", width: "min-w-[90px]" }, + { key: "remain_qty", label: "잔량", width: "min-w-[80px]" }, + { key: "unit_price", label: "단가", width: "min-w-[100px]" }, + { key: "amount", label: "금액", width: "min-w-[100px]" }, + { key: "due_date", label: "납기일", width: "min-w-[160px]" }, + { key: "memo", label: "메모", width: "min-w-[120px]" }, ]; const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16"; @@ -162,7 +162,6 @@ export default function PurchaseOrderPage() { const [excelUploadOpen, setExcelUploadOpen] = useState(false); const [categoryOptions, setCategoryOptions] = useState>({}); const [checkedIds, setCheckedIds] = useState([]); - const [expandedOrders, setExpandedOrders] = useState>(new Set()); // 테이블 설정 const ts = useTableSettings("c16-purchase-order", DETAIL_TABLE, GRID_COLUMNS_CONFIG); @@ -238,7 +237,7 @@ export default function PurchaseOrderPage() { ); try { const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, size: 5000, autoFilter: true, }); const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || []; optMap["supplier_code"] = supps.map((s: any) => ({ @@ -248,7 +247,7 @@ export default function PurchaseOrderPage() { } catch { /* skip */ } try { const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, size: 5000, autoFilter: true, }); const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; optMap["manager"] = users.map((u: any) => ({ @@ -294,7 +293,7 @@ export default function PurchaseOrderPage() { } const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined, autoFilter: true, sort: { columnName: "purchase_no", order: "desc" }, @@ -372,20 +371,6 @@ export default function PurchaseOrderPage() { useEffect(() => { fetchOrders(); }, [fetchOrders]); // purchase_no 기준 그룹핑 - const orderGroups = useMemo(() => { - const map: Record = {}; - for (const row of orders) { - const key = row.purchase_no || row.id; - if (!map[key]) { - map[key] = { - master: { purchase_no: row.purchase_no, order_date: row.order_date, supplier_name: row.supplier_name, status: row.status, memo: row.memo }, - details: [], - }; - } - map[key].details.push(row); - } - return map; - }, [orders]); const getCategoryLabel = (col: string, code: string) => { if (!code) return ""; @@ -553,7 +538,7 @@ export default function PurchaseOrderPage() { } }; - // 품목 검색 + // 품목 검색 (수주관리와 동일한 서버 페이징 방식) const searchItems = async (page?: number, size?: number) => { const p = page ?? itemPage; const s = size ?? itemPageSize; @@ -563,25 +548,24 @@ export default function PurchaseOrderPage() { if (itemSearchKeyword) { filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); } + // 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응) + if (itemSearchDivision !== "all") { + const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; + const divValues = [itemSearchDivision]; + if (divLabel) divValues.push(divLabel); + filters.push({ columnName: "division", operator: "in", value: divValues }); + } const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: p, size: s, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const resData = res.data?.data; - let allRows = resData?.data || resData?.rows || []; - if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - allRows = allRows.filter((item: any) => { - const div = item.division || ""; - return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel)); - }); - } - const total = allRows.length; - const start = (p - 1) * s; - setItemSearchResults(allRows.slice(start, start + s)); - setItemTotal(total); - setItemTotalPages(Math.max(1, Math.ceil(total / s))); + const rows = resData?.data || resData?.rows || []; + const serverTotal = resData?.total || resData?.totalCount || rows.length; + setItemSearchResults(rows); + setItemTotal(serverTotal); + setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); } catch { /* skip */ } finally { setItemSearchLoading(false); } @@ -623,7 +607,7 @@ export default function PurchaseOrderPage() { try { const itemIds = selected.map((item) => item.item_number || item.id); const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [ @@ -686,7 +670,7 @@ export default function PurchaseOrderPage() { if (itemCodes.length === 0) return; try { const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, autoFilter: true, }); @@ -708,7 +692,7 @@ export default function PurchaseOrderPage() { if (itemCodes.length === 0) return; try { const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [ { columnName: "supplier_id", operator: "equals", value: supplierCode }, { columnName: "item_id", operator: "in", value: itemCodes }, @@ -811,125 +795,83 @@ export default function PurchaseOrderPage() {
- {/* 데이터 테이블 — 아코디언 그룹핑 */} + {/* 데이터 테이블 — 플랫 리스트 */}
{loading ? (
- ) : Object.keys(orderGroups).length === 0 ? ( + ) : orders.length === 0 ? (

등록된 발주가 없어요

) : ( (() => { - const MASTER_KEYS = new Set(["purchase_no", "order_date", "supplier_name", "status", "memo"]); const numCols = new Set(["order_qty", "received_qty", "remain_qty", "unit_price", "amount"]); - - // ts.visibleColumns 순서를 따르되, 마스터/디테일 컬럼을 분리 - // 고정 컬럼(품목수)은 마스터 선행 컬럼 뒤에 배치 - const leadingMaster: typeof ts.visibleColumns = []; - const detailCols: typeof ts.visibleColumns = []; - const trailingMaster: typeof ts.visibleColumns = []; - let passedFirstDetail = false; - for (const col of ts.visibleColumns) { - if (MASTER_KEYS.has(col.key)) { - if (passedFirstDetail) trailingMaster.push(col); - else leadingMaster.push(col); - } else { - passedFirstDetail = true; - detailCols.push(col); - } - } - - const renderDetailCell = (row: any, key: string) => { + const renderCell = (row: any, key: string) => { const val = row[key]; if (key === "status") return val ? {val} : "-"; - if (numCols.has(key)) return {val ? Number(val).toLocaleString() : "0"}; - return val || "-"; + if (key === "order_date" || key === "due_date") return val ? new Date(val).toLocaleDateString("ko-KR") : "-"; + if (numCols.has(key)) return {val ? Number(val).toLocaleString() : ""}; + return val || ""; }; - - const renderMasterHead = (col: { key: string; label: string }) => ( - - {col.label} - - ); - - const renderMasterCell = (col: { key: string }, m: any, purchaseNo: string) => { - if (col.key === "purchase_no") return {purchaseNo}; - if (col.key === "order_date") return {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}; - if (col.key === "supplier_name") return {m.supplier_name || "-"}; - if (col.key === "status") return {m.status && {m.status}}; - if (col.key === "memo") return {m.memo || ""}; - return ; - }; - return (
- - - {leadingMaster.map(renderMasterHead)} - 품목수 - {detailCols.map(col => ( - - {col.label}{col.key === "order_qty" || col.key === "amount" ? " 합계" : ""} + { + const allIds = orders.map((r) => r.id); + const allChecked = allIds.length > 0 && allIds.every((id) => checkedIds.includes(id)); + setCheckedIds(allChecked ? [] : allIds); + }} + > + 0 && orders.every((r) => checkedIds.includes(r.id))} + onCheckedChange={() => {}} + /> + + {ts.visibleColumns.map((col) => ( + + {col.label} ))} - {trailingMaster.map(renderMasterHead)} - {Object.entries(orderGroups).map(([purchaseNo, group]) => { - const isExpanded = expandedOrders.has(purchaseNo); - const detailIds = group.details.map(d => d.id); - const allChecked = detailIds.length > 0 && detailIds.every(id => checkedIds.includes(id)); - const someChecked = detailIds.some(id => checkedIds.includes(id)); - const m = group.master; - const totalQty = group.details.reduce((s, d) => s + (Number(d.order_qty) || 0), 0); - const totalAmt = group.details.reduce((s, d) => s + (Number(d.amount) || 0), 0); + {orders.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - setExpandedOrders(prev => { const next = new Set(prev); if (next.has(purchaseNo)) next.delete(purchaseNo); else next.add(purchaseNo); return next; })} - onDoubleClick={() => openEditModal(purchaseNo)} + { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + onDoubleClick={() => openEditModal(row.purchase_no)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} > - - + {}} /> + + {ts.visibleColumns.map((col) => ( + + {renderCell(row, col.key)} - { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !detailIds.includes(id)) : [...new Set([...prev, ...detailIds])]); }}> - {}} /> - - {leadingMaster.map(col => renderMasterCell(col, m, purchaseNo))} - {group.details.length}건 - {detailCols.map(col => ( - - {col.key === "order_qty" ? totalQty.toLocaleString() - : col.key === "amount" ? totalAmt.toLocaleString() - : ""} - - ))} - {trailingMaster.map(col => renderMasterCell(col, m, purchaseNo))} - - {isExpanded && group.details.map((row) => ( - - - { e.stopPropagation(); setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id]); }}> - {}} /> - - {leadingMaster.map(col => )} - - {detailCols.map(col => ( - - {renderDetailCell(row, col.key)} - - ))} - {trailingMaster.map(col => )} - ))} - + ); })} @@ -938,7 +880,7 @@ export default function PurchaseOrderPage() { })() )}
- 전체 {Object.keys(orderGroups).length}건 (발주 기준) / {orders.length}건 (품목 기준) + 전체 {orders.length}건
@@ -1094,7 +1036,7 @@ export default function PurchaseOrderPage() { ) : (
-
+
c.key)} strategy={horizontalListSortingStrategy}> @@ -1118,12 +1060,12 @@ export default function PurchaseOrderPage() { {visibleModalColumns.map((col) => { switch (col.key) { case "item_code": - return {row.item_code}; + return {row.item_code}; case "item_name": - return {row.item_name}; + return {row.item_name}; case "supplier": return ( - + {isReadOnly ? ( {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} ) : ( @@ -1133,7 +1075,7 @@ export default function PurchaseOrderPage() { updateDetailRow(idx, "supplier_code", v); updateDetailRow(idx, "supplier_name", name); }}> - + {(categoryOptions["supplier_code"] || []).map(o => ( {o.label} @@ -1149,11 +1091,11 @@ export default function PurchaseOrderPage() { return {row.unit}; case "order_qty": return ( - + {isReadOnly ? ( {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} ) : ( - updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" /> )} ); @@ -1163,11 +1105,11 @@ export default function PurchaseOrderPage() { return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; case "unit_price": return ( - + {isReadOnly ? ( {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} ) : ( - updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" /> )} ); @@ -1175,21 +1117,21 @@ export default function PurchaseOrderPage() { return {row.amount ? Number(row.amount).toLocaleString() : ""}; case "due_date": return ( - + {isReadOnly ? ( {row.due_date} ) : ( - updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> + updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs w-full" /> )} ); case "memo": return ( - + {isReadOnly ? ( {row.memo} ) : ( - updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> + updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs w-full" /> )} ); diff --git a/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx index 05facbbc..863c0aef 100644 --- a/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx @@ -424,9 +424,10 @@ export default function ItemInspectionInfoPage() { case "inspection_type": return (
- {group.types.map((t: string) => ( - {t} - ))} + {group.types.map((t: string) => { + const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t; + return {label}; + })}
); @@ -494,7 +495,7 @@ export default function ItemInspectionInfoPage() { : "bg-muted/50 text-muted-foreground border-border hover:bg-muted" )} > - {type} + {inspTypeCatOptions.find((o) => o.code === type)?.label || type} o.code === itemSearchDivision)?.label || ""; + // 코드 또는 라벨이 저장된 경우 모두 조회하기 위해 in 연산자 사용 + const divValues = [itemSearchDivision]; + if (divLabel) divValues.push(divLabel); + filters.push({ columnName: "division", operator: "in", value: divValues }); + } + // 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링 const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI"; const partnerId = masterForm.partner_id; @@ -632,7 +641,7 @@ export default function SalesOrderPage() { if (isCustomerPrice && partnerId) { try { const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] }, autoFilter: true, }); @@ -642,31 +651,22 @@ export default function SalesOrderPage() { } const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: p, size: s, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const resData = res.data?.data; - let allRows = resData?.data || resData?.rows || []; + let rows = resData?.data || resData?.rows || []; + const serverTotal = resData?.total || resData?.totalCount || rows.length; - // 거래처우선일 때 연결된 품목만 표시 + // 거래처우선일 때 연결된 품목만 표시 (클라이언트 필터) if (customerItemIds) { - allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); + rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); } - // 관리품목 필터 (코드/라벨 혼재 대응) - if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - allRows = allRows.filter((item: any) => { - const div = item.division || ""; - return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel)); - }); - } - const total = allRows.length; - const start = (p - 1) * s; - setItemSearchResults(allRows.slice(start, start + s)); - setItemTotal(total); - setItemTotalPages(Math.max(1, Math.ceil(total / s))); + setItemSearchResults(rows); + setItemTotal(serverTotal); + setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); } catch { /* skip */ } finally { setItemSearchLoading(false); } @@ -1320,44 +1320,44 @@ export default function SalesOrderPage() {
- No - 품번 - 품명 - 규격 - 재질 - 포장재 - 단위 - 수량 - 포장수량 - 단가 - 금액 - 납기일 - 분할/삭제 + No + 품번 + 품명 + 규격 + 재질 + 포장재 + 단위 + 수량 + 포장수량 + 단가 + 금액 + 납기일 + 분할/삭제 {detailRows.map((row, idx) => ( {idx + 1} - - {row.part_code} + + {row.part_code} - - {row.part_name} + + {row.part_name} - {row.spec} - {row.material} + {row.spec} + {row.material} updateDetailRow(idx, "packing_material", e.target.value)} placeholder="포장재" - className="h-8 text-xs" + className="h-8 text-xs w-full" /> - - - 전체 - {(categoryOptions["item_division"] || []).map((o) => ( - {o.label} - ))} - - +
영업관리
diff --git a/frontend/app/(main)/COMPANY_30/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_30/logistics/outbound/page.tsx index 824e2e86..457e2723 100644 --- a/frontend/app/(main)/COMPANY_30/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_30/logistics/outbound/page.tsx @@ -38,7 +38,6 @@ import { Save, ChevronRight, ChevronLeft, - ChevronDown, ChevronsLeft, ChevronsRight, Inbox, @@ -116,41 +115,8 @@ const GRID_COLUMNS = [ { key: "remark", label: "비고" }, ]; -// 마스터 헤더 레이아웃 (출고번호 뒤) -const MASTER_BODY_LAYOUT = [ - { key: "outbound_type", label: "출고유형", colSpan: 1 }, - { key: "outbound_date", label: "출고일", colSpan: 1 }, - { key: "reference_number", label: "참조번호", colSpan: 1 }, - { key: "customer_name", label: "거래처", colSpan: 1 }, - { key: "warehouse_name", label: "창고", colSpan: 1 }, - { key: "outbound_status", label: "출고상태", colSpan: 1 }, - { key: "memo", label: "비고", colSpan: 1 }, -]; - -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "source_type", label: "출처" }, - { key: "item_code", label: "품목코드" }, - { key: "item_name", label: "품목명" }, - { key: "specification", label: "규격" }, - { key: "outbound_qty", label: "출고수량" }, - { key: "unit_price", label: "단가" }, - { key: "total_amount", label: "금액" }, -]; - -// 마스터 필드 키 목록 (필터 분류용) -const MASTER_KEYS = new Set(["outbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); - -// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) -const DETAIL_KEY_MAP: Record = { - source_type: "source_type", - item_number: "item_code", - item_name: "item_name", - spec: "specification", - outbound_qty: "outbound_qty", - unit_price: "unit_price", - total_amount: "total_amount", -}; +// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16 +const TOTAL_COLS = 16; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -258,30 +224,6 @@ interface SelectedSourceItem { export default function OutboundPage() { const ts = useTableSettings("c16-outbound", "outbound_mng", GRID_COLUMNS); - // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 - const visibleMasterLayout = useMemo(() => { - const ordered: typeof MASTER_BODY_LAYOUT = []; - for (const vc of ts.visibleColumns) { - const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); - if (m) ordered.push(m); - } - return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; - }, [ts.visibleColumns]); - - const visibleDetailCols = useMemo(() => { - const ordered: typeof DETAIL_HEADER_COLS = []; - for (const vc of ts.visibleColumns) { - const detailKey = DETAIL_KEY_MAP[vc.key]; - if (detailKey) { - const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); - if (d) ordered.push(d); - } - } - return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; - }, [ts.visibleColumns]); - - const TOTAL_COLS = 3 + visibleMasterLayout.length; - // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -290,9 +232,7 @@ export default function OutboundPage() { // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); - // 마스터-디테일 그룹 테이블 state - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); + // 헤더 필터 & 정렬 const [headerFilters, setHeaderFilters] = useState>>({}); const [sortState, setSortState] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -403,121 +343,59 @@ export default function OutboundPage() { })(); }, []); - // --- 마스터-디테일 그룹핑, 필터, 정렬 --- + // 플랫 행 생성 (출고유형 코드→라벨 변환, source_type→한글 등) + const flatRows = useMemo(() => { + return data.map((row) => ({ + ...row, + _raw_outbound_type: row.outbound_type, + outbound_type: resolveCat("outbound_type", row.outbound_type) || row.outbound_type || "", + source_type: row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "", + item_number: row.item_code || (row as any).item_number || "", + spec: row.specification || (row as any).spec || "", + })); + }, [data, resolveCat]); - // outbound_number 기준 그룹핑 + 필터 + 정렬 - const filteredGroups = useMemo(() => { - // 1차: outbound_number 기준 그룹핑 - const allGroups: Record = {}; - for (const row of data) { - const key = row.outbound_number || "_no_number"; - if (!allGroups[key]) { - allGroups[key] = { master: row, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - const raw = (group.master as any)?.[colKey] ?? ""; - return values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([outNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - let cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; - if (colKey === "source_type") cellVal = SOURCE_TYPE_LABEL[cellVal] || cellVal; - return values.has(cellVal); - }) - ); - return [outNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - entries.sort(([, a], [, b]) => { - const av = (a.master as any)?.[key] ?? ""; - const bv = (b.master as any)?.[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - let av: any = (a as any)[key] ?? ""; - let bv: any = (b as any)[key] ?? ""; - if (key === "source_type") { av = SOURCE_TYPE_LABEL[av] || av; bv = SOURCE_TYPE_LABEL[bv] || bv; } - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [data, headerFilters, sortState]); - - // 마스터 컬럼별 고유값 (헤더 필터용) - const masterUniqueValues = useMemo(() => { + // 컬럼별 고유값 (헤더 필터용) + const columnUniqueValues = useMemo(() => { const result: Record = {}; - const seenMasters = new Map(); - data.forEach((row) => { - if (row.outbound_number && !seenMasters.has(row.outbound_number)) { - seenMasters.set(row.outbound_number, row); - } - }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "outbound_number", label: "출고번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label }))]) { + for (const col of GRID_COLUMNS) { const values = new Set(); - masters.forEach((m) => { - const val = (m as any)?.[col.key]; + flatRows.forEach((row) => { + const val = (row as any)[col.key]; if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [data]); + }, [flatRows]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) - const columnUniqueValues = useMemo(() => { - const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { - const values = new Set(); - data.forEach((row) => { - let val = (row as any)[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "source_type") val = SOURCE_TYPE_LABEL[val] || val; - values.add(String(val)); - } + // 필터 + 정렬 적용된 플랫 데이터 + const filteredRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; + return values.has(cellVal); }); - result[col.key] = Array.from(values).sort(); } - return result; - }, [data]); + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = (a as any)[key] ?? ""; + const bv = (b as any)[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -938,7 +816,7 @@ export default function OutboundPage() { dataCount={data.length} /> - {/* 출고 목록 테이블 */} + {/* 출고 목록 테이블 (플랫 리스트) */}
{/* 패널 헤더 */}
@@ -946,7 +824,7 @@ export default function OutboundPage() { 출고 목록 - {data.length}건 + {filteredRows.length}건
@@ -971,78 +849,68 @@ export default function OutboundPage() {
-
+
- - {visibleMasterLayout.map((col) => ( - - ))} + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 출고번호 */} - -
-
handleSort("outbound_number")}> - 출고번호 - {sortState?.key === "outbound_number" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["outbound_number"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {GRID_COLUMNS.map((col) => { + const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} + + ); + })} @@ -1052,7 +920,7 @@ export default function OutboundPage() { - ) : Object.keys(filteredGroups).length === 0 ? ( + ) : filteredRows.length === 0 ? (
@@ -1062,214 +930,59 @@ export default function OutboundPage() { ) : ( - Object.entries(filteredGroups).map(([outboundNo, group]) => { - const isExpanded = expandedOrders.has(outboundNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 */} - { - if (expandedOrders.has(outboundNo)) { - setClosingOrders((prev) => new Set(prev).add(outboundNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(outboundNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(outboundNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(outboundNo)); - } - }} - onDoubleClick={() => openEditModal(group.details[0])} - > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 출고번호 */} - - {outboundNo} - ({group.details.length}) - - {/* 마스터 필드 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => { - switch (col.key) { - case "outbound_type": return ( - - - {resolveCat("outbound_type", master.outbound_type) || "-"} - - - ); - case "outbound_date": return ( - - {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} - - ); - case "reference_number": return ( - - {master.reference_number || ""} - - ); - case "customer_name": return ( - - {master.customer_name || ""} - - ); - case "warehouse_name": return ( - - {master.warehouse_name || master.warehouse_code || ""} - - ); - case "outbound_status": return ( - - - {master.outbound_status || "-"} - - - ); - case "memo": return ( - - {master.memo || ""} - - ); - default: return {(master as any)[col.key] ?? ""}; - } - })} - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - - {visibleDetailCols.map((col) => { - const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => { - let v = (d as any)[col.key]; - if (col.key === "source_type") v = SOURCE_TYPE_LABEL[v] || v; - return v; - }).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} -
+ { - const isClosing = closingOrders.has(outboundNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - onDoubleClick={() => openEditModal(row)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - - {visibleDetailCols.map((col) => { - switch (col.key) { - case "source_type": return {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"}; - case "item_code": return {row.item_code || ""}; - case "item_name": return {row.item_name || ""}; - case "specification": return {row.specification || ""}; - case "outbound_qty": return {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}; - case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; - case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; - default: return {(row as any)[col.key] ?? ""}; - } - })} - + onClick={() => { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] ); - })} - + }} + onDoubleClick={() => openEditModal(row as any)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + > + {}} /> + + {row.outbound_number || ""} + + + {row.outbound_type || "-"} + + + + {row.outbound_date ? new Date(row.outbound_date).toLocaleDateString("ko-KR") : ""} + + {row.reference_number || ""} + {row.source_type || ""} + {row.customer_name || ""} + {row.item_number || ""} + {row.item_name || ""} + {row.spec || ""} + {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {row.warehouse_name || row.warehouse_code || ""} + + + {row.outbound_status || "-"} + + + {row.remark || row.memo || ""} + ); }) )} diff --git a/frontend/app/(main)/COMPANY_30/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_30/logistics/receiving/page.tsx index 701ae489..77d21e7b 100644 --- a/frontend/app/(main)/COMPANY_30/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_30/logistics/receiving/page.tsx @@ -43,7 +43,6 @@ import { X, Save, ChevronRight, - ChevronDown, ChevronLeft, ChevronsLeft, ChevronsRight, @@ -64,6 +63,7 @@ import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSea import { getReceivingList, createReceiving, + updateReceiving, deleteReceiving, generateReceivingNumber, getReceivingWarehouses, @@ -95,41 +95,8 @@ const GRID_COLUMNS = [ { key: "remark", label: "비고" }, ]; -// 마스터 헤더 레이아웃 (입고번호 뒤, 디테일 7컬럼 위에 colSpan으로 맵핑) -const MASTER_BODY_LAYOUT = [ - { key: "inbound_type", label: "입고유형", colSpan: 1 }, - { key: "inbound_date", label: "입고일", colSpan: 1 }, - { key: "reference_number", label: "참조번호", colSpan: 1 }, - { key: "supplier_name", label: "공급처", colSpan: 1 }, - { key: "warehouse_name", label: "창고", colSpan: 1 }, - { key: "inbound_status", label: "입고상태", colSpan: 1 }, - { key: "memo", label: "비고", colSpan: 1 }, -]; - -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "source_table", label: "출처" }, - { key: "item_number", label: "품목코드" }, - { key: "item_name", label: "품목명" }, - { key: "spec", label: "규격" }, - { key: "inbound_qty", label: "입고수량" }, - { key: "unit_price", label: "단가" }, - { key: "total_amount", label: "금액" }, -]; - -// 마스터 필드 키 목록 (필터 분류용) -const MASTER_KEYS = new Set(["inbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); - -// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) -const DETAIL_KEY_MAP: Record = { - source_type: "source_table", - item_number: "item_number", - item_name: "item_name", - spec: "spec", - inbound_qty: "inbound_qty", - unit_price: "unit_price", - total_amount: "total_amount", -}; +// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16 +const TOTAL_COLS = 16; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -287,30 +254,6 @@ interface SelectedSourceItem { export default function ReceivingPage() { const ts = useTableSettings("c16-receiving", "inbound_mng", GRID_COLUMNS); - // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 - const visibleMasterLayout = useMemo(() => { - const ordered: typeof MASTER_BODY_LAYOUT = []; - for (const vc of ts.visibleColumns) { - const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); - if (m) ordered.push(m); - } - return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; - }, [ts.visibleColumns]); - - const visibleDetailCols = useMemo(() => { - const ordered: typeof DETAIL_HEADER_COLS = []; - for (const vc of ts.visibleColumns) { - const detailKey = DETAIL_KEY_MAP[vc.key]; - if (detailKey) { - const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); - if (d) ordered.push(d); - } - } - return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; - }, [ts.visibleColumns]); - - const TOTAL_COLS = 3 + visibleMasterLayout.length; - // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -319,9 +262,7 @@ export default function ReceivingPage() { // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); - // 마스터-디테일 그룹 테이블 - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); + // 헤더 필터 & 정렬 const [headerFilters, setHeaderFilters] = useState>>({}); const [sortState, setSortState] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -338,6 +279,10 @@ export default function ReceivingPage() { const [selectedItems, setSelectedItems] = useState([]); const [saving, setSaving] = useState(false); + // 수정 모드 + const [editMode, setEditMode] = useState(false); + const [editItemIds, setEditItemIds] = useState([]); + // 소스 데이터 const [sourceKeyword, setSourceKeyword] = useState(""); const [sourceLoading, setSourceLoading] = useState(false); @@ -377,7 +322,7 @@ export default function ReceivingPage() { Promise.all( ["material", "unit"].map(async (col) => { try { - const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_30`); + const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`); const items = flatten(res.data?.data || []); map[col] = {}; for (const item of items) map[col][item.code] = item.label; @@ -434,124 +379,56 @@ export default function ReceivingPage() { })(); }, []); - // 필터 + 정렬 적용된 데이터 -> 그룹핑 - const filteredGroups = useMemo(() => { - // 1차: inbound_number 기준 그룹핑 - const allGroups: Record = {}; - for (const row of data) { - const key = row.inbound_number || "_no_inbound"; - if (!allGroups[key]) { - allGroups[key] = { master: row, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - let raw = (group.master as any)?.[colKey] ?? ""; - // 입고유형은 코드→라벨 변환된 값으로 비교 - if (colKey === "inbound_type") raw = resolveInboundType(String(raw)); - return values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([inboundNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - let cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; - if (colKey === "source_table") cellVal = SOURCE_TABLE_LABEL[cellVal] || cellVal; - return values.has(cellVal); - }) - ); - return [inboundNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - entries.sort(([, a], [, b]) => { - let av: any = (a.master as any)?.[key] ?? ""; - let bv: any = (b.master as any)?.[key] ?? ""; - if (key === "inbound_type") { av = resolveInboundType(String(av)); bv = resolveInboundType(String(bv)); } - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - const av = (a as any)[key] ?? ""; - const bv = (b as any)[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [data, headerFilters, sortState]); - - // 마스터 컬럼별 고유값 (마스터 헤더 필터용) - const masterUniqueValues = useMemo(() => { - const result: Record = {}; - const seenMasters = new Map(); - data.forEach((row) => { - if (row.inbound_number && !seenMasters.has(row.inbound_number)) { - seenMasters.set(row.inbound_number, row); - } - }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "inbound_number", label: "입고번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label }))]) { - const values = new Set(); - masters.forEach((m) => { - let val = (m as any)?.[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "inbound_type") val = resolveInboundType(String(val)); - values.add(String(val)); - } - }); - result[col.key] = Array.from(values).sort(); - } - return result; + // 플랫 행 생성 (입고유형 코드→라벨 변환, source_table→한글 등) + const flatRows = useMemo(() => { + return data.map((row) => ({ + ...row, + inbound_type: resolveInboundType(row.inbound_type), + source_type: row.source_table ? (SOURCE_TABLE_LABEL[row.source_table] || row.source_table) : (row as any).source_type || "", + })); }, [data]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) + // 컬럼별 고유값 (헤더 필터용) const columnUniqueValues = useMemo(() => { const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { + for (const col of GRID_COLUMNS) { const values = new Set(); - data.forEach((row) => { - let val = (row as any)[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "source_table") val = SOURCE_TABLE_LABEL[val] || val; - values.add(String(val)); - } + flatRows.forEach((row) => { + const val = (row as any)[col.key]; + if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [data]); + }, [flatRows]); + + // 필터 + 정렬 적용된 플랫 데이터 + const filteredRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; + return values.has(cellVal); + }); + } + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = (a as any)[key] ?? ""; + const bv = (b as any)[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -633,6 +510,8 @@ export default function ReceivingPage() { const openRegisterModal = async () => { const defaultType = "구매입고"; + setEditMode(false); + setEditItemIds([]); setModalInboundType(defaultType); setModalInboundDate(new Date().toISOString().split("T")[0]); setModalWarehouse(""); @@ -661,6 +540,51 @@ export default function ReceivingPage() { } }; + // 수정 모달 열기 + const openEditModal = (row: InboundItem) => { + const inNo = row.inbound_number; + const grouped = data.filter((d) => d.inbound_number === inNo); + const first = grouped[0] || row; + + setEditMode(true); + setEditItemIds(grouped.map((g) => g.id)); + setModalInboundNo(inNo); + setModalInboundType(first.inbound_type || "구매입고"); + setModalInboundDate(first.inbound_date ? String(first.inbound_date).slice(0, 10) : ""); + setModalWarehouse(first.warehouse_code || ""); + setModalLocation(first.location_code || ""); + setModalInspector((first as any).inspector || ""); + setModalManager((first as any).manager || ""); + setModalMemo(first.memo || ""); + setSelectedItems( + grouped.map((g) => ({ + key: g.id, + inbound_type: g.inbound_type || "", + reference_number: g.reference_number || "", + supplier_code: (g as any).supplier_code || "", + supplier_name: g.supplier_name || "", + item_number: g.item_number || "", + item_name: g.item_name || "", + spec: g.spec || "", + material: (g as any).material || "", + unit: (g as any).unit || "", + inbound_qty: Number(g.inbound_qty) || 0, + unit_price: Number(g.unit_price) || 0, + total_amount: Number(g.total_amount) || 0, + source_table: (g as any).source_table || "", + source_id: (g as any).source_id || "", + })) + ); + setSourceKeyword(""); + setPurchaseOrders([]); + setShipments([]); + setItems([]); + setSourcePage(1); + setSourceTotalCount(0); + setIsModalOpen(true); + loadSourceData(first.inbound_type || "구매입고", undefined, 1); + }; + // 검색 버튼 클릭 시 const searchSourceData = useCallback(async () => { setSourcePage(1); @@ -811,41 +735,97 @@ export default function ReceivingPage() { } setSaving(true); try { - const res = await createReceiving({ - inbound_number: modalInboundNo, - inbound_date: modalInboundDate, - warehouse_code: modalWarehouse, - location_code: modalLocation || undefined, - inspector: modalInspector || undefined, - manager: modalManager || undefined, - memo: modalMemo || undefined, - items: selectedItems.map((item) => ({ - inbound_type: item.inbound_type, - reference_number: item.reference_number, - supplier_code: item.supplier_code, - supplier_name: item.supplier_name, - item_number: item.item_number, - item_name: item.item_name, - spec: item.spec, - material: item.material, - unit: item.unit, - inbound_qty: item.inbound_qty, - unit_price: item.unit_price, - total_amount: item.total_amount, - source_table: item.source_table, - source_id: item.source_id, - inbound_status: "입고완료", - inspection_status: "대기", - })), - }); + if (editMode) { + // 기존 item 수정 + 삭제된 item 삭제 + 새 item 추가 + const currentKeys = new Set(selectedItems.map((i) => i.key)); + const toDelete = editItemIds.filter((id) => !currentKeys.has(id)); + const toUpdate = selectedItems.filter((i) => editItemIds.includes(i.key)); + const toCreate = selectedItems.filter((i) => !editItemIds.includes(i.key)); - if (res.success) { - alert(res.message || "입고 등록 완료"); + await Promise.all([ + ...toDelete.map((id) => deleteReceiving(id)), + ...toUpdate.map((item) => + updateReceiving(item.key, { + inbound_date: modalInboundDate, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + warehouse_code: modalWarehouse || undefined, + location_code: modalLocation || undefined, + memo: modalMemo || undefined, + } as any) + ), + ...(toCreate.length > 0 + ? [createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: toCreate.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + })] + : []), + ]); + toast.success("입고 정보를 수정했어요"); setIsModalOpen(false); fetchList(); + } else { + const res = await createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: selectedItems.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + }); + + if (res.success) { + toast.success(res.message || "입고 등록 완료"); + setIsModalOpen(false); + fetchList(); + } } - } catch { - alert("입고 등록 중 오류가 발생했습니다."); + } catch (err: any) { + const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "입고 등록 중 오류가 발생했습니다."); + toast.error(msg); } finally { setSaving(false); } @@ -897,13 +877,13 @@ export default function ReceivingPage() { } /> - {/* 입고 목록 테이블 (마스터-디테일 그룹) */} + {/* 입고 목록 테이블 (플랫 리스트) */}

입고 목록

- {Object.keys(filteredGroups).length}건 + {filteredRows.length}건
-
+
- - {visibleMasterLayout.map((col) => ( - - ))} + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 입고번호 (별도 컬럼) */} - -
-
handleSort("inbound_number")}> - 입고번호 - {sortState?.key === "inbound_number" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["inbound_number"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {GRID_COLUMNS.map((col) => { + const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} + + ); + })} @@ -993,7 +963,7 @@ export default function ReceivingPage() { - ) : Object.keys(filteredGroups).length === 0 ? ( + ) : filteredRows.length === 0 ? (
@@ -1003,212 +973,59 @@ export default function ReceivingPage() { ) : ( - Object.entries(filteredGroups).map(([inboundNo, group]) => { - const isExpanded = expandedOrders.has(inboundNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 */} - { - if (expandedOrders.has(inboundNo)) { - setClosingOrders((prev) => new Set(prev).add(inboundNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(inboundNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(inboundNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(inboundNo)); - } + { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + onDoubleClick={() => openEditModal(row as any)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); }} > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 입고번호 */} - - {inboundNo} - ({group.details.length}) - - {/* 마스터 필드 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => { - switch (col.key) { - case "inbound_type": return ( - - - {resolveInboundType(master.inbound_type)} - - - ); - case "inbound_date": return ( - - {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} - - ); - case "reference_number": return ( - - {master.reference_number || ""} - - ); - case "supplier_name": return ( - - {master.supplier_name || ""} - - ); - case "warehouse_name": return ( - - {master.warehouse_name || master.warehouse_code || ""} - - ); - case "inbound_status": return ( - - - {master.inbound_status || "-"} - - - ); - case "memo": return ( - - {master.memo || ""} - - ); - default: return {(master as any)[col.key] ?? ""}; - } - })} - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - - {visibleDetailCols.map((col) => { - const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => { - let v = (d as any)[col.key]; - if (col.key === "source_table") v = SOURCE_TABLE_LABEL[v] || v; - return v; - }).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} -
- )} - - {/* 디테일 행 (펼쳤을 때만) */} - {isExpanded && group.details.map((row, detailIdx) => { - const isClosing = closingOrders.has(inboundNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - - {visibleDetailCols.map((col) => { - switch (col.key) { - case "source_table": return {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"}; - case "item_number": return {row.item_number || ""}; - case "item_name": return {row.item_name || ""}; - case "spec": return {row.spec || ""}; - case "inbound_qty": return {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}; - case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; - case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; - default: return {(row as any)[col.key] ?? ""}; - } - })} - - ); - })} - + {}} /> + + {row.inbound_number || ""} + + + {row.inbound_type || "-"} + + + + {row.inbound_date ? new Date(row.inbound_date).toLocaleDateString("ko-KR") : ""} + + {row.reference_number || ""} + {row.source_type || ""} + {row.supplier_name || ""} + {row.item_number || ""} + {row.item_name || ""} + {row.spec || ""} + {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {row.warehouse_name || (row as any).warehouse_code || ""} + + + {row.inbound_status || "-"} + + + {row.remark || row.memo || ""} + ); }) )} @@ -1221,9 +1038,9 @@ export default function ReceivingPage() { - 입고 등록 + {editMode ? "입고 수정" : "입고 등록"} - 입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해 주세요. + {editMode ? "입고 정보를 수정해 주세요." : "입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해 주세요."}
diff --git a/frontend/app/(main)/COMPANY_30/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_30/production/plan-management/page.tsx index 38b01658..534582f9 100644 --- a/frontend/app/(main)/COMPANY_30/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_30/production/plan-management/page.tsx @@ -1053,14 +1053,14 @@ export default function ProductionPlanManagementPage() { return (
- + - + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - - 품목코드 - 품목명 + + 품목코드 + 품목명 {ts.visibleColumns.map((col) => ( {col.label} @@ -1073,9 +1073,9 @@ export default function ProductionPlanManagementPage() { if (item._isGroupSummary) { return ( - - - + + + {ts.visibleColumns.map((col) => { const v = (item as any)[col.key]; return ( @@ -1090,14 +1090,14 @@ export default function ProductionPlanManagementPage() { return ( - e.stopPropagation()}> + e.stopPropagation()}> toggleItemGroupSelect(item.item_code)} className="h-4 w-4" /> - toggleItemExpand(item.item_code)}> + toggleItemExpand(item.item_code)}> - toggleItemExpand(item.item_code)}>{item.item_code} - toggleItemExpand(item.item_code)}>{item.item_name} + toggleItemExpand(item.item_code)}>{item.item_code} + toggleItemExpand(item.item_code)}>{item.item_name} {ts.visibleColumns.map((col) => renderGroupCell(col, item))} {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => { @@ -1107,9 +1107,9 @@ export default function ProductionPlanManagementPage() { } return ( - - - + + +
수주번호: {detail.order_no} diff --git a/frontend/app/(main)/COMPANY_30/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_30/purchase/order/page.tsx index 143a84a9..f712d3ed 100644 --- a/frontend/app/(main)/COMPANY_30/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_30/purchase/order/page.tsx @@ -13,7 +13,7 @@ import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, - ClipboardList, Pencil, Search, X, Package, ChevronDown, + ClipboardList, Pencil, Search, X, Package, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, GripVertical, } from "lucide-react"; @@ -88,18 +88,18 @@ const GRID_COLUMNS_CONFIG = [ ]; const MODAL_DETAIL_COLUMNS = [ - { key: "item_code", label: "품번", width: "w-[120px]" }, - { key: "item_name", label: "품명", width: "w-[120px]" }, - { key: "supplier", label: "공급업체", width: "w-[150px]" }, - { key: "spec", label: "규격", width: "w-[80px]" }, - { key: "unit", label: "단위", width: "w-[60px]" }, - { key: "order_qty", label: "발주수량", width: "w-[90px]" }, - { key: "received_qty", label: "입고수량", width: "w-[90px]" }, - { key: "remain_qty", label: "잔량", width: "w-[80px]" }, - { key: "unit_price", label: "단가", width: "w-[100px]" }, - { key: "amount", label: "금액", width: "w-[100px]" }, - { key: "due_date", label: "납기일", width: "w-[160px]" }, - { key: "memo", label: "메모", width: "w-[120px]" }, + { key: "item_code", label: "품번", width: "min-w-[120px]" }, + { key: "item_name", label: "품명", width: "min-w-[150px]" }, + { key: "supplier", label: "공급업체", width: "min-w-[150px]" }, + { key: "spec", label: "규격", width: "min-w-[80px]" }, + { key: "unit", label: "단위", width: "min-w-[90px]" }, + { key: "order_qty", label: "발주수량", width: "min-w-[90px]" }, + { key: "received_qty", label: "입고수량", width: "min-w-[90px]" }, + { key: "remain_qty", label: "잔량", width: "min-w-[80px]" }, + { key: "unit_price", label: "단가", width: "min-w-[100px]" }, + { key: "amount", label: "금액", width: "min-w-[100px]" }, + { key: "due_date", label: "납기일", width: "min-w-[160px]" }, + { key: "memo", label: "메모", width: "min-w-[120px]" }, ]; const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16"; @@ -162,7 +162,6 @@ export default function PurchaseOrderPage() { const [excelUploadOpen, setExcelUploadOpen] = useState(false); const [categoryOptions, setCategoryOptions] = useState>({}); const [checkedIds, setCheckedIds] = useState([]); - const [expandedOrders, setExpandedOrders] = useState>(new Set()); // 테이블 설정 const ts = useTableSettings("c16-purchase-order", DETAIL_TABLE, GRID_COLUMNS_CONFIG); @@ -238,7 +237,7 @@ export default function PurchaseOrderPage() { ); try { const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, size: 5000, autoFilter: true, }); const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || []; optMap["supplier_code"] = supps.map((s: any) => ({ @@ -248,7 +247,7 @@ export default function PurchaseOrderPage() { } catch { /* skip */ } try { const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, size: 5000, autoFilter: true, }); const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; optMap["manager"] = users.map((u: any) => ({ @@ -294,7 +293,7 @@ export default function PurchaseOrderPage() { } const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined, autoFilter: true, sort: { columnName: "purchase_no", order: "desc" }, @@ -372,20 +371,6 @@ export default function PurchaseOrderPage() { useEffect(() => { fetchOrders(); }, [fetchOrders]); // purchase_no 기준 그룹핑 - const orderGroups = useMemo(() => { - const map: Record = {}; - for (const row of orders) { - const key = row.purchase_no || row.id; - if (!map[key]) { - map[key] = { - master: { purchase_no: row.purchase_no, order_date: row.order_date, supplier_name: row.supplier_name, status: row.status, memo: row.memo }, - details: [], - }; - } - map[key].details.push(row); - } - return map; - }, [orders]); const getCategoryLabel = (col: string, code: string) => { if (!code) return ""; @@ -553,7 +538,7 @@ export default function PurchaseOrderPage() { } }; - // 품목 검색 + // 품목 검색 (수주관리와 동일한 서버 페이징 방식) const searchItems = async (page?: number, size?: number) => { const p = page ?? itemPage; const s = size ?? itemPageSize; @@ -563,25 +548,24 @@ export default function PurchaseOrderPage() { if (itemSearchKeyword) { filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); } + // 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응) + if (itemSearchDivision !== "all") { + const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; + const divValues = [itemSearchDivision]; + if (divLabel) divValues.push(divLabel); + filters.push({ columnName: "division", operator: "in", value: divValues }); + } const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: p, size: s, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const resData = res.data?.data; - let allRows = resData?.data || resData?.rows || []; - if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - allRows = allRows.filter((item: any) => { - const div = item.division || ""; - return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel)); - }); - } - const total = allRows.length; - const start = (p - 1) * s; - setItemSearchResults(allRows.slice(start, start + s)); - setItemTotal(total); - setItemTotalPages(Math.max(1, Math.ceil(total / s))); + const rows = resData?.data || resData?.rows || []; + const serverTotal = resData?.total || resData?.totalCount || rows.length; + setItemSearchResults(rows); + setItemTotal(serverTotal); + setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); } catch { /* skip */ } finally { setItemSearchLoading(false); } @@ -623,7 +607,7 @@ export default function PurchaseOrderPage() { try { const itemIds = selected.map((item) => item.item_number || item.id); const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [ @@ -686,7 +670,7 @@ export default function PurchaseOrderPage() { if (itemCodes.length === 0) return; try { const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, autoFilter: true, }); @@ -708,7 +692,7 @@ export default function PurchaseOrderPage() { if (itemCodes.length === 0) return; try { const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [ { columnName: "supplier_id", operator: "equals", value: supplierCode }, { columnName: "item_id", operator: "in", value: itemCodes }, @@ -811,125 +795,83 @@ export default function PurchaseOrderPage() {
- {/* 데이터 테이블 — 아코디언 그룹핑 */} + {/* 데이터 테이블 — 플랫 리스트 */}
{loading ? (
- ) : Object.keys(orderGroups).length === 0 ? ( + ) : orders.length === 0 ? (

등록된 발주가 없어요

) : ( (() => { - const MASTER_KEYS = new Set(["purchase_no", "order_date", "supplier_name", "status", "memo"]); const numCols = new Set(["order_qty", "received_qty", "remain_qty", "unit_price", "amount"]); - - // ts.visibleColumns 순서를 따르되, 마스터/디테일 컬럼을 분리 - // 고정 컬럼(품목수)은 마스터 선행 컬럼 뒤에 배치 - const leadingMaster: typeof ts.visibleColumns = []; - const detailCols: typeof ts.visibleColumns = []; - const trailingMaster: typeof ts.visibleColumns = []; - let passedFirstDetail = false; - for (const col of ts.visibleColumns) { - if (MASTER_KEYS.has(col.key)) { - if (passedFirstDetail) trailingMaster.push(col); - else leadingMaster.push(col); - } else { - passedFirstDetail = true; - detailCols.push(col); - } - } - - const renderDetailCell = (row: any, key: string) => { + const renderCell = (row: any, key: string) => { const val = row[key]; if (key === "status") return val ? {val} : "-"; - if (numCols.has(key)) return {val ? Number(val).toLocaleString() : "0"}; - return val || "-"; + if (key === "order_date" || key === "due_date") return val ? new Date(val).toLocaleDateString("ko-KR") : "-"; + if (numCols.has(key)) return {val ? Number(val).toLocaleString() : ""}; + return val || ""; }; - - const renderMasterHead = (col: { key: string; label: string }) => ( - - {col.label} - - ); - - const renderMasterCell = (col: { key: string }, m: any, purchaseNo: string) => { - if (col.key === "purchase_no") return {purchaseNo}; - if (col.key === "order_date") return {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}; - if (col.key === "supplier_name") return {m.supplier_name || "-"}; - if (col.key === "status") return {m.status && {m.status}}; - if (col.key === "memo") return {m.memo || ""}; - return ; - }; - return (
- - - {leadingMaster.map(renderMasterHead)} - 품목수 - {detailCols.map(col => ( - - {col.label}{col.key === "order_qty" || col.key === "amount" ? " 합계" : ""} + { + const allIds = orders.map((r) => r.id); + const allChecked = allIds.length > 0 && allIds.every((id) => checkedIds.includes(id)); + setCheckedIds(allChecked ? [] : allIds); + }} + > + 0 && orders.every((r) => checkedIds.includes(r.id))} + onCheckedChange={() => {}} + /> + + {ts.visibleColumns.map((col) => ( + + {col.label} ))} - {trailingMaster.map(renderMasterHead)} - {Object.entries(orderGroups).map(([purchaseNo, group]) => { - const isExpanded = expandedOrders.has(purchaseNo); - const detailIds = group.details.map(d => d.id); - const allChecked = detailIds.length > 0 && detailIds.every(id => checkedIds.includes(id)); - const someChecked = detailIds.some(id => checkedIds.includes(id)); - const m = group.master; - const totalQty = group.details.reduce((s, d) => s + (Number(d.order_qty) || 0), 0); - const totalAmt = group.details.reduce((s, d) => s + (Number(d.amount) || 0), 0); + {orders.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - setExpandedOrders(prev => { const next = new Set(prev); if (next.has(purchaseNo)) next.delete(purchaseNo); else next.add(purchaseNo); return next; })} - onDoubleClick={() => openEditModal(purchaseNo)} + { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + onDoubleClick={() => openEditModal(row.purchase_no)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} > - - + {}} /> + + {ts.visibleColumns.map((col) => ( + + {renderCell(row, col.key)} - { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !detailIds.includes(id)) : [...new Set([...prev, ...detailIds])]); }}> - {}} /> - - {leadingMaster.map(col => renderMasterCell(col, m, purchaseNo))} - {group.details.length}건 - {detailCols.map(col => ( - - {col.key === "order_qty" ? totalQty.toLocaleString() - : col.key === "amount" ? totalAmt.toLocaleString() - : ""} - - ))} - {trailingMaster.map(col => renderMasterCell(col, m, purchaseNo))} - - {isExpanded && group.details.map((row) => ( - - - { e.stopPropagation(); setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id]); }}> - {}} /> - - {leadingMaster.map(col => )} - - {detailCols.map(col => ( - - {renderDetailCell(row, col.key)} - - ))} - {trailingMaster.map(col => )} - ))} - + ); })} @@ -938,7 +880,7 @@ export default function PurchaseOrderPage() { })() )}
- 전체 {Object.keys(orderGroups).length}건 (발주 기준) / {orders.length}건 (품목 기준) + 전체 {orders.length}건
@@ -1094,7 +1036,7 @@ export default function PurchaseOrderPage() { ) : (
-
+
c.key)} strategy={horizontalListSortingStrategy}> @@ -1118,12 +1060,12 @@ export default function PurchaseOrderPage() { {visibleModalColumns.map((col) => { switch (col.key) { case "item_code": - return {row.item_code}; + return {row.item_code}; case "item_name": - return {row.item_name}; + return {row.item_name}; case "supplier": return ( - + {isReadOnly ? ( {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} ) : ( @@ -1133,7 +1075,7 @@ export default function PurchaseOrderPage() { updateDetailRow(idx, "supplier_code", v); updateDetailRow(idx, "supplier_name", name); }}> - + {(categoryOptions["supplier_code"] || []).map(o => ( {o.label} @@ -1149,11 +1091,11 @@ export default function PurchaseOrderPage() { return {row.unit}; case "order_qty": return ( - + {isReadOnly ? ( {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} ) : ( - updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" /> )} ); @@ -1163,11 +1105,11 @@ export default function PurchaseOrderPage() { return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; case "unit_price": return ( - + {isReadOnly ? ( {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} ) : ( - updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" /> )} ); @@ -1175,21 +1117,21 @@ export default function PurchaseOrderPage() { return {row.amount ? Number(row.amount).toLocaleString() : ""}; case "due_date": return ( - + {isReadOnly ? ( {row.due_date} ) : ( - updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> + updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs w-full" /> )} ); case "memo": return ( - + {isReadOnly ? ( {row.memo} ) : ( - updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> + updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs w-full" /> )} ); diff --git a/frontend/app/(main)/COMPANY_30/purchase/purchase-item/page.tsx b/frontend/app/(main)/COMPANY_30/purchase/purchase-item/page.tsx index ce6e8198..31ca38f4 100644 --- a/frontend/app/(main)/COMPANY_30/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_30/purchase/purchase-item/page.tsx @@ -142,6 +142,10 @@ const FORM_FIELDS = [ { key: "division", label: "관리품목", type: "multi-category" }, { key: "type", label: "품목구분", type: "category" }, { key: "size", label: "규격", type: "text" }, + { key: "width", label: "가로", type: "text", placeholder: "숫자 입력 (mm)" }, + { key: "height", label: "세로", type: "text", placeholder: "숫자 입력 (mm)" }, + { key: "thickness", label: "두께", type: "text", placeholder: "숫자 입력 (mm)" }, + { key: "area", label: "면적", type: "text", placeholder: "숫자 입력 (㎡)" }, { key: "unit", label: "단위", type: "category" }, { key: "material", label: "재질", type: "category" }, { key: "status", label: "상태", type: "category" }, @@ -175,6 +179,10 @@ const ITEM_GRID_COLUMNS = [ { key: "item_number", label: "품번" }, { key: "item_name", label: "품명" }, { key: "size", label: "규격" }, + { key: "width", label: "가로" }, + { key: "height", label: "세로" }, + { key: "thickness", label: "두께" }, + { key: "area", label: "면적" }, { key: "unit", label: "단위" }, { key: "standard_price", label: "기준단가/구매단가" }, { key: "currency_code", label: "통화" }, @@ -1605,9 +1613,25 @@ export default function PurchaseItemPage() { ) : ( setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))} - placeholder={"placeholder" in field ? field.placeholder : field.label} - className="h-9" + readOnly={field.key === "area"} + onChange={(e) => { + if (field.key === "area") return; + const v = e.target.value; + setFormData((prev) => { + const next = { ...prev, [field.key]: v }; + // 가로/세로 변경 시 면적(㎡) 자동 계산: (가로mm × 세로mm) / 1,000,000 + if (field.key === "width" || field.key === "height") { + const w = Number(field.key === "width" ? v : prev.width); + const h = Number(field.key === "height" ? v : prev.height); + if (!isNaN(w) && !isNaN(h) && w > 0 && h > 0) { + next.area = ((w * h) / 1_000_000).toFixed(4); + } + } + return next; + }); + }} + placeholder={field.key === "area" ? "자동 계산" : ("placeholder" in field ? field.placeholder : field.label)} + className={cn("h-9", field.key === "area" && "bg-muted cursor-not-allowed")} /> )} diff --git a/frontend/app/(main)/COMPANY_30/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_30/quality/item-inspection/page.tsx index 05facbbc..863c0aef 100644 --- a/frontend/app/(main)/COMPANY_30/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_30/quality/item-inspection/page.tsx @@ -424,9 +424,10 @@ export default function ItemInspectionInfoPage() { case "inspection_type": return (
- {group.types.map((t: string) => ( - {t} - ))} + {group.types.map((t: string) => { + const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t; + return {label}; + })}
); @@ -494,7 +495,7 @@ export default function ItemInspectionInfoPage() { : "bg-muted/50 text-muted-foreground border-border hover:bg-muted" )} > - {type} + {inspTypeCatOptions.find((o) => o.code === type)?.label || type} { - if (checkedIds.length === 0) { toast.error("삭제할 수주를 선택해주세요."); return; } - const selectedItems = orders.filter((o) => checkedIds.includes(o.id)); - const orderNos = [...new Set(selectedItems.map((o) => o.order_no))]; - const ok = await confirm(`${orderNos.length}건의 수주를 삭제하시겠습니까?`, { + if (checkedIds.length === 0) { toast.error("삭제할 항목을 선택해주세요."); return; } + const ok = await confirm(`${checkedIds.length}건의 수주 항목을 삭제하시겠습니까?`, { description: "삭제된 데이터는 복구할 수 없습니다.", variant: "destructive", confirmText: "삭제", }); if (!ok) return; try { + // 1. 선택한 디테일 삭제 + await apiClient.delete(`/table-management/tables/${DETAIL_TABLE}/delete`, { + data: checkedIds.map((id) => ({ id })), + }); + + // 2. 영향받는 수주번호의 잔여 디테일 확인 → 0건이면 마스터도 삭제 + const selectedItems = orders.filter((o) => checkedIds.includes(o.id)); + const orderNos = [...new Set(selectedItems.map((o) => o.order_no))]; for (const orderNo of orderNos) { - // 디테일 삭제 - const detailRes = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { - page: 1, size: 9999, - dataFilter: { enabled: true, filters: [{ columnName: "order_no", operator: "equals", value: orderNo }] }, - autoFilter: true, - }); - const details = detailRes.data?.data?.data || detailRes.data?.data?.rows || []; - if (details.length > 0) { - await apiClient.delete(`/table-management/tables/${DETAIL_TABLE}/delete`, { - data: details.map((d: any) => ({ id: d.id })), - }); - } - // 마스터 삭제 - const masterRes = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, { + const remainRes = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { page: 1, size: 1, dataFilter: { enabled: true, filters: [{ columnName: "order_no", operator: "equals", value: orderNo }] }, autoFilter: true, }); - const masters = masterRes.data?.data?.data || masterRes.data?.data?.rows || []; - if (masters.length > 0) { - await apiClient.delete(`/table-management/tables/${MASTER_TABLE}/delete`, { - data: masters.map((m: any) => ({ id: m.id })), + const remaining = remainRes.data?.data?.data || remainRes.data?.data?.rows || []; + if (remaining.length === 0) { + // 디테일 0건 → 마스터 삭제 + const masterRes = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, { + page: 1, size: 1, + dataFilter: { enabled: true, filters: [{ columnName: "order_no", operator: "equals", value: orderNo }] }, + autoFilter: true, }); + const masters = masterRes.data?.data?.data || masterRes.data?.data?.rows || []; + if (masters.length > 0) { + await apiClient.delete(`/table-management/tables/${MASTER_TABLE}/delete`, { + data: masters.map((m: any) => ({ id: m.id })), + }); + } } } toast.success("삭제되었습니다."); @@ -622,6 +624,15 @@ export default function SalesOrderPage() { const filters: any[] = []; if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); + // 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응) + if (itemSearchDivision !== "all") { + const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; + // 코드 또는 라벨이 저장된 경우 모두 조회하기 위해 in 연산자 사용 + const divValues = [itemSearchDivision]; + if (divLabel) divValues.push(divLabel); + filters.push({ columnName: "division", operator: "in", value: divValues }); + } + // 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링 const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI"; const partnerId = masterForm.partner_id; @@ -630,7 +641,7 @@ export default function SalesOrderPage() { if (isCustomerPrice && partnerId) { try { const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] }, autoFilter: true, }); @@ -640,31 +651,22 @@ export default function SalesOrderPage() { } const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: p, size: s, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const resData = res.data?.data; - let allRows = resData?.data || resData?.rows || []; + let rows = resData?.data || resData?.rows || []; + const serverTotal = resData?.total || resData?.totalCount || rows.length; - // 거래처우선일 때 연결된 품목만 표시 + // 거래처우선일 때 연결된 품목만 표시 (클라이언트 필터) if (customerItemIds) { - allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); + rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); } - // 관리품목 필터 (코드/라벨 혼재 대응) - if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - allRows = allRows.filter((item: any) => { - const div = item.division || ""; - return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel)); - }); - } - const total = allRows.length; - const start = (p - 1) * s; - setItemSearchResults(allRows.slice(start, start + s)); - setItemTotal(total); - setItemTotalPages(Math.max(1, Math.ceil(total / s))); + setItemSearchResults(rows); + setItemTotal(serverTotal); + setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); } catch { /* skip */ } finally { setItemSearchLoading(false); } @@ -1318,44 +1320,44 @@ export default function SalesOrderPage() {
- No - 품번 - 품명 - 규격 - 재질 - 포장재 - 단위 - 수량 - 포장수량 - 단가 - 금액 - 납기일 - 분할/삭제 + No + 품번 + 품명 + 규격 + 재질 + 포장재 + 단위 + 수량 + 포장수량 + 단가 + 금액 + 납기일 + 분할/삭제 {detailRows.map((row, idx) => ( {idx + 1} - - {row.part_code} + + {row.part_code} - - {row.part_name} + + {row.part_name} - {row.spec} - {row.material} + {row.spec} + {row.material} updateDetailRow(idx, "packing_material", e.target.value)} placeholder="포장재" - className="h-8 text-xs" + className="h-8 text-xs w-full" /> - - - 전체 - {(categoryOptions["item_division"] || []).map((o) => ( - {o.label} - ))} - - +
영업관리
diff --git a/frontend/app/(main)/COMPANY_7/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_7/logistics/outbound/page.tsx index 824e2e86..457e2723 100644 --- a/frontend/app/(main)/COMPANY_7/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_7/logistics/outbound/page.tsx @@ -38,7 +38,6 @@ import { Save, ChevronRight, ChevronLeft, - ChevronDown, ChevronsLeft, ChevronsRight, Inbox, @@ -116,41 +115,8 @@ const GRID_COLUMNS = [ { key: "remark", label: "비고" }, ]; -// 마스터 헤더 레이아웃 (출고번호 뒤) -const MASTER_BODY_LAYOUT = [ - { key: "outbound_type", label: "출고유형", colSpan: 1 }, - { key: "outbound_date", label: "출고일", colSpan: 1 }, - { key: "reference_number", label: "참조번호", colSpan: 1 }, - { key: "customer_name", label: "거래처", colSpan: 1 }, - { key: "warehouse_name", label: "창고", colSpan: 1 }, - { key: "outbound_status", label: "출고상태", colSpan: 1 }, - { key: "memo", label: "비고", colSpan: 1 }, -]; - -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "source_type", label: "출처" }, - { key: "item_code", label: "품목코드" }, - { key: "item_name", label: "품목명" }, - { key: "specification", label: "규격" }, - { key: "outbound_qty", label: "출고수량" }, - { key: "unit_price", label: "단가" }, - { key: "total_amount", label: "금액" }, -]; - -// 마스터 필드 키 목록 (필터 분류용) -const MASTER_KEYS = new Set(["outbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); - -// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) -const DETAIL_KEY_MAP: Record = { - source_type: "source_type", - item_number: "item_code", - item_name: "item_name", - spec: "specification", - outbound_qty: "outbound_qty", - unit_price: "unit_price", - total_amount: "total_amount", -}; +// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16 +const TOTAL_COLS = 16; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -258,30 +224,6 @@ interface SelectedSourceItem { export default function OutboundPage() { const ts = useTableSettings("c16-outbound", "outbound_mng", GRID_COLUMNS); - // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 - const visibleMasterLayout = useMemo(() => { - const ordered: typeof MASTER_BODY_LAYOUT = []; - for (const vc of ts.visibleColumns) { - const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); - if (m) ordered.push(m); - } - return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; - }, [ts.visibleColumns]); - - const visibleDetailCols = useMemo(() => { - const ordered: typeof DETAIL_HEADER_COLS = []; - for (const vc of ts.visibleColumns) { - const detailKey = DETAIL_KEY_MAP[vc.key]; - if (detailKey) { - const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); - if (d) ordered.push(d); - } - } - return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; - }, [ts.visibleColumns]); - - const TOTAL_COLS = 3 + visibleMasterLayout.length; - // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -290,9 +232,7 @@ export default function OutboundPage() { // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); - // 마스터-디테일 그룹 테이블 state - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); + // 헤더 필터 & 정렬 const [headerFilters, setHeaderFilters] = useState>>({}); const [sortState, setSortState] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -403,121 +343,59 @@ export default function OutboundPage() { })(); }, []); - // --- 마스터-디테일 그룹핑, 필터, 정렬 --- + // 플랫 행 생성 (출고유형 코드→라벨 변환, source_type→한글 등) + const flatRows = useMemo(() => { + return data.map((row) => ({ + ...row, + _raw_outbound_type: row.outbound_type, + outbound_type: resolveCat("outbound_type", row.outbound_type) || row.outbound_type || "", + source_type: row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "", + item_number: row.item_code || (row as any).item_number || "", + spec: row.specification || (row as any).spec || "", + })); + }, [data, resolveCat]); - // outbound_number 기준 그룹핑 + 필터 + 정렬 - const filteredGroups = useMemo(() => { - // 1차: outbound_number 기준 그룹핑 - const allGroups: Record = {}; - for (const row of data) { - const key = row.outbound_number || "_no_number"; - if (!allGroups[key]) { - allGroups[key] = { master: row, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - const raw = (group.master as any)?.[colKey] ?? ""; - return values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([outNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - let cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; - if (colKey === "source_type") cellVal = SOURCE_TYPE_LABEL[cellVal] || cellVal; - return values.has(cellVal); - }) - ); - return [outNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - entries.sort(([, a], [, b]) => { - const av = (a.master as any)?.[key] ?? ""; - const bv = (b.master as any)?.[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - let av: any = (a as any)[key] ?? ""; - let bv: any = (b as any)[key] ?? ""; - if (key === "source_type") { av = SOURCE_TYPE_LABEL[av] || av; bv = SOURCE_TYPE_LABEL[bv] || bv; } - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [data, headerFilters, sortState]); - - // 마스터 컬럼별 고유값 (헤더 필터용) - const masterUniqueValues = useMemo(() => { + // 컬럼별 고유값 (헤더 필터용) + const columnUniqueValues = useMemo(() => { const result: Record = {}; - const seenMasters = new Map(); - data.forEach((row) => { - if (row.outbound_number && !seenMasters.has(row.outbound_number)) { - seenMasters.set(row.outbound_number, row); - } - }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "outbound_number", label: "출고번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label }))]) { + for (const col of GRID_COLUMNS) { const values = new Set(); - masters.forEach((m) => { - const val = (m as any)?.[col.key]; + flatRows.forEach((row) => { + const val = (row as any)[col.key]; if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [data]); + }, [flatRows]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) - const columnUniqueValues = useMemo(() => { - const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { - const values = new Set(); - data.forEach((row) => { - let val = (row as any)[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "source_type") val = SOURCE_TYPE_LABEL[val] || val; - values.add(String(val)); - } + // 필터 + 정렬 적용된 플랫 데이터 + const filteredRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; + return values.has(cellVal); }); - result[col.key] = Array.from(values).sort(); } - return result; - }, [data]); + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = (a as any)[key] ?? ""; + const bv = (b as any)[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -938,7 +816,7 @@ export default function OutboundPage() { dataCount={data.length} /> - {/* 출고 목록 테이블 */} + {/* 출고 목록 테이블 (플랫 리스트) */}
{/* 패널 헤더 */}
@@ -946,7 +824,7 @@ export default function OutboundPage() { 출고 목록 - {data.length}건 + {filteredRows.length}건
@@ -971,78 +849,68 @@ export default function OutboundPage() {
-
+
- - {visibleMasterLayout.map((col) => ( - - ))} + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 출고번호 */} - -
-
handleSort("outbound_number")}> - 출고번호 - {sortState?.key === "outbound_number" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["outbound_number"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {GRID_COLUMNS.map((col) => { + const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} + + ); + })} @@ -1052,7 +920,7 @@ export default function OutboundPage() { - ) : Object.keys(filteredGroups).length === 0 ? ( + ) : filteredRows.length === 0 ? (
@@ -1062,214 +930,59 @@ export default function OutboundPage() { ) : ( - Object.entries(filteredGroups).map(([outboundNo, group]) => { - const isExpanded = expandedOrders.has(outboundNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 */} - { - if (expandedOrders.has(outboundNo)) { - setClosingOrders((prev) => new Set(prev).add(outboundNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(outboundNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(outboundNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(outboundNo)); - } - }} - onDoubleClick={() => openEditModal(group.details[0])} - > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 출고번호 */} - - {outboundNo} - ({group.details.length}) - - {/* 마스터 필드 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => { - switch (col.key) { - case "outbound_type": return ( - - - {resolveCat("outbound_type", master.outbound_type) || "-"} - - - ); - case "outbound_date": return ( - - {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} - - ); - case "reference_number": return ( - - {master.reference_number || ""} - - ); - case "customer_name": return ( - - {master.customer_name || ""} - - ); - case "warehouse_name": return ( - - {master.warehouse_name || master.warehouse_code || ""} - - ); - case "outbound_status": return ( - - - {master.outbound_status || "-"} - - - ); - case "memo": return ( - - {master.memo || ""} - - ); - default: return {(master as any)[col.key] ?? ""}; - } - })} - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - - {visibleDetailCols.map((col) => { - const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => { - let v = (d as any)[col.key]; - if (col.key === "source_type") v = SOURCE_TYPE_LABEL[v] || v; - return v; - }).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} -
+ { - const isClosing = closingOrders.has(outboundNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - onDoubleClick={() => openEditModal(row)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - - {visibleDetailCols.map((col) => { - switch (col.key) { - case "source_type": return {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"}; - case "item_code": return {row.item_code || ""}; - case "item_name": return {row.item_name || ""}; - case "specification": return {row.specification || ""}; - case "outbound_qty": return {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}; - case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; - case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; - default: return {(row as any)[col.key] ?? ""}; - } - })} - + onClick={() => { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] ); - })} - + }} + onDoubleClick={() => openEditModal(row as any)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + > + {}} /> + + {row.outbound_number || ""} + + + {row.outbound_type || "-"} + + + + {row.outbound_date ? new Date(row.outbound_date).toLocaleDateString("ko-KR") : ""} + + {row.reference_number || ""} + {row.source_type || ""} + {row.customer_name || ""} + {row.item_number || ""} + {row.item_name || ""} + {row.spec || ""} + {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {row.warehouse_name || row.warehouse_code || ""} + + + {row.outbound_status || "-"} + + + {row.remark || row.memo || ""} + ); }) )} diff --git a/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx index d3d1e955..77d21e7b 100644 --- a/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx @@ -43,7 +43,6 @@ import { X, Save, ChevronRight, - ChevronDown, ChevronLeft, ChevronsLeft, ChevronsRight, @@ -64,6 +63,7 @@ import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSea import { getReceivingList, createReceiving, + updateReceiving, deleteReceiving, generateReceivingNumber, getReceivingWarehouses, @@ -95,41 +95,8 @@ const GRID_COLUMNS = [ { key: "remark", label: "비고" }, ]; -// 마스터 헤더 레이아웃 (입고번호 뒤, 디테일 7컬럼 위에 colSpan으로 맵핑) -const MASTER_BODY_LAYOUT = [ - { key: "inbound_type", label: "입고유형", colSpan: 1 }, - { key: "inbound_date", label: "입고일", colSpan: 1 }, - { key: "reference_number", label: "참조번호", colSpan: 1 }, - { key: "supplier_name", label: "공급처", colSpan: 1 }, - { key: "warehouse_name", label: "창고", colSpan: 1 }, - { key: "inbound_status", label: "입고상태", colSpan: 1 }, - { key: "memo", label: "비고", colSpan: 1 }, -]; - -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "source_table", label: "출처" }, - { key: "item_number", label: "품목코드" }, - { key: "item_name", label: "품목명" }, - { key: "spec", label: "규격" }, - { key: "inbound_qty", label: "입고수량" }, - { key: "unit_price", label: "단가" }, - { key: "total_amount", label: "금액" }, -]; - -// 마스터 필드 키 목록 (필터 분류용) -const MASTER_KEYS = new Set(["inbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); - -// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) -const DETAIL_KEY_MAP: Record = { - source_type: "source_table", - item_number: "item_number", - item_name: "item_name", - spec: "spec", - inbound_qty: "inbound_qty", - unit_price: "unit_price", - total_amount: "total_amount", -}; +// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16 +const TOTAL_COLS = 16; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -287,30 +254,6 @@ interface SelectedSourceItem { export default function ReceivingPage() { const ts = useTableSettings("c16-receiving", "inbound_mng", GRID_COLUMNS); - // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 - const visibleMasterLayout = useMemo(() => { - const ordered: typeof MASTER_BODY_LAYOUT = []; - for (const vc of ts.visibleColumns) { - const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); - if (m) ordered.push(m); - } - return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; - }, [ts.visibleColumns]); - - const visibleDetailCols = useMemo(() => { - const ordered: typeof DETAIL_HEADER_COLS = []; - for (const vc of ts.visibleColumns) { - const detailKey = DETAIL_KEY_MAP[vc.key]; - if (detailKey) { - const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); - if (d) ordered.push(d); - } - } - return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; - }, [ts.visibleColumns]); - - const TOTAL_COLS = 3 + visibleMasterLayout.length; - // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -319,9 +262,7 @@ export default function ReceivingPage() { // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); - // 마스터-디테일 그룹 테이블 - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); + // 헤더 필터 & 정렬 const [headerFilters, setHeaderFilters] = useState>>({}); const [sortState, setSortState] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -338,6 +279,10 @@ export default function ReceivingPage() { const [selectedItems, setSelectedItems] = useState([]); const [saving, setSaving] = useState(false); + // 수정 모드 + const [editMode, setEditMode] = useState(false); + const [editItemIds, setEditItemIds] = useState([]); + // 소스 데이터 const [sourceKeyword, setSourceKeyword] = useState(""); const [sourceLoading, setSourceLoading] = useState(false); @@ -377,7 +322,7 @@ export default function ReceivingPage() { Promise.all( ["material", "unit"].map(async (col) => { try { - const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`); + const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`); const items = flatten(res.data?.data || []); map[col] = {}; for (const item of items) map[col][item.code] = item.label; @@ -434,124 +379,56 @@ export default function ReceivingPage() { })(); }, []); - // 필터 + 정렬 적용된 데이터 -> 그룹핑 - const filteredGroups = useMemo(() => { - // 1차: inbound_number 기준 그룹핑 - const allGroups: Record = {}; - for (const row of data) { - const key = row.inbound_number || "_no_inbound"; - if (!allGroups[key]) { - allGroups[key] = { master: row, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - let raw = (group.master as any)?.[colKey] ?? ""; - // 입고유형은 코드→라벨 변환된 값으로 비교 - if (colKey === "inbound_type") raw = resolveInboundType(String(raw)); - return values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([inboundNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - let cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; - if (colKey === "source_table") cellVal = SOURCE_TABLE_LABEL[cellVal] || cellVal; - return values.has(cellVal); - }) - ); - return [inboundNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - entries.sort(([, a], [, b]) => { - let av: any = (a.master as any)?.[key] ?? ""; - let bv: any = (b.master as any)?.[key] ?? ""; - if (key === "inbound_type") { av = resolveInboundType(String(av)); bv = resolveInboundType(String(bv)); } - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - const av = (a as any)[key] ?? ""; - const bv = (b as any)[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [data, headerFilters, sortState]); - - // 마스터 컬럼별 고유값 (마스터 헤더 필터용) - const masterUniqueValues = useMemo(() => { - const result: Record = {}; - const seenMasters = new Map(); - data.forEach((row) => { - if (row.inbound_number && !seenMasters.has(row.inbound_number)) { - seenMasters.set(row.inbound_number, row); - } - }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "inbound_number", label: "입고번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label }))]) { - const values = new Set(); - masters.forEach((m) => { - let val = (m as any)?.[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "inbound_type") val = resolveInboundType(String(val)); - values.add(String(val)); - } - }); - result[col.key] = Array.from(values).sort(); - } - return result; + // 플랫 행 생성 (입고유형 코드→라벨 변환, source_table→한글 등) + const flatRows = useMemo(() => { + return data.map((row) => ({ + ...row, + inbound_type: resolveInboundType(row.inbound_type), + source_type: row.source_table ? (SOURCE_TABLE_LABEL[row.source_table] || row.source_table) : (row as any).source_type || "", + })); }, [data]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) + // 컬럼별 고유값 (헤더 필터용) const columnUniqueValues = useMemo(() => { const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { + for (const col of GRID_COLUMNS) { const values = new Set(); - data.forEach((row) => { - let val = (row as any)[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "source_table") val = SOURCE_TABLE_LABEL[val] || val; - values.add(String(val)); - } + flatRows.forEach((row) => { + const val = (row as any)[col.key]; + if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [data]); + }, [flatRows]); + + // 필터 + 정렬 적용된 플랫 데이터 + const filteredRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; + return values.has(cellVal); + }); + } + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = (a as any)[key] ?? ""; + const bv = (b as any)[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -633,6 +510,8 @@ export default function ReceivingPage() { const openRegisterModal = async () => { const defaultType = "구매입고"; + setEditMode(false); + setEditItemIds([]); setModalInboundType(defaultType); setModalInboundDate(new Date().toISOString().split("T")[0]); setModalWarehouse(""); @@ -661,6 +540,51 @@ export default function ReceivingPage() { } }; + // 수정 모달 열기 + const openEditModal = (row: InboundItem) => { + const inNo = row.inbound_number; + const grouped = data.filter((d) => d.inbound_number === inNo); + const first = grouped[0] || row; + + setEditMode(true); + setEditItemIds(grouped.map((g) => g.id)); + setModalInboundNo(inNo); + setModalInboundType(first.inbound_type || "구매입고"); + setModalInboundDate(first.inbound_date ? String(first.inbound_date).slice(0, 10) : ""); + setModalWarehouse(first.warehouse_code || ""); + setModalLocation(first.location_code || ""); + setModalInspector((first as any).inspector || ""); + setModalManager((first as any).manager || ""); + setModalMemo(first.memo || ""); + setSelectedItems( + grouped.map((g) => ({ + key: g.id, + inbound_type: g.inbound_type || "", + reference_number: g.reference_number || "", + supplier_code: (g as any).supplier_code || "", + supplier_name: g.supplier_name || "", + item_number: g.item_number || "", + item_name: g.item_name || "", + spec: g.spec || "", + material: (g as any).material || "", + unit: (g as any).unit || "", + inbound_qty: Number(g.inbound_qty) || 0, + unit_price: Number(g.unit_price) || 0, + total_amount: Number(g.total_amount) || 0, + source_table: (g as any).source_table || "", + source_id: (g as any).source_id || "", + })) + ); + setSourceKeyword(""); + setPurchaseOrders([]); + setShipments([]); + setItems([]); + setSourcePage(1); + setSourceTotalCount(0); + setIsModalOpen(true); + loadSourceData(first.inbound_type || "구매입고", undefined, 1); + }; + // 검색 버튼 클릭 시 const searchSourceData = useCallback(async () => { setSourcePage(1); @@ -811,41 +735,97 @@ export default function ReceivingPage() { } setSaving(true); try { - const res = await createReceiving({ - inbound_number: modalInboundNo, - inbound_date: modalInboundDate, - warehouse_code: modalWarehouse, - location_code: modalLocation || undefined, - inspector: modalInspector || undefined, - manager: modalManager || undefined, - memo: modalMemo || undefined, - items: selectedItems.map((item) => ({ - inbound_type: item.inbound_type, - reference_number: item.reference_number, - supplier_code: item.supplier_code, - supplier_name: item.supplier_name, - item_number: item.item_number, - item_name: item.item_name, - spec: item.spec, - material: item.material, - unit: item.unit, - inbound_qty: item.inbound_qty, - unit_price: item.unit_price, - total_amount: item.total_amount, - source_table: item.source_table, - source_id: item.source_id, - inbound_status: "입고완료", - inspection_status: "대기", - })), - }); + if (editMode) { + // 기존 item 수정 + 삭제된 item 삭제 + 새 item 추가 + const currentKeys = new Set(selectedItems.map((i) => i.key)); + const toDelete = editItemIds.filter((id) => !currentKeys.has(id)); + const toUpdate = selectedItems.filter((i) => editItemIds.includes(i.key)); + const toCreate = selectedItems.filter((i) => !editItemIds.includes(i.key)); - if (res.success) { - alert(res.message || "입고 등록 완료"); + await Promise.all([ + ...toDelete.map((id) => deleteReceiving(id)), + ...toUpdate.map((item) => + updateReceiving(item.key, { + inbound_date: modalInboundDate, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + warehouse_code: modalWarehouse || undefined, + location_code: modalLocation || undefined, + memo: modalMemo || undefined, + } as any) + ), + ...(toCreate.length > 0 + ? [createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: toCreate.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + })] + : []), + ]); + toast.success("입고 정보를 수정했어요"); setIsModalOpen(false); fetchList(); + } else { + const res = await createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: selectedItems.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + }); + + if (res.success) { + toast.success(res.message || "입고 등록 완료"); + setIsModalOpen(false); + fetchList(); + } } - } catch { - alert("입고 등록 중 오류가 발생했습니다."); + } catch (err: any) { + const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "입고 등록 중 오류가 발생했습니다."); + toast.error(msg); } finally { setSaving(false); } @@ -897,13 +877,13 @@ export default function ReceivingPage() { } /> - {/* 입고 목록 테이블 (마스터-디테일 그룹) */} + {/* 입고 목록 테이블 (플랫 리스트) */}

입고 목록

- {Object.keys(filteredGroups).length}건 + {filteredRows.length}건
-
+
- - {visibleMasterLayout.map((col) => ( - - ))} + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 입고번호 (별도 컬럼) */} - -
-
handleSort("inbound_number")}> - 입고번호 - {sortState?.key === "inbound_number" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["inbound_number"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {GRID_COLUMNS.map((col) => { + const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} + + ); + })} @@ -993,7 +963,7 @@ export default function ReceivingPage() { - ) : Object.keys(filteredGroups).length === 0 ? ( + ) : filteredRows.length === 0 ? (
@@ -1003,212 +973,59 @@ export default function ReceivingPage() { ) : ( - Object.entries(filteredGroups).map(([inboundNo, group]) => { - const isExpanded = expandedOrders.has(inboundNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 */} - { - if (expandedOrders.has(inboundNo)) { - setClosingOrders((prev) => new Set(prev).add(inboundNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(inboundNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(inboundNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(inboundNo)); - } + { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + onDoubleClick={() => openEditModal(row as any)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); }} > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 입고번호 */} - - {inboundNo} - ({group.details.length}) - - {/* 마스터 필드 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => { - switch (col.key) { - case "inbound_type": return ( - - - {resolveInboundType(master.inbound_type)} - - - ); - case "inbound_date": return ( - - {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} - - ); - case "reference_number": return ( - - {master.reference_number || ""} - - ); - case "supplier_name": return ( - - {master.supplier_name || ""} - - ); - case "warehouse_name": return ( - - {master.warehouse_name || master.warehouse_code || ""} - - ); - case "inbound_status": return ( - - - {master.inbound_status || "-"} - - - ); - case "memo": return ( - - {master.memo || ""} - - ); - default: return {(master as any)[col.key] ?? ""}; - } - })} - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - - {visibleDetailCols.map((col) => { - const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => { - let v = (d as any)[col.key]; - if (col.key === "source_table") v = SOURCE_TABLE_LABEL[v] || v; - return v; - }).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} -
- )} - - {/* 디테일 행 (펼쳤을 때만) */} - {isExpanded && group.details.map((row, detailIdx) => { - const isClosing = closingOrders.has(inboundNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - - {visibleDetailCols.map((col) => { - switch (col.key) { - case "source_table": return {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"}; - case "item_number": return {row.item_number || ""}; - case "item_name": return {row.item_name || ""}; - case "spec": return {row.spec || ""}; - case "inbound_qty": return {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}; - case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; - case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; - default: return {(row as any)[col.key] ?? ""}; - } - })} - - ); - })} - + {}} /> + + {row.inbound_number || ""} + + + {row.inbound_type || "-"} + + + + {row.inbound_date ? new Date(row.inbound_date).toLocaleDateString("ko-KR") : ""} + + {row.reference_number || ""} + {row.source_type || ""} + {row.supplier_name || ""} + {row.item_number || ""} + {row.item_name || ""} + {row.spec || ""} + {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {row.warehouse_name || (row as any).warehouse_code || ""} + + + {row.inbound_status || "-"} + + + {row.remark || row.memo || ""} + ); }) )} @@ -1221,9 +1038,9 @@ export default function ReceivingPage() { - 입고 등록 + {editMode ? "입고 수정" : "입고 등록"} - 입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해 주세요. + {editMode ? "입고 정보를 수정해 주세요." : "입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해 주세요."}
diff --git a/frontend/app/(main)/COMPANY_7/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_7/production/plan-management/page.tsx index 38b01658..534582f9 100644 --- a/frontend/app/(main)/COMPANY_7/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_7/production/plan-management/page.tsx @@ -1053,14 +1053,14 @@ export default function ProductionPlanManagementPage() { return (
- + - + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - - 품목코드 - 품목명 + + 품목코드 + 품목명 {ts.visibleColumns.map((col) => ( {col.label} @@ -1073,9 +1073,9 @@ export default function ProductionPlanManagementPage() { if (item._isGroupSummary) { return ( - - - + + + {ts.visibleColumns.map((col) => { const v = (item as any)[col.key]; return ( @@ -1090,14 +1090,14 @@ export default function ProductionPlanManagementPage() { return ( - e.stopPropagation()}> + e.stopPropagation()}> toggleItemGroupSelect(item.item_code)} className="h-4 w-4" /> - toggleItemExpand(item.item_code)}> + toggleItemExpand(item.item_code)}> - toggleItemExpand(item.item_code)}>{item.item_code} - toggleItemExpand(item.item_code)}>{item.item_name} + toggleItemExpand(item.item_code)}>{item.item_code} + toggleItemExpand(item.item_code)}>{item.item_name} {ts.visibleColumns.map((col) => renderGroupCell(col, item))} {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => { @@ -1107,9 +1107,9 @@ export default function ProductionPlanManagementPage() { } return ( - - - + + +
수주번호: {detail.order_no} diff --git a/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx index 143a84a9..f712d3ed 100644 --- a/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx @@ -13,7 +13,7 @@ import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, - ClipboardList, Pencil, Search, X, Package, ChevronDown, + ClipboardList, Pencil, Search, X, Package, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, GripVertical, } from "lucide-react"; @@ -88,18 +88,18 @@ const GRID_COLUMNS_CONFIG = [ ]; const MODAL_DETAIL_COLUMNS = [ - { key: "item_code", label: "품번", width: "w-[120px]" }, - { key: "item_name", label: "품명", width: "w-[120px]" }, - { key: "supplier", label: "공급업체", width: "w-[150px]" }, - { key: "spec", label: "규격", width: "w-[80px]" }, - { key: "unit", label: "단위", width: "w-[60px]" }, - { key: "order_qty", label: "발주수량", width: "w-[90px]" }, - { key: "received_qty", label: "입고수량", width: "w-[90px]" }, - { key: "remain_qty", label: "잔량", width: "w-[80px]" }, - { key: "unit_price", label: "단가", width: "w-[100px]" }, - { key: "amount", label: "금액", width: "w-[100px]" }, - { key: "due_date", label: "납기일", width: "w-[160px]" }, - { key: "memo", label: "메모", width: "w-[120px]" }, + { key: "item_code", label: "품번", width: "min-w-[120px]" }, + { key: "item_name", label: "품명", width: "min-w-[150px]" }, + { key: "supplier", label: "공급업체", width: "min-w-[150px]" }, + { key: "spec", label: "규격", width: "min-w-[80px]" }, + { key: "unit", label: "단위", width: "min-w-[90px]" }, + { key: "order_qty", label: "발주수량", width: "min-w-[90px]" }, + { key: "received_qty", label: "입고수량", width: "min-w-[90px]" }, + { key: "remain_qty", label: "잔량", width: "min-w-[80px]" }, + { key: "unit_price", label: "단가", width: "min-w-[100px]" }, + { key: "amount", label: "금액", width: "min-w-[100px]" }, + { key: "due_date", label: "납기일", width: "min-w-[160px]" }, + { key: "memo", label: "메모", width: "min-w-[120px]" }, ]; const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16"; @@ -162,7 +162,6 @@ export default function PurchaseOrderPage() { const [excelUploadOpen, setExcelUploadOpen] = useState(false); const [categoryOptions, setCategoryOptions] = useState>({}); const [checkedIds, setCheckedIds] = useState([]); - const [expandedOrders, setExpandedOrders] = useState>(new Set()); // 테이블 설정 const ts = useTableSettings("c16-purchase-order", DETAIL_TABLE, GRID_COLUMNS_CONFIG); @@ -238,7 +237,7 @@ export default function PurchaseOrderPage() { ); try { const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, size: 5000, autoFilter: true, }); const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || []; optMap["supplier_code"] = supps.map((s: any) => ({ @@ -248,7 +247,7 @@ export default function PurchaseOrderPage() { } catch { /* skip */ } try { const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, size: 5000, autoFilter: true, }); const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; optMap["manager"] = users.map((u: any) => ({ @@ -294,7 +293,7 @@ export default function PurchaseOrderPage() { } const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined, autoFilter: true, sort: { columnName: "purchase_no", order: "desc" }, @@ -372,20 +371,6 @@ export default function PurchaseOrderPage() { useEffect(() => { fetchOrders(); }, [fetchOrders]); // purchase_no 기준 그룹핑 - const orderGroups = useMemo(() => { - const map: Record = {}; - for (const row of orders) { - const key = row.purchase_no || row.id; - if (!map[key]) { - map[key] = { - master: { purchase_no: row.purchase_no, order_date: row.order_date, supplier_name: row.supplier_name, status: row.status, memo: row.memo }, - details: [], - }; - } - map[key].details.push(row); - } - return map; - }, [orders]); const getCategoryLabel = (col: string, code: string) => { if (!code) return ""; @@ -553,7 +538,7 @@ export default function PurchaseOrderPage() { } }; - // 품목 검색 + // 품목 검색 (수주관리와 동일한 서버 페이징 방식) const searchItems = async (page?: number, size?: number) => { const p = page ?? itemPage; const s = size ?? itemPageSize; @@ -563,25 +548,24 @@ export default function PurchaseOrderPage() { if (itemSearchKeyword) { filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); } + // 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응) + if (itemSearchDivision !== "all") { + const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; + const divValues = [itemSearchDivision]; + if (divLabel) divValues.push(divLabel); + filters.push({ columnName: "division", operator: "in", value: divValues }); + } const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: p, size: s, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const resData = res.data?.data; - let allRows = resData?.data || resData?.rows || []; - if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - allRows = allRows.filter((item: any) => { - const div = item.division || ""; - return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel)); - }); - } - const total = allRows.length; - const start = (p - 1) * s; - setItemSearchResults(allRows.slice(start, start + s)); - setItemTotal(total); - setItemTotalPages(Math.max(1, Math.ceil(total / s))); + const rows = resData?.data || resData?.rows || []; + const serverTotal = resData?.total || resData?.totalCount || rows.length; + setItemSearchResults(rows); + setItemTotal(serverTotal); + setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); } catch { /* skip */ } finally { setItemSearchLoading(false); } @@ -623,7 +607,7 @@ export default function PurchaseOrderPage() { try { const itemIds = selected.map((item) => item.item_number || item.id); const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [ @@ -686,7 +670,7 @@ export default function PurchaseOrderPage() { if (itemCodes.length === 0) return; try { const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, autoFilter: true, }); @@ -708,7 +692,7 @@ export default function PurchaseOrderPage() { if (itemCodes.length === 0) return; try { const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [ { columnName: "supplier_id", operator: "equals", value: supplierCode }, { columnName: "item_id", operator: "in", value: itemCodes }, @@ -811,125 +795,83 @@ export default function PurchaseOrderPage() {
- {/* 데이터 테이블 — 아코디언 그룹핑 */} + {/* 데이터 테이블 — 플랫 리스트 */}
{loading ? (
- ) : Object.keys(orderGroups).length === 0 ? ( + ) : orders.length === 0 ? (

등록된 발주가 없어요

) : ( (() => { - const MASTER_KEYS = new Set(["purchase_no", "order_date", "supplier_name", "status", "memo"]); const numCols = new Set(["order_qty", "received_qty", "remain_qty", "unit_price", "amount"]); - - // ts.visibleColumns 순서를 따르되, 마스터/디테일 컬럼을 분리 - // 고정 컬럼(품목수)은 마스터 선행 컬럼 뒤에 배치 - const leadingMaster: typeof ts.visibleColumns = []; - const detailCols: typeof ts.visibleColumns = []; - const trailingMaster: typeof ts.visibleColumns = []; - let passedFirstDetail = false; - for (const col of ts.visibleColumns) { - if (MASTER_KEYS.has(col.key)) { - if (passedFirstDetail) trailingMaster.push(col); - else leadingMaster.push(col); - } else { - passedFirstDetail = true; - detailCols.push(col); - } - } - - const renderDetailCell = (row: any, key: string) => { + const renderCell = (row: any, key: string) => { const val = row[key]; if (key === "status") return val ? {val} : "-"; - if (numCols.has(key)) return {val ? Number(val).toLocaleString() : "0"}; - return val || "-"; + if (key === "order_date" || key === "due_date") return val ? new Date(val).toLocaleDateString("ko-KR") : "-"; + if (numCols.has(key)) return {val ? Number(val).toLocaleString() : ""}; + return val || ""; }; - - const renderMasterHead = (col: { key: string; label: string }) => ( - - {col.label} - - ); - - const renderMasterCell = (col: { key: string }, m: any, purchaseNo: string) => { - if (col.key === "purchase_no") return {purchaseNo}; - if (col.key === "order_date") return {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}; - if (col.key === "supplier_name") return {m.supplier_name || "-"}; - if (col.key === "status") return {m.status && {m.status}}; - if (col.key === "memo") return {m.memo || ""}; - return ; - }; - return (
- - - {leadingMaster.map(renderMasterHead)} - 품목수 - {detailCols.map(col => ( - - {col.label}{col.key === "order_qty" || col.key === "amount" ? " 합계" : ""} + { + const allIds = orders.map((r) => r.id); + const allChecked = allIds.length > 0 && allIds.every((id) => checkedIds.includes(id)); + setCheckedIds(allChecked ? [] : allIds); + }} + > + 0 && orders.every((r) => checkedIds.includes(r.id))} + onCheckedChange={() => {}} + /> + + {ts.visibleColumns.map((col) => ( + + {col.label} ))} - {trailingMaster.map(renderMasterHead)} - {Object.entries(orderGroups).map(([purchaseNo, group]) => { - const isExpanded = expandedOrders.has(purchaseNo); - const detailIds = group.details.map(d => d.id); - const allChecked = detailIds.length > 0 && detailIds.every(id => checkedIds.includes(id)); - const someChecked = detailIds.some(id => checkedIds.includes(id)); - const m = group.master; - const totalQty = group.details.reduce((s, d) => s + (Number(d.order_qty) || 0), 0); - const totalAmt = group.details.reduce((s, d) => s + (Number(d.amount) || 0), 0); + {orders.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - setExpandedOrders(prev => { const next = new Set(prev); if (next.has(purchaseNo)) next.delete(purchaseNo); else next.add(purchaseNo); return next; })} - onDoubleClick={() => openEditModal(purchaseNo)} + { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + onDoubleClick={() => openEditModal(row.purchase_no)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} > - - + {}} /> + + {ts.visibleColumns.map((col) => ( + + {renderCell(row, col.key)} - { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !detailIds.includes(id)) : [...new Set([...prev, ...detailIds])]); }}> - {}} /> - - {leadingMaster.map(col => renderMasterCell(col, m, purchaseNo))} - {group.details.length}건 - {detailCols.map(col => ( - - {col.key === "order_qty" ? totalQty.toLocaleString() - : col.key === "amount" ? totalAmt.toLocaleString() - : ""} - - ))} - {trailingMaster.map(col => renderMasterCell(col, m, purchaseNo))} - - {isExpanded && group.details.map((row) => ( - - - { e.stopPropagation(); setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id]); }}> - {}} /> - - {leadingMaster.map(col => )} - - {detailCols.map(col => ( - - {renderDetailCell(row, col.key)} - - ))} - {trailingMaster.map(col => )} - ))} - + ); })} @@ -938,7 +880,7 @@ export default function PurchaseOrderPage() { })() )}
- 전체 {Object.keys(orderGroups).length}건 (발주 기준) / {orders.length}건 (품목 기준) + 전체 {orders.length}건
@@ -1094,7 +1036,7 @@ export default function PurchaseOrderPage() { ) : (
-
+
c.key)} strategy={horizontalListSortingStrategy}> @@ -1118,12 +1060,12 @@ export default function PurchaseOrderPage() { {visibleModalColumns.map((col) => { switch (col.key) { case "item_code": - return {row.item_code}; + return {row.item_code}; case "item_name": - return {row.item_name}; + return {row.item_name}; case "supplier": return ( - + {isReadOnly ? ( {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} ) : ( @@ -1133,7 +1075,7 @@ export default function PurchaseOrderPage() { updateDetailRow(idx, "supplier_code", v); updateDetailRow(idx, "supplier_name", name); }}> - + {(categoryOptions["supplier_code"] || []).map(o => ( {o.label} @@ -1149,11 +1091,11 @@ export default function PurchaseOrderPage() { return {row.unit}; case "order_qty": return ( - + {isReadOnly ? ( {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} ) : ( - updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" /> )} ); @@ -1163,11 +1105,11 @@ export default function PurchaseOrderPage() { return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; case "unit_price": return ( - + {isReadOnly ? ( {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} ) : ( - updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" /> )} ); @@ -1175,21 +1117,21 @@ export default function PurchaseOrderPage() { return {row.amount ? Number(row.amount).toLocaleString() : ""}; case "due_date": return ( - + {isReadOnly ? ( {row.due_date} ) : ( - updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> + updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs w-full" /> )} ); case "memo": return ( - + {isReadOnly ? ( {row.memo} ) : ( - updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> + updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs w-full" /> )} ); diff --git a/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx index 05facbbc..863c0aef 100644 --- a/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx @@ -424,9 +424,10 @@ export default function ItemInspectionInfoPage() { case "inspection_type": return (
- {group.types.map((t: string) => ( - {t} - ))} + {group.types.map((t: string) => { + const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t; + return {label}; + })}
); @@ -494,7 +495,7 @@ export default function ItemInspectionInfoPage() { : "bg-muted/50 text-muted-foreground border-border hover:bg-muted" )} > - {type} + {inspTypeCatOptions.find((o) => o.code === type)?.label || type} o.code === itemSearchDivision)?.label || ""; + // 코드 또는 라벨이 저장된 경우 모두 조회하기 위해 in 연산자 사용 + const divValues = [itemSearchDivision]; + if (divLabel) divValues.push(divLabel); + filters.push({ columnName: "division", operator: "in", value: divValues }); + } + // 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링 const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI"; const partnerId = masterForm.partner_id; @@ -632,7 +641,7 @@ export default function SalesOrderPage() { if (isCustomerPrice && partnerId) { try { const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] }, autoFilter: true, }); @@ -642,31 +651,22 @@ export default function SalesOrderPage() { } const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: p, size: s, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const resData = res.data?.data; - let allRows = resData?.data || resData?.rows || []; + let rows = resData?.data || resData?.rows || []; + const serverTotal = resData?.total || resData?.totalCount || rows.length; - // 거래처우선일 때 연결된 품목만 표시 + // 거래처우선일 때 연결된 품목만 표시 (클라이언트 필터) if (customerItemIds) { - allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); + rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); } - // 관리품목 필터 (코드/라벨 혼재 대응) - if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - allRows = allRows.filter((item: any) => { - const div = item.division || ""; - return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel)); - }); - } - const total = allRows.length; - const start = (p - 1) * s; - setItemSearchResults(allRows.slice(start, start + s)); - setItemTotal(total); - setItemTotalPages(Math.max(1, Math.ceil(total / s))); + setItemSearchResults(rows); + setItemTotal(serverTotal); + setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); } catch { /* skip */ } finally { setItemSearchLoading(false); } @@ -1320,44 +1320,44 @@ export default function SalesOrderPage() {
- No - 품번 - 품명 - 규격 - 재질 - 포장재 - 단위 - 수량 - 포장수량 - 단가 - 금액 - 납기일 - 분할/삭제 + No + 품번 + 품명 + 규격 + 재질 + 포장재 + 단위 + 수량 + 포장수량 + 단가 + 금액 + 납기일 + 분할/삭제 {detailRows.map((row, idx) => ( {idx + 1} - - {row.part_code} + + {row.part_code} - - {row.part_name} + + {row.part_name} - {row.spec} - {row.material} + {row.spec} + {row.material} updateDetailRow(idx, "packing_material", e.target.value)} placeholder="포장재" - className="h-8 text-xs" + className="h-8 text-xs w-full" /> - - - 전체 - {(categoryOptions["item_division"] || []).map((o) => ( - {o.label} - ))} - - +
영업관리
diff --git a/frontend/app/(main)/COMPANY_8/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_8/logistics/outbound/page.tsx index 824e2e86..457e2723 100644 --- a/frontend/app/(main)/COMPANY_8/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_8/logistics/outbound/page.tsx @@ -38,7 +38,6 @@ import { Save, ChevronRight, ChevronLeft, - ChevronDown, ChevronsLeft, ChevronsRight, Inbox, @@ -116,41 +115,8 @@ const GRID_COLUMNS = [ { key: "remark", label: "비고" }, ]; -// 마스터 헤더 레이아웃 (출고번호 뒤) -const MASTER_BODY_LAYOUT = [ - { key: "outbound_type", label: "출고유형", colSpan: 1 }, - { key: "outbound_date", label: "출고일", colSpan: 1 }, - { key: "reference_number", label: "참조번호", colSpan: 1 }, - { key: "customer_name", label: "거래처", colSpan: 1 }, - { key: "warehouse_name", label: "창고", colSpan: 1 }, - { key: "outbound_status", label: "출고상태", colSpan: 1 }, - { key: "memo", label: "비고", colSpan: 1 }, -]; - -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "source_type", label: "출처" }, - { key: "item_code", label: "품목코드" }, - { key: "item_name", label: "품목명" }, - { key: "specification", label: "규격" }, - { key: "outbound_qty", label: "출고수량" }, - { key: "unit_price", label: "단가" }, - { key: "total_amount", label: "금액" }, -]; - -// 마스터 필드 키 목록 (필터 분류용) -const MASTER_KEYS = new Set(["outbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); - -// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) -const DETAIL_KEY_MAP: Record = { - source_type: "source_type", - item_number: "item_code", - item_name: "item_name", - spec: "specification", - outbound_qty: "outbound_qty", - unit_price: "unit_price", - total_amount: "total_amount", -}; +// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16 +const TOTAL_COLS = 16; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -258,30 +224,6 @@ interface SelectedSourceItem { export default function OutboundPage() { const ts = useTableSettings("c16-outbound", "outbound_mng", GRID_COLUMNS); - // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 - const visibleMasterLayout = useMemo(() => { - const ordered: typeof MASTER_BODY_LAYOUT = []; - for (const vc of ts.visibleColumns) { - const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); - if (m) ordered.push(m); - } - return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; - }, [ts.visibleColumns]); - - const visibleDetailCols = useMemo(() => { - const ordered: typeof DETAIL_HEADER_COLS = []; - for (const vc of ts.visibleColumns) { - const detailKey = DETAIL_KEY_MAP[vc.key]; - if (detailKey) { - const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); - if (d) ordered.push(d); - } - } - return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; - }, [ts.visibleColumns]); - - const TOTAL_COLS = 3 + visibleMasterLayout.length; - // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -290,9 +232,7 @@ export default function OutboundPage() { // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); - // 마스터-디테일 그룹 테이블 state - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); + // 헤더 필터 & 정렬 const [headerFilters, setHeaderFilters] = useState>>({}); const [sortState, setSortState] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -403,121 +343,59 @@ export default function OutboundPage() { })(); }, []); - // --- 마스터-디테일 그룹핑, 필터, 정렬 --- + // 플랫 행 생성 (출고유형 코드→라벨 변환, source_type→한글 등) + const flatRows = useMemo(() => { + return data.map((row) => ({ + ...row, + _raw_outbound_type: row.outbound_type, + outbound_type: resolveCat("outbound_type", row.outbound_type) || row.outbound_type || "", + source_type: row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "", + item_number: row.item_code || (row as any).item_number || "", + spec: row.specification || (row as any).spec || "", + })); + }, [data, resolveCat]); - // outbound_number 기준 그룹핑 + 필터 + 정렬 - const filteredGroups = useMemo(() => { - // 1차: outbound_number 기준 그룹핑 - const allGroups: Record = {}; - for (const row of data) { - const key = row.outbound_number || "_no_number"; - if (!allGroups[key]) { - allGroups[key] = { master: row, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - const raw = (group.master as any)?.[colKey] ?? ""; - return values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([outNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - let cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; - if (colKey === "source_type") cellVal = SOURCE_TYPE_LABEL[cellVal] || cellVal; - return values.has(cellVal); - }) - ); - return [outNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - entries.sort(([, a], [, b]) => { - const av = (a.master as any)?.[key] ?? ""; - const bv = (b.master as any)?.[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - let av: any = (a as any)[key] ?? ""; - let bv: any = (b as any)[key] ?? ""; - if (key === "source_type") { av = SOURCE_TYPE_LABEL[av] || av; bv = SOURCE_TYPE_LABEL[bv] || bv; } - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [data, headerFilters, sortState]); - - // 마스터 컬럼별 고유값 (헤더 필터용) - const masterUniqueValues = useMemo(() => { + // 컬럼별 고유값 (헤더 필터용) + const columnUniqueValues = useMemo(() => { const result: Record = {}; - const seenMasters = new Map(); - data.forEach((row) => { - if (row.outbound_number && !seenMasters.has(row.outbound_number)) { - seenMasters.set(row.outbound_number, row); - } - }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "outbound_number", label: "출고번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label }))]) { + for (const col of GRID_COLUMNS) { const values = new Set(); - masters.forEach((m) => { - const val = (m as any)?.[col.key]; + flatRows.forEach((row) => { + const val = (row as any)[col.key]; if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [data]); + }, [flatRows]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) - const columnUniqueValues = useMemo(() => { - const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { - const values = new Set(); - data.forEach((row) => { - let val = (row as any)[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "source_type") val = SOURCE_TYPE_LABEL[val] || val; - values.add(String(val)); - } + // 필터 + 정렬 적용된 플랫 데이터 + const filteredRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; + return values.has(cellVal); }); - result[col.key] = Array.from(values).sort(); } - return result; - }, [data]); + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = (a as any)[key] ?? ""; + const bv = (b as any)[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -938,7 +816,7 @@ export default function OutboundPage() { dataCount={data.length} /> - {/* 출고 목록 테이블 */} + {/* 출고 목록 테이블 (플랫 리스트) */}
{/* 패널 헤더 */}
@@ -946,7 +824,7 @@ export default function OutboundPage() { 출고 목록 - {data.length}건 + {filteredRows.length}건
@@ -971,78 +849,68 @@ export default function OutboundPage() {
-
+
- - {visibleMasterLayout.map((col) => ( - - ))} + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 출고번호 */} - -
-
handleSort("outbound_number")}> - 출고번호 - {sortState?.key === "outbound_number" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["outbound_number"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {GRID_COLUMNS.map((col) => { + const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} + + ); + })} @@ -1052,7 +920,7 @@ export default function OutboundPage() { - ) : Object.keys(filteredGroups).length === 0 ? ( + ) : filteredRows.length === 0 ? (
@@ -1062,214 +930,59 @@ export default function OutboundPage() { ) : ( - Object.entries(filteredGroups).map(([outboundNo, group]) => { - const isExpanded = expandedOrders.has(outboundNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 */} - { - if (expandedOrders.has(outboundNo)) { - setClosingOrders((prev) => new Set(prev).add(outboundNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(outboundNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(outboundNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(outboundNo)); - } - }} - onDoubleClick={() => openEditModal(group.details[0])} - > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 출고번호 */} - - {outboundNo} - ({group.details.length}) - - {/* 마스터 필드 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => { - switch (col.key) { - case "outbound_type": return ( - - - {resolveCat("outbound_type", master.outbound_type) || "-"} - - - ); - case "outbound_date": return ( - - {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} - - ); - case "reference_number": return ( - - {master.reference_number || ""} - - ); - case "customer_name": return ( - - {master.customer_name || ""} - - ); - case "warehouse_name": return ( - - {master.warehouse_name || master.warehouse_code || ""} - - ); - case "outbound_status": return ( - - - {master.outbound_status || "-"} - - - ); - case "memo": return ( - - {master.memo || ""} - - ); - default: return {(master as any)[col.key] ?? ""}; - } - })} - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - - {visibleDetailCols.map((col) => { - const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => { - let v = (d as any)[col.key]; - if (col.key === "source_type") v = SOURCE_TYPE_LABEL[v] || v; - return v; - }).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} -
+ { - const isClosing = closingOrders.has(outboundNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - onDoubleClick={() => openEditModal(row)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - - {visibleDetailCols.map((col) => { - switch (col.key) { - case "source_type": return {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"}; - case "item_code": return {row.item_code || ""}; - case "item_name": return {row.item_name || ""}; - case "specification": return {row.specification || ""}; - case "outbound_qty": return {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}; - case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; - case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; - default: return {(row as any)[col.key] ?? ""}; - } - })} - + onClick={() => { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] ); - })} - + }} + onDoubleClick={() => openEditModal(row as any)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + > + {}} /> + + {row.outbound_number || ""} + + + {row.outbound_type || "-"} + + + + {row.outbound_date ? new Date(row.outbound_date).toLocaleDateString("ko-KR") : ""} + + {row.reference_number || ""} + {row.source_type || ""} + {row.customer_name || ""} + {row.item_number || ""} + {row.item_name || ""} + {row.spec || ""} + {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {row.warehouse_name || row.warehouse_code || ""} + + + {row.outbound_status || "-"} + + + {row.remark || row.memo || ""} + ); }) )} diff --git a/frontend/app/(main)/COMPANY_8/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_8/logistics/receiving/page.tsx index 5e692c46..77d21e7b 100644 --- a/frontend/app/(main)/COMPANY_8/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_8/logistics/receiving/page.tsx @@ -43,7 +43,6 @@ import { X, Save, ChevronRight, - ChevronDown, ChevronLeft, ChevronsLeft, ChevronsRight, @@ -64,6 +63,7 @@ import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSea import { getReceivingList, createReceiving, + updateReceiving, deleteReceiving, generateReceivingNumber, getReceivingWarehouses, @@ -95,41 +95,8 @@ const GRID_COLUMNS = [ { key: "remark", label: "비고" }, ]; -// 마스터 헤더 레이아웃 (입고번호 뒤, 디테일 7컬럼 위에 colSpan으로 맵핑) -const MASTER_BODY_LAYOUT = [ - { key: "inbound_type", label: "입고유형", colSpan: 1 }, - { key: "inbound_date", label: "입고일", colSpan: 1 }, - { key: "reference_number", label: "참조번호", colSpan: 1 }, - { key: "supplier_name", label: "공급처", colSpan: 1 }, - { key: "warehouse_name", label: "창고", colSpan: 1 }, - { key: "inbound_status", label: "입고상태", colSpan: 1 }, - { key: "memo", label: "비고", colSpan: 1 }, -]; - -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "source_table", label: "출처" }, - { key: "item_number", label: "품목코드" }, - { key: "item_name", label: "품목명" }, - { key: "spec", label: "규격" }, - { key: "inbound_qty", label: "입고수량" }, - { key: "unit_price", label: "단가" }, - { key: "total_amount", label: "금액" }, -]; - -// 마스터 필드 키 목록 (필터 분류용) -const MASTER_KEYS = new Set(["inbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); - -// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) -const DETAIL_KEY_MAP: Record = { - source_type: "source_table", - item_number: "item_number", - item_name: "item_name", - spec: "spec", - inbound_qty: "inbound_qty", - unit_price: "unit_price", - total_amount: "total_amount", -}; +// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16 +const TOTAL_COLS = 16; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -287,30 +254,6 @@ interface SelectedSourceItem { export default function ReceivingPage() { const ts = useTableSettings("c16-receiving", "inbound_mng", GRID_COLUMNS); - // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 - const visibleMasterLayout = useMemo(() => { - const ordered: typeof MASTER_BODY_LAYOUT = []; - for (const vc of ts.visibleColumns) { - const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); - if (m) ordered.push(m); - } - return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; - }, [ts.visibleColumns]); - - const visibleDetailCols = useMemo(() => { - const ordered: typeof DETAIL_HEADER_COLS = []; - for (const vc of ts.visibleColumns) { - const detailKey = DETAIL_KEY_MAP[vc.key]; - if (detailKey) { - const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); - if (d) ordered.push(d); - } - } - return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; - }, [ts.visibleColumns]); - - const TOTAL_COLS = 3 + visibleMasterLayout.length; - // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -319,9 +262,7 @@ export default function ReceivingPage() { // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); - // 마스터-디테일 그룹 테이블 - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); + // 헤더 필터 & 정렬 const [headerFilters, setHeaderFilters] = useState>>({}); const [sortState, setSortState] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -338,6 +279,10 @@ export default function ReceivingPage() { const [selectedItems, setSelectedItems] = useState([]); const [saving, setSaving] = useState(false); + // 수정 모드 + const [editMode, setEditMode] = useState(false); + const [editItemIds, setEditItemIds] = useState([]); + // 소스 데이터 const [sourceKeyword, setSourceKeyword] = useState(""); const [sourceLoading, setSourceLoading] = useState(false); @@ -377,7 +322,7 @@ export default function ReceivingPage() { Promise.all( ["material", "unit"].map(async (col) => { try { - const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_8`); + const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`); const items = flatten(res.data?.data || []); map[col] = {}; for (const item of items) map[col][item.code] = item.label; @@ -434,124 +379,56 @@ export default function ReceivingPage() { })(); }, []); - // 필터 + 정렬 적용된 데이터 -> 그룹핑 - const filteredGroups = useMemo(() => { - // 1차: inbound_number 기준 그룹핑 - const allGroups: Record = {}; - for (const row of data) { - const key = row.inbound_number || "_no_inbound"; - if (!allGroups[key]) { - allGroups[key] = { master: row, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - let raw = (group.master as any)?.[colKey] ?? ""; - // 입고유형은 코드→라벨 변환된 값으로 비교 - if (colKey === "inbound_type") raw = resolveInboundType(String(raw)); - return values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([inboundNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - let cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; - if (colKey === "source_table") cellVal = SOURCE_TABLE_LABEL[cellVal] || cellVal; - return values.has(cellVal); - }) - ); - return [inboundNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - entries.sort(([, a], [, b]) => { - let av: any = (a.master as any)?.[key] ?? ""; - let bv: any = (b.master as any)?.[key] ?? ""; - if (key === "inbound_type") { av = resolveInboundType(String(av)); bv = resolveInboundType(String(bv)); } - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - const av = (a as any)[key] ?? ""; - const bv = (b as any)[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [data, headerFilters, sortState]); - - // 마스터 컬럼별 고유값 (마스터 헤더 필터용) - const masterUniqueValues = useMemo(() => { - const result: Record = {}; - const seenMasters = new Map(); - data.forEach((row) => { - if (row.inbound_number && !seenMasters.has(row.inbound_number)) { - seenMasters.set(row.inbound_number, row); - } - }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "inbound_number", label: "입고번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label }))]) { - const values = new Set(); - masters.forEach((m) => { - let val = (m as any)?.[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "inbound_type") val = resolveInboundType(String(val)); - values.add(String(val)); - } - }); - result[col.key] = Array.from(values).sort(); - } - return result; + // 플랫 행 생성 (입고유형 코드→라벨 변환, source_table→한글 등) + const flatRows = useMemo(() => { + return data.map((row) => ({ + ...row, + inbound_type: resolveInboundType(row.inbound_type), + source_type: row.source_table ? (SOURCE_TABLE_LABEL[row.source_table] || row.source_table) : (row as any).source_type || "", + })); }, [data]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) + // 컬럼별 고유값 (헤더 필터용) const columnUniqueValues = useMemo(() => { const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { + for (const col of GRID_COLUMNS) { const values = new Set(); - data.forEach((row) => { - let val = (row as any)[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "source_table") val = SOURCE_TABLE_LABEL[val] || val; - values.add(String(val)); - } + flatRows.forEach((row) => { + const val = (row as any)[col.key]; + if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [data]); + }, [flatRows]); + + // 필터 + 정렬 적용된 플랫 데이터 + const filteredRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; + return values.has(cellVal); + }); + } + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = (a as any)[key] ?? ""; + const bv = (b as any)[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -633,6 +510,8 @@ export default function ReceivingPage() { const openRegisterModal = async () => { const defaultType = "구매입고"; + setEditMode(false); + setEditItemIds([]); setModalInboundType(defaultType); setModalInboundDate(new Date().toISOString().split("T")[0]); setModalWarehouse(""); @@ -661,6 +540,51 @@ export default function ReceivingPage() { } }; + // 수정 모달 열기 + const openEditModal = (row: InboundItem) => { + const inNo = row.inbound_number; + const grouped = data.filter((d) => d.inbound_number === inNo); + const first = grouped[0] || row; + + setEditMode(true); + setEditItemIds(grouped.map((g) => g.id)); + setModalInboundNo(inNo); + setModalInboundType(first.inbound_type || "구매입고"); + setModalInboundDate(first.inbound_date ? String(first.inbound_date).slice(0, 10) : ""); + setModalWarehouse(first.warehouse_code || ""); + setModalLocation(first.location_code || ""); + setModalInspector((first as any).inspector || ""); + setModalManager((first as any).manager || ""); + setModalMemo(first.memo || ""); + setSelectedItems( + grouped.map((g) => ({ + key: g.id, + inbound_type: g.inbound_type || "", + reference_number: g.reference_number || "", + supplier_code: (g as any).supplier_code || "", + supplier_name: g.supplier_name || "", + item_number: g.item_number || "", + item_name: g.item_name || "", + spec: g.spec || "", + material: (g as any).material || "", + unit: (g as any).unit || "", + inbound_qty: Number(g.inbound_qty) || 0, + unit_price: Number(g.unit_price) || 0, + total_amount: Number(g.total_amount) || 0, + source_table: (g as any).source_table || "", + source_id: (g as any).source_id || "", + })) + ); + setSourceKeyword(""); + setPurchaseOrders([]); + setShipments([]); + setItems([]); + setSourcePage(1); + setSourceTotalCount(0); + setIsModalOpen(true); + loadSourceData(first.inbound_type || "구매입고", undefined, 1); + }; + // 검색 버튼 클릭 시 const searchSourceData = useCallback(async () => { setSourcePage(1); @@ -811,41 +735,97 @@ export default function ReceivingPage() { } setSaving(true); try { - const res = await createReceiving({ - inbound_number: modalInboundNo, - inbound_date: modalInboundDate, - warehouse_code: modalWarehouse, - location_code: modalLocation || undefined, - inspector: modalInspector || undefined, - manager: modalManager || undefined, - memo: modalMemo || undefined, - items: selectedItems.map((item) => ({ - inbound_type: item.inbound_type, - reference_number: item.reference_number, - supplier_code: item.supplier_code, - supplier_name: item.supplier_name, - item_number: item.item_number, - item_name: item.item_name, - spec: item.spec, - material: item.material, - unit: item.unit, - inbound_qty: item.inbound_qty, - unit_price: item.unit_price, - total_amount: item.total_amount, - source_table: item.source_table, - source_id: item.source_id, - inbound_status: "입고완료", - inspection_status: "대기", - })), - }); + if (editMode) { + // 기존 item 수정 + 삭제된 item 삭제 + 새 item 추가 + const currentKeys = new Set(selectedItems.map((i) => i.key)); + const toDelete = editItemIds.filter((id) => !currentKeys.has(id)); + const toUpdate = selectedItems.filter((i) => editItemIds.includes(i.key)); + const toCreate = selectedItems.filter((i) => !editItemIds.includes(i.key)); - if (res.success) { - alert(res.message || "입고 등록 완료"); + await Promise.all([ + ...toDelete.map((id) => deleteReceiving(id)), + ...toUpdate.map((item) => + updateReceiving(item.key, { + inbound_date: modalInboundDate, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + warehouse_code: modalWarehouse || undefined, + location_code: modalLocation || undefined, + memo: modalMemo || undefined, + } as any) + ), + ...(toCreate.length > 0 + ? [createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: toCreate.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + })] + : []), + ]); + toast.success("입고 정보를 수정했어요"); setIsModalOpen(false); fetchList(); + } else { + const res = await createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: selectedItems.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + }); + + if (res.success) { + toast.success(res.message || "입고 등록 완료"); + setIsModalOpen(false); + fetchList(); + } } - } catch { - alert("입고 등록 중 오류가 발생했습니다."); + } catch (err: any) { + const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "입고 등록 중 오류가 발생했습니다."); + toast.error(msg); } finally { setSaving(false); } @@ -897,13 +877,13 @@ export default function ReceivingPage() { } /> - {/* 입고 목록 테이블 (마스터-디테일 그룹) */} + {/* 입고 목록 테이블 (플랫 리스트) */}

입고 목록

- {Object.keys(filteredGroups).length}건 + {filteredRows.length}건
-
+
- - {visibleMasterLayout.map((col) => ( - - ))} + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 입고번호 (별도 컬럼) */} - -
-
handleSort("inbound_number")}> - 입고번호 - {sortState?.key === "inbound_number" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["inbound_number"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {GRID_COLUMNS.map((col) => { + const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} + + ); + })} @@ -993,7 +963,7 @@ export default function ReceivingPage() { - ) : Object.keys(filteredGroups).length === 0 ? ( + ) : filteredRows.length === 0 ? (
@@ -1003,212 +973,59 @@ export default function ReceivingPage() { ) : ( - Object.entries(filteredGroups).map(([inboundNo, group]) => { - const isExpanded = expandedOrders.has(inboundNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 */} - { - if (expandedOrders.has(inboundNo)) { - setClosingOrders((prev) => new Set(prev).add(inboundNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(inboundNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(inboundNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(inboundNo)); - } + { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + onDoubleClick={() => openEditModal(row as any)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); }} > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 입고번호 */} - - {inboundNo} - ({group.details.length}) - - {/* 마스터 필드 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => { - switch (col.key) { - case "inbound_type": return ( - - - {resolveInboundType(master.inbound_type)} - - - ); - case "inbound_date": return ( - - {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} - - ); - case "reference_number": return ( - - {master.reference_number || ""} - - ); - case "supplier_name": return ( - - {master.supplier_name || ""} - - ); - case "warehouse_name": return ( - - {master.warehouse_name || master.warehouse_code || ""} - - ); - case "inbound_status": return ( - - - {master.inbound_status || "-"} - - - ); - case "memo": return ( - - {master.memo || ""} - - ); - default: return {(master as any)[col.key] ?? ""}; - } - })} - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - - {visibleDetailCols.map((col) => { - const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => { - let v = (d as any)[col.key]; - if (col.key === "source_table") v = SOURCE_TABLE_LABEL[v] || v; - return v; - }).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} -
- )} - - {/* 디테일 행 (펼쳤을 때만) */} - {isExpanded && group.details.map((row, detailIdx) => { - const isClosing = closingOrders.has(inboundNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - - {visibleDetailCols.map((col) => { - switch (col.key) { - case "source_table": return {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"}; - case "item_number": return {row.item_number || ""}; - case "item_name": return {row.item_name || ""}; - case "spec": return {row.spec || ""}; - case "inbound_qty": return {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}; - case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; - case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; - default: return {(row as any)[col.key] ?? ""}; - } - })} - - ); - })} - + {}} /> + + {row.inbound_number || ""} + + + {row.inbound_type || "-"} + + + + {row.inbound_date ? new Date(row.inbound_date).toLocaleDateString("ko-KR") : ""} + + {row.reference_number || ""} + {row.source_type || ""} + {row.supplier_name || ""} + {row.item_number || ""} + {row.item_name || ""} + {row.spec || ""} + {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {row.warehouse_name || (row as any).warehouse_code || ""} + + + {row.inbound_status || "-"} + + + {row.remark || row.memo || ""} + ); }) )} @@ -1221,9 +1038,9 @@ export default function ReceivingPage() { - 입고 등록 + {editMode ? "입고 수정" : "입고 등록"} - 입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해 주세요. + {editMode ? "입고 정보를 수정해 주세요." : "입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해 주세요."}
diff --git a/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx index 38b01658..534582f9 100644 --- a/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx @@ -1053,14 +1053,14 @@ export default function ProductionPlanManagementPage() { return (
- + - + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - - 품목코드 - 품목명 + + 품목코드 + 품목명 {ts.visibleColumns.map((col) => ( {col.label} @@ -1073,9 +1073,9 @@ export default function ProductionPlanManagementPage() { if (item._isGroupSummary) { return ( - - - + + + {ts.visibleColumns.map((col) => { const v = (item as any)[col.key]; return ( @@ -1090,14 +1090,14 @@ export default function ProductionPlanManagementPage() { return ( - e.stopPropagation()}> + e.stopPropagation()}> toggleItemGroupSelect(item.item_code)} className="h-4 w-4" /> - toggleItemExpand(item.item_code)}> + toggleItemExpand(item.item_code)}> - toggleItemExpand(item.item_code)}>{item.item_code} - toggleItemExpand(item.item_code)}>{item.item_name} + toggleItemExpand(item.item_code)}>{item.item_code} + toggleItemExpand(item.item_code)}>{item.item_name} {ts.visibleColumns.map((col) => renderGroupCell(col, item))} {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => { @@ -1107,9 +1107,9 @@ export default function ProductionPlanManagementPage() { } return ( - - - + + +
수주번호: {detail.order_no} diff --git a/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx index 143a84a9..f712d3ed 100644 --- a/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx @@ -13,7 +13,7 @@ import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, - ClipboardList, Pencil, Search, X, Package, ChevronDown, + ClipboardList, Pencil, Search, X, Package, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, GripVertical, } from "lucide-react"; @@ -88,18 +88,18 @@ const GRID_COLUMNS_CONFIG = [ ]; const MODAL_DETAIL_COLUMNS = [ - { key: "item_code", label: "품번", width: "w-[120px]" }, - { key: "item_name", label: "품명", width: "w-[120px]" }, - { key: "supplier", label: "공급업체", width: "w-[150px]" }, - { key: "spec", label: "규격", width: "w-[80px]" }, - { key: "unit", label: "단위", width: "w-[60px]" }, - { key: "order_qty", label: "발주수량", width: "w-[90px]" }, - { key: "received_qty", label: "입고수량", width: "w-[90px]" }, - { key: "remain_qty", label: "잔량", width: "w-[80px]" }, - { key: "unit_price", label: "단가", width: "w-[100px]" }, - { key: "amount", label: "금액", width: "w-[100px]" }, - { key: "due_date", label: "납기일", width: "w-[160px]" }, - { key: "memo", label: "메모", width: "w-[120px]" }, + { key: "item_code", label: "품번", width: "min-w-[120px]" }, + { key: "item_name", label: "품명", width: "min-w-[150px]" }, + { key: "supplier", label: "공급업체", width: "min-w-[150px]" }, + { key: "spec", label: "규격", width: "min-w-[80px]" }, + { key: "unit", label: "단위", width: "min-w-[90px]" }, + { key: "order_qty", label: "발주수량", width: "min-w-[90px]" }, + { key: "received_qty", label: "입고수량", width: "min-w-[90px]" }, + { key: "remain_qty", label: "잔량", width: "min-w-[80px]" }, + { key: "unit_price", label: "단가", width: "min-w-[100px]" }, + { key: "amount", label: "금액", width: "min-w-[100px]" }, + { key: "due_date", label: "납기일", width: "min-w-[160px]" }, + { key: "memo", label: "메모", width: "min-w-[120px]" }, ]; const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16"; @@ -162,7 +162,6 @@ export default function PurchaseOrderPage() { const [excelUploadOpen, setExcelUploadOpen] = useState(false); const [categoryOptions, setCategoryOptions] = useState>({}); const [checkedIds, setCheckedIds] = useState([]); - const [expandedOrders, setExpandedOrders] = useState>(new Set()); // 테이블 설정 const ts = useTableSettings("c16-purchase-order", DETAIL_TABLE, GRID_COLUMNS_CONFIG); @@ -238,7 +237,7 @@ export default function PurchaseOrderPage() { ); try { const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, size: 5000, autoFilter: true, }); const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || []; optMap["supplier_code"] = supps.map((s: any) => ({ @@ -248,7 +247,7 @@ export default function PurchaseOrderPage() { } catch { /* skip */ } try { const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, size: 5000, autoFilter: true, }); const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; optMap["manager"] = users.map((u: any) => ({ @@ -294,7 +293,7 @@ export default function PurchaseOrderPage() { } const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined, autoFilter: true, sort: { columnName: "purchase_no", order: "desc" }, @@ -372,20 +371,6 @@ export default function PurchaseOrderPage() { useEffect(() => { fetchOrders(); }, [fetchOrders]); // purchase_no 기준 그룹핑 - const orderGroups = useMemo(() => { - const map: Record = {}; - for (const row of orders) { - const key = row.purchase_no || row.id; - if (!map[key]) { - map[key] = { - master: { purchase_no: row.purchase_no, order_date: row.order_date, supplier_name: row.supplier_name, status: row.status, memo: row.memo }, - details: [], - }; - } - map[key].details.push(row); - } - return map; - }, [orders]); const getCategoryLabel = (col: string, code: string) => { if (!code) return ""; @@ -553,7 +538,7 @@ export default function PurchaseOrderPage() { } }; - // 품목 검색 + // 품목 검색 (수주관리와 동일한 서버 페이징 방식) const searchItems = async (page?: number, size?: number) => { const p = page ?? itemPage; const s = size ?? itemPageSize; @@ -563,25 +548,24 @@ export default function PurchaseOrderPage() { if (itemSearchKeyword) { filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); } + // 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응) + if (itemSearchDivision !== "all") { + const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; + const divValues = [itemSearchDivision]; + if (divLabel) divValues.push(divLabel); + filters.push({ columnName: "division", operator: "in", value: divValues }); + } const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: p, size: s, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const resData = res.data?.data; - let allRows = resData?.data || resData?.rows || []; - if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - allRows = allRows.filter((item: any) => { - const div = item.division || ""; - return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel)); - }); - } - const total = allRows.length; - const start = (p - 1) * s; - setItemSearchResults(allRows.slice(start, start + s)); - setItemTotal(total); - setItemTotalPages(Math.max(1, Math.ceil(total / s))); + const rows = resData?.data || resData?.rows || []; + const serverTotal = resData?.total || resData?.totalCount || rows.length; + setItemSearchResults(rows); + setItemTotal(serverTotal); + setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); } catch { /* skip */ } finally { setItemSearchLoading(false); } @@ -623,7 +607,7 @@ export default function PurchaseOrderPage() { try { const itemIds = selected.map((item) => item.item_number || item.id); const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [ @@ -686,7 +670,7 @@ export default function PurchaseOrderPage() { if (itemCodes.length === 0) return; try { const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, autoFilter: true, }); @@ -708,7 +692,7 @@ export default function PurchaseOrderPage() { if (itemCodes.length === 0) return; try { const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [ { columnName: "supplier_id", operator: "equals", value: supplierCode }, { columnName: "item_id", operator: "in", value: itemCodes }, @@ -811,125 +795,83 @@ export default function PurchaseOrderPage() {
- {/* 데이터 테이블 — 아코디언 그룹핑 */} + {/* 데이터 테이블 — 플랫 리스트 */}
{loading ? (
- ) : Object.keys(orderGroups).length === 0 ? ( + ) : orders.length === 0 ? (

등록된 발주가 없어요

) : ( (() => { - const MASTER_KEYS = new Set(["purchase_no", "order_date", "supplier_name", "status", "memo"]); const numCols = new Set(["order_qty", "received_qty", "remain_qty", "unit_price", "amount"]); - - // ts.visibleColumns 순서를 따르되, 마스터/디테일 컬럼을 분리 - // 고정 컬럼(품목수)은 마스터 선행 컬럼 뒤에 배치 - const leadingMaster: typeof ts.visibleColumns = []; - const detailCols: typeof ts.visibleColumns = []; - const trailingMaster: typeof ts.visibleColumns = []; - let passedFirstDetail = false; - for (const col of ts.visibleColumns) { - if (MASTER_KEYS.has(col.key)) { - if (passedFirstDetail) trailingMaster.push(col); - else leadingMaster.push(col); - } else { - passedFirstDetail = true; - detailCols.push(col); - } - } - - const renderDetailCell = (row: any, key: string) => { + const renderCell = (row: any, key: string) => { const val = row[key]; if (key === "status") return val ? {val} : "-"; - if (numCols.has(key)) return {val ? Number(val).toLocaleString() : "0"}; - return val || "-"; + if (key === "order_date" || key === "due_date") return val ? new Date(val).toLocaleDateString("ko-KR") : "-"; + if (numCols.has(key)) return {val ? Number(val).toLocaleString() : ""}; + return val || ""; }; - - const renderMasterHead = (col: { key: string; label: string }) => ( - - {col.label} - - ); - - const renderMasterCell = (col: { key: string }, m: any, purchaseNo: string) => { - if (col.key === "purchase_no") return {purchaseNo}; - if (col.key === "order_date") return {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}; - if (col.key === "supplier_name") return {m.supplier_name || "-"}; - if (col.key === "status") return {m.status && {m.status}}; - if (col.key === "memo") return {m.memo || ""}; - return ; - }; - return (
- - - {leadingMaster.map(renderMasterHead)} - 품목수 - {detailCols.map(col => ( - - {col.label}{col.key === "order_qty" || col.key === "amount" ? " 합계" : ""} + { + const allIds = orders.map((r) => r.id); + const allChecked = allIds.length > 0 && allIds.every((id) => checkedIds.includes(id)); + setCheckedIds(allChecked ? [] : allIds); + }} + > + 0 && orders.every((r) => checkedIds.includes(r.id))} + onCheckedChange={() => {}} + /> + + {ts.visibleColumns.map((col) => ( + + {col.label} ))} - {trailingMaster.map(renderMasterHead)} - {Object.entries(orderGroups).map(([purchaseNo, group]) => { - const isExpanded = expandedOrders.has(purchaseNo); - const detailIds = group.details.map(d => d.id); - const allChecked = detailIds.length > 0 && detailIds.every(id => checkedIds.includes(id)); - const someChecked = detailIds.some(id => checkedIds.includes(id)); - const m = group.master; - const totalQty = group.details.reduce((s, d) => s + (Number(d.order_qty) || 0), 0); - const totalAmt = group.details.reduce((s, d) => s + (Number(d.amount) || 0), 0); + {orders.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - setExpandedOrders(prev => { const next = new Set(prev); if (next.has(purchaseNo)) next.delete(purchaseNo); else next.add(purchaseNo); return next; })} - onDoubleClick={() => openEditModal(purchaseNo)} + { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + onDoubleClick={() => openEditModal(row.purchase_no)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} > - - + {}} /> + + {ts.visibleColumns.map((col) => ( + + {renderCell(row, col.key)} - { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !detailIds.includes(id)) : [...new Set([...prev, ...detailIds])]); }}> - {}} /> - - {leadingMaster.map(col => renderMasterCell(col, m, purchaseNo))} - {group.details.length}건 - {detailCols.map(col => ( - - {col.key === "order_qty" ? totalQty.toLocaleString() - : col.key === "amount" ? totalAmt.toLocaleString() - : ""} - - ))} - {trailingMaster.map(col => renderMasterCell(col, m, purchaseNo))} - - {isExpanded && group.details.map((row) => ( - - - { e.stopPropagation(); setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id]); }}> - {}} /> - - {leadingMaster.map(col => )} - - {detailCols.map(col => ( - - {renderDetailCell(row, col.key)} - - ))} - {trailingMaster.map(col => )} - ))} - + ); })} @@ -938,7 +880,7 @@ export default function PurchaseOrderPage() { })() )}
- 전체 {Object.keys(orderGroups).length}건 (발주 기준) / {orders.length}건 (품목 기준) + 전체 {orders.length}건
@@ -1094,7 +1036,7 @@ export default function PurchaseOrderPage() { ) : (
-
+
c.key)} strategy={horizontalListSortingStrategy}> @@ -1118,12 +1060,12 @@ export default function PurchaseOrderPage() { {visibleModalColumns.map((col) => { switch (col.key) { case "item_code": - return {row.item_code}; + return {row.item_code}; case "item_name": - return {row.item_name}; + return {row.item_name}; case "supplier": return ( - + {isReadOnly ? ( {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} ) : ( @@ -1133,7 +1075,7 @@ export default function PurchaseOrderPage() { updateDetailRow(idx, "supplier_code", v); updateDetailRow(idx, "supplier_name", name); }}> - + {(categoryOptions["supplier_code"] || []).map(o => ( {o.label} @@ -1149,11 +1091,11 @@ export default function PurchaseOrderPage() { return {row.unit}; case "order_qty": return ( - + {isReadOnly ? ( {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} ) : ( - updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" /> )} ); @@ -1163,11 +1105,11 @@ export default function PurchaseOrderPage() { return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; case "unit_price": return ( - + {isReadOnly ? ( {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} ) : ( - updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" /> )} ); @@ -1175,21 +1117,21 @@ export default function PurchaseOrderPage() { return {row.amount ? Number(row.amount).toLocaleString() : ""}; case "due_date": return ( - + {isReadOnly ? ( {row.due_date} ) : ( - updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> + updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs w-full" /> )} ); case "memo": return ( - + {isReadOnly ? ( {row.memo} ) : ( - updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> + updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs w-full" /> )} ); diff --git a/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx index 05facbbc..863c0aef 100644 --- a/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx @@ -424,9 +424,10 @@ export default function ItemInspectionInfoPage() { case "inspection_type": return (
- {group.types.map((t: string) => ( - {t} - ))} + {group.types.map((t: string) => { + const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t; + return {label}; + })}
); @@ -494,7 +495,7 @@ export default function ItemInspectionInfoPage() { : "bg-muted/50 text-muted-foreground border-border hover:bg-muted" )} > - {type} + {inspTypeCatOptions.find((o) => o.code === type)?.label || type} o.code === itemSearchDivision)?.label || ""; + // 코드 또는 라벨이 저장된 경우 모두 조회하기 위해 in 연산자 사용 + const divValues = [itemSearchDivision]; + if (divLabel) divValues.push(divLabel); + filters.push({ columnName: "division", operator: "in", value: divValues }); + } + // 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링 const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI"; const partnerId = masterForm.partner_id; @@ -632,7 +641,7 @@ export default function SalesOrderPage() { if (isCustomerPrice && partnerId) { try { const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] }, autoFilter: true, }); @@ -642,31 +651,22 @@ export default function SalesOrderPage() { } const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: p, size: s, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const resData = res.data?.data; - let allRows = resData?.data || resData?.rows || []; + let rows = resData?.data || resData?.rows || []; + const serverTotal = resData?.total || resData?.totalCount || rows.length; - // 거래처우선일 때 연결된 품목만 표시 + // 거래처우선일 때 연결된 품목만 표시 (클라이언트 필터) if (customerItemIds) { - allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); + rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); } - // 관리품목 필터 (코드/라벨 혼재 대응) - if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - allRows = allRows.filter((item: any) => { - const div = item.division || ""; - return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel)); - }); - } - const total = allRows.length; - const start = (p - 1) * s; - setItemSearchResults(allRows.slice(start, start + s)); - setItemTotal(total); - setItemTotalPages(Math.max(1, Math.ceil(total / s))); + setItemSearchResults(rows); + setItemTotal(serverTotal); + setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); } catch { /* skip */ } finally { setItemSearchLoading(false); } @@ -1320,44 +1320,44 @@ export default function SalesOrderPage() {
- No - 품번 - 품명 - 규격 - 재질 - 포장재 - 단위 - 수량 - 포장수량 - 단가 - 금액 - 납기일 - 분할/삭제 + No + 품번 + 품명 + 규격 + 재질 + 포장재 + 단위 + 수량 + 포장수량 + 단가 + 금액 + 납기일 + 분할/삭제 {detailRows.map((row, idx) => ( {idx + 1} - - {row.part_code} + + {row.part_code} - - {row.part_name} + + {row.part_name} - {row.spec} - {row.material} + {row.spec} + {row.material} updateDetailRow(idx, "packing_material", e.target.value)} placeholder="포장재" - className="h-8 text-xs" + className="h-8 text-xs w-full" /> - - - 전체 - {(categoryOptions["item_division"] || []).map((o) => ( - {o.label} - ))} - - +
영업관리
diff --git a/frontend/app/(main)/COMPANY_9/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_9/logistics/outbound/page.tsx index 824e2e86..457e2723 100644 --- a/frontend/app/(main)/COMPANY_9/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_9/logistics/outbound/page.tsx @@ -38,7 +38,6 @@ import { Save, ChevronRight, ChevronLeft, - ChevronDown, ChevronsLeft, ChevronsRight, Inbox, @@ -116,41 +115,8 @@ const GRID_COLUMNS = [ { key: "remark", label: "비고" }, ]; -// 마스터 헤더 레이아웃 (출고번호 뒤) -const MASTER_BODY_LAYOUT = [ - { key: "outbound_type", label: "출고유형", colSpan: 1 }, - { key: "outbound_date", label: "출고일", colSpan: 1 }, - { key: "reference_number", label: "참조번호", colSpan: 1 }, - { key: "customer_name", label: "거래처", colSpan: 1 }, - { key: "warehouse_name", label: "창고", colSpan: 1 }, - { key: "outbound_status", label: "출고상태", colSpan: 1 }, - { key: "memo", label: "비고", colSpan: 1 }, -]; - -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "source_type", label: "출처" }, - { key: "item_code", label: "품목코드" }, - { key: "item_name", label: "품목명" }, - { key: "specification", label: "규격" }, - { key: "outbound_qty", label: "출고수량" }, - { key: "unit_price", label: "단가" }, - { key: "total_amount", label: "금액" }, -]; - -// 마스터 필드 키 목록 (필터 분류용) -const MASTER_KEYS = new Set(["outbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); - -// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) -const DETAIL_KEY_MAP: Record = { - source_type: "source_type", - item_number: "item_code", - item_name: "item_name", - spec: "specification", - outbound_qty: "outbound_qty", - unit_price: "unit_price", - total_amount: "total_amount", -}; +// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16 +const TOTAL_COLS = 16; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -258,30 +224,6 @@ interface SelectedSourceItem { export default function OutboundPage() { const ts = useTableSettings("c16-outbound", "outbound_mng", GRID_COLUMNS); - // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 - const visibleMasterLayout = useMemo(() => { - const ordered: typeof MASTER_BODY_LAYOUT = []; - for (const vc of ts.visibleColumns) { - const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); - if (m) ordered.push(m); - } - return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; - }, [ts.visibleColumns]); - - const visibleDetailCols = useMemo(() => { - const ordered: typeof DETAIL_HEADER_COLS = []; - for (const vc of ts.visibleColumns) { - const detailKey = DETAIL_KEY_MAP[vc.key]; - if (detailKey) { - const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); - if (d) ordered.push(d); - } - } - return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; - }, [ts.visibleColumns]); - - const TOTAL_COLS = 3 + visibleMasterLayout.length; - // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -290,9 +232,7 @@ export default function OutboundPage() { // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); - // 마스터-디테일 그룹 테이블 state - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); + // 헤더 필터 & 정렬 const [headerFilters, setHeaderFilters] = useState>>({}); const [sortState, setSortState] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -403,121 +343,59 @@ export default function OutboundPage() { })(); }, []); - // --- 마스터-디테일 그룹핑, 필터, 정렬 --- + // 플랫 행 생성 (출고유형 코드→라벨 변환, source_type→한글 등) + const flatRows = useMemo(() => { + return data.map((row) => ({ + ...row, + _raw_outbound_type: row.outbound_type, + outbound_type: resolveCat("outbound_type", row.outbound_type) || row.outbound_type || "", + source_type: row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "", + item_number: row.item_code || (row as any).item_number || "", + spec: row.specification || (row as any).spec || "", + })); + }, [data, resolveCat]); - // outbound_number 기준 그룹핑 + 필터 + 정렬 - const filteredGroups = useMemo(() => { - // 1차: outbound_number 기준 그룹핑 - const allGroups: Record = {}; - for (const row of data) { - const key = row.outbound_number || "_no_number"; - if (!allGroups[key]) { - allGroups[key] = { master: row, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - const raw = (group.master as any)?.[colKey] ?? ""; - return values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([outNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - let cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; - if (colKey === "source_type") cellVal = SOURCE_TYPE_LABEL[cellVal] || cellVal; - return values.has(cellVal); - }) - ); - return [outNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - entries.sort(([, a], [, b]) => { - const av = (a.master as any)?.[key] ?? ""; - const bv = (b.master as any)?.[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - let av: any = (a as any)[key] ?? ""; - let bv: any = (b as any)[key] ?? ""; - if (key === "source_type") { av = SOURCE_TYPE_LABEL[av] || av; bv = SOURCE_TYPE_LABEL[bv] || bv; } - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [data, headerFilters, sortState]); - - // 마스터 컬럼별 고유값 (헤더 필터용) - const masterUniqueValues = useMemo(() => { + // 컬럼별 고유값 (헤더 필터용) + const columnUniqueValues = useMemo(() => { const result: Record = {}; - const seenMasters = new Map(); - data.forEach((row) => { - if (row.outbound_number && !seenMasters.has(row.outbound_number)) { - seenMasters.set(row.outbound_number, row); - } - }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "outbound_number", label: "출고번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label }))]) { + for (const col of GRID_COLUMNS) { const values = new Set(); - masters.forEach((m) => { - const val = (m as any)?.[col.key]; + flatRows.forEach((row) => { + const val = (row as any)[col.key]; if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [data]); + }, [flatRows]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) - const columnUniqueValues = useMemo(() => { - const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { - const values = new Set(); - data.forEach((row) => { - let val = (row as any)[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "source_type") val = SOURCE_TYPE_LABEL[val] || val; - values.add(String(val)); - } + // 필터 + 정렬 적용된 플랫 데이터 + const filteredRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; + return values.has(cellVal); }); - result[col.key] = Array.from(values).sort(); } - return result; - }, [data]); + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = (a as any)[key] ?? ""; + const bv = (b as any)[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -938,7 +816,7 @@ export default function OutboundPage() { dataCount={data.length} /> - {/* 출고 목록 테이블 */} + {/* 출고 목록 테이블 (플랫 리스트) */}
{/* 패널 헤더 */}
@@ -946,7 +824,7 @@ export default function OutboundPage() { 출고 목록 - {data.length}건 + {filteredRows.length}건
@@ -971,78 +849,68 @@ export default function OutboundPage() {
-
+
- - {visibleMasterLayout.map((col) => ( - - ))} + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 출고번호 */} - -
-
handleSort("outbound_number")}> - 출고번호 - {sortState?.key === "outbound_number" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["outbound_number"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {GRID_COLUMNS.map((col) => { + const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} + + ); + })} @@ -1052,7 +920,7 @@ export default function OutboundPage() { - ) : Object.keys(filteredGroups).length === 0 ? ( + ) : filteredRows.length === 0 ? (
@@ -1062,214 +930,59 @@ export default function OutboundPage() { ) : ( - Object.entries(filteredGroups).map(([outboundNo, group]) => { - const isExpanded = expandedOrders.has(outboundNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 */} - { - if (expandedOrders.has(outboundNo)) { - setClosingOrders((prev) => new Set(prev).add(outboundNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(outboundNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(outboundNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(outboundNo)); - } - }} - onDoubleClick={() => openEditModal(group.details[0])} - > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 출고번호 */} - - {outboundNo} - ({group.details.length}) - - {/* 마스터 필드 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => { - switch (col.key) { - case "outbound_type": return ( - - - {resolveCat("outbound_type", master.outbound_type) || "-"} - - - ); - case "outbound_date": return ( - - {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} - - ); - case "reference_number": return ( - - {master.reference_number || ""} - - ); - case "customer_name": return ( - - {master.customer_name || ""} - - ); - case "warehouse_name": return ( - - {master.warehouse_name || master.warehouse_code || ""} - - ); - case "outbound_status": return ( - - - {master.outbound_status || "-"} - - - ); - case "memo": return ( - - {master.memo || ""} - - ); - default: return {(master as any)[col.key] ?? ""}; - } - })} - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - - {visibleDetailCols.map((col) => { - const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => { - let v = (d as any)[col.key]; - if (col.key === "source_type") v = SOURCE_TYPE_LABEL[v] || v; - return v; - }).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} -
+ { - const isClosing = closingOrders.has(outboundNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - onDoubleClick={() => openEditModal(row)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - - {visibleDetailCols.map((col) => { - switch (col.key) { - case "source_type": return {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"}; - case "item_code": return {row.item_code || ""}; - case "item_name": return {row.item_name || ""}; - case "specification": return {row.specification || ""}; - case "outbound_qty": return {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}; - case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; - case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; - default: return {(row as any)[col.key] ?? ""}; - } - })} - + onClick={() => { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] ); - })} - + }} + onDoubleClick={() => openEditModal(row as any)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + > + {}} /> + + {row.outbound_number || ""} + + + {row.outbound_type || "-"} + + + + {row.outbound_date ? new Date(row.outbound_date).toLocaleDateString("ko-KR") : ""} + + {row.reference_number || ""} + {row.source_type || ""} + {row.customer_name || ""} + {row.item_number || ""} + {row.item_name || ""} + {row.spec || ""} + {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {row.warehouse_name || row.warehouse_code || ""} + + + {row.outbound_status || "-"} + + + {row.remark || row.memo || ""} + ); }) )} diff --git a/frontend/app/(main)/COMPANY_9/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_9/logistics/receiving/page.tsx index 4c357120..77d21e7b 100644 --- a/frontend/app/(main)/COMPANY_9/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_9/logistics/receiving/page.tsx @@ -43,7 +43,6 @@ import { X, Save, ChevronRight, - ChevronDown, ChevronLeft, ChevronsLeft, ChevronsRight, @@ -64,6 +63,7 @@ import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSea import { getReceivingList, createReceiving, + updateReceiving, deleteReceiving, generateReceivingNumber, getReceivingWarehouses, @@ -95,41 +95,8 @@ const GRID_COLUMNS = [ { key: "remark", label: "비고" }, ]; -// 마스터 헤더 레이아웃 (입고번호 뒤, 디테일 7컬럼 위에 colSpan으로 맵핑) -const MASTER_BODY_LAYOUT = [ - { key: "inbound_type", label: "입고유형", colSpan: 1 }, - { key: "inbound_date", label: "입고일", colSpan: 1 }, - { key: "reference_number", label: "참조번호", colSpan: 1 }, - { key: "supplier_name", label: "공급처", colSpan: 1 }, - { key: "warehouse_name", label: "창고", colSpan: 1 }, - { key: "inbound_status", label: "입고상태", colSpan: 1 }, - { key: "memo", label: "비고", colSpan: 1 }, -]; - -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "source_table", label: "출처" }, - { key: "item_number", label: "품목코드" }, - { key: "item_name", label: "품목명" }, - { key: "spec", label: "규격" }, - { key: "inbound_qty", label: "입고수량" }, - { key: "unit_price", label: "단가" }, - { key: "total_amount", label: "금액" }, -]; - -// 마스터 필드 키 목록 (필터 분류용) -const MASTER_KEYS = new Set(["inbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); - -// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) -const DETAIL_KEY_MAP: Record = { - source_type: "source_table", - item_number: "item_number", - item_name: "item_name", - spec: "spec", - inbound_qty: "inbound_qty", - unit_price: "unit_price", - total_amount: "total_amount", -}; +// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16 +const TOTAL_COLS = 16; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -287,30 +254,6 @@ interface SelectedSourceItem { export default function ReceivingPage() { const ts = useTableSettings("c16-receiving", "inbound_mng", GRID_COLUMNS); - // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 - const visibleMasterLayout = useMemo(() => { - const ordered: typeof MASTER_BODY_LAYOUT = []; - for (const vc of ts.visibleColumns) { - const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); - if (m) ordered.push(m); - } - return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; - }, [ts.visibleColumns]); - - const visibleDetailCols = useMemo(() => { - const ordered: typeof DETAIL_HEADER_COLS = []; - for (const vc of ts.visibleColumns) { - const detailKey = DETAIL_KEY_MAP[vc.key]; - if (detailKey) { - const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); - if (d) ordered.push(d); - } - } - return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; - }, [ts.visibleColumns]); - - const TOTAL_COLS = 3 + visibleMasterLayout.length; - // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -319,9 +262,7 @@ export default function ReceivingPage() { // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); - // 마스터-디테일 그룹 테이블 - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); + // 헤더 필터 & 정렬 const [headerFilters, setHeaderFilters] = useState>>({}); const [sortState, setSortState] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -338,6 +279,10 @@ export default function ReceivingPage() { const [selectedItems, setSelectedItems] = useState([]); const [saving, setSaving] = useState(false); + // 수정 모드 + const [editMode, setEditMode] = useState(false); + const [editItemIds, setEditItemIds] = useState([]); + // 소스 데이터 const [sourceKeyword, setSourceKeyword] = useState(""); const [sourceLoading, setSourceLoading] = useState(false); @@ -377,7 +322,7 @@ export default function ReceivingPage() { Promise.all( ["material", "unit"].map(async (col) => { try { - const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_9`); + const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`); const items = flatten(res.data?.data || []); map[col] = {}; for (const item of items) map[col][item.code] = item.label; @@ -434,124 +379,56 @@ export default function ReceivingPage() { })(); }, []); - // 필터 + 정렬 적용된 데이터 -> 그룹핑 - const filteredGroups = useMemo(() => { - // 1차: inbound_number 기준 그룹핑 - const allGroups: Record = {}; - for (const row of data) { - const key = row.inbound_number || "_no_inbound"; - if (!allGroups[key]) { - allGroups[key] = { master: row, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - let raw = (group.master as any)?.[colKey] ?? ""; - // 입고유형은 코드→라벨 변환된 값으로 비교 - if (colKey === "inbound_type") raw = resolveInboundType(String(raw)); - return values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([inboundNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - let cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; - if (colKey === "source_table") cellVal = SOURCE_TABLE_LABEL[cellVal] || cellVal; - return values.has(cellVal); - }) - ); - return [inboundNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - entries.sort(([, a], [, b]) => { - let av: any = (a.master as any)?.[key] ?? ""; - let bv: any = (b.master as any)?.[key] ?? ""; - if (key === "inbound_type") { av = resolveInboundType(String(av)); bv = resolveInboundType(String(bv)); } - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - const av = (a as any)[key] ?? ""; - const bv = (b as any)[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [data, headerFilters, sortState]); - - // 마스터 컬럼별 고유값 (마스터 헤더 필터용) - const masterUniqueValues = useMemo(() => { - const result: Record = {}; - const seenMasters = new Map(); - data.forEach((row) => { - if (row.inbound_number && !seenMasters.has(row.inbound_number)) { - seenMasters.set(row.inbound_number, row); - } - }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "inbound_number", label: "입고번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label }))]) { - const values = new Set(); - masters.forEach((m) => { - let val = (m as any)?.[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "inbound_type") val = resolveInboundType(String(val)); - values.add(String(val)); - } - }); - result[col.key] = Array.from(values).sort(); - } - return result; + // 플랫 행 생성 (입고유형 코드→라벨 변환, source_table→한글 등) + const flatRows = useMemo(() => { + return data.map((row) => ({ + ...row, + inbound_type: resolveInboundType(row.inbound_type), + source_type: row.source_table ? (SOURCE_TABLE_LABEL[row.source_table] || row.source_table) : (row as any).source_type || "", + })); }, [data]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) + // 컬럼별 고유값 (헤더 필터용) const columnUniqueValues = useMemo(() => { const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { + for (const col of GRID_COLUMNS) { const values = new Set(); - data.forEach((row) => { - let val = (row as any)[col.key]; - if (val !== null && val !== undefined && val !== "") { - if (col.key === "source_table") val = SOURCE_TABLE_LABEL[val] || val; - values.add(String(val)); - } + flatRows.forEach((row) => { + const val = (row as any)[col.key]; + if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [data]); + }, [flatRows]); + + // 필터 + 정렬 적용된 플랫 데이터 + const filteredRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = (row as any)[colKey] != null ? String((row as any)[colKey]) : ""; + return values.has(cellVal); + }); + } + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = (a as any)[key] ?? ""; + const bv = (b as any)[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -633,6 +510,8 @@ export default function ReceivingPage() { const openRegisterModal = async () => { const defaultType = "구매입고"; + setEditMode(false); + setEditItemIds([]); setModalInboundType(defaultType); setModalInboundDate(new Date().toISOString().split("T")[0]); setModalWarehouse(""); @@ -661,6 +540,51 @@ export default function ReceivingPage() { } }; + // 수정 모달 열기 + const openEditModal = (row: InboundItem) => { + const inNo = row.inbound_number; + const grouped = data.filter((d) => d.inbound_number === inNo); + const first = grouped[0] || row; + + setEditMode(true); + setEditItemIds(grouped.map((g) => g.id)); + setModalInboundNo(inNo); + setModalInboundType(first.inbound_type || "구매입고"); + setModalInboundDate(first.inbound_date ? String(first.inbound_date).slice(0, 10) : ""); + setModalWarehouse(first.warehouse_code || ""); + setModalLocation(first.location_code || ""); + setModalInspector((first as any).inspector || ""); + setModalManager((first as any).manager || ""); + setModalMemo(first.memo || ""); + setSelectedItems( + grouped.map((g) => ({ + key: g.id, + inbound_type: g.inbound_type || "", + reference_number: g.reference_number || "", + supplier_code: (g as any).supplier_code || "", + supplier_name: g.supplier_name || "", + item_number: g.item_number || "", + item_name: g.item_name || "", + spec: g.spec || "", + material: (g as any).material || "", + unit: (g as any).unit || "", + inbound_qty: Number(g.inbound_qty) || 0, + unit_price: Number(g.unit_price) || 0, + total_amount: Number(g.total_amount) || 0, + source_table: (g as any).source_table || "", + source_id: (g as any).source_id || "", + })) + ); + setSourceKeyword(""); + setPurchaseOrders([]); + setShipments([]); + setItems([]); + setSourcePage(1); + setSourceTotalCount(0); + setIsModalOpen(true); + loadSourceData(first.inbound_type || "구매입고", undefined, 1); + }; + // 검색 버튼 클릭 시 const searchSourceData = useCallback(async () => { setSourcePage(1); @@ -811,41 +735,97 @@ export default function ReceivingPage() { } setSaving(true); try { - const res = await createReceiving({ - inbound_number: modalInboundNo, - inbound_date: modalInboundDate, - warehouse_code: modalWarehouse, - location_code: modalLocation || undefined, - inspector: modalInspector || undefined, - manager: modalManager || undefined, - memo: modalMemo || undefined, - items: selectedItems.map((item) => ({ - inbound_type: item.inbound_type, - reference_number: item.reference_number, - supplier_code: item.supplier_code, - supplier_name: item.supplier_name, - item_number: item.item_number, - item_name: item.item_name, - spec: item.spec, - material: item.material, - unit: item.unit, - inbound_qty: item.inbound_qty, - unit_price: item.unit_price, - total_amount: item.total_amount, - source_table: item.source_table, - source_id: item.source_id, - inbound_status: "입고완료", - inspection_status: "대기", - })), - }); + if (editMode) { + // 기존 item 수정 + 삭제된 item 삭제 + 새 item 추가 + const currentKeys = new Set(selectedItems.map((i) => i.key)); + const toDelete = editItemIds.filter((id) => !currentKeys.has(id)); + const toUpdate = selectedItems.filter((i) => editItemIds.includes(i.key)); + const toCreate = selectedItems.filter((i) => !editItemIds.includes(i.key)); - if (res.success) { - alert(res.message || "입고 등록 완료"); + await Promise.all([ + ...toDelete.map((id) => deleteReceiving(id)), + ...toUpdate.map((item) => + updateReceiving(item.key, { + inbound_date: modalInboundDate, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + warehouse_code: modalWarehouse || undefined, + location_code: modalLocation || undefined, + memo: modalMemo || undefined, + } as any) + ), + ...(toCreate.length > 0 + ? [createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: toCreate.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + })] + : []), + ]); + toast.success("입고 정보를 수정했어요"); setIsModalOpen(false); fetchList(); + } else { + const res = await createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: selectedItems.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + }); + + if (res.success) { + toast.success(res.message || "입고 등록 완료"); + setIsModalOpen(false); + fetchList(); + } } - } catch { - alert("입고 등록 중 오류가 발생했습니다."); + } catch (err: any) { + const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "입고 등록 중 오류가 발생했습니다."); + toast.error(msg); } finally { setSaving(false); } @@ -897,13 +877,13 @@ export default function ReceivingPage() { } /> - {/* 입고 목록 테이블 (마스터-디테일 그룹) */} + {/* 입고 목록 테이블 (플랫 리스트) */}

입고 목록

- {Object.keys(filteredGroups).length}건 + {filteredRows.length}건
-
+
- - {visibleMasterLayout.map((col) => ( - - ))} + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 입고번호 (별도 컬럼) */} - -
-
handleSort("inbound_number")}> - 입고번호 - {sortState?.key === "inbound_number" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["inbound_number"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {GRID_COLUMNS.map((col) => { + const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} + + ); + })} @@ -993,7 +963,7 @@ export default function ReceivingPage() { - ) : Object.keys(filteredGroups).length === 0 ? ( + ) : filteredRows.length === 0 ? (
@@ -1003,212 +973,59 @@ export default function ReceivingPage() { ) : ( - Object.entries(filteredGroups).map(([inboundNo, group]) => { - const isExpanded = expandedOrders.has(inboundNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 */} - { - if (expandedOrders.has(inboundNo)) { - setClosingOrders((prev) => new Set(prev).add(inboundNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(inboundNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(inboundNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(inboundNo)); - } + { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + onDoubleClick={() => openEditModal(row as any)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); }} > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 입고번호 */} - - {inboundNo} - ({group.details.length}) - - {/* 마스터 필드 (ts.visibleColumns 순서) */} - {visibleMasterLayout.map((col) => { - switch (col.key) { - case "inbound_type": return ( - - - {resolveInboundType(master.inbound_type)} - - - ); - case "inbound_date": return ( - - {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} - - ); - case "reference_number": return ( - - {master.reference_number || ""} - - ); - case "supplier_name": return ( - - {master.supplier_name || ""} - - ); - case "warehouse_name": return ( - - {master.warehouse_name || master.warehouse_code || ""} - - ); - case "inbound_status": return ( - - - {master.inbound_status || "-"} - - - ); - case "memo": return ( - - {master.memo || ""} - - ); - default: return {(master as any)[col.key] ?? ""}; - } - })} - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - - {visibleDetailCols.map((col) => { - const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => { - let v = (d as any)[col.key]; - if (col.key === "source_table") v = SOURCE_TABLE_LABEL[v] || v; - return v; - }).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} -
- )} - - {/* 디테일 행 (펼쳤을 때만) */} - {isExpanded && group.details.map((row, detailIdx) => { - const isClosing = closingOrders.has(inboundNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - - {visibleDetailCols.map((col) => { - switch (col.key) { - case "source_table": return {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"}; - case "item_number": return {row.item_number || ""}; - case "item_name": return {row.item_name || ""}; - case "spec": return {row.spec || ""}; - case "inbound_qty": return {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}; - case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; - case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; - default: return {(row as any)[col.key] ?? ""}; - } - })} - - ); - })} - + {}} /> + + {row.inbound_number || ""} + + + {row.inbound_type || "-"} + + + + {row.inbound_date ? new Date(row.inbound_date).toLocaleDateString("ko-KR") : ""} + + {row.reference_number || ""} + {row.source_type || ""} + {row.supplier_name || ""} + {row.item_number || ""} + {row.item_name || ""} + {row.spec || ""} + {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {row.warehouse_name || (row as any).warehouse_code || ""} + + + {row.inbound_status || "-"} + + + {row.remark || row.memo || ""} + ); }) )} @@ -1221,9 +1038,9 @@ export default function ReceivingPage() { - 입고 등록 + {editMode ? "입고 수정" : "입고 등록"} - 입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해 주세요. + {editMode ? "입고 정보를 수정해 주세요." : "입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해 주세요."}
diff --git a/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx index 38b01658..534582f9 100644 --- a/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx @@ -1053,14 +1053,14 @@ export default function ProductionPlanManagementPage() { return (
- + - + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - - 품목코드 - 품목명 + + 품목코드 + 품목명 {ts.visibleColumns.map((col) => ( {col.label} @@ -1073,9 +1073,9 @@ export default function ProductionPlanManagementPage() { if (item._isGroupSummary) { return ( - - - + + + {ts.visibleColumns.map((col) => { const v = (item as any)[col.key]; return ( @@ -1090,14 +1090,14 @@ export default function ProductionPlanManagementPage() { return ( - e.stopPropagation()}> + e.stopPropagation()}> toggleItemGroupSelect(item.item_code)} className="h-4 w-4" /> - toggleItemExpand(item.item_code)}> + toggleItemExpand(item.item_code)}> - toggleItemExpand(item.item_code)}>{item.item_code} - toggleItemExpand(item.item_code)}>{item.item_name} + toggleItemExpand(item.item_code)}>{item.item_code} + toggleItemExpand(item.item_code)}>{item.item_name} {ts.visibleColumns.map((col) => renderGroupCell(col, item))} {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => { @@ -1107,9 +1107,9 @@ export default function ProductionPlanManagementPage() { } return ( - - - + + +
수주번호: {detail.order_no} diff --git a/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx index 143a84a9..f712d3ed 100644 --- a/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx @@ -13,7 +13,7 @@ import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, - ClipboardList, Pencil, Search, X, Package, ChevronDown, + ClipboardList, Pencil, Search, X, Package, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, GripVertical, } from "lucide-react"; @@ -88,18 +88,18 @@ const GRID_COLUMNS_CONFIG = [ ]; const MODAL_DETAIL_COLUMNS = [ - { key: "item_code", label: "품번", width: "w-[120px]" }, - { key: "item_name", label: "품명", width: "w-[120px]" }, - { key: "supplier", label: "공급업체", width: "w-[150px]" }, - { key: "spec", label: "규격", width: "w-[80px]" }, - { key: "unit", label: "단위", width: "w-[60px]" }, - { key: "order_qty", label: "발주수량", width: "w-[90px]" }, - { key: "received_qty", label: "입고수량", width: "w-[90px]" }, - { key: "remain_qty", label: "잔량", width: "w-[80px]" }, - { key: "unit_price", label: "단가", width: "w-[100px]" }, - { key: "amount", label: "금액", width: "w-[100px]" }, - { key: "due_date", label: "납기일", width: "w-[160px]" }, - { key: "memo", label: "메모", width: "w-[120px]" }, + { key: "item_code", label: "품번", width: "min-w-[120px]" }, + { key: "item_name", label: "품명", width: "min-w-[150px]" }, + { key: "supplier", label: "공급업체", width: "min-w-[150px]" }, + { key: "spec", label: "규격", width: "min-w-[80px]" }, + { key: "unit", label: "단위", width: "min-w-[90px]" }, + { key: "order_qty", label: "발주수량", width: "min-w-[90px]" }, + { key: "received_qty", label: "입고수량", width: "min-w-[90px]" }, + { key: "remain_qty", label: "잔량", width: "min-w-[80px]" }, + { key: "unit_price", label: "단가", width: "min-w-[100px]" }, + { key: "amount", label: "금액", width: "min-w-[100px]" }, + { key: "due_date", label: "납기일", width: "min-w-[160px]" }, + { key: "memo", label: "메모", width: "min-w-[120px]" }, ]; const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16"; @@ -162,7 +162,6 @@ export default function PurchaseOrderPage() { const [excelUploadOpen, setExcelUploadOpen] = useState(false); const [categoryOptions, setCategoryOptions] = useState>({}); const [checkedIds, setCheckedIds] = useState([]); - const [expandedOrders, setExpandedOrders] = useState>(new Set()); // 테이블 설정 const ts = useTableSettings("c16-purchase-order", DETAIL_TABLE, GRID_COLUMNS_CONFIG); @@ -238,7 +237,7 @@ export default function PurchaseOrderPage() { ); try { const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, size: 5000, autoFilter: true, }); const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || []; optMap["supplier_code"] = supps.map((s: any) => ({ @@ -248,7 +247,7 @@ export default function PurchaseOrderPage() { } catch { /* skip */ } try { const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, size: 5000, autoFilter: true, }); const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; optMap["manager"] = users.map((u: any) => ({ @@ -294,7 +293,7 @@ export default function PurchaseOrderPage() { } const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined, autoFilter: true, sort: { columnName: "purchase_no", order: "desc" }, @@ -372,20 +371,6 @@ export default function PurchaseOrderPage() { useEffect(() => { fetchOrders(); }, [fetchOrders]); // purchase_no 기준 그룹핑 - const orderGroups = useMemo(() => { - const map: Record = {}; - for (const row of orders) { - const key = row.purchase_no || row.id; - if (!map[key]) { - map[key] = { - master: { purchase_no: row.purchase_no, order_date: row.order_date, supplier_name: row.supplier_name, status: row.status, memo: row.memo }, - details: [], - }; - } - map[key].details.push(row); - } - return map; - }, [orders]); const getCategoryLabel = (col: string, code: string) => { if (!code) return ""; @@ -553,7 +538,7 @@ export default function PurchaseOrderPage() { } }; - // 품목 검색 + // 품목 검색 (수주관리와 동일한 서버 페이징 방식) const searchItems = async (page?: number, size?: number) => { const p = page ?? itemPage; const s = size ?? itemPageSize; @@ -563,25 +548,24 @@ export default function PurchaseOrderPage() { if (itemSearchKeyword) { filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); } + // 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응) + if (itemSearchDivision !== "all") { + const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; + const divValues = [itemSearchDivision]; + if (divLabel) divValues.push(divLabel); + filters.push({ columnName: "division", operator: "in", value: divValues }); + } const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: p, size: s, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const resData = res.data?.data; - let allRows = resData?.data || resData?.rows || []; - if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - allRows = allRows.filter((item: any) => { - const div = item.division || ""; - return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel)); - }); - } - const total = allRows.length; - const start = (p - 1) * s; - setItemSearchResults(allRows.slice(start, start + s)); - setItemTotal(total); - setItemTotalPages(Math.max(1, Math.ceil(total / s))); + const rows = resData?.data || resData?.rows || []; + const serverTotal = resData?.total || resData?.totalCount || rows.length; + setItemSearchResults(rows); + setItemTotal(serverTotal); + setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); } catch { /* skip */ } finally { setItemSearchLoading(false); } @@ -623,7 +607,7 @@ export default function PurchaseOrderPage() { try { const itemIds = selected.map((item) => item.item_number || item.id); const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [ @@ -686,7 +670,7 @@ export default function PurchaseOrderPage() { if (itemCodes.length === 0) return; try { const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, autoFilter: true, }); @@ -708,7 +692,7 @@ export default function PurchaseOrderPage() { if (itemCodes.length === 0) return; try { const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [ { columnName: "supplier_id", operator: "equals", value: supplierCode }, { columnName: "item_id", operator: "in", value: itemCodes }, @@ -811,125 +795,83 @@ export default function PurchaseOrderPage() {
- {/* 데이터 테이블 — 아코디언 그룹핑 */} + {/* 데이터 테이블 — 플랫 리스트 */}
{loading ? (
- ) : Object.keys(orderGroups).length === 0 ? ( + ) : orders.length === 0 ? (

등록된 발주가 없어요

) : ( (() => { - const MASTER_KEYS = new Set(["purchase_no", "order_date", "supplier_name", "status", "memo"]); const numCols = new Set(["order_qty", "received_qty", "remain_qty", "unit_price", "amount"]); - - // ts.visibleColumns 순서를 따르되, 마스터/디테일 컬럼을 분리 - // 고정 컬럼(품목수)은 마스터 선행 컬럼 뒤에 배치 - const leadingMaster: typeof ts.visibleColumns = []; - const detailCols: typeof ts.visibleColumns = []; - const trailingMaster: typeof ts.visibleColumns = []; - let passedFirstDetail = false; - for (const col of ts.visibleColumns) { - if (MASTER_KEYS.has(col.key)) { - if (passedFirstDetail) trailingMaster.push(col); - else leadingMaster.push(col); - } else { - passedFirstDetail = true; - detailCols.push(col); - } - } - - const renderDetailCell = (row: any, key: string) => { + const renderCell = (row: any, key: string) => { const val = row[key]; if (key === "status") return val ? {val} : "-"; - if (numCols.has(key)) return {val ? Number(val).toLocaleString() : "0"}; - return val || "-"; + if (key === "order_date" || key === "due_date") return val ? new Date(val).toLocaleDateString("ko-KR") : "-"; + if (numCols.has(key)) return {val ? Number(val).toLocaleString() : ""}; + return val || ""; }; - - const renderMasterHead = (col: { key: string; label: string }) => ( - - {col.label} - - ); - - const renderMasterCell = (col: { key: string }, m: any, purchaseNo: string) => { - if (col.key === "purchase_no") return {purchaseNo}; - if (col.key === "order_date") return {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}; - if (col.key === "supplier_name") return {m.supplier_name || "-"}; - if (col.key === "status") return {m.status && {m.status}}; - if (col.key === "memo") return {m.memo || ""}; - return ; - }; - return (
- - - {leadingMaster.map(renderMasterHead)} - 품목수 - {detailCols.map(col => ( - - {col.label}{col.key === "order_qty" || col.key === "amount" ? " 합계" : ""} + { + const allIds = orders.map((r) => r.id); + const allChecked = allIds.length > 0 && allIds.every((id) => checkedIds.includes(id)); + setCheckedIds(allChecked ? [] : allIds); + }} + > + 0 && orders.every((r) => checkedIds.includes(r.id))} + onCheckedChange={() => {}} + /> + + {ts.visibleColumns.map((col) => ( + + {col.label} ))} - {trailingMaster.map(renderMasterHead)} - {Object.entries(orderGroups).map(([purchaseNo, group]) => { - const isExpanded = expandedOrders.has(purchaseNo); - const detailIds = group.details.map(d => d.id); - const allChecked = detailIds.length > 0 && detailIds.every(id => checkedIds.includes(id)); - const someChecked = detailIds.some(id => checkedIds.includes(id)); - const m = group.master; - const totalQty = group.details.reduce((s, d) => s + (Number(d.order_qty) || 0), 0); - const totalAmt = group.details.reduce((s, d) => s + (Number(d.amount) || 0), 0); + {orders.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - setExpandedOrders(prev => { const next = new Set(prev); if (next.has(purchaseNo)) next.delete(purchaseNo); else next.add(purchaseNo); return next; })} - onDoubleClick={() => openEditModal(purchaseNo)} + { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + onDoubleClick={() => openEditModal(row.purchase_no)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} > - - + {}} /> + + {ts.visibleColumns.map((col) => ( + + {renderCell(row, col.key)} - { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !detailIds.includes(id)) : [...new Set([...prev, ...detailIds])]); }}> - {}} /> - - {leadingMaster.map(col => renderMasterCell(col, m, purchaseNo))} - {group.details.length}건 - {detailCols.map(col => ( - - {col.key === "order_qty" ? totalQty.toLocaleString() - : col.key === "amount" ? totalAmt.toLocaleString() - : ""} - - ))} - {trailingMaster.map(col => renderMasterCell(col, m, purchaseNo))} - - {isExpanded && group.details.map((row) => ( - - - { e.stopPropagation(); setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id]); }}> - {}} /> - - {leadingMaster.map(col => )} - - {detailCols.map(col => ( - - {renderDetailCell(row, col.key)} - - ))} - {trailingMaster.map(col => )} - ))} - + ); })} @@ -938,7 +880,7 @@ export default function PurchaseOrderPage() { })() )}
- 전체 {Object.keys(orderGroups).length}건 (발주 기준) / {orders.length}건 (품목 기준) + 전체 {orders.length}건
@@ -1094,7 +1036,7 @@ export default function PurchaseOrderPage() { ) : (
-
+
c.key)} strategy={horizontalListSortingStrategy}> @@ -1118,12 +1060,12 @@ export default function PurchaseOrderPage() { {visibleModalColumns.map((col) => { switch (col.key) { case "item_code": - return {row.item_code}; + return {row.item_code}; case "item_name": - return {row.item_name}; + return {row.item_name}; case "supplier": return ( - + {isReadOnly ? ( {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} ) : ( @@ -1133,7 +1075,7 @@ export default function PurchaseOrderPage() { updateDetailRow(idx, "supplier_code", v); updateDetailRow(idx, "supplier_name", name); }}> - + {(categoryOptions["supplier_code"] || []).map(o => ( {o.label} @@ -1149,11 +1091,11 @@ export default function PurchaseOrderPage() { return {row.unit}; case "order_qty": return ( - + {isReadOnly ? ( {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} ) : ( - updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" /> )} ); @@ -1163,11 +1105,11 @@ export default function PurchaseOrderPage() { return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; case "unit_price": return ( - + {isReadOnly ? ( {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} ) : ( - updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" /> )} ); @@ -1175,21 +1117,21 @@ export default function PurchaseOrderPage() { return {row.amount ? Number(row.amount).toLocaleString() : ""}; case "due_date": return ( - + {isReadOnly ? ( {row.due_date} ) : ( - updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> + updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs w-full" /> )} ); case "memo": return ( - + {isReadOnly ? ( {row.memo} ) : ( - updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> + updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs w-full" /> )} ); diff --git a/frontend/app/(main)/COMPANY_9/purchase/purchase-item/page.tsx b/frontend/app/(main)/COMPANY_9/purchase/purchase-item/page.tsx index ce6e8198..31ca38f4 100644 --- a/frontend/app/(main)/COMPANY_9/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_9/purchase/purchase-item/page.tsx @@ -142,6 +142,10 @@ const FORM_FIELDS = [ { key: "division", label: "관리품목", type: "multi-category" }, { key: "type", label: "품목구분", type: "category" }, { key: "size", label: "규격", type: "text" }, + { key: "width", label: "가로", type: "text", placeholder: "숫자 입력 (mm)" }, + { key: "height", label: "세로", type: "text", placeholder: "숫자 입력 (mm)" }, + { key: "thickness", label: "두께", type: "text", placeholder: "숫자 입력 (mm)" }, + { key: "area", label: "면적", type: "text", placeholder: "숫자 입력 (㎡)" }, { key: "unit", label: "단위", type: "category" }, { key: "material", label: "재질", type: "category" }, { key: "status", label: "상태", type: "category" }, @@ -175,6 +179,10 @@ const ITEM_GRID_COLUMNS = [ { key: "item_number", label: "품번" }, { key: "item_name", label: "품명" }, { key: "size", label: "규격" }, + { key: "width", label: "가로" }, + { key: "height", label: "세로" }, + { key: "thickness", label: "두께" }, + { key: "area", label: "면적" }, { key: "unit", label: "단위" }, { key: "standard_price", label: "기준단가/구매단가" }, { key: "currency_code", label: "통화" }, @@ -1605,9 +1613,25 @@ export default function PurchaseItemPage() { ) : ( setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))} - placeholder={"placeholder" in field ? field.placeholder : field.label} - className="h-9" + readOnly={field.key === "area"} + onChange={(e) => { + if (field.key === "area") return; + const v = e.target.value; + setFormData((prev) => { + const next = { ...prev, [field.key]: v }; + // 가로/세로 변경 시 면적(㎡) 자동 계산: (가로mm × 세로mm) / 1,000,000 + if (field.key === "width" || field.key === "height") { + const w = Number(field.key === "width" ? v : prev.width); + const h = Number(field.key === "height" ? v : prev.height); + if (!isNaN(w) && !isNaN(h) && w > 0 && h > 0) { + next.area = ((w * h) / 1_000_000).toFixed(4); + } + } + return next; + }); + }} + placeholder={field.key === "area" ? "자동 계산" : ("placeholder" in field ? field.placeholder : field.label)} + className={cn("h-9", field.key === "area" && "bg-muted cursor-not-allowed")} /> )} diff --git a/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx index 05facbbc..863c0aef 100644 --- a/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx @@ -424,9 +424,10 @@ export default function ItemInspectionInfoPage() { case "inspection_type": return (
- {group.types.map((t: string) => ( - {t} - ))} + {group.types.map((t: string) => { + const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t; + return {label}; + })}
); @@ -494,7 +495,7 @@ export default function ItemInspectionInfoPage() { : "bg-muted/50 text-muted-foreground border-border hover:bg-muted" )} > - {type} + {inspTypeCatOptions.find((o) => o.code === type)?.label || type} { - if (checkedIds.length === 0) { toast.error("삭제할 수주를 선택해주세요."); return; } - const selectedItems = orders.filter((o) => checkedIds.includes(o.id)); - const orderNos = [...new Set(selectedItems.map((o) => o.order_no))]; - const ok = await confirm(`${orderNos.length}건의 수주를 삭제하시겠습니까?`, { + if (checkedIds.length === 0) { toast.error("삭제할 항목을 선택해주세요."); return; } + const ok = await confirm(`${checkedIds.length}건의 수주 항목을 삭제하시겠습니까?`, { description: "삭제된 데이터는 복구할 수 없습니다.", variant: "destructive", confirmText: "삭제", }); if (!ok) return; try { + // 1. 선택한 디테일 삭제 + await apiClient.delete(`/table-management/tables/${DETAIL_TABLE}/delete`, { + data: checkedIds.map((id) => ({ id })), + }); + + // 2. 영향받는 수주번호의 잔여 디테일 확인 → 0건이면 마스터도 삭제 + const selectedItems = orders.filter((o) => checkedIds.includes(o.id)); + const orderNos = [...new Set(selectedItems.map((o) => o.order_no))]; for (const orderNo of orderNos) { - // 디테일 삭제 - const detailRes = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { - page: 1, size: 9999, - dataFilter: { enabled: true, filters: [{ columnName: "order_no", operator: "equals", value: orderNo }] }, - autoFilter: true, - }); - const details = detailRes.data?.data?.data || detailRes.data?.data?.rows || []; - if (details.length > 0) { - await apiClient.delete(`/table-management/tables/${DETAIL_TABLE}/delete`, { - data: details.map((d: any) => ({ id: d.id })), - }); - } - // 마스터 삭제 - const masterRes = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, { + const remainRes = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { page: 1, size: 1, dataFilter: { enabled: true, filters: [{ columnName: "order_no", operator: "equals", value: orderNo }] }, autoFilter: true, }); - const masters = masterRes.data?.data?.data || masterRes.data?.data?.rows || []; - if (masters.length > 0) { - await apiClient.delete(`/table-management/tables/${MASTER_TABLE}/delete`, { - data: masters.map((m: any) => ({ id: m.id })), + const remaining = remainRes.data?.data?.data || remainRes.data?.data?.rows || []; + if (remaining.length === 0) { + // 디테일 0건 → 마스터 삭제 + const masterRes = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, { + page: 1, size: 1, + dataFilter: { enabled: true, filters: [{ columnName: "order_no", operator: "equals", value: orderNo }] }, + autoFilter: true, }); + const masters = masterRes.data?.data?.data || masterRes.data?.data?.rows || []; + if (masters.length > 0) { + await apiClient.delete(`/table-management/tables/${MASTER_TABLE}/delete`, { + data: masters.map((m: any) => ({ id: m.id })), + }); + } } } toast.success("삭제되었습니다."); @@ -622,6 +624,15 @@ export default function SalesOrderPage() { const filters: any[] = []; if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); + // 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응) + if (itemSearchDivision !== "all") { + const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; + // 코드 또는 라벨이 저장된 경우 모두 조회하기 위해 in 연산자 사용 + const divValues = [itemSearchDivision]; + if (divLabel) divValues.push(divLabel); + filters.push({ columnName: "division", operator: "in", value: divValues }); + } + // 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링 const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI"; const partnerId = masterForm.partner_id; @@ -630,7 +641,7 @@ export default function SalesOrderPage() { if (isCustomerPrice && partnerId) { try { const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, { - page: 1, size: 500, + page: 1, size: 5000, dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] }, autoFilter: true, }); @@ -640,31 +651,22 @@ export default function SalesOrderPage() { } const res = await apiClient.post(`/table-management/tables/item_info/data`, { - page: 1, size: 500, + page: p, size: s, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const resData = res.data?.data; - let allRows = resData?.data || resData?.rows || []; + let rows = resData?.data || resData?.rows || []; + const serverTotal = resData?.total || resData?.totalCount || rows.length; - // 거래처우선일 때 연결된 품목만 표시 + // 거래처우선일 때 연결된 품목만 표시 (클라이언트 필터) if (customerItemIds) { - allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); + rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); } - // 관리품목 필터 (코드/라벨 혼재 대응) - if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - allRows = allRows.filter((item: any) => { - const div = item.division || ""; - return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel)); - }); - } - const total = allRows.length; - const start = (p - 1) * s; - setItemSearchResults(allRows.slice(start, start + s)); - setItemTotal(total); - setItemTotalPages(Math.max(1, Math.ceil(total / s))); + setItemSearchResults(rows); + setItemTotal(serverTotal); + setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); } catch { /* skip */ } finally { setItemSearchLoading(false); } @@ -1318,44 +1320,44 @@ export default function SalesOrderPage() {
- No - 품번 - 품명 - 규격 - 재질 - 포장재 - 단위 - 수량 - 포장수량 - 단가 - 금액 - 납기일 - 분할/삭제 + No + 품번 + 품명 + 규격 + 재질 + 포장재 + 단위 + 수량 + 포장수량 + 단가 + 금액 + 납기일 + 분할/삭제 {detailRows.map((row, idx) => ( {idx + 1} - - {row.part_code} + + {row.part_code} - - {row.part_name} + + {row.part_name} - {row.spec} - {row.material} + {row.spec} + {row.material} updateDetailRow(idx, "packing_material", e.target.value)} placeholder="포장재" - className="h-8 text-xs" + className="h-8 text-xs w-full" /> - - - 전체 - {(categoryOptions["item_division"] || []).map((o) => ( - {o.label} - ))} - - +
영업관리