From 9a89448b48ac38d14b89b50ede27d8e9d889b120 Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:27:34 +0900 Subject: [PATCH 01/17] =?UTF-8?q?refactor=20(=20#32=20)=20:=20KakaoGeocode?= =?UTF-8?q?Client=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/web/client/KakaoGeocodeClient.kt | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/client/KakaoGeocodeClient.kt diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/client/KakaoGeocodeClient.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/client/KakaoGeocodeClient.kt new file mode 100644 index 00000000..edf30f16 --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/client/KakaoGeocodeClient.kt @@ -0,0 +1,49 @@ +package hs.kr.entrydsm.application.global.web.client + +import hs.kr.entrydsm.application.global.web.KakaoProperties +import hs.kr.entrydsm.domain.examcode.interfaces.KakaoGeocodeContract +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.annotation.service.type.ServiceType +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.reactive.awaitSingle +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.bodyToMono + +/** + * 카카오 주소 -> 좌표 변환 API를 사용해 주소를 위경도로 변환하는 Client 입니다. + * + * @author chaedohun + * @since 2025.08.26 + */ +@Service(name = "KakaoGeocodeClient", type = ServiceType.APPLICATION_SERVICE) +class KakaoGeocodeClient( + private val builder: WebClient.Builder, + private val kakaoBaseProperties: KakaoProperties, +) : KakaoGeocodeContract { + + /** + * 주소를 위경도로 변환합니다. + * + * @param address 변환할 주소 + * @return 변환된 위경도 + */ + override suspend fun geocode(address: String): Pair? = + coroutineScope { + val res = + webClient.get() + .uri { it.queryParam("query", address).build() } + .retrieve().bodyToMono>().awaitSingle() + + @Suppress("UNCHECKED_CAST") + val docs = res["documents"] as? List> ?: return@coroutineScope null + val first = docs.firstOrNull() ?: return@coroutineScope null + val y = (first["y"] as String).toDouble() + val x = (first["x"] as String).toDouble() + y to x + } + + private val webClient = builder + .baseUrl(kakaoBaseProperties.url) + .defaultHeader("Authorization", "KakaoAK ${kakaoBaseProperties.restKey}") + .build() +} \ No newline at end of file From e648b47855a2a352654f9b15d3a1a489f465a13b Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:27:46 +0900 Subject: [PATCH 02/17] =?UTF-8?q?refactor=20(=20#32=20)=20:=20KakaoPropert?= =?UTF-8?q?ies=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{domain/examcode => global/web}/KakaoProperties.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/{domain/examcode => global/web}/KakaoProperties.kt (92%) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/KakaoProperties.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/KakaoProperties.kt similarity index 92% rename from casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/KakaoProperties.kt rename to casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/KakaoProperties.kt index 74f7d6fe..605b88f0 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/KakaoProperties.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/KakaoProperties.kt @@ -1,4 +1,4 @@ -package hs.kr.entrydsm.application.domain.examcode +package hs.kr.entrydsm.application.global.web import org.springframework.boot.context.properties.ConfigurationProperties @@ -30,4 +30,4 @@ data class KakaoProperties( * 카카오 Geocode API URL */ val url: String -) +) \ No newline at end of file From 70ed9daa41ac34a88054ba614fbbae3ca58c15ee Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:27:56 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat=20(=20#32=20)=20:=20DistanceGroup=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hs/kr/entrydsm/domain/examcode/util/DistanceGroup.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/util/DistanceGroup.kt diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/util/DistanceGroup.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/util/DistanceGroup.kt new file mode 100644 index 00000000..7bebe527 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/util/DistanceGroup.kt @@ -0,0 +1,9 @@ +package hs.kr.entrydsm.domain.examcode.util + +import hs.kr.entrydsm.domain.examcode.values.ExamCodeInfo + +data class DistanceGroup( + val applicationType: String, + val distanceCode: String, + val examCodeInfoList: List +) From 31bbb333d67847f2199b8b2a1d41736296409ef9 Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:28:24 +0900 Subject: [PATCH 04/17] =?UTF-8?q?refactor=20(=20#32=20)=20:=20ExamCodePers?= =?UTF-8?q?istenceAdapter=20->=20BaseLocationAdapter=EB=A1=9C=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ExamCodePersistenceAdapter.kt => BaseLocationAdapter.kt} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/{ExamCodePersistenceAdapter.kt => BaseLocationAdapter.kt} (89%) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/ExamCodePersistenceAdapter.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/BaseLocationAdapter.kt similarity index 89% rename from casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/ExamCodePersistenceAdapter.kt rename to casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/BaseLocationAdapter.kt index 346bb67d..1f439def 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/ExamCodePersistenceAdapter.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/BaseLocationAdapter.kt @@ -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 @@ -10,7 +11,7 @@ import org.springframework.stereotype.Component * @since 2025.08.26 */ @Component -class ExamCodePersistenceAdapter( +class BaseLocationAdapter( private val kakaoProperties: KakaoProperties, ) : BaseLocationContract { From e25054f9b6f807ff38fb644c569fe36974ecd4de Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:28:33 +0900 Subject: [PATCH 05/17] =?UTF-8?q?chore=20(=20#32=20)=20:=20ExamCodeFactory?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../examcode/factories/ExamCodeFactory.kt | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/factories/ExamCodeFactory.kt diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/factories/ExamCodeFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/factories/ExamCodeFactory.kt deleted file mode 100644 index fdd95820..00000000 --- a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/factories/ExamCodeFactory.kt +++ /dev/null @@ -1,35 +0,0 @@ -package hs.kr.entrydsm.domain.examcode.factories - -import hs.kr.entrydsm.global.annotation.factory.Factory -import hs.kr.entrydsm.global.annotation.factory.type.Complexity - -/** - * 수험번호를 생성하는 팩토리입니다. - * - * @author chaedohun - * @since 2025.08.26 - */ -@Factory( - context = "examCode", - complexity = Complexity.HIGH, - cache = false -) -class ExamCodeFactory { - - /** - * 수험번호를 생성합니다. - * - * @param applicationType 전형 구분 코드 - * @param distanceCode 거리 코드 - * @param receiptCode 접수 번호 - * @return 생성된 수험번호 - */ - fun create( - applicationType: String, - distanceCode: String, - receiptCode: Long - ): String { - val formattedReceiptCode = String.format("%03d", receiptCode) - return "$applicationType$distanceCode$formattedReceiptCode" - } -} From 46472ecd867beba4d28814583bd42c463bf5d41f Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:28:38 +0900 Subject: [PATCH 06/17] =?UTF-8?q?chore=20(=20#32=20)=20:=20ExamCodeInfoFac?= =?UTF-8?q?tory=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../examcode/factories/ExamCodeInfoFactory.kt | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/factories/ExamCodeInfoFactory.kt diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/factories/ExamCodeInfoFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/factories/ExamCodeInfoFactory.kt deleted file mode 100644 index 1e613799..00000000 --- a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/factories/ExamCodeInfoFactory.kt +++ /dev/null @@ -1,55 +0,0 @@ -package hs.kr.entrydsm.domain.examcode.factories - -import hs.kr.entrydsm.domain.application.aggregates.Application -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.util.DistanceUtil -import hs.kr.entrydsm.domain.examcode.values.ExamCodeInfo -import hs.kr.entrydsm.global.annotation.factory.Factory -import hs.kr.entrydsm.global.annotation.factory.type.Complexity - -/** - * 수험번호 정보를 생성하는 클래스입니다. - * - * @property kakaoGeocodeContract 카카오 지오코드 API - * @property distanceUtil 거리 계산 유틸리티 - * @property baseLocationContract 기준 위치 정보 - * - * @author chaedohun - * @since 2025.08.26 - */ -@Factory( - context = "examCode", - complexity = Complexity.HIGH, - cache = false -) -class ExamCodeInfoFactory( - private val kakaoGeocodeContract: KakaoGeocodeContract, - private val distanceUtil: DistanceUtil, - private val baseLocationContract: BaseLocationContract -) { - - /** - * 지원서 정보를 바탕으로 수험번호 정보를 생성합니다. - * - * @param application 지원서 - * @return 생성된 수험번호 정보 - * @throws ExamCodeException.failedGeocodeConversion 주소 변환에 실패한 경우 - */ - suspend fun create(application: Application): ExamCodeInfo { - val address = application.streetAddress as String - val (userLat, userLon) = kakaoGeocodeContract.geocode(address) - ?: throw ExamCodeException.failedGeocodeConversion(address) - - val distance = distanceUtil.haversine( - baseLocationContract.baseLat, baseLocationContract.baseLon, userLat, userLon - ) - - return ExamCodeInfo( - receiptCode = application.receiptCode, - applicationType = application.applicationType!!, - distance = distance - ) - } -} From 7160317cdb1bef2874e59d493c25b23a855cfe0c Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:28:43 +0900 Subject: [PATCH 07/17] =?UTF-8?q?chore=20(=20#32=20)=20:=20GeneralApplicat?= =?UTF-8?q?ionSpec=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../specifications/GeneralApplicationSpec.kt | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/specifications/GeneralApplicationSpec.kt diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/specifications/GeneralApplicationSpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/specifications/GeneralApplicationSpec.kt deleted file mode 100644 index 7ca92dbf..00000000 --- a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/specifications/GeneralApplicationSpec.kt +++ /dev/null @@ -1,31 +0,0 @@ -package hs.kr.entrydsm.domain.examcode.specifications - -import hs.kr.entrydsm.domain.application.values.ApplicationType -import hs.kr.entrydsm.domain.examcode.values.ExamCodeInfo -import hs.kr.entrydsm.global.annotation.specification.Specification -import hs.kr.entrydsm.global.annotation.specification.type.Priority - -/** - * 일반전형 지원자를 판별하는 규칙을 구현하는 클래스입니다. - * - * @author chaedohun - * @since 2025.08.26 - */ -@Specification( - name = "GeneralApplication", - description = "일반전형 지원자 분류", - domain = "수험번호", - priority = Priority.NORMAL -) -class GeneralApplicationSpec { - - /** - * 지원자가 일반전형인지 확인합니다. - * - * @param info 수험번호 정보 - * @return 일반전형인 경우 true - */ - fun isSatisfiedBy(info: ExamCodeInfo): Boolean { - return info.applicationType == ApplicationType.COMMON - } -} From 3917a28a1bd7c3c4af1f8795770ee1a4a8e87123 Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:28:47 +0900 Subject: [PATCH 08/17] =?UTF-8?q?chore=20(=20#32=20)=20:=20GrantDistanceBa?= =?UTF-8?q?sedExamCodePolicy=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GrantDistanceBasedExamCodePolicy.kt | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/policies/GrantDistanceBasedExamCodePolicy.kt diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/policies/GrantDistanceBasedExamCodePolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/policies/GrantDistanceBasedExamCodePolicy.kt deleted file mode 100644 index c22f715d..00000000 --- a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/policies/GrantDistanceBasedExamCodePolicy.kt +++ /dev/null @@ -1,49 +0,0 @@ -package hs.kr.entrydsm.domain.examcode.policies - -import hs.kr.entrydsm.domain.examcode.factories.ExamCodeFactory -import hs.kr.entrydsm.domain.examcode.values.ExamCodeInfo -import hs.kr.entrydsm.global.annotation.policy.Policy -import hs.kr.entrydsm.global.annotation.policy.type.Scope - -/** - * 지원자의 거리를 기준으로 수험번호를 부여하는 정책을 구현하는 클래스입니다. - * - * @property examCodeFactory 수험번호 생성 팩토리 - * - * @author chaedohun - * @since 2025.08.26 - */ -@Policy( - name = "GrantDistanceBasedExamCode", - description = "거리 기반 수험번호 부여 정책", - domain = "examCode", - scope = Scope.DOMAIN -) -class GrantDistanceBasedExamCodePolicy( - private val examCodeFactory: ExamCodeFactory -) { - - /** - * 주어진 수험번호 정보 리스트에 거리 기반 정책을 적용하여 수험번호를 부여합니다. - * - * @param examCodeInfos 수험번호 정보 리스트 - * @param applicationTypeCode 전형 구분 코드 - */ - fun apply(examCodeInfos: List, applicationTypeCode: String) { - val sortedByDistance = examCodeInfos.sortedByDescending { it.distance } - val uniqueDistances = sortedByDistance.map { it.distance }.distinct() - - uniqueDistances.forEachIndexed { index, distance -> - val distanceCode = String.format("%03d", index + 1) - val infosInGroup = sortedByDistance.filter { it.distance == distance } - - infosInGroup.forEach { info -> - info.examCode = examCodeFactory.create( - applicationType = applicationTypeCode, - distanceCode = distanceCode, - receiptCode = info.receiptCode - ) - } - } - } -} From 2367d5ab2460dc3f4d300ed707bbad91c5fea557 Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:28:57 +0900 Subject: [PATCH 09/17] =?UTF-8?q?chore=20(=20#32=20)=20:=20KakaoGeocodeCli?= =?UTF-8?q?ent=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/web/KakaoGeocodeClient.kt | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/KakaoGeocodeClient.kt diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/KakaoGeocodeClient.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/KakaoGeocodeClient.kt deleted file mode 100644 index b99d20c6..00000000 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/KakaoGeocodeClient.kt +++ /dev/null @@ -1,49 +0,0 @@ -package hs.kr.entrydsm.application.global.web - -import hs.kr.entrydsm.application.domain.examcode.KakaoProperties -import hs.kr.entrydsm.domain.examcode.interfaces.KakaoGeocodeContract -import hs.kr.entrydsm.global.annotation.service.Service -import hs.kr.entrydsm.global.annotation.service.type.ServiceType -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.reactive.awaitSingle -import org.springframework.web.reactive.function.client.WebClient -import org.springframework.web.reactive.function.client.bodyToMono - -/** - * 카카오 주소 -> 좌표 변환 API를 사용해 주소를 위경도로 변환하는 Client 입니다. - * - * @author chaedohun - * @since 2025.08.26 - */ -@Service(name = "KakaoGeocodeClient", type = ServiceType.APPLICATION_SERVICE) -class KakaoGeocodeClient( - private val builder: WebClient.Builder, - private val kakaoBaseProperties: KakaoProperties, -) : KakaoGeocodeContract { - - /** - * 주소를 위경도로 변환합니다. - * - * @param address 변환할 주소 - * @return 변환된 위경도 - */ - override suspend fun geocode(address: String): Pair? = - coroutineScope { - val res = - webClient.get() - .uri { it.queryParam("query", address).build() } - .retrieve().bodyToMono>().awaitSingle() - - @Suppress("UNCHECKED_CAST") - val docs = res["documents"] as? List> ?: return@coroutineScope null - val first = docs.firstOrNull() ?: return@coroutineScope null - val y = (first["y"] as String).toDouble() - val x = (first["x"] as String).toDouble() - y to x - } - - private val webClient = builder - .baseUrl(kakaoBaseProperties.url) - .defaultHeader("Authorization", "KakaoAK ${kakaoBaseProperties.restKey}") - .build() -} From 22bdb206a81776c68f699f2307f0473bfe461906 Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:29:07 +0900 Subject: [PATCH 10/17] =?UTF-8?q?chore=20(=20#32=20)=20:=20SpecialApplicat?= =?UTF-8?q?ionSpec=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../specifications/SpecialApplicationSpec.kt | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/specifications/SpecialApplicationSpec.kt diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/specifications/SpecialApplicationSpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/specifications/SpecialApplicationSpec.kt deleted file mode 100644 index 71824d42..00000000 --- a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/specifications/SpecialApplicationSpec.kt +++ /dev/null @@ -1,32 +0,0 @@ -package hs.kr.entrydsm.domain.examcode.specifications - -import hs.kr.entrydsm.domain.application.values.ApplicationType -import hs.kr.entrydsm.domain.examcode.values.ExamCodeInfo -import hs.kr.entrydsm.global.annotation.specification.Specification -import hs.kr.entrydsm.global.annotation.specification.type.Priority - -/** - * 특별전형 지원자를 판별을 구현하는 클래스입니다. - * - * @author chaedohun - * @since 2025.08.26 - */ -@Specification( - name = "SpecialApplication", - description = "특별전형 지원자 분류", - domain = "수험번호", - priority = Priority.NORMAL -) -class SpecialApplicationSpec { - - /** - * 지원자가 특별전형인지 확인합니다. - * - * @param info 수험번호 정보 - * @return 특별전형인 경우 true - */ - fun isSatisfiedBy(info: ExamCodeInfo): Boolean { - return info.applicationType == ApplicationType.SOCIAL || - info.applicationType == ApplicationType.MEISTER - } -} From 6d3520a113a35819e2b13c58f2a8c359c225ce6c Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:29:18 +0900 Subject: [PATCH 11/17] refactor ( #32 ) : GrantExamCodesUseCase --- .../examcode/usecase/GrantExamCodesUseCase.kt | 108 +++++++++++------- 1 file changed, 68 insertions(+), 40 deletions(-) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt index f136b33b..2ac9e81f 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt @@ -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.util.DistanceGroup +import hs.kr.entrydsm.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 { @@ -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): List = 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, 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, applicationType: String): List { + val groups = mutableListOf() + 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) { examCodeInfos.forEach { info -> info.examCode?.let { examCode -> From 47e8bb782ec96f1ff285fb659593057f76cb5066 Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:29:28 +0900 Subject: [PATCH 12/17] =?UTF-8?q?refactor=20(=20#32=20)=20:=20PdfConfig=20?= =?UTF-8?q?method=20naming=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/global/document/pdf/config/PdfConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/document/pdf/config/PdfConfig.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/document/pdf/config/PdfConfig.kt index 11eb3f78..0b64ea99 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/document/pdf/config/PdfConfig.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/document/pdf/config/PdfConfig.kt @@ -9,7 +9,7 @@ import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver @Configuration class PdfConfig { @Bean - fun templateEngine(): TemplateEngine { + fun pdfTemplateEngine(): TemplateEngine { val templateResolver = ClassLoaderTemplateResolver().apply { prefix = "classpath:/templates/" suffix = ".html" From c7b032ebf4ad7d086070e53edfba223558f73408 Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:29:57 +0900 Subject: [PATCH 13/17] =?UTF-8?q?refactor=20(=20#32=20)=20:=20DistanceGrou?= =?UTF-8?q?p=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/examcode/{util => values}/DistanceGroup.kt | 6 ++---- .../domain/examcode/usecase/GrantExamCodesUseCase.kt | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) rename casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/{util => values}/DistanceGroup.kt (56%) diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/util/DistanceGroup.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/values/DistanceGroup.kt similarity index 56% rename from casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/util/DistanceGroup.kt rename to casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/values/DistanceGroup.kt index 7bebe527..a90841d9 100644 --- a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/util/DistanceGroup.kt +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/values/DistanceGroup.kt @@ -1,9 +1,7 @@ -package hs.kr.entrydsm.domain.examcode.util - -import hs.kr.entrydsm.domain.examcode.values.ExamCodeInfo +package hs.kr.entrydsm.domain.examcode.values data class DistanceGroup( val applicationType: String, val distanceCode: String, val examCodeInfoList: List -) +) \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt index 2ac9e81f..5fd554e4 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt @@ -10,7 +10,7 @@ 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.util.DistanceGroup +import hs.kr.entrydsm.domain.examcode.values.DistanceGroup import hs.kr.entrydsm.domain.examcode.util.DistanceUtil import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope From 9570dcd115762281c94d3116adddbe729ef48739 Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:34:06 +0900 Subject: [PATCH 14/17] =?UTF-8?q?refactor=20(=20#32=20)=20:=20DistanceUtil?= =?UTF-8?q?=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/examcode/usecase/GrantExamCodesUseCase.kt | 2 +- .../application}/domain/examcode/util/DistanceUtil.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename {casper-application-domain/src/main/kotlin/hs/kr/entrydsm => casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application}/domain/examcode/util/DistanceUtil.kt (95%) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt index 5fd554e4..50b35033 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/usecase/GrantExamCodesUseCase.kt @@ -11,7 +11,7 @@ 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.domain.examcode.util.DistanceUtil +import hs.kr.entrydsm.application.domain.examcode.util.DistanceUtil import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/util/DistanceUtil.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/util/DistanceUtil.kt similarity index 95% rename from casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/util/DistanceUtil.kt rename to casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/util/DistanceUtil.kt index 18e645c8..bd54c9ba 100644 --- a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/examcode/util/DistanceUtil.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/util/DistanceUtil.kt @@ -1,4 +1,4 @@ -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 @@ -36,4 +36,4 @@ class DistanceUtil { val c = 2 * atan2(sqrt(a), sqrt(1 - a)) return (R * c).roundToInt() } -} +} \ No newline at end of file From dc37c9fa4208ef5226a0709880bfbe5e98cd5bd9 Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:34:21 +0900 Subject: [PATCH 15/17] =?UTF-8?q?refactor=20(=20#32=20)=20:=20KakaoGeocode?= =?UTF-8?q?Client=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/global/web/client/KakaoGeocodeClient.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/client/KakaoGeocodeClient.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/client/KakaoGeocodeClient.kt index edf30f16..8a8b7fd8 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/client/KakaoGeocodeClient.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/client/KakaoGeocodeClient.kt @@ -6,6 +6,7 @@ import hs.kr.entrydsm.global.annotation.service.Service import hs.kr.entrydsm.global.annotation.service.type.ServiceType import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.reactive.awaitSingle +import org.springframework.stereotype.Component import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.bodyToMono @@ -15,7 +16,7 @@ import org.springframework.web.reactive.function.client.bodyToMono * @author chaedohun * @since 2025.08.26 */ -@Service(name = "KakaoGeocodeClient", type = ServiceType.APPLICATION_SERVICE) +@Component class KakaoGeocodeClient( private val builder: WebClient.Builder, private val kakaoBaseProperties: KakaoProperties, From 164c7acffd1ebb0a442e7e93f01ea3089528604b Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:34:38 +0900 Subject: [PATCH 16/17] =?UTF-8?q?chore=20(=20#32=20)=20:=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/global/web/client/KakaoGeocodeClient.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/client/KakaoGeocodeClient.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/client/KakaoGeocodeClient.kt index 8a8b7fd8..b51543fb 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/client/KakaoGeocodeClient.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/web/client/KakaoGeocodeClient.kt @@ -2,8 +2,6 @@ package hs.kr.entrydsm.application.global.web.client import hs.kr.entrydsm.application.global.web.KakaoProperties import hs.kr.entrydsm.domain.examcode.interfaces.KakaoGeocodeContract -import hs.kr.entrydsm.global.annotation.service.Service -import hs.kr.entrydsm.global.annotation.service.type.ServiceType import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.reactive.awaitSingle import org.springframework.stereotype.Component From 3bdca122f646f13163c0e001bf5ba8059129d0bc Mon Sep 17 00:00:00 2001 From: coehgns Date: Thu, 28 Aug 2025 14:35:26 +0900 Subject: [PATCH 17/17] =?UTF-8?q?refactor=20(=20#32=20)=20:=20DistanceUtil?= =?UTF-8?q?=20@Component=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/domain/examcode/util/DistanceUtil.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/util/DistanceUtil.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/util/DistanceUtil.kt index bd54c9ba..420f56aa 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/util/DistanceUtil.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/examcode/util/DistanceUtil.kt @@ -1,7 +1,6 @@ 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 @@ -12,7 +11,7 @@ import kotlin.math.sqrt /** * 두 지점 간의 거리를 계산하는 유틸리티 클래스입니다. */ -@Service(name = "DistanceUtil", type = ServiceType.APPLICATION_SERVICE) +@Component class DistanceUtil { companion object { /** 지구의 반지름 (미터) */