feat: 예약 오픈 일정을 빌드 전에 공개
초기화 예약의 공개 선택을 operation payload에 저장하고 로비에서 준비 단계별로 표시합니다. RESERVED 인계와 STOPPED 무요청 경계를 테스트합니다.
This commit is contained in:
@@ -63,8 +63,25 @@ type LobbyFixtureOptions = {
|
||||
limitMinutes: number;
|
||||
options: string[];
|
||||
} | null;
|
||||
upcomingReset?: {
|
||||
phase: 'SCHEDULED' | 'PREPARING' | 'READY' | 'DELAYED';
|
||||
scheduledAt: string;
|
||||
preopenAt: string;
|
||||
openAt: string;
|
||||
scenarioId: number;
|
||||
scenarioTitle: string;
|
||||
turnTermMinutes: number;
|
||||
fictionMode: string;
|
||||
npcMode: number;
|
||||
defaultStatTotal: number;
|
||||
otherTextInfo: string;
|
||||
autorunUser: {
|
||||
limitMinutes: number;
|
||||
options: string[];
|
||||
} | null;
|
||||
} | null;
|
||||
lobbyBundleFailures?: number;
|
||||
profileStatus?: 'RUNNING' | 'PREOPEN' | 'PAUSED' | 'COMPLETED' | 'STOPPED';
|
||||
profileStatus?: 'RUNNING' | 'PREOPEN' | 'PAUSED' | 'COMPLETED' | 'STOPPED' | 'RESERVED';
|
||||
includeStoppedProfile?: boolean;
|
||||
};
|
||||
|
||||
@@ -98,10 +115,12 @@ const installFixture = async (page: Page, options: LobbyFixtureOptions = {}) =>
|
||||
korName = 'hwe',
|
||||
otherTextInfo = '',
|
||||
autorunUser = null,
|
||||
upcomingReset = null,
|
||||
lobbyBundleFailures = 0,
|
||||
profileStatus = 'RUNNING',
|
||||
includeStoppedProfile = false,
|
||||
} = options;
|
||||
const runtimeAvailable = ['RUNNING', 'PREOPEN', 'PAUSED', 'COMPLETED'].includes(profileStatus);
|
||||
let remainingLobbyBundleFailures = lobbyBundleFailures;
|
||||
const gameOperations: Array<{ operation: string; authorization: string | undefined }> = [];
|
||||
if (authenticated) {
|
||||
@@ -139,22 +158,23 @@ const installFixture = async (page: Page, options: LobbyFixtureOptions = {}) =>
|
||||
scenario: '903',
|
||||
status: profileStatus,
|
||||
lifecycle: {
|
||||
runtimeExpected: profileStatus !== 'STOPPED',
|
||||
userAccessible: profileStatus !== 'STOPPED',
|
||||
runtimeExpected: runtimeAvailable,
|
||||
userAccessible: runtimeAvailable,
|
||||
turnsRunning: profileStatus === 'RUNNING',
|
||||
operatorResumable: profileStatus === 'PAUSED' || profileStatus === 'STOPPED',
|
||||
dataInitialized: true,
|
||||
},
|
||||
apiPort: 15015,
|
||||
runtime: {
|
||||
apiRunning: true,
|
||||
daemonRunning: true,
|
||||
auctionRunning: true,
|
||||
battleSimRunning: true,
|
||||
tournamentRunning: true,
|
||||
apiRunning: runtimeAvailable,
|
||||
daemonRunning: runtimeAvailable,
|
||||
auctionRunning: runtimeAvailable,
|
||||
battleSimRunning: runtimeAvailable,
|
||||
tournamentRunning: runtimeAvailable,
|
||||
},
|
||||
korName,
|
||||
color: '#ffffff',
|
||||
upcomingReset,
|
||||
localAccountPolicy: {
|
||||
accessAllowed: true,
|
||||
canCreateGeneral,
|
||||
@@ -482,6 +502,114 @@ test('does not contact a STOPPED game runtime and labels it inaccessible', async
|
||||
await page.screenshot({ path: testInfo.outputPath('gateway-stopped-profile-lobby.png'), fullPage: true });
|
||||
});
|
||||
|
||||
test('shows a complete upcoming reset announcement without contacting the stopped runtime', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const gameOperations = await installFixture(page, {
|
||||
profileStatus: 'STOPPED',
|
||||
korName: '체',
|
||||
upcomingReset: {
|
||||
phase: 'SCHEDULED',
|
||||
scheduledAt: '2026-08-27T05:00:00.000Z',
|
||||
preopenAt: '2026-08-27T05:30:00.000Z',
|
||||
openAt: '2026-08-27T11:00:00.000Z',
|
||||
scenarioId: 1010,
|
||||
scenarioTitle: '【가상】황건적의 난',
|
||||
turnTermMinutes: 60,
|
||||
fictionMode: '가상',
|
||||
npcMode: 1,
|
||||
defaultStatTotal: 70,
|
||||
otherTextInfo: '랜덤 임관',
|
||||
autorunUser: {
|
||||
limitMinutes: 1440,
|
||||
options: ['develop', 'battle'],
|
||||
},
|
||||
},
|
||||
});
|
||||
await page.setViewportSize({ width: 1200, height: 900 });
|
||||
|
||||
await page.goto('lobby');
|
||||
const row = page.locator('tbody tr').filter({ hasText: '체섭' });
|
||||
const announcement = row.getByTestId('upcoming-reset-announcement');
|
||||
const autorun = announcement.locator('.copyable-autorun');
|
||||
const tooltip = announcement.locator('.copyable-autorun-detail');
|
||||
await expect(row.getByTestId('upcoming-reset-phase')).toHaveText('오픈 예정 · 빌드 대기');
|
||||
await expect(row.getByTestId('upcoming-reset-scheduled-at')).toHaveText('- 초기화 시작 : 2026-08-27 14:00:00 -');
|
||||
await expect(row.getByTestId('upcoming-reset-preopen-at')).toHaveText('- 가오픈 일시 : 2026-08-27 14:30:00 -');
|
||||
await expect(row.getByTestId('upcoming-reset-open-at')).toHaveText('- 오픈 일시 : 2026-08-27 20:00:00 -');
|
||||
await expect(row.getByTestId('upcoming-reset-scenario-announcement')).toHaveText(
|
||||
'【가상】황건적의 난 60분 턴 서버'
|
||||
);
|
||||
expect((await announcement.textContent())?.replace(/\s+/g, ' ')).toContain(
|
||||
'(상성 설정:가상), (빙의 여부:가능), (최대 스탯:70), (기타 설정:랜덤 임관, 자율행동[내정, 출병, 24시간 유효])'
|
||||
);
|
||||
await expect(row).not.toContainText('서버 중지 · 접근 불가');
|
||||
await expect(row.getByRole('button', { name: '입장' })).toHaveCount(0);
|
||||
await expect(page.getByRole('tab', { name: 'hwe섭' })).toHaveCount(0);
|
||||
expect(gameOperations).toEqual([]);
|
||||
|
||||
const desktopGeometry = await announcement.evaluate((element) => ({
|
||||
announcement: element.getBoundingClientRect().toJSON(),
|
||||
cell: element.parentElement?.getBoundingClientRect().toJSON(),
|
||||
documentWidth: document.documentElement.scrollWidth,
|
||||
viewportWidth: window.innerWidth,
|
||||
}));
|
||||
expect(desktopGeometry.announcement.left).toBeGreaterThanOrEqual(desktopGeometry.cell?.left ?? 0);
|
||||
expect(desktopGeometry.announcement.right).toBeLessThanOrEqual(desktopGeometry.cell?.right ?? 0);
|
||||
await autorun.hover();
|
||||
await expect(tooltip).toBeVisible();
|
||||
await autorun.focus();
|
||||
await expect(autorun).toBeFocused();
|
||||
await expect(autorun).toHaveCSS('outline-width', '2px');
|
||||
await page.screenshot({ path: testInfo.outputPath('gateway-upcoming-reset-desktop.png'), fullPage: true });
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await announcement.scrollIntoViewIfNeeded();
|
||||
const mobileGeometry = await announcement.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
documentWidth: document.documentElement.scrollWidth,
|
||||
viewportWidth: window.innerWidth,
|
||||
};
|
||||
});
|
||||
expect(mobileGeometry.left).toBeGreaterThanOrEqual(0);
|
||||
expect(mobileGeometry.right).toBeLessThanOrEqual(mobileGeometry.viewportWidth);
|
||||
expect(mobileGeometry.documentWidth).toBe(mobileGeometry.viewportWidth);
|
||||
await autorun.hover();
|
||||
await expect(tooltip).toBeVisible();
|
||||
await page.screenshot({ path: testInfo.outputPath('gateway-upcoming-reset-mobile.png'), fullPage: true });
|
||||
});
|
||||
|
||||
test('keeps the announcement through the RESERVED handoff after the build completes', async ({ page }) => {
|
||||
const gameOperations = await installFixture(page, {
|
||||
profileStatus: 'RESERVED',
|
||||
upcomingReset: {
|
||||
phase: 'READY',
|
||||
scheduledAt: '2026-08-27T05:00:00.000Z',
|
||||
preopenAt: '2026-08-27T05:30:00.000Z',
|
||||
openAt: '2026-08-27T11:00:00.000Z',
|
||||
scenarioId: 1010,
|
||||
scenarioTitle: '【가상】황건적의 난',
|
||||
turnTermMinutes: 60,
|
||||
fictionMode: '가상',
|
||||
npcMode: 1,
|
||||
defaultStatTotal: 70,
|
||||
otherTextInfo: '',
|
||||
autorunUser: null,
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto('lobby');
|
||||
const row = page.locator('tbody tr').filter({ hasText: 'hwe섭' });
|
||||
await expect(row.getByTestId('upcoming-reset-phase')).toHaveText('오픈 준비 완료 · 가오픈 대기');
|
||||
await expect(row.getByTestId('upcoming-reset-scenario-title')).toHaveText('【가상】황건적의 난');
|
||||
await expect(row).not.toContainText('준 비 중 · 접근 불가');
|
||||
await expect(row.getByRole('button', { name: '입장' })).toHaveCount(0);
|
||||
expect(gameOperations).toEqual([]);
|
||||
});
|
||||
|
||||
test('automatically recovers profile details after a transient update outage', async ({ page }) => {
|
||||
const gameOperations = await installFixture(page, { lobbyBundleFailures: 1 });
|
||||
|
||||
|
||||
@@ -604,6 +604,19 @@ test('separates branch and commit semantics and submits a reset from the dedicat
|
||||
await page.getByTestId('reset-scheduled-at').fill('2030-08-13T09:30');
|
||||
await page.getByTestId('reset-preopen-at').fill('2030-08-13T10:00');
|
||||
await page.getByTestId('reset-open-at').fill('2030-08-13T11:00');
|
||||
const publishSchedule = page.getByTestId('reset-publish-schedule');
|
||||
await publishSchedule.check();
|
||||
await page.getByTestId('reset-open-at').focus();
|
||||
await page.keyboard.press('Tab');
|
||||
await publishSchedule.focus();
|
||||
await expect(publishSchedule).toBeFocused();
|
||||
const publishFocusStyle = await publishSchedule.evaluate((element) => {
|
||||
const style = getComputedStyle(element);
|
||||
return { style: style.outlineStyle, width: Number.parseFloat(style.outlineWidth) };
|
||||
});
|
||||
expect(publishFocusStyle.style).not.toBe('none');
|
||||
expect(publishFocusStyle.width).toBeGreaterThanOrEqual(2);
|
||||
await expect(page.getByText('빌드는 초기화 시작 시각까지 대기합니다.')).toBeVisible();
|
||||
const scheduledHelp = page.getByTestId('reset-help-scheduled-at');
|
||||
await scheduledHelp.hover();
|
||||
await expect(page.getByTestId('reset-help-scheduled-at-tooltip')).toContainText(
|
||||
@@ -664,6 +677,7 @@ test('separates branch and commit semantics and submits a reset from the dedicat
|
||||
expect(JSON.stringify(resetRequest?.body)).toContain('0123456789abcdef0123456789abcdef01234567');
|
||||
expect(JSON.stringify(resetRequest?.body)).toContain('"scenarioId":5');
|
||||
expect(JSON.stringify(resetRequest?.body)).toContain('"scheduledAt":"2030-08-13T00:30:00.000Z"');
|
||||
expect(JSON.stringify(resetRequest?.body)).toContain('"publishSchedule":true');
|
||||
expect(JSON.stringify(resetRequest?.body)).toContain('"preopenAt":"2030-08-13T01:00:00.000Z"');
|
||||
expect(JSON.stringify(resetRequest?.body)).toContain('"openAt":"2030-08-13T02:00:00.000Z"');
|
||||
await page.screenshot({ path: testInfo.outputPath('reset-operation-log-desktop.png'), fullPage: true });
|
||||
@@ -681,6 +695,20 @@ test('separates branch and commit semantics and submits a reset from the dedicat
|
||||
return children;
|
||||
});
|
||||
expect(mobileGeometry[0]!.width).toBeLessThanOrEqual(390);
|
||||
const mobilePublishGeometry = await publishSchedule.evaluate((element) => {
|
||||
const label = element.closest('label');
|
||||
if (!label) throw new Error('expected publish schedule label');
|
||||
const rect = label.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
documentWidth: document.documentElement.scrollWidth,
|
||||
viewportWidth: window.innerWidth,
|
||||
};
|
||||
});
|
||||
expect(mobilePublishGeometry.left).toBeGreaterThanOrEqual(0);
|
||||
expect(mobilePublishGeometry.right).toBeLessThanOrEqual(mobilePublishGeometry.viewportWidth);
|
||||
expect(mobilePublishGeometry.documentWidth).toBe(mobilePublishGeometry.viewportWidth);
|
||||
const mobileTabs = await page
|
||||
.getByTestId('server-profile-tabs')
|
||||
.locator('a')
|
||||
|
||||
@@ -111,8 +111,7 @@ const formatAnnouncementDate = (value: string | null | undefined): string =>
|
||||
const profileScenarioTitle = (profileName: string): string =>
|
||||
profileDetails.value[profileName]?.scenarioTitle.trim() || '-';
|
||||
const npcModeText = (mode: number): string => ['불가', '가능', '선택 생성'][mode] ?? '불가';
|
||||
const autorunDetailText = (info: LobbyInfo): string => {
|
||||
const autorun = info.autorunUser;
|
||||
const autorunDetailText = (autorun: LobbyInfo['autorunUser']): string => {
|
||||
if (!autorun) return '';
|
||||
|
||||
const enabled = new Set(autorun.options);
|
||||
@@ -134,8 +133,14 @@ const autorunDetailText = (info: LobbyInfo): string => {
|
||||
labels.push(limit);
|
||||
return labels.join(', ');
|
||||
};
|
||||
const autorunTooltipId = (profileName: string): string =>
|
||||
`profile-autorun-${profileName.replaceAll(/[^a-zA-Z0-9_-]/g, '-')}`;
|
||||
const autorunTooltipId = (profileName: string, scope = 'current'): string =>
|
||||
'profile-autorun-' + scope + '-' + profileName.replaceAll(/[^a-zA-Z0-9_-]/g, '-');
|
||||
const upcomingResetPhaseText = (profile: LobbyProfile): string => {
|
||||
if (profile.upcomingReset?.phase === 'DELAYED') return '준비 지연 · 일정 확인 중';
|
||||
if (profile.upcomingReset?.phase === 'READY') return '오픈 준비 완료 · 가오픈 대기';
|
||||
if (profile.upcomingReset?.phase === 'PREPARING') return '오픈 준비 중';
|
||||
return '오픈 예정 · 빌드 대기';
|
||||
};
|
||||
const isProfileRuntimeAvailable = (profile: LobbyProfile): boolean => profile.lifecycle.userAccessible;
|
||||
const unavailableProfileText = (profile: LobbyProfile): string => {
|
||||
if (!profile.lifecycle.dataInitialized) return '- DB 초기화 전 · 접근 불가 -';
|
||||
@@ -503,8 +508,75 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
|
||||
|
||||
<!-- Server Info -->
|
||||
<td class="profile-info-cell px-4 py-4 border-r border-zinc-800">
|
||||
<div
|
||||
v-if="profile.upcomingReset"
|
||||
class="upcoming-reset-announcement"
|
||||
data-testid="upcoming-reset-announcement"
|
||||
>
|
||||
<div
|
||||
class="upcoming-reset-phase"
|
||||
:class="{ 'is-delayed': profile.upcomingReset.phase === 'DELAYED' }"
|
||||
data-testid="upcoming-reset-phase"
|
||||
>
|
||||
{{ upcomingResetPhaseText(profile) }}
|
||||
</div>
|
||||
<div data-testid="upcoming-reset-scheduled-at">
|
||||
- 초기화 시작 :
|
||||
{{ formatAnnouncementDate(profile.upcomingReset.scheduledAt) }} -
|
||||
</div>
|
||||
<div data-testid="upcoming-reset-preopen-at">
|
||||
- 가오픈 일시 :
|
||||
{{ formatAnnouncementDate(profile.upcomingReset.preopenAt) }} -
|
||||
</div>
|
||||
<div data-testid="upcoming-reset-open-at">
|
||||
- 오픈 일시 : {{ formatAnnouncementDate(profile.upcomingReset.openAt) }} -
|
||||
</div>
|
||||
<div data-testid="upcoming-reset-scenario-announcement">
|
||||
<span class="text-orange-400" data-testid="upcoming-reset-scenario-title">
|
||||
{{ profile.upcomingReset.scenarioTitle }} </span
|
||||
>{{ ' ' }}
|
||||
<span class="text-green-400">
|
||||
{{ profile.upcomingReset.turnTermMinutes }}분 턴 서버
|
||||
</span>
|
||||
</div>
|
||||
<div class="profile-announcement-settings text-xs text-zinc-500">
|
||||
(상성 설정:{{ profile.upcomingReset.fictionMode }}), (빙의 여부:{{
|
||||
npcModeText(profile.upcomingReset.npcMode)
|
||||
}}), (최대 스탯:{{ profile.upcomingReset.defaultStatTotal }}), (기타
|
||||
설정:<template v-if="profile.upcomingReset.otherTextInfo"
|
||||
>{{ profile.upcomingReset.otherTextInfo
|
||||
}}<template v-if="profile.upcomingReset.autorunUser"
|
||||
>,
|
||||
</template></template
|
||||
><span
|
||||
v-if="profile.upcomingReset.autorunUser"
|
||||
class="copyable-autorun"
|
||||
tabindex="0"
|
||||
:aria-describedby="autorunTooltipId(profile.profileName, 'upcoming')"
|
||||
>자율행동<span
|
||||
:id="autorunTooltipId(profile.profileName, 'upcoming')"
|
||||
class="copyable-autorun-detail"
|
||||
role="tooltip"
|
||||
><span class="copyable-autorun-bracket">[</span
|
||||
><span>{{
|
||||
autorunDetailText(profile.upcomingReset.autorunUser)
|
||||
}}</span
|
||||
><span class="copyable-autorun-bracket">]</span></span
|
||||
></span
|
||||
><template
|
||||
v-if="
|
||||
!profile.upcomingReset.otherTextInfo &&
|
||||
!profile.upcomingReset.autorunUser
|
||||
"
|
||||
>없음</template
|
||||
>)
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="profileDetails[profile.profileName]">
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
class="space-y-1"
|
||||
:class="{ 'mt-3 border-t border-zinc-800 pt-3': profile.upcomingReset }"
|
||||
>
|
||||
<template v-if="profile.status === 'PREOPEN'">
|
||||
<div
|
||||
v-if="profileDetails[profile.profileName]?.preopenAt"
|
||||
@@ -586,7 +658,9 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
|
||||
role="tooltip"
|
||||
><span class="copyable-autorun-bracket">[</span
|
||||
><span>{{
|
||||
autorunDetailText(profileDetails[profile.profileName]!)
|
||||
autorunDetailText(
|
||||
profileDetails[profile.profileName]!.autorunUser
|
||||
)
|
||||
}}</span
|
||||
><span class="copyable-autorun-bracket">]</span></span
|
||||
></span
|
||||
@@ -594,7 +668,7 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="!isProfileRuntimeAvailable(profile)">
|
||||
<template v-else-if="!isProfileRuntimeAvailable(profile) && !profile.upcomingReset">
|
||||
<div class="text-center text-zinc-600 py-2">
|
||||
{{ unavailableProfileText(profile) }}
|
||||
</div>
|
||||
@@ -929,6 +1003,23 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.upcoming-reset-announcement {
|
||||
border-left: 2px solid #f59e0b;
|
||||
padding-left: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.upcoming-reset-phase {
|
||||
margin-bottom: 4px;
|
||||
color: #fbbf24;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.upcoming-reset-phase.is-delayed {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.copyable-autorun {
|
||||
position: relative;
|
||||
cursor: help;
|
||||
|
||||
@@ -152,6 +152,7 @@ const form = reactive({
|
||||
openAt: '',
|
||||
preopenAt: '',
|
||||
scheduledAt: '',
|
||||
publishSchedule: false,
|
||||
reason: '',
|
||||
});
|
||||
const RESET_AUTORUN_FORM_KEYS = {
|
||||
@@ -653,12 +654,16 @@ const requestReset = async () => {
|
||||
errorMessage.value = '초기화 소스와 시나리오를 먼저 선택해주세요.';
|
||||
return;
|
||||
}
|
||||
if (form.publishSchedule && (!form.scheduledAt || !form.preopenAt || !form.openAt)) {
|
||||
errorMessage.value = '로비 일정 공개에는 초기화 시작, 가오픈 시작과 정식 오픈을 모두 입력해주세요.';
|
||||
return;
|
||||
}
|
||||
const scenarioId = form.scenarioId;
|
||||
const sourceLabel =
|
||||
form.sourceMode === 'CURRENT' ? '서버 지정 버전' : form.sourceMode === 'BRANCH' ? '브랜치' : '커밋';
|
||||
if (
|
||||
!window.confirm(
|
||||
`${selectedProfileName.value}의 게임 DB를 초기화합니다.\n${sourceLabel}${form.sourceMode === 'CURRENT' ? '' : `: ${form.sourceRef}`}\n시나리오: ${scenarioId}`
|
||||
`${selectedProfileName.value}의 게임 DB를 초기화합니다.\n${sourceLabel}${form.sourceMode === 'CURRENT' ? '' : `: ${form.sourceRef}`}\n시나리오: ${scenarioId}${form.publishSchedule ? '\n예약 등록 즉시 로비에 오픈 일정을 공개합니다.' : ''}`
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@@ -670,6 +675,7 @@ const requestReset = async () => {
|
||||
sourceMode: form.sourceMode,
|
||||
sourceRef: form.sourceMode === 'CURRENT' ? undefined : form.sourceRef.trim(),
|
||||
scheduledAt: toIso(form.scheduledAt),
|
||||
publishSchedule: form.publishSchedule,
|
||||
reason: form.reason.trim() || undefined,
|
||||
install: {
|
||||
scenarioId,
|
||||
@@ -1384,6 +1390,23 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label
|
||||
class="flex cursor-pointer items-start gap-3 rounded border border-zinc-800 bg-zinc-950/50 px-3 py-2 text-sm text-zinc-200"
|
||||
>
|
||||
<input
|
||||
v-model="form.publishSchedule"
|
||||
type="checkbox"
|
||||
class="publish-schedule-checkbox mt-0.5 h-4 w-4 accent-amber-500"
|
||||
data-testid="reset-publish-schedule"
|
||||
/>
|
||||
<span>
|
||||
<span class="block font-medium">예약 등록 즉시 로비에 오픈 일정 공개</span>
|
||||
<span class="mt-0.5 block text-xs leading-5 text-zinc-500">
|
||||
빌드는 초기화 시작 시각까지 대기합니다. 공개하려면 위 세 시각을 모두 입력해야
|
||||
합니다.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input
|
||||
@@ -1859,3 +1882,10 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</AdminConsoleLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.publish-schedule-checkbox:focus-visible {
|
||||
outline: 2px solid #fcd34d;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user