package com.sangdol.data import com.sangdol.common.persistence.IDGenerator import com.sangdol.common.persistence.TransactionExecutionUtil import com.sangdol.common.utils.KoreaDateTime import com.sangdol.roomescape.admin.infrastructure.persistence.AdminEntity import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType import com.sangdol.roomescape.payment.business.domain.BankCode import com.sangdol.roomescape.payment.business.domain.CardIssuerCode import com.sangdol.roomescape.payment.business.domain.CardOwnerType import com.sangdol.roomescape.payment.business.domain.CardType import com.sangdol.roomescape.payment.business.domain.EasyPayCompanyCode import com.sangdol.roomescape.payment.business.domain.PaymentMethod import com.sangdol.roomescape.payment.business.domain.PaymentStatus import com.sangdol.roomescape.payment.business.domain.PaymentType import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus import com.sangdol.roomescape.store.infrastructure.persistence.StoreEntity import com.sangdol.roomescape.supports.AdminFixture import com.sangdol.roomescape.supports.FunSpecSpringbootTest import com.sangdol.roomescape.supports.randomPhoneNumber import com.sangdol.roomescape.supports.randomString import com.sangdol.roomescape.theme.infrastructure.persistence.Difficulty import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity import com.sangdol.roomescape.user.business.SIGNUP import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity import com.sangdol.roomescape.user.infrastructure.persistence.UserStatus import com.sangdol.roomescape.user.dto.UserContactResponse import io.kotest.core.test.TestCaseOrder import jakarta.persistence.EntityManager import kotlinx.coroutines.* 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 java.sql.Timestamp import java.time.Instant import java.time.LocalDateTime import java.time.LocalTime import java.time.ZoneId @ActiveProfiles("test", "data") 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 idGenerator: IDGenerator 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>) { 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 = 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 stores: List = transactionExecutionUtil.withNewTransaction(isReadOnly = true) { entityManager.createQuery( "SELECT s FROM StoreEntity s", StoreEntity::class.java ).resultList }!! transactionExecutionUtil.withNewTransaction(isReadOnly = false) { stores.forEach { store -> // StoreManager 1명 생성 val storeManager = AdminFixture.create( account = store.name, name = randomKoreanName(), phone = randomPhoneNumber(), type = AdminType.STORE, storeId = store.id, 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 = "${store.name}-${permissionLevel.ordinal}${index}", name = randomKoreanName(), phone = randomPhoneNumber(), type = AdminType.STORE, storeId = store.id, 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 = 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>() repeat(500) { i -> val randomDay = if (i <= 9) (7..30).random() else (30..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( idGenerator.create(), 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 = 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 = 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 = Instant.now() entityManager.merge(user) } } } jdbcTemplate.execute("DROP INDEX idx_users__phone ON users") } test("회원 상태 변경 이력 저장") { val userId: List = 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) { 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>() val now = LocalDateTime.now() userIds.forEach { userId -> batchArgs.add( arrayOf( idGenerator.create(), 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) { 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>() repeat(chunkSize) { val id: Long = idGenerator.create() 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일차, 매일 최대 10개의 일정을 모든 매장에 생성") { val stores: List> = getStoreWithManagers() val themes: List = getThemes() val maxScheduleCountPerDay = 10 val startTime = LocalTime.of(10, 0) val themeWithTimes: Map> = themes.associateWith { theme -> val times = mutableListOf() val themeAvailableMinutes = theme.availableMinutes var lastTime = startTime while (times.size <= maxScheduleCountPerDay && lastTime.hour in (10..23)) { times.add(lastTime) lastTime = lastTime.plusMinutes(themeAvailableMinutes + 10L) } times } coroutineScope { stores.map { store -> launch(Dispatchers.IO) { processTheme(store, themeWithTimes) } } } } test("내일 ~ 일주일 뒤 까지의 일정 생성") { // val stores: List> = getStoreWithManagers() // val availableThemes: List = transactionExecutionUtil.withNewTransaction(isReadOnly = true) { // entityManager.createQuery( // "SELECT t FROM ThemeEntity t WHERE t.isActive = true AND t.createdAt >", ThemeEntity::class.java // ).resultList // }!!.take(10) coroutineScope { val jobs = (1..100).map { i -> launch(Dispatchers.IO) { val threadName = Thread.currentThread().name println("[$i] 시작: $threadName") delay(1) println("[$i] 완료: $threadName") } } } } } } private suspend fun processTheme( store: Pair, themeWithTimes: Map> ) { 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>() val status = ScheduleStatus.RESERVED.name themeWithTimes.forEach { (theme, times) -> val themeCreatedAt = theme.createdAt (1..3).forEach { val themeCreatedDateTime = themeCreatedAt.atZone(ZoneId.systemDefault()) val themeCreatedDate = themeCreatedDateTime.toLocalDate().plusDays(it.toLong()) val themeCreatedTime = themeCreatedDateTime.toLocalTime() times.forEach { time -> val storeId = store.first val storeAdminId = store.second batchArgs.add( arrayOf( idGenerator.create(), storeId, theme.id, themeCreatedDate, time, status, storeAdminId, storeAdminId, themeCreatedTime.plusHours(1), themeCreatedTime.plusHours(1) ) ) if (batchArgs.size >= 300) { executeBatch(sql, batchArgs).also { batchArgs.clear() } } } } } if (batchArgs.isNotEmpty()) { executeBatch(sql, batchArgs).also { batchArgs.clear() } } } private fun getStoreWithManagers(): List> { 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 { return transactionExecutionUtil.withNewTransaction(isReadOnly = true) { entityManager.createQuery( "SELECT t FROM ThemeEntity t", ThemeEntity::class.java ).resultList }!! } } /** * 아래의 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 = 500 val chunkedSchedules: List> = entityManager.createQuery( "SELECT new com.sangdol.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> = entityManager.createQuery( "SELECT new com.sangdol.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, schedules: List ) { 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>() val createdAt = LocalDateTime.now() schedules.forEachIndexed { idx, schedule -> val user: UserContactResponse = users[idx % users.size] batchArgs.add( arrayOf( idGenerator.create(), 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.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(KoreaDateTime.nowWithOffset().toLocalDateTime()) val approvedAtCache: Timestamp = Timestamp.valueOf(KoreaDateTime.nowWithOffset().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 = 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(500).forEach { reservations -> launch(Dispatchers.IO) { processPaymentAndDefaultDetail(reservations) } } } } test("기존 결제 데이터에 상세 정보(계좌이체, 카드, 간편결제) 데이터를 생성한다.") { val allPayments: List = entityManager.createQuery( "SELECT new com.sangdol.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(500).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) { val paymentBatchArgs = mutableListOf>() val detailBatchArgs = mutableListOf>() reservations.forEachIndexed { idx, reservations -> val id = idGenerator.create() 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, ) ) val suppliedAmount: Int = (totalPrice * 0.9).toInt() val vat: Int = (totalPrice - suppliedAmount) detailBatchArgs.add( arrayOf( idGenerator.create(), id, suppliedAmount, vat ) ) } if (paymentBatchArgs.isNotEmpty()) { executeBatch(paymentSql, paymentBatchArgs).also { paymentBatchArgs.clear() } } if (detailBatchArgs.isNotEmpty()) { executeBatch(paymentDetailSql, detailBatchArgs).also { detailBatchArgs.clear() } } } private suspend fun processPaymentDetail(payments: List) { val transferBatchArgs = mutableListOf>() val cardBatchArgs = mutableListOf>() val easypayPrepaidBatchArgs = mutableListOf>() 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 ) ) } 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 ) ) } else { easypayPrepaidBatchArgs.add( arrayOf( payment.id, EasyPayCompanyCode.entries.random().name, amount, randomDiscountAmount, ) ) } } 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, ) ) } 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() } }