Skip to content

Commit

Permalink
feat: #41 루트 검색 API 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
suyeoniii committed Sep 13, 2024
1 parent 67e95bf commit e01be3c
Show file tree
Hide file tree
Showing 18 changed files with 268 additions and 4 deletions.
5 changes: 5 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ dependencies {

implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")

val jdslVersion = "3.5.2"
implementation("com.linecorp.kotlin-jdsl:jpql-dsl:$jdslVersion")
implementation("com.linecorp.kotlin-jdsl:jpql-render:$jdslVersion")
implementation("com.linecorp.kotlin-jdsl:spring-data-jpa-support:$jdslVersion")
}

kotlin {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.routebox.routebox.application.route

import com.routebox.routebox.application.route.dto.GetRouteDetailResult
import com.routebox.routebox.application.route.dto.SearchCommand
import com.routebox.routebox.domain.route.RouteService
import org.springframework.stereotype.Component

@Component
class SearchRoutesUseCase(
private val routeService: RouteService,
) {
/**
* 루트 검색.
*
* @param request 검색 조건
* @return 루트 상세 정보
* @throws
*/
operator fun invoke(request: SearchCommand): List<GetRouteDetailResult> {
val routes = routeService.searchRoutes(request).map { GetRouteDetailResult.from(it) }
return routes
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.routebox.routebox.application.route.dto

import com.routebox.routebox.controller.route.dto.RouteSortBy
import com.routebox.routebox.controller.route.dto.SearchFilters

data class SearchCommand(
val page: Int,
val size: Int,
val filters: SearchFilters,
val query: String?,
val sortBy: RouteSortBy,
)
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class RouteController(
@Operation(
summary = "루트 단건 조회",
description = "루트 단건 조회",
security = [SecurityRequirement(name = "access-token")],
)
@ApiResponses(
ApiResponse(responseCode = "200"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.routebox.routebox.controller.route

import com.routebox.routebox.application.route.SearchRoutesUseCase
import com.routebox.routebox.application.route.dto.SearchCommand
import com.routebox.routebox.controller.route.dto.RouteSortBy
import com.routebox.routebox.controller.route.dto.SearchFilters
import com.routebox.routebox.controller.route.dto.SearchRouteDto
import com.routebox.routebox.controller.route.dto.SearchRoutesResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.Parameters
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.validation.annotation.Validated
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

@Tag(name = "루트 검색 관련 API")
@RestController
@Validated
@RequestMapping("/api")
class RouteSearchController(
private val searchRoutesUseCase: SearchRoutesUseCase,
) {
@Operation(
summary = "루트 검색",
description = "검색어, 검색 필터, 정렬 등을 이용한 루트 검색",
security = [SecurityRequirement(name = "access-token")],
)
@ApiResponses(
ApiResponse(responseCode = "200"),
)
@Parameters(
Parameter(name = "sortBy", description = "정렬 기준 (NEWEST, OLDEST, POPULAR, COMMENTS)"),
Parameter(name = "whoWith", description = "대상 (예: 친구, 가족)"),
Parameter(name = "numberOfPeople", description = "인원수 (예: 2, 3)"),
Parameter(name = "numberOfDays", description = "머무는 기간 (일 수)"),
Parameter(name = "style", description = "루트 스타일 (예: 모험, 휴식)"),
Parameter(name = "transportation", description = "이동수단 (예: 자동차, 자전거)"),
)
@GetMapping("/v1/search")
fun searchRoutes(
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "10") size: Int,
@RequestParam(required = false) query: String? = null,
@RequestParam(required = false) sortBy: RouteSortBy = RouteSortBy.NEWEST,
@RequestParam(required = false) whoWith: List<String> = emptyList(),
@RequestParam(required = false) numberOfPeople: List<Int> = emptyList(),
@RequestParam(required = false) numberOfDays: List<String> = emptyList(),
@RequestParam(required = false) style: List<String> = emptyList(),
@RequestParam(required = false) transportation: List<String> = emptyList(),
): SearchRoutesResponse {
val filters = SearchFilters.from(whoWith, numberOfPeople, numberOfDays, style, transportation)
val request = SearchCommand(page, size, filters, query, sortBy)
return SearchRoutesResponse.from(searchRoutesUseCase(request).map { SearchRouteDto.from(it) })
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.routebox.routebox.controller.route.dto

enum class RouteSortBy {
NEWEST,
OLDEST,
POPULAR,
COMMENTS,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.routebox.routebox.controller.route.dto

data class SearchFilters(
val whoWith: List<String>,

val numberOfPeople: List<Int>,

val numberOfDays: List<String>,

val style: List<String>,

val transportation: List<String>,
) {
companion object {
fun from(whoWith: List<String>, numberOfPeople: List<Int>, numberOfDays: List<String>, style: List<String>, transportation: List<String>): SearchFilters = SearchFilters(
whoWith = whoWith,
numberOfPeople = numberOfPeople,
numberOfDays = numberOfDays,
style = style,
transportation = transportation,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.routebox.routebox.controller.route.dto

import com.routebox.routebox.application.route.dto.GetRouteDetailResult
import io.swagger.v3.oas.annotations.media.Schema
import java.time.LocalDateTime

data class SearchRouteDto(
@Schema(description = "Id(PK) of route", example = "1")
val routeId: Long,

@Schema(description = "Id(PK) of user", example = "1")
val userId: Long,

@Schema(description = "유저 프로필 이미지(url)", example = "https://user-profile-image")
val profileImageUrl: String,

@Schema(description = "닉네임", example = "고작가")
val nickname: String,

@Schema(description = "루트 제목", example = "서울의 작가들")
val routeName: String?,

@Schema(description = "루트 설명", example = "서울의 작가들을 만나보세요.")
val routeDescription: String?,

@Schema(description = "루트 이미지", example = "https://route-image1")
val routeImageUrl: String,

@Schema(description = "루트 구매 수", example = "0")
val purchaseCount: Int,

@Schema(description = "루트 댓글 수", example = "0")
val commentCount: Int,

@Schema(description = "루트 생성일", example = "2021-08-01T00:00:00")
val createdAt: LocalDateTime?,
) {
companion object {
fun from(
getRouteDetailResult: GetRouteDetailResult,
): SearchRouteDto = SearchRouteDto(
routeId = getRouteDetailResult.routeId,
userId = getRouteDetailResult.userId,
profileImageUrl = getRouteDetailResult.profileImageUrl,
nickname = getRouteDetailResult.nickname,
routeName = getRouteDetailResult.routeName,
routeDescription = getRouteDetailResult.routeDescription,
routeImageUrl = getRouteDetailResult.routeImageUrls.firstOrNull() ?: "",
purchaseCount = getRouteDetailResult.purchaseCount,
commentCount = getRouteDetailResult.commentCount,
createdAt = getRouteDetailResult.recordFinishedAt,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.routebox.routebox.controller.route.dto

import io.swagger.v3.oas.annotations.media.Schema

data class SearchRoutesResponse(
@Schema(description = "검색된 루트 리스트")
val routes: List<SearchRouteDto>,
) {
companion object {
fun from(routes: List<SearchRouteDto>): SearchRoutesResponse = SearchRoutesResponse(
routes = routes,
)
}
}
5 changes: 4 additions & 1 deletion src/main/kotlin/com/routebox/routebox/domain/route/Route.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Route(
numberOfPeople: Int?,
numberOfDays: String?,
style: Array<String>,
styles: String,
transportation: String?,
isPublic: Boolean = false,
) : TimeTrackedBaseEntity() {
Expand Down Expand Up @@ -65,6 +66,8 @@ class Route(
@Convert(converter = StringArrayConverter::class)
@Column(columnDefinition = "json")
var style: Array<String> = style

var styles: String? = styles
private set

var transportation: String? = transportation
Expand Down Expand Up @@ -115,7 +118,7 @@ class Route(
}

fun updateStyle(style: Array<String>) {
this.style = style
this.styles = style.joinToString(",")
}

fun updateTransportation(transportation: String) {
Expand Down
56 changes: 56 additions & 0 deletions src/main/kotlin/com/routebox/routebox/domain/route/RouteService.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.routebox.routebox.domain.route

import com.linecorp.kotlinjdsl.querymodel.jpql.sort.Sorts.asc
import com.linecorp.kotlinjdsl.querymodel.jpql.sort.Sorts.desc
import com.routebox.routebox.application.route.dto.SearchCommand
import com.routebox.routebox.controller.route.dto.RouteSortBy
import com.routebox.routebox.domain.common.FileManager
import com.routebox.routebox.domain.user.User
import com.routebox.routebox.exception.route.RouteNotFoundException
Expand Down Expand Up @@ -77,6 +81,7 @@ class RouteService(
whoWith = null,
numberOfPeople = null,
numberOfDays = null,
styles = "",
style = emptyArray(),
transportation = null,
isPublic = false,
Expand Down Expand Up @@ -341,4 +346,55 @@ class RouteService(
@Transactional(readOnly = true)
fun findRouteActivityById(activityId: Long): RouteActivity? =
routeActivityRepository.findById(activityId).orElse(null)

/**
* 루트 검색
*/
@Transactional(readOnly = true)
fun searchRoutes(request: SearchCommand): List<Route> {
val pageable = PageRequest.of(request.page, request.size)
return routeRepository.findPage(pageable) {
select(
entity(Route::class),
).from(
entity(Route::class),
).where(
and(
path(Route::isPublic).eq(true),
request.query?.let {
or(
path(Route::name).like("%$it%"),
path(Route::description).like("%$it%"),
)
},
request.filters.whoWith.takeIf { it.isNotEmpty() }?.let {
path(Route::whoWith).`in`(it)
},
request.filters.numberOfPeople.takeIf { it.isNotEmpty() }?.let {
path(Route::numberOfPeople).`in`(it)
},
request.filters.numberOfDays.takeIf { it.isNotEmpty() }?.let {
path(Route::numberOfDays).`in`(it)
},
request.filters.style.takeIf { it.isNotEmpty() }?.let { stylesFilter ->
and(
*stylesFilter.map { style ->
path(Route::styles).like("%$style%")
}.toTypedArray(),
)
},
request.filters.transportation.takeIf { it.isNotEmpty() }?.let {
path(Route::transportation).`in`(it)
},
),
).orderBy(
when (request.sortBy) {
RouteSortBy.NEWEST -> desc(path(Route::createdAt))
RouteSortBy.OLDEST -> asc(path(Route::createdAt))
RouteSortBy.POPULAR -> asc(path(Route::createdAt)) // TODO: implements
RouteSortBy.COMMENTS -> asc(path(Route::createdAt)) // TODO: implements
},
)
}.content.filterNotNull()
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.routebox.routebox.infrastructure.route

import com.linecorp.kotlinjdsl.support.spring.data.jpa.repository.KotlinJdslJpqlExecutor
import com.routebox.routebox.domain.route.Route
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
Expand All @@ -9,7 +10,7 @@ import org.springframework.stereotype.Repository

@Suppress("ktlint:standard:function-naming")
@Repository
interface RouteRepository : JpaRepository<Route, Long> {
interface RouteRepository : JpaRepository<Route, Long>, KotlinJdslJpqlExecutor {
@Query(
"""
SELECT r FROM Route r
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ class SecurityConfig {
"/api/v1/auth/login/apple" to HttpMethod.POST,
"/api/v1/auth/tokens/refresh" to HttpMethod.POST,
"/api/v1/users/nickname/*/availability" to HttpMethod.GET,
"/api/v1/routes" to HttpMethod.GET,
"/api/v1/routes/*" to HttpMethod.GET,
"/api/v1/notifications" to HttpMethod.GET,
"/api/v1/notifications/unread" to HttpMethod.GET,
)
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ CREATE TABLE routes
number_of_people INT,
number_of_days VARCHAR(255),
style JSON,
styles VARCHAR(255),
transportation VARCHAR(255),
is_public BOOLEAN NOT NULL DEFAULT FALSE,
record_finished_at DATETIME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class GetUserRoutesResponseLatestRouteUseCaseTest {
numberOfPeople = numberOfPeople,
numberOfDays = numberOfDays,
style = style,
styles = "",
transportation = transportation,
user = user,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class GetUserRoutesResponseRouteDetailUseCaseTest {
numberOfPeople = numberOfPeople,
numberOfDays = numberOfDays,
style = style,
styles = "",
transportation = transportation,
user = user,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class PurchaseRouteUseCaseTest {
numberOfPeople = 2,
numberOfDays = "2박3일",
style = arrayOf("힐링"),
styles = "힐링",
transportation = "뚜벅뚜벅",
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ class RouteServiceTest {
numberOfPeople = 2,
numberOfDays = "2박3일",
style = arrayOf("힐링"),
styles = "",
transportation = "뚜벅뚜벅",
)
}

0 comments on commit e01be3c

Please sign in to comment.