Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 답변 댓글 CRUD, 좋아요 기능 구현 #132

Merged
merged 4 commits into from
Aug 24, 2024
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 @@ -51,6 +51,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/members/sign-in","/members/sign-up", "/members/local-sign-in", "/token/**", "/members/email/check", "/members/nickname/check", "/members/email/certification", "/members/email/certification/check").permitAll()
.requestMatchers("/admin/**", "/members/email/whitelist/register").hasRole(Role.ROLE_ADMIN.getName())
.requestMatchers("/answers","/answers/**").authenticated()
.requestMatchers("/answerComments","/answerComments/**").authenticated()
.requestMatchers("/members","/members/**").authenticated()
.requestMatchers("/tags","/tags/**").authenticated()
.requestMatchers("/questions","/questions/**").authenticated()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.server.capple.domain.answerComment.controller;

import com.server.capple.config.security.AuthMember;
import com.server.capple.domain.answerComment.dto.AnswerCommentRequest;
import com.server.capple.domain.answerComment.dto.AnswerCommentResponse.*;
import com.server.capple.domain.answerComment.service.AnswerCommentService;
import com.server.capple.domain.member.entity.Member;
import com.server.capple.global.common.BaseResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@Tag(name = "답변 댓글 API", description = "답변 댓글 API입니다.")
@RestController
@RequiredArgsConstructor
@RequestMapping("/answerComments")
public class AnswerCommentController {

private final AnswerCommentService answerCommentService;

@Operation(summary = "답변 댓글 생성 API", description = " 답변 댓글 생성 API 입니다. pathvariable로 answerId를 주세요.")
@PostMapping("/answer/{answerId}")
public BaseResponse<AnswerCommentId> createAnswerComment(@AuthMember Member member, @PathVariable(value = "answerId") Long answerId, @RequestBody @Valid AnswerCommentRequest request) {
return BaseResponse.onSuccess(answerCommentService.createAnswerComment(member, answerId, request));
}

@Operation(summary = "답변 댓글 삭제 API", description = " 답변 댓글 삭제 API 입니다. pathvariable 으로 commentId를 주세요.")
@DeleteMapping("/{commentId}")
public BaseResponse<AnswerCommentId> deleteAnswerComment(@AuthMember Member member, @PathVariable(value = "commentId") Long commentId) {
return BaseResponse.onSuccess(answerCommentService.deleteAnswerComment(member, commentId));
}

@Operation(summary = "답변 댓글 수정 API", description = " 답변 댓글 수정 API 입니다. pathvariable 으로 commentId를 주세요.")
@PatchMapping("/{commentId}")
public BaseResponse<AnswerCommentId> updateAnswerComment(@AuthMember Member member, @PathVariable(value = "commentId") Long commentId, @RequestBody @Valid AnswerCommentRequest request) {
return BaseResponse.onSuccess(answerCommentService.updateAnswerComment(member, commentId, request));
}

@Operation(summary = "답변 댓글 좋아요/취소 토글 API", description = " 답변 댓글 좋아요/취소 토글 API 입니다. pathvariable 으로 commentId를 주세요.")
@PatchMapping("/heart/{commentId}")
public BaseResponse<AnswerCommentHeart> heartAnswerComment(@AuthMember Member member, @PathVariable(value = "commentId") Long commentId) {
return BaseResponse.onSuccess(answerCommentService.heartAnswerComment(member, commentId));
}

@Operation(summary = "답변에 대한 댓글 조회 API", description = " 답변에 대한 댓글 조회 API 입니다. pathvariable 으로 answerId를 주세요.")
@GetMapping("/{answerId}")
public BaseResponse<AnswerCommentInfos> getAnswerCommentInfos(@PathVariable(value = "answerId") Long answerId) {
return BaseResponse.onSuccess(answerCommentService.getAnswerCommentInfos(answerId));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.server.capple.domain.answerComment.dto;

import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AnswerCommentRequest {

@NotEmpty
private String answerComment;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.server.capple.domain.answerComment.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;
import java.util.List;

public class AnswerCommentResponse {
@Getter
@AllArgsConstructor
public static class AnswerCommentId {
private Long answerCommentId;
}

@Getter
@AllArgsConstructor
public static class AnswerCommentHeart {
private Long answerCommentId;
private Boolean isLiked;
}

@Getter
@Builder
public static class AnswerCommentInfo {
private Long answerCommentId;
private String writer;
private String content;
private Long heartCount;
private LocalDateTime createdAt;

}

@Getter
@AllArgsConstructor
public static class AnswerCommentInfos {
private List<AnswerCommentInfo> answerCommentInfos;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.server.capple.domain.answerComment.entity;

import com.server.capple.domain.answer.entity.Answer;
import com.server.capple.domain.member.entity.Member;
import com.server.capple.global.common.BaseEntity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.SQLRestriction;

@Getter
@Builder
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@SQLRestriction("deleted_at is null")
@DynamicInsert
public class AnswerComment extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "answer_id", nullable = false)
private Answer answer;

@Column(nullable = false)
private String content;

public void update(String content) {
this.content = content;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.server.capple.domain.answerComment.mapper;

import com.server.capple.domain.answer.entity.Answer;
import com.server.capple.domain.answerComment.dto.AnswerCommentResponse.*;
import com.server.capple.domain.answerComment.entity.AnswerComment;
import com.server.capple.domain.member.entity.Member;
import org.springframework.stereotype.Component;


@Component
public class AnswerCommentMapper {
public AnswerComment toAnswerCommentEntity(Member member, Answer answer, String answerComment) {
return AnswerComment.builder()
.member(member)
.answer(answer)
.content(answerComment)
.build();
}

public AnswerCommentInfo toAnswerCommentInfo(AnswerComment comment, Long heartCount) {
return AnswerCommentInfo.builder()
.answerCommentId(comment.getId())
.writer(comment.getMember().getNickname())
.content(comment.getContent())
.heartCount(heartCount)
.createdAt(comment.getCreatedAt())
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.server.capple.domain.answerComment.repository;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.stereotype.Repository;

import java.io.Serializable;

import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;

@Repository
@RequiredArgsConstructor
public class AnswerCommentHeartRedisRepository implements Serializable {
public static final String ANSWER_COMMENT_HEART_KEY_PREFIX = "answerCommentHeart-";
public static final String MEMBER_KEY_PREFIX = "member-";

private final RedisTemplate<String, String> redisTemplate;

public Boolean toggleAnswerCommentHeart(Long commentId, Long memberId) {
String key = ANSWER_COMMENT_HEART_KEY_PREFIX + commentId.toString();
String member = MEMBER_KEY_PREFIX + memberId.toString();

SetOperations<String, String> setOperations = redisTemplate.opsForSet();

// 유저가 좋아요를 눌렀는지 확인
Boolean isLiked = setOperations.isMember(key, member);

// 좋아요 취소
if (FALSE.equals(isLiked)) {
setOperations.add(key, member);
return TRUE;
} else {
setOperations.remove(key, member);
return FALSE;
}
}

public Long getAnswerCommentHeartsCount(Long commentId) {
String key = ANSWER_COMMENT_HEART_KEY_PREFIX + commentId.toString();
return redisTemplate.opsForSet().size(key);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.server.capple.domain.answerComment.repository;

import com.server.capple.domain.answerComment.entity.AnswerComment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface AnswerCommentRepository extends JpaRepository<AnswerComment, Long> {
@Query("SELECT a FROM AnswerComment a WHERE a.answer.id = :answerId ORDER BY a.createdAt")
List<AnswerComment> findAnswerCommentByAnswerId(Long answerId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.server.capple.domain.answerComment.service;

import com.server.capple.domain.answerComment.dto.AnswerCommentRequest;
import com.server.capple.domain.answerComment.dto.AnswerCommentResponse.*;
import com.server.capple.domain.answerComment.entity.AnswerComment;
import com.server.capple.domain.member.entity.Member;

public interface AnswerCommentService {
AnswerComment findAnswerComment(Long answerCommentId);
AnswerCommentId createAnswerComment(Member member, Long answerId, AnswerCommentRequest request);
AnswerCommentId deleteAnswerComment(Member member, Long commentId);
AnswerCommentId updateAnswerComment(Member member, Long commentId, AnswerCommentRequest request);
AnswerCommentHeart heartAnswerComment(Member member, Long commentId);
AnswerCommentInfos getAnswerCommentInfos(Long answerId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.server.capple.domain.answerComment.service;

import com.server.capple.domain.answer.entity.Answer;
import com.server.capple.domain.answer.service.AnswerService;
import com.server.capple.domain.answerComment.dto.AnswerCommentRequest;
import com.server.capple.domain.answerComment.dto.AnswerCommentResponse.*;
import com.server.capple.domain.answerComment.entity.AnswerComment;
import com.server.capple.domain.answerComment.mapper.AnswerCommentMapper;
import com.server.capple.domain.answerComment.repository.AnswerCommentHeartRedisRepository;
import com.server.capple.domain.answerComment.repository.AnswerCommentRepository;
import com.server.capple.domain.member.entity.Member;
import com.server.capple.domain.member.service.MemberService;
import com.server.capple.global.exception.RestApiException;
import com.server.capple.global.exception.errorCode.CommentErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;


@Service
@RequiredArgsConstructor
public class AnswerCommentServiceImpl implements AnswerCommentService{

private final AnswerCommentRepository answerCommentRepository;
private final AnswerCommentHeartRedisRepository answerCommentHeartRedisRepository;
private final AnswerCommentMapper answerCommentMapper;
private final MemberService memberService;
private final AnswerService answerService;

/* 댓글 작성 */
@Override
@Transactional
public AnswerCommentId createAnswerComment(Member member, Long answerId, AnswerCommentRequest request) {
Member loginMember = memberService.findMember(member.getId());
Answer answer = answerService.findAnswer(answerId);
AnswerComment answerComment = answerCommentRepository.save(answerCommentMapper.toAnswerCommentEntity(loginMember, answer, request.getAnswerComment()));
return new AnswerCommentId(answerComment.getId());
}

/* 댓글 삭제 */
@Override
@Transactional
public AnswerCommentId deleteAnswerComment(Member member, Long commentId) {
AnswerComment answerComment = findAnswerComment(commentId);
checkPermission(member, answerComment); // 유저 권한 체크

answerComment.delete();
return new AnswerCommentId(answerComment.getId());
}

/* 댓글 수정 */
@Override
@Transactional
public AnswerCommentId updateAnswerComment(Member member, Long commentId, AnswerCommentRequest request) {
AnswerComment answerComment = findAnswerComment(commentId);
checkPermission(member, answerComment); // 유저 권한 체크

answerComment.update(request.getAnswerComment());
return new AnswerCommentId(commentId);
}

/* 댓글 좋아요/취소 */
@Override
@Transactional
public AnswerCommentHeart heartAnswerComment(Member member, Long commentId) {
Boolean isLiked = answerCommentHeartRedisRepository.toggleAnswerCommentHeart(commentId, member.getId());
return new AnswerCommentHeart(commentId, isLiked);
}

/* 답변에 대한 댓글 조회 */
@Override
public AnswerCommentInfos getAnswerCommentInfos(Long answerId) {
List<AnswerCommentInfo> commentInfos = answerCommentRepository.findAnswerCommentByAnswerId(answerId).stream()
.map(comment -> {
Long heartCount = answerCommentHeartRedisRepository.getAnswerCommentHeartsCount(comment.getId());
return answerCommentMapper.toAnswerCommentInfo(comment, heartCount);
})
.toList();

return new AnswerCommentInfos(commentInfos);
}

private void checkPermission(Member member, AnswerComment answerComment) {
Member loginMember = memberService.findMember(member.getId());

if (!loginMember.getId().equals(answerComment.getMember().getId()))
throw new RestApiException(CommentErrorCode.COMMENT_NOT_FOUND);
}

@Override
public AnswerComment findAnswerComment(Long answerCommentId) {
return answerCommentRepository.findById(answerCommentId).orElseThrow(
() -> new RestApiException(CommentErrorCode.COMMENT_NOT_FOUND)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.server.capple.global.exception.errorCode;

import com.server.capple.global.exception.ErrorCode;
import com.server.capple.global.exception.ErrorCodeInterface;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum CommentErrorCode implements ErrorCodeInterface {
COMMENT_NOT_FOUND("COMMENT001", "댓글을 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
COMMENT_UNAUTHORIZED("COMMENT002", "댓글에 대한 권한이 없습니다.", HttpStatus.FORBIDDEN);

private final String code;
private final String message;
private final HttpStatus httpStatus;

@Override
public ErrorCode getErrorCode() {
return ErrorCode.builder()
.code(code)
.message(message)
.httpStatus(httpStatus)
.build();
}
}
Loading
Loading