feat(prediction): GSC 외부 사고 목록 API 연동 및 셀렉트박스 자동 채움 (prediction/hns/rescue)

This commit is contained in:
jeonghyo.k 2026-04-14 17:11:38 +09:00
부모 8a0e5daf60
커밋 15ca946a00
11개의 변경된 파일217개의 추가작업 그리고 54개의 파일을 삭제

파일 보기

@ -0,0 +1,20 @@
import { Router } from 'express';
import { requireAuth } from '../auth/authMiddleware.js';
import { listGscAccidents } from './gscAccidentsService.js';
const router = Router();
// ============================================================
// GET /api/gsc/accidents — 외부 수집 사고 목록 (최신 20건)
// ============================================================
router.get('/', requireAuth, async (_req, res) => {
try {
const accidents = await listGscAccidents(20);
res.json(accidents);
} catch (err) {
console.error('[gsc] 사고 목록 조회 오류:', err);
res.status(500).json({ error: '사고 목록 조회 중 오류가 발생했습니다.' });
}
});
export default router;

파일 보기

@ -0,0 +1,65 @@
import { wingPool } from '../db/wingDb.js';
export interface GscAccidentListItem {
acdntMngNo: string;
pollNm: string;
pollDate: string | null;
lat: number | null;
lon: number | null;
}
const ACDNT_ASORT_CODES = [
'055001001',
'055001002',
'055001003',
'055001004',
'055001005',
'055001006',
'055003001',
'055003002',
'055003003',
'055003004',
'055003005',
'055004003',
];
export async function listGscAccidents(limit = 20): Promise<GscAccidentListItem[]> {
const sql = `
SELECT DISTINCT ON (a.acdnt_mng_no)
a.acdnt_mng_no AS "acdntMngNo",
a.acdnt_title AS "pollNm",
to_char(a.rcept_dt, 'YYYY-MM-DD"T"HH24:MI') AS "pollDate",
a.rcept_dt AS "rceptDt",
b.la AS "lat",
b.lo AS "lon"
FROM gsc.tgs_acdnt_info AS a
LEFT JOIN gsc.tgs_acdnt_lc AS b
ON a.acdnt_mng_no = b.acdnt_mng_no
WHERE a.acdnt_asort_code = ANY($1::varchar[])
AND a.acdnt_title IS NOT NULL
ORDER BY a.acdnt_mng_no, b.acdnt_lc_sn ASC
`;
const orderedSql = `
SELECT "acdntMngNo", "pollNm", "pollDate", "lat", "lon"
FROM (${sql}) t
ORDER BY t."rceptDt" DESC NULLS LAST
LIMIT $2
`;
const result = await wingPool.query<{
acdntMngNo: string;
pollNm: string;
pollDate: string | null;
lat: string | null;
lon: string | null;
}>(orderedSql, [ACDNT_ASORT_CODES, limit]);
return result.rows.map((row) => ({
acdntMngNo: row.acdntMngNo,
pollNm: row.pollNm,
pollDate: row.pollDate,
lat: row.lat != null ? Number(row.lat) : null,
lon: row.lon != null ? Number(row.lon) : null,
}));
}

파일 보기

@ -19,6 +19,7 @@ import hnsRouter from './hns/hnsRouter.js'
import reportsRouter from './reports/reportsRouter.js' import reportsRouter from './reports/reportsRouter.js'
import assetsRouter from './assets/assetsRouter.js' import assetsRouter from './assets/assetsRouter.js'
import incidentsRouter from './incidents/incidentsRouter.js' import incidentsRouter from './incidents/incidentsRouter.js'
import gscAccidentsRouter from './gsc/gscAccidentsRouter.js'
import scatRouter from './scat/scatRouter.js' import scatRouter from './scat/scatRouter.js'
import predictionRouter from './prediction/predictionRouter.js' import predictionRouter from './prediction/predictionRouter.js'
import aerialRouter from './aerial/aerialRouter.js' import aerialRouter from './aerial/aerialRouter.js'
@ -168,6 +169,7 @@ app.use('/api/hns', hnsRouter)
app.use('/api/reports', reportsRouter) app.use('/api/reports', reportsRouter)
app.use('/api/assets', assetsRouter) app.use('/api/assets', assetsRouter)
app.use('/api/incidents', incidentsRouter) app.use('/api/incidents', incidentsRouter)
app.use('/api/gsc/accidents', gscAccidentsRouter)
app.use('/api/scat', scatRouter) app.use('/api/scat', scatRouter)
app.use('/api/prediction', predictionRouter) app.use('/api/prediction', predictionRouter)
app.use('/api/aerial', aerialRouter) app.use('/api/aerial', aerialRouter)

파일 보기

@ -3,8 +3,8 @@ import { ComboBox } from '@common/components/ui/ComboBox';
import { useWeatherFetch } from '../hooks/useWeatherFetch'; import { useWeatherFetch } from '../hooks/useWeatherFetch';
import { getSubstanceToxicity } from '../utils/toxicityData'; import { getSubstanceToxicity } from '../utils/toxicityData';
import type { WeatherFetchResult, ReleaseType } from '../utils/dispersionTypes'; import type { WeatherFetchResult, ReleaseType } from '../utils/dispersionTypes';
import { fetchIncidentsRaw } from '@tabs/incidents/services/incidentsApi'; import { fetchGscAccidents } from '@tabs/prediction/services/predictionApi';
import type { IncidentListItem } from '@tabs/incidents/services/incidentsApi'; import type { GscAccidentListItem } from '@tabs/prediction/services/predictionApi';
/** HNS 분석 입력 파라미터 (부모에 전달) */ /** HNS 분석 입력 파라미터 (부모에 전달) */
export interface HNSInputParams { export interface HNSInputParams {
@ -44,6 +44,7 @@ interface HNSLeftPanelProps {
onParamsChange?: (params: HNSInputParams) => void; onParamsChange?: (params: HNSInputParams) => void;
onReset?: () => void; onReset?: () => void;
loadedParams?: Partial<HNSInputParams> | null; loadedParams?: Partial<HNSInputParams> | null;
onFlyToCoord?: (coord: { lon: number; lat: number }) => void;
} }
/** 십진 좌표 → 도분초 변환 */ /** 십진 좌표 → 도분초 변환 */
@ -67,8 +68,9 @@ export function HNSLeftPanel({
onParamsChange, onParamsChange,
onReset, onReset,
loadedParams, loadedParams,
onFlyToCoord,
}: HNSLeftPanelProps) { }: HNSLeftPanelProps) {
const [incidents, setIncidents] = useState<IncidentListItem[]>([]); const [incidents, setIncidents] = useState<GscAccidentListItem[]>([]);
const [selectedIncidentSn, setSelectedIncidentSn] = useState(''); const [selectedIncidentSn, setSelectedIncidentSn] = useState('');
const [expandedSections, setExpandedSections] = useState({ accident: true, params: true }); const [expandedSections, setExpandedSections] = useState({ accident: true, params: true });
const toggleSection = (key: 'accident' | 'params') => const toggleSection = (key: 'accident' | 'params') =>
@ -138,21 +140,26 @@ export function HNSLeftPanel({
// 사고 목록 불러오기 (마운트 시 1회, ref 초기화 패턴) // 사고 목록 불러오기 (마운트 시 1회, ref 초기화 패턴)
const incidentsPromiseRef = useRef<Promise<void> | null>(null); const incidentsPromiseRef = useRef<Promise<void> | null>(null);
if (incidentsPromiseRef.current == null) { if (incidentsPromiseRef.current == null) {
incidentsPromiseRef.current = fetchIncidentsRaw() incidentsPromiseRef.current = fetchGscAccidents()
.then((data) => setIncidents(data)) .then((data) => setIncidents(data))
.catch(() => setIncidents([])); .catch(() => setIncidents([]));
} }
// 사고 선택 시 필드 자동 채움 // 사고 선택 시 필드 자동 채움
const handleSelectIncident = (snStr: string) => { const handleSelectIncident = (mngNo: string) => {
setSelectedIncidentSn(snStr); setSelectedIncidentSn(mngNo);
const sn = parseInt(snStr); const incident = incidents.find((i) => i.acdntMngNo === mngNo);
const incident = incidents.find((i) => i.acdntSn === sn);
if (!incident) return; if (!incident) return;
setAccidentName(incident.acdntNm); setAccidentName(incident.pollNm);
if (incident.lat && incident.lng) { if (incident.pollDate) {
onCoordChange({ lat: incident.lat, lon: incident.lng }); const [d, t] = incident.pollDate.split('T');
if (d) setAccidentDate(d);
if (t) setAccidentTime(t);
}
if (incident.lat != null && incident.lon != null) {
onCoordChange({ lat: incident.lat, lon: incident.lon });
onFlyToCoord?.({ lat: incident.lat, lon: incident.lon });
} }
}; };
@ -266,8 +273,8 @@ export function HNSLeftPanel({
onChange={handleSelectIncident} onChange={handleSelectIncident}
placeholder="또는 사고 리스트에서 선택" placeholder="또는 사고 리스트에서 선택"
options={incidents.map((inc) => ({ options={incidents.map((inc) => ({
value: String(inc.acdntSn), value: inc.acdntMngNo,
label: `${inc.acdntNm} (${new Date(inc.occrnDtm).toLocaleDateString('ko-KR')})`, label: `${inc.pollNm} (${inc.pollDate ? inc.pollDate.replace('T', ' ') : '-'})`,
}))} }))}
/> />

파일 보기

@ -265,6 +265,7 @@ export function HNSView() {
const [leftCollapsed, setLeftCollapsed] = useState(false); const [leftCollapsed, setLeftCollapsed] = useState(false);
const [rightCollapsed, setRightCollapsed] = useState(false); const [rightCollapsed, setRightCollapsed] = useState(false);
const [incidentCoord, setIncidentCoord] = useState<{ lon: number; lat: number } | null>(null); const [incidentCoord, setIncidentCoord] = useState<{ lon: number; lat: number } | null>(null);
const [flyToCoord, setFlyToCoord] = useState<{ lon: number; lat: number } | undefined>(undefined);
const [isSelectingLocation, setIsSelectingLocation] = useState(false); const [isSelectingLocation, setIsSelectingLocation] = useState(false);
const [isRunningPrediction, setIsRunningPrediction] = useState(false); const [isRunningPrediction, setIsRunningPrediction] = useState(false);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -904,6 +905,7 @@ export function HNSView() {
onParamsChange={handleParamsChange} onParamsChange={handleParamsChange}
onReset={handleReset} onReset={handleReset}
loadedParams={loadedParams} loadedParams={loadedParams}
onFlyToCoord={(c) => setFlyToCoord({ lat: c.lat, lon: c.lon })}
/> />
</div> </div>
)} )}
@ -963,6 +965,8 @@ export function HNSView() {
<> <>
<MapView <MapView
incidentCoord={incidentCoord ?? undefined} incidentCoord={incidentCoord ?? undefined}
flyToIncident={flyToCoord}
onIncidentFlyEnd={() => setFlyToCoord(undefined)}
isSelectingLocation={isSelectingLocation} isSelectingLocation={isSelectingLocation}
onMapClick={handleMapClick} onMapClick={handleMapClick}
oilTrajectory={[]} oilTrajectory={[]}

파일 보기

@ -112,6 +112,7 @@ export function LeftPanel({
onLayerColorChange, onLayerColorChange,
sensitiveResources = [], sensitiveResources = [],
onImageAnalysisResult, onImageAnalysisResult,
onFlyToCoord,
validationErrors, validationErrors,
}: LeftPanelProps) { }: LeftPanelProps) {
const [expandedSections, setExpandedSections] = useState<ExpandedSections>({ const [expandedSections, setExpandedSections] = useState<ExpandedSections>({
@ -166,6 +167,7 @@ export function LeftPanel({
spillUnit={spillUnit} spillUnit={spillUnit}
onSpillUnitChange={onSpillUnitChange} onSpillUnitChange={onSpillUnitChange}
onImageAnalysisResult={onImageAnalysisResult} onImageAnalysisResult={onImageAnalysisResult}
onFlyToCoord={onFlyToCoord}
validationErrors={validationErrors} validationErrors={validationErrors}
/> />

파일 보기

@ -1208,6 +1208,9 @@ export function OilSpillView() {
onLayerColorChange={(id, color) => setLayerColors((prev) => ({ ...prev, [id]: color }))} onLayerColorChange={(id, color) => setLayerColors((prev) => ({ ...prev, [id]: color }))}
sensitiveResources={sensitiveResourceCategories} sensitiveResources={sensitiveResourceCategories}
onImageAnalysisResult={handleImageAnalysisResult} onImageAnalysisResult={handleImageAnalysisResult}
onFlyToCoord={(c: { lon: number; lat: number }) =>
setFlyToCoord({ lat: c.lat, lon: c.lon })
}
validationErrors={validationErrors} validationErrors={validationErrors}
/> />
</div> </div>

파일 보기

@ -1,8 +1,8 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { ComboBox } from '@common/components/ui/ComboBox'; import { ComboBox } from '@common/components/ui/ComboBox';
import type { PredictionModel } from './OilSpillView'; import type { PredictionModel } from './OilSpillView';
import { analyzeImage } from '../services/predictionApi'; import { analyzeImage, fetchGscAccidents } from '../services/predictionApi';
import type { ImageAnalyzeResult } from '../services/predictionApi'; import type { ImageAnalyzeResult, GscAccidentListItem } from '../services/predictionApi';
interface PredictionInputSectionProps { interface PredictionInputSectionProps {
expanded: boolean; expanded: boolean;
@ -33,6 +33,7 @@ interface PredictionInputSectionProps {
spillUnit: string; spillUnit: string;
onSpillUnitChange: (unit: string) => void; onSpillUnitChange: (unit: string) => void;
onImageAnalysisResult?: (result: ImageAnalyzeResult) => void; onImageAnalysisResult?: (result: ImageAnalyzeResult) => void;
onFlyToCoord?: (coord: { lon: number; lat: number }) => void;
validationErrors?: Set<string>; validationErrors?: Set<string>;
} }
@ -64,6 +65,7 @@ const PredictionInputSection = ({
spillUnit, spillUnit,
onSpillUnitChange, onSpillUnitChange,
onImageAnalysisResult, onImageAnalysisResult,
onFlyToCoord,
validationErrors, validationErrors,
}: PredictionInputSectionProps) => { }: PredictionInputSectionProps) => {
const [inputMode, setInputMode] = useState<'direct' | 'upload'>('direct'); const [inputMode, setInputMode] = useState<'direct' | 'upload'>('direct');
@ -71,8 +73,41 @@ const PredictionInputSection = ({
const [isAnalyzing, setIsAnalyzing] = useState(false); const [isAnalyzing, setIsAnalyzing] = useState(false);
const [analyzeError, setAnalyzeError] = useState<string | null>(null); const [analyzeError, setAnalyzeError] = useState<string | null>(null);
const [analyzeResult, setAnalyzeResult] = useState<ImageAnalyzeResult | null>(null); const [analyzeResult, setAnalyzeResult] = useState<ImageAnalyzeResult | null>(null);
const [gscAccidents, setGscAccidents] = useState<GscAccidentListItem[]>([]);
const [selectedGscMngNo, setSelectedGscMngNo] = useState<string>('');
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
let cancelled = false;
fetchGscAccidents()
.then((list) => {
if (!cancelled) setGscAccidents(list);
})
.catch((err) => {
console.error('[prediction] GSC 사고 목록 조회 실패:', err);
});
return () => {
cancelled = true;
};
}, []);
const handleGscAccidentSelect = (mngNo: string) => {
setSelectedGscMngNo(mngNo);
const item = gscAccidents.find((a) => a.acdntMngNo === mngNo);
if (!item) return;
onIncidentNameChange(item.pollNm);
if (item.pollDate) onAccidentTimeChange(item.pollDate);
if (item.lat != null && item.lon != null) {
onCoordChange({ lat: item.lat, lon: item.lon });
onFlyToCoord?.({ lat: item.lat, lon: item.lon });
}
};
const gscOptions = gscAccidents.map((a) => ({
value: a.acdntMngNo,
label: `${a.pollNm} (${a.pollDate ? a.pollDate.replace('T', ' ') : '-'})`,
}));
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] ?? null; const file = e.target.files?.[0] ?? null;
setUploadedFile(file); setUploadedFile(file);
@ -161,7 +196,13 @@ const PredictionInputSection = ({
: undefined : undefined
} }
/> />
<input className="prd-i" placeholder="또는 사고 리스트에서 선택" readOnly /> <ComboBox
className="prd-i"
value={selectedGscMngNo}
onChange={handleGscAccidentSelect}
options={gscOptions}
placeholder="또는 사고 리스트에서 선택"
/>
{/* Image Upload Mode */} {/* Image Upload Mode */}
{inputMode === 'upload' && ( {inputMode === 'upload' && (

파일 보기

@ -60,6 +60,8 @@ export interface LeftPanelProps {
sensitiveResources?: SensitiveResourceCategory[]; sensitiveResources?: SensitiveResourceCategory[];
// 이미지 분석 결과 콜백 // 이미지 분석 결과 콜백
onImageAnalysisResult?: (result: ImageAnalyzeResult) => void; onImageAnalysisResult?: (result: ImageAnalyzeResult) => void;
// 사고 리스트 선택 시 지도 이동 콜백
onFlyToCoord?: (coord: { lon: number; lat: number }) => void;
// 유효성 검증 에러 필드 // 유효성 검증 에러 필드
validationErrors?: Set<string>; validationErrors?: Set<string>;
} }

파일 보기

@ -321,3 +321,20 @@ export const analyzeImage = async (file: File, acdntNm?: string): Promise<ImageA
}); });
return response.data; return response.data;
}; };
// ============================================================
// GSC 외부 수집 사고 목록 (확산 예측 입력 셀렉트용)
// ============================================================
export interface GscAccidentListItem {
acdntMngNo: string;
pollNm: string;
pollDate: string | null;
lat: number | null;
lon: number | null;
}
export const fetchGscAccidents = async (): Promise<GscAccidentListItem[]> => {
const response = await api.get<GscAccidentListItem[]>('/gsc/accidents');
return response.data;
};

파일 보기

@ -5,8 +5,8 @@ import { RescueTheoryView } from './RescueTheoryView';
import { RescueScenarioView } from './RescueScenarioView'; import { RescueScenarioView } from './RescueScenarioView';
import { fetchRescueOps } from '../services/rescueApi'; import { fetchRescueOps } from '../services/rescueApi';
import type { RescueOpsItem } from '../services/rescueApi'; import type { RescueOpsItem } from '../services/rescueApi';
import { fetchIncidentsRaw } from '@tabs/incidents/services/incidentsApi'; import { fetchGscAccidents } from '@tabs/prediction/services/predictionApi';
import type { IncidentListItem } from '@tabs/incidents/services/incidentsApi'; import type { GscAccidentListItem } from '@tabs/prediction/services/predictionApi';
/* ─── Types ─── */ /* ─── Types ─── */
type AccidentType = type AccidentType =
@ -230,9 +230,9 @@ function LeftPanel({
}: { }: {
activeType: AccidentType; activeType: AccidentType;
onTypeChange: (t: AccidentType) => void; onTypeChange: (t: AccidentType) => void;
incidents: IncidentListItem[]; incidents: GscAccidentListItem[];
selectedAcdnt: IncidentListItem | null; selectedAcdnt: GscAccidentListItem | null;
onSelectAcdnt: (item: IncidentListItem | null) => void; onSelectAcdnt: (item: GscAccidentListItem | null) => void;
}) { }) {
const [acdntName, setAcdntName] = useState(''); const [acdntName, setAcdntName] = useState('');
const [acdntDate, setAcdntDate] = useState(''); const [acdntDate, setAcdntDate] = useState('');
@ -242,18 +242,25 @@ function LeftPanel({
const [showList, setShowList] = useState(false); const [showList, setShowList] = useState(false);
// 사고 선택 시 필드 자동 채움 // 사고 선택 시 필드 자동 채움
const handlePickIncident = (item: IncidentListItem) => { const handlePickIncident = (item: GscAccidentListItem) => {
onSelectAcdnt(item); onSelectAcdnt(item);
setAcdntName(item.acdntNm); setAcdntName(item.pollNm);
const dt = new Date(item.occrnDtm); if (item.pollDate) {
setAcdntDate( const [d, t] = item.pollDate.split('T');
`${dt.getFullYear()}. ${String(dt.getMonth() + 1).padStart(2, '0')}. ${String(dt.getDate()).padStart(2, '0')}.`, if (d) {
); const [y, m, day] = d.split('-');
setAcdntTime( setAcdntDate(`${y}. ${m}. ${day}.`);
`${dt.getHours() >= 12 ? '오후' : '오전'} ${String(dt.getHours() % 12 || 12).padStart(2, '0')}:${String(dt.getMinutes()).padStart(2, '0')}`, }
); if (t) {
setAcdntLat(String(item.lat)); const [hhStr, mmStr] = t.split(':');
setAcdntLon(String(item.lng)); const hh = parseInt(hhStr, 10);
const ampm = hh >= 12 ? '오후' : '오전';
const hh12 = String(hh % 12 || 12).padStart(2, '0');
setAcdntTime(`${ampm} ${hh12}:${mmStr}`);
}
}
if (item.lat != null) setAcdntLat(String(item.lat));
if (item.lon != null) setAcdntLon(String(item.lon));
setShowList(false); setShowList(false);
}; };
@ -283,7 +290,7 @@ function LeftPanel({
className="w-full px-2 py-1.5 text-caption bg-bg-card border border-stroke rounded font-korean text-left cursor-pointer hover:border-[var(--stroke-light)] flex items-center justify-between" className="w-full px-2 py-1.5 text-caption bg-bg-card border border-stroke rounded font-korean text-left cursor-pointer hover:border-[var(--stroke-light)] flex items-center justify-between"
> >
<span className={selectedAcdnt ? 'text-fg' : 'text-fg-disabled/50'}> <span className={selectedAcdnt ? 'text-fg' : 'text-fg-disabled/50'}>
{selectedAcdnt ? selectedAcdnt.acdntCd : '또는 사고 리스트에서 선택'} {selectedAcdnt ? selectedAcdnt.pollNm : '또는 사고 리스트에서 선택'}
</span> </span>
<span className="text-fg-disabled text-[10px]">{showList ? '▲' : '▼'}</span> <span className="text-fg-disabled text-[10px]">{showList ? '▲' : '▼'}</span>
</button> </button>
@ -296,13 +303,13 @@ function LeftPanel({
)} )}
{incidents.map((item) => ( {incidents.map((item) => (
<button <button
key={item.acdntSn} key={item.acdntMngNo}
onClick={() => handlePickIncident(item)} onClick={() => handlePickIncident(item)}
className="w-full text-left px-2 py-1.5 text-caption font-korean hover:bg-bg-surface cursor-pointer border-b border-stroke last:border-b-0" className="w-full text-left px-2 py-1.5 text-caption font-korean hover:bg-bg-surface cursor-pointer border-b border-stroke last:border-b-0"
> >
<div className="text-fg font-semibold truncate">{item.acdntNm}</div> <div className="text-fg font-semibold truncate">{item.pollNm}</div>
<div className="text-fg-disabled text-[10px]"> <div className="text-fg-disabled text-[10px]">
{item.acdntCd} · {item.regionNm} {item.pollDate ? item.pollDate.replace('T', ' ') : '-'}
</div> </div>
</button> </button>
))} ))}
@ -1523,13 +1530,14 @@ export function RescueView() {
const { activeSubTab } = useSubMenu('rescue'); const { activeSubTab } = useSubMenu('rescue');
const [activeType, setActiveType] = useState<AccidentType>('collision'); const [activeType, setActiveType] = useState<AccidentType>('collision');
const [activeAnalysis, setActiveAnalysis] = useState<AnalysisTab>('rescue'); const [activeAnalysis, setActiveAnalysis] = useState<AnalysisTab>('rescue');
const [incidents, setIncidents] = useState<IncidentListItem[]>([]); const [incidents, setIncidents] = useState<GscAccidentListItem[]>([]);
const [selectedAcdnt, setSelectedAcdnt] = useState<IncidentListItem | null>(null); const [selectedAcdnt, setSelectedAcdnt] = useState<GscAccidentListItem | null>(null);
const [incidentCoord, setIncidentCoord] = useState<{ lon: number; lat: number } | null>(null); const [incidentCoord, setIncidentCoord] = useState<{ lon: number; lat: number } | null>(null);
const [flyToCoord, setFlyToCoord] = useState<{ lon: number; lat: number } | undefined>(undefined);
const [isSelectingLocation, setIsSelectingLocation] = useState(false); const [isSelectingLocation, setIsSelectingLocation] = useState(false);
useEffect(() => { useEffect(() => {
fetchIncidentsRaw() fetchGscAccidents()
.then((items) => setIncidents(items)) .then((items) => setIncidents(items))
.catch(() => setIncidents([])); .catch(() => setIncidents([]));
}, []); }, []);
@ -1540,23 +1548,13 @@ export function RescueView() {
setIsSelectingLocation(false); setIsSelectingLocation(false);
}, []); }, []);
// 사고 선택 시 사고유형 자동 매핑 // 사고 선택 시 좌표 자동 반영 + 지도 이동
const handleSelectAcdnt = useCallback( const handleSelectAcdnt = useCallback(
(item: IncidentListItem | null) => { (item: GscAccidentListItem | null) => {
setSelectedAcdnt(item); setSelectedAcdnt(item);
if (item) { if (item && item.lat != null && item.lon != null) {
const typeMap: Record<string, AccidentType> = { setIncidentCoord({ lon: item.lon, lat: item.lat });
collision: 'collision', setFlyToCoord({ lon: item.lon, lat: item.lat });
grounding: 'grounding',
turning: 'turning',
capsizing: 'capsizing',
sharpTurn: 'sharpTurn',
flooding: 'flooding',
sinking: 'sinking',
};
const mapped = typeMap[item.acdntTpCd];
if (mapped) setActiveType(mapped);
setIncidentCoord({ lon: item.lng, lat: item.lat });
} }
}, },
[], [],
@ -1595,6 +1593,8 @@ export function RescueView() {
<div className="flex-1 relative overflow-hidden"> <div className="flex-1 relative overflow-hidden">
<MapView <MapView
incidentCoord={incidentCoord ?? undefined} incidentCoord={incidentCoord ?? undefined}
flyToIncident={flyToCoord}
onIncidentFlyEnd={() => setFlyToCoord(undefined)}
isSelectingLocation={isSelectingLocation} isSelectingLocation={isSelectingLocation}
onMapClick={handleMapClick} onMapClick={handleMapClick}
oilTrajectory={[]} oilTrajectory={[]}