feat: eslint 적용 및 관련 코드 일괄 수정
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>삼국지 모의전투 HiDCHe - Gateway</title>
|
||||
</head>
|
||||
<body class="bg-black text-white">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>삼국지 모의전투 HiDCHe - Gateway</title>
|
||||
</head>
|
||||
<body class="bg-black text-white">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx --fix --ignore-path .gitignore",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { RouterView } from 'vue-router';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
@import "tailwindcss";
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--color-sammo-dark: #1a1a1a;
|
||||
--color-sammo-gold: #ffd700;
|
||||
--color-sammo-dark: #1a1a1a;
|
||||
--color-sammo-gold: #ffd700;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-black text-gray-200;
|
||||
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, Roboto, 'Helvetica Neue', 'Segoe UI', 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', sans-serif;
|
||||
@apply bg-black text-gray-200;
|
||||
font-family:
|
||||
'Pretendard',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
system-ui,
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
'Segoe UI',
|
||||
'Apple SD Gothic Neo',
|
||||
'Noto Sans KR',
|
||||
'Malgun Gothic',
|
||||
'Apple Color Emoji',
|
||||
'Segoe UI Emoji',
|
||||
'Segoe UI Symbol',
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
Vendored
+3
-3
@@ -1,7 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
import type { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
@@ -1,45 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col bg-black text-gray-200">
|
||||
<!-- Header / Navigation -->
|
||||
<header class="bg-zinc-900 border-b border-zinc-800 py-2 px-4">
|
||||
<div class="max-w-6xl mx-auto flex justify-between items-center">
|
||||
<div class="flex items-center space-x-6">
|
||||
<h1 class="text-xl font-bold text-white">삼국지 모의전투 HiDCHe</h1>
|
||||
<nav class="hidden md:flex space-x-4 text-sm">
|
||||
<a href="#" class="hover:text-white">공지사항</a>
|
||||
<a href="#" class="hover:text-white">커뮤니티</a>
|
||||
<a href="#" class="hover:text-white">건의/제안/개발</a>
|
||||
<a href="#" class="hover:text-white">신고/문의</a>
|
||||
<a href="#" class="hover:text-white">자주 묻는 질문</a>
|
||||
<a href="#" class="hover:text-white">패치 내역</a>
|
||||
<a href="#" class="hover:text-white">Git Repo.</a>
|
||||
<a href="#" class="hover:text-white">위키</a>
|
||||
<RouterLink to="/admin" class="hover:text-white">관리자</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex space-x-4 text-sm">
|
||||
<a href="#" class="hover:text-white">공식 오픈 톡</a>
|
||||
<a href="#" class="hover:text-white">잡담 오픈 톡</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="min-h-screen flex flex-col bg-black text-gray-200">
|
||||
<!-- Header / Navigation -->
|
||||
<header class="bg-zinc-900 border-b border-zinc-800 py-2 px-4">
|
||||
<div class="max-w-6xl mx-auto flex justify-between items-center">
|
||||
<div class="flex items-center space-x-6">
|
||||
<h1 class="text-xl font-bold text-white">삼국지 모의전투 HiDCHe</h1>
|
||||
<nav class="hidden md:flex space-x-4 text-sm">
|
||||
<a href="#" class="hover:text-white">공지사항</a>
|
||||
<a href="#" class="hover:text-white">커뮤니티</a>
|
||||
<a href="#" class="hover:text-white">건의/제안/개발</a>
|
||||
<a href="#" class="hover:text-white">신고/문의</a>
|
||||
<a href="#" class="hover:text-white">자주 묻는 질문</a>
|
||||
<a href="#" class="hover:text-white">패치 내역</a>
|
||||
<a href="#" class="hover:text-white">Git Repo.</a>
|
||||
<a href="#" class="hover:text-white">위키</a>
|
||||
<RouterLink to="/admin" class="hover:text-white">관리자</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex space-x-4 text-sm">
|
||||
<a href="#" class="hover:text-white">공식 오픈 톡</a>
|
||||
<a href="#" class="hover:text-white">잡담 오픈 톡</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-grow">
|
||||
<slot />
|
||||
</main>
|
||||
<!-- Main Content -->
|
||||
<main class="flex-grow">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-zinc-900 border-t border-zinc-800 py-6 px-4 text-center text-xs text-zinc-500">
|
||||
<div class="space-x-4 mb-2">
|
||||
<a href="#" class="hover:text-zinc-300">개인정보처리방침</a>
|
||||
<a href="#" class="hover:text-zinc-300">이용약관</a>
|
||||
</div>
|
||||
<p>© 2023 • HideD</p>
|
||||
<p class="mt-1">크롬, 엣지, 파이어폭스에 최적화되어있습니다.</p>
|
||||
</footer>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<footer class="bg-zinc-900 border-t border-zinc-800 py-6 px-4 text-center text-xs text-zinc-500">
|
||||
<div class="space-x-4 mb-2">
|
||||
<a href="#" class="hover:text-zinc-300">개인정보처리방침</a>
|
||||
<a href="#" class="hover:text-zinc-300">이용약관</a>
|
||||
</div>
|
||||
<p>© 2023 • HideD</p>
|
||||
<p class="mt-1">크롬, 엣지, 파이어폭스에 최적화되어있습니다.</p>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,24 +4,24 @@ import LobbyView from '../views/LobbyView.vue';
|
||||
import AdminView from '../views/AdminView.vue';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: '/lobby',
|
||||
name: 'lobby',
|
||||
component: LobbyView,
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'admin',
|
||||
component: AdminView,
|
||||
},
|
||||
],
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: '/lobby',
|
||||
name: 'lobby',
|
||||
component: LobbyView,
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'admin',
|
||||
component: AdminView,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -2,20 +2,20 @@ import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export interface UserInfo {
|
||||
id: string;
|
||||
username: string;
|
||||
displayName: string;
|
||||
roles: string[];
|
||||
id: string;
|
||||
username: string;
|
||||
displayName: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<UserInfo | null>(null);
|
||||
const isLoggedIn = ref(false);
|
||||
const user = ref<UserInfo | null>(null);
|
||||
const isLoggedIn = ref(false);
|
||||
|
||||
function setUser(userData: UserInfo | null) {
|
||||
user.value = userData;
|
||||
isLoggedIn.value = !!userData;
|
||||
}
|
||||
function setUser(userData: UserInfo | null) {
|
||||
user.value = userData;
|
||||
isLoggedIn.value = !!userData;
|
||||
}
|
||||
|
||||
return { user, isLoggedIn, setUser };
|
||||
return { user, isLoggedIn, setUser };
|
||||
});
|
||||
|
||||
@@ -4,11 +4,11 @@ import type { appRouter } from '../../../game-api/src/router';
|
||||
export type GameRouter = typeof appRouter;
|
||||
|
||||
export const createGameTrpc = (port: number) => {
|
||||
return createTRPCProxyClient<GameRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: `http://localhost:${port}/api/trpc`, // 실제 환경에서는 도메인/경로 조정 필요
|
||||
}),
|
||||
],
|
||||
});
|
||||
return createTRPCProxyClient<GameRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: `http://localhost:${port}/api/trpc`, // 실제 환경에서는 도메인/경로 조정 필요
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,20 +2,20 @@ import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
|
||||
import type { AppRouter } from '@sammo-ts/gateway-api';
|
||||
|
||||
const getSessionToken = (): string | null => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
return window.localStorage.getItem('sammo-session-token');
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
return window.localStorage.getItem('sammo-session-token');
|
||||
};
|
||||
|
||||
export const trpc = createTRPCProxyClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: '/api/trpc', // 실제 환경에 맞게 조정 필요
|
||||
headers() {
|
||||
const token = getSessionToken();
|
||||
return token ? { 'x-session-token': token } : {};
|
||||
},
|
||||
}),
|
||||
],
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: '/api/trpc', // 실제 환경에 맞게 조정 필요
|
||||
headers() {
|
||||
const token = getSessionToken();
|
||||
return token ? { 'x-session-token': token } : {};
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -4,140 +4,153 @@ import DefaultLayout from '../layouts/DefaultLayout.vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
|
||||
type AdminUserSanctions = {
|
||||
bannedUntil?: string;
|
||||
mutedUntil?: string;
|
||||
suspendedUntil?: string;
|
||||
warningCount?: number;
|
||||
flags?: string[];
|
||||
notes?: string;
|
||||
profileIconResetAt?: string;
|
||||
serverRestrictions?: Record<string, {
|
||||
blockedFeatures?: string[];
|
||||
until?: string;
|
||||
reason?: string;
|
||||
bannedUntil?: string;
|
||||
mutedUntil?: string;
|
||||
suspendedUntil?: string;
|
||||
warningCount?: number;
|
||||
flags?: string[];
|
||||
notes?: string;
|
||||
}>;
|
||||
profileIconResetAt?: string;
|
||||
serverRestrictions?: Record<
|
||||
string,
|
||||
{
|
||||
blockedFeatures?: string[];
|
||||
until?: string;
|
||||
reason?: string;
|
||||
notes?: string;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
type AdminSanctionsPatch = {
|
||||
bannedUntil?: string | null;
|
||||
mutedUntil?: string | null;
|
||||
suspendedUntil?: string | null;
|
||||
warningCount?: number | null;
|
||||
flags?: string[] | null;
|
||||
notes?: string | null;
|
||||
profileIconResetAt?: string | null;
|
||||
serverRestrictions?: Record<string, {
|
||||
blockedFeatures?: string[];
|
||||
until?: string | null;
|
||||
reason?: string | null;
|
||||
bannedUntil?: string | null;
|
||||
mutedUntil?: string | null;
|
||||
suspendedUntil?: string | null;
|
||||
warningCount?: number | null;
|
||||
flags?: string[] | null;
|
||||
notes?: string | null;
|
||||
} | null> | null;
|
||||
profileIconResetAt?: string | null;
|
||||
serverRestrictions?: Record<
|
||||
string,
|
||||
{
|
||||
blockedFeatures?: string[];
|
||||
until?: string | null;
|
||||
reason?: string | null;
|
||||
notes?: string | null;
|
||||
} | null
|
||||
> | null;
|
||||
};
|
||||
|
||||
type AdminUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
displayName: string;
|
||||
roles: string[];
|
||||
sanctions: AdminUserSanctions;
|
||||
oauthType: string;
|
||||
oauthId?: string;
|
||||
email?: string;
|
||||
createdAt: string;
|
||||
id: string;
|
||||
username: string;
|
||||
displayName: string;
|
||||
roles: string[];
|
||||
sanctions: AdminUserSanctions;
|
||||
oauthType: string;
|
||||
oauthId?: string;
|
||||
email?: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
type AdminProfile = {
|
||||
profileName: string;
|
||||
profile: string;
|
||||
scenario: string;
|
||||
status: string;
|
||||
apiPort: number;
|
||||
runtime: {
|
||||
apiRunning: boolean;
|
||||
daemonRunning: boolean;
|
||||
};
|
||||
buildCommitSha?: string;
|
||||
meta: Record<string, unknown>;
|
||||
profileName: string;
|
||||
profile: string;
|
||||
scenario: string;
|
||||
status: string;
|
||||
apiPort: number;
|
||||
runtime: {
|
||||
apiRunning: boolean;
|
||||
daemonRunning: boolean;
|
||||
};
|
||||
buildCommitSha?: string;
|
||||
meta: Record<string, unknown>;
|
||||
};
|
||||
|
||||
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: {
|
||||
getNotice: {
|
||||
query: () => Promise<{ notice: string }>;
|
||||
};
|
||||
setNotice: {
|
||||
mutate: (input: { notice: string }) => Promise<{ notice: string }>;
|
||||
};
|
||||
};
|
||||
users: {
|
||||
lookup: {
|
||||
query: (input: { id?: string; username?: string; email?: string }) => Promise<AdminUser | null>;
|
||||
};
|
||||
resetPassword: {
|
||||
mutate: (input: { userId: string; newPassword?: string }) => Promise<{ password: string }>;
|
||||
};
|
||||
updateRoles: {
|
||||
mutate: (input: { userId: string; roles: string[]; mode?: 'set' | 'grant' | 'revoke' }) => Promise<{ roles: string[] }>;
|
||||
};
|
||||
updateSanctions: {
|
||||
mutate: (input: { userId: string; patch: AdminSanctionsPatch }) => Promise<{ sanctions: AdminUserSanctions }>;
|
||||
};
|
||||
setServerRestriction: {
|
||||
mutate: (input: {
|
||||
userId: string;
|
||||
profile: string;
|
||||
restriction: {
|
||||
blockedFeatures?: string[];
|
||||
until?: string | null;
|
||||
reason?: string | null;
|
||||
notes?: string | null;
|
||||
} | null;
|
||||
}) => Promise<{ sanctions: AdminUserSanctions }>;
|
||||
};
|
||||
resetProfileIcon: {
|
||||
mutate: (input: { userId: string }) => Promise<{ profileIconResetAt?: string }>;
|
||||
};
|
||||
forceDelete: {
|
||||
mutate: (input: { userId: string }) => Promise<{ ok: boolean }>;
|
||||
};
|
||||
};
|
||||
profiles: {
|
||||
list: {
|
||||
query: () => Promise<AdminProfile[]>;
|
||||
};
|
||||
updateMeta: {
|
||||
mutate: (input: {
|
||||
profileName: string;
|
||||
patch: {
|
||||
korName?: string | null;
|
||||
color?: string | null;
|
||||
inGameNotice?: string | null;
|
||||
profileImageUrl?: string | null;
|
||||
system: {
|
||||
getNotice: {
|
||||
query: () => Promise<{ notice: string }>;
|
||||
};
|
||||
setNotice: {
|
||||
mutate: (input: { notice: string }) => Promise<{ notice: string }>;
|
||||
};
|
||||
}) => Promise<AdminProfile | null>;
|
||||
};
|
||||
requestAction: {
|
||||
mutate: (input: {
|
||||
profileName: string;
|
||||
action: AdminAction;
|
||||
durationMinutes?: number;
|
||||
scheduledAt?: string;
|
||||
reason?: string;
|
||||
}) => Promise<{ ok: boolean }>;
|
||||
users: {
|
||||
lookup: {
|
||||
query: (input: { id?: string; username?: string; email?: string }) => Promise<AdminUser | null>;
|
||||
};
|
||||
resetPassword: {
|
||||
mutate: (input: { userId: string; newPassword?: string }) => Promise<{ password: string }>;
|
||||
};
|
||||
updateRoles: {
|
||||
mutate: (input: {
|
||||
userId: string;
|
||||
roles: string[];
|
||||
mode?: 'set' | 'grant' | 'revoke';
|
||||
}) => Promise<{ roles: string[] }>;
|
||||
};
|
||||
updateSanctions: {
|
||||
mutate: (input: {
|
||||
userId: string;
|
||||
patch: AdminSanctionsPatch;
|
||||
}) => Promise<{ sanctions: AdminUserSanctions }>;
|
||||
};
|
||||
setServerRestriction: {
|
||||
mutate: (input: {
|
||||
userId: string;
|
||||
profile: string;
|
||||
restriction: {
|
||||
blockedFeatures?: string[];
|
||||
until?: string | null;
|
||||
reason?: string | null;
|
||||
notes?: string | null;
|
||||
} | null;
|
||||
}) => Promise<{ sanctions: AdminUserSanctions }>;
|
||||
};
|
||||
resetProfileIcon: {
|
||||
mutate: (input: { userId: string }) => Promise<{ profileIconResetAt?: string }>;
|
||||
};
|
||||
forceDelete: {
|
||||
mutate: (input: { userId: string }) => Promise<{ ok: boolean }>;
|
||||
};
|
||||
};
|
||||
profiles: {
|
||||
list: {
|
||||
query: () => Promise<AdminProfile[]>;
|
||||
};
|
||||
updateMeta: {
|
||||
mutate: (input: {
|
||||
profileName: string;
|
||||
patch: {
|
||||
korName?: string | null;
|
||||
color?: string | null;
|
||||
inGameNotice?: string | null;
|
||||
profileImageUrl?: string | null;
|
||||
};
|
||||
}) => Promise<AdminProfile | null>;
|
||||
};
|
||||
requestAction: {
|
||||
mutate: (input: {
|
||||
profileName: string;
|
||||
action: AdminAction;
|
||||
durationMinutes?: number;
|
||||
scheduledAt?: string;
|
||||
reason?: string;
|
||||
}) => Promise<{ ok: boolean }>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const adminClient = trpc.admin as unknown as AdminClient;
|
||||
@@ -146,19 +159,19 @@ const sessionToken = ref('');
|
||||
const sessionTokenStatus = ref('');
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionToken.value = window.localStorage.getItem('sammo-session-token') ?? '';
|
||||
sessionToken.value = window.localStorage.getItem('sammo-session-token') ?? '';
|
||||
}
|
||||
|
||||
const saveSessionToken = () => {
|
||||
const value = sessionToken.value.trim();
|
||||
if (typeof window !== 'undefined') {
|
||||
if (value) {
|
||||
window.localStorage.setItem('sammo-session-token', value);
|
||||
} else {
|
||||
window.localStorage.removeItem('sammo-session-token');
|
||||
const value = sessionToken.value.trim();
|
||||
if (typeof window !== 'undefined') {
|
||||
if (value) {
|
||||
window.localStorage.setItem('sammo-session-token', value);
|
||||
} else {
|
||||
window.localStorage.removeItem('sammo-session-token');
|
||||
}
|
||||
}
|
||||
}
|
||||
sessionTokenStatus.value = value ? '저장됨' : '삭제됨';
|
||||
sessionTokenStatus.value = value ? '저장됨' : '삭제됨';
|
||||
};
|
||||
|
||||
const noticeDraft = ref('');
|
||||
@@ -167,17 +180,27 @@ const noticeLoading = ref(false);
|
||||
|
||||
const profiles = ref<AdminProfile[]>([]);
|
||||
const profilesLoading = ref(false);
|
||||
const profileEdits = ref<Record<string, {
|
||||
korName: string;
|
||||
color: string;
|
||||
inGameNotice: string;
|
||||
profileImageUrl: string;
|
||||
}>>({});
|
||||
const profileActions = ref<Record<string, {
|
||||
durationMinutes: string;
|
||||
scheduledAt: string;
|
||||
reason: string;
|
||||
}>>({});
|
||||
const profileEdits = ref<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
korName: string;
|
||||
color: string;
|
||||
inGameNotice: string;
|
||||
profileImageUrl: string;
|
||||
}
|
||||
>
|
||||
>({});
|
||||
const profileActions = ref<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
durationMinutes: string;
|
||||
scheduledAt: string;
|
||||
reason: string;
|
||||
}
|
||||
>
|
||||
>({});
|
||||
const profileActionStatus = ref<Record<string, string>>({});
|
||||
|
||||
const userLookupMode = ref<'username' | 'id' | 'email'>('username');
|
||||
@@ -212,774 +235,768 @@ const forceDeleteStatus = ref('');
|
||||
const hasUser = computed(() => Boolean(userResult.value));
|
||||
|
||||
const loadNotice = async () => {
|
||||
noticeLoading.value = true;
|
||||
try {
|
||||
const result = await adminClient.system.getNotice.query();
|
||||
noticeDraft.value = result.notice;
|
||||
} catch (error) {
|
||||
noticeStatus.value = '공지 불러오기 실패';
|
||||
} finally {
|
||||
noticeLoading.value = false;
|
||||
}
|
||||
noticeLoading.value = true;
|
||||
try {
|
||||
const result = await adminClient.system.getNotice.query();
|
||||
noticeDraft.value = result.notice;
|
||||
} catch (error) {
|
||||
noticeStatus.value = '공지 불러오기 실패';
|
||||
} finally {
|
||||
noticeLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveNotice = async () => {
|
||||
noticeLoading.value = true;
|
||||
noticeStatus.value = '';
|
||||
try {
|
||||
const result = await adminClient.system.setNotice.mutate({
|
||||
notice: noticeDraft.value,
|
||||
});
|
||||
noticeDraft.value = result.notice;
|
||||
noticeStatus.value = '저장 완료';
|
||||
} catch (error) {
|
||||
noticeStatus.value = '저장 실패';
|
||||
} finally {
|
||||
noticeLoading.value = false;
|
||||
}
|
||||
noticeLoading.value = true;
|
||||
noticeStatus.value = '';
|
||||
try {
|
||||
const result = await adminClient.system.setNotice.mutate({
|
||||
notice: noticeDraft.value,
|
||||
});
|
||||
noticeDraft.value = result.notice;
|
||||
noticeStatus.value = '저장 완료';
|
||||
} catch (error) {
|
||||
noticeStatus.value = '저장 실패';
|
||||
} finally {
|
||||
noticeLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const ensureProfileBuffers = (profile: AdminProfile) => {
|
||||
if (!profileEdits.value[profile.profileName]) {
|
||||
const meta = (profile.meta ?? {}) as Record<string, unknown>;
|
||||
profileEdits.value[profile.profileName] = {
|
||||
korName: String(meta.korName ?? profile.profile),
|
||||
color: String(meta.color ?? '#ffffff'),
|
||||
inGameNotice: String(meta.inGameNotice ?? ''),
|
||||
profileImageUrl: String(meta.profileImageUrl ?? ''),
|
||||
};
|
||||
}
|
||||
if (!profileActions.value[profile.profileName]) {
|
||||
profileActions.value[profile.profileName] = {
|
||||
durationMinutes: '',
|
||||
scheduledAt: '',
|
||||
reason: '',
|
||||
};
|
||||
}
|
||||
if (!profileEdits.value[profile.profileName]) {
|
||||
const meta = (profile.meta ?? {}) as Record<string, unknown>;
|
||||
profileEdits.value[profile.profileName] = {
|
||||
korName: String(meta.korName ?? profile.profile),
|
||||
color: String(meta.color ?? '#ffffff'),
|
||||
inGameNotice: String(meta.inGameNotice ?? ''),
|
||||
profileImageUrl: String(meta.profileImageUrl ?? ''),
|
||||
};
|
||||
}
|
||||
if (!profileActions.value[profile.profileName]) {
|
||||
profileActions.value[profile.profileName] = {
|
||||
durationMinutes: '',
|
||||
scheduledAt: '',
|
||||
reason: '',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const loadProfiles = async () => {
|
||||
profilesLoading.value = true;
|
||||
try {
|
||||
const result = await adminClient.profiles.list.query();
|
||||
result.forEach(ensureProfileBuffers);
|
||||
profiles.value = result;
|
||||
} catch (error) {
|
||||
profileActionStatus.value = {
|
||||
...profileActionStatus.value,
|
||||
global: '프로필 목록을 불러오지 못했습니다.',
|
||||
};
|
||||
} finally {
|
||||
profilesLoading.value = false;
|
||||
}
|
||||
profilesLoading.value = true;
|
||||
try {
|
||||
const result = await adminClient.profiles.list.query();
|
||||
result.forEach(ensureProfileBuffers);
|
||||
profiles.value = result;
|
||||
} catch (error) {
|
||||
profileActionStatus.value = {
|
||||
...profileActionStatus.value,
|
||||
global: '프로필 목록을 불러오지 못했습니다.',
|
||||
};
|
||||
} finally {
|
||||
profilesLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateProfileMeta = async (profileName: string) => {
|
||||
const edit = profileEdits.value[profileName];
|
||||
if (!edit) {
|
||||
return;
|
||||
}
|
||||
const patch = {
|
||||
korName: edit.korName.trim() || null,
|
||||
color: edit.color.trim() || null,
|
||||
inGameNotice: edit.inGameNotice.trim() || null,
|
||||
profileImageUrl: edit.profileImageUrl.trim() || null,
|
||||
};
|
||||
try {
|
||||
const updated = await adminClient.profiles.updateMeta.mutate({
|
||||
profileName,
|
||||
patch,
|
||||
});
|
||||
profileActionStatus.value = {
|
||||
...profileActionStatus.value,
|
||||
[profileName]: updated ? '메타 저장 완료' : '메타 저장 실패',
|
||||
};
|
||||
if (updated) {
|
||||
profiles.value = profiles.value.map((item) =>
|
||||
item.profileName === profileName ? { ...item, ...updated } : item
|
||||
);
|
||||
const edit = profileEdits.value[profileName];
|
||||
if (!edit) {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
profileActionStatus.value = {
|
||||
...profileActionStatus.value,
|
||||
[profileName]: '메타 저장 실패',
|
||||
const patch = {
|
||||
korName: edit.korName.trim() || null,
|
||||
color: edit.color.trim() || null,
|
||||
inGameNotice: edit.inGameNotice.trim() || null,
|
||||
profileImageUrl: edit.profileImageUrl.trim() || null,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const updated = await adminClient.profiles.updateMeta.mutate({
|
||||
profileName,
|
||||
patch,
|
||||
});
|
||||
profileActionStatus.value = {
|
||||
...profileActionStatus.value,
|
||||
[profileName]: updated ? '메타 저장 완료' : '메타 저장 실패',
|
||||
};
|
||||
if (updated) {
|
||||
profiles.value = profiles.value.map((item) =>
|
||||
item.profileName === profileName ? { ...item, ...updated } : item
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
profileActionStatus.value = {
|
||||
...profileActionStatus.value,
|
||||
[profileName]: '메타 저장 실패',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const requestProfileAction = async (profileName: string, action: AdminAction) => {
|
||||
const actionState = profileActions.value[profileName];
|
||||
const durationMinutes = actionState?.durationMinutes
|
||||
? Number(actionState.durationMinutes)
|
||||
: undefined;
|
||||
const durationValue =
|
||||
durationMinutes && durationMinutes > 0 ? durationMinutes : undefined;
|
||||
const scheduledAt = actionState?.scheduledAt
|
||||
? new Date(actionState.scheduledAt).toISOString()
|
||||
: undefined;
|
||||
const reason = actionState?.reason.trim() || undefined;
|
||||
try {
|
||||
await adminClient.profiles.requestAction.mutate({
|
||||
profileName,
|
||||
action,
|
||||
durationMinutes: durationValue,
|
||||
scheduledAt,
|
||||
reason,
|
||||
});
|
||||
profileActionStatus.value = {
|
||||
...profileActionStatus.value,
|
||||
[profileName]: `요청 완료: ${action}`,
|
||||
};
|
||||
} catch (error) {
|
||||
profileActionStatus.value = {
|
||||
...profileActionStatus.value,
|
||||
[profileName]: `요청 실패: ${action}`,
|
||||
};
|
||||
}
|
||||
const actionState = profileActions.value[profileName];
|
||||
const durationMinutes = actionState?.durationMinutes ? Number(actionState.durationMinutes) : undefined;
|
||||
const durationValue = durationMinutes && durationMinutes > 0 ? durationMinutes : undefined;
|
||||
const scheduledAt = actionState?.scheduledAt ? new Date(actionState.scheduledAt).toISOString() : undefined;
|
||||
const reason = actionState?.reason.trim() || undefined;
|
||||
try {
|
||||
await adminClient.profiles.requestAction.mutate({
|
||||
profileName,
|
||||
action,
|
||||
durationMinutes: durationValue,
|
||||
scheduledAt,
|
||||
reason,
|
||||
});
|
||||
profileActionStatus.value = {
|
||||
...profileActionStatus.value,
|
||||
[profileName]: `요청 완료: ${action}`,
|
||||
};
|
||||
} catch (error) {
|
||||
profileActionStatus.value = {
|
||||
...profileActionStatus.value,
|
||||
[profileName]: `요청 실패: ${action}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const lookupUser = async () => {
|
||||
userLoading.value = true;
|
||||
userError.value = '';
|
||||
passwordStatus.value = '';
|
||||
rolesStatus.value = '';
|
||||
banStatus.value = '';
|
||||
restrictionStatus.value = '';
|
||||
profileIconStatus.value = '';
|
||||
forceDeleteStatus.value = '';
|
||||
try {
|
||||
const payload =
|
||||
userLookupMode.value === 'id'
|
||||
? { id: userLookupValue.value.trim() }
|
||||
: userLookupMode.value === 'email'
|
||||
? { email: userLookupValue.value.trim() }
|
||||
: { username: userLookupValue.value.trim() };
|
||||
const result = await adminClient.users.lookup.query(payload);
|
||||
if (!result) {
|
||||
userResult.value = null;
|
||||
userError.value = '사용자를 찾을 수 없습니다.';
|
||||
return;
|
||||
userLoading.value = true;
|
||||
userError.value = '';
|
||||
passwordStatus.value = '';
|
||||
rolesStatus.value = '';
|
||||
banStatus.value = '';
|
||||
restrictionStatus.value = '';
|
||||
profileIconStatus.value = '';
|
||||
forceDeleteStatus.value = '';
|
||||
try {
|
||||
const payload =
|
||||
userLookupMode.value === 'id'
|
||||
? { id: userLookupValue.value.trim() }
|
||||
: userLookupMode.value === 'email'
|
||||
? { email: userLookupValue.value.trim() }
|
||||
: { username: userLookupValue.value.trim() };
|
||||
const result = await adminClient.users.lookup.query(payload);
|
||||
if (!result) {
|
||||
userResult.value = null;
|
||||
userError.value = '사용자를 찾을 수 없습니다.';
|
||||
return;
|
||||
}
|
||||
userResult.value = result;
|
||||
} catch (error) {
|
||||
userError.value = '조회 실패';
|
||||
} finally {
|
||||
userLoading.value = false;
|
||||
}
|
||||
userResult.value = result;
|
||||
} catch (error) {
|
||||
userError.value = '조회 실패';
|
||||
} finally {
|
||||
userLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resetUserPassword = async () => {
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
passwordStatus.value = '';
|
||||
passwordResult.value = '';
|
||||
try {
|
||||
const result = await adminClient.users.resetPassword.mutate({
|
||||
userId: userResult.value.id,
|
||||
newPassword: passwordInput.value.trim() || undefined,
|
||||
});
|
||||
passwordResult.value = result.password;
|
||||
passwordStatus.value = '초기화 완료';
|
||||
passwordInput.value = '';
|
||||
} catch (error) {
|
||||
passwordStatus.value = '초기화 실패';
|
||||
}
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
passwordStatus.value = '';
|
||||
passwordResult.value = '';
|
||||
try {
|
||||
const result = await adminClient.users.resetPassword.mutate({
|
||||
userId: userResult.value.id,
|
||||
newPassword: passwordInput.value.trim() || undefined,
|
||||
});
|
||||
passwordResult.value = result.password;
|
||||
passwordStatus.value = '초기화 완료';
|
||||
passwordInput.value = '';
|
||||
} catch (error) {
|
||||
passwordStatus.value = '초기화 실패';
|
||||
}
|
||||
};
|
||||
|
||||
const updateUserRoles = async () => {
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
const roles = rolesInput.value
|
||||
.split(',')
|
||||
.map((role) => role.trim())
|
||||
.filter(Boolean);
|
||||
if (!roles.length) {
|
||||
rolesStatus.value = '역할을 입력하세요.';
|
||||
return;
|
||||
}
|
||||
rolesStatus.value = '';
|
||||
try {
|
||||
const result = await adminClient.users.updateRoles.mutate({
|
||||
userId: userResult.value.id,
|
||||
roles,
|
||||
mode: rolesMode.value,
|
||||
});
|
||||
userResult.value = { ...userResult.value, roles: result.roles };
|
||||
rolesStatus.value = '권한 업데이트 완료';
|
||||
} catch (error) {
|
||||
rolesStatus.value = '권한 업데이트 실패';
|
||||
}
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
const roles = rolesInput.value
|
||||
.split(',')
|
||||
.map((role) => role.trim())
|
||||
.filter(Boolean);
|
||||
if (!roles.length) {
|
||||
rolesStatus.value = '역할을 입력하세요.';
|
||||
return;
|
||||
}
|
||||
rolesStatus.value = '';
|
||||
try {
|
||||
const result = await adminClient.users.updateRoles.mutate({
|
||||
userId: userResult.value.id,
|
||||
roles,
|
||||
mode: rolesMode.value,
|
||||
});
|
||||
userResult.value = { ...userResult.value, roles: result.roles };
|
||||
rolesStatus.value = '권한 업데이트 완료';
|
||||
} catch (error) {
|
||||
rolesStatus.value = '권한 업데이트 실패';
|
||||
}
|
||||
};
|
||||
|
||||
const applyBan = async () => {
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
const until = banUntil.value ? new Date(banUntil.value).toISOString() : null;
|
||||
const patch = {
|
||||
bannedUntil: until,
|
||||
notes: banReason.value.trim() || undefined,
|
||||
};
|
||||
try {
|
||||
const result = await adminClient.users.updateSanctions.mutate({
|
||||
userId: userResult.value.id,
|
||||
patch,
|
||||
});
|
||||
userResult.value = { ...userResult.value, sanctions: result.sanctions };
|
||||
banStatus.value = '차단 설정 완료';
|
||||
} catch (error) {
|
||||
banStatus.value = '차단 설정 실패';
|
||||
}
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
const until = banUntil.value ? new Date(banUntil.value).toISOString() : null;
|
||||
const patch = {
|
||||
bannedUntil: until,
|
||||
notes: banReason.value.trim() || undefined,
|
||||
};
|
||||
try {
|
||||
const result = await adminClient.users.updateSanctions.mutate({
|
||||
userId: userResult.value.id,
|
||||
patch,
|
||||
});
|
||||
userResult.value = { ...userResult.value, sanctions: result.sanctions };
|
||||
banStatus.value = '차단 설정 완료';
|
||||
} catch (error) {
|
||||
banStatus.value = '차단 설정 실패';
|
||||
}
|
||||
};
|
||||
|
||||
const clearBan = async () => {
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await adminClient.users.updateSanctions.mutate({
|
||||
userId: userResult.value.id,
|
||||
patch: { bannedUntil: null },
|
||||
});
|
||||
userResult.value = { ...userResult.value, sanctions: result.sanctions };
|
||||
banStatus.value = '차단 해제 완료';
|
||||
} catch (error) {
|
||||
banStatus.value = '차단 해제 실패';
|
||||
}
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await adminClient.users.updateSanctions.mutate({
|
||||
userId: userResult.value.id,
|
||||
patch: { bannedUntil: null },
|
||||
});
|
||||
userResult.value = { ...userResult.value, sanctions: result.sanctions };
|
||||
banStatus.value = '차단 해제 완료';
|
||||
} catch (error) {
|
||||
banStatus.value = '차단 해제 실패';
|
||||
}
|
||||
};
|
||||
|
||||
const resetProfileIcon = async () => {
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await adminClient.users.resetProfileIcon.mutate({
|
||||
userId: userResult.value.id,
|
||||
});
|
||||
userResult.value = {
|
||||
...userResult.value,
|
||||
sanctions: {
|
||||
...userResult.value.sanctions,
|
||||
profileIconResetAt: result.profileIconResetAt,
|
||||
},
|
||||
};
|
||||
profileIconStatus.value = '아이콘 초기화 요청 완료';
|
||||
} catch (error) {
|
||||
profileIconStatus.value = '아이콘 초기화 실패';
|
||||
}
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await adminClient.users.resetProfileIcon.mutate({
|
||||
userId: userResult.value.id,
|
||||
});
|
||||
userResult.value = {
|
||||
...userResult.value,
|
||||
sanctions: {
|
||||
...userResult.value.sanctions,
|
||||
profileIconResetAt: result.profileIconResetAt,
|
||||
},
|
||||
};
|
||||
profileIconStatus.value = '아이콘 초기화 요청 완료';
|
||||
} catch (error) {
|
||||
profileIconStatus.value = '아이콘 초기화 실패';
|
||||
}
|
||||
};
|
||||
|
||||
const applyRestriction = async () => {
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
if (!restrictionProfile.value.trim()) {
|
||||
restrictionStatus.value = '서버 프로필명을 입력하세요.';
|
||||
return;
|
||||
}
|
||||
const features = restrictionFeatures.value
|
||||
.split(',')
|
||||
.map((feature) => feature.trim())
|
||||
.filter(Boolean);
|
||||
const restriction = {
|
||||
blockedFeatures: features.length ? features : undefined,
|
||||
until: restrictionUntil.value ? new Date(restrictionUntil.value).toISOString() : undefined,
|
||||
reason: restrictionReason.value.trim() || undefined,
|
||||
notes: restrictionNotes.value.trim() || undefined,
|
||||
};
|
||||
try {
|
||||
const result = await adminClient.users.setServerRestriction.mutate({
|
||||
userId: userResult.value.id,
|
||||
profile: restrictionProfile.value.trim(),
|
||||
restriction,
|
||||
});
|
||||
userResult.value = { ...userResult.value, sanctions: result.sanctions };
|
||||
restrictionStatus.value = '서버 제재 적용 완료';
|
||||
} catch (error) {
|
||||
restrictionStatus.value = '서버 제재 적용 실패';
|
||||
}
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
if (!restrictionProfile.value.trim()) {
|
||||
restrictionStatus.value = '서버 프로필명을 입력하세요.';
|
||||
return;
|
||||
}
|
||||
const features = restrictionFeatures.value
|
||||
.split(',')
|
||||
.map((feature) => feature.trim())
|
||||
.filter(Boolean);
|
||||
const restriction = {
|
||||
blockedFeatures: features.length ? features : undefined,
|
||||
until: restrictionUntil.value ? new Date(restrictionUntil.value).toISOString() : undefined,
|
||||
reason: restrictionReason.value.trim() || undefined,
|
||||
notes: restrictionNotes.value.trim() || undefined,
|
||||
};
|
||||
try {
|
||||
const result = await adminClient.users.setServerRestriction.mutate({
|
||||
userId: userResult.value.id,
|
||||
profile: restrictionProfile.value.trim(),
|
||||
restriction,
|
||||
});
|
||||
userResult.value = { ...userResult.value, sanctions: result.sanctions };
|
||||
restrictionStatus.value = '서버 제재 적용 완료';
|
||||
} catch (error) {
|
||||
restrictionStatus.value = '서버 제재 적용 실패';
|
||||
}
|
||||
};
|
||||
|
||||
const clearRestriction = async () => {
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
if (!restrictionProfile.value.trim()) {
|
||||
restrictionStatus.value = '서버 프로필명을 입력하세요.';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await adminClient.users.setServerRestriction.mutate({
|
||||
userId: userResult.value.id,
|
||||
profile: restrictionProfile.value.trim(),
|
||||
restriction: null,
|
||||
});
|
||||
userResult.value = { ...userResult.value, sanctions: result.sanctions };
|
||||
restrictionStatus.value = '서버 제재 해제 완료';
|
||||
} catch (error) {
|
||||
restrictionStatus.value = '서버 제재 해제 실패';
|
||||
}
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
if (!restrictionProfile.value.trim()) {
|
||||
restrictionStatus.value = '서버 프로필명을 입력하세요.';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await adminClient.users.setServerRestriction.mutate({
|
||||
userId: userResult.value.id,
|
||||
profile: restrictionProfile.value.trim(),
|
||||
restriction: null,
|
||||
});
|
||||
userResult.value = { ...userResult.value, sanctions: result.sanctions };
|
||||
restrictionStatus.value = '서버 제재 해제 완료';
|
||||
} catch (error) {
|
||||
restrictionStatus.value = '서버 제재 해제 실패';
|
||||
}
|
||||
};
|
||||
|
||||
const forceDeleteUser = async () => {
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
const confirmed = window.confirm('정말로 강제 탈퇴 처리하시겠습니까?');
|
||||
if (!confirmed) {
|
||||
return;
|
||||
if (!userResult.value) {
|
||||
return;
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
const confirmed = window.confirm('정말로 강제 탈퇴 처리하시겠습니까?');
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
await adminClient.users.forceDelete.mutate({ userId: userResult.value.id });
|
||||
userResult.value = null;
|
||||
forceDeleteStatus.value = '강제 탈퇴 완료';
|
||||
} catch (error) {
|
||||
forceDeleteStatus.value = '강제 탈퇴 실패';
|
||||
}
|
||||
}
|
||||
try {
|
||||
await adminClient.users.forceDelete.mutate({ userId: userResult.value.id });
|
||||
userResult.value = null;
|
||||
forceDeleteStatus.value = '강제 탈퇴 완료';
|
||||
} catch (error) {
|
||||
forceDeleteStatus.value = '강제 탈퇴 실패';
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
void loadNotice();
|
||||
void loadProfiles();
|
||||
void loadNotice();
|
||||
void loadProfiles();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefaultLayout>
|
||||
<div class="max-w-6xl mx-auto px-4 py-10 space-y-10">
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-3xl font-bold text-white">관리자 콘솔</h2>
|
||||
<p class="text-sm text-zinc-400">
|
||||
유저 관리와 서버 운영 제어를 위한 관리자 전용 대시보드입니다.
|
||||
</p>
|
||||
</div>
|
||||
<DefaultLayout>
|
||||
<div class="max-w-6xl mx-auto px-4 py-10 space-y-10">
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-3xl font-bold text-white">관리자 콘솔</h2>
|
||||
<p class="text-sm text-zinc-400">유저 관리와 서버 운영 제어를 위한 관리자 전용 대시보드입니다.</p>
|
||||
</div>
|
||||
|
||||
<section class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">관리자 세션 토큰</h3>
|
||||
<span class="text-xs text-zinc-500">{{ sessionTokenStatus }}</span>
|
||||
<section class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">관리자 세션 토큰</h3>
|
||||
<span class="text-xs text-zinc-500">{{ sessionTokenStatus }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-3">
|
||||
<input
|
||||
v-model="sessionToken"
|
||||
type="password"
|
||||
class="flex-1 bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-yellow-500"
|
||||
placeholder="세션 토큰 입력"
|
||||
/>
|
||||
<button
|
||||
class="bg-yellow-600 hover:bg-yellow-500 text-black font-semibold px-4 py-2 rounded"
|
||||
@click="saveSessionToken"
|
||||
>
|
||||
저장
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid lg:grid-cols-2 gap-8">
|
||||
<section class="space-y-6">
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<h3 class="text-lg font-semibold">유저 관리</h3>
|
||||
<form class="space-y-3" @submit.prevent="lookupUser">
|
||||
<div class="flex flex-col md:flex-row gap-2">
|
||||
<select
|
||||
v-model="userLookupMode"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
>
|
||||
<option value="username">계정명</option>
|
||||
<option value="email">이메일</option>
|
||||
<option value="id">UUID</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="userLookupValue"
|
||||
type="text"
|
||||
class="flex-1 bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-yellow-500"
|
||||
placeholder="검색 값 입력"
|
||||
/>
|
||||
<button
|
||||
class="bg-blue-700 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="userLoading"
|
||||
>
|
||||
조회
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="userError" class="text-xs text-red-400">{{ userError }}</div>
|
||||
</form>
|
||||
|
||||
<div v-if="userResult" class="bg-zinc-950 border border-zinc-800 rounded p-4 space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-sm font-semibold">{{ userResult.username }}</div>
|
||||
<div class="text-xs text-zinc-500">{{ userResult.id }}</div>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-400">표시명: {{ userResult.displayName }}</div>
|
||||
<div class="text-xs text-zinc-400">권한: {{ userResult.roles.join(', ') || '-' }}</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
OAuth: {{ userResult.oauthType }} {{ userResult.email ?? '' }}
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">가입일: {{ userResult.createdAt }}</div>
|
||||
<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
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<h4 class="text-base font-semibold">비밀번호 리셋</h4>
|
||||
<div class="flex flex-col md:flex-row gap-2">
|
||||
<input
|
||||
v-model="passwordInput"
|
||||
type="text"
|
||||
class="flex-1 bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="직접 지정 시 입력"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<button
|
||||
class="bg-emerald-600 hover:bg-emerald-500 text-black font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="resetUserPassword"
|
||||
>
|
||||
초기화
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-400">임시 비밀번호: {{ passwordResult || '-' }}</div>
|
||||
<div class="text-xs text-zinc-500">{{ passwordStatus }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<h4 class="text-base font-semibold">특수 권한 부여</h4>
|
||||
<div class="flex flex-col md:flex-row gap-2">
|
||||
<select
|
||||
v-model="rolesMode"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
:disabled="!hasUser"
|
||||
>
|
||||
<option value="grant">추가</option>
|
||||
<option value="revoke">제거</option>
|
||||
<option value="set">덮어쓰기</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="rolesInput"
|
||||
type="text"
|
||||
class="flex-1 bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="admin, moderator 등"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<button
|
||||
class="bg-indigo-600 hover:bg-indigo-500 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="updateUserRoles"
|
||||
>
|
||||
적용
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">{{ rolesStatus }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<h4 class="text-base font-semibold">유저 차단</h4>
|
||||
<div class="flex flex-col gap-2">
|
||||
<input
|
||||
v-model="banUntil"
|
||||
type="datetime-local"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<input
|
||||
v-model="banReason"
|
||||
type="text"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="사유"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="bg-red-600 hover:bg-red-500 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="applyBan"
|
||||
>
|
||||
차단 설정
|
||||
</button>
|
||||
<button
|
||||
class="bg-zinc-700 hover:bg-zinc-600 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="clearBan"
|
||||
>
|
||||
차단 해제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">{{ banStatus }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<h4 class="text-base font-semibold">서버별 기능 제재</h4>
|
||||
<div class="grid gap-2">
|
||||
<input
|
||||
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"
|
||||
/>
|
||||
<input
|
||||
v-model="restrictionFeatures"
|
||||
type="text"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="login, message 등"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<input
|
||||
v-model="restrictionUntil"
|
||||
type="datetime-local"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<input
|
||||
v-model="restrictionReason"
|
||||
type="text"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="사유"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<input
|
||||
v-model="restrictionNotes"
|
||||
type="text"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="메모"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="bg-amber-600 hover:bg-amber-500 text-black font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="applyRestriction"
|
||||
>
|
||||
제재 적용
|
||||
</button>
|
||||
<button
|
||||
class="bg-zinc-700 hover:bg-zinc-600 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="clearRestriction"
|
||||
>
|
||||
제재 해제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">{{ restrictionStatus }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<h4 class="text-base font-semibold">프로필 아이콘 초기화</h4>
|
||||
<button
|
||||
class="bg-purple-600 hover:bg-purple-500 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="resetProfileIcon"
|
||||
>
|
||||
아이콘 초기화 요청
|
||||
</button>
|
||||
<div class="text-xs text-zinc-500">{{ profileIconStatus }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<h4 class="text-base font-semibold text-red-400">강제 탈퇴</h4>
|
||||
<button
|
||||
class="bg-red-700 hover:bg-red-600 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="forceDeleteUser"
|
||||
>
|
||||
강제 탈퇴 처리
|
||||
</button>
|
||||
<div class="text-xs text-zinc-500">{{ forceDeleteStatus }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-6">
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold">서버 공지</h3>
|
||||
<span class="text-xs text-zinc-500">{{ noticeStatus }}</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="noticeDraft"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded p-3 text-sm text-white min-h-[140px]"
|
||||
placeholder="로비 공지 입력"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="bg-blue-700 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="noticeLoading"
|
||||
@click="saveNotice"
|
||||
>
|
||||
공지 저장
|
||||
</button>
|
||||
<button
|
||||
class="bg-zinc-700 hover:bg-zinc-600 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="noticeLoading"
|
||||
@click="loadNotice"
|
||||
>
|
||||
다시 불러오기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">서버별 관리</h3>
|
||||
<button
|
||||
class="bg-zinc-700 hover:bg-zinc-600 text-white text-sm px-3 py-1.5 rounded"
|
||||
:disabled="profilesLoading"
|
||||
@click="loadProfiles"
|
||||
>
|
||||
새로고침
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-for="profile in profiles"
|
||||
:key="profile.profileName"
|
||||
class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4"
|
||||
>
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2">
|
||||
<div>
|
||||
<div class="text-base font-semibold">
|
||||
{{ profile.profileName }} ({{ profile.profile }})
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">시나리오: {{ profile.scenario }}</div>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-400">
|
||||
상태: {{ profile.status }} / API: {{ profile.runtime.apiRunning ? 'ON' : 'OFF' }} /
|
||||
DAEMON: {{ profile.runtime.daemonRunning ? 'ON' : 'OFF' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-zinc-400">빌드 커밋: {{ profile.buildCommitSha ?? '미지정' }}</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-3">
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs text-zinc-400">표시명</label>
|
||||
<input
|
||||
v-model="profileEdits[profile.profileName].korName"
|
||||
type="text"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
/>
|
||||
<label class="text-xs text-zinc-400">색상</label>
|
||||
<input
|
||||
v-model="profileEdits[profile.profileName].color"
|
||||
type="text"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
/>
|
||||
<label class="text-xs text-zinc-400">인게임 공지</label>
|
||||
<textarea
|
||||
v-model="profileEdits[profile.profileName].inGameNotice"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded p-2 text-sm text-white min-h-[80px]"
|
||||
/>
|
||||
<label class="text-xs text-zinc-400">프로필 이미지</label>
|
||||
<input
|
||||
v-model="profileEdits[profile.profileName].profileImageUrl"
|
||||
type="text"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
/>
|
||||
<button
|
||||
class="bg-emerald-600 hover:bg-emerald-500 text-black font-semibold px-4 py-2 rounded"
|
||||
@click="updateProfileMeta(profile.profileName)"
|
||||
>
|
||||
메타 저장
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs text-zinc-400">특수 동작 메모</label>
|
||||
<input
|
||||
v-model="profileActions[profile.profileName].reason"
|
||||
type="text"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="사유 / 메모"
|
||||
/>
|
||||
<label class="text-xs text-zinc-400">가속/연기 (분)</label>
|
||||
<input
|
||||
v-model="profileActions[profile.profileName].durationMinutes"
|
||||
type="number"
|
||||
min="1"
|
||||
max="1440"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
/>
|
||||
<label class="text-xs text-zinc-400">리셋 예약</label>
|
||||
<input
|
||||
v-model="profileActions[profile.profileName].scheduledAt"
|
||||
type="datetime-local"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-2 pt-2">
|
||||
<button
|
||||
class="bg-blue-700 hover:bg-blue-600 text-white font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'RESUME')"
|
||||
>
|
||||
재개
|
||||
</button>
|
||||
<button
|
||||
class="bg-zinc-700 hover:bg-zinc-600 text-white font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'PAUSE')"
|
||||
>
|
||||
일시정지
|
||||
</button>
|
||||
<button
|
||||
class="bg-red-700 hover:bg-red-600 text-white font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'STOP')"
|
||||
>
|
||||
중지
|
||||
</button>
|
||||
<button
|
||||
class="bg-orange-600 hover:bg-orange-500 text-black font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'ACCELERATE')"
|
||||
>
|
||||
가속
|
||||
</button>
|
||||
<button
|
||||
class="bg-yellow-600 hover:bg-yellow-500 text-black font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'DELAY')"
|
||||
>
|
||||
연기
|
||||
</button>
|
||||
<button
|
||||
class="bg-purple-600 hover:bg-purple-500 text-white font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'RESET_NOW')"
|
||||
>
|
||||
즉시 리셋
|
||||
</button>
|
||||
<button
|
||||
class="bg-purple-800 hover:bg-purple-700 text-white font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'RESET_SCHEDULED')"
|
||||
>
|
||||
리셋 예약
|
||||
</button>
|
||||
<button
|
||||
class="bg-teal-700 hover:bg-teal-600 text-white font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'OPEN_SURVEY')"
|
||||
>
|
||||
설문 오픈
|
||||
</button>
|
||||
<button
|
||||
class="bg-black hover:bg-zinc-800 text-white font-semibold px-3 py-2 rounded col-span-2"
|
||||
@click="requestProfileAction(profile.profileName, 'SHUTDOWN')"
|
||||
>
|
||||
서버 폐쇄
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
{{ profileActionStatus[profile.profileName] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="profileActionStatus.global" class="text-xs text-red-400">
|
||||
{{ profileActionStatus.global }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-3">
|
||||
<input
|
||||
v-model="sessionToken"
|
||||
type="password"
|
||||
class="flex-1 bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-yellow-500"
|
||||
placeholder="세션 토큰 입력"
|
||||
/>
|
||||
<button
|
||||
class="bg-yellow-600 hover:bg-yellow-500 text-black font-semibold px-4 py-2 rounded"
|
||||
@click="saveSessionToken"
|
||||
>
|
||||
저장
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid lg:grid-cols-2 gap-8">
|
||||
<section class="space-y-6">
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<h3 class="text-lg font-semibold">유저 관리</h3>
|
||||
<form class="space-y-3" @submit.prevent="lookupUser">
|
||||
<div class="flex flex-col md:flex-row gap-2">
|
||||
<select
|
||||
v-model="userLookupMode"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
>
|
||||
<option value="username">계정명</option>
|
||||
<option value="email">이메일</option>
|
||||
<option value="id">UUID</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="userLookupValue"
|
||||
type="text"
|
||||
class="flex-1 bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-yellow-500"
|
||||
placeholder="검색 값 입력"
|
||||
/>
|
||||
<button
|
||||
class="bg-blue-700 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="userLoading"
|
||||
>
|
||||
조회
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="userError" class="text-xs text-red-400">{{ userError }}</div>
|
||||
</form>
|
||||
|
||||
<div v-if="userResult" class="bg-zinc-950 border border-zinc-800 rounded p-4 space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-sm font-semibold">{{ userResult.username }}</div>
|
||||
<div class="text-xs text-zinc-500">{{ userResult.id }}</div>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-400">표시명: {{ userResult.displayName }}</div>
|
||||
<div class="text-xs text-zinc-400">권한: {{ userResult.roles.join(', ') || '-' }}</div>
|
||||
<div class="text-xs text-zinc-500">OAuth: {{ userResult.oauthType }} {{ userResult.email ?? '' }}</div>
|
||||
<div class="text-xs text-zinc-500">가입일: {{ userResult.createdAt }}</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<h4 class="text-base font-semibold">비밀번호 리셋</h4>
|
||||
<div class="flex flex-col md:flex-row gap-2">
|
||||
<input
|
||||
v-model="passwordInput"
|
||||
type="text"
|
||||
class="flex-1 bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="직접 지정 시 입력"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<button
|
||||
class="bg-emerald-600 hover:bg-emerald-500 text-black font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="resetUserPassword"
|
||||
>
|
||||
초기화
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-400">임시 비밀번호: {{ passwordResult || '-' }}</div>
|
||||
<div class="text-xs text-zinc-500">{{ passwordStatus }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<h4 class="text-base font-semibold">특수 권한 부여</h4>
|
||||
<div class="flex flex-col md:flex-row gap-2">
|
||||
<select
|
||||
v-model="rolesMode"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
:disabled="!hasUser"
|
||||
>
|
||||
<option value="grant">추가</option>
|
||||
<option value="revoke">제거</option>
|
||||
<option value="set">덮어쓰기</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="rolesInput"
|
||||
type="text"
|
||||
class="flex-1 bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="admin, moderator 등"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<button
|
||||
class="bg-indigo-600 hover:bg-indigo-500 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="updateUserRoles"
|
||||
>
|
||||
적용
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">{{ rolesStatus }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<h4 class="text-base font-semibold">유저 차단</h4>
|
||||
<div class="flex flex-col gap-2">
|
||||
<input
|
||||
v-model="banUntil"
|
||||
type="datetime-local"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<input
|
||||
v-model="banReason"
|
||||
type="text"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="사유"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="bg-red-600 hover:bg-red-500 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="applyBan"
|
||||
>
|
||||
차단 설정
|
||||
</button>
|
||||
<button
|
||||
class="bg-zinc-700 hover:bg-zinc-600 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="clearBan"
|
||||
>
|
||||
차단 해제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">{{ banStatus }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<h4 class="text-base font-semibold">서버별 기능 제재</h4>
|
||||
<div class="grid gap-2">
|
||||
<input
|
||||
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"
|
||||
/>
|
||||
<input
|
||||
v-model="restrictionFeatures"
|
||||
type="text"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="login, message 등"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<input
|
||||
v-model="restrictionUntil"
|
||||
type="datetime-local"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<input
|
||||
v-model="restrictionReason"
|
||||
type="text"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="사유"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<input
|
||||
v-model="restrictionNotes"
|
||||
type="text"
|
||||
class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="메모"
|
||||
:disabled="!hasUser"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="bg-amber-600 hover:bg-amber-500 text-black font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="applyRestriction"
|
||||
>
|
||||
제재 적용
|
||||
</button>
|
||||
<button
|
||||
class="bg-zinc-700 hover:bg-zinc-600 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="clearRestriction"
|
||||
>
|
||||
제재 해제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">{{ restrictionStatus }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<h4 class="text-base font-semibold">프로필 아이콘 초기화</h4>
|
||||
<button
|
||||
class="bg-purple-600 hover:bg-purple-500 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="resetProfileIcon"
|
||||
>
|
||||
아이콘 초기화 요청
|
||||
</button>
|
||||
<div class="text-xs text-zinc-500">{{ profileIconStatus }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<h4 class="text-base font-semibold text-red-400">강제 탈퇴</h4>
|
||||
<button
|
||||
class="bg-red-700 hover:bg-red-600 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="!hasUser"
|
||||
@click="forceDeleteUser"
|
||||
>
|
||||
강제 탈퇴 처리
|
||||
</button>
|
||||
<div class="text-xs text-zinc-500">{{ forceDeleteStatus }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-6">
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold">서버 공지</h3>
|
||||
<span class="text-xs text-zinc-500">{{ noticeStatus }}</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="noticeDraft"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded p-3 text-sm text-white min-h-[140px]"
|
||||
placeholder="로비 공지 입력"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="bg-blue-700 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="noticeLoading"
|
||||
@click="saveNotice"
|
||||
>
|
||||
공지 저장
|
||||
</button>
|
||||
<button
|
||||
class="bg-zinc-700 hover:bg-zinc-600 text-white font-semibold px-4 py-2 rounded"
|
||||
:disabled="noticeLoading"
|
||||
@click="loadNotice"
|
||||
>
|
||||
다시 불러오기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">서버별 관리</h3>
|
||||
<button
|
||||
class="bg-zinc-700 hover:bg-zinc-600 text-white text-sm px-3 py-1.5 rounded"
|
||||
:disabled="profilesLoading"
|
||||
@click="loadProfiles"
|
||||
>
|
||||
새로고침
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-for="profile in profiles"
|
||||
:key="profile.profileName"
|
||||
class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4"
|
||||
>
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2">
|
||||
<div>
|
||||
<div class="text-base font-semibold">
|
||||
{{ profile.profileName }} ({{ profile.profile }})
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">시나리오: {{ profile.scenario }}</div>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-400">
|
||||
상태: {{ profile.status }} / API: {{ profile.runtime.apiRunning ? 'ON' : 'OFF' }} /
|
||||
DAEMON: {{ profile.runtime.daemonRunning ? 'ON' : 'OFF' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-zinc-400">
|
||||
빌드 커밋: {{ profile.buildCommitSha ?? '미지정' }}
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-3">
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs text-zinc-400">표시명</label>
|
||||
<input
|
||||
v-model="profileEdits[profile.profileName].korName"
|
||||
type="text"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
/>
|
||||
<label class="text-xs text-zinc-400">색상</label>
|
||||
<input
|
||||
v-model="profileEdits[profile.profileName].color"
|
||||
type="text"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
/>
|
||||
<label class="text-xs text-zinc-400">인게임 공지</label>
|
||||
<textarea
|
||||
v-model="profileEdits[profile.profileName].inGameNotice"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded p-2 text-sm text-white min-h-[80px]"
|
||||
/>
|
||||
<label class="text-xs text-zinc-400">프로필 이미지</label>
|
||||
<input
|
||||
v-model="profileEdits[profile.profileName].profileImageUrl"
|
||||
type="text"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
/>
|
||||
<button
|
||||
class="bg-emerald-600 hover:bg-emerald-500 text-black font-semibold px-4 py-2 rounded"
|
||||
@click="updateProfileMeta(profile.profileName)"
|
||||
>
|
||||
메타 저장
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs text-zinc-400">특수 동작 메모</label>
|
||||
<input
|
||||
v-model="profileActions[profile.profileName].reason"
|
||||
type="text"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
placeholder="사유 / 메모"
|
||||
/>
|
||||
<label class="text-xs text-zinc-400">가속/연기 (분)</label>
|
||||
<input
|
||||
v-model="profileActions[profile.profileName].durationMinutes"
|
||||
type="number"
|
||||
min="1"
|
||||
max="1440"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
/>
|
||||
<label class="text-xs text-zinc-400">리셋 예약</label>
|
||||
<input
|
||||
v-model="profileActions[profile.profileName].scheduledAt"
|
||||
type="datetime-local"
|
||||
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-2 pt-2">
|
||||
<button
|
||||
class="bg-blue-700 hover:bg-blue-600 text-white font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'RESUME')"
|
||||
>
|
||||
재개
|
||||
</button>
|
||||
<button
|
||||
class="bg-zinc-700 hover:bg-zinc-600 text-white font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'PAUSE')"
|
||||
>
|
||||
일시정지
|
||||
</button>
|
||||
<button
|
||||
class="bg-red-700 hover:bg-red-600 text-white font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'STOP')"
|
||||
>
|
||||
중지
|
||||
</button>
|
||||
<button
|
||||
class="bg-orange-600 hover:bg-orange-500 text-black font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'ACCELERATE')"
|
||||
>
|
||||
가속
|
||||
</button>
|
||||
<button
|
||||
class="bg-yellow-600 hover:bg-yellow-500 text-black font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'DELAY')"
|
||||
>
|
||||
연기
|
||||
</button>
|
||||
<button
|
||||
class="bg-purple-600 hover:bg-purple-500 text-white font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'RESET_NOW')"
|
||||
>
|
||||
즉시 리셋
|
||||
</button>
|
||||
<button
|
||||
class="bg-purple-800 hover:bg-purple-700 text-white font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'RESET_SCHEDULED')"
|
||||
>
|
||||
리셋 예약
|
||||
</button>
|
||||
<button
|
||||
class="bg-teal-700 hover:bg-teal-600 text-white font-semibold px-3 py-2 rounded"
|
||||
@click="requestProfileAction(profile.profileName, 'OPEN_SURVEY')"
|
||||
>
|
||||
설문 오픈
|
||||
</button>
|
||||
<button
|
||||
class="bg-black hover:bg-zinc-800 text-white font-semibold px-3 py-2 rounded col-span-2"
|
||||
@click="requestProfileAction(profile.profileName, 'SHUTDOWN')"
|
||||
>
|
||||
서버 폐쇄
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
{{ profileActionStatus[profile.profileName] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="profileActionStatus.global" class="text-xs text-red-400">
|
||||
{{ profileActionStatus.global }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
</DefaultLayout>
|
||||
</template>
|
||||
|
||||
@@ -9,117 +9,120 @@ const username = ref('');
|
||||
const password = ref('');
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const me = await trpc.me.query();
|
||||
if (me) {
|
||||
router.push('/lobby');
|
||||
try {
|
||||
const me = await trpc.me.query();
|
||||
if (me) {
|
||||
router.push('/lobby');
|
||||
}
|
||||
} catch (e) {
|
||||
// Not logged in or error
|
||||
}
|
||||
} catch (e) {
|
||||
// Not logged in or error
|
||||
}
|
||||
});
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
// TODO: Implement login mutation
|
||||
// const result = await trpc.auth.login.mutation({ username: username.value, password: password.value });
|
||||
// if (result.success) router.push('/lobby');
|
||||
console.log('Login attempt:', username.value);
|
||||
} catch (e) {
|
||||
alert('로그인 실패');
|
||||
}
|
||||
try {
|
||||
// TODO: Implement login mutation
|
||||
// const result = await trpc.auth.login.mutation({ username: username.value, password: password.value });
|
||||
// if (result.success) router.push('/lobby');
|
||||
console.log('Login attempt:', username.value);
|
||||
} catch (e) {
|
||||
alert('로그인 실패');
|
||||
}
|
||||
};
|
||||
|
||||
const handleJoin = () => {
|
||||
console.log('Join attempt');
|
||||
console.log('Join attempt');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefaultLayout>
|
||||
<div class="max-w-4xl mx-auto py-12 px-4 flex flex-col items-center space-y-12">
|
||||
<!-- Logo / Title -->
|
||||
<div class="text-center">
|
||||
<h2 class="text-4xl font-serif font-bold text-white tracking-widest">삼국지 모의전투 HiDCHe</h2>
|
||||
</div>
|
||||
<DefaultLayout>
|
||||
<div class="max-w-4xl mx-auto py-12 px-4 flex flex-col items-center space-y-12">
|
||||
<!-- Logo / Title -->
|
||||
<div class="text-center">
|
||||
<h2 class="text-4xl font-serif font-bold text-white tracking-widest">삼국지 모의전투 HiDCHe</h2>
|
||||
</div>
|
||||
|
||||
<!-- Login Box -->
|
||||
<div class="w-full max-w-md bg-zinc-800 border border-zinc-700 rounded shadow-2xl overflow-hidden">
|
||||
<div class="bg-zinc-700 px-6 py-2 text-center font-bold text-white border-b border-zinc-600">
|
||||
로그인
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="w-20 text-sm font-medium">계정명</label>
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
class="flex-grow bg-zinc-900 border border-zinc-600 rounded px-3 py-1.5 text-white focus:outline-none focus:border-blue-500"
|
||||
placeholder="계정명"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="w-20 text-sm font-medium">비밀번호</label>
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
class="flex-grow bg-zinc-900 border border-zinc-600 rounded px-3 py-1.5 text-white focus:outline-none focus:border-blue-500"
|
||||
placeholder="비밀번호"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-2 pt-2">
|
||||
<button
|
||||
@click="handleJoin"
|
||||
class="flex-1 bg-yellow-600 hover:bg-yellow-500 text-black font-bold py-2 rounded transition-colors flex items-center justify-center space-x-2"
|
||||
>
|
||||
<span>가입 & 로그인</span>
|
||||
</button>
|
||||
<button
|
||||
@click="handleLogin"
|
||||
class="flex-[2] bg-blue-700 hover:bg-blue-600 text-white font-bold py-2 rounded transition-colors"
|
||||
>
|
||||
로그인
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Login Box -->
|
||||
<div class="w-full max-w-md bg-zinc-800 border border-zinc-700 rounded shadow-2xl overflow-hidden">
|
||||
<div class="bg-zinc-700 px-6 py-2 text-center font-bold text-white border-b border-zinc-600">
|
||||
로그인
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="w-20 text-sm font-medium">계정명</label>
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
class="flex-grow bg-zinc-900 border border-zinc-600 rounded px-3 py-1.5 text-white focus:outline-none focus:border-blue-500"
|
||||
placeholder="계정명"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="w-20 text-sm font-medium">비밀번호</label>
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
class="flex-grow bg-zinc-900 border border-zinc-600 rounded px-3 py-1.5 text-white focus:outline-none focus:border-blue-500"
|
||||
placeholder="비밀번호"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-2 pt-2">
|
||||
<button
|
||||
@click="handleJoin"
|
||||
class="flex-1 bg-yellow-600 hover:bg-yellow-500 text-black font-bold py-2 rounded transition-colors flex items-center justify-center space-x-2"
|
||||
>
|
||||
<span>가입 & 로그인</span>
|
||||
</button>
|
||||
<button
|
||||
@click="handleLogin"
|
||||
class="flex-[2] bg-blue-700 hover:bg-blue-600 text-white font-bold py-2 rounded transition-colors"
|
||||
>
|
||||
로그인
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Status Placeholder -->
|
||||
<div class="w-full max-w-2xl bg-zinc-900 border border-zinc-800 rounded-lg overflow-hidden shadow-xl">
|
||||
<div class="bg-zinc-800 px-4 py-2 border-b border-zinc-700 flex justify-between items-center">
|
||||
<span class="font-bold text-sm">체 현황</span>
|
||||
<span class="text-xs text-zinc-400">西紀 197年 7月 秋</span>
|
||||
<!-- Server Status Placeholder -->
|
||||
<div class="w-full max-w-2xl bg-zinc-900 border border-zinc-800 rounded-lg overflow-hidden shadow-xl">
|
||||
<div class="bg-zinc-800 px-4 py-2 border-b border-zinc-700 flex justify-between items-center">
|
||||
<span class="font-bold text-sm">체 현황</span>
|
||||
<span class="text-xs text-zinc-400">西紀 197年 7月 秋</span>
|
||||
</div>
|
||||
<div class="aspect-video bg-zinc-950 relative flex items-center justify-center">
|
||||
<!-- Map Placeholder -->
|
||||
<div class="text-zinc-700 text-lg italic">지도 이미지 및 현황 데이터 영역</div>
|
||||
|
||||
<!-- Example of a city dot if we wanted to mock it -->
|
||||
<div class="absolute bottom-4 right-4 text-[10px] text-blue-400">도시명 표기 끄기</div>
|
||||
</div>
|
||||
<div class="p-4 bg-black text-xs space-y-1 font-mono">
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-blue-400">◆</span>
|
||||
<span
|
||||
>197년 7월: [대회] 황제 수장의 명으로 전력전 대회가 개최됩니다! 천하의 영웅들을 모집하고
|
||||
있습니다!</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-cyan-400">●</span>
|
||||
<span>197년 7월: [재난] 탐라 호토에 메뚜기 떼가 발생하여 도시가 황폐해지고 있습니다.</span>
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-green-400">●</span>
|
||||
<span>197년 7월: [자금] 가을이 되어 봉록에 따라 군량이 지급됩니다.</span>
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-red-400">●</span>
|
||||
<span>197년 4월: [재난] 남피, 무안에 홍수로 인해 피해가 급증하고 있습니다.</span>
|
||||
</div>
|
||||
<!-- ... more logs ... -->
|
||||
<div class="text-zinc-600 pt-2 italic text-center">최근 진행 상황 로그 (Placeholder)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aspect-video bg-zinc-950 relative flex items-center justify-center">
|
||||
<!-- Map Placeholder -->
|
||||
<div class="text-zinc-700 text-lg italic">지도 이미지 및 현황 데이터 영역</div>
|
||||
|
||||
<!-- Example of a city dot if we wanted to mock it -->
|
||||
<div class="absolute bottom-4 right-4 text-[10px] text-blue-400">도시명 표기 끄기</div>
|
||||
</div>
|
||||
<div class="p-4 bg-black text-xs space-y-1 font-mono">
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-blue-400">◆</span>
|
||||
<span>197년 7월: [대회] 황제 수장의 명으로 전력전 대회가 개최됩니다! 천하의 영웅들을 모집하고 있습니다!</span>
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-cyan-400">●</span>
|
||||
<span>197년 7월: [재난] 탐라 호토에 메뚜기 떼가 발생하여 도시가 황폐해지고 있습니다.</span>
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-green-400">●</span>
|
||||
<span>197년 7월: [자금] 가을이 되어 봉록에 따라 군량이 지급됩니다.</span>
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-red-400">●</span>
|
||||
<span>197년 4월: [재난] 남피, 무안에 홍수로 인해 피해가 급증하고 있습니다.</span>
|
||||
</div>
|
||||
<!-- ... more logs ... -->
|
||||
<div class="text-zinc-600 pt-2 italic text-center">최근 진행 상황 로그 (Placeholder)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
</DefaultLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -12,167 +12,232 @@ const profiles = ref<any[]>([]);
|
||||
const profileDetails = ref<Record<string, any>>({});
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
me.value = await trpc.me.query();
|
||||
if (!me.value) {
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
|
||||
notice.value = await trpc.lobby.notice.query();
|
||||
profiles.value = await trpc.lobby.profiles.query();
|
||||
|
||||
// Fetch details for each profile
|
||||
for (const profile of profiles.value) {
|
||||
if (profile.status === 'RUNNING' || profile.status === 'PREOPEN') {
|
||||
try {
|
||||
const gameTrpc = createGameTrpc(profile.apiPort);
|
||||
const info = await gameTrpc.lobby.info.query();
|
||||
profileDetails.value[profile.profileName] = info;
|
||||
} catch (e) {
|
||||
console.error(`Failed to fetch info for ${profile.profileName}`, e);
|
||||
try {
|
||||
me.value = await trpc.me.query();
|
||||
if (!me.value) {
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
notice.value = await trpc.lobby.notice.query();
|
||||
profiles.value = await trpc.lobby.profiles.query();
|
||||
|
||||
// Fetch details for each profile
|
||||
for (const profile of profiles.value) {
|
||||
if (profile.status === 'RUNNING' || profile.status === 'PREOPEN') {
|
||||
try {
|
||||
const gameTrpc = createGameTrpc(profile.apiPort);
|
||||
const info = await gameTrpc.lobby.info.query();
|
||||
profileDetails.value[profile.profileName] = info;
|
||||
} catch (e) {
|
||||
console.error(`Failed to fetch info for ${profile.profileName}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load lobby', e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load lobby', e);
|
||||
}
|
||||
});
|
||||
|
||||
const handleLogout = async () => {
|
||||
// TODO: Implement logout mutation in gateway-api
|
||||
// await trpc.auth.logout.mutation();
|
||||
router.push('/');
|
||||
// TODO: Implement logout mutation in gateway-api
|
||||
// await trpc.auth.logout.mutation();
|
||||
router.push('/');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefaultLayout>
|
||||
<div class="max-w-5xl mx-auto py-8 px-4 space-y-8">
|
||||
<!-- Notice -->
|
||||
<div v-if="notice" class="text-center">
|
||||
<span class="text-orange-500 text-3xl font-bold" v-html="notice"></span>
|
||||
</div>
|
||||
<DefaultLayout>
|
||||
<div class="max-w-5xl mx-auto py-8 px-4 space-y-8">
|
||||
<!-- Notice -->
|
||||
<div v-if="notice" class="text-center">
|
||||
<span class="text-orange-500 text-3xl font-bold" v-html="notice"></span>
|
||||
</div>
|
||||
|
||||
<!-- Server List Table -->
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded shadow-xl overflow-hidden">
|
||||
<div class="bg-zinc-800 px-6 py-3 text-center font-bold text-white border-b border-zinc-700 text-xl tracking-widest">
|
||||
서 버 선 택
|
||||
</div>
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="bg-zinc-800 text-zinc-400 uppercase text-xs">
|
||||
<tr>
|
||||
<th class="px-4 py-3 border-b border-zinc-700 w-24 text-center">서 버</th>
|
||||
<th class="px-4 py-3 border-b border-zinc-700">정 보</th>
|
||||
<th class="px-4 py-3 border-b border-zinc-700 w-48 text-center" colspan="2">캐 릭 터</th>
|
||||
<th class="px-4 py-3 border-b border-zinc-700 w-32 text-center">선 택</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-800">
|
||||
<tr v-for="profile in profiles" :key="profile.profileName" class="hover:bg-zinc-800/50 transition-colors">
|
||||
<!-- Server Name -->
|
||||
<td class="px-4 py-4 text-center border-r border-zinc-800">
|
||||
<div
|
||||
:style="{ color: profile.color }"
|
||||
class="text-lg font-bold cursor-help"
|
||||
:title="profileDetails[profile.profileName] ? `시작일: ${profileDetails[profile.profileName].starttime}` : ''"
|
||||
<!-- Server List Table -->
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded shadow-xl overflow-hidden">
|
||||
<div
|
||||
class="bg-zinc-800 px-6 py-3 text-center font-bold text-white border-b border-zinc-700 text-xl tracking-widest"
|
||||
>
|
||||
{{ profile.korName }}섭
|
||||
서 버 선 택
|
||||
</div>
|
||||
<div v-if="profileDetails[profile.profileName]" class="text-xs text-zinc-500 mt-1">
|
||||
<{{ profileDetails[profile.profileName].nationCnt }}국 경쟁중>
|
||||
</div>
|
||||
</td>
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="bg-zinc-800 text-zinc-400 uppercase text-xs">
|
||||
<tr>
|
||||
<th class="px-4 py-3 border-b border-zinc-700 w-24 text-center">서 버</th>
|
||||
<th class="px-4 py-3 border-b border-zinc-700">정 보</th>
|
||||
<th class="px-4 py-3 border-b border-zinc-700 w-48 text-center" colspan="2">캐 릭 터</th>
|
||||
<th class="px-4 py-3 border-b border-zinc-700 w-32 text-center">선 택</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-800">
|
||||
<tr
|
||||
v-for="profile in profiles"
|
||||
:key="profile.profileName"
|
||||
class="hover:bg-zinc-800/50 transition-colors"
|
||||
>
|
||||
<!-- Server Name -->
|
||||
<td class="px-4 py-4 text-center border-r border-zinc-800">
|
||||
<div
|
||||
:style="{ color: profile.color }"
|
||||
class="text-lg font-bold cursor-help"
|
||||
:title="
|
||||
profileDetails[profile.profileName]
|
||||
? `시작일: ${profileDetails[profile.profileName].starttime}`
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{ profile.korName }}섭
|
||||
</div>
|
||||
<div v-if="profileDetails[profile.profileName]" class="text-xs text-zinc-500 mt-1">
|
||||
<{{ profileDetails[profile.profileName].nationCnt }}국 경쟁중>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Server Info -->
|
||||
<td class="px-4 py-4 border-r border-zinc-800">
|
||||
<template v-if="profileDetails[profile.profileName]">
|
||||
<div class="space-y-1">
|
||||
<div>
|
||||
서기 {{ profileDetails[profile.profileName].year }}년 {{ profileDetails[profile.profileName].month }}월
|
||||
(<span class="text-orange-400">{{ profile.scenario }}</span>)
|
||||
<!-- Server Info -->
|
||||
<td class="px-4 py-4 border-r border-zinc-800">
|
||||
<template v-if="profileDetails[profile.profileName]">
|
||||
<div class="space-y-1">
|
||||
<div>
|
||||
서기 {{ profileDetails[profile.profileName].year }}년
|
||||
{{ profileDetails[profile.profileName].month }}월 (<span
|
||||
class="text-orange-400"
|
||||
>{{ profile.scenario }}</span
|
||||
>)
|
||||
</div>
|
||||
<div class="text-zinc-400">
|
||||
유저 : {{ profileDetails[profile.profileName].userCnt }} /
|
||||
{{ profileDetails[profile.profileName].maxUserCnt }}명
|
||||
<span class="text-cyan-400 ml-2"
|
||||
>NPC : {{ profileDetails[profile.profileName].npcCnt }}명</span
|
||||
>
|
||||
<span class="text-green-400 ml-2"
|
||||
>({{ profileDetails[profile.profileName].turnTerm }}분 턴 서버)</span
|
||||
>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
(상성 설정:{{ profileDetails[profile.profileName].fictionMode }}), (기타
|
||||
설정:{{ profileDetails[profile.profileName].otherTextInfo }})
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="profile.status === 'STOPPED'">
|
||||
<div class="text-center text-zinc-600 py-2">- 폐 쇄 중 -</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-center text-zinc-500 py-2">정보를 불러오는 중...</div>
|
||||
</template>
|
||||
</td>
|
||||
|
||||
<!-- Character Info -->
|
||||
<td class="px-2 py-4 w-16 border-r border-zinc-800">
|
||||
<div
|
||||
v-if="profileDetails[profile.profileName]?.myGeneral"
|
||||
class="w-12 h-12 mx-auto bg-zinc-800 rounded overflow-hidden border border-zinc-700"
|
||||
>
|
||||
<img
|
||||
:src="profileDetails[profile.profileName].myGeneral.picture"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4 border-r border-zinc-800 text-center">
|
||||
<div v-if="profileDetails[profile.profileName]?.myGeneral" class="font-medium">
|
||||
{{ profileDetails[profile.profileName].myGeneral.name }}
|
||||
</div>
|
||||
<div v-else class="text-zinc-600">- 미 등 록 -</div>
|
||||
</td>
|
||||
|
||||
<!-- Action -->
|
||||
<td class="px-4 py-4 text-center">
|
||||
<template v-if="profileDetails[profile.profileName]">
|
||||
<button
|
||||
v-if="profileDetails[profile.profileName].myGeneral"
|
||||
class="w-full bg-zinc-700 hover:bg-zinc-600 text-white py-1.5 rounded text-sm transition-colors"
|
||||
>
|
||||
입장
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="w-full bg-zinc-700 hover:bg-zinc-600 text-white py-1.5 rounded text-sm transition-colors"
|
||||
>
|
||||
장수생성
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="profile.status === 'STOPPED'">
|
||||
<span class="text-zinc-700">-</span>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Footer Info -->
|
||||
<div class="bg-zinc-800/50 p-4 text-xs text-zinc-500 space-y-2 border-t border-zinc-800">
|
||||
<p class="text-red-500 font-bold">
|
||||
★ 1명이 2개 이상의 계정을 사용하거나 타 유저의 턴을 대신 입력하는 것이 적발될 경우 차단 될 수
|
||||
있습니다.
|
||||
</p>
|
||||
<p>계정은 한번 등록으로 계속 사용합니다. 각 서버 리셋시 캐릭터만 새로 생성하면 됩니다.</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-1 mt-2">
|
||||
<p>
|
||||
<span class="text-zinc-300 font-bold">체섭</span> : 메인서버입니다. 천하통일에 도전하여
|
||||
왕조일람과 명예의전당에 올라봅시다! (주로 1턴=60분)
|
||||
</p>
|
||||
<p>
|
||||
<span class="text-zinc-300 font-bold">퀘섭</span> : 마이너 서버 그룹1. 비교적 느린 시간으로
|
||||
운영됩니다.
|
||||
</p>
|
||||
<p>
|
||||
<span class="text-zinc-300 font-bold">풰섭</span> : 마이너 서버 그룹1. 비교적 느린 시간으로
|
||||
운영됩니다.
|
||||
</p>
|
||||
<p>
|
||||
<span class="text-zinc-300 font-bold">퉤섭</span> : 마이너 서버 그룹2. 비교적 빠른 시간으로
|
||||
운영됩니다.
|
||||
</p>
|
||||
<p>
|
||||
<span class="text-zinc-300 font-bold">냐섭</span> : 마이너 서버 그룹3. 독특한 컨셉 위주로
|
||||
운영됩니다.
|
||||
</p>
|
||||
<p>
|
||||
<span class="text-zinc-300 font-bold">퍄섭</span> : 마이너 서버 그룹3. 독특한 컨셉 위주로
|
||||
운영됩니다.
|
||||
</p>
|
||||
<p>
|
||||
<span class="text-zinc-300 font-bold">훼섭</span> : 운영자 테스트 서버입니다. 기습적으로
|
||||
열리고, 닫힐 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-zinc-400">
|
||||
유저 : {{ profileDetails[profile.profileName].userCnt }} / {{ profileDetails[profile.profileName].maxUserCnt }}명
|
||||
<span class="text-cyan-400 ml-2">NPC : {{ profileDetails[profile.profileName].npcCnt }}명</span>
|
||||
<span class="text-green-400 ml-2">({{ profileDetails[profile.profileName].turnTerm }}분 턴 서버)</span>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
(상성 설정:{{ profileDetails[profile.profileName].fictionMode }}), (기타 설정:{{ profileDetails[profile.profileName].otherTextInfo }})
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="profile.status === 'STOPPED'">
|
||||
<div class="text-center text-zinc-600 py-2">- 폐 쇄 중 -</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-center text-zinc-500 py-2">정보를 불러오는 중...</div>
|
||||
</template>
|
||||
</td>
|
||||
|
||||
<!-- Character Info -->
|
||||
<td class="px-2 py-4 w-16 border-r border-zinc-800">
|
||||
<div v-if="profileDetails[profile.profileName]?.myGeneral" class="w-12 h-12 mx-auto bg-zinc-800 rounded overflow-hidden border border-zinc-700">
|
||||
<img :src="profileDetails[profile.profileName].myGeneral.picture" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4 border-r border-zinc-800 text-center">
|
||||
<div v-if="profileDetails[profile.profileName]?.myGeneral" class="font-medium">
|
||||
{{ profileDetails[profile.profileName].myGeneral.name }}
|
||||
</div>
|
||||
|
||||
<!-- Account Management -->
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded shadow-xl overflow-hidden">
|
||||
<div
|
||||
class="bg-zinc-800 px-6 py-2 text-center font-bold text-white border-b border-zinc-700 tracking-widest"
|
||||
>
|
||||
계 정 관 리
|
||||
</div>
|
||||
<div v-else class="text-zinc-600">- 미 등 록 -</div>
|
||||
</td>
|
||||
|
||||
<!-- Action -->
|
||||
<td class="px-4 py-4 text-center">
|
||||
<template v-if="profileDetails[profile.profileName]">
|
||||
<button v-if="profileDetails[profile.profileName].myGeneral" class="w-full bg-zinc-700 hover:bg-zinc-600 text-white py-1.5 rounded text-sm transition-colors">
|
||||
입장
|
||||
</button>
|
||||
<button v-else class="w-full bg-zinc-700 hover:bg-zinc-600 text-white py-1.5 rounded text-sm transition-colors">
|
||||
장수생성
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="profile.status === 'STOPPED'">
|
||||
<span class="text-zinc-700">-</span>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Footer Info -->
|
||||
<div class="bg-zinc-800/50 p-4 text-xs text-zinc-500 space-y-2 border-t border-zinc-800">
|
||||
<p class="text-red-500 font-bold">★ 1명이 2개 이상의 계정을 사용하거나 타 유저의 턴을 대신 입력하는 것이 적발될 경우 차단 될 수 있습니다.</p>
|
||||
<p>계정은 한번 등록으로 계속 사용합니다. 각 서버 리셋시 캐릭터만 새로 생성하면 됩니다.</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-1 mt-2">
|
||||
<p><span class="text-zinc-300 font-bold">체섭</span> : 메인서버입니다. 천하통일에 도전하여 왕조일람과 명예의전당에 올라봅시다! (주로 1턴=60분)</p>
|
||||
<p><span class="text-zinc-300 font-bold">퀘섭</span> : 마이너 서버 그룹1. 비교적 느린 시간으로 운영됩니다.</p>
|
||||
<p><span class="text-zinc-300 font-bold">풰섭</span> : 마이너 서버 그룹1. 비교적 느린 시간으로 운영됩니다.</p>
|
||||
<p><span class="text-zinc-300 font-bold">퉤섭</span> : 마이너 서버 그룹2. 비교적 빠른 시간으로 운영됩니다.</p>
|
||||
<p><span class="text-zinc-300 font-bold">냐섭</span> : 마이너 서버 그룹3. 독특한 컨셉 위주로 운영됩니다.</p>
|
||||
<p><span class="text-zinc-300 font-bold">퍄섭</span> : 마이너 서버 그룹3. 독특한 컨셉 위주로 운영됩니다.</p>
|
||||
<p><span class="text-zinc-300 font-bold">훼섭</span> : 운영자 테스트 서버입니다. 기습적으로 열리고, 닫힐 수 있습니다.</p>
|
||||
</div>
|
||||
<div class="p-6 flex justify-center space-x-4">
|
||||
<button
|
||||
class="bg-zinc-800 hover:bg-zinc-700 text-white px-6 py-2 rounded border border-zinc-700 transition-colors"
|
||||
>
|
||||
비밀번호 & 전콘 & 탈퇴
|
||||
</button>
|
||||
<button
|
||||
@click="handleLogout"
|
||||
class="bg-zinc-800 hover:bg-zinc-700 text-white px-6 py-2 rounded border border-zinc-700 transition-colors"
|
||||
>
|
||||
로 그 아 웃
|
||||
</button>
|
||||
<button
|
||||
v-if="me?.roles?.includes('admin')"
|
||||
class="bg-zinc-800 hover:bg-zinc-700 text-white px-6 py-2 rounded border border-zinc-700 transition-colors"
|
||||
>
|
||||
관리자 페이지
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Management -->
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded shadow-xl overflow-hidden">
|
||||
<div class="bg-zinc-800 px-6 py-2 text-center font-bold text-white border-b border-zinc-700 tracking-widest">
|
||||
계 정 관 리
|
||||
</div>
|
||||
<div class="p-6 flex justify-center space-x-4">
|
||||
<button class="bg-zinc-800 hover:bg-zinc-700 text-white px-6 py-2 rounded border border-zinc-700 transition-colors">
|
||||
비밀번호 & 전콘 & 탈퇴
|
||||
</button>
|
||||
<button @click="handleLogout" class="bg-zinc-800 hover:bg-zinc-700 text-white px-6 py-2 rounded border border-zinc-700 transition-colors">
|
||||
로 그 아 웃
|
||||
</button>
|
||||
<button v-if="me?.roles?.includes('admin')" class="bg-zinc-800 hover:bg-zinc-700 text-white px-6 py-2 rounded border border-zinc-700 transition-colors">
|
||||
관리자 페이지
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
</DefaultLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@sammo-ts/common": ["../../packages/common/src/index.ts"],
|
||||
"@sammo-ts/common/*": ["../../packages/common/src/*"],
|
||||
"@sammo-ts/infra": ["../../packages/infra/src/index.ts"],
|
||||
"@sammo-ts/infra/*": ["../../packages/infra/src/*"],
|
||||
"@sammo-ts/logic": ["../../packages/logic/src/index.ts"],
|
||||
"@sammo-ts/logic/*": ["../../packages/logic/src/*"],
|
||||
"@sammo-ts/game-engine": ["../../app/game-engine/src/index.ts"],
|
||||
"@sammo-ts/game-engine/*": ["../../app/game-engine/src/*"],
|
||||
"@sammo-ts/game-api": ["../../app/game-api/src/index.ts"],
|
||||
"@sammo-ts/game-api/*": ["../../app/game-api/src/*"],
|
||||
"@sammo-ts/gateway-api": ["../../app/gateway-api/src/index.ts"],
|
||||
"@sammo-ts/gateway-api/*": ["../../app/gateway-api/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "../../packages/common" },
|
||||
{ "path": "../../packages/infra" },
|
||||
{ "path": "../../packages/logic" },
|
||||
{ "path": "../../app/game-engine" },
|
||||
{ "path": "../../app/game-api" },
|
||||
{ "path": "../../app/gateway-api" }
|
||||
]
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@sammo-ts/common": ["../../packages/common/src/index.ts"],
|
||||
"@sammo-ts/common/*": ["../../packages/common/src/*"],
|
||||
"@sammo-ts/infra": ["../../packages/infra/src/index.ts"],
|
||||
"@sammo-ts/infra/*": ["../../packages/infra/src/*"],
|
||||
"@sammo-ts/logic": ["../../packages/logic/src/index.ts"],
|
||||
"@sammo-ts/logic/*": ["../../packages/logic/src/*"],
|
||||
"@sammo-ts/game-engine": ["../../app/game-engine/src/index.ts"],
|
||||
"@sammo-ts/game-engine/*": ["../../app/game-engine/src/*"],
|
||||
"@sammo-ts/game-api": ["../../app/game-api/src/index.ts"],
|
||||
"@sammo-ts/game-api/*": ["../../app/game-api/src/*"],
|
||||
"@sammo-ts/gateway-api": ["../../app/gateway-api/src/index.ts"],
|
||||
"@sammo-ts/gateway-api/*": ["../../app/gateway-api/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "test/**/*.ts", "*.ts"],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "../../packages/common" },
|
||||
{ "path": "../../packages/infra" },
|
||||
{ "path": "../../packages/logic" },
|
||||
{ "path": "../../app/game-engine" },
|
||||
{ "path": "../../app/game-api" },
|
||||
{ "path": "../../app/gateway-api" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
declare const _default: import("vite").UserConfig;
|
||||
declare const _default: import('vite').UserConfig;
|
||||
export default _default;
|
||||
|
||||
@@ -4,10 +4,7 @@ import tailwindcss from '@tailwindcss/vite';
|
||||
import path from 'path';
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
tailwindcss(),
|
||||
],
|
||||
plugins: [vue(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
|
||||
@@ -5,16 +5,13 @@ import path from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
tailwindcss(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
plugins: [vue(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user