Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ dependencies {
// H2
testImplementation("com.h2database:h2")

implementation("org.hibernate.orm:hibernate-spatial:6.2.7.Final")
Comment thread
min-0 marked this conversation as resolved.
implementation("org.locationtech.jts:jts-core:1.19.0")

// Spring Batch Test
testImplementation("org.springframework.batch:spring-batch-test")

Expand Down
7 changes: 4 additions & 3 deletions src/main/kotlin/com/eodigo/domain/restaurant/entity/Store.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ package com.eodigo.domain.restaurant.entity
import com.eodigo.common.entity.BaseTimeEntity
import com.eodigo.domain.restaurant.enums.StoreCategory
import jakarta.persistence.*
import org.locationtech.jts.geom.Point // Point 타입 import

@Entity
@Table(name = "store")
@Table(name = "store_backup")
class Store(
@Column(name = "name", nullable = false) val name: String, // 매장명
@Enumerated(EnumType.STRING)
@Column(name = "category", nullable = false)
val category: StoreCategory, // 카테고리 (CAFE, RESTAURANT)
@Column(name = "address", nullable = false) val address: String, // 주소
@Column(name = "latitude", nullable = false) val latitude: Double, // 위도
@Column(name = "longitude", nullable = false) val longitude: Double, // 경도
@Column(name = "location", nullable = false, columnDefinition = "POINT SRID 4326")
val location: Point, // 위치 (경도, 위도)
@Column(name = "img_url", nullable = true) val imgUrl: String?, // 이미지 url
) : BaseTimeEntity() {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,74 @@ package com.eodigo.domain.restaurant.repository

import com.eodigo.domain.restaurant.entity.Store
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param

interface StoreRepository : JpaRepository<Store, Long>
interface StoreSearchResult {
fun getStoreId(): Long

fun getStoreName(): String

fun getDistance(): Double

fun getMenuName(): String

fun getPrice(): Int

fun getLatitude(): Double

fun getLongitude(): Double

fun getAddress(): String

fun getImgUrl(): String?
}

interface StoreRepository : JpaRepository<Store, Long> {

@Query(
nativeQuery = true,
value =
"""
SELECT
s.id as storeId,
s.name as storeName,
ST_Distance_Sphere(s.location, ST_SRID(POINT(:userLng, :userLat), 4326)) as distance,
m.name as menuName,
m.price as price,
ST_X(s.location) as latitude,
ST_Y(s.location) as longitude,
s.address as address,
s.img_url as imgUrl
FROM
Comment thread
min-0 marked this conversation as resolved.
store_backup s
JOIN
menu m ON s.id = m.store_id
WHERE
s.category = :category
AND m.name LIKE CONCAT('%', :menuName, '%')
AND MBRContains(
ST_GeomFromText(CONCAT(
'POLYGON((',
:southWestLat, ' ', :southWestLng, ',',
:northEastLat, ' ', :southWestLng, ',',
:northEastLat, ' ', :northEastLng, ',',
:southWestLat, ' ', :northEastLng, ',',
:southWestLat, ' ', :southWestLng,
'))'
), 4326),
s.location
Comment thread
min-0 marked this conversation as resolved.
)
""",
)
fun findStoresAndMenusInArea(
@Param("userLng") userLng: Double,
@Param("userLat") userLat: Double,
@Param("category") category: String,
@Param("menuName") menuName: String,
@Param("southWestLng") southWestLng: Double,
@Param("southWestLat") southWestLat: Double,
@Param("northEastLng") northEastLng: Double,
@Param("northEastLat") northEastLat: Double,
): List<StoreSearchResult>
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,54 +23,41 @@ class StoreService(
) {
@Transactional(readOnly = true)
fun searchStores(request: SearchRequest): List<StoreDto> {
// 메뉴명과 카테고리로 필터링
val menus =
menuRepository.findByNameContaining(request.menuName).filter {
it.store.category == request.category
}
// DB에서 공간 검색 및 최저가 메뉴 필터링을 모두 수행
val searchResults =
storeRepository.findStoresAndMenusInArea(
menuName = request.menuName,
category = request.category.name,
userLat = request.userLat,
userLng = request.userLng,
southWestLat = request.southWestLat,
southWestLng = request.southWestLng,
northEastLat = request.northEastLat,
northEastLng = request.northEastLng,
)

// 가게별로 그룹화한 뒤, 각 가게에서 가장 저렴한 메뉴 하나만 선택
val cheapestMenuByStore =
menus
.groupBy { it.store }
.mapNotNull { (store, menusInStore) ->
val cheapestMenu = menusInStore.minByOrNull { it.price }
cheapestMenu?.let { store to it }
val cheapestStoresPerStore =
searchResults
.groupBy { it.getStoreId() } // 가게 ID로 그룹화
.mapNotNull { (_, results) -> // 각 가게에 대해
results.minByOrNull { it.getPrice() } // 가격이 가장 낮은 메뉴
}

// 지도 경계값 내의 매장 필터링
val locationFilteredMenus =
cheapestMenuByStore.filter { (store, _) ->
store.latitude >= request.southWestLat &&
store.latitude <= request.northEastLat &&
store.longitude >= request.southWestLng &&
store.longitude <= request.northEastLng
}

val storeDtoList =
locationFilteredMenus.map { (store, menu) ->
val distance =
calculateDistance(
lat1 = request.userLat,
lon1 = request.userLng,
lat2 = store.latitude,
lon2 = store.longitude,
)
val storeId = requireNotNull(store.id)
cheapestStoresPerStore.map { result ->
StoreDto(
storeId = storeId,
storeName = store.name,
distance = distance.toInt(),
menuName = menu.name,
price = menu.price,
latitude = store.latitude,
longitude = store.longitude,
address = store.address,
imgUrl = store.imgUrl,
storeId = result.getStoreId(),
storeName = result.getStoreName(),
distance = result.getDistance().toInt(),
menuName = result.getMenuName(),
price = result.getPrice(),
latitude = result.getLatitude(),
longitude = result.getLongitude(),
address = result.getAddress(),
imgUrl = result.getImgUrl(),
)
}

// 정렬
return when (request.sort) {
SortType.PRICE -> storeDtoList.sortedBy { it.price }
SortType.DISTANCE -> storeDtoList.sortedBy { it.distance }
Expand Down Expand Up @@ -104,8 +91,8 @@ class StoreService(
calculateDistance(
lat1 = latitude,
lon1 = longitude,
lat2 = store.latitude,
lon2 = store.longitude,
lat2 = store.location.x,
lon2 = store.location.y,
)

return StoreDetailDto(
Expand All @@ -114,8 +101,8 @@ class StoreService(
distance = distance.toInt(),
category = store.category,
address = store.address,
latitude = store.latitude,
longitude = store.longitude,
latitude = store.location.y,
longitude = store.location.x,
imgUrl = store.imgUrl,
menus =
menus.map { menu ->
Expand Down