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 3 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.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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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.mainpagerankingInfo.vo.MainPageRankingInfoVo;
import playlist.server.ranking.service.RankingService;

@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<MainPageRankingInfoVo> getDailyRanking(
@RequestParam(name = "type", defaultValue = "like") String type
) {
try {
MainPageRankingInfoVo rankingInfoVo = rankingService.getRanking("daily", type, "boardId");
if (rankingInfoVo == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(rankingInfoVo);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}

@Operation(summary = "주간 랭킹을 조회합니다.")
@GetMapping("/weekly")
public ResponseEntity<MainPageRankingInfoVo> getWeeklyRanking(
@RequestParam(name = "type", defaultValue = "like") String type
) {
try {
MainPageRankingInfoVo rankingInfoVo = rankingService.getRanking("weekly", type, "boardId");
if (rankingInfoVo == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(rankingInfoVo);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
Copy link
Member

Choose a reason for hiding this comment

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

다른메소드 동일하지만 service레벨에서 Exception을 내주고 있는것 같아서 badRequest Return의 경우에는 발생시키는 이유를 모르겠네요.

코드를 이런식으로 만들어 볼 수 있지않을까요?

Suggested change
try {
MainPageRankingInfoVo rankingInfoVo = rankingService.getRanking("weekly", type, "boardId");
if (rankingInfoVo == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(rankingInfoVo);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
MainPageRankingInfoVo rankingInfoVo = rankingService.getRanking(RankingInfo.WEEKLY, type, "boardId");
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.

두번째 인자값인 type의 경우에도 enum으로 정의가 가능할 것 같아요.
세번째 인자값인 boardId는 차라리 어딘가에 public static String으로 변수선언한 이후에 해당 값을 모두 공유해서 사용하면
추후 변경이 발생시 편하게 공유할 수 있지 않을까요?

Copy link
Author

Choose a reason for hiding this comment

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

boardId 이거는 못했습니다..두번째 인자값인 type의 경우에도 enum으로 정의가 가능할 것 같아요. 이건 했습니다!

}

@Operation(summary = "월간 랭킹을 조회합니다.")
@GetMapping("/monthly")
public ResponseEntity<MainPageRankingInfoVo> getMonthlyRanking(
@RequestParam(name = "type", defaultValue = "like") String type
) {
try {
MainPageRankingInfoVo rankingInfoVo = rankingService.getRanking("monthly", type, "boardId");
if (rankingInfoVo == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(rankingInfoVo);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package playlist.server.ranking.service;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class RankingLikeService {

private final RedisTemplate<String, String> redisTemplate;

public void incrementLikes(String rankingType, String boardId) {
try {
String countsKey = rankingType + "_LIKE_COUNTS";
redisTemplate.opsForHash().increment(countsKey, boardId, 1L);
} catch (Exception e) {
throw new RuntimeException("좋아요 증가 실패");
Copy link
Member

Choose a reason for hiding this comment

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

RuntimeException의 사용은 우리 프로젝트에서 사용을 하면 안됩니다.
Handling은 모두 BaseException을 활용하고 있기 때문에, BaseException을 상속받아 Exception을 Custom하게 제작하여 사용해 주시기 바랍니다 :)

그래도 Exception을 선언해서 안전한 개발방식을 만든건 좋네요 👍

Copy link
Author

Choose a reason for hiding this comment

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

LikeIncrementException 완료!

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

import org.springframework.stereotype.Service;
import playlist.server.domain.domains.ranking.domain.RankingInfo;
import playlist.server.domain.domains.ranking.domain.RankingType;
import playlist.server.mainpagerankingInfo.vo.MainPageRankingInfoVo;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class RankingService {

private final RankingLikeService rankingLikeService;
private final RankingViewService rankingViewService;

public MainPageRankingInfoVo getRanking(String rankingType, String type, String boardId) {
try {
RankingInfo rankingInfo;
if ("daily".equalsIgnoreCase(rankingType)) {
rankingInfo = RankingInfo.DAILY;
} else if ("weekly".equalsIgnoreCase(rankingType)) {
rankingInfo = RankingInfo.WEEKLY;
} else if ("monthly".equalsIgnoreCase(rankingType)) {
rankingInfo = RankingInfo.MONTHLY;
} else {
throw new IllegalArgumentException("유효하지 않은 랭킹 타입");
}
Copy link
Member

Choose a reason for hiding this comment

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

Controller레벨에서 rankingType을 문자열로 주지않고, Enum타입을 주면 해당 코드도 필요 없지 않을까요?

그래도 서비스레이어에서 모든 로직처리하는건 괜찮아 보이네요.


if ("like".equals(type)) {
rankingLikeService.incrementLikes(rankingType, boardId);
} else if ("view".equals(type)) {
rankingViewService.incrementViews(rankingType, boardId);
Copy link
Member

Choose a reason for hiding this comment

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

like와 view도 enum으로 정의해보는게 좋을것 같아요

Copy link
Author

Choose a reason for hiding this comment

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

완료!

} else {
throw new IllegalArgumentException("유효하지 않은 파라미터");
}

return MainPageRankingInfoVo.from(rankingInfo, RankingType.valueOf(rankingType.toUpperCase()));
Copy link
Member

Choose a reason for hiding this comment

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

혹시 Enum만 들어있는 정보를 Return하는 이유가 있을까요?
생각해보니 랭킹정보가 들어있는 리스트를 Return하는게 존재하지 않네요

Copy link
Author

Choose a reason for hiding this comment

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

한 번 해봤습니다! 근데

getLikeRankingInfoList
getViewRankingInfoList

이거에 대한 레디스에서 가져오는 로직을 잘 모르겠어서

public List getLikeRankingInfoList() {

    return Collections.emptyList();
}

이런식으로만 일단 해봤습니다!

} catch (Exception e) {
throw new RuntimeException("데이터 가져오는거 실패");
Copy link
Member

Choose a reason for hiding this comment

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

Custom하게 제작한 Exception을 활용해주세요 👍

Copy link
Author

Choose a reason for hiding this comment

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

DataFetchException, InvalidParameterException 완료!

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class RankingViewService {

private final RedisTemplate<String, String> redisTemplate;

public void incrementViews(String rankingType, String boardId) {
try {
String countsKey = rankingType + "_VIEW_COUNTS";
redisTemplate.opsForHash().increment(countsKey, boardId, 1L);
} catch (Exception e) {
throw new RuntimeException("조회수 증가 실패");
Copy link
Member

Choose a reason for hiding this comment

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

Exception!

Copy link
Author

Choose a reason for hiding this comment

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

ViewIncrementException 완료!

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

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.search.service.SearchService;
import playlist.server.search.vo.SearchVo;

import java.util.List;

@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 new IllegalArgumentException("해당 태그가 없다");
Copy link
Member

Choose a reason for hiding this comment

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

Exception!

Copy link
Author

Choose a reason for hiding this comment

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

TagNotFoundException 했습니다!

}

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

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

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

import lombok.RequiredArgsConstructor;

@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());
}
}
13 changes: 13 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,13 @@
package playlist.server.search.vo;

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

@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,15 @@
package playlist.server.domain.domains.ranking.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
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.

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

}
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,24 @@
package playlist.server.domain.domains.search.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Search {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String title;
private String description;
private String tag;
Comment on lines +12 to +24
Copy link
Member

Choose a reason for hiding this comment

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

Search의 경우에는 다른 팀이 직접 구현한 엔티티를 활용해야 하다보니, Search Entity가 필요한 이유가 납득이 가지 않네용.

왜 생성한 것인지 의도를 풀어줄 수 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

다른 팀이 직접 구현한 엔티티를 활용해야 하다보니 사실 이걸 생각한다는 걸 생각하긴 했는데 , 한 번 Search 관련 Api를 쭉 만들어보고 싶어서 해봤습니다!

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

import org.springframework.data.jpa.repository.JpaRepository;
import playlist.server.domain.domains.search.domain.Search;
import java.util.List;

public interface SearchRepository extends JpaRepository<Search, Long> {
List<Search> findByTag(String tag);
}