755 lines
26 KiB
TypeScript
755 lines
26 KiB
TypeScript
/**
|
||
* 리스크/알림 서비스
|
||
* - 기상청 특보 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 interface WeatherData {
|
||
city: string;
|
||
country: string;
|
||
temperature: number;
|
||
feelsLike: number;
|
||
humidity: number;
|
||
pressure: number;
|
||
weatherMain: string;
|
||
weatherDescription: string;
|
||
weatherIcon: string;
|
||
windSpeed: number;
|
||
clouds: number;
|
||
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 this.generateDummyWeatherAlerts();
|
||
}
|
||
|
||
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: 10000,
|
||
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 this.generateDummyWeatherAlerts();
|
||
}
|
||
|
||
// 특보가 없으면 빈 배열 반환 (0건)
|
||
if (alerts.length === 0) {
|
||
console.log('ℹ️ 현재 발효 중인 기상특보 없음 (0건)');
|
||
}
|
||
|
||
return alerts;
|
||
} catch (error: any) {
|
||
console.error('❌ 기상청 특보 API 오류:', error.message);
|
||
// API 오류 시 더미 데이터 반환
|
||
return this.generateDummyWeatherAlerts();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 교통사고 정보 조회 (국토교통부 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 this.generateDummyAccidentAlerts();
|
||
}
|
||
|
||
/**
|
||
* 도로공사 정보 조회 (국토교통부 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 this.generateDummyRoadworkAlerts();
|
||
}
|
||
|
||
/**
|
||
* 기상청 실시간 날씨 정보 조회
|
||
*/
|
||
async getCurrentWeather(city: string = '서울'): Promise<WeatherData> {
|
||
try {
|
||
const apiKey = process.env.KMA_API_KEY;
|
||
|
||
if (!apiKey) {
|
||
console.log('⚠️ 기상청 API 키가 없습니다. 근사 데이터를 반환합니다.');
|
||
return this.generateRealisticWeatherData(city);
|
||
}
|
||
|
||
// 도시명 → 관측소 코드 매핑
|
||
const regionCode = this.getKMARegionCode(city);
|
||
|
||
if (!regionCode) {
|
||
console.log(`⚠️ 지원하지 않는 지역: ${city}. 근사 데이터를 반환합니다.`);
|
||
return this.generateRealisticWeatherData(city);
|
||
}
|
||
|
||
// 기상청 API Hub - 지상관측시간자료
|
||
const now = new Date();
|
||
const minute = now.getMinutes();
|
||
let targetTime = new Date(now);
|
||
|
||
// 데이터가 업데이트되지 않았으면 이전 시간으로
|
||
if (minute < 10) {
|
||
targetTime = new Date(now.getTime() - 60 * 60 * 1000);
|
||
}
|
||
|
||
const year = targetTime.getFullYear();
|
||
const month = String(targetTime.getMonth() + 1).padStart(2, '0');
|
||
const day = String(targetTime.getDate()).padStart(2, '0');
|
||
const hour = String(targetTime.getHours()).padStart(2, '0');
|
||
const tm = `${year}${month}${day}${hour}00`;
|
||
|
||
const url = 'https://apihub.kma.go.kr/api/typ01/url/kma_sfctm2.php';
|
||
|
||
console.log(`📡 기상청 실시간 날씨 조회: ${regionCode.name} (${tm})`);
|
||
|
||
const response = await axios.get(url, {
|
||
params: {
|
||
tm: tm,
|
||
stn: 0,
|
||
authKey: apiKey,
|
||
help: 0,
|
||
disp: 1,
|
||
},
|
||
timeout: 10000,
|
||
responseType: 'arraybuffer',
|
||
});
|
||
|
||
// EUC-KR 디코딩
|
||
const iconv = require('iconv-lite');
|
||
const responseText = iconv.decode(Buffer.from(response.data), 'EUC-KR');
|
||
|
||
// 데이터 파싱
|
||
const lines = responseText.split('\n').filter((line: string) => line.trim() && !line.startsWith('#'));
|
||
|
||
if (lines.length === 0) {
|
||
throw new Error('날씨 데이터가 없습니다.');
|
||
}
|
||
|
||
// 요청한 관측소 데이터 찾기 (없으면 첫 번째 관측소 사용)
|
||
let targetLine = lines.find((line: string) => {
|
||
const cols = line.trim().split(/\s+/);
|
||
return cols[1] === regionCode.stnId;
|
||
});
|
||
|
||
if (!targetLine) {
|
||
console.log(`⚠️ 관측소 ${regionCode.stnId} 데이터 없음. 다른 관측소 데이터 사용`);
|
||
targetLine = lines[0];
|
||
}
|
||
|
||
const values = targetLine.trim().split(/\s+/);
|
||
|
||
// 데이터 추출
|
||
const temperature = parseFloat(values[11]) || 0; // TA: 기온
|
||
const humidity = parseFloat(values[13]) || 0; // HM: 습도
|
||
const pressure = parseFloat(values[7]) || 1013; // PA: 기압
|
||
const windSpeed = parseFloat(values[3]) || 0; // WS: 풍속
|
||
const rainfall = parseFloat(values[15]) || 0; // RN: 강수량
|
||
|
||
console.log(`✅ 기상청 실시간 날씨: ${regionCode.name} ${temperature}°C, 습도 ${humidity}%`);
|
||
|
||
// 날씨 상태 추정
|
||
let weatherMain = 'Clear';
|
||
let weatherDescription = '맑음';
|
||
let weatherIcon = '01d';
|
||
let clouds = 10;
|
||
|
||
if (rainfall > 0) {
|
||
weatherMain = 'Rain';
|
||
weatherDescription = rainfall >= 10 ? '비 (강수)' : '비';
|
||
weatherIcon = rainfall >= 10 ? '10d' : '09d';
|
||
clouds = 100;
|
||
} else if (humidity > 80) {
|
||
weatherMain = 'Clouds';
|
||
weatherDescription = '흐림';
|
||
weatherIcon = '04d';
|
||
clouds = 90;
|
||
} else if (humidity > 60) {
|
||
weatherMain = 'Clouds';
|
||
weatherDescription = '구름 많음';
|
||
weatherIcon = '03d';
|
||
clouds = 60;
|
||
}
|
||
|
||
return {
|
||
city: regionCode.name,
|
||
country: 'KR',
|
||
temperature: Math.round(temperature),
|
||
feelsLike: Math.round(temperature - 2),
|
||
humidity: Math.round(humidity),
|
||
pressure: Math.round(pressure),
|
||
weatherMain,
|
||
weatherDescription,
|
||
weatherIcon,
|
||
windSpeed: Math.round(windSpeed * 10) / 10,
|
||
clouds,
|
||
timestamp: new Date().toISOString(),
|
||
};
|
||
} catch (error: any) {
|
||
console.error('❌ 기상청 실시간 날씨 조회 실패:', error.message);
|
||
return this.generateRealisticWeatherData(city);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 전체 알림 조회 (통합)
|
||
*/
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 도시명 → 기상청 관측소 코드 매핑
|
||
*/
|
||
private getKMARegionCode(city: string): { name: string; stnId: string } | null {
|
||
const regions: Record<string, { name: string; stnId: string }> = {
|
||
'Seoul': { name: '서울', stnId: '108' },
|
||
'Busan': { name: '부산', stnId: '159' },
|
||
'Incheon': { name: '인천', stnId: '112' },
|
||
'서울': { name: '서울', stnId: '108' },
|
||
'부산': { name: '부산', stnId: '159' },
|
||
'인천': { name: '인천', stnId: '112' },
|
||
'대구': { name: '대구', stnId: '143' },
|
||
'광주': { name: '광주', stnId: '156' },
|
||
'대전': { name: '대전', stnId: '133' },
|
||
'울산': { name: '울산', stnId: '152' },
|
||
'세종': { name: '세종', stnId: '239' },
|
||
'제주': { name: '제주', stnId: '184' },
|
||
};
|
||
return regions[city] || null;
|
||
}
|
||
|
||
/**
|
||
* 실시간 근사 날씨 데이터 생성
|
||
*/
|
||
private generateRealisticWeatherData(cityName: string): WeatherData {
|
||
const now = new Date();
|
||
const hour = now.getHours();
|
||
const month = now.getMonth() + 1;
|
||
|
||
// 시간대별 기온 변화
|
||
let baseTemp = 15;
|
||
if (hour >= 6 && hour < 9) baseTemp = 12;
|
||
else if (hour >= 9 && hour < 12) baseTemp = 18;
|
||
else if (hour >= 12 && hour < 15) baseTemp = 22;
|
||
else if (hour >= 15 && hour < 18) baseTemp = 20;
|
||
else if (hour >= 18 && hour < 21) baseTemp = 16;
|
||
else baseTemp = 13;
|
||
|
||
// 계절별 보정
|
||
if (month >= 12 || month <= 2) baseTemp -= 8;
|
||
else if (month >= 3 && month <= 5) baseTemp += 2;
|
||
else if (month >= 6 && month <= 8) baseTemp += 8;
|
||
else baseTemp += 3;
|
||
|
||
const tempVariation = Math.floor(Math.random() * 5) - 2;
|
||
const temperature = baseTemp + tempVariation;
|
||
|
||
return {
|
||
city: cityName,
|
||
country: 'KR',
|
||
temperature: Math.round(temperature),
|
||
feelsLike: Math.round(temperature - 2),
|
||
humidity: Math.floor(Math.random() * 30) + 50,
|
||
pressure: Math.floor(Math.random() * 15) + 1008,
|
||
weatherMain: 'Clear',
|
||
weatherDescription: '맑음',
|
||
weatherIcon: '01d',
|
||
windSpeed: Math.random() * 4 + 1,
|
||
clouds: 10,
|
||
timestamp: now.toISOString(),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 기상청 시간 형식 파싱 (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';
|
||
}
|
||
|
||
/**
|
||
* 테스트용 날씨 특보 더미 데이터
|
||
*/
|
||
private generateDummyWeatherAlerts(): Alert[] {
|
||
return [
|
||
{
|
||
id: `weather-${Date.now()}-1`,
|
||
type: 'weather',
|
||
severity: 'high',
|
||
title: '대설특보',
|
||
location: '강원 영동지역',
|
||
description: '시간당 2cm 이상 폭설. 차량 운행 주의',
|
||
timestamp: new Date(Date.now() - 30 * 60000).toISOString(),
|
||
},
|
||
{
|
||
id: `weather-${Date.now()}-2`,
|
||
type: 'weather',
|
||
severity: 'medium',
|
||
title: '강풍특보',
|
||
location: '남해안 전 지역',
|
||
description: '순간 풍속 20m/s 이상. 고속도로 주행 주의',
|
||
timestamp: new Date(Date.now() - 90 * 60000).toISOString(),
|
||
},
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 테스트용 교통사고 더미 데이터
|
||
*/
|
||
private generateDummyAccidentAlerts(): Alert[] {
|
||
return [
|
||
{
|
||
id: `accident-${Date.now()}-1`,
|
||
type: 'accident',
|
||
severity: 'high',
|
||
title: '교통사고 발생',
|
||
location: '경부고속도로 서울방향 189km',
|
||
description: '3중 추돌사고로 2차로 통제 중. 우회 권장',
|
||
timestamp: new Date(Date.now() - 10 * 60000).toISOString(),
|
||
},
|
||
{
|
||
id: `accident-${Date.now()}-2`,
|
||
type: 'accident',
|
||
severity: 'medium',
|
||
title: '사고 다발 지역',
|
||
location: '영동고속도로 강릉방향 160km',
|
||
description: '안개로 인한 가시거리 50m 이하. 서행 운전',
|
||
timestamp: new Date(Date.now() - 60 * 60000).toISOString(),
|
||
},
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 테스트용 도로공사 더미 데이터
|
||
*/
|
||
private generateDummyRoadworkAlerts(): Alert[] {
|
||
return [
|
||
{
|
||
id: `construction-${Date.now()}-1`,
|
||
type: 'construction',
|
||
severity: 'medium',
|
||
title: '도로 공사',
|
||
location: '서울외곽순환 목동IC~화곡IC',
|
||
description: '야간 공사로 1차로 통제 (22:00~06:00)',
|
||
timestamp: new Date(Date.now() - 45 * 60000).toISOString(),
|
||
},
|
||
{
|
||
id: `construction-${Date.now()}-2`,
|
||
type: 'construction',
|
||
severity: 'low',
|
||
title: '도로 통제',
|
||
location: '중부내륙고속도로 김천JC~현풍IC',
|
||
description: '도로 유지보수 작업. 차량 속도 제한 60km/h',
|
||
timestamp: new Date(Date.now() - 120 * 60000).toISOString(),
|
||
},
|
||
];
|
||
}
|
||
}
|
||
|