Skip to content
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taskid는 dto 선언이 아닌 path variable로 변경 부탁드립니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package clap.server.adapter.inbound.web.dto.task;

import jakarta.validation.constraints.NotNull;

import java.time.LocalDateTime;

public record ApprovalTaskRequest(
@NotNull
Long categoryId,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

카테고리 id(2차)만 받아도 충분할 거 같은데 어떻게 생각하시나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 그 방향이 나을 것 같습니다.

@NotNull
Long processorId,
@NotNull
LocalDateTime dueDate,
@NotNull
Long labelId

) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package clap.server.adapter.inbound.web.dto.task;

import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus;

import java.time.LocalDateTime;

public record ApprovalTaskResponse(
Long taskId,
String processorName,
String reviewerName,
LocalDateTime deadLine,
String labelName,
TaskStatus taskStatus
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ public record FilterTaskListRequest(
@NotNull
List<TaskStatus> taskStatus,

@Schema(description = "정렬 기준", implementation = OrderRequest.class)
@Schema(description = "정렬 기준", implementation = OrderTaskRequest.class)
@NotNull
OrderRequest orderRequest
OrderTaskRequest orderRequest
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import io.swagger.v3.oas.annotations.media.Schema;

public record OrderRequest(
public record OrderTaskRequest(
@Schema(description = "정렬 기준", example = "REQUESTED_AT")
String sortBy,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class FindTaskController {
private final FindTaskListUsecase taskListUsecase;

@Operation(summary = "사용자 요청 작업 목록 조회")
@Secured({"ROLE_USER"})
@Secured({"ROLE_USER", "ROLE_MANAGER"})
@GetMapping("/requests")
public ResponseEntity<Page<FilterTaskListResponse>> findTasksRequestedByUser(
@RequestParam(defaultValue = "0") int page,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package clap.server.adapter.inbound.web.task;

import clap.server.adapter.inbound.security.SecurityUserDetails;
import clap.server.adapter.inbound.web.dto.task.CreateTaskRequest;
import clap.server.adapter.inbound.web.dto.task.CreateTaskResponse;
import clap.server.adapter.inbound.web.dto.task.UpdateTaskRequest;
import clap.server.adapter.inbound.web.dto.task.UpdateTaskResponse;
import clap.server.adapter.inbound.web.dto.task.*;
import clap.server.application.port.inbound.task.ApprovalTaskUsecase;
import clap.server.application.port.inbound.task.CreateTaskUsecase;
import clap.server.application.port.inbound.task.UpdateTaskUsecase;
import clap.server.common.annotation.architecture.WebAdapter;
Expand All @@ -15,6 +13,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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;
Expand All @@ -31,9 +30,11 @@ public class ManagementTaskController {

private final CreateTaskUsecase createTaskUsecase;
private final UpdateTaskUsecase updateTaskUsecase;
private final ApprovalTaskUsecase approvalTaskUsecase;

@Operation(summary = "작업 요청 생성")
@PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
@Secured({"ROLE_MANAGER, ROLE_USER"})
public ResponseEntity<CreateTaskResponse> createTask(
@RequestPart(name = "taskInfo") @Valid CreateTaskRequest createTaskRequest,
@RequestPart(name = "attachment") @NotNull List<MultipartFile> attachments,
Expand All @@ -44,11 +45,22 @@ public ResponseEntity<CreateTaskResponse> createTask(

@Operation(summary = "작업 수정")
@PatchMapping(value = "/{taskId}", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
@Secured({"ROLE_MANAGER, ROLE_USER"})
public ResponseEntity<UpdateTaskResponse> updateTask(
@PathVariable @NotNull Long taskId,
@RequestPart(name = "taskInfo") @Valid UpdateTaskRequest updateTaskRequest,
@RequestPart(name = "attachment") @NotNull List<MultipartFile> attachments,
@AuthenticationPrincipal SecurityUserDetails userInfo){
return ResponseEntity.ok(updateTaskUsecase.updateTask(userInfo.getUserId(), taskId, updateTaskRequest, attachments));
}

@Operation(summary = "작업 승인")
@Secured({"ROLE_MANAGER"})
@PostMapping("/approval/{taskId}")
public ResponseEntity<ApprovalTaskResponse> approvalTask(
@RequestBody @Valid ApprovalTaskRequest approvalTaskRequest,
@PathVariable Long taskId,
@AuthenticationPrincipal SecurityUserDetails userInfo){
return ResponseEntity.ok(approvalTaskUsecase.approvalTaskByReviewer(userInfo.getUserId(), taskId, approvalTaskRequest));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,5 @@ public List<Task> findTasksByMemberIdAndStatus(Long memberId, List<TaskStatus> t
.map(taskPersistenceMapper::toDomain)
.collect(Collectors.toList());
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import clap.server.adapter.outbound.persistense.entity.common.BaseTimeEntity;
import clap.server.adapter.outbound.persistense.entity.member.MemberEntity;
import clap.server.adapter.outbound.persistense.entity.task.constant.LabelType;
import clap.server.adapter.outbound.persistense.entity.task.constant.LabelColor;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
Expand All @@ -24,9 +24,12 @@ public class LabelEntity extends BaseTimeEntity {
@JoinColumn(name = "admin_id", nullable = false)
private MemberEntity admin;

@Column(nullable = false)
private String labelName;

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private LabelType labelType;
private LabelColor labelColor;

@Column(nullable = false)
private boolean isDeleted;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@

@Getter
@RequiredArgsConstructor
public enum LabelType {
EMERGENCY("긴급"),
NORMAL("일반"),
REGULAR("정기");
public enum LabelColor {
RED("빨강"),
ORANGE("주황"),
YELLOW("노랑"),
GREEN("초록"),
BLUE("파랑"),
PURPLE("보라"),
GREY("회색");

private final String description;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package clap.server.adapter.outbound.persistense.repository;

import clap.server.adapter.outbound.persistense.entity.task.LabelEntity;
import clap.server.adapter.outbound.persistense.entity.task.TaskEntity;
import clap.server.adapter.outbound.persistense.mapper.LabelPersistenceMapper;
import clap.server.adapter.outbound.persistense.repository.task.LabelRepository;
import clap.server.application.port.outbound.task.LoadLabelPort;
import clap.server.common.annotation.architecture.PersistenceAdapter;
import clap.server.domain.model.task.Label;
import lombok.RequiredArgsConstructor;

import java.util.Optional;

@PersistenceAdapter
@RequiredArgsConstructor
public class LabelPersistenceAdapter implements LoadLabelPort {

private final LabelRepository labelRepository;
private final LabelPersistenceMapper labelPersistenceMapper;

@Override
public Optional<Label> findById(Long id) {
Optional<LabelEntity> labelEntity = labelRepository.findById(id);
return labelEntity.map(labelPersistenceMapper::toDomain);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package clap.server.application.Task;

import clap.server.adapter.inbound.web.dto.task.ApprovalTaskRequest;
import clap.server.adapter.inbound.web.dto.task.ApprovalTaskResponse;
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.ApprovalTaskUsecase;
import clap.server.application.port.outbound.task.CommandTaskPort;
import clap.server.common.annotation.architecture.ApplicationService;
import clap.server.domain.model.member.Member;
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;

@ApplicationService
@RequiredArgsConstructor
public class ApprovalTaskService implements ApprovalTaskUsecase {

private final MemberService memberService;
private final TaskService taskService;
private final CategoryService categoryService;
private final LabelService labelService;
private final CommandTaskPort commandTaskPort;

@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);
}
Task task = taskService.findById(taskId);
Member processor = memberService.findById(approvalTaskRequest.processorId());
Category category = categoryService.findById(approvalTaskRequest.categoryId());
Label label = labelService.findById(approvalTaskRequest.labelId());

task.approveTask(reviewer, processor, approvalTaskRequest.dueDate(), category, label);
return TaskMapper.toApprovalTaskResponse(commandTaskPort.save(task));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import clap.server.adapter.inbound.web.dto.task.CreateTaskRequest;
import clap.server.adapter.inbound.web.dto.task.CreateTaskResponse;

import clap.server.adapter.outbound.infrastructure.s3.S3UploadAdapter;
import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType;
import clap.server.application.mapper.AttachmentMapper;
Expand All @@ -11,6 +12,7 @@
import clap.server.application.port.inbound.task.CreateTaskUsecase;
import clap.server.application.port.outbound.task.CommandAttachmentPort;
import clap.server.application.port.outbound.task.CommandTaskPort;

import clap.server.common.annotation.architecture.ApplicationService;
import clap.server.domain.model.member.Member;
import clap.server.domain.model.notification.Notification;
Expand All @@ -19,6 +21,7 @@
import clap.server.domain.model.task.FilePath;
import clap.server.domain.model.task.Task;
import lombok.RequiredArgsConstructor;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import clap.server.adapter.inbound.web.dto.task.UpdateTaskRequest;
import clap.server.adapter.inbound.web.dto.task.UpdateTaskResponse;
import clap.server.adapter.outbound.infrastructure.s3.S3UploadAdapter;
import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus;
import clap.server.application.mapper.AttachmentMapper;
import clap.server.application.mapper.TaskMapper;
import clap.server.application.port.inbound.domain.CategoryService;
Expand All @@ -13,10 +14,12 @@
import clap.server.application.port.outbound.task.CommandTaskPort;
import clap.server.application.port.outbound.task.LoadAttachmentPort;
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.Category;
import clap.server.domain.model.task.FilePath;
import clap.server.domain.model.task.Task;

import clap.server.exception.ApplicationException;
import clap.server.exception.code.TaskErrorCode;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -47,7 +50,9 @@ public UpdateTaskResponse updateTask(Long requesterId, Long taskId, UpdateTaskRe
memberService.findActiveMember(requesterId);
Category category = categoryService.findById(updateTaskRequest.categoryId());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

승인 권한이 있는 담당자만 가능하도록 해야해서, SPEL을 사용하여 메서드 단위로 승인을 하거나 MEMBER의 리뷰어 권한을 조회해서 예외 처리 추가하시면 될거같아요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 이해한게 맞다면 말씀하신 부분은 approvalTaskByReviewer()메서드에 추가되어있는데 확인한번 부탁드립니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 승인과 수정이 분리되어야하네요! 네 확인입니다:)

Copy link
Contributor

@joowojr joowojr Jan 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정시에도 요청자나 담당자와 일치하는지 검증하는거 필요할거같아요!

Task task = taskService.findById(taskId);
//TODO: 작업이 요청 상태인 경우만 업데이트 가능
if(task.getTaskStatus() != TaskStatus.REQUESTED){
throw new ApplicationException(TaskErrorCode.TASK_STATUS_MISMATCH);
}
task.updateTask(category, updateTaskRequest.title(), updateTaskRequest.description());
Task updatedTask = commandTaskPort.save(task);

Expand Down
11 changes: 11 additions & 0 deletions src/main/java/clap/server/application/mapper/TaskMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,15 @@ public static FindTaskDetailsResponse toFindTaskDetailResponse(Task task, List<A
attachmentResponses
);
}

public static ApprovalTaskResponse toApprovalTaskResponse(Task approvedTask) {
return new ApprovalTaskResponse(
approvedTask.getTaskId(),
approvedTask.getProcessor().getNickname(),
approvedTask.getReviewer().getNickname(),
approvedTask.getDueDate(),
approvedTask.getLabel().getLabelName(),
approvedTask.getTaskStatus()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package clap.server.application.port.inbound.domain;

import clap.server.application.port.outbound.task.LoadLabelPort;
import clap.server.domain.model.task.Label;
import clap.server.exception.ApplicationException;
import clap.server.exception.code.TaskErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LabelService {

private final LoadLabelPort loadLabelPort;

public Label findById(Long labelId) {
return loadLabelPort.findById(labelId).orElseThrow(
()-> new ApplicationException(TaskErrorCode.LABEL_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package clap.server.application.port.inbound.task;

import clap.server.adapter.inbound.web.dto.task.ApprovalTaskRequest;
import clap.server.adapter.inbound.web.dto.task.ApprovalTaskResponse;

public interface ApprovalTaskUsecase {
ApprovalTaskResponse approvalTaskByReviewer(Long userId, Long taskId, ApprovalTaskRequest approvalTaskRequest);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package clap.server.application.port.outbound.task;

import clap.server.domain.model.task.Label;

import java.util.Optional;

public interface LoadLabelPort {

Optional<Label> findById(Long id);
}
3 changes: 3 additions & 0 deletions src/main/java/clap/server/domain/model/member/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,7 @@ public String getNickname() {
return memberInfo != null ? memberInfo.getNickname() : null;
}

public boolean isReviewer() {
return this.memberInfo != null && this.memberInfo.isReviewer();
}
}
Loading
Loading