148 lines
4.8 KiB
Python
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
|