Skip to content

Conversation

@toychip
Copy link
Member

@toychip toychip commented Jul 18, 2025

요약

  • AI 기반 휴가 일정 생성 및 DB 저장 기능 추가

작업 내용

  • generate-vacation API 및 MCP 연동
  • AiVacation 엔티티/리포지토리/Reader/Writer 계층 추가
  • (userId, searchDate) Unique 제약 적용
  • 이어하기 + 재시도 로직 반영
  • API 반환값 일관성 (startDate/endDate 등)

Summary by CodeRabbit

  • 신규 기능

    • AI가 생성한 휴가 일정이 시작일과 종료일을 포함하여 저장 및 조회됩니다.
    • 휴가 일정 요청 시 기간(시작일, 종료일) 기반 입력으로 변경되었습니다.
    • 생성된 휴가 일정에 제목, 상세 일정, 아이콘 스타일, 기간 정보가 포함됩니다.
    • AI가 추천한 일정 제목을 15자 이내의 한글로 요약해 제공합니다.
    • 생성된 휴가 일정을 사용자별, 날짜별로 조회할 수 있습니다.
  • 기능 개선

    • 휴가 일정 생성 및 조회 API의 응답 구조가 개선되어, 더 상세한 일정 정보와 기간을 제공합니다.
    • 휴가 일정 생성 과정이 비동기 저장 및 캐시 기반으로 변경되어, 생성 후 별도 조회가 가능합니다.
    • 휴가 일정 생성 로직이 콘텐츠 생성과 제목 요약으로 분리되어 안정성과 재시도 로직이 향상되었습니다.
    • 프롬프트(안내문)가 한글로 현지화되어 안내가 더욱 명확해졌습니다.
  • 버그 수정

    • 휴가가 아직 생성되지 않은 경우, 404 에러와 함께 안내 메시지가 제공됩니다.
  • 기타

    • 불필요한 필드(생일, 남은 휴일 등)와 사용되지 않는 API 및 프롬프트가 제거되었습니다.
    • 데이터베이스에 AI 휴가 일정 저장을 위한 구조가 추가되었습니다.

@toychip toychip requested a review from mkSpace as a code owner July 18, 2025 10:52
@coderabbitai
Copy link

coderabbitai bot commented Jul 18, 2025

Caution

Review failed

The pull request is closed.

"""

Walkthrough

이 변경은 AI 기반 휴가 생성 기능의 데이터 모델, API, 서비스, 저장소 계층을 전면적으로 리팩토링합니다. 요청 및 응답 모델이 날짜 범위 기반으로 변경되었고, 휴가 생성 및 조회 흐름이 도메인 엔티티와 캐시 기반으로 재구성되었으며, JPA 기반의 영속성 계층이 새로 도입되었습니다.

Changes

파일/경로 그룹 변경 요약
.../client-mcp/recommend/RecommendApi.kt
.../client-mcp/recommend/RecommendClient.kt
휴가 생성 API 응답 타입을 AiGenerateVacationResponse에서 AiVacationResponse로 변경
.../client-mcp/recommend/model/AiGenerateVacation.kt
.../mcp-host/controller/request/AiGenerateVacation.kt
요청 모델에서 daysstartDate, endDate로 교체, birthDateupcomingHolidays 삭제, 응답 모델 및 네이밍 변경
.../core-api/controller/v1/RecommendController.kt
.../core-api/controller/v1/docs/RecommendControllerDocs.kt
휴가 생성/조회 API 시그니처 및 반환 타입 변경, 정적 응답 제거, 서비스 호출로 변경
.../core-api/controller/v1/response/AiGenerateVacationApiResponse.kt AiGenerateVacationApiResponse 데이터 클래스 삭제
.../core-api/controller/v1/response/AiVacationApiResponse.kt AiVacationApiResponse 데이터 클래스 신설
.../core-api/domain/GenerateVacation.kt IconStyle enum 삭제
.../core-api/domain/recommend/RecommendService.kt
.../core-api/domain/recommend/RecommendServiceImpl.kt
서비스 인터페이스/구현체에서 휴가 생성 반환값 제거, 휴가 조회 메서드 추가, 비즈니스 로직 리팩토링
.../core-api/support/error/ErrorType.kt VACATION_NOT_FOUND 에러 타입 추가
.../core-domain/domain/vacation/AiVacation.kt AiVacation 도메인 클래스 및 IconStyle enum 신설
.../core-domain/domain/vacation/AiVacationReader.kt
.../core-domain/domain/vacation/AiVacationWriter.kt
도메인 리더/라이터 컴포넌트 신설
.../core-domain/domain/vacation/AiVacationRepository.kt 도메인 리포지토리 인터페이스 신설
.../mcp-host/controller/ChatbotController.kt /getSandwich 엔드포인트 삭제, 휴가 생성 엔드포인트 응답 타입 및 내부 처리 변경
.../mcp-host/controller/Prompt.kt sandwich date prompt 함수 삭제, detailed plan prompt 한글화 및 세부 규칙 강화
.../mcp-host/service/ChatbotService.kt 휴가 생성/타이틀 요약 메서드 분리, 날짜 계산 방식 단순화, 재시도 로직 개선
.../client-mcp/resources/client-mcp.yml Feign 클라이언트 타임아웃 설정 대폭 증가
.../db-core/vacation/AiVacationCoreRepository.kt JPA 기반 도메인 리포지토리 구현체 신설
.../db-core/vacation/AiVacationEntity.kt JPA 엔티티 및 도메인-엔티티 변환 함수 신설
.../db-core/vacation/AiVacationJpaRepository.kt JPA 리포지토리 인터페이스 신설

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as RecommendController
    participant Service as RecommendServiceImpl
    participant Repo as AiVacationRepository

    Client->>API: POST /generateVacation (userId, request)
    API->>Service: generateVacation(userId, request)
    Service->>Repo: save(AiVacation)
    API-->>Client: "휴가 생성 성공" 메시지

    Client->>API: GET /getVacation (userId, request)
    API->>Service: getVacation(userId)
    Service->>Repo: findByUserIdAndSearchDate(userId, today)
    Repo-->>Service: AiVacation
    Service-->>API: AiVacationApiResponse
    API-->>Client: ApiResponse<AiVacationApiResponse>
Loading
sequenceDiagram
    participant MCPClient
    participant ChatbotController
    participant ChatbotService
    participant AI

    MCPClient->>ChatbotController: POST /generate-vacation (startDate, endDate)
    ChatbotController->>ChatbotService: generateVacationContent(request)
    loop 날짜 chunk별
        ChatbotService->>AI: detailedPlanPrompt
        AI-->>ChatbotService: itinerary chunk
    end
    ChatbotService->>ChatbotController: AiVacationContent
    ChatbotController->>ChatbotService: summarizeTitle(content)
    ChatbotService->>AI: titlePrompt
    AI-->>ChatbotService: title
    ChatbotService-->>ChatbotController: AiVacationTitle
    ChatbotController-->>MCPClient: AiVacationResponse (title, content, startDate, endDate)
Loading

Possibly related PRs

  • feat: Implement real sandwich recommendation #67: 휴가 생성 API 및 데이터 모델의 초기 버전을 도입하며, AiGenerateVacationResponse 반환 및 샌드위치 휴가 추천 기능을 추가함. 본 PR의 모델/흐름 리팩토링과 연관성이 있음.

Suggested labels

feat
"""


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 248e264 and e725538.

📒 Files selected for processing (5)
  • noweekend-clients/client-mcp/src/main/resources/client-mcp.yml (1 hunks)
  • noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/RecommendController.kt (2 hunks)
  • noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/docs/RecommendControllerDocs.kt (4 hunks)
  • noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/recommend/RecommendServiceImpl.kt (6 hunks)
  • noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/controller/ChatbotController.kt (2 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (5)
noweekend-storage/db-core/src/main/kotlin/noweekend/storage/db/core/vacation/AiVacationJpaRepository.kt (1)

6-9: findAllByUserId에 페이징 옵션을 고려해 주세요

유저가 생성한 휴가가 많아질 경우, 전체 리스트를 한 번에 조회하면 메모리·응답시간이 늘어날 수 있습니다.
Pageable 파라미터를 추가하거나 Slice/Page 형태로 반환하도록 오버로드 메서드를 제공하면 확장성이 좋아집니다.

noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/RecommendClient.kt (1)

82-91: 로그 태그 오타: getSandwichgenerateVacation

남아 있는 이전 명칭 때문에 추적이 어렵습니다. 아래와 같이 교체를 권장합니다.

-            log.error("[getSandwich] 예기치 못한 예외, empty 반환.", e)
+            log.error("[generateVacation] 예기치 못한 예외, null 반환.", e)
noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/response/AiVacationApiResponse.kt (1)

6-12: Serializable 구현 고려

API DTO 가 캐시/세션 등에 저장될 가능성이 있다면 : java.io.Serializable 구현을 권장합니다.
추가 비용이 없으므로 미리 선언해 두면 활용도가 높습니다.

-data class AiVacationApiResponse(
+data class AiVacationApiResponse(

(implements Serializable 코드 추가)

noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/vacation/AiVacationWriter.kt (1)

11-13: 메소드 네이밍과 반환값에 대한 고려사항

register 메소드명이 도메인 의미를 잘 표현하고 있습니다. 다만 save 작업의 결과나 생성된 엔티티를 반환하지 않는 설계가 의도된 것인지 확인해보세요.

향후 저장된 엔티티 정보가 필요할 경우를 고려하여 반환값을 추가할 수 있습니다:

-    fun register(aiVacation: AiVacation) {
-        aiVacationRepository.save(aiVacation)
+    fun register(aiVacation: AiVacation): AiVacation {
+        return aiVacationRepository.save(aiVacation)
     }
noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/vacation/AiVacation.kt (1)

6-15: 데이터 클래스로 변환 고려

이 클래스는 데이터를 보관하는 역할을 하므로 data class로 선언하면 equals(), hashCode(), toString(), copy() 메서드를 자동으로 제공받을 수 있습니다.

-class AiVacation(
+data class AiVacation(
    val id: String,
    val title: String,
    val content: String,
    val startDate: LocalDate,
    val endDate: LocalDate,
    val searchDate: LocalDate,
    val userId: String,
    val iconStyle: IconStyle,
)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd75686 and 248e264.

📒 Files selected for processing (23)
  • noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/RecommendApi.kt (2 hunks)
  • noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/RecommendClient.kt (2 hunks)
  • noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/model/AiGenerateVacation.kt (2 hunks)
  • noweekend-clients/client-mcp/src/main/resources/client-mcp.yml (1 hunks)
  • noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/RecommendController.kt (2 hunks)
  • noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/docs/RecommendControllerDocs.kt (4 hunks)
  • noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/response/AiGenerateVacationApiResponse.kt (0 hunks)
  • noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/response/AiVacationApiResponse.kt (1 hunks)
  • noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/GenerateVacation.kt (0 hunks)
  • noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/recommend/RecommendService.kt (2 hunks)
  • noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/recommend/RecommendServiceImpl.kt (6 hunks)
  • noweekend-core/core-api/src/main/kotlin/noweekend/core/support/error/ErrorType.kt (1 hunks)
  • noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/vacation/AiVacation.kt (1 hunks)
  • noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/vacation/AiVacationReader.kt (1 hunks)
  • noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/vacation/AiVacationRepository.kt (1 hunks)
  • noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/vacation/AiVacationWriter.kt (1 hunks)
  • noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/controller/ChatbotController.kt (2 hunks)
  • noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/controller/Prompt.kt (2 hunks)
  • noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/controller/request/AiGenerateVacation.kt (2 hunks)
  • noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/service/ChatbotService.kt (4 hunks)
  • noweekend-storage/db-core/src/main/kotlin/noweekend/storage/db/core/vacation/AiVacationCoreRepository.kt (1 hunks)
  • noweekend-storage/db-core/src/main/kotlin/noweekend/storage/db/core/vacation/AiVacationEntity.kt (1 hunks)
  • noweekend-storage/db-core/src/main/kotlin/noweekend/storage/db/core/vacation/AiVacationJpaRepository.kt (1 hunks)
💤 Files with no reviewable changes (2)
  • noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/GenerateVacation.kt
  • noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/response/AiGenerateVacationApiResponse.kt
🔇 Additional comments (27)
noweekend-core/core-api/src/main/kotlin/noweekend/core/support/error/ErrorType.kt (1)

29-29: 에러 코드·메시지 중복 관리 필요

VACATION_NOT_FOUND(E404) 추가로 동일 코드(E404)를 갖는 항목이 4개 이상이 되었습니다.
프론트엔드·로깅 측에서 구분이 어려우므로, 코드 혹은 메시지에 세분화가 필요합니다.

noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/RecommendClient.kt (1)

5-5: 미사용 import 여부 확인

AiVacationResponse 는 아래 generateVacation 에서만 사용되므로 import 유지가 타당하지만, IDE 자동 최적화 시 삭제되지 않도록 확인 바랍니다.

noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/RecommendApi.kt (1)

46-46: null 허용 여부 일관성 검토

Feign 인터페이스는 non-nullable AiVacationResponse 를 반환하지만,
RecommendClient.generateVacation 에서는 nullable 로 감쌉니다.
실제 MCP 서버가 204/404 등을 보낼 가능성이 있다면 인터페이스도 AiVacationResponse? 로 맞추는 것이 안전합니다.

noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/vacation/AiVacationWriter.kt (1)

6-14: 트랜잭션 처리와 구조가 올바르게 구현되었습니다.

도메인 계층의 쓰기 담당 컴포넌트로서 적절한 설계입니다. 클래스 레벨의 @Transactional 사용과 생성자 주입 방식이 올바릅니다.

noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/vacation/AiVacationRepository.kt (2)

5-9: 저장소 인터페이스 설계가 잘 구현되었습니다.

도메인 엔티티를 중심으로 한 깔끔한 저장소 인터페이스입니다. 메소드명이 명확하고 반환 타입 선택이 적절합니다.


7-7: 검색 메소드의 nullable 반환 타입이 적절합니다.

findByUserIdAndSearchDate가 nullable AiVacation?을 반환하는 것은 데이터가 없을 수 있는 상황을 명확히 표현합니다.

noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/recommend/RecommendService.kt (1)

14-16: CQRS 패턴을 적용한 인터페이스 설계 개선

generateVacationgetVacation의 분리는 명령과 조회의 책임을 명확히 구분하는 좋은 설계입니다. 비동기 처리 패턴을 지원하여 확장성과 사용자 경험을 개선할 수 있습니다.

noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/vacation/AiVacationReader.kt (2)

7-18: 읽기 전용 트랜잭션 처리가 올바르게 구현되었습니다.

@Transactional(readOnly = true) 사용으로 읽기 성능 최적화를 적용했고, 저장소 계층으로의 깔끔한 위임 패턴을 따르고 있습니다. AiVacationWriter와 일관된 설계입니다.


12-17: 메소드 구현이 간결하고 명확합니다.

두 메소드 모두 저장소 계층으로의 단순 위임으로 구현되어 있어 계층 간 책임 분리가 명확합니다.

noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/docs/RecommendControllerDocs.kt (2)

10-10: 응답 타입 업데이트가 적절합니다.

AiVacationApiResponse로의 변경이 도메인 모델 변경사항과 일치합니다.


415-441: 상세한 예시 응답이 API 이해도를 높입니다.

새로운 필드들(startDate, endDate, title, content)이 포함된 풍부한 예시 응답이 API 사용자에게 도움이 될 것입니다.

noweekend-clients/client-mcp/src/main/kotlin/noweekend/client/mcp/recommend/model/AiGenerateVacation.kt (2)

5-23: 요청 모델의 날짜 범위 기반 리팩토링이 올바르게 구현되었습니다.

days 기반에서 startDateendDate로의 변경은 더 명확한 휴가 기간 정의를 제공합니다. birthDateupcomingHolidays 제거는 모델을 단순화하고 핵심 기능에 집중하게 합니다.


25-30: 응답 모델의 명명과 구조 개선이 적절합니다.

AiGenerateVacationResponse에서 AiVacationResponse로의 리네이밍은 더 직관적이며, startDateendDate 필드 추가로 명시적인 휴가 기간 정보를 제공합니다.

noweekend-storage/db-core/src/main/kotlin/noweekend/storage/db/core/vacation/AiVacationEntity.kt (3)

14-23: 고유 제약 조건이 올바르게 설정되었습니다.

(user_id, search_date) 조합에 대한 고유 제약 조건은 사용자별 특정 검색 날짜에 대한 중복 생성을 방지하여 데이터 무결성을 보장합니다. 이는 캐시 기반 접근 방식에 적합합니다.


32-33: TEXT 타입 사용이 적절합니다.

content 필드에 columnDefinition = "TEXT"를 사용하여 긴 휴가 계획 내용을 저장할 수 있도록 한 것은 올바른 선택입니다.


52-73: 도메인-엔티티 변환 함수가 완전하고 정확합니다.

양방향 변환 함수가 모든 필드를 적절히 매핑하고 있으며, 확장 함수 사용으로 코드의 가독성과 유지보수성이 향상되었습니다.

noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/controller/ChatbotController.kt (1)

39-47: 2단계 휴가 생성 프로세스가 잘 구현되었습니다.

콘텐츠 생성과 제목 요약을 분리한 접근 방식은 관심사 분리 원칙을 잘 따르며, 요청의 날짜 정보를 응답에 포함시켜 명확성을 높였습니다.

noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/RecommendController.kt (1)

60-61: 비동기 휴가 생성 패턴이 적절하게 구현되었습니다.

휴가 생성을 비동기로 처리하고 간단한 성공 메시지를 반환하는 방식은 사용자 경험을 개선하며, 긴 처리 시간에 대한 대응이 잘 되어 있습니다.

noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/controller/request/AiGenerateVacation.kt (2)

5-23: 요청 모델이 날짜 범위 기반으로 효과적으로 단순화되었습니다.

days 기반에서 명시적인 startDateendDate로의 변경은 더 직관적이며, 불필요한 복잡성을 제거하여 모델을 개선했습니다.


25-38: 응답 클래스 구조가 잘 설계되었습니다.

AiVacationContent, AiVacationTitle, AiVacationResponse로 분리한 구조는 각각의 역할이 명확하며, 최종 응답에 날짜 범위 정보를 포함시켜 완전성을 높였습니다.

noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/vacation/AiVacation.kt (1)

40-45: LGTM!

IconStyle enum이 깔끔하게 정의되어 있습니다.

noweekend-storage/db-core/src/main/kotlin/noweekend/storage/db/core/vacation/AiVacationCoreRepository.kt (2)

8-11: LGTM!

Repository 패턴이 깔끔하게 구현되었습니다.


13-23: 확장 함수 정의 확인 완료

noweekend-storage/db-core/src/main/kotlin/noweekend/storage/db/core/vacation/AiVacationEntity.kt 파일에서

  • fun AiVacation.toEntity() (라인 53–62)
  • fun AiVacationEntity.toDomain() (라인 64–73)

확장 함수가 모두 정상 정의되어 있음을 확인했습니다.

noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/controller/Prompt.kt (1)

120-191: 프롬프트 구조 개선 확인

한국어 프롬프트로의 전환과 구조적 개선이 잘 이루어졌습니다. 특히 다음 사항들이 개선되었습니다:

  • 명확한 출력 형식 지정 (하루당 정확히 6개 불릿 포인트)
  • 날씨 정보 사용 금지 명시
  • Perplexity 사용 의무화
  • 재사용 금지 로직 추가
noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/service/ChatbotService.kt (2)

217-233: 제목 요약 로직 검증

제목 요약 구현이 깔끔합니다. 15자 제한도 적절히 처리되었습니다.


208-215: 정규식 패턴 확인

사용된 아이템 추출 로직이 적절하게 구현되었습니다. 향후 포맷이 변경될 경우 정규식 업데이트가 필요할 수 있습니다.

noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/recommend/RecommendServiceImpl.kt (1)

257-270: LGTM!

캐시된 휴가 정보를 조회하는 로직이 명확하게 구현되었습니다.

Comment on lines +155 to 193
for ((idx, datesChunk) in chunkedDates.withIndex()) {
val maxAttempts = 10
var attempt = 0
var chunkContent: String?

while (true) {
try {
val detailPrompt = prompt.detailedPlanPrompt(
request,
datesChunk,
idx * chunkSize,
allDates.size,
usedItems,
)
val userJson = objectMapper.writeValueAsString(
mapOf(
"dates" to datesChunk,
"profile" to minimalProfileMap(request),
"totalDays" to allDates.size,
),
)
val resp = chatClient.prompt()
.system(detailPrompt)
.user(userJson)
.call()
val content = resp.content() ?: error("Empty chunk")
usedItems += extractUsedItems(content)
chunkCache[idx] = content
return@mapIndexed content
} catch (e: NonTransientAiException) {
val cause = e.cause
val status = (cause as? RestClientResponseException)?.statusCode?.value()
val headers = (cause as? RestClientResponseException)?.responseHeaders

if (status == 429) {
val retryAfterSec = headers
?.getFirst("retry-after")
?.toLongOrNull()
?: 180L

logger.warn("429 rate limit hit, waiting $retryAfterSec seconds before retry")
Thread.sleep(retryAfterSec * 1000)
continue
chunkContent = resp.content() ?: error("Empty chunk at index $idx")
usedItems += extractUsedItems(chunkContent)
break // 성공했으면 while 빠져나감
} catch (e: Exception) {
attempt++
if (attempt >= maxAttempts) {
throw IllegalStateException("Chunk $idx 생성 실패: ${e.message}", e)
}
throw e
// 재시도 딜레이 추가
Thread.sleep(1000)
}
}
itineraryChunks += chunkContent ?: "" // null이 올 일은 없음(실패면 위에서 throw)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

재시도 로직 개선 필요

현재 재시도 로직에 몇 가지 개선 사항이 있습니다:

  1. Thread.sleep은 스레드를 블로킹합니다
  2. 고정된 1초 지연은 최적화되지 않았습니다
  3. 지수 백오프가 없습니다
-            val maxAttempts = 10
-            var attempt = 0
-            var chunkContent: String?
-
-            while (true) {
-                try {
-                    val detailPrompt = prompt.detailedPlanPrompt(
-                        request,
-                        datesChunk,
-                        idx * chunkSize,
-                        allDates.size,
-                        usedItems,
-                    )
-                    val userJson = objectMapper.writeValueAsString(
-                        mapOf(
-                            "dates" to datesChunk,
-                            "profile" to minimalProfileMap(request),
-                            "totalDays" to allDates.size,
-                        ),
-                    )
-                    val resp = chatClient.prompt()
-                        .system(detailPrompt)
-                        .user(userJson)
-                        .call()
-                    chunkContent = resp.content() ?: error("Empty chunk at index $idx")
-                    usedItems += extractUsedItems(chunkContent)
-                    break // 성공했으면 while 빠져나감
-                } catch (e: Exception) {
-                    attempt++
-                    if (attempt >= maxAttempts) {
-                        throw IllegalStateException("Chunk $idx 생성 실패: ${e.message}", e)
-                    }
-                    // 재시도 딜레이 추가
-                    Thread.sleep(1000)
-                }
-            }
+            val maxAttempts = 10
+            val backoffDelays = listOf(1000L, 2000L, 4000L, 8000L, 16000L)
+            var lastException: Exception? = null
+            
+            for (attempt in 0 until maxAttempts) {
+                try {
+                    val detailPrompt = prompt.detailedPlanPrompt(
+                        request,
+                        datesChunk,
+                        idx * chunkSize,
+                        allDates.size,
+                        usedItems,
+                    )
+                    val userJson = objectMapper.writeValueAsString(
+                        mapOf(
+                            "dates" to datesChunk,
+                            "profile" to minimalProfileMap(request),
+                            "totalDays" to allDates.size,
+                        ),
+                    )
+                    val resp = chatClient.prompt()
+                        .system(detailPrompt)
+                        .user(userJson)
+                        .call()
+                    val chunkContent = resp.content() ?: error("Empty chunk at index $idx")
+                    usedItems += extractUsedItems(chunkContent)
+                    itineraryChunks += chunkContent
+                    break
+                } catch (e: Exception) {
+                    lastException = e
+                    if (attempt < maxAttempts - 1) {
+                        val delay = backoffDelays.getOrElse(attempt) { backoffDelays.last() }
+                        Thread.sleep(delay)
+                    }
+                }
+            }
+            
+            if (lastException != null) {
+                throw IllegalStateException("Chunk $idx 생성 실패: ${lastException.message}", lastException)
+            }
📝 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
for ((idx, datesChunk) in chunkedDates.withIndex()) {
val maxAttempts = 10
var attempt = 0
var chunkContent: String?
while (true) {
try {
val detailPrompt = prompt.detailedPlanPrompt(
request,
datesChunk,
idx * chunkSize,
allDates.size,
usedItems,
)
val userJson = objectMapper.writeValueAsString(
mapOf(
"dates" to datesChunk,
"profile" to minimalProfileMap(request),
"totalDays" to allDates.size,
),
)
val resp = chatClient.prompt()
.system(detailPrompt)
.user(userJson)
.call()
val content = resp.content() ?: error("Empty chunk")
usedItems += extractUsedItems(content)
chunkCache[idx] = content
return@mapIndexed content
} catch (e: NonTransientAiException) {
val cause = e.cause
val status = (cause as? RestClientResponseException)?.statusCode?.value()
val headers = (cause as? RestClientResponseException)?.responseHeaders
if (status == 429) {
val retryAfterSec = headers
?.getFirst("retry-after")
?.toLongOrNull()
?: 180L
logger.warn("429 rate limit hit, waiting $retryAfterSec seconds before retry")
Thread.sleep(retryAfterSec * 1000)
continue
chunkContent = resp.content() ?: error("Empty chunk at index $idx")
usedItems += extractUsedItems(chunkContent)
break // 성공했으면 while 빠져나감
} catch (e: Exception) {
attempt++
if (attempt >= maxAttempts) {
throw IllegalStateException("Chunk $idx 생성 실패: ${e.message}", e)
}
throw e
// 재시도 딜레이 추가
Thread.sleep(1000)
}
}
itineraryChunks += chunkContent ?: "" // null이 올 일은 없음(실패면 위에서 throw)
}
for ((idx, datesChunk) in chunkedDates.withIndex()) {
val maxAttempts = 10
val backoffDelays = listOf(1000L, 2000L, 4000L, 8000L, 16000L)
var lastException: Exception? = null
for (attempt in 0 until maxAttempts) {
try {
val detailPrompt = prompt.detailedPlanPrompt(
request,
datesChunk,
idx * chunkSize,
allDates.size,
usedItems,
)
val userJson = objectMapper.writeValueAsString(
mapOf(
"dates" to datesChunk,
"profile" to minimalProfileMap(request),
"totalDays" to allDates.size,
),
)
val resp = chatClient.prompt()
.system(detailPrompt)
.user(userJson)
.call()
val chunkContent = resp.content() ?: error("Empty chunk at index $idx")
usedItems += extractUsedItems(chunkContent)
itineraryChunks += chunkContent
break
} catch (e: Exception) {
lastException = e
if (attempt < maxAttempts - 1) {
val delay = backoffDelays.getOrElse(attempt) { backoffDelays.last() }
Thread.sleep(delay)
}
}
}
if (lastException != null) {
throw IllegalStateException("Chunk $idx 생성 실패: ${lastException.message}", lastException)
}
}
🤖 Prompt for AI Agents
In
noweekend-mcp/mcp-host/src/main/kotlin/noweekend/mcphost/service/ChatbotService.kt
between lines 155 and 193, the retry logic uses Thread.sleep with a fixed
1-second delay, which blocks the thread and lacks exponential backoff. Replace
Thread.sleep with a non-blocking delay mechanism if possible, and implement
exponential backoff by increasing the delay time after each failed attempt to
optimize retry intervals and reduce resource blocking.

@toychip toychip merged commit 352eb7a into develop Jul 18, 2025
1 of 2 checks passed
@toychip toychip deleted the feat/generate-vacation branch July 18, 2025 11:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants