From 1270beb2ca5c5974e98da0d0d1f33084e8b2b143 Mon Sep 17 00:00:00 2001 From: KII1ua Date: Wed, 17 Sep 2025 20:16:19 +0900 Subject: [PATCH 01/64] feature/166-potion use --- .../demo/service/GameSessionService.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/main/java/com/scriptopia/demo/service/GameSessionService.java b/src/main/java/com/scriptopia/demo/service/GameSessionService.java index 8987607..3381bf2 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -42,6 +42,7 @@ public class GameSessionService { private final FastApiService fastApiService; private final ItemService itemService; private final InGameMapper inGameMapper; + private final ItemDefRepository itemDefRepository; public ResponseEntity getGameSession(Long userid) { @@ -1004,4 +1005,43 @@ private RewardInfoMongo handleReward(GameSessionMongo gameSessionMongo, RewardTy } return rewardInfo; } + + @Transactional + public void usePotion(Long userId, String ItemId) { + GameSession gameSession = gameSessionRepository.findByMongoId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + GameSessionMongo gameSessionMongo = gameSessionMongoRepository.findById(gameSession.getMongoId()) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + PlayerInfoMongo playerInfo = gameSessionMongo.getPlayerInfo(); + + List items = gameSessionMongo.getInventory(); + + InventoryMongo targetItem = null; + for(InventoryMongo item : items) { + if(item.getItemDefId().equals(ItemId)) { + targetItem = item; + break; + } + } + + if(targetItem == null) { + throw new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND); + } + + com.scriptopia.demo.domain.ItemDef item = itemDefRepository.findById(Long.valueOf(ItemId)) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND)); + + if(item.getItemType() == ItemType.POTION) { + Integer life = playerInfo.getLife(); + Integer tmpLife = life + 1; + playerInfo.setLife(tmpLife); + + gameSessionMongoRepository.save(gameSessionMongo); + } + else { + throw new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND); + } + } } From b7ff01a3b4ab98b15f49536db65abdb370c37dd1 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 18 Sep 2025 00:13:30 +0900 Subject: [PATCH 02/64] =?UTF-8?q?refactor:=20change=20domainType=20Long=20?= =?UTF-8?q?to=20String=20mongoDB=5F=C3=A3ID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/scriptopia/demo/domain/mongo/ShopInfoMongo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/ShopInfoMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/ShopInfoMongo.java index 8518ba9..6c92c91 100644 --- a/src/main/java/com/scriptopia/demo/domain/mongo/ShopInfoMongo.java +++ b/src/main/java/com/scriptopia/demo/domain/mongo/ShopInfoMongo.java @@ -9,5 +9,5 @@ @AllArgsConstructor @NoArgsConstructor public class ShopInfoMongo { - private List itemDefId; + private List itemDefId; } From 61d3c2cd9eb771e94527c8af3c45d63dcfee81f9 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 18 Sep 2025 00:57:10 +0900 Subject: [PATCH 03/64] Refactor: update ChoiceResultType logic --- .../java/com/scriptopia/demo/domain/ChoiceResultType.java | 8 ++++---- .../demo/dto/gamesession/ingame/InGameShopTable.java | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopTable.java diff --git a/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java b/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java index e4c8f4a..84e455e 100644 --- a/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java +++ b/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java @@ -7,10 +7,10 @@ @Getter public enum ChoiceResultType { - BATTLE(20), - CHOICE(40), - DONE(45), - SHOP(5); + BATTLE(2), // 20, 40, 45, 5 + CHOICE(3), + DONE(5), + SHOP(90); private final int nextEventType; diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopTable.java b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopTable.java new file mode 100644 index 0000000..8abe36d --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopTable.java @@ -0,0 +1,4 @@ +package com.scriptopia.demo.dto.gamesession.ingame; + +public class InGameShopTable { +} From e267dde9784ba409ee4203c6a423a5891e32b273 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 18 Sep 2025 00:57:10 +0900 Subject: [PATCH 04/64] Feat: enhance InGameShopResponse DTO structure --- .../ingame/InGameShopResponse.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopResponse.java index 9f3021d..237c8f9 100644 --- a/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopResponse.java @@ -1,4 +1,31 @@ package com.scriptopia.demo.dto.gamesession.ingame; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor public class InGameShopResponse { + private String sceneType; + private LocalDateTime startedAt; + private LocalDateTime updatedAt; + private String background; + private String location; + private int progress; + private int stageSize; + + private InGamePlayerResponse playerInfo; // 외부 + private InGameNpcResponse npcInfo; // 외부 + private List inventory; // 외부 + + private List shopTable; // 외부 + + } From 7f725b029f6ff5693471677e45e651c8a27e4a34 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 18 Sep 2025 00:57:10 +0900 Subject: [PATCH 05/64] Feat: add fields and logic to InGameShopTable --- .../gamesession/ingame/InGameShopTable.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopTable.java b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopTable.java index 8abe36d..d377125 100644 --- a/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopTable.java +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopTable.java @@ -1,4 +1,41 @@ package com.scriptopia.demo.dto.gamesession.ingame; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder public class InGameShopTable { + + // 아이템 정의 정보 + private String name; + private String description; + private String itemPicSrc; + private String category; + private int baseStat; + private List itemEffects; + private int strength; + private int agility; + private int intelligence; + private int luck; + private String mainStat; + private String grade; + private int price; + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class ItemEffect { + private String itemEffectName; + private String itemEffectDescription; + private String grade; + } } From e103de16439ba1164e6a7f2421e006c2f8fa5750 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 18 Sep 2025 00:57:11 +0900 Subject: [PATCH 06/64] Refactor: adjust InGameMapper mapping for shop system --- .../scriptopia/demo/mapper/InGameMapper.java | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/mapper/InGameMapper.java b/src/main/java/com/scriptopia/demo/mapper/InGameMapper.java index 2871a7a..adb0204 100644 --- a/src/main/java/com/scriptopia/demo/mapper/InGameMapper.java +++ b/src/main/java/com/scriptopia/demo/mapper/InGameMapper.java @@ -2,10 +2,7 @@ import com.scriptopia.demo.domain.mongo.*; -import com.scriptopia.demo.dto.gamesession.ingame.InGameChoiceResponse; -import com.scriptopia.demo.dto.gamesession.ingame.InGameInventoryResponse; -import com.scriptopia.demo.dto.gamesession.ingame.InGameNpcResponse; -import com.scriptopia.demo.dto.gamesession.ingame.InGamePlayerResponse; +import com.scriptopia.demo.dto.gamesession.ingame.*; import com.scriptopia.demo.exception.CustomException; import com.scriptopia.demo.exception.ErrorCode; import com.scriptopia.demo.repository.mongo.ItemDefMongoRepository; @@ -106,6 +103,37 @@ public List mapChoice(ChoiceInfoMongo choiceInfo) { .toList(); } + public List mapShopTable(List createShopItems) { + return createShopItems.stream() + .map(shopItem -> { + ItemDefMongo itemDef = itemDefMongoRepository.findById(shopItem) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND)); + + return InGameShopTable.builder() + // 아이템 정의 정보 + .name(itemDef.getName()) + .description(itemDef.getDescription()) + .itemPicSrc(itemDef.getItemPicSrc()) + .category(itemDef.getCategory().name()) + .baseStat(itemDef.getBaseStat()) + .itemEffects(itemDef.getItemEffect().stream() + .map(e -> InGameShopTable.ItemEffect.builder() + .itemEffectName(e.getItemEffectName()) + .itemEffectDescription(e.getItemEffectDescription()) + .grade(e.getEffectProbability().name()) + .build()) + .toList()) + .strength(itemDef.getStrength()) + .agility(itemDef.getAgility()) + .intelligence(itemDef.getIntelligence()) + .luck(itemDef.getLuck()) + .mainStat(itemDef.getMainStat().name()) + .grade(itemDef.getGrade().name()) + .price(itemDef.getPrice().intValue()) + .build(); + }) + .toList(); + } } From 427a7c3ae3e8ed5c126513caad98ba0493803d71 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 18 Sep 2025 00:57:11 +0900 Subject: [PATCH 07/64] Feat: implement in-game shop handling logic in GameSessionService --- .../demo/service/GameSessionService.java | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/scriptopia/demo/service/GameSessionService.java b/src/main/java/com/scriptopia/demo/service/GameSessionService.java index 3381bf2..2fb8760 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -3,6 +3,7 @@ import com.scriptopia.demo.dto.gamesession.ingame.InGameBattleResponse; import com.scriptopia.demo.dto.gamesession.ingame.InGameChoiceResponse; import com.scriptopia.demo.dto.gamesession.ingame.InGameDoneResponse; +import com.scriptopia.demo.dto.gamesession.ingame.InGameShopResponse; import com.scriptopia.demo.dto.items.ItemDefRequest; import com.scriptopia.demo.dto.items.ItemFastApiResponse; import com.scriptopia.demo.mapper.InGameMapper; @@ -339,6 +340,20 @@ public Object getInGameDataDto(Long userId){ } else if (currentSceneType == SceneType.SHOP) { + return InGameShopResponse.builder() + .sceneType("SHOP") + .startedAt(gameSessionMongo.getStartedAt()) + .updatedAt(LocalDateTime.now()) + .background(gameSessionMongo.getBackground()) + .location(gameSessionMongo.getLocation()) + .stageSize(gameSessionMongo.getStage() != null ? gameSessionMongo.getStage().size() : 0) + .playerInfo(inGameMapper.mapPlayer(gameSessionMongo.getPlayerInfo())) + .npcInfo(inGameMapper.mapNpc(gameSessionMongo.getNpcInfo())) + .inventory(inGameMapper.mapInventory(gameSessionMongo.getInventory())) + .shopTable(inGameMapper.mapShopTable(gameSessionMongo.getShopInfo().getItemDefId())) + .build(); + + } else if (currentSceneType == SceneType.BATTLE) { BattleInfoMongo battleInfo = gameSessionMongo.getBattleInfo(); @@ -408,6 +423,9 @@ public GameSessionMongo gameProgress(Long userId) { case SceneType.DONE -> { gameToChoice(userId); } + case SceneType.SHOP -> { + gameToChoice(userId); + } default -> throw new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND); } @@ -807,7 +825,7 @@ public GameSessionMongo gameChoiceSelect(Long userId, GameChoiceRequest request) ChoiceResultType nextScene = choiceMongo.getResultType(); boolean isPass = GameBalanceUtil.isPass(probability); - RewardInfoMongo rewardInfo; + RewardInfoMongo rewardInfo = null; switch (nextScene) { case CHOICE -> { @@ -827,6 +845,33 @@ public GameSessionMongo gameChoiceSelect(Long userId, GameChoiceRequest request) gameSessionMongo = gameSessionMongoRepository.findById(gameId).get(); rewardInfo = handleReward(gameSessionMongo, rewardType, isPass); } + case SHOP -> { + gameSessionMongo = gameSessionMongoRepository.findById(gameId).get(); + List createdItems = gameSessionMongo.getCreatedItems(); + List createShopItems = new ArrayList<>(); + + ItemDefRequest itemDefRequest = ItemDefRequest.builder() + .worldView(gameSessionMongo.getHistoryInfo().getWorldView()) + .location(gameSessionMongo.getLocation()) + .playerTrait(null) + .previousStory(gameSessionMongo.getBackground()) + .build(); + + for (int i=0; i<3; i+=1){ + String itemMongoId = itemService.createItemInGame(itemDefRequest); + createdItems.add(itemMongoId); + createShopItems.add(itemMongoId); + } + + ShopInfoMongo shopInfoMongo = ShopInfoMongo.builder() + .itemDefId(createShopItems) + .build(); + + gameSessionMongo.setCreatedItems(createdItems); + gameSessionMongo.setShopInfo(shopInfoMongo); + gameSessionMongo.setSceneType(SceneType.SHOP); + + } default -> throw new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND); } From afd98dff27def5e5174fa2075376484da21434da Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 18 Sep 2025 01:43:06 +0900 Subject: [PATCH 08/64] Feat: create 409 ERROR --- src/main/java/com/scriptopia/demo/exception/ErrorCode.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/scriptopia/demo/exception/ErrorCode.java b/src/main/java/com/scriptopia/demo/exception/ErrorCode.java index aaa4aa7..3e42905 100644 --- a/src/main/java/com/scriptopia/demo/exception/ErrorCode.java +++ b/src/main/java/com/scriptopia/demo/exception/ErrorCode.java @@ -84,6 +84,7 @@ public enum ErrorCode { E_409_REFRESH_REUSE_DETECTED("E409003", "리프레시 토큰 재사용이 감지되었습니다.", HttpStatus.CONFLICT), E_409_PASSWORD_SAME_AS_OLD("E409004","기존 비밀번호와 동일한 비밀번호는 사용할 수 없습니다.",HttpStatus.CONFLICT), E_409_ALREADY_CONFIRMED("E409005","이미 정산이 완료된 항목입니다.", HttpStatus.CONFLICT), + E_409_NOT_ENOUGH_MONEY("E4090056", "보유 금액이 부족합니다.", HttpStatus.CONFLICT), //412 Precondition Failed From 51a36a8c6808013d5f2401345f306694db77eada Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 18 Sep 2025 01:54:26 +0900 Subject: [PATCH 09/64] feat: create item buy to shop logic --- .../controller/GameSessionController.java | 14 ++++++ .../demo/service/GameSessionService.java | 43 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java index ded8895..cb75945 100644 --- a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java +++ b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java @@ -125,6 +125,20 @@ public ResponseEntity dropItem( return ResponseEntity.ok(response); } + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @DeleteMapping("/buyItem/{gameId}/{itemId}") + public ResponseEntity buyItem( + @PathVariable("gameId") String gameId, + @PathVariable("itemId") String itemId, + Authentication authentication) throws JsonProcessingException { + + Long userId = Long.valueOf(authentication.getName()); + + GameSessionMongo response = gameSessionService.gameBuyItem(userId, itemId); + + return ResponseEntity.ok(response); + } + /* * 게임 -> 기존 게임 조회 diff --git a/src/main/java/com/scriptopia/demo/service/GameSessionService.java b/src/main/java/com/scriptopia/demo/service/GameSessionService.java index 2fb8760..0bf7698 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -961,6 +961,49 @@ public GameSessionMongo gameDropItem(Long userId, String itemId) { } + @Transactional + public GameSessionMongo gameBuyItem(Long userId, String itemId) { + GameSession gameSession = gameSessionRepository.findByMongoId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + GameSessionMongo gameSessionMongo = gameSessionMongoRepository.findById(gameSession.getMongoId()) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + PlayerInfoMongo playerInfo = gameSessionMongo.getPlayerInfo(); + List inventory = gameSessionMongo.getInventory(); + ShopInfoMongo shopInfoMongo = gameSessionMongo.getShopInfo(); + List shopItems = shopInfoMongo.getItemDefId(); + + long playerGold = playerInfo.getGold(); + + + ItemDefMongo targetDef = itemDefMongoRepository.findById(itemId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND)); + + long shopItemGold = targetDef.getPrice(); + + if(playerGold < shopItemGold){ + throw new CustomException(ErrorCode.E_409_NOT_ENOUGH_MONEY); + } + + + InventoryMongo buyItem = InventoryMongo.builder() + .itemDefId(itemId) + .acquiredAt(LocalDateTime.now()) + .equipped(false) + .source("item shop") + .build(); + + inventory.add(buyItem); + shopItems.remove(itemId); + shopInfoMongo.setItemDefId(shopItems); + + gameSessionMongo.setInventory(inventory); + gameSessionMongo.setShopInfo(shopInfoMongo); + + return gameSessionMongoRepository.save(gameSessionMongo); + } + private void addStats(PlayerInfoMongo player, ItemDefMongo item) { player.setStrength(player.getStrength() + safeStat(item.getStrength())); From 400b165241568af442413c30121d07c0f87662f4 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 18 Sep 2025 01:58:37 +0900 Subject: [PATCH 10/64] Feat: add new 409 error if not sceneType --- src/main/java/com/scriptopia/demo/exception/ErrorCode.java | 3 ++- .../java/com/scriptopia/demo/service/GameSessionService.java | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/scriptopia/demo/exception/ErrorCode.java b/src/main/java/com/scriptopia/demo/exception/ErrorCode.java index 3e42905..0296779 100644 --- a/src/main/java/com/scriptopia/demo/exception/ErrorCode.java +++ b/src/main/java/com/scriptopia/demo/exception/ErrorCode.java @@ -84,7 +84,8 @@ public enum ErrorCode { E_409_REFRESH_REUSE_DETECTED("E409003", "리프레시 토큰 재사용이 감지되었습니다.", HttpStatus.CONFLICT), E_409_PASSWORD_SAME_AS_OLD("E409004","기존 비밀번호와 동일한 비밀번호는 사용할 수 없습니다.",HttpStatus.CONFLICT), E_409_ALREADY_CONFIRMED("E409005","이미 정산이 완료된 항목입니다.", HttpStatus.CONFLICT), - E_409_NOT_ENOUGH_MONEY("E4090056", "보유 금액이 부족합니다.", HttpStatus.CONFLICT), + E_409_NOT_ENOUGH_MONEY("E409006", "보유 금액이 부족합니다.", HttpStatus.CONFLICT), + E_409_NOT_THIS_SCENE("E409007", "적합하지 않은 곳입니다.", HttpStatus.CONFLICT), //412 Precondition Failed diff --git a/src/main/java/com/scriptopia/demo/service/GameSessionService.java b/src/main/java/com/scriptopia/demo/service/GameSessionService.java index 0bf7698..2ca7fb2 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -969,6 +969,10 @@ public GameSessionMongo gameBuyItem(Long userId, String itemId) { GameSessionMongo gameSessionMongo = gameSessionMongoRepository.findById(gameSession.getMongoId()) .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + if (gameSessionMongo.getSceneType() != SceneType.SHOP) { + throw new CustomException(ErrorCode.E_409_NOT_THIS_SCENE); + } + PlayerInfoMongo playerInfo = gameSessionMongo.getPlayerInfo(); List inventory = gameSessionMongo.getInventory(); ShopInfoMongo shopInfoMongo = gameSessionMongo.getShopInfo(); From 5a37a64d0c223bdf50001d39801164a81b4d6552 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 18 Sep 2025 02:27:20 +0900 Subject: [PATCH 11/64] refactor: add buy/sell item endpoints to GameSessionController --- .../demo/controller/GameSessionController.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java index cb75945..7afc836 100644 --- a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java +++ b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java @@ -139,6 +139,19 @@ public ResponseEntity buyItem( return ResponseEntity.ok(response); } + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @DeleteMapping("/sellItem/{gameId}/{itemId}") + public ResponseEntity sellItem( + @PathVariable("gameId") String gameId, + @PathVariable("itemId") String itemId, + Authentication authentication) throws JsonProcessingException { + + Long userId = Long.valueOf(authentication.getName()); + + GameSessionMongo response = gameSessionService.gameSellItem(userId, itemId); + + return ResponseEntity.ok(response); + } /* * 게임 -> 기존 게임 조회 From e68f6e1e86be067750dce4411e036213fcf2c25a Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 18 Sep 2025 02:27:23 +0900 Subject: [PATCH 12/64] refactor: add NOT_ENOUGH_MONEY and DONT_SELL_EQUIPPED_ITEM error codes --- src/main/java/com/scriptopia/demo/exception/ErrorCode.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/scriptopia/demo/exception/ErrorCode.java b/src/main/java/com/scriptopia/demo/exception/ErrorCode.java index 0296779..792f854 100644 --- a/src/main/java/com/scriptopia/demo/exception/ErrorCode.java +++ b/src/main/java/com/scriptopia/demo/exception/ErrorCode.java @@ -86,6 +86,8 @@ public enum ErrorCode { E_409_ALREADY_CONFIRMED("E409005","이미 정산이 완료된 항목입니다.", HttpStatus.CONFLICT), E_409_NOT_ENOUGH_MONEY("E409006", "보유 금액이 부족합니다.", HttpStatus.CONFLICT), E_409_NOT_THIS_SCENE("E409007", "적합하지 않은 곳입니다.", HttpStatus.CONFLICT), + E_409_DONT_SELL_EQUIPPED_ITEM("E409008", "착용중인 아이템은 팔 수 없습니다.", HttpStatus.CONFLICT), + E_404_ITEM_NOT_IN_SHOP("E409009", "아이템이 이미 팔렸습니다.", HttpStatus.CONFLICT), //412 Precondition Failed From 2939cdfc2a80c192c3a91e12318a7232675a70a6 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 18 Sep 2025 02:27:26 +0900 Subject: [PATCH 13/64] refactor: implement gameBuyItem and gameSellItem with validation and inventory update --- .../demo/service/GameSessionService.java | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/service/GameSessionService.java b/src/main/java/com/scriptopia/demo/service/GameSessionService.java index 2ca7fb2..8c2db90 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -980,6 +980,9 @@ public GameSessionMongo gameBuyItem(Long userId, String itemId) { long playerGold = playerInfo.getGold(); + if (!shopItems.contains(itemId)) { + throw new CustomException(ErrorCode.E_404_ITEM_NOT_IN_SHOP); + } ItemDefMongo targetDef = itemDefMongoRepository.findById(itemId) .orElseThrow(() -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND)); @@ -990,6 +993,12 @@ public GameSessionMongo gameBuyItem(Long userId, String itemId) { throw new CustomException(ErrorCode.E_409_NOT_ENOUGH_MONEY); } + playerGold = playerGold - shopItemGold; + playerInfo.setGold(playerGold); + + + shopItems.remove(itemId); + shopInfoMongo.setItemDefId(shopItems); InventoryMongo buyItem = InventoryMongo.builder() .itemDefId(itemId) @@ -999,11 +1008,50 @@ public GameSessionMongo gameBuyItem(Long userId, String itemId) { .build(); inventory.add(buyItem); - shopItems.remove(itemId); - shopInfoMongo.setItemDefId(shopItems); + gameSessionMongo.setInventory(inventory); gameSessionMongo.setShopInfo(shopInfoMongo); + gameSessionMongo.setPlayerInfo(playerInfo); + + return gameSessionMongoRepository.save(gameSessionMongo); + } + + @Transactional + public GameSessionMongo gameSellItem(Long userId, String itemId) { + GameSession gameSession = gameSessionRepository.findByMongoId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + GameSessionMongo gameSessionMongo = gameSessionMongoRepository.findById(gameSession.getMongoId()) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + if (gameSessionMongo.getSceneType() != SceneType.SHOP) { + throw new CustomException(ErrorCode.E_409_NOT_THIS_SCENE); + } + + PlayerInfoMongo playerInfo = gameSessionMongo.getPlayerInfo(); + List inventory = gameSessionMongo.getInventory(); + long playerGold = playerInfo.getGold(); + + InventoryMongo targetInventory = inventory.stream() + .filter(inv -> inv.getItemDefId().equals(itemId)) + .findFirst() + .orElseThrow(() -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND)); + + if (targetInventory.isEquipped()) { + throw new CustomException(ErrorCode.E_409_DONT_SELL_EQUIPPED_ITEM); + } + + ItemDefMongo targetDef = itemDefMongoRepository.findById(itemId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND)); + + playerGold = playerGold + targetDef.getPrice(); + + inventory.remove(targetInventory); + playerInfo.setGold(playerGold); + + gameSessionMongo.setInventory(inventory); + gameSessionMongo.setPlayerInfo(playerInfo); return gameSessionMongoRepository.save(gameSessionMongo); } From 1ebd9e25f6dfb9f6e61ebd14a0c836307d297180 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 18 Sep 2025 02:32:50 +0900 Subject: [PATCH 14/64] refactor: add shop domain itemDefMongo ID --- .../scriptopia/demo/dto/gamesession/ingame/InGameShopTable.java | 1 + src/main/java/com/scriptopia/demo/mapper/InGameMapper.java | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopTable.java b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopTable.java index d377125..9eb1284 100644 --- a/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopTable.java +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopTable.java @@ -15,6 +15,7 @@ public class InGameShopTable { // 아이템 정의 정보 + private String shopItemId; private String name; private String description; private String itemPicSrc; diff --git a/src/main/java/com/scriptopia/demo/mapper/InGameMapper.java b/src/main/java/com/scriptopia/demo/mapper/InGameMapper.java index adb0204..3a71ba9 100644 --- a/src/main/java/com/scriptopia/demo/mapper/InGameMapper.java +++ b/src/main/java/com/scriptopia/demo/mapper/InGameMapper.java @@ -112,6 +112,7 @@ public List mapShopTable(List createShopItems) { return InGameShopTable.builder() // 아이템 정의 정보 + .shopItemId(itemDef.getId()) .name(itemDef.getName()) .description(itemDef.getDescription()) .itemPicSrc(itemDef.getItemPicSrc()) From 6f1b2e88db631498f607544473e20046a8018984 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 18 Sep 2025 02:49:14 +0900 Subject: [PATCH 15/64] refactor: change endpint and before merge --- .../com/scriptopia/demo/controller/GameSessionController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java index 7afc836..482c045 100644 --- a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java +++ b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java @@ -126,7 +126,7 @@ public ResponseEntity dropItem( } @PreAuthorize("hasAnyAuthority('USER','ADMIN')") - @DeleteMapping("/buyItem/{gameId}/{itemId}") + @PostMapping("/{gameId}/items/purchase/{itemId}") public ResponseEntity buyItem( @PathVariable("gameId") String gameId, @PathVariable("itemId") String itemId, @@ -140,7 +140,7 @@ public ResponseEntity buyItem( } @PreAuthorize("hasAnyAuthority('USER','ADMIN')") - @DeleteMapping("/sellItem/{gameId}/{itemId}") + @PostMapping("/{gameId}/items/sell/{itemId}") public ResponseEntity sellItem( @PathVariable("gameId") String gameId, @PathVariable("itemId") String itemId, From de8dba4ab792123cca235fcaa189b6ee6585d6e3 Mon Sep 17 00:00:00 2001 From: junseo Lee <74139368+juns0720@users.noreply.github.com> Date: Sat, 20 Sep 2025 16:33:27 +0900 Subject: [PATCH 16/64] feat: update entity attribute name --- src/main/java/com/scriptopia/demo/domain/ItemDef.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/domain/ItemDef.java b/src/main/java/com/scriptopia/demo/domain/ItemDef.java index e3d935c..34e94b6 100644 --- a/src/main/java/com/scriptopia/demo/domain/ItemDef.java +++ b/src/main/java/com/scriptopia/demo/domain/ItemDef.java @@ -20,6 +20,9 @@ public class ItemDef { @ManyToOne(fetch = FetchType.LAZY) private ItemGradeDef itemGradeDef; + @OneToMany(mappedBy = "itemDef", cascade = CascadeType.ALL, orphanRemoval = true) + private List itemEffects = new ArrayList<>(); + private String name; @Column(columnDefinition = "TEXT") @@ -43,7 +46,6 @@ public class ItemDef { private Long price; - @OneToMany(mappedBy = "itemDef", cascade = CascadeType.ALL, orphanRemoval = true) - private List itemEffects = new ArrayList<>(); + } \ No newline at end of file From 5ccbdc1143d7c8ff8eea05bcfe2586177efe50c2 Mon Sep 17 00:00:00 2001 From: juns0720 Date: Sat, 20 Sep 2025 18:04:14 +0900 Subject: [PATCH 17/64] feat: create localDataSeeder initializer --- .../demo/config/LocalDataSeeder.java | 661 ++++++++++++++++++ 1 file changed, 661 insertions(+) create mode 100644 src/main/java/com/scriptopia/demo/config/LocalDataSeeder.java diff --git a/src/main/java/com/scriptopia/demo/config/LocalDataSeeder.java b/src/main/java/com/scriptopia/demo/config/LocalDataSeeder.java new file mode 100644 index 0000000..ed2075b --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/LocalDataSeeder.java @@ -0,0 +1,661 @@ +package com.scriptopia.demo.config; + +import com.scriptopia.demo.domain.*; +import com.scriptopia.demo.repository.*; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +/** + * 로컬 개발용 초기 데이터 시드 + * - Pia 캐시템 3개 (100~300) + * - 로컬 계정 유저 2명(userA, userB) + UserSetting 초기값 + PIA 2000 + * - 각 유저: 랜덤 아이템 20개 보유, 그 중 15개 경매장에 등록 + * - 각 유저: 캐릭터 이미지 5개 + * - 각 유저: 히스토리 4개 (그중 2개는 공유, 태그 매핑, 평점/즐겨찾기 약간) + * - 태그 10개 생성 + * - A↔B 상호 거래 5건 완료(정산 Settlement 기록 포함) + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class LocalDataSeeder implements ApplicationRunner { + + private final ItemDefRepository itemDefRepository; + private final ItemGradeDefRepository itemGradeDefRepository; + private final EffectGradeDefRepository effectGradeDefRepository; + + private final UserRepository userRepository; + private final LocalAccountRepository localAccountRepository; + private final UserSettingRepository userSettingRepository; + + private final PiaItemRepository piaItemRepository; + private final UserItemRepository userItemRepository; + private final AuctionRepository auctionRepository; + + private final TagDefRepository tagDefRepository; + private final SharedGameRepository sharedGameRepository; + private final HistoryRepository historyRepository; + private final GameTagRepository gameTagRepository; + private final SharedGameScoreRepository sharedGameScoreRepository; + private final SharedGameFavoriteRepository sharedGameFavoriteRepository; + private final UserCharacterImgRepository userCharacterImgRepository; + + private final SettlementRepository settlementRepository; + + private final PasswordEncoder passwordEncoder; + + @Override + @Transactional + public void run(ApplicationArguments args) { + log.info("=== LocalDataSeeder: start ==="); + + // 0) 이미 데이터가 있으면 중복 시드 방지 + if (userRepository.count() > 1) { + log.info("Users already exist. Skip seeding."); + return; + } + + // 1) 태그 10개 + List tags = ensureTags(); + + // 2) Pia 캐시 아이템 3개 + ensurePiaItems(); + + // 3) 정의 테이블(등급, 효과 등급) + Map gradeMap = ensureItemGradeDefs(); + Map effectGradeMap = ensureEffectGradeDefs(); + + // 4) 아이템 카탈로그 + 아이템 효과 + ensureItemCatalog(gradeMap, effectGradeMap); + + // 5) 유저 2명 + 로컬계정 + 설정 + pia=2000 + User userA = createUserWithLocal("userA", "userA@example.com", "userA!234"); + User userB = createUserWithLocal("userB", "userB@example.com", "userB!234"); + setUserSettingDefaults(userA); + setUserSettingDefaults(userB); + setPia(userA, 2000); + setPia(userB, 2000); + + // 6) 캐릭터 이미지 5장씩 + addCharacterImages(userA, 5); + addCharacterImages(userB, 5); + + // 7) 유저 인벤토리 20개(그중 15개 경매 등록) + List invA = createRandomInventory(userA, 20); + List invB = createRandomInventory(userB, 20); + List aucA = listFirstNOnAuction(invA, 15); // A가 올린 경매 + List aucB = listFirstNOnAuction(invB, 15); // B가 올린 경매 + + // 8) 히스토리 4개(그중 2개 공유+태그/평점/즐겨찾기) + createHistoriesAndShared(userA, tags); + createHistoriesAndShared(userB, tags); + + // 9) A↔B 상호 거래 5건 완료(정산 기록 포함) + createTradeLogs(userA, userB, aucA, aucB, 5); + + log.info("[seed] done"); + } + + /* ===================================================================================== + 태그 / PIA (정의 테이블) + ===================================================================================== */ + + private List ensureTags() { + if (tagDefRepository.count() >= 10) { + return tagDefRepository.findAll(); + } + List names = List.of("로맨스","판타지","추리","던전","해적","학교물","느와르","우주","요리","타임리프"); + List list = new ArrayList<>(); + for (String n : names) { + TagDef t = new TagDef(); + t.setTagName(n); + list.add(t); + } + return tagDefRepository.saveAll(list); + } + + private void ensurePiaItems() { + if (piaItemRepository.count() >= 3) return; + PiaItem p1 = new PiaItem(); p1.setName("아이템 모루"); p1.setPrice(200L); p1.setDescription("장비 강화용"); + PiaItem p2 = new PiaItem(); p2.setName("연마석"); p2.setPrice(randL(120, 180)); p2.setDescription("날카로움 보정"); + PiaItem p3 = new PiaItem(); p3.setName("수선 키트"); p3.setPrice(randL(100, 160)); p3.setDescription("내구도 회복"); + piaItemRepository.saveAll(List.of(p1,p2,p3)); + } + + /* ===================================================================================== + ItemGradeDef / EffectGradeDef (정의 테이블) + ===================================================================================== */ + + private Map ensureItemGradeDefs() { + List existing = itemGradeDefRepository.findAll(); + Map byEnum = existing.stream() + .collect(Collectors.toMap(ItemGradeDef::getGrade, x -> x)); + + List toSave = new ArrayList<>(); + for (Grade g : Grade.values()) { + if (byEnum.containsKey(g)) continue; + ItemGradeDef def = new ItemGradeDef(); + def.setGrade(g); + def.setWeight(g.getDropRate()); + long base = Math.round((g.getAttackPower() + g.getDefensePower()) / 2.0); + def.setPrice(base * 10L); + toSave.add(def); + } + if (!toSave.isEmpty()) { + itemGradeDefRepository.saveAll(toSave); + toSave.forEach(d -> byEnum.put(d.getGrade(), d)); + } + return byEnum; + } + + private Map ensureEffectGradeDefs() { + List existing = effectGradeDefRepository.findAll(); + Map byEnum = existing.stream() + .collect(Collectors.toMap(EffectGradeDef::getEffectProbability, x -> x)); + + List toSave = new ArrayList<>(); + for (EffectProbability p : EffectProbability.values()) { + if (byEnum.containsKey(p)) continue; + EffectGradeDef def = new EffectGradeDef(); + def.setEffectProbability(p); + def.setWeight( + switch (p) { + case COMMON -> 50d; case UNCOMMON -> 30d; case RARE -> 12d; case EPIC -> 6d; case LEGENDARY -> 2d; + } + ); + def.setPrice( + switch (p) { + case COMMON -> 20L; case UNCOMMON -> 60L; case RARE -> 150L; case EPIC -> 400L; case LEGENDARY -> 1000L; + } + ); + toSave.add(def); + } + if (!toSave.isEmpty()) { + effectGradeDefRepository.saveAll(toSave); + toSave.forEach(d -> byEnum.put(d.getEffectProbability(), d)); + } + return byEnum; + } + + /* ===================================================================================== + ItemDef + ItemEffect (연관관계 제대로 연결) — 컨셉 기반 + ===================================================================================== */ + + private void ensureItemCatalog(Map gradeMap, + Map effectGradeMap) { + if (itemDefRepository.count() >= 32) return; // 컨셉 다양화라 넉넉히 + + List toSave = new ArrayList<>(); + + for (ItemType type : ItemType.values()) { + for (Grade g : Grade.values()) { + int perCombo = 2; // 컨셉 섞어 2개씩 생성 + for (int i = 0; i < perCombo; i++) { + String concept = pickConcept(); + + ItemDef d = new ItemDef(); + d.setItemGradeDef(gradeMap.get(g)); + d.setItemType(type); + d.setMainStat(Stat.getRandomMainStat()); + + // 이름/설명/이미지 + d.setName(buildItemName(concept, type, g, d.getMainStat())); + d.setDescription(buildItemDescription(concept, type, g, d.getMainStat())); + d.setPicSrc(picsumWithSeed(300, 400, concept + "-" + type + "-" + g)); + + // 능력치: 등급 기반 + 컨셉/타입 보정 + int base = Grade.getRandomBaseStat(type, g); + int conceptBonus = conceptBaseBonus(concept, type, d.getMainStat()); + d.setBaseStat(Math.max(1, base + conceptBonus)); + d.setStrength(rand(0, 10)); + d.setAgility(rand(0, 10)); + d.setIntelligence(rand(0, 10)); + d.setLuck(rand(0, 10)); + + d.setCreatedAt(LocalDateTime.now()); + + long basePrice = Optional.ofNullable(d.getItemGradeDef().getPrice()).orElse(100L); + long optSum = nz(d.getStrength()) + nz(d.getAgility()) + nz(d.getIntelligence()) + nz(d.getLuck()); + long conceptPremium = conceptPricePremium(concept, type, g); + d.setPrice(basePrice + optSum * 5 + conceptPremium); + + // 효과 0~3개 + int effectCnt = rand(0, 3); + for (int k = 0; k < effectCnt; k++) { + EffectProbability picked = EffectProbability.getRandomEffectGradeByWeaponGrade(g); + if (picked == null) continue; + EffectGradeDef egd = effectGradeMap.get(picked); + if (egd == null) continue; + + ItemEffect ef = new ItemEffect(); + ef.setItemDef(d); + ef.setEffectGradeDef(egd); + ef.setEffectName(randomEffectName(concept, d.getItemType(), d.getMainStat(), picked)); + ef.setEffectDescription("[" + picked.name() + "] " + conceptTagline(concept)); + d.getItemEffects().add(ef); + } + + toSave.add(d); + } + } + } + + itemDefRepository.saveAll(toSave); + } + + /* ===================================================================================== + 유저/설정/PIA/캐릭터이미지/인벤토리/경매/히스토리/공유/정산 + ===================================================================================== */ + + private User createUserWithLocal(String nickname, String email, String rawPw) { + User u = new User(); + u.setNickname(nickname); + u.setRole(Role.USER); + u.setLoginType(LoginType.LOCAL); + u.setCreatedAt(LocalDateTime.now()); + u.setLastLoginAt(LocalDateTime.now()); + u.setProfileImgUrl(picsum(256,256)); + userRepository.save(u); + + LocalAccount acc = new LocalAccount(); + acc.setUser(u); + acc.setEmail(email); + acc.setPassword(passwordEncoder.encode(rawPw)); + acc.setStatus(UserStatus.VERIFIED); + acc.setUpdatedAt(LocalDateTime.now()); + localAccountRepository.save(acc); + + return u; + } + + private void setUserSettingDefaults(User user) { + UserSetting s = new UserSetting(); + s.setUser(user); + s.setTheme(Theme.DARK); + s.setFontType(FontType.PretendardVariable); + s.setFontSize(16); + s.setLineHeight(1); + s.setWordSpacing(1); + s.setUpdatedAt(LocalDateTime.now()); + userSettingRepository.save(s); + } + + private void setPia(User user, long amount) { + user.setPia(amount); + userRepository.save(user); + } + + private void addCharacterImages(User user, int count) { + List imgs = new ArrayList<>(); + for (int i = 0; i < count; i++) { + UserCharacterImg img = new UserCharacterImg(); + img.setUser(user); + img.setImgUrl("https://picsum.photos/seed/" + user.getId() + "-" + i + "/256/256"); + imgs.add(img); + } + userCharacterImgRepository.saveAll(imgs); + } + + private List createRandomInventory(User owner, int count) { + List catalog = itemDefRepository.findAll(); + if (catalog.isEmpty()) throw new IllegalStateException("Item catalog empty"); + List items = new ArrayList<>(); + for (int i = 0; i < count; i++) { + ItemDef base = catalog.get(rand(0, catalog.size()-1)); + UserItem ui = new UserItem(); + ui.setUser(owner); + ui.setItemDef(base); + ui.setTradeStatus(TradeStatus.OWNED); + ui.setRemainingUses(rand(0,10)); + items.add(ui); + } + return userItemRepository.saveAll(items); + } + + private List listFirstNOnAuction(List items, int n) { + List created = new ArrayList<>(); + items.stream().limit(n).forEach(ui -> { + ui.setTradeStatus(TradeStatus.LISTED); + userItemRepository.save(ui); + + Auction a = new Auction(); + a.setUserItem(ui); + a.setPrice(randL(200, 1800)); + a.setCreatedAt(LocalDateTime.now().minusHours(rand(0,48))); + // 미완료 상태(tradedAt=null) + created.add(auctionRepository.save(a)); + }); + return created; + } + + private void createHistoriesAndShared(User user, List tags) { + // 히스토리 4개 + List histories = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + History h = new History(); + h.setUser(user); + h.setUuid(UUID.randomUUID()); + h.setThumbnailUrl(picsum(300, 400)); + h.setTitle(user.getNickname() + "의 모험 #" + (i+1)); + h.setScore((long) rand(60,98)); + h.setCreatedAt(LocalDateTime.now().minusDays(rand(0,10))); + h.setIsShared(false); + histories.add(h); + } + historyRepository.saveAll(histories); + + // 그중 2개 공유 + for (int i = 0; i < 2; i++) { + History h = histories.get(i); + h.setIsShared(true); + historyRepository.save(h); + + SharedGame sg = new SharedGame(); + sg.setUser(user); + sg.setUuid(h.getUuid()); + sg.setThumbnailUrl(h.getThumbnailUrl()); + sg.setTitle(h.getTitle()); + sg.setWorldView("임시 세계관"); + sg.setBackgroundStory("임시 배경"); + sg.setSharedAt(LocalDateTime.now()); + sharedGameRepository.save(sg); + + // 태그 2~5개 + Collections.shuffle(tags); + int tagCount = rand(2,5); + for (int k=0; k aucA, List aucB, + int totalTrades) { + Collections.shuffle(aucA); + Collections.shuffle(aucB); + + int byB = Math.min(totalTrades / 2, aucA.size()); // B가 A 물건 구매 + int byA = Math.min(totalTrades - byB, aucB.size()); // A가 B 물건 구매 + + for (int i = 0; i < byB; i++) finalizeTrade(userA, userB, aucA.get(i)); + for (int i = 0; i < byA; i++) finalizeTrade(userB, userA, aucB.get(i)); + } + + /** 단일 거래 완료 처리 */ + private void finalizeTrade(User seller, User buyer, Auction auction) { + if (auction.getTradedAt() != null) return; // 이미 완료된 거래 + + UserItem ui = auction.getUserItem(); + long price = Optional.ofNullable(auction.getPrice()).orElse(0L); + + // 구매자 잔액 확인(부족하면 스킵) + if (buyer.getPia() == null || buyer.getPia() < price) return; + + // 정산 + buyer.setPia(buyer.getPia() - price); + seller.setPia(Optional.ofNullable(seller.getPia()).orElse(0L) + price); + userRepository.saveAll(List.of(buyer, seller)); + + // 소유권 이전 + 상태 변경 + ui.setUser(buyer); + ui.setTradeStatus(TradeStatus.OWNED); + userItemRepository.save(ui); + + // 경매 완료 시간 기록 + auction.setTradedAt(LocalDateTime.now()); + auctionRepository.save(auction); + + ItemDef itemDef = ui.getItemDef(); + + // Settlement: 판매자(SELL) + Settlement sellSettle = new Settlement(); + sellSettle.setUser(seller); + sellSettle.setItemDef(itemDef); + sellSettle.setTradeType(TradeType.SELL); // enum: SELL/BUY 필요 + sellSettle.setPrice(price); + sellSettle.setCreatedAt(LocalDateTime.now()); + sellSettle.setSettledAt(LocalDateTime.now()); + settlementRepository.save(sellSettle); + + // Settlement: 구매자(BUY) + Settlement buySettle = new Settlement(); + buySettle.setUser(buyer); + buySettle.setItemDef(itemDef); + buySettle.setTradeType(TradeType.BUY); + buySettle.setPrice(price); + buySettle.setCreatedAt(LocalDateTime.now()); + buySettle.setSettledAt(LocalDateTime.now()); + settlementRepository.save(buySettle); + } + + /* ===================================================================================== + 유틸 + ===================================================================================== */ + + // ======================= 컨셉 사전 ======================= + private static final List CONCEPTS = List.of( + "스팀펑크", "사이버네온", "암흑 판타지", "동양 무협", "우주 SF", + "요리 배틀", "해적 시대", "포스트 아포칼립스", "중세 마법학원", "바이오펑크" + ); + + private static String pickConcept() { return CONCEPTS.get(rand(0, CONCEPTS.size()-1)); } + + private static String conceptTagline(String concept) { + return switch (concept) { + case "스팀펑크" -> "황동과 기어의 울림"; + case "사이버네온" -> "빛번짐 속 프로토콜"; + case "암흑 판타지" -> "어둠이 가르는 맹세"; + case "동양 무협" -> "내공과 검기"; + case "우주 SF" -> "진공 너머 특이점"; + case "요리 배틀" -> "칼끝에서 피어나는 풍미"; + case "해적 시대" -> "검과 파도, 검은 깃발"; + case "포스트 아포칼립스" -> "폐허 속 생존 기술"; + case "중세 마법학원" -> "룬과 마력회로"; + case "바이오펑크" -> "세포 공학적 변이"; + default -> "특별한 콘셉트"; + }; + } + + private static String buildItemName(String concept, ItemType type, Grade g, Stat main) { + String gradePrefix = switch (g) { + case LEGENDARY -> "전설의 "; + case EPIC -> "에픽 "; + case RARE -> "희귀 "; + case UNCOMMON -> "고급 "; + case COMMON -> ""; + }; + + String noun = switch (type) { + case WEAPON -> pickOne(List.of("기어블레이드", "광자검", "혈문도", "룬스태프", "해적커틀러스", "강철장도", "마나활", "열압권총")); + case ARMOR -> pickOne(List.of("기계갑옷", "네온코트", "어둠의 흉갑", "비단갑", "우주복", "셰프앞치마", "해골흉갑", "겐지로브")); + case ARTIFACT -> pickOne(List.of("증기코어", "신경임플란트", "어비스 보주", "학원 배지", "항성 파편", "미각 토템")); + case POTION -> pickOne(List.of("촉매 엘릭서", "신경강화제", "밤피의 혈약", "기혈단", "중력완화제", "풍미증폭 소스")); + }; + + String conceptAdj = switch (concept) { + case "스팀펑크" -> pickOne(List.of("황동제", "증기식", "기어식")); + case "사이버네온" -> pickOne(List.of("네온-튜닝", "양자", "신경망")); + case "암흑 판타지" -> pickOne(List.of("그림자", "혈문", "망령")); + case "동양 무협" -> pickOne(List.of("벽력", "천검", "비연")); + case "우주 SF" -> pickOne(List.of("중성자", "쿼크", "항성")); + case "요리 배틀" -> pickOne(List.of("주방장", "풍미", "향신")); + case "해적 시대" -> pickOne(List.of("검은깃발", "해골", "산호")); + case "포스트 아포칼립스" -> pickOne(List.of("폐허산", "방사", "고철")); + case "중세 마법학원" -> pickOne(List.of("룬각", "비전", "원소")); + case "바이오펑크" -> pickOne(List.of("유전자", "세포", "점액질")); + default -> ""; + }; + + String mainHint = switch (main) { + case STRENGTH -> "괴력"; + case AGILITY -> "신속"; + case INTELLIGENCE -> "지성"; + case LUCK -> "포츈"; + }; + + return gradePrefix + conceptAdj + " " + noun + " • " + mainHint; + } + + private static String buildItemDescription(String concept, ItemType type, Grade g, Stat main) { + String line1 = "세계관: " + concept + " | 주 스탯: " + main; + String line2 = switch (type) { + case WEAPON -> "공격기반 무기. " + conceptTagline(concept); + case ARMOR -> "방어/생존 특화. " + conceptTagline(concept); + case ARTIFACT -> "특수 패시브/효과 중심. " + conceptTagline(concept); + case POTION -> "일시적 버프/회복. " + conceptTagline(concept); + }; + String line3 = switch (g) { + case LEGENDARY -> "희귀한 제작법이 전해진다."; + case EPIC -> "베테랑 장인들의 정수가 담겼다."; + case RARE -> "전투에서 검증된 성능."; + case UNCOMMON -> "균형 잡힌 성능."; + case COMMON -> "보급형 표준 모델."; + }; + return line1 + "\n" + line2 + "\n" + line3; + } + + private static int conceptBaseBonus(String concept, ItemType type, Stat main) { + int bias = 0; + if (concept.equals("스팀펑크") && type == ItemType.WEAPON) bias += 6; + if (concept.equals("사이버네온") && (main == Stat.AGILITY || main == Stat.INTELLIGENCE)) bias += 8; + if (concept.equals("암흑 판타지") && type == ItemType.ARMOR) bias += 5; + if (concept.equals("동양 무협") && (type == ItemType.WEAPON || main == Stat.STRENGTH)) bias += 7; + if (concept.equals("우주 SF") && type == ItemType.ARTIFACT) bias += 9; + if (concept.equals("요리 배틀") && type == ItemType.POTION) bias += 10; + if (concept.equals("해적 시대") && main == Stat.LUCK) bias += 6; + if (concept.equals("포스트 아포칼립스") && type == ItemType.ARMOR) bias += 4; + if (concept.equals("중세 마법학원") && main == Stat.INTELLIGENCE) bias += 8; + if (concept.equals("바이오펑크") && type == ItemType.ARTIFACT) bias += 6; + + bias += rand(-3, 3); + return bias; + } + + private static long conceptPricePremium(String concept, ItemType type, Grade g) { + int tier = switch (g) { + case LEGENDARY -> 5; case EPIC -> 4; case RARE -> 3; case UNCOMMON -> 2; case COMMON -> 1; + }; + int base = switch (type) { + case WEAPON -> 60; case ARMOR -> 45; case ARTIFACT -> 80; case POTION -> 25; + }; + int conceptFactor = switch (concept) { + case "우주 SF", "바이오펑크" -> 50; + case "사이버네온", "중세 마법학원" -> 40; + case "스팀펑크", "암흑 판타지" -> 35; + case "해적 시대" -> 30; + case "포스트 아포칼립스" -> 28; + case "동양 무협" -> 32; + case "요리 배틀" -> 20; + default -> 25; + }; + return (long) ((base + conceptFactor) * tier); + } + + private static String pickOne(List list) { return list.get(rand(0, list.size()-1)); } + private static String picsumWithSeed(int w, int h, String seed) { + return "https://picsum.photos/seed/" + seed.replaceAll("\\s+","_") + "/" + w + "/" + h; + } + + private static int rand(int min, int max) { + return ThreadLocalRandom.current().nextInt(min, max + 1); + } + private static long randL(int min, int max) { return rand(min, max); } + private static boolean randBool() { return ThreadLocalRandom.current().nextBoolean(); } + private static String token() { String a="ABCDEFGHJKLMNPQRSTUVWXYZ"; return ""+a.charAt(rand(0,a.length()-1))+rand(0,999); } + private static String picsum(int w, int h) { return "https://picsum.photos/" + w + "/" + h + "?random=" + UUID.randomUUID(); } + private static String lorem(int words) { + String base="An ancient relic hums with latent power as shadows gather over Scriptopia "; + String[] arr=base.split("\\s+"); StringBuilder sb=new StringBuilder(); + for(int i=0;i pool = new ArrayList<>(); + + // 컨셉 시그니처 + switch (concept) { + case "스팀펑크" -> pool.addAll(List.of("증기 분출", "기어 과부하", "압력 누적", "피스톤 충격")); + case "사이버네온" -> pool.addAll(List.of("신경 가속", "패킷 주입", "광자 잔상", "방화벽 관통")); + case "암흑 판타지" -> pool.addAll(List.of("그림자 맹세", "피의 저주", "망령 서약", "어비스의 응시")); + case "동양 무협" -> pool.addAll(List.of("검기 방출", "경공술", "내공 폭진", "기혈 순환")); + case "우주 SF" -> pool.addAll(List.of("중력 왜곡", "차원 흔들림", "항성열 방사", "양자 난류")); + case "요리 배틀" -> pool.addAll(List.of("풍미 증폭", "감칠맛 폭격", "식감 강화", "향신료 분사")); + case "해적 시대" -> pool.addAll(List.of("대포 사격", "돛바람 가속", "해안 급습", "검은 파도")); + case "포스트 아포칼립스" -> pool.addAll(List.of("방사 저항", "고철 방패", "연료 분사", "황폐의 굴레")); + case "중세 마법학원" -> pool.addAll(List.of("룬 각성", "비전 증폭", "소환 공명", "원소 가호")); + case "바이오펑크" -> pool.addAll(List.of("세포 재생", "신경 동조", "점액질 보호막", "유전자 각성")); + } + + // 타입 기반 추가 + switch (type) { + case WEAPON -> pool.addAll(List.of("치명타 확률", "관통", "연격", "출혈", "화염 부여")); + case ARMOR -> pool.addAll(List.of("피해 감소", "빙결 저항", "화염 저항", "보호막", "재생")); + case ARTIFACT -> pool.addAll(List.of("축복", "가속", "행운의 일격", "마력 증폭", "집중")); + case POTION -> pool.addAll(List.of("즉시 회복", "해제", "광폭화", "은신", "정화")); + } + + // 메인 스탯 보정 + switch (mainStat) { + case STRENGTH -> pool.addAll(List.of("분쇄", "분노", "괴력")); + case AGILITY -> pool.addAll(List.of("신속", "회피", "날렵함")); + case INTELLIGENCE -> pool.addAll(List.of("지성 증폭", "집중", "분석")); + case LUCK -> pool.addAll(List.of("포춘", "크리 운빨", "은총")); + } + + // 등급 접두사 + String prefix = switch (grade) { + case LEGENDARY -> "전설의 "; + case EPIC -> "에픽 "; + case RARE -> "희귀 "; + case UNCOMMON -> "고급 "; + case COMMON -> ""; + }; + + if (pool.isEmpty()) pool = List.of("특수 효과"); + return prefix + pool.get(rand(0, pool.size() - 1)); + } +} From 76f4cb5821e75c4b3ebc92b7863a888903b82bae Mon Sep 17 00:00:00 2001 From: juns0720 Date: Sat, 20 Sep 2025 21:52:59 +0900 Subject: [PATCH 18/64] feat: update yml --- src/main/resources/application.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7e14b2f..0840904 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -57,8 +57,6 @@ oauth: redirect-uri: ${NAVER_REDIRECT_URI} scope: name email profile_image - - auth: jwt: issuer: scriptopia From b8ba46028c76cfd289e9d2b4c7e47723042d2069 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Sat, 27 Sep 2025 18:21:47 +0900 Subject: [PATCH 19/64] refactor: improve game session service, choice result type, game balance util, and index.html UI\ --- .../demo/domain/ChoiceResultType.java | 8 +- .../demo/service/GameSessionService.java | 4 +- .../demo/utils/GameBalanceUtil.java | 13 +- src/main/resources/templates/index.html | 451 ++++++++++++++++-- 4 files changed, 415 insertions(+), 61 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java b/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java index 84e455e..18f0f46 100644 --- a/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java +++ b/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java @@ -7,10 +7,10 @@ @Getter public enum ChoiceResultType { - BATTLE(2), // 20, 40, 45, 5 - CHOICE(3), - DONE(5), - SHOP(90); + BATTLE(60), // 20, 40, 45, 5 + CHOICE(5), + DONE(30), + SHOP(5); private final int nextEventType; diff --git a/src/main/java/com/scriptopia/demo/service/GameSessionService.java b/src/main/java/com/scriptopia/demo/service/GameSessionService.java index 8c2db90..1558795 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -295,7 +295,7 @@ public Object getInGameDataDto(Long userId){ .stageSize(gameSessionMongo.getStage() != null ? gameSessionMongo.getStage().size() : 0) .playerInfo(inGameMapper.mapPlayer(gameSessionMongo.getPlayerInfo())) .npcInfo(inGameMapper.mapNpc(gameSessionMongo.getNpcInfo())) - .inventory(inGameMapper.mapInventory(gameSessionMongo.getInventory()) ) + .inventory(inGameMapper.mapInventory(gameSessionMongo.getInventory())) .choiceInfo(inGameMapper.mapChoice(gameSessionMongo.getChoiceInfo())) .build(); @@ -726,7 +726,7 @@ public GameSessionMongo gameToDone(Long userId) { .selectedChoice(gameSessionMongo.getPreChoice()) .resultContent(RewardType.getRewardSummary(gameSessionMongo.getRewardInfo())) .playerName(gameSessionMongo.getPlayerInfo().getName()) - .playerVictory( (gameSessionMongo.getSceneType() == SceneType.BATTLE)) + .playerVictory( gameSessionMongo.getRewardInfo().getRewardLife() >= 0 ) .build(); diff --git a/src/main/java/com/scriptopia/demo/utils/GameBalanceUtil.java b/src/main/java/com/scriptopia/demo/utils/GameBalanceUtil.java index 03a79c9..d2e6144 100644 --- a/src/main/java/com/scriptopia/demo/utils/GameBalanceUtil.java +++ b/src/main/java/com/scriptopia/demo/utils/GameBalanceUtil.java @@ -75,8 +75,6 @@ public class GameBalanceUtil { }}; - - /** * @param grade * @return (0: STR, 1: AGI, 2: INT, 3: LUCK) @@ -357,8 +355,8 @@ public static boolean isPass(int choiceProbability) { public static RewardInfoMongo getReward(RewardType rewardType, boolean isPass) { RewardInfoMongo.RewardInfoMongoBuilder builder = RewardInfoMongo.builder(); - // 기본값: 성공이면 생명 +1, 실패면 생명 -1 - builder.rewardLife(isPass ? 1 : -1); + // 기본값: 성공이면 생명 0, 실패면 생명 -1 + builder.rewardLife(isPass ? 0 : -1); switch (rewardType) { case GOLD: @@ -404,7 +402,12 @@ public static PlayerInfoMongo updateReward(PlayerInfoMongo playerInfo, RewardInf playerInfo.setLife(playerInfo.getLife() + rewardInfo.getRewardLife()); if (rewardInfo.getRewardGold() != null) - playerInfo.setGold(playerInfo.getGold() + rewardInfo.getRewardGold()); + if (playerInfo.getGold() + rewardInfo.getRewardGold() < 0){ + playerInfo.setGold(0L); + }else{ + playerInfo.setGold(playerInfo.getGold() + rewardInfo.getRewardGold()); + } + if (rewardInfo.getRewardTrait() != null) playerInfo.setTrait(rewardInfo.getRewardTrait()); diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index ef3026d..bc4bd1d 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -46,6 +46,10 @@

로그인

+ +
@@ -54,9 +58,14 @@

로그인

Player Info

HP: 100
MP: 50
Level: 1
-
-

Inventory

-
아이템 없음
+
+

장착 중 장비

+
없음
+
+ +
+

소유 아이템

+
없음
@@ -75,9 +84,23 @@

Inventory

From 88f06a7fec3f9a31337f841571a6172f5085b629 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Sat, 27 Sep 2025 19:55:02 +0900 Subject: [PATCH 20/64] wip: during working --- .../demo/domain/ChoiceResultType.java | 4 +-- .../demo/domain/mongo/RewardInfoMongo.java | 35 ++++++++++++++----- .../demo/service/GameSessionService.java | 13 +++++-- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java b/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java index 18f0f46..a65fefe 100644 --- a/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java +++ b/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java @@ -7,10 +7,10 @@ @Getter public enum ChoiceResultType { - BATTLE(60), // 20, 40, 45, 5 + BATTLE(30), // 20, 40, 45, 5 CHOICE(5), DONE(30), - SHOP(5); + SHOP(35); private final int nextEventType; diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/RewardInfoMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/RewardInfoMongo.java index 34de088..5d01b74 100644 --- a/src/main/java/com/scriptopia/demo/domain/mongo/RewardInfoMongo.java +++ b/src/main/java/com/scriptopia/demo/domain/mongo/RewardInfoMongo.java @@ -1,7 +1,7 @@ package com.scriptopia.demo.domain.mongo; import lombok.*; - +import java.util.ArrayList; import java.util.List; @Data @@ -9,13 +9,30 @@ @AllArgsConstructor @NoArgsConstructor public class RewardInfoMongo { - private List gainedItemDefId; - private List lostItemsDefId; - private Integer rewardStrength; - private Integer rewardAgility; - private Integer rewardIntelligence; - private Integer rewardLuck; - private Integer rewardLife; + + @Builder.Default + private List gainedItemDefId = new ArrayList<>(); + + @Builder.Default + private List lostItemsDefId = new ArrayList<>(); + + @Builder.Default + private Integer rewardStrength = 0; + + @Builder.Default + private Integer rewardAgility = 0; + + @Builder.Default + private Integer rewardIntelligence = 0; + + @Builder.Default + private Integer rewardLuck = 0; + + @Builder.Default + private Integer rewardLife = 0; + private String rewardTrait; - private Integer rewardGold; + + @Builder.Default + private Integer rewardGold = 0; } diff --git a/src/main/java/com/scriptopia/demo/service/GameSessionService.java b/src/main/java/com/scriptopia/demo/service/GameSessionService.java index 1558795..f781a61 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -345,6 +345,7 @@ public Object getInGameDataDto(Long userId){ .startedAt(gameSessionMongo.getStartedAt()) .updatedAt(LocalDateTime.now()) .background(gameSessionMongo.getBackground()) + .progress(gameSessionMongo.getProgress()) .location(gameSessionMongo.getLocation()) .stageSize(gameSessionMongo.getStage() != null ? gameSessionMongo.getStage().size() : 0) .playerInfo(inGameMapper.mapPlayer(gameSessionMongo.getPlayerInfo())) @@ -421,9 +422,13 @@ public GameSessionMongo gameProgress(Long userId) { gameToDone(userId); } case SceneType.DONE -> { + gameSessionMongo.setProgress(gameSessionMongo.getProgress() + 1); + gameSessionMongoRepository.save(gameSessionMongo); gameToChoice(userId); } case SceneType.SHOP -> { + gameSessionMongo.setProgress(gameSessionMongo.getProgress() + 1); + gameSessionMongoRepository.save(gameSessionMongo); gameToChoice(userId); } default -> throw new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND); @@ -718,6 +723,11 @@ public GameSessionMongo gameToDone(Long userId) { GameSessionMongo gameSessionMongo = gameSessionMongoRepository.findById(gameId) .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + SceneType preSceneType = gameSessionMongo.getSceneType(); + boolean isVictory = false; + if (preSceneType == SceneType.BATTLE) { + isVictory = gameSessionMongo.getBattleInfo().getPlayerWin(); + } CreateGameDoneRequest fastApiRequest = CreateGameDoneRequest.builder() .worldView(gameSessionMongo.getHistoryInfo().getWorldView()) @@ -726,7 +736,7 @@ public GameSessionMongo gameToDone(Long userId) { .selectedChoice(gameSessionMongo.getPreChoice()) .resultContent(RewardType.getRewardSummary(gameSessionMongo.getRewardInfo())) .playerName(gameSessionMongo.getPlayerInfo().getName()) - .playerVictory( gameSessionMongo.getRewardInfo().getRewardLife() >= 0 ) + .playerVictory( isVictory ) .build(); @@ -742,7 +752,6 @@ public GameSessionMongo gameToDone(Long userId) { gameSessionMongo.setUpdatedAt(LocalDateTime.now()); gameSessionMongo.setLocation(fastApiResponse.getDoneInfo().getNewLocation()); gameSessionMongo.setBackground(fastApiResponse.getDoneInfo().getReCap()); - gameSessionMongo.setProgress(gameSessionMongo.getProgress() + 1); int currentProgress = gameSessionMongo.getProgress(); From 900e7d410294e6931c8ba52d5ffa33c388383d32 Mon Sep 17 00:00:00 2001 From: KII1ua Date: Wed, 1 Oct 2025 17:10:06 +0900 Subject: [PATCH 21/64] refactor shared-gameservice sort --- gradlew | 0 .../com/scriptopia/demo/controller/SharedGameController.java | 2 +- .../java/com/scriptopia/demo/service/SharedGameService.java | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java index a79c0c8..a32ed28 100644 --- a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -48,7 +48,7 @@ public ResponseEntity> getPublicSharedGames @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) List tagIds, @RequestParam(required = false) String query, - @RequestParam(defaultValue = "LATEST")SharedGameSort sort) { + @RequestParam(defaultValue = "POPULAR")SharedGameSort sort) { Long viewerId = (authentication == null) ? null : Long.valueOf(authentication.getName()); return sharedGameService.getPublicSharedGames(viewerId, lastUUID, size, tagIds, query, sort); } diff --git a/src/main/java/com/scriptopia/demo/service/SharedGameService.java b/src/main/java/com/scriptopia/demo/service/SharedGameService.java index aa815d7..7f83d01 100644 --- a/src/main/java/com/scriptopia/demo/service/SharedGameService.java +++ b/src/main/java/com/scriptopia/demo/service/SharedGameService.java @@ -159,7 +159,7 @@ public ResponseEntity> getPublicSharedGames // 2) 태그/커서/정렬 전처리 boolean tagEmpty = (tagIds == null || tagIds.isEmpty()); - SharedGameSort effectiveSort = qBlank ? sort : SharedGameSort.LATEST; + SharedGameSort effectiveSort = qBlank ? sort : SharedGameSort.POPULAR; boolean useCursor = (lastUuid != null); Long lastId = null; From 30e8f18edb354cf4b0ec4c86b6beb1d55fa1d3a2 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Wed, 1 Oct 2025 22:01:45 +0900 Subject: [PATCH 22/64] feat: create fast api domain --- .../demo/dto/gamesession/GameEndRequest.java | 16 ++++++++++++++++ .../demo/dto/gamesession/GameEndResponse.java | 12 ++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/main/java/com/scriptopia/demo/dto/gamesession/GameEndRequest.java create mode 100644 src/main/java/com/scriptopia/demo/dto/gamesession/GameEndResponse.java diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/GameEndRequest.java b/src/main/java/com/scriptopia/demo/dto/gamesession/GameEndRequest.java new file mode 100644 index 0000000..50bbd4f --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/GameEndRequest.java @@ -0,0 +1,16 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GameEndRequest { + private String worldView; + private String location; + private String previousStory; + private String playerName; + private String gameEnd; +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/GameEndResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/GameEndResponse.java new file mode 100644 index 0000000..fda17c4 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/GameEndResponse.java @@ -0,0 +1,12 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GameEndResponse { + private String endStory; +} From 5717390602c1f54bab8712e5e17fc325ec7870c1 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Wed, 1 Oct 2025 22:02:43 +0900 Subject: [PATCH 23/64] feat: create FASTAPI endpoitn and service --- .../scriptopia/demo/config/fastapi/FastApiEndpoint.java | 3 ++- .../java/com/scriptopia/demo/service/FastApiService.java | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/scriptopia/demo/config/fastapi/FastApiEndpoint.java b/src/main/java/com/scriptopia/demo/config/fastapi/FastApiEndpoint.java index 415c05e..08547f8 100644 --- a/src/main/java/com/scriptopia/demo/config/fastapi/FastApiEndpoint.java +++ b/src/main/java/com/scriptopia/demo/config/fastapi/FastApiEndpoint.java @@ -5,7 +5,8 @@ public enum FastApiEndpoint { CHOICE("/games/choice"), BATTLE("/games/battle"), ITEM("/games/item"), - DONE("/games/done"); + DONE("/games/done"), + END("/games/end"); private final String path; diff --git a/src/main/java/com/scriptopia/demo/service/FastApiService.java b/src/main/java/com/scriptopia/demo/service/FastApiService.java index 53ce7b5..9f39a10 100644 --- a/src/main/java/com/scriptopia/demo/service/FastApiService.java +++ b/src/main/java/com/scriptopia/demo/service/FastApiService.java @@ -65,5 +65,14 @@ public ItemFastApiResponse item(ItemFastApiRequest request) { .block(); } + // 게임 종료 생성 (확장용) + public GameEndResponse end(GameEndRequest request) { + return fastApiWebClient.post() + .uri(FastApiEndpoint.END.getPath()) + .bodyValue(request) + .retrieve() + .bodyToMono(GameEndResponse.class) + .block(); + } } From cca4b1306d0861c050a9eea72f3db6e405c92392 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 2 Oct 2025 15:49:52 +0900 Subject: [PATCH 24/64] refactor: add GAMEOVER, GAMECLEAR --- src/main/java/com/scriptopia/demo/domain/SceneType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/scriptopia/demo/domain/SceneType.java b/src/main/java/com/scriptopia/demo/domain/SceneType.java index b001a92..a56194b 100644 --- a/src/main/java/com/scriptopia/demo/domain/SceneType.java +++ b/src/main/java/com/scriptopia/demo/domain/SceneType.java @@ -1,5 +1,5 @@ package com.scriptopia.demo.domain; public enum SceneType { - BATTLE, CHOICE, SHOP, DONE + BATTLE, CHOICE, SHOP, DONE, GAMEOVER, GAMECLEAR } From 4e9e74c45adb22e5cb7d04f993a12126473e020f Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 2 Oct 2025 18:31:52 +0900 Subject: [PATCH 25/64] feat: add InGameClearResponse and InGameOverResponse DTOs --- .../ingame/InGameClearResponse.java | 27 ++++++++++++++++++ .../ingame/InGameOverResponse.java | 28 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameClearResponse.java create mode 100644 src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameOverResponse.java diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameClearResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameClearResponse.java new file mode 100644 index 0000000..2666aeb --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameClearResponse.java @@ -0,0 +1,27 @@ +package com.scriptopia.demo.dto.gamesession.ingame; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InGameClearResponse { + private String sceneType; + private LocalDateTime startedAt; + private LocalDateTime updatedAt; + private String background; + private String location; + private int progress; + private int stageSize; + + private InGamePlayerResponse playerInfo; + private InGameNpcResponse npcInfo; + private List inventory; +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameOverResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameOverResponse.java new file mode 100644 index 0000000..21f06f9 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameOverResponse.java @@ -0,0 +1,28 @@ +package com.scriptopia.demo.dto.gamesession.ingame; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InGameOverResponse { + private String sceneType; + private LocalDateTime startedAt; + private LocalDateTime updatedAt; + private String background; + private String location; + private int progress; + private int stageSize; + + private InGamePlayerResponse playerInfo; + private InGameNpcResponse npcInfo; + private List inventory; + +} From eef8025aa3f26e85a652c5afde265b789a4ed36a Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 2 Oct 2025 18:31:53 +0900 Subject: [PATCH 26/64] refactor: update game end logic and template to support game over/clear flow --- .../demo/domain/ChoiceResultType.java | 4 +- .../demo/dto/gamesession/GameEndRequest.java | 4 +- .../demo/service/GameSessionService.java | 73 ++++++++++++++++++- src/main/resources/templates/index.html | 43 +++++++++++ 4 files changed, 117 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java b/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java index a65fefe..85dcc74 100644 --- a/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java +++ b/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java @@ -9,8 +9,8 @@ public enum ChoiceResultType { BATTLE(30), // 20, 40, 45, 5 CHOICE(5), - DONE(30), - SHOP(35); + DONE(60), + SHOP(5); private final int nextEventType; diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/GameEndRequest.java b/src/main/java/com/scriptopia/demo/dto/gamesession/GameEndRequest.java index 50bbd4f..ef44b40 100644 --- a/src/main/java/com/scriptopia/demo/dto/gamesession/GameEndRequest.java +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/GameEndRequest.java @@ -1,9 +1,11 @@ package com.scriptopia.demo.dto.gamesession; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +@Builder @Data @NoArgsConstructor @AllArgsConstructor @@ -12,5 +14,5 @@ public class GameEndRequest { private String location; private String previousStory; private String playerName; - private String gameEnd; + private int gameEnd; } diff --git a/src/main/java/com/scriptopia/demo/service/GameSessionService.java b/src/main/java/com/scriptopia/demo/service/GameSessionService.java index f781a61..eb65054 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -1,9 +1,6 @@ package com.scriptopia.demo.service; -import com.scriptopia.demo.dto.gamesession.ingame.InGameBattleResponse; -import com.scriptopia.demo.dto.gamesession.ingame.InGameChoiceResponse; -import com.scriptopia.demo.dto.gamesession.ingame.InGameDoneResponse; -import com.scriptopia.demo.dto.gamesession.ingame.InGameShopResponse; +import com.scriptopia.demo.dto.gamesession.ingame.*; import com.scriptopia.demo.dto.items.ItemDefRequest; import com.scriptopia.demo.dto.items.ItemFastApiResponse; import com.scriptopia.demo.mapper.InGameMapper; @@ -386,6 +383,34 @@ public Object getInGameDataDto(Long userId){ .curTurnId(battleInfo != null ? battleInfo.getCurTurnId() : null) .build(); + } else if (currentSceneType == SceneType.GAMEOVER) { + + return InGameOverResponse.builder() + .sceneType("GAMEOVER") + .startedAt(gameSessionMongo.getStartedAt()) + .updatedAt(gameSessionMongo.getUpdatedAt()) + .background(gameSessionMongo.getBackground()) + .location(gameSessionMongo.getLocation()) + .progress(gameSessionMongo.getProgress()) + .stageSize(gameSessionMongo.getStage() != null ? gameSessionMongo.getStage().size() : 0) + .playerInfo(inGameMapper.mapPlayer(gameSessionMongo.getPlayerInfo())) + .npcInfo(inGameMapper.mapNpc(gameSessionMongo.getNpcInfo())) + .inventory(inGameMapper.mapInventory(gameSessionMongo.getInventory())) + .build(); + } else if (currentSceneType == SceneType.GAMECLEAR) { + + return InGameClearResponse.builder() + .sceneType("GAMECLEAR") + .startedAt(gameSessionMongo.getStartedAt()) + .updatedAt(gameSessionMongo.getUpdatedAt()) + .background(gameSessionMongo.getBackground()) + .location(gameSessionMongo.getLocation()) + .progress(gameSessionMongo.getProgress()) + .stageSize(gameSessionMongo.getStage() != null ? gameSessionMongo.getStage().size() : 0) + .playerInfo(inGameMapper.mapPlayer(gameSessionMongo.getPlayerInfo())) + .npcInfo(inGameMapper.mapNpc(gameSessionMongo.getNpcInfo())) + .inventory(inGameMapper.mapInventory(gameSessionMongo.getInventory())) + .build(); } return null; @@ -413,6 +438,19 @@ public GameSessionMongo gameProgress(Long userId) { .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + if( gameSessionMongo.getPlayerInfo().getLife() <= 0 ){ + // gameOver 메소드 구현 필요 + return gameToEnd(gameSessionMongo, 0); + } + + if ( gameSessionMongo.getProgress() > gameSessionMongo.getStage().size()){ + // gmaeClear 즉 + return gameToEnd(gameSessionMongo, 1); + + } + + + SceneType currentSceneType = gameSessionMongo.getSceneType(); switch (currentSceneType) { case SceneType.CHOICE -> { @@ -1193,4 +1231,31 @@ public void usePotion(Long userId, String ItemId) { throw new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND); } } + + + /** + * 게임 종료 처리 (0 이면 gameover 1이면 gameclear + */ + private GameSessionMongo gameToEnd(GameSessionMongo gameSessionMongo, int gameOver) { + GameEndRequest fastApiRequest = GameEndRequest.builder() + .worldView(gameSessionMongo.getHistoryInfo().getWorldView()) + .location(gameSessionMongo.getLocation()) + .previousStory(gameSessionMongo.getBackground()) + .playerName(gameSessionMongo.getPlayerInfo().getName()) + .gameEnd(gameOver) + .build(); + + SceneType isGameClear = SceneType.GAMEOVER; + if ( gameOver == 1){ + isGameClear = SceneType.GAMECLEAR; + } + GameEndResponse fastApiResponse = fastApiService.end(fastApiRequest); + + gameSessionMongo.setBackground(fastApiResponse.getEndStory()); + gameSessionMongo.setSceneType(isGameClear); + gameSessionMongoRepository.save(gameSessionMongo); + + return gameSessionMongo; + } + } diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index bc4bd1d..9b0b4ab 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -554,6 +554,49 @@

소유 아이템

} break; + case 'GAMEOVER': + choiceContainer.innerHTML = ''; + const gameOverBtn = document.createElement('button'); + gameOverBtn.className = 'btn'; + gameOverBtn.textContent = '게임 패배 - 결과 보기'; + gameOverBtn.addEventListener('click', () => { + if (data.rewardInfo) { + const r = data.rewardInfo; + choiceContainer.innerHTML = ` +
게임 패배 결과
+
획득 아이템: ${r.gainedItemNames?.join(', ') || '없음'}
+
보상: STR ${r.rewardStrength}, AGI ${r.rewardAgility}, + INT ${r.rewardIntelligence}, LUCK ${r.rewardLuck}, + Life ${r.rewardLife}, Gold ${r.rewardGold}
`; + } else { + choiceContainer.innerHTML = '
결과 없음
'; + } + }); + choiceContainer.appendChild(gameOverBtn); + break; + + + case 'GAMECLEAR': + choiceContainer.innerHTML = ''; + const gameClearBtn = document.createElement('button'); + gameClearBtn.className = 'btn'; + gameClearBtn.textContent = '게임 승리 - 결과 보기'; + gameClearBtn.addEventListener('click', () => { + if (data.rewardInfo) { + const r = data.rewardInfo; + choiceContainer.innerHTML = ` +
게임 승리 결과
+
획득 아이템: ${r.gainedItemNames?.join(', ') || '없음'}
+
보상: STR ${r.rewardStrength}, AGI ${r.rewardAgility}, + INT ${r.rewardIntelligence}, LUCK ${r.rewardLuck}, + Life ${r.rewardLife}, Gold ${r.rewardGold}
`; + } else { + choiceContainer.innerHTML = '
결과 없음
'; + } + }); + choiceContainer.appendChild(gameClearBtn); + break; + default: choiceContainer.innerHTML = '
알 수 없는 장면
'; } From c16d7f8784cc433defafaf669347f8c37f4b7133 Mon Sep 17 00:00:00 2001 From: juns0720 Date: Thu, 2 Oct 2025 18:33:27 +0900 Subject: [PATCH 27/64] feat: add dependence org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0 --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 36d6a55..36a2a73 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,9 @@ dependencies { // 소셜 로그인 oauth2 implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' } tasks.named('test') { From b3183255f8d0025631563ce48c4b8e5f3be7e519 Mon Sep 17 00:00:00 2001 From: juns0720 Date: Thu, 2 Oct 2025 20:31:16 +0900 Subject: [PATCH 28/64] feat: implement swagger --- build.gradle | 3 +- .../scriptopia/demo/config/JwtAuthFilter.java | 8 +-- .../demo/config/LocalDataSeeder.java | 2 - .../demo/config/SecurityWhitelist.java | 5 ++ .../scriptopia/demo/config/SwaggerConfig.java | 37 ++++++++++++ .../demo/config/SwaggerExampleConfig.java | 44 ++++++++++++++ .../demo/controller/AuctionController.java | 10 ++++ .../demo/controller/AuthController.java | 20 ++++--- .../controller/GameSessionController.java | 59 +++++++++++-------- .../demo/controller/ItemController.java | 5 +- .../demo/controller/OAuthController.java | 7 ++- .../demo/controller/PiaShopController.java | 9 ++- .../demo/controller/SharedGameController.java | 13 ++++ .../demo/controller/TestEnvController.java | 2 + .../demo/controller/UserController.java | 12 ++++ .../demo/controller/refreshController.java | 4 ++ .../demo/dto/auth/LoginRequest.java | 4 ++ src/main/resources/application.yml | 11 ++++ 18 files changed, 210 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/scriptopia/demo/config/SwaggerConfig.java create mode 100644 src/main/java/com/scriptopia/demo/config/SwaggerExampleConfig.java diff --git a/build.gradle b/build.gradle index 36a2a73..7ed6c56 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.4' + id 'org.springframework.boot' version '3.3.5' id 'io.spring.dependency-management' version '1.1.7' } @@ -63,6 +63,7 @@ dependencies { // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + implementation 'org.apache.commons:commons-lang3:3.18.0' } tasks.named('test') { diff --git a/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java b/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java index 5072b6d..6a09ac6 100644 --- a/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java +++ b/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java @@ -49,13 +49,7 @@ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletExce Arrays.stream(SecurityWhitelist.PUBLIC_GETS) .anyMatch(pattern -> pathMatcher.match(pattern, path)); - boolean skip = authMatch || publicGetMatch; - - if (skip) { - log.debug("➡️ Skipping JwtAuthFilter for whitelisted request: {} {}", method, path); - } - - return skip; + return authMatch || publicGetMatch; } diff --git a/src/main/java/com/scriptopia/demo/config/LocalDataSeeder.java b/src/main/java/com/scriptopia/demo/config/LocalDataSeeder.java index ed2075b..6e8e672 100644 --- a/src/main/java/com/scriptopia/demo/config/LocalDataSeeder.java +++ b/src/main/java/com/scriptopia/demo/config/LocalDataSeeder.java @@ -57,11 +57,9 @@ public class LocalDataSeeder implements ApplicationRunner { @Override @Transactional public void run(ApplicationArguments args) { - log.info("=== LocalDataSeeder: start ==="); // 0) 이미 데이터가 있으면 중복 시드 방지 if (userRepository.count() > 1) { - log.info("Users already exist. Skip seeding."); return; } diff --git a/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java b/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java index d3f7811..a1f0140 100644 --- a/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java +++ b/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java @@ -15,9 +15,14 @@ public class SecurityWhitelist { "/oauth/**", + "/v3/api-docs/**", + "/swagger-ui/**", + "/shops/pia/items" + + }; public static final String[] PUBLIC_GETS = { diff --git a/src/main/java/com/scriptopia/demo/config/SwaggerConfig.java b/src/main/java/com/scriptopia/demo/config/SwaggerConfig.java new file mode 100644 index 0000000..767d7f1 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/SwaggerConfig.java @@ -0,0 +1,37 @@ +package com.scriptopia.demo.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI OpenAPI() { + return new OpenAPI() + .info(new Info().title("Scriptopia API").version("1.0")) + .addSecurityItem(new SecurityRequirement().addList("bearerAuth")) + .components(new io.swagger.v3.oas.models.Components() + .addSecuritySchemes("bearerAuth", + new SecurityScheme() + .name("bearerAuth") + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + ) + ); + } + + private Info ApiInfo() { + return new Info() + .title("Scriptopia Swagger") + .description("Scriptopia 공식 API 문서입니다.") + .version("1.0.0"); + + } +} diff --git a/src/main/java/com/scriptopia/demo/config/SwaggerExampleConfig.java b/src/main/java/com/scriptopia/demo/config/SwaggerExampleConfig.java new file mode 100644 index 0000000..5261a22 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/SwaggerExampleConfig.java @@ -0,0 +1,44 @@ +package com.scriptopia.demo.config; + +import com.scriptopia.demo.dto.auth.LoginRequest; +import io.swagger.v3.oas.models.examples.Example; +import org.springdoc.core.customizers.OpenApiCustomizer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerExampleConfig { + + + @Value("${app.admin.username}") + private String adminUsername; + + @Value("${app.admin.password}") + private String adminPassword; + + + @Bean + public OpenApiCustomizer customiseExamples() { + return openApi -> { + if (openApi.getPaths() == null) return; + + openApi.getPaths().forEach((path, item) -> { + if (path.endsWith("/auth/login") && item.getPost() != null) { + var reqBody = item.getPost().getRequestBody(); + if (reqBody == null) return; + + var content = reqBody.getContent().get("application/json"); + if (content == null) return; + + // DTO 객체 그대로 넣기 + content.addExamples("어드민 계정", + new Example().value(new LoginRequest(adminUsername, adminPassword, "1234"))); + + content.addExamples("일반 유저 계정", + new Example().value(new LoginRequest("userA@example.com", "userA!234", "1234"))); + } + }); + }; + } +} diff --git a/src/main/java/com/scriptopia/demo/controller/AuctionController.java b/src/main/java/com/scriptopia/demo/controller/AuctionController.java index 0e5501f..6e25b7b 100644 --- a/src/main/java/com/scriptopia/demo/controller/AuctionController.java +++ b/src/main/java/com/scriptopia/demo/controller/AuctionController.java @@ -3,6 +3,8 @@ import com.scriptopia.demo.dto.auction.*; import com.scriptopia.demo.service.AuctionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -11,11 +13,13 @@ @RestController @RequiredArgsConstructor +@Tag(name = "거래 API", description = "경매장 관련 거래 API 입니다.") @RequestMapping("/trades") public class AuctionController { private final AuctionService auctionService; + @Operation(summary = "보유 장비 아이템 조회") @GetMapping public ResponseEntity getTrades( @RequestBody TradeFilterRequest requestDto) { @@ -25,6 +29,7 @@ public ResponseEntity getTrades( } + @Operation(summary = "경매장 아이템 구매") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PostMapping("/{auctionId}/purchase") public ResponseEntity purchaseItem( @@ -37,6 +42,7 @@ public ResponseEntity purchaseItem( return ResponseEntity.ok(result); } + @Operation(summary = "내가 등록한 판매 아이템 조회") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @GetMapping("/me") public ResponseEntity mySaleItems( @@ -49,6 +55,7 @@ public ResponseEntity mySaleItems( return ResponseEntity.ok(result); } + @Operation(summary = "경매장 아이템 판매 등록") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PostMapping public ResponseEntity createAuction(@RequestBody AuctionRequest dto, @@ -58,6 +65,7 @@ public ResponseEntity createAuction(@RequestBody AuctionRequest dto, return ResponseEntity.ok(auctionService.createAuction(dto, userId)); } + @Operation(summary = "판매 중인 아이템 등록 취소") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @DeleteMapping("/{auctionId}") public ResponseEntity cancelMySaleItem( @@ -69,6 +77,7 @@ public ResponseEntity cancelMySaleItem( return ResponseEntity.ok(result); } + @Operation(summary = "내 거래 기록 조회(정산 테이블 조회)") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @GetMapping("/me/history") public ResponseEntity settlementHistory( @@ -81,6 +90,7 @@ public ResponseEntity settlementHistory( return ResponseEntity.ok(result); } + @Operation(summary = "구매 아이템/판매 대금 수령") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PostMapping("/{settlementId}/confirm") public ResponseEntity confirmItem( diff --git a/src/main/java/com/scriptopia/demo/controller/AuthController.java b/src/main/java/com/scriptopia/demo/controller/AuthController.java index 9400b49..8aaf83e 100644 --- a/src/main/java/com/scriptopia/demo/controller/AuthController.java +++ b/src/main/java/com/scriptopia/demo/controller/AuthController.java @@ -3,6 +3,8 @@ import com.scriptopia.demo.dto.auth.*; import com.scriptopia.demo.service.LocalAccountService; import com.scriptopia.demo.service.RefreshTokenService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; @@ -15,17 +17,17 @@ @RestController @RequestMapping("/auth") +@Tag(name = "로컬 인증 API", description = "로컬 인증 관련 API 입니다.") @RequiredArgsConstructor public class AuthController { private final LocalAccountService localAccountService; private final RefreshTokenService refreshTokenService; - private static final String RT_COOKIE = "RT"; private static final boolean COOKIE_SECURE = true; private static final String COOKIE_SAMESITE = "None"; - + @Operation(summary = "로그아웃") @PostMapping("/logout") public ResponseEntity logout( @CookieValue(name = RT_COOKIE, required = false) String refreshToken, @@ -38,7 +40,7 @@ public ResponseEntity logout( return ResponseEntity.ok("로그아웃 되었습니다."); } - + @Operation(summary = "로컬 로그인") @PostMapping("/login") public ResponseEntity login( @RequestBody @Valid LoginRequest req, @@ -49,6 +51,7 @@ public ResponseEntity login( return ResponseEntity.ok(localAccountService.login(req, request, response)); } + @Operation(summary = "로컬 계정 회원가입") @PostMapping("/register") public ResponseEntity register( @RequestBody @Valid RegisterRequest request @@ -57,6 +60,7 @@ public ResponseEntity register( return ResponseEntity.ok("회원가입에 성공했습니다."); } + @Operation(summary = "이메일 중복 검증") @PostMapping("/email/verify") public ResponseEntity verifyEmail(@Valid @RequestBody VerifyEmailRequest request) { @@ -65,21 +69,21 @@ public ResponseEntity verifyEmail(@Valid @RequestBody VerifyEmailRequest requ return ResponseEntity.ok("사용 가능한 이메일입니다."); } - + @Operation(summary = "이메일 인증 코드 전송") @PostMapping("/email/code/send") public ResponseEntity sendCode(@RequestBody @Valid SendCodeRequest request) { localAccountService.sendVerificationCode(request.getEmail()); return ResponseEntity.ok("인증 코드가 이메일로 발송되었습니다."); } + @Operation(summary = "이메일 인증 코드 확인") @PostMapping("/email/code/verify") public ResponseEntity verifyCode(@RequestBody @Valid VerifyCodeRequest request) { localAccountService.verifyCode(request.getEmail(), request.getCode()); return ResponseEntity.ok("이메일 인증이 완료되었습니다."); - } - + @Operation(summary = "비밀번호 초기화 링크 발송") @PostMapping("/password/reset/send") public ResponseEntity sendResetMail(@Valid @RequestBody SendCodeRequest request){ @@ -88,7 +92,7 @@ public ResponseEntity sendResetMail(@Valid @RequestBody SendCodeRequest reque return ResponseEntity.ok("비밀번호 초기화 링크를 전송했습니다."); } - + @Operation(summary = "비밀번호 초기화") @PatchMapping("/password/reset") public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { localAccountService.resetPassword(request.getToken(), request.getNewPassword()); @@ -96,7 +100,7 @@ public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest return ResponseEntity.ok("비밀번호가 성공적으로 변경되었습니다."); } - + @Operation(summary = "비밀번호 재설정") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PatchMapping("/password/change") public ResponseEntity changePassword(@RequestBody @Valid ChangePasswordRequest request, diff --git a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java index 482c045..ff3670f 100644 --- a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java +++ b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java @@ -6,6 +6,8 @@ import com.scriptopia.demo.dto.gamesession.*; import com.scriptopia.demo.service.GameSessionService; import com.scriptopia.demo.service.HistoryService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -14,6 +16,7 @@ @RestController @RequestMapping("/games") +@Tag(name = "게임 세션 API", description = "게임 세션 관련 API 입니다.") @RequiredArgsConstructor public class GameSessionController { @@ -23,6 +26,7 @@ public class GameSessionController { /* * 게임 -> 게임 도중 종료 */ + @Operation(summary = "저장 후 게임 종료") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") @PostMapping("/exit") public ResponseEntity createGameSession(Authentication authentication, @RequestBody GameSessionRequest request) { @@ -33,15 +37,26 @@ public ResponseEntity createGameSession(Authentication authentication, @Reque /* * 게임 -> 기존 게임 삭제 */ + @Operation(summary = "기존 게임 삭제") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") @DeleteMapping() public ResponseEntity deleteGameSession(Authentication authentication, @RequestBody GameSessionRequest request) { Long userId = Long.valueOf(authentication.getName()); return gameSessionService.deleteGameSession(userId, request.getGameId()); } - - + + /* + * 게임 -> 기존 게임 조회 + */ + @Operation(summary = "기존 게임 조회") + @GetMapping("/me") + public ResponseEntity loadGameSession(Authentication authentication) { + Long userId = Long.valueOf(authentication.getName()); + return gameSessionService.getGameSession(userId); + } + // 게임 시작 + @Operation(summary = "새 게임 생성") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PostMapping public ResponseEntity startNewGame( @@ -54,7 +69,7 @@ public ResponseEntity startNewGame( return ResponseEntity.ok(response); } - + @Operation(summary = "게임 진입") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @GetMapping("/{gameId}") public ResponseEntity getInGameData( @@ -69,34 +84,36 @@ public ResponseEntity getInGameData( return ResponseEntity.ok(response); } - + @Operation(summary = "선택지 선택") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") - @PostMapping("/{gameId}/progress") - public ResponseEntity keepGame( + @PostMapping("/{gameId}/select") + public ResponseEntity selectChoice( @PathVariable("gameId") String gameId, + @RequestBody GameChoiceRequest request, Authentication authentication) throws JsonProcessingException { Long userId = Long.valueOf(authentication.getName()); - GameSessionMongo response = gameSessionService.gameProgress(userId); + GameSessionMongo response = gameSessionService.gameChoiceSelect(userId, request); + return ResponseEntity.ok(response); } - + @Operation(summary = "게임 진행") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") - @PostMapping("/{gameId}/select") - public ResponseEntity selectChoice( + @PostMapping("/{gameId}/progress") + public ResponseEntity keepGame( @PathVariable("gameId") String gameId, - @RequestBody GameChoiceRequest request, Authentication authentication) throws JsonProcessingException { Long userId = Long.valueOf(authentication.getName()); - GameSessionMongo response = gameSessionService.gameChoiceSelect(userId, request); - + GameSessionMongo response = gameSessionService.gameProgress(userId); return ResponseEntity.ok(response); } + + @Operation(summary = "아이템 장착/해제") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PostMapping("/equipItem/{gameId}/{itemId}") public ResponseEntity equipItem( @@ -111,6 +128,7 @@ public ResponseEntity equipItem( return ResponseEntity.ok(response); } + @Operation(summary = "아이템 버리기") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @DeleteMapping("/dropItem/{gameId}/{itemId}") public ResponseEntity dropItem( @@ -125,6 +143,7 @@ public ResponseEntity dropItem( return ResponseEntity.ok(response); } + @Operation(summary = "아이템 구매") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PostMapping("/{gameId}/items/purchase/{itemId}") public ResponseEntity buyItem( @@ -139,6 +158,7 @@ public ResponseEntity buyItem( return ResponseEntity.ok(response); } + @Operation(summary = "아이템 판매") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PostMapping("/{gameId}/items/sell/{itemId}") public ResponseEntity sellItem( @@ -153,17 +173,6 @@ public ResponseEntity sellItem( return ResponseEntity.ok(response); } - /* - * 게임 -> 기존 게임 조회 - */ - @GetMapping("/me") - public ResponseEntity loadGameSession(Authentication authentication) { - Long userId = Long.valueOf(authentication.getName()); - return gameSessionService.getGameSession(userId); - } - - - /** * 현재는 userId, sessionId를 통해 저장하는데 * 인증 관리 부분 끝나면 header에 token 꺼내오고 requestparameter session_id로 저장하게 수정 @@ -171,6 +180,7 @@ public ResponseEntity loadGameSession(Authentication authentication) { /* * 게임 -> 히스토리 생성 */ + @Operation(summary = "") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") @PostMapping("/history") public ResponseEntity addHistory(Authentication authentication, @RequestBody GameSessionRequest request) { @@ -179,6 +189,7 @@ public ResponseEntity addHistory(Authentication authentication, @RequestBody } /** 개발용: 로컬 MongoDB에 더미 세션 한 건 심어서 테스트용 ObjectId 반환 */ + @Operation(summary = "") @PostMapping("/history/seed") public ResponseEntity seed(Authentication authentication) { Long userId = Long.valueOf(authentication.getName()); diff --git a/src/main/java/com/scriptopia/demo/controller/ItemController.java b/src/main/java/com/scriptopia/demo/controller/ItemController.java index cecdbb2..b872fc3 100644 --- a/src/main/java/com/scriptopia/demo/controller/ItemController.java +++ b/src/main/java/com/scriptopia/demo/controller/ItemController.java @@ -3,6 +3,8 @@ import com.scriptopia.demo.dto.items.ItemDTO; import com.scriptopia.demo.dto.items.ItemDefRequest; import com.scriptopia.demo.service.ItemService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -11,12 +13,13 @@ @RestController @RequestMapping("/items") +@Tag(name = "아이템 관련 API", description = "아이템 관련 API 입니다.") @RequiredArgsConstructor public class ItemController { private final ItemService itemService; - + @Operation(summary = "어드민 테스트용 아이템 생성") @PreAuthorize("hasAnyAuthority('ADMIN')") @PostMapping public ResponseEntity createItem( diff --git a/src/main/java/com/scriptopia/demo/controller/OAuthController.java b/src/main/java/com/scriptopia/demo/controller/OAuthController.java index f065dbc..dcb7365 100644 --- a/src/main/java/com/scriptopia/demo/controller/OAuthController.java +++ b/src/main/java/com/scriptopia/demo/controller/OAuthController.java @@ -4,6 +4,8 @@ import com.scriptopia.demo.dto.oauth.OAuthLoginResponse; import com.scriptopia.demo.dto.oauth.SocialSignupRequest; import com.scriptopia.demo.service.OAuthService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -12,16 +14,19 @@ @RestController @RequestMapping("/oauth") +@Tag(name = "소셜 인증 관련 API", description = "소셜 인증 관련 API 입니다.") @RequiredArgsConstructor public class OAuthController { private final OAuthService oAuthService; + @Operation(summary = "Oauth 로그인 url 발급") @GetMapping("/authorize") public ResponseEntity getAuthorizationUrl(@RequestParam("provider") String provider) { return ResponseEntity.ok(oAuthService.buildAuthorizationUrl(provider)); } + @Operation(summary = "소셜 로그인") @GetMapping("/{provider}") public ResponseEntity login( @PathVariable("provider") String provider, @@ -32,7 +37,7 @@ public ResponseEntity login( OAuthLoginResponse result = oAuthService.login(provider, code, request, response); return ResponseEntity.ok(result); } - + @Operation(summary = "소셜 회원가입") @PostMapping("/register") public ResponseEntity signup( @RequestBody SocialSignupRequest req, diff --git a/src/main/java/com/scriptopia/demo/controller/PiaShopController.java b/src/main/java/com/scriptopia/demo/controller/PiaShopController.java index 8411b9f..6690cfa 100644 --- a/src/main/java/com/scriptopia/demo/controller/PiaShopController.java +++ b/src/main/java/com/scriptopia/demo/controller/PiaShopController.java @@ -8,6 +8,8 @@ import com.scriptopia.demo.dto.piashop.PurchasePiaItemRequest; import com.scriptopia.demo.service.ItemService; import com.scriptopia.demo.service.PiaShopService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -18,11 +20,13 @@ @RestController @RequiredArgsConstructor +@Tag(name = "피아 상점 관련 API", description = "피아 상점 관련 API 입니다.") @RequestMapping("/shops") public class PiaShopController { private final PiaShopService piaShopService; private final ItemService itemService; + @Operation(summary = "PIA 상품 등록") @PreAuthorize("hasAnyAuthority('ADMIN')") @PostMapping("/items/pia") public ResponseEntity createPiaItem(@RequestBody PiaItemRequest request) { @@ -30,7 +34,7 @@ public ResponseEntity createPiaItem(@RequestBody PiaItemRequest request) return ResponseEntity.ok("PIA 아이템이 등록되었습니다."); } - + @Operation(summary = "PIA 상품 수정") @PreAuthorize("hasAnyAuthority('ADMIN')") @PutMapping("/items/pia/{itemId}") public ResponseEntity updatePiaItem( @@ -42,11 +46,13 @@ public ResponseEntity updatePiaItem( return ResponseEntity.ok(result); } + @Operation(summary = "PIA 판매 상품 목록 조회") @GetMapping("/pia/items") public ResponseEntity> getPiaItems() { return ResponseEntity.ok(piaShopService.getPiaItems()); } + @Operation(summary = "PIA 상품 구매") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PostMapping("/pia/purchase") public ResponseEntity purchasePiaItem( @@ -58,6 +64,7 @@ public ResponseEntity purchasePiaItem( return ResponseEntity.ok("PIA 아이템을 구매했습니다."); } + @Operation(summary = "아이템 모루 사용") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PostMapping("/pia/items/anvil") public ResponseEntity useItemAnvil( diff --git a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java index a32ed28..34ffebb 100644 --- a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -11,6 +11,8 @@ import com.scriptopia.demo.service.SharedGameFavoriteService; import com.scriptopia.demo.service.SharedGameService; import com.scriptopia.demo.service.TagDefService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -22,6 +24,7 @@ @RestController @RequestMapping("/shared-games") +@Tag(name = "게임 공유 관련 API", description = "게임 공유 관련 API 입니다.") @RequiredArgsConstructor public class SharedGameController { private final SharedGameService sharedGameService; @@ -31,6 +34,8 @@ public class SharedGameController { /* 게임 공유 -> 게임 공유하기 */ + + @Operation(summary = "게임 공유하기") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") @PostMapping public ResponseEntity share(Authentication authentication, @RequestBody SharedGameRequest req) { @@ -42,6 +47,7 @@ public ResponseEntity share(Authentication authentication, @RequestBody Share /* 게임 공유 -> 공유 게임 목록 조회 */ + @Operation(summary = "공유 게임 목록 조회") @GetMapping public ResponseEntity> getPublicSharedGames(Authentication authentication, @RequestParam(required = false) UUID lastUUID, @@ -56,6 +62,7 @@ public ResponseEntity> getPublicSharedGames /* 게임공유 : 공유된 게임 상세 조회 */ + @Operation(summary = "공유 게임 상세 조회") @GetMapping("/{sharedGameId}") public ResponseEntity getSharedGameDetail(@PathVariable("sharedGameId") UUID sharedGameId) { return sharedGameService.getDetailedSharedGame(sharedGameId); @@ -64,6 +71,7 @@ public ResponseEntity getSharedGameDetail(@PathVariable("sharedGameId") UUID /* 게임공유 : 공유 게임 Like 요청 */ + @Operation(summary = "공유 게임 Like 요청") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") @PostMapping("{sharedGameId}/like") public ResponseEntity likeSharedGame(@PathVariable("sharedGameId") UUID sharedGameId, Authentication authentication) { @@ -75,6 +83,7 @@ public ResponseEntity likeSharedGame(@PathVariable("sharedGameId") UUID share /* 게임공유 : 공유된 게임 태그 조회 */ + @Operation(summary = "게임 태그 조회") @GetMapping("/tags") public ResponseEntity getSharedGameTags() { return sharedGameService.getTag(); @@ -83,6 +92,7 @@ public ResponseEntity getSharedGameTags() { /* 게임 공유 -> 공유한 게임 삭제 */ + @Operation(summary = "공유한 게임 삭제") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") @DeleteMapping("/shared-games") public ResponseEntity delete(Authentication authentication, @RequestBody SharedGameRequest req) { @@ -96,6 +106,7 @@ public ResponseEntity delete(Authentication authentication, @RequestBody Shar /* 게임 공유 -> 공유한 게임 조회(내가 공유한 게임 조회) */ + @Operation(summary = "(내가)공유한 게임 조회") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") @GetMapping("/me") public ResponseEntity getMySharedGames(Authentication authentication) { @@ -104,6 +115,7 @@ public ResponseEntity getMySharedGames(Authentication authentication) { return sharedGameService.getMySharedGames(userId); } + @Operation(summary = "게임 태그 생성") @PreAuthorize("hasAnyAuthority('ADMIN')") @PostMapping("/tags") public ResponseEntity addTag(@RequestBody TagDefCreateRequest req) { @@ -111,6 +123,7 @@ public ResponseEntity addTag(@RequestBody TagDefCreateRequest req) { return tagDefService.addTagName(req); } + @Operation(summary = "게임 태그 삭제 ") @PreAuthorize("hasAnyAuthority('ADMIN')") @DeleteMapping("/tags") public ResponseEntity removeTag(@RequestBody TagDefDeleteRequest req) { diff --git a/src/main/java/com/scriptopia/demo/controller/TestEnvController.java b/src/main/java/com/scriptopia/demo/controller/TestEnvController.java index 759c16e..a7a1145 100644 --- a/src/main/java/com/scriptopia/demo/controller/TestEnvController.java +++ b/src/main/java/com/scriptopia/demo/controller/TestEnvController.java @@ -1,5 +1,6 @@ package com.scriptopia.demo.controller; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -7,6 +8,7 @@ @Controller +@Tag(name = "백엔드 정적 페이지 관련 API", description = "백엔드 정적 페이지 관련 API 입니다.") public class TestEnvController { @GetMapping("/") public String mainPage() { diff --git a/src/main/java/com/scriptopia/demo/controller/UserController.java b/src/main/java/com/scriptopia/demo/controller/UserController.java index b330575..75bdca5 100644 --- a/src/main/java/com/scriptopia/demo/controller/UserController.java +++ b/src/main/java/com/scriptopia/demo/controller/UserController.java @@ -8,6 +8,8 @@ import com.scriptopia.demo.service.HistoryService; import com.scriptopia.demo.service.UserCharacterImgService; import com.scriptopia.demo.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -21,6 +23,7 @@ @RestController @RequestMapping("/users/me") +@Tag(name = "유저 관련 API", description = "유저 관련 API 입니다.") @RequiredArgsConstructor public class UserController { @@ -28,6 +31,7 @@ public class UserController { private final HistoryService historyService; private final UserCharacterImgService userCharacterImgService; + @Operation(summary = "보유 장비 아이템 조회") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @GetMapping("/items/game") public ResponseEntity> getGameItems( @@ -38,6 +42,7 @@ public ResponseEntity> getGameItems( return ResponseEntity.ok(response); } + @Operation(summary = "보유 피아 아이템 조회") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @GetMapping("/items/pia") public ResponseEntity> getPiaItems( @@ -48,6 +53,7 @@ public ResponseEntity> getPiaItems( return ResponseEntity.ok(response); } + @Operation(summary = "사용자 옵션 조회") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @GetMapping("/settings") public ResponseEntity getUserSettings( @@ -58,6 +64,7 @@ public ResponseEntity getUserSettings( return ResponseEntity.ok(response); } + @Operation(summary = "사용자 옵션 변경") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PutMapping("/settings") public ResponseEntity updateUserSettings( @@ -69,6 +76,7 @@ public ResponseEntity updateUserSettings( return ResponseEntity.ok("사용자 설정이 변경되었습니다."); } + @Operation(summary = "사용자 재화 조회") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @GetMapping("/assets") public ResponseEntity getUserAssets( @@ -79,6 +87,7 @@ public ResponseEntity getUserAssets( return ResponseEntity.ok(response); } + @Operation(summary = "사용자 게임 기록 조회") @GetMapping("/games/histories") public ResponseEntity> getHistory(@RequestParam(required = false) UUID lastId, @RequestParam(defaultValue = "10") int size, @@ -88,6 +97,7 @@ public ResponseEntity> getHistory(@RequestParam(requir return historyService.fetchMyHistory(userId, lastId, size); } + @Operation(summary = "프로필 이미지 변경") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") @PostMapping("/profile-images/url") public ResponseEntity saveUserCharacterImg(Authentication authentication, @RequestParam("url") String url) { @@ -96,6 +106,7 @@ public ResponseEntity saveUserCharacterImg(Authentication authentication, @Re return userCharacterImgService.saveUserCharacterImg(userId, url); } + @Operation(summary = "프로필 등록할 수 있는 이미지 조회") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") @GetMapping("/images") public ResponseEntity getUserCharacterImgs(Authentication authentication) { @@ -107,6 +118,7 @@ public ResponseEntity getUserCharacterImgs(Authentication authentication) { /* 등록할 수 있는 이미지 저장 */ + @Operation(summary = "등록할 수 있는 이미지 저장") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") @PostMapping("/save/img") public ResponseEntity saveCharacterImg(Authentication authentication, @RequestParam("file") MultipartFile file) { diff --git a/src/main/java/com/scriptopia/demo/controller/refreshController.java b/src/main/java/com/scriptopia/demo/controller/refreshController.java index 875dc9a..e190402 100644 --- a/src/main/java/com/scriptopia/demo/controller/refreshController.java +++ b/src/main/java/com/scriptopia/demo/controller/refreshController.java @@ -7,6 +7,8 @@ import com.scriptopia.demo.service.LocalAccountService; import com.scriptopia.demo.service.RefreshTokenService; import com.scriptopia.demo.utils.JwtProvider; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; @@ -19,6 +21,7 @@ @RestController @RequestMapping("/token") +@Tag(name = "리프레쉬 토큰 관련 API", description = "리프레쉬 토큰 관련 API 입니다.") @RequiredArgsConstructor public class refreshController { @@ -31,6 +34,7 @@ public class refreshController { private static final boolean COOKIE_SECURE = true; private static final String COOKIE_SAMESITE = "None"; + @Operation(summary = "리프레시 토큰 재발급") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PostMapping("/refresh") public ResponseEntity refresh( diff --git a/src/main/java/com/scriptopia/demo/dto/auth/LoginRequest.java b/src/main/java/com/scriptopia/demo/dto/auth/LoginRequest.java index 6e6c7df..26a6a12 100644 --- a/src/main/java/com/scriptopia/demo/dto/auth/LoginRequest.java +++ b/src/main/java/com/scriptopia/demo/dto/auth/LoginRequest.java @@ -1,5 +1,6 @@ package com.scriptopia.demo.dto.auth; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; @@ -13,12 +14,15 @@ public class LoginRequest { @NotBlank(message = "E_400_MISSING_EMAIL") @Email(message = "E_400_INVALID_EMAIL_FORMAT") + @Schema(description = "사용자 아이디", example = "userA@example.com") private String email; @NotBlank(message = "E_400_MISSING_PASSWORD") + @Schema(description = "비밀번호", example = "userA!234") private String password; @NotBlank(message = "디바이스 식별값이 필요합니다.") + @Schema(description = "디바이스 아이디", example = "1234") private String deviceId; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0840904..0f1419c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -57,6 +57,8 @@ oauth: redirect-uri: ${NAVER_REDIRECT_URI} scope: name email profile_image + + auth: jwt: issuer: scriptopia @@ -69,5 +71,14 @@ app: username: ${ADMIN_NAME} password: ${ADMIN_PASSWORD} +server: + servlet: + context-path: /api/v1 +springdoc: + api-docs: + path: /v3/api-docs + swagger-ui: + path: /swagger-ui + image-dir: ./uploads/ image-url-prefix: /images \ No newline at end of file From 2552c3972d0cb251ab0e7fb6b46f9f8627b0a14e Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 2 Oct 2025 21:14:56 +0900 Subject: [PATCH 29/64] feat: add historyMethod when gameOver and gameClear refactor: update playerHp when ARMOR equiqqed unequiqqed --- .../controller/GameSessionController.java | 16 ++- .../demo/domain/mongo/HistoryInfoMongo.java | 2 +- .../demo/dto/history/HistoryRequest.java | 6 + .../demo/dto/history/HistoryResponse.java | 7 ++ .../demo/service/GameSessionService.java | 73 ++++++++++++- .../demo/service/HistoryService.java | 2 +- src/main/resources/templates/index.html | 103 +++++++++++++----- 7 files changed, 180 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java index 482c045..348b11f 100644 --- a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java +++ b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java @@ -1,9 +1,9 @@ package com.scriptopia.demo.controller; import com.fasterxml.jackson.core.JsonProcessingException; -import com.scriptopia.demo.domain.GameSession; import com.scriptopia.demo.domain.mongo.GameSessionMongo; import com.scriptopia.demo.dto.gamesession.*; +import com.scriptopia.demo.dto.history.HistoryResponse; import com.scriptopia.demo.service.GameSessionService; import com.scriptopia.demo.service.HistoryService; import lombok.RequiredArgsConstructor; @@ -184,4 +184,18 @@ public ResponseEntity seed(Authentication authentication) { Long userId = Long.valueOf(authentication.getName()); return historyService.seedDummySession(userId); } + + + /* + * 게임 종료 후 -> 히스토리 생성 + */ + @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") + @PostMapping("/{gameId}/history") + public ResponseEntity addHistory( + Authentication authentication, + @PathVariable("gameId") String gameId) { + Long userId = Long.valueOf(authentication.getName()); + return gameSessionService.gameToEnd(userId); + } + } diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/HistoryInfoMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/HistoryInfoMongo.java index 217d7e2..cd5dc99 100644 --- a/src/main/java/com/scriptopia/demo/domain/mongo/HistoryInfoMongo.java +++ b/src/main/java/com/scriptopia/demo/domain/mongo/HistoryInfoMongo.java @@ -17,5 +17,5 @@ public class HistoryInfoMongo { private String epilogue2Content; private String epilogue3Title; private String epilogue3Content; - private Integer score; + private Long score; } diff --git a/src/main/java/com/scriptopia/demo/dto/history/HistoryRequest.java b/src/main/java/com/scriptopia/demo/dto/history/HistoryRequest.java index 01cdb1e..3954cc7 100644 --- a/src/main/java/com/scriptopia/demo/dto/history/HistoryRequest.java +++ b/src/main/java/com/scriptopia/demo/dto/history/HistoryRequest.java @@ -1,8 +1,14 @@ package com.scriptopia.demo.dto.history; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@Builder +@NoArgsConstructor +@AllArgsConstructor public class HistoryRequest { private String thumbnailUrl; private String title; diff --git a/src/main/java/com/scriptopia/demo/dto/history/HistoryResponse.java b/src/main/java/com/scriptopia/demo/dto/history/HistoryResponse.java index 050f69b..cf794a3 100644 --- a/src/main/java/com/scriptopia/demo/dto/history/HistoryResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/history/HistoryResponse.java @@ -1,10 +1,17 @@ package com.scriptopia.demo.dto.history; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.LocalDateTime; + +@Builder @Data +@NoArgsConstructor +@AllArgsConstructor public class HistoryResponse { private Long id; private Long userId; diff --git a/src/main/java/com/scriptopia/demo/service/GameSessionService.java b/src/main/java/com/scriptopia/demo/service/GameSessionService.java index eb65054..fe6ef21 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -1,6 +1,8 @@ package com.scriptopia.demo.service; import com.scriptopia.demo.dto.gamesession.ingame.*; +import com.scriptopia.demo.dto.history.HistoryRequest; +import com.scriptopia.demo.dto.history.HistoryResponse; import com.scriptopia.demo.dto.items.ItemDefRequest; import com.scriptopia.demo.dto.items.ItemFastApiResponse; import com.scriptopia.demo.mapper.InGameMapper; @@ -41,6 +43,7 @@ public class GameSessionService { private final ItemService itemService; private final InGameMapper inGameMapper; private final ItemDefRepository itemDefRepository; + private final HistoryRepository historyRepository; public ResponseEntity getGameSession(Long userid) { @@ -590,8 +593,9 @@ public GameSessionMongo gameToChoice(Long userId) { .luck(npcStat[3]) .build(); - gameSessionMongo.setNpcInfo(npcInfoMongo); } + gameSessionMongo.setNpcInfo(npcInfoMongo); + List choiceList = new ArrayList<>(); @@ -635,6 +639,7 @@ public GameSessionMongo gameToChoice(Long userId) { } /** + * 배틍 * @param userId * @return win? */ @@ -1109,6 +1114,9 @@ private void addStats(PlayerInfoMongo player, ItemDefMongo item) { player.setAgility(player.getAgility() + safeStat(item.getAgility())); player.setIntelligence(player.getIntelligence() + safeStat(item.getIntelligence())); player.setLuck(player.getLuck() + safeStat(item.getLuck())); + if (item.getCategory() == ItemType.ARMOR){ + player.setLife( item.getBaseStat() ); + } } private void removeStats(PlayerInfoMongo player, ItemDefMongo item) { @@ -1116,6 +1124,9 @@ private void removeStats(PlayerInfoMongo player, ItemDefMongo item) { player.setAgility(player.getAgility() - safeStat(item.getAgility())); player.setIntelligence(player.getIntelligence() - safeStat(item.getIntelligence())); player.setLuck(player.getLuck() - safeStat(item.getLuck())); + if (item.getCategory() == ItemType.ARMOR){ + player.setLife(80); // 추후 스탯에 따른 체력을 한다면 + } } private int safeStat(Integer stat) { @@ -1258,4 +1269,64 @@ private GameSessionMongo gameToEnd(GameSessionMongo gameSessionMongo, int gameOv return gameSessionMongo; } + + @Transactional + public ResponseEntity gameToEnd(Long userId) { + GameSession gameSession = gameSessionRepository.findByMongoId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + GameSessionMongo gameSessionMongo = gameSessionMongoRepository.findById(gameSession.getMongoId()) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + + + HistoryInfoMongo historyInfoMongo = gameSessionMongo.getHistoryInfo(); + + + HistoryRequest historyRequest = HistoryRequest.builder() + .thumbnailUrl(null) // 필요 시 + .title(historyInfoMongo.getTitle()) + .worldView(historyInfoMongo.getWorldView()) + .backgroundStory(historyInfoMongo.getBackgroundStory()) + .worldPrompt(historyInfoMongo.getWorldPrompt()) + .epilogue1Title(historyInfoMongo.getEpilogue1Title()) + .epilogue1Content(historyInfoMongo.getEpilogue1Content()) + .epilogue2Title(historyInfoMongo.getEpilogue2Title()) + .epilogue2Content(historyInfoMongo.getEpilogue2Content()) + .epilogue3Title(historyInfoMongo.getEpilogue3Title()) + .epilogue3Content(historyInfoMongo.getEpilogue3Content()) + .score(historyInfoMongo.getScore()) + .build(); + + + History history = new History(user, historyRequest); + historyRepository.save(history); + + + HistoryResponse historyResponse = HistoryResponse.builder() + .id(history.getId()) + .userId(user.getId()) + .thumbnailUrl(history.getThumbnailUrl()) + .title(history.getTitle()) + .worldView(history.getWorldView()) + .backgroundStory(history.getBackgroundStory()) + .worldPrompt(history.getWorldPrompt()) + .epilogue1Title(history.getEpilogue1Title()) + .epilogue1Content(history.getEpilogue1Content()) + .epilogue2Title(history.getEpilogue2Title()) + .epilogue2Content(history.getEpilogue2Content()) + .epilogue3Title(history.getEpilogue3Title()) + .epilogue3Content(history.getEpilogue3Content()) + .score(history.getScore()) + .createdAt(history.getCreatedAt()) + .isShared(history.getIsShared()) + .build(); + + + return ResponseEntity.ok(historyResponse); + } + + } diff --git a/src/main/java/com/scriptopia/demo/service/HistoryService.java b/src/main/java/com/scriptopia/demo/service/HistoryService.java index 1ebaf34..35459fa 100644 --- a/src/main/java/com/scriptopia/demo/service/HistoryService.java +++ b/src/main/java/com/scriptopia/demo/service/HistoryService.java @@ -147,4 +147,4 @@ public ResponseEntity> fetchMyHistory(Long userId, UUI return ResponseEntity.ok(page.getContent().stream().map(HistoryPageResponse::from).toList()); } -} +} \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 9b0b4ab..cdf14e0 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -115,6 +115,44 @@

소유 아이템

const getInGameDataBtn = document.getElementById('getInGameDataBtn'); const keepGameBtn = document.getElementById('keepGameBtn'); + + const historyModal = document.getElementById('historyModal'); + const closeHistoryModal = document.getElementById('closeHistoryModal'); + + closeHistoryModal.onclick = () => historyModal.style.display = 'none'; + window.onclick = (event) => { if(event.target === historyModal) historyModal.style.display = 'none'; }; + + const showHistoryModal = (history) => { + if(!history) return; + document.getElementById('historyModalTitle').textContent = history.title || '제목 없음'; + document.getElementById('historyModalWorldView').textContent = history.worldView || ''; + document.getElementById('historyModalBackground').textContent = history.backgroundStory || ''; + document.getElementById('historyModalScore').textContent = `점수: ${history.score || 0}`; + + const epiloguesDiv = document.getElementById('historyModalEpilogues'); + epiloguesDiv.innerHTML = ''; + for(let i=1;i<=3;i++){ + const title = history[`epilogue${i}Title`]; + const content = history[`epilogue${i}Content`]; + if(title || content){ + const div = document.createElement('div'); + div.innerHTML = `

${title || ''}

${content || ''}

`; + epiloguesDiv.appendChild(div); + } + } + + historyModal.style.display = 'block'; + }; + + + + + + + + + + // 초기 로그인 상태 반영 if (accessToken) { loginContainer.style.display = 'none'; @@ -311,6 +349,26 @@

소유 아이템

} }; + // 게임 히스토리 API 호출 + const loadGameHistory = async (gameId) => { + if (!accessToken) { + alert('로그인 필요'); + return; + } + + try { + const history = await safeFetch(null, `/api/v1/games/${gameId}/history`, { + method: 'POST', + headers: { 'Authorization': 'Bearer ' + accessToken } + }); + + console.log('게임 히스토리:', history); + return history; + } catch (err) { + console.error(err); + alert('게임 히스토리 불러오기 오류: ' + err.message); + } + }; const updateGameUI = (data) => { @@ -472,11 +530,12 @@

소유 아이템

switch (data.sceneType) { case 'CHOICE': + console.log(data) if (Array.isArray(data.choiceInfo)) { data.choiceInfo.forEach((choice, idx) => { const btn = document.createElement('button'); btn.className = 'btn'; - btn.textContent = `${idx}. ${choice.detail}`; + btn.textContent = `${idx+1}. ${choice.detail}. ${choice.stats}. ${choice.probability}.`; btn.addEventListener('click', () => sendChoice(idx)); choiceContainer.appendChild(btn); }); @@ -559,18 +618,9 @@

소유 아이템

const gameOverBtn = document.createElement('button'); gameOverBtn.className = 'btn'; gameOverBtn.textContent = '게임 패배 - 결과 보기'; - gameOverBtn.addEventListener('click', () => { - if (data.rewardInfo) { - const r = data.rewardInfo; - choiceContainer.innerHTML = ` -
게임 패배 결과
-
획득 아이템: ${r.gainedItemNames?.join(', ') || '없음'}
-
보상: STR ${r.rewardStrength}, AGI ${r.rewardAgility}, - INT ${r.rewardIntelligence}, LUCK ${r.rewardLuck}, - Life ${r.rewardLife}, Gold ${r.rewardGold}
`; - } else { - choiceContainer.innerHTML = '
결과 없음
'; - } + gameOverBtn.addEventListener('click', async () => { + const history = await loadGameHistory(gameId); + showHistoryModal(history); // ✅ 모달로 표시 }); choiceContainer.appendChild(gameOverBtn); break; @@ -581,18 +631,9 @@

소유 아이템

const gameClearBtn = document.createElement('button'); gameClearBtn.className = 'btn'; gameClearBtn.textContent = '게임 승리 - 결과 보기'; - gameClearBtn.addEventListener('click', () => { - if (data.rewardInfo) { - const r = data.rewardInfo; - choiceContainer.innerHTML = ` -
게임 승리 결과
-
획득 아이템: ${r.gainedItemNames?.join(', ') || '없음'}
-
보상: STR ${r.rewardStrength}, AGI ${r.rewardAgility}, - INT ${r.rewardIntelligence}, LUCK ${r.rewardLuck}, - Life ${r.rewardLife}, Gold ${r.rewardGold}
`; - } else { - choiceContainer.innerHTML = '
결과 없음
'; - } + gameClearBtn.addEventListener('click', async () => { + const history = await loadGameHistory(gameId); + showHistoryModal(history); // ✅ 모달로 표시 }); choiceContainer.appendChild(gameClearBtn); break; @@ -605,5 +646,17 @@

소유 아이템

}); + + + From 2bfb29ac3fd2f633b77d0691217cf5c37e277da5 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 2 Oct 2025 21:55:41 +0900 Subject: [PATCH 30/64] before merge --- .../com/scriptopia/demo/controller/GameSessionController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java index 01362f3..1780e32 100644 --- a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java +++ b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java @@ -200,6 +200,7 @@ public ResponseEntity seed(Authentication authentication) { /* * 게임 종료 후 -> 히스토리 생성 */ + @Operation(summary = "게임 종료 후 히스토리 저장") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") @PostMapping("/{gameId}/history") public ResponseEntity addHistory( From 537921cfdb4d030cea289d70c7b57d6ba0d4347f Mon Sep 17 00:00:00 2001 From: KII1ua Date: Fri, 3 Oct 2025 03:07:31 +0900 Subject: [PATCH 31/64] refactor-shared-games-uuid --- .../demo/controller/SharedGameController.java | 6 ++---- .../PublicSharedGameDetailResponse.java | 11 ++++++---- .../demo/service/SharedGameService.java | 21 +++++++------------ 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java index 34ffebb..e37f964 100644 --- a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -49,14 +49,12 @@ public ResponseEntity share(Authentication authentication, @RequestBody Share */ @Operation(summary = "공유 게임 목록 조회") @GetMapping - public ResponseEntity> getPublicSharedGames(Authentication authentication, - @RequestParam(required = false) UUID lastUUID, + public ResponseEntity> getPublicSharedGames(@RequestParam(required = false) UUID lastUUID, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) List tagIds, @RequestParam(required = false) String query, @RequestParam(defaultValue = "POPULAR")SharedGameSort sort) { - Long viewerId = (authentication == null) ? null : Long.valueOf(authentication.getName()); - return sharedGameService.getPublicSharedGames(viewerId, lastUUID, size, tagIds, query, sort); + return sharedGameService.getPublicSharedGames(lastUUID, size, tagIds, query, sort); } /* diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java index f7f2211..fc98e70 100644 --- a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java @@ -10,12 +10,13 @@ @Data public class PublicSharedGameDetailResponse { private UUID sharedGameUUID; - private String nickname; - private String thumbnailUrl; - private Long totalPlayed; + private String posterUrl; private String title; private String worldView; private String backgroundStory; + private String creator; + private Long playCount; + private Long likeCount; @JsonFormat(shape = JsonFormat.Shape.STRING) private LocalDateTime sharedAt; @@ -24,9 +25,11 @@ public class PublicSharedGameDetailResponse { @Data public static class TagDto { + private Long tagId; private String tagName; - public TagDto(String tagName) { + public TagDto(Long tagId, String tagName) { + this.tagId = tagId; this.tagName = tagName; } } diff --git a/src/main/java/com/scriptopia/demo/service/SharedGameService.java b/src/main/java/com/scriptopia/demo/service/SharedGameService.java index 7f83d01..cfd6581 100644 --- a/src/main/java/com/scriptopia/demo/service/SharedGameService.java +++ b/src/main/java/com/scriptopia/demo/service/SharedGameService.java @@ -99,25 +99,26 @@ public ResponseEntity getDetailedSharedGame(UUID uuid) { SharedGame game = sharedGameRepository.findByUuid(uuid) .orElseThrow(() -> new CustomException(ErrorCode.E_404_SHARED_GAME_NOT_FOUND)); - List tagName = gameTagRepository.findTagNamesBySharedGameId(game.getId()); + List tagDtos = gameTagRepository.findTagDtosBySharedGameId(game.getId()); List score = sharedGameScoreRepository.findAllBySharedGameIdOrderByScoreDescCreatedAtDesc(game.getId()); PublicSharedGameDetailResponse dto = new PublicSharedGameDetailResponse(); dto.setSharedGameUUID(game.getUuid()); - dto.setNickname(game.getUser().getNickname()); - dto.setThumbnailUrl(game.getThumbnailUrl()); - dto.setTotalPlayed(sharedGameScoreRepository.countBySharedGameId(game.getId())); + dto.setPosterUrl(game.getThumbnailUrl()); dto.setTitle(game.getTitle()); dto.setWorldView(game.getWorldView()); dto.setBackgroundStory(game.getBackgroundStory()); + dto.setCreator(game.getUser().getNickname()); + dto.setPlayCount(sharedGameScoreRepository.countBySharedGameId(game.getId())); + dto.setLikeCount(sharedGameFavoriteRepository.countBySharedGameId(game.getId())); dto.setSharedAt(game.getSharedAt()); List tagarray = new ArrayList<>(); List topscorearray = new ArrayList<>(); - for(var tagNames : tagName) { - tagarray.add(new PublicSharedGameDetailResponse.TagDto(tagNames)); + for(var tagDto : tagDtos) { + tagarray.add(new PublicSharedGameDetailResponse.TagDto(tagDto.getTagId(), tagDto.getTagName())); } dto.setTags(tagarray); @@ -146,7 +147,7 @@ public ResponseEntity getTag() { } @Transactional(readOnly = true) - public ResponseEntity> getPublicSharedGames(Long userId, + public ResponseEntity> getPublicSharedGames( UUID lastUuid, int size, List tagIds, @@ -222,12 +223,6 @@ public ResponseEntity> getPublicSharedGames Long topScore = sharedGameScoreRepository.maxScoreBySharedGameId(g.getId()); dto.setTopScore(topScore == null ? 0L : topScore); - // 좋아요 여부 - if (userId != null) { - boolean liked = sharedGameFavoriteRepository.existsByUserIdAndSharedGameId(userId, g.getId()); - dto.setLiked(liked); - } - // 태그 dto.setTags(gameTagRepository.findTagDtosBySharedGameId(g.getId())); return dto; From e44862bd80d1b5b892c61e1f730549692a42f792 Mon Sep 17 00:00:00 2001 From: KII1ua Date: Fri, 3 Oct 2025 03:15:49 +0900 Subject: [PATCH 32/64] refactor-shared-games-uuids --- .../com/scriptopia/demo/controller/SharedGameController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java index e37f964..1168990 100644 --- a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -61,8 +61,8 @@ public ResponseEntity> getPublicSharedGames 게임공유 : 공유된 게임 상세 조회 */ @Operation(summary = "공유 게임 상세 조회") - @GetMapping("/{sharedGameId}") - public ResponseEntity getSharedGameDetail(@PathVariable("sharedGameId") UUID sharedGameId) { + @GetMapping("/{sharedGameUuId}") + public ResponseEntity getSharedGameDetail(@PathVariable("sharedGameUuId") UUID sharedGameId) { return sharedGameService.getDetailedSharedGame(sharedGameId); } From 0f1f4ca2d509c3b27a50e5ecdb44a792db33e71a Mon Sep 17 00:00:00 2001 From: KII1ua Date: Fri, 3 Oct 2025 15:18:47 +0900 Subject: [PATCH 33/64] refactor-shared-game-uuids --- .../demo/controller/SharedGameController.java | 10 +++++----- .../com/scriptopia/demo/dto/sharedgame/CursorPage.java | 2 +- .../demo/repository/SharedGameScoreRepository.java | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java index 1168990..35a0caf 100644 --- a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -49,11 +49,11 @@ public ResponseEntity share(Authentication authentication, @RequestBody Share */ @Operation(summary = "공유 게임 목록 조회") @GetMapping - public ResponseEntity> getPublicSharedGames(@RequestParam(required = false) UUID lastUUID, - @RequestParam(defaultValue = "20") int size, - @RequestParam(required = false) List tagIds, - @RequestParam(required = false) String query, - @RequestParam(defaultValue = "POPULAR")SharedGameSort sort) { + public ResponseEntity> getPublicSharedGames(@RequestParam(value = "lastUUID", required = false) UUID lastUUID, + @RequestParam(value = "size", defaultValue = "20") int size, + @RequestParam(value = "tagIds", required = false) List tagIds, + @RequestParam(value = "query", required = false) String query, + @RequestParam(value = "sort", defaultValue = "POPULAR") SharedGameSort sort) { return sharedGameService.getPublicSharedGames(lastUUID, size, tagIds, query, sort); } diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgame/CursorPage.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/CursorPage.java index 6e2d32a..61af4e3 100644 --- a/src/main/java/com/scriptopia/demo/dto/sharedgame/CursorPage.java +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/CursorPage.java @@ -3,4 +3,4 @@ import java.util.List; import java.util.UUID; -public record CursorPage(List items, UUID nextCursor, boolean hasNext) {} \ No newline at end of file +public record CursorPage(List items, UUID lastUuid, boolean hasNextPage) {} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/repository/SharedGameScoreRepository.java b/src/main/java/com/scriptopia/demo/repository/SharedGameScoreRepository.java index bd759bd..3dd3b41 100644 --- a/src/main/java/com/scriptopia/demo/repository/SharedGameScoreRepository.java +++ b/src/main/java/com/scriptopia/demo/repository/SharedGameScoreRepository.java @@ -2,15 +2,15 @@ import com.scriptopia.demo.domain.SharedGame; import com.scriptopia.demo.domain.SharedGameScore; -import io.lettuce.core.dynamic.annotation.Param; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; public interface SharedGameScoreRepository extends JpaRepository { @Query("Select count(s) from SharedGameScore s where s.sharedGame.id = :sharedGameId") - long countBySharedGameId(Long sharedGameId); + long countBySharedGameId(@Param("sharedGameId") Long sharedGameId); @Query("select coalesce(max(s.score), 0) from SharedGameScore s where s.sharedGame.id = :sharedGameId") Long maxScoreBySharedGameId(@Param("sharedGameId") Long sharedGameId); From f3c78dab1f325fd2dbac002fc2c4cef988494789 Mon Sep 17 00:00:00 2001 From: junseo Lee <74139368+juns0720@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:31:54 +0900 Subject: [PATCH 34/64] feat: refactoring controller and service remove not use methods --- .../controller/GameSessionController.java | 25 --- .../demo/controller/UserController.java | 22 +-- .../demo/service/HistoryService.java | 150 ------------------ .../scriptopia/demo/service/UserService.java | 27 ++++ 4 files changed, 31 insertions(+), 193 deletions(-) delete mode 100644 src/main/java/com/scriptopia/demo/service/HistoryService.java diff --git a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java index 1780e32..50c0b01 100644 --- a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java +++ b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java @@ -5,7 +5,6 @@ import com.scriptopia.demo.dto.gamesession.*; import com.scriptopia.demo.dto.history.HistoryResponse; import com.scriptopia.demo.service.GameSessionService; -import com.scriptopia.demo.service.HistoryService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -21,7 +20,6 @@ public class GameSessionController { private final GameSessionService gameSessionService; - private final HistoryService historyService; /* * 게임 -> 게임 도중 종료 @@ -173,29 +171,6 @@ public ResponseEntity sellItem( return ResponseEntity.ok(response); } - /** - * 현재는 userId, sessionId를 통해 저장하는데 - * 인증 관리 부분 끝나면 header에 token 꺼내오고 requestparameter session_id로 저장하게 수정 - */ - /* - * 게임 -> 히스토리 생성 - */ - @Operation(summary = "") - @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") - @PostMapping("/history") - public ResponseEntity addHistory(Authentication authentication, @RequestBody GameSessionRequest request) { - Long userId = Long.valueOf(authentication.getName()); - return historyService.createHistory(userId, request.getGameId()); - } - - /** 개발용: 로컬 MongoDB에 더미 세션 한 건 심어서 테스트용 ObjectId 반환 */ - @Operation(summary = "") - @PostMapping("/history/seed") - public ResponseEntity seed(Authentication authentication) { - Long userId = Long.valueOf(authentication.getName()); - return historyService.seedDummySession(userId); - } - /* * 게임 종료 후 -> 히스토리 생성 diff --git a/src/main/java/com/scriptopia/demo/controller/UserController.java b/src/main/java/com/scriptopia/demo/controller/UserController.java index 75bdca5..f147a94 100644 --- a/src/main/java/com/scriptopia/demo/controller/UserController.java +++ b/src/main/java/com/scriptopia/demo/controller/UserController.java @@ -5,7 +5,6 @@ import com.scriptopia.demo.dto.users.PiaItemDTO; import com.scriptopia.demo.dto.users.UserAssetsResponse; import com.scriptopia.demo.dto.users.UserSettingsDTO; -import com.scriptopia.demo.service.HistoryService; import com.scriptopia.demo.service.UserCharacterImgService; import com.scriptopia.demo.service.UserService; import io.swagger.v3.oas.annotations.Operation; @@ -16,7 +15,6 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; import java.util.List; import java.util.UUID; @@ -28,7 +26,6 @@ public class UserController { private final UserService userService; - private final HistoryService historyService; private final UserCharacterImgService userCharacterImgService; @Operation(summary = "보유 장비 아이템 조회") @@ -94,36 +91,25 @@ public ResponseEntity> getHistory(@RequestParam(requir Authentication authentication) { Long userId = Long.valueOf(authentication.getName()); - return historyService.fetchMyHistory(userId, lastId, size); + return ResponseEntity.ok(userService.fetchMyHistory(userId, lastId, size)); } @Operation(summary = "프로필 이미지 변경") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") - @PostMapping("/profile-images/url") + @PostMapping("/profile-images") public ResponseEntity saveUserCharacterImg(Authentication authentication, @RequestParam("url") String url) { Long userId = Long.valueOf(authentication.getName()); return userCharacterImgService.saveUserCharacterImg(userId, url); } - @Operation(summary = "프로필 등록할 수 있는 이미지 조회") + @Operation(summary = "프로필 이미지 조회") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") - @GetMapping("/images") + @GetMapping("/profile-images") public ResponseEntity getUserCharacterImgs(Authentication authentication) { Long userId = Long.valueOf(authentication.getName()); return userCharacterImgService.getUserCharacterImg(userId); } - /* - 등록할 수 있는 이미지 저장 - */ - @Operation(summary = "등록할 수 있는 이미지 저장") - @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") - @PostMapping("/save/img") - public ResponseEntity saveCharacterImg(Authentication authentication, @RequestParam("file") MultipartFile file) { - Long userId = Long.valueOf(authentication.getName()); - - return userCharacterImgService.saveCharacterImg(userId, file); - } } diff --git a/src/main/java/com/scriptopia/demo/service/HistoryService.java b/src/main/java/com/scriptopia/demo/service/HistoryService.java deleted file mode 100644 index 35459fa..0000000 --- a/src/main/java/com/scriptopia/demo/service/HistoryService.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.scriptopia.demo.service; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.scriptopia.demo.domain.History; -import com.scriptopia.demo.domain.User; -import com.scriptopia.demo.dto.history.HistoryPageResponse; -import com.scriptopia.demo.dto.history.HistoryRequest; -import com.scriptopia.demo.exception.CustomException; -import com.scriptopia.demo.exception.ErrorCode; -import com.scriptopia.demo.repository.HistoryRepository; -import com.scriptopia.demo.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.bson.Document; -import org.bson.types.ObjectId; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -@Service -@RequiredArgsConstructor -public class HistoryService { - private final UserRepository userRepository; - private final HistoryRepository historyRepository; - private final MongoTemplate mongoTemplate; - private final ObjectMapper objectMapper; - - private static final String COLL = "game_session"; - - @Transactional - public ResponseEntity createHistory(Long userId, String sid) { - ObjectId oid = new ObjectId(sid); - - Query q = Query.query(Criteria.where("_id").is(oid)); - Document doc = mongoTemplate.findOne(q, Document.class, COLL); - if(doc == null) return ResponseEntity.badRequest().body("세션 ID 없음"); - - Object historyIdInSession = doc.get("history_id"); - - if(historyIdInSession != null) { - return ResponseEntity.ok(Map.of("historyId", ((Number)historyIdInSession).longValue())); - } - - HistoryRequest req = mapMongoToHistoryRequest(doc); - User user = userRepository.findById(userId) - .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); - History history = new History(user, req); - - return ResponseEntity.ok(historyRepository.save(history)); - } - - @Transactional - public ResponseEntity seedDummySession(Long userId) { - Document hi = new Document(Map.ofEntries( - Map.entry("title", "임시 여정 제목"), - Map.entry("world_prompt", "임시 세계관 프롬프트"), - Map.entry("background_story", "AI가 생성한 배경 이야기"), - Map.entry("world_view", "AI가 생성한 세계관"), - Map.entry("epilogue_1_title", "엔딩A"), - Map.entry("epilogue_1_content", "엔딩A 내용"), - Map.entry("epilogue_2_title", "엔딩B"), - Map.entry("epilogue_2_content", "엔딩B 내용"), - Map.entry("epilogue_3_title", "엔딩C"), - Map.entry("epilogue_3_content", "엔딩C 내용"), - Map.entry("score", 1234) - )); - - Document doc = new Document(); - doc.put("user_id", userId); - doc.put("scene_type", "done"); - doc.put("started_at", Instant.now()); - doc.put("updated_at", Instant.now()); - doc.put("background", "https://cdn.example.com/bg/temp.png"); // 썸네일로 매핑할 예정 - doc.put("progress", 100); - doc.put("stage", List.of(1,2,3)); - doc.put("history_info", hi); - - Document saved = mongoTemplate.insert(doc, COLL); - return ResponseEntity.ok(saved.getObjectId("_id").toHexString()); - } - - private HistoryRequest mapMongoToHistoryRequest(Document doc) { - JsonNode root = asJson(doc); - JsonNode hi = root.path("history_info"); - - // 필수값: title, world_prompt, score - String title = hi.path("title").asText(""); - String worldPrompt = hi.path("world_prompt").asText(""); - Integer score = hi.path("score").isNumber() ? hi.path("score").asInt() : null; - if (title.isBlank() || worldPrompt.isBlank() || score == null) { - throw new IllegalArgumentException("history_info의 필수값(title, world_prompt, score)이 누락되었습니다."); - } - - HistoryRequest req = new HistoryRequest(); - // thumbnailUrl: Mongo의 background를 임시 썸네일로 사용 - req.setThumbnailUrl(root.path("background").isTextual() ? root.get("background").asText() : null); - - req.setTitle(title); - // 정책에 맞게 매핑: worldView는 비워두거나 world_prompt로 대체 가능 - req.setBackgroundStory(hi.path("background_story").asText(null)); - req.setWorldView(hi.path("world_view").asText(null)); // 또는 req.setWorldView(worldPrompt); - req.setWorldPrompt(worldPrompt); - - req.setEpilogue1Title(hi.path("epilogue_1_title").asText(null)); - req.setEpilogue1Content(hi.path("epilogue_1_content").asText(null)); - req.setEpilogue2Title(hi.path("epilogue_2_title").asText(null)); - req.setEpilogue2Content(hi.path("epilogue_2_content").asText(null)); - req.setEpilogue3Title(hi.path("epilogue_3_title").asText(null)); - req.setEpilogue3Content(hi.path("epilogue_3_content").asText(null)); - - req.setScore(score.longValue()); - return req; - } - - private JsonNode asJson(Document doc) { - try { return objectMapper.readTree(doc.toJson()); } - catch (Exception e) { throw new RuntimeException("Mongo Document → JsonNode 변환 실패", e); } - } - - @Transactional(readOnly = true) - public ResponseEntity> fetchMyHistory(Long userId, UUID lastId, int size) { - PageRequest pr = PageRequest.of(0, size); - Page page; - - User user = userRepository.findById(userId) - .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); - - if(lastId == null) page = historyRepository.findByUserIdOrderByIdDesc(user.getId(), pr); - else { - Long lastIds = historyRepository.findByUserIdAndUuid(user.getId(), lastId) - .orElseThrow(() -> new CustomException(ErrorCode.E_404_PAGE_NOT_FOUND)); - - page = historyRepository.findByUserIdAndIdLessThanOrderByIdDesc(user.getId(), lastIds, pr); - } - - return ResponseEntity.ok(page.getContent().stream().map(HistoryPageResponse::from).toList()); - } - -} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/service/UserService.java b/src/main/java/com/scriptopia/demo/service/UserService.java index f729030..df08bfb 100644 --- a/src/main/java/com/scriptopia/demo/service/UserService.java +++ b/src/main/java/com/scriptopia/demo/service/UserService.java @@ -1,8 +1,10 @@ package com.scriptopia.demo.service; +import com.scriptopia.demo.domain.History; import com.scriptopia.demo.domain.User; import com.scriptopia.demo.domain.UserPiaItem; import com.scriptopia.demo.domain.UserSetting; +import com.scriptopia.demo.dto.history.HistoryPageResponse; import com.scriptopia.demo.dto.items.ItemDTO; import com.scriptopia.demo.dto.users.PiaItemDTO; import com.scriptopia.demo.dto.users.UserAssetsResponse; @@ -10,22 +12,28 @@ import com.scriptopia.demo.exception.CustomException; import com.scriptopia.demo.exception.ErrorCode; import com.scriptopia.demo.mapper.ItemMapper; +import com.scriptopia.demo.repository.HistoryRepository; import com.scriptopia.demo.repository.UserPiaItemRepository; import com.scriptopia.demo.repository.UserRepository; import com.scriptopia.demo.repository.UserSettingRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.UUID; @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class UserService { + private final HistoryRepository historyRepository; private final UserSettingRepository userSettingRepository; private final UserRepository userRepository; private final UserPiaItemRepository userPiaItemRepository; @@ -105,6 +113,25 @@ public List getPiaItems(String userId){ return piaItems; } + @Transactional(readOnly = true) + public List fetchMyHistory(Long userId, UUID lastId, int size) { + PageRequest pr = PageRequest.of(0, size); + Page page; + + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + + if(lastId == null) page = historyRepository.findByUserIdOrderByIdDesc(user.getId(), pr); + else { + Long lastIds = historyRepository.findByUserIdAndUuid(user.getId(), lastId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_PAGE_NOT_FOUND)); + + page = historyRepository.findByUserIdAndIdLessThanOrderByIdDesc(user.getId(), lastIds, pr); + } + + return page.getContent().stream().map(HistoryPageResponse::from).toList(); + } + } From dd845cd0a38356cc83ccd19aba2bf407a6d10aae Mon Sep 17 00:00:00 2001 From: junseo Lee <74139368+juns0720@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:45:14 +0900 Subject: [PATCH 35/64] Delete docker_compose_files/.env --- docker_compose_files/.env | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 docker_compose_files/.env diff --git a/docker_compose_files/.env b/docker_compose_files/.env deleted file mode 100644 index 27c0c4e..0000000 --- a/docker_compose_files/.env +++ /dev/null @@ -1,8 +0,0 @@ -# Postgres -POSTGRES_USER=root -POSTGRES_PASSWORD=tiger -POSTGRES_DB=scriptopia - -# Mongo -MONGO_INITDB_ROOT_USERNAME=root -MONGO_INITDB_ROOT_PASSWORD=tiger From 6706f38e3c4889e9d69833ccdf4012e73ec0b86f Mon Sep 17 00:00:00 2001 From: KII1ua Date: Fri, 3 Oct 2025 18:03:29 +0900 Subject: [PATCH 36/64] refactor-usercontroller-profile-image --- .gitignore | 1 + .../scriptopia/demo/controller/UserController.java | 5 +++-- .../scriptopia/demo/dto/users/UserImageRequest.java | 12 ++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/scriptopia/demo/dto/users/UserImageRequest.java diff --git a/.gitignore b/.gitignore index 5410b95..cfc2e85 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ out/ ### VS Code ### .vscode/ +/docker_compose_file/.env /docker_compose_files/data /docker_compose_files/mongo_data /docker_compose_files/postgres_data diff --git a/src/main/java/com/scriptopia/demo/controller/UserController.java b/src/main/java/com/scriptopia/demo/controller/UserController.java index f147a94..295c43b 100644 --- a/src/main/java/com/scriptopia/demo/controller/UserController.java +++ b/src/main/java/com/scriptopia/demo/controller/UserController.java @@ -4,6 +4,7 @@ import com.scriptopia.demo.dto.items.ItemDTO; import com.scriptopia.demo.dto.users.PiaItemDTO; import com.scriptopia.demo.dto.users.UserAssetsResponse; +import com.scriptopia.demo.dto.users.UserImageRequest; import com.scriptopia.demo.dto.users.UserSettingsDTO; import com.scriptopia.demo.service.UserCharacterImgService; import com.scriptopia.demo.service.UserService; @@ -97,10 +98,10 @@ public ResponseEntity> getHistory(@RequestParam(requir @Operation(summary = "프로필 이미지 변경") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") @PostMapping("/profile-images") - public ResponseEntity saveUserCharacterImg(Authentication authentication, @RequestParam("url") String url) { + public ResponseEntity saveUserCharacterImg(Authentication authentication, @RequestBody UserImageRequest req) { Long userId = Long.valueOf(authentication.getName()); - return userCharacterImgService.saveUserCharacterImg(userId, url); + return userCharacterImgService.saveUserCharacterImg(userId, req.getUrl()); } @Operation(summary = "프로필 이미지 조회") diff --git a/src/main/java/com/scriptopia/demo/dto/users/UserImageRequest.java b/src/main/java/com/scriptopia/demo/dto/users/UserImageRequest.java new file mode 100644 index 0000000..d22faa4 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/users/UserImageRequest.java @@ -0,0 +1,12 @@ +package com.scriptopia.demo.dto.users; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; + +@Data +@NoArgsConstructor +@RequiredArgsConstructor +public class UserImageRequest { + private String url; +} From 8784078357ea7fada9a7d2293282cdfc406840c2 Mon Sep 17 00:00:00 2001 From: KII1ua Date: Fri, 3 Oct 2025 18:11:12 +0900 Subject: [PATCH 37/64] refactor-gamesession-Uuid --- .../java/com/scriptopia/demo/dto/history/HistoryResponse.java | 3 ++- .../java/com/scriptopia/demo/service/GameSessionService.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/dto/history/HistoryResponse.java b/src/main/java/com/scriptopia/demo/dto/history/HistoryResponse.java index cf794a3..0829507 100644 --- a/src/main/java/com/scriptopia/demo/dto/history/HistoryResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/history/HistoryResponse.java @@ -6,6 +6,7 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import java.util.UUID; @Builder @@ -13,7 +14,7 @@ @NoArgsConstructor @AllArgsConstructor public class HistoryResponse { - private Long id; + private UUID uuid; private Long userId; private String thumbnailUrl; private String title; diff --git a/src/main/java/com/scriptopia/demo/service/GameSessionService.java b/src/main/java/com/scriptopia/demo/service/GameSessionService.java index fe6ef21..d7f9574 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -1306,7 +1306,7 @@ public ResponseEntity gameToEnd(Long userId) { HistoryResponse historyResponse = HistoryResponse.builder() - .id(history.getId()) + .uuid(history.getUuid()) .userId(user.getId()) .thumbnailUrl(history.getThumbnailUrl()) .title(history.getTitle()) From 9c4c9dd9c1a1630d3bc42d5042b203f0d5350322 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Fri, 3 Oct 2025 18:25:29 +0900 Subject: [PATCH 38/64] =?UTF-8?q?=C3=A3update=20gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5410b95..7f0d5f3 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ out/ ### VS Code ### .vscode/ +/docker_compose_files/.env /docker_compose_files/data /docker_compose_files/mongo_data /docker_compose_files/postgres_data From 49548e386f0a001244d91e1296e3a9555cfff9c9 Mon Sep 17 00:00:00 2001 From: KII1ua Date: Sat, 4 Oct 2025 17:11:15 +0900 Subject: [PATCH 39/64] refactor-gameHistory --- .../demo/controller/UserController.java | 7 ++-- .../demo/dto/history/HistoryPageResponse.java | 41 +++++++++++++++---- .../dto/history/HistoryPageResponseDto.java | 15 +++++++ .../demo/dto/users/UserImageRequest.java | 2 - .../demo/repository/HistoryRepository.java | 18 +++++--- .../demo/service/SharedGameService.java | 2 + .../scriptopia/demo/service/UserService.java | 31 +++++++------- 7 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/scriptopia/demo/dto/history/HistoryPageResponseDto.java diff --git a/src/main/java/com/scriptopia/demo/controller/UserController.java b/src/main/java/com/scriptopia/demo/controller/UserController.java index 295c43b..471fab1 100644 --- a/src/main/java/com/scriptopia/demo/controller/UserController.java +++ b/src/main/java/com/scriptopia/demo/controller/UserController.java @@ -1,6 +1,7 @@ package com.scriptopia.demo.controller; import com.scriptopia.demo.dto.history.HistoryPageResponse; +import com.scriptopia.demo.dto.history.HistoryPageResponseDto; import com.scriptopia.demo.dto.items.ItemDTO; import com.scriptopia.demo.dto.users.PiaItemDTO; import com.scriptopia.demo.dto.users.UserAssetsResponse; @@ -87,9 +88,9 @@ public ResponseEntity getUserAssets( @Operation(summary = "사용자 게임 기록 조회") @GetMapping("/games/histories") - public ResponseEntity> getHistory(@RequestParam(required = false) UUID lastId, - @RequestParam(defaultValue = "10") int size, - Authentication authentication) { + public ResponseEntity getHistory(@RequestParam(required = false) UUID lastId, + @RequestParam(defaultValue = "10") int size, + Authentication authentication) { Long userId = Long.valueOf(authentication.getName()); return ResponseEntity.ok(userService.fetchMyHistory(userId, lastId, size)); diff --git a/src/main/java/com/scriptopia/demo/dto/history/HistoryPageResponse.java b/src/main/java/com/scriptopia/demo/dto/history/HistoryPageResponse.java index 922a753..5489df9 100644 --- a/src/main/java/com/scriptopia/demo/dto/history/HistoryPageResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/history/HistoryPageResponse.java @@ -1,27 +1,50 @@ package com.scriptopia.demo.dto.history; import com.scriptopia.demo.domain.History; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.LocalDateTime; import java.util.UUID; @Data +@Builder +@NoArgsConstructor +@AllArgsConstructor public class HistoryPageResponse { private UUID uuid; + private String thumbnailUrl; private String title; + private String worldView; + private String backgroundStory; + private String epilogue1Title; + private String epilogue1Content; + private String epilogue2Title; + private String epilogue2Content; + private String epilogue3Title; + private String epilogue3Content; private Long score; - private String thumbnail_url; private LocalDateTime created_at; + private boolean isShared; public static HistoryPageResponse from(History h) { - HistoryPageResponse dto = new HistoryPageResponse(); - dto.setUuid(h.getUuid()); - dto.setTitle(h.getTitle()); - dto.setScore(h.getScore()); - dto.setThumbnail_url(h.getThumbnailUrl()); - dto.setCreated_at(h.getCreatedAt()); - - return dto; + return HistoryPageResponse.builder() + .uuid(h.getUuid()) + .thumbnailUrl(h.getThumbnailUrl()) + .title(h.getTitle()) + .worldView(h.getWorldView()) + .backgroundStory(h.getBackgroundStory()) + .epilogue1Title(h.getEpilogue1Title()) + .epilogue1Content(h.getEpilogue1Content()) + .epilogue2Title(h.getEpilogue2Title()) + .epilogue2Content(h.getEpilogue2Content()) + .epilogue3Title(h.getEpilogue3Title()) + .epilogue3Content(h.getEpilogue3Content()) + .score(h.getScore()) + .created_at(h.getCreatedAt()) + .isShared(h.getIsShared()) + .build(); } } diff --git a/src/main/java/com/scriptopia/demo/dto/history/HistoryPageResponseDto.java b/src/main/java/com/scriptopia/demo/dto/history/HistoryPageResponseDto.java new file mode 100644 index 0000000..8111e74 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/history/HistoryPageResponseDto.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.dto.history; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; +import java.util.UUID; + +@Data +@AllArgsConstructor +public class HistoryPageResponseDto { + private List data; + private UUID nextCursor; + private boolean hasNext; +} diff --git a/src/main/java/com/scriptopia/demo/dto/users/UserImageRequest.java b/src/main/java/com/scriptopia/demo/dto/users/UserImageRequest.java index d22faa4..f00f9cb 100644 --- a/src/main/java/com/scriptopia/demo/dto/users/UserImageRequest.java +++ b/src/main/java/com/scriptopia/demo/dto/users/UserImageRequest.java @@ -2,11 +2,9 @@ import lombok.Data; import lombok.NoArgsConstructor; -import lombok.RequiredArgsConstructor; @Data @NoArgsConstructor -@RequiredArgsConstructor public class UserImageRequest { private String url; } diff --git a/src/main/java/com/scriptopia/demo/repository/HistoryRepository.java b/src/main/java/com/scriptopia/demo/repository/HistoryRepository.java index 245f021..ba43095 100644 --- a/src/main/java/com/scriptopia/demo/repository/HistoryRepository.java +++ b/src/main/java/com/scriptopia/demo/repository/HistoryRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -14,10 +15,15 @@ public interface HistoryRepository extends JpaRepository { @Query("select h from History h where h.uuid = :uuid") Optional findByUuid(@Param("uuid") UUID uuid); - @Query("select h.id from History h where h.user.id = :userId and h.uuid = :uuid") - Optional findByUserIdAndUuid(@Param("userId") Long userId, @Param("uuid") UUID uuid); - - Page findByUserIdAndIdLessThanOrderByIdDesc(Long userId, Long lastId, Pageable pageable); - - Page findByUserIdOrderByIdDesc(Long userId, Pageable pageable); + @Query(""" + SELECT h FROM History h + WHERE h.user.id = :userId + AND (:cursor IS NULL OR h.createdAt < (SELECT h2.createdAt FROM History h2 WHERE h2.uuid = :cursor)) + ORDER BY h.createdAt DESC + """) + List findHistoriesByUserWithCursor( + @Param("userId") Long userId, + @Param("cursor") UUID cursor, + Pageable pageable + ); } diff --git a/src/main/java/com/scriptopia/demo/service/SharedGameService.java b/src/main/java/com/scriptopia/demo/service/SharedGameService.java index cfd6581..856c97d 100644 --- a/src/main/java/com/scriptopia/demo/service/SharedGameService.java +++ b/src/main/java/com/scriptopia/demo/service/SharedGameService.java @@ -41,6 +41,8 @@ public ResponseEntity saveSharedGame(Long Id, UUID uuid) { throw new CustomException(ErrorCode.E_401_NOT_EQUAL_SHARED_GAME); } + history.setIsShared(true); + SharedGame sharedGame = SharedGame.from(user, history); return ResponseEntity.ok(sharedGameRepository.save(sharedGame)); } diff --git a/src/main/java/com/scriptopia/demo/service/UserService.java b/src/main/java/com/scriptopia/demo/service/UserService.java index df08bfb..2667cfe 100644 --- a/src/main/java/com/scriptopia/demo/service/UserService.java +++ b/src/main/java/com/scriptopia/demo/service/UserService.java @@ -5,6 +5,8 @@ import com.scriptopia.demo.domain.UserPiaItem; import com.scriptopia.demo.domain.UserSetting; import com.scriptopia.demo.dto.history.HistoryPageResponse; +import com.scriptopia.demo.dto.history.HistoryPageResponseDto; +import com.scriptopia.demo.dto.history.HistoryResponse; import com.scriptopia.demo.dto.items.ItemDTO; import com.scriptopia.demo.dto.users.PiaItemDTO; import com.scriptopia.demo.dto.users.UserAssetsResponse; @@ -27,6 +29,7 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -114,24 +117,22 @@ public List getPiaItems(String userId){ } @Transactional(readOnly = true) - public List fetchMyHistory(Long userId, UUID lastId, int size) { - PageRequest pr = PageRequest.of(0, size); - Page page; + public HistoryPageResponseDto fetchMyHistory(Long userId, UUID lastId, int size) { + List histories = historyRepository.findHistoriesByUserWithCursor( + userId, + lastId, + PageRequest.of(0, size + 1) + ); - User user = userRepository.findById(userId) - .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + boolean hasNext = histories.size() > size; - if(lastId == null) page = historyRepository.findByUserIdOrderByIdDesc(user.getId(), pr); - else { - Long lastIds = historyRepository.findByUserIdAndUuid(user.getId(), lastId) - .orElseThrow(() -> new CustomException(ErrorCode.E_404_PAGE_NOT_FOUND)); + List result = histories.stream() + .limit(size) + .map(HistoryPageResponse::from) + .collect(Collectors.toList()); - page = historyRepository.findByUserIdAndIdLessThanOrderByIdDesc(user.getId(), lastIds, pr); - } + UUID nextCursor = hasNext ? result.get(result.size() - 1).getUuid() : null; - return page.getContent().stream().map(HistoryPageResponse::from).toList(); + return new HistoryPageResponseDto(result, nextCursor, hasNext); } - - - } From dfc77b72c8db6ed51aa26acaddf59cc99c653175 Mon Sep 17 00:00:00 2001 From: KII1ua Date: Sun, 5 Oct 2025 15:14:26 +0900 Subject: [PATCH 40/64] refactor-gameHistory, gameShared --- .../demo/controller/SharedGameController.java | 20 ++++++++++++----- .../demo/dto/TagDef/TagDefDeleteRequest.java | 2 +- .../PublicSharedGameDetailResponse.java | 4 +++- .../dto/sharedgame/SharedGameSaveDto.java | 21 ++++++++++++++++++ .../SharedGameFavoriteResponse.java | 13 +++++++++-- .../service/SharedGameFavoriteService.java | 21 +++++++++--------- .../demo/service/SharedGameService.java | 22 ++++++++++++++++--- .../demo/service/TagDefService.java | 2 +- 8 files changed, 82 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/scriptopia/demo/dto/sharedgame/SharedGameSaveDto.java diff --git a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java index 35a0caf..ac78a53 100644 --- a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -16,7 +16,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authorization.AuthenticatedAuthorizationManager; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -62,8 +64,16 @@ public ResponseEntity> getPublicSharedGames */ @Operation(summary = "공유 게임 상세 조회") @GetMapping("/{sharedGameUuId}") - public ResponseEntity getSharedGameDetail(@PathVariable("sharedGameUuId") UUID sharedGameId) { - return sharedGameService.getDetailedSharedGame(sharedGameId); + public ResponseEntity getSharedGameDetail(Authentication authentication, @PathVariable("sharedGameUuId") UUID sharedGameId) { + Long userId = null; + if (authentication != null && authentication.isAuthenticated() && authentication.getName() != null) { + try { + userId = Long.valueOf(authentication.getName()); + } catch (NumberFormatException ignored) { + } + } + + return sharedGameService.getDetailedSharedGame(userId, sharedGameId); } /* @@ -71,8 +81,8 @@ public ResponseEntity getSharedGameDetail(@PathVariable("sharedGameUuId") UUI */ @Operation(summary = "공유 게임 Like 요청") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") - @PostMapping("{sharedGameId}/like") - public ResponseEntity likeSharedGame(@PathVariable("sharedGameId") UUID sharedGameId, Authentication authentication) { + @PostMapping("{sharedGameUuId}/like") + public ResponseEntity likeSharedGame(@PathVariable("sharedGameUuId") UUID sharedGameId, Authentication authentication) { Long userId = Long.valueOf(authentication.getName()); return sharedGameFavoriteService.saveFavorite(userId, sharedGameId); @@ -92,7 +102,7 @@ public ResponseEntity getSharedGameTags() { */ @Operation(summary = "공유한 게임 삭제") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") - @DeleteMapping("/shared-games") + @DeleteMapping public ResponseEntity delete(Authentication authentication, @RequestBody SharedGameRequest req) { Long userId = Long.valueOf(authentication.getName()); diff --git a/src/main/java/com/scriptopia/demo/dto/TagDef/TagDefDeleteRequest.java b/src/main/java/com/scriptopia/demo/dto/TagDef/TagDefDeleteRequest.java index d0996d0..6573c47 100644 --- a/src/main/java/com/scriptopia/demo/dto/TagDef/TagDefDeleteRequest.java +++ b/src/main/java/com/scriptopia/demo/dto/TagDef/TagDefDeleteRequest.java @@ -8,5 +8,5 @@ @AllArgsConstructor @NoArgsConstructor public class TagDefDeleteRequest { - private String name; + private String tagName; } diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java index fc98e70..d84bdaf 100644 --- a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java @@ -9,7 +9,7 @@ @Data public class PublicSharedGameDetailResponse { - private UUID sharedGameUUID; + private UUID sharedGameUuID; private String posterUrl; private String title; private String worldView; @@ -17,6 +17,7 @@ public class PublicSharedGameDetailResponse { private String creator; private Long playCount; private Long likeCount; + private boolean isLiked; @JsonFormat(shape = JsonFormat.Shape.STRING) private LocalDateTime sharedAt; @@ -37,6 +38,7 @@ public TagDto(Long tagId, String tagName) { @Data public static class TopScoreDto { private String nickname; + private String profileUrl; private Long score; @JsonFormat(shape = JsonFormat.Shape.STRING) private LocalDateTime createdAt; diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgame/SharedGameSaveDto.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/SharedGameSaveDto.java new file mode 100644 index 0000000..0b1dd6f --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/SharedGameSaveDto.java @@ -0,0 +1,21 @@ +package com.scriptopia.demo.dto.sharedgame; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SharedGameSaveDto { + private String sharedGameUuid; + private String thumbnailUrl; + private Long recommand; + private Long playCount; + private String title; + private String worldView; + private String backgroundStory; + private LocalDateTime sharedAt; +} diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgamefavorite/SharedGameFavoriteResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgamefavorite/SharedGameFavoriteResponse.java index ff09157..e3dca08 100644 --- a/src/main/java/com/scriptopia/demo/dto/sharedgamefavorite/SharedGameFavoriteResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/sharedgamefavorite/SharedGameFavoriteResponse.java @@ -1,15 +1,24 @@ package com.scriptopia.demo.dto.sharedgamefavorite; +import com.scriptopia.demo.dto.sharedgame.TagDto; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class SharedGameFavoriteResponse { - private Long sharedGameId; + private String sharedGameUuid; private String thumbnailUrl; private boolean isLiked; private Long likeCount; private Long totalPlayCount; private String title; - private String[] tags; + private List tags; private Long topScore; } diff --git a/src/main/java/com/scriptopia/demo/service/SharedGameFavoriteService.java b/src/main/java/com/scriptopia/demo/service/SharedGameFavoriteService.java index 038a9d8..3a10bdb 100644 --- a/src/main/java/com/scriptopia/demo/service/SharedGameFavoriteService.java +++ b/src/main/java/com/scriptopia/demo/service/SharedGameFavoriteService.java @@ -48,17 +48,18 @@ public ResponseEntity saveFavorite(Long userId, UUID uuid) { Long maxScore = sharedGameScoreRepository.maxScoreBySharedGameId(game.getId()); // 태그 이름들 - var tagNames = gameTagRepository.findTagNamesBySharedGameId(game.getId()); + var tags = gameTagRepository.findTagDtosBySharedGameId(game.getId()); - var dto = new SharedGameFavoriteResponse(); - dto.setSharedGameId(game.getId()); - dto.setThumbnailUrl(game.getThumbnailUrl()); - dto.setLiked(liked); - dto.setLikeCount(likeCount); - dto.setTotalPlayCount(playCount); - dto.setTitle(game.getTitle()); - dto.setTags(tagNames.isEmpty() ? null : tagNames.toArray(new String[0])); - dto.setTopScore(maxScore); + var dto = SharedGameFavoriteResponse.builder() + .sharedGameUuid(game.getUuid().toString()) + .thumbnailUrl(game.getThumbnailUrl()) + .isLiked(liked) + .likeCount(likeCount) + .totalPlayCount(playCount) + .title(game.getTitle()) + .tags(tags) + .topScore(maxScore) + .build(); return ResponseEntity.ok(dto); } diff --git a/src/main/java/com/scriptopia/demo/service/SharedGameService.java b/src/main/java/com/scriptopia/demo/service/SharedGameService.java index 856c97d..7881abe 100644 --- a/src/main/java/com/scriptopia/demo/service/SharedGameService.java +++ b/src/main/java/com/scriptopia/demo/service/SharedGameService.java @@ -10,6 +10,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -44,7 +45,19 @@ public ResponseEntity saveSharedGame(Long Id, UUID uuid) { history.setIsShared(true); SharedGame sharedGame = SharedGame.from(user, history); - return ResponseEntity.ok(sharedGameRepository.save(sharedGame)); + sharedGameRepository.save(sharedGame); + + SharedGameSaveDto dto = new SharedGameSaveDto(); + dto.setSharedGameUuid(sharedGame.getUuid().toString()); + dto.setThumbnailUrl(sharedGame.getThumbnailUrl()); + dto.setRecommand(sharedGameFavoriteRepository.countBySharedGameId(sharedGame.getId())); + dto.setPlayCount(sharedGameScoreRepository.countBySharedGameId(sharedGame.getId())); + dto.setTitle(sharedGame.getTitle()); + dto.setWorldView(sharedGame.getWorldView()); + dto.setBackgroundStory(sharedGame.getBackgroundStory()); + dto.setSharedAt(sharedGame.getSharedAt()); + + return ResponseEntity.ok(dto); } public ResponseEntity getMySharedGames(Long userId) { @@ -97,16 +110,17 @@ public void deleteSharedGame(Long id, UUID uuid) { sharedGameRepository.delete(game); } - public ResponseEntity getDetailedSharedGame(UUID uuid) { + public ResponseEntity getDetailedSharedGame(Long userId, UUID uuid) { SharedGame game = sharedGameRepository.findByUuid(uuid) .orElseThrow(() -> new CustomException(ErrorCode.E_404_SHARED_GAME_NOT_FOUND)); List tagDtos = gameTagRepository.findTagDtosBySharedGameId(game.getId()); List score = sharedGameScoreRepository.findAllBySharedGameIdOrderByScoreDescCreatedAtDesc(game.getId()); + boolean isLiked = (userId != null) && sharedGameFavoriteRepository.existsByUserIdAndSharedGameId(userId, game.getId()); PublicSharedGameDetailResponse dto = new PublicSharedGameDetailResponse(); - dto.setSharedGameUUID(game.getUuid()); + dto.setSharedGameUuID(game.getUuid()); dto.setPosterUrl(game.getThumbnailUrl()); dto.setTitle(game.getTitle()); dto.setWorldView(game.getWorldView()); @@ -115,6 +129,7 @@ public ResponseEntity getDetailedSharedGame(UUID uuid) { dto.setPlayCount(sharedGameScoreRepository.countBySharedGameId(game.getId())); dto.setLikeCount(sharedGameFavoriteRepository.countBySharedGameId(game.getId())); dto.setSharedAt(game.getSharedAt()); + dto.setLiked(isLiked); List tagarray = new ArrayList<>(); List topscorearray = new ArrayList<>(); @@ -129,6 +144,7 @@ public ResponseEntity getDetailedSharedGame(UUID uuid) { PublicSharedGameDetailResponse.TopScoreDto topscore = new PublicSharedGameDetailResponse.TopScoreDto(); topscore.setNickname(topScoreInfo.getUser().getNickname()); topscore.setScore(topScoreInfo.getScore()); + topscore.setProfileUrl(topScoreInfo.getUser().getProfileImgUrl()); topscore.setCreatedAt(topScoreInfo.getCreatedAt()); topscorearray.add(topscore); } diff --git a/src/main/java/com/scriptopia/demo/service/TagDefService.java b/src/main/java/com/scriptopia/demo/service/TagDefService.java index 4f82034..b004533 100644 --- a/src/main/java/com/scriptopia/demo/service/TagDefService.java +++ b/src/main/java/com/scriptopia/demo/service/TagDefService.java @@ -35,7 +35,7 @@ public ResponseEntity addTagName(TagDefCreateRequest req) { @Transactional public ResponseEntity removeTagName(TagDefDeleteRequest req) { - TagDef tag = tagDefRepository.findByTagName(req.getName()) + TagDef tag = tagDefRepository.findByTagName(req.getTagName()) .orElseThrow(() -> new CustomException(ErrorCode.E_404_Tag_NOT_FOUND)); tagDefRepository.delete(tag); From cd711630686851ffae337269d38a72c2fbcdea38 Mon Sep 17 00:00:00 2001 From: KII1ua Date: Sun, 5 Oct 2025 15:55:15 +0900 Subject: [PATCH 41/64] refactor-gameShared DTO --- .../demo/controller/SharedGameController.java | 12 ++++++------ .../demo/dto/sharedgame/PublicTagDefResponse.java | 2 +- .../SharedGameFavoriteResponse.java | 4 ++++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java index ac78a53..a225102 100644 --- a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -39,11 +39,11 @@ public class SharedGameController { @Operation(summary = "게임 공유하기") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") - @PostMapping - public ResponseEntity share(Authentication authentication, @RequestBody SharedGameRequest req) { + @PostMapping("/{sharedGameUuid}") + public ResponseEntity share(Authentication authentication, @PathVariable("sharedGameUuid") UUID sharedGameUuid) { Long userId = Long.valueOf(authentication.getName()); - return sharedGameService.saveSharedGame(userId, req.getUuid()); + return sharedGameService.saveSharedGame(userId, sharedGameUuid); } /* @@ -102,11 +102,11 @@ public ResponseEntity getSharedGameTags() { */ @Operation(summary = "공유한 게임 삭제") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") - @DeleteMapping - public ResponseEntity delete(Authentication authentication, @RequestBody SharedGameRequest req) { + @DeleteMapping("/{sharedGameUuid}") + public ResponseEntity delete(Authentication authentication, @PathVariable("sharedGameUuid") UUID sharedGameUuid) { Long userId = Long.valueOf(authentication.getName()); - sharedGameService.deleteSharedGame(userId, req.getUuid()); + sharedGameService.deleteSharedGame(userId, sharedGameUuid); return ResponseEntity.ok("게임이 삭제되었습니다."); } diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicTagDefResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicTagDefResponse.java index 1a35d3a..7f5e366 100644 --- a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicTagDefResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicTagDefResponse.java @@ -6,6 +6,6 @@ @Data @AllArgsConstructor public class PublicTagDefResponse { - private Long id; + private Long tagId; private String tagName; } diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgamefavorite/SharedGameFavoriteResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgamefavorite/SharedGameFavoriteResponse.java index e3dca08..093574d 100644 --- a/src/main/java/com/scriptopia/demo/dto/sharedgamefavorite/SharedGameFavoriteResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/sharedgamefavorite/SharedGameFavoriteResponse.java @@ -1,5 +1,6 @@ package com.scriptopia.demo.dto.sharedgamefavorite; +import com.fasterxml.jackson.annotation.JsonProperty; import com.scriptopia.demo.dto.sharedgame.TagDto; import lombok.AllArgsConstructor; import lombok.Builder; @@ -15,7 +16,10 @@ public class SharedGameFavoriteResponse { private String sharedGameUuid; private String thumbnailUrl; + + @JsonProperty("isLiked") private boolean isLiked; + private Long likeCount; private Long totalPlayCount; private String title; From 68e565989eadfb23b85f98a36a5bf6d766a2cba5 Mon Sep 17 00:00:00 2001 From: junseo Lee Date: Sun, 5 Oct 2025 17:14:53 +0900 Subject: [PATCH 42/64] refactor/shared-game --- .../scriptopia/demo/config/JwtAuthFilter.java | 6 ++++- .../demo/config/SecurityConfig.java | 9 ++++++- .../demo/config/SecurityWhitelist.java | 9 +++---- .../demo/controller/SharedGameController.java | 24 ++++--------------- .../sharedgame/PublicSharedGameResponse.java | 2 +- .../exception/GlobalExceptionHandler.java | 14 +++++------ .../demo/service/SharedGameService.java | 6 ++--- src/main/resources/application.yml | 7 +++--- 8 files changed, 34 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java b/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java index 6a09ac6..8d03d84 100644 --- a/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java +++ b/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java @@ -49,7 +49,11 @@ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletExce Arrays.stream(SecurityWhitelist.PUBLIC_GETS) .anyMatch(pattern -> pathMatcher.match(pattern, path)); - return authMatch || publicGetMatch; + boolean publicSharedGameUuidGet = "GET".equalsIgnoreCase(method) && + path.matches("^/shared-games/[0-9a-fA-F\\-]{36}$"); + + return authMatch || publicGetMatch || publicSharedGameUuidGet; + } diff --git a/src/main/java/com/scriptopia/demo/config/SecurityConfig.java b/src/main/java/com/scriptopia/demo/config/SecurityConfig.java index 5f7cff6..873e344 100644 --- a/src/main/java/com/scriptopia/demo/config/SecurityConfig.java +++ b/src/main/java/com/scriptopia/demo/config/SecurityConfig.java @@ -20,8 +20,10 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; import java.util.Arrays; @@ -40,7 +42,11 @@ public PasswordEncoder passwordEncoder() { private final JwtAuthFilter jwtAuthFilter; @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception { + + MvcRequestMatcher publicSharedGameUuidGet = + new MvcRequestMatcher(introspector, "/shared-games/{uuid:[0-9a-fA-F\\-]{36}}"); + publicSharedGameUuidGet.setMethod(HttpMethod.GET); http @@ -52,6 +58,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(SecurityWhitelist.AUTH_WHITELIST).permitAll() //public 권한(GET 요청) .requestMatchers(HttpMethod.GET,SecurityWhitelist.PUBLIC_GETS).permitAll() + .requestMatchers(publicSharedGameUuidGet).permitAll() .anyRequest().authenticated() ) diff --git a/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java b/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java index a1f0140..005ff9e 100644 --- a/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java +++ b/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java @@ -18,15 +18,12 @@ public class SecurityWhitelist { "/v3/api-docs/**", "/swagger-ui/**", - "/shops/pia/items" - - - - + "/shops/pia/items", }; public static final String[] PUBLIC_GETS = { "/trades", - "/shared-games/**" + "/shared-games", + "/shared-games/tags" }; } diff --git a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java index a225102..934664c 100644 --- a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -1,13 +1,10 @@ package com.scriptopia.demo.controller; -import com.scriptopia.demo.domain.SharedGame; -import com.scriptopia.demo.domain.SharedGameFavorite; import com.scriptopia.demo.domain.SharedGameSort; import com.scriptopia.demo.dto.TagDef.TagDefCreateRequest; import com.scriptopia.demo.dto.TagDef.TagDefDeleteRequest; import com.scriptopia.demo.dto.sharedgame.CursorPage; import com.scriptopia.demo.dto.sharedgame.PublicSharedGameResponse; -import com.scriptopia.demo.dto.sharedgame.SharedGameRequest; import com.scriptopia.demo.service.SharedGameFavoriteService; import com.scriptopia.demo.service.SharedGameService; import com.scriptopia.demo.service.TagDefService; @@ -16,9 +13,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.authorization.AuthenticatedAuthorizationManager; import org.springframework.security.core.Authentication; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -63,8 +58,8 @@ public ResponseEntity> getPublicSharedGames 게임공유 : 공유된 게임 상세 조회 */ @Operation(summary = "공유 게임 상세 조회") - @GetMapping("/{sharedGameUuId}") - public ResponseEntity getSharedGameDetail(Authentication authentication, @PathVariable("sharedGameUuId") UUID sharedGameId) { + @GetMapping("/{sharedGameUuid}") + public ResponseEntity getSharedGameDetail(Authentication authentication, @PathVariable("sharedGameUuid") UUID sharedGameUuid) { Long userId = null; if (authentication != null && authentication.isAuthenticated() && authentication.getName() != null) { try { @@ -72,8 +67,9 @@ public ResponseEntity getSharedGameDetail(Authentication authentication, @Pat } catch (NumberFormatException ignored) { } } + System.out.println(userId); - return sharedGameService.getDetailedSharedGame(userId, sharedGameId); + return sharedGameService.getDetailedSharedGame(userId, sharedGameUuid); } /* @@ -111,18 +107,6 @@ public ResponseEntity delete(Authentication authentication, @PathVariable("sh return ResponseEntity.ok("게임이 삭제되었습니다."); } - /* - 게임 공유 -> 공유한 게임 조회(내가 공유한 게임 조회) - */ - @Operation(summary = "(내가)공유한 게임 조회") - @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") - @GetMapping("/me") - public ResponseEntity getMySharedGames(Authentication authentication) { - Long userId = Long.valueOf(authentication.getName()); - - return sharedGameService.getMySharedGames(userId); - } - @Operation(summary = "게임 태그 생성") @PreAuthorize("hasAnyAuthority('ADMIN')") @PostMapping("/tags") diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameResponse.java index c7a70fd..e0a1e1b 100644 --- a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameResponse.java @@ -8,7 +8,7 @@ @Data public class PublicSharedGameResponse { - private UUID sharedGameId; + private UUID sharedGameUuid; private String thumbnailUrl; private boolean isLiked; private Long likeCount; diff --git a/src/main/java/com/scriptopia/demo/exception/GlobalExceptionHandler.java b/src/main/java/com/scriptopia/demo/exception/GlobalExceptionHandler.java index e4d38d1..ed422c1 100644 --- a/src/main/java/com/scriptopia/demo/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/scriptopia/demo/exception/GlobalExceptionHandler.java @@ -63,11 +63,11 @@ public ResponseEntity handleExpired(ExpiredJwtException e) { } -// @ExceptionHandler(Exception.class) -// public ResponseEntity handleGeneralException(Exception ex) { -// -// return ResponseEntity -// .status(HttpStatus.INTERNAL_SERVER_ERROR) -// .body(new ErrorResponse(ErrorCode.E_500)); -// } + @ExceptionHandler(Exception.class) + public ResponseEntity handleGeneralException(Exception ex) { + + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse(ErrorCode.E_500)); + } } diff --git a/src/main/java/com/scriptopia/demo/service/SharedGameService.java b/src/main/java/com/scriptopia/demo/service/SharedGameService.java index 7881abe..3202a15 100644 --- a/src/main/java/com/scriptopia/demo/service/SharedGameService.java +++ b/src/main/java/com/scriptopia/demo/service/SharedGameService.java @@ -6,11 +6,9 @@ import com.scriptopia.demo.exception.ErrorCode; import com.scriptopia.demo.repository.*; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -229,7 +227,7 @@ public ResponseEntity> getPublicSharedGames // 5) DTO 매핑 (집계 일원화) List items = rows.stream().map(g -> { PublicSharedGameResponse dto = new PublicSharedGameResponse(); - dto.setSharedGameId(g.getUuid()); + dto.setSharedGameUuid(g.getUuid()); dto.setThumbnailUrl(g.getThumbnailUrl()); dto.setTitle(g.getTitle()); dto.setSharedAt(g.getSharedAt()); @@ -247,7 +245,7 @@ public ResponseEntity> getPublicSharedGames }).toList(); // 6) 커서/hasNext - UUID nextCursor = items.isEmpty() ? null : items.get(items.size() - 1).getSharedGameId(); + UUID nextCursor = items.isEmpty() ? null : items.get(items.size() - 1).getSharedGameUuid(); boolean hasNext = rows.size() == Math.max(1, size); return ResponseEntity.ok(new CursorPage<>(items, nextCursor, hasNext)); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0f1419c..b9a312a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -57,8 +57,6 @@ oauth: redirect-uri: ${NAVER_REDIRECT_URI} scope: name email profile_image - - auth: jwt: issuer: scriptopia @@ -81,4 +79,7 @@ springdoc: path: /swagger-ui image-dir: ./uploads/ -image-url-prefix: /images \ No newline at end of file +image-url-prefix: /images +logging: + level: + org.springframework.security: DEBUG \ No newline at end of file From 8d06d3d2ccbab0be93410f16910e4f4668f53159 Mon Sep 17 00:00:00 2001 From: KII1ua Date: Sun, 5 Oct 2025 17:20:39 +0900 Subject: [PATCH 43/64] refactor-gameshared edit --- .../demo/controller/SharedGameController.java | 4 +- .../PublicSharedGameDetailResponse.java | 3 ++ .../sharedgame/PublicSharedGameResponse.java | 6 +-- .../demo/service/SharedGameService.java | 42 +------------------ 4 files changed, 7 insertions(+), 48 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java index 934664c..dd86748 100644 --- a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -77,8 +77,8 @@ public ResponseEntity getSharedGameDetail(Authentication authentication, @Pat */ @Operation(summary = "공유 게임 Like 요청") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") - @PostMapping("{sharedGameUuId}/like") - public ResponseEntity likeSharedGame(@PathVariable("sharedGameUuId") UUID sharedGameId, Authentication authentication) { + @PostMapping("{sharedGameUuid}/like") + public ResponseEntity likeSharedGame(@PathVariable("sharedGameUuid") UUID sharedGameId, Authentication authentication) { Long userId = Long.valueOf(authentication.getName()); return sharedGameFavoriteService.saveFavorite(userId, sharedGameId); diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java index d84bdaf..32dccbc 100644 --- a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java @@ -1,6 +1,7 @@ package com.scriptopia.demo.dto.sharedgame; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.time.LocalDateTime; @@ -17,6 +18,8 @@ public class PublicSharedGameDetailResponse { private String creator; private Long playCount; private Long likeCount; + + @JsonProperty("isLiked") private boolean isLiked; @JsonFormat(shape = JsonFormat.Shape.STRING) diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameResponse.java index e0a1e1b..c23a9f0 100644 --- a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameResponse.java @@ -10,12 +10,8 @@ public class PublicSharedGameResponse { private UUID sharedGameUuid; private String thumbnailUrl; - private boolean isLiked; - private Long likeCount; - private Long totalPlayCount; private String title; - private Long topScore; - private LocalDateTime sharedAt; + private Long playCount; private List tags; diff --git a/src/main/java/com/scriptopia/demo/service/SharedGameService.java b/src/main/java/com/scriptopia/demo/service/SharedGameService.java index 3202a15..bc2a71f 100644 --- a/src/main/java/com/scriptopia/demo/service/SharedGameService.java +++ b/src/main/java/com/scriptopia/demo/service/SharedGameService.java @@ -58,41 +58,6 @@ public ResponseEntity saveSharedGame(Long Id, UUID uuid) { return ResponseEntity.ok(dto); } - public ResponseEntity getMySharedGames(Long userId) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); - - List games = sharedGameRepository.findAllByUserid(user.getId()); - - List dtos = new ArrayList<>(); - - for(SharedGame game : games) { - MySharedGameResponse dto = new MySharedGameResponse(); - dto.setShared_game_uuid(game.getUuid()); - dto.setThumbnailUrl(game.getThumbnailUrl()); - dto.setTotalPlayed(sharedGameScoreRepository.countBySharedGameId(game.getId())); - dto.setTitle(game.getTitle()); - dto.setWorldView(game.getWorldView()); - dto.setSharedAt(game.getSharedAt()); - dto.setBackgroundStory(game.getBackgroundStory()); - - boolean liked = sharedGameFavoriteRepository.existsLikeSharedGame(user.getId(), game.getId()); - dto.setRecommand(liked); - - List tagdto = gameTagRepository.findTagNamesBySharedGameId(game.getId()); - List tags = new ArrayList<>(); - - for(String tagName : tagdto) { - tags.add(new MySharedGameResponse.TagDto(tagName)); - } - - dto.setTags(tags); - dtos.add(dto); - } - - return ResponseEntity.ok(dtos); - } - @Transactional public void deleteSharedGame(Long id, UUID uuid) { User user = userRepository.findById(id) @@ -230,14 +195,9 @@ public ResponseEntity> getPublicSharedGames dto.setSharedGameUuid(g.getUuid()); dto.setThumbnailUrl(g.getThumbnailUrl()); dto.setTitle(g.getTitle()); - dto.setSharedAt(g.getSharedAt()); // 집계 - dto.setTotalPlayCount(sharedGameScoreRepository.countBySharedGameId(g.getId())); - dto.setLikeCount(sharedGameFavoriteRepository.countBySharedGameId(g.getId())); - - Long topScore = sharedGameScoreRepository.maxScoreBySharedGameId(g.getId()); - dto.setTopScore(topScore == null ? 0L : topScore); + dto.setPlayCount(sharedGameScoreRepository.countBySharedGameId(g.getId())); // 태그 dto.setTags(gameTagRepository.findTagDtosBySharedGameId(g.getId())); From 7318c879643e714f68d3f4b8413c8282a4d03b9a Mon Sep 17 00:00:00 2001 From: KII1ua Date: Sun, 5 Oct 2025 17:49:23 +0900 Subject: [PATCH 44/64] refactor-shared-game edit --- .../com/scriptopia/demo/controller/SharedGameController.java | 4 ++-- .../demo/dto/sharedgame/PublicSharedGameDetailResponse.java | 2 +- .../java/com/scriptopia/demo/service/SharedGameService.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java index dd86748..80207f1 100644 --- a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -48,10 +48,10 @@ public ResponseEntity share(Authentication authentication, @PathVariable("sha @GetMapping public ResponseEntity> getPublicSharedGames(@RequestParam(value = "lastUUID", required = false) UUID lastUUID, @RequestParam(value = "size", defaultValue = "20") int size, - @RequestParam(value = "tagIds", required = false) List tagIds, + @RequestParam(value = "tags", required = false) List tags, @RequestParam(value = "query", required = false) String query, @RequestParam(value = "sort", defaultValue = "POPULAR") SharedGameSort sort) { - return sharedGameService.getPublicSharedGames(lastUUID, size, tagIds, query, sort); + return sharedGameService.getPublicSharedGames(lastUUID, size, tags, query, sort); } /* diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java index 32dccbc..c646a97 100644 --- a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java @@ -10,7 +10,7 @@ @Data public class PublicSharedGameDetailResponse { - private UUID sharedGameUuID; + private UUID sharedGameUuid; private String posterUrl; private String title; private String worldView; diff --git a/src/main/java/com/scriptopia/demo/service/SharedGameService.java b/src/main/java/com/scriptopia/demo/service/SharedGameService.java index bc2a71f..8ac0f3f 100644 --- a/src/main/java/com/scriptopia/demo/service/SharedGameService.java +++ b/src/main/java/com/scriptopia/demo/service/SharedGameService.java @@ -83,7 +83,7 @@ public ResponseEntity getDetailedSharedGame(Long userId, UUID uuid) { boolean isLiked = (userId != null) && sharedGameFavoriteRepository.existsByUserIdAndSharedGameId(userId, game.getId()); PublicSharedGameDetailResponse dto = new PublicSharedGameDetailResponse(); - dto.setSharedGameUuID(game.getUuid()); + dto.setSharedGameUuid(game.getUuid()); dto.setPosterUrl(game.getThumbnailUrl()); dto.setTitle(game.getTitle()); dto.setWorldView(game.getWorldView()); From 923958a3c329b3cd35df1fc7935a9f78b0a88ee0 Mon Sep 17 00:00:00 2001 From: KII1ua Date: Sun, 5 Oct 2025 23:54:21 +0900 Subject: [PATCH 45/64] refactor-shared-game-edit --- .../com/scriptopia/demo/controller/SharedGameController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java index 80207f1..03e1afe 100644 --- a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -46,12 +46,12 @@ public ResponseEntity share(Authentication authentication, @PathVariable("sha */ @Operation(summary = "공유 게임 목록 조회") @GetMapping - public ResponseEntity> getPublicSharedGames(@RequestParam(value = "lastUUID", required = false) UUID lastUUID, + public ResponseEntity> getPublicSharedGames(@RequestParam(value = "lastUuID", required = false) UUID lastUuID, @RequestParam(value = "size", defaultValue = "20") int size, @RequestParam(value = "tags", required = false) List tags, @RequestParam(value = "query", required = false) String query, @RequestParam(value = "sort", defaultValue = "POPULAR") SharedGameSort sort) { - return sharedGameService.getPublicSharedGames(lastUUID, size, tags, query, sort); + return sharedGameService.getPublicSharedGames(lastUuID, size, tags, query, sort); } /* From 8c70c01bc5078666b328d9858c779afd8ba92d06 Mon Sep 17 00:00:00 2001 From: junseo Lee Date: Sun, 5 Oct 2025 23:57:53 +0900 Subject: [PATCH 46/64] feat: create CommonResponse and apply --- .../demo/controller/AuthController.java | 33 ++++++++++--------- .../scriptopia/demo/dto/CommonResponse.java | 12 +++++++ 2 files changed, 29 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/scriptopia/demo/dto/CommonResponse.java diff --git a/src/main/java/com/scriptopia/demo/controller/AuthController.java b/src/main/java/com/scriptopia/demo/controller/AuthController.java index 8aaf83e..1902fc5 100644 --- a/src/main/java/com/scriptopia/demo/controller/AuthController.java +++ b/src/main/java/com/scriptopia/demo/controller/AuthController.java @@ -1,5 +1,6 @@ package com.scriptopia.demo.controller; +import com.scriptopia.demo.dto.CommonResponse; import com.scriptopia.demo.dto.auth.*; import com.scriptopia.demo.service.LocalAccountService; import com.scriptopia.demo.service.RefreshTokenService; @@ -29,7 +30,7 @@ public class AuthController { @Operation(summary = "로그아웃") @PostMapping("/logout") - public ResponseEntity logout( + public ResponseEntity logout( @CookieValue(name = RT_COOKIE, required = false) String refreshToken, HttpServletResponse response ) { @@ -37,7 +38,7 @@ public ResponseEntity logout( refreshTokenService.logout(refreshToken); } response.addHeader(HttpHeaders.SET_COOKIE, localAccountService.removeRefreshCookie().toString()); - return ResponseEntity.ok("로그아웃 되었습니다."); + return ResponseEntity.ok(new CommonResponse("로그아웃 되었습니다.")); } @Operation(summary = "로컬 로그인") @@ -53,64 +54,64 @@ public ResponseEntity login( @Operation(summary = "로컬 계정 회원가입") @PostMapping("/register") - public ResponseEntity register( + public ResponseEntity register( @RequestBody @Valid RegisterRequest request ) { localAccountService.register(request); - return ResponseEntity.ok("회원가입에 성공했습니다."); + return ResponseEntity.ok(new CommonResponse("회원가입에 성공했습니다.")); } @Operation(summary = "이메일 중복 검증") @PostMapping("/email/verify") - public ResponseEntity verifyEmail(@Valid @RequestBody VerifyEmailRequest request) { + public ResponseEntity verifyEmail(@Valid @RequestBody VerifyEmailRequest request) { localAccountService.verifyEmail(request); - return ResponseEntity.ok("사용 가능한 이메일입니다."); + return ResponseEntity.ok(new CommonResponse("사용 가능한 이메일입니다.")); } @Operation(summary = "이메일 인증 코드 전송") @PostMapping("/email/code/send") - public ResponseEntity sendCode(@RequestBody @Valid SendCodeRequest request) { + public ResponseEntity sendCode(@RequestBody @Valid SendCodeRequest request) { localAccountService.sendVerificationCode(request.getEmail()); - return ResponseEntity.ok("인증 코드가 이메일로 발송되었습니다."); + return ResponseEntity.ok(new CommonResponse("인증 코드가 이메일로 발송되었습니다.")); } @Operation(summary = "이메일 인증 코드 확인") @PostMapping("/email/code/verify") - public ResponseEntity verifyCode(@RequestBody @Valid VerifyCodeRequest request) { + public ResponseEntity verifyCode(@RequestBody @Valid VerifyCodeRequest request) { localAccountService.verifyCode(request.getEmail(), request.getCode()); - return ResponseEntity.ok("이메일 인증이 완료되었습니다."); + return ResponseEntity.ok(new CommonResponse("이메일 인증이 완료되었습니다.")); } @Operation(summary = "비밀번호 초기화 링크 발송") @PostMapping("/password/reset/send") - public ResponseEntity sendResetMail(@Valid @RequestBody SendCodeRequest request){ + public ResponseEntity sendResetMail(@Valid @RequestBody SendCodeRequest request){ localAccountService.sendResetPasswordMail(request.getEmail()); - return ResponseEntity.ok("비밀번호 초기화 링크를 전송했습니다."); + return ResponseEntity.ok(new CommonResponse("비밀번호 초기화 링크를 전송했습니다.")); } @Operation(summary = "비밀번호 초기화") @PatchMapping("/password/reset") - public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { + public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { localAccountService.resetPassword(request.getToken(), request.getNewPassword()); - return ResponseEntity.ok("비밀번호가 성공적으로 변경되었습니다."); + return ResponseEntity.ok(new CommonResponse("비밀번호가 성공적으로 변경되었습니다.")); } @Operation(summary = "비밀번호 재설정") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PatchMapping("/password/change") - public ResponseEntity changePassword(@RequestBody @Valid ChangePasswordRequest request, + public ResponseEntity changePassword(@RequestBody @Valid ChangePasswordRequest request, Authentication authentication) { Long userId = Long.valueOf(authentication.getName()); localAccountService.changePassword(userId,request); - return ResponseEntity.ok("비밀번호가 성공적으로 변경되었습니다."); + return ResponseEntity.ok(new CommonResponse("비밀번호가 성공적으로 변경되었습니다.")); } diff --git a/src/main/java/com/scriptopia/demo/dto/CommonResponse.java b/src/main/java/com/scriptopia/demo/dto/CommonResponse.java new file mode 100644 index 0000000..5d10f83 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/CommonResponse.java @@ -0,0 +1,12 @@ +package com.scriptopia.demo.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CommonResponse { + String message; +} From a9114ee4d721d8fcc88d1ecfce11dab7b1fffe82 Mon Sep 17 00:00:00 2001 From: KII1ua Date: Wed, 8 Oct 2025 13:58:57 +0900 Subject: [PATCH 47/64] variable change --- .../com/scriptopia/demo/controller/SharedGameController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java index 03e1afe..d589732 100644 --- a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -46,12 +46,12 @@ public ResponseEntity share(Authentication authentication, @PathVariable("sha */ @Operation(summary = "공유 게임 목록 조회") @GetMapping - public ResponseEntity> getPublicSharedGames(@RequestParam(value = "lastUuID", required = false) UUID lastUuID, + public ResponseEntity> getPublicSharedGames(@RequestParam(value = "lastUuId", required = false) UUID lastUuId, @RequestParam(value = "size", defaultValue = "20") int size, @RequestParam(value = "tags", required = false) List tags, @RequestParam(value = "query", required = false) String query, @RequestParam(value = "sort", defaultValue = "POPULAR") SharedGameSort sort) { - return sharedGameService.getPublicSharedGames(lastUuID, size, tags, query, sort); + return sharedGameService.getPublicSharedGames(lastUuId, size, tags, query, sort); } /* From d2a822915d3bf2314768012dd94f770f6d756429 Mon Sep 17 00:00:00 2001 From: junseo Lee Date: Thu, 9 Oct 2025 03:39:51 +0900 Subject: [PATCH 48/64] feat: refactor shared-game-controller --- .../com/scriptopia/demo/controller/SharedGameController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java index d589732..e883d7a 100644 --- a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -46,12 +46,12 @@ public ResponseEntity share(Authentication authentication, @PathVariable("sha */ @Operation(summary = "공유 게임 목록 조회") @GetMapping - public ResponseEntity> getPublicSharedGames(@RequestParam(value = "lastUuId", required = false) UUID lastUuId, + public ResponseEntity> getPublicSharedGames(@RequestParam(value = "lastUuid", required = false) UUID lastUuid, @RequestParam(value = "size", defaultValue = "20") int size, @RequestParam(value = "tags", required = false) List tags, @RequestParam(value = "query", required = false) String query, @RequestParam(value = "sort", defaultValue = "POPULAR") SharedGameSort sort) { - return sharedGameService.getPublicSharedGames(lastUuId, size, tags, query, sort); + return sharedGameService.getPublicSharedGames(lastUuid, size, tags, query, sort); } /* From 0c0431789145c0157a6786288436874c655d561d Mon Sep 17 00:00:00 2001 From: junseo Lee Date: Thu, 9 Oct 2025 12:23:12 +0900 Subject: [PATCH 49/64] feat: solving cors error --- src/main/java/com/scriptopia/demo/config/SecurityConfig.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/scriptopia/demo/config/SecurityConfig.java b/src/main/java/com/scriptopia/demo/config/SecurityConfig.java index 873e344..d1326ab 100644 --- a/src/main/java/com/scriptopia/demo/config/SecurityConfig.java +++ b/src/main/java/com/scriptopia/demo/config/SecurityConfig.java @@ -26,6 +26,7 @@ import org.springframework.web.servlet.handler.HandlerMappingIntrospector; import java.util.Arrays; +import java.util.Collections; @Slf4j @Configuration @@ -86,7 +87,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMapping public UrlBasedCorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); - config.setAllowedOrigins(Arrays.asList("http://localhost:3000")); +// config.setAllowedOrigins(Arrays.asList("http://localhost:3000")); + config.setAllowedOriginPatterns(Collections.singletonList("*")); config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(Arrays.asList("*")); // Authorization, Content-Type 등 허용 config.setExposedHeaders(Arrays.asList("Authorization")); // 필요시 노출할 헤더 From c6d7ef691f03b76dd968997a70c8839e6adc388f Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Thu, 9 Oct 2025 23:43:37 +0900 Subject: [PATCH 50/64] refactor: if player come to bigEvent setting title value --- .../java/com/scriptopia/demo/service/GameSessionService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/scriptopia/demo/service/GameSessionService.java b/src/main/java/com/scriptopia/demo/service/GameSessionService.java index d7f9574..33b1e8f 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -527,12 +527,15 @@ public GameSessionMongo gameToChoice(Long userId) { fastApiRequest.setCurrentChoice(null); switch (currentEventStage) { case 2: + gameSessionMongo.getHistoryInfo().setEpilogue1Title("Come Stage"); fastApiRequest.setCurrentStory(gameSessionMongo.getHistoryInfo().getEpilogue1Content()); break; case 4: + gameSessionMongo.getHistoryInfo().setEpilogue2Title("Come Stage"); fastApiRequest.setCurrentStory(gameSessionMongo.getHistoryInfo().getEpilogue2Content()); break; case 6: + gameSessionMongo.getHistoryInfo().setEpilogue3Title("Come Stage"); fastApiRequest.setCurrentStory(gameSessionMongo.getHistoryInfo().getEpilogue3Content()); break; } From 7591d9bb36c294fc0bbd7c22dd405a9f8575a20a Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Fri, 10 Oct 2025 01:14:46 +0900 Subject: [PATCH 51/64] feat: create fastAPI reqeust and response to game/title --- .../demo/dto/gamesession/GameTitleRequest.java | 15 +++++++++++++++ .../demo/dto/gamesession/GameTitleResponse.java | 14 ++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleRequest.java create mode 100644 src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleResponse.java diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleRequest.java b/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleRequest.java new file mode 100644 index 0000000..65c07a3 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleRequest.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.Data; +import lombok.Builder; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GameTitleRequest { + private String content; +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleResponse.java new file mode 100644 index 0000000..d809559 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleResponse.java @@ -0,0 +1,14 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GameTitleResponse { + private String title; +} From e12e31725f7d33b79fd421ddd7177e7f39a715e7 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Fri, 10 Oct 2025 01:15:10 +0900 Subject: [PATCH 52/64] refactor: add fastAPI endPoint --- .../com/scriptopia/demo/config/fastapi/FastApiEndpoint.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/scriptopia/demo/config/fastapi/FastApiEndpoint.java b/src/main/java/com/scriptopia/demo/config/fastapi/FastApiEndpoint.java index 08547f8..8a2c84a 100644 --- a/src/main/java/com/scriptopia/demo/config/fastapi/FastApiEndpoint.java +++ b/src/main/java/com/scriptopia/demo/config/fastapi/FastApiEndpoint.java @@ -6,7 +6,8 @@ public enum FastApiEndpoint { BATTLE("/games/battle"), ITEM("/games/item"), DONE("/games/done"), - END("/games/end"); + END("/games/end"), + TITLE("/games/title"); private final String path; From 477e705467477985043e7ccac380145cce26a3f8 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Fri, 10 Oct 2025 01:17:21 +0900 Subject: [PATCH 53/64] refactor: change dto --- .../com/scriptopia/demo/dto/gamesession/GameTitleRequest.java | 3 ++- .../com/scriptopia/demo/dto/gamesession/GameTitleResponse.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleRequest.java b/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleRequest.java index 65c07a3..920c935 100644 --- a/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleRequest.java +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleRequest.java @@ -4,6 +4,7 @@ import lombok.Builder; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; +import java.util.List; @Data @@ -11,5 +12,5 @@ @AllArgsConstructor @NoArgsConstructor public class GameTitleRequest { - private String content; + private List contents; } diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleResponse.java index d809559..88397e4 100644 --- a/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleResponse.java @@ -4,11 +4,12 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; @Data @Builder @AllArgsConstructor @NoArgsConstructor public class GameTitleResponse { - private String title; + private List titles; } From e45f8f49fd4fec0149352bada591b319a88531ef Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Fri, 10 Oct 2025 01:30:57 +0900 Subject: [PATCH 54/64] refactor: add fastAPI to getTitles --- .../demo/service/FastApiService.java | 13 +++++++++++ .../demo/service/GameSessionService.java | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/main/java/com/scriptopia/demo/service/FastApiService.java b/src/main/java/com/scriptopia/demo/service/FastApiService.java index 9f39a10..a88490b 100644 --- a/src/main/java/com/scriptopia/demo/service/FastApiService.java +++ b/src/main/java/com/scriptopia/demo/service/FastApiService.java @@ -8,6 +8,8 @@ import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; +import java.util.List; + @Service @RequiredArgsConstructor public class FastApiService { @@ -75,4 +77,15 @@ public GameEndResponse end(GameEndRequest request) { .block(); } + // 게임 종료 시 빅 이벤트 타이틀 처리 + public GameTitleResponse title(GameTitleRequest request) { + return fastApiWebClient.post() + .uri(FastApiEndpoint.TITLE.getPath()) + .bodyValue(request) + .retrieve() + .bodyToMono(GameTitleResponse.class) + .block(); + } + + } diff --git a/src/main/java/com/scriptopia/demo/service/GameSessionService.java b/src/main/java/com/scriptopia/demo/service/GameSessionService.java index 33b1e8f..b8f142b 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -1287,6 +1287,29 @@ public ResponseEntity gameToEnd(Long userId) { HistoryInfoMongo historyInfoMongo = gameSessionMongo.getHistoryInfo(); + GameTitleRequest fastApiRequest = new GameTitleRequest(); + if(historyInfoMongo.getBackgroundStory() != null){ + fastApiRequest.getContents().add(historyInfoMongo.getBackgroundStory()); + } + if(historyInfoMongo.getEpilogue1Title() != null && historyInfoMongo.getEpilogue1Content() != null){ + fastApiRequest.getContents().add(historyInfoMongo.getEpilogue1Content()); + } + if(historyInfoMongo.getEpilogue2Title() != null && historyInfoMongo.getEpilogue2Content() != null){ + fastApiRequest.getContents().add(historyInfoMongo.getEpilogue2Content()); + } + if(historyInfoMongo.getEpilogue3Title() != null && historyInfoMongo.getEpilogue3Content() != null){ + fastApiRequest.getContents().add(historyInfoMongo.getEpilogue3Content()); + } + + GameTitleResponse fastApiResponse = fastApiService.title(fastApiRequest); + + List titles = fastApiResponse.getTitles(); + if (titles != null && !titles.isEmpty()) { + if (titles.size() > 0) historyInfoMongo.setTitle(titles.get(0)); + if (titles.size() > 1) historyInfoMongo.setEpilogue1Title(titles.get(1)); + if (titles.size() > 2) historyInfoMongo.setEpilogue2Title(titles.get(2)); + if (titles.size() > 3) historyInfoMongo.setEpilogue3Title(titles.get(3)); + } HistoryRequest historyRequest = HistoryRequest.builder() .thumbnailUrl(null) // 필요 시 From 025fa8b5904676e21a70ccc6cdfa510accc68351 Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Fri, 10 Oct 2025 02:28:42 +0900 Subject: [PATCH 55/64] complete test --- .../demo/dto/gamesession/GameTitleRequest.java | 4 +++- .../demo/service/GameSessionService.java | 16 +++++++++------- src/main/resources/templates/index.html | 8 ++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleRequest.java b/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleRequest.java index 920c935..2bbdbfc 100644 --- a/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleRequest.java +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleRequest.java @@ -4,6 +4,8 @@ import lombok.Builder; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; + +import java.util.ArrayList; import java.util.List; @@ -12,5 +14,5 @@ @AllArgsConstructor @NoArgsConstructor public class GameTitleRequest { - private List contents; + private List contents = new ArrayList<>(); } diff --git a/src/main/java/com/scriptopia/demo/service/GameSessionService.java b/src/main/java/com/scriptopia/demo/service/GameSessionService.java index b8f142b..24dddec 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -27,6 +27,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; @Service @@ -1118,7 +1119,7 @@ private void addStats(PlayerInfoMongo player, ItemDefMongo item) { player.setIntelligence(player.getIntelligence() + safeStat(item.getIntelligence())); player.setLuck(player.getLuck() + safeStat(item.getLuck())); if (item.getCategory() == ItemType.ARMOR){ - player.setLife( item.getBaseStat() ); + player.setHealthPoint( item.getBaseStat() ); } } @@ -1288,6 +1289,7 @@ public ResponseEntity gameToEnd(Long userId) { HistoryInfoMongo historyInfoMongo = gameSessionMongo.getHistoryInfo(); GameTitleRequest fastApiRequest = new GameTitleRequest(); + if(historyInfoMongo.getBackgroundStory() != null){ fastApiRequest.getContents().add(historyInfoMongo.getBackgroundStory()); } @@ -1301,15 +1303,15 @@ public ResponseEntity gameToEnd(Long userId) { fastApiRequest.getContents().add(historyInfoMongo.getEpilogue3Content()); } + + GameTitleResponse fastApiResponse = fastApiService.title(fastApiRequest); List titles = fastApiResponse.getTitles(); - if (titles != null && !titles.isEmpty()) { - if (titles.size() > 0) historyInfoMongo.setTitle(titles.get(0)); - if (titles.size() > 1) historyInfoMongo.setEpilogue1Title(titles.get(1)); - if (titles.size() > 2) historyInfoMongo.setEpilogue2Title(titles.get(2)); - if (titles.size() > 3) historyInfoMongo.setEpilogue3Title(titles.get(3)); - } + if (titles.size() > 0) historyInfoMongo.setTitle(titles.get(0)); + if (titles.size() > 1) historyInfoMongo.setEpilogue1Title(titles.get(1)); + if (titles.size() > 2) historyInfoMongo.setEpilogue2Title(titles.get(2)); + if (titles.size() > 3) historyInfoMongo.setEpilogue3Title(titles.get(3)); HistoryRequest historyRequest = HistoryRequest.builder() .thumbnailUrl(null) // 필요 시 diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index cdf14e0..de23551 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -350,14 +350,14 @@

소유 아이템

}; // 게임 히스토리 API 호출 - const loadGameHistory = async (gameId) => { + const loadGameHistory = async () => { if (!accessToken) { alert('로그인 필요'); return; } try { - const history = await safeFetch(null, `/api/v1/games/${gameId}/history`, { + const history = await safeFetch(null, `/api/v1/games/sss/history`, { method: 'POST', headers: { 'Authorization': 'Bearer ' + accessToken } }); @@ -619,7 +619,7 @@

소유 아이템

gameOverBtn.className = 'btn'; gameOverBtn.textContent = '게임 패배 - 결과 보기'; gameOverBtn.addEventListener('click', async () => { - const history = await loadGameHistory(gameId); + const history = await loadGameHistory(); showHistoryModal(history); // ✅ 모달로 표시 }); choiceContainer.appendChild(gameOverBtn); @@ -632,7 +632,7 @@

소유 아이템

gameClearBtn.className = 'btn'; gameClearBtn.textContent = '게임 승리 - 결과 보기'; gameClearBtn.addEventListener('click', async () => { - const history = await loadGameHistory(gameId); + const history = await loadGameHistory(); showHistoryModal(history); // ✅ 모달로 표시 }); choiceContainer.appendChild(gameClearBtn); From 4bd6196451ecef60d7a31b58ba7f6adf7150fa71 Mon Sep 17 00:00:00 2001 From: junseo Lee Date: Sat, 1 Nov 2025 22:34:55 +0900 Subject: [PATCH 56/64] feat: implement getUserStatus API --- .../demo/controller/UserController.java | 15 +++++++++++---- .../demo/dto/users/UserStatusResponse.java | 18 ++++++++++++++++++ .../scriptopia/demo/service/UserService.java | 14 ++++++++++++++ src/main/resources/application.yml | 2 +- 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/scriptopia/demo/dto/users/UserStatusResponse.java diff --git a/src/main/java/com/scriptopia/demo/controller/UserController.java b/src/main/java/com/scriptopia/demo/controller/UserController.java index 471fab1..7489fc0 100644 --- a/src/main/java/com/scriptopia/demo/controller/UserController.java +++ b/src/main/java/com/scriptopia/demo/controller/UserController.java @@ -1,12 +1,10 @@ package com.scriptopia.demo.controller; +import com.scriptopia.demo.domain.UserStatus; import com.scriptopia.demo.dto.history.HistoryPageResponse; import com.scriptopia.demo.dto.history.HistoryPageResponseDto; import com.scriptopia.demo.dto.items.ItemDTO; -import com.scriptopia.demo.dto.users.PiaItemDTO; -import com.scriptopia.demo.dto.users.UserAssetsResponse; -import com.scriptopia.demo.dto.users.UserImageRequest; -import com.scriptopia.demo.dto.users.UserSettingsDTO; +import com.scriptopia.demo.dto.users.*; import com.scriptopia.demo.service.UserCharacterImgService; import com.scriptopia.demo.service.UserService; import io.swagger.v3.oas.annotations.Operation; @@ -114,4 +112,13 @@ public ResponseEntity getUserCharacterImgs(Authentication authentication) { return userCharacterImgService.getUserCharacterImg(userId); } + @Operation(summary = "사용자 헤더 정보 조회") + @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") + @GetMapping("/status") + public ResponseEntity getUserStatus(Authentication authentication) { + Long userId = Long.valueOf(authentication.getName()); + + return ResponseEntity.ok(userService.getUserStatus(userId)); + } + } diff --git a/src/main/java/com/scriptopia/demo/dto/users/UserStatusResponse.java b/src/main/java/com/scriptopia/demo/dto/users/UserStatusResponse.java new file mode 100644 index 0000000..0d0a54b --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/users/UserStatusResponse.java @@ -0,0 +1,18 @@ +package com.scriptopia.demo.dto.users; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Builder +@Data +@AllArgsConstructor +@NoArgsConstructor +public class UserStatusResponse { + private String nickname; + private String profileImage; + private Integer ticket; + +} diff --git a/src/main/java/com/scriptopia/demo/service/UserService.java b/src/main/java/com/scriptopia/demo/service/UserService.java index 2667cfe..b78532c 100644 --- a/src/main/java/com/scriptopia/demo/service/UserService.java +++ b/src/main/java/com/scriptopia/demo/service/UserService.java @@ -11,6 +11,7 @@ import com.scriptopia.demo.dto.users.PiaItemDTO; import com.scriptopia.demo.dto.users.UserAssetsResponse; import com.scriptopia.demo.dto.users.UserSettingsDTO; +import com.scriptopia.demo.dto.users.UserStatusResponse; import com.scriptopia.demo.exception.CustomException; import com.scriptopia.demo.exception.ErrorCode; import com.scriptopia.demo.mapper.ItemMapper; @@ -135,4 +136,17 @@ public HistoryPageResponseDto fetchMyHistory(Long userId, UUID lastId, int size) return new HistoryPageResponseDto(result, nextCursor, hasNext); } + + @Transactional + public UserStatusResponse getUserStatus(Long userId){ + User user = userRepository.findById(userId).orElseThrow( + () -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND) + ); + + return UserStatusResponse.builder() + .nickname(user.getNickname()) + .profileImage(user.getProfileImgUrl()) + .ticket(0) + .build(); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b9a312a..ad48b02 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,7 +12,7 @@ spring: driver-class-name: org.postgresql.Driver jpa: hibernate: - ddl-auto: update + ddl-auto: create properties: hibernate: show_sql: true From 9cd93789dbe3f7b6b923d208824db6ca4a5c40d1 Mon Sep 17 00:00:00 2001 From: junseo Lee Date: Sat, 1 Nov 2025 23:09:27 +0900 Subject: [PATCH 57/64] feat: update register API --- .../demo/controller/AuthController.java | 12 +++-- .../demo/dto/auth/RegisterRequest.java | 5 ++ .../demo/service/LocalAccountService.java | 51 +++++++++++-------- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/controller/AuthController.java b/src/main/java/com/scriptopia/demo/controller/AuthController.java index 1902fc5..2053d3b 100644 --- a/src/main/java/com/scriptopia/demo/controller/AuthController.java +++ b/src/main/java/com/scriptopia/demo/controller/AuthController.java @@ -11,6 +11,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; @@ -54,11 +55,14 @@ public ResponseEntity login( @Operation(summary = "로컬 계정 회원가입") @PostMapping("/register") - public ResponseEntity register( - @RequestBody @Valid RegisterRequest request + public ResponseEntity register( + + @RequestBody @Valid RegisterRequest req, + HttpServletRequest request, + HttpServletResponse response ) { - localAccountService.register(request); - return ResponseEntity.ok(new CommonResponse("회원가입에 성공했습니다.")); + + return ResponseEntity.status(HttpStatus.CREATED).body(localAccountService.register(req, request, response)); } @Operation(summary = "이메일 중복 검증") diff --git a/src/main/java/com/scriptopia/demo/dto/auth/RegisterRequest.java b/src/main/java/com/scriptopia/demo/dto/auth/RegisterRequest.java index 5e89691..259ae20 100644 --- a/src/main/java/com/scriptopia/demo/dto/auth/RegisterRequest.java +++ b/src/main/java/com/scriptopia/demo/dto/auth/RegisterRequest.java @@ -1,5 +1,6 @@ package com.scriptopia.demo.dto.auth; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; @@ -26,4 +27,8 @@ public class RegisterRequest { @NotBlank(message = "E_400_MISSING_NICKNAME") private String nickname; + + @NotBlank(message = "디바이스 식별값이 필요합니다.") + @Schema(description = "디바이스 아이디", example = "1234") + private String deviceId; } diff --git a/src/main/java/com/scriptopia/demo/service/LocalAccountService.java b/src/main/java/com/scriptopia/demo/service/LocalAccountService.java index fcf95d9..ce7a9a6 100644 --- a/src/main/java/com/scriptopia/demo/service/LocalAccountService.java +++ b/src/main/java/com/scriptopia/demo/service/LocalAccountService.java @@ -12,6 +12,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; @@ -28,6 +29,7 @@ import static org.thymeleaf.util.StringUtils.length; +@Slf4j @Service @Transactional @RequiredArgsConstructor @@ -124,8 +126,8 @@ public void verifyCode(String email, String inputCode) { @Transactional - public void register(RegisterRequest request) { - String email = request.getEmail(); + public LoginResponse register(RegisterRequest registerRequest, HttpServletRequest request, HttpServletResponse response) { + String email = registerRequest.getEmail(); //중복 검증 if (localAccountRepository.existsByEmail(email)){ @@ -140,28 +142,28 @@ public void register(RegisterRequest request) { } // 공백 검증 - if (WS.matcher(request.getPassword()).find()) { + if (WS.matcher(registerRequest.getPassword()).find()) { throw new CustomException(ErrorCode.E_400_PASSWORD_WHITESPACE); } - isAvailable(email, request.getNickname()); + isAvailable(email, registerRequest.getNickname()); //user 객체 생성 User user = new User(); - user.setNickname(request.getNickname()); + user.setNickname(registerRequest.getNickname()); user.setPia(0L); user.setCreatedAt(LocalDateTime.now()); - user.setLastLoginAt(null); + user.setLastLoginAt(LocalDateTime.now()); user.setProfileImgUrl(null); user.setRole(Role.USER); user.setLoginType(LoginType.LOCAL); - userRepository.save(user); + User savedUser = userRepository.save(user); //localAccount 객체 생성 LocalAccount localAccount = new LocalAccount(); localAccount.setUser(user); localAccount.setEmail(email); - localAccount.setPassword(passwordEncoder.encode(request.getPassword())); + localAccount.setPassword(passwordEncoder.encode(registerRequest.getPassword())); localAccount.setUpdatedAt(LocalDateTime.now()); localAccount.setStatus(UserStatus.UNVERIFIED); localAccountRepository.save(localAccount); @@ -177,6 +179,10 @@ public void register(RegisterRequest request) { userSetting.setUpdatedAt(LocalDateTime.now()); userSettingRepository.save(userSetting); + return initLoginResponse(savedUser, registerRequest.getDeviceId(), request, response); + + + } @Transactional @@ -185,27 +191,14 @@ public LoginResponse login(LoginRequest req, HttpServletRequest request, HttpSer LocalAccount localAccount = localAccountRepository.findByEmail(req.getEmail()) .orElseThrow(() -> new CustomException(ErrorCode.E_401_INVALID_CREDENTIALS)); - if (!passwordEncoder.matches(req.getPassword(), localAccount.getPassword())) { throw new CustomException(ErrorCode.E_401_INVALID_CREDENTIALS); } - User user = localAccount.getUser(); user.setLastLoginAt(LocalDateTime.now()); - List roles = List.of(user.getRole().toString()); - String access = jwt.createAccessToken(user.getId(), roles); - String refresh = jwt.createRefreshToken(user.getId(), req.getDeviceId()); - - String ip = request.getRemoteAddr(); - String ua = request.getHeader("User-Agent"); - refreshService.saveLoginRefresh(user.getId(), refresh, req.getDeviceId(), ip, ua); - - response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie(refresh).toString()); - - - return new LoginResponse(access, prop.accessExpSeconds(), user.getRole()); + return initLoginResponse(user, req.getDeviceId(), request, response); } @Transactional @@ -284,4 +277,18 @@ public ResponseCookie removeRefreshCookie() { .maxAge(0) .build(); } + + public LoginResponse initLoginResponse(User user, String deviceId, HttpServletRequest request, HttpServletResponse response){ + List roles = List.of(user.getRole().toString()); + String access = jwt.createAccessToken(user.getId(), roles); + String refresh = jwt.createRefreshToken(user.getId(), deviceId); + + String ip = request.getRemoteAddr(); + String ua = request.getHeader("User-Agent"); + refreshService.saveLoginRefresh(user.getId(), refresh, deviceId, ip, ua); + + response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie(refresh).toString()); + + return new LoginResponse(access, prop.accessExpSeconds(), user.getRole()); + } } From f8a0aea05863001d0d0a04593b7c538d36848e7a Mon Sep 17 00:00:00 2001 From: junseo Lee Date: Mon, 3 Nov 2025 19:56:30 +0900 Subject: [PATCH 58/64] fix: solved cors in refresh access token --- .../com/scriptopia/demo/config/SecurityConfig.java | 12 ++++++++++-- src/main/resources/application.yml | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/config/SecurityConfig.java b/src/main/java/com/scriptopia/demo/config/SecurityConfig.java index d1326ab..0ce2234 100644 --- a/src/main/java/com/scriptopia/demo/config/SecurityConfig.java +++ b/src/main/java/com/scriptopia/demo/config/SecurityConfig.java @@ -87,8 +87,16 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMapping public UrlBasedCorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); -// config.setAllowedOrigins(Arrays.asList("http://localhost:3000")); - config.setAllowedOriginPatterns(Collections.singletonList("*")); + /* + * 로컬 테스트용 + */ + config.setAllowedOriginPatterns(Arrays.asList( + "http://localhost:*", + "http://127.0.0.1:*", + "http://192.168.*:*", + "http://10.*:*" + )); +// config.setAllowedOriginPatterns(Collections.singletonList("*")); config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(Arrays.asList("*")); // Authorization, Content-Type 등 허용 config.setExposedHeaders(Arrays.asList("Authorization")); // 필요시 노출할 헤더 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ad48b02..b6b6d07 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,7 +12,7 @@ spring: driver-class-name: org.postgresql.Driver jpa: hibernate: - ddl-auto: create + ddl-auto: update properties: hibernate: show_sql: true @@ -60,7 +60,7 @@ oauth: auth: jwt: issuer: scriptopia - access-exp-seconds: 1800 + access-exp-seconds: 18000 # 로컬 테스트용 300분으로 변경 refresh-exp-seconds: 1209600 secret: ${JWT_SECRET} From 50fb5cb3a8de53122879cbe805c692ea9084b2c8 Mon Sep 17 00:00:00 2001 From: junseo Lee Date: Mon, 3 Nov 2025 20:14:15 +0900 Subject: [PATCH 59/64] fix: solved cors --- .../com/scriptopia/demo/service/LocalAccountService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/service/LocalAccountService.java b/src/main/java/com/scriptopia/demo/service/LocalAccountService.java index ce7a9a6..ba3c604 100644 --- a/src/main/java/com/scriptopia/demo/service/LocalAccountService.java +++ b/src/main/java/com/scriptopia/demo/service/LocalAccountService.java @@ -45,8 +45,8 @@ public class LocalAccountService { private final MailService mailService; private static final String RT_COOKIE = "RT"; - private static final boolean COOKIE_SECURE = true; - private static final String COOKIE_SAMESITE = "None"; + private static final boolean COOKIE_SECURE = false; + private static final String COOKIE_SAME_SITE = "None"; private static final Pattern WS = Pattern.compile("[\\s\\p{Z}\\u200B\\u200C\\u200D\\uFEFF]"); @@ -262,7 +262,7 @@ public ResponseCookie refreshCookie(String value) { return ResponseCookie.from(RT_COOKIE, value) .httpOnly(true) .secure(COOKIE_SECURE) - .sameSite(COOKIE_SAMESITE) + .sameSite(COOKIE_SAME_SITE) .path("/") .maxAge(Duration.ofDays(14)) .build(); @@ -272,7 +272,7 @@ public ResponseCookie removeRefreshCookie() { return ResponseCookie.from(RT_COOKIE, "") .httpOnly(true) .secure(COOKIE_SECURE) - .sameSite(COOKIE_SAMESITE) + .sameSite(COOKIE_SAME_SITE) .path("/") .maxAge(0) .build(); From 25e93691e2addc13fb7c54abdadb79df90ca3c0b Mon Sep 17 00:00:00 2001 From: Yithian01 Date: Tue, 4 Nov 2025 17:11:13 +0900 Subject: [PATCH 60/64] refactor: add gameProgress end & deleteGameSession method --- .../com/scriptopia/demo/service/GameSessionService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/scriptopia/demo/service/GameSessionService.java b/src/main/java/com/scriptopia/demo/service/GameSessionService.java index 24dddec..3678ef9 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -444,11 +444,17 @@ public GameSessionMongo gameProgress(Long userId) { if( gameSessionMongo.getPlayerInfo().getLife() <= 0 ){ // gameOver 메소드 구현 필요 + + gameToEnd(userId); + deleteGameSession(userId, gameId); return gameToEnd(gameSessionMongo, 0); } if ( gameSessionMongo.getProgress() > gameSessionMongo.getStage().size()){ // gmaeClear 즉 + + gameToEnd(userId); + deleteGameSession(userId, gameId); return gameToEnd(gameSessionMongo, 1); } From 36be1f03160912bfc3971f2891af9b28985fcdd7 Mon Sep 17 00:00:00 2001 From: junseo Lee Date: Tue, 18 Nov 2025 19:07:03 +0900 Subject: [PATCH 61/64] fix: update samesite to lax --- .../java/com/scriptopia/demo/service/LocalAccountService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/scriptopia/demo/service/LocalAccountService.java b/src/main/java/com/scriptopia/demo/service/LocalAccountService.java index ba3c604..02b1136 100644 --- a/src/main/java/com/scriptopia/demo/service/LocalAccountService.java +++ b/src/main/java/com/scriptopia/demo/service/LocalAccountService.java @@ -46,7 +46,7 @@ public class LocalAccountService { private static final String RT_COOKIE = "RT"; private static final boolean COOKIE_SECURE = false; - private static final String COOKIE_SAME_SITE = "None"; + private static final String COOKIE_SAME_SITE = "Lax"; private static final Pattern WS = Pattern.compile("[\\s\\p{Z}\\u200B\\u200C\\u200D\\uFEFF]"); From aa271fcf6611279c0ed0ef789fbd2cb11a982fb6 Mon Sep 17 00:00:00 2001 From: junseo Lee Date: Wed, 19 Nov 2025 13:41:26 +0900 Subject: [PATCH 62/64] fix: fix refresh --- .../java/com/scriptopia/demo/controller/refreshController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/scriptopia/demo/controller/refreshController.java b/src/main/java/com/scriptopia/demo/controller/refreshController.java index e190402..342a074 100644 --- a/src/main/java/com/scriptopia/demo/controller/refreshController.java +++ b/src/main/java/com/scriptopia/demo/controller/refreshController.java @@ -35,7 +35,6 @@ public class refreshController { private static final String COOKIE_SAMESITE = "None"; @Operation(summary = "리프레시 토큰 재발급") - @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PostMapping("/refresh") public ResponseEntity refresh( @CookieValue(name = RT_COOKIE, required = false) String refreshToken, From 1ec52e4c1e216894c9f86d0d33d5adb4e27654b9 Mon Sep 17 00:00:00 2001 From: junseo Lee Date: Wed, 19 Nov 2025 14:53:59 +0900 Subject: [PATCH 63/64] fix: soving refresh error --- .../com/scriptopia/demo/controller/refreshController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/scriptopia/demo/controller/refreshController.java b/src/main/java/com/scriptopia/demo/controller/refreshController.java index e190402..ea4e763 100644 --- a/src/main/java/com/scriptopia/demo/controller/refreshController.java +++ b/src/main/java/com/scriptopia/demo/controller/refreshController.java @@ -31,8 +31,8 @@ public class refreshController { private final JwtProperties props; private static final String RT_COOKIE = "RT"; - private static final boolean COOKIE_SECURE = true; - private static final String COOKIE_SAMESITE = "None"; + private static final boolean COOKIE_SECURE = false; + private static final String COOKIE_SAMESITE = "Lax"; @Operation(summary = "리프레시 토큰 재발급") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") From 7984bb5c6457c1756f1e9e307528bf142d957183 Mon Sep 17 00:00:00 2001 From: junseo Lee Date: Wed, 19 Nov 2025 15:08:57 +0900 Subject: [PATCH 64/64] feat: add whitelist endpoint /token/refresh --- src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java b/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java index 005ff9e..4ea9376 100644 --- a/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java +++ b/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java @@ -19,6 +19,8 @@ public class SecurityWhitelist { "/swagger-ui/**", "/shops/pia/items", + "/token/refresh" + }; public static final String[] PUBLIC_GETS = {