fix: Gateway 화면에서 raw 프로필 ID 노출을 제거한다
불변 profileName은 라우팅과 저장 경계에 유지하고 사용자 출력은 공통 표시명을 사용한다. 기본 인스턴스 suffix를 숨기고 비기본 인스턴스만 사람이 읽을 수 있게 구분한다.
This commit is contained in:
@@ -851,7 +851,8 @@ onBeforeUnmount(() => {
|
||||
:key="profile.profileName"
|
||||
:value="profile.profileName"
|
||||
>
|
||||
{{ profile.profile }} · {{ profile.currentScenario ?? profile.profileName }}
|
||||
{{ profile.displayName ?? profile.profile }} ·
|
||||
{{ profile.currentScenario ?? '시나리오 미설정' }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="currentProfile" class="notification-profile-status">{{
|
||||
|
||||
@@ -134,6 +134,7 @@ type AdminAuditEvent = {
|
||||
targetType?: string;
|
||||
targetId?: string;
|
||||
profileName?: string;
|
||||
profileDisplayName?: string;
|
||||
action: string;
|
||||
outcome: 'STARTED' | 'SUCCEEDED' | 'FAILED';
|
||||
reason?: string;
|
||||
@@ -144,6 +145,9 @@ type AdminAuditEvent = {
|
||||
|
||||
type KakaoGracePolicy = {
|
||||
profileName: string;
|
||||
profile: string;
|
||||
instanceKey: string;
|
||||
displayName: string;
|
||||
requiresKakaoVerification: boolean;
|
||||
kakaoVerified: boolean;
|
||||
accessAllowed: boolean;
|
||||
@@ -185,6 +189,7 @@ type AdminProfile = {
|
||||
profileName: string;
|
||||
profile: string;
|
||||
instanceKey: string;
|
||||
displayName?: string;
|
||||
currentScenario: string | null;
|
||||
/** @deprecated Rollback-compatible mirror of currentScenario. */
|
||||
scenario: string;
|
||||
@@ -444,6 +449,14 @@ const visibleProfiles = computed(() =>
|
||||
props.profileName ? profiles.value.filter((profile) => profile.profileName === props.profileName) : profiles.value
|
||||
);
|
||||
|
||||
const adminProfileDisplayName = (profile: AdminProfile): string => {
|
||||
if (profile.displayName?.trim()) return profile.displayName.trim();
|
||||
const configuredName = profile.meta.korName;
|
||||
const baseName =
|
||||
typeof configuredName === 'string' && configuredName.trim() ? configuredName.trim() : profile.profile;
|
||||
return profile.instanceKey === 'default' ? baseName : `${baseName} [${profile.instanceKey}]`;
|
||||
};
|
||||
|
||||
const runtimeActionPending = (profile: AdminProfile): boolean => {
|
||||
return profile.runtimeActions.some((action) => action.status === 'REQUESTED' || action.status === 'PARTIAL');
|
||||
};
|
||||
@@ -566,7 +579,7 @@ const kakaoGraceStatus = ref('');
|
||||
const kakaoPolicies = ref<KakaoGracePolicy[]>([]);
|
||||
const specialAccessGrants = ref<SpecialAccountAccessGrant[]>([]);
|
||||
const specialAccessKind = ref<SpecialAccountAccessGrant['kind']>('RECOVERY');
|
||||
const specialAccessProfiles = ref('');
|
||||
const specialAccessProfiles = ref<string[]>([]);
|
||||
const specialAccessAllowsGeneralCreation = ref(true);
|
||||
const specialAccessExpiresAt = ref('');
|
||||
const specialAccessStatus = ref('');
|
||||
@@ -574,6 +587,38 @@ const userHistory = ref<AdminAuditEvent[]>([]);
|
||||
const globalAuditHistory = ref<AdminAuditEvent[]>([]);
|
||||
const globalAuditStatus = ref('');
|
||||
|
||||
const profileScopeLabel = (scope: string): string => {
|
||||
const exact = kakaoPolicies.value.find((policy) => policy.profileName === scope);
|
||||
if (exact) return exact.displayName;
|
||||
const base = kakaoPolicies.value.filter((policy) => policy.profile === scope);
|
||||
if (!base.length) return '삭제되었거나 접근할 수 없는 서버';
|
||||
const baseName = base[0]!.displayName.replace(/ \[[^\]]+\]$/, '');
|
||||
return base.length > 1 ? `${baseName} 전체 인스턴스` : baseName;
|
||||
};
|
||||
|
||||
const specialAccessScopeOptions = computed(() => {
|
||||
const grouped = new Map<string, KakaoGracePolicy[]>();
|
||||
for (const policy of kakaoPolicies.value) {
|
||||
const bucket = grouped.get(policy.profile) ?? [];
|
||||
bucket.push(policy);
|
||||
grouped.set(policy.profile, bucket);
|
||||
}
|
||||
return [...grouped.entries()].flatMap(([profile, entries]) => {
|
||||
if (entries.length === 1) return [{ value: profile, label: entries[0]!.displayName }];
|
||||
const baseName = entries[0]!.displayName.replace(/ \[[^\]]+\]$/, '');
|
||||
return [
|
||||
{ value: profile, label: `${baseName} 전체 인스턴스` },
|
||||
...entries.map((entry) => ({ value: entry.profileName, label: entry.displayName })),
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
const auditTargetLabel = (event: AdminAuditEvent): string => {
|
||||
if (event.profileDisplayName) return event.profileDisplayName;
|
||||
if (event.targetType === 'PROFILE' || event.profileName) return '삭제되었거나 접근할 수 없는 서버';
|
||||
return event.targetId ?? '';
|
||||
};
|
||||
|
||||
const { feedback: showFeedbackToast } = useToast();
|
||||
const actionFeedback = [
|
||||
noticeStatus,
|
||||
@@ -1059,7 +1104,7 @@ const applyCapabilitySelection = () => {
|
||||
const capability = capabilities.value.find((entry) => entry.permission === selectedCapability.value);
|
||||
if (!capability) return;
|
||||
if (capability.scope === 'PROFILE' && !capabilityProfile.value.trim()) {
|
||||
rolesStatus.value = 'Profile 범위를 입력하세요.';
|
||||
rolesStatus.value = '대상 서버를 선택하세요.';
|
||||
return;
|
||||
}
|
||||
rolesInput.value =
|
||||
@@ -1101,10 +1146,7 @@ const grantSpecialAccess = async () => {
|
||||
await adminClient.users.grantSpecialAccess.mutate({
|
||||
userId: userResult.value.id,
|
||||
kind: specialAccessKind.value,
|
||||
profiles: specialAccessProfiles.value
|
||||
.split(',')
|
||||
.map((profile) => profile.trim())
|
||||
.filter(Boolean),
|
||||
profiles: specialAccessProfiles.value,
|
||||
allowsGeneralCreation: specialAccessAllowsGeneralCreation.value,
|
||||
expiresAt: specialAccessExpiresAt.value
|
||||
? (serverDateTimeInputToIso(specialAccessExpiresAt.value) ?? null)
|
||||
@@ -1266,7 +1308,7 @@ const applyRestriction = async () => {
|
||||
const reason = requireUserActionReason();
|
||||
if (!reason) return;
|
||||
if (!restrictionProfile.value.trim()) {
|
||||
restrictionStatus.value = '서버 프로필명을 입력하세요.';
|
||||
restrictionStatus.value = '대상 서버를 선택하세요.';
|
||||
return;
|
||||
}
|
||||
const features = restrictionFeatures.value
|
||||
@@ -1301,7 +1343,7 @@ const clearRestriction = async () => {
|
||||
const reason = requireUserActionReason();
|
||||
if (!reason) return;
|
||||
if (!restrictionProfile.value.trim()) {
|
||||
restrictionStatus.value = '서버 프로필명을 입력하세요.';
|
||||
restrictionStatus.value = '대상 서버를 선택하세요.';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -1706,13 +1748,21 @@ onMounted(() => {
|
||||
{{ capability.label }} · {{ capability.risk }}
|
||||
</option>
|
||||
</select>
|
||||
<input
|
||||
<select
|
||||
v-model="capabilityProfile"
|
||||
type="text"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="Profile 범위 (예: che:default)"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
:disabled="!hasUser || !kakaoPolicies.length"
|
||||
aria-label="권한 대상 서버"
|
||||
>
|
||||
<option value="">대상 서버 선택</option>
|
||||
<option
|
||||
v-for="policy in kakaoPolicies"
|
||||
:key="policy.profileName"
|
||||
:value="policy.profileName"
|
||||
>
|
||||
{{ policy.displayName }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
class="bg-zinc-700 hover:bg-zinc-600 px-3 py-2 rounded text-sm"
|
||||
:disabled="!hasUser"
|
||||
@@ -1779,13 +1829,22 @@ onMounted(() => {
|
||||
:disabled="!hasUser"
|
||||
aria-label="특수 접근 만료 시각"
|
||||
/>
|
||||
<input
|
||||
v-model="specialAccessProfiles"
|
||||
type="text"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="che 또는 che:2 (쉼표 구분, 비우면 전체)"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<fieldset class="rounded border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-white">
|
||||
<legend class="px-1 text-xs text-zinc-400">허용 서버 (비우면 전체)</legend>
|
||||
<label
|
||||
v-for="option in specialAccessScopeOptions"
|
||||
:key="option.value"
|
||||
class="mr-4 inline-flex items-center gap-2"
|
||||
>
|
||||
<input
|
||||
v-model="specialAccessProfiles"
|
||||
type="checkbox"
|
||||
:value="option.value"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
{{ option.label }}
|
||||
</label>
|
||||
</fieldset>
|
||||
<label class="flex items-center gap-2 text-sm text-zinc-300 px-2">
|
||||
<input
|
||||
v-model="specialAccessAllowsGeneralCreation"
|
||||
@@ -1812,7 +1871,11 @@ onMounted(() => {
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<span class="font-semibold text-amber-200">
|
||||
{{ grant.kind }} ·
|
||||
{{ grant.profiles.length ? grant.profiles.join(', ') : '전체 profile' }}
|
||||
{{
|
||||
grant.profiles.length
|
||||
? grant.profiles.map(profileScopeLabel).join(', ')
|
||||
: '전체 서버'
|
||||
}}
|
||||
</span>
|
||||
<button
|
||||
v-if="!grant.revokedAt"
|
||||
@@ -1871,7 +1934,7 @@ onMounted(() => {
|
||||
<table class="w-full min-w-[620px] text-xs">
|
||||
<thead class="text-zinc-500">
|
||||
<tr>
|
||||
<th class="p-2 text-left">Profile</th>
|
||||
<th class="p-2 text-left">서버</th>
|
||||
<th>접근</th>
|
||||
<th>장수 생성</th>
|
||||
<th>기본 접근 유예</th>
|
||||
@@ -1885,7 +1948,7 @@ onMounted(() => {
|
||||
:key="policy.profileName"
|
||||
class="border-t border-zinc-800"
|
||||
>
|
||||
<td class="p-2">{{ policy.profileName }}</td>
|
||||
<td class="p-2">{{ policy.displayName }}</td>
|
||||
<td class="text-center">{{ policy.accessAllowed ? '허용' : '차단' }}</td>
|
||||
<td class="text-center">{{ policy.canCreateGeneral ? '허용' : '차단' }}</td>
|
||||
<td class="text-center">{{ policy.accessGraceDays }}일</td>
|
||||
@@ -1944,13 +2007,21 @@ onMounted(() => {
|
||||
>
|
||||
<h4 class="text-base font-semibold">서버별 기능 제재 (서버 시간 UTC+9)</h4>
|
||||
<div class="grid gap-2">
|
||||
<input
|
||||
<select
|
||||
v-model="restrictionProfile"
|
||||
type="text"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="profile:scenario"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
:disabled="!hasUser || !kakaoPolicies.length"
|
||||
aria-label="제재 대상 서버"
|
||||
>
|
||||
<option value="">대상 서버 선택</option>
|
||||
<option
|
||||
v-for="policy in kakaoPolicies"
|
||||
:key="policy.profileName"
|
||||
:value="policy.profileName"
|
||||
>
|
||||
{{ policy.displayName }}
|
||||
</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="restrictionFeatures"
|
||||
type="text"
|
||||
@@ -2115,7 +2186,7 @@ onMounted(() => {
|
||||
</div>
|
||||
<div class="text-zinc-400">
|
||||
{{ event.actorUsername }} · {{ event.targetType ?? '-' }}
|
||||
{{ event.targetId ?? event.profileName ?? '' }} · {{ event.reason ?? '사유 없음' }}
|
||||
{{ auditTargetLabel(event) }} · {{ event.reason ?? '사유 없음' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2180,6 +2251,7 @@ onMounted(() => {
|
||||
>
|
||||
<ServerProfileTabs
|
||||
:profile-name="profile.profileName"
|
||||
:profile-label="adminProfileDisplayName(profile)"
|
||||
active-tab="status"
|
||||
:can-deploy="hasCapability('admin.profiles.deploy', profile.profileName)"
|
||||
:can-reset="hasCapability('admin.scenarios.reset', profile.profileName)"
|
||||
@@ -2189,10 +2261,7 @@ onMounted(() => {
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2">
|
||||
<div>
|
||||
<div class="text-base font-semibold">
|
||||
{{ profile.meta.korName ?? profile.profile }}
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
서버 ID: {{ profile.profileName }} · 인스턴스: {{ profile.instanceKey }}
|
||||
{{ adminProfileDisplayName(profile) }}
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
현재 시나리오: {{ profile.currentScenario ?? '미설정' }}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch }
|
||||
import CompactHelp from '../components/CompactHelp.vue';
|
||||
import ServerProfileTabs from '../components/ServerProfileTabs.vue';
|
||||
import { useToast } from '../composables/useToast';
|
||||
import { loadAdminProfileNavigation, type AdminProfileNavigationItem } from '../composables/useAdminProfileNavigation';
|
||||
import AdminConsoleLayout from '../layouts/AdminConsoleLayout.vue';
|
||||
import {
|
||||
normalizeProfileResetDefaults,
|
||||
@@ -103,6 +104,28 @@ const gatewayReleaseLogConnection = ref<'idle' | 'connected' | 'reconnecting'>('
|
||||
const gatewayReleaseLogViewport = ref<HTMLElement>();
|
||||
const gatewayReleaseAvailable = ref(false);
|
||||
const selectedProfileName = computed(() => props.profileName ?? '');
|
||||
const profileIdentities = ref<AdminProfileNavigationItem[]>([]);
|
||||
const profileDisplayName = (profileName: string): string => {
|
||||
const profile = profileIdentities.value.find((candidate) => candidate.profileName === profileName);
|
||||
if (!profile) return '삭제되었거나 접근할 수 없는 서버';
|
||||
if (profile.displayName?.trim()) return profile.displayName.trim();
|
||||
const configuredName = profile.meta?.korName;
|
||||
const baseName =
|
||||
typeof configuredName === 'string' && configuredName.trim() ? configuredName.trim() : profile.profile;
|
||||
return profile.instanceKey === 'default' ? baseName : `${baseName} [${profile.instanceKey}]`;
|
||||
};
|
||||
const selectedProfileIdentityReady = computed(() =>
|
||||
profileIdentities.value.some((profile) => profile.profileName === selectedProfileName.value)
|
||||
);
|
||||
const selectedProfileDisplayName = computed(() => {
|
||||
if (!selectedProfileName.value) return '대상 서버';
|
||||
return selectedProfileIdentityReady.value ? profileDisplayName(selectedProfileName.value) : '대상 서버';
|
||||
});
|
||||
const cancellationConfirmation = computed(() => `${selectedProfileDisplayName.value} 게임 취소`);
|
||||
const displayOperationText = (value?: string): string => {
|
||||
if (!value || !selectedProfileName.value) return value ?? '';
|
||||
return value.replaceAll(selectedProfileName.value, selectedProfileDisplayName.value);
|
||||
};
|
||||
const capabilities = ref<Array<{ permission: string; scopes?: string[] }>>([]);
|
||||
const loading = ref(false);
|
||||
const catalogLoading = ref(false);
|
||||
@@ -205,7 +228,7 @@ const profileOperationLogEmptyMessage = computed(() => {
|
||||
return '오케스트레이터 로그를 기다리고 있습니다…';
|
||||
}
|
||||
if (operation.error) {
|
||||
return `이 작업에는 진행 로그가 기록되지 않았습니다. 작업 오류: ${operation.error}`;
|
||||
return `이 작업에는 진행 로그가 기록되지 않았습니다. 작업 오류: ${displayOperationText(operation.error)}`;
|
||||
}
|
||||
return '이 작업에는 진행 로그가 기록되지 않았습니다. 로그 기능 적용 전 작업일 수 있습니다.';
|
||||
});
|
||||
@@ -229,9 +252,9 @@ const hasCapability = (permission: string): boolean =>
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
if (props.mode === 'gateway') return 'Gateway 릴리스';
|
||||
if (props.mode === 'cancel') return `${props.profileName ?? ''} 게임 취소`;
|
||||
if (props.mode === 'scenario') return `${props.profileName ?? ''} 시나리오 초기화`;
|
||||
return `${props.profileName ?? ''} 버전 업데이트`;
|
||||
if (props.mode === 'cancel') return `${selectedProfileDisplayName.value} 게임 취소`;
|
||||
if (props.mode === 'scenario') return `${selectedProfileDisplayName.value} 시나리오 초기화`;
|
||||
return `${selectedProfileDisplayName.value} 버전 업데이트`;
|
||||
});
|
||||
|
||||
const pageDescription = computed(() => {
|
||||
@@ -345,6 +368,15 @@ const loadResetDefaults = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadProfileIdentities = async () => {
|
||||
if (props.mode === 'gateway') return;
|
||||
try {
|
||||
profileIdentities.value = await loadAdminProfileNavigation();
|
||||
} catch {
|
||||
profileIdentities.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const loadCapabilities = async () => {
|
||||
try {
|
||||
capabilities.value = (await adminClient.capabilities.list.query()) as typeof capabilities.value;
|
||||
@@ -531,6 +563,7 @@ const requestDeploy = async () => {
|
||||
clearStatus();
|
||||
if (
|
||||
!selectedProfileName.value ||
|
||||
!selectedProfileIdentityReady.value ||
|
||||
activeOperation.value ||
|
||||
!form.sourceRef.trim() ||
|
||||
form.sourceMode === 'CURRENT'
|
||||
@@ -539,7 +572,7 @@ const requestDeploy = async () => {
|
||||
}
|
||||
if (
|
||||
!window.confirm(
|
||||
`${selectedProfileName.value}의 인게임 DB를 유지하고 ${form.sourceRef.trim()} 버전으로 배포하시겠습니까?`
|
||||
`${selectedProfileDisplayName.value}의 인게임 DB를 유지하고 ${form.sourceRef.trim()} 버전으로 배포하시겠습니까?`
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@@ -682,7 +715,7 @@ const selectedAutorunOptions = (): ResetAutorunOption[] => {
|
||||
|
||||
const requestReset = async () => {
|
||||
clearStatus();
|
||||
if (!selectedProfileName.value || activeOperation.value) {
|
||||
if (!selectedProfileName.value || !selectedProfileIdentityReady.value || activeOperation.value) {
|
||||
return;
|
||||
}
|
||||
if ((form.sourceMode !== 'CURRENT' && !form.sourceRef.trim()) || form.scenarioId === null) {
|
||||
@@ -698,7 +731,7 @@ const requestReset = async () => {
|
||||
form.sourceMode === 'CURRENT' ? '서버 지정 버전' : form.sourceMode === 'BRANCH' ? '브랜치' : '커밋';
|
||||
if (
|
||||
!window.confirm(
|
||||
`${selectedProfileName.value}의 게임 DB를 초기화합니다.\n${sourceLabel}${form.sourceMode === 'CURRENT' ? '' : `: ${form.sourceRef}`}\n시나리오: ${scenarioId}${form.publishSchedule ? '\n예약 등록 즉시 로비에 오픈 일정을 공개합니다.' : ''}`
|
||||
`${selectedProfileDisplayName.value}의 게임 DB를 초기화합니다.\n${sourceLabel}${form.sourceMode === 'CURRENT' ? '' : `: ${form.sourceRef}`}\n시나리오: ${scenarioId}${form.publishSchedule ? '\n예약 등록 즉시 로비에 오픈 일정을 공개합니다.' : ''}`
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@@ -746,13 +779,13 @@ const requestReset = async () => {
|
||||
const requestGameCancellation = async () => {
|
||||
clearStatus();
|
||||
const profileName = selectedProfileName.value;
|
||||
if (!profileName || activeOperation.value) return;
|
||||
if (!profileName || !selectedProfileIdentityReady.value || activeOperation.value) return;
|
||||
if (cancellationForm.reason.trim().length < 5) {
|
||||
errorMessage.value = '취소 사유를 5자 이상 입력해주세요.';
|
||||
return;
|
||||
}
|
||||
if (cancellationForm.confirmation.trim() !== profileName) {
|
||||
errorMessage.value = `확인란에 ${profileName}을 정확히 입력해주세요.`;
|
||||
if (cancellationForm.confirmation.trim() !== cancellationConfirmation.value) {
|
||||
errorMessage.value = `확인란에 ${cancellationConfirmation.value}를 정확히 입력해주세요.`;
|
||||
return;
|
||||
}
|
||||
const historyText =
|
||||
@@ -760,7 +793,7 @@ const requestGameCancellation = async () => {
|
||||
const generalText = cancellationForm.generalMode === 'RETAIN' ? '장수 기록 보존' : '장수 기록 삭제';
|
||||
if (
|
||||
!window.confirm(
|
||||
`${profileName}의 진행 중 게임을 취소합니다.\n${historyText}\n${generalText}\n유산 획득분 ${cancellationForm.earnedPointRetentionPercent}% 보전\n취소 후 시나리오 초기화 전에는 재개할 수 없습니다.`
|
||||
`${selectedProfileDisplayName.value}의 진행 중 게임을 취소합니다.\n${historyText}\n${generalText}\n유산 획득분 ${cancellationForm.earnedPointRetentionPercent}% 보전\n취소 후 시나리오 초기화 전에는 재개할 수 없습니다.`
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@@ -853,6 +886,7 @@ onMounted(async () => {
|
||||
componentMounted = true;
|
||||
await Promise.all([
|
||||
loadCapabilities(),
|
||||
loadProfileIdentities(),
|
||||
loadState(),
|
||||
loadResetDefaults(),
|
||||
props.mode === 'scenario' ? loadScenarios() : Promise.resolve(),
|
||||
@@ -887,6 +921,7 @@ onBeforeUnmount(() => {
|
||||
<ServerProfileTabs
|
||||
v-if="mode !== 'gateway' && profileName"
|
||||
:profile-name="profileName"
|
||||
:profile-label="selectedProfileDisplayName"
|
||||
:active-tab="mode === 'scenario' ? 'scenario' : mode === 'cancel' ? 'cancel' : 'version'"
|
||||
:can-deploy="hasCapability('admin.profiles.deploy')"
|
||||
:can-reset="hasCapability('admin.scenarios.reset')"
|
||||
@@ -977,11 +1012,11 @@ onBeforeUnmount(() => {
|
||||
></textarea>
|
||||
</label>
|
||||
<label class="block text-sm text-zinc-300">
|
||||
확인을 위해 <strong>{{ selectedProfileName }}</strong> 입력
|
||||
확인을 위해 <strong>{{ cancellationConfirmation }}</strong> 입력
|
||||
<input
|
||||
v-model="cancellationForm.confirmation"
|
||||
class="mt-1 w-full rounded border border-red-800 bg-zinc-950 px-3 py-2 font-mono"
|
||||
:placeholder="selectedProfileName"
|
||||
:placeholder="cancellationConfirmation"
|
||||
data-testid="cancellation-confirmation"
|
||||
/>
|
||||
</label>
|
||||
@@ -990,9 +1025,10 @@ onBeforeUnmount(() => {
|
||||
class="w-full rounded bg-red-700 px-4 py-3 font-bold text-white hover:bg-red-600 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:disabled="
|
||||
submitting ||
|
||||
!selectedProfileIdentityReady ||
|
||||
Boolean(activeOperation) ||
|
||||
cancellationForm.reason.trim().length < 5 ||
|
||||
cancellationForm.confirmation.trim() !== selectedProfileName
|
||||
cancellationForm.confirmation.trim() !== cancellationConfirmation
|
||||
"
|
||||
data-testid="request-game-cancellation"
|
||||
>
|
||||
@@ -1454,7 +1490,12 @@ onBeforeUnmount(() => {
|
||||
v-if="mode === 'version'"
|
||||
type="submit"
|
||||
class="rounded bg-sky-700 px-4 py-3 font-bold text-white hover:bg-sky-600 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:disabled="submitting || Boolean(activeOperation) || !form.sourceRef.trim()"
|
||||
:disabled="
|
||||
submitting ||
|
||||
!selectedProfileIdentityReady ||
|
||||
Boolean(activeOperation) ||
|
||||
!form.sourceRef.trim()
|
||||
"
|
||||
data-testid="request-deploy"
|
||||
@click="requestDeploy"
|
||||
>
|
||||
@@ -1464,7 +1505,12 @@ onBeforeUnmount(() => {
|
||||
v-else
|
||||
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"
|
||||
:disabled="submitting || Boolean(activeOperation) || form.scenarioId === null"
|
||||
:disabled="
|
||||
submitting ||
|
||||
!selectedProfileIdentityReady ||
|
||||
Boolean(activeOperation) ||
|
||||
form.scenarioId === null
|
||||
"
|
||||
data-testid="request-reset"
|
||||
>
|
||||
{{ form.scheduledAt ? '시나리오 초기화 예약' : '시나리오 초기화' }}
|
||||
@@ -1783,7 +1829,9 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<span class="text-zinc-600">{{ formatLogTime(entry.createdAt) }}</span>
|
||||
<span class="ml-2 text-violet-300">[{{ entry.phase }}]</span>
|
||||
<span class="ml-2 whitespace-pre-wrap break-all">{{ entry.message }}</span>
|
||||
<span class="ml-2 whitespace-pre-wrap break-all">{{
|
||||
displayOperationText(entry.message)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1903,9 +1951,9 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<dl class="grid grid-cols-1 gap-x-6 gap-y-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<dt class="text-[11px] font-semibold text-zinc-500">서버 ID</dt>
|
||||
<dd class="mt-1 break-all font-mono text-xs text-zinc-300">
|
||||
{{ operation.profileName }}
|
||||
<dt class="text-[11px] font-semibold text-zinc-500">서버</dt>
|
||||
<dd class="mt-1 break-all text-xs text-zinc-300">
|
||||
{{ profileDisplayName(operation.profileName) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
@@ -1953,7 +2001,7 @@ onBeforeUnmount(() => {
|
||||
<div v-if="operation.error" class="sm:col-span-2">
|
||||
<dt class="text-[11px] font-semibold text-red-400">오류</dt>
|
||||
<dd class="mt-1 whitespace-pre-wrap break-all text-xs text-red-300">
|
||||
{{ operation.error }}
|
||||
{{ displayOperationText(operation.error) }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
Reference in New Issue
Block a user