From fa5e39b95e2821bebec4fc20e6f2f930780e8e4b Mon Sep 17 00:00:00 2001 From: junhokim Date: Sun, 1 Feb 2026 23:52:38 +0900 Subject: [PATCH] feat: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상세 지역 검색 추가 --- .../in/request/LocationDetailCreateRequest.kt | 5 +- .../response/LocationDetailSearchResponse.kt | 30 ++++++++++ ...kt => LocationDetailDetailIndexService.kt} | 6 +- .../in/service/LocationDetailSearchService.kt | 38 ++++++++++++ .../in/service/LocationDetailService.kt | 58 +++++++++++++------ .../in/service/LocationSearchService.kt | 2 +- .../application/in/service/LocationService.kt | 16 +++-- ...eCase.kt => LocationDetailIndexUseCase.kt} | 2 +- .../in/usecase/LocationDetailSearchUseCase.kt | 10 ++++ .../in/usecase/LocationDetailUseCase.kt | 8 +-- .../LocationDetailElasticRepository.kt | 6 ++ .../LocationDetailQueryRepository.kt | 6 +- .../repository/LocationDetailRepository.kt | 1 + .../repository/LocationElasticRepository.kt | 2 +- .../map/domain/entity/LocationDetail.kt | 19 ++++++ .../LocationDetailDuplicateException.kt | 12 ++++ .../map/domain/exception/common/ErrorCode.kt | 3 +- .../map/infra/adapter/in/batch/MapWriter.kt | 4 +- .../presentation/LocationDetailController.kt | 15 +++-- .../LocationDetailSearchController.kt | 36 ++++++++++++ .../query/LocationDetailQuerydslRepository.kt | 13 +++-- .../entity/LocationDetailDocument.kt | 10 +++- .../elasticsearch/entity/LocationDocument.kt | 8 +-- .../map/infra/config/ElasticsearchConfig.kt | 2 +- .../mappings/mapping-location-detail.json | 5 ++ .../mappings/mapping-location.json | 6 +- 26 files changed, 259 insertions(+), 64 deletions(-) create mode 100644 src/main/kotlin/com/retrip/map/application/in/response/LocationDetailSearchResponse.kt rename src/main/kotlin/com/retrip/map/application/in/service/{LocationDetailIndexService.kt => LocationDetailDetailIndexService.kt} (80%) create mode 100644 src/main/kotlin/com/retrip/map/application/in/service/LocationDetailSearchService.kt rename src/main/kotlin/com/retrip/map/application/in/usecase/{LocationIndexUseCase.kt => LocationDetailIndexUseCase.kt} (85%) create mode 100644 src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailSearchUseCase.kt create mode 100644 src/main/kotlin/com/retrip/map/domain/exception/LocationDetailDuplicateException.kt create mode 100644 src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/LocationDetailSearchController.kt diff --git a/src/main/kotlin/com/retrip/map/application/in/request/LocationDetailCreateRequest.kt b/src/main/kotlin/com/retrip/map/application/in/request/LocationDetailCreateRequest.kt index 88eb59f..65f93c1 100644 --- a/src/main/kotlin/com/retrip/map/application/in/request/LocationDetailCreateRequest.kt +++ b/src/main/kotlin/com/retrip/map/application/in/request/LocationDetailCreateRequest.kt @@ -1,7 +1,9 @@ package com.retrip.map.application.`in`.request +import com.retrip.map.domain.entity.Location import com.retrip.map.domain.entity.LocationDetail import io.swagger.v3.oas.annotations.media.Schema +import java.util.UUID @Schema(description = "장소 상세 생성 Request") data class LocationDetailCreateRequest( @@ -14,8 +16,9 @@ data class LocationDetailCreateRequest( val latitude: Double, val longitude: Double, ) { - fun to(): LocationDetail { + fun to(location: Location): LocationDetail { return LocationDetail.create( + location, name, category, description, diff --git a/src/main/kotlin/com/retrip/map/application/in/response/LocationDetailSearchResponse.kt b/src/main/kotlin/com/retrip/map/application/in/response/LocationDetailSearchResponse.kt new file mode 100644 index 0000000..a9b0be4 --- /dev/null +++ b/src/main/kotlin/com/retrip/map/application/in/response/LocationDetailSearchResponse.kt @@ -0,0 +1,30 @@ +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 LocationDetailSearchResponse( + @Schema(description = "장소 상세 id") + val id: UUID, + @Schema(description = "장소 id") + val locationId: UUID, + @Schema(description = "장소 상세명") + val name: String, + @Schema(description = "장소 상세 카테고리") + val category: String, + @Schema(description = "장소 상세 설명") + val description: String?, + @Schema(description = "장소 상세 전화번호") + val telephone: String?, + @Schema(description = "장소 상세 주소") + val address: String?, + @Schema(description = "장소 상세 도로명 주소") + val roadAddress: String?, + @Schema(description = "장소 상세 위도") + val latitude: Double?, + @Schema(description = "장소 상세 경도") + val longitude: Double?, +) { +} diff --git a/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailIndexService.kt b/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailDetailIndexService.kt similarity index 80% rename from src/main/kotlin/com/retrip/map/application/in/service/LocationDetailIndexService.kt rename to src/main/kotlin/com/retrip/map/application/in/service/LocationDetailDetailIndexService.kt index a891712..6870f26 100644 --- a/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailIndexService.kt +++ b/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailDetailIndexService.kt @@ -1,6 +1,6 @@ package com.retrip.map.application.`in`.service -import com.retrip.map.application.`in`.usecase.LocationIndexUseCase +import com.retrip.map.application.`in`.usecase.LocationDetailIndexUseCase import com.retrip.map.application.out.repository.LocationDetailElasticRepository import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDetailDocument import org.springframework.stereotype.Service @@ -8,9 +8,9 @@ import org.springframework.transaction.annotation.Transactional @Service @Transactional -class LocationDetailIndexService( +class LocationDetailDetailIndexService( private val locationDetailElasticRepository: LocationDetailElasticRepository -) : LocationIndexUseCase { +) : LocationDetailIndexUseCase { override fun indexLocationDetailDocuments(documents: List?) { documents?.let { locationDetailElasticRepository.saveAll(it) } } 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 new file mode 100644 index 0000000..0962b9c --- /dev/null +++ b/src/main/kotlin/com/retrip/map/application/in/service/LocationDetailSearchService.kt @@ -0,0 +1,38 @@ +package com.retrip.map.application.`in`.service + +import com.retrip.map.application.`in`.response.LocationDetailSearchResponse +import com.retrip.map.application.`in`.usecase.LocationDetailSearchUseCase +import com.retrip.map.application.out.repository.LocationDetailElasticRepository +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.util.UUID + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +class LocationDetailSearchService( + private val locationDetailElasticRepository: LocationDetailElasticRepository +) : LocationDetailSearchUseCase { + + override fun getDetailLocation(locationId: UUID?, searchText: String?, page: Pageable): Page { + val locationDetails = locationDetailElasticRepository.findByLocationIdAndSearchText(locationId, searchText, page) + return locationDetails.map { + LocationDetailSearchResponse( + id = it.id, + locationId = it.locationId, + name = it.name, + category = it.category, + description = it.description, + telephone = it.telephone, + address = it.address, + roadAddress = it.roadAddress, + latitude = it.latitude, + longitude = it.longitude, + ) + } + } +} + 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 44163a3..a8872d6 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 @@ -1,19 +1,24 @@ package com.retrip.map.application.`in`.service -import co.elastic.clients.elasticsearch.core.search.ContextBuilders.location import com.retrip.map.application.`in`.request.LocationDetailCreateRequest import com.retrip.map.application.`in`.request.LocationDetailUpdateRequest 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 @@ -23,17 +28,25 @@ import java.util.* @RequiredArgsConstructor @Transactional class LocationDetailService( + val locationRepository: LocationRepository, val locationDetailRepository: LocationDetailRepository, - val locationDetailQueryRepository: LocationDetailQueryRepository + val locationDetailQueryRepository: LocationDetailQueryRepository, + val locationDetailElasticRepository: LocationDetailElasticRepository, ) : LocationDetailUseCase { @Transactional(readOnly = true) - override fun getLocationDetail(id: UUID?, page: Pageable): Page { - return locationDetailQueryRepository.findLocationDetails(id, page) + override fun getLocationDetail(locationId: UUID, id: UUID?, page: Pageable): Page { + return locationDetailQueryRepository.findLocationDetails(locationId, id, page) } - override fun createLocationDetail(request: LocationDetailCreateRequest): LocationDetailCreateResponse { - val locationDetail = locationDetailRepository.save(request.to()) + 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() } + + val locationDetail = locationDetailRepository.save(request.to(location)) + locationDetailElasticRepository.save(LocationDetailDocument.of(locationDetail)) return LocationDetailCreateResponse( locationDetail.id ?: throw LocationDetailNotFoundException(), locationDetail.name?.value ?: throw RequireException(), @@ -47,9 +60,14 @@ class LocationDetailService( ) } - override fun updateLocationDetail(id: UUID, request: LocationDetailUpdateRequest): LocationDetailUpdateResponse { - val location = locationDetailRepository.findByIdOrNull(id) ?: throw LocationDetailNotFoundException() - location.update( + 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() } + val locationDetails = locationDetailRepository.findByIdOrNull(id) ?: throw LocationDetailNotFoundException() + locationDetails.update( + location, request.name, request.category, request.description, @@ -59,21 +77,25 @@ class LocationDetailService( request.latitude, request.longitude, ) + locationDetailElasticRepository.deleteById(id) + locationDetailElasticRepository.save(LocationDetailDocument.of(locationDetails)) return LocationDetailUpdateResponse( - location.id ?: throw LocationDetailNotFoundException(), - location.name?.value ?: throw RequireException(), - location.category?.value ?: throw RequireException(), - location.description?.value, - location.telephone, - location.address?.address, - location.address?.roadAddress, - location.geoPoint?.latitude, - location.geoPoint?.longitude, + locationDetails.id ?: throw LocationDetailNotFoundException(), + locationDetails.name?.value ?: throw RequireException(), + locationDetails.category?.value ?: throw RequireException(), + locationDetails.description?.value, + locationDetails.telephone, + locationDetails.address?.address, + locationDetails.address?.roadAddress, + locationDetails.geoPoint?.latitude, + locationDetails.geoPoint?.longitude, ) } override fun deleteLocationDetail(locationDetailId: UUID) { locationDetailRepository.deleteById(locationDetailId) + locationDetailElasticRepository.deleteById(locationDetailId) + } } 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 29f013c..68303a4 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 @@ -18,7 +18,7 @@ class LocationSearchService( @Transactional(readOnly = true) override fun getLocation(searchText: String?, page: Pageable): Page { - val locations = locationElasticRepository.findBySearchTextContaining(searchText, page) + val locations = locationElasticRepository.findBySearchText(searchText, page) return locations.map { LocationSearchResponse( id = it.id, 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 9f5594c..a5ef5b1 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 @@ -20,7 +20,7 @@ import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import java.util.UUID +import java.util.* @Service @RequiredArgsConstructor @@ -39,11 +39,10 @@ class LocationService( override fun createLocation(request: LocationCreateRequest): LocationCreateResponse { val isDuplicate = - locationRepository.findFirstByNameValueAndCountryValue(request.name, request.country) + locationRepository.findFirstByNameValueAndCountryValue(request.name, request.country)?.run { + throw LocationDuplicateException() + } - if(isDuplicate != null){ - throw LocationDuplicateException() - } val location = locationRepository.save(request.to()) locationElasticRepository.save(LocationDocument.of(location)) return LocationCreateResponse( @@ -57,10 +56,9 @@ class LocationService( override fun updateLocation(id: UUID, request: LocationUpdateRequest): LocationUpdateResponse { val isDuplicate = - locationRepository.findFirstByNameValueAndCountryValue(request.name, request.country) - if(isDuplicate != null){ - throw LocationDuplicateException() - } + locationRepository.findFirstByNameValueAndCountryValue(request.name, request.country)?.run { + throw LocationDuplicateException() + } val location = locationRepository.findByIdOrNull(id) ?: throw LocationNotFoundException() locationElasticRepository.deleteById(id) diff --git a/src/main/kotlin/com/retrip/map/application/in/usecase/LocationIndexUseCase.kt b/src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailIndexUseCase.kt similarity index 85% rename from src/main/kotlin/com/retrip/map/application/in/usecase/LocationIndexUseCase.kt rename to src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailIndexUseCase.kt index 3a83821..b30d133 100644 --- a/src/main/kotlin/com/retrip/map/application/in/usecase/LocationIndexUseCase.kt +++ b/src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailIndexUseCase.kt @@ -2,6 +2,6 @@ package com.retrip.map.application.`in`.usecase import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDetailDocument -interface LocationIndexUseCase { +interface LocationDetailIndexUseCase { fun indexLocationDetailDocuments(documents: List?) } diff --git a/src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailSearchUseCase.kt b/src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailSearchUseCase.kt new file mode 100644 index 0000000..2c5d57b --- /dev/null +++ b/src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailSearchUseCase.kt @@ -0,0 +1,10 @@ +package com.retrip.map.application.`in`.usecase + +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 +} diff --git a/src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailUseCase.kt b/src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailUseCase.kt index 46923da..05835d1 100644 --- a/src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailUseCase.kt +++ b/src/main/kotlin/com/retrip/map/application/in/usecase/LocationDetailUseCase.kt @@ -7,11 +7,11 @@ import com.retrip.map.application.`in`.response.LocationDetailResponse import com.retrip.map.application.`in`.response.LocationDetailUpdateResponse import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable -import java.util.UUID +import java.util.* interface LocationDetailUseCase { - fun createLocationDetail(request: LocationDetailCreateRequest): LocationDetailCreateResponse - fun updateLocationDetail(id: UUID, request: LocationDetailUpdateRequest): LocationDetailUpdateResponse + fun createLocationDetail(locationId: UUID, request: LocationDetailCreateRequest): LocationDetailCreateResponse + fun updateLocationDetail(locationId: UUID, id: UUID, request: LocationDetailUpdateRequest): LocationDetailUpdateResponse fun deleteLocationDetail(locationDetailId: UUID) - fun getLocationDetail(id: UUID?, page: Pageable): Page + fun getLocationDetail(locationId: UUID, id: UUID?, page: Pageable): Page } diff --git a/src/main/kotlin/com/retrip/map/application/out/repository/LocationDetailElasticRepository.kt b/src/main/kotlin/com/retrip/map/application/out/repository/LocationDetailElasticRepository.kt index c9a0d9f..3a4703c 100644 --- a/src/main/kotlin/com/retrip/map/application/out/repository/LocationDetailElasticRepository.kt +++ b/src/main/kotlin/com/retrip/map/application/out/repository/LocationDetailElasticRepository.kt @@ -1,6 +1,10 @@ package com.retrip.map.application.out.repository +import com.retrip.map.application.`in`.response.LocationDetailResponse import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDetailDocument +import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDocument +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.elasticsearch.repository.ElasticsearchRepository import org.springframework.stereotype.Repository import java.util.UUID @@ -8,4 +12,6 @@ import java.util.UUID @Repository interface LocationDetailElasticRepository: ElasticsearchRepository { fun findFirstByOrderByEditedAtDesc(): LocationDetailDocument? + fun findByLocationIdAndSearchText(locationId: UUID?, name: String?, pageable: Pageable): Page + } diff --git a/src/main/kotlin/com/retrip/map/application/out/repository/LocationDetailQueryRepository.kt b/src/main/kotlin/com/retrip/map/application/out/repository/LocationDetailQueryRepository.kt index 7930f54..893c3b4 100644 --- a/src/main/kotlin/com/retrip/map/application/out/repository/LocationDetailQueryRepository.kt +++ b/src/main/kotlin/com/retrip/map/application/out/repository/LocationDetailQueryRepository.kt @@ -5,9 +5,9 @@ import com.retrip.map.domain.entity.LocationDetail import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import java.time.LocalDateTime -import java.util.UUID +import java.util.* interface LocationDetailQueryRepository { - fun findLocationDetails(id: UUID?, page: Pageable): Page - fun findLocationDetailsByEditedAt( editedAt: LocalDateTime): List + fun findLocationDetails(locationId: UUID, id: UUID?, page: Pageable): Page + fun findLocationDetailsByEditedAt(editedAt: LocalDateTime): List } 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 a95de9f..3bcd5ca 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 @@ -5,4 +5,5 @@ import org.springframework.data.jpa.repository.JpaRepository import java.util.UUID interface LocationDetailRepository: JpaRepository { + fun findByNameValueAndLocationId(name: String, locationId: UUID): LocationDetail? } diff --git a/src/main/kotlin/com/retrip/map/application/out/repository/LocationElasticRepository.kt b/src/main/kotlin/com/retrip/map/application/out/repository/LocationElasticRepository.kt index 358d646..fd6a9cb 100644 --- a/src/main/kotlin/com/retrip/map/application/out/repository/LocationElasticRepository.kt +++ b/src/main/kotlin/com/retrip/map/application/out/repository/LocationElasticRepository.kt @@ -10,5 +10,5 @@ import java.util.UUID @Repository interface LocationElasticRepository: ElasticsearchRepository { // 이름 포함 검색 + 페이징 - fun findBySearchTextContaining(searchText: String?, pageable: Pageable): Page + fun findBySearchText(searchText: String?, pageable: Pageable): Page } diff --git a/src/main/kotlin/com/retrip/map/domain/entity/LocationDetail.kt b/src/main/kotlin/com/retrip/map/domain/entity/LocationDetail.kt index e7c01cb..1caf8e8 100644 --- a/src/main/kotlin/com/retrip/map/domain/entity/LocationDetail.kt +++ b/src/main/kotlin/com/retrip/map/domain/entity/LocationDetail.kt @@ -5,10 +5,16 @@ import com.retrip.map.domain.vo.LocationDetailCategory import com.retrip.map.domain.vo.LocationDetailDescription import com.retrip.map.domain.vo.LocationDetailGeoPoint import com.retrip.map.domain.vo.LocationDetailName +import jakarta.persistence.CascadeType import jakarta.persistence.Column import jakarta.persistence.Embedded import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.ForeignKey import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany import jakarta.persistence.Table import jakarta.persistence.Version import lombok.AccessLevel @@ -43,10 +49,20 @@ class LocationDetail( @Embedded var geoPoint: LocationDetailGeoPoint? = null, + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn( + name = "location_id", + nullable = false, + columnDefinition = "varbinary(16)", + foreignKey = ForeignKey(name = "fk_location_details_to_location") + ) + var location: Location? = null, + @Version private val version: Long? = null, ): BaseEntity() { fun update( + location: Location, name: String, category: String, description: String?, @@ -56,6 +72,7 @@ class LocationDetail( latitude: Double, longitude: Double ) { + this.location = location this.name = LocationDetailName(name) this.category = LocationDetailCategory(category) this.description = LocationDetailDescription(description) @@ -66,6 +83,7 @@ class LocationDetail( companion object { fun create( + location: Location, name: String, category: String, description: String?, @@ -77,6 +95,7 @@ class LocationDetail( ): LocationDetail { return LocationDetail( id = UUID.randomUUID(), + location = location, name = LocationDetailName(name), category = LocationDetailCategory(category), description = LocationDetailDescription(description), diff --git a/src/main/kotlin/com/retrip/map/domain/exception/LocationDetailDuplicateException.kt b/src/main/kotlin/com/retrip/map/domain/exception/LocationDetailDuplicateException.kt new file mode 100644 index 0000000..849cd0d --- /dev/null +++ b/src/main/kotlin/com/retrip/map/domain/exception/LocationDetailDuplicateException.kt @@ -0,0 +1,12 @@ +package com.retrip.map.domain.exception + +import com.retrip.map.domain.exception.common.BusinessException +import com.retrip.map.domain.exception.common.ErrorCode + +class LocationDetailDuplicateException : BusinessException { + constructor(): super(ErrorCode.LOCATION_DETAILS_DUPLICATION) {} + constructor(errorCode: ErrorCode): super(errorCode) {} + constructor(message: String): super(ErrorCode.LOCATION_DETAILS_DUPLICATION, message) {} + constructor(errorCode: ErrorCode, message: String): super(errorCode, message) {} + +} diff --git a/src/main/kotlin/com/retrip/map/domain/exception/common/ErrorCode.kt b/src/main/kotlin/com/retrip/map/domain/exception/common/ErrorCode.kt index c535b13..29967f4 100644 --- a/src/main/kotlin/com/retrip/map/domain/exception/common/ErrorCode.kt +++ b/src/main/kotlin/com/retrip/map/domain/exception/common/ErrorCode.kt @@ -18,5 +18,6 @@ enum class ErrorCode( LOCATION_NOT_FOUND(BAD_REQUEST, "Location-001", "로케이션 엔티티를 찾을 수 없습니다."), LOCATION_DETAIL_NOT_FOUND(BAD_REQUEST, "Location-002", "로케이션 디테일 엔티티를 찾을 수 없습니다."), - LOCATION_DUPLICATION(INTERNAL_SERVER_ERROR, "Location-003", "중복된 지역과 찾을 수 없습니다."), + LOCATION_DUPLICATION(INTERNAL_SERVER_ERROR, "Location-003", "지역이 중복됩니다."), + LOCATION_DETAILS_DUPLICATION(INTERNAL_SERVER_ERROR, "Location-004", "상세 지역이 중복됩니다."), } 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 index ae57c61..8b28911 100644 --- 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 @@ -1,6 +1,6 @@ package com.retrip.map.infra.adapter.`in`.batch -import com.retrip.map.application.`in`.usecase.LocationIndexUseCase +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 @@ -8,7 +8,7 @@ import org.springframework.stereotype.Component @Component class MapWriter( - val locationIndexUseCase: LocationIndexUseCase + 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/presentation/LocationDetailController.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/LocationDetailController.kt index ee04bf5..f2e8965 100644 --- a/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/LocationDetailController.kt +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/LocationDetailController.kt @@ -30,32 +30,35 @@ class LocationDetailController( private val locationDetailUseCase: LocationDetailUseCase ) { - @GetMapping("") + @GetMapping("/{locationId}") @Schema(description = "장소 상세 전체 조회") fun getLocationDetail( + @PathVariable locationId: UUID, @RequestParam(name = "locationDetailId") id: UUID?, @PageableDefault(size = 10, page = 0) page: Pageable ): ApiResponse> { - val result = locationDetailUseCase.getLocationDetail(id, page) + val result = locationDetailUseCase.getLocationDetail(locationId, id, page) return ApiResponse.ok(result) } - @PostMapping("") + @PostMapping("/{locationId}") @Schema(description = "장소 상세 등록") fun createLocationDetail( + @PathVariable locationId: UUID, @RequestBody request: LocationDetailCreateRequest ): ApiResponse { - val result = locationDetailUseCase.createLocationDetail(request) + val result = locationDetailUseCase.createLocationDetail(locationId, request) return ApiResponse.create(result) } - @PutMapping("/{locationDetailId}") + @PutMapping("/{locationId}/{locationDetailId}") @Schema(description = "장소 상세 수정") fun updateLocationDetail( + @PathVariable locationId: UUID, @PathVariable locationDetailId: UUID, @RequestBody request: LocationDetailUpdateRequest ): ApiResponse { - val result = locationDetailUseCase.updateLocationDetail(locationDetailId, request) + val result = locationDetailUseCase.updateLocationDetail(locationId, locationDetailId, request) return ApiResponse.ok(result) } diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/LocationDetailSearchController.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/LocationDetailSearchController.kt new file mode 100644 index 0000000..89f3493 --- /dev/null +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/presentation/LocationDetailSearchController.kt @@ -0,0 +1,36 @@ +package com.retrip.map.infra.adapter.`in`.presentation + +import com.retrip.map.application.`in`.response.LocationDetailSearchResponse +import com.retrip.map.application.`in`.response.LocationSearchResponse +import com.retrip.map.application.`in`.usecase.LocationDetailSearchUseCase +import com.retrip.map.application.`in`.usecase.LocationSearchUseCase +import com.retrip.map.infra.adapter.`in`.presentation.common.ApiResponse +import io.swagger.v3.oas.annotations.media.Schema +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.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +@RequiredArgsConstructor +@RequestMapping("/location-detail-search") +class LocationDetailSearchController( + private val locationDetailSearchUseCase: LocationDetailSearchUseCase +) { + + @GetMapping("") + @Schema(description = "장소 검색 엔진 조회") + fun getLocation( + @PageableDefault(size = 10, page = 0) page: Pageable, + @RequestParam(name = "locationId", required = false) locationId: UUID?, + @RequestParam(name = "searchText", required = false) searchText: String? + ): ApiResponse> { + val result = locationDetailSearchUseCase.getDetailLocation(locationId, searchText, page) + return ApiResponse.ok(result) + } +} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/out/persistence/mysql/query/LocationDetailQuerydslRepository.kt b/src/main/kotlin/com/retrip/map/infra/adapter/out/persistence/mysql/query/LocationDetailQuerydslRepository.kt index 8641dff..d135976 100644 --- a/src/main/kotlin/com/retrip/map/infra/adapter/out/persistence/mysql/query/LocationDetailQuerydslRepository.kt +++ b/src/main/kotlin/com/retrip/map/infra/adapter/out/persistence/mysql/query/LocationDetailQuerydslRepository.kt @@ -6,10 +6,12 @@ import com.querydsl.jpa.impl.JPAQueryFactory import com.retrip.map.application.`in`.response.LocationDetailResponse import com.retrip.map.application.out.repository.LocationDetailQueryRepository import com.retrip.map.domain.entity.LocationDetail +import com.retrip.map.domain.entity.QLocation.location import com.retrip.map.domain.entity.QLocationDetail.locationDetail import org.springframework.data.domain.Page import org.springframework.data.domain.PageImpl import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.domain.AbstractPersistable_.id import org.springframework.stereotype.Repository import java.time.LocalDateTime import java.util.* @@ -21,7 +23,7 @@ class LocationDetailQuerydslRepository( ) : LocationDetailQueryRepository { - override fun findLocationDetails(id: UUID?, page: Pageable): Page { + override fun findLocationDetails(locationId: UUID, id: UUID?, page: Pageable): Page { val locationDetails = query.select( Projections.constructor( LocationDetailResponse::class.java, @@ -37,7 +39,7 @@ class LocationDetailQuerydslRepository( ) ).from(locationDetail) .where( - eqLocation(id) + eqLocationDetail(id), eqLocation(locationId) ) .offset(page.offset) .limit(page.pageSize.toLong()) @@ -60,9 +62,10 @@ class LocationDetailQuerydslRepository( ).fetch() } - - - private fun eqLocation(id: UUID?): Predicate? { + private fun eqLocation(locationId: UUID?): Predicate? { + return locationId?.let { locationDetail.location.id.eq(it) } + } + private fun eqLocationDetail(id: UUID?): Predicate? { return id?.let { locationDetail.id.eq(it) } } } diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/out/search/elasticsearch/entity/LocationDetailDocument.kt b/src/main/kotlin/com/retrip/map/infra/adapter/out/search/elasticsearch/entity/LocationDetailDocument.kt index f23d556..cdb69e5 100644 --- a/src/main/kotlin/com/retrip/map/infra/adapter/out/search/elasticsearch/entity/LocationDetailDocument.kt +++ b/src/main/kotlin/com/retrip/map/infra/adapter/out/search/elasticsearch/entity/LocationDetailDocument.kt @@ -2,6 +2,7 @@ package com.retrip.map.infra.adapter.out.search.elasticsearch.entity import com.retrip.map.domain.entity.LocationDetail import com.retrip.map.domain.exception.LocationDetailNotFoundException +import com.retrip.map.domain.exception.LocationNotFoundException import com.retrip.map.domain.exception.common.RequireException import jakarta.persistence.Id import org.springframework.data.elasticsearch.annotations.Document @@ -17,11 +18,12 @@ import java.util.* @Setting(settingPath = "elasticsearch/settings/setting.json") @Mapping(mappingPath = "elasticsearch/mappings/mapping-location-detail.json") data class LocationDetailDocument( - @Id val id: UUID, - @Field(type = FieldType.Text, analyzer = "korean") + @Field(type = FieldType.Text, analyzer = "korean", copyTo = ["searchText"]) val name: String, + @Field(type = FieldType.Text, analyzer = "korean") + val searchText: String? = null, //검색시에만 사용 @Field(type = FieldType.Keyword) val category: String, @Field(type = FieldType.Keyword) @@ -36,6 +38,8 @@ data class LocationDetailDocument( val latitude: Double?, @Field(type = FieldType.Double) val longitude: Double?, + @Field(type = FieldType.Keyword) + val locationId: UUID, @Field(type = FieldType.Date) val createdAt: Long? = null, @Field(type = FieldType.Date) @@ -46,6 +50,7 @@ data class LocationDetailDocument( return LocationDetailDocument( locationDetail.id ?: throw LocationDetailNotFoundException(), locationDetail.name?.value ?: throw RequireException(), + searchText = null, locationDetail.category?.value ?: throw RequireException(), locationDetail.description?.value, locationDetail.telephone, @@ -53,6 +58,7 @@ data class LocationDetailDocument( locationDetail.address?.roadAddress, locationDetail.geoPoint?.latitude, locationDetail.geoPoint?.longitude, + locationDetail.location?.id ?: throw LocationNotFoundException(), locationDetail.createdAt?.toInstant(ZoneOffset.UTC)?.toEpochMilli(), locationDetail.editedAt?.toInstant(ZoneOffset.UTC)?.toEpochMilli() ) diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/out/search/elasticsearch/entity/LocationDocument.kt b/src/main/kotlin/com/retrip/map/infra/adapter/out/search/elasticsearch/entity/LocationDocument.kt index 75fed60..2786c58 100644 --- a/src/main/kotlin/com/retrip/map/infra/adapter/out/search/elasticsearch/entity/LocationDocument.kt +++ b/src/main/kotlin/com/retrip/map/infra/adapter/out/search/elasticsearch/entity/LocationDocument.kt @@ -18,12 +18,12 @@ import java.util.* data class LocationDocument( @Id val id: UUID, - @Field(type = FieldType.Text, analyzer = "korean") + @Field(type = FieldType.Text, analyzer = "korean", copyTo = ["searchText"]) val name: String, - @Field(type = FieldType.Text, analyzer = "korean") + @Field(type = FieldType.Text, analyzer = "korean", copyTo = ["searchText"]) val country: String, @Field(type = FieldType.Text, analyzer = "korean") - val searchText: String, + val searchText: String? = null, //검색시에만 사용 @Field(type = FieldType.Double) val latitude: Double?, @Field(type = FieldType.Double) @@ -39,7 +39,7 @@ data class LocationDocument( location.id ?: throw LocationNotFoundException(), location.name?.value ?: throw RequireException(), location.country?.value ?: throw RequireException(), - "${location.country?.value} + ${location.name?.value}", + null, location.geoPoint?.latitude, location.geoPoint?.longitude, location.createdAt?.toInstant(ZoneOffset.UTC)?.toEpochMilli(), 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 d2d259b..557512b 100644 --- a/src/main/kotlin/com/retrip/map/infra/config/ElasticsearchConfig.kt +++ b/src/main/kotlin/com/retrip/map/infra/config/ElasticsearchConfig.kt @@ -26,7 +26,7 @@ class ElasticsearchConfig { .loadTrustMaterial(null) { _, _ -> true } // 개발용 .build() - val restClient = RestClient.builder(HttpHost("172.31.60.74", 9200, "https")) + val restClient = RestClient.builder(HttpHost("localhost", 9200, "https")) .setHttpClientConfigCallback { httpClientBuilder -> httpClientBuilder .setSSLContext(sslContext) diff --git a/src/main/resources/elasticsearch/mappings/mapping-location-detail.json b/src/main/resources/elasticsearch/mappings/mapping-location-detail.json index 47a50d5..868e433 100644 --- a/src/main/resources/elasticsearch/mappings/mapping-location-detail.json +++ b/src/main/resources/elasticsearch/mappings/mapping-location-detail.json @@ -4,6 +4,11 @@ "type": "keyword" }, "name": { + "type": "text", + "analyzer": "korean", + "copy_to": ["searchText"] + }, + "searchText": { "type": "text", "analyzer": "korean" }, diff --git a/src/main/resources/elasticsearch/mappings/mapping-location.json b/src/main/resources/elasticsearch/mappings/mapping-location.json index 956aa5d..8c203ee 100644 --- a/src/main/resources/elasticsearch/mappings/mapping-location.json +++ b/src/main/resources/elasticsearch/mappings/mapping-location.json @@ -5,11 +5,13 @@ }, "name": { "type": "text", - "analyzer": "korean" + "analyzer": "korean", + "copy_to": ["searchText"] }, "country": { "type": "text", - "analyzer": "korean" + "analyzer": "korean", + "copy_to": ["searchText"] }, "searchText": { "type": "text",