wing-ops/frontend/src/tabs/scat/components/PreScatView.tsx

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>
);
}