feat: 메인에 자율행동 설정을 표시한다

Ref와 Gateway의 옵션 순서와 유효 기간 문구를 따라 8열 마지막 셀에 상태를 더한다.\n활성 상태의 hover와 keyboard focus에서 상세 tooltip을 보이고, 비활성 상태는 표준으로 표시한다.
This commit is contained in:
2026-08-27 16:37:47 +00:00
parent 68870be5e5
commit 9e5e580134
5 changed files with 193 additions and 1 deletions
@@ -78,6 +78,7 @@ type NavigationFixture = {
messages?: unknown;
messageContacts?: unknown;
autorunLimit?: number | null;
autorunUser?: { limitMinutes: number; options: string[] } | null;
dashboardResponses?: Array<{
bytes: number;
contextKind: string | null;
@@ -633,6 +634,7 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
clockStartsAt: state.clockStartsAt ?? null,
turnEngineRunning: state.turnEngineRunning === undefined ? true : state.turnEngineRunning,
scenarioTitle: state.scenarioTitle ?? '',
autorunUser: state.autorunUser ?? null,
});
}
if (operation === 'dashboard.getContextBundleDelta') {
@@ -1370,6 +1372,62 @@ test('the private-message notice moves a mobile reader to the private section',
expect(await page.evaluate(() => window.scrollY)).toBeGreaterThan(500);
});
test('main info fills the eighth slot with Ref-compatible autorun status and an accessible detail tooltip', async ({
page,
}, testInfo) => {
const state: NavigationFixture = {
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 0,
npcMode: 1,
autorunUser: {
limitMinutes: 1_440,
options: ['chief', 'battle', 'train', 'recruit', 'recruit_high', 'warp', 'develop'],
},
generalMeCalls: 0,
operations: [],
};
await installFixture(page, state);
await page.setViewportSize({ width: 1200, height: 900 });
await waitForMain(page);
const gameInfo = page.locator('.legacy-game-info');
const status = page.locator('[data-main-autorun-status]');
const tooltip = page.getByRole('tooltip', {
name: '내정, 순간이동, 모병, 훈련/사기진작, 출병, 사령턴, 24시간 유효',
});
await expect(gameInfo.locator(':scope > *')).toHaveCount(8);
await expect(status).toHaveText(/ : /u);
await expect(status).toHaveAttribute('tabindex', '0');
await expect(tooltip).toBeHidden();
const desktopBefore = await gameInfo.boundingBox();
await status.hover();
await expect(tooltip).toBeVisible();
await expect(tooltip).toHaveText('내정, 순간이동, 모병, 훈련/사기진작, 출병, 사령턴, 24시간 유효');
expect(await status.evaluate((element) => getComputedStyle(element).overflow)).toBe('visible');
expect(await gameInfo.boundingBox()).toEqual(desktopBefore);
await page.screenshot({ path: testInfo.outputPath('main-autorun-hover-desktop-1200.png'), fullPage: false });
await page.setViewportSize({ width: 500, height: 900 });
await status.focus();
await expect(tooltip).toBeVisible();
expect(await status.evaluate((element) => getComputedStyle(element).overflow)).toBe('visible');
const mobileTooltip = await tooltip.boundingBox();
expect(mobileTooltip?.x).toBeGreaterThanOrEqual(16);
expect(mobileTooltip ? mobileTooltip.x + mobileTooltip.width : Infinity).toBeLessThanOrEqual(484);
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(500);
await page.screenshot({ path: testInfo.outputPath('main-autorun-focus-mobile-500.png'), fullPage: false });
state.autorunUser = null;
await page.reload();
await waitForMain(page);
await expect(status).toHaveText('기타 설정: 표준');
await expect(status).not.toHaveAttribute('tabindex', '0');
await expect(page.getByRole('tooltip')).toHaveCount(0);
});
test('desktop menus preserve ref columns, prefix-safe routes, and controlled dropdown behavior', async ({
page,
}, testInfo) => {
@@ -0,0 +1,83 @@
<script setup lang="ts">
import { computed } from 'vue';
import { formatAutorunUserDetail, type AutorunUserSummary } from '../../utils/autorunUser';
const props = defineProps<{
autorun: AutorunUserSummary | null;
}>();
const detail = computed(() => (props.autorun ? formatAutorunUserDetail(props.autorun) : ''));
</script>
<template>
<span
class="main-autorun-status"
:class="{ 'main-autorun-status--enabled': autorun }"
:tabindex="autorun ? 0 : undefined"
:aria-describedby="autorun ? 'main-autorun-detail' : undefined"
data-main-autorun-status
>
기타 설정: {{ autorun ? '자율행동' : '표준' }}
<span v-if="autorun" id="main-autorun-detail" class="main-autorun-detail" role="tooltip">
{{ detail }}
</span>
</span>
</template>
<style scoped>
.main-autorun-status {
position: relative;
}
.main-autorun-status--enabled {
cursor: help;
}
.main-autorun-detail {
position: absolute;
z-index: 40;
top: calc(100% + 6px);
right: 4px;
visibility: hidden;
box-sizing: border-box;
width: max-content;
max-width: min(520px, calc(100vw - 32px));
padding: 6px 8px;
border: 1px solid #52525b;
border-radius: 4px;
background: #18181b;
box-shadow: 0 4px 12px rgb(0 0 0 / 45%);
color: #f4f4f5;
font-size: 12px;
font-weight: 400;
line-height: 1.4;
opacity: 0;
pointer-events: none;
text-align: left;
white-space: normal;
}
.main-autorun-status--enabled:hover .main-autorun-detail,
.main-autorun-status--enabled:focus-visible .main-autorun-detail {
visibility: visible;
opacity: 1;
}
.main-autorun-status--enabled:focus-visible {
outline: 2px solid #fdba74;
outline-offset: -2px;
}
@media (max-width: 939.98px) {
.main-autorun-detail {
position: fixed;
z-index: 100;
top: auto;
right: 16px;
bottom: 61px;
left: 16px;
width: auto;
max-width: none;
}
}
</style>
@@ -0,0 +1,25 @@
export interface AutorunUserSummary {
limitMinutes: number;
options: string[];
}
export const formatAutorunUserDetail = (autorun: AutorunUserSummary): string => {
const enabled = new Set(autorun.options);
const labels: string[] = [];
if (enabled.has('develop')) labels.push('내정');
if (enabled.has('warp')) labels.push('순간이동');
if (enabled.has('recruit_high')) labels.push('모병');
else if (enabled.has('recruit')) labels.push('징병');
if (enabled.has('train')) labels.push('훈련/사기진작');
if (enabled.has('battle')) labels.push('출병');
if (enabled.has('chief')) labels.push('사령턴');
const limit =
autorun.limitMinutes >= 43_200
? '항상 유효'
: autorun.limitMinutes % 60 === 0
? `${autorun.limitMinutes / 60}시간 유효`
: `${autorun.limitMinutes}분 유효`;
labels.push(limit);
return labels.join(', ');
};
+9 -1
View File
@@ -18,6 +18,7 @@ import MainGlobalMenu from '../components/main/MainGlobalMenu.vue';
import MainNationMenu from '../components/main/MainNationMenu.vue';
import MainMobileBottomBar from '../components/main/MainMobileBottomBar.vue';
import MainTurnControls from '../components/main/MainTurnControls.vue';
import MainAutorunStatus from '../components/main/MainAutorunStatus.vue';
import {
defaultGlobalNavigation,
type MainNavigationEntry,
@@ -257,6 +258,7 @@ watch(
<span>국가: {{ lobbyInfo.nationCnt }}</span>
<span>사실/가상: {{ lobbyInfo.fictionMode }}</span>
<span>{{ lobbyInfo.otherTextInfo || '진행 정보 없음' }}</span>
<MainAutorunStatus :autorun="lobbyInfo.autorunUser" />
</section>
<div v-if="error" class="game-feedback game-feedback--error" role="alert">{{ error }}</div>
@@ -728,11 +730,13 @@ button {
}
.legacy-game-info {
position: relative;
z-index: 100;
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));
box-sizing: border-box;
height: 18px;
overflow: hidden;
overflow: visible;
border-top: 1px solid #666;
background: #302016 var(--sammo-texture-walnut);
color: #fff;
@@ -747,6 +751,10 @@ button {
white-space: nowrap;
}
.legacy-game-info > .main-autorun-status {
overflow: visible;
}
.warning {
color: #f5d08a;
font-size: 0.85rem;
@@ -0,0 +1,18 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { formatAutorunUserDetail } from '../src/utils/autorunUser.ts';
void test('uses the Ref and Gateway autorun option order and lets recruit_high replace recruit', () => {
assert.equal(
formatAutorunUserDetail({
limitMinutes: 1_440,
options: ['chief', 'recruit', 'battle', 'train', 'recruit_high', 'warp', 'develop'],
}),
'내정, 순간이동, 모병, 훈련/사기진작, 출병, 사령턴, 24시간 유효'
);
});
void test('formats minute and always-valid autorun limits like Ref', () => {
assert.equal(formatAutorunUserDetail({ limitMinutes: 90, options: ['recruit'] }), '징병, 90분 유효');
assert.equal(formatAutorunUserDetail({ limitMinutes: 43_200, options: ['develop'] }), '내정, 항상 유효');
});