Compare commits
5 커밋
2fe9deeabe
...
ae0a17990b
| 작성자 | SHA1 | 날짜 | |
|---|---|---|---|
| ae0a17990b | |||
| 6b19d34e5b | |||
| 679649ab8c | |||
| 279dcbc0e1 | |||
| 15ca946a00 |
@ -5,7 +5,30 @@
|
||||
},
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(*)"
|
||||
"Bash(*)",
|
||||
"Bash(npm run *)",
|
||||
"Bash(npm install *)",
|
||||
"Bash(npm test *)",
|
||||
"Bash(npx *)",
|
||||
"Bash(node *)",
|
||||
"Bash(git status)",
|
||||
"Bash(git diff *)",
|
||||
"Bash(git log *)",
|
||||
"Bash(git branch *)",
|
||||
"Bash(git checkout *)",
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit *)",
|
||||
"Bash(git pull *)",
|
||||
"Bash(git fetch *)",
|
||||
"Bash(git merge *)",
|
||||
"Bash(git stash *)",
|
||||
"Bash(git remote *)",
|
||||
"Bash(git config *)",
|
||||
"Bash(git rev-parse *)",
|
||||
"Bash(git show *)",
|
||||
"Bash(git tag *)",
|
||||
"Bash(curl -s *)",
|
||||
"Bash(fnm *)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(git push --force*)",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"applied_global_version": "1.6.1",
|
||||
"applied_date": "2026-03-31",
|
||||
"applied_date": "2026-04-14",
|
||||
"project_type": "react-ts",
|
||||
"gitea_url": "https://gitea.gc-si.dev",
|
||||
"custom_pre_commit": true
|
||||
|
||||
20
backend/src/gsc/gscAccidentsRouter.ts
Normal file
20
backend/src/gsc/gscAccidentsRouter.ts
Normal file
@ -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;
|
||||
65
backend/src/gsc/gscAccidentsService.ts
Normal file
65
backend/src/gsc/gscAccidentsService.ts
Normal file
@ -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 assetsRouter from './assets/assetsRouter.js'
|
||||
import incidentsRouter from './incidents/incidentsRouter.js'
|
||||
import gscAccidentsRouter from './gsc/gscAccidentsRouter.js'
|
||||
import scatRouter from './scat/scatRouter.js'
|
||||
import predictionRouter from './prediction/predictionRouter.js'
|
||||
import aerialRouter from './aerial/aerialRouter.js'
|
||||
@ -168,6 +169,7 @@ app.use('/api/hns', hnsRouter)
|
||||
app.use('/api/reports', reportsRouter)
|
||||
app.use('/api/assets', assetsRouter)
|
||||
app.use('/api/incidents', incidentsRouter)
|
||||
app.use('/api/gsc/accidents', gscAccidentsRouter)
|
||||
app.use('/api/scat', scatRouter)
|
||||
app.use('/api/prediction', predictionRouter)
|
||||
app.use('/api/aerial', aerialRouter)
|
||||
|
||||
@ -4,6 +4,9 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### 추가
|
||||
- 확산예측·HNS 대기확산·긴급구난: GSC 외부 사고 목록 API 연동 및 셀렉트박스 자동 채움 (사고명·발생시각·위경도 자동 입력 + 지도 이동)
|
||||
|
||||
### 변경
|
||||
- MapView 컴포넌트 분리 및 전체 탭 디자인 시스템 토큰 적용
|
||||
|
||||
|
||||
@ -3,8 +3,8 @@ import { ComboBox } from '@common/components/ui/ComboBox';
|
||||
import { useWeatherFetch } from '../hooks/useWeatherFetch';
|
||||
import { getSubstanceToxicity } from '../utils/toxicityData';
|
||||
import type { WeatherFetchResult, ReleaseType } from '../utils/dispersionTypes';
|
||||
import { fetchIncidentsRaw } from '@tabs/incidents/services/incidentsApi';
|
||||
import type { IncidentListItem } from '@tabs/incidents/services/incidentsApi';
|
||||
import { fetchGscAccidents } from '@tabs/prediction/services/predictionApi';
|
||||
import type { GscAccidentListItem } from '@tabs/prediction/services/predictionApi';
|
||||
|
||||
/** HNS 분석 입력 파라미터 (부모에 전달) */
|
||||
export interface HNSInputParams {
|
||||
@ -44,6 +44,7 @@ interface HNSLeftPanelProps {
|
||||
onParamsChange?: (params: HNSInputParams) => void;
|
||||
onReset?: () => void;
|
||||
loadedParams?: Partial<HNSInputParams> | null;
|
||||
onFlyToCoord?: (coord: { lon: number; lat: number }) => void;
|
||||
}
|
||||
|
||||
/** 십진 좌표 → 도분초 변환 */
|
||||
@ -67,8 +68,9 @@ export function HNSLeftPanel({
|
||||
onParamsChange,
|
||||
onReset,
|
||||
loadedParams,
|
||||
onFlyToCoord,
|
||||
}: HNSLeftPanelProps) {
|
||||
const [incidents, setIncidents] = useState<IncidentListItem[]>([]);
|
||||
const [incidents, setIncidents] = useState<GscAccidentListItem[]>([]);
|
||||
const [selectedIncidentSn, setSelectedIncidentSn] = useState('');
|
||||
const [expandedSections, setExpandedSections] = useState({ accident: true, params: true });
|
||||
const toggleSection = (key: 'accident' | 'params') =>
|
||||
@ -138,21 +140,26 @@ export function HNSLeftPanel({
|
||||
// 사고 목록 불러오기 (마운트 시 1회, ref 초기화 패턴)
|
||||
const incidentsPromiseRef = useRef<Promise<void> | null>(null);
|
||||
if (incidentsPromiseRef.current == null) {
|
||||
incidentsPromiseRef.current = fetchIncidentsRaw()
|
||||
incidentsPromiseRef.current = fetchGscAccidents()
|
||||
.then((data) => setIncidents(data))
|
||||
.catch(() => setIncidents([]));
|
||||
}
|
||||
|
||||
// 사고 선택 시 필드 자동 채움
|
||||
const handleSelectIncident = (snStr: string) => {
|
||||
setSelectedIncidentSn(snStr);
|
||||
const sn = parseInt(snStr);
|
||||
const incident = incidents.find((i) => i.acdntSn === sn);
|
||||
const handleSelectIncident = (mngNo: string) => {
|
||||
setSelectedIncidentSn(mngNo);
|
||||
const incident = incidents.find((i) => i.acdntMngNo === mngNo);
|
||||
if (!incident) return;
|
||||
|
||||
setAccidentName(incident.acdntNm);
|
||||
if (incident.lat && incident.lng) {
|
||||
onCoordChange({ lat: incident.lat, lon: incident.lng });
|
||||
setAccidentName(incident.pollNm);
|
||||
if (incident.pollDate) {
|
||||
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}
|
||||
placeholder="또는 사고 리스트에서 선택"
|
||||
options={incidents.map((inc) => ({
|
||||
value: String(inc.acdntSn),
|
||||
label: `${inc.acdntNm} (${new Date(inc.occrnDtm).toLocaleDateString('ko-KR')})`,
|
||||
value: inc.acdntMngNo,
|
||||
label: `${inc.pollNm} (${inc.pollDate ? inc.pollDate.replace('T', ' ') : '-'})`,
|
||||
}))}
|
||||
/>
|
||||
|
||||
|
||||
@ -265,6 +265,7 @@ export function HNSView() {
|
||||
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
||||
const [rightCollapsed, setRightCollapsed] = useState(false);
|
||||
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 [isRunningPrediction, setIsRunningPrediction] = useState(false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -904,6 +905,7 @@ export function HNSView() {
|
||||
onParamsChange={handleParamsChange}
|
||||
onReset={handleReset}
|
||||
loadedParams={loadedParams}
|
||||
onFlyToCoord={(c) => setFlyToCoord({ lat: c.lat, lon: c.lon })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -963,6 +965,8 @@ export function HNSView() {
|
||||
<>
|
||||
<MapView
|
||||
incidentCoord={incidentCoord ?? undefined}
|
||||
flyToIncident={flyToCoord}
|
||||
onIncidentFlyEnd={() => setFlyToCoord(undefined)}
|
||||
isSelectingLocation={isSelectingLocation}
|
||||
onMapClick={handleMapClick}
|
||||
oilTrajectory={[]}
|
||||
|
||||
@ -114,6 +114,7 @@ export function LeftPanel({
|
||||
onLayerColorChange,
|
||||
sensitiveResources = [],
|
||||
onImageAnalysisResult,
|
||||
onFlyToCoord,
|
||||
validationErrors,
|
||||
}: LeftPanelProps) {
|
||||
const [expandedSections, setExpandedSections] = useState<ExpandedSections>({
|
||||
@ -168,6 +169,7 @@ export function LeftPanel({
|
||||
spillUnit={spillUnit}
|
||||
onSpillUnitChange={onSpillUnitChange}
|
||||
onImageAnalysisResult={onImageAnalysisResult}
|
||||
onFlyToCoord={onFlyToCoord}
|
||||
validationErrors={validationErrors}
|
||||
/>
|
||||
|
||||
|
||||
@ -1211,6 +1211,9 @@ export function OilSpillView() {
|
||||
onLayerColorChange={(id, color) => setLayerColors((prev) => ({ ...prev, [id]: color }))}
|
||||
sensitiveResources={sensitiveResourceCategories}
|
||||
onImageAnalysisResult={handleImageAnalysisResult}
|
||||
onFlyToCoord={(c: { lon: number; lat: number }) =>
|
||||
setFlyToCoord({ lat: c.lat, lon: c.lon })
|
||||
}
|
||||
validationErrors={validationErrors}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { ComboBox } from '@common/components/ui/ComboBox';
|
||||
import type { PredictionModel } from './OilSpillView';
|
||||
import { analyzeImage } from '../services/predictionApi';
|
||||
import type { ImageAnalyzeResult } from '../services/predictionApi';
|
||||
import { analyzeImage, fetchGscAccidents } from '../services/predictionApi';
|
||||
import type { ImageAnalyzeResult, GscAccidentListItem } from '../services/predictionApi';
|
||||
|
||||
interface PredictionInputSectionProps {
|
||||
expanded: boolean;
|
||||
@ -33,6 +33,7 @@ interface PredictionInputSectionProps {
|
||||
spillUnit: string;
|
||||
onSpillUnitChange: (unit: string) => void;
|
||||
onImageAnalysisResult?: (result: ImageAnalyzeResult) => void;
|
||||
onFlyToCoord?: (coord: { lon: number; lat: number }) => void;
|
||||
validationErrors?: Set<string>;
|
||||
}
|
||||
|
||||
@ -64,6 +65,7 @@ const PredictionInputSection = ({
|
||||
spillUnit,
|
||||
onSpillUnitChange,
|
||||
onImageAnalysisResult,
|
||||
onFlyToCoord,
|
||||
validationErrors,
|
||||
}: PredictionInputSectionProps) => {
|
||||
const [inputMode, setInputMode] = useState<'direct' | 'upload'>('direct');
|
||||
@ -71,8 +73,41 @@ const PredictionInputSection = ({
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
const [analyzeError, setAnalyzeError] = useState<string | null>(null);
|
||||
const [analyzeResult, setAnalyzeResult] = useState<ImageAnalyzeResult | null>(null);
|
||||
const [gscAccidents, setGscAccidents] = useState<GscAccidentListItem[]>([]);
|
||||
const [selectedGscMngNo, setSelectedGscMngNo] = useState<string>('');
|
||||
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 file = e.target.files?.[0] ?? null;
|
||||
setUploadedFile(file);
|
||||
@ -161,7 +196,13 @@ const PredictionInputSection = ({
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<input className="prd-i" placeholder="또는 사고 리스트에서 선택" readOnly />
|
||||
<ComboBox
|
||||
className="prd-i"
|
||||
value={selectedGscMngNo}
|
||||
onChange={handleGscAccidentSelect}
|
||||
options={gscOptions}
|
||||
placeholder="또는 사고 리스트에서 선택"
|
||||
/>
|
||||
|
||||
{/* Image Upload Mode */}
|
||||
{inputMode === 'upload' && (
|
||||
|
||||
@ -62,6 +62,8 @@ export interface LeftPanelProps {
|
||||
sensitiveResources?: SensitiveResourceCategory[];
|
||||
// 이미지 분석 결과 콜백
|
||||
onImageAnalysisResult?: (result: ImageAnalyzeResult) => void;
|
||||
// 사고 리스트 선택 시 지도 이동 콜백
|
||||
onFlyToCoord?: (coord: { lon: number; lat: number }) => void;
|
||||
// 유효성 검증 에러 필드
|
||||
validationErrors?: Set<string>;
|
||||
}
|
||||
|
||||
@ -321,3 +321,20 @@ export const analyzeImage = async (file: File, acdntNm?: string): Promise<ImageA
|
||||
});
|
||||
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 { fetchRescueOps } from '../services/rescueApi';
|
||||
import type { RescueOpsItem } from '../services/rescueApi';
|
||||
import { fetchIncidentsRaw } from '@tabs/incidents/services/incidentsApi';
|
||||
import type { IncidentListItem } from '@tabs/incidents/services/incidentsApi';
|
||||
import { fetchGscAccidents } from '@tabs/prediction/services/predictionApi';
|
||||
import type { GscAccidentListItem } from '@tabs/prediction/services/predictionApi';
|
||||
|
||||
/* ─── Types ─── */
|
||||
type AccidentType =
|
||||
@ -230,9 +230,9 @@ function LeftPanel({
|
||||
}: {
|
||||
activeType: AccidentType;
|
||||
onTypeChange: (t: AccidentType) => void;
|
||||
incidents: IncidentListItem[];
|
||||
selectedAcdnt: IncidentListItem | null;
|
||||
onSelectAcdnt: (item: IncidentListItem | null) => void;
|
||||
incidents: GscAccidentListItem[];
|
||||
selectedAcdnt: GscAccidentListItem | null;
|
||||
onSelectAcdnt: (item: GscAccidentListItem | null) => void;
|
||||
}) {
|
||||
const [acdntName, setAcdntName] = useState('');
|
||||
const [acdntDate, setAcdntDate] = useState('');
|
||||
@ -242,18 +242,25 @@ function LeftPanel({
|
||||
const [showList, setShowList] = useState(false);
|
||||
|
||||
// 사고 선택 시 필드 자동 채움
|
||||
const handlePickIncident = (item: IncidentListItem) => {
|
||||
const handlePickIncident = (item: GscAccidentListItem) => {
|
||||
onSelectAcdnt(item);
|
||||
setAcdntName(item.acdntNm);
|
||||
const dt = new Date(item.occrnDtm);
|
||||
setAcdntDate(
|
||||
`${dt.getFullYear()}. ${String(dt.getMonth() + 1).padStart(2, '0')}. ${String(dt.getDate()).padStart(2, '0')}.`,
|
||||
);
|
||||
setAcdntTime(
|
||||
`${dt.getHours() >= 12 ? '오후' : '오전'} ${String(dt.getHours() % 12 || 12).padStart(2, '0')}:${String(dt.getMinutes()).padStart(2, '0')}`,
|
||||
);
|
||||
setAcdntLat(String(item.lat));
|
||||
setAcdntLon(String(item.lng));
|
||||
setAcdntName(item.pollNm);
|
||||
if (item.pollDate) {
|
||||
const [d, t] = item.pollDate.split('T');
|
||||
if (d) {
|
||||
const [y, m, day] = d.split('-');
|
||||
setAcdntDate(`${y}. ${m}. ${day}.`);
|
||||
}
|
||||
if (t) {
|
||||
const [hhStr, mmStr] = t.split(':');
|
||||
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);
|
||||
};
|
||||
|
||||
@ -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"
|
||||
>
|
||||
<span className={selectedAcdnt ? 'text-fg' : 'text-fg-disabled/50'}>
|
||||
{selectedAcdnt ? selectedAcdnt.acdntCd : '또는 사고 리스트에서 선택'}
|
||||
{selectedAcdnt ? selectedAcdnt.pollNm : '또는 사고 리스트에서 선택'}
|
||||
</span>
|
||||
<span className="text-fg-disabled text-[10px]">{showList ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
@ -296,13 +303,13 @@ function LeftPanel({
|
||||
)}
|
||||
{incidents.map((item) => (
|
||||
<button
|
||||
key={item.acdntSn}
|
||||
key={item.acdntMngNo}
|
||||
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"
|
||||
>
|
||||
<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]">
|
||||
{item.acdntCd} · {item.regionNm}
|
||||
{item.pollDate ? item.pollDate.replace('T', ' ') : '-'}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
@ -1523,13 +1530,14 @@ export function RescueView() {
|
||||
const { activeSubTab } = useSubMenu('rescue');
|
||||
const [activeType, setActiveType] = useState<AccidentType>('collision');
|
||||
const [activeAnalysis, setActiveAnalysis] = useState<AnalysisTab>('rescue');
|
||||
const [incidents, setIncidents] = useState<IncidentListItem[]>([]);
|
||||
const [selectedAcdnt, setSelectedAcdnt] = useState<IncidentListItem | null>(null);
|
||||
const [incidents, setIncidents] = useState<GscAccidentListItem[]>([]);
|
||||
const [selectedAcdnt, setSelectedAcdnt] = useState<GscAccidentListItem | 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);
|
||||
|
||||
useEffect(() => {
|
||||
fetchIncidentsRaw()
|
||||
fetchGscAccidents()
|
||||
.then((items) => setIncidents(items))
|
||||
.catch(() => setIncidents([]));
|
||||
}, []);
|
||||
@ -1540,24 +1548,17 @@ export function RescueView() {
|
||||
setIsSelectingLocation(false);
|
||||
}, []);
|
||||
|
||||
// 사고 선택 시 사고유형 자동 매핑
|
||||
const handleSelectAcdnt = useCallback((item: IncidentListItem | null) => {
|
||||
setSelectedAcdnt(item);
|
||||
if (item) {
|
||||
const typeMap: Record<string, AccidentType> = {
|
||||
collision: 'collision',
|
||||
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 });
|
||||
}
|
||||
}, []);
|
||||
// 사고 선택 시 좌표 자동 반영 + 지도 이동
|
||||
const handleSelectAcdnt = useCallback(
|
||||
(item: GscAccidentListItem | null) => {
|
||||
setSelectedAcdnt(item);
|
||||
if (item && item.lat != null && item.lon != null) {
|
||||
setIncidentCoord({ lon: item.lon, lat: item.lat });
|
||||
setFlyToCoord({ lon: item.lon, lat: item.lat });
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (activeSubTab === 'list') {
|
||||
return (
|
||||
@ -1592,6 +1593,8 @@ export function RescueView() {
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
<MapView
|
||||
incidentCoord={incidentCoord ?? undefined}
|
||||
flyToIncident={flyToCoord}
|
||||
onIncidentFlyEnd={() => setFlyToCoord(undefined)}
|
||||
isSelectingLocation={isSelectingLocation}
|
||||
onMapClick={handleMapClick}
|
||||
oilTrajectory={[]}
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user