From beab617652315b0e0331a99b62f720716df9ba6b Mon Sep 17 00:00:00 2001 From: Pak Su Hyung Date: Wed, 11 Dec 2024 13:38:47 +0900 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=EC=B6=94=EC=B2=9C=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=EB=A1=9C=20=EB=B6=80=ED=84=B0=20=ED=94=BD=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20API=20=EA=B5=AC=ED=98=84=20(#737)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 픽 존재여부 확인을 위한 메소드 구현 * feat: 폴더 id로 조회 메소드 구현 * feat: 추천 링크로 픽 생성 api 구현 * refactor: 컨트롤러 응답 타입 수정 * feat: 응답 타입에서 부모 폴더 정보 제거 * feat: 픽 추가시 이벤트 발행 --- .../pick/controller/PickApiController.java | 42 +++++++++++++++---- .../application/pick/dto/PickApiResponse.java | 7 +++- .../domain/folder/service/FolderService.java | 9 +++- .../api/domain/pick/service/PickService.java | 42 +++++++++++-------- 4 files changed, 74 insertions(+), 26 deletions(-) diff --git a/backend/techpick-api/src/main/java/techpick/api/application/pick/controller/PickApiController.java b/backend/techpick-api/src/main/java/techpick/api/application/pick/controller/PickApiController.java index 3d2da72b6..34d5f96a8 100644 --- a/backend/techpick-api/src/main/java/techpick/api/application/pick/controller/PickApiController.java +++ b/backend/techpick-api/src/main/java/techpick/api/application/pick/controller/PickApiController.java @@ -29,6 +29,7 @@ import techpick.api.application.pick.dto.PickApiRequest; import techpick.api.application.pick.dto.PickApiResponse; import techpick.api.application.pick.dto.PickSliceResponse; +import techpick.api.domain.folder.service.FolderService; import techpick.api.domain.pick.dto.PickResult; import techpick.api.domain.pick.exception.ApiPickException; import techpick.api.domain.pick.service.PickBulkService; @@ -50,6 +51,7 @@ public class PickApiController { private final PickSearchService pickSearchService; private final PickBulkService pickBulkService; private final EventMessenger eventMessenger; + private final FolderService folderService; @GetMapping @Operation(summary = "폴더 리스트 내 픽 리스트 조회", description = "해당 폴더 리스트 각각의 픽 리스트를 조회합니다.") @@ -65,8 +67,8 @@ public ResponseEntity> getFold ); return ResponseEntity.ok( folderPickList.stream() - .map(pickApiMapper::toApiFolderPickListWithViewCount) - .toList()); + .map(pickApiMapper::toApiFolderPickListWithViewCount) + .toList()); } @GetMapping("/search") @@ -112,8 +114,8 @@ public ResponseEntity> searchPick( pickApiMapper.toSearchCommand(userId, request)); List pickResponseList = pickList.stream() - .map(pickApiMapper::toApiResponse) - .toList(); + .map(pickApiMapper::toApiResponse) + .toList(); return ResponseEntity.ok(pickResponseList); } @@ -154,9 +156,9 @@ public ResponseEntity doesUserHasPickWithGivenUrl( @LoginUserId Long userId, @RequestParam String link ) { var response = pickService.findPickUrl(userId, link) - .map(pickApiMapper::toApiResponse) - .map(pick -> new PickApiResponse.PickExists(true, pick)) - .orElseGet(() -> new PickApiResponse.PickExists(false, null)); + .map(pickApiMapper::toApiResponse) + .map(pick -> new PickApiResponse.PickExists(true, pick)) + .orElseGet(() -> new PickApiResponse.PickExists(false, null)); return ResponseEntity.ok(response); } @@ -199,6 +201,32 @@ public ResponseEntity savePick(@LoginUserId Long userId, return ResponseEntity.ok(response); } + @PostMapping("/recommend") + @Operation( + summary = "추천 링크로 픽 생성", + description = "추천 링크로 픽을 생성합니다. 이미 픽으로 등록된 링크의 경우 기존 픽 정보를 응답으로 보냅니다." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "픽 생성 성공"), + @ApiResponse(responseCode = "403", description = "접근할 수 없는 폴더") + }) + public ResponseEntity savePickFromRecommend(@LoginUserId Long userId, + @Valid @RequestBody PickApiRequest.Create request) { + boolean existPick; + PickResult.Pick result; + if (pickService.existPickByUrl(userId, request.linkInfo().url())) { + existPick = true; + result = pickService.getPickUrl(userId, request.linkInfo().url()); + } else { + existPick = false; + var command = pickApiMapper.toCreateCommand(userId, request); + result = pickService.saveNewPick(command); + } + var event = new PickCreateEvent(userId, result.id(), result.linkInfo().url()); + eventMessenger.send(event); + return ResponseEntity.ok(new PickApiResponse.CreateFromRecommend(existPick, result)); + } + @PatchMapping @Operation(summary = "픽 내용 수정", description = "픽 내용(제목, 메모)을 수정합니다.") @ApiResponses(value = { diff --git a/backend/techpick-api/src/main/java/techpick/api/application/pick/dto/PickApiResponse.java b/backend/techpick-api/src/main/java/techpick/api/application/pick/dto/PickApiResponse.java index c0f5723fd..96ed86472 100644 --- a/backend/techpick-api/src/main/java/techpick/api/application/pick/dto/PickApiResponse.java +++ b/backend/techpick-api/src/main/java/techpick/api/application/pick/dto/PickApiResponse.java @@ -4,7 +4,6 @@ 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; @@ -53,4 +52,10 @@ public record PickExists( PickApiResponse.Pick pick ) { } + + public record CreateFromRecommend( + boolean exist, + PickResult.Pick pick + ) { + } } diff --git a/backend/techpick-api/src/main/java/techpick/api/domain/folder/service/FolderService.java b/backend/techpick-api/src/main/java/techpick/api/domain/folder/service/FolderService.java index 825a618d5..c15815fd6 100644 --- a/backend/techpick-api/src/main/java/techpick/api/domain/folder/service/FolderService.java +++ b/backend/techpick-api/src/main/java/techpick/api/domain/folder/service/FolderService.java @@ -13,12 +13,12 @@ import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; +import techpick.api.annotation.LoginUserIdDistributedLock; import techpick.api.domain.folder.dto.FolderCommand; import techpick.api.domain.folder.dto.FolderMapper; import techpick.api.domain.folder.dto.FolderResult; import techpick.api.domain.folder.exception.ApiFolderException; import techpick.api.infrastructure.folder.FolderDataHandler; -import techpick.api.annotation.LoginUserIdDistributedLock; import techpick.api.infrastructure.pick.PickDataHandler; import techpick.api.infrastructure.sharedFolder.SharedFolderDataHandler; import techpick.core.model.folder.Folder; @@ -62,6 +62,13 @@ public List getBasicFolderList(Long userId) { .toList(); } + @Transactional(readOnly = true) + public FolderResult getFolder(Long userId, Long folderId) { + Folder folder = folderDataHandler.getFolder(folderId); + validateFolderAccess(userId, folder); + return folderMapper.toResult(folder); + } + /** * 생성하려는 폴더가 미분류폴더, 휴지통이 아닌지 검증합니다. * */ diff --git a/backend/techpick-api/src/main/java/techpick/api/domain/pick/service/PickService.java b/backend/techpick-api/src/main/java/techpick/api/domain/pick/service/PickService.java index dfa4a066c..4103223a6 100644 --- a/backend/techpick-api/src/main/java/techpick/api/domain/pick/service/PickService.java +++ b/backend/techpick-api/src/main/java/techpick/api/domain/pick/service/PickService.java @@ -22,11 +22,13 @@ import techpick.api.domain.ranking.service.RankingService; import techpick.api.domain.tag.exception.ApiTagException; import techpick.api.infrastructure.folder.FolderDataHandler; +import techpick.api.infrastructure.link.LinkDataHandler; import techpick.api.infrastructure.pick.PickDataHandler; import techpick.api.infrastructure.tag.TagDataHandler; import techpick.core.dto.UrlWithCount; import techpick.core.model.folder.Folder; import techpick.core.model.folder.FolderType; +import techpick.core.model.link.Link; import techpick.core.model.pick.Pick; import techpick.core.model.tag.Tag; @@ -40,6 +42,13 @@ public class PickService { private final PickMapper pickMapper; private final FolderDataHandler folderDataHandler; private final RankingService rankingService; + private final LinkDataHandler linkDataHandler; + + @Transactional(readOnly = true) + public boolean existPickByUrl(Long userId, String url) { + Link link = linkDataHandler.getLink(url); + return pickDataHandler.existsByUserIdAndLink(userId, link); + } @Transactional(readOnly = true) public PickResult.Pick getPick(PickCommand.Read command) { @@ -51,7 +60,6 @@ public PickResult.Pick getPick(PickCommand.Read command) { @Transactional(readOnly = true) public PickResult.Pick getPickUrl(Long userId, String url) { Pick pick = pickDataHandler.getPickUrl(userId, url); - validatePickAccess(userId, pick.getId()); return pickMapper.toPickResult(pick); } @@ -69,17 +77,17 @@ public List getFolderChildPickList(Long userId, Long folderId) List pickList = pickDataHandler.getPickListPreservingOrder(folder.getChildPickIdOrderedList()); return pickList.stream() - .map(pickMapper::toPickResult) - .toList(); + .map(pickMapper::toPickResult) + .toList(); } // 폴더 리스트가 넘어오면, 각 폴더 내부에 있는 픽 리스트 조회 @Transactional(readOnly = true) public List getFolderListChildPickList(PickCommand.ReadList command) { return command.folderIdList().stream() - .peek(folderId -> validateFolderAccess(command.userId(), folderId)) // 폴더 접근 유효성 검사 - .map(this::getFolderChildPickResultList) - .toList(); + .peek(folderId -> validateFolderAccess(command.userId(), folderId)) // 폴더 접근 유효성 검사 + .map(this::getFolderChildPickResultList) + .toList(); } @LoginUserIdDistributedLock @@ -141,20 +149,20 @@ private PickResult.FolderPickWithViewCountList getFolderChildPickResultList(Long // 여기서 pick 주간 인기 데이터 반환 Map viewCountMap = rankingService.getUrlRanking(10) - .weeklyUrlViewRanking().stream() - .collect(Collectors.toMap(UrlWithCount::url, Function.identity())); + .weeklyUrlViewRanking().stream() + .collect(Collectors.toMap(UrlWithCount::url, Function.identity())); List pickResultList = pickList.stream() - .map(pickMapper::toPickResult) - .map(pickResult -> { - var urlWithCount = viewCountMap.get(pickResult.linkInfo().url()); - if (Objects.isNull(urlWithCount)) { - return pickMapper.toPickResultWithViewCount(pickResult, false, null); - } - return pickMapper.toPickResultWithViewCount(pickResult, true, urlWithCount.count()); - }) - .toList(); + .map(pickMapper::toPickResult) + .map(pickResult -> { + var urlWithCount = viewCountMap.get(pickResult.linkInfo().url()); + if (Objects.isNull(urlWithCount)) { + return pickMapper.toPickResultWithViewCount(pickResult, false, null); + } + return pickMapper.toPickResultWithViewCount(pickResult, true, urlWithCount.count()); + }) + .toList(); return pickMapper.toPickResultList(folderId, pickResultList); }