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:
133
lib/common.py
133
lib/common.py
@ -1,31 +1,136 @@
|
||||
# common.py
|
||||
import os, yaml
|
||||
import os
|
||||
import yaml
|
||||
import logging
|
||||
import time
|
||||
import glob
|
||||
from functools import wraps
|
||||
from typing import Any, Callable
|
||||
|
||||
def load_config():
|
||||
# 로거 설정
|
||||
def setup_logging(name: str, level: str = 'INFO') -> logging.Logger:
|
||||
"""
|
||||
conf/config.yaml 파일을 UTF-8로 읽어 파이썬 dict로 반환
|
||||
로거 설정 (일관된 포맷 적용)
|
||||
|
||||
Args:
|
||||
name: 로거 이름
|
||||
level: 로그 레벨 (INFO, DEBUG, WARNING, ERROR)
|
||||
|
||||
Returns:
|
||||
Logger 인스턴스
|
||||
"""
|
||||
path = os.path.join(os.path.dirname(__file__), '..', 'conf', 'config.yaml')
|
||||
with open(path, encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def get_logger(name):
|
||||
logger = logging.getLogger(name)
|
||||
|
||||
if not logger.handlers:
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s')
|
||||
formatter = logging.Formatter(
|
||||
'[%(asctime)s] %(name)s - %(levelname)s: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
|
||||
return logger
|
||||
|
||||
def wait_download_complete(download_dir, ext, timeout=60):
|
||||
for _ in range(timeout):
|
||||
files = glob.glob(os.path.join(download_dir, f"*.{ext.strip('.')}"))
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""기존 호환성 유지"""
|
||||
return setup_logging(name)
|
||||
|
||||
def load_config(config_path: str = None) -> dict:
|
||||
"""
|
||||
conf/config.yaml 파일을 UTF-8로 읽어 파이썬 dict로 반환
|
||||
|
||||
Args:
|
||||
config_path: 설정 파일 경로 (없으면 기본값 사용)
|
||||
|
||||
Returns:
|
||||
설정 딕셔너리
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: 설정 파일을 찾을 수 없을 때
|
||||
yaml.YAMLError: YAML 파싱 실패 시
|
||||
"""
|
||||
if config_path is None:
|
||||
config_path = os.path.join(os.path.dirname(__file__), '..', 'conf', 'config.yaml')
|
||||
|
||||
try:
|
||||
with open(config_path, encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
if config is None:
|
||||
raise ValueError(f"설정 파일이 비어있음: {config_path}")
|
||||
return config
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"설정 파일을 찾을 수 없음: {config_path}")
|
||||
except yaml.YAMLError as e:
|
||||
raise yaml.YAMLError(f"YAML 파싱 오류: {e}")
|
||||
|
||||
def retry_on_exception(max_retries: int = 3, delay: float = 1.0, backoff: float = 2.0):
|
||||
"""
|
||||
지정된 횟수만큼 재시도하는 데코레이터
|
||||
|
||||
Args:
|
||||
max_retries: 최대 재시도 횟수
|
||||
delay: 재시도 간격 (초)
|
||||
backoff: 재시도마다 지연 시간 배수
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs) -> Any:
|
||||
logger = logging.getLogger(func.__module__)
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
if attempt < max_retries - 1:
|
||||
wait_time = delay * (backoff ** attempt)
|
||||
logger.warning(
|
||||
f"{func.__name__} 재시도 {attempt + 1}/{max_retries} "
|
||||
f"({wait_time:.1f}초 대기): {e}"
|
||||
)
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
logger.error(
|
||||
f"{func.__name__} 모든 재시도 실패: {e}"
|
||||
)
|
||||
|
||||
raise last_exception
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def wait_download_complete(download_dir: str, ext: str, timeout: int = 60) -> str:
|
||||
"""
|
||||
파일 다운로드 완료 대기
|
||||
|
||||
Args:
|
||||
download_dir: 다운로드 디렉토리
|
||||
ext: 파일 확장자 (예: 'xlsx', 'csv')
|
||||
timeout: 대기 시간 (초)
|
||||
|
||||
Returns:
|
||||
다운로드된 파일 경로
|
||||
|
||||
Raises:
|
||||
TimeoutError: 지정 시간 내 파일이 나타나지 않을 때
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
ext = ext.lstrip('.')
|
||||
|
||||
for i in range(timeout):
|
||||
files = glob.glob(os.path.join(download_dir, f"*.{ext}"))
|
||||
if files:
|
||||
logger.info(f"다운로드 완료: {files[0]}")
|
||||
return files[0]
|
||||
|
||||
if i > 0 and i % 10 == 0:
|
||||
logger.debug(f"다운로드 대기 중... ({i}초 경과)")
|
||||
|
||||
time.sleep(1)
|
||||
raise TimeoutError("다운로드 대기 시간 초과")
|
||||
|
||||
raise TimeoutError(
|
||||
f"파일 다운로드 대기 시간 초과 ({timeout}초): {download_dir}/*.{ext}"
|
||||
)
|
||||
Reference in New Issue
Block a user