Skip to content

Commit

Permalink
[FEAT] 랭킹 정보를 픽 리스트에서 함께 표시 (#712)
Browse files Browse the repository at this point in the history
  • Loading branch information
kimminkyeu authored Dec 9, 2024
1 parent b4aa9b9 commit 50621f2
Show file tree
Hide file tree
Showing 16 changed files with 228 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,30 @@

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import techpick.api.domain.link.dto.LinkInfo;
import techpick.api.application.event.dto.EventApiRequest;
import techpick.api.domain.link.service.LinkService;
import techpick.core.event.EventMessenger;
import techpick.core.event.events.PickViewEvent;
import techpick.core.event.events.SharedFolderLinkViewEvent;
import techpick.security.annotation.LoginUserId;

@RestController
@Tag(name = "이벤트 수집 API", description = "발생하는 이벤트를 이곳으로 보내줘야 집계에 반영됩니다.")
@RequiredArgsConstructor
@RequestMapping("/api/event")
public class EventController {
@RequestMapping("/api/events")
@Tag(name = "이벤트 수집 API", description = "발생하는 이벤트를 이곳으로 보내줘야 집계에 반영됩니다.")
public class EventApiController {

private final EventMessenger eventMessenger;
private final LinkService linkService;
Expand All @@ -31,17 +35,19 @@ public class EventController {
* [사용자 인증 정보가 필요한 api]
* 내가 나의 북마크를 클릭했을 때 프론트엔드가 보내는 이벤트
*/
@PostMapping("/pick/view")
@PostMapping("/picks/view")
@Operation(
summary = "사용자 자신의 픽 조회 이벤트 수집",
description = "[로그인 필요] 서버에게 사용자 자신의 북마크 조회를 알립니다."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "전송 성공")
})
public ResponseEntity<Void> bookmarkView(
@Parameter(description = "조회되는 픽의 링크 url") @RequestParam String url,
@Parameter(description = "조회되는 픽의 id") @RequestParam Long pickId,
@Valid @RequestBody EventApiRequest.BookmarkView request,
@LoginUserId Long userId
) {
eventMessenger.send(new PickViewEvent(userId, pickId, url));
eventMessenger.send(new PickViewEvent(userId, request.pickId(), request.url()));
return ResponseEntity.noContent().build();
}

Expand All @@ -55,12 +61,14 @@ public ResponseEntity<Void> bookmarkView(
summary = "공개 폴더의 북마크 조회 이벤트 수집",
description = "[인증 불필요] 서버에게 공개 폴더의 어떤 북마크가 조회됬는지 알립니다."
)
public ResponseEntity<LinkInfo> sharedFolderLinkView(
@Parameter(description = "조회된 링크 url") @RequestParam String url,
@Parameter(description = "조회된 공개 폴더 접근용 토큰") @RequestParam String folderAccessToken
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "전송 성공")
})
public ResponseEntity<Void> sharedFolderLinkView(
@Valid @RequestBody EventApiRequest.SharedBookmarkView request
) {
var linkInfo = linkService.getLinkInfo(url); // 서버에 링크 엔티티가 존재해야 이벤트 전송 가능
eventMessenger.send(new SharedFolderLinkViewEvent(url, folderAccessToken));
return ResponseEntity.ok(linkInfo);
var linkInfo = linkService.getLinkInfo(request.url()); // 서버에 링크 엔티티가 존재해야 이벤트 전송 가능
eventMessenger.send(new SharedFolderLinkViewEvent(request.url(), request.folderAccessToken()));
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package techpick.api.application.event.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

public class EventApiRequest {

public record BookmarkView(
@NotNull @Schema(description = "조회된 링크 url") String url,
@NotNull @Schema(description = "조회되는 픽의 id") Long pickId
) {
}

public record SharedBookmarkView(
@NotNull @Schema(description = "조회된 링크 url") String url,
@NotNull @Schema(description = "조회된 공개 폴더 접근용 토큰") String folderAccessToken
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,16 @@ public class PickApiController {
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "픽 리스트 조회 성공")
})
public ResponseEntity<List<PickApiResponse.FolderPickList>> getFolderChildPickList(
public ResponseEntity<List<PickApiResponse.FolderPickListWithViewCount>> getFolderChildPickList(
@LoginUserId Long userId,
@Parameter(description = "조회할 폴더 ID 목록", example = "1, 2, 3") @RequestParam(required = false, defaultValue =
"") List<Long> folderIdList) {
List<PickResult.FolderPickList> folderPickList = pickService.getFolderListChildPickList(
pickApiMapper.toReadListCommand(userId, folderIdList));

var folderPickList = pickService.getFolderListChildPickList(
pickApiMapper.toReadListCommand(userId, folderIdList)
);
return ResponseEntity.ok(
folderPickList.stream()
.map(pickApiMapper::toApiFolderPickList)
.map(pickApiMapper::toApiFolderPickListWithViewCount)
.toList());
}

Expand Down Expand Up @@ -151,8 +151,15 @@ public ResponseEntity<PickApiResponse.PickExists> doesUserHasPickWithGivenUrl(
return ResponseEntity.ok(response);
}

@Deprecated
@GetMapping("/{id}")
@Operation(summary = "픽 상세 조회", description = "픽을 상세 조회합니다.")
@Operation(
summary = "[Deprecated] 픽 상세 조회",
description = """
현재 사용되지 않는 api 입니다.
추후 코드 제거 예정입니다.
"""
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "픽 상세 조회 성공")
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ PickCommand.SearchPagination toSearchPaginationCommand(Long userId, List<Long> f

PickCommand.Delete toDeleteCommand(Long userId, PickApiRequest.Delete request);

PickApiResponse.PickWithViewCount toApiResponseWithPickViewCount(PickResult.PickWithViewCount pickResult);

PickApiResponse.Pick toApiResponse(PickResult.Pick pickResult);

@Mapping(target = "pickList", source = "pickList", qualifiedByName = "mapPickList")
Expand All @@ -45,14 +47,26 @@ PickCommand.SearchPagination toSearchPaginationCommand(Long userId, List<Long> f
@Named("mapPickList")
default List<PickApiResponse.Pick> mapPickList(List<PickResult.Pick> pickList) {
return pickList.stream()
.map(this::toApiResponse)
.toList();
.map(this::toApiResponse)
.toList();
}

@Mapping(target = "pickList", source = "pickList", qualifiedByName = "mapPickListWithViewCount")
PickApiResponse.FolderPickListWithViewCount toApiFolderPickListWithViewCount(
PickResult.FolderPickWithViewCountList folderPickList);

@Named("mapPickListWithViewCount")
default List<PickApiResponse.PickWithViewCount> mapPickListWithViewCount(
List<PickResult.PickWithViewCount> pickList) {
return pickList.stream()
.map(this::toApiResponseWithPickViewCount)
.toList();
}

default Slice<PickApiResponse.Pick> toSliceApiResponse(Slice<PickResult.Pick> source) {
List<PickApiResponse.Pick> convertedContent = source.getContent().stream()
.map(this::toApiResponse)
.toList();
.map(this::toApiResponse)
.toList();
return new SliceImpl<>(convertedContent, source.getPageable(), source.hasNext());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.time.LocalDateTime;
import java.util.List;

import jakarta.validation.constraints.NotNull;
import lombok.NonNull;
import techpick.api.domain.link.dto.LinkInfo;
import techpick.api.domain.pick.dto.PickResult;

Expand All @@ -19,14 +21,35 @@ public record Pick(
) {
}

public record PickWithViewCount(
Long id,
String title,
LinkInfo linkInfo,
Long parentFolderId,
List<Long> tagIdOrderedList,
LocalDateTime createdAt,
LocalDateTime updatedAt,
// 프론트엔드에서 깔끔하게 처리하기 위한 힌트
@NotNull Boolean isHot,
// 랭킹 정보에 표시된 최근 7일간 조회수
Long weeklyViewCount
) {
}

public record FolderPickList(
Long folderId,
List<PickApiResponse.Pick> pickList
) {
}

public record FolderPickListWithViewCount(
Long folderId,
List<PickApiResponse.PickWithViewCount> pickList
) {
}

public record PickExists(
Boolean exist,
@NotNull Boolean exist,
PickApiResponse.Pick pick
) {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package techpick.api.application.ranking.controller;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
Expand All @@ -16,12 +15,12 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import techpick.api.application.ranking.dto.LinkInfoWithViewCount;
import techpick.api.application.ranking.dto.LinkInfoWithCount;
import techpick.api.application.ranking.dto.RankingApiMapper;
import techpick.api.application.ranking.dto.RankingResponse;
import techpick.api.domain.link.exception.ApiLinkException;
import techpick.api.domain.link.service.LinkService;
import techpick.api.infrastructure.ranking.RankingApi;
import techpick.api.application.ranking.dto.RankingByViewCount;
import techpick.api.domain.ranking.service.RankingService;
import techpick.core.dto.UrlWithCount;

/**
Expand All @@ -34,9 +33,9 @@
@Tag(name = "추천/소개 API", description = "링크, 픽 등에 대한 소개")
public class RankingApiController {

private final RankingApi rankingApi;
private final RankingApiMapper rankingApiMapper;
private final LinkService linkService;
private final RankingService rankingService;

/**
* 주별, 일별 조회 수를 기반 으로 추천 한다.
Expand All @@ -55,34 +54,27 @@ public class RankingApiController {
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "조회 성공")
})
public ResponseEntity<RankingByViewCount> getSuggestionByViewCount(
public ResponseEntity<RankingResponse> getSuggestionByViewCount(
) {
Integer LIMIT = 10;
var currentDay = LocalDate.now();
var before1Day = currentDay.minusDays(1);
var before7Days = currentDay.minusDays(7);
var before30Days = currentDay.minusDays(30);

var dailyViewRanking = // 오늘 + 어제
mapToLinkInfoRanking(rankingApi.getUrlRankingByViewCount(before1Day, currentDay, LIMIT).getBody());

var past7DaysViewRanking = // 일주일 전 ~ 어제
mapToLinkInfoRanking(rankingApi.getUrlRankingByViewCount(before7Days, before1Day, LIMIT).getBody());

var past30DaysPickRanking = // 한달 전 ~ 어제
mapToLinkInfoRanking(
rankingApi.getUrlRankingByPickedCount(before30Days, before1Day, LIMIT).getBody());

var response = new RankingByViewCount(dailyViewRanking, past7DaysViewRanking, past30DaysPickRanking);
int LIMIT = 10;
var result = rankingService.getUrlRanking(LIMIT);
var response = new RankingResponse(
urlToLinkInfo(result.dailyUrlViewRanking()),
urlToLinkInfo(result.weeklyUrlViewRanking()),
urlToLinkInfo(result.monthlyUrlPickRanking())
);
return ResponseEntity.ok(response);
}

private List<LinkInfoWithViewCount> mapToLinkInfoRanking(List<UrlWithCount> urlWithCountList) {
if (Objects.isNull(urlWithCountList)) {
/**
* Url만 명시된 랭킹을 Og-Data가 포함된 랭킹 정보로 변환
*/
private List<LinkInfoWithCount> urlToLinkInfo(List<UrlWithCount> urlRanking) {
if (Objects.isNull(urlRanking)) {
return List.of(/* empty list */);
}
var result = new ArrayList<LinkInfoWithViewCount>();
for (UrlWithCount urlWithCount : urlWithCountList) {
var result = new ArrayList<LinkInfoWithCount>();
for (UrlWithCount urlWithCount : urlRanking) {
try {
var linkInfo = linkService.getLinkInfo(urlWithCount.url());
var rankingInfo = rankingApiMapper.toRankingWithLinkInfo(urlWithCount, linkInfo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/**
* 랭킹 서버로 부터 얻은 URL 정보 {@link UrlWithCount} 에 Opengraph Tag를 추가한 DTO
*/
public record LinkInfoWithViewCount(
public record LinkInfoWithCount(
@NotNull String url,
String title,
String description,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@
public interface RankingApiMapper {

@Mapping(target = "url", source = "urlWithCount.url")
LinkInfoWithViewCount toRankingWithLinkInfo(UrlWithCount urlWithCount, LinkInfo linkInfo);
LinkInfoWithCount toRankingWithLinkInfo(UrlWithCount urlWithCount, LinkInfo linkInfo);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

import io.swagger.v3.oas.annotations.media.Schema;

public record RankingByViewCount(
public record RankingResponse(
@Schema(description = "오늘 하루 동안 인기 있는 링크 Top 10")
List<LinkInfoWithViewCount> dailyViewRanking,
List<LinkInfoWithCount> dailyViewRanking,

@Schema(description = "지난 7일동안 링크 조회 수 Top 10")
List<LinkInfoWithViewCount> weeklyViewRanking,
List<LinkInfoWithCount> weeklyViewRanking,

@Schema(description = "지난 30일동안 링크가 픽된 횟수 Top 10")
List<LinkInfoWithViewCount> monthlyPickRanking
List<LinkInfoWithCount> monthlyPickRanking
) {
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package techpick.api.domain.link.dto;

import java.util.List;

import org.mapstruct.InjectionStrategy;
import org.mapstruct.IterableMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.ReportingPolicy;

import techpick.core.model.link.Link;
Expand All @@ -22,5 +26,9 @@ public interface LinkMapper {

LinkInfo of(Link link);

@Named("E2R")
LinkInfo toLinkInfo(Link link);

@IterableMapping(qualifiedByName = "E2R")
List<LinkInfo> toLinkInfoList(List<Link> links);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import techpick.core.model.folder.Folder;
import techpick.core.model.link.Link;
import techpick.core.model.pick.Pick;
import techpick.core.model.pick.PickRepository;
import techpick.core.model.user.User;

@Mapper(
Expand All @@ -23,9 +24,12 @@ public interface PickMapper {
@Mapping(source = "pick.parentFolder.id", target = "parentFolderId")
PickResult.Pick toPickResult(Pick pick);

PickResult.PickWithViewCount toPickResultWithViewCount(PickResult.Pick pickResult, Boolean isHot,
Long weeklyViewCount);

@Mapping(source = "folderId", target = "folderId")
@Mapping(source = "pick", target = "pickList")
PickResult.FolderPickList toPickResultList(Long folderId, List<PickResult.Pick> pick);
PickResult.FolderPickWithViewCountList toPickResultList(Long folderId, List<PickResult.PickWithViewCount> pick);

@Mapping(source = "command.title", target = "title")
@Mapping(source = "parentFolder", target = "parentFolder")
Expand Down
Loading

0 comments on commit 50621f2

Please sign in to comment.