From e0fa100d3129ba4cf380efbb859fe50e8e227ed5 Mon Sep 17 00:00:00 2001 From: msk226 Date: Wed, 31 Dec 2025 14:52:24 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[Feature]=20=ED=9A=8C=EC=9B=90=20=EB=A9=94?= =?UTF-8?q?=ED=83=80=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20API?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20-=20=EB=82=B4=EA=B0=80=20=EC=84=A0?= =?UTF-8?q?=ED=98=B8=ED=95=98=EB=8A=94=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC,=20=EC=A7=80=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20-=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../query/GetMemberInfoService.java | 56 +++++++++++++++++++ .../query/MemberQueryController.java | 25 +++++++++ .../dto/response/GetMemberInfoResponse.java | 52 +++++++++++++++++ .../GetMemberPreferCategoryResponse.java | 14 +++++ .../GetMemberPreferRegionResponse.java | 14 +++++ .../ports/StudyParticipationInfoPort.java | 8 +++ .../ports/dto/StudyParticipationInfo.java | 21 +++++++ .../GetStudyParticipationInfoService.java | 45 +++++++++++++++ .../infrastructure/jpa/StudyRepository.java | 3 + .../associations/StudyMemberRepository.java | 5 ++ 10 files changed, 243 insertions(+) create mode 100644 modules/member/src/main/java/kr/spot/presentation/query/dto/response/GetMemberInfoResponse.java create mode 100644 modules/member/src/main/java/kr/spot/presentation/query/dto/response/GetMemberPreferCategoryResponse.java create mode 100644 modules/member/src/main/java/kr/spot/presentation/query/dto/response/GetMemberPreferRegionResponse.java create mode 100644 modules/study-api/src/main/java/kr/spot/ports/StudyParticipationInfoPort.java create mode 100644 modules/study-api/src/main/java/kr/spot/ports/dto/StudyParticipationInfo.java create mode 100644 modules/study/src/main/java/kr/spot/study/application/ports/GetStudyParticipationInfoService.java 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..6b8266e 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,26 @@ public ResponseEntity> getMemberName( GetMemberNameResponse response = getMemberInfoService.getMemberName(memberId); return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response)); } + + @GetMapping("/info") + public ResponseEntity> getMemberInfo( + @CurrentMember @Parameter(hidden = true) Long memberId) { + GetMemberInfoResponse response = getMemberInfoService.getMemberInfo(memberId); + return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response)); + } + + @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)); + } + + @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/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/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); + } From 80fe54ec2d8fe28f96cc45719ffbc49a8efec87b Mon Sep 17 00:00:00 2001 From: msk226 Date: Wed, 31 Dec 2025 14:52:33 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[Feature]=20=ED=9A=8C=EC=9B=90=20=EB=A9=94?= =?UTF-8?q?=ED=83=80=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20API?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../query/GetMemberInfoServiceTest.java | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 modules/member/src/test/java/kr/spot/application/query/GetMemberInfoServiceTest.java 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(); + } + } +} From 0befc23c73a0c0a4e4467c405a8fd17dd21d917e Mon Sep 17 00:00:00 2001 From: msk226 Date: Wed, 31 Dec 2025 14:54:23 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[Feature]=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=B9=B4=EC=9A=B4=ED=8A=B8=20=ED=8F=AC=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=EC=B2=B4=20=EA=B2=80=EC=A6=9D=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetStudyParticipationInfoServiceTest.java | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 modules/study/src/test/java/kr/spot/study/application/ports/GetStudyParticipationInfoServiceTest.java 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); + } + } +} From c27e64db8da3b9e8645181040b45b97ec606de07 Mon Sep 17 00:00:00 2001 From: msk226 Date: Wed, 31 Dec 2025 14:57:41 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[Feature]=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/spot/presentation/query/MemberQueryController.java | 3 +++ 1 file changed, 3 insertions(+) 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 6b8266e..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 @@ -41,6 +41,7 @@ public ResponseEntity> getMemberName( return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response)); } + @Operation(summary = "[마이페이지] 회원 정보 조회", description = "현재 로그인한 회원의 정보를 조회합니다.") @GetMapping("/info") public ResponseEntity> getMemberInfo( @CurrentMember @Parameter(hidden = true) Long memberId) { @@ -48,6 +49,7 @@ public ResponseEntity> getMemberInfo( return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response)); } + @Operation(summary = "회원 선호 카테고리 조회", description = "현재 로그인한 회원의 선호 카테고리를 조회합니다.") @GetMapping("/prefer-categories") public ResponseEntity> getMemberPreferCategories( @CurrentMember @Parameter(hidden = true) Long memberId) { @@ -56,6 +58,7 @@ public ResponseEntity> getMemberPre return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response)); } + @Operation(summary = "회원 선호 지역 조회", description = "현재 로그인한 회원의 선호 지역을 조회합니다.") @GetMapping("/prefer-regions") public ResponseEntity> getMemberPreferRegions( @CurrentMember @Parameter(hidden = true) Long memberId) { From af1eb8b2da227df3fc892bc0ae71ffb591fa915b Mon Sep 17 00:00:00 2001 From: msk226 Date: Wed, 31 Dec 2025 14:58:42 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[Feature]=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EB=AA=85=EC=84=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/command/ReviewCommandController.java | 5 +++++ .../review/presentation/query/ReviewQueryController.java | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) 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)); } }