diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java index 8605655e5..12ee688a9 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java @@ -5,15 +5,18 @@ import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.domain.Book; import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.room.adapter.out.persistence.RoomJpaRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import static konkuk.thip.common.exception.code.ErrorCode.BOOK_NOT_FOUND; +import static konkuk.thip.common.exception.code.ErrorCode.ROOM_NOT_FOUND; @Repository @RequiredArgsConstructor public class BookCommandPersistenceAdapter implements BookCommandPort { + private final RoomJpaRepository roomJpaRepository; private final BookJpaRepository bookJpaRepository; private final BookMapper bookMapper; @@ -49,4 +52,12 @@ public void updateForPageCount(Book book) { bookJpaEntity.changePageCount(book.getPageCount()); bookJpaRepository.save(bookJpaEntity); } + + @Override + public Book findBookByRoomId(Long roomId) { + BookJpaEntity bookJpaEntity = roomJpaRepository.findById(roomId).orElseThrow( + () -> new EntityNotFoundException(ROOM_NOT_FOUND) + ).getBookJpaEntity(); + return bookMapper.toDomainEntity(bookJpaEntity); + } } diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java index c7ce10e8e..bcc32d044 100644 --- a/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java +++ b/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java @@ -12,4 +12,6 @@ public interface BookCommandPort { Book findById(Long id); void updateForPageCount(Book book); + + Book findBookByRoomId(Long roomId); } diff --git a/src/main/java/konkuk/thip/common/util/DateUtil.java b/src/main/java/konkuk/thip/common/util/DateUtil.java index 872c861e1..cd27b84f7 100644 --- a/src/main/java/konkuk/thip/common/util/DateUtil.java +++ b/src/main/java/konkuk/thip/common/util/DateUtil.java @@ -1,16 +1,13 @@ package konkuk.thip.common.util; -import org.springframework.stereotype.Component; - import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; -@Component public class DateUtil { //마지막 활동 시간 포맷팅 -> ex. 1분 전, 1시간 전, 1일 전 - public String formatLastActivityTime(LocalDateTime createdAt) { + public static String formatBeforeTime(LocalDateTime createdAt) { long minutes = Duration.between(createdAt, LocalDateTime.now()).toMinutes(); if (minutes < 1) return "방금 전"; if (minutes < 60) return minutes + "분 전"; diff --git a/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java b/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java index 34aa496aa..2dcd983d1 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java @@ -28,8 +28,8 @@ public class FeedJpaEntity extends PostJpaEntity { private BookJpaEntity bookJpaEntity; @Builder - public FeedJpaEntity(String content, UserJpaEntity userJpaEntity, Boolean isPublic, int reportCount, BookJpaEntity bookJpaEntity) { - super(content, userJpaEntity); + public FeedJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity, Boolean isPublic, int reportCount, BookJpaEntity bookJpaEntity) { + super(content, likeCount, commentCount, userJpaEntity); this.isPublic = isPublic; this.reportCount = reportCount; this.bookJpaEntity = bookJpaEntity; diff --git a/src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java b/src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java index d680f3ff4..d4106cf3b 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java @@ -19,6 +19,8 @@ public FeedJpaEntity toJpaEntity(Feed feed, UserJpaEntity userJpaEntity, BookJpa .userJpaEntity(userJpaEntity) .isPublic(feed.getIsPublic()) .reportCount(feed.getReportCount()) + .likeCount(feed.getLikeCount()) + .commentCount(feed.getCommentCount()) .bookJpaEntity(bookJpaEntity) .build(); } @@ -30,6 +32,8 @@ public Feed toDomainEntity(FeedJpaEntity feedJpaEntity, List tagJp .creatorId(feedJpaEntity.getUserJpaEntity().getUserId()) .isPublic(feedJpaEntity.getIsPublic()) .reportCount(feedJpaEntity.getReportCount()) + .likeCount(feedJpaEntity.getLikeCount()) + .commentCount(feedJpaEntity.getCommentCount()) .targetBookId(feedJpaEntity.getBookJpaEntity().getBookId()) .tagList(tagJpaEntityList.stream() .map(TagJpaEntity::getValue) diff --git a/src/main/java/konkuk/thip/feed/domain/Feed.java b/src/main/java/konkuk/thip/feed/domain/Feed.java index 8a25b238a..4e7af7b9b 100644 --- a/src/main/java/konkuk/thip/feed/domain/Feed.java +++ b/src/main/java/konkuk/thip/feed/domain/Feed.java @@ -1,6 +1,7 @@ package konkuk.thip.feed.domain; import konkuk.thip.common.entity.BaseDomainEntity; +import lombok.Builder; import lombok.Getter; import lombok.experimental.SuperBuilder; @@ -18,19 +19,28 @@ public class Feed extends BaseDomainEntity { private Boolean isPublic; - private int reportCount; + @Builder.Default + private Integer reportCount = 0; + + @Builder.Default + private Integer likeCount = 0; + + @Builder.Default + private Integer commentCount = 0; private Long targetBookId; private List tagList; - public static Feed withoutId(String content, Long creatorId, Boolean isPublic, int reportCount, Long targetBookId, List tagList) { + public static Feed withoutId(String content, Long creatorId, Boolean isPublic, Long targetBookId, List tagList) { return Feed.builder() .id(null) .content(content) .creatorId(creatorId) .isPublic(isPublic) - .reportCount(reportCount) + .reportCount(0) + .likeCount(0) + .commentCount(0) .targetBookId(targetBookId) .tagList(tagList) .build(); diff --git a/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java b/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java index c23cacd06..35fce5ada 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java +++ b/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java @@ -23,12 +23,18 @@ public abstract class PostJpaEntity extends BaseJpaEntity { @Column(length = 6100, nullable = false) private String content; + private Integer likeCount = 0; + + private Integer commentCount = 0; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; - public PostJpaEntity(String content, UserJpaEntity userJpaEntity) { + public PostJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity) { this.content = content; + this.likeCount = likeCount; + this.commentCount = commentCount; this.userJpaEntity = userJpaEntity; } } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/record/adapter/in/web/RecordQueryController.java b/src/main/java/konkuk/thip/record/adapter/in/web/RecordQueryController.java index 201413f12..93162c9c7 100644 --- a/src/main/java/konkuk/thip/record/adapter/in/web/RecordQueryController.java +++ b/src/main/java/konkuk/thip/record/adapter/in/web/RecordQueryController.java @@ -3,7 +3,6 @@ import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; import konkuk.thip.record.adapter.in.web.response.RecordSearchResponse; -import konkuk.thip.record.application.port.in.dto.RecordSearchQuery; import konkuk.thip.record.application.port.in.dto.RecordSearchUseCase; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; @@ -20,24 +19,15 @@ public class RecordQueryController { @GetMapping("/rooms/{roomId}/posts") public BaseResponse viewRecordList( @PathVariable final Long roomId, - @RequestParam final String type, - @RequestParam final String sort, + @RequestParam(required = false) final String type, + @RequestParam(required = false) final String sort, @RequestParam(required = false) final Integer pageStart, @RequestParam(required = false) final Integer pageEnd, + @RequestParam final Boolean isOverview, @RequestParam final Integer pageNum, @UserId final Long userId ) { - return BaseResponse.ok(recordSearchUseCase.search( - RecordSearchQuery.builder() - .roomId(roomId) - .type(type) - .sort(sort) - .pageStart(pageStart) - .pageEnd(pageEnd) - .pageNum(pageNum) - .userId(userId) - .build() - )); + return BaseResponse.ok(recordSearchUseCase.search(roomId, type, sort, pageStart, pageEnd, isOverview, pageNum, userId)); } } diff --git a/src/main/java/konkuk/thip/record/adapter/in/web/response/RecordDto.java b/src/main/java/konkuk/thip/record/adapter/in/web/response/RecordDto.java index 148fe5c05..f5c04298e 100644 --- a/src/main/java/konkuk/thip/record/adapter/in/web/response/RecordDto.java +++ b/src/main/java/konkuk/thip/record/adapter/in/web/response/RecordDto.java @@ -1,7 +1,5 @@ package konkuk.thip.record.adapter.in.web.response; -import konkuk.thip.record.domain.Record; -import konkuk.thip.user.domain.User; import lombok.Builder; @Builder @@ -17,25 +15,25 @@ public record RecordDto( boolean isLiked, boolean isWriter, Long recordId -) implements RecordSearchResponse.PostDto { +) implements RecordSearchResponse.RecordSearchResult { @Override public String type() { return "RECORD"; } - public static RecordDto of(Record record, String postDate, User user, int likeCount, int commentCount, boolean isLiked, boolean isWriter) { - return RecordDto.builder() - .postDate(postDate) - .page(record.getPage()) - .userId(record.getCreatorId()) - .nickName(user.getNickname()) - .profileImageUrl(user.getAlias().getImageUrl()) - .content(record.getContent()) - .likeCount(likeCount) - .commentCount(commentCount) - .isLiked(isLiked) - .isWriter(isWriter) - .recordId(record.getId()) - .build(); + public RecordDto withIsLiked(boolean isLiked) { + return new RecordDto( + postDate, + page, + userId, + nickName, + profileImageUrl, + content, + likeCount, + commentCount, + isLiked, + isWriter, + recordId + ); } } diff --git a/src/main/java/konkuk/thip/record/adapter/in/web/response/RecordSearchResponse.java b/src/main/java/konkuk/thip/record/adapter/in/web/response/RecordSearchResponse.java index 0a68a857b..46f0edd21 100644 --- a/src/main/java/konkuk/thip/record/adapter/in/web/response/RecordSearchResponse.java +++ b/src/main/java/konkuk/thip/record/adapter/in/web/response/RecordSearchResponse.java @@ -6,14 +6,14 @@ import java.util.List; public record RecordSearchResponse( - List recordList, + List recordList, Integer page, Integer size, Boolean first, Boolean last ){ - public static RecordSearchResponse of(List recordList, + public static RecordSearchResponse of(List recordList, Integer page, Integer size, Boolean first, @@ -26,7 +26,7 @@ public static RecordSearchResponse of(List recordList, @JsonSubTypes.Type(value = RecordDto.class, name = "RECORD"), @JsonSubTypes.Type(value = VoteDto.class, name = "VOTE") }) - public sealed interface PostDto permits RecordDto, VoteDto { + public sealed interface RecordSearchResult permits RecordDto, VoteDto { String type(); String postDate(); int page(); diff --git a/src/main/java/konkuk/thip/record/adapter/in/web/response/VoteDto.java b/src/main/java/konkuk/thip/record/adapter/in/web/response/VoteDto.java index cec556857..6907ddde1 100644 --- a/src/main/java/konkuk/thip/record/adapter/in/web/response/VoteDto.java +++ b/src/main/java/konkuk/thip/record/adapter/in/web/response/VoteDto.java @@ -1,7 +1,5 @@ package konkuk.thip.record.adapter.in.web.response; -import konkuk.thip.user.domain.User; -import konkuk.thip.vote.domain.Vote; import konkuk.thip.vote.domain.VoteItem; import lombok.Builder; @@ -21,30 +19,27 @@ public record VoteDto( boolean isWriter, Long voteId, List voteItems -) implements RecordSearchResponse.PostDto { +) implements RecordSearchResponse.RecordSearchResult { @Override public String type() { return "VOTE"; } - public static VoteDto of( - Vote vote, String postDate, User user, int likeCount, int commentCount, boolean isLiked, boolean isWriter, - List voteItems - ) { - return VoteDto.builder() - .postDate(postDate) - .page(vote.getPage()) - .userId(vote.getCreatorId()) - .nickName(user.getNickname()) - .profileImageUrl(user.getAlias().getImageUrl()) - .content(vote.getContent()) - .likeCount(likeCount) - .commentCount(commentCount) - .isLiked(isLiked) - .isWriter(isWriter) - .voteId(vote.getId()) - .voteItems(voteItems) - .build(); + public VoteDto withIsLikedAndVoteItems(boolean isLiked, List voteItems) { + return new VoteDto( + postDate, + page, + userId, + nickName, + profileImageUrl, + content, + likeCount, + commentCount, + isLiked, + isWriter, + voteId, + voteItems + ); } public record VoteItemDto( diff --git a/src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java b/src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java index 5ad164b39..b18654d09 100644 --- a/src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java +++ b/src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java @@ -26,8 +26,8 @@ public class RecordJpaEntity extends PostJpaEntity { private RoomJpaEntity roomJpaEntity; @Builder - public RecordJpaEntity(String content, UserJpaEntity userJpaEntity, Integer page, boolean isOverview, RoomJpaEntity roomJpaEntity) { - super(content, userJpaEntity); + public RecordJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity, Integer page, boolean isOverview, RoomJpaEntity roomJpaEntity) { + super(content, likeCount, commentCount, userJpaEntity); this.page = page; this.isOverview = isOverview; this.roomJpaEntity = roomJpaEntity; diff --git a/src/main/java/konkuk/thip/record/adapter/out/mapper/RecordMapper.java b/src/main/java/konkuk/thip/record/adapter/out/mapper/RecordMapper.java index 657095717..6997190e6 100644 --- a/src/main/java/konkuk/thip/record/adapter/out/mapper/RecordMapper.java +++ b/src/main/java/konkuk/thip/record/adapter/out/mapper/RecordMapper.java @@ -12,6 +12,8 @@ public class RecordMapper { public RecordJpaEntity toJpaEntity(Record record, UserJpaEntity userJpaEntity, RoomJpaEntity roomJpaEntity) { return RecordJpaEntity.builder() .content(record.getContent()) + .likeCount(record.getLikeCount()) + .commentCount(record.getCommentCount()) .userJpaEntity(userJpaEntity) .page(record.getPage()) .isOverview(record.isOverview()) @@ -27,6 +29,8 @@ public Record toDomainEntity(RecordJpaEntity recordJpaEntity) { .page(recordJpaEntity.getPage()) .isOverview(recordJpaEntity.isOverview()) .roomId(recordJpaEntity.getRoomJpaEntity().getRoomId()) + .likeCount(recordJpaEntity.getLikeCount()) + .commentCount(recordJpaEntity.getCommentCount()) .createdAt(recordJpaEntity.getCreatedAt()) .modifiedAt(recordJpaEntity.getModifiedAt()) .status(recordJpaEntity.getStatus()) diff --git a/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordQueryPersistenceAdapter.java index 8cc37ef58..2d9ccbbec 100644 --- a/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordQueryPersistenceAdapter.java @@ -1,39 +1,21 @@ package konkuk.thip.record.adapter.out.persistence; -import konkuk.thip.record.adapter.out.mapper.RecordMapper; -import konkuk.thip.record.application.port.in.dto.RecordSearchResult; +import konkuk.thip.record.adapter.in.web.response.RecordSearchResponse; import konkuk.thip.record.application.port.out.RecordQueryPort; -import konkuk.thip.record.domain.Record; -import konkuk.thip.vote.adapter.out.mapper.VoteMapper; -import konkuk.thip.vote.adapter.out.persistence.VoteJpaRepository; -import konkuk.thip.vote.domain.Vote; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; -import java.util.List; - @Repository @RequiredArgsConstructor public class RecordQueryPersistenceAdapter implements RecordQueryPort { private final RecordJpaRepository recordJpaRepository; - private final VoteJpaRepository voteJpaRepository; - private final RecordMapper recordMapper; - private final VoteMapper voteMapper; - - private static final Integer PAGE_SIZE = 10; @Override - public RecordSearchResult findRecordsByRoom(Long roomId, String type, Integer pageStart, Integer pageEnd, Long userId, Integer pageNum) { - List records = recordJpaRepository.findRecordsByRoom(roomId, type, pageStart, pageEnd, userId).stream() - .map(recordMapper::toDomainEntity) - .toList(); - - List votes = voteJpaRepository.findVotesByRoom(roomId, type, pageStart, pageEnd, userId).stream() - .map(voteMapper::toDomainEntity) - .toList(); - - return RecordSearchResult.of(records, votes); + public Page findRecordsByRoom(Long roomId, String type, Integer pageStart, Integer pageEnd, Boolean isOverview, Long userId, Pageable pageable) { + return recordJpaRepository.findRecordsByRoom(roomId, type, pageStart, pageEnd, isOverview, userId, pageable); } } diff --git a/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordQueryRepository.java b/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordQueryRepository.java index e6983e320..af4495ae2 100644 --- a/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordQueryRepository.java +++ b/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordQueryRepository.java @@ -1,11 +1,11 @@ package konkuk.thip.record.adapter.out.persistence; -import konkuk.thip.record.adapter.out.jpa.RecordJpaEntity; - -import java.util.List; +import konkuk.thip.record.adapter.in.web.response.RecordSearchResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; public interface RecordQueryRepository { - List findRecordsByRoom(Long roomId, String type, Integer pageStart, Integer pageEnd, Long userId); + Page findRecordsByRoom(Long roomId, String viewType, Integer pageStart, Integer pageEnd, Boolean isOverview, Long userId, Pageable pageable); } diff --git a/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordQueryRepositoryImpl.java b/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordQueryRepositoryImpl.java index 99bdd6de2..f5d0edf24 100644 --- a/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordQueryRepositoryImpl.java @@ -1,46 +1,165 @@ package konkuk.thip.record.adapter.out.persistence; -import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; import com.querydsl.jpa.impl.JPAQueryFactory; +import konkuk.thip.common.exception.InvalidStateException; +import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.common.util.DateUtil; +import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; +import konkuk.thip.post.adapter.out.jpa.QPostJpaEntity; +import konkuk.thip.record.adapter.in.web.response.RecordDto; +import konkuk.thip.record.adapter.in.web.response.RecordSearchResponse; +import konkuk.thip.record.adapter.in.web.response.VoteDto; import konkuk.thip.record.adapter.out.jpa.QRecordJpaEntity; import konkuk.thip.record.adapter.out.jpa.RecordJpaEntity; -import konkuk.thip.user.adapter.out.jpa.QUserJpaEntity; +import konkuk.thip.vote.adapter.out.jpa.QVoteJpaEntity; +import konkuk.thip.vote.adapter.out.jpa.VoteJpaEntity; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Repository; +import java.util.ArrayList; import java.util.List; +import java.util.Optional; @Repository @RequiredArgsConstructor public class RecordQueryRepositoryImpl implements RecordQueryRepository { - private final JPAQueryFactory jpaQueryFactory; + private final JPAQueryFactory queryFactory; @Override - public List findRecordsByRoom(Long roomId, String type, Integer pageStart, Integer pageEnd, Long userId) { + public Page findRecordsByRoom(Long roomId, String viewType, Integer pageStart, Integer pageEnd, Boolean isOverview, Long loginUserId, Pageable pageable) { + QPostJpaEntity post = QPostJpaEntity.postJpaEntity; QRecordJpaEntity record = QRecordJpaEntity.recordJpaEntity; - QUserJpaEntity user = QUserJpaEntity.userJpaEntity; - - return jpaQueryFactory - .select(record) - .from(record) - .leftJoin(record.userJpaEntity, user).fetchJoin() - .where( - record.roomJpaEntity.roomId.eq(roomId), - filterByType(type, record, userId), - (startEndNull(pageStart, pageEnd) ? record.isOverview.isTrue() : record.page.between(pageStart, pageEnd)) - ) + QVoteJpaEntity vote = QVoteJpaEntity.voteJpaEntity; + + BooleanBuilder where = new BooleanBuilder(); + where.and(buildRecordCondition(roomId, pageStart, pageEnd, isOverview, post, record). + or(buildVoteCondition(roomId, pageStart, pageEnd, isOverview, post, vote))); + + if ("mine".equals(viewType)) { + where.and(post.userJpaEntity.userId.eq(loginUserId)); + } + + List> orderSpecifiers = createOrderSpecifiers(pageable, record, vote, post); + + List posts = queryFactory + .selectFrom(post) + .leftJoin(record).on(post.postId.eq(record.postId)) + .leftJoin(vote).on(post.postId.eq(vote.postId)) + .where(where) + .orderBy(orderSpecifiers.toArray(new OrderSpecifier[0])) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) .fetch(); + + List resultList = posts.stream() + .map(p -> { + if (p instanceof RecordJpaEntity r) { + return RecordDto.builder() + .postDate(DateUtil.formatBeforeTime(r.getCreatedAt())) + .page(r.getPage()) + .userId(r.getUserJpaEntity().getUserId()) + .nickName(r.getUserJpaEntity().getNickname()) + .profileImageUrl(r.getUserJpaEntity().getImageUrl()) + .content(r.getContent()) + .likeCount(safeInt(r.getLikeCount())) + .commentCount(safeInt(r.getCommentCount())) + .isLiked(false) // 초기값은 false, 서비스 레벨에서 처리 + .isWriter(loginUserId.equals(r.getUserJpaEntity().getUserId())) + .recordId(r.getPostId()) + .build(); + } else if (p instanceof VoteJpaEntity v) { + // VoteItem은 양방향 매핑이 없으므로 빈 리스트로 처리하고 서비스 레벨에서 파싱 + return VoteDto.builder() + .postDate(DateUtil.formatBeforeTime(v.getCreatedAt())) + .page(v.getPage()) + .userId(v.getUserJpaEntity().getUserId()) + .nickName(v.getUserJpaEntity().getNickname()) + .profileImageUrl(v.getUserJpaEntity().getImageUrl()) + .content(v.getContent()) + .likeCount(safeInt(v.getLikeCount())) + .commentCount(safeInt(v.getCommentCount())) + .isLiked(false) // 초기값은 false, 서비스 레벨에서 처리 + .isWriter(loginUserId.equals(v.getUserJpaEntity().getUserId())) + .voteId(v.getPostId()) + .voteItems(new ArrayList<>()) // 빈 리스트로 초기화, 서비스 레벨에서 처리 + .build(); + } else { + throw new InvalidStateException(ErrorCode.API_SERVER_ERROR, new IllegalStateException("지원되지 않는 게시물 타입: " + p.getClass().getSimpleName())); + } + }) + .map(result -> (RecordSearchResponse.RecordSearchResult) result) + .toList(); + + Long totalCount = queryFactory + .select(post.count()) + .from(post) + .leftJoin(record).on(post.postId.eq(record.postId)) + .leftJoin(vote).on(post.postId.eq(vote.postId)) + .where(where) + .fetchOne(); + long total = (totalCount != null) ? totalCount : 0L; + + return new PageImpl<>(resultList, pageable, total); } - private boolean startEndNull(Integer start, Integer end) { - return start == null && end == null; + private Integer safeInt(Number number) { + return Optional.ofNullable(number).map(Number::intValue).orElse(0); } - private BooleanExpression filterByType(String type, QRecordJpaEntity post, Long userId) { - if ("mine".equalsIgnoreCase(type)) { - return post.userJpaEntity.userId.eq(userId); + private List> createOrderSpecifiers(Pageable pageable, QRecordJpaEntity record, QVoteJpaEntity vote, QPostJpaEntity post) { + List> orderSpecifiers = new ArrayList<>(); + for (Sort.Order order : pageable.getSort()) { + String property = order.getProperty(); + boolean asc = order.getDirection().isAscending(); + + if ("likeCount".equalsIgnoreCase(property)) { + orderSpecifiers.add(new OrderSpecifier<>(asc ? Order.ASC : Order.DESC, + record.likeCount.coalesce(0).add(vote.likeCount.coalesce(0)))); + } else if ("commentCount".equalsIgnoreCase(property)) { + orderSpecifiers.add(new OrderSpecifier<>(asc ? Order.ASC : Order.DESC, + record.commentCount.coalesce(0).add(vote.commentCount.coalesce(0)))); + } else if ("createdAt".equalsIgnoreCase(property)) { + orderSpecifiers.add(asc ? post.createdAt.asc() : post.createdAt.desc()); + } else { + orderSpecifiers.add(post.createdAt.desc()); + } + } + return orderSpecifiers; + } + + private BooleanBuilder buildVoteCondition(Long roomId, Integer pageStart, Integer pageEnd, Boolean isOverview, QPostJpaEntity post, QVoteJpaEntity vote) { + BooleanBuilder voteCondition = new BooleanBuilder(); + voteCondition.and(post.instanceOf(VoteJpaEntity.class)) + .and(vote.roomJpaEntity.roomId.eq(roomId)); + + if (isOverview) { + voteCondition.and(vote.isOverview.isTrue()); + } else { + voteCondition.and(vote.isOverview.isFalse()) + .and(vote.page.between(pageStart, pageEnd)); + } + return voteCondition; + } + + private BooleanBuilder buildRecordCondition(Long roomId, Integer pageStart, Integer pageEnd, Boolean isOverview, QPostJpaEntity post, QRecordJpaEntity record) { + BooleanBuilder recordCondition = new BooleanBuilder(); + recordCondition.and(post.instanceOf(RecordJpaEntity.class)) + .and(record.roomJpaEntity.roomId.eq(roomId)); + + if (isOverview) { + recordCondition.and(record.isOverview.isTrue()); + } else { + recordCondition.and(record.isOverview.isFalse()) + .and(record.page.between(pageStart, pageEnd)); } - return null; + return recordCondition; } } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordSearchSortParams.java b/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordSearchSortParams.java new file mode 100644 index 000000000..ebaa373b7 --- /dev/null +++ b/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordSearchSortParams.java @@ -0,0 +1,30 @@ +package konkuk.thip.record.adapter.out.persistence; + +import konkuk.thip.common.exception.InvalidStateException; +import konkuk.thip.common.exception.code.ErrorCode; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +public enum RecordSearchSortParams { + + LATEST("latest"), + LIKE("like"), + COMMENT("comment"); + + private final String value; + + RecordSearchSortParams(String value) { + this.value = value; + } + + public static RecordSearchSortParams from(String value) { + return Arrays.stream(RecordSearchSortParams.values()) + .filter(param -> param.getValue().equals(value)) + .findFirst() + .orElseThrow( + () -> new InvalidStateException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("현재 정렬 조건 param : " + value)) + ); + } +} diff --git a/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordSearchTypeParams.java b/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordSearchTypeParams.java new file mode 100644 index 000000000..615a13ad7 --- /dev/null +++ b/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordSearchTypeParams.java @@ -0,0 +1,28 @@ +package konkuk.thip.record.adapter.out.persistence; + +import konkuk.thip.common.exception.InvalidStateException; +import konkuk.thip.common.exception.code.ErrorCode; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +public enum RecordSearchTypeParams { + GROUP("group"), + MINE("mine"); + + private final String value; + + RecordSearchTypeParams(String value) { + this.value = value; + } + + public static RecordSearchTypeParams from(String value) { + return Arrays.stream(RecordSearchTypeParams.values()) + .filter(param -> param.getValue().equals(value)) + .findFirst() + .orElseThrow( + () -> new InvalidStateException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("현재 타입 조건 param : " + value)) + ); + } +} diff --git a/src/main/java/konkuk/thip/record/application/port/in/dto/RecordSearchQuery.java b/src/main/java/konkuk/thip/record/application/port/in/dto/RecordSearchQuery.java deleted file mode 100644 index a32a45f90..000000000 --- a/src/main/java/konkuk/thip/record/application/port/in/dto/RecordSearchQuery.java +++ /dev/null @@ -1,14 +0,0 @@ -package konkuk.thip.record.application.port.in.dto; - -import lombok.Builder; - -@Builder -public record RecordSearchQuery( - Long roomId, - String type, - String sort, - Integer pageStart, - Integer pageEnd, - Integer pageNum, - Long userId) { -} diff --git a/src/main/java/konkuk/thip/record/application/port/in/dto/RecordSearchUseCase.java b/src/main/java/konkuk/thip/record/application/port/in/dto/RecordSearchUseCase.java index 031d2ed6b..f27e2da69 100644 --- a/src/main/java/konkuk/thip/record/application/port/in/dto/RecordSearchUseCase.java +++ b/src/main/java/konkuk/thip/record/application/port/in/dto/RecordSearchUseCase.java @@ -4,5 +4,5 @@ public interface RecordSearchUseCase { - RecordSearchResponse search(RecordSearchQuery query); + RecordSearchResponse search(Long roomId, String type, String sort, Integer pageStart, Integer pageEnd, Boolean isOverview, Integer pageNum, Long userId); } diff --git a/src/main/java/konkuk/thip/record/application/port/out/RecordQueryPort.java b/src/main/java/konkuk/thip/record/application/port/out/RecordQueryPort.java index 98605c46c..aad569a42 100644 --- a/src/main/java/konkuk/thip/record/application/port/out/RecordQueryPort.java +++ b/src/main/java/konkuk/thip/record/application/port/out/RecordQueryPort.java @@ -1,9 +1,11 @@ package konkuk.thip.record.application.port.out; -import konkuk.thip.record.application.port.in.dto.RecordSearchResult; +import konkuk.thip.record.adapter.in.web.response.RecordSearchResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; public interface RecordQueryPort { - RecordSearchResult findRecordsByRoom(Long roomId, String type, Integer pageStart, Integer pageEnd, Long userId, Integer pageNum); + Page findRecordsByRoom(Long roomId, String type, Integer pageStart, Integer pageEnd, Boolean isOverview, Long userId, Pageable pageable); } diff --git a/src/main/java/konkuk/thip/record/application/service/RecordSearchService.java b/src/main/java/konkuk/thip/record/application/service/RecordSearchService.java index 6403cd773..dacac95f6 100644 --- a/src/main/java/konkuk/thip/record/application/service/RecordSearchService.java +++ b/src/main/java/konkuk/thip/record/application/service/RecordSearchService.java @@ -1,34 +1,30 @@ package konkuk.thip.record.application.service; -import com.sun.jdi.request.InvalidRequestStateException; -import konkuk.thip.comment.application.port.out.CommentQueryPort; +import konkuk.thip.book.application.port.out.BookCommandPort; +import konkuk.thip.book.domain.Book; import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.common.exception.code.ErrorCode; -import konkuk.thip.common.util.DateUtil; import konkuk.thip.post.application.port.out.PostLikeQueryPort; import konkuk.thip.record.adapter.in.web.response.RecordDto; import konkuk.thip.record.adapter.in.web.response.RecordSearchResponse; import konkuk.thip.record.adapter.in.web.response.VoteDto; -import konkuk.thip.record.application.port.in.dto.RecordSearchQuery; -import konkuk.thip.record.application.port.in.dto.RecordSearchResult; +import konkuk.thip.record.adapter.out.persistence.RecordSearchSortParams; +import konkuk.thip.record.adapter.out.persistence.RecordSearchTypeParams; import konkuk.thip.record.application.port.in.dto.RecordSearchUseCase; import konkuk.thip.record.application.port.out.RecordQueryPort; -import konkuk.thip.record.domain.Record; -import konkuk.thip.user.application.port.out.UserCommandPort; -import konkuk.thip.user.domain.User; import konkuk.thip.vote.application.port.out.VoteCommandPort; import konkuk.thip.vote.application.port.out.VoteQueryPort; -import konkuk.thip.vote.domain.Vote; import konkuk.thip.vote.domain.VoteItem; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.Comparator; import java.util.List; -import java.util.Optional; @Slf4j @Service @@ -36,122 +32,118 @@ public class RecordSearchService implements RecordSearchUseCase { private final RecordQueryPort recordQueryPort; - private final UserCommandPort userCommandPort; - private final PostLikeQueryPort postLikeQueryPort; - private final CommentQueryPort commentQueryPort; + private final BookCommandPort bookCommandPort; private final VoteCommandPort voteCommandPort; private final VoteQueryPort voteQueryPort; + private final PostLikeQueryPort postLikeQueryPort; - private final DateUtil dateUtil; - - private static final int PAGE_SIZE = 10; + private static final int DEFAULT_PAGE_SIZE = 10; @Override @Transactional(readOnly = true) - public RecordSearchResponse search(RecordSearchQuery query) { - validateQueryParams(query); - - // 1. 파라미터에 따라 Record와 Vote를 조회 - RecordSearchResult recordSearchResult = recordQueryPort.findRecordsByRoom( - query.roomId(), - Optional.ofNullable(query.type()).orElse("group"), - query.pageStart(), - query.pageEnd(), - query.userId(), - query.pageNum() - ); - - List records = recordSearchResult.records(); - List votes = recordSearchResult.votes(); - - List combinedPosts = new ArrayList<>(); - - // 2. Record와 Vote를 PostDto로 변환하여 combinedPosts에 추가 - for (Record record : records) { - combinedPosts.add(createRecordDto(record, query.userId())); - } - - for (Vote vote : votes) { - combinedPosts.add(createVoteDto(vote, query.userId())); + public RecordSearchResponse search(Long roomId, String type, String sort, Integer pageStart, Integer pageEnd, Boolean isOverview, Integer pageNum, Long userId) { + // 1. 유효성 검사 + validatePageStartAndEnd(pageStart, pageEnd, isOverview); + pageNum = validatePageNum(pageNum); + + // isOverview가 false일 때 pageStart와 pageEnd가 모두 null이면 전체 페이지 조회 + if(!isOverview && (pageStart == null || pageEnd == null)) { + Book book = bookCommandPort.findBookByRoomId(roomId); + pageStart = 1; + pageEnd = book.getPageCount(); } - // 3. sort에 따라 정렬 (기본값은 "latest") - String sort = Optional.ofNullable(query.sort()).orElse("latest"); - sortCombinedPosts(sort, combinedPosts); - - // 4. 페이지네이션 변수 설정 - int pageNum = Optional.ofNullable(query.pageNum()).orElse(1); - int pageSize = PAGE_SIZE; - int fromIndex = (pageNum - 1) * pageSize; - int toIndex = Math.min(fromIndex + pageSize, combinedPosts.size()); - - // 5. 페이지 범위에 따라 서브리스트 생성 - List pagedList = fromIndex >= combinedPosts.size() ? new ArrayList<>() : combinedPosts.subList(fromIndex, toIndex); + // 2. 정렬 조건 확인 + RecordSearchSortParams sortVal = sort != null ? RecordSearchSortParams.from(sort) : RecordSearchSortParams.LATEST; + RecordSearchTypeParams typeVal = type != null ? RecordSearchTypeParams.from(type) : RecordSearchTypeParams.GROUP; + + // 3. 페이지 인덱스 및 Pageable 객체 생성 + int pageIndex = pageNum - 1; + Pageable pageable = PageRequest.of(pageIndex, DEFAULT_PAGE_SIZE, buildSort(sortVal)); + + // 4. 게시글 조회 + Page result = recordQueryPort.findRecordsByRoom( + roomId, + typeVal.getValue(), + pageStart, + pageEnd, + isOverview, + userId, + pageable + ); - boolean isFirst = pageNum == 1; - boolean isLast = toIndex >= combinedPosts.size(); - if (isLast) { - pageSize = pagedList.size(); // 마지막 페이지일 경우 페이지 크기 업데이트 - } + // 5. isLiked와 voteItems를 포함한 최종 결과 리스트 생성 + List finalList = result.getContent().stream() + .map(post -> { + if (post instanceof RecordDto recordDto) { + boolean isLiked = checkIfLiked(recordDto.recordId(), userId); + return recordDto.withIsLiked(isLiked); + } else if (post instanceof VoteDto voteDto) { + boolean isLiked = checkIfLiked(voteDto.voteId(), userId); + List items = voteCommandPort.findVoteItemsByVoteId(voteDto.voteId()); + List voteItemDtos = mapToVoteItemDtos(items, userId, voteDto.voteId()); + return voteDto.withIsLikedAndVoteItems(isLiked, voteItemDtos); + } else { + throw new InvalidStateException(ErrorCode.API_SERVER_ERROR, new IllegalStateException("지원되지 않는 게시물 타입입니다")); + } + }) + .map(finalResult -> (RecordSearchResponse.RecordSearchResult) finalResult) + .toList(); - return RecordSearchResponse.of(pagedList, pageNum, pageSize, isFirst, isLast); + // 6. response 구성 + return new RecordSearchResponse( + finalList, + pageNum, + result.getNumberOfElements(), + result.isLast(), + result.isFirst()); } - private void validateQueryParams(RecordSearchQuery query) { - if(query.pageStart() != null && query.pageEnd() == null || query.pageStart() == null && query.pageEnd() != null) { - throw new InvalidStateException(ErrorCode.API_INVALID_PARAM, new InvalidRequestStateException("pageStart와 pageEnd는 모두 설정되어야 합니다.")); + private void validatePageStartAndEnd(Integer pageStart, Integer pageEnd, Boolean isOverview) { + if((pageStart != null && pageEnd == null) || (pageStart == null && pageEnd != null)) { + throw new InvalidStateException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("pageStart와 pageEnd는 모두 설정되거나(특정 페이지 조회) 모두 설정되지 않아야 합니다.(전체 페이지 조회)")); } - - if(query.pageNum() != null && query.pageNum() < 1) { - throw new InvalidStateException(ErrorCode.API_INVALID_PARAM, new InvalidRequestStateException("pageNum은 1 이상의 값이어야 합니다.")); + if (pageStart != null && pageEnd != null && pageStart > pageEnd) { + throw new InvalidStateException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("pageStart는 pageEnd보다 작거나 같아야 합니다.")); } - - if(query.sort() != null && !List.of("latest", "like", "comment").contains(query.sort())) { - throw new InvalidStateException(ErrorCode.API_INVALID_PARAM, new InvalidRequestStateException("sort는 'latest', 'like', 'comment' 중 하나여야 합니다.")); + if (isOverview && (pageStart != null || pageEnd != null)) { + throw new InvalidStateException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("pageStart와 pageEnd는 isOverview가 true일 때 유효한 파라미터가 아닙니다.")); } + } - if(query.type() != null && !List.of("group", "mine").contains(query.type())) { - throw new InvalidStateException(ErrorCode.API_INVALID_PARAM, new InvalidRequestStateException("type은 'group', 'mine' 중 하나여야 합니다.")); + private Integer validatePageNum(Integer pageNum) { + if (pageNum == null) { + return 1; // 기본값으로 첫 페이지 반환 + } + if (pageNum < 1) { + throw new InvalidStateException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("pageNum은 1 이상의 값이어야 합니다.")); } + return pageNum; } - private RecordSearchResponse.PostDto createRecordDto(Record record, Long userId) { - User user = userCommandPort.findById(record.getCreatorId()); - int likeCount = postLikeQueryPort.countByPostId(record.getId()); - int commentCount = commentQueryPort.countByPostId(record.getId()); - boolean isLiked = postLikeQueryPort.existsByPostIdAndUserId(userId, record.getId()); - boolean isWriter = record.getCreatorId().equals(userId); - return RecordDto.of(record, dateUtil.formatLastActivityTime(record.getCreatedAt()), user, likeCount, commentCount, isLiked, isWriter); + private Sort buildSort(RecordSearchSortParams sort) { + return switch (sort) { + case LIKE -> Sort.by(Sort.Direction.DESC, "likeCount"); + case COMMENT -> Sort.by(Sort.Direction.DESC, "commentCount"); + default -> Sort.by(Sort.Direction.DESC, "createdAt"); + }; } - private RecordSearchResponse.PostDto createVoteDto(Vote vote, Long userId) { - User user = userCommandPort.findById(vote.getCreatorId()); - int likeCount = postLikeQueryPort.countByPostId(vote.getId()); - int commentCount = commentQueryPort.countByPostId(vote.getId()); - boolean isLiked = postLikeQueryPort.existsByPostIdAndUserId(userId, vote.getId()); - boolean isWriter = vote.getCreatorId().equals(userId); - - List voteItems = voteCommandPort.findVoteItemsByVoteId(vote.getId()); - int totalCount = voteItems.stream().mapToInt(VoteItem::getCount).sum(); - - List voteItemDtos = voteItems.stream() - .map(item -> VoteDto.VoteItemDto.of(item, item.calculatePercentage(totalCount), voteQueryPort.isUserVoted(userId, item.getId()))) + private List mapToVoteItemDtos(List items, Long userId, Long voteId) { + int total = items.stream().mapToInt(VoteItem::getCount).sum(); + return items.stream() + .map(item -> VoteDto.VoteItemDto.of( + item, + item.calculatePercentage(total), + voteQueryPort.isUserVoted(userId, voteId) + ) + ) .toList(); - - return VoteDto.of(vote, dateUtil.formatLastActivityTime(vote.getCreatedAt()), user, likeCount, commentCount, isLiked, isWriter, voteItemDtos); } - private void sortCombinedPosts(String sort, List combinedPosts) { - switch (sort) { - case "like" -> combinedPosts.sort( - Comparator.comparingInt(RecordSearchResponse.PostDto::likeCount).reversed() - ); - case "comment" -> combinedPosts.sort( - Comparator.comparingInt(RecordSearchResponse.PostDto::commentCount).reversed() - ); - default -> combinedPosts.sort( - Comparator.comparing(RecordSearchResponse.PostDto::postDate).reversed() - ); - } + private boolean checkIfLiked(Long postId, Long userId) { + return postLikeQueryPort.existsByPostIdAndUserId(postId, userId); } } + + diff --git a/src/main/java/konkuk/thip/record/domain/Record.java b/src/main/java/konkuk/thip/record/domain/Record.java index ae46fa255..4bcf99ef0 100644 --- a/src/main/java/konkuk/thip/record/domain/Record.java +++ b/src/main/java/konkuk/thip/record/domain/Record.java @@ -2,10 +2,12 @@ import konkuk.thip.common.entity.BaseDomainEntity; import konkuk.thip.common.exception.InvalidStateException; +import lombok.Builder; import lombok.Getter; import lombok.experimental.SuperBuilder; -import static konkuk.thip.common.exception.code.ErrorCode.*; +import static konkuk.thip.common.exception.code.ErrorCode.INVALID_RECORD_PAGE_RANGE; +import static konkuk.thip.common.exception.code.ErrorCode.RECORD_CANNOT_BE_OVERVIEW; @Getter @SuperBuilder @@ -21,6 +23,12 @@ public class Record extends BaseDomainEntity { private boolean isOverview; + @Builder.Default + private Integer likeCount = 0; + + @Builder.Default + private Integer commentCount = 0; + private Long roomId; public static Record withoutId( @@ -35,6 +43,8 @@ public static Record withoutId( .creatorId(creatorId) .page(page) .isOverview(isOverview) + .likeCount(0) + .commentCount(0) .roomId(roomId) .build(); } diff --git a/src/main/java/konkuk/thip/vote/adapter/out/jpa/VoteJpaEntity.java b/src/main/java/konkuk/thip/vote/adapter/out/jpa/VoteJpaEntity.java index e25662e7c..d109c2eee 100644 --- a/src/main/java/konkuk/thip/vote/adapter/out/jpa/VoteJpaEntity.java +++ b/src/main/java/konkuk/thip/vote/adapter/out/jpa/VoteJpaEntity.java @@ -26,8 +26,8 @@ public class VoteJpaEntity extends PostJpaEntity { private RoomJpaEntity roomJpaEntity; @Builder - public VoteJpaEntity(String content, UserJpaEntity userJpaEntity, Integer page, boolean isOverview, RoomJpaEntity roomJpaEntity) { - super(content, userJpaEntity); + public VoteJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity, Integer page, boolean isOverview, RoomJpaEntity roomJpaEntity) { + super(content, likeCount, commentCount, userJpaEntity); this.page = page; this.isOverview = isOverview; this.roomJpaEntity = roomJpaEntity; diff --git a/src/main/java/konkuk/thip/vote/adapter/out/mapper/VoteMapper.java b/src/main/java/konkuk/thip/vote/adapter/out/mapper/VoteMapper.java index a4adb6d3f..d7ea120c4 100644 --- a/src/main/java/konkuk/thip/vote/adapter/out/mapper/VoteMapper.java +++ b/src/main/java/konkuk/thip/vote/adapter/out/mapper/VoteMapper.java @@ -15,6 +15,8 @@ public VoteJpaEntity toJpaEntity(Vote vote, UserJpaEntity userJpaEntity, RoomJpa .userJpaEntity(userJpaEntity) .page(vote.getPage()) .isOverview(vote.isOverview()) + .likeCount(vote.getLikeCount()) + .commentCount(vote.getCommentCount()) .roomJpaEntity(roomJpaEntity) .build(); } @@ -26,6 +28,8 @@ public Vote toDomainEntity(VoteJpaEntity voteJpaEntity) { .creatorId(voteJpaEntity.getUserJpaEntity().getUserId()) .page(voteJpaEntity.getPage()) .isOverview(voteJpaEntity.isOverview()) + .likeCount(voteJpaEntity.getLikeCount()) + .commentCount(voteJpaEntity.getCommentCount()) .roomId(voteJpaEntity.getRoomJpaEntity().getRoomId()) .createdAt(voteJpaEntity.getCreatedAt()) .modifiedAt(voteJpaEntity.getModifiedAt()) diff --git a/src/main/java/konkuk/thip/vote/domain/Vote.java b/src/main/java/konkuk/thip/vote/domain/Vote.java index 6cde37e24..653710ce9 100644 --- a/src/main/java/konkuk/thip/vote/domain/Vote.java +++ b/src/main/java/konkuk/thip/vote/domain/Vote.java @@ -2,10 +2,12 @@ import konkuk.thip.common.entity.BaseDomainEntity; import konkuk.thip.common.exception.InvalidStateException; +import lombok.Builder; import lombok.Getter; import lombok.experimental.SuperBuilder; -import static konkuk.thip.common.exception.code.ErrorCode.*; +import static konkuk.thip.common.exception.code.ErrorCode.INVALID_VOTE_PAGE_RANGE; +import static konkuk.thip.common.exception.code.ErrorCode.VOTE_CANNOT_BE_OVERVIEW; @Getter @SuperBuilder @@ -21,6 +23,12 @@ public class Vote extends BaseDomainEntity { private boolean isOverview; + @Builder.Default + private Integer likeCount = 0; + + @Builder.Default + private Integer commentCount = 0; + private Long roomId; public static Vote withoutId(String content, Long creatorId, Integer page, boolean isOverview, Long roomId) { @@ -30,6 +38,8 @@ public static Vote withoutId(String content, Long creatorId, Integer page, boole .creatorId(creatorId) .page(page) .isOverview(isOverview) + .likeCount(0) + .commentCount(0) .roomId(roomId) .build(); } diff --git a/src/main/java/konkuk/thip/vote/domain/VoteItem.java b/src/main/java/konkuk/thip/vote/domain/VoteItem.java index 240be475d..1aa575f81 100644 --- a/src/main/java/konkuk/thip/vote/domain/VoteItem.java +++ b/src/main/java/konkuk/thip/vote/domain/VoteItem.java @@ -25,6 +25,7 @@ public static VoteItem withoutId(String itemName, int count, Long voteId) { .build(); } + //todo 총 퍼센트가 100이 되는 알고리즘으로 수정! public int calculatePercentage(int totalCount) { return totalCount == 0 ? 0 : (int) Math.round((this.count * 100.0) / totalCount); } diff --git a/src/test/java/konkuk/thip/record/adapter/in/web/RecordQueryControllerTest.java b/src/test/java/konkuk/thip/record/adapter/in/web/RecordQueryControllerTest.java index 5b065844b..43334110e 100644 --- a/src/test/java/konkuk/thip/record/adapter/in/web/RecordQueryControllerTest.java +++ b/src/test/java/konkuk/thip/record/adapter/in/web/RecordQueryControllerTest.java @@ -55,7 +55,6 @@ class RecordSearchControllerTest { @Autowired private CategoryJpaRepository categoryJpaRepository; @Autowired private AliasJpaRepository aliasJpaRepository; @Autowired private UserJpaRepository userJpaRepository; - @Autowired private DateUtil dateUtil; @AfterEach void tearDown() { @@ -111,6 +110,8 @@ void record_with_vote_response_success() throws Exception { RecordJpaEntity record = recordJpaRepository.save(RecordJpaEntity.builder() .userJpaEntity(user) .roomJpaEntity(room) + .likeCount(1) + .commentCount(2) .page(1) .content("레코드 내용") .build()); @@ -118,6 +119,8 @@ void record_with_vote_response_success() throws Exception { VoteJpaEntity vote = voteJpaRepository.save(VoteJpaEntity.builder() .userJpaEntity(user) .roomJpaEntity(room) + .likeCount(1) + .commentCount(2) .page(1) .content("투표 내용") .build()); @@ -142,6 +145,7 @@ void record_with_vote_response_success() throws Exception { .param("pageStart", "1") .param("pageEnd", "10") .param("pageNum", "1") + .param("isOverview", "false") .contentType(MediaType.APPLICATION_JSON)); // then @@ -149,27 +153,28 @@ void record_with_vote_response_success() throws Exception { String json = result.andReturn().getResponse().getContentAsString(); JsonNode jsonNode = objectMapper.readTree(json); - JsonNode recordNode = jsonNode.path("data").path("recordList").get(0); - assertThat(recordNode.path("type").asText()).isEqualTo("RECORD"); - assertThat(recordNode.path("page").asInt()).isEqualTo(1); - assertThat(recordNode.path("content").asText()).isEqualTo("레코드 내용"); - assertThat(recordNode.path("nickName").asText()).isEqualTo("사용자"); - assertThat(recordNode.path("postDate").asText()).isEqualTo(dateUtil.formatLastActivityTime(LocalDateTime.now())); - assertThat(recordNode.path("likeCount").asInt()).isEqualTo(0); - assertThat(recordNode.path("commentCount").asInt()).isEqualTo(0); - - JsonNode voteNode = jsonNode.path("data").path("recordList").get(1); + JsonNode voteNode = jsonNode.path("data").path("recordList").get(0); assertThat(voteNode.path("type").asText()).isEqualTo("VOTE"); assertThat(voteNode.path("page").asInt()).isEqualTo(1); assertThat(voteNode.path("content").asText()).isEqualTo("투표 내용"); assertThat(voteNode.path("nickName").asText()).isEqualTo("사용자"); - assertThat(voteNode.path("postDate").asText()).isEqualTo(dateUtil.formatLastActivityTime(LocalDateTime.now())); + assertThat(voteNode.path("postDate").asText()).isEqualTo(DateUtil.formatBeforeTime(LocalDateTime.now())); JsonNode voteItems = voteNode.path("voteItems"); assertThat(voteItems).hasSize(2); assertThat(voteItems.get(0).get("itemName").asText()).isEqualTo("찬성"); assertThat(voteItems.get(0).get("isVoted").asBoolean()).isEqualTo(false); assertThat(voteItems.get(0).get("percentage").asInt()).isEqualTo(75); + + JsonNode recordNode = jsonNode.path("data").path("recordList").get(1); + assertThat(recordNode.path("type").asText()).isEqualTo("RECORD"); + assertThat(recordNode.path("page").asInt()).isEqualTo(1); + assertThat(recordNode.path("content").asText()).isEqualTo("레코드 내용"); + assertThat(recordNode.path("nickName").asText()).isEqualTo("사용자"); + assertThat(recordNode.path("postDate").asText()).isEqualTo(DateUtil.formatBeforeTime(LocalDateTime.now())); + assertThat(recordNode.path("likeCount").asInt()).isEqualTo(1); + assertThat(recordNode.path("commentCount").asInt()).isEqualTo(2); + } diff --git a/src/test/java/konkuk/thip/record/adapter/out/persistence/RecordQueryRepositoryImplTest.java b/src/test/java/konkuk/thip/record/adapter/out/persistence/RecordQueryRepositoryImplTest.java index 898a19fb5..3ece406e6 100644 --- a/src/test/java/konkuk/thip/record/adapter/out/persistence/RecordQueryRepositoryImplTest.java +++ b/src/test/java/konkuk/thip/record/adapter/out/persistence/RecordQueryRepositoryImplTest.java @@ -1,18 +1,23 @@ package konkuk.thip.record.adapter.out.persistence; import jakarta.persistence.EntityManager; +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.config.TestQuerydslConfig; +import konkuk.thip.record.adapter.in.web.response.RecordSearchResponse; import konkuk.thip.record.adapter.out.jpa.RecordJpaEntity; import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.test.context.ActiveProfiles; import java.util.List; @@ -21,28 +26,27 @@ @DataJpaTest @ActiveProfiles("test") -@Import(konkuk.thip.config.TestQuerydslConfig.class) +@Import(TestQuerydslConfig.class) @DisplayName("[JPA] RecordQueryRepositoryImpl 테스트") class RecordQueryRepositoryImplTest { @Autowired - private EntityManager em; + private RecordQueryRepositoryImpl recordQueryRepository; @Autowired - private RecordJpaRepository recordJpaRepository; + private EntityManager em; - @Autowired - RecordQueryRepositoryImpl recordQueryRepository; + private RoomJpaEntity room; + private UserJpaEntity user1; + private UserJpaEntity user2; - @Test - @DisplayName("mine 타입일 경우 유저 ID에 해당하는 기록만 조회된다") - void testFindRecordsByRoom_mine() { - // given + @BeforeEach + void setUp() { AliasJpaEntity alias = TestEntityFactory.createLiteratureAlias(); em.persist(alias); - UserJpaEntity user1 = TestEntityFactory.createUser(alias); - UserJpaEntity user2 = TestEntityFactory.createUser(alias); + user1 = TestEntityFactory.createUser(alias); + user2 = TestEntityFactory.createUser(alias); em.persist(user1); em.persist(user2); @@ -52,94 +56,148 @@ void testFindRecordsByRoom_mine() { CategoryJpaEntity category = TestEntityFactory.createLiteratureCategory(alias); em.persist(category); - RoomJpaEntity room = TestEntityFactory.createRoom(book, category); + room = TestEntityFactory.createRoom(book, category); em.persist(room); - RecordJpaEntity r1 = RecordJpaEntity.builder() - .userJpaEntity(user1) - .roomJpaEntity(room) - .content("user1의 레코드") - .page(1) - .isOverview(false) - .build(); - - RecordJpaEntity r2 = RecordJpaEntity.builder() - .userJpaEntity(user2) - .roomJpaEntity(room) - .content("user2의 레코드") - .page(1) - .isOverview(false) - .build(); - - em.persist(r1); - em.persist(r2); + for (int i = 0; i < 10; i++) { + RecordJpaEntity record = RecordJpaEntity.builder() + .userJpaEntity(i % 2 == 0 ? user1 : user2) + .roomJpaEntity(room) + .content("레코드 " + i) + .likeCount(10 - i) + .commentCount(i) + .isOverview(false) + .page(1) + .build(); + em.persist(record); + } + + for (int i = 0; i < 10; i++) { + RecordJpaEntity record = RecordJpaEntity.builder() + .userJpaEntity(i % 2 == 0 ? user1 : user2) + .roomJpaEntity(room) + .content("레코드 " + i) + .likeCount(1) + .commentCount(1) + .isOverview(true) + .page(book.getPageCount()) // 총평은 책 전체 페이지로 저장 + .build(); + em.persist(record); + } + em.flush(); em.clear(); + } - // when - List result = recordQueryRepository.findRecordsByRoom( + @Test + @DisplayName("기본 조회 및 페이징 동작 확인") + void test_paging() { + Page result = recordQueryRepository.findRecordsByRoom( room.getRoomId(), - "mine", + "group", 1, 1, - user1.getUserId() + false, + user1.getUserId(), + PageRequest.of(0, 5) ); - // then - assertThat(result).hasSize(1); - assertThat(result.get(0).getUserJpaEntity().getUserId()).isEqualTo(user1.getUserId()); - assertThat(result.get(0).getContent()).isEqualTo("user1의 레코드"); + assertThat(result.getNumberOfElements()).isEqualTo(5); + assertThat(result.getTotalElements()).isEqualTo(10); + assertThat(result.isLast()).isFalse(); + assertThat(result.isFirst()).isTrue(); } @Test - @DisplayName("pageStart, pageEnd가 null이면 isOverview가 true인 레코드만 조회된다") - void testFindRecordsByRoom_overview() { - // given - AliasJpaEntity alias = TestEntityFactory.createLiteratureAlias(); - em.persist(alias); + @DisplayName("viewType이 mine일 때 user1의 레코드만 조회된다") + void test_viewType_mine() { + Page result = recordQueryRepository.findRecordsByRoom( + room.getRoomId(), + "mine", + 1, + 1, + false, + user1.getUserId(), + PageRequest.of(0, 10) + ); - UserJpaEntity user = TestEntityFactory.createUser(alias); - em.persist(user); + assertThat(result).allSatisfy(record -> + assertThat(record.userId()).isEqualTo(user1.getUserId())); + assertThat(result.getNumberOfElements()).isEqualTo(5); // user1이 작성한 레코드가 5개 + } - BookJpaEntity book = TestEntityFactory.createBook(); - em.persist(book); + @Test + @DisplayName("isOverview가 true일 때 레코드가 총평 기록만 조회된다.") + void test_isOverview_true() { + Page result = recordQueryRepository.findRecordsByRoom( + room.getRoomId(), + "group", + 1, + 1, + true, + user1.getUserId(), + PageRequest.of(0, 10) + ); - CategoryJpaEntity category = TestEntityFactory.createLiteratureCategory(alias); - em.persist(category); + assertThat(result.getNumberOfElements()).isEqualTo(10); + assertThat(result.getContent()).allSatisfy(record -> + assertThat(record.page()).isEqualTo(room.getBookJpaEntity().getPageCount())); + } - RoomJpaEntity room = TestEntityFactory.createRoom(book, category); - em.persist(room); + @Test + @DisplayName("latest 기준 정렬 확인") + void test_sortingBy_latest() { + Page result = recordQueryRepository.findRecordsByRoom( + room.getRoomId(), + null, + 1, + 1, + false, + user1.getUserId(), + PageRequest.of(0, 10, org.springframework.data.domain.Sort.by("createdAt").descending()) + ); - RecordJpaEntity overview = RecordJpaEntity.builder() - .userJpaEntity(user) - .roomJpaEntity(room) - .content("요약 레코드") - .isOverview(true) - .build(); - - RecordJpaEntity normal = RecordJpaEntity.builder() - .userJpaEntity(user) - .roomJpaEntity(room) - .content("일반 레코드") - .isOverview(false) - .build(); - - em.persist(overview); - em.persist(normal); - em.flush(); - em.clear(); + List content = result.getContent(); + for (int i = 1; i < content.size(); i++) { + assertThat(content.get(i - 1).postDate()).isGreaterThanOrEqualTo(content.get(i).postDate()); + } + } - // when - List result = recordQueryRepository.findRecordsByRoom( + @Test + @DisplayName("likeCount 기준 정렬 확인") + void test_sortingBy_likeCount() { + Page result = recordQueryRepository.findRecordsByRoom( room.getRoomId(), - "group", null, + 1, + 1, + false, + user1.getUserId(), + PageRequest.of(0, 10, org.springframework.data.domain.Sort.by("likeCount").descending()) + ); + + List content = result.getContent(); + for (int i = 1; i < content.size(); i++) { + assertThat(content.get(i - 1).likeCount()).isGreaterThanOrEqualTo(content.get(i).likeCount()); + } + } + + @Test + @DisplayName("commentCount 기준 정렬 확인") + void test_sortingBy_commentCount() { + Page result = recordQueryRepository.findRecordsByRoom( + room.getRoomId(), null, - user.getUserId() + 1, + 1, + false, + user1.getUserId(), + PageRequest.of(0, 10, org.springframework.data.domain.Sort.by("commentCount").descending()) ); - // then - assertThat(result).hasSize(1); - assertThat(result.get(0).isOverview()).isTrue(); + List content = result.getContent(); + for (int i = 1; i < content.size(); i++) { + assertThat(content.get(i - 1).commentCount()).isGreaterThanOrEqualTo(content.get(i).commentCount()); + } } } \ No newline at end of file diff --git a/src/test/java/konkuk/thip/record/application/service/RecordSearchServiceTest.java b/src/test/java/konkuk/thip/record/application/service/RecordSearchServiceTest.java index a679ed1a7..51ddd5f89 100644 --- a/src/test/java/konkuk/thip/record/application/service/RecordSearchServiceTest.java +++ b/src/test/java/konkuk/thip/record/application/service/RecordSearchServiceTest.java @@ -1,28 +1,21 @@ package konkuk.thip.record.application.service; -import konkuk.thip.comment.application.port.out.CommentQueryPort; +import konkuk.thip.book.application.port.out.BookCommandPort; +import konkuk.thip.book.domain.Book; import konkuk.thip.common.exception.InvalidStateException; -import konkuk.thip.common.util.DateUtil; import konkuk.thip.post.application.port.out.PostLikeQueryPort; +import konkuk.thip.record.adapter.in.web.response.RecordDto; import konkuk.thip.record.adapter.in.web.response.RecordSearchResponse; -import konkuk.thip.record.application.port.in.dto.RecordSearchQuery; -import konkuk.thip.record.application.port.in.dto.RecordSearchResult; import konkuk.thip.record.application.port.out.RecordQueryPort; -import konkuk.thip.record.domain.Record; -import konkuk.thip.user.application.port.out.UserCommandPort; -import konkuk.thip.user.domain.Alias; -import konkuk.thip.user.domain.User; import konkuk.thip.vote.application.port.out.VoteCommandPort; import konkuk.thip.vote.application.port.out.VoteQueryPort; -import konkuk.thip.vote.domain.Vote; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.data.domain.PageImpl; -import java.time.LocalDateTime; import java.util.List; -import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; @@ -31,230 +24,140 @@ class RecordSearchServiceTest { private RecordQueryPort recordQueryPort; - private UserCommandPort userCommandPort; - private PostLikeQueryPort postLikeQueryPort; - private CommentQueryPort commentQueryPort; + private BookCommandPort bookCommandPort; private VoteCommandPort voteCommandPort; private VoteQueryPort voteQueryPort; - private DateUtil dateUtil; + private PostLikeQueryPort postLikeQueryPort; private RecordSearchService recordSearchService; - private final Long userId = 1L; - private User stubUser; - @BeforeEach void setUp() { recordQueryPort = mock(RecordQueryPort.class); - userCommandPort = mock(UserCommandPort.class); - postLikeQueryPort = mock(PostLikeQueryPort.class); - commentQueryPort = mock(CommentQueryPort.class); + bookCommandPort = mock(BookCommandPort.class); voteCommandPort = mock(VoteCommandPort.class); voteQueryPort = mock(VoteQueryPort.class); - dateUtil = mock(DateUtil.class); - - // stubUser 세팅: 항상 같은 Alias와 userId 반환 - stubUser = mock(User.class); - when(stubUser.getAlias()).thenReturn(Alias.WRITER); - when(stubUser.getId()).thenReturn(userId); + postLikeQueryPort = mock(PostLikeQueryPort.class); recordSearchService = new RecordSearchService( - recordQueryPort, - userCommandPort, - postLikeQueryPort, - commentQueryPort, - voteCommandPort, - voteQueryPort, - dateUtil - ); + recordQueryPort, bookCommandPort, voteCommandPort, voteQueryPort, postLikeQueryPort); } @Test - @DisplayName("최신순 정렬이 적용된 결과를 반환한다") - void testSortByLatest() { - // given - Record record = Record.builder() - .id(1L) - .creatorId(userId) - .content("레코드") - .page(1) - .createdAt(LocalDateTime.now().minusMinutes(10)) - .build(); - Vote vote = Vote.builder() - .id(2L) - .creatorId(userId) - .content("투표") - .page(1) - .createdAt(LocalDateTime.now()) - .build(); - - when(recordQueryPort.findRecordsByRoom(any(), any(), any(), any(), any(), any())) - .thenReturn(RecordSearchResult.of(List.of(record), List.of(vote))); - - when(userCommandPort.findById(any())).thenReturn(stubUser); - when(postLikeQueryPort.countByPostId(anyLong())).thenReturn(0); - when(commentQueryPort.countByPostId(anyLong())).thenReturn(0); - when(postLikeQueryPort.existsByPostIdAndUserId(anyLong(), anyLong())).thenReturn(false); - when(voteCommandPort.findVoteItemsByVoteId(anyLong())).thenReturn(emptyList()); - when(voteQueryPort.isUserVoted(anyLong(), anyLong())).thenReturn(false); - when(dateUtil.formatLastActivityTime(any())).thenReturn("방금 전"); - - // when - RecordSearchResponse response = recordSearchService.search(buildQuery("latest", 1, "mine").build()); - - // then - assertThat(response.recordList()).hasSize(2); - assertThat(response.recordList().get(0).type()).isEqualTo("RECORD"); // 최신순이므로 Record가 먼저 - assertThat(response.first()).isTrue(); - assertThat(response.last()).isTrue(); - assertThat(response.page()).isEqualTo(1); - } - - @Test - @DisplayName("좋아요 순 정렬이 적용된 결과를 반환한다") - void testSortByLike() { - // given - Record record = Record.builder() - .id(1L) - .creatorId(userId) - .content("레코드") - .page(1) - .createdAt(LocalDateTime.now()) - .build(); - Vote vote = Vote.builder() - .id(2L) - .creatorId(userId) - .content("투표") - .page(1) - .createdAt(LocalDateTime.now()) - .build(); - - when(recordQueryPort.findRecordsByRoom(any(), any(), any(), any(), any(), any())) - .thenReturn(RecordSearchResult.of(List.of(record), List.of(vote))); - - when(userCommandPort.findById(any())).thenReturn(stubUser); - when(postLikeQueryPort.countByPostId(record.getId())).thenReturn(5); - when(postLikeQueryPort.countByPostId(vote.getId())).thenReturn(10); - when(commentQueryPort.countByPostId(anyLong())).thenReturn(0); - when(postLikeQueryPort.existsByPostIdAndUserId(anyLong(), anyLong())).thenReturn(false); - when(voteCommandPort.findVoteItemsByVoteId(anyLong())).thenReturn(emptyList()); - when(voteQueryPort.isUserVoted(anyLong(), anyLong())).thenReturn(false); - when(dateUtil.formatLastActivityTime(any())).thenReturn("방금 전"); - - // when - RecordSearchResponse response = recordSearchService.search(buildQuery("like", 1, "mine").build()); - - // then - assertThat(response.recordList().get(0).type()).isEqualTo("VOTE"); // 좋아요가 더 많음 + @DisplayName("전체 페이지 조회 성공") + void search_all_pages_success() { + Long roomId = 1L; + String type = "group"; + String sort = "latest"; + Integer pageStart = null; + Integer pageEnd = null; + Boolean isOverview = false; + Integer pageNum = 1; + Long userId = 1L; + + when(bookCommandPort.findBookByRoomId(roomId)).thenReturn(Book.builder().pageCount(100).build()); + RecordDto mockDto = new RecordDto("방금 전", 1, 1L, "사용자", "url", "내용", 1, 1, false, true, 1L); + when(recordQueryPort.findRecordsByRoom(any(), any(), any(), any(), any(), any(), any())) + .thenReturn(new PageImpl<>(List.of(mockDto))); + when(postLikeQueryPort.existsByPostIdAndUserId(1L, userId)).thenReturn(true); + + RecordSearchResponse response = recordSearchService.search(roomId, type, sort, pageStart, pageEnd, isOverview, pageNum, userId); + + assertThat(response.recordList()).hasSize(1); + assertThat(((RecordDto) response.recordList().get(0)).isLiked()).isTrue(); } @Test - @DisplayName("페이징 처리가 잘 적용되는지 확인") - void testPagingLogic() { - // given - List records = List.of( - Record.builder().id(1L).creatorId(userId).content("r1").page(1).createdAt(LocalDateTime.now()).build(), - Record.builder().id(2L).creatorId(userId).content("r2").page(2).createdAt(LocalDateTime.now()).build(), - Record.builder().id(3L).creatorId(userId).content("r3").page(3).createdAt(LocalDateTime.now()).build(), - Record.builder().id(4L).creatorId(userId).content("r4").page(4).createdAt(LocalDateTime.now()).build(), - Record.builder().id(5L).creatorId(userId).content("r5").page(5).createdAt(LocalDateTime.now()).build(), - Record.builder().id(6L).creatorId(userId).content("r6").page(6).createdAt(LocalDateTime.now()).build(), - Record.builder().id(7L).creatorId(userId).content("r7").page(7).createdAt(LocalDateTime.now()).build(), - Record.builder().id(8L).creatorId(userId).content("r8").page(8).createdAt(LocalDateTime.now()).build(), - Record.builder().id(9L).creatorId(userId).content("r9").page(9).createdAt(LocalDateTime.now()).build(), - Record.builder().id(10L).creatorId(userId).content("r10").page(10).createdAt(LocalDateTime.now()).build(), - Record.builder().id(11L).creatorId(userId).content("r11").page(11).createdAt(LocalDateTime.now()).build() - ); - - List votes = List.of(); // 투표 없음 - - when(recordQueryPort.findRecordsByRoom(any(), any(), any(), any(), any(), any())) - .thenReturn(RecordSearchResult.of(records, votes)); - - when(userCommandPort.findById(any())).thenReturn(stubUser); - when(postLikeQueryPort.countByPostId(anyLong())).thenReturn(0); - when(commentQueryPort.countByPostId(anyLong())).thenReturn(0); - when(postLikeQueryPort.existsByPostIdAndUserId(anyLong(), anyLong())).thenReturn(false); - when(voteCommandPort.findVoteItemsByVoteId(anyLong())).thenReturn(emptyList()); - when(voteQueryPort.isUserVoted(anyLong(), anyLong())).thenReturn(false); - when(dateUtil.formatLastActivityTime(any())).thenReturn("방금 전"); - - // when - RecordSearchResponse response = recordSearchService.search(buildQuery("latest", 1, "mine").build()); - - // then - assertThat(response.recordList()).hasSize(10); // 페이지 사이즈만큼 반환 - assertThat(response.size()).isEqualTo(10); - assertThat(response.page()).isEqualTo(1); - assertThat(response.first()).isTrue(); - assertThat(response.last()).isFalse(); // 아직 한 개 남음 - } - - private RecordSearchQuery.RecordSearchQueryBuilder buildQuery(String sort, int pageNum, String type) { - return RecordSearchQuery.builder() - .roomId(1L) - .type(type) - .sort(sort) - .pageStart(1) - .pageEnd(10) - .userId(userId) - .pageNum(pageNum); + @DisplayName("pageStart와 pageEnd가 하나만 null이면 예외 발생") + void invalid_page_range() { + Long roomId = 1L; + String type = null; + String sort = "latest"; + Integer pageStart = 1; + Integer pageEnd = null; + Boolean isOverview = false; + Integer pageNum = 1; + Long userId = 1L; + + assertThrows(InvalidStateException.class, () -> + recordSearchService.search(roomId, type, sort, pageStart, pageEnd, isOverview, pageNum, userId)); } @Test - @DisplayName("pageStart만 설정된 경우 예외가 발생한다") - void testInvalidPageStartOnly() { - // given - RecordSearchQuery query = buildQuery("latest", 1, "mine") - .pageStart(1) - .pageEnd(null) - .build(); - - // when & then - assertThrows(InvalidStateException.class, () -> recordSearchService.search(query)); + @DisplayName("pageStart > pageEnd면 예외 발생") + void invalid_page_range_order() { + Long roomId = 1L; + Integer pageStart = 10; + Integer pageEnd = 5; + Boolean isOverview = false; + Integer pageNum = 1; + Long userId = 1L; + + assertThrows(InvalidStateException.class, () -> + recordSearchService.search(roomId, null, null, pageStart, pageEnd, isOverview, pageNum, userId)); } @Test - @DisplayName("pageEnd만 설정된 경우 예외가 발생한다") - void testInvalidPageEndOnly() { - // given - RecordSearchQuery query = buildQuery("latest", 1, "mine") - .pageStart(null) - .pageEnd(10) - .build(); + @DisplayName("pageNum이 1보다 작으면 예외 발생") + void invalid_pageNum() { + Long roomId = 1L; + Integer pageNum = 0; - // when & then - assertThrows(InvalidStateException.class, () -> recordSearchService.search(query)); + assertThrows(InvalidStateException.class, () -> + recordSearchService.search(roomId, null, null, null, null, true, pageNum, 1L)); } @Test - @DisplayName("pageNum이 0 이하인 경우 예외가 발생한다") - void testInvalidPageNum() { - // given - RecordSearchQuery query = buildQuery("latest", 0, "mine").build(); + @DisplayName("pageNum이 null인 경우 첫번째 페이지 조회") + void search_with_null_pageNum() { + Long roomId = 1L; + String type = "group"; + String sort = "latest"; + Integer pageStart = null; + Integer pageEnd = null; + Boolean isOverview = true; + Long userId = 1L; + + RecordDto mockDto = new RecordDto("방금 전", 1, 1L, "사용자", "url", "내용", 1, 1, false, true, 1L); + when(recordQueryPort.findRecordsByRoom(any(), any(), any(), any(), any(), any(), any())) + .thenReturn(new PageImpl<>(List.of(mockDto))); + when(postLikeQueryPort.existsByPostIdAndUserId(1L, userId)).thenReturn(true); + + RecordSearchResponse response = recordSearchService.search(roomId, type, sort, pageStart, pageEnd, isOverview, null, userId); - // when & then - assertThrows(InvalidStateException.class, () -> recordSearchService.search(query)); + assertThat(response.page()).isEqualTo(1); + assertThat(response.recordList()).hasSize(1); } @Test - @DisplayName("정의되지 않은 sort 값은 예외를 발생시킨다") - void testInvalidSort() { - // given - RecordSearchQuery query = buildQuery("invalidSort", 1, "mine").build(); - - // when & then - assertThrows(InvalidStateException.class, () -> recordSearchService.search(query)); + @DisplayName("형식이 맞지 않는 type으로 조회 시 예외 발생") + void search_with_invalid_type() { + Long roomId = 1L; + String type = "invalidType"; + String sort = "latest"; + Integer pageStart = 1; + Integer pageEnd = 10; + Boolean isOverview = true; + Integer pageNum = 1; + Long userId = 1L; + + assertThrows(InvalidStateException.class, () -> + recordSearchService.search(roomId, type, sort, pageStart, pageEnd, isOverview, pageNum, userId)); } @Test - @DisplayName("정의되지 않은 type 값은 예외를 발생시킨다") - void testInvalidType() { - // given - RecordSearchQuery query = buildQuery("latest", 1, "invalidType") - .build(); - - // when & then - assertThrows(InvalidStateException.class, () -> recordSearchService.search(query)); + @DisplayName("형식이 맞지 않는 sort로 조회 시 예외 발생") + void search_with_invalid_sort() { + Long roomId = 1L; + String type = "group"; + String sort = "invalidSort"; + Integer pageStart = 1; + Integer pageEnd = 10; + Boolean isOverview = true; + Integer pageNum = 1; + Long userId = 1L; + + assertThrows(InvalidStateException.class, () -> + recordSearchService.search(roomId, type, sort, pageStart, pageEnd, isOverview, pageNum, userId)); } } \ No newline at end of file