diff --git a/noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/RecommendApi.kt b/noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/RecommendApi.kt index 3983a00..23878c5 100644 --- a/noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/RecommendApi.kt +++ b/noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/RecommendApi.kt @@ -2,8 +2,6 @@ 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.TagRequest import noweekend.client.mcp.recommend.model.WeatherRequest import noweekend.core.domain.tag.TagRecommendation @@ -40,13 +38,6 @@ interface RecommendApi { ) fun getTagOnlyNew(@RequestBody request: TagRequest): List - @RequestMapping( - value = ["/getSandwich"], - consumes = [MediaType.APPLICATION_JSON_VALUE], - method = [RequestMethod.POST], - ) - fun getSandwich(@RequestBody request: SandwichRequest): List - @RequestMapping( value = ["/generate-vacation"], consumes = [MediaType.APPLICATION_JSON_VALUE], diff --git a/noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/RecommendClient.kt b/noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/RecommendClient.kt index 928b778..382326e 100644 --- a/noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/RecommendClient.kt +++ b/noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/RecommendClient.kt @@ -1,11 +1,8 @@ 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.TagRequest import noweekend.client.mcp.recommend.model.WeatherRequest import noweekend.client.mcp.recommend.model.toRequestType @@ -82,18 +79,6 @@ class RecommendClient( ) } - fun getSandwich(request: SandwichRequest): List { - return try { - api.getSandwich(request) - } catch (e: FeignException) { - 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) diff --git a/noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/model/Sandwich.kt b/noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/model/Sandwich.kt deleted file mode 100644 index f22e4ca..0000000 --- a/noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/model/Sandwich.kt +++ /dev/null @@ -1,26 +0,0 @@ -package noweekend.client.mcp.recommend.model - -import java.time.LocalDate - -data class SandwichRequest( - val birthDay: LocalDate, - val holidays: List, - val remainingAnnualLeave: Int, - val weekends: List, -) - -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, -) diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/TempController.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/TempController.kt index 8b50556..a199623 100644 --- a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/TempController.kt +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/TempController.kt @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.tags.Tag import noweekend.core.domain.auth.TestUserService import noweekend.core.domain.auth.UserWithToken +import noweekend.core.domain.recommend.RecommendService import noweekend.core.support.response.ApiResponse import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController @@ -15,6 +16,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse @RestController class TempController( private val testUserService: TestUserService, + private val recommendService: RecommendService, ) { @Operation( diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/RecommendController.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/RecommendController.kt index 92aca9b..a0087b1 100644 --- a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/RecommendController.kt +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/RecommendController.kt @@ -1,10 +1,9 @@ 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 import noweekend.core.api.controller.v1.response.AiGenerateVacationApiResponse +import noweekend.core.api.controller.v1.response.SandwichApiResponse import noweekend.core.api.controller.v1.response.WeatherResponse import noweekend.core.api.security.annotations.CurrentUserId import noweekend.core.domain.IconStyle @@ -16,7 +15,6 @@ import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -import java.time.LocalDate @RestController @RequestMapping("/api/v1/recommend") @@ -50,27 +48,9 @@ class RecommendController( } @GetMapping("/sandwich") - override fun getSandwich( - @CurrentUserId userId: String, - ): ApiResponse { -// 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(mockResponse) + override fun getSandwich(): ApiResponse { + val sandwichApiResponse = recommendService.getSandwich() + return ApiResponse.success(sandwichApiResponse) } @PostMapping("/generate-vacation") diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/docs/RecommendControllerDocs.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/docs/RecommendControllerDocs.kt index 73e8416..2222030 100644 --- a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/docs/RecommendControllerDocs.kt +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/docs/RecommendControllerDocs.kt @@ -6,9 +6,9 @@ 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.SandwichApiResponse import noweekend.core.api.controller.v1.request.GenerateVacationRequest import noweekend.core.api.controller.v1.response.AiGenerateVacationApiResponse +import noweekend.core.api.controller.v1.response.SandwichApiResponse import noweekend.core.api.controller.v1.response.WeatherResponse import noweekend.core.api.security.annotations.CurrentUserId import noweekend.core.domain.tag.TagRecommendations @@ -320,9 +320,7 @@ interface RecommendControllerDocs { ), ], ) - fun getSandwich( - @Parameter(hidden = true) @CurrentUserId userId: String, - ): ApiResponse + fun getSandwich(): ApiResponse @Operation( summary = "AI 기반 여행 일정 생성", diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/response/SandwichResponse.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/response/SandwichResponse.kt new file mode 100644 index 0000000..c0ba0d4 --- /dev/null +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/response/SandwichResponse.kt @@ -0,0 +1,14 @@ +package noweekend.core.api.controller.v1.response + +import java.time.LocalDate + +data class SandwichResponse( + val startDate: LocalDate, + val endDate: LocalDate, + val useAnnualLeave: Int, + val totalVacationDays: Int, +) + +data class SandwichApiResponse( + val responses: List, +) diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/recommend/RecommendService.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/recommend/RecommendService.kt index 4f1728f..d79ff41 100644 --- a/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/recommend/RecommendService.kt +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/recommend/RecommendService.kt @@ -1,8 +1,8 @@ package noweekend.core.domain.recommend -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.SandwichApiResponse import noweekend.core.api.controller.v1.response.WeatherResponse import noweekend.core.domain.tag.TagRecommendations @@ -10,6 +10,6 @@ interface RecommendService { fun getWeatherRecommend(userId: String): WeatherResponse fun getTagRecommend(userId: String): TagRecommendations fun getTagRecommendOnlyNew(userId: String): TagRecommendations - fun getSandwich(userId: String): SandwichApiResponse + fun getSandwich(): SandwichApiResponse fun generateVacation(userId: String, request: GenerateVacationRequest): AiGenerateVacationApiResponse } diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/recommend/RecommendServiceImpl.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/recommend/RecommendServiceImpl.kt index adb221e..a46f3bb 100644 --- a/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/recommend/RecommendServiceImpl.kt +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/recommend/RecommendServiceImpl.kt @@ -1,22 +1,20 @@ package noweekend.core.domain.recommend -import noweekend.client.mcp.McpNotRespondingException import noweekend.client.mcp.recommend.RecommendClient import noweekend.client.mcp.recommend.model.AiGenerateVacationRequest -import noweekend.client.mcp.recommend.model.SandwichApiResponse -import noweekend.client.mcp.recommend.model.SandwichRequest -import noweekend.client.mcp.recommend.model.SandwichResponse import noweekend.client.mcp.recommend.model.WeatherRequest import noweekend.core.api.controller.v1.request.GenerateVacationRequest import noweekend.core.api.controller.v1.response.AiGenerateVacationApiResponse +import noweekend.core.api.controller.v1.response.SandwichApiResponse +import noweekend.core.api.controller.v1.response.SandwichResponse import noweekend.core.api.controller.v1.response.WeatherResponse import noweekend.core.domain.ActivityType import noweekend.core.domain.IconStyle import noweekend.core.domain.LeisurePreference import noweekend.core.domain.RestPreference import noweekend.core.domain.TravelStyle -import noweekend.core.domain.holiday.Holiday import noweekend.core.domain.holiday.HolidayReader +import noweekend.core.domain.sandwich.SandwichCalculator import noweekend.core.domain.tag.RecommendType import noweekend.core.domain.tag.TagReader import noweekend.core.domain.tag.TagRecommendCache @@ -36,6 +34,7 @@ import noweekend.core.support.error.CoreException import noweekend.core.support.error.ErrorType import org.springframework.stereotype.Service import java.time.LocalDate +import java.time.Year import java.time.temporal.ChronoUnit import kotlin.random.Random @@ -50,6 +49,7 @@ class RecommendServiceImpl( private val tagRecommendCacheReader: TagRecommendCacheReader, private val tagRecommendCacheWriter: TagRecommendCacheWriter, private val weekendReader: WeekendReader, + private val calculator: SandwichCalculator, ) : RecommendService { override fun getWeatherRecommend(userId: String): WeatherResponse { @@ -208,42 +208,43 @@ class RecommendServiceImpl( throw CoreException(ErrorType.MCP_SERVER_TAGS_ERROR) } - override fun getSandwich(userId: String): SandwichApiResponse { - val findUser = userReader.findUserById(userId) ?: throw CoreException(ErrorType.USER_NOT_FOUND_INTERNAL) - val birthDate = findUser.birthDate ?: throw CoreException(ErrorType.USER_BIRTH_DAY_NOT_FOUND) - val holidays: List = holidayReader.findRemainingHolidays(LocalDate.now()) - .map { holiday: Holiday -> holiday.date } - - val weekends: List = weekendReader.getUpcomingWeekends().map { weekend -> weekend.date } + override fun getSandwich(): SandwichApiResponse { + // 1) 기준 날짜 + val today = LocalDate.now() - val holidayOrWeekendSet = holidays.toSet() + weekends.toSet() - val remainingAnnualLeave = findUser.remainingAnnualLeave ?: throw CoreException(ErrorType.INVALID_LOCATION) + // 2) 남은 공휴일, 주말 조회 + val holidays = holidayReader.findRemainingHolidays(today).map { it.date }.toSet() + val weekends = weekendReader.getAllThisYearWeekends() + .map { it.date } + .filter { it.isAfter(today) } + .toSet() + + // 3) 연말까지 계산 + val until = Year.now().atMonth(12).atEndOfMonth() + val periods = calculator.recommendSandwich( + holidays = holidays, + weekends = weekends, + maxGap = 2, + minSpan = 3, + from = today, + until = until, + ) - try { - val bridgePeriods = recommendClient.getSandwich( - SandwichRequest(birthDay = birthDate, holidays = holidays, remainingAnnualLeave.toInt(), weekends), - ) - return SandwichApiResponse( - bridgePeriods.map { period -> - val allDates = generateDateRange(period.startDate, period.endDate) - val useAnnualLeaveDates = allDates.filter { date -> - !holidayOrWeekendSet.contains(date) && date.dayOfWeek.value in 1..5 - } - SandwichResponse( - startDate = period.startDate, - endDate = period.endDate, - useAnnualLeave = useAnnualLeaveDates.size, - totalVacationDays = allDates.size, - ) - }.toList(), + // 4) VacationPeriod → SandwichResponse 매핑 + val responses = periods.map { period -> + val totalDays = ChronoUnit.DAYS.between(period.startDate, period.endDate).toInt() + 1 + val useAnnualLeave = generateSequence(period.startDate) { it.plusDays(1) } + .takeWhile { !it.isAfter(period.endDate) } + .count { date -> date !in holidays && date !in weekends } + SandwichResponse( + startDate = period.startDate, + endDate = period.endDate, + useAnnualLeave = useAnnualLeave, + totalVacationDays = totalDays, ) - } catch (_: McpNotRespondingException) { - throw CoreException(ErrorType.MCP_SERVER_INTERNAL_ERROR) } - } - fun generateDateRange(start: LocalDate, end: LocalDate): List { - return (0..ChronoUnit.DAYS.between(start, end)).map { start.plusDays(it) } + return SandwichApiResponse(responses = responses) } override fun generateVacation(userId: String, request: GenerateVacationRequest): AiGenerateVacationApiResponse { diff --git a/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/sandwich/Sandwich.kt b/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/sandwich/Sandwich.kt new file mode 100644 index 0000000..7d51053 --- /dev/null +++ b/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/sandwich/Sandwich.kt @@ -0,0 +1,5 @@ +package noweekend.core.domain.sandwich + +import java.time.LocalDate + +data class Sandwich(val startDate: LocalDate, val endDate: LocalDate) diff --git a/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/sandwich/SandwichCalculator.kt b/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/sandwich/SandwichCalculator.kt new file mode 100644 index 0000000..8fdf0e6 --- /dev/null +++ b/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/sandwich/SandwichCalculator.kt @@ -0,0 +1,197 @@ +package noweekend.core.domain.sandwich + +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.Year +import java.time.temporal.ChronoUnit +import java.util.concurrent.ThreadLocalRandom + +@Service +class SandwichCalculator { + + private val logger = LoggerFactory.getLogger(this::class.java) + + fun recommendSandwich( + holidays: Set, + weekends: Set, + maxGap: Int = 2, + minSpan: Int = 3, + from: LocalDate = LocalDate.now(), + until: LocalDate = Year.now().atMonth(12).atEndOfMonth(), + ): List { + val candidates = buildCandidates(holidays, weekends, maxGap, from, until) + + val rawPeriods = candidates.mapNotNull { startDate -> + // ↓ 블록 첫날이 아닌 특수일(공휴일/주말) 시작일은 스킵 + val specials = holidays + weekends + if (startDate in specials && (startDate.minusDays(1) in specials)) { + return@mapNotNull null + } + + val period = extendPeriod(startDate, holidays, weekends, maxGap, until, candidates) + val length = ChronoUnit.DAYS.between(period.startDate, period.endDate).toInt() + 1 + if (length < minSpan) return@mapNotNull null + + val firstHol = holidays + .filter { it in period.startDate..period.endDate } + .minOrNull() ?: return@mapNotNull null + + val needed = countRequiredLeaves(period.startDate, firstHol, holidays, weekends) + if (needed > maxGap) return@mapNotNull null + + // 기존 “기간 내 갭(공휴·주말 아닌 날) 여부” 검사도 유지 + var d = period.startDate + var hasGap = false + while (!d.isAfter(period.endDate)) { + if (d !in holidays && d !in weekends) { + hasGap = true + break + } + d = d.plusDays(1) + } + if (!hasGap) return@mapNotNull null + + period + } + .distinct() + + val response = rawPeriods + .sortedBy { it.startDate } + .fold(mutableListOf>()) { groups, period -> + if (groups.isEmpty() || + period.startDate.isAfter(groups.last().maxOf { it.endDate }) + ) { + groups += mutableListOf(period) + } else { + groups.last() += period + } + groups + } + .map { group -> + group[ThreadLocalRandom.current().nextInt(group.size)] + } + + logger.info("=========================== HOLIDAY ===========================") + holidays.forEach { holiday -> + print(holiday) + print(" ") + } + logger.info("") + logger.info("=========================== HOLIDAY ===========================") + + logger.info("=========================== WEEKEND ===========================") + weekends.forEach { weekend -> + print(weekend) + print(" ") + } + logger.info("") + logger.info("=========================== WEEKEND ===========================") + logger.info("") + logger.info("=========================== rawPeriods ===========================") + rawPeriods.forEach { + print(it.startDate) + print(" ~ ") + logger.info(it.endDate.toString()) + } + logger.info("=========================== rawPeriods ===========================") + + logger.info("=========================== response ===========================") + response.forEach { + print(it.startDate) + print(" ~ ") + logger.info(it.endDate.toString()) + } + logger.info("=========================== response ===========================") + + return response + } + + private fun buildCandidates( + holidays: Set, + weekends: Set, + maxGap: Int, + from: LocalDate, + until: LocalDate, + ): List { + val base = holidays + weekends + val preHoliday = holidays.flatMap { hol -> + (1..maxGap).mapNotNull { offset -> + hol.minusDays(offset.toLong()).takeIf { it in from..until } + } + } + return (base + preHoliday) + .filter { it in from..until } + .distinct() + .sorted() + } + + private fun extendPeriod( + startDate: LocalDate, + holidays: Set, + weekends: Set, + globalMaxGap: Int, + until: LocalDate, + specials: List, + ): Sandwich { + val isPreGapStart = startDate !in holidays && startDate !in weekends + + var gapCount = 0 + var contigCount = 0 + var lastWasGap = false + + var current = startDate + var endDate = startDate + + while (!current.isAfter(until)) { + val isHoliday = current in holidays + val isWeekend = current in weekends + + if (isHoliday || isWeekend) { + // reset gap, increment contiguous holiday/weekend count + contigCount = if (lastWasGap) 1 else contigCount + 1 + gapCount = 0 + lastWasGap = false + endDate = current + } else { + if (!lastWasGap) gapCount = 0 + gapCount++ + + val currentGapMax = when { + contigCount >= 3 && isPreGapStart -> 0 + contigCount >= 3 -> 1 + else -> globalMaxGap + } + if (gapCount > currentGapMax) break + + if (contigCount < 3) { + val nextSpecial = specials.firstOrNull { it > current } + val window = (currentGapMax - gapCount + 1).toLong() + if (nextSpecial == null || nextSpecial.isAfter(current.plusDays(window))) { + break + } + } + + lastWasGap = true + endDate = current + } + current = current.plusDays(1) + } + return Sandwich(startDate, endDate) + } + + private fun countRequiredLeaves( + startDate: LocalDate, + firstHoliday: LocalDate, + holidays: Set, + weekends: Set, + ): Int { + var leaves = 0 + var day = startDate.plusDays(1) + while (day.isBefore(firstHoliday)) { + if (day !in holidays && day !in weekends) leaves++ + day = day.plusDays(1) + } + return leaves + } +} diff --git a/noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/controller/request/SandwichRequest.kt b/noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/controller/request/SandwichRequest.kt index ce9913a..f0475eb 100644 --- a/noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/controller/request/SandwichRequest.kt +++ b/noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/controller/request/SandwichRequest.kt @@ -3,8 +3,6 @@ package noweekend.mcphost.controller.request import java.time.LocalDate data class SandwichRequest( - val birthDay: LocalDate, val holidays: List, - val remainingAnnualLeave: Int, val weekends: List, ) diff --git a/noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/service/ChatbotService.kt b/noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/service/ChatbotService.kt index 15933b8..6cc17a7 100644 --- a/noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/service/ChatbotService.kt +++ b/noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/service/ChatbotService.kt @@ -307,7 +307,7 @@ EXAMPLE (must follow this format exactly): val objectMapper = jacksonObjectMapper().findAndRegisterModules() var lastException: Throwable? = null - repeat(10) { attempt -> + repeat(30) { attempt -> try { val rawResponse = chatClient.prompt() .system(systemPrompt) @@ -334,9 +334,9 @@ EXAMPLE (must follow this format exactly): return objectMapper.readValue(cleaned) } catch (e: Throwable) { lastException = e - println("getSandwich retry ${attempt + 1}/5: ${e.message}") + println("getSandwich retry ${attempt + 1}/30: ${e.message}") } } - throw IllegalStateException("Failed to get valid bridge vacation periods after 5 attempts", lastException) + throw IllegalStateException("Failed to get valid bridge vacation periods after 30 attempts", lastException) } }