Add refactoring.md

This commit is contained in:
2026-05-02 16:36:13 +09:00
parent 859c39fe0c
commit 3c6df12e70
3 changed files with 1688 additions and 32 deletions

212
commands/lineup.py Normal file
View File

@@ -0,0 +1,212 @@
from __future__ import annotations
import argparse
import re
from typing import Any
from playwright.sync_api import Page, Playwright
from core.config_loader import position_to_defense_no
from core.normalizer import normalize_stadium_name, normalize_team_name
from commands.base import launch_browser_context
def normalize_number_text(number: str | int | None) -> str:
text = str(number or "").strip()
digits = "".join(char for char in text if char.isdigit())
if not digits:
return ""
return str(int(digits))
def normalize_player_name_text(name: str | None) -> str:
text = (name or "").replace("*", "").strip()
text = re.sub(r"\([^)]*\)\s*$", "", text).strip()
return text
def normalize_option_player_text(text: str) -> tuple[str, str]:
stripped = " ".join(text.split())
matched = re.match(r"^(.*?)\s*\[(\d+)번\]$", stripped)
if matched:
return normalize_player_name_text(matched.group(1)), normalize_number_text(matched.group(2))
return normalize_player_name_text(stripped), ""
def infer_option_role_hint(text: str) -> str:
stripped = " ".join(text.split())
matched = re.search(r"\(([^)]*)\)\s*(?:\[\d+번\])?$", stripped)
if not matched:
return ""
hint = matched.group(1).strip()
if hint == "":
return "pitcher"
if hint == "":
return "batter"
return ""
def infer_target_role_hint(position_name: str | None) -> str:
if position_name == "투수":
return "pitcher"
return "batter"
def get_select_options(page: Page, selector: str) -> list[dict[str, str]]:
return page.locator(selector).evaluate(
"""(el) => [...el.options].map(option => ({
value: option.value,
text: option.textContent.trim()
}))"""
)
def select_player_option(
page: Page,
selector: str,
player_name: str,
player_number: str | None,
position_name: str | None = None,
) -> None:
options = get_select_options(page, selector)
target_number = normalize_number_text(player_number)
normalized_player_name = normalize_player_name_text(player_name)
target_role_hint = infer_target_role_hint(position_name)
name_matches = []
role_filtered_matches = []
for option in options:
option_name, option_number = normalize_option_player_text(option["text"])
if option_name != normalized_player_name:
continue
name_matches.append(option)
option_role_hint = infer_option_role_hint(option["text"])
role_matches = (
not target_role_hint
or not option_role_hint
or option_role_hint == target_role_hint
)
if role_matches:
role_filtered_matches.append(option)
if target_number and option_number == target_number:
if not option_role_hint or option_role_hint == target_role_hint:
page.locator(selector).select_option(value=option["value"])
return
if len(role_filtered_matches) == 1:
page.locator(selector).select_option(value=role_filtered_matches[0]["value"])
return
if len(name_matches) == 1:
page.locator(selector).select_option(value=name_matches[0]["value"])
return
normalized_options = [normalize_option_player_text(option["text"]) for option in options]
similar_options = [
option["text"]
for option, (option_name, option_number) in zip(options, normalized_options)
if normalized_player_name in option_name or option_name in normalized_player_name or (target_number and option_number == target_number)
]
if not similar_options:
similar_options = [option["text"] for option in options]
raise ValueError(
f"{selector}에서 선수 '{player_name}' #{player_number} 옵션을 찾지 못했습니다. "
f"후보: {', '.join(similar_options[:10])}"
)
def select_position_option(page: Page, selector: str, position_name: str) -> None:
position_value = position_to_defense_no().get(position_name)
if not position_value:
raise ValueError(f"포지션 매핑이 없습니다: {position_name}")
page.locator(selector).select_option(value=position_value)
def build_lineup_entries(lineups: dict[str, Any], team_key: str) -> list[tuple[int, dict[str, Any]]]:
team_lineup = lineups[team_key]
entries = [(0, team_lineup["starter_pitcher"])]
entries.extend((int(player["bat_order"]), player) for player in team_lineup["players"])
return entries
def fill_lineup_form(page: Page, report: dict[str, Any]) -> None:
lineups = report["lineups"]
team_selector_map = {
"home_team": "home",
"away_team": "away",
}
for team_key, prefix in team_selector_map.items():
for order, player in build_lineup_entries(lineups, team_key):
if not player:
continue
player_selector = f"#{prefix}_player_id_{order}"
defense_selector = f"#{prefix}_defense_no_{order}"
select_player_option(page, player_selector, player["name"], player.get("number"), player.get("position"))
select_position_option(page, defense_selector, player["position"])
def find_edit_href(page: Page, report: dict[str, Any], manager_game_no: str | None) -> str:
game_info = report["game_info"]
target_date = game_info["date"]
target_stadium = normalize_stadium_name(game_info["stadium"])
target_home_team = normalize_team_name(game_info["home_team"])
target_away_team = normalize_team_name(game_info["away_team"])
rows: list[dict[str, Any]] = page.locator("table.gclist tr").evaluate_all(
"""(rows) => rows.slice(1).map((row) => {
const cells = [...row.cells].map((cell) => cell.innerText.trim());
const editLink = [...row.querySelectorAll('a')].find((anchor) => anchor.textContent.trim() === '수정');
return {
gameNo: cells[0] || '',
date: cells[1] || '',
gameType: cells[2] || '',
stadium: cells[3] || '',
homeTeam: cells[4] || '',
awayTeam: cells[5] || '',
href: editLink ? editLink.getAttribute('href') : '',
};
})"""
)
if manager_game_no:
matched = next((row for row in rows if row["gameNo"] == str(manager_game_no)), None)
if not matched or not matched["href"] or matched["href"].startswith("javascript:"):
raise ValueError(f"관리자 게임번호 {manager_game_no} 행의 수정 링크를 찾지 못했습니다.")
return matched["href"]
candidates = [
row for row in rows if row["href"] and not row["href"].startswith("javascript:")
and row["date"] == target_date
and normalize_stadium_name(row["stadium"]) == target_stadium
and normalize_team_name(row["homeTeam"]) == target_home_team
and normalize_team_name(row["awayTeam"]) == target_away_team
]
if not candidates:
raise ValueError("목록에서 일치하는 경기 수정 행을 찾지 못했습니다.")
return candidates[0]["href"]
def open_edit_page(page: Page, base_url: str, report: dict[str, Any], manager_game_no: str | None) -> None:
if manager_game_no:
page.goto(f"{base_url}/manager/game/write?id={manager_game_no}", wait_until="domcontentloaded")
page.wait_for_selector("#gameFrm", timeout=10000)
page.wait_for_selector("#home_player_id_1", timeout=10000)
return
page.goto(f"{base_url}/manager/game/list", wait_until="domcontentloaded")
page.wait_for_selector("table.gclist", timeout=10000)
edit_href = find_edit_href(page, report, manager_game_no)
with page.expect_navigation(wait_until="domcontentloaded"):
page.locator(f"a[href='{edit_href}']").first.click()
page.wait_for_selector("#gameFrm", timeout=10000)
page.wait_for_selector("#home_player_id_1", timeout=10000)
def run(playwright: Playwright, args: argparse.Namespace, report: dict[str, Any]) -> None:
browser = launch_browser_context(playwright, args.user_data_dir, args.channel, args.headless)
page = browser.pages[0] if browser.pages else browser.new_page()
try:
print(f"[{args.game_id}] 관리자 사이트 라인업 입력 시작...")
open_edit_page(page, args.base_url, report, args.manager_game_no)
fill_lineup_form(page, report)
page.evaluate("""() => { window.confirm = () => true; window.alert = () => {}; }""")
page.locator("#lineupWriteBtn").click()
page.wait_for_timeout(1000)
print(f"✅ 라인업 저장 완료")
finally:
if args.close:
try:
browser.close()
except Exception:
pass