feat: 시나리오 초기화 용어와 도움말 정합화
Ref의 NPC 상성, 확장 NPC, NPC 빙의와 자율행동 표현으로 Gateway 초기화 옵션을 통일한다. 접힌 고급 옵션에 접근 가능한 소형 도움말을 추가하고 desktop/mobile Chromium 회귀를 보강한다.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user