ERP-node/backend-node/src/services/riskAlertService.ts

472 lines
17 KiB
TypeScript
Raw Normal View History

/**
* /
* - 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) {
console.log('⚠️ 기상청 API 키가 없습니다. 빈 데이터를 반환합니다.');
return [];
}
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,
},
timeout: 30000, // 30초로 증가
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);
return [];
}
// 특보가 없으면 빈 배열 반환 (0건)
if (alerts.length === 0) {
console.log(' 현재 발효 중인 기상특보 없음 (0건)');
}
return alerts;
} catch (error: any) {
console.error('❌ 기상청 특보 API 오류:', error.message);
// API 오류 시 빈 배열 반환
return [];
}
}
/**
* ( 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);
}
// 모든 API 실패 시 빈 배열
console.log(' 모든 교통사고 API 실패. 빈 배열을 반환합니다.');
return [];
}
/**
* ( 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);
}
// 모든 API 실패 시 빈 배열
console.log(' 모든 도로공사 API 실패. 빈 배열을 반환합니다.');
return [];
}
/**
* ()
*/
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';
}
}