Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
27 changes: 25 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.1.0"
id("io.spring.dependency-management") version "1.1.0"
id("org.jlleitschuh.gradle.ktlint") version "11.5.0"
id("jacoco")
kotlin("jvm") version "1.8.21"
kotlin("plugin.spring") version "1.8.21"
kotlin("plugin.jpa") version "1.8.21"
kotlin("plugin.allopen") version "1.6.21"
kotlin("plugin.noarg") version "1.6.21"
id("org.jlleitschuh.gradle.ktlint") version "11.5.0"
id("jacoco")
kotlin("kapt") version "1.8.21"
idea
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
val queryDslVersion = "5.0.0"

java {
sourceCompatibility = JavaVersion.VERSION_17
Expand Down Expand Up @@ -74,6 +77,11 @@ dependencies {
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-prometheus")

implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
kapt("com.querydsl:querydsl-apt:${dependencyManagement.importedProperties["querydsl.version"]}:jakarta")
kapt("jakarta.annotation:jakarta.annotation-api")
kapt("jakarta.persistence:jakarta.persistence-api")
}

tasks.withType<KotlinCompile> {
Expand All @@ -88,6 +96,21 @@ tasks.withType<Test> {
finalizedBy(tasks.jacocoTestReport)
}

val querydslDir = "build/generated/source/kapt/main"
idea {
module {
val kaptMain = file(querydslDir)
sourceDirs.add(kaptMain)
generatedSourceDirs.add(kaptMain)
}
}

tasks.named("clean") {
doLast {
file(querydslDir).deleteRecursively()
}
}

allOpen {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.MappedSuperclass")
Expand Down
17 changes: 12 additions & 5 deletions docs/open-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,23 @@ paths:
- event-controller
operationId: getEvents
parameters:
- name: name
- name: sort
in: query
required: false
required: true
schema:
type: string
- name: pageable
- name: id
in: query
required: true
required: false
schema:
$ref: '#/components/schemas/Pageable'
type: integer
format: int32
- name: time
in: query
required: false
schema:
type: string
format: date-time
Comment on lines +196 to +207
Copy link
Member

@junha-ahn junha-ahn Oct 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

키 이름을 last_access_id, last_access_time 등 으로 변경할 필요가 있어보입니다.

  • 더 좋은 이름 있을 수 있으니 찾아보세요.
  • 당근마켓, 인스타, 트위터, 페이스북 등 참고
    • 무한스크롤 형태니까... Open API Docs 또는 개발자 도구 열고 직접 요청을 확인해보세요

responses:
"200":
description: OK
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.group4.ticketingservice.config

import com.querydsl.jpa.impl.JPAQueryFactory
import jakarta.persistence.EntityManager
import jakarta.persistence.PersistenceContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class QuerydslConfiguration(
@PersistenceContext
private val entityManager: EntityManager
) {
@Bean
fun jpaQueryFactory(): JPAQueryFactory = JPAQueryFactory(entityManager)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import com.group4.ticketingservice.dto.EventCreateRequest
import com.group4.ticketingservice.dto.EventResponse
import com.group4.ticketingservice.entity.Event
import com.group4.ticketingservice.service.EventService
import com.group4.ticketingservice.utils.exception.CustomException
import com.group4.ticketingservice.utils.exception.ErrorCodes
import io.swagger.v3.oas.annotations.Hidden
import jakarta.servlet.http.HttpServletRequest
import jakarta.validation.Valid
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.data.web.PageableDefault
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
Expand All @@ -22,12 +21,20 @@ import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.time.OffsetDateTime

@RestController
@RequestMapping("/events")
class EventController @Autowired constructor(
val eventService: EventService
) {
companion object {
const val DESCENDING = "desc"
const val ASCENDING = "asc"
const val SORT_BY_DEADLINE = "deadline"
const val SORT_BY_START_DATE = "startDate"
const val SORT_BY_CREATED_AT = "createdAt"
}

// TimeE2ETest를 위한 임시 EndPoint입니다.
@Hidden
Expand Down Expand Up @@ -84,10 +91,28 @@ class EventController @Autowired constructor(
@GetMapping
fun getEvents(
request: HttpServletRequest,
@RequestParam(required = false) name: String?,
@PageableDefault(size = 10, sort = ["id"], direction = Sort.Direction.DESC) pageable: Pageable
@RequestParam sort: String,
@RequestParam id: Int?,
@RequestParam time: OffsetDateTime?
): ResponseEntity<Page<Event>> {
val page = eventService.getEvents(name, pageable)
val sortProperties = sort.split(",").toTypedArray()

val fieldName = sortProperties.getOrNull(0)
val direction = sortProperties.getOrNull(1)

when (Pair(fieldName, direction)) {
Pair(SORT_BY_DEADLINE, null),
Pair(SORT_BY_START_DATE, null),
Pair(SORT_BY_CREATED_AT, null),
Pair(SORT_BY_DEADLINE, ASCENDING),
Pair(SORT_BY_START_DATE, ASCENDING),
Pair(SORT_BY_CREATED_AT, DESCENDING) -> {
// Valid request
}
else -> throw CustomException(ErrorCodes.INVALID_SORT_FORMAT)
}

val page = eventService.getEvents(fieldName!!, id, time)

val headers = HttpHeaders()
headers.set("Content-Location", request.requestURI)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import org.springframework.data.jpa.domain.Specification
class EventSpecifications {
companion object {
fun withName(name: String?): Specification<Event> {
return Specification { root: Root<Event>, query: CriteriaQuery<*>, criteriaBuilder: CriteriaBuilder ->
return Specification { root: Root<Event>, _: CriteriaQuery<*>, criteriaBuilder: CriteriaBuilder ->
val predicates = mutableListOf<Predicate>()

if (!name.isNullOrBlank()) {
predicates.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), "%${name.toLowerCase()}%"))
predicates.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), "%${name.lowercase()}%"))
}

criteriaBuilder.and(*predicates.toTypedArray())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.group4.ticketingservice.repository

import com.group4.ticketingservice.entity.Event
import com.group4.ticketingservice.entity.QEvent.event
import com.group4.ticketingservice.utils.exception.CustomException
import com.group4.ticketingservice.utils.exception.ErrorCodes
import com.querydsl.core.types.OrderSpecifier
import com.querydsl.core.types.dsl.BooleanExpression
import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport
import org.springframework.stereotype.Repository
import java.time.Duration
import java.time.OffsetDateTime

@Repository
class EventRepositorySupport(
private val queryFactory: JPAQueryFactory
) : QuerydslRepositorySupport(
Event::class.java
) {

fun getEvent(sortBy: String, id: Int?, dateTime: OffsetDateTime?): List<Event> {
val whereSpecifier = buildWhereSpecifier(sortBy, id, dateTime)
val orderSpecifier = buildOrderSpecifier(sortBy)

return queryFactory.selectFrom(event)
.where(whereSpecifier)
.orderBy(orderSpecifier, event.id.desc())
.limit(10)
.fetch()
}

private fun buildWhereSpecifier(sortBy: String?, id: Int?, dateTime: OffsetDateTime?): BooleanExpression? {
return when (sortBy) {
"deadline" -> buildDeadlineSpecifier(id, dateTime)
"startDate" -> buildStartDateSpecifier(id, dateTime)
"createdAt" -> buildCreatedAtSpecifier(id, dateTime)
else -> throw CustomException(ErrorCodes.MESSAGE_NOT_READABLE)
}
}

private fun buildOrderSpecifier(sortBy: String?): OrderSpecifier<*> {
return when (sortBy) {
"deadline" -> event.reservationEndTime.asc()
"startDate" -> event.startDate.asc()
"createdAt" -> event.createdAt.desc()
else -> event.id.desc()
}
}

private fun buildDeadlineSpecifier(id: Int?, dateTime: OffsetDateTime?): BooleanExpression? {
return if (id == null) {
event.reservationEndTime.after(OffsetDateTime.now())
} else {
event.reservationEndTime.gt(dateTime).or(event.reservationEndTime.eq(dateTime).and(ltEventId(id)))
}
}

private fun buildStartDateSpecifier(id: Int?, dateTime: OffsetDateTime?): BooleanExpression? {
return if (id == null) {
event.startDate.before(OffsetDateTime.now() + Duration.ofDays(60))
} else {
event.startDate.gt(dateTime).or(event.startDate.eq(dateTime).and(ltEventId(id)))
}
}

private fun buildCreatedAtSpecifier(id: Int?, dateTime: OffsetDateTime?): BooleanExpression? {
return if (id == null) {
null
} else {
event.createdAt.lt(dateTime).or(event.createdAt.eq(dateTime).and(ltEventId(id)))
}
}

private fun ltEventId(id: Int?): BooleanExpression? {
return if (id == null) null else event.id.lt(id)
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package com.group4.ticketingservice.service

import com.group4.ticketingservice.dto.EventSpecifications
import com.group4.ticketingservice.entity.Event
import com.group4.ticketingservice.repository.EventRepository
import com.group4.ticketingservice.repository.EventRepositorySupport
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
import java.time.OffsetDateTime

@Service
class EventService(
private val eventRepository: EventRepository
private val eventRepository: EventRepository,
private val eventRepositorySupport: EventRepositorySupport

) {
fun createEvent(
name: String,
Expand All @@ -36,8 +37,7 @@ class EventService(
return eventRepository.findById(id).orElse(null)
}

fun getEvents(name: String?, pageable: Pageable): Page<Event> {
val specification = EventSpecifications.withName(name)
return PageImpl(eventRepository.findAllBy(specification, pageable))
fun getEvents(sort: String, id: Int?, time: OffsetDateTime?): Page<Event> {
return PageImpl(eventRepositorySupport.getEvent(sort, id, time))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class ResponseAdvice<T>(
response: ServerHttpResponse
): T? {
if (body == null) {
@Suppress("UNCHECKED_CAST")
return SuccessResponseDTO(
data = null,
path = response.headers.getFirst("Content-Location")
Expand All @@ -62,6 +63,7 @@ class ResponseAdvice<T>(
}

if (body !is Page<*>) {
@Suppress("UNCHECKED_CAST")
return SuccessResponseDTO(
data = body as Any,
path = response.headers.getFirst("Content-Location")
Expand Down Expand Up @@ -97,6 +99,7 @@ class ResponseAdvice<T>(
if (data.isEmpty()) {
data = listOf()
} else if (data[0] is Event) {
@Suppress("UNCHECKED_CAST")
data = (page.content as List<Event>).map {
EventResponse(
id = it.id!!,
Expand All @@ -109,6 +112,7 @@ class ResponseAdvice<T>(
)
}
} else if (data[0] is Reservation) {
@Suppress("UNCHECKED_CAST")
data = (page.content as List<Reservation>).map {
ReservationResponse(
id = it.id!!,
Expand All @@ -122,7 +126,7 @@ class ResponseAdvice<T>(
)
}
}

@Suppress("UNCHECKED_CAST")
return SuccessResponseDTO(
data = data,
path = response.headers.getFirst("Content-Location")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ enum class ErrorCodes(val status: HttpStatus, val message: String, val errorCode
VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "유효성 검증에 실패하였습니다.", 10001),
MESSAGE_NOT_READABLE(HttpStatus.BAD_REQUEST, "올바른 형식의 요청이 아닙니다", 10002),
DATE_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "예약 가능한 시간이 아닙니다.", 10003),
INVALID_SORT_FORMAT(HttpStatus.BAD_REQUEST, "지원하는 정렬 형식이 아닙니다.", 10004),

// 401 Unauthorized
LOGIN_FAIL(HttpStatus.UNAUTHORIZED, "로그인에 실패했습니다", 20000),
Expand Down