feat: implement lobby functionality with user and server information retrieval
This commit is contained in:
@@ -68,6 +68,53 @@ export const appRouter = router({
|
|||||||
now: new Date().toISOString(),
|
now: new Date().toISOString(),
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
|
lobby: router({
|
||||||
|
info: procedure.query(async ({ ctx }) => {
|
||||||
|
const worldState = await ctx.db.worldState.findFirst();
|
||||||
|
if (!worldState) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'World state not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userCnt = await ctx.db.general.count({ where: { npcState: 0 } });
|
||||||
|
const npcCnt = await ctx.db.general.count({ where: { npcState: { gt: 0 } } });
|
||||||
|
const nationCnt = await ctx.db.nation.count({ where: { level: { gt: 0 } } });
|
||||||
|
|
||||||
|
// myGeneral info if authenticated
|
||||||
|
let myGeneral = null;
|
||||||
|
if (ctx.auth?.user.id) {
|
||||||
|
const general = await ctx.db.general.findFirst({
|
||||||
|
where: { userId: ctx.auth.user.id },
|
||||||
|
select: { name: true, picture: true }
|
||||||
|
});
|
||||||
|
if (general) {
|
||||||
|
myGeneral = {
|
||||||
|
name: (general as any).name,
|
||||||
|
picture: (general as any).picture,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
year: worldState.currentYear,
|
||||||
|
month: worldState.currentMonth,
|
||||||
|
userCnt,
|
||||||
|
maxUserCnt: (worldState.config as any).maxUserCnt ?? 500,
|
||||||
|
npcCnt,
|
||||||
|
nationCnt,
|
||||||
|
turnTerm: worldState.tickSeconds / 60,
|
||||||
|
fictionMode: (worldState.config as any).fictionMode ?? '사실',
|
||||||
|
starttime: (worldState.meta as any).starttime ?? '',
|
||||||
|
opentime: (worldState.meta as any).opentime ?? '',
|
||||||
|
turntime: (worldState.meta as any).turntime ?? '',
|
||||||
|
otherTextInfo: (worldState.meta as any).otherTextInfo ?? '',
|
||||||
|
isUnited: (worldState.meta as any).isUnited ?? 0,
|
||||||
|
myGeneral,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}),
|
||||||
battle: router({
|
battle: router({
|
||||||
simulate: procedure
|
simulate: procedure
|
||||||
.input(zBattleSimRequest)
|
.input(zBattleSimRequest)
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ export const createInMemoryUserRepository = (
|
|||||||
const usersByEmail = new Map<string, UserRecord>();
|
const usersByEmail = new Map<string, UserRecord>();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
async findById(id: string): Promise<UserRecord | null> {
|
||||||
|
for (const user of usersByName.values()) {
|
||||||
|
if (user.id === id) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
async findByUsername(username: string): Promise<UserRecord | null> {
|
async findByUsername(username: string): Promise<UserRecord | null> {
|
||||||
return usersByName.get(username) ?? null;
|
return usersByName.get(username) ?? null;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -56,6 +56,14 @@ export const createPostgresUserRepository = (
|
|||||||
hasher: PasswordHasher = createSimplePasswordHasher()
|
hasher: PasswordHasher = createSimplePasswordHasher()
|
||||||
): UserRepository => {
|
): UserRepository => {
|
||||||
return {
|
return {
|
||||||
|
async findById(id: string): Promise<UserRecord | null> {
|
||||||
|
const row = await prisma.appUser.findUnique({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return row ? mapUser(row) : null;
|
||||||
|
},
|
||||||
async findByUsername(username: string): Promise<UserRecord | null> {
|
async findByUsername(username: string): Promise<UserRecord | null> {
|
||||||
const row = await prisma.appUser.findUnique({
|
const row = await prisma.appUser.findUnique({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export interface CreateUserInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UserRepository {
|
export interface UserRepository {
|
||||||
|
findById(id: string): Promise<UserRecord | null>;
|
||||||
findByUsername(username: string): Promise<UserRecord | null>;
|
findByUsername(username: string): Promise<UserRecord | null>;
|
||||||
findByOauthId(type: 'KAKAO', oauthId: string): Promise<UserRecord | null>;
|
findByOauthId(type: 'KAKAO', oauthId: string): Promise<UserRecord | null>;
|
||||||
findByEmail(email: string): Promise<UserRecord | null>;
|
findByEmail(email: string): Promise<UserRecord | null>;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { OAuthSessionStore } from './auth/oauthSessionStore.js';
|
|||||||
import type { GatewayProfileRepository } from './orchestrator/profileRepository.js';
|
import type { GatewayProfileRepository } from './orchestrator/profileRepository.js';
|
||||||
import type { GatewayOrchestratorHandle } from './orchestrator/gatewayOrchestrator.js';
|
import type { GatewayOrchestratorHandle } from './orchestrator/gatewayOrchestrator.js';
|
||||||
import type { GatewayProfileStatusService } from './lobby/profileStatusService.js';
|
import type { GatewayProfileStatusService } from './lobby/profileStatusService.js';
|
||||||
|
import type { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
export interface GatewayApiContext {
|
export interface GatewayApiContext {
|
||||||
users: UserRepository;
|
users: UserRepository;
|
||||||
@@ -21,6 +22,7 @@ export interface GatewayApiContext {
|
|||||||
profileStatus: GatewayProfileStatusService;
|
profileStatus: GatewayProfileStatusService;
|
||||||
adminToken?: string;
|
adminToken?: string;
|
||||||
requestHeaders: Record<string, string | string[] | undefined>;
|
requestHeaders: Record<string, string | string[] | undefined>;
|
||||||
|
prisma: PrismaClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createGatewayApiContext = (options: {
|
export const createGatewayApiContext = (options: {
|
||||||
@@ -37,6 +39,7 @@ export const createGatewayApiContext = (options: {
|
|||||||
profileStatus: GatewayProfileStatusService;
|
profileStatus: GatewayProfileStatusService;
|
||||||
adminToken?: string;
|
adminToken?: string;
|
||||||
requestHeaders?: Record<string, string | string[] | undefined>;
|
requestHeaders?: Record<string, string | string[] | undefined>;
|
||||||
|
prisma: PrismaClient;
|
||||||
}): GatewayApiContext => ({
|
}): GatewayApiContext => ({
|
||||||
users: options.users,
|
users: options.users,
|
||||||
sessions: options.sessions,
|
sessions: options.sessions,
|
||||||
@@ -51,4 +54,5 @@ export const createGatewayApiContext = (options: {
|
|||||||
profileStatus: options.profileStatus,
|
profileStatus: options.profileStatus,
|
||||||
adminToken: options.adminToken,
|
adminToken: options.adminToken,
|
||||||
requestHeaders: options.requestHeaders ?? {},
|
requestHeaders: options.requestHeaders ?? {},
|
||||||
|
prisma: options.prisma,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,12 +22,13 @@ export type LobbyProfileStatus = {
|
|||||||
profile: string;
|
profile: string;
|
||||||
scenario: string;
|
scenario: string;
|
||||||
status: GatewayProfileStatus;
|
status: GatewayProfileStatus;
|
||||||
|
apiPort: number;
|
||||||
runtime: {
|
runtime: {
|
||||||
apiRunning: boolean;
|
apiRunning: boolean;
|
||||||
daemonRunning: boolean;
|
daemonRunning: boolean;
|
||||||
};
|
};
|
||||||
map: LobbyMapSnapshot;
|
korName: string;
|
||||||
myGeneral: LobbyGeneralStatus;
|
color: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface GatewayProfileStatusService {
|
export interface GatewayProfileStatusService {
|
||||||
@@ -73,25 +74,19 @@ export class RepositoryProfileStatusService implements GatewayProfileStatusServi
|
|||||||
row: GatewayProfileRecord,
|
row: GatewayProfileRecord,
|
||||||
runtimeMap: Map<string, { apiRunning: boolean; daemonRunning: boolean }>
|
runtimeMap: Map<string, { apiRunning: boolean; daemonRunning: boolean }>
|
||||||
): LobbyProfileStatus {
|
): LobbyProfileStatus {
|
||||||
|
const meta = row.meta as Record<string, any>;
|
||||||
return {
|
return {
|
||||||
profileName: row.profileName,
|
profileName: row.profileName,
|
||||||
profile: row.profile,
|
profile: row.profile,
|
||||||
scenario: row.scenario,
|
scenario: row.scenario,
|
||||||
status: row.status,
|
status: row.status,
|
||||||
|
apiPort: row.apiPort,
|
||||||
runtime: runtimeMap.get(row.profileName) ?? {
|
runtime: runtimeMap.get(row.profileName) ?? {
|
||||||
apiRunning: false,
|
apiRunning: false,
|
||||||
daemonRunning: false,
|
daemonRunning: false,
|
||||||
},
|
},
|
||||||
map: {
|
korName: (meta.korName as string) ?? row.profile,
|
||||||
updatedAt: null,
|
color: (meta.color as string) ?? '#ffffff',
|
||||||
summary: null,
|
|
||||||
},
|
|
||||||
myGeneral: {
|
|
||||||
exists: false,
|
|
||||||
cityId: null,
|
|
||||||
cityName: null,
|
|
||||||
updatedAt: null,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,21 @@ export const appRouter = router({
|
|||||||
now: new Date().toISOString(),
|
now: new Date().toISOString(),
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
|
me: procedure.query(async ({ ctx }) => {
|
||||||
|
const sessionToken = ctx.requestHeaders['x-session-token'] as string | undefined;
|
||||||
|
if (!sessionToken) return null;
|
||||||
|
const session = await ctx.sessions.getSession(sessionToken);
|
||||||
|
if (!session) return null;
|
||||||
|
const user = await ctx.users.findById(session.userId);
|
||||||
|
return user ? toPublicUser(user) : null;
|
||||||
|
}),
|
||||||
lobby: router({
|
lobby: router({
|
||||||
|
notice: procedure.query(async ({ ctx }) => {
|
||||||
|
const setting = await ctx.prisma.systemSetting.findUnique({
|
||||||
|
where: { id: 1 },
|
||||||
|
});
|
||||||
|
return setting?.notice ?? '';
|
||||||
|
}),
|
||||||
profiles: procedure
|
profiles: procedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export const createGatewayApiServer = async () => {
|
|||||||
profileStatus,
|
profileStatus,
|
||||||
adminToken: config.adminToken,
|
adminToken: config.adminToken,
|
||||||
requestHeaders: req.headers,
|
requestHeaders: req.headers,
|
||||||
|
prisma: postgres.prisma as PrismaClient,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
import HomeView from '../views/HomeView.vue';
|
import HomeView from '../views/HomeView.vue';
|
||||||
|
import LobbyView from '../views/LobbyView.vue';
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
@@ -9,7 +10,11 @@ const router = createRouter({
|
|||||||
name: 'home',
|
name: 'home',
|
||||||
component: HomeView,
|
component: HomeView,
|
||||||
},
|
},
|
||||||
// 추후 추가될 페이지들
|
{
|
||||||
|
path: '/lobby',
|
||||||
|
name: 'lobby',
|
||||||
|
component: LobbyView,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
|
||||||
|
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`, // 실제 환경에서는 도메인/경로 조정 필요
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,13 +1,33 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import DefaultLayout from '../layouts/DefaultLayout.vue';
|
import DefaultLayout from '../layouts/DefaultLayout.vue';
|
||||||
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const username = ref('');
|
const username = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
|
|
||||||
const handleLogin = () => {
|
onMounted(async () => {
|
||||||
console.log('Login attempt:', username.value);
|
try {
|
||||||
// tRPC 호출 로직이 들어갈 자리
|
const me = await trpc.me.query();
|
||||||
|
if (me) {
|
||||||
|
router.push('/lobby');
|
||||||
|
}
|
||||||
|
} 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('로그인 실패');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleJoin = () => {
|
const handleJoin = () => {
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import DefaultLayout from '../layouts/DefaultLayout.vue';
|
||||||
|
import { trpc } from '../utils/trpc';
|
||||||
|
import { createGameTrpc } from '../utils/gameTrpc';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const me = ref<any>(null);
|
||||||
|
const notice = ref('');
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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('/');
|
||||||
|
};
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- 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}` : ''"
|
||||||
|
>
|
||||||
|
{{ 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>)
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
@@ -148,6 +148,7 @@ model City {
|
|||||||
|
|
||||||
model General {
|
model General {
|
||||||
id Int @id
|
id Int @id
|
||||||
|
userId String? @map("user_id")
|
||||||
name String
|
name String
|
||||||
nationId Int @default(0) @map("nation_id")
|
nationId Int @default(0) @map("nation_id")
|
||||||
cityId Int @default(0) @map("city_id")
|
cityId Int @default(0) @map("city_id")
|
||||||
@@ -271,3 +272,10 @@ model LogEntry {
|
|||||||
@@index([userId, category, id])
|
@@index([userId, category, id])
|
||||||
@@map("log_entry")
|
@@map("log_entry")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model SystemSetting {
|
||||||
|
id Int @id @default(1) @map("no")
|
||||||
|
notice String @default("") @map("notice")
|
||||||
|
|
||||||
|
@@map("system")
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,12 +15,20 @@ export interface DatabaseClient<
|
|||||||
};
|
};
|
||||||
general: {
|
general: {
|
||||||
findUnique(args: { where: { id: number } }): Promise<GeneralRow | null>;
|
findUnique(args: { where: { id: number } }): Promise<GeneralRow | null>;
|
||||||
|
findFirst(args: {
|
||||||
|
where: { userId?: string; npcState?: number | { gt: number } };
|
||||||
|
select?: { name?: boolean; picture?: boolean };
|
||||||
|
}): Promise<GeneralRow | null>;
|
||||||
|
count(args?: {
|
||||||
|
where?: { npcState?: number | { gt: number } };
|
||||||
|
}): Promise<number>;
|
||||||
};
|
};
|
||||||
city: {
|
city: {
|
||||||
findUnique(args: { where: { id: number } }): Promise<CityRow | null>;
|
findUnique(args: { where: { id: number } }): Promise<CityRow | null>;
|
||||||
};
|
};
|
||||||
nation: {
|
nation: {
|
||||||
findUnique(args: { where: { id: number } }): Promise<NationRow | null>;
|
findUnique(args: { where: { id: number } }): Promise<NationRow | null>;
|
||||||
|
count(args?: { where?: { level?: { gt: number } } }): Promise<number>;
|
||||||
};
|
};
|
||||||
generalTurn: {
|
generalTurn: {
|
||||||
findMany(args: {
|
findMany(args: {
|
||||||
|
|||||||
Reference in New Issue
Block a user