Files
baseball-automation/automation/review_input.py
2026-05-02 16:24:42 +09:00

234 lines
8.0 KiB
Python

"""
automation/review_input.py — 비디오 판독/합의 판정 입력
비디오 판독 이벤트를 관리자 사이트 팝업에 입력합니다.
"""
from __future__ import annotations
import re
from typing import Any
from playwright.sync_api import Page
from core.review_parser import parse_review_event_text
from automation.page_helpers import (
set_select_by_partial_text,
set_select_by_text_or_value,
)
def _normalize_review_event(review_event: dict[str, Any]) -> dict[str, Any]:
"""텍스트 기반 이벤트 파싱 및 정규화"""
has_results = review_event.get("beforeResult") is not None and review_event.get("finalResult") is not None
if review_event.get("requestInningLabel") and review_event.get("reviewItem") and has_results:
return review_event
text = review_event.get("text") or ""
parsed = parse_review_event_text(text)
parsed.update({k: v for k, v in review_event.items() if k not in parsed})
return parsed
def _open_challenge_popup(page: Page) -> Page:
"""비디오 판독 팝업 열기"""
with page.expect_popup(timeout=5000) as popup_info:
page.locator("#challengeBtn").click()
popup = popup_info.value
popup.wait_for_load_state("domcontentloaded")
popup.wait_for_selector("#requestInning_0", timeout=5000)
return popup
def _select_review_final_result(
popup: Page, row_index: int, review_item: str, final_result: str | None,
) -> None:
"""판독 결과 선택"""
from core.config_loader import review_result_groups
groups = review_result_groups()
# 설정에 매핑된 그룹/기본값 찾기
group_info = groups.get(review_item)
if group_info:
group_key = group_info["type"]
default_a = group_info["options"][0]
else:
group_key = "type3"
default_a = "인정"
result_value = final_result or default_a
# 셀렉터 찾기 시도
select_selector = f"#finalResult_{group_key}_{row_index}"
if not popup.locator(select_selector).count():
select_selector = f"#finalResult_{group_key[-1]}_{row_index}"
if not popup.locator(select_selector).count():
select_selector = f"#finalResult_type{group_key[-1]}_{row_index}"
set_select_by_text_or_value(popup, select_selector, result_value)
# JS 강제 이벤트 발생
try:
popup.locator(f"#finalResult_{row_index}").evaluate(
"""(node, value) => {
node.value = value;
node.dispatchEvent(new Event('change', { bubbles: true }));
}""",
result_value,
)
except Exception:
pass
def _fill_review_row(popup: Page, row_index: int, review_event: dict[str, Any]) -> None:
"""팝업의 한 행에 판독 이벤트 입력"""
request_inning = review_event.get("requestInningLabel") or "1초"
request_team = review_event.get("requestTeam") or popup.locator(f"#requestTeamId_{row_index} option").first.text_content() or ""
review_item = review_event.get("reviewItem") or "기타"
final_result = review_event.get("finalResult")
is_success_val = "Y" if (review_event.get("isSuccess") == "성공") else "N"
popup.wait_for_selector(f"#requestInning_{row_index}", timeout=3000)
set_select_by_text_or_value(popup, f"#requestInning_{row_index}", request_inning)
try:
set_select_by_partial_text(popup, f"#requestTeamId_{row_index}", request_team)
except Exception:
pass
if popup.is_closed(): return
try:
set_select_by_partial_text(popup, f"#forWhat_{row_index}", review_item)
except Exception:
set_select_by_text_or_value(popup, f"#forWhat_{row_index}", "기타")
popup.wait_for_timeout(100)
if popup.is_closed(): return
_select_review_final_result(popup, row_index, review_item, final_result)
if popup.is_closed(): return
set_select_by_text_or_value(popup, f"#isSuccess_{row_index}", is_success_val)
def _append_review_row(popup: Page) -> int:
"""신규 판독 행 추가"""
before_count = popup.locator("select[id^='requestInning_']").count()
popup.get_by_role("button", name="신규추가").click()
popup.wait_for_function(
"""(expectedCount) => {
return document.querySelectorAll("select[id^='requestInning_']").length > expectedCount;
}""",
arg=before_count,
timeout=3000,
)
row_index = popup.evaluate(
"""() => {
const ids = [...document.querySelectorAll("select[id^='requestInning_']")]
.map((el) => el.id)
.map((id) => Number(id.split("_").pop()))
.filter((num) => Number.isFinite(num));
return ids.length ? Math.max(...ids) : 0;
}"""
)
popup.wait_for_selector(f"#requestInning_{row_index}", timeout=5000)
return int(row_index)
def _can_reuse_initial_review_row(popup: Page) -> bool:
"""초기화된 0번 행 재사용 가능 여부"""
try:
row_count = popup.locator("select[id^='requestInning_']").count()
if row_count != 1:
return False
hidden_id = (popup.locator("#id_0").input_value() or "").strip()
if hidden_id:
return False
request_inning = popup.locator("#requestInning_0").input_value()
request_team = popup.locator("#requestTeamId_0").input_value()
review_item = popup.locator("#forWhat_0").input_value()
final_result = (popup.locator("#finalResult_0").input_value() or "").strip()
is_success = popup.locator("#isSuccess_0").input_value()
return (
request_inning == "1"
and review_item == "홈런타구 페어 파울"
and final_result == "페어"
and is_success == "Y"
and bool(request_team)
)
except Exception:
return False
def _save_review_popup(popup: Page) -> None:
"""팝업 저장 및 닫기"""
if popup.is_closed():
return
popup.evaluate("""() => {
window.confirm = () => true;
window.alert = () => {};
}""")
try:
with popup.expect_response(
re.compile(r"/manager/game/status/challenge/ajax"),
timeout=3000,
) as _:
save_btn = popup.locator("#saveLog")
if save_btn.count() > 0:
save_btn.click(force=True)
else:
popup.evaluate("""() => {
const btn = document.querySelector('#btnAdd')
|| document.querySelector('#btnSave')
|| [...document.querySelectorAll('button, a')].find(
el => el.innerText.includes('입력완료') || el.innerText.includes('저장')
);
if (btn) btn.click();
}""")
except Exception:
try:
popup.evaluate("""() => {
const btn = document.querySelector('#saveLog')
|| document.querySelector('#btnAdd');
if (btn) btn.click();
}""")
popup.wait_for_timeout(1000)
except Exception:
pass
try:
if not popup.is_closed():
popup.close()
except Exception:
pass
def record_review_events(page: Page, review_events: list[dict[str, Any]]) -> None:
"""비디오 판독 이벤트 전체 처리 파이프라인"""
normalized_events = [_normalize_review_event(event) for event in (review_events or [])]
if not normalized_events:
return
popup = _open_challenge_popup(page)
reuse_initial_row = _can_reuse_initial_review_row(popup)
for index, review_event in enumerate(normalized_events):
if index == 0 and reuse_initial_row:
row_index = 0
else:
row_index = _append_review_row(popup)
_fill_review_row(popup, row_index, review_event)
_save_review_popup(popup)
page.wait_for_timeout(300)
try:
page.bring_to_front()
except Exception:
pass