Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package hs.kr.entrydsm.domain.examcode.values

data class DistanceGroup(
val applicationType: String,
val distanceCode: String,
val examCodeInfoList: List<ExamCodeInfo>
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package hs.kr.entrydsm.application.domain.examcode

import hs.kr.entrydsm.application.global.web.KakaoProperties
import hs.kr.entrydsm.domain.examcode.interfaces.BaseLocationContract
import org.springframework.stereotype.Component

Expand All @@ -10,7 +11,7 @@ import org.springframework.stereotype.Component
* @since 2025.08.26
*/
@Component
class ExamCodePersistenceAdapter(
class BaseLocationAdapter(
private val kakaoProperties: KakaoProperties,
) : BaseLocationContract {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
package hs.kr.entrydsm.application.domain.examcode.usecase

import hs.kr.entrydsm.domain.application.interfaces.ApplicationContract
import hs.kr.entrydsm.domain.examcode.factories.ExamCodeInfoFactory
import hs.kr.entrydsm.domain.examcode.interfaces.GrantExamCodesContract
import hs.kr.entrydsm.domain.examcode.policies.GrantDistanceBasedExamCodePolicy
import hs.kr.entrydsm.domain.examcode.specifications.GeneralApplicationSpec
import hs.kr.entrydsm.domain.examcode.specifications.SpecialApplicationSpec
import hs.kr.entrydsm.domain.examcode.values.ExamCodeInfo
import hs.kr.entrydsm.domain.status.interfaces.StatusContract
import hs.kr.entrydsm.application.global.annotation.usecase.UseCase
import hs.kr.entrydsm.domain.application.aggregates.Application
import hs.kr.entrydsm.domain.application.values.ApplicationType
import hs.kr.entrydsm.domain.examcode.exceptions.ExamCodeException
import hs.kr.entrydsm.domain.examcode.interfaces.BaseLocationContract
import hs.kr.entrydsm.domain.examcode.interfaces.KakaoGeocodeContract
import hs.kr.entrydsm.domain.examcode.values.DistanceGroup
import hs.kr.entrydsm.application.domain.examcode.util.DistanceUtil
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope

/**
* 1차 전형에 합격한 학생들에게 수험번호를 부여하는 도메인 서비스입니다.
*
* @property applicationContract 애플리케이션 관련 데이터 소스
* @property statusContract 학생 상태 관련 데이터 소스
* @property examCodeInfoFactory 수험번호 정보 생성 팩토리
* @property grantDistanceBasedExamCodePolicy 거리 기반 수험번호 부여 정책
*/
@UseCase
class GrantExamCodesUseCase(
private val applicationContract: ApplicationContract,
private val statusContract: StatusContract,
private val examCodeInfoFactory: ExamCodeInfoFactory,
private val grantDistanceBasedExamCodePolicy: GrantDistanceBasedExamCodePolicy
private val kakaoGeocodeContract: KakaoGeocodeContract,
private val distanceUtil: DistanceUtil,
private val baseLocationContract: BaseLocationContract,
) : GrantExamCodesContract {

companion object {
Expand All @@ -35,40 +31,72 @@ class GrantExamCodesUseCase(
private const val SPECIAL_EXAM_CODE_PREFIX = "02"
}

/**
* 1차 전형 합격자에게 수험번호를 부여하고 저장합니다.
*
* 1. 1차 전형에 합격한 모든 지원서를 조회합니다.
* 2. 각 지원서에 대한 수험번호 정보([hs.kr.entrydsm.domain.examcode.values.ExamCodeInfo])를 생성합니다.
* 3. 지원자를 일반전형과 특별전형으로 분류합니다.
* 4. 각 전형별로 거리 기반 정책을 적용하여 수험번호를 부여합니다.
* 5. 생성된 수험번호를 저장합니다.
*/
override suspend fun execute() {
val applications = applicationContract.queryAllFirstRoundPassedApplication()
val examCodeInfos = coroutineScope {
applications.map { application ->
async { examCodeInfoFactory.create(application) }
}.map { it.await() }
val allFirstRoundPassedApplication = applicationContract.queryAllFirstRoundPassedApplication()
val examCodeInfos = collectDistanceInfo(allFirstRoundPassedApplication)

val generalExamInfos = examCodeInfos.filter { it.applicationType == ApplicationType.COMMON }
val specialExamInfos = examCodeInfos.filter {
it.applicationType == ApplicationType.SOCIAL || it.applicationType == ApplicationType.MEISTER
}

val generalSpec = GeneralApplicationSpec()
val specialSpec = SpecialApplicationSpec()
assignExamCodes(generalExamInfos, GENERAL_EXAM_CODE_PREFIX)
assignExamCodes(specialExamInfos, SPECIAL_EXAM_CODE_PREFIX)

val generalExamInfos = examCodeInfos.filter(generalSpec::isSatisfiedBy)
val specialExamInfos = examCodeInfos.filter(specialSpec::isSatisfiedBy)
saveExamCodes(examCodeInfos)
}

grantDistanceBasedExamCodePolicy.apply(generalExamInfos, GENERAL_EXAM_CODE_PREFIX)
grantDistanceBasedExamCodePolicy.apply(specialExamInfos, SPECIAL_EXAM_CODE_PREFIX)
private suspend fun collectDistanceInfo(applications: List<Application>): List<ExamCodeInfo> = coroutineScope {
applications.map { application ->
async {
val address = application.streetAddress as String
val coordinate = kakaoGeocodeContract.geocode(address)
?: throw ExamCodeException.failedGeocodeConversion(address)
val baseLat = baseLocationContract.baseLat
val baseLon = baseLocationContract.baseLon
val userLat = coordinate.first
val userLon = coordinate.second
val distance = distanceUtil.haversine(baseLat, baseLon, userLat, userLon)
ExamCodeInfo(
receiptCode = application.receiptCode,
applicationType = application.applicationType!!, // 전형 유형
distance = distance
)
}
}.map { it.await() }
}

saveExamCodes(examCodeInfos)
private fun assignExamCodes(examCodeInfos: List<ExamCodeInfo>, applicationType: String) {
val sortedByDistance = examCodeInfos.sortedByDescending { it.distance }

val distanceGroups = createDistanceGroups(sortedByDistance, applicationType)

distanceGroups.forEach { group ->
assignNumbersInGroup(group)
}
}

/**
* 부여된 수험번호를 학생의 상태 정보에 업데이트합니다.
*
* @param examCodeInfos 수험번호 정보 리스트
*/
private fun createDistanceGroups(sortedInfos: List<ExamCodeInfo>, applicationType: String): List<DistanceGroup> {
val groups = mutableListOf<DistanceGroup>()
val uniqueDistances = sortedInfos.map { it.distance }.distinct()
uniqueDistances.forEachIndexed { index, distance ->
val distanceCode = String.format("%03d", index + 1)
val applicationsInGroup = sortedInfos.filter { it.distance == distance }.toMutableList()
groups.add(DistanceGroup(applicationType, distanceCode, applicationsInGroup))
}
return groups
}


private fun assignNumbersInGroup(distanceGroup: DistanceGroup) {
distanceGroup.examCodeInfoList.forEach { examCodeInfo ->
val receiptCode = String.format("%03d", examCodeInfo.receiptCode)
val examCode = "${distanceGroup.applicationType}${distanceGroup.distanceCode}$receiptCode"
examCodeInfo.examCode = examCode
}
}


private suspend fun saveExamCodes(examCodeInfos: List<ExamCodeInfo>) {
examCodeInfos.forEach { info ->
info.examCode?.let { examCode ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package hs.kr.entrydsm.domain.examcode.util
package hs.kr.entrydsm.application.domain.examcode.util

import hs.kr.entrydsm.global.annotation.service.Service
import hs.kr.entrydsm.global.annotation.service.type.ServiceType
import org.springframework.stereotype.Component
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.pow
Expand All @@ -12,7 +11,7 @@ import kotlin.math.sqrt
/**
* 두 지점 간의 거리를 계산하는 유틸리티 클래스입니다.
*/
@Service(name = "DistanceUtil", type = ServiceType.APPLICATION_SERVICE)
@Component
class DistanceUtil {
companion object {
/** 지구의 반지름 (미터) */
Expand All @@ -36,4 +35,4 @@ class DistanceUtil {
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
return (R * c).roundToInt()
}
}
}
Loading