diff --git a/modules/member/src/main/java/kr/spot/application/query/GetMemberInfoService.java b/modules/member/src/main/java/kr/spot/application/query/GetMemberInfoService.java index 8043032..97569b7 100644 --- a/modules/member/src/main/java/kr/spot/application/query/GetMemberInfoService.java +++ b/modules/member/src/main/java/kr/spot/application/query/GetMemberInfoService.java @@ -1,8 +1,19 @@ package kr.spot.application.query; +import java.util.List; import kr.spot.domain.Member; +import kr.spot.domain.association.PreferredCategory; +import kr.spot.domain.association.PreferredRegion; import kr.spot.infrastructure.jpa.MemberRepository; +import kr.spot.infrastructure.jpa.PreferredCategoryRepository; +import kr.spot.infrastructure.jpa.PreferredRegionRepository; +import kr.spot.ports.StudyParticipationInfoPort; +import kr.spot.ports.dto.StudyParticipationInfo; +import kr.spot.presentation.query.dto.response.GetMemberInfoResponse; +import kr.spot.presentation.query.dto.response.GetMemberInfoResponse.StudyParticipationInfoResponse; import kr.spot.presentation.query.dto.response.GetMemberNameResponse; +import kr.spot.presentation.query.dto.response.GetMemberPreferCategoryResponse; +import kr.spot.presentation.query.dto.response.GetMemberPreferRegionResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -12,10 +23,55 @@ @RequiredArgsConstructor public class GetMemberInfoService { + private final StudyParticipationInfoPort studyParticipationInfoPort; private final MemberRepository memberRepository; + private final PreferredCategoryRepository preferredCategoryRepository; + private final PreferredRegionRepository preferredRegionRepository; public GetMemberNameResponse getMemberName(Long memberId) { Member member = memberRepository.getMemberById(memberId); return GetMemberNameResponse.from(member.getName()); } + + public GetMemberInfoResponse getMemberInfo(Long memberId) { + Member member = memberRepository.getMemberById(memberId); + + StudyParticipationInfo studyParticipationInfo = studyParticipationInfoPort.getStudyParticipationInfo( + memberId); + + return GetMemberInfoResponse.from( + member.getId(), + member.getName(), + member.getProfileImageUrl(), + member.getLoginType(), + member.getEmail().getValue(), + StudyParticipationInfoResponse.from( + studyParticipationInfo.participatingStudyCount(), + studyParticipationInfo.recruitingStudyCount(), + studyParticipationInfo.appliedStudyCount() + ) + ); + } + + public GetMemberPreferCategoryResponse getMemberPreferCategories(Long memberId) { + List preferredCategories = preferredCategoryRepository.findAllByMemberId( + memberId); + + List categoryCodes = preferredCategories.stream() + .map(PreferredCategory::getCategory) + .toList(); + + return GetMemberPreferCategoryResponse.from(categoryCodes, categoryCodes.size()); + } + + public GetMemberPreferRegionResponse getMemberPreferRegions(Long memberId) { + List preferredRegions = preferredRegionRepository.findAllByMemberId( + memberId); + + List regionCodes = preferredRegions.stream() + .map(PreferredRegion::getRegionCode) + .toList(); + + return GetMemberPreferRegionResponse.from(regionCodes, regionCodes.size()); + } } diff --git a/modules/member/src/main/java/kr/spot/presentation/query/MemberQueryController.java b/modules/member/src/main/java/kr/spot/presentation/query/MemberQueryController.java index 1d70a4d..055ad19 100644 --- a/modules/member/src/main/java/kr/spot/presentation/query/MemberQueryController.java +++ b/modules/member/src/main/java/kr/spot/presentation/query/MemberQueryController.java @@ -10,7 +10,10 @@ import kr.spot.annotations.CurrentMember; import kr.spot.application.query.GetMemberInfoService; import kr.spot.code.status.SuccessStatus; +import kr.spot.presentation.query.dto.response.GetMemberInfoResponse; import kr.spot.presentation.query.dto.response.GetMemberNameResponse; +import kr.spot.presentation.query.dto.response.GetMemberPreferCategoryResponse; +import kr.spot.presentation.query.dto.response.GetMemberPreferRegionResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -37,4 +40,29 @@ public ResponseEntity> getMemberName( GetMemberNameResponse response = getMemberInfoService.getMemberName(memberId); return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response)); } + + @Operation(summary = "[마이페이지] 회원 정보 조회", description = "현재 로그인한 회원의 정보를 조회합니다.") + @GetMapping("/info") + public ResponseEntity> getMemberInfo( + @CurrentMember @Parameter(hidden = true) Long memberId) { + GetMemberInfoResponse response = getMemberInfoService.getMemberInfo(memberId); + return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response)); + } + + @Operation(summary = "회원 선호 카테고리 조회", description = "현재 로그인한 회원의 선호 카테고리를 조회합니다.") + @GetMapping("/prefer-categories") + public ResponseEntity> getMemberPreferCategories( + @CurrentMember @Parameter(hidden = true) Long memberId) { + GetMemberPreferCategoryResponse response = getMemberInfoService.getMemberPreferCategories( + memberId); + return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response)); + } + + @Operation(summary = "회원 선호 지역 조회", description = "현재 로그인한 회원의 선호 지역을 조회합니다.") + @GetMapping("/prefer-regions") + public ResponseEntity> getMemberPreferRegions( + @CurrentMember @Parameter(hidden = true) Long memberId) { + GetMemberPreferRegionResponse response = getMemberInfoService.getMemberPreferRegions(memberId); + return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response)); + } } diff --git a/modules/member/src/main/java/kr/spot/presentation/query/dto/response/GetMemberInfoResponse.java b/modules/member/src/main/java/kr/spot/presentation/query/dto/response/GetMemberInfoResponse.java new file mode 100644 index 0000000..4be0fe9 --- /dev/null +++ b/modules/member/src/main/java/kr/spot/presentation/query/dto/response/GetMemberInfoResponse.java @@ -0,0 +1,52 @@ +package kr.spot.presentation.query.dto.response; + +import kr.spot.domain.enums.LoginType; + +public record GetMemberInfoResponse( + long memberId, + String nickname, + String profileImageUrl, + LoginType loginType, + String email, + StudyParticipationInfoResponse studyParticipationInfo +) { + + public static GetMemberInfoResponse from( + long memberId, + String nickname, + String profileImageUrl, + LoginType loginType, + String email, + StudyParticipationInfoResponse studyParticipationInfo + ) { + return new GetMemberInfoResponse( + memberId, + nickname, + profileImageUrl, + loginType, + email, + studyParticipationInfo + ); + } + + public record StudyParticipationInfoResponse( + long participatingStudyCount, + long recruitingStudyCount, + long appliedStudyCount + ) { + + public static StudyParticipationInfoResponse from( + long participatingStudyCount, + long recruitingStudyCount, + long appliedStudyCount + ) { + return new StudyParticipationInfoResponse( + participatingStudyCount, + recruitingStudyCount, + appliedStudyCount + ); + } + + } + +} diff --git a/modules/member/src/main/java/kr/spot/presentation/query/dto/response/GetMemberPreferCategoryResponse.java b/modules/member/src/main/java/kr/spot/presentation/query/dto/response/GetMemberPreferCategoryResponse.java new file mode 100644 index 0000000..dd3716d --- /dev/null +++ b/modules/member/src/main/java/kr/spot/presentation/query/dto/response/GetMemberPreferCategoryResponse.java @@ -0,0 +1,14 @@ +package kr.spot.presentation.query.dto.response; + +import java.util.List; + +public record GetMemberPreferCategoryResponse( + List categories, + long totalCount +) { + + public static GetMemberPreferCategoryResponse from(List categories, + long totalCount) { + return new GetMemberPreferCategoryResponse(categories, totalCount); + } +} diff --git a/modules/member/src/main/java/kr/spot/presentation/query/dto/response/GetMemberPreferRegionResponse.java b/modules/member/src/main/java/kr/spot/presentation/query/dto/response/GetMemberPreferRegionResponse.java new file mode 100644 index 0000000..9fd1ecf --- /dev/null +++ b/modules/member/src/main/java/kr/spot/presentation/query/dto/response/GetMemberPreferRegionResponse.java @@ -0,0 +1,14 @@ +package kr.spot.presentation.query.dto.response; + +import java.util.List; + +public record GetMemberPreferRegionResponse( + List regionCodes, + long totalCount +) { + + public static GetMemberPreferRegionResponse from(List regionCodes, + long totalCount) { + return new GetMemberPreferRegionResponse(regionCodes, totalCount); + } +} diff --git a/modules/member/src/test/java/kr/spot/application/query/GetMemberInfoServiceTest.java b/modules/member/src/test/java/kr/spot/application/query/GetMemberInfoServiceTest.java new file mode 100644 index 0000000..7ccd1ab --- /dev/null +++ b/modules/member/src/test/java/kr/spot/application/query/GetMemberInfoServiceTest.java @@ -0,0 +1,185 @@ +package kr.spot.application.query; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.util.List; +import kr.spot.common.fixture.MemberFixture; +import kr.spot.domain.Member; +import kr.spot.domain.association.PreferredCategory; +import kr.spot.domain.association.PreferredRegion; +import kr.spot.domain.enums.LoginType; +import kr.spot.infrastructure.jpa.MemberRepository; +import kr.spot.infrastructure.jpa.PreferredCategoryRepository; +import kr.spot.infrastructure.jpa.PreferredRegionRepository; +import kr.spot.ports.StudyParticipationInfoPort; +import kr.spot.ports.dto.StudyParticipationInfo; +import kr.spot.presentation.query.dto.response.GetMemberInfoResponse; +import kr.spot.presentation.query.dto.response.GetMemberNameResponse; +import kr.spot.presentation.query.dto.response.GetMemberPreferCategoryResponse; +import kr.spot.presentation.query.dto.response.GetMemberPreferRegionResponse; +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; + +@ExtendWith(MockitoExtension.class) +class GetMemberInfoServiceTest { + + @Mock + MemberRepository memberRepository; + + @Mock + PreferredCategoryRepository preferredCategoryRepository; + + @Mock + PreferredRegionRepository preferredRegionRepository; + + @Mock + StudyParticipationInfoPort studyParticipationInfoPort; + + @InjectMocks + GetMemberInfoService sut; + + @Nested + @DisplayName("getMemberName 메서드는") + class GetMemberNameTest { + + @Test + @DisplayName("회원 ID로 회원 이름을 조회한다") + void should_return_member_name_when_member_exists() { + // given + long memberId = MemberFixture.ID; + Member member = MemberFixture.member(); + + when(memberRepository.getMemberById(memberId)).thenReturn(member); + + // when + GetMemberNameResponse response = sut.getMemberName(memberId); + + // then + assertThat(response.name()).isEqualTo(MemberFixture.NAME); + } + } + + @Nested + @DisplayName("getMemberInfo 메서드는") + class GetMemberInfoTest { + + @Test + @DisplayName("회원 ID로 회원 정보와 스터디 참여 정보를 조회한다") + void should_return_member_info_with_study_participation_info() { + // given + long memberId = MemberFixture.ID; + Member member = MemberFixture.member(); + StudyParticipationInfo studyParticipationInfo = StudyParticipationInfo.from(3L, 1L, 2L); + + when(memberRepository.getMemberById(memberId)).thenReturn(member); + when(studyParticipationInfoPort.getStudyParticipationInfo(memberId)) + .thenReturn(studyParticipationInfo); + + // when + GetMemberInfoResponse response = sut.getMemberInfo(memberId); + + // then + assertThat(response.memberId()).isEqualTo(MemberFixture.ID); + assertThat(response.nickname()).isEqualTo(MemberFixture.NAME); + assertThat(response.profileImageUrl()).isEqualTo(MemberFixture.PROFILE_IMAGE); + assertThat(response.loginType()).isEqualTo(LoginType.KAKAO); + assertThat(response.email()).isEqualTo(MemberFixture.EMAIL); + assertThat(response.studyParticipationInfo().participatingStudyCount()).isEqualTo(3L); + assertThat(response.studyParticipationInfo().recruitingStudyCount()).isEqualTo(1L); + assertThat(response.studyParticipationInfo().appliedStudyCount()).isEqualTo(2L); + } + } + + @Nested + @DisplayName("getMemberPreferCategories 메서드는") + class GetMemberPreferCategoriesTest { + + @Test + @DisplayName("회원 ID로 선호 카테고리 목록을 조회한다") + void should_return_preferred_categories_when_member_has_categories() { + // given + long memberId = MemberFixture.ID; + List preferredCategories = List.of( + PreferredCategory.of(1L, memberId, "PROGRAMMING"), + PreferredCategory.of(2L, memberId, "DESIGN"), + PreferredCategory.of(3L, memberId, "LANGUAGE") + ); + + when(preferredCategoryRepository.findAllByMemberId(memberId)) + .thenReturn(preferredCategories); + + // when + GetMemberPreferCategoryResponse response = sut.getMemberPreferCategories(memberId); + + // then + assertThat(response.categories()).containsExactly("PROGRAMMING", "DESIGN", "LANGUAGE"); + assertThat(response.totalCount()).isEqualTo(3); + } + + @Test + @DisplayName("선호 카테고리가 없으면 빈 목록을 반환한다") + void should_return_empty_list_when_member_has_no_categories() { + // given + long memberId = MemberFixture.ID; + + when(preferredCategoryRepository.findAllByMemberId(memberId)) + .thenReturn(List.of()); + + // when + GetMemberPreferCategoryResponse response = sut.getMemberPreferCategories(memberId); + + // then + assertThat(response.categories()).isEmpty(); + assertThat(response.totalCount()).isZero(); + } + } + + @Nested + @DisplayName("getMemberPreferRegions 메서드는") + class GetMemberPreferRegionsTest { + + @Test + @DisplayName("회원 ID로 선호 지역 목록을 조회한다") + void should_return_preferred_regions_when_member_has_regions() { + // given + long memberId = MemberFixture.ID; + List preferredRegions = List.of( + PreferredRegion.of(1L, memberId, "SEOUL"), + PreferredRegion.of(2L, memberId, "BUSAN") + ); + + when(preferredRegionRepository.findAllByMemberId(memberId)) + .thenReturn(preferredRegions); + + // when + GetMemberPreferRegionResponse response = sut.getMemberPreferRegions(memberId); + + // then + assertThat(response.regionCodes()).containsExactly("SEOUL", "BUSAN"); + assertThat(response.totalCount()).isEqualTo(2); + } + + @Test + @DisplayName("선호 지역이 없으면 빈 목록을 반환한다") + void should_return_empty_list_when_member_has_no_regions() { + // given + long memberId = MemberFixture.ID; + + when(preferredRegionRepository.findAllByMemberId(memberId)) + .thenReturn(List.of()); + + // when + GetMemberPreferRegionResponse response = sut.getMemberPreferRegions(memberId); + + // then + assertThat(response.regionCodes()).isEmpty(); + assertThat(response.totalCount()).isZero(); + } + } +} diff --git a/modules/study-api/src/main/java/kr/spot/ports/StudyParticipationInfoPort.java b/modules/study-api/src/main/java/kr/spot/ports/StudyParticipationInfoPort.java new file mode 100644 index 0000000..e0e6673 --- /dev/null +++ b/modules/study-api/src/main/java/kr/spot/ports/StudyParticipationInfoPort.java @@ -0,0 +1,8 @@ +package kr.spot.ports; + +import kr.spot.ports.dto.StudyParticipationInfo; + +public interface StudyParticipationInfoPort { + + StudyParticipationInfo getStudyParticipationInfo(long memberId); +} diff --git a/modules/study-api/src/main/java/kr/spot/ports/dto/StudyParticipationInfo.java b/modules/study-api/src/main/java/kr/spot/ports/dto/StudyParticipationInfo.java new file mode 100644 index 0000000..f810fcf --- /dev/null +++ b/modules/study-api/src/main/java/kr/spot/ports/dto/StudyParticipationInfo.java @@ -0,0 +1,21 @@ +package kr.spot.ports.dto; + +public record StudyParticipationInfo( + long participatingStudyCount, + long recruitingStudyCount, + long appliedStudyCount +) { + + public static StudyParticipationInfo from( + long participatingStudyCount, + long recruitingStudyCount, + long appliedStudyCount + ) { + return new StudyParticipationInfo( + participatingStudyCount, + recruitingStudyCount, + appliedStudyCount + ); + } + +} \ No newline at end of file diff --git a/modules/study/src/main/java/kr/spot/review/presentation/command/ReviewCommandController.java b/modules/study/src/main/java/kr/spot/review/presentation/command/ReviewCommandController.java index 2ddbbd3..b314b71 100644 --- a/modules/study/src/main/java/kr/spot/review/presentation/command/ReviewCommandController.java +++ b/modules/study/src/main/java/kr/spot/review/presentation/command/ReviewCommandController.java @@ -1,5 +1,6 @@ package kr.spot.review.presentation.command; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import kr.spot.ApiResponse; @@ -29,6 +30,7 @@ public class ReviewCommandController { private final ManageReviewService manageReviewService; private final ManageReviewReactionService manageReviewReactionService; + @Operation(summary = "스터디 회고록 작성", description = "특정 스터디에 대한 회고록을 작성합니다.") @PostMapping public ResponseEntity> createReview( @PathVariable Long studyId, @@ -40,6 +42,7 @@ public ResponseEntity> createReview( return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._CREATED)); } + @Operation(summary = "스터디 회고록 삭제", description = "특정 스터디에 대한 회고록을 삭제합니다.") @DeleteMapping("/{reviewId}") public ResponseEntity> deleteReview( @PathVariable Long studyId, @@ -50,6 +53,7 @@ public ResponseEntity> deleteReview( return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._NO_CONTENT)); } + @Operation(summary = "스터디 회고록 반응 추가", description = "특정 스터디 회고록에 대한 반응을 추가합니다.") @PostMapping("/{reviewId}/reactions") public ResponseEntity> addReaction( @PathVariable Long studyId, @@ -61,6 +65,7 @@ public ResponseEntity> addReaction( return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._CREATED)); } + @Operation(summary = "스터디 회고록 반응 제거", description = "특정 스터디 회고록에 대한 반응을 제거합니다.") @DeleteMapping("/{reviewId}/reactions") public ResponseEntity> removeReaction( @PathVariable Long studyId, diff --git a/modules/study/src/main/java/kr/spot/review/presentation/query/ReviewQueryController.java b/modules/study/src/main/java/kr/spot/review/presentation/query/ReviewQueryController.java index b544186..acdf0e7 100644 --- a/modules/study/src/main/java/kr/spot/review/presentation/query/ReviewQueryController.java +++ b/modules/study/src/main/java/kr/spot/review/presentation/query/ReviewQueryController.java @@ -1,5 +1,6 @@ package kr.spot.review.presentation.query; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.constraints.Max; @@ -25,6 +26,8 @@ public class ReviewQueryController { private final GetReviewService getReviewService; + + @Operation(summary = "스터디 회고록 목록 조회", description = "특정 스터디의 회고록 목록을 조회합니다. 커서 기반 페이지네이션을 지원합니다.") @GetMapping public ResponseEntity> getReviewList( @PathVariable Long studyId, @@ -32,7 +35,8 @@ public ResponseEntity> getReviewList( @RequestParam(required = false) Long cursor, @RequestParam(defaultValue = "10") @Min(1) @Max(50) Integer size ) { - GetReviewListResponse response = getReviewService.getReviewList(studyId, memberId, cursor, size); + GetReviewListResponse response = getReviewService.getReviewList(studyId, memberId, cursor, + size); return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response)); } } diff --git a/modules/study/src/main/java/kr/spot/study/application/ports/GetStudyParticipationInfoService.java b/modules/study/src/main/java/kr/spot/study/application/ports/GetStudyParticipationInfoService.java new file mode 100644 index 0000000..2d7fc13 --- /dev/null +++ b/modules/study/src/main/java/kr/spot/study/application/ports/GetStudyParticipationInfoService.java @@ -0,0 +1,45 @@ +package kr.spot.study.application.ports; + +import java.util.List; +import kr.spot.ports.StudyParticipationInfoPort; +import kr.spot.ports.dto.StudyParticipationInfo; +import kr.spot.study.domain.enums.RecruitingStatus; +import kr.spot.study.domain.enums.StudyMemberStatus; +import kr.spot.study.infrastructure.jpa.StudyRepository; +import kr.spot.study.infrastructure.jpa.associations.StudyMemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class GetStudyParticipationInfoService implements StudyParticipationInfoPort { + + private final StudyMemberRepository studyMemberRepository; + private final StudyRepository studyRepository; + + @Override + public StudyParticipationInfo getStudyParticipationInfo(long memberId) { + long participatingStudyCount = studyMemberRepository.countByMemberIdAndStudyMemberStatusIn( + memberId, + List.of(StudyMemberStatus.OWNER, StudyMemberStatus.APPROVED) + ); + + long recruitingStudyCount = studyRepository.countByLeaderIdAndRecruitingStatus( + memberId, + RecruitingStatus.RECRUITING + ); + + long appliedStudyCount = studyMemberRepository.countByMemberIdAndStudyMemberStatus( + memberId, + StudyMemberStatus.APPLIED + ); + + return StudyParticipationInfo.from( + participatingStudyCount, + recruitingStudyCount, + appliedStudyCount + ); + } +} diff --git a/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/StudyRepository.java b/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/StudyRepository.java index ec9bbb0..8e449bf 100644 --- a/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/StudyRepository.java +++ b/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/StudyRepository.java @@ -3,6 +3,7 @@ import kr.spot.code.status.ErrorStatus; import kr.spot.exception.GeneralException; import kr.spot.study.domain.Study; +import kr.spot.study.domain.enums.RecruitingStatus; import org.springframework.data.jpa.repository.JpaRepository; public interface StudyRepository extends JpaRepository { @@ -11,4 +12,6 @@ default Study getStudyById(Long studyId) { return findById(studyId) .orElseThrow(() -> new GeneralException(ErrorStatus._STUDY_NOT_FOUND)); } + + long countByLeaderIdAndRecruitingStatus(long leaderId, RecruitingStatus recruitingStatus); } diff --git a/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/associations/StudyMemberRepository.java b/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/associations/StudyMemberRepository.java index 15f826e..df1985f 100644 --- a/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/associations/StudyMemberRepository.java +++ b/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/associations/StudyMemberRepository.java @@ -20,4 +20,9 @@ boolean existsByStudyIdAndMemberIdAndStudyMemberStatusIn(Long studyId, Long memb List findAllByStudyIdAndStudyMemberStatusIn(Long studyId, List studyMemberStatuses); + long countByMemberIdAndStudyMemberStatusIn(long memberId, + List studyMemberStatuses); + + long countByMemberIdAndStudyMemberStatus(long memberId, StudyMemberStatus studyMemberStatus); + } diff --git a/modules/study/src/test/java/kr/spot/study/application/ports/GetStudyParticipationInfoServiceTest.java b/modules/study/src/test/java/kr/spot/study/application/ports/GetStudyParticipationInfoServiceTest.java new file mode 100644 index 0000000..73f3367 --- /dev/null +++ b/modules/study/src/test/java/kr/spot/study/application/ports/GetStudyParticipationInfoServiceTest.java @@ -0,0 +1,183 @@ +package kr.spot.study.application.ports; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +import java.util.List; +import kr.spot.ports.dto.StudyParticipationInfo; +import kr.spot.study.domain.enums.RecruitingStatus; +import kr.spot.study.domain.enums.StudyMemberStatus; +import kr.spot.study.infrastructure.jpa.StudyRepository; +import kr.spot.study.infrastructure.jpa.associations.StudyMemberRepository; +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; + +@ExtendWith(MockitoExtension.class) +@DisplayName("GetStudyParticipationInfoService 단위 테스트") +class GetStudyParticipationInfoServiceTest { + + @Mock + StudyMemberRepository studyMemberRepository; + + @Mock + StudyRepository studyRepository; + + @InjectMocks + GetStudyParticipationInfoService sut; + + @Nested + @DisplayName("getStudyParticipationInfo 메서드는") + class GetStudyParticipationInfoTest { + + private final long memberId = 1L; + + @Test + @DisplayName("회원의 스터디 참여 정보를 정상적으로 조회한다") + void should_return_study_participation_info() { + // given + long expectedParticipatingCount = 3L; + long expectedRecruitingCount = 1L; + long expectedAppliedCount = 2L; + + given(studyMemberRepository.countByMemberIdAndStudyMemberStatusIn( + memberId, + List.of(StudyMemberStatus.OWNER, StudyMemberStatus.APPROVED) + )).willReturn(expectedParticipatingCount); + + given(studyRepository.countByLeaderIdAndRecruitingStatus( + memberId, + RecruitingStatus.RECRUITING + )).willReturn(expectedRecruitingCount); + + given(studyMemberRepository.countByMemberIdAndStudyMemberStatus( + memberId, + StudyMemberStatus.APPLIED + )).willReturn(expectedAppliedCount); + + // when + StudyParticipationInfo result = sut.getStudyParticipationInfo(memberId); + + // then + assertThat(result.participatingStudyCount()).isEqualTo(expectedParticipatingCount); + assertThat(result.recruitingStudyCount()).isEqualTo(expectedRecruitingCount); + assertThat(result.appliedStudyCount()).isEqualTo(expectedAppliedCount); + } + + @Test + @DisplayName("참여 중인 스터디가 없으면 0을 반환한다") + void should_return_zero_when_no_participating_studies() { + // given + given(studyMemberRepository.countByMemberIdAndStudyMemberStatusIn( + memberId, + List.of(StudyMemberStatus.OWNER, StudyMemberStatus.APPROVED) + )).willReturn(0L); + + given(studyRepository.countByLeaderIdAndRecruitingStatus( + memberId, + RecruitingStatus.RECRUITING + )).willReturn(0L); + + given(studyMemberRepository.countByMemberIdAndStudyMemberStatus( + memberId, + StudyMemberStatus.APPLIED + )).willReturn(0L); + + // when + StudyParticipationInfo result = sut.getStudyParticipationInfo(memberId); + + // then + assertThat(result.participatingStudyCount()).isZero(); + assertThat(result.recruitingStudyCount()).isZero(); + assertThat(result.appliedStudyCount()).isZero(); + } + + @Test + @DisplayName("OWNER와 APPROVED 상태의 스터디만 참여 중으로 카운트한다") + void should_count_only_owner_and_approved_as_participating() { + // given + long participatingCount = 5L; + + given(studyMemberRepository.countByMemberIdAndStudyMemberStatusIn( + memberId, + List.of(StudyMemberStatus.OWNER, StudyMemberStatus.APPROVED) + )).willReturn(participatingCount); + + given(studyRepository.countByLeaderIdAndRecruitingStatus( + memberId, + RecruitingStatus.RECRUITING + )).willReturn(0L); + + given(studyMemberRepository.countByMemberIdAndStudyMemberStatus( + memberId, + StudyMemberStatus.APPLIED + )).willReturn(0L); + + // when + StudyParticipationInfo result = sut.getStudyParticipationInfo(memberId); + + // then + assertThat(result.participatingStudyCount()).isEqualTo(participatingCount); + } + + @Test + @DisplayName("RECRUITING 상태의 스터디만 모집 중으로 카운트한다") + void should_count_only_recruiting_status_as_recruiting() { + // given + long recruitingCount = 2L; + + given(studyMemberRepository.countByMemberIdAndStudyMemberStatusIn( + memberId, + List.of(StudyMemberStatus.OWNER, StudyMemberStatus.APPROVED) + )).willReturn(0L); + + given(studyRepository.countByLeaderIdAndRecruitingStatus( + memberId, + RecruitingStatus.RECRUITING + )).willReturn(recruitingCount); + + given(studyMemberRepository.countByMemberIdAndStudyMemberStatus( + memberId, + StudyMemberStatus.APPLIED + )).willReturn(0L); + + // when + StudyParticipationInfo result = sut.getStudyParticipationInfo(memberId); + + // then + assertThat(result.recruitingStudyCount()).isEqualTo(recruitingCount); + } + + @Test + @DisplayName("APPLIED 상태의 스터디만 신청 중으로 카운트한다") + void should_count_only_applied_status_as_applied() { + // given + long appliedCount = 4L; + + given(studyMemberRepository.countByMemberIdAndStudyMemberStatusIn( + memberId, + List.of(StudyMemberStatus.OWNER, StudyMemberStatus.APPROVED) + )).willReturn(0L); + + given(studyRepository.countByLeaderIdAndRecruitingStatus( + memberId, + RecruitingStatus.RECRUITING + )).willReturn(0L); + + given(studyMemberRepository.countByMemberIdAndStudyMemberStatus( + memberId, + StudyMemberStatus.APPLIED + )).willReturn(appliedCount); + + // when + StudyParticipationInfo result = sut.getStudyParticipationInfo(memberId); + + // then + assertThat(result.appliedStudyCount()).isEqualTo(appliedCount); + } + } +}