refactoring
This commit is contained in:
299
automation/lineup_input.py
Normal file
299
automation/lineup_input.py
Normal file
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
automation/lineup_input.py — 라인업 및 선수 교체 입력
|
||||
|
||||
관리자 사이트의 홈/원정 라인업 조작, 선수명 및 포지션 선택,
|
||||
교체 이벤트 처리를 담당합니다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from core.config_loader import position_to_defense_no
|
||||
from core.change_parser import normalize_change_event
|
||||
|
||||
|
||||
def normalize_lineup_text(text: str) -> str:
|
||||
"""라인업 선수명 정규화 (괄호, 번호 등 제거하고 이름만 추출)"""
|
||||
text = (text or "").strip()
|
||||
text = text.replace("*", "")
|
||||
text = re.sub(r"\[\d+(?:번)?\]", "", text)
|
||||
text = re.sub(r"\s*\(.*?\)\s*", "", text)
|
||||
text = "".join(re.findall(r"[가-힣A-Za-z]+", text))
|
||||
return text.strip()
|
||||
|
||||
|
||||
def get_lineup_state(page: Page) -> dict[str, Any]:
|
||||
"""현재 사이트에 입력된 라인업 상태(홈/원정 0~9번 행) 추출"""
|
||||
return page.evaluate(
|
||||
"""() => {
|
||||
const buildSide = (side) => {
|
||||
const rows = [];
|
||||
for (let idx = 0; idx <= 9; idx += 1) {
|
||||
const player = document.querySelector(`#${side}_player_id_${idx}`);
|
||||
const defense = document.querySelector(`#${side}_defense_no_${idx}`);
|
||||
const orgPlayer = document.querySelector(`#org_${side}_player_id_${idx}`);
|
||||
const orgDefense = document.querySelector(`#org_${side}_defense_no_${idx}`);
|
||||
if (!player && !defense) continue;
|
||||
rows.push({
|
||||
idx,
|
||||
playerText: player ? player.options[player.selectedIndex]?.text || '' : '',
|
||||
playerValue: player ? player.value : '',
|
||||
defenseValue: defense ? defense.value : '',
|
||||
orgPlayerValue: orgPlayer ? orgPlayer.value : '',
|
||||
orgDefenseValue: orgDefense ? orgDefense.value : '',
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
};
|
||||
return { home: buildSide('home'), away: buildSide('away') };
|
||||
}"""
|
||||
)
|
||||
|
||||
|
||||
def detect_change_side(
|
||||
half_inning: dict[str, Any], change_event: dict[str, Any], lineup_state: dict[str, Any],
|
||||
) -> str:
|
||||
"""교체가 일어난 팀(home/away) 추론"""
|
||||
actor_name = normalize_lineup_text(
|
||||
change_event.get("actor_name") or change_event.get("player_name") or ""
|
||||
)
|
||||
offense_side = "away" if half_inning.get("half") == "top" else "home"
|
||||
defense_side = "home" if offense_side == "away" else "away"
|
||||
|
||||
matched_sides: list[str] = []
|
||||
for side in ("home", "away"):
|
||||
for row in lineup_state.get(side, []):
|
||||
if normalize_lineup_text(row.get("playerText") or "") == actor_name:
|
||||
matched_sides.append(side)
|
||||
break
|
||||
|
||||
if len(matched_sides) == 1:
|
||||
return matched_sides[0]
|
||||
|
||||
actor_role = change_event.get("actor_role")
|
||||
if actor_role in {"batter", "대타", "대주자", "1루주자", "2루주자", "3루주자", "주자"}:
|
||||
return offense_side
|
||||
|
||||
return defense_side
|
||||
|
||||
|
||||
def find_change_row(side_rows: list[dict[str, Any]], change_event: dict[str, Any]) -> int | None:
|
||||
"""교체 대상 선수가 속한 라인업 행 인덱스 검색"""
|
||||
if change_event.get("bat_order") is not None:
|
||||
return int(change_event["bat_order"])
|
||||
|
||||
actor_name_raw = (
|
||||
change_event.get("actor_name")
|
||||
or change_event.get("player_name")
|
||||
or change_event.get("out_player")
|
||||
or ""
|
||||
)
|
||||
actor_name = normalize_lineup_text(actor_name_raw)
|
||||
|
||||
# 1단계: 완전 일치
|
||||
for row in side_rows:
|
||||
if normalize_lineup_text(row.get("playerText") or "") == actor_name:
|
||||
return int(row["idx"])
|
||||
|
||||
# 2단계: 부분 일치
|
||||
for row in side_rows:
|
||||
player_text = normalize_lineup_text(row.get("playerText") or "")
|
||||
if actor_name and (actor_name in player_text or player_text in actor_name):
|
||||
return int(row["idx"])
|
||||
|
||||
# 3단계: 역할(defenseValue) 일치
|
||||
actor_role = change_event.get("actor_role")
|
||||
pos_def_map = position_to_defense_no()
|
||||
if actor_role in pos_def_map:
|
||||
defense_no = pos_def_map[actor_role]
|
||||
for row in side_rows:
|
||||
if str(row.get("defenseValue") or "") == defense_no:
|
||||
return int(row["idx"])
|
||||
|
||||
# 4단계: 이름 혼재 경우 (예: "1루주자 문보경")
|
||||
if " " in actor_name_raw:
|
||||
potential_name = normalize_lineup_text(actor_name_raw.split()[-1])
|
||||
for row in side_rows:
|
||||
if normalize_lineup_text(row.get("playerText") or "") == potential_name:
|
||||
return int(row["idx"])
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_pitcher_row(side_rows: list[dict[str, Any]]) -> int | None:
|
||||
"""투수가 위치한 행(일반적으로 0번) 찾기"""
|
||||
pos_def_map = position_to_defense_no()
|
||||
pitcher_no = pos_def_map.get("투수")
|
||||
for row in side_rows:
|
||||
if str(row.get("defenseValue") or "") == pitcher_no:
|
||||
return int(row["idx"])
|
||||
return None
|
||||
|
||||
|
||||
def select_lineup_player(page: Page, side: str, row_idx: int, player_name: str) -> None:
|
||||
"""라인업 select 상자에서 선수명으로 선택"""
|
||||
select_id = f"#{side}_player_id_{row_idx}"
|
||||
options = page.locator(f"{select_id} option").evaluate_all(
|
||||
"""(nodes) => nodes.map((node) => ({ value: node.value, text: node.textContent.trim() }))"""
|
||||
)
|
||||
target_value = None
|
||||
normalized_target = normalize_lineup_text(player_name)
|
||||
for option in options:
|
||||
if normalize_lineup_text(option["text"]) == normalized_target:
|
||||
target_value = option["value"]
|
||||
break
|
||||
|
||||
if not target_value:
|
||||
raise ValueError(f"{side} {row_idx}번 행에서 선수 '{player_name}' 옵션을 찾지 못했습니다.")
|
||||
page.locator(select_id).select_option(value=target_value)
|
||||
|
||||
|
||||
def set_lineup_defense(page: Page, side: str, row_idx: int, position: str | None) -> None:
|
||||
"""라인업 수비 포지션 세팅"""
|
||||
if not position:
|
||||
return
|
||||
defense_no = position_to_defense_no().get(position)
|
||||
if not defense_no:
|
||||
return
|
||||
page.locator(f"#{side}_defense_no_{row_idx}").select_option(value=defense_no)
|
||||
|
||||
|
||||
def get_current_lineup_selection(page: Page, side: str, row_idx: int) -> tuple[str, str]:
|
||||
"""해당 행의 현재 선택된 선수와 수비 번호 값 반환"""
|
||||
player_value = page.locator(f"#{side}_player_id_{row_idx}").input_value()
|
||||
defense_value = page.locator(f"#{side}_defense_no_{row_idx}").input_value()
|
||||
return player_value, defense_value
|
||||
|
||||
|
||||
def get_target_player_value(page: Page, side: str, row_idx: int, player_name: str | None) -> str | None:
|
||||
if not player_name:
|
||||
return None
|
||||
select_id = f"#{side}_player_id_{row_idx}"
|
||||
options = page.locator(f"{select_id} option").evaluate_all(
|
||||
"""(nodes) => nodes.map((node) => ({ value: node.value, text: node.textContent.trim() }))"""
|
||||
)
|
||||
normalized_target = normalize_lineup_text(player_name)
|
||||
for option in options:
|
||||
if normalize_lineup_text(option["text"]) == normalized_target:
|
||||
return option["value"]
|
||||
return None
|
||||
|
||||
|
||||
def get_target_defense_value(position: str | None) -> str | None:
|
||||
if not position:
|
||||
return None
|
||||
return position_to_defense_no().get(position)
|
||||
|
||||
|
||||
def apply_change_event(
|
||||
page: Page,
|
||||
half_inning: dict[str, Any],
|
||||
change_event: dict[str, Any],
|
||||
change_cache: dict[str, tuple[str, int]],
|
||||
) -> None:
|
||||
"""선수 교체 이벤트를 라인업에 반영하고 저장"""
|
||||
change_event = normalize_change_event(change_event)
|
||||
actor_name_raw = change_event.get("actor_name") or change_event.get("player_name") or change_event.get("out_player") or ""
|
||||
cache_key = normalize_lineup_text(actor_name_raw)
|
||||
actor_name = normalize_lineup_text(actor_name_raw)
|
||||
|
||||
lineup_state = get_lineup_state(page)
|
||||
cached = change_cache.get(cache_key)
|
||||
side = cached[0] if cached else detect_change_side(half_inning, change_event, lineup_state)
|
||||
side_rows = lineup_state.get(side, [])
|
||||
row_idx = find_change_row(side_rows, change_event)
|
||||
|
||||
if row_idx is None and cached and cached[0] == side:
|
||||
row_idx = cached[1]
|
||||
|
||||
if row_idx is None:
|
||||
all_players = [normalize_lineup_text(r.get("playerText", "")) for r in side_rows]
|
||||
raise ValueError(
|
||||
f"{side} 교체 행을 찾지 못했습니다 "
|
||||
f"(Target: {actor_name}, Candidates: {all_players}): {change_event.get('text')}"
|
||||
)
|
||||
|
||||
# 확인창 무시
|
||||
page.evaluate("""() => { window.alert = () => {}; window.confirm = () => true; }""")
|
||||
|
||||
def trigger_lineup_save(idx: int):
|
||||
home_away_gb = 2 if side == "home" else 1
|
||||
page.evaluate(
|
||||
"""({ batterNo, homeAwayGb }) => {
|
||||
if (typeof window.f_lineup === 'function') {
|
||||
window.f_lineup(batterNo, homeAwayGb);
|
||||
}
|
||||
}""",
|
||||
{"batterNo": idx, "homeAwayGb": home_away_gb},
|
||||
)
|
||||
page.wait_for_timeout(200)
|
||||
|
||||
page.evaluate(
|
||||
"""({ side, idx }) => {
|
||||
const row = document.querySelector(`#${side}_player_id_${idx}`)?.parentElement?.parentElement;
|
||||
if (row) {
|
||||
const saveBtn = [...row.querySelectorAll("input[type=button], button, a")].find(el =>
|
||||
el.value === 'V' || el.innerText.includes('V') || el.innerText.includes('저장') || el.id.includes('save')
|
||||
);
|
||||
if (saveBtn) {
|
||||
saveBtn.click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}""",
|
||||
{"side": side, "idx": idx}
|
||||
)
|
||||
page.wait_for_timeout(200)
|
||||
|
||||
# 병합 교체 (예: 포수→지명타자 전환 + 새 투수 등판)
|
||||
if change_event.get("change_type") == "merged_pitcher_substitution":
|
||||
actor_player_name = change_event.get("player_name") or change_event.get("actor_name")
|
||||
pitcher_in_player = change_event.get("pitcher_in_player") or change_event.get("in_player")
|
||||
pitcher_row_idx = find_pitcher_row(side_rows)
|
||||
if pitcher_row_idx is None or not pitcher_in_player:
|
||||
raise ValueError(f"투수 교체 행을 찾지 못했습니다: {change_event.get('text')}")
|
||||
|
||||
set_lineup_defense(page, side, row_idx, "지명타자")
|
||||
trigger_lineup_save(row_idx)
|
||||
trigger_lineup_save(row_idx + 1)
|
||||
|
||||
select_lineup_player(page, side, pitcher_row_idx, pitcher_in_player)
|
||||
set_lineup_defense(page, side, pitcher_row_idx, "투수")
|
||||
trigger_lineup_save(pitcher_row_idx)
|
||||
trigger_lineup_save(pitcher_row_idx + 1)
|
||||
|
||||
if actor_player_name:
|
||||
change_cache[normalize_lineup_text(actor_player_name)] = (side, int(row_idx))
|
||||
change_cache[normalize_lineup_text(pitcher_in_player)] = (side, int(pitcher_row_idx))
|
||||
return
|
||||
|
||||
# 단순 선수 교체
|
||||
if change_event.get("change_type") == "substitution":
|
||||
in_player = change_event.get("in_player")
|
||||
if not in_player:
|
||||
return
|
||||
select_lineup_player(page, side, row_idx, in_player)
|
||||
set_lineup_defense(page, side, row_idx, change_event.get("to_position"))
|
||||
trigger_lineup_save(row_idx)
|
||||
trigger_lineup_save(row_idx + 1)
|
||||
|
||||
out_player = change_event.get("out_player")
|
||||
if out_player:
|
||||
change_cache[normalize_lineup_text(out_player)] = (side, int(row_idx))
|
||||
change_cache[normalize_lineup_text(in_player)] = (side, int(row_idx))
|
||||
return
|
||||
|
||||
# 단순 수비 위치 변경
|
||||
if change_event.get("change_type") == "position_change":
|
||||
set_lineup_defense(page, side, row_idx, change_event.get("to_position"))
|
||||
trigger_lineup_save(row_idx)
|
||||
trigger_lineup_save(row_idx + 1)
|
||||
|
||||
player_name = change_event.get("player_name")
|
||||
if player_name:
|
||||
change_cache[normalize_lineup_text(player_name)] = (side, int(row_idx))
|
||||
return
|
||||
Reference in New Issue
Block a user