diff --git a/src/main/java/clap/server/adapter/inbound/web/comment/PostCommentController.java b/src/main/java/clap/server/adapter/inbound/web/comment/PostCommentController.java new file mode 100644 index 00000000..65656da1 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/comment/PostCommentController.java @@ -0,0 +1,51 @@ +package clap.server.adapter.inbound.web.comment; + +import clap.server.adapter.inbound.security.SecurityUserDetails; +import clap.server.adapter.inbound.web.dto.task.PostAndEditCommentRequest; +import clap.server.application.port.inbound.comment.PostCommentUsecase; +import clap.server.common.annotation.architecture.WebAdapter; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Tag(name = "02. Task", description = "작업 생성/수정 API") +@WebAdapter +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/comment") +public class PostCommentController { + + private final PostCommentUsecase postCommentUsecase; + + @Operation(summary = "댓글 작성") + @Parameter(name = "labelId", description = "댓글 작성할 작업 고유 ID", required = true, in = ParameterIn.PATH) + @PostMapping("/{taskId}") + @Secured({"ROLE_MANAGER", "ROLE_USER"}) + public void createTask( + @AuthenticationPrincipal SecurityUserDetails userInfo, + @PathVariable Long taskId, + @RequestBody(required = true) PostAndEditCommentRequest request){ + postCommentUsecase.save(userInfo.getUserId(), taskId, request); + } + + @Operation(summary = "댓글 작성(첨부 파일)") + @Parameter(name = "labelId", description = "댓글 작성할 작업 고유 ID", required = true, in = ParameterIn.PATH) + @PostMapping("/attachment/{taskId}") + @Secured({"ROLE_MANAGER", "ROLE_USER"}) + public void createAttachmentTask( + @AuthenticationPrincipal SecurityUserDetails userInfo, + @PathVariable Long taskId, + @RequestPart(name = "attachment") @NotNull List attachments) { + postCommentUsecase.saveCommentAttachment(userInfo.getUserId(), taskId, attachments); + } + +} diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/task/PostAndEditCommentRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/task/PostAndEditCommentRequest.java new file mode 100644 index 00000000..b66a6506 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/task/PostAndEditCommentRequest.java @@ -0,0 +1,10 @@ +package clap.server.adapter.inbound.web.dto.task; + + +import io.swagger.v3.oas.annotations.media.Schema; + +public record PostAndEditCommentRequest( + @Schema(description = "댓글 내용") + String content +) { +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/CommentPersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/CommentPersistenceAdapter.java index 48b65cf7..927e5d12 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/CommentPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/CommentPersistenceAdapter.java @@ -1,13 +1,31 @@ package clap.server.adapter.outbound.persistense; -import clap.server.application.port.outbound.taskhistory.LoadCommentPort; +import clap.server.adapter.outbound.persistense.entity.task.CommentEntity; +import clap.server.adapter.outbound.persistense.mapper.CommentPersistenceMapper; +import clap.server.adapter.outbound.persistense.repository.task.CommentRepository; +import clap.server.application.port.outbound.task.CommandCommentPort; +import clap.server.application.port.outbound.task.LoadCommentPort; +import clap.server.common.annotation.architecture.PersistenceAdapter; import clap.server.domain.model.task.Comment; +import lombok.RequiredArgsConstructor; import java.util.Optional; -public class CommentPersistenceAdapter implements LoadCommentPort { +@PersistenceAdapter +@RequiredArgsConstructor +public class CommentPersistenceAdapter implements LoadCommentPort, CommandCommentPort { + + private final CommentRepository commentRepository; + private final CommentPersistenceMapper commentPersistenceMapper; + @Override public Optional findById(Long id) { return Optional.empty(); } + + @Override + public Comment saveComment(Comment comment) { + CommentEntity commentEntity = commentRepository.save(commentPersistenceMapper.toEntity(comment)); + return commentPersistenceMapper.toDomain(commentEntity); + } } diff --git a/src/main/java/clap/server/application/mapper/AttachmentMapper.java b/src/main/java/clap/server/application/mapper/AttachmentMapper.java index 119221b6..217780eb 100644 --- a/src/main/java/clap/server/application/mapper/AttachmentMapper.java +++ b/src/main/java/clap/server/application/mapper/AttachmentMapper.java @@ -2,6 +2,7 @@ import clap.server.adapter.inbound.web.dto.task.AttachmentResponse; import clap.server.domain.model.task.Attachment; +import clap.server.domain.model.task.Comment; import clap.server.domain.model.task.Task; import org.springframework.web.multipart.MultipartFile; @@ -9,6 +10,7 @@ import java.util.stream.IntStream; import static clap.server.domain.model.task.Attachment.createAttachment; +import static clap.server.domain.model.task.Attachment.createCommentAttachment; public class AttachmentMapper { private AttachmentMapper() { @@ -26,6 +28,19 @@ public static List toTaskAttachments(Task task, List .toList(); } + public static List toCommentAttachments(Task task, Comment comment, List files, List fileUrls) { + return IntStream.range(0, files.size()) + .mapToObj(i -> createCommentAttachment( + task, + comment, + files.get(i).getOriginalFilename(), + fileUrls.get(i), + files.get(i).getSize() + )) + .toList(); + } + + public static List toAttachmentResponseList(List attachments) { return attachments.stream() .map(attachment -> new AttachmentResponse( diff --git a/src/main/java/clap/server/application/port/inbound/comment/PostCommentUsecase.java b/src/main/java/clap/server/application/port/inbound/comment/PostCommentUsecase.java new file mode 100644 index 00000000..e964316e --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/comment/PostCommentUsecase.java @@ -0,0 +1,13 @@ +package clap.server.application.port.inbound.comment; + +import clap.server.adapter.inbound.web.dto.task.PostAndEditCommentRequest; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +public interface PostCommentUsecase { + + void save(Long userId, Long taskId, PostAndEditCommentRequest request); + + void saveCommentAttachment(Long userId, Long taskId, List files); +} diff --git a/src/main/java/clap/server/application/port/outbound/task/CommandCommentPort.java b/src/main/java/clap/server/application/port/outbound/task/CommandCommentPort.java new file mode 100644 index 00000000..834732f3 --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/task/CommandCommentPort.java @@ -0,0 +1,8 @@ +package clap.server.application.port.outbound.task; + +import clap.server.domain.model.task.Comment; + +public interface CommandCommentPort { + + Comment saveComment(Comment comment); +} diff --git a/src/main/java/clap/server/application/port/outbound/task/LoadCommentPort.java b/src/main/java/clap/server/application/port/outbound/task/LoadCommentPort.java new file mode 100644 index 00000000..d3a42ea2 --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/task/LoadCommentPort.java @@ -0,0 +1,9 @@ +package clap.server.application.port.outbound.task; + +import clap.server.domain.model.task.Comment; + +import java.util.Optional; + +public interface LoadCommentPort { + Optional findById(Long commentId); +} diff --git a/src/main/java/clap/server/application/port/outbound/taskhistory/CommandCommentPort.java b/src/main/java/clap/server/application/port/outbound/taskhistory/CommandCommentPort.java index ed378deb..1adf7116 100644 --- a/src/main/java/clap/server/application/port/outbound/taskhistory/CommandCommentPort.java +++ b/src/main/java/clap/server/application/port/outbound/taskhistory/CommandCommentPort.java @@ -5,5 +5,5 @@ import java.util.Optional; public interface CommandCommentPort { - Optional save(Comment comment); + void save(Comment comment); } \ No newline at end of file diff --git a/src/main/java/clap/server/application/service/comment/PostCommentService.java b/src/main/java/clap/server/application/service/comment/PostCommentService.java new file mode 100644 index 00000000..0d9ca297 --- /dev/null +++ b/src/main/java/clap/server/application/service/comment/PostCommentService.java @@ -0,0 +1,96 @@ +package clap.server.application.service.comment; + +import clap.server.adapter.inbound.web.dto.task.PostAndEditCommentRequest; +import clap.server.adapter.outbound.infrastructure.s3.S3UploadAdapter; +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskHistoryType; +import clap.server.application.mapper.AttachmentMapper; +import clap.server.application.port.inbound.comment.PostCommentUsecase; +import clap.server.application.port.inbound.domain.MemberService; +import clap.server.application.port.inbound.domain.TaskService; +import clap.server.application.port.outbound.task.CommandAttachmentPort; +import clap.server.application.port.outbound.task.CommandCommentPort; +import clap.server.application.port.outbound.taskhistory.CommandTaskHistoryPort; +import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.common.constants.FilePathConstants; +import clap.server.domain.model.member.Member; +import clap.server.domain.model.task.Attachment; +import clap.server.domain.model.task.Comment; +import clap.server.domain.model.task.Task; +import clap.server.domain.model.task.TaskHistory; +import clap.server.exception.ApplicationException; +import clap.server.exception.code.MemberErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@ApplicationService +@RequiredArgsConstructor +public class PostCommentService implements PostCommentUsecase { + + private final MemberService memberService; + private final TaskService taskService; + private final CommandCommentPort commandCommentPort; + private final S3UploadAdapter s3UploadAdapter; + private final CommandAttachmentPort commandAttachmentPort; + private final CommandTaskHistoryPort commandTaskHistoryPort; + + @Transactional + @Override + public void save(Long userId, Long taskId, PostAndEditCommentRequest request) { + Task task = taskService.findById(taskId); + Member member = memberService.findActiveMember(userId); + + // 일반 회원일 경우 => 요청자인지 확인 + // 담당자일 경우 => 처리자인지 확인 + if (checkCommenter(task, member)) { + Comment comment = Comment.createComment(member, task, request.content()); + Comment savedComment = commandCommentPort.saveComment(comment); + + TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.COMMENT, task, null, member,savedComment); + commandTaskHistoryPort.save(taskHistory); + } + } + + @Transactional + @Override + public void saveCommentAttachment(Long userId, Long taskId, List files) { + Task task = taskService.findById(taskId); + Member member = memberService.findActiveMember(userId); + + if (checkCommenter(task, member)) { + Comment comment = Comment.createComment(member, task, "Attachment"); + Comment savedComment = commandCommentPort.saveComment(comment); + saveAttachment(files, task, savedComment); + + TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.COMMENT_FILE, task, null, member, savedComment); + commandTaskHistoryPort.save(taskHistory); + } + } + + private void saveAttachment(List files, Task task, Comment comment) { + List fileUrls = s3UploadAdapter.uploadFiles(FilePathConstants.TASK_IMAGE, files); + List attachments = AttachmentMapper.toCommentAttachments(task, comment, files, fileUrls); + commandAttachmentPort.saveAll(attachments); + } + + public Boolean checkCommenter(Task task, Member member) { + // 일반 회원일 경우 => 요청자인지 확인 + // 담당자일 경우 => 처리자인지 확인 + if ((member.getMemberInfo().getRole() == MemberRole.ROLE_MANAGER) + && !(member.getMemberId() == task.getProcessor().getMemberId())) { + throw new ApplicationException(MemberErrorCode.NOT_A_COMMENTER); + } + + else if ((member.getMemberInfo().getRole() == MemberRole.ROLE_USER) + && !(member.getMemberId() == task.getRequester().getMemberId())) { + throw new ApplicationException(MemberErrorCode.NOT_A_COMMENTER); + } + else { + return true; + } + + } +} diff --git a/src/main/java/clap/server/domain/model/task/Attachment.java b/src/main/java/clap/server/domain/model/task/Attachment.java index 3fafddee..11a2bfee 100644 --- a/src/main/java/clap/server/domain/model/task/Attachment.java +++ b/src/main/java/clap/server/domain/model/task/Attachment.java @@ -40,6 +40,17 @@ public static Attachment createAttachment(Task task, String originalName, String .build(); } + public static Attachment createCommentAttachment(Task task, Comment comment, String originalName, String fileUrl, long fileSize) { + return Attachment.builder() + .task(task) + .comment(comment) + .originalName(originalName) + .fileUrl(fileUrl) + .fileSize(formatFileSize(fileSize)) + .isDeleted(false) + .build(); + } + public void softDelete() { this.isDeleted = true; } diff --git a/src/main/java/clap/server/domain/model/task/Comment.java b/src/main/java/clap/server/domain/model/task/Comment.java index 52822050..ef4a3a43 100644 --- a/src/main/java/clap/server/domain/model/task/Comment.java +++ b/src/main/java/clap/server/domain/model/task/Comment.java @@ -1,5 +1,6 @@ package clap.server.domain.model.task; +import clap.server.adapter.inbound.web.dto.task.PostAndEditCommentRequest; import clap.server.domain.model.common.BaseTime; import clap.server.domain.model.member.Member; import lombok.AccessLevel; @@ -17,4 +18,12 @@ public class Comment extends BaseTime { private String content; private boolean isModified; + public static Comment createComment(Member member, Task task, String content) { + return Comment.builder() + .member(member) + .task(task) + .content(content) + .isModified(false) + .build(); + } } diff --git a/src/main/java/clap/server/exception/code/MemberErrorCode.java b/src/main/java/clap/server/exception/code/MemberErrorCode.java index dae6b6ef..5dc871ab 100644 --- a/src/main/java/clap/server/exception/code/MemberErrorCode.java +++ b/src/main/java/clap/server/exception/code/MemberErrorCode.java @@ -10,7 +10,8 @@ public enum MemberErrorCode implements BaseErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER_001", "회원을 찾을 수 없습니다."), ACTIVE_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER_002", "활성화 회원을 찾을 수 없습니다."), NOT_A_REVIEWER(HttpStatus.FORBIDDEN, "MEMBER_003", "리뷰어 권한이 없습니다."), - MEMBER_REGISTER_FAILED(HttpStatus.BAD_REQUEST, "MEMBER_004", "회원 등록에 실패하였습니다"); + MEMBER_REGISTER_FAILED(HttpStatus.BAD_REQUEST, "MEMBER_004", "회원 등록에 실패하였습니다"), + NOT_A_COMMENTER(HttpStatus.FORBIDDEN, "MEMBER_005", "댓글 작성 권한이 없습니다.") ; private final HttpStatus httpStatus;