o2o-triple-pick/backend/app/config.py

148 lines
6.9 KiB
Python

"""애플리케이션 설정 — 환경변수(.env)에서 로드.
pydantic-settings 로 타입 검증. 누락 시 조용한 실패 없이 명확하게 동작.
시간 윈도우(투표 오픈/마감), 스케줄러 시각, 외부 연동 키를 모두 여기서 관리.
"""
from __future__ import annotations
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env", env_file_encoding="utf-8", extra="ignore"
)
# ── 데이터베이스 ─────────────────────────────────────────
# 외부 DB 연결: DB_HOST 가 설정되면 아래 DB_* 항목으로 접속 URL 을 조합한다
# (비밀번호 특수문자는 SQLAlchemy 가 안전하게 인코딩). DB_HOST 미설정 시
# database_url(또는 sqlite 등 직접 지정값)을 그대로 사용.
db_host: str = ""
db_port: int = 5432
db_name: str = "triplepick"
db_user: str = "triplepick"
db_password: str = "triplepick"
# 직접 지정용 폴백 (DB_HOST 미설정 시 사용). 로컬 sqlite 테스트 등.
database_url: str = (
"postgresql+asyncpg://triplepick:triplepick@db:5432/triplepick"
)
def sqlalchemy_url(self): # noqa: ANN201
"""엔진 생성용 URL. DB_HOST 가 있으면 DB_* 로 조합(특수문자 안전)."""
if self.db_host:
from sqlalchemy import URL
return URL.create(
"postgresql+asyncpg",
username=self.db_user,
password=self.db_password,
host=self.db_host,
port=self.db_port,
database=self.db_name,
)
return self.database_url
# ── 일반 ────────────────────────────────────────────────
timezone: str = "Asia/Seoul" # 모든 경기 시각의 기준 (KST)
cors_origins: str = "*" # 콤마구분. nginx 프록시 사용 시 동일 출처라 보통 불필요.
public_origin: str = "http://localhost:8080" # 공유 딥링크 베이스
# ── 투표 윈도우 (도메인 규칙) ─────────────────────────────
# 오픈 = 킥오프 - VOTE_OPEN_HOURS_BEFORE (D-2 = 48h)
vote_open_hours_before: int = 48
# 마감 = 킥오프 1시간 전 (경기 시작 1시간 전까지 투표). lock 오프셋(분).
vote_lock_minutes_before: int = 60
# 데모: True 면 오픈 게이트 무시(항상 투표 가능). 운영 배포 시 False.
demo_force_open: bool = True
# ── 스케줄러 (별도 워커 프로세스 = "스케줄링 서버") ───────
# 경기 일정 동적 수집: 매일 KST 09:00 외부 소스 크롤링 → 경기·투표시간 갱신.
schedule_sync_hour: int = 9
schedule_sync_minute: int = 0
# 소스: openfootball(키 불필요·기본) | football-data(토큰 필요) | fallback(하드코딩만)
schedule_source: str = "openfootball"
schedule_url: str = (
"https://raw.githubusercontent.com/openfootball/worldcup.json/master/2026/worldcup.json"
)
schedule_group: str = "Group A" # (구) openfootball 단일 그룹 라벨 — 폴백용
# 전체 조별리그(12개조 72경기) 일정을 불러올지. 결과/스코어는 전 경기 표시.
schedule_all_groups: bool = True
# AI 예측·투표 대상 조(이 조만 예측 생성·투표 가능). 그 외는 일정/결과만 표시.
featured_group: str = "A"
football_data_token: str = "" # football-data 사용 시 토큰
football_data_competition: str = "WC"
# 결과(스코어) 소스 — 일정 소스와 분리. 빈값이면 schedule_source 사용.
# openfootball 은 결과 미게시라, 자동 종료를 쓰려면 football-data 권장.
result_source: str = ""
@property
def effective_result_source(self) -> str:
return (self.result_source or self.schedule_source).lower()
# 매일 AI 예측 생성 시각 (KST). 기능정의서: 매일 0시 1회 생성.
ai_generate_hour: int = 0
ai_generate_minute: int = 5
# 투표 오픈(D-2) 선행 생성 시간(h). 1일 1회 생성이므로 다음 주기 전 오픈할
# 경기를 미리 채우려면 ~24h 이상 필요. 30h = 하루치 + 여유. (only_missing 이라 총 호출 수 동일)
ai_generate_lookahead_hours: int = 30
# ── 결과 자동 정산(관리자 입력 불필요) ───────────────────
# 킥오프 + 이 시간(시) 경과 후, 외부 소스에서 스코어를 자동 수집해 채점·집계·메일.
result_settle_hours: int = 3
# 정산/메일 점검 주기(초). status_tick 와 함께 도는 별도 폴링.
settle_tick_seconds: int = 300
# 결과 메일을 '맞춘 사람(승패 적중)'에게만 보낼지 여부. False면 구독자 전체.
result_email_correct_only: bool = True
# 결과 메일: 결과 확정 후 추가 지연(분). 자동정산은 이미 킥오프+Nh라 기본 0.
result_email_delay_minutes: int = 0
# 상태 전이 틱 주기(초): scheduled→open→locked 자동 갱신.
status_tick_seconds: int = 60
# ── 관리자 ──────────────────────────────────────────────
admin_api_token: str = "change-me-admin-token"
# ── 외부 연동: AI 3모델 (실연동) ─────────────────────────
openai_api_key: str = ""
openai_model: str = "gpt-4o"
anthropic_api_key: str = ""
anthropic_model: str = "claude-opus-4-8"
google_api_key: str = ""
google_model: str = "gemini-2.5-flash"
# ── 외부 연동: 이메일 ─────────────────────────────────────
# 1순위: Azure Communication Services(ACS) Email — endpoint + accesskey.
# AZURE_ACS_SENDER 는 검증된 MailFrom 주소(예: donotreply@triplepick.o2o.kr).
azure_acs_endpoint: str = ""
azure_acs_accesskey: str = ""
azure_acs_sender: str = ""
# 2순위(폴백): SMTP — ACS 미설정 시 사용.
smtp_host: str = ""
smtp_port: int = 587
smtp_user: str = ""
smtp_password: str = ""
smtp_from: str = "TriplePick <no-reply@triplepick.app>"
smtp_starttls: bool = True
@property
def acs_configured(self) -> bool:
return bool(
self.azure_acs_endpoint and self.azure_acs_accesskey and self.azure_acs_sender
)
@property
def cors_origin_list(self) -> list[str]:
if self.cors_origins.strip() == "*":
return ["*"]
return [o.strip() for o in self.cors_origins.split(",") if o.strip()]
@lru_cache
def get_settings() -> Settings:
return Settings()
settings = get_settings()