Skip to content

Commit

Permalink
Merge pull request #39 from IT-Cotato/feature/37-comments
Browse files Browse the repository at this point in the history
[FEATURE]  댓글 조회/생성/삭제 기능 구현
  • Loading branch information
goalSetter09 authored Jan 26, 2025
2 parents 459ef26 + ce3ce96 commit d5a8ffe
Show file tree
Hide file tree
Showing 29 changed files with 847 additions and 13 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.cotato.kampus.domain.comment.api;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.cotato.kampus.domain.comment.application.CommentService;
import com.cotato.kampus.domain.comment.dto.request.CommentCreateRequest;
import com.cotato.kampus.domain.comment.dto.response.CommentCreateResponse;
import com.cotato.kampus.domain.comment.dto.response.CommentDeleteResponse;
import com.cotato.kampus.domain.comment.dto.response.CommentLikeResponse;
import com.cotato.kampus.domain.comment.dto.response.CommentListResponse;
import com.cotato.kampus.domain.comment.dto.response.MyCommentResponse;
import com.cotato.kampus.global.common.dto.DataResponse;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;

@Tag(name = "댓글(Comment) API", description = "댓글 관련 API")
@RestController
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
@RequestMapping("/v1/api")
public class CommentController {

private final CommentService commentService;

@PostMapping("/posts/{postId}/comments")
@Operation(summary = "댓글 작성", description = "특정 게시글에 댓글을 추가합니다. 대댓글일 경우 parentId에 원래 댓글의 id를 넣어주세요. (기본값 = null)")
public ResponseEntity<DataResponse<CommentCreateResponse>> createComment(
@PathVariable Long postId,
@RequestBody CommentCreateRequest request){

return ResponseEntity.ok(DataResponse.from(
CommentCreateResponse.of(
commentService.createComment(
postId,
request.content(),
request.anonymity(),
request.parentId()
)
)
)
);
}

@DeleteMapping("/comments/{commentId}")
@Operation(summary = "댓글 삭제", description = "특정 댓글을 삭제합니다.")
public ResponseEntity<DataResponse<CommentDeleteResponse>> deleteComment(
@PathVariable Long commentId
){

return ResponseEntity.ok(DataResponse.from(
CommentDeleteResponse.of(
commentService.deleteComment(
commentId
)
)
)
);
}

@PostMapping("/comments/{commentId}/like")
@Operation(summary = "댓글 좋아요 추가", description = "특정 댓글에 좋아요를 추가합니다.")
public ResponseEntity<DataResponse<CommentLikeResponse>> toggleLikeForComment(
@PathVariable Long commentId
){

return ResponseEntity.ok(DataResponse.from(
CommentLikeResponse.of(
commentService.likeComment(
commentId
)
)
)
);
}

@GetMapping("/posts/{postId}/comments")
@Operation(summary ="댓글 조회", description = "특정 게시글의 모든 댓글과 대댓글을 조회합니다.")
public ResponseEntity<DataResponse<CommentListResponse>> getPostComments(
@PathVariable Long postId
){

return ResponseEntity.ok(DataResponse.from(
CommentListResponse.from(
commentService.findAllCommentsForPost(
postId
)
)
)
);
}

@GetMapping("/comments/my")
@Operation(summary = "내가 쓴 댓글 조회", description = "현재 사용자가 작성한 댓글을 최신순으로 조회합니다.")
public ResponseEntity<DataResponse<MyCommentResponse>> getMyComments(
@RequestParam(required = false, defaultValue = "0") int page
){
return ResponseEntity.ok(DataResponse.from(
MyCommentResponse.from(
commentService.findUserComments(page)
)
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.cotato.kampus.domain.comment.application;

import java.util.Optional;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.cotato.kampus.domain.comment.dao.CommentRepository;
import com.cotato.kampus.domain.comment.domain.Comment;
import com.cotato.kampus.domain.comment.dto.CommentDto;
import com.cotato.kampus.domain.common.application.ApiUserResolver;
import com.cotato.kampus.domain.common.enums.Anonymity;
import com.cotato.kampus.domain.post.application.PostUpdater;
import com.cotato.kampus.domain.user.application.UserFinder;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;

@Component
@Transactional(readOnly = true)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class AuthorResolver {

private final CommentRepository commentRepository;
private final ApiUserResolver apiUserResolver;
private final PostUpdater postUpdater;
private final UserFinder userFinder;

public Optional<Long> allocateAnonymousNumber(Long postId, Anonymity anonymity){

// 익명인 경우
if(anonymity == Anonymity.ANONYMOUS){
// 해당 Post에 현재 User의 댓글 작성 여부 확인
Optional<Comment> comment = commentRepository.findFirstByPostIdAndUserIdAndAnonymity(
postId, apiUserResolver.getUserId(), anonymity
);

return comment.map(Comment::getAnonymousNumber)
.or(() -> Optional.ofNullable(postUpdater.increaseNextAnonymousNumber(postId)));
} else {
return Optional.empty();
}
}

public String resolveAuthorName(CommentDto commentDto){

if(commentDto.anonymity() == Anonymity.ANONYMOUS){
return "Anonymous" + commentDto.anonymousNumber();
}

String nickname = userFinder.findById(commentDto.userId()).getNickname();

return nickname;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.cotato.kampus.domain.comment.application;

import java.util.Optional;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.cotato.kampus.domain.comment.dao.CommentRepository;
import com.cotato.kampus.domain.comment.domain.Comment;
import com.cotato.kampus.domain.comment.enums.CommentStatus;
import com.cotato.kampus.domain.comment.enums.ReportStatus;
import com.cotato.kampus.domain.common.application.ApiUserResolver;
import com.cotato.kampus.domain.common.enums.Anonymity;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;

@Component
@Transactional(readOnly = true)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class CommentAppender {

private final CommentRepository commentRepository;
private final ApiUserResolver apiUserResolver;

@Transactional
public Long append(Long postId, String content, Anonymity anonymity, Optional<Long> anonymousNumber, Long parentId){

Long userId = apiUserResolver.getUserId();

Long anonymousNumberValue = anonymousNumber.orElse(null);

Comment comment = Comment.builder()
.userId(userId)
.postId(postId)
.content(content)
.likes(0L)
.reportStatus(ReportStatus.NORMAL)
.commentStatus(CommentStatus.NORMAL)
.anonymity(anonymity)
.reports(0L)
.anonymousNumber(anonymousNumberValue)
.parentId(parentId)
.build();

return commentRepository.save(comment).getId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.cotato.kampus.domain.comment.application;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.cotato.kampus.domain.comment.dao.CommentRepository;
import com.cotato.kampus.domain.comment.domain.Comment;
import com.cotato.kampus.domain.comment.enums.CommentStatus;
import com.cotato.kampus.domain.common.application.ApiUserResolver;
import com.cotato.kampus.domain.user.domain.User;
import com.cotato.kampus.global.error.ErrorCode;
import com.cotato.kampus.global.error.exception.AppException;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;

@Component
@Transactional(readOnly = true)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class CommentDeleter {

private final CommentRepository commentRepository;
private final ApiUserResolver apiUserResolver;
private final CommentFinder commentFinder;

public Long delete(Long commentId) {

User user = apiUserResolver.getUser();
Comment comment = commentFinder.findComment(commentId);

// 작성자 검증
if(comment.getUserId() != user.getId()){
throw new AppException(ErrorCode.COMMENT_NOT_AUTHOR);
}

// 대댓글이 있으면 삭제된 상태로 업데이트
if(commentRepository.existsByParentId(commentId)){
comment.setCommentStatus(CommentStatus.DELETED_BY_USER);
return commentRepository.save(comment).getId();
}

// 댓글 삭제
commentRepository.delete(comment);
return commentId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.cotato.kampus.domain.comment.application;

import java.util.List;

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.cotato.kampus.domain.comment.dao.CommentRepository;
import com.cotato.kampus.domain.comment.domain.Comment;
import com.cotato.kampus.domain.comment.dto.CommentDto;
import com.cotato.kampus.domain.comment.dto.CommentSummary;
import com.cotato.kampus.domain.common.application.ApiUserResolver;
import com.cotato.kampus.global.error.ErrorCode;
import com.cotato.kampus.global.error.exception.AppException;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;

@Component
@Transactional(readOnly = true)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class CommentFinder {

private final CommentRepository commentRepository;
private final ApiUserResolver apiUserResolver;

public Comment findComment(Long commentId){
return commentRepository.findById(commentId)
.orElseThrow(() -> new AppException(ErrorCode.COMMENT_NOT_FOUND));
}

public List<CommentDto> findComments(Long postId){
List<Comment> comments = commentRepository.findAllByPostIdOrderByCreatedTimeAsc(postId);
List<CommentDto> commentDtos = comments.stream()
.map(CommentDto::from)
.toList();

return commentDtos;
}

public Slice<CommentSummary> findUserComments(int page){

Long userId = apiUserResolver.getUserId();

PageRequest pageRequest = PageRequest.of(page, 10, Sort.by(Sort.Direction.DESC, "createdTime"));
Slice<Comment> comments = commentRepository.findAllByUserId(userId, pageRequest);

return comments.map(CommentSummary::from);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.cotato.kampus.domain.comment.application;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.cotato.kampus.domain.comment.dao.CommentLikeRepository;
import com.cotato.kampus.domain.comment.domain.Comment;
import com.cotato.kampus.domain.comment.domain.CommentLike;
import com.cotato.kampus.domain.common.application.ApiUserResolver;
import com.cotato.kampus.global.error.ErrorCode;
import com.cotato.kampus.global.error.exception.AppException;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;

@Component
@Transactional(readOnly = true)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class CommentLikeAppender {

private final ApiUserResolver apiUserResolver;
private final CommentLikeRepository commentLikeRepository;
private final CommentFinder commentFinder;

public Long append(Long commentId){

Long userId = apiUserResolver.getUserId();

boolean alreadyLiked = commentLikeRepository.existsByUserIdAndCommentId(userId, commentId);

// 이미 좋아요한 경우 예외처리
if(alreadyLiked){
throw new AppException(ErrorCode.ALREADY_LIKED);
}

// 좋아요 추가
CommentLike commentLike = CommentLike.builder()
.commentId(commentId)
.userId(userId)
.build();

Comment comment = commentFinder.findComment(commentId);
comment.increaseLikes();

return commentLikeRepository.save(commentLike).getId();

}
}
Loading

0 comments on commit d5a8ffe

Please sign in to comment.