""" 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