diff --git a/src/main/java/com/back/domain/chatbot/service/ChatbotService.java b/src/main/java/com/back/domain/chatbot/service/ChatbotService.java index c8e2a4dd..fd0c92b0 100644 --- a/src/main/java/com/back/domain/chatbot/service/ChatbotService.java +++ b/src/main/java/com/back/domain/chatbot/service/ChatbotService.java @@ -357,7 +357,9 @@ private String generateRecommendationMessage(String userQuestion, List unkeep( myBarService.unkeep(userId, cocktailId); return RsData.of(200, "deleted"); } + + @DeleteMapping + @Operation(summary = "내 바 전체 삭제", description = "내 바에 담긴 모든 칵테일을 소프트 삭제합니다") + public RsData clearAll( + @AuthenticationPrincipal SecurityUser principal + ) { + Long userId = principal.getId(); + myBarService.clearAll(userId); + return RsData.of(200, "cleared"); + } } diff --git a/src/main/java/com/back/domain/mybar/repository/MyBarRepository.java b/src/main/java/com/back/domain/mybar/repository/MyBarRepository.java index f4c858b3..0b26188f 100644 --- a/src/main/java/com/back/domain/mybar/repository/MyBarRepository.java +++ b/src/main/java/com/back/domain/mybar/repository/MyBarRepository.java @@ -38,6 +38,15 @@ public interface MyBarRepository extends JpaRepository { /** 복원/재킵을 위해 status 무시하고 한 건 찾기 (없으면 Optional.empty) */ Optional findByUser_IdAndCocktail_Id(Long userId, Long cocktailId); + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + update MyBar m + set m.status = 'DELETED', m.deletedAt = CURRENT_TIMESTAMP + where m.user.id = :userId + and m.status = 'ACTIVE' + """) + int softDeleteAllByUser(Long userId); + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(""" update MyBar m diff --git a/src/main/java/com/back/domain/mybar/service/MyBarService.java b/src/main/java/com/back/domain/mybar/service/MyBarService.java index 6f7ced4a..eb56991f 100644 --- a/src/main/java/com/back/domain/mybar/service/MyBarService.java +++ b/src/main/java/com/back/domain/mybar/service/MyBarService.java @@ -123,5 +123,13 @@ public void unkeep(Long userId, Long cocktailId) { abvScoreService.revokeForKeep(userId); } } + + @Transactional + public void clearAll(Long userId) { + int changed = myBarRepository.softDeleteAllByUser(userId); + if (changed > 0) { + abvScoreService.revokeForKeep(userId, changed); + } + } } diff --git a/src/main/java/com/back/domain/notification/dto/NotificationItemDto.java b/src/main/java/com/back/domain/notification/dto/NotificationItemDto.java index 1e54ae9d..7bd7d85e 100644 --- a/src/main/java/com/back/domain/notification/dto/NotificationItemDto.java +++ b/src/main/java/com/back/domain/notification/dto/NotificationItemDto.java @@ -2,6 +2,7 @@ import com.back.domain.notification.entity.Notification; import com.back.domain.notification.enums.NotificationType; +import com.back.domain.post.post.entity.PostImage; import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; @@ -13,18 +14,29 @@ public class NotificationItemDto { private NotificationType type; private Long postId; private String postTitle; + private String postCategoryName; + private String postThumbnailUrl; + private String message; private boolean read; private LocalDateTime createdAt; public static NotificationItemDto from(Notification n) { + String categoryName = n.getPost().getCategory() != null ? n.getPost().getCategory().getName() : null; + String thumbnailUrl = n.getPost().getImages().stream() + .map(PostImage::getUrl) + .findFirst() + .orElse(null); + return NotificationItemDto.builder() .id(n.getId()) .type(n.getType()) .postId(n.getPost().getId()) .postTitle(n.getPost().getTitle()) + .postCategoryName(categoryName) + .postThumbnailUrl(thumbnailUrl) + .message(n.getMessage()) .read(n.isRead()) .createdAt(n.getCreatedAt()) .build(); } } - diff --git a/src/main/java/com/back/domain/post/comment/service/CommentService.java b/src/main/java/com/back/domain/post/comment/service/CommentService.java index 1d0aaca6..adf191e7 100644 --- a/src/main/java/com/back/domain/post/comment/service/CommentService.java +++ b/src/main/java/com/back/domain/post/comment/service/CommentService.java @@ -47,11 +47,12 @@ public CommentResponseDto createComment(Long postId, CommentCreateRequestDto req .build(); // 게시글 작성자에게 알림 전송 + String commentMessage = String.format("%s 님이 '%s' 게시글에 댓글을 남겼습니다.", user.getNickname(), post.getTitle()); notificationService.sendNotification( post.getUser(), post, NotificationType.COMMENT, - user.getNickname() + " 님이 댓글을 남겼습니다." + commentMessage ); Comment saved = commentRepository.save(comment); diff --git a/src/main/java/com/back/domain/post/post/entity/Post.java b/src/main/java/com/back/domain/post/post/entity/Post.java index 70791888..f1037961 100644 --- a/src/main/java/com/back/domain/post/post/entity/Post.java +++ b/src/main/java/com/back/domain/post/post/entity/Post.java @@ -115,6 +115,15 @@ public void updateContent(String content) { this.content = content; } + public void addImage(PostImage image) { + if (this.images == null) { + this.images = new ArrayList<>(); + } + this.images.add(image); + image.updatePost(this); // 양방향 관계 유지 + } + + public void updateImages(List images) { this.images.clear(); for (PostImage i : images) { diff --git a/src/main/java/com/back/domain/post/post/repository/PostRepository.java b/src/main/java/com/back/domain/post/post/repository/PostRepository.java index a45fed3d..fef86c78 100644 --- a/src/main/java/com/back/domain/post/post/repository/PostRepository.java +++ b/src/main/java/com/back/domain/post/post/repository/PostRepository.java @@ -1,6 +1,7 @@ package com.back.domain.post.post.repository; import com.back.domain.post.post.entity.Post; +import com.back.domain.post.post.enums.PostStatus; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -9,28 +10,31 @@ public interface PostRepository extends JpaRepository { // 최신순 (카테고리 없음) - List findTop10ByOrderByIdDesc(); - List findTop10ByIdLessThanOrderByIdDesc(Long lastId); + List findTop10ByStatusNotOrderByIdDesc(PostStatus status); + List findTop10ByStatusNotAndIdLessThanOrderByIdDesc(PostStatus status, Long lastId); // 추천순 (카테고리 없음) - List findTop10ByOrderByLikeCountDescIdDesc(); - List findTop10ByLikeCountLessThanOrLikeCountEqualsAndIdLessThanOrderByLikeCountDescIdDesc( - Integer likeCount, Integer likeCount2, Long id); + List findTop10ByStatusNotOrderByLikeCountDescIdDesc(PostStatus status); + List findTop10ByStatusNotAndLikeCountLessThanOrLikeCountEqualsAndIdLessThanOrderByLikeCountDescIdDesc( + PostStatus status, Integer likeCount, Integer likeCount2, Long id); // 댓글순 (카테고리 없음) - List findTop10ByOrderByCommentCountDescIdDesc(); - List findTop10ByCommentCountLessThanOrCommentCountEqualsAndIdLessThanOrderByCommentCountDescIdDesc( - Integer commentCount, Integer commentCount2, Long id); + List findTop10ByStatusNotOrderByCommentCountDescIdDesc(PostStatus status); + List findTop10ByStatusNotAndCommentCountLessThanOrCommentCountEqualsAndIdLessThanOrderByCommentCountDescIdDesc( + PostStatus status, Integer commentCount, Integer commentCount2, Long id); // 최신순 (카테고리) - List findTop10ByCategoryIdOrderByIdDesc(Long categoryId); - List findTop10ByCategoryIdAndIdLessThanOrderByIdDesc(Long categoryId, Long id); + List findTop10ByCategoryIdAndStatusNotOrderByIdDesc(Long categoryId, PostStatus status); + List findTop10ByCategoryIdAndStatusNotAndIdLessThanOrderByIdDesc(Long categoryId, PostStatus status, Long id); // 추천순 (카테고리) - List findTop10ByCategoryIdOrderByLikeCountDescIdDesc(Long categoryId); - List findTop10ByCategoryIdAndLikeCountLessThanOrLikeCountEqualsAndIdLessThanOrderByLikeCountDescIdDesc(Long categoryId, Integer likeCount, Integer likeCountEquals, Long id); + List findTop10ByCategoryIdAndStatusNotOrderByLikeCountDescIdDesc(Long categoryId, PostStatus status); + List findTop10ByCategoryIdAndStatusNotAndLikeCountLessThanOrLikeCountEqualsAndIdLessThanOrderByLikeCountDescIdDesc( + Long categoryId, PostStatus status, Integer likeCount, Integer likeCountEquals, Long id); // 댓글순 (카테고리) - List findTop10ByCategoryIdOrderByCommentCountDescIdDesc(Long categoryId); - List findTop10ByCategoryIdAndCommentCountLessThanOrCommentCountEqualsAndIdLessThanOrderByCommentCountDescIdDesc(Long categoryId, Integer commentCount, Integer commentCountEquals, Long id); + List findTop10ByCategoryIdAndStatusNotOrderByCommentCountDescIdDesc(Long categoryId, PostStatus status); + List findTop10ByCategoryIdAndStatusNotAndCommentCountLessThanOrCommentCountEqualsAndIdLessThanOrderByCommentCountDescIdDesc( + Long categoryId, PostStatus status, Integer commentCount, Integer commentCountEquals, Long id); } + diff --git a/src/main/java/com/back/domain/post/post/service/PostService.java b/src/main/java/com/back/domain/post/post/service/PostService.java index 59e8d366..603016f2 100644 --- a/src/main/java/com/back/domain/post/post/service/PostService.java +++ b/src/main/java/com/back/domain/post/post/service/PostService.java @@ -24,13 +24,6 @@ import com.back.global.file.dto.UploadedFileDto; import com.back.global.file.service.FileService; import com.back.global.rq.Rq; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -38,6 +31,10 @@ import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.web.multipart.MultipartFile; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + @Service @RequiredArgsConstructor public class PostService { @@ -82,13 +79,12 @@ public PostResponseDto createPost(PostCreateRequestDto reqBody, List findAllPosts(PostSortScrollRequestDto reqBody) { return switch (reqBody.postSortStatus()) { case POPULAR -> { if (reqBody.lastId() == null || reqBody.lastLikeCount() == null) { - yield postRepository.findTop10ByOrderByLikeCountDescIdDesc(); + yield postRepository.findTop10ByStatusNotOrderByLikeCountDescIdDesc(PostStatus.DELETED); } else { - yield postRepository.findTop10ByLikeCountLessThanOrLikeCountEqualsAndIdLessThanOrderByLikeCountDescIdDesc(reqBody.lastLikeCount(), reqBody.lastLikeCount(), reqBody.lastId()); + yield postRepository.findTop10ByStatusNotAndLikeCountLessThanOrLikeCountEqualsAndIdLessThanOrderByLikeCountDescIdDesc(PostStatus.DELETED, reqBody.lastLikeCount(), reqBody.lastLikeCount(), reqBody.lastId()); } } case COMMENTS -> { if (reqBody.lastId() == null || reqBody.lastCommentCount() == null) { - yield postRepository.findTop10ByOrderByCommentCountDescIdDesc(); + yield postRepository.findTop10ByStatusNotOrderByCommentCountDescIdDesc(PostStatus.DELETED); } else { - yield postRepository.findTop10ByCommentCountLessThanOrCommentCountEqualsAndIdLessThanOrderByCommentCountDescIdDesc(reqBody.lastCommentCount(), reqBody.lastCommentCount(), reqBody.lastId()); + yield postRepository.findTop10ByStatusNotAndCommentCountLessThanOrCommentCountEqualsAndIdLessThanOrderByCommentCountDescIdDesc(PostStatus.DELETED, reqBody.lastCommentCount(), reqBody.lastCommentCount(), reqBody.lastId()); } } case LATEST -> { if (reqBody.lastId() == null) { - yield postRepository.findTop10ByOrderByIdDesc(); + yield postRepository.findTop10ByStatusNotOrderByIdDesc(PostStatus.DELETED); } else { - yield postRepository.findTop10ByIdLessThanOrderByIdDesc(reqBody.lastId()); + yield postRepository.findTop10ByStatusNotAndIdLessThanOrderByIdDesc(PostStatus.DELETED, reqBody.lastId()); } } default -> throw new IllegalArgumentException("지원하지 않는 정렬 기준: " + reqBody.postSortStatus()); @@ -322,29 +319,29 @@ private List findPostsByCategory(PostSortScrollRequestDto reqBody) { return switch (reqBody.postSortStatus()) { case POPULAR -> { if (reqBody.lastId() == null || reqBody.lastLikeCount() == null) { - yield postRepository.findTop10ByCategoryIdOrderByLikeCountDescIdDesc( - reqBody.categoryId()); + yield postRepository.findTop10ByCategoryIdAndStatusNotOrderByLikeCountDescIdDesc( + reqBody.categoryId(), PostStatus.DELETED); } else { - yield postRepository.findTop10ByCategoryIdAndLikeCountLessThanOrLikeCountEqualsAndIdLessThanOrderByLikeCountDescIdDesc( - reqBody.categoryId(), reqBody.lastLikeCount(), reqBody.lastLikeCount(), + yield postRepository.findTop10ByCategoryIdAndStatusNotAndLikeCountLessThanOrLikeCountEqualsAndIdLessThanOrderByLikeCountDescIdDesc( + reqBody.categoryId(), PostStatus.DELETED, reqBody.lastLikeCount(), reqBody.lastLikeCount(), reqBody.lastId()); } } case COMMENTS -> { if (reqBody.lastId() == null || reqBody.lastCommentCount() == null) { - yield postRepository.findTop10ByCategoryIdOrderByCommentCountDescIdDesc( - reqBody.categoryId()); + yield postRepository.findTop10ByCategoryIdAndStatusNotOrderByCommentCountDescIdDesc( + reqBody.categoryId(), PostStatus.DELETED); } else { - yield postRepository.findTop10ByCategoryIdAndCommentCountLessThanOrCommentCountEqualsAndIdLessThanOrderByCommentCountDescIdDesc( - reqBody.categoryId(), reqBody.lastCommentCount(), reqBody.lastCommentCount(), + yield postRepository.findTop10ByCategoryIdAndStatusNotAndCommentCountLessThanOrCommentCountEqualsAndIdLessThanOrderByCommentCountDescIdDesc( + reqBody.categoryId(), PostStatus.DELETED, reqBody.lastCommentCount(), reqBody.lastCommentCount(), reqBody.lastId()); } } case LATEST -> { if (reqBody.lastId() == null) { - yield postRepository.findTop10ByCategoryIdOrderByIdDesc(reqBody.categoryId()); + yield postRepository.findTop10ByCategoryIdAndStatusNotOrderByIdDesc(reqBody.categoryId(), PostStatus.DELETED); } else { - yield postRepository.findTop10ByCategoryIdAndIdLessThanOrderByIdDesc(reqBody.categoryId(), + yield postRepository.findTop10ByCategoryIdAndStatusNotAndIdLessThanOrderByIdDesc(reqBody.categoryId(), PostStatus.DELETED, reqBody.lastId()); } } diff --git a/src/main/java/com/back/domain/user/service/AbvScoreService.java b/src/main/java/com/back/domain/user/service/AbvScoreService.java index ec66e02b..48039c7e 100644 --- a/src/main/java/com/back/domain/user/service/AbvScoreService.java +++ b/src/main/java/com/back/domain/user/service/AbvScoreService.java @@ -58,6 +58,14 @@ public void revokeForKeep(Long userId) { addScore(userId, -KEEP_SCORE); } + @Transactional + public void revokeForKeep(Long userId, int count) { + if (count <= 0) { + return; + } + addScore(userId, -KEEP_SCORE * count); + } + private void addScore(Long userId, double delta) { User user = userRepository.findById(userId) .orElseThrow(() -> new ServiceException(404, "사용자를 찾을 수 없습니다.")); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a0594f1b..e0dd08b1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -99,11 +99,10 @@ custom: jwt: secretKey: ${JWT_SECRET_KEY} accessToken: - expirationSeconds: "#{60}" # 15분 곱하기 + expirationSeconds: "#{60*15}" refreshToken: expirationSeconds: "#{60*60*24*30}" - idleTimeoutHours: "#{1}" -# "#{60*6*4}" + idleTimeoutHours: "#{60*6*4}" management: diff --git a/src/test/java/com/back/domain/mybar/controller/MyBarControllerTest.java b/src/test/java/com/back/domain/mybar/controller/MyBarControllerTest.java index 8c1a8854..2a9f8c4c 100644 --- a/src/test/java/com/back/domain/mybar/controller/MyBarControllerTest.java +++ b/src/test/java/com/back/domain/mybar/controller/MyBarControllerTest.java @@ -220,4 +220,22 @@ void unkeepCocktail() throws Exception { verify(myBarService).unkeep(principal.getId(), cocktailId); } + + @Test + @DisplayName("Clear entire my bar") + void clearAllMyBar() throws Exception { + SecurityUser principal = createPrincipal(21L); + + willDoNothing().given(myBarService).clearAll(principal.getId()); + + mockMvc.perform(delete("/me/bar") + .with(withPrincipal(principal)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("cleared")) + .andExpect(jsonPath("$.data").doesNotExist()); + + verify(myBarService).clearAll(principal.getId()); + } } diff --git a/src/test/java/com/back/domain/notification/controller/NotificationControllerTest.java b/src/test/java/com/back/domain/notification/controller/NotificationControllerTest.java index 98aa5ef1..bf7bf050 100644 --- a/src/test/java/com/back/domain/notification/controller/NotificationControllerTest.java +++ b/src/test/java/com/back/domain/notification/controller/NotificationControllerTest.java @@ -129,6 +129,9 @@ void getNotifications_noCursor() throws Exception { .type(NotificationType.COMMENT) .postId(55L) .postTitle("새 댓글") + .postCategoryName("공지") + .postThumbnailUrl("https://example.com/thumb.png") + .message("새 댓글이 달렸습니다.") .read(false) .createdAt(LocalDateTime.of(2025, 1, 2, 12, 0)) .build(); @@ -154,6 +157,9 @@ void getNotifications_noCursor() throws Exception { .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.message").value("success")) .andExpect(jsonPath("$.data.items[0].id").value(101)) + .andExpect(jsonPath("$.data.items[0].postCategoryName").value("공지")) + .andExpect(jsonPath("$.data.items[0].postThumbnailUrl").value("https://example.com/thumb.png")) + .andExpect(jsonPath("$.data.items[0].message").value("새 댓글이 달렸습니다.")) .andExpect(jsonPath("$.data.hasNext").value(false)) .andExpect(jsonPath("$.data.nextCreatedAt").doesNotExist());