import { useEffect, useSyncExternalStore } from 'react' import type { MainTab } from '../types/navigation' import { useAuthStore } from '@common/store/authStore' import { API_BASE_URL } from '@common/services/api' interface SubMenuItem { id: string label: string icon: string } // 메인 탭별 서브 메뉴 설정 const subMenuConfigs: Record = { hns: [ { id: 'analysis', label: '대기확산 분석', icon: '🧪' }, { id: 'list', label: '분석 목록', icon: '📋' }, { id: 'scenario', label: '시나리오 관리', icon: '📊' }, { id: 'manual', label: 'HNS 대응매뉴얼', icon: '📖' }, { id: 'theory', label: '확산모델 이론', icon: '📐' }, { id: 'substance', label: 'HNS 물질정보', icon: '🧬' } ], prediction: [ { id: 'analysis', label: '유출유 확산분석', icon: '🔬' }, { id: 'list', label: '분석 목록', icon: '📋' }, { id: 'theory', label: '유출유확산모델 이론', icon: '📐' }, { id: 'boom-theory', label: '오일펜스 배치 알고리즘 이론', icon: '🛡️' } ], rescue: [ { id: 'rescue', label: '긴급구난예측', icon: '🚨' }, { id: 'list', label: '긴급구난 목록', icon: '📋' }, { id: 'scenario', label: '시나리오 관리', icon: '📊' }, { id: 'theory', label: '긴급구난모델 이론', icon: '📚' } ], reports: [ { id: 'report-list', label: '보고서 목록', icon: '📋' }, { id: 'template', label: '표준보고서 템플릿', icon: '📝' }, { id: 'generate', label: '보고서 생성', icon: '🔄' } ], aerial: [ { id: 'media', label: '영상사진관리', icon: '📷' }, { id: 'analysis', label: '영상사진합성', icon: '🧩' }, { id: 'realtime', label: '실시간드론', icon: '🛸' }, { id: 'satellite', label: '위성영상', icon: '🛰' }, { id: 'cctv', label: 'CCTV 조회', icon: '📹' }, { id: 'spectral', label: 'AI 탐지/분석', icon: '🤖' }, { id: 'sensor', label: '오염/선박3D분석', icon: '🔍' }, { id: 'theory', label: '항공탐색 이론', icon: '📐' } ], assets: null, scat: [ { id: 'survey', label: '해안오염 조사 평가', icon: '📋' }, { id: 'distribution', label: '해양오염분포도', icon: '🗺' }, { id: 'pre-scat', label: 'Pre-SCAT', icon: '🔍' } ], incidents: null, board: [ { id: 'all', label: '전체', icon: '📋' }, { id: 'notice', label: '공지사항', icon: '📢' }, { id: 'data', label: '자료실', icon: '📂' }, { id: 'qna', label: 'Q&A', icon: '❓' }, { id: 'manual', label: '해경매뉴얼', icon: '📘' } ], weather: null, admin: null // 관리자 화면은 자체 사이드바 사용 (AdminSidebar.tsx) } // 전역 상태 관리 (간단한 방식) const subMenuState: Record = { hns: 'analysis', prediction: 'analysis', rescue: 'rescue', reports: 'report-list', aerial: 'media', assets: '', scat: 'survey', incidents: '', board: 'all', weather: '', admin: 'users' } const listeners: Set<() => void> = new Set() function setSubTab(mainTab: MainTab, subTab: string) { subMenuState[mainTab] = subTab listeners.forEach(listener => listener()) } function subscribe(listener: () => void) { listeners.add(listener) return () => { listeners.delete(listener) } } export function useSubMenu(mainTab: MainTab) { const activeSubTab = useSyncExternalStore(subscribe, () => subMenuState[mainTab]) const isAuthenticated = useAuthStore((s) => s.isAuthenticated) const hasPermission = useAuthStore((s) => s.hasPermission) const setActiveSubTab = (subTab: string) => { setSubTab(mainTab, subTab) } // 권한 기반 서브메뉴 필터링 const rawConfig = subMenuConfigs[mainTab] const filteredConfig = rawConfig?.filter(item => hasPermission(`${mainTab}:${item.id}`) ) ?? null // 서브탭 전환 시 자동 감사 로그 (N-depth 지원: 콜론 구분 경로) useEffect(() => { if (!isAuthenticated || !activeSubTab) return const resourcePath = `${mainTab}:${activeSubTab}` const blob = new Blob( [JSON.stringify({ action: 'SUBTAB_VIEW', detail: resourcePath })], { type: 'text/plain' }, ) navigator.sendBeacon(`${API_BASE_URL}/audit/log`, blob) }, [mainTab, activeSubTab, isAuthenticated]) return { activeSubTab, setActiveSubTab, subMenuConfig: filteredConfig, } } // ─── 글로벌 메인탭 전환 (크로스 뷰 네비게이션) ───────────── type MainTabListener = (tab: MainTab) => void let mainTabListener: MainTabListener | null = null /** App.tsx에서 호출하여 글로벌 탭 전환 리스너 등록 */ export function registerMainTabSwitcher(fn: MainTabListener) { mainTabListener = fn } /** 어느 컴포넌트에서든 메인탭 + 서브탭을 한번에 전환 */ export function navigateToTab(mainTab: MainTab, subTab?: string) { if (subTab) setSubTab(mainTab, subTab) if (mainTabListener) mainTabListener(mainTab) } // ─── 보고서 생성 카테고리 힌트 ────────────────────────── /** 보고서 생성 탭으로 이동 시 초기 카테고리 (0=유출유, 1=HNS, 2=긴급구난) */ let _reportGenCategory: number | null = null export function setReportGenCategory(cat: number | null) { _reportGenCategory = cat } export function consumeReportGenCategory(): number | null { const v = _reportGenCategory _reportGenCategory = null return v } // ─── HNS 보고서 실 데이터 전달 ────────────────────────── export interface HnsReportPayload { mapImageDataUrl: string | null; substance: { name: string; un?: string; cas?: string; class?: string; toxicity: string }; hazard: { aegl3: string; aegl2: string; aegl1: string }; atm: { model: string; maxDistance: string }; weather: { windDir: string; windSpeed: string; stability: string; temperature: string }; maxConcentration: string; aeglAreas: { aegl1: string; aegl2: string; aegl3: string }; } let _hnsReportPayload: HnsReportPayload | null = null; export function setHnsReportPayload(d: HnsReportPayload | null) { _hnsReportPayload = d; } export function consumeHnsReportPayload(): HnsReportPayload | null { const v = _hnsReportPayload; _hnsReportPayload = null; return v; } // ─── 유출유 예측 보고서 실 데이터 전달 ────────────────────────── export interface OilReportMapParticle { lat: number; lon: number; time: number; particle?: number; stranded?: 0 | 1; } export interface OilReportPayload { incident: { name: string; occurTime: string; location: string; lat: number | null; lon: number | null; pollutant: string; spillAmount: string; shipName: string; }; pollution: { spillAmount: string; weathered: string; seaRemain: string; pollutionArea: string; coastAttach: string; coastLength: string; oilType: string; }; weather: { windDir: string; windSpeed: string; waveHeight: string; temp: string; pressure?: string; visibility?: string; salinity?: string; waveMaxHeight?: string; wavePeriod?: string; currentDir?: string; currentSpeed?: string; } | null; spread: { kosps: string; openDrift: string; poseidon: string; }; coastal: { firstTime: string | null; }; spreadSteps?: Array<{ elapsed: string; weathered: string; seaRemain: string; coastAttach: string; area: string; }>; hasSimulation: boolean; mapData: { center: [number, number]; zoom: number; trajectory: OilReportMapParticle[]; currentStep: number; centerPoints: { lat: number; lon: number; time: number }[]; simulationStartTime: string; } | null; } let _oilReportPayload: OilReportPayload | null = null; export function setOilReportPayload(d: OilReportPayload | null) { _oilReportPayload = d; } export function consumeOilReportPayload(): OilReportPayload | null { const v = _oilReportPayload; _oilReportPayload = null; return v; } export { subMenuState }