294 lines
9.6 KiB
TypeScript
Executable File
294 lines
9.6 KiB
TypeScript
Executable File
import { useState, useCallback, useEffect } from 'react';
|
|
import type { ScatSegment, ScatDetail } from './scatTypes';
|
|
import {
|
|
fetchOffices,
|
|
fetchSections,
|
|
fetchSectionDetail,
|
|
fetchZones,
|
|
fetchJurisdictions,
|
|
} from '../services/scatApi';
|
|
import type { ApiZoneItem } from '../services/scatApi';
|
|
import ScatLeftPanel from './ScatLeftPanel';
|
|
import ScatMap from './ScatMap';
|
|
import ScatPopup from './ScatPopup';
|
|
import ScatRightPanel from './ScatRightPanel';
|
|
|
|
// ═══ Main PreScatView ═══
|
|
|
|
export function PreScatView() {
|
|
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
|
const [rightCollapsed, setRightCollapsed] = useState(false);
|
|
const [segments, setSegments] = useState<ScatSegment[]>([]);
|
|
const [zones, setZones] = useState<ApiZoneItem[]>([]);
|
|
const [jurisdictions, setJurisdictions] = useState<string[]>([]);
|
|
const [offices, setOffices] = useState<string[]>([]);
|
|
const [selectedOffice, setSelectedOffice] = useState('');
|
|
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);
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const [timelineIdx, setTimelineIdx] = useState(6);
|
|
|
|
// 초기 관할청 목록 로딩
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
async function loadInit() {
|
|
try {
|
|
setLoading(true);
|
|
const officeList = await fetchOffices();
|
|
if (cancelled) return;
|
|
setOffices(officeList);
|
|
const defaultOffice = officeList.includes('제주청') ? '제주청' : officeList[0] || '';
|
|
setSelectedOffice(defaultOffice);
|
|
|
|
const jrsdList = await fetchJurisdictions(defaultOffice);
|
|
if (cancelled) return;
|
|
setJurisdictions(jrsdList);
|
|
setJurisdictionFilter('');
|
|
|
|
const [zonesData, sectionsData] = await Promise.all([
|
|
fetchZones(undefined, defaultOffice),
|
|
fetchSections({ officeCd: defaultOffice }),
|
|
]);
|
|
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);
|
|
}
|
|
}
|
|
loadInit();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
// 관할청 변경 시 관할구역 + zones + sections 재로딩
|
|
useEffect(() => {
|
|
if (offices.length === 0 || !selectedOffice) return;
|
|
let cancelled = false;
|
|
async function reload() {
|
|
try {
|
|
setLoading(true);
|
|
const jrsdList = await fetchJurisdictions(selectedOffice);
|
|
if (cancelled) return;
|
|
setJurisdictions(jrsdList);
|
|
setJurisdictionFilter('');
|
|
|
|
const [zonesData, sectionsData] = await Promise.all([
|
|
fetchZones(undefined, selectedOffice),
|
|
fetchSections({ officeCd: selectedOffice }),
|
|
]);
|
|
if (cancelled) return;
|
|
setZones(zonesData);
|
|
setSegments(sectionsData);
|
|
setAreaFilter('');
|
|
if (sectionsData.length > 0) setSelectedSeg(sectionsData[0]);
|
|
else setSelectedSeg(null);
|
|
} catch (err) {
|
|
console.error('[SCAT] 관할청 변경 오류:', err);
|
|
} finally {
|
|
if (!cancelled) setLoading(false);
|
|
}
|
|
}
|
|
reload();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [selectedOffice]);
|
|
|
|
// 관할구역 필터 변경 시 zones + sections 재로딩 (선택된 관할청 내에서 필터링)
|
|
useEffect(() => {
|
|
if (!selectedOffice || !jurisdictionFilter) return;
|
|
let cancelled = false;
|
|
async function reload() {
|
|
try {
|
|
setLoading(true);
|
|
const [zonesData, sectionsData] = await Promise.all([
|
|
fetchZones(jurisdictionFilter, selectedOffice),
|
|
fetchSections({ jurisdiction: jurisdictionFilter, officeCd: selectedOffice }),
|
|
]);
|
|
if (cancelled) return;
|
|
setZones(zonesData);
|
|
setSegments(sectionsData);
|
|
setAreaFilter('');
|
|
if (sectionsData.length > 0) setSelectedSeg(sectionsData[0]);
|
|
else setSelectedSeg(null);
|
|
} catch (err) {
|
|
console.error('[SCAT] 관할구역 변경 오류:', err);
|
|
} finally {
|
|
if (!cancelled) setLoading(false);
|
|
}
|
|
}
|
|
reload();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [jurisdictionFilter]);
|
|
|
|
// 선택 구간 변경 시 우측 패널 상세 로딩
|
|
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 handleOpenPopup = useCallback(async (sn: number) => {
|
|
try {
|
|
const detail = await fetchSectionDetail(sn);
|
|
setPopupData(detail);
|
|
} catch (err) {
|
|
console.error('[SCAT] 상세 데이터 로딩 오류:', err);
|
|
}
|
|
}, []);
|
|
|
|
const handleClosePopup = useCallback(() => {
|
|
setPopupData(null);
|
|
}, []);
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex w-full h-full bg-bg-base items-center justify-center flex-col gap-3">
|
|
<div className="text-color-danger text-body-2 font-korean">{error}</div>
|
|
<button
|
|
onClick={() => {
|
|
setError(null);
|
|
setLoading(true);
|
|
}}
|
|
className="px-4 py-1.5 bg-color-accent text-white text-caption rounded font-korean"
|
|
>
|
|
다시 시도
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (loading || !selectedSeg) {
|
|
return (
|
|
<div className="flex w-full h-full bg-bg-base items-center justify-center">
|
|
<div className="text-fg-sub text-body-2 font-korean">SCAT 데이터 로딩 중...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex w-full h-full bg-bg-base overflow-hidden">
|
|
{/* Left Panel */}
|
|
<div className="shrink-0 overflow-hidden" style={{ width: leftCollapsed ? 0 : 340 }}>
|
|
<ScatLeftPanel
|
|
segments={segments}
|
|
zones={zones}
|
|
jurisdictions={jurisdictions}
|
|
offices={offices}
|
|
selectedOffice={selectedOffice}
|
|
onOfficeChange={setSelectedOffice}
|
|
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>
|
|
|
|
<div className="flex-1 relative overflow-hidden">
|
|
{/* Left panel toggle button */}
|
|
<button
|
|
onClick={() => setLeftCollapsed((v) => !v)}
|
|
className="absolute z-[500] top-1/2 -translate-y-1/2 flex items-center justify-center text-[10px]"
|
|
style={{
|
|
left: 0,
|
|
width: 18,
|
|
height: 40,
|
|
background: 'var(--bg-elevated)',
|
|
border: '1px solid var(--stroke-default)',
|
|
borderLeft: 'none',
|
|
borderRadius: '0 6px 6px 0',
|
|
color: 'var(--fg-sub)',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
{leftCollapsed ? '▶' : '◀'}
|
|
</button>
|
|
|
|
{/* Right panel toggle button */}
|
|
<button
|
|
onClick={() => setRightCollapsed((v) => !v)}
|
|
className="absolute z-[500] top-1/2 -translate-y-1/2 flex items-center justify-center text-[10px]"
|
|
style={{
|
|
right: 0,
|
|
width: 18,
|
|
height: 40,
|
|
background: 'var(--bg-elevated)',
|
|
border: '1px solid var(--stroke-default)',
|
|
borderRight: 'none',
|
|
borderRadius: '6px 0 0 6px',
|
|
color: 'var(--fg-sub)',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
{rightCollapsed ? '◀' : '▶'}
|
|
</button>
|
|
|
|
<ScatMap
|
|
segments={segments}
|
|
zones={zones}
|
|
selectedSeg={selectedSeg}
|
|
jurisdictionFilter={jurisdictionFilter}
|
|
onSelectSeg={setSelectedSeg}
|
|
onOpenPopup={handleOpenPopup}
|
|
/>
|
|
{/* <ScatTimeline
|
|
segments={segments}
|
|
currentIdx={timelineIdx}
|
|
onSeek={handleTimelineSeek}
|
|
/> */}
|
|
</div>
|
|
|
|
{/* Right Panel */}
|
|
<div className="shrink-0 overflow-hidden" style={{ width: rightCollapsed ? 0 : 280 }}>
|
|
<ScatRightPanel detail={panelDetail} loading={panelLoading} />
|
|
</div>
|
|
|
|
{popupData && (
|
|
<ScatPopup data={popupData} segCode={selectedSeg.code} onClose={handleClosePopup} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|