- Dockerfile: chmod 명령어에 RUN 추가 - .env.example: 모든 설정 항목 및 자세한 주석 추가 - config.yaml: 각 설정 항목에 대한 상세 주석 추가 - config.sample.yaml: 샘플 파일 주석 개선 - conf/db.py: 환경변수 우선 적용 기능 추가 - lib/common.py: load_config에 환경변수 오버라이드 지원 - 환경변수로 모든 설정값 제어 가능 (DB, API, POS 등)
192 lines
6.6 KiB
Python
192 lines
6.6 KiB
Python
# common.py
|
|
import os
|
|
import yaml
|
|
import logging
|
|
import time
|
|
import glob
|
|
from functools import wraps
|
|
from typing import Any, Callable
|
|
|
|
# 로거 설정
|
|
def setup_logging(name: str, level: str = 'INFO') -> logging.Logger:
|
|
"""
|
|
로거 설정 (일관된 포맷 적용)
|
|
|
|
Args:
|
|
name: 로거 이름
|
|
level: 로그 레벨 (INFO, DEBUG, WARNING, ERROR)
|
|
|
|
Returns:
|
|
Logger 인스턴스
|
|
"""
|
|
logger = logging.getLogger(name)
|
|
|
|
if not logger.handlers:
|
|
handler = logging.StreamHandler()
|
|
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(getattr(logging, level.upper(), logging.INFO))
|
|
return logger
|
|
|
|
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}")
|
|
|
|
# 환경변수로 설정 덮어쓰기
|
|
_apply_env_overrides(config)
|
|
|
|
return config
|
|
except FileNotFoundError:
|
|
raise FileNotFoundError(f"설정 파일을 찾을 수 없음: {config_path}")
|
|
except yaml.YAMLError as e:
|
|
raise yaml.YAMLError(f"YAML 파싱 오류: {e}")
|
|
|
|
def _apply_env_overrides(config: dict) -> None:
|
|
"""환경변수로 설정값 덮어쓰기"""
|
|
# 데이터베이스 설정
|
|
if os.getenv('DB_HOST'):
|
|
config.setdefault('database', {})
|
|
config['database']['host'] = os.getenv('DB_HOST', config['database'].get('host'))
|
|
config['database']['user'] = os.getenv('DB_USER', config['database'].get('user'))
|
|
config['database']['password'] = os.getenv('DB_PASSWORD', config['database'].get('password'))
|
|
config['database']['name'] = os.getenv('DB_NAME', config['database'].get('name'))
|
|
|
|
# 테이블 접두사
|
|
if os.getenv('TABLE_PREFIX'):
|
|
config['table_prefix'] = os.getenv('TABLE_PREFIX')
|
|
|
|
# API 설정
|
|
if os.getenv('DATA_API_SERVICE_KEY'):
|
|
config.setdefault('DATA_API', {})
|
|
config['DATA_API']['serviceKey'] = os.getenv('DATA_API_SERVICE_KEY')
|
|
if os.getenv('DATA_API_START_DATE'):
|
|
config.setdefault('DATA_API', {})
|
|
config['DATA_API']['startDt'] = os.getenv('DATA_API_START_DATE')
|
|
if os.getenv('DATA_API_END_DATE'):
|
|
config.setdefault('DATA_API', {})
|
|
config['DATA_API']['endDt'] = os.getenv('DATA_API_END_DATE')
|
|
|
|
# GA4 설정
|
|
if os.getenv('GA4_API_TOKEN'):
|
|
config.setdefault('ga4', {})
|
|
config['ga4']['token'] = os.getenv('GA4_API_TOKEN')
|
|
if os.getenv('GA4_PROPERTY_ID'):
|
|
config.setdefault('ga4', {})
|
|
config['ga4']['property_id'] = int(os.getenv('GA4_PROPERTY_ID'))
|
|
if os.getenv('GA4_SERVICE_ACCOUNT_FILE'):
|
|
config.setdefault('ga4', {})
|
|
config['ga4']['service_account_file'] = os.getenv('GA4_SERVICE_ACCOUNT_FILE')
|
|
|
|
# UPSolution 설정
|
|
if os.getenv('UPSOLUTION_ID'):
|
|
config.setdefault('upsolution', {})
|
|
config['upsolution']['id'] = os.getenv('UPSOLUTION_ID')
|
|
config['upsolution']['code'] = os.getenv('UPSOLUTION_CODE')
|
|
config['upsolution']['pw'] = os.getenv('UPSOLUTION_PW')
|
|
|
|
# 시스템 설정
|
|
if os.getenv('MAX_WORKERS'):
|
|
config['max_workers'] = int(os.getenv('MAX_WORKERS'))
|
|
if os.getenv('DEBUG'):
|
|
config['debug'] = os.getenv('DEBUG', 'false').lower() == 'true'
|
|
if os.getenv('FORCE_UPDATE'):
|
|
config['force_update'] = os.getenv('FORCE_UPDATE', 'false').lower() == 'true'
|
|
|
|
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(
|
|
f"파일 다운로드 대기 시간 초과 ({timeout}초): {download_dir}/*.{ext}"
|
|
) |