first jiwoos commit
This commit is contained in:
709
webapp/app.py
Normal file
709
webapp/app.py
Normal file
@@ -0,0 +1,709 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from flask import Flask, abort, jsonify, redirect, render_template, request, url_for
|
||||
|
||||
APP_DIR = Path(__file__).resolve().parent
|
||||
ROOT_DIR = APP_DIR.parent
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
from register_game_playwright import normalize_team_name
|
||||
|
||||
JOBS_DIR = ROOT_DIR / "jobs"
|
||||
LOGS_DIR = ROOT_DIR / "logs"
|
||||
OUTPUT_DIR = ROOT_DIR / "output"
|
||||
TEAM_OPTIONS = [
|
||||
{"value": "HT", "label": "KIA"},
|
||||
{"value": "KT", "label": "KT"},
|
||||
{"value": "LG", "label": "LG"},
|
||||
{"value": "NC", "label": "NC"},
|
||||
{"value": "OB", "label": "두산"},
|
||||
{"value": "LT", "label": "롯데"},
|
||||
{"value": "SK", "label": "SSG"},
|
||||
{"value": "SS", "label": "삼성"},
|
||||
{"value": "WO", "label": "키움"},
|
||||
{"value": "HH", "label": "한화"},
|
||||
]
|
||||
GAME_TYPE_OPTIONS = [
|
||||
{"value": "regular", "label": "정규경기"},
|
||||
{"value": "wildcard", "label": "와일드카드"},
|
||||
{"value": "semi_playoff", "label": "준PO"},
|
||||
{"value": "playoff", "label": "PO"},
|
||||
{"value": "korean_series", "label": "한국시리즈"},
|
||||
]
|
||||
GAME_TYPE_CODE_MAP = {
|
||||
"regular": None,
|
||||
"wildcard": "4444",
|
||||
"semi_playoff": "3333",
|
||||
"playoff": "5555",
|
||||
"korean_series": "7777",
|
||||
}
|
||||
|
||||
JOBS_DIR.mkdir(exist_ok=True)
|
||||
LOGS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
return datetime.now().isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def safe_name(value: str) -> str:
|
||||
return "".join(char for char in value if char.isalnum() or char in {"-", "_"})
|
||||
|
||||
|
||||
def job_path(job_id: str) -> Path:
|
||||
return JOBS_DIR / f"{job_id}.json"
|
||||
|
||||
|
||||
def log_path(job_id: str) -> Path:
|
||||
return LOGS_DIR / f"{job_id}.log"
|
||||
|
||||
|
||||
def load_job(job_id: str) -> dict[str, Any]:
|
||||
path = job_path(job_id)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(job_id)
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def save_job(data: dict[str, Any]) -> None:
|
||||
job_id = data["job_id"]
|
||||
job_path(job_id).write_text(
|
||||
json.dumps(data, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def list_jobs(limit: int = 30) -> list[dict[str, Any]]:
|
||||
jobs: list[dict[str, Any]] = []
|
||||
for path in sorted(JOBS_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
|
||||
try:
|
||||
jobs.append(json.loads(path.read_text(encoding="utf-8")))
|
||||
except Exception:
|
||||
continue
|
||||
if len(jobs) >= limit:
|
||||
break
|
||||
return jobs
|
||||
|
||||
|
||||
def tail_log_preview(job_id: str) -> str:
|
||||
path = log_path(job_id)
|
||||
if not path.exists():
|
||||
return "로그 없음"
|
||||
try:
|
||||
lines = [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
|
||||
except Exception:
|
||||
return "로그 읽기 실패"
|
||||
if not lines:
|
||||
return "로그 없음"
|
||||
for line in reversed(lines):
|
||||
if line.startswith("[") and ("START " in line or "END " in line):
|
||||
continue
|
||||
return line[:96]
|
||||
return lines[-1][:96]
|
||||
|
||||
|
||||
def enrich_job(job: dict[str, Any]) -> dict[str, Any]:
|
||||
enriched = dict(job)
|
||||
enriched["log_preview"] = tail_log_preview(job["job_id"])
|
||||
return enriched
|
||||
|
||||
|
||||
def get_active_job() -> dict[str, Any] | None:
|
||||
for job in list_jobs(limit=200):
|
||||
if job.get("status") in {"queued", "running"}:
|
||||
return enrich_job(job)
|
||||
return None
|
||||
|
||||
|
||||
def has_active_job() -> bool:
|
||||
return get_active_job() is not None
|
||||
|
||||
|
||||
def list_recent_reports(limit: int = 12) -> list[dict[str, Any]]:
|
||||
reports: list[dict[str, Any]] = []
|
||||
for path in sorted(OUTPUT_DIR.glob("*_report.json"), key=lambda p: p.stat().st_mtime, reverse=True):
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
continue
|
||||
game_info = payload.get("game_info") or {}
|
||||
reports.append(
|
||||
{
|
||||
"game_id": payload.get("game_id") or path.name.replace("_report.json", ""),
|
||||
"date": game_info.get("date") or "-",
|
||||
"home_team": game_info.get("home_team") or "-",
|
||||
"away_team": game_info.get("away_team") or "-",
|
||||
"start_time": game_info.get("start_time") or "-",
|
||||
"path": str(path),
|
||||
}
|
||||
)
|
||||
if len(reports) >= limit:
|
||||
break
|
||||
return reports
|
||||
|
||||
|
||||
def resolve_report_path(game_id: str, report_path: str | None) -> Path:
|
||||
if report_path:
|
||||
return Path(report_path)
|
||||
return OUTPUT_DIR / f"{game_id}_report.json"
|
||||
|
||||
|
||||
def requires_report(job_type: str) -> bool:
|
||||
return job_type == "register_basic"
|
||||
|
||||
|
||||
def validate_job_request(job_type: str, game_id: str, manager_mode: str, manager_game_no: str, report_path: str, inning_no: str) -> str | None:
|
||||
if not game_id:
|
||||
return "경기 식별 정보가 올바르지 않습니다. game_id 또는 날짜/팀 조합을 확인하세요."
|
||||
if manager_mode == "manual" and job_type in {"lineup", "record", "finish", "post_update"} and not manager_game_no:
|
||||
return "관리자 게임번호 직접 입력을 선택했다면 게임번호를 입력하세요."
|
||||
def get_inn_val(s: str) -> float | None:
|
||||
if not s: return None
|
||||
m = re.match(r"^(\d+)([TB]?)$", s.upper().strip())
|
||||
if not m: return None
|
||||
num = int(m.group(1))
|
||||
half = 0.5 if m.group(2) == "B" else 0.0
|
||||
return num + half
|
||||
|
||||
if job_type == "record":
|
||||
if not inning_no:
|
||||
return "경기기록 입력은 이닝을 선택해야 합니다."
|
||||
if inning_no != "all":
|
||||
if "-" in inning_no:
|
||||
start_str, end_str = inning_no.split("-", 1)
|
||||
s_val = get_inn_val(start_str)
|
||||
e_val = get_inn_val(end_str)
|
||||
if s_val is None or e_val is None:
|
||||
return f"이닝 범위 형식이 올바르지 않습니다 (예: 3B-9B). 입력값: {inning_no}"
|
||||
if not (1.0 <= s_val <= e_val <= 15.5):
|
||||
return "이닝 범위가 올바르지 않습니다 (1회 초 ~ 15회 말)."
|
||||
else:
|
||||
val = get_inn_val(inning_no)
|
||||
if val is None:
|
||||
return f"이닝 형식이 올바르지 않습니다 (예: 5T). 입력값: {inning_no}"
|
||||
if not (1.0 <= val <= 15.5):
|
||||
return "이닝은 1회부터 15회까지만 선택할 수 있습니다."
|
||||
if requires_report(job_type):
|
||||
resolved = resolve_report_path(game_id, report_path)
|
||||
# 자동화 작업들은 첫 단계에서 리포트를 생성하므로 사전 파일 체크를 하지 않음
|
||||
# "수동 모드"나 특수한 경우가 아니면 거의 모든 자동화 버튼을 허용
|
||||
automated_jobs = {"lineup", "record", "register_basic", "post_update", "finish", "compare", "video_review"}
|
||||
if job_type not in automated_jobs and not resolved.exists():
|
||||
return f"리포트 파일이 없습니다: {resolved}"
|
||||
return None
|
||||
|
||||
|
||||
def normalize_team_key(value: str) -> str:
|
||||
raw = (value or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
return raw.casefold()
|
||||
|
||||
|
||||
def team_label_from_code(team_code: str) -> str:
|
||||
for option in TEAM_OPTIONS:
|
||||
if option["value"] == team_code:
|
||||
return option["label"]
|
||||
return team_code
|
||||
|
||||
|
||||
def team_alias_keys(value: str) -> set[str]:
|
||||
raw = (value or "").strip()
|
||||
if not raw:
|
||||
return set()
|
||||
normalized = normalize_team_name(raw)
|
||||
return {raw.casefold(), normalized.casefold()}
|
||||
|
||||
|
||||
def compose_game_id_from_form(form) -> str:
|
||||
def clean(v: str) -> str:
|
||||
return "".join((v or "").split())
|
||||
|
||||
game_type = clean(form.get("game_type") or "regular")
|
||||
game_date = clean(form.get("game_date"))
|
||||
away_code = clean(form.get("away_team_code")).upper()
|
||||
home_code = clean(form.get("home_team_code")).upper()
|
||||
dh_raw = clean(form.get("doubleheader_no") or "0")
|
||||
|
||||
if not game_date or not away_code or not home_code:
|
||||
return ""
|
||||
pure_date = game_date.replace("-", "")
|
||||
year = pure_date[:4]
|
||||
month = pure_date[4:6]
|
||||
day = pure_date[6:8]
|
||||
|
||||
type_code = GAME_TYPE_CODE_MAP.get(game_type)
|
||||
if not type_code or game_type == "regular":
|
||||
type_code = year
|
||||
|
||||
dh = "0"
|
||||
if dh_raw.isdigit():
|
||||
dh = str(max(0, min(9, int(dh_raw))))
|
||||
|
||||
# 네이버 표준 ID: [연도/타입][월][일][원정][홈][더블헤더][연도]
|
||||
# 예: 2026 04 14 LT LG 0 2026
|
||||
return f"{type_code}{month}{day}{away_code}{home_code}{dh}{year}"
|
||||
|
||||
|
||||
def resolve_game_id_from_form(form) -> str:
|
||||
mode = (form.get("game_id_mode") or "direct").strip()
|
||||
raw_id = ""
|
||||
if mode == "direct":
|
||||
raw_id = (form.get("game_id") or "").strip()
|
||||
else:
|
||||
raw_id = compose_game_id_from_form(form)
|
||||
|
||||
# 영문/숫자만 남기고 한글 및 특수문자 제거
|
||||
return "".join(re.findall(r"[A-Za-z0-9]", raw_id))
|
||||
|
||||
|
||||
def build_generated_report_paths(game_id: str, job_type: str, inning_no: str | None = None) -> tuple[Path, Path]:
|
||||
if job_type == "lineup":
|
||||
stem = f"{game_id}_lineup_report"
|
||||
elif job_type == "record" and inning_no and inning_no != "all":
|
||||
# 범위를 파일명에 표시 (예: 5-9 -> inning_5-9)
|
||||
safe_inning = safe_name(inning_no)
|
||||
stem = f"{game_id}_inning_{safe_inning}_report"
|
||||
elif job_type == "record":
|
||||
stem = f"{game_id}_full_record_report"
|
||||
elif job_type == "finish":
|
||||
stem = f"{game_id}_finish_report"
|
||||
elif job_type == "post_update":
|
||||
stem = f"{game_id}_post_report"
|
||||
elif job_type == "compare":
|
||||
stem = f"{game_id}_compare_report"
|
||||
elif job_type == "video_review":
|
||||
stem = f"{game_id}_review_report"
|
||||
else:
|
||||
stem = f"{game_id}_report"
|
||||
return OUTPUT_DIR / f"{stem}.json", OUTPUT_DIR / f"{stem}.txt"
|
||||
|
||||
|
||||
def build_command(job_type: str, game_id: str, manager_game_no: str | None, report_path: str | None, inning_no: str | None = None) -> list[str]:
|
||||
python_exe = sys.executable
|
||||
cmd = [python_exe]
|
||||
|
||||
if job_type == "register_basic":
|
||||
cmd.extend([str(ROOT_DIR / "register_game_basic_playwright.py"), "--game-id", game_id, "--close"])
|
||||
elif job_type == "lineup":
|
||||
cmd.extend([str(ROOT_DIR / "lineup_only_playwright.py"), "--game-id", game_id, "--close"])
|
||||
if manager_game_no:
|
||||
cmd.extend(["--manager-game-no", manager_game_no])
|
||||
elif job_type == "record":
|
||||
cmd.extend([str(ROOT_DIR / "record_game_playwright.py"), "--game-id", game_id, "--close"])
|
||||
if manager_game_no:
|
||||
cmd.extend(["--manager-game-no", manager_game_no])
|
||||
elif job_type == "finish":
|
||||
cmd.extend([str(ROOT_DIR / "finish_game_playwright.py"), "--game-id", game_id, "--close"])
|
||||
if manager_game_no:
|
||||
cmd.extend(["--manager-game-no", manager_game_no])
|
||||
elif job_type == "post_update":
|
||||
cmd.extend([str(ROOT_DIR / "update_game_post_playwright.py"), "--game-id", game_id, "--close"])
|
||||
if manager_game_no:
|
||||
cmd.extend(["--manager-game-no", manager_game_no])
|
||||
elif job_type == "compare":
|
||||
cmd.extend([str(ROOT_DIR / "compare_history_with_report.py"), "--game-id", game_id])
|
||||
elif job_type == "video_review":
|
||||
cmd.extend([str(ROOT_DIR / "video_review_playwright.py"), "--game-id", game_id, "--review-only", "--close"])
|
||||
else:
|
||||
raise ValueError(f"지원하지 않는 작업 타입입니다: {job_type}")
|
||||
|
||||
if report_path:
|
||||
cmd.extend(["--report-path", report_path])
|
||||
return cmd
|
||||
|
||||
|
||||
def build_job_steps(job: dict[str, Any]) -> list[list[str]]:
|
||||
job_type = job["type"]
|
||||
game_id = job["game_id"]
|
||||
manager_game_no = job.get("manager_game_no") or None
|
||||
report_path = job.get("report_path") or None
|
||||
inning_no = job.get("inning_no") or None
|
||||
python_exe = sys.executable
|
||||
|
||||
if job_type == "video_review":
|
||||
return [
|
||||
[
|
||||
python_exe,
|
||||
str(ROOT_DIR / "game_report.py"),
|
||||
"--game-id",
|
||||
game_id,
|
||||
"--output-json",
|
||||
report_path,
|
||||
],
|
||||
build_command(job_type, game_id, manager_game_no, report_path, inning_no),
|
||||
]
|
||||
|
||||
if job_type == "register_basic":
|
||||
return [
|
||||
[
|
||||
python_exe,
|
||||
str(ROOT_DIR / "game_report.py"),
|
||||
"--game-id",
|
||||
game_id,
|
||||
"--output-json",
|
||||
report_path,
|
||||
],
|
||||
build_command(job_type, game_id, manager_game_no, report_path, inning_no),
|
||||
]
|
||||
|
||||
if job_type == "lineup":
|
||||
return [
|
||||
[
|
||||
python_exe,
|
||||
str(ROOT_DIR / "game_report.py"),
|
||||
"--game-id",
|
||||
game_id,
|
||||
"--lineup-only",
|
||||
"--output-json",
|
||||
report_path,
|
||||
],
|
||||
build_command(job_type, game_id, manager_game_no, report_path, inning_no),
|
||||
]
|
||||
|
||||
if job_type == "record":
|
||||
report_command = [
|
||||
python_exe,
|
||||
str(ROOT_DIR / "game_report.py"),
|
||||
"--game-id",
|
||||
game_id,
|
||||
"--output-json",
|
||||
report_path,
|
||||
]
|
||||
if inning_no and inning_no != "all":
|
||||
if "-" in inning_no:
|
||||
try:
|
||||
# 1T-9B 같은 형식 처리
|
||||
start_val, end_val = inning_no.split("-", 1)
|
||||
report_command.extend(["--start-inning", start_val, "--end-inning", end_val])
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
report_command.extend(["--inning", str(inning_no)])
|
||||
return [
|
||||
report_command,
|
||||
build_command(job_type, game_id, manager_game_no, report_path, inning_no),
|
||||
]
|
||||
|
||||
if job_type in {"finish", "post_update"}:
|
||||
return [
|
||||
[
|
||||
python_exe,
|
||||
str(ROOT_DIR / "game_report.py"),
|
||||
"--game-id",
|
||||
game_id,
|
||||
"--output-json",
|
||||
report_path,
|
||||
],
|
||||
build_command(job_type, game_id, manager_game_no, report_path, inning_no),
|
||||
]
|
||||
|
||||
if job_type == "compare":
|
||||
return [
|
||||
[
|
||||
python_exe,
|
||||
str(ROOT_DIR / "game_report.py"),
|
||||
"--game-id",
|
||||
game_id,
|
||||
"--output-json",
|
||||
report_path,
|
||||
"--output-txt",
|
||||
str(Path(report_path).with_suffix(".txt")),
|
||||
],
|
||||
build_command(job_type, game_id, manager_game_no, report_path, inning_no),
|
||||
]
|
||||
|
||||
return [build_command(job_type, game_id, manager_game_no, report_path, inning_no)]
|
||||
|
||||
|
||||
def run_job(job_id: str) -> None:
|
||||
job = load_job(job_id)
|
||||
job["status"] = "running"
|
||||
job["started_at"] = now_iso()
|
||||
save_job(job)
|
||||
|
||||
env = os.environ.copy()
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
env["JOB_ID"] = job_id
|
||||
|
||||
log_file = log_path(job_id)
|
||||
with log_file.open("a", encoding="utf-8") as log:
|
||||
steps = build_job_steps(job)
|
||||
log.write(f"[{now_iso()}] START steps={len(steps)}\n")
|
||||
log.flush()
|
||||
try:
|
||||
return_code = 0
|
||||
for step_index, command in enumerate(steps, start=1):
|
||||
log.write(f"[{now_iso()}] STEP {step_index}/{len(steps)} {' '.join(command)}\n")
|
||||
log.flush()
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
cwd=str(ROOT_DIR),
|
||||
stdout=log,
|
||||
stderr=log,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
return_code = process.wait()
|
||||
if return_code != 0:
|
||||
break
|
||||
job = load_job(job_id)
|
||||
job["finished_at"] = now_iso()
|
||||
job["return_code"] = return_code
|
||||
if return_code == 0:
|
||||
job["status"] = "success"
|
||||
job["error"] = None
|
||||
else:
|
||||
job["status"] = "failed"
|
||||
job["error"] = f"프로세스 종료 코드: {return_code}"
|
||||
save_job(job)
|
||||
log.write(f"[{now_iso()}] END return_code={return_code}\n")
|
||||
except Exception as exc:
|
||||
job = load_job(job_id)
|
||||
job["status"] = "failed"
|
||||
job["finished_at"] = now_iso()
|
||||
job["error"] = str(exc)
|
||||
save_job(job)
|
||||
log.write(f"[{now_iso()}] EXCEPTION {exc}\n")
|
||||
log.write(traceback.format_exc())
|
||||
|
||||
|
||||
def create_job(job_type: str, game_id: str, manager_game_no: str | None, report_path: str | None, inning_no: str | None = None) -> dict[str, Any]:
|
||||
normalized_game_id = safe_name(game_id) or "unknown"
|
||||
job_id = f"{normalized_game_id}-{job_type}-{uuid.uuid4().hex[:8]}"
|
||||
if report_path:
|
||||
report_path_value = report_path
|
||||
elif job_type in {"lineup", "record", "compare", "video_review"}:
|
||||
report_path_value = str(build_generated_report_paths(game_id, job_type, inning_no)[0])
|
||||
else:
|
||||
report_path_value = str(OUTPUT_DIR / f"{game_id}_report.json")
|
||||
try:
|
||||
from db_logging import start_job as db_start_job
|
||||
db_start_job(job_id=job_id, game_id=game_id, start_inning=inning_no or "")
|
||||
except Exception as e:
|
||||
print(f"DB logging init error: {e}")
|
||||
|
||||
job = {
|
||||
"job_id": job_id,
|
||||
"type": job_type,
|
||||
"game_id": game_id,
|
||||
"manager_game_no": manager_game_no or "",
|
||||
"report_path": report_path_value,
|
||||
"inning_no": inning_no or "",
|
||||
"status": "queued",
|
||||
"created_at": now_iso(),
|
||||
"started_at": None,
|
||||
"finished_at": None,
|
||||
"log_file": str(log_path(job_id)),
|
||||
"error": None,
|
||||
"return_code": None,
|
||||
}
|
||||
save_job(job)
|
||||
thread = threading.Thread(target=run_job, args=(job_id,), daemon=True)
|
||||
thread.start()
|
||||
return job
|
||||
|
||||
|
||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def index():
|
||||
jobs = [enrich_job(job) for job in list_jobs()]
|
||||
active_job = get_active_job()
|
||||
recent_reports = list_recent_reports()
|
||||
error = request.args.get("error", "").strip()
|
||||
message = request.args.get("message", "").strip()
|
||||
def split_inning(val: str):
|
||||
if not val: return "1", "T"
|
||||
m = re.match(r"^(\d+)([TB])$", val.upper())
|
||||
if m: return m.group(1), m.group(2)
|
||||
return val, "T"
|
||||
|
||||
start_num, start_half = split_inning(request.args.get("start_inning") or "1T")
|
||||
end_num, end_half = split_inning(request.args.get("end_inning") or "9B")
|
||||
|
||||
defaults = {
|
||||
"game_id_mode": (request.args.get("game_id_mode") or "parse").strip(),
|
||||
"game_id": (request.args.get("game_id") or "").strip(),
|
||||
"game_type": (request.args.get("game_type") or "regular").strip(),
|
||||
"game_date": (request.args.get("game_date") or "").strip(),
|
||||
"home_team_code": (request.args.get("home_team_code") or "").strip(),
|
||||
"away_team_code": (request.args.get("away_team_code") or "").strip(),
|
||||
"doubleheader_no": (request.args.get("doubleheader_no") or "0").strip(),
|
||||
"manager_mode": (request.args.get("manager_mode") or "auto").strip(),
|
||||
"manager_game_no": (request.args.get("manager_game_no") or "").strip(),
|
||||
"report_path": (request.args.get("report_path") or "").strip(),
|
||||
"inning_no": (request.args.get("inning_no") or "1T-9B").strip(),
|
||||
"start_inning_num": start_num,
|
||||
"start_inning_half": start_half,
|
||||
"end_inning_num": end_num,
|
||||
"end_inning_half": end_half,
|
||||
}
|
||||
return render_template(
|
||||
"index.html",
|
||||
jobs=jobs,
|
||||
defaults=defaults,
|
||||
error=error,
|
||||
message=message,
|
||||
active_job=active_job,
|
||||
recent_reports=recent_reports,
|
||||
team_options=TEAM_OPTIONS,
|
||||
game_type_options=GAME_TYPE_OPTIONS,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/jobs/<job_type>")
|
||||
def start_job(job_type: str):
|
||||
if job_type not in {"register_basic", "lineup", "record", "finish", "post_update", "compare", "video_review"}:
|
||||
abort(404)
|
||||
|
||||
game_id = resolve_game_id_from_form(request.form)
|
||||
manager_mode = (request.form.get("manager_mode") or "auto").strip()
|
||||
manager_game_no = (request.form.get("manager_game_no") or "").strip() if manager_mode == "manual" else ""
|
||||
report_path = (request.form.get("report_path") or "").strip()
|
||||
inning_no = (request.form.get("inning_no") or "").strip()
|
||||
redirect_params = {
|
||||
"game_id_mode": (request.form.get("game_id_mode") or "direct").strip(),
|
||||
"game_id": (request.form.get("game_id") or "").strip(),
|
||||
"game_type": (request.form.get("game_type") or "regular").strip(),
|
||||
"game_date": (request.form.get("game_date") or "").strip(),
|
||||
"home_team_code": (request.form.get("home_team_code") or "").strip(),
|
||||
"away_team_code": (request.form.get("away_team_code") or "").strip(),
|
||||
"doubleheader_no": (request.form.get("doubleheader_no") or "0").strip(),
|
||||
"manager_mode": manager_mode,
|
||||
"manager_game_no": manager_game_no,
|
||||
"report_path": report_path,
|
||||
"inning_no": inning_no,
|
||||
"start_inning": (request.form.get("start_inning") or "1").strip(),
|
||||
"end_inning": (request.form.get("end_inning") or "9").strip(),
|
||||
}
|
||||
|
||||
validation_error = validate_job_request(job_type, game_id, manager_mode, manager_game_no, report_path, inning_no)
|
||||
if validation_error:
|
||||
return redirect(url_for("index", error=validation_error, **redirect_params))
|
||||
|
||||
if has_active_job():
|
||||
return redirect(url_for("index", error="실행 중인 작업이 있습니다. 완료 후 다시 시도하세요.", **redirect_params))
|
||||
|
||||
create_job(
|
||||
job_type=job_type,
|
||||
game_id=game_id,
|
||||
manager_game_no=None if job_type == "register_basic" else (manager_game_no or None),
|
||||
report_path=report_path or None,
|
||||
inning_no=inning_no or None,
|
||||
)
|
||||
return redirect(url_for("index", **redirect_params))
|
||||
|
||||
|
||||
def clear_files(paths: list[Path]) -> int:
|
||||
deleted = 0
|
||||
for path in paths:
|
||||
if path.exists() and path.is_file():
|
||||
path.unlink()
|
||||
deleted += 1
|
||||
return deleted
|
||||
|
||||
|
||||
@app.post("/maintenance/clear-logs")
|
||||
def clear_logs():
|
||||
deleted = clear_files(list(LOGS_DIR.glob("*.log")))
|
||||
return redirect(url_for("index", message=f"로그 {deleted}개를 삭제했습니다."))
|
||||
|
||||
|
||||
@app.post("/maintenance/clear-jobs")
|
||||
def clear_jobs():
|
||||
deleted = clear_files(list(JOBS_DIR.glob("*.json")))
|
||||
return redirect(url_for("index", message=f"작업 상태 {deleted}개를 삭제했습니다."))
|
||||
|
||||
|
||||
@app.post("/maintenance/clear-reports")
|
||||
def clear_reports():
|
||||
targets = list(OUTPUT_DIR.glob("*_report.json")) + list(OUTPUT_DIR.glob("*_report.txt"))
|
||||
deleted = clear_files(targets)
|
||||
return redirect(url_for("index", message=f"리포트 {deleted}개를 삭제했습니다."))
|
||||
|
||||
|
||||
@app.get("/db-logs/<job_id>")
|
||||
def view_db_logs(job_id: str):
|
||||
try:
|
||||
from db_logging import get_combined_logs
|
||||
logs = get_combined_logs(job_id)
|
||||
except Exception as e:
|
||||
return f"DB 로드 실패: {e}", 500
|
||||
|
||||
return render_template("logs.html", job_id=job_id, logs=logs)
|
||||
|
||||
|
||||
|
||||
@app.post("/maintenance/clear-runtime-profiles")
|
||||
def clear_runtime_profiles():
|
||||
runtime_dirs = [path for path in ROOT_DIR.glob("playwright-user-data-runtime-*") if path.is_dir()]
|
||||
deleted = 0
|
||||
for path in runtime_dirs:
|
||||
try:
|
||||
shutil.rmtree(path, ignore_errors=False)
|
||||
deleted += 1
|
||||
except Exception:
|
||||
continue
|
||||
return redirect(url_for("index", message=f"런타임 프로필 {deleted}개를 삭제했습니다."))
|
||||
|
||||
|
||||
@app.get("/api/dashboard")
|
||||
def dashboard_api():
|
||||
jobs = [enrich_job(job) for job in list_jobs()]
|
||||
active_job = get_active_job()
|
||||
recent_reports = list_recent_reports()
|
||||
return jsonify({
|
||||
"jobs": jobs,
|
||||
"active_job": active_job,
|
||||
"recent_reports": recent_reports
|
||||
})
|
||||
|
||||
|
||||
@app.get("/jobs")
|
||||
def jobs_api():
|
||||
return jsonify(list_jobs())
|
||||
|
||||
|
||||
@app.get("/jobs/<job_id>")
|
||||
def job_detail(job_id: str):
|
||||
try:
|
||||
job = load_job(job_id)
|
||||
except FileNotFoundError:
|
||||
abort(404)
|
||||
return render_template("job.html", job=job)
|
||||
|
||||
|
||||
@app.get("/jobs/<job_id>/status")
|
||||
def job_status(job_id: str):
|
||||
try:
|
||||
return jsonify(load_job(job_id))
|
||||
except FileNotFoundError:
|
||||
abort(404)
|
||||
|
||||
|
||||
@app.get("/jobs/<job_id>/log")
|
||||
def job_log(job_id: str):
|
||||
path = log_path(job_id)
|
||||
if not path.exists():
|
||||
abort(404)
|
||||
return path.read_text(encoding="utf-8"), 200, {"Content-Type": "text/plain; charset=utf-8"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000, debug=False, use_reloader=False)
|
||||
Reference in New Issue
Block a user