Skip to content

[feat] AI 기반 독후감 생성 api 구현#323

Merged
buzz0331 merged 14 commits intodevelopfrom
feat/#321-ai-review
Oct 14, 2025
Merged

[feat] AI 기반 독후감 생성 api 구현#323
buzz0331 merged 14 commits intodevelopfrom
feat/#321-ai-review

Conversation

@buzz0331
Copy link
Contributor

@buzz0331 buzz0331 commented Oct 13, 2025

#️⃣ 연관된 이슈

closes #321

📝 작업 내용

spring ai(google ai - gemini 2.0 flash 모델)를 활용해서 독후감 생성 기능을 구현하였습니다.

독후감 생성 기능 서비스 흐름

  1. 사용자가 방 접근 권한이 있는지 판단
  2. 사용자가 해당 방에서 작성한 기록이 2개 이상인지 확인
  3. 사용자의 서비스 내 독후감 생성 횟수가 5회를 초과하지 않았는지 확인 -> 1회 증가
  4. Gemini 템플릿에 사용자의 기록(페이지별 기록을 페이지 순서대로)들을 프롬프트에 넣어서 요청
  5. Gemini 응답과 사용자의 독후감 생성 횟수를 반환

자세한 코드 내용은 통합 테스트 코드 확인해주세요~ (Google API 의존하지 않기 위해 GeminiLoadPort를 스텁해서 사용하였습니다.)

📸 스크린샷

스크린샷 2025-10-13 오후 4 38 41

💬 리뷰 요구사항

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

📌 PR 진행 시 이러한 점들을 참고해 주세요

* P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
* P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
* P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)

Summary by CodeRabbit

  • 신기능
    • AI 기반 독후감 생성 추가: POST /rooms/{roomId}/record/ai-review
    • 사용자 AI 사용량 조회 추가: GET /rooms/{roomId}/users/ai-usage (recordCount, recordReviewCount 반환)
    • 생성 요청 시 AI가 독후감을 작성하여 내용과 생성 횟수 반환
  • 제약
    • 독후감 생성은 최소 기록 2개 필요
    • 사용자별 생성 횟수 제한: 최대 5회
  • 문서
    • Swagger에 AI 독후감 생성/사용량 응답 및 관련 오류 케이스 추가
  • 테스트
    • AI 독후감 생성 및 사용량 조회 통합 테스트 추가
  • 기타
    • AI 연동 라이브러리 추가 및 사용자 생성 횟수 저장 컬럼 마이그레이션 적용

@coderabbitai
Copy link

coderabbitai bot commented Oct 13, 2025

Walkthrough

Spring AI 의존성 및 Gemini(ChatClient) 통합을 추가합니다. AI 독후감 생성 포트·어댑터·설정과 관련 유스케이스·서비스·컨트롤러·DTO·영속성 확장이 도입되며, 사용자 엔티티에 recordReviewCount 필드와 DB 마이그레이션 및 통합 테스트가 추가됩니다.

Changes

Cohort / File(s) Summary
Build & Dependencies
build.gradle
Spring AI BOM(org.springframework.ai:spring-ai-bom:1.0.0-M6) 및 org.springframework.ai:spring-ai-openai-spring-boot-starter 의존성 추가.
AI Integration (Port / Adapter / Config)
src/main/java/konkuk/thip/common/ai/application/out/GeminiLoadPort.java, src/main/java/konkuk/thip/common/ai/adapter/out/GeminiAdapter.java, src/main/java/konkuk/thip/config/GeminiAiConfig.java
GeminiLoadPort 계약 추가, GeminiAdapter 구현(프롬프트 생성·records 조합·ChatClient 호출·예외 래핑), ChatClient 빈 구성.
Error Codes & Swagger
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java, src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java
AI 관련 에러 코드 4건 추가 및 Swagger 응답 그룹 RECORD_AI_REVIEW_CREATE / RECORD_AI_USAGE 추가.
Web Controllers & DTOs
src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostCommandController.java, .../RoomPostQueryController.java, .../adapter/in/web/request/RecordAiUsageResponse.java, .../adapter/in/web/response/RecordReviewCreateResponse.java
POST /rooms/{roomId}/record/ai-review 및 GET /rooms/{roomId}/users/ai-usage 엔드포인트와 대응 DTO 추가.
Application Ports & Services
src/main/java/konkuk/thip/roompost/application/port/in/RecordReviewCreateUseCase.java, .../RecordAiUsageUseCase.java, .../application/port/in/dto/record/*, .../application/service/RecordReviewCreateService.java, .../application/service/RecordAiUsageService.java
AI 리뷰 생성·AI 사용량 조회 유스케이스 및 DTO 추가, 서비스 구현(검증·레코드 조회·카운트·Gemini 호출·유저 카운트 증가 등).
Record Persistence & Repository
src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordQueryPersistenceAdapter.java, src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java
roomId/userId 기준(개요 제외) 레코드 목록 조회 및 카운트 메서드 추가, RecordMapper 주입 및 매핑 반영.
User Domain & Persistence
src/main/java/konkuk/thip/user/domain/User.java, .../adapter/out/jpa/UserJpaEntity.java, .../adapter/out/mapper/UserMapper.java, src/main/resources/db/migration/V251012__Add_record_review_count.sql
사용자 도메인 recordReviewCount 필드 및 증가 로직(최대 5 제약) 추가, JPA 컬럼·매퍼·DB 마이그레이션 추가.
Tests
src/test/java/konkuk/thip/roompost/adapter/in/web/RecordAiReviewCreateApiTest.java, src/test/java/konkuk/thip/roompost/adapter/in/web/RecordAiUsageApiTest.java
AI 리뷰 생성 및 AI 사용량 조회에 대한 통합 테스트 추가(성공/실패 케이스 포함).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant WC as RoomPostCommandController
  participant S as RecordReviewCreateService
  participant V as RoomParticipantValidator
  participant RQ as RecordQueryPort
  participant U as UserCommandPort
  participant B as BookCommandPort
  participant G as GeminiLoadPort

  C->>WC: POST /rooms/{roomId}/record/ai-review
  WC->>S: createAiRecordReview(roomId, userId)
  S->>V: validate(userId, roomId)
  S->>RQ: findAllByRoomIdAndUserId(roomId, userId)
  S->>B: findByRoomId(roomId)
  S->>U: findById(userId)
  alt records < 2
    S-->>WC: throw RECORD_REVIEW_NOT_ENOUGH_RECORDS
    WC-->>C: 400 error
  else
    S->>U: increaseRecordReviewCount()
    S->>G: generateRecordReview(user, records, book, min,max)
    alt Gemini error
      G-->>S: throw GEMINI_API_*_ERROR
      S-->>WC: propagate 500
      WC-->>C: 500 error
    else success
      S->>U: save(updated user)
      S-->>WC: RecordReviewCreateResult(content, count)
      WC-->>C: 200 BaseResponse
    end
  end
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant WQ as RoomPostQueryController
  participant S as RecordAiUsageService
  participant V as RoomParticipantValidator
  participant RQ as RecordQueryPort
  participant U as UserCommandPort

  C->>WQ: GET /rooms/{roomId}/users/ai-usage
  WQ->>S: getUserAiUsage(userId, roomId)
  S->>V: validate(userId, roomId)
  S->>RQ: countAllByRoomIdAndUserId(roomId, userId)
  S->>U: findById(userId)
  S-->>WQ: RecordAiUsageResult(reviewCount, recordCount)
  WQ-->>C: 200 BaseResponse
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • hd0rable
  • seongjunnoh

Poem

토끼가 속삭여요, 깡충깡충 🐇
프롬프트에 씨앗 뿌려 글이 피어나네.
기록 셈해 다섯 번 안 넘기고,
개요 빼고 진짜만 골라 담아,
방에선 AI가 살짝 읊조려요.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed PR 제목은 AI 기반 독후감 생성 API 구현이라는 주요 변경사항을 명확하게 요약하여 내용을 잘 전달하고 있으며 불필요한 정보 없이 간결합니다.
Linked Issues Check ✅ Passed 관련된 이슈(#321)의 모든 요구 사항인 룸 접근 검사, 기록 개수 및 생성 횟수 검증, Gemini 템플릿 기반 프롬프트 생성, AI 응답 반환 및 카운트 증가 로직이 구현되어 있으며 이에 대한 통합 테스트도 포함되어 있어 이슈의 주요 목표를 충실히 충족합니다.
Out of Scope Changes Check ✅ Passed 변경된 모든 코드가 AI 기반 독후감 생성 API 구현과 직접 연관된 기능들로 이루어져 있어 범위를 벗어난 수정이 포함되어 있지 않습니다.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#321-ai-review

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Oct 13, 2025

Test Results

486 tests  +5   486 ✅ +5   48s ⏱️ +4s
145 suites +2     0 💤 ±0 
145 files   +2     0 ❌ ±0 

Results for commit e5a0384. ± Comparison against base commit fc8ff1a.

♻️ This comment has been updated with latest results.

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: 2

🧹 Nitpick comments (1)
src/main/resources/db/migration/V251012__Add_record_review_count.sql (1)

4-6: 불필요한 UPDATE 문을 제거하세요.

NOT NULL DEFAULT 0 제약조건과 함께 컬럼을 추가하면, MySQL이 자동으로 기존의 모든 행에 대해 값을 0으로 설정합니다. 따라서 WHERE record_review_count IS NULL 조건은 절대 매칭되지 않으며, 이 UPDATE 문은 불필요합니다.

다음 diff를 적용하여 불필요한 코드를 제거하세요:

 ALTER TABLE users
     ADD COLUMN record_review_count INT NOT NULL DEFAULT 0 AFTER follower_count;
-
-UPDATE users
-SET record_review_count = 0
-WHERE record_review_count IS NULL;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fc8ff1a and 97c11d0.

📒 Files selected for processing (25)
  • build.gradle (1 hunks)
  • src/main/java/konkuk/thip/common/ai/adapter/out/GeminiAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/common/ai/application/out/GeminiLoadPort.java (1 hunks)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (3 hunks)
  • src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java (1 hunks)
  • src/main/java/konkuk/thip/config/GeminiAiConfig.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostCommandController.java (2 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostQueryController.java (3 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/in/web/request/RecordAiUsageResponse.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/in/web/response/RecordReviewCreateResponse.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordQueryPersistenceAdapter.java (3 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/application/port/in/RecordAiUsageUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/application/port/in/RecordReviewCreateUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordAiUsageResult.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordReviewCreateResult.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/application/port/out/RecordQueryPort.java (2 hunks)
  • src/main/java/konkuk/thip/roompost/application/service/RecordAiUsageService.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/application/service/RecordReviewCreateService.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java (5 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/mapper/UserMapper.java (2 hunks)
  • src/main/java/konkuk/thip/user/domain/User.java (3 hunks)
  • src/main/resources/db/migration/V251012__Add_record_review_count.sql (1 hunks)
  • src/test/java/konkuk/thip/roompost/adapter/in/web/RecordAiReviewCreateApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/roompost/adapter/in/web/RecordAiUsageApiTest.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/test/java/konkuk/thip/roompost/adapter/in/web/RecordAiReviewCreateApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (35-403)
src/main/java/konkuk/thip/common/ai/adapter/out/GeminiAdapter.java (1)
src/main/java/konkuk/thip/common/exception/InternalServerException.java (1)
  • InternalServerException (5-17)
src/test/java/konkuk/thip/roompost/adapter/in/web/RecordAiUsageApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (35-403)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (19)
src/main/java/konkuk/thip/user/adapter/out/mapper/UserMapper.java (1)

18-18: LGTM!

도메인 엔티티와 JPA 엔티티 간의 recordReviewCount 필드 매핑이 양방향으로 정확하게 구현되었습니다.

Also applies to: 31-31

src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordReviewCreateResult.java (1)

3-6: LGTM!

AI 기반 독후감 생성 결과를 담는 간결하고 명확한 DTO입니다.

src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordAiUsageResult.java (1)

3-6: LGTM!

AI 사용량 정보를 담는 간결하고 명확한 DTO입니다.

src/main/java/konkuk/thip/roompost/application/port/in/RecordAiUsageUseCase.java (1)

5-7: LGTM!

AI 사용량 조회를 위한 명확한 유스케이스 인터페이스입니다.

src/main/java/konkuk/thip/config/GeminiAiConfig.java (1)

8-14: LGTM!

Gemini AI ChatClient를 위한 명확하고 간결한 Spring 설정입니다.

src/main/java/konkuk/thip/roompost/adapter/in/web/response/RecordReviewCreateResponse.java (1)

5-11: LGTM!

응용 계층의 결과를 웹 응답으로 변환하는 정확한 매핑이 구현되었습니다.

src/main/java/konkuk/thip/user/domain/User.java (1)

32-32: LGTM!

recordReviewCount 필드가 적절하게 추가되었고, 새 사용자 생성 시 0으로 올바르게 초기화됩니다.

Also applies to: 44-44

src/main/java/konkuk/thip/common/ai/application/out/GeminiLoadPort.java (1)

9-11: LGTM! 깔끔한 포트 인터페이스입니다.

헥사고날 아키텍처 패턴을 잘 따르고 있으며, 메서드 시그니처가 명확합니다. minLengthmaxLength 파라미터로 생성될 독후감의 길이를 제어할 수 있어 좋습니다.

src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostCommandController.java (2)

36-36: 의존성 주입이 올바르게 추가되었습니다.

새로운 AI 독후감 생성 기능을 위한 use case가 적절히 주입되었습니다.


185-199: 새로운 AI 독후감 생성 엔드포인트가 잘 구현되었습니다.

  • RESTful한 경로 설계 (POST /rooms/{roomId}/record/ai-review)
  • 보안 컨텍스트에서 사용자 ID 추출 (@UserId)
  • Swagger 문서화 포함
  • Use case로의 적절한 위임

관심사의 분리가 잘 이루어져 있습니다.

src/main/java/konkuk/thip/roompost/adapter/in/web/request/RecordAiUsageResponse.java (1)

5-14: LGTM! 간결하고 명확한 DTO입니다.

Java record를 활용한 불변 DTO로 잘 설계되었습니다. 정적 팩토리 메서드 of()를 통한 변환 로직도 깔끔합니다.

src/main/java/konkuk/thip/roompost/application/port/out/RecordQueryPort.java (1)

20-22: 새로운 쿼리 메서드가 적절히 추가되었습니다.

AI 독후감 생성을 위해 필요한 레코드 조회 및 카운트 메서드가 포트에 잘 정의되었습니다. 메서드 이름이 명확하고 반환 타입도 적절합니다.

구현체에서 isOverview = false 필터링이 올바르게 적용되었는지 확인하세요. AI 독후감 생성 시 개요 레코드는 제외되어야 합니다.

src/main/java/konkuk/thip/roompost/application/port/in/RecordReviewCreateUseCase.java (1)

5-8: LGTM! 단일 책임 원칙을 잘 따르는 use case 인터페이스입니다.

명확한 메서드 시그니처와 반환 타입으로 AI 독후감 생성의 의도를 잘 표현하고 있습니다.

src/main/java/konkuk/thip/roompost/application/service/RecordAiUsageService.java (1)

20-27: 서비스 로직이 잘 구현되었습니다.

방 멤버십 검증 후 필요한 데이터를 조회하는 흐름이 올바릅니다. countAllByRoomIdAndUserId 메서드를 사용하여 효율적으로 카운트를 가져오고 있습니다.

recordCountrecordReviewCountInteger 타입이므로 null이 될 수 있습니다. 포트 구현체에서 null 대신 0을 반환하도록 보장되어 있는지 확인하세요.

src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java (3)

50-52: 새 필드가 올바르게 추가되었습니다.

recordReviewCount 필드가 적절히 정의되었습니다:

  • @Builder.Default = 0으로 기본값 설정
  • nullable = false 제약조건 추가
  • 컬럼명 명시 (record_review_count)

62-77: 엔티티 업데이트 메서드가 일관성 있게 수정되었습니다.

updateIncludeAliasFromupdateFrom 두 메서드 모두에서 recordReviewCount 필드를 동기화하고 있어 데이터 일관성이 보장됩니다.


87-90: 테스트 전용 setter가 적절히 제한되었습니다.

@VisibleForTesting 어노테이션을 사용하여 테스트 목적임을 명확히 했습니다. 이는 프로덕션 코드에서 실수로 사용되는 것을 방지하는 좋은 패턴입니다.

src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java (2)

26-31: 레코드 조회 쿼리가 잘 작성되었습니다.

AI 독후감 생성에 필요한 조건을 정확히 구현했습니다:

  • isOverview = false: 개요 레코드 제외
  • ORDER BY r.page ASC: 페이지 순서대로 정렬하여 독후감 생성 시 맥락 유지
  • 명시적인 JOIN 조건으로 정확한 데이터 조회

33-37: 카운트 쿼리가 조회 쿼리와 일관성 있게 작성되었습니다.

findAllByRoomIdAndUserIdOrderByPageAsc와 동일한 필터 조건(isOverview = false)을 사용하여 카운트와 조회 결과가 일치하도록 보장합니다.

Comment on lines +102 to +104
// Spring AI - Google AI(Gemini) 연동
implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-M6")
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
Copy link

@coderabbitai coderabbitai bot Oct 13, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

잘못된 Spring AI 스타터 선택으로 컴파일/런타임 실패 가능

구글 Gemini 연동인데 spring-ai-openai-spring-boot-starter를 추가하면 OpenAI 전용 오토컨피그만 제공되어, GeminiAiConfig에서 필요한 org.springframework.ai.googleai.gemini 관련 빈을 못 찾아 컴파일이 깨지거나 런타임에 빈 생성이 실패합니다. Google Gemini용 스타터로 교체해야 합니다.

-	implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
+	implementation 'org.springframework.ai:spring-ai-google-ai-gemini-spring-boot-starter'
📝 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
// Spring AI - Google AI(Gemini) 연동
implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-M6")
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
// Spring AI - Google AI(Gemini) 연동
implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-M6")
implementation 'org.springframework.ai:spring-ai-google-ai-gemini-spring-boot-starter'
🤖 Prompt for AI Agents
build.gradle lines 102-104: 현재 Google Gemini 연동인데 잘못된 OpenAI 스타터가 추가되어 있어 빈 생성
실패/컴파일 오류가 발생할 수 있으니, implementation
'org.springframework.ai:spring-ai-openai-spring-boot-starter' 를 제거하고 Google
Gemini용 스타터로 교체하십시오; 예를 들어 implementation
'org.springframework.ai:spring-ai-googleai-spring-boot-starter' (또는 프로젝트/버전 정책에
맞는 공식 Google AI 스타터 아티팩트와 버전)를 사용해 Gemini 관련
빈(org.springframework.ai.googleai.gemini 등)이 올바르게 로드되도록 수정하세요.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

google ai로 하면 빈 등록이 안되어서 openai 의존성을 주입하고 내부 모델로 google ai의 gemini 모델을 사용한거야.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

Spring AI 1.0.0-M6 Google Gemini integration setup bean registration

💡 Result:

Short answer — in Spring AI 1.0.0-M6 you register Gemini (Vertex AI) clients and functions the same way you register other Spring AI beans: rely on the VertexAiGemini auto-configuration and expose beans for the ChatClient and any function callbacks.

Key steps (minimal example)

  • Ensure Vertex/Google credentials are available (ADC / service account) and Spring AI Vertex auto-config is on.
  • Expose a ChatClient bean using the provided ChatClient.Builder:
    • Example:
      • @configuration
        public class ChatClientConfig {
        @bean
        public ChatClient chatClient(ChatClient.Builder builder) {
        return builder.build();
        }
        }
  • Register callable functions (for Gemini function-calling) as normal Spring beans:
    • Option A — simple POJO/Function bean:
      • @bean
        public Function<MyRequest,MyResponse> currentWeather() { ... }
    • Option B — explicit FunctionCallback wrapper (required when you need OpenAPI schema / descriptions):
      • @bean
        public FunctionCallback weatherFunctionInfo() {
        return FunctionCallback.builder()
        .function("CurrentWeather", new MyWeatherService())
        .description("Get the current weather")
        .schemaType(SchemaType.OPEN_API_SCHEMA) // IMPORTANT for Gemini function calling
        .inputType(MyWeatherService.Request.class)
        .build();
        }
  • If you want ChatClient preconfigured with registered tools, inject ToolCallbackProvider into your chatClient bean and call defaultTools(tools) before build() (M6 docs show this pattern).
  • Note: M6’s “Function Calling” docs are marked deprecated in later releases; later milestones (M7+) changed tool/callback wiring (see migration notes) — if you upgrade you may need to enable tool callback support via properties or follow the newer Tool Calling APIs.

References

  • VertexAi / Gemini function registration and bean examples (Function bean and FunctionCallback wrapper). [1]
  • General M6 function-calling docs + examples for registering Function beans and ChatClient usage. [2]
  • Community tutorial showing ChatClient bean + ToolCallbackProvider usage and notes about changes in later milestones. [3]

If you want, I can paste a ready-to-run small code example (pom/deps + config + sample function) tailored to M6.


🌐 Web query:

Spring AI 1.0.0-M6 spring-ai-google-ai-gemini-spring-boot-starter artifact maven

💡 Result:

Maven coordinates (Spring AI 1.0.0‑M6):

  • GroupId: org.springframework.ai
  • ArtifactId: spring-ai-vertex-ai-gemini-spring-boot-starter
  • Version: 1.0.0-M6

Maven dependency:

org.springframework.ai
spring-ai-vertex-ai-gemini-spring-boot-starter
1.0.0-M6

Sources: Maven Central index and mvnrepository listing for the 1.0.0‑M6 starter. [1][2]

References
[1] Maven Central index for org.springframework.ai/spring-ai-vertex-ai-gemini-spring-boot-starter (1.0.0‑M6). (repo1.maven.org)
[2] mvnrepository entry for spring-ai-vertex-ai-gemini-spring-boot-starter 1.0.0‑M6. (mvnrepository.com)

Citations:


🌐 Web query:

Spring AI Google AI Gemini API spring-boot-starter artifact not Vertex AI 1.0.0-M6

💡 Result:

Short answer: Spring AI doesn't provide a standalone "Google AI Gemini" starter separate from Vertex AI in 1.0.0‑M6. The provided starters target Gemini via Vertex AI — use the Vertex/Gemini starter coordinates.

Maven (example):

  • org.springframework.ai:spring-ai-vertex-ai-gemini-spring-boot-starter:1.0.0-M6. [1][2]

Alternate/newer autoconfiguration name shown in docs:

  • org.springframework.ai:spring-ai-starter-model-vertex-ai-gemini (documented as the model starter). [3]

Sources:
[1] mvnrepository listing for spring-ai-vertex-ai-gemini-spring-boot-starter 1.0.0‑M6.
[2] Spring AI 1.0.0‑M6 release notes.
[3] Spring AI reference (VertexAI Gemini Chat / starter naming).


🌐 Web query:

Spring AI ChatClient OpenAI starter with Google Gemini model configuration baseUrl

💡 Result:

You can use the Spring AI OpenAI starter but point its OpenAI base-url at Vertex AI’s OpenAI-compatible endpoint and set the Gemini model name. Also supply a Google Cloud access token (refreshable) as the OpenAI api-key. Example application.yml:

Notes:

  • The Google Cloud access token is used in place of an OpenAI API key and must be refreshed (tokens typically expire ~1 hour). [1]
  • The base-url above matches Google’s OpenAI-compatible endpoint example; choose the Gemini model name supported by your project/location. [1]
  • Spring AI’s OpenAI starter supports overriding base-url and chat options via spring.ai.openai.* properties. [2]

References:
[1] Google Cloud — OpenAI compatibility / Vertex AI (examples showing base_url and use of Google credentials).
[2] Spring AI — OpenAI Chat configuration properties (spring.ai.openai.base-url, chat options).


🏁 Script executed:

#!/bin/bash
# Find Spring AI configuration in application properties/yml files
echo "=== Checking application configuration files ==="
fd -e yml -e yaml -e properties . src/main/resources src/test/resources 2>/dev/null | while read f; do
  echo "--- File: $f ---"
  cat "$f"
  echo ""
done

echo ""
echo "=== Searching for ChatModel bean definitions ==="
rg -n "ChatModel|GoogleAi|VertexAi|GeminiChatModel" --type java -C 3

echo ""
echo "=== Checking GeminiAiConfig implementation ==="
cat src/main/java/konkuk/thip/config/GeminiAiConfig.java

Length of output: 1773


🏁 Script executed:

#!/bin/bash
# Search for application.yml or application.properties with AI configuration
echo "=== Looking for Spring AI properties configuration ==="
rg -n "spring\.ai\.|openai|gemini|google" --type yaml --type properties src/main src/test 2>/dev/null || echo "No properties found with rg"

echo ""
echo "=== Checking if application.yml exists in main resources ==="
if [ -f "src/main/resources/application.yml" ]; then
  cat src/main/resources/application.yml
else
  echo "application.yml not found"
fi

echo ""
echo "=== Checking if application.properties exists ==="
if [ -f "src/main/resources/application.properties" ]; then
  cat src/main/resources/application.properties
else
  echo "application.properties not found"
fi

echo ""
echo "=== Searching for any yml/properties files in resources ==="
find src/main/resources -name "*.yml" -o -name "*.properties" 2>/dev/null | head -10

Length of output: 315


🏁 Script executed:

#!/bin/bash
# Check if there are environment-specific configuration files or external config
echo "=== Searching for profile-specific or external configuration ==="
find . -name "application-*.yml" -o -name "application-*.properties" 2>/dev/null | head -10

echo ""
echo "=== Checking docker-compose or deployment configs for AI configuration ==="
fd -e yml -e yaml docker-compose . 2>/dev/null | xargs grep -l "OPENAI\|GEMINI\|GOOGLE_AI" 2>/dev/null || echo "No docker-compose AI config found"

echo ""
echo "=== Checking if Spring AI OpenAI autoconfiguration classes are available ==="
echo "Checking what ChatModel implementation OpenAI starter provides..."
rg -n "OpenAiChatModel|ChatModel" build.gradle

echo ""
echo "=== Verifying the actual Spring AI OpenAI starter artifact name ==="
grep -A2 -B2 "spring-ai-openai" build.gradle

Length of output: 579


🌐 Web query:

Spring AI 1.0.0-M6 OpenAI starter autoconfiguration ChatModel bean registration

💡 Result:

Short answer — in Spring AI 1.0.0‑M6 the OpenAI starter auto‑configures a ChatModel bean named by the factory method openAiChatModel. The registration details:

  • Auto‑configuration class: org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration. [1]
  • Bean factory method: public OpenAiChatModel openAiChatModel(...) — the method is annotated @bean and @ConditionalOnMissingBean, so it only creates a bean when no other ChatModel (or compatible bean) is defined. [1][2]
  • Enable/disable toggle: controlled by property spring.ai.openai.chat.enabled (havingValue="true", matchIfMissing=true) — enabled by default. [1]
  • Implementation type: OpenAiChatModel implements ChatModel/StreamingChatModel. [2]

If you want to override it, define your own ChatModel/OpenAiChatModel bean or set spring.ai.openai.chat.enabled=false. References: Spring AI 1.0.0‑M6 API docs for OpenAiAutoConfiguration and OpenAiChatModel. [1][2]

Sources:
[1] Spring AI 1.0.0‑M6 — OpenAiAutoConfiguration (api docs).
[2] Spring AI 1.0.0‑M6 — OpenAiChatModel (api docs).


@buzz0331, 설명 감사합니다! 이해했습니다.

OpenAI 스타터를 사용하면서 내부적으로 Google Gemini 모델을 사용하시는 방식이군요. 다만, 현재 코드베이스에서는 Spring AI 관련 설정 파일(application.yml/application.properties)이 보이지 않는데, 외부에서 환경 변수나 설정을 통해 다음과 같이 구성하고 계신 건가요?

  • spring.ai.openai.base-url: Google의 OpenAI 호환 엔드포인트로 설정
  • spring.ai.openai.api-key: Google Cloud 액세스 토큰
  • spring.ai.openai.chat.options.model: google/gemini-2.0-flash-001 등 Gemini 모델명

이 설정들이 환경 변수나 Kubernetes Secrets 등으로 주입되고 있다면 현재 방식이 동작할 것입니다.

만약 이러한 외부 설정이 아직 구성되지 않은 상태라면, OpenAiChatModel이 기본적으로 OpenAI의 공식 API(api.openai.com)로 요청을 보내게 되어 Gemini 연동이 실제로는 작동하지 않을 수 있습니다. 현재 설정 상태를 확인해 주실 수 있을까요?


✏️ Learnings added
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#323
File: build.gradle:102-104
Timestamp: 2025-10-13T08:39:43.808Z
Learning: Spring AI 1.0.0-M6에서 Google AI Gemini 전용 스타터가 빈 등록에 실패하는 경우, spring-ai-openai-spring-boot-starter를 사용하고 외부 설정(환경 변수 등)으로 spring.ai.openai.base-url을 Google의 OpenAI 호환 엔드포인트로, spring.ai.openai.api-key를 Google Cloud 액세스 토큰으로, spring.ai.openai.chat.options.model을 Gemini 모델명으로 지정하여 우회할 수 있습니다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ㅇㅇ 너말대로 yml 파일을 secret으로 관리하고 있어.

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 97c11d0 and 9886a21.

📒 Files selected for processing (1)
  • src/main/java/konkuk/thip/user/domain/User.java (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (2)
src/main/java/konkuk/thip/user/domain/User.java (2)

44-44: LGTM!

새 사용자의 recordReviewCount 초기값을 0으로 설정한 것이 적절합니다.


32-32: 마이그레이션 스크립트가 null 방지를 보장합니다
V251012__Add_record_review_count.sql에서 INT NOT NULL DEFAULT 0과 기존 레코드 백필(UPDATE … SET … WHERE record_review_count IS NULL)이 적용되어 NPE 우려가 없습니다.

hd0rable
hd0rable previously approved these changes Oct 13, 2025
Copy link
Member

@hd0rable hd0rable left a comment

Choose a reason for hiding this comment

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

수고하셨습니당!! 👍🏻👍🏻

this.oauth2Id = "deleted:" + this.oauth2Id;
}

public void increaseRecordReviewCount() {
Copy link
Member

Choose a reason for hiding this comment

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

도메인내에서 예외처리 좋습니다 굿굿

입력 기록(페이지 오름차순, 요약/정제됨):
{records}

작성 지시:
Copy link
Member

Choose a reason for hiding this comment

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

옼ㅋㅋㅋ 좋네요 이거 현준띠가 직접 짜신건가여

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ai 도움을 좀 받았습니다 ㅎㅎ

User user = userCommandPort.findById(userId);
Book book = bookCommandPort.findBookByRoomId(roomId);

// 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상, 독후감 생성 횟수는 5회 이하)
Copy link
Member

Choose a reason for hiding this comment

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

아직 기록의 최대값은 기획적으로 미정인거죠?? 만약 한책에대해 기록이 10개이상에 각 기록의 글도 몇 천자이상일때 ai가 어떻게 잘 요약할지 궁금하네여..

Copy link
Collaborator

@seongjunnoh seongjunnoh Oct 14, 2025

Choose a reason for hiding this comment

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

@hd0rable 엇 현재 하나의 기록의 길이 최댓값은 제한되어 있긴한데(= 최대 500자), 희진님이 말하는 기록의 최댓값이라는게 모임방에서 작성할 수 있는 기록의 개수의 최댓값을 말하시는 걸까요?

Copy link
Member

Choose a reason for hiding this comment

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

네네 맞아요!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

한 모임방 내에서 기록의 갯수 제한이 딱히 없기 때문에 추후에 만약에 기록이 너무 넘쳐서 gemini 입력값을 초과하는 일이 발생한다면, 그때부터는 다음과 같은 다른 옵션을 좀 고려해봐야 할 것 같습니다.

  1. 페이지 순으로 정렬후 절반으로 끊어서 2번에 걸쳐서 gemini 호출
  2. 사용자가 원하는 기록을 선택하게끔 한 후 그 기록들에 한해서만 gemini 호출

recordJpaRepository.save(buildRecord(true, 9, "총평"));

// LLM 호출 모킹
final String AI_CONTENT = "AI가 생성한 독후감 본문";
Copy link
Member

Choose a reason for hiding this comment

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

LGTM

Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

Copy link
Collaborator

@seongjunnoh seongjunnoh left a comment

Choose a reason for hiding this comment

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

고생하셨습니다! ai 독서감상문 service 코드에서 코드 흐름과 관련된 간단한 리뷰 남겼는데, 확인부탁드립니다!

User user = userCommandPort.findById(userId);
Book book = bookCommandPort.findBookByRoomId(roomId);

// 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상, 독후감 생성 횟수는 5회 이하)
Copy link
Collaborator

@seongjunnoh seongjunnoh Oct 14, 2025

Choose a reason for hiding this comment

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

@hd0rable 엇 현재 하나의 기록의 길이 최댓값은 제한되어 있긴한데(= 최대 500자), 희진님이 말하는 기록의 최댓값이라는게 모임방에서 작성할 수 있는 기록의 개수의 최댓값을 말하시는 걸까요?

Comment on lines +1 to +6
ALTER TABLE users
ADD COLUMN record_review_count INT NOT NULL DEFAULT 0 AFTER follower_count;

UPDATE users
SET record_review_count = 0
WHERE record_review_count IS NULL; No newline at end of file
Copy link
Collaborator

Choose a reason for hiding this comment

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

DB 스키마 변경 확인했습니다

Comment on lines 43 to 47
user.increaseRecordReviewCount();
if(records.size() < 2) {
throw new BusinessException(ErrorCode.RECORD_REVIEW_NOT_ENOUGH_RECORDS,
new IllegalArgumentException("현재 기록 개수: " + records.size()));
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3 : 별건 아니긴한데, 처음에 records 를 조회한 이후, 위 business exception 검증을 수행하는건 어떤가요?? 코드 가독성 측면에서 더 좋을 것 같습니다!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

현재 서비스 로직에서 record를 조회한 이후 List를 반환받은 다음 business exception 검증을 수행하고 있는데 성준님이 말씀하시는 건 어떤 부분이죠?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

business exception 검증이 아니라 도메인 규칙 검증 말씀하시는건가요??

Copy link
Collaborator

Choose a reason for hiding this comment

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

// 1. 필요한 엔티티 조회
List<Record> records = recordQueryPort.findAllByRoomIdAndUserId(roomId, userId);
// 조회한 데이터에 대한 검증을 바로 수행
if(records.size() < 2) {
    throw new BusinessException(ErrorCode.RECORD_REVIEW_NOT_ENOUGH_RECORDS,
            new IllegalArgumentException("현재 기록 개수: " + records.size()));
}

// 이후 비즈니스 로직 수행
User user = userCommandPort.findById(userId);
Book book = bookCommandPort.findBookByRoomId(roomId);

// 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상, 독후감 생성 횟수는 5회 이하)
user.increaseRecordReviewCount();

// 3. 독후감 생성
String reviewContent = geminiQueryPort.generateRecordReview(user, records, book, MIN_REVIEW_LENGTH, MAX_REVIEW_LENGTH);

// 4. 독후감 생성 횟수 갱신
userCommandPort.update(user);

return new RecordReviewCreateResult(reviewContent, user.getRecordReviewCount());

위와 같은 흐름을 말씀드린 거였습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

음 현재 user.increaseRecordReviewCount() 내부에서 유효성 검증을 수행하고 있어서 제 생각은 오히려 주석에 적힌 흐름처럼 필요한 엔티티를 한번에 조회한 뒤, 이후 단계에서 유효성 검증 로직을 함께 수행하는 방식이 가독성 측면에서 더 명확하다는 생각이 들긴 했습니다. 다만, 기록 갯수 검증 전에 recordReviewCount 증가 함수(increaseRecordReviewCount)가 호출되다 보니 가독성이 떨어질수도 있겠다는 생각이 들어서, 두 코드의 순서만 바꾸는 식으로 코드를 수정해보려고 하는데 어떠신가요?

Copy link
Collaborator

Choose a reason for hiding this comment

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

음 현재 user.increaseRecordReviewCount() 내부에서 유효성 검증을 수행하고 있어서 제 생각은 오히려 주석에 적힌 흐름처럼 필요한 엔티티를 한번에 조회한 뒤, 이후 단계에서 유효성 검증 로직을 함께 수행하는 방식이 가독성 측면에서 더 명확하다는 생각이 들긴 했습니다. 다만, 기록 갯수 검증 전에 recordReviewCount 증가 함수(increaseRecordReviewCount)가 호출되다 보니 가독성이 떨어질수도 있겠다는 생각이 들어서, 두 코드의 순서만 바꾸는 식으로 코드를 수정해보려고 하는데 어떠신가요?

좋습니다!!

Comment on lines +26 to +37
@Query("SELECT r FROM RecordJpaEntity r " +
"WHERE r.roomJpaEntity.roomId = :roomId " +
"AND r.userJpaEntity.userId = :userId " +
"AND r.isOverview = false " +
"ORDER BY r.page ASC")
List<RecordJpaEntity> findAllByRoomIdAndUserIdOrderByPageAsc(Long roomId, Long userId);

@Query("SELECT COUNT(r) FROM RecordJpaEntity r " +
"WHERE r.roomJpaEntity.roomId = :roomId " +
"AND r.userJpaEntity.userId = :userId " +
"AND r.isOverview = false")
Integer countAllByRoomIdAndUserId(Long roomId, Long userId);
Copy link
Collaborator

Choose a reason for hiding this comment

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

확인했습니다

recordJpaRepository.save(buildRecord(true, 9, "총평"));

// LLM 호출 모킹
final String AI_CONTENT = "AI가 생성한 독후감 본문";
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

Comment on lines +120 to +121
@Test
@DisplayName("총평 기록(isOverview=true)은 기록 개수 집계에서 제외된다.")
Copy link
Collaborator

Choose a reason for hiding this comment

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

굳굳


UserJpaEntity persisted = userJpaRepository.findById(user.getUserId()).orElseThrow();
assertThat(returnedCount).isEqualTo(persisted.getRecordReviewCount());
assertThat(persisted.getRecordReviewCount()).isGreaterThanOrEqualTo(1);
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3 : 단순 궁금증인데, isEqualTo(1) 이 아니라 isGreaterThanOrEqualTo(1) 로 작성하신 이유가 있을까요??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

엇 그러게요. 잘못 작성한거 같네요. 수정하겠습니다~

seongjunnoh
seongjunnoh previously approved these changes Oct 14, 2025
Copy link
Collaborator

@seongjunnoh seongjunnoh left a comment

Choose a reason for hiding this comment

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

확인했슴다!

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: 2

🧹 Nitpick comments (1)
src/main/java/konkuk/thip/roompost/application/service/RecordReviewCreateService.java (1)

30-31: 독후감 길이 제약을 설정으로 외부화하는 것을 고려해보세요.

현재 독후감의 최소/최대 길이가 하드코딩되어 있습니다. 향후 요구사항 변경 시(예: 도서 유형별 다른 길이, 사용자 선호도 반영) 코드 수정이 필요합니다. application.yml에서 설정하거나 @Value 어노테이션을 통해 주입하는 방식으로 유연성을 높일 수 있습니다.

예시:

@Value("${ai.review.min-length:600}")
private int minReviewLength;

@Value("${ai.review.max-length:900}")
private int maxReviewLength;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 139f9bd and e5a0384.

📒 Files selected for processing (1)
  • src/main/java/konkuk/thip/roompost/application/service/RecordReviewCreateService.java (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#323
File: build.gradle:102-104
Timestamp: 2025-10-13T08:39:43.833Z
Learning: Spring AI 1.0.0-M6에서 Google AI Gemini 전용 스타터가 빈 등록에 실패하는 경우, spring-ai-openai-spring-boot-starter를 사용하고 외부 설정(환경 변수 등)으로 spring.ai.openai.base-url을 Google의 OpenAI 호환 엔드포인트로, spring.ai.openai.api-key를 Google Cloud 액세스 토큰으로, spring.ai.openai.chat.options.model을 Gemini 모델명으로 지정하여 우회할 수 있습니다.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (1)
src/main/java/konkuk/thip/roompost/application/service/RecordReviewCreateService.java (1)

38-38: 정렬 확인 완료
RecordQueryPersistenceAdapterfindAllByRoomIdAndUserId가 내부적으로 findAllByRoomIdAndUserIdOrderByPageAsc를 호출해 page 오름차순 정렬을 보장합니다.

Comment on lines +33 to +58
@Override
public RecordReviewCreateResult createAiRecordReview(Long roomId, Long userId) {
roomParticipantValidator.validateUserIsRoomMember(roomId, userId);

// 1. 필요한 엔티티 조회
List<Record> records = recordQueryPort.findAllByRoomIdAndUserId(roomId, userId);
User user = userCommandPort.findById(userId);
Book book = bookCommandPort.findBookByRoomId(roomId);

// 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상
if(records.size() < 2) {
throw new BusinessException(ErrorCode.RECORD_REVIEW_NOT_ENOUGH_RECORDS,
new IllegalArgumentException("현재 기록 개수: " + records.size()));
}

// 3. 독후감 생성 횟수 증가 (독후감 생성 횟수는 5회 이하일 경우만)
user.increaseRecordReviewCount();

// 4. 독후감 생성
String reviewContent = geminiQueryPort.generateRecordReview(user, records, book, MIN_REVIEW_LENGTH, MAX_REVIEW_LENGTH);

// 5. 독후감 생성 횟수 갱신
userCommandPort.update(user);

return new RecordReviewCreateResult(reviewContent, user.getRecordReviewCount());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

트랜잭션 경계와 동시성 제어가 누락되었습니다.

현재 메서드에 @Transactional 어노테이션이 없어서 다음과 같은 문제가 발생할 수 있습니다:

  1. 데이터 일관성 문제:

    • Line 49에서 user.increaseRecordReviewCount()로 메모리에서 카운트를 증가시키고
    • Line 52에서 Gemini API를 호출한 후
    • Line 55에서 userCommandPort.update(user)로 영속화합니다
    • 만약 update 호출이 실패하면 카운트 증가가 누락되어 상태가 불일치합니다
  2. 동시성 race condition:

    사용자 A의 현재 카운트 = 4
    
    Request 1: findById(A) → count=4 → increment → count=5
    Request 2: findById(A) → count=4 → increment → count=5
    Request 1: update(count=5) 
    Request 2: update(count=5)
    
    결과: 두 요청 모두 성공하여 총 6회 생성 가능 (제한 위반)
    

해결 방법:

 @Override
+@Transactional
 public RecordReviewCreateResult createAiRecordReview(Long roomId, Long userId) {

추가로 User 엔티티에 낙관적 락(@Version)을 추가하거나, 비관적 락(userCommandPort.findByIdWithLock)을 사용하여 동시성 문제를 해결해야 합니다.

-User user = userCommandPort.findById(userId);
+User user = userCommandPort.findByIdWithLock(userId); // 비관적 락 사용

Committable suggestion skipped: line range outside the PR's diff.

User user = userCommandPort.findById(userId);
Book book = bookCommandPort.findBookByRoomId(roomId);

// 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

주석을 완성해주세요.

Line 42의 주석 "기록 개수는 2개 이상"이 불완전합니다. 실제로는 2개 이상 검증과 독후감 생성 횟수 5회 이하 검증이 모두 수행됩니다.

다음과 같이 수정하세요:

-// 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상
+// 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상, 독후감 생성 횟수는 5회 이하)
📝 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
// 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상
// 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상, 독후감 생성 횟수는 5회 이하)
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/roompost/application/service/RecordReviewCreateService.java
around line 42, the inline comment "기록 개수는 2개 이상" is incomplete; update it to
clearly state both validations performed: check that the user has at least 2
records and that the user’s review creation count is 5 or fewer before allowing
review creation. Replace the incomplete comment with a concise description
covering both checks (record count >= 2 and review creation count <= 5).

throw new BusinessException(ErrorCode.RECORD_REVIEW_NOT_ENOUGH_RECORDS,
new IllegalArgumentException("현재 기록 개수: " + records.size()));
}
user.increaseRecordReviewCount();
Copy link
Collaborator

Choose a reason for hiding this comment

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

확인했습니다!!

@buzz0331 buzz0331 merged commit 5494213 into develop Oct 14, 2025
4 checks passed
@buzz0331 buzz0331 deleted the feat/#321-ai-review branch October 14, 2025 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[THIP2025-365] [feat] AI 기반 독후감 생성 api 구현

3 participants