diff --git a/src/main/kotlin/picklab/backend/activity/domain/repository/ActivityRepositoryCustom.kt b/src/main/kotlin/picklab/backend/activity/domain/repository/ActivityRepositoryCustom.kt index 5e3c1fa4..ff9b5705 100644 --- a/src/main/kotlin/picklab/backend/activity/domain/repository/ActivityRepositoryCustom.kt +++ b/src/main/kotlin/picklab/backend/activity/domain/repository/ActivityRepositoryCustom.kt @@ -4,6 +4,9 @@ import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import picklab.backend.activity.application.model.ActivitySearchCondition import picklab.backend.activity.application.model.ActivityView +import picklab.backend.activity.domain.enums.ActivitySortType +import picklab.backend.activity.domain.enums.RecruitmentStatus +import picklab.backend.job.domain.enums.JobGroup interface ActivityRepositoryCustom { fun getActivities( @@ -21,4 +24,15 @@ interface ActivityRepositoryCustom { keyword: String, limit: Int, ): List + + fun searchActivitiesByKeyword( + keyword: String, + activityType: String?, + status: RecruitmentStatus?, + jobGroups: List?, + sort: ActivitySortType, + pageable: PageRequest, + ): Page + + fun countActivitiesByKeywordPerType(keyword: String): Map } diff --git a/src/main/kotlin/picklab/backend/activity/domain/repository/ActivityRepositoryImpl.kt b/src/main/kotlin/picklab/backend/activity/domain/repository/ActivityRepositoryImpl.kt index b749faff..97f76688 100644 --- a/src/main/kotlin/picklab/backend/activity/domain/repository/ActivityRepositoryImpl.kt +++ b/src/main/kotlin/picklab/backend/activity/domain/repository/ActivityRepositoryImpl.kt @@ -20,8 +20,10 @@ import picklab.backend.activity.domain.enums.EducationCostType import picklab.backend.activity.domain.enums.EducationFormatType import picklab.backend.activity.domain.enums.LocationType import picklab.backend.activity.domain.enums.RecruitmentEndType +import picklab.backend.activity.domain.enums.RecruitmentStatus import picklab.backend.activity.infrastructure.QActivityItem import picklab.backend.job.domain.entity.QJobCategory +import picklab.backend.job.domain.enums.JobGroup import java.time.LocalDate @Repository @@ -80,14 +82,18 @@ class ActivityRepositoryImpl( val orderBy = when (queryData.sort) { - ActivitySortType.LATEST -> listOf(QActivity.activity.createdAt.desc()) - ActivitySortType.DEADLINE_ASC -> + ActivitySortType.LATEST -> { + listOf(QActivity.activity.createdAt.desc()) + } + + ActivitySortType.DEADLINE_ASC -> { listOf( QActivity.activity.recruitmentEndDate.asc(), QActivity.activity.createdAt.desc(), ) + } - ActivitySortType.DEADLINE_DESC -> + ActivitySortType.DEADLINE_DESC -> { listOf( Expressions .numberTemplate( @@ -98,6 +104,7 @@ class ActivityRepositoryImpl( ).desc(), QActivity.activity.createdAt.desc(), ) + } } val items = @@ -165,6 +172,150 @@ class ActivityRepositoryImpl( ).orderBy(QActivity.activity.title.asc()) .limit(limit.toLong()) .fetch() + + override fun searchActivitiesByKeyword( + keyword: String, + activityType: String?, + status: RecruitmentStatus?, + jobGroups: List?, + sort: ActivitySortType, + pageable: PageRequest, + ): Page { + val condition = + BooleanBuilder().apply { + and(QActivity.activity.deletedAt.isNull) + and( + QActivity.activity.title + .containsIgnoreCase(keyword) + .or(QActivity.activity.organizer.containsIgnoreCase(keyword)), + ) + activityType?.let { and(QActivity.activity.activityType.eq(it)) } + status?.let { and(QActivity.activity.status.eq(it)) } + jobGroups?.let { and(QJobCategory.jobCategory.jobGroup.`in`(it)) } + } + + val orderBy = + when (sort) { + ActivitySortType.LATEST -> { + listOf(QActivity.activity.createdAt.desc()) + } + + ActivitySortType.DEADLINE_ASC -> { + listOf( + QActivity.activity.recruitmentEndDate.asc(), + QActivity.activity.createdAt.desc(), + ) + } + + ActivitySortType.DEADLINE_DESC -> { + listOf( + Expressions + .numberTemplate( + Long::class.java, + "DATEDIFF({0}, {1})", + QActivity.activity.recruitmentEndDate, + LocalDate.now(), + ).desc(), + QActivity.activity.createdAt.desc(), + ) + } + } + + val activityIds = + jpaQueryFactory + .select(QActivity.activity.id) + .distinct() + .from(QActivity.activity) + .leftJoin(QActivityJobCategory.activityJobCategory) + .on( + QActivityJobCategory.activityJobCategory.activity.id + .eq(QActivity.activity.id), + ).leftJoin(QJobCategory.jobCategory) + .on( + QActivityJobCategory.activityJobCategory.jobCategory.id + .eq(QJobCategory.jobCategory.id), + ).where(condition) + .orderBy(*orderBy.toTypedArray()) + .offset(pageable.offset) + .limit(pageable.pageSize.toLong()) + .fetch() + + if (activityIds.isEmpty()) { + return PageImpl(emptyList(), pageable, 0) + } + + val itemsMap = + jpaQueryFactory + .selectFrom(QActivity.activity) + .leftJoin(QActivityJobCategory.activityJobCategory) + .on( + QActivityJobCategory.activityJobCategory.activity.id + .eq(QActivity.activity.id), + ).leftJoin(QJobCategory.jobCategory) + .on( + QActivityJobCategory.activityJobCategory.jobCategory.id + .eq(QJobCategory.jobCategory.id), + ).where(QActivity.activity.id.`in`(activityIds)) + .transform( + GroupBy.groupBy(QActivity.activity.id).`as`( + QActivityItem( + QActivity.activity.id, + QActivity.activity.title, + QActivity.activity.organizer, + QActivity.activity.organizerType.stringValue(), + QActivity.activity.startDate, + QActivity.activity.activityType, + GroupBy.list(QJobCategory.jobCategory.jobDetail.stringValue()), + QActivity.activity.activityThumbnailUrl, + QActivity.activity.viewCount, + QActivity.activity.recruitmentEndDate, + QActivity.activity.recruitmentEndType, + ), + ), + ) + + val items = activityIds.mapNotNull { itemsMap[it] } + + val count = + jpaQueryFactory + .select(QActivity.activity.id.countDistinct()) + .from(QActivity.activity) + .leftJoin(QActivityJobCategory.activityJobCategory) + .on( + QActivityJobCategory.activityJobCategory.activity.id + .eq(QActivity.activity.id), + ).leftJoin(QJobCategory.jobCategory) + .on( + QActivityJobCategory.activityJobCategory.jobCategory.id + .eq(QJobCategory.jobCategory.id), + ).where(condition) + .fetchOne() ?: 0L + + return PageImpl(items, pageable, count) + } + + override fun countActivitiesByKeywordPerType(keyword: String): Map { + val condition = + BooleanBuilder().apply { + and(QActivity.activity.deletedAt.isNull) + and( + QActivity.activity.title + .containsIgnoreCase(keyword) + .or(QActivity.activity.organizer.containsIgnoreCase(keyword)), + ) + } + + return jpaQueryFactory + .select(QActivity.activity.activityType, QActivity.activity.id.count()) + .from(QActivity.activity) + .where(condition) + .groupBy(QActivity.activity.activityType) + .fetch() + .associate { tuple -> + (tuple.get(QActivity.activity.activityType) ?: "") to + (tuple.get(QActivity.activity.id.count()) ?: 0L) + } + } } inline fun BooleanBuilder.andIfNotNullOrEmpty( diff --git a/src/main/kotlin/picklab/backend/activity/domain/service/ActivityService.kt b/src/main/kotlin/picklab/backend/activity/domain/service/ActivityService.kt index 1dd6c87d..ef56ea80 100644 --- a/src/main/kotlin/picklab/backend/activity/domain/service/ActivityService.kt +++ b/src/main/kotlin/picklab/backend/activity/domain/service/ActivityService.kt @@ -8,12 +8,14 @@ import picklab.backend.activity.application.ActivityQueryRepository import picklab.backend.activity.application.model.ActivitySearchCondition import picklab.backend.activity.application.model.ActivityView import picklab.backend.activity.domain.entity.Activity +import picklab.backend.activity.domain.enums.ActivitySortType import picklab.backend.activity.domain.enums.ActivityType import picklab.backend.activity.domain.enums.EducationFormatType import picklab.backend.activity.domain.enums.RecruitmentStatus import picklab.backend.activity.domain.repository.ActivityRepository import picklab.backend.common.model.BusinessException import picklab.backend.common.model.ErrorCode +import picklab.backend.job.domain.enums.JobGroup import java.time.LocalDate @Service @@ -201,4 +203,17 @@ class ActivityService( * 인기도는 조회수와 북마크 수를 합산하여 계산합니다. */ fun getPopularActivities(pageable: PageRequest): Page = activityQueryRepository.findPopularActivities(pageable) + + @Transactional(readOnly = true) + fun searchActivitiesByKeyword( + keyword: String, + activityType: String?, + status: RecruitmentStatus?, + jobGroups: List?, + sort: ActivitySortType, + pageable: PageRequest, + ): Page = activityRepository.searchActivitiesByKeyword(keyword, activityType, status, jobGroups, sort, pageable) + + @Transactional(readOnly = true) + fun countActivitiesByKeywordPerType(keyword: String): Map = activityRepository.countActivitiesByKeywordPerType(keyword) } diff --git a/src/main/kotlin/picklab/backend/activity/infrastructure/ActivityQueryRepositoryImpl.kt b/src/main/kotlin/picklab/backend/activity/infrastructure/ActivityQueryRepositoryImpl.kt index 19d87f1d..be0e070d 100644 --- a/src/main/kotlin/picklab/backend/activity/infrastructure/ActivityQueryRepositoryImpl.kt +++ b/src/main/kotlin/picklab/backend/activity/infrastructure/ActivityQueryRepositoryImpl.kt @@ -15,9 +15,11 @@ import picklab.backend.activity.domain.entity.QActivity import picklab.backend.activity.domain.entity.QActivityBookmark import picklab.backend.activity.domain.entity.QActivityJobCategory import picklab.backend.activity.domain.enums.ActivityBookmarkSortType +import picklab.backend.activity.domain.enums.RecruitmentEndType import picklab.backend.activity.domain.enums.RecruitmentStatus import picklab.backend.job.domain.entity.QJobCategory import picklab.backend.member.domain.entity.QMemberActivityViewHistory +import java.time.LocalDate @Repository class ActivityQueryRepositoryImpl( @@ -42,7 +44,12 @@ class ActivityQueryRepositoryImpl( .eq(QJobCategory.jobCategory.id), ).where( QActivity.activity.deletedAt.isNull - .and(QJobCategory.jobCategory.id.`in`(jobIds)), + .and(QJobCategory.jobCategory.id.`in`(jobIds)) + .and( + QActivity.activity.recruitmentEndType + .ne(RecruitmentEndType.FIXED) + .or(QActivity.activity.recruitmentEndDate.goe(LocalDate.now())), + ), ).groupBy(QActivity.activity.id) .orderBy( QActivity.activity.viewCount @@ -116,7 +123,12 @@ class ActivityQueryRepositoryImpl( .eq(QJobCategory.jobCategory.id), ).where( QActivity.activity.deletedAt.isNull - .and(QJobCategory.jobCategory.id.`in`(jobIds)), + .and(QJobCategory.jobCategory.id.`in`(jobIds)) + .and( + QActivity.activity.recruitmentEndType + .ne(RecruitmentEndType.FIXED) + .or(QActivity.activity.recruitmentEndDate.goe(LocalDate.now())), + ), ).fetchOne() ?: 0L return PageImpl(items, pageable, count) @@ -127,6 +139,11 @@ class ActivityQueryRepositoryImpl( BooleanBuilder().apply { and(QActivity.activity.status.eq(RecruitmentStatus.OPEN)) and(QActivity.activity.deletedAt.isNull) + and( + QActivity.activity.recruitmentEndType + .ne(RecruitmentEndType.FIXED) + .or(QActivity.activity.recruitmentEndDate.goe(LocalDate.now())), + ) } val orderBy = @@ -224,19 +241,27 @@ class ActivityQueryRepositoryImpl( val orderBy = when (queryData.sortType) { - ActivityBookmarkSortType.RECENTLY_BOOKMARKED -> listOf(QActivityBookmark.activityBookmark.createdAt.desc()) - ActivityBookmarkSortType.LATEST -> listOf(QActivity.activity.createdAt.desc()) - ActivityBookmarkSortType.DEADLINE_ASC -> + ActivityBookmarkSortType.RECENTLY_BOOKMARKED -> { + listOf(QActivityBookmark.activityBookmark.createdAt.desc()) + } + + ActivityBookmarkSortType.LATEST -> { + listOf(QActivity.activity.createdAt.desc()) + } + + ActivityBookmarkSortType.DEADLINE_ASC -> { listOf( QActivity.activity.recruitmentEndDate.asc(), QActivity.activity.createdAt.desc(), ) + } - ActivityBookmarkSortType.DEADLINE_DESC -> + ActivityBookmarkSortType.DEADLINE_DESC -> { listOf( QActivity.activity.recruitmentEndDate.desc(), QActivity.activity.createdAt.desc(), ) + } } val items = diff --git a/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt b/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt index 7be0435f..8157e348 100644 --- a/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt +++ b/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt @@ -58,6 +58,8 @@ enum class SuccessCode( GET_SATISFACTION_AVG_SCORES_SUCCESS(HttpStatus.OK, "활동별 만족도 통계 조회에 성공했습니다."), // Search 관련 + SEARCH_SUCCESS(HttpStatus.OK, "검색에 성공했습니다."), + SEARCH_ACTIVITIES_SUCCESS(HttpStatus.OK, "검색 결과 조회에 성공했습니다."), SEARCH_AUTOCOMPLETE_SUCCESS(HttpStatus.OK, "자동완성 검색에 성공했습니다."), SEARCH_HISTORY_CREATED(HttpStatus.CREATED, "검색 기록을 생성했습니다."), SEARCH_HISTORY_RETRIEVED(HttpStatus.OK, "검색 기록 조회에 성공했습니다."), diff --git a/src/main/kotlin/picklab/backend/search/application/SearchUseCase.kt b/src/main/kotlin/picklab/backend/search/application/SearchUseCase.kt index e7130395..78fa1bdd 100644 --- a/src/main/kotlin/picklab/backend/search/application/SearchUseCase.kt +++ b/src/main/kotlin/picklab/backend/search/application/SearchUseCase.kt @@ -1,19 +1,30 @@ package picklab.backend.search.application import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional +import picklab.backend.activity.application.mapper.withBookmark +import picklab.backend.activity.application.model.ActivityItemWithBookmark +import picklab.backend.activity.domain.enums.ActivitySortType +import picklab.backend.activity.domain.enums.ActivityType +import picklab.backend.activity.domain.enums.RecruitmentStatus +import picklab.backend.activity.domain.service.ActivityBookmarkService import picklab.backend.activity.domain.service.ActivityService +import picklab.backend.job.domain.enums.JobGroup import picklab.backend.member.domain.MemberService import picklab.backend.search.domain.service.MemberSearchHistoryService import picklab.backend.search.entrypoint.response.AutocompleteResponse import picklab.backend.search.entrypoint.response.RecentKeywordItem import picklab.backend.search.entrypoint.response.RecentKeywordsResponse +import picklab.backend.search.entrypoint.response.SearchCategoryGroup import picklab.backend.search.entrypoint.response.SearchHistoryResponse +import picklab.backend.search.entrypoint.response.SearchResultResponse @Component class SearchUseCase( private val activityService: ActivityService, + private val activityBookmarkService: ActivityBookmarkService, private val memberService: MemberService, private val memberSearchHistoryService: MemberSearchHistoryService, ) { @@ -28,6 +39,86 @@ class SearchUseCase( return AutocompleteResponse(suggestions) } + /** + * 통합 검색 - 카테고리별 미리보기 그룹 반환 (전체 탭) + */ + @Transactional(readOnly = true) + fun search( + keyword: String, + memberId: Long?, + ): SearchResultResponse { + val trimmed = keyword.trim() + val countPerType = activityService.countActivitiesByKeywordPerType(trimmed) + val totalCount = countPerType.values.sum() + + val groups = + ActivityType.entries.mapNotNull { type -> + val count = countPerType[type.discriminator] ?: 0L + if (count == 0L) return@mapNotNull null + + val previewPage = + activityService.searchActivitiesByKeyword( + keyword = trimmed, + activityType = type.discriminator, + status = null, + jobGroups = null, + sort = ActivitySortType.LATEST, + pageable = PageRequest.of(0, 5), + ) + val previewIds = previewPage.content.map { it.id } + val bookmarkedIds = + memberId + ?.let { activityBookmarkService.getMyBookmarkedActivityIds(it, previewIds) } + ?: emptySet() + + SearchCategoryGroup( + activityType = type.discriminator, + activityTypeName = type.label, + count = count, + items = previewPage.content.map { it.withBookmark(it.id in bookmarkedIds) }, + ) + } + + return SearchResultResponse( + keyword = trimmed, + totalCount = totalCount, + groups = groups, + ) + } + + /** + * 카테고리별 검색 결과 페이지네이션 (카테고리 탭) + */ + @Transactional(readOnly = true) + fun searchActivities( + keyword: String, + activityType: String, + status: RecruitmentStatus?, + jobGroups: List?, + sort: ActivitySortType, + page: Int, + size: Int, + memberId: Long?, + ): Page { + val trimmed = keyword.trim() + val pageable = PageRequest.of(page - 1, size) + val activityPage = + activityService.searchActivitiesByKeyword( + keyword = trimmed, + activityType = activityType, + status = status, + jobGroups = jobGroups, + sort = sort, + pageable = pageable, + ) + val activityIds = activityPage.content.map { it.id } + val bookmarkedIds = + memberId + ?.let { activityBookmarkService.getMyBookmarkedActivityIds(it, activityIds) } + ?: emptySet() + return activityPage.map { it.withBookmark(it.id in bookmarkedIds) } + } + /** * 검색 기록 생성 */ @@ -55,17 +146,13 @@ class SearchUseCase( memberId: Long, page: Int, size: Int, - ): Page { - val searchHistoryPage = memberSearchHistoryService.getSearchHistory(memberId, page, size) - - return searchHistoryPage.map { history -> - SearchHistoryResponse( - id = history.id, - keyword = history.keyword, - searchedAt = history.searchedAt, - createdAt = history.createdAt, - ) - } + ) = memberSearchHistoryService.getSearchHistory(memberId, page, size).map { history -> + SearchHistoryResponse( + id = history.id, + keyword = history.keyword, + searchedAt = history.searchedAt, + createdAt = history.createdAt, + ) } /** @@ -76,17 +163,14 @@ class SearchUseCase( memberId: Long, limit: Int, ): RecentKeywordsResponse { - val searchHistories = memberSearchHistoryService.getRecentKeywords(memberId, limit) - val keywords = - searchHistories.map { history -> + memberSearchHistoryService.getRecentKeywords(memberId, limit).map { history -> RecentKeywordItem( id = history.id, keyword = history.keyword, searchedAt = history.searchedAt, ) } - return RecentKeywordsResponse(keywords) } diff --git a/src/main/kotlin/picklab/backend/search/entrypoint/SearchApi.kt b/src/main/kotlin/picklab/backend/search/entrypoint/SearchApi.kt index 724fd969..c4c03a1b 100644 --- a/src/main/kotlin/picklab/backend/search/entrypoint/SearchApi.kt +++ b/src/main/kotlin/picklab/backend/search/entrypoint/SearchApi.kt @@ -5,33 +5,65 @@ import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.ResponseEntity import org.springframework.security.core.annotation.AuthenticationPrincipal +import picklab.backend.activity.application.model.ActivityItemWithBookmark +import picklab.backend.activity.domain.enums.ActivitySortType +import picklab.backend.activity.domain.enums.RecruitmentStatus import picklab.backend.common.model.MemberPrincipal import picklab.backend.common.model.PageResponse import picklab.backend.common.model.ResponseWrapper +import picklab.backend.job.domain.enums.JobGroup import picklab.backend.search.entrypoint.request.CreateSearchHistoryRequest import picklab.backend.search.entrypoint.response.AutocompleteResponse import picklab.backend.search.entrypoint.response.RecentKeywordsResponse import picklab.backend.search.entrypoint.response.SearchHistoryResponse +import picklab.backend.search.entrypoint.response.SearchResultResponse @Tag(name = "Search", description = "검색 API") interface SearchApi { - @Operation(summary = "통합 검색", description = "전체 도메인에서 키워드 검색을 수행합니다.") - fun search(): String // TODO: 반환 타입 및 파라미터 정의 예정 + @Operation( + summary = "통합 검색", + description = "키워드로 전체 도메인을 검색합니다. 카테고리별 건수와 미리보기 항목(최대 5개)을 반환합니다.", + ) + fun search( + @Parameter(description = "검색 키워드", required = true, example = "개발") + keyword: String, + ): ResponseEntity> + + @Operation( + summary = "카테고리별 검색 결과", + description = "특정 카테고리의 검색 결과를 페이지네이션으로 반환합니다.", + ) + fun searchActivities( + @Parameter(description = "검색 키워드", required = true, example = "개발") + keyword: String, + @Parameter(description = "활동 타입 (EXTRACURRICULAR, SEMINAR, EDUCATION, COMPETITION)", required = true) + type: String, + @Parameter(description = "모집 상태 (OPEN, CLOSED)") + status: RecruitmentStatus? = null, + @Parameter(description = "직무 그룹 (PLANNING, DEVELOPMENT, DESIGN, MARKETING, AI)") + jobGroups: List? = null, + @Parameter(description = "정렬 기준 (LATEST, DEADLINE_ASC, DEADLINE_DESC)", example = "LATEST") + sort: ActivitySortType = ActivitySortType.LATEST, + @Parameter(description = "페이지 번호", example = "1") + page: Int = 1, + @Parameter(description = "페이지 크기", example = "10") + size: Int = 10, + ): ResponseEntity>> @Operation( summary = "자동완성 검색", description = """ 활동명을 기준으로 자동완성 검색을 수행합니다. 입력한 키워드로 시작하는 활동명들을 알파벳 순으로 반환합니다. - + ## 응답 코드 - **SEARCH_AUTOCOMPLETE_SUCCESS**: 자동완성 검색에 성공했습니다. - + ## 예시 요청 ``` GET /v1/search/autocomplete?keyword=개발&limit=5 ``` - + ## 예시 응답 ```json { @@ -71,7 +103,7 @@ interface SearchApi { description = """ 최근 검색어를 최신순으로 조회합니다. 삭제 기능을 위해 각 검색어의 ID를 포함하여 반환합니다. - + ## 응답 특징 - 사용자별 고유한 키워드만 최신순으로 반환 - 각 항목에 삭제 가능한 ID 포함 diff --git a/src/main/kotlin/picklab/backend/search/entrypoint/SearchController.kt b/src/main/kotlin/picklab/backend/search/entrypoint/SearchController.kt index 55a6ec41..c22d84a2 100644 --- a/src/main/kotlin/picklab/backend/search/entrypoint/SearchController.kt +++ b/src/main/kotlin/picklab/backend/search/entrypoint/SearchController.kt @@ -3,6 +3,7 @@ package picklab.backend.search.entrypoint import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.context.SecurityContextHolder import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -11,16 +12,21 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController +import picklab.backend.activity.application.model.ActivityItemWithBookmark +import picklab.backend.activity.domain.enums.ActivitySortType +import picklab.backend.activity.domain.enums.RecruitmentStatus import picklab.backend.common.model.MemberPrincipal import picklab.backend.common.model.PageResponse import picklab.backend.common.model.ResponseWrapper import picklab.backend.common.model.SuccessCode import picklab.backend.common.model.toPageResponse +import picklab.backend.job.domain.enums.JobGroup import picklab.backend.search.application.SearchUseCase import picklab.backend.search.entrypoint.request.CreateSearchHistoryRequest import picklab.backend.search.entrypoint.response.AutocompleteResponse import picklab.backend.search.entrypoint.response.RecentKeywordsResponse import picklab.backend.search.entrypoint.response.SearchHistoryResponse +import picklab.backend.search.entrypoint.response.SearchResultResponse @RestController @RequestMapping("/v1/search") @@ -28,9 +34,40 @@ class SearchController( private val searchUseCase: SearchUseCase, ) : SearchApi { @GetMapping("") - override fun search(): String { - // TODO: 검색 로직 구현 예정 - return "Search endpoint ready" + override fun search( + @RequestParam keyword: String, + ): ResponseEntity> { + val authentication = SecurityContextHolder.getContext().authentication + val memberId: Long? = (authentication?.principal as? MemberPrincipal)?.memberId + val response = searchUseCase.search(keyword, memberId) + return ResponseEntity.ok(ResponseWrapper.success(SuccessCode.SEARCH_SUCCESS, response)) + } + + @GetMapping("/activities") + override fun searchActivities( + @RequestParam keyword: String, + @RequestParam type: String, + @RequestParam(required = false) status: RecruitmentStatus?, + @RequestParam(required = false) jobGroups: List?, + @RequestParam(defaultValue = "LATEST") sort: ActivitySortType, + @RequestParam(defaultValue = "1") page: Int, + @RequestParam(defaultValue = "10") size: Int, + ): ResponseEntity>> { + val authentication = SecurityContextHolder.getContext().authentication + val memberId: Long? = (authentication?.principal as? MemberPrincipal)?.memberId + val response = + searchUseCase + .searchActivities( + keyword = keyword, + activityType = type, + status = status, + jobGroups = jobGroups, + sort = sort, + page = page, + size = size, + memberId = memberId, + ).toPageResponse() + return ResponseEntity.ok(ResponseWrapper.success(SuccessCode.SEARCH_ACTIVITIES_SUCCESS, response)) } @GetMapping("/autocomplete") diff --git a/src/main/kotlin/picklab/backend/search/entrypoint/response/SearchResultResponse.kt b/src/main/kotlin/picklab/backend/search/entrypoint/response/SearchResultResponse.kt new file mode 100644 index 00000000..8af98b6b --- /dev/null +++ b/src/main/kotlin/picklab/backend/search/entrypoint/response/SearchResultResponse.kt @@ -0,0 +1,24 @@ +package picklab.backend.search.entrypoint.response + +import io.swagger.v3.oas.annotations.media.Schema +import picklab.backend.activity.application.model.ActivityItemWithBookmark + +data class SearchResultResponse( + @field:Schema(description = "검색 키워드") + val keyword: String, + @field:Schema(description = "전체 결과 수") + val totalCount: Long, + @field:Schema(description = "카테고리별 검색 결과 그룹") + val groups: List, +) + +data class SearchCategoryGroup( + @field:Schema(description = "활동 유형 코드") + val activityType: String, + @field:Schema(description = "활동 유형명") + val activityTypeName: String, + @field:Schema(description = "해당 유형 결과 수") + val count: Long, + @field:Schema(description = "활동 목록") + val items: List, +)