Add head-to-head record lookup to rank snapshot tool

- get_team_rank_snapshot에 vs 인자 추가 — 순위표의 팀간 승패표(vsRecords)에서 두 팀의 올 시즌 맞대결 승패무를 반환
- formatH2hResult 순수 함수 분리 및 단위 테스트 추가
- 오늘 상대가 아닌 임의 팀과의 상대 전적 질문을 도구 1회 호출로 처리(일정→전적 체이닝도 가능). 기존에는 vsRecords가 도구 계층에서 버려져 모델이 접근할 경로가 없었음
This commit is contained in:
윤정민 2026-07-03 16:41:44 +09:00
parent 9e24611db0
commit 148e26d1bb
2 changed files with 61 additions and 3 deletions

View File

@ -11,7 +11,7 @@ import type { ChatTool } from "./chatProviderService";
import type { ChatToolCallInfo } from "../types/chat"; import type { ChatToolCallInfo } from "../types/chat";
import type { Lineup, KeyPlayerRanking } from "../kbo/game-detail"; import type { Lineup, KeyPlayerRanking } from "../kbo/game-detail";
import type { PlayerRecord } from "../kbo/player/common"; 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 type { ScheduleGame } from "../types/kbo";
import { TeamCode, type AttendanceMonthDoc, type VoteHistoryDoc } from "../types/panit"; import { TeamCode, type AttendanceMonthDoc, type VoteHistoryDoc } from "../types/panit";
import { addDays, type DateString } from "../types/dateString"; 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( export function formatAttendanceResult(
monthLabel: string, monthLabel: string,
@ -438,7 +453,8 @@ export function buildChatTools(ctx: ChatToolContext): ChatTool[] {
description: description:
"KBO 팀 순위와 성적을 조회한다. team 지정 시 그 팀의 순위·승패·승률·게임차·최근10경기·" + "KBO 팀 순위와 성적을 조회한다. team 지정 시 그 팀의 순위·승패·승률·게임차·최근10경기·" +
"연승연패·홈원정 성적을 상세히, all=true면 전체 순위표를 돌려준다. " + "연승연패·홈원정 성적을 상세히, all=true면 전체 순위표를 돌려준다. " +
"\"우리 몇 위?\", \"LG 순위\", \"전체 순위 보여줘\" 류에 사용한다.", "vs에 상대 팀을 함께 주면 두 팀의 올 시즌 맞대결 상대 전적(승패무)을 돌려준다. " +
"\"우리 몇 위?\", \"LG 순위\", \"전체 순위 보여줘\", \"LG전 상대 전적 어때?\" 류에 사용한다.",
parameters: { parameters: {
type: "object", type: "object",
properties: { properties: {
@ -450,11 +466,31 @@ export function buildChatTools(ctx: ChatToolContext): ChatTool[] {
type: "boolean", type: "boolean",
description: "전체 순위표를 원하면 true. 특정 팀 상세면 생략.", description: "전체 순위표를 원하면 true. 특정 팀 상세면 생략.",
}, },
vs: {
type: "string",
description:
"상대 전적을 조회할 상대 팀 코드(예: LG). team(생략 시 응원팀)과 이 팀의 " +
"올 시즌 맞대결 승패무를 돌려준다. 두 팀 간 전적 질문에만 사용.",
},
}, },
additionalProperties: false, additionalProperties: false,
}, },
async run(args) { async run(args) {
try { 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 team = args.all === true ? null : (resolveTeamCode(args.team) ?? ctx.teamCode);
const result = await getRank([year]); const result = await getRank([year]);
if (!result || result.length === 0 || !result[0].teams) { if (!result || result.length === 0 || !result[0].teams) {

View File

@ -3,6 +3,7 @@ import {
buildChatTools, buildChatTools,
formatAttendanceResult, formatAttendanceResult,
formatGameStandouts, formatGameStandouts,
formatH2hResult,
formatLineupResult, formatLineupResult,
formatPredictionBreakdown, formatPredictionBreakdown,
formatRankResult, formatRankResult,
@ -14,7 +15,7 @@ import {
type ChatToolContext, type ChatToolContext,
} from "../../src/services/chatToolService"; } from "../../src/services/chatToolService";
import type { KeyPlayerRanking } from "../../src/kbo/game-detail"; 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 type { AttendanceMonthDoc, VoteHistoryDoc } from "../../src/types/panit";
import { getChatProvider } from "../../src/services/chatProviderService"; import { getChatProvider } from "../../src/services/chatProviderService";
import { DEFAULT_PROVIDER_CONFIG } from "../../src/services/chatConfigService"; 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 — 출석·포인트", () => { describe("formatAttendanceResult — 출석·포인트", () => {
it("출석 일자·누적·잔액을 전한다", () => { it("출석 일자·누적·잔액을 전한다", () => {
const doc = { days: [3, 1, 2], lastCheckedInAt: null } as unknown as AttendanceMonthDoc; const doc = { days: [3, 1, 2], lastCheckedInAt: null } as unknown as AttendanceMonthDoc;