턴 대상의 원본 이름과 누락값 표시를 분리해 검색 정확도 개선

This commit is contained in:
2026-09-14 14:24:59 +00:00
parent 8760eab1a9
commit 7522f5100e
11 changed files with 220 additions and 29 deletions
+2 -1
View File
@@ -365,7 +365,7 @@ export const getTurnCommandTable = async (ctx: GameApiContext, generalId: number
id: entry.id,
name: entry.name,
color: entry.color,
capitalName: entry.capitalCityId ? (cityById.get(entry.capitalCityId)?.name ?? '-') : '-',
capitalName: entry.capitalCityId ? cityById.get(entry.capitalCityId)?.name : undefined,
level: entry.level,
power: readGeneralMetaNumber(entry.meta, 'power') ?? 0,
generalCount: generalCountByNation.get(entry.id) ?? 0,
@@ -402,6 +402,7 @@ export const getTurnCommandTable = async (ctx: GameApiContext, generalId: number
cities: cities.map((entry) => ({
value: entry.id,
label: `${entry.name} (${nationById.get(entry.nationId)?.name ?? '무주'})`,
targetNames: { name: entry.name, nationName: nationById.get(entry.nationId)?.name ?? null },
})),
nations: nationTargetOptions.nations,
nationTargets: nationTargetOptions.nationTargets,
+8
View File
@@ -20,6 +20,14 @@ export type TurnCommandOptionValue = string | number;
export interface TurnCommandOption {
value: TurnCommandOptionValue;
label: string;
/** 원본 이름. 없는 소속/부대/수도는 null이며 대체 문구는 화면에서 표시한다. */
targetNames?: {
name: string;
nationName?: string | null;
cityName?: string | null;
troopName?: string | null;
capitalName?: string | null;
};
color?: string;
description?: string;
availableNow?: boolean;
+11 -12
View File
@@ -24,7 +24,7 @@ export interface NationTargetSource {
id: number;
name: string;
color: string;
capitalName: string;
capitalName?: string;
level: number;
power: number;
generalCount: number;
@@ -60,9 +60,6 @@ export const buildRefGeneralTargetOptions = (options: {
const toOption = (entry: GeneralTargetSource, action?: string): TurnCommandOption => {
const troopName = entry.troopId ? options.troopNames?.get(entry.troopId) : undefined;
const cityName = options.cityNames.get(entry.cityId) ?? '재야';
const troopLabel = entry.troopId
? `${troopName ?? `#${entry.troopId}`}${entry.troopId === entry.id ? ' (부대장)' : ''}`
: '부대 없음';
const isTroopMember = Boolean(entry.troopId && entry.troopId !== entry.id);
const isTroopExit = action === 'che_부대탈퇴지시';
const availableNow = isTroopExit ? isTroopMember && entry.id !== options.actorId : undefined;
@@ -77,18 +74,12 @@ export const buildRefGeneralTargetOptions = (options: {
entry.crew === undefined ? null : `병력 ${entry.crew.toLocaleString()}`,
entry.train === undefined ? null : `훈련 ${entry.train.toLocaleString()}`,
entry.atmos === undefined ? null : `사기 ${entry.atmos.toLocaleString()}`,
action === 'che_발령' || action === 'che_포상' || action === 'che_몰수'
? null
: entry.troopId
? `탑승 부대 ${troopLabel}`
: '탑승 부대 없음',
].filter((value): value is string => Boolean(value));
// 발령 후보는 Ref처럼 능력치와 병력 준비 상태를 함께 비교한다.
// 예약 요약에 쓰는 label은 그대로 두고, 같은 국가 후보의 상세 정보만 보강한다.
const assignmentDetails =
action === 'che_발령'
? [
entry.troopId ? `탑승 부대 ${troopLabel}` : '탑승 부대 없음',
[
entry.leadership === undefined ? null : `통솔 ${entry.leadership.toLocaleString()}`,
entry.strength === undefined ? null : `무력 ${entry.strength.toLocaleString()}`,
@@ -119,6 +110,12 @@ export const buildRefGeneralTargetOptions = (options: {
return {
value: entry.id,
label,
targetNames: {
name: entry.name,
nationName: options.nationNames.get(entry.nationId) ?? null,
cityName: options.cityNames.get(entry.cityId) ?? null,
troopName: troopName ?? null,
},
description: assignmentDetails ?? details.join(' · '),
...(availableNow === undefined ? {} : { availableNow }),
...(entry.gold === undefined ? {} : { gold: entry.gold }),
@@ -212,8 +209,9 @@ export const buildRefNationTargetOptions = (options: {
const baseOptions = options.nations.map<TurnCommandOption>((entry) => ({
value: entry.id,
label: entry.name,
targetNames: { name: entry.name, capitalName: entry.capitalName ?? null },
color: entry.color,
description: `수도 ${entry.capitalName} · 국력 ${entry.power.toLocaleString()} · 도시 ${entry.cityCount.toLocaleString()} · 장수 ${entry.generalCount.toLocaleString()}`,
description: `수도 ${entry.capitalName ?? '-'} · 국력 ${entry.power.toLocaleString()} · 도시 ${entry.cityCount.toLocaleString()} · 장수 ${entry.generalCount.toLocaleString()}`,
}));
const nationTargets: Record<string, TurnCommandOption[]> = {};
for (const action of NATION_TARGET_COMMANDS) {
@@ -225,9 +223,10 @@ export const buildRefNationTargetOptions = (options: {
return {
value: entry.id,
label: entry.name,
targetNames: { name: entry.name, capitalName: entry.capitalName ?? null },
color: entry.color,
availableNow: availability.available,
description: `${availability.reason} · ${relation}${term} · 수도 ${entry.capitalName} · 국력 ${entry.power.toLocaleString()} · 도시 ${entry.cityCount.toLocaleString()} · 장수 ${entry.generalCount.toLocaleString()}`,
description: `${availability.reason} · ${relation}${term} · 수도 ${entry.capitalName ?? '-'} · 국력 ${entry.power.toLocaleString()} · 도시 ${entry.cityCount.toLocaleString()} · 장수 ${entry.generalCount.toLocaleString()}`,
power: entry.power,
} as TurnCommandOption & { power: number };
})
+45 -6
View File
@@ -46,6 +46,24 @@ describe('Ref command general targets', () => {
}
});
it('indexes genuine names even when named 없음, and excludes missing-name placeholders', () => {
const options = buildRefGeneralTargetOptions({
actorId: 1,
actorNationId: 0,
generals: [general({ name: '없음', nationId: 0, cityId: 0, troopId: 99 })],
nationNames: new Map(),
cityNames: new Map(),
troopNames: new Map(),
});
expect(options.generalTargets.che_증여?.[0]?.targetNames).toEqual({
name: '없음',
nationName: null,
cityName: null,
troopName: null,
});
expect(options.generalTargets.che_증여?.[0]?.description).not.toContain('없음');
});
it('preserves the distinct Ref filters for gift, abdication, recruitment, and target-based joining', () => {
expect(ids('che_증여')).toEqual([1, 2, 3]);
expect(ids('che_선양')).toEqual([2, 3]);
@@ -88,7 +106,7 @@ describe('Ref command general targets', () => {
rice: 200,
crew: 900,
troopId: 3,
description: expect.stringContaining('탑승 부대 청룡대'),
targetNames: { name: '부대원', nationName: '아국', cityName: '업', troopName: '청룡대' },
});
expect(detailed.generalTargets.che_발령?.map((entry) => entry.label)).toEqual([
'본인 (업)',
@@ -96,13 +114,16 @@ describe('Ref command general targets', () => {
'부대장 (업)',
]);
expect(detailed.generalTargets.che_발령?.[1]?.description).toBe(
'탑승 부대 청룡대\n통솔 100 · 무력 95 · 지력 0\n병력 900 · 훈련 80 · 사기 70\n금 100 · 쌀 200'
'통솔 100 · 무력 95 · 지력 0\n병력 900 · 훈련 80 · 사기 70\n금 100 · 쌀 200'
);
expect(detailed.generalTargets.che_발령?.[0]?.description).toBe(
'탑승 부대 없음\n병력 1,000\n금 5,000 · 쌀 4,000'
);
expect(detailed.generalTargets.che_발령?.[2]?.description).toContain('청룡대 (부대장)');
expect(detailed.generalTargets.che_발령?.[0]?.description).toBe('병력 1,000\n금 5,000 · 쌀 4,000');
expect(detailed.generalTargets.che_발령?.[2]?.targetNames?.troopName).toBe('청룡대');
expect(detailed.generalTargets.che_증여?.[1]?.description).not.toContain('통솔');
expect(detailed.generalTargets.che_증여?.map((entry) => entry.targetNames)).toEqual([
{ name: '본인', nationName: '아국', cityName: '업', troopName: null },
{ name: '부대원', nationName: '아국', cityName: '업', troopName: '청룡대' },
{ name: '부대장', nationName: '아국', cityName: '업', troopName: '청룡대' },
]);
expect(detailed.generalTargets.che_포상?.map((entry) => entry.label)).toEqual([
'본인 (업)',
'부대원 (업)',
@@ -170,6 +191,24 @@ describe('Ref nation target guidance', () => {
},
];
it('indexes real nation and capital names without missing-capital or diplomacy copy', () => {
const result = buildRefNationTargetOptions({
actorNationId: 1,
nations: nations.map((entry) => ({ ...entry, capitalName: undefined })),
});
expect(result.nations.map((entry) => entry.targetNames)).toEqual(
nations.map((entry) => ({ name: entry.name, capitalName: null }))
);
for (const options of Object.values(result.nationTargets)) {
for (const option of options) expect(option.targetNames).toEqual({ name: option.label, capitalName: null });
}
const populated = buildRefNationTargetOptions({ actorNationId: 1, nations });
expect(populated.nations[0]?.targetNames).toEqual({
name: nations[0]!.name,
capitalName: nations[0]!.capitalName,
});
});
it('sorts the currently relevant relation first for each diplomacy command', () => {
const result = buildRefNationTargetOptions({ actorNationId: 1, nations });
expect(result.nationTargets.che_선전포고?.map((entry) => entry.value)).toEqual([2, 1, 3, 4]);