Add refactoring.md
This commit is contained in:
68
cli.py
68
cli.py
@@ -15,6 +15,8 @@ from playwright.sync_api import sync_playwright
|
|||||||
from commands.base import add_common_arguments, load_report
|
from commands.base import add_common_arguments, load_report
|
||||||
from commands.record import run as run_record
|
from commands.record import run as run_record
|
||||||
|
|
||||||
|
DEFAULT_BASE_URL = "http://58.229.253.168:8089"
|
||||||
|
|
||||||
|
|
||||||
def _parse_args() -> argparse.Namespace:
|
def _parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(description="Baseball Automation CLI")
|
parser = argparse.ArgumentParser(description="Baseball Automation CLI")
|
||||||
@@ -39,39 +41,41 @@ def _parse_args() -> argparse.Namespace:
|
|||||||
def interactive_mode() -> int:
|
def interactive_mode() -> int:
|
||||||
print("=== Baseball Automation CLI ===")
|
print("=== Baseball Automation CLI ===")
|
||||||
print("1. 네이버 데이터 크롤링 (crawl)")
|
print("1. 네이버 데이터 크롤링 (crawl)")
|
||||||
print("2. 관리자 사이트 기록 자동 입력 (record)")
|
print("2. 관리자 사이트 라인업 입력 (lineup)")
|
||||||
print("3. 크롤링 + 기록 입력 한 번에 실행 (crawl & record)")
|
print("3. 관리자 사이트 기록 자동 입력 (record)")
|
||||||
|
print("4. 크롤링 + 라인업 + 기록 한 번에 실행 (crawl -> lineup -> record)")
|
||||||
|
|
||||||
choice = input("\n실행할 작업을 선택하세요 (1/2/3) [3]: ").strip() or "3"
|
choice = input("\n실행할 작업을 선택하세요 (1/2/3/4) [4]: ").strip() or "4"
|
||||||
if choice not in {"1", "2", "3"}:
|
if choice not in {"1", "2", "3", "4"}:
|
||||||
print("잘못된 선택입니다.")
|
print("잘못된 입력입니다.")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
game_id = input("경기 ID를 입력하세요 (예: 20260501NCLG02026): ").strip()
|
game_id = input("경기 ID를 입력하세요 (예: 20260501NCLG02026): ").strip()
|
||||||
if not game_id:
|
if not game_id:
|
||||||
print("경기 ID가 필요합니다.")
|
print("경기 ID가 필요합니다.")
|
||||||
return 1
|
return 1
|
||||||
game_id = "".join(game_id.split())
|
|
||||||
|
|
||||||
base_url = ""
|
base_url = DEFAULT_BASE_URL
|
||||||
user_data_dir = ""
|
user_data_dir = None
|
||||||
if choice in {"2", "3"}:
|
manager_game_no = ""
|
||||||
base_url = input("기록 사이트 기본 URL을 입력하세요 (엔터 시 site.txt 자동 참조): ").strip()
|
|
||||||
if not base_url:
|
|
||||||
site_txt = Path("site.txt")
|
|
||||||
if site_txt.exists():
|
|
||||||
lines = site_txt.read_text(encoding="utf-8").splitlines()
|
|
||||||
if lines and lines[0].startswith("http"):
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
parsed = urlparse(lines[0])
|
|
||||||
base_url = f"{parsed.scheme}://{parsed.netloc}"
|
|
||||||
print(f"👉 [자동 설정] 기록 사이트 URL: {base_url}")
|
|
||||||
|
|
||||||
if not base_url:
|
if choice in {"2", "3", "4"}:
|
||||||
base_url = input("URL을 찾을 수 없습니다. 직접 입력하세요: ").strip()
|
base_url_input = input("기록 사이트 기본 URL을 입력하세요 (엔터 시 site.txt 자동 참조): ").strip()
|
||||||
if not base_url:
|
if base_url_input:
|
||||||
print("URL이 필요합니다.")
|
base_url = base_url_input
|
||||||
return 1
|
else:
|
||||||
|
try:
|
||||||
|
site_txt_path = Path("site.txt")
|
||||||
|
if site_txt_path.exists():
|
||||||
|
lines = site_txt_path.read_text(encoding="utf-8").splitlines()
|
||||||
|
if lines and lines[0].startswith("http"):
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
parsed = urlparse(lines[0])
|
||||||
|
base_url = f"{parsed.scheme}://{parsed.netloc}"
|
||||||
|
print(f"👉 [자동 설정] 기록 사이트 URL: {base_url}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"site.txt 파일을 읽는 중 오류가 발생했습니다: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
user_data_dir = input("크롬 프로필 경로를 입력하세요 (엔터 시 임시 세션): ").strip()
|
user_data_dir = input("크롬 프로필 경로를 입력하세요 (엔터 시 임시 세션): ").strip()
|
||||||
manager_game_no = input("관리자 사이트 게임번호를 입력하세요 (예: 11211, 모르면 엔터): ").strip()
|
manager_game_no = input("관리자 사이트 게임번호를 입력하세요 (예: 11211, 모르면 엔터): ").strip()
|
||||||
@@ -92,9 +96,10 @@ def interactive_mode() -> int:
|
|||||||
start_inning=None,
|
start_inning=None,
|
||||||
end_inning=None,
|
end_inning=None,
|
||||||
output_dir="output",
|
output_dir="output",
|
||||||
|
save=True, # for lineup
|
||||||
)
|
)
|
||||||
|
|
||||||
if choice in {"1", "3"}:
|
if choice in {"1", "4"}:
|
||||||
from crawler.report_builder import build_report, filter_report, save_report
|
from crawler.report_builder import build_report, filter_report, save_report
|
||||||
print(f"\n[{args.game_id}] 데이터 크롤링 시작...")
|
print(f"\n[{args.game_id}] 데이터 크롤링 시작...")
|
||||||
report = build_report(args.game_id)
|
report = build_report(args.game_id)
|
||||||
@@ -102,12 +107,19 @@ def interactive_mode() -> int:
|
|||||||
out_path = save_report(filtered, Path(args.output_dir))
|
out_path = save_report(filtered, Path(args.output_dir))
|
||||||
print(f"✅ 크롤링 및 리포트 저장 완료: {out_path}")
|
print(f"✅ 크롤링 및 리포트 저장 완료: {out_path}")
|
||||||
|
|
||||||
if choice in {"2", "3"}:
|
if choice in {"2", "3", "4"}:
|
||||||
report_path = Path(args.output_dir) / f"{args.game_id}_report.json"
|
report_path = Path(args.output_dir) / f"{args.game_id}_report.json"
|
||||||
report = load_report(report_path)
|
report = load_report(report_path)
|
||||||
print(f"\n[{args.game_id}] 관리자 사이트 자동 입력 시작...")
|
|
||||||
with sync_playwright() as playwright:
|
with sync_playwright() as playwright:
|
||||||
run_record(playwright, args, report)
|
if choice in {"2", "4"}:
|
||||||
|
from commands.lineup import run as run_lineup
|
||||||
|
run_lineup(playwright, args, report)
|
||||||
|
|
||||||
|
if choice in {"3", "4"}:
|
||||||
|
from commands.record import run as run_record
|
||||||
|
print(f"\n[{args.game_id}] 관리자 사이트 기록 자동 입력 시작...")
|
||||||
|
run_record(playwright, args, report)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
212
commands/lineup.py
Normal file
212
commands/lineup.py
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from playwright.sync_api import Page, Playwright
|
||||||
|
|
||||||
|
from core.config_loader import position_to_defense_no
|
||||||
|
from core.normalizer import normalize_stadium_name, normalize_team_name
|
||||||
|
from commands.base import launch_browser_context
|
||||||
|
|
||||||
|
def normalize_number_text(number: str | int | None) -> str:
|
||||||
|
text = str(number or "").strip()
|
||||||
|
digits = "".join(char for char in text if char.isdigit())
|
||||||
|
if not digits:
|
||||||
|
return ""
|
||||||
|
return str(int(digits))
|
||||||
|
|
||||||
|
def normalize_player_name_text(name: str | None) -> str:
|
||||||
|
text = (name or "").replace("*", "").strip()
|
||||||
|
text = re.sub(r"\([^)]*\)\s*$", "", text).strip()
|
||||||
|
return text
|
||||||
|
|
||||||
|
def normalize_option_player_text(text: str) -> tuple[str, str]:
|
||||||
|
stripped = " ".join(text.split())
|
||||||
|
matched = re.match(r"^(.*?)\s*\[(\d+)번\]$", stripped)
|
||||||
|
if matched:
|
||||||
|
return normalize_player_name_text(matched.group(1)), normalize_number_text(matched.group(2))
|
||||||
|
return normalize_player_name_text(stripped), ""
|
||||||
|
|
||||||
|
def infer_option_role_hint(text: str) -> str:
|
||||||
|
stripped = " ".join(text.split())
|
||||||
|
matched = re.search(r"\(([^)]*)\)\s*(?:\[\d+번\])?$", stripped)
|
||||||
|
if not matched:
|
||||||
|
return ""
|
||||||
|
hint = matched.group(1).strip()
|
||||||
|
if hint == "투":
|
||||||
|
return "pitcher"
|
||||||
|
if hint == "타":
|
||||||
|
return "batter"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def infer_target_role_hint(position_name: str | None) -> str:
|
||||||
|
if position_name == "투수":
|
||||||
|
return "pitcher"
|
||||||
|
return "batter"
|
||||||
|
|
||||||
|
def get_select_options(page: Page, selector: str) -> list[dict[str, str]]:
|
||||||
|
return page.locator(selector).evaluate(
|
||||||
|
"""(el) => [...el.options].map(option => ({
|
||||||
|
value: option.value,
|
||||||
|
text: option.textContent.trim()
|
||||||
|
}))"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def select_player_option(
|
||||||
|
page: Page,
|
||||||
|
selector: str,
|
||||||
|
player_name: str,
|
||||||
|
player_number: str | None,
|
||||||
|
position_name: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
options = get_select_options(page, selector)
|
||||||
|
target_number = normalize_number_text(player_number)
|
||||||
|
normalized_player_name = normalize_player_name_text(player_name)
|
||||||
|
target_role_hint = infer_target_role_hint(position_name)
|
||||||
|
name_matches = []
|
||||||
|
role_filtered_matches = []
|
||||||
|
|
||||||
|
for option in options:
|
||||||
|
option_name, option_number = normalize_option_player_text(option["text"])
|
||||||
|
if option_name != normalized_player_name:
|
||||||
|
continue
|
||||||
|
name_matches.append(option)
|
||||||
|
option_role_hint = infer_option_role_hint(option["text"])
|
||||||
|
role_matches = (
|
||||||
|
not target_role_hint
|
||||||
|
or not option_role_hint
|
||||||
|
or option_role_hint == target_role_hint
|
||||||
|
)
|
||||||
|
if role_matches:
|
||||||
|
role_filtered_matches.append(option)
|
||||||
|
if target_number and option_number == target_number:
|
||||||
|
if not option_role_hint or option_role_hint == target_role_hint:
|
||||||
|
page.locator(selector).select_option(value=option["value"])
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(role_filtered_matches) == 1:
|
||||||
|
page.locator(selector).select_option(value=role_filtered_matches[0]["value"])
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(name_matches) == 1:
|
||||||
|
page.locator(selector).select_option(value=name_matches[0]["value"])
|
||||||
|
return
|
||||||
|
|
||||||
|
normalized_options = [normalize_option_player_text(option["text"]) for option in options]
|
||||||
|
similar_options = [
|
||||||
|
option["text"]
|
||||||
|
for option, (option_name, option_number) in zip(options, normalized_options)
|
||||||
|
if normalized_player_name in option_name or option_name in normalized_player_name or (target_number and option_number == target_number)
|
||||||
|
]
|
||||||
|
if not similar_options:
|
||||||
|
similar_options = [option["text"] for option in options]
|
||||||
|
raise ValueError(
|
||||||
|
f"{selector}에서 선수 '{player_name}' #{player_number} 옵션을 찾지 못했습니다. "
|
||||||
|
f"후보: {', '.join(similar_options[:10])}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def select_position_option(page: Page, selector: str, position_name: str) -> None:
|
||||||
|
position_value = position_to_defense_no().get(position_name)
|
||||||
|
if not position_value:
|
||||||
|
raise ValueError(f"포지션 매핑이 없습니다: {position_name}")
|
||||||
|
page.locator(selector).select_option(value=position_value)
|
||||||
|
|
||||||
|
def build_lineup_entries(lineups: dict[str, Any], team_key: str) -> list[tuple[int, dict[str, Any]]]:
|
||||||
|
team_lineup = lineups[team_key]
|
||||||
|
entries = [(0, team_lineup["starter_pitcher"])]
|
||||||
|
entries.extend((int(player["bat_order"]), player) for player in team_lineup["players"])
|
||||||
|
return entries
|
||||||
|
|
||||||
|
def fill_lineup_form(page: Page, report: dict[str, Any]) -> None:
|
||||||
|
lineups = report["lineups"]
|
||||||
|
team_selector_map = {
|
||||||
|
"home_team": "home",
|
||||||
|
"away_team": "away",
|
||||||
|
}
|
||||||
|
|
||||||
|
for team_key, prefix in team_selector_map.items():
|
||||||
|
for order, player in build_lineup_entries(lineups, team_key):
|
||||||
|
if not player:
|
||||||
|
continue
|
||||||
|
player_selector = f"#{prefix}_player_id_{order}"
|
||||||
|
defense_selector = f"#{prefix}_defense_no_{order}"
|
||||||
|
select_player_option(page, player_selector, player["name"], player.get("number"), player.get("position"))
|
||||||
|
select_position_option(page, defense_selector, player["position"])
|
||||||
|
|
||||||
|
def find_edit_href(page: Page, report: dict[str, Any], manager_game_no: str | None) -> str:
|
||||||
|
game_info = report["game_info"]
|
||||||
|
target_date = game_info["date"]
|
||||||
|
target_stadium = normalize_stadium_name(game_info["stadium"])
|
||||||
|
target_home_team = normalize_team_name(game_info["home_team"])
|
||||||
|
target_away_team = normalize_team_name(game_info["away_team"])
|
||||||
|
|
||||||
|
rows: list[dict[str, Any]] = page.locator("table.gclist tr").evaluate_all(
|
||||||
|
"""(rows) => rows.slice(1).map((row) => {
|
||||||
|
const cells = [...row.cells].map((cell) => cell.innerText.trim());
|
||||||
|
const editLink = [...row.querySelectorAll('a')].find((anchor) => anchor.textContent.trim() === '수정');
|
||||||
|
return {
|
||||||
|
gameNo: cells[0] || '',
|
||||||
|
date: cells[1] || '',
|
||||||
|
gameType: cells[2] || '',
|
||||||
|
stadium: cells[3] || '',
|
||||||
|
homeTeam: cells[4] || '',
|
||||||
|
awayTeam: cells[5] || '',
|
||||||
|
href: editLink ? editLink.getAttribute('href') : '',
|
||||||
|
};
|
||||||
|
})"""
|
||||||
|
)
|
||||||
|
|
||||||
|
if manager_game_no:
|
||||||
|
matched = next((row for row in rows if row["gameNo"] == str(manager_game_no)), None)
|
||||||
|
if not matched or not matched["href"] or matched["href"].startswith("javascript:"):
|
||||||
|
raise ValueError(f"관리자 게임번호 {manager_game_no} 행의 수정 링크를 찾지 못했습니다.")
|
||||||
|
return matched["href"]
|
||||||
|
|
||||||
|
candidates = [
|
||||||
|
row for row in rows if row["href"] and not row["href"].startswith("javascript:")
|
||||||
|
and row["date"] == target_date
|
||||||
|
and normalize_stadium_name(row["stadium"]) == target_stadium
|
||||||
|
and normalize_team_name(row["homeTeam"]) == target_home_team
|
||||||
|
and normalize_team_name(row["awayTeam"]) == target_away_team
|
||||||
|
]
|
||||||
|
if not candidates:
|
||||||
|
raise ValueError("목록에서 일치하는 경기 수정 행을 찾지 못했습니다.")
|
||||||
|
return candidates[0]["href"]
|
||||||
|
|
||||||
|
def open_edit_page(page: Page, base_url: str, report: dict[str, Any], manager_game_no: str | None) -> None:
|
||||||
|
if manager_game_no:
|
||||||
|
page.goto(f"{base_url}/manager/game/write?id={manager_game_no}", wait_until="domcontentloaded")
|
||||||
|
page.wait_for_selector("#gameFrm", timeout=10000)
|
||||||
|
page.wait_for_selector("#home_player_id_1", timeout=10000)
|
||||||
|
return
|
||||||
|
|
||||||
|
page.goto(f"{base_url}/manager/game/list", wait_until="domcontentloaded")
|
||||||
|
page.wait_for_selector("table.gclist", timeout=10000)
|
||||||
|
edit_href = find_edit_href(page, report, manager_game_no)
|
||||||
|
with page.expect_navigation(wait_until="domcontentloaded"):
|
||||||
|
page.locator(f"a[href='{edit_href}']").first.click()
|
||||||
|
page.wait_for_selector("#gameFrm", timeout=10000)
|
||||||
|
page.wait_for_selector("#home_player_id_1", timeout=10000)
|
||||||
|
|
||||||
|
def run(playwright: Playwright, args: argparse.Namespace, report: dict[str, Any]) -> None:
|
||||||
|
browser = launch_browser_context(playwright, args.user_data_dir, args.channel, args.headless)
|
||||||
|
page = browser.pages[0] if browser.pages else browser.new_page()
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"[{args.game_id}] 관리자 사이트 라인업 입력 시작...")
|
||||||
|
open_edit_page(page, args.base_url, report, args.manager_game_no)
|
||||||
|
fill_lineup_form(page, report)
|
||||||
|
|
||||||
|
page.evaluate("""() => { window.confirm = () => true; window.alert = () => {}; }""")
|
||||||
|
page.locator("#lineupWriteBtn").click()
|
||||||
|
page.wait_for_timeout(1000)
|
||||||
|
print(f"✅ 라인업 저장 완료")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if args.close:
|
||||||
|
try:
|
||||||
|
browser.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
1432
refactoring.md
Normal file
1432
refactoring.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user