From 148e26d1bbcfc9a6486c2821d26e9722c3ca3f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=EC=A0=95=EB=AF=BC?= Date: Fri, 3 Jul 2026 16:41:44 +0900 Subject: [PATCH] Add head-to-head record lookup to rank snapshot tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get_team_rank_snapshot에 vs 인자 추가 — 순위표의 팀간 승패표(vsRecords)에서 두 팀의 올 시즌 맞대결 승패무를 반환 - formatH2hResult 순수 함수 분리 및 단위 테스트 추가 - 오늘 상대가 아닌 임의 팀과의 상대 전적 질문을 도구 1회 호출로 처리(일정→전적 체이닝도 가능). 기존에는 vsRecords가 도구 계층에서 버려져 모델이 접근할 경로가 없었음 --- src/services/chatToolService.ts | 40 ++++++++++++++++++++++++-- tests/services/chatToolService.test.ts | 24 +++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/services/chatToolService.ts b/src/services/chatToolService.ts index c91240d..7c83235 100644 --- a/src/services/chatToolService.ts +++ b/src/services/chatToolService.ts @@ -11,7 +11,7 @@ import type { ChatTool } from "./chatProviderService"; import type { ChatToolCallInfo } from "../types/chat"; import type { Lineup, KeyPlayerRanking } from "../kbo/game-detail"; import type { PlayerRecord } from "../kbo/player/common"; -import type { TeamRank } from "../kbo/team-rank"; +import type { TeamRank, TeamVsRecord } from "../kbo/team-rank"; import type { ScheduleGame } from "../types/kbo"; import { TeamCode, type AttendanceMonthDoc, type VoteHistoryDoc } from "../types/panit"; import { addDays, type DateString } from "../types/dateString"; @@ -218,6 +218,21 @@ export function formatRankResult(teams: TeamRank[], teamCode: TeamCode | null): ); } +/** + * 팀간 시즌 상대 전적(h2h) 포맷(순수 함수). KBO 순위표의 팀간 승패표(vsRecords)에서 + * 두 팀의 맞대결 성적을 꺼낸다 — 오늘 상대가 아닌 임의 팀과의 전적 질문용. + */ +export function formatH2hResult(team: TeamCode, vs: TeamCode, vsRecords: TeamVsRecord[]): string { + const myName = KBO_RANK_TEAM_NAMES[team]; + const oppName = KBO_RANK_TEAM_NAMES[vs]; + const wld = vsRecords.find((r) => r.team === myName)?.headToHead[oppName]; + if (!wld) return `${teamLabel(team)}와 ${teamLabel(vs)}의 상대 전적 정보를 못 찾았어.`; + return ( + `[${teamLabel(team)} vs ${teamLabel(vs)}] 올 시즌 상대 전적 ` + + `${wld.wins}승 ${wld.losses}패 ${wld.draws}무 (${teamLabel(team)} 기준)` + ); +} + /** 출석 결과 포맷(순수 함수). 이번 달 출석 일자·누적·포인트 잔액. */ export function formatAttendanceResult( monthLabel: string, @@ -438,7 +453,8 @@ export function buildChatTools(ctx: ChatToolContext): ChatTool[] { description: "KBO 팀 순위와 성적을 조회한다. team 지정 시 그 팀의 순위·승패·승률·게임차·최근10경기·" + "연승연패·홈원정 성적을 상세히, all=true면 전체 순위표를 돌려준다. " + - "\"우리 몇 위?\", \"LG 순위\", \"전체 순위 보여줘\" 류에 사용한다.", + "vs에 상대 팀을 함께 주면 두 팀의 올 시즌 맞대결 상대 전적(승패무)을 돌려준다. " + + "\"우리 몇 위?\", \"LG 순위\", \"전체 순위 보여줘\", \"LG전 상대 전적 어때?\" 류에 사용한다.", parameters: { type: "object", properties: { @@ -450,11 +466,31 @@ export function buildChatTools(ctx: ChatToolContext): ChatTool[] { type: "boolean", description: "전체 순위표를 원하면 true. 특정 팀 상세면 생략.", }, + vs: { + type: "string", + description: + "상대 전적을 조회할 상대 팀 코드(예: LG). team(생략 시 응원팀)과 이 팀의 " + + "올 시즌 맞대결 승패무를 돌려준다. 두 팀 간 전적 질문에만 사용.", + }, }, additionalProperties: false, }, async run(args) { try { + // 상대 전적(h2h) 경로 — vs 지정 시 순위표의 팀간 승패표(vsRecords)에서 꺼낸다 + const vsRaw = typeof args.vs === "string" && args.vs.trim() !== "" ? args.vs.trim() : null; + if (vsRaw) { + const vs = resolveTeamCode(vsRaw); + if (!vs) return "상대 팀은 HH, LG, OB 같은 팀 코드로 알려줘."; + const team = pickTeam(args, ctx); + if (!team) return NO_TEAM_NOTICE; + if (team === vs) return "같은 팀끼리는 상대 전적이 없어 — 서로 다른 두 팀을 알려줘."; + const result = await getRank([year]); + if (!result || result.length === 0 || result[0].vsRecords.length === 0) { + return "상대 전적 정보를 지금은 못 가져왔어."; + } + return formatH2hResult(team, vs, result[0].vsRecords); + } const team = args.all === true ? null : (resolveTeamCode(args.team) ?? ctx.teamCode); const result = await getRank([year]); if (!result || result.length === 0 || !result[0].teams) { diff --git a/tests/services/chatToolService.test.ts b/tests/services/chatToolService.test.ts index 89a6060..c3bbb42 100644 --- a/tests/services/chatToolService.test.ts +++ b/tests/services/chatToolService.test.ts @@ -3,6 +3,7 @@ import { buildChatTools, formatAttendanceResult, formatGameStandouts, + formatH2hResult, formatLineupResult, formatPredictionBreakdown, formatRankResult, @@ -14,7 +15,7 @@ import { type ChatToolContext, } from "../../src/services/chatToolService"; import type { KeyPlayerRanking } from "../../src/kbo/game-detail"; -import type { TeamRank } from "../../src/kbo/team-rank"; +import type { TeamRank, TeamVsRecord } from "../../src/kbo/team-rank"; import type { AttendanceMonthDoc, VoteHistoryDoc } from "../../src/types/panit"; import { getChatProvider } from "../../src/services/chatProviderService"; import { DEFAULT_PROVIDER_CONFIG } from "../../src/services/chatConfigService"; @@ -231,6 +232,27 @@ describe("chatToolService", () => { }); }); + describe("formatH2hResult — 팀간 시즌 상대 전적(vsRecords)", () => { + const vsRecords: TeamVsRecord[] = [ + { + team: "한화", + headToHead: { LG: { wins: 9, losses: 5, draws: 1 } }, + total: { wins: 9, losses: 5, draws: 1 }, + }, + ]; + + it("두 팀의 맞대결 승패무를 조회 팀 기준으로 전한다", () => { + const out = formatH2hResult(TeamCode.HH, TeamCode.LG, vsRecords); + expect(out).toContain("한화 이글스 vs LG 트윈스"); + expect(out).toContain("9승 5패 1무"); + expect(out).toContain("한화 이글스 기준"); + }); + + it("승패표에 상대 항목이 없으면 부재를 알린다", () => { + expect(formatH2hResult(TeamCode.LG, TeamCode.OB, vsRecords)).toContain("못 찾았어"); + }); + }); + describe("formatAttendanceResult — 출석·포인트", () => { it("출석 일자·누적·잔액을 전한다", () => { const doc = { days: [3, 1, 2], lastCheckedInAt: null } as unknown as AttendanceMonthDoc;