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
12 changes: 12 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"Bash(./gradlew compileKotlin)",
"Bash(./gradlew compileKotlin --stacktrace)",
"Bash(java -version)",
"Bash(./gradlew --version)",
"Bash(xargs ls -la)",
"Bash(xargs cat)"
]
}
}
4 changes: 4 additions & 0 deletions src/main/kotlin/com/retrip/map/MapApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package com.retrip.map

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling

@SpringBootApplication
@EnableScheduling
@EnableAsync
class MapApplication()

fun main(args: Array<String>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ package com.retrip.map.application.`in`.request.context
import java.util.UUID

data class UserContext(
val memberId: UUID,
val memberId: UUID?,
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ import io.swagger.v3.oas.annotations.media.Schema
@Schema(description = "상세 μž₯μ†Œ 졜근 쑰회 Response")
data class LocationDetailRecentSearchResponse(
@Schema(description = "검색어")
val searchTexts: List<String>? = null,
val searchTexts: List<String> = emptyList(),
)
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
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,
) {
}
val searchTexts: List<String> = emptyList(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.retrip.map.application.`in`.service

import com.retrip.map.application.out.repository.LocationDetailElasticRepository
import com.retrip.map.application.out.repository.LocationDetailRepository
import com.retrip.map.application.out.repository.LocationElasticRepository
import com.retrip.map.application.out.repository.LocationRepository
import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDetailDocument
import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDocument
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.util.concurrent.atomic.AtomicReference

@Service
class EsSyncService(
private val locationDetailRepository: LocationDetailRepository,
private val locationRepository: LocationRepository,
private val locationDetailElasticRepository: LocationDetailElasticRepository,
private val locationElasticRepository: LocationElasticRepository,
) {
private val log = LoggerFactory.getLogger(javaClass)

// LocalDateTime.MIN = μ•± 졜초 기동 μ‹œ 전체 색인
private val lastLocationDetailSync = AtomicReference(LocalDateTime.MIN)
private val lastLocationSync = AtomicReference(LocalDateTime.MIN)

@Transactional(readOnly = true)
fun syncLocationDetails() {
val syncStart = LocalDateTime.now()
val lastSync = lastLocationDetailSync.get()

// μ‹ κ·œ/μˆ˜μ •: λ§ˆμ§€λ§‰ 동기화 이후 editedAt이 λ³€κ²½λœ λ ˆμ½”λ“œ
val toUpsert = if (lastSync == LocalDateTime.MIN) {
locationDetailRepository.findAll()
} else {
locationDetailRepository.findByEditedAtAfter(lastSync)
}
if (toUpsert.isNotEmpty()) {
val docs = toUpsert.mapNotNull { entity ->
runCatching { LocationDetailDocument.of(entity) }.getOrElse { e ->
log.warn("[ES Sync] LocationDetail λ³€ν™˜ μ‹€νŒ¨: id={}", entity.id, e)
null
}
}
locationDetailElasticRepository.saveAll(docs)
}

// μ‚­μ œ: ES에 μžˆμ§€λ§Œ DB에 μ—†λŠ” λ¬Έμ„œ 제거
val dbIds = locationDetailRepository.findAllIds().toSet()
val toDelete = locationDetailElasticRepository.findAll()
.map { it.id }
.filter { it !in dbIds }
if (toDelete.isNotEmpty()) {
locationDetailElasticRepository.deleteAllById(toDelete)
}

lastLocationDetailSync.set(syncStart)
log.info("[ES Sync] LocationDetail - 색인: {}건, μ‚­μ œ: {}건", toUpsert.size, toDelete.size)
}

@Transactional(readOnly = true)
fun syncLocations() {
val syncStart = LocalDateTime.now()
val lastSync = lastLocationSync.get()

val toUpsert = if (lastSync == LocalDateTime.MIN) {
locationRepository.findAll()
} else {
locationRepository.findByEditedAtAfter(lastSync)
}
if (toUpsert.isNotEmpty()) {
val docs = toUpsert.mapNotNull { entity ->
runCatching { LocationDocument.of(entity) }.getOrElse { e ->
log.warn("[ES Sync] Location λ³€ν™˜ μ‹€νŒ¨: id={}", entity.id, e)
null
}
}
locationElasticRepository.saveAll(docs)
}

val dbIds = locationRepository.findAllIds().toSet()
val toDelete = locationElasticRepository.findAll()
.map { it.id }
.filter { it !in dbIds }
if (toDelete.isNotEmpty()) {
locationElasticRepository.deleteAllById(toDelete)
}

lastLocationSync.set(syncStart)
log.info("[ES Sync] Location - 색인: {}건, μ‚­μ œ: {}건", toUpsert.size, toDelete.size)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,25 @@ import com.retrip.map.application.`in`.response.LocationDetailRecentSearchRespon
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? {
override fun getRecentLocationDetail(context: UserContext): LocationDetailRecentSearchResponse {
val memberId = context.memberId ?: return 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 }
)
}
val result = locationDetailRecentSearchRepository.findByMemberId(memberId, pageRequest)
return LocationDetailRecentSearchResponse(
result?.map { it.keyword } ?: emptyList()
)
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
Expand Down Expand Up @@ -60,13 +57,14 @@ class LocationDetailRecentSearchService(

@Transactional
override fun deleteRecentLocationDetailsByKeyword(context: UserContext, keyword: String?) {
val memberId = context.memberId ?: return
if (keyword != null) {
val recentSearch = locationDetailRecentSearchRepository.findByMemberIdAndKeyword(context.memberId, keyword)
val recentSearch = locationDetailRecentSearchRepository.findByMemberIdAndKeyword(memberId, keyword)
recentSearch?.run {
locationDetailRecentSearchRepository.delete(this)
}
} else {
val recentSearches = locationDetailRecentSearchRepository.findByMemberId(context.memberId)
val recentSearches = locationDetailRecentSearchRepository.findByMemberId(memberId)
recentSearches?.run {
locationDetailRecentSearchRepository.deleteAllInBatch(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ 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.LocationDetailSearchQueryRepository
import lombok.RequiredArgsConstructor
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
Expand All @@ -15,7 +14,6 @@ import java.time.LocalDateTime
import java.util.UUID

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
class LocationDetailSearchService(
private val locationDetailSearchQueryRepository: LocationDetailSearchQueryRepository,
Expand All @@ -24,7 +22,7 @@ class LocationDetailSearchService(

override fun getDetailLocation(locationId: UUID?, searchText: String?, page: Pageable, context: UserContext): Page<LocationDetailSearchResponse> {
val locationDetails = locationDetailSearchQueryRepository.findByLocationIdAndSearchText(locationId, searchText, page)
if (!searchText.isNullOrBlank()) {
if (!searchText.isNullOrBlank() && context.memberId != null) {
locationDetailRecentSearchUseCase.addLocationDetailRecentSearch(
LocationDetailRecentSearchModel(
searchText = searchText,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,26 @@ import com.retrip.map.application.`in`.response.LocationDetailCreateResponse
import com.retrip.map.application.`in`.response.LocationDetailResponse
import com.retrip.map.application.`in`.response.LocationDetailUpdateResponse
import com.retrip.map.application.`in`.usecase.LocationDetailUseCase
import com.retrip.map.application.out.repository.LocationDetailElasticRepository
import com.retrip.map.application.out.repository.LocationDetailQueryRepository
import com.retrip.map.application.out.repository.LocationDetailRepository
import com.retrip.map.application.out.repository.LocationRepository
import com.retrip.map.domain.exception.LocationDetailDuplicateException
import com.retrip.map.domain.exception.LocationDetailNotFoundException
import com.retrip.map.domain.exception.LocationNotFoundException
import com.retrip.map.domain.exception.common.RequireException
import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDetailDocument
import lombok.RequiredArgsConstructor
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.domain.AbstractPersistable_.id
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.*

@Service
@RequiredArgsConstructor
@Transactional
class LocationDetailService(
val locationRepository: LocationRepository,
val locationDetailRepository: LocationDetailRepository,
val locationDetailQueryRepository: LocationDetailQueryRepository,
val locationDetailElasticRepository: LocationDetailElasticRepository,
) : LocationDetailUseCase {

@Transactional(readOnly = true)
Expand All @@ -41,12 +35,10 @@ class LocationDetailService(

override fun createLocationDetail(locationId: UUID, request: LocationDetailCreateRequest): LocationDetailCreateResponse {
val location = locationRepository.findByIdOrNull(locationId) ?: throw LocationNotFoundException()
val isDuplicate =
locationDetailRepository.findByNameValueAndLocationId(request.name, locationId)
?.run { throw LocationDetailDuplicateException() }
locationDetailRepository.findByNameValueAndLocationId(request.name, locationId)
?.run { throw LocationDetailDuplicateException() }

val locationDetail = locationDetailRepository.save(request.to(location))
locationDetailElasticRepository.save(LocationDetailDocument.of(locationDetail))
return LocationDetailCreateResponse(
locationDetail.id ?: throw LocationDetailNotFoundException(),
locationDetail.name?.value ?: throw RequireException(),
Expand All @@ -62,9 +54,8 @@ class LocationDetailService(

override fun updateLocationDetail(locationId: UUID, id: UUID, request: LocationDetailUpdateRequest): LocationDetailUpdateResponse {
val location = locationRepository.findByIdOrNull(locationId) ?: throw LocationNotFoundException()
val isDuplicate =
locationDetailRepository.findByNameValueAndLocationId(request.name, locationId)
?.run { throw LocationDetailDuplicateException() }
locationDetailRepository.findByNameValueAndLocationId(request.name, locationId)
?.run { throw LocationDetailDuplicateException() }
val locationDetails = locationDetailRepository.findByIdOrNull(id) ?: throw LocationDetailNotFoundException()
locationDetails.update(
location,
Expand All @@ -77,8 +68,6 @@ class LocationDetailService(
request.latitude,
request.longitude,
)
locationDetailElasticRepository.deleteById(id)
locationDetailElasticRepository.save(LocationDetailDocument.of(locationDetails))
return LocationDetailUpdateResponse(
locationDetails.id ?: throw LocationDetailNotFoundException(),
locationDetails.name?.value ?: throw RequireException(),
Expand All @@ -94,8 +83,5 @@ class LocationDetailService(

override fun deleteLocationDetail(locationDetailId: UUID) {
locationDetailRepository.deleteById(locationDetailId)
locationDetailElasticRepository.deleteById(locationDetailId)

}
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,27 @@ 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? {
@Transactional(readOnly = true)
override fun getRecentLocation(context: UserContext): LocationRecentSearchResponse {
val memberId = context.memberId ?: return 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 }
)
}

val result = locationRecentSearchRepository.findByMemberId(memberId, pageRequest)
return LocationRecentSearchResponse(
result?.map { it.keyword } ?: emptyList()
)
}

@Transactional
override fun addLocationRecentSearch(locationRecentSearchModel: LocationRecentSearchModel) {
val existingLocationRecentSearch = locationRecentSearchRepository.findByMemberIdAndKeyword(
locationRecentSearchModel.memberId,
Expand Down Expand Up @@ -62,21 +58,19 @@ class LocationRecentSearchService(
}
}

override fun delectRecentLocationsByKeyword(context: UserContext, keyword: String?) {
@Transactional
override fun deleteRecentLocationsByKeyword(context: UserContext, keyword: String?) {
val memberId = context.memberId ?: return
if (keyword != null) {
//단건 제거
val recentSearchKeyword =
locationRecentSearchRepository.findByMemberIdAndKeyword(context.memberId, keyword)
val recentSearchKeyword = locationRecentSearchRepository.findByMemberIdAndKeyword(memberId, keyword)
recentSearchKeyword?.run {
locationRecentSearchRepository.delete(this)
}
} else {
//λͺ¨λ‘ 제거
val recentSearchKeywords = locationRecentSearchRepository.findByMemberId(context.memberId)
val recentSearchKeywords = locationRecentSearchRepository.findByMemberId(memberId)
recentSearchKeywords?.run {
locationRecentSearchRepository.deleteAllInBatch(this)
}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import com.retrip.map.application.`in`.usecase.LocationSearchUseCase
import com.retrip.map.application.out.repository.LocationSearchHistoryRepository
import com.retrip.map.application.out.repository.LocationSearchQueryRepository
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
Expand All @@ -16,7 +15,6 @@ import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime

@Service
@RequiredArgsConstructor
@Transactional
class LocationSearchService(
val locationSearchQueryRepository: LocationSearchQueryRepository,
Expand All @@ -27,7 +25,7 @@ class LocationSearchService(
@Transactional
override fun getLocation(searchText: String?, page: Pageable, context: UserContext): Page<LocationSearchResponse> {
val locations = locationSearchQueryRepository.findBySearchText(searchText, page)
if (!searchText.isNullOrBlank()) {
if (!searchText.isNullOrBlank() && context.memberId != null) {
val memberId = context.memberId
val locationSearchHistory =
locationSearchHistoryRepository.save(LocationSearchHistory.create(searchText, memberId))
Expand Down
Loading