fix(game-ui): 턴 입력기 모드를 서버별로 기억한다

This commit is contained in:
2026-08-28 01:12:34 +00:00
parent ca3e028603
commit ab0f1b9cd5
5 changed files with 108 additions and 14 deletions
+60 -4
View File
@@ -763,8 +763,17 @@ const chiefCenter = {
})), })),
}; };
const install = async (page: Page, rejectGeneral = false, commandTableResponse: unknown = commandTable) => { const install = async (
page: Page,
rejectGeneral = false,
commandTableResponse: unknown = commandTable,
generalId = 1
) => {
const requests: unknown[] = []; const requests: unknown[] = [];
const currentGeneralContext = {
...generalContext,
general: { ...generalContext.general, id: generalId },
};
const generalTurns = turns(30); const generalTurns = turns(30);
const nationTurns = turns(12); const nationTurns = turns(12);
let generalRevision = 0; let generalRevision = 0;
@@ -811,7 +820,7 @@ const install = async (page: Page, rejectGeneral = false, commandTableResponse:
? { ? {
kind: 'snapshot', kind: 'snapshot',
revision: 'AAAAAAAAAAAAAAAAAAAAAA', revision: 'AAAAAAAAAAAAAAAAAAAAAA',
data: generalContext, data: currentGeneralContext,
} }
: { kind: 'unchanged', revision: 'AAAAAAAAAAAAAAAAAAAAAA' }, : { kind: 'unchanged', revision: 'AAAAAAAAAAAAAAAAAAAAAA' },
commandTable: initial commandTable: initial
@@ -830,7 +839,7 @@ const install = async (page: Page, rejectGeneral = false, commandTableResponse:
: { kind: 'unchanged', revision: 'CCCCCCCCCCCCCCCCCCCCCC' }, : { kind: 'unchanged', revision: 'CCCCCCCCCCCCCCCCCCCCCC' },
}); });
} }
if (name === 'general.me') return response(generalContext); if (name === 'general.me') return response(currentGeneralContext);
if (name === 'world.getMapLayout') if (name === 'world.getMapLayout')
return response({ return response({
mapName: 'che', mapName: 'che',
@@ -844,7 +853,7 @@ const install = async (page: Page, rejectGeneral = false, commandTableResponse:
if (name === 'auth.status') return response({ ok: true }); if (name === 'auth.status') return response({ ok: true });
if (name === 'lobby.info') if (name === 'lobby.info')
return response({ return response({
myGeneral: { id: 1, name: '장수' }, myGeneral: { id: generalId, name: '장수' },
year: 200, year: 200,
month: 1, month: 1,
turnTerm: 10, turnTerm: 10,
@@ -1376,6 +1385,53 @@ test('keeps general and chief command categories after input and across page rel
}); });
}); });
test('keeps the general turn editor mode across general recreation within one server profile', async ({
page,
context,
}) => {
await install(page, false, commandTable, 1);
await page.addInitScript(() => {
localStorage.setItem('core2026:general:1:editMode', '1');
});
await page.setViewportSize({ width: 1200, height: 900 });
await page.goto('/');
const profileModeKey = 'core2026:profile:che:general-turn-editor:editMode';
const firstEditor = page.locator('[data-command-scope="general"]');
await expect(firstEditor.getByRole('button', { name: '일반 모드', exact: true })).toBeVisible();
await expect.poll(() => page.evaluate((key) => localStorage.getItem(key), profileModeKey)).toBe('1');
await page.evaluate(() => localStorage.removeItem('core2026:general:1:editMode'));
const recreatedPage = await context.newPage();
await install(recreatedPage, false, commandTable, 2);
await recreatedPage.setViewportSize({ width: 500, height: 900 });
await recreatedPage.goto('/');
const recreatedEditor = recreatedPage.locator('[data-command-scope="general"]');
await expect(recreatedEditor.getByRole('button', { name: '일반 모드', exact: true })).toBeVisible();
await expect
.poll(() => recreatedPage.evaluate(() => localStorage.getItem('core2026:general:2:editMode')))
.toBeNull();
await recreatedEditor.screenshot({
path: test.info().outputPath('general-advanced-mode-after-recreation-mobile-500.png'),
});
await recreatedEditor.getByRole('button', { name: '일반 모드', exact: true }).click();
await expect(recreatedEditor.getByRole('button', { name: '고급 모드', exact: true })).toBeVisible();
await expect.poll(() => recreatedPage.evaluate((key) => localStorage.getItem(key), profileModeKey)).toBe('0');
const nextGeneralPage = await context.newPage();
await install(nextGeneralPage, false, commandTable, 3);
await nextGeneralPage.goto('/');
await expect(
nextGeneralPage.locator('[data-command-scope="general"]').getByRole('button', {
name: '고급 모드',
exact: true,
})
).toBeVisible();
await nextGeneralPage.close();
await recreatedPage.close();
});
test('shows all 12 advanced chief turns before the actions and uses the full mobile chief matrix', async ({ page }) => { test('shows all 12 advanced chief turns before the actions and uses the full mobile chief matrix', async ({ page }) => {
await install(page); await install(page);
await page.setViewportSize({ width: 500, height: 900 }); await page.setViewportSize({ width: 500, height: 900 });
@@ -30,6 +30,7 @@ const props = withDefaults(
commandTable: CommandTable | null; commandTable: CommandTable | null;
loading: boolean; loading: boolean;
storageKey: string; storageKey: string;
editModeStorageKey?: string;
maxPushTurn?: number; maxPushTurn?: number;
compact?: boolean; compact?: boolean;
mobile?: boolean; mobile?: boolean;
@@ -41,6 +42,7 @@ const props = withDefaults(
autonomousUntil?: string | null; autonomousUntil?: string | null;
}>(), }>(),
{ {
editModeStorageKey: undefined,
maxPushTurn: 6, maxPushTurn: 6,
compact: false, compact: false,
mobile: false, mobile: false,
@@ -82,20 +84,20 @@ const editorElement = ref<HTMLElement | null>(null);
const pickerElement = ref<HTMLElement | null>(null); const pickerElement = ref<HTMLElement | null>(null);
const collapsedRowCount = 15; const collapsedRowCount = 15;
const loadStorage = (key: string) => { const loadStorage = (key: string, editModeKey: string | undefined) => {
storage.value = new CommandStorage(key); storage.value = new CommandStorage(key, { editModeKey });
editMode.value = storage.value.editMode; editMode.value = storage.value.editMode;
activeCategory.value = storage.value.activeCategory; activeCategory.value = storage.value.activeCategory;
}; };
onMounted(() => { onMounted(() => {
loadStorage(props.storageKey); loadStorage(props.storageKey, props.editModeStorageKey);
}); });
watch( watch(
() => props.storageKey, () => [props.storageKey, props.editModeStorageKey] as const,
(key, previousKey) => { ([key, editModeKey], [previousKey, previousEditModeKey]) => {
if (key !== previousKey) loadStorage(key); if (key !== previousKey || editModeKey !== previousEditModeKey) loadStorage(key, editModeKey);
} }
); );
@@ -95,6 +95,18 @@ export const moveQueueRange = (
return next.map((entry, index) => ({ turnList: [index], ...entry })); return next.map((entry, index) => ({ turnList: [index], ...entry }));
}; };
export const generalTurnEditorModeStorageKey = (profile: string | undefined, appBasePath: string): string => {
const runtimeProfile = profile?.trim().split(':', 1)[0];
const basePathProfile = appBasePath.split('/').find((segment) => segment.length > 0);
const profileId = runtimeProfile || basePathProfile || 'default';
return `core2026:profile:${encodeURIComponent(profileId)}:general-turn-editor`;
};
interface CommandStorageOptions {
maxRecent?: number;
editModeKey?: string;
}
export class CommandStorage { export class CommandStorage {
readonly recent = new Map<string, CommandPatternEntry>(); readonly recent = new Map<string, CommandPatternEntry>();
readonly templates = new Map<string, CommandPatternEntry[]>(); readonly templates = new Map<string, CommandPatternEntry[]>();
@@ -102,11 +114,13 @@ export class CommandStorage {
editMode = false; editMode = false;
activeCategory = ''; activeCategory = '';
private readonly key: string; private readonly key: string;
private readonly editModeKey: string;
private readonly maxRecent: number; private readonly maxRecent: number;
constructor(key: string, maxRecent = 10) { constructor(key: string, options: CommandStorageOptions = {}) {
this.key = key; this.key = key;
this.maxRecent = maxRecent; this.editModeKey = options.editModeKey ?? key;
this.maxRecent = options.maxRecent ?? 10;
this.load(); this.load();
} }
@@ -126,12 +140,18 @@ export class CommandStorage {
this.templates.set(name, entries); this.templates.set(name, entries);
} }
this.clipboard = this.read<CommandPatternEntry[] | undefined>('clipboard', undefined); this.clipboard = this.read<CommandPatternEntry[] | undefined>('clipboard', undefined);
this.editMode = localStorage.getItem(`${this.key}:editMode`) === '1'; const editModeStorageKey = `${this.editModeKey}:editMode`;
let storedEditMode = localStorage.getItem(editModeStorageKey);
if (storedEditMode === null && this.editModeKey !== this.key) {
storedEditMode = localStorage.getItem(`${this.key}:editMode`);
if (storedEditMode !== null) localStorage.setItem(editModeStorageKey, storedEditMode);
}
this.editMode = storedEditMode === '1';
this.activeCategory = this.read('category', ''); this.activeCategory = this.read('category', '');
} }
saveState(): void { saveState(): void {
localStorage.setItem(`${this.key}:editMode`, this.editMode ? '1' : '0'); localStorage.setItem(`${this.editModeKey}:editMode`, this.editMode ? '1' : '0');
localStorage.setItem(`${this.key}:category`, JSON.stringify(this.activeCategory)); localStorage.setItem(`${this.key}:category`, JSON.stringify(this.activeCategory));
} }
@@ -2,8 +2,10 @@
import { computed, onUnmounted, ref, watch } from 'vue'; import { computed, onUnmounted, ref, watch } from 'vue';
import { addMinutes } from 'date-fns'; import { addMinutes } from 'date-fns';
import ReservedCommandEditor from '../command/ReservedCommandEditor.vue'; import ReservedCommandEditor from '../command/ReservedCommandEditor.vue';
import { generalTurnEditorModeStorageKey } from '../command/commandQueue';
import { formatLocalDateTime, formatLocalTimeSeconds } from '../../utils/legacyDateTime'; import { formatLocalDateTime, formatLocalTimeSeconds } from '../../utils/legacyDateTime';
import { projectServerClock, sampleServerClock, type SampledServerClock } from '../../utils/serverClockProjection'; import { projectServerClock, sampleServerClock, type SampledServerClock } from '../../utils/serverClockProjection';
import { gameFrontendRuntimeConfig } from '../../config/runtimeConfig';
import type { import type {
CommandMapData, CommandMapData,
CommandMapLayout, CommandMapLayout,
@@ -38,6 +40,11 @@ const emit = defineEmits<{
(event: 'repeat-general-turns', amount: number): void; (event: 'repeat-general-turns', amount: number): void;
}>(); }>();
const editModeStorageKey = generalTurnEditorModeStorageKey(
gameFrontendRuntimeConfig.profile,
gameFrontendRuntimeConfig.appBasePath
);
const labelMap = computed(() => { const labelMap = computed(() => {
const result = new Map<string, string>([['휴식', '휴식']]); const result = new Map<string, string>([['휴식', '휴식']]);
for (const group of props.commandTable?.general ?? []) { for (const group of props.commandTable?.general ?? []) {
@@ -140,6 +147,7 @@ onUnmounted(() => {
:command-table="props.commandTable" :command-table="props.commandTable"
:loading="props.loading" :loading="props.loading"
:storage-key="props.storageKey ?? `core2026:general:${props.general?.id ?? 0}`" :storage-key="props.storageKey ?? `core2026:general:${props.general?.id ?? 0}`"
:edit-mode-storage-key="editModeStorageKey"
:current-time="currentServerTime" :current-time="currentServerTime"
:map-data="props.mapData" :map-data="props.mapData"
:map-layout="props.mapLayout" :map-layout="props.mapLayout"
@@ -4,11 +4,19 @@ import test from 'node:test';
import { import {
amplifyPattern, amplifyPattern,
extractPattern, extractPattern,
generalTurnEditorModeStorageKey,
moveQueueRange, moveQueueRange,
normalizedSelection, normalizedSelection,
selectStep, selectStep,
} from '../src/components/command/commandQueue.ts'; } from '../src/components/command/commandQueue.ts';
void test('scopes the general turn editor mode to the stable server profile', () => {
assert.equal(generalTurnEditorModeStorageKey('che:default', '/che/'), 'core2026:profile:che:general-turn-editor');
assert.equal(generalTurnEditorModeStorageKey('che:2601', '/che/'), 'core2026:profile:che:general-turn-editor');
assert.equal(generalTurnEditorModeStorageKey('hwe:2601', '/hwe/'), 'core2026:profile:hwe:general-turn-editor');
assert.equal(generalTurnEditorModeStorageKey(undefined, '/nya/'), 'core2026:profile:nya:general-turn-editor');
});
const rows = ['A', 'B', 'A', 'C', '휴식', '휴식'].map((action, index) => ({ const rows = ['A', 'B', 'A', 'C', '휴식', '휴식'].map((action, index) => ({
index, index,
action, action,