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
Expand Up @@ -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(),
Expand All @@ -48,7 +50,9 @@ public static CommunityResponseDTO toCommunityResponse(
community.getTitle(),
String.valueOf(community.getCategory().getCode()),
toPreview(community.getContent()),
writer
writer,
profileImageUrl,
characterCode
);
}

Expand All @@ -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
Expand Down Expand Up @@ -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()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
public record CommunityCommentResponseDTO(
Long commentId,
String writer,
String profileImageUrl,
String characterCode,
String content,
LocalDateTime createdAt
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ public record CommunityDetailResponseDTO(
Long id,
String title,
String writer,
String profileImageUrl,
String characterCode,
String content,
LocalDateTime createdAt,
String imageUrl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public record CommunityResponseDTO(
String title,
String category,
String preview,
String writer
String writer,
String profileImageUrl,
String characterCode
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,26 @@ public interface CommunityRepository extends JpaRepository<Community, Long> {

Optional<Community> 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<CommunityWriter> findWritersByCommunities(@Param("communities") List<Community> communities);

// 게시글별 작성자 닉네임 조회 결과 projection
// 게시글별 작성자 프로필 조회 결과 projection
// LEFT JOIN 이므로 프로필이 없는 작성자는 communityId 를 제외한 값이 모두 null 이다.
interface CommunityWriter {
Long getCommunityId();
String getNickname();
String getProfileImageKey();
String getCharacterCode();
}

// 동시 요청에서 갱신 유실(Lost Update)이 발생하지 않도록 좋아요 수를 DB에서 원자적으로 증가시킨다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,34 @@ public Page<CommunityResponseDTO> getCommunityPosts(Integer category, Pageable p

Map<Long, Long> commentCounts = getCommentCounts(communities.getContent());
Map<Long, String> representativeImageKeys = getRepresentativeImageKeys(communities.getContent());
Map<Long, String> 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<Long, CommunityRepository.CommunityWriter> 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()
);
});
}

// 게시글 상세 조회 로직
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)
);
}
Expand All @@ -107,7 +117,7 @@ public CommunityCreateResponseDTO createCommunityPost(Long userId, CommunityCrea
// (등록 직후 즉시 조회 용도가 아니며, 조회 시점에 상세/목록 API가 Presigned URL을 새로 발급한다.)
List<String> imageKeys = saveImages(community, request.images());

return CommunityConverter.toCommunityCreateResponse(community, imageKeys, getNickname(userId));
return CommunityConverter.toCommunityCreateResponse(community, imageKeys, getNickname(getProfile(userId)));
}

// 댓글 목록 조회 로직(comment ID 기준 커서 페이징, cursor/length 없으면 전체 반환)
Expand All @@ -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()
);
Comment on lines 134 to 147
}
Expand Down Expand Up @@ -268,18 +284,17 @@ private Map<Long, Long> getCommentCounts(List<Community> communities) {
));
}

// 목록의 게시글별 작성자 닉네임을 한 번의 쿼리로 조회하는 로직
private Map<Long, String> getWriters(List<Community> communities) {
// 목록의 게시글별 작성자 프로필을 한 번의 쿼리로 조회하는 로직
// 프로필이 없는 작성자도 projection 자체는 반환되므로(각 필드가 null) Map 수집에서 제외하지 않는다.
private Map<Long, CommunityRepository.CommunityWriter> getWriters(List<Community> 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
));
}

Expand Down Expand Up @@ -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) {
Expand Down