Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -40,13 +38,6 @@ interface RecommendApi {
)
fun getTagOnlyNew(@RequestBody request: TagRequest): List<TagRecommendation>

@RequestMapping(
value = ["/getSandwich"],
consumes = [MediaType.APPLICATION_JSON_VALUE],
method = [RequestMethod.POST],
)
fun getSandwich(@RequestBody request: SandwichRequest): List<BridgeVacationPeriod>

@RequestMapping(
value = ["/generate-vacation"],
consumes = [MediaType.APPLICATION_JSON_VALUE],
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -82,18 +79,6 @@ class RecommendClient(
)
}

fun getSandwich(request: SandwichRequest): List<BridgeVacationPeriod> {
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)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class SecurityConfig {
"/v3/api-docs/**",
"/api/v1/login/**",
"/test-gen",
"/sandwich",
),
"deny" to arrayOf(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.tags.Tag
import noweekend.core.api.controller.v1.response.SandwichApiResponse
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
Expand All @@ -15,6 +17,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse
@RestController
class TempController(
private val testUserService: TestUserService,
private val recommendService: RecommendService,
) {

@Operation(
Expand Down Expand Up @@ -42,4 +45,10 @@ class TempController(
testUserService.testUserGen(),
)
}

@GetMapping("/sandwich")
fun getSandwich(): ApiResponse<SandwichApiResponse> {
val sandwichApiResponse = recommendService.getSandwich()
return ApiResponse.success(sandwichApiResponse)
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -50,27 +48,9 @@ class RecommendController(
}

@GetMapping("/sandwich")
override fun getSandwich(
@CurrentUserId userId: String,
): 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(mockResponse)
override fun getSandwich(): ApiResponse<SandwichApiResponse> {
val sandwichApiResponse = recommendService.getSandwich()
return ApiResponse.success(sandwichApiResponse)
}

@PostMapping("/generate-vacation")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -320,9 +320,7 @@ interface RecommendControllerDocs {
),
],
)
fun getSandwich(
@Parameter(hidden = true) @CurrentUserId userId: String,
): ApiResponse<SandwichApiResponse>
fun getSandwich(): ApiResponse<SandwichApiResponse>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

메서드 시그니처 변경이 적절하지만 문서화 내용과 불일치합니다.

userId 파라미터가 제거된 것은 내부 계산 로직으로의 변경과 일치하지만, 문서화 내용에서 여전히 "유저의 남은 연차"를 언급하고 있어 실제 구현과 일치하지 않습니다.

문서화 내용을 실제 구현에 맞게 수정하세요:

@Operation(
-    summary = "유저의 남은 연차와 올해 남은 공휴일/주말로 샌드위치 휴가(bridge vacation) 추천",
+    summary = "올해 남은 공휴일/주말로 샌드위치 휴가(bridge vacation) 추천",
    description = """
-유저의 남은 연차, 올해 남은 공휴일/주말을 바탕으로, 연속으로 쉴 수 있는 휴가(샌드위치 휴가) 구간을 추천합니다.
+올해 남은 공휴일/주말을 바탕으로, 연속으로 쉴 수 있는 휴가(샌드위치 휴가) 구간을 추천합니다.
각 휴가 구간별 실제 사용 연차 일수와 전체 휴가 일수도 반환됩니다.
""",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun getSandwich(): ApiResponse<SandwichApiResponse>
@Operation(
- summary = "유저의 남은 연차와 올해 남은 공휴일/주말로 샌드위치 휴가(bridge vacation) 추천",
+ summary = "올해 남은 공휴일/주말로 샌드위치 휴가(bridge vacation) 추천",
- description = """
-유저의 남은 연차, 올해 남은 공휴일/주말을 바탕으로, 연속으로 쉴 수 있는 휴가(샌드위치 휴가) 구간을 추천합니다.
+ description = """
+올해 남은 공휴일/주말을 바탕으로, 연속으로 쉴 수 있는 휴가(샌드위치 휴가) 구간을 추천합니다.
+각 휴가 구간별 실제 사용 연차 일수와 전체 휴가 일수도 반환됩니다.
""",
)
fun getSandwich(): ApiResponse<SandwichApiResponse>
🤖 Prompt for AI Agents
In
noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/docs/RecommendControllerDocs.kt
at line 323, the method signature for getSandwich no longer includes the userId
parameter, but the documentation still references "user's remaining annual
leave." Update the documentation to remove any mention of userId or
user-specific data and align the description with the current implementation
that does not require user identification.


@Operation(
summary = "AI 기반 여행 일정 생성",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SandwichResponse>,
)
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
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

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
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand All @@ -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 {
Expand Down Expand Up @@ -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<LocalDate> = holidayReader.findRemainingHolidays(LocalDate.now())
.map { holiday: Holiday -> holiday.date }

val weekends: List<LocalDate> = 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<LocalDate> {
return (0..ChronoUnit.DAYS.between(start, end)).map { start.plusDays(it) }
return SandwichApiResponse(responses = responses)
}

override fun generateVacation(userId: String, request: GenerateVacationRequest): AiGenerateVacationApiResponse {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package noweekend.core.domain.sandwich

import java.time.LocalDate

data class Sandwich(val startDate: LocalDate, val endDate: LocalDate)
Loading