jskim-node #16

Merged
jskim merged 19 commits from jskim-node into main 2026-04-09 06:59:07 +00:00
7 changed files with 784 additions and 252 deletions
Showing only changes of commit 4424071e47 - Show all commits

View File

@@ -118,11 +118,20 @@ interface Equipment {
interface WorkInstruction {
id: string;
wi_id?: string;
instruction_number: string;
work_instruction_no?: string;
item_name: string;
equipment_id: string;
worker_name: string;
status: string;
progress_status?: string;
}
interface ProcessRow {
wo_id: string;
status: string; // acceptable / in_progress / completed
parent_process_id?: string | null;
}
/* ───── 컴포넌트 ───── */
@@ -135,6 +144,7 @@ export default function EquipmentMonitoringPage() {
const [equipments, setEquipments] = useState<Equipment[]>([]);
const [workInstructions, setWorkInstructions] = useState<WorkInstruction[]>([]);
const [processRows, setProcessRows] = useState<ProcessRow[]>([]);
const [loading, setLoading] = useState(true);
const [currentTime, setCurrentTime] = useState(new Date());
const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh);
@@ -156,20 +166,28 @@ export default function EquipmentMonitoringPage() {
const fetchData = useCallback(async () => {
try {
setLoading(true);
const [equipRes, wiRes] = await Promise.all([
const [equipRes, wiRes, procRes] = await Promise.all([
apiClient.post("/table-management/tables/equipment_mng/data", {
autoFilter: true,
page: 1,
size: 500,
}),
apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })),
apiClient.post("/table-management/tables/work_order_process/data", {
page: 1,
size: 2000,
autoFilter: true,
}).catch(() => ({ data: { data: { data: [] } } })),
]);
const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? [];
const eqRows: Equipment[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? [];
setEquipments(eqRows);
const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? [];
setWorkInstructions(wiRows);
const pRows: ProcessRow[] = procRes.data?.data?.data ?? procRes.data?.data?.rows ?? [];
setProcessRows(pRows);
} catch (err) {
console.error("설비 모니터링 데이터 조회 실패:", err);
} finally {
@@ -189,49 +207,107 @@ export default function EquipmentMonitoringPage() {
return () => clearInterval(interval);
}, [fetchData, settings.refreshInterval]);
/* ── 요약 통계 ── */
const stats = useMemo(() => {
const counts: Record<OperationStatus, number> = {
running: 0,
idle: 0,
maintenance: 0,
off: 0,
unknown: 0,
};
equipments.forEach((eq) => {
const s = resolveStatus(eq.operation_status);
counts[s]++;
});
return { total: equipments.length, ...counts };
}, [equipments]);
/* ── 필터된 설비 ── */
const filteredEquipments = useMemo(() => {
if (filterStatus === "all") return equipments;
return equipments.filter((eq) => resolveStatus(eq.operation_status) === filterStatus);
}, [equipments, filterStatus]);
/* ── 설비별 작업지시 맵 ── */
const wiMap = useMemo(() => {
const map: Record<string, WorkInstruction[]> = {};
workInstructions.forEach((wi) => {
if (wi.equipment_id) {
if (!map[wi.equipment_id]) map[wi.equipment_id] = [];
map[wi.equipment_id].push(wi);
const eqId = wi.equipment_id;
if (eqId) {
if (!map[eqId]) map[eqId] = [];
map[eqId].push(wi);
}
});
return map;
}, [workInstructions]);
/* ── 가동률 (모킹 — 센서 미연동) ── */
const getUtilization = (eq: Equipment): number | null => {
const s = resolveStatus(eq.operation_status);
if (s === "running") return 75 + Math.floor(Math.random() * 20); // 75~94
if (s === "idle") return 20 + Math.floor(Math.random() * 30); // 20~49
if (s === "maintenance") return 0;
if (s === "off") return 0;
return null;
};
/* ── 설비 상태 자동 판단: 공정 데이터 기반 ── */
const inferredStatus = useMemo(() => {
// 작업지시 ID → 설비 ID 매핑
const wiToEquip: Record<string, string> = {};
workInstructions.forEach((wi) => {
const wiId = wi.wi_id || wi.id;
if (wi.equipment_id) wiToEquip[wiId] = wi.equipment_id;
});
// 설비별 공정 상태 집계
const equipProcessStatus: Record<string, Set<string>> = {};
processRows.forEach((p) => {
const eqId = wiToEquip[p.wo_id];
if (!eqId) return;
if (!equipProcessStatus[eqId]) equipProcessStatus[eqId] = new Set();
equipProcessStatus[eqId].add(p.status);
});
// 설비별 상태 판단
const result: Record<string, OperationStatus> = {};
equipments.forEach((eq) => {
const dbStatus = resolveStatus(eq.operation_status);
// DB에 점검/수리가 명시되어 있으면 그대로 사용
if (dbStatus === "maintenance") {
result[eq.id] = "maintenance";
return;
}
const statuses = equipProcessStatus[eq.id];
if (statuses) {
if (statuses.has("in_progress")) {
result[eq.id] = "running"; // 진행중 공정 있음 → 가동중
} else if (statuses.has("acceptable")) {
result[eq.id] = "idle"; // 접수 대기 공정 있음 → 대기
} else {
// 전부 completed → DB 상태 사용
result[eq.id] = dbStatus !== "unknown" ? dbStatus : "idle";
}
} else {
// 공정 데이터 없음 → 작업지시 여부로 판단
const eqWIs = wiMap[eq.id];
if (eqWIs && eqWIs.length > 0) {
result[eq.id] = "idle"; // 작업지시 배정됨 → 대기
} else {
result[eq.id] = dbStatus !== "unknown" ? dbStatus : "off";
}
}
});
return result;
}, [equipments, workInstructions, processRows, wiMap]);
/* ── 요약 통계 (추론 상태 기반) ── */
const stats = useMemo(() => {
const counts: Record<OperationStatus, number> = { running: 0, idle: 0, maintenance: 0, off: 0, unknown: 0 };
equipments.forEach((eq) => {
counts[inferredStatus[eq.id] ?? "unknown"]++;
});
return { total: equipments.length, ...counts };
}, [equipments, inferredStatus]);
/* ── 필터된 설비 ── */
const filteredEquipments = useMemo(() => {
if (filterStatus === "all") return equipments;
return equipments.filter((eq) => (inferredStatus[eq.id] ?? "unknown") === filterStatus);
}, [equipments, filterStatus, inferredStatus]);
/* ── 가동률 (센서 미연동 — 상태 기반 고정값) ── */
const utilizationMap = useMemo(() => {
const map: Record<string, number | null> = {};
// 설비 ID를 해시하여 상태별 고정 범위 내 값 생성 (리렌더링해도 안 변함)
const hash = (id: string) => {
let h = 0;
for (let i = 0; i < id.length; i++) h = ((h << 5) - h + id.charCodeAt(i)) | 0;
return Math.abs(h);
};
equipments.forEach((eq) => {
const s = inferredStatus[eq.id] ?? "unknown";
const h = hash(eq.id);
if (s === "running") map[eq.id] = 75 + (h % 20); // 75~94 고정
else if (s === "idle") map[eq.id] = 20 + (h % 30); // 20~49 고정
else if (s === "maintenance") map[eq.id] = 0;
else if (s === "off") map[eq.id] = 0;
else map[eq.id] = null;
});
return map;
}, [equipments, inferredStatus]);
const getUtilization = (eq: Equipment): number | null => utilizationMap[eq.id] ?? null;
/* ── 요약 카드 배열 ── */
const summaryCards: {
@@ -430,7 +506,7 @@ export default function EquipmentMonitoringPage() {
{filteredEquipments.length > 0 && (
<div className="grid gap-4" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(340px, 1fr))" }}>
{filteredEquipments.map((eq) => {
const status = resolveStatus(eq.operation_status);
const status = inferredStatus[eq.id] ?? "unknown";
const cfg = STATUS_MAP[status];
const utilization = getUtilization(eq);
const eqWIs = wiMap[eq.id] ?? [];

View File

@@ -118,11 +118,20 @@ interface Equipment {
interface WorkInstruction {
id: string;
wi_id?: string;
instruction_number: string;
work_instruction_no?: string;
item_name: string;
equipment_id: string;
worker_name: string;
status: string;
progress_status?: string;
}
interface ProcessRow {
wo_id: string;
status: string; // acceptable / in_progress / completed
parent_process_id?: string | null;
}
/* ───── 컴포넌트 ───── */
@@ -135,6 +144,7 @@ export default function EquipmentMonitoringPage() {
const [equipments, setEquipments] = useState<Equipment[]>([]);
const [workInstructions, setWorkInstructions] = useState<WorkInstruction[]>([]);
const [processRows, setProcessRows] = useState<ProcessRow[]>([]);
const [loading, setLoading] = useState(true);
const [currentTime, setCurrentTime] = useState(new Date());
const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh);
@@ -156,20 +166,28 @@ export default function EquipmentMonitoringPage() {
const fetchData = useCallback(async () => {
try {
setLoading(true);
const [equipRes, wiRes] = await Promise.all([
const [equipRes, wiRes, procRes] = await Promise.all([
apiClient.post("/table-management/tables/equipment_mng/data", {
autoFilter: true,
page: 1,
size: 500,
}),
apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })),
apiClient.post("/table-management/tables/work_order_process/data", {
page: 1,
size: 2000,
autoFilter: true,
}).catch(() => ({ data: { data: { data: [] } } })),
]);
const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? [];
const eqRows: Equipment[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? [];
setEquipments(eqRows);
const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? [];
setWorkInstructions(wiRows);
const pRows: ProcessRow[] = procRes.data?.data?.data ?? procRes.data?.data?.rows ?? [];
setProcessRows(pRows);
} catch (err) {
console.error("설비 모니터링 데이터 조회 실패:", err);
} finally {
@@ -189,49 +207,107 @@ export default function EquipmentMonitoringPage() {
return () => clearInterval(interval);
}, [fetchData, settings.refreshInterval]);
/* ── 요약 통계 ── */
const stats = useMemo(() => {
const counts: Record<OperationStatus, number> = {
running: 0,
idle: 0,
maintenance: 0,
off: 0,
unknown: 0,
};
equipments.forEach((eq) => {
const s = resolveStatus(eq.operation_status);
counts[s]++;
});
return { total: equipments.length, ...counts };
}, [equipments]);
/* ── 필터된 설비 ── */
const filteredEquipments = useMemo(() => {
if (filterStatus === "all") return equipments;
return equipments.filter((eq) => resolveStatus(eq.operation_status) === filterStatus);
}, [equipments, filterStatus]);
/* ── 설비별 작업지시 맵 ── */
const wiMap = useMemo(() => {
const map: Record<string, WorkInstruction[]> = {};
workInstructions.forEach((wi) => {
if (wi.equipment_id) {
if (!map[wi.equipment_id]) map[wi.equipment_id] = [];
map[wi.equipment_id].push(wi);
const eqId = wi.equipment_id;
if (eqId) {
if (!map[eqId]) map[eqId] = [];
map[eqId].push(wi);
}
});
return map;
}, [workInstructions]);
/* ── 가동률 (모킹 — 센서 미연동) ── */
const getUtilization = (eq: Equipment): number | null => {
const s = resolveStatus(eq.operation_status);
if (s === "running") return 75 + Math.floor(Math.random() * 20); // 75~94
if (s === "idle") return 20 + Math.floor(Math.random() * 30); // 20~49
if (s === "maintenance") return 0;
if (s === "off") return 0;
return null;
};
/* ── 설비 상태 자동 판단: 공정 데이터 기반 ── */
const inferredStatus = useMemo(() => {
// 작업지시 ID → 설비 ID 매핑
const wiToEquip: Record<string, string> = {};
workInstructions.forEach((wi) => {
const wiId = wi.wi_id || wi.id;
if (wi.equipment_id) wiToEquip[wiId] = wi.equipment_id;
});
// 설비별 공정 상태 집계
const equipProcessStatus: Record<string, Set<string>> = {};
processRows.forEach((p) => {
const eqId = wiToEquip[p.wo_id];
if (!eqId) return;
if (!equipProcessStatus[eqId]) equipProcessStatus[eqId] = new Set();
equipProcessStatus[eqId].add(p.status);
});
// 설비별 상태 판단
const result: Record<string, OperationStatus> = {};
equipments.forEach((eq) => {
const dbStatus = resolveStatus(eq.operation_status);
// DB에 점검/수리가 명시되어 있으면 그대로 사용
if (dbStatus === "maintenance") {
result[eq.id] = "maintenance";
return;
}
const statuses = equipProcessStatus[eq.id];
if (statuses) {
if (statuses.has("in_progress")) {
result[eq.id] = "running"; // 진행중 공정 있음 → 가동중
} else if (statuses.has("acceptable")) {
result[eq.id] = "idle"; // 접수 대기 공정 있음 → 대기
} else {
// 전부 completed → DB 상태 사용
result[eq.id] = dbStatus !== "unknown" ? dbStatus : "idle";
}
} else {
// 공정 데이터 없음 → 작업지시 여부로 판단
const eqWIs = wiMap[eq.id];
if (eqWIs && eqWIs.length > 0) {
result[eq.id] = "idle"; // 작업지시 배정됨 → 대기
} else {
result[eq.id] = dbStatus !== "unknown" ? dbStatus : "off";
}
}
});
return result;
}, [equipments, workInstructions, processRows, wiMap]);
/* ── 요약 통계 (추론 상태 기반) ── */
const stats = useMemo(() => {
const counts: Record<OperationStatus, number> = { running: 0, idle: 0, maintenance: 0, off: 0, unknown: 0 };
equipments.forEach((eq) => {
counts[inferredStatus[eq.id] ?? "unknown"]++;
});
return { total: equipments.length, ...counts };
}, [equipments, inferredStatus]);
/* ── 필터된 설비 ── */
const filteredEquipments = useMemo(() => {
if (filterStatus === "all") return equipments;
return equipments.filter((eq) => (inferredStatus[eq.id] ?? "unknown") === filterStatus);
}, [equipments, filterStatus, inferredStatus]);
/* ── 가동률 (센서 미연동 — 상태 기반 고정값) ── */
const utilizationMap = useMemo(() => {
const map: Record<string, number | null> = {};
// 설비 ID를 해시하여 상태별 고정 범위 내 값 생성 (리렌더링해도 안 변함)
const hash = (id: string) => {
let h = 0;
for (let i = 0; i < id.length; i++) h = ((h << 5) - h + id.charCodeAt(i)) | 0;
return Math.abs(h);
};
equipments.forEach((eq) => {
const s = inferredStatus[eq.id] ?? "unknown";
const h = hash(eq.id);
if (s === "running") map[eq.id] = 75 + (h % 20); // 75~94 고정
else if (s === "idle") map[eq.id] = 20 + (h % 30); // 20~49 고정
else if (s === "maintenance") map[eq.id] = 0;
else if (s === "off") map[eq.id] = 0;
else map[eq.id] = null;
});
return map;
}, [equipments, inferredStatus]);
const getUtilization = (eq: Equipment): number | null => utilizationMap[eq.id] ?? null;
/* ── 요약 카드 배열 ── */
const summaryCards: {
@@ -430,7 +506,7 @@ export default function EquipmentMonitoringPage() {
{filteredEquipments.length > 0 && (
<div className="grid gap-4" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(340px, 1fr))" }}>
{filteredEquipments.map((eq) => {
const status = resolveStatus(eq.operation_status);
const status = inferredStatus[eq.id] ?? "unknown";
const cfg = STATUS_MAP[status];
const utilization = getUtilization(eq);
const eqWIs = wiMap[eq.id] ?? [];

View File

@@ -118,11 +118,20 @@ interface Equipment {
interface WorkInstruction {
id: string;
wi_id?: string;
instruction_number: string;
work_instruction_no?: string;
item_name: string;
equipment_id: string;
worker_name: string;
status: string;
progress_status?: string;
}
interface ProcessRow {
wo_id: string;
status: string; // acceptable / in_progress / completed
parent_process_id?: string | null;
}
/* ───── 컴포넌트 ───── */
@@ -135,6 +144,7 @@ export default function EquipmentMonitoringPage() {
const [equipments, setEquipments] = useState<Equipment[]>([]);
const [workInstructions, setWorkInstructions] = useState<WorkInstruction[]>([]);
const [processRows, setProcessRows] = useState<ProcessRow[]>([]);
const [loading, setLoading] = useState(true);
const [currentTime, setCurrentTime] = useState(new Date());
const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh);
@@ -156,20 +166,28 @@ export default function EquipmentMonitoringPage() {
const fetchData = useCallback(async () => {
try {
setLoading(true);
const [equipRes, wiRes] = await Promise.all([
const [equipRes, wiRes, procRes] = await Promise.all([
apiClient.post("/table-management/tables/equipment_mng/data", {
autoFilter: true,
page: 1,
size: 500,
}),
apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })),
apiClient.post("/table-management/tables/work_order_process/data", {
page: 1,
size: 2000,
autoFilter: true,
}).catch(() => ({ data: { data: { data: [] } } })),
]);
const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? [];
const eqRows: Equipment[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? [];
setEquipments(eqRows);
const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? [];
setWorkInstructions(wiRows);
const pRows: ProcessRow[] = procRes.data?.data?.data ?? procRes.data?.data?.rows ?? [];
setProcessRows(pRows);
} catch (err) {
console.error("설비 모니터링 데이터 조회 실패:", err);
} finally {
@@ -189,49 +207,107 @@ export default function EquipmentMonitoringPage() {
return () => clearInterval(interval);
}, [fetchData, settings.refreshInterval]);
/* ── 요약 통계 ── */
const stats = useMemo(() => {
const counts: Record<OperationStatus, number> = {
running: 0,
idle: 0,
maintenance: 0,
off: 0,
unknown: 0,
};
equipments.forEach((eq) => {
const s = resolveStatus(eq.operation_status);
counts[s]++;
});
return { total: equipments.length, ...counts };
}, [equipments]);
/* ── 필터된 설비 ── */
const filteredEquipments = useMemo(() => {
if (filterStatus === "all") return equipments;
return equipments.filter((eq) => resolveStatus(eq.operation_status) === filterStatus);
}, [equipments, filterStatus]);
/* ── 설비별 작업지시 맵 ── */
const wiMap = useMemo(() => {
const map: Record<string, WorkInstruction[]> = {};
workInstructions.forEach((wi) => {
if (wi.equipment_id) {
if (!map[wi.equipment_id]) map[wi.equipment_id] = [];
map[wi.equipment_id].push(wi);
const eqId = wi.equipment_id;
if (eqId) {
if (!map[eqId]) map[eqId] = [];
map[eqId].push(wi);
}
});
return map;
}, [workInstructions]);
/* ── 가동률 (모킹 — 센서 미연동) ── */
const getUtilization = (eq: Equipment): number | null => {
const s = resolveStatus(eq.operation_status);
if (s === "running") return 75 + Math.floor(Math.random() * 20); // 75~94
if (s === "idle") return 20 + Math.floor(Math.random() * 30); // 20~49
if (s === "maintenance") return 0;
if (s === "off") return 0;
return null;
};
/* ── 설비 상태 자동 판단: 공정 데이터 기반 ── */
const inferredStatus = useMemo(() => {
// 작업지시 ID → 설비 ID 매핑
const wiToEquip: Record<string, string> = {};
workInstructions.forEach((wi) => {
const wiId = wi.wi_id || wi.id;
if (wi.equipment_id) wiToEquip[wiId] = wi.equipment_id;
});
// 설비별 공정 상태 집계
const equipProcessStatus: Record<string, Set<string>> = {};
processRows.forEach((p) => {
const eqId = wiToEquip[p.wo_id];
if (!eqId) return;
if (!equipProcessStatus[eqId]) equipProcessStatus[eqId] = new Set();
equipProcessStatus[eqId].add(p.status);
});
// 설비별 상태 판단
const result: Record<string, OperationStatus> = {};
equipments.forEach((eq) => {
const dbStatus = resolveStatus(eq.operation_status);
// DB에 점검/수리가 명시되어 있으면 그대로 사용
if (dbStatus === "maintenance") {
result[eq.id] = "maintenance";
return;
}
const statuses = equipProcessStatus[eq.id];
if (statuses) {
if (statuses.has("in_progress")) {
result[eq.id] = "running"; // 진행중 공정 있음 → 가동중
} else if (statuses.has("acceptable")) {
result[eq.id] = "idle"; // 접수 대기 공정 있음 → 대기
} else {
// 전부 completed → DB 상태 사용
result[eq.id] = dbStatus !== "unknown" ? dbStatus : "idle";
}
} else {
// 공정 데이터 없음 → 작업지시 여부로 판단
const eqWIs = wiMap[eq.id];
if (eqWIs && eqWIs.length > 0) {
result[eq.id] = "idle"; // 작업지시 배정됨 → 대기
} else {
result[eq.id] = dbStatus !== "unknown" ? dbStatus : "off";
}
}
});
return result;
}, [equipments, workInstructions, processRows, wiMap]);
/* ── 요약 통계 (추론 상태 기반) ── */
const stats = useMemo(() => {
const counts: Record<OperationStatus, number> = { running: 0, idle: 0, maintenance: 0, off: 0, unknown: 0 };
equipments.forEach((eq) => {
counts[inferredStatus[eq.id] ?? "unknown"]++;
});
return { total: equipments.length, ...counts };
}, [equipments, inferredStatus]);
/* ── 필터된 설비 ── */
const filteredEquipments = useMemo(() => {
if (filterStatus === "all") return equipments;
return equipments.filter((eq) => (inferredStatus[eq.id] ?? "unknown") === filterStatus);
}, [equipments, filterStatus, inferredStatus]);
/* ── 가동률 (센서 미연동 — 상태 기반 고정값) ── */
const utilizationMap = useMemo(() => {
const map: Record<string, number | null> = {};
// 설비 ID를 해시하여 상태별 고정 범위 내 값 생성 (리렌더링해도 안 변함)
const hash = (id: string) => {
let h = 0;
for (let i = 0; i < id.length; i++) h = ((h << 5) - h + id.charCodeAt(i)) | 0;
return Math.abs(h);
};
equipments.forEach((eq) => {
const s = inferredStatus[eq.id] ?? "unknown";
const h = hash(eq.id);
if (s === "running") map[eq.id] = 75 + (h % 20); // 75~94 고정
else if (s === "idle") map[eq.id] = 20 + (h % 30); // 20~49 고정
else if (s === "maintenance") map[eq.id] = 0;
else if (s === "off") map[eq.id] = 0;
else map[eq.id] = null;
});
return map;
}, [equipments, inferredStatus]);
const getUtilization = (eq: Equipment): number | null => utilizationMap[eq.id] ?? null;
/* ── 요약 카드 배열 ── */
const summaryCards: {
@@ -430,7 +506,7 @@ export default function EquipmentMonitoringPage() {
{filteredEquipments.length > 0 && (
<div className="grid gap-4" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(340px, 1fr))" }}>
{filteredEquipments.map((eq) => {
const status = resolveStatus(eq.operation_status);
const status = inferredStatus[eq.id] ?? "unknown";
const cfg = STATUS_MAP[status];
const utilization = getUtilization(eq);
const eqWIs = wiMap[eq.id] ?? [];

View File

@@ -118,11 +118,20 @@ interface Equipment {
interface WorkInstruction {
id: string;
wi_id?: string;
instruction_number: string;
work_instruction_no?: string;
item_name: string;
equipment_id: string;
worker_name: string;
status: string;
progress_status?: string;
}
interface ProcessRow {
wo_id: string;
status: string; // acceptable / in_progress / completed
parent_process_id?: string | null;
}
/* ───── 컴포넌트 ───── */
@@ -135,6 +144,7 @@ export default function EquipmentMonitoringPage() {
const [equipments, setEquipments] = useState<Equipment[]>([]);
const [workInstructions, setWorkInstructions] = useState<WorkInstruction[]>([]);
const [processRows, setProcessRows] = useState<ProcessRow[]>([]);
const [loading, setLoading] = useState(true);
const [currentTime, setCurrentTime] = useState(new Date());
const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh);
@@ -156,20 +166,28 @@ export default function EquipmentMonitoringPage() {
const fetchData = useCallback(async () => {
try {
setLoading(true);
const [equipRes, wiRes] = await Promise.all([
const [equipRes, wiRes, procRes] = await Promise.all([
apiClient.post("/table-management/tables/equipment_mng/data", {
autoFilter: true,
page: 1,
size: 500,
}),
apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })),
apiClient.post("/table-management/tables/work_order_process/data", {
page: 1,
size: 2000,
autoFilter: true,
}).catch(() => ({ data: { data: { data: [] } } })),
]);
const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? [];
const eqRows: Equipment[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? [];
setEquipments(eqRows);
const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? [];
setWorkInstructions(wiRows);
const pRows: ProcessRow[] = procRes.data?.data?.data ?? procRes.data?.data?.rows ?? [];
setProcessRows(pRows);
} catch (err) {
console.error("설비 모니터링 데이터 조회 실패:", err);
} finally {
@@ -189,49 +207,107 @@ export default function EquipmentMonitoringPage() {
return () => clearInterval(interval);
}, [fetchData, settings.refreshInterval]);
/* ── 요약 통계 ── */
const stats = useMemo(() => {
const counts: Record<OperationStatus, number> = {
running: 0,
idle: 0,
maintenance: 0,
off: 0,
unknown: 0,
};
equipments.forEach((eq) => {
const s = resolveStatus(eq.operation_status);
counts[s]++;
});
return { total: equipments.length, ...counts };
}, [equipments]);
/* ── 필터된 설비 ── */
const filteredEquipments = useMemo(() => {
if (filterStatus === "all") return equipments;
return equipments.filter((eq) => resolveStatus(eq.operation_status) === filterStatus);
}, [equipments, filterStatus]);
/* ── 설비별 작업지시 맵 ── */
const wiMap = useMemo(() => {
const map: Record<string, WorkInstruction[]> = {};
workInstructions.forEach((wi) => {
if (wi.equipment_id) {
if (!map[wi.equipment_id]) map[wi.equipment_id] = [];
map[wi.equipment_id].push(wi);
const eqId = wi.equipment_id;
if (eqId) {
if (!map[eqId]) map[eqId] = [];
map[eqId].push(wi);
}
});
return map;
}, [workInstructions]);
/* ── 가동률 (모킹 — 센서 미연동) ── */
const getUtilization = (eq: Equipment): number | null => {
const s = resolveStatus(eq.operation_status);
if (s === "running") return 75 + Math.floor(Math.random() * 20); // 75~94
if (s === "idle") return 20 + Math.floor(Math.random() * 30); // 20~49
if (s === "maintenance") return 0;
if (s === "off") return 0;
return null;
};
/* ── 설비 상태 자동 판단: 공정 데이터 기반 ── */
const inferredStatus = useMemo(() => {
// 작업지시 ID → 설비 ID 매핑
const wiToEquip: Record<string, string> = {};
workInstructions.forEach((wi) => {
const wiId = wi.wi_id || wi.id;
if (wi.equipment_id) wiToEquip[wiId] = wi.equipment_id;
});
// 설비별 공정 상태 집계
const equipProcessStatus: Record<string, Set<string>> = {};
processRows.forEach((p) => {
const eqId = wiToEquip[p.wo_id];
if (!eqId) return;
if (!equipProcessStatus[eqId]) equipProcessStatus[eqId] = new Set();
equipProcessStatus[eqId].add(p.status);
});
// 설비별 상태 판단
const result: Record<string, OperationStatus> = {};
equipments.forEach((eq) => {
const dbStatus = resolveStatus(eq.operation_status);
// DB에 점검/수리가 명시되어 있으면 그대로 사용
if (dbStatus === "maintenance") {
result[eq.id] = "maintenance";
return;
}
const statuses = equipProcessStatus[eq.id];
if (statuses) {
if (statuses.has("in_progress")) {
result[eq.id] = "running"; // 진행중 공정 있음 → 가동중
} else if (statuses.has("acceptable")) {
result[eq.id] = "idle"; // 접수 대기 공정 있음 → 대기
} else {
// 전부 completed → DB 상태 사용
result[eq.id] = dbStatus !== "unknown" ? dbStatus : "idle";
}
} else {
// 공정 데이터 없음 → 작업지시 여부로 판단
const eqWIs = wiMap[eq.id];
if (eqWIs && eqWIs.length > 0) {
result[eq.id] = "idle"; // 작업지시 배정됨 → 대기
} else {
result[eq.id] = dbStatus !== "unknown" ? dbStatus : "off";
}
}
});
return result;
}, [equipments, workInstructions, processRows, wiMap]);
/* ── 요약 통계 (추론 상태 기반) ── */
const stats = useMemo(() => {
const counts: Record<OperationStatus, number> = { running: 0, idle: 0, maintenance: 0, off: 0, unknown: 0 };
equipments.forEach((eq) => {
counts[inferredStatus[eq.id] ?? "unknown"]++;
});
return { total: equipments.length, ...counts };
}, [equipments, inferredStatus]);
/* ── 필터된 설비 ── */
const filteredEquipments = useMemo(() => {
if (filterStatus === "all") return equipments;
return equipments.filter((eq) => (inferredStatus[eq.id] ?? "unknown") === filterStatus);
}, [equipments, filterStatus, inferredStatus]);
/* ── 가동률 (센서 미연동 — 상태 기반 고정값) ── */
const utilizationMap = useMemo(() => {
const map: Record<string, number | null> = {};
// 설비 ID를 해시하여 상태별 고정 범위 내 값 생성 (리렌더링해도 안 변함)
const hash = (id: string) => {
let h = 0;
for (let i = 0; i < id.length; i++) h = ((h << 5) - h + id.charCodeAt(i)) | 0;
return Math.abs(h);
};
equipments.forEach((eq) => {
const s = inferredStatus[eq.id] ?? "unknown";
const h = hash(eq.id);
if (s === "running") map[eq.id] = 75 + (h % 20); // 75~94 고정
else if (s === "idle") map[eq.id] = 20 + (h % 30); // 20~49 고정
else if (s === "maintenance") map[eq.id] = 0;
else if (s === "off") map[eq.id] = 0;
else map[eq.id] = null;
});
return map;
}, [equipments, inferredStatus]);
const getUtilization = (eq: Equipment): number | null => utilizationMap[eq.id] ?? null;
/* ── 요약 카드 배열 ── */
const summaryCards: {
@@ -430,7 +506,7 @@ export default function EquipmentMonitoringPage() {
{filteredEquipments.length > 0 && (
<div className="grid gap-4" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(340px, 1fr))" }}>
{filteredEquipments.map((eq) => {
const status = resolveStatus(eq.operation_status);
const status = inferredStatus[eq.id] ?? "unknown";
const cfg = STATUS_MAP[status];
const utilization = getUtilization(eq);
const eqWIs = wiMap[eq.id] ?? [];

View File

@@ -118,11 +118,20 @@ interface Equipment {
interface WorkInstruction {
id: string;
wi_id?: string;
instruction_number: string;
work_instruction_no?: string;
item_name: string;
equipment_id: string;
worker_name: string;
status: string;
progress_status?: string;
}
interface ProcessRow {
wo_id: string;
status: string; // acceptable / in_progress / completed
parent_process_id?: string | null;
}
/* ───── 컴포넌트 ───── */
@@ -135,6 +144,7 @@ export default function EquipmentMonitoringPage() {
const [equipments, setEquipments] = useState<Equipment[]>([]);
const [workInstructions, setWorkInstructions] = useState<WorkInstruction[]>([]);
const [processRows, setProcessRows] = useState<ProcessRow[]>([]);
const [loading, setLoading] = useState(true);
const [currentTime, setCurrentTime] = useState(new Date());
const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh);
@@ -156,20 +166,28 @@ export default function EquipmentMonitoringPage() {
const fetchData = useCallback(async () => {
try {
setLoading(true);
const [equipRes, wiRes] = await Promise.all([
const [equipRes, wiRes, procRes] = await Promise.all([
apiClient.post("/table-management/tables/equipment_mng/data", {
autoFilter: true,
page: 1,
size: 500,
}),
apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })),
apiClient.post("/table-management/tables/work_order_process/data", {
page: 1,
size: 2000,
autoFilter: true,
}).catch(() => ({ data: { data: { data: [] } } })),
]);
const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? [];
const eqRows: Equipment[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? [];
setEquipments(eqRows);
const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? [];
setWorkInstructions(wiRows);
const pRows: ProcessRow[] = procRes.data?.data?.data ?? procRes.data?.data?.rows ?? [];
setProcessRows(pRows);
} catch (err) {
console.error("설비 모니터링 데이터 조회 실패:", err);
} finally {
@@ -189,49 +207,107 @@ export default function EquipmentMonitoringPage() {
return () => clearInterval(interval);
}, [fetchData, settings.refreshInterval]);
/* ── 요약 통계 ── */
const stats = useMemo(() => {
const counts: Record<OperationStatus, number> = {
running: 0,
idle: 0,
maintenance: 0,
off: 0,
unknown: 0,
};
equipments.forEach((eq) => {
const s = resolveStatus(eq.operation_status);
counts[s]++;
});
return { total: equipments.length, ...counts };
}, [equipments]);
/* ── 필터된 설비 ── */
const filteredEquipments = useMemo(() => {
if (filterStatus === "all") return equipments;
return equipments.filter((eq) => resolveStatus(eq.operation_status) === filterStatus);
}, [equipments, filterStatus]);
/* ── 설비별 작업지시 맵 ── */
const wiMap = useMemo(() => {
const map: Record<string, WorkInstruction[]> = {};
workInstructions.forEach((wi) => {
if (wi.equipment_id) {
if (!map[wi.equipment_id]) map[wi.equipment_id] = [];
map[wi.equipment_id].push(wi);
const eqId = wi.equipment_id;
if (eqId) {
if (!map[eqId]) map[eqId] = [];
map[eqId].push(wi);
}
});
return map;
}, [workInstructions]);
/* ── 가동률 (모킹 — 센서 미연동) ── */
const getUtilization = (eq: Equipment): number | null => {
const s = resolveStatus(eq.operation_status);
if (s === "running") return 75 + Math.floor(Math.random() * 20); // 75~94
if (s === "idle") return 20 + Math.floor(Math.random() * 30); // 20~49
if (s === "maintenance") return 0;
if (s === "off") return 0;
return null;
};
/* ── 설비 상태 자동 판단: 공정 데이터 기반 ── */
const inferredStatus = useMemo(() => {
// 작업지시 ID → 설비 ID 매핑
const wiToEquip: Record<string, string> = {};
workInstructions.forEach((wi) => {
const wiId = wi.wi_id || wi.id;
if (wi.equipment_id) wiToEquip[wiId] = wi.equipment_id;
});
// 설비별 공정 상태 집계
const equipProcessStatus: Record<string, Set<string>> = {};
processRows.forEach((p) => {
const eqId = wiToEquip[p.wo_id];
if (!eqId) return;
if (!equipProcessStatus[eqId]) equipProcessStatus[eqId] = new Set();
equipProcessStatus[eqId].add(p.status);
});
// 설비별 상태 판단
const result: Record<string, OperationStatus> = {};
equipments.forEach((eq) => {
const dbStatus = resolveStatus(eq.operation_status);
// DB에 점검/수리가 명시되어 있으면 그대로 사용
if (dbStatus === "maintenance") {
result[eq.id] = "maintenance";
return;
}
const statuses = equipProcessStatus[eq.id];
if (statuses) {
if (statuses.has("in_progress")) {
result[eq.id] = "running"; // 진행중 공정 있음 → 가동중
} else if (statuses.has("acceptable")) {
result[eq.id] = "idle"; // 접수 대기 공정 있음 → 대기
} else {
// 전부 completed → DB 상태 사용
result[eq.id] = dbStatus !== "unknown" ? dbStatus : "idle";
}
} else {
// 공정 데이터 없음 → 작업지시 여부로 판단
const eqWIs = wiMap[eq.id];
if (eqWIs && eqWIs.length > 0) {
result[eq.id] = "idle"; // 작업지시 배정됨 → 대기
} else {
result[eq.id] = dbStatus !== "unknown" ? dbStatus : "off";
}
}
});
return result;
}, [equipments, workInstructions, processRows, wiMap]);
/* ── 요약 통계 (추론 상태 기반) ── */
const stats = useMemo(() => {
const counts: Record<OperationStatus, number> = { running: 0, idle: 0, maintenance: 0, off: 0, unknown: 0 };
equipments.forEach((eq) => {
counts[inferredStatus[eq.id] ?? "unknown"]++;
});
return { total: equipments.length, ...counts };
}, [equipments, inferredStatus]);
/* ── 필터된 설비 ── */
const filteredEquipments = useMemo(() => {
if (filterStatus === "all") return equipments;
return equipments.filter((eq) => (inferredStatus[eq.id] ?? "unknown") === filterStatus);
}, [equipments, filterStatus, inferredStatus]);
/* ── 가동률 (센서 미연동 — 상태 기반 고정값) ── */
const utilizationMap = useMemo(() => {
const map: Record<string, number | null> = {};
// 설비 ID를 해시하여 상태별 고정 범위 내 값 생성 (리렌더링해도 안 변함)
const hash = (id: string) => {
let h = 0;
for (let i = 0; i < id.length; i++) h = ((h << 5) - h + id.charCodeAt(i)) | 0;
return Math.abs(h);
};
equipments.forEach((eq) => {
const s = inferredStatus[eq.id] ?? "unknown";
const h = hash(eq.id);
if (s === "running") map[eq.id] = 75 + (h % 20); // 75~94 고정
else if (s === "idle") map[eq.id] = 20 + (h % 30); // 20~49 고정
else if (s === "maintenance") map[eq.id] = 0;
else if (s === "off") map[eq.id] = 0;
else map[eq.id] = null;
});
return map;
}, [equipments, inferredStatus]);
const getUtilization = (eq: Equipment): number | null => utilizationMap[eq.id] ?? null;
/* ── 요약 카드 배열 ── */
const summaryCards: {
@@ -430,7 +506,7 @@ export default function EquipmentMonitoringPage() {
{filteredEquipments.length > 0 && (
<div className="grid gap-4" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(340px, 1fr))" }}>
{filteredEquipments.map((eq) => {
const status = resolveStatus(eq.operation_status);
const status = inferredStatus[eq.id] ?? "unknown";
const cfg = STATUS_MAP[status];
const utilization = getUtilization(eq);
const eqWIs = wiMap[eq.id] ?? [];

View File

@@ -118,11 +118,20 @@ interface Equipment {
interface WorkInstruction {
id: string;
wi_id?: string;
instruction_number: string;
work_instruction_no?: string;
item_name: string;
equipment_id: string;
worker_name: string;
status: string;
progress_status?: string;
}
interface ProcessRow {
wo_id: string;
status: string; // acceptable / in_progress / completed
parent_process_id?: string | null;
}
/* ───── 컴포넌트 ───── */
@@ -135,6 +144,7 @@ export default function EquipmentMonitoringPage() {
const [equipments, setEquipments] = useState<Equipment[]>([]);
const [workInstructions, setWorkInstructions] = useState<WorkInstruction[]>([]);
const [processRows, setProcessRows] = useState<ProcessRow[]>([]);
const [loading, setLoading] = useState(true);
const [currentTime, setCurrentTime] = useState(new Date());
const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh);
@@ -156,20 +166,28 @@ export default function EquipmentMonitoringPage() {
const fetchData = useCallback(async () => {
try {
setLoading(true);
const [equipRes, wiRes] = await Promise.all([
const [equipRes, wiRes, procRes] = await Promise.all([
apiClient.post("/table-management/tables/equipment_mng/data", {
autoFilter: true,
page: 1,
size: 500,
}),
apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })),
apiClient.post("/table-management/tables/work_order_process/data", {
page: 1,
size: 2000,
autoFilter: true,
}).catch(() => ({ data: { data: { data: [] } } })),
]);
const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? [];
const eqRows: Equipment[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? [];
setEquipments(eqRows);
const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? [];
setWorkInstructions(wiRows);
const pRows: ProcessRow[] = procRes.data?.data?.data ?? procRes.data?.data?.rows ?? [];
setProcessRows(pRows);
} catch (err) {
console.error("설비 모니터링 데이터 조회 실패:", err);
} finally {
@@ -189,49 +207,107 @@ export default function EquipmentMonitoringPage() {
return () => clearInterval(interval);
}, [fetchData, settings.refreshInterval]);
/* ── 요약 통계 ── */
const stats = useMemo(() => {
const counts: Record<OperationStatus, number> = {
running: 0,
idle: 0,
maintenance: 0,
off: 0,
unknown: 0,
};
equipments.forEach((eq) => {
const s = resolveStatus(eq.operation_status);
counts[s]++;
});
return { total: equipments.length, ...counts };
}, [equipments]);
/* ── 필터된 설비 ── */
const filteredEquipments = useMemo(() => {
if (filterStatus === "all") return equipments;
return equipments.filter((eq) => resolveStatus(eq.operation_status) === filterStatus);
}, [equipments, filterStatus]);
/* ── 설비별 작업지시 맵 ── */
const wiMap = useMemo(() => {
const map: Record<string, WorkInstruction[]> = {};
workInstructions.forEach((wi) => {
if (wi.equipment_id) {
if (!map[wi.equipment_id]) map[wi.equipment_id] = [];
map[wi.equipment_id].push(wi);
const eqId = wi.equipment_id;
if (eqId) {
if (!map[eqId]) map[eqId] = [];
map[eqId].push(wi);
}
});
return map;
}, [workInstructions]);
/* ── 가동률 (모킹 — 센서 미연동) ── */
const getUtilization = (eq: Equipment): number | null => {
const s = resolveStatus(eq.operation_status);
if (s === "running") return 75 + Math.floor(Math.random() * 20); // 75~94
if (s === "idle") return 20 + Math.floor(Math.random() * 30); // 20~49
if (s === "maintenance") return 0;
if (s === "off") return 0;
return null;
};
/* ── 설비 상태 자동 판단: 공정 데이터 기반 ── */
const inferredStatus = useMemo(() => {
// 작업지시 ID → 설비 ID 매핑
const wiToEquip: Record<string, string> = {};
workInstructions.forEach((wi) => {
const wiId = wi.wi_id || wi.id;
if (wi.equipment_id) wiToEquip[wiId] = wi.equipment_id;
});
// 설비별 공정 상태 집계
const equipProcessStatus: Record<string, Set<string>> = {};
processRows.forEach((p) => {
const eqId = wiToEquip[p.wo_id];
if (!eqId) return;
if (!equipProcessStatus[eqId]) equipProcessStatus[eqId] = new Set();
equipProcessStatus[eqId].add(p.status);
});
// 설비별 상태 판단
const result: Record<string, OperationStatus> = {};
equipments.forEach((eq) => {
const dbStatus = resolveStatus(eq.operation_status);
// DB에 점검/수리가 명시되어 있으면 그대로 사용
if (dbStatus === "maintenance") {
result[eq.id] = "maintenance";
return;
}
const statuses = equipProcessStatus[eq.id];
if (statuses) {
if (statuses.has("in_progress")) {
result[eq.id] = "running"; // 진행중 공정 있음 → 가동중
} else if (statuses.has("acceptable")) {
result[eq.id] = "idle"; // 접수 대기 공정 있음 → 대기
} else {
// 전부 completed → DB 상태 사용
result[eq.id] = dbStatus !== "unknown" ? dbStatus : "idle";
}
} else {
// 공정 데이터 없음 → 작업지시 여부로 판단
const eqWIs = wiMap[eq.id];
if (eqWIs && eqWIs.length > 0) {
result[eq.id] = "idle"; // 작업지시 배정됨 → 대기
} else {
result[eq.id] = dbStatus !== "unknown" ? dbStatus : "off";
}
}
});
return result;
}, [equipments, workInstructions, processRows, wiMap]);
/* ── 요약 통계 (추론 상태 기반) ── */
const stats = useMemo(() => {
const counts: Record<OperationStatus, number> = { running: 0, idle: 0, maintenance: 0, off: 0, unknown: 0 };
equipments.forEach((eq) => {
counts[inferredStatus[eq.id] ?? "unknown"]++;
});
return { total: equipments.length, ...counts };
}, [equipments, inferredStatus]);
/* ── 필터된 설비 ── */
const filteredEquipments = useMemo(() => {
if (filterStatus === "all") return equipments;
return equipments.filter((eq) => (inferredStatus[eq.id] ?? "unknown") === filterStatus);
}, [equipments, filterStatus, inferredStatus]);
/* ── 가동률 (센서 미연동 — 상태 기반 고정값) ── */
const utilizationMap = useMemo(() => {
const map: Record<string, number | null> = {};
// 설비 ID를 해시하여 상태별 고정 범위 내 값 생성 (리렌더링해도 안 변함)
const hash = (id: string) => {
let h = 0;
for (let i = 0; i < id.length; i++) h = ((h << 5) - h + id.charCodeAt(i)) | 0;
return Math.abs(h);
};
equipments.forEach((eq) => {
const s = inferredStatus[eq.id] ?? "unknown";
const h = hash(eq.id);
if (s === "running") map[eq.id] = 75 + (h % 20); // 75~94 고정
else if (s === "idle") map[eq.id] = 20 + (h % 30); // 20~49 고정
else if (s === "maintenance") map[eq.id] = 0;
else if (s === "off") map[eq.id] = 0;
else map[eq.id] = null;
});
return map;
}, [equipments, inferredStatus]);
const getUtilization = (eq: Equipment): number | null => utilizationMap[eq.id] ?? null;
/* ── 요약 카드 배열 ── */
const summaryCards: {
@@ -430,7 +506,7 @@ export default function EquipmentMonitoringPage() {
{filteredEquipments.length > 0 && (
<div className="grid gap-4" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(340px, 1fr))" }}>
{filteredEquipments.map((eq) => {
const status = resolveStatus(eq.operation_status);
const status = inferredStatus[eq.id] ?? "unknown";
const cfg = STATUS_MAP[status];
const utilization = getUtilization(eq);
const eqWIs = wiMap[eq.id] ?? [];

View File

@@ -118,11 +118,20 @@ interface Equipment {
interface WorkInstruction {
id: string;
wi_id?: string;
instruction_number: string;
work_instruction_no?: string;
item_name: string;
equipment_id: string;
worker_name: string;
status: string;
progress_status?: string;
}
interface ProcessRow {
wo_id: string;
status: string; // acceptable / in_progress / completed
parent_process_id?: string | null;
}
/* ───── 컴포넌트 ───── */
@@ -135,6 +144,7 @@ export default function EquipmentMonitoringPage() {
const [equipments, setEquipments] = useState<Equipment[]>([]);
const [workInstructions, setWorkInstructions] = useState<WorkInstruction[]>([]);
const [processRows, setProcessRows] = useState<ProcessRow[]>([]);
const [loading, setLoading] = useState(true);
const [currentTime, setCurrentTime] = useState(new Date());
const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh);
@@ -156,20 +166,28 @@ export default function EquipmentMonitoringPage() {
const fetchData = useCallback(async () => {
try {
setLoading(true);
const [equipRes, wiRes] = await Promise.all([
const [equipRes, wiRes, procRes] = await Promise.all([
apiClient.post("/table-management/tables/equipment_mng/data", {
autoFilter: true,
page: 1,
size: 500,
}),
apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })),
apiClient.post("/table-management/tables/work_order_process/data", {
page: 1,
size: 2000,
autoFilter: true,
}).catch(() => ({ data: { data: { data: [] } } })),
]);
const eqRows: Equipment[] = equipRes.data?.data?.rows ?? equipRes.data?.rows ?? [];
const eqRows: Equipment[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? [];
setEquipments(eqRows);
const wiRows: WorkInstruction[] = wiRes.data?.data ?? wiRes.data?.rows ?? [];
setWorkInstructions(wiRows);
const pRows: ProcessRow[] = procRes.data?.data?.data ?? procRes.data?.data?.rows ?? [];
setProcessRows(pRows);
} catch (err) {
console.error("설비 모니터링 데이터 조회 실패:", err);
} finally {
@@ -189,49 +207,107 @@ export default function EquipmentMonitoringPage() {
return () => clearInterval(interval);
}, [fetchData, settings.refreshInterval]);
/* ── 요약 통계 ── */
const stats = useMemo(() => {
const counts: Record<OperationStatus, number> = {
running: 0,
idle: 0,
maintenance: 0,
off: 0,
unknown: 0,
};
equipments.forEach((eq) => {
const s = resolveStatus(eq.operation_status);
counts[s]++;
});
return { total: equipments.length, ...counts };
}, [equipments]);
/* ── 필터된 설비 ── */
const filteredEquipments = useMemo(() => {
if (filterStatus === "all") return equipments;
return equipments.filter((eq) => resolveStatus(eq.operation_status) === filterStatus);
}, [equipments, filterStatus]);
/* ── 설비별 작업지시 맵 ── */
const wiMap = useMemo(() => {
const map: Record<string, WorkInstruction[]> = {};
workInstructions.forEach((wi) => {
if (wi.equipment_id) {
if (!map[wi.equipment_id]) map[wi.equipment_id] = [];
map[wi.equipment_id].push(wi);
const eqId = wi.equipment_id;
if (eqId) {
if (!map[eqId]) map[eqId] = [];
map[eqId].push(wi);
}
});
return map;
}, [workInstructions]);
/* ── 가동률 (모킹 — 센서 미연동) ── */
const getUtilization = (eq: Equipment): number | null => {
const s = resolveStatus(eq.operation_status);
if (s === "running") return 75 + Math.floor(Math.random() * 20); // 75~94
if (s === "idle") return 20 + Math.floor(Math.random() * 30); // 20~49
if (s === "maintenance") return 0;
if (s === "off") return 0;
return null;
};
/* ── 설비 상태 자동 판단: 공정 데이터 기반 ── */
const inferredStatus = useMemo(() => {
// 작업지시 ID → 설비 ID 매핑
const wiToEquip: Record<string, string> = {};
workInstructions.forEach((wi) => {
const wiId = wi.wi_id || wi.id;
if (wi.equipment_id) wiToEquip[wiId] = wi.equipment_id;
});
// 설비별 공정 상태 집계
const equipProcessStatus: Record<string, Set<string>> = {};
processRows.forEach((p) => {
const eqId = wiToEquip[p.wo_id];
if (!eqId) return;
if (!equipProcessStatus[eqId]) equipProcessStatus[eqId] = new Set();
equipProcessStatus[eqId].add(p.status);
});
// 설비별 상태 판단
const result: Record<string, OperationStatus> = {};
equipments.forEach((eq) => {
const dbStatus = resolveStatus(eq.operation_status);
// DB에 점검/수리가 명시되어 있으면 그대로 사용
if (dbStatus === "maintenance") {
result[eq.id] = "maintenance";
return;
}
const statuses = equipProcessStatus[eq.id];
if (statuses) {
if (statuses.has("in_progress")) {
result[eq.id] = "running"; // 진행중 공정 있음 → 가동중
} else if (statuses.has("acceptable")) {
result[eq.id] = "idle"; // 접수 대기 공정 있음 → 대기
} else {
// 전부 completed → DB 상태 사용
result[eq.id] = dbStatus !== "unknown" ? dbStatus : "idle";
}
} else {
// 공정 데이터 없음 → 작업지시 여부로 판단
const eqWIs = wiMap[eq.id];
if (eqWIs && eqWIs.length > 0) {
result[eq.id] = "idle"; // 작업지시 배정됨 → 대기
} else {
result[eq.id] = dbStatus !== "unknown" ? dbStatus : "off";
}
}
});
return result;
}, [equipments, workInstructions, processRows, wiMap]);
/* ── 요약 통계 (추론 상태 기반) ── */
const stats = useMemo(() => {
const counts: Record<OperationStatus, number> = { running: 0, idle: 0, maintenance: 0, off: 0, unknown: 0 };
equipments.forEach((eq) => {
counts[inferredStatus[eq.id] ?? "unknown"]++;
});
return { total: equipments.length, ...counts };
}, [equipments, inferredStatus]);
/* ── 필터된 설비 ── */
const filteredEquipments = useMemo(() => {
if (filterStatus === "all") return equipments;
return equipments.filter((eq) => (inferredStatus[eq.id] ?? "unknown") === filterStatus);
}, [equipments, filterStatus, inferredStatus]);
/* ── 가동률 (센서 미연동 — 상태 기반 고정값) ── */
const utilizationMap = useMemo(() => {
const map: Record<string, number | null> = {};
// 설비 ID를 해시하여 상태별 고정 범위 내 값 생성 (리렌더링해도 안 변함)
const hash = (id: string) => {
let h = 0;
for (let i = 0; i < id.length; i++) h = ((h << 5) - h + id.charCodeAt(i)) | 0;
return Math.abs(h);
};
equipments.forEach((eq) => {
const s = inferredStatus[eq.id] ?? "unknown";
const h = hash(eq.id);
if (s === "running") map[eq.id] = 75 + (h % 20); // 75~94 고정
else if (s === "idle") map[eq.id] = 20 + (h % 30); // 20~49 고정
else if (s === "maintenance") map[eq.id] = 0;
else if (s === "off") map[eq.id] = 0;
else map[eq.id] = null;
});
return map;
}, [equipments, inferredStatus]);
const getUtilization = (eq: Equipment): number | null => utilizationMap[eq.id] ?? null;
/* ── 요약 카드 배열 ── */
const summaryCards: {
@@ -430,7 +506,7 @@ export default function EquipmentMonitoringPage() {
{filteredEquipments.length > 0 && (
<div className="grid gap-4" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(340px, 1fr))" }}>
{filteredEquipments.map((eq) => {
const status = resolveStatus(eq.operation_status);
const status = inferredStatus[eq.id] ?? "unknown";
const cfg = STATUS_MAP[status];
const utilization = getUtilization(eq);
const eqWIs = wiMap[eq.id] ?? [];