Files
baekjoon-bot/utils.py
2026-01-14 14:54:42 +09:00

148 lines
4.8 KiB
Python

import os
import random
import time
from typing import Optional, Tuple, List
import requests
# ====== HTTP Session ======
SESSION = requests.Session()
SESSION.headers.update({"User-Agent": "baekjoon-n8n-bot/1.0"})
# 필요한 언어만 관리(엄격 모드에서 제외할 대상)
KNOWN_LANGS = ["ko", "en", "ja", "ru", "zh", "de", "fr", "es", "pt", "it"]
def fetch_json_with_retry(url: str, params: dict, retries: int = 3, timeout=(3.05, 10)) -> dict:
last_err = None
for i in range(retries):
try:
res = SESSION.get(url, params=params, timeout=timeout)
res.raise_for_status()
return res.json()
except Exception as e:
last_err = e
time.sleep(0.7 * (2 ** i))
raise last_err
def parse_csv(s: str) -> List[str]:
return [x.strip() for x in (s or "").split(",") if x.strip()]
def env(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
def build_lang_filter(lang: str) -> str:
"""
사용자가 읽을 수 있는 언어가 '하나라도' 포함된 문제를 찾는 것이 목적입니다.
-%en 처럼 제외 필터를 쓰면, 한국어와 영어가 모두 있는 양질의 문제가 제외되므로
긍정 필터(%ko) 위주로 구성합니다.
"""
raw = (lang or "all").strip().lower()
if raw in ("all", ""):
return ""
allow = set(parse_csv(raw)) & set(KNOWN_LANGS)
if not allow:
return ""
# 여러 언어를 선택했을 경우(예: ko,en) -> (%ko | %en)
# 즉, 한국어 '또는' 영어 중 하나라도 지문이 있는 문제
if len(allow) == 1:
return f"%{next(iter(allow))}"
else:
expr = " | ".join(f"%{c}" for c in sorted(allow))
return f"({expr})"
def resolve_difficulty(difficulty: Optional[str], difficulty_mode: str) -> str:
if difficulty and difficulty.strip():
return difficulty.strip()
mode = (difficulty_mode or env("DIFFICULTY_MODE_DEFAULT", "easy")).lower()
if mode == "easy":
return env("DIFFICULTY_EASY", "6..10")
if mode == "hard":
return env("DIFFICULTY_HARD", "11..15")
if mode == "all":
return env("DIFFICULTY_ALL", "1..30")
return env("DIFFICULTY_EASY", "6..10")
def resolve_tags(tags_csv: Optional[str], tag_mode: str) -> List[str]:
"""
tags_csv(쿼리)가 있으면 그것이 최우선.
없으면 tag_mode 프리셋 기반으로 선택.
TAG_PICK_* 정책에 따라 랜덤 1개 / 전체 / 필터 없음으로 결정.
"""
if tags_csv is not None:
return parse_csv(tags_csv)
mode = (tag_mode or env("TAG_MODE_DEFAULT", "easy")).lower()
if mode == "easy":
preset = parse_csv(env("TAGS_EASY", ""))
pick = env("TAG_PICK_EASY", env("TAG_PICK", "random")).lower()
elif mode == "hard":
preset = parse_csv(env("TAGS_HARD", ""))
pick = env("TAG_PICK_HARD", env("TAG_PICK", "random")).lower()
elif mode == "all":
preset = parse_csv(env("TAGS_ALL", ""))
pick = env("TAG_PICK_ALL", env("TAG_PICK", "none")).lower()
else:
preset = parse_csv(env("TAGS_EASY", ""))
pick = env("TAG_PICK_EASY", "random").lower()
if pick == "none":
return []
if pick == "random":
return [random.choice(preset)] if preset else []
return preset
def build_query(difficulty: str, tags: List[str], lang: str) -> str:
# 1. 난이도 기본 조건
query_parts = [f"*{difficulty}"]
# 2. 태그 조건 (괄호로 감싸서 우선순위 확보)
if tags:
join_op = env("TAGS_JOIN", "or").lower()
if join_op == "and":
# 모든 태그가 다 있어야 함: tag:a tag:b
tag_expr = " ".join(f"tag:{t}" for t in tags)
else:
# 태그 중 하나만 있어도 됨: (tag:a | tag:b)
tag_expr = "(" + " | ".join(f"tag:{t}" for t in tags) + ")"
query_parts.append(tag_expr)
# 3. 언어 조건 (괄호로 감싸기)
lang_filter = build_lang_filter(lang)
if lang_filter:
# 언어 필터가 복합적일 수 있으므로 괄호 처리
query_parts.append(f"({lang_filter})")
# 결과 예시: *6..10 (tag:dp | tag:bfs) (%ko)
return " ".join(query_parts)
def get_problem(query: str, size: int = 50) -> Tuple[Optional[int], Optional[str], Optional[int]]:
url = "https://solved.ac/api/v3/search/problem"
params = {
"query": query,
"sort": "random",
"direction": "desc",
"page": 1,
"size": size,
}
try:
data = fetch_json_with_retry(url, params=params)
items = data.get("items", [])
if not items:
return None, None, None
p = random.choice(items)
return p.get("problemId"), (p.get("titleKo") or p.get("titleEn") or "제목 없음"), p.get("level")
except Exception:
return None, None, None