refactoring
This commit is contained in:
271
automation/runner_input.py
Normal file
271
automation/runner_input.py
Normal file
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
automation/runner_input.py — 주루 입력
|
||||
|
||||
주루 이벤트(진루, 도루, 견제, 실책 진루 등)를 관리자 사이트에 입력합니다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from time import time, sleep
|
||||
from typing import Any
|
||||
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from core.field_calculator import is_double_play_result, extract_error_position
|
||||
from core.runner_classifier import infer_runner_action_label
|
||||
from automation.page_helpers import (
|
||||
get_last_visible_enabled_locator,
|
||||
set_radio_by_label,
|
||||
get_checked_event_name,
|
||||
)
|
||||
from automation.defense_popup import (
|
||||
fill_error_defense_popup,
|
||||
fill_runner_out_defense,
|
||||
)
|
||||
|
||||
|
||||
def _split_complex_runner_event(
|
||||
runner_event: dict[str, Any],
|
||||
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
||||
"""복합 주루 이벤트(예: 1루주자 2루까지 진루 / 홈까지 들어오다 아웃) 분할"""
|
||||
text = runner_event.get("text") or ""
|
||||
|
||||
if "/" not in text or ("실책" not in text and "아웃" not in text):
|
||||
return runner_event, None
|
||||
|
||||
parts = [p.strip() for p in text.split("/") if p.strip()]
|
||||
if len(parts) < 2:
|
||||
return runner_event, None
|
||||
|
||||
def extract_base(t: str) -> int | None:
|
||||
m = re.search(r"([123])루", t)
|
||||
return int(m.group(1)) if m else None
|
||||
|
||||
# 1차 이벤트
|
||||
primary = dict(runner_event)
|
||||
primary["text"] = parts[0]
|
||||
intermediate_to = extract_base(parts[0])
|
||||
if intermediate_to:
|
||||
primary["toBase"] = intermediate_to
|
||||
|
||||
# 2차 이벤트
|
||||
secondary = dict(runner_event)
|
||||
secondary["fromBase"] = primary.get("toBase")
|
||||
secondary["text"] = parts[1]
|
||||
|
||||
if "실책" in parts[1]:
|
||||
secondary["type"] = "error_advance"
|
||||
elif "태그" in parts[1]:
|
||||
secondary["type"] = "tag_out"
|
||||
elif "포스" in parts[1]:
|
||||
secondary["type"] = "force_out"
|
||||
else:
|
||||
secondary["type"] = "out"
|
||||
|
||||
return primary, secondary
|
||||
|
||||
|
||||
def _open_runner_area(page: Page, from_base: int, area_type: int) -> None:
|
||||
"""주루 영역(1: 진루, 2: 액션) 열기"""
|
||||
function_name = f"changRunnerArea{from_base}"
|
||||
page.evaluate(
|
||||
"""({ functionName, areaType }) => {
|
||||
const fn = window[functionName];
|
||||
if (typeof fn === 'function') {
|
||||
fn(areaType);
|
||||
}
|
||||
}""",
|
||||
{"functionName": function_name, "areaType": area_type},
|
||||
)
|
||||
|
||||
deadline = time() + 2
|
||||
radio_name = f"evt_runner_{from_base}"
|
||||
advance_name = f"dat_evt_runner_{from_base}_advance"
|
||||
|
||||
while time() < deadline:
|
||||
ready = page.evaluate(
|
||||
"""({ radioName, advanceName }) => {
|
||||
const hasEnabledVisibleRadio = [...document.querySelectorAll(`input[type=radio][name='${radioName}']`)].some((node) => {
|
||||
const rect = node.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(node);
|
||||
return !!(rect.width && rect.height && style.display !== 'none' && style.visibility !== 'hidden' && !node.disabled);
|
||||
});
|
||||
const hasEnabledVisibleAdvance = [...document.querySelectorAll(`input[type=radio][name='${advanceName}']`)].some((node) => {
|
||||
const rect = node.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(node);
|
||||
return !!(rect.width && rect.height && style.display !== 'none' && style.visibility !== 'hidden' && !node.disabled);
|
||||
});
|
||||
return hasEnabledVisibleRadio || hasEnabledVisibleAdvance;
|
||||
}""",
|
||||
{"radioName": radio_name, "advanceName": advance_name},
|
||||
)
|
||||
if ready:
|
||||
return
|
||||
sleep(0.1)
|
||||
|
||||
|
||||
def _set_runner_action(page: Page, from_base: int, label: str) -> None:
|
||||
"""주자 액션(일반진루, 도루성공 등) 라디오 버튼 세팅"""
|
||||
radio_name = f"evt_runner_{from_base}"
|
||||
locator = page.evaluate(
|
||||
r"""({ radioName, eventName }) => {
|
||||
const nodes = [...document.querySelectorAll(`input[type=radio][name='${radioName}']`)];
|
||||
// 1단계: eventName 속성으로 매칭
|
||||
for (const node of nodes) {
|
||||
const rect = node.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(node);
|
||||
if (!rect.width || !rect.height || style.display === 'none' || style.visibility === 'hidden' || node.disabled) {
|
||||
continue;
|
||||
}
|
||||
if ((node.getAttribute('eventName') || '') === eventName) {
|
||||
const marker = `codex-runner-${Math.random().toString(36).slice(2)}`;
|
||||
node.setAttribute('data-codex-marker', marker);
|
||||
return marker;
|
||||
}
|
||||
}
|
||||
// 2단계: 텍스트로 부분 매칭
|
||||
for (const node of nodes) {
|
||||
let text = '';
|
||||
let p = node.parentElement;
|
||||
if (p) text = p.textContent.trim();
|
||||
if (!text && node.nextSibling) text = node.nextSibling.textContent || '';
|
||||
|
||||
if (eventName && text.replace(/\s/g, '').includes(eventName.replace(/\s/g, ''))) {
|
||||
const marker = `codex-runner-${Math.random().toString(36).slice(2)}`;
|
||||
node.setAttribute('data-codex-marker', marker);
|
||||
return marker;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}""",
|
||||
{"radioName": radio_name, "eventName": label},
|
||||
)
|
||||
|
||||
if locator:
|
||||
candidate = page.locator(f"[data-codex-marker='{locator}']")
|
||||
for _ in range(5):
|
||||
try:
|
||||
candidate.click(force=True, timeout=500)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
candidate.evaluate(
|
||||
"""(node) => {
|
||||
node.disabled = false;
|
||||
node.checked = true;
|
||||
node.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
node.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}"""
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
page.wait_for_timeout(100)
|
||||
if get_checked_event_name(page, radio_name) == label:
|
||||
return
|
||||
else:
|
||||
for _ in range(5):
|
||||
set_radio_by_label(page, radio_name, label)
|
||||
page.wait_for_timeout(50)
|
||||
if get_checked_event_name(page, radio_name) == label:
|
||||
return
|
||||
|
||||
|
||||
def _set_runner_advance(page: Page, from_base: int, to_base: int | None) -> None:
|
||||
"""주자 최종 목적지 루 세팅"""
|
||||
if to_base is None:
|
||||
return
|
||||
selector = f"input[name='dat_evt_runner_{from_base}_advance'][value='{to_base}']"
|
||||
deadline = time() + 3
|
||||
locator = None
|
||||
|
||||
while time() < deadline:
|
||||
locator = get_last_visible_enabled_locator(page, selector)
|
||||
if locator is not None:
|
||||
break
|
||||
sleep(0.1)
|
||||
|
||||
if locator is None:
|
||||
fallback = page.locator(selector)
|
||||
if fallback.count():
|
||||
locator = fallback.last
|
||||
else:
|
||||
return # timeout
|
||||
|
||||
try:
|
||||
locator.click(force=True)
|
||||
except Exception:
|
||||
locator.evaluate(
|
||||
"""(node) => {
|
||||
node.disabled = false;
|
||||
node.checked = true;
|
||||
node.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
node.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}"""
|
||||
)
|
||||
|
||||
|
||||
def set_runner_events(
|
||||
page: Page, event: dict[str, Any], runner_events: list[dict[str, Any]] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""모든 주루 이벤트를 처리하고 지연 처리할 이벤트(late_events) 반환"""
|
||||
if runner_events is None:
|
||||
runner_events = (event.get("runnerEvents") or []).copy()
|
||||
|
||||
late_events = []
|
||||
filtered_events = []
|
||||
for re_item in runner_events:
|
||||
primary, secondary = _split_complex_runner_event(re_item)
|
||||
filtered_events.append(primary)
|
||||
if secondary:
|
||||
late_events.append(secondary)
|
||||
|
||||
for runner_event in filtered_events:
|
||||
from_base = runner_event.get("fromBase")
|
||||
if from_base not in {1, 2, 3}:
|
||||
continue
|
||||
|
||||
label = infer_runner_action_label(event, runner_event)
|
||||
if not label:
|
||||
continue
|
||||
|
||||
if any(k in label for k in ["도루", "견제", "폭투", "포일", "아웃"]):
|
||||
area_type = 2
|
||||
else:
|
||||
area_type = 1
|
||||
|
||||
if any(k in label for k in ["일반 진루", "볼넷 진루", "수비 실책"]):
|
||||
area_type = 1
|
||||
|
||||
_open_runner_area(page, from_base, area_type)
|
||||
_set_runner_action(page, from_base, label)
|
||||
|
||||
runner_text = runner_event.get("text") or ""
|
||||
is_error_related_label = (
|
||||
(label and "실책" in label)
|
||||
or label in {"견제 에러", "수비 실책", "도루성공&실책"}
|
||||
or "실책" in runner_text
|
||||
)
|
||||
if is_error_related_label:
|
||||
if extract_error_position(runner_text):
|
||||
fill_error_defense_popup(page, runner_text)
|
||||
|
||||
if label in {"태그아웃", "도루시도 아웃", "포스아웃"}:
|
||||
fill_runner_out_defense(page, runner_text)
|
||||
|
||||
page.wait_for_timeout(150)
|
||||
_set_runner_advance(page, from_base, runner_event.get("toBase"))
|
||||
|
||||
try:
|
||||
extra_advance = runner_event.get("extra_advance")
|
||||
if extra_advance and extra_advance > 0:
|
||||
locator = get_last_visible_enabled_locator(page, f"select[name='runner{from_base}_running_add']")
|
||||
if locator is not None:
|
||||
locator.select_option(value=str(extra_advance))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
page.wait_for_timeout(150)
|
||||
|
||||
return late_events
|
||||
Reference in New Issue
Block a user