chore: 초기 데이터 추가를 위한 테스트 클래스 패키지 이동

This commit is contained in:
이상진 2025-09-28 13:16:15 +09:00
parent 1cbece032f
commit 30eb2e3b03
3 changed files with 913 additions and 913 deletions

View File

@ -0,0 +1,910 @@
package com.sangdol.data
import com.sangdol.common.persistence.IDGenerator
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.common.util.TransactionExecutionUtil
import com.sangdol.roomescape.payment.infrastructure.common.*
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
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.user.business.SIGNUP
import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity
import com.sangdol.roomescape.user.infrastructure.persistence.UserStatus
import com.sangdol.roomescape.user.web.UserContactResponse
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 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 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<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(
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<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(
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<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 = 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일차, 매일 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(
idGenerator.create(), 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 com.sangdol.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 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<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(
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.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 com.sangdol.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 = 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,
)
)
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(
idGenerator.create(),
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() }
}

View File

@ -1,4 +1,4 @@
package com.sangdol.roomescape.data package com.sangdol.data
import com.sangdol.roomescape.store.infrastructure.persistence.StoreStatus import com.sangdol.roomescape.store.infrastructure.persistence.StoreStatus
import com.sangdol.roomescape.supports.IDGenerator import com.sangdol.roomescape.supports.IDGenerator
@ -10,9 +10,9 @@ import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.random.Random import kotlin.random.Random
const val BASE_DIR = "data" const val BASE_DIR = "../data"
const val PARSED_REGION_POPULATION_FILE = "$BASE_DIR/region_population.txt" const val PARSED_REGION_POPULATION_FILE = "$BASE_DIR/region_population.txt"
const val REGION_SQL_FILE = "data/region.sql" const val REGION_SQL_FILE = "${BASE_DIR}/region.sql"
const val MIN_POPULATION_FOR_PER_STORE = 200_000 const val MIN_POPULATION_FOR_PER_STORE = 200_000
/** /**

View File

@ -1,910 +0,0 @@
//package com.sangdol.roomescape.data
//
//import com.sangdol.common.persistence.IDGenerator
//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.common.util.TransactionExecutionUtil
//import com.sangdol.roomescape.payment.infrastructure.common.*
//import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus
//import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
//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.user.business.SIGNUP
//import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity
//import com.sangdol.roomescape.user.infrastructure.persistence.UserStatus
//import com.sangdol.roomescape.user.web.UserContactResponse
//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 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 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<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(
// 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<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(
// 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<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 = 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일차, 매일 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(
// idGenerator.create(), 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 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<List<UserContactResponse>> = 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<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(
// 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.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 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(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 = 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,
// )
// )
// 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(
// idGenerator.create(),
// 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() }
//}