generated from pricelees/issue-pr-template
[#48] Tosspay mocking 서버 구현을 위한 멀티모듈 전환 #49
@ -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() }
|
||||
}
|
||||
@ -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.supports.IDGenerator
|
||||
@ -10,9 +10,9 @@ import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
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 REGION_SQL_FILE = "data/region.sql"
|
||||
const val REGION_SQL_FILE = "${BASE_DIR}/region.sql"
|
||||
const val MIN_POPULATION_FOR_PER_STORE = 200_000
|
||||
|
||||
/**
|
||||
@ -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() }
|
||||
//}
|
||||
Loading…
x
Reference in New Issue
Block a user