Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.ezcode.codetest.application.game.dto.response.encounter;

import java.time.LocalDateTime;

import org.ezcode.codetest.domain.game.model.encounter.BattleHistory;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@Builder
@Schema(description = "캐릭터 방어 전투 결과 응답")
public record DefenceBattleHistoryResponse(

@Schema(description = "공격자 캐릭터 닉네임")
String attackerNickName,

@Schema(description = "플레이어 캐릭터 닉네임")
String PlayerNickName,

@Schema(description = "전투 로그")
String battleLog,

@Schema(description = "플레이어 승패 여부(이겼을시 true, 패배시 false)")
boolean isDefenderWin,

@Schema(description = "PVP 가 일어난 시점")
LocalDateTime battleCreatedAt

) {
public static DefenceBattleHistoryResponse from(BattleHistory history) {
return DefenceBattleHistoryResponse.builder()
.attackerNickName(history.getAttacker().getName())
.PlayerNickName(history.getDefender().getName())
.battleLog(history.getBattleLog())
.isDefenderWin(!history.getIsAttackerWin())
.battleCreatedAt(history.getCreatedAt())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.ezcode.codetest.application.game.dto.request.skill.SkillUnEquipRequest;
import org.ezcode.codetest.application.game.dto.response.character.CharacterStatusResponse;
import org.ezcode.codetest.application.game.dto.response.encounter.BattleHistoryResponse;
import org.ezcode.codetest.application.game.dto.response.encounter.DefenceBattleHistoryResponse;
import org.ezcode.codetest.application.game.dto.response.encounter.EncounterResultResponse;
import org.ezcode.codetest.application.game.dto.response.encounter.MatchingBattleResponse;
import org.ezcode.codetest.application.game.dto.response.encounter.MatchingEncounterResponse;
Expand All @@ -18,6 +19,7 @@
import org.ezcode.codetest.application.game.dto.response.skill.SkillResponse;
import org.ezcode.codetest.common.security.util.JwtUtil;
import org.ezcode.codetest.domain.game.model.character.GameCharacter;
import org.ezcode.codetest.domain.game.model.encounter.BattleHistory;
import org.ezcode.codetest.domain.game.model.encounter.EncounterHistory;
import org.ezcode.codetest.domain.game.model.encounter.EncounterLog;
import org.ezcode.codetest.domain.game.model.encounter.MatchMessageTemplate;
Expand Down Expand Up @@ -160,25 +162,6 @@ public BattleHistoryResponse battle(Long playerId, BattleRequest request) {
);
}

@Transactional
public BattleHistoryResponse randomBattle(Long userId) {

GameCharacter player = characterService.getGameCharacter(userId);

GameCharacter enemy = encounterDomainService.getRandomEnemyCharacter(userId, player.getId());

BattleLog log = encounterDomainService.battle(player, enemy);

encounterDomainService.createBattleHistory(player, enemy, log);

return BattleHistoryResponse.of(
player.getName(),
enemy.getName(),
log.getMessages(),
log.getPlayerWin()
);
}

@Transactional
public MatchingBattleResponse randomBattleMatching(Long userId) {

Expand Down Expand Up @@ -222,4 +205,14 @@ public EncounterResultResponse encounterChoice(Long userId, EncounterChoiceReque
return EncounterResultResponse.from(history);
}

@Transactional
public List<DefenceBattleHistoryResponse> getPlayerDefenceHistory(Long userId) {

GameCharacter playerCharacter = characterService.getGameCharacter(userId);

List<BattleHistory> history = encounterDomainService.getCharacterBattleHistory(playerCharacter);

return history.stream().map(DefenceBattleHistoryResponse::from).toList();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,12 @@ public void increase(Stat stat, double rate) {
case DATA_STRUCTURE:
this.def += rate / 10;
this.atk += rate / 10;
this.accuracy += rate / 10;
this.accuracy += rate / 5;
break;
case SPEED:
this.speed += rate / 10;
this.speed += rate / 5;
this.atk += rate / 10;
this.accuracy += rate / 10;
this.accuracy += rate / 5;
break;
case DEBUGGING:
this.crit += rate / 5;
Expand All @@ -101,7 +101,7 @@ public void increase(Stat stat, double rate) {
case OPTIMIZATION:
this.evasion += rate / 5;
this.def += rate / 10;
this.accuracy += rate / 10;
this.accuracy += rate / 5;
break;
default:
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ public interface BattleHistoryRepository {
void delete(BattleHistory battleHistory);

List<BattleHistory> findAll();

List<BattleHistory> findCreatedInLast24Hours(Long playerId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
public class GameEncounterDomainService {

private final CharacterEquipService characterEquipService;
private final SkillStrategyFactory skillStrategyFactory;
private final SkillStrategyFactory skillStrategyFactory;
private final EncounterStrategyFactory encounterFactory;

private final BattleHistoryRepository battleHistoryRepository;
Expand All @@ -65,13 +65,13 @@ public BattleLog battle(GameCharacter player, GameCharacter opponent) {

WeaponType playerWeaponType = playerItems.stream()
.filter(item -> item instanceof Weapon)
.map(item -> (WeaponType) item.getItemType())
.map(item -> (WeaponType)item.getItemType())
.findFirst()
.orElse(WeaponType.NOTHING);

WeaponType opponentWeaponType = opponentItems.stream()
WeaponType opponentWeaponType = opponentItems.stream()
.filter(item -> item instanceof Weapon)
.map(item -> (WeaponType) item.getItemType())
.map(item -> (WeaponType)item.getItemType())
.findFirst()
.orElse(WeaponType.NOTHING);

Expand Down Expand Up @@ -106,10 +106,12 @@ public BattleLog battle(GameCharacter player, GameCharacter opponent) {

if (attacker == playerContext) {
player.earnGold(100L);
battleLog.add("전투 승리보상으로 100 골드가 지급되었습니다.");
battleLog.add("%s(이)가 전투 승리보상으로 100 골드가 지급되었습니다.", playerContext.getName());
} else {
player.loseGold(25L);
battleLog.add("패배하여 25 골드를 갈취당했습니다.");
opponent.earnGold(25L);
battleLog.add("%s님이 %s님에게 패배하여 25 골드를 갈취당했습니다.", playerContext.getName(),
opponentContext.getName());
}

return battleLog;
Expand All @@ -128,10 +130,12 @@ public BattleLog battle(GameCharacter player, GameCharacter opponent) {

if (defender == playerContext) {
player.earnGold(100L);
battleLog.add("전투 승리보상으로 100 골드가 지급되었습니다.");
battleLog.add("%s님에게 전투 승리보상으로 100 골드가 지급되었습니다.", playerContext.getName());
} else {
player.loseGold(25L);
battleLog.add("패배하여 25 골드를 갈취당했습니다.");
opponent.earnGold(25L);
battleLog.add("%s님이 %s님에게 패배하여 25 골드를 갈취당했습니다.", playerContext.getName(),
opponentContext.getName());
}

return battleLog;
Expand All @@ -140,7 +144,7 @@ public BattleLog battle(GameCharacter player, GameCharacter opponent) {
currentSkillIndex = (currentSkillIndex + 1) % 3;
}

battleLog.add("전투가 종료되었습니다. 양쪽 모두 살아남았습니다. 무승부입니다!");
battleLog.add("전투가 종료되었습니다. 양쪽 모두 살아남았습니다. 무승부입니다.");
battleLog.setPlayerWin(false);
return battleLog;
}
Expand Down Expand Up @@ -175,7 +179,7 @@ public List<BattleHistory> getBattleHistory(GameCharacter character) {

public GameCharacter getRandomEnemyCharacter(Long userId, Long playerId) {

GameCharacterMatchTokenBucket playerTokenBucket = matchTokenBucketRepository.findByCharacterId(playerId)
GameCharacterMatchTokenBucket playerTokenBucket = matchTokenBucketRepository.findByCharacterId(playerId)
.orElseThrow(() -> new GameException(GameExceptionCode.PLAYER_TOKEN_BUCKET_NOT_EXISTS));

playerTokenBucket.consumeBattleToken();
Expand All @@ -186,7 +190,7 @@ public GameCharacter getRandomEnemyCharacter(Long userId, Long playerId) {

public RandomEncounter getRandomEncounter(Long playerId) {

GameCharacterMatchTokenBucket playerTokenBucket = matchTokenBucketRepository.findByCharacterId(playerId)
GameCharacterMatchTokenBucket playerTokenBucket = matchTokenBucketRepository.findByCharacterId(playerId)
.orElseThrow(() -> new GameException(GameExceptionCode.PLAYER_TOKEN_BUCKET_NOT_EXISTS));

playerTokenBucket.consumeEncounterToken();
Expand Down Expand Up @@ -217,7 +221,7 @@ public EncounterLog encounterHappen(GameCharacter player, Long encounterId, bool
CharacterContext playerContext = CharacterContext.from(player.getName(), playerStats);

Inventory playerInventory = inventoryRepository.findByGameCharacterId(player.getId())
.orElseThrow(() -> new GameException(GameExceptionCode.INVENTORY_NOT_FOUND));
.orElseThrow(() -> new GameException(GameExceptionCode.INVENTORY_NOT_FOUND));

EncounterLog resultLog = new EncounterLog();

Expand All @@ -235,4 +239,9 @@ public EncounterHistory createEncounterHistory(GameCharacter player, EncounterLo
.build());
}

public List<BattleHistory> getCharacterBattleHistory(GameCharacter player) {

return battleHistoryRepository.findCreatedInLast24Hours(player.getId());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public boolean useSkill(CharacterContext attacker, CharacterContext defender, Ba

if (RNG.nextDouble() * 100 < attacker.getStun()) {
defender.consumeActionPoints();
log.add("스턴! %s가 잠시 머리를 잃었습니다 — 행동력 1 감소 → 남은 AP %d", defender.getName(), defender.getAp());
log.add("스턴! %s(이)가 잠시 정신을 잃었습니다 — 행동력 1 감소 → 남은 AP %d", defender.getName(), defender.getAp());
}

log.add("[%s] 남은 체력: %,.1f | [%s] 남은 체력: %,.1f", attacker.getName(), attacker.getHp(), defender.getName(), defender.getHp());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ public StatUpdateUtil() {
}

public Map<Stat, Double> getStatIncreasePerProblem(String categoryDescription) {
//TODO : 나중에 NULL 처리하기
return increasedStatRate.get(categoryDescription).asImmutableMap();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ public CacheManager cacheManager() {
.expireAfterWrite(Duration.ofMinutes(1))
.build());

CaffeineCache historyCache = new CaffeineCache("histories",
Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(1))
.build());

CaffeineCache skillCache = new CaffeineCache("skill",
Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(10))
Expand All @@ -38,8 +43,14 @@ public CacheManager cacheManager() {
.expireAfterWrite(Duration.ofMinutes(10))
.build());

CaffeineCache choiceCache = new CaffeineCache("choices",
Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(10))
.build());

SimpleCacheManager manager = new SimpleCacheManager();
manager.setCaches(List.of(skillCache, itemsByCategoryCache, encountersCache, countCache));
manager.setCaches(
List.of(skillCache, itemsByCategoryCache, encountersCache, countCache, historyCache, choiceCache));
return manager;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ protected Principal determineUser(
if (query != null && query.startsWith("token=")) {
tokenParam = query.substring(6);
}
//TODO : 토큰의 대한 예외처리 아직 구현 x

Claims claims = jwtUtil.extractClaims(tokenParam);
String email = claims.get("email", String.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
import org.springframework.web.socket.messaging.SessionDisconnectEvent;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class WebSocketEventListener implements ApplicationListener<SessionDisconnectEvent> {
Expand All @@ -29,23 +31,27 @@ public class WebSocketEventListener implements ApplicationListener<SessionDiscon
@Override
public void onApplicationEvent(SessionDisconnectEvent event) {

StompHeaderAccessor h = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = h.getSessionId();
try {
StompHeaderAccessor h = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = h.getSessionId();

String email = h.getUser().getName();
String nickName = userDomainService.getUser(email).getNickname();
String email = h.getUser().getName();
String nickName = userDomainService.getUser(email).getNickname();

RoomSessionInfo roomData = sessionService.removeSessionCount(sessionId);
ChatRoom chatRoom = chattingDomainService.getChatRoom(roomData.roomId());
RoomSessionInfo roomData = sessionService.removeSessionCount(sessionId);
ChatRoom chatRoom = chattingDomainService.getChatRoom(roomData.roomId());

Map<String, Object> payload = new HashMap<>();
payload.put("roomId", chatRoom.getId());
payload.put("title", chatRoom.getTitle());
payload.put("headCount", roomData.headCount());
payload.put("eventType", "UPDATE");
Map<String, Object> payload = new HashMap<>();
payload.put("roomId", chatRoom.getId());
payload.put("title", chatRoom.getTitle());
payload.put("headCount", roomData.headCount());
payload.put("eventType", "UPDATE");

messageService.handleChatRoomParticipantCountChange(payload);
messageService.handleChatRoomEntryExitMessage(ChatMessageTemplate.CHAT_ROOM_LEFT.format(nickName),
chatRoom.getId());
messageService.handleChatRoomParticipantCountChange(payload);
messageService.handleChatRoomEntryExitMessage(ChatMessageTemplate.CHAT_ROOM_LEFT.format(nickName),
chatRoom.getId());
} catch (Exception e) {
log.warn("SessionDisconnectEvent 처리 중 예외 발생, 채팅 관련 웹소켓 세션이 아닙니다.", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public Optional<GameCharacter> findByUserId(Long userId) {
public Optional<GameCharacter> findRandomCharacter(Long userId) {

long count = characterRepository.count();
if (count == 0) return Optional.empty();
if (count == 0 || count == 1) return Optional.empty();

for (int i = 0; i < 10; i++) {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.ezcode.codetest.infrastructure.persistence.repository.game.mysql.encounter;

import java.time.LocalDateTime;
import java.util.List;

import org.ezcode.codetest.domain.game.model.encounter.BattleHistory;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BattleHistoryJpaRepository extends JpaRepository<BattleHistory, Long> {
Expand All @@ -12,4 +14,8 @@ public interface BattleHistoryJpaRepository extends JpaRepository<BattleHistory,
List<BattleHistory> findByDefenderId(Long characterId);

List<BattleHistory> findByAttackerIdOrDefenderId(Long attackerId, Long defenderId);

@EntityGraph(attributePaths = {"attacker", "defender"})
List<BattleHistory> findByDefenderIdAndCreatedAtAfterOrderByCreatedAtDesc(Long defenderId,
LocalDateTime last24Hours);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package org.ezcode.codetest.infrastructure.persistence.repository.game.mysql.encounter;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import org.ezcode.codetest.domain.game.model.encounter.BattleHistory;
import org.ezcode.codetest.domain.game.repository.BattleHistoryRepository;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Repository;

import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -56,4 +58,12 @@ public List<BattleHistory> findAll() {

return battleHistoryRepository.findAll();
}

@Override
@Cacheable(value = "histories", key = "#playerId")
public List<BattleHistory> findCreatedInLast24Hours(Long playerId) {

return battleHistoryRepository.findByDefenderIdAndCreatedAtAfterOrderByCreatedAtDesc(playerId,
LocalDateTime.now().minusDays(1));
Comment on lines +66 to +67
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

시간대 처리를 명시적으로 관리하세요.

LocalDateTime.now()는 시스템 시간대에 의존하므로 다국가 서비스에서 예상치 못한 결과를 초래할 수 있습니다. UTC 기준으로 시간을 처리하거나 애플리케이션의 표준 시간대를 명시적으로 사용하는 것을 권장합니다.

-		return battleHistoryRepository.findByDefenderIdAndCreatedAtAfterOrderByCreatedAtDesc(playerId,
-			LocalDateTime.now().minusDays(1));
+		return battleHistoryRepository.findByDefenderIdAndCreatedAtAfterOrderByCreatedAtDesc(playerId,
+			LocalDateTime.now(ZoneOffset.UTC).minusDays(1));

추가로 java.time.ZoneOffset import가 필요합니다.

🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/encounter/BattleHistoryRepositoryImpl.java
around lines 66 to 67, replace LocalDateTime.now() with
LocalDateTime.now(ZoneOffset.UTC) to explicitly use UTC timezone instead of the
system default. This ensures consistent time handling across different regions.
Also, add the necessary import for java.time.ZoneOffset at the top of the file.

}
}
Loading