From 882d50cebcfb1645c8ee8442a29bd75ca26c1f2e Mon Sep 17 00:00:00 2001 From: youKeon Date: Thu, 26 Oct 2023 22:07:00 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EC=A1=B0=ED=9A=8C=20=EC=88=98=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20Top10=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/controller/ProjectController.java | 12 ++++--- .../response/GetProjectRankingResponse.java | 27 +++++++++++++++ .../project/service/ProjectService.java | 27 +++++++++++++++ .../backend/global/config/RedisConfig.java | 33 +++++++++++++++++++ 4 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 backend/src/main/java/com/graphy/backend/domain/project/dto/response/GetProjectRankingResponse.java diff --git a/backend/src/main/java/com/graphy/backend/domain/project/controller/ProjectController.java b/backend/src/main/java/com/graphy/backend/domain/project/controller/ProjectController.java index 7534afc5..504069de 100644 --- a/backend/src/main/java/com/graphy/backend/domain/project/controller/ProjectController.java +++ b/backend/src/main/java/com/graphy/backend/domain/project/controller/ProjectController.java @@ -6,10 +6,7 @@ import com.graphy.backend.domain.project.dto.request.GetProjectPlanRequest; import com.graphy.backend.domain.project.dto.request.GetProjectsRequest; import com.graphy.backend.domain.project.dto.request.UpdateProjectRequest; -import com.graphy.backend.domain.project.dto.response.CreateProjectResponse; -import com.graphy.backend.domain.project.dto.response.GetProjectDetailResponse; -import com.graphy.backend.domain.project.dto.response.GetProjectResponse; -import com.graphy.backend.domain.project.dto.response.UpdateProjectResponse; +import com.graphy.backend.domain.project.dto.response.*; import com.graphy.backend.domain.project.service.ProjectService; import com.graphy.backend.global.common.dto.PageRequest; import com.graphy.backend.global.error.ErrorCode; @@ -104,6 +101,13 @@ public ResponseEntity projectDetails(@PathVariable Long projectI .body(ResultResponse.of(ResultCode.PROJECT_GET_SUCCESS, result)); } + @Operation(summary = "findProjectRank", description = "프로젝트 랭킹 조회") + @GetMapping("/rank") + public ResponseEntity projectRankList() { + List result = projectService.findProjectRank(); + return ResponseEntity.ok(ResultResponse.of(ResultCode.PROJECT_GET_SUCCESS, result)); + } + @Operation(summary = "getProjectPlan", description = "프로젝트 고도화 계획 제안") @PostMapping("/plans") public ResponseEntity projectPlanDetails(final @RequestBody GetProjectPlanRequest getPlanRequest, @CurrentUser Member loginUser) throws ExecutionException, InterruptedException { diff --git a/backend/src/main/java/com/graphy/backend/domain/project/dto/response/GetProjectRankingResponse.java b/backend/src/main/java/com/graphy/backend/domain/project/dto/response/GetProjectRankingResponse.java new file mode 100644 index 00000000..52887b93 --- /dev/null +++ b/backend/src/main/java/com/graphy/backend/domain/project/dto/response/GetProjectRankingResponse.java @@ -0,0 +1,27 @@ +package com.graphy.backend.domain.project.dto.response; + +import com.graphy.backend.domain.project.domain.Project; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GetProjectRankingResponse { + private Long id; + private String projectName; + private int viewCount; + private int likeCount; + + public static GetProjectRankingResponse from(Project project) { + return GetProjectRankingResponse.builder() + .id(project.getId()) + .projectName(project.getProjectName()) + .viewCount(project.getViewCount()) + .likeCount(project.getLikeCount()) + .build(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java b/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java index 2b9d2c8f..97dd86b8 100644 --- a/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java +++ b/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java @@ -25,6 +25,8 @@ import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,6 +34,8 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -50,6 +54,8 @@ public class ProjectService { private final TagService tagService; private final GPTChatRestService gptChatRestService; private final TagRepository tagRepository; + private final RedisTemplate redisTemplate; + private final String KEY = "ranking"; // @PostConstruct // public void initTag() throws IOException { @@ -114,6 +120,7 @@ public Cookie addViewCount(HttpServletRequest request, Long projectId) { if (!oldCookie.getValue().contains("[" + projectId + "]")) { oldCookie.setValue(oldCookie.getValue() + "[" + projectId + "]"); project.addViewCount(); + redisTemplate.opsForZSet().incrementScore(KEY, projectId, 1); } oldCookie.setPath("/"); return oldCookie; @@ -121,6 +128,7 @@ public Cookie addViewCount(HttpServletRequest request, Long projectId) { Cookie newCookie = new Cookie("View_Count", "[" + projectId + "]"); newCookie.setPath("/"); project.addViewCount(); + redisTemplate.opsForZSet().incrementScore(KEY, projectId, 1); return newCookie; } } @@ -203,4 +211,23 @@ public String getPrompt(final GetProjectPlanRequest request){ return techStacks + "를 이용해" + request.getTopic() +"를 개발 중이고, 현재" + features + "까지 기능 구현한 상태에서 고도화된 기능과 " + plans + "을 사용한 고도화 방안을 알려줘"; } + + public List findProjectRank() { + Set> projectRanking = getProjectRank(); + + return projectRanking.stream() + .map(this::getProjectFromTypedTuple) + .map(GetProjectRankingResponse::from) + .collect(Collectors.toList()); + } + + private Set> getProjectRank() { + ZSetOperations ZSetOperations = redisTemplate.opsForZSet(); + return ZSetOperations.reverseRangeWithScores(KEY, 0, 9); + } + + private Project getProjectFromTypedTuple(ZSetOperations.TypedTuple tuple) { + return projectRepository.findById(Objects.requireNonNull(tuple.getValue())) + .orElseThrow(() -> new EmptyResultException(ErrorCode.PROJECT_DELETED_OR_NOT_EXIST)); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/graphy/backend/global/config/RedisConfig.java b/backend/src/main/java/com/graphy/backend/global/config/RedisConfig.java index 84315235..ddc4037c 100644 --- a/backend/src/main/java/com/graphy/backend/global/config/RedisConfig.java +++ b/backend/src/main/java/com/graphy/backend/global/config/RedisConfig.java @@ -1,10 +1,18 @@ package com.graphy.backend.global.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.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; @EnableRedisRepositories @Configuration @@ -20,4 +28,29 @@ public class RedisConfig { public RedisConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(redisHost, redisPort); } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Long.class)); + return redisTemplate; + } + + @Bean + public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) { + RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(RedisSerializationContext + .SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext + .SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); + + + return RedisCacheManager + .RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration) + .build(); + } } \ No newline at end of file From 4b983a6e480473b129c28c5ab9eaf487dcea7a0a Mon Sep 17 00:00:00 2001 From: youKeon Date: Fri, 27 Oct 2023 03:50:28 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=9D=B8=EA=B8=B0=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/controller/ProjectController.java | 2 +- .../project/service/ProjectService.java | 44 +++++++++++++------ .../backend/global/config/CacheConfig.java | 9 ++++ .../backend/global/config/RedisConfig.java | 10 +++++ .../backend/global/config/SecurityConfig.java | 1 + 5 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 backend/src/main/java/com/graphy/backend/global/config/CacheConfig.java diff --git a/backend/src/main/java/com/graphy/backend/domain/project/controller/ProjectController.java b/backend/src/main/java/com/graphy/backend/domain/project/controller/ProjectController.java index 504069de..124d64d8 100644 --- a/backend/src/main/java/com/graphy/backend/domain/project/controller/ProjectController.java +++ b/backend/src/main/java/com/graphy/backend/domain/project/controller/ProjectController.java @@ -104,7 +104,7 @@ public ResponseEntity projectDetails(@PathVariable Long projectI @Operation(summary = "findProjectRank", description = "프로젝트 랭킹 조회") @GetMapping("/rank") public ResponseEntity projectRankList() { - List result = projectService.findProjectRank(); + List result = projectService.findTopRankingProjectList(); return ResponseEntity.ok(ResultResponse.of(ResultCode.PROJECT_GET_SUCCESS, result)); } diff --git a/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java b/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java index 97dd86b8..44114db8 100644 --- a/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java +++ b/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java @@ -1,5 +1,6 @@ package com.graphy.backend.domain.project.service; +import com.fasterxml.jackson.databind.ObjectMapper; import com.graphy.backend.domain.comment.dto.response.GetCommentWithMaskingResponse; import com.graphy.backend.domain.comment.service.CommentService; import com.graphy.backend.domain.member.domain.Member; @@ -22,21 +23,23 @@ import com.graphy.backend.global.error.exception.LongRequestException; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ZSetOperations; import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import java.util.List; -import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -55,7 +58,13 @@ public class ProjectService { private final GPTChatRestService gptChatRestService; private final TagRepository tagRepository; private final RedisTemplate redisTemplate; - private final String KEY = "ranking"; + private final RedisTemplate redisRankingTemplate; + private final String RANKING_KEY = "ranking"; + private final String TOP_RANKING_PROJECT_KEY = "topRankingProject"; + private final int START_RANKING = 0; + private final int END_RANKING = 9; + ObjectMapper objectMapper = new ObjectMapper(); + // @PostConstruct // public void initTag() throws IOException { @@ -120,7 +129,7 @@ public Cookie addViewCount(HttpServletRequest request, Long projectId) { if (!oldCookie.getValue().contains("[" + projectId + "]")) { oldCookie.setValue(oldCookie.getValue() + "[" + projectId + "]"); project.addViewCount(); - redisTemplate.opsForZSet().incrementScore(KEY, projectId, 1); + redisTemplate.opsForZSet().incrementScore(RANKING_KEY, projectId, 1); } oldCookie.setPath("/"); return oldCookie; @@ -128,7 +137,7 @@ public Cookie addViewCount(HttpServletRequest request, Long projectId) { Cookie newCookie = new Cookie("View_Count", "[" + projectId + "]"); newCookie.setPath("/"); project.addViewCount(); - redisTemplate.opsForZSet().incrementScore(KEY, projectId, 1); + redisTemplate.opsForZSet().incrementScore(RANKING_KEY, projectId, 1); return newCookie; } } @@ -212,22 +221,31 @@ public String getPrompt(final GetProjectPlanRequest request){ + features + "까지 기능 구현한 상태에서 고도화된 기능과 " + plans + "을 사용한 고도화 방안을 알려줘"; } - public List findProjectRank() { - Set> projectRanking = getProjectRank(); + @Cacheable(value = "topRankingProjects") + public List findTopRankingProjectList() { + return redisRankingTemplate.opsForList().range(TOP_RANKING_PROJECT_KEY, START_RANKING, END_RANKING); + } - return projectRanking.stream() - .map(this::getProjectFromTypedTuple) - .map(GetProjectRankingResponse::from) + @Scheduled(cron = "0 0 6 ? * MON", zone = "Asia/Seoul") + protected void initializeProjectRanking() { + List projectIds = getProjectRank().stream() + .map(ZSetOperations.TypedTuple::getValue) .collect(Collectors.toList()); + + getRankedProjectListById(projectIds).stream() + .map(GetProjectRankingResponse::from) + .forEach(e -> { + redisRankingTemplate.opsForList().rightPush(TOP_RANKING_PROJECT_KEY, e); + }); + redisRankingTemplate.expire(TOP_RANKING_PROJECT_KEY, 7, TimeUnit.DAYS); } private Set> getProjectRank() { ZSetOperations ZSetOperations = redisTemplate.opsForZSet(); - return ZSetOperations.reverseRangeWithScores(KEY, 0, 9); + return ZSetOperations.reverseRangeWithScores(RANKING_KEY, START_RANKING, END_RANKING); } - private Project getProjectFromTypedTuple(ZSetOperations.TypedTuple tuple) { - return projectRepository.findById(Objects.requireNonNull(tuple.getValue())) - .orElseThrow(() -> new EmptyResultException(ErrorCode.PROJECT_DELETED_OR_NOT_EXIST)); + private List getRankedProjectListById(List projectIds) { + return projectRepository.findAllById(projectIds); } } \ No newline at end of file diff --git a/backend/src/main/java/com/graphy/backend/global/config/CacheConfig.java b/backend/src/main/java/com/graphy/backend/global/config/CacheConfig.java new file mode 100644 index 00000000..f7e8e2cf --- /dev/null +++ b/backend/src/main/java/com/graphy/backend/global/config/CacheConfig.java @@ -0,0 +1,9 @@ +package com.graphy.backend.global.config; + +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Configuration; + +@EnableCaching +@Configuration +public class CacheConfig { +} diff --git a/backend/src/main/java/com/graphy/backend/global/config/RedisConfig.java b/backend/src/main/java/com/graphy/backend/global/config/RedisConfig.java index ddc4037c..85397537 100644 --- a/backend/src/main/java/com/graphy/backend/global/config/RedisConfig.java +++ b/backend/src/main/java/com/graphy/backend/global/config/RedisConfig.java @@ -1,5 +1,6 @@ package com.graphy.backend.global.config; +import com.graphy.backend.domain.project.dto.response.GetProjectRankingResponse; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -38,6 +39,15 @@ public RedisTemplate redisTemplate(RedisConnectionFactory redisCon return redisTemplate; } + @Bean + public RedisTemplate redisRankDtoTemplate(RedisConnectionFactory redisConnectionFactory){ + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(GetProjectRankingResponse.class)); + return redisTemplate; + } + @Bean public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() diff --git a/backend/src/main/java/com/graphy/backend/global/config/SecurityConfig.java b/backend/src/main/java/com/graphy/backend/global/config/SecurityConfig.java index fba011cf..82479128 100644 --- a/backend/src/main/java/com/graphy/backend/global/config/SecurityConfig.java +++ b/backend/src/main/java/com/graphy/backend/global/config/SecurityConfig.java @@ -39,6 +39,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws "/api/v1/auth/signin", "/api/v1/auth/logout", "/api/v1/projects", + "/api/v1/projects/rank", "/api/v1/members/**", "/swagger-ui/**").permitAll() .antMatchers(HttpMethod.GET, "/api/v1/projects/{projectId}").permitAll() From dff2bde61227ef15ca112d587c0dce853c92f697 Mon Sep 17 00:00:00 2001 From: youKeon Date: Fri, 27 Oct 2023 21:12:15 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=88=9C=EC=9C=84=EA=B6=8C=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=88=98=EC=A0=95/?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=8B=9C=20Redis=EC=97=90=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle | 5 ++ .../GetCommentWithMaskingResponse.java | 8 +- .../dto/response/GetMemberResponse.java | 4 +- .../project/controller/ProjectController.java | 7 +- .../domain/project/domain/Project.java | 13 ++- .../response/GetProjectDetailResponse.java | 16 +++- .../project/service/ProjectService.java | 86 +++++++++++++------ .../backend/global/config/RedisConfig.java | 11 ++- 8 files changed, 106 insertions(+), 44 deletions(-) diff --git a/backend/build.gradle b/backend/build.gradle index a7739985..e9cdb512 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -97,6 +97,11 @@ dependencies { // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE' + // LocalDataTime 역직렬화 문제 해결 패키지 + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + implementation 'com.fasterxml.jackson.core:jackson-databind' + + } tasks.named('test') { diff --git a/backend/src/main/java/com/graphy/backend/domain/comment/dto/response/GetCommentWithMaskingResponse.java b/backend/src/main/java/com/graphy/backend/domain/comment/dto/response/GetCommentWithMaskingResponse.java index f110413c..762a5f80 100644 --- a/backend/src/main/java/com/graphy/backend/domain/comment/dto/response/GetCommentWithMaskingResponse.java +++ b/backend/src/main/java/com/graphy/backend/domain/comment/dto/response/GetCommentWithMaskingResponse.java @@ -1,7 +1,10 @@ package com.graphy.backend.domain.comment.dto.response; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.querydsl.core.annotations.QueryProjection; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -14,6 +17,9 @@ public class GetCommentWithMaskingResponse { private String content; private Long commentId; + + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) private LocalDateTime createdAt; private String nickname; private Long childCount; diff --git a/backend/src/main/java/com/graphy/backend/domain/member/dto/response/GetMemberResponse.java b/backend/src/main/java/com/graphy/backend/domain/member/dto/response/GetMemberResponse.java index 360e249d..ec129461 100644 --- a/backend/src/main/java/com/graphy/backend/domain/member/dto/response/GetMemberResponse.java +++ b/backend/src/main/java/com/graphy/backend/domain/member/dto/response/GetMemberResponse.java @@ -3,11 +3,13 @@ import com.graphy.backend.domain.member.domain.Member; import lombok.*; +import java.io.Serializable; + @Getter @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor -public class GetMemberResponse { +public class GetMemberResponse implements Serializable { private String nickname; private String email; diff --git a/backend/src/main/java/com/graphy/backend/domain/project/controller/ProjectController.java b/backend/src/main/java/com/graphy/backend/domain/project/controller/ProjectController.java index 124d64d8..acce67dd 100644 --- a/backend/src/main/java/com/graphy/backend/domain/project/controller/ProjectController.java +++ b/backend/src/main/java/com/graphy/backend/domain/project/controller/ProjectController.java @@ -6,7 +6,10 @@ import com.graphy.backend.domain.project.dto.request.GetProjectPlanRequest; import com.graphy.backend.domain.project.dto.request.GetProjectsRequest; import com.graphy.backend.domain.project.dto.request.UpdateProjectRequest; -import com.graphy.backend.domain.project.dto.response.*; +import com.graphy.backend.domain.project.dto.response.CreateProjectResponse; +import com.graphy.backend.domain.project.dto.response.GetProjectDetailResponse; +import com.graphy.backend.domain.project.dto.response.GetProjectResponse; +import com.graphy.backend.domain.project.dto.response.UpdateProjectResponse; import com.graphy.backend.domain.project.service.ProjectService; import com.graphy.backend.global.common.dto.PageRequest; import com.graphy.backend.global.error.ErrorCode; @@ -104,7 +107,7 @@ public ResponseEntity projectDetails(@PathVariable Long projectI @Operation(summary = "findProjectRank", description = "프로젝트 랭킹 조회") @GetMapping("/rank") public ResponseEntity projectRankList() { - List result = projectService.findTopRankingProjectList(); + List result = projectService.findTopRankingProjectList(); return ResponseEntity.ok(ResultResponse.of(ResultCode.PROJECT_GET_SUCCESS, result)); } diff --git a/backend/src/main/java/com/graphy/backend/domain/project/domain/Project.java b/backend/src/main/java/com/graphy/backend/domain/project/domain/Project.java index 0283ea07..160322be 100644 --- a/backend/src/main/java/com/graphy/backend/domain/project/domain/Project.java +++ b/backend/src/main/java/com/graphy/backend/domain/project/domain/Project.java @@ -2,6 +2,7 @@ import com.graphy.backend.domain.comment.domain.Comment; import com.graphy.backend.domain.member.domain.Member; +import com.graphy.backend.domain.project.dto.request.UpdateProjectRequest; import com.graphy.backend.global.common.BaseEntity; import lombok.AllArgsConstructor; import lombok.Builder; @@ -61,13 +62,11 @@ public class Project extends BaseEntity { @ColumnDefault("0") private int viewCount = 0; - public void updateProject(String projectName, String content, - String description, Tags tags, - String thumbNail) { - this.projectName = projectName; - this.content = content; - this.description = description; - this.thumbNail = thumbNail; + public void updateProject(UpdateProjectRequest dto, Tags tags) { + this.projectName = dto.getProjectName(); + this.content = dto.getContent(); + this.description = dto.getDescription(); + this.thumbNail = dto.getThumbNail(); projectTags.clear(); addTag(tags); } diff --git a/backend/src/main/java/com/graphy/backend/domain/project/dto/response/GetProjectDetailResponse.java b/backend/src/main/java/com/graphy/backend/domain/project/dto/response/GetProjectDetailResponse.java index 4e08a99d..ed952949 100644 --- a/backend/src/main/java/com/graphy/backend/domain/project/dto/response/GetProjectDetailResponse.java +++ b/backend/src/main/java/com/graphy/backend/domain/project/dto/response/GetProjectDetailResponse.java @@ -1,21 +1,29 @@ package com.graphy.backend.domain.project.dto.response; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.graphy.backend.domain.comment.dto.response.GetCommentWithMaskingResponse; import com.graphy.backend.domain.member.dto.response.GetMemberResponse; import com.graphy.backend.domain.project.domain.Project; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import java.io.Serializable; import java.time.LocalDateTime; import java.util.List; -import static com.graphy.backend.domain.member.dto.response.GetMemberResponse.*; +import static com.graphy.backend.domain.member.dto.response.GetMemberResponse.from; @Getter @Builder @NoArgsConstructor @AllArgsConstructor -public class GetProjectDetailResponse { +public class GetProjectDetailResponse implements Serializable { private Long id; @@ -29,6 +37,8 @@ public class GetProjectDetailResponse { private List commentsList; + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) private LocalDateTime createdAt; private List techTags; diff --git a/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java b/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java index 44114db8..1ec03503 100644 --- a/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java +++ b/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java @@ -1,6 +1,5 @@ package com.graphy.backend.domain.project.service; -import com.fasterxml.jackson.databind.ObjectMapper; import com.graphy.backend.domain.comment.dto.response.GetCommentWithMaskingResponse; import com.graphy.backend.domain.comment.service.CommentService; import com.graphy.backend.domain.member.domain.Member; @@ -23,10 +22,10 @@ import com.graphy.backend.global.error.exception.LongRequestException; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; -import org.springframework.cache.annotation.Cacheable; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ZSetOperations; import org.springframework.scheduling.annotation.Async; @@ -36,8 +35,9 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; import java.util.List; -import java.util.Set; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -58,12 +58,12 @@ public class ProjectService { private final GPTChatRestService gptChatRestService; private final TagRepository tagRepository; private final RedisTemplate redisTemplate; - private final RedisTemplate redisRankingTemplate; + private final RedisTemplate redisRankingTemplate; private final String RANKING_KEY = "ranking"; private final String TOP_RANKING_PROJECT_KEY = "topRankingProject"; private final int START_RANKING = 0; private final int END_RANKING = 9; - ObjectMapper objectMapper = new ObjectMapper(); + private int NEXT_RANKING = END_RANKING; // @PostConstruct @@ -96,6 +96,7 @@ public CreateProjectResponse addProject(CreateProjectRequest dto, Member loginUs public void removeProject(Long projectId) { try { projectRepository.deleteById(projectId); + if (isProjectIdExistInRanking(projectId)) addNextRankingProject(projectId); } catch (EmptyResultDataAccessException e) { throw new EmptyResultException(ErrorCode.PROJECT_DELETED_OR_NOT_EXIST); } @@ -103,11 +104,12 @@ public void removeProject(Long projectId) { @Transactional public UpdateProjectResponse modifyProject(Long projectId, UpdateProjectRequest dto) { - Project project = projectRepository.findById(projectId).get(); + Project project = projectRepository.findById(projectId).orElseThrow(() -> new EmptyResultException(ErrorCode.PROJECT_DELETED_OR_NOT_EXIST)); projectTagService.removeProjectTag(project.getId()); Tags updatedTags = tagService.findTagListByName(dto.getTechTags()); - project.updateProject(dto.getProjectName(), dto.getContent(), dto.getDescription(), updatedTags, dto.getThumbNail()); + project.updateProject(dto, updatedTags); + if (isProjectIdExistInRanking(projectId)) modifyProjectInRanking(project); return UpdateProjectResponse.from(project); } @@ -221,31 +223,67 @@ public String getPrompt(final GetProjectPlanRequest request){ + features + "까지 기능 구현한 상태에서 고도화된 기능과 " + plans + "을 사용한 고도화 방안을 알려줘"; } - @Cacheable(value = "topRankingProjects") - public List findTopRankingProjectList() { - return redisRankingTemplate.opsForList().range(TOP_RANKING_PROJECT_KEY, START_RANKING, END_RANKING); + public List findTopRankingProjectList() { + HashOperations hashOperation = redisRankingTemplate.opsForHash(); + List topRankingProjectIdList = getTopRankingProjectIdList(); + + return topRankingProjectIdList.stream() + .map(projectId -> hashOperation.get(TOP_RANKING_PROJECT_KEY, projectId)) + .collect(Collectors.toList()); } + @Scheduled(cron = "0 0 6 ? * MON", zone = "Asia/Seoul") - protected void initializeProjectRanking() { - List projectIds = getProjectRank().stream() - .map(ZSetOperations.TypedTuple::getValue) - .collect(Collectors.toList()); + public void initializeProjectRanking() { + NEXT_RANKING = END_RANKING; + HashOperations hashOperation = redisRankingTemplate.opsForHash(); + List topRankingProjectList = getRankedProjectListById(getTopRankingProjectIdList()); - getRankedProjectListById(projectIds).stream() - .map(GetProjectRankingResponse::from) - .forEach(e -> { - redisRankingTemplate.opsForList().rightPush(TOP_RANKING_PROJECT_KEY, e); - }); + topRankingProjectList.forEach(project -> hashOperation.put(TOP_RANKING_PROJECT_KEY, project.getId(), project)); redisRankingTemplate.expire(TOP_RANKING_PROJECT_KEY, 7, TimeUnit.DAYS); } - private Set> getProjectRank() { - ZSetOperations ZSetOperations = redisTemplate.opsForZSet(); - return ZSetOperations.reverseRangeWithScores(RANKING_KEY, START_RANKING, END_RANKING); + private List getTopRankingProjectIdList() { + ZSetOperations zSetOperations = redisTemplate.opsForZSet(); + return new ArrayList<>(Objects.requireNonNull(zSetOperations.reverseRange(RANKING_KEY, START_RANKING, END_RANKING))); + } + + private List getRankedProjectListById(List projectIds) { + List projectList = projectRepository.findAllById(projectIds); + + return projectList.stream() + .map(project -> { + List comments = commentService.findCommentListWithMasking(project.getId()); + return GetProjectDetailResponse.of(project, comments); + }) + .collect(Collectors.toList()); + } + + private void addNextRankingProject(Long deletedProjectId) { + ZSetOperations zSetOperations = redisTemplate.opsForZSet(); + HashOperations hashOperation = redisRankingTemplate.opsForHash(); + + hashOperation.delete(TOP_RANKING_PROJECT_KEY, deletedProjectId); + zSetOperations.remove(RANKING_KEY, deletedProjectId); + + Long nextRankingProjectId = Objects.requireNonNull(zSetOperations.reverseRange(RANKING_KEY, NEXT_RANKING, NEXT_RANKING)) + .iterator() + .next(); + NEXT_RANKING++; + + GetProjectDetailResponse nextRankingProject = this.findProjectById(nextRankingProjectId); + hashOperation.put(TOP_RANKING_PROJECT_KEY, nextRankingProjectId, nextRankingProject); + } + + private void modifyProjectInRanking(Project project) { + HashOperations hashOperation = redisRankingTemplate.opsForHash(); + List comments = commentService.findCommentListWithMasking(project.getId()); + GetProjectDetailResponse updateProject = GetProjectDetailResponse.of(project, comments); + hashOperation.put(TOP_RANKING_PROJECT_KEY, project.getId(), updateProject); } - private List getRankedProjectListById(List projectIds) { - return projectRepository.findAllById(projectIds); + private boolean isProjectIdExistInRanking(Long projectId) { + HashOperations hashOperation = redisRankingTemplate.opsForHash(); + return hashOperation.hasKey(TOP_RANKING_PROJECT_KEY, projectId); } } \ No newline at end of file diff --git a/backend/src/main/java/com/graphy/backend/global/config/RedisConfig.java b/backend/src/main/java/com/graphy/backend/global/config/RedisConfig.java index 85397537..b2be364b 100644 --- a/backend/src/main/java/com/graphy/backend/global/config/RedisConfig.java +++ b/backend/src/main/java/com/graphy/backend/global/config/RedisConfig.java @@ -1,6 +1,6 @@ package com.graphy.backend.global.config; -import com.graphy.backend.domain.project.dto.response.GetProjectRankingResponse; +import com.graphy.backend.domain.project.dto.response.GetProjectDetailResponse; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -31,7 +31,7 @@ public RedisConnectionFactory redisConnectionFactory() { } @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ + public RedisTemplate redisProjectRankingTemplate(RedisConnectionFactory redisConnectionFactory){ RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); @@ -40,11 +40,11 @@ public RedisTemplate redisTemplate(RedisConnectionFactory redisCon } @Bean - public RedisTemplate redisRankDtoTemplate(RedisConnectionFactory redisConnectionFactory){ - RedisTemplate redisTemplate = new RedisTemplate<>(); + public RedisTemplate redisTopRankingProjectTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(GetProjectRankingResponse.class)); + redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(GetProjectDetailResponse.class)); return redisTemplate; } @@ -56,7 +56,6 @@ public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectio .serializeValuesWith(RedisSerializationContext .SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); - return RedisCacheManager .RedisCacheManagerBuilder .fromConnectionFactory(redisConnectionFactory) From 429c91962e06168452472e342a3a96bdf5fe5744 Mon Sep 17 00:00:00 2001 From: youKeon Date: Fri, 27 Oct 2023 23:00:02 +0900 Subject: [PATCH 4/5] =?UTF-8?q?test:=20CI=20=EC=97=90=EB=9F=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/project/service/ProjectServiceTest.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/test/java/com/graphy/backend/domain/project/service/ProjectServiceTest.java b/backend/src/test/java/com/graphy/backend/domain/project/service/ProjectServiceTest.java index 6bb0d6c7..b51e4950 100644 --- a/backend/src/test/java/com/graphy/backend/domain/project/service/ProjectServiceTest.java +++ b/backend/src/test/java/com/graphy/backend/domain/project/service/ProjectServiceTest.java @@ -30,6 +30,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.data.redis.core.HashOperations; import java.util.ArrayList; import java.util.Arrays; @@ -56,6 +57,8 @@ class ProjectServiceTest extends MockTest { @Mock private CustomUserDetailsService customUserDetailsService; + @Mock + HashOperations hashOperations; @InjectMocks private ProjectService projectService; @@ -86,14 +89,15 @@ void updateProject() throws Exception { Tag tag1 = Tag.builder().tech("Vue").build(); Tag tag2 = Tag.builder().tech("Java").build(); - //when when(projectRepository.findById(project.getId())).thenReturn(Optional.of(project)); when(tagService.findTagListByName(techTags)).thenReturn(new Tags(List.of(tag1, tag2))); + when(redisRankingTemplate.opsForHash()).thenReturn(hashOperations); UpdateProjectResponse result = projectService.modifyProject(project.getId(), request); - assertThat(result.getProjectName()).isEqualTo(project.getProjectName()); + //then + assertThat(result.getProjectName()).isEqualTo("afterUpdate"); // 수정된 부분 assertThat(result.getDescription()).isEqualTo(project.getDescription()); assertThat(result.getThumbNail()).isEqualTo(project.getThumbNail()); assertThat(result.getTechTags()).isEqualTo(new ArrayList<>(Arrays.asList("Vue", "Java"))); @@ -177,6 +181,7 @@ void getProjects() throws Exception { @DisplayName("프로젝트 삭제") void deleteProject() throws Exception { //when + when(redisRankingTemplate.opsForHash()).thenReturn(hashOperations); projectService.removeProject(1L); //then From bcb6e056ae52dd0a81072f81a0276fe123469be3 Mon Sep 17 00:00:00 2001 From: youKeon Date: Fri, 27 Oct 2023 23:05:53 +0900 Subject: [PATCH 5/5] =?UTF-8?q?test:=20CI=20=EC=97=90=EB=9F=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/project/service/ProjectServiceTest.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/src/test/java/com/graphy/backend/domain/project/service/ProjectServiceTest.java b/backend/src/test/java/com/graphy/backend/domain/project/service/ProjectServiceTest.java index b51e4950..a62a99f7 100644 --- a/backend/src/test/java/com/graphy/backend/domain/project/service/ProjectServiceTest.java +++ b/backend/src/test/java/com/graphy/backend/domain/project/service/ProjectServiceTest.java @@ -11,10 +11,7 @@ import com.graphy.backend.domain.project.dto.request.CreateProjectRequest; import com.graphy.backend.domain.project.dto.request.GetProjectsRequest; import com.graphy.backend.domain.project.dto.request.UpdateProjectRequest; -import com.graphy.backend.domain.project.dto.response.CreateProjectResponse; -import com.graphy.backend.domain.project.dto.response.GetProjectInfoResponse; -import com.graphy.backend.domain.project.dto.response.GetProjectResponse; -import com.graphy.backend.domain.project.dto.response.UpdateProjectResponse; +import com.graphy.backend.domain.project.dto.response.*; import com.graphy.backend.domain.project.repository.ProjectRepository; import com.graphy.backend.global.common.dto.PageRequest; import com.graphy.backend.global.error.ErrorCode; @@ -31,6 +28,7 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; import java.util.ArrayList; import java.util.Arrays; @@ -57,6 +55,10 @@ class ProjectServiceTest extends MockTest { @Mock private CustomUserDetailsService customUserDetailsService; + + @Mock + private RedisTemplate redisRankingTemplate; // RedisTemplate 모킹 + @Mock HashOperations hashOperations;