/** * KBO CLI * Usage: npx tsx src/kbo/cli.ts [options] * * Commands: * rank [year...] 팀 순위 * player 선수 기록 * * Options: * --json JSON 출력 * --all 모든 페이지 조회 (player) * --team=팀명 팀 필터 (e.g. --team=LG) */ import { padCell } from "./html-utils.js"; import { fetchTeamRankInitial, fetchTeamRank, type TeamRank, type TeamVsRecord, type TeamRankResult, type WinLossDraw, } from "./team-rank.js"; import { fetchPlayerStatsInitial, fetchPlayerStats, fetchAllPlayerStats, TEAM_CODES, type PlayerPageConfig, type PlayerFilters, type PlayerStatsResult, } from "./player/common.js"; import { HITTER_CONFIG } from "./player/hitter.js"; import { PITCHER_CONFIG } from "./player/pitcher.js"; import { DEFENSE_CONFIG } from "./player/defense.js"; import { RUNNER_CONFIG } from "./player/runner.js"; const PLAYER_CONFIGS: Record = { hitter: HITTER_CONFIG, pitcher: PITCHER_CONFIG, defense: DEFENSE_CONFIG, runner: RUNNER_CONFIG, }; // ── Rank Formatting ── function fmtWLD(wld: WinLossDraw): string { return `${wld[0]}-${wld[1]}-${wld[2]}`; } function printRankTable(year: number, teams: TeamRank[]) { console.log(`\n${"═".repeat(90)}`); console.log(` KBO ${year} Team Rankings`); console.log(`${"═".repeat(90)}`); if (teams.length === 0) { console.log(" No data found."); return; } const header = ["#", "Team", "G", "W", "L", "D", "PCT", "GB", "L10", "STR"]; const widths = [4, 6, 4, 3, 3, 3, 6, 6, 12, 8]; console.log(" " + header.map((h, i) => padCell(h, widths[i])).join(" | ")); console.log(" " + widths.map((w) => "-".repeat(w)).join("-+-")); for (const t of teams) { const row = [ t.rank, t.team, t.games, t.wins, t.losses, t.draws, t.winRate, t.gamesBehind, t.last10, t.streak, ]; console.log(" " + row.map((val, i) => padCell(val, widths[i])).join(" | ")); } } function printVsTable(year: number, vsRecords: TeamVsRecord[]) { if (vsRecords.length === 0) return; console.log(`\n${"─".repeat(90)}`); console.log(` KBO ${year} Head-to-Head (W-L-D)`); console.log(`${"─".repeat(90)}`); const teamNames = vsRecords.map((r) => r.team); const colW = 7; console.log( " " + padCell("", 6) + " | " + teamNames.map((n) => padCell(n, colW)).join("| ") + "| " + padCell("Total", colW) ); console.log( " " + "-".repeat(6) + "-+-" + teamNames.map(() => "-".repeat(colW)).join("+-") + "+-" + "-".repeat(colW) ); for (const rec of vsRecords) { const cells = teamNames.map((name) => { if (name === rec.team) return padCell(" -", colW); const wld = rec.headToHead[name]; return padCell(wld ? fmtWLD(wld) : "-", colW); }); console.log( " " + padCell(rec.team, 6) + " | " + cells.join("| ") + "| " + padCell(fmtWLD(rec.total), colW) ); } } // ── Player Formatting ── function printPlayerTable( subcommand: string, columns: readonly string[], result: PlayerStatsResult ) { const filterDesc = Object.entries(result.filters) .map(([k, v]) => `${k}=${v}`) .join(", "); const label = filterDesc ? `KBO ${result.year} ${subcommand} (${filterDesc})` : `KBO ${result.year} ${subcommand}`; console.log(`\n${"═".repeat(100)}`); console.log(` ${label}`); console.log(`${"═".repeat(100)}`); if (result.records.length === 0) { console.log(" No data found."); return; } const widths = columns.map((col) => { if (col === "player") return 10; if (col === "team") return 6; return Math.max(col.length, 4); }); console.log( " " + columns.map((c, i) => padCell(c, widths[i])).join(" | ") ); console.log(" " + widths.map((w) => "-".repeat(w)).join("-+-")); for (const rec of result.records) { const row = columns.map((c) => rec[c] ?? ""); console.log( " " + row.map((val, i) => padCell(val, widths[i])).join(" | ") ); } console.log(`\n Total: ${result.records.length} players`); } // ── Commands ── async function rankCommand(years: number[], jsonMode: boolean) { console.log("Fetching KBO team rankings...\n"); const { result: initial, state: initState, currentYear } = await fetchTeamRankInitial(); let state = initState; const results: TeamRankResult[] = []; for (const year of years) { process.stdout.write(` ${year}...`); if (year === currentYear && results.length === 0) { results.push(initial); console.log(` ${initial.teams.length} teams (initial page)`); } else { const { result, newState } = await fetchTeamRank(year, state, currentYear); state = newState; results.push(result); console.log(` ${result.teams.length} teams`); } } if (jsonMode) { console.log(JSON.stringify(results, null, 2)); } else { for (const { year, teams, vsRecords } of results) { printRankTable(year, teams); printVsTable(year, vsRecords); } } } async function playerCommand( subcommand: string, config: PlayerPageConfig, years: number[], jsonMode: boolean, filters: PlayerFilters, allPages: boolean ) { const filterDesc = Object.entries(filters) .map(([k, v]) => `${k}=${v}`) .join(", "); console.log(`Fetching KBO ${subcommand} stats...\n`); // team 필터는 팀명 → 팀코드 변환 const resolved: PlayerFilters = { ...filters }; if (resolved.team) { resolved.team = TEAM_CODES[resolved.team] ?? resolved.team; } const { state: initState } = await fetchPlayerStatsInitial(config); let state = initState; const results: PlayerStatsResult[] = []; for (const year of years) { process.stdout.write(` ${year}${filterDesc ? ` (${filterDesc})` : ""}...`); const fetchFn = allPages ? fetchAllPlayerStats : fetchPlayerStats; const { result, newState } = await fetchFn(config, year, state, resolved); state = newState; results.push(result); console.log(` ${result.records.length} players`); } if (jsonMode) { console.log(JSON.stringify(results, null, 2)); } else { for (const result of results) { printPlayerTable(subcommand, result.columns, result); } } } // ── Main ── function printHelp() { console.log("Usage: npx tsx src/kbo/cli.ts [options]"); console.log(""); console.log("Commands:"); console.log(" rank [year...] Team rankings"); console.log(" player Player stats"); console.log(""); console.log("Options:"); console.log(" --json JSON output"); console.log(" --all Fetch all pages (player)"); console.log(""); console.log("Player filters:"); console.log(" --team=팀명 팀 (LG, 삼성, KT, ...)"); console.log(" --series=값 시리즈 (0=정규, 1=시범, 3=준PO, 4=와카, 5=PO, 7=한국시리즈)"); console.log(" --pos=값 포지션 (2=포수, 3,4,5,6=내야수, 7,8,9=외야수)"); console.log(" --situation=값 상황별 (MONTH_SC, WEEK_SC, STADIUM_SC, HOMEAYAY_SC, ...)"); console.log(" --situationDetail=값"); } async function main() { const args = process.argv.slice(2); const command = args[0]; if (!command || command === "help" || command === "--help") { printHelp(); return; } if (command === "player") { const subcommand = args[1]; if (!subcommand || !(subcommand in PLAYER_CONFIGS)) { console.error( `Usage: player <${Object.keys(PLAYER_CONFIGS).join("|")}> [year] [options]` ); process.exit(1); } const rest = args.slice(2); let jsonMode = false; let allPages = false; const filters: PlayerFilters = {}; const years: number[] = []; const filterKeys: (keyof PlayerFilters)[] = ["team", "series", "pos", "situation", "situationDetail"]; for (const arg of rest) { if (arg === "--json") { jsonMode = true; } else if (arg === "--all") { allPages = true; } else if (arg.startsWith("--")) { const eqIdx = arg.indexOf("="); if (eqIdx === -1) { console.error(`Invalid option: ${arg} (expected --key=value)`); process.exit(1); } const key = arg.slice(2, eqIdx) as keyof PlayerFilters; const value = arg.slice(eqIdx + 1); if (filterKeys.includes(key)) { filters[key] = value; } else { console.error(`Unknown filter: --${key}`); process.exit(1); } } else { const y = parseInt(arg, 10); if (y >= 1982 && y <= 2100) years.push(y); else { console.error(`Invalid year: ${arg} (1982~2100)`); process.exit(1); } } } if (years.length === 0) years.push(new Date().getFullYear()); await playerCommand( subcommand, PLAYER_CONFIGS[subcommand], years, jsonMode, filters, allPages ); return; } // rank 및 기타 커맨드 const rest = args.slice(1); let jsonMode = false; const years: number[] = []; for (const arg of rest) { if (arg === "--json") jsonMode = true; else { const y = parseInt(arg, 10); if (y >= 1982 && y <= 2100) years.push(y); else { console.error(`Invalid year: ${arg} (1982~2100)`); process.exit(1); } } } if (years.length === 0) years.push(new Date().getFullYear()); switch (command) { case "rank": await rankCommand(years, jsonMode); break; default: console.error(`Unknown command: ${command}`); process.exit(1); } } main().catch((err) => { console.error("Error:", err); process.exit(1); });