Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package org.devkor.apu.saerok_server.domain.admin.audit.application.dto;

public record AdminAuditQueryCommand(Integer page, Integer size) {
import org.devkor.apu.saerok_server.global.shared.util.Pageable;

public boolean hasValidPagination() {
if (page == null && size == null) return true;
if (page == null || size == null) return false;
return page >= 1 && size > 0;
}
}
public record AdminAuditQueryCommand(
Integer page,
Integer size
) implements Pageable {}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.GetCollectionCommentsResponse;
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.GetCollectionDetailResponse;
import org.devkor.apu.saerok_server.domain.collection.application.CollectionCommentQueryService;
import org.devkor.apu.saerok_server.domain.collection.application.dto.CommentQueryCommand;
import org.devkor.apu.saerok_server.domain.collection.application.helper.CollectionImageUrlService;
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollection;
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollectionComment;
Expand Down Expand Up @@ -99,7 +100,7 @@ public ReportedCollectionDetailResponse getReportedCollectionDetail(Long reportI
);

// 댓글 목록 (관리자 기준 isLiked/isMine 계산 불필요)
GetCollectionCommentsResponse comments = commentQueryService.getComments(collection.getId(), null);
GetCollectionCommentsResponse comments = commentQueryService.getComments(collection.getId(), null, new CommentQueryCommand(null, null));

return new ReportedCollectionDetailResponse(report.getId(), collectionDetail, comments);
}
Expand All @@ -126,7 +127,7 @@ public ReportedCommentDetailResponse getReportedCommentDetail(Long reportId) {
);

// 댓글 목록
GetCollectionCommentsResponse comments = commentQueryService.getComments(parentCollection.getId(), null);
GetCollectionCommentsResponse comments = commentQueryService.getComments(parentCollection.getId(), null, new CommentQueryCommand(null, null));

// 신고된 댓글 정보
ReportedCommentDetailResponse.ReportedComment commentDto =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.devkor.apu.saerok_server.domain.collection.api;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
Expand All @@ -13,7 +14,9 @@
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.*;
import org.devkor.apu.saerok_server.domain.collection.application.CollectionCommentCommandService;
import org.devkor.apu.saerok_server.domain.collection.application.CollectionCommentQueryService;
import org.devkor.apu.saerok_server.domain.collection.application.dto.CommentQueryCommand;
import org.devkor.apu.saerok_server.global.security.principal.UserPrincipal;
import org.devkor.apu.saerok_server.global.shared.exception.BadRequestException;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand Down Expand Up @@ -102,19 +105,34 @@ public void deleteComment(
@PermitAll
@Operation(
summary = "컬렉션 댓글 목록 조회 (인증: optional)",
description = """
컬렉션의 댓글 목록을 조회합니다.

📄 **페이징 (선택)**
- `page`와 `size`는 둘 다 제공해야 하며, 하나만 제공 시 Bad Request가 발생합니다.
- 생략하면 전체 결과를 반환합니다.
""",
security = @SecurityRequirement(name = "bearerAuth"),
responses = {
@ApiResponse(responseCode = "200", description = "댓글 목록 조회 성공",
content = @Content(schema = @Schema(implementation = GetCollectionCommentsResponse.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "404", description = "컬렉션이 존재하지 않음", content = @Content)
}
)
public GetCollectionCommentsResponse listComments(
@PathVariable Long collectionId,
@AuthenticationPrincipal UserPrincipal userPrincipal
@AuthenticationPrincipal UserPrincipal userPrincipal,
@Parameter(description = "페이지 번호 (1부터 시작)", example = "1") @RequestParam(required = false) Integer page,
@Parameter(description = "페이지 크기", example = "20") @RequestParam(required = false) Integer size
) {
CommentQueryCommand command = new CommentQueryCommand(page, size);
if (!command.hasValidPagination()) {
throw new BadRequestException("page와 size 값이 유효하지 않아요.");
}

Long userId = userPrincipal == null ? null : userPrincipal.getId();
return commentQueryService.getComments(collectionId, userId);
return commentQueryService.getComments(collectionId, userId, command);
}

/* 댓글 개수 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
public record GetCollectionCommentsResponse(
List<Item> items,
@Schema(description = "내 컬렉션인지 여부", example = "true")
Boolean isMyCollection
Boolean isMyCollection,
@Schema(description = "다음 페이지 존재 여부 (페이징 요청 시에만 유효)", example = "true")
Boolean hasNext
) {

public record Item(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.RequiredArgsConstructor;
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.GetCollectionCommentCountResponse;
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.GetCollectionCommentsResponse;
import org.devkor.apu.saerok_server.domain.collection.application.dto.CommentQueryCommand;
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollection;
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollectionComment;
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionCommentLikeRepository;
Expand All @@ -16,6 +17,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
Expand All @@ -33,18 +35,27 @@ public class CollectionCommentQueryService {
private final CommentContentResolver commentContentResolver;

/* 댓글 목록 (createdAt ASC) */
public GetCollectionCommentsResponse getComments(Long collectionId, Long userId) {
public GetCollectionCommentsResponse getComments(Long collectionId, Long userId, CommentQueryCommand command) {

UserBirdCollection collection = collectionRepository.findById(collectionId)
.orElseThrow(() -> new NotFoundException("해당 id의 컬렉션이 존재하지 않아요"));

// 내 컬렉션인지 여부 판단 (비회원인 경우 false)
boolean isMyCollection = userId != null && userId.equals(collection.getUser().getId());

// 1. 댓글 목록 조회
List<UserBirdCollectionComment> comments = commentRepository.findByCollectionId(collectionId);

// 2. 댓글 ID 목록 추출
// 1. 댓글 목록 조회 (페이징 시 size+1개 조회됨)
List<UserBirdCollectionComment> comments = commentRepository.findByCollectionId(collectionId, command);

// 2. hasNext 판단 및 초과분 제거
Boolean hasNext = null;
if (command.hasPagination()) {
hasNext = comments.size() > command.size();
if (hasNext) {
comments = new ArrayList<>(comments.subList(0, command.size()));
}
}

// 3. 댓글 ID 목록 추출
List<Long> commentIds = comments.stream()
.map(UserBirdCollectionComment::getId)
.toList();
Expand Down Expand Up @@ -73,8 +84,8 @@ public GetCollectionCommentsResponse getComments(Long collectionId, Long userId)
Map<Long, String> profileImageUrls = userProfileImageUrlService.getProfileImageUrlsFor(users);
Map<Long, String> thumbnailProfileImageUrls = userProfileImageUrlService.getProfileThumbnailImageUrlsFor(users);

// 7. 응답 생성
return collectionCommentWebMapper.toGetCollectionCommentsResponse(comments, likeCounts, likeStatuses, mineStatuses, profileImageUrls, thumbnailProfileImageUrls, isMyCollection, commentContentResolver);
// 8. 응답 생성
return collectionCommentWebMapper.toGetCollectionCommentsResponse(comments, likeCounts, likeStatuses, mineStatuses, profileImageUrls, thumbnailProfileImageUrls, isMyCollection, hasNext, commentContentResolver);
}

/* 댓글 개수 */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.devkor.apu.saerok_server.domain.collection.application.dto;

import org.devkor.apu.saerok_server.global.shared.util.Pageable;

public record CommentQueryCommand(
Integer page,
Integer size
) implements Pageable {}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.devkor.apu.saerok_server.domain.collection.core.repository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import lombok.RequiredArgsConstructor;
import org.devkor.apu.saerok_server.domain.collection.application.dto.CommentQueryCommand;
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollectionComment;
import org.springframework.stereotype.Repository;

Expand All @@ -24,16 +26,27 @@ public Optional<UserBirdCollectionComment> findById(Long id) {

public void remove(UserBirdCollectionComment comment) { em.remove(comment); }

public List<UserBirdCollectionComment> findByCollectionId(Long collectionId) {
return em.createQuery(
public List<UserBirdCollectionComment> findByCollectionId(Long collectionId, CommentQueryCommand command) {
Query query = em.createQuery(
"SELECT DISTINCT c FROM UserBirdCollectionComment c " +
"LEFT JOIN FETCH c.user " +
"LEFT JOIN FETCH c.parent " +
"WHERE c.collection.id = :collectionId " +
"ORDER BY c.createdAt ASC",
UserBirdCollectionComment.class)
.setParameter("collectionId", collectionId)
.getResultList();
.setParameter("collectionId", collectionId);

applyPagination(query, command);
return query.getResultList();
}

// 헬퍼 메서드 (hasNext 판단을 위해 size+1개 조회)
private void applyPagination(Query query, CommentQueryCommand command) {
if (command.hasPagination()) {
int offset = (command.page() - 1) * command.size();
query.setFirstResult(offset);
query.setMaxResults(command.size() + 1);
}
}

public long countByCollectionId(Long collectionId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ default GetCollectionCommentsResponse toGetCollectionCommentsResponse(
Map<Long, String> profileImageUrls,
Map<Long, String> thumbnailProfileImageUrls,
Boolean isMyCollection,
Boolean hasNext,
@Context CommentContentResolver commentContentResolver) {
if (entities == null || entities.isEmpty()) {
return new GetCollectionCommentsResponse(List.of(), isMyCollection);
return new GetCollectionCommentsResponse(List.of(), isMyCollection, hasNext);
}

for (UserBirdCollectionComment entity : entities) {
Expand Down Expand Up @@ -73,7 +74,7 @@ default GetCollectionCommentsResponse toGetCollectionCommentsResponse(
return buildCommentItem(comment, likeCounts, likeStatuses, mineStatuses, profileImageUrls, thumbnailProfileImageUrls, replies, commentContentResolver);
})
.toList();
return new GetCollectionCommentsResponse(items, isMyCollection);
return new GetCollectionCommentsResponse(items, isMyCollection, hasNext);
}

/* 댓글 엔티티 → Item DTO (공통 매핑 로직) */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
package org.devkor.apu.saerok_server.domain.community.application.dto;

import org.devkor.apu.saerok_server.global.shared.util.Pageable;

public record CommunityQueryCommand(
Integer page,
Integer size,
String query
) {
public boolean hasValidPagination() {
if ((page != null && size == null) || (page == null && size != null)) {
return false;
}

if (page == null) { // page == null && size == null
return true;
}

return page >= 1 && size >= 1;
}

public boolean hasPagination() {
return page != null && size != null;
}
}
) implements Pageable {}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.devkor.apu.saerok_server.domain.dex.bird.application.dto;

import org.devkor.apu.saerok_server.global.shared.util.Pageable;

import java.util.List;

public record BirdSearchCommand (
public record BirdSearchCommand(
Integer page,
Integer size,
String q,
Expand All @@ -11,23 +13,7 @@ public record BirdSearchCommand (
List<String> seasons,
String sort,
String sortDir
){
/**
* size와 page 중 한쪽만 null일 수 없고,
* size >= 1, page >= 1
* @return 해당 조건을 만족하는지
*/
public boolean hasValidPagination() {
if ((page != null && size == null) || (page == null && size != null)) {
return false;
}

if (page == null) { // page == null && size == null
return true;
}

return page >= 1 && size >= 1;
}
) implements Pageable {

public List<String> getSizeCategories() {
return sizeCategories == null ? List.of() : sizeCategories;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.devkor.apu.saerok_server.global.shared.util;

/**
* 페이징 기능을 제공하는 커맨드 객체를 위한 인터페이스.
* record에서 구현하면 page(), size() 메서드가 자동으로 제공됩니다.
*/
public interface Pageable {

Integer page();

Integer size();

/**
* 페이징 파라미터 유효성 검증.
* - page와 size는 둘 다 제공하거나 둘 다 null이어야 함
* - 제공 시 page >= 1, size >= 1
*/
default boolean hasValidPagination() {
if ((page() != null && size() == null) || (page() == null && size() != null)) {
return false;
}

if (page() == null) { // page == null && size == null
return true;
}

return page() >= 1 && size() >= 1;
}

/**
* 페이징이 요청되었는지 확인.
*/
default boolean hasPagination() {
return page() != null && size() != null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.GetCollectionCommentsResponse;
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.GetCollectionDetailResponse;
import org.devkor.apu.saerok_server.domain.collection.application.CollectionCommentQueryService;
import org.devkor.apu.saerok_server.domain.collection.application.dto.CommentQueryCommand;
import org.devkor.apu.saerok_server.domain.collection.application.helper.CollectionImageUrlService;
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollection;
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollectionComment;
Expand Down Expand Up @@ -164,7 +165,7 @@ void getReportedCollectionDetail_success() {
ReflectionTestUtils.setField(rep, "collection", col);

GetCollectionDetailResponse detail = new GetCollectionDetailResponse();
GetCollectionCommentsResponse comments = new GetCollectionCommentsResponse(List.of(), false);
GetCollectionCommentsResponse comments = new GetCollectionCommentsResponse(List.of(), false, null);

when(collectionReportRepository.findById(999L)).thenReturn(Optional.of(rep));
when(collectionImageUrlService.getPrimaryImageUrlFor(col)).thenReturn(Optional.of("img"));
Expand All @@ -174,7 +175,7 @@ void getReportedCollectionDetail_success() {
when(userProfileImageUrlService.getProfileThumbnailImageUrlFor(owner)).thenReturn("profile_thumb");
when(collectionWebMapper.toGetCollectionDetailResponse(col, "img", "profile", "profile_thumb", 10L, 2L, false, false))
.thenReturn(detail);
when(commentQueryService.getComments(10L, null)).thenReturn(comments);
when(commentQueryService.getComments(eq(10L), isNull(), any(CommentQueryCommand.class))).thenReturn(comments);

ReportedCollectionDetailResponse res = sut.getReportedCollectionDetail(999L);

Expand Down Expand Up @@ -206,7 +207,7 @@ void getReportedCommentDetail_success() {
ReflectionTestUtils.setField(rep, "comment", cm);

GetCollectionDetailResponse detail = new GetCollectionDetailResponse();
GetCollectionCommentsResponse comments = new GetCollectionCommentsResponse(List.of(), false);
GetCollectionCommentsResponse comments = new GetCollectionCommentsResponse(List.of(), false, null);

when(commentReportRepository.findById(777L)).thenReturn(Optional.of(rep));
when(collectionImageUrlService.getPrimaryImageUrlFor(col)).thenReturn(Optional.empty());
Expand All @@ -216,7 +217,7 @@ void getReportedCommentDetail_success() {
when(userProfileImageUrlService.getProfileThumbnailImageUrlFor(owner)).thenReturn("p_thumb");
when(collectionWebMapper.toGetCollectionDetailResponse(col, null, "p", "p_thumb", 5L, 1L, false, false))
.thenReturn(detail);
when(commentQueryService.getComments(100L, null)).thenReturn(comments);
when(commentQueryService.getComments(eq(100L), isNull(), any(CommentQueryCommand.class))).thenReturn(comments);

ReportedCommentDetailResponse res = sut.getReportedCommentDetail(777L);

Expand Down
Loading