This commit is contained in:
윤정민 2026-04-13 09:18:30 +09:00
parent fe0ae5b372
commit b58479bfe7
17 changed files with 1811 additions and 156 deletions

View File

@ -19,6 +19,9 @@ module.exports = {
}, },
ignorePatterns: [ ignorePatterns: [
"/lib/**/*", // Ignore built files. "/lib/**/*", // Ignore built files.
"/dist/**/*", // Ignore compiled output.
"/tests/**/*",
"vitest.config.ts",
"/generated/**/*", // Ignore generated files. "/generated/**/*", // Ignore generated files.
], ],
plugins: [ plugins: [
@ -26,8 +29,13 @@ module.exports = {
"import", "import",
], ],
rules: { rules: {
"quotes": ["error", "double"], quotes: ["error", "double"],
"import/no-unresolved": 0, "import/no-unresolved": 0,
"indent": ["error", 2], indent: ["error", 2],
"require-jsdoc": "off",
"valid-jsdoc": "off",
"max-len": ["error", { code: 120 }],
"object-curly-spacing": ["error", "always"],
"quote-props": ["error", "as-needed"],
}, },
}; };

View File

@ -1,7 +1,5 @@
{ {
"projects": { "projects": {
"default": "mmday-7900f" "default": "mmday-panit"
}, }
"targets": {},
"etags": {}
} }

View File

@ -1,6 +1,23 @@
{ {
"rules": { "rules": {
"votes": {
"$gameId": {
".read": "auth != null", ".read": "auth != null",
".write": "auth != null" "counts": {
".read": true
},
".write": false
}
},
"userVotes": {
"$uid": {
".read": "auth != null && auth.uid == $uid",
".write": false
}
},
"cache": {
".read": false,
".write": false
}
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"database": { "database": {
"rules": "y" "rules": "database.rules.json"
}, },
"firestore": { "firestore": {
"database": "(default)", "database": "(default)",
@ -21,7 +21,6 @@
"*.local" "*.local"
], ],
"predeploy": [ "predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint",
"npm --prefix \"$RESOURCE_DIR\" run build" "npm --prefix \"$RESOURCE_DIR\" run build"
] ]
} }
@ -44,5 +43,35 @@
}, },
"auth": { "auth": {
"providers": {} "providers": {}
},
"emulators": {
"auth": {
"host": "0.0.0.0",
"port": 9099
},
"functions": {
"host": "0.0.0.0",
"port": 5001
},
"firestore": {
"host": "0.0.0.0",
"port": 8080
},
"database": {
"host": "0.0.0.0",
"port": 9000
},
"storage": {
"host": "0.0.0.0",
"port": 9199
},
"ui": {
"enabled": true,
"host": "0.0.0.0"
},
"singleProjectMode": true
},
"remoteconfig": {
"template": "remoteconfig.template.json"
} }
} }

View File

@ -1,4 +1,51 @@
{ {
// Example (Standard Edition):
//
// "indexes": [
// {
// "collectionGroup": "widgets",
// "queryScope": "COLLECTION",
// "fields": [
// { "fieldPath": "foo", "arrayConfig": "CONTAINS" },
// { "fieldPath": "bar", "mode": "DESCENDING" }
// ]
// },
//
// "fieldOverrides": [
// {
// "collectionGroup": "widgets",
// "fieldPath": "baz",
// "indexes": [
// { "order": "ASCENDING", "queryScope": "COLLECTION" }
// ]
// },
// ]
// ]
//
// Example (Enterprise Edition):
//
// "indexes": [
// {
// "collectionGroup": "reviews",
// "queryScope": "COLLECTION_GROUP",
// "apiScope": "MONGODB_COMPATIBLE_API",
// "density": "DENSE",
// "multikey": false,
// "fields": [
// { "fieldPath": "baz", "mode": "ASCENDING" }
// ]
// },
// {
// "collectionGroup": "items",
// "queryScope": "COLLECTION_GROUP",
// "apiScope": "MONGODB_COMPATIBLE_API",
// "density": "SPARSE_ANY",
// "multikey": true,
// "fields": [
// { "fieldPath": "baz", "mode": "ASCENDING" }
// ]
// },
// ]
"indexes": [], "indexes": [],
"fieldOverrides": [] "fieldOverrides": []
} }

View File

@ -1,19 +1,33 @@
rules_version = '2'; rules_version='2'
service cloud.firestore { service cloud.firestore {
match /databases/{database}/documents { match /databases/{database}/documents {
match /users/{uid} {
allow read: if request.auth != null && request.auth.uid == uid;
allow write: if false;
match /voteHistory/{date} {
allow read: if request.auth != null && request.auth.uid == uid;
allow write: if false;
}
}
match /games/{gameId} {
allow read: if true;
allow write: if false;
}
match /kboCache/{key} {
allow read: if true;
allow write: if false;
}
match /kboLocks/{key} {
allow read, write: if false;
}
// This rule allows anyone with your Firestore database reference to view, edit,
// and delete all data in your Firestore database. It is useful for getting
// started, but it is configured to expire after 30 days because it
// leaves your app open to attackers. At that time, all client
// requests to your Firestore database will be denied.
//
// Make sure to write security rules for your app before that time, or else
// all client requests to your Firestore database will be denied until you Update
// your rules
match /{document=**} { match /{document=**} {
allow read, write: if request.time < timestamp.date(2026, 5, 2); allow read, write: if false;
} }
} }
} }

1553
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,11 +4,13 @@
"lint": "eslint --ext .js,.ts .", "lint": "eslint --ext .js,.ts .",
"build": "tsc", "build": "tsc",
"build:watch": "tsc --watch", "build:watch": "tsc --watch",
"serve": "npm run build && firebase emulators:start --only functions", "serve": "npm run build && firebase emulators:start --only auth,functions,firestore,database,storage",
"shell": "npm run build && firebase functions:shell", "shell": "npm run build && firebase functions:shell",
"start": "npm run shell", "start": "npm run shell",
"deploy": "firebase deploy --only functions", "deploy": "firebase deploy --only functions",
"logs": "firebase functions:log" "logs": "firebase functions:log",
"test": "firebase emulators:exec --only firestore,database,auth \"vitest run\"",
"test:watch": "vitest"
}, },
"engines": { "engines": {
"node": "24" "node": "24"
@ -25,7 +27,8 @@
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4", "eslint-plugin-import": "^2.25.4",
"firebase-functions-test": "^3.4.1", "firebase-functions-test": "^3.4.1",
"typescript": "^5.7.3" "typescript": "^5.7.3",
"vitest": "^4.1.4"
}, },
"private": true "private": true
} }

View File

@ -1,32 +1,12 @@
/**
* Import function triggers from their respective submodules:
*
* import {onCall} from "firebase-functions/v2/https";
* import {onDocumentWritten} from "firebase-functions/v2/firestore";
*
* See a full list of supported triggers at https://firebase.google.com/docs/functions
*/
import { setGlobalOptions } from "firebase-functions"; import { setGlobalOptions } from "firebase-functions";
import {onRequest} from "firebase-functions/https"; import "./firebase";
import * as logger from "firebase-functions/logger";
// Start writing functions setGlobalOptions({ maxInstances: 10, region: "asia-northeast3" });
// https://firebase.google.com/docs/functions/typescript
// For cost control, you can set the maximum number of containers that can be export { kbo } from "./handlers/kboHandlers";
// running at the same time. This helps mitigate the impact of unexpected export { auth } from "./handlers/authHandlers";
// traffic spikes by instead downgrading performance. This limit is a export { prediction } from "./handlers/predictionHandlers";
// per-function limit. You can override the limit for each function using the export { stats } from "./handlers/statsHandlers";
// `maxInstances` option in the function's options, e.g. export { admin } from "./handlers/adminHandlers";
// `onRequest({ maxInstances: 5 }, (req, res) => { ... })`. export { kboDailyRefresh } from "./scheduled/kboRefresh";
// NOTE: setGlobalOptions does not apply to functions using the v1 API. V1 export { dailyArchive } from "./scheduled/dailyArchive";
// functions should each use functions.runWith({ maxInstances: 10 }) instead.
// In the v1 API, each function can only serve one request per container, so
// this will be the maximum concurrent request count.
setGlobalOptions({ maxInstances: 10 });
// export const helloWorld = onRequest((request, response) => {
// logger.info("Hello logs!", {structuredData: true});
// response.send("Hello from Firebase!");
// });

View File

@ -5,11 +5,13 @@
* Commands: * Commands:
* rank [year...] * rank [year...]
* player <hitter|pitcher|defense|runner> * player <hitter|pitcher|defense|runner>
* schedule [year] [month] /
* *
* Options: * Options:
* --json JSON * --json JSON
* --all (player) * --all (player)
* --team= (e.g. --team=LG) * --team= (e.g. --team=LG)
* --series= (schedule: 정규//)
*/ */
import { padCell } from "./html-utils.js"; import { padCell } from "./html-utils.js";
@ -34,6 +36,12 @@ import { HITTER_CONFIG } from "./player/hitter.js";
import { PITCHER_CONFIG } from "./player/pitcher.js"; import { PITCHER_CONFIG } from "./player/pitcher.js";
import { DEFENSE_CONFIG } from "./player/defense.js"; import { DEFENSE_CONFIG } from "./player/defense.js";
import { RUNNER_CONFIG } from "./player/runner.js"; import { RUNNER_CONFIG } from "./player/runner.js";
import {
fetchSchedule,
SERIES_CODES,
type ScheduleGame,
type ScheduleResult,
} from "./schedule.js";
const PLAYER_CONFIGS: Record<string, PlayerPageConfig> = { const PLAYER_CONFIGS: Record<string, PlayerPageConfig> = {
hitter: HITTER_CONFIG, hitter: HITTER_CONFIG,
@ -118,9 +126,9 @@ function printPlayerTable(
const filterDesc = Object.entries(result.filters) const filterDesc = Object.entries(result.filters)
.map(([k, v]) => `${k}=${v}`) .map(([k, v]) => `${k}=${v}`)
.join(", "); .join(", ");
const label = filterDesc const label = filterDesc ?
? `KBO ${result.year} ${subcommand} (${filterDesc})` `KBO ${result.year} ${subcommand} (${filterDesc})` :
: `KBO ${result.year} ${subcommand}`; `KBO ${result.year} ${subcommand}`;
console.log(`\n${"═".repeat(100)}`); console.log(`\n${"═".repeat(100)}`);
console.log(` ${label}`); console.log(` ${label}`);
@ -143,7 +151,7 @@ function printPlayerTable(
console.log(" " + widths.map((w) => "-".repeat(w)).join("-+-")); console.log(" " + widths.map((w) => "-".repeat(w)).join("-+-"));
for (const rec of result.records) { for (const rec of result.records) {
const row = columns.map((c) => rec[c] ?? ""); const row = columns.map((c) => String(rec[c] ?? ""));
console.log( console.log(
" " + row.map((val, i) => padCell(val, widths[i])).join(" | ") " " + row.map((val, i) => padCell(val, widths[i])).join(" | ")
); );
@ -152,8 +160,89 @@ function printPlayerTable(
console.log(`\n Total: ${result.records.length} players`); console.log(`\n Total: ${result.records.length} players`);
} }
// ── Schedule Formatting ──
function fmtScore(game: ScheduleGame): string {
if (game.status === "completed") {
const away = String(game.awayScore ?? 0).padStart(2);
const home = String(game.homeScore ?? 0).padStart(2);
return `${away} vs ${home}`;
}
if (game.status === "cancelled") {
return " 취소 ";
}
return " vs ";
}
function printScheduleTable(result: ScheduleResult) {
const label = `KBO ${result.year}-${String(result.month).padStart(2, "0")} Schedule`;
console.log(`\n${"═".repeat(75)}`);
console.log(` ${label}`);
console.log(`${"═".repeat(75)}`);
if (result.games.length === 0) {
console.log(" No data found.");
return;
}
let lastDate = "";
for (const game of result.games) {
const dateKey = `${game.date}(${game.dayOfWeek})`;
if (dateKey !== lastDate) {
if (lastDate) console.log(` ${"─".repeat(71)}`);
console.log(` ${dateKey}`);
lastDate = dateKey;
}
const time = padCell(game.time, 5);
const away = padCell(game.awayTeamCode, 4);
const home = padCell(game.homeTeamCode, 4);
const score = fmtScore(game);
const stadium = padCell(game.stadium, 4);
const broadcast = game.broadcast || "";
let line = ` ${time} ${away} ${score} ${home} ${stadium}`;
if (broadcast) line += ` ${broadcast}`;
if (game.note && game.note !== "-") line += ` ${game.note}`;
console.log(line);
}
}
// ── Commands ── // ── Commands ──
async function scheduleCommand(
year: number,
month: number,
jsonMode: boolean,
team?: string,
series?: string
) {
const monthStr = String(month).padStart(2, "0");
console.log(`Fetching KBO schedule for ${year}-${monthStr}...\n`);
const resolvedTeam = team ? (TEAM_CODES[team] ?? team) : undefined;
const resolvedSeries = series ? (SERIES_CODES[series] ?? series) : undefined;
const result = await fetchSchedule({
year,
month,
series: resolvedSeries,
team: resolvedTeam,
});
console.log(` ${result.games.length} games found`);
if (jsonMode) {
console.log(JSON.stringify(result, null, 2));
} else {
printScheduleTable(result);
}
}
async function rankCommand(years: number[], jsonMode: boolean) { async function rankCommand(years: number[], jsonMode: boolean) {
console.log("Fetching KBO team rankings...\n"); console.log("Fetching KBO team rankings...\n");
@ -236,6 +325,7 @@ function printHelp() {
console.log("Commands:"); console.log("Commands:");
console.log(" rank [year...] Team rankings"); console.log(" rank [year...] Team rankings");
console.log(" player <hitter|pitcher|defense|runner> Player stats"); console.log(" player <hitter|pitcher|defense|runner> Player stats");
console.log(" schedule [year] [month] 경기 일정/결과");
console.log(""); console.log("");
console.log("Options:"); console.log("Options:");
console.log(" --json JSON output"); console.log(" --json JSON output");
@ -247,6 +337,10 @@ function printHelp() {
console.log(" --pos=값 포지션 (2=포수, 3,4,5,6=내야수, 7,8,9=외야수)"); console.log(" --pos=값 포지션 (2=포수, 3,4,5,6=내야수, 7,8,9=외야수)");
console.log(" --situation=값 상황별 (MONTH_SC, WEEK_SC, STADIUM_SC, HOMEAYAY_SC, ...)"); console.log(" --situation=값 상황별 (MONTH_SC, WEEK_SC, STADIUM_SC, HOMEAYAY_SC, ...)");
console.log(" --situationDetail=값"); console.log(" --situationDetail=값");
console.log("");
console.log("Schedule filters:");
console.log(" --team=팀명 팀 (LG, 삼성, KT, ...)");
console.log(" --series=값 시리즈 (정규, 시범, 포스트)");
} }
async function main() { async function main() {
@ -317,6 +411,39 @@ async function main() {
return; return;
} }
if (command === "schedule") {
const rest = args.slice(1);
let jsonMode = false;
const now = new Date();
let year = now.getFullYear();
let month = now.getMonth() + 1;
let schedTeam: string | undefined;
let schedSeries: string | undefined;
for (const arg of rest) {
if (arg === "--json") {
jsonMode = true;
} else if (arg.startsWith("--team=")) {
schedTeam = arg.slice("--team=".length);
} else if (arg.startsWith("--series=")) {
schedSeries = arg.slice("--series=".length);
} else {
const n = parseInt(arg, 10);
if (n >= 1 && n <= 12) {
month = n;
} else if (n >= 1982 && n <= 2100) {
year = n;
} else {
console.error(`Invalid argument: ${arg}`);
process.exit(1);
}
}
}
await scheduleCommand(year, month, jsonMode, schedTeam, schedSeries);
return;
}
// rank 및 기타 커맨드 // rank 및 기타 커맨드
const rest = args.slice(1); const rest = args.slice(1);
let jsonMode = false; let jsonMode = false;

View File

@ -16,7 +16,7 @@ export function decodeHtmlEntities(text: string): string {
.replace(/&amp;/g, "&") .replace(/&amp;/g, "&")
.replace(/&lt;/g, "<") .replace(/&lt;/g, "<")
.replace(/&gt;/g, ">") .replace(/&gt;/g, ">")
.replace(/&quot;/g, '"') .replace(/&quot;/g, "\"")
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n))); .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
} }

View File

@ -43,7 +43,19 @@ export interface PlayerPageConfig {
* const hitter = record as HitterStats; * const hitter = record as HitterStats;
* console.log(hitter.avg, hitter.hr); * console.log(hitter.avg, hitter.hr);
*/ */
export type PlayerRecord = Record<string, string>; export type PlayerValue = string | number | null;
export type PlayerRecord = Record<string, PlayerValue>;
/** 숫자로 변환하지 않고 문자열 그대로 유지할 컬럼 */
const STRING_COLUMNS: ReadonlySet<string> = new Set(["player", "team", "pos"]);
function parsePlayerCell(col: string, text: string): PlayerValue {
if (STRING_COLUMNS.has(col)) return text;
const trimmed = text.trim();
if (trimmed === "" || trimmed === "-") return null;
const n = Number(trimmed);
return Number.isNaN(n) ? trimmed : n;
}
/** /**
* . * .
@ -95,7 +107,7 @@ export const TEAM_CODES: Record<string, string> = {
* *
* @param html - `<tr>`, `<td>` HTML * @param html - `<tr>`, `<td>` HTML
* @param columns - `<td>` ( ) * @param columns - `<td>` ( )
* @returns `{ [컬럼명]: 값 }` . * @return `{ [컬럼명]: 값 }` .
* `<td>` () . * `<td>` () .
*/ */
export function parsePlayerTable( export function parsePlayerTable(
@ -121,7 +133,7 @@ export function parsePlayerTable(
if (/^\d+$/.test(rank)) { if (/^\d+$/.test(rank)) {
const record: PlayerRecord = {}; const record: PlayerRecord = {};
for (let i = 0; i < columns.length; i++) { for (let i = 0; i < columns.length; i++) {
record[columns[i]] = tds[i]; record[columns[i]] = parsePlayerCell(columns[i], tds[i]);
} }
records.push(record); records.push(record);
} }
@ -157,7 +169,7 @@ const DDL_DEFAULTS: Record<string, string> = {
* @param config - (dropdowns, defaultSortCol ) * @param config - (dropdowns, defaultSortCol )
* @param year - * @param year -
* @param filters - (team, series, pos ) * @param filters - (team, series, pos )
* @returns `URLSearchParams` - * @return `URLSearchParams` -
*/ */
function buildFormFields( function buildFormFields(
config: PlayerPageConfig, config: PlayerPageConfig,
@ -200,7 +212,7 @@ function buildFormFields(
* postback에 ASP.NET (ViewState, ) . * postback에 ASP.NET (ViewState, ) .
* *
* @param config - (url, columns ) * @param config - (url, columns )
* @returns (result) (state) * @return (result) (state)
*/ */
export async function fetchPlayerStatsInitial( export async function fetchPlayerStatsInitial(
config: PlayerPageConfig config: PlayerPageConfig
@ -210,9 +222,9 @@ export async function fetchPlayerStatsInitial(
const yearMatch = html.match( const yearMatch = html.match(
/ddlSeason_ddlSeason[\s\S]*?selected="selected"\s+value="(\d{4})"/ /ddlSeason_ddlSeason[\s\S]*?selected="selected"\s+value="(\d{4})"/
); );
const year = yearMatch const year = yearMatch ?
? parseInt(yearMatch[1], 10) parseInt(yearMatch[1], 10) :
: new Date().getFullYear(); new Date().getFullYear();
return { return {
result: { result: {
@ -239,7 +251,7 @@ export async function fetchPlayerStatsInitial(
* @param year - * @param year -
* @param state - ASP.NET * @param state - ASP.NET
* @param filters - (team, series, pos, situation ) * @param filters - (team, series, pos, situation )
* @returns (result) (newState) * @return (result) (newState)
*/ */
export async function fetchPlayerStats( export async function fetchPlayerStats(
config: PlayerPageConfig, config: PlayerPageConfig,
@ -252,9 +264,9 @@ export async function fetchPlayerStats(
const useUrl = const useUrl =
isPostseason && config.postseasonUrl ? config.postseasonUrl : config.url; isPostseason && config.postseasonUrl ? config.postseasonUrl : config.url;
const useColumns = const useColumns =
isPostseason && config.postseasonColumns isPostseason && config.postseasonColumns ?
? config.postseasonColumns config.postseasonColumns :
: config.columns; config.columns;
// 포스트시즌은 별도 페이지이므로 해당 페이지의 초기 상태에서 시작 // 포스트시즌은 별도 페이지이므로 해당 페이지의 초기 상태에서 시작
if (isPostseason && config.postseasonUrl) { if (isPostseason && config.postseasonUrl) {
@ -373,7 +385,7 @@ export async function fetchPlayerStats(
* @param year - * @param year -
* @param state - ASP.NET * @param state - ASP.NET
* @param filters - * @param filters -
* @returns * @return
*/ */
export async function fetchAllPlayerStats( export async function fetchAllPlayerStats(
config: PlayerPageConfig, config: PlayerPageConfig,
@ -388,6 +400,7 @@ export async function fetchAllPlayerStats(
let currentState = firstState; let currentState = firstState;
let page = 2; let page = 2;
// eslint-disable-next-line no-constant-condition
while (true) { while (true) {
const pagerTarget = `${PREFIX}ucPager$btnNo${page}`; const pagerTarget = `${PREFIX}ucPager$btnNo${page}`;
const fields = buildFormFields(config, year, filters); const fields = buildFormFields(config, year, filters);

View File

@ -1,4 +1,4 @@
import type { PlayerPageConfig } from "./common.js"; import type { PlayerPageConfig, PlayerValue } from "./common.js";
export const DEFENSE_COLUMNS = [ export const DEFENSE_COLUMNS = [
"rank", "player", "team", "pos", "games", "gs", "ip", "rank", "player", "team", "pos", "games", "gs", "ip",
@ -6,7 +6,7 @@ export const DEFENSE_COLUMNS = [
"sb", "cs", "csPct", "sb", "cs", "csPct",
] as const; ] as const;
export type DefenseStats = Record<(typeof DEFENSE_COLUMNS)[number], string>; export type DefenseStats = Record<(typeof DEFENSE_COLUMNS)[number], PlayerValue>;
export const DEFENSE_CONFIG: PlayerPageConfig = { export const DEFENSE_CONFIG: PlayerPageConfig = {
url: "https://www.koreabaseball.com/Record/Player/Defense/Basic.aspx", url: "https://www.koreabaseball.com/Record/Player/Defense/Basic.aspx",

View File

@ -1,4 +1,4 @@
import type { PlayerPageConfig } from "./common.js"; import type { PlayerPageConfig, PlayerValue } from "./common.js";
export const HITTER_COLUMNS = [ export const HITTER_COLUMNS = [
"rank", "player", "team", "avg", "games", "pa", "ab", "rank", "player", "team", "avg", "games", "pa", "ab",
@ -11,7 +11,7 @@ export const HITTER_POSTSEASON_COLUMNS = [
"bb", "hbp", "so", "gdp", "errors", "bb", "hbp", "so", "gdp", "errors",
] as const; ] as const;
export type HitterStats = Record<(typeof HITTER_COLUMNS)[number], string>; export type HitterStats = Record<(typeof HITTER_COLUMNS)[number], PlayerValue>;
export const HITTER_CONFIG: PlayerPageConfig = { export const HITTER_CONFIG: PlayerPageConfig = {
url: "https://www.koreabaseball.com/Record/Player/HitterBasic/Basic1.aspx", url: "https://www.koreabaseball.com/Record/Player/HitterBasic/Basic1.aspx",

View File

@ -1,4 +1,4 @@
import type { PlayerPageConfig } from "./common.js"; import type { PlayerPageConfig, PlayerValue } from "./common.js";
export const PITCHER_COLUMNS = [ export const PITCHER_COLUMNS = [
"rank", "player", "team", "era", "games", "wins", "losses", "rank", "player", "team", "era", "games", "wins", "losses",
@ -12,7 +12,7 @@ export const PITCHER_POSTSEASON_COLUMNS = [
"hits", "hr", "bb", "hbp", "so", "runs", "er", "hits", "hr", "bb", "hbp", "so", "runs", "er",
] as const; ] as const;
export type PitcherStats = Record<(typeof PITCHER_COLUMNS)[number], string>; export type PitcherStats = Record<(typeof PITCHER_COLUMNS)[number], PlayerValue>;
export const PITCHER_CONFIG: PlayerPageConfig = { export const PITCHER_CONFIG: PlayerPageConfig = {
url: "https://www.koreabaseball.com/Record/Player/PitcherBasic/Basic1.aspx", url: "https://www.koreabaseball.com/Record/Player/PitcherBasic/Basic1.aspx",

View File

@ -1,11 +1,11 @@
import type { PlayerPageConfig } from "./common.js"; import type { PlayerPageConfig, PlayerValue } from "./common.js";
export const RUNNER_COLUMNS = [ export const RUNNER_COLUMNS = [
"rank", "player", "team", "games", "sba", "sb", "cs", "rank", "player", "team", "games", "sba", "sb", "cs",
"sbPct", "oob", "pko", "sbPct", "oob", "pko",
] as const; ] as const;
export type RunnerStats = Record<(typeof RUNNER_COLUMNS)[number], string>; export type RunnerStats = Record<(typeof RUNNER_COLUMNS)[number], PlayerValue>;
export const RUNNER_CONFIG: PlayerPageConfig = { export const RUNNER_CONFIG: PlayerPageConfig = {
url: "https://www.koreabaseball.com/Record/Player/Runner/Basic.aspx", url: "https://www.koreabaseball.com/Record/Player/Runner/Basic.aspx",

View File

@ -8,7 +8,7 @@
"outDir": "lib", "outDir": "lib",
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
"target": "es2017" "target": "es2017",
}, },
"compileOnSave": true, "compileOnSave": true,
"include": [ "include": [