2025-10-14 16:36:00 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 리스크/알림 서비스
|
|
|
|
|
|
* - 기상청 특보 API
|
|
|
|
|
|
* - 국토교통부 교통사고/도로공사 API 연동
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import axios from 'axios';
|
|
|
|
|
|
|
|
|
|
|
|
export interface Alert {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
type: 'accident' | 'weather' | 'construction';
|
|
|
|
|
|
severity: 'high' | 'medium' | 'low';
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
location: string;
|
|
|
|
|
|
description: string;
|
|
|
|
|
|
timestamp: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class RiskAlertService {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 기상청 특보 정보 조회 (기상청 API 허브 - 현재 발효 중인 특보 API)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getWeatherAlerts(): Promise<Alert[]> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const apiKey = process.env.KMA_API_KEY;
|
|
|
|
|
|
|
|
|
|
|
|
if (!apiKey) {
|
2025-10-16 16:34:59 +09:00
|
|
|
|
console.log('⚠️ 기상청 API 키가 없습니다. 빈 데이터를 반환합니다.');
|
|
|
|
|
|
return [];
|
2025-10-14 16:36:00 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const alerts: Alert[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 기상청 특보 현황 조회 API (실제 발효 중인 특보)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const warningUrl = 'https://apihub.kma.go.kr/api/typ01/url/wrn_now_data.php';
|
|
|
|
|
|
const warningResponse = await axios.get(warningUrl, {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
fe: 'f', // 발표 중인 특보
|
|
|
|
|
|
tm: '', // 현재 시각
|
|
|
|
|
|
disp: 0,
|
|
|
|
|
|
authKey: apiKey,
|
|
|
|
|
|
},
|
2025-10-27 18:33:15 +09:00
|
|
|
|
timeout: 30000, // 30초로 증가
|
2025-10-14 16:36:00 +09:00
|
|
|
|
responseType: 'arraybuffer', // 인코딩 문제 해결
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log('✅ 기상청 특보 현황 API 응답 수신 완료');
|
|
|
|
|
|
|
|
|
|
|
|
// 텍스트 응답 파싱 (EUC-KR 인코딩)
|
|
|
|
|
|
const iconv = require('iconv-lite');
|
|
|
|
|
|
const responseText = iconv.decode(Buffer.from(warningResponse.data), 'EUC-KR');
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof responseText === 'string' && responseText.includes('#START7777')) {
|
|
|
|
|
|
const lines = responseText.split('\n');
|
|
|
|
|
|
|
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
|
// 주석 및 헤더 라인 무시
|
|
|
|
|
|
if (line.startsWith('#') || line.trim() === '' || line.includes('7777END')) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 데이터 라인 파싱
|
|
|
|
|
|
const fields = line.split(',').map((f) => f.trim());
|
|
|
|
|
|
if (fields.length >= 7) {
|
|
|
|
|
|
const regUpKo = fields[1]; // 상위 특보 지역명
|
|
|
|
|
|
const regKo = fields[3]; // 특보 지역명
|
|
|
|
|
|
const tmFc = fields[4]; // 발표 시각
|
|
|
|
|
|
const wrnType = fields[6]; // 특보 종류
|
|
|
|
|
|
const wrnLevel = fields[7]; // 특보 수준 (주의보/경보)
|
|
|
|
|
|
|
|
|
|
|
|
// 특보 종류별 매핑
|
|
|
|
|
|
const warningMap: Record<string, { title: string; severity: 'high' | 'medium' | 'low' }> = {
|
|
|
|
|
|
'풍랑': { title: '풍랑주의보', severity: 'medium' },
|
|
|
|
|
|
'강풍': { title: '강풍주의보', severity: 'medium' },
|
|
|
|
|
|
'대설': { title: '대설특보', severity: 'high' },
|
|
|
|
|
|
'폭설': { title: '대설특보', severity: 'high' },
|
|
|
|
|
|
'태풍': { title: '태풍특보', severity: 'high' },
|
|
|
|
|
|
'호우': { title: '호우특보', severity: 'high' },
|
|
|
|
|
|
'한파': { title: '한파특보', severity: 'high' },
|
|
|
|
|
|
'폭염': { title: '폭염특보', severity: 'high' },
|
|
|
|
|
|
'건조': { title: '건조특보', severity: 'low' },
|
|
|
|
|
|
'해일': { title: '해일특보', severity: 'high' },
|
|
|
|
|
|
'너울': { title: '너울주의보', severity: 'low' },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const warningInfo = warningMap[wrnType];
|
|
|
|
|
|
if (warningInfo) {
|
|
|
|
|
|
// 경보는 심각도 높이기
|
|
|
|
|
|
const severity = wrnLevel.includes('경보') ? 'high' : warningInfo.severity;
|
|
|
|
|
|
const title = wrnLevel.includes('경보')
|
|
|
|
|
|
? wrnType + '경보'
|
|
|
|
|
|
: warningInfo.title;
|
|
|
|
|
|
|
|
|
|
|
|
alerts.push({
|
|
|
|
|
|
id: `warning-${Date.now()}-${alerts.length}`,
|
|
|
|
|
|
type: 'weather' as const,
|
|
|
|
|
|
severity: severity,
|
|
|
|
|
|
title: title,
|
|
|
|
|
|
location: regKo || regUpKo || '전국',
|
|
|
|
|
|
description: `${wrnLevel} 발표 - ${regUpKo} ${regKo}`,
|
|
|
|
|
|
timestamp: this.parseKmaTime(tmFc),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 총 ${alerts.length}건의 기상특보 감지`);
|
|
|
|
|
|
} catch (warningError: any) {
|
|
|
|
|
|
console.error('❌ 기상청 특보 API 오류:', warningError.message);
|
2025-10-16 16:34:59 +09:00
|
|
|
|
return [];
|
2025-10-14 16:36:00 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 특보가 없으면 빈 배열 반환 (0건)
|
|
|
|
|
|
if (alerts.length === 0) {
|
|
|
|
|
|
console.log('ℹ️ 현재 발효 중인 기상특보 없음 (0건)');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return alerts;
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('❌ 기상청 특보 API 오류:', error.message);
|
2025-10-16 16:34:59 +09:00
|
|
|
|
// API 오류 시 빈 배열 반환
|
|
|
|
|
|
return [];
|
2025-10-14 16:36:00 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 교통사고 정보 조회 (국토교통부 ITS API 우선, 실패 시 한국도로공사)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getAccidentAlerts(): Promise<Alert[]> {
|
|
|
|
|
|
// 1순위: 국토교통부 ITS API (실시간 돌발정보)
|
|
|
|
|
|
const itsApiKey = process.env.ITS_API_KEY;
|
|
|
|
|
|
if (itsApiKey) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const url = `https://openapi.its.go.kr:9443/eventInfo`;
|
|
|
|
|
|
|
|
|
|
|
|
const response = await axios.get(url, {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
apiKey: itsApiKey,
|
|
|
|
|
|
type: 'all',
|
|
|
|
|
|
eventType: 'acc', // 교통사고
|
|
|
|
|
|
minX: 124, // 전국 범위
|
|
|
|
|
|
maxX: 132,
|
|
|
|
|
|
minY: 33,
|
|
|
|
|
|
maxY: 43,
|
|
|
|
|
|
getType: 'json',
|
|
|
|
|
|
},
|
|
|
|
|
|
timeout: 10000,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log('✅ 국토교통부 ITS 교통사고 API 응답 수신 완료');
|
|
|
|
|
|
|
|
|
|
|
|
const alerts: Alert[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data?.header?.resultCode === 0 && response.data?.body?.items) {
|
|
|
|
|
|
const items = Array.isArray(response.data.body.items) ? response.data.body.items : [response.data.body.items];
|
|
|
|
|
|
|
|
|
|
|
|
items.forEach((item: any, index: number) => {
|
|
|
|
|
|
// ITS API 필드: eventType(교통사고), roadName, message, startDate, lanesBlocked
|
|
|
|
|
|
const lanesCount = (item.lanesBlocked || '').match(/\d+/)?.[0] || 0;
|
|
|
|
|
|
const severity = Number(lanesCount) >= 2 ? 'high' : Number(lanesCount) === 1 ? 'medium' : 'low';
|
|
|
|
|
|
|
|
|
|
|
|
alerts.push({
|
|
|
|
|
|
id: `accident-its-${Date.now()}-${index}`,
|
|
|
|
|
|
type: 'accident' as const,
|
|
|
|
|
|
severity: severity as 'high' | 'medium' | 'low',
|
|
|
|
|
|
title: `[${item.roadName || '고속도로'}] 교통사고`,
|
|
|
|
|
|
location: `${item.roadName || ''} ${item.roadDrcType || ''}`.trim() || '정보 없음',
|
|
|
|
|
|
description: item.message || `${item.eventDetailType || '사고 발생'} - ${item.lanesBlocked || '차로 통제'}`,
|
|
|
|
|
|
timestamp: this.parseITSTime(item.startDate || ''),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (alerts.length === 0) {
|
|
|
|
|
|
console.log('ℹ️ 현재 교통사고 없음 (0건)');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`✅ 총 ${alerts.length}건의 교통사고 감지 (ITS)`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return alerts;
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('❌ 국토교통부 ITS API 오류:', error.message);
|
|
|
|
|
|
console.log('ℹ️ 2순위 API로 전환합니다.');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2순위: 한국도로공사 API (현재 차단됨)
|
|
|
|
|
|
const exwayApiKey = process.env.EXWAY_API_KEY || '7820214492';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const url = 'https://data.ex.co.kr/openapi/business/trafficFcst';
|
|
|
|
|
|
|
|
|
|
|
|
const response = await axios.get(url, {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
key: exwayApiKey,
|
|
|
|
|
|
type: 'json',
|
|
|
|
|
|
},
|
|
|
|
|
|
timeout: 10000,
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
|
|
|
|
'Referer': 'https://data.ex.co.kr/',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log('✅ 한국도로공사 교통예보 API 응답 수신 완료');
|
|
|
|
|
|
|
|
|
|
|
|
const alerts: Alert[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data?.list) {
|
|
|
|
|
|
const items = Array.isArray(response.data.list) ? response.data.list : [response.data.list];
|
|
|
|
|
|
|
|
|
|
|
|
items.forEach((item: any, index: number) => {
|
|
|
|
|
|
const contentType = item.conzoneCd || item.contentType || '';
|
|
|
|
|
|
|
|
|
|
|
|
if (contentType === '00' || item.content?.includes('사고')) {
|
|
|
|
|
|
const severity = contentType === '31' ? 'high' : contentType === '30' ? 'medium' : 'low';
|
|
|
|
|
|
|
|
|
|
|
|
alerts.push({
|
|
|
|
|
|
id: `accident-exway-${Date.now()}-${index}`,
|
|
|
|
|
|
type: 'accident' as const,
|
|
|
|
|
|
severity: severity as 'high' | 'medium' | 'low',
|
|
|
|
|
|
title: '교통사고',
|
|
|
|
|
|
location: item.routeName || item.location || '고속도로',
|
|
|
|
|
|
description: item.content || item.message || '교통사고 발생',
|
|
|
|
|
|
timestamp: new Date(item.regDate || Date.now()).toISOString(),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (alerts.length > 0) {
|
|
|
|
|
|
console.log(`✅ 총 ${alerts.length}건의 교통사고 감지 (한국도로공사)`);
|
|
|
|
|
|
return alerts;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('❌ 한국도로공사 API 오류:', error.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-16 16:34:59 +09:00
|
|
|
|
// 모든 API 실패 시 빈 배열
|
|
|
|
|
|
console.log('ℹ️ 모든 교통사고 API 실패. 빈 배열을 반환합니다.');
|
|
|
|
|
|
return [];
|
2025-10-14 16:36:00 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 도로공사 정보 조회 (국토교통부 ITS API 우선, 실패 시 한국도로공사)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getRoadworkAlerts(): Promise<Alert[]> {
|
|
|
|
|
|
// 1순위: 국토교통부 ITS API (실시간 돌발정보 - 공사)
|
|
|
|
|
|
const itsApiKey = process.env.ITS_API_KEY;
|
|
|
|
|
|
if (itsApiKey) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const url = `https://openapi.its.go.kr:9443/eventInfo`;
|
|
|
|
|
|
|
|
|
|
|
|
const response = await axios.get(url, {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
apiKey: itsApiKey,
|
|
|
|
|
|
type: 'all',
|
|
|
|
|
|
eventType: 'all', // 전체 조회 후 필터링
|
|
|
|
|
|
minX: 124,
|
|
|
|
|
|
maxX: 132,
|
|
|
|
|
|
minY: 33,
|
|
|
|
|
|
maxY: 43,
|
|
|
|
|
|
getType: 'json',
|
|
|
|
|
|
},
|
|
|
|
|
|
timeout: 10000,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log('✅ 국토교통부 ITS 도로공사 API 응답 수신 완료');
|
|
|
|
|
|
|
|
|
|
|
|
const alerts: Alert[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data?.header?.resultCode === 0 && response.data?.body?.items) {
|
|
|
|
|
|
const items = Array.isArray(response.data.body.items) ? response.data.body.items : [response.data.body.items];
|
|
|
|
|
|
|
|
|
|
|
|
items.forEach((item: any, index: number) => {
|
|
|
|
|
|
// 공사/작업만 필터링
|
|
|
|
|
|
if (item.eventType === '공사' || item.eventDetailType === '작업') {
|
|
|
|
|
|
const lanesCount = (item.lanesBlocked || '').match(/\d+/)?.[0] || 0;
|
|
|
|
|
|
const severity = Number(lanesCount) >= 2 ? 'high' : 'medium';
|
|
|
|
|
|
|
|
|
|
|
|
alerts.push({
|
|
|
|
|
|
id: `construction-its-${Date.now()}-${index}`,
|
|
|
|
|
|
type: 'construction' as const,
|
|
|
|
|
|
severity: severity as 'high' | 'medium' | 'low',
|
|
|
|
|
|
title: `[${item.roadName || '고속도로'}] 도로 공사`,
|
|
|
|
|
|
location: `${item.roadName || ''} ${item.roadDrcType || ''}`.trim() || '정보 없음',
|
|
|
|
|
|
description: item.message || `${item.eventDetailType || '작업'} - ${item.lanesBlocked || '차로 통제'}`,
|
|
|
|
|
|
timestamp: this.parseITSTime(item.startDate || ''),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (alerts.length === 0) {
|
|
|
|
|
|
console.log('ℹ️ 현재 도로공사 없음 (0건)');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`✅ 총 ${alerts.length}건의 도로공사 감지 (ITS)`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return alerts;
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('❌ 국토교통부 ITS API 오류:', error.message);
|
|
|
|
|
|
console.log('ℹ️ 2순위 API로 전환합니다.');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2순위: 한국도로공사 API
|
|
|
|
|
|
const exwayApiKey = process.env.EXWAY_API_KEY || '7820214492';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const url = 'https://data.ex.co.kr/openapi/business/trafficFcst';
|
|
|
|
|
|
|
|
|
|
|
|
const response = await axios.get(url, {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
key: exwayApiKey,
|
|
|
|
|
|
type: 'json',
|
|
|
|
|
|
},
|
|
|
|
|
|
timeout: 10000,
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
|
|
|
|
'Referer': 'https://data.ex.co.kr/',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log('✅ 한국도로공사 교통예보 API 응답 수신 완료 (도로공사)');
|
|
|
|
|
|
|
|
|
|
|
|
const alerts: Alert[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data?.list) {
|
|
|
|
|
|
const items = Array.isArray(response.data.list) ? response.data.list : [response.data.list];
|
|
|
|
|
|
|
|
|
|
|
|
items.forEach((item: any, index: number) => {
|
|
|
|
|
|
const contentType = item.conzoneCd || item.contentType || '';
|
|
|
|
|
|
|
|
|
|
|
|
if (contentType === '03' || item.content?.includes('작업') || item.content?.includes('공사')) {
|
|
|
|
|
|
const severity = contentType === '31' ? 'high' : contentType === '30' ? 'medium' : 'low';
|
|
|
|
|
|
|
|
|
|
|
|
alerts.push({
|
|
|
|
|
|
id: `construction-exway-${Date.now()}-${index}`,
|
|
|
|
|
|
type: 'construction' as const,
|
|
|
|
|
|
severity: severity as 'high' | 'medium' | 'low',
|
|
|
|
|
|
title: '도로 공사',
|
|
|
|
|
|
location: item.routeName || item.location || '고속도로',
|
|
|
|
|
|
description: item.content || item.message || '도로 공사 진행 중',
|
|
|
|
|
|
timestamp: new Date(item.regDate || Date.now()).toISOString(),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (alerts.length > 0) {
|
|
|
|
|
|
console.log(`✅ 총 ${alerts.length}건의 도로공사 감지 (한국도로공사)`);
|
|
|
|
|
|
return alerts;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('❌ 한국도로공사 API 오류:', error.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-16 16:34:59 +09:00
|
|
|
|
// 모든 API 실패 시 빈 배열
|
|
|
|
|
|
console.log('ℹ️ 모든 도로공사 API 실패. 빈 배열을 반환합니다.');
|
|
|
|
|
|
return [];
|
2025-10-14 16:36:00 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 전체 알림 조회 (통합)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getAllAlerts(): Promise<Alert[]> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [weatherAlerts, accidentAlerts, roadworkAlerts] = await Promise.all([
|
|
|
|
|
|
this.getWeatherAlerts(),
|
|
|
|
|
|
this.getAccidentAlerts(),
|
|
|
|
|
|
this.getRoadworkAlerts(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 모든 알림 합치기
|
|
|
|
|
|
const allAlerts = [...weatherAlerts, ...accidentAlerts, ...roadworkAlerts];
|
|
|
|
|
|
|
|
|
|
|
|
// 시간 순으로 정렬 (최신순)
|
|
|
|
|
|
allAlerts.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
|
|
|
|
|
|
|
|
|
|
return allAlerts;
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('❌ 전체 알림 조회 오류:', error.message);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 기상청 시간 형식 파싱 (YYYYMMDDHHmm -> ISO)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private parseKmaTime(tmFc: string): string {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!tmFc || tmFc.length !== 12) {
|
|
|
|
|
|
return new Date().toISOString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const year = tmFc.substring(0, 4);
|
|
|
|
|
|
const month = tmFc.substring(4, 6);
|
|
|
|
|
|
const day = tmFc.substring(6, 8);
|
|
|
|
|
|
const hour = tmFc.substring(8, 10);
|
|
|
|
|
|
const minute = tmFc.substring(10, 12);
|
|
|
|
|
|
|
|
|
|
|
|
return new Date(`${year}-${month}-${day}T${hour}:${minute}:00+09:00`).toISOString();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return new Date().toISOString();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ITS API 시간 형식 파싱 (YYYYMMDDHHmmss -> ISO)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private parseITSTime(dateStr: string): string {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!dateStr || dateStr.length !== 14) {
|
|
|
|
|
|
return new Date().toISOString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const year = dateStr.substring(0, 4);
|
|
|
|
|
|
const month = dateStr.substring(4, 6);
|
|
|
|
|
|
const day = dateStr.substring(6, 8);
|
|
|
|
|
|
const hour = dateStr.substring(8, 10);
|
|
|
|
|
|
const minute = dateStr.substring(10, 12);
|
|
|
|
|
|
const second = dateStr.substring(12, 14);
|
|
|
|
|
|
|
|
|
|
|
|
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}+09:00`).toISOString();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return new Date().toISOString();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 기상 특보 심각도 판단
|
|
|
|
|
|
*/
|
|
|
|
|
|
private getWeatherSeverity(wrnLv: string): 'high' | 'medium' | 'low' {
|
|
|
|
|
|
if (wrnLv.includes('경보') || wrnLv.includes('특보')) {
|
|
|
|
|
|
return 'high';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (wrnLv.includes('주의보')) {
|
|
|
|
|
|
return 'medium';
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'low';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 기상 특보 제목 생성
|
|
|
|
|
|
*/
|
|
|
|
|
|
private getWeatherTitle(wrnLv: string): string {
|
|
|
|
|
|
if (wrnLv.includes('대설')) return '대설특보';
|
|
|
|
|
|
if (wrnLv.includes('태풍')) return '태풍특보';
|
|
|
|
|
|
if (wrnLv.includes('강풍')) return '강풍특보';
|
|
|
|
|
|
if (wrnLv.includes('호우')) return '호우특보';
|
|
|
|
|
|
if (wrnLv.includes('한파')) return '한파특보';
|
|
|
|
|
|
if (wrnLv.includes('폭염')) return '폭염특보';
|
|
|
|
|
|
return '기상특보';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 교통사고 심각도 판단
|
|
|
|
|
|
*/
|
|
|
|
|
|
private getAccidentSeverity(accInfo: string): 'high' | 'medium' | 'low' {
|
|
|
|
|
|
if (accInfo.includes('중대') || accInfo.includes('다중') || accInfo.includes('추돌')) {
|
|
|
|
|
|
return 'high';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (accInfo.includes('접촉') || accInfo.includes('경상')) {
|
|
|
|
|
|
return 'medium';
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'low';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|