feat(assets): 장비 유형별 필터 + 합계 행 + 컬럼 하이라이트

- 방제선/유회수기/이송펌프/방제차량/살포장치 장비 필터 드롭다운 추가
- 페이지네이션 위 합계 행에 필터된 기관의 장비별 총합 표시
- 장비 필터 선택 시 해당 컬럼 헤더/셀/합계 항목 cyan 하이라이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nan Kyung Lee 2026-03-05 13:10:51 +09:00
부모 29686f9476
커밋 618d898a6c

파일 보기

@ -12,6 +12,7 @@ function AssetManagement() {
const [regionFilter, setRegionFilter] = useState('all') const [regionFilter, setRegionFilter] = useState('all')
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
const [typeFilterVal, setTypeFilterVal] = useState('all') const [typeFilterVal, setTypeFilterVal] = useState('all')
const [equipFilter, setEquipFilter] = useState('all')
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const pageSize = 15 const pageSize = 15
@ -44,6 +45,16 @@ function AssetManagement() {
const filtered = organizations.filter(o => { const filtered = organizations.filter(o => {
if (regionFilter !== 'all' && !o.jurisdiction.includes(regionFilter)) return false if (regionFilter !== 'all' && !o.jurisdiction.includes(regionFilter)) return false
if (typeFilterVal !== 'all' && o.type !== typeFilterVal) return false if (typeFilterVal !== 'all' && o.type !== typeFilterVal) return false
if (equipFilter !== 'all') {
const equipMap: Record<string, (org: AssetOrgCompat) => boolean> = {
vessel: org => org.vessel > 0,
skimmer: org => org.skimmer > 0,
pump: org => org.pump > 0,
vehicle: org => org.vehicle > 0,
sprayer: org => org.sprayer > 0,
}
if (equipMap[equipFilter] && !equipMap[equipFilter](o)) return false
}
if (searchTerm && !o.name.includes(searchTerm) && !o.address.includes(searchTerm)) return false if (searchTerm && !o.name.includes(searchTerm) && !o.address.includes(searchTerm)) return false
return true return true
}) })
@ -54,7 +65,7 @@ function AssetManagement() {
// 필터 변경 시 첫 페이지로 // 필터 변경 시 첫 페이지로
// eslint-disable-next-line react-hooks/set-state-in-effect // eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => { setCurrentPage(1) }, [regionFilter, typeFilterVal, searchTerm]) useEffect(() => { setCurrentPage(1) }, [regionFilter, typeFilterVal, equipFilter, searchTerm])
const regionShort = (j: string) => { const regionShort = (j: string) => {
if (j.includes('중부')) return '중부청' if (j.includes('중부')) return '중부청'
@ -129,6 +140,14 @@ function AssetManagement() {
<option value="해군"></option> <option value="해군"></option>
<option value="기타"></option> <option value="기타"></option>
</select> </select>
<select value={equipFilter} onChange={e => setEquipFilter(e.target.value)} className="prd-i w-[100px] py-1.5 px-2">
<option value="all"> </option>
<option value="vessel"></option>
<option value="skimmer"></option>
<option value="pump"></option>
<option value="vehicle"></option>
<option value="sprayer"></option>
</select>
</div> </div>
</div> </div>
@ -152,11 +171,15 @@ function AssetManagement() {
</colgroup> </colgroup>
<thead> <thead>
<tr className="border-b border-border bg-bg-0"> <tr className="border-b border-border bg-bg-0">
{['번호', '유형', '관할청', '기관명', '주소', '방제선', '유회수기', '이송펌프', '방제차량', '살포장치', '총자산'].map((h, i) => ( {['번호', '유형', '관할청', '기관명', '주소', '방제선', '유회수기', '이송펌프', '방제차량', '살포장치', '총자산'].map((h, i) => {
<th key={i} className={`px-2.5 py-2.5 text-[10px] font-bold text-text-2 font-korean border-b border-border ${[0,5,6,7,8,9,10].includes(i) ? 'text-center' : ''}`}> const equipColMap: Record<string, number> = { vessel: 5, skimmer: 6, pump: 7, vehicle: 8, sprayer: 9 }
const isHighlight = equipFilter !== 'all' && equipColMap[equipFilter] === i
return (
<th key={i} className={`px-2.5 py-2.5 text-[10px] font-bold font-korean border-b border-border ${[0,5,6,7,8,9,10].includes(i) ? 'text-center' : ''} ${isHighlight ? 'text-primary-cyan bg-primary-cyan/5' : 'text-text-2'}`}>
{h} {h}
</th> </th>
))} )
})}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -175,11 +198,11 @@ function AssetManagement() {
<td className="px-2.5 py-2 text-[10px] font-semibold font-korean">{regionShort(org.jurisdiction)}</td> <td className="px-2.5 py-2 text-[10px] font-semibold font-korean">{regionShort(org.jurisdiction)}</td>
<td className="px-2.5 py-2 text-[10px] font-semibold text-primary-cyan font-korean cursor-pointer truncate">{org.name}</td> <td className="px-2.5 py-2 text-[10px] font-semibold text-primary-cyan font-korean cursor-pointer truncate">{org.name}</td>
<td className="px-2.5 py-2 text-[10px] text-text-3 font-korean truncate">{org.address}</td> <td className="px-2.5 py-2 text-[10px] text-text-3 font-korean truncate">{org.address}</td>
<td className="px-2.5 py-2 text-center font-mono text-[10px] font-semibold">{org.vessel}</td> <td className={`px-2.5 py-2 text-center font-mono text-[10px] font-semibold ${equipFilter === 'vessel' ? 'text-primary-cyan bg-primary-cyan/5' : ''}`}>{org.vessel}</td>
<td className="px-2.5 py-2 text-center font-mono text-[10px]">{org.skimmer}</td> <td className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'skimmer' ? 'text-primary-cyan font-semibold bg-primary-cyan/5' : ''}`}>{org.skimmer}</td>
<td className="px-2.5 py-2 text-center font-mono text-[10px]">{org.pump}</td> <td className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'pump' ? 'text-primary-cyan font-semibold bg-primary-cyan/5' : ''}`}>{org.pump}</td>
<td className="px-2.5 py-2 text-center font-mono text-[10px]">{org.vehicle}</td> <td className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'vehicle' ? 'text-primary-cyan font-semibold bg-primary-cyan/5' : ''}`}>{org.vehicle}</td>
<td className="px-2.5 py-2 text-center font-mono text-[10px]">{org.sprayer}</td> <td className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'sprayer' ? 'text-primary-cyan font-semibold bg-primary-cyan/5' : ''}`}>{org.sprayer}</td>
<td className="px-2.5 py-2 text-center font-bold text-primary-cyan font-mono text-[10px]">{org.totalAssets}</td> <td className="px-2.5 py-2 text-center font-bold text-primary-cyan font-mono text-[10px]">{org.totalAssets}</td>
</tr> </tr>
))} ))}
@ -187,6 +210,29 @@ function AssetManagement() {
</table> </table>
</div> </div>
{/* Totals Summary */}
<div className="flex items-center justify-end gap-4 px-4 py-2 border-t border-border bg-bg-0/80">
<span className="text-[10px] text-text-3 font-korean font-semibold mr-auto">
({filtered.length} )
</span>
{[
{ key: 'vessel', label: '방제선', value: filtered.reduce((s, o) => s + o.vessel, 0), unit: '척' },
{ key: 'skimmer', label: '유회수기', value: filtered.reduce((s, o) => s + o.skimmer, 0), unit: '대' },
{ key: 'pump', label: '이송펌프', value: filtered.reduce((s, o) => s + o.pump, 0), unit: '대' },
{ key: 'vehicle', label: '방제차량', value: filtered.reduce((s, o) => s + o.vehicle, 0), unit: '대' },
{ key: 'sprayer', label: '살포장치', value: filtered.reduce((s, o) => s + o.sprayer, 0), unit: '대' },
{ key: 'total', label: '총자산', value: filtered.reduce((s, o) => s + o.totalAssets, 0), unit: '' },
].map((t) => {
const isActive = equipFilter === t.key || t.key === 'total'
return (
<div key={t.key} className={`flex items-center gap-1 px-1.5 py-0.5 rounded ${equipFilter === t.key ? 'bg-primary-cyan/10' : ''}`}>
<span className={`text-[9px] font-korean ${isActive ? 'text-primary-cyan' : 'text-text-3'}`}>{t.label}</span>
<span className={`text-[10px] font-mono font-bold ${isActive ? 'text-primary-cyan' : 'text-text-1'}`}>{t.value.toLocaleString()}{t.unit}</span>
</div>
)
})}
</div>
{/* Pagination */} {/* Pagination */}
<div className="flex items-center justify-center gap-4 px-4 py-2.5 border-t border-border bg-bg-0"> <div className="flex items-center justify-center gap-4 px-4 py-2.5 border-t border-border bg-bg-0">
<span className="text-[10px] text-text-3 font-korean"> <span className="text-[10px] text-text-3 font-korean">