wing-ops/frontend/src/tabs/admin/components/SensitiveLayerPanel.tsx

333 lines
13 KiB
TypeScript
Raw Blame 히스토리

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState, useCallback } from 'react';
import { api } from '@common/services/api';
interface SensitiveLayerPanelProps {
categoryCode: string;
title: string;
}
interface LayerAdminItem {
layerCd: string;
upLayerCd: string | null;
layerFullNm: string;
layerNm: string;
layerLevel: number;
wmsLayerNm: string | null;
useYn: string;
sortOrd: number;
regDtm: string | null;
}
interface LayerListResponse {
items: LayerAdminItem[];
total: number;
page: number;
totalPages: number;
}
const PAGE_SIZE = 10;
async function fetchSensitiveLayers(
page: number,
search: string,
useYn: string,
rootCd: string,
): Promise<LayerListResponse> {
const params = new URLSearchParams({ page: String(page), limit: String(PAGE_SIZE), rootCd });
if (search) params.set('search', search);
if (useYn) params.set('useYn', useYn);
const res = await api.get<LayerListResponse>(`/layers/admin/list?${params}`);
return res.data;
}
async function toggleLayerUse(layerCd: string): Promise<{ layerCd: string; useYn: string }> {
const res = await api.post<{ layerCd: string; useYn: string }>('/layers/admin/toggle-use', {
layerCd,
});
return res.data;
}
// ---------- SensitiveLayerPanel ----------
const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps) => {
const [items, setItems] = useState<LayerAdminItem[]>([]);
const [total, setTotal] = useState(0);
const [totalPages, setTotalPages] = useState(1);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [toggling, setToggling] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [searchInput, setSearchInput] = useState('');
const [appliedSearch, setAppliedSearch] = useState('');
const [filterUseYn, setFilterUseYn] = useState('');
const load = useCallback(
async (p: number, search: string, useYn: string) => {
setLoading(true);
setError(null);
try {
const res = await fetchSensitiveLayers(p, search, useYn, categoryCode);
setItems(res.items);
setTotal(res.total);
setTotalPages(res.totalPages);
} catch {
setError('레이어 목록을 불러오지 못했습니다.');
} finally {
setLoading(false);
}
},
[categoryCode],
);
useEffect(() => {
setPage(1);
setAppliedSearch('');
setFilterUseYn('');
setSearchInput('');
}, [categoryCode]);
useEffect(() => {
load(page, appliedSearch, filterUseYn);
}, [load, page, appliedSearch, filterUseYn]);
const handleSearch = () => {
setAppliedSearch(searchInput);
setPage(1);
};
const handleToggle = async (layerCd: string) => {
if (toggling) return;
setToggling(layerCd);
try {
const result = await toggleLayerUse(layerCd);
setItems((prev) =>
prev.map((item) =>
item.layerCd === result.layerCd ? { ...item, useYn: result.useYn } : item,
),
);
} catch {
setError('사용여부 변경에 실패했습니다.');
} finally {
setToggling(null);
}
};
const buildPageButtons = () => {
const buttons: (number | 'ellipsis')[] = [];
const delta = 2;
const left = page - delta;
const right = page + delta;
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= left && i <= right)) {
buttons.push(i);
} else if (buttons[buttons.length - 1] !== 'ellipsis') {
buttons.push('ellipsis');
}
}
return buttons;
};
return (
<div className="flex flex-col h-full overflow-hidden">
{/* 헤더 */}
<div className="px-6 py-4 border-b border-stroke shrink-0">
<div className="flex items-center justify-between mb-3">
<div>
<h1 className="text-lg font-bold text-fg font-korean">{title}</h1>
<p className="text-xs text-fg-disabled mt-1 font-korean"> {total}</p>
</div>
</div>
<div className="flex items-center gap-2">
<input
type="text"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
placeholder="레이어코드 / 레이어명 검색"
className="flex-1 px-3 py-1.5 text-xs bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
/>
<select
value={filterUseYn}
onChange={(e) => setFilterUseYn(e.target.value)}
className="px-2 py-1.5 text-xs bg-bg-elevated border border-stroke rounded text-fg focus:border-color-accent focus:outline-none font-korean"
>
<option value=""></option>
<option value="Y"></option>
<option value="N"></option>
</select>
<button
onClick={handleSearch}
className="px-3 py-1.5 text-xs border border-stroke text-fg-sub rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
>
</button>
</div>
</div>
{/* 오류 메시지 */}
{error && (
<div className="px-6 py-2 text-xs text-red-400 bg-[rgba(239,68,68,0.05)] border-b border-stroke shrink-0 font-korean">
{error}
</div>
)}
{/* 테이블 영역 */}
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center h-full text-fg-disabled text-sm font-korean">
...
</div>
) : (
<table className="w-full">
<thead>
<tr className="border-b border-stroke bg-bg-surface sticky top-0 z-10">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean w-10 whitespace-nowrap">
</th>
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-mono">
</th>
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-center text-label-2 font-semibold text-fg-disabled font-korean w-12 whitespace-nowrap">
</th>
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-mono">
WMS레이어명
</th>
<th className="px-4 py-3 text-center text-label-2 font-semibold text-fg-disabled font-korean w-16">
</th>
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean w-28">
</th>
<th className="px-4 py-3 text-center text-label-2 font-semibold text-fg-disabled font-korean w-20">
</th>
</tr>
</thead>
<tbody>
{items.length === 0 ? (
<tr>
<td
colSpan={9}
className="px-4 py-12 text-center text-fg-disabled text-sm font-korean"
>
.
</td>
</tr>
) : (
items.map((item, idx) => (
<tr
key={item.layerCd}
className="border-b border-stroke hover:bg-[rgba(255,255,255,0.02)] transition-colors"
>
<td className="px-4 py-3 text-xs text-fg-disabled font-mono">
{(page - 1) * PAGE_SIZE + idx + 1}
</td>
<td className="px-4 py-3 text-label-2 text-fg-sub font-mono">{item.layerCd}</td>
<td className="px-4 py-3 text-xs text-fg font-korean">{item.layerNm}</td>
<td className="px-4 py-3 text-xs text-fg-sub font-korean max-w-[200px]">
<span className="block truncate" title={item.layerFullNm}>
{item.layerFullNm}
</span>
</td>
<td className="px-4 py-3 text-center">
<span className="inline-flex items-center justify-center w-5 h-5 rounded text-caption font-semibold bg-[rgba(6,182,212,0.1)] text-color-accent border border-[rgba(6,182,212,0.3)]">
{item.layerLevel}
</span>
</td>
<td className="px-4 py-3 text-label-2 text-fg-sub font-mono">
{item.wmsLayerNm ?? <span className="text-fg-disabled">-</span>}
</td>
<td className="px-4 py-3 text-xs text-fg-disabled text-center font-mono">
{item.sortOrd}
</td>
<td className="px-4 py-3 text-label-2 text-fg-disabled font-mono">
{item.regDtm ?? '-'}
</td>
<td className="px-4 py-3 text-center">
<button
onClick={() => handleToggle(item.layerCd)}
disabled={toggling === item.layerCd}
title={
item.useYn === 'Y'
? '사용 중 (클릭하여 비활성화)'
: '미사용 (클릭하여 활성화)'
}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:opacity-50 ${
item.useYn === 'Y'
? 'bg-color-accent'
: 'bg-[rgba(255,255,255,0.08)] border border-stroke'
}`}
>
<span
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow transition-transform ${
item.useYn === 'Y' ? 'translate-x-[18px]' : 'translate-x-0.5'
}`}
/>
</button>
</td>
</tr>
))
)}
</tbody>
</table>
)}
</div>
{/* 페이지네이션 */}
{!loading && totalPages > 1 && (
<div className="flex items-center justify-between px-6 py-3 border-t border-stroke bg-bg-surface shrink-0">
<span className="text-label-2 text-fg-disabled font-korean">
{(page - 1) * PAGE_SIZE + 1}{Math.min(page * PAGE_SIZE, total)} / {total}
</span>
<div className="flex items-center gap-1">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="px-2.5 py-1 text-label-2 border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
>
</button>
{buildPageButtons().map((btn, i) =>
btn === 'ellipsis' ? (
<span key={`e${i}`} className="px-1.5 text-label-2 text-fg-disabled">
</span>
) : (
<button
key={btn}
onClick={() => setPage(btn)}
className={`px-2.5 py-1 text-label-2 rounded transition-all ${
page === btn
? 'bg-color-accent text-bg-0 font-semibold shadow-[0_0_8px_rgba(6,182,212,0.25)]'
: 'border border-stroke text-fg-disabled hover:bg-[rgba(255,255,255,0.04)]'
}`}
>
{btn}
</button>
),
)}
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="px-2.5 py-1 text-label-2 border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
>
</button>
</div>
</div>
)}
</div>
);
};
export default SensitiveLayerPanel;