feat: Flask 애플리케이션 모듈화 및 웹 대시보드 구현
- Flask Blueprint 아키텍처로 전환 (dashboard, upload, backup, status) - app.py 681줄 95줄로 축소 (86% 감소) - HTML 템플릿 모듈화 (base.html + 기능별 templates) - CSS/JS 파일 분리 (common + 기능별 파일) - 대시보드 기능 추가 (통계, 주간 예보, 방문객 추이) - 파일 업로드 웹 인터페이스 구현 - 백업/복구 관리 UI 구현 - Docker 배포 환경 개선 - .gitignore 업데이트 (uploads, backups, cache 등)
This commit is contained in:
@ -1,13 +1,17 @@
|
||||
import os, sys
|
||||
import os, sys
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import select, func, and_
|
||||
from sqlalchemy.dialects.mysql import insert as mysql_insert
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
import traceback
|
||||
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
from conf import db, db_schema
|
||||
from requests_utils import make_requests_session
|
||||
|
||||
CACHE_FILE = os.path.join(os.path.dirname(__file__), '..', 'cache', 'air_num_rows.json')
|
||||
|
||||
class AirQualityCollector:
|
||||
def __init__(self, config, engine, table):
|
||||
@ -20,6 +24,24 @@ class AirQualityCollector:
|
||||
self.engine = engine
|
||||
self.table = table
|
||||
self.yesterday = (datetime.now() - timedelta(days=1)).date()
|
||||
self.session = make_requests_session()
|
||||
|
||||
# load cache
|
||||
self._num_rows_cache = {}
|
||||
try:
|
||||
if os.path.exists(CACHE_FILE):
|
||||
with open(CACHE_FILE, 'r', encoding='utf-8') as f:
|
||||
self._num_rows_cache = json.load(f)
|
||||
except Exception:
|
||||
self._num_rows_cache = {}
|
||||
|
||||
def _save_num_rows_cache(self):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(CACHE_FILE), exist_ok=True)
|
||||
with open(CACHE_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(self._num_rows_cache, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
print(f"[WARN] num_rows 캐시 저장 실패: {e}")
|
||||
|
||||
def get_latest_date(self, conn, station):
|
||||
try:
|
||||
@ -30,6 +52,7 @@ class AirQualityCollector:
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 가장 최근 날짜 조회 실패: {e}")
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def save_data_to_db(self, items, conn, station):
|
||||
@ -37,7 +60,7 @@ class AirQualityCollector:
|
||||
try:
|
||||
item_date = datetime.strptime(item['msurDt'], '%Y-%m-%d').date()
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 날짜 파싱 오류: {item.get('msurDt')} → {e}")
|
||||
print(f"[ERROR] 날짜 파싱 오류: {item.get('msurDt')} {e}")
|
||||
continue
|
||||
|
||||
data = {
|
||||
@ -53,7 +76,7 @@ class AirQualityCollector:
|
||||
|
||||
try:
|
||||
if self.debug:
|
||||
print(f"[DEBUG] {item_date} [{station}] → DB 저장 시도: {data}")
|
||||
print(f"[DEBUG] {item_date} [{station}] DB 저장 시도: {data}")
|
||||
continue
|
||||
|
||||
if self.force_update:
|
||||
@ -75,6 +98,7 @@ class AirQualityCollector:
|
||||
print(f"[ERROR] DB 중복 오류: {e}")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] DB 저장 실패: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
def fetch_air_quality_data_range(self, start_date_str, end_date_str, station_name, num_of_rows=100, page_no=1):
|
||||
url = "http://apis.data.go.kr/B552584/ArpltnStatsSvc/getMsrstnAcctoRDyrg"
|
||||
@ -88,23 +112,39 @@ class AirQualityCollector:
|
||||
'msrstnName': station_name,
|
||||
}
|
||||
|
||||
resp = None
|
||||
try:
|
||||
resp = requests.get(url, params=params, timeout=10)
|
||||
resp = self.session.get(url, params=params, timeout=20)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
return data.get('response', {}).get('body', {}).get('items', [])
|
||||
except requests.RequestException as e:
|
||||
print(f"[ERROR] 요청 실패: {e}")
|
||||
return []
|
||||
except ValueError as e:
|
||||
print(f"[ERROR] JSON 파싱 실패: {e}")
|
||||
except Exception as e:
|
||||
body_preview = None
|
||||
try:
|
||||
if resp is not None:
|
||||
body_preview = resp.text[:1000]
|
||||
except Exception:
|
||||
body_preview = None
|
||||
print(f"[ERROR] 요청 실패: {e} status={getattr(resp, 'status_code', 'n/a')} body_preview={body_preview}")
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
def test_num_of_rows(self, station_name, date_str):
|
||||
# 캐시 확인
|
||||
try:
|
||||
cache_key = f"{station_name}:{date_str}"
|
||||
if cache_key in self._num_rows_cache:
|
||||
val = int(self._num_rows_cache[cache_key])
|
||||
print(f"[INFO] 캐시된 numOfRows 사용: {val} ({cache_key})")
|
||||
return val
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
max_rows = 1000
|
||||
min_rows = 100
|
||||
|
||||
while max_rows >= min_rows:
|
||||
resp = None
|
||||
try:
|
||||
url = "http://apis.data.go.kr/B552584/ArpltnStatsSvc/getMsrstnAcctoRDyrg"
|
||||
params = {
|
||||
@ -116,13 +156,25 @@ class AirQualityCollector:
|
||||
'inqEndDt': date_str,
|
||||
'msrstnName': station_name,
|
||||
}
|
||||
resp = requests.get(url, params=params, timeout=10)
|
||||
resp = self.session.get(url, params=params, timeout=20)
|
||||
resp.raise_for_status()
|
||||
resp.json()
|
||||
resp.json() # 성공하면 해당 max_rows 사용 가능
|
||||
print(f"[INFO] numOfRows 최대값 탐색 성공: {max_rows}")
|
||||
# 캐시에 저장
|
||||
try:
|
||||
self._num_rows_cache[f"{station_name}:{date_str}"] = max_rows
|
||||
self._save_num_rows_cache()
|
||||
except Exception:
|
||||
pass
|
||||
return max_rows
|
||||
except Exception as e:
|
||||
print(f"[WARN] numOfRows={max_rows} 실패: {e}, 100 감소 후 재시도")
|
||||
body_preview = None
|
||||
try:
|
||||
if resp is not None:
|
||||
body_preview = resp.text[:500]
|
||||
except Exception:
|
||||
body_preview = None
|
||||
print(f"[WARN] numOfRows={max_rows} 실패: {e}, body_preview={body_preview} 100 감소 후 재시도")
|
||||
max_rows -= 100
|
||||
|
||||
print("[WARN] 최소 numOfRows 값(100)도 실패했습니다. 기본값 100 사용")
|
||||
@ -160,7 +212,7 @@ class AirQualityCollector:
|
||||
current_start = current_end + timedelta(days=1)
|
||||
|
||||
print("\n[INFO] 모든 측정소 데이터 처리 완료")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
config = db.load_config()
|
||||
collector = AirQualityCollector(config, db.engine, db_schema.air)
|
||||
|
||||
Reference in New Issue
Block a user