diff --git a/app/game-api/src/router.ts b/app/game-api/src/router.ts index ae60ac4..b58ee52 100644 --- a/app/game-api/src/router.ts +++ b/app/game-api/src/router.ts @@ -68,6 +68,53 @@ export const appRouter = router({ 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({ simulate: procedure .input(zBattleSimRequest) diff --git a/app/gateway-api/src/auth/inMemoryUserRepository.ts b/app/gateway-api/src/auth/inMemoryUserRepository.ts index 683907f..5bea0a3 100644 --- a/app/gateway-api/src/auth/inMemoryUserRepository.ts +++ b/app/gateway-api/src/auth/inMemoryUserRepository.ts @@ -12,6 +12,14 @@ export const createInMemoryUserRepository = ( const usersByEmail = new Map(); return { + async findById(id: string): Promise { + for (const user of usersByName.values()) { + if (user.id === id) { + return user; + } + } + return null; + }, async findByUsername(username: string): Promise { return usersByName.get(username) ?? null; }, diff --git a/app/gateway-api/src/auth/postgresUserRepository.ts b/app/gateway-api/src/auth/postgresUserRepository.ts index 53c65c0..6cac303 100644 --- a/app/gateway-api/src/auth/postgresUserRepository.ts +++ b/app/gateway-api/src/auth/postgresUserRepository.ts @@ -56,6 +56,14 @@ export const createPostgresUserRepository = ( hasher: PasswordHasher = createSimplePasswordHasher() ): UserRepository => { return { + async findById(id: string): Promise { + const row = await prisma.appUser.findUnique({ + where: { + id, + }, + }); + return row ? mapUser(row) : null; + }, async findByUsername(username: string): Promise { const row = await prisma.appUser.findUnique({ where: { diff --git a/app/gateway-api/src/auth/userRepository.ts b/app/gateway-api/src/auth/userRepository.ts index 89debbc..95287d1 100644 --- a/app/gateway-api/src/auth/userRepository.ts +++ b/app/gateway-api/src/auth/userRepository.ts @@ -51,6 +51,7 @@ export interface CreateUserInput { } export interface UserRepository { + findById(id: string): Promise; findByUsername(username: string): Promise; findByOauthId(type: 'KAKAO', oauthId: string): Promise; findByEmail(email: string): Promise; diff --git a/app/gateway-api/src/context.ts b/app/gateway-api/src/context.ts index df199ed..4cbbd41 100644 --- a/app/gateway-api/src/context.ts +++ b/app/gateway-api/src/context.ts @@ -6,6 +6,7 @@ import type { OAuthSessionStore } from './auth/oauthSessionStore.js'; import type { GatewayProfileRepository } from './orchestrator/profileRepository.js'; import type { GatewayOrchestratorHandle } from './orchestrator/gatewayOrchestrator.js'; import type { GatewayProfileStatusService } from './lobby/profileStatusService.js'; +import type { PrismaClient } from '@prisma/client'; export interface GatewayApiContext { users: UserRepository; @@ -21,6 +22,7 @@ export interface GatewayApiContext { profileStatus: GatewayProfileStatusService; adminToken?: string; requestHeaders: Record; + prisma: PrismaClient; } export const createGatewayApiContext = (options: { @@ -37,6 +39,7 @@ export const createGatewayApiContext = (options: { profileStatus: GatewayProfileStatusService; adminToken?: string; requestHeaders?: Record; + prisma: PrismaClient; }): GatewayApiContext => ({ users: options.users, sessions: options.sessions, @@ -51,4 +54,5 @@ export const createGatewayApiContext = (options: { profileStatus: options.profileStatus, adminToken: options.adminToken, requestHeaders: options.requestHeaders ?? {}, + prisma: options.prisma, }); diff --git a/app/gateway-api/src/lobby/profileStatusService.ts b/app/gateway-api/src/lobby/profileStatusService.ts index 8eddf38..b95a7e3 100644 --- a/app/gateway-api/src/lobby/profileStatusService.ts +++ b/app/gateway-api/src/lobby/profileStatusService.ts @@ -22,12 +22,13 @@ export type LobbyProfileStatus = { profile: string; scenario: string; status: GatewayProfileStatus; + apiPort: number; runtime: { apiRunning: boolean; daemonRunning: boolean; }; - map: LobbyMapSnapshot; - myGeneral: LobbyGeneralStatus; + korName: string; + color: string; }; export interface GatewayProfileStatusService { @@ -73,25 +74,19 @@ export class RepositoryProfileStatusService implements GatewayProfileStatusServi row: GatewayProfileRecord, runtimeMap: Map ): LobbyProfileStatus { + const meta = row.meta as Record; return { profileName: row.profileName, profile: row.profile, scenario: row.scenario, status: row.status, + apiPort: row.apiPort, runtime: runtimeMap.get(row.profileName) ?? { apiRunning: false, daemonRunning: false, }, - map: { - updatedAt: null, - summary: null, - }, - myGeneral: { - exists: false, - cityId: null, - cityName: null, - updatedAt: null, - }, + korName: (meta.korName as string) ?? row.profile, + color: (meta.color as string) ?? '#ffffff', }; } } diff --git a/app/gateway-api/src/router.ts b/app/gateway-api/src/router.ts index 91f3178..884e3d8 100644 --- a/app/gateway-api/src/router.ts +++ b/app/gateway-api/src/router.ts @@ -28,7 +28,21 @@ export const appRouter = router({ 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({ + notice: procedure.query(async ({ ctx }) => { + const setting = await ctx.prisma.systemSetting.findUnique({ + where: { id: 1 }, + }); + return setting?.notice ?? ''; + }), profiles: procedure .input( z.object({ diff --git a/app/gateway-api/src/server.ts b/app/gateway-api/src/server.ts index 29c0dae..f69c1d8 100644 --- a/app/gateway-api/src/server.ts +++ b/app/gateway-api/src/server.ts @@ -82,6 +82,7 @@ export const createGatewayApiServer = async () => { profileStatus, adminToken: config.adminToken, requestHeaders: req.headers, + prisma: postgres.prisma as PrismaClient, }), }, }); diff --git a/app/gateway-frontend/src/router/index.ts b/app/gateway-frontend/src/router/index.ts index c3ee030..531ff4f 100644 --- a/app/gateway-frontend/src/router/index.ts +++ b/app/gateway-frontend/src/router/index.ts @@ -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, + }, ], }); diff --git a/app/gateway-frontend/src/utils/gameTrpc.ts b/app/gateway-frontend/src/utils/gameTrpc.ts new file mode 100644 index 0000000..39bdfb5 --- /dev/null +++ b/app/gateway-frontend/src/utils/gameTrpc.ts @@ -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({ + links: [ + httpBatchLink({ + url: `http://localhost:${port}/api/trpc`, // 실제 환경에서는 도메인/경로 조정 필요 + }), + ], + }); +}; diff --git a/app/gateway-frontend/src/views/HomeView.vue b/app/gateway-frontend/src/views/HomeView.vue index 0463d7a..02f05dc 100644 --- a/app/gateway-frontend/src/views/HomeView.vue +++ b/app/gateway-frontend/src/views/HomeView.vue @@ -1,13 +1,33 @@ + + diff --git a/packages/infra/prisma/schema.prisma b/packages/infra/prisma/schema.prisma index 85cd7f9..36dc12a 100644 --- a/packages/infra/prisma/schema.prisma +++ b/packages/infra/prisma/schema.prisma @@ -148,6 +148,7 @@ model City { model General { id Int @id + userId String? @map("user_id") name String nationId Int @default(0) @map("nation_id") cityId Int @default(0) @map("city_id") @@ -271,3 +272,10 @@ model LogEntry { @@index([userId, category, id]) @@map("log_entry") } + +model SystemSetting { + id Int @id @default(1) @map("no") + notice String @default("") @map("notice") + + @@map("system") +} diff --git a/packages/infra/src/db.ts b/packages/infra/src/db.ts index ccb4288..3d8b7c4 100644 --- a/packages/infra/src/db.ts +++ b/packages/infra/src/db.ts @@ -15,12 +15,20 @@ export interface DatabaseClient< }; general: { findUnique(args: { where: { id: number } }): Promise; + findFirst(args: { + where: { userId?: string; npcState?: number | { gt: number } }; + select?: { name?: boolean; picture?: boolean }; + }): Promise; + count(args?: { + where?: { npcState?: number | { gt: number } }; + }): Promise; }; city: { findUnique(args: { where: { id: number } }): Promise; }; nation: { findUnique(args: { where: { id: number } }): Promise; + count(args?: { where?: { level?: { gt: number } } }): Promise; }; generalTurn: { findMany(args: {