diff --git a/src/main/java/org/ezcode/codetest/application/game/dto/response/encounter/DefenceBattleHistoryResponse.java b/src/main/java/org/ezcode/codetest/application/game/dto/response/encounter/DefenceBattleHistoryResponse.java new file mode 100644 index 00000000..763f662f --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/game/dto/response/encounter/DefenceBattleHistoryResponse.java @@ -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(); + } +} diff --git a/src/main/java/org/ezcode/codetest/application/game/play/GamePlayUseCase.java b/src/main/java/org/ezcode/codetest/application/game/play/GamePlayUseCase.java index 50c6bb77..2be34a50 100644 --- a/src/main/java/org/ezcode/codetest/application/game/play/GamePlayUseCase.java +++ b/src/main/java/org/ezcode/codetest/application/game/play/GamePlayUseCase.java @@ -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; @@ -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; @@ -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) { @@ -222,4 +205,14 @@ public EncounterResultResponse encounterChoice(Long userId, EncounterChoiceReque return EncounterResultResponse.from(history); } + @Transactional + public List getPlayerDefenceHistory(Long userId) { + + GameCharacter playerCharacter = characterService.getGameCharacter(userId); + + List history = encounterDomainService.getCharacterBattleHistory(playerCharacter); + + return history.stream().map(DefenceBattleHistoryResponse::from).toList(); + } + } diff --git a/src/main/java/org/ezcode/codetest/domain/game/model/character/CharacterRealStat.java b/src/main/java/org/ezcode/codetest/domain/game/model/character/CharacterRealStat.java index 7e0c4b41..9c0d93f3 100644 --- a/src/main/java/org/ezcode/codetest/domain/game/model/character/CharacterRealStat.java +++ b/src/main/java/org/ezcode/codetest/domain/game/model/character/CharacterRealStat.java @@ -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; @@ -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; diff --git a/src/main/java/org/ezcode/codetest/domain/game/repository/BattleHistoryRepository.java b/src/main/java/org/ezcode/codetest/domain/game/repository/BattleHistoryRepository.java index f9520559..7ff8c457 100644 --- a/src/main/java/org/ezcode/codetest/domain/game/repository/BattleHistoryRepository.java +++ b/src/main/java/org/ezcode/codetest/domain/game/repository/BattleHistoryRepository.java @@ -20,4 +20,6 @@ public interface BattleHistoryRepository { void delete(BattleHistory battleHistory); List findAll(); + + List findCreatedInLast24Hours(Long playerId); } diff --git a/src/main/java/org/ezcode/codetest/domain/game/service/GameEncounterDomainService.java b/src/main/java/org/ezcode/codetest/domain/game/service/GameEncounterDomainService.java index 7d77596c..e71c1a61 100644 --- a/src/main/java/org/ezcode/codetest/domain/game/service/GameEncounterDomainService.java +++ b/src/main/java/org/ezcode/codetest/domain/game/service/GameEncounterDomainService.java @@ -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; @@ -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); @@ -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; @@ -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; @@ -140,7 +144,7 @@ public BattleLog battle(GameCharacter player, GameCharacter opponent) { currentSkillIndex = (currentSkillIndex + 1) % 3; } - battleLog.add("전투가 종료되었습니다. 양쪽 모두 살아남았습니다. 무승부입니다!"); + battleLog.add("전투가 종료되었습니다. 양쪽 모두 살아남았습니다. 무승부입니다."); battleLog.setPlayerWin(false); return battleLog; } @@ -175,7 +179,7 @@ public List 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(); @@ -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(); @@ -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(); @@ -235,4 +239,9 @@ public EncounterHistory createEncounterHistory(GameCharacter player, EncounterLo .build()); } + public List getCharacterBattleHistory(GameCharacter player) { + + return battleHistoryRepository.findCreatedInLast24Hours(player.getId()); + } + } diff --git a/src/main/java/org/ezcode/codetest/domain/game/strategy/skill/skillstrategyimpl/LifeStealSkill.java b/src/main/java/org/ezcode/codetest/domain/game/strategy/skill/skillstrategyimpl/LifeStealSkill.java index fe3a02de..572c4d24 100644 --- a/src/main/java/org/ezcode/codetest/domain/game/strategy/skill/skillstrategyimpl/LifeStealSkill.java +++ b/src/main/java/org/ezcode/codetest/domain/game/strategy/skill/skillstrategyimpl/LifeStealSkill.java @@ -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()); diff --git a/src/main/java/org/ezcode/codetest/domain/game/util/StatUpdateUtil.java b/src/main/java/org/ezcode/codetest/domain/game/util/StatUpdateUtil.java index b9d15f5b..e156ed0a 100644 --- a/src/main/java/org/ezcode/codetest/domain/game/util/StatUpdateUtil.java +++ b/src/main/java/org/ezcode/codetest/domain/game/util/StatUpdateUtil.java @@ -111,7 +111,6 @@ public StatUpdateUtil() { } public Map getStatIncreasePerProblem(String categoryDescription) { - //TODO : 나중에 NULL 처리하기 return increasedStatRate.get(categoryDescription).asImmutableMap(); } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/cache/config/CaffeineCacheConfig.java b/src/main/java/org/ezcode/codetest/infrastructure/cache/config/CaffeineCacheConfig.java index ff668588..3548e653 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/cache/config/CaffeineCacheConfig.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/cache/config/CaffeineCacheConfig.java @@ -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)) @@ -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; } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/config/CustomHandShakeHandler.java b/src/main/java/org/ezcode/codetest/infrastructure/event/config/CustomHandShakeHandler.java index c90ba817..75c5e6fe 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/config/CustomHandShakeHandler.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/config/CustomHandShakeHandler.java @@ -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); diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/listener/WebSocketEventListener.java b/src/main/java/org/ezcode/codetest/infrastructure/event/listener/WebSocketEventListener.java index 46a8e616..033815dd 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/listener/WebSocketEventListener.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/listener/WebSocketEventListener.java @@ -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 { @@ -29,23 +31,27 @@ public class WebSocketEventListener implements ApplicationListener payload = new HashMap<>(); - payload.put("roomId", chatRoom.getId()); - payload.put("title", chatRoom.getTitle()); - payload.put("headCount", roomData.headCount()); - payload.put("eventType", "UPDATE"); + Map 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); + } } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/character/GameCharacterRepositoryImpl.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/character/GameCharacterRepositoryImpl.java index c225d07e..df8bb200 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/character/GameCharacterRepositoryImpl.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/character/GameCharacterRepositoryImpl.java @@ -31,7 +31,7 @@ public Optional findByUserId(Long userId) { public Optional 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++) { diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/encounter/BattleHistoryJpaRepository.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/encounter/BattleHistoryJpaRepository.java index 7a6224f1..37c1266b 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/encounter/BattleHistoryJpaRepository.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/encounter/BattleHistoryJpaRepository.java @@ -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 { @@ -12,4 +14,8 @@ public interface BattleHistoryJpaRepository extends JpaRepository findByDefenderId(Long characterId); List findByAttackerIdOrDefenderId(Long attackerId, Long defenderId); + + @EntityGraph(attributePaths = {"attacker", "defender"}) + List findByDefenderIdAndCreatedAtAfterOrderByCreatedAtDesc(Long defenderId, + LocalDateTime last24Hours); } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/encounter/BattleHistoryRepositoryImpl.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/encounter/BattleHistoryRepositoryImpl.java index d6026074..45e831fa 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/encounter/BattleHistoryRepositoryImpl.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/encounter/BattleHistoryRepositoryImpl.java @@ -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; @@ -56,4 +58,12 @@ public List findAll() { return battleHistoryRepository.findAll(); } + + @Override + @Cacheable(value = "histories", key = "#playerId") + public List findCreatedInLast24Hours(Long playerId) { + + return battleHistoryRepository.findByDefenderIdAndCreatedAtAfterOrderByCreatedAtDesc(playerId, + LocalDateTime.now().minusDays(1)); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/encounter/EncounterChoiceRepositoryImpl.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/encounter/EncounterChoiceRepositoryImpl.java index 9442f6ef..c31837ab 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/encounter/EncounterChoiceRepositoryImpl.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/mysql/encounter/EncounterChoiceRepositoryImpl.java @@ -5,6 +5,7 @@ import org.ezcode.codetest.domain.game.model.encounter.EncounterChoice; import org.ezcode.codetest.domain.game.repository.EncounterChoiceRepository; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Repository; import lombok.RequiredArgsConstructor; @@ -34,6 +35,7 @@ public Optional findByName(String name) { } @Override + @Cacheable(value = "choices", key = "#encounterId + '-' + #playerDecision") public List findChoiceByPlayerDecision(Long encounterId, boolean playerDecision) { return encounterRepository.findByEncounterIdAndPlayerDecision(encounterId, playerDecision); diff --git a/src/main/java/org/ezcode/codetest/presentation/game/play/GamePlayController.java b/src/main/java/org/ezcode/codetest/presentation/game/play/GamePlayController.java index c49ad644..9376e7bb 100644 --- a/src/main/java/org/ezcode/codetest/presentation/game/play/GamePlayController.java +++ b/src/main/java/org/ezcode/codetest/presentation/game/play/GamePlayController.java @@ -10,6 +10,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; @@ -209,6 +210,22 @@ public ResponseEntity battle( .body(gamePlayUseCase.battle(authUser.getId(), request)); } + @Operation( + summary = "방어 PVP 기록 조회 API", + description = "상대방이 자신을 대상으로 한 PVP 기록을 조회합니다.(24시간전까지만)", + responses = { + @ApiResponse(responseCode = "200", description = "PVP 결과 반환") + } + ) + @ResponseMessage("정상적으로 PVP 기록 조회에 성공하였습니다.") + @GetMapping("/characters/battles") + public ResponseEntity> getDefenceBattleHistory( + @AuthenticationPrincipal AuthUser authUser + ) { + return ResponseEntity.status(HttpStatus.OK) + .body(gamePlayUseCase.getPlayerDefenceHistory(authUser.getId())); + } + @Operation( summary = "무작위 배틀 매칭 API", description = "무작위로 다른 캐릭터 중 하나를 매칭시켜줍니다.", diff --git a/src/main/resources/templates/game-page.html b/src/main/resources/templates/game-page.html index f29e49cb..d6734723 100644 --- a/src/main/resources/templates/game-page.html +++ b/src/main/resources/templates/game-page.html @@ -363,7 +363,7 @@ panel.scrollTop = 0; panel.innerHTML = `Vault Boy HUD`; - // 왼쪽 패널: BATTLE, GAMBLE, ADVENTURE + // ← LEFT panels: BATTLE, GAMBLE, ADVENTURE if (!isRight) { const titles = { battle: '⚔️ BATTLE', @@ -372,53 +372,119 @@ }; panel.innerHTML += `

${titles[panelId]}

`; - // BATTLE 분기 + // BATTLE panel if (panelId === 'battle') { panel.innerHTML += ` -

- 전투 매칭 시스템이 활성화되었습니다. 시스템은 유저의 능력치를 기준으로 '적절히 불공정한' 상대를 찾아드립니다. -

- `; - panel.innerHTML += ``; +

+ 전투 매칭 시스템이 활성화되었습니다. 시스템은 유저의 능력치를 기준으로 '적절히 불공정한' 상대를 찾아드립니다. +

+
+ + +
+`; document.getElementById('battle-match-btn') .addEventListener('click', matchingBattle); + + document.getElementById('defence-log-btn') + .addEventListener('click', async () => { + // 1) fetch defence history + const res = await fetch('/api/games/characters/battles', { + headers: {'Authorization': 'Bearer ' + savedToken} + }); + const data = await res.json(); + if (!data.success) { + panel.innerHTML = `

Failed to load defence log.

`; + return; + } + const list = data.result; + + // 2) back button & count + const backHtml = ` +`; + const countHtml = ` +

+ 총 ${list.length}건의 방어 로그가 조회되었습니다. +

`; + + // 3) assemble history HTML + let historyHtml = ` +

🛡️ Defence Battle History

+
    `; + list.forEach((item, idx) => { + const color = item.isDefenderWin ? '#0f0' : '#f00'; + const when = new Date(item.battleCreatedAt).toLocaleString(); + const lines = item.battleLog.split('\n'); + historyHtml += ` +
  • +
    + • ${item.attackerNickName} vs ${item.PlayerNickName} +
    +
    + (${when}) +
    + +
  • +
    +`; + }); + historyHtml += `
`; + + // 4) render all at once + panel.innerHTML = backHtml + countHtml + historyHtml; + + // 5) bind back button + document.getElementById('back-to-battle') + .addEventListener('click', () => showPanel('battle')); + + // 6) bind toggles + document.querySelectorAll('.defence-item-header').forEach(header => { + header.addEventListener('click', () => { + const idx = header.dataset.idx; + const detail = document.getElementById(`dtl-${idx}`); + detail.style.display = detail.style.display === 'none' ? 'block' : 'none'; + }); + }); + }); } - // GAMBLE 분기 + // GAMBLE panel (unchanged) if (panelId === 'gamble') { panel.innerHTML += ` -

- 여기는 등록된 합법 도박 상점입니다. 유저의 골드를 빠르게 소각하며, 낮은 확률로 장비 혹은 스킬을 지급합니다. - 환불, 책임, 위로는 제공되지 않습니다. 뽑기 결과는 즉시 적용됩니다. 후회는 선택사항입니다. - 시스템은 감정을 고려하지 않으며, 통계는 당신 편이 아닙니다. 그래도 시도하시겠습니까? -

- `; - // ITEM GAMBLE 버튼만 남깁니다. - panel.innerHTML += ` - - `; +

+ 여기는 등록된 합법 도박 상점입니다. 유저의 골드를 빠르게 소각하며, 낮은 확률로 장비 혹은 스킬을 지급합니다. + 환불, 책임, 위로는 제공되지 않습니다. 뽑기 결과는 즉시 적용됩니다. 후회는 선택사항입니다. + 시스템은 감정을 고려하지 않으며, 통계는 당신 편이 아닙니다. 그래도 시도하시겠습니까? +

+ +`; document.getElementById('item-gamble-btn') .addEventListener('click', openItemGamblePopup); - // 더 이상 별도 SKILL GAMBLE 버튼은 없음. } - // ADVENTURE 분기 + // ADVENTURE panel (unchanged) if (panelId === 'adventure') { - // 1) 경고 문구 추가 (흰색) - panel.innerHTML += ` -

- 무작위 인카운터는 시스템에 의해 자동으로 배정됩니다.
- 실패 확률, 부상 가능성, 트라우마는 포함되지만 사전 고지는 생략됩니다.
- 그래도 버튼을 누른 건 당신입니다—책임은 시스템이 지지 않습니다. - (부정적 결과 발생 시 영구적인 능력치 하락 및 골드 분실이 일어날 수 있습니다) -

- `; - // 2) 매칭 버튼 panel.innerHTML += ` - - `; +

+ 무작위 인카운터는 시스템에 의해 자동으로 배정됩니다.
+ 실패 확률, 부상 가능성, 트라우마는 포함되지만 사전 고지는 생략됩니다.
+ 그래도 버튼을 누른 건 당신입니다—책임은 시스템이 지지 않습니다. + (부정적 결과 발생 시 영구적인 능력치 하락 및 골드 분실이 일어날 수 있습니다) +

+ +`; document.getElementById('adventure-match-btn') .addEventListener('click', matchingAdventure); } @@ -426,70 +492,71 @@ return; } - // 오른쪽 패널: STATUS, INVENTORY, SKILLS + // → RIGHT panels: STATUS, INVENTORY, SKILLS const urlMap = { status: '/api/games/characters', inventory: '/api/games/characters/inventories', skills: '/api/games/characters/skills/unequipped' }; - const res = await fetch(urlMap[panelId], {headers: {'Authorization': 'Bearer ' + savedToken}}); + const res = await fetch(urlMap[panelId], { + headers: {'Authorization': 'Bearer ' + savedToken} + }); const json = await res.json(); if (panelId === 'status') { const r = json.result; panel.innerHTML += ` -

👤 ${r.name}


-

🧠 Stats

-
    ${Object.entries(r.stats).map(([k, v]) => `
  • ${formatNum1(v)}
  • `).join('')}
-

⚔️ Real Stats

-
    ${Object.entries(r.realStat).map(([k, v]) => `
  • ${formatNum1(v)}
  • `).join('')}
-

💰 Gold

${formatNum1(r.gold)}

-

🎒 Items

- ${r.items.map(item => ` -
- ${item.name} [${item.grade} ${item.itemCategory}-${item.itemType}]
- ${item.description} -
    ${Object.entries(item) +

    👤 ${r.name}


    +

    🧠 Stats

    +
      ${Object.entries(r.stats).map(([k, v]) => `
    • ${formatNum1(v)}
    • `).join('')}
    +

    ⚔️ Real Stats

    +
      ${Object.entries(r.realStat).map(([k, v]) => `
    • ${formatNum1(v)}
    • `).join('')}
    +

    💰 Gold

    ${formatNum1(r.gold)}

    +

    🎒 Items

    +${r.items.map(item => ` +
    + ${item.name} [${item.grade} ${item.itemCategory}-${item.itemType}]
    + ${item.description} +
      ${Object.entries(item) .filter(([k]) => !['name', 'description', 'itemCategory', 'itemType', 'grade'].includes(k)) .map(([k, v]) => `
    • ${formatNum1(v)}
    • `).join('')} -
    -
    - `).join('')} -

    📚 Skills

    - ${r.skills.map(skill => ` -
    - ${skill.name} - [${skill.grade}-${skill.skillEffect}] - ${skill.slotType}
    - ${skill.skillDetails} -
    - `).join('')} - `; +
+
+`).join('')} +

📚 Skills

+${r.skills.map(skill => ` +
+ ${skill.name} + [${skill.grade}-${skill.skillEffect}]${skill.slotType}
+ ${skill.skillDetails} +
+`).join('')} +`; } else if (panelId === 'inventory') { panel.innerHTML += `

📦 INVENTORY

`; (json.result || []).forEach(item => { panel.innerHTML += ` -
- ${item.name} - [${item.grade} ${item.itemCategory}-${item.itemType}]
- ${item.description} -
    ${Object.entries(item) +
    + ${item.name} + [${item.grade} ${item.itemCategory}-${item.itemType}]
    + ${item.description} +
      ${Object.entries(item) .filter(([k]) => !['name', 'description', 'itemCategory', 'itemType', 'grade'].includes(k)) .map(([k, v]) => `
    • ${formatNum1(v)}
    • `).join('')} -
    -
    - `; +
+
+`; }); } else { // skills panel.innerHTML += `

🧠 SKILLS

`; json.result.forEach(skill => { panel.innerHTML += ` -
- ${skill.name} - [${skill.grade}-${skill.skillEffect}]
- ${skill.skillDetails} -
- `; +
+ ${skill.name} + [${skill.grade}-${skill.skillEffect}]
+ ${skill.skillDetails} +
+`; }); } }