Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.hrr.backend.global.response.SuccessCode;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
Expand All @@ -36,9 +37,11 @@ public class CommentController {
@Operation(summary = "댓글 작성", description = "인증글에 댓글 또는 대댓글을 작성합니다.")
@PostMapping("/{verificationId}")
public ApiResponse<CommentResponseDto> createComment(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long verificationId,
@Valid @RequestBody CommentCreateRequestDto requestDto
@Parameter(hidden = true)
@AuthenticationPrincipal CustomUserDetails userDetails,

@PathVariable Long verificationId,
@Valid @RequestBody CommentCreateRequestDto requestDto
) {
Long userId = userDetails.getUser().getId();

Expand All @@ -51,27 +54,33 @@ public ApiResponse<CommentResponseDto> createComment(
@Operation(summary = "댓글/대댓글 조회", description = "특정 인증글의 모든 댓글 및 대댓글을 조회합니다.")
@GetMapping("/{verificationId}")
public ApiResponse<CommentListResponseDto> getComments(
@PathVariable Long verificationId,
@Parameter(hidden = true)
@AuthenticationPrincipal CustomUserDetails userDetails,

@PathVariable Long verificationId,

// 페이징
@Min(1)
@RequestParam(name = "page", defaultValue = "1") int page, // 페이지 번호 (1-based)
@RequestParam(name = "size", defaultValue = "10") int size // 페이지 크기
// 페이징
@Min(1)
@RequestParam(name = "page", defaultValue = "1") int page, // 페이지 번호 (1-based)
@RequestParam(name = "size", defaultValue = "10") int size // 페이지 크기
) {
Long userId = userDetails.getUser().getId();

Pageable pageable = PageRequest.of(page-1, size);

CommentListResponseDto response = commentService.getComments(verificationId, pageable);
CommentListResponseDto response = commentService.getComments(verificationId, userId, pageable);
return ApiResponse.onSuccess(SuccessCode.OK, response);
}

/** 댓글 수정 */
@Operation(summary = "댓글 수정", description = "본인이 작성한 댓글만 수정할 수 있습니다.")
@PatchMapping("/{commentId}")
public ApiResponse<CommentResponseDto> updateComment(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long commentId,
@RequestBody CommentUpdateRequestDto requestDto
@Parameter(hidden = true)
@AuthenticationPrincipal CustomUserDetails userDetails,

@PathVariable Long commentId,
@RequestBody CommentUpdateRequestDto requestDto
) {
Long userId = userDetails.getUser().getId();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,110 @@
package com.hrr.backend.domain.comment.converter;

import java.time.LocalDateTime;

import org.springframework.stereotype.Component;

import com.hrr.backend.domain.comment.dto.CommentResponseDto;
import com.hrr.backend.domain.comment.entity.Comment;
import com.hrr.backend.domain.user.entity.User;
import lombok.experimental.UtilityClass;
import com.hrr.backend.domain.user.entity.enums.UserStatus;
import com.hrr.backend.global.s3.S3UrlUtil;

@UtilityClass
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class CommentConverter {

public static CommentResponseDto toDto(Comment comment) {
private final S3UrlUtil s3UrlUtil;

User user = comment.getUser();
/**
* Comment 객체 받아서 응답 형태로 변환
* @param comment 변환할 댓글 객체
* @param currentUserId 조회하는 사용자의 userId; 마스킹 처리 여부를 판단하는 데 사용
* @return
*/
public CommentResponseDto toDto(Comment comment, Long currentUserId) {
/**
* reponse에 필요한 항목들 계산 - 1. comment에서 그대로 가져오는 값들
*/
Long commentId = comment.getId();
Long verificationId = comment.getVerification().getId(); // 어떤 인증글(게시글)에 달린 댓글인지
User commentOwner = comment.getUser(); // 댓글 작성자

return CommentResponseDto.builder()
.commentId(comment.getId())
.parentId(comment.getParent() != null ? comment.getParent().getId() : null)
.verificationId(comment.getVerification().getId())
boolean isAnonymous = comment.isAnonymous();
int depth = comment.getDepth();
boolean isAdopted = comment.isAdopted();
int likesCount = comment.getLikesCount();

// 작성자 ID
.userId(user.getId())
LocalDateTime createdAt = comment.getCreatedAt();
LocalDateTime updatedAt = comment.getUpdatedAt();

/**
* reponse에 필요한 항목들 계산 - 2. 추가 계산 필요한 값들
*/
// 부모 댓글 id
Long parentId = comment.getParent() != null ? comment.getParent().getId() : null;

// 프로필 이미지 - 소셜 플랫폼에서 그대로 가져오는 경우와 직접 업로드 한 이미지인 경우 분리
// 소셜 플랫폼의 프로필: 이미 full URL이라 그대로 반환
// 직접 업로드한 이미지: AWS S3의 image Key만 저장되기에 prefix 추가 필요
// 익명이면 null 반환
String userProfileUrl = !isAnonymous? s3UrlUtil.toFullUrl(commentOwner.getProfileImage()) : null ; //util에서 자동 처리

// 익명이면 "익명", 아니면 nickname
.userName(comment.isAnonymous() ? "익명" : user.getNickname())
// 닉네임 - 닉네임 or 마스킹 or (알 수 없음)
// 마스킹 조건 - 익명이면서, 조회하는 사람의 게시글이 아니면서, 조회하는 사람의 댓글이 아님
boolean isMyVerificationPost = comment.getVerification().getUserChallenge().getUser().getId().equals(currentUserId);
boolean isMyComment = commentOwner.getId().equals(currentUserId);

// 익명이면 null, 아니면 profileImage (URL)
.userProfileUrl(comment.isAnonymous() ? null : user.getProfileImage())
boolean maskingCondition =
isAnonymous && !isMyVerificationPost && !isMyComment;

.isAnonymous(comment.isAnonymous())
.depth(comment.getDepth())
.content(comment.isDeleted() ? "삭제된 댓글입니다." : comment.getContent())
.likesCount(comment.getLikesCount())
.isAdopted(Boolean.TRUE.equals(comment.getIsAdopted()))
.createdAt(comment.getCreatedAt())
String userName = "";

// 우선순위: 삭제>탈퇴>익명>실명
if (comment.isDeleted()) {
// 삭제된 댓글의 경우 (삭제) 처리
userName = "삭제";
} else if (commentOwner.getUserStatus() == UserStatus.DELETED) {
// 탈퇴 유저
userName = "알 수 없음";
} else if (maskingCondition) {
// 마스킹 처리
userName = "익명" + comment.getAnonymousNumber();
} else if (isMyVerificationPost) {
// 인증 게시글 작성자
userName = commentOwner.getNickname() + "(글쓴이)";
} else {
// 작성자 닉네임 그대로
userName = commentOwner.getNickname();
}

// userId
Long userId = (maskingCondition)? null : commentOwner.getId(); // 마스킹 조건이면 null, 아니면 작성자의 userId

// 내용
String content = comment.isDeleted() ? "삭제된 댓글입니다." : comment.getContent();


return CommentResponseDto.builder()
.commentId(commentId)
.parentId(parentId)
.verificationId(verificationId)
// 작성자 ID
.userId(userId)
// 닉네임 반환
.userName(userName)
// 프로필 이미지 URL
.userProfileUrl(userProfileUrl)
.isMyComment(isMyComment)
.isAnonymous(isAnonymous)
.depth(depth)
.content(content)
.likesCount(likesCount)
.isAdopted(isAdopted)
.createdAt(createdAt)
.updatedAt(updatedAt)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public class CommentResponseDto {
private String content;
private int likesCount;

private boolean isMyComment; // 조회하는 사람이 쓴 댓글인지(프론트 분기 처리용)

private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
11 changes: 8 additions & 3 deletions src/main/java/com/hrr/backend/domain/comment/entity/Comment.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ public class Comment extends BaseEntity {
@Column(nullable = false)
private boolean isAnonymous;

/** 익명 번호 */
@Column
private Integer anonymousNumber; // 익명1, 익명2 등에 사용

/** 좋아요 수 (develop 추가) */
@Column(nullable = false)
private int likesCount;
Expand All @@ -67,9 +71,8 @@ public class Comment extends BaseEntity {

/** 채택 여부 (feat/90 추가) */
@Column(nullable = false)
@ColumnDefault("false")
@Builder.Default
private Boolean isAdopted = false;
private boolean isAdopted = false;

// === 정적 팩토리 메서드 (develop) ===
public static Comment create(
Expand All @@ -78,6 +81,7 @@ public static Comment create(
Comment parent,
String content,
boolean isAnonymous,
Integer anonymousNumber,
int depth
) {
return Comment.builder()
Expand All @@ -86,6 +90,7 @@ public static Comment create(
.parent(parent)
.content(content)
.isAnonymous(isAnonymous)
.anonymousNumber(anonymousNumber)
.depth(depth)
.likesCount(0)
.isDeleted(false)
Expand All @@ -111,4 +116,4 @@ public void adopt() {
public void unAdopt() {
this.isAdopted = false;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.hrr.backend.domain.comment.repository;

import com.hrr.backend.domain.comment.entity.Comment;
import com.hrr.backend.domain.user.entity.User;
import com.hrr.backend.domain.verification.entity.Verification;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -39,4 +40,15 @@ Page<Comment> findByVerificationAndDepthAndIsDeletedFalseOrderByCreatedAtAsc(
List<Comment> findByParentInAndIsDeletedFalseOrderByCreatedAtAsc(List<Comment> parents);

Optional<Comment> findByIdAndIsDeletedFalse(Long id);

/**
* 특정 인증글에 특정 유저가 익명으로 남긴 댓글이 있는지 조회
*/
Optional<Comment> findFirstByVerificationAndUserAndIsAnonymousTrue(Verification verification, User user);

/**
* 특정 인증글에 달린 익명 댓글 중 최대 익명 번호 조회 (익명1, 익명2, 익명3 있으면 3 반환)
*/
@Query("SELECT MAX(c.anonymousNumber) FROM Comment c WHERE c.verification = :verification")
Integer findMaxAnonymousNumberByVerification(Verification verification);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public interface CommentService {
CommentResponseDto createComment(Long verificationId, Long userId, CommentCreateRequestDto requestDto);

/** 댓글 조회 */
CommentListResponseDto getComments(Long verificationId, Pageable pageable);
CommentListResponseDto getComments(Long verificationId, Long userId, Pageable pageable);

/** 댓글 수정 */
CommentResponseDto updateComment(Long commentId, Long userId, CommentUpdateRequestDto requestDto);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class CommentServiceImpl implements CommentService {
private final VerificationRepository verificationRepository;
private final CommentRepository commentRepository;
private final UserRepository userRepository;
private final CommentConverter commentConverter;

/** 댓글 작성 */
@Override
Expand Down Expand Up @@ -60,23 +61,41 @@ public CommentResponseDto createComment(Long verificationId, Long userId, Commen
}
}

Integer anonymousNumber = null;

if (requestDto.isAnonymous()) {
// 해당 인증글에서 이 유저가 이미 쓴 익명 댓글이 있는지 확인
Optional<Comment> existingComment = commentRepository
.findFirstByVerificationAndUserAndIsAnonymousTrue(verification, user);

if (existingComment.isPresent()) {
// 있다면 해당 익명 번호 그대로 사용
anonymousNumber = existingComment.get().getAnonymousNumber();
} else {
// 없다면 해당 인증글의 최대 익명 번호 조회 후 +1
Integer maxNumber = commentRepository.findMaxAnonymousNumberByVerification(verification);
anonymousNumber = (maxNumber == null) ? 1 : maxNumber + 1;
}
}

Comment comment = Comment.create(
verification,
user,
parent,
requestDto.getContent(),
requestDto.isAnonymous(),
anonymousNumber,
parent == null ? 0 : parent.getDepth() + 1
);

commentRepository.save(comment);

return CommentConverter.toDto(comment);
return commentConverter.toDto(comment, userId);
}

/** 댓글/대댓글 조회 */
@Override
public CommentListResponseDto getComments(Long verificationId, Pageable pageable) {
public CommentListResponseDto getComments(Long verificationId, Long userId, Pageable pageable) {

Verification verification = verificationRepository.findById(verificationId)
.orElseThrow(() -> new GlobalException(ErrorCode.VERIFICATION_NOT_FOUND));
Expand Down Expand Up @@ -105,7 +124,7 @@ public CommentListResponseDto getComments(Long verificationId, Pageable pageable
adoptedChildren = commentRepository
.findByParentAndIsDeletedFalseOrderByCreatedAtAsc(adoptedParent)
.stream()
.map(CommentConverter::toDto)
.map(child -> commentConverter.toDto(child, userId))
.toList();


Expand Down Expand Up @@ -133,18 +152,18 @@ public CommentListResponseDto getComments(Long verificationId, Pageable pageable

for (Comment parent : parents) {
// 부모 댓글 추가
result.add(CommentConverter.toDto(parent));
result.add(commentConverter.toDto(parent, userId));

// 부모에 해당하는 자식(대댓글)들 추가
List<Comment> childList = childrenMap.getOrDefault(parent.getId(), Collections.emptyList());
for (Comment child : childList) {
result.add(CommentConverter.toDto(child));
result.add(commentConverter.toDto(child, userId));
}
}
}

return CommentListResponseDto.builder()
.adoptedParent(adoptedParent != null ? CommentConverter.toDto(adoptedParent) : null)
.adoptedParent(adoptedParent != null ? commentConverter.toDto(adoptedParent, userId) : null)
.adoptedChildren(adoptedChildren)
.comments(result)
.currentPage(parentPage.getNumber() + 1)
Expand Down Expand Up @@ -175,7 +194,10 @@ public CommentResponseDto updateComment(Long commentId, Long userId, CommentUpda

comment.updateContent(requestDto.getContent());

return CommentConverter.toDto(comment);
// 수동으로 DB 에 반영하여 Auditing 필드(updatedAt)를 갱신
commentRepository.saveAndFlush(comment);

return commentConverter.toDto(comment, userId);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public VerificationDetailResponseDto toDetailDto(
.userId(user.getId())
.nickname(user.getNickname())
.profileImageUrl(user.getProfileImage())
.role(userChallenge.getRole())
.level(userChallenge.getUser().getUserLevel())
.build();

VerificationDetailResponseDto.RoundInfo roundInfo =
Expand Down Expand Up @@ -169,4 +169,4 @@ public VerificationDetailResponseDto toDetailDto(
}


}
}
Loading