diff --git a/src/main/java/clap/server/adapter/inbound/web/admin/FindMemberController.java b/src/main/java/clap/server/adapter/inbound/web/admin/FindMemberController.java index 0c487daa..94f3607b 100644 --- a/src/main/java/clap/server/adapter/inbound/web/admin/FindMemberController.java +++ b/src/main/java/clap/server/adapter/inbound/web/admin/FindMemberController.java @@ -3,8 +3,8 @@ import clap.server.adapter.inbound.web.dto.admin.FindMemberRequest; import clap.server.adapter.inbound.web.dto.admin.RetrieveAllMemberResponse; import clap.server.application.mapper.RetrieveAllMemberMapper; -import clap.server.application.port.inbound.management.FindAllMembersUsecase; -import clap.server.application.port.inbound.management.FindMembersWithFilterUsecase; +import clap.server.application.port.inbound.admin.FindAllMembersUsecase; +import clap.server.application.port.inbound.admin.FindMembersWithFilterUsecase; import clap.server.domain.model.member.Member; import clap.server.exception.ApplicationException; import clap.server.exception.code.MemberErrorCode; 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 b45e9264..954fa0a5 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 @@ -8,6 +8,7 @@ import clap.server.application.port.inbound.task.FilterTaskBoardUsecase; import clap.server.application.port.inbound.task.UpdateTaskBoardUsecase; import clap.server.application.port.inbound.task.GetTaskBoardUsecase; +import clap.server.application.port.inbound.task.UpdateTaskOrderAndStatusUsecase; import clap.server.common.annotation.architecture.WebAdapter; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -35,15 +36,16 @@ public class TaskBoardController { private final GetTaskBoardUsecase getTaskBoardUsecase; private final FilterTaskBoardUsecase filterTaskBoardUsecase; private final UpdateTaskBoardUsecase updateTaskBoardUsecase; + private final UpdateTaskOrderAndStatusUsecase updateTaskOrderAndStatus; @Operation(summary = "작업 보드 조회 API") @Secured({"ROLE_MANAGER"}) - @PostMapping + @GetMapping public ResponseEntity getTaskBoard(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int pageSize, - @Parameter(description = "완료 일자 조회 기준, yyyy-mm-dd 형식으로 입력합니다.") @RequestParam(required = false) + @Parameter(description = "작업 완료 일자 조회 기준, yyyy-mm-dd 형식으로 입력합니다.") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate untilDate, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "필터링 조회 request") @RequestBody(required = false) FilterTaskBoardRequest request, + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "필터링 조회 request") @ModelAttribute FilterTaskBoardRequest request, @AuthenticationPrincipal SecurityUserDetails userInfo) { Pageable pageable = PageRequest.of(page, pageSize); if (request != null) { @@ -62,7 +64,7 @@ public void updateTaskBoard(@Parameter(description = "전환될 작업의 상태 if (status == null) { updateTaskBoardUsecase.updateTaskOrder(userInfo.getUserId(), request); } else { - updateTaskBoardUsecase.updateTaskOrderAndStatus(userInfo.getUserId(), request, status); + updateTaskOrderAndStatus.updateTaskOrderAndStatus(userInfo.getUserId(), request, status); } } 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 38718363..5b36e33a 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java @@ -8,38 +8,20 @@ import clap.server.application.port.outbound.member.CommandMemberPort; import clap.server.application.port.outbound.member.LoadMemberPort; import clap.server.common.annotation.architecture.PersistenceAdapter; -import clap.server.domain.model.task.Task; -import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus ; -import clap.server.adapter.outbound.persistense.entity.task.TaskEntity; -import clap.server.adapter.outbound.persistense.repository.task.TaskRepository; -import clap.server.adapter.outbound.persistense.mapper.TaskPersistenceMapper; - -import java.util.stream.Collectors; -import java.util.List; - import clap.server.domain.model.member.Member; -import com.querydsl.core.BooleanBuilder; -import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; -import static clap.server.adapter.outbound.persistense.entity.member.QMemberEntity.memberEntity; - @PersistenceAdapter @RequiredArgsConstructor - public class MemberPersistenceAdapter implements LoadMemberPort, CommandMemberPort { +public class MemberPersistenceAdapter implements LoadMemberPort, CommandMemberPort { private final MemberRepository memberRepository; private final MemberPersistenceMapper memberPersistenceMapper; - private final TaskRepository taskRepository; - private final TaskPersistenceMapper taskPersistenceMapper; - private final JPAQueryFactory jpaQueryFactory; - @Override public Optional findById(final Long id) { @@ -82,61 +64,13 @@ public void save(final Member member) { @Override public Page findAllMembers(Pageable pageable) { - return executeQueryWithPageable(pageable, new BooleanBuilder().and(memberEntity.status.ne(MemberStatus.DELETED))); + return memberRepository.findAllMembers(pageable).map(memberPersistenceMapper::toDomain); } @Override public Page findMembersWithFilter(Pageable pageable, FindMemberRequest filterRequest) { - BooleanBuilder whereClause = createMemberFilter(filterRequest); - return executeQueryWithPageable(pageable, whereClause); - } - - // 공통 쿼리 처리 - private Page executeQueryWithPageable(Pageable pageable, BooleanBuilder whereClause) { - List entities = jpaQueryFactory - .selectFrom(memberEntity) - .where(whereClause) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - long total = jpaQueryFactory - .select(memberEntity.count()) - .from(memberEntity) - .where(whereClause) - .fetchOne(); - - return new PageImpl<>( - entities.stream() - .map(memberPersistenceMapper::toDomain) - .toList(), - pageable, - total - ); - } - - // 필터 조건 생성 - private BooleanBuilder createMemberFilter(FindMemberRequest filterRequest) { - BooleanBuilder whereClause = new BooleanBuilder(); - whereClause.and(memberEntity.status.ne(MemberStatus.DELETED)); - - if (filterRequest.name() != null) { - whereClause.and(memberEntity.name.containsIgnoreCase(filterRequest.name())); - } - if (filterRequest.email() != null) { - whereClause.and(memberEntity.email.containsIgnoreCase(filterRequest.email())); - } - if (filterRequest.nickName() != null) { - whereClause.and(memberEntity.nickname.containsIgnoreCase(filterRequest.nickName())); - } - if (filterRequest.departmentName() != null) { - whereClause.and(memberEntity.department.name.containsIgnoreCase(filterRequest.departmentName())); - } - if (filterRequest.role() != null) { - whereClause.and(memberEntity.role.eq(filterRequest.role())); - } + return memberRepository.findMembersWithFilter(pageable, filterRequest).map(memberPersistenceMapper::toDomain); - return whereClause; } } 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 3eb78723..c2990dfd 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,11 +1,8 @@ package clap.server.adapter.outbound.persistense.entity.task.constant; -import com.fasterxml.jackson.annotation.JsonCreator; import lombok.Getter; import lombok.RequiredArgsConstructor; -import java.util.List; - @Getter @RequiredArgsConstructor public enum TaskStatus { @@ -16,11 +13,4 @@ public enum TaskStatus { TERMINATED("종료"); private final String 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/MemberCustomRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberCustomRepository.java new file mode 100644 index 00000000..f6eff057 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberCustomRepository.java @@ -0,0 +1,11 @@ +package clap.server.adapter.outbound.persistense.repository.member; + +import clap.server.adapter.inbound.web.dto.admin.FindMemberRequest; +import clap.server.adapter.outbound.persistense.entity.member.MemberEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface MemberCustomRepository { + Page findAllMembers(Pageable pageable); + Page findMembersWithFilter(Pageable pageable, FindMemberRequest filterRequest) ; +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberCustomRepositoryImpl.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberCustomRepositoryImpl.java new file mode 100644 index 00000000..f9ed9a79 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberCustomRepositoryImpl.java @@ -0,0 +1,78 @@ +package clap.server.adapter.outbound.persistense.repository.member; + +import clap.server.adapter.inbound.web.dto.admin.FindMemberRequest; +import clap.server.adapter.outbound.persistense.entity.member.MemberEntity; +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static clap.server.adapter.outbound.persistense.entity.member.QMemberEntity.memberEntity; + +@Repository +@RequiredArgsConstructor +public class MemberCustomRepositoryImpl implements MemberCustomRepository { + private final JPAQueryFactory queryFactory; + + private Page executeQueryWithPageable(Pageable pageable, BooleanBuilder whereClause) { + List result = queryFactory + .selectFrom(memberEntity) + .where(whereClause) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + long total = queryFactory + .select(memberEntity.count()) + .from(memberEntity) + .where(whereClause) + .fetch().size(); + + return new PageImpl<>( + result, + pageable, + total + ); + } + + // 필터 조건 생성 + private BooleanBuilder createMemberFilter(FindMemberRequest filterRequest) { + BooleanBuilder whereClause = new BooleanBuilder(); + whereClause.and(memberEntity.status.ne(MemberStatus.DELETED)); + + if (filterRequest.name() != null) { + whereClause.and(memberEntity.name.containsIgnoreCase(filterRequest.name())); + } + if (filterRequest.email() != null) { + whereClause.and(memberEntity.email.containsIgnoreCase(filterRequest.email())); + } + if (filterRequest.nickName() != null) { + whereClause.and(memberEntity.nickname.containsIgnoreCase(filterRequest.nickName())); + } + if (filterRequest.departmentName() != null) { + whereClause.and(memberEntity.department.name.eq(filterRequest.departmentName())); + } + if (filterRequest.role() != null) { + whereClause.and(memberEntity.role.eq(filterRequest.role())); + } + + return whereClause; + } + + @Override + public Page findAllMembers(Pageable pageable) { + return executeQueryWithPageable(pageable, new BooleanBuilder().and(memberEntity.status.ne(MemberStatus.DELETED))); + } + + @Override + public Page findMembersWithFilter(Pageable pageable, FindMemberRequest filterRequest) { + BooleanBuilder whereClause = createMemberFilter(filterRequest); + return executeQueryWithPageable(pageable, whereClause); + } +} 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 3e186438..fc8899a4 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 @@ -4,14 +4,13 @@ import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository -public interface MemberRepository extends JpaRepository, JpaSpecificationExecutor { +public interface MemberRepository extends JpaRepository, MemberCustomRepository { List findByRoleAndStatus(MemberRole role, MemberStatus status); @@ -23,6 +22,9 @@ public interface MemberRepository extends JpaRepository, Jpa List findByIsReviewerTrue(); + List findAll(); // 전체 회원 조회 + + Optional findByMemberIdAndIsReviewerTrue(Long memberId); } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepositoryImpl.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepositoryImpl.java index e2b36970..600c27d8 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepositoryImpl.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepositoryImpl.java @@ -18,7 +18,8 @@ import java.util.List; import static clap.server.adapter.outbound.persistense.entity.task.QTaskEntity.taskEntity; -import static com.querydsl.core.types.Order.*; +import static com.querydsl.core.types.Order.ASC; +import static com.querydsl.core.types.Order.DESC; @Repository @RequiredArgsConstructor diff --git a/src/main/java/clap/server/application/mapper/AttachmentMapper.java b/src/main/java/clap/server/application/mapper/AttachmentMapper.java index 56d516de..cb0326c7 100644 --- a/src/main/java/clap/server/application/mapper/AttachmentMapper.java +++ b/src/main/java/clap/server/application/mapper/AttachmentMapper.java @@ -28,18 +28,6 @@ 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() diff --git a/src/main/java/clap/server/application/port/inbound/admin/FindAllMembersUsecase.java b/src/main/java/clap/server/application/port/inbound/admin/FindAllMembersUsecase.java new file mode 100644 index 00000000..b06a30a9 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/admin/FindAllMembersUsecase.java @@ -0,0 +1,9 @@ +package clap.server.application.port.inbound.admin; + +import clap.server.domain.model.member.Member; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface FindAllMembersUsecase { + Page findAllMembers(Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/clap/server/application/port/inbound/admin/FindMembersWithFilterUsecase.java b/src/main/java/clap/server/application/port/inbound/admin/FindMembersWithFilterUsecase.java new file mode 100644 index 00000000..4f263b51 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/admin/FindMembersWithFilterUsecase.java @@ -0,0 +1,10 @@ +package clap.server.application.port.inbound.admin; + +import clap.server.adapter.inbound.web.dto.admin.FindMemberRequest; +import clap.server.domain.model.member.Member; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface FindMembersWithFilterUsecase { + Page findMembersWithFilter(Pageable pageable, FindMemberRequest filterRequest); +} 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 001bf53e..b8396065 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 @@ -1,6 +1,5 @@ package clap.server.application.port.inbound.domain; -import clap.server.application.port.outbound.member.CommandMemberPort; import clap.server.application.port.outbound.member.LoadMemberPort; import clap.server.domain.model.member.Member; import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; @@ -15,8 +14,6 @@ @RequiredArgsConstructor public class MemberService { private final LoadMemberPort loadMemberPort; - private final CommandMemberPort commandMemberPort; - public Member findById(Long memberId) { return loadMemberPort.findById(memberId).orElseThrow( 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 index 3a7ebf9a..467f6d0b 100644 --- a/src/main/java/clap/server/application/port/inbound/task/UpdateTaskBoardUsecase.java +++ b/src/main/java/clap/server/application/port/inbound/task/UpdateTaskBoardUsecase.java @@ -1,9 +1,7 @@ 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/inbound/task/UpdateTaskOrderAndStatusUsecase.java b/src/main/java/clap/server/application/port/inbound/task/UpdateTaskOrderAndStatusUsecase.java new file mode 100644 index 00000000..a5a8c132 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/task/UpdateTaskOrderAndStatusUsecase.java @@ -0,0 +1,8 @@ +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 UpdateTaskOrderAndStatusUsecase { + 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 164aa81a..5b63970c 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 @@ -2,8 +2,8 @@ import clap.server.adapter.inbound.web.dto.admin.FindMemberRequest; import clap.server.domain.model.member.Member; -import clap.server.domain.model.task.Task; -import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import clap.server.domain.model.task.Task; // Task 클래스 임포트 확인 +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; // TaskStatus 임포트 import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -15,6 +15,8 @@ public interface LoadMemberPort { Optional findActiveMemberById(Long id); + Optional findReviewerById(Long id); + Optional findByNickname(String nickname); List findReviewers(); @@ -23,6 +25,5 @@ public interface LoadMemberPort { Page findMembersWithFilter(Pageable pageable, FindMemberRequest filterRequest); - Optional findReviewerById(Long id); } diff --git a/src/main/java/clap/server/application/service/member/FindAllMembersService.java b/src/main/java/clap/server/application/service/admin/FindAllMembersService.java similarity index 82% rename from src/main/java/clap/server/application/service/member/FindAllMembersService.java rename to src/main/java/clap/server/application/service/admin/FindAllMembersService.java index 4467bd84..55874fb3 100644 --- a/src/main/java/clap/server/application/service/member/FindAllMembersService.java +++ b/src/main/java/clap/server/application/service/admin/FindAllMembersService.java @@ -1,6 +1,6 @@ -package clap.server.application.service.member; +package clap.server.application.service.admin; -import clap.server.application.port.inbound.management.FindAllMembersUsecase; +import clap.server.application.port.inbound.admin.FindAllMembersUsecase; import clap.server.application.port.outbound.member.LoadMemberPort; import clap.server.domain.model.member.Member; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/clap/server/application/service/member/FindMembersWithFilterService.java b/src/main/java/clap/server/application/service/admin/FindMembersWithFilterService.java similarity index 84% rename from src/main/java/clap/server/application/service/member/FindMembersWithFilterService.java rename to src/main/java/clap/server/application/service/admin/FindMembersWithFilterService.java index c13176e7..529ce8ec 100644 --- a/src/main/java/clap/server/application/service/member/FindMembersWithFilterService.java +++ b/src/main/java/clap/server/application/service/admin/FindMembersWithFilterService.java @@ -1,7 +1,7 @@ -package clap.server.application.service.member; +package clap.server.application.service.admin; import clap.server.adapter.inbound.web.dto.admin.FindMemberRequest; -import clap.server.application.port.inbound.management.FindMembersWithFilterUsecase; +import clap.server.application.port.inbound.admin.FindMembersWithFilterUsecase; import clap.server.application.port.outbound.member.LoadMemberPort; import clap.server.domain.model.member.Member; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/clap/server/application/service/auth/ManageTokenService.java b/src/main/java/clap/server/application/service/auth/ManageTokenService.java index fecfdc5a..5a029d2d 100644 --- a/src/main/java/clap/server/application/service/auth/ManageTokenService.java +++ b/src/main/java/clap/server/application/service/auth/ManageTokenService.java @@ -11,14 +11,14 @@ import clap.server.domain.model.member.Member; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import java.time.Duration; import java.time.LocalDateTime; import java.util.function.Function; @RequiredArgsConstructor -@Component +@Service @Slf4j class ManageTokenService { private final JwtProvider accessTokenProvider; diff --git a/src/main/java/clap/server/application/service/comment/PostCommentService.java b/src/main/java/clap/server/application/service/comment/PostCommentService.java index e493598a..8ec5f262 100644 --- a/src/main/java/clap/server/application/service/comment/PostCommentService.java +++ b/src/main/java/clap/server/application/service/comment/PostCommentService.java @@ -1,19 +1,18 @@ package clap.server.application.service.comment; import clap.server.adapter.inbound.web.dto.comment.CreateCommentRequest; -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.notification.constant.NotificationType; import clap.server.adapter.outbound.persistense.entity.task.constant.TaskHistoryType; -import clap.server.application.mapper.AttachmentMapper; import clap.server.application.port.inbound.comment.SaveCommentAttachmentUsecase; import clap.server.application.port.inbound.comment.SaveCommentUsecase; import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.inbound.domain.TaskService; +import clap.server.application.port.outbound.s3.S3UploadPort; 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.application.service.webhook.SendPushNotificationService; +import clap.server.application.service.webhook.SendNotificationService; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.common.constants.FilePathConstants; import clap.server.domain.model.member.Member; @@ -25,10 +24,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.util.ArrayList; -import java.util.List; - - @ApplicationService @RequiredArgsConstructor public class PostCommentService implements SaveCommentUsecase, SaveCommentAttachmentUsecase { @@ -36,10 +31,10 @@ public class PostCommentService implements SaveCommentUsecase, SaveCommentAttach private final MemberService memberService; private final TaskService taskService; private final CommandCommentPort commandCommentPort; - private final S3UploadAdapter s3UploadAdapter; + private final S3UploadPort s3UploadPort; private final CommandAttachmentPort commandAttachmentPort; private final CommandTaskHistoryPort commandTaskHistoryPort; - private final SendPushNotificationService sendPushNotificationService; + private final SendNotificationService sendNotificationService; @Transactional @Override @@ -58,8 +53,7 @@ public void save(Long userId, Long taskId, CreateCommentRequest request) { if (member.getMemberInfo().getRole() == MemberRole.ROLE_USER) { publishNotification(task.getProcessor(), task, comment.getContent(), member.getNickname()); - } - else { + } else { publishNotification(task.getRequester(), task, comment.getContent(), task.getProcessor().getNickname()); } } @@ -74,29 +68,27 @@ public void saveCommentAttachment(Long userId, Long taskId, MultipartFile file) if (Member.checkCommenter(task, member)) { Comment comment = Comment.createComment(member, task, null); Comment savedComment = commandCommentPort.saveComment(comment); - List files = new ArrayList<>(); - files.add(file); - saveAttachment(files, task, savedComment); + saveAttachment(file, task, savedComment); TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.COMMENT_FILE, task, null, member, savedComment); commandTaskHistoryPort.save(taskHistory); if (member.getMemberInfo().getRole() == MemberRole.ROLE_USER) { publishNotification(task.getProcessor(), task, "첨부파일", member.getNickname()); - } - else { + } else { publishNotification(task.getRequester(), task, "첨부파일", task.getProcessor().getNickname()); } } } - 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); + private void saveAttachment(MultipartFile file, Task task, Comment comment) { + String fileUrl = s3UploadPort.uploadSingleFile(FilePathConstants.TASK_COMMENT, file); + Attachment attachment = Attachment.createCommentAttachment(task, comment, file.getOriginalFilename(), fileUrl, file.getSize()); + commandAttachmentPort.save(attachment); } - private void publishNotification(Member receiver, Task task, String message, String commenterName){ - sendPushNotificationService.sendPushNotification(receiver, NotificationType.COMMENT, task, message, commenterName); + private void publishNotification(Member receiver, Task task, String message, String commenterName) { + sendNotificationService.sendPushNotification(receiver, NotificationType.COMMENT, task, message, commenterName); } + } 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 3dc9d7bc..74d3b13e 100644 --- a/src/main/java/clap/server/application/service/task/ApprovalTaskService.java +++ b/src/main/java/clap/server/application/service/task/ApprovalTaskService.java @@ -13,20 +13,19 @@ import clap.server.application.port.inbound.task.ApprovalTaskUsecase; import clap.server.application.port.outbound.task.CommandTaskPort; import clap.server.application.port.outbound.taskhistory.CommandTaskHistoryPort; -import clap.server.application.service.webhook.SendPushNotificationService; +import clap.server.application.service.webhook.SendNotificationService; 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.domain.model.task.TaskHistory; +import clap.server.domain.policy.task.RequestedTaskUpdatePolicy; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.List; - @ApplicationService @RequiredArgsConstructor @Transactional(readOnly = true) @@ -37,8 +36,9 @@ public class ApprovalTaskService implements ApprovalTaskUsecase { private final CategoryService categoryService; private final LabelService labelService; private final CommandTaskPort commandTaskPort; + private final RequestedTaskUpdatePolicy requestedTaskUpdatePolicy; private final CommandTaskHistoryPort commandTaskHistoryPort; - private final SendPushNotificationService sendPushNotificationService; + private final SendNotificationService sendNotificationService; @Override @Transactional @@ -49,15 +49,14 @@ public ApprovalTaskResponse approvalTaskByReviewer(Long reviewerId, Long taskId, Category category = categoryService.findById(approvalTaskRequest.categoryId()); Label label = labelService.findById(approvalTaskRequest.labelId()); + requestedTaskUpdatePolicy.validateTaskRequested(task); task.approveTask(reviewer, processor, approvalTaskRequest.dueDate(), category, label); TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.PROCESSOR_ASSIGNED, task, null, processor,null); commandTaskHistoryPort.save(taskHistory); - List receivers = new ArrayList<>(); - receivers.add(task.getRequester()); - receivers.add(task.getProcessor()); - + List receivers = List.of(reviewer, processor); publishNotification(receivers, task); + return TaskMapper.toApprovalTaskResponse(commandTaskPort.save(task)); } @@ -65,15 +64,15 @@ public ApprovalTaskResponse approvalTaskByReviewer(Long reviewerId, Long taskId, public FindApprovalFormResponse findApprovalForm(Long managerId, Long taskId) { memberService.findActiveMember(managerId); Task task = taskService.findById(taskId); - task.validateTaskRequested(); + requestedTaskUpdatePolicy.validateTaskRequested(task); return TaskMapper.toFindApprovalFormResponse(task); } private void publishNotification(List receivers, Task task){ - for (Member receiver : receivers) { - - sendPushNotificationService.sendPushNotification(receiver, NotificationType.PROCESSOR_ASSIGNED, + receivers.forEach(receiver -> { + sendNotificationService.sendPushNotification(receiver, NotificationType.PROCESSOR_ASSIGNED, task, task.getProcessor().getNickname(), null); - } + }); } + } 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 32899443..174d5b16 100644 --- a/src/main/java/clap/server/application/service/task/CreateTaskService.java +++ b/src/main/java/clap/server/application/service/task/CreateTaskService.java @@ -10,24 +10,23 @@ import clap.server.application.port.inbound.domain.CategoryService; import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.inbound.task.CreateTaskUsecase; +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.service.webhook.SendPushNotificationService; +import clap.server.application.service.webhook.SendNotificationService; 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.Category; -import clap.server.common.constants.FilePathConstants; 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; import java.util.List; + @ApplicationService @RequiredArgsConstructor public class CreateTaskService implements CreateTaskUsecase { @@ -36,9 +35,8 @@ public class CreateTaskService implements CreateTaskUsecase { private final CategoryService categoryService; private final CommandTaskPort commandTaskPort; private final CommandAttachmentPort commandAttachmentPort; - private final S3UploadAdapter s3UploadAdapter; - private final SendPushNotificationService sendPushNotificationService; - private final ApplicationEventPublisher applicationEventPublisher; + private final S3UploadPort s3UploadPort; + private final SendNotificationService sendNotificationService; @Override @Transactional @@ -51,29 +49,21 @@ public CreateTaskResponse createTask(Long requesterId, CreateTaskRequest createT commandTaskPort.save(savedTask); if (files != null) { - saveAttachments(files, savedTask); - } - + saveAttachments(files, savedTask);} publishNotification(savedTask); return TaskMapper.toCreateTaskResponse(savedTask); } private void saveAttachments(List files, Task task) { - List fileUrls = s3UploadAdapter.uploadFiles(FilePathConstants.TASK_IMAGE, files); + List fileUrls = s3UploadPort.uploadFiles(FilePathConstants.TASK_IMAGE, files); List attachments = AttachmentMapper.toTaskAttachments(task, files, fileUrls); commandAttachmentPort.saveAll(attachments); } private void publishNotification(Task task) { List reviewers = memberService.findReviewers(); + reviewers.forEach(reviewer -> {sendNotificationService.sendPushNotification(reviewer, NotificationType.TASK_REQUESTED, + task, null, null);}); - // 검토자들 각각에 대한 알림 생성 후 event 발행 - for (Member reviewer : reviewers) { - - sendPushNotificationService.sendPushNotification(reviewer, NotificationType.TASK_REQUESTED, - task, null, null); - } } - - } \ No newline at end of file diff --git a/src/main/java/clap/server/application/service/task/GetTaskBoardService.java b/src/main/java/clap/server/application/service/task/GetTaskBoardService.java index 16a3569f..e624f1a2 100644 --- a/src/main/java/clap/server/application/service/task/GetTaskBoardService.java +++ b/src/main/java/clap/server/application/service/task/GetTaskBoardService.java @@ -10,6 +10,7 @@ import clap.server.application.port.outbound.task.LoadTaskPort; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.task.Task; +import clap.server.domain.policy.task.TaskValuePolicy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; @@ -25,7 +26,6 @@ @RequiredArgsConstructor @Transactional(readOnly = true) class GetTaskBoardService implements GetTaskBoardUsecase, FilterTaskBoardUsecase { - private final static List VIEWABLE_STATUSES = TaskStatus.getTaskBoardStatusList(); private final MemberService memberService; private final LoadTaskPort loadTaskPort; @@ -34,7 +34,7 @@ class GetTaskBoardService implements GetTaskBoardUsecase, FilterTaskBoardUsecase public TaskBoardResponse getTaskBoards(Long processorId, LocalDate untilDate, Pageable pageable) { memberService.findActiveMember(processorId); LocalDateTime untilDateTime = untilDate == null ? LocalDate.now().plusDays(1).atStartOfDay() : untilDate.plusDays(1).atStartOfDay(); - Slice tasks = loadTaskPort.findByProcessorAndStatus(processorId, VIEWABLE_STATUSES, untilDateTime, pageable); + Slice tasks = loadTaskPort.findByProcessorAndStatus(processorId, TaskValuePolicy.TASK_BOARD_STATUS_FILTER, untilDateTime, pageable); return TaskMapper.toSliceTaskItemResponse(tasks); } @@ -42,7 +42,7 @@ public TaskBoardResponse getTaskBoards(Long processorId, LocalDate untilDate, Pa public TaskBoardResponse getTaskBoardByFilter(Long processorId, LocalDate untilDate, FilterTaskBoardRequest request, Pageable pageable) { memberService.findActiveMember(processorId); LocalDateTime untilDateTime = untilDate == null ? LocalDate.now().plusDays(1).atStartOfDay() : untilDate.plusDays(1).atStartOfDay(); - Slice tasks = loadTaskPort.findTaskBoardByFilter(processorId, VIEWABLE_STATUSES, untilDateTime, request, pageable); + Slice tasks = loadTaskPort.findTaskBoardByFilter(processorId, TaskValuePolicy.TASK_BOARD_STATUS_FILTER, untilDateTime, request, 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 index 25a45a6e..1e0f7dca 100644 --- a/src/main/java/clap/server/application/service/task/UpdateTaskBoardService.java +++ b/src/main/java/clap/server/application/service/task/UpdateTaskBoardService.java @@ -5,11 +5,15 @@ 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.inbound.task.UpdateTaskOrderAndStatusUsecase; import clap.server.application.port.outbound.task.CommandTaskPort; import clap.server.application.port.outbound.task.LoadTaskPort; +import clap.server.domain.policy.task.TaskOrderCalculationPolicy; +import clap.server.domain.policy.task.ProcessorValidationPolicy; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.member.Member; import clap.server.domain.model.task.Task; +import clap.server.domain.policy.task.TaskValuePolicy; import clap.server.exception.ApplicationException; import clap.server.exception.code.TaskErrorCode; import lombok.RequiredArgsConstructor; @@ -19,21 +23,23 @@ @Slf4j @ApplicationService @RequiredArgsConstructor -class UpdateTaskBoardService implements UpdateTaskBoardUsecase { +class UpdateTaskBoardService implements UpdateTaskBoardUsecase, UpdateTaskOrderAndStatusUsecase { private final MemberService memberService; private final TaskService taskService; private final LoadTaskPort loadTaskPort; private final CommandTaskPort commandTaskPort; + private final TaskOrderCalculationPolicy taskOrderCalculationPolicy; + private final ProcessorValidationPolicy processorValidationPolicy; private Task findByIdAndStatus(Long taskId, TaskStatus status) { - return loadTaskPort.findByIdAndStatus(taskId, status).orElseThrow(()-> new ApplicationException(TaskErrorCode.TASK_NOT_FOUND)); + return loadTaskPort.findByIdAndStatus(taskId, status).orElseThrow(() -> new ApplicationException(TaskErrorCode.TASK_NOT_FOUND)); } /** * 작업(Task)의 순서를 업데이트하는 메서드 * * @param processorId 작업을 수행하는 멤버 ID - * @param request 순서 변경 요청 객체 + * @param request 순서 변경 요청 객체 */ @Override @Transactional @@ -42,148 +48,100 @@ public void updateTaskOrder(Long processorId, UpdateTaskOrderRequest request) { validateRequest(request, null); Member processor = memberService.findActiveMember(processorId); Task targetTask = taskService.findById(request.targetTaskId()); + processorValidationPolicy.validateProcessor(processorId, targetTask); // 가장 상위로 이동 if (request.prevTaskId() == 0) { - moveToTop(processorId, targetTask, request.nextTaskId()); + Task nextTask = findByIdAndStatus(request.targetTaskId(), targetTask.getTaskStatus()); + // 해당 상태에서 바로 앞에 있는 작업 찾기 + Task prevTask = loadTaskPort.findPrevOrderTaskByProcessorIdAndStatus(processorId, targetTask.getTaskStatus(), nextTask.getProcessorOrder()).orElse(null); + long newOrder = taskOrderCalculationPolicy.calculateOrderForTop(targetTask, nextTask); + updateNewTaskOrder(targetTask, newOrder); } // 가장 하위로 이동 else if (request.nextTaskId() == 0) { - moveToBottom(processorId, targetTask, request.prevTaskId()); + Task prevTask = findByIdAndStatus(request.targetTaskId(), targetTask.getTaskStatus()); + // 해당 상태에서 바로 뒤에 있는 작업 찾기 + Task nextTask = loadTaskPort.findNextOrderTaskByProcessorIdAndStatus(processorId, targetTask.getTaskStatus(), prevTask.getProcessorOrder()).orElse(null); + long newOrder = taskOrderCalculationPolicy.calculateOrderForBottom(prevTask, nextTask); + updateNewTaskOrder(targetTask, newOrder); } else { Task prevTask = findByIdAndStatus(request.prevTaskId(), targetTask.getTaskStatus()); Task nextTask = findByIdAndStatus(request.nextTaskId(), targetTask.getTaskStatus()); - - // 새로운 작업 순서 업데이트 - updateNewTaskOrder(processor.getMemberId(), targetTask, prevTask.getProcessorOrder(), nextTask.getProcessorOrder()); + long newOrder = taskOrderCalculationPolicy.calculateNewProcessorOrder(prevTask.getProcessorOrder(), nextTask.getProcessorOrder()); + updateNewTaskOrder(targetTask, newOrder); } } - /** - * 순서 변경 요청의 유효성을 검증하는 메서드 - */ - 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); + private void updateNewTaskOrder(Task targetTask, Long newOrder) { + targetTask.updateProcessorOrder(newOrder); 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 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()); + processorValidationPolicy.validateProcessor(processorId, targetTask); if (request.prevTaskId() == 0) { - moveToTopWithStatusChange(processorId, targetTask, request.nextTaskId(), targetStatus); + Task nextTask = findByIdAndStatus(request.targetTaskId(), targetStatus); + // 해당 상태에서 바로 앞 있는 작업 찾기 + Task prevTask = loadTaskPort.findPrevOrderTaskByProcessorIdAndStatus(processorId, targetStatus, nextTask.getProcessorOrder()).orElse(null); + long newOrder = taskOrderCalculationPolicy.calculateOrderForTop(prevTask,nextTask); + updateNewTaskOrderAndStatus(targetStatus, targetTask, newOrder); } else if (request.nextTaskId() == 0) { - moveToBottomWithStatusChange(processorId, targetTask, request.prevTaskId(), targetStatus); + Task prevTask = findByIdAndStatus(request.targetTaskId(), targetStatus); + // 해당 상태에서 바로 뒤에 있는 작업 찾기 + Task nextTask = loadTaskPort.findNextOrderTaskByProcessorIdAndStatus(processorId, targetStatus, prevTask.getProcessorOrder()).orElse(null); + long newOrder = taskOrderCalculationPolicy.calculateOrderForBottom(prevTask, nextTask); + updateNewTaskOrderAndStatus(targetStatus, targetTask, newOrder); } else { Task prevTask = findByIdAndStatus(request.prevTaskId(), targetStatus); Task nextTask = findByIdAndStatus(request.nextTaskId(), targetStatus); - - updateNewTaskOrderAndStatus(targetStatus, targetTask, processor.getMemberId(), prevTask.getProcessorOrder(), nextTask.getProcessorOrder()); + long newOrder = taskOrderCalculationPolicy.calculateNewProcessorOrder(nextTask.getProcessorOrder(), prevTask.getProcessorOrder()); + updateNewTaskOrderAndStatus(targetStatus, targetTask, newOrder); } } /** * 작업의 상태와 순서를 업데이트하는 메서드 */ - private void updateNewTaskOrderAndStatus(TaskStatus targetStatus, Task targetTask, Long processorId, Long prevTaskOrder, Long nextTaskOrder) { - targetTask.updateProcessorOrder(processorId, prevTaskOrder, nextTaskOrder); + private void updateNewTaskOrderAndStatus(TaskStatus targetStatus, Task targetTask, long newOrder) { + targetTask.updateProcessorOrder(newOrder); 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(); + public void validateRequest(UpdateTaskOrderRequest request, TaskStatus targetStatus) { + // 이전 및 다음 작업 ID가 모두 0인 경우 예외 발생 + if (request.prevTaskId() == 0 && request.nextTaskId() == 0) { + throw new ApplicationException(TaskErrorCode.INVALID_TASK_STATUS_TRANSITION); + } - updateNewTaskOrderAndStatus(targetStatus, targetTask, processorId, prevTaskOrder, nextTask.getProcessorOrder()); + // 타겟 상태가 유효한지 검증 + if (targetStatus != null && !TaskValuePolicy.TASK_BOARD_STATUS_FILTER.contains(targetStatus)) { + throw new ApplicationException(TaskErrorCode.INVALID_TASK_STATUS_TRANSITION); + } } - /** - * 상태 변경을 포함하여 작업을 가장 하위로 이동하는 메서드 - */ - 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 339e4011..06d65aa3 100644 --- a/src/main/java/clap/server/application/service/task/UpdateTaskService.java +++ b/src/main/java/clap/server/application/service/task/UpdateTaskService.java @@ -5,7 +5,6 @@ import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskRequest; import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskStatusRequest; import clap.server.adapter.inbound.web.dto.task.response.UpdateTaskResponse; -import clap.server.adapter.outbound.infrastructure.s3.S3UploadAdapter; import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; import clap.server.application.mapper.AttachmentMapper; import clap.server.application.mapper.TaskMapper; @@ -17,10 +16,11 @@ 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.service.webhook.SendPushNotificationService; +import clap.server.application.service.webhook.SendNotificationService; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.common.constants.FilePathConstants; import clap.server.domain.model.member.Member; @@ -35,7 +35,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.util.ArrayList; import java.util.List; @@ -47,13 +46,13 @@ public class UpdateTaskService implements UpdateTaskUsecase, UpdateTaskStatusUse private final MemberService memberService; private final CategoryService categoryService; private final TaskService taskService; - private final SendPushNotificationService sendPushNotificationService; + private final SendNotificationService sendNotificationService; private final CommandTaskPort commandTaskPort; private final LoadAttachmentPort loadAttachmentPort; private final LabelService labelService; private final CommandAttachmentPort commandAttachmentPort; - private final S3UploadAdapter s3UploadAdapter; + private final S3UploadPort s3UploadPort; @Override @Transactional @@ -78,9 +77,8 @@ public UpdateTaskResponse updateTaskState(Long memberId, Long taskId, UpdateTask Task task = taskService.findById(taskId); task.updateTaskStatus(updateTaskStatusRequest.taskStatus()); Task updateTask = commandTaskPort.save(task); - List receiver = new ArrayList<>(); - receiver.add(task.getRequester()); - publishNotification(receiver, updateTask, NotificationType.STATUS_SWITCHED, String.valueOf(updateTask.getTaskStatus())); + + publishNotification(updateTask, NotificationType.STATUS_SWITCHED, String.valueOf(updateTask.getTaskStatus())); return TaskMapper.toUpdateTaskResponse(updateTask); } @@ -94,11 +92,7 @@ public UpdateTaskResponse updateTaskProcessor(Long taskId, Long userId, UpdateTa task.updateProcessor(processor); Task updateTask = commandTaskPort.save(task); - List receivers = new ArrayList<>(); - receivers.add(updateTask.getRequester()); - receivers.add(updateTask.getProcessor()); - - publishNotification(receivers, updateTask, NotificationType.PROCESSOR_CHANGED, updateTask.getProcessor().getNickname()); + publishNotification(updateTask, NotificationType.PROCESSOR_CHANGED, updateTask.getProcessor().getNickname()); return TaskMapper.toUpdateTaskResponse(updateTask); } @@ -114,14 +108,12 @@ public UpdateTaskResponse updateTaskLabel(Long taskId, Long userId, UpdateTaskLa return TaskMapper.toUpdateTaskResponse(updatetask); } - - private void updateAttachments(List attachmentIdsToDelete, List files, Task task) { List attachmentsToDelete = validateAndGetAttachments(attachmentIdsToDelete, task); attachmentsToDelete.forEach(Attachment::softDelete); - if(files != null) { - List fileUrls = s3UploadAdapter.uploadFiles(FilePathConstants.TASK_IMAGE, files); + if (files != null) { + List fileUrls = s3UploadPort.uploadFiles(FilePathConstants.TASK_IMAGE, files); List attachments = AttachmentMapper.toTaskAttachments(task, files, fileUrls); commandAttachmentPort.saveAll(attachments); } @@ -135,10 +127,11 @@ private List validateAndGetAttachments(List attachmentIdsToDel return attachmentsOfTask; } - private void publishNotification(List receivers, Task task, NotificationType notificationType, String message){ - for (Member receiver : receivers) { - sendPushNotificationService.sendPushNotification(receiver, notificationType, + private void publishNotification(Task task, NotificationType notificationType, String message) { + List receivers = List.of(task.getRequester(), task.getProcessor()); + receivers.forEach(receiver -> { + sendNotificationService.sendPushNotification(receiver, notificationType, task, message, null); - } + }); } } diff --git a/src/main/java/clap/server/application/service/webhook/SendNotificationService.java b/src/main/java/clap/server/application/service/webhook/SendNotificationService.java new file mode 100644 index 00000000..dc31d418 --- /dev/null +++ b/src/main/java/clap/server/application/service/webhook/SendNotificationService.java @@ -0,0 +1,77 @@ +package clap.server.application.service.webhook; + +import clap.server.adapter.inbound.web.dto.notification.SseRequest; +import clap.server.adapter.outbound.api.dto.SendWebhookRequest; +import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; +import clap.server.application.port.outbound.notification.CommandNotificationPort; +import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.domain.model.member.Member; +import clap.server.domain.model.notification.Notification; +import clap.server.domain.model.task.Task; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; + +import java.util.concurrent.CompletableFuture; + +import static clap.server.domain.model.notification.Notification.createTaskNotification; + +@ApplicationService +@RequiredArgsConstructor +public class SendNotificationService { + + private final SendSseService sendSseService; + private final SendAgitService sendAgitService; + private final SendEmailService sendEmailService; + private final SendKaKaoWorkService sendKaKaoWorkService; + private final CommandNotificationPort commandNotificationPort; + + @Async("notificationExecutor") + public void sendPushNotification(Member receiver, NotificationType notificationType, + Task task, String message, String commenterName) { + String email = receiver.getMemberInfo().getEmail(); + String taskTitle = task.getTitle(); + String requesterNickname = task.getRequester().getNickname(); + + Notification notification = createTaskNotification(task, receiver, notificationType); + + SseRequest sseRequest = new SseRequest( + task.getTitle(), + notificationType, + receiver.getMemberId(), + message + ); + + SendWebhookRequest sendWebhookRequest = new SendWebhookRequest( + email, notificationType, taskTitle, requesterNickname, message, commenterName + ); + + CompletableFuture saveNotification = CompletableFuture.runAsync(() -> { + commandNotificationPort.save(notification); + }); + + CompletableFuture sendSseFuture = CompletableFuture.runAsync(() -> { + sendSseService.send(sseRequest); + }); + + CompletableFuture sendEmailFuture = CompletableFuture.runAsync(() -> { + if (receiver.getEmailNotificationEnabled()) { + sendEmailService.sendEmail(sendWebhookRequest); + } + }); + + CompletableFuture sendAgitFuture = CompletableFuture.runAsync(() -> { + if (receiver.getAgitNotificationEnabled()) { + sendAgitService.sendAgit(sendWebhookRequest); + } + }); + + CompletableFuture sendKakaoWorkFuture = CompletableFuture.runAsync(() -> { + if (receiver.getKakaoworkNotificationEnabled()) { + sendKaKaoWorkService.sendKaKaoWork(sendWebhookRequest); + } + }); + + CompletableFuture allOf = CompletableFuture.allOf(saveNotification, sendSseFuture, sendEmailFuture, sendKakaoWorkFuture, sendAgitFuture); + allOf.join(); + } +} diff --git a/src/main/java/clap/server/common/annotation/architecture/Policy.java b/src/main/java/clap/server/common/annotation/architecture/Policy.java new file mode 100644 index 00000000..28ddfa87 --- /dev/null +++ b/src/main/java/clap/server/common/annotation/architecture/Policy.java @@ -0,0 +1,15 @@ +package clap.server.common.annotation.architecture; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Policy { + @AliasFor(annotation = Component.class) + String value() default ""; +} \ No newline at end of file diff --git a/src/main/java/clap/server/common/constants/FilePathConstants.java b/src/main/java/clap/server/common/constants/FilePathConstants.java index 0b13ec57..3d2e6fd5 100644 --- a/src/main/java/clap/server/common/constants/FilePathConstants.java +++ b/src/main/java/clap/server/common/constants/FilePathConstants.java @@ -8,6 +8,7 @@ public enum FilePathConstants { TASK_IMAGE("task/image"), TASK_DOCUMENT("task/docs"), + TASK_COMMENT("task/comments"), MEMBER_IMAGE("member"), ; private final String path; 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 fd076ac7..89abf6e2 100644 --- a/src/main/java/clap/server/domain/model/task/Task.java +++ b/src/main/java/clap/server/domain/model/task/Task.java @@ -15,7 +15,7 @@ import java.time.format.DateTimeFormatter; import java.util.Objects; -import static clap.server.domain.model.task.constants.TaskProcessorOrderPolicy.DEFAULT_PROCESSOR_ORDER_GAP; +import static clap.server.domain.policy.task.TaskValuePolicy.DEFAULT_PROCESSOR_ORDER_GAP; @Getter @SuperBuilder @@ -50,19 +50,12 @@ public void updateTask(Long requesterId, Category category, String title, String 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 * DEFAULT_PROCESSOR_ORDER_GAP; @@ -85,7 +78,6 @@ public void updateLabel(Label label) { } public void approveTask(Member reviewer, Member processor, LocalDateTime dueDate, Category category, Label label) { - validateTaskRequested(); this.reviewer = reviewer; this.processor = processor; this.dueDate = dueDate; @@ -98,32 +90,7 @@ 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; - } + public void updateProcessorOrder(long newProcessorOrder) { 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 deleted file mode 100644 index 78bc9cfc..00000000 --- a/src/main/java/clap/server/domain/model/task/constants/TaskProcessorOrderPolicy.java +++ /dev/null @@ -1,8 +0,0 @@ -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/domain/policy/task/ProcessorValidationPolicy.java b/src/main/java/clap/server/domain/policy/task/ProcessorValidationPolicy.java new file mode 100644 index 00000000..16f7bd73 --- /dev/null +++ b/src/main/java/clap/server/domain/policy/task/ProcessorValidationPolicy.java @@ -0,0 +1,17 @@ +package clap.server.domain.policy.task; + +import clap.server.common.annotation.architecture.Policy; +import clap.server.domain.model.task.Task; +import clap.server.exception.ApplicationException; +import clap.server.exception.code.TaskErrorCode; + +import java.util.Objects; + +@Policy +public class ProcessorValidationPolicy { + public void validateProcessor(Long processorId, Task targetTask) { + if (!Objects.equals(processorId, targetTask.getProcessor().getMemberId())) { + throw new ApplicationException(TaskErrorCode.NOT_A_PROCESSOR); + } + } +} diff --git a/src/main/java/clap/server/domain/policy/task/RequestedTaskUpdatePolicy.java b/src/main/java/clap/server/domain/policy/task/RequestedTaskUpdatePolicy.java new file mode 100644 index 00000000..ce1eb38a --- /dev/null +++ b/src/main/java/clap/server/domain/policy/task/RequestedTaskUpdatePolicy.java @@ -0,0 +1,16 @@ +package clap.server.domain.policy.task; + +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import clap.server.common.annotation.architecture.Policy; +import clap.server.domain.model.task.Task; +import clap.server.exception.DomainException; +import clap.server.exception.code.TaskErrorCode; + +@Policy +public class RequestedTaskUpdatePolicy { + public void validateTaskRequested(Task task) { + if (task.getTaskStatus() != TaskStatus.REQUESTED) { + throw new DomainException(TaskErrorCode.TASK_STATUS_MISMATCH); + } + } +} diff --git a/src/main/java/clap/server/domain/policy/task/TaskOrderCalculationPolicy.java b/src/main/java/clap/server/domain/policy/task/TaskOrderCalculationPolicy.java new file mode 100644 index 00000000..64f7ec29 --- /dev/null +++ b/src/main/java/clap/server/domain/policy/task/TaskOrderCalculationPolicy.java @@ -0,0 +1,43 @@ +package clap.server.domain.policy.task; + +import clap.server.common.annotation.architecture.Policy; +import clap.server.domain.model.task.Task; +import clap.server.exception.DomainException; +import clap.server.exception.code.TaskErrorCode; + +import static clap.server.domain.policy.task.TaskValuePolicy.DEFAULT_PROCESSOR_ORDER_GAP; + +@Policy +public class TaskOrderCalculationPolicy { + + public long calculateOrderForTop(Task prevTask, Task nextTask) { + Long prevTaskOrder = prevTask == null ? null : prevTask.getProcessorOrder(); + if (prevTaskOrder == null){ + return nextTask.getProcessorOrder() - DEFAULT_PROCESSOR_ORDER_GAP; + } + return calculateNewProcessorOrder(prevTaskOrder, nextTask.getProcessorOrder()); + } + + public long calculateOrderForBottom(Task prevTask, Task nextTask) { + Long nextTaskOrder = nextTask == null ? null : nextTask.getProcessorOrder(); + if (nextTaskOrder == null){ + return prevTask.getProcessorOrder() + DEFAULT_PROCESSOR_ORDER_GAP; + } + return calculateNewProcessorOrder(prevTask.getProcessorOrder(), nextTaskOrder); + } + + public long calculateNewProcessorOrder(Long prevTaskOrder, Long nextTaskOrder) { + if (prevTaskOrder != null && nextTaskOrder != null) { + if (nextTaskOrder - prevTaskOrder < 2) { + throw new DomainException(TaskErrorCode.INVALID_TASK_ORDER); + } + return (prevTaskOrder + nextTaskOrder) / 2; + } + // 기본값 (예외적인 상황 방지) + else { + return DEFAULT_PROCESSOR_ORDER_GAP; + } + } + +} + diff --git a/src/main/java/clap/server/domain/policy/task/TaskValuePolicy.java b/src/main/java/clap/server/domain/policy/task/TaskValuePolicy.java new file mode 100644 index 00000000..2663d656 --- /dev/null +++ b/src/main/java/clap/server/domain/policy/task/TaskValuePolicy.java @@ -0,0 +1,16 @@ +package clap.server.domain.policy.task; + +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import lombok.Getter; + +import java.util.List; + +@Getter +public class TaskValuePolicy { + public static final long DEFAULT_PROCESSOR_ORDER_GAP = (long) Math.pow(2,6); + + public static final List TASK_BOARD_STATUS_FILTER = List.of( + TaskStatus.IN_PROGRESS, + TaskStatus.PENDING_COMPLETED, + TaskStatus.COMPLETED); +} \ No newline at end of file