diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..141528d --- /dev/null +++ b/.claude/settings.local.json @@ -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)" + ] + } +} diff --git a/src/main/kotlin/com/retrip/map/MapApplication.kt b/src/main/kotlin/com/retrip/map/MapApplication.kt index 697ae39..2cf0190 100644 --- a/src/main/kotlin/com/retrip/map/MapApplication.kt +++ b/src/main/kotlin/com/retrip/map/MapApplication.kt @@ -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) { diff --git a/src/main/kotlin/com/retrip/map/application/in/request/context/UserContext.kt b/src/main/kotlin/com/retrip/map/application/in/request/context/UserContext.kt index b54a17e..b45939f 100644 --- a/src/main/kotlin/com/retrip/map/application/in/request/context/UserContext.kt +++ b/src/main/kotlin/com/retrip/map/application/in/request/context/UserContext.kt @@ -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?, ) { } diff --git a/src/main/kotlin/com/retrip/map/application/in/response/LocationDetailRecentSearchResponse.kt b/src/main/kotlin/com/retrip/map/application/in/response/LocationDetailRecentSearchResponse.kt index b331e29..53c2c6b 100644 --- a/src/main/kotlin/com/retrip/map/application/in/response/LocationDetailRecentSearchResponse.kt +++ b/src/main/kotlin/com/retrip/map/application/in/response/LocationDetailRecentSearchResponse.kt @@ -5,5 +5,5 @@ import io.swagger.v3.oas.annotations.media.Schema @Schema(description = "상세 장소 최근 조회 Response") data class LocationDetailRecentSearchResponse( @Schema(description = "검색어") - val searchTexts: List? = null, + val searchTexts: List = emptyList(), ) diff --git a/src/main/kotlin/com/retrip/map/application/in/response/LocationRecentSearchResponse.kt b/src/main/kotlin/com/retrip/map/application/in/response/LocationRecentSearchResponse.kt index 97c72f2..84306f3 100644 --- a/src/main/kotlin/com/retrip/map/application/in/response/LocationRecentSearchResponse.kt +++ b/src/main/kotlin/com/retrip/map/application/in/response/LocationRecentSearchResponse.kt @@ -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? = null, -) { -} + val searchTexts: List = emptyList(), +) diff --git a/src/main/kotlin/com/retrip/map/application/in/service/EsSyncService.kt b/src/main/kotlin/com/retrip/map/application/in/service/EsSyncService.kt new file mode 100644 index 0000000..d33ba6c --- /dev/null +++ b/src/main/kotlin/com/retrip/map/application/in/service/EsSyncService.kt @@ -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) + } +} diff --git a/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailRecentSearchService.kt b/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailRecentSearchService.kt index 6fc847e..c8eccea 100644 --- a/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailRecentSearchService.kt +++ b/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailRecentSearchService.kt @@ -6,7 +6,6 @@ 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 @@ -14,20 +13,18 @@ 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) @@ -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) } diff --git a/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailSearchService.kt b/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailSearchService.kt index 8ceca2b..3f79e59 100644 --- a/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailSearchService.kt +++ b/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailSearchService.kt @@ -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 @@ -15,7 +14,6 @@ import java.time.LocalDateTime import java.util.UUID @Service -@RequiredArgsConstructor @Transactional(readOnly = true) class LocationDetailSearchService( private val locationDetailSearchQueryRepository: LocationDetailSearchQueryRepository, @@ -24,7 +22,7 @@ class LocationDetailSearchService( override fun getDetailLocation(locationId: UUID?, searchText: String?, page: Pageable, context: UserContext): Page { val locationDetails = locationDetailSearchQueryRepository.findByLocationIdAndSearchText(locationId, searchText, page) - if (!searchText.isNullOrBlank()) { + if (!searchText.isNullOrBlank() && context.memberId != null) { locationDetailRecentSearchUseCase.addLocationDetailRecentSearch( LocationDetailRecentSearchModel( searchText = searchText, diff --git a/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailService.kt b/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailService.kt index a8872d6..5ecd83c 100644 --- a/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailService.kt +++ b/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailService.kt @@ -6,7 +6,6 @@ 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 @@ -14,24 +13,19 @@ 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) @@ -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(), @@ -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, @@ -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(), @@ -94,8 +83,5 @@ class LocationDetailService( override fun deleteLocationDetail(locationDetailId: UUID) { locationDetailRepository.deleteById(locationDetailId) - locationDetailElasticRepository.deleteById(locationDetailId) - } } - diff --git a/src/main/kotlin/com/retrip/map/application/in/service/LocationRecentSearchService.kt b/src/main/kotlin/com/retrip/map/application/in/service/LocationRecentSearchService.kt index 8a5e66e..7d8ca67 100644 --- a/src/main/kotlin/com/retrip/map/application/in/service/LocationRecentSearchService.kt +++ b/src/main/kotlin/com/retrip/map/application/in/service/LocationRecentSearchService.kt @@ -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, @@ -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) } } } } - diff --git a/src/main/kotlin/com/retrip/map/application/in/service/LocationSearchService.kt b/src/main/kotlin/com/retrip/map/application/in/service/LocationSearchService.kt index 73eca5a..1e547b0 100644 --- a/src/main/kotlin/com/retrip/map/application/in/service/LocationSearchService.kt +++ b/src/main/kotlin/com/retrip/map/application/in/service/LocationSearchService.kt @@ -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 @@ -16,7 +15,6 @@ import org.springframework.transaction.annotation.Transactional import java.time.LocalDateTime @Service -@RequiredArgsConstructor @Transactional class LocationSearchService( val locationSearchQueryRepository: LocationSearchQueryRepository, @@ -27,7 +25,7 @@ class LocationSearchService( @Transactional override fun getLocation(searchText: String?, page: Pageable, context: UserContext): Page { 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)) diff --git a/src/main/kotlin/com/retrip/map/application/in/service/LocationService.kt b/src/main/kotlin/com/retrip/map/application/in/service/LocationService.kt index a5ef5b1..50c3df8 100644 --- a/src/main/kotlin/com/retrip/map/application/in/service/LocationService.kt +++ b/src/main/kotlin/com/retrip/map/application/in/service/LocationService.kt @@ -6,15 +6,12 @@ import com.retrip.map.application.`in`.response.LocationCreateResponse import com.retrip.map.application.`in`.response.LocationResponse import com.retrip.map.application.`in`.response.LocationUpdateResponse import com.retrip.map.application.`in`.usecase.LocationUseCase -import com.retrip.map.application.out.repository.LocationElasticRepository import com.retrip.map.application.out.repository.LocationQueryRepository import com.retrip.map.application.out.repository.LocationRepository import com.retrip.map.domain.exception.LocationDetailNotFoundException import com.retrip.map.domain.exception.LocationDuplicateException 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.LocationDocument -import lombok.RequiredArgsConstructor import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull @@ -23,12 +20,10 @@ import org.springframework.transaction.annotation.Transactional import java.util.* @Service -@RequiredArgsConstructor @Transactional class LocationService( val locationRepository: LocationRepository, - val locationElasticRepository: LocationElasticRepository, - val locationQueryRepository: LocationQueryRepository + val locationQueryRepository: LocationQueryRepository, ) : LocationUseCase { @Transactional(readOnly = true) @@ -38,13 +33,10 @@ class LocationService( override fun createLocation(request: LocationCreateRequest): LocationCreateResponse { - val isDuplicate = - locationRepository.findFirstByNameValueAndCountryValue(request.name, request.country)?.run { - throw LocationDuplicateException() - } + locationRepository.findFirstByNameValueAndCountryValue(request.name, request.country) + ?.run { throw LocationDuplicateException() } val location = locationRepository.save(request.to()) - locationElasticRepository.save(LocationDocument.of(location)) return LocationCreateResponse( location.id ?: throw LocationNotFoundException(), location.name?.value ?: throw RequireException(), @@ -55,20 +47,16 @@ class LocationService( } override fun updateLocation(id: UUID, request: LocationUpdateRequest): LocationUpdateResponse { - val isDuplicate = - locationRepository.findFirstByNameValueAndCountryValue(request.name, request.country)?.run { - throw LocationDuplicateException() - } + locationRepository.findFirstByNameValueAndCountryValue(request.name, request.country) + ?.run { throw LocationDuplicateException() } val location = locationRepository.findByIdOrNull(id) ?: throw LocationNotFoundException() - locationElasticRepository.deleteById(id) location.update( request.name, request.country, request.latitude, request.longitude, ) - locationElasticRepository.save(LocationDocument.of(location)) return LocationUpdateResponse( location.id ?: throw LocationDetailNotFoundException(), location.name?.value ?: throw RequireException(), @@ -80,7 +68,6 @@ class LocationService( override fun deleteLocation(locationId: UUID) { locationRepository.deleteById(locationId) - locationElasticRepository.deleteById(locationId) } } diff --git a/src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailRecentSearchUseCase.kt b/src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailRecentSearchUseCase.kt index bb5f83b..5757043 100644 --- a/src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailRecentSearchUseCase.kt +++ b/src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailRecentSearchUseCase.kt @@ -5,7 +5,7 @@ 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 getRecentLocationDetail(context: UserContext): LocationDetailRecentSearchResponse fun addLocationDetailRecentSearch(model: LocationDetailRecentSearchModel) fun deleteRecentLocationDetailsByKeyword(context: UserContext, keyword: String?) } diff --git a/src/main/kotlin/com/retrip/map/application/in/usecase/LocationRecentSearchUseCase.kt b/src/main/kotlin/com/retrip/map/application/in/usecase/LocationRecentSearchUseCase.kt index 0c50b21..cc2fa40 100644 --- a/src/main/kotlin/com/retrip/map/application/in/usecase/LocationRecentSearchUseCase.kt +++ b/src/main/kotlin/com/retrip/map/application/in/usecase/LocationRecentSearchUseCase.kt @@ -8,7 +8,7 @@ import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable interface LocationRecentSearchUseCase { - fun getRecentLocation( context: UserContext) : LocationRecentSearchResponse? + fun getRecentLocation(context: UserContext): LocationRecentSearchResponse fun addLocationRecentSearch(locationRecentSearchModel: LocationRecentSearchModel) - fun delectRecentLocationsByKeyword(context: UserContext, keyword: String?) + fun deleteRecentLocationsByKeyword(context: UserContext, keyword: String?) } diff --git a/src/main/kotlin/com/retrip/map/application/out/repository/LocationDetailRepository.kt b/src/main/kotlin/com/retrip/map/application/out/repository/LocationDetailRepository.kt index 3bcd5ca..fd987b1 100644 --- a/src/main/kotlin/com/retrip/map/application/out/repository/LocationDetailRepository.kt +++ b/src/main/kotlin/com/retrip/map/application/out/repository/LocationDetailRepository.kt @@ -2,8 +2,14 @@ package com.retrip.map.application.out.repository import com.retrip.map.domain.entity.LocationDetail import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import java.time.LocalDateTime import java.util.UUID interface LocationDetailRepository: JpaRepository { fun findByNameValueAndLocationId(name: String, locationId: UUID): LocationDetail? + fun findByEditedAtAfter(editedAt: LocalDateTime): List + + @Query("SELECT ld.id FROM LocationDetail ld WHERE ld.id IS NOT NULL") + fun findAllIds(): List } diff --git a/src/main/kotlin/com/retrip/map/application/out/repository/LocationRepository.kt b/src/main/kotlin/com/retrip/map/application/out/repository/LocationRepository.kt index ac3f4bf..fcd8cbf 100644 --- a/src/main/kotlin/com/retrip/map/application/out/repository/LocationRepository.kt +++ b/src/main/kotlin/com/retrip/map/application/out/repository/LocationRepository.kt @@ -2,8 +2,14 @@ package com.retrip.map.application.out.repository import com.retrip.map.domain.entity.Location import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import java.time.LocalDateTime import java.util.UUID interface LocationRepository: JpaRepository { fun findFirstByNameValueAndCountryValue(name: String, country: String): Location? + fun findByEditedAtAfter(editedAt: LocalDateTime): List + + @Query("SELECT l.id FROM Location l WHERE l.id IS NOT NULL") + fun findAllIds(): List } diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/EsSyncScheduler.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/EsSyncScheduler.kt new file mode 100644 index 0000000..d4420ff --- /dev/null +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/EsSyncScheduler.kt @@ -0,0 +1,39 @@ +package com.retrip.map.infra.adapter.`in`.batch + +import org.slf4j.LoggerFactory +import org.springframework.batch.core.Job +import org.springframework.batch.core.JobParametersBuilder +import org.springframework.batch.core.launch.JobLauncher +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component + +@Component +class EsSyncScheduler( + private val jobLauncher: JobLauncher, + @Qualifier("locationDetailSyncJob") private val locationDetailSyncJob: Job, + @Qualifier("locationSyncJob") private val locationSyncJob: Job, +) { + private val log = LoggerFactory.getLogger(javaClass) + + @Scheduled(fixedDelay = 3 * 60 * 1000) + fun syncLocationDetails() { + runJob(locationDetailSyncJob, "location-detail-sync-job") + } + + @Scheduled(fixedDelay = 3 * 60 * 1000) + fun syncLocations() { + runJob(locationSyncJob, "location-sync-job") + } + + private fun runJob(job: Job, name: String) { + try { + val params = JobParametersBuilder() + .addLong("time", System.currentTimeMillis()) + .toJobParameters() + jobLauncher.run(job, params) + } catch (e: Exception) { + log.error("[ES Sync] {} 실행 실패", name, e) + } + } +} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/LocationDetailSyncTasklet.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/LocationDetailSyncTasklet.kt new file mode 100644 index 0000000..425e3b7 --- /dev/null +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/LocationDetailSyncTasklet.kt @@ -0,0 +1,18 @@ +package com.retrip.map.infra.adapter.`in`.batch + +import com.retrip.map.application.`in`.service.EsSyncService +import org.springframework.batch.core.StepContribution +import org.springframework.batch.core.scope.context.ChunkContext +import org.springframework.batch.core.step.tasklet.Tasklet +import org.springframework.batch.repeat.RepeatStatus +import org.springframework.stereotype.Component + +@Component +class LocationDetailSyncTasklet( + private val esSyncService: EsSyncService, +) : Tasklet { + override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { + esSyncService.syncLocationDetails() + return RepeatStatus.FINISHED + } +} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/LocationSyncTasklet.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/LocationSyncTasklet.kt new file mode 100644 index 0000000..2a6696b --- /dev/null +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/LocationSyncTasklet.kt @@ -0,0 +1,18 @@ +package com.retrip.map.infra.adapter.`in`.batch + +import com.retrip.map.application.`in`.service.EsSyncService +import org.springframework.batch.core.StepContribution +import org.springframework.batch.core.scope.context.ChunkContext +import org.springframework.batch.core.step.tasklet.Tasklet +import org.springframework.batch.repeat.RepeatStatus +import org.springframework.stereotype.Component + +@Component +class LocationSyncTasklet( + private val esSyncService: EsSyncService, +) : Tasklet { + override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { + esSyncService.syncLocations() + return RepeatStatus.FINISHED + } +} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapBatchConfig.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapBatchConfig.kt index 67ddffe..d92b048 100644 --- a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapBatchConfig.kt +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapBatchConfig.kt @@ -1,50 +1,50 @@ package com.retrip.map.infra.adapter.`in`.batch -import com.retrip.map.domain.entity.LocationDetail -import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDetailDocument import org.springframework.batch.core.Job import org.springframework.batch.core.Step import org.springframework.batch.core.job.builder.JobBuilder import org.springframework.batch.core.launch.support.RunIdIncrementer import org.springframework.batch.core.repository.JobRepository import org.springframework.batch.core.step.builder.StepBuilder -import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.transaction.PlatformTransactionManager @Configuration -//@EnableBatchProcessing class MapBatchConfig( - @Value("\${spring.batch.job.name}") - private val jobName: String, - private val reader: MapReader, - private val processor: MapProcessor, - private val writer: MapWriter, + private val locationDetailSyncTasklet: LocationDetailSyncTasklet, + private val locationSyncTasklet: LocationSyncTasklet, ) { @Bean - fun mapJob( + fun locationDetailSyncJob(jobRepository: JobRepository, locationDetailSyncStep: Step): Job = + JobBuilder("location-detail-sync-job", jobRepository) + .incrementer(RunIdIncrementer()) + .start(locationDetailSyncStep) + .build() + + @Bean + fun locationDetailSyncStep( jobRepository: JobRepository, - mapStep: Step - ): Job { - return JobBuilder(jobName, jobRepository) - .start(mapStep) - .incrementer(RunIdIncrementer()) // 🔥 이 라인 추가 + transactionManager: PlatformTransactionManager, + ): Step = + StepBuilder("location-detail-sync-step", jobRepository) + .tasklet(locationDetailSyncTasklet, transactionManager) .build() - } + @Bean + fun locationSyncJob(jobRepository: JobRepository, locationSyncStep: Step): Job = + JobBuilder("location-sync-job", jobRepository) + .incrementer(RunIdIncrementer()) + .start(locationSyncStep) + .build() @Bean - fun mapStep( + fun locationSyncStep( jobRepository: JobRepository, - transactionManager: PlatformTransactionManager - ): Step { - return StepBuilder("detail-location-step", jobRepository) - .chunk, List>(10, transactionManager) - .reader(reader) - .processor(processor) - .writer(writer) + transactionManager: PlatformTransactionManager, + ): Step = + StepBuilder("location-sync-step", jobRepository) + .tasklet(locationSyncTasklet, transactionManager) .build() - } } diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapProcessor.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapProcessor.kt deleted file mode 100644 index 213bf86..0000000 --- a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapProcessor.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.retrip.map.infra.adapter.`in`.batch - -import com.retrip.map.domain.entity.LocationDetail -import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDetailDocument -import org.springframework.batch.item.ItemProcessor -import org.springframework.stereotype.Component - -@Component -class MapProcessor() - : ItemProcessor, List> { - override fun process(item: List): List? { - return item.map { LocationDetailDocument.of(it) } - } -} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapReader.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapReader.kt deleted file mode 100644 index de07e75..0000000 --- a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapReader.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.retrip.map.infra.adapter.`in`.batch - -import com.retrip.map.application.out.repository.LocationDetailElasticRepository -import com.retrip.map.application.out.repository.LocationDetailQueryRepository -import com.retrip.map.domain.entity.LocationDetail -import org.springframework.batch.item.ItemReader -import org.springframework.stereotype.Component -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneOffset - -@Component -class MapReader( - private val locationDetailQueryRepository: LocationDetailQueryRepository, - private val locationDetailElasticRepository: LocationDetailElasticRepository -) : ItemReader> { - override fun read(): List? { - val lastUpdateDocument = locationDetailElasticRepository.findFirstByOrderByEditedAtDesc() - val editedAt = - lastUpdateDocument?.editedAt?.let { LocalDateTime.ofInstant(it, ZoneOffset.UTC) } - ?: LocalDateTime.now() - - return locationDetailQueryRepository.findLocationDetailsByEditedAt(editedAt) - } -} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapWriter.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapWriter.kt deleted file mode 100644 index 8b28911..0000000 --- a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapWriter.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.retrip.map.infra.adapter.`in`.batch - -import com.retrip.map.application.`in`.usecase.LocationDetailIndexUseCase -import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDetailDocument -import org.springframework.batch.item.Chunk -import org.springframework.batch.item.ItemWriter -import org.springframework.stereotype.Component - -@Component -class MapWriter( - val locationIndexUseCase: LocationDetailIndexUseCase -) : ItemWriter> { - override fun write(chunk: Chunk?>) { - chunk.items.forEach { documents -> locationIndexUseCase.indexLocationDetailDocuments(documents) } - } -} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/event/LocationDetailElasticEvent.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/event/LocationDetailElasticEvent.kt new file mode 100644 index 0000000..77f839f --- /dev/null +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/event/LocationDetailElasticEvent.kt @@ -0,0 +1,10 @@ +package com.retrip.map.infra.adapter.`in`.event + +import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDetailDocument +import java.util.UUID + +sealed class LocationDetailElasticEvent { + data class Created(val document: LocationDetailDocument) : LocationDetailElasticEvent() + data class Updated(val id: UUID, val document: LocationDetailDocument) : LocationDetailElasticEvent() + data class Deleted(val id: UUID) : LocationDetailElasticEvent() +} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/event/LocationDetailElasticEventHandler.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/event/LocationDetailElasticEventHandler.kt new file mode 100644 index 0000000..89dc820 --- /dev/null +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/event/LocationDetailElasticEventHandler.kt @@ -0,0 +1,32 @@ +package com.retrip.map.infra.adapter.`in`.event + +import com.retrip.map.application.out.repository.LocationDetailElasticRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import org.springframework.transaction.event.TransactionPhase +import org.springframework.transaction.event.TransactionalEventListener + +@Component +class LocationDetailElasticEventHandler( + private val locationDetailElasticRepository: LocationDetailElasticRepository, +) { + private val log = LoggerFactory.getLogger(javaClass) + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + fun handle(event: LocationDetailElasticEvent) { + try { + when (event) { + is LocationDetailElasticEvent.Created -> + locationDetailElasticRepository.save(event.document) + is LocationDetailElasticEvent.Updated -> { + locationDetailElasticRepository.deleteById(event.id) + locationDetailElasticRepository.save(event.document) + } + is LocationDetailElasticEvent.Deleted -> + locationDetailElasticRepository.deleteById(event.id) + } + } catch (e: Exception) { + log.error("[ES Sync Failed] DB와 ES 동기화 실패. event={}", event, e) + } + } +} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/event/LocationElasticEvent.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/event/LocationElasticEvent.kt new file mode 100644 index 0000000..6002c7f --- /dev/null +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/event/LocationElasticEvent.kt @@ -0,0 +1,10 @@ +package com.retrip.map.infra.adapter.`in`.event + +import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDocument +import java.util.UUID + +sealed class LocationElasticEvent { + data class Created(val document: LocationDocument) : LocationElasticEvent() + data class Updated(val id: UUID, val document: LocationDocument) : LocationElasticEvent() + data class Deleted(val id: UUID) : LocationElasticEvent() +} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/event/LocationElasticEventHandler.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/event/LocationElasticEventHandler.kt new file mode 100644 index 0000000..9b34187 --- /dev/null +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/event/LocationElasticEventHandler.kt @@ -0,0 +1,32 @@ +package com.retrip.map.infra.adapter.`in`.event + +import com.retrip.map.application.out.repository.LocationElasticRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import org.springframework.transaction.event.TransactionPhase +import org.springframework.transaction.event.TransactionalEventListener + +@Component +class LocationElasticEventHandler( + private val locationElasticRepository: LocationElasticRepository, +) { + private val log = LoggerFactory.getLogger(javaClass) + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + fun handle(event: LocationElasticEvent) { + try { + when (event) { + is LocationElasticEvent.Created -> + locationElasticRepository.save(event.document) + is LocationElasticEvent.Updated -> { + locationElasticRepository.deleteById(event.id) + locationElasticRepository.save(event.document) + } + is LocationElasticEvent.Deleted -> + locationElasticRepository.deleteById(event.id) + } + } catch (e: Exception) { + log.error("[ES Sync Failed] DB와 ES 동기화 실패. event={}", event, e) + } + } +} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/filter/AuthenticationFilter.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/filter/AuthenticationFilter.kt index 933ec50..110aebf 100644 --- a/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/filter/AuthenticationFilter.kt +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/filter/AuthenticationFilter.kt @@ -31,7 +31,8 @@ class AuthenticationFilter : OncePerRequestFilter() { path.contains("h2-console") || path.contains("status-check") || path.startsWith("/locations") || // 장소 등록 ADMIN - path.startsWith("/location-details") // 장소 상세 등록 ADMIN + path.startsWith("/location-details") || // 장소 상세 등록 ADMIN + (path.startsWith("/search/") && !path.contains("recent")) // 검색 API (최근 검색 제외) -> { filterChain.doFilter(request, response) return diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/resolver/UserContextArgumentResolver.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/resolver/UserContextArgumentResolver.kt index ddc0677..6477973 100644 --- a/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/resolver/UserContextArgumentResolver.kt +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/resolver/UserContextArgumentResolver.kt @@ -17,8 +17,8 @@ class UserContextArgumentResolver: HandlerMethodArgumentResolver { } override fun resolveArgument(parameter: MethodParameter, mavContainer: ModelAndViewContainer?, webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory?): UserContext { - val userContext = webRequest.getAttribute("userContext", RequestAttributes.SCOPE_REQUEST) as UserContext - return userContext + return webRequest.getAttribute("userContext", RequestAttributes.SCOPE_REQUEST) as? UserContext + ?: UserContext(memberId = null) } } diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationController.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationController.kt index 6e7c177..f04ec13 100644 --- a/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationController.kt +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationController.kt @@ -10,7 +10,6 @@ import com.retrip.map.infra.adapter.`in`.presentation.common.ApiResponse 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 @@ -26,7 +25,6 @@ import org.springframework.web.bind.annotation.RestController import java.util.* @RestController -@RequiredArgsConstructor @Tag(name = "LocationCRUD", description = "여행 지역 정보 등록,수정,삭제,조회 API 입니다.") @RequestMapping("/locations") class LocationController( diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationDetailController.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationDetailController.kt index 678c2c3..782860b 100644 --- a/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationDetailController.kt +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationDetailController.kt @@ -10,7 +10,6 @@ import com.retrip.map.infra.adapter.`in`.presentation.common.ApiResponse 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 @@ -26,7 +25,6 @@ import org.springframework.web.bind.annotation.RestController import java.util.* @RestController -@RequiredArgsConstructor @Tag(name = "LocationDetailCRUD", description = "여행 상세 지역 정보 등록,수정,삭제,조회 API 입니다.") @RequestMapping("/location-details") class LocationDetailController( diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationDetailSearchController.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationDetailSearchController.kt index d002ee9..a644a72 100644 --- a/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationDetailSearchController.kt +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationDetailSearchController.kt @@ -10,7 +10,6 @@ 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.tags.Tag -import lombok.RequiredArgsConstructor import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.web.PageableDefault @@ -22,7 +21,6 @@ import org.springframework.web.bind.annotation.RestController import java.util.* @RestController -@RequiredArgsConstructor @RequestMapping("search/location-details") @Tag(name = "LocationDetail", description = "여행 상세 지역 정보 조회용 API 입니다.") class LocationDetailSearchController( @@ -47,7 +45,7 @@ class LocationDetailSearchController( @Operation(summary = "최근 여행 상세 지역 조회", description = "최근 여행 상세 지역 조회 API 입니다.") fun getRecentLocationDetail( @WithUserContext context: UserContext - ): ApiResponse { + ): ApiResponse { val result = locationDetailRecentSearchUseCase.getRecentLocationDetail(context) return ApiResponse.ok(result) } @@ -57,7 +55,7 @@ class LocationDetailSearchController( fun deleteRecentLocationDetailsByKeyword( @WithUserContext context: UserContext, @RequestParam("keyword", required = false) keyword: String? - ): ApiResponse { + ): ApiResponse { locationDetailRecentSearchUseCase.deleteRecentLocationDetailsByKeyword(context, keyword) return ApiResponse.noContent() } diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationSearchController.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationSearchController.kt index 767ad21..9c66b96 100644 --- a/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationSearchController.kt +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/rest/LocationSearchController.kt @@ -11,11 +11,8 @@ 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.PageRequest import org.springframework.data.domain.Pageable -import org.springframework.data.domain.Sort import org.springframework.data.web.PageableDefault import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping @@ -24,7 +21,6 @@ import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController -@RequiredArgsConstructor @Tag(name = "Location", description = "여행 지역 정보 조회용 API 입니다.") @RequestMapping("search/locations") class LocationSearchController( @@ -49,18 +45,18 @@ class LocationSearchController( @Operation(summary = "최근 여행 지역 조회", description = "최근 여행 지역 조회 API 입니다.") fun getRecentLocation( @WithUserContext context: UserContext - ): ApiResponse { + ): ApiResponse { val result = locationRecentSearchUseCase.getRecentLocation(context) return ApiResponse.ok(result) } @DeleteMapping("recent") @Operation(summary = "최근 여행 지역 조회 제거", description = "최근 여행 지역 조회 제거 API 입니다.") - fun delectRecentLocationsByKeyword( + fun deleteRecentLocationsByKeyword( @WithUserContext context: UserContext, @RequestParam("keyword", required = false) keyword: String? - ): ApiResponse { - locationRecentSearchUseCase.delectRecentLocationsByKeyword(context, keyword) + ): ApiResponse { + locationRecentSearchUseCase.deleteRecentLocationsByKeyword(context, keyword) return ApiResponse.noContent() } diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/out/persistence/elasticsearch/query/LocationDetailsSearchElasticQueryRepository.kt b/src/main/kotlin/com/retrip/map/infra/adapter/out/persistence/elasticsearch/query/LocationDetailsSearchElasticQueryRepository.kt index e21129f..ca56a6d 100644 --- a/src/main/kotlin/com/retrip/map/infra/adapter/out/persistence/elasticsearch/query/LocationDetailsSearchElasticQueryRepository.kt +++ b/src/main/kotlin/com/retrip/map/infra/adapter/out/persistence/elasticsearch/query/LocationDetailsSearchElasticQueryRepository.kt @@ -1,25 +1,14 @@ package com.retrip.map.infra.adapter.out.persistence.elasticsearch.query import com.retrip.map.application.out.repository.LocationDetailSearchQueryRepository -import com.retrip.map.application.out.repository.LocationSearchQueryRepository -import com.retrip.map.infra.adapter.out.persistence.elasticsearch.convert.LocationDetailsQueryDocumentConvert.toDocument -import com.retrip.map.infra.adapter.out.persistence.elasticsearch.convert.LocationQueryDocumentConvert.toDocument -import com.retrip.map.infra.adapter.out.persistence.elasticsearch.query.dto.LocationDetailsQueryDocument -import com.retrip.map.infra.adapter.out.persistence.elasticsearch.query.dto.LocationQueryDocument import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDetailDocument -import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDocument -import jakarta.persistence.Id import org.springframework.data.domain.Page import org.springframework.data.domain.PageImpl import org.springframework.data.domain.Pageable -import org.springframework.data.elasticsearch.annotations.Field -import org.springframework.data.elasticsearch.annotations.FieldType import org.springframework.stereotype.Repository import org.springframework.data.elasticsearch.client.elc.NativeQuery import org.springframework.data.elasticsearch.core.ElasticsearchOperations import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates -import org.springframework.data.elasticsearch.core.query.FetchSourceFilter -import java.time.Instant import java.util.UUID @@ -30,40 +19,24 @@ class LocationDetailsSearchElasticQueryRepository( override fun findByLocationIdAndSearchText(locationId: UUID?, searchText: String?, pageable: Pageable): Page { val queryBuilder = NativeQuery.builder() .withPageable(pageable) - .withSourceFilter(FetchSourceFilter(false, emptyArray(), arrayOf("*"))) - .withFields( - "id", - "name", - "searchText", - "category", - "description", - "telephone", - "address", - "roadAddress", - "latitude", - "longitude", - "locationId", - "createdAt", - "editedAt", - ) + if (!searchText.isNullOrBlank()) { queryBuilder.withQuery { q -> q.match { m -> m.field("searchText").query(searchText) } } } - locationId?.run { + locationId?.let { queryBuilder.withFilter { f -> - f.term { t -> t.field("locationId").value(this.toString()) } + f.term { t -> t.field("locationId").value(it.toString()) } } } val searchHits = elasticsearchOperations.search( queryBuilder.build(), - LocationDetailsQueryDocument::class.java, + LocationDetailDocument::class.java, IndexCoordinates.of("location-details") ) - val content = searchHits.searchHits.map { it.content.toDocument() } - return PageImpl(content, pageable, searchHits.totalHits) + return PageImpl(searchHits.searchHits.map { it.content }, pageable, searchHits.totalHits) } } diff --git a/src/main/kotlin/com/retrip/map/infra/config/ElasticsearchConfig.kt b/src/main/kotlin/com/retrip/map/infra/config/ElasticsearchConfig.kt index d2f8473..0b3e9f6 100644 --- a/src/main/kotlin/com/retrip/map/infra/config/ElasticsearchConfig.kt +++ b/src/main/kotlin/com/retrip/map/infra/config/ElasticsearchConfig.kt @@ -9,11 +9,16 @@ import org.apache.http.auth.UsernamePasswordCredentials import org.apache.http.impl.client.BasicCredentialsProvider import org.apache.http.ssl.SSLContexts import org.elasticsearch.client.RestClient +import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration -class ElasticsearchConfig { +class ElasticsearchConfig( + @Value("\${elasticsearch.host:172.31.60.74}") private val host: String, + @Value("\${elasticsearch.port:9200}") private val port: Int, + @Value("\${elasticsearch.scheme:https}") private val scheme: String, +) { @Bean fun elasticsearchClient(): ElasticsearchClient { val credentialsProvider = BasicCredentialsProvider() @@ -23,10 +28,10 @@ class ElasticsearchConfig { ) val sslContext = SSLContexts.custom() - .loadTrustMaterial(null) { _, _ -> true } // 개발용 + .loadTrustMaterial(null) { _, _ -> true } .build() - val restClient = RestClient.builder(HttpHost("43.203.108.129", 9200, "http")) + val restClient = RestClient.builder(HttpHost(host, port, scheme)) .setHttpClientConfigCallback { httpClientBuilder -> httpClientBuilder .setSSLContext(sslContext) diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..f03c9a4 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,19 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:map;MODE=MySQL;DB_CLOSE_DELAY=-1 + username: sa + password: + h2: + console: + enabled: true + path: /h2-console + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: create + +elasticsearch: + host: 43.203.150.52 + port: 9200 + scheme: http diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index f751e65..bdd2e65 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -9,3 +9,8 @@ spring: database-platform: org.hibernate.dialect.MySQLDialect hibernate: ddl-auto: update + +elasticsearch: + host: 172.31.60.74 + port: 9200 + scheme: https diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2b4bb9b..4ef6baf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,17 +1,14 @@ spring: + profiles: + active: local application: name: map ############# Data Source ###### datasource: - driver-class-name: org.h2.Driver - #url: jdbc:h2:file:~/map;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;MODE=MySQL - url: jdbc:h2:tcp://localhost/~/Desktop/db/map;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;MODE=MySQL - username: sa - password: - h2: - console: - enabled: true - path: /h2-console + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${DB_HOST:172.31.46.83}:3306/map?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + username: "map_user" + password: "map_password" ############# Batch ############ batch: job: @@ -21,8 +18,9 @@ spring: initialize-schema: always jpa: show-sql: true + database-platform: org.hibernate.dialect.MySQLDialect hibernate: - ddl-auto: create + ddl-auto: update #logging: # level: # 1. Spring Data ES의 최상위 로그