diff --git a/app/gateway-api/src/adminRouter.ts b/app/gateway-api/src/adminRouter.ts index 4307d71a..ff99494a 100644 --- a/app/gateway-api/src/adminRouter.ts +++ b/app/gateway-api/src/adminRouter.ts @@ -27,7 +27,11 @@ import type { GatewayApiContext } from './context.js'; import { resolveLocalAccountProfilePolicy } from './auth/localAccountPolicy.js'; import { GATEWAY_BUILD_STATUSES, GATEWAY_PROFILE_STATUSES } from './orchestrator/profileRepository.js'; import { readProfileReleaseSource } from './orchestrator/profileReleaseSource.js'; -import { orderGatewayProfiles, resolveGatewayProfileKoreanName } from './profileOrder.js'; +import { + orderGatewayProfiles, + resolveGatewayProfileDisplayName, + resolveGatewayProfileKoreanName, +} from './profileOrder.js'; import { purifyGatewayNoticeHtml } from './security/gatewayNoticeHtml.js'; const zProfileStatus = z.enum(GATEWAY_PROFILE_STATUSES); @@ -650,7 +654,25 @@ export const adminRouter = router({ }) .optional() ) - .query(({ ctx, input }) => (ctx as GatewayApiContext).adminAudit.list(input)), + .query(async ({ ctx, input }) => { + const gatewayCtx = ctx as GatewayApiContext; + const [events, profiles] = await Promise.all([ + gatewayCtx.adminAudit.list(input), + gatewayCtx.profiles.listProfiles(), + ]); + const displayNames = new Map( + profiles.map((profile) => [ + profile.profileName, + resolveGatewayProfileDisplayName(profile.profile, profile.instanceKey, profile.meta.korName), + ]) + ); + return events.map((event) => ({ + ...event, + ...(event.profileName && displayNames.has(event.profileName) + ? { profileDisplayName: displayNames.get(event.profileName) } + : {}), + })); + }), }), system: router({ getNotice: adminProcedure.query(async ({ ctx }) => { @@ -760,6 +782,13 @@ export const adminRouter = router({ specialAccessGrants, profiles: profiles.map((profile) => ({ profileName: profile.profileName, + profile: profile.profile, + instanceKey: profile.instanceKey, + displayName: resolveGatewayProfileDisplayName( + profile.profile, + profile.instanceKey, + profile.meta.korName + ), ...resolveLocalAccountProfilePolicy({ profile: profile.profile, profileName: profile.profileName, @@ -1727,6 +1756,11 @@ export const adminRouter = router({ profileName: profile.profileName, profile: profile.profile, instanceKey: profile.instanceKey, + displayName: resolveGatewayProfileDisplayName( + profile.profile, + profile.instanceKey, + profile.meta.korName + ), currentScenario: profile.currentScenario, meta: { korName: resolveGatewayProfileKoreanName(profile.profile, profile.meta.korName), @@ -1771,6 +1805,11 @@ export const adminRouter = router({ const runtimeSettingsMap = new Map(runtimeSettings.map((settings) => [settings.profileName, settings])); return profiles.map((profile) => ({ ...profile, + displayName: resolveGatewayProfileDisplayName( + profile.profile, + profile.instanceKey, + profile.meta.korName + ), runtimeActions: runtimeActionsByProfile.get(profile.profileName) ?? [], runtimeSettings: runtimeSettingsMap.get(profile.profileName) ?? null, activeOperation: activeOperationByProfile.get(profile.profileName) ?? null, @@ -1805,7 +1844,7 @@ export const adminRouter = router({ if (sourceMode === 'CURRENT') { if (!input?.profileName) { if (!adminAuth.isSuperuser) { - throw new TRPCError({ code: 'BAD_REQUEST', message: 'profileName is required.' }); + throw new TRPCError({ code: 'BAD_REQUEST', message: '대상 서버를 선택해야 합니다.' }); } } else { assertPermission(adminAuth, ROLE_ADMIN_SCENARIO_RESET, input.profileName); diff --git a/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts b/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts index 816f506b..b541f0af 100644 --- a/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts +++ b/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts @@ -23,7 +23,7 @@ import { import { isRecord } from '@sammo-ts/common'; import { resolveGatewayPostgresConfigFromEnv } from '../gatewayPostgresConfig.js'; -import { resolveGatewayProfileKoreanName } from '../profileOrder.js'; +import { resolveGatewayProfileDisplayName, resolveGatewayProfileKoreanName } from '../profileOrder.js'; import { buildTurboReleaseCommand, @@ -1626,7 +1626,11 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle { this.processConfig.workspaceRoot )), ]; - await this.appendOperationLog(operationId, 'build', `${profile.profileName} 구성 요소를 빌드합니다.`); + await this.appendOperationLog( + operationId, + 'build', + `${resolveGatewayProfileDisplayName(profile.profile, profile.instanceKey, profile.meta.korName)} 구성 요소를 빌드합니다.` + ); const result = await this.releaseBuildRunner.run(commands, this.buildProgress(operationId, 'build'), { signal: this.activeOperationAbortSignal, }); @@ -2233,7 +2237,11 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle { await this.appendOperationLog( operationId, 'build', - `${profile?.profileName ?? 'profile'} 구성 요소를 빌드합니다.` + `${ + profile + ? resolveGatewayProfileDisplayName(profile.profile, profile.instanceKey, profile.meta.korName) + : '대상 서버' + } 구성 요소를 빌드합니다.` ); } return { @@ -2435,7 +2443,13 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle { private async stageStaticProfileFrontend(profile: GatewayProfileRecord): Promise { if (!profile.buildCommitSha) { - throw new Error(`Profile ${profile.profileName} is missing the build commit SHA.`); + throw new Error( + `${resolveGatewayProfileDisplayName( + profile.profile, + profile.instanceKey, + profile.meta.korName + )} 서버의 build commit SHA가 없습니다.` + ); } const runtimeWorkspace = profile.buildWorkspace ?? this.processConfig.workspaceRoot; const sharedSourceRoot = buildSharedProfileFrontendOutDir(runtimeWorkspace); diff --git a/app/gateway-api/src/profileOrder.ts b/app/gateway-api/src/profileOrder.ts index 0a86a710..236b8401 100644 --- a/app/gateway-api/src/profileOrder.ts +++ b/app/gateway-api/src/profileOrder.ts @@ -20,6 +20,19 @@ export const resolveGatewayProfileKoreanName = (profile: string, configuredName? return gatewayProfileKoreanNames.get(profile) ?? profile; }; +/** + * User-facing profile label. The immutable profileName (`che:default`) remains + * an internal routing/storage key and must not leak into ordinary UI copy. + */ +export const resolveGatewayProfileDisplayName = ( + profile: string, + instanceKey: string, + configuredName?: unknown +): string => { + const koreanName = resolveGatewayProfileKoreanName(profile, configuredName); + return instanceKey === 'default' ? koreanName : `${koreanName} [${instanceKey}]`; +}; + const compareGatewayProfiles = ( left: { profile: string; instanceKey: string }, right: { profile: string; instanceKey: string } diff --git a/app/gateway-api/src/webPush/coordinator.ts b/app/gateway-api/src/webPush/coordinator.ts index 25759d4b..dbea569a 100644 --- a/app/gateway-api/src/webPush/coordinator.ts +++ b/app/gateway-api/src/webPush/coordinator.ts @@ -10,6 +10,8 @@ import { import { GatewayPrisma, type GatewayPrismaClient } from '@sammo-ts/infra'; import webPush from 'web-push'; +import { resolveGatewayProfileDisplayName } from '../profileOrder.js'; + export interface WebPushCoordinatorConfig { enabled: boolean; vapidSubject?: string; @@ -104,7 +106,14 @@ export class WebPushCoordinator { const [profiles, preferences, subscriptionCount, currentSubscription] = await Promise.all([ this.prisma.gatewayProfile.findMany({ orderBy: [{ profile: 'asc' }, { instanceKey: 'asc' }], - select: { profileName: true, profile: true, currentScenario: true, status: true }, + select: { + profileName: true, + profile: true, + instanceKey: true, + currentScenario: true, + status: true, + meta: true, + }, }), this.prisma.webPushPreference.findMany({ where: { userId }, @@ -128,7 +137,15 @@ export class WebPushCoordinator { capability: this.getCapability(), eventTypes: WEB_PUSH_EVENT_TYPES, profiles: profiles.map((profile) => ({ - ...profile, + profileName: profile.profileName, + profile: profile.profile, + instanceKey: profile.instanceKey, + displayName: resolveGatewayProfileDisplayName( + profile.profile, + profile.instanceKey, + (profile.meta as Record | null)?.korName + ), + currentScenario: profile.currentScenario, status: String(profile.status), })), preferences: preferences.filter((preference) => isWebPushEventType(preference.eventType)), @@ -218,7 +235,7 @@ export class WebPushCoordinator { if (!this.configured) return false; const profile = await tx.gatewayProfile.findUnique({ where: { profileName: event.profileName }, - select: { profile: true, profileName: true }, + select: { profile: true, profileName: true, instanceKey: true, meta: true }, }); if (!profile) return false; const receipt = await tx.webPushEventReceipt.createMany({ @@ -258,7 +275,16 @@ export class WebPushCoordinator { ids.push(subscription.id); subscriptionIdsByUser.set(subscription.userId, ids); } - const copy = copyFor(event.eventType, profile.profile, event.year, event.month); + const copy = copyFor( + event.eventType, + resolveGatewayProfileDisplayName( + profile.profile, + profile.instanceKey, + (profile.meta as Record | null)?.korName + ), + event.year, + event.month + ); for (const userId of selectedUserIds) { const subscriptionIds = subscriptionIdsByUser.get(userId) ?? []; if (subscriptionIds.length === 0) continue; @@ -395,10 +421,7 @@ export class WebPushCoordinator { }); for (const delivery of claimed) { - if ( - delivery.subscription.expirationTime && - delivery.subscription.expirationTime.getTime() <= Date.now() - ) { + if (delivery.subscription.expirationTime && delivery.subscription.expirationTime.getTime() <= Date.now()) { await this.prisma.$transaction(async (tx) => { await tx.webPushDelivery.updateMany({ where: { id: delivery.id, lockOwner: this.owner }, @@ -445,11 +468,15 @@ export class WebPushCoordinator { typeof error === 'object' && error !== null && 'statusCode' in error ? Number((error as { statusCode?: unknown }).statusCode) : 0; - const terminal = statusCode === 404 || statusCode === 410 || (statusCode >= 400 && statusCode < 500 && statusCode !== 429); + const terminal = + statusCode === 404 || + statusCode === 410 || + (statusCode >= 400 && statusCode < 500 && statusCode !== 429); const attempts = delivery.attempts; const exhausted = attempts >= 8; const delaySeconds = Math.min(300, 2 ** Math.min(attempts, 8)); - const safeError = statusCode > 0 ? `Push service returned HTTP ${statusCode}.` : 'Push service request failed.'; + const safeError = + statusCode > 0 ? `Push service returned HTTP ${statusCode}.` : 'Push service request failed.'; await this.prisma.$transaction(async (tx) => { await tx.webPushDelivery.updateMany({ where: { id: delivery.id, lockOwner: this.owner }, diff --git a/app/gateway-api/test/adminOperations.test.ts b/app/gateway-api/test/adminOperations.test.ts index 1c35892d..b466c631 100644 --- a/app/gateway-api/test/adminOperations.test.ts +++ b/app/gateway-api/test/adminOperations.test.ts @@ -380,6 +380,7 @@ describe('admin profile navigation API', () => { profileName: 'che:2', profile: 'che', instanceKey: '2', + displayName: '체 [2]', currentScenario: '2', meta: { korName: '체' }, }, diff --git a/app/gateway-api/test/profileOrder.test.ts b/app/gateway-api/test/profileOrder.test.ts index 8dd710b4..35b12ba1 100644 --- a/app/gateway-api/test/profileOrder.test.ts +++ b/app/gateway-api/test/profileOrder.test.ts @@ -4,6 +4,7 @@ import { GATEWAY_PROFILE_KOREAN_NAMES, GATEWAY_PROFILE_ORDER, orderGatewayProfiles, + resolveGatewayProfileDisplayName, resolveGatewayProfileKoreanName, } from '../src/profileOrder.js'; @@ -56,3 +57,12 @@ describe('resolveGatewayProfileKoreanName', () => { expect(resolveGatewayProfileKoreanName('custom')).toBe('custom'); }); }); + +describe('resolveGatewayProfileDisplayName', () => { + it('hides the default instance key and distinguishes non-default instances', () => { + expect(resolveGatewayProfileDisplayName('che', 'default')).toBe('체'); + expect(resolveGatewayProfileDisplayName('hwe', '2')).toBe('훼 [2]'); + expect(resolveGatewayProfileDisplayName('che', 'default', ' 천하서버 ')).toBe('천하서버'); + expect(resolveGatewayProfileDisplayName('custom', 'blue')).toBe('custom [blue]'); + }); +}); diff --git a/app/gateway-frontend/e2e/admin-account-controls.spec.ts b/app/gateway-frontend/e2e/admin-account-controls.spec.ts index cdb45e28..b2ac3bd1 100644 --- a/app/gateway-frontend/e2e/admin-account-controls.spec.ts +++ b/app/gateway-frontend/e2e/admin-account-controls.spec.ts @@ -131,6 +131,9 @@ const installFixture = async (page: Page) => { profiles: [ { profileName: 'che:default', + profile: 'che', + instanceKey: 'default', + displayName: '체', requiresKakaoVerification: true, kakaoVerified: false, accessAllowed: true, @@ -219,15 +222,16 @@ test('operates OAuth grace and scheduled deletion with reasoned audit history', await page.getByRole('button', { name: /접근 · 권한/ }).click(); await expect(page.getByRole('option', { name: /Profile 전체 운영/ })).toHaveCount(0); await expect(page.getByRole('option', { name: /Profile 실행 관리/ })).toHaveCount(1); - await expect(page.getByRole('cell', { name: 'che:default' })).toBeVisible(); + await expect(page.getByRole('cell', { name: '체' })).toBeVisible(); + await expect(page.getByText('che:default', { exact: true })).toHaveCount(0); await page.screenshot({ path: testInfo.outputPath('gateway-admin-account-controls-desktop.png'), fullPage: true }); await page.getByPlaceholder('권한·제재·복구·탈퇴 조치 사유 (필수)').fill('본인 확인 처리 중'); await page.getByLabel('특수 접근 만료 시각').fill('2026-08-20T00:00'); - await page.getByPlaceholder('che 또는 che:2 (쉼표 구분, 비우면 전체)').fill('che'); + await page.getByRole('checkbox', { name: '체' }).check(); await page.getByPlaceholder('권한·제재·복구·탈퇴 조치 사유 (필수)').fill('휴대폰 분실 임시 복구'); await page.getByRole('button', { name: '특수 접근 부여', exact: true }).click(); await expect(page.getByText('특수 접근 자격을 부여했습니다.').first()).toBeVisible(); - await expect(page.getByText(/RECOVERY · che/)).toBeVisible(); + await expect(page.getByText(/RECOVERY · 체/)).toBeVisible(); await page.screenshot({ path: testInfo.outputPath('gateway-admin-special-access-granted.png'), fullPage: true }); const gracePanel = page.getByRole('heading', { name: 'Kakao 인증 유예' }).locator('..'); diff --git a/app/gateway-frontend/e2e/admin-runtime-actions.spec.ts b/app/gateway-frontend/e2e/admin-runtime-actions.spec.ts index 7c27a78b..8ec646c3 100644 --- a/app/gateway-frontend/e2e/admin-runtime-actions.spec.ts +++ b/app/gateway-frontend/e2e/admin-runtime-actions.spec.ts @@ -165,6 +165,7 @@ const installFixture = async ( profileName: 'hwe:default', profile: 'hwe', instanceKey: 'default', + displayName: '훼', currentScenario: options.currentScenario === undefined ? '1010' : options.currentScenario, meta: {}, }, @@ -177,6 +178,7 @@ const installFixture = async ( profileName: 'hwe:default', profile: 'hwe', instanceKey: 'default', + displayName: '훼', currentScenario: options.currentScenario === undefined ? '1010' : options.currentScenario, scenario: options.currentScenario ?? 'default', apiPort: 15015, @@ -542,7 +544,7 @@ test('directs profile deployment to the selected server version tab', async ({ p const tabAndHeaderGeometry = await Promise.all([ tabs.evaluate((element) => element.getBoundingClientRect().top), page - .getByText('서버 ID: hwe:default · 인스턴스: default', { exact: true }) + .getByText('현재 시나리오: 1010', { exact: true }) .evaluate((element) => element.getBoundingClientRect().top), ]); expect(tabAndHeaderGeometry[0]).toBeLessThan(tabAndHeaderGeometry[1]); diff --git a/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts b/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts index d3ae7b0c..7d4b39dd 100644 --- a/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts +++ b/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts @@ -48,6 +48,7 @@ const installGatewayFixture = async (page: Page, roles: string[]) => { profileName: 'hwe:2', profile: 'hwe', instanceKey: '2', + displayName: '환상서버 [2]', currentScenario: '1010', scenario: '1010', status: 'RUNNING', @@ -68,6 +69,7 @@ const installGatewayFixture = async (page: Page, roles: string[]) => { profileName: 'hwe:2', profile: 'hwe', instanceKey: '2', + displayName: '환상서버 [2]', currentScenario: '1010', meta: { korName: '환상서버' }, }, @@ -306,23 +308,26 @@ test('keeps mobile account actions on clean rows for users and administrators', const adminLink = adminPage.getByRole('link', { name: '관리자 페이지' }); const baseBackground = await adminLink.evaluate((element) => getComputedStyle(element).backgroundColor); await adminLink.hover(); - await expect.poll(() => adminLink.evaluate((element) => getComputedStyle(element).backgroundColor)).not.toBe( - baseBackground - ); + await expect + .poll(() => adminLink.evaluate((element) => getComputedStyle(element).backgroundColor)) + .not.toBe(baseBackground); await adminLink.focus(); await expect(adminLink).toBeFocused(); - await adminPage.screenshot({ path: testInfo.outputPath('gateway-account-actions-admin-500px.png'), fullPage: true }); + await adminPage.screenshot({ + path: testInfo.outputPath('gateway-account-actions-admin-500px.png'), + fullPage: true, + }); await adminPage.setViewportSize({ width: 390, height: 844 }); const adminNarrowGeometry = await measureAccountActions(adminPage); expect(adminNarrowGeometry.documentWidth).toBe(adminNarrowGeometry.viewportWidth); expect(adminNarrowGeometry.items[0]?.top).toBeCloseTo(adminNarrowGeometry.items[1]?.top ?? 0, 0); - expect(adminNarrowGeometry.items[2]?.top).toBeCloseTo( - (adminNarrowGeometry.items[1]?.bottom ?? 0) + 16, - 0 - ); + expect(adminNarrowGeometry.items[2]?.top).toBeCloseTo((adminNarrowGeometry.items[1]?.bottom ?? 0) + 16, 0); expect(adminNarrowGeometry.items.every(({ scrollWidth, clientWidth }) => scrollWidth <= clientWidth)).toBe(true); - await adminPage.screenshot({ path: testInfo.outputPath('gateway-account-actions-admin-390px.png'), fullPage: true }); + await adminPage.screenshot({ + path: testInfo.outputPath('gateway-account-actions-admin-390px.png'), + fullPage: true, + }); await adminPage.setViewportSize({ width: 360, height: 800 }); const adminSmallGeometry = await measureAccountActions(adminPage); diff --git a/app/gateway-frontend/e2e/server-operations.spec.ts b/app/gateway-frontend/e2e/server-operations.spec.ts index a5b2eccb..cdbdf58d 100644 --- a/app/gateway-frontend/e2e/server-operations.spec.ts +++ b/app/gateway-frontend/e2e/server-operations.spec.ts @@ -71,6 +71,7 @@ const profile = (runtimeRunning: boolean, resetDefaults?: Record { profileName: 'che:default', profile: 'che', instanceKey: 'default', + displayName: '천하서버', currentScenario: '2', meta: { korName: '천하서버' }, }, @@ -639,7 +641,7 @@ test('separates branch and commit semantics and submits a reset from the dedicat await expect(page.getByText('초기화 작업을 등록했습니다.').first()).toBeVisible(); await expect(page.getByTestId('operations-table')).toContainText('시나리오 초기화'); await expect(page.getByTestId('profile-operation-log-panel')).toBeVisible(); - await expect(page.getByTestId('profile-operation-log')).toContainText('che:default 구성 요소를 빌드합니다.'); + await expect(page.getByTestId('profile-operation-log')).toContainText('천하서버 구성 요소를 빌드합니다.'); await expect(page.getByTestId('profile-operation-log')).toContainText('시나리오 초기 데이터 생성을 완료했습니다.'); await expect(page.getByTestId('profile-operation-log-status')).toContainText('SUCCEEDED'); const operationTable = page.getByTestId('operations-table'); @@ -652,7 +654,8 @@ test('separates branch and commit semantics and submits a reset from the dedicat await detailsToggle.click(); await expect(detailsToggle).toHaveAttribute('aria-expanded', 'true'); const operationDetail = operationTable.getByTestId('operation-detail'); - await expect(operationDetail).toContainText('che:default'); + await expect(operationDetail).toContainText('천하서버'); + await expect(operationDetail).not.toContainText('che:default'); await expect(operationDetail).toContainText('0123456789abcdef0123456789abcdef01234567'); await expect(operationDetail).toContainText('admin'); const operationTableGeometry = await page.getByTestId('operations-table').evaluate((table) => { @@ -807,7 +810,7 @@ test('separates DB-preserving profile deployment from DB reset', async ({ page } await expect(page.getByText('DB 보존 배포 작업을 등록했습니다.').first()).toBeVisible(); await expect(page.getByTestId('operations-table')).toContainText('버전 업데이트'); await expect(page.getByTestId('profile-operation-log-panel')).toBeVisible(); - await expect(page.getByTestId('profile-operation-log')).toContainText('che:default 구성 요소를 빌드합니다.'); + await expect(page.getByTestId('profile-operation-log')).toContainText('천하서버 구성 요소를 빌드합니다.'); await expect(page.getByTestId('profile-operation-log')).toContainText('game-frontend build complete'); await expect(page.getByTestId('profile-operation-log-status')).toContainText('SUCCEEDED'); expect(state.requestBodies.some((entry) => entry.operation === 'admin.operations.requestDeploy')).toBe(true); @@ -834,7 +837,7 @@ test('submits a separately authorized destructive game cancellation on desktop a await page.goto('admin/servers/che%3Adefault/cancel'); await expect(page).toHaveURL(/\/gateway\/admin\/servers\/che%3Adefault\/cancel$/); - await expect(page.getByRole('heading', { name: 'che:default 게임 취소' })).toBeVisible(); + await expect(page.getByRole('heading', { name: '천하서버 게임 취소' })).toBeVisible(); await expect(page.getByRole('link', { name: '게임 취소', exact: true })).toHaveAttribute('aria-current', 'page'); await expect(page.getByRole('link', { name: '시나리오 초기화', exact: true })).toHaveCount(0); await expect(page.getByTestId('request-game-cancellation')).toBeDisabled(); @@ -843,7 +846,7 @@ test('submits a separately authorized destructive game cancellation on desktop a await page.getByTestId('cancellation-general-mode').selectOption('RETAIN'); await page.getByTestId('cancellation-retention-percent').fill('35'); await page.getByTestId('cancellation-reason').fill('잘못된 시나리오로 개장함'); - await page.getByTestId('cancellation-confirmation').fill('che:default'); + await page.getByTestId('cancellation-confirmation').fill('천하서버 게임 취소'); const cancelButton = page.getByTestId('request-game-cancellation'); await expect(cancelButton).toBeEnabled(); await cancelButton.hover(); @@ -1348,10 +1351,11 @@ test('renders the stable server identity without exposing the default suffix as const navigation = page.getByRole('navigation', { name: '관리자 메뉴' }); const profileLink = navigation.getByRole('link', { name: '천하서버' }); await expect(profileLink).toBeVisible({ timeout: 900 }); - await expect(profileLink).toHaveAttribute('title', '서버 ID: che:default'); + await expect(profileLink).toHaveAttribute('title', '천하서버 서버 관리'); await expect(navigation).not.toContainText('천하서버 (che:default)'); await expect(navigation.getByRole('link', { name: 'Gateway 릴리스' })).toBeVisible({ timeout: 900 }); - await expect(page.getByText('서버 ID: che:default · 인스턴스: default')).toBeVisible(); + await expect(page.getByText('서버 ID: che:default · 인스턴스: default')).toHaveCount(0); + await expect(page.getByText('천하서버', { exact: true })).toBeVisible(); await expect(page.getByText('현재 시나리오: 2')).toBeVisible(); await profileLink.focus(); const desktop = await profileLink.evaluate((element) => { diff --git a/app/gateway-frontend/e2e/web-push-settings.spec.ts b/app/gateway-frontend/e2e/web-push-settings.spec.ts index e885531f..f32da536 100644 --- a/app/gateway-frontend/e2e/web-push-settings.spec.ts +++ b/app/gateway-frontend/e2e/web-push-settings.spec.ts @@ -59,6 +59,8 @@ const installFixture = async (page: Page) => { { profileName: 'hwe:default', profile: 'hwe', + instanceKey: 'default', + displayName: '훼', currentScenario: 'default', status: 'RUNNING', }, @@ -93,6 +95,8 @@ test('web push settings are default-off and remain configurable while delivery i const table = page.locator('#notification-table'); await expect(table).toBeVisible(); await expect(table).toContainText('준비됨 · 운영 비활성'); + await expect(table).toContainText('훼 · default'); + await expect(table).not.toContainText('hwe:default'); await expect(page.getByRole('button', { name: '이 기기 알림 켜기' })).toBeDisabled(); const checkboxes = table.getByRole('checkbox'); await expect(checkboxes).toHaveCount(9); diff --git a/app/gateway-frontend/src/components/ServerProfileTabs.vue b/app/gateway-frontend/src/components/ServerProfileTabs.vue index 8f9d9bd5..174d408a 100644 --- a/app/gateway-frontend/src/components/ServerProfileTabs.vue +++ b/app/gateway-frontend/src/components/ServerProfileTabs.vue @@ -5,6 +5,7 @@ type ServerProfileTab = 'status' | 'version' | 'scenario' | 'cancel'; const props = defineProps<{ profileName: string; + profileLabel: string; activeTab: ServerProfileTab; canDeploy: boolean; canReset: boolean; @@ -45,7 +46,7 @@ const tabs = computed(() =>