From f398ce505f5bcdd738dbf152aa3addfbb9938714 Mon Sep 17 00:00:00 2001 From: msk226 Date: Wed, 31 Dec 2025 23:38:41 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[Feature]=20=EC=B6=94=EC=B2=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../query/GetMyStudyInfoService.java | 47 ++++++++ .../jpa/querydsl/StudyQueryRepository.java | 42 ++++++++ .../query/StudySearchController.java | 8 +- .../query/GetMyStudyInfoServiceTest.java | 101 ++++++++++++++++++ 4 files changed, 195 insertions(+), 3 deletions(-) diff --git a/modules/study/src/main/java/kr/spot/study/application/query/GetMyStudyInfoService.java b/modules/study/src/main/java/kr/spot/study/application/query/GetMyStudyInfoService.java index 358c796..e024e83 100644 --- a/modules/study/src/main/java/kr/spot/study/application/query/GetMyStudyInfoService.java +++ b/modules/study/src/main/java/kr/spot/study/application/query/GetMyStudyInfoService.java @@ -1,5 +1,7 @@ package kr.spot.study.application.query; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import kr.spot.ports.GetPreferredCategoryPort; import kr.spot.ports.GetPreferredRegionPort; @@ -116,6 +118,51 @@ public GetStudyOverviewResponse getLikedStudies(long memberId, Long cursor, int return toCursorPage(rows, pageSize, totalElements); } + public GetStudyOverviewResponse getRecommendedStudies(long memberId) { + final int recommendCount = 3; + + List preferredCategories = getPreferredCategories(memberId); + List result = pickRandomStudiesFromPreferred(preferredCategories, recommendCount); + fillWithPopularStudies(result, recommendCount); + + return StudyDTOMapper.toDTO(result, false, null, (long) result.size()); + } + + private List getPreferredCategories(long memberId) { + return getPreferredCategoryPort.get(memberId) + .stream() + .map(Category::fromString) + .toList(); + } + + private List pickRandomStudiesFromPreferred(List categories, int count) { + if (categories.isEmpty()) { + return new ArrayList<>(); + } + + final int fetchLimit = 20; + List candidates = studyQueryRepository.findRecruitingStudiesByCategories( + categories, fetchLimit); + + List shuffled = new ArrayList<>(candidates); + Collections.shuffle(shuffled); + + return new ArrayList<>(shuffled.stream().limit(count).toList()); + } + + private void fillWithPopularStudies(List result, int targetCount) { + if (result.size() >= targetCount) { + return; + } + + List excludeIds = result.stream().map(Study::getId).toList(); + int remaining = targetCount - result.size(); + + List popularStudies = studyQueryRepository.findPopularRecruitingStudies( + excludeIds, remaining); + result.addAll(popularStudies); + } + private GetStudyOverviewResponse toCursorPage(List rows, int pageSize, Long totalElements) { boolean hasNext = rows.size() > pageSize; diff --git a/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/querydsl/StudyQueryRepository.java b/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/querydsl/StudyQueryRepository.java index cda33d5..f6aa851 100644 --- a/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/querydsl/StudyQueryRepository.java +++ b/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/querydsl/StudyQueryRepository.java @@ -236,4 +236,46 @@ public long countLikedStudies(long memberId) { .where(studyLike.memberId.eq(memberId)) .fetchOne(); } + + public List findRecruitingStudiesByCategories(List categories, int limit) { + QStudy study = QStudy.study; + QStudyCategory studyCategory = QStudyCategory.studyCategory; + + return query + .select(study) + .from(study) + .join(studyCategory).on(studyCategory.studyId.eq(study.id)) + .where( + studyCategory.category.in(categories), + study.recruitingStatus.eq(RecruitingStatus.RECRUITING) + ) + .groupBy(study.id) + .orderBy(study.id.desc()) + .limit(limit) + .fetch(); + } + + public List findPopularRecruitingStudies(List excludeIds, int limit) { + QStudy study = QStudy.study; + QStudyStats studyStats = QStudyStats.studyStats; + + return query + .select(study) + .from(study) + .leftJoin(studyStats).on(studyStats.studyId.eq(study.id)) + .where( + study.recruitingStatus.eq(RecruitingStatus.RECRUITING), + notInIds(excludeIds, study) + ) + .orderBy(studyStats.likeCount.desc(), study.id.desc()) + .limit(limit) + .fetch(); + } + + private BooleanExpression notInIds(List excludeIds, QStudy study) { + if (excludeIds == null || excludeIds.isEmpty()) { + return null; + } + return study.id.notIn(excludeIds); + } } diff --git a/modules/study/src/main/java/kr/spot/study/presentation/query/StudySearchController.java b/modules/study/src/main/java/kr/spot/study/presentation/query/StudySearchController.java index d1e5d89..d9a837b 100644 --- a/modules/study/src/main/java/kr/spot/study/presentation/query/StudySearchController.java +++ b/modules/study/src/main/java/kr/spot/study/presentation/query/StudySearchController.java @@ -55,12 +55,14 @@ public ResponseEntity> getHotStudies( return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, null)); } - @Operation(summary = "전공/진로학습 이건 어때요? 스터디 조회 - 구현 중", - description = "전공/진로학습 이건 어때요? 스터디를 조회합니다. (page, size 파라미터로 페이징 처리 가능)") + @Operation(summary = "전공/진로학습 이건 어때요? 스터디 조회", + description = "회원의 선호 카테고리에 해당하는 스터디 중 랜덤하게 3개를 조회합니다. 매 요청마다 결과가 달라질 수 있습니다.") @GetMapping("/recommended") public ResponseEntity> getRecommendedStudies( + @CurrentMember @Parameter(hidden = true) long memberId ) { - return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, null)); + return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, + getMyStudyInfoService.getRecommendedStudies(memberId))); } // 필터링 O diff --git a/modules/study/src/test/java/kr/spot/study/application/query/GetMyStudyInfoServiceTest.java b/modules/study/src/test/java/kr/spot/study/application/query/GetMyStudyInfoServiceTest.java index 3c8ea97..908f3ef 100644 --- a/modules/study/src/test/java/kr/spot/study/application/query/GetMyStudyInfoServiceTest.java +++ b/modules/study/src/test/java/kr/spot/study/application/query/GetMyStudyInfoServiceTest.java @@ -6,6 +6,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import java.util.Collections; @@ -306,4 +307,104 @@ void should_return_empty_when_no_preferred_regions() { assertThat(response.nextCursor()).isNull(); } } + + @Nested + @DisplayName("추천 스터디 조회 (getRecommendedStudies)") + class GetRecommendedStudies { + + private final long memberId = 1L; + + @Test + @DisplayName("선호 카테고리 스터디가 3개 이상이면, 인기 스터디를 조회하지 않는다") + void should_not_fetch_popular_when_enough_preferred_studies() { + // given + List preferredCategoryNames = List.of("SELF_STUDY", "CAREER"); + List preferredCategories = List.of(Category.SELF_STUDY, Category.CAREER); + List candidates = createStudies(10); + + given(getPreferredCategoryPort.get(memberId)).willReturn(preferredCategoryNames); + given(studyQueryRepository.findRecruitingStudiesByCategories(preferredCategories, 20)) + .willReturn(candidates); + + // when + GetStudyOverviewResponse response = getMyStudyInfoService.getRecommendedStudies(memberId); + + // then + verify(getPreferredCategoryPort).get(memberId); + verify(studyQueryRepository).findRecruitingStudiesByCategories(preferredCategories, 20); + verify(studyQueryRepository, never()).findPopularRecruitingStudies(any(), anyInt()); + assertThat(response.content()).hasSize(3); + assertThat(response.totalElements()).isEqualTo(3L); + } + + @Test + @DisplayName("선호 카테고리 스터디가 3개 미만이면, 인기 스터디로 부족한 만큼 채운다") + void should_fill_with_popular_studies_when_not_enough_preferred() { + // given + List preferredCategoryNames = List.of("SELF_STUDY"); + List preferredCategories = List.of(Category.SELF_STUDY); + List preferredStudies = createStudies(1); + List popularStudies = createStudies(2); + + given(getPreferredCategoryPort.get(memberId)).willReturn(preferredCategoryNames); + given(studyQueryRepository.findRecruitingStudiesByCategories(preferredCategories, 20)) + .willReturn(preferredStudies); + given(studyQueryRepository.findPopularRecruitingStudies( + List.of(preferredStudies.getFirst().getId()), 2)) + .willReturn(popularStudies); + + // when + GetStudyOverviewResponse response = getMyStudyInfoService.getRecommendedStudies(memberId); + + // then + verify(studyQueryRepository).findRecruitingStudiesByCategories(preferredCategories, 20); + verify(studyQueryRepository).findPopularRecruitingStudies( + List.of(preferredStudies.getFirst().getId()), 2); + assertThat(response.content()).hasSize(3); + assertThat(response.totalElements()).isEqualTo(3L); + } + + @Test + @DisplayName("선호 카테고리가 없으면, 인기 스터디 3개를 반환한다") + void should_return_popular_studies_when_no_preferred_categories() { + // given + List popularStudies = createStudies(3); + + given(getPreferredCategoryPort.get(memberId)).willReturn(Collections.emptyList()); + given(studyQueryRepository.findPopularRecruitingStudies(Collections.emptyList(), 3)) + .willReturn(popularStudies); + + // when + GetStudyOverviewResponse response = getMyStudyInfoService.getRecommendedStudies(memberId); + + // then + verify(getPreferredCategoryPort).get(memberId); + verify(studyQueryRepository).findPopularRecruitingStudies(Collections.emptyList(), 3); + assertThat(response.content()).hasSize(3); + assertThat(response.totalElements()).isEqualTo(3L); + } + + @Test + @DisplayName("선호 카테고리 스터디와 인기 스터디 모두 없으면, 빈 결과를 반환한다") + void should_return_empty_when_no_studies_at_all() { + // given + List preferredCategoryNames = List.of("SELF_STUDY"); + List preferredCategories = List.of(Category.SELF_STUDY); + + given(getPreferredCategoryPort.get(memberId)).willReturn(preferredCategoryNames); + given(studyQueryRepository.findRecruitingStudiesByCategories(preferredCategories, 20)) + .willReturn(Collections.emptyList()); + given(studyQueryRepository.findPopularRecruitingStudies(Collections.emptyList(), 3)) + .willReturn(Collections.emptyList()); + + // when + GetStudyOverviewResponse response = getMyStudyInfoService.getRecommendedStudies(memberId); + + // then + verify(studyQueryRepository).findRecruitingStudiesByCategories(preferredCategories, 20); + verify(studyQueryRepository).findPopularRecruitingStudies(Collections.emptyList(), 3); + assertThat(response.content()).isEmpty(); + assertThat(response.totalElements()).isEqualTo(0L); + } + } } From d22f0f2e00a45140eac8f32635a21bb1164dc794 Mon Sep 17 00:00:00 2001 From: msk226 Date: Wed, 31 Dec 2025 23:54:14 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[Feature]=20=EB=AA=A8=EC=A7=91=20=EC=A4=91,?= =?UTF-8?q?=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EB=B3=84=20=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../query/GetMyStudyInfoService.java | 84 +++++++++-- .../main/java/kr/spot/study/domain/Study.java | 12 +- .../jpa/querydsl/StudyQueryRepository.java | 130 +++++++++++++++++- .../query/StudySearchController.java | 55 +++++++- .../query/GetMyStudyInfoServiceTest.java | 50 +++---- 5 files changed, 286 insertions(+), 45 deletions(-) diff --git a/modules/study/src/main/java/kr/spot/study/application/query/GetMyStudyInfoService.java b/modules/study/src/main/java/kr/spot/study/application/query/GetMyStudyInfoService.java index e024e83..28664f1 100644 --- a/modules/study/src/main/java/kr/spot/study/application/query/GetMyStudyInfoService.java +++ b/modules/study/src/main/java/kr/spot/study/application/query/GetMyStudyInfoService.java @@ -51,62 +51,128 @@ public GetStudyOverviewResponse getMyStudyOverview( } public GetStudyOverviewResponse getMyPreferredRegionStudies( - Long viewerId, + long viewerId, RecruitingStatus recruitingStatus, FeeCategory feeCategory, List categories, + Boolean isOnline, SortBy sortBy, Long cursor, - Integer size, + int size, List regionCodes ) { final int pageSize = Math.min(size, MAX_PAGE_SIZE); List preferredRegionCodes = getPreferredRegionPort.get(viewerId); + List filteredRegionCodes = filterPreferredRegionCodes(regionCodes, preferredRegionCodes); List rows = studyQueryRepository.findMyPreferredRegionStudies( recruitingStatus, feeCategory, categories, + isOnline, sortBy, cursor, pageSize + 1, - filterPreferredRegionCodes(regionCodes, preferredRegionCodes) + filteredRegionCodes ); long totalElements = studyQueryRepository.countMyPreferredRegionStudies( recruitingStatus, feeCategory, categories, - filterPreferredRegionCodes(regionCodes, preferredRegionCodes) + isOnline, + filteredRegionCodes ); return toCursorPage(rows, pageSize, totalElements); } public GetStudyOverviewResponse getMyPreferredCategoryStudies( - Long viewerId, + long viewerId, Category category, RecruitingStatus recruitingStatus, FeeCategory feeCategory, + Boolean isOnline, SortBy sortBy, Long cursor, - Integer size + int size ) { final int pageSize = Math.min(size, MAX_PAGE_SIZE); - List preferredRegionCodes = getCategories(viewerId, category); + List preferredCategories = getCategories(viewerId, category); List rows = studyQueryRepository.findMyPreferredCategoryStudies( recruitingStatus, feeCategory, + isOnline, sortBy, cursor, pageSize + 1, - preferredRegionCodes + preferredCategories ); long totalElements = studyQueryRepository.countMyPreferredCategoryStudies( recruitingStatus, feeCategory, - preferredRegionCodes + isOnline, + preferredCategories + ); + return toCursorPage(rows, pageSize, totalElements); + } + + public GetStudyOverviewResponse getRecruitingStudies( + long viewerId, + FeeCategory feeCategory, + List categories, + Boolean isOnline, + SortBy sortBy, + Long cursor, + int size + ) { + final int pageSize = Math.min(size, MAX_PAGE_SIZE); + + List rows = studyQueryRepository.findRecruitingStudies( + feeCategory, + categories, + isOnline, + sortBy, + cursor, + pageSize + 1 + ); + + long totalElements = studyQueryRepository.countRecruitingStudies( + feeCategory, + categories, + isOnline + ); + return toCursorPage(rows, pageSize, totalElements); + } + + public GetStudyOverviewResponse getStudiesByCategory( + long viewerId, + RecruitingStatus recruitingStatus, + FeeCategory feeCategory, + Category category, + Boolean isOnline, + SortBy sortBy, + Long cursor, + int size + ) { + final int pageSize = Math.min(size, MAX_PAGE_SIZE); + + List rows = studyQueryRepository.findStudiesByCategory( + recruitingStatus, + feeCategory, + category, + isOnline, + sortBy, + cursor, + pageSize + 1 + ); + + long totalElements = studyQueryRepository.countStudiesByCategory( + recruitingStatus, + feeCategory, + category, + isOnline ); return toCursorPage(rows, pageSize, totalElements); } diff --git a/modules/study/src/main/java/kr/spot/study/domain/Study.java b/modules/study/src/main/java/kr/spot/study/domain/Study.java index 4c74946..f40a161 100644 --- a/modules/study/src/main/java/kr/spot/study/domain/Study.java +++ b/modules/study/src/main/java/kr/spot/study/domain/Study.java @@ -57,17 +57,25 @@ public class Study extends BaseEntity { @Enumerated(EnumType.STRING) private RecruitingStatus recruitingStatus; + @Column(nullable = false) + private Boolean isOnline = false; + public static Study of(Long id, Long leaderId, String name, Integer maxMembers, Fee fee, String description) { - return of(id, leaderId, name, maxMembers, fee, null, description); + return of(id, leaderId, name, maxMembers, fee, null, description, false); } public static Study of(Long id, Long leaderId, String name, Integer maxMembers, Fee fee, String imageUrl, String description) { + return of(id, leaderId, name, maxMembers, fee, imageUrl, description, false); + } + + public static Study of(Long id, Long leaderId, String name, Integer maxMembers, Fee fee, + String imageUrl, String description, Boolean isOnline) { validateStudyNameIsNotBlank(name); validateMaxMembers(maxMembers); return new Study(id, leaderId, name, maxMembers, CURRENT_MEMBERS, fee, imageUrl, description, - RecruitingStatus.RECRUITING); + RecruitingStatus.RECRUITING, isOnline != null && isOnline); } private static void validateStudyNameIsNotBlank(String name) { diff --git a/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/querydsl/StudyQueryRepository.java b/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/querydsl/StudyQueryRepository.java index f6aa851..85c4c7d 100644 --- a/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/querydsl/StudyQueryRepository.java +++ b/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/querydsl/StudyQueryRepository.java @@ -69,6 +69,7 @@ public List findMyPreferredRegionStudies( RecruitingStatus recruitingStatus, FeeCategory feeCategory, List categories, + Boolean isOnline, SortBy sortBy, Long cursor, int limit, @@ -90,6 +91,7 @@ public List findMyPreferredRegionStudies( eqRecruitingStatus(recruitingStatus, study), eqFeeCategory(feeCategory, study), inCategories(categories, studyCategory), + eqIsOnline(isOnline, study), ltCursor(cursor, study) ) .groupBy(study.id) @@ -105,6 +107,7 @@ public long countMyPreferredRegionStudies( RecruitingStatus recruitingStatus, FeeCategory feeCategory, List categories, + Boolean isOnline, List regionCodes ) { QStudy study = QStudy.study; @@ -120,7 +123,8 @@ public long countMyPreferredRegionStudies( studyRegion.regionCode.in(regionCodes), eqRecruitingStatus(recruitingStatus, study), eqFeeCategory(feeCategory, study), - inCategories(categories, studyCategory) + inCategories(categories, studyCategory), + eqIsOnline(isOnline, study) ) .fetchOne(); } @@ -128,6 +132,7 @@ public long countMyPreferredRegionStudies( public List findMyPreferredCategoryStudies( RecruitingStatus recruitingStatus, FeeCategory feeCategory, + Boolean isOnline, SortBy sortBy, Long cursor, int limit, @@ -146,6 +151,7 @@ public List findMyPreferredCategoryStudies( eqRecruitingStatus(recruitingStatus, study), eqFeeCategory(feeCategory, study), inCategories(categories, studyCategory), + eqIsOnline(isOnline, study), ltCursor(cursor, study) ) .groupBy(study.id) @@ -160,6 +166,7 @@ public List findMyPreferredCategoryStudies( public long countMyPreferredCategoryStudies( RecruitingStatus recruitingStatus, FeeCategory feeCategory, + Boolean isOnline, List categories ) { QStudy study = QStudy.study; @@ -172,7 +179,118 @@ public long countMyPreferredCategoryStudies( .where( eqRecruitingStatus(recruitingStatus, study), eqFeeCategory(feeCategory, study), - inCategories(categories, studyCategory) + inCategories(categories, studyCategory), + eqIsOnline(isOnline, study) + ) + .fetchOne(); + } + + public List findRecruitingStudies( + FeeCategory feeCategory, + List categories, + Boolean isOnline, + SortBy sortBy, + Long cursor, + int limit + ) { + QStudy study = QStudy.study; + QStudyCategory studyCategory = QStudyCategory.studyCategory; + QStudyStats studyStats = QStudyStats.studyStats; + + return query + .select(study) + .from(study) + .leftJoin(studyCategory).on(studyCategory.studyId.eq(study.id)) + .leftJoin(studyStats).on(studyStats.studyId.eq(study.id)) + .where( + study.recruitingStatus.eq(RecruitingStatus.RECRUITING), + eqFeeCategory(feeCategory, study), + inCategories(categories, studyCategory), + eqIsOnline(isOnline, study), + ltCursor(cursor, study) + ) + .groupBy(study.id) + .orderBy( + orderBy(sortBy, study, studyStats), + study.id.desc() + ) + .limit(limit) + .fetch(); + } + + public long countRecruitingStudies( + FeeCategory feeCategory, + List categories, + Boolean isOnline + ) { + QStudy study = QStudy.study; + QStudyCategory studyCategory = QStudyCategory.studyCategory; + + return query + .select(study.id.countDistinct()) + .from(study) + .leftJoin(studyCategory).on(studyCategory.studyId.eq(study.id)) + .where( + study.recruitingStatus.eq(RecruitingStatus.RECRUITING), + eqFeeCategory(feeCategory, study), + inCategories(categories, studyCategory), + eqIsOnline(isOnline, study) + ) + .fetchOne(); + } + + public List findStudiesByCategory( + RecruitingStatus recruitingStatus, + FeeCategory feeCategory, + Category category, + Boolean isOnline, + SortBy sortBy, + Long cursor, + int limit + ) { + QStudy study = QStudy.study; + QStudyCategory studyCategory = QStudyCategory.studyCategory; + QStudyStats studyStats = QStudyStats.studyStats; + + return query + .select(study) + .from(study) + .join(studyCategory).on(studyCategory.studyId.eq(study.id)) + .leftJoin(studyStats).on(studyStats.studyId.eq(study.id)) + .where( + eqRecruitingStatus(recruitingStatus, study), + eqFeeCategory(feeCategory, study), + eqCategory(category, studyCategory), + eqIsOnline(isOnline, study), + ltCursor(cursor, study) + ) + .groupBy(study.id) + .orderBy( + orderBy(sortBy, study, studyStats), + study.id.desc() + ) + .limit(limit) + .fetch(); + } + + public long countStudiesByCategory( + RecruitingStatus recruitingStatus, + FeeCategory feeCategory, + Category category, + Boolean isOnline + ) { + QStudy study = QStudy.study; + QStudyCategory studyCategory = QStudyCategory.studyCategory; + + return query + .select(study.id.countDistinct()) + .from(study) + .join(studyCategory).on(studyCategory.studyId.eq(study.id)) + .where( + eqRecruitingStatus(recruitingStatus, study), + eqFeeCategory(feeCategory, study), + eqCategory(category, studyCategory), + eqIsOnline(isOnline, study) ) .fetchOne(); } @@ -192,6 +310,14 @@ private BooleanExpression inCategories(List categories, QStudyCategory return studyCategory.category.in(categories); } + private BooleanExpression eqCategory(Category category, QStudyCategory studyCategory) { + return category == null ? null : studyCategory.category.eq(category); + } + + private BooleanExpression eqIsOnline(Boolean isOnline, QStudy study) { + return isOnline == null ? null : study.isOnline.eq(isOnline); + } + private BooleanExpression ltCursor(Long cursor, QStudy study) { return cursor == null ? null : study.id.lt(cursor); } diff --git a/modules/study/src/main/java/kr/spot/study/presentation/query/StudySearchController.java b/modules/study/src/main/java/kr/spot/study/presentation/query/StudySearchController.java index d9a837b..4ff628e 100644 --- a/modules/study/src/main/java/kr/spot/study/presentation/query/StudySearchController.java +++ b/modules/study/src/main/java/kr/spot/study/presentation/query/StudySearchController.java @@ -75,6 +75,7 @@ public ResponseEntity> getRegionStudies( @RequestParam(required = false) RecruitingStatus recruitingStatus, @RequestParam(required = false) FeeCategory feeCategory, @RequestParam(required = false) List categories, + @RequestParam(required = false) Boolean isOnline, @RequestParam(required = false) SortBy sortBy, @RequestParam(required = false) Long cursor, @RequestParam(defaultValue = "10") @Min(1) @Max(50) Integer size, @@ -86,6 +87,7 @@ public ResponseEntity> getRegionStudies( recruitingStatus, feeCategory, categories, + isOnline, sortBy, cursor, size, @@ -102,6 +104,7 @@ public ResponseEntity> getMyPreferredCateg @RequestParam(required = false) Category category, @RequestParam(required = false) RecruitingStatus recruitingStatus, @RequestParam(required = false) FeeCategory feeCategory, + @RequestParam(required = false) Boolean isOnline, @RequestParam(required = false) SortBy sortBy, @RequestParam(required = false) Long cursor, @RequestParam(defaultValue = "10") @Min(1) @Max(50) Integer size @@ -113,6 +116,7 @@ public ResponseEntity> getMyPreferredCateg category, recruitingStatus, feeCategory, + isOnline, sortBy, cursor, size @@ -132,7 +136,54 @@ public ResponseEntity> getLikedStudies( getMyStudyInfoService.getLikedStudies(viewerId, cursor, size))); } - // TODO 모집중 스터디 - // TODO 전체 스터디 조회 + @Operation(summary = "모집 중 스터디 조회", + description = "모집 중인 스터디를 조회합니다. 카테고리, 정렬 방식 등을 필터링할 수 있습니다.") + @GetMapping("/recruiting") + public ResponseEntity> getRecruitingStudies( + @CurrentMember @Parameter(hidden = true) Long viewerId, + @RequestParam(required = false) FeeCategory feeCategory, + @RequestParam(required = false) List categories, + @RequestParam(required = false) Boolean isOnline, + @RequestParam(required = false) SortBy sortBy, + @RequestParam(required = false) Long cursor, + @RequestParam(defaultValue = "10") @Min(1) @Max(50) Integer size + ) { + return ResponseEntity.ok( + ApiResponse.onSuccess(SuccessStatus._OK, getMyStudyInfoService.getRecruitingStudies( + viewerId, + feeCategory, + categories, + isOnline, + sortBy, + cursor, + size + ))); + } + + @Operation(summary = "카테고리 별 스터디 조회", + description = "카테고리 별 스터디를 조회합니다. 모집 상태, 정렬 방식 등을 필터링할 수 있습니다.") + @GetMapping("/categories") + public ResponseEntity> getStudiesByCategory( + @CurrentMember @Parameter(hidden = true) Long viewerId, + @RequestParam(required = false) RecruitingStatus recruitingStatus, + @RequestParam(required = false) FeeCategory feeCategory, + @RequestParam(required = false) Category category, + @RequestParam(required = false) Boolean isOnline, + @RequestParam(required = false) SortBy sortBy, + @RequestParam(required = false) Long cursor, + @RequestParam(defaultValue = "10") @Min(1) @Max(50) Integer size + ) { + return ResponseEntity.ok( + ApiResponse.onSuccess(SuccessStatus._OK, getMyStudyInfoService.getStudiesByCategory( + viewerId, + recruitingStatus, + feeCategory, + category, + isOnline, + sortBy, + cursor, + size + ))); + } } diff --git a/modules/study/src/test/java/kr/spot/study/application/query/GetMyStudyInfoServiceTest.java b/modules/study/src/test/java/kr/spot/study/application/query/GetMyStudyInfoServiceTest.java index 908f3ef..a5ea8f1 100644 --- a/modules/study/src/test/java/kr/spot/study/application/query/GetMyStudyInfoServiceTest.java +++ b/modules/study/src/test/java/kr/spot/study/application/query/GetMyStudyInfoServiceTest.java @@ -145,7 +145,7 @@ void should_cap_page_size_at_max_page_size() { @DisplayName("내 관심 지역 스터디 목록 조회 (getMyPreferredRegionStudies)") class GetMyPreferredRegionStudies { - private final Long viewerId = 1L; + private final long viewerId = 1L; private final int pageSize = 10; @Test @@ -158,23 +158,20 @@ void should_find_studies_in_all_preferred_regions_when_no_filter() { given(getPreferredRegionPort.get(viewerId)).willReturn(preferredRegions); given(studyQueryRepository.findMyPreferredRegionStudies(any(), any(), any(), any(), any(), - anyInt(), - eq(preferredRegions))) + any(), anyInt(), eq(preferredRegions))) .willReturn(studies); // when GetStudyOverviewResponse response = getMyStudyInfoService.getMyPreferredRegionStudies( viewerId, RecruitingStatus.RECRUITING, FeeCategory.ABOVE_50K, Collections.emptyList(), - SortBy.HITS, - null, pageSize, Collections.emptyList() + null, SortBy.HITS, null, pageSize, Collections.emptyList() ); // then verify(getPreferredRegionPort).get(viewerId); verify(studyQueryRepository).findMyPreferredRegionStudies( eq(RecruitingStatus.RECRUITING), eq(FeeCategory.ABOVE_50K), eq(Collections.emptyList()), - eq(SortBy.HITS), - eq(null), eq(pageSize + 1), eq(preferredRegions) + eq(null), eq(SortBy.HITS), eq(null), eq(pageSize + 1), eq(preferredRegions) ); assertThat(response.content()).hasSize(pageSize); assertThat(response.hasNext()).isTrue(); @@ -192,21 +189,19 @@ void should_find_studies_in_filtered_preferred_regions() { given(getPreferredRegionPort.get(viewerId)).willReturn(preferredRegions); given(studyQueryRepository.findMyPreferredRegionStudies(any(), any(), any(), any(), any(), - anyInt(), - eq(expectedRegions))) + any(), anyInt(), eq(expectedRegions))) .willReturn(studies); // when GetStudyOverviewResponse response = getMyStudyInfoService.getMyPreferredRegionStudies( viewerId, RecruitingStatus.RECRUITING, FeeCategory.ABOVE_50K, Collections.emptyList(), - SortBy.HITS, - null, pageSize, filterRegions + null, SortBy.HITS, null, pageSize, filterRegions ); // then verify(getPreferredRegionPort).get(viewerId); verify(studyQueryRepository).findMyPreferredRegionStudies( - any(), any(), any(), any(), any(), eq(pageSize + 1), eq(expectedRegions) + any(), any(), any(), any(), any(), any(), eq(pageSize + 1), eq(expectedRegions) ); assertThat(response.content()).hasSize(5); assertThat(response.hasNext()).isFalse(); @@ -218,21 +213,19 @@ void should_return_empty_when_no_preferred_regions() { // given given(getPreferredRegionPort.get(viewerId)).willReturn(Collections.emptyList()); given(studyQueryRepository.findMyPreferredRegionStudies(any(), any(), any(), any(), any(), - anyInt(), - eq(Collections.emptyList()))) + any(), anyInt(), eq(Collections.emptyList()))) .willReturn(Collections.emptyList()); // when GetStudyOverviewResponse response = getMyStudyInfoService.getMyPreferredRegionStudies( viewerId, RecruitingStatus.RECRUITING, FeeCategory.ABOVE_50K, Collections.emptyList(), - SortBy.HITS, - null, pageSize, Collections.emptyList() + null, SortBy.HITS, null, pageSize, Collections.emptyList() ); // then verify(getPreferredRegionPort).get(viewerId); verify(studyQueryRepository).findMyPreferredRegionStudies( - any(), any(), any(), any(), any(), eq(pageSize + 1), eq(Collections.emptyList()) + any(), any(), any(), any(), any(), any(), eq(pageSize + 1), eq(Collections.emptyList()) ); assertThat(response.content()).isEmpty(); assertThat(response.hasNext()).isFalse(); @@ -244,7 +237,7 @@ void should_return_empty_when_no_preferred_regions() { @DisplayName("내 관심 카테고리 스터디 목록 조회 (getMyPreferredCategoryStudies)") class GetMyPreferredCategoryStudies { - private final Long viewerId = 1L; + private final long viewerId = 1L; private final int pageSize = 10; @Test @@ -257,23 +250,21 @@ void should_find_studies_in_all_preferred_regions_when_no_filter() { Long expectedNextCursor = studies.get(pageSize - 1).getId(); given(getPreferredCategoryPort.get(viewerId)).willReturn(preferredCategoryName); - given( - studyQueryRepository.findMyPreferredCategoryStudies(any(), any(), any(), any(), anyInt(), - eq(preferredCategory))) + given(studyQueryRepository.findMyPreferredCategoryStudies(any(), any(), any(), any(), any(), + anyInt(), eq(preferredCategory))) .willReturn(studies); // when GetStudyOverviewResponse response = getMyStudyInfoService.getMyPreferredCategoryStudies( viewerId, null, RecruitingStatus.RECRUITING, FeeCategory.ABOVE_50K, - SortBy.HITS, null, pageSize + null, SortBy.HITS, null, pageSize ); // then verify(getPreferredCategoryPort).get(viewerId); verify(studyQueryRepository).findMyPreferredCategoryStudies( - eq(RecruitingStatus.RECRUITING), eq(FeeCategory.ABOVE_50K), eq(SortBy.HITS), - eq(null), eq(pageSize + 1), - eq(List.of(Category.SELF_STUDY, Category.CAREER)) + eq(RecruitingStatus.RECRUITING), eq(FeeCategory.ABOVE_50K), eq(null), eq(SortBy.HITS), + eq(null), eq(pageSize + 1), eq(preferredCategory) ); assertThat(response.content()).hasSize(pageSize); assertThat(response.hasNext()).isTrue(); @@ -286,21 +277,20 @@ void should_find_studies_in_all_preferred_regions_when_no_filter() { void should_return_empty_when_no_preferred_regions() { // given given(getPreferredCategoryPort.get(viewerId)).willReturn(Collections.emptyList()); - given( - studyQueryRepository.findMyPreferredCategoryStudies(any(), any(), any(), any(), anyInt(), - eq(Collections.emptyList()))) + given(studyQueryRepository.findMyPreferredCategoryStudies(any(), any(), any(), any(), any(), + anyInt(), eq(Collections.emptyList()))) .willReturn(Collections.emptyList()); // when GetStudyOverviewResponse response = getMyStudyInfoService.getMyPreferredCategoryStudies( viewerId, null, RecruitingStatus.RECRUITING, FeeCategory.ABOVE_50K, - SortBy.HITS, null, pageSize + null, SortBy.HITS, null, pageSize ); // then verify(getPreferredCategoryPort).get(viewerId); verify(studyQueryRepository).findMyPreferredCategoryStudies( - any(), any(), any(), any(), eq(pageSize + 1), eq(Collections.emptyList()) + any(), any(), any(), any(), any(), eq(pageSize + 1), eq(Collections.emptyList()) ); assertThat(response.content()).isEmpty(); assertThat(response.hasNext()).isFalse(); From a2e89c1f158f7049a770ed4cbd5564510edddc7d Mon Sep 17 00:00:00 2001 From: msk226 Date: Thu, 1 Jan 2026 00:05:11 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[Feature]=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=8B=9C,=20=EC=98=A8/=EC=98=A4=ED=94=84?= =?UTF-8?q?=EB=9D=BC=EC=9D=B8=20=EC=9C=A0=EB=AC=B4=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/spot/study/application/command/CreateStudyService.java | 2 +- .../presentation/command/dto/request/CreateStudyRequest.java | 1 + .../spot/study/application/command/CreateStudyServiceTest.java | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/study/src/main/java/kr/spot/study/application/command/CreateStudyService.java b/modules/study/src/main/java/kr/spot/study/application/command/CreateStudyService.java index 7a7f03e..03970ad 100644 --- a/modules/study/src/main/java/kr/spot/study/application/command/CreateStudyService.java +++ b/modules/study/src/main/java/kr/spot/study/application/command/CreateStudyService.java @@ -40,7 +40,7 @@ public class CreateStudyService { public void createStudy(CreateStudyRequest request, Long leaderId, MultipartFile imageFile) { long studyId = idGenerator.nextId(); Study study = Study.of(studyId, leaderId, request.name(), request.maxMembers(), - Fee.of(request.hasFee(), request.amount()), request.description()); + Fee.of(request.hasFee(), request.amount()), null, request.description(), request.isOnline()); StudyStats studyStats = StudyStats.of(studyId); StudyMember studyMember = StudyMember.create(idGenerator.nextId(), studyId, leaderId); diff --git a/modules/study/src/main/java/kr/spot/study/presentation/command/dto/request/CreateStudyRequest.java b/modules/study/src/main/java/kr/spot/study/presentation/command/dto/request/CreateStudyRequest.java index 551eba5..0609485 100644 --- a/modules/study/src/main/java/kr/spot/study/presentation/command/dto/request/CreateStudyRequest.java +++ b/modules/study/src/main/java/kr/spot/study/presentation/command/dto/request/CreateStudyRequest.java @@ -10,6 +10,7 @@ public record CreateStudyRequest( Boolean hasFee, Integer amount, String description, + Boolean isOnline, Set categories, Set