[#46] 더미 데이터 생성 및 1개의 슬로우쿼리 개선 #47

Merged
pricelees merged 15 commits from feat/#46 into main 2025-09-27 06:38:44 +00:00
Showing only changes of commit f1f7af165b - Show all commits

View File

@ -0,0 +1,911 @@
package roomescape.data
import com.github.f4b6a3.tsid.TsidFactory
import io.kotest.core.test.TestCaseOrder
import jakarta.persistence.EntityManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.test.context.ActiveProfiles
import roomescape.admin.infrastructure.persistence.AdminEntity
import roomescape.admin.infrastructure.persistence.AdminPermissionLevel
import roomescape.admin.infrastructure.persistence.AdminType
import roomescape.common.config.next
import roomescape.common.util.TransactionExecutionUtil
import roomescape.payment.infrastructure.common.*
import roomescape.reservation.infrastructure.persistence.ReservationStatus
import roomescape.schedule.infrastructure.persistence.ScheduleStatus
import roomescape.supports.AdminFixture
import roomescape.supports.FunSpecSpringbootTest
import roomescape.supports.randomPhoneNumber
import roomescape.supports.randomString
import roomescape.theme.infrastructure.persistence.Difficulty
import roomescape.user.business.SIGNUP
import roomescape.user.infrastructure.persistence.UserEntity
import roomescape.user.infrastructure.persistence.UserStatus
import roomescape.user.web.UserContactResponse
import java.sql.Timestamp
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.OffsetDateTime
@ActiveProfiles("test", "test-mysql")
abstract class AbstractDataInitializer(
val semaphore: Semaphore = Semaphore(permits = 10),
) : FunSpecSpringbootTest(
enableCleanerExtension = false
) {
@Autowired
lateinit var entityManager: EntityManager
@Autowired
lateinit var jdbcTemplate: JdbcTemplate
@Autowired
lateinit var transactionExecutionUtil: TransactionExecutionUtil
@Autowired
lateinit var tsidFactory: TsidFactory
override fun testCaseOrder(): TestCaseOrder? = TestCaseOrder.Sequential
suspend fun initialize() {
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 0")
jdbcTemplate.query("SHOW TABLES") { rs, _ ->
rs.getString(1).lowercase()
}.forEach {
jdbcTemplate.execute("TRUNCATE TABLE $it")
}
jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 1")
this::class.java.getResource("/schema/region-data.sql")?.readText()?.let { sql ->
jdbcTemplate.execute(sql)
}
}
}
suspend fun executeBatch(sql: String, batchArgs: List<Array<Any>>) {
semaphore.acquire()
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
jdbcTemplate.batchUpdate(sql, batchArgs)
}
semaphore.release()
}
}
class DefaultDataInitializer : AbstractDataInitializer() {
// 1. HQ Admin 추가
// 2. Store 추가 -> CreatedBy / UpdatedBy는 HQ Admin 중 한명으로 고정
// 3. Store Admin 추가 -> CreatedBy / UpdatedBy는 HQ Admin 본인으로 고정
init {
lateinit var superHQAdmin: AdminEntity
// 모든 테이블 초기화 + 지역 데이터 삽입 + HQ 관리자 1명 생성
beforeSpec {
initialize()
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
superHQAdmin = testAuthUtil.createAdmin(AdminFixture.hqDefault.apply {
this.createdBy = this.id
this.updatedBy = this.id
})
}
}
context("관리자, 매장, 테마 초기 데이터 생성") {
test("각 PermissionLevel 마다 20명의 HQ 관리자 생성") {
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
AdminPermissionLevel.entries.forEach {
repeat(20) { index ->
AdminFixture.create(
account = "hq_${it.name.lowercase()}_$index",
name = randomKoreanName(),
phone = randomPhoneNumber(),
type = AdminType.HQ,
permissionLevel = it
).apply {
this.createdBy = superHQAdmin.id
this.updatedBy = superHQAdmin.id
}.also {
entityManager.persist(it)
}
}
}
}
}
test("전체 매장 생성") {
val storeDataInitializer = StoreDataInitializer()
val creatableAdminIds: List<String> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"SELECT a.id FROM AdminEntity a WHERE a.type = :type AND a.permissionLevel IN (:permissionLevels)",
Long::class.java
).setParameter("type", AdminType.HQ)
.setParameter(
"permissionLevels",
listOf(AdminPermissionLevel.FULL_ACCESS, AdminPermissionLevel.WRITABLE)
)
.resultList
}.map { it.toString() }
val sqlFile = storeDataInitializer.createStoreDataSqlFile(creatableAdminIds)
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
jdbcTemplate.execute(sqlFile.readText())
}
}
test("각 매장당 1명의 ${AdminPermissionLevel.FULL_ACCESS} 권한의 StoreManager 1명 + ${AdminPermissionLevel.WRITABLE}의 2명 + 나머지 권한은 3명씩 생성") {
val storeAdminCountsByPermissionLevel = mapOf(
AdminPermissionLevel.WRITABLE to 2,
AdminPermissionLevel.READ_ALL to 3,
AdminPermissionLevel.READ_SUMMARY to 3
)
val storeIds: List<Long> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"SELECT s.id FROM StoreEntity s",
Long::class.java
).resultList
}.map { it as Long }
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
storeIds.forEach { storeId ->
// StoreManager 1명 생성
val storeManager = AdminFixture.create(
account = "$storeId",
name = randomKoreanName(),
phone = randomPhoneNumber(),
type = AdminType.STORE,
storeId = storeId,
permissionLevel = AdminPermissionLevel.FULL_ACCESS
).apply {
this.createdBy = superHQAdmin.id
this.updatedBy = superHQAdmin.id
}.also {
entityManager.persist(it)
}
storeAdminCountsByPermissionLevel.forEach { (permissionLevel, count) ->
repeat(count) { index ->
AdminFixture.create(
account = randomString(),
name = randomKoreanName(),
phone = randomPhoneNumber(),
type = AdminType.STORE,
storeId = storeId,
permissionLevel = permissionLevel
).apply {
this.createdBy = storeManager.id
this.updatedBy = storeManager.id
}.also {
entityManager.persist(it)
}
}
}
entityManager.flush()
entityManager.clear()
}
}
}
test("총 500개의 테마 생성: 지난 2년 전 부터 지금까지의 랜덤 테마 + active 상태인 1달 이내 생성 테마 10개") {
val creatableAdminIds: List<Long> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"SELECT a.id FROM AdminEntity a WHERE a.type = :type AND a.permissionLevel IN (:permissionLevels)",
Long::class.java
).setParameter("type", AdminType.HQ)
.setParameter(
"permissionLevels",
listOf(AdminPermissionLevel.FULL_ACCESS, AdminPermissionLevel.WRITABLE)
)
.resultList
}
val sql =
"INSERT INTO theme (id, name, description, thumbnail_url, is_active, available_minutes, expected_minutes_from, expected_minutes_to, price, difficulty, min_participants, max_participants, created_at, created_by, updated_at, updated_by) VALUES (?," +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
val batchSize = 100
val batchArgs = mutableListOf<Array<Any>>()
repeat(500) { i ->
val randomDay = if (i <= 9) (1..30).random() else (1..365 * 2).random()
val randomCreatedAt: LocalDateTime = LocalDateTime.now().minusDays(randomDay.toLong())
val randomThemeName =
(1..7).random().let { repeat -> (1..repeat).joinToString("") { randomKoreanName() } }
val availableMinutes = (6..20).random() * 10
val expectedMinutesTo = availableMinutes - ((1..3).random() * 10)
val expectedMinutesFrom = expectedMinutesTo - ((1..2).random() * 10)
val randomPrice = (0..40).random() * 500
val minParticipant = (1..10).random()
val maxParticipant = minParticipant + (1..10).random()
val createdBy = creatableAdminIds.random()
batchArgs.add(
arrayOf(
tsidFactory.next(),
randomThemeName,
"$randomThemeName 설명이에요!!",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRFiuCdwdz88l6pdfsRy1nFl0IHUVI7JMTQHg&s",
if (randomDay <= 30) true else false,
availableMinutes.toShort(),
expectedMinutesFrom.toShort(),
expectedMinutesTo.toShort(),
randomPrice,
Difficulty.entries.random().name,
minParticipant.toShort(),
maxParticipant.toShort(),
Timestamp.valueOf(randomCreatedAt),
createdBy,
Timestamp.valueOf(randomCreatedAt),
createdBy
)
)
}
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
jdbcTemplate.batchUpdate(sql, batchArgs)
}
}
}
}
}
class UserDataInitializer : AbstractDataInitializer() {
val userCount = 1_000_000
init {
context("유저 초기 데이터 생성") {
test("$userCount 명의 회원 생성") {
val regions: List<String> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"SELECT r.code FROM RegionEntity r",
String::class.java
).resultList
}
val chunkSize = 10_000
val chunks = userCount / chunkSize
coroutineScope {
(1..chunks).map {
launch(Dispatchers.IO) {
processUsers(chunkSize, regions)
}
}.joinAll()
}
}
test("휴대폰 번호가 중복된 유저들에게 재배정") {
val duplicatePhoneUsers: List<UserEntity> =
transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"""
SELECT u FROM UserEntity u
WHERE u.phone IN (
SELECT u2.phone FROM UserEntity u2
GROUP BY u2.phone
HAVING COUNT(u2.id) > 1
)
ORDER BY u.phone, u.id
""".trimIndent(),
UserEntity::class.java
).resultList
}
jdbcTemplate.execute("CREATE INDEX idx_users__phone ON users (phone)")
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
var currentPhone: String? = null
duplicatePhoneUsers.forEach { user ->
if (user.phone != currentPhone) {
currentPhone = user.phone
} else {
var newPhone: String
do {
newPhone = randomPhoneNumber()
val count: Long = entityManager.createQuery(
"SELECT COUNT(u.id) FROM UserEntity u WHERE u.phone = :phone",
Long::class.java
).setParameter("phone", newPhone)
.singleResult
if (count == 0L) break
} while (true)
user.phone = newPhone
user.updatedAt = LocalDateTime.now()
entityManager.merge(user)
}
}
}
jdbcTemplate.execute("DROP INDEX idx_users__phone ON users")
}
test("회원 상태 변경 이력 저장") {
val userId: List<Long> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"SELECT u.id FROM UserEntity u",
Long::class.java
).resultList
}
coroutineScope {
userId.chunked(10_000).map { chunk ->
launch(Dispatchers.IO) {
processStatus(chunk)
}
}.joinAll()
}
}
}
}
private suspend fun processStatus(userIds: List<Long>) {
val sql = """
INSERT INTO user_status_history (
id, user_id, reason, status,
created_by, updated_by, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
val batchArgs = mutableListOf<Array<Any>>()
val now = LocalDateTime.now()
userIds.forEach { userId ->
batchArgs.add(
arrayOf(
tsidFactory.next(),
userId,
SIGNUP,
UserStatus.ACTIVE.name,
userId,
userId,
Timestamp.valueOf(now),
Timestamp.valueOf(now)
)
)
}
executeBatch(sql, batchArgs).also { batchArgs.clear() }
}
private suspend fun processUsers(chunkSize: Int, regions: List<String>) {
val sql = """
INSERT INTO users (
id, name, email, password, phone, region_code, status,
created_by, updated_by, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
val batchArgs = mutableListOf<Array<Any>>()
repeat(chunkSize) {
val id: Long = tsidFactory.next()
batchArgs.add(
arrayOf(
id,
randomKoreanName(),
"${randomString()}@sangdol.com",
randomString(),
randomPhoneNumber(),
regions.random(),
UserStatus.ACTIVE.name,
id,
id,
Timestamp.valueOf(LocalDateTime.now()),
Timestamp.valueOf(LocalDateTime.now())
)
)
if (batchArgs.size >= 1_000) {
executeBatch(sql, batchArgs).also { batchArgs.clear() }
}
}
if (batchArgs.isNotEmpty()) executeBatch(sql, batchArgs).also { batchArgs.clear() }
}
}
class ScheduleDataInitializer : AbstractDataInitializer() {
init {
context("일정 초기 데이터 생성") {
test("테마 생성일 기준으로 다음 3일차, 매일 5개의 일정을 모든 매장에 생성") {
val stores: List<Pair<Long, Long>> = getStoreWithManagers()
val themes: List<Triple<Long, Short, LocalDateTime>> = getThemes()
val maxAvailableMinutes = themes.maxOf { it.second.toInt() }
val scheduleCountPerDay = 5
val startTime = LocalTime.of(10, 0)
var lastTime = startTime
val times = mutableListOf<LocalTime>()
repeat(scheduleCountPerDay) {
times.add(lastTime)
lastTime = lastTime.plusMinutes(maxAvailableMinutes.toLong() + 10L)
}
coroutineScope {
themes.forEach { theme ->
launch(Dispatchers.IO) {
processTheme(theme, stores, times)
}
}
}
}
}
}
private suspend fun processTheme(
theme: Triple<Long, Short, LocalDateTime>,
stores: List<Pair<Long, Long>>,
times: List<LocalTime>
) {
val sql = """
INSERT INTO schedule (
id, store_id, theme_id, date, time, status,
created_by, updated_by, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
val batchArgs = mutableListOf<Array<Any>>()
val now = LocalDateTime.now()
stores.forEach { (storeId, adminId) ->
(1..3).forEach { dayOffset ->
val date = theme.third.toLocalDate().plusDays(dayOffset.toLong())
times.forEach { time ->
val scheduledAt = LocalDateTime.of(date, time)
val status =
if (scheduledAt.isAfter(now)) ScheduleStatus.AVAILABLE.name else ScheduleStatus.RESERVED.name
batchArgs.add(
arrayOf(
tsidFactory.next(), storeId, theme.first, date, time,
status, adminId, adminId, Timestamp.valueOf(now), Timestamp.valueOf(now)
)
)
if (batchArgs.size >= 500) {
executeBatch(sql, batchArgs).also { batchArgs.clear() }
}
}
}
}
if (batchArgs.isNotEmpty()) {
executeBatch(sql, batchArgs).also { batchArgs.clear() }
}
}
private fun getStoreWithManagers(): List<Pair<Long, Long>> {
return transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"SELECT a.storeId, a.id FROM AdminEntity a WHERE a.type = :type AND a.permissionLevel = :permissionLevel",
List::class.java
).setParameter("type", AdminType.STORE)
.setParameter("permissionLevel", AdminPermissionLevel.FULL_ACCESS)
.resultList
}.map {
val array = it as List<*>
Pair(array[0] as Long, array[1] as Long)
}
}
private fun getThemes(): List<Triple<Long, Short, LocalDateTime>> {
return transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"SELECT t._id, t.availableMinutes, t.createdAt FROM ThemeEntity t",
List::class.java
)
.resultList
}.map {
val array = it as List<*>
Triple(array[0] as Long, array[1] as Short, array[2] as LocalDateTime)
}
}
}
/**
* 아래의 ReservationDataInitializer 에서 사용할 임시 DTO 클래스
*/
data class ScheduleWithThemeParticipants(
val scheduleId: Long,
val themeMinParticipants: Short,
val themeMaxParticipants: Short,
)
class ReservationDataInitializer : AbstractDataInitializer() {
init {
context("예약 초기 데이터 생성") {
test("${ScheduleStatus.RESERVED}인 모든 일정에 예약을 1개씩 배정한다.") {
val chunkSize = 10_000
val chunkedSchedules: List<List<ScheduleWithThemeParticipants>> = entityManager.createQuery(
"SELECT new roomescape.data.ScheduleWithThemeParticipants(s._id, t.minParticipants, t.maxParticipants) FROM ScheduleEntity s JOIN ThemeEntity t ON s.themeId = t.id WHERE s.status = :status",
ScheduleWithThemeParticipants::class.java
).setParameter("status", ScheduleStatus.RESERVED).resultList.chunked(chunkSize)
val chunkedUsers: List<List<UserContactResponse>> = entityManager.createQuery(
"SELECT new roomescape.user.web.UserContactResponse(u._id, u.name, u.phone) FROM UserEntity u",
UserContactResponse::class.java
).resultList.chunked(chunkSize)
coroutineScope {
chunkedSchedules.forEachIndexed { idx, schedules ->
launch(Dispatchers.IO) {
processReservation(chunkedUsers[idx % chunkedUsers.size], schedules)
}
}
}
}
}
}
private suspend fun processReservation(
users: List<UserContactResponse>,
schedules: List<ScheduleWithThemeParticipants>
) {
val sql = """
INSERT INTO reservation (
id, user_id, schedule_id,
reserver_name, reserver_contact, participant_count, requirement,
status, created_at, created_by, updated_at, updated_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
val batchArgs = mutableListOf<Array<Any>>()
val createdAt = LocalDateTime.now()
schedules.forEachIndexed { idx, schedule ->
val user: UserContactResponse = users[idx % users.size]
batchArgs.add(
arrayOf(
tsidFactory.next(),
user.id,
schedule.scheduleId,
user.name,
user.phone,
(schedule.themeMinParticipants..schedule.themeMaxParticipants).random(),
randomKoreanWords(length = (20..100).random()),
ReservationStatus.CONFIRMED.name,
Timestamp.valueOf(createdAt),
user.id,
Timestamp.valueOf(createdAt),
user.id,
)
)
if (batchArgs.size >= 1_000) {
executeBatch(sql, batchArgs).also { batchArgs.clear() }
}
}
if (batchArgs.isNotEmpty()) executeBatch(sql, batchArgs).also { batchArgs.clear() }
}
}
class ReservationWithPrice(
themePrice: Int,
participantCount: Short,
val reservationId: Long,
val totalPrice: Int = (themePrice * participantCount),
)
data class PaymentWithMethods(
val id: Long,
val totalPrice: Int,
val method: PaymentMethod
)
class PaymentDataInitializer : AbstractDataInitializer() {
companion object {
val requestedAtCache: Timestamp = Timestamp.valueOf(OffsetDateTime.now().toLocalDateTime())
val approvedAtCache: Timestamp = Timestamp.valueOf(OffsetDateTime.now().plusSeconds(5).toLocalDateTime())
val supportedPaymentMethods = listOf(PaymentMethod.TRANSFER, PaymentMethod.EASY_PAY, PaymentMethod.CARD)
val supportedCardType = listOf(CardType.CREDIT, CardType.CHECK)
val settlementStatus = "COMPLETED"
val paymentSql: String = """
INSERT INTO payment(
id, reservation_id, type, method,
payment_key, order_id, total_amount, status,
requested_at, approved_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
val paymentDetailSql: String = """
INSERT INTO payment_detail(
id, payment_id, supplied_amount, vat
) VALUES (?, ?, ?, ?)
""".trimIndent()
val paymentCardDetailSql: String = """
INSERT INTO payment_card_detail(
id, issuer_code, card_type, owner_type,
amount, card_number, approval_number, installment_plan_months,
is_interest_free, easypay_provider_code, easypay_discount_amount
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
val paymentEasypayPrepaidDetailSql: String = """
INSERT INTO payment_easypay_prepaid_detail(
id, easypay_provider_code, amount, discount_amount
) VALUES (?, ?, ?, ?)
""".trimIndent()
val paymentBankTransferDetailSql: String = """
INSERT INTO payment_bank_transfer_detail(
id, bank_code, settlement_status
) VALUES (?, ?, ?)
""".trimIndent()
}
init {
context("결제 데이터 초기화") {
test("모든 예약에 맞춰 1:1로 결제 및 결제 상세 데이터를 생성한다.") {
val allReservations: List<ReservationWithPrice> = entityManager.createQuery(
"SELECT t.price, r.participantCount, r._id FROM ReservationEntity r JOIN ScheduleEntity s ON s._id = r.scheduleId JOIN ThemeEntity t ON t.id = s.themeId",
List::class.java
).resultList.map {
val items = it as List<*>
ReservationWithPrice(
themePrice = items[0] as Int,
participantCount = items[1] as Short,
reservationId = items[2] as Long
)
}
coroutineScope {
allReservations.chunked(10_000).forEach { reservations ->
launch(Dispatchers.IO) {
processPaymentAndDefaultDetail(reservations)
}
}
}
}
test("기존 결제 데이터에 상세 정보(계좌이체, 카드, 간편결제) 데이터를 생성한다.") {
val allPayments: List<PaymentWithMethods> = entityManager.createQuery(
"SELECT new roomescape.data.PaymentWithMethods(pd._id, p.totalAmount, p.method) FROM PaymentEntity p JOIN PaymentDetailEntity pd ON p._id = pd.paymentId",
PaymentWithMethods::class.java
).resultList
coroutineScope {
allPayments.chunked(10_000).forEach { payments ->
launch(Dispatchers.IO) {
processPaymentDetail(payments)
}
}
}
}
test("null 컴파일 에러를 피하기 위해 문자열 null로 임시 지정한 컬럼을 변경한다.") {
jdbcTemplate.execute("CREATE INDEX idx_payment_card_detail_easypay ON payment_card_detail (easypay_provider_code)")
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
jdbcTemplate.update(
"UPDATE payment_card_detail SET easypay_provider_code = ? WHERE easypay_provider_code = ?",
null,
"null"
)
}
jdbcTemplate.execute("DROP INDEX idx_payment_card_detail_easypay ON payment_card_detail")
}
}
}
private suspend fun processPaymentAndDefaultDetail(reservations: List<ReservationWithPrice>) {
val paymentBatchArgs = mutableListOf<Array<Any>>()
val detailBatchArgs = mutableListOf<Array<Any>>()
reservations.forEachIndexed { idx, reservations ->
val id = tsidFactory.next()
val totalPrice = reservations.totalPrice
paymentBatchArgs.add(
arrayOf(
id,
reservations.reservationId,
PaymentType.NORMAL.name,
randomPaymentMethod(),
randomString(length = 64),
randomString(length = 20),
totalPrice,
PaymentStatus.DONE.name,
requestedAtCache,
approvedAtCache,
)
)
if (paymentBatchArgs.size >= 1_000) {
executeBatch(paymentSql, paymentBatchArgs).also { paymentBatchArgs.clear() }
}
val suppliedAmount: Int = (totalPrice * 0.9).toInt()
val vat: Int = (totalPrice - suppliedAmount)
detailBatchArgs.add(
arrayOf(
tsidFactory.next(),
id,
suppliedAmount,
vat
)
)
if (detailBatchArgs.size >= 1_000) {
executeBatch(paymentDetailSql, detailBatchArgs).also { detailBatchArgs.clear() }
}
}
if (paymentBatchArgs.isNotEmpty()) {
executeBatch(paymentSql, paymentBatchArgs).also { paymentBatchArgs.clear() }
}
if (detailBatchArgs.isNotEmpty()) {
executeBatch(paymentDetailSql, detailBatchArgs).also { detailBatchArgs.clear() }
}
}
private suspend fun processPaymentDetail(payments: List<PaymentWithMethods>) {
val transferBatchArgs = mutableListOf<Array<Any>>()
val cardBatchArgs = mutableListOf<Array<Any>>()
val easypayPrepaidBatchArgs = mutableListOf<Array<Any>>()
payments.forEach { payment ->
val totalPrice = payment.totalPrice
val randomDiscountAmount =
if (totalPrice < 100 || Math.random() < 0.8) 0 else ((100..totalPrice).random() / 100) * 100
val amount = totalPrice - randomDiscountAmount
when (payment.method) {
PaymentMethod.TRANSFER -> {
transferBatchArgs.add(
arrayOf(
payment.id,
BankCode.entries.random().name,
settlementStatus
)
)
if (transferBatchArgs.size >= 1_000) {
executeBatch(paymentBankTransferDetailSql, transferBatchArgs).also { transferBatchArgs.clear() }
}
}
PaymentMethod.EASY_PAY -> {
if (Math.random() <= 0.7) {
cardBatchArgs.add(
arrayOf(
payment.id,
CardIssuerCode.entries.random().name,
supportedCardType.random().name,
CardOwnerType.PERSONAL.name,
amount,
randomCardNumber(),
randomApprovalNumber(),
randomInstallmentPlanMonths(amount),
true,
EasyPayCompanyCode.entries.random().name,
randomDiscountAmount
)
)
if (cardBatchArgs.size >= 1_000) {
executeBatch(paymentCardDetailSql, cardBatchArgs).also { cardBatchArgs.clear() }
}
} else {
easypayPrepaidBatchArgs.add(
arrayOf(
payment.id,
EasyPayCompanyCode.entries.random().name,
amount,
randomDiscountAmount,
)
)
if (easypayPrepaidBatchArgs.size >= 1_000) {
executeBatch(paymentEasypayPrepaidDetailSql, easypayPrepaidBatchArgs).also { easypayPrepaidBatchArgs.clear() }
}
}
}
PaymentMethod.CARD -> {
cardBatchArgs.add(
arrayOf(
payment.id,
CardIssuerCode.entries.random().name,
supportedCardType.random().name,
CardOwnerType.PERSONAL.name,
totalPrice,
randomCardNumber(),
randomApprovalNumber(),
randomInstallmentPlanMonths(totalPrice),
true,
"null",
0,
)
)
if (cardBatchArgs.size >= 1_000) {
executeBatch(paymentCardDetailSql, cardBatchArgs).also { cardBatchArgs.clear() }
}
}
else -> return@forEach
}
}
if (transferBatchArgs.isNotEmpty()) {
executeBatch(paymentBankTransferDetailSql, transferBatchArgs).also { transferBatchArgs.clear() }
}
if (cardBatchArgs.isNotEmpty()) {
executeBatch(paymentCardDetailSql, cardBatchArgs).also { cardBatchArgs.clear() }
}
if (easypayPrepaidBatchArgs.isNotEmpty()) {
executeBatch(paymentEasypayPrepaidDetailSql, easypayPrepaidBatchArgs).also { easypayPrepaidBatchArgs.clear() }
}
}
private suspend fun randomPaymentMethod(): String {
val random = Math.random()
return if (random <= 0.5) {
PaymentMethod.EASY_PAY.name
} else if (random <= 0.9) {
PaymentMethod.CARD.name
} else {
PaymentMethod.TRANSFER.name
}
}
private suspend fun randomCardNumber(): String {
return "${(10000000..99999999).random()}****${(100..999).random()}*"
}
private suspend fun randomApprovalNumber(): String {
return "${(10000000..99999999).random()}"
}
private suspend fun randomInstallmentPlanMonths(amount: Int): Int {
return if (amount < 50_000 || Math.random() < 0.9) {
0
} else {
(1..6).random()
}
}
}
fun randomKoreanName(): String {
val lastNames = listOf(
"", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""
)
return "${lastNames.random()}${if (Math.random() < 0.1) randomKoreanWords(1) else randomKoreanWords(2)}"
}
fun randomKoreanWords(length: Int = 1): String {
val words = listOf(
"", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "",
"", "", "", "", "",
)
return (1..length).joinToString("") { words.random() }
}