Merge branch 'be/feat/156-comment' into 'be/develop'

Feat: Comment 기능 구현 - S11P21S002-156

See merge request s11-s-project/S11P21S002!55
This commit is contained in:
김용수 2024-09-09 16:32:33 +09:00
commit d55d39eba3
9 changed files with 335 additions and 16 deletions

View File

@ -0,0 +1,89 @@
package com.worlabel.domain.comment.contoller;
import com.worlabel.domain.comment.entity.dto.CommentRequest;
import com.worlabel.domain.comment.entity.dto.CommentResponse;
import com.worlabel.domain.comment.entity.dto.CommentResponses;
import com.worlabel.domain.comment.service.CommentService;
import com.worlabel.global.annotation.CurrentUser;
import com.worlabel.global.config.swagger.SwaggerApiError;
import com.worlabel.global.config.swagger.SwaggerApiSuccess;
import com.worlabel.global.exception.ErrorCode;
import com.worlabel.global.response.BaseResponse;
import com.worlabel.global.response.SuccessResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/projects/{project_id}/comments")
@RequiredArgsConstructor
@Tag(name = "댓글 관련 API")
public class CommentController {
private final CommentService commentService;
@GetMapping("/images/{image_id}")
@SwaggerApiSuccess(description = "댓글 목록을 성공적으로 조회합니다.")
@Operation(summary = "댓글 목록 조회", description = "댓글 목록을 조회합니다.")
@SwaggerApiError({ErrorCode.BAD_REQUEST, ErrorCode.NOT_AUTHOR, ErrorCode.SERVER_ERROR})
public BaseResponse<CommentResponses> getAllComments(
@CurrentUser final Integer memberId,
@PathVariable("project_id") final Integer projectId,
@PathVariable("image_id") final Long imageId) {
List<CommentResponse> comments = commentService.getAllComments(memberId, projectId, imageId);
return new SuccessResponse<>(CommentResponses.from(comments));
}
@GetMapping("/{comment_id}")
@SwaggerApiSuccess(description = "댓글을 성공적으로 조회합니다.")
@Operation(summary = "댓글 조회", description = "댓글을 조회합니다.")
@SwaggerApiError({ErrorCode.COMMENT_NOT_FOUND, ErrorCode.NOT_AUTHOR, ErrorCode.SERVER_ERROR})
public BaseResponse<CommentResponse> getCommentById(
@CurrentUser final Integer memberId,
@PathVariable("project_id") final Integer projectId,
@PathVariable("comment_id") final Integer commentId) {
CommentResponse comment = commentService.getCommentById(memberId, projectId, commentId);
return new SuccessResponse<>(comment);
}
@PostMapping("/images/{image_id}")
@SwaggerApiSuccess(description = "댓글을 성공적으로 생성합니다.")
@Operation(summary = "댓글 생성", description = "댓글을 생성합니다.")
@SwaggerApiError({ErrorCode.NOT_AUTHOR, ErrorCode.SERVER_ERROR})
public BaseResponse<CommentResponse> createComment(
@RequestBody final CommentRequest commentRequest,
@CurrentUser final Integer memberId,
@PathVariable("project_id") final Integer projectId,
@PathVariable("image_id") final Long imageId) {
CommentResponse comment = commentService.createComment(commentRequest, memberId, projectId, imageId);
return new SuccessResponse<>(comment);
}
@PutMapping("/{comment_id}")
@SwaggerApiSuccess(description = "댓글을 성공적으로 수정합니다.")
@Operation(summary = "댓글 수정", description = "댓글을 수정합니다.")
@SwaggerApiError({ErrorCode.NOT_AUTHOR, ErrorCode.SERVER_ERROR})
public BaseResponse<CommentResponse> updateComment(
@RequestBody final CommentRequest commentRequest,
@CurrentUser final Integer memberId,
@PathVariable("project_id") final Integer projectId,
@PathVariable("comment_id") final Integer commentId) {
CommentResponse comment = commentService.updateComment(commentRequest, memberId, projectId, commentId);
return new SuccessResponse<>(comment);
}
@DeleteMapping("/{comment_id}")
@SwaggerApiSuccess(description = "댓글을 성공적으로 생성합니다.")
@Operation(summary = "댓글 생성", description = "댓글을 생성합니다.")
@SwaggerApiError({ErrorCode.NOT_AUTHOR, ErrorCode.SERVER_ERROR})
public BaseResponse<Void> deleteComment(
@CurrentUser final Integer memberId,
@PathVariable("project_id") final Integer projectId,
@PathVariable("comment_id") final Integer commentId) {
commentService.deleteComment(memberId, projectId, commentId);
return new SuccessResponse<>();
}
}

View File

@ -1,6 +1,7 @@
package com.worlabel.domain.comment.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.worlabel.domain.image.entity.Image;
import com.worlabel.domain.member.entity.Member;
import com.worlabel.global.common.BaseEntity;
import jakarta.persistence.*;
@ -10,7 +11,7 @@ import lombok.NoArgsConstructor;
@Getter
@Entity
@Table(name= "comment")
@Table(name = "comment")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment extends BaseEntity {
@ -18,33 +19,58 @@ public class Comment extends BaseEntity {
* 코멘트 PK
*/
@Id
@Column(name = "comment_id",nullable = false)
@Column(name = "comment_id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
/**
* 코멘트 내용
*/
@Column(name = "content",nullable = false)
@Column(name = "content", nullable = false)
private String content;
/**
* 코멘트 위치(x)
*/
@Column(name = "positionX",nullable = true)
@Column(name = "positionX", nullable = true)
private double positionX;
/**
* 코멘트 위치(y)
*/
@Column(name = "positionY",nullable = true)
@Column(name = "positionY", nullable = true)
private double positionY;
/**
* 속한 사용자
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id",nullable = false)
@JoinColumn(name = "member_id", nullable = false)
@JsonIgnore
private Member member;
/**
* 속한 이미지
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "image_id", nullable = false)
private Image image;
public Comment(final String content, final double positionX, final double positionY, final Member member, final Image image) {
this.content = content;
this.positionX = positionX;
this.positionY = positionY;
this.member = member;
this.image = image;
}
public static Comment of(final String content, final double positionX, final double positionY, final Member member, final Image image) {
return new Comment(content, positionX, positionY, member, image);
}
public void update(final String content, final double positionX, final double positionY) {
this.content = content;
this.positionX = positionX;
this.positionY = positionY;
}
}

View File

@ -0,0 +1,28 @@
package com.worlabel.domain.comment.entity.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Schema(name = "댓글 요청 dto", description = "댓글 요청 DTO")
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
@Getter
public class CommentRequest {
@Schema(description = "댓글 내용", example = "여기 부분 더 상세하게 나타내야해요")
@NotEmpty(message = "내용을 입력하세요.")
private String content;
@Schema(description = "X 좌표", example = "3.1462")
@NotNull(message = "x좌표를 입력해주세요.")
private double positionX;
@Schema(description = "Y 좌표", example = "7.1462")
@NotNull(message = "y좌표를 입력해주세요.")
private double positionY;
}

View File

@ -0,0 +1,49 @@
package com.worlabel.domain.comment.entity.dto;
import com.worlabel.domain.comment.entity.Comment;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.time.LocalDateTime;
@Schema(name = "댓글 응답 DTO", description = "댓글 조회 응답 DTO")
@Getter
@AllArgsConstructor
public class CommentResponse {
@Schema(description = "댓글 ID", example = "1")
private Integer id;
@Schema(description = "작성자 ID", example = "1")
private Integer memberId;
@Schema(description = "작성자 닉네임", example = "javajoha")
private String memberNickname;
@Schema(description = "작성자 프로필", example = "profile.jpg")
private String memberProfileImage;
@Schema(description = "y좌표", example = "3.16324")
private double positionY;
@Schema(description = "x좌표", example = "7.16324")
private double positionX;
@Schema(description = "댓글 내용", example = "이 부분 더 자세하게 표현해주세요")
private String content;
@Schema(description = "작성 일자", example = "2024-09-09T14:47:45")
private LocalDateTime createTime;
public static CommentResponse from(final Comment comment) {
return new CommentResponse(comment.getId(),
comment.getMember().getId(),
comment.getMember().getNickname(),
comment.getMember().getProfileImage(),
comment.getPositionY(),
comment.getPositionX(),
comment.getContent(),
comment.getCreatedAt());
}
}

View File

@ -0,0 +1,21 @@
package com.worlabel.domain.comment.entity.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
@Schema(name = "댓글 목록 응답 dto", description = "댓글 목록 응답 DTO")
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class CommentResponses {
@Schema(description = "댓글 목록", example = "")
private List<CommentResponse> commentResponses;
public static CommentResponses from(final List<CommentResponse> commentResponses) {
return new CommentResponses(commentResponses);
}
}

View File

@ -0,0 +1,16 @@
package com.worlabel.domain.comment.repository;
import com.worlabel.domain.comment.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface CommentRepository extends JpaRepository<Comment, Integer> {
List<Comment> findByImageId(Long image_id);
Optional<Comment> findByIdAndMemberId(Integer commentId, Integer memberId);
}

View File

@ -0,0 +1,95 @@
package com.worlabel.domain.comment.service;
import com.worlabel.domain.comment.entity.Comment;
import com.worlabel.domain.comment.entity.dto.CommentRequest;
import com.worlabel.domain.comment.entity.dto.CommentResponse;
import com.worlabel.domain.comment.repository.CommentRepository;
import com.worlabel.domain.image.entity.Image;
import com.worlabel.domain.image.repository.ImageRepository;
import com.worlabel.domain.member.entity.Member;
import com.worlabel.domain.member.repository.MemberRepository;
import com.worlabel.domain.participant.repository.ParticipantRepository;
import com.worlabel.global.exception.CustomException;
import com.worlabel.global.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
@RequiredArgsConstructor
public class CommentService {
private final CommentRepository commentRepository;
private final ParticipantRepository participantRepository;
private final MemberRepository memberRepository;
private final ImageRepository imageRepository;
@Transactional(readOnly = true)
public List<CommentResponse> getAllComments(final Integer memberId, final Integer projectId, final Long imageId) {
checkAuthorized(memberId, projectId);
return commentRepository.findByImageId(imageId).stream()
.map(CommentResponse::from)
.toList();
}
@Transactional(readOnly = true)
public CommentResponse getCommentById(final Integer memberId, final Integer projectId, final Integer commentId) {
checkAuthorized(memberId, projectId);
Comment comment = getComment(commentId);
return CommentResponse.from(comment);
}
public CommentResponse createComment(final CommentRequest commentRequest, Integer memberId, final Integer projectId, final Long imageId) {
checkAuthorized(memberId, projectId);
Member member = getMember(memberId);
Image image = getImage(imageId);
Comment comment = Comment.of(commentRequest.getContent(), commentRequest.getPositionX(), commentRequest.getPositionY(), member, image);
comment = commentRepository.save(comment);
return CommentResponse.from(comment);
}
public CommentResponse updateComment(final CommentRequest commentRequest, final Integer memberId, final Integer projectId, final Integer commentId) {
Comment comment = getCommentWithMemberId(commentId, memberId);
comment.update(commentRequest.getContent(), commentRequest.getPositionX(), commentRequest.getPositionY());
return CommentResponse.from(comment);
}
public void deleteComment(final Integer memberId, final Integer projectId, final Integer commentId) {
Comment comment = getCommentWithMemberId(commentId, memberId);
commentRepository.delete(comment);
}
private void checkAuthorized(final Integer memberId, final Integer projectId) {
if (!participantRepository.existsByMemberIdAndProjectId(memberId, projectId)) {
throw new CustomException(ErrorCode.UNAUTHORIZED);
}
}
private Comment getComment(final Integer commentId) {
return commentRepository.findById(commentId)
.orElseThrow(() -> new CustomException(ErrorCode.COMMENT_NOT_FOUND));
}
private Comment getCommentWithMemberId(final Integer commentId, final Integer memberId) {
return commentRepository.findByIdAndMemberId(commentId, memberId)
.orElseThrow(() -> new CustomException(ErrorCode.COMMENT_NOT_FOUND));
}
private Member getMember(final Integer memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
}
private Image getImage(final Long imageId) {
return imageRepository.findById(imageId)
.orElseThrow(() -> new CustomException(ErrorCode.IMAGE_NOT_FOUND));
}
}

View File

@ -1,13 +1,8 @@
package com.worlabel.domain.image.controller;
import com.worlabel.domain.folder.entity.dto.FolderRequest;
import com.worlabel.domain.folder.entity.dto.FolderResponse;
import com.worlabel.domain.folder.service.FolderService;
import com.worlabel.domain.image.entity.dto.ImageMoveRequest;
import com.worlabel.domain.image.entity.dto.ImageResponse;
import com.worlabel.domain.image.service.ImageService;
import com.worlabel.domain.project.service.ProjectService;
import com.worlabel.domain.workspace.entity.dto.WorkspaceResponse;
import com.worlabel.global.annotation.CurrentUser;
import com.worlabel.global.config.swagger.SwaggerApiError;
import com.worlabel.global.config.swagger.SwaggerApiSuccess;
@ -24,7 +19,6 @@ import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@Slf4j
@RestController
@RequiredArgsConstructor
@ -95,5 +89,3 @@ public class ImageController {
return SuccessResponse.empty();
}
}

View File

@ -16,7 +16,7 @@ public enum ErrorCode {
INVALID_URL(HttpStatus.BAD_REQUEST, 1004, "제공하지 않는 주소입니다. 확인해주세요"),
FAIL_TO_CREATE_FILE(HttpStatus.BAD_REQUEST, 1005, "파일 업로드에 실패하였습니다. 다시 한번 확인해주세요"),
FAIL_TO_DELETE_FILE(HttpStatus.BAD_REQUEST, 1006, "파일 삭제에 실패하였습니다. 다시 한번 확인해주세요"),
INVALID_FILE_PATH(HttpStatus.BAD_REQUEST,1007 , "파일 경로가 잘못되었습니다. 다시 한번 확인해주세요"),
INVALID_FILE_PATH(HttpStatus.BAD_REQUEST, 1007, "파일 경로가 잘못되었습니다. 다시 한번 확인해주세요"),
// Auth & Member - 2000
USER_NOT_FOUND(HttpStatus.NOT_FOUND, 2000, "해당 ID의 사용자를 찾을 수 없습니다."),
@ -46,11 +46,14 @@ public enum ErrorCode {
FOLDER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 6001, "해당 폴더에 접근 권한이 없습니다."),
// Image - 7000
IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, 7000,"해당 이미지를 찾을 수 없습니다."),
IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, 7000, "해당 이미지를 찾을 수 없습니다."),
// AI - 8000
AI_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 8000, "AI 서버 오류 입니다."),
// Comment - 9000
COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, 9000, "해당 댓글을 찾을 수 없습니다."),
;
private final HttpStatus status;
private final int code;