diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindAllDepartmentsResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindAllDepartmentsResponse.java index c9afe042..2a4fb39c 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindAllDepartmentsResponse.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindAllDepartmentsResponse.java @@ -1,6 +1,10 @@ package clap.server.adapter.inbound.web.dto.admin.response; +import io.swagger.v3.oas.annotations.media.Schema; + public record FindAllDepartmentsResponse( Long departmentId, - String name) { + String name, + @Schema(description = "담당자 권한 여부") + Boolean isManager) { } diff --git a/src/main/java/clap/server/adapter/inbound/web/example/ErrorExampleController.java b/src/main/java/clap/server/adapter/inbound/web/example/ErrorExampleController.java index 6a631df3..0fb28f6e 100644 --- a/src/main/java/clap/server/adapter/inbound/web/example/ErrorExampleController.java +++ b/src/main/java/clap/server/adapter/inbound/web/example/ErrorExampleController.java @@ -53,7 +53,7 @@ public void getCommentErrorCode() {} @GetMapping("/statistic") @DevelopOnlyApi @Operation(summary = "작업 통계 관련 에러 코드 나열") - @ApiErrorCodes(LabelErrorCode.class) + @ApiErrorCodes(StatisticsErrorCode.class) public void getStatisticsErrorCode() {} @GetMapping("/label") 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 a3f472fe..021210ca 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java @@ -4,23 +4,19 @@ import clap.server.adapter.outbound.persistense.entity.member.MemberEntity; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; -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.MemberPersistenceMapper; -import clap.server.adapter.outbound.persistense.mapper.TaskPersistenceMapper; import clap.server.adapter.outbound.persistense.repository.member.MemberRepository; -import clap.server.adapter.outbound.persistense.repository.task.TaskRepository; 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.member.Member; -import clap.server.domain.model.task.Task; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; @PersistenceAdapter @@ -28,8 +24,6 @@ public class MemberPersistenceAdapter implements LoadMemberPort, CommandMemberPort { private final MemberRepository memberRepository; private final MemberPersistenceMapper memberPersistenceMapper; - private final TaskRepository taskRepository; - private final TaskPersistenceMapper taskPersistenceMapper; @Override public Optional findById(final Long id) { @@ -83,14 +77,6 @@ public List findActiveManagers() { .collect(Collectors.toList()); } - @Override - public List findTasksByMemberIdAndStatus(final Long memberId, final List taskStatuses) { - List taskEntities = taskRepository.findByProcessor_MemberIdAndTaskStatusIn(memberId, taskStatuses); - return taskEntities.stream() - .map(taskPersistenceMapper::toDomain) - .collect(Collectors.toList()); - } - @Override public Page findAllMembers(final Pageable pageable) { return memberRepository.findAllMembers(pageable).map(memberPersistenceMapper::toDomain); @@ -117,4 +103,8 @@ public Optional findByEmail(final String email) { .map(memberPersistenceMapper::toDomain); } + @Override + public boolean existsByNicknamesOrEmails(Set nicknames, Set emails) { + return memberRepository.existsByNicknamesOrEmails(nicknames, emails); + } } \ No newline at end of file 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 cfd86cd7..820f6568 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/TaskPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/TaskPersistenceAdapter.java @@ -21,6 +21,7 @@ import java.time.LocalTime; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; @Slf4j @PersistenceAdapter @@ -116,5 +117,12 @@ public List findTeamStatus(final Long memberId, final FilterTeamStatusRequ .map(taskPersistenceMapper::toDomain).toList(); } + @Override + public List findTasksByMemberIdAndStatus(final Long memberId, final List taskStatuses) { + List taskEntities = taskRepository.findByProcessor_MemberIdAndTaskStatusIn(memberId, taskStatuses); + return taskEntities.stream() + .map(taskPersistenceMapper::toDomain) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/entity/member/DepartmentEntity.java b/src/main/java/clap/server/adapter/outbound/persistense/entity/member/DepartmentEntity.java index 2da65f2b..4a706254 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/entity/member/DepartmentEntity.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/entity/member/DepartmentEntity.java @@ -30,4 +30,7 @@ public class DepartmentEntity extends BaseTimeEntity { @Column(nullable = false) @Enumerated(EnumType.STRING) private DepartmentStatus status; + + @Column(nullable = false) + private boolean isManager; } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/mapper/DepartmentPersistenceMapper.java b/src/main/java/clap/server/adapter/outbound/persistense/mapper/DepartmentPersistenceMapper.java index 87e6947b..436c3f64 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/mapper/DepartmentPersistenceMapper.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/mapper/DepartmentPersistenceMapper.java @@ -10,8 +10,10 @@ public interface DepartmentPersistenceMapper extends PersistenceMapper { @Mapping(source = "admin.memberId", target = "adminId") + @Mapping(source = "manager", target = "isManager") Department toDomain(DepartmentEntity entity); @Mapping(source = "adminId", target = "admin.memberId") + @Mapping(source = "manager", target = "isManager") DepartmentEntity toEntity(Department domain); } \ No newline at end of file diff --git a/src/main/java/clap/server/adapter/outbound/persistense/mapper/MemberPersistenceMapper.java b/src/main/java/clap/server/adapter/outbound/persistense/mapper/MemberPersistenceMapper.java index b4e55d6d..f0775c2a 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/mapper/MemberPersistenceMapper.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/mapper/MemberPersistenceMapper.java @@ -45,15 +45,12 @@ protected Member toDomainAdmin(MemberEntity admin) { if (admin == null) return null; return Member.builder() .memberId(admin.getMemberId()) - .memberInfo(new MemberInfo( - admin.getName(), - admin.getEmail(), - admin.getNickname(), - admin.isReviewer(), - departmentPersistenceMapper.toDomain(admin.getDepartment()), - admin.getRole(), - admin.getDepartmentRole() - )) + .memberInfo(MemberInfo.builder() + .name(admin.getName()) + .email(admin.getEmail()) + .nickname(admin.getNickname()) + .isReviewer(admin.isReviewer()) + .build()) .createdAt(admin.getCreatedAt()) .updatedAt(admin.getUpdatedAt()) .build(); 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 d7d967aa..cfb6fec8 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,10 +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.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; +import java.util.Set; @Repository public interface MemberRepository extends JpaRepository, MemberCustomRepository { @@ -28,5 +31,8 @@ public interface MemberRepository extends JpaRepository, Me Optional findByNameAndEmail(String name, String email); Optional findByEmail(String email); + + @Query("SELECT COUNT(m) > 0 FROM MemberEntity m WHERE m.nickname IN :nicknames OR m.email IN :emails") + boolean existsByNicknamesOrEmails(@Param("nicknames") Set nicknames, @Param("emails") Set emails); } diff --git a/src/main/java/clap/server/application/mapper/response/DepartmentResponseMapper.java b/src/main/java/clap/server/application/mapper/response/DepartmentResponseMapper.java index 0c115702..a14d10fd 100644 --- a/src/main/java/clap/server/application/mapper/response/DepartmentResponseMapper.java +++ b/src/main/java/clap/server/application/mapper/response/DepartmentResponseMapper.java @@ -7,7 +7,11 @@ public class DepartmentResponseMapper { private DepartmentResponseMapper() { throw new IllegalStateException("Utility class"); } + public static FindAllDepartmentsResponse toFindAllDepartmentsResponse(Department department) { - return new FindAllDepartmentsResponse(department.getDepartmentId(), department.getName()); + return new FindAllDepartmentsResponse( + department.getDepartmentId(), + department.getName(), + department.isManager()); } } 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 1d4afe12..a8899189 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 @@ -25,11 +25,6 @@ public Member findActiveMember(Long memberId) { () -> new ApplicationException(MemberErrorCode.ACTIVE_MEMBER_NOT_FOUND)); } - public int getRemainingTasks(Long memberId) { - List targetStatuses = List.of(TaskStatus.IN_PROGRESS, TaskStatus.IN_REVIEWING); - return loadMemberPort.findTasksByMemberIdAndStatus(memberId, targetStatuses).size(); - } - public List findActiveManagers() { List activeManagers = loadMemberPort.findActiveManagers(); 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 dff8b206..a377bdc6 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 @@ -11,6 +11,7 @@ import org.springframework.data.domain.Pageable; import java.util.Optional; +import java.util.Set; public interface LoadMemberPort { Optional findById(Long id); @@ -19,8 +20,6 @@ public interface LoadMemberPort { List findActiveManagers(); - List findTasksByMemberIdAndStatus(Long memberId, List taskStatuses); - Optional findReviewerById(Long id); Optional findByNickname(String nickname); @@ -37,4 +36,5 @@ public interface LoadMemberPort { Optional findByEmail(String email); + boolean existsByNicknamesOrEmails(Set nicknames, Set emails); } 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 a0f64d7e..43067966 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 @@ -40,4 +40,6 @@ public interface LoadTaskPort { List findTaskBoardByFilter(Long processorId, List statuses, LocalDateTime untilDateTime, FilterTaskBoardRequest request); List findTeamStatus(Long memberId, FilterTeamStatusRequest filter); + + List findTasksByMemberIdAndStatus(Long memberId, List taskStatuses); } diff --git a/src/main/java/clap/server/application/service/admin/CsvParseService.java b/src/main/java/clap/server/application/service/admin/CsvParseService.java index 76199e41..38578481 100644 --- a/src/main/java/clap/server/application/service/admin/CsvParseService.java +++ b/src/main/java/clap/server/application/service/admin/CsvParseService.java @@ -5,6 +5,7 @@ import clap.server.domain.model.member.Department; import clap.server.domain.model.member.Member; import clap.server.domain.model.member.MemberInfo; +import clap.server.domain.policy.member.ManagerDepartmentPolicy; import clap.server.exception.ApplicationException; import clap.server.exception.code.DepartmentErrorCode; import clap.server.exception.code.MemberErrorCode; @@ -28,6 +29,7 @@ public class CsvParseService { private final LoadDepartmentPort loadDepartmentPort; + private final ManagerDepartmentPolicy managerDepartmentPolicy; public List parseDataAndMapToMember(MultipartFile file) { List members = new ArrayList<>(); @@ -59,6 +61,7 @@ private Member mapToMember(String[] fields, List departments) { .findFirst() .orElseThrow(() -> new ApplicationException(DepartmentErrorCode.DEPARTMENT_NOT_FOUND)); + managerDepartmentPolicy.validateDepartment(department, MemberRole.valueOf(fields[3].trim())); MemberInfo memberInfo = toMemberInfo( fields[0].trim(), // name fields[4].trim(), // email diff --git a/src/main/java/clap/server/application/service/admin/RegisterMemberCSVService.java b/src/main/java/clap/server/application/service/admin/RegisterMemberCSVService.java index 6557c3ec..d4e22179 100644 --- a/src/main/java/clap/server/application/service/admin/RegisterMemberCSVService.java +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberCSVService.java @@ -12,7 +12,9 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.util.HashSet; import java.util.List; +import java.util.Set; @ApplicationService @RequiredArgsConstructor @@ -29,20 +31,29 @@ public int registerMembersFromCsv(Long adminId, MultipartFile file) { List members = csvParser.parseDataAndMapToMember(file); Member admin = memberService.findActiveMember(adminId); - members.forEach(member -> { - String nickname = member.getMemberInfo().getNickname(); - String email = member.getMemberInfo().getEmail(); - if (loadMemberPort.findByNickname(nickname).isPresent() || - loadMemberPort.findByEmail(email).isPresent()) { - throw new ApplicationException(MemberErrorCode.DUPLICATE_NICKNAME_OR_EMAIL); - } - }); + validateMembers(members); List newMembers = members.stream() - .map(memberData -> Member.createMember(admin, memberData.getMemberInfo())) + .map(memberData -> + Member.createMember(admin, memberData.getMemberInfo())) .toList(); commandMemberPort.saveAll(newMembers); return members.size(); } + + public void validateMembers(List members) { + Set nicknames = new HashSet<>(); + Set emails = new HashSet<>(); + + for (Member member : members) { + nicknames.add(member.getMemberInfo().getNickname()); + emails.add(member.getMemberInfo().getEmail()); + } + + if(loadMemberPort.existsByNicknamesOrEmails(nicknames, emails)) { + throw new ApplicationException(MemberErrorCode.DUPLICATE_NICKNAME_OR_EMAIL); + } + } + } \ No newline at end of file diff --git a/src/main/java/clap/server/application/service/admin/RegisterMemberService.java b/src/main/java/clap/server/application/service/admin/RegisterMemberService.java index 60d72b4b..4934f74d 100644 --- a/src/main/java/clap/server/application/service/admin/RegisterMemberService.java +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberService.java @@ -10,17 +10,14 @@ import clap.server.domain.model.member.Department; import clap.server.domain.model.member.Member; import clap.server.domain.model.member.MemberInfo; +import clap.server.domain.policy.member.ManagerDepartmentPolicy; import clap.server.exception.ApplicationException; -import clap.server.exception.AuthException; -import clap.server.exception.code.AuthErrorCode; import clap.server.exception.code.DepartmentErrorCode; import clap.server.exception.code.MemberErrorCode; import lombok.RequiredArgsConstructor; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.transaction.annotation.Transactional; - -import java.util.List; +import java.util.Set; @ApplicationService @RequiredArgsConstructor @@ -29,6 +26,7 @@ class RegisterMemberService implements RegisterMemberUsecase { private final CommandMemberPort commandMemberPort; private final LoadDepartmentPort loadDepartmentPort; private final LoadMemberPort loadMemberPort; + private final ManagerDepartmentPolicy managerDepartmentPolicy; @Override @Transactional @@ -37,13 +35,11 @@ public void registerMember(Long adminId, RegisterMemberRequest request) { Department department = loadDepartmentPort.findById(request.departmentId()) .orElseThrow(() -> new ApplicationException(DepartmentErrorCode.DEPARTMENT_NOT_FOUND)); - loadMemberPort.findByNickname(request.nickname()).ifPresent( - member -> { - throw new ApplicationException(MemberErrorCode.DUPLICATE_NICKNAME); - } - ); + if (loadMemberPort.existsByNicknamesOrEmails(Set.of(request.nickname()), Set.of(request.email()))) { + throw new ApplicationException(MemberErrorCode.DUPLICATE_NICKNAME_OR_EMAIL); + } - // TODO: 인프라팀만 담당자가 될 수 있도록 수정해야함 + managerDepartmentPolicy.validateDepartment(department, request.role()); MemberInfo memberInfo = MemberInfo.toMemberInfo(request.name(), request.email(), request.nickname(), request.isReviewer(), department, request.role(), request.departmentRole()); Member member = Member.createMember(admin, memberInfo); diff --git a/src/main/java/clap/server/application/service/task/FindManagersService.java b/src/main/java/clap/server/application/service/task/FindManagersService.java index 1a20c6d3..285915f1 100644 --- a/src/main/java/clap/server/application/service/task/FindManagersService.java +++ b/src/main/java/clap/server/application/service/task/FindManagersService.java @@ -1,8 +1,10 @@ package clap.server.application.service.task; import clap.server.adapter.inbound.web.dto.task.response.FindManagersResponse; -import clap.server.application.port.inbound.task.FindManagersUsecase; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; import clap.server.application.port.inbound.domain.MemberService; +import clap.server.application.port.inbound.task.FindManagersUsecase; +import clap.server.application.port.outbound.task.LoadTaskPort; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.member.Member; import jakarta.transaction.Transactional; @@ -17,14 +19,16 @@ public class FindManagersService implements FindManagersUsecase { private final MemberService memberService; + private final LoadTaskPort loadTaskPort; @Transactional @Override public List findManagers() { + List targetStatuses = List.of(TaskStatus.IN_PROGRESS, TaskStatus.IN_REVIEWING); List managers = memberService.findActiveManagers(); return managers.stream() .map(manager -> { - int remainingTasks = memberService.getRemainingTasks(manager.getMemberId()); + int remainingTasks = loadTaskPort.findTasksByMemberIdAndStatus(manager.getMemberId(), targetStatuses).size(); return toFindManagersResponse(manager, remainingTasks); }).toList(); } 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 e88f1e05..095efc53 100644 --- a/src/main/java/clap/server/application/service/task/UpdateTaskBoardService.java +++ b/src/main/java/clap/server/application/service/task/UpdateTaskBoardService.java @@ -159,8 +159,7 @@ public void updateTaskOrderAndStatus(Long processorId, UpdateTaskOrderRequest re TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.STATUS_SWITCHED, updatedTask, targetStatus.getDescription(), null,null); commandTaskHistoryPort.save(taskHistory); - //TODO: 최종 단계에서 주석 처리 해제 - //publishNotification(targetTask, NotificationType.STATUS_SWITCHED, String.valueOf(updatedTask.getTaskStatus())); + publishNotification(targetTask, NotificationType.STATUS_SWITCHED, String.valueOf(updatedTask.getTaskStatus())); } /** @@ -182,7 +181,7 @@ public void validateRequest(UpdateTaskOrderRequest request, TaskStatus targetSta } } - private void publishNotification(Task task, NotificationType notificationType, String message, String taskTitle) { + private void publishNotification(Task task, NotificationType notificationType, String message) { List receivers = List.of(task.getRequester(), task.getProcessor()); receivers.forEach(receiver -> { boolean isManager = receiver.getMemberInfo().getRole() == MemberRole.ROLE_MANAGER; diff --git a/src/main/java/clap/server/domain/model/auth/LoginLog.java b/src/main/java/clap/server/domain/model/auth/LoginLog.java index 11f89c19..29792ede 100644 --- a/src/main/java/clap/server/domain/model/auth/LoginLog.java +++ b/src/main/java/clap/server/domain/model/auth/LoginLog.java @@ -1,14 +1,14 @@ package clap.server.domain.model.auth; import lombok.AccessLevel; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; import java.time.LocalDateTime; @Getter -@Builder +@SuperBuilder @NoArgsConstructor(access = AccessLevel.PROTECTED) public class LoginLog { private String clientIp; @@ -17,16 +17,6 @@ public class LoginLog { private int failedCount; private boolean isLocked; - @Builder - private LoginLog(String clientIp, String attemptNickname, LocalDateTime lastAttemptAt, - int failedCount, boolean isLocked) { - this.clientIp = clientIp; - this.attemptNickname = attemptNickname; - this.lastAttemptAt = lastAttemptAt; - this.failedCount = failedCount; - this.isLocked = isLocked; - } - public static LoginLog createLoginLog(String clientIp, String attemptNickname) { return LoginLog.builder() .clientIp(clientIp) diff --git a/src/main/java/clap/server/domain/model/auth/RefreshToken.java b/src/main/java/clap/server/domain/model/auth/RefreshToken.java index fa090ab9..7b011cf4 100644 --- a/src/main/java/clap/server/domain/model/auth/RefreshToken.java +++ b/src/main/java/clap/server/domain/model/auth/RefreshToken.java @@ -14,13 +14,6 @@ public class RefreshToken { private String token; private long ttl; - @Builder - private RefreshToken(Long memberId, String token, long ttl) { - this.memberId = memberId; - this.token = token; - this.ttl = ttl; - } - public static RefreshToken of(Long memberId, String token, long ttl) { return RefreshToken.builder() .memberId(memberId) diff --git a/src/main/java/clap/server/domain/model/member/Department.java b/src/main/java/clap/server/domain/model/member/Department.java index 565357c5..cf559510 100644 --- a/src/main/java/clap/server/domain/model/member/Department.java +++ b/src/main/java/clap/server/domain/model/member/Department.java @@ -15,4 +15,5 @@ public class Department extends BaseTime { private Long adminId; private String name; private DepartmentStatus status; + private boolean isManager; } 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 682fbcd8..7239cbab 100644 --- a/src/main/java/clap/server/domain/model/member/Member.java +++ b/src/main/java/clap/server/domain/model/member/Member.java @@ -1,16 +1,13 @@ 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 clap.server.domain.model.task.Task; -import clap.server.exception.DomainException; -import clap.server.exception.code.MemberErrorCode; -import lombok.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; -import java.util.Objects; - @Getter @SuperBuilder @@ -28,7 +25,6 @@ public class Member extends BaseTime { private String password; private Department department; - @Builder public Member(MemberInfo memberInfo, Boolean agitNotificationEnabled, Boolean emailNotificationEnabled, Boolean kakaoworkNotificationEnabled, Member admin, String imageUrl, MemberStatus status, String password) { this.memberInfo = memberInfo; diff --git a/src/main/java/clap/server/domain/model/member/MemberInfo.java b/src/main/java/clap/server/domain/model/member/MemberInfo.java index b6fca8f5..6ee9ba90 100644 --- a/src/main/java/clap/server/domain/model/member/MemberInfo.java +++ b/src/main/java/clap/server/domain/model/member/MemberInfo.java @@ -4,7 +4,6 @@ import clap.server.exception.DomainException; import clap.server.exception.code.MemberErrorCode; import lombok.AccessLevel; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; @@ -21,18 +20,6 @@ public class MemberInfo { private MemberRole role; private String departmentRole; - @Builder - public MemberInfo(String name, String email, String nickname, boolean isReviewer, - Department department, MemberRole role, String departmentRole) { - this.name = name; - this.email = email; - this.nickname = nickname; - this.isReviewer = isReviewer; - this.department = department; - this.role = role; - this.departmentRole = departmentRole; - } - public static MemberInfo toMemberInfo(String name, String email, String nickname, boolean isReviewer, Department department, MemberRole role, String departmentRole) { assertReviewerIsManager(isReviewer, role); @@ -64,7 +51,7 @@ public void updateName(String name) { public static void assertReviewerIsManager(boolean isReviewer, MemberRole role) { if (isReviewer) { if (role != MemberRole.ROLE_MANAGER) { - throw new DomainException(MemberErrorCode.MEMBER_REGISTRATION_FAILED); + throw new DomainException(MemberErrorCode.REVIEW_PERMISSION_DENIED); } } } diff --git a/src/main/java/clap/server/domain/model/notification/Notification.java b/src/main/java/clap/server/domain/model/notification/Notification.java index 8a95112f..8186c248 100644 --- a/src/main/java/clap/server/domain/model/notification/Notification.java +++ b/src/main/java/clap/server/domain/model/notification/Notification.java @@ -1,10 +1,8 @@ package clap.server.domain.model.notification; import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; -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.domain.model.task.Category; import clap.server.domain.model.task.Task; import lombok.AccessLevel; import lombok.Builder; @@ -12,9 +10,6 @@ import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - @Getter @SuperBuilder @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -27,16 +22,6 @@ public class Notification extends BaseTime { private String taskTitle; private boolean isRead; - @Builder - public Notification(Task task, NotificationType type, Member receiver, String message, String taskTitle) { - this.task = task; - this.type = type; - this.receiver = receiver; - this.message = message; - this.taskTitle = taskTitle; - this.isRead = false; - } - public void updateNotificationIsRead() { this.isRead = true; } diff --git a/src/main/java/clap/server/domain/model/task/Attachment.java b/src/main/java/clap/server/domain/model/task/Attachment.java index 11a2bfee..3d48ff55 100644 --- a/src/main/java/clap/server/domain/model/task/Attachment.java +++ b/src/main/java/clap/server/domain/model/task/Attachment.java @@ -2,7 +2,6 @@ import clap.server.domain.model.common.BaseTime; import lombok.AccessLevel; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; @@ -19,16 +18,6 @@ public class Attachment extends BaseTime { private String fileSize; private boolean isDeleted; - @Builder - public Attachment(Task task, Comment comment, String originalName, String fileUrl, String fileSize, boolean isDeleted) { - this.task = task; - this.comment = comment; - this.originalName = originalName; - this.fileUrl = fileUrl; - this.fileSize = fileSize; - this.isDeleted = isDeleted; - } - public static Attachment createAttachment(Task task, String originalName, String fileUrl, long fileSize) { return Attachment.builder() .task(task) diff --git a/src/main/java/clap/server/domain/policy/member/ManagerDepartmentPolicy.java b/src/main/java/clap/server/domain/policy/member/ManagerDepartmentPolicy.java new file mode 100644 index 00000000..08f8f6c1 --- /dev/null +++ b/src/main/java/clap/server/domain/policy/member/ManagerDepartmentPolicy.java @@ -0,0 +1,17 @@ +package clap.server.domain.policy.member; + +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; +import clap.server.common.annotation.architecture.Policy; +import clap.server.domain.model.member.Department; +import clap.server.exception.DomainException; +import clap.server.exception.code.MemberErrorCode; + +@Policy +public class ManagerDepartmentPolicy { + public void validateDepartment(final Department department, final MemberRole memberRole) { + if (!(department.isManager() + && memberRole == MemberRole.ROLE_MANAGER)) { + throw new DomainException(MemberErrorCode.MANAGER_PERMISSION_DENIED); + } + } +} diff --git a/src/main/java/clap/server/domain/policy/task/ProcessorValidationPolicy.java b/src/main/java/clap/server/domain/policy/task/ProcessorValidationPolicy.java index 16f7bd73..561345a1 100644 --- a/src/main/java/clap/server/domain/policy/task/ProcessorValidationPolicy.java +++ b/src/main/java/clap/server/domain/policy/task/ProcessorValidationPolicy.java @@ -9,7 +9,7 @@ @Policy public class ProcessorValidationPolicy { - public void validateProcessor(Long processorId, Task targetTask) { + public void validateProcessor(final Long processorId,final 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 index ce1eb38a..2c2da4b1 100644 --- a/src/main/java/clap/server/domain/policy/task/RequestedTaskUpdatePolicy.java +++ b/src/main/java/clap/server/domain/policy/task/RequestedTaskUpdatePolicy.java @@ -8,7 +8,7 @@ @Policy public class RequestedTaskUpdatePolicy { - public void validateTaskRequested(Task task) { + public void validateTaskRequested(final 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/TaskCommentPolicy.java b/src/main/java/clap/server/domain/policy/task/TaskCommentPolicy.java index 663bffea..ce52fbae 100644 --- a/src/main/java/clap/server/domain/policy/task/TaskCommentPolicy.java +++ b/src/main/java/clap/server/domain/policy/task/TaskCommentPolicy.java @@ -11,7 +11,7 @@ @Policy public class TaskCommentPolicy { - public void validateCommentPermission(Task task, Member member) { + public void validateCommentPermission(final Task task,final Member member) { boolean isUser = member.getMemberInfo().getRole() == MemberRole.ROLE_USER; boolean isNotRequester = !Objects.equals(member.getMemberId(), task.getRequester().getMemberId()); diff --git a/src/main/java/clap/server/exception/code/MemberErrorCode.java b/src/main/java/clap/server/exception/code/MemberErrorCode.java index 09e20f4b..48c632aa 100644 --- a/src/main/java/clap/server/exception/code/MemberErrorCode.java +++ b/src/main/java/clap/server/exception/code/MemberErrorCode.java @@ -9,15 +9,16 @@ public enum MemberErrorCode implements BaseErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER_001", "회원을 찾을 수 없습니다."), ACTIVE_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER_002", "활성화 회원을 찾을 수 없습니다."), - NOT_A_REVIEWER(HttpStatus.FORBIDDEN, "MEMBER_003", "리뷰어 권한이 없습니다."), - COMMENT_PERMISSION_DENIED(HttpStatus.FORBIDDEN, "MEMBER_005", "댓글 권한이 없습니다."), + NOT_A_REVIEWER(HttpStatus.FORBIDDEN, "MEMBER_003", "승인 권한이 없습니다."), + COMMENT_PERMISSION_DENIED(HttpStatus.FORBIDDEN, "MEMBER_005", "댓글 작성 권한이 없습니다."), PASSWORD_VERIFY_FAILED(HttpStatus.BAD_REQUEST, "MEMBER_006", "비밀번호 검증에 실패하였습니다"), INVALID_CSV_FORMAT(HttpStatus.BAD_REQUEST, "MEMBER_007", "CSV 파일 형식이 잘못되었습니다."), CSV_PARSING_ERROR(HttpStatus.BAD_REQUEST, "MEMBER_008", "CSV 데이터 파싱 중 오류가 발생했습니다."), - MEMBER_REGISTRATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "MEMBER_009", "담당자만 리뷰 권한이 있습니다."), + REVIEW_PERMISSION_DENIED(HttpStatus.BAD_REQUEST, "MEMBER_009", "담당자만 리뷰 권한이 있습니다."), NAME_CANNOT_BE_EMPTY(HttpStatus.BAD_REQUEST, "MEMBER_010", "이름은 공백일 수 없습니다."), DUPLICATE_NICKNAME(HttpStatus.BAD_REQUEST,"MEMBER_011", "중복된 닉네임입니다"), - DUPLICATE_NICKNAME_OR_EMAIL(HttpStatus.BAD_REQUEST, "MEMBER_012", "중복된 닉네임이나 email이 존재합니다") + DUPLICATE_NICKNAME_OR_EMAIL(HttpStatus.BAD_REQUEST, "MEMBER_012", "중복된 닉네임이나 email이 존재합니다"), + MANAGER_PERMISSION_DENIED(HttpStatus.BAD_REQUEST, "MEMBER_013", "담당자 권한이 없는 부서입니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/resources/db/migration/dev/V20250208335__Add_Is_Manager_To_Departement.sql b/src/main/resources/db/migration/dev/V20250208335__Add_Is_Manager_To_Departement.sql new file mode 100644 index 00000000..a657f947 --- /dev/null +++ b/src/main/resources/db/migration/dev/V20250208335__Add_Is_Manager_To_Departement.sql @@ -0,0 +1,2 @@ +ALTER TABLE department + ADD is_manager BOOLEAN NOT NULL DEFAULT false; \ No newline at end of file diff --git a/src/test/java/clap/server/TestDataFactory.java b/src/test/java/clap/server/TestDataFactory.java index d3cd1fd0..31f1ac69 100644 --- a/src/test/java/clap/server/TestDataFactory.java +++ b/src/test/java/clap/server/TestDataFactory.java @@ -1,6 +1,5 @@ package clap.server; -import clap.server.adapter.inbound.web.dto.task.request.FilterTaskListRequest; import clap.server.adapter.outbound.persistense.entity.member.constant.DepartmentStatus; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; @@ -80,25 +79,57 @@ public static Member createUser() { } public static MemberInfo createAdminInfo() { - return new MemberInfo("홍길동(관리자)", "atom8426@naver.com", "atom.admin", false, null, MemberRole.ROLE_ADMIN, "인프라"); + return MemberInfo.builder() + .name("홍길동(관리자)") + .email("atom8426@naver.com") + .nickname("atom.admin") + .isReviewer(false) + .department(null) + .role(MemberRole.ROLE_ADMIN) + .departmentRole("인프라") + .build(); } public static MemberInfo createManagerWithReviewerInfo() { - return new MemberInfo("홍길동(리뷰어)", "atom8426@naver.com", "atom.manager", true, null, MemberRole.ROLE_MANAGER, "인프라"); + return MemberInfo.builder() + .name("홍길동(리뷰어)") + .email("atom8426@naver.com") + .nickname("atom.manager") + .isReviewer(true) + .department(null) + .role(MemberRole.ROLE_MANAGER) + .departmentRole("인프라") + .build(); } public static MemberInfo createManagerInfo() { - return new MemberInfo("홍길동(매니저)", "atom8426@naver.com", "atom.manager", false, null, MemberRole.ROLE_MANAGER, "인프라"); + return MemberInfo.builder() + .name("홍길동(매니저)") + .email("atom8426@naver.com") + .nickname("atom.manager") + .isReviewer(false) + .department(null) + .role(MemberRole.ROLE_MANAGER) + .departmentRole("인프라") + .build(); } public static MemberInfo createUserInfo() { - return new MemberInfo("홍길동(사용자)", "atom8426@naver.com", "atom.user", false, null, MemberRole.ROLE_USER, "인프라"); + return MemberInfo.builder() + .name("홍길동(사용자)") + .email("atom8426@naver.com") + .nickname("atom.user") + .isReviewer(false) + .department(null) + .role(MemberRole.ROLE_USER) + .departmentRole("인프라") + .build(); } public static Department createDepartment() { return Department.builder() .departmentId(1L) - .adminId(1L) + .adminId(null) .name("IT 부서") .status(DepartmentStatus.ACTIVE) .build(); diff --git a/src/test/java/clap/server/application/service/admin/RegisterMemberCsvServiceTest.java b/src/test/java/clap/server/application/service/admin/RegisterMemberCsvServiceTest.java index 4188197e..e71c339e 100644 --- a/src/test/java/clap/server/application/service/admin/RegisterMemberCsvServiceTest.java +++ b/src/test/java/clap/server/application/service/admin/RegisterMemberCsvServiceTest.java @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.Set; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; @@ -142,8 +143,8 @@ void testRegisterMembersFromCsv_duplicateThrowsException() throws Exception { when(memberService.findActiveMember(adminId)).thenReturn(adminMember); // 중복 체크: 닉네임 또는 email 중 하나라도 중복이 있으면 에러 발생 - when(loadMemberPort.findByNickname(dummyMemberInfo1.getNickname())) - .thenReturn(Optional.of(mock(Member.class))); + when(loadMemberPort.existsByNicknamesOrEmails(Set.of(dummyMemberInfo1.getNickname()), Set.of(dummyMemberInfo1.getEmail()))) + .thenReturn(true); ApplicationException exception = assertThrows(ApplicationException.class, () -> registerMemberCSVService.registerMembersFromCsv(adminId, file)