턴 대상의 원본 이름과 누락값 표시를 분리해 검색 정확도 개선
This commit is contained in:
@@ -365,7 +365,7 @@ export const getTurnCommandTable = async (ctx: GameApiContext, generalId: number
|
|||||||
id: entry.id,
|
id: entry.id,
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
color: entry.color,
|
color: entry.color,
|
||||||
capitalName: entry.capitalCityId ? (cityById.get(entry.capitalCityId)?.name ?? '-') : '-',
|
capitalName: entry.capitalCityId ? cityById.get(entry.capitalCityId)?.name : undefined,
|
||||||
level: entry.level,
|
level: entry.level,
|
||||||
power: readGeneralMetaNumber(entry.meta, 'power') ?? 0,
|
power: readGeneralMetaNumber(entry.meta, 'power') ?? 0,
|
||||||
generalCount: generalCountByNation.get(entry.id) ?? 0,
|
generalCount: generalCountByNation.get(entry.id) ?? 0,
|
||||||
@@ -402,6 +402,7 @@ export const getTurnCommandTable = async (ctx: GameApiContext, generalId: number
|
|||||||
cities: cities.map((entry) => ({
|
cities: cities.map((entry) => ({
|
||||||
value: entry.id,
|
value: entry.id,
|
||||||
label: `${entry.name} (${nationById.get(entry.nationId)?.name ?? '무주'})`,
|
label: `${entry.name} (${nationById.get(entry.nationId)?.name ?? '무주'})`,
|
||||||
|
targetNames: { name: entry.name, nationName: nationById.get(entry.nationId)?.name ?? null },
|
||||||
})),
|
})),
|
||||||
nations: nationTargetOptions.nations,
|
nations: nationTargetOptions.nations,
|
||||||
nationTargets: nationTargetOptions.nationTargets,
|
nationTargets: nationTargetOptions.nationTargets,
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ export type TurnCommandOptionValue = string | number;
|
|||||||
export interface TurnCommandOption {
|
export interface TurnCommandOption {
|
||||||
value: TurnCommandOptionValue;
|
value: TurnCommandOptionValue;
|
||||||
label: string;
|
label: string;
|
||||||
|
/** 원본 이름. 없는 소속/부대/수도는 null이며 대체 문구는 화면에서 표시한다. */
|
||||||
|
targetNames?: {
|
||||||
|
name: string;
|
||||||
|
nationName?: string | null;
|
||||||
|
cityName?: string | null;
|
||||||
|
troopName?: string | null;
|
||||||
|
capitalName?: string | null;
|
||||||
|
};
|
||||||
color?: string;
|
color?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
availableNow?: boolean;
|
availableNow?: boolean;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export interface NationTargetSource {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
color: string;
|
color: string;
|
||||||
capitalName: string;
|
capitalName?: string;
|
||||||
level: number;
|
level: number;
|
||||||
power: number;
|
power: number;
|
||||||
generalCount: number;
|
generalCount: number;
|
||||||
@@ -60,9 +60,6 @@ export const buildRefGeneralTargetOptions = (options: {
|
|||||||
const toOption = (entry: GeneralTargetSource, action?: string): TurnCommandOption => {
|
const toOption = (entry: GeneralTargetSource, action?: string): TurnCommandOption => {
|
||||||
const troopName = entry.troopId ? options.troopNames?.get(entry.troopId) : undefined;
|
const troopName = entry.troopId ? options.troopNames?.get(entry.troopId) : undefined;
|
||||||
const cityName = options.cityNames.get(entry.cityId) ?? '재야';
|
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 isTroopMember = Boolean(entry.troopId && entry.troopId !== entry.id);
|
||||||
const isTroopExit = action === 'che_부대탈퇴지시';
|
const isTroopExit = action === 'che_부대탈퇴지시';
|
||||||
const availableNow = isTroopExit ? isTroopMember && entry.id !== options.actorId : undefined;
|
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.crew === undefined ? null : `병력 ${entry.crew.toLocaleString()}`,
|
||||||
entry.train === undefined ? null : `훈련 ${entry.train.toLocaleString()}`,
|
entry.train === undefined ? null : `훈련 ${entry.train.toLocaleString()}`,
|
||||||
entry.atmos === undefined ? null : `사기 ${entry.atmos.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));
|
].filter((value): value is string => Boolean(value));
|
||||||
// 발령 후보는 Ref처럼 능력치와 병력 준비 상태를 함께 비교한다.
|
// 발령 후보는 Ref처럼 능력치와 병력 준비 상태를 함께 비교한다.
|
||||||
// 예약 요약에 쓰는 label은 그대로 두고, 같은 국가 후보의 상세 정보만 보강한다.
|
// 예약 요약에 쓰는 label은 그대로 두고, 같은 국가 후보의 상세 정보만 보강한다.
|
||||||
const assignmentDetails =
|
const assignmentDetails =
|
||||||
action === 'che_발령'
|
action === 'che_발령'
|
||||||
? [
|
? [
|
||||||
entry.troopId ? `탑승 부대 ${troopLabel}` : '탑승 부대 없음',
|
|
||||||
[
|
[
|
||||||
entry.leadership === undefined ? null : `통솔 ${entry.leadership.toLocaleString()}`,
|
entry.leadership === undefined ? null : `통솔 ${entry.leadership.toLocaleString()}`,
|
||||||
entry.strength === undefined ? null : `무력 ${entry.strength.toLocaleString()}`,
|
entry.strength === undefined ? null : `무력 ${entry.strength.toLocaleString()}`,
|
||||||
@@ -119,6 +110,12 @@ export const buildRefGeneralTargetOptions = (options: {
|
|||||||
return {
|
return {
|
||||||
value: entry.id,
|
value: entry.id,
|
||||||
label,
|
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(' · '),
|
description: assignmentDetails ?? details.join(' · '),
|
||||||
...(availableNow === undefined ? {} : { availableNow }),
|
...(availableNow === undefined ? {} : { availableNow }),
|
||||||
...(entry.gold === undefined ? {} : { gold: entry.gold }),
|
...(entry.gold === undefined ? {} : { gold: entry.gold }),
|
||||||
@@ -212,8 +209,9 @@ export const buildRefNationTargetOptions = (options: {
|
|||||||
const baseOptions = options.nations.map<TurnCommandOption>((entry) => ({
|
const baseOptions = options.nations.map<TurnCommandOption>((entry) => ({
|
||||||
value: entry.id,
|
value: entry.id,
|
||||||
label: entry.name,
|
label: entry.name,
|
||||||
|
targetNames: { name: entry.name, capitalName: entry.capitalName ?? null },
|
||||||
color: entry.color,
|
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[]> = {};
|
const nationTargets: Record<string, TurnCommandOption[]> = {};
|
||||||
for (const action of NATION_TARGET_COMMANDS) {
|
for (const action of NATION_TARGET_COMMANDS) {
|
||||||
@@ -225,9 +223,10 @@ export const buildRefNationTargetOptions = (options: {
|
|||||||
return {
|
return {
|
||||||
value: entry.id,
|
value: entry.id,
|
||||||
label: entry.name,
|
label: entry.name,
|
||||||
|
targetNames: { name: entry.name, capitalName: entry.capitalName ?? null },
|
||||||
color: entry.color,
|
color: entry.color,
|
||||||
availableNow: availability.available,
|
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,
|
power: entry.power,
|
||||||
} as TurnCommandOption & { power: number };
|
} as TurnCommandOption & { power: number };
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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', () => {
|
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([1, 2, 3]);
|
||||||
expect(ids('che_선양')).toEqual([2, 3]);
|
expect(ids('che_선양')).toEqual([2, 3]);
|
||||||
@@ -88,7 +106,7 @@ describe('Ref command general targets', () => {
|
|||||||
rice: 200,
|
rice: 200,
|
||||||
crew: 900,
|
crew: 900,
|
||||||
troopId: 3,
|
troopId: 3,
|
||||||
description: expect.stringContaining('탑승 부대 청룡대'),
|
targetNames: { name: '부대원', nationName: '아국', cityName: '업', troopName: '청룡대' },
|
||||||
});
|
});
|
||||||
expect(detailed.generalTargets.che_발령?.map((entry) => entry.label)).toEqual([
|
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(
|
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(
|
expect(detailed.generalTargets.che_발령?.[0]?.description).toBe('병력 1,000\n금 5,000 · 쌀 4,000');
|
||||||
'탑승 부대 없음\n병력 1,000\n금 5,000 · 쌀 4,000'
|
expect(detailed.generalTargets.che_발령?.[2]?.targetNames?.troopName).toBe('청룡대');
|
||||||
);
|
|
||||||
expect(detailed.generalTargets.che_발령?.[2]?.description).toContain('청룡대 (부대장)');
|
|
||||||
expect(detailed.generalTargets.che_증여?.[1]?.description).not.toContain('통솔');
|
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([
|
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', () => {
|
it('sorts the currently relevant relation first for each diplomacy command', () => {
|
||||||
const result = buildRefNationTargetOptions({ actorNationId: 1, nations });
|
const result = buildRefNationTargetOptions({ actorNationId: 1, nations });
|
||||||
expect(result.nationTargets.che_선전포고?.map((entry) => entry.value)).toEqual([2, 1, 3, 4]);
|
expect(result.nationTargets.che_선전포고?.map((entry) => entry.value)).toEqual([2, 1, 3, 4]);
|
||||||
|
|||||||
@@ -2892,7 +2892,8 @@ for (const width of [1200, 500]) {
|
|||||||
? {
|
? {
|
||||||
...option,
|
...option,
|
||||||
label: `${longName} (업)`,
|
label: `${longName} (업)`,
|
||||||
description: option.description?.replace('청룡대', longTroop),
|
description: option.description,
|
||||||
|
targetNames: { ...option.targetNames!, name: longName, troopName: longTroop },
|
||||||
}
|
}
|
||||||
: option
|
: option
|
||||||
);
|
);
|
||||||
@@ -3660,6 +3661,10 @@ for (const width of [1200, 390]) {
|
|||||||
await form.getByRole('button', { name: '검색 꺼짐', exact: true }).click();
|
await form.getByRole('button', { name: '검색 꺼짐', exact: true }).click();
|
||||||
const generalSearch = form.locator('#command-search-destGeneralId');
|
const generalSearch = form.locator('#command-search-destGeneralId');
|
||||||
const citySearch = form.locator('#command-search-destCityId');
|
const citySearch = form.locator('#command-search-destCityId');
|
||||||
|
for (const query of ['ㅇㅇ', '없음', '통솔', '1,200']) {
|
||||||
|
await generalSearch.fill(query);
|
||||||
|
await expect(form.getByTestId('general-target-list').locator('button')).toHaveCount(0);
|
||||||
|
}
|
||||||
await generalSearch.fill('ㅊㄹㄷ');
|
await generalSearch.fill('ㅊㄹㄷ');
|
||||||
await expect(form.getByTestId('general-target-list')).toContainText('청룡대');
|
await expect(form.getByTestId('general-target-list')).toContainText('청룡대');
|
||||||
await generalSearch.fill('ㄱㅇ');
|
await generalSearch.fill('ㄱㅇ');
|
||||||
@@ -3722,3 +3727,47 @@ for (const width of [1200, 390]) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('gift search indexes nullable names without matching the displayed no-troop fallback', async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
const targets = buildRefGeneralTargetOptions({
|
||||||
|
actorId: 1,
|
||||||
|
actorNationId: 1,
|
||||||
|
generals: [
|
||||||
|
{ id: 1, name: '관우', nationId: 1, cityId: 1, troopId: 0, npcState: 0, officerLevel: 5 },
|
||||||
|
{ id: 2, name: '원우', nationId: 1, cityId: 1, troopId: 0, npcState: 0, officerLevel: 5 },
|
||||||
|
{ id: 3, name: '조조', nationId: 1, cityId: 1, troopId: 3, npcState: 0, officerLevel: 5 },
|
||||||
|
],
|
||||||
|
nationNames: new Map([[1, '피곤']]),
|
||||||
|
cityNames: new Map([[1, '업']]),
|
||||||
|
troopNames: new Map([[3, '원위대']]),
|
||||||
|
});
|
||||||
|
await install(page, false, {
|
||||||
|
...commandTable,
|
||||||
|
general: [{ category: '인사', values: [buildGeneralCommand('che_증여', '증여')] }],
|
||||||
|
inputOptions: { ...inputOptions, ...targets },
|
||||||
|
});
|
||||||
|
await page.goto(gamePath('/'));
|
||||||
|
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
|
||||||
|
const picker = page.getByTestId('command-picker');
|
||||||
|
await picker.getByRole('button', { name: /증여/ }).click();
|
||||||
|
const form = picker.getByTestId('command-argument-form');
|
||||||
|
const results = form.getByTestId('general-target-list');
|
||||||
|
await expect(results.locator('button')).toHaveCount(3);
|
||||||
|
await expect(results.getByRole('button', { name: /관우/ })).toContainText('탑승 부대 없음');
|
||||||
|
await form.getByRole('button', { name: '검색 꺼짐', exact: true }).click();
|
||||||
|
const search = form.locator('input[type=search]');
|
||||||
|
await search.fill('ㅇㅇ');
|
||||||
|
await expect(results.locator('button strong')).toHaveText(['원우 (피곤 · 업)', '조조 (피곤 · 업)']);
|
||||||
|
await expect(form.locator('#command-arg-destGeneralId')).toHaveValue('1');
|
||||||
|
await form.screenshot({ path: testInfo.outputPath('gift-real-name-search.png') });
|
||||||
|
await writeFile(
|
||||||
|
testInfo.outputPath('gift-real-name-search.html'),
|
||||||
|
await form.evaluate((element) => element.outerHTML)
|
||||||
|
);
|
||||||
|
await search.fill('없음');
|
||||||
|
await expect(results.locator('button')).toHaveCount(0);
|
||||||
|
await search.fill('피곤');
|
||||||
|
await expect(results.locator('button')).toHaveCount(3);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
export type CommandOption = {
|
export type CommandOption = {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
label: string;
|
label: string;
|
||||||
|
/** 원본 이름. 없는 소속/부대/수도는 null이며 대체 문구는 화면에서 표시한다. */
|
||||||
|
targetNames?: {
|
||||||
|
name: string;
|
||||||
|
nationName?: string | null;
|
||||||
|
cityName?: string | null;
|
||||||
|
troopName?: string | null;
|
||||||
|
capitalName?: string | null;
|
||||||
|
};
|
||||||
color?: string;
|
color?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
availableNow?: boolean;
|
availableNow?: boolean;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, watch, type CSSProperties } from 'vue';
|
import { computed, reactive, watch, type CSSProperties } from 'vue';
|
||||||
|
import { commandTargetDescription } from '../../utils/commandTargetDescription';
|
||||||
import { useStorage } from '@vueuse/core';
|
import { useStorage } from '@vueuse/core';
|
||||||
import { buildCommandTargetSearchIndex, matchesCommandTargetSearch } from '../../utils/commandTargetSearch';
|
import { buildCommandOptionSearchIndex, matchesCommandTargetSearch } from '../../utils/commandTargetSearch';
|
||||||
import MapViewer from './MapViewer.vue';
|
import MapViewer from './MapViewer.vue';
|
||||||
import NationColorSelect from './NationColorSelect.vue';
|
import NationColorSelect from './NationColorSelect.vue';
|
||||||
import { commandArgumentPresentation, resolveCommandArgumentMapTarget } from '../command/commandArgumentPresentation';
|
import { commandArgumentPresentation, resolveCommandArgumentMapTarget } from '../command/commandArgumentPresentation';
|
||||||
@@ -82,9 +83,7 @@ const targetIndexes = computed(
|
|||||||
field.key,
|
field.key,
|
||||||
optionsFor(field).map((option) => ({
|
optionsFor(field).map((option) => ({
|
||||||
option,
|
option,
|
||||||
index: [option.label, option.description ?? '']
|
index: buildCommandOptionSearchIndex(option),
|
||||||
.filter(Boolean)
|
|
||||||
.flatMap(buildCommandTargetSearchIndex),
|
|
||||||
})),
|
})),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
@@ -578,7 +577,7 @@ watch(
|
|||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
field.kind === 'select' &&
|
field.kind === 'select' &&
|
||||||
(selectedOptionFor(field)?.description || selectedOptionFor(field)?.color)
|
(commandTargetDescription(commandKey, selectedOptionFor(field)) || selectedOptionFor(field)?.color)
|
||||||
"
|
"
|
||||||
class="option-detail"
|
class="option-detail"
|
||||||
:class="{ 'assignment-detail': commandKey === 'che_발령' && field.optionSource === 'generals' }"
|
:class="{ 'assignment-detail': commandKey === 'che_발령' && field.optionSource === 'generals' }"
|
||||||
@@ -589,7 +588,7 @@ watch(
|
|||||||
:style="{ backgroundColor: selectedOptionFor(field)?.color }"
|
:style="{ backgroundColor: selectedOptionFor(field)?.color }"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span>{{ selectedOptionFor(field)?.description }}</span>
|
<span>{{ commandTargetDescription(commandKey, selectedOptionFor(field)) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="searchEnabled && isSearchable(field)"
|
v-if="searchEnabled && isSearchable(field)"
|
||||||
@@ -603,7 +602,7 @@ watch(
|
|||||||
type="search"
|
type="search"
|
||||||
inputmode="search"
|
inputmode="search"
|
||||||
enterkeyhint="done"
|
enterkeyhint="done"
|
||||||
placeholder="이름·정보 또는 초성"
|
placeholder="이름 또는 초성"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:spellcheck="false"
|
:spellcheck="false"
|
||||||
@keydown.enter.prevent="($event.target as HTMLInputElement).blur()"
|
@keydown.enter.prevent="($event.target as HTMLInputElement).blur()"
|
||||||
@@ -653,7 +652,7 @@ watch(
|
|||||||
<span class="target-state">{{
|
<span class="target-state">{{
|
||||||
option.availableNow === false ? '현재 불가' : option.availableNow ? '우선 대상' : '대상'
|
option.availableNow === false ? '현재 불가' : option.availableNow ? '우선 대상' : '대상'
|
||||||
}}</span>
|
}}</span>
|
||||||
<small>{{ option.description }}</small>
|
<small>{{ commandTargetDescription(commandKey, option) }}</small>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { CommandOption } from '../components/command/types';
|
||||||
|
|
||||||
|
export const commandTargetDescription = (commandKey: string, option?: CommandOption): string => {
|
||||||
|
if (!option) return '';
|
||||||
|
const description = option.description ?? '';
|
||||||
|
// 이전 API는 완성된 description을 주므로 새 원본 필드가 없으면 그대로 표시한다.
|
||||||
|
if (option.targetNames?.troopName === undefined || commandKey === 'che_포상' || commandKey === 'che_몰수') {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
const troopName = option.targetNames.troopName ?? (option.troopId ? `#${option.troopId}` : '없음');
|
||||||
|
const leader = option.troopId && option.troopId === option.value ? ' (부대장)' : '';
|
||||||
|
const troop = `탑승 부대 ${troopName}${leader}`;
|
||||||
|
return commandKey === 'che_발령'
|
||||||
|
? [troop, description].filter(Boolean).join('\n')
|
||||||
|
: [description, troop].filter(Boolean).join(' · ');
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { CommandOption } from '../components/command/types';
|
||||||
|
|
||||||
// Ref convertSearch초성: 원문, 두벌식 초성, 한글 초성, 두 단계 IME 겹자음 인덱스.
|
// Ref convertSearch초성: 원문, 두벌식 초성, 한글 초성, 두 단계 IME 겹자음 인덱스.
|
||||||
const initials = 'ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ';
|
const initials = 'ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ';
|
||||||
const keyboard = 'rRseEfaqQtTdwWczxvg';
|
const keyboard = 'rRseEfaqQtTdwWczxvg';
|
||||||
@@ -45,3 +47,9 @@ export const matchesCommandTargetSearch = (index: readonly string[], query: stri
|
|||||||
const normalized = normalize(query);
|
const normalized = normalize(query);
|
||||||
return index.some((text) => text.includes(normalized));
|
return index.some((text) => text.includes(normalized));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 구 API 응답은 표시 이름까지만 검색한다. description은 결코 검색하지 않는다.
|
||||||
|
export const buildCommandOptionSearchIndex = (option: Pick<CommandOption, 'label' | 'targetNames'>): string[] =>
|
||||||
|
(option.targetNames ? Object.values(option.targetNames) : [option.label])
|
||||||
|
.filter((name): name is string => typeof name === 'string' && Boolean(name.trim()))
|
||||||
|
.flatMap(buildCommandTargetSearchIndex);
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import { commandTargetDescription } from '../src/utils/commandTargetDescription.ts';
|
||||||
|
|
||||||
|
void test('nullable troop names receive display-only fallbacks and preserve leader and member information', () => {
|
||||||
|
const base = { value: 1, label: '관우', description: '금 100', targetNames: { name: '관우', troopName: null } };
|
||||||
|
assert.equal(commandTargetDescription('che_증여', base), '금 100 · 탑승 부대 없음');
|
||||||
|
assert.equal(commandTargetDescription('che_발령', base), '탑승 부대 없음\n금 100');
|
||||||
|
assert.equal(commandTargetDescription('che_포상', base), '금 100');
|
||||||
|
assert.equal(commandTargetDescription('che_몰수', base), '금 100');
|
||||||
|
assert.equal(commandTargetDescription('che_증여', { ...base, troopId: 99 }), '금 100 · 탑승 부대 #99');
|
||||||
|
assert.equal(
|
||||||
|
commandTargetDescription('che_증여', {
|
||||||
|
...base,
|
||||||
|
troopId: 1,
|
||||||
|
targetNames: { name: '관우', troopName: '청룡대' },
|
||||||
|
}),
|
||||||
|
'금 100 · 탑승 부대 청룡대 (부대장)'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
commandTargetDescription('che_증여', {
|
||||||
|
...base,
|
||||||
|
targetNames: undefined,
|
||||||
|
description: '금 100 · 탑승 부대 없음',
|
||||||
|
}),
|
||||||
|
'금 100 · 탑승 부대 없음'
|
||||||
|
);
|
||||||
|
assert.equal(commandTargetDescription('che_증여'), '');
|
||||||
|
});
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import { buildCommandTargetSearchIndex, matchesCommandTargetSearch } from '../src/utils/commandTargetSearch.ts';
|
import {
|
||||||
|
buildCommandTargetSearchIndex,
|
||||||
|
buildCommandOptionSearchIndex,
|
||||||
|
matchesCommandTargetSearch,
|
||||||
|
} from '../src/utils/commandTargetSearch.ts';
|
||||||
|
|
||||||
void test('Ref name index includes keyboard initials and both IME compound levels', () => {
|
void test('Ref name index includes keyboard initials and both IME compound levels', () => {
|
||||||
assert.deepEqual(buildCommandTargetSearchIndex('강서'), ['강서', 'rt', 'ㄱㅅ', 'ㄳ']);
|
assert.deepEqual(buildCommandTargetSearchIndex('강서'), ['강서', 'rt', 'ㄱㅅ', 'ㄳ']);
|
||||||
@@ -19,3 +23,34 @@ void test('literal, initial, keyboard and normalized queries preserve meaningful
|
|||||||
assert.equal(matchesCommandTargetSearch(buildCommandTargetSearchIndex('쌍성'), 'ㅆㅅ'), true);
|
assert.equal(matchesCommandTargetSearch(buildCommandTargetSearchIndex('쌍성'), 'ㅆㅅ'), true);
|
||||||
assert.equal(matchesCommandTargetSearch(buildCommandTargetSearchIndex('쌍성'), 'ㅅㅅ'), false);
|
assert.equal(matchesCommandTargetSearch(buildCommandTargetSearchIndex('쌍성'), 'ㅅㅅ'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void test('explicit names exclude descriptive copy and placeholders without blacklisting genuine names', () => {
|
||||||
|
const option = {
|
||||||
|
label: '관우 (무소속 · 재야)',
|
||||||
|
targetNames: { name: '관우', troopName: '청룡대', nationName: null, cityName: null },
|
||||||
|
description: '탑승 부대 없음 · 현재 불가 · 금 100',
|
||||||
|
};
|
||||||
|
const index = buildCommandOptionSearchIndex(option);
|
||||||
|
for (const query of ['ㅇㅇ', '없음', '무소속', '재야', '현재', '100'])
|
||||||
|
assert.equal(matchesCommandTargetSearch(index, query), false, query);
|
||||||
|
for (const query of ['관우', 'ㄱㅇ', '청룡대', 'ㅊㄹㄷ'])
|
||||||
|
assert.equal(matchesCommandTargetSearch(index, query), true, query);
|
||||||
|
assert.equal(
|
||||||
|
matchesCommandTargetSearch(
|
||||||
|
buildCommandOptionSearchIndex({ label: '별명', targetNames: { name: '없음', troopName: null } }),
|
||||||
|
'ㅇㅇ'
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
matchesCommandTargetSearch(
|
||||||
|
buildCommandOptionSearchIndex({ label: '관우', ...{ description: '부대 없음' } }),
|
||||||
|
'ㅇㅇ'
|
||||||
|
),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
buildCommandOptionSearchIndex({ label: '누락 안내', targetNames: { name: '', troopName: null } }),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user