Skip to content

Commit 9f00930

Browse files
authored
Feat(be) 언어별 호출 추가를 위한 Tour 도메인 수정 (최종) (#133)
* fix(be) : Test코드 통과하도록 수정, Mock 테스트 추가 # Conflicts: # src/test/kotlin/com/back/koreaTravelGuide/domain/ai/tour/client/TourApiClientTest.kt * feat(be) : service의 parsing 기능 테스트 추가 * refactor(be) : 실제 api 호출 테스트 제거 * feat(be) : 언어별 호출(한, 중, 일, 영어) 추가에 따른 Client 수정 및 리팩토링 * fix(be) : 언어 별 url 구분에 따른, TOUR_API_BASE_URL 수정 * feat(be) : 서비스, 코어, 유즈케이스까지 TourLanguage 반영 * fix(be) : Test 코드 수정 * refactor(be) : language.yml을 추가해 언어 문자열을 BuildConfig로 생성, TourTool에서 사용할 수 있도록 Client와 Service 수정 * fix(be) : Test 코드 에러 수정
1 parent bc001db commit 9f00930

File tree

14 files changed

+235
-81
lines changed

14 files changed

+235
-81
lines changed

build.gradle.kts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dependencies {
3434
implementation("org.springframework.boot:spring-boot-starter-web")
3535
implementation("org.springframework.boot:spring-boot-starter-webflux")
3636
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
37+
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
3738
implementation("org.jetbrains.kotlin:kotlin-reflect")
3839

3940
// jwt
@@ -140,6 +141,17 @@ buildConfig {
140141
"${parts[0].trim()}: ${parts[1].trim().replace("\"", "")}"
141142
}
142143

144+
val languageCodesDescription =
145+
file("src/main/resources/language.yml")
146+
.readText()
147+
.substringAfter("codes:")
148+
.lines()
149+
.filter { it.contains(":") }
150+
.joinToString(", ") { line ->
151+
val parts = line.split(":")
152+
"${parts[0].trim()}: ${parts[1].trim().replace("\"", "")}"
153+
}
154+
143155
val regionCodes =
144156
file("src/main/resources/region-codes.yml")
145157
.readText()
@@ -165,6 +177,7 @@ buildConfig {
165177

166178
buildConfigField("String", "AREA_CODES_DESCRIPTION", "\"\"\"$areaCodes\"\"\"")
167179
buildConfigField("String", "CONTENT_TYPE_CODES_DESCRIPTION", "\"\"\"$contentTypeCodes\"\"\"")
180+
buildConfigField("String", "LANGUAGE_CODES_DESCRIPTION", "\"\"\"$languageCodesDescription\"\"\"")
168181
buildConfigField("String", "REGION_CODES_DESCRIPTION", "\"\"\"$regionCodes\"\"\"")
169182
buildConfigField("String", "KOREA_TRAVEL_GUIDE_SYSTEM", "\"\"\"$systemPrompt\"\"\"")
170183
buildConfigField("String", "AI_ERROR_FALLBACK", "\"\"\"$errorPrompt\"\"\"")

src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/client/TourApiClient.kt

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,40 +13,32 @@ import com.fasterxml.jackson.databind.ObjectMapper
1313
import org.springframework.beans.factory.annotation.Value
1414
import org.springframework.stereotype.Component
1515
import org.springframework.web.client.RestTemplate
16-
import org.springframework.web.reactive.function.server.RequestPredicates.queryParam
1716
import org.springframework.web.util.UriComponentsBuilder
1817
import java.net.URI
1918

20-
// 09.26 양현준
19+
// 10.12 양현준
2120
@Component
2221
class TourApiClient(
2322
private val restTemplate: RestTemplate,
2423
private val objectMapper: ObjectMapper,
2524
@Value("\${tour.api.key}") private val serviceKey: String,
2625
@Value("\${tour.api.base-url}") private val apiUrl: String,
2726
) {
28-
// 요청 URL 구성
29-
private fun buildUrl(params: TourParams): URI =
30-
UriComponentsBuilder.fromUri(URI.create(apiUrl))
31-
.path("/areaBasedList2")
32-
.queryParam("serviceKey", serviceKey)
33-
.queryParam("MobileOS", "WEB")
34-
.queryParam("MobileApp", "KoreaTravelGuide")
35-
.queryParam("_type", "json")
36-
.queryParam("contentTypeId", params.contentTypeId)
37-
.queryParam("areaCode", params.areaCode)
38-
.queryParam("sigunguCode", params.sigunguCode)
39-
.build()
40-
.encode()
41-
.toUri()
42-
4327
// 지역 기반 관광 정보 조회 (areaBasedList2)
44-
fun fetchTourInfo(params: TourParams): TourResponse {
45-
val url = buildUrl(params)
28+
fun fetchTourInfo(
29+
params: TourParams,
30+
serviceSegment: String,
31+
): TourResponse {
32+
val url =
33+
buildTourUri(serviceSegment, "areaBasedList2") {
34+
queryParam("contentTypeId", params.contentTypeId)
35+
queryParam("areaCode", params.areaCode)
36+
queryParam("sigunguCode", params.sigunguCode)
37+
}
4638

4739
val body =
4840
runCatching { restTemplate.getForObject(url, String::class.java) }
49-
.onFailure { log.error("관광 정보 조회 실패", it) }
41+
.onFailure { log.error("관광 정보 조회 실패 - serviceSegment={}", serviceSegment, it) }
5042
.getOrNull()
5143

5244
return body
@@ -59,27 +51,21 @@ class TourApiClient(
5951
fun fetchLocationBasedTours(
6052
tourParams: TourParams,
6153
locationParams: TourLocationBasedParams,
54+
serviceSegment: String,
6255
): TourResponse {
6356
val url =
64-
UriComponentsBuilder.fromUri(URI.create(apiUrl))
65-
.path("/locationBasedList2")
66-
.queryParam("serviceKey", serviceKey)
67-
.queryParam("MobileOS", "WEB")
68-
.queryParam("MobileApp", "KoreaTravelGuide")
69-
.queryParam("_type", "json")
70-
.queryParam("mapX", locationParams.mapX)
71-
.queryParam("mapY", locationParams.mapY)
72-
.queryParam("radius", locationParams.radius)
73-
.queryParam("contentTypeId", tourParams.contentTypeId)
74-
.queryParam("areaCode", tourParams.areaCode)
75-
.queryParam("sigunguCode", tourParams.sigunguCode)
76-
.build()
77-
.encode()
78-
.toUri()
57+
buildTourUri(serviceSegment, "locationBasedList2") {
58+
queryParam("mapX", locationParams.mapX)
59+
queryParam("mapY", locationParams.mapY)
60+
queryParam("radius", locationParams.radius)
61+
queryParam("contentTypeId", tourParams.contentTypeId)
62+
queryParam("areaCode", tourParams.areaCode)
63+
queryParam("sigunguCode", tourParams.sigunguCode)
64+
}
7965

8066
val body =
8167
runCatching { restTemplate.getForObject(url, String::class.java) }
82-
.onFailure { log.error("위치기반 관광 정보 조회 실패", it) }
68+
.onFailure { log.error("위치기반 관광 정보 조회 실패 - serviceSegment={}", serviceSegment, it) }
8369
.getOrNull()
8470

8571
return body
@@ -89,22 +75,18 @@ class TourApiClient(
8975
}
9076

9177
// 공통정보 조회 (detailCommon2)
92-
fun fetchTourDetail(params: TourDetailParams): TourDetailResponse {
78+
fun fetchTourDetail(
79+
params: TourDetailParams,
80+
serviceSegment: String,
81+
): TourDetailResponse {
9382
val url =
94-
UriComponentsBuilder.fromUri(URI.create(apiUrl))
95-
.path("/detailCommon2")
96-
.queryParam("serviceKey", serviceKey)
97-
.queryParam("MobileOS", "WEB")
98-
.queryParam("MobileApp", "KoreaTravelGuide")
99-
.queryParam("_type", "json")
100-
.queryParam("contentId", params.contentId)
101-
.build()
102-
.encode()
103-
.toUri()
83+
buildTourUri(serviceSegment, "detailCommon2") {
84+
queryParam("contentId", params.contentId)
85+
}
10486

10587
val body =
10688
runCatching { restTemplate.getForObject(url, String::class.java) }
107-
.onFailure { log.error("공통정보 조회 실패", it) }
89+
.onFailure { log.error("공통정보 조회 실패 - serviceSegment={}", serviceSegment, it) }
10890
.getOrNull()
10991

11092
return body
@@ -192,4 +174,22 @@ class TourApiClient(
192174

193175
return itemsNode.map { it }
194176
}
177+
178+
private fun buildTourUri(
179+
serviceSegment: String,
180+
vararg pathSegments: String,
181+
customize: UriComponentsBuilder.() -> Unit = {},
182+
): URI =
183+
UriComponentsBuilder.fromUri(URI.create(apiUrl))
184+
.pathSegment(serviceSegment, *pathSegments)
185+
.apply {
186+
queryParam("serviceKey", serviceKey)
187+
queryParam("MobileOS", "WEB")
188+
queryParam("MobileApp", "KoreaTravelGuide")
189+
queryParam("_type", "json")
190+
customize()
191+
}
192+
.build()
193+
.encode()
194+
.toUri()
195195
}

src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/TourService.kt

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,44 @@ class TourService(
2525
return tourParamsParser.parse(contentTypeId, areaAndSigunguCode)
2626
}
2727

28-
fun fetchTours(tourParams: TourParams): TourResponse {
29-
return tourAreaBasedUseCase.fetchAreaBasedTours(tourParams)
28+
/**
29+
* 지역 기반 관광 정보를 조회한다.
30+
* 언어 문자열을 설정으로 정규화해 다국어 엔드포인트에 맞춰 전달한다.
31+
*/
32+
fun fetchTours(
33+
tourParams: TourParams,
34+
languageCode: String? = null,
35+
): TourResponse {
36+
val serviceSegment = languageCode?.takeIf { it.isNotBlank() } ?: DEFAULT_LANGUAGE_SEGMENT
37+
return tourAreaBasedUseCase.fetchAreaBasedTours(tourParams, serviceSegment)
3038
}
3139

40+
/**
41+
* 위치 기반 관광 정보를 조회한다.
42+
* 전달받은 언어 값을 설정 기반 서비스 세그먼트로 치환해 API 클라이언트를 호출한다.
43+
*/
3244
fun fetchLocationBasedTours(
3345
tourParams: TourParams,
3446
locationParams: TourLocationBasedParams,
47+
languageCode: String? = null,
3548
): TourResponse {
36-
return tourLocationBasedUseCase.fetchLocationBasedTours(tourParams, locationParams)
49+
val serviceSegment = languageCode?.takeIf { it.isNotBlank() } ?: DEFAULT_LANGUAGE_SEGMENT
50+
return tourLocationBasedUseCase.fetchLocationBasedTours(tourParams, locationParams, serviceSegment)
51+
}
52+
53+
/**
54+
* 관광지 상세 정보를 조회한다.
55+
* 언어 값을 정규화해 상세 API 호출 시 사용한다.
56+
*/
57+
fun fetchTourDetail(
58+
detailParams: TourDetailParams,
59+
languageCode: String? = null,
60+
): TourDetailResponse {
61+
val serviceSegment = languageCode?.takeIf { it.isNotBlank() } ?: DEFAULT_LANGUAGE_SEGMENT
62+
return tourDetailUseCase.fetchTourDetail(detailParams, serviceSegment)
3763
}
3864

39-
fun fetchTourDetail(detailParams: TourDetailParams): TourDetailResponse {
40-
return tourDetailUseCase.fetchTourDetail(detailParams)
65+
companion object {
66+
private const val DEFAULT_LANGUAGE_SEGMENT = "KorService2"
4167
}
4268
}

src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourAreaBasedServiceCore.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ class TourAreaBasedServiceCore(
1414
) : TourAreaBasedUseCase {
1515
@Cacheable(
1616
"tourAreaBased",
17-
key = "#tourParams.contentTypeId + '_' + #tourParams.areaCode + '_' + #tourParams.sigunguCode",
17+
key =
18+
"#tourParams.contentTypeId + '_' + #tourParams.areaCode + '_' + #tourParams.sigunguCode + '_' + #serviceSegment",
1819
unless = "#result == null",
1920
)
20-
override fun fetchAreaBasedTours(tourParams: TourParams): TourResponse {
21+
override fun fetchAreaBasedTours(
22+
tourParams: TourParams,
23+
serviceSegment: String,
24+
): TourResponse {
2125
if (
2226
tourParams.contentTypeId == "12" &&
2327
tourParams.areaCode == "6" &&
@@ -26,7 +30,7 @@ class TourAreaBasedServiceCore(
2630
return PRESET_AREA_TOUR_RESPONSE
2731
}
2832

29-
return tourApiClient.fetchTourInfo(tourParams)
33+
return tourApiClient.fetchTourInfo(tourParams, serviceSegment)
3034
}
3135

3236
private companion object {

src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourDetailServiceCore.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,20 @@ import org.springframework.stereotype.Service
1212
class TourDetailServiceCore(
1313
private val tourApiClient: TourApiClient,
1414
) : TourDetailUseCase {
15-
@Cacheable("tourDetail", key = "#detailParams.contentId", unless = "#result == null")
16-
override fun fetchTourDetail(detailParams: TourDetailParams): TourDetailResponse {
15+
@Cacheable(
16+
"tourDetail",
17+
key = "#detailParams.contentId + '_' + #serviceSegment",
18+
unless = "#result == null",
19+
)
20+
override fun fetchTourDetail(
21+
detailParams: TourDetailParams,
22+
serviceSegment: String,
23+
): TourDetailResponse {
1724
if (detailParams.contentId == "127974") {
1825
return PRESET_DETAIL_RESPONSE
1926
}
2027

21-
return tourApiClient.fetchTourDetail(detailParams)
28+
return tourApiClient.fetchTourDetail(detailParams, serviceSegment)
2229
}
2330

2431
private companion object {

src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourLocationBasedServiceCore.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ class TourLocationBasedServiceCore(
1616
@Cacheable(
1717
"tourLocationBased",
1818
key =
19-
"#tourParams.contentTypeId + '_' + #tourParams.areaCode + '_' + #tourParams.sigunguCode + " +
20-
"'_' + #locationParams.mapX + '_' + #locationParams.mapY + '_' + #locationParams.radius",
19+
"#tourParams.contentTypeId + '_' + #tourParams.areaCode + '_' + #tourParams.sigunguCode + '_' + " +
20+
"#locationParams.mapX + '_' + #locationParams.mapY + '_' + #locationParams.radius + '_' + " +
21+
"#serviceSegment",
2122
unless = "#result == null",
2223
)
2324
override fun fetchLocationBasedTours(
2425
tourParams: TourParams,
2526
locationParams: TourLocationBasedParams,
27+
serviceSegment: String,
2628
): TourResponse {
2729
if (
2830
tourParams.contentTypeId == "39" &&
@@ -35,7 +37,7 @@ class TourLocationBasedServiceCore(
3537
return PRESET_LOCATION_BASED_RESPONSE
3638
}
3739

38-
return tourApiClient.fetchLocationBasedTours(tourParams, locationParams)
40+
return tourApiClient.fetchLocationBasedTours(tourParams, locationParams, serviceSegment)
3941
}
4042

4143
private companion object {

src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourAreaBasedUseCase.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ import com.back.koreaTravelGuide.domain.ai.tour.dto.TourParams
44
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourResponse
55

66
interface TourAreaBasedUseCase {
7-
fun fetchAreaBasedTours(tourParams: TourParams): TourResponse
7+
fun fetchAreaBasedTours(
8+
tourParams: TourParams,
9+
serviceSegment: String,
10+
): TourResponse
811
}

src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourDetailUseCase.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailParams
44
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailResponse
55

66
interface TourDetailUseCase {
7-
fun fetchTourDetail(detailParams: TourDetailParams): TourDetailResponse
7+
fun fetchTourDetail(
8+
detailParams: TourDetailParams,
9+
serviceSegment: String,
10+
): TourDetailResponse
811
}

src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourLocationBasedUseCase.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ interface TourLocationBasedUseCase {
88
fun fetchLocationBasedTours(
99
tourParams: TourParams,
1010
locationParams: TourLocationBasedParams,
11+
serviceSegment: String,
1112
): TourResponse
1213
}

src/main/resources/application.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ weather:
150150
tour:
151151
api:
152152
key: ${TOUR_API_KEY:dev-tour-api-key-placeholder}
153-
base-url: ${TOUR_API_BASE_URL:http://apis.data.go.kr/B551011/KorService1}
153+
base-url: ${TOUR_API_BASE_URL:http://apis.data.go.kr/B551011}
154154

155155

156156
# 로깅 설정 (주니어 개발자 디버깅용)
@@ -197,4 +197,4 @@ custom:
197197
jwt:
198198
secret-key: ${CUSTOM__JWT__SECRET_KEY:dev-secret-key-for-local-testing-please-change}
199199
access-token-expiration-minutes: ${JWT_ACCESS_TOKEN_EXPIRATION_MINUTES:60}
200-
refresh-token-expiration-days: ${JWT_REFRESH_TOKEN_EXPIRATION_DAYS:7}
200+
refresh-token-expiration-days: ${JWT_REFRESH_TOKEN_EXPIRATION_DAYS:7}

0 commit comments

Comments
 (0)