Skip to content

Commit

Permalink
feat: 통합 검색 기능 구현 (#65)
Browse files Browse the repository at this point in the history
* feat: 지역 관련 코드 추가

* feat: 통합 검색 UseCase 작성

* feat: 통합 검색 API 작성

* chore: ktlint 적용

* feat: 리뷰 반영
  • Loading branch information
shin-mallang authored Sep 22, 2024
1 parent 361ecfd commit 54e20a2
Show file tree
Hide file tree
Showing 27 changed files with 407 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,9 @@ class CelebrityPersistenceAdapter(
)
}
}

override fun readByName(name: String): List<Celebrity> {
val celebrities = celebrityJpaRepository.readByNameContains(name)
return celebrities.map { celebrityPersistenceMapper.toDomainWithoutYoutubeContent(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ interface CelebrityJpaRepository : JpaRepository<CelebrityJpaEntity, Long> {
""",
)
fun findAllBySubscriberCountDescTop10(): Set<CelebrityJpaEntity>

@Query(value = "SELECT c FROM CelebrityJpaEntity c WHERE c.name LIKE %:name%")
fun readByNameContains(name: String): List<CelebrityJpaEntity>
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ interface ReadCelebritiesPort {
fun readById(celebrityId: Long): Celebrity

fun readByYoutubeContentIds(youtubeContentIds: List<Long>): List<Celebrity>

fun readByName(name: String): List<Celebrity>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.celuveat.region.adapter.out.persistence

import com.celuveat.common.annotation.Adapter
import com.celuveat.region.adapter.out.persistence.entity.RegionJpaRepository
import com.celuveat.region.adapter.out.persistence.entity.RegionPersistenceMapper
import com.celuveat.region.application.port.out.ReadRegionPort
import com.celuveat.region.domain.Region

@Adapter
class RegionPersistenceAdapter(
private val regionJpaRepository: RegionJpaRepository,
private val regionPersistenceMapper: RegionPersistenceMapper,
) : ReadRegionPort {
override fun readByName(name: String): List<Region> {
val regions = regionJpaRepository.readByNameContains(name)
return regions.map { regionPersistenceMapper.toDomain(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.celuveat.region.adapter.out.persistence.entity

import com.celuveat.common.adapter.out.persistence.entity.RootEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table

@Table(name = "region")
@Entity
class RegionJpaEntity(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
@Column(nullable = false)
val name: String,
val latitude: Double,
val longitude: Double,
) : RootEntity<Long>() {
override fun id(): Long {
return this.id
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.celuveat.region.adapter.out.persistence.entity

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query

interface RegionJpaRepository : JpaRepository<RegionJpaEntity, Long> {
@Query(value = "SELECT r FROM RegionJpaEntity r WHERE r.name LIKE %:name%")
fun readByNameContains(name: String): List<RegionJpaEntity>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.celuveat.region.adapter.out.persistence.entity

import com.celuveat.common.annotation.Mapper
import com.celuveat.region.domain.Region

@Mapper
class RegionPersistenceMapper {
fun toDomain(region: RegionJpaEntity): Region {
return Region(id = region.id, name = region.name, latitude = region.latitude, longitude = region.longitude)
}

fun toEntity(region: Region): RegionJpaEntity {
return RegionJpaEntity(
id = region.id,
name = region.name,
latitude = region.latitude,
longitude = region.longitude,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.celuveat.region.application.port.out

import com.celuveat.region.domain.Region

interface ReadRegionPort {
fun readByName(name: String): List<Region>
}
8 changes: 8 additions & 0 deletions src/main/kotlin/com/celuveat/region/domain/Region.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.celuveat.region.domain

data class Region(
val id: Long = 0,
val name: String,
val latitude: Double = 0.0,
val longitude: Double = 0.0,
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import com.celuveat.restaurant.adapter.out.persistence.entity.InterestedRestaura
import com.celuveat.restaurant.adapter.out.persistence.entity.InterestedRestaurantPersistenceMapper
import com.celuveat.restaurant.adapter.out.persistence.entity.RestaurantImageJpaRepository
import com.celuveat.restaurant.adapter.out.persistence.entity.RestaurantJpaRepository
import com.celuveat.restaurant.adapter.out.persistence.entity.RestaurantPersistenceMapper
import com.celuveat.restaurant.application.port.out.DeleteInterestedRestaurantPort
import com.celuveat.restaurant.application.port.out.ReadInterestedRestaurantPort
import com.celuveat.restaurant.application.port.out.SaveInterestedRestaurantPort
Expand All @@ -25,7 +24,6 @@ class InterestedRestaurantPersistenceAdapter(
private val interestedRestaurantPersistenceMapper: InterestedRestaurantPersistenceMapper,
private val restaurantJpaRepository: RestaurantJpaRepository,
private val memberJpaRepository: MemberJpaRepository,
private val restaurantPersistenceMapper: RestaurantPersistenceMapper,
) : ReadInterestedRestaurantPort, SaveInterestedRestaurantPort, DeleteInterestedRestaurantPort {
@Transactional(readOnly = true)
override fun readInterestedRestaurants(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import com.celuveat.restaurant.adapter.out.persistence.entity.RestaurantJpaRepos
import com.celuveat.restaurant.adapter.out.persistence.entity.RestaurantPersistenceMapper
import com.celuveat.restaurant.application.port.out.ReadRestaurantPort
import com.celuveat.restaurant.domain.Restaurant
import java.time.LocalDate
import java.time.LocalTime
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import java.time.LocalDate
import java.time.LocalTime

@Adapter
class RestaurantPersistenceAdapter(
Expand Down Expand Up @@ -132,7 +132,7 @@ class RestaurantPersistenceAdapter(

override fun readTop10InterestedRestaurantsInDate(
startOfDate: LocalDate,
endOfDate: LocalDate
endOfDate: LocalDate,
): List<Restaurant> {
val restaurants = interestedRestaurantJpaRepository.findTop10InterestedRestaurantInDate(
startOfDate = startOfDate.atStartOfDay(),
Expand All @@ -148,6 +148,11 @@ class RestaurantPersistenceAdapter(
}
}

override fun readByName(name: String): List<Restaurant> {
val restaurants = restaurantJpaRepository.readByNameContains(name)
return restaurants.map { restaurantPersistenceMapper.toDomainWithoutImage(it) }
}

companion object {
val LATEST_SORTER = Sort.by("createdAt").descending()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.celuveat.restaurant.adapter.out.persistence.entity

import java.time.LocalDateTime
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Slice
import org.springframework.data.jpa.repository.EntityGraph
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import java.time.LocalDateTime

interface InterestedRestaurantJpaRepository : JpaRepository<InterestedRestaurantJpaEntity, Long> {
@EntityGraph(attributePaths = ["restaurant"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ interface RestaurantJpaRepository : JpaRepository<RestaurantJpaEntity, Long>, Cu
longitude: Double,
distanceKilometer: Double = 2.0,
): List<RestaurantJpaEntity>

@Query(value = "SELECT r FROM RestaurantJpaEntity r WHERE r.name LIKE %:name%")
fun readByNameContains(name: String): List<RestaurantJpaEntity>
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import com.celuveat.restaurant.application.port.`in`.result.RestaurantDetailResu
import com.celuveat.restaurant.application.port.`in`.result.RestaurantPreviewResult
import com.celuveat.restaurant.application.port.out.ReadInterestedRestaurantPort
import com.celuveat.restaurant.application.port.out.ReadRestaurantPort
import org.springframework.stereotype.Service
import java.time.DayOfWeek
import java.time.temporal.TemporalAdjusters
import org.springframework.stereotype.Service

@Service
class RestaurantQueryService(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,6 @@ interface ReadRestaurantPort {
startOfDate: LocalDate,
endOfDate: LocalDate,
): List<Restaurant>

fun readByName(name: String): List<Restaurant>
}
24 changes: 24 additions & 0 deletions src/main/kotlin/com/celuveat/search/adapter/in/rest/SearchApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.celuveat.search.adapter.`in`.rest

import com.celuveat.search.adapter.`in`.rest.response.IntegratedSearchResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.enums.ParameterIn
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam

@Tag(name = "검색 API")
interface SearchApi {
@Operation(summary = "지역, 셀럽, 음식점 통합 검색")
@GetMapping("/integrated")
fun integratedSearch(
@Parameter(
`in` = ParameterIn.QUERY,
description = "이름",
example = "감자",
required = true,
)
@RequestParam(name = "name") name: String,
): IntegratedSearchResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.celuveat.search.adapter.`in`.rest

import com.celuveat.search.adapter.`in`.rest.response.IntegratedSearchResponse
import com.celuveat.search.application.port.`in`.IntegratedSearchUseCase
import com.celuveat.search.application.port.`in`.query.IntegratedSearchQuery
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

@RequestMapping("/search")
@RestController
class SearchController(
private val integratedSearchUseCase: IntegratedSearchUseCase,
) : SearchApi {
@GetMapping("/integrated")
override fun integratedSearch(
@RequestParam(name = "name") name: String,
): IntegratedSearchResponse {
return IntegratedSearchResponse.from(integratedSearchUseCase.searchByName(IntegratedSearchQuery(name)))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.celuveat.search.adapter.`in`.rest.response

import com.celuveat.search.application.port.`in`.result.IntegratedSearchResult
import com.celuveat.search.application.port.`in`.result.RegionResult
import io.swagger.v3.oas.annotations.media.Schema

data class IntegratedSearchResponse(
val regionResults: List<RegionResponse>,
val restaurantResults: List<ResponseWithId>,
val celebrityResults: List<ResponseWithId>,
) {
companion object {
fun from(result: IntegratedSearchResult): IntegratedSearchResponse {
return IntegratedSearchResponse(
regionResults = result.regionResults.map { RegionResponse.from(it) },
restaurantResults = result.restaurantResults.map { ResponseWithId(id = it.id, name = it.name) },
celebrityResults = result.celebrityResults.map { ResponseWithId(id = it.id, name = it.name) },
)
}
}
}

data class ResponseWithId(
val id: Long,
val name: String,
)

data class RegionResponse(
val id: Long,
val name: String,
@Schema(
description = "위도",
example = "37.123456",
)
val latitude: Double,
@Schema(
description = "경도",
example = "127.123456",
)
val longitude: Double,
) {
companion object {
fun from(result: RegionResult): RegionResponse {
return RegionResponse(
id = result.id,
name = result.name,
latitude = result.latitude,
longitude = result.longitude,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.celuveat.search.application

import com.celuveat.celeb.application.port.out.ReadCelebritiesPort
import com.celuveat.region.application.port.out.ReadRegionPort
import com.celuveat.restaurant.application.port.out.ReadRestaurantPort
import com.celuveat.search.application.port.`in`.IntegratedSearchUseCase
import com.celuveat.search.application.port.`in`.query.IntegratedSearchQuery
import com.celuveat.search.application.port.`in`.result.IntegratedSearchResult
import org.springframework.stereotype.Service

@Service
class SearchQueryService(
private val readRegionPort: ReadRegionPort,
private val readRestaurantPort: ReadRestaurantPort,
private val readCelebritiesPort: ReadCelebritiesPort,
) : IntegratedSearchUseCase {
override fun searchByName(query: IntegratedSearchQuery): IntegratedSearchResult {
val regions = readRegionPort.readByName(query.name)
val restaurants = readRestaurantPort.readByName(query.name)
val celebrities = readCelebritiesPort.readByName(query.name)
return IntegratedSearchResult.of(regions = regions, restaurants = restaurants, celebrities = celebrities)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.celuveat.search.application.port.`in`

import com.celuveat.search.application.port.`in`.query.IntegratedSearchQuery
import com.celuveat.search.application.port.`in`.result.IntegratedSearchResult

interface IntegratedSearchUseCase {
fun searchByName(query: IntegratedSearchQuery): IntegratedSearchResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.celuveat.search.application.port.`in`.query

data class IntegratedSearchQuery(
val name: String,
)
Loading

0 comments on commit 54e20a2

Please sign in to comment.