From ef841789716675623b91280a228c667e0d725057 Mon Sep 17 00:00:00 2001 From: coPpark Date: Sun, 27 Oct 2024 20:02:35 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A6=AC=EC=95=A1=EC=85=98=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=B6=94=EC=B2=9C=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20API,=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=EC=A1=B0=ED=9A=8C=20API=20=EC=9E=AC=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#304)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 리액션 API 추가 및 추천리스트 API, 리스트 상세조회 API 재구현 * test: 리액션 관련 테스트 추가 및 이에 따른 변경된 API에 대한 테스트코드 수정 * refactor: 코드리뷰에 의한 네이밍 변경 및 리액션 패키지구조 별도 추출 * refactor: 코드리뷰에 의한 네이밍 변경 --- .../common/auth/AuthorizationInterceptor.java | 2 +- .../application/domain/list/ListEntity.java | 4 + .../dto/response/ListDetailResponse.java | 15 ++- .../dto/response/ListTrandingResponse.java | 33 ------ .../dto/response/RecommendedListResponse.java | 54 +++++++++ .../list/application/service/ListService.java | 45 ++++++-- .../controller/ListController.java | 10 +- .../list/custom/CustomListRepository.java | 3 +- .../custom/impl/CustomListRepositoryImpl.java | 74 ++++-------- .../reaction/application/domain/Reaction.java | 15 +++ .../application/domain/ReactionStats.java | 45 ++++++++ .../application/domain/UserReaction.java | 72 ++++++++++++ .../dto/response/ReactionResponse.java | 19 +++ .../application/service/ReactionService.java | 92 +++++++++++++++ .../controller/ReactionController.java | 28 +++++ .../dto/request/ReactionRequest.java | 8 ++ .../repository/ReactionStatsRepository.java | 15 +++ .../repository/UserReactionRepository.java | 16 +++ .../collection/CollectionAcceptanceTest.java | 29 ++--- .../acceptance/list/ListAcceptanceTest.java | 93 +++++++-------- .../list/ListAcceptanceTestHelper.java | 22 ++-- .../reaction/ReactionAcceptanceTest.java | 108 ++++++++++++++++++ .../ReactionAcceptanceTestHelper.java | 34 ++++++ 23 files changed, 653 insertions(+), 183 deletions(-) delete mode 100644 src/main/java/com/listywave/list/application/dto/response/ListTrandingResponse.java create mode 100644 src/main/java/com/listywave/list/application/dto/response/RecommendedListResponse.java create mode 100644 src/main/java/com/listywave/reaction/application/domain/Reaction.java create mode 100644 src/main/java/com/listywave/reaction/application/domain/ReactionStats.java create mode 100644 src/main/java/com/listywave/reaction/application/domain/UserReaction.java create mode 100644 src/main/java/com/listywave/reaction/application/dto/response/ReactionResponse.java create mode 100644 src/main/java/com/listywave/reaction/application/service/ReactionService.java create mode 100644 src/main/java/com/listywave/reaction/presentation/controller/ReactionController.java create mode 100644 src/main/java/com/listywave/reaction/presentation/dto/request/ReactionRequest.java create mode 100644 src/main/java/com/listywave/reaction/repository/ReactionStatsRepository.java create mode 100644 src/main/java/com/listywave/reaction/repository/UserReactionRepository.java create mode 100644 src/test/java/com/listywave/acceptance/reaction/ReactionAcceptanceTest.java create mode 100644 src/test/java/com/listywave/acceptance/reaction/ReactionAcceptanceTestHelper.java diff --git a/src/main/java/com/listywave/common/auth/AuthorizationInterceptor.java b/src/main/java/com/listywave/common/auth/AuthorizationInterceptor.java index 66cbe6c9..376bc647 100644 --- a/src/main/java/com/listywave/common/auth/AuthorizationInterceptor.java +++ b/src/main/java/com/listywave/common/auth/AuthorizationInterceptor.java @@ -21,7 +21,7 @@ public class AuthorizationInterceptor implements HandlerInterceptor { private static final UriAndMethod[] whiteList = { - new UriAndMethod("/lists/explore", GET), + new UriAndMethod("/lists/recommended", GET), new UriAndMethod("/lists/search", GET), new UriAndMethod("/lists/{listId}/comments", GET), new UriAndMethod("/lists/upload-url", GET), diff --git a/src/main/java/com/listywave/list/application/domain/list/ListEntity.java b/src/main/java/com/listywave/list/application/domain/list/ListEntity.java index 8d35f98e..02eb2f49 100644 --- a/src/main/java/com/listywave/list/application/domain/list/ListEntity.java +++ b/src/main/java/com/listywave/list/application/domain/list/ListEntity.java @@ -266,4 +266,8 @@ public void validateUpdateAuthority(User loginUser, Collaborators beforeCollabor public void increaseUpdateCount() { this.updateCount++; } + + public boolean isOwner(User loginUser) { + return this.user.equals(loginUser); + } } diff --git a/src/main/java/com/listywave/list/application/dto/response/ListDetailResponse.java b/src/main/java/com/listywave/list/application/dto/response/ListDetailResponse.java index dd5197bd..daec8b0a 100644 --- a/src/main/java/com/listywave/list/application/dto/response/ListDetailResponse.java +++ b/src/main/java/com/listywave/list/application/dto/response/ListDetailResponse.java @@ -5,6 +5,7 @@ import com.listywave.list.application.domain.item.Item; import com.listywave.list.application.domain.label.Label; import com.listywave.list.application.domain.list.ListEntity; +import com.listywave.reaction.application.dto.response.ReactionResponse; import com.listywave.user.application.domain.User; import jakarta.annotation.Nullable; import java.time.LocalDateTime; @@ -29,20 +30,24 @@ public record ListDetailResponse( boolean isPublic, String backgroundPalette, String backgroundColor, - int collectCount, + Integer collectCount, int viewCount, + int updateCount, long totalCommentCount, - @Nullable NewestComment newestComment + @Nullable NewestComment newestComment, + List reactions ) { public static ListDetailResponse of( ListEntity list, User owner, + boolean isOwner, boolean isCollected, List collaborators, long totalCommentCount, Comment newestComment, - Long totalReplyCount + Long totalReplyCount, + List reactions ) { return ListDetailResponse.builder() .categoryEngName(list.getCategory().name().toLowerCase()) @@ -61,10 +66,12 @@ public static ListDetailResponse of( .isPublic(list.isPublic()) .backgroundColor(list.getBackgroundColor().name()) .backgroundPalette(list.getBackgroundPalette().name()) - .collectCount(list.getCollectCount()) + .collectCount(isOwner ? list.getCollectCount() : null) .viewCount(list.getViewCount()) + .updateCount(list.getUpdateCount()) .totalCommentCount(totalCommentCount) .newestComment(NewestComment.of(newestComment, totalReplyCount)) + .reactions(reactions) .build(); } diff --git a/src/main/java/com/listywave/list/application/dto/response/ListTrandingResponse.java b/src/main/java/com/listywave/list/application/dto/response/ListTrandingResponse.java deleted file mode 100644 index 450d418d..00000000 --- a/src/main/java/com/listywave/list/application/dto/response/ListTrandingResponse.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.listywave.list.application.dto.response; - -import com.listywave.list.application.domain.list.BackgroundColor; - -public record ListTrandingResponse( - Long id, - Long ownerId, - String ownerNickname, - String ownerProfileImageUrl, - String title, - String description, - BackgroundColor backgroundColor, - Long trandingScore, - String itemImageUrl -) { - - public ListTrandingResponse( - Long id, - Long ownerId, - String ownerNickname, - String ownerProfileImageUrl, - String title, - String description, - BackgroundColor backgroundColor, - Long trandingScore - ) { - this(id, ownerId, ownerNickname, ownerProfileImageUrl, title, description, backgroundColor, trandingScore, ""); - } - - public ListTrandingResponse with(String imageUrl) { - return new ListTrandingResponse(id, ownerId, ownerNickname, ownerProfileImageUrl, title, description, backgroundColor, trandingScore, imageUrl); - } -} diff --git a/src/main/java/com/listywave/list/application/dto/response/RecommendedListResponse.java b/src/main/java/com/listywave/list/application/dto/response/RecommendedListResponse.java new file mode 100644 index 00000000..68fce2a7 --- /dev/null +++ b/src/main/java/com/listywave/list/application/dto/response/RecommendedListResponse.java @@ -0,0 +1,54 @@ +package com.listywave.list.application.dto.response; + +import com.listywave.list.application.domain.item.Item; +import com.listywave.list.application.domain.list.ListEntity; +import lombok.Builder; + +import java.util.List; + +@Builder +public record RecommendedListResponse( + Long id, + Long ownerId, + String ownerNickname, + String title, + String itemImageUrl, + String category, + String backgroundColor, + List items +) { + public static RecommendedListResponse of(ListEntity list) { + return RecommendedListResponse.builder() + .id(list.getId()) + .ownerId(list.getUser().getId()) + .ownerNickname(list.getUser().getNickname()) + .title(list.getTitle().getValue()) + .itemImageUrl(list.getRepresentImageUrl()) + .category(list.getCategory().getViewName()) + .backgroundColor(list.getBackgroundColor().name()) + .items(Top3ItemResponse.toList(list.getTop3Items().getValues())) + .build(); + } + + @Builder + public record Top3ItemResponse( + Long id, + int rank, + String title + ) { + + public static List toList(List items) { + return items.stream() + .map(Top3ItemResponse::of) + .toList(); + } + + public static Top3ItemResponse of(Item item) { + return Top3ItemResponse.builder() + .id(item.getId()) + .rank(item.getRanking()) + .title(item.getTitle().getValue()) + .build(); + } + } +} diff --git a/src/main/java/com/listywave/list/application/service/ListService.java b/src/main/java/com/listywave/list/application/service/ListService.java index 089ac073..0f75670f 100644 --- a/src/main/java/com/listywave/list/application/service/ListService.java +++ b/src/main/java/com/listywave/list/application/service/ListService.java @@ -32,7 +32,7 @@ import com.listywave.list.application.dto.response.ListDetailResponse; import com.listywave.list.application.dto.response.ListRecentResponse; import com.listywave.list.application.dto.response.ListSearchResponse; -import com.listywave.list.application.dto.response.ListTrandingResponse; +import com.listywave.list.application.dto.response.RecommendedListResponse; import com.listywave.list.presentation.dto.request.ItemCreateRequest; import com.listywave.list.presentation.dto.request.ListCreateRequest; import com.listywave.list.presentation.dto.request.ListUpdateRequest; @@ -41,6 +41,8 @@ import com.listywave.list.repository.label.LabelRepository; import com.listywave.list.repository.list.ListRepository; import com.listywave.list.repository.reply.ReplyRepository; +import com.listywave.reaction.application.dto.response.ReactionResponse; +import com.listywave.reaction.application.service.ReactionService; import com.listywave.user.application.domain.Follow; import com.listywave.user.application.domain.User; import com.listywave.user.application.dto.FindFeedListResponse; @@ -75,6 +77,7 @@ public class ListService { private final AlarmRepository alarmRepository; private final CollaboratorService collaboratorService; private final HistoryService historyService; + private final ReactionService reactionService; private final ApplicationEventPublisher applicationEventPublisher; public ListCreateResponse listCreate(ListCreateRequest request, Long loginUserId) { @@ -134,21 +137,33 @@ public ListDetailResponse getListDetail(Long listId, Long loginUserId) { list.validateOwnerIsNotDeleted(); List collaborators = collaboratorService.findAllByList(list).collaborators(); - boolean isCollected = false; - if (loginUserId != null) { - User user = userRepository.getById(loginUserId); - isCollected = collectionRepository.existsByListAndUserId(list, user.getId()); - } + User user = getUserIfLoggedIn(loginUserId); + boolean isCollected = checkIsCollected(list, user); + boolean isOwner = checkIsOwner(list, user); + List reactions = reactionService.createReactionResponses(list, user, isOwner); long totalCommentCount = commentRepository.countCommentsByList(list); Comment newestComment = commentRepository.findFirstByListOrderByCreatedDateDesc(list); Long totalReplyCount = replyRepository.countByComment(newestComment); - return ListDetailResponse.of(list, list.getUser(), isCollected, collaborators, totalCommentCount, newestComment, totalReplyCount); + return ListDetailResponse.of( + list, + list.getUser(), + isOwner, + isCollected, + collaborators, + totalCommentCount, + newestComment, + totalReplyCount, + reactions + ); } @Transactional(readOnly = true) - public List fetchTrandingLists() { - return listRepository.fetchTrandingLists(); + public List getRecommendedLists() { + List recommendedLists = listRepository.findRecommendedLists(); + return recommendedLists.stream() + .map(RecommendedListResponse::of) + .toList(); } public void deleteList(Long listId, Long loginUserId) { @@ -312,4 +327,16 @@ public void changeVisibility(Long loginUserId, Long listId) { list.validateOwner(user); list.updateVisibility(); } + + private User getUserIfLoggedIn(Long loginUserId) { + return (loginUserId != null) ? userRepository.getById(loginUserId) : null; + } + + private boolean checkIsCollected(ListEntity list, User user) { + return (user != null) && collectionRepository.existsByListAndUserId(list, user.getId()); + } + + private boolean checkIsOwner(ListEntity list, User user) { + return (user != null) && list.isOwner(user); + } } diff --git a/src/main/java/com/listywave/list/presentation/controller/ListController.java b/src/main/java/com/listywave/list/presentation/controller/ListController.java index dce54443..70ebab16 100644 --- a/src/main/java/com/listywave/list/presentation/controller/ListController.java +++ b/src/main/java/com/listywave/list/presentation/controller/ListController.java @@ -8,7 +8,7 @@ import com.listywave.list.application.dto.response.ListDetailResponse; import com.listywave.list.application.dto.response.ListRecentResponse; import com.listywave.list.application.dto.response.ListSearchResponse; -import com.listywave.list.application.dto.response.ListTrandingResponse; +import com.listywave.list.application.dto.response.RecommendedListResponse; import com.listywave.list.application.service.ListService; import com.listywave.list.presentation.dto.request.ListCreateRequest; import com.listywave.list.presentation.dto.request.ListUpdateRequest; @@ -54,10 +54,10 @@ ResponseEntity getListDetail( return ResponseEntity.ok(listDetailResponse); } - @GetMapping("/lists/explore") - ResponseEntity> fetchTrandingLists() { - List trandingList = listService.fetchTrandingLists(); - return ResponseEntity.ok().body(trandingList); + @GetMapping("/lists/recommended") + ResponseEntity> getRecommendedLists() { + List recommendedLists = listService.getRecommendedLists(); + return ResponseEntity.ok().body(recommendedLists); } @DeleteMapping("/lists/{listId}") diff --git a/src/main/java/com/listywave/list/repository/list/custom/CustomListRepository.java b/src/main/java/com/listywave/list/repository/list/custom/CustomListRepository.java index 8758c1e5..00222d02 100644 --- a/src/main/java/com/listywave/list/repository/list/custom/CustomListRepository.java +++ b/src/main/java/com/listywave/list/repository/list/custom/CustomListRepository.java @@ -2,7 +2,6 @@ import com.listywave.list.application.domain.category.CategoryType; import com.listywave.list.application.domain.list.ListEntity; -import com.listywave.list.application.dto.response.ListTrandingResponse; import com.listywave.user.application.domain.User; import java.time.LocalDateTime; import java.util.List; @@ -11,7 +10,7 @@ public interface CustomListRepository { - List fetchTrandingLists(); + List findRecommendedLists(); Slice getRecentLists(LocalDateTime cursorUpdatedDate, CategoryType category, Pageable pageable); diff --git a/src/main/java/com/listywave/list/repository/list/custom/impl/CustomListRepositoryImpl.java b/src/main/java/com/listywave/list/repository/list/custom/impl/CustomListRepositoryImpl.java index 0d3c1562..8d9c0512 100644 --- a/src/main/java/com/listywave/list/repository/list/custom/impl/CustomListRepositoryImpl.java +++ b/src/main/java/com/listywave/list/repository/list/custom/impl/CustomListRepositoryImpl.java @@ -5,28 +5,21 @@ import static com.listywave.common.exception.ErrorCode.NOT_SUPPORT_FILTER_ARGUMENT_EXCEPTION; import static com.listywave.common.util.PaginationUtils.checkEndPage; import static com.listywave.list.application.domain.category.CategoryType.ENTIRE; -import static com.listywave.list.application.domain.comment.QComment.comment; import static com.listywave.list.application.domain.item.QItem.item; import static com.listywave.list.application.domain.list.QListEntity.listEntity; -import static com.listywave.list.application.domain.reply.QReply.reply; +import static com.listywave.reaction.application.domain.QReactionStats.reactionStats; import static com.listywave.user.application.domain.QUser.user; -import static com.querydsl.jpa.JPAExpressions.select; import com.listywave.common.exception.CustomException; import com.listywave.list.application.domain.category.CategoryType; import com.listywave.list.application.domain.list.ListEntity; -import com.listywave.list.application.dto.response.ListTrandingResponse; import com.listywave.list.repository.list.custom.CustomListRepository; import com.listywave.user.application.domain.User; -import com.querydsl.core.types.ExpressionUtils; -import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; -import com.querydsl.core.types.dsl.Expressions; -import com.querydsl.core.types.dsl.NumberPath; import com.querydsl.jpa.impl.JPAQueryFactory; import java.time.LocalDateTime; +import java.util.Comparator; import java.util.List; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -36,64 +29,39 @@ public class CustomListRepositoryImpl implements CustomListRepository { private final JPAQueryFactory queryFactory; - @Override - public List fetchTrandingLists() { - List responses = getTrandingResponses(); - return responses.stream() - .map(t -> t.with(getRepresentImageUrl(t.id()))) - .collect(Collectors.toList()); + public List findRecommendedLists() { + List recommendedListIds = findRecommendedListIds(); + return findRecommendedListsByIds(recommendedListIds); } - private List getTrandingResponses() { + public List findRecommendedListIds() { LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30); - NumberPath trandingScoreAlias = Expressions.numberPath(Long.class, "trandingScore"); - return queryFactory - .select(Projections.constructor(ListTrandingResponse.class, - listEntity.id.as("id"), - listEntity.user.id.as("ownerId"), - listEntity.user.nickname.value.as("ownerNickname"), - listEntity.user.profileImageUrl.value.as("ownerProfileImageUrl"), - listEntity.title.value.as("title"), - listEntity.description.value.as("description"), - listEntity.backgroundColor.as("backgroundColor"), - ExpressionUtils.as( - select( - listEntity.collectCount.multiply(3).add( - comment.countDistinct().add(reply.count()).multiply(2) - ).castToNum(Long.class) - ) - .from(comment) - .leftJoin(reply).on(reply.comment.id.eq(comment.id)) - .where(comment.list.id.eq(listEntity.id)), trandingScoreAlias)) - ) - .from(listEntity) + .select(reactionStats.list.id) + .from(reactionStats) + .join(reactionStats.list, listEntity) .join(listEntity.user, user) .where( - listEntity.updatedDate.goe(thirtyDaysAgo), + reactionStats.updatedDate.goe(thirtyDaysAgo), listEntity.isPublic.eq(true), listEntity.user.isDelete.eq(false) ) - .distinct() - .orderBy(trandingScoreAlias.desc()) + .groupBy(reactionStats.list.id) + .orderBy(reactionStats.count.sum().desc(), listEntity.updatedDate.desc()) .limit(10) .fetch(); } - private String getRepresentImageUrl(Long id) { - String imageUrl = queryFactory - .select(item.imageUrl.value) - .from(item) - .where( - item.list.id.eq(id).and( - item.imageUrl.value.ne("") - ) - ) - .orderBy(item.ranking.asc()) - .limit(1) - .fetchOne(); + public List findRecommendedListsByIds(List listIds) { + List recommendedLists = queryFactory + .selectFrom(listEntity) + .join(listEntity.user, user).fetchJoin() + .leftJoin(item).on(listEntity.id.eq(item.list.id)) + .where(listEntity.id.in(listIds)) + .fetch(); - return imageUrl != null ? imageUrl : ""; + recommendedLists.sort(Comparator.comparingInt(o -> listIds.indexOf(o.getId()))); + return recommendedLists; } @Override diff --git a/src/main/java/com/listywave/reaction/application/domain/Reaction.java b/src/main/java/com/listywave/reaction/application/domain/Reaction.java new file mode 100644 index 00000000..d27ec8e7 --- /dev/null +++ b/src/main/java/com/listywave/reaction/application/domain/Reaction.java @@ -0,0 +1,15 @@ +package com.listywave.reaction.application.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum Reaction { + + COOL("멋져요"), + AGREE("공감해요"), + THANKS("감사해요"); + + private final String viewName; +} diff --git a/src/main/java/com/listywave/reaction/application/domain/ReactionStats.java b/src/main/java/com/listywave/reaction/application/domain/ReactionStats.java new file mode 100644 index 00000000..d932146b --- /dev/null +++ b/src/main/java/com/listywave/reaction/application/domain/ReactionStats.java @@ -0,0 +1,45 @@ +package com.listywave.reaction.application.domain; + +import static jakarta.persistence.FetchType.LAZY; +import static lombok.AccessLevel.PROTECTED; + +import com.listywave.common.BaseEntity; +import com.listywave.list.application.domain.list.ListEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Getter +@Entity +@Builder +@AllArgsConstructor +@Table(name = "reaction_stats") +@NoArgsConstructor(access = PROTECTED) +@EntityListeners(AuditingEntityListener.class) +public class ReactionStats extends BaseEntity { + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "list_id", nullable = false) + private ListEntity list; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Reaction reaction; + + @Column(nullable = false) + private int count; + + public synchronized void updateCount(int changeCount) { + this.count += changeCount; + } +} diff --git a/src/main/java/com/listywave/reaction/application/domain/UserReaction.java b/src/main/java/com/listywave/reaction/application/domain/UserReaction.java new file mode 100644 index 00000000..33b1bd65 --- /dev/null +++ b/src/main/java/com/listywave/reaction/application/domain/UserReaction.java @@ -0,0 +1,72 @@ +package com.listywave.reaction.application.domain; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.TemporalType.TIMESTAMP; +import static lombok.AccessLevel.PROTECTED; + +import com.listywave.list.application.domain.list.ListEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Temporal; +import jakarta.persistence.UniqueConstraint; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Getter +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = PROTECTED) +@EntityListeners(AuditingEntityListener.class) +@Table(name = "user_reaction", + uniqueConstraints = { + @UniqueConstraint( + name = "UniqueIdAndUserIdAndReaction", + columnNames = {"id", "user_id", "reaction"} + ) + } +) +public class UserReaction { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "list_id", nullable = false) + private ListEntity list; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Reaction reaction; + + @CreatedDate + @Temporal(TIMESTAMP) + @Column(updatable = false) + private LocalDateTime createdDate; + + public static UserReaction create(Long userId, ListEntity list, Reaction reaction) { + return UserReaction.builder() + .userId(userId) + .list(list) + .reaction(reaction) + .build(); + } +} diff --git a/src/main/java/com/listywave/reaction/application/dto/response/ReactionResponse.java b/src/main/java/com/listywave/reaction/application/dto/response/ReactionResponse.java new file mode 100644 index 00000000..ec711d98 --- /dev/null +++ b/src/main/java/com/listywave/reaction/application/dto/response/ReactionResponse.java @@ -0,0 +1,19 @@ +package com.listywave.reaction.application.dto.response; + +import lombok.Builder; + +@Builder +public record ReactionResponse( + String reaction, + Integer count, + boolean isReacted +) { + + public static ReactionResponse of(String name, Integer count, boolean isReacted) { + return ReactionResponse.builder() + .reaction(name) + .count(count) + .isReacted(isReacted) + .build(); + } +} diff --git a/src/main/java/com/listywave/reaction/application/service/ReactionService.java b/src/main/java/com/listywave/reaction/application/service/ReactionService.java new file mode 100644 index 00000000..8f4c37ec --- /dev/null +++ b/src/main/java/com/listywave/reaction/application/service/ReactionService.java @@ -0,0 +1,92 @@ +package com.listywave.reaction.application.service; + +import com.listywave.list.application.domain.list.ListEntity; +import com.listywave.list.repository.list.ListRepository; +import com.listywave.reaction.application.domain.Reaction; +import com.listywave.reaction.application.domain.ReactionStats; +import com.listywave.reaction.application.domain.UserReaction; +import com.listywave.reaction.application.dto.response.ReactionResponse; +import com.listywave.reaction.repository.ReactionStatsRepository; +import com.listywave.reaction.repository.UserReactionRepository; +import com.listywave.user.application.domain.User; +import com.listywave.user.application.service.UserService; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class ReactionService { + + private final UserReactionRepository userReactionRepository; + private final ReactionStatsRepository reactionStatsRepository; + private final ListRepository listRepository; + private final UserService userService; + + public void react(Long userId, Long listId, Reaction reaction) { + User user = userService.getById(userId); + ListEntity list = listRepository.getById(listId); + + if (userReactionRepository.existsByUserIdAndListAndReaction(user.getId(), list, reaction)) { + userReactionRepository.deleteByUserIdAndListAndReaction(user.getId(), list, reaction); + updateReactionStats(list, reaction, -1); + } else { + //TODO: 리액션할 때 알림 필요 + UserReaction newReaction = UserReaction.create(user.getId(), list, reaction); + userReactionRepository.save(newReaction); + updateReactionStats(list, reaction, 1); + } + } + + public void updateReactionStats(ListEntity list, Reaction reaction, int changeCount) { + ReactionStats stats = reactionStatsRepository.findByListAndReaction(list, reaction) + .orElseGet(() -> { + ReactionStats newStats = new ReactionStats(list, reaction, 0); + reactionStatsRepository.save(newStats); + return newStats; + }); + stats.updateCount(changeCount); + } + + @Transactional(readOnly = true) + public List createReactionResponses(ListEntity list, User user, boolean isOwner) { + Map reactionStatsMap = toReactionStatsMap(list); + Set userReactionSet = toUserReactionSet(list, user); + + return Arrays.stream(Reaction.values()) + .map(reaction -> toReactionResponse(reaction, reactionStatsMap, userReactionSet, isOwner)) + .collect(Collectors.toList()); + } + + private Map toReactionStatsMap(ListEntity list) { + return reactionStatsRepository.findByList(list).stream() + .collect(Collectors.toMap(ReactionStats::getReaction, ReactionStats::getCount)); + } + + private Set toUserReactionSet(ListEntity list, User user) { + if (user == null) { + return Collections.emptySet(); + } + return userReactionRepository.findByUserIdAndList(user.getId(), list).stream() + .map(UserReaction::getReaction) + .collect(Collectors.toSet()); + } + + private ReactionResponse toReactionResponse( + Reaction reaction, + Map reactionStatsMap, + Set userReactionSet, + boolean isOwner + ) { + int count = reactionStatsMap.getOrDefault(reaction, 0); + boolean isReacted = userReactionSet.contains(reaction); + return ReactionResponse.of(reaction.name(), isOwner ? count : null, isReacted); + } +} diff --git a/src/main/java/com/listywave/reaction/presentation/controller/ReactionController.java b/src/main/java/com/listywave/reaction/presentation/controller/ReactionController.java new file mode 100644 index 00000000..15edb401 --- /dev/null +++ b/src/main/java/com/listywave/reaction/presentation/controller/ReactionController.java @@ -0,0 +1,28 @@ +package com.listywave.reaction.presentation.controller; + +import com.listywave.common.auth.Auth; +import com.listywave.reaction.application.service.ReactionService; +import com.listywave.reaction.presentation.dto.request.ReactionRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class ReactionController { + + private final ReactionService reactionService; + + @PostMapping("/lists/{listId}/reaction") + public ResponseEntity react( + @Auth Long loginUserId, + @PathVariable("listId") Long listId, + @RequestBody ReactionRequest request + ) { + reactionService.react(loginUserId, listId, request.reaction()); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/listywave/reaction/presentation/dto/request/ReactionRequest.java b/src/main/java/com/listywave/reaction/presentation/dto/request/ReactionRequest.java new file mode 100644 index 00000000..d1acb2f8 --- /dev/null +++ b/src/main/java/com/listywave/reaction/presentation/dto/request/ReactionRequest.java @@ -0,0 +1,8 @@ +package com.listywave.reaction.presentation.dto.request; + +import com.listywave.reaction.application.domain.Reaction; + +public record ReactionRequest( + Reaction reaction +) { +} diff --git a/src/main/java/com/listywave/reaction/repository/ReactionStatsRepository.java b/src/main/java/com/listywave/reaction/repository/ReactionStatsRepository.java new file mode 100644 index 00000000..8362b24e --- /dev/null +++ b/src/main/java/com/listywave/reaction/repository/ReactionStatsRepository.java @@ -0,0 +1,15 @@ +package com.listywave.reaction.repository; + +import com.listywave.list.application.domain.list.ListEntity; +import com.listywave.reaction.application.domain.Reaction; +import com.listywave.reaction.application.domain.ReactionStats; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReactionStatsRepository extends JpaRepository { + + Optional findByListAndReaction(ListEntity list, Reaction reaction); + + List findByList(ListEntity list); +} diff --git a/src/main/java/com/listywave/reaction/repository/UserReactionRepository.java b/src/main/java/com/listywave/reaction/repository/UserReactionRepository.java new file mode 100644 index 00000000..6054eac2 --- /dev/null +++ b/src/main/java/com/listywave/reaction/repository/UserReactionRepository.java @@ -0,0 +1,16 @@ +package com.listywave.reaction.repository; + +import com.listywave.list.application.domain.list.ListEntity; +import com.listywave.reaction.application.domain.Reaction; +import com.listywave.reaction.application.domain.UserReaction; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserReactionRepository extends JpaRepository { + + boolean existsByUserIdAndListAndReaction(Long userId, ListEntity list, Reaction reaction); + + void deleteByUserIdAndListAndReaction(Long userId, ListEntity list, Reaction reaction); + + List findByUserIdAndList(Long loginUserId, ListEntity list); +} diff --git a/src/test/java/com/listywave/acceptance/collection/CollectionAcceptanceTest.java b/src/test/java/com/listywave/acceptance/collection/CollectionAcceptanceTest.java index 335718be..4c99e8da 100644 --- a/src/test/java/com/listywave/acceptance/collection/CollectionAcceptanceTest.java +++ b/src/test/java/com/listywave/acceptance/collection/CollectionAcceptanceTest.java @@ -1,21 +1,14 @@ package com.listywave.acceptance.collection; -import com.listywave.acceptance.common.AcceptanceTest; -import com.listywave.collection.application.dto.CollectionResponse; -import com.listywave.collection.application.dto.CollectionResponse.CollectionListsResponse; -import com.listywave.list.application.domain.list.ListEntity; -import com.listywave.list.application.dto.response.ListCreateResponse; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.stream.Collectors; - import static com.listywave.acceptance.collection.CollectionAcceptanceTestHelper.나의_콜렉션_조회_API_호출; import static com.listywave.acceptance.collection.CollectionAcceptanceTestHelper.콜렉트_또는_콜렉트취소_API_호출; import static com.listywave.acceptance.common.CommonAcceptanceHelper.HTTP_상태_코드를_검증한다; -import static com.listywave.acceptance.folder.FolderAcceptanceTestHelper.*; -import static com.listywave.acceptance.list.ListAcceptanceTestHelper.*; +import static com.listywave.acceptance.folder.FolderAcceptanceTestHelper.폴더_생성_API_호출; +import static com.listywave.acceptance.folder.FolderAcceptanceTestHelper.폴더_생성_요청_데이터; +import static com.listywave.acceptance.folder.FolderAcceptanceTestHelper.폴더_선택_요청_데이터; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.가장_좋아하는_견종_TOP3_생성_요청_데이터; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.리스트_저장_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.회원용_리스트_상세_조회_API_호출; import static com.listywave.list.fixture.ListFixture.지정된_개수만큼_리스트를_생성한다; import static com.listywave.user.fixture.UserFixture.동호; import static com.listywave.user.fixture.UserFixture.정수; @@ -23,6 +16,16 @@ import static org.springframework.http.HttpStatus.FORBIDDEN; import static org.springframework.http.HttpStatus.NO_CONTENT; +import com.listywave.acceptance.common.AcceptanceTest; +import com.listywave.collection.application.dto.CollectionResponse; +import com.listywave.collection.application.dto.CollectionResponse.CollectionListsResponse; +import com.listywave.list.application.domain.list.ListEntity; +import com.listywave.list.application.dto.response.ListCreateResponse; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + @DisplayName("콜렉션 관련 인수테스트") public class CollectionAcceptanceTest extends AcceptanceTest { diff --git a/src/test/java/com/listywave/acceptance/list/ListAcceptanceTest.java b/src/test/java/com/listywave/acceptance/list/ListAcceptanceTest.java index a9e6b165..5aeca051 100644 --- a/src/test/java/com/listywave/acceptance/list/ListAcceptanceTest.java +++ b/src/test/java/com/listywave/acceptance/list/ListAcceptanceTest.java @@ -1,7 +1,6 @@ package com.listywave.acceptance.list; import static com.listywave.acceptance.collection.CollectionAcceptanceTestHelper.콜렉트_또는_콜렉트취소_API_호출; -import static com.listywave.acceptance.comment.CommentAcceptanceTestHelper.n개의_댓글_생성_요청; import static com.listywave.acceptance.comment.CommentAcceptanceTestHelper.댓글_저장_API_호출; import static com.listywave.acceptance.common.CommonAcceptanceHelper.HTTP_상태_코드를_검증한다; import static com.listywave.acceptance.folder.FolderAcceptanceTestHelper.폴더_생성_API_호출; @@ -26,14 +25,16 @@ import static com.listywave.acceptance.list.ListAcceptanceTestHelper.정렬기준을_포함한_검색_API_호출; import static com.listywave.acceptance.list.ListAcceptanceTestHelper.좋아하는_라면_TOP3_생성_요청_데이터; import static com.listywave.acceptance.list.ListAcceptanceTestHelper.최신_리스트_10개_조회_카테고리_필터링_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.추천_리스트_조회_API_호출; import static com.listywave.acceptance.list.ListAcceptanceTestHelper.카테고리로_검색_API_호출; import static com.listywave.acceptance.list.ListAcceptanceTestHelper.카테고리와_키워드로_검색_API_호출; import static com.listywave.acceptance.list.ListAcceptanceTestHelper.키워드로_검색_API_호출; import static com.listywave.acceptance.list.ListAcceptanceTestHelper.키워드와_정렬기준을_포함한_검색_API_호출; -import static com.listywave.acceptance.list.ListAcceptanceTestHelper.트랜딩_리스트_조회_API_호출; import static com.listywave.acceptance.list.ListAcceptanceTestHelper.팔로우한_사용자의_최신_리스트_10개_조회_API_호출; import static com.listywave.acceptance.list.ListAcceptanceTestHelper.회원_피드_리스트_조회; import static com.listywave.acceptance.list.ListAcceptanceTestHelper.회원용_리스트_상세_조회_API_호출; +import static com.listywave.acceptance.reaction.ReactionAcceptanceTestHelper.리액션_요청_데이터_리스트; +import static com.listywave.acceptance.reaction.ReactionAcceptanceTestHelper.리액션_일괄_호출; import static com.listywave.acceptance.reply.ReplyAcceptanceTestHelper.답글_등록_API_호출; import static com.listywave.list.fixture.ListFixture.가장_좋아하는_견종_TOP3; import static com.listywave.list.fixture.ListFixture.가장_좋아하는_견종_TOP3_순위_변경; @@ -69,17 +70,16 @@ import com.listywave.list.application.dto.response.ListDetailResponse; import com.listywave.list.application.dto.response.ListRecentResponse; import com.listywave.list.application.dto.response.ListSearchResponse; -import com.listywave.list.application.dto.response.ListTrandingResponse; +import com.listywave.list.application.dto.response.RecommendedListResponse; import com.listywave.list.presentation.dto.request.ItemCreateRequest; import com.listywave.list.presentation.dto.request.ListUpdateRequest; import com.listywave.list.presentation.dto.request.ReplyCreateRequest; import com.listywave.list.presentation.dto.request.comment.CommentCreateRequest; +import com.listywave.reaction.application.domain.Reaction; import com.listywave.user.application.dto.FindFeedListResponse; import com.listywave.user.application.dto.FindFeedListResponse.FeedListInfo; -import com.listywave.user.application.dto.FindFeedListResponse.ListItemsResponse; import io.restassured.common.mapper.TypeRef; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; @@ -181,7 +181,7 @@ class 리스트_상세_조회 { () -> assertThat(결과.ownerId()).isEqualTo(동호.getId()), () -> assertThat(결과.title()).isEqualTo(동호_리스트.getTitle().getValue()), () -> assertThat(결과.categoryKorName()).isEqualTo(동호_리스트.getCategory().getViewName()), - () -> assertThat(결과.collectCount()).isZero(), + () -> assertThat(결과.collectCount()).isNull(), () -> assertThat(결과.collaborators()).isEmpty() ); } @@ -209,7 +209,7 @@ class 리스트_상세_조회 { // then assertAll( () -> assertThat(결과.ownerId()).isEqualTo(동호.getId()), - () -> assertThat(결과.collectCount()).isEqualTo(1) + () -> assertThat(결과.collectCount()).isNull() ); } @@ -255,7 +255,7 @@ class 리스트_상세_조회 { var 결과 = 비회원_리스트_상세_조회_API_호출(동호_리스트_ID).as(ListDetailResponse.class); // then - var 기대값 = ListDetailResponse.of(가장_좋아하는_견종_TOP3_순위_변경(동호, List.of()), 동호, false, List.of(), 0, null, 0L); + var 기대값 = ListDetailResponse.of(가장_좋아하는_견종_TOP3_순위_변경(동호, List.of()), 동호, false, false, List.of(), 0, null, 0L, List.of()); 리스트_상세_조회를_검증한다(결과, 기대값); } @@ -344,7 +344,7 @@ class 리스트_수정 { // then var 리스트_상세_조회_결과 = 회원용_리스트_상세_조회_API_호출(동호_액세스_토큰, 동호_리스트_ID); ListEntity 수정된_리스트 = 가장_좋아하는_견종_TOP3_순위_변경(동호, List.of()); - ListDetailResponse 기대값 = ListDetailResponse.of(수정된_리스트, 동호, false, List.of(Collaborator.init(유진, 수정된_리스트)), 0, null, 0L); + ListDetailResponse 기대값 = ListDetailResponse.of(수정된_리스트, 동호, true, false, List.of(Collaborator.init(유진, 수정된_리스트)), 0, null, 0L, List.of()); 리스트_상세_조회를_검증한다(리스트_상세_조회_결과, 기대값); } @@ -627,66 +627,51 @@ class 피드_리스트_조회 { } @Nested - class 리스트_팀섹 { + class 리스트_탐색 { @Test - void 트랜딩_리스트를_조회한다() { + void 추천_리스트를_조회한다() { // given var 동호 = 회원을_저장한다(동호()); var 정수 = 회원을_저장한다(정수()); var 동호_액세스_토큰 = 액세스_토큰을_발급한다(동호); var 정수_액세스_토큰 = 액세스_토큰을_발급한다(정수); - 리스트를_모두_저장한다(지정된_개수만큼_리스트를_생성한다(동호, 5)); - 리스트를_모두_저장한다(지정된_개수만큼_리스트를_생성한다(정수, 5)); - 리스트를_모두_저장한다(지정된_개수만큼_리스트를_생성한다(동호, 5)); - var 댓글_생성_요청들 = n개의_댓글_생성_요청(4); - var 댓글_생성_요청들2 = n개의_댓글_생성_요청(8); - 댓글_생성_요청들.forEach(댓글_생성요청 -> 댓글_저장_API_호출(동호_액세스_토큰, 2L, 댓글_생성요청)); - 댓글_생성_요청들2.forEach(댓글_생성요청 -> 댓글_저장_API_호출(동호_액세스_토큰, 4L, 댓글_생성요청)); + 리스트_저장_API_호출(좋아하는_라면_TOP3_생성_요청_데이터(List.of(정수.getId())), 동호_액세스_토큰); + 리스트_저장_API_호출(좋아하는_라면_TOP3_생성_요청_데이터(List.of(정수.getId())), 정수_액세스_토큰); + 리스트_저장_API_호출(좋아하는_라면_TOP3_생성_요청_데이터(List.of(정수.getId())), 동호_액세스_토큰); + 리스트_저장_API_호출(좋아하는_라면_TOP3_생성_요청_데이터(List.of(정수.getId())), 동호_액세스_토큰); + 리스트_저장_API_호출(좋아하는_라면_TOP3_생성_요청_데이터(List.of(정수.getId())), 동호_액세스_토큰); + 리스트_저장_API_호출(좋아하는_라면_TOP3_생성_요청_데이터(List.of(정수.getId())), 동호_액세스_토큰); - var 답글_생성_요청들 = Arrays.asList(new ReplyCreateRequest("답글1", List.of()), new ReplyCreateRequest("답글2", List.of())); - 답글_생성_요청들.forEach(답글_생성요청 -> 답글_등록_API_호출(동호_액세스_토큰, 답글_생성요청, 2L, 2L)); - - var 폴더_생성_요청_데이터 = 폴더_생성_요청_데이터("맛집"); - var 동호_폴더_ID = 폴더_생성_API_호출(동호_액세스_토큰, 폴더_생성_요청_데이터) - .as(FolderCreateResponse.class) - .folderId(); - var 정수_폴더_ID = 폴더_생성_API_호출(정수_액세스_토큰, 폴더_생성_요청_데이터) - .as(FolderCreateResponse.class) - .folderId(); - var 정수_폴더_선택_데이터 = 폴더_선택_요청_데이터(정수_폴더_ID); - var 동호_폴더_선택_데이터 = 폴더_선택_요청_데이터(동호_폴더_ID); - 콜렉트_또는_콜렉트취소_API_호출(정수_액세스_토큰, 2L, 정수_폴더_선택_데이터); - 콜렉트_또는_콜렉트취소_API_호출(동호_액세스_토큰, 7L, 동호_폴더_선택_데이터); + 리액션_일괄_호출(정수_액세스_토큰, 2L, 리액션_요청_데이터_리스트(Reaction.COOL, Reaction.AGREE, Reaction.THANKS)); + 리액션_일괄_호출(동호_액세스_토큰, 2L, 리액션_요청_데이터_리스트(Reaction.COOL, Reaction.THANKS)); + 리액션_일괄_호출(동호_액세스_토큰, 1L, 리액션_요청_데이터_리스트(Reaction.COOL, Reaction.THANKS)); + 리액션_일괄_호출(동호_액세스_토큰, 5L, 리액션_요청_데이터_리스트(Reaction.THANKS)); + 리액션_일괄_호출(동호_액세스_토큰, 3L, 리액션_요청_데이터_리스트(Reaction.THANKS)); // when - List 결과 = 트랜딩_리스트_조회_API_호출().as(new TypeRef<>() { + List 결과 = 추천_리스트_조회_API_호출().as(new TypeRef<>() { }); // then - var 동호_리스트 = 비회원_피드_리스트_조회_API_호출(동호).as(FindFeedListResponse.class).feedLists(); - var 정수_리스트 = 비회원_피드_리스트_조회_API_호출(정수).as(FindFeedListResponse.class).feedLists(); - var 모든_리스트 = new ArrayList<>(동호_리스트); - 모든_리스트.addAll(정수_리스트); - - var 대표_이미지들 = 모든_리스트.stream() - .sorted(comparing(FeedListInfo::id, reverseOrder())) - .map(feedListInfo -> feedListInfo.listItems().stream() - .sorted(comparing(ListItemsResponse::rank)) - .filter(listItemsResponse -> listItemsResponse.imageUrl() != null && !listItemsResponse.imageUrl().isBlank()) - .map(ListItemsResponse::imageUrl) - .findFirst() - .orElse("")) - .toList(); - assertAll( - () -> assertThat(결과).usingRecursiveComparison() - .comparingOnlyFields("itemImageUrl") - .isEqualTo(대표_이미지들), - () -> assertThat(결과.get(0).trandingScore()).isEqualTo(16), - () -> assertThat(결과.get(1).trandingScore()).isEqualTo(15), - () -> assertThat(결과.get(2).trandingScore()).isEqualTo(3) + () -> assertThat(결과).hasSize(4), + () -> assertThat(결과.get(0).id()).isEqualTo(2L), + () -> assertThat(결과.get(1).id()).isEqualTo(1L), + () -> assertThat(결과.get(2).id()).isEqualTo(5L), + () -> assertThat(결과.get(3).id()).isEqualTo(3L), + + () -> assertThat(결과.get(0).ownerNickname()).isEqualTo("pparkjs"), + () -> assertThat(결과.get(1).ownerNickname()).isEqualTo("kdkdhoho"), + + () -> assertThat(결과.get(0).items()).hasSize(3), + () -> assertThat(결과.get(0).items().get(0).rank()).isEqualTo(1), + () -> assertThat(결과.get(0).items().get(1).rank()).isEqualTo(2), + () -> assertThat(결과.get(0).items().get(2).rank()).isEqualTo(3), + + () -> assertThat(결과.get(0).itemImageUrl()).isEqualTo("이미지1"), + () -> assertThat(결과.get(1).itemImageUrl()).isEqualTo("이미지1") ); } diff --git a/src/test/java/com/listywave/acceptance/list/ListAcceptanceTestHelper.java b/src/test/java/com/listywave/acceptance/list/ListAcceptanceTestHelper.java index 4ac86381..23fd9dde 100644 --- a/src/test/java/com/listywave/acceptance/list/ListAcceptanceTestHelper.java +++ b/src/test/java/com/listywave/acceptance/list/ListAcceptanceTestHelper.java @@ -3,6 +3,7 @@ import static com.listywave.acceptance.common.CommonAcceptanceHelper.given; import static com.listywave.list.application.domain.category.CategoryType.MOVIE_DRAMA; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.springframework.http.HttpHeaders.AUTHORIZATION; import com.listywave.history.application.dto.HistorySearchResponse; @@ -104,18 +105,21 @@ public abstract class ListAcceptanceTestHelper { BackgroundPalette.PASTEL, BackgroundColor.PASTEL_GREEN, List.of( - new ItemCreateRequest(1, "신라면", "", "", ""), - new ItemCreateRequest(2, "육개장 사발면", "", "", ""), - new ItemCreateRequest(3, "김치 사발면", "", "", "") + new ItemCreateRequest(1, "신라면", "", "", "이미지1"), + new ItemCreateRequest(2, "육개장 사발면", "", "", "이미지2"), + new ItemCreateRequest(3, "김치 사발면", "", "", "이미지3") ) ); } public static void 리스트_상세_조회를_검증한다(ListDetailResponse 결과값, ListDetailResponse 기대값) { - assertThat(결과값).usingRecursiveComparison() - .ignoringFieldsOfTypes(Long.class) - .ignoringFields("createdDate", "lastUpdatedDate") - .isEqualTo(기대값); + assertAll( + () -> assertThat(결과값).usingRecursiveComparison() + .ignoringFieldsOfTypes(Long.class) + .ignoringFields("createdDate", "lastUpdatedDate", "reactions", "updateCount") + .isEqualTo(기대값), + () -> assertThat(결과값.updateCount()).isOne() + ); } public static List 비회원_히스토리_조회_API_호출(Long listId) { @@ -177,9 +181,9 @@ public abstract class ListAcceptanceTestHelper { .extract(); } - public static ExtractableResponse 트랜딩_리스트_조회_API_호출() { + public static ExtractableResponse 추천_리스트_조회_API_호출() { return given() - .when().get("/lists/explore") + .when().get("/lists/recommended") .then().log().all() .extract(); } diff --git a/src/test/java/com/listywave/acceptance/reaction/ReactionAcceptanceTest.java b/src/test/java/com/listywave/acceptance/reaction/ReactionAcceptanceTest.java new file mode 100644 index 00000000..3e36a6ba --- /dev/null +++ b/src/test/java/com/listywave/acceptance/reaction/ReactionAcceptanceTest.java @@ -0,0 +1,108 @@ +package com.listywave.acceptance.reaction; + +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.가장_좋아하는_견종_TOP3_생성_요청_데이터; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.리스트_저장_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.비회원_리스트_상세_조회_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.회원용_리스트_상세_조회_API_호출; +import static com.listywave.acceptance.reaction.ReactionAcceptanceTestHelper.리액션_API_호출; +import static com.listywave.acceptance.reaction.ReactionAcceptanceTestHelper.리액션_요청_데이터_리스트; +import static com.listywave.acceptance.reaction.ReactionAcceptanceTestHelper.리액션_일괄_호출; +import static com.listywave.user.fixture.UserFixture.동호; +import static com.listywave.user.fixture.UserFixture.정수; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.listywave.acceptance.common.AcceptanceTest; +import com.listywave.list.application.dto.response.ListCreateResponse; +import com.listywave.list.application.dto.response.ListDetailResponse; +import com.listywave.reaction.application.domain.Reaction; +import com.listywave.reaction.presentation.dto.request.ReactionRequest; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("리액션 관련 인수테스트") +public class ReactionAcceptanceTest extends AcceptanceTest { + + @Test + void 리스트에_대한_리액션을_성공적으로_수행한다() { + // given + var 정수 = 회원을_저장한다(정수()); + var 동호 = 회원을_저장한다(동호()); + var 정수_액세스_토큰 = 액세스_토큰을_발급한다(정수); + var 동호_액세스_토큰 = 액세스_토큰을_발급한다(동호); + var 정수_리스트_ID = 리스트_저장_API_호출(가장_좋아하는_견종_TOP3_생성_요청_데이터(List.of()), 정수_액세스_토큰) + .as(ListCreateResponse.class) + .listId(); + + // when + 리액션_일괄_호출(정수_액세스_토큰, 정수_리스트_ID, 리액션_요청_데이터_리스트(Reaction.COOL, Reaction.AGREE)); + 리액션_일괄_호출(동호_액세스_토큰, 정수_리스트_ID, 리액션_요청_데이터_리스트(Reaction.COOL)); + var 결과 = 회원용_리스트_상세_조회_API_호출(정수_액세스_토큰, 정수_리스트_ID); + + // then + assertAll( + () -> assertThat(결과.reactions().get(0).count()).isEqualTo(2), + () -> assertThat(결과.reactions().get(0).reaction()).isEqualTo("COOL"), + () -> assertThat(결과.reactions().get(1).count()).isEqualTo(1), + () -> assertThat(결과.reactions().get(1).reaction()).isEqualTo("AGREE"), + () -> assertThat(결과.reactions().get(2).count()).isEqualTo(0), + () -> assertThat(결과.reactions().get(2).reaction()).isEqualTo("THANKS") + ); + } + + @Test + void 리스트에_대한_리액션을_취소한다() { + // given + var 정수 = 회원을_저장한다(정수()); + var 동호 = 회원을_저장한다(동호()); + var 정수_액세스_토큰 = 액세스_토큰을_발급한다(정수); + var 동호_액세스_토큰 = 액세스_토큰을_발급한다(동호); + var 정수_리스트_ID = 리스트_저장_API_호출(가장_좋아하는_견종_TOP3_생성_요청_데이터(List.of()), 정수_액세스_토큰) + .as(ListCreateResponse.class) + .listId(); + + // when + 리액션_일괄_호출(동호_액세스_토큰, 정수_리스트_ID, 리액션_요청_데이터_리스트(Reaction.COOL, Reaction.COOL)); + + var 결과 = 회원용_리스트_상세_조회_API_호출(정수_액세스_토큰, 정수_리스트_ID); + + // then + assertAll( + () -> assertThat(결과.reactions().get(0).count()).isEqualTo(0), + () -> assertThat(결과.reactions().get(0).reaction()).isEqualTo("COOL"), + () -> assertThat(결과.reactions().get(1).count()).isEqualTo(0), + () -> assertThat(결과.reactions().get(1).reaction()).isEqualTo("AGREE"), + () -> assertThat(결과.reactions().get(2).count()).isEqualTo(0), + () -> assertThat(결과.reactions().get(2).reaction()).isEqualTo("THANKS") + ); + } + + @Test + void 리스트_생성자가_아닌_사용자_및_비회원은_리액션_수를_볼_수_없다() { + // given + var 정수 = 회원을_저장한다(정수()); + var 동호 = 회원을_저장한다(동호()); + var 정수_액세스_토큰 = 액세스_토큰을_발급한다(정수); + var 동호_액세스_토큰 = 액세스_토큰을_발급한다(동호); + var 정수_리스트_ID = 리스트_저장_API_호출(가장_좋아하는_견종_TOP3_생성_요청_데이터(List.of()), 정수_액세스_토큰) + .as(ListCreateResponse.class) + .listId(); + var 동호_리액션_요청_데이터1 = new ReactionRequest(Reaction.COOL); + + // when + 리액션_API_호출(동호_액세스_토큰, 정수_리스트_ID, 동호_리액션_요청_데이터1); + var 비회원_상세_결과 = 비회원_리스트_상세_조회_API_호출(정수_리스트_ID).as(ListDetailResponse.class); + var 비소유자_상세_결과 = 회원용_리스트_상세_조회_API_호출(동호_액세스_토큰, 정수_리스트_ID); + + // then + assertAll( + () -> assertThat(비회원_상세_결과.reactions().get(0).count()).isNull(), + () -> assertThat(비회원_상세_결과.reactions().get(0).count()).isNull(), + () -> assertThat(비회원_상세_결과.reactions().get(0).count()).isNull(), + () -> assertThat(비소유자_상세_결과.reactions().get(0).count()).isNull(), + () -> assertThat(비소유자_상세_결과.reactions().get(0).count()).isNull(), + () -> assertThat(비소유자_상세_결과.reactions().get(0).count()).isNull() + ); + } +} diff --git a/src/test/java/com/listywave/acceptance/reaction/ReactionAcceptanceTestHelper.java b/src/test/java/com/listywave/acceptance/reaction/ReactionAcceptanceTestHelper.java new file mode 100644 index 00000000..845b3083 --- /dev/null +++ b/src/test/java/com/listywave/acceptance/reaction/ReactionAcceptanceTestHelper.java @@ -0,0 +1,34 @@ +package com.listywave.acceptance.reaction; + +import static com.listywave.acceptance.common.CommonAcceptanceHelper.given; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + +import com.listywave.reaction.application.domain.Reaction; +import com.listywave.reaction.presentation.dto.request.ReactionRequest; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public abstract class ReactionAcceptanceTestHelper { + + public static ExtractableResponse 리액션_API_호출(String accessToken, Long listId, ReactionRequest request) { + return given() + .header(AUTHORIZATION, "Bearer " + accessToken) + .body(request) + .when().post("/lists/{listId}/reaction", listId) + .then().log().all() + .extract(); + } + + public static List 리액션_요청_데이터_리스트(Reaction... reactions) { + return Arrays.stream(reactions) + .map(ReactionRequest::new) + .collect(Collectors.toList()); + } + + public static void 리액션_일괄_호출(String accessToken, Long listId, List reactionRequests) { + reactionRequests.forEach(reaction -> 리액션_API_호출(accessToken, listId, reaction)); + } +}