merge: 최종 main을 Profile 포괄 권한 제거에 통합
This commit is contained in:
@@ -8,8 +8,8 @@ const operationNames = (route: Route): string[] => {
|
||||
|
||||
type RuntimeAction = {
|
||||
id: string;
|
||||
action: 'ACCELERATE' | 'DELAY';
|
||||
durationMinutes: number;
|
||||
action: 'ACCELERATE' | 'DELAY' | 'UPDATE_RUNTIME_SETTINGS';
|
||||
durationMinutes: number | null;
|
||||
status: 'REQUESTED' | 'PARTIAL' | 'APPLIED' | 'FAILED' | 'IGNORED';
|
||||
detail: string;
|
||||
handler: string | null;
|
||||
@@ -45,6 +45,7 @@ const installFixture = async (
|
||||
let installRequested = false;
|
||||
let installActive = false;
|
||||
let postRequestProfileReads = 0;
|
||||
let requestedRuntimeSettings = false;
|
||||
const requestBodies: unknown[] = [];
|
||||
let releaseRequest = (): void => {};
|
||||
const requestGate = options.deferRequest
|
||||
@@ -66,6 +67,7 @@ const installFixture = async (
|
||||
const operations = operationNames(route);
|
||||
if (operations.includes('admin.profiles.requestAction')) {
|
||||
requested = true;
|
||||
requestedRuntimeSettings = JSON.stringify(body).includes('UPDATE_RUNTIME_SETTINGS');
|
||||
requestBodies.push(body);
|
||||
await requestGate;
|
||||
}
|
||||
@@ -171,6 +173,20 @@ const installFixture = async (
|
||||
status: options.profileStatus ?? 'RUNNING',
|
||||
buildStatus: 'SUCCEEDED',
|
||||
meta: {},
|
||||
runtimeSettings: requestedRuntimeSettings
|
||||
? {
|
||||
turnTermMinutes: 20,
|
||||
blockGeneralCreate: 1,
|
||||
autorunUser: {
|
||||
limitMinutes: 720,
|
||||
options: ['develop', 'recruit_high', 'chief'],
|
||||
},
|
||||
}
|
||||
: {
|
||||
turnTermMinutes: 10,
|
||||
blockGeneralCreate: 2,
|
||||
autorunUser: null,
|
||||
},
|
||||
activeOperation: installActive
|
||||
? {
|
||||
id: '77777777-7777-4777-8777-777777777777',
|
||||
@@ -189,11 +205,20 @@ const installFixture = async (
|
||||
runtimeActions: keepPending
|
||||
? [runtimeAction('REQUESTED')]
|
||||
: requested
|
||||
? (options.afterRequestActions ?? [
|
||||
runtimeAction('APPLIED', {
|
||||
detail: '15분 가속 · 장수 2명 · 경매 1건',
|
||||
}),
|
||||
])
|
||||
? (options.afterRequestActions ??
|
||||
(requestedRuntimeSettings
|
||||
? [
|
||||
runtimeAction('APPLIED', {
|
||||
action: 'UPDATE_RUNTIME_SETTINGS',
|
||||
durationMinutes: null,
|
||||
detail: '턴 20분 · 장수 생성 불가 · 유저 자동턴 적용',
|
||||
}),
|
||||
]
|
||||
: [
|
||||
runtimeAction('APPLIED', {
|
||||
detail: '15분 가속 · 장수 2명 · 경매 1건',
|
||||
}),
|
||||
]))
|
||||
: (options.initialActions ?? []),
|
||||
},
|
||||
]);
|
||||
@@ -203,8 +228,8 @@ const installFixture = async (
|
||||
ok: true,
|
||||
action: {
|
||||
id: '68f1f0e4-3b95-4aeb-9925-c7e93caf1ba7',
|
||||
action: 'ACCELERATE',
|
||||
durationMinutes: 15,
|
||||
action: requestedRuntimeSettings ? 'UPDATE_RUNTIME_SETTINGS' : 'ACCELERATE',
|
||||
durationMinutes: requestedRuntimeSettings ? null : 15,
|
||||
status: 'REQUESTED',
|
||||
detail: null,
|
||||
handler: null,
|
||||
@@ -284,8 +309,69 @@ test('reports clock-shift acceptance separately from actual application', async
|
||||
expect(fixture.requestBodies).toHaveLength(1);
|
||||
expect(JSON.stringify(fixture.requestBodies[0])).toContain('"ACCELERATE"');
|
||||
expect(JSON.stringify(fixture.requestBodies[0])).toContain('"durationMinutes":15');
|
||||
await expect(page.getByRole('button', { name: '설문 오픈 (게임 내 관리)' })).toBeDisabled();
|
||||
await expect(page.getByText('설문 생성은 해당 게임의 설문 관리 화면에서 진행해 주세요.')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '설문 오픈 (게임 내 관리)' })).toHaveCount(0);
|
||||
await expect(page.getByText('설문 생성은 해당 게임의 설문 관리 화면에서 진행해 주세요.')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('updates live game options from the authoritative database snapshot', async ({ page }, testInfo) => {
|
||||
const fixture = await installFixture(page);
|
||||
await page.goto('/gateway/admin/servers/hwe%3Adefault');
|
||||
|
||||
const settings = page.getByTestId('runtime-settings');
|
||||
await expect(settings).toBeVisible();
|
||||
await expect(page.getByTestId('runtime-turn-term')).toHaveValue('10');
|
||||
await expect(page.getByTestId('runtime-block-general-create')).toHaveValue('2');
|
||||
await expect(page.getByTestId('runtime-autorun-enabled')).not.toBeChecked();
|
||||
await expect(page.getByRole('button', { name: '설문 오픈 (게임 내 관리)' })).toHaveCount(0);
|
||||
|
||||
await page.getByTestId('runtime-turn-term').selectOption('20');
|
||||
await page.getByTestId('runtime-block-general-create').selectOption('1');
|
||||
await page.getByTestId('runtime-autorun-enabled').check();
|
||||
await page.getByTestId('runtime-autorun-minutes').fill('720');
|
||||
for (const label of ['이동', '징병', '훈련', '전투']) {
|
||||
await settings.getByLabel(label, { exact: true }).uncheck();
|
||||
}
|
||||
await page.getByPlaceholder('사유 / 메모').fill('운영 중 설정 변경');
|
||||
|
||||
const submit = page.getByTestId('runtime-settings-submit');
|
||||
await submit.hover();
|
||||
const hoverBackground = await submit.evaluate((element) => getComputedStyle(element).backgroundColor);
|
||||
await submit.focus();
|
||||
await expect(submit).toBeFocused();
|
||||
const click = submit.click();
|
||||
await expect.poll(() => fixture.requestBodies.length).toBe(1);
|
||||
await click;
|
||||
|
||||
const requestJson = JSON.stringify(fixture.requestBodies[0]);
|
||||
expect(requestJson).toContain('UPDATE_RUNTIME_SETTINGS');
|
||||
expect(requestJson).toContain('"turnTermMinutes":20');
|
||||
expect(requestJson).toContain('"blockGeneralCreate":1');
|
||||
expect(requestJson).toContain('"limitMinutes":720');
|
||||
expect(requestJson).toContain('"recruit_high"');
|
||||
expect(requestJson).toContain('"chief"');
|
||||
expect(hoverBackground).not.toBe('rgba(0, 0, 0, 0)');
|
||||
await expect(page.getByText('APPLIED · UPDATE_RUNTIME_SETTINGS')).toBeVisible();
|
||||
await expect(page.getByTestId('runtime-turn-term')).toHaveValue('20');
|
||||
await expect(page.getByTestId('runtime-block-general-create')).toHaveValue('1');
|
||||
await expect(page.getByTestId('runtime-autorun-enabled')).toBeChecked();
|
||||
|
||||
const geometry = await settings.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return { left: rect.left, right: rect.right, width: rect.width, viewport: window.innerWidth };
|
||||
});
|
||||
expect(geometry.left).toBeGreaterThanOrEqual(0);
|
||||
expect(geometry.right).toBeLessThanOrEqual(geometry.viewport);
|
||||
expect(geometry.width).toBeGreaterThan(250);
|
||||
await page.screenshot({ path: testInfo.outputPath('runtime-settings-desktop.png'), fullPage: true });
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
const mobileGeometry = await settings.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return { left: rect.left, right: rect.right, viewport: window.innerWidth };
|
||||
});
|
||||
expect(mobileGeometry.left).toBeGreaterThanOrEqual(0);
|
||||
expect(mobileGeometry.right).toBeLessThanOrEqual(mobileGeometry.viewport);
|
||||
await page.screenshot({ path: testInfo.outputPath('runtime-settings-mobile.png'), fullPage: true });
|
||||
});
|
||||
|
||||
test('distinguishes a turn pause from an inaccessible stopped server in operator controls', async ({ page }) => {
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
export const RESET_AUTORUN_OPTIONS = ['develop', 'warp', 'recruit', 'train', 'battle'] as const;
|
||||
export const PROFILE_TURN_TERM_MINUTES = [1, 2, 5, 10, 20, 30, 60, 120] as const;
|
||||
export const RESET_AUTORUN_OPTIONS = [
|
||||
'develop',
|
||||
'warp',
|
||||
'recruit',
|
||||
'recruit_high',
|
||||
'train',
|
||||
'battle',
|
||||
'chief',
|
||||
] as const;
|
||||
|
||||
export type ResetAutorunOption = (typeof RESET_AUTORUN_OPTIONS)[number];
|
||||
|
||||
@@ -48,7 +57,7 @@ export const normalizeProfileResetDefaults = (value: unknown): ProfileResetDefau
|
||||
const autorunLimit = rawAutorun?.limitMinutes;
|
||||
|
||||
return {
|
||||
turnTermMinutes: enumNumber(raw.turnTermMinutes, [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 24, 30, 40, 60, 120], 60),
|
||||
turnTermMinutes: enumNumber(raw.turnTermMinutes, PROFILE_TURN_TERM_MINUTES, 60),
|
||||
sync: typeof raw.sync === 'boolean' ? raw.sync : SYSTEM_PROFILE_RESET_DEFAULTS.sync,
|
||||
fiction: enumNumber(raw.fiction, [0, 1], SYSTEM_PROFILE_RESET_DEFAULTS.fiction),
|
||||
extend: typeof raw.extend === 'boolean' ? raw.extend : SYSTEM_PROFILE_RESET_DEFAULTS.extend,
|
||||
|
||||
@@ -12,6 +12,7 @@ import AdminConsoleLayout from '../layouts/AdminConsoleLayout.vue';
|
||||
import { useToast } from '../composables/useToast';
|
||||
import {
|
||||
normalizeProfileResetDefaults,
|
||||
PROFILE_TURN_TERM_MINUTES,
|
||||
type ProfileResetDefaults,
|
||||
type ResetAutorunOption,
|
||||
} from '../utils/resetDefaults';
|
||||
@@ -201,6 +202,7 @@ type AdminProfile = {
|
||||
status: 'QUEUED' | 'RUNNING';
|
||||
} | null;
|
||||
meta: Record<string, unknown>;
|
||||
runtimeSettings: ProfileRuntimeSettings | null;
|
||||
runtimeActions: Array<{
|
||||
id: string;
|
||||
action: string;
|
||||
@@ -210,11 +212,22 @@ type AdminProfile = {
|
||||
handler: string | null;
|
||||
handledAt: string | null;
|
||||
createdAt: string;
|
||||
payload?: Record<string, unknown>;
|
||||
}>;
|
||||
};
|
||||
|
||||
type ProfileRuntimeSettings = Pick<ProfileResetDefaults, 'turnTermMinutes' | 'blockGeneralCreate' | 'autorunUser'>;
|
||||
|
||||
type AdminAction =
|
||||
'RESUME' | 'PAUSE' | 'STOP' | 'ACCELERATE' | 'DELAY' | 'RESET_NOW' | 'RESET_SCHEDULED' | 'OPEN_SURVEY' | 'SHUTDOWN';
|
||||
| 'RESUME'
|
||||
| 'PAUSE'
|
||||
| 'STOP'
|
||||
| 'ACCELERATE'
|
||||
| 'DELAY'
|
||||
| 'UPDATE_RUNTIME_SETTINGS'
|
||||
| 'RESET_NOW'
|
||||
| 'RESET_SCHEDULED'
|
||||
| 'SHUTDOWN';
|
||||
|
||||
type AdminClient = {
|
||||
capabilities: {
|
||||
@@ -351,6 +364,7 @@ type AdminClient = {
|
||||
profileName: string;
|
||||
action: AdminAction;
|
||||
durationMinutes?: number;
|
||||
runtimeSettings?: ProfileRuntimeSettings;
|
||||
scheduledAt?: string;
|
||||
reason?: string;
|
||||
}) => Promise<{ ok: boolean; action?: AdminProfile['runtimeActions'][number] }>;
|
||||
@@ -409,6 +423,11 @@ const profileActions = ref<
|
||||
durationMinutes: string;
|
||||
scheduledAt: string;
|
||||
reason: string;
|
||||
turnTermMinutes: number;
|
||||
blockGeneralCreate: 0 | 1 | 2;
|
||||
autorunEnabled: boolean;
|
||||
autorunUserMinutes: string;
|
||||
autorunOptions: ResetAutorunOption[];
|
||||
}
|
||||
>
|
||||
>({});
|
||||
@@ -416,8 +435,10 @@ 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>>({});
|
||||
@@ -450,6 +471,16 @@ const validDuration = (profileName: string): boolean => {
|
||||
return Number.isInteger(value) && value >= 1 && value <= 1440;
|
||||
};
|
||||
|
||||
const validRuntimeSettings = (profileName: string): boolean => {
|
||||
const value = profileActions.value[profileName];
|
||||
if (!value || !PROFILE_TURN_TERM_MINUTES.some((minutes) => minutes === value.turnTermMinutes)) return false;
|
||||
if (!value.autorunEnabled) return true;
|
||||
const limitMinutes = Number(value.autorunUserMinutes);
|
||||
return (
|
||||
Number.isInteger(limitMinutes) && limitMinutes >= 1 && limitMinutes <= 43200 && value.autorunOptions.length > 0
|
||||
);
|
||||
};
|
||||
|
||||
const runtimeActionStatusClass = (status: AdminProfile['runtimeActions'][number]['status']): string => {
|
||||
if (status === 'APPLIED') return 'text-emerald-400';
|
||||
if (status === 'FAILED') return 'text-red-400';
|
||||
@@ -647,14 +678,31 @@ const ensureProfileBuffers = (profile: AdminProfile) => {
|
||||
};
|
||||
}
|
||||
if (!profileActions.value[profile.profileName]) {
|
||||
const settings = profile.runtimeSettings ?? normalizeProfileResetDefaults(profile.meta?.resetDefaults);
|
||||
profileActions.value[profile.profileName] = {
|
||||
durationMinutes: '',
|
||||
scheduledAt: '',
|
||||
reason: '',
|
||||
turnTermMinutes: settings.turnTermMinutes,
|
||||
blockGeneralCreate: settings.blockGeneralCreate,
|
||||
autorunEnabled: settings.autorunUser !== null,
|
||||
autorunUserMinutes: String(settings.autorunUser?.limitMinutes ?? 1440),
|
||||
autorunOptions: settings.autorunUser?.options.slice() ?? resetAutorunLabels.map(({ value }) => value),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const syncRuntimeSettingsBuffer = (profile: AdminProfile): void => {
|
||||
const settings = profile.runtimeSettings;
|
||||
const buffer = profileActions.value[profile.profileName];
|
||||
if (!settings || !buffer) return;
|
||||
buffer.turnTermMinutes = settings.turnTermMinutes;
|
||||
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);
|
||||
};
|
||||
|
||||
const toLocalInputValue = (value: string): string => toServerDateTimeInputValue(value);
|
||||
|
||||
const loadProfiles = async () => {
|
||||
@@ -688,6 +736,8 @@ const refreshRuntimeActionUntilTerminal = async (profileName: string, actionId:
|
||||
.find((profile) => profile.profileName === profileName)
|
||||
?.runtimeActions.find((action) => action.id === actionId);
|
||||
if (current && isRuntimeActionTerminal(current.status)) {
|
||||
const profile = profiles.value.find((item) => item.profileName === profileName);
|
||||
if (profile && current.status === 'APPLIED') syncRuntimeSettingsBuffer(profile);
|
||||
profileActionStatus.value = {
|
||||
...profileActionStatus.value,
|
||||
[profileName]: '',
|
||||
@@ -701,6 +751,13 @@ const refreshRuntimeActionUntilTerminal = async (profileName: string, actionId:
|
||||
ensureProfileBuffers(profile);
|
||||
});
|
||||
profiles.value = result;
|
||||
const refreshedAction = result
|
||||
.find((profile) => profile.profileName === profileName)
|
||||
?.runtimeActions.find((action) => action.id === actionId);
|
||||
if (refreshedAction?.status === 'APPLIED') {
|
||||
const profile = result.find((item) => item.profileName === profileName);
|
||||
if (profile) syncRuntimeSettingsBuffer(profile);
|
||||
}
|
||||
} catch {
|
||||
// 일시적인 조회 실패는 다음 bounded poll에서 다시 확인합니다.
|
||||
}
|
||||
@@ -816,12 +873,37 @@ const requestProfileAction = async (profileName: string, action: AdminAction) =>
|
||||
? serverDateTimeInputToIso(actionState.scheduledAt)
|
||||
: undefined;
|
||||
const reason = actionState?.reason.trim() || undefined;
|
||||
const runtimeSettings: ProfileRuntimeSettings | undefined =
|
||||
action === 'UPDATE_RUNTIME_SETTINGS' && actionState
|
||||
? {
|
||||
turnTermMinutes: actionState.turnTermMinutes,
|
||||
blockGeneralCreate: actionState.blockGeneralCreate,
|
||||
autorunUser: actionState.autorunEnabled
|
||||
? {
|
||||
limitMinutes: Number(actionState.autorunUserMinutes),
|
||||
options: actionState.autorunOptions,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
: undefined;
|
||||
if (action === 'UPDATE_RUNTIME_SETTINGS' && (!validRuntimeSettings(profileName) || !reason || reason.length < 3)) {
|
||||
profileActionStatus.value = {
|
||||
...profileActionStatus.value,
|
||||
[profileName]:
|
||||
!reason || reason.length < 3
|
||||
? '현재 기수 설정 변경 사유를 입력하세요.'
|
||||
: '턴 간격과 유저 자동턴 값을 확인하세요.',
|
||||
};
|
||||
profileActionSubmitting.value = { ...profileActionSubmitting.value, [profileName]: false };
|
||||
return;
|
||||
}
|
||||
let runtimeActionId: string | undefined;
|
||||
try {
|
||||
const result = await adminClient.profiles.requestAction.mutate({
|
||||
profileName,
|
||||
action,
|
||||
durationMinutes: durationValue,
|
||||
runtimeSettings,
|
||||
scheduledAt,
|
||||
reason,
|
||||
});
|
||||
@@ -830,7 +912,9 @@ const requestProfileAction = async (profileName: string, action: AdminAction) =>
|
||||
[profileName]:
|
||||
result.action && (action === 'ACCELERATE' || action === 'DELAY')
|
||||
? `접수됨 · ${result.action.status} · ${action} ${result.action.durationMinutes ?? ''}분`
|
||||
: `요청 접수: ${action}`,
|
||||
: result.action && action === 'UPDATE_RUNTIME_SETTINGS'
|
||||
? `접수됨 · ${result.action.status} · 현재 기수 설정 변경`
|
||||
: `요청 접수: ${action}`,
|
||||
};
|
||||
if (result.action) {
|
||||
runtimeActionId = result.action.id;
|
||||
@@ -2166,9 +2250,7 @@ onMounted(() => {
|
||||
data-testid="meta-reset-turn-term"
|
||||
>
|
||||
<option
|
||||
v-for="minutes in [
|
||||
1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 24, 30, 40, 60, 120,
|
||||
]"
|
||||
v-for="minutes in PROFILE_TURN_TERM_MINUTES"
|
||||
:key="minutes"
|
||||
:value="minutes"
|
||||
>
|
||||
@@ -2207,9 +2289,9 @@ onMounted(() => {
|
||||
"
|
||||
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2"
|
||||
>
|
||||
<option :value="0">없음</option>
|
||||
<option :value="1">제한</option>
|
||||
<option :value="2">차단</option>
|
||||
<option :value="0">가능</option>
|
||||
<option :value="2">장수명 무작위</option>
|
||||
<option :value="1">불가</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
@@ -2351,6 +2433,102 @@ onMounted(() => {
|
||||
v-if="hasCapability('admin.profiles.runtime', profile.profileName)"
|
||||
class="space-y-2"
|
||||
>
|
||||
<fieldset
|
||||
class="space-y-3 rounded border border-zinc-700 bg-zinc-950/60 p-3"
|
||||
data-testid="runtime-settings"
|
||||
>
|
||||
<legend class="px-1 text-sm font-semibold text-zinc-200">
|
||||
실행 중 게임 옵션
|
||||
</legend>
|
||||
<p class="text-xs text-zinc-500">
|
||||
현재 기수에 즉시 적용합니다. 턴 간격 변경 시 tick 기준 게임 시각은 같은 게임
|
||||
시각을 유지하도록 재계산하고, 기존 로그 시각은 유지합니다.
|
||||
</p>
|
||||
<p v-if="!profile.runtimeSettings" class="text-xs text-amber-400" role="alert">
|
||||
게임 DB에서 현재 설정을 읽지 못해 변경할 수 없습니다. 실제 처리 상태를
|
||||
새로고침해 주세요.
|
||||
</p>
|
||||
<label class="block text-xs text-zinc-400">
|
||||
턴 간격
|
||||
<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"
|
||||
data-testid="runtime-turn-term"
|
||||
>
|
||||
<option
|
||||
v-for="minutes in PROFILE_TURN_TERM_MINUTES"
|
||||
:key="minutes"
|
||||
:value="minutes"
|
||||
>
|
||||
{{ minutes }}분
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="block text-xs text-zinc-400">
|
||||
장수 생성 제한
|
||||
<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"
|
||||
data-testid="runtime-block-general-create"
|
||||
>
|
||||
<option :value="0">가능</option>
|
||||
<option :value="2">장수명 무작위</option>
|
||||
<option :value="1">불가</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-xs text-zinc-300">
|
||||
<input
|
||||
v-model="profileActions[profile.profileName].autorunEnabled"
|
||||
type="checkbox"
|
||||
data-testid="runtime-autorun-enabled"
|
||||
/>
|
||||
유저 자동턴 사용
|
||||
</label>
|
||||
<template v-if="profileActions[profile.profileName].autorunEnabled">
|
||||
<label class="block text-xs text-zinc-400">
|
||||
자동턴 제한 분
|
||||
<input
|
||||
v-model="profileActions[profile.profileName].autorunUserMinutes"
|
||||
type="number"
|
||||
min="1"
|
||||
max="43200"
|
||||
step="1"
|
||||
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2 text-sm text-white"
|
||||
data-testid="runtime-autorun-minutes"
|
||||
/>
|
||||
</label>
|
||||
<div class="flex flex-wrap items-center gap-3 text-xs text-zinc-300">
|
||||
<label
|
||||
v-for="option in resetAutorunLabels"
|
||||
:key="`runtime-${option.value}`"
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<input
|
||||
v-model="profileActions[profile.profileName].autorunOptions"
|
||||
type="checkbox"
|
||||
:value="option.value"
|
||||
/>
|
||||
{{ option.label }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<button
|
||||
class="w-full rounded bg-cyan-600 px-4 py-2 font-semibold text-black hover:bg-cyan-500 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
data-testid="runtime-settings-submit"
|
||||
:disabled="
|
||||
profileActionSubmitting[profile.profileName] ||
|
||||
!profile.runtimeSettings ||
|
||||
!gatewayProfileCapabilities(profile.status).runtimeExpected ||
|
||||
!validRuntimeSettings(profile.profileName) ||
|
||||
runtimeActionPending(profile)
|
||||
"
|
||||
@click="
|
||||
requestProfileAction(profile.profileName, 'UPDATE_RUNTIME_SETTINGS')
|
||||
"
|
||||
>
|
||||
현재 기수에 적용
|
||||
</button>
|
||||
</fieldset>
|
||||
<label class="text-xs text-zinc-400">특수 동작 메모</label>
|
||||
<input
|
||||
v-model="profileActions[profile.profileName].reason"
|
||||
@@ -2438,15 +2616,7 @@ onMounted(() => {
|
||||
연기
|
||||
</button>
|
||||
<button
|
||||
class="bg-zinc-800 text-zinc-500 font-semibold px-3 py-2 rounded cursor-not-allowed"
|
||||
disabled
|
||||
title="게임 내 설문 관리 화면에서 생성해 주세요."
|
||||
:aria-describedby="`survey-action-help-${profile.profileName}`"
|
||||
>
|
||||
설문 오픈 (게임 내 관리)
|
||||
</button>
|
||||
<button
|
||||
class="bg-black hover:bg-zinc-800 text-white font-semibold px-3 py-2 rounded col-span-2"
|
||||
class="bg-black hover:bg-zinc-800 text-white font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'SHUTDOWN')"
|
||||
>
|
||||
서버 폐쇄
|
||||
@@ -2487,12 +2657,6 @@ onMounted(() => {
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:id="`survey-action-help-${profile.profileName}`"
|
||||
class="text-xs text-zinc-500"
|
||||
>
|
||||
설문 생성은 해당 게임의 설문 관리 화면에서 진행해 주세요.
|
||||
</div>
|
||||
<button type="button" class="text-xs text-zinc-400 underline" @click="loadProfiles">
|
||||
실제 처리 상태 새로고침
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user