Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,13 @@ class ActivityRepositoryImpl(
jobGroups?.let { and(QJobCategory.jobCategory.jobGroup.`in`(it)) }
}

val daysUntilDeadline =
Expressions.numberTemplate(
Long::class.java,
"DATEDIFF({0}, {1})",
QActivity.activity.recruitmentEndDate,
LocalDate.now(),
)
val orderBy =
when (sort) {
ActivitySortType.LATEST -> {
Expand All @@ -209,36 +216,54 @@ class ActivityRepositoryImpl(

ActivitySortType.DEADLINE_DESC -> {
listOf(
Expressions
.numberTemplate(
Long::class.java,
"DATEDIFF({0}, {1})",
QActivity.activity.recruitmentEndDate,
LocalDate.now(),
).desc(),
daysUntilDeadline.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()
when (sort) {
ActivitySortType.DEADLINE_DESC ->
jpaQueryFactory
.select(QActivity.activity, daysUntilDeadline)
.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()
.mapNotNull { it.get(QActivity.activity)?.id }

else ->
jpaQueryFactory
.select(QActivity.activity)
.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()
.map { it.id }
}

if (activityIds.isEmpty()) {
return PageImpl(emptyList(), pageable, 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ class SecurityConfig(
"/swagger",
"/v1/auth/login/*",
"/v1/activities",
"/v1/search",
"/v1/search/autocomplete",
"/v1/search/popular-keywords",
"/v1/activities/*/reviews/statistics/**",
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ enum class SuccessCode(
SEARCH_HISTORY_CREATED(HttpStatus.CREATED, "검색 기록을 생성했습니다."),
SEARCH_HISTORY_RETRIEVED(HttpStatus.OK, "검색 기록 조회에 성공했습니다."),
RECENT_KEYWORDS_RETRIEVED(HttpStatus.OK, "최근 검색어 조회에 성공했습니다."),
POPULAR_SEARCH_KEYWORDS_RETRIEVED(HttpStatus.OK, "인기 검색어 조회에 성공했습니다."),
SEARCH_HISTORY_DELETED(HttpStatus.OK, "검색 기록을 삭제했습니다."),
SEARCH_HISTORY_ALL_DELETED(HttpStatus.OK, "모든 검색 기록을 삭제했습니다."),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ 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.model.PopularSearchKeywords
import picklab.backend.search.domain.service.MemberSearchHistoryService
import picklab.backend.search.domain.service.PopularSearchKeywordService
import picklab.backend.search.entrypoint.response.AutocompleteResponse
import picklab.backend.search.entrypoint.response.RecentKeywordItem
import picklab.backend.search.entrypoint.response.RecentKeywordsResponse
Expand All @@ -27,6 +29,7 @@ class SearchUseCase(
private val activityBookmarkService: ActivityBookmarkService,
private val memberService: MemberService,
private val memberSearchHistoryService: MemberSearchHistoryService,
private val popularSearchKeywordService: PopularSearchKeywordService,
) {
/**
* 활동명 자동완성 검색
Expand All @@ -46,6 +49,7 @@ class SearchUseCase(
fun search(
keyword: String,
memberId: Long?,
searcherKey: String,
): SearchResultResponse {
val trimmed = keyword.trim()
val countPerType = activityService.countActivitiesByKeywordPerType(trimmed)
Expand Down Expand Up @@ -79,13 +83,21 @@ class SearchUseCase(
)
}

popularSearchKeywordService.recordSearch(
keyword = trimmed,
searcherKey = searcherKey,
totalCount = totalCount,
)

return SearchResultResponse(
keyword = trimmed,
totalCount = totalCount,
groups = groups,
)
}

fun getPopularKeywords(): PopularSearchKeywords = popularSearchKeywordService.getPopularKeywords()

/**
* 카테고리별 검색 결과 페이지네이션 (카테고리 탭)
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package picklab.backend.search.domain.entity

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Table
import org.hibernate.annotations.Comment
import picklab.backend.common.model.BaseEntity

@Entity
@Table(name = "blocked_search_keyword")
class BlockedSearchKeyword(
@Column(name = "keyword", nullable = false, unique = true, length = 255)
@Comment("정규화된 차단 검색어")
val keyword: String,
) : BaseEntity()
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package picklab.backend.search.domain.entity

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Table
import jakarta.persistence.UniqueConstraint
import org.hibernate.annotations.Comment
import picklab.backend.common.model.BaseEntity
import java.time.LocalDateTime

@Entity
@Table(
name = "popular_search_keyword_event",
uniqueConstraints = [
UniqueConstraint(
name = "uk_popular_search_keyword_event_hour",
columnNames = ["keyword", "searcher_key", "search_hour"],
),
],
)
class PopularSearchKeywordEvent(
@Column(name = "keyword", nullable = false, length = 255)
@Comment("정규화된 검색 키워드")
val keyword: String,
@Column(name = "searcher_key", nullable = false, length = 100)
@Comment("검색자 식별 키")
val searcherKey: String,
@Column(name = "search_hour", nullable = false)
@Comment("검색 집계 시간대")
val searchHour: LocalDateTime,
@Column(name = "searched_at", nullable = false)
@Comment("검색 실행 시간")
val searchedAt: LocalDateTime,
) : BaseEntity()
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package picklab.backend.search.domain.enums

enum class PopularSearchKeywordTrend {
UP,
DOWN,
SAME,
NEW,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package picklab.backend.search.domain.model

import picklab.backend.search.domain.enums.PopularSearchKeywordTrend
import java.time.LocalDateTime

data class PopularSearchKeywords(
val aggregatedAt: LocalDateTime,
val keywords: List<PopularSearchKeyword>,
)

data class PopularSearchKeyword(
val rank: Int,
val keyword: String,
val trend: PopularSearchKeywordTrend,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package picklab.backend.search.domain.repository

import org.springframework.data.jpa.repository.JpaRepository
import picklab.backend.search.domain.entity.BlockedSearchKeyword

interface BlockedSearchKeywordRepository : JpaRepository<BlockedSearchKeyword, Long> {
fun existsByKeyword(keyword: String): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package picklab.backend.search.domain.repository

import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import picklab.backend.search.domain.entity.PopularSearchKeywordEvent
import java.time.LocalDateTime

interface PopularSearchKeywordEventRepository : JpaRepository<PopularSearchKeywordEvent, Long> {
@Modifying
@Query(
value =
"""
INSERT IGNORE INTO popular_search_keyword_event
(keyword, searcher_key, search_hour, searched_at, created_at, updated_at)
VALUES
(:keyword, :searcherKey, :searchHour, :searchedAt, NOW(), NOW())
""",
nativeQuery = true,
)
fun insertIgnore(
@Param("keyword") keyword: String,
@Param("searcherKey") searcherKey: String,
@Param("searchHour") searchHour: LocalDateTime,
@Param("searchedAt") searchedAt: LocalDateTime,
): Int

@Query(
"""
SELECT
event.keyword AS keyword,
COUNT(event.id) AS searchCount,
MAX(event.searchedAt) AS lastSearchedAt
FROM PopularSearchKeywordEvent event
WHERE event.searchHour = :searchHour
AND NOT EXISTS (
SELECT blocked.id
FROM BlockedSearchKeyword blocked
WHERE blocked.keyword = event.keyword
)
GROUP BY event.keyword
HAVING COUNT(event.id) >= :minSearchCount
ORDER BY COUNT(event.id) DESC, MAX(event.searchedAt) DESC
""",
)
fun findRanksBySearchHour(
@Param("searchHour") searchHour: LocalDateTime,
@Param("minSearchCount") minSearchCount: Long,
pageable: Pageable,
): List<PopularSearchKeywordRankProjection>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package picklab.backend.search.domain.repository

import java.time.LocalDateTime

interface PopularSearchKeywordRankProjection {
val keyword: String
val searchCount: Long
val lastSearchedAt: LocalDateTime
}
Loading
Loading