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 8b0df79..0bf5c11 100644 --- a/src/main/java/com/redo/domain/community/converter/CommunityConverter.java +++ b/src/main/java/com/redo/domain/community/converter/CommunityConverter.java @@ -37,7 +37,9 @@ public static CommunityResponseDTO toCommunityResponse( Community community, long numComments, String imageUrl, - String writer + String writer, + String profileImageUrl, + String characterCode ) { return new CommunityResponseDTO( community.getId(), @@ -48,7 +50,9 @@ public static CommunityResponseDTO toCommunityResponse( community.getTitle(), String.valueOf(community.getCategory().getCode()), toPreview(community.getContent()), - writer + writer, + profileImageUrl, + characterCode ); } @@ -66,12 +70,16 @@ private static String toPreview(String content) { public static CommunityDetailResponseDTO toCommunityDetailResponse( Community community, String writer, + String profileImageUrl, + String characterCode, String imageUrl ) { return new CommunityDetailResponseDTO( community.getId(), community.getTitle(), writer, + profileImageUrl, + characterCode, community.getContent(), community.getCreatedAt(), imageUrl @@ -134,10 +142,17 @@ public static CommunityComment toCommunityComment(Community community, User user .build(); } - public static CommunityCommentResponseDTO toCommunityCommentResponse(CommunityComment comment, String writer) { + public static CommunityCommentResponseDTO toCommunityCommentResponse( + CommunityComment comment, + String writer, + String profileImageUrl, + String characterCode + ) { return new CommunityCommentResponseDTO( comment.getId(), writer, + profileImageUrl, + characterCode, comment.getContent(), comment.getCreatedAt() ); diff --git a/src/main/java/com/redo/domain/community/dto/res/CommunityCommentResponseDTO.java b/src/main/java/com/redo/domain/community/dto/res/CommunityCommentResponseDTO.java index 6b66e95..37d6ed7 100644 --- a/src/main/java/com/redo/domain/community/dto/res/CommunityCommentResponseDTO.java +++ b/src/main/java/com/redo/domain/community/dto/res/CommunityCommentResponseDTO.java @@ -5,6 +5,8 @@ public record CommunityCommentResponseDTO( Long commentId, String writer, + String profileImageUrl, + String characterCode, String content, LocalDateTime createdAt ) { 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 8875c7d..b6d25d0 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 @@ -6,6 +6,8 @@ public record CommunityDetailResponseDTO( Long id, String title, String writer, + String profileImageUrl, + String characterCode, String content, LocalDateTime createdAt, String imageUrl diff --git a/src/main/java/com/redo/domain/community/dto/res/CommunityResponseDTO.java b/src/main/java/com/redo/domain/community/dto/res/CommunityResponseDTO.java index d59aa1e..2cf1d68 100644 --- a/src/main/java/com/redo/domain/community/dto/res/CommunityResponseDTO.java +++ b/src/main/java/com/redo/domain/community/dto/res/CommunityResponseDTO.java @@ -11,6 +11,8 @@ public record CommunityResponseDTO( String title, String category, String preview, - String writer + String writer, + String profileImageUrl, + String characterCode ) { } diff --git a/src/main/java/com/redo/domain/community/repository/CommunityRepository.java b/src/main/java/com/redo/domain/community/repository/CommunityRepository.java index cdd06cb..3899079 100644 --- a/src/main/java/com/redo/domain/community/repository/CommunityRepository.java +++ b/src/main/java/com/redo/domain/community/repository/CommunityRepository.java @@ -20,20 +20,26 @@ public interface CommunityRepository extends JpaRepository { Optional findByIdAndDeletedAtIsNull(Long id); - // 목록 조회용: 게시글별 작성자 닉네임을 단건 쿼리 반복 없이 한 번에 조회한다. + // 목록 조회용: 게시글별 작성자 프로필(닉네임/프로필 이미지 키/캐릭터 코드)을 단건 쿼리 반복 없이 한 번에 조회한다. // 프로필이 없는 사용자도 게시글은 조회되어야 하므로 UserProfile 은 LEFT JOIN 한다. @Query(""" - SELECT c.id AS communityId, p.nickname AS nickname + SELECT c.id AS communityId, + p.nickname AS nickname, + p.profileImageKey AS profileImageKey, + p.characterCode AS characterCode FROM Community c LEFT JOIN UserProfile p ON p.user = c.user WHERE c IN :communities """) List findWritersByCommunities(@Param("communities") List communities); - // 게시글별 작성자 닉네임 조회 결과 projection + // 게시글별 작성자 프로필 조회 결과 projection + // LEFT JOIN 이므로 프로필이 없는 작성자는 communityId 를 제외한 값이 모두 null 이다. interface CommunityWriter { Long getCommunityId(); String getNickname(); + String getProfileImageKey(); + String getCharacterCode(); } // 동시 요청에서 갱신 유실(Lost Update)이 발생하지 않도록 좋아요 수를 DB에서 원자적으로 증가시킨다. 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 7861f74..b708d08 100644 --- a/src/main/java/com/redo/domain/community/service/CommunityService.java +++ b/src/main/java/com/redo/domain/community/service/CommunityService.java @@ -70,14 +70,20 @@ public Page getCommunityPosts(Integer category, Pageable p Map commentCounts = getCommentCounts(communities.getContent()); Map representativeImageKeys = getRepresentativeImageKeys(communities.getContent()); - Map writers = getWriters(communities.getContent()); - - return communities.map(community -> CommunityConverter.toCommunityResponse( - community, - commentCounts.getOrDefault(community.getId(), 0L), - createImageUrl(representativeImageKeys.get(community.getId())), - writers.get(community.getId()) - )); + Map writers = getWriters(communities.getContent()); + + return communities.map(community -> { + CommunityRepository.CommunityWriter writer = writers.get(community.getId()); + + return CommunityConverter.toCommunityResponse( + community, + commentCounts.getOrDefault(community.getId(), 0L), + createImageUrl(representativeImageKeys.get(community.getId())), + writer == null ? null : writer.getNickname(), + writer == null ? null : createImageUrl(writer.getProfileImageKey()), + writer == null ? null : writer.getCharacterCode() + ); + }); } // 게시글 상세 조회 로직 @@ -85,9 +91,13 @@ public CommunityDetailResponseDTO getCommunityPost(Long communityId) { Community community = communityRepository.findByIdAndDeletedAtIsNull(communityId) .orElseThrow(() -> new CommunityException(CommunityErrorCode.COMMUNITY_NOT_FOUND)); + UserProfile profile = getProfile(community.getUser().getId()); + return CommunityConverter.toCommunityDetailResponse( community, - getNickname(community.getUser().getId()), + getNickname(profile), + getProfileImageUrl(profile), + getCharacterCode(profile), getRepresentativeImageUrl(community) ); } @@ -107,7 +117,7 @@ public CommunityCreateResponseDTO createCommunityPost(Long userId, CommunityCrea // (등록 직후 즉시 조회 용도가 아니며, 조회 시점에 상세/목록 API가 Presigned URL을 새로 발급한다.) List imageKeys = saveImages(community, request.images()); - return CommunityConverter.toCommunityCreateResponse(community, imageKeys, getNickname(userId)); + return CommunityConverter.toCommunityCreateResponse(community, imageKeys, getNickname(getProfile(userId))); } // 댓글 목록 조회 로직(comment ID 기준 커서 페이징, cursor/length 없으면 전체 반환) @@ -123,10 +133,16 @@ public CommunityCommentListResponseDTO getCommunityComments(Long communityId, Lo return CommunityConverter.toCommunityCommentListResponse( comments.stream() - .map(comment -> CommunityConverter.toCommunityCommentResponse( - comment, - getNickname(comment.getUser().getId()) - )) + .map(comment -> { + UserProfile profile = getProfile(comment.getUser().getId()); + + return CommunityConverter.toCommunityCommentResponse( + comment, + getNickname(profile), + getProfileImageUrl(profile), + getCharacterCode(profile) + ); + }) .toList() ); } @@ -268,18 +284,17 @@ private Map getCommentCounts(List communities) { )); } - // 목록의 게시글별 작성자 닉네임을 한 번의 쿼리로 조회하는 로직 - private Map getWriters(List communities) { + // 목록의 게시글별 작성자 프로필을 한 번의 쿼리로 조회하는 로직 + // 프로필이 없는 작성자도 projection 자체는 반환되므로(각 필드가 null) Map 수집에서 제외하지 않는다. + private Map getWriters(List communities) { if (communities.isEmpty()) { return Map.of(); } return communityRepository.findWritersByCommunities(communities).stream() - // 프로필이 없어 닉네임이 null 인 경우 Map 수집에서 제외한다(조회 시 null 로 응답된다). - .filter(writer -> writer.getNickname() != null) .collect(Collectors.toMap( CommunityRepository.CommunityWriter::getCommunityId, - CommunityRepository.CommunityWriter::getNickname + writer -> writer )); } @@ -314,10 +329,23 @@ private String createImageUrl(String imageKey) { return s3Service.createPresignedUrl(imageKey); } - private String getNickname(Long userId) { - return userProfileRepository.findByUserId(userId) - .map(UserProfile::getNickname) - .orElse(null); + // 작성자 프로필을 조회하는 로직(프로필을 아직 만들지 않은 사용자는 null 을 반환한다) + private UserProfile getProfile(Long userId) { + return userProfileRepository.findByUserId(userId).orElse(null); + } + + private String getNickname(UserProfile profile) { + return profile == null ? null : profile.getNickname(); + } + + // 작성자 프로필 이미지의 S3 객체 키를 조회용 Presigned URL로 변환하는 로직 + // 프로필 이미지를 등록하지 않은 사용자는 null 이 되며, 이 경우 characterCode 로 대체 표시한다. + private String getProfileImageUrl(UserProfile profile) { + return profile == null ? null : createImageUrl(profile.getProfileImageKey()); + } + + private String getCharacterCode(UserProfile profile) { + return profile == null ? null : profile.getCharacterCode(); } private Community getActiveCommunity(Long communityId) {