diff --git a/app/game-api/src/context.ts b/app/game-api/src/context.ts index 71e42f1..6ea53d8 100644 --- a/app/game-api/src/context.ts +++ b/app/game-api/src/context.ts @@ -1,3 +1,4 @@ +import { z } from 'zod'; import type { GameSessionTokenPayload } from '@sammo-ts/common'; import type { DatabaseClient as InfraDatabaseClient, RedisConnector } from '@sammo-ts/infra'; @@ -10,6 +11,21 @@ export interface GameProfile { name: string; } +export const zWorldStateConfig = z.object({ + maxUserCnt: z.number().optional(), + fictionMode: z.string().optional(), +}); +export type WorldStateConfig = z.infer; + +export const zWorldStateMeta = z.object({ + starttime: z.string().optional(), + opentime: z.string().optional(), + turntime: z.string().optional(), + otherTextInfo: z.string().optional(), + isUnited: z.number().optional(), +}); +export type WorldStateMeta = z.infer; + export interface WorldStateRow { scenarioCode: string; currentYear: number; @@ -22,7 +38,7 @@ export interface WorldStateRow { export interface GeneralRow { id: number; - userId: number | null; + userId: string | null; name: string; nationId: number; cityId: number; @@ -49,6 +65,7 @@ export interface GeneralRow { atmos: number; age: number; npcState: number; + picture: string | null; meta: unknown; } diff --git a/app/game-api/src/router.ts b/app/game-api/src/router.ts index 48bf5e6..e6f89ee 100644 --- a/app/game-api/src/router.ts +++ b/app/game-api/src/router.ts @@ -1,7 +1,8 @@ import { TRPCError } from '@trpc/server'; import { z } from 'zod'; -import type { WorldStateRow } from './context.js'; +import { zWorldStateConfig, zWorldStateMeta } from './context.js'; +import type { GameApiContext, WorldStateRow } from './context.js'; import { authedProcedure, procedure, router } from './trpc.js'; import { buildTurnCommandTable } from './turns/commandTable.js'; import { @@ -67,12 +68,12 @@ const toWorldStateSnapshot = (row: WorldStateRow) => ({ updatedAt: row.updatedAt.toISOString(), }); -const getMyGeneral = async (ctx: { db: any, auth: any }) => { +const getMyGeneral = async (ctx: Pick) => { if (!ctx.auth?.user.id) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } const general = await ctx.db.general.findFirst({ - where: { userId: parseInt(ctx.auth.user.id) }, + where: { userId: ctx.auth.user.id }, }); if (!general) { throw new TRPCError({ code: 'NOT_FOUND', message: 'General not found' }); @@ -90,14 +91,20 @@ export const appRouter = router({ }), lobby: router({ info: procedure.query(async ({ ctx }) => { - const worldState = await ctx.db.worldState.findFirst(); - if (!worldState) { + const rawWorldState = await ctx.db.worldState.findFirst(); + if (!rawWorldState) { throw new TRPCError({ code: 'NOT_FOUND', message: 'World state not found', }); } + const worldState = { + ...rawWorldState, + config: zWorldStateConfig.parse(rawWorldState.config), + meta: zWorldStateMeta.parse(rawWorldState.meta), + }; + 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 } } }); @@ -111,8 +118,8 @@ export const appRouter = router({ }); if (general) { myGeneral = { - name: (general as any).name, - picture: (general as any).picture, + name: general.name, + picture: general.picture, }; } } @@ -121,16 +128,16 @@ export const appRouter = router({ year: worldState.currentYear, month: worldState.currentMonth, userCnt, - maxUserCnt: (worldState.config as any).maxUserCnt ?? 500, + maxUserCnt: worldState.config.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, + fictionMode: worldState.config.fictionMode ?? '사실', + starttime: worldState.meta.starttime ?? '', + opentime: worldState.meta.opentime ?? '', + turntime: worldState.meta.turntime ?? '', + otherTextInfo: worldState.meta.otherTextInfo ?? '', + isUnited: worldState.meta.isUnited ?? 0, myGeneral, }; }), diff --git a/app/game-api/src/server.ts b/app/game-api/src/server.ts index 81db134..9bdc42f 100644 --- a/app/game-api/src/server.ts +++ b/app/game-api/src/server.ts @@ -84,7 +84,7 @@ export const createGameApiServer = async () => { const token = extractBearerToken(req.headers.authorization); const auth = token ? tokenVerifier.verify(token) : null; return createGameApiContext({ - db: postgres.prisma as unknown as DatabaseClient, + db: postgres.prisma, redis: redis.client, turnDaemon, battleSim, diff --git a/app/game-engine/src/scenario/scenarioSeeder.ts b/app/game-engine/src/scenario/scenarioSeeder.ts index 8991548..b80ffe6 100644 --- a/app/game-engine/src/scenario/scenarioSeeder.ts +++ b/app/game-engine/src/scenario/scenarioSeeder.ts @@ -1,7 +1,6 @@ import { createGamePostgresConnector, type InputJsonValue, - type TurnEngineDatabaseClient, type TurnEngineEventCreateManyInput, } from '@sammo-ts/infra'; import { @@ -125,7 +124,7 @@ export const seedScenarioToDatabase = async ( await connector.connect(); try { - const prisma = connector.prisma as unknown as TurnEngineDatabaseClient; + const prisma = connector.prisma; if (options.resetTables ?? true) { await prisma.event.deleteMany(); diff --git a/app/game-engine/src/turn/databaseHooks.ts b/app/game-engine/src/turn/databaseHooks.ts index dd06789..68d848e 100644 --- a/app/game-engine/src/turn/databaseHooks.ts +++ b/app/game-engine/src/turn/databaseHooks.ts @@ -2,7 +2,6 @@ import { createGamePostgresConnector, type InputJsonValue, type TurnEngineCityUpdateInput, - type TurnEngineDatabaseClient, type TurnEngineDiplomacyCreateManyInput, type TurnEngineDiplomacyUpdateInput, type TurnEngineGeneralCreateManyInput, @@ -238,7 +237,7 @@ export const createDatabaseTurnHooks = async ( // 턴 처리 결과를 DB에 반영하는 훅을 만든다. const connector = createGamePostgresConnector({ url: databaseUrl }); await connector.connect(); - const prisma = connector.prisma as unknown as TurnEngineDatabaseClient; + const prisma = connector.prisma; const hooks: TurnDaemonHooks = { flushChanges: async () => { @@ -346,8 +345,10 @@ export const createDatabaseTurnHooks = async ( .map((entry) => prisma.diplomacy.update({ where: { - srcNationId: entry.fromNationId, - destNationId: entry.toNationId, + srcNationId_destNationId: { + srcNationId: entry.fromNationId, + destNationId: entry.toNationId, + }, }, data: buildDiplomacyUpdate(entry), }) diff --git a/app/game-engine/src/turn/gatewayAdminActions.ts b/app/game-engine/src/turn/gatewayAdminActions.ts index 5ffb865..97f5681 100644 --- a/app/game-engine/src/turn/gatewayAdminActions.ts +++ b/app/game-engine/src/turn/gatewayAdminActions.ts @@ -36,15 +36,6 @@ export interface GatewayAdminActionConsumer { stop(): Promise; } -type GatewayProfileRow = { - meta: unknown; -}; - -type GatewayProfileClient = { - findUnique(args: unknown): Promise; - update(args: unknown): Promise; -}; - const DEFAULT_POLL_MS = 5000; const isRecord = (value: unknown): value is Record => @@ -75,9 +66,7 @@ export const createGatewayAdminActionConsumer = async ( url: options.gatewayDatabaseUrl ?? options.databaseUrl, }); await connector.connect(); - const prisma = connector.prisma as unknown as { - gatewayProfile: GatewayProfileClient; - }; + const prisma = connector.prisma; let timer: NodeJS.Timeout | null = null; let inFlight = false; diff --git a/app/game-engine/src/turn/gatewayProfileGate.ts b/app/game-engine/src/turn/gatewayProfileGate.ts index 0c2bd90..8343d01 100644 --- a/app/game-engine/src/turn/gatewayProfileGate.ts +++ b/app/game-engine/src/turn/gatewayProfileGate.ts @@ -18,15 +18,6 @@ const DEFAULT_CACHE_MS = 2000; const isRunningStatus = (status: string | null | undefined): boolean => status === 'RUNNING'; -type GatewayProfileRow = { - status: string | null; -}; - -type GatewayProfileClient = { - findUnique(args: unknown): Promise; - update(args: unknown): Promise; -}; - export const createGatewayProfileGate = async ( options: GatewayProfileGateOptions ): Promise => { @@ -34,9 +25,7 @@ export const createGatewayProfileGate = async ( url: options.gatewayDatabaseUrl ?? options.databaseUrl, }); await connector.connect(); - const prisma = connector.prisma as unknown as { - gatewayProfile: GatewayProfileClient; - }; + const prisma = connector.prisma; let lastCheckedAt = 0; let cachedPause = false; diff --git a/app/game-engine/src/turn/reservedTurnStore.ts b/app/game-engine/src/turn/reservedTurnStore.ts index 0847cb6..b075aed 100644 --- a/app/game-engine/src/turn/reservedTurnStore.ts +++ b/app/game-engine/src/turn/reservedTurnStore.ts @@ -272,7 +272,7 @@ export const createReservedTurnStore = async ( const connector = createGamePostgresConnector({ url: options.databaseUrl }); await connector.connect(); const store = new InMemoryReservedTurnStore( - connector.prisma as unknown as ReservedTurnDatabaseClient, + connector.prisma, { maxGeneralTurns: options.maxGeneralTurns ?? DEFAULT_GENERAL_TURNS, maxNationTurns: options.maxNationTurns ?? DEFAULT_NATION_TURNS, diff --git a/app/game-engine/src/turn/worldLoader.ts b/app/game-engine/src/turn/worldLoader.ts index 3a82501..287cf1b 100644 --- a/app/game-engine/src/turn/worldLoader.ts +++ b/app/game-engine/src/turn/worldLoader.ts @@ -255,7 +255,7 @@ export const loadTurnWorldFromDatabase = async ( const connector = createGamePostgresConnector({ url: options.databaseUrl }); await connector.connect(); try { - const prisma = connector.prisma as unknown as TurnEngineDatabaseClient; + const prisma: TurnEngineDatabaseClient = connector.prisma; const worldState = await prisma.worldState.findFirst(); if (!worldState) { throw new Error('world_state row is required to start turn daemon.'); diff --git a/app/gateway-api/src/lobby/profileStatusService.ts b/app/gateway-api/src/lobby/profileStatusService.ts index b95a7e3..c18e53b 100644 --- a/app/gateway-api/src/lobby/profileStatusService.ts +++ b/app/gateway-api/src/lobby/profileStatusService.ts @@ -74,7 +74,7 @@ export class RepositoryProfileStatusService implements GatewayProfileStatusServi row: GatewayProfileRecord, runtimeMap: Map ): LobbyProfileStatus { - const meta = row.meta as Record; + const meta = row.meta; return { profileName: row.profileName, profile: row.profile, @@ -85,8 +85,8 @@ export class RepositoryProfileStatusService implements GatewayProfileStatusServi apiRunning: false, daemonRunning: false, }, - korName: (meta.korName as string) ?? row.profile, - color: (meta.color as string) ?? '#ffffff', + korName: (meta.korName as string | undefined) ?? row.profile, + color: (meta.color as string | undefined) ?? '#ffffff', }; } } diff --git a/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts b/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts index 3121670..f0dbd6d 100644 --- a/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts +++ b/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts @@ -587,15 +587,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle { const connector = createGamePostgresConnector({ url: databaseUrl }); await connector.connect(); try { - const prisma = connector.prisma as unknown as { - worldState: { - findFirst: (args: unknown) => Promise<{ - scenarioCode: string | null; - tickSeconds: number | null; - } | null>; - }; - }; - const row = await prisma.worldState.findFirst({ + const row = await connector.prisma.worldState.findFirst({ select: { scenarioCode: true, tickSeconds: true }, }); if (row) { diff --git a/app/gateway-api/src/orchestrator/profileRepository.ts b/app/gateway-api/src/orchestrator/profileRepository.ts index ba89bf7..9b4e2ae 100644 --- a/app/gateway-api/src/orchestrator/profileRepository.ts +++ b/app/gateway-api/src/orchestrator/profileRepository.ts @@ -122,15 +122,6 @@ type GatewayProfileRow = { updatedAt: Date; }; -type GatewayProfileClient = { - findMany(args: unknown): Promise; - findUnique(args: unknown): Promise; - findFirst(args: unknown): Promise; - upsert(args: unknown): Promise; - update(args: unknown): Promise; - updateMany(args: unknown): Promise; -}; - const mapProfile = (row: GatewayProfileRow): GatewayProfileRecord => ({ profileName: row.profileName, profile: row.profile, @@ -161,23 +152,20 @@ export const createGatewayProfileRepository = ( prisma: GatewayPrismaClient ): GatewayProfileRepository => ({ async listProfiles(): Promise { - const gatewayProfile = prisma.gatewayProfile as unknown as GatewayProfileClient; - const rows = await gatewayProfile.findMany({ + const rows = await prisma.gatewayProfile.findMany({ orderBy: [{ profile: 'asc' }, { scenario: 'asc' }], }); return rows.map(mapProfile); }, async getProfile(profileName: string): Promise { - const gatewayProfile = prisma.gatewayProfile as unknown as GatewayProfileClient; - const row = await gatewayProfile.findUnique({ + const row = await prisma.gatewayProfile.findUnique({ where: { profileName }, }); return row ? mapProfile(row) : null; }, async upsertProfile(input: GatewayProfileUpsertInput): Promise { - const gatewayProfile = prisma.gatewayProfile as unknown as GatewayProfileClient; const profileName = buildProfileName(input.profile, input.scenario); - const row = await gatewayProfile.upsert({ + const row = await prisma.gatewayProfile.upsert({ where: { profileName }, create: { profileName, @@ -229,7 +217,7 @@ export const createGatewayProfileRepository = ( scheduledStartAt?: string | null; } ): Promise { - const gatewayProfile = prisma.gatewayProfile as unknown as GatewayProfileClient; + const gatewayProfile = prisma.gatewayProfile; const row = await gatewayProfile.update({ where: { profileName }, data: { @@ -269,7 +257,7 @@ export const createGatewayProfileRepository = ( lastUsedAt?: string | null; } ): Promise { - const gatewayProfile = prisma.gatewayProfile as unknown as GatewayProfileClient; + const gatewayProfile = prisma.gatewayProfile; const row = await gatewayProfile.update({ where: { profileName }, data: { @@ -311,7 +299,7 @@ export const createGatewayProfileRepository = ( profileName: string, meta: Record ): Promise { - const gatewayProfile = prisma.gatewayProfile as unknown as GatewayProfileClient; + const gatewayProfile = prisma.gatewayProfile; const row = await gatewayProfile.update({ where: { profileName }, data: { @@ -321,7 +309,7 @@ export const createGatewayProfileRepository = ( return row ? mapProfile(row) : null; }, async listReservedToStart(now: Date): Promise { - const gatewayProfile = prisma.gatewayProfile as unknown as GatewayProfileClient; + const gatewayProfile = prisma.gatewayProfile; const rows = await gatewayProfile.findMany({ where: { status: 'RESERVED', @@ -333,7 +321,7 @@ export const createGatewayProfileRepository = ( return rows.map(mapProfile); }, async findQueuedBuild(): Promise { - const gatewayProfile = prisma.gatewayProfile as unknown as GatewayProfileClient; + const gatewayProfile = prisma.gatewayProfile; const row = await gatewayProfile.findFirst({ where: { buildStatus: 'QUEUED' }, orderBy: { buildRequestedAt: 'asc' }, @@ -341,7 +329,7 @@ export const createGatewayProfileRepository = ( return row ? mapProfile(row) : null; }, async updateLastError(profileName: string, lastError: string | null): Promise { - const gatewayProfile = prisma.gatewayProfile as unknown as GatewayProfileClient; + const gatewayProfile = prisma.gatewayProfile; await gatewayProfile.update({ where: { profileName }, data: { lastError }, @@ -352,7 +340,7 @@ export const createGatewayProfileRepository = ( workspace: string, lastUsedAt: string ): Promise { - const gatewayProfile = prisma.gatewayProfile as unknown as GatewayProfileClient; + const gatewayProfile = prisma.gatewayProfile; await gatewayProfile.update({ where: { profileName }, data: { @@ -365,7 +353,7 @@ export const createGatewayProfileRepository = ( if (!profileNames.length) { return; } - const gatewayProfile = prisma.gatewayProfile as unknown as GatewayProfileClient; + const gatewayProfile = prisma.gatewayProfile; await gatewayProfile.updateMany({ where: { profileName: { in: profileNames }, diff --git a/app/gateway-frontend/src/stores/auth.ts b/app/gateway-frontend/src/stores/auth.ts index bf0168c..76d3895 100644 --- a/app/gateway-frontend/src/stores/auth.ts +++ b/app/gateway-frontend/src/stores/auth.ts @@ -1,11 +1,18 @@ import { defineStore } from 'pinia'; import { ref } from 'vue'; +export interface UserInfo { + id: string; + username: string; + displayName: string; + roles: string[]; +} + export const useAuthStore = defineStore('auth', () => { - const user = ref(null); + const user = ref(null); const isLoggedIn = ref(false); - function setUser(userData: any) { + function setUser(userData: UserInfo | null) { user.value = userData; isLoggedIn.value = !!userData; } diff --git a/packages/infra/src/turnEngineDb.ts b/packages/infra/src/turnEngineDb.ts index 0a21fa3..62c2a1d 100644 --- a/packages/infra/src/turnEngineDb.ts +++ b/packages/infra/src/turnEngineDb.ts @@ -1,12 +1,7 @@ -export type JsonValue = - | null - | boolean - | number - | string - | JsonValue[] - | { [key: string]: JsonValue }; +import type { GamePrisma, LogCategory, LogScope } from './gamePrisma.js'; -export type InputJsonValue = JsonValue; +export type JsonValue = GamePrisma.JsonValue; +export type InputJsonValue = GamePrisma.InputJsonValue; export interface TurnEngineWorldStateRow { id: number; @@ -148,7 +143,7 @@ export interface TurnEngineGeneralUpdateInput { name: string; nationId: number; cityId: number; - troopId: number | null; + troopId: number; leadership: number; strength: number; intel: number; @@ -181,7 +176,7 @@ export interface TurnEngineGeneralCreateManyInput { name: string; nationId: number; cityId: number; - troopId?: number | null; + troopId?: number; npcState: number; leadership: number; strength: number; @@ -323,8 +318,8 @@ export interface TurnEngineEventCreateManyInput { } export interface TurnEngineLogEntryCreateManyInput { - scope: string; - category: string; + scope: LogScope; + category: LogCategory; subType: string | null; year: number; month: number; @@ -387,7 +382,12 @@ export interface TurnEngineDatabaseClient { data: TurnEngineDiplomacyCreateManyInput[]; }): Promise; update(args: { - where: { srcNationId: number; destNationId: number }; + where: { + srcNationId_destNationId: { + srcNationId: number; + destNationId: number; + }; + }; data: TurnEngineDiplomacyUpdateInput; }): Promise; deleteMany(args?: unknown): Promise; @@ -416,36 +416,13 @@ export interface TurnEngineDatabaseClient { }): Promise; }; generalTurn: { - findMany(args?: { - where?: { generalId?: number }; - orderBy?: { turnIdx: 'asc' | 'desc' }[]; - }): Promise; - deleteMany(args: { where: { generalId: number } }): Promise; - createMany(args: { - data: Array<{ - generalId: number; - turnIdx: number; - actionCode: string; - arg: InputJsonValue; - }>; - }): Promise; + findMany(args?: unknown): Promise; + deleteMany(args?: unknown): Promise; + createMany(args?: unknown): Promise; }; nationTurn: { - findMany(args?: { - where?: { nationId?: number; officerLevel?: number }; - orderBy?: { turnIdx: 'asc' | 'desc' }[]; - }): Promise; - deleteMany(args: { - where: { nationId: number; officerLevel: number }; - }): Promise; - createMany(args: { - data: Array<{ - nationId: number; - officerLevel: number; - turnIdx: number; - actionCode: string; - arg: InputJsonValue; - }>; - }): Promise; + findMany(args?: unknown): Promise; + deleteMany(args?: unknown): Promise; + createMany(args?: unknown): Promise; }; } diff --git a/packages/logic/src/actions/turn/commandModule.ts b/packages/logic/src/actions/turn/commandModule.ts index 44cef8f..f71bf55 100644 --- a/packages/logic/src/actions/turn/commandModule.ts +++ b/packages/logic/src/actions/turn/commandModule.ts @@ -15,6 +15,6 @@ export interface TurnCommandModule GeneralActionDefinition; ActionResolver?: new (...args: any[]) => GeneralActionResolver; - CommandResolver?: new (...args: any[]) => unknown; + CommandResolver?: new (...args: any[]) => any; actionContextBuilder?: ActionContextBuilder; } diff --git a/packages/logic/src/actions/turn/general/cityDevelopment.ts b/packages/logic/src/actions/turn/general/cityDevelopment.ts index e9b07a4..781b070 100644 --- a/packages/logic/src/actions/turn/general/cityDevelopment.ts +++ b/packages/logic/src/actions/turn/general/cityDevelopment.ts @@ -29,11 +29,13 @@ export interface CityDevelopmentEnvironment { amount?: number; } +type NumberKeys = { [K in keyof T]: T[K] extends number ? K : never }[keyof T]; + export interface CityDevelopmentConfig { key: string; name: string; - statKey: keyof City; - maxKey: keyof City; + statKey: NumberKeys; + maxKey: NumberKeys; label: string; baseAmount: number; } @@ -74,8 +76,8 @@ export class CityDevelopmentActionDefinition< occupiedCity(), suppliedCity(), remainCityCapacityByMax( - String(this.config.statKey), - String(this.config.maxKey), + this.config.statKey, + this.config.maxKey, this.config.label ), reqGeneralGold(getRequiredGold), @@ -105,7 +107,7 @@ export class CityDevelopmentActionDefinition< const costGold = this.env.develCost ?? 0; // 직접 수정 (Immer Draft) - (city as any)[this.config.statKey] = nextValue; + city[this.config.statKey] = nextValue; general.gold = Math.max(0, general.gold - costGold); const logMessage = `${this.config.label}이 ${nextValue - current} 증가했습니다.`; diff --git a/packages/logic/src/constraints/city.ts b/packages/logic/src/constraints/city.ts index a2e46f6..6f55a43 100644 --- a/packages/logic/src/constraints/city.ts +++ b/packages/logic/src/constraints/city.ts @@ -143,7 +143,7 @@ export const suppliedDestCity = (): Constraint => ({ }); export const remainCityCapacity = ( - key: string, + key: keyof City, label: string ): Constraint => ({ name: 'RemainCityCapacity', @@ -158,11 +158,10 @@ export const remainCityCapacity = ( const req: RequirementKey = { kind: 'city', id: ctx.cityId }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } - const record = city as unknown as Record; - const maxKey = `${key}_max`; - const current = record[key]; - const max = record[maxKey]; - if (current === undefined || max === undefined) { + const maxKey = `${String(key)}Max` as keyof City; + const current = city[key]; + const max = city[maxKey]; + if (typeof current !== 'number' || typeof max !== 'number') { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } if (current < max) { @@ -173,8 +172,8 @@ export const remainCityCapacity = ( }); export const remainCityCapacityByMax = ( - key: string, - maxKey: string, + key: keyof City, + maxKey: keyof City, label: string ): Constraint => ({ name: 'RemainCityCapacityByMax', @@ -189,10 +188,9 @@ export const remainCityCapacityByMax = ( const req: RequirementKey = { kind: 'city', id: ctx.cityId }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } - const record = city as unknown as Record; - const current = record[key]; - const max = record[maxKey]; - if (current === undefined || max === undefined) { + const current = city[key]; + const max = city[maxKey]; + if (typeof current !== 'number' || typeof max !== 'number') { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } if (current < max) { @@ -203,7 +201,7 @@ export const remainCityCapacityByMax = ( }); export const reqCityCapacity = ( - key: string, + key: keyof City, label: string, required: number | string ): Constraint => ({ @@ -219,16 +217,15 @@ export const reqCityCapacity = ( const req: RequirementKey = { kind: 'city', id: ctx.cityId }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } - const record = city as unknown as Record; - const current = record[key]; - if (current === undefined) { + const current = city[key]; + if (typeof current !== 'number') { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } if (typeof required === 'string') { const ratio = parsePercent(required); - const maxKey = `${key}Max`; - const max = record[maxKey]; - if (ratio === null || max === undefined) { + const maxKey = `${String(key)}Max` as keyof City; + const max = city[maxKey]; + if (ratio === null || typeof max !== 'number') { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } if (current >= max * ratio) { @@ -256,7 +253,7 @@ export const reqCityTrust = (minTrust: number): Constraint => ({ } const trust = readMetaNumberFromUnknown( - city.meta as Record, + city.meta, 'trust' ) ?? null; if (trust === null) {