init
This commit is contained in:
parent
fe0ae5b372
commit
b58479bfe7
12
.eslintrc.js
12
.eslintrc.js
@ -19,6 +19,9 @@ module.exports = {
|
||||
},
|
||||
ignorePatterns: [
|
||||
"/lib/**/*", // Ignore built files.
|
||||
"/dist/**/*", // Ignore compiled output.
|
||||
"/tests/**/*",
|
||||
"vitest.config.ts",
|
||||
"/generated/**/*", // Ignore generated files.
|
||||
],
|
||||
plugins: [
|
||||
@ -26,8 +29,13 @@ module.exports = {
|
||||
"import",
|
||||
],
|
||||
rules: {
|
||||
"quotes": ["error", "double"],
|
||||
quotes: ["error", "double"],
|
||||
"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"],
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
{
|
||||
"projects": {
|
||||
"default": "mmday-7900f"
|
||||
},
|
||||
"targets": {},
|
||||
"etags": {}
|
||||
"default": "mmday-panit"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,23 @@
|
||||
{
|
||||
"rules": {
|
||||
".read": "auth != null",
|
||||
".write": "auth != null"
|
||||
"votes": {
|
||||
"$gameId": {
|
||||
".read": "auth != null",
|
||||
"counts": {
|
||||
".read": true
|
||||
},
|
||||
".write": false
|
||||
}
|
||||
},
|
||||
"userVotes": {
|
||||
"$uid": {
|
||||
".read": "auth != null && auth.uid == $uid",
|
||||
".write": false
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
".read": false,
|
||||
".write": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"database": {
|
||||
"rules": "y"
|
||||
"rules": "database.rules.json"
|
||||
},
|
||||
"firestore": {
|
||||
"database": "(default)",
|
||||
@ -21,7 +21,6 @@
|
||||
"*.local"
|
||||
],
|
||||
"predeploy": [
|
||||
"npm --prefix \"$RESOURCE_DIR\" run lint",
|
||||
"npm --prefix \"$RESOURCE_DIR\" run build"
|
||||
]
|
||||
}
|
||||
@ -44,5 +43,35 @@
|
||||
},
|
||||
"auth": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": [],
|
||||
"fieldOverrides": []
|
||||
}
|
||||
@ -1,19 +1,33 @@
|
||||
rules_version = '2';
|
||||
rules_version='2'
|
||||
|
||||
service cloud.firestore {
|
||||
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=**} {
|
||||
allow read, write: if request.time < timestamp.date(2026, 5, 2);
|
||||
allow read, write: if false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1553
package-lock.json
generated
1553
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,11 +4,13 @@
|
||||
"lint": "eslint --ext .js,.ts .",
|
||||
"build": "tsc",
|
||||
"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",
|
||||
"start": "npm run shell",
|
||||
"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": {
|
||||
"node": "24"
|
||||
@ -25,7 +27,8 @@
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"firebase-functions-test": "^3.4.1",
|
||||
"typescript": "^5.7.3"
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.4"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
40
src/index.ts
40
src/index.ts
@ -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 "./firebase";
|
||||
|
||||
import {setGlobalOptions} from "firebase-functions";
|
||||
import {onRequest} from "firebase-functions/https";
|
||||
import * as logger from "firebase-functions/logger";
|
||||
setGlobalOptions({ maxInstances: 10, region: "asia-northeast3" });
|
||||
|
||||
// Start writing functions
|
||||
// https://firebase.google.com/docs/functions/typescript
|
||||
|
||||
// For cost control, you can set the maximum number of containers that can be
|
||||
// running at the same time. This helps mitigate the impact of unexpected
|
||||
// traffic spikes by instead downgrading performance. This limit is a
|
||||
// per-function limit. You can override the limit for each function using the
|
||||
// `maxInstances` option in the function's options, e.g.
|
||||
// `onRequest({ maxInstances: 5 }, (req, res) => { ... })`.
|
||||
// NOTE: setGlobalOptions does not apply to functions using the v1 API. V1
|
||||
// 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!");
|
||||
// });
|
||||
export { kbo } from "./handlers/kboHandlers";
|
||||
export { auth } from "./handlers/authHandlers";
|
||||
export { prediction } from "./handlers/predictionHandlers";
|
||||
export { stats } from "./handlers/statsHandlers";
|
||||
export { admin } from "./handlers/adminHandlers";
|
||||
export { kboDailyRefresh } from "./scheduled/kboRefresh";
|
||||
export { dailyArchive } from "./scheduled/dailyArchive";
|
||||
|
||||
147
src/kbo/cli.ts
147
src/kbo/cli.ts
@ -5,11 +5,13 @@
|
||||
* Commands:
|
||||
* rank [year...] 팀 순위
|
||||
* player <hitter|pitcher|defense|runner> 선수 기록
|
||||
* schedule [year] [month] 경기 일정/결과
|
||||
*
|
||||
* Options:
|
||||
* --json JSON 출력
|
||||
* --all 모든 페이지 조회 (player)
|
||||
* --team=팀명 팀 필터 (e.g. --team=LG)
|
||||
* --series=값 시리즈 필터 (schedule: 정규/시범/포스트)
|
||||
*/
|
||||
|
||||
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 { DEFENSE_CONFIG } from "./player/defense.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> = {
|
||||
hitter: HITTER_CONFIG,
|
||||
@ -118,9 +126,9 @@ function printPlayerTable(
|
||||
const filterDesc = Object.entries(result.filters)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(", ");
|
||||
const label = filterDesc
|
||||
? `KBO ${result.year} ${subcommand} (${filterDesc})`
|
||||
: `KBO ${result.year} ${subcommand}`;
|
||||
const label = filterDesc ?
|
||||
`KBO ${result.year} ${subcommand} (${filterDesc})` :
|
||||
`KBO ${result.year} ${subcommand}`;
|
||||
|
||||
console.log(`\n${"═".repeat(100)}`);
|
||||
console.log(` ${label}`);
|
||||
@ -143,7 +151,7 @@ function printPlayerTable(
|
||||
console.log(" " + widths.map((w) => "-".repeat(w)).join("-+-"));
|
||||
|
||||
for (const rec of result.records) {
|
||||
const row = columns.map((c) => rec[c] ?? "");
|
||||
const row = columns.map((c) => String(rec[c] ?? ""));
|
||||
console.log(
|
||||
" " + row.map((val, i) => padCell(val, widths[i])).join(" | ")
|
||||
);
|
||||
@ -152,8 +160,89 @@ function printPlayerTable(
|
||||
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 ──
|
||||
|
||||
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) {
|
||||
console.log("Fetching KBO team rankings...\n");
|
||||
|
||||
@ -236,6 +325,7 @@ function printHelp() {
|
||||
console.log("Commands:");
|
||||
console.log(" rank [year...] Team rankings");
|
||||
console.log(" player <hitter|pitcher|defense|runner> Player stats");
|
||||
console.log(" schedule [year] [month] 경기 일정/결과");
|
||||
console.log("");
|
||||
console.log("Options:");
|
||||
console.log(" --json JSON output");
|
||||
@ -247,6 +337,10 @@ function printHelp() {
|
||||
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=값");
|
||||
console.log("");
|
||||
console.log("Schedule filters:");
|
||||
console.log(" --team=팀명 팀 (LG, 삼성, KT, ...)");
|
||||
console.log(" --series=값 시리즈 (정규, 시범, 포스트)");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@ -317,6 +411,39 @@ async function main() {
|
||||
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 및 기타 커맨드
|
||||
const rest = args.slice(1);
|
||||
let jsonMode = false;
|
||||
@ -337,12 +464,12 @@ async function main() {
|
||||
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);
|
||||
case "rank":
|
||||
await rankCommand(years, jsonMode);
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ export function decodeHtmlEntities(text: string): string {
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/"/g, "\"")
|
||||
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
|
||||
}
|
||||
|
||||
|
||||
@ -43,7 +43,19 @@ export interface PlayerPageConfig {
|
||||
* const hitter = record as HitterStats;
|
||||
* 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 columns - 각 `<td>`에 매핑할 컬럼 이름 배열 (순서대로 매핑)
|
||||
* @returns 각 행을 `{ [컬럼명]: 값 }` 형태로 변환한 배열.
|
||||
* @return 각 행을 `{ [컬럼명]: 값 }` 형태로 변환한 배열.
|
||||
* 첫 번째 `<td>`가 숫자(순위)인 행만 포함한다.
|
||||
*/
|
||||
export function parsePlayerTable(
|
||||
@ -121,7 +133,7 @@ export function parsePlayerTable(
|
||||
if (/^\d+$/.test(rank)) {
|
||||
const record: PlayerRecord = {};
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
record[columns[i]] = tds[i];
|
||||
record[columns[i]] = parsePlayerCell(columns[i], tds[i]);
|
||||
}
|
||||
records.push(record);
|
||||
}
|
||||
@ -157,7 +169,7 @@ const DDL_DEFAULTS: Record<string, string> = {
|
||||
* @param config - 페이지 설정 (dropdowns, defaultSortCol 등)
|
||||
* @param year - 조회 연도
|
||||
* @param filters - 사용자 지정 필터 (team, series, pos 등)
|
||||
* @returns `URLSearchParams`에 설정할 키-값 쌍 객체
|
||||
* @return `URLSearchParams`에 설정할 키-값 쌍 객체
|
||||
*/
|
||||
function buildFormFields(
|
||||
config: PlayerPageConfig,
|
||||
@ -200,7 +212,7 @@ function buildFormFields(
|
||||
* 후속 postback에 필요한 ASP.NET 상태(ViewState, 쿠키 등)를 반환한다.
|
||||
*
|
||||
* @param config - 페이지 설정 (url, columns 등)
|
||||
* @returns 초기 데이터(result)와 후속 요청용 상태(state)
|
||||
* @return 초기 데이터(result)와 후속 요청용 상태(state)
|
||||
*/
|
||||
export async function fetchPlayerStatsInitial(
|
||||
config: PlayerPageConfig
|
||||
@ -210,9 +222,9 @@ export async function fetchPlayerStatsInitial(
|
||||
const yearMatch = html.match(
|
||||
/ddlSeason_ddlSeason[\s\S]*?selected="selected"\s+value="(\d{4})"/
|
||||
);
|
||||
const year = yearMatch
|
||||
? parseInt(yearMatch[1], 10)
|
||||
: new Date().getFullYear();
|
||||
const year = yearMatch ?
|
||||
parseInt(yearMatch[1], 10) :
|
||||
new Date().getFullYear();
|
||||
|
||||
return {
|
||||
result: {
|
||||
@ -239,7 +251,7 @@ export async function fetchPlayerStatsInitial(
|
||||
* @param year - 조회 연도
|
||||
* @param state - 이전 요청에서 받은 ASP.NET 상태
|
||||
* @param filters - 조회 필터 (team, series, pos, situation 등)
|
||||
* @returns 조회 결과(result)와 갱신된 상태(newState)
|
||||
* @return 조회 결과(result)와 갱신된 상태(newState)
|
||||
*/
|
||||
export async function fetchPlayerStats(
|
||||
config: PlayerPageConfig,
|
||||
@ -252,9 +264,9 @@ export async function fetchPlayerStats(
|
||||
const useUrl =
|
||||
isPostseason && config.postseasonUrl ? config.postseasonUrl : config.url;
|
||||
const useColumns =
|
||||
isPostseason && config.postseasonColumns
|
||||
? config.postseasonColumns
|
||||
: config.columns;
|
||||
isPostseason && config.postseasonColumns ?
|
||||
config.postseasonColumns :
|
||||
config.columns;
|
||||
|
||||
// 포스트시즌은 별도 페이지이므로 해당 페이지의 초기 상태에서 시작
|
||||
if (isPostseason && config.postseasonUrl) {
|
||||
@ -373,7 +385,7 @@ export async function fetchPlayerStats(
|
||||
* @param year - 조회 연도
|
||||
* @param state - 이전 요청에서 받은 ASP.NET 상태
|
||||
* @param filters - 조회 필터
|
||||
* @returns 모든 페이지의 레코드를 합친 결과와 갱신된 상태
|
||||
* @return 모든 페이지의 레코드를 합친 결과와 갱신된 상태
|
||||
*/
|
||||
export async function fetchAllPlayerStats(
|
||||
config: PlayerPageConfig,
|
||||
@ -388,6 +400,7 @@ export async function fetchAllPlayerStats(
|
||||
let currentState = firstState;
|
||||
let page = 2;
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const pagerTarget = `${PREFIX}ucPager$btnNo${page}`;
|
||||
const fields = buildFormFields(config, year, filters);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { PlayerPageConfig } from "./common.js";
|
||||
import type { PlayerPageConfig, PlayerValue } from "./common.js";
|
||||
|
||||
export const DEFENSE_COLUMNS = [
|
||||
"rank", "player", "team", "pos", "games", "gs", "ip",
|
||||
@ -6,7 +6,7 @@ export const DEFENSE_COLUMNS = [
|
||||
"sb", "cs", "csPct",
|
||||
] 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 = {
|
||||
url: "https://www.koreabaseball.com/Record/Player/Defense/Basic.aspx",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { PlayerPageConfig } from "./common.js";
|
||||
import type { PlayerPageConfig, PlayerValue } from "./common.js";
|
||||
|
||||
export const HITTER_COLUMNS = [
|
||||
"rank", "player", "team", "avg", "games", "pa", "ab",
|
||||
@ -11,7 +11,7 @@ export const HITTER_POSTSEASON_COLUMNS = [
|
||||
"bb", "hbp", "so", "gdp", "errors",
|
||||
] 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 = {
|
||||
url: "https://www.koreabaseball.com/Record/Player/HitterBasic/Basic1.aspx",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { PlayerPageConfig } from "./common.js";
|
||||
import type { PlayerPageConfig, PlayerValue } from "./common.js";
|
||||
|
||||
export const PITCHER_COLUMNS = [
|
||||
"rank", "player", "team", "era", "games", "wins", "losses",
|
||||
@ -12,7 +12,7 @@ export const PITCHER_POSTSEASON_COLUMNS = [
|
||||
"hits", "hr", "bb", "hbp", "so", "runs", "er",
|
||||
] 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 = {
|
||||
url: "https://www.koreabaseball.com/Record/Player/PitcherBasic/Basic1.aspx",
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import type { PlayerPageConfig } from "./common.js";
|
||||
import type { PlayerPageConfig, PlayerValue } from "./common.js";
|
||||
|
||||
export const RUNNER_COLUMNS = [
|
||||
"rank", "player", "team", "games", "sba", "sb", "cs",
|
||||
"sbPct", "oob", "pko",
|
||||
] 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 = {
|
||||
url: "https://www.koreabaseball.com/Record/Player/Runner/Basic.aspx",
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"outDir": "lib",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"target": "es2017"
|
||||
"target": "es2017",
|
||||
},
|
||||
"compileOnSave": true,
|
||||
"include": [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user