diff --git a/.gitignore b/.gitignore index 5410b950..cfc2e859 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/build.gradle b/build.gradle index 36d6a55b..7ed6c563 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' } @@ -60,6 +60,10 @@ dependencies { // 소셜 로그인 oauth2 implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // 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/docker_compose_files/.env b/docker_compose_files/.env deleted file mode 100644 index 27c0c4e4..00000000 --- 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 diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java b/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java index 5072b6de..8d03d84f 100644 --- a/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java +++ b/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java @@ -49,13 +49,11 @@ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletExce Arrays.stream(SecurityWhitelist.PUBLIC_GETS) .anyMatch(pattern -> pathMatcher.match(pattern, path)); - boolean skip = authMatch || publicGetMatch; + boolean publicSharedGameUuidGet = "GET".equalsIgnoreCase(method) && + path.matches("^/shared-games/[0-9a-fA-F\\-]{36}$"); - if (skip) { - log.debug("➡️ Skipping JwtAuthFilter for whitelisted request: {} {}", method, path); - } + return authMatch || publicGetMatch || publicSharedGameUuidGet; - return skip; } 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 00000000..6e8e6723 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/LocalDataSeeder.java @@ -0,0 +1,659 @@ +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) { + + // 0) 이미 데이터가 있으면 중복 시드 방지 + if (userRepository.count() > 1) { + 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)); + } +} diff --git a/src/main/java/com/scriptopia/demo/config/SecurityConfig.java b/src/main/java/com/scriptopia/demo/config/SecurityConfig.java index 5f7cff64..0ce22348 100644 --- a/src/main/java/com/scriptopia/demo/config/SecurityConfig.java +++ b/src/main/java/com/scriptopia/demo/config/SecurityConfig.java @@ -20,10 +20,13 @@ 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; +import java.util.Collections; @Slf4j @Configuration @@ -40,7 +43,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 +59,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() ) @@ -79,7 +87,16 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti public UrlBasedCorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); - config.setAllowedOrigins(Arrays.asList("http://localhost:3000")); + /* + * 로컬 테스트용 + */ + 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/java/com/scriptopia/demo/config/SecurityWhitelist.java b/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java index d3f7811f..4ea93762 100644 --- a/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java +++ b/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java @@ -15,13 +15,17 @@ public class SecurityWhitelist { "/oauth/**", - "/shops/pia/items" + "/v3/api-docs/**", + "/swagger-ui/**", + "/shops/pia/items", + "/token/refresh" }; public static final String[] PUBLIC_GETS = { "/trades", - "/shared-games/**" + "/shared-games", + "/shared-games/tags" }; } 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 00000000..767d7f1d --- /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 00000000..5261a223 --- /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/config/fastapi/FastApiEndpoint.java b/src/main/java/com/scriptopia/demo/config/fastapi/FastApiEndpoint.java index 415c05e9..8a2c84a3 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,9 @@ public enum FastApiEndpoint { CHOICE("/games/choice"), BATTLE("/games/battle"), ITEM("/games/item"), - DONE("/games/done"); + DONE("/games/done"), + END("/games/end"), + TITLE("/games/title"); private final String path; diff --git a/src/main/java/com/scriptopia/demo/controller/AuctionController.java b/src/main/java/com/scriptopia/demo/controller/AuctionController.java index 0e5501fd..6e25b7b5 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 9400b495..2053d3b9 100644 --- a/src/main/java/com/scriptopia/demo/controller/AuthController.java +++ b/src/main/java/com/scriptopia/demo/controller/AuthController.java @@ -1,13 +1,17 @@ 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; +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; 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; @@ -15,19 +19,19 @@ @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( + public ResponseEntity logout( @CookieValue(name = RT_COOKIE, required = false) String refreshToken, HttpServletResponse response ) { @@ -35,10 +39,10 @@ public ResponseEntity logout( refreshTokenService.logout(refreshToken); } response.addHeader(HttpHeaders.SET_COOKIE, localAccountService.removeRefreshCookie().toString()); - return ResponseEntity.ok("로그아웃 되었습니다."); + return ResponseEntity.ok(new CommonResponse("로그아웃 되었습니다.")); } - + @Operation(summary = "로컬 로그인") @PostMapping("/login") public ResponseEntity login( @RequestBody @Valid LoginRequest req, @@ -49,64 +53,69 @@ public ResponseEntity login( return ResponseEntity.ok(localAccountService.login(req, request, response)); } + @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("회원가입에 성공했습니다."); + + return ResponseEntity.status(HttpStatus.CREATED).body(localAccountService.register(req, request, response)); } + @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/controller/GameSessionController.java b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java index ded8895b..50c0b01d 100644 --- a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java +++ b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java @@ -1,11 +1,12 @@ 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 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,15 +15,16 @@ @RestController @RequestMapping("/games") +@Tag(name = "게임 세션 API", description = "게임 세션 관련 API 입니다.") @RequiredArgsConstructor public class GameSessionController { private final GameSessionService gameSessionService; - private final HistoryService historyService; /* * 게임 -> 게임 도중 종료 */ + @Operation(summary = "저장 후 게임 종료") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") @PostMapping("/exit") public ResponseEntity createGameSession(Authentication authentication, @RequestBody GameSessionRequest request) { @@ -33,15 +35,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 +67,7 @@ public ResponseEntity startNewGame( return ResponseEntity.ok(response); } - + @Operation(summary = "게임 진입") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @GetMapping("/{gameId}") public ResponseEntity getInGameData( @@ -69,34 +82,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 +126,7 @@ public ResponseEntity equipItem( return ResponseEntity.ok(response); } + @Operation(summary = "아이템 버리기") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @DeleteMapping("/dropItem/{gameId}/{itemId}") public ResponseEntity dropItem( @@ -125,36 +141,48 @@ public ResponseEntity dropItem( return ResponseEntity.ok(response); } + @Operation(summary = "아이템 구매") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @PostMapping("/{gameId}/items/purchase/{itemId}") + public ResponseEntity buyItem( + @PathVariable("gameId") String gameId, + @PathVariable("itemId") String itemId, + Authentication authentication) throws JsonProcessingException { - /* - * 게임 -> 기존 게임 조회 - */ - @GetMapping("/me") - public ResponseEntity loadGameSession(Authentication authentication) { Long userId = Long.valueOf(authentication.getName()); - return gameSessionService.getGameSession(userId); + + GameSessionMongo response = gameSessionService.gameBuyItem(userId, itemId); + + return ResponseEntity.ok(response); } + @Operation(summary = "아이템 판매") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @PostMapping("/{gameId}/items/sell/{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); + } - /** - * 현재는 userId, sessionId를 통해 저장하는데 - * 인증 관리 부분 끝나면 header에 token 꺼내오고 requestparameter session_id로 저장하게 수정 - */ /* - * 게임 -> 히스토리 생성 + * 게임 종료 후 -> 히스토리 생성 */ + @Operation(summary = "게임 종료 후 히스토리 저장") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") - @PostMapping("/history") - public ResponseEntity addHistory(Authentication authentication, @RequestBody GameSessionRequest request) { + @PostMapping("/{gameId}/history") + public ResponseEntity addHistory( + Authentication authentication, + @PathVariable("gameId") String gameId) { Long userId = Long.valueOf(authentication.getName()); - return historyService.createHistory(userId, request.getGameId()); + return gameSessionService.gameToEnd(userId); } - /** 개발용: 로컬 MongoDB에 더미 세션 한 건 심어서 테스트용 ObjectId 반환 */ - @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/ItemController.java b/src/main/java/com/scriptopia/demo/controller/ItemController.java index cecdbb2c..b872fc3d 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 f065dbcd..dcb73650 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 8411b9f4..6690cfa5 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 a79c0c86..e883d7a1 100644 --- a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -1,16 +1,15 @@ 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; +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 +21,7 @@ @RestController @RequestMapping("/shared-games") +@Tag(name = "게임 공유 관련 API", description = "게임 공유 관련 API 입니다.") @RequiredArgsConstructor public class SharedGameController { private final SharedGameService sharedGameService; @@ -31,42 +31,54 @@ 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); } /* 게임 공유 -> 공유 게임 목록 조회 */ + @Operation(summary = "공유 게임 목록 조회") @GetMapping - public ResponseEntity> getPublicSharedGames(Authentication authentication, - @RequestParam(required = false) UUID lastUUID, - @RequestParam(defaultValue = "20") int size, - @RequestParam(required = false) List tagIds, - @RequestParam(required = false) String query, - @RequestParam(defaultValue = "LATEST")SharedGameSort sort) { - Long viewerId = (authentication == null) ? null : Long.valueOf(authentication.getName()); - return sharedGameService.getPublicSharedGames(viewerId, lastUUID, size, tagIds, query, sort); + 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); } /* 게임공유 : 공유된 게임 상세 조회 */ - @GetMapping("/{sharedGameId}") - public ResponseEntity getSharedGameDetail(@PathVariable("sharedGameId") UUID sharedGameId) { - return sharedGameService.getDetailedSharedGame(sharedGameId); + @Operation(summary = "공유 게임 상세 조회") + @GetMapping("/{sharedGameUuid}") + public ResponseEntity getSharedGameDetail(Authentication authentication, @PathVariable("sharedGameUuid") UUID sharedGameUuid) { + Long userId = null; + if (authentication != null && authentication.isAuthenticated() && authentication.getName() != null) { + try { + userId = Long.valueOf(authentication.getName()); + } catch (NumberFormatException ignored) { + } + } + System.out.println(userId); + + return sharedGameService.getDetailedSharedGame(userId, sharedGameUuid); } /* 게임공유 : 공유 게임 Like 요청 */ + @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); @@ -75,6 +87,7 @@ public ResponseEntity likeSharedGame(@PathVariable("sharedGameId") UUID share /* 게임공유 : 공유된 게임 태그 조회 */ + @Operation(summary = "게임 태그 조회") @GetMapping("/tags") public ResponseEntity getSharedGameTags() { return sharedGameService.getTag(); @@ -83,27 +96,18 @@ public ResponseEntity getSharedGameTags() { /* 게임 공유 -> 공유한 게임 삭제 */ + @Operation(summary = "공유한 게임 삭제") @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") - @DeleteMapping("/shared-games") - 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("게임이 삭제되었습니다."); } - /* - 게임 공유 -> 공유한 게임 조회(내가 공유한 게임 조회) - */ - @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") public ResponseEntity addTag(@RequestBody TagDefCreateRequest req) { @@ -111,6 +115,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 759c16e1..a7a11459 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 b330575c..7489fc07 100644 --- a/src/main/java/com/scriptopia/demo/controller/UserController.java +++ b/src/main/java/com/scriptopia/demo/controller/UserController.java @@ -1,33 +1,34 @@ 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.UserSettingsDTO; -import com.scriptopia.demo.service.HistoryService; +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; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; 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; @RestController @RequestMapping("/users/me") +@Tag(name = "유저 관련 API", description = "유저 관련 API 입니다.") @RequiredArgsConstructor public class UserController { private final UserService userService; - private final HistoryService historyService; private final UserCharacterImgService userCharacterImgService; + @Operation(summary = "보유 장비 아이템 조회") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @GetMapping("/items/game") public ResponseEntity> getGameItems( @@ -38,6 +39,7 @@ public ResponseEntity> getGameItems( return ResponseEntity.ok(response); } + @Operation(summary = "보유 피아 아이템 조회") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @GetMapping("/items/pia") public ResponseEntity> getPiaItems( @@ -48,6 +50,7 @@ public ResponseEntity> getPiaItems( return ResponseEntity.ok(response); } + @Operation(summary = "사용자 옵션 조회") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @GetMapping("/settings") public ResponseEntity getUserSettings( @@ -58,6 +61,7 @@ public ResponseEntity getUserSettings( return ResponseEntity.ok(response); } + @Operation(summary = "사용자 옵션 변경") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @PutMapping("/settings") public ResponseEntity updateUserSettings( @@ -69,6 +73,7 @@ public ResponseEntity updateUserSettings( return ResponseEntity.ok("사용자 설정이 변경되었습니다."); } + @Operation(summary = "사용자 재화 조회") @PreAuthorize("hasAnyAuthority('USER','ADMIN')") @GetMapping("/assets") public ResponseEntity getUserAssets( @@ -79,39 +84,41 @@ 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, - Authentication authentication) { + public ResponseEntity getHistory(@RequestParam(required = false) UUID lastId, + @RequestParam(defaultValue = "10") int size, + 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") - public ResponseEntity saveUserCharacterImg(Authentication authentication, @RequestParam("url") String url) { + @PostMapping("/profile-images") + 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 = "프로필 이미지 조회") @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) { + @GetMapping("/status") + public ResponseEntity getUserStatus(Authentication authentication) { Long userId = Long.valueOf(authentication.getName()); - return userCharacterImgService.saveCharacterImg(userId, file); + return ResponseEntity.ok(userService.getUserStatus(userId)); } + } diff --git a/src/main/java/com/scriptopia/demo/controller/refreshController.java b/src/main/java/com/scriptopia/demo/controller/refreshController.java index 875dc9a3..1bd8d777 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 { @@ -28,10 +31,10 @@ 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"; - @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @Operation(summary = "리프레시 토큰 재발급") @PostMapping("/refresh") public ResponseEntity refresh( @CookieValue(name = RT_COOKIE, required = false) String refreshToken, diff --git a/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java b/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java index e4c8f4a7..85dcc74c 100644 --- a/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java +++ b/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java @@ -7,9 +7,9 @@ @Getter public enum ChoiceResultType { - BATTLE(20), - CHOICE(40), - DONE(45), + BATTLE(30), // 20, 40, 45, 5 + CHOICE(5), + DONE(60), SHOP(5); private final int nextEventType; diff --git a/src/main/java/com/scriptopia/demo/domain/ItemDef.java b/src/main/java/com/scriptopia/demo/domain/ItemDef.java index e3d935c0..34e94b6c 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 diff --git a/src/main/java/com/scriptopia/demo/domain/SceneType.java b/src/main/java/com/scriptopia/demo/domain/SceneType.java index b001a92c..a56194b5 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 } 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 217d7e2e..cd5dc993 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/domain/mongo/RewardInfoMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/RewardInfoMongo.java index 34de088e..5d01b74d 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/domain/mongo/ShopInfoMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/ShopInfoMongo.java index 8518ba98..6c92c913 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; } 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 00000000..5d10f838 --- /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; +} 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 d0996d03..6573c474 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/auth/LoginRequest.java b/src/main/java/com/scriptopia/demo/dto/auth/LoginRequest.java index 6e6c7df7..26a6a129 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/java/com/scriptopia/demo/dto/auth/RegisterRequest.java b/src/main/java/com/scriptopia/demo/dto/auth/RegisterRequest.java index 5e896916..259ae206 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/dto/gamesession/GameEndRequest.java b/src/main/java/com/scriptopia/demo/dto/gamesession/GameEndRequest.java new file mode 100644 index 00000000..ef44b40d --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/GameEndRequest.java @@ -0,0 +1,18 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GameEndRequest { + private String worldView; + private String location; + private String previousStory; + private String playerName; + private int 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 00000000..fda17c43 --- /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; +} 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 00000000..2bbdbfc0 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleRequest.java @@ -0,0 +1,18 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.Data; +import lombok.Builder; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GameTitleRequest { + private List contents = new ArrayList<>(); +} 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 00000000..88397e47 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/GameTitleResponse.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GameTitleResponse { + private List titles; +} 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 00000000..2666aeb9 --- /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 00000000..21f06f93 --- /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; + +} 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 9f3021d0..237c8f99 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; // 외부 + + } 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 00000000..9eb12842 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopTable.java @@ -0,0 +1,42 @@ +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 shopItemId; + 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; + } +} 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 922a7535..5489df9f 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 00000000..8111e74e --- /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/history/HistoryRequest.java b/src/main/java/com/scriptopia/demo/dto/history/HistoryRequest.java index 01cdb1e8..3954cc7b 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 050f69b2..08295073 100644 --- a/src/main/java/com/scriptopia/demo/dto/history/HistoryResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/history/HistoryResponse.java @@ -1,12 +1,20 @@ package com.scriptopia.demo.dto.history; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import java.util.UUID; + +@Builder @Data +@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/dto/sharedgame/CursorPage.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/CursorPage.java index 6e2d32a1..61af4e35 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/dto/sharedgame/PublicSharedGameDetailResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java index f7f22112..c646a976 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; @@ -9,13 +10,17 @@ @Data public class PublicSharedGameDetailResponse { - private UUID sharedGameUUID; - private String nickname; - private String thumbnailUrl; - private Long totalPlayed; + private UUID sharedGameUuid; + private String posterUrl; private String title; private String worldView; private String backgroundStory; + private String creator; + private Long playCount; + private Long likeCount; + + @JsonProperty("isLiked") + private boolean isLiked; @JsonFormat(shape = JsonFormat.Shape.STRING) private LocalDateTime sharedAt; @@ -24,9 +29,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; } } @@ -34,6 +41,7 @@ public TagDto(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/PublicSharedGameResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameResponse.java index c7a70fdd..c23a9f06 100644 --- a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameResponse.java +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameResponse.java @@ -8,14 +8,10 @@ @Data public class PublicSharedGameResponse { - private UUID sharedGameId; + 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/dto/sharedgame/PublicTagDefResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicTagDefResponse.java index 1a35d3a2..7f5e3668 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/sharedgame/SharedGameSaveDto.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/SharedGameSaveDto.java new file mode 100644 index 00000000..0b1dd6f7 --- /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 ff091570..093574da 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,28 @@ 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; 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; + + @JsonProperty("isLiked") 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/dto/users/UserImageRequest.java b/src/main/java/com/scriptopia/demo/dto/users/UserImageRequest.java new file mode 100644 index 00000000..f00f9cbd --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/users/UserImageRequest.java @@ -0,0 +1,10 @@ +package com.scriptopia.demo.dto.users; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class UserImageRequest { + private String url; +} 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 00000000..0d0a54b5 --- /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/exception/ErrorCode.java b/src/main/java/com/scriptopia/demo/exception/ErrorCode.java index aaa4aa7f..792f854b 100644 --- a/src/main/java/com/scriptopia/demo/exception/ErrorCode.java +++ b/src/main/java/com/scriptopia/demo/exception/ErrorCode.java @@ -84,6 +84,10 @@ 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("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 diff --git a/src/main/java/com/scriptopia/demo/exception/GlobalExceptionHandler.java b/src/main/java/com/scriptopia/demo/exception/GlobalExceptionHandler.java index e4d38d15..ed422c1d 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/mapper/InGameMapper.java b/src/main/java/com/scriptopia/demo/mapper/InGameMapper.java index 2871a7ae..3a71ba9a 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,38 @@ 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() + // 아이템 정의 정보 + .shopItemId(itemDef.getId()) + .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(); + } } diff --git a/src/main/java/com/scriptopia/demo/repository/HistoryRepository.java b/src/main/java/com/scriptopia/demo/repository/HistoryRepository.java index 245f0217..ba430950 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/repository/SharedGameScoreRepository.java b/src/main/java/com/scriptopia/demo/repository/SharedGameScoreRepository.java index bd759bde..3dd3b415 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); diff --git a/src/main/java/com/scriptopia/demo/service/FastApiService.java b/src/main/java/com/scriptopia/demo/service/FastApiService.java index 53ce7b56..a88490be 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 { @@ -65,5 +67,25 @@ 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(); + } + + // 게임 종료 시 빅 이벤트 타이틀 처리 + 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 89876075..3678ef97 100644 --- a/src/main/java/com/scriptopia/demo/service/GameSessionService.java +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -1,8 +1,8 @@ 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.*; +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; @@ -27,6 +27,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; @Service @@ -42,6 +43,8 @@ public class GameSessionService { private final FastApiService fastApiService; private final ItemService itemService; private final InGameMapper inGameMapper; + private final ItemDefRepository itemDefRepository; + private final HistoryRepository historyRepository; public ResponseEntity getGameSession(Long userid) { @@ -293,7 +296,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(); @@ -338,6 +341,21 @@ public Object getInGameDataDto(Long userId){ } else if (currentSceneType == SceneType.SHOP) { + return InGameShopResponse.builder() + .sceneType("SHOP") + .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())) + .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(); @@ -369,6 +387,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; @@ -396,6 +442,25 @@ public GameSessionMongo gameProgress(Long userId) { .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + 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); + + } + + + SceneType currentSceneType = gameSessionMongo.getSceneType(); switch (currentSceneType) { case SceneType.CHOICE -> { @@ -405,6 +470,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); @@ -462,12 +534,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; } @@ -528,8 +603,9 @@ public GameSessionMongo gameToChoice(Long userId) { .luck(npcStat[3]) .build(); - gameSessionMongo.setNpcInfo(npcInfoMongo); } + gameSessionMongo.setNpcInfo(npcInfoMongo); + List choiceList = new ArrayList<>(); @@ -573,6 +649,7 @@ public GameSessionMongo gameToChoice(Long userId) { } /** + * 배틍 * @param userId * @return win? */ @@ -699,6 +776,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()) @@ -707,7 +789,7 @@ public GameSessionMongo gameToDone(Long userId) { .selectedChoice(gameSessionMongo.getPreChoice()) .resultContent(RewardType.getRewardSummary(gameSessionMongo.getRewardInfo())) .playerName(gameSessionMongo.getPlayerInfo().getName()) - .playerVictory( (gameSessionMongo.getSceneType() == SceneType.BATTLE)) + .playerVictory( isVictory ) .build(); @@ -723,7 +805,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(); @@ -806,7 +887,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 -> { @@ -826,6 +907,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); } @@ -915,12 +1023,110 @@ 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)); + + 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(); + List shopItems = shopInfoMongo.getItemDefId(); + + 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)); + + long shopItemGold = targetDef.getPrice(); + + if(playerGold < shopItemGold){ + 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) + .acquiredAt(LocalDateTime.now()) + .equipped(false) + .source("item shop") + .build(); + + inventory.add(buyItem); + + + 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); + } + private void addStats(PlayerInfoMongo player, ItemDefMongo item) { player.setStrength(player.getStrength() + safeStat(item.getStrength())); 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.setHealthPoint( item.getBaseStat() ); + } } private void removeStats(PlayerInfoMongo player, ItemDefMongo item) { @@ -928,6 +1134,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) { @@ -1004,4 +1213,154 @@ 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); + } + } + + + /** + * 게임 종료 처리 (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; + } + + + @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(); + + 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.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) // 필요 시 + .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() + .uuid(history.getUuid()) + .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 deleted file mode 100644 index 1ebaf343..00000000 --- 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()); - } - -} diff --git a/src/main/java/com/scriptopia/demo/service/LocalAccountService.java b/src/main/java/com/scriptopia/demo/service/LocalAccountService.java index fcf95d97..02b11368 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 @@ -43,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 = "Lax"; private static final Pattern WS = Pattern.compile("[\\s\\p{Z}\\u200B\\u200C\\u200D\\uFEFF]"); @@ -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 @@ -269,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(); @@ -279,9 +272,23 @@ public ResponseCookie removeRefreshCookie() { return ResponseCookie.from(RT_COOKIE, "") .httpOnly(true) .secure(COOKIE_SECURE) - .sameSite(COOKIE_SAMESITE) + .sameSite(COOKIE_SAME_SITE) .path("/") .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()); + } } diff --git a/src/main/java/com/scriptopia/demo/service/SharedGameFavoriteService.java b/src/main/java/com/scriptopia/demo/service/SharedGameFavoriteService.java index 038a9d86..3a10bdb5 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 aa815d76..8ac0f3fb 100644 --- a/src/main/java/com/scriptopia/demo/service/SharedGameService.java +++ b/src/main/java/com/scriptopia/demo/service/SharedGameService.java @@ -6,7 +6,6 @@ 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; @@ -41,43 +40,22 @@ public ResponseEntity saveSharedGame(Long Id, UUID uuid) { throw new CustomException(ErrorCode.E_401_NOT_EQUAL_SHARED_GAME); } - SharedGame sharedGame = SharedGame.from(user, history); - return ResponseEntity.ok(sharedGameRepository.save(sharedGame)); - } - - 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<>(); + history.setIsShared(true); - for(String tagName : tagdto) { - tags.add(new MySharedGameResponse.TagDto(tagName)); - } - - dto.setTags(tags); - dtos.add(dto); - } + SharedGame sharedGame = SharedGame.from(user, history); + 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(dtos); + return ResponseEntity.ok(dto); } @Transactional @@ -95,29 +73,32 @@ 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 tagName = gameTagRepository.findTagNamesBySharedGameId(game.getId()); + 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.setNickname(game.getUser().getNickname()); - dto.setThumbnailUrl(game.getThumbnailUrl()); - dto.setTotalPlayed(sharedGameScoreRepository.countBySharedGameId(game.getId())); + dto.setSharedGameUuid(game.getUuid()); + 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()); + dto.setLiked(isLiked); 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); @@ -126,6 +107,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); } @@ -146,7 +128,7 @@ public ResponseEntity getTag() { } @Transactional(readOnly = true) - public ResponseEntity> getPublicSharedGames(Long userId, + public ResponseEntity> getPublicSharedGames( UUID lastUuid, int size, List tagIds, @@ -159,7 +141,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; @@ -210,23 +192,12 @@ 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()); // 집계 - dto.setTotalPlayCount(sharedGameScoreRepository.countBySharedGameId(g.getId())); - dto.setLikeCount(sharedGameFavoriteRepository.countBySharedGameId(g.getId())); - - 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.setPlayCount(sharedGameScoreRepository.countBySharedGameId(g.getId())); // 태그 dto.setTags(gameTagRepository.findTagDtosBySharedGameId(g.getId())); @@ -234,7 +205,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/java/com/scriptopia/demo/service/TagDefService.java b/src/main/java/com/scriptopia/demo/service/TagDefService.java index 4f820349..b0045339 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); diff --git a/src/main/java/com/scriptopia/demo/service/UserService.java b/src/main/java/com/scriptopia/demo/service/UserService.java index f729030a..b78532c2 100644 --- a/src/main/java/com/scriptopia/demo/service/UserService.java +++ b/src/main/java/com/scriptopia/demo/service/UserService.java @@ -1,31 +1,43 @@ 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.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; 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; +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; +import java.util.stream.Collectors; @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 +117,36 @@ public List getPiaItems(String userId){ return piaItems; } + @Transactional(readOnly = true) + public HistoryPageResponseDto fetchMyHistory(Long userId, UUID lastId, int size) { + List histories = historyRepository.findHistoriesByUserWithCursor( + userId, + lastId, + PageRequest.of(0, size + 1) + ); + + boolean hasNext = histories.size() > size; + + List result = histories.stream() + .limit(size) + .map(HistoryPageResponse::from) + .collect(Collectors.toList()); + UUID nextCursor = hasNext ? result.get(result.size() - 1).getUuid() : null; + 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/java/com/scriptopia/demo/utils/GameBalanceUtil.java b/src/main/java/com/scriptopia/demo/utils/GameBalanceUtil.java index 03a79c9d..d2e61442 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/application.yml b/src/main/resources/application.yml index 7e14b2fe..b6b6d07d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -57,12 +57,10 @@ oauth: redirect-uri: ${NAVER_REDIRECT_URI} scope: name email profile_image - - auth: jwt: issuer: scriptopia - access-exp-seconds: 1800 + access-exp-seconds: 18000 # 로컬 테스트용 300분으로 변경 refresh-exp-seconds: 1209600 secret: ${JWT_SECRET} @@ -71,5 +69,17 @@ 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 +image-url-prefix: /images +logging: + level: + org.springframework.security: DEBUG \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index ef3026d7..de235518 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

+ + +