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
13 changes: 13 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
implementation("org.jetbrains.kotlin:kotlin-reflect")

// jwt
Expand Down Expand Up @@ -140,6 +141,17 @@ buildConfig {
"${parts[0].trim()}: ${parts[1].trim().replace("\"", "")}"
}

val languageCodesDescription =
file("src/main/resources/language.yml")
.readText()
.substringAfter("codes:")
.lines()
.filter { it.contains(":") }
.joinToString(", ") { line ->
val parts = line.split(":")
"${parts[0].trim()}: ${parts[1].trim().replace("\"", "")}"
}

val regionCodes =
file("src/main/resources/region-codes.yml")
.readText()
Expand All @@ -165,6 +177,7 @@ buildConfig {

buildConfigField("String", "AREA_CODES_DESCRIPTION", "\"\"\"$areaCodes\"\"\"")
buildConfigField("String", "CONTENT_TYPE_CODES_DESCRIPTION", "\"\"\"$contentTypeCodes\"\"\"")
buildConfigField("String", "LANGUAGE_CODES_DESCRIPTION", "\"\"\"$languageCodesDescription\"\"\"")
buildConfigField("String", "REGION_CODES_DESCRIPTION", "\"\"\"$regionCodes\"\"\"")
buildConfigField("String", "KOREA_TRAVEL_GUIDE_SYSTEM", "\"\"\"$systemPrompt\"\"\"")
buildConfigField("String", "AI_ERROR_FALLBACK", "\"\"\"$errorPrompt\"\"\"")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,32 @@ import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import org.springframework.web.client.RestTemplate
import org.springframework.web.reactive.function.server.RequestPredicates.queryParam
import org.springframework.web.util.UriComponentsBuilder
import java.net.URI

// 09.26 양현준
// 10.12 양현준
@Component
class TourApiClient(
private val restTemplate: RestTemplate,
private val objectMapper: ObjectMapper,
@Value("\${tour.api.key}") private val serviceKey: String,
@Value("\${tour.api.base-url}") private val apiUrl: String,
) {
// 요청 URL 구성
private fun buildUrl(params: TourParams): URI =
UriComponentsBuilder.fromUri(URI.create(apiUrl))
.path("/areaBasedList2")
.queryParam("serviceKey", serviceKey)
.queryParam("MobileOS", "WEB")
.queryParam("MobileApp", "KoreaTravelGuide")
.queryParam("_type", "json")
.queryParam("contentTypeId", params.contentTypeId)
.queryParam("areaCode", params.areaCode)
.queryParam("sigunguCode", params.sigunguCode)
.build()
.encode()
.toUri()

// 지역 기반 관광 정보 조회 (areaBasedList2)
fun fetchTourInfo(params: TourParams): TourResponse {
val url = buildUrl(params)
fun fetchTourInfo(
params: TourParams,
serviceSegment: String,
): TourResponse {
val url =
buildTourUri(serviceSegment, "areaBasedList2") {
queryParam("contentTypeId", params.contentTypeId)
queryParam("areaCode", params.areaCode)
queryParam("sigunguCode", params.sigunguCode)
}

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

return body
Expand All @@ -59,27 +51,21 @@ class TourApiClient(
fun fetchLocationBasedTours(
tourParams: TourParams,
locationParams: TourLocationBasedParams,
serviceSegment: String,
): TourResponse {
val url =
UriComponentsBuilder.fromUri(URI.create(apiUrl))
.path("/locationBasedList2")
.queryParam("serviceKey", serviceKey)
.queryParam("MobileOS", "WEB")
.queryParam("MobileApp", "KoreaTravelGuide")
.queryParam("_type", "json")
.queryParam("mapX", locationParams.mapX)
.queryParam("mapY", locationParams.mapY)
.queryParam("radius", locationParams.radius)
.queryParam("contentTypeId", tourParams.contentTypeId)
.queryParam("areaCode", tourParams.areaCode)
.queryParam("sigunguCode", tourParams.sigunguCode)
.build()
.encode()
.toUri()
buildTourUri(serviceSegment, "locationBasedList2") {
queryParam("mapX", locationParams.mapX)
queryParam("mapY", locationParams.mapY)
queryParam("radius", locationParams.radius)
queryParam("contentTypeId", tourParams.contentTypeId)
queryParam("areaCode", tourParams.areaCode)
queryParam("sigunguCode", tourParams.sigunguCode)
}

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

return body
Expand All @@ -89,22 +75,18 @@ class TourApiClient(
}

// 공통정보 조회 (detailCommon2)
fun fetchTourDetail(params: TourDetailParams): TourDetailResponse {
fun fetchTourDetail(
params: TourDetailParams,
serviceSegment: String,
): TourDetailResponse {
val url =
UriComponentsBuilder.fromUri(URI.create(apiUrl))
.path("/detailCommon2")
.queryParam("serviceKey", serviceKey)
.queryParam("MobileOS", "WEB")
.queryParam("MobileApp", "KoreaTravelGuide")
.queryParam("_type", "json")
.queryParam("contentId", params.contentId)
.build()
.encode()
.toUri()
buildTourUri(serviceSegment, "detailCommon2") {
queryParam("contentId", params.contentId)
}

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

return body
Expand Down Expand Up @@ -192,4 +174,22 @@ class TourApiClient(

return itemsNode.map { it }
}

private fun buildTourUri(
serviceSegment: String,
vararg pathSegments: String,
customize: UriComponentsBuilder.() -> Unit = {},
): URI =
UriComponentsBuilder.fromUri(URI.create(apiUrl))
.pathSegment(serviceSegment, *pathSegments)
.apply {
queryParam("serviceKey", serviceKey)
queryParam("MobileOS", "WEB")
queryParam("MobileApp", "KoreaTravelGuide")
queryParam("_type", "json")
customize()
}
.build()
.encode()
.toUri()
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,44 @@ class TourService(
return tourParamsParser.parse(contentTypeId, areaAndSigunguCode)
}

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

/**
* 위치 기반 관광 정보를 조회한다.
* 전달받은 언어 값을 설정 기반 서비스 세그먼트로 치환해 API 클라이언트를 호출한다.
*/
fun fetchLocationBasedTours(
tourParams: TourParams,
locationParams: TourLocationBasedParams,
languageCode: String? = null,
): TourResponse {
return tourLocationBasedUseCase.fetchLocationBasedTours(tourParams, locationParams)
val serviceSegment = languageCode?.takeIf { it.isNotBlank() } ?: DEFAULT_LANGUAGE_SEGMENT
return tourLocationBasedUseCase.fetchLocationBasedTours(tourParams, locationParams, serviceSegment)
}

/**
* 관광지 상세 정보를 조회한다.
* 언어 값을 정규화해 상세 API 호출 시 사용한다.
*/
fun fetchTourDetail(
detailParams: TourDetailParams,
languageCode: String? = null,
): TourDetailResponse {
val serviceSegment = languageCode?.takeIf { it.isNotBlank() } ?: DEFAULT_LANGUAGE_SEGMENT
return tourDetailUseCase.fetchTourDetail(detailParams, serviceSegment)
}

fun fetchTourDetail(detailParams: TourDetailParams): TourDetailResponse {
return tourDetailUseCase.fetchTourDetail(detailParams)
companion object {
private const val DEFAULT_LANGUAGE_SEGMENT = "KorService2"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ class TourAreaBasedServiceCore(
) : TourAreaBasedUseCase {
@Cacheable(
"tourAreaBased",
key = "#tourParams.contentTypeId + '_' + #tourParams.areaCode + '_' + #tourParams.sigunguCode",
key =
"#tourParams.contentTypeId + '_' + #tourParams.areaCode + '_' + #tourParams.sigunguCode + '_' + #serviceSegment",
unless = "#result == null",
)
override fun fetchAreaBasedTours(tourParams: TourParams): TourResponse {
override fun fetchAreaBasedTours(
tourParams: TourParams,
serviceSegment: String,
): TourResponse {
if (
tourParams.contentTypeId == "12" &&
tourParams.areaCode == "6" &&
Expand All @@ -26,7 +30,7 @@ class TourAreaBasedServiceCore(
return PRESET_AREA_TOUR_RESPONSE
}

return tourApiClient.fetchTourInfo(tourParams)
return tourApiClient.fetchTourInfo(tourParams, serviceSegment)
}

private companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@ import org.springframework.stereotype.Service
class TourDetailServiceCore(
private val tourApiClient: TourApiClient,
) : TourDetailUseCase {
@Cacheable("tourDetail", key = "#detailParams.contentId", unless = "#result == null")
override fun fetchTourDetail(detailParams: TourDetailParams): TourDetailResponse {
@Cacheable(
"tourDetail",
key = "#detailParams.contentId + '_' + #serviceSegment",
unless = "#result == null",
)
override fun fetchTourDetail(
detailParams: TourDetailParams,
serviceSegment: String,
): TourDetailResponse {
if (detailParams.contentId == "127974") {
return PRESET_DETAIL_RESPONSE
}

return tourApiClient.fetchTourDetail(detailParams)
return tourApiClient.fetchTourDetail(detailParams, serviceSegment)
}

private companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ class TourLocationBasedServiceCore(
@Cacheable(
"tourLocationBased",
key =
"#tourParams.contentTypeId + '_' + #tourParams.areaCode + '_' + #tourParams.sigunguCode + " +
"'_' + #locationParams.mapX + '_' + #locationParams.mapY + '_' + #locationParams.radius",
"#tourParams.contentTypeId + '_' + #tourParams.areaCode + '_' + #tourParams.sigunguCode + '_' + " +
"#locationParams.mapX + '_' + #locationParams.mapY + '_' + #locationParams.radius + '_' + " +
"#serviceSegment",
unless = "#result == null",
)
override fun fetchLocationBasedTours(
tourParams: TourParams,
locationParams: TourLocationBasedParams,
serviceSegment: String,
): TourResponse {
if (
tourParams.contentTypeId == "39" &&
Expand All @@ -35,7 +37,7 @@ class TourLocationBasedServiceCore(
return PRESET_LOCATION_BASED_RESPONSE
}

return tourApiClient.fetchLocationBasedTours(tourParams, locationParams)
return tourApiClient.fetchLocationBasedTours(tourParams, locationParams, serviceSegment)
}

private companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ import com.back.koreaTravelGuide.domain.ai.tour.dto.TourParams
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourResponse

interface TourAreaBasedUseCase {
fun fetchAreaBasedTours(tourParams: TourParams): TourResponse
fun fetchAreaBasedTours(
tourParams: TourParams,
serviceSegment: String,
): TourResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailParams
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailResponse

interface TourDetailUseCase {
fun fetchTourDetail(detailParams: TourDetailParams): TourDetailResponse
fun fetchTourDetail(
detailParams: TourDetailParams,
serviceSegment: String,
): TourDetailResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ interface TourLocationBasedUseCase {
fun fetchLocationBasedTours(
tourParams: TourParams,
locationParams: TourLocationBasedParams,
serviceSegment: String,
): TourResponse
}
4 changes: 2 additions & 2 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ weather:
tour:
api:
key: ${TOUR_API_KEY:dev-tour-api-key-placeholder}
base-url: ${TOUR_API_BASE_URL:http://apis.data.go.kr/B551011/KorService1}
base-url: ${TOUR_API_BASE_URL:http://apis.data.go.kr/B551011}


# 로깅 설정 (주니어 개발자 디버깅용)
Expand Down Expand Up @@ -197,4 +197,4 @@ custom:
jwt:
secret-key: ${CUSTOM__JWT__SECRET_KEY:dev-secret-key-for-local-testing-please-change}
access-token-expiration-minutes: ${JWT_ACCESS_TOKEN_EXPIRATION_MINUTES:60}
refresh-token-expiration-days: ${JWT_REFRESH_TOKEN_EXPIRATION_DAYS:7}
refresh-token-expiration-days: ${JWT_REFRESH_TOKEN_EXPIRATION_DAYS:7}
8 changes: 8 additions & 0 deletions src/main/resources/language.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
tour:
language:
codes:
한국어: "KorService2"
영어: "EngService2"
일본어: "JpnService2"
중국어 간체: "ChsService2"
중국어 번체: "ChtService2"
Loading