From 60ebc4a180e3dac5eab81689957cc1c421560a52 Mon Sep 17 00:00:00 2001 From: nano-mm Date: Fri, 24 Jan 2025 03:55:27 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=EB=82=B4=20=EC=9E=91=EC=97=85=ED=95=9C=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=84=A4?= =?UTF-8?q?=EB=AA=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/dto/admin/FindManagersResponse.java | 15 ++ .../inbound/web/member/ManagerController.java | 23 +++ .../persistense/MemberPersistenceAdapter.java | 39 +++- .../persistense/TaskPersistenceAdapter.java | 2 + .../repository/member/MemberRepository.java | 8 +- .../repository/task/TaskRepository.java | 26 ++- .../application/mapper/MemberMapper.java | 2 +- .../inbound/domain/FindManagersUsecase.java | 8 + .../domain/FindManagersUsecaseImpl.java | 36 ++++ .../port/inbound/domain/MemberService.java | 34 +++- .../port/outbound/member/LoadMemberPort.java | 10 +- .../server/domain/model/member/Member.java | 4 + .../domain/model/member/MemberInfo.java | 1 + src/main/resources/application.yml | 6 +- src/main/resources/data.sql | 30 +++ src/main/resources/env.properties | 31 +++ src/main/resources/mysql.yml | 2 +- .../web/admin/MemberControllerTest.java | 188 ++++++++++++++++++ 18 files changed, 453 insertions(+), 12 deletions(-) create mode 100644 src/main/java/clap/server/adapter/inbound/web/dto/admin/FindManagersResponse.java create mode 100644 src/main/java/clap/server/adapter/inbound/web/member/ManagerController.java create mode 100644 src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecase.java create mode 100644 src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecaseImpl.java create mode 100644 src/main/resources/data.sql create mode 100644 src/main/resources/env.properties create mode 100644 src/test/java/clap/server/adapter/inbound/web/admin/MemberControllerTest.java diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/admin/FindManagersResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/admin/FindManagersResponse.java new file mode 100644 index 00000000..645268e6 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/admin/FindManagersResponse.java @@ -0,0 +1,15 @@ +package clap.server.adapter.inbound.web.dto.admin; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class FindManagersResponse { + + private Long memberId; + private String nickname; // 닉네임 필드 추가 + private String imageUrl; // 이미지 URL 필드 추가 + private int remainingTasks; + +} diff --git a/src/main/java/clap/server/adapter/inbound/web/member/ManagerController.java b/src/main/java/clap/server/adapter/inbound/web/member/ManagerController.java new file mode 100644 index 00000000..66c80ae6 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/member/ManagerController.java @@ -0,0 +1,23 @@ +package clap.server.adapter.inbound.web.member; + +import clap.server.application.port.inbound.domain.FindManagersUsecase; +import clap.server.adapter.inbound.web.dto.admin.FindManagersResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/manager") +@RequiredArgsConstructor +public class ManagerController { + + private final FindManagersUsecase findManagersUsecase; + + @GetMapping + public List findManagers() { + return findManagersUsecase.execute(); + } +} 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 c327d26f..b158176f 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java @@ -1,12 +1,21 @@ package clap.server.adapter.outbound.persistense; 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.mapper.MemberPersistenceMapper; import clap.server.adapter.outbound.persistense.repository.member.MemberRepository; import clap.server.application.port.outbound.member.LoadMemberPort; import clap.server.application.port.outbound.member.CommandMemberPort; 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 lombok.RequiredArgsConstructor; @@ -14,9 +23,12 @@ @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; + @Override public Optional findById(final Long id) { @@ -35,4 +47,29 @@ public void save(final Member member) { MemberEntity memberEntity = memberPersistenceMapper.toEntity(member); memberRepository.save(memberEntity); } + + @Override + public List findActiveManagers() { + List memberEntities = memberRepository.findByRoleAndStatus(MemberRole.valueOf("ROLE_MANAGER"), MemberStatus.ACTIVE); + return memberEntities.stream() + .map(memberPersistenceMapper::toDomain) + .collect(Collectors.toList()); + } + + @Override + public int getRemainingTasks(Long memberId) { + List targetStatuses = List.of(TaskStatus.IN_PROGRESS, TaskStatus.PENDING_COMPLETED); + return findTasksByMemberIdAndStatus(memberId, targetStatuses).size(); + } + + + @Override + public List findTasksByMemberIdAndStatus(Long memberId, 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/TaskPersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/TaskPersistenceAdapter.java index 763230a5..e9ed5605 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/TaskPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/TaskPersistenceAdapter.java @@ -42,4 +42,6 @@ public Page findAllByRequesterId(Long requesterId, Pageabl .map(taskPersistenceMapper::toDomain); return taskList.map(TaskMapper::toFindTaskListResponse); } + + } 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 99be21ab..f6de7fcf 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 @@ -1,13 +1,17 @@ package clap.server.adapter.outbound.persistense.repository.member; 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 org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface MemberRepository extends JpaRepository { - Optional findByStatusAndMemberId(MemberStatus memberStatus, Long memberId); -} \ No newline at end of file + List findByRoleAndStatus(MemberRole role, MemberStatus status); + + Optional findByStatusAndMemberId(MemberStatus memberStatus, Long id); +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskRepository.java index 3dabf934..06196a78 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskRepository.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskRepository.java @@ -1,9 +1,33 @@ package clap.server.adapter.outbound.persistense.repository.task; +import clap.server.adapter.inbound.web.dto.task.FindTaskListRequest; import clap.server.adapter.outbound.persistense.entity.task.TaskEntity; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.nio.channels.FileChannel; +import java.time.LocalDateTime; +import java.util.Collection; + +import java.util.List; + @Repository public interface TaskRepository extends JpaRepository, TaskCustomRepository { -} \ No newline at end of file + @Query("select t from TaskEntity t join fetch t.processor p where t.updatedAt between :updatedAtAfter and :updatedAtBefore") + List findYesterdayTaskByUpdatedAtIsBetween( + @Param("updatedAtAfter") LocalDateTime updatedAtAfter, + @Param("updatedAtBefore") LocalDateTime updatedAtBefore + ); + + // 'processor'의 'id'로 검색하기 + List findByProcessor_MemberIdAndTaskStatusIn(Long memberId, Collection taskStatuses); +} + + + + + diff --git a/src/main/java/clap/server/application/mapper/MemberMapper.java b/src/main/java/clap/server/application/mapper/MemberMapper.java index aeed7e4b..6d4f14ef 100644 --- a/src/main/java/clap/server/application/mapper/MemberMapper.java +++ b/src/main/java/clap/server/application/mapper/MemberMapper.java @@ -15,4 +15,4 @@ public static Member toMember(MemberInfo memberInfo) { .imageUrl(null) .build(); } -} +} \ No newline at end of file diff --git a/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecase.java b/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecase.java new file mode 100644 index 00000000..57327d16 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecase.java @@ -0,0 +1,8 @@ +package clap.server.application.port.inbound.domain; + +import clap.server.adapter.inbound.web.dto.admin.FindManagersResponse; +import java.util.List; + +public interface FindManagersUsecase { + List execute(); +} diff --git a/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecaseImpl.java b/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecaseImpl.java new file mode 100644 index 00000000..1fb1052a --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecaseImpl.java @@ -0,0 +1,36 @@ +package clap.server.application.port.inbound.domain; + +import clap.server.adapter.inbound.web.dto.admin.FindManagersResponse; +import clap.server.domain.model.member.Member; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class FindManagersUsecaseImpl implements FindManagersUsecase { + + private final MemberService memberService; + + @Override + @Transactional + public List execute() { + List managers = memberService.findActiveManagers(); + + return managers.stream().map(manager -> { + int remainingTasks = memberService.getRemainingTasks(manager.getMemberId()); + String nickname = memberService.getMemberNickname(manager.getMemberId()); + String imageUrl = memberService.getMemberImageUrl(manager.getMemberId()); + + return new FindManagersResponse( + manager.getMemberId(), + nickname, + imageUrl, + remainingTasks + ); + }).collect(Collectors.toList()); + } +} 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 47de39c7..d31a4111 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,12 +1,16 @@ package clap.server.application.port.inbound.domain; import clap.server.application.port.outbound.member.LoadMemberPort; -import clap.server.exception.ApplicationException; 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.exception.ApplicationException; import clap.server.exception.code.MemberErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.List; + @Service @RequiredArgsConstructor public class MemberService { @@ -14,11 +18,33 @@ public class MemberService { public Member findById(Long memberId) { return loadMemberPort.findById(memberId).orElseThrow( - ()-> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); + () -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); } - + public Member findActiveMember(Long memberId) { return loadMemberPort.findActiveMemberById(memberId).orElseThrow( - ()-> new ApplicationException(MemberErrorCode.ACTIVE_MEMBER_NOT_FOUND)); + () -> new ApplicationException(MemberErrorCode.ACTIVE_MEMBER_NOT_FOUND)); + } + + public int getRemainingTasks(Long memberId) { + List targetStatuses = List.of(TaskStatus.IN_PROGRESS, TaskStatus.PENDING_COMPLETED); + return loadMemberPort.findTasksByMemberIdAndStatus(memberId, targetStatuses).size(); + } + + public String getMemberNickname(Long memberId) { + Member member = findById(memberId); + if (member.getMemberInfo() == null) { + throw new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND); + } + return member.getMemberInfo().getNickname(); + } + + public String getMemberImageUrl(Long memberId) { + Member member = findById(memberId); + return member.getImageUrl() != null ? member.getImageUrl() : "default-image-url"; + } + + public List findActiveManagers() { + return 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 40647bb7..0a9e8f99 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 @@ -1,11 +1,19 @@ package clap.server.application.port.outbound.member; import clap.server.domain.model.member.Member; - +import clap.server.domain.model.task.Task; // Task 클래스 임포트 확인 +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; // TaskStatus 임포트 +import java.util.List; import java.util.Optional; public interface LoadMemberPort { Optional findById(Long id); Optional findActiveMemberById(Long id); + + List findActiveManagers(); + + List findTasksByMemberIdAndStatus(Long memberId, List taskStatuses); + + int getRemainingTasks(Long memberId); } 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 dde59d7e..3dbe7bae 100644 --- a/src/main/java/clap/server/domain/model/member/Member.java +++ b/src/main/java/clap/server/domain/model/member/Member.java @@ -35,4 +35,8 @@ public void register(Member admin) { this.status = MemberStatus.PENDING; this.password = ""; } + public String getNickname() { + return memberInfo != null ? memberInfo.getNickname() : null; + } + } 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 69a6b55c..31917d52 100644 --- a/src/main/java/clap/server/domain/model/member/MemberInfo.java +++ b/src/main/java/clap/server/domain/model/member/MemberInfo.java @@ -27,4 +27,5 @@ public MemberInfo(String name, String email, String nickname, boolean isReviewer this.role = role; this.departmentRole = departmentRole; } + } \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 64c7f036..bc75e8f7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,9 @@ spring.profiles.active: local spring: + jpa: + hibernate: + ddl-auto: none config: import: - mysql.yml @@ -8,7 +11,7 @@ spring: - redis.yml - optional:classpath:env.properties application: - name: taskflow + name: taskflow2 web.resources.add-mappings: false @@ -44,3 +47,4 @@ spring.config.activate.on-profile: prod logging: level: root: INFO +--- diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 00000000..a1bfb236 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,30 @@ +-- 부서 추가 +INSERT INTO department (id, code, name, status) VALUES (1, 'DEPT001', 'IT Department', 'ACTIVE'); + +-- 관리자 추가 +INSERT INTO member (id, name, email, nickname, is_reviewer, role, department_role, status, password, image_url, notification_enabled, department_id) +VALUES (1, 'Admin User', 'admin@example.com', 'Admin1', FALSE, 'ROLE_ADMIN', 'Admin', 'ACTIVE', 'admin123', 'http://example.com/admin.jpg', TRUE, 1); + +-- 카테고리 추가 +INSERT INTO category (id, code, name, description_example, is_deleted, admin_id) +VALUES (1, 'CATEGORY001', 'Development', 'Development tasks category', FALSE, 1); + +-- 라벨 추가 +INSERT INTO label (id, label_type, admin_id, is_deleted) +VALUES (1, 'EMERGENCY', 1, FALSE); + +-- 첫 번째 관리자 추가 +INSERT INTO member (id, name, email, nickname, is_reviewer, role, department_role, status, password, image_url, notification_enabled, department_id) +VALUES (2, 'Manager1', 'manager1@example.com', 'Manager1', TRUE, 'ROLE_MANAGER', 'Manager', 'ACTIVE', 'manager123', 'http://example.com/manager1.jpg', TRUE, 1); + +-- 두 번째 관리자 추가 +INSERT INTO member (id, name, email, nickname, is_reviewer, role, department_role, status, password, image_url, notification_enabled, department_id) +VALUES (3, 'Manager2', 'manager2@example.com', 'Manager2', TRUE, 'ROLE_MANAGER', 'Manager', 'ACTIVE', 'manager123', 'http://example.com/manager2.jpg', TRUE, 1); + +-- 일반 사용자 추가 +INSERT INTO member (id, name, email, nickname, is_reviewer, role, department_role, status, password, image_url, notification_enabled, department_id) +VALUES (4, 'User1', 'user1@example.com', 'User1', FALSE, 'ROLE_USER', 'User', 'ACTIVE', 'user123', 'http://example.com/user1.jpg', TRUE, 1); + +-- 태스크 추가 +INSERT INTO task (id, task_code, title, description, category_id, requester_id, task_status, processor_order, reviewer_id, processor_id, label_id, due_date, completed_at) +VALUES (1, 'TASK001', 'Task Title', 'Task Description', 1, 1, 'PENDING_COMPLETED', 1, 1, 2, 1, '2025-01-31 23:59:59', NULL); diff --git a/src/main/resources/env.properties b/src/main/resources/env.properties new file mode 100644 index 00000000..c3456735 --- /dev/null +++ b/src/main/resources/env.properties @@ -0,0 +1,31 @@ +# application.properties + +spring.config.import=classpath:env.properties + +# MySQL Configuration +spring.datasource.url=${DB_URL} +spring.datasource.username=${DB_USERNAME} +spring.datasource.password=${DB_PASSWORD} + +# Application Profile +APPLICATION_PORT=8080 + +# Redis Configuration +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= + +# Swagger Configuration +SWAGGER_SERVER_URL=http://localhost:8080 + +# MySQL Configuration +MYSQL_HOST=localhost +MYSQL_PORT=3306 +MYSQL_USERNAME=root +MYSQL_PASSWORD=km7971na +MYSQL_DATABASE=taskflow2 + +# Additional Environment-specific Configurations +DB_URL=jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE} +DB_USERNAME=${MYSQL_USERNAME} +DB_PASSWORD=${MYSQL_PASSWORD} diff --git a/src/main/resources/mysql.yml b/src/main/resources/mysql.yml index b77551db..4efa241f 100644 --- a/src/main/resources/mysql.yml +++ b/src/main/resources/mysql.yml @@ -10,7 +10,7 @@ spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://${DATABASE_HOST:localhost}:${DATABASE_PORT:3306}/${DATABASE_NAME:taskflow}?characterEncoding=UTF-8&serverTimezone=Asia/Seoul&autoReconnect=true + url: jdbc:mysql://${DATABASE_HOST:localhost}:${DATABASE_PORT:3306}/${DATABASE_NAME:taskflow2}?characterEncoding=UTF-8&serverTimezone=Asia/Seoul&autoReconnect=true username: ${DATABASE_USERNAME} password: ${DATABASE_PASSWORD} data-source-properties: diff --git a/src/test/java/clap/server/adapter/inbound/web/admin/MemberControllerTest.java b/src/test/java/clap/server/adapter/inbound/web/admin/MemberControllerTest.java new file mode 100644 index 00000000..c6b7c87a --- /dev/null +++ b/src/test/java/clap/server/adapter/inbound/web/admin/MemberControllerTest.java @@ -0,0 +1,188 @@ +package clap.server.adapter.inbound.web.admin; + +import clap.server.adapter.outbound.persistense.entity.member.DepartmentEntity; +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.MemberEntity; +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; +import clap.server.adapter.outbound.persistense.entity.task.CategoryEntity; +import clap.server.adapter.outbound.persistense.entity.task.LabelEntity; +import clap.server.adapter.outbound.persistense.entity.task.TaskEntity; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.web.servlet.MockMvc; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import clap.server.adapter.outbound.persistense.entity.task.constant.LabelType; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@Transactional +@AutoConfigureMockMvc +public class MemberControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private EntityManager entityManager; + + @BeforeEach + @Transactional + @Rollback(false) + public void setupTestData() { + // 부서 추가 + DepartmentEntity department = DepartmentEntity.builder() + .code("DEPT001") + .name("IT Department") + .status(DepartmentStatus.ACTIVE) + .build(); + entityManager.persist(department); // department 먼저 persist + + // 관리자 추가 + MemberEntity admin = MemberEntity.builder() + .name("Admin User") + .email("admin@example.com") + .nickname("Admin1") + .isReviewer(false) + .role(MemberRole.ROLE_ADMIN) + .departmentRole("Admin") + .status(MemberStatus.ACTIVE) + .password("admin123") + .imageUrl("http://example.com/admin.jpg") + .notificationEnabled(true) + .department(department) // 부서 할당 + .build(); + entityManager.persist(admin); + + // 카테고리 추가 + CategoryEntity category = CategoryEntity.builder() + .code("CATEGORY001") + .name("Development") + .descriptionExample("Development tasks category") + .isDeleted(false) + .admin(admin) + .build(); + entityManager.persist(category); + + // 라벨 추가 + LabelEntity label = LabelEntity.builder() + .labelType(LabelType.EMERGENCY) + .admin(admin) + .isDeleted(false) + .build(); + entityManager.persist(label); + + // 관리자 (manager) 추가 + MemberEntity manager1 = MemberEntity.builder() // manager1 정의 + .name("Manager1") + .email("manager1@example.com") + .nickname("Manager1") + .isReviewer(true) + .role(MemberRole.ROLE_MANAGER) // 관리자로 설정 + .departmentRole("Manager") + .status(MemberStatus.ACTIVE) + .password("manager123") + .imageUrl("http://example.com/manager1.jpg") + .notificationEnabled(true) + .department(department) // 부서 할당 + .build(); + entityManager.persist(manager1); // manager1 저장 + + // 태스크 추가 + TaskEntity task = TaskEntity.builder() + .taskCode("TASK001") + .title("Task Title") + .description("Task Description") + .category(category) // 카테고리 연결 + .requester(admin) + .taskStatus(TaskStatus.PENDING_COMPLETED) + .processorOrder(1) + .reviewer(admin) + .processor(manager1) // 여기서 manager1을 processor로 설정 + .label(label) + .dueDate(LocalDateTime.now().plusDays(7)) + .completedAt(null) + .build(); + entityManager.persist(task); + + // 일반 사용자 (user) 추가 + MemberEntity user = MemberEntity.builder() + .name("User1") + .email("user1@example.com") + .nickname("User1") + .isReviewer(false) + .role(MemberRole.ROLE_USER) + .departmentRole("User") + .status(MemberStatus.ACTIVE) + .password("user123") + .imageUrl("http://example.com/user1.jpg") + .notificationEnabled(true) + .department(department) // 부서 할당 + .build(); + entityManager.persist(user); + // 두 번째 관리자 추가 + MemberEntity manager2 = MemberEntity.builder() + .name("Manager2") + .email("manager2@example.com") + .nickname("Manager2") + .isReviewer(true) + .role(MemberRole.ROLE_MANAGER) + .departmentRole("Manager") + .status(MemberStatus.ACTIVE) + .password("manager123") + .imageUrl("http://example.com/manager2.jpg") + .notificationEnabled(true) + .department(department) + .build(); + entityManager.persist(manager2); // 두 번째 관리자 저장 + + // 추가로 5명의 사용자 생성 + List members = new ArrayList<>(); + for (int i = 2; i <= 5; i++) { + members.add(MemberEntity.builder() + .name("User" + i) + .email("user" + i + "@example.com") + .nickname("User" + i) + .isReviewer(i % 2 == 0) + .role(MemberRole.ROLE_USER) + .departmentRole("DepartmentUser") + .status(MemberStatus.ACTIVE) + .password("user123") + .imageUrl("http://example.com/user" + i + ".jpg") + .notificationEnabled(i % 2 != 0) + .department(department) // 부서 할당 + .build()); + } + + // 멤버들을 데이터베이스에 저장 + members.forEach(entityManager::persist); + } + + + @Test + public void testFindManagers() throws Exception { + mockMvc.perform(get("/manager")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].nickname").value("Manager1")) + .andExpect(jsonPath("$[0].imageUrl").value("http://example.com/manager1.jpg")) + .andExpect(jsonPath("$[0].remainingTasks").value(1)) + .andExpect(jsonPath("$[1].nickname").value("Manager2")) // 추가된 관리자 + .andExpect(jsonPath("$[1].imageUrl").value("http://example.com/manager2.jpg")) + .andExpect(jsonPath("$[1].remainingTasks").value(0)); // 예시로 추가된 관리자들의 데이터를 검증 + } + +} From a88a6bf1e7c51f8cffa4cb89eb635f898bbe286e Mon Sep 17 00:00:00 2001 From: nano-mm Date: Fri, 24 Jan 2025 04:18:33 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=EB=82=B4=20=EC=9E=91=EC=97=85=ED=95=9C=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=84=A4?= =?UTF-8?q?=EB=AA=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistense/repository/member/MemberRepository.java | 2 ++ 1 file changed, 2 insertions(+) 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 f6de7fcf..9e284dac 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 @@ -11,7 +11,9 @@ @Repository public interface MemberRepository extends JpaRepository { + List findByRoleAndStatus(MemberRole role, MemberStatus status); Optional findByStatusAndMemberId(MemberStatus memberStatus, Long id); } + From 283e5a50c27842cf685e569b69da89d1e1235722 Mon Sep 17 00:00:00 2001 From: nano-mm Date: Fri, 24 Jan 2025 11:40:50 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=EB=8B=B4=EB=8B=B9=EC=9E=90=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/dto/admin/FindManagersResponse.java | 12 ++++++-- .../inbound/web/member/ManagerController.java | 9 +++++- .../domain/FindManagersUsecaseImpl.java | 5 ++++ .../port/inbound/domain/MemberService.java | 7 ++++- src/main/resources/application.yml | 9 ++---- src/main/resources/data.sql | 30 ------------------- .../web/admin/MemberControllerTest.java | 9 +++++- src/test/resources/application.yml | 14 +++++---- 8 files changed, 47 insertions(+), 48 deletions(-) delete mode 100644 src/main/resources/data.sql diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/admin/FindManagersResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/admin/FindManagersResponse.java index 645268e6..26e64a60 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/admin/FindManagersResponse.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/admin/FindManagersResponse.java @@ -3,13 +3,21 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import java.util.List; + @Getter @AllArgsConstructor public class FindManagersResponse { private Long memberId; - private String nickname; // 닉네임 필드 추가 - private String imageUrl; // 이미지 URL 필드 추가 + private String nickname; + private String imageUrl; private int remainingTasks; + public static List emptyListResponse() { + return List.of(); + } + } + + diff --git a/src/main/java/clap/server/adapter/inbound/web/member/ManagerController.java b/src/main/java/clap/server/adapter/inbound/web/member/ManagerController.java index 66c80ae6..8354dd05 100644 --- a/src/main/java/clap/server/adapter/inbound/web/member/ManagerController.java +++ b/src/main/java/clap/server/adapter/inbound/web/member/ManagerController.java @@ -18,6 +18,13 @@ public class ManagerController { @GetMapping public List findManagers() { - return findManagersUsecase.execute(); + + List managers = findManagersUsecase.execute(); + + if (managers.isEmpty()) { + return FindManagersResponse.emptyListResponse(); + } + + return managers; } } diff --git a/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecaseImpl.java b/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecaseImpl.java index 1fb1052a..0148fb72 100644 --- a/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecaseImpl.java +++ b/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecaseImpl.java @@ -18,8 +18,13 @@ public class FindManagersUsecaseImpl implements FindManagersUsecase { @Override @Transactional public List execute() { + List managers = memberService.findActiveManagers(); + if (managers.isEmpty()) { + return FindManagersResponse.emptyListResponse(); // 빈 리스트 반환 + } + return managers.stream().map(manager -> { int remainingTasks = memberService.getRemainingTasks(manager.getMemberId()); String nickname = memberService.getMemberNickname(manager.getMemberId()); 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 d31a4111..a3d31fd3 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 @@ -45,6 +45,11 @@ public String getMemberImageUrl(Long memberId) { } public List findActiveManagers() { - return loadMemberPort.findActiveManagers(); + List activeManagers = loadMemberPort.findActiveManagers(); + + if (activeManagers.isEmpty()) { + return List.of(); + } + return activeManagers; } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c968d861..ef3f186c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,9 +1,6 @@ spring.profiles.active: local spring: - jpa: - hibernate: - ddl-auto: none config: import: - mysql.yml @@ -12,12 +9,11 @@ spring: - auth.yml - optional:classpath:env.properties application: - name: taskflow2 elasticsearch: uris: ${ELASTIC_URI:127.0.0.1:9200} - web.resources.add-mappings: false + web.resources.add-mappings: false server: @@ -54,5 +50,4 @@ logging: spring.config.activate.on-profile: prod logging: level: - root: INFO ---- + root: INFO \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql deleted file mode 100644 index a1bfb236..00000000 --- a/src/main/resources/data.sql +++ /dev/null @@ -1,30 +0,0 @@ --- 부서 추가 -INSERT INTO department (id, code, name, status) VALUES (1, 'DEPT001', 'IT Department', 'ACTIVE'); - --- 관리자 추가 -INSERT INTO member (id, name, email, nickname, is_reviewer, role, department_role, status, password, image_url, notification_enabled, department_id) -VALUES (1, 'Admin User', 'admin@example.com', 'Admin1', FALSE, 'ROLE_ADMIN', 'Admin', 'ACTIVE', 'admin123', 'http://example.com/admin.jpg', TRUE, 1); - --- 카테고리 추가 -INSERT INTO category (id, code, name, description_example, is_deleted, admin_id) -VALUES (1, 'CATEGORY001', 'Development', 'Development tasks category', FALSE, 1); - --- 라벨 추가 -INSERT INTO label (id, label_type, admin_id, is_deleted) -VALUES (1, 'EMERGENCY', 1, FALSE); - --- 첫 번째 관리자 추가 -INSERT INTO member (id, name, email, nickname, is_reviewer, role, department_role, status, password, image_url, notification_enabled, department_id) -VALUES (2, 'Manager1', 'manager1@example.com', 'Manager1', TRUE, 'ROLE_MANAGER', 'Manager', 'ACTIVE', 'manager123', 'http://example.com/manager1.jpg', TRUE, 1); - --- 두 번째 관리자 추가 -INSERT INTO member (id, name, email, nickname, is_reviewer, role, department_role, status, password, image_url, notification_enabled, department_id) -VALUES (3, 'Manager2', 'manager2@example.com', 'Manager2', TRUE, 'ROLE_MANAGER', 'Manager', 'ACTIVE', 'manager123', 'http://example.com/manager2.jpg', TRUE, 1); - --- 일반 사용자 추가 -INSERT INTO member (id, name, email, nickname, is_reviewer, role, department_role, status, password, image_url, notification_enabled, department_id) -VALUES (4, 'User1', 'user1@example.com', 'User1', FALSE, 'ROLE_USER', 'User', 'ACTIVE', 'user123', 'http://example.com/user1.jpg', TRUE, 1); - --- 태스크 추가 -INSERT INTO task (id, task_code, title, description, category_id, requester_id, task_status, processor_order, reviewer_id, processor_id, label_id, due_date, completed_at) -VALUES (1, 'TASK001', 'Task Title', 'Task Description', 1, 1, 'PENDING_COMPLETED', 1, 1, 2, 1, '2025-01-31 23:59:59', NULL); diff --git a/src/test/java/clap/server/adapter/inbound/web/admin/MemberControllerTest.java b/src/test/java/clap/server/adapter/inbound/web/admin/MemberControllerTest.java index c6b7c87a..5264ed4f 100644 --- a/src/test/java/clap/server/adapter/inbound/web/admin/MemberControllerTest.java +++ b/src/test/java/clap/server/adapter/inbound/web/admin/MemberControllerTest.java @@ -8,6 +8,7 @@ import clap.server.adapter.outbound.persistense.entity.task.CategoryEntity; import clap.server.adapter.outbound.persistense.entity.task.LabelEntity; import clap.server.adapter.outbound.persistense.entity.task.TaskEntity; +import clap.server.config.elastic.ElasticsearchConfig; import jakarta.persistence.EntityManager; import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; @@ -15,6 +16,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.annotation.Rollback; import org.springframework.test.web.servlet.MockMvc; import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; @@ -26,9 +29,11 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@Import(ElasticsearchConfig.class) @SpringBootTest @Transactional @AutoConfigureMockMvc @@ -174,6 +179,7 @@ public void setupTestData() { @Test + @WithMockUser(username = "admin", roles = "ADMIN") public void testFindManagers() throws Exception { mockMvc.perform(get("/manager")) .andExpect(status().isOk()) @@ -182,7 +188,8 @@ public void testFindManagers() throws Exception { .andExpect(jsonPath("$[0].remainingTasks").value(1)) .andExpect(jsonPath("$[1].nickname").value("Manager2")) // 추가된 관리자 .andExpect(jsonPath("$[1].imageUrl").value("http://example.com/manager2.jpg")) - .andExpect(jsonPath("$[1].remainingTasks").value(0)); // 예시로 추가된 관리자들의 데이터를 검증 + .andExpect(jsonPath("$[1].remainingTasks").value(0)) // 예시로 추가된 관리자들의 데이터를 검증 + .andDo(print()); } } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index a0c7628e..47f4a275 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -1,24 +1,26 @@ spring: - # H2 Setting Info (H2 Console에 접속하기 위한 설정정보 입력) + # H2 설정 정보 (H2 Console에 접속하기 위한 설정) h2: console: enabled: false # H2 Console을 사용할지 여부 (H2 Console은 H2 Database를 UI로 제공해주는 기능) path: /h2 # H2 Console의 Path - # Database Setting Info (Database를 H2로 사용하기 위해 H2연결 정보 입력) + # Database 설정 정보 (H2 연동 정보) datasource: - driver-class-name: org.h2.Driver # Database를 H2로 사용하겠다. + driver-class-name: org.h2.Driver # H2 Database 사용 url: jdbc:h2:mem:testdb # H2 접속 정보 - username: taskflow # H2 접속 시 입력할 username 정보 (원하는 것으로 입력) - password: # H2 접속 시 입력할 password 정보 (원하는 것으로 입력) + username: taskflow # H2 접속 시 입력할 username 정보 + password: # H2 접속 시 입력할 password 정보 jpa: hibernate: ddl-auto: create + elasticsearch: + uris: ${ELASTIC_URI:127.0.0.1:9200} # 기본 URL로 localhost 사용 + swagger: server: url: http://localhost:8080 - jwt: secret-key: access-token: exampleSecretKeyForTFSystemAccessSecretKeyTestForPadding From 2947f8fa6b8517eb3c9ed2d2a4a314a6b451039d Mon Sep 17 00:00:00 2001 From: nano-mm Date: Fri, 24 Jan 2025 14:28:14 +0900 Subject: [PATCH 4/7] =?UTF-8?q?CLAP-76=20Feat:=20=EB=8B=B4=EB=8B=B9?= =?UTF-8?q?=EC=9E=90=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 담당자 조회 API를 구현하였으며, 매니저의 정보를 조회할 수 있도록 처리 - 필요한 데이터는 담당자의 닉네임, 이미지 URL, 남은 작업 개수 등을 포함