diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindAllCategoryResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindAllCategoryResponse.java index 91799bba..5878ba80 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindAllCategoryResponse.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindAllCategoryResponse.java @@ -1,11 +1,20 @@ package clap.server.adapter.inbound.web.dto.admin.response; +import io.swagger.v3.oas.annotations.media.Schema; + import java.util.List; public record FindAllCategoryResponse( - Long id, + @Schema(description = "메인 카테고리 ID", example = "1") + Long mainCategoryId, + + @Schema(description = "카테고리 이름", example = "VM 관련") String name, + + @Schema(description = "카테고리 코드", example = "VM") String code, + + @Schema(description = "서브 카테고리 목록") List subCategory ) { } diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindMainCategoryResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindMainCategoryResponse.java index 8d8fce5b..e80cdded 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindMainCategoryResponse.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindMainCategoryResponse.java @@ -1,8 +1,15 @@ package clap.server.adapter.inbound.web.dto.admin.response; +import io.swagger.v3.oas.annotations.media.Schema; + public record FindMainCategoryResponse( - Long id, + @Schema(description = "메인 카테고리 ID", example = "1") + Long mainCategoryId, + + @Schema(description = "카테고리 이름", example = "VM 관련") String name, + + @Schema(description = "카테고리 코드", example = "VM") String code ) { } diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindSubCategoryResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindSubCategoryResponse.java index 5dc588b7..49ad3504 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindSubCategoryResponse.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/FindSubCategoryResponse.java @@ -1,10 +1,21 @@ package clap.server.adapter.inbound.web.dto.admin.response; +import io.swagger.v3.oas.annotations.media.Schema; + public record FindSubCategoryResponse( - Long id, + @Schema(description = "서브 카테고리 ID", example = "4") + Long subCategoryId, + + @Schema(description = "메인 카테고리 ID", example = "1") Long mainCategoryId, + + @Schema(description = "카테고리 이름", example = "VM 수정") String name, + + @Schema(description = "카테고리 코드", example = "VU") String code, + + @Schema(description = "카테고리 설명 예시", example = "VM을 수정합니다.") String descriptionExample ) { } diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/RetrieveAllMemberResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/RetrieveAllMemberResponse.java index d9ff178f..b9bbf956 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/RetrieveAllMemberResponse.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/admin/response/RetrieveAllMemberResponse.java @@ -1,6 +1,7 @@ package clap.server.adapter.inbound.web.dto.admin.response; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; @@ -31,7 +32,10 @@ public record RetrieveAllMemberResponse( String departmentRole, @Schema(description = "가입일", example = "2024-01-01T12:00:00") - LocalDateTime createdAt + LocalDateTime createdAt, + + @Schema(description = "회원 상태", example = "ACTIVE") + MemberStatus memberStatus ) {} diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/history/request/EditCommentRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/history/request/EditCommentRequest.java index ca1453bc..9c346d86 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/history/request/EditCommentRequest.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/history/request/EditCommentRequest.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +@Deprecated public record EditCommentRequest( @Schema(description = "댓글 내용") @NotBlank diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/log/request/FilterLogRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/log/request/FilterLogRequest.java index cbdea698..f18da370 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/log/request/FilterLogRequest.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/log/request/FilterLogRequest.java @@ -14,8 +14,7 @@ public record FilterLogRequest( Integer term, @NotNull - @Schema(description = "로그 상태 목록", - example = "[\"LOGIN\", \"ASSIGNER_CHANGED\", \"COMMENT_ADDED\"]") + @Schema(description = "로그 상태 목록") List logStatus, @NotNull diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/member/request/SendVerificationCodeRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/member/request/SendVerificationCodeRequest.java index c7c07367..fe519557 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/member/request/SendVerificationCodeRequest.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/member/request/SendVerificationCodeRequest.java @@ -2,6 +2,7 @@ import jakarta.validation.constraints.NotBlank; +@Deprecated public record SendVerificationCodeRequest( @NotBlank String nickname, diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/member/request/VerifyCodeRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/member/request/VerifyCodeRequest.java index f7df8e2f..162bffa3 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/member/request/VerifyCodeRequest.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/member/request/VerifyCodeRequest.java @@ -3,6 +3,7 @@ import jakarta.validation.constraints.NotBlank; +@Deprecated public record VerifyCodeRequest( @NotBlank String email, diff --git a/src/main/java/clap/server/adapter/outbound/api/agit/AgitTemplateBuilder.java b/src/main/java/clap/server/adapter/outbound/api/agit/AgitTemplateBuilder.java index 452956bb..cac0c9f1 100644 --- a/src/main/java/clap/server/adapter/outbound/api/agit/AgitTemplateBuilder.java +++ b/src/main/java/clap/server/adapter/outbound/api/agit/AgitTemplateBuilder.java @@ -42,19 +42,19 @@ public String createPayLoad(PushNotificationTemplate request, Task task, String public String createMessage(PushNotificationTemplate request, String taskDetailUrl) { - return switch (request.notificationType()) { - case TASK_REQUESTED -> "📌 *새 작업 요청:* `" + request.taskName() + "`\\n" - + "\\t\\t*•요청자: " + request.senderName() + "*\\n" - + "[확인하러 가기](" + taskDetailUrl + ")"; - case STATUS_SWITCHED -> "⚙️ *작업 상태 변경:* `" + request.taskName() + "\\n" - + "\\t\\t*•작업 상태: " + request.message() + "*\\n" - + "[확인하러 가기](" + taskDetailUrl + ")"; - case PROCESSOR_CHANGED -> "🔄 *담당자 변경:* `" + request.taskName() + "\\n" - + "\\t\\t*•새 담당자: " + request.message() + "*\\n" - + "[확인하러 가기](" + taskDetailUrl + ")"; - case PROCESSOR_ASSIGNED -> "👤 *작업 담당자 배정:* `" + request.taskName() + "\\n" - + "\\t\\t*•담당자: " + request.message() + "*\\n" - + "[확인하러 가기](" + taskDetailUrl + ")"; + return switch (request.notificationType()) { + case TASK_REQUESTED -> "📌 *새 작업이 요청되었습니다.*\\n" + + "\\t\\t*• 🔖 작업명:* " + "*" + request.taskName() + "*" + "\\n" + + "\\t\\t*• 🙋 요청자:* " + "*" + request.senderName() + "*" + "\\n\\n" + + "\\t[자세히 보기](" + taskDetailUrl + ")"; + + case STATUS_SWITCHED -> "작업 상태가 " + "*" + request.message() + "*" + "으로 변경되었습니다."; + + case PROCESSOR_CHANGED -> "담당자가 " + "*" + request.message() + "*" + "으로 변경되었습니다."; + + case PROCESSOR_ASSIGNED -> "작업이 *승인*되었습니다.*\n" + + "\\t\\t*• 👤 담당자:* " + "*" + request.message() + "*"; + default -> null; }; } diff --git a/src/main/java/clap/server/adapter/outbound/api/data/PushNotificationTemplate.java b/src/main/java/clap/server/adapter/outbound/api/data/PushNotificationTemplate.java index 2a077a4b..154df739 100644 --- a/src/main/java/clap/server/adapter/outbound/api/data/PushNotificationTemplate.java +++ b/src/main/java/clap/server/adapter/outbound/api/data/PushNotificationTemplate.java @@ -9,6 +9,7 @@ public record PushNotificationTemplate( String taskName, String senderName, String message, - String commenterName + String commenterName, + String reason ) { } diff --git a/src/main/java/clap/server/adapter/outbound/api/email/EmailTemplateBuilder.java b/src/main/java/clap/server/adapter/outbound/api/email/EmailTemplateBuilder.java index a6a7ec49..bfdd1786 100644 --- a/src/main/java/clap/server/adapter/outbound/api/email/EmailTemplateBuilder.java +++ b/src/main/java/clap/server/adapter/outbound/api/email/EmailTemplateBuilder.java @@ -30,12 +30,20 @@ public EmailTemplate createWebhookTemplate(PushNotificationTemplate request, Str context.setVariable("title", request.taskName()); break; case STATUS_SWITCHED: - templateName = "status-switched"; - subject = "[TaskFlow] "+ request.taskName()+ " 작업의 상태가 " + request.message() + "으로 변경되었습니다."; - context.setVariable("taskDetailUrl", taskDetailUrl); - context.setVariable("receiverName", request.senderName()); - context.setVariable("title", request.taskName()); - context.setVariable("status", request.message()); + if (request.message().equals("TERMINATED")) { + templateName = "task-terminated"; + subject = "[TaskFlow] " + request.taskName() + " 작업이 종료되었습니다."; + context.setVariable("taskDetailUrl", taskDetailUrl); + context.setVariable("reason", request.reason()); + context.setVariable("title", request.taskName()); + } else { + templateName = "status-switched"; + subject = "[TaskFlow] "+ request.taskName()+ " 작업의 상태가 " + request.message() + "으로 변경되었습니다."; + context.setVariable("taskDetailUrl", taskDetailUrl); + context.setVariable("receiverName", request.senderName()); + context.setVariable("title", request.taskName()); + context.setVariable("status", request.message()); + } break; case PROCESSOR_CHANGED: templateName = "processor-changed"; diff --git a/src/main/java/clap/server/adapter/outbound/api/kakaoWork/KakaoWorkBlockBuilder.java b/src/main/java/clap/server/adapter/outbound/api/kakaoWork/KakaoWorkBlockBuilder.java index 97a5399f..9b450c61 100644 --- a/src/main/java/clap/server/adapter/outbound/api/kakaoWork/KakaoWorkBlockBuilder.java +++ b/src/main/java/clap/server/adapter/outbound/api/kakaoWork/KakaoWorkBlockBuilder.java @@ -17,7 +17,10 @@ public class KakaoWorkBlockBuilder { public String makeObjectBlock(PushNotificationTemplate request, String taskDetailUrl){ return switch (request.notificationType()) { case TASK_REQUESTED -> makeTaskRequestBlock(request, taskDetailUrl); - case STATUS_SWITCHED -> makeTaskStatusBlock(request, taskDetailUrl); + case STATUS_SWITCHED -> switch (request.message()) { // getStatusChangeType() 메서드로 추가 분기 + case "TERMINATED" -> makeTerminatedStatusBlock(request, taskDetailUrl); + default -> makeTaskStatusBlock(request, taskDetailUrl); + }; case PROCESSOR_CHANGED -> makeProcessorChangeBlock(request, taskDetailUrl); case PROCESSOR_ASSIGNED -> makeNewProcessorBlock(request, taskDetailUrl); case COMMENT -> makeCommentBlock(request, taskDetailUrl); @@ -366,4 +369,72 @@ private String makeTaskStatusBlock(PushNotificationTemplate request, String task return payload; } + + private String makeTerminatedStatusBlock(PushNotificationTemplate request, String taskDetailUrl) { + Object[] blocks = new Object[]{ + Map.of( + "type", "header", + "text", "TaskFlow 알림 서비스", + "style", "blue" + ), + Map.of( + "type", "text", + "text", "TaskFlow 작업 종료 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", "TaskFlow 작업 종료 알림", + "bold", true + ) + } + ), + Map.of( + "type", "text", + "text", "TaskFlow 작업 종료 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", " - Task Title : " + request.taskName(), + "bold", false + ) + } + ), + Map.of( + "type", "text", + "text", "TaskFlow 작업 종료 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", " - 거절 사유 : " + request.reason(), + "bold", false + ) + } + ), + Map.of( + "type", "button", + "text", "확인하기", + "style", "default", + "action", Map.of( + "type", "open_system_browser", + "name", "button1", + "value", taskDetailUrl + ) + ) + }; + + String payload; + try { + payload = "{" + + "\"email\":\"" + request.email() + "\"," + + "\"text\":\"작업 종료 알림\"," + + "\"blocks\":" + objectMapper.writeValueAsString(blocks) + + "}"; + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + return payload; + } + + } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/CategoryPersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/CategoryPersistenceAdapter.java index e99883db..c34d1539 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/CategoryPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/CategoryPersistenceAdapter.java @@ -50,10 +50,17 @@ public List findSubCategory() { } @Override - public boolean existsByNameOrCode(String name, String code) { - return categoryRepository.existsByNameOrCodeAndIsDeletedFalse(name, code); + public boolean existsMainCategoryByNameOrCode(String name, String code) { + return categoryRepository.existsByNameOrCodeAndMainCategoryIsNullAndIsDeletedFalse(name, code); } + @Override + public boolean existsSubCategoryByNameOrCode(Category category, String name, String code) { + CategoryEntity categoryEntity = categoryPersistenceMapper.toEntity(category); + return categoryRepository.existsByMainCategoryAndIsDeletedFalseAndNameOrCode(categoryEntity, name, code); + } + + @Override public void save(final Category category) { categoryRepository.save(categoryPersistenceMapper.toEntity(category)); diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/log/AnonymousLogCustomRepositoryImpl.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/log/AnonymousLogCustomRepositoryImpl.java index 80cc61b2..4fd3e7e4 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/log/AnonymousLogCustomRepositoryImpl.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/log/AnonymousLogCustomRepositoryImpl.java @@ -29,9 +29,6 @@ public Page filterAnonymousLogs(FilterLogRequest request, Pa LocalDateTime fromDate = LocalDateTime.now().minusHours(request.term()); builder.and(anonymousLogEntity.requestAt.after(fromDate)); } - if (!request.logStatus().isEmpty()) { - builder.and(anonymousLogEntity.logStatus.in(request.logStatus())); - } if (!request.nickName().isEmpty()) { builder.and(anonymousLogEntity.loginNickname.contains(request.nickName())); } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/notification/NotificationRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/notification/NotificationRepository.java index e4462dd5..0a8b86e1 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/notification/NotificationRepository.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/notification/NotificationRepository.java @@ -5,6 +5,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; 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; @@ -13,7 +15,14 @@ @Repository public interface NotificationRepository extends JpaRepository { - Slice findAllByReceiver_MemberIdOrderByCreatedAtDesc(Long receiverId, Pageable pageable); + @Query("SELECT n FROM NotificationEntity n " + + "JOIN FETCH n.task t " + + "JOIN FETCH n.receiver r " + + "WHERE n.receiver.memberId = :receiverId " + + "AND t.isDeleted = false " + + "ORDER BY n.createdAt DESC") + Slice findAllByReceiver_MemberIdOrderByCreatedAtDesc( + @Param("receiverId") Long receiverId, Pageable pageable); List findAllByReceiver_MemberId(Long memberId); diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/CategoryRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/CategoryRepository.java index 5cdcc2b1..b2cdf46c 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/CategoryRepository.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/CategoryRepository.java @@ -1,6 +1,8 @@ package clap.server.adapter.outbound.persistense.repository.task; import clap.server.adapter.outbound.persistense.entity.task.CategoryEntity; 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; @@ -12,5 +14,9 @@ public interface CategoryRepository extends JpaRepository List findByIsDeletedFalseAndMainCategoryIsNull(); List findByIsDeletedFalseAndMainCategoryIsNotNull(); - boolean existsByNameOrCodeAndIsDeletedFalse(String name, String code); + @Query("SELECT CASE WHEN COUNT(c) > 0 THEN true ELSE false END FROM CategoryEntity c WHERE c.mainCategory IS NULL AND c.isDeleted = false AND (c.name = :name OR c.code = :code)") + boolean existsByNameOrCodeAndMainCategoryIsNullAndIsDeletedFalse(@Param("name") String name, @Param("code") String code); + + @Query("SELECT CASE WHEN COUNT(c) > 0 THEN true ELSE false END FROM CategoryEntity c WHERE c.mainCategory = :mainCategory AND c.isDeleted = false AND (c.name = :name OR c.code = :code)") + boolean existsByMainCategoryAndIsDeletedFalseAndNameOrCode(@Param("mainCategory")CategoryEntity mainCategory, @Param("name") String name, @Param("code") String code); } \ No newline at end of file diff --git a/src/main/java/clap/server/application/mapper/response/AdminResponseMapper.java b/src/main/java/clap/server/application/mapper/response/AdminResponseMapper.java index 68bf27c9..01153abc 100644 --- a/src/main/java/clap/server/application/mapper/response/AdminResponseMapper.java +++ b/src/main/java/clap/server/application/mapper/response/AdminResponseMapper.java @@ -5,6 +5,7 @@ import java.util.List; + public class AdminResponseMapper { private AdminResponseMapper() { throw new IllegalStateException("Utility class"); @@ -26,7 +27,8 @@ public static RetrieveAllMemberResponse toRetrieveAllMemberResponse(Member membe member.getMemberInfo().getDepartment().getName(), member.getMemberInfo().getRole(), member.getMemberInfo().getDepartmentRole(), - member.getCreatedAt() + member.getCreatedAt(), + member.getStatus() ); } } diff --git a/src/main/java/clap/server/application/mapper/response/CategoryResponseMapper.java b/src/main/java/clap/server/application/mapper/response/CategoryResponseMapper.java index 3be73c7d..43751c5d 100644 --- a/src/main/java/clap/server/application/mapper/response/CategoryResponseMapper.java +++ b/src/main/java/clap/server/application/mapper/response/CategoryResponseMapper.java @@ -11,11 +11,11 @@ public class CategoryResponseMapper { private CategoryResponseMapper() {throw new IllegalStateException("Utility class");} public static FindAllCategoryResponse toFindAllCategoryResponse( - Long id, + Long mainCategoryId, String name, String code, List subCategoryResponses) { - return new FindAllCategoryResponse(id, name, code, subCategoryResponses); + return new FindAllCategoryResponse(mainCategoryId, name, code, subCategoryResponses); } public static FindMainCategoryResponse toFindMainCategoryResponse(Category category) { diff --git a/src/main/java/clap/server/application/port/outbound/task/LoadCategoryPort.java b/src/main/java/clap/server/application/port/outbound/task/LoadCategoryPort.java index 2533e8ff..9349f583 100644 --- a/src/main/java/clap/server/application/port/outbound/task/LoadCategoryPort.java +++ b/src/main/java/clap/server/application/port/outbound/task/LoadCategoryPort.java @@ -12,5 +12,7 @@ public interface LoadCategoryPort { List findMainCategory(); List findSubCategory(); - boolean existsByNameOrCode(String name, String code); + boolean existsMainCategoryByNameOrCode(String name, String code); + + boolean existsSubCategoryByNameOrCode(Category category, String name, String code); } diff --git a/src/main/java/clap/server/application/service/admin/AddCategoryService.java b/src/main/java/clap/server/application/service/admin/AddCategoryService.java index 4b934dc4..ff2476cb 100644 --- a/src/main/java/clap/server/application/service/admin/AddCategoryService.java +++ b/src/main/java/clap/server/application/service/admin/AddCategoryService.java @@ -29,7 +29,7 @@ public class AddCategoryService implements AddMainCategoryUsecase, AddSubCategor @Transactional public void addMainCategory(Long adminId, String code, String name) { Optional activeMember = loadMemberPort.findActiveMemberById(adminId); - if (loadCategoryPort.existsByNameOrCode(name, code)) throw new ApplicationException(CATEGORY_DUPLICATE); + if (loadCategoryPort.existsMainCategoryByNameOrCode(name, code)) throw new ApplicationException(CATEGORY_DUPLICATE); Category mainCategory = Category.createMainCategory( activeMember.orElseThrow(() -> new ApplicationException(ACTIVE_MEMBER_NOT_FOUND)), code, name); @@ -39,13 +39,11 @@ public void addMainCategory(Long adminId, String code, String name) { @Override @Transactional public void addSubCategory(Long adminId, Long mainCategoryId, String code, String name, String descriptionExample) { - Optional activeMember = loadMemberPort.findActiveMemberById(adminId); - Optional mainCategory = loadCategoryPort.findById(mainCategoryId); - if (loadCategoryPort.existsByNameOrCode(name, code)) throw new ApplicationException(CATEGORY_DUPLICATE); - Category subCategory = Category.createSubCategory( - activeMember.orElseThrow(() -> new ApplicationException(ACTIVE_MEMBER_NOT_FOUND)), - mainCategory.orElseThrow(() -> new ApplicationException(CATEGORY_NOT_FOUND)), - code, name, descriptionExample); + Member activeMember = loadMemberPort.findActiveMemberById(adminId).orElseThrow(() -> new ApplicationException(ACTIVE_MEMBER_NOT_FOUND)); + Category mainCategory = loadCategoryPort.findById(mainCategoryId).orElseThrow(() -> new ApplicationException(CATEGORY_NOT_FOUND)); + + if (loadCategoryPort.existsSubCategoryByNameOrCode(mainCategory, name, code)) throw new ApplicationException(CATEGORY_DUPLICATE); + Category subCategory = Category.createSubCategory(activeMember, mainCategory,code, name, descriptionExample); commandCategoryPort.save(subCategory); } } \ No newline at end of file diff --git a/src/main/java/clap/server/application/service/admin/SendInvitationEmailService.java b/src/main/java/clap/server/application/service/admin/SendInvitationEmailService.java index f7795289..06c2e1a3 100644 --- a/src/main/java/clap/server/application/service/admin/SendInvitationEmailService.java +++ b/src/main/java/clap/server/application/service/admin/SendInvitationEmailService.java @@ -19,11 +19,7 @@ public void sendInvitationEmail(String memberEmail, String receiverName, String try { sendEmailPort.sendInvitationEmail(memberEmail, receiverName, initialPassword, userNickname); } catch (Exception e) { - log.error("Failed to send new password email to: {}", memberEmail, e); } - }).exceptionally(ex -> { - log.error("Unexpected error occurred while sending new password email", ex); - return null; - }); + }).exceptionally(ex -> null); } } \ No newline at end of file diff --git a/src/main/java/clap/server/application/service/admin/SendInvitationService.java b/src/main/java/clap/server/application/service/admin/SendInvitationService.java index f350726c..86653863 100644 --- a/src/main/java/clap/server/application/service/admin/SendInvitationService.java +++ b/src/main/java/clap/server/application/service/admin/SendInvitationService.java @@ -32,10 +32,12 @@ public void sendInvitation(SendInvitationRequest request) { String encodedPassword = passwordEncoder.encode(initialPassword); member.resetPassword(encodedPassword); - commandMemberPort.save(member); member.changeToApproveRequested(); + commandMemberPort.save(member); + + sendInvitationEmailService.sendInvitationEmail( member.getMemberInfo().getEmail(), member.getMemberInfo().getName(), diff --git a/src/main/java/clap/server/application/service/admin/UpdateCategoryService.java b/src/main/java/clap/server/application/service/admin/UpdateCategoryService.java index cae97710..4c06415b 100644 --- a/src/main/java/clap/server/application/service/admin/UpdateCategoryService.java +++ b/src/main/java/clap/server/application/service/admin/UpdateCategoryService.java @@ -26,9 +26,15 @@ public class UpdateCategoryService implements UpdateCategoryUsecase { @Transactional public void updateCategory(Long adminId, Long categoryId, String name, String code, String descriptionExample) { Member admin = loadMemberPort.findActiveMemberById(adminId).orElseThrow(() -> new ApplicationException(ACTIVE_MEMBER_NOT_FOUND)); - if (loadCategoryPort.existsByNameOrCode(name, code)) throw new ApplicationException(CATEGORY_DUPLICATE); - Category category = loadCategoryPort.findById(categoryId) - .orElseThrow(() -> new ApplicationException(CATEGORY_NOT_FOUND)); + Category category = loadCategoryPort.findById(categoryId).orElseThrow(() -> new ApplicationException(CATEGORY_NOT_FOUND)); + boolean isDuplicate; + if (category.getMainCategory() == null) { + isDuplicate = loadCategoryPort.existsMainCategoryByNameOrCode(name, code); + } else { + isDuplicate = loadCategoryPort.existsSubCategoryByNameOrCode(category.getMainCategory(), name, code); + } + if (isDuplicate) throw new ApplicationException(CATEGORY_DUPLICATE); + category.updateCategory(admin, name, code, descriptionExample); commandCategoryPort.save(category); } diff --git a/src/main/java/clap/server/application/service/history/PostCommentService.java b/src/main/java/clap/server/application/service/history/PostCommentService.java index eaa20d31..1f08e2ba 100644 --- a/src/main/java/clap/server/application/service/history/PostCommentService.java +++ b/src/main/java/clap/server/application/service/history/PostCommentService.java @@ -97,6 +97,6 @@ private String saveAttachment(MultipartFile file, Task task, Comment comment) { private void publishNotification(Member receiver, Task task, String message, String commenterName) { boolean isManager = receiver.getMemberInfo().getRole() == MemberRole.ROLE_MANAGER; - sendNotificationService.sendPushNotification(receiver, NotificationType.COMMENT, task, message, commenterName, isManager); + sendNotificationService.sendPushNotification(receiver, NotificationType.COMMENT, task, message, null, commenterName, isManager); } } diff --git a/src/main/java/clap/server/application/service/log/FindApiLogsService.java b/src/main/java/clap/server/application/service/log/FindApiLogsService.java index 83ba4a49..c360f176 100644 --- a/src/main/java/clap/server/application/service/log/FindApiLogsService.java +++ b/src/main/java/clap/server/application/service/log/FindApiLogsService.java @@ -24,7 +24,7 @@ public class FindApiLogsService implements FindApiLogsUsecase { @Override public PageResponse filterAnonymousLogs(FilterLogRequest anonymousLogRequest, Pageable pageable, String sortDirection) { Page anonymousLogs = loadLogPort.filterAnonymousLogs(anonymousLogRequest, pageable, sortDirection); - Page anonymousLogResponses = anonymousLogs.map(anonymousLog -> LogResponseMapper.toAnonymousLogResponse(anonymousLog)); + Page anonymousLogResponses = anonymousLogs.map(LogResponseMapper::toAnonymousLogResponse); return PageResponse.from(anonymousLogResponses); } 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 26dbd793..2fb3e305 100644 --- a/src/main/java/clap/server/application/service/task/ApprovalTaskService.java +++ b/src/main/java/clap/server/application/service/task/ApprovalTaskService.java @@ -75,7 +75,7 @@ private void publishNotification(List receivers, Task task, String proce receivers.forEach(receiver -> { boolean isManager = receiver.getMemberInfo().getRole() == MemberRole.ROLE_MANAGER; sendNotificationService.sendPushNotification(receiver, NotificationType.PROCESSOR_ASSIGNED, - task, processorName, null, isManager); + task, processorName, null, null, isManager); }); sendNotificationService.sendAgitNotification(NotificationType.PROCESSOR_ASSIGNED, task, processorName, 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 d29c0d50..60c6f010 100644 --- a/src/main/java/clap/server/application/service/task/CreateTaskService.java +++ b/src/main/java/clap/server/application/service/task/CreateTaskService.java @@ -70,7 +70,7 @@ private void publishNotification(Task task) { reviewers.forEach(reviewer -> { boolean isManager = reviewer.getMemberInfo().getRole() == MemberRole.ROLE_MANAGER; sendNotificationService.sendPushNotification(reviewer, NotificationType.TASK_REQUESTED, - task, null, null, isManager); + task, null, null, null, isManager); }); sendNotificationService.sendAgitNotification(NotificationType.TASK_REQUESTED, diff --git a/src/main/java/clap/server/application/service/task/TerminateTaskService.java b/src/main/java/clap/server/application/service/task/TerminateTaskService.java index 43281dfd..ecb92333 100644 --- a/src/main/java/clap/server/application/service/task/TerminateTaskService.java +++ b/src/main/java/clap/server/application/service/task/TerminateTaskService.java @@ -1,16 +1,22 @@ package clap.server.application.service.task; +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.port.inbound.domain.MemberService; import clap.server.application.port.inbound.domain.TaskService; import clap.server.application.port.inbound.task.TerminateTaskUsecase; import clap.server.application.port.outbound.taskhistory.CommandTaskHistoryPort; +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.Task; import clap.server.domain.model.task.TaskHistory; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @ApplicationService @RequiredArgsConstructor @Transactional @@ -18,6 +24,7 @@ public class TerminateTaskService implements TerminateTaskUsecase { private final MemberService memberService; private final TaskService taskService; private final CommandTaskHistoryPort commandTaskHistoryPort; + private final SendNotificationService sendNotificationService; @Override public void terminateTask(Long memberId, Long taskId, String reason) { @@ -28,5 +35,12 @@ public void terminateTask(Long memberId, Long taskId, String reason) { TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.TASK_TERMINATED, task, reason, null, null); commandTaskHistoryPort.save(taskHistory); + + publishNotification(task.getRequester(), task, String.valueOf(task.getTaskStatus()), reason); + + } + + private void publishNotification(Member receiver, Task task, String message, String reason) { + sendNotificationService.sendPushNotification(receiver, NotificationType.STATUS_SWITCHED, task, message, reason, null, false); } } 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 095efc53..44836fd6 100644 --- a/src/main/java/clap/server/application/service/task/UpdateTaskBoardService.java +++ b/src/main/java/clap/server/application/service/task/UpdateTaskBoardService.java @@ -159,7 +159,7 @@ public void updateTaskOrderAndStatus(Long processorId, UpdateTaskOrderRequest re TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.STATUS_SWITCHED, updatedTask, targetStatus.getDescription(), null,null); commandTaskHistoryPort.save(taskHistory); - publishNotification(targetTask, NotificationType.STATUS_SWITCHED, String.valueOf(updatedTask.getTaskStatus())); + publishNotification(targetTask); } /** @@ -181,14 +181,14 @@ public void validateRequest(UpdateTaskOrderRequest request, TaskStatus targetSta } } - private void publishNotification(Task task, NotificationType notificationType, String message) { + private void publishNotification(Task task) { List receivers = List.of(task.getRequester(), task.getProcessor()); receivers.forEach(receiver -> { boolean isManager = receiver.getMemberInfo().getRole() == MemberRole.ROLE_MANAGER; - sendNotificationService.sendPushNotification(receiver, notificationType, task, message, null, isManager); + sendNotificationService.sendPushNotification(receiver, NotificationType.STATUS_SWITCHED, task, String.valueOf(task.getTaskStatus()), null, null, isManager); }); - sendNotificationService.sendAgitNotification(notificationType, - task, message, null); + sendNotificationService.sendAgitNotification(NotificationType.STATUS_SWITCHED, + task, String.valueOf(task.getTaskStatus()), null); } } 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 64af78a5..403f8545 100644 --- a/src/main/java/clap/server/application/service/task/UpdateTaskService.java +++ b/src/main/java/clap/server/application/service/task/UpdateTaskService.java @@ -77,7 +77,6 @@ public void updateTask(Long requesterId, Long taskId, UpdateTaskRequest request, @Transactional public void updateTaskStatus(Long memberId, Long taskId, TaskStatus taskStatus) { memberService.findActiveMember(memberId); - memberService.findReviewer(memberId); Task task = taskService.findById(taskId); if (!TASK_UPDATABLE_STATUS.contains(taskStatus)) { @@ -99,7 +98,6 @@ public void updateTaskStatus(Long memberId, Long taskId, TaskStatus taskStatus) @Override public void updateTaskProcessor(Long taskId, Long memberId, UpdateTaskProcessorRequest request) { memberService.findActiveMember(memberId); - memberService.findReviewer(memberId); Task task = taskService.findById(taskId); Member processor = memberService.findById(request.processorId()); @@ -151,7 +149,7 @@ private void publishNotification(List receivers, Task task, Notification receivers.forEach(receiver -> { boolean isManager = receiver.getMemberInfo().getRole() == MemberRole.ROLE_MANAGER; sendNotificationService.sendPushNotification(receiver, notificationType, - task, message, null, isManager); + task, message, null, null, isManager); }); sendNotificationService.sendAgitNotification(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 index e3d28748..f28c4852 100644 --- a/src/main/java/clap/server/application/service/webhook/SendNotificationService.java +++ b/src/main/java/clap/server/application/service/webhook/SendNotificationService.java @@ -37,7 +37,7 @@ public class SendNotificationService { @Async("notificationExecutor") public void sendPushNotification(Member receiver, NotificationType notificationType, - Task task, String message, String commenterName, Boolean isManager) { + Task task, String message, String reason, String commenterName, Boolean isManager) { String email = receiver.getMemberInfo().getEmail(); String taskTitle = task.getTitle(); @@ -48,7 +48,7 @@ public void sendPushNotification(Member receiver, NotificationType notificationT Notification notification = createTaskNotification(task, receiver, notificationType, message, taskTitle); PushNotificationTemplate pushNotificationTemplate = new PushNotificationTemplate( - email, notificationType, taskTitle, requesterNickname, message, commenterName + email, notificationType, taskTitle, requesterNickname, message, commenterName, reason ); CompletableFuture saveNotification = CompletableFuture.runAsync(() -> { @@ -95,7 +95,8 @@ public void sendAgitNotification(NotificationType notificationType, task.getTitle(), task.getRequester().getNickname(), message, - commenterName + commenterName, + null ); String taskDetailUrl = extractTaskUrl(notificationType, task, true); diff --git a/src/main/java/clap/server/common/annotation/validation/password/ValidPassword.java b/src/main/java/clap/server/common/annotation/validation/password/ValidPassword.java index 8b8b436b..bd7e0b78 100644 --- a/src/main/java/clap/server/common/annotation/validation/password/ValidPassword.java +++ b/src/main/java/clap/server/common/annotation/validation/password/ValidPassword.java @@ -1,5 +1,6 @@ package clap.server.common.annotation.validation.password; +import clap.server.domain.policy.member.PasswordPolicy; import jakarta.validation.Constraint; import jakarta.validation.Payload; @@ -10,7 +11,7 @@ @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = PasswordValidator.class) +@Constraint(validatedBy = PasswordPolicy.class) public @interface ValidPassword { String message() default "대문자, 소문자, 숫자, 특수문자를 포함하며 8자 이상이어야 합니다."; diff --git a/src/main/java/clap/server/common/annotation/validation/password/PasswordValidator.java b/src/main/java/clap/server/domain/policy/member/PasswordPolicy.java similarity index 53% rename from src/main/java/clap/server/common/annotation/validation/password/PasswordValidator.java rename to src/main/java/clap/server/domain/policy/member/PasswordPolicy.java index 03b82fd7..2237ee9e 100644 --- a/src/main/java/clap/server/common/annotation/validation/password/PasswordValidator.java +++ b/src/main/java/clap/server/domain/policy/member/PasswordPolicy.java @@ -1,13 +1,16 @@ -package clap.server.common.annotation.validation.password; +package clap.server.domain.policy.member; +import clap.server.common.annotation.architecture.Policy; +import clap.server.common.annotation.validation.password.ValidPassword; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import java.util.regex.Pattern; -public class PasswordValidator implements ConstraintValidator { +@Policy +public class PasswordPolicy implements ConstraintValidator { - private static final String PASSWORD_REGEX = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[!@#$%^&*()_+={}\\[\\]:;\"'<>,.?/\\\\|]).{8,}$"; + private static final String PASSWORD_REGEX = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[!@#$%^&*()_+{}\\[\\]:;<>,.?/~`-]).{8,20}$"; @Override public boolean isValid(String password, ConstraintValidatorContext context) { diff --git a/src/main/resources/templates/task-terminated.html b/src/main/resources/templates/task-terminated.html new file mode 100644 index 00000000..6c01732f --- /dev/null +++ b/src/main/resources/templates/task-terminated.html @@ -0,0 +1,86 @@ + + + + + Notion Notification + + + + + + diff --git a/src/test/java/clap/server/domain/policy/member/PasswordPolicyTest.java b/src/test/java/clap/server/domain/policy/member/PasswordPolicyTest.java new file mode 100644 index 00000000..6a39df2f --- /dev/null +++ b/src/test/java/clap/server/domain/policy/member/PasswordPolicyTest.java @@ -0,0 +1,85 @@ +package clap.server.domain.policy.member; + +import jakarta.validation.ConstraintValidatorContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class PasswordPolicyTest { + + private PasswordPolicy passwordPolicy; + private ConstraintValidatorContext context; + + @BeforeEach + void setUp() { + passwordPolicy = new PasswordPolicy(); + context = mock(ConstraintValidatorContext.class); // Mock ConstraintValidatorContext + } + + @Test + @DisplayName("유효한 비밀번호 - 대문자, 소문자, 숫자, 특수문자 포함") + void validPassword() { + String validPassword = "Abcdef1!"; + boolean result = passwordPolicy.isValid(validPassword, context); + assertThat(result).isTrue(); + } + + @Test + @DisplayName("유효하지 않은 비밀번호 - 대문자 없음") + void invalidPassword_noUpperCase() { + String invalidPassword = "abcdef1!"; + boolean result = passwordPolicy.isValid(invalidPassword, context); + assertThat(result).isFalse(); + } + + @Test + @DisplayName("유효하지 않은 비밀번호 - 소문자 없음") + void invalidPassword_noLowerCase() { + String invalidPassword = "ABCDEF1!"; + boolean result = passwordPolicy.isValid(invalidPassword, context); + assertThat(result).isFalse(); + } + + @Test + @DisplayName("유효하지 않은 비밀번호 - 숫자 없음") + void invalidPassword_noDigit() { + String invalidPassword = "Abcdefgh!"; + boolean result = passwordPolicy.isValid(invalidPassword, context); + assertThat(result).isFalse(); + } + + @Test + @DisplayName("유효하지 않은 비밀번호 - 특수문자 없음") + void invalidPassword_noSpecialCharacter() { + String invalidPassword = "Abcdefg1"; + boolean result = passwordPolicy.isValid(invalidPassword, context); + assertThat(result).isFalse(); + } + + @Test + @DisplayName("유효하지 않은 비밀번호 - 길이가 8자 미만") + void invalidPassword_tooShort() { + String invalidPassword = "Ab1!"; + boolean result = passwordPolicy.isValid(invalidPassword, context); + assertThat(result).isFalse(); + } + + @Test + @DisplayName("유효하지 않은 비밀번호 - 길이가 20자 초과") + void invalidPassword_tooLong() { + String invalidPassword = "Abcdefg1!Abcdefg1!Abcdefg1!"; + boolean result = passwordPolicy.isValid(invalidPassword, context); + assertThat(result).isFalse(); + } + + @Test + @DisplayName("유효하지 않은 비밀번호 - null 값") + void invalidPassword_null() { + String invalidPassword = null; + boolean result = passwordPolicy.isValid(invalidPassword, context); + assertThat(result).isFalse(); + } +}