feat: complete tournament API lifecycle
This commit is contained in:
@@ -38,6 +38,7 @@ const profile = (runtimeRunning: boolean) => ({
|
||||
profileName: 'che:2',
|
||||
apiRunning: runtimeRunning,
|
||||
daemonRunning: runtimeRunning,
|
||||
tournamentRunning: runtimeRunning,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -147,13 +148,17 @@ test('separates branch and commit semantics and submits a reset from the dedicat
|
||||
await expect(page.getByTestId('source-help')).toContainText('실제로 시작될 때');
|
||||
await expect(page.getByTestId('scenario-select')).toHaveValue('2');
|
||||
|
||||
const desktopGeometry = await page.getByTestId('server-operations-page').locator('section').first().evaluate((section) => {
|
||||
const children = Array.from(section.children).map((child) => {
|
||||
const rect = child.getBoundingClientRect();
|
||||
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
||||
const desktopGeometry = await page
|
||||
.getByTestId('server-operations-page')
|
||||
.locator('section')
|
||||
.first()
|
||||
.evaluate((section) => {
|
||||
const children = Array.from(section.children).map((child) => {
|
||||
const rect = child.getBoundingClientRect();
|
||||
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
||||
});
|
||||
return children;
|
||||
});
|
||||
return children;
|
||||
});
|
||||
expect(desktopGeometry).toHaveLength(2);
|
||||
expect(desktopGeometry[1]!.x).toBeGreaterThan(desktopGeometry[0]!.x);
|
||||
const sourceInput = page.getByTestId('source-ref');
|
||||
@@ -191,13 +196,17 @@ test('separates branch and commit semantics and submits a reset from the dedicat
|
||||
expect(JSON.stringify(resetRequest?.body)).toContain('"scenarioId":5');
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
const mobileGeometry = await page.getByTestId('server-operations-page').locator('section').first().evaluate((section) => {
|
||||
const children = Array.from(section.children).map((child) => {
|
||||
const rect = child.getBoundingClientRect();
|
||||
return { x: rect.x, y: rect.y, width: rect.width };
|
||||
const mobileGeometry = await page
|
||||
.getByTestId('server-operations-page')
|
||||
.locator('section')
|
||||
.first()
|
||||
.evaluate((section) => {
|
||||
const children = Array.from(section.children).map((child) => {
|
||||
const rect = child.getBoundingClientRect();
|
||||
return { x: rect.x, y: rect.y, width: rect.width };
|
||||
});
|
||||
return children;
|
||||
});
|
||||
return children;
|
||||
});
|
||||
expect(mobileGeometry[1]!.y).toBeGreaterThan(mobileGeometry[0]!.y);
|
||||
expect(mobileGeometry[0]!.width).toBeLessThanOrEqual(390);
|
||||
await page.screenshot({ path: testInfo.outputPath('mobile-operations.png'), fullPage: true });
|
||||
|
||||
@@ -70,6 +70,7 @@ type AdminProfile = {
|
||||
runtime: {
|
||||
apiRunning: boolean;
|
||||
daemonRunning: boolean;
|
||||
tournamentRunning: boolean;
|
||||
};
|
||||
buildCommitSha?: string;
|
||||
meta: Record<string, unknown>;
|
||||
@@ -121,15 +122,7 @@ type InstallFormState = {
|
||||
};
|
||||
|
||||
type AdminAction =
|
||||
| 'RESUME'
|
||||
| 'PAUSE'
|
||||
| 'STOP'
|
||||
| 'ACCELERATE'
|
||||
| 'DELAY'
|
||||
| 'RESET_NOW'
|
||||
| 'RESET_SCHEDULED'
|
||||
| 'OPEN_SURVEY'
|
||||
| 'SHUTDOWN';
|
||||
'RESUME' | 'PAUSE' | 'STOP' | 'ACCELERATE' | 'DELAY' | 'RESET_NOW' | 'RESET_SCHEDULED' | 'OPEN_SURVEY' | 'SHUTDOWN';
|
||||
|
||||
type AdminClient = {
|
||||
system: {
|
||||
@@ -436,8 +429,7 @@ const toLocalInputValue = (value: unknown): string => {
|
||||
const readNumber = (value: unknown, fallback: number): number =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
||||
|
||||
const readBoolean = (value: unknown, fallback: boolean): boolean =>
|
||||
typeof value === 'boolean' ? value : fallback;
|
||||
const readBoolean = (value: unknown, fallback: boolean): boolean => (typeof value === 'boolean' ? value : fallback);
|
||||
|
||||
const readString = (value: unknown, fallback: string): string => (typeof value === 'string' ? value : fallback);
|
||||
|
||||
@@ -1090,21 +1082,16 @@ onMounted(() => {
|
||||
<div class="text-xs text-zinc-400 mt-2">제재 상태</div>
|
||||
<pre class="text-[11px] text-zinc-400 bg-black/50 p-2 rounded whitespace-pre-wrap"
|
||||
>{{ JSON.stringify(userResult.sanctions, null, 2) }}
|
||||
</pre
|
||||
>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-base font-semibold">로컬 계정 생성</h4>
|
||||
<span class="text-xs text-zinc-500">
|
||||
ENV {{ localAccountEnabled ? 'ON' : 'OFF' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
카카오 OAuth 없이 로그인 가능한 계정을 생성합니다.
|
||||
<span class="text-xs text-zinc-500"> ENV {{ localAccountEnabled ? 'ON' : 'OFF' }} </span>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">카카오 OAuth 없이 로그인 가능한 계정을 생성합니다.</div>
|
||||
<div class="grid gap-2">
|
||||
<input
|
||||
v-model="localAccountForm.username"
|
||||
@@ -1365,7 +1352,8 @@ onMounted(() => {
|
||||
</div>
|
||||
<div class="text-xs text-zinc-400">
|
||||
상태: {{ profile.status }} / API: {{ profile.runtime.apiRunning ? 'ON' : 'OFF' }} /
|
||||
DAEMON: {{ profile.runtime.daemonRunning ? 'ON' : 'OFF' }}
|
||||
DAEMON: {{ profile.runtime.daemonRunning ? 'ON' : 'OFF' }} / TOURNAMENT:
|
||||
{{ profile.runtime.tournamentRunning ? 'ON' : 'OFF' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1504,7 +1492,9 @@ onMounted(() => {
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-semibold">설치/리셋</h4>
|
||||
<span class="text-xs text-zinc-500">{{ profileInstallStatus[profile.profileName] }}</span>
|
||||
<span class="text-xs text-zinc-500">{{
|
||||
profileInstallStatus[profile.profileName]
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="grid lg:grid-cols-2 gap-4">
|
||||
<div class="space-y-3">
|
||||
@@ -1525,7 +1515,9 @@ onMounted(() => {
|
||||
불러오기
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">비워두면 현재 저장소 기준으로 불러옵니다.</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
비워두면 현재 저장소 기준으로 불러옵니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
@@ -1535,16 +1527,28 @@ onMounted(() => {
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
:disabled="getScenarioLoading(profile.profileName)"
|
||||
>
|
||||
<option v-if="getScenarioLoading(profile.profileName)" disabled>불러오는 중...</option>
|
||||
<template v-for="(items, group) in getScenarioGroups(profile.profileName)" :key="group">
|
||||
<option v-if="getScenarioLoading(profile.profileName)" disabled>
|
||||
불러오는 중...
|
||||
</option>
|
||||
<template
|
||||
v-for="(items, group) in getScenarioGroups(profile.profileName)"
|
||||
:key="group"
|
||||
>
|
||||
<optgroup :label="group">
|
||||
<option v-for="scenario in items" :key="scenario.id" :value="scenario.id">
|
||||
<option
|
||||
v-for="scenario in items"
|
||||
:key="scenario.id"
|
||||
:value="scenario.id"
|
||||
>
|
||||
{{ scenario.title }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</template>
|
||||
</select>
|
||||
<div v-if="getScenarioStatus(profile.profileName)" class="text-xs text-red-400">
|
||||
<div
|
||||
v-if="getScenarioStatus(profile.profileName)"
|
||||
class="text-xs text-red-400"
|
||||
>
|
||||
{{ getScenarioStatus(profile.profileName) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1553,7 +1557,9 @@ onMounted(() => {
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-zinc-400">턴 시간(분)</label>
|
||||
<select
|
||||
v-model.number="profileInstalls[profile.profileName].turnTermMinutes"
|
||||
v-model.number="
|
||||
profileInstalls[profile.profileName].turnTermMinutes
|
||||
"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
>
|
||||
<option v-for="term in turnTermOptions" :key="term" :value="term">
|
||||
@@ -1664,7 +1670,9 @@ onMounted(() => {
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<label class="flex items-center gap-1 text-xs text-zinc-300">
|
||||
<input
|
||||
v-model.number="profileInstalls[profile.profileName].blockGeneralCreate"
|
||||
v-model.number="
|
||||
profileInstalls[profile.profileName].blockGeneralCreate
|
||||
"
|
||||
class="accent-yellow-500"
|
||||
type="radio"
|
||||
:value="0"
|
||||
@@ -1673,7 +1681,9 @@ onMounted(() => {
|
||||
</label>
|
||||
<label class="flex items-center gap-1 text-xs text-zinc-300">
|
||||
<input
|
||||
v-model.number="profileInstalls[profile.profileName].blockGeneralCreate"
|
||||
v-model.number="
|
||||
profileInstalls[profile.profileName].blockGeneralCreate
|
||||
"
|
||||
class="accent-yellow-500"
|
||||
type="radio"
|
||||
:value="2"
|
||||
@@ -1682,7 +1692,9 @@ onMounted(() => {
|
||||
</label>
|
||||
<label class="flex items-center gap-1 text-xs text-zinc-300">
|
||||
<input
|
||||
v-model.number="profileInstalls[profile.profileName].blockGeneralCreate"
|
||||
v-model.number="
|
||||
profileInstalls[profile.profileName].blockGeneralCreate
|
||||
"
|
||||
class="accent-yellow-500"
|
||||
type="radio"
|
||||
:value="1"
|
||||
@@ -1732,7 +1744,9 @@ onMounted(() => {
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<label class="flex items-center gap-1 text-xs text-zinc-300">
|
||||
<input
|
||||
v-model.number="profileInstalls[profile.profileName].showImgLevel"
|
||||
v-model.number="
|
||||
profileInstalls[profile.profileName].showImgLevel
|
||||
"
|
||||
class="accent-yellow-500"
|
||||
type="radio"
|
||||
:value="0"
|
||||
@@ -1741,7 +1755,9 @@ onMounted(() => {
|
||||
</label>
|
||||
<label class="flex items-center gap-1 text-xs text-zinc-300">
|
||||
<input
|
||||
v-model.number="profileInstalls[profile.profileName].showImgLevel"
|
||||
v-model.number="
|
||||
profileInstalls[profile.profileName].showImgLevel
|
||||
"
|
||||
class="accent-yellow-500"
|
||||
type="radio"
|
||||
:value="1"
|
||||
@@ -1750,7 +1766,9 @@ onMounted(() => {
|
||||
</label>
|
||||
<label class="flex items-center gap-1 text-xs text-zinc-300">
|
||||
<input
|
||||
v-model.number="profileInstalls[profile.profileName].showImgLevel"
|
||||
v-model.number="
|
||||
profileInstalls[profile.profileName].showImgLevel
|
||||
"
|
||||
class="accent-yellow-500"
|
||||
type="radio"
|
||||
:value="2"
|
||||
@@ -1759,7 +1777,9 @@ onMounted(() => {
|
||||
</label>
|
||||
<label class="flex items-center gap-1 text-xs text-zinc-300">
|
||||
<input
|
||||
v-model.number="profileInstalls[profile.profileName].showImgLevel"
|
||||
v-model.number="
|
||||
profileInstalls[profile.profileName].showImgLevel
|
||||
"
|
||||
class="accent-yellow-500"
|
||||
type="radio"
|
||||
:value="3"
|
||||
@@ -1802,7 +1822,11 @@ onMounted(() => {
|
||||
class="flex items-center gap-1 text-xs text-zinc-300"
|
||||
>
|
||||
<input
|
||||
v-model="profileInstalls[profile.profileName].autorunUserOptions[option.key]"
|
||||
v-model="
|
||||
profileInstalls[profile.profileName].autorunUserOptions[
|
||||
option.key
|
||||
]
|
||||
"
|
||||
class="accent-yellow-500"
|
||||
type="checkbox"
|
||||
/>
|
||||
@@ -1874,30 +1898,41 @@ onMounted(() => {
|
||||
설치 적용
|
||||
</button>
|
||||
|
||||
<div v-if="getScenarioPreview(profile.profileName)" class="bg-zinc-950 border border-zinc-800 rounded p-3 text-xs text-zinc-300 space-y-2">
|
||||
<div
|
||||
v-if="getScenarioPreview(profile.profileName)"
|
||||
class="bg-zinc-950 border border-zinc-800 rounded p-3 text-xs text-zinc-300 space-y-2"
|
||||
>
|
||||
<div class="font-semibold text-zinc-200">
|
||||
{{ getScenarioPreview(profile.profileName)?.title }}
|
||||
</div>
|
||||
<div>시작 연도: {{ getScenarioPreview(profile.profileName)?.year ?? '-' }}년</div>
|
||||
<div>
|
||||
시작 연도: {{ getScenarioPreview(profile.profileName)?.year ?? '-' }}년
|
||||
</div>
|
||||
<div>
|
||||
NPC: {{ getScenarioPreview(profile.profileName)?.npcCount }}명
|
||||
<span v-if="getScenarioPreview(profile.profileName)?.npcExCount">+{{ getScenarioPreview(profile.profileName)?.npcExCount }}명</span>
|
||||
<span v-if="getScenarioPreview(profile.profileName)?.npcExCount"
|
||||
>+{{ getScenarioPreview(profile.profileName)?.npcExCount }}명</span
|
||||
>
|
||||
<span v-if="getScenarioPreview(profile.profileName)?.npcNeutralCount">
|
||||
/ 중립 {{ getScenarioPreview(profile.profileName)?.npcNeutralCount }}명
|
||||
/ 중립
|
||||
{{ getScenarioPreview(profile.profileName)?.npcNeutralCount }}명
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-zinc-400">국가</div>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="nation in getScenarioPreview(profile.profileName)?.nations ?? []"
|
||||
v-for="nation in getScenarioPreview(profile.profileName)
|
||||
?.nations ?? []"
|
||||
:key="nation.id"
|
||||
class="text-[11px]"
|
||||
>
|
||||
<span :style="{ color: nation.color }">{{ nation.name }}</span>
|
||||
{{ nation.generals }}명
|
||||
<span v-if="nation.generalsEx">(+{{ nation.generalsEx }})</span>
|
||||
<span class="text-zinc-500">· {{ nation.cities.join(', ') }}</span>
|
||||
<span class="text-zinc-500"
|
||||
>· {{ nation.cities.join(', ') }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ type Profile = {
|
||||
buildWorkspace?: string;
|
||||
buildError?: string;
|
||||
lastError?: string;
|
||||
runtime: { apiRunning: boolean; daemonRunning: boolean };
|
||||
runtime: { apiRunning: boolean; daemonRunning: boolean; tournamentRunning: boolean };
|
||||
};
|
||||
|
||||
type Scenario = {
|
||||
@@ -192,9 +192,7 @@ const requestRuntime = async (action: 'START' | 'STOP') => {
|
||||
}
|
||||
};
|
||||
|
||||
const selectedAutorunOptions = (): Array<
|
||||
'develop' | 'warp' | 'recruit' | 'train' | 'battle'
|
||||
> => {
|
||||
const selectedAutorunOptions = (): Array<'develop' | 'warp' | 'recruit' | 'train' | 'battle'> => {
|
||||
const options: Array<'develop' | 'warp' | 'recruit' | 'train' | 'battle'> = [];
|
||||
if (form.autorunDevelop) options.push('develop');
|
||||
if (form.autorunWarp) options.push('warp');
|
||||
@@ -331,7 +329,10 @@ onBeforeUnmount(() => {
|
||||
<div v-if="errorMessage" class="rounded border border-red-800 bg-red-950/50 px-4 py-3 text-sm text-red-200">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<div v-if="message" class="rounded border border-emerald-800 bg-emerald-950/40 px-4 py-3 text-sm text-emerald-200">
|
||||
<div
|
||||
v-if="message"
|
||||
class="rounded border border-emerald-800 bg-emerald-950/40 px-4 py-3 text-sm text-emerald-200"
|
||||
>
|
||||
{{ message }}
|
||||
</div>
|
||||
|
||||
@@ -376,12 +377,27 @@ onBeforeUnmount(() => {
|
||||
{{ selectedProfile.runtime.daemonRunning ? 'RUNNING' : 'STOPPED' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded bg-zinc-950 p-3">
|
||||
<div class="text-xs text-zinc-500">Tournament worker</div>
|
||||
<div
|
||||
:class="
|
||||
selectedProfile.runtime.tournamentRunning ? 'text-emerald-400' : 'text-zinc-500'
|
||||
"
|
||||
>
|
||||
{{ selectedProfile.runtime.tournamentRunning ? 'RUNNING' : 'STOPPED' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedProfile" class="space-y-1 text-xs text-zinc-500">
|
||||
<div>현재 커밋: <span class="font-mono text-zinc-300">{{ shortSha(selectedProfile.buildCommitSha) }}</span></div>
|
||||
<div>
|
||||
현재 커밋:
|
||||
<span class="font-mono text-zinc-300">{{ shortSha(selectedProfile.buildCommitSha) }}</span>
|
||||
</div>
|
||||
<div class="break-all">worktree: {{ selectedProfile.buildWorkspace ?? '기본 workspace' }}</div>
|
||||
<div v-if="selectedProfile.buildError" class="text-red-400">{{ selectedProfile.buildError }}</div>
|
||||
<div v-if="selectedProfile.buildError" class="text-red-400">
|
||||
{{ selectedProfile.buildError }}
|
||||
</div>
|
||||
<div v-if="selectedProfile.lastError" class="text-red-400">{{ selectedProfile.lastError }}</div>
|
||||
</div>
|
||||
|
||||
@@ -405,10 +421,16 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="rounded-lg border border-zinc-800 bg-zinc-900 p-5 space-y-5" @submit.prevent="requestReset">
|
||||
<form
|
||||
class="rounded-lg border border-zinc-800 bg-zinc-900 p-5 space-y-5"
|
||||
@submit.prevent="requestReset"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">시나리오 초기화</h3>
|
||||
<span v-if="activeOperation" class="rounded-full bg-amber-500/15 px-3 py-1 text-xs text-amber-300">
|
||||
<span
|
||||
v-if="activeOperation"
|
||||
class="rounded-full bg-amber-500/15 px-3 py-1 text-xs text-amber-300"
|
||||
>
|
||||
{{ activeOperation.type }} · {{ activeOperation.status }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -417,11 +439,21 @@ onBeforeUnmount(() => {
|
||||
<legend class="text-xs text-zinc-400">소스 종류</legend>
|
||||
<div class="flex gap-5">
|
||||
<label class="flex items-center gap-2">
|
||||
<input v-model="form.sourceMode" type="radio" value="BRANCH" data-testid="source-branch" />
|
||||
<input
|
||||
v-model="form.sourceMode"
|
||||
type="radio"
|
||||
value="BRANCH"
|
||||
data-testid="source-branch"
|
||||
/>
|
||||
브랜치
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input v-model="form.sourceMode" type="radio" value="COMMIT" data-testid="source-commit" />
|
||||
<input
|
||||
v-model="form.sourceMode"
|
||||
type="radio"
|
||||
value="COMMIT"
|
||||
data-testid="source-commit"
|
||||
/>
|
||||
커밋
|
||||
</label>
|
||||
</div>
|
||||
@@ -432,7 +464,11 @@ onBeforeUnmount(() => {
|
||||
<input
|
||||
v-model="form.sourceRef"
|
||||
class="rounded border border-zinc-700 bg-zinc-950 px-3 py-2 font-mono text-sm text-white"
|
||||
:placeholder="form.sourceMode === 'BRANCH' ? '예: main 또는 release/season-12' : '예: 40자리 commit SHA'"
|
||||
:placeholder="
|
||||
form.sourceMode === 'BRANCH'
|
||||
? '예: main 또는 release/season-12'
|
||||
: '예: 40자리 commit SHA'
|
||||
"
|
||||
data-testid="source-ref"
|
||||
/>
|
||||
<button
|
||||
@@ -465,7 +501,11 @@ onBeforeUnmount(() => {
|
||||
v-model.number="form.turnTermMinutes"
|
||||
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-white"
|
||||
>
|
||||
<option v-for="minutes in [1, 2, 5, 10, 20, 30, 60, 120]" :key="minutes" :value="minutes">
|
||||
<option
|
||||
v-for="minutes in [1, 2, 5, 10, 20, 30, 60, 120]"
|
||||
:key="minutes"
|
||||
:value="minutes"
|
||||
>
|
||||
{{ minutes }}분
|
||||
</option>
|
||||
</select>
|
||||
@@ -475,39 +515,59 @@ onBeforeUnmount(() => {
|
||||
<details class="rounded border border-zinc-800 bg-zinc-950/50 p-4">
|
||||
<summary class="cursor-pointer text-sm font-semibold">고급 시나리오 옵션</summary>
|
||||
<div class="mt-4 grid gap-4 md:grid-cols-2 text-sm">
|
||||
<label>동기화
|
||||
<label
|
||||
>동기화
|
||||
<select v-model="form.sync" class="ml-2 rounded bg-zinc-900 px-2 py-1">
|
||||
<option :value="true">사용</option><option :value="false">미사용</option>
|
||||
<option :value="true">사용</option>
|
||||
<option :value="false">미사용</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>가상 장수
|
||||
<label
|
||||
>가상 장수
|
||||
<select v-model.number="form.fiction" class="ml-2 rounded bg-zinc-900 px-2 py-1">
|
||||
<option :value="1">허용</option><option :value="0">금지</option>
|
||||
<option :value="1">허용</option>
|
||||
<option :value="0">금지</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>연장
|
||||
<label
|
||||
>연장
|
||||
<select v-model="form.extend" class="ml-2 rounded bg-zinc-900 px-2 py-1">
|
||||
<option :value="true">사용</option><option :value="false">미사용</option>
|
||||
<option :value="true">사용</option>
|
||||
<option :value="false">미사용</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>가입 방식
|
||||
<label
|
||||
>가입 방식
|
||||
<select v-model="form.joinMode" class="ml-2 rounded bg-zinc-900 px-2 py-1">
|
||||
<option value="full">전체</option><option value="onlyRandom">랜덤만</option>
|
||||
<option value="full">전체</option>
|
||||
<option value="onlyRandom">랜덤만</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>장수 생성 제한
|
||||
<select v-model.number="form.blockGeneralCreate" class="ml-2 rounded bg-zinc-900 px-2 py-1">
|
||||
<option :value="0">없음</option><option :value="1">제한</option><option :value="2">차단</option>
|
||||
<label
|
||||
>장수 생성 제한
|
||||
<select
|
||||
v-model.number="form.blockGeneralCreate"
|
||||
class="ml-2 rounded bg-zinc-900 px-2 py-1"
|
||||
>
|
||||
<option :value="0">없음</option>
|
||||
<option :value="1">제한</option>
|
||||
<option :value="2">차단</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>NPC 모드
|
||||
<label
|
||||
>NPC 모드
|
||||
<select v-model.number="form.npcMode" class="ml-2 rounded bg-zinc-900 px-2 py-1">
|
||||
<option :value="0">기본</option><option :value="1">확장</option><option :value="2">전체</option>
|
||||
<option :value="0">기본</option>
|
||||
<option :value="1">확장</option>
|
||||
<option :value="2">전체</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>이미지 표시
|
||||
<label
|
||||
>이미지 표시
|
||||
<select v-model.number="form.showImgLevel" class="ml-2 rounded bg-zinc-900 px-2 py-1">
|
||||
<option v-for="level in [0, 1, 2, 3]" :key="level" :value="level">{{ level }}</option>
|
||||
<option v-for="level in [0, 1, 2, 3]" :key="level" :value="level">
|
||||
{{ level }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
@@ -536,14 +596,29 @@ onBeforeUnmount(() => {
|
||||
</details>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<label class="text-xs text-zinc-400">작업 예약
|
||||
<input v-model="form.scheduledAt" type="datetime-local" class="mt-1 w-full rounded border border-zinc-700 bg-zinc-950 px-3 py-2 text-white" />
|
||||
<label class="text-xs text-zinc-400"
|
||||
>작업 예약
|
||||
<input
|
||||
v-model="form.scheduledAt"
|
||||
type="datetime-local"
|
||||
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-950 px-3 py-2 text-white"
|
||||
/>
|
||||
</label>
|
||||
<label class="text-xs text-zinc-400">가오픈
|
||||
<input v-model="form.preopenAt" type="datetime-local" class="mt-1 w-full rounded border border-zinc-700 bg-zinc-950 px-3 py-2 text-white" />
|
||||
<label class="text-xs text-zinc-400"
|
||||
>가오픈
|
||||
<input
|
||||
v-model="form.preopenAt"
|
||||
type="datetime-local"
|
||||
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-950 px-3 py-2 text-white"
|
||||
/>
|
||||
</label>
|
||||
<label class="text-xs text-zinc-400">정식 오픈
|
||||
<input v-model="form.openAt" type="datetime-local" class="mt-1 w-full rounded border border-zinc-700 bg-zinc-950 px-3 py-2 text-white" />
|
||||
<label class="text-xs text-zinc-400"
|
||||
>정식 오픈
|
||||
<input
|
||||
v-model="form.openAt"
|
||||
type="datetime-local"
|
||||
class="mt-1 w-full rounded border border-zinc-700 bg-zinc-950 px-3 py-2 text-white"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -572,13 +647,23 @@ onBeforeUnmount(() => {
|
||||
<table class="w-full min-w-[920px] text-left text-sm" data-testid="operations-table">
|
||||
<thead class="border-b border-zinc-700 text-xs text-zinc-500">
|
||||
<tr>
|
||||
<th class="p-2">요청/예약</th><th class="p-2">프로필</th><th class="p-2">작업</th>
|
||||
<th class="p-2">상태</th><th class="p-2">소스</th><th class="p-2">해석 커밋</th>
|
||||
<th class="p-2">요청자/사유</th><th class="p-2">완료/오류</th><th class="p-2"></th>
|
||||
<th class="p-2">요청/예약</th>
|
||||
<th class="p-2">프로필</th>
|
||||
<th class="p-2">작업</th>
|
||||
<th class="p-2">상태</th>
|
||||
<th class="p-2">소스</th>
|
||||
<th class="p-2">해석 커밋</th>
|
||||
<th class="p-2">요청자/사유</th>
|
||||
<th class="p-2">완료/오류</th>
|
||||
<th class="p-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="operation in operations" :key="operation.id" class="border-b border-zinc-800 align-top">
|
||||
<tr
|
||||
v-for="operation in operations"
|
||||
:key="operation.id"
|
||||
class="border-b border-zinc-800 align-top"
|
||||
>
|
||||
<td class="p-2 text-xs">
|
||||
{{ formatTime(operation.createdAt) }}
|
||||
<div v-if="operation.scheduledAt" class="mt-1 text-amber-300">
|
||||
@@ -588,7 +673,9 @@ onBeforeUnmount(() => {
|
||||
<td class="p-2">{{ operation.profileName }}</td>
|
||||
<td class="p-2">{{ operation.type }}</td>
|
||||
<td class="p-2 font-semibold">{{ operation.status }}</td>
|
||||
<td class="p-2 font-mono text-xs">{{ operation.sourceMode ?? '-' }}<br />{{ operation.sourceRef ?? '' }}</td>
|
||||
<td class="p-2 font-mono text-xs">
|
||||
{{ operation.sourceMode ?? '-' }}<br />{{ operation.sourceRef ?? '' }}
|
||||
</td>
|
||||
<td class="p-2 font-mono text-xs">{{ shortSha(operation.resolvedCommitSha) }}</td>
|
||||
<td class="max-w-xs p-2 text-xs">
|
||||
<div class="font-mono">{{ operation.requestedBy }}</div>
|
||||
|
||||
Reference in New Issue
Block a user