feat: 시나리오 초기화 용어와 도움말 정합화

Ref의 NPC 상성, 확장 NPC, NPC 빙의와 자율행동 표현으로 Gateway 초기화 옵션을 통일한다. 접힌 고급 옵션에 접근 가능한 소형 도움말을 추가하고 desktop/mobile Chromium 회귀를 보강한다.
This commit is contained in:
2026-08-20 05:16:19 +00:00
parent fafe4909dc
commit e33478a7e0
5 changed files with 483 additions and 140 deletions
@@ -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<HTMLElement>('[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);
@@ -0,0 +1,45 @@
<script setup lang="ts">
import { useId } from 'vue';
const props = defineProps<{
label: string;
text: string;
testId?: string;
}>();
const tooltipId = useId();
</script>
<template>
<span class="group relative inline-flex shrink-0 align-middle">
<button
type="button"
class="inline-flex size-[18px] items-center justify-center rounded-full border border-zinc-600 bg-zinc-900 text-[11px] font-bold leading-none text-zinc-300 transition-colors duration-100 hover:border-sky-400 hover:text-sky-200 focus-visible:border-sky-400 focus-visible:text-sky-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-400"
:aria-label="`${props.label} 도움말`"
:aria-describedby="tooltipId"
:data-testid="props.testId"
>
?
</button>
<span
:id="tooltipId"
role="tooltip"
class="compact-help-tooltip pointer-events-none invisible absolute left-0 top-[calc(100%+0.4rem)] z-40 w-72 max-w-[calc(100vw-2rem)] rounded border border-zinc-600 bg-zinc-950 px-3 py-2 text-left text-xs font-normal leading-5 text-zinc-100 opacity-0 shadow-xl transition-opacity duration-100 group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100"
:data-testid="props.testId ? `${props.testId}-tooltip` : undefined"
>
{{ props.text }}
</span>
</span>
</template>
<style scoped>
@media (max-width: 639px) {
.compact-help-tooltip {
position: fixed;
inset: auto 1rem 1rem;
width: auto;
max-width: none;
z-index: 60;
}
}
</style>
@@ -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;
+35 -41
View File
@@ -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<Record<string, string>>({});
const profileActionSubmitting = ref<Record<string, boolean>>({});
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(() => {
</p>
<div class="mt-3 grid gap-3 text-xs sm:grid-cols-2">
<label>
턴 간격
{{ RESET_OPTION_COPY.turnTerm.label }}
<select
v-model.number="
profileEdits[profile.profileName].resetDefaults.turnTermMinutes
@@ -2265,29 +2258,29 @@ onMounted(() => {
</select>
</label>
<label>
가입 방식
{{ RESET_OPTION_COPY.joinMode.label }}
<select
v-model="profileEdits[profile.profileName].resetDefaults.joinMode"
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2"
>
<option value="full">전체</option>
<option value="onlyRandom">랜덤</option>
<option value="full">일반</option>
<option value="onlyRandom">랜덤 임관</option>
</select>
</label>
<label>
가상 장수
{{ RESET_OPTION_COPY.fiction.label }}
<select
v-model.number="
profileEdits[profile.profileName].resetDefaults.fiction
"
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2"
>
<option :value="1">허용</option>
<option :value="0">금지</option>
<option :value="0">연의</option>
<option :value="1">가상</option>
</select>
</label>
<label>
장수 생성 제한
{{ RESET_OPTION_COPY.blockGeneralCreate.label }}
<select
v-model.number="
profileEdits[profile.profileName].resetDefaults
@@ -2301,7 +2294,7 @@ onMounted(() => {
</select>
</label>
<label>
NPC 모드
{{ RESET_OPTION_COPY.npcMode.label }}
<select
v-model.number="
profileEdits[profile.profileName].resetDefaults.npcMode
@@ -2309,22 +2302,23 @@ onMounted(() => {
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2"
data-testid="meta-reset-npc-mode"
>
<option :value="0">기본</option>
<option :value="1">확장</option>
<option :value="2">전체</option>
<option :value="0">불가</option>
<option :value="1">가능</option>
<option :value="2">선택 생성 가능</option>
</select>
</label>
<label>
이미지 표시
{{ RESET_OPTION_COPY.showImgLevel.label }}
<select
v-model.number="
profileEdits[profile.profileName].resetDefaults.showImgLevel
"
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2"
>
<option v-for="level in [0, 1, 2, 3]" :key="level" :value="level">
{{ level }}
</option>
<option :value="0">안함</option>
<option :value="1">전콘</option>
<option :value="2">전콘, 병종</option>
<option :value="3">전콘, 병종, NPC</option>
</select>
</label>
<label class="flex items-center gap-2">
@@ -2332,14 +2326,14 @@ onMounted(() => {
v-model="profileEdits[profile.profileName].resetDefaults.sync"
type="checkbox"
/>
동기화 사용
{{ RESET_OPTION_COPY.sync.label }} 사용
</label>
<label class="flex items-center gap-2">
<input
v-model="profileEdits[profile.profileName].resetDefaults.extend"
type="checkbox"
/>
연장 사용
{{ RESET_OPTION_COPY.extend.label }} 포함
</label>
<label class="flex items-center gap-2">
<input
@@ -2348,7 +2342,7 @@ onMounted(() => {
"
type="checkbox"
/>
토너먼트 사용
{{ RESET_OPTION_COPY.tournamentTrig.label }}
</label>
<label class="flex items-center gap-2">
<input
@@ -2356,7 +2350,7 @@ onMounted(() => {
type="checkbox"
@change="ensureResetAutorun(profile.profileName)"
/>
유저 자동턴
{{ RESET_OPTION_COPY.autorun.label }}
</label>
<template
v-if="
@@ -2365,7 +2359,7 @@ onMounted(() => {
"
>
<label>
자동턴 제한 분
{{ RESET_OPTION_COPY.autorunLimit.label }}
<input
v-model.number="
profileEdits[profile.profileName].resetDefaults.autorunUser!
@@ -2379,7 +2373,7 @@ onMounted(() => {
</label>
<div class="flex flex-wrap items-center gap-3 sm:col-span-2">
<label
v-for="option in resetAutorunLabels"
v-for="option in RESET_AUTORUN_LABELS"
:key="option.value"
class="flex items-center gap-1"
>
@@ -2455,7 +2449,7 @@ onMounted(() => {
새로고침해 주세요.
</p>
<label class="block text-xs text-zinc-400">
턴 간격
{{ RESET_OPTION_COPY.turnTerm.label }}
<select
v-model.number="profileActions[profile.profileName].turnTermMinutes"
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2 text-sm text-white"
@@ -2471,7 +2465,7 @@ onMounted(() => {
</select>
</label>
<label class="block text-xs text-zinc-400">
장수 생성 제한
{{ RESET_OPTION_COPY.blockGeneralCreate.label }}
<select
v-model.number="profileActions[profile.profileName].blockGeneralCreate"
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2 text-sm text-white"
@@ -2488,11 +2482,11 @@ onMounted(() => {
type="checkbox"
data-testid="runtime-autorun-enabled"
/>
유저 자동턴 사용
{{ RESET_OPTION_COPY.autorun.label }} 사용
</label>
<template v-if="profileActions[profile.profileName].autorunEnabled">
<label class="block text-xs text-zinc-400">
자동턴 제한 분
{{ RESET_OPTION_COPY.autorunLimit.label }}
<input
v-model="profileActions[profile.profileName].autorunUserMinutes"
type="number"
@@ -2505,7 +2499,7 @@ onMounted(() => {
</label>
<div class="flex flex-wrap items-center gap-3 text-xs text-zinc-300">
<label
v-for="option in resetAutorunLabels"
v-for="option in RESET_AUTORUN_LABELS"
:key="`runtime-${option.value}`"
class="flex items-center gap-1"
>
@@ -2,11 +2,14 @@
import { formatServerDateTime, serverDateTimeInputToIso } from '@sammo-ts/common';
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import CompactHelp from '../components/CompactHelp.vue';
import ServerProfileTabs from '../components/ServerProfileTabs.vue';
import { useToast } from '../composables/useToast';
import AdminConsoleLayout from '../layouts/AdminConsoleLayout.vue';
import {
normalizeProfileResetDefaults,
RESET_AUTORUN_LABELS,
RESET_OPTION_COPY,
SYSTEM_PROFILE_RESET_DEFAULTS,
type ProfileResetDefaults,
type ResetAutorunOption,
@@ -150,6 +153,15 @@ const form = reactive({
scheduledAt: '',
reason: '',
});
const RESET_AUTORUN_FORM_KEYS = {
develop: 'autorunDevelop',
warp: 'autorunWarp',
recruit: 'autorunRecruit',
recruit_high: 'autorunRecruitHigh',
train: 'autorunTrain',
battle: 'autorunBattle',
chief: 'autorunChief',
} as const satisfies Record<ResetAutorunOption, keyof typeof form>;
const gatewayForm = reactive({
sourceMode: 'BRANCH' as 'BRANCH' | 'COMMIT',
sourceRef: 'main',
@@ -1037,11 +1049,19 @@ onBeforeUnmount(() => {
</option>
</select>
</label>
<label class="text-xs text-zinc-400">
간격
<div class="space-y-1 text-xs text-zinc-400">
<div class="flex items-center gap-1.5">
<label for="reset-turn-term">{{ RESET_OPTION_COPY.turnTerm.label }}</label>
<CompactHelp
:label="RESET_OPTION_COPY.turnTerm.label"
:text="RESET_OPTION_COPY.turnTerm.help"
test-id="reset-help-turn-term"
/>
</div>
<select
id="reset-turn-term"
v-model.number="form.turnTermMinutes"
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-white"
class="w-full rounded border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-white"
data-testid="reset-turn-term"
>
<option
@@ -1052,7 +1072,7 @@ onBeforeUnmount(() => {
{{ minutes }}
</option>
</select>
</label>
</div>
</div>
<details v-if="mode === 'scenario'" class="rounded border border-zinc-800 bg-zinc-950/50 p-4">
@@ -1064,104 +1084,232 @@ onBeforeUnmount(() => {
: '서버별 기본값이 없어 시스템 기본값을 적용했습니다.'
}}
</p>
<div class="mt-4 grid gap-4 md:grid-cols-2 text-sm">
<label
>동기화
<select v-model="form.sync" class="ml-2 rounded bg-zinc-900 px-2 py-1">
<option :value="true">사용</option>
<option :value="false">미사용</option>
</select>
</label>
<label
>가상 장수
<select v-model.number="form.fiction" class="ml-2 rounded bg-zinc-900 px-2 py-1">
<option :value="1">허용</option>
<option :value="0">금지</option>
</select>
</label>
<label
>연장
<select v-model="form.extend" class="ml-2 rounded bg-zinc-900 px-2 py-1">
<option :value="true">사용</option>
<option :value="false">미사용</option>
</select>
</label>
<label
>가입 방식
<select v-model="form.joinMode" class="ml-2 rounded bg-zinc-900 px-2 py-1">
<option value="full">전체</option>
<option value="onlyRandom">랜덤만</option>
</select>
</label>
<label
>장수 생성 제한
<div class="mt-4 grid gap-4 text-sm md:grid-cols-2">
<div class="space-y-1">
<div class="flex items-center gap-1.5">
<label for="reset-sync">{{ RESET_OPTION_COPY.sync.label }}</label>
<CompactHelp
:label="RESET_OPTION_COPY.sync.label"
:text="RESET_OPTION_COPY.sync.help"
test-id="reset-help-sync"
/>
</div>
<select
v-model.number="form.blockGeneralCreate"
class="ml-2 rounded bg-zinc-900 px-2 py-1"
id="reset-sync"
v-model="form.sync"
class="w-full rounded bg-zinc-900 px-2 py-1.5"
>
<option :value="0">없음</option>
<option :value="1">제한</option>
<option :value="2">차단</option>
<option :value="true">사용</option>
<option :value="false">미사용</option>
</select>
</label>
<label
>NPC 모드
</div>
<div class="space-y-1">
<div class="flex items-center gap-1.5">
<label for="reset-fiction">{{ RESET_OPTION_COPY.fiction.label }}</label>
<CompactHelp
:label="RESET_OPTION_COPY.fiction.label"
:text="RESET_OPTION_COPY.fiction.help"
test-id="reset-help-fiction"
/>
</div>
<select
id="reset-fiction"
v-model.number="form.fiction"
class="w-full rounded bg-zinc-900 px-2 py-1.5"
data-testid="reset-fiction"
>
<option :value="0">연의</option>
<option :value="1">가상</option>
</select>
</div>
<div class="space-y-1">
<div class="flex items-center gap-1.5">
<label for="reset-extend">{{ RESET_OPTION_COPY.extend.label }}</label>
<CompactHelp
:label="RESET_OPTION_COPY.extend.label"
:text="RESET_OPTION_COPY.extend.help"
test-id="reset-help-extend"
/>
</div>
<select
id="reset-extend"
v-model="form.extend"
class="w-full rounded bg-zinc-900 px-2 py-1.5"
data-testid="reset-extend"
>
<option :value="true">포함</option>
<option :value="false">미포함</option>
</select>
</div>
<div class="space-y-1">
<div class="flex items-center gap-1.5">
<label for="reset-block-general-create">
{{ RESET_OPTION_COPY.blockGeneralCreate.label }}
</label>
<CompactHelp
:label="RESET_OPTION_COPY.blockGeneralCreate.label"
:text="RESET_OPTION_COPY.blockGeneralCreate.help"
test-id="reset-help-block-general-create"
/>
</div>
<select
id="reset-block-general-create"
v-model.number="form.blockGeneralCreate"
class="w-full rounded bg-zinc-900 px-2 py-1.5"
data-testid="reset-block-general-create"
>
<option :value="0">가능</option>
<option :value="2">장수명 무작위</option>
<option :value="1">불가</option>
</select>
</div>
<div class="space-y-1">
<div class="flex items-center gap-1.5">
<label for="reset-npc-mode">{{ RESET_OPTION_COPY.npcMode.label }}</label>
<CompactHelp
:label="RESET_OPTION_COPY.npcMode.label"
:text="RESET_OPTION_COPY.npcMode.help"
test-id="reset-help-npc-mode"
/>
</div>
<select
id="reset-npc-mode"
v-model.number="form.npcMode"
class="ml-2 rounded bg-zinc-900 px-2 py-1"
class="w-full rounded bg-zinc-900 px-2 py-1.5"
data-testid="reset-npc-mode"
>
<option :value="0">기본</option>
<option :value="1">확장</option>
<option :value="2">전체</option>
<option :value="0">불가</option>
<option :value="1">가능</option>
<option :value="2">선택 생성 가능</option>
</select>
</label>
<label
>이미지 표시
<select v-model.number="form.showImgLevel" class="ml-2 rounded bg-zinc-900 px-2 py-1">
<option v-for="level in [0, 1, 2, 3]" :key="level" :value="level">
{{ level }}
</option>
</select>
</label>
<label class="flex items-center gap-2">
<input v-model="form.tournamentTrig" type="checkbox" /> 토너먼트 사용
</label>
<label class="flex items-center gap-2">
<input v-model="form.autorunEnabled" type="checkbox" /> 유저 자동턴
</label>
<input
v-if="form.autorunEnabled"
v-model.number="form.autorunUserMinutes"
type="number"
min="1"
max="43200"
class="rounded border border-zinc-700 bg-zinc-900 px-2 py-1"
aria-label="자동턴 제한 "
/>
<div v-if="form.autorunEnabled" class="md:col-span-2 flex flex-wrap gap-3 text-xs">
<label><input v-model="form.autorunDevelop" type="checkbox" /> 내정</label>
<label><input v-model="form.autorunWarp" type="checkbox" /> 이동</label>
<label><input v-model="form.autorunRecruit" type="checkbox" /> 징병</label>
<label
><input
v-model="form.autorunRecruitHigh"
type="checkbox"
data-testid="reset-autorun-recruit-high"
/>
고급 징병</label
>
<label><input v-model="form.autorunTrain" type="checkbox" /> 훈련</label>
<label><input v-model="form.autorunBattle" type="checkbox" /> 전투</label>
<label
><input
v-model="form.autorunChief"
type="checkbox"
data-testid="reset-autorun-chief"
/>
중신</label
>
</div>
<div class="space-y-1">
<div class="flex items-center gap-1.5">
<label for="reset-autorun-enabled">{{ RESET_OPTION_COPY.autorun.label }}</label>
<CompactHelp
:label="RESET_OPTION_COPY.autorun.label"
:text="RESET_OPTION_COPY.autorun.help"
test-id="reset-help-autorun"
/>
</div>
<select
id="reset-autorun-enabled"
v-model="form.autorunEnabled"
class="w-full rounded bg-zinc-900 px-2 py-1.5"
data-testid="reset-autorun-enabled"
>
<option :value="false">꺼짐</option>
<option :value="true">사용</option>
</select>
</div>
<div class="space-y-1">
<div class="flex items-center gap-1.5">
<label for="reset-join-mode">{{ RESET_OPTION_COPY.joinMode.label }}</label>
<CompactHelp
:label="RESET_OPTION_COPY.joinMode.label"
:text="RESET_OPTION_COPY.joinMode.help"
test-id="reset-help-join-mode"
/>
</div>
<select
id="reset-join-mode"
v-model="form.joinMode"
class="w-full rounded bg-zinc-900 px-2 py-1.5"
data-testid="reset-join-mode"
>
<option value="full">일반</option>
<option value="onlyRandom">랜덤 임관</option>
</select>
</div>
<div class="space-y-1">
<div class="flex items-center gap-1.5">
<label for="reset-show-img-level">{{ RESET_OPTION_COPY.showImgLevel.label }}</label>
<CompactHelp
:label="RESET_OPTION_COPY.showImgLevel.label"
:text="RESET_OPTION_COPY.showImgLevel.help"
test-id="reset-help-show-img-level"
/>
</div>
<select
id="reset-show-img-level"
v-model.number="form.showImgLevel"
class="w-full rounded bg-zinc-900 px-2 py-1.5"
data-testid="reset-show-img-level"
>
<option :value="0">안함</option>
<option :value="1">전콘</option>
<option :value="2">전콘, 병종</option>
<option :value="3">전콘, 병종, NPC</option>
</select>
</div>
<div class="space-y-1">
<div class="flex items-center gap-1.5">
<label for="reset-tournament-trig">
{{ RESET_OPTION_COPY.tournamentTrig.label }}
</label>
<CompactHelp
:label="RESET_OPTION_COPY.tournamentTrig.label"
:text="RESET_OPTION_COPY.tournamentTrig.help"
test-id="reset-help-tournament-trig"
/>
</div>
<select
id="reset-tournament-trig"
v-model="form.tournamentTrig"
class="w-full rounded bg-zinc-900 px-2 py-1.5"
data-testid="reset-tournament-trig"
>
<option :value="false">수동</option>
<option :value="true">자동</option>
</select>
</div>
<div v-if="form.autorunEnabled" class="space-y-1">
<div class="flex items-center gap-1.5">
<label for="reset-autorun-minutes">{{
RESET_OPTION_COPY.autorunLimit.label
}}</label>
<CompactHelp
:label="RESET_OPTION_COPY.autorunLimit.label"
:text="RESET_OPTION_COPY.autorunLimit.help"
test-id="reset-help-autorun-limit"
/>
</div>
<input
id="reset-autorun-minutes"
v-model.number="form.autorunUserMinutes"
type="number"
min="1"
max="43200"
class="w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-1.5"
data-testid="reset-autorun-minutes"
/>
</div>
<fieldset
v-if="form.autorunEnabled"
class="space-y-2 rounded border border-zinc-800 p-3 md:col-span-2"
>
<legend class="px-1 text-xs text-zinc-400">자율행동 종류</legend>
<div class="flex flex-wrap gap-x-4 gap-y-2 text-xs">
<label
v-for="option in RESET_AUTORUN_LABELS"
:key="option.value"
class="flex items-center gap-1.5"
>
<input
v-model="form[RESET_AUTORUN_FORM_KEYS[option.value]]"
type="checkbox"
:data-testid="
option.value === 'recruit_high'
? 'reset-autorun-recruit-high'
: option.value === 'chief'
? 'reset-autorun-chief'
: undefined
"
/>
{{ option.label }}
</label>
</div>
</fieldset>
</div>
</details>
@@ -1230,9 +1378,9 @@ onBeforeUnmount(() => {
class="rounded border border-amber-800/80 bg-amber-950/30 px-4 py-3 text-sm text-amber-100"
data-testid="gateway-build-recovery-guide"
>
<strong>빌드가 멈춘 경우:</strong> 로그의 마지막 단계가 build일 때만
<strong>빌드 중단</strong> 누르고 CANCELLED를 확인한 <strong>재시도</strong>하세요.
migration 또는 process 전환이 시작된 뒤에는 DB와 runtime 보호를 위해 중단할 없습니다.
<strong>빌드가 멈춘 경우:</strong> 로그의 마지막 단계가 build일 때만 <strong>빌드 중단</strong>
누르고 CANCELLED를 확인한 <strong>재시도</strong>하세요. migration 또는 process 전환이 시작된
뒤에는 DB와 runtime 보호를 위해 중단할 없습니다.
</div>
<div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div>
@@ -1541,9 +1689,9 @@ onBeforeUnmount(() => {
class="mb-4 rounded border border-amber-800/80 bg-amber-950/30 px-4 py-3 text-sm text-amber-100"
data-testid="profile-build-recovery-guide"
>
<strong>DB 보존 업데이트 빌드가 멈춘 경우:</strong> <strong>빌드 중단</strong>을 누르고
CANCELLED를 확인한 뒤 <strong>재시도</strong>하세요. 기존 profile runtime과 게임 DB는
유지됩니다. RESET·migration·process 전환 단계는 이 화면에서 강제 중단하지 않습니다.
<strong>DB 보존 업데이트 빌드가 멈춘 경우:</strong> <strong>빌드 중단</strong>을 누르고 CANCELLED를
확인한 뒤 <strong>재시도</strong>하세요. 기존 profile runtime과 게임 DB는 유지됩니다.
RESET·migration·process 전환 단계는 이 화면에서 강제 중단하지 않습니다.
</div>
<div class="mb-4 flex items-center justify-between">
<h3 class="text-lg font-semibold">작업 이력</h3>