merge: 최신 main을 Gateway 입구 공지 브랜치에 통합

This commit is contained in:
2026-08-19 11:08:45 +00:00
4 changed files with 63 additions and 3 deletions
@@ -789,6 +789,30 @@ describe('admin operation API', () => {
).rejects.toBeDefined(); ).rejects.toBeDefined();
}); });
it('stores event season zero as the next season number', async () => {
const harness = await buildCaller(
async () => {
throw new Error('not used');
},
{ adminRoles: ['admin.profiles.settings:che:2'], firstUserIsAdmin: false }
);
await harness.caller.admin.profiles.updateMeta({
profileName: 'che:2',
patch: { nextSeasonIdx: 0 },
reason: 'prepare event season',
});
expect(harness.updatedMetas.at(-1)).toMatchObject({ nextSeasonIdx: 0 });
await expect(
harness.caller.admin.profiles.updateMeta({
profileName: 'che:2',
patch: { nextSeasonIdx: -1 },
reason: 'reject negative season',
})
).rejects.toBeDefined();
});
it('does not let a scenario-only operator combine a Git update with reset', async () => { it('does not let a scenario-only operator combine a Git update with reset', async () => {
const harness = await buildCaller( const harness = await buildCaller(
async () => { async () => {
@@ -1040,6 +1040,33 @@ test('edits server reset defaults through profile metadata settings', async ({ p
expect(request).toContain('"npcMode":2'); expect(request).toContain('"npcMode":2');
}); });
test('stores event season zero from server metadata settings', async ({ page }) => {
const state: FixtureState = { operations: [], gatewayOperations: [], runtimeRunning: true, requestBodies: [] };
await installFixture(page, state);
await page.goto('admin/servers/che%3Adefault');
const nextSeasonInput = page.getByTestId('next-season-idx');
await nextSeasonInput.fill('0');
await expect(nextSeasonInput).toHaveValue('0');
expect(
await nextSeasonInput.evaluate((element: HTMLInputElement) => ({
valid: element.validity.valid,
valueAsNumber: element.valueAsNumber,
}))
).toEqual({ valid: true, valueAsNumber: 0 });
await page.getByPlaceholder('변경 사유 (필수)').fill('prepare event season');
await page.getByRole('button', { name: '메타 저장' }).click();
await expect
.poll(() => state.requestBodies.find((entry) => entry.operation === 'admin.profiles.updateMeta'))
.toBeTruthy();
const request = JSON.stringify(
state.requestBodies.find((entry) => entry.operation === 'admin.profiles.updateMeta')?.body
);
expect(request).toContain('"nextSeasonIdx":0');
await expect(page.getByTestId('action-toast').filter({ hasText: '메타 저장 완료' })).toBeVisible();
});
test('shows a dismissible error toast when profile metadata persistence fails', async ({ page }, testInfo) => { test('shows a dismissible error toast when profile metadata persistence fails', async ({ page }, testInfo) => {
const state: FixtureState = { const state: FixtureState = {
operations: [], operations: [],
+6 -2
View File
@@ -407,7 +407,7 @@ const profileEdits = ref<
color: string; color: string;
inGameNotice: string; inGameNotice: string;
profileImageUrl: string; profileImageUrl: string;
nextSeasonIdx: string; nextSeasonIdx: string | number;
localAccountAccessGraceDays: string; localAccountAccessGraceDays: string;
localAccountGeneralCreationGraceDays: string; localAccountGeneralCreationGraceDays: string;
resetDefaults: ProfileResetDefaults; resetDefaults: ProfileResetDefaults;
@@ -770,7 +770,10 @@ const updateProfileMeta = async (profileName: string) => {
if (!edit) { if (!edit) {
return; return;
} }
const nextSeasonRaw = edit.nextSeasonIdx.trim(); // Vue casts non-empty values from type="number" inputs to numbers even when
// the buffer was initialized with a string. Normalize both runtime shapes so
// event season 0 reaches the metadata mutation instead of throwing on trim().
const nextSeasonRaw = String(edit.nextSeasonIdx).trim();
const nextSeasonIdx = nextSeasonRaw === '' ? null : Number(nextSeasonRaw); const nextSeasonIdx = nextSeasonRaw === '' ? null : Number(nextSeasonRaw);
if (nextSeasonIdx !== null && (!Number.isFinite(nextSeasonIdx) || nextSeasonIdx < 0)) { if (nextSeasonIdx !== null && (!Number.isFinite(nextSeasonIdx) || nextSeasonIdx < 0)) {
profileActionStatus.value = { profileActionStatus.value = {
@@ -2229,6 +2232,7 @@ onMounted(() => {
type="number" type="number"
min="0" min="0"
step="1" step="1"
data-testid="next-season-idx"
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white" class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
placeholder="예: 12" placeholder="예: 12"
/> />
+6 -1
View File
@@ -76,6 +76,11 @@ Gateway 관리자 콘솔은 `/gateway/admin`에서 시작합니다. 공개 로
메타가 없거나 유효하지 않으면 기존 시스템 기본값을 사용합니다. 시나리오와 메타가 없거나 유효하지 않으면 기존 시스템 기본값을 사용합니다. 시나리오와
예약·가오픈·정식 오픈 시각은 매 실행마다 선택하므로 서버 기본값에 포함하지 예약·가오픈·정식 오픈 시각은 매 실행마다 선택하므로 서버 기본값에 포함하지
않습니다. 않습니다.
- 서버 상태의 `다음 시즌 번호`는 위 리셋 옵션과 별도의
`GatewayProfile.meta.nextSeasonIdx`이며 0 이상의 정수를 허용합니다. 이벤트
기수는 0을 저장한 뒤 RESET하고, 이벤트 종료 뒤 정상 기수 번호를 다시 저장한
다음 RESET합니다. 빈 값은 강제 번호를 해제하여 기존 게임의 season 또는 신규
기본값 1을 사용한다는 뜻입니다.
- 같은 화면의 `실행 중 게임 옵션`은 리셋 기본값과 별개로 현재 기수 DB의 턴 - 같은 화면의 `실행 중 게임 옵션`은 리셋 기본값과 별개로 현재 기수 DB의 턴
간격, 장수 생성 제한, 유저 자동턴 제한·동작을 읽어 표시합니다. 세 값은 간격, 장수 생성 제한, 유저 자동턴 제한·동작을 읽어 표시합니다. 세 값은
`admin.profiles.runtime:<name>` 권한과 3자 이상의 사유가 있을 때 하나의 `admin.profiles.runtime:<name>` 권한과 3자 이상의 사유가 있을 때 하나의
@@ -96,7 +101,7 @@ Gateway 관리자 콘솔은 `/gateway/admin`에서 시작합니다. 공개 로
| `admin.profiles.settings:<name>` | 표시 정보·리셋 기본 옵션·Kakao 미인증 접근/장수 생성 유예 | | `admin.profiles.settings:<name>` | 표시 정보·리셋 기본 옵션·Kakao 미인증 접근/장수 생성 유예 |
| `admin.profiles.deploy:<name>` | DB를 유지하는 Git 버전 업데이트, 초기화와 새 버전 결합 | | `admin.profiles.deploy:<name>` | DB를 유지하는 Git 버전 업데이트, 초기화와 새 버전 결합 |
| `admin.scenarios.reset:<name>` | 현재 배포 버전으로 시나리오 초기화 | | `admin.scenarios.reset:<name>` | 현재 배포 버전으로 시나리오 초기화 |
| `admin.games.cancel:<name>` | 진행 게임 취소, 기록 옵션과 유산 포인트 보전율 확정 | | `admin.games.cancel:<name>` | 진행 게임 취소, 기록 옵션과 유산 포인트 보전율 확정 |
| `admin.reset.schedule:<name>` | 허용된 시나리오 초기화를 미래 시각에 예약 | | `admin.reset.schedule:<name>` | 허용된 시나리오 초기화를 미래 시각에 예약 |
| `admin.releases.manage` | profile과 분리된 Gateway control plane 배포·rollback | | `admin.releases.manage` | profile과 분리된 Gateway control plane 배포·rollback |