1주차 2주차 미션이 끝난 후 2일 휴강과 주말을 푹 쉬었다...
새로운 마음으로 새 기능을 구현해 보았다.
오늘 구현한 기능은 댓글 기능이다.
- 댓글 목록은 모든 유저가 조회 가능
- 댓글 작성은 로그인 한 유저만 가능
- 댓글 수정/삭제는 댓글 작성 유저만 가능 (ADMIN유저는 모든 댓글에 대하여 수정/삭제 가능)
1. Configuration
SwaggerConfig
@Configuration
@RequiredArgsConstructor
public class SwaggerConfig {
private final TypeResolver typeResolver;
@Bean
public Docket api() {
return new Docket(DocumentationType.OAS_30)
//swagger pageable 설정
.alternateTypeRules(AlternateTypeRules
.newRule(typeResolver.resolve(Pageable.class), typeResolver.resolve(Page.class)))
.securityContexts(Arrays.asList(securityContext()))
.securitySchemes(Arrays.asList(apiKey()))
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build();
}
@Getter
@Setter
@ApiModel
static class Page {
@ApiModelProperty(value = "페이지 번호")
private Integer page;
}
...
}
댓글 조회를 swagger로 테스트 하던 도중 pageable이 Swagger에서 잘 작동하지 않은 것을 보았다.
pageable에는 ?page=0 과 같은 형태로 URL에 접근하였는데, swagger pageNumber와 같은 형태로 넘어가기 때문으로 보였다.
이를 해결하기 위해 아래 Reference를 참고하여 SwaggerConfiguration에 설정을 하였다.
Pageable Class를 아래 정의한 Page Class로 대체하도록 설정하였다.
<Reference>
https://wellbell.tistory.com/19
2. Domain
Comment (Entity)
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Table(name = "comment")
@Entity
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
//soft delete
@SQLDelete(sql = "UPDATE comment SET deleted_at = current_timestamp WHERE id = ?")
@Where(clause = "deleted_at is null")
public class Comment extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String comment;
private LocalDateTime deletedAt;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "post_id")
private Post post;
public CommentDto toResponse() {
return CommentDto.builder()
.id(this.getId())
.comment(this.getComment())
.userName(this.getUser().getUserName())
.postId(this.getPost().getId())
.createdAt(this.getCreatedAt())
.build();
}
public CommentModifyResponse toModifiedResponse() {
return CommentModifyResponse.builder()
.id(this.getId())
.comment(this.getComment())
.userName(this.getUser().getUserName())
.postId(this.getPost().getId())
.createdAt(this.getCreatedAt())
.lastModifiedAt(this.getLastModifiedAt())
.build();
}
public void modify(String comment) {
this.comment = comment;
}
}
기존 User와 Post Entity처럼 생성시간과 수정시간을 갖고있는 BaseEntity를 extends해주었다.
다만 나중에 댓글이 있는 post삭제 시, 발생한 error를 대비하여 Post와 Comment Entity에 softe delete를 적용해 주었다.
이는 직접 DB에 있는 데이터를 삭제하는 것이 아니라 (delete query),
deletedAt을 업데이트하거나, delete를 true로 업데이트 하는 등 (update query) 논리적으로 삭제 처리를 한다.
또한 @SqlDelete annotation을 추가하여, JpaRepository에서 delete를 수행하면, 해당 query가 대신 수행되고,
조회시에는 @Where annotation에서 설정한 조건을 자동으로 추가해주도록 설정해 주었다.
그리고, user_id와 post_id를 Foreign key로 설정해주었다.
<Reference>
https://velog.io/@max9106/JPA-soft-delete
CommentRequest (dto)
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class CommentRequest {
private String comment;
}
클라이언트가 댓글을 작성하거나 수정할 때 사용되는 Request dto 이다.
CommentDto (dto)
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class CommentDto {
private int id;
private String comment;
private String userName;
private int postId;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime createdAt;
}
댓글을 작성하거나 조회 할 때 service에서 controller로 필요 정보를 옮길 때 사용되는 dto겸 controller에서 return할 때 사용되는 response형태이다.
CommentModifyResponse (dto)
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class CommentModifyResponse {
private int id;
private String comment;
private String userName;
private int postId;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime createdAt;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime lastModifiedAt;
}
위의 CommentDto와 비슷하나, 댓글 수정 시 수정시간을 표시하기 위하여 lastModifiedAt을 추가하였다.
CommentDeleteResponse (dto)
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class CommentDeleteResponse {
private String message;
private int id;
}
댓글 삭제시 controller 에서 return할 때 사용되는 response이다.
3. Repository
CommentRepository
public interface CommentRepository extends JpaRepository<Comment, Integer> {
Page<Comment> findAllByPost(Post post, Pageable pageable);
}
해당 post의 댓글을 찾기 위한 method를 추가하였다.
4. Service
PostService
@Service
@RequiredArgsConstructor
@Slf4j
public class PostService {
private final PostRepository postRepository;
private final UserRepository userRepository;
private final CommentRepository commentRepository;
...
//댓글 기능
public CommentDto createComment(Integer postId, String userName, String comment) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new AppException(ErrorCode.POST_NOT_FOUND, ""));
User user = userRepository.findByUserName(userName)
.orElseThrow(() -> new AppException(ErrorCode.NOT_FOUNDED_USER_NAME, ""));
Comment commentEntity = Comment.builder()
.comment(comment)
.user(user)
.post(post)
.build();
commentRepository.save(commentEntity);
return commentEntity.toResponse();
}
public Page<CommentDto> getComment(Integer postId, Pageable pageable) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new AppException(ErrorCode.POST_NOT_FOUND, ""));
Page<Comment> commentPages = commentRepository.findAllByPost(post, pageable);
return new PageImpl<>(commentPages.stream()
.map(Comment::toResponse)
.collect(Collectors.toList()));
}
public void deleteComment(String userName, Integer postId, Integer commentId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new AppException(ErrorCode.POST_NOT_FOUND, ""));
Comment comment = commentRepository.findById(commentId)
.orElseThrow(() -> new AppException(ErrorCode.COMMENT_NOT_FOUND, ""));
User user = userRepository.findByUserName(userName)
.orElseThrow(() -> new AppException(ErrorCode.NOT_FOUNDED_USER_NAME, ""));
//userRole이 USER이고, 작성자와 삭제자 불일치시.
//ADMIN은 모두 삭제 가능.
if (user.getRole() == UserRole.USER && !Objects.equals(post.getUser().getUserName(), userName)) {
throw new AppException(ErrorCode.INVALID_PERMISSION, "");
}
//soft delete로 처리됨.
commentRepository.delete(comment);
}
public CommentModifyResponse modifyComment(String userName, Integer postId, Integer commentId, String comment) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new AppException(ErrorCode.POST_NOT_FOUND, ""));
Comment commentEntity = commentRepository.findById(commentId)
.orElseThrow(() -> new AppException(ErrorCode.COMMENT_NOT_FOUND, ""));
User user = userRepository.findByUserName(userName)
.orElseThrow(() -> new AppException(ErrorCode.NOT_FOUNDED_USER_NAME, ""));
//userRole이 USER이고, 작성자와 삭제자 불일치시.
//ADMIN은 모두 수정 가능.
if (user.getRole() == UserRole.USER && !Objects.equals(post.getUser().getUserName(), userName)) {
throw new AppException(ErrorCode.INVALID_PERMISSION, "");
}
commentEntity.modify(comment);
Comment savedComment = commentRepository.saveAndFlush(commentEntity);
return savedComment.toModifiedResponse();
}
}
댓글 작성, 수정, 삭제, 조회에 관한 비지니스 로직을 추가하였다.
exception을 throw하는 과정은 기존 post와 거의 동일하다.
5. Controller
PostController
@RestController
@RequestMapping("/api/v1/posts")
@RequiredArgsConstructor
public class PostController {
private final PostService postService;
...
//댓글 기능
@PostMapping("/{postId}/comments")
public Response<CommentDto> createComment(@PathVariable Integer postId, @RequestBody CommentRequest req, @ApiIgnore Authentication authentication) {
CommentDto commentDto = postService.createComment(postId, authentication.getName(), req.getComment());
return Response.success(commentDto);
}
@GetMapping("/{postId}/comments")
public Response<Page<CommentDto>> getComment(@PathVariable Integer postId,
@PageableDefault(size=10, sort="createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
Page<CommentDto> commentDtos = postService.getComment(postId, pageable);
return Response.success(commentDtos);
}
@DeleteMapping("/{postId}/comments/{commentId}")
public Response<CommentDeleteResponse> deleteComment(@PathVariable Integer postId, @PathVariable Integer commentId, @ApiIgnore Authentication authentication) {
postService.deleteComment(authentication.getName(), postId, commentId);
return Response.success(new CommentDeleteResponse("댓글 삭제 완료", commentId));
}
@PutMapping("/{postId}/comments/{commentId}")
public Response<CommentModifyResponse> modifyComment(@PathVariable Integer postId, @PathVariable Integer commentId, @RequestBody CommentRequest req, @ApiIgnore Authentication authentication) {
CommentModifyResponse commentModifyResponse = postService.modifyComment(authentication.getName(), postId, commentId, req.getComment());
return Response.success(commentModifyResponse);
}
}
댓글 작성, 수정, 삭제, 조회 관련 기능을 추가하였다. 기존 post작성, 수정, 삭제, 조회와 거의 비슷하다.
다만, 댓글의 page size는 10으로 설정하였다.
<전체 Reference>
https://blog.jiniworld.me/20
https://wellbell.tistory.com/19
https://abbo.tistory.com/312
https://velog.io/@max9106/JPA-soft-delete
'프로젝트 > 멋사 개인 프로젝트 (mutsa-SNS)' 카테고리의 다른 글
[18] mutsa-SNS-2 2일차 - (1) 좋아요 기능 구현 (soft delete 복구) (0) | 2023.01.04 |
---|---|
[17] mutsa-SNS-2 1일차 - (2) 댓글 기능 test code (0) | 2023.01.03 |
[15] 3주차/4주차 미션 개요 (0) | 2023.01.03 |
[14] mutsa-SNS 7일차 - 코드 리펙토링 (0) | 2022.12.28 |
[13] mutsa-SNS 6일차 - (2) Admin 권한 부여 기능 추가 (2) | 2022.12.27 |