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