From 88d0e008ace1bcc32a37d4f9ad16f40fba302f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9A=B0?= Date: Tue, 4 Aug 2026 00:26:26 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[Feat]#108=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 상세 조회 응답에 category, numComments, numLikes, isLiked, isMine 을 추가한다. 필드명은 목록 조회 응답과 동일하게 맞춘다. - isLiked/isMine 판별에 조회자 ID가 필요해 상세 조회에도 @AuthenticationPrincipal 을 추가한다(좋아요/삭제 API와 동일한 방식). - numComments 는 소프트 삭제된 댓글을 제외하기 위해 countByCommunityAndDeletedAtIsNull 을 새로 추가해 집계한다. - isLiked/isMine 은 Jackson 이 is 접두사를 제거하지 않도록 @JsonProperty 로 이름을 고정한다(ApiResponse 의 isSuccess 와 동일). Co-Authored-By: Claude Opus 5 --- .../controller/CommunityController.java | 3 ++- .../converter/CommunityConverter.java | 12 ++++++++++-- .../dto/res/CommunityDetailResponseDTO.java | 10 +++++++++- .../repository/CommunityCommentRepository.java | 3 +++ .../community/service/CommunityService.java | 18 ++++++++++++++++-- 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/redo/domain/community/controller/CommunityController.java b/src/main/java/com/redo/domain/community/controller/CommunityController.java index 818d317..6f31837 100644 --- a/src/main/java/com/redo/domain/community/controller/CommunityController.java +++ b/src/main/java/com/redo/domain/community/controller/CommunityController.java @@ -77,11 +77,12 @@ public ApiResponse getCommunityPosts( // 커뮤니티 게시글 상세 조회 API @GetMapping("/{communityId}") public ApiResponse getCommunityPost( + @AuthenticationPrincipal Long userId, @PathVariable Long communityId ) { return ApiResponse.onSuccess( CommunitySuccessCode.GET_COMMUNITY_POST_SUCCESS, - communityService.getCommunityPost(communityId) + communityService.getCommunityPost(userId, communityId) ); } diff --git a/src/main/java/com/redo/domain/community/converter/CommunityConverter.java b/src/main/java/com/redo/domain/community/converter/CommunityConverter.java index 0bf5c11..3c0d1c3 100644 --- a/src/main/java/com/redo/domain/community/converter/CommunityConverter.java +++ b/src/main/java/com/redo/domain/community/converter/CommunityConverter.java @@ -72,7 +72,10 @@ public static CommunityDetailResponseDTO toCommunityDetailResponse( String writer, String profileImageUrl, String characterCode, - String imageUrl + String imageUrl, + long numComments, + boolean isLiked, + boolean isMine ) { return new CommunityDetailResponseDTO( community.getId(), @@ -82,7 +85,12 @@ public static CommunityDetailResponseDTO toCommunityDetailResponse( characterCode, community.getContent(), community.getCreatedAt(), - imageUrl + imageUrl, + String.valueOf(community.getCategory().getCode()), + numComments, + community.getLikeCount() == null ? 0 : community.getLikeCount(), + isLiked, + isMine ); } diff --git a/src/main/java/com/redo/domain/community/dto/res/CommunityDetailResponseDTO.java b/src/main/java/com/redo/domain/community/dto/res/CommunityDetailResponseDTO.java index b6d25d0..0fc475b 100644 --- a/src/main/java/com/redo/domain/community/dto/res/CommunityDetailResponseDTO.java +++ b/src/main/java/com/redo/domain/community/dto/res/CommunityDetailResponseDTO.java @@ -1,5 +1,7 @@ package com.redo.domain.community.dto.res; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.time.LocalDateTime; public record CommunityDetailResponseDTO( @@ -10,6 +12,12 @@ public record CommunityDetailResponseDTO( String characterCode, String content, LocalDateTime createdAt, - String imageUrl + String imageUrl, + String category, + Long numComments, + Integer numLikes, + // ApiResponse 의 isSuccess 와 동일하게, Jackson 이 is 접두사를 떼지 않도록 이름을 고정한다. + @JsonProperty("isLiked") boolean isLiked, + @JsonProperty("isMine") boolean isMine ) { } diff --git a/src/main/java/com/redo/domain/community/repository/CommunityCommentRepository.java b/src/main/java/com/redo/domain/community/repository/CommunityCommentRepository.java index 0b52134..d1583e5 100644 --- a/src/main/java/com/redo/domain/community/repository/CommunityCommentRepository.java +++ b/src/main/java/com/redo/domain/community/repository/CommunityCommentRepository.java @@ -27,6 +27,9 @@ interface CommunityCommentCount { long getCommentCount(); } + // 상세 조회용: 단일 게시글의 삭제되지 않은 댓글 수를 집계한다. + long countByCommunityAndDeletedAtIsNull(Community community); + List findByCommunityAndIdGreaterThanAndDeletedAtIsNullOrderByIdAsc( Community community, Long cursor, diff --git a/src/main/java/com/redo/domain/community/service/CommunityService.java b/src/main/java/com/redo/domain/community/service/CommunityService.java index b708d08..91f3f4a 100644 --- a/src/main/java/com/redo/domain/community/service/CommunityService.java +++ b/src/main/java/com/redo/domain/community/service/CommunityService.java @@ -87,7 +87,7 @@ public Page getCommunityPosts(Integer category, Pageable p } // 게시글 상세 조회 로직 - public CommunityDetailResponseDTO getCommunityPost(Long communityId) { + public CommunityDetailResponseDTO getCommunityPost(Long userId, Long communityId) { Community community = communityRepository.findByIdAndDeletedAtIsNull(communityId) .orElseThrow(() -> new CommunityException(CommunityErrorCode.COMMUNITY_NOT_FOUND)); @@ -98,10 +98,24 @@ public CommunityDetailResponseDTO getCommunityPost(Long communityId) { getNickname(profile), getProfileImageUrl(profile), getCharacterCode(profile), - getRepresentativeImageUrl(community) + getRepresentativeImageUrl(community), + communityCommentRepository.countByCommunityAndDeletedAtIsNull(community), + isLiked(userId, community), + isMine(userId, community) ); } + // 조회자가 해당 게시글에 좋아요를 눌렀는지 판별하는 로직 + private boolean isLiked(Long userId, Community community) { + return userId != null + && communityLikeRepository.existsById(new CommunityLikeId(community.getId(), userId)); + } + + // 조회자가 해당 게시글의 작성자인지 판별하는 로직 + private boolean isMine(Long userId, Community community) { + return userId != null && userId.equals(community.getUser().getId()); + } + // 게시글 등록 로직 @Transactional public CommunityCreateResponseDTO createCommunityPost(Long userId, CommunityCreateRequestDTO request) { From e747f519191405734d1fadb7a8db2784247dd14d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9A=B0?= Date: Tue, 4 Aug 2026 00:32:28 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[Fix]#108=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=B2=A8=EB=B6=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 상세 조회가 목록용 대표 이미지 로직을 재사용해 display_order 가 가장 작은 1장만 반환하고 있었다. 등록 시 여러 장이 저장되지만 2번째 이후 이미지는 조회 경로가 없어 노출되지 않았다. - CommunityDetailResponseDTO 의 imageUrl(String) 을 imageUrls(List) 로 교체한다. 첨부가 없으면 빈 배열이다. - findByCommunityOrderByDisplayOrderAsc 를 추가해 등록 순서대로 조회하고, 상세에서만 쓰이던 findFirstByCommunityOrderByDisplayOrderAsc 와 getRepresentativeImageUrl 을 제거한다. - 목록 조회는 기존대로 대표 이미지 1장(imageUrl)을 유지한다. Co-Authored-By: Claude Opus 5 --- .../community/converter/CommunityConverter.java | 4 ++-- .../dto/res/CommunityDetailResponseDTO.java | 3 ++- .../community/repository/CommunityImgRepository.java | 4 ++-- .../domain/community/service/CommunityService.java | 12 +++++++----- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/redo/domain/community/converter/CommunityConverter.java b/src/main/java/com/redo/domain/community/converter/CommunityConverter.java index 3c0d1c3..61170a0 100644 --- a/src/main/java/com/redo/domain/community/converter/CommunityConverter.java +++ b/src/main/java/com/redo/domain/community/converter/CommunityConverter.java @@ -72,7 +72,7 @@ public static CommunityDetailResponseDTO toCommunityDetailResponse( String writer, String profileImageUrl, String characterCode, - String imageUrl, + List imageUrls, long numComments, boolean isLiked, boolean isMine @@ -85,7 +85,7 @@ public static CommunityDetailResponseDTO toCommunityDetailResponse( characterCode, community.getContent(), community.getCreatedAt(), - imageUrl, + imageUrls, String.valueOf(community.getCategory().getCode()), numComments, community.getLikeCount() == null ? 0 : community.getLikeCount(), diff --git a/src/main/java/com/redo/domain/community/dto/res/CommunityDetailResponseDTO.java b/src/main/java/com/redo/domain/community/dto/res/CommunityDetailResponseDTO.java index 0fc475b..36405bd 100644 --- a/src/main/java/com/redo/domain/community/dto/res/CommunityDetailResponseDTO.java +++ b/src/main/java/com/redo/domain/community/dto/res/CommunityDetailResponseDTO.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.time.LocalDateTime; +import java.util.List; public record CommunityDetailResponseDTO( Long id, @@ -12,7 +13,7 @@ public record CommunityDetailResponseDTO( String characterCode, String content, LocalDateTime createdAt, - String imageUrl, + List imageUrls, String category, Long numComments, Integer numLikes, diff --git a/src/main/java/com/redo/domain/community/repository/CommunityImgRepository.java b/src/main/java/com/redo/domain/community/repository/CommunityImgRepository.java index 4c85da0..34385a5 100644 --- a/src/main/java/com/redo/domain/community/repository/CommunityImgRepository.java +++ b/src/main/java/com/redo/domain/community/repository/CommunityImgRepository.java @@ -5,11 +5,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; -import java.util.Optional; public interface CommunityImgRepository extends JpaRepository { - Optional findFirstByCommunityOrderByDisplayOrderAsc(Community community); + // 상세 조회용: 단일 게시글의 첨부 이미지 전체를 display_order 오름차순으로 조회한다. + List findByCommunityOrderByDisplayOrderAsc(Community community); // 목록 조회용: 여러 게시글의 이미지를 한 번에 조회한다(display_order 오름차순, 게시글별 첫 항목이 대표 이미지). List findByCommunityInOrderByDisplayOrderAsc(List communities); diff --git a/src/main/java/com/redo/domain/community/service/CommunityService.java b/src/main/java/com/redo/domain/community/service/CommunityService.java index 91f3f4a..ba5975d 100644 --- a/src/main/java/com/redo/domain/community/service/CommunityService.java +++ b/src/main/java/com/redo/domain/community/service/CommunityService.java @@ -41,6 +41,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; @Service @@ -98,7 +99,7 @@ public CommunityDetailResponseDTO getCommunityPost(Long userId, Long communityId getNickname(profile), getProfileImageUrl(profile), getCharacterCode(profile), - getRepresentativeImageUrl(community), + getImageUrls(community), communityCommentRepository.countByCommunityAndDeletedAtIsNull(community), isLiked(userId, community), isMine(userId, community) @@ -326,12 +327,13 @@ private Map getRepresentativeImageKeys(List communities )); } - // 대표 이미지(display_order 최솟값)의 S3 객체 키를 조회용 Presigned URL로 변환하는 로직 - private String getRepresentativeImageUrl(Community community) { - return communityImgRepository.findFirstByCommunityOrderByDisplayOrderAsc(community) + // 상세 조회용: 첨부 이미지 전체의 S3 객체 키를 등록 순서(display_order)대로 Presigned URL로 변환하는 로직 + private List getImageUrls(Community community) { + return communityImgRepository.findByCommunityOrderByDisplayOrderAsc(community).stream() .map(CommunityImg::getImageKey) .map(this::createImageUrl) - .orElse(null); + .filter(Objects::nonNull) + .toList(); } // S3 객체 키를 이미지 조회용 Presigned URL로 변환하는 로직