diff --git a/src/main/java/clap/server/adapter/inbound/web/comment/CommandCommentController.java b/src/main/java/clap/server/adapter/inbound/web/comment/CommandCommentController.java new file mode 100644 index 00000000..144203df --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/comment/CommandCommentController.java @@ -0,0 +1,48 @@ +package clap.server.adapter.inbound.web.comment; + +import clap.server.adapter.inbound.security.SecurityUserDetails; +import clap.server.adapter.inbound.web.dto.task.DeleteCommentRequest; +import clap.server.adapter.inbound.web.dto.task.PostAndEditCommentRequest; +import clap.server.application.port.inbound.comment.CommandCommentUsecase; +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 lombok.RequiredArgsConstructor; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "02. Task", description = "작업 생성/수정 API") +@WebAdapter +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/comment") +public class CommandCommentController { + + private final CommandCommentUsecase commandCommentUsecase; + + @Operation(summary = "댓글 수정") + @Parameter(name = "commentId", description = "수정할 댓글 고유 ID", required = true, in = ParameterIn.PATH) + @PatchMapping("/{commentId}") + @Secured({"ROLE_MANAGER", "ROLE_USER"}) + public void editComment( + @AuthenticationPrincipal SecurityUserDetails userInfo, + @PathVariable Long commentId, + @RequestBody PostAndEditCommentRequest request) { + commandCommentUsecase.updateComment(userInfo.getUserId(), commentId, request); + } + + @Operation(summary = "댓글 삭제", description = "첨부파일 댓글일 경우 request body에 삭제할 파일 ID를 리스트로 전달") + @Parameter(name = "commentId", description = "수정할 댓글 고유 ID", required = true, in = ParameterIn.PATH) + @DeleteMapping("/{commentId}") + @Secured({"ROLE_MANAGER", "ROLE_USER"}) + public void deleteComment( + @AuthenticationPrincipal SecurityUserDetails userInfo, + @PathVariable Long commentId, + @RequestBody DeleteCommentRequest request) { + commandCommentUsecase.deleteComment(userInfo.getUserId(), commentId, request); + } + +} 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..d269da51 --- /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 = "taskId", description = "댓글 작성할 작업 고유 ID", required = true, in = ParameterIn.PATH) + @PostMapping("/{taskId}") + @Secured({"ROLE_MANAGER", "ROLE_USER"}) + public void createComment( + @AuthenticationPrincipal SecurityUserDetails userInfo, + @PathVariable Long taskId, + @RequestBody(required = true) PostAndEditCommentRequest request){ + postCommentUsecase.save(userInfo.getUserId(), taskId, request); + } + + @Operation(summary = "댓글 작성(첨부 파일)") + @Parameter(name = "taskId", description = "댓글 작성할 작업 고유 ID", required = true, in = ParameterIn.PATH) + @PostMapping("/attachment/{taskId}") + @Secured({"ROLE_MANAGER", "ROLE_USER"}) + public void createAttachmentComment( + @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/DeleteCommentRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/task/DeleteCommentRequest.java new file mode 100644 index 00000000..05c717a6 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/task/DeleteCommentRequest.java @@ -0,0 +1,13 @@ +package clap.server.adapter.inbound.web.dto.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +public record DeleteCommentRequest( + @Schema(description = "삭제할 파일 ID 목록, 없을 경우 emptylist 전송") + @NotNull + List attachmentsToDelete +) { +} 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/AttachmentPersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/AttachmentPersistenceAdapter.java index c4a7bb75..41ee5492 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/AttachmentPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/AttachmentPersistenceAdapter.java @@ -53,6 +53,14 @@ public List findAllByTaskIdAndCommentIsNullAndAttachmentId(final Lon .collect(Collectors.toList()); } + @Override + public List findAllyByTaskIdAndCommentIdAndAttachmentId(Long taskId, Long commentId, List attachmentIds) { + List attachmentEntities = attachmentRepository.findActiveAttachmentsByTask_TaskIdAndComment_CommentIdAndAttachmentIdIn(taskId, commentId, attachmentIds); + return attachmentEntities.stream() + .map(attachmentPersistenceMapper::toDomain) + .collect(Collectors.toList()); + } + @Override public List findAllByTaskIdAndCommentIsNotNull(final Long taskId) { List attachmentEntities = attachmentRepository.findAllByTask_TaskIdAndCommentIsNotNullAndIsDeletedIsFalse(taskId); 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..aec21821 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,32 @@ 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 commentId) { + Optional commentEntity = commentRepository.findById(commentId); + return commentEntity.map(commentPersistenceMapper::toDomain); + } + @Override - public Optional findById(Long id) { - return Optional.empty(); + public Comment saveComment(Comment comment) { + CommentEntity commentEntity = commentRepository.save(commentPersistenceMapper.toEntity(comment)); + return commentPersistenceMapper.toDomain(commentEntity); } } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/entity/member/MemberEntity.java b/src/main/java/clap/server/adapter/outbound/persistense/entity/member/MemberEntity.java index 4a107b7b..ba9862ad 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/entity/member/MemberEntity.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/entity/member/MemberEntity.java @@ -45,6 +45,7 @@ public class MemberEntity extends BaseTimeEntity { @Column(nullable = false) private MemberStatus status; + // TODO: spring security 적용 예정 @Column(nullable = false) private String password; @@ -52,11 +53,7 @@ public class MemberEntity extends BaseTimeEntity { private String imageUrl; @Column - private Boolean kakaoworkNotificationEnabled; - @Column - private Boolean agitNotificationEnabled; - @Column - private Boolean emailNotificationEnabled; + private Boolean notificationEnabled; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "admin_id") diff --git a/src/main/java/clap/server/adapter/outbound/persistense/entity/task/CommentEntity.java b/src/main/java/clap/server/adapter/outbound/persistense/entity/task/CommentEntity.java index 23af3bac..1b508c50 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/entity/task/CommentEntity.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/entity/task/CommentEntity.java @@ -32,4 +32,7 @@ public class CommentEntity extends BaseTimeEntity { @Column(name = "is_modified", nullable = false) private boolean isModified; + + @Column(name="is_deleted", nullable = false) + private boolean isDeleted; } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/mapper/CommentPersistenceMapper.java b/src/main/java/clap/server/adapter/outbound/persistense/mapper/CommentPersistenceMapper.java index 8f6eedb8..546bf750 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/mapper/CommentPersistenceMapper.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/mapper/CommentPersistenceMapper.java @@ -14,9 +14,11 @@ public interface CommentPersistenceMapper extends PersistenceMapper { List findAllByTask_TaskIdAndCommentIsNullAndIsDeletedIsFalse(Long taskId); List findAllByTask_TaskIdAndCommentIsNullAndIsDeletedIsFalseAndAttachmentIdIn(Long task_taskId, List attachmentId); + List findActiveAttachmentsByTask_TaskIdAndComment_CommentIdAndAttachmentIdIn(Long task_taskId, Long comment_commentId, List attachmentIds); List findAllByTask_TaskIdAndCommentIsNotNullAndIsDeletedIsFalse(Long taskId); } \ No newline at end of file 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/CommandCommentUsecase.java b/src/main/java/clap/server/application/port/inbound/comment/CommandCommentUsecase.java new file mode 100644 index 00000000..4fcd653f --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/comment/CommandCommentUsecase.java @@ -0,0 +1,13 @@ +package clap.server.application.port.inbound.comment; + +import clap.server.adapter.inbound.web.dto.task.DeleteCommentRequest; +import clap.server.adapter.inbound.web.dto.task.PostAndEditCommentRequest; + +import java.util.List; + +public interface CommandCommentUsecase { + + void updateComment(Long userId, Long commentId, PostAndEditCommentRequest request); + + void deleteComment(Long userId, Long commentId, DeleteCommentRequest request); +} 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/LoadAttachmentPort.java b/src/main/java/clap/server/application/port/outbound/task/LoadAttachmentPort.java index bec2ac52..023f4db6 100644 --- a/src/main/java/clap/server/application/port/outbound/task/LoadAttachmentPort.java +++ b/src/main/java/clap/server/application/port/outbound/task/LoadAttachmentPort.java @@ -8,5 +8,6 @@ public interface LoadAttachmentPort { List findAllByTaskIdAndCommentIsNull(Long taskId); List findAllByTaskIdAndCommentIsNullAndAttachmentId(Long taskId, List attachmentIds); + List findAllyByTaskIdAndCommentIdAndAttachmentId(Long taskId, Long commentId, List attachmentIds); List findAllByTaskIdAndCommentIsNotNull(Long taskId); } 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/CommandCommentService.java b/src/main/java/clap/server/application/service/comment/CommandCommentService.java new file mode 100644 index 00000000..6312c90b --- /dev/null +++ b/src/main/java/clap/server/application/service/comment/CommandCommentService.java @@ -0,0 +1,103 @@ +package clap.server.application.service.comment; + +import clap.server.adapter.inbound.web.dto.task.DeleteCommentRequest; +import clap.server.adapter.inbound.web.dto.task.PostAndEditCommentRequest; +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; +import clap.server.application.port.inbound.comment.CommandCommentUsecase; +import clap.server.application.port.inbound.domain.MemberService; +import clap.server.application.port.outbound.task.CommandAttachmentPort; +import clap.server.application.port.outbound.task.CommandCommentPort; +import clap.server.application.port.outbound.task.LoadAttachmentPort; +import clap.server.application.port.outbound.task.LoadCommentPort; +import clap.server.common.annotation.architecture.ApplicationService; +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.exception.ApplicationException; +import clap.server.exception.code.CommentErrorCode; +import clap.server.exception.code.MemberErrorCode; +import clap.server.exception.code.TaskErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@ApplicationService +@RequiredArgsConstructor +public class CommandCommentService implements CommandCommentUsecase { + + private final MemberService memberService; + private final LoadCommentPort loadCommentPort; + private final CommandCommentPort commandCommentPort; + private final LoadAttachmentPort loadAttachmentPort; + private final CommandAttachmentPort commandAttachmentPort; + + @Transactional + @Override + public void updateComment(Long userId, Long commentId, PostAndEditCommentRequest request) { + + Member member = memberService.findActiveMember(userId); + + Comment comment = loadCommentPort.findById(commentId) + .orElseThrow(() -> new ApplicationException(CommentErrorCode.COMMENT_NOT_FOUND)); + + if (checkCommenter(comment.getTask(), member)) { + + comment.updateComment(request.content()); + commandCommentPort.saveComment(comment); + }; + } + + @Transactional + @Override + public void deleteComment(Long userId, Long commentId, DeleteCommentRequest request) { + Member member = memberService.findActiveMember(userId); + + + Comment comment = loadCommentPort.findById(commentId) + .orElseThrow(() -> new ApplicationException(CommentErrorCode.COMMENT_NOT_FOUND)); + + if (checkCommenter(comment.getTask(), member)) { + + // 삭제할 댓글이 첨부파일일 경우 + if (!request.attachmentsToDelete().isEmpty()) { + deleteAttachments(request.attachmentsToDelete(), comment.getTask(), comment.getCommentId()); + } + + comment.softDelete(); + commandCommentPort.saveComment(comment); + }; + } + + private void deleteAttachments(List attachmentIdsToDelete, Task task, Long commentId) { + List attachmentsToDelete = validateAndGetAttachments(attachmentIdsToDelete, task, commentId); + attachmentsToDelete.forEach(Attachment::softDelete); + commandAttachmentPort.saveAll(attachmentsToDelete); + } + + private List validateAndGetAttachments(List attachmentIdsToDelete, Task task, Long commentId) { + List attachmentsOfTask = loadAttachmentPort.findAllyByTaskIdAndCommentIdAndAttachmentId(task.getTaskId(), commentId, attachmentIdsToDelete); + if (attachmentsOfTask.size() != attachmentIdsToDelete.size()) { + throw new ApplicationException(TaskErrorCode.TASK_ATTACHMENT_NOT_FOUND); + } + return attachmentsOfTask; + } + + 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/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/application/service/member/UpdateMemberInfoService.java b/src/main/java/clap/server/application/service/member/UpdateMemberInfoService.java index 3c6271fd..7d79fadd 100644 --- a/src/main/java/clap/server/application/service/member/UpdateMemberInfoService.java +++ b/src/main/java/clap/server/application/service/member/UpdateMemberInfoService.java @@ -3,7 +3,6 @@ import clap.server.adapter.inbound.web.dto.member.UpdateMemberInfoRequest; import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.inbound.member.UpdateMemberInfoUsecase; -import clap.server.application.port.outbound.member.CommandMemberPort; import clap.server.application.port.outbound.s3.S3UploadPort; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.common.constants.FilePathConstants; @@ -11,6 +10,7 @@ import clap.server.domain.model.member.Member; import clap.server.exception.ApplicationException; import clap.server.exception.code.AttachmentErrorcode; +import clap.server.exception.code.MemberErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -23,20 +23,15 @@ class UpdateMemberInfoService implements UpdateMemberInfoUsecase { private final MemberService memberService; private final S3UploadPort s3UploadPort; - private final CommandMemberPort commandMemberPort; @Override public void updateMemberInfo(Long memberId, UpdateMemberInfoRequest request, MultipartFile profileImage) throws IOException { - Member member = memberService.findActiveMember(memberId); - String profileImageUrl = null; - if (!profileImage.isEmpty()){ - if(!FileUtils.validImageFile(profileImage.getInputStream())) { - throw new ApplicationException(AttachmentErrorcode.UNSUPPORTED_FILE_TYPE); - } - profileImageUrl = s3UploadPort.uploadSingleFile(FilePathConstants.MEMBER_IMAGE, profileImage); + if (!FileUtils.validImageFile(profileImage.getInputStream())) { + throw new ApplicationException(AttachmentErrorcode.UNSUPPORTED_FILE_TYPE); } + Member member = memberService.findActiveMember(memberId); + String profileImageUrl = s3UploadPort.uploadSingleFile(FilePathConstants.MEMBER_IMAGE, profileImage); member.updateMemberInfo(request.name(), request.agitNotification(), request.emailNotification(), request.kakaoWorkNotification(), profileImageUrl); - commandMemberPort.save(member); } } diff --git a/src/main/java/clap/server/application/service/task/UpdateTaskService.java b/src/main/java/clap/server/application/service/task/UpdateTaskService.java index 214f9348..b25e355f 100644 --- a/src/main/java/clap/server/application/service/task/UpdateTaskService.java +++ b/src/main/java/clap/server/application/service/task/UpdateTaskService.java @@ -1,7 +1,7 @@ package clap.server.application.service.task; import clap.server.adapter.inbound.web.dto.task.*; -import clap.server.adapter.outbound.persistense.entity.task.constant.TaskHistoryType; +import clap.server.adapter.outbound.infrastructure.s3.S3UploadAdapter; import clap.server.application.mapper.AttachmentMapper; import clap.server.application.mapper.TaskMapper; import clap.server.application.port.inbound.domain.CategoryService; @@ -12,15 +12,16 @@ import clap.server.application.port.inbound.task.UpdateTaskProcessorUsecase; import clap.server.application.port.inbound.task.UpdateTaskStatusUsecase; import clap.server.application.port.inbound.task.UpdateTaskUsecase; -import clap.server.application.port.outbound.s3.S3UploadPort; import clap.server.application.port.outbound.task.CommandAttachmentPort; import clap.server.application.port.outbound.task.CommandTaskPort; import clap.server.application.port.outbound.task.LoadAttachmentPort; -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.*; +import clap.server.domain.model.task.Attachment; +import clap.server.domain.model.task.Category; +import clap.server.domain.model.task.Label; +import clap.server.domain.model.task.Task; import clap.server.exception.ApplicationException; import clap.server.exception.code.TaskErrorCode; import lombok.RequiredArgsConstructor; @@ -44,8 +45,7 @@ public class UpdateTaskService implements UpdateTaskUsecase, UpdateTaskStatusUse private final LoadAttachmentPort loadAttachmentPort; private final LabelService labelService; private final CommandAttachmentPort commandAttachmentPort; - private final S3UploadPort s3UploadPort; - private final CommandTaskHistoryPort commandTaskHistoryPort; + private final S3UploadAdapter s3UploadAdapter; @Override @Transactional @@ -69,9 +69,8 @@ public UpdateTaskResponse updateTaskState(Long memberId, Long taskId, UpdateTask memberService.findActiveMember(memberId); Task task = taskService.findById(taskId); task.updateTaskStatus(updateTaskStatusRequest.taskStatus()); - TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.STATUS_SWITCHED, task, task.getTaskStatus().getDescription(), null, null); - commandTaskHistoryPort.save(taskHistory); - return TaskMapper.toUpdateTaskResponse(commandTaskPort.save(task)); + Task updateTask = commandTaskPort.save(task); + return TaskMapper.toUpdateTaskResponse(updateTask); // TODO : 알림 생성 로직 및 푸시 알림 로직 추가 } @@ -79,13 +78,13 @@ public UpdateTaskResponse updateTaskState(Long memberId, Long taskId, UpdateTask @Transactional @Override public UpdateTaskResponse updateTaskProcessor(Long taskId, Long userId, UpdateTaskProcessorRequest request) { - memberService.findReviewer(userId); + Member reviewer = memberService.findReviewer(userId); Member processor = memberService.findById(request.processorId()); + Task task = taskService.findById(taskId); task.updateProcessor(processor); - TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.PROCESSOR_CHANGED, task, null, processor, null); - commandTaskHistoryPort.save(taskHistory); - return TaskMapper.toUpdateTaskResponse(commandTaskPort.save(task)); + Task updateTask = commandTaskPort.save(task); + return TaskMapper.toUpdateTaskResponse(updateTask); // TODO : 알림 생성 로직 및 푸시 알림 로직 추가 } @@ -93,18 +92,20 @@ public UpdateTaskResponse updateTaskProcessor(Long taskId, Long userId, UpdateTa @Transactional @Override public UpdateTaskResponse updateTaskLabel(Long taskId, Long userId, UpdateTaskLabelRequest request) { - memberService.findReviewer(userId); + Member reviewer = memberService.findReviewer(userId); Task task = taskService.findById(taskId); Label label = labelService.findById(request.labelId()); + task.updateLabel(label); - return TaskMapper.toUpdateTaskResponse(commandTaskPort.save(task)); + Task updatetask = commandTaskPort.save(task); + return TaskMapper.toUpdateTaskResponse(updatetask); } private void updateAttachments(List attachmentIdsToDelete, List files, Task task) { List attachmentsToDelete = validateAndGetAttachments(attachmentIdsToDelete, task); attachmentsToDelete.forEach(Attachment::softDelete); - List fileUrls = s3UploadPort.uploadFiles(FilePathConstants.TASK_IMAGE, files); + List fileUrls = s3UploadAdapter.uploadFiles(FilePathConstants.TASK_IMAGE, files); List attachments = AttachmentMapper.toTaskAttachments(task, files, fileUrls); commandAttachmentPort.saveAll(attachments); } diff --git a/src/main/java/clap/server/domain/model/member/Member.java b/src/main/java/clap/server/domain/model/member/Member.java index 1832b0e9..f3279b13 100644 --- a/src/main/java/clap/server/domain/model/member/Member.java +++ b/src/main/java/clap/server/domain/model/member/Member.java @@ -75,7 +75,6 @@ public void updateMemberInfo(String name, Boolean agitNotificationEnabled, Boole this.imageUrl = imageUrl; } } - public void setStatusDeleted() { this.status = MemberStatus.DELETED; } 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..25ce0b46 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; @@ -16,5 +17,25 @@ public class Comment extends BaseTime { private Task task; private String content; private boolean isModified; + private boolean isDeleted; + public static Comment createComment(Member member, Task task, String content) { + return Comment.builder() + .member(member) + .task(task) + .content(content) + .isModified(false) + .build(); + } + + public void updateComment(String content) { + this.content = content; + if(!this.isModified) { + this.isModified = true; + } + } + + public void softDelete() { + this.isDeleted = true; + } } diff --git a/src/main/java/clap/server/exception/code/CommentErrorCode.java b/src/main/java/clap/server/exception/code/CommentErrorCode.java new file mode 100644 index 00000000..97aa1baf --- /dev/null +++ b/src/main/java/clap/server/exception/code/CommentErrorCode.java @@ -0,0 +1,17 @@ +package clap.server.exception.code; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum CommentErrorCode implements BaseErrorCode { + + COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMENT_001", "댓글을 찾을 수 없습니다."); + + + private final HttpStatus httpStatus; + private final String customCode; + private final String message; +} diff --git a/src/main/java/clap/server/exception/code/MemberErrorCode.java b/src/main/java/clap/server/exception/code/MemberErrorCode.java index 5dc871ab..00ab7664 100644 --- a/src/main/java/clap/server/exception/code/MemberErrorCode.java +++ b/src/main/java/clap/server/exception/code/MemberErrorCode.java @@ -11,7 +11,7 @@ public enum MemberErrorCode implements BaseErrorCode { ACTIVE_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER_002", "활성화 회원을 찾을 수 없습니다."), NOT_A_REVIEWER(HttpStatus.FORBIDDEN, "MEMBER_003", "리뷰어 권한이 없습니다."), MEMBER_REGISTER_FAILED(HttpStatus.BAD_REQUEST, "MEMBER_004", "회원 등록에 실패하였습니다"), - NOT_A_COMMENTER(HttpStatus.FORBIDDEN, "MEMBER_005", "댓글 작성 권한이 없습니다.") + NOT_A_COMMENTER(HttpStatus.FORBIDDEN, "MEMBER_005", "댓글 권한이 없습니다."), ; private final HttpStatus httpStatus;