Files
pipeline/frontend/components/pop/hardcoded/equipment/EquipmentList.tsx
T
SeongHyun Kim 9f00988110 feat: POP 전면 개선 — 신규 화면 5개 + 버그 수정 9건 + UI 개선
[신규 화면]
- 설비허브 + 설비관리 + 설비점검
- 재고조정 + 재고이동

[버그 수정]
- 창고 NULL status 누락
- 작업지시 sync detail fallback
- InspectionModal API 경로
- 검사결과 DB 저장
- seq_no 비순차 대응
- 출고 재고 부족 검증
- 자동 창고 매칭
- 내 접수 목록 필터

[UI 개선]
- 사이드바 카드형
- 자재투입 컴팩트
- 커스텀 모달
- 불필요 버튼 제거
2026-04-10 10:28:39 +09:00

170 lines
6.8 KiB
TypeScript

"use client";
import React, { useState, useEffect, useCallback } from "react";
import { useRouter } from "next/navigation";
import { apiClient } from "@/lib/api/client";
import { PopShell } from "../PopShell";
interface EquipmentItem {
id: string;
equipment_code: string;
equipment_name: string;
equipment_type?: string;
location?: string;
status?: string;
last_inspection_date?: string;
next_inspection_date?: string;
memo?: string;
}
export function EquipmentList() {
const router = useRouter();
const [items, setItems] = useState<EquipmentItem[]>([]);
const [loading, setLoading] = useState(true);
const [searchKeyword, setSearchKeyword] = useState("");
const fetchEquipments = useCallback(async () => {
setLoading(true);
try {
const res = await apiClient.get("/data/equipment_mng", {
params: { pageSize: 500 },
});
const data = res.data?.data?.data ?? res.data?.data ?? [];
setItems(Array.isArray(data) ? data : []);
} catch {
setItems([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchEquipments();
}, [fetchEquipments]);
const filtered = items.filter((item) => {
if (!searchKeyword) return true;
const kw = searchKeyword.toLowerCase();
return (
(item.equipment_code || "").toLowerCase().includes(kw) ||
(item.equipment_name || "").toLowerCase().includes(kw) ||
(item.equipment_type || "").toLowerCase().includes(kw)
);
});
// KPI
const totalCount = items.length;
const activeCount = items.filter((i) => i.status === "가동" || i.status === "정상" || !i.status).length;
const stopCount = items.filter((i) => i.status === "정지" || i.status === "비가동").length;
return (
<PopShell title="설비관리" showBanner={false}>
<div className="min-h-screen bg-gray-50">
{/* Header */}
<div className="bg-white border-b border-gray-200 px-5 pt-5 pb-4">
<div className="flex items-center gap-3 mb-4">
<button
onClick={() => router.back()}
className="w-9 h-9 rounded-xl bg-gray-100 flex items-center justify-center"
>
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
</button>
<div>
<h1 className="text-xl font-bold text-gray-900"></h1>
<p className="text-sm text-gray-500"> </p>
</div>
</div>
{/* Search */}
<div className="relative">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg>
<input
type="text"
placeholder="설비명 또는 코드 검색..."
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
className="w-full pl-10 pr-4 py-3 rounded-xl border border-gray-200 text-sm focus:outline-none focus:border-blue-400"
/>
</div>
</div>
{/* KPI */}
<div className="px-5 py-3">
<div className="grid grid-cols-3 gap-3">
<div className="bg-white rounded-xl border border-gray-200 p-3 text-center">
<p className="text-xs text-gray-400"></p>
<p className="text-2xl font-bold text-gray-900">{totalCount}</p>
</div>
<div className="bg-white rounded-xl border border-green-200 p-3 text-center">
<p className="text-xs text-green-500"></p>
<p className="text-2xl font-bold text-green-600">{activeCount}</p>
</div>
<div className="bg-white rounded-xl border border-red-200 p-3 text-center">
<p className="text-xs text-red-500"></p>
<p className="text-2xl font-bold text-red-600">{stopCount}</p>
</div>
</div>
</div>
{/* List */}
<div className="px-5 pb-24">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-gray-500"> </h2>
<span className="text-xs text-gray-400">{filtered.length}</span>
</div>
{loading ? (
<div className="flex items-center justify-center py-20">
<div className="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" />
</div>
) : filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-gray-400">
<svg className="w-16 h-16 mb-4 text-gray-200" fill="none" stroke="currentColor" strokeWidth={1} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M11.42 15.17l-5.65-5.65a8 8 0 1111.31 0l-5.65 5.65z" />
</svg>
<p className="text-sm"> </p>
</div>
) : (
<div className="space-y-3">
{filtered.map((item) => (
<div
key={item.id}
className="bg-white rounded-2xl border border-gray-200 p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between mb-2">
<div>
<h3 className="font-bold text-gray-900">{item.equipment_name || "-"}</h3>
<p className="text-xs text-gray-400">{item.equipment_code}</p>
</div>
<span
className={`text-xs px-2.5 py-1 rounded-full font-semibold ${
item.status === "정지" || item.status === "비가동"
? "bg-red-100 text-red-600"
: "bg-green-100 text-green-600"
}`}
>
{item.status || "정상"}
</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs text-gray-500">
{item.equipment_type && (
<div>: <span className="text-gray-700 font-medium">{item.equipment_type}</span></div>
)}
{item.location && (
<div>: <span className="text-gray-700 font-medium">{item.location}</span></div>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
</PopShell>
);
}