feat(gateway): show action result toasts

This commit is contained in:
2026-08-11 12:02:27 +00:00
parent c737ee2cd1
commit 0cff7f681f
12 changed files with 388 additions and 25 deletions
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useToast } from '../composables/useToast';
import DefaultLayout from '../layouts/DefaultLayout.vue';
import { createGameTrpc } from '../utils/gameTrpc';
import { trpc } from '../utils/trpc';
@@ -22,6 +23,10 @@ const loading = ref(true);
const busy = ref(false);
const errorMessage = ref('');
const successMessage = ref('');
const { success: showSuccessToast, error: showErrorToast } = useToast();
watch(successMessage, (value) => value && showSuccessToast(value), { flush: 'sync' });
watch(errorMessage, (value) => value && showErrorToast(value), { flush: 'sync' });
const currentPassword = ref('');
const newPassword = ref('');
const newPasswordConfirm = ref('');
+47 -6
View File
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import ServerProfileTabs from '../components/ServerProfileTabs.vue';
import AdminConsoleLayout from '../layouts/AdminConsoleLayout.vue';
import { useToast } from '../composables/useToast';
import {
normalizeProfileResetDefaults,
type ProfileResetDefaults,
@@ -513,6 +514,45 @@ const userHistory = ref<AdminAuditEvent[]>([]);
const globalAuditHistory = ref<AdminAuditEvent[]>([]);
const globalAuditStatus = ref('');
const { feedback: showFeedbackToast } = useToast();
const actionFeedback = [
noticeStatus,
userError,
kakaoGraceStatus,
specialAccessStatus,
passwordStatus,
rolesStatus,
banStatus,
profileIconStatus,
restrictionStatus,
forceDeleteStatus,
];
watch(
actionFeedback,
(current, previous) => {
current.forEach((message, index) => {
if (message && message !== previous[index]) showFeedbackToast(message);
});
},
{ flush: 'sync' }
);
const setLocalAccountFeedback = (message: string): void => {
localAccountStatus.value = message;
showFeedbackToast(message);
};
watch(
profileActionStatus,
(current, previous) => {
Object.entries(current).forEach(([profileName, message]) => {
if (message && message !== previous[profileName]) showFeedbackToast(message);
});
},
{ flush: 'sync' }
);
const hasUser = computed(() => Boolean(userResult.value));
const loadLocalAccountStatus = async () => {
@@ -723,9 +763,10 @@ const updateProfileMeta = async (profileName: string) => {
);
}
} catch (error) {
const detail = error instanceof Error ? error.message : '';
profileActionStatus.value = {
...profileActionStatus.value,
[profileName]: '메타 저장 실패',
[profileName]: detail ? `메타 저장 실패: ${detail}` : '메타 저장 실패',
};
}
};
@@ -1183,14 +1224,14 @@ const createLocalAccount = async () => {
localAccountStatus.value = '';
localAccountResult.value = '';
if (!localAccountEnabled.value) {
localAccountStatus.value = 'ENV 설정이 비활성화 상태입니다.';
setLocalAccountFeedback('ENV 설정이 비활성화 상태입니다.');
return;
}
const username = localAccountForm.value.username.trim();
const password = localAccountForm.value.password.trim();
const displayName = localAccountForm.value.displayName.trim();
if (!username || !password) {
localAccountStatus.value = '아이디와 비밀번호를 입력하세요.';
setLocalAccountFeedback('아이디와 비밀번호를 입력하세요.');
return;
}
localAccountLoading.value = true;
@@ -1201,7 +1242,7 @@ const createLocalAccount = async () => {
displayName: displayName || undefined,
});
localAccountResult.value = `생성됨: ${result.user.username} (${result.user.id})`;
localAccountStatus.value = '로컬 계정 생성 완료';
setLocalAccountFeedback('로컬 계정 생성 완료');
localAccountForm.value = {
username: result.user.username,
password: '',
@@ -1211,7 +1252,7 @@ const createLocalAccount = async () => {
userLookupValue.value = result.user.username;
await Promise.all([lookupUser(), loadUserDirectory()]);
} catch (error) {
localAccountStatus.value = '로컬 계정 생성 실패';
setLocalAccountFeedback('로컬 계정 생성 실패');
} finally {
localAccountLoading.value = false;
}
+7 -3
View File
@@ -5,6 +5,7 @@ import type { inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from '@sammo-ts/gateway-api';
import DefaultLayout from '../layouts/DefaultLayout.vue';
import MapPreview from '../components/MapPreview.vue';
import { useToast } from '../composables/useToast';
import { trpc } from '../utils/trpc';
import { createGameTrpc } from '../utils/gameTrpc';
import type { GameRouter } from '../utils/gameTrpc';
@@ -34,6 +35,9 @@ const selectedMapProfileName = ref<string | null>(null);
const entryLoading = ref<Record<string, boolean>>({});
const logoutLoading = ref(false);
const logoutError = ref('');
const { error: showErrorToast } = useToast();
watch(logoutError, (value) => value && showErrorToast(value), { flush: 'sync' });
const canAccessAdmin = computed(
() =>
me.value?.roles.some(
@@ -207,7 +211,7 @@ const handleKakaoVerification = async (): Promise<void> => {
});
window.location.assign(result.authUrl);
} catch (error) {
alert(error instanceof Error ? error.message : '카카오 인증을 시작하지 못했습니다.');
showErrorToast(error instanceof Error ? error.message : '카카오 인증을 시작하지 못했습니다.');
}
};
@@ -245,13 +249,13 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
});
const url = resolveGameUrl(targetPath, issued.profile, issued.gameToken);
if (!url) {
alert('게임 프론트엔드 주소가 설정되지 않았습니다.');
showErrorToast('게임 프론트엔드 주소가 설정되지 않았습니다.');
return;
}
window.location.href = url;
} catch (e) {
console.error('Failed to issue game session', e);
alert(e instanceof Error ? e.message : '게임 서버 접속에 실패했습니다.');
showErrorToast(e instanceof Error ? e.message : '게임 서버 접속에 실패했습니다.');
} finally {
entryLoading.value[profile.profileName] = false;
}
@@ -2,6 +2,7 @@
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import ServerProfileTabs from '../components/ServerProfileTabs.vue';
import { useToast } from '../composables/useToast';
import AdminConsoleLayout from '../layouts/AdminConsoleLayout.vue';
import {
normalizeProfileResetDefaults,
@@ -95,6 +96,10 @@ const catalogAttempted = ref(false);
const submitting = ref(false);
const message = ref('');
const errorMessage = ref('');
const { success: showSuccessToast, error: showErrorToast } = useToast();
watch(message, (value) => value && showSuccessToast(value), { flush: 'sync' });
watch(errorMessage, (value) => value && showErrorToast(value), { flush: 'sync' });
const resetDefaultsSource = ref<'SYSTEM' | 'PROFILE'>('SYSTEM');
let pollTimer: ReturnType<typeof setInterval> | undefined;
let stateRequestInFlight = false;