234 lines
8.0 KiB
Python
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
|