fix(game-ui): 메인 갱신 제어를 턴 입력기 아래로 이동

This commit is contained in:
2026-08-21 04:49:52 +00:00
parent 8e616ed07b
commit aa479c1f0f
5 changed files with 275 additions and 147 deletions
+140 -81
View File
@@ -1047,10 +1047,10 @@ const persistArtifact = async (page: Page, name: string) => {
quickPopup: describe('#mobile-quick-menu'), quickPopup: describe('#mobile-quick-menu'),
gameHeader: describe('.game-shell__header'), gameHeader: describe('.game-shell__header'),
gameTitle: describe('.game-shell__title'), gameTitle: describe('.game-shell__title'),
gameHeaderActions: describe('.desktop-action-controls'), turnControls: describe('.main-turn-controls'),
headerRealtime: describe('.desktop-action-controls__realtime'), turnAutoRefresh: describe('.main-turn-controls__auto'),
headerRefresh: describe('.desktop-action-controls__refresh'), turnManualRefresh: describe('.main-turn-controls__manual'),
headerLobby: describe('.desktop-action-controls__lobby'), turnLobby: describe('.main-turn-controls__lobby'),
legacyGameInfo: describe('.legacy-game-info'), legacyGameInfo: describe('.legacy-game-info'),
activityStatus: describe('.activity-status'), activityStatus: describe('.activity-status'),
executionStatus: describe('.execution-status'), executionStatus: describe('.execution-status'),
@@ -2382,21 +2382,18 @@ test('the 939/940 boundary switches to the Ref-style 500px single document', asy
const activityGeometry = await page.locator('.activity-status').evaluate((element) => { const activityGeometry = await page.locator('.activity-status').evaluate((element) => {
const main = element.closest<HTMLElement>('.main-page'); const main = element.closest<HTMLElement>('.main-page');
const header = main?.querySelector<HTMLElement>('.game-shell__header'); const header = main?.querySelector<HTMLElement>('.game-shell__header');
const headerActions = header?.querySelector<HTMLElement>('.desktop-action-controls');
const execution = element.querySelector<HTMLElement>('.execution-status'); const execution = element.querySelector<HTMLElement>('.execution-status');
const tournament = element.querySelector<HTMLElement>('.tournament-status'); const tournament = element.querySelector<HTMLElement>('.tournament-status');
const survey = element.querySelector<HTMLElement>('.vote-status'); const survey = element.querySelector<HTMLElement>('.vote-status');
if (!header || !headerActions || !execution || !tournament || !survey) { if (!header || !execution || !tournament || !survey) {
throw new Error('mobile header or activity status is incomplete'); throw new Error('mobile header or activity status is incomplete');
} }
const headerRect = header.getBoundingClientRect(); const headerRect = header.getBoundingClientRect();
const headerActionsRect = headerActions.getBoundingClientRect();
return { return {
headerHeight: headerRect.height, headerHeight: headerRect.height,
headerLeft: headerRect.left, headerLeft: headerRect.left,
headerRight: headerRect.right, headerRight: headerRect.right,
headerActionsLeft: headerActionsRect.left, headerActionCount: header.querySelectorAll('button').length,
headerActionsRight: headerActionsRect.right,
width: element.getBoundingClientRect().width, width: element.getBoundingClientRect().width,
executionWidth: execution.getBoundingClientRect().width, executionWidth: execution.getBoundingClientRect().width,
tournamentWidth: tournament.getBoundingClientRect().width, tournamentWidth: tournament.getBoundingClientRect().width,
@@ -2404,10 +2401,9 @@ test('the 939/940 boundary switches to the Ref-style 500px single document', asy
columns: getComputedStyle(element).gridTemplateColumns, columns: getComputedStyle(element).gridTemplateColumns,
}; };
}); });
expect(activityGeometry.headerHeight).toBeGreaterThan(90); expect(activityGeometry.headerHeight).toBeGreaterThan(40);
expect(activityGeometry.headerHeight).toBeLessThan(110); expect(activityGeometry.headerHeight).toBeLessThan(70);
expect(activityGeometry.headerActionsLeft).toBeGreaterThanOrEqual(activityGeometry.headerLeft); expect(activityGeometry.headerActionCount).toBe(0);
expect(activityGeometry.headerActionsRight).toBeLessThanOrEqual(activityGeometry.headerRight);
expect(activityGeometry.width).toBe(500); expect(activityGeometry.width).toBe(500);
expect(activityGeometry.executionWidth).toBeCloseTo(166.67, 0); expect(activityGeometry.executionWidth).toBeCloseTo(166.67, 0);
expect(activityGeometry.tournamentWidth).toBeCloseTo(166.67, 0); expect(activityGeometry.tournamentWidth).toBeCloseTo(166.67, 0);
@@ -3050,7 +3046,7 @@ test('all main Lumen button families share the rounded pressed geometry', async
await page.setViewportSize({ width: 1200, height: 900 }); await page.setViewportSize({ width: 1200, height: 900 });
await waitForMain(page); await waitForMain(page);
const controls: Array<[string, Locator]> = [ const controls: Array<[string, Locator, { borderLeft?: string; radius?: string }?]> = [
[ [
'천통국 베팅', '천통국 베팅',
page.locator('.main-global-menu[data-menu-position="top"] [data-navigation-id="nation-betting"]'), page.locator('.main-global-menu[data-menu-position="top"] [data-navigation-id="nation-betting"]'),
@@ -3076,9 +3072,17 @@ test('all main Lumen button families share the rounded pressed geometry', async
'펼치기', '펼치기',
page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '펼치기' }), page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '펼치기' }),
], ],
['실시간 동기화', page.locator('.desktop-action-controls').getByRole('button', { name: / :/u })], [
['갱 신', page.locator('.desktop-action-controls').getByRole('button', { name: '갱 신' })], '자동 갱신',
['로비로', page.locator('.desktop-action-controls').getByRole('button', { name: '로비로' })], page.locator('.main-turn-controls').getByRole('button', { name: '자동 갱신 ON' }),
{ borderLeft: '0px', radius: '0px 5.25px 5.25px 0px' },
],
[
'갱 신',
page.locator('.main-turn-controls').getByRole('button', { name: '갱 신' }),
{ radius: '5.25px 0px 0px 5.25px' },
],
['로비로', page.locator('.main-turn-controls').getByRole('button', { name: '로비로' })],
]; ];
const measure = (control: Locator) => const measure = (control: Locator) =>
@@ -3101,7 +3105,7 @@ test('all main Lumen button families share the rounded pressed geometry', async
}); });
const evidence: Record<string, Record<string, unknown>> = {}; const evidence: Record<string, Record<string, unknown>> = {};
for (const [index, [label, control]] of controls.entries()) { for (const [index, [label, control, expectedGeometry]] of controls.entries()) {
await expect(control, `${label} control`).toBeVisible(); await expect(control, `${label} control`).toBeVisible();
await expect(control).toHaveClass(/legacy-button/u); await expect(control).toHaveClass(/legacy-button/u);
await control.scrollIntoViewIfNeeded(); await control.scrollIntoViewIfNeeded();
@@ -3116,8 +3120,8 @@ test('all main Lumen button families share the rounded pressed geometry', async
borderTop: '0px', borderTop: '0px',
borderRight: '1px', borderRight: '1px',
borderBottom: '4px', borderBottom: '4px',
borderLeft: '1px', borderLeft: expectedGeometry?.borderLeft ?? '1px',
radius: '5.25px', radius: expectedGeometry?.radius ?? '5.25px',
filter: 'none', filter: 'none',
}); });
@@ -3164,7 +3168,7 @@ test('all main Lumen button families share the rounded pressed geometry', async
} }
state.permission = 0; state.permission = 0;
await page.locator('.desktop-action-controls').getByRole('button', { name: '갱 신' }).click(); await page.locator('.main-turn-controls').getByRole('button', { name: '갱 신' }).click();
const disabledSecret = page.locator('.layout-desktop [data-navigation-id="secret-board"]'); const disabledSecret = page.locator('.layout-desktop [data-navigation-id="secret-board"]');
await expect(disabledSecret).toHaveAttribute('aria-disabled', 'true'); await expect(disabledSecret).toHaveAttribute('aria-disabled', 'true');
await disabledSecret.scrollIntoViewIfNeeded(); await disabledSecret.scrollIntoViewIfNeeded();
@@ -3186,7 +3190,9 @@ test('all main Lumen button families share the rounded pressed geometry', async
await persistArtifact(page, `${basePath.slice(1)}-main-lumen-button-families`); await persistArtifact(page, `${basePath.slice(1)}-main-lumen-button-families`);
}); });
test('lobby action is separated on desktop and anchors opposite the refresh action on mobile', async ({ page }) => { test('places the joined refresh controls and lobby below the turn editor without changing the desktop baseline', async ({
page,
}) => {
const state: NavigationFixture = { const state: NavigationFixture = {
officerLevel: 5, officerLevel: 5,
permission: 2, permission: 2,
@@ -3195,67 +3201,114 @@ test('lobby action is separated on desktop and anchors opposite the refresh acti
npcMode: 1, npcMode: 1,
generalMeCalls: 0, generalMeCalls: 0,
operations: [], operations: [],
refreshDelayMs: 300,
largeCommandTable: true,
reservedTurns: Array.from({ length: 30 }, (_, index) => ({
index,
action: index === 0 ? '휴식' : `command-${index}`,
args: {},
})),
}; };
await installFixture(page, state); await installFixture(page, state);
await page.setViewportSize({ width: 1200, height: 900 }); await page.setViewportSize({ width: 1200, height: 900 });
await waitForMain(page); await waitForMain(page);
const actions = page.locator('.desktop-action-controls'); await expect(page.locator('.game-shell__header button')).toHaveCount(0);
const realtime = page.locator('.desktop-action-controls__realtime');
const refresh = page.locator('.desktop-action-controls__refresh');
const lobby = page.locator('.desktop-action-controls__lobby');
const measure = () => const measure = () =>
page.evaluate(() => { page.locator('[data-main-target="commands"]').evaluate((commands) => {
const rect = (selector: string) => { const box = (element: Element) => {
const element = document.querySelector<HTMLElement>(selector); const rect = element.getBoundingClientRect();
if (!element) throw new Error(`${selector} is missing`);
const box = element.getBoundingClientRect();
return { left: box.left, right: box.right, top: box.top, bottom: box.bottom, width: box.width };
};
const actionStyle = getComputedStyle(document.querySelector<HTMLElement>('.desktop-action-controls')!);
return { return {
actions: rect('.desktop-action-controls'), left: rect.left,
realtime: rect('.desktop-action-controls__realtime'), right: rect.right,
refresh: rect('.desktop-action-controls__refresh'), top: rect.top,
lobby: rect('.desktop-action-controls__lobby'), bottom: rect.bottom,
title: rect('.game-shell__title'), width: rect.width,
display: actionStyle.display, height: rect.height,
columns: actionStyle.gridTemplateColumns, };
};
const find = (selector: string) => {
const element = commands.querySelector<HTMLElement>(selector);
if (!element) throw new Error(`${selector} is missing`);
return element;
};
const editor = find('.reserved-command-editor');
const controls = find('.main-turn-controls');
const pair = find('.main-turn-controls__refresh-pair');
const manual = find('.main-turn-controls__manual');
const auto = find('.main-turn-controls__auto');
const lobby = find('.main-turn-controls__lobby');
const manualStyle = getComputedStyle(manual);
const autoStyle = getComputedStyle(auto);
return {
commands: box(commands),
editor: box(editor),
controls: box(controls),
pair: box(pair),
manual: box(manual),
auto: box(auto),
lobby: box(lobby),
controlsAfterEditor: Boolean(
editor.compareDocumentPosition(controls) & Node.DOCUMENT_POSITION_FOLLOWING
),
manualRadius: {
topRight: manualStyle.borderTopRightRadius,
bottomRight: manualStyle.borderBottomRightRadius,
},
autoRadius: {
topLeft: autoStyle.borderTopLeftRadius,
bottomLeft: autoStyle.borderBottomLeftRadius,
},
manualRightBorder: manualStyle.borderRightWidth,
autoLeftBorder: autoStyle.borderLeftWidth,
overflow: commands.scrollWidth - commands.clientWidth,
}; };
}); });
await expect(actions).toHaveCSS('display', 'flex');
let layout = await measure(); let layout = await measure();
expect(layout.lobby.left - layout.refresh.right).toBeGreaterThanOrEqual(20); const cityBottom = await page
expect(layout.refresh.top).toBeCloseTo(layout.lobby.top, 2); .locator('[data-main-target="city"]')
expect(layout.refresh.width).toBeLessThanOrEqual(62); .evaluate((element) => element.getBoundingClientRect().bottom);
expect(layout.lobby.width).toBeLessThanOrEqual(62); expect(layout.commands.bottom).toBe(cityBottom);
expect(layout.commands.height).toBeCloseTo(645, 0);
expect(layout.controlsAfterEditor).toBe(true);
expect(layout.controls.top).toBeGreaterThanOrEqual(layout.editor.bottom);
expect(layout.manual.right).toBe(layout.auto.left);
expect(layout.pair.right + 4).toBe(layout.lobby.left);
expect(layout.manualRadius).toEqual({ topRight: '0px', bottomRight: '0px' });
expect(layout.autoRadius).toEqual({ topLeft: '0px', bottomLeft: '0px' });
expect(layout.manualRightBorder).toBe('1px');
expect(layout.autoLeftBorder).toBe('0px');
expect(layout.overflow).toBeLessThanOrEqual(0);
const autoRefresh = page.locator('.layout-desktop .main-turn-controls__auto');
await expect(autoRefresh).toHaveAttribute('aria-pressed', 'true');
await autoRefresh.click();
await expect(page.locator('.layout-desktop .main-turn-controls__auto')).toHaveAccessibleName('자동 갱신 OFF');
await expect(page.locator('.layout-desktop .main-turn-controls__auto')).toHaveAttribute('aria-pressed', 'false');
const manualRefresh = page.locator('.layout-desktop .main-turn-controls__manual');
const callsBeforeManualRefresh = state.generalMeCalls;
await manualRefresh.click();
await expect(manualRefresh).toBeEnabled();
await expect(manualRefresh).toHaveAttribute('aria-busy', 'true');
await manualRefresh.click();
await expect(page.getByTestId('game-toast')).toContainText('이미 정보를 갱신하고 있습니다.');
await expect(manualRefresh).toHaveAttribute('aria-busy', 'false');
expect(state.generalMeCalls).toBe(callsBeforeManualRefresh + 1);
await page.setViewportSize({ width: 500, height: 900 }); await page.setViewportSize({ width: 500, height: 900 });
await expect(actions).toHaveCSS('display', 'grid'); await expect(page.locator('.layout-mobile .main-turn-controls')).toBeVisible();
await expect(realtime).toBeVisible();
await expect(refresh).toBeVisible();
await expect(lobby).toBeVisible();
layout = await measure(); layout = await measure();
expect(layout.columns.split(' ')).toHaveLength(3); expect(layout.commands.width).toBe(500);
expect(layout.actions.left).toBeCloseTo(0, 2); expect(layout.controlsAfterEditor).toBe(true);
expect(layout.actions.right).toBeCloseTo(500, 2); expect(layout.controls.top).toBeGreaterThanOrEqual(layout.editor.bottom);
expect(layout.refresh.left).toBeCloseTo(layout.actions.left, 2); expect(layout.manual.right).toBe(layout.auto.left);
expect(layout.lobby.right).toBeCloseTo(layout.actions.right, 2); expect(layout.pair.right + 4).toBe(layout.lobby.left);
expect(layout.refresh.right).toBeLessThan(layout.realtime.left); expect(layout.overflow).toBeLessThanOrEqual(0);
expect(layout.realtime.right).toBeLessThan(layout.lobby.left); expect(await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBe(
expect(layout.refresh.top).toBeCloseTo(layout.lobby.top, 2); 0
expect(layout.actions.top).toBeGreaterThanOrEqual(layout.title.bottom); );
expect(layout.refresh.width).toBeLessThanOrEqual(62);
expect(layout.lobby.width).toBeLessThanOrEqual(62);
expect(
await page.evaluate(() => ({
document: document.documentElement.scrollWidth - document.documentElement.clientWidth,
body: document.body.scrollWidth - document.body.clientWidth,
}))
).toEqual({ document: 0, body: 0 });
await persistArtifact(page, `${basePath.slice(1)}-main-lobby-action-layout`); await persistArtifact(page, `${basePath.slice(1)}-main-turn-action-layout`);
}); });
test('mobile main Lumen button families keep the same state geometry without overflow', async ({ page }) => { test('mobile main Lumen button families keep the same state geometry without overflow', async ({ page }) => {
@@ -3284,14 +3337,17 @@ test('mobile main Lumen button families keep the same state geometry without ove
page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '당기기' }), page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '당기기' }),
page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '미루기' }), page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '미루기' }),
page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '펼치기' }), page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '펼치기' }),
page.locator('.desktop-action-controls').getByRole('button', { name: / :/u }), page.locator('.layout-mobile .main-turn-controls').getByRole('button', { name: / /u }),
page.locator('.desktop-action-controls').getByRole('button', { name: '갱 신' }), page.locator('.layout-mobile .main-turn-controls').getByRole('button', { name: '갱 신' }),
page.locator('.desktop-action-controls').getByRole('button', { name: '로비로' }), page.locator('.layout-mobile .main-turn-controls').getByRole('button', { name: '로비로' }),
]; ];
for (const control of controls) { for (const [index, control] of controls.entries()) {
await expect(control).toBeVisible(); await expect(control).toBeVisible();
await expect(control).toHaveClass(/legacy-button/u); await expect(control).toHaveClass(/legacy-button/u);
await expect(control).toHaveCSS('border-radius', '5.25px'); await expect(control).toHaveCSS(
'border-radius',
index === 7 ? '0px 5.25px 5.25px 0px' : index === 8 ? '5.25px 0px 0px 5.25px' : '5.25px'
);
await expect(control).toHaveCSS('border-bottom-width', '4px'); await expect(control).toHaveCSS('border-bottom-width', '4px');
} }
@@ -3356,8 +3412,9 @@ test('mobile single document refreshes once and preserves tokens on lobby return
await expect(page.locator(selector)).toBeVisible(); await expect(page.locator(selector)).toBeVisible();
} }
const autoRefresh = page.getByRole('button', { name: '자동 갱신 ON' }); const mobileBottom = page.locator('.main-mobile-bottom');
const manualRefresh = page.getByRole('button', { name: '직접 갱신' }); const autoRefresh = mobileBottom.getByRole('button', { name: '자동 갱신 ON' });
const manualRefresh = mobileBottom.getByRole('button', { name: '직접 갱신' });
await expect(autoRefresh).toHaveAttribute('aria-pressed', 'true'); await expect(autoRefresh).toHaveAttribute('aria-pressed', 'true');
await expect(autoRefresh.locator('strong')).toHaveCSS('color', 'rgb(158, 240, 184)'); await expect(autoRefresh.locator('strong')).toHaveCSS('color', 'rgb(158, 240, 184)');
await expect(manualRefresh).toHaveAttribute('aria-busy', 'false'); await expect(manualRefresh).toHaveAttribute('aria-busy', 'false');
@@ -3397,7 +3454,7 @@ test('mobile single document refreshes once and preserves tokens on lobby return
await expect(autoRefresh).toHaveCSS('border-bottom-width', '3px'); await expect(autoRefresh).toHaveCSS('border-bottom-width', '3px');
await expect(autoRefresh).toHaveCSS('margin-top', '1px'); await expect(autoRefresh).toHaveCSS('margin-top', '1px');
await autoRefresh.click(); await autoRefresh.click();
const disabledAutoRefresh = page.getByRole('button', { name: '자동 갱신 OFF' }); const disabledAutoRefresh = mobileBottom.getByRole('button', { name: '자동 갱신 OFF' });
await expect(disabledAutoRefresh).toHaveAttribute('aria-pressed', 'false'); await expect(disabledAutoRefresh).toHaveAttribute('aria-pressed', 'false');
await expect(disabledAutoRefresh.locator('strong')).toHaveCSS('color', 'rgb(187, 187, 187)'); await expect(disabledAutoRefresh.locator('strong')).toHaveCSS('color', 'rgb(187, 187, 187)');
await expect await expect
@@ -3414,8 +3471,8 @@ test('mobile single document refreshes once and preserves tokens on lobby return
await expect(page.locator('.general-title')).toContainText('직접갱신된장수'); await expect(page.locator('.general-title')).toContainText('직접갱신된장수');
const callsBeforeEnable = state.generalMeCalls; const callsBeforeEnable = state.generalMeCalls;
await page.getByRole('button', { name: '자동 갱신 OFF' }).click(); await mobileBottom.getByRole('button', { name: '자동 갱신 OFF' }).click();
await expect(page.getByRole('button', { name: '자동 갱신 ON' })).toHaveAttribute('aria-pressed', 'true'); await expect(mobileBottom.getByRole('button', { name: '자동 갱신 ON' })).toHaveAttribute('aria-pressed', 'true');
await expect.poll(() => state.generalMeCalls).toBeGreaterThan(callsBeforeEnable); await expect.poll(() => state.generalMeCalls).toBeGreaterThan(callsBeforeEnable);
await expect await expect
.poll(() => .poll(() =>
@@ -4155,8 +4212,10 @@ test('same-account main tabs share one realtime diff and exclude a tab while syn
pages.map((currentPage) => expect(currentPage.locator('.general-title')).toContainText('탭공유갱신장수')) pages.map((currentPage) => expect(currentPage.locator('.general-title')).toContainText('탭공유갱신장수'))
); );
await followerPage.getByRole('button', { name: / /u }).click(); await followerPage.locator('.layout-desktop .main-turn-controls__auto').click();
await expect(followerPage.getByRole('button', { name: / : /u })).toBeVisible(); await expect(followerPage.locator('.layout-desktop .main-turn-controls__auto')).toHaveAccessibleName(
'자동 갱신 OFF'
);
const callsBeforeExcludedRefresh = state.generalMeCalls; const callsBeforeExcludedRefresh = state.generalMeCalls;
state.generalName = '리더만갱신장수'; state.generalName = '리더만갱신장수';
await leaderPage.evaluate(() => { await leaderPage.evaluate(() => {
@@ -4182,7 +4241,7 @@ test('same-account main tabs share one realtime diff and exclude a tab while syn
await expect(leaderPage.locator('.general-title')).toContainText('리더만갱신장수'); await expect(leaderPage.locator('.general-title')).toContainText('리더만갱신장수');
await expect(followerPage.locator('.general-title')).toContainText('탭공유갱신장수'); await expect(followerPage.locator('.general-title')).toContainText('탭공유갱신장수');
await followerPage.getByRole('button', { name: / : /u }).click(); await followerPage.locator('.layout-desktop .main-turn-controls__auto').click();
await expect(followerPage.locator('.general-title')).toContainText('리더만갱신장수'); await expect(followerPage.locator('.general-title')).toContainText('리더만갱신장수');
await expect await expect
.poll(async () => { .poll(async () => {
@@ -160,7 +160,7 @@
* toggle's overlapping left border. These rules intentionally follow the * toggle's overlapping left border. These rules intentionally follow the
* Lumen family so its border shorthand cannot restore the inner rounding. * Lumen family so its border shorthand cannot restore the inner rounding.
*/ */
.legacy-split-button > .main-menu-link { .legacy-split-button > :is(.main-menu-link, .legacy-split-button__main) {
border-radius: 5.25px 0 0 5.25px; border-radius: 5.25px 0 0 5.25px;
} }
@@ -0,0 +1,84 @@
<script setup lang="ts">
defineProps<{
realtimeEnabled: boolean;
refreshing: boolean;
}>();
const emit = defineEmits<{
refresh: [];
toggleRealtime: [];
lobby: [];
}>();
</script>
<template>
<section class="main-turn-controls" aria-label="메인 갱신 이동">
<div class="main-turn-controls__refresh-pair legacy-split-button">
<button
class="main-turn-controls__manual legacy-split-button__main legacy-button legacy-button--navigation"
type="button"
:aria-busy="refreshing"
@click="emit('refresh')"
>
</button>
<button
class="main-turn-controls__auto legacy-split-button__toggle legacy-button legacy-button--navigation"
:class="{ active: realtimeEnabled }"
type="button"
:aria-label="`자동 갱신 ${realtimeEnabled ? 'ON' : 'OFF'}`"
:aria-pressed="realtimeEnabled"
@click="emit('toggleRealtime')"
>
<span>자동 갱신</span>
<strong>{{ realtimeEnabled ? 'ON' : 'OFF' }}</strong>
</button>
</div>
<button
class="main-turn-controls__lobby legacy-button legacy-button--navigation"
type="button"
@click="emit('lobby')"
>
로비로
</button>
</section>
</template>
<style scoped>
.main-turn-controls {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
gap: 4px;
}
.main-turn-controls__refresh-pair {
display: grid;
min-width: 0;
grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
}
.main-turn-controls .legacy-button {
width: 100%;
min-width: 0;
padding-right: 4px;
padding-left: 4px;
font-weight: 400;
white-space: nowrap;
}
.main-turn-controls__auto {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.main-turn-controls__auto strong {
color: #bbb;
font-size: 0.85em;
}
.main-turn-controls__auto.active strong {
color: #9ef0b8;
}
</style>
+22 -66
View File
@@ -17,6 +17,7 @@ import MainFrontStatus from '../components/main/MainFrontStatus.vue';
import MainGlobalMenu from '../components/main/MainGlobalMenu.vue'; import MainGlobalMenu from '../components/main/MainGlobalMenu.vue';
import MainNationMenu from '../components/main/MainNationMenu.vue'; import MainNationMenu from '../components/main/MainNationMenu.vue';
import MainMobileBottomBar from '../components/main/MainMobileBottomBar.vue'; import MainMobileBottomBar from '../components/main/MainMobileBottomBar.vue';
import MainTurnControls from '../components/main/MainTurnControls.vue';
import { import {
defaultGlobalNavigation, defaultGlobalNavigation,
type MainNavigationEntry, type MainNavigationEntry,
@@ -86,7 +87,6 @@ const {
messageDraftText, messageDraftText,
targetMailbox, targetMailbox,
mailboxGroups, mailboxGroups,
realtimeLabel,
} = storeToRefs(dashboard); } = storeToRefs(dashboard);
const nationAccess = computed(() => ({ const nationAccess = computed(() => ({
@@ -223,31 +223,6 @@ watch(
<h1 class="game-shell__title"> <h1 class="game-shell__title">
{{ gameTitle }} {{ gameTitle }}
</h1> </h1>
<div class="game-shell__actions desktop-action-controls">
<button
class="game-shell__action desktop-action-controls__realtime toggle legacy-button legacy-button--navigation"
:class="{ active: realtimeEnabled }"
type="button"
@click="dashboard.setRealtimeEnabled(!realtimeEnabled)"
>
실시간 동기화: {{ realtimeLabel }}
</button>
<button
class="game-shell__action desktop-action-controls__refresh legacy-button legacy-button--navigation"
type="button"
:aria-busy="refreshing"
@click="requestManualRefresh"
>
</button>
<button
class="game-shell__action desktop-action-controls__lobby legacy-button legacy-button--navigation"
type="button"
@click="moveLobby"
>
로비로
</button>
</div>
</header> </header>
<section v-if="lobbyInfo" class="legacy-game-info" aria-label="게임 진행 정보"> <section v-if="lobbyInfo" class="legacy-game-info" aria-label="게임 진행 정보">
@@ -282,7 +257,7 @@ watch(
<section v-if="isMobile" class="layout-mobile"> <section v-if="isMobile" class="layout-mobile">
<template v-for="(panelId, panelIndex) in mobilePanelOrder" :key="panelId"> <template v-for="(panelId, panelIndex) in mobilePanelOrder" :key="panelId">
<div v-if="panelId === 'commands'" class="mobile-panel" data-mobile-panel-id="commands"> <div v-if="panelId === 'commands'" class="mobile-panel" data-mobile-panel-id="commands">
<PanelCard title="명령 목록" subtitle="예턴/명령 배치 영역" data-main-target="commands"> <PanelCard title="명령 목록" aria-label="명령 목록" data-main-target="commands">
<CommandListPanel <CommandListPanel
:command-table="commandTable" :command-table="commandTable"
:loading="loading" :loading="loading"
@@ -300,6 +275,13 @@ watch(
@shift-general-turns="shiftGeneralTurns" @shift-general-turns="shiftGeneralTurns"
@repeat-general-turns="repeatGeneralTurns" @repeat-general-turns="repeatGeneralTurns"
/> />
<MainTurnControls
:realtime-enabled="realtimeEnabled"
:refreshing="refreshing"
@refresh="requestManualRefresh"
@toggle-realtime="dashboard.setRealtimeEnabled(!realtimeEnabled)"
@lobby="moveLobby"
/>
</PanelCard> </PanelCard>
</div> </div>
@@ -435,7 +417,7 @@ watch(
<PanelCard title="지도" subtitle="실시간 지도 + 도시 상황" data-main-target="map"> <PanelCard title="지도" subtitle="실시간 지도 + 도시 상황" data-main-target="map">
<MapViewer :map-data="worldMap" :map-layout="mapLayout" :loading="loading" /> <MapViewer :map-data="worldMap" :map-layout="mapLayout" :loading="loading" />
</PanelCard> </PanelCard>
<PanelCard title="명령 목록" subtitle="예턴/명령 배치 영역" data-main-target="commands"> <PanelCard title="명령 목록" aria-label="명령 목록" data-main-target="commands">
<CommandListPanel <CommandListPanel
:command-table="commandTable" :command-table="commandTable"
:loading="loading" :loading="loading"
@@ -453,6 +435,13 @@ watch(
@shift-general-turns="shiftGeneralTurns" @shift-general-turns="shiftGeneralTurns"
@repeat-general-turns="repeatGeneralTurns" @repeat-general-turns="repeatGeneralTurns"
/> />
<MainTurnControls
:realtime-enabled="realtimeEnabled"
:refreshing="refreshing"
@refresh="requestManualRefresh"
@toggle-realtime="dashboard.setRealtimeEnabled(!realtimeEnabled)"
@lobby="moveLobby"
/>
</PanelCard> </PanelCard>
<PanelCard title="도시 정보" data-main-target="city"> <PanelCard title="도시 정보" data-main-target="city">
<CityBasicCard :city="city" :loading="loading" /> <CityBasicCard :city="city" :loading="loading" />
@@ -746,6 +735,7 @@ button {
.layout-desktop > [data-main-target='commands'] { .layout-desktop > [data-main-target='commands'] {
grid-column: 8 / 11; grid-column: 8 / 11;
grid-row: 1 / 3; grid-row: 1 / 3;
align-self: stretch;
min-height: 645px; min-height: 645px;
width: 290px; width: 290px;
margin-left: 10px; margin-left: 10px;
@@ -785,6 +775,10 @@ button {
background-color: #222; background-color: #222;
} }
[data-main-target='commands'] :deep(.panel-header) {
display: none;
}
.nation-menu-middle { .nation-menu-middle {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
@@ -912,14 +906,6 @@ button {
margin-top: 31px; margin-top: 31px;
} }
.desktop-action-controls .game-shell__action {
font-weight: 400;
}
.desktop-action-controls__lobby {
margin-left: 12px;
}
.placeholder { .placeholder {
font-size: 0.85rem; font-size: 0.85rem;
color: rgba(232, 221, 196, 0.7); color: rgba(232, 221, 196, 0.7);
@@ -933,36 +919,6 @@ button {
height: 45px; height: 45px;
} }
.desktop-action-controls {
display: grid;
width: 100%;
grid-template-areas: 'refresh realtime lobby';
grid-template-columns: max-content 1fr max-content;
align-items: start;
gap: 0;
}
.desktop-action-controls .game-shell__action {
padding-right: 4px;
padding-left: 4px;
}
.desktop-action-controls__refresh {
grid-area: refresh;
justify-self: start;
}
.desktop-action-controls__realtime {
grid-area: realtime;
justify-self: center;
}
.desktop-action-controls__lobby {
grid-area: lobby;
justify-self: end;
margin-left: 0;
}
.main-page { .main-page {
width: 500px; width: 500px;
min-height: 3688px; min-height: 3688px;
@@ -97,8 +97,28 @@ try {
color: style.color, color: style.color,
}; };
}; };
const inspectControl = (element) => {
if (!(element instanceof HTMLElement)) return null;
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return {
text: element.textContent?.replace(/\s+/gu, ' ').trim() ?? '',
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
display: style.display,
backgroundColor: style.backgroundColor,
color: style.color,
borderTopWidth: style.borderTopWidth,
borderRightWidth: style.borderRightWidth,
borderBottomWidth: style.borderBottomWidth,
borderLeftWidth: style.borderLeftWidth,
borderRadius: style.borderRadius,
};
};
const cityCard = document.querySelector('.city-card-basic'); const cityCard = document.querySelector('.city-card-basic');
if (!(cityCard instanceof HTMLElement)) throw new Error('reference city card missing'); if (!(cityCard instanceof HTMLElement)) throw new Error('reference city card missing');
const actionMiniPlate = document.querySelector('#actionMiniPlate');
const actionMiniPlateSub = document.querySelector('#actionMiniPlateSub');
const reservedCommandZone = document.querySelector('.reservedCommandZone');
return { return {
viewport: { width: innerWidth, height: innerHeight }, viewport: { width: innerWidth, height: innerHeight },
city: [...document.querySelectorAll('.city-card-basic .sammo-bar')].map(inspect), city: [...document.querySelectorAll('.city-card-basic .sammo-bar')].map(inspect),
@@ -113,6 +133,15 @@ try {
officers: [4, 3, 2].map((level) => inspectPanel(`.city-card-basic .officer${level}Panel`)), officers: [4, 3, 2].map((level) => inspectPanel(`.city-card-basic .officer${level}Panel`)),
}, },
generalCard: document.querySelector('.general-card-basic').getBoundingClientRect().toJSON(), generalCard: document.querySelector('.general-card-basic').getBoundingClientRect().toJSON(),
turnControls: {
reservedCommandZone: inspectControl(reservedCommandZone),
actionMiniPlate: inspectControl(actionMiniPlate),
buttons: actionMiniPlate ? [...actionMiniPlate.querySelectorAll('button')].map(inspectControl) : [],
actionMiniPlateSub: inspectControl(actionMiniPlateSub),
subButtons: actionMiniPlateSub
? [...actionMiniPlateSub.querySelectorAll('button')].map(inspectControl)
: [],
},
}; };
}); });
await Promise.all([ await Promise.all([