댓글 시스템 (Comment System) - fitpassTeam/fitpass GitHub Wiki
FitPass의 댓글 시스템은 게시물에 대한 실시간 댓글 작성과 대댓글(답글) 기능을 제공하며, 계층형 구조로 효율적인 댓글 관리를 지원합니다.
- 계층형 댓글: 부모 댓글과 자식 댓글(대댓글) 구조로 체계적 관리
- 실시간 작성: 게시물별 댓글 즉시 작성 및 조회
- 권한 관리: 댓글 작성자 및 게시글 작성자 권한 기반 수정/삭제
- 유연한 구조: 1000자 제한 내에서 자유로운 댓글 작성
- 연관 관계: 게시물과 사용자 간의 안전한 연관 관계 관리
필드 | 타입 | 제약조건 | 설명 |
---|---|---|---|
id | Long | PK, AUTO_INCREMENT | 댓글 고유 ID |
content | String | NOT NULL, MAX 1000자 | 댓글 내용 |
post_id | Long | FK, NOT NULL | 게시물 ID |
user_id | Long | FK, NOT NULL | 작성자 ID |
parent_id | Long | FK, NULLABLE | 부모 댓글 ID (대댓글인 경우) |
사용자 댓글 작성 → 게시물 연관 → 댓글 저장
↓
부모 댓글 설정 (대댓글인 경우)
↓
계층형 구조로 조회 및 표시
↓
작성자/게시글 작성자만 수정/삭제 가능
사용자 요청 → 게시물 ID로 부모 댓글 조회 → 자식 댓글 매핑 → 계층형 응답 반환
private List<Comment> getParentComments(Long postId) {
return commentRepository.findByPostIdAndParentIsNull(postId);
}
- 해당 게시물의 부모 댓글(parent_id가 null)만 조회
- 자식 댓글들은 엔티티 매핑을 통해 자동 로딩
- 최종 계층형 구조로 응답 반환
엔티티 조회 → 부모 댓글 확인 → 댓글 생성 → 저장
@Transactional
public void createComment(Long postId, Long userId, String content, Long parentId) {
// 1. 필수 엔티티 조회 및 검증
Post post = postRepository.findByIdOrElseThrow(postId);
User user = userRepository.findByIdOrElseThrow(userId);
// 2. 부모 댓글 확인 (대댓글인 경우)
Comment parent = null;
if (parentId != null) {
parent = commentRepository.findByIdOrElseThrow(parentId);
}
// 3. 댓글 생성 및 저장
Comment comment = Comment.of(post, user, content, parent);
commentRepository.save(comment);
}
- 게시물 존재: 존재하는 게시물에만 댓글 작성 가능
- 사용자 인증: 로그인한 사용자만 댓글 작성 가능
- 부모 댓글: 대댓글인 경우 부모 댓글 존재 확인
- 내용 검증: 1000자 이내의 유효한 내용 필수
- 작성자 확인: 댓글 작성자 본인만 수정 가능
- 게시물 존재: 댓글이 속한 게시물 존재 확인
- 댓글 존재: 수정할 댓글 존재 확인
@Transactional
public void updateComment(Long commentId, String content, Long userId, Long postId) {
Post post = postRepository.findByIdOrElseThrow(postId);
Comment comment = commentRepository.findByIdOrElseThrow(commentId);
User user = userRepository.findByIdOrElseThrow(userId);
// 권한 검증: 작성자 본인만 수정 가능
if(!comment.getUser().equals(user)){
throw new BaseException(ExceptionCode.NOT_GYM_OWNER);
}
comment.update(content);
}
- 댓글 작성자: 본인이 작성한 댓글 삭제 가능
- 게시글 작성자: 본인 게시글의 모든 댓글 삭제 가능
- 계층형 삭제: 부모 댓글 삭제 시 자식 댓글들 함께 삭제 (CASCADE)
- 즉시 삭제: 별도의 상태 변경 없이 즉시 물리적 삭제
public void deleteComment(Long commentId, Long userId, Long postId) {
Post post = postRepository.findByIdOrElseThrow(postId);
Comment comment = commentRepository.findByIdOrElseThrow(commentId);
User user = userRepository.findByIdOrElseThrow(userId);
// 권한 검증: 댓글 작성자 또는 게시글 작성자
if (!comment.getUser().getId().equals(userId) &&
!comment.getPost().getUser().getId().equals(userId)) {
throw new BaseException(ExceptionCode.NOT_HAS_AUTHORITY);
}
commentRepository.delete(comment);
}
Method | Endpoint | 설명 | 권한 |
---|---|---|---|
POST | /posts/{postId}/comments | 댓글 작성 | USER |
GET | /posts/{postId}/comments | 댓글 목록 조회 | ALL |
PATCH | /posts/{postId}/comments/{commentId} | 댓글 수정 | USER (작성자) |
DELETE | /posts/{postId}/comments/{commentId} | 댓글 삭제 | USER (작성자, 게시글 작성자) |
{
"content": "좋은 글 감사합니다!",
"parentId": null
}
{
"content": "저도 동감입니다.",
"parentId": 123
}
{
"statusCode": 200,
"message": "댓글 조회가 성공적으로 완료되었습니다.",
"data": [
{
"id": 101,
"content": "좋은 글 감사합니다!",
"name": "홍길동",
"writerId": 12,
"postOwnerId": 10,
"children": [
{
"id": 102,
"content": "저도 동감입니다.",
"name": "김철수",
"writerId": 15,
"postOwnerId": 10,
"children": []
}
]
}
]
}
- 작성자 확인: 모든 CUD 작업에서 댓글 소유자 검증
- 게시글 작성자: 삭제 시 게시글 작성자 추가 권한 부여
- Cross-cutting 보안: 다른 사용자 댓글 접근 차단
- 외래키 제약: post_id, user_id, parent_id 참조 무결성
- 계층 구조: 부모-자식 관계의 일관성 보장
- 내용 검증: 1000자 이내 댓글 내용 제한
- 엔티티 부재: 게시물, 사용자, 댓글 미존재 시 예외 발생
- 권한 부족: 수정/삭제 권한 없을 시 예외 발생
- 잘못된 요청: 유효하지 않은 데이터 요청 시 예외 발생
User (1) ─── (N) Comment (N) ─── (1) Post
│
└── parent_id (self-reference)
- 복합 인덱스: (post_id, parent_id) - 부모 댓글 조회 최적화
- 단일 인덱스: user_id - 사용자별 댓글 조회 최적화
- 계층 쿼리: parent IS NULL 조건 최적화