feat: implement lobby functionality with user and server information retrieval

This commit is contained in:
2026-01-03 13:35:16 +00:00
parent 1988e180c5
commit 67d995c65f
14 changed files with 328 additions and 17 deletions
+6 -1
View File
@@ -1,5 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';
import LobbyView from '../views/LobbyView.vue';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -9,7 +10,11 @@ const router = createRouter({
name: 'home',
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`, // 실제 환경에서는 도메인/경로 조정 필요
}),
],
});
};
+24 -4
View File
@@ -1,13 +1,33 @@
<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 { trpc } from '../utils/trpc';
const router = useRouter();
const username = ref('');
const password = ref('');
const handleLogin = () => {
console.log('Login attempt:', username.value);
// tRPC 호출 로직이 들어갈 자리
onMounted(async () => {
try {
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 = () => {
@@ -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">
&lt;{{ profileDetails[profile.profileName].nationCnt }} 경쟁중&gt;
</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>