Skip to content

Commit

Permalink
Merge pull request #280 from techeer-sv/BE/#279
Browse files Browse the repository at this point in the history
feat: 조회 수 기준 Top10 프로젝트 조회 기능 구현
  • Loading branch information
youKeon authored Oct 27, 2023
2 parents fcfd23a + bcb6e05 commit 78efb68
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 20 deletions.
5 changes: 5 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ public ResponseEntity<ResultResponse> projectDetails(@PathVariable Long projectI
.body(ResultResponse.of(ResultCode.PROJECT_GET_SUCCESS, result));
}

@Operation(summary = "findProjectRank", description = "프로젝트 랭킹 조회")
@GetMapping("/rank")
public ResponseEntity<ResultResponse> projectRankList() {
List<GetProjectDetailResponse> result = projectService.findTopRankingProjectList();
return ResponseEntity.ok(ResultResponse.of(ResultCode.PROJECT_GET_SUCCESS, result));
}

@Operation(summary = "getProjectPlan", description = "프로젝트 고도화 계획 제안")
@PostMapping("/plans")
public ResponseEntity<ResultResponse> projectPlanDetails(final @RequestBody GetProjectPlanRequest getPlanRequest, @CurrentUser Member loginUser) throws ExecutionException, InterruptedException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -29,6 +37,8 @@ public class GetProjectDetailResponse {

private List<GetCommentWithMaskingResponse> commentsList;

@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime createdAt;

private List<String> techTags;
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,21 @@
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;
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.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;

Expand All @@ -50,6 +57,14 @@ public class ProjectService {
private final TagService tagService;
private final GPTChatRestService gptChatRestService;
private final TagRepository tagRepository;
private final RedisTemplate<String, Long> redisTemplate;
private final RedisTemplate<String, GetProjectDetailResponse> 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;
private int NEXT_RANKING = END_RANKING;


// @PostConstruct
// public void initTag() throws IOException {
Expand Down Expand Up @@ -81,18 +96,20 @@ 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);
}
}

@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);
}
Expand All @@ -114,13 +131,15 @@ public Cookie addViewCount(HttpServletRequest request, Long projectId) {
if (!oldCookie.getValue().contains("[" + projectId + "]")) {
oldCookie.setValue(oldCookie.getValue() + "[" + projectId + "]");
project.addViewCount();
redisTemplate.opsForZSet().incrementScore(RANKING_KEY, projectId, 1);
}
oldCookie.setPath("/");
return oldCookie;
} else {
Cookie newCookie = new Cookie("View_Count", "[" + projectId + "]");
newCookie.setPath("/");
project.addViewCount();
redisTemplate.opsForZSet().incrementScore(RANKING_KEY, projectId, 1);
return newCookie;
}
}
Expand Down Expand Up @@ -203,4 +222,68 @@ public String getPrompt(final GetProjectPlanRequest request){
return techStacks + "를 이용해" + request.getTopic() +"를 개발 중이고, 현재"
+ features + "까지 기능 구현한 상태에서 고도화된 기능과 " + plans + "을 사용한 고도화 방안을 알려줘";
}

public List<GetProjectDetailResponse> findTopRankingProjectList() {
HashOperations<String, Long, GetProjectDetailResponse> hashOperation = redisRankingTemplate.opsForHash();
List<Long> 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")
public void initializeProjectRanking() {
NEXT_RANKING = END_RANKING;
HashOperations<String, Long, GetProjectDetailResponse> hashOperation = redisRankingTemplate.opsForHash();
List<GetProjectDetailResponse> topRankingProjectList = getRankedProjectListById(getTopRankingProjectIdList());

topRankingProjectList.forEach(project -> hashOperation.put(TOP_RANKING_PROJECT_KEY, project.getId(), project));
redisRankingTemplate.expire(TOP_RANKING_PROJECT_KEY, 7, TimeUnit.DAYS);
}

private List<Long> getTopRankingProjectIdList() {
ZSetOperations<String, Long> zSetOperations = redisTemplate.opsForZSet();
return new ArrayList<>(Objects.requireNonNull(zSetOperations.reverseRange(RANKING_KEY, START_RANKING, END_RANKING)));
}

private List<GetProjectDetailResponse> getRankedProjectListById(List<Long> projectIds) {
List<Project> projectList = projectRepository.findAllById(projectIds);

return projectList.stream()
.map(project -> {
List<GetCommentWithMaskingResponse> comments = commentService.findCommentListWithMasking(project.getId());
return GetProjectDetailResponse.of(project, comments);
})
.collect(Collectors.toList());
}

private void addNextRankingProject(Long deletedProjectId) {
ZSetOperations<String, Long> zSetOperations = redisTemplate.opsForZSet();
HashOperations<String, Long, GetProjectDetailResponse> 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<String, Long, GetProjectDetailResponse> hashOperation = redisRankingTemplate.opsForHash();
List<GetCommentWithMaskingResponse> comments = commentService.findCommentListWithMasking(project.getId());
GetProjectDetailResponse updateProject = GetProjectDetailResponse.of(project, comments);
hashOperation.put(TOP_RANKING_PROJECT_KEY, project.getId(), updateProject);
}

private boolean isProjectIdExistInRanking(Long projectId) {
HashOperations<String, Long, GetProjectDetailResponse> hashOperation = redisRankingTemplate.opsForHash();
return hashOperation.hasKey(TOP_RANKING_PROJECT_KEY, projectId);
}
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package com.graphy.backend.global.config;

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;
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
Expand All @@ -20,4 +29,37 @@ public class RedisConfig {
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}

@Bean
public RedisTemplate<String, Long> redisProjectRankingTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Long> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Long.class));
return redisTemplate;
}

@Bean
public RedisTemplate<String, GetProjectDetailResponse> redisTopRankingProjectTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, GetProjectDetailResponse> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(GetProjectDetailResponse.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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading

0 comments on commit 78efb68

Please sign in to comment.