jskim-node #16
@@ -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] ?? [];
|
||||
|
||||
@@ -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] ?? [];
|
||||
|
||||
@@ -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] ?? [];
|
||||
|
||||
@@ -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] ?? [];
|
||||
|
||||
@@ -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] ?? [];
|
||||
|
||||
@@ -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] ?? [];
|
||||
|
||||
@@ -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] ?? [];
|
||||
|
||||
Reference in New Issue
Block a user