- className 중복 속성 31건 수정 (12파일) - KOSPS codeBox spread TypeError 해결 - HNS 페놀(C₆H₅OH) 물질 데이터 추가 - ScatRightPanel 280px 우측 패널 신규 구현 (3탭+액션버튼) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
176 lines
5.7 KiB
TypeScript
Executable File
176 lines
5.7 KiB
TypeScript
Executable File
import { useState, useCallback, useEffect } from 'react';
|
|
import type { ScatSegment, ScatDetail } from './scatTypes';
|
|
import { fetchSections, fetchSectionDetail, fetchZones } from '../services/scatApi';
|
|
import type { ApiZoneItem } from '../services/scatApi';
|
|
import ScatLeftPanel from './ScatLeftPanel';
|
|
import ScatMap from './ScatMap';
|
|
import ScatTimeline from './ScatTimeline';
|
|
import ScatPopup from './ScatPopup';
|
|
import ScatRightPanel from './ScatRightPanel';
|
|
|
|
// ═══ Main PreScatView ═══
|
|
|
|
export function PreScatView() {
|
|
const [segments, setSegments] = useState<ScatSegment[]>([]);
|
|
const [zones, setZones] = useState<ApiZoneItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [selectedSeg, setSelectedSeg] = useState<ScatSegment | null>(null);
|
|
const [jurisdictionFilter, setJurisdictionFilter] = useState('전체 (제주도)');
|
|
const [areaFilter, setAreaFilter] = useState('전체');
|
|
const [phaseFilter, setPhaseFilter] = useState('Pre-SCAT (사전조사)');
|
|
const [statusFilter, setStatusFilter] = useState('전체');
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [popupData, setPopupData] = useState<ScatDetail | null>(null);
|
|
const [panelDetail, setPanelDetail] = useState<ScatDetail | null>(null);
|
|
const [panelLoading, setPanelLoading] = useState(false);
|
|
const [timelineIdx, setTimelineIdx] = useState(6);
|
|
|
|
// API에서 구역 및 구간 데이터 로딩
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
async function loadData() {
|
|
try {
|
|
setLoading(true);
|
|
const [zonesData, sectionsData] = await Promise.all([fetchZones(), fetchSections()]);
|
|
if (cancelled) return;
|
|
setZones(zonesData);
|
|
setSegments(sectionsData);
|
|
if (sectionsData.length > 0) {
|
|
setSelectedSeg(sectionsData[0]);
|
|
}
|
|
} catch (err) {
|
|
console.error('[SCAT] 데이터 로딩 오류:', err);
|
|
if (!cancelled) setError('데이터를 불러오지 못했습니다.');
|
|
} finally {
|
|
if (!cancelled) setLoading(false);
|
|
}
|
|
}
|
|
|
|
loadData();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
// 선택 구간 변경 시 우측 패널 상세 로딩
|
|
useEffect(() => {
|
|
if (!selectedSeg) {
|
|
setPanelDetail(null);
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
setPanelLoading(true);
|
|
fetchSectionDetail(selectedSeg.id)
|
|
.then(detail => { if (!cancelled) setPanelDetail(detail); })
|
|
.catch(err => console.error('[SCAT] 패널 상세 로딩 오류:', err))
|
|
.finally(() => { if (!cancelled) setPanelLoading(false); });
|
|
return () => { cancelled = true; };
|
|
}, [selectedSeg]);
|
|
|
|
// 관할 기반 세그먼트 필터링
|
|
const filteredSegments = segments.filter((s) => {
|
|
if (jurisdictionFilter === '서귀포해양경비안전서') return s.jurisdiction === '서귀포';
|
|
if (jurisdictionFilter === '제주해양경비안전서') return s.jurisdiction === '제주';
|
|
return true; // 전체
|
|
});
|
|
|
|
const handleOpenPopup = useCallback(async (sn: number) => {
|
|
try {
|
|
const detail = await fetchSectionDetail(sn);
|
|
setPopupData(detail);
|
|
} catch (err) {
|
|
console.error('[SCAT] 상세 데이터 로딩 오류:', err);
|
|
}
|
|
}, []);
|
|
|
|
const handleClosePopup = useCallback(() => {
|
|
setPopupData(null);
|
|
}, []);
|
|
|
|
const handleTimelineSeek = useCallback(
|
|
(idx: number) => {
|
|
if (idx === -1) {
|
|
// advance signal from play
|
|
setTimelineIdx((prev) => {
|
|
const next = (prev + 1) % Math.min(filteredSegments.length, 12);
|
|
if (filteredSegments[next]) setSelectedSeg(filteredSegments[next]);
|
|
return next;
|
|
});
|
|
} else {
|
|
setTimelineIdx(idx);
|
|
if (filteredSegments[idx]) setSelectedSeg(filteredSegments[idx]);
|
|
}
|
|
},
|
|
[filteredSegments],
|
|
);
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex w-full h-full bg-bg-0 items-center justify-center flex-col gap-3">
|
|
<div className="text-status-red text-sm font-korean">{error}</div>
|
|
<button
|
|
onClick={() => { setError(null); setLoading(true); }}
|
|
className="px-4 py-1.5 bg-primary-cyan text-white text-xs rounded font-korean"
|
|
>
|
|
다시 시도
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (loading || !selectedSeg) {
|
|
return (
|
|
<div className="flex w-full h-full bg-bg-0 items-center justify-center">
|
|
<div className="text-text-2 text-sm font-korean">SCAT 데이터 로딩 중...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex w-full h-full bg-bg-0 overflow-hidden">
|
|
<ScatLeftPanel
|
|
segments={filteredSegments}
|
|
zones={zones}
|
|
selectedSeg={selectedSeg}
|
|
onSelectSeg={setSelectedSeg}
|
|
onOpenPopup={handleOpenPopup}
|
|
jurisdictionFilter={jurisdictionFilter}
|
|
onJurisdictionChange={setJurisdictionFilter}
|
|
areaFilter={areaFilter}
|
|
onAreaChange={setAreaFilter}
|
|
phaseFilter={phaseFilter}
|
|
onPhaseChange={setPhaseFilter}
|
|
statusFilter={statusFilter}
|
|
onStatusChange={setStatusFilter}
|
|
searchTerm={searchTerm}
|
|
onSearchChange={setSearchTerm}
|
|
/>
|
|
|
|
<div className="flex-1 relative">
|
|
<ScatMap
|
|
segments={filteredSegments}
|
|
selectedSeg={selectedSeg}
|
|
onSelectSeg={setSelectedSeg}
|
|
onOpenPopup={handleOpenPopup}
|
|
/>
|
|
<ScatTimeline
|
|
segments={filteredSegments}
|
|
currentIdx={timelineIdx}
|
|
onSeek={handleTimelineSeek}
|
|
/>
|
|
</div>
|
|
|
|
<ScatRightPanel
|
|
detail={panelDetail}
|
|
loading={panelLoading}
|
|
/>
|
|
|
|
{popupData && (
|
|
<ScatPopup data={popupData} segCode={selectedSeg.code} onClose={handleClosePopup} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|