장수 목록 기본 정렬 순서를 통일
This commit is contained in:
@@ -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 };
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user