Skip to content

Commit 7f7bb73

Browse files
authored
Merge pull request #148 from hamlsy/feat/147
[Feat/147] 게임 진행 중 플레이어가 탈주해도 게임이 진행되던 현상 수정 및 플레이어 정보 조회 api 구현
2 parents cf45744 + a677f92 commit 7f7bb73

55 files changed

Lines changed: 1485 additions & 558 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/main/java/com/kospot/application/admin/statistics/GetOverallStatisticsUseCase.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,14 @@ private GameModeStatisticSummary mapToSummary(Object[] row) {
3232
Double avgRankScore = row[3] != null ? ((Number) row[3]).doubleValue() : null;
3333
Double avgMultiScore = row[4] != null ? ((Number) row[4]).doubleValue() : null;
3434
Long totalFirstPlace = row[5] != null ? ((Number) row[5]).longValue() : 0L;
35-
Long totalSecondPlace = row[6] != null ? ((Number) row[6]).longValue() : 0L;
36-
Long totalThirdPlace = row[7] != null ? ((Number) row[7]).longValue() : 0L;
3735

3836
return new GameModeStatisticSummary(
3937
gameMode,
4038
totalGames,
4139
avgPracticeScore,
4240
avgRankScore,
4341
avgMultiScore,
44-
totalFirstPlace,
45-
totalSecondPlace,
46-
totalThirdPlace
42+
totalFirstPlace
4743
);
4844
}
4945
}

src/main/java/com/kospot/application/admin/statistics/GetStatisticsByPeriodUseCase.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,14 @@ private GameModeStatisticSummary mapToSummary(Object[] row) {
4141
Double avgRankScore = row[3] != null ? ((Number) row[3]).doubleValue() : null;
4242
Double avgMultiScore = row[4] != null ? ((Number) row[4]).doubleValue() : null;
4343
Long totalFirstPlace = row[5] != null ? ((Number) row[5]).longValue() : 0L;
44-
Long totalSecondPlace = row[6] != null ? ((Number) row[6]).longValue() : 0L;
45-
Long totalThirdPlace = row[7] != null ? ((Number) row[7]).longValue() : 0L;
4644

4745
return new GameModeStatisticSummary(
4846
gameMode,
4947
totalGames,
5048
avgPracticeScore,
5149
avgRankScore,
5250
avgMultiScore,
53-
totalFirstPlace,
54-
totalSecondPlace,
55-
totalThirdPlace
51+
totalFirstPlace
5652
);
5753
}
5854
}

src/main/java/com/kospot/application/member/GetMemberProfileUseCase.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@ private GameStatistics buildGameStatistics(MemberStatistic statistic) {
7373
.totalGames(roadViewStatistic.getMulti().getGames())
7474
.averageScore(roadViewStatistic.getMulti().getAvgScore())
7575
.firstPlaceCount(roadViewStatistic.getMulti().getFirstPlace())
76-
.secondPlaceCount(roadViewStatistic.getMulti().getSecondPlace())
77-
.thirdPlaceCount(roadViewStatistic.getMulti().getThirdPlace())
7876
.build())
7977
.build())
8078
.photo(PhotoGameStats.builder()
@@ -90,8 +88,6 @@ private GameStatistics buildGameStatistics(MemberStatistic statistic) {
9088
.totalGames(photoStatistic.getMulti().getGames())
9189
.averageScore(photoStatistic.getMulti().getAvgScore())
9290
.firstPlaceCount(photoStatistic.getMulti().getFirstPlace())
93-
.secondPlaceCount(photoStatistic.getMulti().getSecondPlace())
94-
.thirdPlaceCount(photoStatistic.getMulti().getThirdPlace())
9591
.build())
9692
.build())
9793
.build();
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.kospot.application.member;
2+
3+
import com.kospot.domain.game.vo.GameMode;
4+
import com.kospot.domain.gamerank.adaptor.GameRankAdaptor;
5+
import com.kospot.domain.gamerank.entity.GameRank;
6+
import com.kospot.domain.member.adaptor.MemberAdaptor;
7+
import com.kospot.domain.member.entity.Member;
8+
import com.kospot.domain.statistic.adaptor.MemberStatisticAdaptor;
9+
import com.kospot.domain.statistic.entity.GameModeStatistic;
10+
import com.kospot.domain.statistic.entity.MemberStatistic;
11+
import com.kospot.infrastructure.annotation.usecase.UseCase;
12+
import com.kospot.presentation.member.dto.response.PlayerSummaryResponse;
13+
import lombok.RequiredArgsConstructor;
14+
import lombok.extern.slf4j.Slf4j;
15+
import org.springframework.transaction.annotation.Transactional;
16+
17+
import java.util.List;
18+
19+
@Slf4j
20+
@UseCase
21+
@RequiredArgsConstructor
22+
@Transactional(readOnly = true)
23+
public class GetPlayerSummaryUseCase {
24+
25+
private final MemberAdaptor memberAdaptor;
26+
private final MemberStatisticAdaptor memberStatisticAdaptor;
27+
private final GameRankAdaptor gameRankAdaptor;
28+
29+
public PlayerSummaryResponse execute(Long memberId) {
30+
// Member 조회 (equippedMarkerImage 포함)
31+
Member member = memberAdaptor.queryByIdFetchMarkerImage(memberId);
32+
33+
// MemberStatistic 조회 (modeStatistics 포함)
34+
MemberStatistic statistic = memberStatisticAdaptor.queryByMemberIdFetchModeStatistics(memberId);
35+
36+
// GameRank 목록 조회
37+
List<GameRank> ranks = gameRankAdaptor.queryAllByMember(member);
38+
39+
// equippedMarkerImageUrl 추출
40+
String equippedMarkerImageUrl = member.getEquippedMarkerImage() != null
41+
? member.getEquippedMarkerImage().getImageUrl()
42+
: null;
43+
44+
// PlayStreak 추출
45+
int playStreak = statistic.getPlayStreak() != null
46+
? statistic.getPlayStreak().getCurrentStreak()
47+
: 0;
48+
49+
// GameModeStatistic 추출
50+
List<GameModeStatistic> modeStatistics = statistic.getModeStatistics();
51+
GameModeStatistic roadViewStatistic = findModeStatistic(modeStatistics, GameMode.ROADVIEW);
52+
GameModeStatistic photoStatistic = findModeStatistic(modeStatistics, GameMode.PHOTO);
53+
54+
// GameRank 추출
55+
GameRank roadViewRank = findModeRank(ranks, GameMode.ROADVIEW);
56+
GameRank photoRank = findModeRank(ranks, GameMode.PHOTO);
57+
58+
// Response 생성
59+
return PlayerSummaryResponse.builder()
60+
.nickname(member.getNickname())
61+
.playStreak(playStreak)
62+
.equippedMarkerImageUrl(equippedMarkerImageUrl)
63+
.joinedAt(member.getCreatedDate())
64+
.rankInfo(buildRankInfo(roadViewRank, photoRank, roadViewStatistic, photoStatistic))
65+
.multiGameStats(buildMultiGameStats(roadViewStatistic, photoStatistic))
66+
.build();
67+
}
68+
69+
private GameModeStatistic findModeStatistic(List<GameModeStatistic> modeStatistics, GameMode gameMode) {
70+
return modeStatistics.stream()
71+
.filter(stat -> stat.getGameMode() == gameMode)
72+
.findFirst()
73+
.orElseThrow(() -> new IllegalStateException("GameModeStatistic not found: " + gameMode));
74+
}
75+
76+
private GameRank findModeRank(List<GameRank> ranks, GameMode gameMode) {
77+
return ranks.stream()
78+
.filter(rank -> rank.getGameMode() == gameMode)
79+
.findFirst()
80+
.orElseThrow(() -> new IllegalStateException("GameRank not found: " + gameMode));
81+
}
82+
83+
private PlayerSummaryResponse.RankInfo buildRankInfo(
84+
GameRank roadViewRank,
85+
GameRank photoRank,
86+
GameModeStatistic roadViewStatistic,
87+
GameModeStatistic photoStatistic) {
88+
return PlayerSummaryResponse.RankInfo.builder()
89+
.roadView(PlayerSummaryResponse.RankInfo.RoadViewRankInfo.builder()
90+
.ratingScore(roadViewRank.getRatingScore())
91+
.rankLevel(roadViewRank.getRankLevel())
92+
.rankTier(roadViewRank.getRankTier())
93+
.rankAvgScore(roadViewStatistic.getRank().getAvgScore())
94+
.build())
95+
.photo(PlayerSummaryResponse.RankInfo.PhotoRankInfo.builder()
96+
.ratingScore(photoRank.getRatingScore())
97+
.rankLevel(photoRank.getRankLevel())
98+
.rankTier(photoRank.getRankTier())
99+
.rankAvgScore(photoStatistic.getRank().getAvgScore())
100+
.build())
101+
.build();
102+
}
103+
104+
private PlayerSummaryResponse.MultiGameStats buildMultiGameStats(
105+
GameModeStatistic roadViewStatistic,
106+
GameModeStatistic photoStatistic) {
107+
return PlayerSummaryResponse.MultiGameStats.builder()
108+
.roadView(PlayerSummaryResponse.MultiGameStats.RoadViewMultiStats.builder()
109+
.totalGames(roadViewStatistic.getMulti().getGames())
110+
.firstPlaceCount(roadViewStatistic.getMulti().getFirstPlace())
111+
.build())
112+
.photo(PlayerSummaryResponse.MultiGameStats.PhotoMultiStats.builder()
113+
.totalGames(photoStatistic.getMulti().getGames())
114+
.firstPlaceCount(photoStatistic.getMulti().getFirstPlace())
115+
.build())
116+
.build();
117+
}
118+
}
119+
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.kospot.application.multi.flow;
2+
3+
import com.kospot.application.multi.game.message.LoadingStatusMessage;
4+
import com.kospot.application.multi.round.roadview.NextRoadViewRoundUseCase;
5+
import com.kospot.domain.multi.game.adaptor.MultiRoadViewGameAdaptor;
6+
import com.kospot.domain.multi.game.entity.MultiRoadViewGame;
7+
import com.kospot.infrastructure.websocket.auth.WebSocketMemberPrincipal;
8+
import com.kospot.presentation.multi.game.dto.message.LoadingAckMessage;
9+
import lombok.RequiredArgsConstructor;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
12+
import org.springframework.stereotype.Service;
13+
import org.springframework.transaction.annotation.Transactional;
14+
15+
import java.time.Duration;
16+
17+
/**
18+
* 게임 전환을 조율하는 서비스
19+
* 로딩 완료 확인, 타임아웃 처리, 게임 시작/취소 등을 조율한다.
20+
*/
21+
@Slf4j
22+
@Service
23+
@RequiredArgsConstructor
24+
public class GameTransitionOrchestrator {
25+
26+
private static final Duration LOADING_TIMEOUT_DURATION = Duration.ofSeconds(10);
27+
28+
private final LoadingPhaseService loadingPhaseService;
29+
private final MultiGameFlowScheduler multiGameFlowScheduler;
30+
private final NextRoadViewRoundUseCase nextRoadViewRoundUseCase;
31+
private final MultiRoadViewGameAdaptor multiRoadViewGameAdaptor;
32+
33+
/**
34+
* 게임 시작 브로드캐스트 직후 로딩 단계를 시작한다.
35+
*
36+
* @param roomId 방 ID
37+
* @param gameId 게임 ID
38+
*/
39+
public void initializeLoadingPhase(String roomId, Long gameId) {
40+
loadingPhaseService.initializeLoadingPhase(roomId, gameId);
41+
42+
multiGameFlowScheduler.schedule(roomId, MultiGameFlowScheduler.FlowTaskType.LOADING_TIMEOUT,
43+
LOADING_TIMEOUT_DURATION, () -> handleLoadingTimeout(roomId));
44+
log.info("Scheduled loading timeout - RoomId: {}, GameId: {}", roomId, gameId);
45+
}
46+
47+
/**
48+
* 플레이어가 로딩 완료를 알리면 처리한다.
49+
*
50+
* @param roomId 방 ID
51+
* @param message 로딩 ACK 메시지
52+
* @param headerAccessor WebSocket 헤더
53+
*/
54+
public void handleLoadingAck(String roomId, LoadingAckMessage message,
55+
SimpMessageHeaderAccessor headerAccessor) {
56+
WebSocketMemberPrincipal principal = WebSocketMemberPrincipal.getPrincipal(headerAccessor);
57+
Long memberId = principal != null ? principal.getMemberId() : null;
58+
59+
if (memberId == null) {
60+
log.warn("Missing member id in loading ack - RoomId: {}", roomId);
61+
return;
62+
}
63+
64+
Long acknowledgedAt = message.getClientTimestamp() != null
65+
? message.getClientTimestamp()
66+
: System.currentTimeMillis();
67+
68+
loadingPhaseService.markPlayerReady(roomId, message.getRoundId(), memberId, acknowledgedAt);
69+
70+
LoadingStatusMessage statusMessage = loadingPhaseService.buildLoadingStatusMessage(roomId);
71+
loadingPhaseService.broadcastLoadingStatus(roomId, statusMessage);
72+
73+
if (statusMessage.isAllArrived()) {
74+
onAllPlayersArrived(roomId, message.getRoundId());
75+
}
76+
}
77+
78+
/**
79+
* 모든 플레이어가 도착했을 때 게임을 시작한다.
80+
*/
81+
private void onAllPlayersArrived(String roomId, Long roundId) {
82+
multiGameFlowScheduler.cancel(roomId, MultiGameFlowScheduler.FlowTaskType.LOADING_TIMEOUT);
83+
Long currentGameId = loadingPhaseService.getCurrentGameId(roomId);
84+
85+
if (currentGameId == null) {
86+
log.warn("Current game id not found for room - RoomId: {}", roomId);
87+
return;
88+
}
89+
90+
log.info("All players arrived. Starting game - RoomId: {}, GameId: {}", roomId, currentGameId);
91+
loadingPhaseService.cleanupLoadingState(roomId);
92+
93+
try {
94+
Long numericRoomId = Long.parseLong(roomId);
95+
nextRoadViewRoundUseCase.executeInitial(numericRoomId, currentGameId);
96+
} catch (NumberFormatException e) {
97+
log.error("Failed to parse room id for starting game - RoomId: {}, GameId: {}",
98+
roomId, currentGameId, e);
99+
}
100+
}
101+
102+
/**
103+
* 로딩 타임아웃이 발생하면 게임을 취소한다.
104+
*/
105+
@Transactional
106+
void handleLoadingTimeout(String roomId) {
107+
LoadingStatusMessage statusMessage = loadingPhaseService.buildLoadingStatusMessage(roomId);
108+
109+
if (statusMessage.isAllArrived()) {
110+
log.info("Timeout fired but all players already arrived - RoomId: {}", roomId);
111+
return;
112+
}
113+
114+
loadingPhaseService.broadcastLoadingStatus(roomId, statusMessage);
115+
Long currentGameId = loadingPhaseService.getCurrentGameId(roomId);
116+
117+
if (currentGameId == null) {
118+
log.warn("Timeout triggered without current game id - RoomId: {}", roomId);
119+
return;
120+
}
121+
122+
MultiRoadViewGame game = multiRoadViewGameAdaptor.queryById(currentGameId);
123+
game.cancelGame();
124+
loadingPhaseService.cleanupLoadingState(roomId);
125+
126+
log.warn("Loading timeout reached. Game cancelled - RoomId: {}, GameId: {}", roomId, currentGameId);
127+
}
128+
}

0 commit comments

Comments
 (0)