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
@@ -0,0 +1,10 @@
package com.retrip.map.application.`in`.request

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

data class LocationDetailRecentSearchModel(
val memberId: UUID,
val searchText: String,
val updateTime: LocalDateTime = LocalDateTime.now(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.retrip.map.application.`in`.response

import io.swagger.v3.oas.annotations.media.Schema

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

import com.retrip.map.application.`in`.request.LocationDetailRecentSearchModel
import com.retrip.map.application.`in`.request.context.UserContext
import com.retrip.map.application.`in`.response.LocationDetailRecentSearchResponse
import com.retrip.map.application.`in`.usecase.LocationDetailRecentSearchUseCase
import com.retrip.map.application.out.repository.LocationDetailRecentSearchRepository
import com.retrip.map.domain.entity.LocationDetailRecentSearch
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.Propagation
import org.springframework.transaction.annotation.Transactional

@Service
@RequiredArgsConstructor
class LocationDetailRecentSearchService(
private val locationDetailRecentSearchRepository: LocationDetailRecentSearchRepository
) : LocationDetailRecentSearchUseCase {

@Transactional(readOnly = true)
override fun getRecentLocationDetail(context: UserContext): LocationDetailRecentSearchResponse? {
val pageRequest = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "lastSearchedAt"))
val result = locationDetailRecentSearchRepository.findByMemberId(context.memberId, pageRequest)
return result?.let {
LocationDetailRecentSearchResponse(
it.map { recentSearch -> recentSearch.keyword }
)
}
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
override fun addLocationDetailRecentSearch(model: LocationDetailRecentSearchModel) {
val existing = locationDetailRecentSearchRepository.findByMemberIdAndKeyword(
model.memberId,
model.searchText,
)
if (existing != null) {
existing.updateTime(model.updateTime)
} else {
locationDetailRecentSearchRepository.save(
LocationDetailRecentSearch.create(
model.memberId,
model.searchText,
model.updateTime
)
)
}
val pageRequest = PageRequest.of(9, 1, Sort.by(Sort.Direction.DESC, "lastSearchedAt"))
val threshold = locationDetailRecentSearchRepository.findThresholdTime(
model.memberId,
pageRequest
)?.firstOrNull()

threshold?.let {
locationDetailRecentSearchRepository.deleteOlderThan(model.memberId, it)
}
}

@Transactional
override fun deleteRecentLocationDetailsByKeyword(context: UserContext, keyword: String?) {
if (keyword != null) {
val recentSearch = locationDetailRecentSearchRepository.findByMemberIdAndKeyword(context.memberId, keyword)
recentSearch?.run {
locationDetailRecentSearchRepository.delete(this)
}
} else {
val recentSearches = locationDetailRecentSearchRepository.findByMemberId(context.memberId)
recentSearches?.run {
locationDetailRecentSearchRepository.deleteAllInBatch(this)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,38 @@
package com.retrip.map.application.`in`.service

import com.retrip.map.application.`in`.request.LocationDetailRecentSearchModel
import com.retrip.map.application.`in`.request.context.UserContext
import com.retrip.map.application.`in`.response.LocationDetailSearchResponse
import com.retrip.map.application.`in`.usecase.LocationDetailRecentSearchUseCase
import com.retrip.map.application.`in`.usecase.LocationDetailSearchUseCase
import com.retrip.map.application.out.repository.LocationDetailElasticRepository
import com.retrip.map.application.out.repository.LocationDetailSearchQueryRepository
import com.retrip.map.application.out.repository.LocationSearchQueryRepository
import lombok.RequiredArgsConstructor
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
import java.util.UUID

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
class LocationDetailSearchService(
private val locationDetailSearchQueryRepository: LocationDetailSearchQueryRepository,
private val locationDetailRecentSearchUseCase: LocationDetailRecentSearchUseCase,
) : LocationDetailSearchUseCase {

override fun getDetailLocation(locationId: UUID?, searchText: String?, page: Pageable): Page<LocationDetailSearchResponse> {
override fun getDetailLocation(locationId: UUID?, searchText: String?, page: Pageable, context: UserContext): Page<LocationDetailSearchResponse> {
val locationDetails = locationDetailSearchQueryRepository.findByLocationIdAndSearchText(locationId, searchText, page)
if (!searchText.isNullOrBlank()) {
locationDetailRecentSearchUseCase.addLocationDetailRecentSearch(
LocationDetailRecentSearchModel(
searchText = searchText,
memberId = context.memberId,
updateTime = LocalDateTime.now(),
)
)
}
return locationDetails.map {
LocationDetailSearchResponse(
id = it.id,
Expand All @@ -37,4 +49,3 @@ class LocationDetailSearchService(
}
}
}

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

import com.retrip.map.application.`in`.request.LocationDetailRecentSearchModel
import com.retrip.map.application.`in`.request.context.UserContext
import com.retrip.map.application.`in`.response.LocationDetailRecentSearchResponse

interface LocationDetailRecentSearchUseCase {
fun getRecentLocationDetail(context: UserContext): LocationDetailRecentSearchResponse?
fun addLocationDetailRecentSearch(model: LocationDetailRecentSearchModel)
fun deleteRecentLocationDetailsByKeyword(context: UserContext, keyword: String?)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.retrip.map.application.`in`.usecase

import com.retrip.map.application.`in`.request.context.UserContext
import com.retrip.map.application.`in`.response.LocationDetailSearchResponse
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import java.util.UUID

interface LocationDetailSearchUseCase {
fun getDetailLocation(locationId: UUID?,searchText: String?, page: Pageable): Page<LocationDetailSearchResponse>
fun getDetailLocation(locationId: UUID?, searchText: String?, page: Pageable, context: UserContext): Page<LocationDetailSearchResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.retrip.map.application.out.repository

import com.retrip.map.domain.entity.LocationDetailRecentSearch
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 LocationDetailRecentSearchRepository : JpaRepository<LocationDetailRecentSearch, UUID> {

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

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

@Modifying
@Query(
"""
DELETE FROM LocationDetailRecentSearch r WHERE r.memberId = :memberId AND r.lastSearchedAt < :threshold
"""
)
fun deleteOlderThan(memberId: UUID, threshold: LocalDateTime)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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_detail_recent_search",
uniqueConstraints = [UniqueConstraint(
name = "uk_location_detail_user_keyword",
columnNames = ["keyword", "memberId"]
)],
indexes = [
Index(name = "idx_user_detail_recent_searched", columnList = "member_id, last_searched_at DESC")
]
)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Setter(value = AccessLevel.PROTECTED)
class LocationDetailRecentSearch(
@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): LocationDetailRecentSearch {
return LocationDetailRecentSearch(
id = UUID.randomUUID(),
keyword = searchText,
memberId = memberId,
lastSearchedAt = updateTime
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.retrip.map.infra.adapter.`in`.presentation.rest

import com.retrip.map.application.`in`.request.context.UserContext
import com.retrip.map.application.`in`.request.context.WithUserContext
import com.retrip.map.application.`in`.response.LocationDetailRecentSearchResponse
import com.retrip.map.application.`in`.response.LocationDetailSearchResponse
import com.retrip.map.application.`in`.usecase.LocationDetailRecentSearchUseCase
import com.retrip.map.application.`in`.usecase.LocationDetailSearchUseCase
import com.retrip.map.infra.adapter.`in`.presentation.common.ApiResponse
import com.retrip.map.infra.adapter.`in`.presentation.common.PageUtils
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.tags.Tag
import lombok.RequiredArgsConstructor
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.web.PageableDefault
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
Expand All @@ -22,18 +26,39 @@ import java.util.*
@RequestMapping("search/location-details")
@Tag(name = "LocationDetail", description = "여행 상세 지역 정보 조회용 API 입니다.")
class LocationDetailSearchController(
private val locationDetailSearchUseCase: LocationDetailSearchUseCase
private val locationDetailSearchUseCase: LocationDetailSearchUseCase,
private val locationDetailRecentSearchUseCase: LocationDetailRecentSearchUseCase,
) {

@Operation(summary = "여행 상세 지역 조회", description = "여행 상세 지역 조회(검색엔진)시, 사용하는 API 입니다.")
@GetMapping("")
fun getLocation(
@WithUserContext context: UserContext,
@PageableDefault(size = 10, page = 0) page: Pageable,
@RequestParam(name = "locationId", required = false) locationId: UUID?,
@RequestParam(name = "searchText", required = false) searchText: String?
): ApiResponse<Page<LocationDetailSearchResponse>> {
val safePageable = PageUtils.getSafePageable(page)
val result = locationDetailSearchUseCase.getDetailLocation(locationId, searchText, safePageable)
val result = locationDetailSearchUseCase.getDetailLocation(locationId, searchText, safePageable, context)
return ApiResponse.ok(result)
}

@GetMapping("recent")
@Operation(summary = "최근 여행 상세 지역 조회", description = "최근 여행 상세 지역 조회 API 입니다.")
fun getRecentLocationDetail(
@WithUserContext context: UserContext
): ApiResponse<LocationDetailRecentSearchResponse?> {
val result = locationDetailRecentSearchUseCase.getRecentLocationDetail(context)
return ApiResponse.ok(result)
}

@DeleteMapping("recent")
@Operation(summary = "최근 여행 상세 지역 조회 제거", description = "최근 여행 상세 지역 조회 제거 API 입니다.")
fun deleteRecentLocationDetailsByKeyword(
@WithUserContext context: UserContext,
@RequestParam("keyword", required = false) keyword: String?
): ApiResponse<LocationDetailRecentSearchResponse?> {
locationDetailRecentSearchUseCase.deleteRecentLocationDetailsByKeyword(context, keyword)
return ApiResponse.noContent()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,16 @@ object LocationDetailsQueryDocumentConvert {
id = this.id?.firstOrNull()?.let { UUID.fromString(it) } ?: throw IllegalArgumentException("Cannot find location query document id"),
name = this.name?.firstOrNull() ?: throw IllegalArgumentException("Cannot find location query document name"),
category = this.category?.firstOrNull() ?: throw IllegalArgumentException("Cannot find location query document category"),
searchText = this.searchText?.firstOrNull() ?: throw IllegalArgumentException("Cannot find location query document searchText"),
latitude = this.latitude?.firstOrNull() ?: throw IllegalArgumentException("Cannot find location query document latitude"),
longitude = this.longitude?.firstOrNull() ?: throw IllegalArgumentException("Cannot find location query document longitude"),
createdAt = this.createdAt?.firstOrNull() ?: throw IllegalArgumentException("Cannot find location query document createdAt"),
editedAt = this.editedAt?.firstOrNull() ?: throw IllegalArgumentException("Cannot find location query document editedAt"),
roadAddress = this.roadAddress?.firstOrNull() ?: throw IllegalArgumentException("Cannot find location query document roadAddress"),
telephone = this.telephone?.firstOrNull() ?: throw IllegalArgumentException("Cannot find location query document telephone"),
address = this.address?.firstOrNull() ?: throw IllegalArgumentException("Cannot find location query document address"),
description = this.description?.firstOrNull() ?: throw IllegalArgumentException("Cannot find location query document description"),
locationId = this.locationId?.firstOrNull()?.let { UUID.fromString(it) } ?: throw IllegalArgumentException("Cannot find location query document locationId"),
searchText = this.searchText?.firstOrNull(),
latitude = this.latitude?.firstOrNull(),
longitude = this.longitude?.firstOrNull(),
createdAt = this.createdAt?.firstOrNull(),
editedAt = this.editedAt?.firstOrNull(),
roadAddress = this.roadAddress?.firstOrNull(),
telephone = this.telephone?.firstOrNull(),
address = this.address?.firstOrNull(),
description = this.description?.firstOrNull(),
locationId = this.locationId?.firstOrNull()?.let { UUID.fromString(it) } ?: throw IllegalArgumentException("Cannot find location query document locationId"),
)

}
}
6 changes: 6 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,9 @@ jwt:
u59J3+ToLRrIqIszRZqmasrWTL2/ihPO76PSTIMAsJMScjAwjUXA47YOOy6Vkzy8r3bPTYHp5C1N
KucWwwIDAQAB
-----END PUBLIC KEY-----
#logging:
# level:
#tracer: TRACE
#org.springframework.data.elasticsearch.client.WIRE: TRACE
#org.elasticsearch.client.RestClient: TRACE
#org.springframework.data.elasticsearch.core: DEBUG
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
},
"searchText": {
"type": "text",
"analyzer": "korean"
"analyzer": "korean_ngram",
"search_analyzer": "korean_ngram"
},
"category": {
"type": "keyword"
Expand All @@ -27,6 +28,9 @@
"roadAddress": {
"type": "keyword"
},
"locationId": {
"type": "keyword"
},
"latitude": {
"type": "double"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
},
"searchText": {
"type": "text",
"analyzer": "korean"
"analyzer": "korean_ngram",
"search_analyzer": "korean_ngram"
},
"latitude": {
"type": "double"
Expand Down
Loading