feat: initial commit - unified FGTools from static, weather, mattermost-noti
This commit is contained in:
31
services/weather/__init__.py
Normal file
31
services/weather/__init__.py
Normal file
@ -0,0 +1,31 @@
|
||||
# ===================================================================
|
||||
# services/weather/__init__.py
|
||||
# 기상 데이터 서비스 패키지 초기화
|
||||
# ===================================================================
|
||||
# 기상청 API를 통한 날씨 데이터 수집 및 처리 서비스입니다.
|
||||
# 초단기예보, 단기예보, 중기예보, ASOS 종관기상 데이터를 지원합니다.
|
||||
# ===================================================================
|
||||
|
||||
from .forecast import (
|
||||
get_ultra_forecast,
|
||||
get_vilage_forecast,
|
||||
get_daily_ultra_forecast,
|
||||
get_daily_vilage_forecast,
|
||||
get_midterm_forecast,
|
||||
get_midterm_temperature,
|
||||
get_weekly_precip,
|
||||
)
|
||||
from .asos import get_asos_weather
|
||||
from .precipitation import PrecipitationService
|
||||
|
||||
__all__ = [
|
||||
'get_ultra_forecast',
|
||||
'get_vilage_forecast',
|
||||
'get_daily_ultra_forecast',
|
||||
'get_daily_vilage_forecast',
|
||||
'get_midterm_forecast',
|
||||
'get_midterm_temperature',
|
||||
'get_weekly_precip',
|
||||
'get_asos_weather',
|
||||
'PrecipitationService',
|
||||
]
|
||||
383
services/weather/asos.py
Normal file
383
services/weather/asos.py
Normal file
@ -0,0 +1,383 @@
|
||||
# ===================================================================
|
||||
# services/weather/asos.py
|
||||
# 기상청 ASOS 종관기상 데이터 서비스 모듈
|
||||
# ===================================================================
|
||||
# 기상청 종관기상관측(ASOS) API를 통해 일별 기상 데이터를 조회합니다.
|
||||
# 과거 날씨 데이터 수집 및 DB 저장 기능을 제공합니다.
|
||||
# ===================================================================
|
||||
"""
|
||||
기상청 ASOS 종관기상 데이터 서비스 모듈
|
||||
|
||||
기상청 공공데이터포털의 종관기상관측(ASOS) API를 통해
|
||||
일별 기상 데이터(기온, 강수량, 습도 등)를 조회합니다.
|
||||
|
||||
사용 예시:
|
||||
from services.weather.asos import get_asos_weather, ASOSDataCollector
|
||||
|
||||
# 단일 기간 조회
|
||||
data = get_asos_weather(service_key, stn_id=99, start_dt='20240101', end_dt='20240131')
|
||||
|
||||
# 수집기를 통한 자동 데이터 수집
|
||||
collector = ASOSDataCollector(service_key)
|
||||
collector.collect_and_save([99])
|
||||
"""
|
||||
|
||||
import traceback
|
||||
from datetime import datetime, timedelta, date
|
||||
from typing import Dict, List, Optional, Generator, Tuple, Any
|
||||
|
||||
from sqlalchemy import select, Table
|
||||
from sqlalchemy.dialects.mysql import insert as mysql_insert
|
||||
from sqlalchemy.engine import Connection
|
||||
|
||||
from core.logging_utils import get_logger
|
||||
from core.http_client import create_retry_session
|
||||
from core.config import get_config
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# ASOS API URL
|
||||
ASOS_API_URL = "http://apis.data.go.kr/1360000/AsosDalyInfoService/getWthrDataList"
|
||||
|
||||
# 시간 관련 컬럼 (음수 값을 null로 처리)
|
||||
HRMT_KEYS = [
|
||||
"minTaHrmt", "maxTaHrmt", "mi10MaxRnHrmt", "hr1MaxRnHrmt",
|
||||
"maxInsWsHrmt", "maxWsHrmt", "minRhmHrmt", "maxPsHrmt",
|
||||
"minPsHrmt", "hr1MaxIcsrHrmt", "ddMefsHrmt", "ddMesHrmt"
|
||||
]
|
||||
|
||||
|
||||
def fetch_date_range_chunks(
|
||||
start_dt: str,
|
||||
end_dt: str,
|
||||
chunk_days: int = 10
|
||||
) -> Generator[Tuple[str, str], None, None]:
|
||||
"""
|
||||
날짜 범위를 청크 단위로 분할
|
||||
|
||||
대량의 데이터를 조회할 때 API 요청을 분할하여 처리합니다.
|
||||
|
||||
Args:
|
||||
start_dt: 시작 날짜 (YYYYMMDD)
|
||||
end_dt: 종료 날짜 (YYYYMMDD)
|
||||
chunk_days: 청크 크기 (일 단위)
|
||||
|
||||
Yields:
|
||||
(시작일, 종료일) 튜플
|
||||
"""
|
||||
current_start = datetime.strptime(start_dt, "%Y%m%d")
|
||||
end_date = datetime.strptime(end_dt, "%Y%m%d")
|
||||
|
||||
while current_start <= end_date:
|
||||
current_end = min(current_start + timedelta(days=chunk_days - 1), end_date)
|
||||
yield current_start.strftime("%Y%m%d"), current_end.strftime("%Y%m%d")
|
||||
current_start = current_end + timedelta(days=1)
|
||||
|
||||
|
||||
def get_asos_weather(
|
||||
service_key: str,
|
||||
stn_id: int,
|
||||
start_dt: str,
|
||||
end_dt: str,
|
||||
session = None
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
ASOS 종관기상 데이터 조회
|
||||
|
||||
기상청 ASOS API를 호출하여 일별 기상 데이터를 조회합니다.
|
||||
|
||||
Args:
|
||||
service_key: 공공데이터포털 API 키
|
||||
stn_id: 관측 지점 ID
|
||||
start_dt: 시작 날짜 (YYYYMMDD)
|
||||
end_dt: 종료 날짜 (YYYYMMDD)
|
||||
session: requests 세션 (재사용용)
|
||||
|
||||
Returns:
|
||||
일별 기상 데이터 리스트
|
||||
|
||||
데이터 항목:
|
||||
- tm: 일시 (YYYY-MM-DD)
|
||||
- avgTa: 평균 기온 (℃)
|
||||
- minTa: 최저 기온 (℃)
|
||||
- maxTa: 최고 기온 (℃)
|
||||
- sumRn: 일 강수량 (mm)
|
||||
- avgRhm: 평균 상대습도 (%)
|
||||
- avgWs: 평균 풍속 (m/s)
|
||||
등
|
||||
"""
|
||||
if session is None:
|
||||
session = create_retry_session(retries=3)
|
||||
|
||||
params = {
|
||||
"serviceKey": service_key,
|
||||
"pageNo": "1",
|
||||
"numOfRows": "500",
|
||||
"dataType": "JSON",
|
||||
"dataCd": "ASOS",
|
||||
"dateCd": "DAY",
|
||||
"startDt": start_dt,
|
||||
"endDt": end_dt,
|
||||
"stnIds": str(stn_id),
|
||||
}
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
try:
|
||||
response = session.get(ASOS_API_URL, params=params, headers=headers, timeout=20)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
items = data.get("response", {}).get("body", {}).get("items", {}).get("item", [])
|
||||
|
||||
if items is None:
|
||||
return []
|
||||
|
||||
# 단일 항목인 경우 리스트로 변환
|
||||
if isinstance(items, dict):
|
||||
items = [items]
|
||||
|
||||
logger.debug(f"ASOS 데이터 조회 완료: 지점={stn_id}, 건수={len(items)}")
|
||||
return items
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"ASOS API 요청 실패: {e}")
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
|
||||
class ASOSDataCollector:
|
||||
"""
|
||||
ASOS 데이터 자동 수집기
|
||||
|
||||
설정에 따라 ASOS 데이터를 자동으로 수집하고 DB에 저장합니다.
|
||||
마지막 저장 일자를 확인하여 중복 없이 증분 수집합니다.
|
||||
|
||||
Attributes:
|
||||
service_key: API 서비스 키
|
||||
force_update: 기존 데이터 덮어쓰기 여부
|
||||
debug: 디버그 모드 (True면 실제 저장 안 함)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
service_key: Optional[str] = None,
|
||||
force_update: bool = False,
|
||||
debug: bool = False
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
service_key: API 키 (None이면 설정에서 로드)
|
||||
force_update: 기존 데이터 덮어쓰기 여부
|
||||
debug: 디버그 모드
|
||||
"""
|
||||
if service_key is None:
|
||||
config = get_config()
|
||||
service_key = config.data_api['service_key']
|
||||
|
||||
self.service_key = service_key
|
||||
self.force_update = force_update
|
||||
self.debug = debug
|
||||
self.session = create_retry_session(retries=3)
|
||||
|
||||
def get_latest_date_from_db(self, conn: Connection, table: Table) -> Optional[date]:
|
||||
"""
|
||||
DB에서 가장 최근 저장된 날짜 조회
|
||||
|
||||
Args:
|
||||
conn: DB 연결
|
||||
table: 대상 테이블
|
||||
|
||||
Returns:
|
||||
최근 날짜 또는 None
|
||||
"""
|
||||
sel = select(table.c.date).order_by(table.c.date.desc()).limit(1)
|
||||
result = conn.execute(sel).fetchone()
|
||||
return result[0] if result else None
|
||||
|
||||
def parse_item_to_record(self, item: Dict, table: Table) -> Optional[Dict]:
|
||||
"""
|
||||
API 응답 아이템을 DB 레코드로 변환
|
||||
|
||||
Args:
|
||||
item: API 응답 아이템
|
||||
table: 대상 테이블
|
||||
|
||||
Returns:
|
||||
DB 레코드 딕셔너리 또는 None
|
||||
"""
|
||||
date_str = item.get("tm")
|
||||
if not date_str:
|
||||
return None
|
||||
|
||||
try:
|
||||
record_date = datetime.strptime(date_str, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
logger.warning(f"날짜 파싱 실패: {date_str}")
|
||||
return None
|
||||
|
||||
data = {"date": record_date}
|
||||
|
||||
for key in table.c.keys():
|
||||
if key == "date":
|
||||
continue
|
||||
|
||||
value = item.get(key)
|
||||
|
||||
# 빈 값 처리
|
||||
if value in ["", None, "-"]:
|
||||
data[key] = None
|
||||
continue
|
||||
|
||||
try:
|
||||
# 시간 관련 컬럼 또는 특수 컬럼 처리
|
||||
if key in HRMT_KEYS or key == "iscs":
|
||||
fval = float(value)
|
||||
data[key] = str(int(fval)) if fval >= 0 else None
|
||||
elif key == "stnId":
|
||||
data[key] = int(float(value))
|
||||
else:
|
||||
data[key] = float(value)
|
||||
except (ValueError, TypeError):
|
||||
data[key] = None
|
||||
|
||||
return data
|
||||
|
||||
def save_items_to_db(
|
||||
self,
|
||||
items: List[Dict],
|
||||
conn: Connection,
|
||||
table: Table
|
||||
) -> int:
|
||||
"""
|
||||
데이터 항목들을 DB에 저장
|
||||
|
||||
Args:
|
||||
items: 저장할 데이터 리스트
|
||||
conn: DB 연결
|
||||
table: 대상 테이블
|
||||
|
||||
Returns:
|
||||
저장된 레코드 수
|
||||
"""
|
||||
saved_count = 0
|
||||
|
||||
for item in items:
|
||||
data = self.parse_item_to_record(item, table)
|
||||
if not data:
|
||||
continue
|
||||
|
||||
record_date = data['date']
|
||||
|
||||
if self.debug:
|
||||
logger.debug(f"[DEBUG] {record_date} DB 저장 시도: {data}")
|
||||
continue
|
||||
|
||||
try:
|
||||
if self.force_update:
|
||||
# UPSERT: 존재하면 업데이트, 없으면 삽입
|
||||
stmt = mysql_insert(table).values(**data)
|
||||
stmt = stmt.on_duplicate_key_update(**data)
|
||||
conn.execute(stmt)
|
||||
logger.info(f"{record_date} 저장/업데이트 완료")
|
||||
else:
|
||||
# 중복 확인 후 삽입
|
||||
sel = select(table.c.date).where(table.c.date == record_date)
|
||||
if conn.execute(sel).fetchone():
|
||||
logger.debug(f"{record_date} 이미 존재, 저장 생략")
|
||||
continue
|
||||
|
||||
conn.execute(table.insert().values(**data))
|
||||
logger.info(f"{record_date} 저장 완료")
|
||||
|
||||
saved_count += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"저장 실패 ({record_date}): {e}")
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
return saved_count
|
||||
|
||||
def collect_and_save(
|
||||
self,
|
||||
stn_ids: List[int],
|
||||
engine,
|
||||
table: Table,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
chunk_days: int = 1000
|
||||
) -> int:
|
||||
"""
|
||||
데이터 수집 및 저장 실행
|
||||
|
||||
Args:
|
||||
stn_ids: 관측 지점 ID 리스트
|
||||
engine: SQLAlchemy 엔진
|
||||
table: 대상 테이블
|
||||
start_date: 시작 날짜 (None이면 자동 계산)
|
||||
end_date: 종료 날짜 (None이면 자동 계산)
|
||||
chunk_days: 청크 크기
|
||||
|
||||
Returns:
|
||||
총 저장된 레코드 수
|
||||
"""
|
||||
now = datetime.now()
|
||||
today = now.date()
|
||||
|
||||
# 종료일 계산 (오전 11시 이전이면 전전일, 이후면 전일)
|
||||
if end_date is None:
|
||||
if now.hour < 11:
|
||||
end_dt = (today - timedelta(days=2)).strftime("%Y%m%d")
|
||||
logger.info(f"오전 11시 이전, 전전일까지 조회: {end_dt}")
|
||||
else:
|
||||
end_dt = (today - timedelta(days=1)).strftime("%Y%m%d")
|
||||
logger.info(f"전일까지 조회: {end_dt}")
|
||||
else:
|
||||
end_dt = end_date
|
||||
|
||||
total_saved = 0
|
||||
|
||||
with engine.begin() as conn:
|
||||
# 시작일 계산
|
||||
if start_date is None:
|
||||
latest_date = self.get_latest_date_from_db(conn, table)
|
||||
if latest_date:
|
||||
start_dt = (latest_date + timedelta(days=1)).strftime("%Y%m%d")
|
||||
logger.info(f"마지막 저장일: {latest_date}, 시작일: {start_dt}")
|
||||
else:
|
||||
config = get_config()
|
||||
start_dt = config.data_api.get('start_date', '20170101')
|
||||
logger.info(f"저장된 데이터 없음, 기본 시작일: {start_dt}")
|
||||
else:
|
||||
start_dt = start_date
|
||||
|
||||
# 날짜 검증
|
||||
if start_dt > end_dt:
|
||||
logger.info("최신 데이터가 이미 존재합니다.")
|
||||
return 0
|
||||
|
||||
# 각 관측 지점별 데이터 수집
|
||||
for stn_id in stn_ids:
|
||||
for chunk_start, chunk_end in fetch_date_range_chunks(start_dt, end_dt, chunk_days):
|
||||
logger.info(f"지점 {stn_id} 데이터 요청: {chunk_start} ~ {chunk_end}")
|
||||
|
||||
items = get_asos_weather(
|
||||
self.service_key,
|
||||
stn_id,
|
||||
chunk_start,
|
||||
chunk_end,
|
||||
self.session
|
||||
)
|
||||
|
||||
if items:
|
||||
saved = self.save_items_to_db(items, conn, table)
|
||||
total_saved += saved
|
||||
else:
|
||||
logger.warning(f"지점 {stn_id} {chunk_start}~{chunk_end} 데이터 없음")
|
||||
|
||||
logger.info(f"총 {total_saved}건 저장 완료")
|
||||
return total_saved
|
||||
530
services/weather/forecast.py
Normal file
530
services/weather/forecast.py
Normal file
@ -0,0 +1,530 @@
|
||||
# ===================================================================
|
||||
# services/weather/forecast.py
|
||||
# 기상청 예보 API 서비스 모듈
|
||||
# ===================================================================
|
||||
# 기상청 공공데이터포털 API를 통해 다양한 예보 데이터를 조회합니다:
|
||||
# - 초단기예보: 향후 6시간 이내 예보
|
||||
# - 단기예보: 향후 3일간 예보
|
||||
# - 중기예보: 3~10일 후 예보
|
||||
# ===================================================================
|
||||
"""
|
||||
기상청 예보 API 서비스 모듈
|
||||
|
||||
기상청 공공데이터포털 API를 통해 초단기, 단기, 중기 예보 데이터를 조회합니다.
|
||||
|
||||
사용 예시:
|
||||
from services.weather.forecast import get_daily_vilage_forecast, get_midterm_forecast
|
||||
|
||||
# 단기 예보 조회
|
||||
forecast = get_daily_vilage_forecast(service_key)
|
||||
|
||||
# 중기 예보 조회
|
||||
precip_probs, raw_data = get_midterm_forecast(service_key)
|
||||
"""
|
||||
|
||||
import re
|
||||
from datetime import datetime, timedelta, date
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
|
||||
import requests
|
||||
|
||||
from core.logging_utils import get_logger
|
||||
from core.http_client import create_retry_session
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 기상청 API 기본 URL
|
||||
VILAGE_FCST_URL = "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0"
|
||||
MID_FCST_URL = "http://apis.data.go.kr/1360000/MidFcstInfoService"
|
||||
|
||||
# 기본 좌표 (파주 지역)
|
||||
DEFAULT_NX = 57
|
||||
DEFAULT_NY = 130
|
||||
|
||||
|
||||
def parse_precip(value: Any) -> float:
|
||||
"""
|
||||
강수량 텍스트를 숫자(mm)로 변환
|
||||
|
||||
기상청 API에서 반환하는 강수량 텍스트를 파싱합니다.
|
||||
|
||||
Args:
|
||||
value: 강수량 값 ('강수없음', '1mm 미만', '3.5mm' 등)
|
||||
|
||||
Returns:
|
||||
강수량 (mm). 파싱 실패 시 0.0
|
||||
|
||||
Examples:
|
||||
>>> parse_precip('강수없음')
|
||||
0.0
|
||||
>>> parse_precip('1mm 미만')
|
||||
0.5
|
||||
>>> parse_precip('3.5')
|
||||
3.5
|
||||
"""
|
||||
if value == '강수없음':
|
||||
return 0.0
|
||||
elif '1mm 미만' in str(value):
|
||||
return 0.5
|
||||
else:
|
||||
# 숫자 추출 시도
|
||||
match = re.search(r"[\d.]+", str(value))
|
||||
if match:
|
||||
try:
|
||||
return float(match.group())
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
return float(value)
|
||||
except (ValueError, TypeError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def get_latest_base_datetime(now: Optional[datetime] = None) -> Tuple[str, str]:
|
||||
"""
|
||||
최신 발표 시각 계산
|
||||
|
||||
단기예보 API의 base_time은 특정 시간에만 발표됩니다.
|
||||
현재 시간 기준 가장 최근 발표 시각을 계산합니다.
|
||||
|
||||
Args:
|
||||
now: 기준 시간 (None이면 현재 시간)
|
||||
|
||||
Returns:
|
||||
(base_date, base_time) 튜플 (YYYYMMDD, HHMM)
|
||||
"""
|
||||
if now is None:
|
||||
now = datetime.now()
|
||||
|
||||
# 발표 시각 목록 (하루 8회)
|
||||
base_times = ["0200", "0500", "0800", "1100", "1400", "1700", "2000", "2300"]
|
||||
|
||||
# 현재 시간에 맞는 가장 최근 발표 시각 찾기
|
||||
candidate = None
|
||||
for bt in reversed(base_times):
|
||||
hour = int(bt[:2])
|
||||
minute = int(bt[2:])
|
||||
|
||||
if (now.hour > hour) or (now.hour == hour and now.minute >= minute + 10):
|
||||
candidate = bt
|
||||
break
|
||||
|
||||
# 적합한 시각이 없으면 전날 마지막 발표 사용
|
||||
if candidate is None:
|
||||
candidate = "2300"
|
||||
now -= timedelta(days=1)
|
||||
|
||||
base_date = now.strftime("%Y%m%d")
|
||||
return base_date, candidate
|
||||
|
||||
|
||||
def get_ultra_forecast(
|
||||
service_key: str,
|
||||
base_date: Optional[str] = None,
|
||||
base_time: Optional[str] = None,
|
||||
nx: int = DEFAULT_NX,
|
||||
ny: int = DEFAULT_NY
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
초단기예보 조회 (향후 6시간)
|
||||
|
||||
기상청 초단기예보 API를 호출하여 원시 데이터를 반환합니다.
|
||||
|
||||
Args:
|
||||
service_key: 공공데이터포털 API 키
|
||||
base_date: 발표 날짜 (YYYYMMDD). None이면 현재 날짜
|
||||
base_time: 발표 시각 (HHMM). None이면 자동 계산
|
||||
nx: 격자 X 좌표
|
||||
ny: 격자 Y 좌표
|
||||
|
||||
Returns:
|
||||
예보 아이템 리스트 (기상청 API 원시 응답)
|
||||
"""
|
||||
if base_date is None or base_time is None:
|
||||
base_date, base_time = get_latest_base_datetime()
|
||||
|
||||
url = f"{VILAGE_FCST_URL}/getUltraSrtFcst"
|
||||
params = {
|
||||
'serviceKey': service_key,
|
||||
'numOfRows': '1000',
|
||||
'pageNo': '1',
|
||||
'dataType': 'JSON',
|
||||
'base_date': base_date,
|
||||
'base_time': base_time,
|
||||
'nx': str(nx),
|
||||
'ny': str(ny)
|
||||
}
|
||||
|
||||
try:
|
||||
session = create_retry_session(retries=3)
|
||||
response = session.get(url, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
items = data.get('response', {}).get('body', {}).get('items', {}).get('item', [])
|
||||
|
||||
logger.debug(f"초단기예보 조회 완료: {len(items)}건")
|
||||
return items
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"초단기예보 조회 실패: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def get_vilage_forecast(
|
||||
service_key: str,
|
||||
base_date: Optional[str] = None,
|
||||
base_time: str = "0200",
|
||||
nx: int = DEFAULT_NX,
|
||||
ny: int = DEFAULT_NY
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
단기예보 조회 (향후 3일)
|
||||
|
||||
기상청 단기예보 API를 호출하여 원시 데이터를 반환합니다.
|
||||
|
||||
Args:
|
||||
service_key: 공공데이터포털 API 키
|
||||
base_date: 발표 날짜 (YYYYMMDD). None이면 현재 날짜
|
||||
base_time: 발표 시각 (기본: 0200)
|
||||
nx: 격자 X 좌표
|
||||
ny: 격자 Y 좌표
|
||||
|
||||
Returns:
|
||||
예보 아이템 리스트 (기상청 API 원시 응답)
|
||||
"""
|
||||
if base_date is None:
|
||||
base_date = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
url = f"{VILAGE_FCST_URL}/getVilageFcst"
|
||||
params = {
|
||||
'serviceKey': service_key,
|
||||
'numOfRows': '1000',
|
||||
'pageNo': '1',
|
||||
'dataType': 'JSON',
|
||||
'base_date': base_date,
|
||||
'base_time': base_time,
|
||||
'nx': str(nx),
|
||||
'ny': str(ny)
|
||||
}
|
||||
|
||||
try:
|
||||
session = create_retry_session(retries=3)
|
||||
response = session.get(url, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
items = data.get('response', {}).get('body', {}).get('items', {}).get('item', [])
|
||||
|
||||
logger.debug(f"단기예보 조회 완료: {len(items)}건")
|
||||
return items
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"단기예보 조회 실패: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def get_daily_ultra_forecast(service_key: str) -> Dict[str, Dict]:
|
||||
"""
|
||||
초단기예보 일별 요약 데이터 조회
|
||||
|
||||
초단기예보 데이터를 날짜별로 집계하여 반환합니다.
|
||||
|
||||
Args:
|
||||
service_key: 공공데이터포털 API 키
|
||||
|
||||
Returns:
|
||||
날짜별 요약 딕셔너리
|
||||
{날짜: {'sumRn': 강수량합계, 'minTa': 최저기온, 'maxTa': 최고기온, 'avgRhm': 평균습도}}
|
||||
"""
|
||||
base_date, base_time = get_latest_base_datetime()
|
||||
items = get_ultra_forecast(service_key, base_date, base_time)
|
||||
|
||||
if not items:
|
||||
return {}
|
||||
|
||||
# 날짜별 데이터 집계
|
||||
daily_data: Dict[str, Dict] = {}
|
||||
|
||||
for item in items:
|
||||
dt = item.get('fcstDate', '')
|
||||
cat = item.get('category', '')
|
||||
val = item.get('fcstValue', '')
|
||||
|
||||
if not dt:
|
||||
continue
|
||||
|
||||
if dt not in daily_data:
|
||||
daily_data[dt] = {'sumRn': 0, 'temps': [], 'rhm': []}
|
||||
|
||||
# 카테고리별 처리
|
||||
if cat == 'RN1': # 1시간 강수량
|
||||
daily_data[dt]['sumRn'] += parse_precip(val)
|
||||
elif cat == 'T1H': # 기온
|
||||
try:
|
||||
daily_data[dt]['temps'].append(float(val))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
elif cat == 'REH': # 습도
|
||||
try:
|
||||
daily_data[dt]['rhm'].append(float(val))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 집계 결과 변환
|
||||
result = {}
|
||||
for dt, vals in daily_data.items():
|
||||
temps = vals['temps']
|
||||
rhm = vals['rhm']
|
||||
|
||||
result[dt] = {
|
||||
'sumRn': round(vals['sumRn'], 2),
|
||||
'minTa': min(temps) if temps else 0,
|
||||
'maxTa': max(temps) if temps else 0,
|
||||
'avgRhm': round(sum(rhm) / len(rhm), 1) if rhm else 0
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_daily_vilage_forecast(service_key: str) -> Dict[str, Dict]:
|
||||
"""
|
||||
단기예보 일별 요약 데이터 조회
|
||||
|
||||
단기예보 데이터를 날짜별로 집계하여 반환합니다.
|
||||
|
||||
Args:
|
||||
service_key: 공공데이터포털 API 키
|
||||
|
||||
Returns:
|
||||
날짜별 요약 딕셔너리
|
||||
{날짜: {'sumRn': 강수량합계, 'minTa': 최저기온, 'maxTa': 최고기온, 'avgRhm': 평균습도}}
|
||||
"""
|
||||
base_date, _ = get_latest_base_datetime()
|
||||
items = get_vilage_forecast(service_key, base_date)
|
||||
|
||||
if not items:
|
||||
return {}
|
||||
|
||||
# 날짜별 데이터 집계
|
||||
daily_data: Dict[str, Dict] = {}
|
||||
|
||||
for item in items:
|
||||
dt = item.get('fcstDate', '')
|
||||
cat = item.get('category', '')
|
||||
val = item.get('fcstValue', '')
|
||||
|
||||
if not dt:
|
||||
continue
|
||||
|
||||
if dt not in daily_data:
|
||||
daily_data[dt] = {'sumRn': 0, 'minTa': [], 'maxTa': [], 'rhm': []}
|
||||
|
||||
# 카테고리별 처리
|
||||
if cat == 'PCP': # 강수량
|
||||
daily_data[dt]['sumRn'] += parse_precip(val)
|
||||
elif cat == 'TMN': # 최저기온
|
||||
try:
|
||||
daily_data[dt]['minTa'].append(float(val))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
elif cat == 'TMX': # 최고기온
|
||||
try:
|
||||
daily_data[dt]['maxTa'].append(float(val))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
elif cat == 'REH': # 습도
|
||||
try:
|
||||
daily_data[dt]['rhm'].append(float(val))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 집계 결과 변환
|
||||
result = {}
|
||||
for dt, vals in daily_data.items():
|
||||
min_ta = vals['minTa']
|
||||
max_ta = vals['maxTa']
|
||||
rhm = vals['rhm']
|
||||
|
||||
result[dt] = {
|
||||
'sumRn': round(vals['sumRn'], 2),
|
||||
'minTa': min(min_ta) if min_ta else 0,
|
||||
'maxTa': max(max_ta) if max_ta else 0,
|
||||
'avgRhm': round(sum(rhm) / len(rhm), 1) if rhm else 0
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_midterm_forecast(
|
||||
service_key: str,
|
||||
reg_id: str = '11B20305'
|
||||
) -> Tuple[Dict[int, int], Dict]:
|
||||
"""
|
||||
중기 강수확률 예보 조회 (3~10일 후)
|
||||
|
||||
Args:
|
||||
service_key: 공공데이터포털 API 키
|
||||
reg_id: 예보 지역 코드 (기본: 파주)
|
||||
|
||||
Returns:
|
||||
(강수확률 딕셔너리, 원시 응답) 튜플
|
||||
강수확률: {일수: 확률} (예: {3: 30, 4: 20, ...})
|
||||
"""
|
||||
url = f"{MID_FCST_URL}/getMidLandFcst"
|
||||
|
||||
# 발표 시각 계산 (06시 또는 18시)
|
||||
now = datetime.now()
|
||||
if now.hour < 6:
|
||||
tm_fc = (now - timedelta(days=1)).strftime("%Y%m%d") + "1800"
|
||||
elif now.hour < 18:
|
||||
tm_fc = now.strftime("%Y%m%d") + "0600"
|
||||
else:
|
||||
tm_fc = now.strftime("%Y%m%d") + "1800"
|
||||
|
||||
params = {
|
||||
'serviceKey': service_key,
|
||||
'regId': reg_id,
|
||||
'tmFc': tm_fc,
|
||||
'numOfRows': '10',
|
||||
'pageNo': '1',
|
||||
'dataType': 'JSON',
|
||||
}
|
||||
|
||||
try:
|
||||
session = create_retry_session(retries=3)
|
||||
response = session.get(url, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
items = data.get('response', {}).get('body', {}).get('items', {}).get('item', [])
|
||||
|
||||
if not items:
|
||||
logger.warning(f"중기예보 응답 없음: tmFc={tm_fc}, regId={reg_id}")
|
||||
return {}, {}
|
||||
|
||||
item = items[0]
|
||||
|
||||
# 3~10일 후 강수확률 추출
|
||||
precip_probs = {}
|
||||
for day in range(3, 11):
|
||||
key = f'rnSt{day}'
|
||||
try:
|
||||
precip_probs[day] = int(item.get(key, 0))
|
||||
except (ValueError, TypeError):
|
||||
precip_probs[day] = 0
|
||||
|
||||
logger.debug(f"중기 강수확률 조회 완료: {precip_probs}")
|
||||
return precip_probs, item
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"중기예보 조회 실패: {e}")
|
||||
return {}, {}
|
||||
|
||||
|
||||
def get_midterm_temperature(
|
||||
service_key: str,
|
||||
reg_id: str = '11B20305'
|
||||
) -> Dict[int, Dict[str, int]]:
|
||||
"""
|
||||
중기 기온 예보 조회 (3~10일 후)
|
||||
|
||||
Args:
|
||||
service_key: 공공데이터포털 API 키
|
||||
reg_id: 예보 지역 코드 (기본: 파주)
|
||||
|
||||
Returns:
|
||||
일자별 기온 딕셔너리
|
||||
{일수: {'min': 최저기온, 'max': 최고기온}}
|
||||
"""
|
||||
url = f"{MID_FCST_URL}/getMidTa"
|
||||
|
||||
# 발표 시각 계산
|
||||
now = datetime.now()
|
||||
if now.hour < 6:
|
||||
tm_fc = (now - timedelta(days=1)).strftime("%Y%m%d") + "1800"
|
||||
elif now.hour < 18:
|
||||
tm_fc = now.strftime("%Y%m%d") + "0600"
|
||||
else:
|
||||
tm_fc = now.strftime("%Y%m%d") + "1800"
|
||||
|
||||
params = {
|
||||
'serviceKey': service_key,
|
||||
'regId': reg_id,
|
||||
'tmFc': tm_fc,
|
||||
'pageNo': '1',
|
||||
'numOfRows': '10',
|
||||
'dataType': 'JSON'
|
||||
}
|
||||
|
||||
try:
|
||||
session = create_retry_session(retries=3)
|
||||
response = session.get(url, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
items = data.get('response', {}).get('body', {}).get('items', {}).get('item', [])
|
||||
|
||||
if not items:
|
||||
logger.warning(f"중기기온예보 응답 없음: tmFc={tm_fc}, regId={reg_id}")
|
||||
return {}
|
||||
|
||||
item = items[0]
|
||||
|
||||
# 3~10일 후 기온 추출
|
||||
temps = {}
|
||||
for day in range(3, 11):
|
||||
min_key = f'taMin{day}'
|
||||
max_key = f'taMax{day}'
|
||||
try:
|
||||
temps[day] = {
|
||||
'min': int(item.get(min_key, 0)),
|
||||
'max': int(item.get(max_key, 0))
|
||||
}
|
||||
except (ValueError, TypeError):
|
||||
temps[day] = {'min': 0, 'max': 0}
|
||||
|
||||
logger.debug(f"중기 기온 조회 완료: {temps}")
|
||||
return temps
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"중기기온예보 조회 실패: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def get_weekly_precip(service_key: str) -> Dict[str, float]:
|
||||
"""
|
||||
금주 일요일까지의 일별 예상 강수량 조회
|
||||
|
||||
단기예보와 중기예보를 조합하여 금주 일요일까지의 강수량을 예측합니다.
|
||||
|
||||
Args:
|
||||
service_key: 공공데이터포털 API 키
|
||||
|
||||
Returns:
|
||||
날짜별 예상 강수량 딕셔너리 {YYYYMMDD: 강수량(mm)}
|
||||
"""
|
||||
today = date.today()
|
||||
sunday = today + timedelta(days=(6 - today.weekday()))
|
||||
|
||||
result = {}
|
||||
|
||||
# 단기예보 데이터 (오늘~+2일)
|
||||
vilage_data = get_daily_vilage_forecast(service_key)
|
||||
for dt, vals in vilage_data.items():
|
||||
dt_date = datetime.strptime(dt, "%Y%m%d").date()
|
||||
if dt_date <= sunday:
|
||||
result[dt] = vals['sumRn']
|
||||
|
||||
# 중기예보 강수확률 (3일 이후)
|
||||
precip_probs, _ = get_midterm_forecast(service_key)
|
||||
for day, prob in precip_probs.items():
|
||||
target_date = today + timedelta(days=day)
|
||||
if target_date <= sunday:
|
||||
dt = target_date.strftime("%Y%m%d")
|
||||
if dt not in result:
|
||||
# 강수확률을 기반으로 예상 강수량 추정
|
||||
# (간단한 휴리스틱: 확률 * 0.1mm)
|
||||
result[dt] = round(prob * 0.1, 1)
|
||||
|
||||
return result
|
||||
343
services/weather/precipitation.py
Normal file
343
services/weather/precipitation.py
Normal file
@ -0,0 +1,343 @@
|
||||
# ===================================================================
|
||||
# services/weather/precipitation.py
|
||||
# 강수량 데이터 서비스 모듈
|
||||
# ===================================================================
|
||||
# 시간별 강수량 예보 데이터를 조회하고 요약 정보를 생성합니다.
|
||||
# HTML 테이블 생성 및 SQLite/MySQL 저장을 지원합니다.
|
||||
# ===================================================================
|
||||
"""
|
||||
강수량 데이터 서비스 모듈
|
||||
|
||||
시간별 강수량 예보 데이터를 조회하고 요약 정보를 생성합니다.
|
||||
다양한 출력 형식(HTML, JSON, 텍스트)을 지원합니다.
|
||||
|
||||
사용 예시:
|
||||
from services.weather.precipitation import PrecipitationService
|
||||
|
||||
service = PrecipitationService(service_key)
|
||||
summary = service.get_daily_summary()
|
||||
html = service.generate_html_table()
|
||||
"""
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
|
||||
from core.logging_utils import get_logger
|
||||
from core.config import get_config
|
||||
from .forecast import parse_precip, get_ultra_forecast, get_vilage_forecast
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PrecipitationService:
|
||||
"""
|
||||
강수량 데이터 서비스 클래스
|
||||
|
||||
초단기예보와 단기예보를 조합하여 시간별 강수량 예보를 제공합니다.
|
||||
|
||||
Attributes:
|
||||
service_key: 기상청 API 서비스 키
|
||||
start_hour: 집계 시작 시간 (기본: 10)
|
||||
end_hour: 집계 종료 시간 (기본: 22)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
service_key: Optional[str] = None,
|
||||
start_hour: int = 10,
|
||||
end_hour: int = 22
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
service_key: API 키 (None이면 설정에서 로드)
|
||||
start_hour: 집계 시작 시간
|
||||
end_hour: 집계 종료 시간
|
||||
"""
|
||||
if service_key is None:
|
||||
config = get_config()
|
||||
service_key = config.weather_service.get('service_key') or config.data_api.get('service_key', '')
|
||||
|
||||
self.service_key = service_key
|
||||
self.start_hour = start_hour
|
||||
self.end_hour = end_hour
|
||||
|
||||
def get_hourly_ultra_data(self, target_date: Optional[str] = None) -> Dict[int, float]:
|
||||
"""
|
||||
초단기예보에서 시간별 강수량 추출
|
||||
|
||||
Args:
|
||||
target_date: 대상 날짜 (YYYYMMDD). None이면 오늘
|
||||
|
||||
Returns:
|
||||
시간별 강수량 딕셔너리 {시간: 강수량(mm)}
|
||||
"""
|
||||
if target_date is None:
|
||||
target_date = datetime.now().strftime('%Y%m%d')
|
||||
|
||||
items = get_ultra_forecast(self.service_key)
|
||||
result = {}
|
||||
|
||||
for item in items:
|
||||
if item.get('category') != 'RN1':
|
||||
continue
|
||||
if item.get('fcstDate') != target_date:
|
||||
continue
|
||||
|
||||
try:
|
||||
hour = int(item['fcstTime'][:2])
|
||||
if self.start_hour <= hour <= self.end_hour:
|
||||
result[hour] = parse_precip(item['fcstValue'])
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
|
||||
return result
|
||||
|
||||
def get_hourly_vilage_data(self, target_date: Optional[str] = None) -> Dict[int, float]:
|
||||
"""
|
||||
단기예보에서 시간별 강수량 추출
|
||||
|
||||
Args:
|
||||
target_date: 대상 날짜 (YYYYMMDD). None이면 오늘
|
||||
|
||||
Returns:
|
||||
시간별 강수량 딕셔너리 {시간: 강수량(mm)}
|
||||
"""
|
||||
if target_date is None:
|
||||
target_date = datetime.now().strftime('%Y%m%d')
|
||||
|
||||
items = get_vilage_forecast(self.service_key, base_date=target_date)
|
||||
result = {}
|
||||
|
||||
for item in items:
|
||||
if item.get('category') != 'PCP':
|
||||
continue
|
||||
if item.get('fcstDate') != target_date:
|
||||
continue
|
||||
|
||||
try:
|
||||
hour = int(item['fcstTime'][:2])
|
||||
if self.start_hour <= hour <= self.end_hour:
|
||||
result[hour] = parse_precip(item['fcstValue'])
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
|
||||
return result
|
||||
|
||||
def get_daily_summary(
|
||||
self,
|
||||
target_date: Optional[str] = None
|
||||
) -> Tuple[List[Tuple[str, float]], float]:
|
||||
"""
|
||||
일별 강수량 요약 조회
|
||||
|
||||
초단기예보를 우선으로 하고, 없는 시간대는 단기예보로 보완합니다.
|
||||
|
||||
Args:
|
||||
target_date: 대상 날짜 (YYYYMMDD). None이면 오늘
|
||||
|
||||
Returns:
|
||||
(시간별 강수량 리스트, 총 강수량) 튜플
|
||||
시간별: [('10:00', 0.5), ('11:00', 0.0), ...]
|
||||
"""
|
||||
if target_date is None:
|
||||
target_date = datetime.now().strftime('%Y%m%d')
|
||||
|
||||
# 초단기예보 데이터 (우선)
|
||||
ultra_data = self.get_hourly_ultra_data(target_date)
|
||||
|
||||
# 단기예보 데이터 (보완용)
|
||||
vilage_data = self.get_hourly_vilage_data(target_date)
|
||||
|
||||
time_precip_list = []
|
||||
total_rainfall = 0.0
|
||||
|
||||
for hour in range(self.start_hour, self.end_hour + 1):
|
||||
# 초단기예보 우선, 없으면 단기예보 사용
|
||||
rainfall = ultra_data.get(hour, vilage_data.get(hour, 0.0))
|
||||
time_str = f"{hour:02d}:00"
|
||||
time_precip_list.append((time_str, rainfall))
|
||||
total_rainfall += rainfall
|
||||
|
||||
return time_precip_list, round(total_rainfall, 1)
|
||||
|
||||
def generate_html_table(
|
||||
self,
|
||||
target_date: Optional[str] = None,
|
||||
title: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
강수량 요약 HTML 테이블 생성
|
||||
|
||||
Args:
|
||||
target_date: 대상 날짜 (YYYYMMDD). None이면 오늘
|
||||
title: 테이블 제목 (None이면 기본 제목)
|
||||
|
||||
Returns:
|
||||
HTML 문자열
|
||||
"""
|
||||
if target_date is None:
|
||||
target_date = datetime.now().strftime('%Y%m%d')
|
||||
|
||||
time_precip_list, total_rainfall = self.get_daily_summary(target_date)
|
||||
|
||||
if title is None:
|
||||
title = f"[{self.start_hour}:00 ~ {self.end_hour}:00 예상 강수량]"
|
||||
|
||||
lines = [
|
||||
'<div class="weatherinfo" style="max-width: 100%; overflow-x: auto; padding: 10px;">',
|
||||
f'<h3 style="font-size: 1.8em; text-align: center; margin: 20px 0;">{title}</h3>',
|
||||
'<table style="border-collapse: collapse; width: 100%; max-width: 400px; margin: 0 auto;">',
|
||||
'<thead><tr>',
|
||||
'<th style="border: 1px solid #333; padding: 2px; background-color: #f0f0f0;">시간</th>',
|
||||
'<th style="border: 1px solid #333; padding: 2px; background-color: #f0f0f0;">강수량</th>',
|
||||
'</tr></thead><tbody>'
|
||||
]
|
||||
|
||||
for time_str, rainfall in time_precip_list:
|
||||
lines.append(
|
||||
f'<tr><td style="border: 1px solid #333; text-align: center;">{time_str}</td>'
|
||||
f'<td style="border: 1px solid #333; text-align: center;">{rainfall}mm</td></tr>'
|
||||
)
|
||||
|
||||
lines.append(
|
||||
f'<tr><td colspan="2" style="border: 1px solid #333; text-align: center; font-weight: bold;">'
|
||||
f'총 예상 강수량: {total_rainfall:.1f}mm</td></tr>'
|
||||
)
|
||||
lines.append('</tbody></table>')
|
||||
lines.append('<p style="text-align:right; font-size: 0.8em;">초단기 + 단기 예보 기준</p>')
|
||||
lines.append('</div>')
|
||||
|
||||
return ''.join(lines)
|
||||
|
||||
def generate_text_summary(
|
||||
self,
|
||||
target_date: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
강수량 요약 텍스트 생성
|
||||
|
||||
Args:
|
||||
target_date: 대상 날짜 (YYYYMMDD). None이면 오늘
|
||||
|
||||
Returns:
|
||||
텍스트 요약 문자열
|
||||
"""
|
||||
if target_date is None:
|
||||
target_date = datetime.now().strftime('%Y%m%d')
|
||||
|
||||
time_precip_list, total_rainfall = self.get_daily_summary(target_date)
|
||||
|
||||
lines = [
|
||||
f"📅 {target_date} 예상 강수량",
|
||||
f"⏰ {self.start_hour}:00 ~ {self.end_hour}:00",
|
||||
"-" * 20
|
||||
]
|
||||
|
||||
for time_str, rainfall in time_precip_list:
|
||||
lines.append(f"{time_str} → {rainfall}mm")
|
||||
|
||||
lines.append("-" * 20)
|
||||
lines.append(f"☔ 총 예상 강수량: {total_rainfall:.1f}mm")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def save_to_sqlite(
|
||||
self,
|
||||
target_date: Optional[str] = None,
|
||||
db_path: str = '/data/weather.sqlite'
|
||||
):
|
||||
"""
|
||||
강수량 데이터를 SQLite에 저장
|
||||
|
||||
Args:
|
||||
target_date: 대상 날짜 (YYYYMMDD). None이면 오늘
|
||||
db_path: SQLite 데이터베이스 파일 경로
|
||||
"""
|
||||
if target_date is None:
|
||||
target_date = datetime.now().strftime('%Y%m%d')
|
||||
|
||||
time_precip_list, total_rainfall = self.get_daily_summary(target_date)
|
||||
|
||||
# 디렉토리 생성
|
||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 테이블 생성
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS precipitation (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
date TEXT NOT NULL,
|
||||
time TEXT NOT NULL,
|
||||
rainfall REAL NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS precipitation_summary (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
date TEXT NOT NULL UNIQUE,
|
||||
total_rainfall REAL NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
# 기존 데이터 삭제
|
||||
cursor.execute('DELETE FROM precipitation WHERE date = ?', (target_date,))
|
||||
cursor.execute('DELETE FROM precipitation_summary WHERE date = ?', (target_date,))
|
||||
|
||||
# 시간별 데이터 삽입
|
||||
cursor.executemany(
|
||||
'INSERT INTO precipitation (date, time, rainfall) VALUES (?, ?, ?)',
|
||||
[(target_date, t, r) for t, r in time_precip_list]
|
||||
)
|
||||
|
||||
# 총 강수량 삽입
|
||||
cursor.execute(
|
||||
'INSERT INTO precipitation_summary (date, total_rainfall) VALUES (?, ?)',
|
||||
(target_date, total_rainfall)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"SQLite 저장 완료: {target_date}, 총 {total_rainfall}mm")
|
||||
|
||||
def get_precipitation_info(
|
||||
self,
|
||||
target_date: Optional[str] = None,
|
||||
output_format: str = 'dict'
|
||||
) -> Any:
|
||||
"""
|
||||
강수량 정보 조회 (다양한 형식 지원)
|
||||
|
||||
Args:
|
||||
target_date: 대상 날짜 (YYYYMMDD). None이면 오늘
|
||||
output_format: 출력 형식 ('dict', 'html', 'text', 'json')
|
||||
|
||||
Returns:
|
||||
요청된 형식의 강수량 정보
|
||||
"""
|
||||
if target_date is None:
|
||||
target_date = datetime.now().strftime('%Y%m%d')
|
||||
|
||||
if output_format == 'html':
|
||||
return self.generate_html_table(target_date)
|
||||
elif output_format == 'text':
|
||||
return self.generate_text_summary(target_date)
|
||||
elif output_format == 'json':
|
||||
time_precip_list, total = self.get_daily_summary(target_date)
|
||||
return {
|
||||
'date': target_date,
|
||||
'hourly': [{'time': t, 'rainfall': r} for t, r in time_precip_list],
|
||||
'total': total
|
||||
}
|
||||
else:
|
||||
time_precip_list, total = self.get_daily_summary(target_date)
|
||||
return {
|
||||
'date': target_date,
|
||||
'hourly': time_precip_list,
|
||||
'total': total
|
||||
}
|
||||
Reference in New Issue
Block a user