Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package noweekend.client.mcp

import java.lang.RuntimeException

class McpNotRespondingException() : RuntimeException()
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package noweekend.client.mcp.recommend

import noweekend.client.mcp.recommend.model.AiGenerateVacationRequest
import noweekend.client.mcp.recommend.model.AiGenerateVacationResponse
import noweekend.client.mcp.recommend.model.BridgeVacationPeriod
import noweekend.client.mcp.recommend.model.SandwichRequest
import noweekend.client.mcp.recommend.model.SandwichResponse
import noweekend.client.mcp.recommend.model.TagRequest
import noweekend.client.mcp.recommend.model.WeatherRequest
import noweekend.core.domain.tag.TagRecommendation
Expand Down Expand Up @@ -43,5 +45,12 @@ interface RecommendApi {
consumes = [MediaType.APPLICATION_JSON_VALUE],
method = [RequestMethod.POST],
)
fun getSandwich(@RequestBody request: SandwichRequest): SandwichResponse
fun getSandwich(@RequestBody request: SandwichRequest): List<BridgeVacationPeriod>

@RequestMapping(
value = ["/generate-vacation"],
consumes = [MediaType.APPLICATION_JSON_VALUE],
method = [RequestMethod.POST],
)
fun generateVacation(@RequestBody request: AiGenerateVacationRequest): AiGenerateVacationResponse
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package noweekend.client.mcp.recommend

import feign.FeignException
import noweekend.client.mcp.McpNotRespondingException
import noweekend.client.mcp.recommend.model.AiGenerateVacationRequest
import noweekend.client.mcp.recommend.model.AiGenerateVacationResponse
import noweekend.client.mcp.recommend.model.BridgeVacationPeriod
import noweekend.client.mcp.recommend.model.SandwichRequest
import noweekend.client.mcp.recommend.model.SandwichResponse
import noweekend.client.mcp.recommend.model.TagRequest
import noweekend.client.mcp.recommend.model.WeatherRequest
import noweekend.client.mcp.recommend.model.toRequestType
Expand Down Expand Up @@ -79,11 +82,23 @@ class RecommendClient(
)
}

fun getSandwich(request: SandwichRequest): SandwichResponse? {
fun getSandwich(request: SandwichRequest): List<BridgeVacationPeriod> {
return try {
api.getSandwich(request)
} catch (e: FeignException) {
log.warn("[getSandwich] FeignException, empty 반환. msg=${e.message}")
log.warn("[getSandwich] FeignException occurred. msg=${e.message}")
throw McpNotRespondingException()
} catch (e: Exception) {
log.error("[getSandwich] Unexpected exception occurred.", e)
throw McpNotRespondingException()
}
}

fun generateVacation(request: AiGenerateVacationRequest): AiGenerateVacationResponse? {
return try {
api.generateVacation(request)
} catch (e: FeignException) {
log.warn("[generateVacation] FeignException, empty 반환. msg=${e.message}")
return null
} catch (e: Exception) {
log.error("[getSandwich] 예기치 못한 예외, empty 반환.", e)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package noweekend.client.mcp.recommend.model

import java.time.LocalDate

data class AiGenerateVacationRequest(
val days: Int,

val travelStyleOptionLabels: List<String>,
val chosenTravelStyleLabel: String,

val activityTypeOptionLabels: List<String>,
val chosenActivityTypeLabel: String,

val restPreferenceOptionLabels: List<String>,
val chosenRestPreferenceLabel: String,

val leisurePreferenceOptionLabels: List<String>,
val chosenLeisurePreferenceLabel: String,

val birthDate: LocalDate,
val selectedTags: List<String>,
val unselectedTags: List<String>,
val upcomingHolidays: List<String>,
)

data class AiGenerateVacationResponse(
val title: String,
val content: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,22 @@ import java.time.LocalDate
data class SandwichRequest(
val birthDay: LocalDate,
val holidays: List<LocalDate>,
val remainingAnnualLeave: Int,
val weekends: List<LocalDate>,
)

data class BridgeVacationPeriod(
val startDate: LocalDate,
val endDate: LocalDate,
)

data class SandwichResponse(
val startDate: LocalDate,
val endDate: LocalDate,
val useAnnualLeave: Int,
val totalVacationDays: Int,
)

data class SandwichApiResponse(
val responses: List<SandwichResponse>,
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package noweekend.core.api.controller.v1

import noweekend.client.mcp.recommend.model.SandwichApiResponse
import noweekend.client.mcp.recommend.model.SandwichResponse
import noweekend.core.api.controller.v1.docs.RecommendControllerDocs
import noweekend.core.api.controller.v1.request.GenerateVacationRequest
Expand Down Expand Up @@ -51,13 +52,25 @@ class RecommendController(
@GetMapping("/sandwich")
override fun getSandwich(
@CurrentUserId userId: String,
): ApiResponse<SandwichResponse> {
val mockData = SandwichResponse(
startDate = LocalDate.now(),
endDate = LocalDate.now().plusDays(3),
): ApiResponse<SandwichApiResponse> {
// return ApiResponse.success(recommendService.getSandwich(userId))
val mockResponse = SandwichApiResponse(
responses = listOf(
SandwichResponse(
startDate = LocalDate.of(2025, 8, 14),
endDate = LocalDate.of(2025, 8, 16),
useAnnualLeave = 1,
totalVacationDays = 3,
),
SandwichResponse(
startDate = LocalDate.of(2025, 9, 11),
endDate = LocalDate.of(2025, 9, 15),
useAnnualLeave = 2,
totalVacationDays = 5,
),
),
)
return ApiResponse.success(mockData)
// return ApiResponse.success(recommendService.getSandwich(userId))
return ApiResponse.success(mockResponse)
}

@PostMapping("/generate-vacation")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.ExampleObject
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.tags.Tag
import noweekend.client.mcp.recommend.model.SandwichResponse
import noweekend.client.mcp.recommend.model.SandwichApiResponse
import noweekend.core.api.controller.v1.request.GenerateVacationRequest
import noweekend.core.api.controller.v1.response.AiGenerateVacationApiResponse
import noweekend.core.api.controller.v1.response.WeatherResponse
Expand Down Expand Up @@ -249,15 +249,15 @@ interface RecommendControllerDocs {
): ApiResponse<TagRecommendations>

@Operation(
summary = "샌드위치 연휴 추천",
summary = "유저의 남은 연차와 올해 남은 공휴일/주말로 샌드위치 휴가(bridge vacation) 추천",
description = """
사용자 생일 및 공휴일 정보를 기반으로
샌드위치 연차(연휴 시작일·종료일) 기간을 추천합니다.
유저의 남은 연차, 올해 남은 공휴일/주말을 바탕으로, 연속으로 쉴 수 있는 휴가(샌드위치 휴가) 구간을 추천합니다.
각 휴가 구간별 실제 사용 연차 일수와 전체 휴가 일수도 반환됩니다.
""",
responses = [
SwaggerApiResponse(
responseCode = "200",
description = "샌드위치 연차 기간 반환 성공",
description = "샌드위치 휴가 추천 성공",
content = [
Content(
mediaType = "application/json",
Expand All @@ -269,8 +269,20 @@ interface RecommendControllerDocs {
{
"result": "SUCCESS",
"data": {
"startDate": "2025-07-14",
"endDate": "2025-07-16"
"responses": [
{
"startDate": "2025-10-02",
"endDate": "2025-10-09",
"useAnnualLeave": 1,
"totalVacationDays": 8
},
{
"startDate": "2025-12-24",
"endDate": "2025-12-28",
"useAnnualLeave": 1,
"totalVacationDays": 5
}
]
},
"error": null
}
Expand All @@ -289,66 +301,14 @@ interface RecommendControllerDocs {
schema = Schema(implementation = ApiResponse::class),
examples = [
ExampleObject(
name = "잘못된 요청 예시",
name = "에러 예시",
value = """
{
"result": "ERROR",
"data": null,
"error": {
"code": "INVALID_PARAMETER",
"message": "잘못된 요청입니다.",
"data": {}
}
}
""",
),
],
),
],
),
SwaggerApiResponse(
responseCode = "400",
description = "생일 정보 없음",
content = [
Content(
mediaType = "application/json",
schema = Schema(implementation = ApiResponse::class),
examples = [
ExampleObject(
name = "생일 정보 없음 에러 예시",
value = """
{
"result": "ERROR",
"data": null,
"error": {
"code": "USER_BIRTH_DAY_NOT_FOUND",
"message": "사용자가 생일을 갖고있지 않습니다. 생일 먼저 추가해주세요.",
"data": {}
}
}
""",
),
],
),
],
),
SwaggerApiResponse(
responseCode = "504",
description = "MCP 추천 서버 무응답",
content = [
Content(
mediaType = "application/json",
schema = Schema(implementation = ApiResponse::class),
examples = [
ExampleObject(
name = "MCP 서버 타임아웃 에러 예시",
value = """
{
"result": "ERROR",
"data": null,
"error": {
"code": "MCP_SERVER_SANDWICH_ERROR",
"message": "MCP 추천 서버의 응답이 없습니다. 잠시 후 다시 시도해주세요.",
"message": "사용자 정보를 찾을 수 없습니다.",
"data": {}
}
}
Expand All @@ -362,7 +322,7 @@ interface RecommendControllerDocs {
)
fun getSandwich(
@Parameter(hidden = true) @CurrentUserId userId: String,
): ApiResponse<SandwichResponse>
): ApiResponse<SandwichApiResponse>

@Operation(
summary = "AI 기반 여행 일정 생성",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,22 @@ import java.time.LocalDate
@Service
class HolidayServiceImpl(
private val holidayClient: HolidayClient,
private val holidayRepository: HolidayRepository,
private val holidayWriter: HolidayWriter,
private val holidayReader: HolidayReader,
) : HolidayService {

override fun getMonthHolidays(
year: Int,
month: Int,
): List<HolidayResponse> {
return holidayRepository.findAllByYear(year)
.asSequence()
.filter { it.month == month }
.sortedBy { it.day }
return holidayReader.findMonthHolidays(year, month)
.map { HolidayResponse.from(it) }
.toList()
}

override fun getRemainingHolidays(): List<HolidayResponse> {
val today = LocalDate.now()
val year = today.year

return holidayRepository.findAllByYear(year)
.asSequence()
.map { it to LocalDate.of(it.year, it.month, it.day) }
.filter { (_, date) -> date.isAfter(today) || date.isEqual(today) }
.sortedBy { (_, date) -> date }
.map { (entity, _) -> HolidayResponse.from(entity) }
.toList()
return holidayReader.findRemainingHolidays(today)
.map { HolidayResponse.from(it) }
}

override fun updateHolidays(year: Int) {
Expand All @@ -45,15 +34,12 @@ class HolidayServiceImpl(
}

private fun syncHolidays(year: Int): List<Holiday> {
// 1) DB에 이미 저장된 공휴일 불러오기
val saved = holidayRepository.findAllByYear(year)
val saved = holidayReader.findAllByYear(year)

// 2) 기존에 저장된 키 집합 생성 ((month, day, content) + dayOfWeekKor)
val existing = saved
.map { Triple(it.month, it.day, it.content) to it.dayOfWeekKor }
.toSet()

// 3) 외부 API 호출 → Domain 객체 리스트
val fetched = (1..12).flatMap { month ->
holidayClient
.getHolidays(HolidayRequest(year, month))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package noweekend.core.domain.recommend

import noweekend.client.mcp.recommend.model.SandwichResponse
import noweekend.client.mcp.recommend.model.SandwichApiResponse
import noweekend.core.api.controller.v1.request.GenerateVacationRequest
import noweekend.core.api.controller.v1.response.AiGenerateVacationApiResponse
import noweekend.core.api.controller.v1.response.WeatherResponse
import noweekend.core.domain.tag.TagRecommendations

interface RecommendService {
fun getWeatherRecommend(userId: String): WeatherResponse
fun getTagRecommend(userId: String): TagRecommendations
fun getTagRecommendOnlyNew(userId: String): TagRecommendations
fun getSandwich(userId: String): SandwichResponse
fun getSandwich(userId: String): SandwichApiResponse
fun generateVacation(userId: String, request: GenerateVacationRequest): AiGenerateVacationApiResponse
}
Loading