diff --git a/src/main/java/com/workhub/dashboard/service/admin/DashBoardAdminService.java b/src/main/java/com/workhub/dashboard/service/admin/DashBoardAdminService.java index bbc7b99..a1237d8 100644 --- a/src/main/java/com/workhub/dashboard/service/admin/DashBoardAdminService.java +++ b/src/main/java/com/workhub/dashboard/service/admin/DashBoardAdminService.java @@ -8,7 +8,7 @@ import com.workhub.projectNode.dto.ProjectNodeCategoryCount; import com.workhub.projectNode.entity.NodeCategory; import com.workhub.projectNode.service.ProjectNodeService; -import com.workhub.userTable.service.company.CompanyService; +import com.workhub.userTable.service.company.ReadCompanyService; import com.workhub.userTable.service.user.UserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -36,7 +36,7 @@ public class DashBoardAdminService { private static final String PROJECT_CRITERIA = "계약 기간이 월과 겹치는 프로젝트"; private final UserService userService; - private final CompanyService companyService; + private final ReadCompanyService readCompanyService; private final ProjectService projectService; private final ProjectNodeService projectNodeService; @@ -53,7 +53,7 @@ public UserCountResponse getUserCount() { */ public CompanyCountResponse getCompanyCount() { - return CompanyCountResponse.from(companyService.countActiveCompanies()); + return CompanyCountResponse.from(readCompanyService.countActiveCompanies()); } /** diff --git a/src/main/java/com/workhub/project/service/ProjectService.java b/src/main/java/com/workhub/project/service/ProjectService.java index 1c4c1f7..e2f25dd 100644 --- a/src/main/java/com/workhub/project/service/ProjectService.java +++ b/src/main/java/com/workhub/project/service/ProjectService.java @@ -171,4 +171,14 @@ public List findProjectsWithPaging(List projectIds, LocalDate sta return projectRepository.findProjectsWithPaging(projectIds, startDate, endDate, status, sortOrder, cursor, size); } + + /** + * 특정 회사의 모든 프로젝트 조회 + * + * @param clientCompanyId 클라이언트 회사 ID + * @return 해당 회사의 프로젝트 리스트 + */ + public List findAllByClientCompanyId(Long clientCompanyId) { + return projectRepository.findAllByClientCompanyId(clientCompanyId); + } } diff --git a/src/main/java/com/workhub/project/service/ReadProjectService.java b/src/main/java/com/workhub/project/service/ReadProjectService.java index abcbd91..0783579 100644 --- a/src/main/java/com/workhub/project/service/ReadProjectService.java +++ b/src/main/java/com/workhub/project/service/ReadProjectService.java @@ -18,6 +18,7 @@ import com.workhub.userTable.entity.UserRole; import com.workhub.userTable.entity.UserTable; import com.workhub.userTable.service.company.CompanyService; +import com.workhub.userTable.service.company.ReadCompanyService; import com.workhub.userTable.service.user.UserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -41,6 +42,7 @@ public class ReadProjectService { private final ProjectService projectService; private final ProjectNodeService projectNodeService; private final UserService userService; + private final ReadCompanyService readCompanyService; private final CompanyService companyService; /** @@ -156,7 +158,7 @@ private BatchData loadBatchData(List projectIds, List projects) { .map(Project::getClientCompanyId) .distinct() .toList(); - Map companyMap = companyService.getCompanyMapByCompanyIdIn(companyIds); + Map companyMap = readCompanyService.getCompanyMapByCompanyIdIn(companyIds); Map> clientMemberMap = groupClientMembersByProjectId(allClientMembers); Map> devMemberMap = groupDevMembersByProjectId(allDevMembers); diff --git a/src/main/java/com/workhub/userTable/controller/CompanyController.java b/src/main/java/com/workhub/userTable/controller/CompanyController.java index 7a6ae24..a5506da 100644 --- a/src/main/java/com/workhub/userTable/controller/CompanyController.java +++ b/src/main/java/com/workhub/userTable/controller/CompanyController.java @@ -9,7 +9,10 @@ import com.workhub.userTable.dto.company.response.CompanyResponse; import com.workhub.userTable.dto.company.response.CompanyTitleResponse; import com.workhub.userTable.dto.user.response.UserNameResponse; -import com.workhub.userTable.service.company.CompanyService; +import com.workhub.userTable.service.company.CreateCompanyService; +import com.workhub.userTable.service.company.DeleteCompanyService; +import com.workhub.userTable.service.company.ReadCompanyService; +import com.workhub.userTable.service.company.UpdateCompanyService; import com.workhub.userTable.service.user.UserService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -27,13 +30,16 @@ @RequiredArgsConstructor public class CompanyController implements CompanyApi { - private final CompanyService companyService; + private final CreateCompanyService createCompanyService; + private final ReadCompanyService readCompanyService; + private final UpdateCompanyService updateCompanyService; + private final DeleteCompanyService deleteCompanyService; private final UserService userService; @PostMapping("/add") @Override public ResponseEntity> registerCompany(@RequestBody @Valid CompanyRegisterRequest request) { - CompanyResponse response = companyService.registerCompany(request); + CompanyResponse response = createCompanyService.registerCompany(request); return ApiResponse.created(response, "고객사가 등록되었습니다."); } @@ -42,7 +48,7 @@ public ResponseEntity> registerCompany(@RequestBody public ResponseEntity>> getCompanies( @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable ) { - Page companies = companyService.getCompanies(pageable); + Page companies = readCompanyService.getCompanies(pageable); return ApiResponse.success(companies); } @@ -50,7 +56,7 @@ public ResponseEntity>> getCompanies( @Override public ResponseEntity>> getCompanyNames() { - List response = companyService.getCompanyNameList(); + List response = readCompanyService.getCompanyNameList(); return ApiResponse.success(response); } @@ -58,13 +64,13 @@ public ResponseEntity>> getCompanyNames() @Override public ResponseEntity> getCompany(@PathVariable("companyId") Long companyId) { - CompanyDetailResponse company = companyService.getCompany(companyId); + CompanyDetailResponse company = readCompanyService.getCompany(companyId); return ApiResponse.success(company); } @DeleteMapping("/{companyId}") @Override public ResponseEntity> deleteCompany(@PathVariable("companyId") Long companyId) { - companyService.deleteCompany(companyId); + deleteCompanyService.deleteCompany(companyId); return ApiResponse.success(null, "고객사가 비활성화되었습니다."); } @@ -74,7 +80,7 @@ public ResponseEntity> updateCompanyStatus( @PathVariable("companyId") Long companyId, @RequestBody @Valid CompanyStatusUpdateRequest request ) { - CompanyResponse response = companyService.updateCompanyStatus(companyId, request.status()); + CompanyResponse response = updateCompanyService.updateCompanyStatus(companyId, request.status()); return ApiResponse.success(response, "고객사 상태가 변경되었습니다."); } diff --git a/src/main/java/com/workhub/userTable/service/company/CompanyService.java b/src/main/java/com/workhub/userTable/service/company/CompanyService.java index ed46e23..88f2391 100644 --- a/src/main/java/com/workhub/userTable/service/company/CompanyService.java +++ b/src/main/java/com/workhub/userTable/service/company/CompanyService.java @@ -2,19 +2,9 @@ import com.workhub.global.error.ErrorCode; import com.workhub.global.error.exception.BusinessException; -import com.workhub.project.entity.Project; -import com.workhub.project.entity.Status; -import com.workhub.project.repository.ProjectRepository; -import com.workhub.userTable.dto.company.request.CompanyRegisterRequest; -import com.workhub.userTable.dto.company.response.CompanyDetailResponse; -import com.workhub.userTable.dto.company.response.CompanyListResponse; -import com.workhub.userTable.dto.company.response.CompanyResponse; -import com.workhub.userTable.dto.company.response.CompanyTitleResponse; import com.workhub.userTable.entity.Company; import com.workhub.userTable.entity.CompanyStatus; -import com.workhub.userTable.entity.UserRole; import com.workhub.userTable.repository.CompanyRepository; -import com.workhub.userTable.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -30,94 +20,72 @@ public class CompanyService { private final CompanyRepository companyRepository; - private final ProjectRepository projectRepository; - private final UserRepository userRepository; - @Transactional - public CompanyResponse registerCompany(CompanyRegisterRequest request) { - - validateDuplicateCompanyNumber(request.companyNumber()); - - Company company = Company.of(request); - Company savedCompany = companyRepository.save(company); - - return CompanyResponse.from(savedCompany); + /** + * Company ID로 활성 회사 조회 (Core 메서드) + * 다른 도메인 서비스에서 사용 + * + * @param companyId 회사 ID + * @return 활성 회사 엔티티 + */ + @Transactional(readOnly = true) + public Company findById(Long companyId) { + return companyRepository.findByCompanyIdAndCompanystatus(companyId, CompanyStatus.ACTIVE) + .orElseThrow(() -> new BusinessException(ErrorCode.Company_NOT_EXISTS)); } - private void validateDuplicateCompanyNumber(String companyNumber) { + /** + * 사업자번호 중복 검증 (Core 유틸) + * + * @param companyNumber 사업자번호 + * @throws BusinessException 중복된 사업자번호가 존재할 경우 + */ + public void validateDuplicateCompanyNumber(String companyNumber) { if (companyRepository.existsByCompanyNumber(companyNumber)) { throw new BusinessException(ErrorCode.COMPANY_ALREADY_EXISTS); } } - @Transactional(readOnly = true) - public Page getCompanies(Pageable pageable) { - Page companies = companyRepository.findAllByCompanystatus(CompanyStatus.ACTIVE, pageable); - - return companies.map(company -> { - // 해당 회사의 프로젝트 조회 - List projects = projectRepository.findAllByClientCompanyId(company.getCompanyId()); - - // IN_PROGRESS와 총 프로젝트 카운트 - long inProgressCount = projects.stream() - .filter(p -> p.getStatus() == Status.IN_PROGRESS) - .count(); - long totalProjectCount = projects.size(); - - // 활성 클라이언트 멤버 수 조회 (company_id 기준) - long clientMemberCount = userRepository.countByCompanyIdAndRoleAndStatus( - company.getCompanyId(), - UserRole.CLIENT, - com.workhub.userTable.entity.Status.ACTIVE - ); - // CompanyListResponse 생성 - return CompanyListResponse.from(company, inProgressCount, totalProjectCount, clientMemberCount); - }); - } - - @Transactional(readOnly = true) - public List getCompanyNameList() { - return companyRepository.findAllByCompanystatus(CompanyStatus.ACTIVE).stream() - .map(CompanyTitleResponse::from) - .toList(); + /** + * Company 저장 + * + * @param company 저장할 회사 엔티티 + * @return 저장된 회사 엔티티 + */ + @Transactional + public Company save(Company company) { + return companyRepository.save(company); } + /** + * 활성 회사 페이징 조회 + * + * @param pageable 페이징 정보 + * @return 활성 회사 페이지 + */ @Transactional(readOnly = true) - public CompanyDetailResponse getCompany(Long companyId) { - Company company = companyRepository.findByCompanyIdAndCompanystatus(companyId, CompanyStatus.ACTIVE) - .orElseThrow(() -> new BusinessException(ErrorCode.Company_NOT_EXISTS)); - return CompanyDetailResponse.from(company); + public Page findAllByCompanystatus(Pageable pageable) { + return companyRepository.findAllByCompanystatus(CompanyStatus.ACTIVE, pageable); } + /** + * 모든 활성 회사 조회 + * + * @return 활성 회사 리스트 + */ @Transactional(readOnly = true) - public Company findById(Long companyId) { - return companyRepository.findByCompanyIdAndCompanystatus(companyId, CompanyStatus.ACTIVE) - .orElseThrow(() -> new BusinessException(ErrorCode.Company_NOT_EXISTS)); - } - - @Transactional - public void deleteCompany(Long companyId) { - Company company = companyRepository.findById(companyId) - .orElseThrow(() -> new BusinessException(ErrorCode.Company_NOT_EXISTS)); - company.markDeleted(); - } - - @Transactional - public CompanyResponse updateCompanyStatus(Long companyId, CompanyStatus status) { - Company company = companyRepository.findById(companyId) - .orElseThrow(() -> new BusinessException(ErrorCode.Company_NOT_EXISTS)); - company.updateStatus(status); - return CompanyResponse.from(company); + public List findAllActiveCompanies() { + return companyRepository.findAllByCompanystatus(CompanyStatus.ACTIVE); } /** - * 여러 companyId로 Company 맵을 배치 조회 + * 여러 Company ID로 활성 회사 배치 조회 * - * @param companyIds 조회할 Company ID 리스트 - * @return companyId를 키로 하는 Company 맵 + * @param companyIds 회사 ID 리스트 + * @return 회사 ID를 키로 하는 회사 맵 */ @Transactional(readOnly = true) - public Map getCompanyMapByCompanyIdIn(List companyIds) { + public Map findAllByCompanyIdInAndCompanystatus(List companyIds) { if (companyIds == null || companyIds.isEmpty()) { return Map.of(); } @@ -127,8 +95,25 @@ public Map getCompanyMapByCompanyIdIn(List companyIds) { .collect(Collectors.toMap(Company::getCompanyId, company -> company)); } + /** + * 활성 회사 수 조회 + * + * @return 활성 회사 개수 + */ @Transactional(readOnly = true) public Long countActiveCompanies() { return companyRepository.countByCompanystatus(CompanyStatus.ACTIVE); } + + /** + * Company ID로 활성 회사 상세 조회 (DTO 반환용) + * + * @param companyId 회사 ID + * @return 활성 회사 엔티티 + */ + @Transactional(readOnly = true) + public Company findByCompanyIdAndCompanystatus(Long companyId) { + return companyRepository.findByCompanyIdAndCompanystatus(companyId, CompanyStatus.ACTIVE) + .orElseThrow(() -> new BusinessException(ErrorCode.Company_NOT_EXISTS)); + } } diff --git a/src/main/java/com/workhub/userTable/service/company/CreateCompanyService.java b/src/main/java/com/workhub/userTable/service/company/CreateCompanyService.java new file mode 100644 index 0000000..e27ffa9 --- /dev/null +++ b/src/main/java/com/workhub/userTable/service/company/CreateCompanyService.java @@ -0,0 +1,34 @@ +package com.workhub.userTable.service.company; + +import com.workhub.userTable.dto.company.request.CompanyRegisterRequest; +import com.workhub.userTable.dto.company.response.CompanyResponse; +import com.workhub.userTable.entity.Company; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class CreateCompanyService { + + private final CompanyService companyService; + + /** + * 새로운 회사 등록 + * 사업자번호 중복 검증 후 회사 정보를 저장 + * + * @param request 회사 등록 요청 정보 (회사명, 사업자번호 등) + * @return 등록된 회사 정보 + * @throws BusinessException 사업자번호가 이미 존재할 경우 + */ + public CompanyResponse registerCompany(CompanyRegisterRequest request) { + + companyService.validateDuplicateCompanyNumber(request.companyNumber()); + + Company company = Company.of(request); + Company savedCompany = companyService.save(company); + + return CompanyResponse.from(savedCompany); + } +} diff --git a/src/main/java/com/workhub/userTable/service/company/DeleteCompanyService.java b/src/main/java/com/workhub/userTable/service/company/DeleteCompanyService.java new file mode 100644 index 0000000..2b487d1 --- /dev/null +++ b/src/main/java/com/workhub/userTable/service/company/DeleteCompanyService.java @@ -0,0 +1,26 @@ +package com.workhub.userTable.service.company; + +import com.workhub.userTable.entity.Company; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class DeleteCompanyService { + + private final CompanyService companyService; + + /** + * 회사 소프트 삭제 + * 실제 데이터를 삭제하지 않고 deletedAt 타임스탬프를 기록 + * + * @param companyId 삭제할 회사 ID + * @throws BusinessException 회사가 존재하지 않을 경우 + */ + public void deleteCompany(Long companyId) { + Company company = companyService.findById(companyId); + company.markDeleted(); + } +} diff --git a/src/main/java/com/workhub/userTable/service/company/ReadCompanyService.java b/src/main/java/com/workhub/userTable/service/company/ReadCompanyService.java new file mode 100644 index 0000000..5bd6a52 --- /dev/null +++ b/src/main/java/com/workhub/userTable/service/company/ReadCompanyService.java @@ -0,0 +1,106 @@ +package com.workhub.userTable.service.company; + +import com.workhub.project.entity.Project; +import com.workhub.project.entity.Status; +import com.workhub.project.service.ProjectService; +import com.workhub.userTable.dto.company.response.CompanyDetailResponse; +import com.workhub.userTable.dto.company.response.CompanyListResponse; +import com.workhub.userTable.dto.company.response.CompanyTitleResponse; +import com.workhub.userTable.entity.Company; +import com.workhub.userTable.entity.UserRole; +import com.workhub.userTable.service.user.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ReadCompanyService { + + private final CompanyService companyService; + private final ProjectService projectService; + private final UserService userService; + + /** + * 활성 회사 목록을 페이징하여 조회 + * 각 회사별 프로젝트 통계(진행 중/총 프로젝트 수)와 클라이언트 멤버 수를 포함 + * + * @param pageable 페이징 정보 (size, sort 등) + * @return 회사 목록 응답 (통계 포함) + */ + public Page getCompanies(Pageable pageable) { + Page companies = companyService.findAllByCompanystatus(pageable); + + return companies.map(company -> { + // 해당 회사의 프로젝트 조회 (ProjectService를 통해) + List projects = projectService.findAllByClientCompanyId(company.getCompanyId()); + + // IN_PROGRESS와 총 프로젝트 카운트 + long inProgressCount = projects.stream() + .filter(p -> p.getStatus() == Status.IN_PROGRESS) + .count(); + long totalProjectCount = projects.size(); + + // 활성 클라이언트 멤버 수 조회 (UserService를 통해) + long clientMemberCount = userService.countByCompanyIdAndRoleAndStatus( + company.getCompanyId(), + UserRole.CLIENT, + com.workhub.userTable.entity.Status.ACTIVE + ); + + // CompanyListResponse 생성 + return CompanyListResponse.from(company, inProgressCount, totalProjectCount, clientMemberCount); + }); + } + + /** + * 모든 활성 회사의 이름 목록 조회 + * 드롭다운이나 선택 UI에서 사용 + * + * @return 회사 ID와 이름을 포함한 간단한 응답 리스트 + */ + public List getCompanyNameList() { + return companyService.findAllActiveCompanies().stream() + .map(CompanyTitleResponse::from) + .toList(); + } + + /** + * 특정 회사의 상세 정보 조회 + * + * @param companyId 조회할 회사 ID + * @return 회사 상세 정보 + * @throws BusinessException 회사가 존재하지 않거나 비활성 상태일 경우 + */ + public CompanyDetailResponse getCompany(Long companyId) { + Company company = companyService.findByCompanyIdAndCompanystatus(companyId); + return CompanyDetailResponse.from(company); + } + + /** + * 여러 회사를 배치로 조회하여 Map으로 반환 + * 다른 도메인(Project 등)에서 N+1 문제 방지를 위해 사용 + * + * @param companyIds 조회할 회사 ID 리스트 + * @return 회사 ID를 키로 하는 Company 엔티티 맵 + */ + public Map getCompanyMapByCompanyIdIn(List companyIds) { + return companyService.findAllByCompanyIdInAndCompanystatus(companyIds); + } + + /** + * 활성 상태인 회사의 총 개수 조회 + * 관리자 대시보드 통계에서 사용 + * + * @return 활성 회사 수 + */ + public Long countActiveCompanies() { + return companyService.countActiveCompanies(); + } +} diff --git a/src/main/java/com/workhub/userTable/service/company/UpdateCompanyService.java b/src/main/java/com/workhub/userTable/service/company/UpdateCompanyService.java new file mode 100644 index 0000000..a223551 --- /dev/null +++ b/src/main/java/com/workhub/userTable/service/company/UpdateCompanyService.java @@ -0,0 +1,31 @@ +package com.workhub.userTable.service.company; + +import com.workhub.userTable.dto.company.response.CompanyResponse; +import com.workhub.userTable.entity.Company; +import com.workhub.userTable.entity.CompanyStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class UpdateCompanyService { + + private final CompanyService companyService; + + /** + * 회사의 상태 변경 + * 활성(ACTIVE) ↔ 비활성(INACTIVE) 전환 + * + * @param companyId 상태를 변경할 회사 ID + * @param status 변경할 상태 (ACTIVE, INACTIVE) + * @return 변경된 회사 정보 + * @throws BusinessException 회사가 존재하지 않을 경우 + */ + public CompanyResponse updateCompanyStatus(Long companyId, CompanyStatus status) { + Company company = companyService.findById(companyId); + company.updateStatus(status); + return CompanyResponse.from(company); + } +} diff --git a/src/main/java/com/workhub/userTable/service/user/UserService.java b/src/main/java/com/workhub/userTable/service/user/UserService.java index bd20595..2c5c872 100644 --- a/src/main/java/com/workhub/userTable/service/user/UserService.java +++ b/src/main/java/com/workhub/userTable/service/user/UserService.java @@ -9,6 +9,7 @@ import com.workhub.userTable.dto.user.response.UserLoginResponse; import com.workhub.userTable.dto.user.response.UserNameResponse; import com.workhub.userTable.entity.Status; +import com.workhub.userTable.entity.UserRole; import com.workhub.userTable.entity.UserTable; import com.workhub.userTable.repository.UserRepository; import lombok.RequiredArgsConstructor; @@ -115,4 +116,17 @@ public Long countActiveUsers(){ public Long countActiveUsersUntil(LocalDateTime monthEnd) { return userRepository.countActiveUsersUntil(monthEnd); } + + /** + * 특정 회사의 특정 역할 및 상태를 가진 사용자 수 조회 + * + * @param companyId 회사 ID + * @param role 사용자 역할 + * @param status 사용자 상태 + * @return 조건에 맞는 사용자 수 + */ + @Transactional(readOnly = true) + public Long countByCompanyIdAndRoleAndStatus(Long companyId, UserRole role, Status status) { + return userRepository.countByCompanyIdAndRoleAndStatus(companyId, role, status); + } } diff --git a/src/test/java/com/workhub/project/service/ReadProjectServiceTest.java b/src/test/java/com/workhub/project/service/ReadProjectServiceTest.java index 7b766a3..e912f55 100644 --- a/src/test/java/com/workhub/project/service/ReadProjectServiceTest.java +++ b/src/test/java/com/workhub/project/service/ReadProjectServiceTest.java @@ -13,6 +13,7 @@ import com.workhub.userTable.entity.Company; import com.workhub.userTable.entity.CompanyStatus; import com.workhub.userTable.service.company.CompanyService; +import com.workhub.userTable.service.company.ReadCompanyService; import com.workhub.userTable.service.user.UserService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -46,6 +47,9 @@ class ReadProjectServiceTest { @Mock private ProjectNodeService projectNodeService; + @Mock + private ReadCompanyService readCompanyService; + @Mock private CompanyService companyService; @@ -153,6 +157,7 @@ void init() { .build(); lenient().when(companyService.findById(anyLong())).thenReturn(company); + lenient().when(readCompanyService.getCompanyMapByCompanyIdIn(anyList())).thenReturn(Map.of(1L, company)); } @Test