diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/task/request/UpdateTaskOrderRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/task/request/UpdateTaskOrderRequest.java new file mode 100644 index 00000000..b39ded57 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/task/request/UpdateTaskOrderRequest.java @@ -0,0 +1,14 @@ +package clap.server.adapter.inbound.web.dto.task.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; + +public record UpdateTaskOrderRequest( + @Schema(description = "변경할 위치의 상위 작업 ID, 가장 상위일 경우 0 입력") + long prevTaskId, + @Min(1) @Schema(description = "순서 또는 상태를 변경할 작업의 ID") + long targetTaskId, + @Schema(description = "변경할 위치의 하위 작업 ID, 가장 하위일 경우 0 입력") + long nextTaskId +) { +} diff --git a/src/main/java/clap/server/adapter/inbound/web/task/TaskBoardController.java b/src/main/java/clap/server/adapter/inbound/web/task/TaskBoardController.java index b2a5a6ea..00e7b505 100644 --- a/src/main/java/clap/server/adapter/inbound/web/task/TaskBoardController.java +++ b/src/main/java/clap/server/adapter/inbound/web/task/TaskBoardController.java @@ -1,10 +1,15 @@ package clap.server.adapter.inbound.web.task; import clap.server.adapter.inbound.security.SecurityUserDetails; +import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskOrderRequest; import clap.server.adapter.inbound.web.dto.task.response.TaskBoardResponse; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import clap.server.application.port.inbound.task.UpdateTaskBoardUsecase; import clap.server.application.port.inbound.task.TaskBoardUsecase; 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.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; @@ -12,10 +17,7 @@ import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.time.LocalDate; @@ -26,16 +28,30 @@ @RequestMapping("/api/task-board") public class TaskBoardController { private final TaskBoardUsecase taskBoardUsecase; + private final UpdateTaskBoardUsecase updateTaskBoardUsecase; + @Operation(summary = "작업 보드 조회 API") @GetMapping public ResponseEntity getTaskBoard(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int pageSize, @Parameter(description = "yyyy-mm-dd 형식으로 입력합니다.") @RequestParam(required = false) - @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate untilDate, + @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate untilDate, @AuthenticationPrincipal SecurityUserDetails userInfo) { Pageable pageable = PageRequest.of(page, pageSize); return ResponseEntity.ok(taskBoardUsecase.getTaskBoards(userInfo.getUserId(), untilDate, pageable)); } + @Operation(summary = "작업 보드 순서 및 상태 변경 API") + @PatchMapping + public void updateTaskBoard(@Parameter(description = "전환될 작업의 상태, 상태 전환이 아니라면 입력 X", schema = @Schema(allowableValues = {"IN_PROGRESS", "PENDING_COMPLETED", "COMPLETED"})) + @RequestParam(required = false) TaskStatus status, + @RequestBody UpdateTaskOrderRequest request, + @AuthenticationPrincipal SecurityUserDetails userInfo) { + if (status == null) { + updateTaskBoardUsecase.updateTaskOrder(userInfo.getUserId(), request); + } else { + updateTaskBoardUsecase.updateTaskOrderAndStatus(userInfo.getUserId(), request, status); + } + } } diff --git a/src/main/java/clap/server/adapter/outbound/api/AgitClient.java b/src/main/java/clap/server/adapter/outbound/api/AgitClient.java index 026018cd..ee4ff073 100644 --- a/src/main/java/clap/server/adapter/outbound/api/AgitClient.java +++ b/src/main/java/clap/server/adapter/outbound/api/AgitClient.java @@ -3,7 +3,7 @@ import clap.server.adapter.outbound.api.dto.SendAgitRequest; import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; import clap.server.application.port.outbound.webhook.SendAgitPort; -import clap.server.common.annotation.architecture.PersistenceAdapter; +import clap.server.common.annotation.architecture.ExternalApiAdapter; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; @@ -12,12 +12,12 @@ import org.springframework.web.client.RestTemplate; -@PersistenceAdapter +@ExternalApiAdapter @RequiredArgsConstructor public class AgitClient implements SendAgitPort { - @Value("${agit.url}") - private String AGITWEBHOOK_URL; + @Value("${webhook.agit.url}") + private String AGIT_WEBHOOK_URL; @Override public void sendAgit(SendAgitRequest request) { @@ -48,6 +48,6 @@ else if (request.notificationType() == NotificationType.PROCESSOR_CHANGED) { HttpEntity entity = new HttpEntity<>(payload, headers); // Post 요청 - restTemplate.exchange(AGITWEBHOOK_URL, HttpMethod.POST, entity, String.class); + restTemplate.exchange(AGIT_WEBHOOK_URL, HttpMethod.POST, entity, String.class); } } diff --git a/src/main/java/clap/server/adapter/outbound/api/EmailClient.java b/src/main/java/clap/server/adapter/outbound/api/EmailClient.java index ee5fb2f9..979233b0 100644 --- a/src/main/java/clap/server/adapter/outbound/api/EmailClient.java +++ b/src/main/java/clap/server/adapter/outbound/api/EmailClient.java @@ -3,7 +3,7 @@ import clap.server.adapter.outbound.api.dto.SendWebhookRequest; import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; import clap.server.application.port.outbound.webhook.SendEmailPort; -import clap.server.common.annotation.architecture.PersistenceAdapter; +import clap.server.common.annotation.architecture.ExternalApiAdapter; import clap.server.exception.ApplicationException; import clap.server.exception.code.NotificationErrorCode; import jakarta.mail.internet.MimeMessage; @@ -13,7 +13,7 @@ import org.thymeleaf.context.Context; import org.thymeleaf.spring6.SpringTemplateEngine; -@PersistenceAdapter +@ExternalApiAdapter @RequiredArgsConstructor public class EmailClient implements SendEmailPort { diff --git a/src/main/java/clap/server/adapter/outbound/api/KakaoWorkClient.java b/src/main/java/clap/server/adapter/outbound/api/KakaoWorkClient.java index 9557f973..aa0a7743 100644 --- a/src/main/java/clap/server/adapter/outbound/api/KakaoWorkClient.java +++ b/src/main/java/clap/server/adapter/outbound/api/KakaoWorkClient.java @@ -3,7 +3,7 @@ import clap.server.adapter.outbound.api.dto.SendKakaoWorkRequest; import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; import clap.server.application.port.outbound.webhook.SendKaKaoWorkPort; -import clap.server.common.annotation.architecture.PersistenceAdapter; +import clap.server.common.annotation.architecture.ExternalApiAdapter; import clap.server.exception.ApplicationException; import clap.server.exception.code.NotificationErrorCode; import lombok.RequiredArgsConstructor; @@ -13,14 +13,14 @@ import org.springframework.http.HttpMethod; import org.springframework.web.client.RestTemplate; -@PersistenceAdapter +@ExternalApiAdapter @RequiredArgsConstructor public class KakaoWorkClient implements SendKaKaoWorkPort { - @Value("${kakaowork.url}") + @Value("${webhook.kakaowork.url}") private String kakaworkUrl; - @Value("${kakaowork.auth}") + @Value("${webhook.kakaowork.auth}") private String kakaworkAuth; private final ObjectBlockService makeObjectBlock; diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/s3/S3UploadAdapter.java b/src/main/java/clap/server/adapter/outbound/infrastructure/s3/S3UploadAdapter.java index 44c7149e..68798b73 100644 --- a/src/main/java/clap/server/adapter/outbound/infrastructure/s3/S3UploadAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/s3/S3UploadAdapter.java @@ -3,7 +3,7 @@ import clap.server.application.port.outbound.s3.S3UploadPort; import clap.server.common.annotation.architecture.InfrastructureAdapter; import clap.server.config.s3.KakaoS3Config; -import clap.server.domain.model.task.FilePath; +import clap.server.common.constants.FilePathConstants; import clap.server.exception.S3Exception; import clap.server.exception.code.S3Errorcode; import lombok.RequiredArgsConstructor; @@ -26,11 +26,11 @@ public class S3UploadAdapter implements S3UploadPort { private final KakaoS3Config kakaoS3Config; private final S3Client s3Client; - public List uploadFiles(FilePath filePrefix, List multipartFiles) { + public List uploadFiles(FilePathConstants filePrefix, List multipartFiles) { return multipartFiles.stream().map((file) -> uploadSingleFile(filePrefix, file)).toList(); } - public String uploadSingleFile(FilePath filePrefix, MultipartFile file) { + public String uploadSingleFile(FilePathConstants filePrefix, MultipartFile file) { try { Path filePath = getFilePath(file); String objectKey = createObjectKey(filePrefix.getPath(), file.getOriginalFilename()); diff --git a/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java index 18a1f9a0..f07be47c 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java @@ -48,6 +48,12 @@ public List findReviewers() { .collect(Collectors.toList()); } + @Override + public Optional findReviewerById(Long id) { + Optional memberEntity = memberRepository.findByMemberIdAndIsReviewerTrue(id); + return memberEntity.map(memberPersistenceMapper::toDomain); + } + @Override public void save(final Member member) { MemberEntity memberEntity = memberPersistenceMapper.toEntity(member); diff --git a/src/main/java/clap/server/adapter/outbound/persistense/TaskPersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/TaskPersistenceAdapter.java index c0ea7eea..1b72d24a 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/TaskPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/TaskPersistenceAdapter.java @@ -1,9 +1,9 @@ package clap.server.adapter.outbound.persistense; import clap.server.adapter.inbound.web.dto.task.FilterAllTasksResponse; -import clap.server.adapter.inbound.web.dto.task.FilterTaskListRequest; -import clap.server.adapter.inbound.web.dto.task.FilterRequestedTasksResponse; import clap.server.adapter.inbound.web.dto.task.FilterPendingApprovalResponse; +import clap.server.adapter.inbound.web.dto.task.FilterRequestedTasksResponse; +import clap.server.adapter.inbound.web.dto.task.FilterTaskListRequest; import clap.server.adapter.outbound.persistense.entity.task.TaskEntity; import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; import clap.server.adapter.outbound.persistense.mapper.TaskPersistenceMapper; @@ -63,6 +63,12 @@ public Slice findByProcessorAndStatus(Long processorId, List s return tasks.map(taskPersistenceMapper::toDomain); } + @Override + public Optional findByIdAndStatus(Long id, TaskStatus status) { + Optional taskEntity = taskRepository.findByTaskIdAndTaskStatus(id, status); + return taskEntity.map(taskPersistenceMapper::toDomain); + } + @Override public List findYesterdayTaskByDate(LocalDateTime now) { return taskRepository.findYesterdayTaskByUpdatedAtIsBetween(now.minusDays(1), now) @@ -75,4 +81,16 @@ public Page findAllTasks (Pageable pageable, FilterTaskL .map(taskPersistenceMapper::toDomain); return taskList.map(TaskMapper::toFilterAllTasksResponse); } + @Override + public Optional findPrevOrderTaskByProcessorIdAndStatus(Long processorId, TaskStatus taskStatus, Long processorOrder){ + Optional taskEntity = taskRepository.findTopByProcessor_MemberIdAndTaskStatusAndProcessorOrderLessThanOrderByProcessorOrderDesc(processorId, taskStatus, processorOrder); + return taskEntity.map(taskPersistenceMapper::toDomain); + } + + @Override + public Optional findNextOrderTaskByProcessorIdAndStatus(Long processorId, TaskStatus taskStatus, Long processorOrder){ + Optional taskEntity = taskRepository.findTopByProcessor_MemberIdAndTaskStatusAndProcessorOrderAfterOrderByProcessorOrderDesc(processorId, taskStatus, processorOrder); + return taskEntity.map(taskPersistenceMapper::toDomain); + } + } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/entity/task/constant/TaskStatus.java b/src/main/java/clap/server/adapter/outbound/persistense/entity/task/constant/TaskStatus.java index 29972c3d..83a653f4 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/entity/task/constant/TaskStatus.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/entity/task/constant/TaskStatus.java @@ -1,10 +1,11 @@ package clap.server.adapter.outbound.persistense.entity.task.constant; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; import lombok.Getter; import lombok.RequiredArgsConstructor; +import java.util.List; + @Getter @RequiredArgsConstructor public enum TaskStatus { @@ -16,10 +17,6 @@ public enum TaskStatus { private final String description; - @JsonValue - public String getDescription() { - return description; - } @JsonCreator public static TaskStatus fromDescription(String description) { for (TaskStatus status : TaskStatus.values()) { @@ -29,4 +26,11 @@ public static TaskStatus fromDescription(String description) { } throw new IllegalArgumentException("Unknown description: " + description); } + + public static List getTaskBoardStatusList() { + return List.of( + TaskStatus.IN_PROGRESS, + TaskStatus.PENDING_COMPLETED, + TaskStatus.COMPLETED); + } } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java index 0bbec88e..e1bd0b45 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java @@ -21,5 +21,7 @@ public interface MemberRepository extends JpaRepository { Optional findByNickname(String nickname); List findByIsReviewerTrue(); + + Optional findByMemberIdAndIsReviewerTrue(Long memberId); } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskRepository.java index 6fa3eb6c..99099b28 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskRepository.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskRepository.java @@ -12,6 +12,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; @Repository public interface TaskRepository extends JpaRepository, TaskCustomRepository { @@ -28,7 +29,8 @@ List findYesterdayTaskByUpdatedAtIsBetween( @Query("SELECT t FROM TaskEntity t " + "WHERE t.processor.memberId = :processorId " + "AND t.taskStatus IN :taskStatus " + - "AND (t.taskStatus != 'COMPLETED' OR t.finishedAt >= :untilDate)") + "AND (t.taskStatus != 'COMPLETED' OR t.finishedAt <= :untilDate) " + + "ORDER BY t.processorOrder ASC ") Slice findTasksWithTaskStatusAndCompletedAt( @Param("processorId") Long processorId, @Param("taskStatus") List taskStatus, @@ -36,7 +38,12 @@ Slice findTasksWithTaskStatusAndCompletedAt( Pageable pageable ); + Optional findByTaskIdAndTaskStatus(Long id, TaskStatus status); + Optional findTopByProcessor_MemberIdAndTaskStatusAndProcessorOrderLessThanOrderByProcessorOrderDesc(Long processorId, TaskStatus taskStatus, Long processorOrder); + + Optional findTopByProcessor_MemberIdAndTaskStatusAndProcessorOrderAfterOrderByProcessorOrderDesc( + Long processorId, TaskStatus taskStatus, Long processorOrder); } diff --git a/src/main/java/clap/server/application/port/inbound/domain/MemberService.java b/src/main/java/clap/server/application/port/inbound/domain/MemberService.java index ac2fb651..99e23229 100644 --- a/src/main/java/clap/server/application/port/inbound/domain/MemberService.java +++ b/src/main/java/clap/server/application/port/inbound/domain/MemberService.java @@ -24,6 +24,12 @@ public Member findActiveMember(Long memberId) { () -> new ApplicationException(MemberErrorCode.ACTIVE_MEMBER_NOT_FOUND)); } + public Member findReviewer(Long memberId) { + return loadMemberPort.findReviewerById(memberId).orElseThrow( + ()-> new ApplicationException(MemberErrorCode.NOT_A_REVIEWER) + ); + } + public List findReviewers() { return loadMemberPort.findReviewers(); } diff --git a/src/main/java/clap/server/application/port/inbound/label/FindLabelListUsecase.java b/src/main/java/clap/server/application/port/inbound/label/FindLabelListUsecase.java index fbfe94cb..e475b489 100644 --- a/src/main/java/clap/server/application/port/inbound/label/FindLabelListUsecase.java +++ b/src/main/java/clap/server/application/port/inbound/label/FindLabelListUsecase.java @@ -8,6 +8,6 @@ public interface FindLabelListUsecase { - SliceResponse findLabelList(Long userId, Pageable pageable); - List findLabelListAdmin(Long userId); + SliceResponse findLabelList(Long memberId, Pageable pageable); + List findLabelListAdmin(Long memberId); } diff --git a/src/main/java/clap/server/application/port/inbound/task/UpdateTaskBoardUsecase.java b/src/main/java/clap/server/application/port/inbound/task/UpdateTaskBoardUsecase.java new file mode 100644 index 00000000..3a7ebf9a --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/task/UpdateTaskBoardUsecase.java @@ -0,0 +1,9 @@ +package clap.server.application.port.inbound.task; + +import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskOrderRequest; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; + +public interface UpdateTaskBoardUsecase { + void updateTaskOrder(Long processorId, UpdateTaskOrderRequest request); + void updateTaskOrderAndStatus(Long processorId, UpdateTaskOrderRequest request, TaskStatus status); +} diff --git a/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java b/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java index ab81c42d..70c04c75 100644 --- a/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java +++ b/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java @@ -14,4 +14,6 @@ public interface LoadMemberPort { List findReviewers(); + Optional findReviewerById(Long id); + } diff --git a/src/main/java/clap/server/application/port/outbound/s3/S3UploadPort.java b/src/main/java/clap/server/application/port/outbound/s3/S3UploadPort.java index a55f06a9..63423da3 100644 --- a/src/main/java/clap/server/application/port/outbound/s3/S3UploadPort.java +++ b/src/main/java/clap/server/application/port/outbound/s3/S3UploadPort.java @@ -1,12 +1,12 @@ package clap.server.application.port.outbound.s3; -import clap.server.domain.model.task.FilePath; +import clap.server.common.constants.FilePathConstants; import org.springframework.web.multipart.MultipartFile; import java.util.List; public interface S3UploadPort { - List uploadFiles(FilePath filePrefix, List multipartFiles); + List uploadFiles(FilePathConstants filePrefix, List multipartFiles); - String uploadSingleFile(FilePath filePrefix, MultipartFile file); + String uploadSingleFile(FilePathConstants filePrefix, MultipartFile file); } diff --git a/src/main/java/clap/server/application/port/outbound/task/LoadTaskPort.java b/src/main/java/clap/server/application/port/outbound/task/LoadTaskPort.java index ff18bf14..c6e69572 100644 --- a/src/main/java/clap/server/application/port/outbound/task/LoadTaskPort.java +++ b/src/main/java/clap/server/application/port/outbound/task/LoadTaskPort.java @@ -1,13 +1,10 @@ package clap.server.application.port.outbound.task; import clap.server.adapter.inbound.web.dto.task.FilterAllTasksResponse; -import clap.server.adapter.inbound.web.dto.task.FilterTaskListRequest; -import clap.server.adapter.inbound.web.dto.task.FilterRequestedTasksResponse; import clap.server.adapter.inbound.web.dto.task.FilterPendingApprovalResponse; -import clap.server.adapter.inbound.web.dto.task.FilterAllTasksResponse; +import clap.server.adapter.inbound.web.dto.task.FilterRequestedTasksResponse; import clap.server.adapter.inbound.web.dto.task.FilterTaskListRequest; import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; -import clap.server.adapter.inbound.web.dto.task.FilterRequestedTasksResponse; import clap.server.domain.model.task.Task; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -29,4 +26,10 @@ public interface LoadTaskPort { Page findAllTasks(Pageable pageable, FilterTaskListRequest findTaskListRequest); Slice findByProcessorAndStatus(Long processorId, List statuses, LocalDateTime untilDate, Pageable pageable); + + Optional findByIdAndStatus(Long id, TaskStatus status); + + Optional findPrevOrderTaskByProcessorIdAndStatus(Long processorId, TaskStatus taskStatus, Long processorOrder); + + Optional findNextOrderTaskByProcessorIdAndStatus(Long processorId, TaskStatus taskStatus, Long processorOrder); } \ No newline at end of file diff --git a/src/main/java/clap/server/application/service/label/FindLabelListService.java b/src/main/java/clap/server/application/service/label/FindLabelListService.java index 77f3bd26..57d23a49 100644 --- a/src/main/java/clap/server/application/service/label/FindLabelListService.java +++ b/src/main/java/clap/server/application/service/label/FindLabelListService.java @@ -25,13 +25,8 @@ public class FindLabelListService implements FindLabelListUsecase { private final MemberService memberService; @Override - public SliceResponse findLabelList(Long userId, Pageable pageable) { - Member member = memberService.findActiveMember(userId); - - // 담당자인데 검토자가 아닌 경우 Error 처리 - if (!member.getMemberInfo().isReviewer()) { - throw new ApplicationException(MemberErrorCode.NOT_A_REVIEWER); - } + public SliceResponse findLabelList(Long memberId, Pageable pageable) { + Member member = memberService.findReviewer(memberId); return loadLabelPort.findLabelListBySlice(pageable); } diff --git a/src/main/java/clap/server/application/service/task/ApprovalTaskService.java b/src/main/java/clap/server/application/service/task/ApprovalTaskService.java index cad2d10c..2c4d88b4 100644 --- a/src/main/java/clap/server/application/service/task/ApprovalTaskService.java +++ b/src/main/java/clap/server/application/service/task/ApprovalTaskService.java @@ -3,7 +3,6 @@ import clap.server.adapter.inbound.web.dto.task.ApprovalTaskRequest; import clap.server.adapter.inbound.web.dto.task.ApprovalTaskResponse; import clap.server.adapter.inbound.web.dto.task.FindApprovalFormResponse; -import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; import clap.server.application.mapper.TaskMapper; import clap.server.application.port.inbound.domain.CategoryService; import clap.server.application.port.inbound.domain.LabelService; @@ -16,9 +15,6 @@ 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.MemberErrorCode; -import clap.server.exception.code.TaskErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; @@ -36,10 +32,7 @@ public class ApprovalTaskService implements ApprovalTaskUsecase { @Override @Transactional public ApprovalTaskResponse approvalTaskByReviewer(Long reviewerId, Long taskId, ApprovalTaskRequest approvalTaskRequest) { - Member reviewer = memberService.findActiveMember(reviewerId); - if (!reviewer.isReviewer()) { - throw new ApplicationException(MemberErrorCode.NOT_A_REVIEWER); - } + Member reviewer = memberService.findReviewer(reviewerId); Task task = taskService.findById(taskId); Member processor = memberService.findById(approvalTaskRequest.processorId()); Category category = categoryService.findById(approvalTaskRequest.categoryId()); @@ -54,9 +47,7 @@ public ApprovalTaskResponse approvalTaskByReviewer(Long reviewerId, Long taskId, public FindApprovalFormResponse findApprovalForm(Long managerId, Long taskId) { memberService.findActiveMember(managerId); Task task = taskService.findById(taskId); - if (task.getTaskStatus() != TaskStatus.REQUESTED) { - throw new ApplicationException(TaskErrorCode.TASK_STATUS_MISMATCH); - } + task.validateTaskRequested(); return TaskMapper.toFindApprovalFormResponse(task); } } diff --git a/src/main/java/clap/server/application/service/task/CreateTaskService.java b/src/main/java/clap/server/application/service/task/CreateTaskService.java index a704e13c..60e4ec18 100644 --- a/src/main/java/clap/server/application/service/task/CreateTaskService.java +++ b/src/main/java/clap/server/application/service/task/CreateTaskService.java @@ -23,7 +23,7 @@ import clap.server.domain.model.notification.Notification; import clap.server.domain.model.task.Attachment; import clap.server.domain.model.task.Category; -import clap.server.domain.model.task.FilePath; +import clap.server.common.constants.FilePathConstants; import clap.server.domain.model.task.Task; import lombok.RequiredArgsConstructor; @@ -64,7 +64,7 @@ public CreateTaskResponse createTask(Long requesterId, CreateTaskRequest createT } private void saveAttachments(List files, Task task) { - List fileUrls = s3UploadAdapter.uploadFiles(FilePath.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/application/service/task/TaskBoardService.java b/src/main/java/clap/server/application/service/task/TaskBoardService.java index f515b4cb..e2332833 100644 --- a/src/main/java/clap/server/application/service/task/TaskBoardService.java +++ b/src/main/java/clap/server/application/service/task/TaskBoardService.java @@ -4,9 +4,7 @@ import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; import clap.server.application.mapper.TaskMapper; import clap.server.application.port.inbound.domain.MemberService; -import clap.server.application.port.inbound.domain.TaskService; import clap.server.application.port.inbound.task.TaskBoardUsecase; -import clap.server.application.port.outbound.task.CommandTaskPort; import clap.server.application.port.outbound.task.LoadTaskPort; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.task.Task; @@ -22,23 +20,17 @@ @ApplicationService @RequiredArgsConstructor class TaskBoardService implements TaskBoardUsecase { - private final static List VIEWABLE_STATUSES = List.of( - TaskStatus.IN_PROGRESS, - TaskStatus.PENDING_COMPLETED, - TaskStatus.COMPLETED - ); + private final static List VIEWABLE_STATUSES = TaskStatus.getTaskBoardStatusList(); + private final MemberService memberService; - private final TaskService taskService; private final LoadTaskPort loadTaskPort; - private final CommandTaskPort commandTaskPort; - @Transactional(readOnly = true) @Override + @Transactional(readOnly = true) public TaskBoardResponse getTaskBoards(Long processorId, LocalDate untilDate, Pageable pageable) { memberService.findById(processorId); - LocalDateTime untilDateTime = untilDate==null ? LocalDate.now().plusDays(1).atStartOfDay() : untilDate.plusDays(1).atStartOfDay(); + LocalDateTime untilDateTime = untilDate == null ? LocalDate.now().plusDays(1).atStartOfDay() : untilDate.plusDays(1).atStartOfDay(); Slice tasks = loadTaskPort.findByProcessorAndStatus(processorId, VIEWABLE_STATUSES, untilDateTime, pageable); return TaskMapper.toSliceTaskItemResponse(tasks); } - } diff --git a/src/main/java/clap/server/application/service/task/UpdateTaskBoardService.java b/src/main/java/clap/server/application/service/task/UpdateTaskBoardService.java new file mode 100644 index 00000000..25a45a6e --- /dev/null +++ b/src/main/java/clap/server/application/service/task/UpdateTaskBoardService.java @@ -0,0 +1,189 @@ +package clap.server.application.service.task; + +import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskOrderRequest; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import clap.server.application.port.inbound.domain.MemberService; +import clap.server.application.port.inbound.domain.TaskService; +import clap.server.application.port.inbound.task.UpdateTaskBoardUsecase; +import clap.server.application.port.outbound.task.CommandTaskPort; +import clap.server.application.port.outbound.task.LoadTaskPort; +import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.domain.model.member.Member; +import clap.server.domain.model.task.Task; +import clap.server.exception.ApplicationException; +import clap.server.exception.code.TaskErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@ApplicationService +@RequiredArgsConstructor +class UpdateTaskBoardService implements UpdateTaskBoardUsecase { + private final MemberService memberService; + private final TaskService taskService; + private final LoadTaskPort loadTaskPort; + private final CommandTaskPort commandTaskPort; + + private Task findByIdAndStatus(Long taskId, TaskStatus status) { + return loadTaskPort.findByIdAndStatus(taskId, status).orElseThrow(()-> new ApplicationException(TaskErrorCode.TASK_NOT_FOUND)); + } + + /** + * 작업(Task)의 순서를 업데이트하는 메서드 + * + * @param processorId 작업을 수행하는 멤버 ID + * @param request 순서 변경 요청 객체 + */ + @Override + @Transactional + public void updateTaskOrder(Long processorId, UpdateTaskOrderRequest request) { + // 요청 유효성 검증 + validateRequest(request, null); + Member processor = memberService.findActiveMember(processorId); + Task targetTask = taskService.findById(request.targetTaskId()); + + // 가장 상위로 이동 + if (request.prevTaskId() == 0) { + moveToTop(processorId, targetTask, request.nextTaskId()); + } + // 가장 하위로 이동 + else if (request.nextTaskId() == 0) { + moveToBottom(processorId, targetTask, request.prevTaskId()); + } else { + Task prevTask = findByIdAndStatus(request.prevTaskId(), targetTask.getTaskStatus()); + Task nextTask = findByIdAndStatus(request.nextTaskId(), targetTask.getTaskStatus()); + + // 새로운 작업 순서 업데이트 + updateNewTaskOrder(processor.getMemberId(), targetTask, prevTask.getProcessorOrder(), nextTask.getProcessorOrder()); + } + } + + /** + * 순서 변경 요청의 유효성을 검증하는 메서드 + */ + private static void validateRequest(UpdateTaskOrderRequest request, TaskStatus targetStatus) { + // 이전 및 다음 작업 ID가 모두 0인 경우 예외 발생 + if (request.prevTaskId() == 0 && request.nextTaskId() == 0) { + throw new ApplicationException(TaskErrorCode.INVALID_TASK_STATUS_TRANSITION); + } + + // 타겟 상태가 유효한지 검증 + if (targetStatus != null && !TaskStatus.getTaskBoardStatusList().contains(targetStatus)) { + throw new ApplicationException(TaskErrorCode.INVALID_TASK_STATUS_TRANSITION); + } + } + + /** + * 새로운 processorOrder를 계산하고 저장하는 메서드 + * + * @param processorId 작업을 수행하는 멤버 ID + * @param targetTask 순서를 변경할 대상 작업 + * @param prevTaskOrder 이전 작업의 processorOrder 값 + * @param nextTaskOrder 다음 작업의 processorOrder 값 + */ + private void updateNewTaskOrder(Long processorId, Task targetTask, Long prevTaskOrder, Long nextTaskOrder) { + targetTask.updateProcessorOrder(processorId, prevTaskOrder, nextTaskOrder); + commandTaskPort.save(targetTask); + } + + /** + * 작업을 가장 상위로 이동시키는 메서드 + * + * @param processorId 작업을 수행하는 멤버 ID + * @param targetTask 순서를 변경할 대상 작업 + * @param nextTaskId 다음 작업의 ID + */ + private void moveToTop(Long processorId, Task targetTask, Long nextTaskId) { + // 다음 작업 찾기 + Task nextTask = findByIdAndStatus(nextTaskId, targetTask.getTaskStatus()); + + // 해당 상태에서 바로 앞에 있는 작업 찾기 + Task prevTask = loadTaskPort.findPrevOrderTaskByProcessorIdAndStatus(processorId, targetTask.getTaskStatus(), nextTask.getProcessorOrder()).orElse(null); + + Long prevTaskOrder = prevTask == null ? null : prevTask.getProcessorOrder(); + + updateNewTaskOrder(processorId, targetTask, prevTaskOrder, nextTask.getProcessorOrder()); + } + + /** + * 작업을 가장 하위로 이동시키는 메서드 + * + * @param processorId 작업을 수행하는 멤버 ID + * @param targetTask 순서를 변경할 대상 작업 + * @param prevTaskId 이전 작업의 ID + */ + private void moveToBottom(Long processorId, Task targetTask, Long prevTaskId) { + // 이전 작업 찾기 + Task prevTask = findByIdAndStatus(prevTaskId, targetTask.getTaskStatus()); + + // 해당 상태에서 바로 뒤에 있는 작업 찾기 + Task nextTask = loadTaskPort.findNextOrderTaskByProcessorIdAndStatus(processorId, targetTask.getTaskStatus(), prevTask.getProcessorOrder()).orElse(null); + + Long nextTaskOrder = nextTask == null ? null : nextTask.getProcessorOrder(); + + updateNewTaskOrder(processorId, targetTask, prevTask.getProcessorOrder(), nextTaskOrder); + } + + /** + * 작업의 상태와 순서를 동시에 변경하는 메서드 + * + * @param processorId 작업을 수행하는 멤버 ID + * @param request 순서 변경 요청 객체 + * @param targetStatus 변경할 작업 상태 + */ + @Override + @Transactional + public void updateTaskOrderAndStatus(Long processorId, UpdateTaskOrderRequest request, TaskStatus targetStatus) { + validateRequest(request, targetStatus); + + Member processor = memberService.findActiveMember(processorId); + + Task targetTask = taskService.findById(request.targetTaskId()); + + if (request.prevTaskId() == 0) { + moveToTopWithStatusChange(processorId, targetTask, request.nextTaskId(), targetStatus); + } else if (request.nextTaskId() == 0) { + moveToBottomWithStatusChange(processorId, targetTask, request.prevTaskId(), targetStatus); + } else { + Task prevTask = findByIdAndStatus(request.prevTaskId(), targetStatus); + Task nextTask = findByIdAndStatus(request.nextTaskId(), targetStatus); + + updateNewTaskOrderAndStatus(targetStatus, targetTask, processor.getMemberId(), prevTask.getProcessorOrder(), nextTask.getProcessorOrder()); + } + } + + /** + * 작업의 상태와 순서를 업데이트하는 메서드 + */ + private void updateNewTaskOrderAndStatus(TaskStatus targetStatus, Task targetTask, Long processorId, Long prevTaskOrder, Long nextTaskOrder) { + targetTask.updateProcessorOrder(processorId, prevTaskOrder, nextTaskOrder); + targetTask.updateTaskStatus(targetStatus); + commandTaskPort.save(targetTask); + } + + /** + * 상태 변경을 포함하여 작업을 가장 상위로 이동하는 메서드 + */ + private void moveToTopWithStatusChange(Long processorId, Task targetTask, Long nextTaskId, TaskStatus targetStatus) { + Task nextTask = findByIdAndStatus(nextTaskId, targetStatus); + Task prevTask = loadTaskPort.findPrevOrderTaskByProcessorIdAndStatus(processorId, targetStatus, nextTask.getProcessorOrder()).orElse(null); + + Long prevTaskOrder = prevTask == null ? null : prevTask.getProcessorOrder(); + + updateNewTaskOrderAndStatus(targetStatus, targetTask, processorId, prevTaskOrder, nextTask.getProcessorOrder()); + } + + /** + * 상태 변경을 포함하여 작업을 가장 하위로 이동하는 메서드 + */ + private void moveToBottomWithStatusChange(Long processorId, Task targetTask, Long prevTaskId, TaskStatus targetStatus) { + Task prevTask = findByIdAndStatus(prevTaskId, targetStatus); + Task nextTask = loadTaskPort.findNextOrderTaskByProcessorIdAndStatus(processorId, targetStatus, prevTask.getProcessorOrder()).orElse(null); + + Long nextTaskOrder = nextTask == null ? null : nextTask.getProcessorOrder(); + + updateNewTaskOrderAndStatus(targetStatus, targetTask, processorId, prevTask.getProcessorOrder(), nextTaskOrder); + } +} + 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 251b2b92..b25e355f 100644 --- a/src/main/java/clap/server/application/service/task/UpdateTaskService.java +++ b/src/main/java/clap/server/application/service/task/UpdateTaskService.java @@ -5,6 +5,7 @@ import clap.server.application.mapper.AttachmentMapper; import clap.server.application.mapper.TaskMapper; import clap.server.application.port.inbound.domain.CategoryService; +import clap.server.application.port.inbound.domain.LabelService; import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.inbound.domain.TaskService; import clap.server.application.port.inbound.task.UpdateTaskLabelUsecase; @@ -14,14 +15,14 @@ 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.task.LoadLabelPort; 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.LabelErrorCode; -import clap.server.exception.code.MemberErrorCode; import clap.server.exception.code.TaskErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -29,9 +30,6 @@ import org.springframework.web.multipart.MultipartFile; import java.util.List; -import java.util.Objects; - -import static clap.server.exception.code.MemberErrorCode.ACTIVE_MEMBER_NOT_FOUND; @ApplicationService @@ -42,9 +40,10 @@ public class UpdateTaskService implements UpdateTaskUsecase, UpdateTaskStatusUse private final MemberService memberService; private final CategoryService categoryService; private final TaskService taskService; + private final CommandTaskPort commandTaskPort; private final LoadAttachmentPort loadAttachmentPort; - private final LoadLabelPort loadLabelPort; + private final LabelService labelService; private final CommandAttachmentPort commandAttachmentPort; private final S3UploadAdapter s3UploadAdapter; @@ -55,14 +54,10 @@ public UpdateTaskResponse updateTask(Long requesterId, Long taskId, UpdateTaskRe Category category = categoryService.findById(updateTaskRequest.categoryId()); Task task = taskService.findById(taskId); - if(!Objects.equals(requester.getMemberId(), task.getRequester().getMemberId())) { - throw new ApplicationException(TaskErrorCode.TASK_STATUS_MISMATCH); - } - - task.updateTask(task.getTaskStatus(), category, updateTaskRequest.title(), updateTaskRequest.description()); + task.updateTask(requesterId, category, updateTaskRequest.title(), updateTaskRequest.description()); Task updatedTask = commandTaskPort.save(task); - if (!updateTaskRequest.attachmentsToDelete().isEmpty()){ + if (!updateTaskRequest.attachmentsToDelete().isEmpty()) { updateAttachments(updateTaskRequest.attachmentsToDelete(), files, task); } return TaskMapper.toUpdateTaskResponse(updatedTask); @@ -83,11 +78,9 @@ public UpdateTaskResponse updateTaskState(Long memberId, Long taskId, UpdateTask @Transactional @Override public UpdateTaskResponse updateTaskProcessor(Long taskId, Long userId, UpdateTaskProcessorRequest request) { - Member reviewer = memberService.findActiveMember(userId); + Member reviewer = memberService.findReviewer(userId); Member processor = memberService.findById(request.processorId()); - if (!reviewer.isReviewer()) { - throw new ApplicationException(MemberErrorCode.NOT_A_REVIEWER); - } + Task task = taskService.findById(taskId); task.updateProcessor(processor); Task updateTask = commandTaskPort.save(task); @@ -99,12 +92,9 @@ public UpdateTaskResponse updateTaskProcessor(Long taskId, Long userId, UpdateTa @Transactional @Override public UpdateTaskResponse updateTaskLabel(Long taskId, Long userId, UpdateTaskLabelRequest request) { - Member reviewer = memberService.findActiveMember(userId); - if (!reviewer.isReviewer()) { - throw new ApplicationException(MemberErrorCode.NOT_A_REVIEWER); - } + Member reviewer = memberService.findReviewer(userId); Task task = taskService.findById(taskId); - Label label = loadLabelPort.findById(request.labelId()).orElseThrow(() -> new ApplicationException(LabelErrorCode.LABEL_NOT_FOUND)); + Label label = labelService.findById(request.labelId()); task.updateLabel(label); Task updatetask = commandTaskPort.save(task); @@ -115,14 +105,14 @@ private void updateAttachments(List attachmentIdsToDelete, List attachmentsToDelete = validateAndGetAttachments(attachmentIdsToDelete, task); attachmentsToDelete.forEach(Attachment::softDelete); - List fileUrls = s3UploadAdapter.uploadFiles(FilePath.TASK_IMAGE, files); + List fileUrls = s3UploadAdapter.uploadFiles(FilePathConstants.TASK_IMAGE, files); List attachments = AttachmentMapper.toTaskAttachments(task, files, fileUrls); commandAttachmentPort.saveAll(attachments); } private List validateAndGetAttachments(List attachmentIdsToDelete, Task task) { List attachmentsOfTask = loadAttachmentPort.findAllByTaskIdAndCommentIsNullAndAttachmentId(task.getTaskId(), attachmentIdsToDelete); - if(attachmentsOfTask.size() != attachmentIdsToDelete.size()) { + if (attachmentsOfTask.size() != attachmentIdsToDelete.size()) { throw new ApplicationException(TaskErrorCode.TASK_ATTACHMENT_NOT_FOUND); } return attachmentsOfTask; diff --git a/src/main/java/clap/server/domain/model/task/FilePath.java b/src/main/java/clap/server/common/constants/FilePathConstants.java similarity index 76% rename from src/main/java/clap/server/domain/model/task/FilePath.java rename to src/main/java/clap/server/common/constants/FilePathConstants.java index 6786b741..92393f90 100644 --- a/src/main/java/clap/server/domain/model/task/FilePath.java +++ b/src/main/java/clap/server/common/constants/FilePathConstants.java @@ -1,11 +1,11 @@ -package clap.server.domain.model.task; +package clap.server.common.constants; import lombok.Getter; import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor -public enum FilePath { +public enum FilePathConstants { TASK_IMAGE("task/image"), TASK_DOCUMENT("task/docs"), MEMBER_IMAGE("member/image"), diff --git a/src/main/java/clap/server/config/mail/EmailConfig.java b/src/main/java/clap/server/config/mail/EmailConfig.java new file mode 100644 index 00000000..b2a1b065 --- /dev/null +++ b/src/main/java/clap/server/config/mail/EmailConfig.java @@ -0,0 +1,43 @@ +package clap.server.config.mail; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +public class EmailConfig { + + @Value("${spring.mail.host}") + private String host; + + @Value("${spring.mail.port}") + private int port; + + @Value("${spring.mail.username}") + private String username; + + @Value("${spring.mail.password}") + private String password; + + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(host); + mailSender.setPort(port); + mailSender.setUsername(username); + mailSender.setPassword(password); + + Properties props = mailSender.getJavaMailProperties(); + props.put("mail.transport.protocol", "smtp"); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.debug", "true"); + + return mailSender; + } +} + 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 f55b3fcc..c1e2f682 100644 --- a/src/main/java/clap/server/domain/model/member/Member.java +++ b/src/main/java/clap/server/domain/model/member/Member.java @@ -1,6 +1,5 @@ package clap.server.domain.model.member; -import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; import clap.server.domain.model.common.BaseTime; import lombok.*; diff --git a/src/main/java/clap/server/domain/model/task/Task.java b/src/main/java/clap/server/domain/model/task/Task.java index 3b0b2814..efdfeba5 100644 --- a/src/main/java/clap/server/domain/model/task/Task.java +++ b/src/main/java/clap/server/domain/model/task/Task.java @@ -3,6 +3,7 @@ import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; import clap.server.domain.model.common.BaseTime; import clap.server.domain.model.member.Member; +import clap.server.exception.ApplicationException; import clap.server.exception.DomainException; import clap.server.exception.code.TaskErrorCode; import lombok.AccessLevel; @@ -14,6 +15,8 @@ import java.time.format.DateTimeFormatter; import java.util.Objects; +import static clap.server.domain.model.task.constants.TaskProcessorOrderPolicy.DEFAULT_PROCESSOR_ORDER_GAP; + @Getter @SuperBuilder @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -43,23 +46,33 @@ public static Task createTask(Member member, Category category, String title, St .build(); } - public void updateTask(TaskStatus status, Category category, String title, String description) { - if (status != TaskStatus.REQUESTED) { - throw new DomainException(TaskErrorCode.TASK_STATUS_MISMATCH); + public void updateTask(Long requesterId, Category category, String title, String description) { + if(!Objects.equals(requesterId, this.requester.getMemberId() )) { + throw new ApplicationException(TaskErrorCode.NOT_A_REQUESTER); } + validateTaskRequested(); this.category = category; this.title = title; this.description = description; this.taskCode = toTaskCode(category); } + public void validateTaskRequested() { + if (this.taskStatus != TaskStatus.REQUESTED) { + throw new DomainException(TaskErrorCode.TASK_STATUS_MISMATCH); + } + } + public void setInitialProcessorOrder() { - if(this.processor == null) { - this.processorOrder = this.taskId * 128L; + if (this.processor == null) { + this.processorOrder = this.taskId * DEFAULT_PROCESSOR_ORDER_GAP; } } public void updateTaskStatus(TaskStatus status) { + if (status == null) { + throw new DomainException(TaskErrorCode.INVALID_TASK_STATUS_TRANSITION); + } this.taskStatus = status; } @@ -80,7 +93,36 @@ public void approveTask(Member reviewer, Member processor, LocalDateTime dueDate this.taskStatus = TaskStatus.IN_PROGRESS; } - private static String toTaskCode(Category category){ + private static String toTaskCode(Category category) { return category.getMainCategory().getCode() + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmm")); } + + public void updateProcessorOrder(Long processorId, Long prevTaskOrder, Long nextTaskOrder) { + if (!Objects.equals(processorId, this.processor.getMemberId())) { + throw new DomainException(TaskErrorCode.NOT_A_PROCESSOR); + } + long newProcessorOrder; + + // 최상위 이동: 가장 작은 processorOrder보다 더 작은 값 설정 + if (prevTaskOrder == null && nextTaskOrder != null) { + newProcessorOrder = nextTaskOrder - DEFAULT_PROCESSOR_ORDER_GAP; + } + // 최하위 이동: 가장 큰 processorOrder보다 더 큰 값 설정 + else if (prevTaskOrder != null && nextTaskOrder == null) { + newProcessorOrder = prevTaskOrder + DEFAULT_PROCESSOR_ORDER_GAP; + } + // 중간 위치로 이동: prevTask와 nextTask의 processorOrder 평균값 사용 + else if (prevTaskOrder != null && nextTaskOrder != null) { + if (nextTaskOrder - prevTaskOrder < 2) { + throw new DomainException(TaskErrorCode.INVALID_TASK_ORDER); + } + newProcessorOrder = (prevTaskOrder + nextTaskOrder) / 2; + } + // 기본값 (예외적인 상황 방지) + else { + newProcessorOrder = DEFAULT_PROCESSOR_ORDER_GAP; + } + this.processorOrder = newProcessorOrder; + } + } diff --git a/src/main/java/clap/server/domain/model/task/constants/TaskProcessorOrderPolicy.java b/src/main/java/clap/server/domain/model/task/constants/TaskProcessorOrderPolicy.java new file mode 100644 index 00000000..78bc9cfc --- /dev/null +++ b/src/main/java/clap/server/domain/model/task/constants/TaskProcessorOrderPolicy.java @@ -0,0 +1,8 @@ +package clap.server.domain.model.task.constants; + +import lombok.Getter; + +@Getter +public class TaskProcessorOrderPolicy { + public static final long DEFAULT_PROCESSOR_ORDER_GAP = (long) Math.pow(2,6); +} \ No newline at end of file diff --git a/src/main/java/clap/server/exception/code/TaskErrorCode.java b/src/main/java/clap/server/exception/code/TaskErrorCode.java index 6d6ff5c7..8c8a2cf7 100644 --- a/src/main/java/clap/server/exception/code/TaskErrorCode.java +++ b/src/main/java/clap/server/exception/code/TaskErrorCode.java @@ -11,7 +11,12 @@ public enum TaskErrorCode implements BaseErrorCode { CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "TASK_002", "카테고리를 찾을 수 없습니다."), TASK_STATUS_MISMATCH(HttpStatus.BAD_REQUEST, "TASK_003", "작업 상태가 일치하지 않습니다."), TASK_ATTACHMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "TASK_004", "첨부파일을 찾을 수 없습니다."), - LABEL_NOT_FOUND(HttpStatus.NOT_FOUND, "TASK_005", "작업구분을 찾을 수 없습니다.") + LABEL_NOT_FOUND(HttpStatus.NOT_FOUND, "TASK_005", "작업구분을 찾을 수 없습니다."), + NOT_A_PROCESSOR(HttpStatus.FORBIDDEN, "TASK_006", "작업 처리 및 수정 권한이 없습니다."), + INVALID_TASK_ORDER(HttpStatus.INTERNAL_SERVER_ERROR, "TASK_007", "유효하지 않은 task order입니다."), + INVALID_TASK_STATUS_TRANSITION(HttpStatus.BAD_REQUEST, "TASK_008", "유효하지 않은 작업 상태 전환입니다."), + NOT_A_REVIEWER(HttpStatus.FORBIDDEN, "TASK_009", "작업 승인 및 수정 권한이 없습니다."), + NOT_A_REQUESTER(HttpStatus.FORBIDDEN, "TASK_009", "작업 수정 권한이 없습니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 713aff54..c9efe054 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,25 +9,11 @@ spring: - auth.yml - elasticsearch.yml - s3.yml - + - notifications.yml application: name: taskflow web.resources.add-mappings: false - # 구글 알림 이메일 발신자 정보 설정(논의 필요) - mail: - host: smtp.gmail.com - port: 587 - username: leegd120@gmail.com - password: znlictzarqurxlla - properties: - mail: - smtp: - auth: true - starttls: - enable: true - - server: port: ${APPLICATION_PORT:8080} @@ -45,15 +31,6 @@ web: local: ${TASKFLOW_LOCAL_WEB:127.0.0.1:5173} service: ${TASKFLOW_SERVICE_WEB:127.0.0.1:5173} - -# 카카오워크 및 agit url, accessKey값 환경 변수로 설정 -kakaowork: - url: https://api.kakaowork.com/v1/messages.send_by_email - auth: Bearer 1b01becc.a7f10da76d2e4038948771107cfe5c1d - -agit: - url: https://agit.io/webhook/a342181d-fb18-4eb0-a99a-30f4fb5b14b1 - local: ${TASKFLOW_LOCAL_WEB:127.0.0.1:5173} service: ${TASKFLOW_SERVICE_WEB:127.0.0.1:5173} @@ -83,4 +60,4 @@ logging: spring.config.activate.on-profile: prod logging: level: - root: INFO + root: INFO \ No newline at end of file diff --git a/src/main/resources/notifications.yml b/src/main/resources/notifications.yml new file mode 100644 index 00000000..e4ef655e --- /dev/null +++ b/src/main/resources/notifications.yml @@ -0,0 +1,20 @@ +#TODO 구글 알림 이메일 발신자 정보 설정(논의 필요) +spring: + mail: + host: smtp.gmail.com + port: 587 + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} + properties: + mail: + smtp: + auth: true + starttls: + enable: true + +webhook: + kakaowork: + url: ${KAKAOWORK_WEBHOOK_URL} + auth: ${KAKAOWORK_WEBHOOK_AUTH} + agit: + url: ${AGIT_WEBHOOK_URL} \ No newline at end of file diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index b10d6c61..470fc61a 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -58,11 +58,3 @@ password: length: 12 characters: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+ -kakaowork: - url: https://api.kakaowork.com/v1/messages.send_by_email - auth: Bearer 1b01becc.a7f10da76d2e4038948771107cfe5c1d - -agit: - url: https://agit.io/webhook/a342181d-fb18-4eb0-a99a-30f4fb5b14b1 - -