diff --git a/build.gradle b/build.gradle index 5f4ccee..c00610e 100644 --- a/build.gradle +++ b/build.gradle @@ -49,6 +49,7 @@ dependencies { // AWS SDK for S3 and Secrets Manager Integration implementation platform('io.awspring.cloud:spring-cloud-aws-dependencies:3.1.1') implementation 'io.awspring.cloud:spring-cloud-aws-starter-secrets-manager' + implementation(platform("software.amazon.awssdk:bom:2.21.46")) implementation 'software.amazon.awssdk:s3' implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap' diff --git a/src/main/java/com/joycrew/backend/controller/EmployeeQueryController.java b/src/main/java/com/joycrew/backend/controller/EmployeeQueryController.java index 5d3d3df..33384e2 100644 --- a/src/main/java/com/joycrew/backend/controller/EmployeeQueryController.java +++ b/src/main/java/com/joycrew/backend/controller/EmployeeQueryController.java @@ -1,7 +1,6 @@ package com.joycrew.backend.controller; import com.joycrew.backend.dto.PagedEmployeeResponse; -import com.joycrew.backend.entity.enums.AccessStatus; import com.joycrew.backend.security.UserPrincipal; import com.joycrew.backend.service.EmployeeQueryService; import io.swagger.v3.oas.annotations.Operation; @@ -21,35 +20,40 @@ @Tag(name = "Employee Query", description = "API for searching employees") public class EmployeeQueryController { - private final EmployeeQueryService employeeQueryService; + private final EmployeeQueryService employeeQueryService; - @Operation( - summary = "Search employee list", - description = "Performs a unified search by name, email, or department. The current user is excluded from the search results.", - parameters = { - @Parameter(name = "keyword", description = "Search keyword", example = "John"), - @Parameter(name = "page", description = "Page number (0-based)", example = "0"), - @Parameter(name = "size", description = "Items per page", example = "20") - }, - responses = { - @ApiResponse( - responseCode = "200", - description = "Employee list retrieved successfully", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = PagedEmployeeResponse.class) - ) - ) - } - ) - @GetMapping - public ResponseEntity searchEmployees( - @RequestParam(required = false) String keyword, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size, - @AuthenticationPrincipal UserPrincipal principal - ) { - PagedEmployeeResponse response = employeeQueryService.getEmployees(keyword, page, size, principal.getEmployee().getEmployeeId(), AccessStatus.ACTIVE); - return ResponseEntity.ok(response); - } -} \ No newline at end of file + @Operation( + summary = "Search employee list", + description = "Performs a unified search by name, email, or department. The current user is excluded from the search results.", + parameters = { + @Parameter(name = "keyword", description = "Search keyword", example = "John"), + @Parameter(name = "page", description = "Page number (0-based)", example = "0"), + @Parameter(name = "size", description = "Items per page", example = "20") + }, + responses = { + @ApiResponse( + responseCode = "200", + description = "Employee list retrieved successfully", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = PagedEmployeeResponse.class) + ) + ) + } + ) + @GetMapping + public ResponseEntity searchEmployees( + @RequestParam(required = false) String keyword, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @AuthenticationPrincipal UserPrincipal principal + ) { + var me = principal.getEmployee(); + PagedEmployeeResponse response = employeeQueryService.getEmployees( + keyword, page, size, + me.getEmployeeId(), + me.getRole() // 요청자 역할 전달 + ); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/joycrew/backend/repository/EmployeeQueryRepository.java b/src/main/java/com/joycrew/backend/repository/EmployeeQueryRepository.java deleted file mode 100644 index fab9d0c..0000000 --- a/src/main/java/com/joycrew/backend/repository/EmployeeQueryRepository.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.joycrew.backend.repository; - -import com.joycrew.backend.dto.EmployeeQueryResponse; -import com.joycrew.backend.dto.PagedEmployeeResponse; -import com.joycrew.backend.entity.Employee; -import com.joycrew.backend.service.mapper.EmployeeMapper; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.persistence.TypedQuery; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; -import org.springframework.util.StringUtils; - -import java.util.List; -import java.util.stream.Collectors; - -@Repository -@RequiredArgsConstructor -public class EmployeeQueryRepository { - - @PersistenceContext - private final EntityManager em; - private final EmployeeMapper employeeMapper; - - public PagedEmployeeResponse getEmployees(String keyword, int page, int size, Long currentEmployeeId) { - StringBuilder whereClause = new StringBuilder(); - boolean hasKeyword = StringUtils.hasText(keyword); - - // Base condition to exclude the current user - whereClause.append("WHERE e.id != :currentEmployeeId "); - - // Dynamic condition for keyword search - if (hasKeyword) { - whereClause.append("AND (LOWER(e.employeeName) LIKE :keyword ") - .append("OR LOWER(e.email) LIKE :keyword ") - .append("OR LOWER(e.department.name) LIKE :keyword) "); - } - - // Count Query - String countJpql = "SELECT COUNT(e) FROM Employee e LEFT JOIN e.department d " + whereClause; - TypedQuery countQuery = em.createQuery(countJpql, Long.class); - countQuery.setParameter("currentEmployeeId", currentEmployeeId); - if (hasKeyword) { - countQuery.setParameter("keyword", "%" + keyword.toLowerCase() + "%"); - } - long totalCount = countQuery.getSingleResult(); - int totalPages = (int) Math.ceil((double) totalCount / size); - - // Data Query - String dataJpql = "SELECT e FROM Employee e " + - "LEFT JOIN FETCH e.department d " + - whereClause + - "ORDER BY e.employeeName ASC"; - TypedQuery dataQuery = em.createQuery(dataJpql, Employee.class); - dataQuery.setParameter("currentEmployeeId", currentEmployeeId); - if (hasKeyword) { - dataQuery.setParameter("keyword", "%" + keyword.toLowerCase() + "%"); - } - dataQuery.setFirstResult(page * size); - dataQuery.setMaxResults(size); - - List employees = dataQuery.getResultList().stream() - .map(employeeMapper::toEmployeeQueryResponse) - .collect(Collectors.toList()); - - return new PagedEmployeeResponse( - employees, - page, - totalPages, - page >= totalPages - 1 - ); - } -} \ No newline at end of file diff --git a/src/main/java/com/joycrew/backend/service/EmployeeQueryService.java b/src/main/java/com/joycrew/backend/service/EmployeeQueryService.java index 1e142e0..0c2da99 100644 --- a/src/main/java/com/joycrew/backend/service/EmployeeQueryService.java +++ b/src/main/java/com/joycrew/backend/service/EmployeeQueryService.java @@ -3,7 +3,7 @@ import com.joycrew.backend.dto.EmployeeQueryResponse; import com.joycrew.backend.dto.PagedEmployeeResponse; import com.joycrew.backend.entity.Employee; -import com.joycrew.backend.entity.enums.AccessStatus; +import com.joycrew.backend.entity.enums.AdminLevel; import com.joycrew.backend.service.mapper.EmployeeMapper; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; @@ -25,51 +25,57 @@ public class EmployeeQueryService { private final EntityManager em; private final EmployeeMapper employeeMapper; - public PagedEmployeeResponse getEmployees(String keyword, int page, int size, Long currentUserId, AccessStatus accessStatus) { - StringBuilder whereClause = new StringBuilder(); - boolean hasKeyword = StringUtils.hasText(keyword); + // EmployeeQueryService.java + public PagedEmployeeResponse getEmployees( + String keyword, int page, int size, Long currentUserId, AdminLevel requesterRole + ) { + StringBuilder where = new StringBuilder("WHERE e.employeeId != :currentUserId "); + boolean hasKeyword = StringUtils.hasText(keyword); if (hasKeyword) { - whereClause.append("WHERE (LOWER(e.employeeName) LIKE :keyword ") - .append("OR LOWER(e.email) LIKE :keyword ") - .append("OR LOWER(d.name) LIKE :keyword) "); + where.append("AND (LOWER(e.employeeName) LIKE :keyword ") + .append("OR LOWER(e.email) LIKE :keyword ") + .append("OR LOWER(d.name) LIKE :keyword) "); } - whereClause.append(hasKeyword ? "AND " : "WHERE "); - whereClause.append("e.id != :currentUserId "); - - String countJpql = "SELECT COUNT(e) FROM Employee e LEFT JOIN e.department d " + whereClause; - TypedQuery countQuery = em.createQuery(countJpql, Long.class); - countQuery.setParameter("currentUserId", currentUserId); - if (hasKeyword) { - countQuery.setParameter("keyword", "%" + keyword.toLowerCase() + "%"); + boolean hideSuperAdmin = (requesterRole != AdminLevel.SUPER_ADMIN); + if (hideSuperAdmin) { + // JPQL에 SUPER_ADMIN 제외 + where.append("AND e.role <> :superAdmin "); } + + String countJpql = "SELECT COUNT(e) FROM Employee e LEFT JOIN e.department d " + where; + TypedQuery countQuery = em.createQuery(countJpql, Long.class) + .setParameter("currentUserId", currentUserId); + if (hasKeyword) countQuery.setParameter("keyword", "%" + keyword.toLowerCase() + "%"); + if (hideSuperAdmin) countQuery.setParameter("superAdmin", AdminLevel.SUPER_ADMIN); + long totalCount = countQuery.getSingleResult(); int totalPages = (int) Math.ceil((double) totalCount / size); - String dataJpql = "SELECT e FROM Employee e " + - "JOIN FETCH e.company c " + - "LEFT JOIN FETCH e.department d " + - whereClause + - "ORDER BY e.employeeName ASC"; - TypedQuery dataQuery = em.createQuery(dataJpql, Employee.class); - dataQuery.setParameter("currentUserId", currentUserId); - if (hasKeyword) { - dataQuery.setParameter("keyword", "%" + keyword.toLowerCase() + "%"); - } + String dataJpql = + "SELECT e FROM Employee e " + + "JOIN FETCH e.company c " + + "LEFT JOIN FETCH e.department d " + + where + + "ORDER BY e.employeeName ASC"; + TypedQuery dataQuery = em.createQuery(dataJpql, Employee.class) + .setParameter("currentUserId", currentUserId); + if (hasKeyword) dataQuery.setParameter("keyword", "%" + keyword.toLowerCase() + "%"); + if (hideSuperAdmin) dataQuery.setParameter("superAdmin", AdminLevel.SUPER_ADMIN); dataQuery.setFirstResult(page * size); dataQuery.setMaxResults(size); + // ✅ 최종 안전망: 결과에서 SUPER_ADMIN 제거 (요청자가 SUPER_ADMIN이 아닐 때) List employees = dataQuery.getResultList().stream() - .map(employeeMapper::toEmployeeQueryResponse) - .collect(Collectors.toList()); + .filter(e -> !(hideSuperAdmin && e.getRole() == AdminLevel.SUPER_ADMIN)) + .map(employeeMapper::toEmployeeQueryResponse) + .collect(Collectors.toList()); return new PagedEmployeeResponse( - employees, - page, - totalPages, - page >= totalPages - 1 + employees, page, totalPages, page >= totalPages - 1 ); } -} \ No newline at end of file + +} diff --git a/src/test/java/com/joycrew/backend/service/EmployeeQueryServiceTest.java b/src/test/java/com/joycrew/backend/service/EmployeeQueryServiceTest.java deleted file mode 100644 index 561636e..0000000 --- a/src/test/java/com/joycrew/backend/service/EmployeeQueryServiceTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.joycrew.backend.service; - -import com.joycrew.backend.dto.EmployeeQueryResponse; -import com.joycrew.backend.dto.PagedEmployeeResponse; -import com.joycrew.backend.entity.Employee; -import com.joycrew.backend.service.mapper.EmployeeMapper; -import jakarta.persistence.EntityManager; -import jakarta.persistence.TypedQuery; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class EmployeeQueryServiceTest { - - @Mock - private EntityManager em; - @Mock - private EmployeeMapper employeeMapper; - - @InjectMocks - private EmployeeQueryService employeeQueryService; - - @Test - @DisplayName("[Unit] Get employee list - Should return with paging information") - void getEmployees_Success() { - // Given - String keyword = "test"; - int page = 0; - int size = 10; - Long currentUserId = 1L; - - // Mocking TypedQuery for both count and data - TypedQuery countQuery = mock(TypedQuery.class); - TypedQuery dataQuery = mock(TypedQuery.class); - Employee mockEmployee = Employee.builder().employeeId(2L).employeeName("Test User").build(); - EmployeeQueryResponse mockDto = new EmployeeQueryResponse(2L, null, "Test User", "Test Dept", "Tester"); - - // Mocking for the count query - when(em.createQuery(anyString(), eq(Long.class))).thenReturn(countQuery); - when(countQuery.setParameter(anyString(), any())).thenReturn(countQuery); - when(countQuery.getSingleResult()).thenReturn(1L); - - // Mocking for the data query - when(em.createQuery(anyString(), eq(Employee.class))).thenReturn(dataQuery); - when(dataQuery.setParameter(anyString(), any())).thenReturn(dataQuery); - when(dataQuery.setFirstResult(anyInt())).thenReturn(dataQuery); - when(dataQuery.setMaxResults(anyInt())).thenReturn(dataQuery); - when(dataQuery.getResultList()).thenReturn(List.of(mockEmployee)); - - // Mocking the mapper's behavior - when(employeeMapper.toEmployeeQueryResponse(any(Employee.class))).thenReturn(mockDto); - - // When - PagedEmployeeResponse response = employeeQueryService.getEmployees(keyword, page, size, currentUserId); - - // Then - assertThat(response).isNotNull(); - assertThat(response.employees()).hasSize(1); - assertThat(response.employees().get(0).employeeName()).isEqualTo("Test User"); - assertThat(response.currentPage()).isEqualTo(page); - assertThat(response.totalPages()).isEqualTo(1); - assertThat(response.isLastPage()).isTrue(); - - verify(em, times(1)).createQuery(contains("SELECT COUNT(e)"), eq(Long.class)); - verify(em, times(1)).createQuery(contains("SELECT e FROM Employee e"), eq(Employee.class)); - verify(dataQuery, times(1)).setParameter("keyword", "%" + keyword.toLowerCase() + "%"); - verify(dataQuery, times(1)).setParameter("currentUserId", currentUserId); - } -} \ No newline at end of file