merge: 최신 main을 Gateway 입구 공지 브랜치에 통합
This commit is contained in:
@@ -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: [],
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user