Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

*feat : redis 랭킹, 검색 구현 #31

Open
wants to merge 6 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package playlist.server.ranking;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

search의 경우 vo패키지에 클래스를 정리한거 같아 보이는데, 해당 패이지는 그냥 ranking패키지 root경로에 vo를 생성한 이유가 있을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\MainPageRankingInfo 이게 너무 네이밍이 구린거 같아 고민하다 놓친거 같습니다! 패키지 만들어서 vo 에 넣어뒀습니다!


import lombok.Builder;
import lombok.Getter;
import playlist.server.domain.domains.ranking.domain.RankingInfo;
import playlist.server.domain.domains.ranking.domain.RankingType;

@Getter
@Builder
public class MainPageRankingInfoVo {
private final RankingInfo rankingInfo;
private final RankingType rankingType;

public static MainPageRankingInfoVo from(RankingInfo rankingInfo, RankingType rankingType) {
return MainPageRankingInfoVo.builder()
.rankingInfo(rankingInfo)
.rankingType(rankingType)
.build();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빌더의 활용 정말 잘했습니당.

주의할점을 말해주자면, Annotation이 편하긴 한데 완벽은 없더라구요,
추후에는 @Builder의 작동원리, 커스텀하는 방법에 대해서도 공부해보면 좋을 것 같아요.

  • 저도 최근에 이 어노테이션에 한번 발등을 찍혀봤거든요.ㅋㅋㅋㅋ

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다!! 감사해요 증말!! 하트 뿅뿅

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package playlist.server.ranking.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import playlist.server.domain.domains.ranking.domain.RankingType;
import playlist.server.ranking.MainPageRankingInfoVo;
import playlist.server.ranking.service.RankingLikeService;
import playlist.server.ranking.service.RankingViewService;
import playlist.server.domain.domains.ranking.domain.RankingInfo; // RankingInfo enum 추가

@RestController
@RequestMapping("/ranking")
@RequiredArgsConstructor
@Tag(name = "1. [랭킹]")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

태그로 어울리는 정보를 전달하는거 매우 좋아보입니다 ^^

public class RankingController {

private final RankingLikeService rankingLikeService;
private final RankingViewService rankingViewService;

@Operation(summary = "일간 랭킹을 조회합니다.")
@GetMapping("/daily")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mapping을 차라리

  • /daily?type=view 또는 /daily?type=like
  • /daily/{type} 같은 방식으로 받은 이후 PathParameter로 받은 type을 view또는 like로 구분해서 로직을 태우는게 맞지 않을 까 합니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type 매개변수 사용해서 type값에 따라 좋아요랑 조회수 랭킹 정보 가져오는 로직 만들어 봤습니다!

public ResponseEntity<MainPageRankingInfoVo> getDailyRanking(
@RequestParam(name = "rankingType", required = false, defaultValue = "DAILY") RankingType rankingType) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메소드를 daily, week, month로 분류를 하였기 때문에, 해당 파라미터가 꼭 필요한가?라는 생각이 드네요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호라!!! 수정했습니다

RankingInfo rankingInfo = RankingInfo.DAILY;
rankingLikeService.incrementLikes(rankingType.name(), rankingInfo);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기에서 타입에 따라 로직을 다르게 태우도록 하면 어떘을까 싶네요.

MainPageRankingInfoVo rankingInfoVo = MainPageRankingInfoVo.from(rankingInfo, rankingType);

if (rankingInfoVo == null) {
return ResponseEntity.notFound().build();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미 위에서 객체를 생성해 받고 있는데, If문이 왜 필요한지를 모르겠네요

또한 오류에 대해서 표기하고싶으면 NotFound를 직접적으로 사용하는 것보단 Exception을 사용해 보면 좋을 것 같습니다.

마지막으로, 이건 제 의견인데, Controller에서의 역할은 사전에 데이터를 검수하고,
Service에서 사용해야 하는 객체를 생성하고 전달하고 바로 반환한다는 역할이라고 저는 생각합니다.
로직을 태우면 안된다고 생각하는데, 현우님은 현우님만의 생각을 정립해보면 좋을것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한번 쪼개봤습니다... 맞는지 확인 한 번 부탁드립니다


return ResponseEntity.ok(rankingInfoVo);
}

@Operation(summary = "주간 랭킹을 조회합니다.")
@GetMapping("/weekly")
public ResponseEntity<MainPageRankingInfoVo> getWeeklyRanking(
@RequestParam(name = "rankingType", required = false, defaultValue = "WEEKLY") RankingType rankingType) {
RankingInfo rankingInfo = RankingInfo.WEEKLY;
rankingLikeService.incrementLikes(rankingType.name(), rankingInfo);
MainPageRankingInfoVo rankingInfoVo = MainPageRankingInfoVo.from(rankingInfo, rankingType);

if (rankingInfoVo == null) {
return ResponseEntity.notFound().build();
}

return ResponseEntity.ok(rankingInfoVo);
}

@Operation(summary = "월간 랭킹을 조회합니다.")
@GetMapping("/monthly")
public ResponseEntity<MainPageRankingInfoVo> getMonthlyRanking(
@RequestParam(name = "rankingType", required = false, defaultValue = "MONTHLY") RankingType rankingType) {
RankingInfo rankingInfo = RankingInfo.MONTHLY;
rankingLikeService.incrementLikes(rankingType.name(), rankingInfo);
MainPageRankingInfoVo rankingInfoVo = MainPageRankingInfoVo.from(rankingInfo, rankingType);

if (rankingInfoVo == null) {
return ResponseEntity.notFound().build();
}

return ResponseEntity.ok(rankingInfoVo);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/daily 에서 했던 말과 동일합니다 ~ :)


@Operation(summary = "일간 조회수 랭킹을 조회합니다.")
@GetMapping("/daily-views")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 Mapping이 꼭 필요한지 의문이네요. /daily의 Mapping관련해서 했던말과 동일합니다 :), 기능이 중복되는 느낌을 강하게 받네요.

public ResponseEntity<MainPageRankingInfoVo> getDailyViewsRanking(
@RequestParam(name = "rankingType", required = false, defaultValue = "DAILY") RankingType rankingType) {
RankingInfo rankingInfo = RankingInfo.DAILY;
rankingViewService.incrementViews(rankingType.name(), rankingInfo);
MainPageRankingInfoVo rankingInfoVo = MainPageRankingInfoVo.from(rankingInfo, rankingType);

if (rankingInfoVo == null) {
return ResponseEntity.notFound().build();
}

return ResponseEntity.ok(rankingInfoVo);
}

@Operation(summary = "주간 조회수 랭킹을 조회합니다.")
@GetMapping("/weekly-views")
public ResponseEntity<MainPageRankingInfoVo> getWeeklyViewsRanking(
@RequestParam(name = "rankingType", required = false, defaultValue = "WEEKLY") RankingType rankingType) {
RankingInfo rankingInfo = RankingInfo.WEEKLY;
rankingViewService.incrementViews(rankingType.name(), rankingInfo);
MainPageRankingInfoVo rankingInfoVo = MainPageRankingInfoVo.from(rankingInfo, rankingType);

if (rankingInfoVo == null) {
return ResponseEntity.notFound().build();
}

return ResponseEntity.ok(rankingInfoVo);
}

@Operation(summary = "월간 조회수 랭킹을 조회합니다.")
@GetMapping("/monthly-views")
public ResponseEntity<MainPageRankingInfoVo> getMonthlyViewsRanking(
@RequestParam(name = "rankingType", required = false, defaultValue = "MONTHLY") RankingType rankingType) {
RankingInfo rankingInfo = RankingInfo.MONTHLY;
rankingViewService.incrementViews(rankingType.name(), rankingInfo);
MainPageRankingInfoVo rankingInfoVo = MainPageRankingInfoVo.from(rankingInfo, rankingType);

if (rankingInfoVo == null) {
return ResponseEntity.notFound().build();
}

return ResponseEntity.ok(rankingInfoVo);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package playlist.server.ranking.service;

import lombok.RequiredArgsConstructor;
import playlist.server.annotation.Adaptor;
import playlist.server.domain.domains.ranking.domain.Ranking;
import playlist.server.domain.domains.ranking.domain.RankingInfo;
import playlist.server.domain.domains.ranking.repository.RankingRepository;

@Adaptor
@RequiredArgsConstructor
public class RankingAdaptor {
private final RankingRepository rankingRepository;

public Ranking queryByRankingInfo(RankingInfo rankingInfo) {
return rankingRepository.findByRankingInfo(rankingInfo);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 Adaptor는 실사용은 안하는것 같은데, 삭제해도 괜찮지 않나요?

또한 레이어 구조가 Controller-Service-Repositry 이기 때문에 불필요해 보입니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

삭제했습니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package playlist.server.ranking.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import playlist.server.domain.domains.ranking.domain.RankingInfo; // RankingInfo enum 추가

@Service
public class RankingLikeService {

private final RedisTemplate<String, String> redisTemplate;

@Autowired
public RankingLikeService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}

// 좋아요 증가, 랭킹 업데이트 (일간)
public void incrementLikes(String boardId, RankingInfo rankingInfo) {
redisTemplate.opsForHash().increment(rankingInfo.getCountsKey(), boardId, 1L);
redisTemplate.opsForZSet().add(rankingInfo.getRankingKey(), boardId, (double) getCurrentLikes(rankingInfo.getCountsKey(), boardId));
}

// 현재 게시물의 좋아요를 가져오는 메서드
private long getCurrentLikes(String hash, String boardId) {
Object likes = redisTemplate.opsForHash().get(hash, boardId);
if (likes != null) {
return (long) likes;
} else {
return 0L;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package playlist.server.ranking.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import playlist.server.domain.domains.ranking.domain.RankingInfo; // RankingInfo enum 추가

@Service
public class RankingViewService {

private final RedisTemplate<String, String> redisTemplate;

@Autowired
public RankingViewService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RequiredArgsTemplate 한줄이면 해당 생성자가 필요 없다는 생각이 드네요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했어요!


// 조회수 증가, 랭킹 업데이트 (일간)
public void incrementViews(String boardId, RankingInfo rankingInfo) {
redisTemplate.opsForHash().increment(rankingInfo.getCountsKey(), boardId, 1L);
redisTemplate.opsForZSet().add(rankingInfo.getRankingKey(), boardId, (double) getCurrentViews(rankingInfo.getCountsKey(), boardId));
}

// 현재 게시물의 조회수를 가져오는 메서드
private long getCurrentViews(String hash, String boardId) {
Object views = redisTemplate.opsForHash().get(hash, boardId);
if (views != null) {
return (long) views;
} else {
return 0L;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package playlist.server.search.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import playlist.server.search.service.SearchService;
import playlist.server.search.vo.SearchVo;

import java.util.List;

@RestController
@RequestMapping("/playlist/server/search")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 생각했을때는 /playlist/search또는 /search라는 Mapping Value면 괜찮을꺼라 생각이 드는데 '/playlist/server/search'로 잡은 이유가 있을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니다!

public class SearchController {

private final SearchService searchService;

public SearchController(SearchService searchService) {
this.searchService = searchService;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RequiredArgsConstructor 에 대해서 알아보면 좋을 것 같습니다.


// 태그 기반으로 검색
@GetMapping("/tag")
public ResponseEntity<List<SearchVo>> searchByTag(@RequestParam("tag") String tag) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResponseEntity로 제작하는건 Controller에서 하는게 좋아보이네요.

List<SearchVo> searchResults = searchService.searchByTag(tag);

if (searchResults.isEmpty()) {
return ResponseEntity.notFound().build();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NotFound를 주기보단 Exception, 또는 Size가 0인 List를 반환하는것도 괜찮지 않을까요?

notFound를 직접 선언한 이유가 궁금하네요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception 으로 수정했고 @requiredargsconstructor 사용했습니다!!
notFound ... 단순하고.... Exception 예외 처리 로직이 두려워 써봤습니다....


return ResponseEntity.ok(searchResults);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package playlist.server.search.service;

import org.springframework.stereotype.Service;
import playlist.server.domain.domains.search.domain.Search;
import playlist.server.domain.domains.search.repository.SearchRepository;
import playlist.server.search.vo.SearchVo;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class SearchService {

private final SearchRepository searchRepository;

public SearchService(SearchRepository searchRepository) {
this.searchRepository = searchRepository;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RequiredArgsConstructor

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했슴둥


public List<SearchVo> searchByTag(String tag) {
// 태그를 기반으로 검색 결과를 데이터베이스에서 조회
List<Search> searchEntities = searchRepository.findByTag(tag);

// 검색 결과를 SearchVo로 변환
List<SearchVo> searchResults = searchEntities.stream()
.map(search -> new SearchVo(search.getTitle(), search.getDescription()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

.collect(Collectors.toList());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lambda 멋져요 👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헤헤헤헤히히히히히


return searchResults;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 return searchEntities.stream()
                .map(search -> new SearchVo(search.getTitle(), search.getDescription()))
                .collect(Collectors.toList());

로 해보는게 어떨까요

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했어요!

}
}
18 changes: 18 additions & 0 deletions Api/src/main/java/playlist/server/search/vo/SearchVo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package playlist.server.search.vo;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@NoArgsConstructor
@AllArgsConstructor
@ToString
@Getter
@Setter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Data라는 Annotation도 있으니 활용해보면 좋을 것 같습니다 👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

활용 완료!

public class SearchVo {

private String title;
private String description;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package playlist.server.domain.domains.ranking.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;

import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Table(name = "tbl_ranking")
@NoArgsConstructor
public class Ranking {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Enumerated(EnumType.STRING)
private RankingType rankingType;


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package playlist.server.domain.domains.ranking.domain;

public enum RankingInfo {
DAILY("like_daily_counts", "like_daily_ranking"),
WEEKLY("like_weekly_counts", "like_weekly_ranking"),
MONTHLY("like_monthly_counts", "like_monthly_ranking");

private final String countsKey;
private final String rankingKey;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변수명 직관적인거 보기 좋네요


RankingInfo(String countsKey, String rankingKey) {
this.countsKey = countsKey;
this.rankingKey = rankingKey;
}

public String getCountsKey() {
return countsKey;
}

public String getRankingKey() {
return rankingKey;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AllArgsConstructors, @Getter, @Setter

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니다!

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package playlist.server.domain.domains.ranking.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum RankingType {
DAILY("일간"),
WEEKLY("주간"),
MONTHLY("월간");

private final String description;

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package playlist.server.domain.domains.ranking.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import playlist.server.domain.domains.ranking.domain.Ranking;
import playlist.server.domain.domains.ranking.domain.RankingInfo;


public interface RankingRepository extends JpaRepository<Ranking, Long> {
Ranking findByRankingInfo(RankingInfo rankingInfo);
}
Loading
Loading