diff --git a/app/gateway-frontend/e2e/server-operations.spec.ts b/app/gateway-frontend/e2e/server-operations.spec.ts index fcdd3e87..86369af4 100644 --- a/app/gateway-frontend/e2e/server-operations.spec.ts +++ b/app/gateway-frontend/e2e/server-operations.spec.ts @@ -1010,6 +1010,105 @@ test('loads server metadata defaults into the reset form and submits them', asyn expect(request).toContain('"options":["develop","recruit_high","chief"]'); }); +test('uses ref reset terms with compact hover, focus, and mobile help', async ({ page }, testInfo) => { + const state: FixtureState = { + operations: [], + gatewayOperations: [], + runtimeRunning: true, + requestBodies: [], + }; + await installFixture(page, state); + + await page.goto('admin/servers/che%3Adefault/scenario'); + await page.getByText('고급 시나리오 옵션').click(); + + await expect(page.getByText('NPC 상성', { exact: true })).toBeVisible(); + await expect(page.getByText('확장 NPC', { exact: true })).toBeVisible(); + await expect(page.getByText('장수 임의 생성', { exact: true })).toBeVisible(); + await expect(page.getByText('NPC 빙의', { exact: true })).toBeVisible(); + await expect(page.getByText('자율행동', { exact: true })).toBeVisible(); + await expect(page.getByText('임관 모드', { exact: true })).toBeVisible(); + await expect(page.getByText('이미지 표기', { exact: true })).toBeVisible(); + await expect(page.getByText('토너먼트 자동 시작', { exact: true })).toBeVisible(); + await expect(page.getByTestId('reset-fiction').locator('option')).toHaveText(['연의', '가상']); + await expect(page.getByTestId('reset-extend').locator('option')).toHaveText(['포함', '미포함']); + await expect(page.getByTestId('reset-block-general-create').locator('option')).toHaveText([ + '가능', + '장수명 무작위', + '불가', + ]); + await expect(page.getByTestId('reset-npc-mode').locator('option')).toHaveText(['불가', '가능', '선택 생성 가능']); + await expect(page.getByTestId('reset-show-img-level').locator('option')).toHaveText([ + '안함', + '전콘', + '전콘, 병종', + '전콘, 병종, NPC', + ]); + + const helpButtons = page.getByRole('button', { name: /도움말$/ }); + await expect(helpButtons).toHaveCount(10); + const fictionHelp = page.getByTestId('reset-help-fiction'); + const fictionTooltip = page.getByTestId('reset-help-fiction-tooltip'); + await expect(fictionTooltip).toBeHidden(); + await fictionHelp.hover(); + await expect(fictionTooltip).toBeVisible(); + await expect(fictionTooltip).toContainText('연의는 서버 정보에서 사실 모드로도 표시'); + await expect.poll(() => fictionTooltip.evaluate((element) => getComputedStyle(element).opacity)).toBe('1'); + const hoverMetrics = await fictionHelp.evaluate((element) => { + const buttonRect = element.getBoundingClientRect(); + const tooltip = document.querySelector('[data-testid="reset-help-fiction-tooltip"]'); + const tooltipRect = tooltip?.getBoundingClientRect(); + const buttonStyle = getComputedStyle(element); + const tooltipStyle = tooltip ? getComputedStyle(tooltip) : null; + return { + button: { width: buttonRect.width, height: buttonRect.height }, + tooltip: tooltipRect + ? { x: tooltipRect.x, y: tooltipRect.y, width: tooltipRect.width, height: tooltipRect.height } + : null, + buttonBorderRadius: buttonStyle.borderRadius, + tooltipVisibility: tooltipStyle?.visibility, + tooltipOpacity: tooltipStyle?.opacity, + }; + }); + expect(hoverMetrics.button).toEqual({ width: 18, height: 18 }); + expect(hoverMetrics.tooltipVisibility).toBe('visible'); + expect(hoverMetrics.tooltipOpacity).toBe('1'); + + await page.mouse.move(0, 0); + const npcHelp = page.getByTestId('reset-help-npc-mode'); + await npcHelp.focus(); + await expect(page.getByTestId('reset-help-npc-mode-tooltip')).toBeVisible(); + await expect(npcHelp).toBeFocused(); + + await page.getByTestId('reset-autorun-enabled').selectOption({ label: '사용' }); + await expect(page.getByText('자율행동 종류', { exact: true })).toBeVisible(); + for (const action of ['내정', '순간이동', '징병', '모병', '훈사', '출병', '기본 사령턴']) { + await expect(page.getByText(action, { exact: true })).toBeVisible(); + } + + await page.setViewportSize({ width: 390, height: 844 }); + const mobileHelp = page.getByTestId('reset-help-npc-mode'); + await mobileHelp.focus(); + const mobileMetrics = await page.getByTestId('reset-help-npc-mode-tooltip').evaluate((element) => { + const rect = element.getBoundingClientRect(); + return { + x: rect.x, + right: rect.right, + width: rect.width, + viewportWidth: document.documentElement.clientWidth, + documentScrollWidth: document.documentElement.scrollWidth, + }; + }); + expect(mobileMetrics.x).toBeGreaterThanOrEqual(0); + expect(mobileMetrics.right).toBeLessThanOrEqual(mobileMetrics.viewportWidth); + expect(mobileMetrics.documentScrollWidth).toBeLessThanOrEqual(mobileMetrics.viewportWidth); + await page.screenshot({ path: testInfo.outputPath('mobile-reset-option-help.png'), fullPage: true }); + await writeFile( + testInfo.outputPath('reset-option-help-metrics.json'), + JSON.stringify({ hoverMetrics, mobileMetrics }, null, 2) + ); +}); + test('edits server reset defaults through profile metadata settings', async ({ page }, testInfo) => { const state: FixtureState = { operations: [], gatewayOperations: [], runtimeRunning: true, requestBodies: [] }; await installFixture(page, state); diff --git a/app/gateway-frontend/src/components/CompactHelp.vue b/app/gateway-frontend/src/components/CompactHelp.vue new file mode 100644 index 00000000..e89e4040 --- /dev/null +++ b/app/gateway-frontend/src/components/CompactHelp.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/app/gateway-frontend/src/utils/resetDefaults.ts b/app/gateway-frontend/src/utils/resetDefaults.ts index 1d855431..bd39095f 100644 --- a/app/gateway-frontend/src/utils/resetDefaults.ts +++ b/app/gateway-frontend/src/utils/resetDefaults.ts @@ -11,6 +11,63 @@ export const RESET_AUTORUN_OPTIONS = [ export type ResetAutorunOption = (typeof RESET_AUTORUN_OPTIONS)[number]; +export const RESET_AUTORUN_LABELS: ReadonlyArray<{ value: ResetAutorunOption; label: string }> = [ + { value: 'develop', label: '내정' }, + { value: 'warp', label: '순간이동' }, + { value: 'recruit', label: '징병' }, + { value: 'recruit_high', label: '모병' }, + { value: 'train', label: '훈사' }, + { value: 'battle', label: '출병' }, + { value: 'chief', label: '기본 사령턴' }, +]; + +export const RESET_OPTION_COPY = { + turnTerm: { + label: '턴 시간(분)', + help: '한 턴이 진행되는 실제 시간입니다. ref 초기화 화면과 같은 분 단위 값입니다.', + }, + sync: { + label: '시간 동기화', + help: '실제 시각의 시간 단위에 게임의 년·월 경계를 맞춥니다. ref 기준 120분은 오전 1시, 60분은 오전·오후 1시에 1월이 시작됩니다.', + }, + fiction: { + label: 'NPC 상성', + help: 'ref의 연의/가상 모드입니다. 연의는 서버 정보에서 사실 모드로도 표시됩니다. 가상 장수 생성 허용 여부를 뜻하는 옵션은 아닙니다.', + }, + extend: { + label: '확장 NPC', + help: '시나리오에 별도로 정의된 확장 장수 목록을 초기 배치에 포함할지 정합니다. 게임 기간을 연장하는 옵션은 아닙니다.', + }, + blockGeneralCreate: { + label: '장수 임의 생성', + help: '가능은 이름을 직접 정해 생성, 장수명 무작위는 생성은 허용하되 이름을 무작위로 지정, 불가는 일반 장수 생성을 막습니다.', + }, + npcMode: { + label: 'NPC 빙의', + help: '불가는 빙의를 막고, 가능은 배치된 NPC에 빙의할 수 있게 합니다. 선택 생성 가능은 장수 풀에서 대상을 골라 생성하는 방식입니다.', + }, + autorun: { + label: '자율행동', + help: 'ref 초기화 화면의 휴식 턴 시 장수 턴입니다. 설정한 유효 시간 동안 휴식 턴에 선택한 행동을 대신 수행합니다. 훈사는 훈련/사기진작을 뜻합니다.', + }, + autorunLimit: { + label: '유효 시간(분)', + help: '자율행동을 사용할 수 있는 기간입니다. 43200분은 ref의 항상 유효에 해당합니다.', + }, + joinMode: { + label: '임관 모드', + help: '일반은 지원되는 임관 방식을 허용하고, 랜덤 임관은 무작위 국가 임관만 허용합니다.', + }, + showImgLevel: { + label: '이미지 표기', + help: '전콘은 장수 개인 아이콘입니다. 단계에 따라 전콘, 병종 이미지, NPC 이미지를 차례로 표시합니다.', + }, + tournamentTrig: { + label: '토너먼트 자동 시작', + help: '자동은 기수 진행 중 토너먼트를 자동으로 시작하고, 수동은 자동 시작을 끕니다.', + }, +} as const; + export type ProfileResetDefaults = { turnTermMinutes: number; sync: boolean; diff --git a/app/gateway-frontend/src/views/AdminView.vue b/app/gateway-frontend/src/views/AdminView.vue index a78367ef..f1277e1f 100644 --- a/app/gateway-frontend/src/views/AdminView.vue +++ b/app/gateway-frontend/src/views/AdminView.vue @@ -13,6 +13,8 @@ import { useToast } from '../composables/useToast'; import { normalizeProfileResetDefaults, PROFILE_TURN_TERM_MINUTES, + RESET_AUTORUN_LABELS, + RESET_OPTION_COPY, type ProfileResetDefaults, type ResetAutorunOption, } from '../utils/resetDefaults'; @@ -431,15 +433,6 @@ const profileActions = ref< } > >({}); -const resetAutorunLabels: Array<{ value: ResetAutorunOption; label: string }> = [ - { value: 'develop', label: '내정' }, - { value: 'warp', label: '이동' }, - { value: 'recruit', label: '징병' }, - { value: 'recruit_high', label: '고급 징병' }, - { value: 'train', label: '훈련' }, - { value: 'battle', label: '전투' }, - { value: 'chief', label: '참모' }, -]; const profileActionStatus = ref>({}); const profileActionSubmitting = ref>({}); const visibleProfiles = computed(() => @@ -688,7 +681,7 @@ const ensureProfileBuffers = (profile: AdminProfile) => { blockGeneralCreate: settings.blockGeneralCreate, autorunEnabled: settings.autorunUser !== null, autorunUserMinutes: String(settings.autorunUser?.limitMinutes ?? 1440), - autorunOptions: settings.autorunUser?.options.slice() ?? resetAutorunLabels.map(({ value }) => value), + autorunOptions: settings.autorunUser?.options.slice() ?? RESET_AUTORUN_LABELS.map(({ value }) => value), }; } }; @@ -701,7 +694,7 @@ const syncRuntimeSettingsBuffer = (profile: AdminProfile): void => { buffer.blockGeneralCreate = settings.blockGeneralCreate; buffer.autorunEnabled = settings.autorunUser !== null; buffer.autorunUserMinutes = String(settings.autorunUser?.limitMinutes ?? 1440); - buffer.autorunOptions = settings.autorunUser?.options.slice() ?? resetAutorunLabels.map(({ value }) => value); + buffer.autorunOptions = settings.autorunUser?.options.slice() ?? RESET_AUTORUN_LABELS.map(({ value }) => value); }; const toLocalInputValue = (value: string): string => toServerDateTimeInputValue(value); @@ -809,7 +802,7 @@ const updateProfileMeta = async (profileName: string) => { ) { profileActionStatus.value = { ...profileActionStatus.value, - [profileName]: '유저 자동턴은 제한 시간과 한 개 이상의 동작을 선택해야 합니다.', + [profileName]: '자율행동은 유효 시간과 한 개 이상의 행동을 선택해야 합니다.', }; return; } @@ -855,7 +848,7 @@ const ensureResetAutorun = (profileName: string) => { if (!edit?.resetAutorunEnabled || edit.resetDefaults.autorunUser) return; edit.resetDefaults.autorunUser = { limitMinutes: 1440, - options: resetAutorunLabels.map(({ value }) => value), + options: RESET_AUTORUN_LABELS.map(({ value }) => value), }; }; @@ -896,7 +889,7 @@ const requestProfileAction = async (profileName: string, action: AdminAction) => [profileName]: !reason || reason.length < 3 ? '현재 기수 설정 변경 사유를 입력하세요.' - : '턴 간격과 유저 자동턴 값을 확인하세요.', + : '턴 시간과 자율행동 값을 확인하세요.', }; profileActionSubmitting.value = { ...profileActionSubmitting.value, [profileName]: false }; return; @@ -2247,7 +2240,7 @@ onMounted(() => {