package com.sangdol.data import com.sangdol.roomescape.store.infrastructure.persistence.StoreStatus import com.sangdol.roomescape.supports.IDGenerator import com.sangdol.roomescape.supports.randomPhoneNumber import org.apache.poi.xssf.usermodel.XSSFWorkbook import java.io.File import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter import kotlin.random.Random const val BASE_DIR = "../data" const val PARSED_REGION_POPULATION_FILE = "$BASE_DIR/region_population.txt" const val REGION_SQL_FILE = "${BASE_DIR}/region.sql" const val MIN_POPULATION_FOR_PER_STORE = 200_000 /** * 행안부 202508 인구 동향 데이터 사용( /data/population.xlsx ) */ class PopulationDataSqlParser() { val regionCodePattern = Regex("^[0-9]{10}$") // 인구 데이터를 이용하여 지역 정보 SQL 파일로 변환하고, 추가로 $MIN_POPULATION_FOR_PER_STORE 이상의 시/군/구는 매장 데이터 생성을 위해 따로 분류한다. fun createParsedRegionPopulationFiles() { val populationXlsx = XSSFWorkbook(File("${BASE_DIR}/population.xlsx")) val sheet = populationXlsx.getSheetAt(0) val allRegion = mutableListOf>() val regionsMoreThanMinPopulation = mutableListOf>() sheet.rowIterator().forEach { row -> val regionCode = row.getCell(0)?.stringCellValue ?: return@forEach if (regionCodePattern.matches(regionCode).not()) { return@forEach } val sidoCode = regionCode.substring(0, 2) val sigunguCode = regionCode.substring(2, 5) val population = row.getCell(2).stringCellValue.replace(",", "") if (Regex("^[0-9]+$").matches(population).not()) { return@forEach } val regionName = row.getCell(1).stringCellValue if (!regionName.trim().contains(" ")) { return@forEach } val parts = regionName.split(" ") if (parts.size < 2) { return@forEach } val sidoName = parts[0].trim() val sigunguName = parts[1].trim() val populationInt = population.toInt() if (populationInt <= 0) { return@forEach } if (populationInt >= MIN_POPULATION_FOR_PER_STORE) { regionsMoreThanMinPopulation.add( listOf( regionCode, sidoCode, sigunguCode, sidoName, sigunguName, population ) ) } allRegion.add(listOf(regionCode, sidoCode, sigunguCode, sidoName, sigunguName)) } regionsMoreThanMinPopulation.filter { val sidoName = it[3] val sigunguName = it[4] val sameSigungu = allRegion.filter { r -> r[3] == sidoName && r[4] == sigunguName } !((sameSigungu.size > 1) && sameSigungu.minByOrNull { r -> r[2].toInt() }!![2] != it[2]) }.mapIndexed { idx, values -> "${values[0]}, ${values[1]}, ${values[2]}, ${values[3]}, ${values[4]}, ${values[5]}" }.joinToString(separator = "\n").also { File(PARSED_REGION_POPULATION_FILE).writeText(it) } allRegion.distinctBy { it[1] to it[2] }.filter { val sidoName = it[3] val sigunguName = it[4] val sameSigungu = allRegion.filter { r -> r[3] == sidoName && r[4] == sigunguName } !((sameSigungu.size > 1) && sameSigungu.minByOrNull { r -> r[2].toInt() }!![2] != it[2]) }.joinToString( prefix = "INSERT INTO region(code, sido_code, sigungu_code, sido_name, sigungu_name) VALUES ", separator = ",\n" ) { region -> "('${region[0]}', '${region[1]}', '${region[2]}', '${region[3]}', '${region[4]}')" }.also { File(REGION_SQL_FILE).writeText("${it};") } } } /** * PopulationDataSqlParser에서 전처리된 지역 + 인구 정보를 이용하여 Store 초기 데이터 SQL 생성 */ class StoreDataInitializer { val positiveWords = listOf( "사랑", "행복", "희망", "감사", "기쁨", "미소", "축복", "선물", "평화", "열정", "미래", "자유", "도전", "지혜", "행운" ) fun createStoreDataSqlFile(creatableAdminIds: List): File { val regions = initializeRegionWithStoreCount() val usedStoreName = mutableListOf() val usedBusinessRegNums = mutableListOf() val storeSqlRows = mutableListOf() val storeDataRows = mutableListOf() val storeIds = mutableListOf() regions.forEachIndexed { idx, region -> for (i in 0..region.storeCount) { var address: String var storeName: String do { val randomPositiveWord = positiveWords.random() storeName = "${parseSigunguName(region.sigunguName)}${randomPositiveWord}점" address = "${region.sidoName} ${region.sigunguName} ${randomPositiveWord}${Random.nextInt(1, 10)}길 ${Random.nextInt(1, 100)}" } while (usedStoreName.contains(storeName)) usedStoreName.add(storeName) val contact = randomPhoneNumber() var businessRegNum: String do { businessRegNum = generateBusinessRegNum() } while (usedBusinessRegNums.contains(businessRegNum)) usedBusinessRegNums.add(businessRegNum) val createdAt = randomLocalDateTime() val updatedAt = createdAt val id: Long = IDGenerator.create().also { storeIds.add(it) } val createdBy = creatableAdminIds.random() storeSqlRows.add( "(${id}, '$storeName', '$address', '$contact', '$businessRegNum', '${region.regionCode}', '${StoreStatus.ACTIVE.name}', '$createdAt', '${createdBy}', '$updatedAt', '${createdBy}')" ) storeDataRows.add( "$id, $storeName, $address, $contact, $businessRegNum, ${region.regionCode}, ${StoreStatus.ACTIVE.name}, $createdAt, $createdBy, $updatedAt, $createdBy" ) } } File("$BASE_DIR/store_data.txt").also { if (it.exists()) { it.delete() } }.writeText( storeDataRows.joinToString("\n") ) return File("$BASE_DIR/store_data.sql").also { if (it.exists()) { it.delete() } StringBuilder("INSERT INTO store (id, name, address, contact, business_reg_num, region_code, status, created_at, created_by, updated_at, updated_by) VALUES ") .append(storeSqlRows.joinToString(",\n")) .append(";") .toString() .also { sql -> it.writeText(sql) } } } } private fun parseSigunguName(sigunguName: String): String { if (sigunguName.length == 2) { return sigunguName } return sigunguName.substring(0, sigunguName.length - 1) } private fun randomLocalDateTime(): String { val year = Random.nextInt(2020, 2024) val month = Random.nextInt(1, 13) val day = when (month) { 2 -> Random.nextInt(1, 29) else -> Random.nextInt(1, 31) } val hour = Random.nextInt(9, 19) val minute = Random.nextInt(0, 60) val second = Random.nextInt(0, 60) return LocalDateTime.of(year, month, day, hour, minute, second) .atZone(ZoneId.systemDefault()) .toOffsetDateTime() .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX")) } private fun generateBusinessRegNum(): String { val part1 = Random.nextInt(100, 1000) val part2 = Random.nextInt(10, 100) val part3 = Random.nextInt(10000, 100000) return "$part1-$part2-$part3" } private fun initializeRegionWithStoreCount(): List { return File(PARSED_REGION_POPULATION_FILE).also { if (it.exists().not()) { PopulationDataSqlParser().createParsedRegionPopulationFiles() } }.readText().lines().map { val parts = it.split(", ") val regionCode = parts[0] val sidoName = parts[3] val sigunguName = parts[4] val storeCount: Int = (parts[5].toInt() / MIN_POPULATION_FOR_PER_STORE) RegionWithStoreCount( regionCode = regionCode, sidoName = sidoName, sigunguName = sigunguName, storeCount = storeCount ) } } data class RegionWithStoreCount( val regionCode: String, val sidoName: String, val sigunguName: String, var storeCount: Int )