""" core/pitch_classifier.py — 투구/타자 결과 분류 네이버 리포트 데이터를 기반으로 관리자 사이트에서 선택해야 할 라디오 버튼의 라벨(eventName)을 결정합니다. Playwright 의존성 없이 순수 파이썬 로직만 포함합니다. """ from __future__ import annotations import re from typing import Any from core.config_loader import ( pitch_type_map, pitch_result_map, batter_result_map, ) # ────────────────────────────────────────────── # 구종 분류 # ────────────────────────────────────────────── def classify_pitch_type(pitch_type_text: str) -> str | None: """네이버 구종 텍스트 → 사이트 구종 라벨 예: '직구' → '패스트볼', '포크' → '포크볼' """ return pitch_type_map().get(pitch_type_text or "") # ────────────────────────────────────────────── # 투구 결과 분류 # ────────────────────────────────────────────── def classify_pitch_result(pitch_result_text: str) -> str | None: """네이버 투구결과 텍스트 → 사이트 투구결과 라벨 예: '볼' → '볼', '스트라이크' → '스트라이크(루킹)' """ return pitch_result_map().get(pitch_result_text or "") def normalize_pitch_result_code(pitch: dict[str, Any], event: dict[str, Any] | None = None) -> str: """투구의 pitchResult 코드를 정규화 피치클락, 번트헛스윙, 폭투/포일 등 특수 상황 처리 """ pitch_result = (pitch.get("pitchResult") or "").strip() pitch_result_text = (pitch.get("pitchResultText") or "").strip() normalized_text = pitch_result_text.replace(" ", "") # 피치클락 투수위반 → 볼 if "피치클락" in pitch_result_text and "투수위반" in pitch_result_text: return "B" # 번트 헛스윙 → BS if "번트" in normalized_text and "헛스윙" in normalized_text: return "BS" # 폭투/포일 진루 시 → 볼 runner_events = _get_pitch_runner_events(pitch, event) if any( re_.get("type") == "wild_pitch_advance" or "폭투" in (re_.get("text") or "") for re_ in runner_events ): return "B" if any( re_.get("type") == "passed_ball_advance" or "포일" in (re_.get("text") or "") for re_ in runner_events ): return "B" return pitch_result # ────────────────────────────────────────────── # 타자 결과 분류 # ────────────────────────────────────────────── def classify_batter_result(result_type: str) -> str | None: """결과 타입 코드 → 사이트 타자결과 라벨 (기본 매핑) 더 복잡한 추론이 필요한 경우 infer_batter_result_label 사용. """ return batter_result_map().get(result_type or "") def infer_batter_result_label( result: dict[str, Any], event: dict[str, Any] | None = None, ) -> str | None: """타석 결과를 종합적으로 추론하여 사이트 라벨 반환 result.type, result.text, 주루이벤트, 마지막 투구 등을 모두 분석. """ result_type = result.get("type") or "" result_text = (result.get("text") or "").strip() runner_events = (event or {}).get("runnerEvents") or [] last_pitch_result_text = get_last_pitch_result_text(event) # 낫아웃 if result_type == "strikeout_not_out" or "낫아웃" in result_text: if "폭투" in result_text: return "폭투 낫아웃 진루" if "포일" in result_text: return "포일 낫아웃 진루" if "아웃" in result_text: return "스트라이크-낫아웃" return "낫아웃-출루" # 삼진 if result_type == "strikeout": if "헛스윙" in last_pitch_result_text or "헛스윙" in result_text: return "스윙 스트라이크-아웃" return "루킹스트라이크-아웃" # 희생 번트 if "희생 번트" in result_text or "희생번트" in result_text: return "희생 번트" # 번트 아웃 if "번트 아웃" in result_text or "번트아웃" in result_text: return "번트-아웃" # 보크 if any( "보크" in (re_.get("text") or "") and "진루" in (re_.get("text") or "") for re_ in runner_events ): if "볼" in last_pitch_result_text: return "보크-볼" return "보크" # 폭투-볼 if any(re_.get("type") == "wild_pitch_advance" for re_ in runner_events): return "폭투-볼" # 포볼 if result_type == "walk": if any( re_.get("type") == "wild_pitch_advance" or "폭투" in (re_.get("text") or "") for re_ in runner_events ): return "폭투-포볼" if any( re_.get("type") == "passed_ball_advance" or "포일" in (re_.get("text") or "") for re_ in runner_events ): return "포일-포볼" return "포볼" # 포일-볼/스트라이크 if any( (re_.get("type") or "") == "passed_ball_advance" for re_ in runner_events ): if "볼" in last_pitch_result_text: return "포일-볼" return "포일-스트라이크" # 수비실책 if result_type == "reach_on_error" or "실책" in result_text: return "수비실책" # 야수선택 if result_type == "reach_on_fielder_choice": return "야수선택" # 땅볼출루 if result_type == "reach_on_grounder": return "땅볼출루(무안타)" # 병살 if result_type == "double_play": if "번트" in result_text: return "번트-병살" return "병살-아웃" # N루타 후 주루아웃 if result_type == "single_runner_out": return "1루타 후 주루아웃" if result_type == "double_runner_out": return "2루타 후 주루아웃" if result_type == "triple_runner_out": return "3루타 후 주루아웃" # N루타 후 수비실책진루 if result_type == "single_error_advance": return "1루타 후 수비실책진루" if result_type == "double_error_advance": return "2루타 후 수비실책진루" if result_type == "triple_error_advance": return "3루타 후 수비실책진루" # 파울희생플라이 if "파울희생플라이" in result_text or "파울 희생플라이" in result_text: return "희생 플라이" # 아웃 상세 if result_type == "out": if "병살" in result_text: if "번트" in result_text: return "번트-병살" return "병살-아웃" if "희생 플라이" in result_text or "희생플라이" in result_text: return "희생 플라이" if "인필드플라이" in result_text: return "인필드플라이" if "파울플라이" in result_text: return "파울플라이-아웃" return "아웃" # 번트안타 if result_type == "bunt_hit": return "번트안타" # 내야안타 if result_type == "single": if "번트안타" in result_text: return "번트안타" if "내야안타" in result_text: return "내야안타" # 몸에 맞는 볼 if result_type == "hit_by_pitch" or "헤드샷" in result_text: return "몸에 맞는 볼" # 기본 매핑 폴백 return classify_batter_result(result_type) def is_simple_terminal_result_type(result_type: str) -> bool: """팝업 없이 즉시 완료되는 결과 타입인지 확인""" return result_type in {"strikeout", "strikeout_not_out", "walk", "intentional_walk", "hit_by_pitch"} def is_ball_in_play_event(event: dict[str, Any]) -> bool: """인플레이 이벤트인지 확인 (마지막 투구가 H)""" pitches = event.get("pitches") or [] result = event.get("result") or {} if not pitches or not result: return False return pitches[-1].get("pitchResult") == "H" # ────────────────────────────────────────────── # 내부 헬퍼 # ────────────────────────────────────────────── def _get_pitch_runner_events( pitch: dict[str, Any], event: dict[str, Any] | None, ) -> list[dict[str, Any]]: """투구에 연결된 주루이벤트 반환""" if pitch.get("runnerEvents"): return pitch["runnerEvents"] if event: pitch_num = pitch.get("pitchNum") for re_ in (event.get("runnerEvents") or []): if re_.get("pitchNum") == pitch_num: return [re_] return [] def get_last_pitch_result_text(event: dict[str, Any] | None) -> str: """이벤트의 마지막 투구 결과 텍스트 반환""" if not event: return "" pitches = event.get("pitches") or [] if not pitches: return "" return (pitches[-1].get("pitchResultText") or "").strip()