장수 목록 기본 정렬 순서를 통일

This commit is contained in:
2026-08-30 12:19:17 +00:00
parent c56610b179
commit dd66da14f4
9 changed files with 110 additions and 44 deletions
+26 -17
View File
@@ -109,14 +109,14 @@ const installFixture = async (page: Page, state: FixtureState): Promise<void> =>
state.generalRequests += 1;
return response([
{
id: 1,
name: '유비',
npcState: 0,
nationId: 1,
nationName: '',
leadership: 72,
strength: 67,
intelligence: 76,
id: 3,
name: '황건장수',
npcState: 2,
nationId: 0,
nationName: '무주',
leadership: 55,
strength: 65,
intelligence: 45,
},
{
id: 2,
@@ -129,14 +129,14 @@ const installFixture = async (page: Page, state: FixtureState): Promise<void> =>
intelligence: 80,
},
{
id: 3,
name: '황건장수',
npcState: 2,
nationId: 0,
nationName: '무주',
leadership: 55,
strength: 65,
intelligence: 45,
id: 1,
name: '유비',
npcState: 0,
nationId: 1,
nationName: '',
leadership: 72,
strength: 67,
intelligence: 76,
},
]);
}
@@ -336,7 +336,11 @@ test('prioritizes core general fields and keeps context and inheritance progress
const generalTab = page.getByRole('tab', { name: '장수 목록' });
await generalTab.click();
await expect(page.locator('.context-general-table tbody tr')).toHaveCount(3);
const generalRows = page.locator('.context-general-table tbody tr');
await expect(generalRows).toHaveCount(3);
await expect(generalRows.nth(0)).toContainText('유비');
await expect(generalRows.nth(1)).toContainText('조조');
await expect(generalRows.nth(2)).toContainText('황건장수');
expect(state.generalRequests).toBe(1);
await page.getByPlaceholder('장수명 또는 국가 검색').fill('촉');
await expect(page.locator('.context-general-table tbody tr')).toHaveCount(1);
@@ -386,6 +390,11 @@ test('keeps the primary creation flow readable without horizontal overflow on mo
expect(mobileStatButtons).toHaveLength(4);
expect(mobileStatButtons.every(({ width, height }) => width >= 160 && height === 40)).toBe(true);
await expect(page.locator('.advanced-options')).not.toHaveAttribute('open');
await page.getByRole('tab', { name: '장수 목록' }).click();
const mobileGeneralRows = page.locator('.context-general-table tbody tr');
await expect(mobileGeneralRows.nth(0)).toContainText('유비');
await expect(mobileGeneralRows.nth(1)).toContainText('조조');
await expect(mobileGeneralRows.nth(2)).toContainText('황건장수');
await page.getByRole('tab', { name: '임관 권유' }).focus();
await expect(page.getByRole('tab', { name: '임관 권유' })).toBeFocused();
await page.screenshot({ path: testInfo.outputPath('join-layout-mobile.png'), fullPage: true });
@@ -0,0 +1,17 @@
export type GeneralOrderIdentity = {
id: number;
name: string;
npcState: number;
};
const koreanNameCollator = new Intl.Collator('ko-KR', { numeric: true, sensitivity: 'base' });
/**
* Ref의 npc 값은 0 유저장, 1 빙의장, 2 N장, 3 M장, 4 의병장,
* 5 부대장 순으로 장수 종류 자체의 표시 우선순위를 표현한다.
*/
export const compareGeneralTypeThenName = <T extends GeneralOrderIdentity>(left: T, right: T): number =>
left.npcState - right.npcState || koreanNameCollator.compare(left.name, right.name) || left.id - right.id;
export const sortGeneralsByTypeThenName = <T extends GeneralOrderIdentity>(generals: readonly T[]): T[] =>
[...generals].sort(compareGeneralTypeThenName);
@@ -7,6 +7,7 @@ import BattleGeneralCard from '../components/battle/BattleGeneralCard.vue';
import { useGameFeedback } from '../composables/useGameFeedback';
import { trpc } from '../utils/trpc';
import { getNpcColor } from '../utils/npcColor';
import { compareGeneralTypeThenName } from '../utils/generalOrder';
import type { BattleSimOptions, GeneralDraft, InheritBuff } from '../utils/battleSimulatorTypes';
import { BattleSimulatorWorkerClient } from '../utils/battleSimulatorWorkerClient';
import { formatSeoulDateTime } from '../utils/legacyDateTime';
@@ -953,12 +954,7 @@ const generalGroups = computed(() => {
.map((nationId) => {
const nation = nationMap.get(nationId) ?? { id: nationId, name: '재야', color: '#000000' };
const generals = [...(generalList.value?.generalsByNation[nationId] ?? [])];
generals.sort((lhs, rhs) => {
if (lhs.npcState !== rhs.npcState) {
return lhs.npcState - rhs.npcState;
}
return lhs.name.localeCompare(rhs.name);
});
generals.sort(compareGeneralTypeThenName);
return { nation, generals };
});
});
+9 -8
View File
@@ -6,6 +6,7 @@ import PanelCard from '../components/ui/PanelCard.vue';
import SkeletonLines from '../components/ui/SkeletonLines.vue';
import MapViewer from '../components/main/MapViewer.vue';
import { trpc } from '../utils/trpc';
import { sortGeneralsByTypeThenName } from '../utils/generalOrder';
import { useSessionStore } from '../stores/session';
import { cityLevelMap, formatOfficerLevelText, regionMap } from '../utils/nationFormat';
import { getNpcColor } from '../utils/npcColor';
@@ -247,14 +248,14 @@ const npcGeneralRows = computed<NpcGeneralRow[]>(() => {
const visibleNpcGeneralRows = computed(() => npcGeneralRows.value.slice(0, npcGeneralListVisibleCount.value));
const filteredPublicGenerals = computed(() => {
const keyword = publicGeneralFilter.value.trim().toLocaleLowerCase('ko-KR');
if (!keyword) {
return publicGenerals.value;
}
return publicGenerals.value.filter(
(general) =>
general.name.toLocaleLowerCase('ko-KR').includes(keyword) ||
general.nationName.toLocaleLowerCase('ko-KR').includes(keyword)
);
const filtered = keyword
? publicGenerals.value.filter(
(general) =>
general.name.toLocaleLowerCase('ko-KR').includes(keyword) ||
general.nationName.toLocaleLowerCase('ko-KR').includes(keyword)
)
: publicGenerals.value;
return sortGeneralsByTypeThenName(filtered);
});
const npcValidColor = computed(() => {
const remaining = npcValidUntilMs.value - nowMs.value;
@@ -8,6 +8,7 @@ import type { CommandTable } from '../components/command/types';
import LegacySortControls from '../components/ui/LegacySortControls.vue';
import { useGameFeedback } from '../composables/useGameFeedback';
import { getNpcColor } from '../utils/npcColor';
import { sortGeneralsByTypeThenName } from '../utils/generalOrder';
import { legacyNationTextColor } from '../utils/legacyNationColor';
import { cityLevelMap, regionMap } from '../utils/nationFormat';
import { trpc } from '../utils/trpc';
@@ -62,9 +63,10 @@ const sortOptions = [
const officerLabels: Record<OfficerLevel, string> = { 4: '태수', 3: '군사', 2: '종사' };
const appointmentDescription = (city: City, general: SecretGeneral, level: OfficerLevel): string =>
`${JosaUtil.put(general.name, '을')} ${city.name} ${JosaUtil.put(officerLabels[level], '으로')} 임명`;
const generalsForCity = (cityId: number) => data.value?.generals.filter((general) => general.cityId === cityId) ?? [];
const generalsForCity = (cityId: number) =>
sortGeneralsByTypeThenName(data.value?.generals.filter((general) => general.cityId === cityId) ?? []);
const secretGeneralsForCity = (cityId: number) =>
secretData.value?.generals.filter((general) => general.cityId === cityId) ?? [];
sortGeneralsByTypeThenName(secretData.value?.generals.filter((general) => general.cityId === cityId) ?? []);
const displayGeneralName = (general: Result['generals'][number]) =>
general.npcState > 0 && !/^[ⓜⓝ]/u.test(general.name) ? `${general.name}` : general.name;
const displaySecretGeneralName = (general: SecretGeneral) =>
@@ -6,6 +6,7 @@ import { formatReservedCommandBrief } from '../components/command/reservedComman
import type { CommandTable } from '../components/command/types';
import { formatOfficerLevelText } from '../utils/nationFormat';
import { getNpcColor } from '../utils/npcColor';
import { compareGeneralTypeThenName } from '../utils/generalOrder';
import { resolveGeneralIconUrl } from '../utils/generalIcon';
import {
DISPLAY_SETTINGS_KEY,
@@ -587,7 +588,7 @@ const generals = computed(() => {
const compared = compareGridValues(sortValue(left, sort.colId), sortValue(right, sort.colId));
if (compared) return sort.sort === 'asc' ? compared : -compared;
}
return left.id - right.id;
return compareGeneralTypeThenName(left, right);
});
});
@@ -3,6 +3,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import GeneralDirectoryTable from '../components/directory/GeneralDirectoryTable.vue';
import type { GeneralDirectoryGeneral } from '../types/directory';
import { sortGeneralsByTypeThenName } from '../utils/generalOrder';
import { formatNationLevelText, formatOfficerLevelText } from '../utils/nationFormat';
import { getNpcColor } from '../utils/npcColor';
import { legacyNationTextColor } from '../utils/legacyNationColor';
@@ -46,6 +47,7 @@ const officerName = (nation: Nation, officerLevel: number) =>
nation.officers.find((officer) => officer.officerLevel === officerLevel)?.general;
const displayGeneralName = (general: { name: string; npcState: number }) =>
general.npcState > 0 && !/^[ⓜⓝ㉥]/u.test(general.name) ? `${general.name}` : general.name;
const orderedNationGenerals = (nation: Nation) => sortGeneralsByTypeThenName(nation.generals);
const displayAnalyzedGeneralColor = (general: { npcState: number; accessGrade: 'normal' | 'medium' | 'high' }) => {
if (general.accessGrade === 'high') return 'yellow';
if (general.accessGrade === 'medium') return 'lightgreen';
@@ -344,7 +346,7 @@ onBeforeUnmount(() => {
<tr>
<td colspan="5">
장수 일람 :
<template v-for="general in nation.generals" :key="general.id">
<template v-for="general in orderedNationGenerals(nation)" :key="general.id">
<button
type="button"
class="general-preview-trigger"
+7 -9
View File
@@ -6,6 +6,7 @@ import SkeletonLines from '../components/ui/SkeletonLines.vue';
import MapViewer from '../components/main/MapViewer.vue';
import RecentLogList from '../components/main/RecentLogList.vue';
import { trpc } from '../utils/trpc';
import { sortGeneralsByTypeThenName } from '../utils/generalOrder';
import { useSessionStore } from '../stores/session';
type MapData = Awaited<ReturnType<typeof trpc.public.getCachedMap.query>>;
@@ -30,15 +31,12 @@ const generalFilter = ref('');
const filteredGenerals = computed(() => {
const keyword = generalFilter.value.trim().toLowerCase();
if (!keyword) {
return generalList.value;
}
return generalList.value.filter((general) => {
return (
general.name.toLowerCase().includes(keyword) ||
general.nationName.toLowerCase().includes(keyword)
);
});
const filtered = keyword
? generalList.value.filter((general) => {
return general.name.toLowerCase().includes(keyword) || general.nationName.toLowerCase().includes(keyword);
})
: generalList.value;
return sortGeneralsByTypeThenName(filtered);
});
const resolveErrorMessage = (value: unknown): string => {
@@ -0,0 +1,40 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { compareGeneralTypeThenName, sortGeneralsByTypeThenName } from '../src/utils/generalOrder.ts';
void describe('general display order', () => {
void it('orders general types before Korean names and ids', () => {
const source = [
{ id: 8, name: '㉥부대장', npcState: 5 },
{ id: 7, name: 'ⓖ의병장', npcState: 4 },
{ id: 6, name: 'ⓜ마초', npcState: 3 },
{ id: 5, name: 'ⓝ조조', npcState: 2 },
{ id: 4, name: 'ⓝ빙의장', npcState: 1 },
{ id: 3, name: '하후돈', npcState: 0 },
{ id: 2, name: '가후', npcState: 0 },
{ id: 1, name: '가후', npcState: 0 },
];
assert.deepEqual(
sortGeneralsByTypeThenName(source).map((general) => general.id),
[1, 2, 3, 4, 5, 6, 7, 8]
);
assert.deepEqual(
source.map((general) => general.id),
[8, 7, 6, 5, 4, 3, 2, 1]
);
});
void it('keeps later special NPC types in their persisted numeric order', () => {
assert.deepEqual(
[
{ id: 9, name: 'ⓞ오랑캐', npcState: 9 },
{ id: 6, name: 'ⓤ중립장', npcState: 6 },
]
.sort(compareGeneralTypeThenName)
.map((general) => general.npcState),
[6, 9]
);
});
});