merge: restore current scenario catalog selection
This commit is contained in:
@@ -1549,6 +1549,7 @@ export const adminRouter = router({
|
|||||||
const adminAuth = requireAdminAuth(ctx);
|
const adminAuth = requireAdminAuth(ctx);
|
||||||
const sourceMode = input?.sourceMode ?? 'CURRENT';
|
const sourceMode = input?.sourceMode ?? 'CURRENT';
|
||||||
let gitRef = input?.gitRef?.trim();
|
let gitRef = input?.gitRef?.trim();
|
||||||
|
let currentScenarioId: number | null = null;
|
||||||
if (sourceMode === 'CURRENT') {
|
if (sourceMode === 'CURRENT') {
|
||||||
if (!input?.profileName) {
|
if (!input?.profileName) {
|
||||||
if (!adminAuth.isSuperuser) {
|
if (!adminAuth.isSuperuser) {
|
||||||
@@ -1562,6 +1563,8 @@ export const adminRouter = router({
|
|||||||
);
|
);
|
||||||
const profile = await ctx.profiles.getProfile(input.profileName);
|
const profile = await ctx.profiles.getProfile(input.profileName);
|
||||||
if (!profile) throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found.' });
|
if (!profile) throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found.' });
|
||||||
|
const parsedScenarioId = Number(profile.scenario);
|
||||||
|
currentScenarioId = Number.isInteger(parsedScenarioId) ? parsedScenarioId : null;
|
||||||
gitRef = profile.buildCommitSha?.trim();
|
gitRef = profile.buildCommitSha?.trim();
|
||||||
if (!gitRef) {
|
if (!gitRef) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -1575,14 +1578,18 @@ export const adminRouter = router({
|
|||||||
} else {
|
} else {
|
||||||
assertAnyPermission(adminAuth, [ROLE_ADMIN_PROFILES, ROLE_ADMIN_PROFILE_DEPLOY]);
|
assertAnyPermission(adminAuth, [ROLE_ADMIN_PROFILES, ROLE_ADMIN_PROFILE_DEPLOY]);
|
||||||
}
|
}
|
||||||
if (!gitRef) {
|
const scenarios = !gitRef
|
||||||
return listScenarioPreviews();
|
? await listScenarioPreviews()
|
||||||
}
|
: await listScenarioPreviews({
|
||||||
const resolved =
|
gitRef:
|
||||||
sourceMode === 'BRANCH'
|
sourceMode === 'BRANCH'
|
||||||
? await resolveGitBranchCommitSha(gitRef)
|
? await resolveGitBranchCommitSha(gitRef)
|
||||||
: await resolveGitCommitSha(gitRef);
|
: await resolveGitCommitSha(gitRef),
|
||||||
return listScenarioPreviews({ gitRef: resolved });
|
});
|
||||||
|
return scenarios.map((scenario) => ({
|
||||||
|
...scenario,
|
||||||
|
isCurrent: currentScenarioId === scenario.id,
|
||||||
|
}));
|
||||||
}),
|
}),
|
||||||
upsert: profileAdminProcedure
|
upsert: profileAdminProcedure
|
||||||
.input(
|
.input(
|
||||||
|
|||||||
@@ -326,6 +326,24 @@ describe('admin profile navigation API', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('admin scenario catalog API', () => {
|
||||||
|
it('marks scenario zero as the current selectable scenario', async () => {
|
||||||
|
const harness = await buildCaller(
|
||||||
|
async () => {
|
||||||
|
throw new Error('not used');
|
||||||
|
},
|
||||||
|
{ profileScenario: '0' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const scenarios = await harness.caller.admin.profiles.listScenarios({
|
||||||
|
profileName: 'che:2',
|
||||||
|
sourceMode: 'CURRENT',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(scenarios.find((scenario) => scenario.id === 0)?.isCurrent).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('gateway notice API', () => {
|
describe('gateway notice API', () => {
|
||||||
const dirtyNotice =
|
const dirtyNotice =
|
||||||
'<b>점검</b><br><script>globalThis.__noticeXss=1</script>' +
|
'<b>점검</b><br><script>globalThis.__noticeXss=1</script>' +
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type FixtureState = {
|
|||||||
profileNavigationDelayMs?: number;
|
profileNavigationDelayMs?: number;
|
||||||
profileNavigationRequests?: number;
|
profileNavigationRequests?: number;
|
||||||
profileNavigationResolved?: boolean;
|
profileNavigationResolved?: boolean;
|
||||||
|
scenarioFailuresRemaining?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const profile = (runtimeRunning: boolean) => ({
|
const profile = (runtimeRunning: boolean) => ({
|
||||||
@@ -65,6 +66,16 @@ const profile = (runtimeRunning: boolean) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const scenarios = [
|
const scenarios = [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
title: '【테스트】공백지',
|
||||||
|
year: null,
|
||||||
|
npcCount: 0,
|
||||||
|
npcExCount: 0,
|
||||||
|
npcNeutralCount: 0,
|
||||||
|
nations: [],
|
||||||
|
isCurrent: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: '【테스트】황건의 난',
|
title: '【테스트】황건의 난',
|
||||||
@@ -73,6 +84,7 @@ const scenarios = [
|
|||||||
npcExCount: 0,
|
npcExCount: 0,
|
||||||
npcNeutralCount: 0,
|
npcNeutralCount: 0,
|
||||||
nations: [],
|
nations: [],
|
||||||
|
isCurrent: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
@@ -82,6 +94,7 @@ const scenarios = [
|
|||||||
npcExCount: 0,
|
npcExCount: 0,
|
||||||
npcNeutralCount: 0,
|
npcNeutralCount: 0,
|
||||||
nations: [],
|
nations: [],
|
||||||
|
isCurrent: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -109,6 +122,11 @@ const installFixture = async (page: Page, state: FixtureState) => {
|
|||||||
if (names.includes('admin.profiles.list') && state.profileListDelayMs) {
|
if (names.includes('admin.profiles.list') && state.profileListDelayMs) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, state.profileListDelayMs));
|
await new Promise((resolve) => setTimeout(resolve, state.profileListDelayMs));
|
||||||
}
|
}
|
||||||
|
if (names.includes('admin.profiles.listScenarios') && (state.scenarioFailuresRemaining ?? 0) > 0) {
|
||||||
|
state.scenarioFailuresRemaining = (state.scenarioFailuresRemaining ?? 0) - 1;
|
||||||
|
await route.abort('failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const results = names.map((name) => {
|
const results = names.map((name) => {
|
||||||
if (route.request().method() === 'POST') {
|
if (route.request().method() === 'POST') {
|
||||||
state.requestBodies.push({ operation: name, body });
|
state.requestBodies.push({ operation: name, body });
|
||||||
@@ -185,6 +203,7 @@ const installFixture = async (page: Page, state: FixtureState) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (name === 'admin.profiles.listScenarios') {
|
if (name === 'admin.profiles.listScenarios') {
|
||||||
|
expect(names).toEqual(['admin.profiles.listScenarios']);
|
||||||
return response(scenarios);
|
return response(scenarios);
|
||||||
}
|
}
|
||||||
if (name === 'admin.operations.requestReset') {
|
if (name === 'admin.operations.requestReset') {
|
||||||
@@ -295,6 +314,26 @@ test('separates branch and commit semantics and submits a reset from the dedicat
|
|||||||
await expect(page.getByTestId('source-current')).toBeChecked();
|
await expect(page.getByTestId('source-current')).toBeChecked();
|
||||||
await expect(page.getByTestId('source-help')).toContainText('현재 서버에 배포된 커밋');
|
await expect(page.getByTestId('source-help')).toContainText('현재 서버에 배포된 커밋');
|
||||||
await expect(page.getByTestId('scenario-select')).toHaveValue('2');
|
await expect(page.getByTestId('scenario-select')).toHaveValue('2');
|
||||||
|
await expect(page.getByTestId('request-reset')).toBeEnabled();
|
||||||
|
await expect(page.getByTestId('scenario-select').locator('option:checked')).toContainText('현재 시나리오');
|
||||||
|
const catalogGeometry = await page.getByTestId('scenario-select').evaluate((select) => {
|
||||||
|
const scenarioSelect = select as HTMLSelectElement;
|
||||||
|
const rect = select.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: rect.x,
|
||||||
|
y: rect.y,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
optionCount: scenarioSelect.options.length,
|
||||||
|
value: scenarioSelect.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(catalogGeometry.optionCount).toBe(3);
|
||||||
|
expect(catalogGeometry.value).toBe('2');
|
||||||
|
expect(catalogGeometry.width).toBeGreaterThan(300);
|
||||||
|
await page.screenshot({ path: testInfo.outputPath('current-scenario-catalog.png'), fullPage: true });
|
||||||
|
await page.getByTestId('scenario-select').selectOption('0');
|
||||||
|
await expect(page.getByTestId('request-reset')).toBeEnabled();
|
||||||
await expect(page.getByTestId('server-profile-tabs')).toBeVisible();
|
await expect(page.getByTestId('server-profile-tabs')).toBeVisible();
|
||||||
await expect(page.getByRole('link', { name: '시나리오 초기화', exact: true })).toHaveAttribute(
|
await expect(page.getByRole('link', { name: '시나리오 초기화', exact: true })).toHaveAttribute(
|
||||||
'aria-current',
|
'aria-current',
|
||||||
@@ -331,7 +370,7 @@ test('separates branch and commit semantics and submits a reset from the dedicat
|
|||||||
});
|
});
|
||||||
await writeFile(
|
await writeFile(
|
||||||
testInfo.outputPath('layout-metrics.json'),
|
testInfo.outputPath('layout-metrics.json'),
|
||||||
JSON.stringify({ desktopGeometry, focusedInputStyle }, null, 2)
|
JSON.stringify({ catalogGeometry, desktopGeometry, focusedInputStyle }, null, 2)
|
||||||
);
|
);
|
||||||
await page.screenshot({ path: testInfo.outputPath('desktop-operations.png'), fullPage: true });
|
await page.screenshot({ path: testInfo.outputPath('desktop-operations.png'), fullPage: true });
|
||||||
|
|
||||||
@@ -415,6 +454,24 @@ test('renders the fixed-profile version form without waiting for the server list
|
|||||||
expect(state.profileNavigationRequests).toBe(1);
|
expect(state.profileNavigationRequests).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('recovers the current-version scenario catalog after the initial request fails', async ({ page }) => {
|
||||||
|
const state: FixtureState = {
|
||||||
|
operations: [],
|
||||||
|
gatewayOperations: [],
|
||||||
|
runtimeRunning: true,
|
||||||
|
requestBodies: [],
|
||||||
|
scenarioFailuresRemaining: 1,
|
||||||
|
};
|
||||||
|
await installFixture(page, state);
|
||||||
|
|
||||||
|
await page.goto('admin/servers/che%3A2/scenario');
|
||||||
|
await expect(page.getByTestId('scenario-select')).toContainText('선택할 수 있는 시나리오가 없습니다.');
|
||||||
|
await expect(page.getByTestId('request-reset')).toBeDisabled();
|
||||||
|
await page.getByTestId('load-scenarios').click();
|
||||||
|
await expect(page.getByTestId('scenario-select')).toHaveValue('2');
|
||||||
|
await expect(page.getByTestId('request-reset')).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
test('renders the server navigation before the detailed runtime profile request resolves', async ({ page }) => {
|
test('renders the server navigation before the detailed runtime profile request resolves', async ({ page }) => {
|
||||||
const state: FixtureState = {
|
const state: FixtureState = {
|
||||||
operations: [],
|
operations: [],
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch }
|
|||||||
|
|
||||||
import ServerProfileTabs from '../components/ServerProfileTabs.vue';
|
import ServerProfileTabs from '../components/ServerProfileTabs.vue';
|
||||||
import AdminConsoleLayout from '../layouts/AdminConsoleLayout.vue';
|
import AdminConsoleLayout from '../layouts/AdminConsoleLayout.vue';
|
||||||
import { trpc } from '../utils/trpc';
|
import { directTrpc, trpc } from '../utils/trpc';
|
||||||
|
|
||||||
type OperationPageMode = 'version' | 'scenario' | 'gateway';
|
type OperationPageMode = 'version' | 'scenario' | 'gateway';
|
||||||
|
|
||||||
@@ -13,12 +13,14 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const adminClient = trpc.admin;
|
const adminClient = trpc.admin;
|
||||||
|
const scenarioClient = directTrpc.admin;
|
||||||
|
|
||||||
type Scenario = {
|
type Scenario = {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
year: number | null;
|
year: number | null;
|
||||||
npcCount: number;
|
npcCount: number;
|
||||||
|
isCurrent: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Operation = {
|
type Operation = {
|
||||||
@@ -84,6 +86,7 @@ const selectedProfileName = computed(() => props.profileName ?? '');
|
|||||||
const capabilities = ref<Array<{ permission: string; scopes?: string[] }>>([]);
|
const capabilities = ref<Array<{ permission: string; scopes?: string[] }>>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const catalogLoading = ref(false);
|
const catalogLoading = ref(false);
|
||||||
|
const catalogAttempted = ref(false);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const message = ref('');
|
const message = ref('');
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
@@ -95,7 +98,7 @@ let componentMounted = false;
|
|||||||
const form = reactive({
|
const form = reactive({
|
||||||
sourceMode: (props.mode === 'scenario' ? 'CURRENT' : 'BRANCH') as 'CURRENT' | 'BRANCH' | 'COMMIT',
|
sourceMode: (props.mode === 'scenario' ? 'CURRENT' : 'BRANCH') as 'CURRENT' | 'BRANCH' | 'COMMIT',
|
||||||
sourceRef: 'main',
|
sourceRef: 'main',
|
||||||
scenarioId: 0,
|
scenarioId: null as number | null,
|
||||||
turnTermMinutes: 60,
|
turnTermMinutes: 60,
|
||||||
sync: true,
|
sync: true,
|
||||||
fiction: 1,
|
fiction: 1,
|
||||||
@@ -372,20 +375,22 @@ const loadScenarios = async () => {
|
|||||||
}
|
}
|
||||||
catalogLoading.value = true;
|
catalogLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await adminClient.profiles.listScenarios.query({
|
const result = await scenarioClient.profiles.listScenarios.query({
|
||||||
profileName: selectedProfileName.value,
|
profileName: selectedProfileName.value,
|
||||||
gitRef: form.sourceMode === 'CURRENT' ? undefined : form.sourceRef.trim(),
|
gitRef: form.sourceMode === 'CURRENT' ? undefined : form.sourceRef.trim(),
|
||||||
sourceMode: form.sourceMode,
|
sourceMode: form.sourceMode,
|
||||||
});
|
});
|
||||||
scenarios.value = result as Scenario[];
|
scenarios.value = result as Scenario[];
|
||||||
if (!scenarios.value.some((scenario) => scenario.id === form.scenarioId)) {
|
if (!scenarios.value.some((scenario) => scenario.id === form.scenarioId)) {
|
||||||
form.scenarioId = scenarios.value[0]?.id ?? 0;
|
form.scenarioId =
|
||||||
|
scenarios.value.find((scenario) => scenario.isCurrent)?.id ?? scenarios.value[0]?.id ?? null;
|
||||||
}
|
}
|
||||||
message.value = `${scenarios.value.length}개 시나리오를 확인했습니다.`;
|
message.value = `${scenarios.value.length}개 시나리오를 확인했습니다.`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
scenarios.value = [];
|
scenarios.value = [];
|
||||||
errorMessage.value = error instanceof Error ? error.message : '소스에서 시나리오를 읽지 못했습니다.';
|
errorMessage.value = error instanceof Error ? error.message : '소스에서 시나리오를 읽지 못했습니다.';
|
||||||
} finally {
|
} finally {
|
||||||
|
catalogAttempted.value = true;
|
||||||
catalogLoading.value = false;
|
catalogLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -405,15 +410,16 @@ const requestReset = async () => {
|
|||||||
if (!selectedProfileName.value || activeOperation.value) {
|
if (!selectedProfileName.value || activeOperation.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((form.sourceMode !== 'CURRENT' && !form.sourceRef.trim()) || !form.scenarioId) {
|
if ((form.sourceMode !== 'CURRENT' && !form.sourceRef.trim()) || form.scenarioId === null) {
|
||||||
errorMessage.value = '초기화 소스와 시나리오를 먼저 선택해주세요.';
|
errorMessage.value = '초기화 소스와 시나리오를 먼저 선택해주세요.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const scenarioId = form.scenarioId;
|
||||||
const sourceLabel =
|
const sourceLabel =
|
||||||
form.sourceMode === 'CURRENT' ? '현재 배포 버전' : form.sourceMode === 'BRANCH' ? '브랜치' : '커밋';
|
form.sourceMode === 'CURRENT' ? '현재 배포 버전' : form.sourceMode === 'BRANCH' ? '브랜치' : '커밋';
|
||||||
if (
|
if (
|
||||||
!window.confirm(
|
!window.confirm(
|
||||||
`${selectedProfileName.value}의 게임 DB를 초기화합니다.\n${sourceLabel}${form.sourceMode === 'CURRENT' ? '' : `: ${form.sourceRef}`}\n시나리오: ${form.scenarioId}`
|
`${selectedProfileName.value}의 게임 DB를 초기화합니다.\n${sourceLabel}${form.sourceMode === 'CURRENT' ? '' : `: ${form.sourceRef}`}\n시나리오: ${scenarioId}`
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -427,7 +433,7 @@ const requestReset = async () => {
|
|||||||
scheduledAt: toIso(form.scheduledAt),
|
scheduledAt: toIso(form.scheduledAt),
|
||||||
reason: form.reason.trim() || undefined,
|
reason: form.reason.trim() || undefined,
|
||||||
install: {
|
install: {
|
||||||
scenarioId: form.scenarioId,
|
scenarioId,
|
||||||
turnTermMinutes: form.turnTermMinutes,
|
turnTermMinutes: form.turnTermMinutes,
|
||||||
sync: form.sync,
|
sync: form.sync,
|
||||||
fiction: form.fiction,
|
fiction: form.fiction,
|
||||||
@@ -493,6 +499,18 @@ watch(selectedGatewayOperationId, (operationId) => {
|
|||||||
if (operationId && componentMounted) void pollGatewayReleaseLogs(operationId, releaseLogLoopGeneration);
|
if (operationId && componentMounted) void pollGatewayReleaseLogs(operationId, releaseLogLoopGeneration);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => form.sourceMode,
|
||||||
|
(sourceMode) => {
|
||||||
|
scenarios.value = [];
|
||||||
|
form.scenarioId = null;
|
||||||
|
catalogAttempted.value = false;
|
||||||
|
if (sourceMode === 'CURRENT' && componentMounted) {
|
||||||
|
void loadScenarios();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
componentMounted = true;
|
componentMounted = true;
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -601,8 +619,12 @@ onBeforeUnmount(() => {
|
|||||||
<p class="text-xs text-amber-200/80" data-testid="source-help">{{ sourceHelp }}</p>
|
<p class="text-xs text-amber-200/80" data-testid="source-help">{{ sourceHelp }}</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div v-if="form.sourceMode !== 'CURRENT'" class="grid gap-3 md:grid-cols-[1fr_auto]">
|
<div
|
||||||
|
v-if="form.sourceMode !== 'CURRENT' || mode === 'scenario'"
|
||||||
|
class="grid gap-3 md:grid-cols-[1fr_auto]"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
|
v-if="form.sourceMode !== 'CURRENT'"
|
||||||
v-model="form.sourceRef"
|
v-model="form.sourceRef"
|
||||||
class="rounded border border-zinc-700 bg-zinc-950 px-3 py-2 font-mono text-sm text-white"
|
class="rounded border border-zinc-700 bg-zinc-950 px-3 py-2 font-mono text-sm text-white"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
@@ -620,7 +642,7 @@ onBeforeUnmount(() => {
|
|||||||
data-testid="load-scenarios"
|
data-testid="load-scenarios"
|
||||||
@click="loadScenarios"
|
@click="loadScenarios"
|
||||||
>
|
>
|
||||||
시나리오 확인
|
{{ catalogLoading ? '확인 중…' : catalogAttempted ? '시나리오 재확인' : '시나리오 확인' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -632,8 +654,15 @@ onBeforeUnmount(() => {
|
|||||||
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-white"
|
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-white"
|
||||||
data-testid="scenario-select"
|
data-testid="scenario-select"
|
||||||
>
|
>
|
||||||
|
<option v-if="catalogLoading" :value="null" disabled>
|
||||||
|
시나리오를 불러오는 중입니다…
|
||||||
|
</option>
|
||||||
|
<option v-else-if="scenarios.length === 0" :value="null" disabled>
|
||||||
|
선택할 수 있는 시나리오가 없습니다.
|
||||||
|
</option>
|
||||||
<option v-for="scenario in scenarios" :key="scenario.id" :value="scenario.id">
|
<option v-for="scenario in scenarios" :key="scenario.id" :value="scenario.id">
|
||||||
{{ scenario.id }} · {{ scenario.title }} (NPC {{ scenario.npcCount }})
|
{{ scenario.id }} · {{ scenario.title }} (NPC {{ scenario.npcCount }})
|
||||||
|
{{ scenario.isCurrent ? '· 현재 시나리오' : '' }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -784,7 +813,7 @@ onBeforeUnmount(() => {
|
|||||||
v-else
|
v-else
|
||||||
type="submit"
|
type="submit"
|
||||||
class="w-full rounded bg-amber-500 px-4 py-3 font-bold text-black hover:bg-amber-400 disabled:cursor-not-allowed disabled:opacity-40"
|
class="w-full rounded bg-amber-500 px-4 py-3 font-bold text-black hover:bg-amber-400 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
:disabled="submitting || Boolean(activeOperation) || !form.scenarioId"
|
:disabled="submitting || Boolean(activeOperation) || form.scenarioId === null"
|
||||||
data-testid="request-reset"
|
data-testid="request-reset"
|
||||||
>
|
>
|
||||||
{{ form.scheduledAt ? '시나리오 초기화 예약' : '시나리오 초기화' }}
|
{{ form.scheduledAt ? '시나리오 초기화 예약' : '시나리오 초기화' }}
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ Gateway 관리자 콘솔은 `/gateway/admin`에서 시작합니다. 공개 로
|
|||||||
- 시나리오 초기화는 기본적으로 서버에 현재 게시된 commit을 사용하므로 Git
|
- 시나리오 초기화는 기본적으로 서버에 현재 게시된 commit을 사용하므로 Git
|
||||||
업데이트가 필요하지 않습니다. 새 branch/commit과 함께 초기화하려면 초기화
|
업데이트가 필요하지 않습니다. 새 branch/commit과 함께 초기화하려면 초기화
|
||||||
권한과 버전 배포 권한이 모두 필요합니다.
|
권한과 버전 배포 권한이 모두 필요합니다.
|
||||||
|
- 현재 배포 버전의 시나리오 catalog는 capability·operation polling batch와
|
||||||
|
분리된 요청으로 읽습니다. API가 profile의 현재 scenario를 표시하며 화면은
|
||||||
|
그 항목을 기본 선택합니다. scenario ID `0`도 유효한 값이고, 초기 요청이
|
||||||
|
실패하면 현재 버전 모드에서 다시 확인할 수 있습니다.
|
||||||
- Gateway 릴리스는 profile 작업과 다른 전역 `admin.releases.manage` 권한을
|
- Gateway 릴리스는 profile 작업과 다른 전역 `admin.releases.manage` 권한을
|
||||||
사용하며 외부 release-controller가 실행합니다. 선택한 릴리스 작업의 단계와
|
사용하며 외부 release-controller가 실행합니다. 선택한 릴리스 작업의 단계와
|
||||||
명령 출력을 관리자 화면이 long polling으로 이어 받아 표시하며, 완료된 이력의
|
명령 출력을 관리자 화면이 long polling으로 이어 받아 표시하며, 완료된 이력의
|
||||||
|
|||||||
Reference in New Issue
Block a user