diff --git a/frontend/app/(main)/COMPANY_10/logistics/inbound-outbound/page.tsx b/frontend/app/(main)/COMPANY_10/logistics/inbound-outbound/page.tsx index fed63cef..587940ce 100644 --- a/frontend/app/(main)/COMPANY_10/logistics/inbound-outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_10/logistics/inbound-outbound/page.tsx @@ -38,6 +38,30 @@ const fmtDate = (v: any) => { return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10); }; +/** remark가 JSON이면 사람이 읽을 수 있는 텍스트로 변환 */ +const parseRemark = (remark: string | null | undefined): string => { + if (!remark) return ""; + const trimmed = remark.trim(); + if (!trimmed.startsWith("{")) return trimmed; + try { + const d = JSON.parse(trimmed); + switch (d.type) { + case "move": + return `창고이동 (${d.from_warehouse} → ${d.to_warehouse})`; + case "adjust": + return `재고조정 (${d.reason || "사유 없음"}, ${d.system_qty}→${d.actual_qty}, 차이:${d.diff >= 0 ? "+" : ""}${d.diff})`; + case "confirm": + return `재고확인 (${d.reason || "이상없음"})`; + case "process_inbound": + return "공정입고"; + default: + return d.reason || d.memo || trimmed; + } + } catch { + return trimmed; + } +}; + export default function InboundOutboundPage() { const { user } = useAuth(); @@ -62,7 +86,6 @@ export default function InboundOutboundPage() { try { const filters: any[] = []; if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter }); - if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter }); for (const f of searchFilters) { if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value }); @@ -81,6 +104,21 @@ export default function InboundOutboundPage() { const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))]; if (itemCodes.length > 0) { try { + // 단위 카테고리 코드→라벨 매핑 로드 + let unitLabelMap: Record = {}; + try { + const catRes = await apiClient.get("/table-categories/item_info/unit/values"); + if (catRes.data?.success && catRes.data.data?.length > 0) { + const flatten = (vals: any[]) => { + for (const v of vals) { + unitLabelMap[v.valueCode] = v.valueLabel; + if (v.children?.length) flatten(v.children); + } + }; + flatten(catRes.data.data); + } + } catch { /* skip */ } + const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, { page: 1, size: itemCodes.length + 10, dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, @@ -89,7 +127,8 @@ export default function InboundOutboundPage() { const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || []; const map: Record = {}; for (const i of items) { - if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" }; + const rawUnit = i.unit || ""; + if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit }; } setItemMap(map); } catch { /* skip */ } @@ -124,7 +163,7 @@ export default function InboundOutboundPage() { } finally { setLoading(false); } - }, [searchFilters, typeFilter, categoryFilter]); + }, [searchFilters, typeFilter]); useEffect(() => { fetchData(); }, [fetchData]); @@ -132,20 +171,26 @@ export default function InboundOutboundPage() { const categoryOptions = useMemo(() => { const set = new Set(); - data.forEach((r) => { if (r.remark) set.add(r.remark); }); + data.forEach((r) => { if (r.remark) set.add(parseRemark(r.remark)); }); return Array.from(set).sort(); }, [data]); // ════════ 그룹핑 ════════ + // 카테고리 필터 (클라이언트) + const filteredData = useMemo(() => { + if (categoryFilter === "all") return data; + return data.filter((r) => parseRemark(r.remark) === categoryFilter); + }, [data, categoryFilter]); + const groupedData = useMemo(() => { - if (groupBy === "none") return data; + if (groupBy === "none") return filteredData; const groups = new Map(); - for (const row of data) { + for (const row of filteredData) { let key: string; switch (groupBy) { case "transaction_type": key = row.transaction_type || "미지정"; break; - case "remark": key = row.remark || "미지정"; break; + case "remark": key = parseRemark(row.remark) || "미지정"; break; case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; default: key = row[groupBy] || "미지정"; @@ -191,7 +236,7 @@ export default function InboundOutboundPage() { const rows = data.map((r, i) => ({ No: i + 1, 입출고구분: r.transaction_type || "", - 카테고리: r.remark || "", + 카테고리: parseRemark(r.remark), 처리일자: fmtDate(r.transaction_date), 창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "", 위치: r.location_code || "", @@ -252,7 +297,7 @@ export default function InboundOutboundPage() {
입출고 내역 - {data.length}건 + {filteredData.length}건
{ setGroupBy(v); setExpandedGroups(new Set()); }}> @@ -353,7 +398,7 @@ export default function InboundOutboundPage() { {row.transaction_type} - {row.remark || "-"} + {parseRemark(row.remark) || "-"} {fmtDate(row.transaction_date)} {warehouseMap[row.warehouse_code] || row.warehouse_code || "-"} {row.location_code || "-"} diff --git a/frontend/app/(main)/COMPANY_16/logistics/material-status/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/material-status/page.tsx index eb87ba92..58354385 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/material-status/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/material-status/page.tsx @@ -43,6 +43,7 @@ import { } from "@/lib/api/materialStatus"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { apiClient } from "@/lib/api/client"; const GRID_COLUMNS = [ { key: "plan_no", label: "계획번호" }, @@ -60,26 +61,31 @@ const formatDate = (date: Date) => { return `${y}-${m}-${d}`; }; -const getStatusLabel = (status: string) => { - const map: Record = { - planned: "계획", - in_progress: "진행중", - completed: "완료", - pending: "대기", - cancelled: "취소", - }; - return map[status] || status; +const FALLBACK_STATUS_MAP: Record = { + planned: "계획", + in_progress: "진행중", + completed: "완료", + pending: "대기", + cancelled: "취소", }; -const getStatusStyle = (status: string) => { - const map: Record = { - planned: "bg-secondary text-secondary-foreground border-border", - pending: "bg-secondary text-secondary-foreground border-border", - in_progress: "bg-primary/10 text-primary border-primary/20", - completed: "bg-accent text-accent-foreground border-accent/50", - cancelled: "bg-muted text-muted-foreground border-border", - }; - return map[status] || "bg-muted text-muted-foreground border-border"; +const STATUS_STYLE_MAP: Record = { + planned: "bg-secondary text-secondary-foreground border-border", + pending: "bg-secondary text-secondary-foreground border-border", + in_progress: "bg-primary/10 text-primary border-primary/20", + completed: "bg-accent text-accent-foreground border-accent/50", + cancelled: "bg-muted text-muted-foreground border-border", +}; + +// 카테고리 라벨 기반으로 스타일 매칭 +const LABEL_STYLE_MAP: Record = { + "일반": "bg-secondary text-secondary-foreground border-border", + "긴급": "bg-destructive/10 text-destructive border-destructive/20", + "계획": "bg-secondary text-secondary-foreground border-border", + "대기": "bg-secondary text-secondary-foreground border-border", + "진행중": "bg-primary/10 text-primary border-primary/20", + "완료": "bg-accent text-accent-foreground border-accent/50", + "취소": "bg-muted text-muted-foreground border-border", }; export default function MaterialStatusPage() { @@ -93,6 +99,32 @@ export default function MaterialStatusPage() { const [searchItemCode, setSearchItemCode] = useState(""); const [searchItemName, setSearchItemName] = useState(""); + // 카테고리 코드→라벨 매핑 + const [statusMap, setStatusMap] = useState>({}); + + useEffect(() => { + (async () => { + try { + const res = await apiClient.get("/table-categories/work_instruction/status/values"); + if (res.data?.success && res.data.data?.length > 0) { + const map: Record = {}; + const flatten = (vals: any[]) => { + for (const v of vals) { + map[v.valueCode] = v.valueLabel; + if (v.children?.length) flatten(v.children); + } + }; + flatten(res.data.data); + setStatusMap(map); + } + } catch { /* ignore */ } + })(); + }, []); + + const getStatusLabel = useCallback((status: string) => { + return statusMap[status] || FALLBACK_STATUS_MAP[status] || status; + }, [statusMap]); + const [workOrders, setWorkOrders] = useState([]); const [workOrdersLoading, setWorkOrdersLoading] = useState(false); const [checkedWoIds, setCheckedWoIds] = useState([]); @@ -369,7 +401,7 @@ export default function MaterialStatusPage() { {getStatusLabel(wo.status)} diff --git a/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx index 96ce95f8..824e2e86 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx @@ -324,25 +324,28 @@ export default function OutboundPage() { return result; }; const map: Record> = {}; - Promise.all( - ["material", "unit"].map(async (col) => { + Promise.all([ + ...["material", "unit"].map(async (col) => { try { - const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`); + const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`); const items = flatten(res.data?.data || []); map[col] = {}; for (const item of items) map[col][item.code] = item.label; - } catch (e: any) { console.error("[outbound] cat load error:", col, e?.message, e?.response?.status, e?.response?.data); } - }) - ).then(() => { - console.log("[outbound] catMap loaded:", JSON.stringify(map).slice(0, 200)); - setCatMap(map); - }); + } catch { /* skip */ } + }), + (async () => { + try { + const res = await apiClient.get(`/table-categories/outbound_mng/outbound_type/values`); + const items = flatten(res.data?.data || []); + map["outbound_type"] = {}; + for (const item of items) map["outbound_type"][item.code] = item.label; + } catch { /* skip */ } + })(), + ]).then(() => setCatMap(map)); }, []); const resolveCat = useCallback((col: string, code: string) => { if (!code) return ""; - const result = catMap[col]?.[code] || code; - if (code.startsWith("CAT_")) console.log("[outbound] resolveCat:", col, code, "->", result, "catMap keys:", Object.keys(catMap)); - return result; + return catMap[col]?.[code] || code; }, [catMap]); // 소스 데이터 @@ -377,7 +380,7 @@ export default function OutboundPage() { } const res = await getOutboundList(params); if (res.success) setData(res.data); - } catch (err: any) { + } catch { // ignore } finally { setLoading(false); @@ -394,7 +397,7 @@ export default function OutboundPage() { try { const res = await getOutboundWarehouses(); if (res.success) setWarehouses(res.data); - } catch (err: any) { + } catch { // ignore } })(); @@ -570,7 +573,7 @@ export default function OutboundPage() { const res = await getItemSources(keyword || undefined); if (res.success) setItems(res.data); } - } catch (err: any) { + } catch { // ignore } finally { setSourceLoading(false); @@ -602,7 +605,7 @@ export default function OutboundPage() { loadSourceData(defaultType), ]); if (numRes.success) setModalOutboundNo(numRes.data); - } catch (err: any) { + } catch { setModalOutboundNo(""); } }; @@ -906,7 +909,8 @@ export default function OutboundPage() { } } } catch (err: any) { - const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다."); toast.error(msg); + const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다."); + toast.error(msg); } finally { setSaving(false); } @@ -1119,7 +1123,7 @@ export default function OutboundPage() { case "outbound_type": return ( - {master.outbound_type || "-"} + {resolveCat("outbound_type", master.outbound_type) || "-"} ); @@ -1537,7 +1541,7 @@ export default function OutboundPage() { {idx + 1} - {item.outbound_type || "-"} + {resolveCat("outbound_type", item.outbound_type) || "-"} diff --git a/frontend/app/(main)/COMPANY_29/logistics/inbound-outbound/page.tsx b/frontend/app/(main)/COMPANY_29/logistics/inbound-outbound/page.tsx index fed63cef..587940ce 100644 --- a/frontend/app/(main)/COMPANY_29/logistics/inbound-outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_29/logistics/inbound-outbound/page.tsx @@ -38,6 +38,30 @@ const fmtDate = (v: any) => { return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10); }; +/** remark가 JSON이면 사람이 읽을 수 있는 텍스트로 변환 */ +const parseRemark = (remark: string | null | undefined): string => { + if (!remark) return ""; + const trimmed = remark.trim(); + if (!trimmed.startsWith("{")) return trimmed; + try { + const d = JSON.parse(trimmed); + switch (d.type) { + case "move": + return `창고이동 (${d.from_warehouse} → ${d.to_warehouse})`; + case "adjust": + return `재고조정 (${d.reason || "사유 없음"}, ${d.system_qty}→${d.actual_qty}, 차이:${d.diff >= 0 ? "+" : ""}${d.diff})`; + case "confirm": + return `재고확인 (${d.reason || "이상없음"})`; + case "process_inbound": + return "공정입고"; + default: + return d.reason || d.memo || trimmed; + } + } catch { + return trimmed; + } +}; + export default function InboundOutboundPage() { const { user } = useAuth(); @@ -62,7 +86,6 @@ export default function InboundOutboundPage() { try { const filters: any[] = []; if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter }); - if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter }); for (const f of searchFilters) { if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value }); @@ -81,6 +104,21 @@ export default function InboundOutboundPage() { const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))]; if (itemCodes.length > 0) { try { + // 단위 카테고리 코드→라벨 매핑 로드 + let unitLabelMap: Record = {}; + try { + const catRes = await apiClient.get("/table-categories/item_info/unit/values"); + if (catRes.data?.success && catRes.data.data?.length > 0) { + const flatten = (vals: any[]) => { + for (const v of vals) { + unitLabelMap[v.valueCode] = v.valueLabel; + if (v.children?.length) flatten(v.children); + } + }; + flatten(catRes.data.data); + } + } catch { /* skip */ } + const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, { page: 1, size: itemCodes.length + 10, dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, @@ -89,7 +127,8 @@ export default function InboundOutboundPage() { const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || []; const map: Record = {}; for (const i of items) { - if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" }; + const rawUnit = i.unit || ""; + if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit }; } setItemMap(map); } catch { /* skip */ } @@ -124,7 +163,7 @@ export default function InboundOutboundPage() { } finally { setLoading(false); } - }, [searchFilters, typeFilter, categoryFilter]); + }, [searchFilters, typeFilter]); useEffect(() => { fetchData(); }, [fetchData]); @@ -132,20 +171,26 @@ export default function InboundOutboundPage() { const categoryOptions = useMemo(() => { const set = new Set(); - data.forEach((r) => { if (r.remark) set.add(r.remark); }); + data.forEach((r) => { if (r.remark) set.add(parseRemark(r.remark)); }); return Array.from(set).sort(); }, [data]); // ════════ 그룹핑 ════════ + // 카테고리 필터 (클라이언트) + const filteredData = useMemo(() => { + if (categoryFilter === "all") return data; + return data.filter((r) => parseRemark(r.remark) === categoryFilter); + }, [data, categoryFilter]); + const groupedData = useMemo(() => { - if (groupBy === "none") return data; + if (groupBy === "none") return filteredData; const groups = new Map(); - for (const row of data) { + for (const row of filteredData) { let key: string; switch (groupBy) { case "transaction_type": key = row.transaction_type || "미지정"; break; - case "remark": key = row.remark || "미지정"; break; + case "remark": key = parseRemark(row.remark) || "미지정"; break; case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; default: key = row[groupBy] || "미지정"; @@ -191,7 +236,7 @@ export default function InboundOutboundPage() { const rows = data.map((r, i) => ({ No: i + 1, 입출고구분: r.transaction_type || "", - 카테고리: r.remark || "", + 카테고리: parseRemark(r.remark), 처리일자: fmtDate(r.transaction_date), 창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "", 위치: r.location_code || "", @@ -252,7 +297,7 @@ export default function InboundOutboundPage() {
입출고 내역 - {data.length}건 + {filteredData.length}건
{ setGroupBy(v); setExpandedGroups(new Set()); }}> @@ -353,7 +398,7 @@ export default function InboundOutboundPage() { {row.transaction_type} - {row.remark || "-"} + {parseRemark(row.remark) || "-"} {fmtDate(row.transaction_date)} {warehouseMap[row.warehouse_code] || row.warehouse_code || "-"} {row.location_code || "-"} diff --git a/frontend/app/(main)/COMPANY_30/logistics/material-status/page.tsx b/frontend/app/(main)/COMPANY_30/logistics/material-status/page.tsx index eb87ba92..58354385 100644 --- a/frontend/app/(main)/COMPANY_30/logistics/material-status/page.tsx +++ b/frontend/app/(main)/COMPANY_30/logistics/material-status/page.tsx @@ -43,6 +43,7 @@ import { } from "@/lib/api/materialStatus"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { apiClient } from "@/lib/api/client"; const GRID_COLUMNS = [ { key: "plan_no", label: "계획번호" }, @@ -60,26 +61,31 @@ const formatDate = (date: Date) => { return `${y}-${m}-${d}`; }; -const getStatusLabel = (status: string) => { - const map: Record = { - planned: "계획", - in_progress: "진행중", - completed: "완료", - pending: "대기", - cancelled: "취소", - }; - return map[status] || status; +const FALLBACK_STATUS_MAP: Record = { + planned: "계획", + in_progress: "진행중", + completed: "완료", + pending: "대기", + cancelled: "취소", }; -const getStatusStyle = (status: string) => { - const map: Record = { - planned: "bg-secondary text-secondary-foreground border-border", - pending: "bg-secondary text-secondary-foreground border-border", - in_progress: "bg-primary/10 text-primary border-primary/20", - completed: "bg-accent text-accent-foreground border-accent/50", - cancelled: "bg-muted text-muted-foreground border-border", - }; - return map[status] || "bg-muted text-muted-foreground border-border"; +const STATUS_STYLE_MAP: Record = { + planned: "bg-secondary text-secondary-foreground border-border", + pending: "bg-secondary text-secondary-foreground border-border", + in_progress: "bg-primary/10 text-primary border-primary/20", + completed: "bg-accent text-accent-foreground border-accent/50", + cancelled: "bg-muted text-muted-foreground border-border", +}; + +// 카테고리 라벨 기반으로 스타일 매칭 +const LABEL_STYLE_MAP: Record = { + "일반": "bg-secondary text-secondary-foreground border-border", + "긴급": "bg-destructive/10 text-destructive border-destructive/20", + "계획": "bg-secondary text-secondary-foreground border-border", + "대기": "bg-secondary text-secondary-foreground border-border", + "진행중": "bg-primary/10 text-primary border-primary/20", + "완료": "bg-accent text-accent-foreground border-accent/50", + "취소": "bg-muted text-muted-foreground border-border", }; export default function MaterialStatusPage() { @@ -93,6 +99,32 @@ export default function MaterialStatusPage() { const [searchItemCode, setSearchItemCode] = useState(""); const [searchItemName, setSearchItemName] = useState(""); + // 카테고리 코드→라벨 매핑 + const [statusMap, setStatusMap] = useState>({}); + + useEffect(() => { + (async () => { + try { + const res = await apiClient.get("/table-categories/work_instruction/status/values"); + if (res.data?.success && res.data.data?.length > 0) { + const map: Record = {}; + const flatten = (vals: any[]) => { + for (const v of vals) { + map[v.valueCode] = v.valueLabel; + if (v.children?.length) flatten(v.children); + } + }; + flatten(res.data.data); + setStatusMap(map); + } + } catch { /* ignore */ } + })(); + }, []); + + const getStatusLabel = useCallback((status: string) => { + return statusMap[status] || FALLBACK_STATUS_MAP[status] || status; + }, [statusMap]); + const [workOrders, setWorkOrders] = useState([]); const [workOrdersLoading, setWorkOrdersLoading] = useState(false); const [checkedWoIds, setCheckedWoIds] = useState([]); @@ -369,7 +401,7 @@ export default function MaterialStatusPage() { {getStatusLabel(wo.status)} diff --git a/frontend/app/(main)/COMPANY_30/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_30/logistics/outbound/page.tsx index 9c7eafcb..824e2e86 100644 --- a/frontend/app/(main)/COMPANY_30/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_30/logistics/outbound/page.tsx @@ -324,16 +324,24 @@ export default function OutboundPage() { return result; }; const map: Record> = {}; - Promise.all( - ["material", "unit"].map(async (col) => { + 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_7`); const items = flatten(res.data?.data || []); map[col] = {}; for (const item of items) map[col][item.code] = item.label; } catch { /* skip */ } - }) - ).then(() => setCatMap(map)); + }), + (async () => { + try { + const res = await apiClient.get(`/table-categories/outbound_mng/outbound_type/values`); + const items = flatten(res.data?.data || []); + map["outbound_type"] = {}; + for (const item of items) map["outbound_type"][item.code] = item.label; + } catch { /* skip */ } + })(), + ]).then(() => setCatMap(map)); }, []); const resolveCat = useCallback((col: string, code: string) => { if (!code) return ""; @@ -372,7 +380,7 @@ export default function OutboundPage() { } const res = await getOutboundList(params); if (res.success) setData(res.data); - } catch (err: any) { + } catch { // ignore } finally { setLoading(false); @@ -389,7 +397,7 @@ export default function OutboundPage() { try { const res = await getOutboundWarehouses(); if (res.success) setWarehouses(res.data); - } catch (err: any) { + } catch { // ignore } })(); @@ -565,7 +573,7 @@ export default function OutboundPage() { const res = await getItemSources(keyword || undefined); if (res.success) setItems(res.data); } - } catch (err: any) { + } catch { // ignore } finally { setSourceLoading(false); @@ -597,7 +605,7 @@ export default function OutboundPage() { loadSourceData(defaultType), ]); if (numRes.success) setModalOutboundNo(numRes.data); - } catch (err: any) { + } catch { setModalOutboundNo(""); } }; @@ -901,7 +909,8 @@ export default function OutboundPage() { } } } catch (err: any) { - const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다."); toast.error(msg); + const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다."); + toast.error(msg); } finally { setSaving(false); } @@ -1114,7 +1123,7 @@ export default function OutboundPage() { case "outbound_type": return ( - {master.outbound_type || "-"} + {resolveCat("outbound_type", master.outbound_type) || "-"} ); @@ -1532,7 +1541,7 @@ export default function OutboundPage() { {idx + 1} - {item.outbound_type || "-"} + {resolveCat("outbound_type", item.outbound_type) || "-"} diff --git a/frontend/app/(main)/COMPANY_7/logistics/inbound-outbound/page.tsx b/frontend/app/(main)/COMPANY_7/logistics/inbound-outbound/page.tsx index fed63cef..587940ce 100644 --- a/frontend/app/(main)/COMPANY_7/logistics/inbound-outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_7/logistics/inbound-outbound/page.tsx @@ -38,6 +38,30 @@ const fmtDate = (v: any) => { return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10); }; +/** remark가 JSON이면 사람이 읽을 수 있는 텍스트로 변환 */ +const parseRemark = (remark: string | null | undefined): string => { + if (!remark) return ""; + const trimmed = remark.trim(); + if (!trimmed.startsWith("{")) return trimmed; + try { + const d = JSON.parse(trimmed); + switch (d.type) { + case "move": + return `창고이동 (${d.from_warehouse} → ${d.to_warehouse})`; + case "adjust": + return `재고조정 (${d.reason || "사유 없음"}, ${d.system_qty}→${d.actual_qty}, 차이:${d.diff >= 0 ? "+" : ""}${d.diff})`; + case "confirm": + return `재고확인 (${d.reason || "이상없음"})`; + case "process_inbound": + return "공정입고"; + default: + return d.reason || d.memo || trimmed; + } + } catch { + return trimmed; + } +}; + export default function InboundOutboundPage() { const { user } = useAuth(); @@ -62,7 +86,6 @@ export default function InboundOutboundPage() { try { const filters: any[] = []; if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter }); - if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter }); for (const f of searchFilters) { if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value }); @@ -81,6 +104,21 @@ export default function InboundOutboundPage() { const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))]; if (itemCodes.length > 0) { try { + // 단위 카테고리 코드→라벨 매핑 로드 + let unitLabelMap: Record = {}; + try { + const catRes = await apiClient.get("/table-categories/item_info/unit/values"); + if (catRes.data?.success && catRes.data.data?.length > 0) { + const flatten = (vals: any[]) => { + for (const v of vals) { + unitLabelMap[v.valueCode] = v.valueLabel; + if (v.children?.length) flatten(v.children); + } + }; + flatten(catRes.data.data); + } + } catch { /* skip */ } + const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, { page: 1, size: itemCodes.length + 10, dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, @@ -89,7 +127,8 @@ export default function InboundOutboundPage() { const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || []; const map: Record = {}; for (const i of items) { - if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" }; + const rawUnit = i.unit || ""; + if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit }; } setItemMap(map); } catch { /* skip */ } @@ -124,7 +163,7 @@ export default function InboundOutboundPage() { } finally { setLoading(false); } - }, [searchFilters, typeFilter, categoryFilter]); + }, [searchFilters, typeFilter]); useEffect(() => { fetchData(); }, [fetchData]); @@ -132,20 +171,26 @@ export default function InboundOutboundPage() { const categoryOptions = useMemo(() => { const set = new Set(); - data.forEach((r) => { if (r.remark) set.add(r.remark); }); + data.forEach((r) => { if (r.remark) set.add(parseRemark(r.remark)); }); return Array.from(set).sort(); }, [data]); // ════════ 그룹핑 ════════ + // 카테고리 필터 (클라이언트) + const filteredData = useMemo(() => { + if (categoryFilter === "all") return data; + return data.filter((r) => parseRemark(r.remark) === categoryFilter); + }, [data, categoryFilter]); + const groupedData = useMemo(() => { - if (groupBy === "none") return data; + if (groupBy === "none") return filteredData; const groups = new Map(); - for (const row of data) { + for (const row of filteredData) { let key: string; switch (groupBy) { case "transaction_type": key = row.transaction_type || "미지정"; break; - case "remark": key = row.remark || "미지정"; break; + case "remark": key = parseRemark(row.remark) || "미지정"; break; case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; default: key = row[groupBy] || "미지정"; @@ -191,7 +236,7 @@ export default function InboundOutboundPage() { const rows = data.map((r, i) => ({ No: i + 1, 입출고구분: r.transaction_type || "", - 카테고리: r.remark || "", + 카테고리: parseRemark(r.remark), 처리일자: fmtDate(r.transaction_date), 창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "", 위치: r.location_code || "", @@ -252,7 +297,7 @@ export default function InboundOutboundPage() {
입출고 내역 - {data.length}건 + {filteredData.length}건
{ setGroupBy(v); setExpandedGroups(new Set()); }}> @@ -353,7 +398,7 @@ export default function InboundOutboundPage() { {row.transaction_type} - {row.remark || "-"} + {parseRemark(row.remark) || "-"} {fmtDate(row.transaction_date)} {warehouseMap[row.warehouse_code] || row.warehouse_code || "-"} {row.location_code || "-"} diff --git a/frontend/app/(main)/COMPANY_8/logistics/material-status/page.tsx b/frontend/app/(main)/COMPANY_8/logistics/material-status/page.tsx index eb87ba92..58354385 100644 --- a/frontend/app/(main)/COMPANY_8/logistics/material-status/page.tsx +++ b/frontend/app/(main)/COMPANY_8/logistics/material-status/page.tsx @@ -43,6 +43,7 @@ import { } from "@/lib/api/materialStatus"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { apiClient } from "@/lib/api/client"; const GRID_COLUMNS = [ { key: "plan_no", label: "계획번호" }, @@ -60,26 +61,31 @@ const formatDate = (date: Date) => { return `${y}-${m}-${d}`; }; -const getStatusLabel = (status: string) => { - const map: Record = { - planned: "계획", - in_progress: "진행중", - completed: "완료", - pending: "대기", - cancelled: "취소", - }; - return map[status] || status; +const FALLBACK_STATUS_MAP: Record = { + planned: "계획", + in_progress: "진행중", + completed: "완료", + pending: "대기", + cancelled: "취소", }; -const getStatusStyle = (status: string) => { - const map: Record = { - planned: "bg-secondary text-secondary-foreground border-border", - pending: "bg-secondary text-secondary-foreground border-border", - in_progress: "bg-primary/10 text-primary border-primary/20", - completed: "bg-accent text-accent-foreground border-accent/50", - cancelled: "bg-muted text-muted-foreground border-border", - }; - return map[status] || "bg-muted text-muted-foreground border-border"; +const STATUS_STYLE_MAP: Record = { + planned: "bg-secondary text-secondary-foreground border-border", + pending: "bg-secondary text-secondary-foreground border-border", + in_progress: "bg-primary/10 text-primary border-primary/20", + completed: "bg-accent text-accent-foreground border-accent/50", + cancelled: "bg-muted text-muted-foreground border-border", +}; + +// 카테고리 라벨 기반으로 스타일 매칭 +const LABEL_STYLE_MAP: Record = { + "일반": "bg-secondary text-secondary-foreground border-border", + "긴급": "bg-destructive/10 text-destructive border-destructive/20", + "계획": "bg-secondary text-secondary-foreground border-border", + "대기": "bg-secondary text-secondary-foreground border-border", + "진행중": "bg-primary/10 text-primary border-primary/20", + "완료": "bg-accent text-accent-foreground border-accent/50", + "취소": "bg-muted text-muted-foreground border-border", }; export default function MaterialStatusPage() { @@ -93,6 +99,32 @@ export default function MaterialStatusPage() { const [searchItemCode, setSearchItemCode] = useState(""); const [searchItemName, setSearchItemName] = useState(""); + // 카테고리 코드→라벨 매핑 + const [statusMap, setStatusMap] = useState>({}); + + useEffect(() => { + (async () => { + try { + const res = await apiClient.get("/table-categories/work_instruction/status/values"); + if (res.data?.success && res.data.data?.length > 0) { + const map: Record = {}; + const flatten = (vals: any[]) => { + for (const v of vals) { + map[v.valueCode] = v.valueLabel; + if (v.children?.length) flatten(v.children); + } + }; + flatten(res.data.data); + setStatusMap(map); + } + } catch { /* ignore */ } + })(); + }, []); + + const getStatusLabel = useCallback((status: string) => { + return statusMap[status] || FALLBACK_STATUS_MAP[status] || status; + }, [statusMap]); + const [workOrders, setWorkOrders] = useState([]); const [workOrdersLoading, setWorkOrdersLoading] = useState(false); const [checkedWoIds, setCheckedWoIds] = useState([]); @@ -369,7 +401,7 @@ export default function MaterialStatusPage() { {getStatusLabel(wo.status)} diff --git a/frontend/app/(main)/COMPANY_8/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_8/logistics/outbound/page.tsx index 86c425dd..824e2e86 100644 --- a/frontend/app/(main)/COMPANY_8/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_8/logistics/outbound/page.tsx @@ -324,16 +324,24 @@ export default function OutboundPage() { return result; }; const map: Record> = {}; - Promise.all( - ["material", "unit"].map(async (col) => { + 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_7`); const items = flatten(res.data?.data || []); map[col] = {}; for (const item of items) map[col][item.code] = item.label; } catch { /* skip */ } - }) - ).then(() => setCatMap(map)); + }), + (async () => { + try { + const res = await apiClient.get(`/table-categories/outbound_mng/outbound_type/values`); + const items = flatten(res.data?.data || []); + map["outbound_type"] = {}; + for (const item of items) map["outbound_type"][item.code] = item.label; + } catch { /* skip */ } + })(), + ]).then(() => setCatMap(map)); }, []); const resolveCat = useCallback((col: string, code: string) => { if (!code) return ""; @@ -372,7 +380,7 @@ export default function OutboundPage() { } const res = await getOutboundList(params); if (res.success) setData(res.data); - } catch (err: any) { + } catch { // ignore } finally { setLoading(false); @@ -389,7 +397,7 @@ export default function OutboundPage() { try { const res = await getOutboundWarehouses(); if (res.success) setWarehouses(res.data); - } catch (err: any) { + } catch { // ignore } })(); @@ -565,7 +573,7 @@ export default function OutboundPage() { const res = await getItemSources(keyword || undefined); if (res.success) setItems(res.data); } - } catch (err: any) { + } catch { // ignore } finally { setSourceLoading(false); @@ -597,7 +605,7 @@ export default function OutboundPage() { loadSourceData(defaultType), ]); if (numRes.success) setModalOutboundNo(numRes.data); - } catch (err: any) { + } catch { setModalOutboundNo(""); } }; @@ -901,7 +909,8 @@ export default function OutboundPage() { } } } catch (err: any) { - const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다."); toast.error(msg); + const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다."); + toast.error(msg); } finally { setSaving(false); } @@ -1114,7 +1123,7 @@ export default function OutboundPage() { case "outbound_type": return ( - {master.outbound_type || "-"} + {resolveCat("outbound_type", master.outbound_type) || "-"} ); @@ -1532,7 +1541,7 @@ export default function OutboundPage() { {idx + 1} - {item.outbound_type || "-"} + {resolveCat("outbound_type", item.outbound_type) || "-"} diff --git a/frontend/app/(main)/COMPANY_9/logistics/inbound-outbound/page.tsx b/frontend/app/(main)/COMPANY_9/logistics/inbound-outbound/page.tsx index fed63cef..587940ce 100644 --- a/frontend/app/(main)/COMPANY_9/logistics/inbound-outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_9/logistics/inbound-outbound/page.tsx @@ -38,6 +38,30 @@ const fmtDate = (v: any) => { return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10); }; +/** remark가 JSON이면 사람이 읽을 수 있는 텍스트로 변환 */ +const parseRemark = (remark: string | null | undefined): string => { + if (!remark) return ""; + const trimmed = remark.trim(); + if (!trimmed.startsWith("{")) return trimmed; + try { + const d = JSON.parse(trimmed); + switch (d.type) { + case "move": + return `창고이동 (${d.from_warehouse} → ${d.to_warehouse})`; + case "adjust": + return `재고조정 (${d.reason || "사유 없음"}, ${d.system_qty}→${d.actual_qty}, 차이:${d.diff >= 0 ? "+" : ""}${d.diff})`; + case "confirm": + return `재고확인 (${d.reason || "이상없음"})`; + case "process_inbound": + return "공정입고"; + default: + return d.reason || d.memo || trimmed; + } + } catch { + return trimmed; + } +}; + export default function InboundOutboundPage() { const { user } = useAuth(); @@ -62,7 +86,6 @@ export default function InboundOutboundPage() { try { const filters: any[] = []; if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter }); - if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter }); for (const f of searchFilters) { if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value }); @@ -81,6 +104,21 @@ export default function InboundOutboundPage() { const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))]; if (itemCodes.length > 0) { try { + // 단위 카테고리 코드→라벨 매핑 로드 + let unitLabelMap: Record = {}; + try { + const catRes = await apiClient.get("/table-categories/item_info/unit/values"); + if (catRes.data?.success && catRes.data.data?.length > 0) { + const flatten = (vals: any[]) => { + for (const v of vals) { + unitLabelMap[v.valueCode] = v.valueLabel; + if (v.children?.length) flatten(v.children); + } + }; + flatten(catRes.data.data); + } + } catch { /* skip */ } + const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, { page: 1, size: itemCodes.length + 10, dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, @@ -89,7 +127,8 @@ export default function InboundOutboundPage() { const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || []; const map: Record = {}; for (const i of items) { - if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" }; + const rawUnit = i.unit || ""; + if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit }; } setItemMap(map); } catch { /* skip */ } @@ -124,7 +163,7 @@ export default function InboundOutboundPage() { } finally { setLoading(false); } - }, [searchFilters, typeFilter, categoryFilter]); + }, [searchFilters, typeFilter]); useEffect(() => { fetchData(); }, [fetchData]); @@ -132,20 +171,26 @@ export default function InboundOutboundPage() { const categoryOptions = useMemo(() => { const set = new Set(); - data.forEach((r) => { if (r.remark) set.add(r.remark); }); + data.forEach((r) => { if (r.remark) set.add(parseRemark(r.remark)); }); return Array.from(set).sort(); }, [data]); // ════════ 그룹핑 ════════ + // 카테고리 필터 (클라이언트) + const filteredData = useMemo(() => { + if (categoryFilter === "all") return data; + return data.filter((r) => parseRemark(r.remark) === categoryFilter); + }, [data, categoryFilter]); + const groupedData = useMemo(() => { - if (groupBy === "none") return data; + if (groupBy === "none") return filteredData; const groups = new Map(); - for (const row of data) { + for (const row of filteredData) { let key: string; switch (groupBy) { case "transaction_type": key = row.transaction_type || "미지정"; break; - case "remark": key = row.remark || "미지정"; break; + case "remark": key = parseRemark(row.remark) || "미지정"; break; case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; default: key = row[groupBy] || "미지정"; @@ -191,7 +236,7 @@ export default function InboundOutboundPage() { const rows = data.map((r, i) => ({ No: i + 1, 입출고구분: r.transaction_type || "", - 카테고리: r.remark || "", + 카테고리: parseRemark(r.remark), 처리일자: fmtDate(r.transaction_date), 창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "", 위치: r.location_code || "", @@ -252,7 +297,7 @@ export default function InboundOutboundPage() {
입출고 내역 - {data.length}건 + {filteredData.length}건