diff --git a/app/gateway-api/src/context.ts b/app/gateway-api/src/context.ts index a1e7bb2..df199ed 100644 --- a/app/gateway-api/src/context.ts +++ b/app/gateway-api/src/context.ts @@ -5,6 +5,7 @@ import type { KakaoOAuthClient } from './auth/kakaoClient.js'; 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'; export interface GatewayApiContext { users: UserRepository; @@ -17,6 +18,7 @@ export interface GatewayApiContext { publicBaseUrl: string; profiles: GatewayProfileRepository; orchestrator: GatewayOrchestratorHandle; + profileStatus: GatewayProfileStatusService; adminToken?: string; requestHeaders: Record; } @@ -32,6 +34,7 @@ export const createGatewayApiContext = (options: { publicBaseUrl: string; profiles: GatewayProfileRepository; orchestrator: GatewayOrchestratorHandle; + profileStatus: GatewayProfileStatusService; adminToken?: string; requestHeaders?: Record; }): GatewayApiContext => ({ @@ -45,6 +48,7 @@ export const createGatewayApiContext = (options: { publicBaseUrl: options.publicBaseUrl, profiles: options.profiles, orchestrator: options.orchestrator, + profileStatus: options.profileStatus, adminToken: options.adminToken, requestHeaders: options.requestHeaders ?? {}, }); diff --git a/app/gateway-api/src/lobby/profileStatusService.ts b/app/gateway-api/src/lobby/profileStatusService.ts new file mode 100644 index 0000000..8eddf38 --- /dev/null +++ b/app/gateway-api/src/lobby/profileStatusService.ts @@ -0,0 +1,97 @@ +import type { GatewayOrchestratorHandle } from '../orchestrator/gatewayOrchestrator.js'; +import type { + GatewayProfileRecord, + GatewayProfileRepository, + GatewayProfileStatus, +} from '../orchestrator/profileRepository.js'; + +export type LobbyMapSnapshot = { + updatedAt: string | null; + summary?: string | null; +}; + +export type LobbyGeneralStatus = { + exists: boolean; + cityId: number | null; + cityName: string | null; + updatedAt: string | null; +}; + +export type LobbyProfileStatus = { + profileName: string; + profile: string; + scenario: string; + status: GatewayProfileStatus; + runtime: { + apiRunning: boolean; + daemonRunning: boolean; + }; + map: LobbyMapSnapshot; + myGeneral: LobbyGeneralStatus; +}; + +export interface GatewayProfileStatusService { + listLobbyProfiles(input: { userId?: string } | undefined): Promise; +} + +// 로비에서 사용할 프로필 상태를 메모리에서 반환하는 구현체. +export class InMemoryProfileStatusService implements GatewayProfileStatusService { + private profiles: LobbyProfileStatus[]; + + constructor(initial: LobbyProfileStatus[] = []) { + this.profiles = [...initial]; + } + + async listLobbyProfiles(): Promise { + return this.profiles; + } + + setProfiles(profiles: LobbyProfileStatus[]): void { + this.profiles = [...profiles]; + } +} + +// 프로필 저장소/오케스트레이터를 이용해 로비 상태를 구성하는 기본 구현체. +export class RepositoryProfileStatusService implements GatewayProfileStatusService { + constructor( + private readonly profiles: GatewayProfileRepository, + private readonly orchestrator: GatewayOrchestratorHandle + ) {} + + async listLobbyProfiles(): Promise { + const rows = await this.profiles.listProfiles(); + const runtimeStates = await this.orchestrator.listRuntimeStates( + rows.map((profile) => profile.profileName) + ); + const runtimeMap = new Map( + runtimeStates.map((state) => [state.profileName, state]) + ); + return rows.map((row) => this.mapProfile(row, runtimeMap)); + } + + private mapProfile( + row: GatewayProfileRecord, + runtimeMap: Map + ): LobbyProfileStatus { + return { + profileName: row.profileName, + profile: row.profile, + scenario: row.scenario, + status: row.status, + runtime: runtimeMap.get(row.profileName) ?? { + apiRunning: false, + daemonRunning: false, + }, + map: { + updatedAt: null, + summary: null, + }, + myGeneral: { + exists: false, + cityId: null, + cityName: null, + updatedAt: null, + }, + }; + } +} diff --git a/app/gateway-api/src/router.ts b/app/gateway-api/src/router.ts index 40bfb30..91f3178 100644 --- a/app/gateway-api/src/router.ts +++ b/app/gateway-api/src/router.ts @@ -28,6 +28,23 @@ export const appRouter = router({ now: new Date().toISOString(), })), }), + lobby: router({ + profiles: procedure + .input( + z.object({ + sessionToken: z.string().min(1).optional(), + }).optional() + ) + .query(async ({ ctx, input }) => { + const sessionToken = input?.sessionToken; + const session = sessionToken + ? await ctx.sessions.getSession(sessionToken) + : null; + return ctx.profileStatus.listLobbyProfiles({ + userId: session?.userId, + }); + }), + }), admin: adminRouter, auth: router({ kakaoStart: procedure diff --git a/app/gateway-api/src/server.ts b/app/gateway-api/src/server.ts index 0634a44..29c0dae 100644 --- a/app/gateway-api/src/server.ts +++ b/app/gateway-api/src/server.ts @@ -18,6 +18,7 @@ import { createPostgresUserRepository } from './auth/postgresUserRepository.js'; import { RedisGatewaySessionService } from './auth/redisSessionService.js'; import { createGatewayOrchestrator } from './orchestrator/orchestratorFactory.js'; import { appRouter } from './router.js'; +import { RepositoryProfileStatusService } from './lobby/profileStatusService.js'; export const createGatewayApiServer = async () => { const config = resolveGatewayApiConfigFromEnv(); @@ -51,6 +52,7 @@ export const createGatewayApiServer = async () => { config, process.env ); + const profileStatus = new RepositoryProfileStatusService(profiles, orchestrator); const app = fastify({ logger: true, @@ -77,6 +79,7 @@ export const createGatewayApiServer = async () => { publicBaseUrl: config.publicBaseUrl, profiles, orchestrator, + profileStatus, adminToken: config.adminToken, requestHeaders: req.headers, }), diff --git a/app/gateway-api/test/authFlow.test.ts b/app/gateway-api/test/authFlow.test.ts index 8349aaa..a8958cc 100644 --- a/app/gateway-api/test/authFlow.test.ts +++ b/app/gateway-api/test/authFlow.test.ts @@ -4,6 +4,7 @@ import { InMemoryGatewaySessionService } from '../src/auth/inMemorySessionServic import { createInMemoryUserRepository } from '../src/auth/inMemoryUserRepository.js'; import { InMemoryOAuthSessionStore } from '../src/auth/oauthSessionStore.js'; import { createGatewayApiContext } from '../src/context.js'; +import { InMemoryProfileStatusService } from '../src/lobby/profileStatusService.js'; import { appRouter } from '../src/router.js'; const buildCaller = () => { @@ -62,6 +63,7 @@ const buildCaller = () => { }), listRuntimeStates: async () => [], }; + const profileStatus = new InMemoryProfileStatusService(); const caller = appRouter.createCaller( createGatewayApiContext({ users, @@ -74,6 +76,7 @@ const buildCaller = () => { publicBaseUrl: 'http://localhost', profiles, orchestrator, + profileStatus, requestHeaders: {}, }) );