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
6 changes: 5 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ plugins {
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
kotlin("kapt") version "1.9.25" // 추가

kotlin("plugin.jpa") version "1.9.25" // 현재 사용 중인 코틀린 버전에 맞게
id("org.springframework.boot") version "3.5.6"
id("io.spring.dependency-management") version "1.1.7"
}
Expand Down Expand Up @@ -35,6 +35,10 @@ dependencies {
implementation("org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.5")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5")
implementation("com.querydsl:querydsl-jpa:5.1.0:jakarta")
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
runtimeOnly("com.h2database:h2")

kapt("com.querydsl:querydsl-apt:5.1.0:jakarta") // annotationProcessor → kapt로 변경

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.retrip.map.application.`in`.request

import java.time.LocalDateTime
import java.util.UUID

data class LocationRecentSearchModel(
val memberId: UUID,
val searchText: String,
val updateTime: LocalDateTime = LocalDateTime.now(),
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.retrip.map.application.`in`.request.context

import java.util.UUID

data class UserContext(
val memberId: UUID,
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.retrip.map.application.`in`.request.context

import io.swagger.v3.oas.annotations.Parameter


@Retention(AnnotationRetention.RUNTIME)
@Target(allowedTargets = [AnnotationTarget.VALUE_PARAMETER])
@Parameter(hidden = true)
annotation class WithUserContext()
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.retrip.map.application.`in`.response

import com.retrip.map.domain.vo.LocationCountry
import io.swagger.v3.oas.annotations.media.Schema
import java.util.UUID

@Schema(description = "장소 최근 조회 Response")
data class LocationRecentSearchResponse(
@Schema(description = "검색어")
val searchTexts: List<String>? = null,
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.retrip.map.application.`in`.service

import com.retrip.map.application.`in`.request.LocationRecentSearchModel
import com.retrip.map.application.`in`.request.context.UserContext
import com.retrip.map.application.`in`.response.LocationRecentSearchResponse
import com.retrip.map.application.`in`.usecase.LocationRecentSearchUseCase
import com.retrip.map.application.out.repository.LocationRecentSearchRepository
import com.retrip.map.domain.entity.LocationRecentSearch
import lombok.RequiredArgsConstructor
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@RequiredArgsConstructor
@Transactional
class LocationRecentSearchService(
private val locationRecentSearchRepository: LocationRecentSearchRepository
) : LocationRecentSearchUseCase {

override fun getRecentLocation(context: UserContext): LocationRecentSearchResponse? {
val pageRequest = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "lastSearchedAt"))
//최대 10개만 조회
val result = locationRecentSearchRepository.findByMemberId(context.memberId, pageRequest)
return result?.let {
LocationRecentSearchResponse(
it.map { recentSearch -> recentSearch.keyword }
)
}

}

override fun addLocationRecentSearch(locationRecentSearchModel: LocationRecentSearchModel) {
val existingLocationRecentSearch = locationRecentSearchRepository.findByMemberIdAndKeyword(
locationRecentSearchModel.memberId,
locationRecentSearchModel.searchText,
)
if (existingLocationRecentSearch != null) {
existingLocationRecentSearch.updateTime(
locationRecentSearchModel.updateTime
)
} else {
locationRecentSearchRepository.save(
LocationRecentSearch.create(
locationRecentSearchModel.memberId,
locationRecentSearchModel.searchText,
locationRecentSearchModel.updateTime
)
)
}
val pageRequest = PageRequest.of(9, 1, Sort.by(Sort.Direction.DESC, "lastSearchedAt"))
val threshold = locationRecentSearchRepository.findThresholdTime(
locationRecentSearchModel.memberId,
pageRequest
)?.firstOrNull()

threshold?.let {
locationRecentSearchRepository.deleteOlderThan(
locationRecentSearchModel.memberId, it
)
}
}

override fun delectRecentLocationsByKeyword(context: UserContext, keyword: String?) {
if (keyword != null) {
//단건 제거
val recentSearchKeyword =
locationRecentSearchRepository.findByMemberIdAndKeyword(context.memberId, keyword)
recentSearchKeyword?.run {
locationRecentSearchRepository.delete(this)
}
} else {
//모두 제거
val recentSearchKeywords = locationRecentSearchRepository.findByMemberId(context.memberId)
recentSearchKeywords?.run {
locationRecentSearchRepository.deleteAllInBatch(this)
}
}
}
}

Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
package com.retrip.map.application.`in`.service

import com.retrip.map.application.`in`.request.LocationRecentSearchModel
import com.retrip.map.application.`in`.request.context.UserContext
import com.retrip.map.application.`in`.response.LocationSearchResponse
import com.retrip.map.application.`in`.usecase.LocationSearchUseCase
import com.retrip.map.application.out.repository.LocationElasticRepository
import com.retrip.map.application.out.repository.LocationSearchHistoryRepository
import com.retrip.map.domain.entity.LocationSearchHistory
import lombok.RequiredArgsConstructor
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime

@Service
@RequiredArgsConstructor
@Transactional
class LocationSearchService(
val locationElasticRepository: LocationElasticRepository
val locationElasticRepository: LocationElasticRepository,
val locationSearchHistoryRepository: LocationSearchHistoryRepository,
val eventPublisher: ApplicationEventPublisher,
) : LocationSearchUseCase {

@Transactional(readOnly = true)
override fun getLocation(searchText: String?, page: Pageable): Page<LocationSearchResponse> {
@Transactional
override fun getLocation(searchText: String?, page: Pageable, context: UserContext): Page<LocationSearchResponse> {
val locations = locationElasticRepository.findBySearchText(searchText, page)
if (!searchText.isNullOrBlank()) {
val memberId = context.memberId
val locationSearchHistory =
locationSearchHistoryRepository.save(LocationSearchHistory.create(searchText, memberId))
eventPublisher.publishEvent(
LocationRecentSearchModel(
searchText = searchText,
memberId = memberId,
updateTime = locationSearchHistory.createdAt ?: LocalDateTime.now(),
)
)
}
return locations.map {
LocationSearchResponse(
id = it.id,
Expand All @@ -29,6 +49,5 @@ class LocationSearchService(
)
}
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.retrip.map.application.`in`.usecase

import com.retrip.map.application.`in`.request.LocationRecentSearchModel
import com.retrip.map.application.`in`.request.context.UserContext
import com.retrip.map.application.`in`.response.LocationRecentSearchResponse
import com.retrip.map.application.`in`.response.LocationSearchResponse
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable

interface LocationRecentSearchUseCase {
fun getRecentLocation( context: UserContext) : LocationRecentSearchResponse?
fun addLocationRecentSearch(locationRecentSearchModel: LocationRecentSearchModel)
fun delectRecentLocationsByKeyword(context: UserContext, keyword: String?)
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
package com.retrip.map.application.`in`.usecase

import com.retrip.map.application.`in`.request.LocationCreateRequest
import com.retrip.map.application.`in`.request.LocationUpdateRequest
import com.retrip.map.application.`in`.response.LocationCreateResponse
import com.retrip.map.application.`in`.response.LocationResponse
import com.retrip.map.application.`in`.request.context.UserContext
import com.retrip.map.application.`in`.response.LocationRecentSearchResponse
import com.retrip.map.application.`in`.response.LocationSearchResponse
import com.retrip.map.application.`in`.response.LocationUpdateResponse
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import java.util.*

interface LocationSearchUseCase {
fun getLocation(searchText: String?, page: Pageable): Page<LocationSearchResponse>
fun getLocation(searchText: String?, page: Pageable, context: UserContext): Page<LocationSearchResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.retrip.map.application.out.repository

import com.retrip.map.domain.entity.LocationRecentSearch
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 java.time.LocalDateTime
import java.util.*

interface LocationRecentSearchRepository : JpaRepository<LocationRecentSearch, UUID> {

fun findByMemberIdAndKeyword(memberId: UUID, keyword: String): LocationRecentSearch?
fun findByMemberId(memberId: UUID, pageable: Pageable): List<LocationRecentSearch>?
fun findByMemberId(memberId: UUID): List<LocationRecentSearch>?

@Query(
"""
SELECT r.lastSearchedAt
FROM LocationRecentSearch r
WHERE r.memberId = :memberId
ORDER BY r.lastSearchedAt DESC
"""
)
fun findThresholdTime(memberId: UUID, pageable: Pageable): List<LocalDateTime>?

@Modifying
@Query(
"""
DELETE FROM LocationRecentSearch r WHERE r.memberId = :memberId AND r.lastSearchedAt < :threshold
"""
)
fun deleteOlderThan(memberId: UUID, it: LocalDateTime)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.retrip.map.application.out.repository

import com.retrip.map.domain.entity.LocationSearchHistory
import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID

interface LocationSearchHistoryRepository: JpaRepository<LocationSearchHistory, UUID> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.retrip.map.domain.entity

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Index
import jakarta.persistence.Table
import jakarta.persistence.UniqueConstraint
import jakarta.persistence.Version
import lombok.AccessLevel
import lombok.NoArgsConstructor
import lombok.Setter
import java.time.LocalDateTime
import java.util.*

@Entity
@Table(
name = "location_recent_search",
uniqueConstraints = [UniqueConstraint(
name = "uk_location_user_keyword",
columnNames = ["keyword", "memberId"]
)],
indexes = [
Index(name = "idx_user_recent_searched", columnList = "member_id, last_searched_at DESC")
]
)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Setter(value = AccessLevel.PROTECTED)
class LocationRecentSearch(
@Id
@Column(columnDefinition = "varbinary(16)")
val id: UUID? = null,

@Column(nullable = false)
val keyword: String,

@Column(nullable = false)
val memberId: UUID,

@Column(nullable = false)
var lastSearchedAt: LocalDateTime,

@Version
private val version: Long? = null,
) : BaseEntity() {
fun updateTime(updateTime: LocalDateTime) {
this.lastSearchedAt = updateTime
}

companion object {
fun create(memberId: UUID, searchText: String, updateTime: LocalDateTime): LocationRecentSearch {
return LocationRecentSearch(
id = UUID.randomUUID(),
keyword = searchText,
memberId = memberId,
lastSearchedAt = updateTime
)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.retrip.map.domain.entity

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Version
import lombok.AccessLevel
import lombok.NoArgsConstructor
import lombok.Setter
import java.util.*

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Setter(value = AccessLevel.PROTECTED)
class LocationSearchHistory(
@Id
@Column(columnDefinition = "varbinary(16)")
val id: UUID? = null,

val searchText: String,

val memberId: UUID,

@Version
private val version: Long? = null,
): BaseEntity() {
companion object {
fun create(searchText: String, memberId: UUID): LocationSearchHistory {
return LocationSearchHistory(
id = UUID.randomUUID(),
searchText = searchText,
memberId = memberId,
)
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ enum class ErrorCode(
LOCATION_DETAIL_NOT_FOUND(BAD_REQUEST, "Location-002", "로케이션 디테일 엔티티를 찾을 수 없습니다."),
LOCATION_DUPLICATION(INTERNAL_SERVER_ERROR, "Location-003", "지역이 중복됩니다."),
LOCATION_DETAILS_DUPLICATION(INTERNAL_SERVER_ERROR, "Location-004", "상세 지역이 중복됩니다."),

}
Loading