fix(game-ui): 턴 입력기 모드를 서버별로 기억한다
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user