231 lines
11 KiB
TypeScript
231 lines
11 KiB
TypeScript
import { useState, useEffect, useMemo } from 'react';
|
||
import { fetchOrganizations } from '@tabs/assets/services/assetsApi';
|
||
import type { AssetOrgCompat } from '@tabs/assets/services/assetsApi';
|
||
import { typeTagCls } from '@tabs/assets/components/assetTypes';
|
||
|
||
const PAGE_SIZE = 20;
|
||
|
||
const regionShort = (j: string) =>
|
||
j.includes('남해') ? '남해청' : j.includes('서해') ? '서해청' :
|
||
j.includes('중부') ? '중부청' : j.includes('동해') ? '동해청' :
|
||
j.includes('제주') ? '제주청' : j;
|
||
|
||
function CleanupEquipPanel() {
|
||
const [organizations, setOrganizations] = useState<AssetOrgCompat[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [searchTerm, setSearchTerm] = useState('');
|
||
const [regionFilter, setRegionFilter] = useState('전체');
|
||
const [typeFilter, setTypeFilter] = useState('전체');
|
||
const [currentPage, setCurrentPage] = useState(1);
|
||
|
||
const load = () => {
|
||
setLoading(true);
|
||
fetchOrganizations()
|
||
.then(setOrganizations)
|
||
.catch(err => console.error('[CleanupEquipPanel] 데이터 로드 실패:', err))
|
||
.finally(() => setLoading(false));
|
||
};
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
fetchOrganizations()
|
||
.then(data => { if (!cancelled) setOrganizations(data); })
|
||
.catch(err => console.error('[CleanupEquipPanel] 데이터 로드 실패:', err))
|
||
.finally(() => { if (!cancelled) setLoading(false); });
|
||
return () => { cancelled = true; };
|
||
}, []);
|
||
|
||
const typeOptions = useMemo(() => {
|
||
const set = new Set(organizations.map(o => o.type));
|
||
return Array.from(set).sort();
|
||
}, [organizations]);
|
||
|
||
const filtered = useMemo(() =>
|
||
organizations
|
||
.filter(o => regionFilter === '전체' || o.jurisdiction.includes(regionFilter))
|
||
.filter(o => typeFilter === '전체' || o.type === typeFilter)
|
||
.filter(o => !searchTerm || o.name.includes(searchTerm) || o.address.includes(searchTerm)),
|
||
[organizations, regionFilter, typeFilter, searchTerm]
|
||
);
|
||
|
||
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
||
const safePage = Math.min(currentPage, totalPages);
|
||
const paged = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE);
|
||
|
||
const handleFilterChange = (setter: (v: string) => void) => (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||
setter(e.target.value);
|
||
setCurrentPage(1);
|
||
};
|
||
|
||
const pageNumbers = (() => {
|
||
const range: number[] = [];
|
||
const start = Math.max(1, safePage - 2);
|
||
const end = Math.min(totalPages, safePage + 2);
|
||
for (let i = start; i <= end; i++) range.push(i);
|
||
return range;
|
||
})();
|
||
|
||
return (
|
||
<div className="flex flex-col h-full">
|
||
{/* 헤더 */}
|
||
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
||
<div>
|
||
<h1 className="text-lg font-bold text-text-1 font-korean">방제장비 현황</h1>
|
||
<p className="text-xs text-text-3 mt-1 font-korean">총 {filtered.length}개 기관</p>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<select
|
||
value={regionFilter}
|
||
onChange={handleFilterChange(setRegionFilter)}
|
||
className="px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
|
||
>
|
||
<option value="전체">전체 관할청</option>
|
||
<option value="남해">남해청</option>
|
||
<option value="서해">서해청</option>
|
||
<option value="중부">중부청</option>
|
||
<option value="동해">동해청</option>
|
||
<option value="제주">제주청</option>
|
||
</select>
|
||
<select
|
||
value={typeFilter}
|
||
onChange={handleFilterChange(setTypeFilter)}
|
||
className="px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
|
||
>
|
||
<option value="전체">전체 유형</option>
|
||
{typeOptions.map(t => (
|
||
<option key={t} value={t}>{t}</option>
|
||
))}
|
||
</select>
|
||
<input
|
||
type="text"
|
||
placeholder="기관명, 주소 검색..."
|
||
value={searchTerm}
|
||
onChange={e => { setSearchTerm(e.target.value); setCurrentPage(1); }}
|
||
className="w-56 px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-korean"
|
||
/>
|
||
<button
|
||
onClick={load}
|
||
className="px-4 py-2 text-xs font-semibold rounded-md bg-bg-2 border border-border text-text-2 hover:border-primary-cyan hover:text-primary-cyan transition-all font-korean"
|
||
>
|
||
새로고침
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 테이블 */}
|
||
<div className="flex-1 overflow-auto">
|
||
{loading ? (
|
||
<div className="flex items-center justify-center h-32 text-text-3 text-sm font-korean">
|
||
불러오는 중...
|
||
</div>
|
||
) : (
|
||
<table className="w-full">
|
||
<thead>
|
||
<tr className="border-b border-border bg-bg-1">
|
||
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean w-10">번호</th>
|
||
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean">유형</th>
|
||
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean">관할청</th>
|
||
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean">기관명</th>
|
||
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean">주소</th>
|
||
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean">방제선</th>
|
||
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean">유회수기</th>
|
||
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean">이송펌프</th>
|
||
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean">방제차량</th>
|
||
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean">살포장치</th>
|
||
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean">총자산</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{paged.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={11} className="px-6 py-10 text-center text-xs text-text-3 font-korean">
|
||
조회된 기관이 없습니다.
|
||
</td>
|
||
</tr>
|
||
) : paged.map((org, idx) => (
|
||
<tr key={org.id} className="border-b border-border hover:bg-[rgba(255,255,255,0.02)] transition-colors">
|
||
<td className="px-4 py-3 text-[11px] text-text-3 font-mono text-center">
|
||
{(safePage - 1) * PAGE_SIZE + idx + 1}
|
||
</td>
|
||
<td className="px-4 py-3">
|
||
<span className={`text-[10px] px-1.5 py-0.5 rounded font-bold font-korean ${typeTagCls(org.type)}`}>
|
||
{org.type}
|
||
</span>
|
||
</td>
|
||
<td className="px-4 py-3 text-[11px] text-text-2 font-korean">
|
||
{regionShort(org.jurisdiction)}
|
||
</td>
|
||
<td className="px-4 py-3 text-[11px] text-text-1 font-korean font-semibold">
|
||
{org.name}
|
||
</td>
|
||
<td className="px-4 py-3 text-[11px] text-text-3 font-korean max-w-[200px] truncate">
|
||
{org.address}
|
||
</td>
|
||
<td className="px-4 py-3 text-[11px] font-mono text-center text-text-2">
|
||
{org.vessel > 0 ? <span className="text-text-1">{org.vessel}</span> : <span className="text-text-3">—</span>}
|
||
</td>
|
||
<td className="px-4 py-3 text-[11px] font-mono text-center text-text-2">
|
||
{org.skimmer > 0 ? <span className="text-text-1">{org.skimmer}</span> : <span className="text-text-3">—</span>}
|
||
</td>
|
||
<td className="px-4 py-3 text-[11px] font-mono text-center text-text-2">
|
||
{org.pump > 0 ? <span className="text-text-1">{org.pump}</span> : <span className="text-text-3">—</span>}
|
||
</td>
|
||
<td className="px-4 py-3 text-[11px] font-mono text-center text-text-2">
|
||
{org.vehicle > 0 ? <span className="text-text-1">{org.vehicle}</span> : <span className="text-text-3">—</span>}
|
||
</td>
|
||
<td className="px-4 py-3 text-[11px] font-mono text-center text-text-2">
|
||
{org.sprayer > 0 ? <span className="text-text-1">{org.sprayer}</span> : <span className="text-text-3">—</span>}
|
||
</td>
|
||
<td className="px-4 py-3 text-[11px] font-mono text-center font-bold text-primary-cyan">
|
||
{org.totalAssets.toLocaleString()}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
|
||
{/* 페이지네이션 */}
|
||
{!loading && filtered.length > 0 && (
|
||
<div className="flex items-center justify-between px-6 py-3 border-t border-border">
|
||
<span className="text-[11px] text-text-3 font-korean">
|
||
{(safePage - 1) * PAGE_SIZE + 1}–{Math.min(safePage * PAGE_SIZE, filtered.length)} / 전체 {filtered.length}개
|
||
</span>
|
||
<div className="flex items-center gap-1">
|
||
<button
|
||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||
disabled={safePage === 1}
|
||
className="px-2.5 py-1 text-[11px] border border-border rounded text-text-2 hover:border-primary-cyan hover:text-primary-cyan disabled:opacity-40 transition-colors"
|
||
>
|
||
<
|
||
</button>
|
||
{pageNumbers.map(p => (
|
||
<button
|
||
key={p}
|
||
onClick={() => setCurrentPage(p)}
|
||
className="px-2.5 py-1 text-[11px] border rounded transition-colors"
|
||
style={p === safePage
|
||
? { borderColor: 'var(--cyan)', color: 'var(--cyan)', background: 'rgba(6,182,212,0.1)' }
|
||
: { borderColor: 'var(--border)', color: 'var(--text-2)' }
|
||
}
|
||
>
|
||
{p}
|
||
</button>
|
||
))}
|
||
<button
|
||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||
disabled={safePage === totalPages}
|
||
className="px-2.5 py-1 text-[11px] border border-border rounded text-text-2 hover:border-primary-cyan hover:text-primary-cyan disabled:opacity-40 transition-colors"
|
||
>
|
||
>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default CleanupEquipPanel;
|