@@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'
22import { Prisma } from 'generated/prisma/client'
33import { type Battle as PrismaBattle } from 'generated/prisma/client'
44import { PrismaService } from '../../../../prisma/prisma.service'
5+ import { RedisRepository } from '../../../../redis/redis.repository'
56import {
67 ActiveBattleState ,
78 BattleTeam ,
@@ -33,64 +34,63 @@ interface BattleChatSnapshot extends Omit<BattleChat, 'createdAt'> {
3334 createdAt : string
3435}
3536
37+ interface SerializedBattleState {
38+ battleId : string
39+ all : {
40+ roomId : string
41+ chats : BattleChatSnapshot [ ]
42+ attacks : ( BattleDiscussion | null ) [ ]
43+ defenses : ( BattleDefense | null ) [ ]
44+ }
45+ teamA : {
46+ roomId : string
47+ chats : BattleChatSnapshot [ ]
48+ attacks : ( BattleDiscussion | null ) [ ]
49+ defenses : ( BattleDefense | null ) [ ]
50+ users : string [ ]
51+ }
52+ teamB : {
53+ roomId : string
54+ chats : BattleChatSnapshot [ ]
55+ attacks : ( BattleDiscussion | null ) [ ]
56+ defenses : ( BattleDefense | null ) [ ]
57+ users : string [ ]
58+ }
59+ participants : [ string , BattleTeam ] [ ]
60+ teamVotes : [ string , BattleTeam ] [ ]
61+ userInfoMap : [ string , string ] [ ]
62+ opinionHistory : BattleDiscussion [ ]
63+ skipState : string [ ]
64+ round : number
65+ topics : string [ ]
66+ totalRounds : number
67+ phase : BattlePhaseName
68+ phaseCount : number
69+ startedAt : number | null
70+ expiredAt : number | null
71+ }
72+
3673@Injectable ( )
3774export class BattleStateRepositoryAdapter implements BattleStatePort {
38- constructor ( private readonly prisma : PrismaService ) { }
75+ private readonly cachePrefix = 'battle:state:'
76+ constructor (
77+ private readonly prisma : PrismaService ,
78+ private readonly redis : RedisRepository ,
79+ ) { }
3980
4081 async loadBattleState ( battleId : string ) : Promise < { battle : PrismaBattle ; state : ActiveBattleState } > {
82+ const cachedState = await this . loadStateFromCache ( battleId )
83+
4184 const battle = await this . prisma . battle . findUnique ( { where : { id : battleId } } )
4285 if ( ! battle ) throw new NotFoundException ( '배틀이 존재하지 않습니다.' )
4386
44- const participants = this . parseParticipantsState ( battle . participantsState )
45- const teamVotes = this . parseTeamVotesState ( battle . teamVotesState )
46- const userInfoMap = this . parseUserInfoState ( battle . userInfoState )
47- const attackState = this . parseAttackState ( battle . attacksState )
48- const defenseState = this . parseDefenseState ( battle . defensesState )
49- const opinionHistory = this . parseOpinionHistoryState ( battle . opinionHistoryState )
50- const skipState = new Set < string > ( Array . isArray ( battle . skipState ) ? battle . skipState : [ ] )
51-
52- const playTime = BATTLE_PLAYTIME [ battle . playTime as keyof typeof BATTLE_PLAYTIME ]
53- if ( ! playTime ) {
54- throw new NotFoundException ( '올바르지 않은 배틀 진행 시간입니다.' )
55- }
56-
57- const state : ActiveBattleState = {
58- battleId,
59- all : {
60- roomId : this . getBattleRoomId ( battleId ) ,
61- chats : this . parseChatState ( battle . chatsAllState ) ,
62- attacks : attackState . all ,
63- defenses : defenseState . all ,
64- } ,
65- teamA : {
66- roomId : this . getBattleRoomId ( battleId , BATTLE_TEAM . A ) ,
67- users : [ ] ,
68- chats : this . parseChatState ( battle . chatsTeamAState ) ,
69- attacks : attackState . teamA ,
70- defenses : defenseState . teamA ,
71- } ,
72- teamB : {
73- roomId : this . getBattleRoomId ( battleId , BATTLE_TEAM . B ) ,
74- users : [ ] ,
75- chats : this . parseChatState ( battle . chatsTeamBState ) ,
76- attacks : attackState . teamB ,
77- defenses : defenseState . teamB ,
78- } ,
79- participants,
80- teamVotes,
81- userInfoMap,
82- opinionHistory,
83- skipState,
84- round : battle . currentRound ?? 1 ,
85- topics : battle . topics ,
86- totalRounds : playTime . rounds ,
87- phase : ( battle . currentPhase ?? BATTLE_PHASE . PENDING . name ) as BattlePhaseName ,
88- phaseCount : battle . phaseCount ?? 1 ,
89- startedAt : battle . startedAt ? battle . startedAt . getTime ( ) : null ,
90- expiredAt : battle . expiredAt ? battle . expiredAt . getTime ( ) : null ,
87+ //캐시에서 가져온 데이터가 있으면 바로 반환
88+ if ( cachedState ) {
89+ return { battle, state : cachedState }
9190 }
9291
93- this . rebuildTeamUsers ( state )
92+ const state = this . buildStateFromBattle ( battle )
93+ await this . persistStateCache ( state )
9494 return { battle, state }
9595 }
9696
@@ -124,6 +124,7 @@ export class BattleStateRepositoryAdapter implements BattleStatePort {
124124 updatedAt : new Date ( ) ,
125125 } ,
126126 } )
127+ await this . persistStateCache ( state )
127128 }
128129
129130 async updateSkipState ( battleId : string , skipList : Set < string > ) : Promise < void > {
@@ -134,6 +135,11 @@ export class BattleStateRepositoryAdapter implements BattleStatePort {
134135 updatedAt : new Date ( ) ,
135136 } ,
136137 } )
138+ await this . patchSkipCache ( battleId , skipList )
139+ }
140+
141+ async clearCache ( battleId : string ) : Promise < void > {
142+ await this . redis . del ( this . getCacheKey ( battleId ) )
137143 }
138144
139145 parseMvpsState ( value : unknown ) : Mvp [ ] {
@@ -257,6 +263,171 @@ export class BattleStateRepositoryAdapter implements BattleStatePort {
257263 return value . filter ( item => item && typeof item === 'object' ) as BattleDiscussion [ ]
258264 }
259265
266+ //캐시에서 가져오기
267+ private async loadStateFromCache ( battleId : string ) : Promise < ActiveBattleState | null > {
268+ try {
269+ const payload = await this . redis . get ( this . getCacheKey ( battleId ) )
270+ if ( ! payload ) return null
271+ return this . deserializeState ( JSON . parse ( payload ) as SerializedBattleState )
272+ } catch {
273+ return null
274+ }
275+ }
276+
277+ //캐시에 저장하기
278+ private async persistStateCache ( state : ActiveBattleState ) : Promise < void > {
279+ const payload = this . serializeState ( state )
280+ await this . redis . set ( this . getCacheKey ( state . battleId ) , JSON . stringify ( payload ) )
281+ }
282+
283+ //skipState 업데이트 시 캐시 업데이트
284+ private async patchSkipCache ( battleId : string , skipList : Set < string > ) : Promise < void > {
285+ const cachedState = await this . loadStateFromCache ( battleId )
286+ if ( ! cachedState ) return
287+ cachedState . skipState = new Set ( skipList )
288+ await this . persistStateCache ( cachedState )
289+ }
290+
291+ //캐시에 저장할 데이터 직렬화
292+ private serializeState ( state : ActiveBattleState ) : SerializedBattleState {
293+ return {
294+ battleId : state . battleId ,
295+ all : {
296+ roomId : state . all . roomId ,
297+ chats : this . serializeChatState ( state . all . chats ) ,
298+ attacks : state . all . attacks ,
299+ defenses : state . all . defenses ,
300+ } ,
301+ teamA : {
302+ roomId : state . teamA . roomId ,
303+ chats : this . serializeChatState ( state . teamA . chats ) ,
304+ attacks : state . teamA . attacks ,
305+ defenses : state . teamA . defenses ,
306+ users : [ ...state . teamA . users ] ,
307+ } ,
308+ teamB : {
309+ roomId : state . teamB . roomId ,
310+ chats : this . serializeChatState ( state . teamB . chats ) ,
311+ attacks : state . teamB . attacks ,
312+ defenses : state . teamB . defenses ,
313+ users : [ ...state . teamB . users ] ,
314+ } ,
315+ participants : [ ...state . participants . entries ( ) ] ,
316+ teamVotes : [ ...state . teamVotes . entries ( ) ] ,
317+ userInfoMap : [ ...state . userInfoMap . entries ( ) ] ,
318+ opinionHistory : state . opinionHistory ,
319+ skipState : Array . from ( state . skipState ) ,
320+ round : state . round ,
321+ topics : state . topics ,
322+ totalRounds : state . totalRounds ,
323+ phase : state . phase ,
324+ phaseCount : state . phaseCount ,
325+ startedAt : state . startedAt ,
326+ expiredAt : state . expiredAt ,
327+ }
328+ }
329+
330+ //캐시에서 가져온 데이터 역직렬화
331+ private deserializeState ( payload : SerializedBattleState ) : ActiveBattleState {
332+ const participants = new Map < string , BattleTeam > ( payload . participants ?? [ ] )
333+ const teamVotes = new Map < string , BattleTeam > ( payload . teamVotes ?? [ ] )
334+ const userInfoMap = new Map < string , string > ( payload . userInfoMap ?? [ ] )
335+
336+ const state : ActiveBattleState = {
337+ battleId : payload . battleId ,
338+ all : {
339+ roomId : payload . all . roomId ,
340+ chats : this . parseChatState ( payload . all . chats ) ,
341+ attacks : payload . all . attacks ?? [ ] ,
342+ defenses : payload . all . defenses ?? [ ] ,
343+ } ,
344+ teamA : {
345+ roomId : payload . teamA . roomId ,
346+ chats : this . parseChatState ( payload . teamA . chats ) ,
347+ attacks : payload . teamA . attacks ?? [ ] ,
348+ defenses : payload . teamA . defenses ?? [ ] ,
349+ users : payload . teamA . users ?? [ ] ,
350+ } ,
351+ teamB : {
352+ roomId : payload . teamB . roomId ,
353+ chats : this . parseChatState ( payload . teamB . chats ) ,
354+ attacks : payload . teamB . attacks ?? [ ] ,
355+ defenses : payload . teamB . defenses ?? [ ] ,
356+ users : payload . teamB . users ?? [ ] ,
357+ } ,
358+ participants,
359+ teamVotes,
360+ userInfoMap,
361+ opinionHistory : payload . opinionHistory ?? [ ] ,
362+ skipState : new Set ( payload . skipState ?? [ ] ) ,
363+ round : payload . round ,
364+ topics : payload . topics ,
365+ totalRounds : payload . totalRounds ,
366+ phase : payload . phase ,
367+ phaseCount : payload . phaseCount ,
368+ startedAt : payload . startedAt ,
369+ expiredAt : payload . expiredAt ,
370+ }
371+
372+ this . rebuildTeamUsers ( state )
373+ return state
374+ }
375+
376+ //데이터베이스에서 가져온 데이터 빌드
377+ private buildStateFromBattle ( battle : PrismaBattle ) : ActiveBattleState {
378+ const participants = this . parseParticipantsState ( battle . participantsState )
379+ const teamVotes = this . parseTeamVotesState ( battle . teamVotesState )
380+ const userInfoMap = this . parseUserInfoState ( battle . userInfoState )
381+ const attackState = this . parseAttackState ( battle . attacksState )
382+ const defenseState = this . parseDefenseState ( battle . defensesState )
383+ const opinionHistory = this . parseOpinionHistoryState ( battle . opinionHistoryState )
384+ const skipState = new Set < string > ( Array . isArray ( battle . skipState ) ? battle . skipState : [ ] )
385+
386+ const playTime = BATTLE_PLAYTIME [ battle . playTime as keyof typeof BATTLE_PLAYTIME ]
387+ if ( ! playTime ) {
388+ throw new NotFoundException ( '올바르지 않은 배틀 진행 시간입니다.' )
389+ }
390+
391+ const state : ActiveBattleState = {
392+ battleId : battle . id ,
393+ all : {
394+ roomId : this . getBattleRoomId ( battle . id ) ,
395+ chats : this . parseChatState ( battle . chatsAllState ) ,
396+ attacks : attackState . all ,
397+ defenses : defenseState . all ,
398+ } ,
399+ teamA : {
400+ roomId : this . getBattleRoomId ( battle . id , BATTLE_TEAM . A ) ,
401+ users : [ ] ,
402+ chats : this . parseChatState ( battle . chatsTeamAState ) ,
403+ attacks : attackState . teamA ,
404+ defenses : defenseState . teamA ,
405+ } ,
406+ teamB : {
407+ roomId : this . getBattleRoomId ( battle . id , BATTLE_TEAM . B ) ,
408+ users : [ ] ,
409+ chats : this . parseChatState ( battle . chatsTeamBState ) ,
410+ attacks : attackState . teamB ,
411+ defenses : defenseState . teamB ,
412+ } ,
413+ participants,
414+ teamVotes,
415+ userInfoMap,
416+ opinionHistory,
417+ skipState,
418+ round : battle . currentRound ?? 1 ,
419+ topics : battle . topics ,
420+ totalRounds : playTime . rounds ,
421+ phase : ( battle . currentPhase ?? BATTLE_PHASE . PENDING . name ) as BattlePhaseName ,
422+ phaseCount : battle . phaseCount ?? 1 ,
423+ startedAt : battle . startedAt ? battle . startedAt . getTime ( ) : null ,
424+ expiredAt : battle . expiredAt ? battle . expiredAt . getTime ( ) : null ,
425+ }
426+
427+ this . rebuildTeamUsers ( state )
428+ return state
429+ }
430+
260431 private rebuildTeamUsers ( state : ActiveBattleState ) : void {
261432 state . teamA . users = [ ]
262433 state . teamB . users = [ ]
@@ -267,6 +438,10 @@ export class BattleStateRepositoryAdapter implements BattleStatePort {
267438 }
268439 }
269440
441+ private getCacheKey ( battleId : string ) : string {
442+ return `${ this . cachePrefix } ${ battleId } `
443+ }
444+
270445 private getBattleRoomId ( battleId : string , team ?: BattleTeam ) : string {
271446 return team ? `battle:${ battleId } :${ team } ` : `battle:${ battleId } `
272447 }
0 commit comments