From dd66da14f42f8bd2aa16c3ef4f2579409ce3efeb Mon Sep 17 00:00:00 2001 From: hided62 Date: Sun, 30 Aug 2026 12:19:17 +0000 Subject: [PATCH] =?UTF-8?q?=EC=9E=A5=EC=88=98=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=A0=95=EB=A0=AC=20=EC=88=9C=EC=84=9C?= =?UTF-8?q?=EB=A5=BC=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/game-frontend/e2e/joinLayout.spec.ts | 43 +++++++++++-------- app/game-frontend/src/utils/generalOrder.ts | 17 ++++++++ .../src/views/BattleSimulatorView.vue | 8 +--- app/game-frontend/src/views/JoinView.vue | 17 ++++---- .../src/views/NationCitiesView.vue | 6 ++- .../src/views/NationGeneralsView.vue | 3 +- .../src/views/NationListView.vue | 4 +- app/game-frontend/src/views/PublicView.vue | 16 +++---- app/game-frontend/test/generalOrder.test.ts | 40 +++++++++++++++++ 9 files changed, 110 insertions(+), 44 deletions(-) create mode 100644 app/game-frontend/src/utils/generalOrder.ts create mode 100644 app/game-frontend/test/generalOrder.test.ts diff --git a/app/game-frontend/e2e/joinLayout.spec.ts b/app/game-frontend/e2e/joinLayout.spec.ts index e7d8615c..ec544988 100644 --- a/app/game-frontend/e2e/joinLayout.spec.ts +++ b/app/game-frontend/e2e/joinLayout.spec.ts @@ -109,14 +109,14 @@ const installFixture = async (page: Page, state: FixtureState): Promise => 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 => 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 }); diff --git a/app/game-frontend/src/utils/generalOrder.ts b/app/game-frontend/src/utils/generalOrder.ts new file mode 100644 index 00000000..ce159030 --- /dev/null +++ b/app/game-frontend/src/utils/generalOrder.ts @@ -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 = (left: T, right: T): number => + left.npcState - right.npcState || koreanNameCollator.compare(left.name, right.name) || left.id - right.id; + +export const sortGeneralsByTypeThenName = (generals: readonly T[]): T[] => + [...generals].sort(compareGeneralTypeThenName); diff --git a/app/game-frontend/src/views/BattleSimulatorView.vue b/app/game-frontend/src/views/BattleSimulatorView.vue index bfca6055..48d26bdf 100644 --- a/app/game-frontend/src/views/BattleSimulatorView.vue +++ b/app/game-frontend/src/views/BattleSimulatorView.vue @@ -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 }; }); }); diff --git a/app/game-frontend/src/views/JoinView.vue b/app/game-frontend/src/views/JoinView.vue index ff5a5a86..38e70fda 100644 --- a/app/game-frontend/src/views/JoinView.vue +++ b/app/game-frontend/src/views/JoinView.vue @@ -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(() => { 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; diff --git a/app/game-frontend/src/views/NationCitiesView.vue b/app/game-frontend/src/views/NationCitiesView.vue index 3d110e3a..689884cc 100644 --- a/app/game-frontend/src/views/NationCitiesView.vue +++ b/app/game-frontend/src/views/NationCitiesView.vue @@ -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 = { 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) => diff --git a/app/game-frontend/src/views/NationGeneralsView.vue b/app/game-frontend/src/views/NationGeneralsView.vue index 9108f193..45625f80 100644 --- a/app/game-frontend/src/views/NationGeneralsView.vue +++ b/app/game-frontend/src/views/NationGeneralsView.vue @@ -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); }); }); diff --git a/app/game-frontend/src/views/NationListView.vue b/app/game-frontend/src/views/NationListView.vue index 51d57317..966e52e4 100644 --- a/app/game-frontend/src/views/NationListView.vue +++ b/app/game-frontend/src/views/NationListView.vue @@ -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(() => { 장수 일람 : -