diff --git a/app/gateway-api/src/adminRouter.ts b/app/gateway-api/src/adminRouter.ts index bb1848fe..21c34707 100644 --- a/app/gateway-api/src/adminRouter.ts +++ b/app/gateway-api/src/adminRouter.ts @@ -1406,13 +1406,13 @@ export const adminRouter = router({ install: input.install, requestedSource: input.sourceMode, releaseSource: { mode: sourceMode, ref: sourceRef }, - ...(input.publishSchedule && selectedScenario + ...(selectedScenario && input.install.preopenAt && input.install.openAt ? { publicAnnouncement: { - enabled: true, + enabled: input.publishSchedule, scenarioId: selectedScenario.id, scenarioTitle: selectedScenario.title, - scheduledAt: input.scheduledAt, + scheduledAt: input.scheduledAt ?? null, preopenAt: input.install.preopenAt, openAt: input.install.openAt, turnTermMinutes: input.install.turnTermMinutes, diff --git a/app/gateway-api/src/lobby/profileStatusService.ts b/app/gateway-api/src/lobby/profileStatusService.ts index be36bc77..7fdf2788 100644 --- a/app/gateway-api/src/lobby/profileStatusService.ts +++ b/app/gateway-api/src/lobby/profileStatusService.ts @@ -25,7 +25,7 @@ type PublicAutorunOption = (typeof PUBLIC_AUTORUN_OPTIONS)[number]; export type LobbyUpcomingReset = { phase: 'SCHEDULED' | 'PREPARING' | 'READY' | 'DELAYED'; - scheduledAt: string; + scheduledAt: string | null; preopenAt: string; openAt: string; scenarioId: number; @@ -215,7 +215,7 @@ export const resolveUpcomingResetAnnouncement = ( if (operation.type !== 'RESET' || !['QUEUED', 'RUNNING', 'SUCCEEDED'].includes(operation.status)) return null; const payload = asRecord(operation.payload); const announcement = asRecord(payload?.publicAnnouncement); - if (!announcement || announcement.enabled !== true) return null; + if (!announcement || (announcement.enabled !== true && operation.status !== 'SUCCEEDED')) return null; const scheduledAt = readDateTime(announcement.scheduledAt); const preopenAt = readDateTime(announcement.preopenAt); @@ -226,7 +226,7 @@ export const resolveUpcomingResetAnnouncement = ( const defaultStatTotal = readFiniteNumber(announcement.defaultStatTotal); const autorunUser = readAutorun(announcement.autorunUser); if ( - !scheduledAt || + (announcement.enabled === true && !scheduledAt) || !preopenAt || !openAt || !Number.isInteger(scenarioId) || @@ -248,7 +248,7 @@ export const resolveUpcomingResetAnnouncement = ( ? 'DELAYED' : operation.status === 'SUCCEEDED' ? 'READY' - : operation.status === 'RUNNING' || nowMs >= new Date(scheduledAt).getTime() + : operation.status === 'RUNNING' || (scheduledAt && nowMs >= new Date(scheduledAt).getTime()) ? 'PREPARING' : 'SCHEDULED'; return { diff --git a/app/gateway-api/test/adminOperations.test.ts b/app/gateway-api/test/adminOperations.test.ts index 4be8c797..f0972c3a 100644 --- a/app/gateway-api/test/adminOperations.test.ts +++ b/app/gateway-api/test/adminOperations.test.ts @@ -772,6 +772,35 @@ describe('admin operation API', () => { }, }); + await harness.caller.admin.operations.requestReset({ + profileName: 'che:2', + sourceMode: 'COMMIT', + sourceRef: 'HEAD', + install, + }); + + expect(harness.createdInputs[1]).toMatchObject({ + type: 'RESET', + scheduledAt: undefined, + payload: { + install, + publicAnnouncement: { + enabled: false, + scenarioId: 1010, + scenarioTitle: expect.any(String), + scheduledAt: null, + preopenAt: install.preopenAt, + openAt: install.openAt, + turnTermMinutes: 60, + fictionMode: '가상', + npcMode: 0, + defaultStatTotal: expect.any(Number), + otherTextInfo: expect.any(String), + autorunUser: null, + }, + }, + }); + await expect( harness.caller.admin.operations.requestReset({ profileName: 'che:2', diff --git a/app/gateway-api/test/profileStatusService.test.ts b/app/gateway-api/test/profileStatusService.test.ts index 996ff735..bcd41c8c 100644 --- a/app/gateway-api/test/profileStatusService.test.ts +++ b/app/gateway-api/test/profileStatusService.test.ts @@ -96,10 +96,21 @@ describe('resolveUpcomingResetAnnouncement', () => { } }); - it('fails closed for an unpublished or incomplete snapshot', () => { + it('keeps an unpublished snapshot hidden until a completed build is ready', () => { const unpublished = buildOperation(); - unpublished.payload = { publicAnnouncement: { enabled: false } }; + unpublished.payload = { + publicAnnouncement: { + ...(unpublished.payload as { publicAnnouncement: Record }).publicAnnouncement, + enabled: false, + scheduledAt: null, + }, + }; expect(resolveUpcomingResetAnnouncement(unpublished, new Date('2026-08-27T04:00:00.000Z'))).toBeNull(); + unpublished.status = 'SUCCEEDED'; + expect(resolveUpcomingResetAnnouncement(unpublished, new Date('2026-08-27T04:00:00.000Z'))).toMatchObject({ + phase: 'READY', + scheduledAt: null, + }); const incomplete = buildOperation(); incomplete.payload = { publicAnnouncement: { enabled: true, scenarioTitle: '황건적의 난' } }; diff --git a/app/gateway-frontend/e2e/lobby-game-auth.spec.ts b/app/gateway-frontend/e2e/lobby-game-auth.spec.ts index ecd34de9..80bd3833 100644 --- a/app/gateway-frontend/e2e/lobby-game-auth.spec.ts +++ b/app/gateway-frontend/e2e/lobby-game-auth.spec.ts @@ -65,7 +65,7 @@ type LobbyFixtureOptions = { } | null; upcomingReset?: { phase: 'SCHEDULED' | 'PREPARING' | 'READY' | 'DELAYED'; - scheduledAt: string; + scheduledAt: string | null; preopenAt: string; openAt: string; scenarioId: number; @@ -640,12 +640,12 @@ test('matches the normal preopen copy text and labels an immediate announcement 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 }) => { +test('shows a build-complete RESERVED announcement without the early-publication badge', async ({ page }) => { const gameOperations = await installFixture(page, { profileStatus: 'RESERVED', upcomingReset: { phase: 'READY', - scheduledAt: '2026-08-27T05:00:00.000Z', + scheduledAt: null, preopenAt: '2026-08-27T05:30:00.000Z', openAt: '2026-08-27T11:00:00.000Z', scenarioId: 1010, @@ -661,14 +661,10 @@ test('keeps the announcement through the RESERVED handoff after the build comple await page.goto('lobby'); const row = page.locator('tbody tr').filter({ hasText: 'hwe섭' }); - const badge = row.getByTestId('reserved-announcement-badge'); await expect(row.getByTestId('upcoming-reset-phase')).toHaveCount(0); await expect(row.getByTestId('upcoming-reset-scenario-title')).toHaveText('【가상】황건적의 난'); - await badge.hover(); - await expect(row.getByTestId('reserved-announcement-build-tooltip')).toHaveText( - '실제 빌드 시작 : 2026-08-27 14:00:00' - ); - await expect(row.getByTestId('reserved-announcement-build-tooltip')).not.toContainText('오픈 준비 완료'); + await expect(row.getByTestId('reserved-announcement-badge')).toHaveCount(0); + await expect(row.getByTestId('reserved-announcement-build-tooltip')).toHaveCount(0); await expect(row).not.toContainText('준 비 중 · 접근 불가'); await expect(row.getByRole('button', { name: '입장' })).toHaveCount(0); expect(gameOperations).toEqual([]);