generated from pricelees/issue-pr-template
[#28] 쿠버네티스 환경 배포 #29
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@ -0,0 +1,10 @@
|
||||
FROM gradle:8-jdk17 AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN ./gradlew bootjar --no-daemon
|
||||
|
||||
FROM amazoncorretto:17
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
COPY --from=builder /app/build/libs/*.jar app.jar
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
@ -39,7 +39,9 @@ dependencies {
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9")
|
||||
|
||||
// DB
|
||||
implementation("com.github.f4b6a3:tsid-creator:5.2.6")
|
||||
runtimeOnly("com.h2database:h2")
|
||||
runtimeOnly("com.mysql:mysql-connector-j")
|
||||
|
||||
// Jwt
|
||||
implementation("io.jsonwebtoken:jjwt:0.12.6")
|
||||
|
||||
6
frontend/.dockerignore
Normal file
6
frontend/.dockerignore
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
.git
|
||||
.DS_Store
|
||||
npm-debug.log
|
||||
dist
|
||||
build
|
||||
@ -1 +1 @@
|
||||
VITE_API_BASE_URL = "http://localhost:8080"
|
||||
VITE_API_BASE_URL = '/api'
|
||||
18
frontend/Dockerfile
Normal file
18
frontend/Dockerfile
Normal file
@ -0,0 +1,18 @@
|
||||
# Stage 1: Build the React app
|
||||
FROM node:24 AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
COPY package-lock.json ./
|
||||
|
||||
RUN npm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Serve with Nginx
|
||||
FROM nginx:latest
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
15
frontend/nginx.conf
Normal file
15
frontend/nginx.conf
Normal file
@ -0,0 +1,15 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import axios, { type AxiosError, type AxiosRequestConfig, type Method } from 'axios';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080',
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { type ReactNode } from 'react';
|
||||
import Navbar from './Navbar';
|
||||
|
||||
interface LayoutProps {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { checkLogin } from '@_api/auth/authAPI';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from 'src/context/AuthContext';
|
||||
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import React, { createContext, useState, useEffect, ReactNode, useContext } from 'react';
|
||||
import { checkLogin as apiCheckLogin, login as apiLogin, logout as apiLogout } from '@_api/auth/authAPI';
|
||||
import type { LoginRequest, LoginCheckResponse } from '@_api/auth/authTypes';
|
||||
import type { LoginRequest, LoginResponse } from '@_api/auth/authTypes';
|
||||
import React, { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
|
||||
|
||||
interface AuthContextType {
|
||||
loggedIn: boolean;
|
||||
userName: string | null;
|
||||
role: 'ADMIN' | 'MEMBER' | null;
|
||||
loading: boolean; // Add loading state to type
|
||||
login: (data: LoginRequest) => Promise<LoginCheckResponse>;
|
||||
login: (data: LoginRequest) => Promise<LoginResponse>;
|
||||
logout: () => Promise<void>;
|
||||
checkLogin: () => Promise<void>;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { type ReactNode } from 'react';
|
||||
import AdminNavbar from './AdminNavbar';
|
||||
|
||||
interface AdminLayoutProps {
|
||||
|
||||
@ -8,4 +8,19 @@ export default defineConfig({
|
||||
react(),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
// 실제 백엔드 서버 주소로 전달
|
||||
target: 'http://localhost:8080',
|
||||
|
||||
// Origin 헤더를 target의 Origin으로 변경 (CORS 에러 방지)
|
||||
changeOrigin: true,
|
||||
|
||||
// Ingress의 rewrite-target과 동일한 역할.
|
||||
// '/api/themes' -> '/themes'로 경로를 재작성하여 백엔드에 전달
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -3,7 +3,9 @@ package roomescape
|
||||
import org.springframework.boot.Banner
|
||||
import org.springframework.boot.SpringApplication
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
|
||||
|
||||
@EnableJpaAuditing
|
||||
@SpringBootApplication
|
||||
class RoomescapeApplication
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ class JwtHandler(
|
||||
|
||||
fun createToken(memberId: Long): String {
|
||||
val date = Date()
|
||||
val accessTokenExpiredAt = Date(date.time + tokenTtlSeconds)
|
||||
val accessTokenExpiredAt = Date(date.time + (tokenTtlSeconds * 1_000))
|
||||
|
||||
return Jwts.builder()
|
||||
.claim(MEMBER_ID_CLAIM_KEY, memberId)
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
package roomescape.common.config
|
||||
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
|
||||
@Configuration
|
||||
class CorsConfig : WebMvcConfigurer {
|
||||
override fun addCorsMappings(registry: CorsRegistry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins("http://localhost:5173")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
|
||||
.allowedHeaders("Authorization", "Content-Type")
|
||||
.maxAge(3600) // 1 hour
|
||||
}
|
||||
}
|
||||
17
src/main/kotlin/roomescape/common/config/TsidConfig.kt
Normal file
17
src/main/kotlin/roomescape/common/config/TsidConfig.kt
Normal file
@ -0,0 +1,17 @@
|
||||
package roomescape.common.config
|
||||
|
||||
import com.github.f4b6a3.tsid.TsidFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
class TsidConfig {
|
||||
@Value("\${POD_NAME:app-0}")
|
||||
private lateinit var podName: String
|
||||
|
||||
@Bean
|
||||
fun tsidFactory(): TsidFactory = TsidFactory(podName.substringAfterLast("-").toInt())
|
||||
}
|
||||
|
||||
fun TsidFactory.next(): Long = this.create().toLong()
|
||||
35
src/main/kotlin/roomescape/common/entity/BaseEntity.kt
Normal file
35
src/main/kotlin/roomescape/common/entity/BaseEntity.kt
Normal file
@ -0,0 +1,35 @@
|
||||
package roomescape.common.entity
|
||||
|
||||
import jakarta.persistence.*
|
||||
import org.springframework.data.annotation.CreatedDate
|
||||
import org.springframework.data.annotation.LastModifiedDate
|
||||
import org.springframework.data.domain.Persistable
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.jvm.Transient
|
||||
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener::class)
|
||||
abstract class BaseEntity(
|
||||
@Column(updatable = false)
|
||||
@CreatedDate
|
||||
var createdAt: LocalDateTime? = null,
|
||||
|
||||
@LastModifiedDate
|
||||
var lastModifiedAt: LocalDateTime? = null,
|
||||
) : Persistable<Long> {
|
||||
|
||||
@Transient
|
||||
private var isNewEntity: Boolean = true
|
||||
|
||||
@PostLoad
|
||||
@PostPersist
|
||||
fun markNotNew() {
|
||||
isNewEntity = false
|
||||
}
|
||||
|
||||
override fun isNew(): Boolean = isNewEntity
|
||||
|
||||
abstract override fun getId(): Long?
|
||||
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package roomescape.common.log
|
||||
|
||||
import net.ttddyy.dsproxy.ExecutionInfo
|
||||
import net.ttddyy.dsproxy.QueryInfo
|
||||
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel
|
||||
import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener
|
||||
import java.util.function.Predicate
|
||||
|
||||
class MDCAwareSlowQueryListenerWithoutParams : SLF4JQueryLoggingListener {
|
||||
private val slowQueryPredicate: SlowQueryPredicate
|
||||
private val sqlLogFormatter: SqlLogFormatter
|
||||
|
||||
constructor(logLevel: SLF4JLogLevel, thresholdMs: Long) {
|
||||
this.logLevel = logLevel
|
||||
this.slowQueryPredicate = SlowQueryPredicate(thresholdMs)
|
||||
this.sqlLogFormatter = SqlLogFormatter()
|
||||
}
|
||||
|
||||
override fun afterQuery(
|
||||
execInfo: ExecutionInfo,
|
||||
queryInfoList: List<QueryInfo>
|
||||
) {
|
||||
if (slowQueryPredicate.test(execInfo.elapsedTime)) {
|
||||
super.afterQuery(execInfo, queryInfoList)
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeLog(message: String) {
|
||||
super.writeLog(sqlLogFormatter.maskParams(message))
|
||||
}
|
||||
}
|
||||
|
||||
class SqlLogFormatter {
|
||||
fun maskParams(message: String) = message.replace(Regex("""(,?\s*)Params:\[.*?]"""), "")
|
||||
}
|
||||
|
||||
class SlowQueryPredicate(
|
||||
private val thresholdMs: Long
|
||||
) : Predicate<Long> {
|
||||
override fun test(t: Long): Boolean = (t >= thresholdMs)
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package roomescape.common.log
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel
|
||||
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Primary
|
||||
import org.springframework.context.annotation.Profile
|
||||
import javax.sql.DataSource
|
||||
|
||||
@Configuration
|
||||
@Profile("deploy")
|
||||
@EnableConfigurationProperties(SlowQueryProperties::class)
|
||||
class ProxyDataSourceConfig {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
fun dataSource(
|
||||
@Qualifier("actualDataSource") actualDataSource: DataSource,
|
||||
properties: SlowQueryProperties
|
||||
): DataSource = ProxyDataSourceBuilder.create(actualDataSource)
|
||||
.name(properties.loggerName)
|
||||
.listener(
|
||||
MDCAwareSlowQueryListenerWithoutParams(
|
||||
logLevel = SLF4JLogLevel.nullSafeValueOf(properties.logLevel.uppercase()),
|
||||
thresholdMs = properties.thresholdMs
|
||||
)
|
||||
)
|
||||
.buildProxy()
|
||||
|
||||
@Bean
|
||||
@ConfigurationProperties(prefix = "spring.datasource.hikari")
|
||||
fun actualDataSource(): DataSource = DataSourceBuilder.create()
|
||||
.type(HikariDataSource::class.java)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Profile("deploy")
|
||||
@ConfigurationProperties(prefix = "slow-query")
|
||||
data class SlowQueryProperties(
|
||||
val loggerName: String,
|
||||
val logLevel: String,
|
||||
val thresholdMs: Long,
|
||||
)
|
||||
@ -36,7 +36,7 @@ class RoomescapeLogMaskingConverter : MessageConverter() {
|
||||
|
||||
private fun maskedPlainMessage(message: String): String {
|
||||
val keys: String = SENSITIVE_KEYS.joinToString("|")
|
||||
val regex = Regex("(?i)($keys)(\\s*=\\s*)([^,\\s]+)")
|
||||
val regex = Regex("(?i)($keys)(\\s*=\\s*)([^(,|\"|?)\\s]+)")
|
||||
|
||||
return regex.replace(message) { matchResult ->
|
||||
val key = matchResult.groupValues[1]
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
package roomescape.member.business
|
||||
|
||||
import com.github.f4b6a3.tsid.TsidFactory
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.common.config.next
|
||||
import roomescape.member.exception.MemberErrorCode
|
||||
import roomescape.member.exception.MemberException
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
@ -16,6 +18,7 @@ private val log = KotlinLogging.logger {}
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
class MemberService(
|
||||
private val tsidFactory: TsidFactory,
|
||||
private val memberRepository: MemberRepository,
|
||||
) {
|
||||
fun findMembers(): MemberRetrieveListResponse {
|
||||
@ -46,6 +49,7 @@ class MemberService(
|
||||
}
|
||||
|
||||
val member = MemberEntity(
|
||||
_id = tsidFactory.next(),
|
||||
name = request.name,
|
||||
email = request.email,
|
||||
password = request.password,
|
||||
|
||||
@ -1,20 +1,30 @@
|
||||
package roomescape.member.infrastructure.persistence
|
||||
|
||||
import jakarta.persistence.*
|
||||
import roomescape.common.entity.BaseEntity
|
||||
|
||||
@Entity
|
||||
@Table(name = "members")
|
||||
class MemberEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
@Column(name = "member_id")
|
||||
private var _id: Long?,
|
||||
|
||||
@Column(name = "name", nullable = false)
|
||||
var name: String,
|
||||
|
||||
@Column(name = "email", nullable = false)
|
||||
var email: String,
|
||||
|
||||
@Column(name = "password", nullable = false)
|
||||
var password: String,
|
||||
|
||||
@Column(name = "role", nullable = false, length = 20)
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
var role: Role
|
||||
) {
|
||||
): BaseEntity() {
|
||||
override fun getId(): Long? = _id
|
||||
|
||||
fun isAdmin(): Boolean = role == Role.ADMIN
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
package roomescape.payment.business
|
||||
|
||||
import com.github.f4b6a3.tsid.TsidFactory
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.common.config.next
|
||||
import roomescape.payment.exception.PaymentErrorCode
|
||||
import roomescape.payment.exception.PaymentException
|
||||
import roomescape.payment.infrastructure.client.PaymentApproveResponse
|
||||
@ -21,6 +23,7 @@ private val log = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class PaymentService(
|
||||
private val tsidFactory: TsidFactory,
|
||||
private val paymentRepository: PaymentRepository,
|
||||
private val canceledPaymentRepository: CanceledPaymentRepository,
|
||||
) {
|
||||
@ -31,6 +34,7 @@ class PaymentService(
|
||||
): PaymentCreateResponse {
|
||||
log.debug { "[PaymentService.createPayment] 결제 정보 저장 시작: request=$approveResponse, reservationId=${reservation.id}" }
|
||||
val payment = PaymentEntity(
|
||||
_id = tsidFactory.next(),
|
||||
orderId = approveResponse.orderId,
|
||||
paymentKey = approveResponse.paymentKey,
|
||||
totalAmount = approveResponse.totalAmount,
|
||||
@ -62,6 +66,7 @@ class PaymentService(
|
||||
", cancelInfo=$cancelInfo"
|
||||
}
|
||||
val canceledPayment = CanceledPaymentEntity(
|
||||
_id = tsidFactory.next(),
|
||||
paymentKey = paymentKey,
|
||||
cancelReason = cancelInfo.cancelReason,
|
||||
cancelAmount = cancelInfo.cancelAmount,
|
||||
@ -110,6 +115,7 @@ class PaymentService(
|
||||
}
|
||||
|
||||
val canceledPayment = CanceledPaymentEntity(
|
||||
_id = tsidFactory.next(),
|
||||
paymentKey = paymentKey,
|
||||
cancelReason = cancelReason,
|
||||
cancelAmount = payment.totalAmount,
|
||||
|
||||
@ -1,18 +1,33 @@
|
||||
package roomescape.payment.infrastructure.persistence
|
||||
|
||||
import jakarta.persistence.*
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import roomescape.common.entity.BaseEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity
|
||||
@Table(name = "canceled_payments")
|
||||
class CanceledPaymentEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
@Column(name = "canceled_payment_id")
|
||||
private var _id: Long?,
|
||||
|
||||
@Column(name = "payment_key", nullable = false)
|
||||
var paymentKey: String,
|
||||
|
||||
@Column(name = "cancel_reason", nullable = false)
|
||||
var cancelReason: String,
|
||||
|
||||
@Column(name = "cancel_amount", nullable = false)
|
||||
var cancelAmount: Long,
|
||||
|
||||
@Column(name = "approved_at", nullable = false)
|
||||
var approvedAt: OffsetDateTime,
|
||||
|
||||
@Column(name = "canceled_at", nullable = false)
|
||||
var canceledAt: OffsetDateTime,
|
||||
)
|
||||
): BaseEntity() {
|
||||
override fun getId(): Long? = _id
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package roomescape.payment.infrastructure.persistence
|
||||
|
||||
import jakarta.persistence.*
|
||||
import roomescape.common.entity.BaseEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@ -8,22 +9,24 @@ import java.time.OffsetDateTime
|
||||
@Table(name = "payments")
|
||||
class PaymentEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
@Column(name = "payment_id")
|
||||
private var _id: Long?,
|
||||
|
||||
@Column(nullable = false)
|
||||
@Column(name = "order_id", nullable = false)
|
||||
var orderId: String,
|
||||
|
||||
@Column(nullable = false)
|
||||
@Column(name="payment_key", nullable = false)
|
||||
var paymentKey: String,
|
||||
|
||||
@Column(nullable = false)
|
||||
@Column(name="total_amount", nullable = false)
|
||||
var totalAmount: Long,
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "reservation_id", nullable = false)
|
||||
var reservation: ReservationEntity,
|
||||
|
||||
@Column(nullable = false)
|
||||
@Column(name="approved_at", nullable = false)
|
||||
var approvedAt: OffsetDateTime
|
||||
)
|
||||
): BaseEntity() {
|
||||
override fun getId(): Long? = _id
|
||||
}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import com.github.f4b6a3.tsid.TsidFactory
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.data.jpa.domain.Specification
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.common.config.next
|
||||
import roomescape.member.business.MemberService
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.reservation.exception.ReservationErrorCode
|
||||
@ -26,6 +28,7 @@ private val log = KotlinLogging.logger {}
|
||||
@Service
|
||||
@Transactional
|
||||
class ReservationService(
|
||||
private val tsidFactory: TsidFactory,
|
||||
private val reservationRepository: ReservationRepository,
|
||||
private val timeService: TimeService,
|
||||
private val memberService: MemberService,
|
||||
@ -79,7 +82,7 @@ class ReservationService(
|
||||
createEntity(timeId, themeId, date, memberId, ReservationStatus.CONFIRMED)
|
||||
|
||||
return reservationRepository.save(reservation)
|
||||
.also { log.info { "[ReservationService.createConfirmedReservation] 예약 추가 완료: reservationId=${it.id}, status=${it.reservationStatus}" } }
|
||||
.also { log.info { "[ReservationService.createConfirmedReservation] 예약 추가 완료: reservationId=${it.id}, status=${it.status}" } }
|
||||
}
|
||||
|
||||
fun createReservationByAdmin(request: AdminReservationCreateRequest): ReservationRetrieveResponse {
|
||||
@ -188,11 +191,12 @@ class ReservationService(
|
||||
validateDateAndTime(date, time)
|
||||
|
||||
return ReservationEntity(
|
||||
_id = tsidFactory.next(),
|
||||
date = date,
|
||||
time = time,
|
||||
theme = theme,
|
||||
member = member,
|
||||
reservationStatus = status
|
||||
status = status
|
||||
)
|
||||
}
|
||||
|
||||
@ -259,7 +263,7 @@ class ReservationService(
|
||||
if (!reservation.isWaiting()) {
|
||||
log.warn {
|
||||
"[ReservationService.deleteWaiting] 대기 취소 실패(대기 예약이 아님): reservationId=$reservationId" +
|
||||
", currentStatus=${reservation.reservationStatus} memberId=$memberId"
|
||||
", currentStatus=${reservation.status} memberId=$memberId"
|
||||
}
|
||||
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
||||
}
|
||||
@ -284,7 +288,7 @@ class ReservationService(
|
||||
if (!reservation.isWaiting()) {
|
||||
log.warn {
|
||||
"[ReservationService.rejectWaiting] 대기 예약 삭제 실패(이미 확정 상태): reservationId=$reservationId" +
|
||||
", status=${reservation.reservationStatus}"
|
||||
", status=${reservation.status}"
|
||||
}
|
||||
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package roomescape.reservation.infrastructure.persistence
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import jakarta.persistence.*
|
||||
import roomescape.common.entity.BaseEntity
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||
@ -11,9 +12,10 @@ import java.time.LocalDate
|
||||
@Table(name = "reservations")
|
||||
class ReservationEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
@Column(name = "reservation_id")
|
||||
private var _id: Long?,
|
||||
|
||||
@Column(name = "date", nullable = false)
|
||||
var date: LocalDate,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@ -29,10 +31,13 @@ class ReservationEntity(
|
||||
var member: MemberEntity,
|
||||
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
var reservationStatus: ReservationStatus
|
||||
) {
|
||||
@Column(name = "status", nullable = false, length = 30)
|
||||
var status: ReservationStatus,
|
||||
): BaseEntity() {
|
||||
override fun getId(): Long? = _id
|
||||
|
||||
@JsonIgnore
|
||||
fun isWaiting(): Boolean = reservationStatus == ReservationStatus.WAITING
|
||||
fun isWaiting(): Boolean = status == ReservationStatus.WAITING
|
||||
|
||||
@JsonIgnore
|
||||
fun isReservedBy(memberId: Long): Boolean {
|
||||
|
||||
@ -19,7 +19,7 @@ interface ReservationRepository
|
||||
@Query(
|
||||
"""
|
||||
UPDATE ReservationEntity r
|
||||
SET r.reservationStatus = :status
|
||||
SET r.status = :status
|
||||
WHERE r.id = :id
|
||||
"""
|
||||
)
|
||||
@ -39,7 +39,7 @@ interface ReservationRepository
|
||||
WHERE r.theme.id = r2.theme.id
|
||||
AND r.time.id = r2.time.id
|
||||
AND r.date = r2.date
|
||||
AND r.reservationStatus != 'WAITING'
|
||||
AND r.status != 'WAITING'
|
||||
)
|
||||
)
|
||||
"""
|
||||
@ -53,7 +53,7 @@ interface ReservationRepository
|
||||
t.name,
|
||||
r.date,
|
||||
r.time.startAt,
|
||||
r.reservationStatus,
|
||||
r.status,
|
||||
(SELECT COUNT (r2) * 1L FROM ReservationEntity r2 WHERE r2.theme = r.theme AND r2.date = r.date AND r2.time = r.time AND r2.id < r.id),
|
||||
p.paymentKey,
|
||||
p.totalAmount
|
||||
|
||||
@ -36,11 +36,11 @@ class ReservationSearchSpecification(
|
||||
fun confirmed(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
|
||||
cb.or(
|
||||
cb.equal(
|
||||
root.get<ReservationStatus>("reservationStatus"),
|
||||
root.get<ReservationStatus>("status"),
|
||||
ReservationStatus.CONFIRMED
|
||||
),
|
||||
cb.equal(
|
||||
root.get<ReservationStatus>("reservationStatus"),
|
||||
root.get<ReservationStatus>("status"),
|
||||
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
)
|
||||
)
|
||||
@ -48,7 +48,7 @@ class ReservationSearchSpecification(
|
||||
|
||||
fun waiting(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
|
||||
cb.equal(
|
||||
root.get<ReservationStatus>("reservationStatus"),
|
||||
root.get<ReservationStatus>("status"),
|
||||
ReservationStatus.WAITING
|
||||
)
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ fun ReservationEntity.toRetrieveResponse(): ReservationRetrieveResponse = Reserv
|
||||
member = this.member.toRetrieveResponse(),
|
||||
time = this.time.toCreateResponse(),
|
||||
theme = this.theme.toResponse(),
|
||||
status = this.reservationStatus
|
||||
status = this.status
|
||||
)
|
||||
|
||||
data class ReservationRetrieveListResponse(
|
||||
|
||||
@ -1,20 +1,26 @@
|
||||
package roomescape.theme.business
|
||||
|
||||
import com.github.f4b6a3.tsid.TsidFactory
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.common.config.next
|
||||
import roomescape.theme.exception.ThemeErrorCode
|
||||
import roomescape.theme.exception.ThemeException
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||
import roomescape.theme.web.*
|
||||
import roomescape.theme.web.ThemeCreateRequest
|
||||
import roomescape.theme.web.ThemeRetrieveListResponse
|
||||
import roomescape.theme.web.ThemeRetrieveResponse
|
||||
import roomescape.theme.web.toResponse
|
||||
import java.time.LocalDate
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class ThemeService(
|
||||
private val tsidFactory: TsidFactory,
|
||||
private val themeRepository: ThemeRepository,
|
||||
) {
|
||||
@Transactional(readOnly = true)
|
||||
@ -60,7 +66,12 @@ class ThemeService(
|
||||
throw ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED)
|
||||
}
|
||||
|
||||
val theme: ThemeEntity = request.toEntity()
|
||||
val theme = ThemeEntity(
|
||||
_id = tsidFactory.next(),
|
||||
name = request.name,
|
||||
description = request.description,
|
||||
thumbnail = request.thumbnail
|
||||
)
|
||||
return themeRepository.save(theme)
|
||||
.also { log.info { "[ThemeService.createTheme] 테마 생성 완료: themeId=${it.id}" } }
|
||||
.toResponse()
|
||||
|
||||
@ -1,15 +1,26 @@
|
||||
package roomescape.theme.infrastructure.persistence
|
||||
|
||||
import jakarta.persistence.*
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import roomescape.common.entity.BaseEntity
|
||||
|
||||
@Entity
|
||||
@Table(name = "themes")
|
||||
class ThemeEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
@Column(name = "theme_id")
|
||||
private var _id: Long?,
|
||||
|
||||
@Column(name = "name", nullable = false)
|
||||
var name: String,
|
||||
|
||||
@Column(name = "description", nullable = false)
|
||||
var description: String,
|
||||
var thumbnail: String
|
||||
)
|
||||
|
||||
@Column(name = "thumbnail", nullable = false)
|
||||
var thumbnail: String,
|
||||
): BaseEntity() {
|
||||
override fun getId(): Long? = _id
|
||||
}
|
||||
|
||||
@ -21,12 +21,6 @@ data class ThemeCreateRequest(
|
||||
val thumbnail: String
|
||||
)
|
||||
|
||||
fun ThemeCreateRequest.toEntity(): ThemeEntity = ThemeEntity(
|
||||
name = this.name,
|
||||
description = this.description,
|
||||
thumbnail = this.thumbnail
|
||||
)
|
||||
|
||||
data class ThemeRetrieveResponse(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
package roomescape.time.business
|
||||
|
||||
import com.github.f4b6a3.tsid.TsidFactory
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.common.config.next
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
||||
import roomescape.time.exception.TimeErrorCode
|
||||
@ -18,6 +20,7 @@ private val log = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class TimeService(
|
||||
private val tsidFactory: TsidFactory,
|
||||
private val timeRepository: TimeRepository,
|
||||
private val reservationRepository: ReservationRepository,
|
||||
) {
|
||||
@ -50,7 +53,10 @@ class TimeService(
|
||||
throw TimeException(TimeErrorCode.TIME_DUPLICATED)
|
||||
}
|
||||
|
||||
val time: TimeEntity = request.toEntity()
|
||||
val time = TimeEntity(
|
||||
_id = tsidFactory.next(),
|
||||
startAt = request.startAt
|
||||
)
|
||||
return timeRepository.save(time)
|
||||
.also { log.info { "[TimeService.createTime] 시간 생성 완료: timeId=${it.id}" } }
|
||||
.toCreateResponse()
|
||||
|
||||
@ -1,13 +1,21 @@
|
||||
package roomescape.time.infrastructure.persistence
|
||||
|
||||
import jakarta.persistence.*
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import roomescape.common.entity.BaseEntity
|
||||
import java.time.LocalTime
|
||||
|
||||
@Entity
|
||||
@Table(name = "times")
|
||||
class TimeEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
var startAt: LocalTime
|
||||
)
|
||||
@Column(name = "time_id")
|
||||
private var _id: Long?,
|
||||
|
||||
@Column(name = "start_at", nullable = false)
|
||||
var startAt: LocalTime,
|
||||
): BaseEntity() {
|
||||
override fun getId(): Long? = _id
|
||||
}
|
||||
|
||||
@ -10,8 +10,6 @@ data class TimeCreateRequest(
|
||||
val startAt: LocalTime
|
||||
)
|
||||
|
||||
fun TimeCreateRequest.toEntity(): TimeEntity = TimeEntity(startAt = this.startAt)
|
||||
|
||||
@Schema(name = "예약 시간 정보", description = "예약 시간 추가 및 조회 응답시 사용됩니다.")
|
||||
data class TimeCreateResponse(
|
||||
@Schema(description = "시간 식별자")
|
||||
|
||||
42
src/main/resources/application-deploy.yaml
Normal file
42
src/main/resources/application-deploy.yaml
Normal file
@ -0,0 +1,42 @@
|
||||
server:
|
||||
forward-headers-strategy: framework
|
||||
|
||||
spring:
|
||||
sql:
|
||||
init:
|
||||
schema-locations: classpath:schema/schema-mysql.sql
|
||||
jpa:
|
||||
defer-datasource-initialization: false
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
datasource:
|
||||
hikari:
|
||||
driver-class-name: ${DATASOURCE_DRIVER_CLASS_NAME}
|
||||
jdbc-url: ${DATASOURCE_URL}
|
||||
username: ${DATASOURCE_USERNAME}
|
||||
password: ${DATASOURCE_PASSWORD}
|
||||
|
||||
security:
|
||||
jwt:
|
||||
token:
|
||||
secret-key: ${JWT_SECRET_KEY}
|
||||
ttl-seconds: ${JWT_TOKEN_TTL_SECONDS}
|
||||
|
||||
payment:
|
||||
confirm-secret-key: ${TOSS_SECRET_KEY}
|
||||
read-timeout: ${PAYMENT_CLIENT_READ_TIMEOUT}
|
||||
connect-timeout: ${PAYMENT_CLIENT_CONNECT_TIMEOUT}
|
||||
|
||||
slow-query:
|
||||
logger-name: ${SLOW_QUERY_LOGGER}
|
||||
log-level: ${SLOW_QUERY_LOG_LEVEL}
|
||||
threshold_ms: ${SLOW_QUERY_LOGGING_THRESHOLD}
|
||||
|
||||
management:
|
||||
tracing:
|
||||
sampling:
|
||||
probability: ${TRACE_SAMPLING_PROBABILITY}
|
||||
otlp:
|
||||
tracing:
|
||||
transport: ${OTLP_TRACING_PROTOCOL}
|
||||
endpoint: ${OTLP_TRACING_ENDPOINT}
|
||||
@ -1,27 +1,29 @@
|
||||
spring:
|
||||
jpa:
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
ddl-auto: create-drop
|
||||
defer-datasource-initialization: true
|
||||
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
path: /h2-console
|
||||
datasource:
|
||||
driver-class-name: org.h2.Driver
|
||||
url: jdbc:h2:mem:database
|
||||
username: sa
|
||||
password:
|
||||
hikari:
|
||||
jdbc-url: jdbc:h2:mem:database
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
sql:
|
||||
init:
|
||||
schema-locations: classpath:schema/schema-h2.sql
|
||||
|
||||
security:
|
||||
jwt:
|
||||
token:
|
||||
secret-key: daijawligagaf@LIJ$@U)9nagnalkkgalijaddljfi
|
||||
ttl-seconds: 1800000
|
||||
ttl-seconds: 1800
|
||||
|
||||
payment:
|
||||
confirm-secret-key: test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6
|
||||
@ -35,7 +37,7 @@ jdbc:
|
||||
query:
|
||||
enable-logging: true
|
||||
log-level: DEBUG
|
||||
logger-name: query-logger
|
||||
logger-name: all-query-logger
|
||||
multiline: true
|
||||
includes: connection,query,keys,fetch
|
||||
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
insert into times(start_at)
|
||||
values ('15:00');
|
||||
insert into times(start_at)
|
||||
values ('16:00');
|
||||
insert into times(start_at)
|
||||
values ('17:00');
|
||||
insert into times(start_at)
|
||||
values ('18:00');
|
||||
|
||||
insert into themes(name, description, thumbnail)
|
||||
values ('테스트1', '테스트중', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg');
|
||||
insert into themes(name, description, thumbnail)
|
||||
values ('테스트2', '테스트중', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg');
|
||||
insert into themes(name, description, thumbnail)
|
||||
values ('테스트3', '테스트중', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg');
|
||||
insert into themes(name, description, thumbnail)
|
||||
values ('테스트4', '테스트중', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg');
|
||||
|
||||
insert into members(name, email, password, role)
|
||||
values ('어드민', 'a@a.a', 'a', 'ADMIN');
|
||||
insert into members(name, email, password, role)
|
||||
values ('1호', '1@1.1', '1', 'MEMBER');
|
||||
insert into members(name, email, password, role)
|
||||
values ('2호', '2@2.2', '2', 'MEMBER');
|
||||
insert into members(name, email, password, role)
|
||||
values ('3호', '3@3.3', '3', 'MEMBER');
|
||||
insert into members(name, email, password, role)
|
||||
values ('4호', '4@4.4', '4', 'MEMBER');
|
||||
|
||||
-- 예약: 결제 완료
|
||||
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
|
||||
values (1, DATEADD('DAY', -1, CURRENT_DATE()) - 1, 1, 1, 'CONFIRMED');
|
||||
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
|
||||
values (2, DATEADD('DAY', -2, CURRENT_DATE()) - 2, 3, 2, 'CONFIRMED');
|
||||
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
|
||||
values (3, DATEADD('DAY', -3, CURRENT_DATE()), 2, 2, 'CONFIRMED');
|
||||
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
|
||||
values (4, DATEADD('DAY', -4, CURRENT_DATE()), 1, 2, 'CONFIRMED');
|
||||
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
|
||||
values (5, DATEADD('DAY', -5, CURRENT_DATE()), 1, 3, 'CONFIRMED');
|
||||
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
|
||||
values (2, DATEADD('DAY', 7, CURRENT_DATE()), 2, 4, 'CONFIRMED');
|
||||
|
||||
-- 예약: 결제 대기
|
||||
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
|
||||
values (2, DATEADD('DAY', 8, CURRENT_DATE()), 2, 4, 'CONFIRMED_PAYMENT_REQUIRED');
|
||||
|
||||
-- 예약 대기
|
||||
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
|
||||
values (3, DATEADD('DAY', 7, CURRENT_DATE()), 2, 4, 'WAITING');
|
||||
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
|
||||
values (4, DATEADD('DAY', 7, CURRENT_DATE()), 2, 4, 'WAITING');
|
||||
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
|
||||
values (5, DATEADD('DAY', 7, CURRENT_DATE()), 2, 4, 'WAITING');
|
||||
|
||||
-- 결제 정보
|
||||
insert into payments(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-1', 'paymentKey-1', 10000, 1, CURRENT_DATE);
|
||||
insert into payments(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-2', 'paymentKey-2', 20000, 2, CURRENT_DATE);
|
||||
insert into payments(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-3', 'paymentKey-3', 30000, 3, CURRENT_DATE);
|
||||
insert into payments(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-4', 'paymentKey-4', 40000, 4, CURRENT_DATE);
|
||||
insert into payments(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-5', 'paymentKey-5', 50000, 5, CURRENT_DATE);
|
||||
insert into payments(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-6', 'paymentKey-6', 60000, 6, CURRENT_DATE);
|
||||
95
src/main/resources/logback-deploy.xml
Normal file
95
src/main/resources/logback-deploy.xml
Normal file
@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<included>
|
||||
<conversionRule conversionWord="maskedMessage"
|
||||
class="roomescape.common.log.RoomescapeLogMaskingConverter"/>
|
||||
|
||||
<appender name="CONSOLE_JSON" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
|
||||
<providers>
|
||||
<timestamp>
|
||||
<fieldName>timestamp</fieldName>
|
||||
<timeZone>UTC</timeZone>
|
||||
</timestamp>
|
||||
<logLevel>
|
||||
<fieldName>level</fieldName>
|
||||
</logLevel>
|
||||
<loggerName>
|
||||
<fieldName>logger</fieldName>
|
||||
</loggerName>
|
||||
<threadName>
|
||||
<fieldName>thread</fieldName>
|
||||
</threadName>
|
||||
<mdc/>
|
||||
<pattern>
|
||||
<pattern>
|
||||
{
|
||||
"message": "%maskedMessage"
|
||||
}
|
||||
</pattern>
|
||||
</pattern>
|
||||
<stackTrace>
|
||||
<fieldName>stack_trace</fieldName>
|
||||
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
|
||||
<maxDepthPerThrowable>5</maxDepthPerThrowable>
|
||||
<maxLength>2048</maxLength>
|
||||
<rootCauseFirst>true</rootCauseFirst>
|
||||
</throwableConverter>
|
||||
</stackTrace>
|
||||
</providers>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
|
||||
<appender name="FILE_JSON" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>logs/application-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
|
||||
<maxFileSize>100MB</maxFileSize>
|
||||
<maxHistory>3</maxHistory>
|
||||
<totalSizeCap>1GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
|
||||
<providers>
|
||||
<timestamp>
|
||||
<fieldName>timestamp</fieldName>
|
||||
<timeZone>UTC</timeZone>
|
||||
</timestamp>
|
||||
<logLevel>
|
||||
<fieldName>level</fieldName>
|
||||
</logLevel>
|
||||
<loggerName>
|
||||
<fieldName>logger</fieldName>
|
||||
</loggerName>
|
||||
<threadName>
|
||||
<fieldName>thread</fieldName>
|
||||
</threadName>
|
||||
<mdc/>
|
||||
<pattern>
|
||||
<pattern>
|
||||
{
|
||||
"message": "%maskedMessage"
|
||||
}
|
||||
</pattern>
|
||||
</pattern>
|
||||
<stackTrace>
|
||||
<fieldName>stack_trace</fieldName>
|
||||
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
|
||||
<maxDepthPerThrowable>5</maxDepthPerThrowable>
|
||||
<maxLength>2048</maxLength>
|
||||
<rootCauseFirst>true</rootCauseFirst>
|
||||
</throwableConverter>
|
||||
</stackTrace>
|
||||
</providers>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="ASYNC_FILE_JSON" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<appender-ref ref="FILE_JSON"/>
|
||||
<queueSize>512</queueSize>
|
||||
<discardingThreshold>0</discardingThreshold> <includeCallerData>false</includeCallerData>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE_JSON"/>
|
||||
<appender-ref ref="ASYNC_FILE_JSON"/>
|
||||
</root>
|
||||
</included>
|
||||
@ -20,7 +20,7 @@
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
<logger name="query-logger" level="debug" additivity="false">
|
||||
<logger name="all-query-logger" level="debug" additivity="false">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
</included>
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration scan="true">
|
||||
<springProfile name="deploy">
|
||||
<include resource="logback-deploy.xml"/>
|
||||
</springProfile>
|
||||
|
||||
<springProfile name="local">
|
||||
<include resource="logback-local.xml"/>
|
||||
</springProfile>
|
||||
|
||||
63
src/main/resources/schema/schema-h2.sql
Normal file
63
src/main/resources/schema/schema-h2.sql
Normal file
@ -0,0 +1,63 @@
|
||||
create table if not exists members (
|
||||
member_id bigint primary key,
|
||||
email varchar(255) not null,
|
||||
name varchar(255) not null,
|
||||
password varchar(255) not null,
|
||||
role varchar(20) not null,
|
||||
created_at timestamp null,
|
||||
last_modified_at timestamp null
|
||||
);
|
||||
|
||||
create table if not exists themes (
|
||||
theme_id bigint primary key,
|
||||
description varchar(255) not null,
|
||||
name varchar(255) not null,
|
||||
thumbnail varchar(255) not null,
|
||||
created_at timestamp null,
|
||||
last_modified_at timestamp null
|
||||
);
|
||||
|
||||
create table if not exists times (
|
||||
time_id bigint primary key,
|
||||
start_at time not null,
|
||||
created_at timestamp null,
|
||||
last_modified_at timestamp null
|
||||
);
|
||||
|
||||
create table if not exists reservations (
|
||||
reservation_id bigint primary key,
|
||||
date date not null,
|
||||
member_id bigint not null,
|
||||
theme_id bigint not null,
|
||||
time_id bigint not null,
|
||||
status varchar(30) not null,
|
||||
created_at timestamp null,
|
||||
last_modified_at timestamp null,
|
||||
constraint fk_reservations__themeId foreign key (theme_id) references themes (theme_id),
|
||||
constraint fk_reservations__memberId foreign key (member_id) references members (member_id),
|
||||
constraint fk_reservations__timeId foreign key (time_id) references times (time_id)
|
||||
);
|
||||
|
||||
create table if not exists payments (
|
||||
payment_id bigint primary key,
|
||||
approved_at timestamp not null,
|
||||
reservation_id bigint not null,
|
||||
total_amount bigint not null,
|
||||
order_id varchar(255) not null,
|
||||
payment_key varchar(255) not null,
|
||||
created_at timestamp null,
|
||||
last_modified_at timestamp null,
|
||||
constraint uk_payments__reservationId unique (reservation_id),
|
||||
constraint fk_payments__reservationId foreign key (reservation_id) references reservations (reservation_id)
|
||||
);
|
||||
|
||||
create table if not exists canceled_payments (
|
||||
canceled_payment_id bigint primary key,
|
||||
payment_key varchar(255) not null,
|
||||
cancel_reason varchar(255) not null,
|
||||
cancel_amount bigint not null,
|
||||
approved_at timestamp not null,
|
||||
canceled_at timestamp not null,
|
||||
created_at timestamp null,
|
||||
last_modified_at timestamp null
|
||||
);
|
||||
69
src/main/resources/schema/schema-mysql.sql
Normal file
69
src/main/resources/schema/schema-mysql.sql
Normal file
@ -0,0 +1,69 @@
|
||||
create table if not exists members
|
||||
(
|
||||
member_id bigint primary key,
|
||||
email varchar(255) not null,
|
||||
name varchar(255) not null,
|
||||
password varchar(255) not null,
|
||||
role varchar(20) not null,
|
||||
created_at datetime(6) null,
|
||||
last_modified_at datetime(6) null
|
||||
);
|
||||
|
||||
create table if not exists themes
|
||||
(
|
||||
theme_id bigint primary key,
|
||||
description varchar(255) not null,
|
||||
name varchar(255) not null,
|
||||
thumbnail varchar(255) not null,
|
||||
created_at datetime(6) null,
|
||||
last_modified_at datetime(6) null
|
||||
);
|
||||
|
||||
create table if not exists times
|
||||
(
|
||||
time_id bigint primary key,
|
||||
start_at time(6) not null,
|
||||
created_at datetime(6) null,
|
||||
last_modified_at datetime(6) null
|
||||
);
|
||||
|
||||
create table if not exists reservations
|
||||
(
|
||||
reservation_id bigint primary key,
|
||||
date date not null,
|
||||
member_id bigint not null,
|
||||
theme_id bigint not null,
|
||||
time_id bigint not null,
|
||||
status varchar(30) not null,
|
||||
created_at datetime(6) null,
|
||||
last_modified_at datetime(6) null,
|
||||
constraint fk_reservations__themeId foreign key (theme_id) references themes (theme_id),
|
||||
constraint fk_reservations__memberId foreign key (member_id) references members (member_id),
|
||||
constraint fk_reservations__timeId foreign key (time_id) references times (time_id)
|
||||
);
|
||||
|
||||
create table if not exists payments
|
||||
(
|
||||
payment_id bigint primary key,
|
||||
approved_at datetime(6) not null,
|
||||
reservation_id bigint not null,
|
||||
total_amount bigint not null,
|
||||
order_id varchar(255) not null,
|
||||
payment_key varchar(255) not null,
|
||||
created_at datetime(6) null,
|
||||
last_modified_at datetime(6) null,
|
||||
constraint uk_payments__reservationId unique (reservation_id),
|
||||
constraint fk_payments__reservationId foreign key (reservation_id) references reservations (reservation_id)
|
||||
);
|
||||
|
||||
create table if not exists canceled_payments
|
||||
(
|
||||
canceled_payment_id bigint primary key,
|
||||
payment_key varchar(255) not null,
|
||||
cancel_reason varchar(255) not null,
|
||||
cancel_amount bigint not null,
|
||||
approved_at datetime(6) not null,
|
||||
canceled_at datetime(6) not null,
|
||||
created_at datetime(6) null,
|
||||
last_modified_at datetime(6) null
|
||||
);
|
||||
@ -15,10 +15,11 @@ import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||
import roomescape.util.JwtFixture
|
||||
import roomescape.util.MemberFixture
|
||||
import roomescape.util.TsidFactory
|
||||
|
||||
class AuthServiceTest : BehaviorSpec({
|
||||
val memberRepository: MemberRepository = mockk()
|
||||
val memberService: MemberService = MemberService(memberRepository)
|
||||
val memberService = MemberService(TsidFactory, memberRepository)
|
||||
val jwtHandler: JwtHandler = JwtFixture.create()
|
||||
|
||||
val authService = AuthService(memberService, jwtHandler)
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
package roomescape.common.log
|
||||
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.core.spec.style.StringSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
|
||||
class MDCAwareSlowQueryListenerWithoutParamsTest : StringSpec({
|
||||
"SQL 메시지에서 Params 항목은 가린다." {
|
||||
val message = """Query:["select * from members m where m.email=?"], Params:[(a@a.a)]"""
|
||||
val expected = """Query:["select * from members m where m.email=?"]"""
|
||||
val result = SqlLogFormatter().maskParams(message)
|
||||
|
||||
result shouldBe expected
|
||||
}
|
||||
|
||||
"입력된 thresholdMs 보다 소요시간이 긴 쿼리를 기록한다." {
|
||||
val slowQueryThreshold = 10L
|
||||
val slowQueryPredicate = SlowQueryPredicate(thresholdMs = slowQueryThreshold)
|
||||
|
||||
assertSoftly(slowQueryPredicate) {
|
||||
it.test(slowQueryThreshold) shouldBe true
|
||||
it.test(slowQueryThreshold + 1) shouldBe true
|
||||
it.test(slowQueryThreshold - 1) shouldBe false
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -14,13 +14,14 @@ import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
|
||||
import roomescape.payment.infrastructure.persistence.PaymentRepository
|
||||
import roomescape.payment.web.PaymentCancelRequest
|
||||
import roomescape.util.PaymentFixture
|
||||
import roomescape.util.TsidFactory
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class PaymentServiceTest : FunSpec({
|
||||
val paymentRepository: PaymentRepository = mockk()
|
||||
val canceledPaymentRepository: CanceledPaymentRepository = mockk()
|
||||
|
||||
val paymentService = PaymentService(paymentRepository, canceledPaymentRepository)
|
||||
val paymentService = PaymentService(TsidFactory, paymentRepository, canceledPaymentRepository)
|
||||
|
||||
context("createCanceledPaymentByReservationId") {
|
||||
val reservationId = 1L
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
package roomescape.payment.infrastructure.client
|
||||
|
||||
import com.ninjasquad.springmockk.MockkBean
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest
|
||||
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
@ -20,6 +22,7 @@ import roomescape.payment.web.PaymentCancelRequest
|
||||
import roomescape.payment.web.PaymentCancelResponse
|
||||
|
||||
@RestClientTest(TossPaymentClient::class)
|
||||
@MockkBean(JpaMetamodelMappingContext::class)
|
||||
class TossPaymentClientTest(
|
||||
@Autowired val client: TossPaymentClient,
|
||||
@Autowired val mockServer: MockRestServiceServer
|
||||
|
||||
@ -5,10 +5,12 @@ import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import roomescape.common.config.next
|
||||
import roomescape.util.PaymentFixture
|
||||
import roomescape.util.TsidFactory
|
||||
import java.util.*
|
||||
|
||||
@DataJpaTest
|
||||
@DataJpaTest(showSql = false)
|
||||
class CanceledPaymentRepositoryTest(
|
||||
@Autowired val canceledPaymentRepository: CanceledPaymentRepository,
|
||||
) : FunSpec() {
|
||||
@ -16,7 +18,7 @@ class CanceledPaymentRepositoryTest(
|
||||
context("paymentKey로 CanceledPaymentEntity 조회") {
|
||||
val paymentKey = "test-payment-key"
|
||||
beforeTest {
|
||||
PaymentFixture.createCanceled(paymentKey = paymentKey)
|
||||
PaymentFixture.createCanceled(id = TsidFactory.next(), paymentKey = paymentKey)
|
||||
.also { canceledPaymentRepository.save(it) }
|
||||
}
|
||||
|
||||
|
||||
@ -6,11 +6,13 @@ import io.kotest.matchers.shouldBe
|
||||
import jakarta.persistence.EntityManager
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import roomescape.common.config.next
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.util.PaymentFixture
|
||||
import roomescape.util.ReservationFixture
|
||||
import roomescape.util.TsidFactory
|
||||
|
||||
@DataJpaTest
|
||||
@DataJpaTest(showSql = false)
|
||||
class PaymentRepositoryTest(
|
||||
@Autowired val paymentRepository: PaymentRepository,
|
||||
@Autowired val entityManager: EntityManager
|
||||
@ -91,7 +93,9 @@ class PaymentRepositoryTest(
|
||||
}
|
||||
|
||||
private fun setupReservation(): ReservationEntity {
|
||||
return ReservationFixture.create().also {
|
||||
return ReservationFixture.create(
|
||||
id = TsidFactory.next()
|
||||
).also {
|
||||
entityManager.persist(it.member)
|
||||
entityManager.persist(it.theme)
|
||||
entityManager.persist(it.time)
|
||||
|
||||
@ -16,6 +16,7 @@ import roomescape.theme.business.ThemeService
|
||||
import roomescape.time.business.TimeService
|
||||
import roomescape.util.MemberFixture
|
||||
import roomescape.util.ReservationFixture
|
||||
import roomescape.util.TsidFactory
|
||||
import roomescape.util.TimeFixture
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
@ -27,6 +28,7 @@ class ReservationServiceTest : FunSpec({
|
||||
val memberService: MemberService = mockk()
|
||||
val themeService: ThemeService = mockk()
|
||||
val reservationService = ReservationService(
|
||||
TsidFactory,
|
||||
reservationRepository,
|
||||
timeService,
|
||||
memberService,
|
||||
|
||||
@ -15,7 +15,7 @@ import roomescape.util.ReservationFixture
|
||||
import roomescape.util.ThemeFixture
|
||||
import roomescape.util.TimeFixture
|
||||
|
||||
@DataJpaTest
|
||||
@DataJpaTest(showSql = false)
|
||||
class ReservationRepositoryTest(
|
||||
val entityManager: EntityManager,
|
||||
val reservationRepository: ReservationRepository,
|
||||
@ -106,7 +106,7 @@ class ReservationRepositoryTest(
|
||||
entityManager.clear()
|
||||
|
||||
reservationRepository.findByIdOrNull(reservationId)?.also { updated ->
|
||||
updated.reservationStatus shouldBe it
|
||||
updated.status shouldBe it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ import roomescape.util.ThemeFixture
|
||||
import roomescape.util.TimeFixture
|
||||
import java.time.LocalDate
|
||||
|
||||
@DataJpaTest
|
||||
@DataJpaTest(showSql = false)
|
||||
class ReservationSearchSpecificationTest(
|
||||
val entityManager: EntityManager,
|
||||
val reservationRepository: ReservationRepository
|
||||
|
||||
@ -39,7 +39,7 @@ import java.time.LocalTime
|
||||
class ReservationControllerTest(
|
||||
@LocalServerPort val port: Int,
|
||||
val entityManager: EntityManager,
|
||||
val transactionTemplate: TransactionTemplate
|
||||
val transactionTemplate: TransactionTemplate,
|
||||
) : FunSpec({
|
||||
extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_EACH_TEST))
|
||||
}) {
|
||||
@ -55,24 +55,34 @@ class ReservationControllerTest(
|
||||
@MockkBean
|
||||
lateinit var jwtHandler: JwtHandler
|
||||
|
||||
lateinit var testDataHelper: TestDataHelper
|
||||
|
||||
fun login(member: MemberEntity) {
|
||||
every { jwtHandler.getMemberIdFromToken(any()) } returns member.id!!
|
||||
every { memberService.findById(member.id!!) } returns member
|
||||
every { memberIdResolver.resolveArgument(any(), any(), any(), any()) } returns member.id!!
|
||||
}
|
||||
|
||||
init {
|
||||
beforeSpec {
|
||||
testDataHelper = TestDataHelper(entityManager, transactionTemplate)
|
||||
}
|
||||
|
||||
context("POST /reservations") {
|
||||
lateinit var member: MemberEntity
|
||||
beforeTest {
|
||||
member = login(MemberFixture.create(role = Role.MEMBER))
|
||||
val member = testDataHelper.createMember(role = Role.MEMBER)
|
||||
login(member)
|
||||
}
|
||||
|
||||
test("정상 응답") {
|
||||
val reservationRequest = createRequest()
|
||||
val reservationRequest = testDataHelper.createReservationRequest()
|
||||
val paymentApproveResponse = PaymentFixture.createApproveResponse().copy(
|
||||
paymentKey = reservationRequest.paymentKey,
|
||||
orderId = reservationRequest.orderId,
|
||||
totalAmount = reservationRequest.amount,
|
||||
)
|
||||
|
||||
every {
|
||||
paymentClient.confirm(any())
|
||||
} returns paymentApproveResponse
|
||||
every { paymentClient.confirm(any()) } returns paymentApproveResponse
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
@ -88,12 +98,10 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
test("결제 과정에서 발생하는 에러는 그대로 응답") {
|
||||
val reservationRequest = createRequest()
|
||||
val reservationRequest = testDataHelper.createReservationRequest()
|
||||
val paymentException = PaymentException(PaymentErrorCode.PAYMENT_PROVIDER_ERROR)
|
||||
|
||||
every {
|
||||
paymentClient.confirm(any())
|
||||
} throws paymentException
|
||||
every { paymentClient.confirm(any()) } throws paymentException
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
@ -108,24 +116,20 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
test("결제 완료 후 예약 / 결제 정보 저장 과정에서 에러 발생시 결제 취소 후 에러 응답을 받는다.") {
|
||||
val reservationRequest = createRequest()
|
||||
val reservationRequest = testDataHelper.createReservationRequest()
|
||||
val paymentApproveResponse = PaymentFixture.createApproveResponse().copy(
|
||||
paymentKey = reservationRequest.paymentKey,
|
||||
orderId = reservationRequest.orderId,
|
||||
totalAmount = reservationRequest.amount,
|
||||
)
|
||||
|
||||
every {
|
||||
paymentClient.confirm(any())
|
||||
} returns paymentApproveResponse
|
||||
every { paymentClient.confirm(any()) } returns paymentApproveResponse
|
||||
|
||||
// 예약 저장 과정에서 테마가 없는 예외
|
||||
val invalidRequest = reservationRequest.copy(themeId = reservationRequest.themeId + 1)
|
||||
val expectedException = ThemeErrorCode.THEME_NOT_FOUND
|
||||
|
||||
every {
|
||||
paymentClient.cancel(any())
|
||||
} returns PaymentFixture.createCancelResponse()
|
||||
every { paymentClient.cancel(any()) } returns PaymentFixture.createCancelResponse()
|
||||
|
||||
val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery(
|
||||
"SELECT COUNT(c) FROM CanceledPaymentEntity c",
|
||||
@ -153,13 +157,13 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
context("GET /reservations") {
|
||||
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
|
||||
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = createDummyReservations()
|
||||
reservations = testDataHelper.createDummyReservations()
|
||||
}
|
||||
|
||||
test("관리자이면 정상 응답") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
@ -173,13 +177,14 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
context("GET /reservations-mine") {
|
||||
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
|
||||
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = createDummyReservations()
|
||||
reservations = testDataHelper.createDummyReservations()
|
||||
}
|
||||
|
||||
test("로그인한 회원이 자신의 예약 목록을 조회한다.") {
|
||||
val member: MemberEntity = login(reservations.keys.first())
|
||||
val member = reservations.keys.first()
|
||||
login(member)
|
||||
val expectedReservations: Int = reservations[member]?.size ?: 0
|
||||
|
||||
Given {
|
||||
@ -195,9 +200,9 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
context("GET /reservations/search") {
|
||||
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
|
||||
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = createDummyReservations()
|
||||
reservations = testDataHelper.createDummyReservations()
|
||||
}
|
||||
|
||||
test("관리자만 검색할 수 있다.") {
|
||||
@ -216,7 +221,7 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
test("파라미터를 지정하지 않으면 전체 목록 응답") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
@ -230,7 +235,7 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
test("시작 날짜가 종료 날짜 이전이면 예외 응답") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
|
||||
val startDate = LocalDate.now().plusDays(1)
|
||||
val endDate = LocalDate.now()
|
||||
@ -250,8 +255,8 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
test("동일한 회원의 모든 예약 응답") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
val member: MemberEntity = reservations.keys.first()
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val member = reservations.keys.first()
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
@ -266,7 +271,7 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
test("동일한 테마의 모든 예약 응답") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val themes = reservations.values.flatten().map { it.theme }
|
||||
val requestThemeId: Long = themes.first().id!!
|
||||
|
||||
@ -278,12 +283,12 @@ class ReservationControllerTest(
|
||||
get("/reservations/search")
|
||||
}.Then {
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(themes.filter { it.id == requestThemeId }.size))
|
||||
body("data.reservations.size()", equalTo(themes.count { it.id == requestThemeId }))
|
||||
}
|
||||
}
|
||||
|
||||
test("시작 날짜와 종료 날짜 사이의 예약 응답") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val dateFrom: LocalDate = reservations.values.flatten().minOf { it.date }
|
||||
val dateTo: LocalDate = reservations.values.flatten().maxOf { it.date }
|
||||
|
||||
@ -302,14 +307,14 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
context("DELETE /reservations/{id}") {
|
||||
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
|
||||
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = createDummyReservations()
|
||||
reservations = testDataHelper.createDummyReservations()
|
||||
}
|
||||
|
||||
test("관리자만 예약을 삭제할 수 있다.") {
|
||||
login(MemberFixture.create(role = Role.MEMBER))
|
||||
val reservation: ReservationEntity = reservations.values.flatten().first()
|
||||
login(testDataHelper.createMember(role = Role.MEMBER))
|
||||
val reservation = reservations.values.flatten().first()
|
||||
val expectedError = AuthErrorCode.ACCESS_DENIED
|
||||
|
||||
Given {
|
||||
@ -323,18 +328,12 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
test("결제되지 않은 예약은 바로 제거") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
val reservationId: Long = reservations.values.flatten().first().id!!
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val reservationId = reservations.values.flatten().first().id!!
|
||||
|
||||
transactionTemplate.execute {
|
||||
val reservation: ReservationEntity = entityManager.find(
|
||||
ReservationEntity::class.java,
|
||||
reservationId
|
||||
)
|
||||
reservation.reservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
entityManager.persist(reservation)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val reservation = entityManager.find(ReservationEntity::class.java, reservationId)
|
||||
reservation.status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
}
|
||||
|
||||
Given {
|
||||
@ -345,32 +344,18 @@ class ReservationControllerTest(
|
||||
statusCode(HttpStatus.NO_CONTENT.value())
|
||||
}
|
||||
|
||||
// 예약이 삭제되었는지 확인
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val deletedReservation = entityManager.find(
|
||||
ReservationEntity::class.java,
|
||||
reservationId
|
||||
)
|
||||
deletedReservation shouldBe null
|
||||
val deletedReservation = transactionTemplate.execute {
|
||||
entityManager.find(ReservationEntity::class.java, reservationId)
|
||||
}
|
||||
deletedReservation shouldBe null
|
||||
}
|
||||
|
||||
test("결제된 예약은 취소 후 제거") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
val reservation: ReservationEntity = reservations.values.flatten().first()
|
||||
lateinit var payment: PaymentEntity
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val reservation = reservations.values.flatten().first { it.status == ReservationStatus.CONFIRMED }
|
||||
testDataHelper.createPayment(reservation)
|
||||
|
||||
transactionTemplate.execute {
|
||||
payment = PaymentFixture.create(reservation = reservation).also {
|
||||
entityManager.persist(it)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
}
|
||||
|
||||
every {
|
||||
paymentClient.cancel(any())
|
||||
} returns PaymentFixture.createCancelResponse()
|
||||
every { paymentClient.cancel(any()) } returns PaymentFixture.createCancelResponse()
|
||||
|
||||
val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery(
|
||||
"SELECT COUNT(c) FROM CanceledPaymentEntity c",
|
||||
@ -396,15 +381,17 @@ class ReservationControllerTest(
|
||||
|
||||
context("POST /reservations/admin") {
|
||||
test("관리자가 예약을 추가하면 결제 대기 상태로 예약 생성") {
|
||||
val member = login(MemberFixture.create(role = Role.ADMIN))
|
||||
val adminRequest: AdminReservationCreateRequest = createRequest().let {
|
||||
AdminReservationCreateRequest(
|
||||
date = it.date,
|
||||
themeId = it.themeId,
|
||||
timeId = it.timeId,
|
||||
memberId = member.id!!,
|
||||
)
|
||||
}
|
||||
val admin = testDataHelper.createMember(role = Role.ADMIN)
|
||||
login(admin)
|
||||
val theme = testDataHelper.createTheme()
|
||||
val time = testDataHelper.createTime()
|
||||
|
||||
val adminRequest = AdminReservationCreateRequest(
|
||||
date = LocalDate.now().plusDays(1),
|
||||
themeId = theme.id!!,
|
||||
timeId = time.id!!,
|
||||
memberId = admin.id!!,
|
||||
)
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
@ -420,13 +407,13 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
context("GET /reservations/waiting") {
|
||||
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
|
||||
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = createDummyReservations()
|
||||
reservations = testDataHelper.createDummyReservations(reservationCount = 5)
|
||||
}
|
||||
|
||||
test("관리자가 아니면 조회할 수 없다.") {
|
||||
login(MemberFixture.create(role = Role.MEMBER))
|
||||
login(testDataHelper.createMember(role = Role.MEMBER))
|
||||
val expectedError = AuthErrorCode.ACCESS_DENIED
|
||||
|
||||
Given {
|
||||
@ -441,9 +428,9 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
test("대기 중인 예약 목록을 조회한다.") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val expected = reservations.values.flatten()
|
||||
.count { it.reservationStatus == ReservationStatus.WAITING }
|
||||
.count { it.status == ReservationStatus.WAITING }
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
@ -459,14 +446,16 @@ class ReservationControllerTest(
|
||||
|
||||
context("POST /reservations/waiting") {
|
||||
test("회원이 대기 예약을 추가한다.") {
|
||||
val member = login(MemberFixture.create(role = Role.MEMBER))
|
||||
val waitingCreateRequest: WaitingCreateRequest = createRequest().let {
|
||||
WaitingCreateRequest(
|
||||
date = it.date,
|
||||
themeId = it.themeId,
|
||||
timeId = it.timeId
|
||||
)
|
||||
}
|
||||
val member = testDataHelper.createMember(role = Role.MEMBER)
|
||||
login(member)
|
||||
val theme = testDataHelper.createTheme()
|
||||
val time = testDataHelper.createTime()
|
||||
|
||||
val waitingCreateRequest = WaitingCreateRequest(
|
||||
date = LocalDate.now().plusDays(1),
|
||||
themeId = theme.id!!,
|
||||
timeId = time.id!!
|
||||
)
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
@ -476,33 +465,30 @@ class ReservationControllerTest(
|
||||
post("/reservations/waiting")
|
||||
}.Then {
|
||||
statusCode(201)
|
||||
body("data.member.id", equalTo(member.id!!.toInt()))
|
||||
body("data.member.id", equalTo(member.id!!))
|
||||
body("data.status", equalTo(ReservationStatus.WAITING.name))
|
||||
}
|
||||
}
|
||||
|
||||
test("이미 예약된 시간, 테마로 대기 예약 요청 시 예외 응답") {
|
||||
val member = login(MemberFixture.create(role = Role.MEMBER))
|
||||
val reservationRequest = createRequest()
|
||||
val member = testDataHelper.createMember(role = Role.MEMBER)
|
||||
login(member)
|
||||
val theme = testDataHelper.createTheme()
|
||||
val time = testDataHelper.createTime()
|
||||
val date = LocalDate.now().plusDays(1)
|
||||
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val reservation = ReservationFixture.create(
|
||||
date = reservationRequest.date,
|
||||
theme = entityManager.find(ThemeEntity::class.java, reservationRequest.themeId),
|
||||
time = entityManager.find(TimeEntity::class.java, reservationRequest.timeId),
|
||||
member = member,
|
||||
status = ReservationStatus.WAITING
|
||||
)
|
||||
entityManager.persist(reservation)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
testDataHelper.createReservation(
|
||||
date = date,
|
||||
theme = theme,
|
||||
time = time,
|
||||
member = member,
|
||||
status = ReservationStatus.CONFIRMED
|
||||
)
|
||||
|
||||
// 이미 예약된 시간, 테마로 대기 예약 요청
|
||||
val waitingCreateRequest = WaitingCreateRequest(
|
||||
date = reservationRequest.date,
|
||||
themeId = reservationRequest.themeId,
|
||||
timeId = reservationRequest.timeId
|
||||
date = date,
|
||||
themeId = theme.id!!,
|
||||
timeId = time.id!!
|
||||
)
|
||||
val expectedError = ReservationErrorCode.ALREADY_RESERVE
|
||||
|
||||
@ -520,14 +506,10 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
context("DELETE /reservations/waiting/{id}") {
|
||||
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = createDummyReservations()
|
||||
}
|
||||
|
||||
test("대기 중인 예약을 취소한다.") {
|
||||
val member = login(MemberFixture.create(role = Role.MEMBER))
|
||||
val waiting: ReservationEntity = createSingleReservation(
|
||||
val member = testDataHelper.createMember(role = Role.MEMBER)
|
||||
login(member)
|
||||
val waiting = testDataHelper.createReservation(
|
||||
member = member,
|
||||
status = ReservationStatus.WAITING
|
||||
)
|
||||
@ -540,17 +522,16 @@ class ReservationControllerTest(
|
||||
statusCode(HttpStatus.NO_CONTENT.value())
|
||||
}
|
||||
|
||||
transactionTemplate.executeWithoutResult { _ ->
|
||||
entityManager.find(
|
||||
ReservationEntity::class.java,
|
||||
waiting.id
|
||||
) shouldBe null
|
||||
val deleted = transactionTemplate.execute {
|
||||
entityManager.find(ReservationEntity::class.java, waiting.id)
|
||||
}
|
||||
deleted shouldBe null
|
||||
}
|
||||
|
||||
test("이미 확정된 예약을 삭제하면 예외 응답") {
|
||||
val member = login(MemberFixture.create(role = Role.MEMBER))
|
||||
val reservation: ReservationEntity = createSingleReservation(
|
||||
val member = testDataHelper.createMember(role = Role.MEMBER)
|
||||
login(member)
|
||||
val reservation = testDataHelper.createReservation(
|
||||
member = member,
|
||||
status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
)
|
||||
@ -559,7 +540,7 @@ class ReservationControllerTest(
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
delete("/reservations/waiting/{id}", reservation.id)
|
||||
delete("/reservations/waiting/${reservation.id}")
|
||||
}.Then {
|
||||
statusCode(expectedError.httpStatus.value())
|
||||
body("code", equalTo(expectedError.errorCode))
|
||||
@ -569,7 +550,7 @@ class ReservationControllerTest(
|
||||
|
||||
context("POST /reservations/waiting/{id}/confirm") {
|
||||
test("관리자만 승인할 수 있다.") {
|
||||
login(MemberFixture.create(role = Role.MEMBER))
|
||||
login(testDataHelper.createMember(role = Role.MEMBER))
|
||||
val expectedError = AuthErrorCode.ACCESS_DENIED
|
||||
Given {
|
||||
port(port)
|
||||
@ -582,9 +563,8 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
test("대기 예약을 승인하면 결제 대기 상태로 변경") {
|
||||
val member = login(MemberFixture.create(role = Role.ADMIN))
|
||||
val reservation = createSingleReservation(
|
||||
member = member,
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val reservation = testDataHelper.createReservation(
|
||||
status = ReservationStatus.WAITING
|
||||
)
|
||||
|
||||
@ -596,39 +576,28 @@ class ReservationControllerTest(
|
||||
statusCode(200)
|
||||
}
|
||||
|
||||
transactionTemplate.executeWithoutResult { _ ->
|
||||
entityManager.find(
|
||||
ReservationEntity::class.java,
|
||||
reservation.id
|
||||
)?.also {
|
||||
it.reservationStatus shouldBe ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
} ?: throw AssertionError("Reservation not found")
|
||||
val updatedReservation = transactionTemplate.execute {
|
||||
entityManager.find(ReservationEntity::class.java, reservation.id)
|
||||
}
|
||||
updatedReservation?.status shouldBe ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
}
|
||||
|
||||
test("다른 확정된 예약을 승인하면 예외 응답") {
|
||||
val admin = login(MemberFixture.create(role = Role.ADMIN))
|
||||
val alreadyReserved = createSingleReservation(
|
||||
val admin = testDataHelper.createMember(role = Role.ADMIN)
|
||||
login(admin)
|
||||
val alreadyReserved = testDataHelper.createReservation(
|
||||
member = admin,
|
||||
status = ReservationStatus.CONFIRMED
|
||||
)
|
||||
|
||||
val member = MemberFixture.create(account = "account", role = Role.MEMBER).also { it ->
|
||||
transactionTemplate.executeWithoutResult { _ ->
|
||||
entityManager.persist(it)
|
||||
}
|
||||
}
|
||||
val waiting = ReservationFixture.create(
|
||||
val member = testDataHelper.createMember(role = Role.MEMBER)
|
||||
val waiting = testDataHelper.createReservation(
|
||||
date = alreadyReserved.date,
|
||||
time = alreadyReserved.time,
|
||||
theme = alreadyReserved.theme,
|
||||
member = member,
|
||||
status = ReservationStatus.WAITING
|
||||
).also {
|
||||
transactionTemplate.executeWithoutResult { _ ->
|
||||
entityManager.persist(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val expectedError = ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS
|
||||
Given {
|
||||
@ -636,7 +605,6 @@ class ReservationControllerTest(
|
||||
}.When {
|
||||
post("/reservations/waiting/${waiting.id!!}/confirm")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(expectedError.httpStatus.value())
|
||||
body("code", equalTo(expectedError.errorCode))
|
||||
}
|
||||
@ -645,7 +613,7 @@ class ReservationControllerTest(
|
||||
|
||||
context("POST /reservations/waiting/{id}/reject") {
|
||||
test("관리자만 거절할 수 있다.") {
|
||||
login(MemberFixture.create(role = Role.MEMBER))
|
||||
login(testDataHelper.createMember(role = Role.MEMBER))
|
||||
val expectedError = AuthErrorCode.ACCESS_DENIED
|
||||
|
||||
Given {
|
||||
@ -659,9 +627,8 @@ class ReservationControllerTest(
|
||||
}
|
||||
|
||||
test("거절된 예약은 삭제된다.") {
|
||||
val member = login(MemberFixture.create(role = Role.ADMIN))
|
||||
val reservation = createSingleReservation(
|
||||
member = member,
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val reservation = testDataHelper.createReservation(
|
||||
status = ReservationStatus.WAITING
|
||||
)
|
||||
|
||||
@ -673,125 +640,91 @@ class ReservationControllerTest(
|
||||
statusCode(204)
|
||||
}
|
||||
|
||||
transactionTemplate.executeWithoutResult { _ ->
|
||||
entityManager.find(
|
||||
ReservationEntity::class.java,
|
||||
reservation.id
|
||||
) shouldBe null
|
||||
val rejected = transactionTemplate.execute {
|
||||
entityManager.find(ReservationEntity::class.java, reservation.id)
|
||||
}
|
||||
rejected shouldBe null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createSingleReservation(
|
||||
date: LocalDate = LocalDate.now().plusDays(1),
|
||||
time: LocalTime = LocalTime.now(),
|
||||
themeName: String = "Default Theme",
|
||||
member: MemberEntity = MemberFixture.create(role = Role.MEMBER),
|
||||
status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
): ReservationEntity {
|
||||
return ReservationFixture.create(
|
||||
date = date,
|
||||
theme = ThemeFixture.create(name = themeName),
|
||||
time = TimeFixture.create(startAt = time),
|
||||
member = member,
|
||||
status = status
|
||||
).also { it ->
|
||||
transactionTemplate.execute { _ ->
|
||||
if (member.id == null) {
|
||||
entityManager.persist(member)
|
||||
}
|
||||
entityManager.persist(it.time)
|
||||
entityManager.persist(it.theme)
|
||||
entityManager.persist(it)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createDummyReservations(): MutableMap<MemberEntity, MutableList<ReservationEntity>> {
|
||||
val reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>> = mutableMapOf()
|
||||
val members: List<MemberEntity> = listOf(
|
||||
MemberFixture.create(role = Role.MEMBER),
|
||||
MemberFixture.create(role = Role.MEMBER)
|
||||
)
|
||||
|
||||
transactionTemplate.executeWithoutResult {
|
||||
members.forEach { member ->
|
||||
entityManager.persist(member)
|
||||
}
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
transactionTemplate.executeWithoutResult {
|
||||
repeat(10) { index ->
|
||||
val theme = ThemeFixture.create(name = "theme$index")
|
||||
val time = TimeFixture.create(startAt = LocalTime.now().plusMinutes(index.toLong()))
|
||||
entityManager.persist(theme)
|
||||
entityManager.persist(time)
|
||||
|
||||
val reservation = ReservationFixture.create(
|
||||
date = LocalDate.now().plusDays(index.toLong()),
|
||||
theme = theme,
|
||||
time = time,
|
||||
member = members[index % members.size],
|
||||
status = ReservationStatus.CONFIRMED
|
||||
)
|
||||
entityManager.persist(reservation)
|
||||
reservations.getOrPut(reservation.member) { mutableListOf() }.add(reservation)
|
||||
}
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
return reservations
|
||||
}
|
||||
|
||||
fun createRequest(
|
||||
theme: ThemeEntity = ThemeFixture.create(),
|
||||
time: TimeEntity = TimeFixture.create(),
|
||||
): ReservationCreateWithPaymentRequest {
|
||||
lateinit var reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest
|
||||
|
||||
transactionTemplate.executeWithoutResult {
|
||||
entityManager.persist(theme)
|
||||
entityManager.persist(time)
|
||||
|
||||
reservationCreateWithPaymentRequest = ReservationFixture.createRequest(
|
||||
themeId = theme.id!!,
|
||||
timeId = time.id!!,
|
||||
)
|
||||
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
return reservationCreateWithPaymentRequest
|
||||
}
|
||||
|
||||
fun login(member: MemberEntity): MemberEntity {
|
||||
if (member.id == null) {
|
||||
transactionTemplate.executeWithoutResult {
|
||||
entityManager.persist(member)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
}
|
||||
|
||||
every {
|
||||
jwtHandler.getMemberIdFromToken(any())
|
||||
} returns member.id!!
|
||||
|
||||
every {
|
||||
memberService.findById(member.id!!)
|
||||
} returns member
|
||||
|
||||
every {
|
||||
memberIdResolver.resolveArgument(any(), any(), any(), any())
|
||||
} returns member.id!!
|
||||
|
||||
return member
|
||||
}
|
||||
}
|
||||
|
||||
class TestDataHelper(
|
||||
private val entityManager: EntityManager,
|
||||
private val transactionTemplate: TransactionTemplate,
|
||||
) {
|
||||
private var memberSequence = 0L
|
||||
private var themeSequence = 0L
|
||||
private var timeSequence = 0L
|
||||
|
||||
fun createMember(
|
||||
role: Role = Role.MEMBER,
|
||||
account: String = "member${++memberSequence}@test.com",
|
||||
): MemberEntity {
|
||||
val member = MemberFixture.create(role = role, account = account)
|
||||
return persist(member)
|
||||
}
|
||||
|
||||
fun createTheme(name: String = "theme-${++themeSequence}"): ThemeEntity {
|
||||
val theme = ThemeFixture.create(name = name)
|
||||
return persist(theme)
|
||||
}
|
||||
|
||||
fun createTime(startAt: LocalTime = LocalTime.of(10, 0).plusMinutes(++timeSequence * 10)): TimeEntity {
|
||||
val time = TimeFixture.create(startAt = startAt)
|
||||
return persist(time)
|
||||
}
|
||||
|
||||
fun createReservation(
|
||||
date: LocalDate = LocalDate.now().plusDays(1),
|
||||
theme: ThemeEntity = createTheme(),
|
||||
time: TimeEntity = createTime(),
|
||||
member: MemberEntity = createMember(),
|
||||
status: ReservationStatus = ReservationStatus.CONFIRMED,
|
||||
): ReservationEntity {
|
||||
val reservation = ReservationFixture.create(
|
||||
date = date,
|
||||
theme = theme,
|
||||
time = time,
|
||||
member = member,
|
||||
status = status
|
||||
)
|
||||
return persist(reservation)
|
||||
}
|
||||
|
||||
fun createPayment(reservation: ReservationEntity): PaymentEntity {
|
||||
val payment = PaymentFixture.create(reservation = reservation)
|
||||
return persist(payment)
|
||||
}
|
||||
|
||||
fun createReservationRequest(
|
||||
theme: ThemeEntity = createTheme(),
|
||||
time: TimeEntity = createTime(),
|
||||
): ReservationCreateWithPaymentRequest {
|
||||
return ReservationFixture.createRequest(
|
||||
themeId = theme.id!!,
|
||||
timeId = time.id!!,
|
||||
)
|
||||
}
|
||||
|
||||
fun createDummyReservations(
|
||||
memberCount: Int = 2,
|
||||
reservationCount: Int = 10,
|
||||
): Map<MemberEntity, List<ReservationEntity>> {
|
||||
val members = (1..memberCount).map { createMember(role = Role.MEMBER) }
|
||||
val reservations = (1..reservationCount).map { index ->
|
||||
createReservation(
|
||||
member = members[index % memberCount],
|
||||
status = ReservationStatus.CONFIRMED
|
||||
)
|
||||
}
|
||||
return reservations.groupBy { it.member }
|
||||
}
|
||||
|
||||
private fun <T> persist(entity: T): T {
|
||||
transactionTemplate.executeWithoutResult {
|
||||
entityManager.persist(entity)
|
||||
}
|
||||
return entity
|
||||
}
|
||||
}
|
||||
@ -13,12 +13,13 @@ import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||
import roomescape.theme.web.ThemeCreateRequest
|
||||
import roomescape.theme.web.ThemeRetrieveResponse
|
||||
import roomescape.util.TsidFactory
|
||||
import roomescape.util.ThemeFixture
|
||||
|
||||
class ThemeServiceTest : FunSpec({
|
||||
|
||||
val themeRepository: ThemeRepository = mockk()
|
||||
val themeService = ThemeService(themeRepository)
|
||||
val themeService = ThemeService(TsidFactory, themeRepository)
|
||||
|
||||
context("findThemeById") {
|
||||
val themeId = 1L
|
||||
|
||||
@ -8,7 +8,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import roomescape.theme.util.TestThemeCreateUtil
|
||||
import java.time.LocalDate
|
||||
|
||||
@DataJpaTest
|
||||
@DataJpaTest(showSql = false)
|
||||
class ThemeRepositoryTest(
|
||||
val themeRepository: ThemeRepository,
|
||||
val entityManager: EntityManager
|
||||
|
||||
@ -10,6 +10,7 @@ import io.mockk.just
|
||||
import io.mockk.runs
|
||||
import org.hamcrest.Matchers.equalTo
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import roomescape.auth.exception.AuthErrorCode
|
||||
|
||||
@ -14,6 +14,7 @@ import roomescape.time.exception.TimeErrorCode
|
||||
import roomescape.time.exception.TimeException
|
||||
import roomescape.time.infrastructure.persistence.TimeRepository
|
||||
import roomescape.time.web.TimeCreateRequest
|
||||
import roomescape.util.TsidFactory
|
||||
import roomescape.util.TimeFixture
|
||||
import java.time.LocalTime
|
||||
|
||||
@ -22,6 +23,7 @@ class TimeServiceTest : FunSpec({
|
||||
val reservationRepository: ReservationRepository = mockk()
|
||||
|
||||
val timeService = TimeService(
|
||||
tsidFactory = TsidFactory,
|
||||
timeRepository = timeRepository,
|
||||
reservationRepository = reservationRepository
|
||||
)
|
||||
|
||||
@ -7,7 +7,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import roomescape.util.TimeFixture
|
||||
import java.time.LocalTime
|
||||
|
||||
@DataJpaTest
|
||||
@DataJpaTest(showSql = false)
|
||||
class TimeRepositoryTest(
|
||||
val entityManager: EntityManager,
|
||||
val timeRepository: TimeRepository,
|
||||
|
||||
@ -9,6 +9,7 @@ import io.mockk.every
|
||||
import org.hamcrest.Matchers.equalTo
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
@ -27,7 +28,6 @@ import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
|
||||
@WebMvcTest(TimeController::class)
|
||||
@Import(JacksonConfig::class)
|
||||
class TimeControllerTest(
|
||||
val mockMvc: MockMvc,
|
||||
) : RoomescapeApiTest() {
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package roomescape.util
|
||||
|
||||
import com.github.f4b6a3.tsid.TsidFactory
|
||||
import roomescape.auth.infrastructure.jwt.JwtHandler
|
||||
import roomescape.auth.web.LoginRequest
|
||||
import roomescape.common.config.next
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.member.infrastructure.persistence.Role
|
||||
import roomescape.payment.infrastructure.client.PaymentApproveRequest
|
||||
@ -20,11 +22,14 @@ import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
|
||||
val TsidFactory: TsidFactory = TsidFactory(0)
|
||||
|
||||
object MemberFixture {
|
||||
const val NOT_LOGGED_IN_USERID: Long = 0
|
||||
|
||||
fun create(
|
||||
id: Long? = null,
|
||||
id: Long? = TsidFactory.next(),
|
||||
name: String = "sangdol",
|
||||
account: String = "default",
|
||||
password: String = "password",
|
||||
@ -56,14 +61,14 @@ object MemberFixture {
|
||||
|
||||
object TimeFixture {
|
||||
fun create(
|
||||
id: Long? = null,
|
||||
id: Long? = TsidFactory.next(),
|
||||
startAt: LocalTime = LocalTime.now().plusHours(1),
|
||||
): TimeEntity = TimeEntity(id, startAt)
|
||||
}
|
||||
|
||||
object ThemeFixture {
|
||||
fun create(
|
||||
id: Long? = null,
|
||||
id: Long? = TsidFactory.next(),
|
||||
name: String = "Default Theme",
|
||||
description: String = "Default Description",
|
||||
thumbnail: String = "https://example.com/default-thumbnail.jpg"
|
||||
@ -72,7 +77,7 @@ object ThemeFixture {
|
||||
|
||||
object ReservationFixture {
|
||||
fun create(
|
||||
id: Long? = null,
|
||||
id: Long? = TsidFactory.next(),
|
||||
date: LocalDate = LocalDate.now().plusWeeks(1),
|
||||
theme: ThemeEntity = ThemeFixture.create(),
|
||||
time: TimeEntity = TimeFixture.create(),
|
||||
@ -125,14 +130,14 @@ object PaymentFixture {
|
||||
const val AMOUNT: Long = 10000L
|
||||
|
||||
fun create(
|
||||
id: Long? = null,
|
||||
id: Long? = TsidFactory.next(),
|
||||
orderId: String = ORDER_ID,
|
||||
paymentKey: String = PAYMENT_KEY,
|
||||
totalAmount: Long = AMOUNT,
|
||||
reservation: ReservationEntity = ReservationFixture.create(id = 1L),
|
||||
approvedAt: OffsetDateTime = OffsetDateTime.now()
|
||||
): PaymentEntity = PaymentEntity(
|
||||
id = id,
|
||||
_id = id,
|
||||
orderId = orderId,
|
||||
paymentKey = paymentKey,
|
||||
totalAmount = totalAmount,
|
||||
@ -141,14 +146,14 @@ object PaymentFixture {
|
||||
)
|
||||
|
||||
fun createCanceled(
|
||||
id: Long? = null,
|
||||
id: Long? = TsidFactory.next(),
|
||||
paymentKey: String = PAYMENT_KEY,
|
||||
cancelReason: String = "Test Cancel",
|
||||
cancelAmount: Long = AMOUNT,
|
||||
approvedAt: OffsetDateTime = OffsetDateTime.now(),
|
||||
canceledAt: OffsetDateTime = approvedAt.plusHours(1)
|
||||
): CanceledPaymentEntity = CanceledPaymentEntity(
|
||||
id = id,
|
||||
_id = id,
|
||||
paymentKey = paymentKey,
|
||||
cancelReason = cancelReason,
|
||||
cancelAmount = cancelAmount,
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
package roomescape.util
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.github.f4b6a3.tsid.TsidFactory
|
||||
import com.ninjasquad.springmockk.MockkBean
|
||||
import com.ninjasquad.springmockk.SpykBean
|
||||
import io.kotest.core.spec.style.BehaviorSpec
|
||||
import io.mockk.every
|
||||
import org.springframework.boot.test.context.TestConfiguration
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.context.annotation.Primary
|
||||
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.MediaType
|
||||
@ -21,6 +27,8 @@ import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||
import roomescape.util.MemberFixture.NOT_LOGGED_IN_USERID
|
||||
|
||||
@Import(TestConfig::class, JacksonConfig::class)
|
||||
@MockkBean(JpaMetamodelMappingContext::class)
|
||||
abstract class RoomescapeApiTest : BehaviorSpec() {
|
||||
|
||||
@SpykBean
|
||||
@ -128,3 +136,10 @@ abstract class RoomescapeApiTest : BehaviorSpec() {
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
class TestConfig {
|
||||
@Bean
|
||||
@Primary
|
||||
fun tsidFactory(): TsidFactory = TsidFactory
|
||||
}
|
||||
5
src/test/resources/application-test.yaml
Normal file
5
src/test/resources/application-test.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
org.springframework.orm.jpa: INFO
|
||||
org.springframework.transaction: DEBUG
|
||||
@ -1,29 +0,0 @@
|
||||
spring:
|
||||
jpa:
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
ddl-auto: create-drop
|
||||
defer-datasource-initialization: true
|
||||
sql:
|
||||
init:
|
||||
data-locations:
|
||||
|
||||
security:
|
||||
jwt:
|
||||
token:
|
||||
secret-key: daijawligagaf@LIJ$@U)9nagnalkkgalijaddljfi
|
||||
ttl-seconds: 1800000
|
||||
|
||||
payment:
|
||||
api-base-url: https://api.tosspayments.com
|
||||
confirm-secret-key: test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6
|
||||
read-timeout: 3
|
||||
connect-timeout: 30
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
org.springframework.orm.jpa: INFO
|
||||
org.springframework.transaction: DEBUG
|
||||
Loading…
x
Reference in New Issue
Block a user