diff --git a/backend-node/src/controllers/receivingController.ts b/backend-node/src/controllers/receivingController.ts index ef732ace..c57cd7c0 100644 --- a/backend-node/src/controllers/receivingController.ts +++ b/backend-node/src/controllers/receivingController.ts @@ -1050,7 +1050,7 @@ export async function getWarehouses(req: AuthenticatedRequest, res: Response) { const result = await pool.query( `SELECT warehouse_code, warehouse_name, warehouse_type FROM warehouse_info - WHERE company_code = $1 AND status != '삭제' + WHERE company_code = $1 AND COALESCE(status, '') != '삭제' ORDER BY warehouse_name`, [companyCode], ); diff --git a/frontend/app/(pop)/pop/equipment/inspection/page.tsx b/frontend/app/(pop)/pop/equipment/inspection/page.tsx new file mode 100644 index 00000000..3c7a5444 --- /dev/null +++ b/frontend/app/(pop)/pop/equipment/inspection/page.tsx @@ -0,0 +1,6 @@ +"use client"; +import { EquipmentInspection } from "@/components/pop/hardcoded/equipment/EquipmentInspection"; + +export default function EquipmentInspectionPage() { + return ; +} diff --git a/frontend/app/(pop)/pop/equipment/management/page.tsx b/frontend/app/(pop)/pop/equipment/management/page.tsx new file mode 100644 index 00000000..90abb5ef --- /dev/null +++ b/frontend/app/(pop)/pop/equipment/management/page.tsx @@ -0,0 +1,6 @@ +"use client"; +import { EquipmentList } from "@/components/pop/hardcoded/equipment/EquipmentList"; + +export default function EquipmentManagementPage() { + return ; +} diff --git a/frontend/app/(pop)/pop/equipment/page.tsx b/frontend/app/(pop)/pop/equipment/page.tsx new file mode 100644 index 00000000..421afbc8 --- /dev/null +++ b/frontend/app/(pop)/pop/equipment/page.tsx @@ -0,0 +1,6 @@ +"use client"; +import { EquipmentHome } from "@/components/pop/hardcoded/equipment/EquipmentHome"; + +export default function EquipmentPage() { + return ; +} diff --git a/frontend/app/(pop)/pop/inventory/move/page.tsx b/frontend/app/(pop)/pop/inventory/move/page.tsx new file mode 100644 index 00000000..d47b06ef --- /dev/null +++ b/frontend/app/(pop)/pop/inventory/move/page.tsx @@ -0,0 +1,6 @@ +"use client"; +import { InventoryMove } from "@/components/pop/hardcoded/inventory/InventoryMove"; + +export default function InventoryMovePage() { + return ; +} diff --git a/frontend/app/(pop)/pop/inventory/transfer/page.tsx b/frontend/app/(pop)/pop/inventory/transfer/page.tsx new file mode 100644 index 00000000..c58b92db --- /dev/null +++ b/frontend/app/(pop)/pop/inventory/transfer/page.tsx @@ -0,0 +1,6 @@ +"use client"; +import { InventoryTransfer } from "@/components/pop/hardcoded/inventory/InventoryTransfer"; + +export default function TransferPage() { + return ; +} diff --git a/frontend/components/pop/hardcoded/MenuIcons.tsx b/frontend/components/pop/hardcoded/MenuIcons.tsx index 4d2f93e2..66ddc7a5 100644 --- a/frontend/components/pop/hardcoded/MenuIcons.tsx +++ b/frontend/components/pop/hardcoded/MenuIcons.tsx @@ -126,7 +126,7 @@ const MENU_ITEMS: MenuIconItem[] = [ /> ), - href: "/pop/screens/equipment", + href: "/pop/equipment", }, { id: "inventory", diff --git a/frontend/components/pop/hardcoded/equipment/EquipmentHome.tsx b/frontend/components/pop/hardcoded/equipment/EquipmentHome.tsx new file mode 100644 index 00000000..bfba61fe --- /dev/null +++ b/frontend/components/pop/hardcoded/equipment/EquipmentHome.tsx @@ -0,0 +1,198 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import React, { useEffect, useState } from "react"; +import { apiClient } from "@/lib/api/client"; + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +interface EquipmentItem { + id: string; + equipment_code: string; + equipment_name: string; + equipment_type?: string; + status?: string; +} + +interface KpiData { + total: number; + active: number; + idle: number; + inspect: number; + rate: string; +} + +/* ------------------------------------------------------------------ */ +/* Menu */ +/* ------------------------------------------------------------------ */ + +const MENU_ITEMS = [ + { + id: "management", + title: "설비관리", + gradient: "linear-gradient(135deg,#8b5cf6,#6d28d9)", + shadowColor: "rgba(139,92,246,.3)", + icon: ( + + + + ), + href: "/pop/equipment/management", + }, + { + id: "inspection", + title: "설비점검", + gradient: "linear-gradient(135deg,#f59e0b,#d97706)", + shadowColor: "rgba(245,158,11,.3)", + icon: ( + + + + ), + href: "/pop/equipment/inspection", + }, +]; + +/* ------------------------------------------------------------------ */ +/* Component */ +/* ------------------------------------------------------------------ */ + +export function EquipmentHome() { + const router = useRouter(); + + const [kpi, setKpi] = useState({ + total: 0, + active: 0, + idle: 0, + inspect: 0, + rate: "0%", + }); + const [recentItems, setRecentItems] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + setLoading(true); + try { + const res = await apiClient.get("/data/equipment_mng", { + params: { pageSize: 500 }, + }); + const data = res.data?.data?.data ?? res.data?.data ?? []; + const items = Array.isArray(data) ? data : []; + const total = items.length; + const active = items.filter((i: EquipmentItem) => !i.status || i.status === "가동" || i.status === "정상").length; + const idle = items.filter((i: EquipmentItem) => i.status === "대기").length; + const inspect = items.filter((i: EquipmentItem) => i.status === "점검").length; + const rate = total > 0 ? `${Math.round((active / total) * 100)}%` : "0%"; + setKpi({ total, active, idle, inspect, rate }); + setRecentItems(items.slice(0, 5)); + } catch { /* */ } + setLoading(false); + }; + fetchData(); + }, []); + + return ( +
+ {/* Header */} +
+
+ +
+

설비

+

설비 현황 및 점검 관리

+
+
+ + {/* KPI */} +
+
+ {[ + { value: loading ? "-" : kpi.total, label: "전체", color: "text-gray-900" }, + { value: loading ? "-" : kpi.active, label: "가동", color: "text-green-600" }, + { value: loading ? "-" : kpi.idle, label: "대기", color: "text-blue-600" }, + { value: loading ? "-" : kpi.inspect, label: "점검", color: "text-red-600" }, + { value: loading ? "-" : kpi.rate, label: "가동률", color: "text-purple-600" }, + ].map((item) => ( +
+

{item.value}

+

{item.label}

+
+ ))} +
+
+
+ + {/* Menu Icons */} +
+

+ + 설비 관리 +

+
+ {MENU_ITEMS.map((item) => ( + + ))} +
+
+ + {/* Recent Equipment */} +
+
+
+

최근 설비

+ 최근 5건 +
+ {loading ? ( +
+
+
+ ) : recentItems.length === 0 ? ( +
등록된 설비가 없습니다
+ ) : ( +
+ {recentItems.map((item) => ( +
+
+

{item.equipment_name || "-"}

+

{item.equipment_code} {item.equipment_type ? `· ${item.equipment_type}` : ""}

+
+ + {item.status || "정상"} + +
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/frontend/components/pop/hardcoded/equipment/EquipmentInspection.tsx b/frontend/components/pop/hardcoded/equipment/EquipmentInspection.tsx new file mode 100644 index 00000000..a78374b4 --- /dev/null +++ b/frontend/components/pop/hardcoded/equipment/EquipmentInspection.tsx @@ -0,0 +1,119 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { PopShell } from "../PopShell"; + +type TabType = "all" | "running" | "idle" | "inspect" | "stopped"; + +export function EquipmentInspection() { + const router = useRouter(); + const [activeTab, setActiveTab] = useState("all"); + const [searchKeyword, setSearchKeyword] = useState(""); + + // 시안 기준 KPI (점검 테이블 없으므로 0) + const kpi = { total: 0, running: 0, idle: 0, inspect: 0, rate: "0%" }; + + const tabs: { key: TabType; label: string; count: number }[] = [ + { key: "all", label: "전체", count: kpi.total }, + { key: "running", label: "가동", count: kpi.running }, + { key: "idle", label: "대기", count: kpi.idle }, + { key: "inspect", label: "점검", count: kpi.inspect }, + { key: "stopped", label: "비가동", count: 0 }, + ]; + + return ( + +
+ {/* Header */} +
+
+ +
+

🔧 점검관리

+
+
+ + {/* Search */} +
+ setSearchKeyword(e.target.value)} + className="flex-1 px-4 py-3 rounded-xl border border-gray-200 text-sm focus:outline-none focus:border-amber-400" + /> + + +
+
+ + {/* KPI */} +
+
+ {[ + { label: "전체", value: kpi.total, color: "border-t-amber-500", icon: "🎬" }, + { label: "가동", value: kpi.running, color: "border-t-green-500", icon: "🟢" }, + { label: "대기", value: kpi.idle, color: "border-t-blue-500", icon: "🔵" }, + { label: "점검", value: kpi.inspect, color: "border-t-red-500", icon: "🔴" }, + { label: "가동률", value: kpi.rate, color: "border-t-purple-500", icon: "📊" }, + ].map((item) => ( +
+

{item.icon}

+

{item.value}

+

{item.label}

+
+ ))} +
+
+ + {/* Tabs */} +
+
+ {tabs.map((tab) => ( + + ))} +
+
+ + {/* Card List — 데이터 없음 */} +
+
+ 🔧 +

등록된 설비 점검 정보가 없습니다

+

PC에서 설비 점검 데이터를 등록하면 여기에 표시됩니다

+
+
+
+
+ ); +} diff --git a/frontend/components/pop/hardcoded/equipment/EquipmentList.tsx b/frontend/components/pop/hardcoded/equipment/EquipmentList.tsx new file mode 100644 index 00000000..9de9b20b --- /dev/null +++ b/frontend/components/pop/hardcoded/equipment/EquipmentList.tsx @@ -0,0 +1,169 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { apiClient } from "@/lib/api/client"; +import { PopShell } from "../PopShell"; + +interface EquipmentItem { + id: string; + equipment_code: string; + equipment_name: string; + equipment_type?: string; + location?: string; + status?: string; + last_inspection_date?: string; + next_inspection_date?: string; + memo?: string; +} + +export function EquipmentList() { + const router = useRouter(); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [searchKeyword, setSearchKeyword] = useState(""); + + const fetchEquipments = useCallback(async () => { + setLoading(true); + try { + const res = await apiClient.get("/data/equipment_mng", { + params: { pageSize: 500 }, + }); + const data = res.data?.data?.data ?? res.data?.data ?? []; + setItems(Array.isArray(data) ? data : []); + } catch { + setItems([]); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchEquipments(); + }, [fetchEquipments]); + + const filtered = items.filter((item) => { + if (!searchKeyword) return true; + const kw = searchKeyword.toLowerCase(); + return ( + (item.equipment_code || "").toLowerCase().includes(kw) || + (item.equipment_name || "").toLowerCase().includes(kw) || + (item.equipment_type || "").toLowerCase().includes(kw) + ); + }); + + // KPI + const totalCount = items.length; + const activeCount = items.filter((i) => i.status === "가동" || i.status === "정상" || !i.status).length; + const stopCount = items.filter((i) => i.status === "정지" || i.status === "비가동").length; + + return ( + +
+ {/* Header */} +
+
+ +
+

설비관리

+

설비 현황을 조회합니다

+
+
+ + {/* Search */} +
+ + + + setSearchKeyword(e.target.value)} + className="w-full pl-10 pr-4 py-3 rounded-xl border border-gray-200 text-sm focus:outline-none focus:border-blue-400" + /> +
+
+ + {/* KPI */} +
+
+
+

전체

+

{totalCount}

+
+
+

가동

+

{activeCount}

+
+
+

비가동

+

{stopCount}

+
+
+
+ + {/* List */} +
+
+

설비 목록

+ {filtered.length}건 +
+ + {loading ? ( +
+
+
+ ) : filtered.length === 0 ? ( +
+ + + +

등록된 설비가 없습니다

+
+ ) : ( +
+ {filtered.map((item) => ( +
+
+
+

{item.equipment_name || "-"}

+

{item.equipment_code}

+
+ + {item.status || "정상"} + +
+
+ {item.equipment_type && ( +
유형: {item.equipment_type}
+ )} + {item.location && ( +
위치: {item.location}
+ )} +
+
+ ))} +
+ )} +
+
+ + ); +} diff --git a/frontend/components/pop/hardcoded/inventory/InventoryHome.tsx b/frontend/components/pop/hardcoded/inventory/InventoryHome.tsx index bf61e374..aa6d0ac9 100644 --- a/frontend/components/pop/hardcoded/inventory/InventoryHome.tsx +++ b/frontend/components/pop/hardcoded/inventory/InventoryHome.tsx @@ -95,7 +95,29 @@ const MENU_ITEMS = [ /> ), - href: "#", + href: "/pop/inventory/transfer", + }, + { + id: "move", + title: "재고이동", + gradient: "linear-gradient(135deg,#10b981,#059669)", + shadowColor: "rgba(16,185,129,.3)", + icon: ( + + + + ), + href: "/pop/inventory/move", }, ]; diff --git a/frontend/components/pop/hardcoded/inventory/InventoryMove.tsx b/frontend/components/pop/hardcoded/inventory/InventoryMove.tsx new file mode 100644 index 00000000..247076d9 --- /dev/null +++ b/frontend/components/pop/hardcoded/inventory/InventoryMove.tsx @@ -0,0 +1,289 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { apiClient } from "@/lib/api/client"; +import { PopShell } from "../PopShell"; + +interface Warehouse { + id: string; + warehouse_code: string; + warehouse_name: string; +} + +interface StockItem { + id: string; + item_code: string; + item_name?: string; + warehouse_code: string; + location_code?: string; + current_qty: string; +} + +interface PendingItem { + stock: StockItem; + moveQty: number; + toWarehouse: string; +} + +export function InventoryMove() { + const router = useRouter(); + const [warehouses, setWarehouses] = useState([]); + const [fromWarehouse, setFromWarehouse] = useState(""); + const [toWarehouse, setToWarehouse] = useState(""); + const [stockItems, setStockItems] = useState([]); + const [loading, setLoading] = useState(false); + const [searchKeyword, setSearchKeyword] = useState(""); + const [pendingItems, setPendingItems] = useState([]); + + const fetchWarehouses = useCallback(async () => { + try { + const res = await apiClient.get("/outbound/warehouses"); + setWarehouses(res.data?.data || []); + } catch { /* */ } + }, []); + + const fetchStock = useCallback(async () => { + if (!fromWarehouse) { setStockItems([]); return; } + setLoading(true); + try { + const res = await apiClient.get("/data/inventory_stock", { + params: { pageSize: "500", filters: JSON.stringify({ warehouse_code: fromWarehouse }) }, + }); + const data = res.data?.data?.data ?? res.data?.data ?? []; + setStockItems(Array.isArray(data) ? data.filter((d: StockItem) => parseFloat(d.current_qty || "0") > 0) : []); + } catch { + setStockItems([]); + } finally { + setLoading(false); + } + }, [fromWarehouse]); + + useEffect(() => { fetchWarehouses(); }, [fetchWarehouses]); + useEffect(() => { fetchStock(); }, [fetchStock]); + + const filtered = stockItems.filter((item) => { + if (!searchKeyword) return true; + const kw = searchKeyword.toLowerCase(); + return (item.item_code || "").toLowerCase().includes(kw) || (item.item_name || "").toLowerCase().includes(kw); + }); + + const addToPending = (stock: StockItem) => { + if (!toWarehouse) { alert("도착 창고를 먼저 선택하세요."); return; } + if (pendingItems.find((p) => p.stock.id === stock.id)) return; + const qty = parseFloat(stock.current_qty || "0"); + setPendingItems((prev) => [...prev, { stock, moveQty: qty, toWarehouse }]); + }; + + const removePending = (id: string) => { + setPendingItems((prev) => prev.filter((p) => p.stock.id !== id)); + }; + + const fromWh = warehouses.find((w) => w.warehouse_code === fromWarehouse); + const toWh = warehouses.find((w) => w.warehouse_code === toWarehouse); + + return ( + +
+ {/* Header */} +
+ +
+

📦 재고 이동

+

창고 간 재고를 이동합니다

+
+
+ + {/* 좌우 분할 */} +
+ {/* ===== 왼쪽: 출발 창고 + 품목 선택 ===== */} +
+ {/* 출발 창고 헤더 */} +
+
+ 📤 출발 창고 + FROM +
+
+ {warehouses.map((wh) => ( + + ))} +
+
+ + {/* 검색 */} + {fromWarehouse && ( +
+
+ setSearchKeyword(e.target.value)} + className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 text-sm focus:outline-none focus:border-blue-400" + /> + +
+
+ )} + + {/* 품목 리스트 */} +
+ {!fromWarehouse ? ( +
+ 📦 +

출발 창고를 선택하세요

+
+ ) : loading ? ( +
+
+
+ ) : filtered.length === 0 ? ( +
+

해당 창고에 재고가 없습니다

+
+ ) : ( +
+ {filtered.map((item) => { + const isPending = pendingItems.some((p) => p.stock.id === item.id); + return ( + + ); + })} +
+ )} +
+
+ + {/* ===== 오른쪽: 도착 창고 + 이동 대기열 ===== */} +
+ {/* 도착 창고 헤더 */} +
+
+ 📥 도착 창고 + TO +
+
+ {warehouses + .filter((wh) => wh.warehouse_code !== fromWarehouse) + .map((wh) => ( + + ))} +
+
+ + {/* 이동 방향 표시 */} + {fromWh && toWh && ( +
+ {fromWh.warehouse_name} + + {toWh.warehouse_name} +
+ )} + + {/* 이동 대기 목록 */} +
+ {pendingItems.length === 0 ? ( +
+ 📋 +

왼쪽에서 품목을 선택하세요

+

선택한 품목이 여기에 표시됩니다

+
+ ) : ( +
+ {pendingItems.map((p) => ( +
+
+

{p.stock.item_name || p.stock.item_code}

+ +
+

{p.stock.item_code}

+
+ + {p.moveQty.toLocaleString()} EA + + + {p.stock.warehouse_code} → {p.toWarehouse} + +
+
+ ))} +
+ )} +
+ + {/* 하단 확정 바 */} +
+
+ 이동 대기: {pendingItems.length}건 +
+ +
+
+
+
+ + ); +} diff --git a/frontend/components/pop/hardcoded/inventory/InventoryTransfer.tsx b/frontend/components/pop/hardcoded/inventory/InventoryTransfer.tsx new file mode 100644 index 00000000..427b36ae --- /dev/null +++ b/frontend/components/pop/hardcoded/inventory/InventoryTransfer.tsx @@ -0,0 +1,252 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { apiClient } from "@/lib/api/client"; +import { PopShell } from "../PopShell"; + +interface Warehouse { + id: string; + warehouse_code: string; + warehouse_name: string; + warehouse_type?: string; +} + +interface StockItem { + id: string; + item_code: string; + item_name?: string; + warehouse_code: string; + location_code?: string; + current_qty: string; + unit?: string; +} + +interface SelectedItem { + stock: StockItem; + adjustQty: string; + type: "confirm" | "adjust"; +} + +export function InventoryTransfer() { + const router = useRouter(); + const [warehouses, setWarehouses] = useState([]); + const [selectedWarehouse, setSelectedWarehouse] = useState("all"); + const [stockItems, setStockItems] = useState([]); + const [loading, setLoading] = useState(true); + const [searchKeyword, setSearchKeyword] = useState(""); + const [selectedItems, setSelectedItems] = useState([]); + + const fetchWarehouses = useCallback(async () => { + try { + const res = await apiClient.get("/outbound/warehouses"); + setWarehouses(res.data?.data || []); + } catch { /* */ } + }, []); + + const fetchStock = useCallback(async () => { + setLoading(true); + try { + const params: Record = { pageSize: "500" }; + if (selectedWarehouse !== "all") { + params.filters = JSON.stringify({ warehouse_code: selectedWarehouse }); + } + const res = await apiClient.get("/data/inventory_stock", { params }); + const data = res.data?.data?.data ?? res.data?.data ?? []; + setStockItems(Array.isArray(data) ? data.filter((d: StockItem) => parseFloat(d.current_qty || "0") > 0) : []); + } catch { + setStockItems([]); + } finally { + setLoading(false); + } + }, [selectedWarehouse]); + + useEffect(() => { fetchWarehouses(); }, [fetchWarehouses]); + useEffect(() => { fetchStock(); }, [fetchStock]); + + const filtered = stockItems.filter((item) => { + if (!searchKeyword) return true; + const kw = searchKeyword.toLowerCase(); + return (item.item_code || "").toLowerCase().includes(kw) || (item.item_name || "").toLowerCase().includes(kw); + }); + + const addItem = (stock: StockItem) => { + if (selectedItems.find((s) => s.stock.id === stock.id)) return; + setSelectedItems((prev) => [...prev, { stock, adjustQty: "", type: "confirm" }]); + }; + + const removeItem = (id: string) => { + setSelectedItems((prev) => prev.filter((s) => s.stock.id !== id)); + }; + + const confirmCount = selectedItems.filter((s) => s.type === "confirm").length; + const adjustCount = selectedItems.filter((s) => s.type === "adjust").length; + + return ( + +
+ {/* Header */} +
+
+ +

📦 재고조정

+
+
+ + {/* Main — 2단 레이아웃 */} +
+ {/* 왼쪽: 제품 선택 */} +
+
+
+

📦 제품 선택

+ +
+ + {/* 창고 탭 */} +
+ + {warehouses.map((wh) => ( + + ))} +
+ + {/* 검색 */} +
+ setSearchKeyword(e.target.value)} + className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 text-sm focus:outline-none focus:border-amber-400" + /> + +
+
+ + {/* 품목 리스트 */} +
+ {loading ? ( +
+
+
+ ) : filtered.length === 0 ? ( +
+ 📦 +

해당 창고에 재고가 없습니다

+
+ ) : ( +
+ {filtered.map((item) => ( +
+
+
📦
+
+

+ {item.item_name || item.item_code} + {item.item_name && ({item.item_code})} +

+

{item.warehouse_code}{item.location_code ? ` · ${item.location_code}` : ""}

+
+
+
+
+

{parseFloat(item.current_qty || "0").toLocaleString()}

+

{item.location_code || item.warehouse_code}

+
+ +
+
+ ))} +
+ )} +
+
+ + {/* 오른쪽: 처리 결과 */} +
+
+

📋 처리 결과

+ + {selectedItems.length}건 + +
+ +
+ {selectedItems.length === 0 ? ( +
+ 📋 +

제품을 스캔/선택하여 처리하세요

+
+ ) : ( +
+ {selectedItems.map((sel) => ( +
+
+

{sel.stock.item_name || sel.stock.item_code}

+ +
+

현재 재고: {parseFloat(sel.stock.current_qty || "0").toLocaleString()}

+ +
+ ))} +
+ )} +
+ + {/* Footer */} +
+
+ 확인 {confirmCount} + 조정 {adjustCount} +
+
+ + +
+
+
+
+
+ + ); +} diff --git a/frontend/components/pop/hardcoded/production/ProcessWork.tsx b/frontend/components/pop/hardcoded/production/ProcessWork.tsx index 6f603e43..748627a0 100644 --- a/frontend/components/pop/hardcoded/production/ProcessWork.tsx +++ b/frontend/components/pop/hardcoded/production/ProcessWork.tsx @@ -1286,26 +1286,6 @@ export function ProcessWork({ processId }: ProcessWorkProps) { )} -
-
- - - -
- - 실적 - -
)} @@ -2779,18 +2708,16 @@ function MaterialQtyInputRow({ }) { const [open, setOpen] = useState(false); return ( -
+
- {material.unit} setOpen(false)} @@ -2915,50 +2842,44 @@ function MaterialInputSection({ processId }: { processId: string }) { } return ( -
- {/* BOM 기준 자재 목록 */} -
-

- BOM 자재 목록 -

+
+ {/* BOM 기준 자재 목록 — 컴팩트 */} +
+
+

BOM 자재 목록

+ {bomMaterials.length}건 +
{bomMaterials.length === 0 ? (

BOM 자재 정보가 없습니다

) : ( -
- {bomMaterials.map((m) => ( -
-
-
-

- {m.child_item_name} -

-

{m.child_item_code}

-
-
-

소요량

-

- {m.required_qty} {m.unit} -

+
+
+ {bomMaterials.map((m) => ( +
+ {/* 자재명(코드) + 소요량 */} +
+ {m.child_item_name} + ({m.child_item_code}) + 소요 {m.required_qty}
+ {/* 입력 버튼 + 단위 */} + + setInputValues((prev) => ({ ...prev, [m.id]: v })) + } + /> + {m.unit}
- - setInputValues((prev) => ({ ...prev, [m.id]: v })) - } - /> -
- ))} + ))} +