generated from pricelees/issue-pr-template
[#58] K6 성능 테스트 도입 #59
12
frontend/src/api/order/orderAPI.ts
Normal file
12
frontend/src/api/order/orderAPI.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import apiClient from "@_api/apiClient";
|
||||||
|
import type { PaymentConfirmRequest } from "@_api/payment/PaymentTypes";
|
||||||
|
|
||||||
|
export const confirm = async (
|
||||||
|
reservationId: string,
|
||||||
|
data: PaymentConfirmRequest,
|
||||||
|
): Promise<void> => {
|
||||||
|
return await apiClient.post<void>(
|
||||||
|
`/orders/${reservationId}/confirm`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
};
|
||||||
5
frontend/src/api/order/orderTypes.ts
Normal file
5
frontend/src/api/order/orderTypes.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface OrderErrorResponse {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
trial: number;
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { confirm } from '@_api/order/orderAPI';
|
import { confirm } from '@_api/order/orderAPI';
|
||||||
import type { BookingErrorResponse } from '@_api/order/orderTypes';
|
import type { OrderErrorResponse } from '@_api/order/orderTypes';
|
||||||
import { type PaymentConfirmRequest } from '@_api/payment/PaymentTypes';
|
import { type PaymentConfirmRequest } from '@_api/payment/PaymentTypes';
|
||||||
import { confirmReservation } from '@_api/reservation/reservationAPI';
|
import { confirmReservation } from '@_api/reservation/reservationAPI';
|
||||||
import '@_css/reservation-v2-1.css';
|
import '@_css/reservation-v2-1.css';
|
||||||
@ -83,7 +83,7 @@ const ReservationStep2Page: React.FC = () => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
const error = err as AxiosError<BookingErrorResponse>;
|
const error = err as AxiosError<OrderErrorResponse>;
|
||||||
const errorCode = error.response?.data?.code;
|
const errorCode = error.response?.data?.code;
|
||||||
const errorMessage = error.response?.data?.message;
|
const errorMessage = error.response?.data?.message;
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.sangdol.roomescape.auth.web.support.resolver
|
package com.sangdol.roomescape.auth.web.support.resolver
|
||||||
|
|
||||||
|
import com.sangdol.common.utils.MdcPrincipalIdUtil
|
||||||
import com.sangdol.roomescape.auth.exception.AuthErrorCode
|
import com.sangdol.roomescape.auth.exception.AuthErrorCode
|
||||||
import com.sangdol.roomescape.auth.exception.AuthException
|
import com.sangdol.roomescape.auth.exception.AuthException
|
||||||
import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils
|
import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils
|
||||||
@ -38,7 +39,9 @@ class UserContextResolver(
|
|||||||
val token: String? = request.accessToken()
|
val token: String? = request.accessToken()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val id: Long = jwtUtils.extractSubject(token).toLong()
|
val id: Long = jwtUtils.extractSubject(token).also {
|
||||||
|
MdcPrincipalIdUtil.set(it)
|
||||||
|
}.toLong()
|
||||||
|
|
||||||
return userService.findContextById(id)
|
return userService.findContextById(id)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import org.springframework.stereotype.Component
|
|||||||
private val log: KLogger = KotlinLogging.logger {}
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Profile("local")
|
@Profile("!deploy & local")
|
||||||
class LocalDatabaseCleaner(
|
class LocalDatabaseCleaner(
|
||||||
private val jdbcTemplate: JdbcTemplate
|
private val jdbcTemplate: JdbcTemplate
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.sangdol.roomescape.schedule.infrastructure.persistence
|
package com.sangdol.roomescape.schedule.infrastructure.persistence
|
||||||
|
|
||||||
import com.sangdol.roomescape.schedule.business.domain.ScheduleOverview
|
import com.sangdol.roomescape.schedule.business.domain.ScheduleOverview
|
||||||
|
import com.sangdol.roomescape.test.ScheduleWithThemeId
|
||||||
import jakarta.persistence.LockModeType
|
import jakarta.persistence.LockModeType
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
import org.springframework.data.jpa.repository.Lock
|
import org.springframework.data.jpa.repository.Lock
|
||||||
@ -133,14 +134,14 @@ interface ScheduleRepository : JpaRepository<ScheduleEntity, Long> {
|
|||||||
s.id
|
s.id
|
||||||
FROM
|
FROM
|
||||||
schedule s
|
schedule s
|
||||||
|
LEFT JOIN
|
||||||
|
reservation r
|
||||||
|
ON
|
||||||
|
r.schedule_id = s.id AND r.status IN ('PENDING', 'PAYMENT_IN_PROGRESS')
|
||||||
WHERE
|
WHERE
|
||||||
s.status = 'HOLD'
|
s.status = 'HOLD'
|
||||||
AND s.hold_expired_at <= :now
|
AND s.hold_expired_at <= :now
|
||||||
AND NOT EXISTS (
|
AND r.id IS NULL
|
||||||
SELECT 1
|
|
||||||
FROM reservation r
|
|
||||||
WHERE r.schedule_id = s.id AND (r.status = 'PENDING' OR r.status = 'PAYMENT_IN_PROGRESS')
|
|
||||||
)
|
|
||||||
FOR UPDATE SKIP LOCKED
|
FOR UPDATE SKIP LOCKED
|
||||||
""", nativeQuery = true
|
""", nativeQuery = true
|
||||||
)
|
)
|
||||||
@ -159,4 +160,18 @@ interface ScheduleRepository : JpaRepository<ScheduleEntity, Long> {
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun releaseHeldSchedules(@Param("scheduleIds") scheduleIds: List<Long>): Int
|
fun releaseHeldSchedules(@Param("scheduleIds") scheduleIds: List<Long>): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* for test
|
||||||
|
*/
|
||||||
|
@Query("""
|
||||||
|
SELECT
|
||||||
|
s.id, s.theme_id
|
||||||
|
FROM
|
||||||
|
schedule s
|
||||||
|
WHERE
|
||||||
|
s.status = 'AVAILABLE'
|
||||||
|
AND s.date > CURRENT_DATE
|
||||||
|
""", nativeQuery = true)
|
||||||
|
fun findAllAvailableSchedules(): List<ScheduleWithThemeId>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,45 @@
|
|||||||
|
package com.sangdol.roomescape.test
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/tests")
|
||||||
|
class TestSetupController(
|
||||||
|
private val testSetupService: TestSetupService
|
||||||
|
) {
|
||||||
|
|
||||||
|
@GetMapping("/themes")
|
||||||
|
fun findAllThemeWithTimes(): ThemeWithTimesList {
|
||||||
|
return testSetupService.findAllThemeWithTimes()
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/max-iterations")
|
||||||
|
fun maxIterations(): MaxIterations {
|
||||||
|
return testSetupService.calculateMaxIterations()
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/admin/accounts")
|
||||||
|
fun findAllStoreAdminAccounts(): AccountList {
|
||||||
|
return testSetupService.findAllStoreAdminAccounts()
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/schedules/available")
|
||||||
|
fun findAllAvailableSchedules(): ScheduleWithThemeIdList {
|
||||||
|
return testSetupService.findAllAvailableSchedule()
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/users")
|
||||||
|
fun findAllUsers(
|
||||||
|
@RequestParam("count") count: Long
|
||||||
|
): AccountList {
|
||||||
|
return testSetupService.findAllUserAccounts(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/stores")
|
||||||
|
fun findAllStoreIds(): StoreIdList {
|
||||||
|
return testSetupService.findAllStores()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package com.sangdol.roomescape.test
|
||||||
|
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
data class ThemeWithTimesList(
|
||||||
|
val results: List<ThemeWithTimes>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ThemeWithTimes(
|
||||||
|
val id: Long,
|
||||||
|
val times: List<ScheduleTime>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ScheduleTime(
|
||||||
|
val startFrom: LocalTime,
|
||||||
|
val endAt: LocalTime
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AccountList(
|
||||||
|
val results: List<Account>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Account(
|
||||||
|
val account: String,
|
||||||
|
val password: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ScheduleWithThemeIdList(
|
||||||
|
val results: List<ScheduleWithThemeId>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ScheduleWithThemeId(
|
||||||
|
val scheduleId: Long,
|
||||||
|
val themeId: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MaxIterations(
|
||||||
|
val count: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StoreIdList(
|
||||||
|
val results: List<StoreId>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StoreId(
|
||||||
|
val storeId: Long
|
||||||
|
)
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
package com.sangdol.roomescape.test
|
||||||
|
|
||||||
|
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel
|
||||||
|
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminRepository
|
||||||
|
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType
|
||||||
|
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository
|
||||||
|
import com.sangdol.roomescape.store.infrastructure.persistence.StoreRepository
|
||||||
|
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||||
|
import com.sangdol.roomescape.user.infrastructure.persistence.UserRepository
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class TestSetupService(
|
||||||
|
private val themeRepository: ThemeRepository,
|
||||||
|
private val storeRepository: StoreRepository,
|
||||||
|
private val adminRepository: AdminRepository,
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
private val scheduleRepository: ScheduleRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun findAllThemeWithTimes(): ThemeWithTimesList {
|
||||||
|
val storeOpenTime = LocalTime.of(10, 0)
|
||||||
|
val storeCloseTime = LocalTime.of(22, 0)
|
||||||
|
val timeGapMinutes = 10L
|
||||||
|
|
||||||
|
return ThemeWithTimesList(themeRepository.findAll()
|
||||||
|
.filter { it.isActive }
|
||||||
|
.map { theme ->
|
||||||
|
val times: MutableList<ScheduleTime> = mutableListOf()
|
||||||
|
var startTime: LocalTime = storeOpenTime
|
||||||
|
|
||||||
|
while (startTime.isBefore(storeCloseTime)) {
|
||||||
|
val themeAvailableMinute = theme.availableMinutes.toLong()
|
||||||
|
val endTime: LocalTime = startTime.plusMinutes(themeAvailableMinute)
|
||||||
|
|
||||||
|
if (endTime.isAfter(storeCloseTime)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
times.add(ScheduleTime(startTime, endTime))
|
||||||
|
startTime = endTime.plusMinutes(timeGapMinutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeWithTimes(theme.id, times)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun calculateMaxIterations(): MaxIterations {
|
||||||
|
val max = findAllThemeWithTimes().results.sumOf { it.times.size }
|
||||||
|
val stores = storeRepository.findAll().size
|
||||||
|
val days = 6
|
||||||
|
|
||||||
|
return MaxIterations((max * stores * days).toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun findAllStoreAdminAccounts(): AccountList {
|
||||||
|
return AccountList(adminRepository.findAll()
|
||||||
|
.filter { it.permissionLevel == AdminPermissionLevel.FULL_ACCESS }
|
||||||
|
.filter { it.type == AdminType.STORE }
|
||||||
|
.map { Account(it.account, it.password) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun findAllUserAccounts(count: Long): AccountList {
|
||||||
|
return AccountList(userRepository.findUsersByCount(count)
|
||||||
|
.map { Account(it.email, it.password) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun findAllAvailableSchedule(): ScheduleWithThemeIdList {
|
||||||
|
return ScheduleWithThemeIdList(scheduleRepository.findAllAvailableSchedules())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun findAllStores(): StoreIdList {
|
||||||
|
return StoreIdList(storeRepository.findAll().map {
|
||||||
|
StoreId(it.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,21 @@
|
|||||||
package com.sangdol.roomescape.user.infrastructure.persistence
|
package com.sangdol.roomescape.user.infrastructure.persistence
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.data.jpa.repository.Query
|
||||||
|
|
||||||
interface UserRepository : JpaRepository<UserEntity, Long> {
|
interface UserRepository : JpaRepository<UserEntity, Long> {
|
||||||
|
|
||||||
fun existsByEmail(email: String): Boolean
|
fun existsByEmail(email: String): Boolean
|
||||||
fun existsByPhone(phone: String): Boolean
|
fun existsByPhone(phone: String): Boolean
|
||||||
fun findByEmail(email: String): UserEntity?
|
fun findByEmail(email: String): UserEntity?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* for test
|
||||||
|
*/
|
||||||
|
@Query("""
|
||||||
|
SELECT * FROM users u LIMIT :count
|
||||||
|
""", nativeQuery = true)
|
||||||
|
fun findUsersByCount(count: Long): List<UserEntity>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserStatusHistoryRepository : JpaRepository<UserStatusHistoryEntity, Long>
|
interface UserStatusHistoryRepository : JpaRepository<UserStatusHistoryEntity, Long>
|
||||||
|
|||||||
@ -1,236 +0,0 @@
|
|||||||
create table if not exists region (
|
|
||||||
code varchar(10) primary key,
|
|
||||||
sido_code varchar(2) not null,
|
|
||||||
sigungu_code varchar(3) not null,
|
|
||||||
sido_name varchar(20) not null,
|
|
||||||
sigungu_name varchar(20) not null,
|
|
||||||
|
|
||||||
constraint uk_region__sido_sigungu_code unique (sido_code, sigungu_code)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists store(
|
|
||||||
id bigint primary key,
|
|
||||||
name varchar(20) not null,
|
|
||||||
address varchar(100) not null,
|
|
||||||
contact varchar(50) not null,
|
|
||||||
business_reg_num varchar(12) not null,
|
|
||||||
region_code varchar(10) not null,
|
|
||||||
status varchar(20) not null,
|
|
||||||
|
|
||||||
created_at timestamp not null,
|
|
||||||
created_by bigint not null,
|
|
||||||
updated_at timestamp not null,
|
|
||||||
updated_by bigint not null,
|
|
||||||
|
|
||||||
constraint uk_store__name unique (name),
|
|
||||||
constraint uk_store__contact unique (contact),
|
|
||||||
constraint uk_store__address unique (address),
|
|
||||||
constraint uk_store__business_reg_num unique (business_reg_num),
|
|
||||||
constraint fk_store__region_code foreign key (region_code) references region (code)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists users(
|
|
||||||
id bigint primary key,
|
|
||||||
name varchar(50) not null,
|
|
||||||
email varchar(255) not null,
|
|
||||||
password varchar(255) not null,
|
|
||||||
phone varchar(20) not null,
|
|
||||||
region_code varchar(10) null,
|
|
||||||
status varchar(20) not null,
|
|
||||||
created_at datetime(6) not null,
|
|
||||||
created_by bigint not null,
|
|
||||||
updated_at datetime(6) not null,
|
|
||||||
updated_by bigint not null,
|
|
||||||
|
|
||||||
constraint uk__users_email unique (email),
|
|
||||||
constraint uk__users_phone unique (phone),
|
|
||||||
constraint fk__users_region_code foreign key (region_code) references region (code)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists user_status_history(
|
|
||||||
id bigint primary key,
|
|
||||||
user_id bigint not null,
|
|
||||||
status varchar(20) not null,
|
|
||||||
reason varchar(255) not null,
|
|
||||||
created_at datetime(6) not null,
|
|
||||||
created_by bigint not null,
|
|
||||||
updated_at datetime(6) not null,
|
|
||||||
updated_by bigint not null,
|
|
||||||
|
|
||||||
constraint fk__user_status_history_user_id foreign key (user_id) references users (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists admin(
|
|
||||||
id bigint primary key,
|
|
||||||
account varchar(20) not null,
|
|
||||||
password varchar(255) not null,
|
|
||||||
name varchar(20) not null,
|
|
||||||
phone varchar(20) not null,
|
|
||||||
type varchar(20) not null,
|
|
||||||
store_id bigint,
|
|
||||||
permission_level varchar(20) not null,
|
|
||||||
created_at timestamp not null,
|
|
||||||
created_by bigint not null,
|
|
||||||
updated_at timestamp not null,
|
|
||||||
updated_by bigint not null,
|
|
||||||
|
|
||||||
constraint uk_admin__account unique (account),
|
|
||||||
constraint uk_admin__phone unique (phone),
|
|
||||||
constraint chk_admin__type check (type in ('HQ', 'STORE')),
|
|
||||||
constraint chk_admin__store_id check (
|
|
||||||
(type = 'HQ' AND store_id IS NULL) OR
|
|
||||||
(type = 'STORE' AND store_id IS NOT NULL)
|
|
||||||
),
|
|
||||||
constraint fk_admin__store_id foreign key (store_id) references store (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists login_history(
|
|
||||||
id bigint primary key,
|
|
||||||
principal_id bigint not null,
|
|
||||||
principal_type varchar(20) not null,
|
|
||||||
success boolean not null,
|
|
||||||
ip_address varchar(45) not null,
|
|
||||||
user_agent varchar(255) not null,
|
|
||||||
created_at timestamp not null
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists theme (
|
|
||||||
id bigint primary key ,
|
|
||||||
name varchar(30) not null,
|
|
||||||
difficulty varchar(20) not null,
|
|
||||||
description varchar(255) not null,
|
|
||||||
thumbnail_url varchar(255) not null,
|
|
||||||
price int not null,
|
|
||||||
min_participants smallint not null,
|
|
||||||
max_participants smallint not null,
|
|
||||||
available_minutes smallint not null,
|
|
||||||
expected_minutes_from smallint not null,
|
|
||||||
expected_minutes_to smallint not null,
|
|
||||||
is_active boolean not null,
|
|
||||||
created_at timestamp not null,
|
|
||||||
created_by bigint not null,
|
|
||||||
updated_at timestamp not null,
|
|
||||||
updated_by bigint not null
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists schedule (
|
|
||||||
id bigint primary key,
|
|
||||||
date date not null,
|
|
||||||
time time not null,
|
|
||||||
store_id bigint not null,
|
|
||||||
theme_id bigint not null,
|
|
||||||
status varchar(30) not null,
|
|
||||||
created_at timestamp not null,
|
|
||||||
created_by bigint not null,
|
|
||||||
updated_at timestamp not null,
|
|
||||||
updated_by bigint not null,
|
|
||||||
hold_expired_at timestamp null,
|
|
||||||
|
|
||||||
constraint uk_schedule__store_id_date_time_theme_id unique (store_id, date, time, theme_id),
|
|
||||||
constraint fk_schedule__store_id foreign key (store_id) references store (id),
|
|
||||||
constraint fk_schedule__theme_id foreign key (theme_id) references theme (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists reservation (
|
|
||||||
id bigint primary key,
|
|
||||||
user_id bigint not null,
|
|
||||||
schedule_id bigint not null,
|
|
||||||
reserver_name varchar(30) not null,
|
|
||||||
reserver_contact varchar(30) not null,
|
|
||||||
participant_count smallint not null,
|
|
||||||
requirement varchar(255) not null,
|
|
||||||
status varchar(30) not null,
|
|
||||||
created_at timestamp not null,
|
|
||||||
created_by bigint not null,
|
|
||||||
updated_at timestamp not null,
|
|
||||||
updated_by bigint not null,
|
|
||||||
|
|
||||||
constraint fk_reservation__user_id foreign key (user_id) references users (id),
|
|
||||||
constraint fk_reservation__schedule_id foreign key (schedule_id) references schedule (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists canceled_reservation (
|
|
||||||
id bigint primary key,
|
|
||||||
reservation_id bigint not null,
|
|
||||||
canceled_by bigint not null,
|
|
||||||
cancel_reason varchar(50) not null,
|
|
||||||
canceled_at timestamp not null,
|
|
||||||
status varchar(30) not null,
|
|
||||||
|
|
||||||
constraint uk_canceled_reservations__reservation_id unique (reservation_id),
|
|
||||||
constraint fk_canceled_reservations__reservation_id foreign key (reservation_id) references reservation (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists payment (
|
|
||||||
id bigint primary key,
|
|
||||||
reservation_id bigint not null,
|
|
||||||
type varchar(20) not null,
|
|
||||||
method varchar(30) not null,
|
|
||||||
payment_key varchar(255) not null unique,
|
|
||||||
order_id varchar(255) not null unique,
|
|
||||||
total_amount integer not null,
|
|
||||||
status varchar(20) not null,
|
|
||||||
requested_at timestamp not null,
|
|
||||||
approved_at timestamp not null,
|
|
||||||
|
|
||||||
constraint uk_payment__reservationId unique (reservation_id),
|
|
||||||
constraint fk_payment__reservationId foreign key (reservation_id) references reservation (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists payment_detail(
|
|
||||||
id bigint primary key,
|
|
||||||
payment_id bigint not null unique,
|
|
||||||
supplied_amount integer not null,
|
|
||||||
vat integer not null,
|
|
||||||
|
|
||||||
constraint fk_payment_detail__paymentId foreign key (payment_id) references payment (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists payment_bank_transfer_detail (
|
|
||||||
id bigint primary key,
|
|
||||||
bank_code varchar(20) not null,
|
|
||||||
settlement_status varchar(20) not null,
|
|
||||||
|
|
||||||
constraint fk_payment_bank_transfer_details__id foreign key (id) references payment_detail (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists payment_card_detail (
|
|
||||||
id bigint primary key,
|
|
||||||
issuer_code varchar(20) not null,
|
|
||||||
card_type varchar(10) not null,
|
|
||||||
owner_type varchar(10) not null,
|
|
||||||
amount integer not null,
|
|
||||||
card_number varchar(20) not null,
|
|
||||||
approval_number varchar(8) not null, -- 실제로는 unique 이지만 테스트 결제 위젯에서는 항상 000000으로 동일한 값이 나옴.
|
|
||||||
installment_plan_months tinyint not null,
|
|
||||||
is_interest_free boolean not null,
|
|
||||||
easypay_provider_code varchar(20),
|
|
||||||
easypay_discount_amount integer,
|
|
||||||
|
|
||||||
constraint fk_payment_card_detail__id foreign key (id) references payment_detail (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists payment_easypay_prepaid_detail(
|
|
||||||
id bigint primary key,
|
|
||||||
easypay_provider_code varchar(20) not null,
|
|
||||||
amount integer not null,
|
|
||||||
discount_amount integer not null,
|
|
||||||
|
|
||||||
constraint fk_payment_easypay_prepaid_detail__id foreign key (id) references payment_detail (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists canceled_payment(
|
|
||||||
id bigint primary key,
|
|
||||||
payment_id bigint not null,
|
|
||||||
requested_at timestamp not null,
|
|
||||||
canceled_at timestamp not null,
|
|
||||||
canceled_by bigint not null,
|
|
||||||
cancel_reason varchar(255) not null,
|
|
||||||
cancel_amount integer not null,
|
|
||||||
card_discount_amount integer not null,
|
|
||||||
transfer_discount_amount integer not null,
|
|
||||||
easypay_discount_amount integer not null,
|
|
||||||
|
|
||||||
constraint uk_canceled_payment__paymentId unique (payment_id),
|
|
||||||
constraint fk_canceled_payment__paymentId foreign key (payment_id) references payment(id)
|
|
||||||
);
|
|
||||||
91
test-scripts/common.js
Normal file
91
test-scripts/common.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import http from 'k6/http';
|
||||||
|
|
||||||
|
export const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
|
||||||
|
|
||||||
|
export function generateRandomBase64String(length) {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseIdToString(response) {
|
||||||
|
try {
|
||||||
|
const safeJsonString = response.body.replace(/"(\w*Id|id)"\s*:\s*(\d{16,})/g, '"$1":"$2"');
|
||||||
|
return JSON.parse(safeJsonString);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`JSON parsing failed for VU ${__VU}: ${e}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function maxIterations() {
|
||||||
|
const maxIterationsRes = http.get(`${BASE_URL}/tests/max-iterations`)
|
||||||
|
if (maxIterationsRes.status !== 200) {
|
||||||
|
throw new Error('max-iterations 조회 실패')
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxIterationsRes.json('count')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUsers() {
|
||||||
|
const userCount = Math.round(maxIterations() * 1.2)
|
||||||
|
const userAccountRes = http.get(`${BASE_URL}/tests/users?count=${userCount}`)
|
||||||
|
|
||||||
|
if (userAccountRes.status !== 200) {
|
||||||
|
throw new Error('users 조회 실패')
|
||||||
|
}
|
||||||
|
|
||||||
|
return userAccountRes.json('results')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchStores() {
|
||||||
|
const storeIdListRes = http.get(`${BASE_URL}/tests/stores`)
|
||||||
|
|
||||||
|
if (storeIdListRes.status !== 200) {
|
||||||
|
throw new Error('stores 조회 실패')
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseIdToString(storeIdListRes).results
|
||||||
|
}
|
||||||
|
|
||||||
|
export function login(account, password, principalType) {
|
||||||
|
const loginPayload = JSON.stringify({
|
||||||
|
account: account,
|
||||||
|
password: password,
|
||||||
|
principalType: principalType
|
||||||
|
})
|
||||||
|
const params = { headers: { 'Content-Type': 'application/json' } }
|
||||||
|
|
||||||
|
const loginRes = http.post(`${BASE_URL}/auth/login`, loginPayload, params)
|
||||||
|
|
||||||
|
if (loginRes.status !== 200) {
|
||||||
|
throw new Error(`로그인 실패: ${__VU}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = parseIdToString(loginRes).data
|
||||||
|
if (principalType === 'ADMIN') {
|
||||||
|
return {
|
||||||
|
storeId: body.storeId,
|
||||||
|
accessToken: body.accessToken
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
accessToken: body.accessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHeaders(token) {
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { headers: headers };
|
||||||
|
}
|
||||||
250
test-scripts/create-reservation-scripts.js
Normal file
250
test-scripts/create-reservation-scripts.js
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import {
|
||||||
|
BASE_URL,
|
||||||
|
fetchStores,
|
||||||
|
fetchUsers,
|
||||||
|
generateRandomBase64String,
|
||||||
|
getHeaders,
|
||||||
|
login,
|
||||||
|
parseIdToString
|
||||||
|
} from "./common.js";
|
||||||
|
import {check, group, sleep} from 'k6';
|
||||||
|
import {randomIntBetween, randomItem} from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
|
||||||
|
import http from 'k6/http';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
scenarios: {
|
||||||
|
user_reservation: {
|
||||||
|
executor: 'ramping-vus',
|
||||||
|
startVUs: 0,
|
||||||
|
stages: [
|
||||||
|
{ duration: '1m', target: 5 },
|
||||||
|
{ duration: '1m', target: 10 },
|
||||||
|
{ duration: '1m', target: 15 },
|
||||||
|
{ duration: '1m', target: 20 },
|
||||||
|
{ duration: '1m', target: 25 },
|
||||||
|
{ duration: '1m', target: 30 },
|
||||||
|
{ duration: '1m', target: 35 },
|
||||||
|
{ duration: '1m', target: 40 },
|
||||||
|
{ duration: '1m', target: 0 },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
thresholds: {
|
||||||
|
http_req_duration: ['p(95)<3000'],
|
||||||
|
http_req_failed: ['rate<0.15'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function randomDayBetween(startFrom, endAt) {
|
||||||
|
const random = randomIntBetween(startFrom, endAt)
|
||||||
|
const targetDate = new Date()
|
||||||
|
targetDate.setDate(targetDate.getDate() + random)
|
||||||
|
|
||||||
|
return targetDate.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function countForFetchDetails(themeCount) {
|
||||||
|
const random = Math.random()
|
||||||
|
|
||||||
|
if (random < 0.5) {
|
||||||
|
return Math.min(0, themeCount)
|
||||||
|
}
|
||||||
|
if (random < 0.75) {
|
||||||
|
return Math.min(1, themeCount)
|
||||||
|
}
|
||||||
|
if (random < 0.9) {
|
||||||
|
return Math.min(2, themeCount)
|
||||||
|
}
|
||||||
|
return Math.min(randomIntBetween(3, themeCount), themeCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractRandomThemeForFetchDetail(themes) {
|
||||||
|
const count = countForFetchDetails(themes.length)
|
||||||
|
const shuffled = [...themes].sort(() => Math.random() - 0.5)
|
||||||
|
|
||||||
|
return shuffled.slice(0, count).map(t => t.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setup() {
|
||||||
|
const users = fetchUsers()
|
||||||
|
const stores = fetchStores()
|
||||||
|
|
||||||
|
console.log(`회원 수 = ${users.length} 조회 완료`)
|
||||||
|
console.log(`매장 수 = ${stores.length} 조회 완료`)
|
||||||
|
|
||||||
|
return { users, stores }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (data) {
|
||||||
|
const { users, stores } = data;
|
||||||
|
|
||||||
|
const user = randomItem(users)
|
||||||
|
const accessToken = login(user.account, user.password, 'USER').accessToken
|
||||||
|
const storeId = randomItem(stores).storeId
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
console.log(`로그인 실패: token=${accessToken}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const targetDate = randomDayBetween(1, 6)
|
||||||
|
|
||||||
|
let availableScheduleId, selectedThemeId, selectedThemeInfo, reservationId, totalAmount
|
||||||
|
|
||||||
|
group(`매장=${storeId}, 날짜=${targetDate}의 일정 조회`, function () {
|
||||||
|
const res = http.get(`${BASE_URL}/stores/${storeId}/schedules?date=${targetDate}`)
|
||||||
|
if (check(res, { '일정 조회 성공': (r) => r.status === 200 })) {
|
||||||
|
sleep(20)
|
||||||
|
let schedules = parseIdToString(res).data.schedules
|
||||||
|
|
||||||
|
if (!schedules || schedules.length === 0) {
|
||||||
|
console.log("일정 없음. 1회 재시도")
|
||||||
|
const storeId = randomItem(stores).storeId
|
||||||
|
const targetDate = randomDayBetween(0, 6)
|
||||||
|
const res = http.get(`${BASE_URL}/stores/${storeId}/schedules?date=${targetDate}`)
|
||||||
|
schedules = parseIdToString(res).data.schedules
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schedules && schedules.length > 0) {
|
||||||
|
group(`일부 테마는 상세 조회`, function () {
|
||||||
|
const themesByStoreAndDate = schedules.map(s => s.theme)
|
||||||
|
if (!themesByStoreAndDate && themesByStoreAndDate.length <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const randomThemesForFetchDetail = extractRandomThemeForFetchDetail(themesByStoreAndDate)
|
||||||
|
|
||||||
|
randomThemesForFetchDetail.forEach(id => {
|
||||||
|
http.get(`${BASE_URL}/themes/${id}`)
|
||||||
|
sleep(10)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const availableSchedules = schedules.filter((s) => s.schedule.status === 'AVAILABLE')
|
||||||
|
if (availableSchedules.length > 0) {
|
||||||
|
const availableSchedule = randomItem(availableSchedules)
|
||||||
|
availableScheduleId = availableSchedule.schedule.id
|
||||||
|
selectedThemeId = availableSchedule.theme.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!availableScheduleId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
group(`일정 Holding 및 테마 정보 조회 -> 예약 과정중 첫 페이지의 작업 완료`, function () {
|
||||||
|
const holdRes = http.post(`${BASE_URL}/schedules/${availableScheduleId}/hold`, null, getHeaders(accessToken))
|
||||||
|
|
||||||
|
if (check(holdRes, { '일정 점유 성공': (r) => r.status === 200 })) {
|
||||||
|
const themeInfoRes = http.get(`${BASE_URL}/themes/${selectedThemeId}`)
|
||||||
|
if (themeInfoRes.status !== 200) {
|
||||||
|
throw new Error("테마 상세 조회 실패")
|
||||||
|
}
|
||||||
|
selectedThemeInfo = parseIdToString(themeInfoRes).data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let isPendingReservationCreated = false
|
||||||
|
|
||||||
|
group(`예약 정보 입력 페이지`, function () {
|
||||||
|
let userName, userContact
|
||||||
|
group(`회원 연락처 조회`, function () {
|
||||||
|
const userContactRes = http.get(`${BASE_URL}/users/contact`, getHeaders(accessToken))
|
||||||
|
|
||||||
|
if (!check(userContactRes, { '회원 연락처 조회 성공': (r) => r.status === 200 })) {
|
||||||
|
throw new Error("회원 연락처 조회 과정에서 예외 발생")
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseBody = JSON.parse(userContactRes.body).data
|
||||||
|
userName = responseBody.name
|
||||||
|
userContact = responseBody.phone
|
||||||
|
})
|
||||||
|
|
||||||
|
// 20%의 유저는 예약 정보 입력창에서 나감 => 배치의 자동 활성화 테스트
|
||||||
|
if (Math.random() <= 0.2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(20)
|
||||||
|
|
||||||
|
group(`예약 정보 입력 및 Pending 예약 생성`, function () {
|
||||||
|
const requirement = `${selectedThemeInfo.name}을 잘부탁드려요!`
|
||||||
|
const participants = randomIntBetween(selectedThemeInfo.minParticipants, selectedThemeInfo.maxParticipants)
|
||||||
|
totalAmount = Math.round(participants * selectedThemeInfo.price)
|
||||||
|
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
scheduleId: availableScheduleId,
|
||||||
|
reserverName: userName,
|
||||||
|
reserverContact: userContact,
|
||||||
|
participantCount: participants,
|
||||||
|
requirement: requirement
|
||||||
|
})
|
||||||
|
|
||||||
|
const pendingReservationCreateRes = http.post(`${BASE_URL}/reservations/pending`, payload, getHeaders(accessToken))
|
||||||
|
const responseBody = parseIdToString(pendingReservationCreateRes)
|
||||||
|
|
||||||
|
if (pendingReservationCreateRes.status !== 200) {
|
||||||
|
const errorCode = responseBody.code
|
||||||
|
const errorMessage = responseBody.message
|
||||||
|
|
||||||
|
throw new Error(`Pending 예약 중 실패: code=${errorCode}, message=${errorMessage}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
reservationId = responseBody.data.id
|
||||||
|
isPendingReservationCreated = true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isPendingReservationCreated) {
|
||||||
|
console.log("회원의 예약 정보 입력 중 페이지 이탈")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
group(`결제 및 예약 확정`, function () {
|
||||||
|
// 20%의 유저는 결제 화면에서 나감 => 배치의 자동 만료 처리 테스트
|
||||||
|
if (Math.random() <= 0.2) {
|
||||||
|
console.log("결제 페이지에서의 이탈")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentKey = generateRandomBase64String(64)
|
||||||
|
const orderId = generateRandomBase64String(25)
|
||||||
|
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
paymentKey: paymentKey,
|
||||||
|
orderId: orderId,
|
||||||
|
amount: totalAmount,
|
||||||
|
})
|
||||||
|
|
||||||
|
let trial = 0
|
||||||
|
let isConfirmed = false
|
||||||
|
while (trial < 2) {
|
||||||
|
sleep(30)
|
||||||
|
const confirmOrderRes = http.post(`${BASE_URL}/orders/${reservationId}/confirm`, payload, getHeaders(accessToken))
|
||||||
|
|
||||||
|
if (check(confirmOrderRes, { '예약 확정 성공': (r) => r.status === 200 })) {
|
||||||
|
isConfirmed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorResponse = JSON.parse(confirmOrderRes.body)
|
||||||
|
console.log(`예약 확정 실패: message=${errorResponse.message}`)
|
||||||
|
trial = errorResponse.trial + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 예약이 확정되었으면 종료, 아니면 임시 확정
|
||||||
|
if (isConfirmed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(10)
|
||||||
|
const temporalConfirmRes = http.post(`${BASE_URL}/reservations/${reservationId}/confirm`)
|
||||||
|
if (check(temporalConfirmRes, { '임시 예약 확정 성공': (r) => r.status === 200})) {
|
||||||
|
console.log("예약 확정 성공")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throw new Error('임시 예약 확정 실패')
|
||||||
|
})
|
||||||
|
}
|
||||||
135
test-scripts/create-schedule-scripts.js
Normal file
135
test-scripts/create-schedule-scripts.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import http from 'k6/http';
|
||||||
|
import {check, sleep} from 'k6';
|
||||||
|
import exec from 'k6/execution';
|
||||||
|
import {BASE_URL, getHeaders, login, parseIdToString} from "./common.js";
|
||||||
|
import {randomIntBetween} from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
|
||||||
|
|
||||||
|
|
||||||
|
const TOTAL_ITERATIONS = 85212;
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
scenarios: {
|
||||||
|
schedule_creation: {
|
||||||
|
executor: 'shared-iterations',
|
||||||
|
vus: 263,
|
||||||
|
iterations: TOTAL_ITERATIONS,
|
||||||
|
maxDuration: '10m',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
thresholds: {
|
||||||
|
'http_req_duration': ['p(95)<3000'],
|
||||||
|
'http_req_failed': ['rate<0.1'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function setup() {
|
||||||
|
console.log('=== Setup: 테스트 데이터 및 작업 목록 준비 중 ===');
|
||||||
|
|
||||||
|
const themesRes = http.get(`${BASE_URL}/tests/themes`);
|
||||||
|
const themes = parseIdToString(themesRes).results
|
||||||
|
|
||||||
|
const accountsRes = http.get(`${BASE_URL}/tests/admin/accounts`);
|
||||||
|
const accounts = JSON.parse(accountsRes.body).results;
|
||||||
|
|
||||||
|
const dates = generateDates(7);
|
||||||
|
|
||||||
|
console.log(`총 매장 수: ${accounts.length}`);
|
||||||
|
console.log(`총 테마 수: ${themes.length}`);
|
||||||
|
console.log(`생성 기간: ${dates.length}일`);
|
||||||
|
|
||||||
|
const tasks = [];
|
||||||
|
for (const account of accounts) {
|
||||||
|
const loginResult = login(account.account, account.password, 'ADMIN');
|
||||||
|
if (loginResult === null) {
|
||||||
|
console.error(`[Setup] 로그인 실패: ${account.account}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const {storeId, accessToken} = loginResult;
|
||||||
|
|
||||||
|
// 5 ~ ${themes.size} 인 random 숫자 생성
|
||||||
|
const selectedThemes = selectRandomThemes(themes, randomIntBetween(5, themes.length));
|
||||||
|
|
||||||
|
for (const theme of selectedThemes) {
|
||||||
|
for (const date of dates) {
|
||||||
|
for (const time of theme.times) {
|
||||||
|
tasks.push({
|
||||||
|
storeId,
|
||||||
|
accessToken,
|
||||||
|
date,
|
||||||
|
time: time.startFrom,
|
||||||
|
themeId: theme.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`총 생성할 스케줄 수(iterations): ${tasks.length}`);
|
||||||
|
|
||||||
|
return {tasks};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (data) {
|
||||||
|
// 👈 3. 현재 반복 횟수가 준비된 작업 수를 초과하는지 확인
|
||||||
|
const taskIndex = exec.scenario.iterationInTest;
|
||||||
|
|
||||||
|
if (taskIndex >= data.tasks.length) {
|
||||||
|
// 첫 번째로 이 조건에 도달한 VU가 테스트를 종료
|
||||||
|
if (taskIndex === data.tasks.length) {
|
||||||
|
console.log('모든 스케쥴 생성 완료. 테스트 종료');
|
||||||
|
exec.test.abort();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const task = data.tasks[taskIndex];
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
console.log(`[VU ${__VU}] 알 수 없는 오류: task가 없습니다. (index: ${taskIndex})`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
createSchedule(task.storeId, task.accessToken, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSchedule(storeId, accessToken, schedule) {
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
date: schedule.date,
|
||||||
|
time: schedule.time,
|
||||||
|
themeId: schedule.themeId,
|
||||||
|
});
|
||||||
|
const params = getHeaders(accessToken)
|
||||||
|
const res = http.post(`${BASE_URL}/admin/stores/${storeId}/schedules`, payload, params);
|
||||||
|
|
||||||
|
const success = check(res, {'일정 생성 성공': (r) => r.status === 200 || r.status === 201});
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
console.error(`일정 생성 실패 [${res.status}]: 매장=${storeId}, ${schedule.date} ${schedule.time} (테마: ${schedule.themeId}) | 응답: ${res.body}`);
|
||||||
|
}
|
||||||
|
sleep(5)
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDates(days) {
|
||||||
|
const dates = [];
|
||||||
|
const today = new Date();
|
||||||
|
for (let i = 1; i < days; i++) {
|
||||||
|
const date = new Date(today);
|
||||||
|
date.setDate(today.getDate() + i);
|
||||||
|
dates.push(date.toISOString().split('T')[0]);
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectRandomThemes(themes, count) {
|
||||||
|
const shuffled = [...themes].sort(() => 0.5 - Math.random());
|
||||||
|
return shuffled.slice(0, Math.min(count, themes.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function teardown(data) {
|
||||||
|
if (data.tasks) {
|
||||||
|
console.log(`\n=== 테스트 완료: 총 ${data.tasks.length}개의 스케줄 생성을 시도했습니다. ===`);
|
||||||
|
} else {
|
||||||
|
console.log('\n=== 테스트 완료: setup 단계에서 오류가 발생하여 작업을 실행하지 못했습니다. ===');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,7 +28,7 @@ spring:
|
|||||||
sql:
|
sql:
|
||||||
init:
|
init:
|
||||||
mode: always
|
mode: always
|
||||||
schema-locations: classpath:schema/schema-h2.sql
|
schema-locations: classpath:schema/schema-mysql.sql
|
||||||
|
|
||||||
management:
|
management:
|
||||||
endpoints:
|
endpoints:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user