import { useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { Button } from '@shared/components/ui/button'; import { PageContainer, PageHeader } from '@shared/components/layout'; import type { BadgeIntent } from '@lib/theme/variants'; import { Settings, Database, Search, Map, Fish, Anchor, Ship, Globe, BarChart3, Download, Filter, RefreshCw, Hash, Info, } from 'lucide-react'; import { AREA_CODES, SPECIES_CODES, FISHERY_CODES, VESSEL_TYPE_CODES, CODE_STATS, getAreaMajors, getSpeciesMajors, getFisheryMajors, getVesselTypeMajors, filterByMajor, searchCodes, type AreaCode, type SpeciesCode, type FisheryCode, type VesselTypeCode, } from '@/data/commonCodes'; /* * SFR-02: 시스템 기본 환경설정 및 공통 기능 * - 공통코드 기준정보 조회·검색·필터링 * - 해역분류(52) / 어종(578) / 어업유형(59) / 선박유형(186) * - 시스템 기본 환경설정 관리 */ type CodeTab = 'areas' | 'species' | 'fishery' | 'vessels' | 'settings'; const TAB_ITEMS: { key: CodeTab; icon: React.ElementType; label: string; count?: number }[] = [ { key: 'areas', icon: Map, label: '해역분류', count: CODE_STATS.areas }, { key: 'species', icon: Fish, label: '어종', count: CODE_STATS.species }, { key: 'fishery', icon: Anchor, label: '어업유형', count: CODE_STATS.fishery }, { key: 'vessels', icon: Ship, label: '선박유형', count: CODE_STATS.vesselTypes }, { key: 'settings', icon: Settings, label: '환경설정' }, ]; const PAGE_SIZE = 30; // ─── 시스템 설정 기본값 ────────────────── const SYSTEM_SETTINGS = { general: [ { key: 'system_name', label: '시스템명', value: 'AI 기반 불법조업 탐지·차단 플랫폼', type: 'text' }, { key: 'org_name', label: '운영기관', value: '해양경찰청', type: 'text' }, { key: 'version', label: '시스템 버전', value: 'v1.0.0', type: 'text' }, { key: 'timezone', label: '시간대', value: 'Asia/Seoul (UTC+9)', type: 'text' }, { key: 'language', label: '기본 언어', value: '한국어 (ko-KR)', type: 'text' }, { key: 'date_format', label: '날짜 형식', value: 'YYYY-MM-DD HH:mm:ss', type: 'text' }, { key: 'coord_format', label: '좌표 형식', value: 'DD.DDDDDD (십진도)', type: 'select' }, ], map: [ { key: 'default_center', label: '기본 지도 중심', value: 'N35.5° E127.0°', type: 'text' }, { key: 'default_zoom', label: '기본 줌 레벨', value: '7', type: 'number' }, { key: 'map_provider', label: '지도 타일', value: 'CartoDB Dark Matter', type: 'select' }, { key: 'eez_layer', label: 'EEZ 경계선', value: '활성', type: 'toggle' }, { key: 'nll_layer', label: 'NLL 표시', value: '활성', type: 'toggle' }, { key: 'ais_refresh', label: 'AIS 갱신 주기', value: '10초', type: 'select' }, { key: 'trail_length', label: '항적 표시 기간', value: '24시간', type: 'select' }, ], alert: [ { key: 'eez_alert', label: 'EEZ 침범 경보', value: '활성', type: 'toggle' }, { key: 'dark_vessel', label: '다크베셀 경보', value: '활성', type: 'toggle' }, { key: 'mmsi_spoof', label: 'MMSI 변조 경보', value: '활성', type: 'toggle' }, { key: 'transfer_alert', label: '불법환적 경보', value: '활성', type: 'toggle' }, { key: 'speed_alert', label: '속도이상 경보', value: '활성', type: 'toggle' }, { key: 'alert_sound', label: '경보 사운드', value: '활성', type: 'toggle' }, { key: 'alert_retention', label: '경보 보존기간', value: '90일', type: 'select' }, ], data: [ { key: 'ais_source', label: 'AIS 데이터 출처', value: 'LRIT / S-AIS / T-AIS 통합', type: 'text' }, { key: 'vms_source', label: 'VMS 데이터 출처', value: '해수부 VMS 연동', type: 'text' }, { key: 'sat_source', label: '위성 데이터', value: 'SAR / 광학위성 연동', type: 'text' }, { key: 'data_retention', label: '데이터 보존기간', value: '5년', type: 'select' }, { key: 'backup_cycle', label: '백업 주기', value: '일 1회 (03:00)', type: 'select' }, { key: 'db_encryption', label: 'DB 암호화', value: 'AES-256', type: 'text' }, ], }; export function SystemConfig() { const { t } = useTranslation('admin'); const { t: tc } = useTranslation('common'); const [tab, setTab] = useState('areas'); const [query, setQuery] = useState(''); const [majorFilter, setMajorFilter] = useState(''); const [page, setPage] = useState(0); const [expandedRow, setExpandedRow] = useState(null); // 탭 변경 시 필터 초기화 const changeTab = (t: CodeTab) => { setTab(t); setQuery(''); setMajorFilter(''); setPage(0); setExpandedRow(null); }; // ─── 해역분류 필터링 ───────────────── const filteredAreas = useMemo(() => { const result = filterByMajor(AREA_CODES, majorFilter); return searchCodes(result, query); }, [query, majorFilter]); // ─── 어종 필터링 ────────────────────── const filteredSpecies = useMemo(() => { const result = filterByMajor(SPECIES_CODES, majorFilter); if (query) { const q = query.toLowerCase(); return result.filter( (s) => s.code.includes(q) || s.name.includes(q) || s.nameEn.toLowerCase().includes(q) ); } return result; }, [query, majorFilter]); // ─── 어업유형 필터링 ────────────────── const filteredFishery = useMemo(() => { const result = filterByMajor(FISHERY_CODES, majorFilter); return searchCodes(result, query); }, [query, majorFilter]); // ─── 선박유형 필터링 ────────────────── const filteredVessels = useMemo(() => { const result = filterByMajor(VESSEL_TYPE_CODES, majorFilter); if (query) { const q = query.toLowerCase(); return result.filter( (v) => v.code.toLowerCase().includes(q) || v.name.toLowerCase().includes(q) || v.aisCode.toLowerCase().includes(q) ); } return result; }, [query, majorFilter]); // ─── 현재 탭에 따른 데이터 ──────────── const currentData = tab === 'areas' ? filteredAreas : tab === 'species' ? filteredSpecies : tab === 'fishery' ? filteredFishery : tab === 'vessels' ? filteredVessels : []; const totalItems = currentData.length; const totalPages = Math.ceil(totalItems / PAGE_SIZE); const pagedData = currentData.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE); const majors = tab === 'areas' ? getAreaMajors() : tab === 'species' ? getSpeciesMajors() : tab === 'fishery' ? getFisheryMajors() : tab === 'vessels' ? getVesselTypeMajors() : []; return ( } /> {/* KPI 카드 */}
{[ { icon: Map, label: '해역분류', count: CODE_STATS.areas, color: 'text-label', bg: 'bg-blue-500/10', desc: '해양경찰청 관할 기준' }, { icon: Fish, label: '어종 코드', count: CODE_STATS.species, color: 'text-label', bg: 'bg-green-500/10', desc: '국립수산과학원 기준' }, { icon: Anchor, label: '어업유형', count: CODE_STATS.fishery, color: 'text-heading', bg: 'bg-purple-500/10', desc: '수산업법 허가·면허' }, { icon: Ship, label: '선박유형', count: CODE_STATS.vesselTypes, color: 'text-label', bg: 'bg-orange-500/10', desc: 'MDA 5개출처 통합' }, { icon: Globe, label: '전체 코드', count: CODE_STATS.total, color: 'text-label', bg: 'bg-cyan-500/10', desc: '공통코드 총계' }, ].map((kpi) => (
{kpi.count.toLocaleString()}
{kpi.label}
{kpi.desc}
))}
{/* 탭 */}
{TAB_ITEMS.map((t) => ( ))}
{/* 검색·필터 (코드 탭에서만) */} {tab !== 'settings' && (
{ setQuery(e.target.value); setPage(0); }} placeholder={ tab === 'areas' ? '코드번호, 해역명 검색...' : tab === 'species' ? '코드, 어종명, 영문명 검색...' : tab === 'fishery' ? '코드, 어업유형명 검색...' : '코드, 선박유형명, AIS코드 검색...' } className="w-full bg-surface-overlay border border-slate-700/50 rounded-lg pl-9 pr-4 py-2 text-[11px] text-label placeholder:text-hint focus:outline-none focus:border-cyan-500/50" />
{totalItems.toLocaleString()}건 {query && ` (검색: "${query}")`}
)} {/* ── 해역분류 테이블 ── */} {tab === 'areas' && ( {(pagedData as AreaCode[]).map((a) => ( ))}
코드 대분류 중분류 해역명 관할기관 비고
{a.code} {(() => { const intent: BadgeIntent = a.major === '서해' ? 'info' : a.major === '남해' ? 'success' : a.major === '동해' ? 'purple' : a.major === '제주' ? 'high' : 'cyan'; return {a.major}; })()} {a.mid} {a.name} {a.authority} {a.note}
)} {/* ── 어종 테이블 ── */} {tab === 'species' && ( {(pagedData as SpeciesCode[]).map((s) => ( setExpandedRow(expandedRow === s.code ? null : s.code)} > ))}
코드 대분류 중분류 어종명 영문명 서식해역 사용 낚시
{s.code} {s.major} {s.mid} {s.name} {s.nameEn} {s.area} {s.active ? Y : N } {s.fishing && }
)} {/* ── 어업유형 테이블 ── */} {tab === 'fishery' && ( {(pagedData as FisheryCode[]).map((f) => ( ))}
코드 대분류 중분류 어업유형명 주요 어획대상 허가 법적 근거
{f.code} {(() => { const intent: BadgeIntent = f.major === '근해어업' ? 'info' : f.major === '연안어업' ? 'success' : f.major === '양식어업' ? 'cyan' : f.major === '원양어업' ? 'purple' : f.major === '구획어업' ? 'high' : f.major === '마을어업' ? 'warning' : 'muted'; return {f.major}; })()} {f.mid} {f.name} {f.target} {f.permit} {f.law}
)} {/* ── 선박유형 테이블 ── */} {tab === 'vessels' && ( {(pagedData as VesselTypeCode[]).map((v) => ( ))}
코드 대분류 중분류 선박유형명 출처 톤수기준 주요용도 AIS코드
{v.code} {(() => { const intent: BadgeIntent = v.major === '어선' ? 'info' : v.major === '여객선' ? 'success' : v.major === '화물선' ? 'high' : v.major === '유조선' ? 'critical' : v.major === '관공선' ? 'purple' : v.major === '함정' ? 'cyan' : 'muted'; return {v.major}; })()} {v.mid} {v.name} {v.source} {v.tonnage} {v.purpose} {v.aisCode}
)} {/* 페이지네이션 (코드 탭에서만) */} {tab !== 'settings' && totalPages > 1 && (
{page + 1} / {totalPages} 페이지 ({totalItems.toLocaleString()}건)
)} {/* ── 환경설정 탭 ── */} {tab === 'settings' && (
{Object.entries(SYSTEM_SETTINGS).map(([section, items]) => { const sectionLabels: Record = { general: { title: '일반 설정', icon: Settings }, map: { title: '지도 설정', icon: Map }, alert: { title: '경보 설정', icon: BarChart3 }, data: { title: '데이터 설정', icon: Database }, }; const meta = sectionLabels[section] || { title: section, icon: Info }; return ( {meta.title} {items.map((item) => (
{item.label} {item.value}
))}
); })}
)}
); }