diff --git a/build.gradle b/build.gradle index ccc506a5..2b685f44 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,8 @@ dependencies { implementation group: 'com.google.http-client', name: 'google-http-client-jackson2', version: '1.25.0' implementation group: 'com.google.collections', name: 'google-collections', version: '1.0' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-cache' + implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-openfeign', version: '4.1.3' diff --git a/src/main/java/com/recipe/app/src/common/client/apple/dto/AppleAuthResponse.java b/src/main/java/com/recipe/app/src/common/client/apple/dto/AppleAuthResponse.java index d7ee26d0..85d02f23 100644 --- a/src/main/java/com/recipe/app/src/common/client/apple/dto/AppleAuthResponse.java +++ b/src/main/java/com/recipe/app/src/common/client/apple/dto/AppleAuthResponse.java @@ -1,5 +1,6 @@ package com.recipe.app.src.common.client.apple.dto; +import com.recipe.app.src.user.domain.NicknameGenerator; import com.recipe.app.src.user.domain.User; import lombok.Builder; import lombok.Getter; @@ -16,7 +17,7 @@ public User toEntity(String fcmToken) { return User.builder() .socialId("apple_" + sub) - .nickname(name != null ? name : "Apple User") + .nickname(NicknameGenerator.generate()) .email(email) .deviceToken(fcmToken) .build(); diff --git a/src/main/java/com/recipe/app/src/common/client/google/dto/GoogleAuthResponse.java b/src/main/java/com/recipe/app/src/common/client/google/dto/GoogleAuthResponse.java index 0c91580b..dd62ba99 100644 --- a/src/main/java/com/recipe/app/src/common/client/google/dto/GoogleAuthResponse.java +++ b/src/main/java/com/recipe/app/src/common/client/google/dto/GoogleAuthResponse.java @@ -1,5 +1,6 @@ package com.recipe.app.src.common.client.google.dto; +import com.recipe.app.src.user.domain.NicknameGenerator; import com.recipe.app.src.user.domain.User; import lombok.Getter; @@ -14,7 +15,7 @@ public User toEntity(String fcmToken) { return User.builder() .socialId("google_" + sub) - .nickname(name) + .nickname(NicknameGenerator.generate()) .email(email) .deviceToken(fcmToken) .build(); diff --git a/src/main/java/com/recipe/app/src/common/client/kakao/dto/KakaoAuthResponse.java b/src/main/java/com/recipe/app/src/common/client/kakao/dto/KakaoAuthResponse.java index 516dd902..13c1b62c 100644 --- a/src/main/java/com/recipe/app/src/common/client/kakao/dto/KakaoAuthResponse.java +++ b/src/main/java/com/recipe/app/src/common/client/kakao/dto/KakaoAuthResponse.java @@ -1,5 +1,6 @@ package com.recipe.app.src.common.client.kakao.dto; +import com.recipe.app.src.user.domain.NicknameGenerator; import com.recipe.app.src.user.domain.User; import lombok.Getter; @@ -13,7 +14,7 @@ public User toEntity(String fcmToken) { return User.builder() .socialId("kakao_" + id) - .nickname(kakao_account.getNickname()) + .nickname(NicknameGenerator.generate()) .email(kakao_account.getEmail()) .deviceToken(fcmToken) .build(); diff --git a/src/main/java/com/recipe/app/src/common/client/naver/dto/NaverAuthInfoResponse.java b/src/main/java/com/recipe/app/src/common/client/naver/dto/NaverAuthInfoResponse.java index 04167c62..5e25a8bb 100644 --- a/src/main/java/com/recipe/app/src/common/client/naver/dto/NaverAuthInfoResponse.java +++ b/src/main/java/com/recipe/app/src/common/client/naver/dto/NaverAuthInfoResponse.java @@ -1,5 +1,6 @@ package com.recipe.app.src.common.client.naver.dto; +import com.recipe.app.src.user.domain.NicknameGenerator; import com.recipe.app.src.user.domain.User; import lombok.Getter; @@ -15,7 +16,7 @@ public User toEntity(String fcmToken) { return User.builder() .socialId("naver_" + id) - .nickname(name) + .nickname(NicknameGenerator.generate()) .email(email) .phoneNumber(mobile) .deviceToken(fcmToken) diff --git a/src/main/java/com/recipe/app/src/common/config/CacheConfig.java b/src/main/java/com/recipe/app/src/common/config/CacheConfig.java new file mode 100644 index 00000000..ebb01a04 --- /dev/null +++ b/src/main/java/com/recipe/app/src/common/config/CacheConfig.java @@ -0,0 +1,23 @@ +package com.recipe.app.src.common.config; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.TimeUnit; + +@Configuration +public class CacheConfig { + + /** + * Access Token 블랙리스트 전용 Caffeine Cache + * JWT exp와 동일하게 24시간 유지 + */ + @Bean(name = "accessTokenBlacklistCache") + public com.github.benmanes.caffeine.cache.Cache accessTokenBlacklistCache() { + return Caffeine.newBuilder() + .expireAfterWrite(24, TimeUnit.HOURS) + .maximumSize(10000) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/recipe/app/src/common/config/RedisConfig.java b/src/main/java/com/recipe/app/src/common/config/RedisConfig.java deleted file mode 100644 index 3d6f5ad0..00000000 --- a/src/main/java/com/recipe/app/src/common/config/RedisConfig.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.recipe.app.src.common.config; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; - -@Configuration -public class RedisConfig { - - @Value("${spring.data.redis.host}") - private String host; - - @Value("${spring.data.redis.port}") - private int port; - - @Bean - public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(host, port); - } - - @Bean - public RedisTemplate redisTemplate() { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(redisConnectionFactory()); - return redisTemplate; - } -} diff --git a/src/main/java/com/recipe/app/src/common/config/WebSecurityConfig.java b/src/main/java/com/recipe/app/src/common/config/WebSecurityConfig.java index 21b3ff1b..1cf129f9 100644 --- a/src/main/java/com/recipe/app/src/common/config/WebSecurityConfig.java +++ b/src/main/java/com/recipe/app/src/common/config/WebSecurityConfig.java @@ -50,6 +50,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/fridges/**").authenticated() .requestMatchers("/fridges/basket/**").authenticated() .requestMatchers("/ingredients/**").authenticated() + .requestMatchers("/recipes/public/**").permitAll() .requestMatchers("/recipes/**").authenticated() .anyRequest().permitAll()) .addFilterBefore(new JwtFilter(jwtUtil, userDetailsService), UsernamePasswordAuthenticationFilter.class) diff --git a/src/main/java/com/recipe/app/src/common/utils/JwtUtil.java b/src/main/java/com/recipe/app/src/common/utils/JwtUtil.java index 99a4ed02..552c11e3 100644 --- a/src/main/java/com/recipe/app/src/common/utils/JwtUtil.java +++ b/src/main/java/com/recipe/app/src/common/utils/JwtUtil.java @@ -1,16 +1,21 @@ package com.recipe.app.src.common.utils; +import com.github.benmanes.caffeine.cache.Cache; import com.recipe.app.src.common.client.apple.dto.ApplePublicKeyResponse; -import io.jsonwebtoken.*; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.security.SignatureException; import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; import javax.crypto.spec.SecretKeySpec; import java.math.BigInteger; @@ -18,17 +23,15 @@ import java.security.KeyFactory; import java.security.PublicKey; import java.security.spec.RSAPublicKeySpec; -import java.time.Duration; import java.util.Base64; import java.util.Date; @Service public class JwtUtil { - private final RedisTemplate redisTemplate; + private final Cache accessTokenBlacklistCache; private final Logger logger = LoggerFactory.getLogger(JwtUtil.class); private final static String TOKEN_KEY = "userId"; - private final static String REFRESH_TOKEN_KEY_PREFIX = "refresh_token_user_id_"; private final static String ACCESS_TOKEN_BLACKLIST_VALUE = "access_token_blacklist"; private final static String TOKEN_HEADER = "Authorization"; @Value("${jwt.secret}") @@ -38,8 +41,8 @@ public class JwtUtil { @Value("${jwt.refresh-token-validity-in-ms}") private long refreshTokenValidMillisecond; - public JwtUtil(RedisTemplate redisTemplate) { - this.redisTemplate = redisTemplate; + public JwtUtil(@Qualifier("accessTokenBlacklistCache") Cache accessTokenBlacklistCache) { + this.accessTokenBlacklistCache = accessTokenBlacklistCache; } public String createAccessToken(Long userId) { @@ -60,16 +63,12 @@ public String createRefreshToken(Long userId) { Date now = new Date(); Key key = new SecretKeySpec(Base64.getDecoder().decode(this.secretKey), SignatureAlgorithm.HS256.getJcaName()); - String token = Jwts.builder() + return Jwts.builder() .claim(TOKEN_KEY, userId) .setIssuedAt(now) .setExpiration(new Date(now.getTime() + refreshTokenValidMillisecond)) .signWith(key) .compact(); - - redisTemplate.opsForValue().set(REFRESH_TOKEN_KEY_PREFIX + userId, token, Duration.ofMillis(refreshTokenValidMillisecond)); - - return token; } public String resolveAccessToken(HttpServletRequest request) { @@ -89,10 +88,9 @@ public long getUserId(String token) { .get(TOKEN_KEY, Long.class); } - @Transactional(readOnly = true) public boolean isValidAccessToken(String accessToken) { - if (StringUtils.hasText(redisTemplate.opsForValue().get(accessToken))) { + if (accessTokenBlacklistCache.getIfPresent(accessToken) != null) { return false; } @@ -101,15 +99,7 @@ public boolean isValidAccessToken(String accessToken) { public boolean isValidRefreshToken(String refreshToken) { - if (isValidToken(refreshToken)) { - - long userId = getUserId(refreshToken); - String foundRefreshToken = redisTemplate.opsForValue().get(REFRESH_TOKEN_KEY_PREFIX + userId); - - return refreshToken.equals(foundRefreshToken); - } - - return false; + return isValidToken(refreshToken); } private boolean isValidToken(String token) { @@ -129,16 +119,9 @@ private boolean isValidToken(String token) { return false; } - @Transactional - public void removeRefreshToken(Long userId) { - - redisTemplate.delete(REFRESH_TOKEN_KEY_PREFIX + userId); - } - - @Transactional public void setAccessTokenBlacklist(String accessToken) { - redisTemplate.opsForValue().set(accessToken, ACCESS_TOKEN_BLACKLIST_VALUE, Duration.ofMillis(accessTokenValidMillisecond)); + accessTokenBlacklistCache.put(accessToken, ACCESS_TOKEN_BLACKLIST_VALUE); } public Claims parseAppleIdToken(String idToken, ApplePublicKeyResponse publicKey) { diff --git a/src/main/java/com/recipe/app/src/file/S3FileService.java b/src/main/java/com/recipe/app/src/file/S3FileService.java index 316c4e4c..fa96c780 100644 --- a/src/main/java/com/recipe/app/src/file/S3FileService.java +++ b/src/main/java/com/recipe/app/src/file/S3FileService.java @@ -25,7 +25,7 @@ public S3FileService(AmazonS3 s3Client) { public String uploadFile(MultipartFile file) throws IOException { - String fileName = UUID.randomUUID().toString() + "-" + file.getOriginalFilename(); + String fileName = UUID.randomUUID() + "-" + file.getOriginalFilename(); ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentType(file.getContentType()); objectMetadata.setContentLength(file.getSize()); diff --git a/src/main/java/com/recipe/app/src/recipe/api/PublicRecipeController.java b/src/main/java/com/recipe/app/src/recipe/api/PublicRecipeController.java new file mode 100644 index 00000000..4326979a --- /dev/null +++ b/src/main/java/com/recipe/app/src/recipe/api/PublicRecipeController.java @@ -0,0 +1,52 @@ +package com.recipe.app.src.recipe.api; + +import com.recipe.app.src.recipe.application.RecipeSearchService; +import com.recipe.app.src.recipe.application.dto.RecipeDetailResponse; +import com.recipe.app.src.recipe.application.dto.RecommendedRecipesResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Arrays; +import java.util.List; + +@Tag(name = "공개 레시피 Controller") +@RestController +@RequestMapping("/recipes/public") +public class PublicRecipeController { + + private final RecipeSearchService recipeSearchService; + + public PublicRecipeController(RecipeSearchService recipeSearchService) { + this.recipeSearchService = recipeSearchService; + } + + @Operation(summary = "공개 레시피 상세 조회 API (로그인 불필요)") + @GetMapping("/{recipeId}") + public RecipeDetailResponse getPublicRecipe(@PathVariable long recipeId) { + + return recipeSearchService.findPublicRecipeDetail(recipeId); + } + + @Operation(summary = "공개 추천 레시피 목록 조회 API (로그인 불필요)") + @GetMapping("/recommendation") + public RecommendedRecipesResponse getPublicRecipesByIngredients( + @Parameter(example = "감자,당근,양파", name = "재료 목록 (쉼표로 구분)") + @RequestParam(value = "ingredients") String ingredients, + @Parameter(example = "0", name = "마지막 조회 레시피 아이디") + @RequestParam(value = "startAfter", defaultValue = "0") long startAfter, + @Parameter(example = "20", name = "사이즈") + @RequestParam(value = "size", defaultValue = "20") int size) { + + List ingredientNames = Arrays.stream(ingredients.split(",")) + .map(String::trim) + .toList(); + + return recipeSearchService.findPublicRecommendedRecipesByIngredients(ingredientNames, startAfter, size); + } +} \ No newline at end of file diff --git a/src/main/java/com/recipe/app/src/recipe/application/RecipeSearchService.java b/src/main/java/com/recipe/app/src/recipe/application/RecipeSearchService.java index 0e388e16..29315ae6 100644 --- a/src/main/java/com/recipe/app/src/recipe/application/RecipeSearchService.java +++ b/src/main/java/com/recipe/app/src/recipe/application/RecipeSearchService.java @@ -149,4 +149,38 @@ public RecommendedRecipesResponse findRecommendedRecipesByUserFridge(User user, return RecommendedRecipesResponse.from(recipes, recipePostUsers, recipeScraps, user, ingredientNamesInFridge, lastRecipe, size); } + + @Transactional(readOnly = true) + public RecipeDetailResponse findPublicRecipeDetail(long recipeId) { + + Recipe recipe = recipeRepository.findById(recipeId) + .orElseThrow(() -> { + throw new NotFoundRecipeException(); + }); + + User postUser = null; + if (recipe.getUserId() != null) { + postUser = userService.findByUserId(recipe.getUserId()); + } + + List emptyIngredientList = List.of(); + + return RecipeDetailResponse.from(recipe, false, postUser, emptyIngredientList); + } + + @Transactional(readOnly = true) + public RecommendedRecipesResponse findPublicRecommendedRecipesByIngredients(List ingredientNames, long lastRecipeId, int size) { + + Recipes recipes = new Recipes(recipeRepository.findRecipesInFridge(ingredientNames)); + + List recipePostUsers = userService.findByUserIds(recipes.getUserIds()); + + List recipeScraps = recipeScrapService.findByRecipeIds(recipes.getRecipeIds()); + + Recipe lastRecipe = recipeRepository.findById(lastRecipeId).orElse(null); + + User anonymousUser = new User(); + + return RecommendedRecipesResponse.from(recipes, recipePostUsers, recipeScraps, anonymousUser, ingredientNames, lastRecipe, size); + } } diff --git a/src/main/java/com/recipe/app/src/recipe/domain/Recipe.java b/src/main/java/com/recipe/app/src/recipe/domain/Recipe.java index 743f631e..e5252918 100644 --- a/src/main/java/com/recipe/app/src/recipe/domain/Recipe.java +++ b/src/main/java/com/recipe/app/src/recipe/domain/Recipe.java @@ -70,10 +70,10 @@ public class Recipe extends BaseEntity { @Column(name = "reportYn", nullable = false) private String reportYn = "N"; - @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL) + @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) List ingredients = new ArrayList<>(); - @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL) + @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) List processes = new ArrayList<>(); @Builder @@ -106,25 +106,24 @@ public void updateRecipe(Recipe updateRecipe) { this.imgUrl = updateRecipe.imgUrl; this.hiddenYn = updateRecipe.hiddenYn; + // 재료 업데이트 - orphanRemoval로 인해 제거된 항목은 자동 삭제됨 ingredients.clear(); - - if (updateRecipe.ingredients != null && !updateRecipe.ingredients.isEmpty()) { + if (updateRecipe.ingredients != null) { updateRecipe.ingredients.forEach(ing -> { - ing.setRecipe(this); // 부모 Recipe 변경 + ing.setRecipe(this); ingredients.add(ing); }); - updateRecipe.ingredients.clear(); // updateRecipe에서는 제거 + updateRecipe.ingredients.clear(); } + // 요리 과정 업데이트 - orphanRemoval로 인해 제거된 항목은 자동 삭제됨 processes.clear(); - - // 새 요리 과정 추가 - if (updateRecipe.processes != null && !updateRecipe.processes.isEmpty()) { + if (updateRecipe.processes != null) { updateRecipe.processes.forEach(proc -> { - proc.setRecipe(this); // 부모 Recipe 변경 + proc.setRecipe(this); processes.add(proc); }); - updateRecipe.processes.clear(); // updateRecipe에서는 제거 + updateRecipe.processes.clear(); } } diff --git a/src/main/java/com/recipe/app/src/user/application/UserService.java b/src/main/java/com/recipe/app/src/user/application/UserService.java index 264cffcc..f490b3da 100644 --- a/src/main/java/com/recipe/app/src/user/application/UserService.java +++ b/src/main/java/com/recipe/app/src/user/application/UserService.java @@ -162,7 +162,6 @@ public void logout(HttpServletRequest request) { String accessToken = jwtUtil.resolveAccessToken(request); jwtUtil.setAccessTokenBlacklist(accessToken); - jwtUtil.removeRefreshToken(jwtUtil.getUserId(accessToken)); } @Transactional(readOnly = true) diff --git a/src/main/java/com/recipe/app/src/user/domain/Adjective.java b/src/main/java/com/recipe/app/src/user/domain/Adjective.java new file mode 100644 index 00000000..b4cc16e0 --- /dev/null +++ b/src/main/java/com/recipe/app/src/user/domain/Adjective.java @@ -0,0 +1,72 @@ +package com.recipe.app.src.user.domain; + +import java.util.Random; + +public enum Adjective { + FRESH("신선한"), + SPICY("매운"), + SWEET("달콤한"), + SAVORY("고소한"), + SOFT("부드러운"), + CRISPY("바삭한"), + DEEP("진한"), + LIGHT("가벼운"), + SALTY("짭짤한"), + SOUR("상큼한"), + WARM("따뜻한"), + COOL("차가운"), + FRAGRANT("향기로운"), + MILD("순한"), + RICH("풍부한"), + TENDER("연한"), + JUICY("육즙많은"), + CHEWY("쫄깃한"), + FLAVORFUL("맛있는"), + AROMATIC("향긋한"), + THICK("농후한"), + THIN("묽은"), + HOT("뜨거운"), + COLD("차가운"), + TANGY("톡쏘는"), + BITTER("쓴"), + UMAMI("감칠맛나는"), + PICKLED("절인"), + SMOKED("훈제된"), + TOASTED("구운"), + ROASTED("로스트한"), + STEAMED("쪄낸"), + BRAISED("조린"), + GRILLED("그릴구이"), + FRIED("튀긴"), + BAKED("구워진"), + SIMMERED("졸인"), + CHILLED("얼린"), + CARAMELIZED("카라멜화된"), + CREAMY("크리미한"), + CRISP("아삭한"), + BUTTERY("버터향의"), + ZESTY("톡톡한"), + NUTTY("견과향의"), + SILKY("비단같은"), + CRUNCHY("바삭바삭한"), + FLUFFY("폭신한"), + MOIST("촉촉한"), + SMOKY("연기향의"), + TENDER_MEAT("말랑한"); + + private final String koreanName; + private static final Random RANDOM = new Random(); + + Adjective(String koreanName) { + this.koreanName = koreanName; + } + + public String getKoreanName() { + return koreanName; + } + + public static Adjective getRandomAdjective() { + Adjective[] adjectives = Adjective.values(); + return adjectives[RANDOM.nextInt(adjectives.length)]; + } +} diff --git a/src/main/java/com/recipe/app/src/user/domain/Ingredient.java b/src/main/java/com/recipe/app/src/user/domain/Ingredient.java new file mode 100644 index 00000000..a55845d5 --- /dev/null +++ b/src/main/java/com/recipe/app/src/user/domain/Ingredient.java @@ -0,0 +1,72 @@ +package com.recipe.app.src.user.domain; + +import java.util.Random; + +public enum Ingredient { + POTATO("감자"), + CARROT("당근"), + ONION("양파"), + SWEET_POTATO("고구마"), + BROCCOLI("브로콜리"), + BELL_PEPPER("피망"), + TOMATO("토마토"), + CHEESE("치즈"), + EGG("계란"), + RICE("밥"), + MILK("우유"), + BUTTER("버터"), + SALT("소금"), + SUGAR("설탕"), + GARLIC("마늘"), + GINGER("생강"), + SCALLION("파"), + LETTUCE("상추"), + CUCUMBER("오이"), + CHILI("고추"), + SOYBEAN("콩"), + BEEF("소고기"), + PORK("돼지고기"), + CHICKEN("닭고기"), + FISH("생선"), + SHRIMP("새우"), + SQUID("오징어"), + CLAM("조개"), + MUSHROOM("버섯"), + SPINACH("시금치"), + CABBAGE("양배추"), + CELERY("셀러리"), + LEEK("대파"), + RADISH("무"), + BEET("비트"), + CORN("옥수수"), + PEA("완두콩"), + ASPARAGUS("아스파라거스"), + ZUCCHINI("주키니"), + EGGPLANT("가지"), + AVOCADO("아보카도"), + LIME("라임"), + LEMON("레몬"), + ORANGE("오렌지"), + APPLE("사과"), + BANANA("바나나"), + STRAWBERRY("딸기"), + BLUEBERRY("블루베리"), + MANGO("망고"), + PINEAPPLE("파인애플"); + + private final String koreanName; + private static final Random RANDOM = new Random(); + + Ingredient(String koreanName) { + this.koreanName = koreanName; + } + + public String getKoreanName() { + return koreanName; + } + + public static Ingredient getRandomIngredient() { + Ingredient[] ingredients = Ingredient.values(); + return ingredients[RANDOM.nextInt(ingredients.length)]; + } +} diff --git a/src/main/java/com/recipe/app/src/user/domain/NicknameGenerator.java b/src/main/java/com/recipe/app/src/user/domain/NicknameGenerator.java new file mode 100644 index 00000000..3bf71178 --- /dev/null +++ b/src/main/java/com/recipe/app/src/user/domain/NicknameGenerator.java @@ -0,0 +1,20 @@ +package com.recipe.app.src.user.domain; + +public class NicknameGenerator { + + private NicknameGenerator() { + // Utility class + } + + /** + * 형용사 + 재료명 + 나노초 기반 숫자로 닉네임을 생성합니다. + * 예시: 신선한감자35980622 + */ + public static String generate() { + Adjective adjective = Adjective.getRandomAdjective(); + Ingredient ingredient = Ingredient.getRandomIngredient(); + String uniqueId = String.valueOf(System.nanoTime()).substring(5, 13); + + return adjective.getKoreanName() + ingredient.getKoreanName() + uniqueId; + } +} diff --git a/src/test/groovy/com/recipe/app/src/user/application/UserServiceTest.groovy b/src/test/groovy/com/recipe/app/src/user/application/UserServiceTest.groovy index 86e360cd..1161c0fe 100644 --- a/src/test/groovy/com/recipe/app/src/user/application/UserServiceTest.groovy +++ b/src/test/groovy/com/recipe/app/src/user/application/UserServiceTest.groovy @@ -296,8 +296,6 @@ class UserServiceTest extends Specification { } 1 * jwtUtil.resolveAccessToken(request) 1 * jwtUtil.setAccessTokenBlacklist(_) - 1 * jwtUtil.getUserId(_) - 1 * jwtUtil.removeRefreshToken(_) 0 * userWithdrawalService.saveWithdrawalReason(_, _) } @@ -330,8 +328,6 @@ class UserServiceTest extends Specification { } 1 * jwtUtil.resolveAccessToken(request) 1 * jwtUtil.setAccessTokenBlacklist(_) - 1 * jwtUtil.getUserId(_) - 1 * jwtUtil.removeRefreshToken(_) 1 * userWithdrawalService.saveWithdrawalReason(user.userId, withdrawRequest.withdrawalReason) } @@ -357,8 +353,6 @@ class UserServiceTest extends Specification { 1 * userRepository.save(user) 1 * jwtUtil.resolveAccessToken(request) 1 * jwtUtil.setAccessTokenBlacklist(_) - 1 * jwtUtil.getUserId(_) - 1 * jwtUtil.removeRefreshToken(_) 0 * userWithdrawalService.saveWithdrawalReason(_, _) } @@ -421,8 +415,6 @@ class UserServiceTest extends Specification { then: 1 * jwtUtil.resolveAccessToken(request) 1 * jwtUtil.setAccessTokenBlacklist(_) - 1 * jwtUtil.getUserId(_) - 1 * jwtUtil.removeRefreshToken(_) } def "토큰 재발급"() {