diff --git a/backend/src/main/java/org/example/image/redis/ColorApiClient.java b/backend/src/main/java/org/example/image/redis/ColorApiClient.java index 949b8d14..4189c807 100644 --- a/backend/src/main/java/org/example/image/redis/ColorApiClient.java +++ b/backend/src/main/java/org/example/image/redis/ColorApiClient.java @@ -18,7 +18,6 @@ public class ColorApiClient { @LogExecution public String getColorInfo(String hexColor) { RestTemplate restTemplate = new RestTemplate(); - ObjectMapper objectMapper = new ObjectMapper(); try { // Exchange method for direct object mapping ResponseEntity response = restTemplate.exchange( diff --git a/backend/src/main/java/org/example/image/redis/service/ImageRedisService.java b/backend/src/main/java/org/example/image/redis/service/ImageRedisService.java index c315aa59..fbc5b801 100644 --- a/backend/src/main/java/org/example/image/redis/service/ImageRedisService.java +++ b/backend/src/main/java/org/example/image/redis/service/ImageRedisService.java @@ -3,15 +3,16 @@ import static org.example.image.ImageAnalyzeManager.analyzer.tools.ColorConverter.*; import static org.example.image.redis.tools.RgbColorSimilarity.*; -import java.io.IOException; import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import org.example.config.log.LogExecution; import org.example.image.ImageAnalyzeManager.ImageAnalyzeManager; @@ -29,74 +30,42 @@ import org.springframework.data.redis.core.ZSetOperations; import org.springframework.stereotype.Service; -import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; -; - -/** - * The type Image redis service. - */ @Service @RequiredArgsConstructor public class ImageRedisService { + private static final String HASH_KEY = "ColorHash"; + private static final String ZSET_KEY = "ColorZSet"; + private static final String COLOR_DIST_CAL_ZSET_KEY = "ColorDistCalZSet"; + private static final int STANDARD_DIST = 10; + private static final int WEIGHT_LDT = 1; + private static final int WEIGHT_LIKE = 1; + private static final int WEIGHT_VIEW = 1; private final RedisTemplate redisTemplate; private final PostRepository postRepository; private final ImageAnalyzeManager imageAnalyzeManager; + private final ColorApiClient colorApiClient; + private final ObjectMapper objectMapper; - protected static final String HASH_KEY = "ColorHash"; - protected static final String ZSET_KEY = "ColorZSet"; - private static final String COLOR_DIST_CAL_ZSET_KEY = "ColorDistCalZSet"; - private static final Integer STANDARD_DIST = 10; - private static final Integer WEIGHT_LDT = 1; - private static final Integer WEIGHT_LIKE = 1; - private static final Integer WEIGHT_VIEW = 1; + // Methods called from external methods @LogExecution - public ListsaveNewColor(Long imageLocationId) throws JsonProcessingException { + public List saveNewColor(Long imageLocationId) throws JsonProcessingException { List colorNameList = new ArrayList<>(); - ColorApiClient client = new ColorApiClient(); - HashOperations hashOps = redisTemplate.opsForHash(); - ZSetOperations zSetOps = redisTemplate.opsForZSet(); - ObjectMapper objectMapper = new ObjectMapper(); - ImageAnalyzeData imageAnalyzeData = imageAnalyzeManager.getAnalyzedData(imageLocationId); - for (ClothAnalyzeData clothAnalyzeData : imageAnalyzeData.clothAnalyzeDataList()) { // Clothes from an image - int[] colorRGB = { - clothAnalyzeData.rgbColor().getRed(), - clothAnalyzeData.rgbColor().getGreen(), - clothAnalyzeData.rgbColor().getBlue() - }; - if (calcCloseColorsDist(colorRGB, 1) == null - || calcCloseColorsDist(colorRGB, 1).get(0).distance() > STANDARD_DIST) { - - String hexColor = RGBtoHEX(colorRGB[0], colorRGB[1], colorRGB[2]); - - // thecolorapi API for get color name as string - String colorName = client.getColorInfo(hexColor); - - // save new color - if (hashOps.get(HASH_KEY, colorName) == null) { + for (ClothAnalyzeData clothAnalyzeData : imageAnalyzeData.clothAnalyzeDataList()) { + int[] colorRGB = extractRGB(clothAnalyzeData); + if (isNewColor(colorRGB)) { + String colorName = getColorName(colorRGB); + if (redisTemplate.opsForHash().get(HASH_KEY, colorName) == null) { colorNameList.add(colorName); - - ColorDto.ColorSaveDtoRequest colorSaveDtoRequest - = new ColorDto.ColorSaveDtoRequest( - colorRGB[0], colorRGB[1], colorRGB[2], - colorRGB[0], colorRGB[1], colorRGB[2] - ); - - // Serialization - String colorSaveDtoJson = objectMapper.writeValueAsString(colorSaveDtoRequest); - - hashOps.put(HASH_KEY, colorName, colorSaveDtoJson); - - // save only color name to Redis by using ZSet - zSetOps.add(ZSET_KEY, colorName, 0); + saveColor(colorName, colorRGB); } } } @@ -106,270 +75,225 @@ public class ImageRedisService { @LogExecution public List updateExistingColor(PostPopularSearchCondition postPopularSearchCondition) - throws JsonProcessingException, RuntimeException { + throws JsonProcessingException { List savedColorNameList = new ArrayList<>(); - HashOperations hashOps = redisTemplate.opsForHash(); - ObjectMapper objectMapper = new ObjectMapper(); - - List postEntities = postRepository.findAllByPostStatusAndCreatedAtAfterAndLikeCountGreaterThanEqualAndHitsGreaterThanEqual( - PostStatus.PUBLISHED, - postPopularSearchCondition.getCreatedAt(), - postPopularSearchCondition.getLikeCount(), - postPopularSearchCondition.getViewCount() - ); - - // get popular color of popular ClothAnalyzeData's image analyze data to change RGB - HashMap selectedColorHashMap = new HashMap<>(); - for (PostEntity post : postEntities) { - ImageAnalyzeData imageAnalyzeData = imageAnalyzeManager.getAnalyzedData(post.getImageLocationId()); - - // Selection Colors for updating because we can get several requests about same color - for (ClothAnalyzeData clothAnalyzeData : imageAnalyzeData.clothAnalyzeDataList()) { - int[] colorRGB = { - clothAnalyzeData.rgbColor().getRed(), - clothAnalyzeData.rgbColor().getGreen(), - clothAnalyzeData.rgbColor().getBlue() - }; - - ColorDto.ColorDistanceResponse colorDistanceResponse = calcCloseColorsDist(colorRGB, 1).get(0); - String name = colorDistanceResponse.name(); - - PostPopularSearchCondition condition = new PostPopularSearchCondition(); - condition.setCreatedAt(post.getCreatedAt()); - condition.setLikeCount(post.getLikeCount()); - condition.setViewCount(post.getHits()); - - ColorDto.ColorSelectedDtoRequest colorSelectedDtoRequest = new ColorDto.ColorSelectedDtoRequest( - post.getPostId(), - colorDistanceResponse.distance(), - colorRGB[0], colorRGB[1], colorRGB[2], - condition - ); - // Get the existing color request for the given name - ColorDto.ColorSelectedDtoRequest existingColorRequest = selectedColorHashMap.get(name); - - // Check if the existingColorRequest is not null before accessing its condition - if (existingColorRequest != null) { - // Compare scores only if existingColorRequest is not null - if (calcScoreOfPost(existingColorRequest.condition()) < calcScoreOfPost( - postPopularSearchCondition)) { - selectedColorHashMap.put(name, colorSelectedDtoRequest); - } - } else { - // If no existing request found, simply put the new colorSelectedDtoRequest - selectedColorHashMap.put(name, colorSelectedDtoRequest); - } - } - } - - // Followed logic can be changed to get color more than 1 and compare all dist for colorRGB - // and update color like make 3 color as 1 color - // If you do that, must update return type as List - - // Update selected Colors to ColorHash - for (String name : selectedColorHashMap.keySet()) { - // color for update - ColorDto.ColorSelectedDtoRequest colorSelectedDtoRequest = selectedColorHashMap.get(name); - int[] colorRGB = new int[] { - colorSelectedDtoRequest.r(), - colorSelectedDtoRequest.g(), - colorSelectedDtoRequest.b() - }; - - // Check whether new color is enough close to originRGB - Object storedColor = hashOps.get(HASH_KEY, name); - - // JSON Parsing - JsonNode rootNode = objectMapper.readTree(storedColor.toString()); - - int[] storedColorRGB = new int[] { - rootNode.get("r").asInt(), - rootNode.get("g").asInt(), - rootNode.get("b").asInt() - }; - - // Assuming storedColor is a JSON string, not a ColorSaveDtoRequest object - String colorJson = (String)storedColor; - - // Convert JSON string to ColorSaveDtoRequest object - ColorDto.ColorSaveDtoRequest storedColorRequest = objectMapper.readValue(colorJson, - ColorDto.ColorSaveDtoRequest.class); - - // Update the ColorSaveDtoRequest object with new RGB values - ColorDto.ColorSaveDtoRequest updatedColorRequest = ColorDto.ColorSaveDtoRequest.update( - storedColorRequest, colorRGB[0], colorRGB[1], colorRGB[2] - ); - - // Serialize the updated ColorSaveDtoRequest object to JSON - String colorSaveDtoJson = objectMapper.writeValueAsString(updatedColorRequest); - - if (calculateEuclideanDistance(colorRGB, storedColorRGB) <= STANDARD_DIST) { - savedColorNameList.add(name); - hashOps.put(HASH_KEY, name, colorSaveDtoJson); + List postEntities = getPopularPosts(postPopularSearchCondition); + Map selectedColorHashMap + = selectColorsForUpdate(postEntities); + + for (Map.Entry entry : selectedColorHashMap.entrySet()) { + String colorName = entry.getKey(); + ColorDto.ColorSelectedDtoRequest colorSelectedDto = entry.getValue(); + if (updateColorIfCloseEnough(colorName, colorSelectedDto)) { + savedColorNameList.add(colorName); } - } return savedColorNameList; } @LogExecution - public void updateZSetColorScore( - Long imageLocationId, UpdateScoreType updateScoreType - ) throws JsonProcessingException { + public void updateZSetColorScore(Long imageLocationId, UpdateScoreType updateScoreType) + throws JsonProcessingException { ZSetOperations zSetOps = redisTemplate.opsForZSet(); - ImageAnalyzeData imageAnalyzeData = imageAnalyzeManager.getAnalyzedData(imageLocationId); + for (ClothAnalyzeData clothAnalyzeData : imageAnalyzeData.clothAnalyzeDataList()) { - int[] rgbColor = { - clothAnalyzeData.rgbColor().getRed(), - clothAnalyzeData.rgbColor().getBlue(), - clothAnalyzeData.rgbColor().getGreen() - }; + int[] rgbColor = extractRGB(clothAnalyzeData); String colorName = calcCloseColorsDist(rgbColor, 1).get(0).name(); - Double currentScore = zSetOps.score(ZSET_KEY, colorName); if (currentScore == null) { currentScore = 0.0; } currentScore += updateScoreType.getValue(); - zSetOps.add(ZSET_KEY, colorName, currentScore); } } public List getPopularColorList() { - HashOperations hashOps = redisTemplate.opsForHash(); + HashOperations hashOps = redisTemplate.opsForHash(); ZSetOperations zSetOps = redisTemplate.opsForZSet(); - ObjectMapper objectMapper = new ObjectMapper(); - // Return most popular 10 color as list of name and rgb Set> typedTuples = zSetOps.reverseRangeWithScores(ZSET_KEY, 0, 9); - - assert typedTuples != null; + if (typedTuples == null) + return Collections.emptyList(); return typedTuples.stream() - .map( - tuple -> { - String colorName = (String)tuple.getValue(); - JsonNode rootNode; - try { - rootNode = objectMapper.readTree(hashOps.get(HASH_KEY, colorName).toString()); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - + .map(tuple -> { + String colorName = (String)tuple.getValue(); + try { + JsonNode rootNode = objectMapper.readTree(hashOps.get(HASH_KEY, colorName)); int[] rgb = {rootNode.get("r").asInt(), rootNode.get("g").asInt(), rootNode.get("b").asInt()}; - return new ColorDto.ColorPopularResponse( - colorName, - rgb[0], rgb[1], rgb[2] - ); + return new ColorDto.ColorPopularResponse(colorName, rgb[0], rgb[1], rgb[2]); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()); + } + + @LogExecution + public List getCloseColorList(int[] rgb, int num) throws JsonProcessingException { + HashOperations hashOps = redisTemplate.opsForHash(); + + List closestColorNames = calcCloseColorsDist(rgb, num).stream() + .map(ColorDto.ColorDistanceResponse::name) + .toList(); + + return closestColorNames.stream() + .map(color -> { + try { + JsonNode rootNode + = objectMapper.readTree(Objects.requireNonNull(hashOps.get(HASH_KEY, color)).toString()); + return new int[] { + rootNode.get("r").asInt(), rootNode.get("g").asInt(), rootNode.get("b").asInt() + }; + } catch (JsonProcessingException e) { + throw new RuntimeException(e); } - ).toList(); + }) + .collect(Collectors.toList()); } - // Private Internal Method ------------------------------------------------------------------- + // Only Internally Used Methods + @LogExecution - protected List calcCloseColorsDist( - int[] rgb, int num - ) throws JsonProcessingException { + protected List calcCloseColorsDist(int[] rgb, int num) + throws JsonProcessingException { HashOperations hashOps = redisTemplate.opsForHash(); - Map colorMembers = hashOps.entries(HASH_KEY); ZSetOperations closestColorZSet = redisTemplate.opsForZSet(); - ObjectMapper objectMapper = new ObjectMapper(); + redisTemplate.delete(COLOR_DIST_CAL_ZSET_KEY); + Map colorMembers = hashOps.entries(HASH_KEY); for (Map.Entry entry : colorMembers.entrySet()) { - String colorJson = (String)entry.getValue(); - String colorName = (String)entry.getKey(); - - ColorDto.ColorUpdateDtoRequest originalRequest = objectMapper.readValue(colorJson, - ColorDto.ColorUpdateDtoRequest.class); + String colorJson = entry.getValue().toString(); + String colorName = entry.getKey().toString(); + ColorDto.ColorUpdateDtoRequest originalRequest + = objectMapper.readValue(colorJson, ColorDto.ColorUpdateDtoRequest.class); ColorDto.ColorUpdateDtoRequest colorUpdateDtoRequest = new ColorDto.ColorUpdateDtoRequest( - colorName, - originalRequest.r(), - originalRequest.g(), - originalRequest.b(), - originalRequest.originR(), - originalRequest.originG(), - originalRequest.originB() + colorName, originalRequest.r(), originalRequest.g(), originalRequest.b(), + originalRequest.originR(), originalRequest.originG(), originalRequest.originB() ); String colorUpdateJson = objectMapper.writeValueAsString(colorUpdateDtoRequest); - double dist = calculateEuclideanDistance( rgb, new int[] {colorUpdateDtoRequest.r(), colorUpdateDtoRequest.g(), colorUpdateDtoRequest.b()} ); - closestColorZSet.add(COLOR_DIST_CAL_ZSET_KEY, colorUpdateJson, dist); } Set> typedTuples = closestColorZSet.rangeWithScores(COLOR_DIST_CAL_ZSET_KEY, 0, num - 1); - - if (typedTuples == null || typedTuples.isEmpty()) { // TODO: 수정 필요 + if (typedTuples == null || typedTuples.isEmpty()) { return null; } return typedTuples.stream() .map(tuple -> { - String colorJson = (String)tuple.getValue(); // JSON 문자열 (ZSet value) - double distance = tuple.getScore(); // 거리 (ZSet score) - ColorDto.ColorUpdateDtoRequest colorRequest = null; + String colorJson = (String)tuple.getValue(); + double distance = tuple.getScore(); try { - colorRequest = objectMapper.readValue(colorJson, ColorDto.ColorUpdateDtoRequest.class); + ColorDto.ColorUpdateDtoRequest colorRequest + = objectMapper.readValue(colorJson, ColorDto.ColorUpdateDtoRequest.class); + return new ColorDto.ColorDistanceResponse(colorRequest.name(), distance); } catch (JsonProcessingException e) { throw new RuntimeException(e); } - return new ColorDto.ColorDistanceResponse(colorRequest.name(), distance); }) - .toList(); + .collect(Collectors.toList()); } - @LogExecution - public List getCloseColorList(int[] rgb, int num) + private Map selectColorsForUpdate(List postEntities) throws JsonProcessingException { - HashOperations hashOps = redisTemplate.opsForHash(); - ObjectMapper objectMapper = new ObjectMapper(); + Map selectedColorHashMap = new HashMap<>(); - List closetsColorName = calcCloseColorsDist(rgb, num) - .stream() - .map(ColorDto.ColorDistanceResponse::name) - .toList(); + for (PostEntity post : postEntities) { + ImageAnalyzeData imageAnalyzeData = imageAnalyzeManager.getAnalyzedData(post.getImageLocationId()); + for (ClothAnalyzeData clothAnalyzeData : imageAnalyzeData.clothAnalyzeDataList()) { + int[] colorRGB = extractRGB(clothAnalyzeData); + ColorDto.ColorDistanceResponse colorDistanceResponse = calcCloseColorsDist(colorRGB, 1).get(0); + String name = colorDistanceResponse.name(); - List rgbList = new ArrayList<>(); - for (String color : closetsColorName) { - // Check whether new color is enough close to originRGB - Object storedColor = hashOps.get(HASH_KEY, color); - // JSON Parsing - JsonNode rootNode = objectMapper.readTree(storedColor.toString()); - - rgbList.add(new int[] { - rootNode.get("r").asInt(), - rootNode.get("g").asInt(), - rootNode.get("b").asInt() - }); + PostPopularSearchCondition condition = new PostPopularSearchCondition(post); + ColorDto.ColorSelectedDtoRequest colorSelectedDto = new ColorDto.ColorSelectedDtoRequest( + post.getPostId(), colorDistanceResponse.distance(), colorRGB[0], colorRGB[1], colorRGB[2], condition + ); + + selectedColorHashMap.compute + (name, (k, v) -> + (v == null || calcScoreOfPost(condition) > calcScoreOfPost(v.condition())) + ? colorSelectedDto : v + ); + } } - return rgbList; + return selectedColorHashMap; } - @LogExecution - protected double calcScoreOfPost(PostPopularSearchCondition condition) { - double score = 0.0; + private boolean updateColorIfCloseEnough(String colorName, ColorDto.ColorSelectedDtoRequest colorSelectedDto) + throws JsonProcessingException { + HashOperations hashOps = redisTemplate.opsForHash(); + String storedColorJson = hashOps.get(HASH_KEY, colorName); + if (storedColorJson == null) + return false; + + ColorDto.ColorSaveDtoRequest storedColor = objectMapper.readValue(storedColorJson, + ColorDto.ColorSaveDtoRequest.class); + int[] storedColorRGB = {storedColor.originR(), storedColor.originG(), storedColor.originB()}; + int[] newColorRGB = {colorSelectedDto.r(), colorSelectedDto.g(), colorSelectedDto.b()}; + + if (calculateEuclideanDistance(newColorRGB, storedColorRGB) <= STANDARD_DIST) { + ColorDto.ColorSaveDtoRequest updatedColor + = ColorDto.ColorSaveDtoRequest.update(storedColor, newColorRGB[0], newColorRGB[1], newColorRGB[2]); + hashOps.put(HASH_KEY, colorName, objectMapper.writeValueAsString(updatedColor)); + return true; + } - Duration duration = Duration.between(condition.getCreatedAt(), LocalDateTime.now()); - long durationDays = duration.toDays(); + return false; + } + + private int[] extractRGB(ClothAnalyzeData clothAnalyzeData) { + return new int[] { + clothAnalyzeData.rgbColor().getRed(), + clothAnalyzeData.rgbColor().getGreen(), + clothAnalyzeData.rgbColor().getBlue() + }; + } - score += (Math.max((12.0 - (durationDays / 7.0)), 0)) - * WEIGHT_LDT; // 3 month over post can't get score about duration + private boolean isNewColor(int[] colorRGB) throws JsonProcessingException { + List closeColors = calcCloseColorsDist(colorRGB, 1); + return closeColors == null || closeColors.get(0).distance() > STANDARD_DIST; + } - int likeCount = condition.getLikeCount(); - score += likeCount * WEIGHT_LIKE; + private String getColorName(int[] colorRGB) { + String hexColor = RGBtoHEX(colorRGB[0], colorRGB[1], colorRGB[2]); + return colorApiClient.getColorInfo(hexColor); + } - int viewCount = condition.getViewCount(); - score += viewCount * WEIGHT_VIEW; + private void saveColor(String colorName, int[] colorRGB) throws JsonProcessingException { + ColorDto.ColorSaveDtoRequest colorSaveDto = new ColorDto.ColorSaveDtoRequest( + colorRGB[0], colorRGB[1], colorRGB[2], colorRGB[0], colorRGB[1], colorRGB[2] + ); + String colorSaveDtoJson = objectMapper.writeValueAsString(colorSaveDto); - return score; + redisTemplate.opsForHash().put(HASH_KEY, colorName, colorSaveDtoJson); + redisTemplate.opsForZSet().add(ZSET_KEY, colorName, 0); + } + + private List getPopularPosts(PostPopularSearchCondition condition) { + return postRepository.findAllByPostStatusAndCreatedAtAfterAndLikeCountGreaterThanEqualAndHitsGreaterThanEqual( + PostStatus.PUBLISHED, condition.getCreatedAt(), condition.getLikeCount(), condition.getViewCount() + ); } -} + @LogExecution + protected double calcScoreOfPost(PostPopularSearchCondition condition) { + double score = 0.0; + Duration duration = Duration.between(condition.getCreatedAt(), LocalDateTime.now()); + long durationDays = duration.toDays(); + score += (Math.max((12.0 - (durationDays / 7.0)), 0)) * WEIGHT_LDT; + score += condition.getLikeCount() * WEIGHT_LIKE; + score += condition.getViewCount() * WEIGHT_VIEW; + return score; + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/example/post/repository/custom/PostPopularSearchCondition.java b/backend/src/main/java/org/example/post/repository/custom/PostPopularSearchCondition.java index 70ca50f4..c183c1a6 100644 --- a/backend/src/main/java/org/example/post/repository/custom/PostPopularSearchCondition.java +++ b/backend/src/main/java/org/example/post/repository/custom/PostPopularSearchCondition.java @@ -2,11 +2,21 @@ import java.time.LocalDateTime; +import org.example.post.domain.entity.PostEntity; + import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor public class PostPopularSearchCondition { private LocalDateTime createdAt; private int likeCount; private int viewCount; + + public PostPopularSearchCondition(PostEntity post) { + createdAt = post.getCreatedAt(); + likeCount = post.getLikeCount(); + viewCount = post.getHits(); + } }