diff --git a/src/main/java/org/ezcode/codetest/application/community/dto/response/DiscussionResponse.java b/src/main/java/org/ezcode/codetest/application/community/dto/response/DiscussionResponse.java index ed3c5317..aa69b8aa 100644 --- a/src/main/java/org/ezcode/codetest/application/community/dto/response/DiscussionResponse.java +++ b/src/main/java/org/ezcode/codetest/application/community/dto/response/DiscussionResponse.java @@ -2,13 +2,18 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.*; +import java.time.LocalDateTime; + import org.ezcode.codetest.application.usermanagement.user.dto.response.SimpleUserInfoResponse; +import org.ezcode.codetest.domain.community.dto.DiscussionQueryResult; import org.ezcode.codetest.domain.community.model.entity.Discussion; +import org.ezcode.codetest.domain.community.model.enums.VoteType; import io.swagger.v3.oas.annotations.media.Schema; -@Schema(name = "DiscussionResponse", description = "Discussion 조회 응답 DTO") +@Schema(name = "DiscussionResponse", description = "Discussion 응답 DTO, 목록 조회 시에만 추천 수, 비추천 수 등의 데이터가 포함됨") public record DiscussionResponse( + @Schema(description = "Discussion 고유 ID", example = "123", requiredMode = REQUIRED) Long discussionId, @@ -18,20 +23,51 @@ public record DiscussionResponse( @Schema(description = "관련 문제 ID", example = "45", requiredMode = REQUIRED) Long problemId, - @Schema(description = "사용 언어명", example = "Java 17", requiredMode = REQUIRED) - String languages, - @Schema(description = "토론 내용", example = "이 문제는 이렇게 풀 수 있습니다...", requiredMode = REQUIRED) - String content + String content, + + @Schema(description = "생성 일시", example = "2025-06-25T14:30:00", requiredMode = REQUIRED) + LocalDateTime createdAt, + + @Schema(description = "총 추천 수 (upvote)", example = "10") + Long upvoteCount, + + @Schema(description = "총 비추천 수 (downvote)", example = "2") + Long downvoteCount, + + @Schema(description = "총 댓글 수", example = "5") + Long replyCount, + + @Schema(description = "현재 사용자의 추천 상태 (UP, DOWN, NONE)", example = "UP") + VoteType voteStatus + ) { public static DiscussionResponse fromEntity(Discussion discussion) { return new DiscussionResponse( discussion.getId(), SimpleUserInfoResponse.fromEntity(discussion.getUser()), - discussion.getProblem().getId(), // 문제 id가 굳이 필요한가? - discussion.getLanguage().getName(), // TODO: 가공해줘야 할듯? - discussion.getContent() + discussion.getProblem().getId(), + discussion.getContent(), + discussion.getCreatedAt(), + null, + null, + null, + null + ); + } + + public static DiscussionResponse from(DiscussionQueryResult result) { + return new DiscussionResponse( + result.getDiscussionId(), + result.getUserInfo(), + result.getProblemId(), + result.getContent(), + result.getCreatedAt(), + result.getUpvoteCount(), + result.getDownvoteCount(), + result.getReplyCount(), + result.getVoteStatus() ); } } diff --git a/src/main/java/org/ezcode/codetest/application/community/dto/response/ReplyResponse.java b/src/main/java/org/ezcode/codetest/application/community/dto/response/ReplyResponse.java index 34e71cc4..6e1eb3b7 100644 --- a/src/main/java/org/ezcode/codetest/application/community/dto/response/ReplyResponse.java +++ b/src/main/java/org/ezcode/codetest/application/community/dto/response/ReplyResponse.java @@ -2,18 +2,23 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.*; +import java.time.LocalDateTime; + import org.ezcode.codetest.application.usermanagement.user.dto.response.SimpleUserInfoResponse; +import org.ezcode.codetest.domain.community.dto.ReplyQueryResult; import org.ezcode.codetest.domain.community.model.entity.Reply; +import org.ezcode.codetest.domain.community.model.enums.VoteType; import io.swagger.v3.oas.annotations.media.Schema; @Schema(name = "ReplyResponse", description = "Reply 조회 응답 DTO") public record ReplyResponse( + @Schema(description = "Reply 고유 ID", example = "456", requiredMode = REQUIRED) Long replyId, @Schema(description = "부모 Reply ID (없으면 null)", example = "123", nullable = true, requiredMode = NOT_REQUIRED) - Long parentId, + Long parentReplyId, @Schema(description = "소속 Discussion ID", example = "123", requiredMode = REQUIRED) Long discussionId, @@ -22,19 +27,58 @@ public record ReplyResponse( SimpleUserInfoResponse userInfo, @Schema(description = "댓글 내용", example = "동의합니다!", requiredMode = REQUIRED) - String content + String content, + + @Schema(description = "생성 일시", example = "2025-06-25T14:30:00", requiredMode = REQUIRED) + LocalDateTime createdAt, + + @Schema(description = "총 추천 수 (upvote)", example = "10") + Long upvoteCount, + + @Schema(description = "총 비추천 수 (downvote)", example = "2") + Long downvoteCount, + + @Schema(description = "총 댓글 수", example = "5") + Long childReplyCount, + + @Schema(description = "현재 사용자의 추천 상태 (UP, DOWN, NONE)", example = "UP") + VoteType voteStatus + ) { public static ReplyResponse fromEntity(Reply reply) { + Long parentId = (reply.getParent() != null) ? reply.getParent().getId() : null; + return new ReplyResponse( reply.getId(), parentId, reply.getDiscussion().getId(), SimpleUserInfoResponse.fromEntity(reply.getUser()), - reply.getContent() + reply.getContent(), + reply.getCreatedAt(), + null, + null, + null, + null + ); + } + + public static ReplyResponse from(ReplyQueryResult result) { + + return new ReplyResponse( + result.getReplyId(), + result.getParentReplyId(), + result.getDiscussionId(), + result.getUserInfo(), + result.getContent(), + result.getCreatedAt(), + result.getUpvoteCount(), + result.getDownvoteCount(), + result.getChildReplyCount(), + result.getVoteStatus() ); } } diff --git a/src/main/java/org/ezcode/codetest/application/community/dto/response/VoteResponse.java b/src/main/java/org/ezcode/codetest/application/community/dto/response/VoteResponse.java index b514fa6d..6406d049 100644 --- a/src/main/java/org/ezcode/codetest/application/community/dto/response/VoteResponse.java +++ b/src/main/java/org/ezcode/codetest/application/community/dto/response/VoteResponse.java @@ -2,7 +2,7 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.*; -import org.ezcode.codetest.domain.community.model.VoteResult; +import org.ezcode.codetest.domain.community.dto.VoteResult; import org.ezcode.codetest.domain.community.model.enums.VoteType; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/org/ezcode/codetest/application/community/service/BaseVoteService.java b/src/main/java/org/ezcode/codetest/application/community/service/BaseVoteService.java index bf71d27f..72f91af7 100644 --- a/src/main/java/org/ezcode/codetest/application/community/service/BaseVoteService.java +++ b/src/main/java/org/ezcode/codetest/application/community/service/BaseVoteService.java @@ -1,7 +1,7 @@ package org.ezcode.codetest.application.community.service; import org.ezcode.codetest.application.community.dto.response.VoteResponse; -import org.ezcode.codetest.domain.community.model.VoteResult; +import org.ezcode.codetest.domain.community.dto.VoteResult; import org.ezcode.codetest.domain.community.model.entity.BaseVote; import org.ezcode.codetest.domain.community.model.enums.VoteType; import org.ezcode.codetest.domain.community.service.BaseVoteDomainService; diff --git a/src/main/java/org/ezcode/codetest/application/community/service/DiscussionService.java b/src/main/java/org/ezcode/codetest/application/community/service/DiscussionService.java index ab751b27..85962829 100644 --- a/src/main/java/org/ezcode/codetest/application/community/service/DiscussionService.java +++ b/src/main/java/org/ezcode/codetest/application/community/service/DiscussionService.java @@ -3,6 +3,7 @@ import org.ezcode.codetest.application.community.dto.request.DiscussionCreateRequest; import org.ezcode.codetest.application.community.dto.request.DiscussionModifyRequest; import org.ezcode.codetest.application.community.dto.response.DiscussionResponse; +import org.ezcode.codetest.domain.community.dto.DiscussionQueryResult; import org.ezcode.codetest.domain.community.model.entity.Discussion; import org.ezcode.codetest.domain.community.service.DiscussionDomainService; import org.ezcode.codetest.domain.language.model.entity.Language; @@ -40,10 +41,11 @@ public DiscussionResponse createDiscussion(Long problemId, DiscussionCreateReque } @Transactional(readOnly = true) - public Page getDiscussions(Long problemId, Pageable pageable) { + public Page getDiscussions(Long problemId, String sortBy, Long userId, Pageable pageable) { - Page discussionResponsePage = discussionDomainService.getAllDiscussionsByProblemId(problemId, pageable); - return discussionResponsePage.map(DiscussionResponse::fromEntity); + Page result = + discussionDomainService.getAllDiscussionsByProblemId(problemId, sortBy, userId, pageable); + return result.map(DiscussionResponse::from); } @Transactional diff --git a/src/main/java/org/ezcode/codetest/application/community/service/ReplyService.java b/src/main/java/org/ezcode/codetest/application/community/service/ReplyService.java index 1e8002e8..c8e345c9 100644 --- a/src/main/java/org/ezcode/codetest/application/community/service/ReplyService.java +++ b/src/main/java/org/ezcode/codetest/application/community/service/ReplyService.java @@ -7,6 +7,7 @@ import org.ezcode.codetest.application.community.dto.response.ReplyResponse; import org.ezcode.codetest.application.notification.event.NotificationCreateEvent; import org.ezcode.codetest.application.notification.port.NotificationEventService; +import org.ezcode.codetest.domain.community.dto.ReplyQueryResult; import org.ezcode.codetest.domain.community.model.entity.Discussion; import org.ezcode.codetest.domain.community.model.entity.Reply; import org.ezcode.codetest.domain.community.service.DiscussionDomainService; @@ -58,13 +59,13 @@ public ReplyResponse createReply( } @Transactional(readOnly = true) - public Page getReplies(Long problemId, Long discussionId, Pageable pageable) { + public Page getReplies(Long problemId, Long discussionId, Long currentUserId, Pageable pageable) { Discussion discussion = discussionDomainService.getDiscussionForProblem(discussionId, problemId); - Page replies = replyDomainService.getRepliesByDiscussionId(discussion, pageable); + Page replies = replyDomainService.getRepliesByDiscussionId(discussion, currentUserId, pageable); - return replies.map(ReplyResponse::fromEntity); + return replies.map(ReplyResponse::from); } @Transactional(readOnly = true) @@ -72,14 +73,16 @@ public Page getChildReplies( Long problemId, Long discussionId, Long parentReplyId, + Long currentUserId, Pageable pageable ) { Discussion discussion = discussionDomainService.getDiscussionForProblem(discussionId, problemId); - Page replies = replyDomainService.getRepliesByParentReplyId(parentReplyId, discussion, pageable); + Page childReplies = + replyDomainService.getRepliesByParentReplyId(parentReplyId, discussion, currentUserId, pageable); - return replies.map(ReplyResponse::fromEntity); + return childReplies.map(ReplyResponse::from); } @Transactional diff --git a/src/main/java/org/ezcode/codetest/domain/community/dto/DiscussionQueryResult.java b/src/main/java/org/ezcode/codetest/domain/community/dto/DiscussionQueryResult.java new file mode 100644 index 00000000..5525736d --- /dev/null +++ b/src/main/java/org/ezcode/codetest/domain/community/dto/DiscussionQueryResult.java @@ -0,0 +1,63 @@ +package org.ezcode.codetest.domain.community.dto; + +import java.time.LocalDateTime; + +import org.ezcode.codetest.application.usermanagement.user.dto.response.SimpleUserInfoResponse; +import org.ezcode.codetest.domain.community.model.enums.VoteType; + +import com.querydsl.core.annotations.QueryProjection; + +import lombok.Getter; + +@Getter +public class DiscussionQueryResult { + + private final Long discussionId; + + private final SimpleUserInfoResponse userInfo; + + private final Long problemId; + + private final String content; + + private final LocalDateTime createdAt; + + private final Long upvoteCount; + + private final Long downvoteCount; + + private final Long replyCount; + + private final VoteType voteStatus; + + @QueryProjection + public DiscussionQueryResult( + Long discussionId, + SimpleUserInfoResponse userInfo, + Long problemId, + String content, + LocalDateTime createdAt, + Long upvoteCount, + Long downvoteCount, + Long replyCount, + VoteType voteType + ) { + + this.discussionId = discussionId; + this.userInfo = userInfo; + this.problemId = problemId; + this.content = content; + this.createdAt = createdAt; + this.upvoteCount = upvoteCount; + this.downvoteCount = downvoteCount; + this.replyCount = replyCount; + + if (voteType == null) { + this.voteStatus = VoteType.NONE; + } else if (voteType == VoteType.UP) { + this.voteStatus = VoteType.UP; + } else { + this.voteStatus = VoteType.DOWN; + } + } +} diff --git a/src/main/java/org/ezcode/codetest/domain/community/dto/ReplyQueryResult.java b/src/main/java/org/ezcode/codetest/domain/community/dto/ReplyQueryResult.java new file mode 100644 index 00000000..05cac7d1 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/domain/community/dto/ReplyQueryResult.java @@ -0,0 +1,67 @@ +package org.ezcode.codetest.domain.community.dto; + +import java.time.LocalDateTime; + +import org.ezcode.codetest.application.usermanagement.user.dto.response.SimpleUserInfoResponse; +import org.ezcode.codetest.domain.community.model.enums.VoteType; + +import com.querydsl.core.annotations.QueryProjection; + +import lombok.Getter; + +@Getter +public class ReplyQueryResult { + + private final Long replyId; + + private final SimpleUserInfoResponse userInfo; + + private final Long parentReplyId; + + private final Long discussionId; + + private final String content; + + private final LocalDateTime createdAt; + + private final Long upvoteCount; + + private final Long downvoteCount; + + private final Long childReplyCount; + + private final VoteType voteStatus; + + @QueryProjection + public ReplyQueryResult( + Long replyId, + SimpleUserInfoResponse userInfo, + Long parentReplyId, + Long discussionId, + String content, + LocalDateTime createdAt, + Long upvoteCount, + Long downvoteCount, + Long childReplyCount, + VoteType voteType + ) { + + this.replyId = replyId; + this.userInfo = userInfo; + this.parentReplyId = parentReplyId; + this.discussionId = discussionId; + this.content = content; + this.createdAt = createdAt; + this.upvoteCount = upvoteCount; + this.downvoteCount = downvoteCount; + this.childReplyCount = childReplyCount; + + if (voteType == null) { + this.voteStatus = VoteType.NONE; + } else if (voteType == VoteType.UP) { + this.voteStatus = VoteType.UP; + } else { + this.voteStatus = VoteType.DOWN; + } + } +} diff --git a/src/main/java/org/ezcode/codetest/domain/community/model/VoteResult.java b/src/main/java/org/ezcode/codetest/domain/community/dto/VoteResult.java similarity index 82% rename from src/main/java/org/ezcode/codetest/domain/community/model/VoteResult.java rename to src/main/java/org/ezcode/codetest/domain/community/dto/VoteResult.java index b1ef1eca..dfe65d57 100644 --- a/src/main/java/org/ezcode/codetest/domain/community/model/VoteResult.java +++ b/src/main/java/org/ezcode/codetest/domain/community/dto/VoteResult.java @@ -1,4 +1,4 @@ -package org.ezcode.codetest.domain.community.model; +package org.ezcode.codetest.domain.community.dto; import org.ezcode.codetest.domain.community.model.enums.VoteType; diff --git a/src/main/java/org/ezcode/codetest/domain/community/repository/DiscussionRepository.java b/src/main/java/org/ezcode/codetest/domain/community/repository/DiscussionRepository.java index b81ef787..ac76176c 100644 --- a/src/main/java/org/ezcode/codetest/domain/community/repository/DiscussionRepository.java +++ b/src/main/java/org/ezcode/codetest/domain/community/repository/DiscussionRepository.java @@ -1,10 +1,11 @@ package org.ezcode.codetest.domain.community.repository; +import java.util.List; import java.util.Optional; +import org.ezcode.codetest.domain.community.dto.DiscussionQueryResult; import org.ezcode.codetest.domain.community.model.entity.Discussion; import org.ezcode.codetest.domain.language.model.entity.Language; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; public interface DiscussionRepository { @@ -13,10 +14,14 @@ public interface DiscussionRepository { Optional findById(Long discussionId); - Page findAllByProblemId(Long problemId, Pageable pageable); + List findDiscussionIdsByProblemId(Long problemId, String sortBy, Pageable pageable); + + List findDiscussionsByIds(List discussionIds, Long currentUserId); void updateDiscussion(Discussion discussion, Language language, String content); void deleteDiscussion(Discussion discussion); + Long countByProblemId(Long problemId); + } diff --git a/src/main/java/org/ezcode/codetest/domain/community/repository/ReplyRepository.java b/src/main/java/org/ezcode/codetest/domain/community/repository/ReplyRepository.java index b4cc83e8..5f6b3837 100644 --- a/src/main/java/org/ezcode/codetest/domain/community/repository/ReplyRepository.java +++ b/src/main/java/org/ezcode/codetest/domain/community/repository/ReplyRepository.java @@ -2,6 +2,7 @@ import java.util.Optional; +import org.ezcode.codetest.domain.community.dto.ReplyQueryResult; import org.ezcode.codetest.domain.community.model.entity.Reply; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -12,9 +13,9 @@ public interface ReplyRepository { Optional findReplyById(Long replyId); - Page findAllRepliesByDiscussionId(Long discussionId, Pageable pageable); + Page findAllRepliesByDiscussionId(Long discussionId, Long currentUserId, Pageable pageable); - Page findAllChildRepliesByParentReplyId(Long parentReplyId, Pageable pageable); + Page findAllChildRepliesByParentReplyId(Long parentReplyId, Long currentUserId, Pageable pageable); void updateReply(Reply reply, String content); diff --git a/src/main/java/org/ezcode/codetest/domain/community/service/BaseVoteDomainService.java b/src/main/java/org/ezcode/codetest/domain/community/service/BaseVoteDomainService.java index a6bfebce..12920756 100644 --- a/src/main/java/org/ezcode/codetest/domain/community/service/BaseVoteDomainService.java +++ b/src/main/java/org/ezcode/codetest/domain/community/service/BaseVoteDomainService.java @@ -2,7 +2,7 @@ import java.util.Optional; -import org.ezcode.codetest.domain.community.model.VoteResult; +import org.ezcode.codetest.domain.community.dto.VoteResult; import org.ezcode.codetest.domain.community.model.entity.BaseVote; import org.ezcode.codetest.domain.community.model.enums.VoteType; import org.ezcode.codetest.domain.community.repository.BaseVoteRepository; diff --git a/src/main/java/org/ezcode/codetest/domain/community/service/DiscussionDomainService.java b/src/main/java/org/ezcode/codetest/domain/community/service/DiscussionDomainService.java index e26700a5..fcd80689 100644 --- a/src/main/java/org/ezcode/codetest/domain/community/service/DiscussionDomainService.java +++ b/src/main/java/org/ezcode/codetest/domain/community/service/DiscussionDomainService.java @@ -1,11 +1,19 @@ package org.ezcode.codetest.domain.community.service; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.ezcode.codetest.domain.community.dto.DiscussionQueryResult; import org.ezcode.codetest.domain.community.exception.CommunityException; import org.ezcode.codetest.domain.community.exception.CommunityExceptionCode; import org.ezcode.codetest.domain.community.model.entity.Discussion; import org.ezcode.codetest.domain.community.repository.DiscussionRepository; import org.ezcode.codetest.domain.language.model.entity.Language; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -36,9 +44,24 @@ public Discussion getDiscussionForProblem(Long discussionId, Long problemId) { return discussion; } - public Page getAllDiscussionsByProblemId(Long problemId, Pageable pageable) { + public Page getAllDiscussionsByProblemId(Long problemId, String sortBy, Long userId, Pageable pageable) { + + // 성능 향상 위해 쿼리 분리 + List discussionIds = discussionRepository.findDiscussionIdsByProblemId(problemId, sortBy, pageable); + List results = discussionRepository.findDiscussionsByIds(discussionIds, userId); + + Long totalCount = discussionRepository.countByProblemId(problemId); + + // `WHERE IN` 절은 ID 목록의 순서를 보장하지 않으므로, 처음에 정렬해서 얻은 ID 목록의 순서대로 results를 다시 정렬 + Map resultMap = results.stream() + .collect(Collectors.toMap(DiscussionQueryResult::getDiscussionId, Function.identity())); + + List sortedResults = discussionIds.stream() + .map(resultMap::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); - return discussionRepository.findAllByProblemId(problemId, pageable); + return new PageImpl<>(sortedResults, pageable, totalCount); } public Discussion modify(Long discussionId, Long problemId, Long userId, Language language, String content) { diff --git a/src/main/java/org/ezcode/codetest/domain/community/service/ReplyDomainService.java b/src/main/java/org/ezcode/codetest/domain/community/service/ReplyDomainService.java index f20dcefc..5e5a4917 100644 --- a/src/main/java/org/ezcode/codetest/domain/community/service/ReplyDomainService.java +++ b/src/main/java/org/ezcode/codetest/domain/community/service/ReplyDomainService.java @@ -3,6 +3,7 @@ import org.ezcode.codetest.application.notification.enums.NotificationType; import org.ezcode.codetest.application.notification.event.NotificationCreateEvent; import org.ezcode.codetest.application.notification.event.payload.ReplyCreatePayload; +import org.ezcode.codetest.domain.community.dto.ReplyQueryResult; import org.ezcode.codetest.domain.community.exception.CommunityException; import org.ezcode.codetest.domain.community.exception.CommunityExceptionCode; import org.ezcode.codetest.domain.community.model.entity.Discussion; @@ -54,17 +55,17 @@ public Reply getReplyForDiscussion(Long replyId, Discussion discussion) { return reply; } - public Page getRepliesByDiscussionId(Discussion discussion, Pageable pageable) { + public Page getRepliesByDiscussionId(Discussion discussion, Long currentUserId, Pageable pageable) { - return replyRepository.findAllRepliesByDiscussionId(discussion.getId(), pageable); + return replyRepository.findAllRepliesByDiscussionId(discussion.getId(), currentUserId, pageable); } - public Page getRepliesByParentReplyId(Long parentReplyId, Discussion discussion, Pageable pageable) { + public Page getRepliesByParentReplyId(Long parentReplyId, Discussion discussion, Long currentUserId, Pageable pageable) { Reply parentReply = getReplyById(parentReplyId); validateDiscussionMatches(parentReply, discussion); - return replyRepository.findAllChildRepliesByParentReplyId(parentReply.getId(), pageable); + return replyRepository.findAllChildRepliesByParentReplyId(parentReply.getId(), currentUserId, pageable); } public Reply modify(Long replyId, Discussion discussion, Long userId, String content) { diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionJpaRepository.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionJpaRepository.java index d3daa089..302c9f04 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionJpaRepository.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionJpaRepository.java @@ -1,22 +1,7 @@ package org.ezcode.codetest.infrastructure.persistence.repository.community.discussion; import org.ezcode.codetest.domain.community.model.entity.Discussion; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - -public interface DiscussionJpaRepository extends JpaRepository { - - @EntityGraph(attributePaths = { "user", "problem", "language" }) - @Query(""" - SELECT d - FROM Discussion d - WHERE d.problem.id = :problemId - AND d.isDeleted = false - ORDER BY d.createdAt DESC - """) - Page findAllByProblemId(Long problemId, Pageable pageable); +public interface DiscussionJpaRepository extends JpaRepository, DiscussionQueryRepository { } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionQueryRepository.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionQueryRepository.java new file mode 100644 index 00000000..9c86ea10 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionQueryRepository.java @@ -0,0 +1,16 @@ +package org.ezcode.codetest.infrastructure.persistence.repository.community.discussion; + +import java.util.List; + +import org.ezcode.codetest.domain.community.dto.DiscussionQueryResult; +import org.springframework.data.domain.Pageable; + +public interface DiscussionQueryRepository { + + List findDiscussionIdsByProblemId(Long problemId, String sortBy, Pageable pageable); + + List findDiscussionsByIds(List discussionIds, Long currentUserId); + + Long countByProblemId(Long problemId); + +} diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionQueryRepositoryImpl.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionQueryRepositoryImpl.java new file mode 100644 index 00000000..43f4c187 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionQueryRepositoryImpl.java @@ -0,0 +1,149 @@ +package org.ezcode.codetest.infrastructure.persistence.repository.community.discussion; + +import static org.ezcode.codetest.domain.community.model.entity.QDiscussion.*; +import static org.ezcode.codetest.domain.community.model.entity.QDiscussionVote.*; +import static org.ezcode.codetest.domain.community.model.entity.QReply.*; +import static org.ezcode.codetest.domain.user.model.entity.QUser.*; + +import java.util.ArrayList; +import java.util.List; + +import org.ezcode.codetest.application.usermanagement.user.dto.response.SimpleUserInfoResponse; +import org.ezcode.codetest.domain.community.dto.DiscussionQueryResult; +import org.ezcode.codetest.domain.community.dto.QDiscussionQueryResult; +import org.ezcode.codetest.domain.community.model.enums.VoteType; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class DiscussionQueryRepositoryImpl implements DiscussionQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List findDiscussionIdsByProblemId(Long problemId, String sortBy, Pageable pageable) { + + NumberExpression upvoteCount = getVoteCount(VoteType.UP); + NumberExpression downvoteCount = getVoteCount(VoteType.DOWN); + + NumberExpression bestScore = upvoteCount.subtract(downvoteCount); + + return jpaQueryFactory + .select(discussion.id) + .from(discussion) + .leftJoin(discussionVote).on(discussionVote.discussion.eq(discussion)) + .where(discussion.problem.id.eq(problemId)) + .groupBy(discussion.id) + .orderBy(getOrderSpecifier(sortBy, bestScore, upvoteCount)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + } + + @Override + public List findDiscussionsByIds(List discussionIds, Long currentUserId) { + + if (discussionIds == null || discussionIds.isEmpty()) { + return new ArrayList<>(); + } + + NumberExpression upvoteCount = getVoteCount(VoteType.UP); + NumberExpression downvoteCount = getVoteCount(VoteType.DOWN); + + Expression replyCount = reply.id.countDistinct(); + + Expression userVoteType; + if (currentUserId != null) { + userVoteType = new CaseBuilder() + .when(discussionVote.voter.id.eq(currentUserId)).then(discussionVote.voteType) + .otherwise(Expressions.nullExpression(VoteType.class)).max(); + } else { + userVoteType = Expressions.nullExpression(VoteType.class); + } + + return jpaQueryFactory + .select(new QDiscussionQueryResult( + discussion.id, + Projections.constructor(SimpleUserInfoResponse.class, + user.id, + user.nickname, + user.tier, + user.profileImageUrl + ), + discussion.problem.id, + discussion.content, + discussion.createdAt, + upvoteCount, + downvoteCount, + replyCount, + userVoteType + )) + .from(discussion) + .join(discussion.user, user) + .leftJoin(discussionVote).on(discussionVote.discussion.eq(discussion)) + .leftJoin(reply).on(reply.discussion.eq(discussion)) + .where(discussion.id.in(discussionIds)) + .groupBy( + discussion.id, + user.id, + user.nickname, + user.tier, + user.profileImageUrl, + discussion.problem.id, + discussion.content, + discussion.createdAt + ) + .fetch(); + } + + @Override + public Long countByProblemId(Long problemId) { + + Long count = jpaQueryFactory + .select(discussion.count()) + .from(discussion) + .where(discussion.problem.id.eq(problemId)) + .fetchOne(); + + return count != null ? count : 0L; + } + + + private NumberExpression getVoteCount(VoteType voteType) { + return new CaseBuilder() + .when(discussionVote.voteType.eq(voteType)).then(discussionVote.id) + .otherwise((Long) null) + .countDistinct(); + } + + private OrderSpecifier[] getOrderSpecifier(String sort, NumberExpression bestScore, Expression upvoteCount) { + OrderSpecifier primarySort; + + switch (sort.toLowerCase()) { + case "best": + primarySort = new OrderSpecifier<>(Order.DESC, bestScore); + break; + case "upvote": + primarySort = new OrderSpecifier<>(Order.DESC, upvoteCount); + break; + default: + return new OrderSpecifier[]{ new OrderSpecifier<>(Order.DESC, discussion.id) }; + } + + OrderSpecifier secondarySort = new OrderSpecifier<>(Order.DESC, discussion.id); + + return new OrderSpecifier[] { primarySort, secondarySort }; + } +} diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionRepositoryImpl.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionRepositoryImpl.java index 4783f3b3..213136d8 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionRepositoryImpl.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionRepositoryImpl.java @@ -1,11 +1,12 @@ package org.ezcode.codetest.infrastructure.persistence.repository.community.discussion; +import java.util.List; import java.util.Optional; +import org.ezcode.codetest.domain.community.dto.DiscussionQueryResult; import org.ezcode.codetest.domain.community.model.entity.Discussion; import org.ezcode.codetest.domain.community.repository.DiscussionRepository; import org.ezcode.codetest.domain.language.model.entity.Language; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @@ -31,9 +32,21 @@ public Optional findById(Long discussionId) { } @Override - public Page findAllByProblemId(Long problemId, Pageable pageable) { + public List findDiscussionIdsByProblemId(Long problemId, String sortBy, Pageable pageable) { - return discussionJpaRepository.findAllByProblemId(problemId, pageable); + return discussionJpaRepository.findDiscussionIdsByProblemId(problemId, sortBy, pageable); + } + + @Override + public List findDiscussionsByIds(List discussionIds, Long currentUserId) { + + return discussionJpaRepository.findDiscussionsByIds(discussionIds, currentUserId); + } + + @Override + public Long countByProblemId(Long problemId) { + + return discussionJpaRepository.countByProblemId(problemId); } @Override diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyJpaRepository.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyJpaRepository.java index b7e8b250..d0fcd4a1 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyJpaRepository.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyJpaRepository.java @@ -1,33 +1,7 @@ package org.ezcode.codetest.infrastructure.persistence.repository.community.reply; import org.ezcode.codetest.domain.community.model.entity.Reply; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - -public interface ReplyJpaRepository extends JpaRepository { - - @EntityGraph(attributePaths = { "user" }) - @Query(""" - SELECT r - FROM Reply r - WHERE r.discussion.id = :discussionId - AND r.isDeleted = false - AND r.parent IS NULL - ORDER BY r.createdAt DESC - """) - Page findAllByDiscussionId(Long discussionId, Pageable pageable); - - @EntityGraph(attributePaths = { "user" }) - @Query(""" - SELECT r - FROM Reply r - WHERE r.parent.id = :parentReplyId - AND r.isDeleted = false - ORDER BY r.createdAt ASC - """) - Page findAllByParentReplyId(Long parentReplyId, Pageable pageable); +public interface ReplyJpaRepository extends JpaRepository, ReplyQueryRepository { } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyQueryRepository.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyQueryRepository.java new file mode 100644 index 00000000..fb85cd01 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyQueryRepository.java @@ -0,0 +1,19 @@ +package org.ezcode.codetest.infrastructure.persistence.repository.community.reply; + +import org.ezcode.codetest.domain.community.dto.ReplyQueryResult; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface ReplyQueryRepository { + + // 댓글 목록 조회 + Page findRepliesByDiscussionId(Long discussionId, Long currentUserId, Pageable pageable); + + // 대댓글 목록 조회 + Page findRepliesByParentId(Long parentId, Long currentUserId, Pageable pageable); + + Long countByDiscussionId(Long discussionId); + + Long countByParentId(Long parentId); + +} diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyQueryRepositoryImpl.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyQueryRepositoryImpl.java new file mode 100644 index 00000000..50038023 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyQueryRepositoryImpl.java @@ -0,0 +1,131 @@ +package org.ezcode.codetest.infrastructure.persistence.repository.community.reply; + +import static org.ezcode.codetest.domain.community.model.entity.QReply.*; +import static org.ezcode.codetest.domain.user.model.entity.QUser.*; +import static org.ezcode.codetest.domain.community.model.entity.QReplyVote.*; + +import java.util.List; + +import org.ezcode.codetest.application.usermanagement.user.dto.response.SimpleUserInfoResponse; +import org.ezcode.codetest.domain.community.dto.QReplyQueryResult; +import org.ezcode.codetest.domain.community.dto.ReplyQueryResult; +import org.ezcode.codetest.domain.community.model.entity.QReply; +import org.ezcode.codetest.domain.community.model.enums.VoteType; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class ReplyQueryRepositoryImpl implements ReplyQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + // 댓글 목록 조회 + @Override + public Page findRepliesByDiscussionId(Long discussionId, Long currentUserId, Pageable pageable) { + + BooleanExpression condition = reply.discussion.id.eq(discussionId) + .and(reply.parent.isNull()); + + List results = findRepliesByCondition(condition, currentUserId, pageable); + Long total = countByDiscussionId(discussionId); + + return new PageImpl<>(results, pageable, total); + } + + // 대댓글 목록 조회 + @Override + public Page findRepliesByParentId(Long parentId, Long currentUserId, Pageable pageable) { + + BooleanExpression condition = reply.parent.id.eq(parentId); + + List results = findRepliesByCondition(condition, currentUserId, pageable); + Long total = countByParentId(parentId); + + return new PageImpl<>(results, pageable, total); + } + + private List findRepliesByCondition(BooleanExpression condition, Long currentUserId, Pageable pageable) { + QReply childReply = new QReply("childReply"); + + NumberExpression upvoteCount = getVoteCount(VoteType.UP); + NumberExpression downvoteCount = getVoteCount(VoteType.DOWN); + + Expression childReplyCount = childReply.id.count(); + + Expression userVoteType; + if (currentUserId != null) { + userVoteType = new CaseBuilder() + .when(replyVote.voter.id.eq(currentUserId)).then(replyVote.voteType) + .otherwise(Expressions.nullExpression(VoteType.class)).max(); + } else { + userVoteType = Expressions.nullExpression(VoteType.class); + } + + return jpaQueryFactory + .select(new QReplyQueryResult( + reply.id, + Projections.constructor(SimpleUserInfoResponse.class, + user.id, + user.nickname, + user.tier, + user.profileImageUrl + ), + reply.parent.id, + reply.discussion.id, + reply.content, + reply.createdAt, + upvoteCount, + downvoteCount, + childReplyCount, + userVoteType + )) + .from(reply) + .join(reply.user, user) + .leftJoin(replyVote).on(replyVote.reply.eq(reply)) + .leftJoin(childReply).on(childReply.parent.eq(reply)) + .where(condition) + .groupBy(reply.id) + .orderBy(reply.id.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + } + + @Override + public Long countByDiscussionId(Long discussionId) { + return jpaQueryFactory + .select(reply.count()) + .from(reply) + .where(reply.discussion.id.eq(discussionId).and(reply.parent.isNull())) + .fetchOne(); + } + + @Override + public Long countByParentId(Long parentId) { + return jpaQueryFactory + .select(reply.count()) + .from(reply) + .where(reply.parent.id.eq(parentId)) + .fetchOne(); + } + + private NumberExpression getVoteCount(VoteType voteType) { + return new CaseBuilder() + .when(replyVote.voteType.eq(voteType)).then(replyVote.id) + .otherwise((Long) null) + .countDistinct(); + } +} diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyRepositoryImpl.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyRepositoryImpl.java index 617096f9..da637acb 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyRepositoryImpl.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyRepositoryImpl.java @@ -2,6 +2,7 @@ import java.util.Optional; +import org.ezcode.codetest.domain.community.dto.ReplyQueryResult; import org.ezcode.codetest.domain.community.model.entity.Reply; import org.ezcode.codetest.domain.community.repository.ReplyRepository; import org.springframework.data.domain.Page; @@ -30,15 +31,15 @@ public Optional findReplyById(Long replyId) { } @Override - public Page findAllRepliesByDiscussionId(Long discussionId, Pageable pageable) { + public Page findAllRepliesByDiscussionId(Long discussionId, Long currentUserId, Pageable pageable) { - return replyJpaRepository.findAllByDiscussionId(discussionId, pageable); + return replyJpaRepository.findRepliesByDiscussionId(discussionId, currentUserId, pageable); } @Override - public Page findAllChildRepliesByParentReplyId(Long parentReplyId, Pageable pageable) { + public Page findAllChildRepliesByParentReplyId(Long parentReplyId, Long currentUserId, Pageable pageable) { - return replyJpaRepository.findAllByParentReplyId(parentReplyId, pageable); + return replyJpaRepository.findRepliesByParentId(parentReplyId, currentUserId, pageable); } @Override diff --git a/src/main/java/org/ezcode/codetest/presentation/community/DiscussionController.java b/src/main/java/org/ezcode/codetest/presentation/community/DiscussionController.java index afa0960a..1c8a43eb 100644 --- a/src/main/java/org/ezcode/codetest/presentation/community/DiscussionController.java +++ b/src/main/java/org/ezcode/codetest/presentation/community/DiscussionController.java @@ -18,18 +18,32 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @RestController @RequestMapping("/api/problems/{problemId}/discussions") +@Tag(name = "Discussions", description = "문제별 토론글 관리 API") @RequiredArgsConstructor public class DiscussionController { private final DiscussionService discussionService; + @Operation( + summary = "토론 생성", + description = "주어진 문제 ID에 새로운 Discussion을 생성합니다.", + parameters = { + @Parameter(name = "problemId", description = "문제 ID", required = true) + } + ) + @ApiResponse(responseCode = "201", description = "생성된 DiscussionResponse 반환") @PostMapping public ResponseEntity createDiscussion( @PathVariable Long problemId, @@ -41,16 +55,39 @@ public ResponseEntity createDiscussion( .body(discussionService.createDiscussion(problemId, request, authUser.getId())); } + @Operation( + summary = "토론 목록 조회", + description = "문제별 Discussion 목록을 정렬(sortBy) 및 페이징(pageable)하여 조회합니다. 로그인하지 않은 상태로도 조회할 수 있습니다.", + parameters = { + @Parameter(name = "problemId", description = "문제 ID", required = true), + @Parameter(name = "sortBy", description = "정렬 기준 (best, upvote, latest), best: upvote-downvote 순으로 정렬", example = "best", required = false) + } + ) + @ApiResponse(responseCode = "200", description = "토론 목록 조회 성공") @GetMapping public ResponseEntity> getDiscussions( @PathVariable Long problemId, - @PageableDefault Pageable pageable + @RequestParam(defaultValue = "best") String sortBy, + @PageableDefault Pageable pageable, + @AuthenticationPrincipal AuthUser authUser ) { + + Long currentUserId = (authUser != null ? authUser.getId() : null); + return ResponseEntity .ok() - .body(discussionService.getDiscussions(problemId, pageable)); + .body(discussionService.getDiscussions(problemId, sortBy, currentUserId, pageable)); } + @Operation( + summary = "토론 수정", + description = "특정 Discussion을 수정합니다.", + parameters = { + @Parameter(name = "problemId", description = "문제 ID", required = true), + @Parameter(name = "discussionId", description = "토론 ID", required = true) + } + ) + @ApiResponse(responseCode = "200", description = "수정된 DiscussionResponse 반환") @PutMapping("/{discussionId}") public ResponseEntity modifyDiscussion( @PathVariable Long problemId, @@ -63,6 +100,15 @@ public ResponseEntity modifyDiscussion( .body(discussionService.modifyDiscussion(problemId, discussionId, request, authUser.getId())); } + @Operation( + summary = "토론 삭제", + description = "특정 Discussion을 삭제합니다.", + parameters = { + @Parameter(name = "problemId", description = "문제 ID", required = true), + @Parameter(name = "discussionId", description = "토론 ID", required = true) + } + ) + @ApiResponse(responseCode = "200", description = "삭제 성공") @DeleteMapping("/{discussionId}") public ResponseEntity removeDiscussion( @PathVariable Long problemId, diff --git a/src/main/java/org/ezcode/codetest/presentation/community/ReplyController.java b/src/main/java/org/ezcode/codetest/presentation/community/ReplyController.java index 88a193db..a58dda87 100644 --- a/src/main/java/org/ezcode/codetest/presentation/community/ReplyController.java +++ b/src/main/java/org/ezcode/codetest/presentation/community/ReplyController.java @@ -70,10 +70,13 @@ public ResponseEntity createReply( public ResponseEntity> getReplies( @PathVariable Long problemId, @PathVariable Long discussionId, - @ParameterObject @PageableDefault Pageable pageable + @ParameterObject @PageableDefault Pageable pageable, + @AuthenticationPrincipal AuthUser authUser ) { - Page page = replyService.getReplies(problemId, discussionId, pageable); + Long currentUserId = (authUser != null ? authUser.getId() : null); + + Page page = replyService.getReplies(problemId, discussionId, currentUserId, pageable); return ResponseEntity.ok(page); } @@ -92,10 +95,13 @@ public ResponseEntity> getReplies( @PathVariable Long problemId, @PathVariable Long discussionId, @PathVariable Long parentReplyId, - @ParameterObject @PageableDefault Pageable pageable + @ParameterObject @PageableDefault Pageable pageable, + @AuthenticationPrincipal AuthUser authUser ) { - Page page = replyService.getChildReplies(problemId, discussionId, parentReplyId, pageable); + Long currentUserId = (authUser != null ? authUser.getId() : null); + + Page page = replyService.getChildReplies(problemId, discussionId, parentReplyId, currentUserId, pageable); return ResponseEntity.ok(page); } diff --git a/src/test/java/org/ezcode/codetest/application/community/service/DiscussionVoteServiceConcurrencyTest.java b/src/test/java/org/ezcode/codetest/concurrency/DiscussionVoteServiceConcurrencyTest.java similarity index 97% rename from src/test/java/org/ezcode/codetest/application/community/service/DiscussionVoteServiceConcurrencyTest.java rename to src/test/java/org/ezcode/codetest/concurrency/DiscussionVoteServiceConcurrencyTest.java index 765060e2..5de08050 100644 --- a/src/test/java/org/ezcode/codetest/application/community/service/DiscussionVoteServiceConcurrencyTest.java +++ b/src/test/java/org/ezcode/codetest/concurrency/DiscussionVoteServiceConcurrencyTest.java @@ -1,4 +1,4 @@ -package org.ezcode.codetest.application.community.service; +package org.ezcode.codetest.concurrency; import static org.assertj.core.api.Assertions.*; @@ -8,6 +8,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.ezcode.codetest.application.community.dto.request.VoteRequest; +import org.ezcode.codetest.application.community.service.DiscussionVoteService; import org.ezcode.codetest.domain.community.model.enums.VoteType; import org.ezcode.codetest.domain.community.repository.DiscussionRepository; import org.ezcode.codetest.domain.community.repository.DiscussionVoteRepository; diff --git a/src/test/java/org/ezcode/codetest/domain/community/service/DiscussionVoteDomainServiceTest.java b/src/test/java/org/ezcode/codetest/domain/community/service/DiscussionVoteDomainServiceTest.java index 079e7739..935d8e70 100644 --- a/src/test/java/org/ezcode/codetest/domain/community/service/DiscussionVoteDomainServiceTest.java +++ b/src/test/java/org/ezcode/codetest/domain/community/service/DiscussionVoteDomainServiceTest.java @@ -8,7 +8,7 @@ import org.ezcode.codetest.application.notification.enums.NotificationType; import org.ezcode.codetest.application.notification.event.NotificationCreateEvent; -import org.ezcode.codetest.domain.community.model.VoteResult; +import org.ezcode.codetest.domain.community.dto.VoteResult; import org.ezcode.codetest.domain.community.model.entity.Discussion; import org.ezcode.codetest.domain.community.model.entity.DiscussionVote; import org.ezcode.codetest.domain.community.model.enums.VoteType; diff --git a/src/test/java/org/ezcode/codetest/domain/community/service/ReplyVoteDomainServiceTest.java b/src/test/java/org/ezcode/codetest/domain/community/service/ReplyVoteDomainServiceTest.java index 79d15780..a927066a 100644 --- a/src/test/java/org/ezcode/codetest/domain/community/service/ReplyVoteDomainServiceTest.java +++ b/src/test/java/org/ezcode/codetest/domain/community/service/ReplyVoteDomainServiceTest.java @@ -7,9 +7,8 @@ import org.ezcode.codetest.application.notification.enums.NotificationType; import org.ezcode.codetest.application.notification.event.NotificationCreateEvent; -import org.ezcode.codetest.domain.community.model.VoteResult; +import org.ezcode.codetest.domain.community.dto.VoteResult; import org.ezcode.codetest.domain.community.model.entity.Discussion; -import org.ezcode.codetest.domain.community.model.entity.DiscussionVote; import org.ezcode.codetest.domain.community.model.entity.Reply; import org.ezcode.codetest.domain.community.model.entity.ReplyVote; import org.ezcode.codetest.domain.community.model.enums.VoteType; diff --git a/src/test/java/org/ezcode/codetest/util/DummyDataInit.java b/src/test/java/org/ezcode/codetest/util/DummyDataInit.java new file mode 100644 index 00000000..e416733e --- /dev/null +++ b/src/test/java/org/ezcode/codetest/util/DummyDataInit.java @@ -0,0 +1,39 @@ +package org.ezcode.codetest.util; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; + +@Disabled +@ActiveProfiles("test") +@SpringBootTest +public class DummyDataInit { + + @Autowired + private TestDataGenerator testDataGenerator; + + @Autowired + private TestDataGeneratorForDiscussion testDataGeneratorForDiscussion; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Test + // @Disabled("데이터 생성 필요 시에만 활성화") // 매번 실행되지 않도록 @Disabled 처리 + void generateDummyData() { + // 주의: 이 테스트는 실행에 수 분 ~ 수십 분이 소요될 수 있습니다. + // jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 0"); + // jdbcTemplate.execute("TRUNCATE TABLE discussion_vote"); + // jdbcTemplate.execute("TRUNCATE TABLE reply"); + // jdbcTemplate.execute("TRUNCATE TABLE discussion"); + // jdbcTemplate.execute("TRUNCATE TABLE users"); + // jdbcTemplate.execute("TRUNCATE TABLE problem"); + // jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 1"); + + // testDataGenerator.generate(); + testDataGeneratorForDiscussion.generate(); + } +} diff --git a/src/test/java/org/ezcode/codetest/util/TestDataGenerator.java b/src/test/java/org/ezcode/codetest/util/TestDataGenerator.java new file mode 100644 index 00000000..b1b6e5b8 --- /dev/null +++ b/src/test/java/org/ezcode/codetest/util/TestDataGenerator.java @@ -0,0 +1,285 @@ +package org.ezcode.codetest.util; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.support.TransactionTemplate; + +@Component +public class TestDataGenerator { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private TransactionTemplate transactionTemplate; + + private static final Logger log = LoggerFactory.getLogger(TestDataGenerator.class); + + private static final int USER_COUNT = 100_000; + private static final int PROBLEM_COUNT = 1_000; + private static final int LANGUAGE_COUNT = 5; + private static final int DISCUSSION_COUNT = 1_000_000; + + private static final int AVG_REPLIES_PER_DISCUSSION = 3; + private static final int AVG_VOTES_PER_DISCUSSION = 8; + + private static final int CHUNK_SIZE = 50_000; // 트랜잭션을 커밋하는 단위 + + public void generate() { + log.info("Test data generation started."); + long startTime = System.currentTimeMillis(); + + jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 0"); + + // 각 엔티티 생성을 독립적으로 실행 + createLanguages(); + createUsers(); + createProblems(); + createDiscussions(); + createReplies(); + createVotes(); + + jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 1"); + + long endTime = System.currentTimeMillis(); + log.info("Test data generation finished. Total time: {} ms", (endTime - startTime)); + } + + private void createLanguages() { + log.info("Generating {} languages...", LANGUAGE_COUNT); + String sql = "INSERT INTO language (`name`, `version`, `judge0_id`) VALUES (?, ?, ?)"; + + transactionTemplate.execute(status -> { + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + // i는 0부터 시작 + ps.setString(1, "Language " + (i + 1)); + ps.setString(2, "v" + (i + 1)); + ps.setLong(3, (long) (i + 1)); + } + @Override + public int getBatchSize() { + return LANGUAGE_COUNT; + } + }); + return null; + }); + log.info("... Committed {} languages.", LANGUAGE_COUNT); + } + + private void createUsers() { + log.info("Generating {} users...", USER_COUNT); + String sql = "INSERT INTO users (username, email, password, nickname, role, tier, verified, is_deleted, review_token, created_at, modified_at) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + for (int i = 0; i < USER_COUNT; i += CHUNK_SIZE) { + final int start = i; + final int end = Math.min(start + CHUNK_SIZE, USER_COUNT); + final int currentChunkSize = end - start; + + transactionTemplate.execute(status -> { + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int j) throws SQLException { + int userId = start + j + 1; + ps.setString(1, "testuser" + userId); + ps.setString(2, "testuser" + userId + "@example.com"); + ps.setString(3, "password" + userId); + ps.setString(4, "TestUserNickname" + userId); + ps.setString(5, "USER"); + ps.setString(6, "PRO"); + ps.setBoolean(7, true); + ps.setBoolean(8, false); + ps.setInt(9, 10); + Timestamp now = Timestamp.from(Instant.now()); + ps.setTimestamp(10, now); + ps.setTimestamp(11, now); + } + @Override + public int getBatchSize() { + return currentChunkSize; + } + }); + return null; + }); + log.info("... Committed users from {} to {}", start, end); + } + } + + private void createProblems() { + log.info("Generating {} problems...", PROBLEM_COUNT); + String sql = "INSERT INTO problem (creator_id, category, title, description, score, difficulty, memory_limit, time_limit, reference, is_deleted, created_at, modified_at) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + for (int i = 0; i < PROBLEM_COUNT; i += CHUNK_SIZE) { + final int start = i; + final int end = Math.min(start + CHUNK_SIZE, PROBLEM_COUNT); + final int currentChunkSize = end - start; + + transactionTemplate.execute(status -> { + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int j) throws SQLException { + int problemId = start + j + 1; + ps.setLong(1, ThreadLocalRandom.current().nextLong(1, USER_COUNT + 1)); + ps.setString(2, "DP"); + ps.setString(3, "Problem Title " + problemId); + ps.setString(4, "This is a description for problem " + problemId); + ps.setInt(5, 100); + ps.setString(6, "EASY"); + ps.setLong(7, 256L); + ps.setLong(8, 1000L); + ps.setString(9, "AI"); + ps.setBoolean(10, false); + Timestamp now = Timestamp.from(Instant.now()); + ps.setTimestamp(11, now); + ps.setTimestamp(12, now); + } + @Override + public int getBatchSize() { + return currentChunkSize; + } + }); + return null; + }); + log.info("... Committed problems from {} to {}", start, end); + } + } + + private void createDiscussions() { + log.info("Generating {} discussions...", DISCUSSION_COUNT); + String sql = "INSERT INTO discussion (user_id, problem_id, language_id, content, is_deleted, created_at, modified_at) " + + "VALUES (?, ?, ?, ?, ?, ?, ?)"; + + for (int i = 0; i < DISCUSSION_COUNT; i += CHUNK_SIZE) { + final int start = i; + final int end = Math.min(start + CHUNK_SIZE, DISCUSSION_COUNT); + final int currentChunkSize = end - start; + + transactionTemplate.execute(status -> { + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int j) throws SQLException { + int discussionId = start + j + 1; + ps.setLong(1, ThreadLocalRandom.current().nextLong(1, USER_COUNT + 1)); + ps.setLong(2, ThreadLocalRandom.current().nextLong(1, PROBLEM_COUNT + 1)); + ps.setLong(3, ThreadLocalRandom.current().nextLong(1, LANGUAGE_COUNT + 1)); + ps.setString(4, "This is a discussion content " + discussionId); + ps.setBoolean(5, false); + Timestamp now = Timestamp.from(Instant.now()); + ps.setTimestamp(6, now); + ps.setTimestamp(7, now); + } + @Override + public int getBatchSize() { + return currentChunkSize; + } + }); + return null; + }); + log.info("... Committed discussions from {} to {}", start, end); + } + } + + private void createReplies() { + int totalReplies = DISCUSSION_COUNT * AVG_REPLIES_PER_DISCUSSION; + log.info("Generating {} replies...", totalReplies); + String sql = "INSERT INTO reply (discussion_id, user_id, content, is_deleted, created_at, modified_at) " + + "VALUES (?, ?, ?, ?, ?, ?)"; + + for (int i = 0; i < totalReplies; i += CHUNK_SIZE) { + final int start = i; + final int end = Math.min(start + CHUNK_SIZE, totalReplies); + final int currentChunkSize = end - start; + + transactionTemplate.execute(status -> { + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int j) throws SQLException { + ps.setLong(1, ThreadLocalRandom.current().nextLong(1, DISCUSSION_COUNT + 1)); + ps.setLong(2, ThreadLocalRandom.current().nextLong(1, USER_COUNT + 1)); + ps.setString(3, "This is a reply content " + (start + j + 1)); + ps.setBoolean(4, false); + Timestamp now = Timestamp.from(Instant.now()); + ps.setTimestamp(5, now); + ps.setTimestamp(6, now); + } + @Override + public int getBatchSize() { + return currentChunkSize; + } + }); + return null; + }); + log.info("... Committed replies from {} to {}", start, end); + } + } + + private void createVotes() { + int totalVotes = DISCUSSION_COUNT * AVG_VOTES_PER_DISCUSSION; + log.info("Generating {} discussion votes systematically...", totalVotes); + + // INSERT IGNORE를 일반 INSERT로 변경 (중복이 없으므로) + String sql = "INSERT INTO discussion_vote (voter_id, discussion_id, vote_type, created_at) " + + "VALUES (?, ?, ?, ?)"; + + final Random random = new Random(); + + // CHUNK_SIZE는 그대로 트랜잭션 단위로 사용 + for (int i = 0; i < totalVotes; i += CHUNK_SIZE) { + final int start = i; + final int end = Math.min(start + CHUNK_SIZE, totalVotes); + final int currentChunkSize = end - start; + + // 트랜잭션 시작 + transactionTemplate.execute(status -> { + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int j) throws SQLException { + // i: 전체 투표 데이터에 대한 절대 인덱스 (0 ~ 7,999,999) + // j: 현재 청크 내의 상대 인덱스 (0 ~ 49,999) + int absoluteIndex = start + j; + + // 데이터를 순차적으로 생성하여 랜덤 I/O를 최소화 + // 1. discussion_id를 순차적으로 할당 (0, 0, ... 1, 1, ... ) + long discussionId = (long) (absoluteIndex / AVG_VOTES_PER_DISCUSSION) + 1; + if (discussionId > DISCUSSION_COUNT) { + discussionId = DISCUSSION_COUNT; // 마지막 토론에 나머지 투표 몰아주기 + } + + // 2. voter_id를 순차적으로 할당하여 중복을 피함 + // 한 토론 내에서 투표자(voter)가 겹치지 않도록 + long voterIdOffset = absoluteIndex % AVG_VOTES_PER_DISCUSSION; + long voterId = (discussionId - 1) * AVG_VOTES_PER_DISCUSSION + voterIdOffset + 1; + // voterId가 USER_COUNT를 넘지 않도록 보정 (선택적) + voterId = (voterId % USER_COUNT) + 1; + + ps.setLong(1, voterId); + ps.setLong(2, discussionId); + ps.setString(3, random.nextBoolean() ? "UP" : "DOWN"); + ps.setTimestamp(4, Timestamp.from(Instant.now())); + } + + @Override + public int getBatchSize() { + return currentChunkSize; + } + }); + return null; + }); + log.info("... Committed votes from {} to {}", start, end); + } + } +} diff --git a/src/test/java/org/ezcode/codetest/util/TestDataGeneratorForDiscussion.java b/src/test/java/org/ezcode/codetest/util/TestDataGeneratorForDiscussion.java new file mode 100644 index 00000000..9ecbab18 --- /dev/null +++ b/src/test/java/org/ezcode/codetest/util/TestDataGeneratorForDiscussion.java @@ -0,0 +1,177 @@ +package org.ezcode.codetest.util; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.support.TransactionTemplate; + +@Component +public class TestDataGeneratorForDiscussion { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private TransactionTemplate transactionTemplate; + + private static final Logger log = LoggerFactory.getLogger(TestDataGeneratorForDiscussion.class); + + private static final int USER_COUNT = 100_000; + private static final int PROBLEM_COUNT = 1_000; + private static final int LANGUAGE_COUNT = 5; + private static final int DISCUSSION_COUNT = 40000; + + private static final int AVG_REPLIES_PER_DISCUSSION = 3; + private static final int AVG_VOTES_PER_DISCUSSION = 8; + + private static final int CHUNK_SIZE = 50_000; // 트랜잭션을 커밋하는 단위 + + public void generate() { + log.info("Test data generation started."); + long startTime = System.currentTimeMillis(); + + jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 0"); + + createDiscussions(); + + jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 1"); + + long endTime = System.currentTimeMillis(); + log.info("Test data generation finished. Total time: {} ms", (endTime - startTime)); + } + + private void createDiscussions() { + log.info("Generating {} discussions...", DISCUSSION_COUNT); + String sql = "INSERT INTO discussion (user_id, problem_id, language_id, content, is_deleted, created_at, modified_at) " + + "VALUES (?, ?, ?, ?, ?, ?, ?)"; + + for (int i = 0; i < DISCUSSION_COUNT; i += CHUNK_SIZE) { + final int start = i; + final int end = Math.min(start + CHUNK_SIZE, DISCUSSION_COUNT); + final int currentChunkSize = end - start; + + transactionTemplate.execute(status -> { + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int j) throws SQLException { + int discussionId = start + j + 1; + ps.setLong(1, ThreadLocalRandom.current().nextLong(1, USER_COUNT + 1)); + ps.setLong(2, 500); + ps.setLong(3, ThreadLocalRandom.current().nextLong(1, LANGUAGE_COUNT + 1)); + ps.setString(4, "This is a discussion content " + discussionId); + ps.setBoolean(5, false); + Timestamp now = Timestamp.from(Instant.now()); + ps.setTimestamp(6, now); + ps.setTimestamp(7, now); + } + @Override + public int getBatchSize() { + return currentChunkSize; + } + }); + return null; + }); + log.info("... Committed discussions from {} to {}", start, end); + } + } + + private void createReplies() { + int totalReplies = DISCUSSION_COUNT * AVG_REPLIES_PER_DISCUSSION; + log.info("Generating {} replies...", totalReplies); + String sql = "INSERT INTO reply (discussion_id, user_id, content, is_deleted, created_at, modified_at) " + + "VALUES (?, ?, ?, ?, ?, ?)"; + + for (int i = 0; i < totalReplies; i += CHUNK_SIZE) { + final int start = i; + final int end = Math.min(start + CHUNK_SIZE, totalReplies); + final int currentChunkSize = end - start; + + transactionTemplate.execute(status -> { + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int j) throws SQLException { + ps.setLong(1, ThreadLocalRandom.current().nextLong(1, DISCUSSION_COUNT + 1)); + ps.setLong(2, ThreadLocalRandom.current().nextLong(1, USER_COUNT + 1)); + ps.setString(3, "This is a reply content " + (start + j + 1)); + ps.setBoolean(4, false); + Timestamp now = Timestamp.from(Instant.now()); + ps.setTimestamp(5, now); + ps.setTimestamp(6, now); + } + @Override + public int getBatchSize() { + return currentChunkSize; + } + }); + return null; + }); + log.info("... Committed replies from {} to {}", start, end); + } + } + + private void createVotes() { + int totalVotes = DISCUSSION_COUNT * AVG_VOTES_PER_DISCUSSION; + log.info("Generating {} discussion votes systematically...", totalVotes); + + // INSERT IGNORE를 일반 INSERT로 변경 (중복이 없으므로) + String sql = "INSERT INTO discussion_vote (voter_id, discussion_id, vote_type, created_at) " + + "VALUES (?, ?, ?, ?)"; + + final Random random = new Random(); + + // CHUNK_SIZE는 그대로 트랜잭션 단위로 사용 + for (int i = 0; i < totalVotes; i += CHUNK_SIZE) { + final int start = i; + final int end = Math.min(start + CHUNK_SIZE, totalVotes); + final int currentChunkSize = end - start; + + // 트랜잭션 시작 + transactionTemplate.execute(status -> { + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int j) throws SQLException { + // i: 전체 투표 데이터에 대한 절대 인덱스 (0 ~ 7,999,999) + // j: 현재 청크 내의 상대 인덱스 (0 ~ 49,999) + int absoluteIndex = start + j; + + // 데이터를 순차적으로 생성하여 랜덤 I/O를 최소화 + // 1. discussion_id를 순차적으로 할당 (0, 0, ... 1, 1, ... ) + long discussionId = (long) (absoluteIndex / AVG_VOTES_PER_DISCUSSION) + 1; + if (discussionId > DISCUSSION_COUNT) { + discussionId = DISCUSSION_COUNT; // 마지막 토론에 나머지 투표 몰아주기 + } + + // 2. voter_id를 순차적으로 할당하여 중복을 피함 + // 한 토론 내에서 투표자(voter)가 겹치지 않도록 + long voterIdOffset = absoluteIndex % AVG_VOTES_PER_DISCUSSION; + long voterId = (discussionId - 1) * AVG_VOTES_PER_DISCUSSION + voterIdOffset + 1; + // voterId가 USER_COUNT를 넘지 않도록 보정 (선택적) + voterId = (voterId % USER_COUNT) + 1; + + ps.setLong(1, voterId); + ps.setLong(2, discussionId); + ps.setString(3, random.nextBoolean() ? "UP" : "DOWN"); + ps.setTimestamp(4, Timestamp.from(Instant.now())); + } + + @Override + public int getBatchSize() { + return currentChunkSize; + } + }); + return null; + }); + log.info("... Committed votes from {} to {}", start, end); + } + } +} diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 125aa86d..ceb203f0 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -1,7 +1,7 @@ # ======================== # Spring Config # ======================== -spring.application.name=min +spring.application.name=ezcode spring.config.import=optional:file:.env[.properties] # ======================== diff --git a/src/test/resources/discussion-query-explain.sql b/src/test/resources/discussion-query-explain.sql new file mode 100644 index 00000000..58eacedb --- /dev/null +++ b/src/test/resources/discussion-query-explain.sql @@ -0,0 +1,162 @@ +# 기존 코드 +EXPLAIN +SELECT + d1_0.id, + d1_0.user_id, + u1_0.nickname, + u1_0.tier, + u1_0.profile_image_url, + d1_0.problem_id, + d1_0.content, + d1_0.created_at, + (SELECT count(dv1_0.id) FROM discussion_vote dv1_0 WHERE dv1_0.discussion_id = d1_0.id AND dv1_0.vote_type = 'UP'), + (SELECT count(dv2_0.id) FROM discussion_vote dv2_0 WHERE dv2_0.discussion_id = d1_0.id AND dv2_0.vote_type = 'DOWN'), + (SELECT count(r1_0.id) FROM reply r1_0 WHERE r1_0.discussion_id = d1_0.id), + (SELECT dv3_0.vote_type FROM discussion_vote dv3_0 WHERE dv3_0.discussion_id = d1_0.id AND dv3_0.voter_id = 1) +FROM + discussion d1_0 + JOIN + users u1_0 ON u1_0.id = d1_0.user_id +WHERE + d1_0.problem_id = 500 +ORDER BY + ((SELECT count(dv4_0.id) FROM discussion_vote dv4_0 WHERE dv4_0.discussion_id = d1_0.id AND dv4_0.vote_type = 'UP') + - + (SELECT count(dv5_0.id) FROM discussion_vote dv5_0 WHERE dv5_0.discussion_id = d1_0.id AND dv5_0.vote_type = 'DOWN')) DESC, + d1_0.id DESC + LIMIT 10 OFFSET 0; + +# 1차 개선 +EXPLAIN +SELECT + d1_0.id, + u1_0.id, + u1_0.nickname, + u1_0.tier, + u1_0.profile_image_url, + d1_0.problem_id, + d1_0.content, + d1_0.created_at, + count(distinct case when (dv1_0.vote_type = 'UP') then dv1_0.id else null end), + count(distinct case when (dv1_0.vote_type = 'DOWN') then dv1_0.id else null end), + count(distinct r1_0.id), + max(case when (dv1_0.voter_id = 1) then dv1_0.vote_type else null end) +FROM + discussion d1_0 + JOIN + users u1_0 + ON d1_0.user_id = u1_0.id + LEFT JOIN + discussion_vote dv1_0 + ON dv1_0.discussion_id = d1_0.id + LEFT JOIN + reply r1_0 + ON r1_0.discussion_id = d1_0.id +WHERE + d1_0.problem_id = 500 +GROUP BY + d1_0.id +ORDER BY + (count(distinct case when (dv1_0.vote_type = 'UP') then dv1_0.id else null end) + - + count(distinct case when (dv1_0.vote_type = 'DOWN') then dv1_0.id else null end)) DESC, + d1_0.id DESC + LIMIT 10 OFFSET 0; + +# 2차 개선 - 조건에 해당하는 ids 조회 +EXPLAIN +SELECT + d1_0.id +FROM + discussion d1_0 + LEFT JOIN + discussion_vote dv1_0 + ON dv1_0.discussion_id = d1_0.id +WHERE + d1_0.problem_id = 500 +GROUP BY + d1_0.id +ORDER BY + (count(distinct case when (dv1_0.vote_type = 'UP') then dv1_0.id else null end) + - + count(distinct case when (dv1_0.vote_type = 'DOWN') then dv1_0.id else null end)) DESC, + d1_0.id DESC +LIMIT 10 OFFSET 0; +# 2차 개선 - 찾은 ID에 해당하는 상세 데이터 조회 +EXPLAIN +SELECT + d1_0.id, + u1_0.id, + u1_0.nickname, + u1_0.tier, + u1_0.profile_image_url, + d1_0.problem_id, + d1_0.content, + d1_0.created_at, + count(distinct case when (dv1_0.vote_type = 'UP') then dv1_0.id else null end), + count(distinct case when (dv1_0.vote_type = 'DOWN') then dv1_0.id else null end), + count(distinct r1_0.id), + max(case when (dv1_0.voter_id = 1) then dv1_0.vote_type else null end) +FROM + discussion d1_0 + JOIN + users u1_0 + ON d1_0.user_id = u1_0.id + LEFT JOIN + discussion_vote dv1_0 + ON dv1_0.discussion_id = d1_0.id + LEFT JOIN + reply r1_0 + ON r1_0.discussion_id = d1_0.id +WHERE + d1_0.id IN (101, 205, 301, 411, 562, 633, 745, 812, 933, 1054) -- 첫 쿼리 결과로 나온 ID 10개 (예시) +GROUP BY + d1_0.id, + u1_0.id, + u1_0.nickname, + u1_0.tier, + u1_0.profile_image_url, + d1_0.content, + d1_0.created_at; + +# 번외 - 쿼리 분리 + 서브쿼리 사용 (1) +EXPLAIN +SELECT + d1_0.id +FROM + discussion d1_0 + LEFT JOIN + discussion_vote dv1_0 + ON dv1_0.discussion_id = d1_0.id +WHERE + d1_0.problem_id = 500 +GROUP BY + d1_0.id +ORDER BY + (count(distinct case when (dv1_0.vote_type = 'UP') then dv1_0.id else null end) + - + count(distinct case when (dv1_0.vote_type = 'DOWN') then dv1_0.id else null end)) DESC, + d1_0.id DESC +LIMIT 10 OFFSET 0; + +# 번외 - 쿼리 분리 + 서브쿼리 사용 (2) +EXPLAIN +SELECT + d1_0.id, + d1_0.user_id, + u1_0.nickname, + u1_0.tier, + u1_0.profile_image_url, + d1_0.problem_id, + d1_0.content, + d1_0.created_at, + (SELECT count(dv1_0.id) FROM discussion_vote dv1_0 WHERE dv1_0.discussion_id = d1_0.id AND dv1_0.vote_type = 'UP'), + (SELECT count(dv2_0.id) FROM discussion_vote dv2_0 WHERE dv2_0.discussion_id = d1_0.id AND dv2_0.vote_type = 'DOWN'), + (SELECT count(r1_0.id) FROM reply r1_0 WHERE r1_0.discussion_id = d1_0.id), + (SELECT dv3_0.vote_type FROM discussion_vote dv3_0 WHERE dv3_0.discussion_id = d1_0.id AND dv3_0.voter_id = 1) +FROM + discussion d1_0 + JOIN + users u1_0 ON u1_0.id = d1_0.user_id +WHERE + d1_0.id IN (101, 205, 301, 411, 562, 633, 745, 812, 933, 1054); -- 첫 쿼리 결과로 나온 ID 10개 (예시) \ No newline at end of file