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 all 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,25 @@
package playlist.server.mainpagerankingInfo.vo;

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

import java.util.List; // 추가

@Getter
@Builder
public class MainPageRankingInfoVo {
private final RankingInfo rankingInfo;
private final RankingType rankingType;
private final List<RankingInfo> rankingInfoList;


public static MainPageRankingInfoVo from(RankingInfo rankingInfo, RankingType rankingType, List<RankingInfo> rankingInfoList) {
return MainPageRankingInfoVo.builder()
.rankingInfo(rankingInfo)
.rankingType(rankingType)
.rankingInfoList(rankingInfoList)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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.RankingInfo;
import playlist.server.domain.domains.ranking.domain.RankingType;
import playlist.server.ranking.service.RankingService;
import playlist.server.ranking.vo.ResponseRankingDto;

import java.util.List;

import static playlist.server.domain.domains.ranking.domain.RankingInfo.MONTH;
import static playlist.server.domain.domains.ranking.domain.RankingInfo.WEEK;
import static playlist.server.domain.domains.ranking.domain.RankingType.isStringLikeOrView;

@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 RankingService rankingService;

@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<List<ResponseRankingDto>> getDailyRanking(
@RequestParam(name = "type", defaultValue = "like") String type) {
return ResponseEntity.ok(getRanking(RankingInfo.DAILY, isStringLikeOrView(type)));
}

@Operation(summary = "주간 랭킹을 조회합니다.")
@GetMapping("/weekly")
public ResponseEntity<List<ResponseRankingDto>> getWeeklyRanking(
@RequestParam(name = "type", defaultValue = "like") String type) {
return ResponseEntity.ok(getRanking(WEEK, isStringLikeOrView(type)));
}


@Operation(summary = "월간 랭킹을 조회합니다.")
@GetMapping("/monthly")
public ResponseEntity<List<ResponseRankingDto>> getMonthlyRanking(
@RequestParam(name = "type", defaultValue = "like") String type) {
return ResponseEntity.ok(getRanking(MONTH, isStringLikeOrView(type)));
}


private List<ResponseRankingDto> getRanking(RankingInfo period, RankingType searchType) {
return rankingService.getRankingList(period, searchType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package playlist.server.ranking.service;

import playlist.server.domain.domains.ranking.domain.RankingInfo;
import playlist.server.domain.domains.ranking.domain.RankingType;
import playlist.server.ranking.vo.ResponseRankingDto;

import java.util.List;

public interface RankingService {

List<ResponseRankingDto> getRankingList(RankingInfo period, RankingType type);

void incrementCountProcess(RankingType type, String boardId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package playlist.server.ranking.service;


import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import playlist.server.domain.domains.ranking.domain.RankingInfo;
import playlist.server.domain.domains.ranking.domain.RankingType;
import playlist.server.ranking.vo.ResponseRankingDto;

import java.util.Arrays;
import java.util.List;

/**
* 참고한 Reference : https://cantcoding.tistory.com/82
*/
@Service
@RequiredArgsConstructor
public class RedisRankingService implements RankingService {

private final RedisTemplate<String, String> redisTemplate;

/**
* 랭킹 리스트를 0~10까지 가져온다.
* @param period
* @param type
* @return
*/
@Override
public List<ResponseRankingDto> getRankingList(RankingInfo period, RankingType type) {
return redisTemplate.opsForZSet()
.reverseRangeWithScores(getRankingTypeKey(period, type), 0, 10)
.stream()
.map(ResponseRankingDto::convertToResponseRankingDto).toList();
}

/**
* Like, View의 Daily, Week, Month의 카운트를 모두 증가시킨다.
*
* @param type
* @param boardId
*/
@Override
public void incrementCountProcess(RankingType type, String boardId) {
Arrays.stream(RankingInfo.values()).forEach(period -> {
if (isExistInRanking(period, type, boardId)) {
incrementRankingCount(period, type, boardId);
} else {
addRanking(period, type, boardId);
}
});
}


/**
* 현재 ZSet에 값이 존재하는지 확인한다.
* @param period
* @param type
* @param boardId
* @return
*/
private boolean isExistInRanking(RankingInfo period, RankingType type, String boardId) {
return redisTemplate.opsForZSet()
.score(getRankingTypeKey(period, type), boardId) != null;
}

/**
* 값이 존재하지 않는 경우 Default Value로 1을 제공한다.
*
* @param period
* @param type
* @param boardId
*/
private void addRanking(RankingInfo period, RankingType type, String boardId) {
redisTemplate.opsForZSet().add(getRankingTypeKey(period, type), boardId, 1);
}

/**
* Zset에 존재하는 경우 해당 값을 증가시킨다
*
* @param period
* @param type
* @param boardId
*/
private void incrementRankingCount(RankingInfo period, RankingType type, String boardId) {
redisTemplate.opsForZSet()
.incrementScore(getRankingTypeKey(period, type), boardId, 1);
}

/**
* Type에 따라 Key를 다르게 가져온다.
*
* @param period
* @param type
* @return
*/
private String getRankingTypeKey(RankingInfo period, RankingType type) {
return RankingType.LIKE.equals(type) ? period.getLikeKey() : period.getViewKey();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package playlist.server.ranking.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.redis.core.ZSetOperations;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ResponseRankingDto {
private String boardId;
private Long score;

public static ResponseRankingDto convertToResponseRankingDto(ZSetOperations.TypedTuple typedTuple) {
return ResponseRankingDto.builder()
.boardId(typedTuple.getValue().toString())
.score(typedTuple.getScore().longValue())
.build();
}

}


Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package playlist.server.search.controller;


import java.util.List;
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.exception.TagNotFoundException;
import playlist.server.search.service.SearchService;
import playlist.server.search.vo.SearchVo;

@RestController
@RequestMapping("/search")
@RequiredArgsConstructor
public class SearchController {

private final SearchService searchService;

// 태그 기반으로 검색
@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()) {
throw TagNotFoundException.EXCEPTION;
}

return ResponseEntity.ok(searchResults);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package playlist.server.search.service;


import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import playlist.server.domain.domains.search.repository.SearchRepository;
import playlist.server.search.vo.SearchVo;

@Service
@RequiredArgsConstructor
public class SearchService {

private final SearchRepository searchRepository;

public List<SearchVo> searchByTag(String tag) {
return searchRepository.findByTag(tag).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());
}
}
14 changes: 14 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,14 @@
package playlist.server.search.vo;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SearchVo {
private String title;
private String description;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package playlist.server.exception;

public class DataFetchException extends BaseException {

public static final BaseException EXCEPTION = new DataFetchException();

public DataFetchException() {
super(GlobalException.DATA_FETCH_ERROR);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ public enum GlobalException implements BaseErrorCode {
EXPIRED_REFRESH_TOKEN_ERROR(UNAUTHORIZED.value(), "401-1", "리프레시 토큰이 만료되었습니다."),
INVALID_TOKEN_ERROR(UNAUTHORIZED.value(), "401-2", "올바르지 않은 토큰입니다."),
DATE_FORMAT_ERROR(BAD_REQUEST.value(), "400-2", "날짜 형식을 확인해주세요."),
;
LIKE_INCREMENT_ERROR(INTERNAL_SERVER_ERROR.value(), "500-3", "좋아요 증가 실패"),
VIEW_INCREMENT_ERROR(INTERNAL_SERVER_ERROR.value(), "500-3", "조회수 증가 실패"),
TAG_NOT_FOUND(BAD_REQUEST.value(), "400-3", "태그를 찾을 수 없습니다."),
INVALID_PARAMETER_ERROR(BAD_REQUEST.value(), "400-4", "유효하지 않은 파라미터입니다."),
DATA_FETCH_ERROR(INTERNAL_SERVER_ERROR.value(), "500-4", "데이터를 가져오는데 실패하였습니다.");

private final Integer statusCode;
private final String errorCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package playlist.server.exception;

public class InvalidParameterException extends BaseException {

public static final BaseException EXCEPTION = new InvalidParameterException();

public InvalidParameterException() {
super(GlobalException.INVALID_PARAMETER_ERROR);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package playlist.server.exception;

public class LikeIncrementException extends BaseException {

public static final BaseException EXCEPTION = new LikeIncrementException();

public LikeIncrementException() {
super(GlobalException.LIKE_INCREMENT_ERROR);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package playlist.server.exception;

import playlist.server.exception.BaseErrorCode;
import playlist.server.exception.BaseException;
import static org.springframework.http.HttpStatus.NOT_FOUND;

public class TagNotFoundException extends BaseException {

public static final BaseException EXCEPTION = new TagNotFoundException();

private TagNotFoundException() {
super(GlobalException.TAG_NOT_FOUND);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package playlist.server.exception;

public class ViewIncrementException extends BaseException {

public static final BaseException EXCEPTION = new ViewIncrementException();

public ViewIncrementException() {
super(GlobalException.VIEW_INCREMENT_ERROR);
}
}
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 RankingInfo {
DAILY("daily_like", "daily_view"),
WEEK("week_like", "week_view"),
MONTH("month_like", "month_view");

private final String likeKey;
private final String viewKey;
}
Loading