diff --git a/src/main/java/leets/bookmark/domain/bookmark/application/dto/response/BookmarkFullResponse.java b/src/main/java/leets/bookmark/domain/bookmark/application/dto/response/BookmarkFullResponse.java new file mode 100644 index 00000000..9974e186 --- /dev/null +++ b/src/main/java/leets/bookmark/domain/bookmark/application/dto/response/BookmarkFullResponse.java @@ -0,0 +1,24 @@ +package leets.bookmark.domain.bookmark.application.dto.response; + +import leets.bookmark.domain.file.application.dto.response.FileResponse; +import leets.bookmark.domain.notification.application.dto.response.NotificationResponse; +import lombok.Builder; + +import java.time.LocalDateTime; +import java.util.List; + +@Builder +public record BookmarkFullResponse( + Long id, + String url, + String title, + String memo, + String platform, + String faviconUrl, + List categoryTagInfos, + FileResponse file, + NotificationResponse notificationResponse, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { +} diff --git a/src/main/java/leets/bookmark/domain/bookmark/application/mapper/BookmarkMapper.java b/src/main/java/leets/bookmark/domain/bookmark/application/mapper/BookmarkMapper.java index 8a09a2ad..e6e25dac 100644 --- a/src/main/java/leets/bookmark/domain/bookmark/application/mapper/BookmarkMapper.java +++ b/src/main/java/leets/bookmark/domain/bookmark/application/mapper/BookmarkMapper.java @@ -1,12 +1,13 @@ package leets.bookmark.domain.bookmark.application.mapper; import leets.bookmark.domain.bookmark.application.dto.request.BookmarkSaveRequest; +import leets.bookmark.domain.bookmark.application.dto.response.BookmarkFullResponse; import leets.bookmark.domain.bookmark.application.dto.response.BookmarkResponse; import leets.bookmark.domain.bookmark.domain.entity.Bookmark; import leets.bookmark.domain.bookmark.application.dto.response.BookmarkTagInfoResponse; import leets.bookmark.domain.bookmark.domain.entity.BookmarkTagMapping; import leets.bookmark.domain.file.application.dto.response.FileResponse; -import leets.bookmark.domain.file.domain.entity.File; +import leets.bookmark.domain.notification.application.dto.response.NotificationResponse; import leets.bookmark.domain.tag.domain.entity.Tag; import leets.bookmark.domain.category.domain.entity.Category; @@ -111,4 +112,24 @@ public List toMappings(Bookmark bookmark, List tags) { .map(tag -> toMapping(bookmark, tag)) .toList(); } + + public BookmarkFullResponse toFullResponse(Bookmark bookmark, List bookmarkTagMappings, + FileResponse fileResponse, NotificationResponse notificationResponse) { + + BookmarkTagInfoResponse tagInfo = toBookmarkTagInfoResponseFromMappings(bookmarkTagMappings); + + return BookmarkFullResponse.builder() + .id(bookmark.getId()) + .url(bookmark.getUrl()) + .title(bookmark.getTitle()) + .memo(bookmark.getMemo()) + .platform(String.valueOf(bookmark.getPlatform())) + .faviconUrl(bookmark.getFaviconUrl()) + .categoryTagInfos(List.of(tagInfo)) + .file(fileResponse) + .notificationResponse(notificationResponse) + .createdAt(bookmark.getCreatedAt()) + .updatedAt(bookmark.getUpdatedAt()) + .build(); + } } diff --git a/src/main/java/leets/bookmark/domain/bookmark/application/usecase/BookmarkUseCase.java b/src/main/java/leets/bookmark/domain/bookmark/application/usecase/BookmarkUseCase.java index 00983f3d..5440d1c9 100644 --- a/src/main/java/leets/bookmark/domain/bookmark/application/usecase/BookmarkUseCase.java +++ b/src/main/java/leets/bookmark/domain/bookmark/application/usecase/BookmarkUseCase.java @@ -2,6 +2,7 @@ import leets.bookmark.domain.bookmark.application.dto.request.BookmarkSaveRequest; import leets.bookmark.domain.bookmark.application.dto.request.BookmarkUpdateRequest; +import leets.bookmark.domain.bookmark.application.dto.response.BookmarkFullResponse; import leets.bookmark.domain.bookmark.application.dto.response.BookmarkPreviewResponse; import leets.bookmark.domain.bookmark.application.dto.request.BookmarkSearchRequest; @@ -15,7 +16,7 @@ @Service public interface BookmarkUseCase { - Slice getFilteredBookmarks(Long userId, BookmarkSearchRequest request); + Slice getFilteredBookmarks(Long userId, BookmarkSearchRequest request); void delete(Long userId, Long bookmarkId); @@ -26,4 +27,6 @@ public interface BookmarkUseCase { List extractPreviewFromUrl(String url); List getAllPlatforms(Long userId); + + BookmarkFullResponse findBookmark(Long userId, Long bookmarkId); } diff --git a/src/main/java/leets/bookmark/domain/bookmark/application/usecase/BookmarkUseCaseImpl.java b/src/main/java/leets/bookmark/domain/bookmark/application/usecase/BookmarkUseCaseImpl.java index f77c7a16..6b47675a 100644 --- a/src/main/java/leets/bookmark/domain/bookmark/application/usecase/BookmarkUseCaseImpl.java +++ b/src/main/java/leets/bookmark/domain/bookmark/application/usecase/BookmarkUseCaseImpl.java @@ -8,6 +8,7 @@ import leets.bookmark.domain.bookmark.application.dto.request.BookmarkSearchCondition; import leets.bookmark.domain.bookmark.application.dto.request.CategoryTagRequest; +import leets.bookmark.domain.bookmark.application.dto.response.BookmarkFullResponse; import leets.bookmark.domain.bookmark.application.dto.response.BookmarkPreviewResponse; import leets.bookmark.domain.bookmark.application.dto.response.BookmarkPlatformResponse; import leets.bookmark.domain.bookmark.application.exception.TagCategoryMismatchException; @@ -23,6 +24,7 @@ import leets.bookmark.domain.file.application.dto.response.FileResponse; import leets.bookmark.domain.file.application.mapper.FileMapper; import leets.bookmark.domain.file.application.usecase.FileUseCase; +import leets.bookmark.domain.notification.application.dto.response.NotificationResponse; import leets.bookmark.domain.notification.domain.service.NotificationDeleteService; import leets.bookmark.domain.notification.domain.service.NotificationGetService; import leets.bookmark.domain.user.domain.entity.User; @@ -66,6 +68,7 @@ public class BookmarkUseCaseImpl implements BookmarkUseCase { private final FileUseCase fileUseCase; private final NotificationDeleteService notificationDeleteService; private final NotificationGetService notificationGetService; + private final NotificationUseCase notificationUseCase; private final FileMapper fileMapper; @@ -74,7 +77,20 @@ public class BookmarkUseCaseImpl implements BookmarkUseCase { @Override @Transactional(readOnly = true) - public Slice getFilteredBookmarks(Long userId, BookmarkSearchRequest request) { + public BookmarkFullResponse findBookmark(Long userId, Long bookmarkId){ + User user = userGetService.findById(userId); + Bookmark bookmark = bookmarkGetService.getBookmarkById(bookmarkId); + + validateBookmarkOwner(user.getId(), bookmark); + + FileResponse fileResponse = fileMapper.toFileResponse(bookmark.getFile()); + NotificationResponse notificationResponse = notificationUseCase.getNotification(user, bookmark); + return bookmarkMapper.toFullResponse(bookmark, bookmarkGetService.getMappingsByBookmark(bookmark), fileResponse, notificationResponse); + } + + @Override + @Transactional(readOnly = true) + public Slice getFilteredBookmarks(Long userId, BookmarkSearchRequest request) { User user = userGetService.findById(userId); Pageable pageable = PageRequest.of(request.page(), @@ -134,7 +150,9 @@ public Slice getFilteredBookmarks(Long userId, BookmarkSearchR return bookmarks.map(bookmark -> { FileResponse fileResponse = fileMapper.toFileResponse(bookmark.getFile()); - return bookmarkMapper.toResponse(bookmark, bookmarkGetService.getMappingsByBookmark(bookmark), fileResponse); + NotificationResponse notificationResponse = notificationUseCase.getNotification(user, bookmark); + return bookmarkMapper.toFullResponse(bookmark, bookmarkGetService.getMappingsByBookmark(bookmark), + fileResponse, notificationResponse); } ); } diff --git a/src/main/java/leets/bookmark/domain/bookmark/domain/repository/BookmarkRepositoryImpl.java b/src/main/java/leets/bookmark/domain/bookmark/domain/repository/BookmarkRepositoryImpl.java index 7b82b564..fb8bfd20 100644 --- a/src/main/java/leets/bookmark/domain/bookmark/domain/repository/BookmarkRepositoryImpl.java +++ b/src/main/java/leets/bookmark/domain/bookmark/domain/repository/BookmarkRepositoryImpl.java @@ -18,6 +18,8 @@ import org.springframework.data.domain.SliceImpl; import org.springframework.stereotype.Repository; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -30,25 +32,18 @@ public class BookmarkRepositoryImpl implements BookmarkRepositoryCustom { private final QBookmark bookmark = QBookmark.bookmark; private final QBookmarkTagMapping tagMapping = QBookmarkTagMapping.bookmarkTagMapping; - private final QTag tag = QTag.tag; - private final QCategory category = QCategory.category; - private final QFile file = QFile.file; @Override public Slice searchWithFilters(Long userId, BookmarkSearchCondition condition, Pageable pageable) { - Slice idSlice = findBookmarkIds(userId, condition, pageable); - - List bookmarks = findBookmarksWithAssociations(idSlice.getContent()); - - return new SliceImpl<>(bookmarks, pageable, idSlice.hasNext()); + return findBookmarkIds(userId, condition, pageable); } - public Slice findBookmarkIds(Long userId, BookmarkSearchCondition condition, Pageable pageable) { + public Slice findBookmarkIds(Long userId, BookmarkSearchCondition condition, Pageable pageable) { BooleanBuilder builder = buildCondition(userId, condition); - List ids = queryFactory - .select(bookmark.id) + List bookmarks = queryFactory + .selectDistinct(bookmark) .from(tagMapping) .leftJoin(tagMapping.bookmark, bookmark) .where(builder) @@ -57,24 +52,10 @@ public Slice findBookmarkIds(Long userId, BookmarkSearchCondition conditio .limit(pageable.getPageSize() + 1) .fetch(); - boolean hasNext = ids.size() > pageable.getPageSize(); - if (hasNext) ids.remove(pageable.getPageSize()); - - return new SliceImpl<>(ids, pageable, hasNext); - } + boolean hasNext = bookmarks.size() > pageable.getPageSize(); + if (hasNext) bookmarks.remove(pageable.getPageSize()); - public List findBookmarksWithAssociations(List ids) { - if (ids.isEmpty()) return List.of(); - - return queryFactory - .select(bookmark) - .distinct() - .from(tagMapping) - .join(tagMapping.bookmark, bookmark) - .leftJoin(bookmark.category, category).fetchJoin() - .leftJoin(bookmark.file, file).fetchJoin() - .where(bookmark.id.in(ids)) - .fetch(); + return new SliceImpl<>(bookmarks, pageable, hasNext); } private BooleanBuilder buildCondition(Long userId, BookmarkSearchCondition condition) { diff --git a/src/main/java/leets/bookmark/domain/bookmark/presentation/BookmarkController.java b/src/main/java/leets/bookmark/domain/bookmark/presentation/BookmarkController.java index f16d7d20..b2511458 100644 --- a/src/main/java/leets/bookmark/domain/bookmark/presentation/BookmarkController.java +++ b/src/main/java/leets/bookmark/domain/bookmark/presentation/BookmarkController.java @@ -6,6 +6,7 @@ import leets.bookmark.domain.bookmark.application.dto.request.BookmarkSearchRequest; import leets.bookmark.domain.bookmark.application.dto.request.BookmarkSaveRequest; import leets.bookmark.domain.bookmark.application.dto.request.BookmarkUpdateRequest; +import leets.bookmark.domain.bookmark.application.dto.response.BookmarkFullResponse; import leets.bookmark.domain.notification.application.usecase.NotificationUseCase; import leets.bookmark.global.auth.annotation.CurrentUser; import leets.bookmark.global.common.response.CommonResponse; @@ -25,12 +26,19 @@ public class BookmarkController { private final BookmarkUseCase bookmarkUseCase; private final NotificationUseCase notificationUseCase; + @GetMapping("/{bookmarkId}") + @Operation(summary = "북마크 단일 조회 API", description = "북마크를 조회합니다.") + public CommonResponse getBookmark(@CurrentUser Long userId, @PathVariable Long bookmarkId) { + BookmarkFullResponse response = bookmarkUseCase.findBookmark(userId, bookmarkId); + return CommonResponse.createSuccess(BOOKMARK_FIND_SUCCESS.getMessage(), response); + } + @PostMapping("/search") @Operation(summary = "북마크 필터링 API") - public CommonResponse> getFilteredBookmarks(@CurrentUser Long userId, + public CommonResponse> getFilteredBookmarks(@CurrentUser Long userId, @RequestBody @Valid BookmarkSearchRequest bookmarkSearchRequest) { - Slice responses = bookmarkUseCase.getFilteredBookmarks(userId, bookmarkSearchRequest); - return CommonResponse.createSuccess(BOOKMARK_FILTER_SUCCESS.getMessage(), responses); + Slice responses = bookmarkUseCase.getFilteredBookmarks(userId, bookmarkSearchRequest); + return CommonResponse.createSuccess(BOOKMARK_FIND_SUCCESS.getMessage(), responses); } @PostMapping() diff --git a/src/main/java/leets/bookmark/domain/bookmark/presentation/BookmarkResponseMessage.java b/src/main/java/leets/bookmark/domain/bookmark/presentation/BookmarkResponseMessage.java index 0849b7de..a32a7392 100644 --- a/src/main/java/leets/bookmark/domain/bookmark/presentation/BookmarkResponseMessage.java +++ b/src/main/java/leets/bookmark/domain/bookmark/presentation/BookmarkResponseMessage.java @@ -6,6 +6,8 @@ @Getter @AllArgsConstructor public enum BookmarkResponseMessage { + + BOOKMARK_FIND_SUCCESS("북마크 단일 조회에 성공했습니다"), BOOKMARK_SEARCH_SUCCESS("북마크 검색에 성공했습니다."), BOOKMARK_MEMO_SEARCH_SUCCESS("북마크 메모 검색에 성공했습니다."), BOOKMARK_FILTER_SUCCESS("북마크 필터링에 성공했습니다."), diff --git a/src/main/java/leets/bookmark/domain/file/application/dto/response/FileResponse.java b/src/main/java/leets/bookmark/domain/file/application/dto/response/FileResponse.java index d1965bc0..0ca7e9da 100644 --- a/src/main/java/leets/bookmark/domain/file/application/dto/response/FileResponse.java +++ b/src/main/java/leets/bookmark/domain/file/application/dto/response/FileResponse.java @@ -2,8 +2,13 @@ import lombok.Builder; +import java.time.LocalDateTime; + @Builder public record FileResponse( + Long fileId, String fileName, - String fileUrl + String fileUrl, + LocalDateTime createdAt, + LocalDateTime updatedAt ) {} diff --git a/src/main/java/leets/bookmark/domain/file/application/mapper/FileMapper.java b/src/main/java/leets/bookmark/domain/file/application/mapper/FileMapper.java index 9b76da3b..39c70ab5 100644 --- a/src/main/java/leets/bookmark/domain/file/application/mapper/FileMapper.java +++ b/src/main/java/leets/bookmark/domain/file/application/mapper/FileMapper.java @@ -23,8 +23,11 @@ public File toFile(User user, Bookmark bookmark, FileSaveRequest fileSaveRequest public FileResponse toFileResponse(File file){ return FileResponse.builder() + .fileId(file.getId()) .fileUrl(file.getFileUrl()) .fileName(file.getFileName()) + .createdAt(file.getCreatedAt()) + .updatedAt(file.getUpdatedAt()) .build(); } } diff --git a/src/main/java/leets/bookmark/domain/notification/application/usecase/NotificationUseCase.java b/src/main/java/leets/bookmark/domain/notification/application/usecase/NotificationUseCase.java index e9a60d3d..dcc94fb9 100644 --- a/src/main/java/leets/bookmark/domain/notification/application/usecase/NotificationUseCase.java +++ b/src/main/java/leets/bookmark/domain/notification/application/usecase/NotificationUseCase.java @@ -33,6 +33,16 @@ public class NotificationUseCase { private final UserGetService userGetService; + @Transactional(readOnly = true) + public NotificationResponse getNotification(User user, Bookmark bookmark) { + Optional notification = notificationGetService.findByBookmarkId(bookmark.getId()); + + notification.ifPresent(value -> validateNotificationOwner(user, value)); + return notification + .map(notificationMapper::toNotificationResponse) + .orElse(null); + } + @Transactional(readOnly = true) public NotificationResponse getNotification(Long userId, Long bookmarkId) { User user = userGetService.findById(userId);