Standardize KBO team codes and improve API parameter validation.

- KBO API 요청 매개변수에 `TeamCode` enum을 적용하여 타입 안정성을 강화하고 서비스 레이어의 불필요한 매핑 로직을 제거했습니다.
- `TEAM_CODES` 상수를 내부 데이터 정규화용 어댑터로 한정하여 외부 API 의존성을 줄였습니다.
- 핸들러 단계에서 유효한 팀 코드인지 검증하는 로직을 추가하여 잘못된 요청에 대한 에러 처리를 강화했습니다.
- `getPlayerStats` 함수의 인자 전달 방식을 객체 형태로 리팩토링하여 가독성과 유지보수성을 높였습니다.
This commit is contained in:
윤정민 2026-05-06 16:15:39 +09:00
parent c94e2eb81b
commit 7496cb7de7
5 changed files with 79 additions and 28 deletions

View File

@ -4,7 +4,7 @@ import { getSchedule } from "../services/scheduleService.js";
import { getPlayerStats, getValidPlayerTypes } from "../services/playerService.js"; import { getPlayerStats, getValidPlayerTypes } from "../services/playerService.js";
import { getGameList } from "../services/gameListService.js"; import { getGameList } from "../services/gameListService.js";
import { getGameDetail } from "../services/gameDetailService.js"; import { getGameDetail } from "../services/gameDetailService.js";
import type { PlayerFilters } from "../types/kbo.js"; import { TeamCode } from "../types/panit.js";
enum KboPath { enum KboPath {
Rank = "rank", Rank = "rank",
@ -59,7 +59,6 @@ export const kbo = onRequest(async (req, res) => {
const now = new Date(); const now = new Date();
const year = parseIntParam(req.query.year, now.getFullYear()); const year = parseIntParam(req.query.year, now.getFullYear());
const month = parseIntParam(req.query.month, now.getMonth() + 1); const month = parseIntParam(req.query.month, now.getMonth() + 1);
const team = req.query.team ? String(req.query.team) : undefined;
const series = req.query.series ? String(req.query.series) : undefined; const series = req.query.series ? String(req.query.series) : undefined;
const day = req.query.day !== undefined const day = req.query.day !== undefined
? parseIntParam(req.query.day, NaN) ? parseIntParam(req.query.day, NaN)
@ -78,6 +77,19 @@ export const kbo = onRequest(async (req, res) => {
return; return;
} }
let team: TeamCode | undefined;
if (req.query.team) {
const raw = String(req.query.team);
const validCodes = Object.values(TeamCode) as string[];
if (!validCodes.includes(raw)) {
res.status(400).json({
error: `Invalid team: "${raw}". Use: ${validCodes.join(", ")}`,
});
return;
}
team = raw as TeamCode;
}
const result = await getSchedule(year, month, team, series, day); const result = await getSchedule(year, month, team, series, day);
res.status(200).json(result); res.status(200).json(result);
return; return;
@ -141,16 +153,30 @@ export const kbo = onRequest(async (req, res) => {
return; return;
} }
const filters: PlayerFilters = {}; let team: TeamCode | undefined;
if (req.query.team) filters.team = String(req.query.team); if (req.query.team) {
if (req.query.series) filters.series = String(req.query.series); const raw = String(req.query.team);
if (req.query.pos) filters.pos = String(req.query.pos); const validCodes = Object.values(TeamCode) as string[];
if (req.query.situation) filters.situation = String(req.query.situation); if (!validCodes.includes(raw)) {
if (req.query.situationDetail) { res.status(400).json({
filters.situationDetail = String(req.query.situationDetail); error: `Invalid team: "${raw}". Use: ${validCodes.join(", ")}`,
});
return;
}
team = raw as TeamCode;
} }
const result = await getPlayerStats(type, year, filters, allPages); const result = await getPlayerStats({
type,
year,
team,
series: req.query.series ? String(req.query.series) : undefined,
pos: req.query.pos ? String(req.query.pos) : undefined,
situation: req.query.situation ? String(req.query.situation) : undefined,
situationDetail: req.query.situationDetail ?
String(req.query.situationDetail) : undefined,
allPages,
});
res.status(200).json(result); res.status(200).json(result);
return; return;
} }

View File

@ -82,10 +82,24 @@ export interface PlayerStatsResult {
} }
/** /**
* KBO . * (shortName/) KBO .
* *
* KBO value로 . * KBO value로 .
* : SSG는 SK "SK", "WO" . * : SSG는 SK "SK", "WO" .
*
* ##
* API의 `team` `TeamCode` enum으로
* (TeamCode KBO ).
*
* :
* 1. `kbo/schedule.ts` `toTeamCode()` HTML
* ("두산", "한화" ) `awayTeamCode`/`homeTeamCode`
* .
* 2. CLI `kbo/cli.ts` /shortName을
* KBO .
*
* "사람이 읽는 표기 → KBO 내부 코드" ,
* API .
*/ */
export const TEAM_CODES: Record<string, string> = { export const TEAM_CODES: Record<string, string> = {
KT: "KT", KT: "KT",

View File

@ -1,6 +1,6 @@
import { fetchPlayerFromKbo } from "../repositories/kboRepository.js"; import { fetchPlayerFromKbo } from "../repositories/kboRepository.js";
import { TEAM_CODES } from "../types/kbo.js";
import type { PlayerFilters, PlayerPageConfig, PlayerStatsResult } from "../types/kbo.js"; import type { PlayerFilters, PlayerPageConfig, PlayerStatsResult } from "../types/kbo.js";
import { TeamCode } from "../types/panit.js";
import { HITTER_CONFIG } from "../kbo/player/hitter.js"; import { HITTER_CONFIG } from "../kbo/player/hitter.js";
import { PITCHER_CONFIG } from "../kbo/player/pitcher.js"; import { PITCHER_CONFIG } from "../kbo/player/pitcher.js";
import { DEFENSE_CONFIG } from "../kbo/player/defense.js"; import { DEFENSE_CONFIG } from "../kbo/player/defense.js";
@ -21,12 +21,22 @@ export function getValidPlayerTypes(): string[] {
return Object.keys(PLAYER_CONFIGS); return Object.keys(PLAYER_CONFIGS);
} }
export interface GetPlayerStatsArgs {
type: string;
year: number;
team?: TeamCode;
series?: string;
pos?: string;
situation?: string;
situationDetail?: string;
allPages: boolean;
}
export async function getPlayerStats( export async function getPlayerStats(
type: string, args: GetPlayerStatsArgs
year: number,
filters: PlayerFilters,
allPages: boolean
): Promise<PlayerStatsResult> { ): Promise<PlayerStatsResult> {
const { type, year, team, series, pos, situation, situationDetail, allPages } = args;
const config = PLAYER_CONFIGS[type]; const config = PLAYER_CONFIGS[type];
if (!config) { if (!config) {
throw new Error( throw new Error(
@ -34,10 +44,12 @@ export async function getPlayerStats(
); );
} }
const resolvedFilters = { ...filters }; const filters: PlayerFilters = {};
if (resolvedFilters.team) { if (team) filters.team = team;
resolvedFilters.team = TEAM_CODES[resolvedFilters.team] ?? resolvedFilters.team; if (series) filters.series = series;
} if (pos) filters.pos = pos;
if (situation) filters.situation = situation;
if (situationDetail) filters.situationDetail = situationDetail;
return fetchPlayerFromKbo(type, config, year, resolvedFilters, allPages); return fetchPlayerFromKbo(type, config, year, filters, allPages);
} }

View File

@ -1,15 +1,15 @@
import { fetchScheduleFromKbo } from "../repositories/kboRepository.js"; import { fetchScheduleFromKbo } from "../repositories/kboRepository.js";
import { TEAM_CODES, SERIES_CODES } from "../types/kbo.js"; import { SERIES_CODES } from "../types/kbo.js";
import type { ScheduleResult } from "../types/kbo.js"; import type { ScheduleResult } from "../types/kbo.js";
import type { TeamCode } from "../types/panit.js";
export async function getSchedule( export async function getSchedule(
year: number, year: number,
month: number, month: number,
team?: string, team?: TeamCode,
series?: string, series?: string,
day?: number day?: number
): Promise<ScheduleResult> { ): Promise<ScheduleResult> {
const resolvedTeam = team ? (TEAM_CODES[team] ?? team) : undefined;
const resolvedSeries = series ? (SERIES_CODES[series] ?? series) : undefined; const resolvedSeries = series ? (SERIES_CODES[series] ?? series) : undefined;
return fetchScheduleFromKbo({ return fetchScheduleFromKbo({
@ -17,6 +17,6 @@ export async function getSchedule(
month, month,
day, day,
series: resolvedSeries, series: resolvedSeries,
team: resolvedTeam, team,
}); });
} }

View File

@ -22,7 +22,6 @@ export type {
PlayerStatsResult, PlayerStatsResult,
} from "../kbo/player/common.js"; } from "../kbo/player/common.js";
export { TEAM_CODES } from "../kbo/player/common.js";
export { SERIES_CODES } from "../kbo/schedule.js"; export { SERIES_CODES } from "../kbo/schedule.js";
export type { export type {