diff --git a/app/game-engine/src/turn/reservedTurnHandler.ts b/app/game-engine/src/turn/reservedTurnHandler.ts index 270f0fdf..5a3d4438 100644 --- a/app/game-engine/src/turn/reservedTurnHandler.ts +++ b/app/game-engine/src/turn/reservedTurnHandler.ts @@ -1993,7 +1993,7 @@ export const createReservedTurnHandler = async (options: { let deleteGeneral = false; const deletedTroopIds = Array.from(commandDeletedTroopIds); const lifecycleSnapshot = cloneTurnGeneral(currentGeneral); - if (currentGeneral.meta.killturn <= 0 && typeof currentGeneral.deadYear === 'number') { + if (currentGeneral.meta.killturn <= 0) { if ( currentGeneral.npcState === 1 && typeof currentGeneral.deadYear === 'number' && diff --git a/app/game-engine/test/generalTurnLifecycle.test.ts b/app/game-engine/test/generalTurnLifecycle.test.ts index 05f6b330..ce7a1eb8 100644 --- a/app/game-engine/test/generalTurnLifecycle.test.ts +++ b/app/game-engine/test/generalTurnLifecycle.test.ts @@ -316,12 +316,13 @@ describe('legacy general turn lifecycle', () => { expect(harness.world.peekDirtyState().deletedGenerals).toContain(1); }); - it('keeps compatibility fixtures without legacy lifespan metadata alive', async () => { + it('deletes an expired NPC even when its in-memory lifespan metadata is missing', async () => { const harness = await createTurnTestHarness({ snapshot: makeSnapshot([ makeGeneral({ deadYear: undefined, - npcState: 2, + npcState: 4, + name: 'ⓖ의병', meta: { killturn: 1 }, }), ]), @@ -332,9 +333,9 @@ describe('legacy general turn lifecycle', () => { await harness.runOneTick(); - expect(harness.world.getGeneralById(1)).not.toBeNull(); - expect(harness.world.getGeneralById(1)!.meta.killturn).toBe(0); - expect(harness.world.peekDirtyState().lifecycleEvents[0]?.outcome).toBe('active'); + expect(harness.world.getGeneralById(1)).toBeNull(); + expect(harness.world.peekDirtyState().deletedGenerals).toContain(1); + expect(harness.world.peekDirtyState().lifecycleEvents[0]?.outcome).toBe('deleted'); }); it('retires a player general and resets inherited stats and rank state', async () => { diff --git a/app/game-frontend/e2e/directoryLists.spec.ts b/app/game-frontend/e2e/directoryLists.spec.ts index 0d84baff..560e2b67 100644 --- a/app/game-frontend/e2e/directoryLists.spec.ts +++ b/app/game-frontend/e2e/directoryLists.spec.ts @@ -130,6 +130,45 @@ const generals = [ }, ]; +const npcGenerals = [ + { + id: 10, + name: '낮은장수', + ownerName: '', + npcState: 0, + level: 4, + nationId: 2, + nationName: '촉', + personality: null, + specialDomestic: null, + specialWar: null, + statTotal: 120, + leadership: 30, + strength: 50, + intelligence: 40, + experience: 100, + dedication: 50, + }, + { + id: 20, + name: '높은장수', + ownerName: '빙의자', + npcState: 1, + level: 8, + nationId: 1, + nationName: '위', + personality: null, + specialDomestic: null, + specialWar: null, + statTotal: 240, + leadership: 90, + strength: 70, + intelligence: 80, + experience: 500, + dedication: 300, + }, +]; + const parseSort = (route: Route): number => { try { const request = route.request(); @@ -224,6 +263,20 @@ const install = async ( sort === 8 ? [...generals].sort((left, right) => left.killturn - right.killturn) : generals; return response({ sort, generals: rows }); } + if (operation === 'public.getNpcList') { + const sort = parseSort(route); + const rows = [...npcGenerals].sort((left, right) => { + if (sort === 2) return left.nationId - right.nationId || left.id - right.id; + if (sort === 3) return right.statTotal - left.statTotal || left.id - right.id; + if (sort === 4) return right.leadership - left.leadership || left.id - right.id; + if (sort === 5) return right.strength - left.strength || left.id - right.id; + if (sort === 6) return right.intelligence - left.intelligence || left.id - right.id; + if (sort === 7) return right.experience - left.experience || left.id - right.id; + if (sort === 8) return right.dedication - left.dedication || left.id - right.id; + return left.name.localeCompare(right.name) || left.id - right.id; + }); + return response({ sort, generals: rows, tokenKeepCounts: {} }); + } return { error: { message: `unhandled ${operation}`, data: { code: 'BAD_REQUEST' } } }; }); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(results) }); @@ -340,7 +393,7 @@ test('nation and general directories preserve the fixed legacy Chromium geometry await expect.poll(() => accessPages).toContain('nation-list'); expect(accessPages).not.toContain('general-list'); - const header = page.locator('.general-table thead td').first(); + const header = page.locator('.general-table thead th').first(); expect(await header.evaluate((element) => getComputedStyle(element).backgroundImage)).toContain('back_green.jpg'); const icon = page.locator('.general-icon').first(); await expect(icon).toBeVisible(); @@ -392,6 +445,78 @@ test('general directory submits the legacy sort selector and keeps wounded/bonus expect(await page.locator('#viewType').evaluate((element) => document.activeElement === element)).toBe(true); }); +test('directory sort controls stay legible in dark mode and sortable headers apply the matching option', async ({ + page, +}, testInfo) => { + await install(page); + await page.goto('general-list'); + + const select = page.locator('#viewType'); + const submit = page.getByRole('button', { name: '정렬하기' }); + const colors = await select.evaluate((element) => { + const selectStyle = getComputedStyle(element); + const optionStyle = getComputedStyle(element.querySelector('option')!); + return { + selectBackground: selectStyle.backgroundColor, + selectColor: selectStyle.color, + optionBackground: optionStyle.backgroundColor, + optionColor: optionStyle.color, + }; + }); + expect(colors).toEqual({ + selectBackground: 'rgb(24, 35, 29)', + selectColor: 'rgb(247, 250, 248)', + optionBackground: 'rgb(24, 35, 29)', + optionColor: 'rgb(247, 250, 248)', + }); + const defaultButton = await submit.evaluate((element) => { + const style = getComputedStyle(element); + return { + background: style.backgroundColor, + color: style.color, + borderBottomWidth: style.borderBottomWidth, + cursor: style.cursor, + }; + }); + expect(defaultButton).toEqual({ + background: 'rgb(55, 90, 127)', + color: 'rgb(255, 255, 255)', + borderBottomWidth: '3px', + cursor: 'pointer', + }); + await submit.hover(); + await page.mouse.down(); + expect(await submit.evaluate((element) => getComputedStyle(element).borderBottomWidth)).toBe('1px'); + await page.mouse.up(); + + await page.getByRole('button', { name: '삭턴 기준 정렬' }).click(); + await expect(select).toHaveValue('8'); + await expect(page.locator('th[aria-sort="ascending"]')).toContainText('삭턴'); + await expect(page.locator('tbody tr[data-general-id]').first()).toHaveAttribute('data-general-id', '20'); + await page.screenshot({ path: testInfo.outputPath('directory-sort-controls-desktop.png'), fullPage: true }); + + await page.setViewportSize({ width: 500, height: 844 }); + await expect(select).toHaveCSS('background-color', 'rgb(24, 35, 29)'); + await expect(submit).toHaveCSS('border-bottom-width', '3px'); + await page.screenshot({ path: testInfo.outputPath('directory-sort-controls-mobile.png'), fullPage: true }); +}); + +test('npc directory reuses the dark sort controls and sorts from a table header', async ({ page }, testInfo) => { + await install(page); + await page.goto('npc-list'); + await expect(page.locator('.npc-table tbody tr[data-general-id]')).toHaveCount(2); + await page.getByRole('button', { name: '통솔 기준 정렬' }).click(); + await expect(page.locator('#npc-list-sort')).toHaveValue('4'); + await expect(page.locator('.npc-table th[aria-sort="descending"]')).toContainText('통솔'); + await expect(page.locator('.npc-table tbody tr[data-general-id]').first()).toHaveAttribute('data-general-id', '20'); + await expect(page.locator('#npc-list-sort')).toHaveCSS('background-color', 'rgb(24, 35, 29)'); + await expect(page.getByRole('button', { name: '정렬하기' })).toHaveCSS('background-color', 'rgb(55, 90, 127)'); + await page.screenshot({ path: testInfo.outputPath('npc-sort-controls-desktop.png'), fullPage: true }); + await page.setViewportSize({ width: 500, height: 844 }); + await expect(page.locator('#npc-list-sort')).toHaveCSS('color', 'rgb(247, 250, 248)'); + await page.screenshot({ path: testInfo.outputPath('npc-sort-controls-mobile.png'), fullPage: true }); +}); + test('nation directory reuses only the public general-directory row on hover and keyboard focus', async ({ page }) => { const requestedOperations: string[] = []; await install(page, 'general', [], requestedOperations); diff --git a/app/game-frontend/e2e/nationCityOfficeIntegration.spec.ts b/app/game-frontend/e2e/nationCityOfficeIntegration.spec.ts index 7cdb7855..04aff5ae 100644 --- a/app/game-frontend/e2e/nationCityOfficeIntegration.spec.ts +++ b/app/game-frontend/e2e/nationCityOfficeIntegration.spec.ts @@ -353,6 +353,15 @@ test('암행부 행을 도시별로 나누고 수뇌의 인사부 즉시 임명 await expect(page.locator('.city-user-table')).toHaveCount(0); await expect(page.getByRole('button', { name: '인사부 연동' })).toHaveCount(0); + const citySort = page.locator('#nation-city-sort'); + await expect(citySort).toHaveCSS('background-color', 'rgb(24, 35, 29)'); + await citySort.selectOption('5'); + await page.getByRole('button', { name: '정렬하기' }).click(); + await expect(page.locator('.city th[aria-sort="descending"]').first()).toContainText('농업'); + await page.getByRole('button', { name: '시세 기준 정렬' }).first().click(); + await expect(citySort).toHaveValue('10'); + await expect(page.locator('.city th[aria-sort="descending"]').first()).toContainText('시세'); + await page.getByRole('button', { name: '암행부 연동' }).click(); await expect(page.locator('.city-user-table')).toHaveCount(2); await expect(page.locator('.city[data-city-id="1"] .city-user-table tr[data-general-id="21"]')).toContainText( diff --git a/app/game-frontend/e2e/nationGeneralSecret.spec.ts b/app/game-frontend/e2e/nationGeneralSecret.spec.ts index 47387c3f..906c40d2 100644 --- a/app/game-frontend/e2e/nationGeneralSecret.spec.ts +++ b/app/game-frontend/e2e/nationGeneralSecret.spec.ts @@ -149,6 +149,30 @@ const install = async (page: Page, secretAllowed = true) => { { action: '휴식', args: {} }, ], }, + { + id: 2, + name: '부유장수', + npcState: 0, + injury: 0, + stats: { leadership: 60, strength: 50, intelligence: 40 }, + leadershipBonus: 0, + experienceLevel: 8, + troopId: 1, + troopName: '제1부대', + gold: 3000, + rice: 1000, + cityId: 2, + cityName: '낙양', + defenceTrain: 80, + defenceTrainText: '◎', + crewTypeId: 2, + crew: 100, + train: 80, + atmos: 80, + killTurn: 3, + turnTime: '2026-01-01T02:02:00.000Z', + reservedCommands: [], + }, ], }); } @@ -381,7 +405,7 @@ test('secret office renders five Ref-style command briefs and the forbidden erro '5 : 휴식', ]); await expect(commandRows.nth(2)).toHaveAttribute('title', '【다른장수】에게 쌀 200을 증여'); - const geometry = await page.locator('#secret-general-list .turns').evaluate((element) => { + const geometry = await page.locator('#secret-general-list .turns').first().evaluate((element) => { const rect = element.getBoundingClientRect(); const style = getComputedStyle(element); return { @@ -416,3 +440,22 @@ test('secret office renders five Ref-style command briefs and the forbidden erro await expect(page.getByRole('alert')).toContainText('권한이 부족합니다.'); await expect(page.locator('#secret-general-list')).toHaveCount(0); }); + +test('secret office applies the selected sort on submit and immediately from sortable headers', async ({ page }) => { + await install(page); + await page.goto('nation/secret'); + const rows = page.locator('#secret-general-list tbody tr[data-general-id]'); + await expect(rows.first()).toHaveAttribute('data-general-id', '1'); + + await page.locator('#secret-list-sort').selectOption('1'); + await expect(rows.first()).toHaveAttribute('data-general-id', '1'); + await page.getByRole('button', { name: '정렬하기' }).click(); + await expect(rows.first()).toHaveAttribute('data-general-id', '2'); + await expect(page.locator('#secret-general-list th[aria-sort="descending"]')).toContainText('자 금'); + + await page.getByRole('button', { name: '도시 기준 정렬' }).click(); + await expect(page.locator('#secret-list-sort')).toHaveValue('3'); + await expect(rows.first()).toHaveAttribute('data-general-id', '1'); + await expect(page.locator('#secret-list-sort')).toHaveCSS('color', 'rgb(247, 250, 248)'); + await expect(page.getByRole('button', { name: '정렬하기' })).toHaveCSS('border-bottom-width', '3px'); +}); diff --git a/app/game-frontend/src/assets/styles/legacy-controls.css b/app/game-frontend/src/assets/styles/legacy-controls.css index 66ffc7f0..b7111403 100644 --- a/app/game-frontend/src/assets/styles/legacy-controls.css +++ b/app/game-frontend/src/assets/styles/legacy-controls.css @@ -38,6 +38,109 @@ opacity: 0.65; } +/* + * Compact sorting controls used by the Ref-style directory pages. Native + * dark-mode selects vary by browser, so both the closed control and its option + * popup own an explicit high-contrast palette. The submit control keeps a + * raised face and pressed edge without increasing the legacy title row. + */ +.legacy-sort-form { + min-height: 25px; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + margin: 0; +} + +.legacy-sort-select, +.legacy-sort-submit { + box-sizing: border-box; + height: 25px; + font: inherit; +} + +.legacy-sort-select { + min-width: 78px; + border: 1px solid #91a39a; + border-radius: 3px; + padding: 1px 24px 1px 6px; + background-color: #18231d; + color: #f7faf8; + color-scheme: dark; + cursor: pointer; +} + +.legacy-sort-select option { + background-color: #18231d; + color: #f7faf8; +} + +.legacy-sort-select option:checked { + background-color: #375a7f; + color: #fff; +} + +.legacy-sort-submit { + margin-top: 0; + border-color: #27405a; + border-style: solid; + border-width: 0 1px 3px; + border-radius: 3px; + padding: 1px 9px; + background: #375a7f; + color: #fff; + font-weight: 700; + line-height: 21px; + vertical-align: middle; + cursor: pointer; +} + +.legacy-sort-submit:hover { + margin-top: 1px; + border-bottom-width: 2px; +} + +.legacy-sort-submit:active { + margin-top: 2px; + border-bottom-width: 1px; +} + +.legacy-sort-select:focus-visible, +.legacy-sort-submit:focus-visible, +.legacy-sort-header:focus-visible { + outline: 2px solid var(--sammo-color-accent); + outline-offset: 1px; +} + +.legacy-sort-header { + width: 100%; + min-height: 18px; + margin: 0; + border: 0; + padding: 0 2px; + background: transparent; + color: inherit; + font: inherit; + line-height: inherit; + cursor: pointer; +} + +.legacy-sort-header:hover { + background: rgb(255 255 255 / 12%); +} + +.legacy-sort-indicator { + margin-left: 2px; + color: #9ee7ba; + font-size: 0.75em; + opacity: 0.65; +} + +[aria-sort] .legacy-sort-indicator { + opacity: 1; +} + /* * Ref Bootstrap 5.2 + Lumen button family. This class owns the common raised * edge and pressed movement. Semantic modifiers below only select face, edge, diff --git a/app/game-frontend/src/components/directory/GeneralDirectoryTable.vue b/app/game-frontend/src/components/directory/GeneralDirectoryTable.vue index ac0b74ea..141d09cb 100644 --- a/app/game-frontend/src/components/directory/GeneralDirectoryTable.vue +++ b/app/game-frontend/src/components/directory/GeneralDirectoryTable.vue @@ -4,18 +4,51 @@ import { formatOfficerLevelText } from '../../utils/nationFormat'; import { getNpcColor } from '../../utils/npcColor'; import type { GeneralDirectoryGeneral } from '../../types/directory'; -withDefaults( +type SortDirection = 'ascending' | 'descending'; +type Header = { + label: string; + sort?: number; + direction?: SortDirection; + title?: string; +}; + +const props = withDefaults( defineProps<{ generals: GeneralDirectoryGeneral[]; loading?: boolean; layout?: 'responsive' | 'card'; + activeSort?: number; }>(), { loading: false, layout: 'responsive', + activeSort: undefined, } ); +const emit = defineEmits<{ sort: [value: number] }>(); + +const headers: ReadonlyArray
= [ + { label: '얼 굴' }, + { label: '이 름' }, + { label: '연령', sort: 14, direction: 'descending' }, + { label: '성격', sort: 11, direction: 'descending' }, + { label: '특기' }, + { label: '레 벨', sort: 10, direction: 'descending' }, + { label: '국 가', sort: 1, direction: 'ascending' }, + { label: '명 성', sort: 5, direction: 'descending' }, + { label: '계 급', sort: 6, direction: 'descending' }, + { label: '관 직', sort: 7, direction: 'descending' }, + { label: '통솔', sort: 2, direction: 'descending' }, + { label: '무력', sort: 3, direction: 'descending' }, + { label: '지력', sort: 4, direction: 'descending' }, + { label: '삭턴', sort: 8, direction: 'ascending' }, + { label: '벌점', sort: 9, direction: 'descending' }, +]; + +const ariaSort = (header: Header): SortDirection | undefined => + header.sort === props.activeSort ? header.direction : undefined; + const injuredStat = (value: number, injury: number): number => Math.trunc((value * (100 - injury)) / 100); @@ -40,21 +73,28 @@ const injuredStat = (value: number, injury: number): number => Math.trunc((value - 얼 굴 - 이 름 - 연령 - 성격 - 특기 - 레 벨 - 국 가 - 명 성 - 계 급 - 관 직 - 통솔 - 무력 - 지력 - 삭턴 - 벌점 + + + + @@ -232,7 +272,8 @@ const injuredStat = (value: number, injury: number): number => Math.trunc((value line-height: 1.3; word-break: break-all; } -.directory-table td { +.directory-table td, +.directory-table th { border: 1px solid gray; padding: 0; word-break: break-all; @@ -242,6 +283,8 @@ const injuredStat = (value: number, injury: number): number => Math.trunc((value text-align: center; background-color: #14241b; background-image: var(--sammo-texture-green); + color: inherit; + font-weight: 400; } .general-icon { display: inline; diff --git a/app/game-frontend/src/components/ui/LegacySortControls.vue b/app/game-frontend/src/components/ui/LegacySortControls.vue new file mode 100644 index 00000000..2dcfbe16 --- /dev/null +++ b/app/game-frontend/src/components/ui/LegacySortControls.vue @@ -0,0 +1,37 @@ + + + diff --git a/app/game-frontend/src/views/GeneralListView.vue b/app/game-frontend/src/views/GeneralListView.vue index d74ec57b..9ba1e5fc 100644 --- a/app/game-frontend/src/views/GeneralListView.vue +++ b/app/game-frontend/src/views/GeneralListView.vue @@ -3,6 +3,7 @@ import { onMounted, ref } from 'vue'; import { useRouter } from 'vue-router'; import GeneralDirectoryTable from '../components/directory/GeneralDirectoryTable.vue'; +import LegacySortControls from '../components/ui/LegacySortControls.vue'; import type { GeneralDirectoryGeneral } from '../types/directory'; import { trpc } from '../utils/trpc'; @@ -45,6 +46,15 @@ const loadDirectory = async () => { } }; +const updateSort = (value: number): void => { + sort.value = value as SortKey; +}; + +const sortByHeader = (value: number): void => { + updateSort(value); + void loadDirectory(); +}; + onMounted(() => { void loadDirectory(); }); @@ -63,22 +73,21 @@ onMounted(() => { -
- - - -
+ - + @@ -121,13 +130,6 @@ onMounted(() => { padding: 5px 10px; font-size: 14px; } -.sort-form { - margin: 0; -} -.sort-form select, -.sort-form button { - font-size: 14px; -} .directory-error { width: 998px; margin: 0; diff --git a/app/game-frontend/src/views/NationCitiesView.vue b/app/game-frontend/src/views/NationCitiesView.vue index 2bf3987c..2648e12d 100644 --- a/app/game-frontend/src/views/NationCitiesView.vue +++ b/app/game-frontend/src/views/NationCitiesView.vue @@ -4,6 +4,7 @@ import { computed, onMounted, ref } from 'vue'; import { useRouter } from 'vue-router'; import { formatReservedCommandBrief } from '../components/command/reservedCommandBrief'; 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 { legacyNationTextColor } from '../utils/legacyNationColor'; @@ -28,6 +29,7 @@ const secretLoading = ref(false); const personnelLoading = ref(false); const pendingAppointment = ref(''); const sort = ref(10); +const selectedSort = ref(10); const extraSort = ref< | 'name' | 'populationRate' @@ -42,7 +44,20 @@ const extraSort = ref< >(null); const router = useRouter(); const { error: showErrorToast, info: showInfoToast, success: showSuccessToast } = useGameFeedback(); -const options = ['기본', '인구', '인구율', '민심', '농업', '상업', '치안', '수비', '성벽', '시세', '지역', '규모']; +const sortOptions = [ + '기본', + '인구', + '인구율', + '민심', + '농업', + '상업', + '치안', + '수비', + '성벽', + '시세', + '지역', + '규모', +].map((label, index) => ({ value: index + 1, label })); const officerLabels: Record = { 4: '태수', 3: '군사', 2: '종사' }; const generalsForCity = (cityId: number) => data.value?.generals.filter((general) => general.cityId === cityId) ?? []; const secretGeneralsForCity = (cityId: number) => @@ -89,6 +104,20 @@ const cities = computed(() => { const setExtraSort = (value: NonNullable) => { extraSort.value = value; }; +const updateSelectedSort = (value: number): void => { + selectedSort.value = value as Sort; +}; +const applySelectedSort = (): void => { + sort.value = selectedSort.value; + extraSort.value = null; +}; +const sortByHeader = (value: Sort): void => { + selectedSort.value = value; + sort.value = value; + extraSort.value = null; +}; +const sortIndicator = (value: Sort, direction: 'ascending' | 'descending'): string => + sort.value === value && extraSort.value === null ? (direction === 'ascending' ? '▲' : '▼') : '↕'; const remain = (value: number, maximum: number) => value - maximum; const warnRemain = ( kind: 'agriculture' | 'commerce' | 'security' | 'defence' | 'wall', @@ -272,14 +301,14 @@ onMounted(async () => { @@ -337,11 +366,29 @@ onMounted(async () => { - + - + @@ -353,35 +400,80 @@ onMounted(async () => { - + - + - + - + - + - + - + @@ -117,22 +134,94 @@ onMounted(load); - - - - + + + + - - + + - - + + - +
-
- 정렬순서 : - - +
+ @@ -292,7 +321,7 @@ onMounted(async () => { > 인사부 연동 - +
주민 + + {{ city.population }}/{{ city.populationMax }} 인구율 + + {{ Number(((city.population / city.populationMax) * 100).toFixed(2)) }}% {{ city.incomes.wall.toLocaleString() }}
농업 + + {{ city.agriculture }}/{{ city.agricultureMax }}[{{ remain(city.agriculture, city.agricultureMax) }}] 상업 + + {{ city.commerce }}/{{ city.commerceMax }}[{{ remain(city.commerce, city.commerceMax) }}] 치안 + + {{ city.security }}/{{ city.securityMax }}[{{ remain(city.security, city.securityMax) }}] 수비 + + {{ city.defence }}/{{ city.defenceMax }}[{{ remain(city.defence, city.defenceMax) }}] 성벽 + + {{ city.wall }}/{{ city.wallMax }} {
민심 + + {{ city.trust.toFixed(1) }}시세 + + {{ city.trade ?? '-' }}% 태수 @@ -567,6 +677,13 @@ onMounted(async () => { .title { text-align: left; } +.city-sort-actions { + min-height: 25px; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; +} .city { margin-top: 0; } @@ -669,7 +786,7 @@ onMounted(async () => { .footer { margin-top: 0; } -.nation-cities-page button, +.nation-cities-page button:not(.legacy-sort-submit, .legacy-sort-header), .nation-cities-page input[type='submit'] { border: 2px outset #fff; background-color: buttonface; diff --git a/app/game-frontend/src/views/NationSecretView.vue b/app/game-frontend/src/views/NationSecretView.vue index 72f8ac2b..395da3c0 100644 --- a/app/game-frontend/src/views/NationSecretView.vue +++ b/app/game-frontend/src/views/NationSecretView.vue @@ -3,6 +3,7 @@ import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime'; import { computed, onMounted, ref } from 'vue'; import { formatReservedCommandBrief } from '../components/command/reservedCommandBrief'; import type { CommandTable } from '../components/command/types'; +import LegacySortControls from '../components/ui/LegacySortControls.vue'; import { trpc } from '../utils/trpc'; type Result = Awaited>; type ReservedCommand = Result['generals'][number]['reservedCommands'][number]; @@ -12,7 +13,11 @@ const commandTable = ref(null); const error = ref(''); const loading = ref(false); const sort = ref(7); -const options = ['자금', '군량', '도시', '병종', '병사', '삭제턴', '턴', '부대']; +const selectedSort = ref(7); +const sortOptions = ['자금', '군량', '도시', '병종', '병사', '삭제턴', '턴', '부대'].map((label, index) => ({ + value: index + 1, + label, +})); const load = async () => { loading.value = true; error.value = ''; @@ -44,6 +49,18 @@ const displayName = (general: { name: string; npcState: number }) => general.npcState > 0 && !/^[ⓜⓝ㉥]/u.test(general.name) ? `ⓝ${general.name}` : general.name; const commandBrief = (command: ReservedCommand): string => formatReservedCommandBrief('general', command.action, command.args, commandTable.value); +const updateSelectedSort = (value: number): void => { + selectedSort.value = value as Sort; +}; +const applySelectedSort = (): void => { + sort.value = selectedSort.value; +}; +const sortByHeader = (value: Sort): void => { + selectedSort.value = value; + sort.value = value; +}; +const sortIndicator = (value: Sort, direction: 'ascending' | 'descending'): string => + sort.value === value ? (direction === 'ascending' ? '▲' : '▼') : '↕'; onMounted(load); @@ -58,13 +75,13 @@ onMounted(load);
- 정렬순서 : - - +
이 름 통무지부 대자 금군 량도시 + + + + + + + + 병 종병 사 + + + + 훈련 사기 명 령삭턴 + + + +
{{ displayName(general) }}
Lv {{ general.experienceLevel }}
{{ general.stats.leadership @@ -235,19 +324,6 @@ th, border-bottom-width: 2px; } -input[type='submit'] { - cursor: pointer; - padding: 1px 6px; - border: 2px outset #fff; - background: rgb(107, 107, 107); - color: #fff; -} -select { - padding: 0; - border: 1px solid rgb(133, 133, 133); - background: rgb(107, 107, 107); - color: #fff; -} .legacy-bg0 { background-color: transparent; } diff --git a/app/game-frontend/src/views/NpcListView.vue b/app/game-frontend/src/views/NpcListView.vue index 0c884ed6..dfbc82f2 100644 --- a/app/game-frontend/src/views/NpcListView.vue +++ b/app/game-frontend/src/views/NpcListView.vue @@ -1,6 +1,7 @@