diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..07162ff4 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 16b60f04..cfc2e859 100644 --- a/.gitignore +++ b/.gitignore @@ -36,10 +36,11 @@ out/ ### VS Code ### .vscode/ +/docker_compose_file/.env /docker_compose_files/data /docker_compose_files/mongo_data /docker_compose_files/postgres_data /docker_compose_files/redis_data - +*.properties diff --git a/build.gradle b/build.gradle index 3d730214..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' } @@ -24,27 +24,46 @@ repositories { } dependencies { - // ๐Ÿ”ง ํ•ต์‹ฌ ์˜์กด์„ฑ + // ๊ธฐ๋ณธ implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-webflux' - // ๐Ÿณ DB ๊ด€๋ จ + // DB ๊ด€๋ จ runtimeOnly 'org.postgresql:postgresql' - // ๐Ÿ“ฆ NoSQL (MongoDB, Redis) + // NoSQL (MongoDB, Redis) implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' implementation 'org.springframework.boot:spring-boot-starter-data-redis' - // ๐Ÿ”ง Lombok + // Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' - // โœ… ํ…Œ์ŠคํŠธ + // ํ…Œ์ŠคํŠธ testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // jwt ์ธ์ฆ ๊ด€๋ จ + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + //์ด๋ฉ”์ผ ์ธ์ฆ + implementation 'org.springframework.boot:spring-boot-starter-mail' + + // ์†Œ์…œ ๋กœ๊ทธ์ธ 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/ScriptopiaDemoApplication.java b/src/main/java/com/scriptopia/demo/ScriptopiaDemoApplication.java index a86fa96c..b0478aee 100644 --- a/src/main/java/com/scriptopia/demo/ScriptopiaDemoApplication.java +++ b/src/main/java/com/scriptopia/demo/ScriptopiaDemoApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication +@ConfigurationPropertiesScan public class ScriptopiaDemoApplication { public static void main(String[] args) { diff --git a/src/main/java/com/scriptopia/demo/config/AdminInitializer.java b/src/main/java/com/scriptopia/demo/config/AdminInitializer.java new file mode 100644 index 00000000..c18b1650 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/AdminInitializer.java @@ -0,0 +1,72 @@ +package com.scriptopia.demo.config; + +import com.scriptopia.demo.domain.*; +import com.scriptopia.demo.repository.LocalAccountRepository; +import com.scriptopia.demo.repository.UserRepository; +import com.scriptopia.demo.repository.UserSettingRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.cglib.core.Local; +import org.springframework.context.annotation.Bean; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +@RequiredArgsConstructor +public class AdminInitializer implements ApplicationRunner { + + private final UserRepository userRepository; + private final LocalAccountRepository localAccountRepository; + private final PasswordEncoder passwordEncoder; + private final UserSettingRepository userSettingRepository; + + @Value("${app.admin.username}") + private String adminUsername; + + @Value("${app.admin.password}") + private String adminPassword; + + + + + @Override + public void run(ApplicationArguments args) { + if (!userRepository.existsByNickname("admin")) { + + User admin = new User(); + admin.setPia(999999L); + admin.setNickname("admin"); + admin.setCreatedAt(LocalDateTime.now()); + admin.setLastLoginAt(LocalDateTime.now()); + admin.setProfileImgUrl(null); + admin.setRole(Role.ADMIN); + admin.setLoginType(LoginType.LOCAL); + User adminUser = userRepository.save(admin); + + + LocalAccount localAccount = new LocalAccount(); + localAccount.setUser(adminUser); + localAccount.setEmail(adminUsername); + localAccount.setPassword(passwordEncoder.encode(adminPassword)); + localAccount.setUpdatedAt(LocalDateTime.now()); + localAccount.setStatus(UserStatus.VERIFIED); + LocalAccount adminAccount = localAccountRepository.save(localAccount); + + UserSetting userSetting = new UserSetting(); + userSetting.setUser(adminUser); + userSetting.setTheme(Theme.DARK); + userSetting.setFontType(FontType.PretendardVariable); + userSetting.setFontSize(16); + userSetting.setLineHeight(1); + userSetting.setWordSpacing(1); + userSetting.setUpdatedAt(LocalDateTime.now()); + userSettingRepository.save(userSetting); + + } + } +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/config/DataLoaderConfig.java b/src/main/java/com/scriptopia/demo/config/DataLoaderConfig.java new file mode 100644 index 00000000..3e5f2876 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/DataLoaderConfig.java @@ -0,0 +1,89 @@ +package com.scriptopia.demo.config; + +import com.scriptopia.demo.domain.*; +import com.scriptopia.demo.repository.*; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +@Configuration +@RequiredArgsConstructor +public class DataLoaderConfig { + + private final EffectGradeDefRepository effectGradeDefRepository; + private final ItemGradeDefRepository itemGradeDefRepository; + + @Bean + public ApplicationRunner dataLoader() { + return args -> { + // EffectGradeDef ์ดˆ๊ธฐํ™” + Map effectPriceMap = Map.of( + EffectProbability.COMMON, 10L, + EffectProbability.UNCOMMON, 30L, + EffectProbability.RARE, 50L, + EffectProbability.EPIC, 80L, + EffectProbability.LEGENDARY, 100L + ); + + Map effectAtkMultiplierMap = Map.of( + EffectProbability.COMMON, 0.10, // C + EffectProbability.UNCOMMON, 0.15, // U + EffectProbability.RARE, 0.20, // R + EffectProbability.EPIC, 0.25, // E + EffectProbability.LEGENDARY, 0.30 // L + ); + + for (EffectProbability prob : EffectProbability.values()) { + if (prob == null) continue; + + effectGradeDefRepository.findByEffectProbability(prob).ifPresentOrElse( + def -> { + // ์ด๋ฏธ ์žˆ์œผ๋ฉด ์—…๋ฐ์ดํŠธ + def.setPrice(effectPriceMap.get(prob)); + def.setWeight(effectAtkMultiplierMap.get(prob)); + effectGradeDefRepository.save(def); + }, + () -> { + // ์—†์œผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑ + EffectGradeDef def = new EffectGradeDef(); + def.setEffectProbability(prob); + def.setPrice(effectPriceMap.get(prob)); + def.setWeight(effectAtkMultiplierMap.get(prob)); + effectGradeDefRepository.save(def); + } + ); + } + + // ItemGradeDef ์ดˆ๊ธฐํ™” + Map itemGradePriceMap = Map.of( + Grade.COMMON, 10L, + Grade.UNCOMMON, 30L, + Grade.RARE, 50L, + Grade.EPIC, 80L, + Grade.LEGENDARY, 100L + ); + + for (Grade grade : Grade.values()) { + if (grade == null) continue; + + itemGradeDefRepository.findByGrade(grade).ifPresentOrElse( + def -> { + def.setPrice(itemGradePriceMap.get(grade)); + def.setWeight(1.0); + itemGradeDefRepository.save(def); + }, + () -> { + ItemGradeDef def = new ItemGradeDef(); + def.setGrade(grade); + def.setPrice(itemGradePriceMap.get(grade)); + def.setWeight(1.0); + itemGradeDefRepository.save(def); + } + ); + } + }; + } +} diff --git a/src/main/java/com/scriptopia/demo/config/GameTagLoaderConfig.java b/src/main/java/com/scriptopia/demo/config/GameTagLoaderConfig.java new file mode 100644 index 00000000..fb2963a5 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/GameTagLoaderConfig.java @@ -0,0 +1,29 @@ +package com.scriptopia.demo.config; + +import com.scriptopia.demo.domain.TagDef; +import com.scriptopia.demo.repository.TagDefRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class GameTagLoaderConfig implements CommandLineRunner { + private final TagDefRepository tagDefRepository; + + @Override + public void run(String... args) { + List tags = List.of("๋‚จ์„ฑ ์ธ๊ธฐ", "์‹œ๋ฎฌ๋ ˆ์ด์…˜", "๋กœ๋งจ์Šค", "SF", "์ข€๋น„", "์ƒ์กด", "๊ฒฉํˆฌ", "๋ชจํ—˜", "ํƒํ—˜", + "์ „ํˆฌ", "ํŒํƒ€์ง€", "ํ˜„๋Œ€", "๋ฒ”์ฃ„", "์•„ํฌ์นผ๋ฆฝ์Šค", "๋“œ๋ž˜๊ณค", "๋˜์ „"); + + for (String tagName : tags) { + if (!tagDefRepository.existsByTagName(tagName)) { + TagDef tagDef = new TagDef(); + tagDef.setTagName(tagName); + tagDefRepository.save(tagDef); + } + } + } +} diff --git a/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java b/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java new file mode 100644 index 00000000..8d03d84f --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/JwtAuthFilter.java @@ -0,0 +1,112 @@ +package com.scriptopia.demo.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.scriptopia.demo.dto.exception.ErrorResponse; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.utils.JwtProvider; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import java.io.IOException; +import java.util.Arrays; +import java.util.stream.Collectors; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtProvider jwt; + private final AntPathMatcher pathMatcher = new AntPathMatcher(); + + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String path = request.getServletPath(); + String method = request.getMethod(); + + boolean authMatch = Arrays.stream(SecurityWhitelist.AUTH_WHITELIST) + .anyMatch(pattern -> pathMatcher.match(pattern, path)); + + boolean publicGetMatch = "GET".equalsIgnoreCase(method) && + Arrays.stream(SecurityWhitelist.PUBLIC_GETS) + .anyMatch(pattern -> pathMatcher.match(pattern, path)); + + boolean publicSharedGameUuidGet = "GET".equalsIgnoreCase(method) && + path.matches("^/shared-games/[0-9a-fA-F\\-]{36}$"); + + return authMatch || publicGetMatch || publicSharedGameUuidGet; + + } + + + + @Override + protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) + throws ServletException, IOException { + + String authHeader = req.getHeader("Authorization"); + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + setErrorResponse(res, ErrorCode.E_400_MISSING_JWT); + return; + } + + String token = authHeader.substring(7); + try { + jwt.parse(token); // ์œ ํšจ์„ฑ ์ฒดํฌ + + String userId = jwt.getUserId(token).toString(); + var roles = jwt.getRoles(token).stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + var authentication = new UsernamePasswordAuthenticationToken(userId, null, roles); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req)); + SecurityContextHolder.getContext().setAuthentication(authentication); + + + } catch (IllegalArgumentException e) { + setErrorResponse(res,ErrorCode.E_400_MISSING_JWT); + return; + } catch (ExpiredJwtException e) { + setErrorResponse(res,ErrorCode.E_401_EXPIRED_JWT); + return; + } catch (MalformedJwtException e) { + setErrorResponse(res,ErrorCode.E_401_MALFORMED); + return; + } catch (UnsupportedJwtException e) { + setErrorResponse(res,ErrorCode.E_401_UNSUPPORTED_JWT); + return; + } catch (JwtException e) { + setErrorResponse(res,ErrorCode.E_401_INVALID_SIGNATURE); + return; + } + + chain.doFilter(req, res); + } + + + private void setErrorResponse(HttpServletResponse res, ErrorCode code) throws IOException { + res.setStatus(code.getStatus().value()); + res.setContentType(MediaType.APPLICATION_JSON_VALUE); + res.setCharacterEncoding("UTF-8"); + new ObjectMapper().writeValue(res.getOutputStream(), new ErrorResponse(code)); + } +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/config/JwtProperties.java b/src/main/java/com/scriptopia/demo/config/JwtProperties.java new file mode 100644 index 00000000..b6ce66d5 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/JwtProperties.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.config; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +@Validated +@ConfigurationProperties(prefix = "auth.jwt") +public record JwtProperties( + @NotBlank String issuer, + @Min(60) long accessExpSeconds, + @Min(60) long refreshExpSeconds, + @NotBlank String secret +) {} 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/OAuthProperties.java b/src/main/java/com/scriptopia/demo/config/OAuthProperties.java new file mode 100644 index 00000000..52d5c6d3 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/OAuthProperties.java @@ -0,0 +1,26 @@ +package com.scriptopia.demo.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "oauth") +public class OAuthProperties { + + private ProviderProperties google; + private ProviderProperties kakao; + private ProviderProperties naver; + + @Getter + @Setter + public static class ProviderProperties { + private String clientId; + private String clientSecret; + private String redirectUri; + private String scope; + } +} diff --git a/src/main/java/com/scriptopia/demo/config/PiaShopInitializer.java b/src/main/java/com/scriptopia/demo/config/PiaShopInitializer.java new file mode 100644 index 00000000..abc7148e --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/PiaShopInitializer.java @@ -0,0 +1,41 @@ +package com.scriptopia.demo.config; + +import com.scriptopia.demo.domain.PiaItem; +import com.scriptopia.demo.repository.PiaItemRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PiaShopInitializer implements ApplicationRunner { + + private final PiaItemRepository piaItemRepository; + + @Override + public void run(ApplicationArguments args) { + + if (!(piaItemRepository.existsByName("์•„์ดํ…œ ๋ชจ๋ฃจ"))) { + PiaItem piaItem = initializePiaItem( + "์•„์ดํ…œ ๋ชจ๋ฃจ", + 300L, + "์ด์„ธ๊ณ„ ํฌํ„ธ์—์„œ ๋žœ๋คํ•œ ์•„์ดํ…œ์„ ํ•œ ๊ฐœ ๊บผ๋‚ด์˜จ๋‹ค. ๋ฌด์—‡์ด ๋“ค์–ด์žˆ์„ ์ง€๋Š” ์•„๋ฌด๋„ ๋ชจ๋ฅธ๋‹ค..." + ); + + piaItemRepository.save(piaItem); + } + } + + + + + + private PiaItem initializePiaItem(String name, Long price, String desc) { + PiaItem piaItem = new PiaItem(); + piaItem.setName(name); + piaItem.setPrice(price); + piaItem.setDescription(desc); + return piaItem; + } +} diff --git a/src/main/java/com/scriptopia/demo/config/RestTemplateConfig.java b/src/main/java/com/scriptopia/demo/config/RestTemplateConfig.java new file mode 100644 index 00000000..d4647fd7 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/RestTemplateConfig.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.config; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder.build(); + } +} diff --git a/src/main/java/com/scriptopia/demo/config/SecurityConfig.java b/src/main/java/com/scriptopia/demo/config/SecurityConfig.java new file mode 100644 index 00000000..0ce22348 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/SecurityConfig.java @@ -0,0 +1,108 @@ +package com.scriptopia.demo.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.scriptopia.demo.dto.exception.ErrorResponse; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.utils.JwtProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +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 +@EnableWebSecurity +@RequiredArgsConstructor +@EnableMethodSecurity +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + private final JwtAuthFilter jwtAuthFilter; + + @Bean + 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 + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + //public ๊ถŒํ•œ + .requestMatchers(SecurityWhitelist.AUTH_WHITELIST).permitAll() + //public ๊ถŒํ•œ(GET ์š”์ฒญ) + .requestMatchers(HttpMethod.GET,SecurityWhitelist.PUBLIC_GETS).permitAll() + .requestMatchers(publicSharedGameUuidGet).permitAll() + + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(ex -> ex + .authenticationEntryPoint((req, res, e) -> { + res.setStatus(ErrorCode.E_401.getStatus().value()); + res.setContentType(MediaType.APPLICATION_JSON_VALUE); + res.setCharacterEncoding("UTF-8"); + new ObjectMapper().writeValue(res.getOutputStream(), + new ErrorResponse(ErrorCode.E_401)); + }) + .accessDeniedHandler((req, res, e) -> { + res.setStatus(ErrorCode.E_403.getStatus().value()); + res.setContentType(MediaType.APPLICATION_JSON_VALUE); + res.setCharacterEncoding("UTF-8"); + new ObjectMapper().writeValue(res.getOutputStream(), + new ErrorResponse(ErrorCode.E_403)); + }) + ); + return http.build(); + } + + @Bean + public UrlBasedCorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + /* + * ๋กœ์ปฌ ํ…Œ์ŠคํŠธ์šฉ + */ + 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")); // ํ•„์š”์‹œ ๋…ธ์ถœํ•  ํ—ค๋” + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java b/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java new file mode 100644 index 00000000..4ea93762 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/SecurityWhitelist.java @@ -0,0 +1,31 @@ +package com.scriptopia.demo.config; + + +public class SecurityWhitelist { + public static final String[] AUTH_WHITELIST = { + "/", + + "/error", + + "/auth/logout", + "/auth/login", + "/auth/register", + "/auth/email/**", + "/auth/password/reset/**", + + "/oauth/**", + + "/v3/api-docs/**", + "/swagger-ui/**", + + "/shops/pia/items", + "/token/refresh" + + }; + + public static final String[] PUBLIC_GETS = { + "/trades", + "/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/WebConfig.java b/src/main/java/com/scriptopia/demo/config/WebConfig.java new file mode 100644 index 00000000..fb9a1d5b --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/WebConfig.java @@ -0,0 +1,21 @@ +package com.scriptopia.demo.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + @Value("${image-dir}") + private String imageDir; + + @Value("${image-url-prefix:/images}") + private String imageUrlPrefix; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler(imageUrlPrefix + "/**") + .addResourceLocations("file:" + imageDir); + } +} diff --git a/src/main/java/com/scriptopia/demo/config/fastapi/FastApiClient.java b/src/main/java/com/scriptopia/demo/config/fastapi/FastApiClient.java new file mode 100644 index 00000000..64a6ae68 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/fastapi/FastApiClient.java @@ -0,0 +1,16 @@ +package com.scriptopia.demo.config.fastapi; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class FastApiClient { + + @Bean + public WebClient fastApiWebClient() { + return WebClient.builder() + .baseUrl("http://localhost:8000") + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/config/fastapi/FastApiEndpoint.java b/src/main/java/com/scriptopia/demo/config/fastapi/FastApiEndpoint.java new file mode 100644 index 00000000..8a2c84a3 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/fastapi/FastApiEndpoint.java @@ -0,0 +1,21 @@ +package com.scriptopia.demo.config.fastapi; + +public enum FastApiEndpoint { + INIT("/games/init"), + CHOICE("/games/choice"), + BATTLE("/games/battle"), + ITEM("/games/item"), + DONE("/games/done"), + END("/games/end"), + TITLE("/games/title"); + + private final String path; + + FastApiEndpoint(String path) { + this.path = path; + } + + public String getPath() { + return path; + } +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/config/jwtConfig.java b/src/main/java/com/scriptopia/demo/config/jwtConfig.java new file mode 100644 index 00000000..850d7709 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/config/jwtConfig.java @@ -0,0 +1,9 @@ +package com.scriptopia.demo.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(JwtProperties.class) +public class jwtConfig { +} diff --git a/src/main/java/com/scriptopia/demo/controller/AuctionController.java b/src/main/java/com/scriptopia/demo/controller/AuctionController.java new file mode 100644 index 00000000..6e25b7b5 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/controller/AuctionController.java @@ -0,0 +1,106 @@ +package com.scriptopia.demo.controller; + + +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; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +@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) { + + TradeResponse response = auctionService.getTrades(requestDto); + return ResponseEntity.ok(response); + + } + + @Operation(summary = "๊ฒฝ๋งค์žฅ ์•„์ดํ…œ ๊ตฌ๋งค") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @PostMapping("/{auctionId}/purchase") + public ResponseEntity purchaseItem( + @PathVariable String auctionId, + Authentication authentication) { + + + Long userId = Long.valueOf(authentication.getName()); + String result = auctionService.purchaseItem(auctionId, userId); + return ResponseEntity.ok(result); + } + + @Operation(summary = "๋‚ด๊ฐ€ ๋“ฑ๋กํ•œ ํŒ๋งค ์•„์ดํ…œ ์กฐํšŒ") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @GetMapping("/me") + public ResponseEntity mySaleItems( + @RequestBody MySaleItemRequest requestDto, + Authentication authentication) { + + + Long userId = Long.valueOf(authentication.getName()); + MySaleItemResponse result = auctionService.getMySaleItems(userId, requestDto); + return ResponseEntity.ok(result); + } + + @Operation(summary = "๊ฒฝ๋งค์žฅ ์•„์ดํ…œ ํŒ๋งค ๋“ฑ๋ก") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @PostMapping + public ResponseEntity createAuction(@RequestBody AuctionRequest dto, + Authentication authentication ){ + + Long userId = Long.valueOf(authentication.getName()); + return ResponseEntity.ok(auctionService.createAuction(dto, userId)); + } + + @Operation(summary = "ํŒ๋งค ์ค‘์ธ ์•„์ดํ…œ ๋“ฑ๋ก ์ทจ์†Œ") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @DeleteMapping("/{auctionId}") + public ResponseEntity cancelMySaleItem( + @PathVariable String auctionId, + Authentication authentication) { + + Long userId = Long.valueOf(authentication.getName()); + String result = auctionService.cancelMySaleItem(userId, auctionId); + return ResponseEntity.ok(result); + } + + @Operation(summary = "๋‚ด ๊ฑฐ๋ž˜ ๊ธฐ๋ก ์กฐํšŒ(์ •์‚ฐ ํ…Œ์ด๋ธ” ์กฐํšŒ)") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @GetMapping("/me/history") + public ResponseEntity settlementHistory( + @RequestBody SettlementHistoryRequest requestDto, + Authentication authentication) { + + + Long userId = Long.valueOf(authentication.getName()); + SettlementHistoryResponse result = auctionService.settlementHistory(userId, requestDto); + return ResponseEntity.ok(result); + } + + @Operation(summary = "๊ตฌ๋งค ์•„์ดํ…œ/ํŒ๋งค ๋Œ€๊ธˆ ์ˆ˜๋ น") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @PostMapping("/{settlementId}/confirm") + public ResponseEntity confirmItem( + @PathVariable String settlementId, + Authentication authentication) { + + + Long userId = Long.valueOf(authentication.getName()); + String result = auctionService.confirmItem(settlementId, userId); + return ResponseEntity.ok(result); + } + +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/controller/AuthController.java b/src/main/java/com/scriptopia/demo/controller/AuthController.java new file mode 100644 index 00000000..2053d3b9 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/controller/AuthController.java @@ -0,0 +1,123 @@ +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; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/auth") +@Tag(name = "๋กœ์ปฌ ์ธ์ฆ API", description = "๋กœ์ปฌ ์ธ์ฆ ๊ด€๋ จ API ์ž…๋‹ˆ๋‹ค.") +@RequiredArgsConstructor +public class AuthController { + private final LocalAccountService localAccountService; + private final RefreshTokenService refreshTokenService; + private static final String RT_COOKIE = "RT"; + private static final boolean COOKIE_SECURE = true; + private static final String COOKIE_SAMESITE = "None"; + + + @Operation(summary = "๋กœ๊ทธ์•„์›ƒ") + @PostMapping("/logout") + public ResponseEntity logout( + @CookieValue(name = RT_COOKIE, required = false) String refreshToken, + HttpServletResponse response + ) { + if (refreshToken != null && !refreshToken.isBlank()) { + refreshTokenService.logout(refreshToken); + } + response.addHeader(HttpHeaders.SET_COOKIE, localAccountService.removeRefreshCookie().toString()); + return ResponseEntity.ok(new CommonResponse("๋กœ๊ทธ์•„์›ƒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")); + } + + @Operation(summary = "๋กœ์ปฌ ๋กœ๊ทธ์ธ") + @PostMapping("/login") + public ResponseEntity login( + @RequestBody @Valid LoginRequest req, + HttpServletRequest request, + HttpServletResponse response + ) { + + return ResponseEntity.ok(localAccountService.login(req, request, response)); + } + + @Operation(summary = "๋กœ์ปฌ ๊ณ„์ • ํšŒ์›๊ฐ€์ž…") + @PostMapping("/register") + public ResponseEntity register( + + @RequestBody @Valid RegisterRequest req, + HttpServletRequest request, + HttpServletResponse response + ) { + + return ResponseEntity.status(HttpStatus.CREATED).body(localAccountService.register(req, request, response)); + } + + @Operation(summary = "์ด๋ฉ”์ผ ์ค‘๋ณต ๊ฒ€์ฆ") + @PostMapping("/email/verify") + public ResponseEntity verifyEmail(@Valid @RequestBody VerifyEmailRequest request) { + + localAccountService.verifyEmail(request); + + return ResponseEntity.ok(new CommonResponse("์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.")); + } + + @Operation(summary = "์ด๋ฉ”์ผ ์ธ์ฆ ์ฝ”๋“œ ์ „์†ก") + @PostMapping("/email/code/send") + public ResponseEntity sendCode(@RequestBody @Valid SendCodeRequest request) { + localAccountService.sendVerificationCode(request.getEmail()); + return ResponseEntity.ok(new CommonResponse("์ธ์ฆ ์ฝ”๋“œ๊ฐ€ ์ด๋ฉ”์ผ๋กœ ๋ฐœ์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")); + } + + @Operation(summary = "์ด๋ฉ”์ผ ์ธ์ฆ ์ฝ”๋“œ ํ™•์ธ") + @PostMapping("/email/code/verify") + public ResponseEntity verifyCode(@RequestBody @Valid VerifyCodeRequest request) { + localAccountService.verifyCode(request.getEmail(), request.getCode()); + return ResponseEntity.ok(new CommonResponse("์ด๋ฉ”์ผ ์ธ์ฆ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")); + } + + @Operation(summary = "๋น„๋ฐ€๋ฒˆํ˜ธ ์ดˆ๊ธฐํ™” ๋งํฌ ๋ฐœ์†ก") + @PostMapping("/password/reset/send") + public ResponseEntity sendResetMail(@Valid @RequestBody SendCodeRequest request){ + + localAccountService.sendResetPasswordMail(request.getEmail()); + + return ResponseEntity.ok(new CommonResponse("๋น„๋ฐ€๋ฒˆํ˜ธ ์ดˆ๊ธฐํ™” ๋งํฌ๋ฅผ ์ „์†กํ–ˆ์Šต๋‹ˆ๋‹ค.")); + } + + @Operation(summary = "๋น„๋ฐ€๋ฒˆํ˜ธ ์ดˆ๊ธฐํ™”") + @PatchMapping("/password/reset") + public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { + localAccountService.resetPassword(request.getToken(), request.getNewPassword()); + + return ResponseEntity.ok(new CommonResponse("๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")); + } + + @Operation(summary = "๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @PatchMapping("/password/change") + public ResponseEntity changePassword(@RequestBody @Valid ChangePasswordRequest request, + Authentication authentication) { + + Long userId = Long.valueOf(authentication.getName()); + + localAccountService.changePassword(userId,request); + + return ResponseEntity.ok(new CommonResponse("๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")); + } + + + +} diff --git a/src/main/java/com/scriptopia/demo/controller/ErrorController.java b/src/main/java/com/scriptopia/demo/controller/ErrorController.java new file mode 100644 index 00000000..a45b231d --- /dev/null +++ b/src/main/java/com/scriptopia/demo/controller/ErrorController.java @@ -0,0 +1,33 @@ +package com.scriptopia.demo.controller; + +import com.scriptopia.demo.dto.exception.ErrorResponse; +import com.scriptopia.demo.exception.ErrorCode; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ErrorController implements org.springframework.boot.web.servlet.error.ErrorController { + + @RequestMapping("/error") + public ResponseEntity handleError(HttpServletRequest request) { + Object statusCode = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); + + if (statusCode != null) { + int status = Integer.parseInt(statusCode.toString()); + + if (status == HttpStatus.NOT_FOUND.value()) { + return ResponseEntity + .status(ErrorCode.E_404.getStatus()) + .body(new ErrorResponse(ErrorCode.E_404)); + } + } + + return ResponseEntity + .status(ErrorCode.E_500.getStatus()) + .body(new ErrorResponse(ErrorCode.E_500)); + } +} diff --git a/src/main/java/com/scriptopia/demo/controller/GameSessionController.java b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java new file mode 100644 index 00000000..50c0b01d --- /dev/null +++ b/src/main/java/com/scriptopia/demo/controller/GameSessionController.java @@ -0,0 +1,188 @@ +package com.scriptopia.demo.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +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 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; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/games") +@Tag(name = "๊ฒŒ์ž„ ์„ธ์…˜ API", description = "๊ฒŒ์ž„ ์„ธ์…˜ ๊ด€๋ จ API ์ž…๋‹ˆ๋‹ค.") +@RequiredArgsConstructor +public class GameSessionController { + + private final GameSessionService gameSessionService; + + /* + * ๊ฒŒ์ž„ -> ๊ฒŒ์ž„ ๋„์ค‘ ์ข…๋ฃŒ + */ + @Operation(summary = "์ €์žฅ ํ›„ ๊ฒŒ์ž„ ์ข…๋ฃŒ") + @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") + @PostMapping("/exit") + public ResponseEntity createGameSession(Authentication authentication, @RequestBody GameSessionRequest request) { + Long userId = Long.valueOf(authentication.getName()); + return gameSessionService.saveGameSession(userId, request.getGameId()); + } + + /* + * ๊ฒŒ์ž„ -> ๊ธฐ์กด ๊ฒŒ์ž„ ์‚ญ์ œ + */ + @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( + @RequestBody StartGameRequest request, + Authentication authentication) throws JsonProcessingException { + + Long userId = Long.valueOf(authentication.getName()); + + StartGameResponse response = gameSessionService.startNewGame(userId, request); + return ResponseEntity.ok(response); + } + + @Operation(summary = "๊ฒŒ์ž„ ์ง„์ž…") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @GetMapping("/{gameId}") + public ResponseEntity getInGameData( + @PathVariable("gameId") String gameId, + Authentication authentication) throws JsonProcessingException { + + Long userId = Long.valueOf(authentication.getName()); + + // service์—์„œ sceneType๋ณ„ DTO๋ฅผ ๋ฐ˜ํ™˜ + Object response = gameSessionService.getInGameDataDto(userId); + + return ResponseEntity.ok(response); + } + + @Operation(summary = "์„ ํƒ์ง€ ์„ ํƒ") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @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.gameChoiceSelect(userId, request); + + return ResponseEntity.ok(response); + } + + @Operation(summary = "๊ฒŒ์ž„ ์ง„ํ–‰") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @PostMapping("/{gameId}/progress") + public ResponseEntity keepGame( + @PathVariable("gameId") String gameId, + Authentication authentication) throws JsonProcessingException { + + Long userId = Long.valueOf(authentication.getName()); + + GameSessionMongo response = gameSessionService.gameProgress(userId); + return ResponseEntity.ok(response); + } + + + @Operation(summary = "์•„์ดํ…œ ์žฅ์ฐฉ/ํ•ด์ œ") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @PostMapping("/equipItem/{gameId}/{itemId}") + public ResponseEntity equipItem( + @PathVariable("gameId") String gameId, + @PathVariable("itemId") String itemId, + Authentication authentication) throws JsonProcessingException { + + Long userId = Long.valueOf(authentication.getName()); + + GameSessionMongo response = gameSessionService.gameEquipItem(userId, itemId); + + return ResponseEntity.ok(response); + } + + @Operation(summary = "์•„์ดํ…œ ๋ฒ„๋ฆฌ๊ธฐ") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @DeleteMapping("/dropItem/{gameId}/{itemId}") + public ResponseEntity dropItem( + @PathVariable("gameId") String gameId, + @PathVariable("itemId") String itemId, + Authentication authentication) throws JsonProcessingException { + + Long userId = Long.valueOf(authentication.getName()); + + GameSessionMongo response = gameSessionService.gameDropItem(userId, itemId); + + 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 { + + Long userId = Long.valueOf(authentication.getName()); + + 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); + } + + + /* + * ๊ฒŒ์ž„ ์ข…๋ฃŒ ํ›„ -> ํžˆ์Šคํ† ๋ฆฌ ์ƒ์„ฑ + */ + @Operation(summary = "๊ฒŒ์ž„ ์ข…๋ฃŒ ํ›„ ํžˆ์Šคํ† ๋ฆฌ ์ €์žฅ") + @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") + @PostMapping("/{gameId}/history") + public ResponseEntity addHistory( + Authentication authentication, + @PathVariable("gameId") String gameId) { + Long userId = Long.valueOf(authentication.getName()); + return gameSessionService.gameToEnd(userId); + } + +} diff --git a/src/main/java/com/scriptopia/demo/controller/ItemController.java b/src/main/java/com/scriptopia/demo/controller/ItemController.java new file mode 100644 index 00000000..b872fc3d --- /dev/null +++ b/src/main/java/com/scriptopia/demo/controller/ItemController.java @@ -0,0 +1,37 @@ +package com.scriptopia.demo.controller; + +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; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +@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( + Authentication authentication, + @RequestBody ItemDefRequest request + ) { + String userId = authentication.getName(); + ItemDTO itemInWeb = itemService.createItemInWeb(userId, request); + return ResponseEntity.ok(itemInWeb); + } + + + + +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/controller/OAuthController.java b/src/main/java/com/scriptopia/demo/controller/OAuthController.java new file mode 100644 index 00000000..dcb73650 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/controller/OAuthController.java @@ -0,0 +1,51 @@ +package com.scriptopia.demo.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +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; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@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, + @RequestParam("code") String code, + HttpServletRequest request, + HttpServletResponse response + ) throws JsonProcessingException { + OAuthLoginResponse result = oAuthService.login(provider, code, request, response); + return ResponseEntity.ok(result); + } + @Operation(summary = "์†Œ์…œ ํšŒ์›๊ฐ€์ž…") + @PostMapping("/register") + public ResponseEntity signup( + @RequestBody SocialSignupRequest req, + HttpServletRequest request, + HttpServletResponse response + ) { + OAuthLoginResponse result = oAuthService.signup(req, request, response); + return ResponseEntity.ok(result); + } + +} diff --git a/src/main/java/com/scriptopia/demo/controller/PiaShopController.java b/src/main/java/com/scriptopia/demo/controller/PiaShopController.java new file mode 100644 index 00000000..6690cfa5 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/controller/PiaShopController.java @@ -0,0 +1,80 @@ +package com.scriptopia.demo.controller; + +import com.scriptopia.demo.dto.items.ItemDTO; +import com.scriptopia.demo.dto.items.ItemDefRequest; +import com.scriptopia.demo.dto.piashop.PiaItemRequest; +import com.scriptopia.demo.dto.piashop.PiaItemResponse; +import com.scriptopia.demo.dto.piashop.PiaItemUpdateRequest; +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; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@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) { + piaShopService.createPiaItem(request); + return ResponseEntity.ok("PIA ์•„์ดํ…œ์ด ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + } + + @Operation(summary = "PIA ์ƒํ’ˆ ์ˆ˜์ •") + @PreAuthorize("hasAnyAuthority('ADMIN')") + @PutMapping("/items/pia/{itemId}") + public ResponseEntity updatePiaItem( + @PathVariable("itemId") String itemId, + @RequestBody PiaItemUpdateRequest requestDto) { + + + String result = piaShopService.updatePiaItem(itemId, requestDto); + 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( + @RequestBody PurchasePiaItemRequest requestDto, + Authentication authentication) { + + Long userId = Long.valueOf(authentication.getName()); + piaShopService.purchasePiaItem(userId, requestDto); + return ResponseEntity.ok("PIA ์•„์ดํ…œ์„ ๊ตฌ๋งคํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + + @Operation(summary = "์•„์ดํ…œ ๋ชจ๋ฃจ ์‚ฌ์šฉ") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @PostMapping("/pia/items/anvil") + public ResponseEntity useItemAnvil( + @RequestBody ItemDefRequest request, + Authentication authentication) { + + ItemDTO response = piaShopService.useItemAnvil(authentication.getName(), request); + + return ResponseEntity.ok(response); + } + + +} diff --git a/src/main/java/com/scriptopia/demo/controller/SharedGameController.java b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java new file mode 100644 index 00000000..e883d7a1 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/controller/SharedGameController.java @@ -0,0 +1,124 @@ +package com.scriptopia.demo.controller; + +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.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; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/shared-games") +@Tag(name = "๊ฒŒ์ž„ ๊ณต์œ  ๊ด€๋ จ API", description = "๊ฒŒ์ž„ ๊ณต์œ  ๊ด€๋ จ API ์ž…๋‹ˆ๋‹ค.") +@RequiredArgsConstructor +public class SharedGameController { + private final SharedGameService sharedGameService; + private final SharedGameFavoriteService sharedGameFavoriteService; + private final TagDefService tagDefService; + + /* + ๊ฒŒ์ž„ ๊ณต์œ  -> ๊ฒŒ์ž„ ๊ณต์œ ํ•˜๊ธฐ + */ + + @Operation(summary = "๊ฒŒ์ž„ ๊ณต์œ ํ•˜๊ธฐ") + @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") + @PostMapping("/{sharedGameUuid}") + public ResponseEntity share(Authentication authentication, @PathVariable("sharedGameUuid") UUID sharedGameUuid) { + Long userId = Long.valueOf(authentication.getName()); + + return sharedGameService.saveSharedGame(userId, sharedGameUuid); + } + + /* + ๊ฒŒ์ž„ ๊ณต์œ  -> ๊ณต์œ  ๊ฒŒ์ž„ ๋ชฉ๋ก ์กฐํšŒ + */ + @Operation(summary = "๊ณต์œ  ๊ฒŒ์ž„ ๋ชฉ๋ก ์กฐํšŒ") + @GetMapping + 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); + } + + /* + ๊ฒŒ์ž„๊ณต์œ  : ๊ณต์œ ๋œ ๊ฒŒ์ž„ ์ƒ์„ธ ์กฐํšŒ + */ + @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("{sharedGameUuid}/like") + public ResponseEntity likeSharedGame(@PathVariable("sharedGameUuid") UUID sharedGameId, Authentication authentication) { + Long userId = Long.valueOf(authentication.getName()); + + return sharedGameFavoriteService.saveFavorite(userId, sharedGameId); + } + + /* + ๊ฒŒ์ž„๊ณต์œ  : ๊ณต์œ ๋œ ๊ฒŒ์ž„ ํƒœ๊ทธ ์กฐํšŒ + */ + @Operation(summary = "๊ฒŒ์ž„ ํƒœ๊ทธ ์กฐํšŒ") + @GetMapping("/tags") + public ResponseEntity getSharedGameTags() { + return sharedGameService.getTag(); + } + + /* + ๊ฒŒ์ž„ ๊ณต์œ  -> ๊ณต์œ ํ•œ ๊ฒŒ์ž„ ์‚ญ์ œ + */ + @Operation(summary = "๊ณต์œ ํ•œ ๊ฒŒ์ž„ ์‚ญ์ œ") + @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") + @DeleteMapping("/{sharedGameUuid}") + public ResponseEntity delete(Authentication authentication, @PathVariable("sharedGameUuid") UUID sharedGameUuid) { + Long userId = Long.valueOf(authentication.getName()); + + sharedGameService.deleteSharedGame(userId, sharedGameUuid); + + return ResponseEntity.ok("๊ฒŒ์ž„์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + } + + @Operation(summary = "๊ฒŒ์ž„ ํƒœ๊ทธ ์ƒ์„ฑ") + @PreAuthorize("hasAnyAuthority('ADMIN')") + @PostMapping("/tags") + public ResponseEntity addTag(@RequestBody TagDefCreateRequest req) { + + return tagDefService.addTagName(req); + } + + @Operation(summary = "๊ฒŒ์ž„ ํƒœ๊ทธ ์‚ญ์ œ ") + @PreAuthorize("hasAnyAuthority('ADMIN')") + @DeleteMapping("/tags") + public ResponseEntity removeTag(@RequestBody TagDefDeleteRequest req) { + return tagDefService.removeTagName(req); + } +} diff --git a/src/main/java/com/scriptopia/demo/controller/TestEnvController.java b/src/main/java/com/scriptopia/demo/controller/TestEnvController.java new file mode 100644 index 00000000..a7a11459 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/controller/TestEnvController.java @@ -0,0 +1,18 @@ +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; +import org.springframework.web.bind.annotation.RequestParam; + + +@Controller +@Tag(name = "๋ฐฑ์—”๋“œ ์ •์  ํŽ˜์ด์ง€ ๊ด€๋ จ API", description = "๋ฐฑ์—”๋“œ ์ •์  ํŽ˜์ด์ง€ ๊ด€๋ จ API ์ž…๋‹ˆ๋‹ค.") +public class TestEnvController { + @GetMapping("/") + public String mainPage() { + return "index"; // templates/index.html + } + +} diff --git a/src/main/java/com/scriptopia/demo/controller/UserController.java b/src/main/java/com/scriptopia/demo/controller/UserController.java new file mode 100644 index 00000000..7489fc07 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/controller/UserController.java @@ -0,0 +1,124 @@ +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.*; +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 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 UserCharacterImgService userCharacterImgService; + + @Operation(summary = "๋ณด์œ  ์žฅ๋น„ ์•„์ดํ…œ ์กฐํšŒ") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @GetMapping("/items/game") + public ResponseEntity> getGameItems( + Authentication authentication + ) { + String userId = authentication.getName(); + List response = userService.getGameItems(userId); + return ResponseEntity.ok(response); + } + + @Operation(summary = "๋ณด์œ  ํ”ผ์•„ ์•„์ดํ…œ ์กฐํšŒ") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @GetMapping("/items/pia") + public ResponseEntity> getPiaItems( + Authentication authentication + ) { + String userId = authentication.getName(); + List response = userService.getPiaItems(userId); + return ResponseEntity.ok(response); + } + + @Operation(summary = "์‚ฌ์šฉ์ž ์˜ต์…˜ ์กฐํšŒ") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @GetMapping("/settings") + public ResponseEntity getUserSettings( + Authentication authentication + ) { + String userId = authentication.getName(); + UserSettingsDTO response = userService.getUserSettings(userId); + return ResponseEntity.ok(response); + } + + @Operation(summary = "์‚ฌ์šฉ์ž ์˜ต์…˜ ๋ณ€๊ฒฝ") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @PutMapping("/settings") + public ResponseEntity updateUserSettings( + Authentication authentication, + @RequestBody @Valid UserSettingsDTO request + ) { + String userId = authentication.getName(); + userService.updateUserSettings(userId,request); + return ResponseEntity.ok("์‚ฌ์šฉ์ž ์„ค์ •์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + } + + @Operation(summary = "์‚ฌ์šฉ์ž ์žฌํ™” ์กฐํšŒ") + @PreAuthorize("hasAnyAuthority('USER','ADMIN')") + @GetMapping("/assets") + public ResponseEntity getUserAssets( + Authentication authentication + ) { + String userId = authentication.getName(); + UserAssetsResponse response = userService.getUserAssets(userId); + return ResponseEntity.ok(response); + } + + @Operation(summary = "์‚ฌ์šฉ์ž ๊ฒŒ์ž„ ๊ธฐ๋ก ์กฐํšŒ") + @GetMapping("/games/histories") + public ResponseEntity getHistory(@RequestParam(required = false) UUID lastId, + @RequestParam(defaultValue = "10") int size, + Authentication authentication) { + Long userId = Long.valueOf(authentication.getName()); + + return ResponseEntity.ok(userService.fetchMyHistory(userId, lastId, size)); + } + + @Operation(summary = "ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ๋ณ€๊ฒฝ") + @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") + @PostMapping("/profile-images") + public ResponseEntity saveUserCharacterImg(Authentication authentication, @RequestBody UserImageRequest req) { + Long userId = Long.valueOf(authentication.getName()); + + return userCharacterImgService.saveUserCharacterImg(userId, req.getUrl()); + } + + @Operation(summary = "ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์กฐํšŒ") + @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") + @GetMapping("/profile-images") + public ResponseEntity getUserCharacterImgs(Authentication authentication) { + Long userId = Long.valueOf(authentication.getName()); + + return userCharacterImgService.getUserCharacterImg(userId); + } + + @Operation(summary = "์‚ฌ์šฉ์ž ํ—ค๋” ์ •๋ณด ์กฐํšŒ") + @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')") + @GetMapping("/status") + public ResponseEntity getUserStatus(Authentication authentication) { + Long userId = Long.valueOf(authentication.getName()); + + return ResponseEntity.ok(userService.getUserStatus(userId)); + } + +} diff --git a/src/main/java/com/scriptopia/demo/controller/refreshController.java b/src/main/java/com/scriptopia/demo/controller/refreshController.java new file mode 100644 index 00000000..1bd8d777 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/controller/refreshController.java @@ -0,0 +1,66 @@ +package com.scriptopia.demo.controller; + + +import com.scriptopia.demo.config.JwtProperties; +import com.scriptopia.demo.dto.auth.RefreshResponse; +import com.scriptopia.demo.dto.token.RefreshRequest; +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; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.time.Duration; +import java.util.List; + +@RestController +@RequestMapping("/token") +@Tag(name = "๋ฆฌํ”„๋ ˆ์‰ฌ ํ† ํฐ ๊ด€๋ จ API", description = "๋ฆฌํ”„๋ ˆ์‰ฌ ํ† ํฐ ๊ด€๋ จ API ์ž…๋‹ˆ๋‹ค.") +@RequiredArgsConstructor +public class refreshController { + + private final LocalAccountService localAccountService; + private final JwtProvider jwt; + private final RefreshTokenService refreshTokenService; + private final JwtProperties props; + + private static final String RT_COOKIE = "RT"; + private static final boolean COOKIE_SECURE = false; + private static final String COOKIE_SAMESITE = "Lax"; + + @Operation(summary = "๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์žฌ๋ฐœ๊ธ‰") + @PostMapping("/refresh") + public ResponseEntity refresh( + @CookieValue(name = RT_COOKIE, required = false) String refreshToken, + @RequestBody RefreshRequest request + ) { + if (refreshToken == null || refreshToken.isBlank()) { + return ResponseEntity.status(401).build(); + } + Long userId = jwt.getUserId(refreshToken); + List roles = localAccountService.getRoles(userId); + + var pair = refreshTokenService.rotate(refreshToken, request.getDeviceId(), roles); + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, refreshCookie(pair.refreshToken()).toString()) + .body(new RefreshResponse(pair.accessToken(), props.accessExpSeconds())); + } + + private ResponseCookie refreshCookie(String value) { + return ResponseCookie.from(RT_COOKIE, value) + .httpOnly(true) + .secure(COOKIE_SECURE) + .sameSite(COOKIE_SAMESITE) + .path("/") + .maxAge(Duration.ofDays(14)) + .build(); + } + + +} diff --git a/src/main/java/com/scriptopia/demo/domain/Chapter.java b/src/main/java/com/scriptopia/demo/domain/Chapter.java new file mode 100644 index 00000000..5af98675 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/Chapter.java @@ -0,0 +1,21 @@ +package com.scriptopia.demo.domain; + + +import lombok.Getter; + +@Getter +public enum Chapter { + CHAPTER1(new int[]{14,16,14,12,10,9,8,7,6,4,1,1}), + CHAPTER2(new int[]{8,9,9,12,12,12,11,10,10,7,5,5}), + CHAPTER3(new int[]{4,5,6,7,9,11,13,13,12,10,6,7}); + + private final int[] npcProbabilities; + + Chapter(int[] npcProbabilities) { + this.npcProbabilities = npcProbabilities; + } + + public int[] getNpcProbabilities() { + return npcProbabilities; + } +} diff --git a/src/main/java/com/scriptopia/demo/domain/ChoiceEventType.java b/src/main/java/com/scriptopia/demo/domain/ChoiceEventType.java new file mode 100644 index 00000000..2ee47a05 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/ChoiceEventType.java @@ -0,0 +1,36 @@ +package com.scriptopia.demo.domain; + +import lombok.Getter; + +import java.security.SecureRandom; + + +@Getter +public enum ChoiceEventType { + LIVING(50), + NONLIVING(50); + + + private final int ChoiceEventChance; + + + private static final SecureRandom random = new SecureRandom(); + + + ChoiceEventType(final int ChoiceEventChance) { + this.ChoiceEventChance = ChoiceEventChance; + } + + public static ChoiceEventType getChoiceEventType() { + int rand = random.nextInt(100) + 1; + int cumulative = 0; + + for (ChoiceEventType Choicetype : ChoiceEventType.values()) { + cumulative += Choicetype.getChoiceEventChance(); + if (cumulative >= rand) { + return Choicetype; + } + } + return NONLIVING; + } +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java b/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java new file mode 100644 index 00000000..85dcc74c --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/ChoiceResultType.java @@ -0,0 +1,42 @@ +package com.scriptopia.demo.domain; + +import lombok.Getter; + +import java.security.SecureRandom; + + +@Getter +public enum ChoiceResultType { + BATTLE(30), // 20, 40, 45, 5 + CHOICE(5), + DONE(60), + SHOP(5); + + private final int nextEventType; + + private static final SecureRandom random = new SecureRandom(); + + ChoiceResultType(int nextEventType) { + this.nextEventType = nextEventType; + } + + + public static ChoiceResultType nextResultType(ChoiceEventType nextEventType) { + int rand = 0; + if( nextEventType == ChoiceEventType.LIVING){ + rand = random.nextInt(100) + 1; // 1 ~ 100 + }else{ + rand = random.nextInt(60) + 41; // 41 ~ 100 + } + + int cumulative = 0; + + for(ChoiceResultType type : values()) { + cumulative += type.getNextEventType(); + if(rand <= cumulative ) { + return type; + } + } + return DONE; + } +} diff --git a/src/main/java/com/scriptopia/demo/domain/EffectGradeDef.java b/src/main/java/com/scriptopia/demo/domain/EffectGradeDef.java index 91a1b067..b3bfe213 100644 --- a/src/main/java/com/scriptopia/demo/domain/EffectGradeDef.java +++ b/src/main/java/com/scriptopia/demo/domain/EffectGradeDef.java @@ -18,5 +18,6 @@ public class EffectGradeDef { private Double weight; @Enumerated(EnumType.STRING) - private Grade grade; + private EffectProbability effectProbability; + } diff --git a/src/main/java/com/scriptopia/demo/domain/EffectProbability.java b/src/main/java/com/scriptopia/demo/domain/EffectProbability.java new file mode 100644 index 00000000..74b532ca --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/EffectProbability.java @@ -0,0 +1,75 @@ +package com.scriptopia.demo.domain; + +import lombok.Getter; + +import java.security.SecureRandom; + +@Getter +public enum EffectProbability { + + COMMON(80, 16, 4, 0, 0, 0), + UNCOMMON(75, 7, 12, 5, 0, 0), + RARE(65, 5, 5, 17, 7, 0), + EPIC(60, 4, 4, 4, 20, 8), + LEGENDARY(55, 1, 2, 7, 23, 12); + + private final int nullProb; + private final int commonProb; + private final int uncommonProb; + private final int rareProb; + private final int epicProb; + private final int legendaryProb; + + private static final SecureRandom random = new SecureRandom(); + + + + EffectProbability(int n, int c, int u, int r, int e, int l) { + this.nullProb = n; + this.commonProb = c; + this.uncommonProb = u; + this.rareProb = r; + this.epicProb = e; + this.legendaryProb = l; + } + + /** + * @param weaponGrade + * @return + */ + public static EffectProbability getRandomEffectGradeByWeaponGrade(Grade weaponGrade) { + EffectProbability prob = null; + + switch (weaponGrade) { + case COMMON -> prob = EffectProbability.COMMON; + case UNCOMMON -> prob = EffectProbability.UNCOMMON; + case RARE -> prob = EffectProbability.RARE; + case EPIC -> prob = EffectProbability.EPIC; + case LEGENDARY -> prob = EffectProbability.LEGENDARY; + } + + int[] probabilities = { + prob.getNullProb(), + prob.getCommonProb(), + prob.getUncommonProb(), + prob.getRareProb(), + prob.getEpicProb(), + prob.getLegendaryProb() + }; + + EffectProbability[] grades = {null, EffectProbability.COMMON, EffectProbability.UNCOMMON, EffectProbability.RARE, EffectProbability.EPIC, EffectProbability.LEGENDARY}; + + int rand = random.nextInt(100) + 1; // 1~100 + int cumulative = 0; + + for (int i = 0; i < probabilities.length; i++) { + cumulative += probabilities[i]; + if (rand <= cumulative) { + return grades[i]; + } + } + + return null; + } + +} diff --git a/src/main/java/com/scriptopia/demo/domain/FontType.java b/src/main/java/com/scriptopia/demo/domain/FontType.java new file mode 100644 index 00000000..0b0db9af --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/FontType.java @@ -0,0 +1,5 @@ +package com.scriptopia.demo.domain; + +public enum FontType { + F1,F2, PretendardVariable +} diff --git a/src/main/java/com/scriptopia/demo/domain/GameTag.java b/src/main/java/com/scriptopia/demo/domain/GameTag.java index d06c935d..6501657e 100644 --- a/src/main/java/com/scriptopia/demo/domain/GameTag.java +++ b/src/main/java/com/scriptopia/demo/domain/GameTag.java @@ -14,6 +14,9 @@ public class GameTag { @GeneratedValue private Long id; + @ManyToOne(fetch = FetchType.LAZY) + private SharedGame sharedGame; + @ManyToOne(fetch = FetchType.LAZY) private TagDef tagDef; } diff --git a/src/main/java/com/scriptopia/demo/domain/Grade.java b/src/main/java/com/scriptopia/demo/domain/Grade.java index 172bd74a..0b6c6783 100644 --- a/src/main/java/com/scriptopia/demo/domain/Grade.java +++ b/src/main/java/com/scriptopia/demo/domain/Grade.java @@ -1,9 +1,71 @@ package com.scriptopia.demo.domain; +import lombok.Getter; + +import java.security.SecureRandom; + +@Getter public enum Grade { - COMMON, - UNCOMMON, - RARE, - EPIC, - LEGENDARY + COMMON(30, 137, 50), + UNCOMMON(35, 177, 30), + RARE(40, 216, 15), + EPIC(45, 260, 4), + LEGENDARY(50, 312, 1); + + private final int attackPower; + private final int defensePower; + private final int dropRate; // ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•œ ํ•„๋“œ + + Grade(int attackPower, int defensePower, int dropRate) { + this.attackPower = attackPower; + this.defensePower = defensePower; + this.dropRate = dropRate; + } + + private static final SecureRandom random = new SecureRandom(); + + /** + * ์•„์ดํ…œ, ์•„์ดํ…œ ํšจ๊ณผ + * Grade ์ค‘ ํ•˜๋‚˜๋ฅผ ๋žœ๋ค์œผ๋กœ ๋ฐ˜ํ™˜ + */ + public static Grade getRandomGradeByProbability() { + int rand = random.nextInt(100) + 1; // 1~100 + int cumulative = 0; + + for (Grade grade : Grade.values()) { + cumulative += grade.getDropRate(); + if (rand <= cumulative) { + return grade; + } + } + return LEGENDARY; + } + + /** + * ์•„์ดํ…œ ์ข…๋ฅ˜, ๋“ฑ๊ธ‰์— ๋”ฐ๋ฅธ ๊ธฐ๋ณธ ์„ฑ๋Šฅ์„ ๋ฆฌํ„ด + * ๊ธฐ๋ณธ ๊ณต๊ฒฉ๋ ฅ์— +- 10% + */ + public static int getRandomBaseStat(ItemType itemType, Grade grade) { + if (itemType == ItemType.WEAPON){ + return getRandomBaseStatWeapon(grade); + } + return getRandomBaseStatArmor(grade); + } + + + // ยฑ10% ๋žœ๋ค ๊ณต๊ฒฉ๋ ฅ + public static int getRandomBaseStatWeapon(Grade grade) { + int base = grade.getAttackPower(); + int delta = (int) (base * 0.1); + return base - delta + random.nextInt(2 * delta + 1); + } + + // ยฑ10% ๋žœ๋ค ๋ฐฉ์–ด๋ ฅ + public static int getRandomBaseStatArmor(Grade grade) { + int base = grade.getDefensePower(); + int delta = (int) (base * 0.1); + return base - delta + random.nextInt(2 * delta + 1); + } + + } diff --git a/src/main/java/com/scriptopia/demo/domain/History.java b/src/main/java/com/scriptopia/demo/domain/History.java index bc0d3199..e89d9c4d 100644 --- a/src/main/java/com/scriptopia/demo/domain/History.java +++ b/src/main/java/com/scriptopia/demo/domain/History.java @@ -1,36 +1,87 @@ package com.scriptopia.demo.domain; +import com.scriptopia.demo.dto.history.HistoryRequest; import jakarta.persistence.*; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import java.time.LocalDateTime; +import java.util.UUID; @Entity @Getter @Setter +@NoArgsConstructor public class History { @Id @GeneratedValue private Long id; + @Column(nullable = false, unique = true) + private UUID uuid; + @ManyToOne(fetch = FetchType.LAZY) private User user; private String thumbnailUrl; + + @Column(columnDefinition = "TEXT") private String title; + + @Column(columnDefinition = "TEXT") private String worldView; + + @Column(columnDefinition = "TEXT") private String backgroundStory; + + @Column(columnDefinition = "TEXT") private String worldPrompt; + + @Column(columnDefinition = "TEXT") private String epilogue1Title; + + @Column(columnDefinition = "TEXT") private String epilogue1Content; + + @Column(columnDefinition = "TEXT") private String epilogue2Title; + + @Column(columnDefinition = "TEXT") private String epilogue2Content; + + @Column(columnDefinition = "TEXT") private String epilogue3Title; + + @Column(columnDefinition = "TEXT") private String epilogue3Content; private Long score; private LocalDateTime createdAt; private Boolean isShared; + + @PrePersist + public void prePersist() { + if(uuid == null) uuid = UUID.randomUUID(); + if(createdAt == null) createdAt = LocalDateTime.now(); + } + + public History(User id, HistoryRequest req) { + this.user = id; + this.thumbnailUrl = req.getThumbnailUrl(); + this.title = req.getTitle(); + this.worldView = req.getWorldView(); + this.backgroundStory = req.getBackgroundStory(); + this.worldPrompt = req.getWorldPrompt(); + this.epilogue1Title = req.getEpilogue1Title(); + this.epilogue1Content = req.getEpilogue1Content(); + this.epilogue2Title = req.getEpilogue2Title(); + this.epilogue2Content = req.getEpilogue2Content(); + this.epilogue3Title = req.getEpilogue3Title(); + this.epilogue3Content = req.getEpilogue3Content(); + this.score = req.getScore(); + this.createdAt = LocalDateTime.now(); + this.isShared = false; + } } diff --git a/src/main/java/com/scriptopia/demo/domain/ItemDef.java b/src/main/java/com/scriptopia/demo/domain/ItemDef.java index 88b0330a..34e94b6c 100644 --- a/src/main/java/com/scriptopia/demo/domain/ItemDef.java +++ b/src/main/java/com/scriptopia/demo/domain/ItemDef.java @@ -5,18 +5,24 @@ import lombok.Setter; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @Setter public class ItemDef { - @Id @GeneratedValue + @Id + @GeneratedValue private Long id; @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") @@ -34,7 +40,12 @@ public class ItemDef { private Integer luck; @Enumerated(EnumType.STRING) - private MainStat mainStat; + private Stat mainStat; private LocalDateTime createdAt; -} + + private Long price; + + + +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/domain/ItemEffect.java b/src/main/java/com/scriptopia/demo/domain/ItemEffect.java index ebae0e0e..8c39f417 100644 --- a/src/main/java/com/scriptopia/demo/domain/ItemEffect.java +++ b/src/main/java/com/scriptopia/demo/domain/ItemEffect.java @@ -10,18 +10,20 @@ public class ItemEffect { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + @GeneratedValue private Long id; // FK: ItemDefs @ManyToOne(fetch = FetchType.LAZY) - private ItemDef itemDefs; + private ItemDef itemDef; // FK: EffectGradeDef @ManyToOne(fetch = FetchType.LAZY) private EffectGradeDef effectGradeDef; - // ์˜ˆ๋ฅผ ๋“ค์–ด ํšจ๊ณผ ์ด๋ฆ„์ด๋‚˜ ์ˆ˜์น˜ ๊ฐ™์€ ํ•„๋“œ๊ฐ€ ์žˆ๋‹ค๋ฉด ์—ฌ๊ธฐ์— ์ถ”๊ฐ€ + // ์˜ˆ๋ฅผ ๋“ค์–ด ํšจ๊ณผ ์ด๋ฆ„ private String effectName; - private int effectValue; -} + + // ์•„์ดํ…œ ํšจ๊ณผ ์„ค๋ช… + private String effectDescription; +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/domain/ItemGradeDef.java b/src/main/java/com/scriptopia/demo/domain/ItemGradeDef.java index 80c014f2..162f1dc2 100644 --- a/src/main/java/com/scriptopia/demo/domain/ItemGradeDef.java +++ b/src/main/java/com/scriptopia/demo/domain/ItemGradeDef.java @@ -17,4 +17,6 @@ public class ItemGradeDef { @Enumerated(EnumType.STRING) private Grade grade; // enum ํ•„๋“œ ์ถ”๊ฐ€ + + private Long price; } diff --git a/src/main/java/com/scriptopia/demo/domain/ItemType.java b/src/main/java/com/scriptopia/demo/domain/ItemType.java index ab475770..a41a27fc 100644 --- a/src/main/java/com/scriptopia/demo/domain/ItemType.java +++ b/src/main/java/com/scriptopia/demo/domain/ItemType.java @@ -1,7 +1,33 @@ package com.scriptopia.demo.domain; +import java.security.SecureRandom; + public enum ItemType { - WEAPON, - ARMOR, - ARTIFACT + WEAPON(40), + ARMOR(40), + ARTIFACT(20), + POTION(0); + + + private final int dropRate; + + private static final SecureRandom random = new SecureRandom(); + + ItemType(int dropRate) { + this.dropRate = dropRate; + } + + public static ItemType getRandomItemType() { + int rand = random.nextInt(100) + 1; + int cumulative = 0; + + for (ItemType itemType : ItemType.values()) { + cumulative += itemType.dropRate; + if ( rand <= cumulative) { + return itemType; + } + } + return null; + } + } \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/domain/LocalAccount.java b/src/main/java/com/scriptopia/demo/domain/LocalAccount.java index 9f4eac3d..af63817f 100644 --- a/src/main/java/com/scriptopia/demo/domain/LocalAccount.java +++ b/src/main/java/com/scriptopia/demo/domain/LocalAccount.java @@ -19,7 +19,11 @@ public class LocalAccount { @OneToOne(fetch = FetchType.LAZY) private User user; + @Column(unique = true, nullable = false) private String email; private String password; private LocalDateTime updatedAt; + + @Enumerated(EnumType.STRING) + private UserStatus status; } diff --git a/src/main/java/com/scriptopia/demo/domain/LoginType.java b/src/main/java/com/scriptopia/demo/domain/LoginType.java new file mode 100644 index 00000000..bc506a3b --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/LoginType.java @@ -0,0 +1,6 @@ +package com.scriptopia.demo.domain; + + +public enum LoginType { + LOCAL, SOCIAL +} diff --git a/src/main/java/com/scriptopia/demo/domain/MainStat.java b/src/main/java/com/scriptopia/demo/domain/MainStat.java deleted file mode 100644 index 7776ca58..00000000 --- a/src/main/java/com/scriptopia/demo/domain/MainStat.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.scriptopia.demo.domain; - -public enum MainStat { - STRENGTH, - AGILITY, - INTELLIGENCE, - LUCK, - NONE -} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/domain/NpcGrade.java b/src/main/java/com/scriptopia/demo/domain/NpcGrade.java new file mode 100644 index 00000000..d158f48b --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/NpcGrade.java @@ -0,0 +1,79 @@ +package com.scriptopia.demo.domain; + +import lombok.Getter; + +import java.security.SecureRandom; +import java.util.List; + +@Getter +public enum NpcGrade { + GRADE1(1, 70, 18, List.of(14, 8, 4)), + GRADE2(2, 105, 26, List.of(16, 9, 5)), + GRADE3(3, 140, 35, List.of(14, 9, 6)), + GRADE4(4, 152, 38, List.of(12, 12, 7)), + GRADE5(5, 188, 47, List.of(10, 12, 9)), + GRADE6(6, 204, 51, List.of(9, 12, 11)), + GRADE7(7, 248, 62, List.of(8, 11, 13)), + GRADE8(8, 268, 67, List.of(7, 10, 13)), + GRADE9(9, 320, 80, List.of(6, 10, 12)), + GRADE10(10, 344, 86, List.of(3, 5, 7)), + GRADE11(11, 707, 101, List.of(1, 1, 2)), + GRADE12(12, 963, 107, List.of(0, 1, 1)); + + + private final int gradeNumber; + private final int defense; + private final int attack; + private final List chapter; + + + private static final SecureRandom random = new SecureRandom(); + + NpcGrade(int gradeNumber, int defense, + int attack, List chapter) { + this.gradeNumber = gradeNumber; + this.defense = defense; + this.attack = attack; + this.chapter = chapter; + } + + public static NpcGrade getByGradeNumber(int gradeNumber) { + for (NpcGrade grade : NpcGrade.values()) { + if (grade.getGradeNumber() == gradeNumber) { + return grade; + } + } + return null; + } + + public static Integer getNpcNumberByRandom(int currentChapter) { + int rand = random.nextInt(100) + 1; // 1~100 + int cumulative = 0; + + for (NpcGrade grade : NpcGrade.values()) { + int weight = grade.getChapter().get(currentChapter - 1); + + cumulative += weight; + if (rand <= cumulative) { + return grade.getGradeNumber(); + } + } + + return NpcGrade.GRADE12.getGradeNumber(); + } + + + // ยฑ10% ๋žœ๋ค ๋ฐฉ์–ด๋ ฅ + public int getRandomDefense() { + int delta = (int)(defense * 0.1); + return defense - delta + random.nextInt(2 * delta + 1); + } + + // ยฑ10% ๋žœ๋ค ๊ณต๊ฒฉ๋ ฅ + public int getRandomAttack() { + int delta = (int)(attack * 0.1); + return attack - delta + random.nextInt(2 * delta + 1); + } + + +} diff --git a/src/main/java/com/scriptopia/demo/domain/PiaItem.java b/src/main/java/com/scriptopia/demo/domain/PiaItem.java index 12377873..061ff177 100644 --- a/src/main/java/com/scriptopia/demo/domain/PiaItem.java +++ b/src/main/java/com/scriptopia/demo/domain/PiaItem.java @@ -15,6 +15,6 @@ public class PiaItem { private Long id; private String name; - private String price; + private Long price; private String description; } diff --git a/src/main/java/com/scriptopia/demo/domain/PiaItemPurchaseLog.java b/src/main/java/com/scriptopia/demo/domain/PiaItemPurchaseLog.java new file mode 100644 index 00000000..b21ea875 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/PiaItemPurchaseLog.java @@ -0,0 +1,32 @@ +package com.scriptopia.demo.domain; + + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +public class PiaItemPurchaseLog { + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + private PiaItem piaItem; + + private LocalDateTime purchaseDate; + private Long price; + + @PrePersist + public void prePersist() { + this.purchaseDate = LocalDateTime.now(); + } + +} diff --git a/src/main/java/com/scriptopia/demo/domain/RewardType.java b/src/main/java/com/scriptopia/demo/domain/RewardType.java new file mode 100644 index 00000000..be6363c0 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/RewardType.java @@ -0,0 +1,94 @@ +package com.scriptopia.demo.domain; + + +import com.scriptopia.demo.domain.mongo.GameSessionMongo; +import com.scriptopia.demo.domain.mongo.RewardInfoMongo; + +import java.security.SecureRandom; + +public enum RewardType { + GOLD(60), + STAT(10), + ITEM(10), + NONE(20); + + private final int dropRate; + private static final SecureRandom random = new SecureRandom(); + + RewardType(int dropRate) { + this.dropRate = dropRate; + } + + public int getDropRate() { + return dropRate; + } + + // ๋žœ๋ค ๋ณด์ƒ ์ถ”์ถœ + public static RewardType getRandomRewardType() { + int rand = random.nextInt(100) + 1; // 1~100 + int cumulative = 0; + + for (RewardType reward : RewardType.values()) { + cumulative += reward.getDropRate(); + if (rand <= cumulative) { + return reward; + } + } + return null; // ํ˜น์€ ๊ธฐ๋ณธ๊ฐ’ + } + + + public static String getRewardSummary(RewardInfoMongo rewardInfo) { + if (rewardInfo == null) { + return "๋ณด์ƒ์ด ์—†์Šต๋‹ˆ๋‹ค."; + } + + StringBuilder sb = new StringBuilder(); + + // GOLD + if (rewardInfo.getRewardGold() != null && rewardInfo.getRewardGold() != 0) { + if (rewardInfo.getRewardGold() > 0) { + sb.append(rewardInfo.getRewardGold()).append(" ๊ณจ๋“œ๋ฅผ ํš๋“ํ•˜์˜€์Šต๋‹ˆ๋‹ค.\n"); + } else { + sb.append(Math.abs(rewardInfo.getRewardGold())).append(" ๊ณจ๋“œ๋ฅผ ์žƒ์—ˆ์Šต๋‹ˆ๋‹ค.\n"); + } + } + + // STAT + if (rewardInfo.getRewardStrength() != null && rewardInfo.getRewardStrength() != 0) { + sb.append("ํž˜ ").append(formatChange(rewardInfo.getRewardStrength())).append("\n"); + } + if (rewardInfo.getRewardAgility() != null && rewardInfo.getRewardAgility() != 0) { + sb.append("๋ฏผ์ฒฉ ").append(formatChange(rewardInfo.getRewardAgility())).append("\n"); + } + if (rewardInfo.getRewardIntelligence() != null && rewardInfo.getRewardIntelligence() != 0) { + sb.append("์ง€๋Šฅ ").append(formatChange(rewardInfo.getRewardIntelligence())).append("\n"); + } + if (rewardInfo.getRewardLuck() != null && rewardInfo.getRewardLuck() != 0) { + sb.append("์šด ").append(formatChange(rewardInfo.getRewardLuck())).append("\n"); + } + if (rewardInfo.getRewardLife() != null && rewardInfo.getRewardLife() != 0) { + sb.append("์ƒ๋ช… ").append(formatChange(rewardInfo.getRewardLife())).append("\n"); + } + + // TRAIT + if (rewardInfo.getRewardTrait() != null) { + sb.append("ํŠน์„ฑ ํš๋“: ").append(rewardInfo.getRewardTrait()).append("\n"); + } + + // ITEM + if (rewardInfo.getGainedItemDefId() != null && !rewardInfo.getGainedItemDefId().isEmpty()) { + sb.append("์•„์ดํ…œ ํš๋“: ").append(rewardInfo.getGainedItemDefId()).append("\n"); + } + if (rewardInfo.getLostItemsDefId() != null && !rewardInfo.getLostItemsDefId().isEmpty()) { + sb.append("์•„์ดํ…œ ์žƒ์Œ: ").append(rewardInfo.getLostItemsDefId()).append("\n"); + } + + return sb.length() == 0 ? "๋ณด์ƒ์ด ์—†์Šต๋‹ˆ๋‹ค." : sb.toString().trim(); + } + + private static String formatChange(int value) { + return value > 0 ? "+" + value : String.valueOf(value); + } + +} diff --git a/src/main/java/com/scriptopia/demo/domain/SceneType.java b/src/main/java/com/scriptopia/demo/domain/SceneType.java new file mode 100644 index 00000000..a56194b5 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/SceneType.java @@ -0,0 +1,5 @@ +package com.scriptopia.demo.domain; + +public enum SceneType { + BATTLE, CHOICE, SHOP, DONE, GAMEOVER, GAMECLEAR +} diff --git a/src/main/java/com/scriptopia/demo/domain/Settlement.java b/src/main/java/com/scriptopia/demo/domain/Settlement.java index 4707a0f5..3542a89a 100644 --- a/src/main/java/com/scriptopia/demo/domain/Settlement.java +++ b/src/main/java/com/scriptopia/demo/domain/Settlement.java @@ -23,11 +23,12 @@ public class Settlement { private ItemDef itemDef; - private TradeStatus tradeStatus; + @Enumerated(EnumType.STRING) + private TradeType tradeType; private Long price; private LocalDateTime settledAt; private LocalDateTime createdAt; -} +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/domain/SharedGame.java b/src/main/java/com/scriptopia/demo/domain/SharedGame.java index 661782d2..f3cfd849 100644 --- a/src/main/java/com/scriptopia/demo/domain/SharedGame.java +++ b/src/main/java/com/scriptopia/demo/domain/SharedGame.java @@ -2,14 +2,17 @@ import jakarta.persistence.*; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import java.time.LocalDateTime; +import java.util.UUID; @Entity @Getter @Setter +@NoArgsConstructor public class SharedGame { @Id @GeneratedValue @@ -19,11 +22,41 @@ public class SharedGame { @ManyToOne(fetch = FetchType.LAZY) private User user; + @Column(nullable = false, unique = true) + private UUID uuid; + private String thumbnailUrl; - private Long recommand; - private Long totalPlayed; + + @Column(columnDefinition = "TEXT") private String title; + + @Column(columnDefinition = "TEXT") private String worldView; + + @Column(columnDefinition = "TEXT") private String backgroundStory; private LocalDateTime sharedAt; + + @PrePersist + public void generateUuid() { + if(uuid == null) { + uuid = UUID.randomUUID(); + } + + if(sharedAt == null) { + sharedAt = LocalDateTime.now(); + } + } + + public static SharedGame from(User user, History h) { + SharedGame game = new SharedGame(); + game.user = user; + game.thumbnailUrl = h.getThumbnailUrl(); + game.title = h.getTitle(); + game.worldView = h.getWorldView(); + game.backgroundStory = h.getBackgroundStory(); + game.sharedAt = LocalDateTime.now(); + + return game; + } } diff --git a/src/main/java/com/scriptopia/demo/domain/SharedGameSort.java b/src/main/java/com/scriptopia/demo/domain/SharedGameSort.java new file mode 100644 index 00000000..c55acc89 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/SharedGameSort.java @@ -0,0 +1,7 @@ +package com.scriptopia.demo.domain; + +public enum SharedGameSort { + LATEST, + POPULAR, + TOP_SCORE +} diff --git a/src/main/java/com/scriptopia/demo/domain/SocialAccount.java b/src/main/java/com/scriptopia/demo/domain/SocialAccount.java index bdfa1b70..9235fab6 100644 --- a/src/main/java/com/scriptopia/demo/domain/SocialAccount.java +++ b/src/main/java/com/scriptopia/demo/domain/SocialAccount.java @@ -18,5 +18,10 @@ public class SocialAccount{ private User user; private String socialId; + + @Column(unique = true, nullable = false) + private String email; + + @Enumerated(EnumType.STRING) private Provider provider; } diff --git a/src/main/java/com/scriptopia/demo/domain/Stat.java b/src/main/java/com/scriptopia/demo/domain/Stat.java new file mode 100644 index 00000000..4cafefc1 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/Stat.java @@ -0,0 +1,26 @@ +package com.scriptopia.demo.domain; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.security.SecureRandom; + +public enum Stat { + INTELLIGENCE, + STRENGTH, + AGILITY, + LUCK; + + + + private static final SecureRandom random = new SecureRandom(); + + /** + * ๋ฉ”์ธ ์Šคํƒฏ์˜ ๊ฒฝ์šฐ ์‚ฌ์šฉ + * ๋ฌด์ž‘์œ„๋กœ ํ•˜๋‚˜์˜ ์Šคํƒฏ์„ ๋ฐ˜ํ™˜ + */ + public static Stat getRandomMainStat() { + Stat[] values = Stat.values(); + return values[random.nextInt(values.length)]; + } + +} diff --git a/src/main/java/com/scriptopia/demo/domain/TagDef.java b/src/main/java/com/scriptopia/demo/domain/TagDef.java index e90497cd..1ab662b1 100644 --- a/src/main/java/com/scriptopia/demo/domain/TagDef.java +++ b/src/main/java/com/scriptopia/demo/domain/TagDef.java @@ -16,4 +16,5 @@ public class TagDef { private Long id; private String tagName; + } diff --git a/src/main/java/com/scriptopia/demo/domain/Theme.java b/src/main/java/com/scriptopia/demo/domain/Theme.java new file mode 100644 index 00000000..40806439 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/Theme.java @@ -0,0 +1,5 @@ +package com.scriptopia.demo.domain; + +public enum Theme { + LIGHT, DARK +} diff --git a/src/main/java/com/scriptopia/demo/domain/TradeType.java b/src/main/java/com/scriptopia/demo/domain/TradeType.java new file mode 100644 index 00000000..1743d5c6 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/TradeType.java @@ -0,0 +1,6 @@ +package com.scriptopia.demo.domain; + +public enum TradeType { + BUY, + SELL +} diff --git a/src/main/java/com/scriptopia/demo/domain/User.java b/src/main/java/com/scriptopia/demo/domain/User.java index 7d75e02d..a2c4486f 100644 --- a/src/main/java/com/scriptopia/demo/domain/User.java +++ b/src/main/java/com/scriptopia/demo/domain/User.java @@ -1,9 +1,8 @@ package com.scriptopia.demo.domain; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import com.scriptopia.demo.exception.CustomException; +import com.scriptopia.demo.exception.ErrorCode; +import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; @@ -17,13 +16,32 @@ public class User { @Id @GeneratedValue private Long id; + @Column(nullable = false, unique = true) private String nickname; + private Long pia; private LocalDateTime createdAt; private LocalDateTime lastLoginAt; private String profileImgUrl; + + + @Enumerated(EnumType.STRING) private Role role; + @Enumerated(EnumType.STRING) + private LoginType loginType; + + // ๊ฑฐ๋ž˜ ๊ด€๋ จ ๋„๋ฉ”์ธ ๋ฉ”์†Œ๋“œ + public void addPia(Long amount) { + if (amount <= 0) throw new CustomException(ErrorCode.E_400_INVALID_AMOUNT); + this.pia += amount; + } + + public void subtractPia(Long amount) { + if (amount <= 0) throw new CustomException(ErrorCode.E_400_INVALID_AMOUNT); + if (this.pia < amount) throw new CustomException(ErrorCode.E_400_INSUFFICIENT_PIA); + this.pia -= amount; + } } diff --git a/src/main/java/com/scriptopia/demo/domain/UserItem.java b/src/main/java/com/scriptopia/demo/domain/UserItem.java index 9559507e..a41de3f8 100644 --- a/src/main/java/com/scriptopia/demo/domain/UserItem.java +++ b/src/main/java/com/scriptopia/demo/domain/UserItem.java @@ -23,5 +23,7 @@ public class UserItem { private int remainingUses; + + @Enumerated(EnumType.STRING) private TradeStatus tradeStatus; -} +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/domain/UserSetting.java b/src/main/java/com/scriptopia/demo/domain/UserSetting.java new file mode 100644 index 00000000..fecc92de --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/UserSetting.java @@ -0,0 +1,31 @@ +package com.scriptopia.demo.domain; + +import jakarta.persistence.*; +import lombok.Data; + +import java.awt.*; +import java.time.LocalDateTime; + +@Entity +@Data +public class UserSetting { + + @Id@GeneratedValue + private long id; + + @OneToOne + private User user; + + private Theme theme; + + @Enumerated(EnumType.STRING) + private FontType fontType; + + private int fontSize; + + private int lineHeight; + + private int wordSpacing; + + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/scriptopia/demo/domain/UserStatus.java b/src/main/java/com/scriptopia/demo/domain/UserStatus.java new file mode 100644 index 00000000..7b37b47e --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/UserStatus.java @@ -0,0 +1,5 @@ +package com.scriptopia.demo.domain; + +public enum UserStatus { + UNVERIFIED, VERIFIED +} diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/BattleInfoMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/BattleInfoMongo.java new file mode 100644 index 00000000..e1e12cc7 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/mongo/BattleInfoMongo.java @@ -0,0 +1,17 @@ +package com.scriptopia.demo.domain.mongo; + +import lombok.*; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class BattleInfoMongo { + private Long curTurnId; // ํ˜„์žฌ ํ„ด + private List playerHp; // ํ”Œ๋ ˆ์ด์–ด HP ๋กœ๊ทธ + private List enemyHp; // NPC HP ๋กœ๊ทธ + private List battleTurn;// ์ˆ˜์น˜ ๊ธฐ๋ฐ˜ ํ„ด ๊ธฐ๋ก + private Boolean playerWin; // ์ŠนํŒจ ์—ฌ๋ถ€ +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/BattleStoryMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/BattleStoryMongo.java new file mode 100644 index 00000000..1886adf3 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/mongo/BattleStoryMongo.java @@ -0,0 +1,11 @@ +package com.scriptopia.demo.domain.mongo; + +import lombok.*; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class BattleStoryMongo { + private String turnInfo; +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/ChoiceInfoMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/ChoiceInfoMongo.java new file mode 100644 index 00000000..d4eee367 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/mongo/ChoiceInfoMongo.java @@ -0,0 +1,16 @@ +package com.scriptopia.demo.domain.mongo; + +import com.scriptopia.demo.domain.ChoiceEventType; +import lombok.*; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ChoiceInfoMongo { + private ChoiceEventType eventType; // living, nonliving + private String story; + private List choice; +} diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/ChoiceMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/ChoiceMongo.java new file mode 100644 index 00000000..3c80f292 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/mongo/ChoiceMongo.java @@ -0,0 +1,20 @@ +package com.scriptopia.demo.domain.mongo; + +import com.scriptopia.demo.domain.ChoiceResultType; +import com.scriptopia.demo.domain.RewardType; +import com.scriptopia.demo.domain.Stat; +import lombok.*; + + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ChoiceMongo { + private String detail; + private Stat stats; // STRENGTH, AGILITY, INTELLIGENCE, LUCK + private Integer probability; + private ChoiceResultType resultType; // BATTLE, SHOP, CHOICE, NONE + private RewardType rewardType; // GOLD, STAT, ITEM, NONE + +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/DoneInfoMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/DoneInfoMongo.java new file mode 100644 index 00000000..eae0980c --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/mongo/DoneInfoMongo.java @@ -0,0 +1,11 @@ +package com.scriptopia.demo.domain.mongo; + +import lombok.*; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class DoneInfoMongo { + private String story; +} diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/GameSessionMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/GameSessionMongo.java new file mode 100644 index 00000000..470892be --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/mongo/GameSessionMongo.java @@ -0,0 +1,47 @@ +package com.scriptopia.demo.domain.mongo; + +import com.scriptopia.demo.domain.RewardType; +import com.scriptopia.demo.domain.SceneType; +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Document(collection = "gameSessions") +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GameSessionMongo { + + @Id + private String id; // MongoDB ๊ธฐ๋ณธํ‚ค + + private Long userId; // MySQL ์‚ฌ์šฉ์ž ID + + private SceneType sceneType; // battle, choice, shop, done + + private LocalDateTime startedAt; + private LocalDateTime updatedAt; + + private String background; + private String preChoice; + private RewardType preReward; + private String location; + private Integer progress; + private List stage; + + private PlayerInfoMongo playerInfo; + private NpcInfoMongo npcInfo; + private List inventory; + private List createdItems; + + private ChoiceInfoMongo choiceInfo; + private DoneInfoMongo doneInfo; + private ShopInfoMongo shopInfo; + private BattleInfoMongo battleInfo; + private RewardInfoMongo rewardInfo; + private HistoryInfoMongo historyInfo; +} diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/HistoryInfoMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/HistoryInfoMongo.java new file mode 100644 index 00000000..cd5dc993 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/mongo/HistoryInfoMongo.java @@ -0,0 +1,21 @@ +package com.scriptopia.demo.domain.mongo; + +import lombok.*; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class HistoryInfoMongo { + private String title; + private String worldView; + private String backgroundStory; + private String worldPrompt; + private String epilogue1Title; + private String epilogue1Content; + private String epilogue2Title; + private String epilogue2Content; + private String epilogue3Title; + private String epilogue3Content; + private Long score; +} diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/InventoryMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/InventoryMongo.java new file mode 100644 index 00000000..76dc68a1 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/mongo/InventoryMongo.java @@ -0,0 +1,24 @@ +package com.scriptopia.demo.domain.mongo; + +import jakarta.persistence.Id; +import lombok.*; + +import java.time.LocalDateTime; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class InventoryMongo { + @Id + private String id; + + private String itemDefId; + private LocalDateTime acquiredAt; + private Boolean equipped; + private String source; + + public boolean isEquipped() { + return this.equipped; + } +} diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/ItemDefMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/ItemDefMongo.java new file mode 100644 index 00000000..b5632235 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/mongo/ItemDefMongo.java @@ -0,0 +1,35 @@ +package com.scriptopia.demo.domain.mongo; + +import com.scriptopia.demo.domain.Grade; +import com.scriptopia.demo.domain.ItemType; +import com.scriptopia.demo.domain.Stat; +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.List; + +@Document(collection = "itemDefMongo") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ItemDefMongo { + @Id + private String id; + + private String itemPicSrc; + private String name; + private String description; + private ItemType category; // WEAPON, ARMOR, ARTIFACT, *POTION* ํƒ€์ž… ๋•Œ๋ฌธ์— ์• ๋งคํ•จ + private Integer baseStat; + private List itemEffect; + private Integer strength; + private Integer agility; + private Integer intelligence; + private Integer luck; + private Stat mainStat; // strength, agility, intelligence, luck + private Grade grade; // COMMON, UNCOMMON, RARE, EPIC, LEGENDARY + private Long price; + +} diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/ItemEffectMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/ItemEffectMongo.java new file mode 100644 index 00000000..5506823f --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/mongo/ItemEffectMongo.java @@ -0,0 +1,14 @@ +package com.scriptopia.demo.domain.mongo; + +import com.scriptopia.demo.domain.EffectProbability; +import lombok.*; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ItemEffectMongo { + private String itemEffectName; + private String itemEffectDescription; + private EffectProbability effectProbability; // COMMON, UNCOMMON, RARE, EPIC, LEGENDARY +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/NpcInfoMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/NpcInfoMongo.java new file mode 100644 index 00000000..6edfed68 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/mongo/NpcInfoMongo.java @@ -0,0 +1,24 @@ +package com.scriptopia.demo.domain.mongo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.mongodb.core.mapping.Document; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Document(collection = "item_def") +public class NpcInfoMongo { + private String name; + private Integer rank; + private String trait; + private Integer strength; + private Integer agility; + private Integer intelligence; + private Integer luck; + private String NpcWeaponName; + private String NpcWeaponDescription; +} diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/PlayerInfoMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/PlayerInfoMongo.java new file mode 100644 index 00000000..6adbb068 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/mongo/PlayerInfoMongo.java @@ -0,0 +1,22 @@ +package com.scriptopia.demo.domain.mongo; + +import lombok.*; +import org.springframework.data.mongodb.core.mapping.Document; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PlayerInfoMongo { + private String name; + private Integer life; + private Integer level; + private Integer healthPoint; // ๋‚œ์ˆ˜ + private Integer experiencePoint; + private String trait; + private Integer strength; // ๋‚œ์ˆ˜ + private Integer agility; // ๋‚œ์ˆ˜ + private Integer intelligence; // ๋‚œ์ˆ˜ + private Integer luck; // ๋‚œ์ˆ˜ + private Long gold; // ๋‚œ์ˆ˜ +} diff --git a/src/main/java/com/scriptopia/demo/domain/mongo/RewardInfoMongo.java b/src/main/java/com/scriptopia/demo/domain/mongo/RewardInfoMongo.java new file mode 100644 index 00000000..5d01b74d --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/mongo/RewardInfoMongo.java @@ -0,0 +1,38 @@ +package com.scriptopia.demo.domain.mongo; + +import lombok.*; +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class RewardInfoMongo { + + @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; + + @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 new file mode 100644 index 00000000..6c92c913 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/domain/mongo/ShopInfoMongo.java @@ -0,0 +1,13 @@ +package com.scriptopia.demo.domain.mongo; + +import lombok.*; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ShopInfoMongo { + 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/TagDefCreateRequest.java b/src/main/java/com/scriptopia/demo/dto/TagDef/TagDefCreateRequest.java new file mode 100644 index 00000000..1cbb3085 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/TagDef/TagDefCreateRequest.java @@ -0,0 +1,12 @@ +package com.scriptopia.demo.dto.TagDef; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +@AllArgsConstructor +public class TagDefCreateRequest { + private String tagName; +} diff --git a/src/main/java/com/scriptopia/demo/dto/TagDef/TagDefDeleteRequest.java b/src/main/java/com/scriptopia/demo/dto/TagDef/TagDefDeleteRequest.java new file mode 100644 index 00000000..6573c474 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/TagDef/TagDefDeleteRequest.java @@ -0,0 +1,12 @@ +package com.scriptopia.demo.dto.TagDef; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TagDefDeleteRequest { + private String tagName; +} diff --git a/src/main/java/com/scriptopia/demo/dto/auction/AuctionItemResponse.java b/src/main/java/com/scriptopia/demo/dto/auction/AuctionItemResponse.java new file mode 100644 index 00000000..e3abffd6 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auction/AuctionItemResponse.java @@ -0,0 +1,61 @@ +package com.scriptopia.demo.dto.auction; + +import com.scriptopia.demo.domain.EffectProbability; +import com.scriptopia.demo.domain.TradeStatus; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AuctionItemResponse { + + private Long auctionId; + private Long price; + private LocalDateTime createdAt; + + private UserDto seller; + private ItemDto item; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class UserDto { + private Long userId; + private String nickname; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ItemDto { + private Long userItemId; + private Long itemDefId; + private String name; + private String description; + private String picSrc; + private int remainingUses; + private TradeStatus tradeStatus; + private String grade; + private int baseStat; + private int strength; + private int agility; + private int intelligence; + private int luck; + private List effects; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ItemEffectDto { + private String effectName; + private String effectDescription; + private EffectProbability effectProbability; + } +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/dto/auction/AuctionRequest.java b/src/main/java/com/scriptopia/demo/dto/auction/AuctionRequest.java new file mode 100644 index 00000000..903ab142 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auction/AuctionRequest.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.dto.auction; + + +import com.scriptopia.demo.domain.TradeStatus; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AuctionRequest { + private String itemDefId; // ๋‹จ์ˆ˜ํ˜•์œผ๋กœ ๋ณ€๊ฒฝํ•จ + private Long price; +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/dto/auction/MySaleItemRequest.java b/src/main/java/com/scriptopia/demo/dto/auction/MySaleItemRequest.java new file mode 100644 index 00000000..eeda6fa7 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auction/MySaleItemRequest.java @@ -0,0 +1,13 @@ +package com.scriptopia.demo.dto.auction; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MySaleItemRequest { + private Long pageIndex; + private Long pageSize; +} diff --git a/src/main/java/com/scriptopia/demo/dto/auction/MySaleItemResponse.java b/src/main/java/com/scriptopia/demo/dto/auction/MySaleItemResponse.java new file mode 100644 index 00000000..f2e0cd92 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auction/MySaleItemResponse.java @@ -0,0 +1,26 @@ +package com.scriptopia.demo.dto.auction; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MySaleItemResponse { + + private List content; + private PageInfo pageInfo; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class PageInfo { + private int currentPage; + private int pageSize; + } + +} diff --git a/src/main/java/com/scriptopia/demo/dto/auction/MySaleItemResponseItem.java b/src/main/java/com/scriptopia/demo/dto/auction/MySaleItemResponseItem.java new file mode 100644 index 00000000..43a46212 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auction/MySaleItemResponseItem.java @@ -0,0 +1,30 @@ +package com.scriptopia.demo.dto.auction; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MySaleItemResponseItem { + private Long auctionId; + private Long price; + private LocalDateTime createdAt; + private ItemDto item; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ItemDto { + private Long itemDefId; + private String name; + private String itemGrade; + private String itemType; + private String mainStat; + private String picSrc; + } +} diff --git a/src/main/java/com/scriptopia/demo/dto/auction/SettlementHistoryRequest.java b/src/main/java/com/scriptopia/demo/dto/auction/SettlementHistoryRequest.java new file mode 100644 index 00000000..bde72c41 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auction/SettlementHistoryRequest.java @@ -0,0 +1,14 @@ +package com.scriptopia.demo.dto.auction; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SettlementHistoryRequest { + private Long pageIndex; + private Long pageSize; +} diff --git a/src/main/java/com/scriptopia/demo/dto/auction/SettlementHistoryResponse.java b/src/main/java/com/scriptopia/demo/dto/auction/SettlementHistoryResponse.java new file mode 100644 index 00000000..5bda0b7f --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auction/SettlementHistoryResponse.java @@ -0,0 +1,21 @@ +package com.scriptopia.demo.dto.auction; + +import lombok.*; + +import java.util.List; + + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class SettlementHistoryResponse { + private List content; + private PageInfo pageInfo; + + + @Data + public static class PageInfo { + private int currentPage; + private int pageSize; + } +} diff --git a/src/main/java/com/scriptopia/demo/dto/auction/SettlementHistoryResponseItem.java b/src/main/java/com/scriptopia/demo/dto/auction/SettlementHistoryResponseItem.java new file mode 100644 index 00000000..57885cdd --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auction/SettlementHistoryResponseItem.java @@ -0,0 +1,17 @@ +package com.scriptopia.demo.dto.auction; +import lombok.*; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SettlementHistoryResponseItem { + private Long settlementId; + private String itemName; + private String itemType; + private String itemGrade; + private Long price; + private String tradeType; // BUY / SELL + private LocalDateTime settledAt; +} diff --git a/src/main/java/com/scriptopia/demo/dto/auction/TradeFilterRequest.java b/src/main/java/com/scriptopia/demo/dto/auction/TradeFilterRequest.java new file mode 100644 index 00000000..edb1b6f9 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auction/TradeFilterRequest.java @@ -0,0 +1,23 @@ +package com.scriptopia.demo.dto.auction; + +import com.scriptopia.demo.domain.Grade; +import com.scriptopia.demo.domain.ItemType; +import com.scriptopia.demo.domain.Stat; +import lombok.Data; + +import java.util.List; + +@Data +public class TradeFilterRequest { + + private Long pageIndex; // 0๋ถ€ํ„ฐ ์‹œ์ž‘ + private Long pageSize; // ํ•œ ํŽ˜์ด์ง€๋‹น ์•„์ดํ…œ ์ˆ˜ + + private String itemName; // ํŒ๋งค ์•„์ดํ…œ ์ด๋ฆ„ + private ItemType category; // ์•„์ดํ…œ ๋ถ„๋ฅ˜ (WEAPON, ARMOR, ARTIFACT, POTION) + private Long minPrice; // ์ตœ์†Œ ๊ฐ€๊ฒฉ (nullable) + private Long maxPrice; // ์ตœ๋Œ€ ๊ฐ€๊ฒฉ (nullable) + private Grade grade; // ์•„์ดํ…œ ๋“ฑ๊ธ‰ (nullable) + private List effectGrades; // ์•„์ดํ…œ ํšจ๊ณผ ๋“ฑ๊ธ‰ ํ•„ํ„ฐ (nullable) + private Stat stat; // ์ฃผ ์Šคํƒฏ (nullable) +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/dto/auction/TradeResponse.java b/src/main/java/com/scriptopia/demo/dto/auction/TradeResponse.java new file mode 100644 index 00000000..34cf120e --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auction/TradeResponse.java @@ -0,0 +1,19 @@ +// PagedAuctionResponse.java +package com.scriptopia.demo.dto.auction; + +import lombok.Data; +import java.util.List; + +@Data +public class TradeResponse { + private List content; + private PageInfo pageInfo; + + @Data + public static class PageInfo { + private int currentPage; + private int pageSize; + private long totalItems; + private int totalPages; + } +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/dto/auth/ChangePasswordRequest.java b/src/main/java/com/scriptopia/demo/dto/auth/ChangePasswordRequest.java new file mode 100644 index 00000000..349a0d60 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auth/ChangePasswordRequest.java @@ -0,0 +1,33 @@ +package com.scriptopia.demo.dto.auth; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChangePasswordRequest { + + + @NotBlank(message = "E_400_MISSING_PASSWORD") + @Size(min = 8, max = 20, message = "E_400_PASSWORD_SIZE") + @Pattern( + regexp = "^(?=.*[a-z])(?=.*\\d)(?=.*[!@#$%^&*()_+=\\-{}\\[\\]:;\"'<>,.?/]).+$", + message = "E_400_PASSWORD_COMPLEXITY" + ) + private String oldPassword; + + @NotBlank(message = "E_400_MISSING_PASSWORD\"") + @Size(min = 8, max = 20, message = "E_400_PASSWORD_SIZE") + @Pattern( + regexp = "^(?=.*[a-z])(?=.*\\d)(?=.*[!@#$%^&*()_+=\\-{}\\[\\]:;\"'<>,.?/]).+$", + message = "E_400_PASSWORD_COMPLEXITY" + ) + private String newPassword; + + private String confirmPassword; +} diff --git a/src/main/java/com/scriptopia/demo/dto/auth/LoginRequest.java b/src/main/java/com/scriptopia/demo/dto/auth/LoginRequest.java new file mode 100644 index 00000000..26a6a129 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auth/LoginRequest.java @@ -0,0 +1,28 @@ +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; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +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/LoginResponse.java b/src/main/java/com/scriptopia/demo/dto/auth/LoginResponse.java new file mode 100644 index 00000000..f09375c1 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auth/LoginResponse.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.dto.auth; + +import com.scriptopia.demo.domain.Role; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class LoginResponse { + private String accessToken; + private Long expiresIn; + private Role role; +} diff --git a/src/main/java/com/scriptopia/demo/dto/auth/RefreshResponse.java b/src/main/java/com/scriptopia/demo/dto/auth/RefreshResponse.java new file mode 100644 index 00000000..7f8b8cad --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auth/RefreshResponse.java @@ -0,0 +1,13 @@ +package com.scriptopia.demo.dto.auth; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class RefreshResponse { + private String accessToken; + private Long expiresIn; +} diff --git a/src/main/java/com/scriptopia/demo/dto/auth/RegisterRequest.java b/src/main/java/com/scriptopia/demo/dto/auth/RegisterRequest.java new file mode 100644 index 00000000..259ae206 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auth/RegisterRequest.java @@ -0,0 +1,34 @@ +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; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class RegisterRequest { + + @NotBlank(message = "E_400_MISSING_EMAIL") + @Email(message = "E_400_INVALID_EMAIL_FORMAT") + private String email; + + @Size(min = 8, max = 20, message = "E_400_PASSWORD_SIZE") + @Pattern( + regexp = "^(?=.*[a-z])(?=.*\\d)(?=.*[!@#$%^&*()_+=\\-{}\\[\\]:;\"'<>,.?/]).+$", + message = "E_400_PASSWORD_COMPLEXITY" + ) + private String password; + + @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/auth/ResetPasswordRequest.java b/src/main/java/com/scriptopia/demo/dto/auth/ResetPasswordRequest.java new file mode 100644 index 00000000..8c01cd8a --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auth/ResetPasswordRequest.java @@ -0,0 +1,24 @@ +package com.scriptopia.demo.dto.auth; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ResetPasswordRequest { + + private String token; + + @NotBlank(message = "E_400_MISSING_PASSWORD") + @Size(min = 8, max = 20, message = "E_400_PASSWORD_SIZE") + @Pattern( + regexp = "^(?=.*[a-z])(?=.*\\d)(?=.*[!@#$%^&*()_+=\\-{}\\[\\]:;\"'<>,.?/]).+$", + message = "E_400_PASSWORD_COMPLEXITY" + ) + private String newPassword; +} diff --git a/src/main/java/com/scriptopia/demo/dto/auth/SendCodeRequest.java b/src/main/java/com/scriptopia/demo/dto/auth/SendCodeRequest.java new file mode 100644 index 00000000..7792999b --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auth/SendCodeRequest.java @@ -0,0 +1,17 @@ +package com.scriptopia.demo.dto.auth; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class SendCodeRequest { + + @NotBlank(message = "E_400_MISSING_EMAIL") + @Email(message = "E_400_INVALID_EMAIL_FORMAT") + private String email; +} diff --git a/src/main/java/com/scriptopia/demo/dto/auth/SendResetMailRequest.java b/src/main/java/com/scriptopia/demo/dto/auth/SendResetMailRequest.java new file mode 100644 index 00000000..e8fd085a --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auth/SendResetMailRequest.java @@ -0,0 +1,17 @@ +package com.scriptopia.demo.dto.auth; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class SendResetMailRequest { + + @NotBlank(message = "E_400_MISSING_EMAIL") + @Email(message = "E_400_INVALID_EMAIL_FORMAT") + private String email; +} diff --git a/src/main/java/com/scriptopia/demo/dto/auth/VerifyCodeRequest.java b/src/main/java/com/scriptopia/demo/dto/auth/VerifyCodeRequest.java new file mode 100644 index 00000000..599d7c49 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auth/VerifyCodeRequest.java @@ -0,0 +1,21 @@ +package com.scriptopia.demo.dto.auth; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class VerifyCodeRequest { + + @NotBlank(message = "E_400_MISSING_EMAIL") + @Email(message = "E_400_INVALID_EMAIL_FORMAT") + private String email; + + private String code; + + +} diff --git a/src/main/java/com/scriptopia/demo/dto/auth/VerifyEmailRequest.java b/src/main/java/com/scriptopia/demo/dto/auth/VerifyEmailRequest.java new file mode 100644 index 00000000..cc6d5abc --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/auth/VerifyEmailRequest.java @@ -0,0 +1,17 @@ +package com.scriptopia.demo.dto.auth; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class VerifyEmailRequest { + + @NotBlank(message = "E_400_MISSING_EMAIL") + @Email(message = "E_400_INVALID_EMAIL_FORMAT") + private String email; +} diff --git a/src/main/java/com/scriptopia/demo/dto/exception/ErrorResponse.java b/src/main/java/com/scriptopia/demo/dto/exception/ErrorResponse.java new file mode 100644 index 00000000..2165d980 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/exception/ErrorResponse.java @@ -0,0 +1,26 @@ +package com.scriptopia.demo.dto.exception; + +import com.scriptopia.demo.exception.ErrorCode; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class ErrorResponse { + private final String code; + private final String message; + private final HttpStatus status; + + public ErrorResponse(String code, String message, HttpStatus status) { + this.code = code; + this.message = message; + this.status = status; + } + + public ErrorResponse(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.message = errorCode.getMessage(); + this.status = errorCode.getStatus(); + } + + +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameBattleRequest.java b/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameBattleRequest.java new file mode 100644 index 00000000..d3d62c35 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameBattleRequest.java @@ -0,0 +1,54 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CreateGameBattleRequest { + + private int turnCount; + private String worldView; + private String location; + + private String playerName; + private String playerTrait; + private int playerDmg; + private Item playerWeapon; + private Item playerArmor; + private Item playerArtifact; + + private String npcName; + private String npcTrait; + private int npcDmg; + private String npcWeapon; + private String npcWeaponDescription; + + private Integer battleResult; + private List> hpLog; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Item { + private String name; + private String description; + private List effects; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ItemEffect { + private String name; + private String description; + } + } +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameBattleResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameBattleResponse.java new file mode 100644 index 00000000..fe962061 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameBattleResponse.java @@ -0,0 +1,21 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.*; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CreateGameBattleResponse { + private BattleInfoDto battleInfo; + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class BattleInfoDto { + private List turnInfo; // ํ„ด๋ณ„ ์ „ํˆฌ ๋กœ๊ทธ + private String reCap; // ์ „ํˆฌ ์š”์•ฝ + } +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameChoiceRequest.java b/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameChoiceRequest.java new file mode 100644 index 00000000..96c09658 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameChoiceRequest.java @@ -0,0 +1,39 @@ +package com.scriptopia.demo.dto.gamesession; + +import com.scriptopia.demo.domain.ChoiceEventType; +import com.scriptopia.demo.domain.Stat; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CreateGameChoiceRequest { + private String worldView; + private String currentStory; + private String location; + private String currentChoice; + private List choiceStat; + private ChoiceEventType eventType; + private Integer npcRank; + private PlayerInfo playerInfo; + private List itemInfo; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class PlayerInfo { + private String name; + private String trait; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class ItemInfo { + private String name; + private String description; + } +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameChoiceResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameChoiceResponse.java new file mode 100644 index 00000000..0af4abe1 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameChoiceResponse.java @@ -0,0 +1,43 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CreateGameChoiceResponse { + + private ChoiceInfo choiceInfo; + private NpcInfo npcInfo; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class ChoiceInfo { + private String story; + private String title; + private List choice; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class ChoiceOption { + private String detail; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class NpcInfo { + private String name; + private Integer rank; + private String trait; + private String npcWeaponName; + private String npcWeaponDescription; + } +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameDoneRequest.java b/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameDoneRequest.java new file mode 100644 index 00000000..7efe257c --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameDoneRequest.java @@ -0,0 +1,25 @@ +package com.scriptopia.demo.dto.gamesession; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CreateGameDoneRequest { + + private String worldView; + private String location; + + private String previousStory; + private String selectedChoice; + + private String resultContent; + private String playerName; + private boolean playerVictory; + +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameDoneResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameDoneResponse.java new file mode 100644 index 00000000..7089d14e --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameDoneResponse.java @@ -0,0 +1,27 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CreateGameDoneResponse { + private DoneInfo doneInfo; + + + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class DoneInfo { + private String newLocation; + private String reCap; + } + +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameRequest.java b/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameRequest.java new file mode 100644 index 00000000..3dbe1a8e --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/CreateGameRequest.java @@ -0,0 +1,14 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CreateGameRequest { + private String background; + private String characterName; + private String characterDescription; +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/ExternalGameResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/ExternalGameResponse.java new file mode 100644 index 00000000..f36ee735 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ExternalGameResponse.java @@ -0,0 +1,50 @@ +package com.scriptopia.demo.dto.gamesession; + +import com.scriptopia.demo.domain.Grade; +import com.scriptopia.demo.domain.Stat; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ExternalGameResponse { + private PlayerInfo playerInfo; + private ItemDef itemDef; + private String worldView; + private String backgroundStory; + private String location; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class PlayerInfo { + private String name; + private Stat startStat; + private String trait; + + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class ItemDef { + private String name; + private String description; + private ItemEffect itemEffect; + private Stat mainStat; + private Grade grade; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class ItemEffect { + private String itemEffectName; + private String itemEffectDescription; + private Grade grade; + } + } +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/GameChoiceRequest.java b/src/main/java/com/scriptopia/demo/dto/gamesession/GameChoiceRequest.java new file mode 100644 index 00000000..c2bf6fb1 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/GameChoiceRequest.java @@ -0,0 +1,13 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class GameChoiceRequest { + private Integer choiceIndex; + private String customAction; +} 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/GameSessionRequest.java b/src/main/java/com/scriptopia/demo/dto/gamesession/GameSessionRequest.java new file mode 100644 index 00000000..89014086 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/GameSessionRequest.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 GameSessionRequest { + private String gameId; +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/GameSessionResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/GameSessionResponse.java new file mode 100644 index 00000000..887dfd32 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/GameSessionResponse.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 GameSessionResponse { + private String sessionId; +} 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/StartGameRequest.java b/src/main/java/com/scriptopia/demo/dto/gamesession/StartGameRequest.java new file mode 100644 index 00000000..ae8f10d3 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/StartGameRequest.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class StartGameRequest { + private String background; + private String characterName; + private String characterDescription; + private String itemId; // uuid๋ฅผ ๋ฐ›์„ ๊ฒƒ (์ž„์‹œ์ž„) +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/StartGameResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/StartGameResponse.java new file mode 100644 index 00000000..c2dfd654 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/StartGameResponse.java @@ -0,0 +1,13 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class StartGameResponse { + private String message; + private String gameId; +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameBattleResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameBattleResponse.java new file mode 100644 index 00000000..6ab96352 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameBattleResponse.java @@ -0,0 +1,43 @@ +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 InGameBattleResponse { + 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 Long curTurnId; // ํ˜„์žฌ ํ„ด + private List playerHp; // ํ”Œ๋ ˆ์ด์–ด ์ฒด๋ ฅ ๋กœ๊ทธ + private List enemyHp; // NPC ์ฒด๋ ฅ ๋กœ๊ทธ + private List battleStory; // ํ„ด๋ณ„ ์ „ํˆฌ ์Šคํ† ๋ฆฌ + private Boolean playerWin; // ์Šน๋ฆฌ ์—ฌ๋ถ€ + + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class BattleStoryResponse { + private String turnInfo; // ํ•ด๋‹น ํ„ด ์„ค๋ช… + } +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameChoiceResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameChoiceResponse.java new file mode 100644 index 00000000..2a738db0 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameChoiceResponse.java @@ -0,0 +1,40 @@ +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 InGameChoiceResponse { + + 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 choiceInfo; // ๋‚ด๋ถ€ + + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class Choice { + private String detail; + private String stats; // "STRENGTH", "AGILITY", "INTELLIGENCE", "LUCK" + private int probability; + } +} 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/InGameDoneResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameDoneResponse.java new file mode 100644 index 00000000..25276557 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameDoneResponse.java @@ -0,0 +1,45 @@ +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 InGameDoneResponse { + 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 RewardInfoResponse rewardInfo; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RewardInfoResponse { + private List gainedItemNames; + private int rewardStrength; + private int rewardAgility; + private int rewardIntelligence; + private int rewardLuck; + private int rewardLife; + private int rewardGold; + } +} diff --git a/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameInventoryResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameInventoryResponse.java new file mode 100644 index 00000000..861bed77 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameInventoryResponse.java @@ -0,0 +1,47 @@ +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 +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class InGameInventoryResponse { + // ์†Œ์œ ์ž ์ •๋ณด + private String itemDefId; + private LocalDateTime acquiredAt; + private boolean equipped; + private String source; + + // ์•„์ดํ…œ ์ •์˜ ์ •๋ณด + 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/gamesession/ingame/InGameNpcResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameNpcResponse.java new file mode 100644 index 00000000..4daaddb4 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameNpcResponse.java @@ -0,0 +1,23 @@ +package com.scriptopia.demo.dto.gamesession.ingame; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class InGameNpcResponse { + private String name; + private Integer rank; + private String trait; + private Integer strength; + private Integer agility; + private Integer intelligence; + private Integer luck; + private String npcWeaponName; + private String npcWeaponDescription; + +} 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/InGamePlayerResponse.java b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGamePlayerResponse.java new file mode 100644 index 00000000..59535dc6 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGamePlayerResponse.java @@ -0,0 +1,27 @@ +package com.scriptopia.demo.dto.gamesession.ingame; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class InGamePlayerResponse { + private String name; + private int life; + private int level; + private int healthPoint; + private int experiencePoint; + private String trait; + private int strength; + private int agility; + private int intelligence; + private int luck; + private long gold; +} + + 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 new file mode 100644 index 00000000..237c8f99 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/ingame/InGameShopResponse.java @@ -0,0 +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/gamesession/startNewGameRequest.java b/src/main/java/com/scriptopia/demo/dto/gamesession/startNewGameRequest.java new file mode 100644 index 00000000..98448686 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/gamesession/startNewGameRequest.java @@ -0,0 +1,14 @@ +package com.scriptopia.demo.dto.gamesession; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class startNewGameRequest { + private String message; + private String mongoId; + +} diff --git a/src/main/java/com/scriptopia/demo/dto/history/HistoryPageResponse.java b/src/main/java/com/scriptopia/demo/dto/history/HistoryPageResponse.java new file mode 100644 index 00000000..5489df9f --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/history/HistoryPageResponse.java @@ -0,0 +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 LocalDateTime created_at; + private boolean isShared; + + public static HistoryPageResponse from(History h) { + 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 new file mode 100644 index 00000000..3954cc7b --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/history/HistoryRequest.java @@ -0,0 +1,25 @@ +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; + private String worldView; + private String backgroundStory; + private String worldPrompt; + private String epilogue1Title; + private String epilogue1Content; + private String epilogue2Title; + private String epilogue2Content; + private String epilogue3Title; + private String epilogue3Content; + private Long score; +} diff --git a/src/main/java/com/scriptopia/demo/dto/history/HistoryResponse.java b/src/main/java/com/scriptopia/demo/dto/history/HistoryResponse.java new file mode 100644 index 00000000..08295073 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/history/HistoryResponse.java @@ -0,0 +1,33 @@ +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 UUID uuid; + private Long userId; + private String thumbnailUrl; + private String title; + private String worldView; + private String backgroundStory; + private String worldPrompt; + private String epilogue1Title; + private String epilogue1Content; + private String epilogue2Title; + private String epilogue2Content; + private String epilogue3Title; + private String epilogue3Content; + private Long score; + private LocalDateTime createdAt; + private Boolean isShared; +} diff --git a/src/main/java/com/scriptopia/demo/dto/items/ItemDTO.java b/src/main/java/com/scriptopia/demo/dto/items/ItemDTO.java new file mode 100644 index 00000000..f2b416d1 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/items/ItemDTO.java @@ -0,0 +1,32 @@ +package com.scriptopia.demo.dto.items; + +import com.scriptopia.demo.domain.Grade; +import com.scriptopia.demo.domain.ItemType; +import com.scriptopia.demo.domain.Stat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ItemDTO { + private String name; + private String description; + private String picSrc; + private ItemType itemType; // WEAPON, ARMOR, ARTIFACT + private Integer baseStat; + private Integer strength; + private Integer agility; + private Integer intelligence; + private Integer luck; + private Stat mainStat; // STRENGTH, AGILITY, INTELLIGENCE, LUCK + private Grade grade; // COMMON, UNCOMMON, RARE, EPIC, LEGENDARY + private List itemEffects; + private Integer remainingUses; + private Long price; +} diff --git a/src/main/java/com/scriptopia/demo/dto/items/ItemDefRequest.java b/src/main/java/com/scriptopia/demo/dto/items/ItemDefRequest.java new file mode 100644 index 00000000..dc7816b6 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/items/ItemDefRequest.java @@ -0,0 +1,24 @@ +package com.scriptopia.demo.dto.items; + +import com.scriptopia.demo.domain.Grade; +import com.scriptopia.demo.domain.ItemType; +import com.scriptopia.demo.domain.Stat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ItemDefRequest { + + private String worldView; + private String location; + private String playerTrait; + private String previousStory; + +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/dto/items/ItemEffectDTO.java b/src/main/java/com/scriptopia/demo/dto/items/ItemEffectDTO.java new file mode 100644 index 00000000..54233867 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/items/ItemEffectDTO.java @@ -0,0 +1,17 @@ +package com.scriptopia.demo.dto.items; + +import com.scriptopia.demo.domain.EffectProbability; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ItemEffectDTO { + private EffectProbability effectProbability; + private String effectName; + private String description; +} diff --git a/src/main/java/com/scriptopia/demo/dto/items/ItemFastApiRequest.java b/src/main/java/com/scriptopia/demo/dto/items/ItemFastApiRequest.java new file mode 100644 index 00000000..6a37658e --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/items/ItemFastApiRequest.java @@ -0,0 +1,32 @@ +package com.scriptopia.demo.dto.items; + +import com.scriptopia.demo.domain.EffectProbability; +import com.scriptopia.demo.domain.Grade; +import com.scriptopia.demo.domain.ItemType; +import com.scriptopia.demo.domain.Stat; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + + +@Data +@Builder +public class ItemFastApiRequest { + + private String worldView; + private String location; + private ItemType category; + private int baseStat; + private Stat mainStat; + private Grade grade; + private List itemEffects; + private int strength; + private int agility; + private int intelligence; + private int luck; + private Long price; + private String playerTrait; + private String previousStory; + +} diff --git a/src/main/java/com/scriptopia/demo/dto/items/ItemFastApiResponse.java b/src/main/java/com/scriptopia/demo/dto/items/ItemFastApiResponse.java new file mode 100644 index 00000000..8b34d63a --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/items/ItemFastApiResponse.java @@ -0,0 +1,25 @@ +package com.scriptopia.demo.dto.items; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ItemFastApiResponse { + + private String itemName; + private String itemDescription; + private List itemEffects; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ItemEffect { + private String itemEffectName; + private String itemEffectDescription; + } +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/dto/oauth/LoginStatus.java b/src/main/java/com/scriptopia/demo/dto/oauth/LoginStatus.java new file mode 100644 index 00000000..d7905e04 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/oauth/LoginStatus.java @@ -0,0 +1,5 @@ +package com.scriptopia.demo.dto.oauth; + +public enum LoginStatus { + LOGIN_SUCCESS, SIGNUP_REQUIRED +} diff --git a/src/main/java/com/scriptopia/demo/dto/oauth/OAuthLoginResponse.java b/src/main/java/com/scriptopia/demo/dto/oauth/OAuthLoginResponse.java new file mode 100644 index 00000000..14c94d1b --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/oauth/OAuthLoginResponse.java @@ -0,0 +1,17 @@ +package com.scriptopia.demo.dto.oauth; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class OAuthLoginResponse { + private LoginStatus status; + private String accessToken; + private String signupToken; +} + diff --git a/src/main/java/com/scriptopia/demo/dto/oauth/OAuthUserInfo.java b/src/main/java/com/scriptopia/demo/dto/oauth/OAuthUserInfo.java new file mode 100644 index 00000000..e5f3b0a7 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/oauth/OAuthUserInfo.java @@ -0,0 +1,18 @@ +package com.scriptopia.demo.dto.oauth; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class OAuthUserInfo { + private String id; + private String email; + private String name; + private String profileImage; + private String provider; + +} diff --git a/src/main/java/com/scriptopia/demo/dto/oauth/SocialSignupRequest.java b/src/main/java/com/scriptopia/demo/dto/oauth/SocialSignupRequest.java new file mode 100644 index 00000000..2f099a22 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/oauth/SocialSignupRequest.java @@ -0,0 +1,18 @@ +package com.scriptopia.demo.dto.oauth; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SocialSignupRequest { + @NotBlank(message = "E_400_MISSING_NICKNAME") + private String nickname; + private String deviceId; + private String signupToken; +} diff --git a/src/main/java/com/scriptopia/demo/dto/piashop/PiaItemRequest.java b/src/main/java/com/scriptopia/demo/dto/piashop/PiaItemRequest.java new file mode 100644 index 00000000..b5f4b8e1 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/piashop/PiaItemRequest.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.dto.piashop; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PiaItemRequest { + private String name; + private Long price; + private String description; +} diff --git a/src/main/java/com/scriptopia/demo/dto/piashop/PiaItemResponse.java b/src/main/java/com/scriptopia/demo/dto/piashop/PiaItemResponse.java new file mode 100644 index 00000000..3c580070 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/piashop/PiaItemResponse.java @@ -0,0 +1,27 @@ +package com.scriptopia.demo.dto.piashop; + +import com.scriptopia.demo.domain.PiaItem; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PiaItemResponse { + private Long id; + private String name; + private Long price; + private String description; + + + public static PiaItemResponse fromEntity(PiaItem item) { + return new PiaItemResponse( + item.getId(), + item.getName(), + item.getPrice(), + item.getDescription() + ); + } + +} diff --git a/src/main/java/com/scriptopia/demo/dto/piashop/PiaItemUpdateRequest.java b/src/main/java/com/scriptopia/demo/dto/piashop/PiaItemUpdateRequest.java new file mode 100644 index 00000000..e2794ac2 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/piashop/PiaItemUpdateRequest.java @@ -0,0 +1,14 @@ +package com.scriptopia.demo.dto.piashop; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PiaItemUpdateRequest { + private String name; // ์ด๋ฆ„ + private Long price; // ๊ฐ€๊ฒฉ + private String description; // ์„ค๋ช… +} diff --git a/src/main/java/com/scriptopia/demo/dto/piashop/PurchasePiaItemRequest.java b/src/main/java/com/scriptopia/demo/dto/piashop/PurchasePiaItemRequest.java new file mode 100644 index 00000000..c1fcec13 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/piashop/PurchasePiaItemRequest.java @@ -0,0 +1,14 @@ +package com.scriptopia.demo.dto.piashop; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PurchasePiaItemRequest { + private String itemId; + private int quantity; +} diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgame/CursorPage.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/CursorPage.java new file mode 100644 index 00000000..61af4e35 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/CursorPage.java @@ -0,0 +1,6 @@ +package com.scriptopia.demo.dto.sharedgame; + +import java.util.List; +import java.util.UUID; + +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/MySharedGameResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/MySharedGameResponse.java new file mode 100644 index 00000000..c34d029b --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/MySharedGameResponse.java @@ -0,0 +1,29 @@ +package com.scriptopia.demo.dto.sharedgame; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@Data +public class MySharedGameResponse { + private UUID shared_game_uuid; + private String thumbnailUrl; + private boolean recommand; + private Long totalPlayed; + private String title; + private String worldView; + private String backgroundStory; + private LocalDateTime sharedAt; + private List tags; + + @Data + public static class TagDto { + private String tagName; + + public TagDto(String tagName) { + this.tagName = tagName; + } + } +} \ 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 new file mode 100644 index 00000000..c646a976 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameDetailResponse.java @@ -0,0 +1,49 @@ +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; +import java.util.List; +import java.util.UUID; + +@Data +public class PublicSharedGameDetailResponse { + 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; + private List tags; + private List topScores; + + @Data + public static class TagDto { + private Long tagId; + private String tagName; + + public TagDto(Long tagId, String tagName) { + this.tagId = tagId; + this.tagName = 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 new file mode 100644 index 00000000..c23a9f06 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicSharedGameResponse.java @@ -0,0 +1,18 @@ +package com.scriptopia.demo.dto.sharedgame; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@Data +public class PublicSharedGameResponse { + private UUID sharedGameUuid; + private String thumbnailUrl; + private String title; + private Long playCount; + + private List tags; + +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicTagDefResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicTagDefResponse.java new file mode 100644 index 00000000..7f5e3668 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/PublicTagDefResponse.java @@ -0,0 +1,11 @@ +package com.scriptopia.demo.dto.sharedgame; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class PublicTagDefResponse { + private Long tagId; + private String tagName; +} diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgame/SharedGameRequest.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/SharedGameRequest.java new file mode 100644 index 00000000..25a2f758 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/SharedGameRequest.java @@ -0,0 +1,12 @@ +package com.scriptopia.demo.dto.sharedgame; + +import com.scriptopia.demo.domain.User; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +public class SharedGameRequest { + private UUID uuid; +} 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/sharedgame/TagDto.java b/src/main/java/com/scriptopia/demo/dto/sharedgame/TagDto.java new file mode 100644 index 00000000..a819c668 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/sharedgame/TagDto.java @@ -0,0 +1,11 @@ +package com.scriptopia.demo.dto.sharedgame; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class TagDto { + private Long tagId; + private String tagName; +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/dto/sharedgamefavorite/SharedGameFavoriteResponse.java b/src/main/java/com/scriptopia/demo/dto/sharedgamefavorite/SharedGameFavoriteResponse.java new file mode 100644 index 00000000..093574da --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/sharedgamefavorite/SharedGameFavoriteResponse.java @@ -0,0 +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 String sharedGameUuid; + private String thumbnailUrl; + + @JsonProperty("isLiked") + private boolean isLiked; + + private Long likeCount; + private Long totalPlayCount; + private String title; + private List tags; + private Long topScore; +} diff --git a/src/main/java/com/scriptopia/demo/dto/token/RefreshRequest.java b/src/main/java/com/scriptopia/demo/dto/token/RefreshRequest.java new file mode 100644 index 00000000..0a8ee97a --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/token/RefreshRequest.java @@ -0,0 +1,12 @@ +package com.scriptopia.demo.dto.token; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class RefreshRequest { + private String deviceId; +} diff --git a/src/main/java/com/scriptopia/demo/dto/usercharacterimg/UserCharacterImgResponse.java b/src/main/java/com/scriptopia/demo/dto/usercharacterimg/UserCharacterImgResponse.java new file mode 100644 index 00000000..e361f732 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/usercharacterimg/UserCharacterImgResponse.java @@ -0,0 +1,9 @@ +package com.scriptopia.demo.dto.usercharacterimg; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +public class UserCharacterImgResponse { + private String imgUrl; +} diff --git a/src/main/java/com/scriptopia/demo/dto/users/PiaItemDTO.java b/src/main/java/com/scriptopia/demo/dto/users/PiaItemDTO.java new file mode 100644 index 00000000..df125513 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/users/PiaItemDTO.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.dto.users; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PiaItemDTO { + private String name; + private Long quantity; +} diff --git a/src/main/java/com/scriptopia/demo/dto/users/UserAssetsResponse.java b/src/main/java/com/scriptopia/demo/dto/users/UserAssetsResponse.java new file mode 100644 index 00000000..ec4b0dbe --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/users/UserAssetsResponse.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.dto.users; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class UserAssetsResponse { + private Long pia; +} 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/UserSettingsDTO.java b/src/main/java/com/scriptopia/demo/dto/users/UserSettingsDTO.java new file mode 100644 index 00000000..0ed93cf3 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/dto/users/UserSettingsDTO.java @@ -0,0 +1,39 @@ +package com.scriptopia.demo.dto.users; + +import com.scriptopia.demo.domain.FontType; +import com.scriptopia.demo.domain.Theme; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class UserSettingsDTO { + + @NotNull(message = "E_400") + private Theme theme; + + @NotNull(message = "E_400") + @Min(value = 10, message = "E_400") + @Max(value = 24, message = "E_400") + private Integer fontSize; + + @NotNull(message = "E_400") + private FontType font; + + @NotNull(message = "E_400") + @Min(value = 1, message = "E_400") + @Max(value = 10, message = "E_400") + private Integer lineHeight; + + @NotNull(message = "E_400") + @Min(value = 1, message = "E_400") + @Max(value = 10, message = "E_400") + private Integer wordSpacing; +} 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/CustomException.java b/src/main/java/com/scriptopia/demo/exception/CustomException.java new file mode 100644 index 00000000..264340d8 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/exception/CustomException.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.exception; + + +import lombok.Getter; + +@Getter +public class CustomException extends RuntimeException { + private final ErrorCode errorCode; + + public CustomException(final ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + +} diff --git a/src/main/java/com/scriptopia/demo/exception/ErrorCode.java b/src/main/java/com/scriptopia/demo/exception/ErrorCode.java new file mode 100644 index 00000000..792f854b --- /dev/null +++ b/src/main/java/com/scriptopia/demo/exception/ErrorCode.java @@ -0,0 +1,113 @@ +package com.scriptopia.demo.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.boot.autoconfigure.graphql.GraphQlProperties; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorCode { + + + //400 Bad Request + E_400("E400000", "์ž˜๋ชป๋œ ์š”์ฒญ ํ˜•์‹์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_MISSING_EMAIL("E400001", "์ด๋ฉ”์ผ์€ ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_INVALID_EMAIL_FORMAT("E400002","์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.",HttpStatus.BAD_REQUEST), + E_400_INVALID_CODE("E400003", "์ธ์ฆ ์ฝ”๋“œ๋Š” 6์ž๋ฆฌ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_MISSING_PASSWORD("E400004", "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_PASSWORD_SIZE("E400005", "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8~20์ž๋ฆฌ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_PASSWORD_COMPLEXITY("E400006", "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์†Œ๋ฌธ์ž, ์ˆซ์ž, ํŠน์ˆ˜๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_MISSING_NICKNAME("E400007", "๋‹‰๋„ค์ž„์€ ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_REFRESH_REQUIRED("E400008", "๋ฆฌํ”„๋ ˆ์‰ฌ ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_PASSWORD_CONFIRM_MISMATCH("E400009", "์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ์ด ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.",HttpStatus.BAD_REQUEST), + E_400_PASSWORD_WHITESPACE("E400010","๋น„๋ฐ€๋ฒˆํ˜ธ์— ๊ณต๋ฐฑ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.",HttpStatus.BAD_REQUEST), + E_400_INSUFFICIENT_PIA("E400011", "๊ธˆ์•ก์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_SELF_PURCHASE("E400012", "์ž๊ธฐ ๋ฌผ๊ฑด์€ ๊ตฌ๋งคํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_INVALID_USER_ITEM_ID("E400013", "์ž˜๋ชป๋œ ์•„์ดํ…œ ID ํ˜•์‹์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_ITEM_NOT_OWNED("E400014", "ํ•ด๋‹น ์•„์ดํ…œ์€ ์‚ฌ์šฉ์ž๊ฐ€ ์†Œ์œ ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_ITEM_NOT_TRADE_ABLE("E400015", "ํ•ด๋‹น ์•„์ดํ…œ์€ ํ˜„์žฌ ๊ฒฝ๋งค์žฅ์— ์˜ฌ๋ฆด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_ITEM_ALREADY_REGISTERED("E400016", "์ด๋ฏธ ๊ฒฝ๋งค์žฅ์— ๋“ฑ๋ก๋œ ์•„์ดํ…œ์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_INVALID_AMOUNT("E400017", "๊ธˆ์•ก์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_MISSING_JWT("E400018", "ํ† ํฐ ๊ฐ’์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_PIA_ITEM_DUPLICATE("E400019", "์ด๋ฏธ ์กด์žฌํ•˜๋Š” PIA ์•„์ดํ…œ ์ด๋ฆ„์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_INVALID_REQUEST("E400020", "์ด๋ฆ„์ด๋‚˜, ๊ธˆ์•ก์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_GAME_ALREADY_IN_PROGRESS("E400021", "์ง„ํ–‰ ์ค‘์ธ ๊ฒŒ์ž„์ด ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_INVALID_SOCIAL_LOGIN_CODE("E400022", "์œ ํšจํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋œ ์ธ์ฆ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_NO_EMAIL("E400023", "์†Œ์…œ ๊ณ„์ •์—์„œ ์ด๋ฉ”์ผ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_UNSUPPORTED_PROVIDER("E400024", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ณต๊ธ‰์ž์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_ITEM_NO_USES_LEFT("E400025", "์•„์ดํ…œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ํšŸ์ˆ˜๊ฐ€ ๋‚จ์•„์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_EMPTY_FILE("E400026", "ํŒŒ์ผ์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_INVALID_NPC_RANK("E400027", "์ž˜๋ชป๋œ NPC ๋žญํฌ์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_INVALID_ENUM_TYPE("E400028","์š”์ฒญ ๊ฐ’์ด ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (Enum ํƒ€์ž… ํ™•์ธ ํ•„์š”)",HttpStatus.BAD_REQUEST), + E_400_TAG_DUPLICATED("E400029", "์ค‘๋ณต๋œ ํƒœ๊ทธ์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + E_400_IMAGE_URL_ERROR("E40030", "์š”์ฒญ๊ฐ’์ด ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + + //401 Unauthorized + E_401("401000", "์ธ์ฆ๋˜์ง€ ์•Š์€ ์š”์ฒญ์ž…๋‹ˆ๋‹ค. (ํ† ํฐ ์—†์Œ, ๋งŒ๋ฃŒ, ์ž˜๋ชป๋จ)",HttpStatus.UNAUTHORIZED), + E_401_INVALID_CREDENTIALS("E401001","์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.UNAUTHORIZED), + E_401_CODE_MISMATCH("E401002","์ธ์ฆ ์ฝ”๋“œ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.UNAUTHORIZED), + E_401_REFRESH_EXPIRED("E401003","๋ฆฌํ”„๋ ˆ์‰ฌ ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.UNAUTHORIZED), + E_401_CURRENT_PASSWORD_MISMATCH("E401004","ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.",HttpStatus.UNAUTHORIZED), + E_401_INVALID_SIGNATURE("E401005", "JWT ์„œ๋ช…์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.UNAUTHORIZED), + E_401_MALFORMED("E401006", "JWT ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.UNAUTHORIZED), + E_401_EXPIRED_JWT("E401007", "JWT ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.UNAUTHORIZED), + E_401_UNSUPPORTED_JWT("E401008", "์ง€์›ํ•˜์ง€ ์•Š๋Š” JWT ํ˜•์‹์ž…๋‹ˆ๋‹ค.", HttpStatus.UNAUTHORIZED), + E_401_NOT_EQUAL_SHARED_GAME("E401009", "์‚ฌ์šฉ์ž๊ฐ€ ๊ณต์œ ํ•œ ๊ฒŒ์ž„์ด ์•„๋‹™๋‹ˆ๋‹ค.", HttpStatus.UNAUTHORIZED), + + //403 Forbidden + E_403("E403000", "์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.", HttpStatus.FORBIDDEN), + E_403_DEVICE_MISMATCH("E403001", "์š”์ฒญ ๋””๋ฐ”์ด์Šค์™€ ํ† ํฐ์˜ ๋””๋ฐ”์ด์Šค๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.FORBIDDEN), + E_403_SETTLEMENT_FORBIDDEN("E403002", "ํ•ด๋‹น ์ •์‚ฐ ๋‚ด์—ญ์— ์ ‘๊ทผํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.", HttpStatus.FORBIDDEN), + + + //404 Not Found + E_404("E404000","์š”์ฒญํ•˜์‹  ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.",HttpStatus.NOT_FOUND), + E_404_REFRESH_NOT_FOUND("E404001", "์œ ํšจํ•œ ๋ฆฌํ”„๋ ˆ์‹œ ์„ธ์…˜์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.",HttpStatus.NOT_FOUND), + E_404_USER_NOT_FOUND("E404002","์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.",HttpStatus.NOT_FOUND), + E_404_AUCTION_NOT_FOUND("E404003", "ํ•ด๋‹น ์•„์ดํ…œ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), + E_404_SETTLEMENT_NOT_FOUND("E404004","์ •์‚ฐ ๋‚ด์—ญ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), + E_404_SHARED_GAME_NOT_FOUND("E404005", "๊ณต์œ ๋œ ๊ฒŒ์ž„์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), + E_404_GAME_SESSION_NOT_FOUND("E404006", "๊ฒŒ์ž„์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), + E_404_STORED_GAME_NOT_FOUND("E404007", "์ €์žฅ๋œ ๊ฒŒ์ž„์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), + E_404_Duplicated_Game_Session("E404008", "์ด๋ฏธ ์ €์žฅ๋œ ๊ฒŒ์ž„์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), + E_404_ITEM_NOT_FOUND("E404009", "์•„์ดํ…œ์ด ์—†์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), + E_404_PAGE_NOT_FOUND("E404010", "ํŽ˜์ด์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), + E_404_SETTING_NOT_FOUND("E404011", "์œ ์ € ์„ค์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), + E_404_Tag_NOT_FOUND("E404012", "ํƒœ๊ทธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), + + + + //409 Conflict + E_409_EMAIL_TAKEN("E409001", "์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.", HttpStatus.CONFLICT), + E_409_NICKNAME_TAKEN("E409002", "์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ๋‹‰๋„ค์ž„์ž…๋‹ˆ๋‹ค.", HttpStatus.CONFLICT), + 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 + E_412_EMAIL_NOT_VERIFIED("E412001", "์ด๋ฉ”์ผ ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.",HttpStatus.PRECONDITION_FAILED), + + //500 Internal Server Error + E_500("E_500000", "์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR), + E_500_TOKEN_HASHING_FAILED("E_500001","๋ฆฌํ”„๋ ˆ์‰ฌ ํ† ํฐ ํ•ด์‹ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.",HttpStatus.INTERNAL_SERVER_ERROR), + E_500_EXTERNAL_API_ERROR("E500002", "์™ธ๋ถ€ ๊ฒŒ์ž„ API ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR), + E_500_DATABASE_ERROR("E500003", "๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR), + E_500_TOKEN_CREATION_FAILED("E500004", "์ธ์ฆ ํ† ํฐ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR), + E_500_TOKEN_STORAGE_FAILED("E500005", "๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR), + E_500_File_SAVED_FAILED("E500006", "ํŒŒ์ผ ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR), + + //502 BAD_GATEWAY + E_502_OAUTH_SERVER_ERROR("E502001", "์†Œ์…œ ๋กœ๊ทธ์ธ ์„œ๋ฒ„์™€์˜ ํ†ต์‹ ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_GATEWAY); + + + private final String code; + private final String message; + private final HttpStatus status; + +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/exception/GlobalExceptionHandler.java b/src/main/java/com/scriptopia/demo/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..ed422c1d --- /dev/null +++ b/src/main/java/com/scriptopia/demo/exception/GlobalExceptionHandler.java @@ -0,0 +1,73 @@ +package com.scriptopia.demo.exception; + +import io.jsonwebtoken.ExpiredJwtException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import com.scriptopia.demo.dto.exception.ErrorResponse; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidation(MethodArgumentNotValidException e) { + FieldError fieldError = e.getBindingResult().getFieldError(); + + ErrorCode errorCode = ErrorCode.E_400; + if (fieldError != null && fieldError.getDefaultMessage() != null) { + errorCode = ErrorCode.valueOf(fieldError.getDefaultMessage()); + } + + return ResponseEntity.badRequest().body(new ErrorResponse(errorCode)); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleEnumBinding(HttpMessageNotReadableException ex) { + + return ResponseEntity + .status(ErrorCode.E_400.getStatus()) + .body(new ErrorResponse(ErrorCode.E_400)); + } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDenied(AccessDeniedException ex) { + return ResponseEntity + .status(ErrorCode.E_403.getStatus()) + .body(new ErrorResponse(ErrorCode.E_403)); + } + + @ExceptionHandler(CustomException.class) + public ResponseEntity handleCustomException(final CustomException e) { + ErrorCode errorCode = e.getErrorCode(); + + return ResponseEntity + .status(errorCode.getStatus()) + .body(new ErrorResponse(errorCode)); + } + + + @ExceptionHandler(ExpiredJwtException.class) + public ResponseEntity handleExpired(ExpiredJwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(new ErrorResponse(ErrorCode.E_401_REFRESH_EXPIRED)); + } + + + @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 new file mode 100644 index 00000000..3a71ba9a --- /dev/null +++ b/src/main/java/com/scriptopia/demo/mapper/InGameMapper.java @@ -0,0 +1,140 @@ +package com.scriptopia.demo.mapper; + + +import com.scriptopia.demo.domain.mongo.*; +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; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class InGameMapper { + private final ItemDefMongoRepository itemDefMongoRepository; + + + + public InGamePlayerResponse mapPlayer(PlayerInfoMongo player) { + if (player == null) return null; + return InGamePlayerResponse.builder() + .name(player.getName()) + .life(player.getLife()) + .level(player.getLevel()) + .healthPoint(player.getHealthPoint()) + .experiencePoint(player.getExperiencePoint()) + .trait(player.getTrait()) + .strength(player.getStrength()) + .agility(player.getAgility()) + .intelligence(player.getIntelligence()) + .luck(player.getLuck()) + .gold(player.getGold()) + .build(); + } + + public InGameNpcResponse mapNpc(NpcInfoMongo npc) { + if (npc == null) return null; + return InGameNpcResponse.builder() + .name(npc.getName()) + .rank(npc.getRank()) + .trait(npc.getTrait()) + .strength(npc.getStrength()) + .agility(npc.getAgility()) + .intelligence(npc.getIntelligence()) + .luck(npc.getLuck()) + .npcWeaponName(npc.getNpcWeaponName()) + .npcWeaponDescription(npc.getNpcWeaponDescription()) + .build(); + } + + public List mapInventory(List inventoryList) { + if (inventoryList == null) return List.of(); + + return inventoryList.stream() + .map(inv -> { + ItemDefMongo itemDef = itemDefMongoRepository.findById(inv.getItemDefId()) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND)); + + return InGameInventoryResponse.builder() + // ์ธ๋ฒคํ† ๋ฆฌ(์†Œ์œ ) ์ •๋ณด + .itemDefId(inv.getItemDefId()) + .acquiredAt(inv.getAcquiredAt()) + .equipped(inv.isEquipped()) + .source(inv.getSource()) + + // ์•„์ดํ…œ ์ •์˜ ์ •๋ณด + .name(itemDef.getName()) + .description(itemDef.getDescription()) + .itemPicSrc(itemDef.getItemPicSrc()) + .category(itemDef.getCategory().name()) + .baseStat(itemDef.getBaseStat()) + .itemEffects(itemDef.getItemEffect().stream() + .map(e -> InGameInventoryResponse.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(); + } + + public List mapChoice(ChoiceInfoMongo choiceInfo) { + if (choiceInfo == null || choiceInfo.getChoice() == null) return List.of(); + + return choiceInfo.getChoice().stream() + .limit(3) // ์ตœ๋Œ€ 3๊ฐœ๋งŒ + .map(ch -> InGameChoiceResponse.Choice.builder() + .detail(ch.getDetail()) + .stats(ch.getStats() != null ? ch.getStats().name() : null) // null-safe + .probability(ch.getProbability()) + .build()) + .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/mapper/ItemMapper.java b/src/main/java/com/scriptopia/demo/mapper/ItemMapper.java new file mode 100644 index 00000000..7664e0d1 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/mapper/ItemMapper.java @@ -0,0 +1,70 @@ +package com.scriptopia.demo.mapper; + +import com.scriptopia.demo.domain.ItemDef; +import com.scriptopia.demo.domain.ItemEffect; +import com.scriptopia.demo.domain.User; +import com.scriptopia.demo.domain.UserItem; +import com.scriptopia.demo.dto.items.ItemDTO; +import com.scriptopia.demo.dto.items.ItemEffectDTO; +import com.scriptopia.demo.repository.UserItemRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class ItemMapper { + + + private final UserItemRepository userItemRepository; + + public List mapUser(User user) { + List items = new ArrayList<>(); + + List userItems = userItemRepository.findAllByUserId(user.getId()); + + for (UserItem userItem : userItems) { + ItemDef item = userItem.getItemDef(); + + List itemEffects = new ArrayList<>(); + + for (ItemEffect effect : item.getItemEffects()) { + itemEffects.add( + ItemEffectDTO.builder() + .effectProbability(effect.getEffectGradeDef().getEffectProbability()) + .effectName(effect.getEffectName()) + .description(effect.getEffectDescription()) + .build() + ); + + } + + items.add( + ItemDTO.builder() + .name(item.getName()) + .description(item.getDescription()) + .picSrc(item.getPicSrc()) + .itemType(item.getItemType()) + .baseStat(item.getBaseStat()) + .strength(item.getStrength()) + .agility(item.getAgility()) + .intelligence(item.getIntelligence()) + .luck(item.getLuck()) + .mainStat(item.getMainStat()) + .grade(item.getItemGradeDef().getGrade()) + .itemEffects(itemEffects) // ๋ฆฌ์ŠคํŠธ ์ฃผ์ž… + .remainingUses(userItem.getRemainingUses()) + .price(item.getPrice()) + .build() + ); + } + return items; + } + + + + + +} diff --git a/src/main/java/com/scriptopia/demo/record/RefreshSession.java b/src/main/java/com/scriptopia/demo/record/RefreshSession.java new file mode 100644 index 00000000..a7d44c4d --- /dev/null +++ b/src/main/java/com/scriptopia/demo/record/RefreshSession.java @@ -0,0 +1,18 @@ +package com.scriptopia.demo.record; + +import com.fasterxml.jackson.annotation.JsonInclude; + + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record RefreshSession( + Long userId, + String jti, + String tokenHash, // refresh ์›๋ฌธ ํ•ด์‹œ + String deviceId, + long expEpochSec, + long createdEpochSec, + String ip, + String ua +) { + +} diff --git a/src/main/java/com/scriptopia/demo/repository/AuctionRepository.java b/src/main/java/com/scriptopia/demo/repository/AuctionRepository.java new file mode 100644 index 00000000..1c49c201 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/AuctionRepository.java @@ -0,0 +1,68 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +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 AuctionRepository extends JpaRepository { + boolean existsByUserItem(UserItem userItem); + + + // itemName ์šฐ์„  ๊ฒ€์ƒ‰ (์—ฐ๊ด€ ํ…Œ์ด๋ธ” ์กฐ์ธ) + @Query(""" + SELECT DISTINCT a FROM Auction a + JOIN FETCH a.userItem ui + JOIN FETCH ui.user u + JOIN FETCH ui.itemDef idf + LEFT JOIN FETCH idf.itemEffects ie + LEFT JOIN FETCH ie.effectGradeDef e + WHERE LOWER(idf.name) LIKE LOWER(CONCAT('%', :itemName, '%')) + ORDER BY a.createdAt DESC + """) + Page findByItemName(@Param("itemName") String itemName, Pageable pageable); + + + // ํ•„ํ„ฐ ์กฐ๊ฑด ๊ฒ€์ƒ‰ (itemName ์—†๋Š” ๊ฒฝ์šฐ) + @Query(""" + SELECT DISTINCT a + FROM Auction a + JOIN a.userItem ui + JOIN ui.itemDef id + LEFT JOIN id.itemEffects ie + WHERE (:category IS NULL OR id.itemType = :category) + AND (:grade IS NULL OR id.itemGradeDef.grade = :grade) + AND (:minPrice IS NULL OR a.price >= :minPrice) + AND (:maxPrice IS NULL OR a.price <= :maxPrice) + AND (:stat IS NULL OR id.mainStat = :stat) + AND ( + :effectGrades IS NULL + OR EXISTS ( + SELECT 1 FROM ItemEffect ie2 + WHERE ie2.itemDef = id + AND ie2.effectGradeDef.effectProbability IN :effectGrades + ) + ) +""") + Page findByFilters( + @Param("category") ItemType category, + @Param("grade") Grade grade, + @Param("minPrice") Long minPrice, + @Param("maxPrice") Long maxPrice, + @Param("stat") Stat stat, + @Param("effectGrades") List effectGrades, + Pageable pageable + ); + + + Page findByUserItem_User_IdAndUserItem_TradeStatus( + Long userId, + TradeStatus tradeStatus, + Pageable pageable + ); +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/repository/EffectGradeDefRepository.java b/src/main/java/com/scriptopia/demo/repository/EffectGradeDefRepository.java new file mode 100644 index 00000000..361a3a3c --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/EffectGradeDefRepository.java @@ -0,0 +1,18 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.EffectGradeDef; +import com.scriptopia.demo.domain.EffectProbability; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface EffectGradeDefRepository extends JpaRepository { + Optional findByEffectProbability(EffectProbability effectProbability); + + @Query("SELECT egd.price FROM EffectGradeDef egd WHERE egd.effectProbability = :effectProbability") + Optional findPriceByEffectProbability(@Param("effectProbability") EffectProbability effectProbability); + +} diff --git a/src/main/java/com/scriptopia/demo/repository/GameSessionRepository.java b/src/main/java/com/scriptopia/demo/repository/GameSessionRepository.java new file mode 100644 index 00000000..34253bcf --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/GameSessionRepository.java @@ -0,0 +1,22 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.GameSession; +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; +import java.util.Optional; + +public interface GameSessionRepository extends JpaRepository { + Optional findByUserIdAndMongoId(Long userId, String mongoId); + + @Query("select g from GameSession g join g.user u where u.id = :userId") + Optional findByMongoId(@Param("userId") Long userId); + + @Query("select case when (Count(g) > 0) then true else false end from GameSession g join g.user u where u.id = :userId") + boolean existsByUserId(@Param("userId") Long userId); + + boolean existsByMongoId(String mongoId); + +} diff --git a/src/main/java/com/scriptopia/demo/repository/GameTagRepository.java b/src/main/java/com/scriptopia/demo/repository/GameTagRepository.java new file mode 100644 index 00000000..863b2da1 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/GameTagRepository.java @@ -0,0 +1,27 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.GameTag; +import com.scriptopia.demo.dto.TagDef.TagDefCreateRequest; +import com.scriptopia.demo.dto.sharedgame.MySharedGameResponse; +import com.scriptopia.demo.dto.sharedgame.TagDto; +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 GameTagRepository extends JpaRepository { + @Query("select gt.tagDef.tagName " + + "from GameTag gt " + + "where gt.sharedGame.id = :sharedGameId") + List findTagNamesBySharedGameId(@Param("sharedGameId") Long sharedGameId); + + @Query(""" + select new com.scriptopia.demo.dto.sharedgame.TagDto(td.id, td.tagName) + from GameTag gt + join gt.tagDef td + where gt.sharedGame.id = :sharedGameId + order by td.tagName asc + """) + List findTagDtosBySharedGameId(@Param("sharedGameId") Long sharedGameId); +} diff --git a/src/main/java/com/scriptopia/demo/repository/HistoryRepository.java b/src/main/java/com/scriptopia/demo/repository/HistoryRepository.java new file mode 100644 index 00000000..ba430950 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/HistoryRepository.java @@ -0,0 +1,29 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.History; +import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +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; + +public interface HistoryRepository extends JpaRepository { + @Query("select h from History h where h.uuid = :uuid") + Optional findByUuid(@Param("uuid") UUID uuid); + + @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/ItemDefRepository.java b/src/main/java/com/scriptopia/demo/repository/ItemDefRepository.java new file mode 100644 index 00000000..df2e0a1a --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/ItemDefRepository.java @@ -0,0 +1,7 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.ItemDef; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ItemDefRepository extends JpaRepository { +} diff --git a/src/main/java/com/scriptopia/demo/repository/ItemEffectRepository.java b/src/main/java/com/scriptopia/demo/repository/ItemEffectRepository.java new file mode 100644 index 00000000..169422b9 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/ItemEffectRepository.java @@ -0,0 +1,8 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.ItemEffect; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ItemEffectRepository extends JpaRepository { + +} diff --git a/src/main/java/com/scriptopia/demo/repository/ItemGradeDefRepository.java b/src/main/java/com/scriptopia/demo/repository/ItemGradeDefRepository.java new file mode 100644 index 00000000..b2fce9f3 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/ItemGradeDefRepository.java @@ -0,0 +1,20 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.Grade; +import com.scriptopia.demo.domain.ItemGradeDef; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + + +public interface ItemGradeDefRepository extends JpaRepository { + Optional findByGrade(Grade grade); + + @Query("SELECT igm.price FROM ItemGradeDef igm WHERE igm.grade = :grade") + Long findPriceByGrade(@Param("grade") Grade grade); + + +} diff --git a/src/main/java/com/scriptopia/demo/repository/LocalAccountRepository.java b/src/main/java/com/scriptopia/demo/repository/LocalAccountRepository.java new file mode 100644 index 00000000..229a1207 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/LocalAccountRepository.java @@ -0,0 +1,19 @@ +package com.scriptopia.demo.repository; + +import aj.org.objectweb.asm.commons.Remapper; +import com.scriptopia.demo.domain.LocalAccount; +import com.scriptopia.demo.domain.User; +import org.springframework.cglib.core.Local; +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; +import java.util.Optional; + +public interface LocalAccountRepository extends JpaRepository { + Optional findByEmail(String email); + Optional findByUserId(Long user_id); + boolean existsByEmail(String email); +} + diff --git a/src/main/java/com/scriptopia/demo/repository/PiaItemRepository.java b/src/main/java/com/scriptopia/demo/repository/PiaItemRepository.java index 836af29d..f19585f4 100644 --- a/src/main/java/com/scriptopia/demo/repository/PiaItemRepository.java +++ b/src/main/java/com/scriptopia/demo/repository/PiaItemRepository.java @@ -1,8 +1,15 @@ package com.scriptopia.demo.repository; +import com.scriptopia.demo.domain.LocalAccount; import com.scriptopia.demo.domain.PiaItem; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface PiaItemRepository extends JpaRepository { + boolean existsByName(String name); // ์ด๋ฆ„์œผ๋กœ ์ค‘๋ณต ์ฒดํฌ + + boolean existsByNameAndIdNot(String name, Long id); + Optional findByName(String itemName); } diff --git a/src/main/java/com/scriptopia/demo/repository/PurchaseLogRepository.java b/src/main/java/com/scriptopia/demo/repository/PurchaseLogRepository.java new file mode 100644 index 00000000..172eb88f --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/PurchaseLogRepository.java @@ -0,0 +1,7 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.PiaItemPurchaseLog; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PurchaseLogRepository extends JpaRepository { +} diff --git a/src/main/java/com/scriptopia/demo/repository/RedisRefreshRepository.java b/src/main/java/com/scriptopia/demo/repository/RedisRefreshRepository.java new file mode 100644 index 00000000..c4014d15 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/RedisRefreshRepository.java @@ -0,0 +1,103 @@ +package com.scriptopia.demo.repository; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.scriptopia.demo.record.RefreshSession; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; +import java.util.Set; + +@Repository +@RequiredArgsConstructor +public class RedisRefreshRepository implements RefreshRepository { + + private final StringRedisTemplate redis; + private final ObjectMapper om; + + private static String kSession(long userId, String jti) { return "rt:%d:%s".formatted(userId, jti); } + private static String kUserIdx(long userId) {return "rt:idx:u:%d".formatted(userId); } + private static String kDeviceIdx(long userId, String deviceId) { return "rt:idx:u:%d:d:%s".formatted(userId, deviceId); } + + @Override + public void save(RefreshSession s) { + long now = Instant.now().getEpochSecond(); + long ttlSec = Math.max(1, s.expEpochSec() - now); + + String sessionKey = kSession(s.userId(), s.jti()); + String userIdxKey = kUserIdx(s.userId()); + + try { + String json = om.writeValueAsString(s); + + // ์„ธ์…˜ ์ €์žฅ + TTL + redis.opsForValue().set(sessionKey, json, Duration.ofSeconds(ttlSec)); + + // ์œ ์ € ์ธ๋ฑ์Šค(JTI ๋ชจ์Œ) ๊ฐฑ์‹  + redis.opsForSet().add(userIdxKey, s.jti()); + Long currentTtl = redis.getExpire(userIdxKey); + if (currentTtl == null || currentTtl < ttlSec) { + redis.expire(userIdxKey, Duration.ofSeconds(ttlSec)); + } + + // ๋””๋ฐ”์ด์Šค ์ธ๋ฑ์Šค + if (s.deviceId() != null) { + String deviceKey = kDeviceIdx(s.userId(), s.deviceId()); + + redis.opsForValue().set(deviceKey, s.jti(), Duration.ofSeconds(ttlSec)); + } + } catch (Exception e) { + throw new IllegalStateException("Failed to save refresh session", e); + } + } + + @Override + public Optional find(long userId, String jti) { + String json = redis.opsForValue().get(kSession(userId, jti)); + if (json == null) return Optional.empty(); + try { + return Optional.of(om.readValue(json, RefreshSession.class)); + } catch (Exception e) { + return Optional.empty(); + } + } + + @Override + public void delete(long userId, String jti) { + redis.delete(kSession(userId, jti)); + redis.opsForSet().remove(kUserIdx(userId), jti); + + } + + @Override + public Optional findByDevice(long userId, String deviceId) { + String jti = redis.opsForValue().get(kDeviceIdx(userId, deviceId)); + if (jti == null) return Optional.empty(); + return find(userId, jti); + } + + @Override + public void deleteByDevice(long userId, String deviceId) { + String deviceKey = kDeviceIdx(userId, deviceId); + String jti = redis.opsForValue().get(deviceKey); + if (jti != null) { + delete(userId, jti); + } + redis.delete(deviceKey); + } + + @Override + public void deleteAllForUser(long userId) { + String userIdxKey = kUserIdx(userId); + Set jtIs = redis.opsForSet().members(userIdxKey); + if (jtIs != null) { + for (String jti : jtIs) { + redis.delete(kSession(userId, jti)); + } + } + redis.delete(userIdxKey); + } +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/repository/RefreshRepository.java b/src/main/java/com/scriptopia/demo/repository/RefreshRepository.java new file mode 100644 index 00000000..a729e900 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/RefreshRepository.java @@ -0,0 +1,17 @@ +package com.scriptopia.demo.repository; + + +import com.scriptopia.demo.record.RefreshSession; + +import java.util.Optional; + +public interface RefreshRepository { + void save(RefreshSession session); + Optional find(long userId, String jti); + void delete(long userId, String jti); + + Optional findByDevice(long userId, String device); + void deleteByDevice(long userId, String device); + + void deleteAllForUser(long userId); +} diff --git a/src/main/java/com/scriptopia/demo/repository/SettlementRepository.java b/src/main/java/com/scriptopia/demo/repository/SettlementRepository.java new file mode 100644 index 00000000..6b52a911 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/SettlementRepository.java @@ -0,0 +1,13 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.PiaItem; +import com.scriptopia.demo.domain.Settlement; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SettlementRepository extends JpaRepository { + + // userId ๊ธฐ์ค€์œผ๋กœ ํŽ˜์ด์ง• ์กฐํšŒ + Page findByUserId(Long userId, Pageable pageable); +} diff --git a/src/main/java/com/scriptopia/demo/repository/SharedGameFavoriteRepository.java b/src/main/java/com/scriptopia/demo/repository/SharedGameFavoriteRepository.java new file mode 100644 index 00000000..49585b0d --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/SharedGameFavoriteRepository.java @@ -0,0 +1,23 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.SharedGame; +import com.scriptopia.demo.domain.SharedGameFavorite; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface SharedGameFavoriteRepository extends JpaRepository { + Optional findByUserIdAndSharedGameId(Long userId, Long sharedGameId); + + boolean existsByUserIdAndSharedGameId(Long userId, Long sharedGameId); + + long countBySharedGameId(Long sharedGameId); + + @Query(""" + Select case when Count(sgf) > 0 then true else false end from SharedGameFavorite sgf + join sgf.user u join sgf.sharedGame sg where u.id = :userId and sg.id = :sharedGameId + """) + boolean existsLikeSharedGame(@Param("userId") Long userId, @Param("sharedGameId") Long sharedGameId); +} diff --git a/src/main/java/com/scriptopia/demo/repository/SharedGameRepository.java b/src/main/java/com/scriptopia/demo/repository/SharedGameRepository.java new file mode 100644 index 00000000..b9ea57e8 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/SharedGameRepository.java @@ -0,0 +1,111 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.PiaItem; +import com.scriptopia.demo.domain.SharedGame; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface SharedGameRepository extends JpaRepository { + @Query("select sg from SharedGame sg where sg.user.id = :userId") + List findAllByUserid(@Param("userId") Long userId); + + @Query("select sg from SharedGame sg where sg.uuid = :uuid") + Optional findByUuid(@Param("uuid") UUID uuid); + + @Query("select g.sharedAt from SharedGame g where g.id = :id") + LocalDateTime findSharedAtById(@Param("id") Long id); + + // ์ตœ์‹ ์ˆœ + @Query(""" + select g + from SharedGame g + where (:tagEmpty = true + or exists (select 1 from GameTag gt where gt.sharedGame = g and gt.tagDef.id in :tagIds)) + and (:qBlank = true + or lower(coalesce(g.title,'')) like :qLike + or lower(coalesce(g.worldView,'')) like :qLike + or lower(coalesce(g.backgroundStory,'')) like :qLike) + and ( + :useCursor = false + or g.sharedAt < :lastKey + or (g.sharedAt = :lastKey and g.id < :lastId) + ) + order by g.sharedAt desc, g.id desc + """) + List sliceLatest( + @Param("tagIds") List tagIds, @Param("tagEmpty") boolean tagEmpty, + @Param("qLike") String qLike, @Param("qBlank") boolean qBlank, + @Param("useCursor") boolean useCursor, + @Param("lastKey") LocalDateTime lastKey, @Param("lastId") Long lastId, + Pageable pageable + ); + + @Query(""" + select g + from SharedGame g + where (:tagEmpty = true + or exists (select 1 from GameTag gt where gt.sharedGame = g and gt.tagDef.id in :tagIds)) + and (:qBlank = true + or lower(coalesce(g.title,'')) like :qLike + or lower(coalesce(g.worldView,'')) like :qLike + or lower(coalesce(g.backgroundStory,'')) like :qLike) + and ( + :useCursor = false + or ( + (select count(s.id) from SharedGameScore s where s.sharedGame = g) < :lastKey + or ( + (select count(s2.id) from SharedGameScore s2 where s2.sharedGame = g) = :lastKey + and g.id < :lastId + ) + ) + ) + order by (select count(s3.id) from SharedGameScore s3 where s3.sharedGame = g) desc, + g.id desc + """) + List slicePopular( + @Param("tagIds") List tagIds, @Param("tagEmpty") boolean tagEmpty, + @Param("qLike") String qLike, @Param("qBlank") boolean qBlank, + @Param("useCursor") boolean useCursor, + @Param("lastKey") Long lastKey, @Param("lastId") Long lastId, + Pageable pageable + ); + + @Query(""" + select g + from SharedGame g + where (:tagEmpty = true + or exists (select 1 from GameTag gt where gt.sharedGame = g and gt.tagDef.id in :tagIds)) + and (:qBlank = true + or lower(coalesce(g.title,'')) like :qLike + or lower(coalesce(g.worldView,'')) like :qLike + or lower(coalesce(g.backgroundStory,'')) like :qLike) + and ( + :useCursor = false + or ( + (select coalesce(max(s.score),0) from SharedGameScore s where s.sharedGame = g) < :lastKey + or ( + (select coalesce(max(s2.score),0) from SharedGameScore s2 where s2.sharedGame = g) = :lastKey + and g.id < :lastId + ) + ) + ) + order by (select coalesce(max(s3.score),0) from SharedGameScore s3 where s3.sharedGame = g) desc, + g.id desc + """) + List sliceTopScore( + @Param("tagIds") List tagIds, @Param("tagEmpty") boolean tagEmpty, + @Param("qLike") String qLike, @Param("qBlank") boolean qBlank, + @Param("useCursor") boolean useCursor, + @Param("lastKey") Long lastKey, @Param("lastId") Long lastId, + Pageable pageable + ); + +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/repository/SharedGameScoreRepository.java b/src/main/java/com/scriptopia/demo/repository/SharedGameScoreRepository.java new file mode 100644 index 00000000..3dd3b415 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/SharedGameScoreRepository.java @@ -0,0 +1,19 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.SharedGame; +import com.scriptopia.demo.domain.SharedGameScore; +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(@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); + + List findAllBySharedGameIdOrderByScoreDescCreatedAtDesc(Long sharedGameId); +} diff --git a/src/main/java/com/scriptopia/demo/repository/SocialAccountRepository.java b/src/main/java/com/scriptopia/demo/repository/SocialAccountRepository.java new file mode 100644 index 00000000..d0a30c98 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/SocialAccountRepository.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.Provider; +import com.scriptopia.demo.domain.SharedGameScore; +import com.scriptopia.demo.domain.SocialAccount; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface SocialAccountRepository extends JpaRepository { + + Optional findBySocialIdAndProvider(String id, Provider provider); + + boolean existsBySocialIdAndProvider(String socialId, Provider provider); +} diff --git a/src/main/java/com/scriptopia/demo/repository/TagDefRepository.java b/src/main/java/com/scriptopia/demo/repository/TagDefRepository.java new file mode 100644 index 00000000..9dae6bae --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/TagDefRepository.java @@ -0,0 +1,13 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.SocialAccount; +import com.scriptopia.demo.domain.TagDef; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface TagDefRepository extends JpaRepository { + boolean existsByTagName(String tagName); + + Optional findByTagName(String tagName); +} diff --git a/src/main/java/com/scriptopia/demo/repository/UserCharacterImgRepository.java b/src/main/java/com/scriptopia/demo/repository/UserCharacterImgRepository.java new file mode 100644 index 00000000..68d357e1 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/UserCharacterImgRepository.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.User; +import com.scriptopia.demo.domain.UserCharacterImg; +import org.springframework.data.jpa.repository.JpaRepository; + +import javax.swing.text.html.Option; +import java.util.List; +import java.util.Optional; + +public interface UserCharacterImgRepository extends JpaRepository { + Optional findByUserIdAndImgUrl(Long userId, String imgUrl); + boolean existsByUserIdAndImgUrl(Long userId, String imgUrl); + List findAllByUserId(Long userId); +} diff --git a/src/main/java/com/scriptopia/demo/repository/UserItemRepository.java b/src/main/java/com/scriptopia/demo/repository/UserItemRepository.java new file mode 100644 index 00000000..2ff90ea6 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/UserItemRepository.java @@ -0,0 +1,21 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.ItemDef; +import com.scriptopia.demo.domain.TradeStatus; +import com.scriptopia.demo.domain.User; +import com.scriptopia.demo.domain.UserItem; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface UserItemRepository extends JpaRepository { + Optional findByItemDefAndTradeStatus(ItemDef itemDef, TradeStatus tradeStatus); + + Optional findByUserIdAndItemDefId(Long userId, Long itemDefId); + + List findAllByUserId(Long userId); + + +} diff --git a/src/main/java/com/scriptopia/demo/repository/UserPiaItemRepository.java b/src/main/java/com/scriptopia/demo/repository/UserPiaItemRepository.java new file mode 100644 index 00000000..4f162d5a --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/UserPiaItemRepository.java @@ -0,0 +1,15 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.PiaItem; +import com.scriptopia.demo.domain.User; +import com.scriptopia.demo.domain.UserPiaItem; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface UserPiaItemRepository extends JpaRepository { + Optional findByUserAndPiaItem(User user, PiaItem piaItem); + + List findByUserId(Long UserId); +} diff --git a/src/main/java/com/scriptopia/demo/repository/UserRepository.java b/src/main/java/com/scriptopia/demo/repository/UserRepository.java new file mode 100644 index 00000000..0f9ded90 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/UserRepository.java @@ -0,0 +1,11 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.TagDef; +import com.scriptopia.demo.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; + + +public interface UserRepository extends JpaRepository { + + boolean existsByNickname(String nickname); +} diff --git a/src/main/java/com/scriptopia/demo/repository/UserSettingRepository.java b/src/main/java/com/scriptopia/demo/repository/UserSettingRepository.java new file mode 100644 index 00000000..54a32036 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/UserSettingRepository.java @@ -0,0 +1,12 @@ +package com.scriptopia.demo.repository; + +import com.scriptopia.demo.domain.User; +import com.scriptopia.demo.domain.UserSetting; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserSettingRepository extends JpaRepository { + + Optional findByUserId(Long userId); +} diff --git a/src/main/java/com/scriptopia/demo/repository/mongo/GameSessionMongoRepository.java b/src/main/java/com/scriptopia/demo/repository/mongo/GameSessionMongoRepository.java new file mode 100644 index 00000000..f3c292de --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/mongo/GameSessionMongoRepository.java @@ -0,0 +1,13 @@ +package com.scriptopia.demo.repository.mongo; + +import com.scriptopia.demo.domain.mongo.GameSessionMongo; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.Optional; + +public interface GameSessionMongoRepository extends MongoRepository { + + + // userId๋กœ ์ง„ํ–‰ ์ค‘์ธ ๊ฒŒ์ž„ ์กฐํšŒ + Optional findByUserIdAndSceneTypeNot(String userId, String sceneType); +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/repository/mongo/ItemDefMongoRepository.java b/src/main/java/com/scriptopia/demo/repository/mongo/ItemDefMongoRepository.java new file mode 100644 index 00000000..4325b865 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/repository/mongo/ItemDefMongoRepository.java @@ -0,0 +1,7 @@ +package com.scriptopia.demo.repository.mongo; + +import com.scriptopia.demo.domain.mongo.ItemDefMongo; +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface ItemDefMongoRepository extends MongoRepository { +} diff --git a/src/main/java/com/scriptopia/demo/service/AuctionService.java b/src/main/java/com/scriptopia/demo/service/AuctionService.java new file mode 100644 index 00000000..b01d7de0 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/service/AuctionService.java @@ -0,0 +1,378 @@ +package com.scriptopia.demo.service; + +import com.scriptopia.demo.domain.*; +import com.scriptopia.demo.dto.auction.*; +import com.scriptopia.demo.exception.CustomException; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.repository.AuctionRepository; +import com.scriptopia.demo.repository.SettlementRepository; +import com.scriptopia.demo.repository.UserItemRepository; +import com.scriptopia.demo.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; + + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AuctionService { + + private final AuctionRepository auctionRepository; + private final UserItemRepository userItemRepository; + private final UserRepository userRepository; + private final SettlementRepository settlementRepository; + + @Transactional + public String createAuction(AuctionRequest requestDto, Long userId) { + + + + // UUID(String) โ†’ Long ๋ณ€ํ™˜ (์ž„์‹œ) + long userItemId = Long.parseLong(requestDto.getItemDefId()); + + + // UserItem ์กฐํšŒ + UserItem userItem = userItemRepository.findById(userItemId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + + // ์œ ์ € ์†Œ์œ  ์—ฌ๋ถ€ ํ™•์ธ + if (!userItem.getUser().getId().equals(userId)) { + throw new CustomException(ErrorCode.E_400_ITEM_NOT_OWNED); + } + + // ๊ฑฐ๋ž˜ ์ƒํƒœ ํ™•์ธ + if (userItem.getTradeStatus() != TradeStatus.OWNED) { + throw new CustomException(ErrorCode.E_400_ITEM_NOT_TRADE_ABLE); + } + + if (userItem.getRemainingUses() <= 0){ + throw new CustomException(ErrorCode.E_400_ITEM_NO_USES_LEFT); + } + + // ์ด๋ฏธ ๊ฒฝ๋งค์žฅ์— ๋“ฑ๋ก๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ + if (auctionRepository.existsByUserItem(userItem)) { + throw new CustomException(ErrorCode.E_400_ITEM_ALREADY_REGISTERED); + } + + // Auction ๋“ฑ๋ก + Auction auction = new Auction(); + auction.setUserItem(userItem); + auction.setPrice(requestDto.getPrice()); + auction.setCreatedAt(LocalDateTime.now()); + auctionRepository.save(auction); + + // UserItem ์ƒํƒœ ์—…๋ฐ์ดํŠธ + userItem.setTradeStatus(TradeStatus.LISTED); + userItemRepository.save(userItem); + + return "๋“ฑ๋ก ์™„๋ฃŒ: ๊ฐ€๊ฒฉ=" + requestDto.getPrice(); + } + + + + + public TradeResponse getTrades(TradeFilterRequest request) { + int page = request.getPageIndex().intValue(); + int size = request.getPageSize().intValue(); + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + + Page auctionPage; + if (request.getItemName() != null && !request.getItemName().isEmpty()) { + // itemName์œผ๋กœ ๊ฒ€์ƒ‰, ์—ฐ๊ด€ ํ…Œ์ด๋ธ”๊นŒ์ง€ fetch + auctionPage = auctionRepository.findByItemName(request.getItemName(), pageable); + } else { + // ์ด๋ฆ„์ด ์—†์œผ๋ฉด ๋‹ค๋ฅธ ํ•„ํ„ฐ ์กฐ๊ฑด์œผ๋กœ ๊ฒ€์ƒ‰ + auctionPage = auctionRepository.findByFilters( + request.getCategory(), + request.getGrade(), + request.getMinPrice(), + request.getMaxPrice(), + request.getStat(), + request.getEffectGrades(), + pageable + ); + } + + + List items = auctionPage.stream() + .map(a -> { + AuctionItemResponse dto = new AuctionItemResponse(); + dto.setAuctionId(a.getId()); + dto.setPrice(a.getPrice()); + dto.setCreatedAt(a.getCreatedAt()); + + // seller ์ •๋ณด + AuctionItemResponse.UserDto userDto = new AuctionItemResponse.UserDto(); + userDto.setUserId(a.getUserItem().getUser().getId()); + userDto.setNickname(a.getUserItem().getUser().getNickname()); + dto.setSeller(userDto); + + // item ์ •๋ณด + AuctionItemResponse.ItemDto itemDto = new AuctionItemResponse.ItemDto(); + itemDto.setUserItemId(a.getUserItem().getId()); + itemDto.setItemDefId(a.getUserItem().getItemDef().getId()); + itemDto.setName(a.getUserItem().getItemDef().getName()); + itemDto.setDescription(a.getUserItem().getItemDef().getDescription()); + itemDto.setPicSrc(a.getUserItem().getItemDef().getPicSrc()); + itemDto.setRemainingUses(a.getUserItem().getRemainingUses()); + itemDto.setTradeStatus(a.getUserItem().getTradeStatus()); + itemDto.setGrade(String.valueOf(a.getUserItem().getItemDef().getItemGradeDef().getGrade())); + itemDto.setBaseStat(a.getUserItem().getItemDef().getBaseStat()); + itemDto.setStrength(a.getUserItem().getItemDef().getStrength()); + itemDto.setAgility(a.getUserItem().getItemDef().getAgility()); + itemDto.setIntelligence(a.getUserItem().getItemDef().getIntelligence()); + itemDto.setLuck(a.getUserItem().getItemDef().getLuck()); + + // ํšจ๊ณผ ๋ฆฌ์ŠคํŠธ โ†’ ์กฐ๊ฑด๊ณผ ๊ด€๊ณ„์—†์ด "๋ชจ๋“  ํšจ๊ณผ"๋ฅผ ํฌํ•จ + // (JPQL์—์„œ ํ•œ ๊ฐœ๋งŒ ๋‚จ์•˜๋”๋ผ๋„, Hibernate ์—”ํ‹ฐํ‹ฐ๋ฅผ ํ†ตํ•ด ์ „์ฒด ์ปฌ๋ ‰์…˜ ๋‹ค์‹œ ๋กœ๋”ฉ) + a.getUserItem().getItemDef().getItemEffects().size(); // lazy ์ดˆ๊ธฐํ™” ๊ฐ•์ œ + + List effects = + a.getUserItem().getItemDef().getItemEffects().stream() + .map(e -> { + AuctionItemResponse.ItemEffectDto effDto = new AuctionItemResponse.ItemEffectDto(); + effDto.setEffectName(e.getEffectName()); + effDto.setEffectDescription(e.getEffectDescription()); + effDto.setEffectProbability(e.getEffectGradeDef().getEffectProbability()); + return effDto; + }) + .toList(); + + itemDto.setEffects(effects); + dto.setItem(itemDto); + + return dto; + }) + .toList(); + + + TradeResponse response = new TradeResponse(); + response.setContent(items); + + TradeResponse.PageInfo pageInfo = new TradeResponse.PageInfo(); + pageInfo.setCurrentPage(auctionPage.getNumber()); // ํ˜„์žฌ ํŽ˜์ด์ง€ + pageInfo.setPageSize(auctionPage.getSize()); // ํŽ˜์ด์ง€ ๋‹น ํ•ญ๋ชฉ ์ˆ˜ + pageInfo.setTotalPages(auctionPage.getTotalPages()); // ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜ + pageInfo.setTotalItems(auctionPage.getTotalElements()); // ์ „์ฒด ์•„์ดํ…œ ์ˆ˜ + + response.setPageInfo(pageInfo); + + return response; + + } + + + + @Transactional + public String purchaseItem(String auctionIdStr, Long userId) { + Long auctionId = Long.parseLong(auctionIdStr); + + // 1. ๊ฑฐ๋ž˜์†Œ ์ •๋ณด ์กฐํšŒ + Auction auction = auctionRepository.findById(auctionId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_AUCTION_NOT_FOUND)); + + User buyer = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + + User seller = auction.getUserItem().getUser(); + + // 2. ์ž๊ธฐ ๋ฌผ๊ฑด ๊ตฌ๋งค ๊ธˆ์ง€ + if (buyer.getId().equals(seller.getId())) { + throw new CustomException(ErrorCode.E_400_SELF_PURCHASE); + } + + // 3. ๊ธˆ์•ก ํ™•์ธ + if (buyer.getPia() < auction.getPrice()) { + throw new CustomException(ErrorCode.E_400_INSUFFICIENT_PIA); + } + + // 4. ๊ธˆ์•ก ์ฒ˜๋ฆฌ + buyer.subtractPia(auction.getPrice()); + userRepository.save(buyer); + + // 5. UserItem ์ƒํƒœ ๋ณ€๊ฒฝ + UserItem userItem = auction.getUserItem(); + userItem.setTradeStatus(TradeStatus.SOLD); + + // 6. Settlement ๊ธฐ๋ก ์ถ”๊ฐ€ + Settlement buyerSettlement = new Settlement(); + buyerSettlement.setUser(buyer); + buyerSettlement.setItemDef(userItem.getItemDef()); + buyerSettlement.setPrice(auction.getPrice()); + buyerSettlement.setTradeType(TradeType.BUY); + buyerSettlement.setCreatedAt(LocalDateTime.now()); + buyerSettlement.setSettledAt(null); + settlementRepository.save(buyerSettlement); + + Settlement sellerSettlement = new Settlement(); + sellerSettlement.setUser(seller); + sellerSettlement.setItemDef(userItem.getItemDef()); + sellerSettlement.setPrice(auction.getPrice()); + sellerSettlement.setTradeType(TradeType.SELL); + sellerSettlement.setCreatedAt(LocalDateTime.now()); + sellerSettlement.setSettledAt(null); + settlementRepository.save(sellerSettlement); + + // 7. ๊ฒฝ๋งค ํ…Œ์ด๋ธ”์—์„œ ์‚ญ์ œ + auctionRepository.delete(auction); + + return "๊ตฌ๋งค ์™„๋ฃŒ"; + } + + + @Transactional + public String confirmItem(String settlementIdStr, Long userId) { + Long settlementId = Long.parseLong(settlementIdStr); + + Settlement settlement = settlementRepository.findById(settlementId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_SETTLEMENT_NOT_FOUND)); + + // ์ •์‚ฐ ๋Œ€์ƒ ์œ ์ € ํ™•์ธ + if (!settlement.getUser().getId().equals(userId)) { + throw new CustomException(ErrorCode.E_403); + } + + // ์ด๋ฏธ ์ •์‚ฐ ์™„๋ฃŒ ์—ฌ๋ถ€ ํ™•์ธ + if (settlement.getSettledAt() != null) { + throw new CustomException(ErrorCode.E_409_ALREADY_CONFIRMED); + } + + if (settlement.getTradeType() == TradeType.BUY) { + // ๊ตฌ๋งค์ž โ†’ ๊ธฐ์กด UserItem ์†Œ์œ ๊ถŒ ์ด์ „ ๋ฐ ์ƒํƒœ ๋ณ€๊ฒฝ + User buyer = settlement.getUser(); + + UserItem userItem = userItemRepository + .findByItemDefAndTradeStatus(settlement.getItemDef(), TradeStatus.SOLD) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_AUCTION_NOT_FOUND)); + + userItem.setUser(settlement.getUser()); // ๊ตฌ๋งค์ž๋กœ ์†Œ์œ ๊ถŒ ์ด์ „ + userItem.setTradeStatus(TradeStatus.OWNED); // ๊ฑฐ๋ž˜ ๊ฐ€๋Šฅ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ + userItemRepository.save(userItem); + + } else if (settlement.getTradeType() == TradeType.SELL) { + // ํŒ๋งค์ž โ†’ ๊ธˆ์•ก ์ง€๊ธ‰ + User seller = settlement.getUser(); + seller.addPia(settlement.getPrice()); // user domain ์—์„œ ๊ด€๋ฆฌ + userRepository.save(seller); + } + + settlement.setSettledAt(LocalDateTime.now()); + settlementRepository.save(settlement); + + return "์ •์‚ฐ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."; + } + + + + public SettlementHistoryResponse settlementHistory(Long userId, SettlementHistoryRequest requestDto) { + int page = requestDto.getPageIndex().intValue(); + int size = requestDto.getPageSize().intValue(); + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + + // 1. userId ์œผ๋กœ Settlement ์กฐํšŒ + Page settlements = settlementRepository.findByUserId(userId, pageable); + + + // 2. Settlement โ†’ SettlementHistoryResponseItem ๋ณ€ํ™˜ + List content = settlements.stream() + .map(s -> new SettlementHistoryResponseItem( + s.getId(), + s.getItemDef().getName(), + s.getItemDef().getItemType().name(), + s.getItemDef().getItemGradeDef().getGrade().name(), + s.getPrice(), + s.getTradeType().name(), + s.getSettledAt() + )) + .toList(); + + // 3. ํŽ˜์ด์ง€ ์ •๋ณด ๊ตฌ์„ฑ + SettlementHistoryResponse.PageInfo pageInfo = new SettlementHistoryResponse.PageInfo(); + pageInfo.setCurrentPage(settlements.getNumber()); + pageInfo.setPageSize(settlements.getSize()); + + // 4. Response DTO ์ƒ์„ฑ + return new SettlementHistoryResponse(content, pageInfo); + + } + + + + public MySaleItemResponse getMySaleItems(Long userId, MySaleItemRequest requestDto) { + int page = requestDto.getPageIndex().intValue(); + int size = requestDto.getPageSize().intValue(); + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + + + // ํ˜„์žฌ ํŒ๋งค์ค‘(LISTED)์ธ ์•„์ดํ…œ๋งŒ ์กฐํšŒ + Page auctions = auctionRepository.findByUserItem_User_IdAndUserItem_TradeStatus( + userId, + TradeStatus.LISTED, + pageable + ); + + // ์‘๋‹ต content ๋ณ€ํ™˜ + List content = auctions.stream() + .map(auction -> new MySaleItemResponseItem( + auction.getId(), + auction.getPrice(), + auction.getCreatedAt(), + new MySaleItemResponseItem.ItemDto( + auction.getUserItem().getItemDef().getId(), + auction.getUserItem().getItemDef().getName(), + auction.getUserItem().getItemDef().getItemGradeDef().getGrade().name(), + auction.getUserItem().getItemDef().getItemType().name(), + auction.getUserItem().getItemDef().getMainStat().name(), + auction.getUserItem().getItemDef().getPicSrc() + ) + )).toList(); + + + + // ํŽ˜์ด์ง€ ์ •๋ณด + MySaleItemResponse.PageInfo pageInfo = new MySaleItemResponse.PageInfo(page, size); + return new MySaleItemResponse(content, pageInfo); + } + + + + @Transactional + public String cancelMySaleItem(Long userId, String auctionIdStr) { + + //์ž„์‹œ uuid๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์ถ”ํ›„ ๋ณ€ํ˜•ํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋กœ ๋ณ€๊ฒฝ๋ฐ”๋žŒ + Long auctionId = Long.parseLong(auctionIdStr); + + + // 1. ๊ฒฝ๋งค ์ •๋ณด ์กฐํšŒ + Auction auction = auctionRepository.findById(auctionId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_AUCTION_NOT_FOUND)); + + UserItem userItem = auction.getUserItem(); + + // 2. ๋ณธ์ธ ๊ฒ€์ฆ + if (!userItem.getUser().getId().equals(userId)) { + throw new CustomException(ErrorCode.E_403); + } + + + // 3. UserItem ์ƒํƒœ ์›๋ณต + userItem.setTradeStatus(TradeStatus.OWNED); + userItemRepository.save(userItem); + + + // 4. ๊ฒฝ๋งค์žฅ์—์„œ ์‚ญ์ œ + auctionRepository.delete(auction); + + return "ํŒ๋งค ๋“ฑ๋ก์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."; + } + +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/service/FastApiService.java b/src/main/java/com/scriptopia/demo/service/FastApiService.java new file mode 100644 index 00000000..a88490be --- /dev/null +++ b/src/main/java/com/scriptopia/demo/service/FastApiService.java @@ -0,0 +1,91 @@ +package com.scriptopia.demo.service; + +import com.scriptopia.demo.config.fastapi.FastApiEndpoint; +import com.scriptopia.demo.dto.gamesession.*; +import com.scriptopia.demo.dto.items.ItemFastApiRequest; +import com.scriptopia.demo.dto.items.ItemFastApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FastApiService { + + private final WebClient fastApiWebClient; + + // ๊ฒŒ์ž„ ์ดˆ๊ธฐํ™” + public ExternalGameResponse initGame(CreateGameRequest request) { + return fastApiWebClient.post() + .uri(FastApiEndpoint.INIT.getPath()) + .bodyValue(request) + .retrieve() + .bodyToMono(ExternalGameResponse.class) + .block(); + } + + // ์„ ํƒ์ง€ ์ƒ์„ฑ + public CreateGameChoiceResponse makeChoice(CreateGameChoiceRequest request) { + return fastApiWebClient.post() + .uri(FastApiEndpoint.CHOICE.getPath()) + .bodyValue(request) + .retrieve() + .bodyToMono(CreateGameChoiceResponse.class) + .block(); + } + + // ์ „ํˆฌ ํ˜ธ์ถœ (ํ™•์žฅ์šฉ) + public CreateGameBattleResponse battle(CreateGameBattleRequest request) { + return fastApiWebClient.post() + .uri(FastApiEndpoint.BATTLE.getPath()) + .bodyValue(request) + .retrieve() + .bodyToMono(CreateGameBattleResponse.class) + .block(); + } + + // ๊ฒฐ๊ณผ ์ƒ์„ฑ (ํ™•์žฅ์šฉ) + public CreateGameDoneResponse done(CreateGameDoneRequest request) { + return fastApiWebClient.post() + .uri(FastApiEndpoint.DONE.getPath()) + .bodyValue(request) + .retrieve() + .bodyToMono(CreateGameDoneResponse.class) + .block(); + } + + + // ์•„์ดํ…œ ์ƒ์„ฑ (ํ™•์žฅ์šฉ) + public ItemFastApiResponse item(ItemFastApiRequest request) { + return fastApiWebClient.post() + .uri(FastApiEndpoint.ITEM.getPath()) + .bodyValue(request) + .retrieve() + .bodyToMono(ItemFastApiResponse.class) + .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 new file mode 100644 index 00000000..3678ef97 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/service/GameSessionService.java @@ -0,0 +1,1366 @@ +package com.scriptopia.demo.service; + +import com.scriptopia.demo.dto.gamesession.ingame.*; +import com.scriptopia.demo.dto.history.HistoryRequest; +import com.scriptopia.demo.dto.history.HistoryResponse; +import com.scriptopia.demo.dto.items.ItemDefRequest; +import com.scriptopia.demo.dto.items.ItemFastApiResponse; +import com.scriptopia.demo.mapper.InGameMapper; +import com.scriptopia.demo.repository.mongo.GameSessionMongoRepository; +import com.scriptopia.demo.repository.mongo.ItemDefMongoRepository; +import com.scriptopia.demo.utils.GameBalanceUtil; +import com.scriptopia.demo.utils.InitGameData; +import com.scriptopia.demo.domain.*; +import com.scriptopia.demo.domain.mongo.*; +import com.scriptopia.demo.dto.gamesession.*; +import com.scriptopia.demo.exception.CustomException; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.repository.*; +import com.scriptopia.demo.dto.gamesession.ExternalGameResponse.PlayerInfo; +import com.scriptopia.demo.dto.gamesession.ExternalGameResponse.ItemDef; + + +import lombok.RequiredArgsConstructor; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class GameSessionService { + private final ItemDefMongoRepository itemDefMongoRepository; + private final GameSessionRepository gameSessionRepository; + private final ItemGradeDefRepository itemGradeDefRepository; + private final EffectGradeDefRepository effectGradeDefRepository; + private final UserRepository userRepository; + private final GameSessionMongoRepository gameSessionMongoRepository; + private final UserItemRepository userItemRepository; + 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) { + User user = userRepository.findById(userid) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + + + GameSession sessions = gameSessionRepository.findByMongoId(user.getId()) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_STORED_GAME_NOT_FOUND)); + + GameSessionResponse gameSessionResponse = new GameSessionResponse(); + gameSessionResponse.setSessionId(sessions.getMongoId()); + + return ResponseEntity.ok(gameSessionResponse); + } + + @Transactional + public ResponseEntity saveGameSession(Long userId, String sessionId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + boolean game = gameSessionRepository.existsByUserId(user.getId()); + + if(!game) { + GameSession gameSession = new GameSession(); + gameSession.setUser(user); + gameSession.setMongoId(sessionId); + return ResponseEntity.ok(gameSessionRepository.save(gameSession)); + } + else throw new CustomException(ErrorCode.E_404_Duplicated_Game_Session); + } + + @Transactional + public ResponseEntity deleteGameSession(Long userId, String sessionId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + + GameSession gameSession = gameSessionRepository.findByUserIdAndMongoId(user.getId(), sessionId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + gameSessionRepository.delete(gameSession); + + return ResponseEntity.ok("์„ ํƒํ•˜์‹  ๊ฒŒ์ž„์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + } + + + @Transactional + public StartGameResponse startNewGame(Long userId, StartGameRequest request) { + + // 1. ์ง„ํ–‰์ค‘์ธ ๊ฒŒ์ž„ ์ฒดํฌ + if (gameSessionRepository.existsByUserId(userId)) { + throw new CustomException(ErrorCode.E_400_GAME_ALREADY_IN_PROGRESS); + } + + + UserItem userItem = null; + + // ๋ฌผ๊ฑด์„ ๊ฐ€์ ธ์™”๋‹ค๋ฉด ๊ทธ ๋ฌผ๊ฑด์ด ํ•ด๋‹น ํ”Œ๋ ˆ์ด์–ด์˜ ๊ฒƒ์ธ์ง€, ์กด์žฌํ•˜๋Š” ๊ฒƒ์ธ์ง€ ํ™•์ธ + if (request.getItemId() != null){ + Long itemId = Long.parseLong(request.getItemId()); + userItem = userItemRepository.findByUserIdAndItemDefId(userId, itemId) + .orElseThrow(() -> new CustomException(ErrorCode.E_400_ITEM_NOT_OWNED)); + + if (userItem.getRemainingUses() <= 0) { + throw new CustomException(ErrorCode.E_400_ITEM_NO_USES_LEFT); + } + } + + + CreateGameRequest createGameRequest = new CreateGameRequest( + request.getBackground(), + request.getCharacterName(), + request.getCharacterDescription() + ); + + + ExternalGameResponse externalGame = fastApiService.initGame(createGameRequest); + + if (externalGame == null) { + throw new CustomException(ErrorCode.E_500_EXTERNAL_API_ERROR); + } + + InitGameData initGameData = new InitGameData( + externalGame.getPlayerInfo().getStartStat(), + Grade.COMMON, + itemGradeDefRepository, + effectGradeDefRepository + ); + + + // GameSession Data + GameSessionMongo mongoSession = new GameSessionMongo(); + mongoSession.setUserId(userId); + mongoSession.setSceneType(SceneType.CHOICE); // ์‹œ์ž‘์€ choice ๊ธฐ๋ณธ๊ฐ’ + mongoSession.setStartedAt(LocalDateTime.now()); + mongoSession.setUpdatedAt(LocalDateTime.now()); + mongoSession.setPreChoice(null); + mongoSession.setBackground(externalGame.getBackgroundStory()); + mongoSession.setLocation(externalGame.getLocation()); + mongoSession.setProgress(0); + mongoSession.setStage(initGameData.getStages()); + + // PlayerInfo Data + PlayerInfo playerInfo = externalGame.getPlayerInfo(); + + PlayerInfoMongo playerInfoMongo = PlayerInfoMongo.builder() + .name(playerInfo.getName()) + .life(initGameData.getLife()) + .level(initGameData.getLevel()) + .healthPoint(initGameData.getHealthPoint()) + .experiencePoint(initGameData.getExperiencePoint()) + .trait(playerInfo.getTrait()) + .strength(initGameData.getPlayerStr()) + .agility(initGameData.getPlayerAgi()) + .intelligence(initGameData.getPlayerInt()) + .luck(initGameData.getPlayerLuk()) + .gold(initGameData.getGold()) + .build(); + + mongoSession.setPlayerInfo(playerInfoMongo); + + // NpcInfo Data + mongoSession.setNpcInfo(new NpcInfoMongo()); + + //ItemEffect Data + ItemDef itemDef = externalGame.getItemDef(); + ItemDef.ItemEffect itemEffect = itemDef.getItemEffect(); + + List itemEffectsMongo = new ArrayList<>(); + itemEffectsMongo.add( + ItemEffectMongo.builder() + .itemEffectName(itemEffect.getItemEffectName()) + .itemEffectDescription(itemEffect.getItemEffectDescription()) + .effectProbability(EffectProbability.COMMON) + .build() + ); + + //ItemDef Data + ItemDefMongo itemDefMongo = ItemDefMongo.builder() + .itemPicSrc("common item img") + .name(itemDef.getName()) + .description(itemDef.getDescription()) + .category(ItemType.WEAPON) + .baseStat(initGameData.getBaseStat()) + .itemEffect(itemEffectsMongo) + .strength(initGameData.getItemStr()) + .agility(initGameData.getItemAgi()) + .intelligence(initGameData.getItemInt()) + .luck(initGameData.getItemLuk()) + .mainStat(itemDef.getMainStat()) + .grade(Grade.COMMON) + .price(initGameData.getItemPrice()) + .build(); + + ItemDefMongo savedItemDefMongo = itemDefMongoRepository.save(itemDefMongo); + + // CreatedItem Data + List createdItems = new ArrayList<>(); + createdItems.add(savedItemDefMongo.getId()); + + mongoSession.setCreatedItems(createdItems); + + // Inventory Data + List inventoryMongoList = new ArrayList<>(); + inventoryMongoList.add(InventoryMongo.builder() + .itemDefId(savedItemDefMongo.getId()) + .acquiredAt(LocalDateTime.now()) + .equipped(true) + .source("StartWeapon") + .build() + ); + + mongoSession.setInventory(inventoryMongoList); + + // ChoiceInfo Data + mongoSession.setChoiceInfo(new ChoiceInfoMongo()); + + //DoneInfoMongo Data + mongoSession.setDoneInfo(new DoneInfoMongo()); + + //ShopInfoMongo Data + mongoSession.setShopInfo(new ShopInfoMongo()); + + //BattleInfoMongo Data + mongoSession.setBattleInfo(new BattleInfoMongo()); + + //RewardInfoMongo Data + mongoSession.setRewardInfo(new RewardInfoMongo()); + + //HistoryInfoMongo Data + mongoSession.setHistoryInfo( + HistoryInfoMongo.builder() + .worldView(externalGame.getWorldView()) + .backgroundStory(externalGame.getBackgroundStory()) + .worldPrompt(request.getBackground()) + .build() + ); + + // ์ตœ์ข… GameSession ์ €์žฅ + GameSessionMongo savedMongo = gameSessionMongoRepository.save(mongoSession); + + + // MySQL GameSession MongoDB PK ์ €์žฅ + User user = userRepository.findById(userId).orElseThrow( + () -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND) + ); + + GameSession mysqlSession = new GameSession(); + mysqlSession.setUser(user); + mysqlSession.setMongoId(savedMongo.getId()); + gameSessionRepository.save(mysqlSession); + + if (userItem != null) { + userItem.setRemainingUses(userItem.getRemainingUses() - 1); + userItemRepository.save(userItem); + } + + addStats(savedMongo.getPlayerInfo(), savedItemDefMongo); + gameToChoice(userId); + + // MongoDB PK ๋ฐ˜ํ™˜ + return new StartGameResponse( + "๊ฒŒ์ž„์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + mysqlSession.getMongoId() + ); + } + + public Object getInGameDataDto(Long userId){ + if (!gameSessionRepository.existsByUserId(userId)) { + throw new CustomException(ErrorCode.E_404_STORED_GAME_NOT_FOUND); + } + + GameSession gameSession = gameSessionRepository.findByMongoId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + + String gameId = gameSession.getMongoId(); + GameSessionMongo gameSessionMongo = gameSessionMongoRepository.findById(gameId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + + SceneType currentSceneType = gameSessionMongo.getSceneType(); + if (currentSceneType == SceneType.CHOICE) { + return InGameChoiceResponse.builder() + .sceneType("CHOICE") + .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())) + .choiceInfo(inGameMapper.mapChoice(gameSessionMongo.getChoiceInfo())) + .build(); + + } else if (currentSceneType == SceneType.DONE) { + + RewardInfoMongo rewardInfo = gameSessionMongo.getRewardInfo(); + + List gainedItemNames = List.of(); + if (rewardInfo != null && rewardInfo.getGainedItemDefId() != null) { + gainedItemNames = rewardInfo.getGainedItemDefId().stream() + .map(itemDefId -> itemDefMongoRepository.findById(itemDefId) + .map(ItemDefMongo::getName) + .orElse("Unknown Item")) + .toList(); + } + + + return InGameDoneResponse.builder() + .sceneType("DONE") + .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())) + .rewardInfo( + InGameDoneResponse.RewardInfoResponse.builder() + .gainedItemNames(gainedItemNames) + .rewardStrength(rewardInfo != null && rewardInfo.getRewardStrength() != null ? rewardInfo.getRewardStrength() : 0) + .rewardAgility(rewardInfo != null && rewardInfo.getRewardAgility() != null ? rewardInfo.getRewardAgility() : 0) + .rewardIntelligence(rewardInfo != null && rewardInfo.getRewardIntelligence() != null ? rewardInfo.getRewardIntelligence() : 0) + .rewardLuck(rewardInfo != null && rewardInfo.getRewardLuck() != null ? rewardInfo.getRewardLuck() : 0) + .rewardLife(rewardInfo != null && rewardInfo.getRewardLife() != null ? rewardInfo.getRewardLife() : 0) + .rewardGold(rewardInfo != null && rewardInfo.getRewardGold() != null ? rewardInfo.getRewardGold() : 0) + .build() + ) + .build(); + + + } 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(); + + return InGameBattleResponse.builder() + .sceneType("BATTLE") + .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())) + .playerHp(battleInfo != null && battleInfo.getPlayerHp() != null + ? battleInfo.getPlayerHp().stream().map(Long::intValue).toList() + : List.of()) + .enemyHp(battleInfo != null && battleInfo.getEnemyHp() != null + ? battleInfo.getEnemyHp().stream().map(Long::intValue).toList() + : List.of()) + .battleStory(battleInfo != null && battleInfo.getBattleTurn() != null + ? battleInfo.getBattleTurn().stream() + .map(bs -> InGameBattleResponse.BattleStoryResponse.builder() + .turnInfo(bs.getTurnInfo()) + .build()) + .toList() + : List.of()) + .playerWin(battleInfo != null ? battleInfo.getPlayerWin() : null) + .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; + } + + + /** + * ๊ฒŒ์ž„ ์ง„ํ–‰ + * @param userId + */ + @Transactional + public GameSessionMongo gameProgress(Long userId) { + + if (!gameSessionRepository.existsByUserId(userId)) { + throw new CustomException(ErrorCode.E_404_STORED_GAME_NOT_FOUND); + } + + + GameSession gameSession = gameSessionRepository.findByMongoId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + + String gameId = gameSession.getMongoId(); + GameSessionMongo gameSessionMongo = gameSessionMongoRepository.findById(gameId) + .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 -> { + gameToChoice(userId); + } + case SceneType.BATTLE -> { + 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); + } + + return gameSessionMongo; + } + + /** + * ์„ ํƒ์ง€ ์ƒ์„ฑ + * @param userId + */ + @Transactional + public GameSessionMongo gameToChoice(Long userId) { + + if (!gameSessionRepository.existsByUserId(userId)) { + throw new CustomException(ErrorCode.E_404_STORED_GAME_NOT_FOUND); + } + + + GameSession gameSession = gameSessionRepository.findByMongoId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + + String gameId = gameSession.getMongoId(); + GameSessionMongo gameSessionMongo = gameSessionMongoRepository.findById(gameId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + CreateGameChoiceRequest fastApiRequest = new CreateGameChoiceRequest(); + fastApiRequest.setWorldView(gameSessionMongo.getHistoryInfo().getWorldView()); + fastApiRequest.setLocation(gameSessionMongo.getLocation()); + + + List statInfo = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + statInfo.add(Stat.getRandomMainStat().toString()); + } + + + fastApiRequest.setChoiceStat(statInfo); + + + int progress = gameSessionMongo.getProgress(); + List stage = gameSessionMongo.getStage(); + int currentEventStage = stage.get(progress); // ์ง์ˆ˜๋ฉด -1ํ•œ history์˜ ์ด์•ผ๊ธฐ๋ฅผ ๋„ฃ์–ด์ค˜์•ผ ํ•จ + + if (currentEventStage == 0) { + fastApiRequest.setCurrentStory(gameSessionMongo.getBackground()); + fastApiRequest.setCurrentChoice(gameSessionMongo.getPreChoice()); + } else { + if (currentEventStage % 2 == 1) { + fastApiRequest.setCurrentStory(gameSessionMongo.getHistoryInfo().getBackgroundStory()); + fastApiRequest.setCurrentChoice(null); + } else { + 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; + } + } + } + + ChoiceEventType currentEventType = ChoiceEventType.getChoiceEventType(); + fastApiRequest.setEventType(currentEventType); + + int currentNpcRank = 0; + if (currentEventType == ChoiceEventType.LIVING) { + int currentChapter = progress / (stage.size() / 3 + 1) + 1; + currentNpcRank = NpcGrade.getNpcNumberByRandom(currentChapter); + fastApiRequest.setNpcRank(currentNpcRank); + } + + // playerInfo ๋งคํ•‘ + CreateGameChoiceRequest.PlayerInfo playerInfo = new CreateGameChoiceRequest.PlayerInfo(); + playerInfo.setName(gameSessionMongo.getPlayerInfo().getName()); + playerInfo.setTrait(gameSessionMongo.getPlayerInfo().getTrait()); + fastApiRequest.setPlayerInfo(playerInfo); + + // itemInfo ๋งคํ•‘ + List itemInfoList = gameSessionMongo.getInventory().stream() + .map(inv -> { + CreateGameChoiceRequest.ItemInfo itemInfo = new CreateGameChoiceRequest.ItemInfo(); + ItemDefMongo itemDef = itemDefMongoRepository.findById(inv.getItemDefId()) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND)); + itemInfo.setName(itemDef.getName()); + itemInfo.setDescription(itemDef.getDescription()); + return itemInfo; + }).toList(); + fastApiRequest.setItemInfo(itemInfoList); + + CreateGameChoiceResponse createGameChoiceResponse = fastApiService.makeChoice(fastApiRequest); + + if (createGameChoiceResponse == null) { + throw new CustomException(ErrorCode.E_500_EXTERNAL_API_ERROR); + } + + gameSessionMongo.setSceneType(SceneType.CHOICE); + gameSessionMongo.setUpdatedAt(LocalDateTime.now()); + gameSessionMongo.setBackground(createGameChoiceResponse.getChoiceInfo().getStory()); + gameSessionMongo.setProgress(gameSessionMongo.getProgress()); + + NpcInfoMongo npcInfoMongo = null; + if (currentNpcRank > 0){ + int[] npcStat = GameBalanceUtil.getNpcStatsByRank(currentNpcRank); + npcInfoMongo = NpcInfoMongo.builder() + .rank(currentNpcRank) + .name(createGameChoiceResponse.getNpcInfo().getName()) + .trait(createGameChoiceResponse.getNpcInfo().getTrait()) + .NpcWeaponName(createGameChoiceResponse.getNpcInfo().getNpcWeaponName()) + .NpcWeaponDescription(createGameChoiceResponse.getNpcInfo().getNpcWeaponDescription()) + .strength(npcStat[0]) + .agility(npcStat[1]) + .intelligence(npcStat[2]) + .luck(npcStat[3]) + .build(); + + } + gameSessionMongo.setNpcInfo(npcInfoMongo); + + + + List choiceList = new ArrayList<>(); + for (int i = 0; i < createGameChoiceResponse.getChoiceInfo().getChoice().size(); i++) { + var choice = createGameChoiceResponse.getChoiceInfo().getChoice().get(i); + + ChoiceMongo choiceMongo = ChoiceMongo.builder() + .detail(choice.getDetail()) + .stats(Stat.valueOf(statInfo.get(i))) + .probability(GameBalanceUtil.getChoiceProbability(Stat.valueOf(statInfo.get(i)), gameSessionMongo.getPlayerInfo())) + .resultType(ChoiceResultType.nextResultType(currentEventType)) + .rewardType(RewardType.getRandomRewardType()) + .build(); + + choiceList.add(choiceMongo); + } + + + ChoiceMongo promptChoice = ChoiceMongo.builder() + .detail(null) + .stats(null) + .probability(null) + .resultType(ChoiceResultType.nextResultType(currentEventType)) + .rewardType(RewardType.getRandomRewardType()) + .build(); + choiceList.add(promptChoice); + + + ChoiceInfoMongo choiceInfoMongo = ChoiceInfoMongo.builder() + .eventType(fastApiRequest.getEventType()) + .story(createGameChoiceResponse.getChoiceInfo().getStory()) + .choice(choiceList) + .build(); + + gameSessionMongo.setChoiceInfo(choiceInfoMongo); + + + gameSessionMongoRepository.save(gameSessionMongo); + + return gameSessionMongo; + } + + /** + * ๋ฐฐํ‹ + * @param userId + * @return win? + */ + @Transactional + public int gameToBattle(Long userId) { + if (!gameSessionRepository.existsByUserId(userId)) { + throw new CustomException(ErrorCode.E_404_STORED_GAME_NOT_FOUND); + } + + + GameSession gameSession = gameSessionRepository.findByMongoId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + + String gameId = gameSession.getMongoId(); + GameSessionMongo gameSessionMongo = gameSessionMongoRepository.findById(gameId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + + // ์žฅ์ฐฉ๋œ ํ”Œ๋ ˆ์ด์–ด ์•„์ดํ…œ + List equippedItems = new ArrayList<>(); + for (InventoryMongo inv : gameSessionMongo.getInventory()) { + if (inv.isEquipped()) { + ItemDefMongo item = itemDefMongoRepository.findById(inv.getItemDefId()) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND)); + equippedItems.add(item); + } + } + + // weapon, armor, artifact ๋ถ„๋ฅ˜ + ItemDefMongo weapon = null; + ItemDefMongo armor = null; + ItemDefMongo artifact = null; + + for (ItemDefMongo item : equippedItems) { + switch (item.getCategory()) { + case ItemType.WEAPON -> weapon = item; + case ItemType.ARMOR -> armor = item; + case ItemType.ARTIFACT -> artifact = item; + } + } + + int playerDmg = (weapon == null) ? 22 : weapon.getBaseStat(); + int playerHp = (armor == null) ? gameSessionMongo.getPlayerInfo().getHealthPoint() : armor.getBaseStat(); + + int npcRank = gameSessionMongo.getNpcInfo().getRank(); + int playerWeaponDmg = GameBalanceUtil.getPlayerWeaponDmg(gameSessionMongo.getPlayerInfo(), weapon); + int playerCombatPoint = GameBalanceUtil.getBattlePlayerCombatPoint(gameSessionMongo.getPlayerInfo(), weapon, effectGradeDefRepository); + int npcCombatPoint = GameBalanceUtil.getNpcCombatPoint(npcRank); + Integer playerWin = GameBalanceUtil.simulateBattle(playerCombatPoint,npcCombatPoint); + + + List> battleLog = GameBalanceUtil.getBattleLog(playerWin, playerDmg, playerHp, playerCombatPoint, npcRank); + + + // Builder ํŒจํ„ด์œผ๋กœ CreateGameBattleRequest ๊ตฌ์„ฑ + CreateGameBattleRequest fastApiRequest = CreateGameBattleRequest.builder() + .turnCount(battleLog.size()) + .worldView(gameSessionMongo.getHistoryInfo().getWorldView()) + .location(gameSessionMongo.getLocation()) + .playerName(gameSessionMongo.getPlayerInfo().getName()) + .playerTrait(gameSessionMongo.getPlayerInfo().getTrait()) + .playerDmg(playerWeaponDmg) + .playerWeapon(weapon != null ? mapToItemEffect(weapon) : null) + .playerArmor(armor != null ? mapToItemEffect(armor) : null) + .playerArtifact(artifact != null ? mapToItemEffect(artifact) : null) + .npcName(gameSessionMongo.getNpcInfo().getName()) + .npcTrait(gameSessionMongo.getNpcInfo().getTrait()) + .npcDmg(npcCombatPoint) + .npcWeapon(gameSessionMongo.getNpcInfo().getNpcWeaponName()) + .npcWeaponDescription(gameSessionMongo.getNpcInfo().getNpcWeaponDescription()) + .battleResult(playerWin) + .hpLog( battleLog ) + .build(); + + + System.out.println("ํ”Œ๋ ˆ์ด์–ด ๊ณต๊ฒฉ๋ ฅ = " + playerDmg + "ํ”Œ๋ ˆ์ด์–ด ์ฒด๋ ฅ = " + playerHp + " npc ๊ณต๊ฒฉ๋ ฅ = " + npcCombatPoint + " npc ์ฒด๋ ฅ = " + GameBalanceUtil.getNpcHealthPoint(npcRank)); + System.out.println("์ „ํˆฌ๋กœ๊ทธ = " + battleLog + " ํ„ด = " + battleLog.size() + " ์ด๊ธด์‚ฌ๋žŒ = " + playerWin); + + CreateGameBattleResponse fastApiResponse = fastApiService.battle(fastApiRequest); + + if (fastApiResponse == null) { + throw new CustomException(ErrorCode.E_500_EXTERNAL_API_ERROR); + } + + + List turnLogs = fastApiResponse.getBattleInfo().getTurnInfo() + .stream() + .map(info -> BattleStoryMongo.builder().turnInfo(info).build()) + .toList(); + + BattleInfoMongo battleInfoMongo = BattleInfoMongo.builder() + .curTurnId(0L) + .playerHp(battleLog.stream().map(t -> t.get(0).longValue()).toList()) + .enemyHp(battleLog.stream().map(t -> t.get(1).longValue()).toList()) + .battleTurn(turnLogs) + .playerWin( playerWin == 1 ) + .build(); + + + gameSessionMongo.setBattleInfo(battleInfoMongo); + gameSessionMongo.setBackground(fastApiResponse.getBattleInfo().getReCap()); + gameSessionMongo.setSceneType(SceneType.BATTLE); + gameSessionMongo.setUpdatedAt(LocalDateTime.now()); + + + gameSessionMongoRepository.save(gameSessionMongo); + + return playerWin; + } + + + @Transactional + public GameSessionMongo gameToDone(Long userId) { + if (!gameSessionRepository.existsByUserId(userId)) { + throw new CustomException(ErrorCode.E_404_STORED_GAME_NOT_FOUND); + } + + GameSession gameSession = gameSessionRepository.findByMongoId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + + String gameId = gameSession.getMongoId(); + 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()) + .location(gameSessionMongo.getLocation()) + .previousStory(gameSessionMongo.getBackground()) + .selectedChoice(gameSessionMongo.getPreChoice()) + .resultContent(RewardType.getRewardSummary(gameSessionMongo.getRewardInfo())) + .playerName(gameSessionMongo.getPlayerInfo().getName()) + .playerVictory( isVictory ) + .build(); + + + CreateGameDoneResponse fastApiResponse = fastApiService.done(fastApiRequest); + + + if (fastApiResponse == null) { + throw new CustomException(ErrorCode.E_500_EXTERNAL_API_ERROR); + } + + + gameSessionMongo.setSceneType(SceneType.DONE); + gameSessionMongo.setUpdatedAt(LocalDateTime.now()); + gameSessionMongo.setLocation(fastApiResponse.getDoneInfo().getNewLocation()); + gameSessionMongo.setBackground(fastApiResponse.getDoneInfo().getReCap()); + + + int currentProgress = gameSessionMongo.getProgress(); + List stage = gameSessionMongo.getStage(); + int currentEventStage = stage.get(currentProgress); + HistoryInfoMongo historyInfoMongo = gameSessionMongo.getHistoryInfo(); + + if (currentEventStage > 0) { + switch (currentEventStage) { + case 1: + case 2: + historyInfoMongo.setEpilogue1Content(fastApiResponse.getDoneInfo().getReCap()); + break; + case 3: + case 4: + historyInfoMongo.setEpilogue2Content(fastApiResponse.getDoneInfo().getReCap()); + break; + case 5: + case 6: + historyInfoMongo.setEpilogue3Content(fastApiResponse.getDoneInfo().getReCap()); + break; + } + } + + /** + * + * reward๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ํ˜„์žฌ๋ฅผ ๊ฐฑ์‹ ํ•œ ํ›„ mongoDB ์ €์žฅ + */ + PlayerInfoMongo playerInfoMongo = gameSessionMongo.getPlayerInfo(); + RewardInfoMongo rewardInfoMongo = gameSessionMongo.getRewardInfo(); + List inventory = gameSessionMongo.getInventory(); + List inGameItem = gameSessionMongo.getCreatedItems(); + + + playerInfoMongo = GameBalanceUtil.updateReward(playerInfoMongo, rewardInfoMongo); + inventory = GameBalanceUtil.updateRewardItem(inventory, rewardInfoMongo); + inGameItem = GameBalanceUtil.updateInGameItem(inGameItem, rewardInfoMongo); + + gameSessionMongo.setPlayerInfo(playerInfoMongo); + gameSessionMongo.setInventory(inventory); + gameSessionMongo.setCreatedItems(inGameItem); + + + gameSessionMongo.setHistoryInfo(historyInfoMongo); + gameSessionMongoRepository.save(gameSessionMongo); + + return gameSessionMongo; + } + + + + @Transactional + public GameSessionMongo gameChoiceSelect(Long userId, GameChoiceRequest request) { + if (!gameSessionRepository.existsByUserId(userId)) { + throw new CustomException(ErrorCode.E_404_STORED_GAME_NOT_FOUND); + } + + GameSession gameSession = gameSessionRepository.findByMongoId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + String gameId = gameSession.getMongoId(); + GameSessionMongo gameSessionMongo = gameSessionMongoRepository.findById(gameId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + Stat userChoiceStat = null; + Integer probability = null; + + Integer choiceIndex = request.getChoiceIndex(); + choiceIndex = (choiceIndex == null) ? 3 : choiceIndex; + ChoiceMongo choiceMongo = gameSessionMongo.getChoiceInfo().getChoice().get(choiceIndex); + + if (request.getChoiceIndex() == null) { + // ์‚ฌ์šฉ์ž ํ”„๋กฌํ”„ํŠธ ์ž…๋ ฅ โ†’ FAST API ํ˜ธ์ถœํ•ด์•ผ ํ•จ + } else { + userChoiceStat = choiceMongo.getStats(); + probability = GameBalanceUtil.getChoiceProbability(userChoiceStat, gameSessionMongo.getPlayerInfo()); + } + + RewardType rewardType = choiceMongo.getRewardType(); + ChoiceResultType nextScene = choiceMongo.getResultType(); + boolean isPass = GameBalanceUtil.isPass(probability); + + RewardInfoMongo rewardInfo = null; + + switch (nextScene) { + case CHOICE -> { + // ๋ณด์ƒ ์—†์Œ โ†’ ๊ทธ๋ƒฅ ๋‹ค์Œ Choice ๋ฆฌํ„ด + return gameToChoice(userId); + } + case BATTLE -> { + isPass = (gameToBattle(userId) == 1); + gameSessionMongo = gameSessionMongoRepository.findById(gameId).get(); + rewardInfo = handleReward(gameSessionMongo, rewardType, isPass); + + + } + case DONE -> { + gameToDone(userId); + + 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); + } + + // ๋ณด์ƒ ์ €์žฅ + gameSessionMongo.setRewardInfo(rewardInfo); + return gameSessionMongoRepository.save(gameSessionMongo); + } + + + + + @Transactional + public GameSessionMongo gameEquipItem(Long userId, String itemId) { + GameSession gameSession = gameSessionRepository.findByMongoId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + GameSessionMongo gameSessionMongo = gameSessionMongoRepository.findById(gameSession.getMongoId()) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + PlayerInfoMongo playerInfo = gameSessionMongo.getPlayerInfo(); + List inventory = gameSessionMongo.getInventory(); + + InventoryMongo targetInventory = inventory.stream() + .filter(inv -> inv.getItemDefId().equals(itemId)) + .findFirst() + .orElseThrow(() -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND)); + + ItemDefMongo targetDef = itemDefMongoRepository.findById(itemId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND)); + + ItemType category = targetDef.getCategory(); + + if (targetInventory.isEquipped()) { + targetInventory.setEquipped(false); + removeStats(playerInfo, targetDef); + return gameSessionMongoRepository.save(gameSessionMongo); + } + + InventoryMongo currentlyEquipped = inventory.stream() + .filter(InventoryMongo::isEquipped) + .filter(inv -> { + ItemDefMongo def = itemDefMongoRepository.findById(inv.getItemDefId()).orElse(null); + return def != null && def.getCategory() == category; + }) + .findFirst() + .orElse(null); + + if (currentlyEquipped != null) { + ItemDefMongo oldDef = itemDefMongoRepository.findById(currentlyEquipped.getItemDefId()) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND)); + currentlyEquipped.setEquipped(false); + removeStats(playerInfo, oldDef); + } + + targetInventory.setEquipped(true); + addStats(playerInfo, targetDef); + + return gameSessionMongoRepository.save(gameSessionMongo); + } + + @Transactional + public GameSessionMongo gameDropItem(Long userId, String itemId) { + GameSession gameSession = gameSessionRepository.findByMongoId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + GameSessionMongo gameSessionMongo = gameSessionMongoRepository.findById(gameSession.getMongoId()) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + PlayerInfoMongo playerInfo = gameSessionMongo.getPlayerInfo(); + List inventory = gameSessionMongo.getInventory(); + + InventoryMongo targetInventory = inventory.stream() + .filter(inv -> inv.getItemDefId().equals(itemId)) + .findFirst() + .orElseThrow(() -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND)); + + ItemDefMongo targetDef = itemDefMongoRepository.findById(itemId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND)); + + if (targetInventory.isEquipped()) { + removeStats(playerInfo, targetDef); + } + + inventory.remove(targetInventory); + + return gameSessionMongoRepository.save(gameSessionMongo); + } + + + @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) { + 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.setLife(80); // ์ถ”ํ›„ ์Šคํƒฏ์— ๋”ฐ๋ฅธ ์ฒด๋ ฅ์„ ํ•œ๋‹ค๋ฉด + } + } + + private int safeStat(Integer stat) { + return stat != null ? stat : 0; + } + + + + + /** + * battle์—์„œ ์‚ฌ์šฉ + * item -> request๋กœ ์‰ฝ๊ฒŒ ๋งคํ•„ + */ + // ItemDefMongo -> CreateGameBattleRequest.Item ๋ณ€ํ™˜ + private CreateGameBattleRequest.Item mapToItemEffect(ItemDefMongo item) { + List effects = item.getItemEffect().stream() + .map(e -> CreateGameBattleRequest.Item.ItemEffect.builder() + .name(e.getItemEffectName()) + .description(e.getItemEffectDescription()) + .build()) + .toList(); + + return CreateGameBattleRequest.Item.builder() + .name(item.getName()) + .description(item.getDescription()) + .effects(effects) + .build(); + } + + + private ItemDefMongo convertToItemDefMongo(ItemFastApiResponse response) { + List effects = new ArrayList<>(); + + if (response.getItemEffects() != null) { + for (ItemFastApiResponse.ItemEffect e : response.getItemEffects()) { + ItemEffectMongo effectMongo = ItemEffectMongo.builder() + .itemEffectName(e.getItemEffectName()) + .itemEffectDescription(e.getItemEffectDescription()) + .build(); + + effects.add(effectMongo); + } + } + + return ItemDefMongo.builder() + .name(response.getItemName()) + .description(response.getItemDescription()) + .itemEffect(effects) + .build(); + } + + + /** + * ๋ณด์ƒ ์ฒ˜๋ฆฌ (ITEM ํฌํ•จ) + */ + private RewardInfoMongo handleReward(GameSessionMongo gameSessionMongo, RewardType rewardType, boolean isPass) { + RewardInfoMongo rewardInfo = GameBalanceUtil.getReward(rewardType, isPass); + + if (rewardType == RewardType.ITEM && isPass) { + ItemDefRequest itemDefRequest = ItemDefRequest.builder() + .worldView(gameSessionMongo.getHistoryInfo().getWorldView()) + .location(gameSessionMongo.getLocation()) + .playerTrait(null) + .previousStory(gameSessionMongo.getBackground()) + .build(); + + String itemMongoId = itemService.createItemInGame(itemDefRequest); + List gainItemList = rewardInfo.getGainedItemDefId(); + if (gainItemList == null) { + gainItemList = new ArrayList<>(); // null์ด๋ฉด ์ƒˆ ๋ฆฌ์ŠคํŠธ ์ƒ์„ฑ + } + gainItemList.add(itemMongoId); + rewardInfo.setGainedItemDefId(gainItemList); + } + 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/ItemService.java b/src/main/java/com/scriptopia/demo/service/ItemService.java new file mode 100644 index 00000000..30aeb5ba --- /dev/null +++ b/src/main/java/com/scriptopia/demo/service/ItemService.java @@ -0,0 +1,245 @@ +package com.scriptopia.demo.service; + +import com.scriptopia.demo.domain.*; +import com.scriptopia.demo.domain.mongo.ItemDefMongo; +import com.scriptopia.demo.domain.mongo.ItemEffectMongo; +import com.scriptopia.demo.dto.items.*; +import com.scriptopia.demo.exception.CustomException; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.repository.*; +import com.scriptopia.demo.repository.mongo.ItemDefMongoRepository; +import com.scriptopia.demo.utils.InitItemData; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ItemService { + + private final ItemDefRepository itemDefRepository; + private final ItemGradeDefRepository itemGradeDefRepository; + private final EffectGradeDefRepository effectGradeDefRepository; + private final ItemDefMongoRepository itemDefMongoRepository; + private final FastApiService fastApiService; + private final UserItemRepository userItemRepository; + private final UserRepository userRepository; + private final ItemEffectRepository itemEffectRepository; + + + @Transactional + public ItemFastApiResponse createItemInit(ItemDefRequest request, InitItemData initItemData) { + /** + * 1. ์นดํ…Œ๊ณ ๋ฆฌ + * 2. ๋“ฑ๊ธ‰ + * 3. ๋ฉ”์ธ ์Šคํƒฏ + * 4. ๋ฒ ์ด์Šค ์Šคํƒฏ (๊ณต๊ฒฉ๋ ฅ, ์ฒด๋ ฅ) + * 5. ์•„์ดํ…œ ์ดํŽ™ํŠธ( ์ตœ๋Œ€ ๋“ฑ๊ธ‰ 3๊ฐœ) + * 6. ์ถ”๊ฐ€ ์Šคํƒฏ + */ + ItemFastApiRequest fastRequest = ItemFastApiRequest.builder() + .worldView(request.getWorldView()) + .location(request.getLocation()) + .category(initItemData.getItemType()) + .baseStat(initItemData.getBaseStat()) + .mainStat(initItemData.getMainStat()) + .grade(initItemData.getGrade()) + .itemEffects(initItemData.getEffectGrades()) + .strength(initItemData.getStats()[0]) + .agility(initItemData.getStats()[1]) + .intelligence(initItemData.getStats()[2]) + .luck(initItemData.getStats()[3]) + .price(initItemData.getItemPrice()) + .playerTrait(request.getPlayerTrait()) + .previousStory(request.getPreviousStory()) + .build(); + + + ItemFastApiResponse response = fastApiService.item(fastRequest); + + if (response == null) { + throw new CustomException(ErrorCode.E_500_EXTERNAL_API_ERROR); + } + return response; + } + + + /** + * mongoDB, RDB์— ์ €์žฅ ํ›„ mongoDB์˜ item_Def_id๋ฅผ ๋ฆฌํ„ด + * + * @param request + * @return + */ + @Transactional(readOnly = false) + public String createItemInGame(ItemDefRequest request) { + + //์•„์ดํ…œ ์ดˆ๊ธฐ ์ˆ˜์น˜ ์ƒ์„ฑ + InitItemData initItemData = new InitItemData(itemGradeDefRepository, effectGradeDefRepository); + + //fastAPI ํ†ตํ•ด์„œ ์•„์ดํ…œ ์ƒ์„ฑ + ItemFastApiResponse response = createItemInit(request, initItemData); + + + // ์ƒ์„ฑํ•œ ์•„์ดํ…œ ํšจ๊ณผ MongoDB ๋งคํ•‘ + List mongoEffects = new ArrayList<>(); + + List createdItemEffects = response.getItemEffects(); + List effectProbabilities = initItemData.getEffectGrades(); + + for (int i = 0; i < createdItemEffects.size(); i++) { + ItemFastApiResponse.ItemEffect createdEffect = createdItemEffects.get(i); + EffectProbability effectProbability = i < effectProbabilities.size() ? effectProbabilities.get(i) : null; + + mongoEffects.add(ItemEffectMongo.builder() + .effectProbability(effectProbability != null ? (effectProbability) : EffectProbability.COMMON) + .itemEffectName(createdEffect.getItemEffectName()) + .itemEffectDescription(createdEffect.getItemEffectDescription()) + .build()); + } + + + //์•„์ดํ…œ ์ •๋ณด MongoDB ๋งคํ•‘ + ItemDefMongo itemDefMongo = ItemDefMongo.builder() + .itemPicSrc("test link") + .name(response.getItemName()) + .description(response.getItemDescription()) + .category(initItemData.getItemType()) + .baseStat(initItemData.getBaseStat()) + .itemEffect(mongoEffects) + .strength(initItemData.getStats()[0]) + .agility(initItemData.getStats()[1]) + .intelligence(initItemData.getStats()[2]) + .luck(initItemData.getStats()[3]) + .mainStat(initItemData.getMainStat()) + .grade(initItemData.getGrade()) + .price(initItemData.getItemPrice()) + .build(); + + itemDefMongoRepository.save(itemDefMongo); + + + //์•„์ดํ…œ ์ •๋ณด RDBMS ๋งคํ•‘ + ItemDef itemDefRdb = new ItemDef(); + itemDefRdb.setName(response.getItemName()); + itemDefRdb.setDescription(response.getItemDescription()); + itemDefRdb.setItemGradeDef(itemGradeDefRepository.findByGrade(initItemData.getGrade()).get()); + itemDefRdb.setPicSrc("test link"); // Mongo ์ฐธ์กฐ ๋Œ€์‹  ์ง์ ‘ ๊ฐ’ ์ง€์ • + itemDefRdb.setItemType(initItemData.getItemType()); + itemDefRdb.setBaseStat(initItemData.getBaseStat()); + itemDefRdb.setStrength(initItemData.getStats()[0]); + itemDefRdb.setAgility(initItemData.getStats()[1]); + itemDefRdb.setIntelligence(initItemData.getStats()[2]); + itemDefRdb.setLuck(initItemData.getStats()[3]); + itemDefRdb.setMainStat(initItemData.getMainStat()); + itemDefRdb.setPrice(initItemData.getItemPrice()); + itemDefRdb.setCreatedAt(LocalDateTime.now()); + + + + List rdbEffects = new ArrayList<>(); + + //์•„์ดํ…œ ํšจ๊ณผ ์ •๋ณด RDBMS ๋งคํ•‘ + for (int i = 0; i < effectProbabilities.size(); i++) { + ItemEffect effect = new ItemEffect(); + effect.setItemDef(itemDefRdb); + effect.setEffectName(createdItemEffects.get(i).getItemEffectName()); + effect.setEffectDescription(createdItemEffects.get(i).getItemEffectDescription()); + effect.setEffectGradeDef(effectGradeDefRepository.findByEffectProbability(effectProbabilities.get(i)).get()); + rdbEffects.add(effect); + } + + itemDefRdb.setItemEffects(rdbEffects); + + itemDefRepository.save(itemDefRdb); + + return itemDefMongo.getId(); + } + + @Transactional + public ItemDTO createItemInWeb(String userId, ItemDefRequest request) { + + User user = userRepository.findById(Long.valueOf(userId)).orElseThrow( + () -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND) + ); + + InitItemData initItemData = new InitItemData(itemGradeDefRepository, effectGradeDefRepository); + + //fastAPI ํ†ตํ•ด์„œ ์•„์ดํ…œ ์ƒ์„ฑ + ItemFastApiResponse response = createItemInit(request, initItemData); + + List effectProbabilities = initItemData.getEffectGrades(); + List createdItemEffects = response.getItemEffects(); + + //์•„์ดํ…œ ์ •๋ณด RDBMS ๋งคํ•‘ + ItemDef itemDefRdb = new ItemDef(); + itemDefRdb.setName(response.getItemName()); + itemDefRdb.setDescription(response.getItemDescription()); + itemDefRdb.setItemGradeDef(itemGradeDefRepository.findByGrade(initItemData.getGrade()).get()); + itemDefRdb.setPicSrc("test link"); // Mongo ์ฐธ์กฐ ๋Œ€์‹  ์ง์ ‘ ๊ฐ’ ์ง€์ • + itemDefRdb.setItemType(initItemData.getItemType()); + itemDefRdb.setBaseStat(initItemData.getBaseStat()); + itemDefRdb.setStrength(initItemData.getStats()[0]); + itemDefRdb.setAgility(initItemData.getStats()[1]); + itemDefRdb.setIntelligence(initItemData.getStats()[2]); + itemDefRdb.setLuck(initItemData.getStats()[3]); + itemDefRdb.setMainStat(initItemData.getMainStat()); + itemDefRdb.setPrice(initItemData.getItemPrice()); + itemDefRdb.setCreatedAt(LocalDateTime.now()); + + List rdbEffects = new ArrayList<>(); + + //์•„์ดํ…œ ํšจ๊ณผ ์ •๋ณด RDBMS ๋งคํ•‘ + for (int i = 0; i < effectProbabilities.size(); i++) { + ItemEffect effect = new ItemEffect(); + effect.setItemDef(itemDefRdb); + effect.setEffectName(createdItemEffects.get(i).getItemEffectName()); + effect.setEffectDescription(createdItemEffects.get(i).getItemEffectDescription()); + effect.setEffectGradeDef(effectGradeDefRepository.findByEffectProbability(effectProbabilities.get(i)).get()); + ItemEffect savedItemEffect = itemEffectRepository.save(effect); + rdbEffects.add(savedItemEffect); + } + itemDefRepository.save(itemDefRdb); + + UserItem userItem = new UserItem(); + userItem.setItemDef(itemDefRdb); + userItem.setUser(user); + userItem.setRemainingUses(initItemData.getRemainingUses()); + userItem.setTradeStatus(TradeStatus.OWNED); + + userItemRepository.save(userItem); + + + List effects = rdbEffects.stream() + .map(e -> ItemEffectDTO.builder() + .effectProbability(e.getEffectGradeDef().getEffectProbability()) // enum/๊ฐ์ฒด ๊ทธ๋Œ€๋กœ ๋งคํ•‘ + .effectName(e.getEffectName()) + .description(e.getEffectDescription()) + .build()) + .toList(); + + return ItemDTO.builder() + .name(response.getItemName()) + .description(response.getItemDescription()) + .picSrc("test link") + .itemType(initItemData.getItemType()) + .baseStat(initItemData.getBaseStat()) + .strength(initItemData.getStats()[0]) + .agility(initItemData.getStats()[1]) + .intelligence(initItemData.getStats()[2]) + .luck(initItemData.getStats()[3]) + .mainStat(initItemData.getMainStat()) + .grade(initItemData.getGrade()) + .itemEffects(effects) // ๋ฆฌ์ŠคํŠธ ์ฃผ์ž… + .remainingUses(initItemData.getRemainingUses()) + .price(initItemData.getItemPrice()) + .build(); + + } + + +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/service/LocalAccountService.java b/src/main/java/com/scriptopia/demo/service/LocalAccountService.java new file mode 100644 index 00000000..02b11368 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/service/LocalAccountService.java @@ -0,0 +1,294 @@ +package com.scriptopia.demo.service; + +import com.scriptopia.demo.config.JwtProperties; +import com.scriptopia.demo.domain.*; +import com.scriptopia.demo.dto.auth.*; +import com.scriptopia.demo.exception.CustomException; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.repository.LocalAccountRepository; +import com.scriptopia.demo.repository.UserRepository; +import com.scriptopia.demo.repository.UserSettingRepository; +import com.scriptopia.demo.utils.JwtProvider; +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; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +import static org.thymeleaf.util.StringUtils.length; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class LocalAccountService { + + private final StringRedisTemplate redisTemplate; + private final LocalAccountRepository localAccountRepository; + private final PasswordEncoder passwordEncoder; + private final UserRepository userRepository; + private final JwtProvider jwt; + private final RefreshTokenService refreshService; + private final JwtProperties prop; + private final MailService mailService; + + private static final String RT_COOKIE = "RT"; + 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]"); + + private static final long TOKEN_EXPIRATION = 30; + private final UserSettingRepository userSettingRepository; + + + @Transactional + public void resetPassword(String token,String newPassword) { + + String key = "reset:token:" + token; + String email = redisTemplate.opsForValue().get(key); + if (email == null) { + throw new CustomException(ErrorCode.E_401); + } + + LocalAccount localAccount = localAccountRepository.findByEmail(email).orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + + localAccount.setPassword(passwordEncoder.encode(newPassword)); + localAccountRepository.save(localAccount); + + redisTemplate.delete(key); + } + + @Transactional + public void verifyEmail(VerifyEmailRequest request) { + String email = request.getEmail(); + + if (localAccountRepository.existsByEmail(email)){ + throw new CustomException(ErrorCode.E_409_EMAIL_TAKEN); + } + + } + + @Transactional + public void sendVerificationCode(String email) { + if (localAccountRepository.existsByEmail(email)){ + throw new CustomException(ErrorCode.E_409_EMAIL_TAKEN); + } + String code = String.format("%06d", (int)(Math.random() * 999999)); + mailService.saveCode(email, code); + mailService.sendVerificationCode(email, code); + } + + @Transactional + public void sendResetPasswordMail(String email) { + + if (!localAccountRepository.existsByEmail(email)){ + throw new CustomException(ErrorCode.E_404_USER_NOT_FOUND); + } + + String resetToken = createResetToken(email); + mailService.sendResetLink(email, resetToken); + } + + @Transactional + public void verifyCode(String email, String inputCode) { + + if (length(inputCode) != 6){ + throw new CustomException(ErrorCode.E_400_INVALID_CODE); + } + + String savedCode = redisTemplate.opsForValue().get("email:verify:" + email); + + if (savedCode != null && savedCode.equals(inputCode)) { + // ์ธ์ฆ ์™„๋ฃŒ ํ›„ 30๋ถ„ ์œ ์ง€ + redisTemplate.opsForValue().set("email:verified:" + email, "true", TOKEN_EXPIRATION, TimeUnit.MINUTES); + redisTemplate.delete("email:verify:" + email); // ์ฝ”๋“œ ์ œ๊ฑฐ + } + else{ + throw new CustomException(ErrorCode.E_401_CODE_MISMATCH); + } + + } + + + + @Transactional + public LoginResponse register(RegisterRequest registerRequest, HttpServletRequest request, HttpServletResponse response) { + String email = registerRequest.getEmail(); + + //์ค‘๋ณต ๊ฒ€์ฆ + if (localAccountRepository.existsByEmail(email)){ + throw new CustomException(ErrorCode.E_409_EMAIL_TAKEN); + } + + String verified = redisTemplate.opsForValue().get("email:verified:" + email); + + //์ด๋ฉ”์ผ ์ธ์ฆ ๊ฒ€์ฆ + if (verified == null || !verified.equals("true")) { + throw new CustomException(ErrorCode.E_412_EMAIL_NOT_VERIFIED); + } + + // ๊ณต๋ฐฑ ๊ฒ€์ฆ + if (WS.matcher(registerRequest.getPassword()).find()) { + throw new CustomException(ErrorCode.E_400_PASSWORD_WHITESPACE); + } + + isAvailable(email, registerRequest.getNickname()); + + //user ๊ฐ์ฒด ์ƒ์„ฑ + User user = new User(); + user.setNickname(registerRequest.getNickname()); + user.setPia(0L); + user.setCreatedAt(LocalDateTime.now()); + user.setLastLoginAt(LocalDateTime.now()); + user.setProfileImgUrl(null); + user.setRole(Role.USER); + user.setLoginType(LoginType.LOCAL); + User savedUser = userRepository.save(user); + + //localAccount ๊ฐ์ฒด ์ƒ์„ฑ + LocalAccount localAccount = new LocalAccount(); + localAccount.setUser(user); + localAccount.setEmail(email); + localAccount.setPassword(passwordEncoder.encode(registerRequest.getPassword())); + localAccount.setUpdatedAt(LocalDateTime.now()); + localAccount.setStatus(UserStatus.UNVERIFIED); + localAccountRepository.save(localAccount); + + //ํ™˜๊ฒฝ ์„ค์ • ์ดˆ๊ธฐ ๊ฐ’ + UserSetting userSetting = new UserSetting(); + userSetting.setUser(user); + userSetting.setTheme(Theme.DARK); + userSetting.setFontType(FontType.PretendardVariable); + userSetting.setFontSize(16); + userSetting.setLineHeight(1); + userSetting.setWordSpacing(1); + userSetting.setUpdatedAt(LocalDateTime.now()); + userSettingRepository.save(userSetting); + + return initLoginResponse(savedUser, registerRequest.getDeviceId(), request, response); + + + + } + + @Transactional + public LoginResponse login(LoginRequest req, HttpServletRequest request, HttpServletResponse response) { + + 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()); + + return initLoginResponse(user, req.getDeviceId(), request, response); + } + + @Transactional + public void changePassword(Long userId, ChangePasswordRequest request) { + + //ํ˜„์žฌ, ๋ณ€๊ฒฝ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ + if(!request.getNewPassword().equals(request.getConfirmPassword())){ + throw new CustomException(ErrorCode.E_400_PASSWORD_CONFIRM_MISMATCH); + } + + // ๊ณต๋ฐฑ ๊ฒ€์ฆ + if (WS.matcher(request.getNewPassword()).find()) { + throw new CustomException(ErrorCode.E_400_PASSWORD_WHITESPACE); + } + + LocalAccount localAccount = localAccountRepository.findByUserId(userId).orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + + + //ํ˜„์žฌ ์•”ํ˜ธ ๋ถˆ์ผ์น˜ + if (!passwordEncoder.matches(request.getOldPassword(), localAccount.getPassword())) { + throw new CustomException(ErrorCode.E_401_CURRENT_PASSWORD_MISMATCH); + } + + //์ด์ „๊ณผ ๋™์ผํ•œ ์•”ํ˜ธ + if (passwordEncoder.matches(request.getNewPassword(), localAccount.getPassword())) { + throw new CustomException(ErrorCode.E_409_PASSWORD_SAME_AS_OLD); + } + + localAccount.setPassword(passwordEncoder.encode(request.getNewPassword())); + + } + + public String createResetToken(String email) { + String token = UUID.randomUUID().toString(); + + redisTemplate.opsForValue() + .set("reset:token:" + token, email, TOKEN_EXPIRATION, TimeUnit.MINUTES); + + return token; + } + + + + public List getRoles(Long userId) { + return List.of(Role.USER.toString()); + } + + + private void isAvailable(String email, String nickname) { + if (localAccountRepository.existsByEmail(email)) { + throw new CustomException(ErrorCode.E_409_EMAIL_TAKEN); + } + + if (userRepository.existsByNickname(nickname)) { + throw new CustomException(ErrorCode.E_409_NICKNAME_TAKEN); + } + + } + + public ResponseCookie refreshCookie(String value) { + return ResponseCookie.from(RT_COOKIE, value) + .httpOnly(true) + .secure(COOKIE_SECURE) + .sameSite(COOKIE_SAME_SITE) + .path("/") + .maxAge(Duration.ofDays(14)) + .build(); + } + + public ResponseCookie removeRefreshCookie() { + return ResponseCookie.from(RT_COOKIE, "") + .httpOnly(true) + .secure(COOKIE_SECURE) + .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/MailService.java b/src/main/java/com/scriptopia/demo/service/MailService.java new file mode 100644 index 00000000..af3837f8 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/service/MailService.java @@ -0,0 +1,76 @@ +package com.scriptopia.demo.service; + +import com.scriptopia.demo.exception.CustomException; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.repository.LocalAccountRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +public class MailService { + private final JavaMailSender mailSender; + private final StringRedisTemplate redisTemplate; + private final LocalAccountRepository localAccountRepository; + @Value("${spring.mail.username}") + private String fromEmail; + + @Value("${reset-url}") + private String resetUrl; + + public void sendVerificationCode(String toEmail, String code) { + mailSender.send(initMessage( + toEmail, + fromEmail, + "[Scriptopia] ํšŒ์›๊ฐ€์ž… ์ด๋ฉ”์ผ ์ธ์ฆ๋ฒˆํ˜ธ", + "์ธ์ฆ๋ฒˆํ˜ธ: " + code + "\n5๋ถ„ ์ด๋‚ด์— ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”." + + )); + } + + public void saveCode(String email, String code) { + redisTemplate.opsForValue().set( + "email:verify:" + email, + code, + 5, + TimeUnit.MINUTES + ); + } + + public void sendResetLink(String toEmail, String token) { + + String link = resetUrl + "?token=" + token; + mailSender.send(initMessage( + toEmail, + fromEmail, + "[Scriptopia] ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์•ˆ๋‚ด", + "์•„๋ž˜ ๋งํฌ๋ฅผ ํด๋ฆญํ•˜์—ฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ณ€๊ฒฝํ•˜์„ธ์š”:\n" +link + )); + } + + + public SimpleMailMessage initMessage(String toEmail, String fromEmail, String subject, String text) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo(toEmail); + message.setFrom(fromEmail); + message.setSubject(subject); + message.setText(text); + return message; + } + public String getCode(String email) { + return redisTemplate.opsForValue().get("email:verify" + email); + } + + public void deleteCode(String email) { + redisTemplate.delete("email:verify:" + email); + } + + + +} diff --git a/src/main/java/com/scriptopia/demo/service/OAuthService.java b/src/main/java/com/scriptopia/demo/service/OAuthService.java new file mode 100644 index 00000000..f5b4edba --- /dev/null +++ b/src/main/java/com/scriptopia/demo/service/OAuthService.java @@ -0,0 +1,218 @@ +package com.scriptopia.demo.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.scriptopia.demo.config.OAuthProperties; +import com.scriptopia.demo.domain.*; +import com.scriptopia.demo.dto.oauth.LoginStatus; +import com.scriptopia.demo.dto.oauth.OAuthLoginResponse; +import com.scriptopia.demo.dto.oauth.OAuthUserInfo; +import com.scriptopia.demo.dto.oauth.SocialSignupRequest; +import com.scriptopia.demo.exception.CustomException; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.repository.SocialAccountRepository; +import com.scriptopia.demo.repository.UserRepository; +import com.scriptopia.demo.utils.client.GoogleClient; +import com.scriptopia.demo.utils.JwtProvider; +import com.scriptopia.demo.utils.client.KakaoClient; +import com.scriptopia.demo.utils.client.NaverClient; +import io.jsonwebtoken.JwtException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +public class OAuthService { + + private static final String RT_COOKIE = "RT"; + private static final boolean COOKIE_SECURE = true; + private static final String COOKIE_SAMESITE = "None"; + + private final UserRepository userRepository; + private final SocialAccountRepository socialAccountRepository; + private final JwtProvider jwtProvider; + private final RefreshTokenService refreshTokenService; // RefreshToken ๊ด€๋ฆฌ ์„œ๋น„์Šค + private final GoogleClient googleClient; + private final NaverClient naverClient; + private final KakaoClient kakaoClient; + private final StringRedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + private final OAuthProperties props; + + @Transactional + public OAuthLoginResponse login(String provider, String code, + HttpServletRequest request, HttpServletResponse response) throws JsonProcessingException { + + Provider providerEnum = Provider.valueOf(provider.toUpperCase()); + + OAuthUserInfo userInfo = fetchUserInfoFromProvider(provider, code); + + Optional accountOpt = + socialAccountRepository.findBySocialIdAndProvider(userInfo.getId(), providerEnum); + + String json = objectMapper.writeValueAsString(userInfo); + if (accountOpt.isEmpty()) { + String signupToken = UUID.randomUUID().toString(); + // Redis TTL = 5๋ถ„ + redisTemplate.opsForValue().set("signup:" + signupToken, json, 5, TimeUnit.MINUTES); + + return OAuthLoginResponse.builder() + .status(LoginStatus.SIGNUP_REQUIRED) + .signupToken(signupToken) + .build(); + } + + User user = accountOpt.get().getUser(); + user.setLastLoginAt(LocalDateTime.now()); + + try { + List roles = List.of(user.getRole().toString()); + String access = jwtProvider.createAccessToken(user.getId(), roles); + String refresh = jwtProvider.createRefreshToken(user.getId(), "SOCIAL-" + provider); + + String ip = request.getRemoteAddr(); + String ua = request.getHeader("User-Agent"); + refreshTokenService.saveLoginRefresh(user.getId(), refresh, "SOCIAL-" + provider, ip, ua); + + response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie(refresh).toString()); + + return OAuthLoginResponse.builder() + .status(LoginStatus.LOGIN_SUCCESS) + .accessToken(access) + .build(); + + } catch (JwtException e) { + throw new CustomException(ErrorCode.E_500_TOKEN_CREATION_FAILED); + } catch (Exception e) { + throw new CustomException(ErrorCode.E_500_TOKEN_STORAGE_FAILED); + } + } + + @Transactional + public OAuthLoginResponse signup(SocialSignupRequest req, + HttpServletRequest request, HttpServletResponse response) { + String key = "signup:" + req.getSignupToken(); + + try { + + String json = (String) redisTemplate.opsForValue().get(key); + if (json == null) { + throw new CustomException(ErrorCode.E_400_INVALID_SOCIAL_LOGIN_CODE); + } + + OAuthUserInfo userInfo = objectMapper.readValue(json, OAuthUserInfo.class); + + Provider provider = Provider.valueOf(userInfo.getProvider().toUpperCase()); + + if (socialAccountRepository.existsBySocialIdAndProvider(userInfo.getId(), provider)) { + throw new CustomException(ErrorCode.E_409_EMAIL_TAKEN); + } + + if (userRepository.existsByNickname(req.getNickname())) { + throw new CustomException(ErrorCode.E_409_NICKNAME_TAKEN); + } + + + User user = new User(); + user.setNickname(req.getNickname()); + user.setPia(0L); + user.setCreatedAt(LocalDateTime.now()); + user.setLastLoginAt(LocalDateTime.now()); + user.setProfileImgUrl(null); + user.setRole(Role.USER); + user.setLoginType(LoginType.SOCIAL); + userRepository.save(user); + + SocialAccount account = new SocialAccount(); + account.setUser(user); + account.setSocialId(userInfo.getId()); + account.setEmail(userInfo.getEmail()); + account.setProvider(provider); + socialAccountRepository.save(account); + + List roles = List.of(user.getRole().toString()); + String deviceId = "SOCIAL-" + provider.name(); + String access = jwtProvider.createAccessToken(user.getId(), roles); + String refresh = jwtProvider.createRefreshToken(user.getId(), deviceId); + + String ip = request.getRemoteAddr(); + String ua = request.getHeader("User-Agent"); + refreshTokenService.saveLoginRefresh(user.getId(), refresh, deviceId, ip, ua); + + response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie(refresh).toString()); + + redisTemplate.delete(key); + + return OAuthLoginResponse.builder() + .status(LoginStatus.LOGIN_SUCCESS) + .accessToken(access) + .build(); + + } catch (JsonProcessingException e) { + throw new CustomException(ErrorCode.E_500_DATABASE_ERROR); + } + } + + public ResponseCookie refreshCookie(String value) { + return ResponseCookie.from(RT_COOKIE, value) + .httpOnly(true) + .secure(COOKIE_SECURE) + .sameSite(COOKIE_SAMESITE) + .path("/") + .maxAge(Duration.ofDays(14)) + .build(); + } + + public String buildAuthorizationUrl(String provider) { + switch (provider.toUpperCase()) { + case "GOOGLE": + return "https://accounts.google.com/o/oauth2/v2/auth" + + "?client_id=" + props.getGoogle().getClientId() + + "&redirect_uri=" + props.getGoogle().getRedirectUri() + + "&response_type=code" + + "&scope=" + URLEncoder.encode(props.getGoogle().getScope(), StandardCharsets.UTF_8); + case "KAKAO": + return "https://kauth.kakao.com/oauth/authorize" + + "?client_id=" + props.getKakao().getClientId() + + "&redirect_uri=" + props.getKakao().getRedirectUri() + + "&response_type=code"; + case "NAVER": + return "https://nid.naver.com/oauth2.0/authorize" + + "?client_id=" + props.getNaver().getClientId() + + "&redirect_uri=" + props.getNaver().getRedirectUri() + + "&response_type=code" + + "&state=" + UUID.randomUUID(); + default: + throw new CustomException(ErrorCode.E_400_UNSUPPORTED_PROVIDER); + } + } + + + private OAuthUserInfo fetchUserInfoFromProvider(String provider, String code) { + switch (provider.toLowerCase()) { + case "google": + return googleClient.getUserInfo(code); + case "naver": + return naverClient.getUserInfo(code); + case "kakao": + return kakaoClient.getUserInfo(code); + default: + throw new CustomException(ErrorCode.E_400_UNSUPPORTED_PROVIDER); + } + } +} diff --git a/src/main/java/com/scriptopia/demo/service/PiaShopService.java b/src/main/java/com/scriptopia/demo/service/PiaShopService.java new file mode 100644 index 00000000..728e5f7d --- /dev/null +++ b/src/main/java/com/scriptopia/demo/service/PiaShopService.java @@ -0,0 +1,190 @@ +package com.scriptopia.demo.service; + +import com.scriptopia.demo.domain.PiaItem; +import com.scriptopia.demo.domain.PiaItemPurchaseLog; +import com.scriptopia.demo.domain.User; +import com.scriptopia.demo.domain.UserPiaItem; +import com.scriptopia.demo.dto.items.ItemDTO; +import com.scriptopia.demo.dto.items.ItemDefRequest; +import com.scriptopia.demo.dto.piashop.PiaItemRequest; +import com.scriptopia.demo.dto.piashop.PiaItemResponse; +import com.scriptopia.demo.dto.piashop.PiaItemUpdateRequest; +import com.scriptopia.demo.dto.piashop.PurchasePiaItemRequest; +import com.scriptopia.demo.exception.CustomException; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.repository.*; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class PiaShopService { + + private final PiaItemRepository piaItemRepository; + private final UserRepository userRepository; + private final UserPiaItemRepository userPiaItemRepository; + private final PurchaseLogRepository purchaseLogRepository; + private final ItemService itemService; + private final UserItemRepository userItemRepository; + + @Transactional + public String createPiaItem(PiaItemRequest request) { + + // 1. ํ•„์ˆ˜ ๊ฐ’ ํ™•์ธ + if(request.getName() == null || request.getName().isBlank()) { + throw new CustomException(ErrorCode.E_400_INVALID_REQUEST); // ์ด๋ฆ„ ํ•„์ˆ˜ ์ฒดํฌ + } + + if(request.getPrice() == null || request.getPrice() <= 0) { + throw new CustomException(ErrorCode.E_400_INVALID_REQUEST); // ๊ธˆ์•ก ์œ ํšจ์„ฑ ์ฒดํฌ + } + + // 2. ์ค‘๋ณต ์ด๋ฆ„ ์ฒดํฌ + if(piaItemRepository.existsByName(request.getName())) { + throw new CustomException(ErrorCode.E_400_PIA_ITEM_DUPLICATE); // ์ค‘๋ณต ์ด๋ฆ„ ์˜ค๋ฅ˜ + } + + // 3. PiaItem ์ƒ์„ฑ + PiaItem piaItem = new PiaItem(); + piaItem.setName(request.getName()); + piaItem.setPrice(request.getPrice()); + piaItem.setDescription(request.getDescription()); + + piaItemRepository.save(piaItem); + + return "PIA ์•„์ดํ…œ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."; + } + + + @Transactional + public String updatePiaItem(String itemsIdStr, PiaItemUpdateRequest request) { + + // uuid ์‚ฌ์šฉ ์‹œ ๋ณ€๊ฒฝ (์ž„์‹œ์ž„) + Long itemsId = Long.valueOf(itemsIdStr); + + + // 1. ํ•„์ˆ˜ ๊ฐ’ ์ฒดํฌ + if (request.getName() == null || request.getName().isBlank()) { + throw new CustomException(ErrorCode.E_400_MISSING_NICKNAME); // ์ด๋ฆ„ ํ•„์ˆ˜ + } + if (request.getPrice() == null || request.getPrice() <= 0) { + throw new CustomException(ErrorCode.E_400_INVALID_AMOUNT); // ๊ฐ€๊ฒฉ ํ•„์ˆ˜ + } + + // 2. ์•„์ดํ…œ ์กด์žฌ ํ™•์ธ + PiaItem piaItem = piaItemRepository.findById(itemsId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_AUCTION_NOT_FOUND)); // ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์•„์ดํ…œ + + // 3. ์ด๋ฆ„ ์ค‘๋ณต ์ฒดํฌ (์ž๊ธฐ ์ž์‹  ์ œ์™ธ) + if (piaItemRepository.existsByNameAndIdNot(request.getName(),itemsId)){ + throw new CustomException(ErrorCode.E_409_ALREADY_CONFIRMED); // ์ด๋ฆ„ ์ค‘๋ณต + } + + // 4. ์ •๋ณด ์—…๋ฐ์ดํŠธ + piaItem.setName(request.getName()); + piaItem.setPrice(request.getPrice()); + piaItem.setDescription(request.getDescription()); + + piaItemRepository.save(piaItem); + + return "PIA ์•„์ดํ…œ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค."; + } + + + + public List getPiaItems() { + return piaItemRepository.findAll().stream() + .map(PiaItemResponse::fromEntity) + .collect(Collectors.toList()); + } + + + @Transactional + public void purchasePiaItem(Long userId, PurchasePiaItemRequest request) { + + // uuid ๋กœ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ž„์‹œ์ž„ + Long piaItemId = Long.valueOf(request.getItemId()); + + // 1. ์œ ์ € ์กฐํšŒ + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + + // 2. ์•„์ดํ…œ ์กฐํšŒ + PiaItem piaItem = piaItemRepository.findById(piaItemId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_AUCTION_NOT_FOUND)); + + // 3. ์ด ๊ตฌ๋งค ๊ธˆ์•ก ๊ณ„์‚ฐ + long totalPrice = piaItem.getPrice() * request.getQuantity(); + if (user.getPia() < totalPrice) { + throw new CustomException(ErrorCode.E_400_INSUFFICIENT_PIA); + } + + // 4. UserPiaItem ์กฐํšŒ ๋ฐ ์ˆ˜๋Ÿ‰ ์—…๋ฐ์ดํŠธ + UserPiaItem userPiaItem = userPiaItemRepository.findByUserAndPiaItem(user, piaItem) + .orElseGet(() -> { + UserPiaItem newItem = new UserPiaItem(); + newItem.setUser(user); + newItem.setPiaItem(piaItem); + newItem.setQuantity(0L); + return newItem; + }); + + userPiaItem.setQuantity(userPiaItem.getQuantity() + request.getQuantity()); + userPiaItemRepository.save(userPiaItem); + + // 5. ์œ ์ € ๊ธˆ์•ก ์ฐจ๊ฐ + user.setPia(user.getPia() - totalPrice); + userRepository.save(user); + + // 6. ๊ตฌ๋งค ๋กœ๊ทธ ๊ธฐ๋ก + PiaItemPurchaseLog log = new PiaItemPurchaseLog(); + log.setUser(user); + log.setPiaItem(piaItem); + log.setPrice(totalPrice); + purchaseLogRepository.save(log); + } + + @Transactional + public ItemDTO useItemAnvil(String userId, ItemDefRequest request){ + + User user = userRepository.findById(Long.valueOf(userId)).orElseThrow( + () -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND) + ); + String ItemName = "์•„์ดํ…œ ๋ชจ๋ฃจ"; + PiaItem piaItem = piaItemRepository.findByName(ItemName).orElseThrow( + () -> new CustomException(ErrorCode.E_404_ITEM_NOT_FOUND) + ); + + UserPiaItem userPiaItem = userPiaItemRepository.findByUserAndPiaItem(user, piaItem).orElseThrow( + () -> new CustomException(ErrorCode.E_400_ITEM_NOT_OWNED) + ); + + Long quantity = userPiaItem.getQuantity(); + if (quantity < 1){ + throw new CustomException(ErrorCode.E_400_ITEM_NOT_OWNED); + } + + ItemDTO itemInWeb = itemService.createItemInWeb(userId, request); + quantity-=1; + if (quantity == 0){ + userPiaItemRepository.delete(userPiaItem); + } + else { + userPiaItem.setQuantity(quantity); + } + return itemInWeb; + + + + + } + + +} diff --git a/src/main/java/com/scriptopia/demo/service/RefreshTokenService.java b/src/main/java/com/scriptopia/demo/service/RefreshTokenService.java new file mode 100644 index 00000000..fdaf701d --- /dev/null +++ b/src/main/java/com/scriptopia/demo/service/RefreshTokenService.java @@ -0,0 +1,99 @@ +package com.scriptopia.demo.service; + +import com.scriptopia.demo.exception.CustomException; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.utils.JwtProvider; +import com.scriptopia.demo.record.RefreshSession; +import com.scriptopia.demo.repository.RefreshRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.Instant; +import java.util.Base64; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RefreshTokenService { + private final RefreshRepository refreshRepository; + private final JwtProvider jwt; + private final PasswordEncoder passwordEncoder; + + public void saveLoginRefresh(Long userId, String refreshToken, String deviceId, String ip, String ua) { + if (deviceId != null) { + refreshRepository.deleteByDevice(userId, deviceId); + } + var jti = jwt.getJti(refreshToken); + var exp = jwt.getExpiry(refreshToken).toInstant().getEpochSecond(); + + String rtHashInput = sha256Base64Url(refreshToken); + + var session = new RefreshSession( + userId, jti, + + passwordEncoder.encode(rtHashInput), + deviceId, exp, + Instant.now().getEpochSecond(), + ip, ua + ); + refreshRepository.save(session); + } + + public TokenPair rotate(String refreshToken, String expectedDeviceId, List roles) { + var parsed = jwt.parse(refreshToken); + Long userId = Long.valueOf(parsed.getBody().getSubject()); + String jti = parsed.getBody().getId(); + String deviceInToken = (String) parsed.getBody().get("device"); + + if (expectedDeviceId != null && !expectedDeviceId.equals(deviceInToken)) { + throw new CustomException(ErrorCode.E_403_DEVICE_MISMATCH); + } + + var saved = refreshRepository.find(userId, jti) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_REFRESH_NOT_FOUND)); + + + String input = sha256Base64Url(refreshToken); + if (!passwordEncoder.matches(input, saved.tokenHash())) { + throw new CustomException(ErrorCode.E_409_REFRESH_REUSE_DETECTED); + } + + + refreshRepository.delete(userId, jti); + + + String newAccess = jwt.createAccessToken(userId, roles); + String newRefresh = jwt.createRefreshToken(userId, deviceInToken); + + + saveLoginRefresh(userId, newRefresh, deviceInToken, saved.ip(), saved.ua()); + + return new TokenPair(newAccess, newRefresh); + } + + public void logout(String refreshToken) { + var parsed = jwt.parse(refreshToken); + long userId = Long.parseLong(parsed.getBody().getSubject()); + String jti = parsed.getBody().getId(); + refreshRepository.delete(userId, jti); + } + + public void logoutAll(Long userId) { + refreshRepository.deleteAllForUser(userId); + } + + public record TokenPair(String accessToken, String refreshToken) {} + + private static String sha256Base64Url(String s) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(s.getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + } catch (Exception e) { + throw new CustomException(ErrorCode.E_500_TOKEN_HASHING_FAILED); + } + } +} diff --git a/src/main/java/com/scriptopia/demo/service/SharedGameFavoriteService.java b/src/main/java/com/scriptopia/demo/service/SharedGameFavoriteService.java new file mode 100644 index 00000000..3a10bdb5 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/service/SharedGameFavoriteService.java @@ -0,0 +1,66 @@ +package com.scriptopia.demo.service; + +import com.scriptopia.demo.domain.SharedGameFavorite; +import com.scriptopia.demo.dto.sharedgamefavorite.SharedGameFavoriteResponse; +import com.scriptopia.demo.exception.CustomException; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.repository.*; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + + +@Service +@RequiredArgsConstructor +public class SharedGameFavoriteService { + private final SharedGameFavoriteRepository sharedGameFavoriteRepository; + private final SharedGameRepository sharedGameRepository; + private final GameTagRepository gameTagRepository; + private final UserRepository userRepository; + private final SharedGameScoreRepository sharedGameScoreRepository; + + @Transactional + public ResponseEntity saveFavorite(Long userId, UUID uuid) { + var user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + var game = sharedGameRepository.findByUuid(uuid) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_SHARED_GAME_NOT_FOUND)); + + // ํ† ๊ธ€ ์ฒ˜๋ฆฌ + var existing = sharedGameFavoriteRepository.findByUserIdAndSharedGameId(userId, game.getId()); + boolean liked; + if (existing.isPresent()) { + sharedGameFavoriteRepository.delete(existing.get()); + liked = false; + } else { + var fav = new SharedGameFavorite(); + fav.setUser(user); + fav.setSharedGame(game); + sharedGameFavoriteRepository.save(fav); + liked = true; + } + + long likeCount = sharedGameFavoriteRepository.countBySharedGameId(game.getId()); + long playCount = sharedGameScoreRepository.countBySharedGameId(game.getId()); + Long maxScore = sharedGameScoreRepository.maxScoreBySharedGameId(game.getId()); + + // ํƒœ๊ทธ ์ด๋ฆ„๋“ค + var tags = gameTagRepository.findTagDtosBySharedGameId(game.getId()); + + 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 new file mode 100644 index 00000000..8ac0f3fb --- /dev/null +++ b/src/main/java/com/scriptopia/demo/service/SharedGameService.java @@ -0,0 +1,213 @@ +package com.scriptopia.demo.service; + +import com.scriptopia.demo.domain.*; +import com.scriptopia.demo.dto.sharedgame.*; +import com.scriptopia.demo.exception.CustomException; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.repository.*; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class SharedGameService { + private final SharedGameRepository sharedGameRepository; + private final HistoryRepository historyRepository; + private final UserRepository userRepository; + private final SharedGameScoreRepository sharedGameScoreRepository; + private final SharedGameFavoriteRepository sharedGameFavoriteRepository; + private final GameTagRepository gameTagRepository; + private final TagDefRepository tagDefRepository; + + @Transactional + public ResponseEntity saveSharedGame(Long Id, UUID uuid) { + User user = userRepository.findById(Id) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + + History history = historyRepository.findByUuid(uuid) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + if(!history.getUser().getId().equals(Id)) { + throw new CustomException(ErrorCode.E_401_NOT_EQUAL_SHARED_GAME); + } + + history.setIsShared(true); + + 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(dto); + } + + @Transactional + public void deleteSharedGame(Long id, UUID uuid) { + User user = userRepository.findById(id) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + + SharedGame game = sharedGameRepository.findByUuid(uuid) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_GAME_SESSION_NOT_FOUND)); + + if(!game.getUser().getId().equals(user.getId())) { // ๊ณต์œ ๋œ ๊ฒŒ์ž„๊ณผ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ + throw new CustomException(ErrorCode.E_401_NOT_EQUAL_SHARED_GAME); + } + + sharedGameRepository.delete(game); + } + + public ResponseEntity getDetailedSharedGame(Long userId, UUID uuid) { + SharedGame game = sharedGameRepository.findByUuid(uuid) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_SHARED_GAME_NOT_FOUND)); + + List tagDtos = gameTagRepository.findTagDtosBySharedGameId(game.getId()); + + List score = sharedGameScoreRepository.findAllBySharedGameIdOrderByScoreDescCreatedAtDesc(game.getId()); + boolean isLiked = (userId != null) && sharedGameFavoriteRepository.existsByUserIdAndSharedGameId(userId, game.getId()); + + PublicSharedGameDetailResponse dto = new PublicSharedGameDetailResponse(); + dto.setSharedGameUuid(game.getUuid()); + dto.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 tagDto : tagDtos) { + tagarray.add(new PublicSharedGameDetailResponse.TagDto(tagDto.getTagId(), tagDto.getTagName())); + } + + dto.setTags(tagarray); + + for(var topScoreInfo : score) { + 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); + } + + dto.setTopScores(topscorearray); + + return ResponseEntity.ok(dto); + } + + public ResponseEntity getTag() { + List tag = tagDefRepository.findAll(); + + List dtoList = tag.stream() + .map(t -> new PublicTagDefResponse(t.getId(), t.getTagName())) + .toList(); + + return ResponseEntity.ok(dtoList); + } + + @Transactional(readOnly = true) + public ResponseEntity> getPublicSharedGames( + UUID lastUuid, + int size, + List tagIds, + String q, + SharedGameSort sort) { + String raw = (q == null) ? "" : q; + String trimmed = raw.strip(); + boolean qBlank = trimmed.isEmpty(); + String qLike = "%" + trimmed.toLowerCase() + "%"; + + // 2) ํƒœ๊ทธ/์ปค์„œ/์ •๋ ฌ ์ „์ฒ˜๋ฆฌ + boolean tagEmpty = (tagIds == null || tagIds.isEmpty()); + SharedGameSort effectiveSort = qBlank ? sort : SharedGameSort.POPULAR; + + boolean useCursor = (lastUuid != null); + Long lastId = null; + LocalDateTime lastSharedAt = null; + Long lastPlayCount = null; + Long lastTopScore = null; + + if (useCursor) { + SharedGame pivot = sharedGameRepository.findByUuid(lastUuid) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_PAGE_NOT_FOUND)); + lastId = pivot.getId(); + + switch (effectiveSort) { + case LATEST -> lastSharedAt = pivot.getSharedAt(); + case POPULAR -> lastPlayCount = sharedGameScoreRepository.countBySharedGameId(lastId); + case TOP_SCORE -> { + Long max = sharedGameScoreRepository.maxScoreBySharedGameId(lastId); + lastTopScore = (max == null) ? 0L : max; + } + } + } + + // 3) ํŽ˜์ด์ง€ ์‚ฌ์ด์ฆˆ/ํŽ˜์ด์ง• + Pageable pageable = PageRequest.of(0, Math.max(1, size)); + + // 4) ์ •๋ ฌ ์Šค์œ„์น˜๋ณ„ ์Šฌ๋ผ์ด์Šค ์กฐํšŒ (qLike/qBlank ์‚ฌ์šฉ) + List rows = switch (effectiveSort) { + case LATEST -> sharedGameRepository.sliceLatest( + tagEmpty ? List.of(-1L) : tagIds, tagEmpty, + qLike, qBlank, + useCursor, lastSharedAt, lastId, + pageable + ); + case POPULAR -> sharedGameRepository.slicePopular( + tagEmpty ? List.of(-1L) : tagIds, tagEmpty, + qLike, qBlank, + useCursor, lastPlayCount, lastId, + pageable + ); + case TOP_SCORE -> sharedGameRepository.sliceTopScore( + tagEmpty ? List.of(-1L) : tagIds, tagEmpty, + qLike, qBlank, + useCursor, lastTopScore, lastId, + pageable + ); + }; + + // 5) DTO ๋งคํ•‘ (์ง‘๊ณ„ ์ผ์›ํ™”) + List items = rows.stream().map(g -> { + PublicSharedGameResponse dto = new PublicSharedGameResponse(); + dto.setSharedGameUuid(g.getUuid()); + dto.setThumbnailUrl(g.getThumbnailUrl()); + dto.setTitle(g.getTitle()); + + // ์ง‘๊ณ„ + dto.setPlayCount(sharedGameScoreRepository.countBySharedGameId(g.getId())); + + // ํƒœ๊ทธ + dto.setTags(gameTagRepository.findTagDtosBySharedGameId(g.getId())); + return dto; + }).toList(); + + // 6) ์ปค์„œ/hasNext + 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 new file mode 100644 index 00000000..b0045339 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/service/TagDefService.java @@ -0,0 +1,44 @@ +package com.scriptopia.demo.service; + +import com.scriptopia.demo.domain.TagDef; +import com.scriptopia.demo.dto.TagDef.TagDefCreateRequest; +import com.scriptopia.demo.dto.TagDef.TagDefDeleteRequest; +import com.scriptopia.demo.exception.CustomException; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.repository.TagDefRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class TagDefService { + private final TagDefRepository tagDefRepository; + + @Transactional + public ResponseEntity addTagName(TagDefCreateRequest req) { + String tagName = req.getTagName(); + + if(!tagDefRepository.existsByTagName(tagName)) { // ์ž…๋ ฅ๋œ ํƒœ๊ทธ ์ด๋ฏธ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ + TagDef tagDef = new TagDef(); + tagDef.setTagName(tagName); + tagDefRepository.save(tagDef); + + return ResponseEntity.ok(tagDef); + } + + throw new CustomException(ErrorCode.E_400_TAG_DUPLICATED); + } + + @Transactional + public ResponseEntity removeTagName(TagDefDeleteRequest req) { + TagDef tag = tagDefRepository.findByTagName(req.getTagName()) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_Tag_NOT_FOUND)); + + tagDefRepository.delete(tag); + return ResponseEntity.ok("์„ ํƒํ•˜์‹  ํƒœ๊ทธ๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + } +} diff --git a/src/main/java/com/scriptopia/demo/service/UserCharacterImgService.java b/src/main/java/com/scriptopia/demo/service/UserCharacterImgService.java new file mode 100644 index 00000000..3bc09286 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/service/UserCharacterImgService.java @@ -0,0 +1,126 @@ +package com.scriptopia.demo.service; + +import com.scriptopia.demo.domain.User; +import com.scriptopia.demo.domain.UserCharacterImg; +import com.scriptopia.demo.dto.usercharacterimg.UserCharacterImgResponse; +import com.scriptopia.demo.exception.CustomException; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.repository.UserCharacterImgRepository; +import com.scriptopia.demo.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class UserCharacterImgService { + private final UserCharacterImgRepository userCharacterImgRepository; + private final UserRepository userRepository; + + @Value("${image-dir}") + private String imageDir; + + @Value("${image-url-prefix:/images}") + private String imageUrlPrefix; + + @Transactional + public ResponseEntity saveCharacterImg(Long userId, MultipartFile file) { + String url = storeUserImage(userId, file); + return ResponseEntity.ok(url); + } + + @Transactional + public ResponseEntity saveUserCharacterImg(Long userId, String imageUrl) { + if(imageUrl == null || imageUrl.isEmpty()) { + throw new CustomException(ErrorCode.E_400_IMAGE_URL_ERROR); + } + + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + + UserCharacterImg src = userCharacterImgRepository.findByUserIdAndImgUrl(userId, imageUrl) + .orElseThrow(() -> new CustomException(ErrorCode.E_400_IMAGE_URL_ERROR)); + + user.setProfileImgUrl(src.getImgUrl()); + userRepository.save(user); + + return ResponseEntity.ok(user.getProfileImgUrl()); + } + + public ResponseEntity getUserCharacterImg(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + + List img = userCharacterImgRepository.findAllByUserId(user.getId()); + + List dto = new ArrayList<>(); + for(UserCharacterImg imgItem : img) { + UserCharacterImgResponse imgdto = new UserCharacterImgResponse(); + imgdto.setImgUrl(imgItem.getImgUrl()); + dto.add(imgdto); + } + + return ResponseEntity.ok(dto); + } + + private String storeUserImage(Long userId, MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new CustomException(ErrorCode.E_400_EMPTY_FILE); + } + + String contentType = file.getContentType(); + if (contentType == null || !contentType.startsWith("image/")) { + throw new CustomException(ErrorCode.E_400_EMPTY_FILE); // ์ด๋ฏธ์ง€ ์™ธ ์—…๋กœ๋“œ ์ฐจ๋‹จ + } + + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND)); + + // ํŒŒ์ผ๋ช…/๊ฒฝ๋กœ ์ƒ์„ฑ + String ext = getExtension(file.getOriginalFilename(), contentType); + String saveName = UUID.randomUUID() + ext; + + // ์‚ฌ์šฉ์ž๋ณ„ ํ•˜์œ„ ํด๋”(์˜ˆ: {imageDir}/character/{userId}/) + Path dir = Paths.get(imageDir, "character", String.valueOf(userId)) + .toAbsolutePath().normalize(); + Path dest = dir.resolve(saveName); + + try { + Files.createDirectories(dir); // ๋””๋ ‰ํ„ฐ๋ฆฌ ์—†์œผ๋ฉด ์ƒ์„ฑ + file.transferTo(dest.toFile()); // ํŒŒ์ผ ์ €์žฅ + + // ์ •์  ๋งคํ•‘ ๊ธฐ์ค€ ๊ณต๊ฐœ URL ์ƒ์„ฑ: /images/character/{userId}/{uuid}.png + String publicUrl = String.format("%s/character/%d/%s", imageUrlPrefix, userId, saveName); + + // DB์—๋Š” ๊ณต๊ฐœ URL ์ €์žฅ(ํ”„๋ก ํŠธ๊ฐ€ ๊ทธ๋Œ€๋กœ ๋กœ ์‚ฌ์šฉ) + UserCharacterImg entity = new UserCharacterImg(); + entity.setUser(user); + entity.setImgUrl(publicUrl); + userCharacterImgRepository.save(entity); + + return publicUrl; + } catch (Exception e) { + throw new CustomException(ErrorCode.E_500_File_SAVED_FAILED); + } + } + + private String getExtension(String originalFilename, String contentType) { + // 1) ํŒŒ์ผ๋ช…์—์„œ ํ™•์žฅ์ž ์šฐ์„  + if (originalFilename != null && originalFilename.contains(".")) { + return originalFilename.substring(originalFilename.lastIndexOf(".")); + } + // 2) ์—†์œผ๋ฉด MIME์œผ๋กœ ๋Œ€์ฒด + String subtype = contentType.substring(contentType.indexOf('/') + 1); // e.g. image/png โ†’ png + return "." + subtype; + } +} diff --git a/src/main/java/com/scriptopia/demo/service/UserService.java b/src/main/java/com/scriptopia/demo/service/UserService.java new file mode 100644 index 00000000..b78532c2 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/service/UserService.java @@ -0,0 +1,152 @@ +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; + private final ItemMapper itemMapper; + + @Transactional + public List getGameItems(String userId){ + + User user = userRepository.findById(Long.valueOf(userId)).orElseThrow( + () -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND) + ); + + return itemMapper.mapUser(user); + } + + + @Transactional + public UserSettingsDTO getUserSettings(String userId){ + + UserSetting userSetting = userSettingRepository.findByUserId(Long.valueOf(userId)).orElseThrow( + () -> new CustomException(ErrorCode.E_404_SETTING_NOT_FOUND) + ); + + return UserSettingsDTO.builder() + .theme(userSetting.getTheme()) + .fontSize(userSetting.getFontSize()) + .font(userSetting.getFontType()) + .lineHeight(userSetting.getLineHeight()) + .wordSpacing(userSetting.getWordSpacing()) + .build(); + + } + + @Transactional + public void updateUserSettings(String userId, UserSettingsDTO request){ + UserSetting userSetting = userSettingRepository.findByUserId(Long.valueOf(userId)).orElseThrow( + () -> new CustomException(ErrorCode.E_404_SETTING_NOT_FOUND) + ); + + userSetting.setTheme(request.getTheme()); + userSetting.setFontSize(request.getFontSize()); + userSetting.setFontType(request.getFont()); + userSetting.setLineHeight(request.getLineHeight()); + userSetting.setWordSpacing(request.getWordSpacing()); + userSetting.setUpdatedAt(LocalDateTime.now()); + + + } + + @Transactional + public UserAssetsResponse getUserAssets(String userId){ + User user = userRepository.findById(Long.valueOf(userId)).orElseThrow( + () -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND) + ); + + return UserAssetsResponse.builder() + .pia(user.getPia()) + .build(); + + } + + @Transactional + public List getPiaItems(String userId){ + User user = userRepository.findById(Long.valueOf(userId)).orElseThrow( + () -> new CustomException(ErrorCode.E_404_USER_NOT_FOUND) + ); + List piaItems = new ArrayList<>(); + for (UserPiaItem userPiaItem : userPiaItemRepository.findByUserId(Long.valueOf(userId))) { + piaItems.add( + PiaItemDTO.builder() + .name(userPiaItem.getPiaItem().getName()) + .quantity(userPiaItem.getQuantity()) + .build() + ); + } + + 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 new file mode 100644 index 00000000..d2e61442 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/utils/GameBalanceUtil.java @@ -0,0 +1,448 @@ +package com.scriptopia.demo.utils; + +import com.scriptopia.demo.domain.EffectProbability; +import com.scriptopia.demo.domain.Grade; +import com.scriptopia.demo.domain.RewardType; +import com.scriptopia.demo.domain.Stat; +import com.scriptopia.demo.domain.mongo.*; +import com.scriptopia.demo.exception.CustomException; +import com.scriptopia.demo.exception.ErrorCode; +import com.scriptopia.demo.repository.EffectGradeDefRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.security.SecureRandom; +import java.time.LocalDateTime; +import java.util.*; + +@Component +@RequiredArgsConstructor +public class GameBalanceUtil { + + static SecureRandom secureRandom = new SecureRandom(); + static final int PLAYER_BASE_STAT = 5; + + /** + * NPC ๊ธฐ๋ณธ ์Šคํƒฏ ํ…Œ์ด๋ธ” + * rank โ†’ [STR, AGI, INT, LUK] + */ + private static final int[][] NPC_BASE_STATS = { + {3, 3, 3, 3}, // rank 0 (dummy, ์‚ฌ์šฉ ์•ˆ ํ•จ) + {5, 4, 3, 2}, // rank 1 : C ํ•˜๊ธ‰ + {7, 6, 4, 3}, // rank 2 : C ์ผ๋ฐ˜ + {9, 7, 5, 4}, // rank 3 : C ์ƒ๊ธ‰ + {12, 9, 6, 5}, // rank 4 : U ์ค‘๊ธ‰ + {15, 12, 8, 6}, // rank 5 : U ์ค‘๊ธ‰+ + {18, 14, 10, 7},// rank 6 : R ์ •์˜ˆ ์‹œ์ž‘ + {22, 17, 12, 9},// rank 7 : R ๊ฐ•ํ•œ ์ •์˜ˆ + {28, 20, 15, 11},// rank 8 : E ๋ณด์Šค + {35, 25, 18, 14},// rank 9 : E ๋ณด์Šค๊ธ‰+ + {50, 35, 25, 20},// rank 10 : E+ ๋Œ€๋ฅ™ ์œ„ํ˜‘ + {70, 50, 35, 25},// rank 11 : L ๊ตญ๊ฐ€๊ธ‰ + {100, 70, 50, 40}// rank 12 : L+ ์ดˆ์›”๊ธ‰ + }; + + // NPC ๋“ฑ๊ธ‰๋ณ„ ์˜ˆ์ƒ HP์™€ ๊ณต๊ฒฉ๋ ฅ (HealthPoint, CombatPoint) + public static final int[][] NPC_BATTLE_STATS = { + {}, // index 0 dummy + {70, 18}, // rank 1: {HealthPoint, CombatPoint} + {105, 26}, // rank 2 + {140, 35}, // rank 3 + {152, 38}, // rank 4 + {188, 47}, // rank 5 + {204, 51}, // rank 6 + {248, 62}, // rank 7 + {268, 67}, // rank 8 + {320, 80}, // rank 9 + {344, 86}, // rank 10 + {707, 101}, // rank 11 + {963, 107} // rank 12 + }; + + + + private static final NavigableMap STAT_MULTIPLIERS = new TreeMap<>() {{ + put(5, 1.1); + put(10, 1.2); + put(15, 1.3); + put(20, 1.4); + put(25, 1.5); + put(30, 1.6); + put(35, 1.7); + put(40, 1.8); + put(45, 1.9); + put(Integer.MAX_VALUE, 2.0); // 46 ์ด์ƒ + }}; + + + /** + * @param grade + * @return (0: STR, 1: AGI, 2: INT, 3: LUCK) + */ + public static int[] getRandomItemStatsByGrade(Grade grade) { + int min = 0, max = 0; + switch (grade) { + case COMMON -> { min = 0; max = 3; } + case UNCOMMON -> { min = 1; max = 4; } + case RARE -> { min = 2; max = 5; } + case EPIC -> { min = 4; max = 7; } + case LEGENDARY -> { min = 5; max = 8; } + } + + int totalPoints = secureRandom.nextInt(max - min + 1) + min; + + int[] stats = new int[4]; + + for (int i = 0; i < totalPoints; i++) { + stats[secureRandom.nextInt(4)]++; + } + + return stats; + } + + + /** + * @param itemGradePrice + * @param effectGradeList + * @return Long + */ + public static Long getItemPriceByGrade(Long itemGradePrice, List effectGradeList) { + return getRandomItemPriceByGrade(itemGradePrice) + getRandomItemPriceByGrade(effectGradeList); + } + + + public static Long getRandomItemPriceByGrade(Long itemGradePrice) { + int priceRate = secureRandom.nextInt(21) - 10; + + Long itemPrice = 0L; + itemPrice += (long) Math.floor(itemGradePrice * (1 + priceRate / 100.0)); + return itemPrice; + } + + public static Long getRandomItemPriceByGrade(List effectGradeList) { + int priceRate = secureRandom.nextInt(21) - 10; + + Long effectPrice = 0L; + for (Long grade : effectGradeList) { + effectPrice += (long) Math.floor(grade * (1 + priceRate / 100.0)); + } + + return effectPrice; + } + + public static Integer getChoiceProbability(Stat stat, PlayerInfoMongo playerInfo) { + int baseProbability = 40; // ๊ธฐ๋ณธ ์„ ํƒ์ง€ ํ™•๋ฅ  40% + double multiplier = 0.9 + (0.2 * secureRandom.nextDouble()); // 0.9 ~ 1.1 + baseProbability = (int) (baseProbability * multiplier); + + double statBonus = 0; + + switch (stat) { + case STRENGTH -> statBonus = playerInfo.getStrength() * 0.8; + case AGILITY -> statBonus = playerInfo.getAgility() * 0.8; + case INTELLIGENCE -> statBonus = playerInfo.getIntelligence() * 0.8; + case LUCK -> statBonus = playerInfo.getLuck() * 0.8; + } + + double probability = baseProbability + statBonus; + + // ์ตœ์†Œ 30%, ์ตœ๋Œ€ 80% ์ œํ•œ + probability = Math.max(30, Math.min(80, probability)); + + return (int) Math.round(probability); + } + + + + /** + * ํ”Œ๋ ˆ์ด์–ด ๊ธฐ๋ณธ ์Šคํƒฏ ์ดˆ๊ธฐํ™” + * @param playerStat (ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์„ ํƒํ•œ ์ฃผ ์Šคํƒฏ) + * @return (0: STR, 1: AGI, 2: INT, 3: LUK) + */ + public static int[] getInitialPlayerStats(Stat playerStat) { + int mainStat = secureRandom.nextInt(3); // 0 ~ 2 + int subStat = secureRandom.nextInt(3) - 2; // -2 ~ 0 + + int[] stats = new int[4]; + stats[0] = (playerStat.equals(Stat.STRENGTH)) ? PLAYER_BASE_STAT + mainStat : PLAYER_BASE_STAT + subStat; + stats[1] = (playerStat.equals(Stat.AGILITY)) ? PLAYER_BASE_STAT + mainStat : PLAYER_BASE_STAT + subStat; + stats[2] = (playerStat.equals(Stat.INTELLIGENCE)) ? PLAYER_BASE_STAT + mainStat : PLAYER_BASE_STAT + subStat; + stats[3] = (playerStat.equals(Stat.LUCK)) ? PLAYER_BASE_STAT + mainStat : PLAYER_BASE_STAT + subStat; + + return stats; + } + + + /** + * NPC ์Šคํƒฏ ์ƒ์„ฑ (๋žญํฌ ๊ธฐ๋ฐ˜) + * @param rank (1 ~ 12) + * @return (0: STR, 1: AGI, 2: INT, 3: LUK) + */ + public static int[] getNpcStatsByRank(int rank) { + if (rank < 1 || rank >= NPC_BASE_STATS.length) { + throw new CustomException(ErrorCode.E_400_INVALID_NPC_RANK); + } + + int[] base = NPC_BASE_STATS[rank]; + int[] npcStats = new int[4]; + + // ์•ฝ๊ฐ„์˜ ๋žœ๋ค ํŽธ์ฐจ ์ถ”๊ฐ€ (ยฑ10%) + for (int i = 0; i < 4; i++) { + int variance = (int) Math.round(base[i] * (secureRandom.nextDouble() * 0.2 - 0.1)); + npcStats[i] = base[i] + variance; + } + + return npcStats; + } + + public static boolean getChoiceProbability(int statValue) { + double baseRate = 40.0; // ๊ธฐ๋ณธ ํ™•๋ฅ  40% + double minRate = 30.0; // ์ตœ์†Œ 30% + double maxRate = 80.0; // ์ตœ๋Œ€ 80% + double statBonus = 0.8 * statValue; // ์Šคํƒฏ 1๋‹น +0.8% + + double finalRate = baseRate + statBonus; + finalRate = Math.max(minRate, Math.min(finalRate, maxRate)); + + return secureRandom.nextDouble() * 100 < finalRate; + } + + public static int getPlayerWeaponDmg(PlayerInfoMongo player, ItemDefMongo weapon) { + int baseCombatPoint = 22; // ๋งจ์†์ผ ๋•Œ + + if (weapon == null) { + return baseCombatPoint; + } + + // ๋ฌด๊ธฐ ๋ฉ”์ธ ์Šคํƒฏ ๊ฒฐ์ • + Stat mainStat = weapon.getMainStat(); + int playerStatValue = switch (mainStat) { + case STRENGTH -> player.getStrength(); + case AGILITY -> player.getAgility(); + case INTELLIGENCE -> player.getIntelligence(); + case LUCK -> player.getLuck(); + }; + + double multiplier = STAT_MULTIPLIERS.ceilingEntry(playerStatValue).getValue(); + + // ์ตœ์ข… ์ „ํˆฌ๋ ฅ ๊ณ„์‚ฐ + int weaponStat = weapon.getBaseStat(); + int combatPoint = (int) (weaponStat * multiplier); + + return combatPoint; + } + + + /** + * @param player + * @param weapon + * @return PlayerCombatPoint + */ + public static int getBattlePlayerCombatPoint( + PlayerInfoMongo player, + ItemDefMongo weapon, + EffectGradeDefRepository effectGradeDefRepository + ) { + int combatPoint = getPlayerWeaponDmg(player, weapon); + + if (weapon == null) { + return combatPoint; + } + + double totalMultiplier = 1.0; + + for (ItemEffectMongo effect : weapon.getItemEffect()) { + EffectProbability prob = effect.getEffectProbability(); + var defOpt = effectGradeDefRepository.findByEffectProbability(prob); + if (defOpt.isPresent()) { + totalMultiplier += defOpt.get().getWeight(); // ์—ฌ๊ธฐ์„œ๋Š” ๋ฌธ์ œ ์—†์Œ + } + } + + // ์ตœ์ข… ๊ณ„์‚ฐ + combatPoint = (int) Math.round(combatPoint * totalMultiplier); + + return combatPoint; + } + + + + public static int simulateBattle(int playerCombatPoint, int npcCombatPoint){ + int maxNum = playerCombatPoint + npcCombatPoint; + int num = secureRandom.nextInt(maxNum) + 1; + + return playerCombatPoint >= num ? 1 : 0 ; + } + + public static List> getBattleLog( + int playerWin, + int playerDmg, + int playerHp, + int npcDmg, + int npcRank + ) { + List> hpLog = new ArrayList<>(); + + int npcHp = getNpcHealthPoint(npcRank); + int winnerDmg = (playerWin == 1) ? playerDmg : npcDmg; + int loserDmg = (playerWin == 1) ? npcDmg : playerDmg; + + int winnerCurrentHp = (playerWin == 1) ? playerHp : npcHp; + int loserCurrentHp = (playerWin == 1) ? npcHp : playerHp; + + int winnerIndex = (playerWin == 1) ? 0 : 1; + int loserIndex = (playerWin == 1) ? 1 : 0; + + List originHp = new ArrayList<>(Arrays.asList(0, 0)); + + originHp.set(loserIndex, loserCurrentHp); + originHp.set(winnerIndex, winnerCurrentHp); + + hpLog.add(new ArrayList<>(originHp)); // ์ดˆ๊ธฐ ๋กœ๊ทธ ์ถ”๊ฐ€ + List currentHp = originHp; + + + while (loserCurrentHp > 0) { + + + // ํŒจ๋ฐฐ์ž์—๊ฒŒ๋งŒ ๋ฐ๋ฏธ์ง€ ์ ์šฉ + int damage = (int) Math.round(winnerDmg * (0.9 + 0.4 * secureRandom.nextDouble())); + loserCurrentHp -= damage; + if (loserCurrentHp < 0) loserCurrentHp = 0; + + // winnerIndex, loserIndex ๊ธฐ์ค€์œผ๋กœ ์ •ํ™•ํžˆ ๋„ฃ๊ธฐ + currentHp.set(winnerIndex, winnerCurrentHp); + currentHp.set(loserIndex, loserCurrentHp); + + hpLog.add(new ArrayList<>(currentHp)); + } + + // ์Šน๋ฆฌ์ž ์ตœ์†Œ HP (ํŒจ๋ฐฐ์ž๊ฐ€ 0์ผ ๋•Œ) + int winnerMaxHp = (int) (winnerCurrentHp * (0.1 + secureRandom.nextDouble() * 0.4)); + + for (int i = 1 ; i < hpLog.size(); i++) { + List turnHp = hpLog.get(i); + int damage = (int) Math.round(loserDmg * (0.9 + 0.4 * secureRandom.nextDouble())); + winnerCurrentHp = Math.max(winnerCurrentHp - damage, winnerMaxHp); + turnHp.set(winnerIndex,winnerCurrentHp); + } + + + return hpLog; + } + + public static int getNpcCombatPoint(int npcRank) { + int base = NPC_BATTLE_STATS[npcRank][1]; + + double variance = 0.9 + secureRandom.nextDouble() * 0.2; // 0.9 ~ 1.1 + return (int) Math.round(base * variance); + } + + public static int getNpcHealthPoint(int npcRank) { + int base = NPC_BATTLE_STATS[npcRank][0]; + + double variance = 0.9 + secureRandom.nextDouble() * 0.2; // 0.9 ~ 1.1 + return (int) Math.round(base * variance); + } + + + public static boolean isPass(int choiceProbability) { + int randomCount = secureRandom.nextInt(100) + 1; + + return (choiceProbability >= randomCount); + } + + public static RewardInfoMongo getReward(RewardType rewardType, boolean isPass) { + RewardInfoMongo.RewardInfoMongoBuilder builder = RewardInfoMongo.builder(); + + // ๊ธฐ๋ณธ๊ฐ’: ์„ฑ๊ณต์ด๋ฉด ์ƒ๋ช… 0, ์‹คํŒจ๋ฉด ์ƒ๋ช… -1 + builder.rewardLife(isPass ? 0 : -1); + + switch (rewardType) { + case GOLD: + int gold = secureRandom.nextInt(50) + 1; // 1~50 ๊ณจ๋“œ + builder.rewardGold(isPass ? gold : -gold); + break; + + case STAT: + int statValue = secureRandom.nextInt(3) + 1; // 1~3 ์‚ฌ์ด ์Šคํƒฏ ๋ณ€ํ™” + int statType = secureRandom.nextInt(4); // 0~3 โ†’ ํž˜, ๋ฏผ์ฒฉ, ์ง€๋Šฅ, ์šด ์ค‘ ํ•˜๋‚˜ + switch (statType) { + case 0 -> builder.rewardStrength(isPass ? statValue : -statValue); + case 1 -> builder.rewardAgility(isPass ? statValue : -statValue); + case 2 -> builder.rewardIntelligence(isPass ? statValue : -statValue); + case 3 -> builder.rewardLuck(isPass ? statValue : -statValue); + } + break; + case ITEM: + case NONE: + default: + // ์•„๋ฌด ๋ณด์ƒ ์—†์Œ + break; + } + + return builder.build(); + } + + public static PlayerInfoMongo updateReward(PlayerInfoMongo playerInfo, RewardInfoMongo rewardInfo) { + if (rewardInfo != null) { + if (rewardInfo.getRewardStrength() != null) + playerInfo.setStrength(playerInfo.getStrength() + rewardInfo.getRewardStrength()); + + if (rewardInfo.getRewardAgility() != null) + playerInfo.setAgility(playerInfo.getAgility() + rewardInfo.getRewardAgility()); + + if (rewardInfo.getRewardIntelligence() != null) + playerInfo.setIntelligence(playerInfo.getIntelligence() + rewardInfo.getRewardIntelligence()); + + if (rewardInfo.getRewardLuck() != null) + playerInfo.setLuck(playerInfo.getLuck() + rewardInfo.getRewardLuck()); + + if (rewardInfo.getRewardLife() != null) + playerInfo.setLife(playerInfo.getLife() + rewardInfo.getRewardLife()); + + if (rewardInfo.getRewardGold() != null) + if (playerInfo.getGold() + rewardInfo.getRewardGold() < 0){ + playerInfo.setGold(0L); + }else{ + playerInfo.setGold(playerInfo.getGold() + rewardInfo.getRewardGold()); + } + + + if (rewardInfo.getRewardTrait() != null) + playerInfo.setTrait(rewardInfo.getRewardTrait()); + } + + return playerInfo; + } + + + public static List updateRewardItem(List inventory, RewardInfoMongo rewardInfo) { + if (rewardInfo == null || rewardInfo.getGainedItemDefId() == null) { + return inventory; + } + + for (String id : rewardInfo.getGainedItemDefId()) { + InventoryMongo newItem = InventoryMongo.builder() + .itemDefId(id) + .equipped(false) + .source("Scenario") + .build(); + inventory.add(newItem); + } + + return inventory; + } + + public static List updateInGameItem(List inGameItem, RewardInfoMongo rewardInfo) { + if (rewardInfo == null || rewardInfo.getGainedItemDefId() == null) { + return inGameItem; + } + + for (String id : rewardInfo.getGainedItemDefId()) { + inGameItem.add(id); + } + + return inGameItem; + } +} \ No newline at end of file diff --git a/src/main/java/com/scriptopia/demo/utils/InitGameData.java b/src/main/java/com/scriptopia/demo/utils/InitGameData.java new file mode 100644 index 00000000..6c1c8596 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/utils/InitGameData.java @@ -0,0 +1,115 @@ +package com.scriptopia.demo.utils; + +import com.scriptopia.demo.domain.*; +import com.scriptopia.demo.repository.EffectGradeDefRepository; +import com.scriptopia.demo.repository.ItemGradeDefRepository; +import lombok.*; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Data +public class InitGameData { + + private final ItemGradeDefRepository itemGradeDefRepository; + private final EffectGradeDefRepository effectGradeDefRepository; + + static SecureRandom secureRandom = new SecureRandom(); + static final int PLAYER_BASE_STAT = 5; + //Game Session + private List stages; + + //PlayerInfo + private Integer life; + private Integer level; + private Integer healthPoint; + private Integer experiencePoint; + private Integer playerStr; + private Integer playerAgi; + private Integer playerInt; + private Integer playerLuk; + private Long gold; + + //ItemDef + private ItemType category; + private Integer baseStat; + private Integer itemStr; + private Integer itemAgi; + private Integer itemInt; + private Integer itemLuk; + private Long itemPrice; + + public InitGameData(Stat playerStat, Grade grade, ItemGradeDefRepository itemRepo, + EffectGradeDefRepository effectRepo) { + + + this.itemGradeDefRepository = itemRepo; + this.effectGradeDefRepository = effectRepo; + + this.stages = initStage(); + + this.life = 5; + this.level = 1; + this.experiencePoint = 0; + this.healthPoint = 80; + + int mainStat = secureRandom.nextInt(3); + int subStat = secureRandom.nextInt(3) - 2; + + + + this.playerStr = (playerStat.equals(Stat.STRENGTH)) ? PLAYER_BASE_STAT + mainStat : PLAYER_BASE_STAT + subStat; + this.playerAgi = (playerStat.equals(Stat.AGILITY)) ? PLAYER_BASE_STAT + mainStat : PLAYER_BASE_STAT + subStat; + this.playerInt = (playerStat.equals(Stat.INTELLIGENCE)) ? PLAYER_BASE_STAT + mainStat : PLAYER_BASE_STAT + subStat; + this.playerLuk = (playerStat.equals(Stat.LUCK)) ? PLAYER_BASE_STAT + mainStat : PLAYER_BASE_STAT + subStat; + this.gold = secureRandom.nextLong(50) + 50; + + this.category = ItemType.WEAPON; + + int attackRate = secureRandom.nextInt(21) - 10; + this.baseStat = (int) Math.floor(Grade.COMMON.getAttackPower() * (1 + attackRate / 100.0)); + + + + + + // ๋ฐฐ์—ด ์ƒ์„ฑ (0: STR, 1: AGI, 2: INT, 3: LUCK) + int[] stats = GameBalanceUtil.getRandomItemStatsByGrade(grade); + this.itemStr = stats[0]; + this.itemAgi = stats[1]; + this.itemInt = stats[2]; + this.itemLuk = stats[3]; + + + List itemEffectList = new ArrayList<>(); + itemEffectList.add(effectGradeDefRepository.findPriceByEffectProbability(EffectProbability.COMMON).get()); + Long gradePrice = itemGradeDefRepository.findPriceByGrade(grade); + + this.itemPrice = GameBalanceUtil.getItemPriceByGrade(gradePrice , itemEffectList); + + } + + private List initStage(){ + List arr = Arrays.asList(1, 2, 3, 4, 5, 6); + List result = new ArrayList<>(); + + int leadingZeros = 2 + secureRandom.nextInt(3); // 2~4 + for (int j = 0; j < leadingZeros; j++) { + result.add(0); + } + + for (int i = 0; i < arr.size(); i++) { + result.add(arr.get(i)); + if (i < arr.size() - 1) { + int zeros = 2 + secureRandom.nextInt(3); + for (int j = 0; j < zeros; j++) { + result.add(0); + } + } + } + return result; + } + +} diff --git a/src/main/java/com/scriptopia/demo/utils/InitItemData.java b/src/main/java/com/scriptopia/demo/utils/InitItemData.java new file mode 100644 index 00000000..3ed6c2cb --- /dev/null +++ b/src/main/java/com/scriptopia/demo/utils/InitItemData.java @@ -0,0 +1,57 @@ +package com.scriptopia.demo.utils; + +import com.scriptopia.demo.domain.EffectProbability; +import com.scriptopia.demo.domain.Grade; +import com.scriptopia.demo.domain.ItemType; +import com.scriptopia.demo.domain.Stat; +import com.scriptopia.demo.repository.EffectGradeDefRepository; +import com.scriptopia.demo.repository.ItemGradeDefRepository; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class InitItemData { + private ItemType itemType; + private Grade grade; + private Integer baseStat; + private Stat mainStat; + private int[] stats; + private List effectGrades; + private List effectPrices; + private Long gradePrice; + private Long itemPrice; + private Integer remainingUses; + + public InitItemData(ItemGradeDefRepository itemGradeDefRepository, + EffectGradeDefRepository effectGradeDefRepository) { + + // ์•„์ดํ…œ ํƒ€์ž…, ๋“ฑ๊ธ‰, ๊ธฐ๋ณธ ์Šคํƒฏ, ๋ฉ”์ธ ์Šคํƒฏ + this.itemType = ItemType.getRandomItemType(); + this.grade = Grade.getRandomGradeByProbability(); + this.baseStat = Grade.getRandomBaseStat(itemType, grade); + this.mainStat = Stat.getRandomMainStat(); + + // ์ถ”๊ฐ€ ์Šคํƒฏ + this.stats = GameBalanceUtil.getRandomItemStatsByGrade(grade); + + // ์ดํŽ™ํŠธ ์Šคํƒฏ ๋ฐ ๊ฐ€๊ฒฉ + this.effectGrades = new ArrayList<>(); + this.effectPrices = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + EffectProbability effectGrade = EffectProbability.getRandomEffectGradeByWeaponGrade(grade); + if (effectGrade != null) { + Long effectPrice = effectGradeDefRepository.findPriceByEffectProbability(effectGrade) + .orElseThrow(() -> new IllegalStateException("EffectGradeDef not found: " + effectGrade)); + this.effectGrades.add(effectGrade); + this.effectPrices.add(effectPrice); + } + } + + // ๋“ฑ๊ธ‰ ๊ฐ€๊ฒฉ, ์ตœ์ข… ์•„์ดํ…œ ๊ฐ€๊ฒฉ + this.gradePrice = itemGradeDefRepository.findPriceByGrade(grade); + this.itemPrice = GameBalanceUtil.getItemPriceByGrade(gradePrice, effectPrices); + this.remainingUses = 5; + } +} diff --git a/src/main/java/com/scriptopia/demo/utils/JwtKeyFactory.java b/src/main/java/com/scriptopia/demo/utils/JwtKeyFactory.java new file mode 100644 index 00000000..64859da1 --- /dev/null +++ b/src/main/java/com/scriptopia/demo/utils/JwtKeyFactory.java @@ -0,0 +1,31 @@ +package com.scriptopia.demo.utils; + +import com.scriptopia.demo.config.JwtProperties; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.security.Key; + +@Component +public class JwtKeyFactory { + private final JwtProperties props; + public JwtKeyFactory(final JwtProperties props) { this.props = props; } + + public Key hmacKey() { + byte[] keyBytes = toKeyBytes(props.secret()); + if (keyBytes.length < 32) { + throw new IllegalStateException(); + } + return Keys.hmacShaKeyFor(keyBytes); + } + + private static byte[] toKeyBytes(String secret) { + try { + return Decoders.BASE64.decode(secret); + } catch (IllegalArgumentException notBase64) { + return secret.getBytes(StandardCharsets.UTF_8); + } + } +} diff --git a/src/main/java/com/scriptopia/demo/utils/JwtProvider.java b/src/main/java/com/scriptopia/demo/utils/JwtProvider.java new file mode 100644 index 00000000..856fc42b --- /dev/null +++ b/src/main/java/com/scriptopia/demo/utils/JwtProvider.java @@ -0,0 +1,93 @@ +package com.scriptopia.demo.utils; + +import com.scriptopia.demo.config.JwtProperties; +import io.jsonwebtoken.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class JwtProvider { + + private final JwtProperties props; + private final JwtKeyFactory keyFactory; + + private Key signingKey() { + return keyFactory.hmacKey(); + } + + public String createAccessToken(Long userId, List roles) { + Instant now = Instant.now(); + return Jwts.builder() + .setIssuer(props.issuer()) + .setSubject(String.valueOf(userId)) // userId๋ฅผ subject๋กœ + .claim("roles", roles) + .setId(UUID.randomUUID().toString()) + .setIssuedAt(Date.from(now)) + .setExpiration(Date.from(now.plusSeconds(props.accessExpSeconds()))) + .signWith(signingKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public String createRefreshToken(Long userId, String deviceId) { + Instant now = Instant.now(); + return Jwts.builder() + .setIssuer(props.issuer()) + .setSubject(String.valueOf(userId)) + .claim("device",deviceId) + .setId(UUID.randomUUID().toString()) + .setIssuedAt(Date.from(now)) + .setExpiration(Date.from(now.plusSeconds(props.refreshExpSeconds()))) + .signWith(signingKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public Jws parse(String token) throws JwtException { + return Jwts.parserBuilder() + .setSigningKey(signingKey()) + .build() + .parseClaimsJws(token); + } + + public Long getUserId(String token) { + return Long.valueOf(parse(token).getBody().getSubject()); + } + + @SuppressWarnings("unchecked") + public List getRoles(String token) { + return (List) parse(token).getBody().get("roles"); + } + + public String getDeviceId(String token) { + Object v = parse(token).getBody().get("device"); + return v == null ? null : v.toString(); + } + + public String getJti(String token) { + return parse(token).getBody().getId(); + } + + public Date getExpiry(String token) { + return parse(token).getBody().getExpiration(); + } + + public boolean isValid(String token) { + try { + parse(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + + public String getEmail(String token) { + return parse(token).getBody().getSubject(); + } + +} diff --git a/src/main/java/com/scriptopia/demo/utils/client/GoogleClient.java b/src/main/java/com/scriptopia/demo/utils/client/GoogleClient.java new file mode 100644 index 00000000..67fd955b --- /dev/null +++ b/src/main/java/com/scriptopia/demo/utils/client/GoogleClient.java @@ -0,0 +1,66 @@ +package com.scriptopia.demo.utils.client; + +import com.scriptopia.demo.config.OAuthProperties; +import com.scriptopia.demo.domain.Provider; +import com.scriptopia.demo.dto.oauth.OAuthUserInfo; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class GoogleClient { + + private final OAuthProperties props; + private final RestTemplate restTemplate = new RestTemplate(); + + public OAuthUserInfo getUserInfo(String code) { + + String tokenUrl = "https://oauth2.googleapis.com/token"; + + Map params = new HashMap<>(); + params.put("code", code); + params.put("client_id", props.getGoogle().getClientId()); + params.put("client_secret", props.getGoogle().getClientSecret()); + params.put("redirect_uri", props.getGoogle().getRedirectUri()); + params.put("grant_type", "authorization_code"); + + ResponseEntity tokenResponse = + restTemplate.postForEntity(tokenUrl, params, Map.class); + + String accessToken = (String) tokenResponse.getBody().get("access_token"); + + + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity userInfoResponse = + restTemplate.exchange( + "https://www.googleapis.com/oauth2/v2/userinfo", + HttpMethod.GET, + entity, + Map.class + ); + + Map userInfo = userInfoResponse.getBody(); + + + return OAuthUserInfo.builder() + .id((String) userInfo.get("id")) + .email((String) userInfo.get("email")) + .name((String) userInfo.get("name")) + .profileImage((String) userInfo.get("picture")) + .provider(Provider.GOOGLE.toString()) + .build(); + } +} diff --git a/src/main/java/com/scriptopia/demo/utils/client/KakaoClient.java b/src/main/java/com/scriptopia/demo/utils/client/KakaoClient.java new file mode 100644 index 00000000..a0aaca5c --- /dev/null +++ b/src/main/java/com/scriptopia/demo/utils/client/KakaoClient.java @@ -0,0 +1,72 @@ +package com.scriptopia.demo.utils.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.scriptopia.demo.config.OAuthProperties; +import com.scriptopia.demo.dto.oauth.OAuthUserInfo; +import lombok.RequiredArgsConstructor; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class KakaoClient { + + private final RestTemplate restTemplate; + private final OAuthProperties props; + private final ObjectMapper objectMapper; + + public OAuthUserInfo getUserInfo(String code) { + // ์•ก์„ธ์Šค ํ† ํฐ ๋ฐœ๊ธ‰ + String tokenUrl = "https://kauth.kakao.com/oauth/token"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", props.getKakao().getClientId()); + params.add("client_secret", props.getKakao().getClientSecret()); + params.add("redirect_uri", props.getKakao().getRedirectUri()); + params.add("code", code); + + HttpEntity> request = new HttpEntity<>(params, headers); + ResponseEntity tokenRes = restTemplate.postForEntity(tokenUrl, request, Map.class); + + String accessToken = (String) tokenRes.getBody().get("access_token"); + + // 2. ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ + HttpHeaders userHeaders = new HttpHeaders(); + userHeaders.setBearerAuth(accessToken); + HttpEntity userReq = new HttpEntity<>(userHeaders); + + ResponseEntity userRes = restTemplate.exchange( + "https://kapi.kakao.com/v2/user/me", + HttpMethod.GET, + userReq, + Map.class + ); + + Map body = userRes.getBody(); + String id = String.valueOf(body.get("id")); + + Map kakaoAccount = (Map) body.get("kakao_account"); + String email = (String) kakaoAccount.get("email"); + + Map profile = (Map) kakaoAccount.get("profile"); + String nickname = (String) profile.get("nickname"); + String profileImage = (String) profile.get("profile_image_url"); + + return OAuthUserInfo.builder() + .id(id) + .email(email) + .name(nickname) + .profileImage(profileImage) + .provider("KAKAO") + .build(); + } +} + diff --git a/src/main/java/com/scriptopia/demo/utils/client/NaverClient.java b/src/main/java/com/scriptopia/demo/utils/client/NaverClient.java new file mode 100644 index 00000000..1d6b17ce --- /dev/null +++ b/src/main/java/com/scriptopia/demo/utils/client/NaverClient.java @@ -0,0 +1,69 @@ +package com.scriptopia.demo.utils.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.scriptopia.demo.config.OAuthProperties; +import com.scriptopia.demo.dto.oauth.OAuthUserInfo; +import lombok.RequiredArgsConstructor; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class NaverClient { + + private final RestTemplate restTemplate; + private final OAuthProperties props; + private final ObjectMapper objectMapper; + + public OAuthUserInfo getUserInfo(String code) { + // ์•ก์„ธ์Šค ํ† ํฐ ๋ฐœ๊ธ‰ + String tokenUrl = "https://nid.naver.com/oauth2.0/token"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", props.getNaver().getClientId()); + params.add("client_secret", props.getNaver().getClientSecret()); + params.add("redirect_uri", props.getNaver().getRedirectUri()); + params.add("code", code); + + HttpEntity> request = new HttpEntity<>(params, headers); + ResponseEntity tokenRes = restTemplate.postForEntity(tokenUrl, request, Map.class); + + String accessToken = (String) tokenRes.getBody().get("access_token"); + + // ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ + HttpHeaders userHeaders = new HttpHeaders(); + userHeaders.setBearerAuth(accessToken); + HttpEntity userReq = new HttpEntity<>(userHeaders); + + ResponseEntity userRes = restTemplate.exchange( + "https://openapi.naver.com/v1/nid/me", + HttpMethod.GET, + userReq, + Map.class + ); + + Map body = userRes.getBody(); + Map resp = (Map) body.get("response"); + + String id = (String) resp.get("id"); + String email = (String) resp.get("email"); + String name = (String) resp.get("name"); + String profileImage = (String) resp.get("profile_image"); + + return OAuthUserInfo.builder() + .id(id) + .email(email) + .name(name) + .profileImage(profileImage) + .provider("NAVER") + .build(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1c1221a2..b6b6d07d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,10 @@ spring: + thymeleaf: + prefix: classpath:/templates/ + suffix: .html + + config: + import: optional:file:.env[.properties] datasource: url: jdbc:postgresql://localhost:5432/scriptopia username: root # PostgreSQL์—์„œ ์„ค์ • ๊ณ„์ • @@ -6,13 +12,74 @@ spring: driver-class-name: org.postgresql.Driver jpa: hibernate: - ddl-auto: create + ddl-auto: update properties: hibernate: show_sql: true format_sql: true database-platform: org.hibernate.dialect.PostgreSQLDialect + web: + resources: + add-mappings: true data: mongodb: uri: mongodb://root:tiger@localhost:27017/scriptopia_mongo?authSource=admin + redis: + host: 127.0.0.1 + port: 6379 + mail: + host: smtp.gmail.com + port: 587 + username: ${SPRING_MAIL_NAME} + password: ${SPRING_MAIL_PASSWORD} + properties: + mail: + smtp: + auth: true + starttls: + enable: true + +oauth: + google: + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_SECRET_KEY} + redirect-uri: ${GOOGLE_REDIRECT_URI} + scope: openid email profile + kakao: + client-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_SECRET} + redirect-uri: ${KAKAO_REDIRECT_URI} + scope: account_email profile_nickname profile_image + naver: + client-id: ${NAVER_CLIENT_ID} + client-secret: ${NAVER_SECRET} + redirect-uri: ${NAVER_REDIRECT_URI} + scope: name email profile_image + +auth: + jwt: + issuer: scriptopia + access-exp-seconds: 18000 # ๋กœ์ปฌ ํ…Œ์ŠคํŠธ์šฉ 300๋ถ„์œผ๋กœ ๋ณ€๊ฒฝ + refresh-exp-seconds: 1209600 + secret: ${JWT_SECRET} + +app: + admin: + 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 +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 12b283c7..de235518 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -2,9 +2,661 @@ - Scriptopia! + Scriptopia - Main + + -

Hello, Scriptopia!

+
+ + +
+
+

๋กœ๊ทธ์ธ

+
+
+
+ +
+
+ ๋กœ๊ทธ์ธ ํ† ํฐ: +
+ +
+
+ + + + +
+ +
+
+

Player Info

+
HP: 100
MP: 50
Level: 1
+
+
+

์žฅ์ฐฉ ์ค‘ ์žฅ๋น„

+
์—†์Œ
+
+ +
+

์†Œ์œ  ์•„์ดํ…œ

+
์—†์Œ
+
+
+ + +
+
๊ฒŒ์ž„ ์‹œ์ž‘ ์ „...
+
+
+ + +
+ + +
+
+
+ + + + + diff --git a/src/test/java/com/scriptopia/demo/jwt/JwtProviderTest.java b/src/test/java/com/scriptopia/demo/jwt/JwtProviderTest.java new file mode 100644 index 00000000..c86e372a --- /dev/null +++ b/src/test/java/com/scriptopia/demo/jwt/JwtProviderTest.java @@ -0,0 +1,42 @@ +package com.scriptopia.demo.jwt; + +import com.scriptopia.demo.config.JwtProperties; +import com.scriptopia.demo.utils.JwtKeyFactory; +import com.scriptopia.demo.utils.JwtProvider; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +public class JwtProviderTest { + + @Autowired + JwtProperties props; + + @Test + void issue_and_parse() { + // ๊ฐ€์งœ ํ”„๋กœํผํ‹ฐ + var props = new JwtProperties("scriptopia", 1800, 1209600, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@"); // >= 64 bytes + var keyFactory = new JwtKeyFactory(props); + + var provider = new JwtProvider(props, keyFactory); + + String at = provider.createAccessToken(1L, List.of("ROLE_USER")); + assertThat(provider.isValid(at)).isTrue(); + assertThat(provider.getUserId(at)).isEqualTo(1L); + assertThat(provider.getRoles(at)).containsExactly("ROLE_USER"); + System.out.println(at); + String rt = provider.createRefreshToken(1L, "dev-abc"); + System.out.println(rt); + assertThat(provider.isValid(rt)).isTrue(); + assertThat(provider.getDeviceId(rt)).isEqualTo("dev-abc"); + } + @Test + void test() { + System.out.println(props.secret()); + } +} diff --git a/src/test/java/com/scriptopia/demo/jwt/RefreshTokenServiceTest.java b/src/test/java/com/scriptopia/demo/jwt/RefreshTokenServiceTest.java new file mode 100644 index 00000000..1c0e2578 --- /dev/null +++ b/src/test/java/com/scriptopia/demo/jwt/RefreshTokenServiceTest.java @@ -0,0 +1,80 @@ +package com.scriptopia.demo.jwt; + +import com.scriptopia.demo.config.JwtProperties; +import com.scriptopia.demo.repository.RedisRefreshRepository; +import com.scriptopia.demo.repository.RefreshRepository; +import com.scriptopia.demo.service.RefreshTokenService; +import com.scriptopia.demo.utils.JwtKeyFactory; +import com.scriptopia.demo.utils.JwtProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +@SpringBootTest +@ActiveProfiles("test") +class RefreshTokenServiceTest { + + @Autowired StringRedisTemplate redis; + @Autowired JwtProperties props; + @Autowired PasswordEncoder passwordEncoder; + + JwtProvider jwt; + RefreshTokenService refreshSvc; + RefreshRepository refreshRepository; + + @BeforeEach + void setup() { + // ํ…Œ์ŠคํŠธ๋งˆ๋‹ค Redis DB ๋น„์šฐ๊ธฐ + var conn = redis.getConnectionFactory().getConnection(); + conn.serverCommands().flushDb(); + + // ์‹ค์ œ ๊ตฌ์„ฑ์š”์†Œ ์ƒ์„ฑ (ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์™€ ๋™์ผ) + var keyFactory = new JwtKeyFactory(props); + this.jwt = new JwtProvider(props, keyFactory); + this.refreshRepository = new RedisRefreshRepository(redis, new com.fasterxml.jackson.databind.ObjectMapper()); + this.refreshSvc = new RefreshTokenService(refreshRepository, jwt, passwordEncoder); + } + + @Test + void login_save_rotate_logout_flow() { + long userId = 2L; + String deviceId = "dev-win"; + List roles = List.of("ROLE_USER"); + + // ๋กœ๊ทธ์ธ: RT ๋ฐœ๊ธ‰ + ์ €์žฅ + String rt1 = jwt.createRefreshToken(userId, deviceId); + refreshSvc.saveLoginRefresh(userId, rt1, deviceId, "127.0.0.1", "JUnit"); + + // ์ €์žฅ ํ™•์ธ + var jti1 = jwt.getJti(rt1); + assertThat(refreshRepository.find(userId, jti1)).isPresent(); + + // ํšŒ์ „: ๊ธฐ์กด RT๋กœ ์ƒˆ AT/RT ๋ฐœ๊ธ‰ โ†’ ๊ธฐ์กด ์„ธ์…˜ ์‚ญ์ œ + var pair = refreshSvc.rotate(rt1, deviceId, roles); + assertThat(pair.accessToken()).isNotBlank(); + assertThat(pair.refreshToken()).isNotBlank(); + + // ๊ธฐ์กด ์„ธ์…˜์€ ์‚ญ์ œํ™•์ธ + assertThat(refreshRepository.find(userId, jti1)).isEmpty(); + + // ์ƒˆ ์„ธ์…˜ ์ €์žฅ ํ™•์ธ + var rt2 = pair.refreshToken(); + var jti2 = jwt.getJti(rt2); + assertThat(refreshRepository.find(userId, jti2)).isPresent(); + + assertThatThrownBy(() -> refreshSvc.rotate(rt1, deviceId, roles)) + .isInstanceOf(IllegalArgumentException.class); + + // ๋กœ๊ทธ์•„์›ƒ: ํ˜„์žฌ RT ์‚ญ์ œ + refreshSvc.logout(rt2); + assertThat(refreshRepository.find(userId, jti2)).isEmpty(); + } +} diff --git a/uploads/character/2/93e02b99-a2c5-4119-9ffd-c9e522bf2348.jpeg b/uploads/character/2/93e02b99-a2c5-4119-9ffd-c9e522bf2348.jpeg new file mode 100644 index 00000000..98795f0c Binary files /dev/null and b/uploads/character/2/93e02b99-a2c5-4119-9ffd-c9e522bf2348.jpeg differ