diff --git a/src/main/java/run/backend/domain/crew/controller/CrewController.java b/src/main/java/run/backend/domain/crew/controller/CrewController.java index d70ac7b..bccd8fc 100644 --- a/src/main/java/run/backend/domain/crew/controller/CrewController.java +++ b/src/main/java/run/backend/domain/crew/controller/CrewController.java @@ -8,9 +8,11 @@ import org.springframework.web.multipart.MultipartFile; import run.backend.domain.crew.dto.common.CrewInviteCodeDto; import run.backend.domain.crew.dto.request.CrewInfoRequest; +import run.backend.domain.crew.dto.request.MemberRoleChangeRequest; import run.backend.domain.crew.dto.response.*; import run.backend.domain.crew.entity.Crew; import run.backend.domain.crew.service.CrewEventService; +import run.backend.domain.crew.service.CrewMemberService; import run.backend.domain.crew.service.CrewRankingService; import run.backend.domain.crew.service.CrewService; import run.backend.domain.member.entity.Member; @@ -27,6 +29,7 @@ public class CrewController { private final CrewService crewService; private final CrewEventService crewEventService; + private final CrewMemberService crewMemberService; private final CrewRankingService crewRankingService; @PostMapping @@ -132,4 +135,34 @@ public CommonResponse getCrewRankingsStatus(@MemberCr CrewRankingStatusResponse response = crewRankingService.getCrewRankingStatus(crew); return new CommonResponse<>("크루 땅따먹기 현황 조회 성공", response); } + + @GetMapping("/members") + @Operation(summary = "크루원 조회", description = "전체 크루원 조회하는 API 입니다.") + public CommonResponse getCrewMembers(@MemberCrew Crew crew) { + + CrewMemberResponse response = crewMemberService.getCrewMembers(crew); + return new CommonResponse<>("크루원 조회 성공", response); + } + + @PostMapping("/members/{memberId}/role") + @PreAuthorize("hasRole('MANAGER') or hasRole('LEADER')") + @Operation(summary = "크루원 역할 변경", description = "크루원 역할 변경하는 API 입니다.") + public CommonResponse updateCrewMemberRole( + @PathVariable Long memberId, + @RequestBody MemberRoleChangeRequest request + ) { + crewMemberService.updateCrewMemberRole(memberId, request); + return new CommonResponse<>("크루원 역할 변경 성공"); + } + + @GetMapping("/search") + @Operation(summary = "크루 검색", description = "크루 이름으로 검색 API 입니다.") + public CommonResponse> searchCrew( + @RequestParam String crewName, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ) { + PageResponse response = crewService.searchCrewsByName(crewName, page, size); + return new CommonResponse<>("크루 검색 성공", response); + } } diff --git a/src/main/java/run/backend/domain/crew/dto/query/CrewMemberProfileDto.java b/src/main/java/run/backend/domain/crew/dto/query/CrewMemberProfileDto.java new file mode 100644 index 0000000..8109976 --- /dev/null +++ b/src/main/java/run/backend/domain/crew/dto/query/CrewMemberProfileDto.java @@ -0,0 +1,10 @@ +package run.backend.domain.crew.dto.query; + +import run.backend.domain.member.enums.Role; + +public record CrewMemberProfileDto( + String image, + String nickname, + Role role +) { +} diff --git a/src/main/java/run/backend/domain/crew/dto/query/CrewProfileDto.java b/src/main/java/run/backend/domain/crew/dto/query/CrewProfileDto.java new file mode 100644 index 0000000..1f9b5a8 --- /dev/null +++ b/src/main/java/run/backend/domain/crew/dto/query/CrewProfileDto.java @@ -0,0 +1,8 @@ +package run.backend.domain.crew.dto.query; + +public record CrewProfileDto( + String image, + String name, + String description +) { +} diff --git a/src/main/java/run/backend/domain/crew/dto/request/MemberRoleChangeRequest.java b/src/main/java/run/backend/domain/crew/dto/request/MemberRoleChangeRequest.java new file mode 100644 index 0000000..140f1ad --- /dev/null +++ b/src/main/java/run/backend/domain/crew/dto/request/MemberRoleChangeRequest.java @@ -0,0 +1,8 @@ +package run.backend.domain.crew.dto.request; + +import run.backend.domain.member.enums.Role; + +public record MemberRoleChangeRequest( + Role role +) { +} diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewMemberProfile.java b/src/main/java/run/backend/domain/crew/dto/response/CrewMemberProfile.java deleted file mode 100644 index d65e068..0000000 --- a/src/main/java/run/backend/domain/crew/dto/response/CrewMemberProfile.java +++ /dev/null @@ -1,4 +0,0 @@ -package run.backend.domain.crew.dto.response; - -public record CrewMemberProfile() { -} diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewMemberProfileResponse.java b/src/main/java/run/backend/domain/crew/dto/response/CrewMemberProfileResponse.java new file mode 100644 index 0000000..79d6cbb --- /dev/null +++ b/src/main/java/run/backend/domain/crew/dto/response/CrewMemberProfileResponse.java @@ -0,0 +1,10 @@ +package run.backend.domain.crew.dto.response; + +import run.backend.domain.member.enums.Role; + +public record CrewMemberProfileResponse( + String image, + String nickname, + Role role +) { +} diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewMemberResponse.java b/src/main/java/run/backend/domain/crew/dto/response/CrewMemberResponse.java index 693abe3..439f51b 100644 --- a/src/main/java/run/backend/domain/crew/dto/response/CrewMemberResponse.java +++ b/src/main/java/run/backend/domain/crew/dto/response/CrewMemberResponse.java @@ -3,6 +3,7 @@ import java.util.List; public record CrewMemberResponse( - List crewMembers + List managers, + List members ) { } diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewSearchResponse.java b/src/main/java/run/backend/domain/crew/dto/response/CrewSearchResponse.java index 448ea7d..aa2334b 100644 --- a/src/main/java/run/backend/domain/crew/dto/response/CrewSearchResponse.java +++ b/src/main/java/run/backend/domain/crew/dto/response/CrewSearchResponse.java @@ -1,8 +1,9 @@ package run.backend.domain.crew.dto.response; -import java.util.List; public record CrewSearchResponse( - List crewProfiles + String image, + String name, + String description ) { } diff --git a/src/main/java/run/backend/domain/crew/entity/Crew.java b/src/main/java/run/backend/domain/crew/entity/Crew.java index 40ee194..6dbf6f6 100644 --- a/src/main/java/run/backend/domain/crew/entity/Crew.java +++ b/src/main/java/run/backend/domain/crew/entity/Crew.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.SQLDelete; import run.backend.global.common.BaseEntity; import java.math.BigDecimal; @@ -12,6 +13,7 @@ @Table(name = "crews") @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE crews SET deleted_at = NOW() WHERE id = ?") public class Crew extends BaseEntity { @Id diff --git a/src/main/java/run/backend/domain/crew/entity/JoinCrew.java b/src/main/java/run/backend/domain/crew/entity/JoinCrew.java index 5716da9..b23cf62 100644 --- a/src/main/java/run/backend/domain/crew/entity/JoinCrew.java +++ b/src/main/java/run/backend/domain/crew/entity/JoinCrew.java @@ -5,6 +5,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; import run.backend.domain.crew.enums.JoinStatus; import run.backend.domain.member.entity.Member; import run.backend.domain.member.enums.Role; @@ -17,6 +18,7 @@ @Getter @Table(name = "join_crews") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE join_crews SET deleted_at = NOW() WHERE id = ?") public class JoinCrew extends BaseEntity { @Id diff --git a/src/main/java/run/backend/domain/crew/mapper/CrewMapper.java b/src/main/java/run/backend/domain/crew/mapper/CrewMapper.java index 447f22c..2ee9bb9 100644 --- a/src/main/java/run/backend/domain/crew/mapper/CrewMapper.java +++ b/src/main/java/run/backend/domain/crew/mapper/CrewMapper.java @@ -3,10 +3,9 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.ReportingPolicy; -import run.backend.domain.crew.dto.response.CrewBaseInfoResponse; -import run.backend.domain.crew.dto.response.CrewProfileResponse; -import run.backend.domain.crew.dto.response.CrewRankingResponse; -import run.backend.domain.crew.dto.response.CrewRankingStatusResponse; +import run.backend.domain.crew.dto.query.CrewMemberProfileDto; +import run.backend.domain.crew.dto.query.CrewProfileDto; +import run.backend.domain.crew.dto.response.*; import run.backend.domain.crew.entity.Crew; import run.backend.domain.member.entity.Member; @@ -26,6 +25,14 @@ public interface CrewMapper { List toCrewRankingResponseList(List crews); + CrewMemberProfileResponse toCrewMemberProfileResponse(CrewMemberProfileDto dto); + + List toCrewMemberProfileResponseList(List dtos); + + CrewSearchResponse toCrewSearchResponse(CrewProfileDto dto); + + List toCrewSearchResponseList(List dtos); + default Crew toEntity(String imageName, String name, String description) { return Crew.builder() .image(imageName) diff --git a/src/main/java/run/backend/domain/crew/repository/CrewRepository.java b/src/main/java/run/backend/domain/crew/repository/CrewRepository.java index 6c53f0e..50b3c93 100644 --- a/src/main/java/run/backend/domain/crew/repository/CrewRepository.java +++ b/src/main/java/run/backend/domain/crew/repository/CrewRepository.java @@ -3,14 +3,27 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import run.backend.domain.crew.dto.query.CrewProfileDto; import run.backend.domain.crew.entity.Crew; import java.util.Optional; public interface CrewRepository extends JpaRepository { - Optional findByInviteCode(String inviteCode); + Optional findByInviteCodeAndDeletedAtIsNull(String inviteCode); + Page findAllByDeletedAtIsNullOrderByMonthlyScoreTotalDesc(Pageable pageable); - Page findAllByOrderByMonthlyScoreTotalDesc(Pageable pageable); + @Query(""" + SELECT new run.backend.domain.crew.dto.query.CrewProfileDto( + c.image, + c.name, + c.description + ) + FROM Crew c + WHERE LOWER(c.name) LIKE LOWER(CONCAT('%', :name, '%')) + AND c.deletedAt IS NULL + """) + Page findByNameContainingIgnoreCase(String name, Pageable pageable); } diff --git a/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java b/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java index fd551ba..ec0a6a7 100644 --- a/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java +++ b/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java @@ -4,11 +4,14 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import run.backend.domain.crew.dto.query.CrewMemberProfileDto; import run.backend.domain.crew.entity.Crew; import run.backend.domain.crew.entity.JoinCrew; import run.backend.domain.crew.enums.JoinStatus; import run.backend.domain.member.entity.Member; import run.backend.domain.member.enums.Role; + +import java.util.List; import java.util.Optional; import run.backend.domain.event.dto.response.EventCreationValidationDto; @@ -60,4 +63,17 @@ Optional findCrewMemberById( @Param("crewId") Long crewId, @Param("status") JoinStatus status ); + + @Query(""" + SELECT new run.backend.domain.crew.dto.query.CrewMemberProfileDto( + m.profileImage, + m.nickname, + m.role + ) + FROM JoinCrew jc + JOIN jc.member m + WHERE jc.crew.id = :crewId + AND jc.joinStatus = :status + """) + List findAllCrewMemberByCrewId(@Param("crewId") Long crewId, @Param("status") JoinStatus status); } diff --git a/src/main/java/run/backend/domain/crew/service/CrewMemberService.java b/src/main/java/run/backend/domain/crew/service/CrewMemberService.java new file mode 100644 index 0000000..634956f --- /dev/null +++ b/src/main/java/run/backend/domain/crew/service/CrewMemberService.java @@ -0,0 +1,59 @@ +package run.backend.domain.crew.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import run.backend.domain.crew.dto.query.CrewMemberProfileDto; +import run.backend.domain.crew.dto.request.MemberRoleChangeRequest; +import run.backend.domain.crew.dto.response.CrewMemberProfileResponse; +import run.backend.domain.crew.dto.response.CrewMemberResponse; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.crew.enums.JoinStatus; +import run.backend.domain.crew.mapper.CrewMapper; +import run.backend.domain.crew.repository.JoinCrewRepository; +import run.backend.domain.member.entity.Member; +import run.backend.domain.member.enums.Role; +import run.backend.domain.member.exception.MemberException; +import run.backend.domain.member.repository.MemberRepository; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CrewMemberService { + + private final CrewMapper crewMapper; + private final MemberRepository memberRepository; + private final JoinCrewRepository joinCrewRepository; + + public CrewMemberResponse getCrewMembers(Crew crew) { + + List dtos = joinCrewRepository.findAllCrewMemberByCrewId(crew.getId(), JoinStatus.APPROVED); + + List all = crewMapper.toCrewMemberProfileResponseList(dtos); + + List managers = new ArrayList<>(); + List members = new ArrayList<>(); + + for (CrewMemberProfileResponse profile : all) { + + if (profile.role() == Role.MEMBER) + members.add(profile); + else + managers.add(profile); + } + return new CrewMemberResponse(managers, members); + } + + @Transactional + public void updateCrewMemberRole(Long memberId, MemberRoleChangeRequest request) { + + Member member = memberRepository.findById(memberId) + .orElseThrow(MemberException.MemberNotFound::new); + + member.updateRole(request.role()); + memberRepository.save(member); + } +} diff --git a/src/main/java/run/backend/domain/crew/service/CrewRankingService.java b/src/main/java/run/backend/domain/crew/service/CrewRankingService.java index 8001892..dd5571d 100644 --- a/src/main/java/run/backend/domain/crew/service/CrewRankingService.java +++ b/src/main/java/run/backend/domain/crew/service/CrewRankingService.java @@ -24,7 +24,7 @@ public class CrewRankingService { public PageResponse getCrewRanking(int page, int size) { - Page pageResult = crewRepository.findAllByOrderByMonthlyScoreTotalDesc(PageRequest.of(page, size)); + Page pageResult = crewRepository.findAllByDeletedAtIsNullOrderByMonthlyScoreTotalDesc(PageRequest.of(page, size)); List content = crewMapper.toCrewRankingResponseList(pageResult.getContent()); return PageResponse.toPageResponse(pageResult, content); diff --git a/src/main/java/run/backend/domain/crew/service/CrewService.java b/src/main/java/run/backend/domain/crew/service/CrewService.java index 8987602..31fcb10 100644 --- a/src/main/java/run/backend/domain/crew/service/CrewService.java +++ b/src/main/java/run/backend/domain/crew/service/CrewService.java @@ -1,10 +1,13 @@ package run.backend.domain.crew.service; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import run.backend.domain.crew.dto.common.CrewInviteCodeDto; +import run.backend.domain.crew.dto.query.CrewProfileDto; import run.backend.domain.crew.dto.request.CrewInfoRequest; import run.backend.domain.crew.dto.response.*; import run.backend.domain.crew.entity.Crew; @@ -18,6 +21,9 @@ import run.backend.domain.member.entity.Member; import run.backend.domain.member.enums.Role; import run.backend.domain.member.repository.MemberRepository; +import run.backend.global.common.response.PageResponse; + +import java.util.List; @Service @RequiredArgsConstructor @@ -68,7 +74,7 @@ public CrewInviteCodeDto getCrewInviteCode(Crew crew) { public CrewProfileResponse getCrewByInviteCode(String inviteCode) { - Crew crew = crewRepository.findByInviteCode(inviteCode) + Crew crew = crewRepository.findByInviteCodeAndDeletedAtIsNull(inviteCode) .orElseThrow(CrewException.NotFoundCrew::new); Member leader = joinCrewRepository.findCrewLeader(Role.LEADER, crew); @@ -93,4 +99,12 @@ public CrewBaseInfoResponse getCrewBaseInfo(Crew crew) { return crewMapper.toCrewBaseInfo(rank, crew); } + + public PageResponse searchCrewsByName(String crewName, int page, int size) { + + Page crewPage = crewRepository.findByNameContainingIgnoreCase(crewName, PageRequest.of(page, size)); + List content = crewMapper.toCrewSearchResponseList(crewPage.getContent()); + + return PageResponse.toPageResponse(crewPage, content); + } } diff --git a/src/test/java/run/backend/domain/crew/service/CrewMemberServiceTest.java b/src/test/java/run/backend/domain/crew/service/CrewMemberServiceTest.java new file mode 100644 index 0000000..d4a373b --- /dev/null +++ b/src/test/java/run/backend/domain/crew/service/CrewMemberServiceTest.java @@ -0,0 +1,123 @@ +package run.backend.domain.crew.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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 run.backend.domain.crew.dto.query.CrewMemberProfileDto; +import run.backend.domain.crew.dto.request.MemberRoleChangeRequest; +import run.backend.domain.crew.dto.response.CrewMemberProfileResponse; +import run.backend.domain.crew.dto.response.CrewMemberResponse; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.crew.enums.JoinStatus; +import run.backend.domain.crew.mapper.CrewMapper; +import run.backend.domain.crew.repository.JoinCrewRepository; +import run.backend.domain.member.entity.Member; +import run.backend.domain.member.enums.Role; +import run.backend.domain.member.exception.MemberException; +import run.backend.domain.member.repository.MemberRepository; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DisplayName("CrewMember 서비스 테스트") +@ExtendWith(MockitoExtension.class) +public class CrewMemberServiceTest { + + @InjectMocks + private CrewMemberService crewMemberService; + + @Mock + private CrewMapper crewMapper; + + @Mock + private MemberRepository memberRepository; + + @Mock + private JoinCrewRepository joinCrewRepository; + + @Mock + private Member member; + + @Mock + private Crew crew; + + + @Nested + @DisplayName("getCrewMembers 메서드는 ") + class getCrewMembersTest { + + @Test + @DisplayName("크루원 조회 시 역할에 따라 관리자와 일반 멤버로 구분하여 응답한다.") + void returnsCrewMembersGroupedByRole() { + + // given + List dtos = List.of( + new CrewMemberProfileDto("img1", "user1", Role.MANAGER), + new CrewMemberProfileDto("img2", "user2", Role.MEMBER) + ); + List responses = List.of( + new CrewMemberProfileResponse("img1", "user1", Role.MANAGER), + new CrewMemberProfileResponse("img2", "user2", Role.MEMBER) + ); + when(joinCrewRepository.findAllCrewMemberByCrewId(crew.getId(), JoinStatus.APPROVED)) + .thenReturn(dtos); + when(crewMapper.toCrewMemberProfileResponseList(dtos)) + .thenReturn(responses); + + // when + CrewMemberResponse result = crewMemberService.getCrewMembers(crew); + + // then + assertEquals(1, result.managers().size()); + assertEquals(1, result.members().size()); + assertEquals("user1", result.managers().get(0).nickname()); + assertEquals("user2", result.members().get(0).nickname()); + } + } + + @Nested + @DisplayName("updateCrewMemberRole 메서드는 ") + class updateCrewMemberRoleTest { + + @Test + @DisplayName("회원 역할 변경 시 해당 회원의 역할을 요청된 값으로 변경한다.") + void updatesMemberRole_whenMemberExists() { + + // given + MemberRoleChangeRequest request = new MemberRoleChangeRequest(Role.MANAGER); + when(memberRepository.findById(member.getId())) + .thenReturn(Optional.of(member)); + + // when + crewMemberService.updateCrewMemberRole(member.getId(), request); + + // then + verify(member).updateRole(Role.MANAGER); + verify(memberRepository).save(member); + } + + @Test + @DisplayName("존재하지 않는 회원에게 역할 변경을 시도하면 예외가 발생한다.") + void throwsException_whenMemberNotFound() { + + // given + Long memberId = 999L; + MemberRoleChangeRequest request = new MemberRoleChangeRequest(Role.MANAGER); + + when(memberRepository.findById(memberId)).thenReturn(Optional.empty()); + + // then + assertThrows(MemberException.MemberNotFound.class, + () -> crewMemberService.updateCrewMemberRole(memberId, request)); + } + } +} diff --git a/src/test/java/run/backend/domain/crew/service/CrewRankingServiceTest.java b/src/test/java/run/backend/domain/crew/service/CrewRankingServiceTest.java index 3ef643b..bb0d574 100644 --- a/src/test/java/run/backend/domain/crew/service/CrewRankingServiceTest.java +++ b/src/test/java/run/backend/domain/crew/service/CrewRankingServiceTest.java @@ -84,7 +84,7 @@ void getCrewRanking_whenValidPageRequest_thenReturnsPageResponse() { new CrewRankingResponse(5L, "name5", "image5", 1) ); - when(crewRepository.findAllByOrderByMonthlyScoreTotalDesc(PageRequest.of(page, size))) + when(crewRepository.findAllByDeletedAtIsNullOrderByMonthlyScoreTotalDesc(PageRequest.of(page, size))) .thenReturn(crewPage); when(crewMapper.toCrewRankingResponseList(crewPage.getContent())) .thenReturn(responseList);