Skip to content

feat(chatbot): 마이페이지 고객문의 챗봇 구현 - #125

Merged
evenif99 merged 3 commits into
devfrom
feature/chatbot-ui
Jun 23, 2026
Merged

feat(chatbot): 마이페이지 고객문의 챗봇 구현#125
evenif99 merged 3 commits into
devfrom
feature/chatbot-ui

Conversation

@evenif99

@evenif99 evenif99 commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Gemini API 기반 FAQ 고객문의 챗봇을 마이페이지에 플로팅 버튼으로 추가
  • GSAP 애니메이션 적용 (열기/닫기, 메시지 등장, 캐릭터 바운스/눈 깜빡임)
  • 대화 내역 DB 저장/조회/초기화 기능 구현

변경 사항

신규

  • src/components/chatbot/Chatbot.tsx — 챗봇 UI (플로팅 버튼 + 채팅 화면 + 퀵리플라이 + 커스텀 메뉴)
  • src/components/chatbot/Chatbot.module.scss — 챗봇 스타일 (프로젝트 디자인 토큰 사용)
  • src/lib/ai/chatbot.ts — Gemini 2.5 Flash 연동 FAQ 응답 서비스
  • src/app/api/chatbot/route.ts — 챗봇 API (POST 메시지 전송 / GET 내역 조회 / DELETE 초기화)
  • src/domains/common/chatbot.service.ts — DB CRUD (세션/메시지 저장/조회)
  • prisma/schema.prisma — ChatSession, ChatMessage 모델 추가

수정

  • src/app/(page)/mypage/page.tsx — contact 항목 제거, Chatbot 컴포넌트 추가
  • src/middleware.ts — contact 인증 예외 경로 제거

삭제

  • src/app/(page)/contact/page.tsx — 미사용 고객문의 플레이스홀더 페이지

Test plan

  • 마이페이지 플로팅 챗봇 버튼 표시 확인
  • 버튼 hover 시 눈 등장 + 통통 바운스 애니메이션 확인
  • 챗봇 열기/닫기 GSAP 애니메이션 확인
  • 퀵리플라이 / 커스텀 메뉴 버튼으로 질문 전송 확인
  • Gemini API FAQ 응답 정상 수신 확인
  • 대화 내역 DB 저장 후 재진입 시 이전 대화 로드 확인
  • "처음으로" 클릭 시 대화 초기화 확인
  • contact 페이지 삭제 후 마이페이지에서 고객문의 항목 미노출 확인

🤖 Generated with Claude Code

Summary by CodeRabbit

릴리스 노트

  • 새 기능
    • 마이페이지에 고객문의 챗봇 UI 추가
    • 사용자별 대화 세션/메시지 저장 및 조회 제공
    • “처음으로”로 대화 이력 초기화 기능 제공
  • 변경 사항
    • 챗봇 응답 요청에 입력 검증, 타임아웃 및 재시도 적용
    • 대화 히스토리 자동 불러오기 및 전송 애니메이션 강화
  • 제거
    • 고객문의 페이지 및 관련 메뉴 항목 제거
    • 일부 하단 메뉴 노출 범위 조정

@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
talky-owl Ready Ready Preview, Comment Jun 23, 2026 5:50am

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Prisma 스키마에 ChatSession·ChatMessage 모델을 추가하고, Gemini 2.5 Flash 기반 AI 응답 함수(503/429 재시도 로직 포함), DB 서비스 계층, /api/chatbot REST 라우트(POST/GET/DELETE), GSAP 애니메이션 챗봇 UI 컴포넌트를 신규 구현했습니다. 마이페이지에 챗봇을 통합하고 기존 contact 페이지 및 미들웨어 제외 설정을 제거했습니다.

Changes

챗봇 기능 추가

Layer / File(s) Summary
Prisma 스키마: ChatSession·ChatMessage 모델 정의
prisma/schema.prisma
User 모델에 chatSessions 관계 필드를 추가하고, ChatSession(userId·createdAt·updatedAt·messages 관계)과 ChatMessage(sessionId·role·content·createdAt) 모델을 신규 정의했습니다.
Gemini AI 연동: getChatbotResponse 구현
src/lib/ai/chatbot.ts
gemini-2.5-flash 모델명·한국어 시스템 프롬프트·ChatMessage 인터페이스를 정의하고, 503/429 에러 조건 기반 재시도 로직(최대 2회, 1.5초 증분 지연), 30초 타임아웃을 포함한 getChatbotResponse 함수를 추가했습니다.
chatbot.service: DB 세션/메시지 CRUD
src/domains/common/chatbot.service.ts
getOrCreateSession, getSessionMessages(최대 30개), saveMessage($transaction 원자 처리), clearSession 함수를 추가해 챗봇 세션·메시지 DB 접근 계층을 구현했습니다.
API 라우트: POST·GET·DELETE 핸들러
src/app/api/chatbot/route.ts
인증·입력 검증(500자 제한)·AI 응답 생성 흐름의 POST, 히스토리 조회 GET(ko-KR 시간 포맷 HH:mm), 세션 초기화 DELETE 핸들러를 추가했습니다. 타임아웃 504, Gemini 서비스 오류 502, 기타 500으로 상태 코드를 분기합니다.
Chatbot 컴포넌트: UI·상태·GSAP 애니메이션
src/components/chatbot/Chatbot.tsx, src/components/chatbot/Chatbot.module.scss
Message 타입·빠른 답변·메뉴 상수, 열기/닫기 GSAP 타임라인, 타이핑 도트·부유 버튼 애니메이션, 히스토리 로딩, sendMessage 로직(DELETE 기반 초기화 포함), 메시지 렌더링(유저/봇 말풍선·빠른 답변·커스텀 메뉴·입력 폼)과 전체 SCSS 스타일을 구현했습니다.
마이페이지 및 미들웨어: 챗봇 통합·contact 제거
src/app/(page)/mypage/page.tsx, src/middleware.ts
MyPage<Chatbot /> 컴포넌트를 삽입하고 contact 메뉴 항목을 제거했으며, 미들웨어 config.matcher의 negative lookahead에서 contact(?:/|$) 예외 항목을 삭제했습니다.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Chatbot UI
  participant Route as /api/chatbot
  participant Service as chatbot.service
  participant Gemini as Gemini API

  rect rgba(100, 149, 237, 0.5)
    note over UI, Gemini: 메시지 전송 (POST)
    UI->>Route: POST { message }
    Route->>Route: 세션 인증 / 입력 검증(500자)
    Route->>Service: getOrCreateSession(userId)
    Service-->>Route: session
    Route->>Service: getSessionMessages(sessionId)
    Service-->>Route: history (최대 30개)
    Route->>Service: saveMessage('user', message)
    Route->>Gemini: getChatbotResponse(message, history)
    Gemini-->>Route: reply (재시도 최대 2회, 타임아웃 30초)
    Route->>Service: saveMessage('bot', reply)
    Route-->>UI: { success: true, data: { reply } }
  end

  rect rgba(144, 238, 144, 0.5)
    note over UI, Service: 히스토리 로딩 (GET)
    UI->>Route: GET
    Route->>Service: getOrCreateSession → getSessionMessages
    Service-->>Route: messages (timestamp: ko-KR HH:mm)
    Route-->>UI: { success: true, data: { messages } }
  end

  rect rgba(255, 165, 0, 0.5)
    note over UI, Service: 대화 초기화 (DELETE)
    UI->>Route: DELETE ("처음으로" 클릭)
    Route->>Service: clearSession(userId)
    Service-->>Route: deleted
    Route-->>UI: { success: true }
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

  • I5-Project/TALKY-OWL#63: 두 PR 모두 마이페이지의 "고객문의/연락처(contact)" 관련 라우팅·메뉴 구성을 건드리는데, 메인 PR이 contact(고객문의) 메뉴 노출을 제거하는 변경을 포함하고 있어 retrieved PR의 고객문의 항목 통합과 직접적으로 맞물립니다.
  • I5-Project/TALKY-OWL#95: 두 PR 모두 src/app/(page)/mypage/page.tsx의 하단 UI/네비게이션 구성을 수정하는데, 메인 PR이 Chatbot UI를 추가하고 메뉴를 변경하는 반면 retrieved PR이 BottomNavigation을 제거하므로 코드 레벨에서 상충할 가능성이 있습니다.

Suggested reviewers

  • juahcheon

Poem

🦉 부엉이가 키보드를 두드리며 노래하네,
Gemini야, 내 말 들려? 답해다오~
세션 열고, 메시지 저장, 트랜잭션 철컹!
GSAP으로 말풍선이 사르르 등장~
contact 페이지는 이제 안녕👋
챗봇이 왔으니 문의는 여기서! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 추가된 챗봇 기능의 핵심 변경사항을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿의 주요 섹션을 충실히 구성하고 있으며, 변경사항과 테스트 계획이 잘 정리되어 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/chatbot-ui

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🧹 Nitpick comments (3)
prisma/schema.prisma (1)

364-384: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

ChatSession/ChatMessage 조회 패턴에 맞는 인덱스를 추가해주세요.

Line 364-384 모델은 서비스 계층의 userId+updatedAt, sessionId+createdAt 조회를 직접 받는데, 현재 인덱스가 없어 데이터 증가 시 정렬 비용이 커집니다.

♻️ 제안 diff
 model ChatSession {
   id        String        `@id` `@default`(dbgenerated("gen_random_uuid()")) `@db.Uuid`
   userId    String        `@map`("user_id") `@db.Uuid`
   createdAt DateTime      `@default`(now()) `@map`("created_at")
   updatedAt DateTime      `@updatedAt` `@map`("updated_at")
   user      User          `@relation`(fields: [userId], references: [id], onDelete: Cascade)
   messages  ChatMessage[]

+  @@index([userId, updatedAt(sort: Desc)])
   @@map("chat_sessions")
 }

 model ChatMessage {
   id        String      `@id` `@default`(dbgenerated("gen_random_uuid()")) `@db.Uuid`
   sessionId String      `@map`("session_id") `@db.Uuid`
   role      String      `@db.VarChar`(10)
   content   String
   createdAt DateTime    `@default`(now()) `@map`("created_at")
   session   ChatSession `@relation`(fields: [sessionId], references: [id], onDelete: Cascade)

+  @@index([sessionId, createdAt(sort: Asc)])
   @@map("chat_messages")
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@prisma/schema.prisma` around lines 364 - 384, Add compound indexes to the
ChatSession and ChatMessage models to optimize query performance. In the
ChatSession model, add an index combining userId and updatedAt fields since the
service layer queries sessions by userId and orders by updatedAt. In the
ChatMessage model, add an index combining sessionId and createdAt fields since
the service layer queries messages by sessionId and orders by createdAt. These
indexes should be defined using the @@index directive in each model block to
prevent performance degradation as data grows.
src/app/(page)/about/page.module.scss (1)

369-371: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

중복 선언을 정리해 스타일 드리프트를 줄이세요.

동일한 규칙이 중복 선언되어 있어 이후 수정 시 불일치가 발생하기 쉽습니다. 한 곳으로 합쳐 유지보수 포인트를 줄이는 게 좋습니다.

♻️ 제안 수정안
 .showcasePhoneWrap {
   flex-shrink: 0;
 }
@@
-.showcasePhoneWrap {
-  flex-shrink: 0;
-}
@@
 `@media` (max-width: 640px) {
   .heroInner {
     height: 520px;
   }
@@
-  .heroInner {
-    height: 520px;
-  }

Also applies to: 396-398, 509-511, 536-538

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(page)/about/page.module.scss around lines 369 - 371, The
`.showcasePhoneWrap` class with the `flex-shrink: 0;` rule is declared multiple
times across the stylesheet, creating duplicate definitions that will be
difficult to maintain. Consolidate all instances of the `.showcasePhoneWrap`
selector into a single declaration, removing the redundant duplicate definitions
of this class. Keep only one `.showcasePhoneWrap` block in the file to ensure
consistent styling and reduce maintenance complexity.
src/app/(page)/mypage/page.tsx (1)

14-15: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

챗봇 번들을 지연 로딩해 초기 렌더 비용을 줄이세요.

현재는 마이페이지 진입 시 챗봇(애니메이션/아이콘 포함)을 즉시 로드합니다. next/dynamic으로 분리하면 초기 JS 부담을 줄일 수 있습니다.

♻️ 제안 수정안
+import dynamic from 'next/dynamic';
 import styles from './page.module.scss';
 
+const Chatbot = dynamic(() => import('`@/components/chatbot/Chatbot`'), {
+  ssr: false,
+});

Also applies to: 108-108

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(page)/mypage/page.tsx around lines 14 - 15, The Chatbot component
is currently being imported statically at the top of the page file, which loads
it immediately on page render. Replace the static import of Chatbot with a
dynamic import using the dynamic function from next/dynamic. This will defer
loading the Chatbot component and reduce the initial JavaScript bundle size for
the mypage. Update the import statement to use dynamic with the path to the
Chatbot component, and the component will be lazy-loaded only when needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.gitignore:
- Around line 39-40: The gitignore file currently contains two rules on lines
39-40 that ignore CSS Module source files (src/app/**/*.module.css and
src/components/**/*.module.css). Since CSS Module files are source code and not
build artifacts in Next.js projects, they should be tracked in version control.
Remove these two lines from the gitignore file to ensure CSS Module source files
are properly committed and tracked, preventing UI regression risks from missing
style definitions.

In `@src/app/`(page)/about/page.module.scss:
- Around line 132-133: The `.heroPhone` selector block is empty, which triggers
a `block-no-empty` lint error in the SCSS file. Remove the entire `.heroPhone
{}` block from the file if it is not being actively used. This will resolve the
lint violation and clean up unused CSS rules in the module.

In `@src/app/`(page)/about/page.tsx:
- Around line 14-16: The REVOLVING_WORDS constant in the about/page.tsx file
contains values that do not align with the policy-allowed category set of 연애,
가족, 친구, 직장. Update the REVOLVING_WORDS array to use only these 4
policy-compliant category values. If different display labels are needed for UI
presentation, create a separate constant (such as a category-to-label mapping
object) to handle the text mapping instead of storing non-standard labels
directly in REVOLVING_WORDS. Ensure REVOLVING_ORDER indices are adjusted
accordingly to maintain the correct correspondence between the words array and
the order array.

In `@src/app/api/chatbot/route.ts`:
- Around line 45-46: The role comparison in the message mapping is checking for
uppercase 'USER' but the stored values use lowercase 'user' and 'bot', causing
user messages to be incorrectly mapped as bot messages. In the ternary operator
where you check m.role === 'USER', change the comparison to use lowercase 'user'
to match the actual stored role values. This same case mismatch issue appears on
both lines 45 and 80, so both locations need to be updated consistently to
ensure proper message role mapping for both GET rendering and POST history
operations.
- Around line 24-25: The error handling in the route handler currently treats
JSON parsing failures, Gemini API failures, and timeouts as a single 500 error,
making client recovery and operational diagnostics difficult. Separate the error
handling for the req.json() call at line 24 with its own try-catch block to
return 400 for JSON parsing errors, and add distinct error handling for the
Gemini API call around line 51 to differentiate between upstream API failures
(502) and timeout errors (504), then update the error response logic in lines
56-61 to check the error type and return the appropriate HTTP status code
accordingly.

In `@src/components/chatbot/Chatbot.module.scss`:
- Around line 5-24: The `.floatingButton` style lacks keyboard focus visibility,
making it difficult for keyboard users to identify which button has focus during
navigation. Add a `:focus-visible` pseudo-class selector to the
`.floatingButton` rule that includes visual feedback such as an outline or
box-shadow (glow effect) to clearly indicate when the button receives keyboard
focus. This should be a common style applied consistently across all main
buttons in the application.
- Around line 176-182: The `.bubble` class uses the deprecated CSS value
`word-break: break-word` which is no longer recommended by the W3C CSS Text
Module Level 3 specification. Replace the `word-break: break-word` property in
the `.bubble` selector with two properties: `word-break: normal` and
`overflow-wrap: anywhere` to achieve the same text wrapping behavior using the
modern standard approach.

In `@src/components/chatbot/Chatbot.tsx`:
- Around line 303-425: The chatbot modal component is missing accessibility
features required for keyboard navigation. Add a role="dialog" attribute to the
containerRef element and implement proper ARIA attributes (aria-modal="true",
aria-labelledby, etc.). Add an effect hook that sets initial focus to the input
field when isOpen becomes true, implement an Escape key listener in the same
effect to call handleClose when Escape is pressed, and add a focus trap
mechanism that prevents keyboard tab navigation from escaping the modal by
cycling focus between the last focusable element and the first when users
attempt to tab past the boundaries (consider using the last button in customMenu
or the close button as the last focusable element, and the close button or input
as the first). Ensure cleanup of event listeners when isOpen becomes false or
the component unmounts.
- Around line 210-223: The loadHistory function in Chatbot.tsx sets
historyLoaded to true in the finally block unconditionally, which prevents retry
attempts if there's a temporary network or server error. Move the
setHistoryLoaded(true) call from the finally block to the end of the successful
data processing logic within the try block, specifically after setting the
messages with the fetched history data. This ensures the flag is only set to
true when the history actually loads successfully, allowing the component to
retry loading on subsequent mount attempts if the initial request fails.
- Around line 241-244: The code immediately clears the messages state with
setMessages([WELCOME_MESSAGE]) before waiting for the DELETE request to
complete, causing the UI to appear reset even if the server deletion fails. Move
the setMessages call inside a .then() block (or use await) to only clear the
local state after the fetch request succeeds, and add proper error handling in
the .catch() block to inform the user if the deletion operation fails on the
server.

In `@src/domains/common/chatbot.service.ts`:
- Around line 16-20: The getSessionMessages function retrieves all messages from
the database without any limit, causing the prompt to grow infinitely as
sessions accumulate more messages, which leads to potential Gemini API failures
or delays. Modify the prisma.chatMessage.findMany call to add a limit by using
the take parameter set to a reasonable number (30-50 messages) to ensure only
the most recent messages are retrieved. Adjust the query to fetch the latest
messages while maintaining the correct chronological order when returning
results to the caller.
- Around line 3-13: The getOrCreateSession function has a race condition where
concurrent requests can create duplicate sessions with the same userId between
the findFirst check and create operation. Replace the separate findFirst and
create operations with a single upsert operation using prisma.chatSession.upsert
to ensure atomicity. Additionally, review the session cleanup logic at lines
42-51 (where initialization deletes sessions) and ensure it deletes all sessions
for the user instead of just the latest one to prevent orphaned session data.

In `@src/lib/ai/chatbot.ts`:
- Around line 47-71: The getChatbotResponse function lacks timeout handling and
error type distinction for the external Gemini API call. Add a timeout mechanism
to the chat.sendMessage call and wrap it in error handling that distinguishes
between timeout errors, Gemini API errors, and other failures. Use a timeout
utility or Promise.race with a timeout Promise to prevent requests from hanging
indefinitely, then implement error handling that catches and re-throws or maps
errors with clear type information so upstream callers can branch responses
based on the specific failure type (timeout vs API failure vs other errors).

In `@src/styles/abstracts/_variables.scss`:
- Around line 74-79: The `BlinkMacSystemFont` value in the $font-family-base
variable is unquoted, which violates the stylelint `value-keyword-case` rule. To
fix this, wrap `BlinkMacSystemFont` in single quotes to match the formatting of
the other font family names like 'Pretendard' and 'Segoe UI' in the same
variable definition, ensuring consistent styling and compliance with the linting
rule.

---

Nitpick comments:
In `@prisma/schema.prisma`:
- Around line 364-384: Add compound indexes to the ChatSession and ChatMessage
models to optimize query performance. In the ChatSession model, add an index
combining userId and updatedAt fields since the service layer queries sessions
by userId and orders by updatedAt. In the ChatMessage model, add an index
combining sessionId and createdAt fields since the service layer queries
messages by sessionId and orders by createdAt. These indexes should be defined
using the @@index directive in each model block to prevent performance
degradation as data grows.

In `@src/app/`(page)/about/page.module.scss:
- Around line 369-371: The `.showcasePhoneWrap` class with the `flex-shrink: 0;`
rule is declared multiple times across the stylesheet, creating duplicate
definitions that will be difficult to maintain. Consolidate all instances of the
`.showcasePhoneWrap` selector into a single declaration, removing the redundant
duplicate definitions of this class. Keep only one `.showcasePhoneWrap` block in
the file to ensure consistent styling and reduce maintenance complexity.

In `@src/app/`(page)/mypage/page.tsx:
- Around line 14-15: The Chatbot component is currently being imported
statically at the top of the page file, which loads it immediately on page
render. Replace the static import of Chatbot with a dynamic import using the
dynamic function from next/dynamic. This will defer loading the Chatbot
component and reduce the initial JavaScript bundle size for the mypage. Update
the import statement to use dynamic with the path to the Chatbot component, and
the component will be lazy-loaded only when needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 0ba35aa2-7bb3-48b4-8837-2e9d1c54b780

📥 Commits

Reviewing files that changed from the base of the PR and between bfd1cf0 and 604f890.

⛔ Files ignored due to path filters (24)
  • package-lock.json is excluded by !**/package-lock.json
  • public/images/about/1.png is excluded by !**/*.png
  • public/images/about/10.png is excluded by !**/*.png
  • public/images/about/11.png is excluded by !**/*.png
  • public/images/about/12.png is excluded by !**/*.png
  • public/images/about/13.png is excluded by !**/*.png
  • public/images/about/14.png is excluded by !**/*.png
  • public/images/about/2.png is excluded by !**/*.png
  • public/images/about/3.png is excluded by !**/*.png
  • public/images/about/5.png is excluded by !**/*.png
  • public/images/about/6.png is excluded by !**/*.png
  • public/images/about/7.png is excluded by !**/*.png
  • public/images/about/8.png is excluded by !**/*.png
  • public/images/about/animal01.png is excluded by !**/*.png
  • public/images/about/animal02.png is excluded by !**/*.png
  • public/images/about/animal03.png is excluded by !**/*.png
  • public/images/about/mockup01.png is excluded by !**/*.png
  • public/images/about/mockup02.png is excluded by !**/*.png
  • public/images/about/mockup03.png is excluded by !**/*.png
  • public/images/about/mockup04.png is excluded by !**/*.png
  • public/images/about/s1_bg copy.svg is excluded by !**/*.svg
  • public/images/about/s1_bg.svg is excluded by !**/*.svg
  • public/images/characters-actions/Judge Owl-striking gavel down.gif is excluded by !**/*.gif
  • public/images/characters-actions/Judge Owl-striking_gavel_down.png is excluded by !**/*.png
📒 Files selected for processing (20)
  • .gitignore
  • package.json
  • prisma/schema.prisma
  • public/images/characters-actions/README (1).txt
  • src/app/(page)/about/page.module.scss
  • src/app/(page)/about/page.tsx
  • src/app/(page)/contact/page.tsx
  • src/app/(page)/landing/.gitkeep
  • src/app/(page)/layout.tsx
  • src/app/(page)/mypage/page.tsx
  • src/app/api/chatbot/route.ts
  • src/app/layout.tsx
  • src/components/about/SpriteAnimation.module.scss
  • src/components/about/SpriteAnimation.tsx
  • src/components/chatbot/Chatbot.module.scss
  • src/components/chatbot/Chatbot.tsx
  • src/domains/common/chatbot.service.ts
  • src/lib/ai/chatbot.ts
  • src/middleware.ts
  • src/styles/abstracts/_variables.scss
💤 Files with no reviewable changes (1)
  • src/app/(page)/contact/page.tsx

Comment thread .gitignore
Comment on lines +39 to +40
src/app/**/*.module.css
src/components/**/*.module.css

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) 현재 src 내 .module.css 파일 확인"
fd -t f '\.module\.css$' src || true

echo
echo "2) 현재 ignore 규칙이 실제/가상 경로에 매칭되는지 확인"
printf "src/app/sample.module.css\nsrc/components/sample.module.css\n" | git check-ignore -v --stdin || true

echo
echo "3) 이미 무시되고 있는 .module.css 파일 상태 확인"
git status --ignored --short | rg '\.module\.css$' || true

Repository: I5-Project/TALKY-OWL

Length of output: 403


Lines 39-40의 ignore 패턴은 소스 CSS Module을 자동으로 제외하는 활성 규칙입니다.

검증 결과, src/app/**/*.module.csssrc/components/**/*.module.css 규칙이 git에서 정상 작동하며 해당 경로의 모든 CSS Module 파일을 무시합니다. Next.js에서 .module.css는 빌드 결과물이 아닌 소스 코드이므로, 이 규칙이 활성화되어 있으면 개발자가 추가하는 CSS Module 파일들이 자동으로 버전 관리에서 제외되어 UI 회귀 위험을 초래합니다. 이 규칙을 제거하는 것을 권장합니다.

권장 수정
 # SCSS compiled output
-src/app/**/*.module.css
-src/components/**/*.module.css
📝 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
src/app/**/*.module.css
src/components/**/*.module.css
# SCSS compiled output
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore around lines 39 - 40, The gitignore file currently contains two
rules on lines 39-40 that ignore CSS Module source files
(src/app/**/*.module.css and src/components/**/*.module.css). Since CSS Module
files are source code and not build artifacts in Next.js projects, they should
be tracked in version control. Remove these two lines from the gitignore file to
ensure CSS Module source files are properly committed and tracked, preventing UI
regression risks from missing style definitions.

Comment thread src/app/(page)/about/page.module.scss Outdated
Comment on lines +14 to +16
const REVOLVING_WORDS = ['남친', '친구', '상사', '엄마'];
const REVOLVING_ORDER = [2, 3, 0, 1];
const CYCLE_DURATION = 2000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

카테고리 상수 값을 정책 허용값으로 통일하세요.

Line [14]의 REVOLVING_WORDS 값(남친, 상사, 엄마)은 카테고리 허용 집합(연애/가족/친구/직장)과 불일치합니다. 카테고리 기준값이라면 허용 4개 값으로 고정하고, 노출 문구가 필요하면 별도 라벨 매핑으로 분리하는 편이 안전합니다.

🔧 제안 수정안
-const REVOLVING_WORDS = ['남친', '친구', '상사', '엄마'];
+const REVOLVING_WORDS = ['연애', '친구', '직장', '가족'] as const;

As per coding guidelines, **/*.{ts,tsx}: Use only 4 writing categories: 연애, 가족, 친구, 직장. Do not use: 관계, 금전, 공간, 시간, 가치관, 역할.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(page)/about/page.tsx around lines 14 - 16, The REVOLVING_WORDS
constant in the about/page.tsx file contains values that do not align with the
policy-allowed category set of 연애, 가족, 친구, 직장. Update the REVOLVING_WORDS array
to use only these 4 policy-compliant category values. If different display
labels are needed for UI presentation, create a separate constant (such as a
category-to-label mapping object) to handle the text mapping instead of storing
non-standard labels directly in REVOLVING_WORDS. Ensure REVOLVING_ORDER indices
are adjusted accordingly to maintain the correct correspondence between the
words array and the order array.

Source: Coding guidelines

Comment on lines +24 to +25
const body = await req.json()
const { message } = body as { message: string }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

오류 유형 분기가 없어 JSON 파싱 실패/ Gemini 실패/타임아웃이 모두 500으로 처리됩니다.

Line 24와 Line 51의 실패를 Line 56-61에서 단일 500으로 처리하면, 클라이언트 복구 동작과 운영 진단이 어렵습니다. 최소한 파싱 실패(400), Gemini 업스트림 실패(502), 타임아웃(504)로 분리해주세요.

As per coding guidelines, "src/app/api/**/*.{ts,tsx}: ... Distinguish between Gemini API failures, JSON parsing failures, and timeouts."

Also applies to: 51-51, 56-61

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/chatbot/route.ts` around lines 24 - 25, The error handling in the
route handler currently treats JSON parsing failures, Gemini API failures, and
timeouts as a single 500 error, making client recovery and operational
diagnostics difficult. Separate the error handling for the req.json() call at
line 24 with its own try-catch block to return 400 for JSON parsing errors, and
add distinct error handling for the Gemini API call around line 51 to
differentiate between upstream API failures (502) and timeout errors (504), then
update the error response logic in lines 56-61 to check the error type and
return the appropriate HTTP status code accordingly.

Source: Coding guidelines

Comment thread src/app/api/chatbot/route.ts Outdated
Comment on lines +303 to +425
{isOpen && (
<>
<div ref={overlayRef} className={styles.overlay} onClick={handleClose} />
<div ref={containerRef} className={styles.container}>
<div className={styles.header}>
<div className={styles.header__spacer} />
<div className={styles.header__info}>
<span className={styles.header__name}>{displayName}</span>
<div className={styles.header__status}>
<span className={styles.header__lantern} />
<span className={styles.header__statusText}>봇과 대화중</span>
</div>
</div>
<button className={styles.header__close} onClick={handleClose} aria-label="닫기">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>

<div className={styles.messages}>
{messages.map((msg, i) => (
<div
key={i}
className={`${styles.message} ${msg.role === 'user' ? styles['message--user'] : styles['message--bot']}`}
ref={i === messages.length - 1 ? (el) => { lastMessageElRef.current = el } : undefined}
>
{msg.role === 'bot' ? (
<>
<span className={styles.message__sender}>토키올</span>
<div className={styles.botRow}>
<div className={styles.botAvatar}>AI</div>
<div className={styles.botContent}>
<div className={`${styles.bubble} ${styles['bubble--bot']}`}>
{msg.content}
</div>
<span className={styles.timestamp}>{msg.timestamp}</span>
</div>
</div>
</>
) : (
<>
<div className={`${styles.bubble} ${styles['bubble--user']}`}>
{msg.content}
</div>
<span className={styles.timestamp}>{msg.timestamp}</span>
</>
)}
</div>
))}

{isLoading && (
<div className={styles.typing} ref={typingRef}>
<div className={styles.botAvatar}>AI</div>
<div className={styles.typingDots}>
{[0, 1, 2].map((i) => (
<span
key={i}
className={styles.typingDot}
ref={(el) => { dotRefs.current[i] = el }}
/>
))}
</div>
</div>
)}

<div ref={messagesEndRef} />
</div>

<div ref={quickRepliesRef} className={styles.quickReplies}>
{QUICK_REPLIES.map((text) => (
<button
key={text}
className={styles.quickReply}
onClick={() => sendMessage(text)}
disabled={isLoading}
>
{text}
</button>
))}
</div>

<div className={styles.customMenu}>
<div className={styles.customMenu__box}>
<div className={styles.customMenu__grid}>
{MENU_ITEMS.map((item) => (
<button
key={item.label}
data-menu-item
className={styles.customMenu__item}
onClick={() => sendMessage(item.message)}
disabled={isLoading}
>
<item.icon className={styles.customMenu__icon} />
<span className={styles.customMenu__label}>{item.label}</span>
</button>
))}
</div>
</div>
</div>

<form className={styles.bottomBar} onSubmit={handleSubmit}>
<input
className={styles.bottomBar__input}
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="메시지를 입력하세요"
maxLength={500}
disabled={isLoading}
/>
<button
type="submit"
className={styles.bottomBar__send}
disabled={!input.trim() || isLoading}
aria-label="전송"
>
<svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
</svg>
</button>
</form>
</div>
</>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

모달 접근성(키보드 포커스/시맨틱) 처리가 빠져 있습니다.

현재 구조는 dialog 시맨틱, 초기 포커스 이동, Esc 닫기, 포커스 트랩이 없어 키보드 사용자가 배경 UI로 이탈할 수 있습니다. 챗봇을 모달로 연 이상 해당 접근성 동작을 같이 보장해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/chatbot/Chatbot.tsx` around lines 303 - 425, The chatbot modal
component is missing accessibility features required for keyboard navigation.
Add a role="dialog" attribute to the containerRef element and implement proper
ARIA attributes (aria-modal="true", aria-labelledby, etc.). Add an effect hook
that sets initial focus to the input field when isOpen becomes true, implement
an Escape key listener in the same effect to call handleClose when Escape is
pressed, and add a focus trap mechanism that prevents keyboard tab navigation
from escaping the modal by cycling focus between the last focusable element and
the first when users attempt to tab past the boundaries (consider using the last
button in customMenu or the close button as the last focusable element, and the
close button or input as the first). Ensure cleanup of event listeners when
isOpen becomes false or the component unmounts.

Comment on lines +3 to +13
export async function getOrCreateSession(userId: string) {
const existing = await prisma.chatSession.findFirst({
where: { userId },
orderBy: { updatedAt: 'desc' },
})

if (existing) return existing

return prisma.chatSession.create({
data: { userId },
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

세션 생성 로직이 원자적이지 않아 사용자당 세션 중복이 발생할 수 있습니다.

Line 4→11 흐름은 동시 POST에서 레이스가 나면 같은 userId로 여러 ChatSession이 생길 수 있고, Line 42-51의 초기화는 최신 세션 1개만 지워서 히스토리가 부분적으로 남습니다.

🐛 제안 방향
- const existing = await prisma.chatSession.findFirst(...)
- if (existing) return existing
- return prisma.chatSession.create({ data: { userId } })
+ // schema에서 ChatSession.userId unique 보장 후 upsert 사용
+ return prisma.chatSession.upsert({
+   where: { userId },
+   update: { updatedAt: new Date() },
+   create: { userId },
+ })

- await prisma.chatMessage.deleteMany({ where: { sessionId: session.id } })
+ await prisma.chatMessage.deleteMany({
+   where: { session: { userId } },
+ })

Also applies to: 41-51

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/domains/common/chatbot.service.ts` around lines 3 - 13, The
getOrCreateSession function has a race condition where concurrent requests can
create duplicate sessions with the same userId between the findFirst check and
create operation. Replace the separate findFirst and create operations with a
single upsert operation using prisma.chatSession.upsert to ensure atomicity.
Additionally, review the session cleanup logic at lines 42-51 (where
initialization deletes sessions) and ensure it deletes all sessions for the user
instead of just the latest one to prevent orphaned session data.

Comment thread src/domains/common/chatbot.service.ts
Comment thread src/lib/ai/chatbot.ts Outdated
Comment on lines +74 to +79
$font-family-base:
'Pretendard',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Line 77 스타일린트 오류를 직접 해소해주세요.

BlinkMacSystemFont가 비인용 식별자로 남아 있어 value-keyword-case 에러가 발생합니다. 문자열로 감싸면 규칙 위반 없이 동일한 폰트 fallback을 유지할 수 있습니다.

수정 예시
 $font-family-base:
   'Pretendard',
   -apple-system,
-  BlinkMacSystemFont,
+  'BlinkMacSystemFont',
   'Segoe UI',
   sans-serif;
📝 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
$font-family-base:
'Pretendard',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
$font-family-base:
'Pretendard',
-apple-system,
'BlinkMacSystemFont',
'Segoe UI',
sans-serif;
🧰 Tools
🪛 Stylelint (17.13.0)

[error] 77-77: Expected "BlinkMacSystemFont" to be "blinkmacsystemfont" (value-keyword-case)

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/styles/abstracts/_variables.scss` around lines 74 - 79, The
`BlinkMacSystemFont` value in the $font-family-base variable is unquoted, which
violates the stylelint `value-keyword-case` rule. To fix this, wrap
`BlinkMacSystemFont` in single quotes to match the formatting of the other font
family names like 'Pretendard' and 'Segoe UI' in the same variable definition,
ensuring consistent styling and compliance with the linting rule.

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/lib/ai/chatbot.ts (1)

83-88: 🩺 Stability & Availability | 🔵 Trivial

상태 코드 기반 재시도 판별로 변경하기

error.message 부분 문자열 매칭은 SDK 메시지 포맷 변경에 취약합니다. @google/generative-ai 0.21.0에서 제공하는 GoogleGenerativeAIFetchError는 HTTP 상태 코드를 나타내는 status?: number 속성을 가지고 있으므로, 다음과 같이 상태 코드 기반으로 판별하는 것이 더욱 견고합니다:

error.status === 503 || error.status === 429

현재 문자열 매칭 방식('503', '429', 'Service Unavailable', 'high demand')은 SDK의 메시지 포맷이 변경될 경우 의도하지 않은 동작을 초래할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/ai/chatbot.ts` around lines 83 - 88, The isRetryable check in the
error handling block currently uses fragile string matching against
error.message to detect retryable HTTP errors, which can break if the SDK
changes its error message format. Instead, use the status property from the
GoogleGenerativeAIFetchError object that is available in `@google/generative-ai`
0.21.0 and later. Replace the substring matching for '503', '429', 'Service
Unavailable', and 'high demand' with direct status code checks by accessing
error.status and comparing it to the numeric values 503 and 429, which provides
more robust and maintainable error detection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/ai/chatbot.ts`:
- Around line 75-96: The retry loop in the function uses the same chat session
instance across all retry attempts, which fails to properly retry due to a bug
in `@google/generative-ai` version 0.21.0 where ChatSession's _sendPromise gets
stuck in a reject state after an error. To fix this, move the chat session
creation (the model.startChat() call) inside the for loop that starts with `for
(let attempt = 0; attempt <= MAX_RETRIES; attempt++)`, so that a fresh
ChatSession instance is created for each retry attempt instead of reusing the
same chat object throughout the retry loop.

---

Nitpick comments:
In `@src/lib/ai/chatbot.ts`:
- Around line 83-88: The isRetryable check in the error handling block currently
uses fragile string matching against error.message to detect retryable HTTP
errors, which can break if the SDK changes its error message format. Instead,
use the status property from the GoogleGenerativeAIFetchError object that is
available in `@google/generative-ai` 0.21.0 and later. Replace the substring
matching for '503', '429', 'Service Unavailable', and 'high demand' with direct
status code checks by accessing error.status and comparing it to the numeric
values 503 and 429, which provides more robust and maintainable error detection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 756a5cba-eca5-4c89-bb76-a09473d8588a

📥 Commits

Reviewing files that changed from the base of the PR and between 604f890 and 7d7b6bc.

📒 Files selected for processing (1)
  • src/lib/ai/chatbot.ts

Comment thread src/lib/ai/chatbot.ts Outdated
Comment on lines +75 to +96
let lastError: unknown

for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
const result = await chat.sendMessage(userMessage)
return result.response.text()
} catch (error) {
lastError = error
const isRetryable =
error instanceof Error &&
(error.message.includes('503') ||
error.message.includes('429') ||
error.message.includes('Service Unavailable') ||
error.message.includes('high demand'))

if (!isRetryable || attempt === MAX_RETRIES) break

await sleep(RETRY_DELAY_MS * (attempt + 1))
}
}

throw lastError

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@google/generative-ai ChatSession sendMessage internal _sendPromise rejected state retry behavior

💡 Result:

In the @google/generative-ai SDK, the ChatSession class maintains an internal promise called _sendPromise to ensure that chat messages are processed sequentially and that message history is maintained correctly [1][2]. Historically, this implementation contained a bug where, if a call to sendMessage() failed, _sendPromise would remain in a rejected state [3][4]. Because all subsequent calls to sendMessage() awaited this same rejected _sendPromise, every future attempt on that ChatSession instance would instantly fail with the same original error, regardless of whether the underlying issue (e.g., a temporary network failure or 429 rate limit) had been resolved [3][4]. This behavior was addressed in updates to the SDK (such as PR #420 for the deprecated library and similar fixes in newer iterations) [5][6][4]. The fix involves adding an error handler (typically a .catch() block) to the promise chain that resets _sendPromise to Promise.resolve() upon failure [1][5]. This reset allows the ChatSession to recover and process new messages after a failed attempt [3][5]. If you encounter this issue in older versions of the SDK, you can work around it by manually resetting the promise if it reaches a rejected state [3][4]: chatSession._sendPromise = Promise.resolve; Note that the @google/generative-ai SDK is deprecated in favor of the newer @google/genai library [7]. The newer library handles retry logic differently, often using opt-in RetryOptions and the p-retry library to manage retries for failed HTTP requests, rather than relying solely on the internal promise state of the chat session [8].

Citations:


🏁 Script executed:

#!/bin/bash
# Find and check package.json for `@google/generative-ai` version
find . -name "package.json" -o -name "package-lock.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" | head -20

Repository: I5-Project/TALKY-OWL

Length of output: 193


🏁 Script executed:

#!/bin/bash
# Read package.json to find `@google/generative-ai` version
cat package.json | grep -A 2 -B 2 "generative-ai"

Repository: I5-Project/TALKY-OWL

Length of output: 341


🏁 Script executed:

#!/bin/bash
# Also check if there are any lock files that might have version info
if [ -f "package-lock.json" ]; then
  cat package-lock.json | grep -A 5 '"`@google/generative-ai`"'
fi

Repository: I5-Project/TALKY-OWL

Length of output: 396


🌐 Web query:

@google/generative-ai 0.21.0 release notes changelog _sendPromise fix

💡 Result:

The release notes for @google/generative-ai version 0.21.0, published on October 2, 2024, do not contain a fix for sendPromise [1][2]. The changes included in version 0.21.0 were: - Added a files_create_pdf example [1][2]. - Added search grounding features to the SDK [1][3]. - Standard version package updates [1][2]. Issues related to sendPromise in various Google generative AI SDKs (such as the deprecated generative-ai-js or the newer googleapis/js-genai SDK) were reported and addressed in later versions and separate projects [4][5][6][7]. Specifically, fixes involving sendPromise to resolve deadlocks or unhandled rejections occurred in 2025 across different library versions [5][6][7]. If you are experiencing issues with sendPromise in chat sessions, ensure you are using the latest version of the SDK, as subsequent updates have included patches for state management and error handling in chat instances [5][6][8].

Citations:


🌐 Web query:

@google/generative-ai PR #420 ChatSession sendMessage retry when released

💡 Result:

Pull Request #420 in the deprecated @google/generative-ai (generative-ai-js) repository was a fix for an issue where API errors caused all subsequent calls on a ChatSession instance to fail [1][2]. Before this fix, if an API call (such as sendMessage) encountered an error, the internal _sendPromise property of the ChatSession instance remained in a rejected state [3]. Consequently, every future attempt to use the same ChatSession instance would immediately reject with the same error, regardless of whether the underlying issue (e.g., a temporary 429 or 503 error) had been resolved [3]. The PR addressed this by adding a.catch block to the _sendPromise chain [4]. This block ensures that if a request fails, the _sendPromise is reset to a resolved state (Promise.resolve), allowing subsequent calls to proceed normally instead of inheriting the previous failure [4]. It is important to note that this fix pertains to the deprecated library. Modern implementations, such as the unified Google GenAI SDK (@google/genai), handle retries and client stability differently, often utilizing integrated retry logic (such as p-retry) and configurable retry options, which should be used for new projects [5][6].

Citations:


🌐 Web query:

@google/generative-ai 0.21.x patch versions after 0.21.0 changelog fix _sendPromise

💡 Result:

The fix for the issue where _sendPromise caused subsequent calls to fail after an API error was not included in the 0.21.x patch versions [1][2][3]. The problem, identified as an issue where _sendPromise remained in a rejected state after a failure, was documented in issue #417 [4] and addressed in pull request #420 [5]. This fix was implemented later in the SDK's lifecycle (specifically appearing in version 0.24.0) [4][3]. The 0.21.0 release focused on features such as search grounding and did not include this error handling refactor [1][6][7]. The repository for the affected SDK (@google/generative-ai) is now considered legacy, and developers are advised to migrate to the official Google Generative AI SDK for JavaScript [3].

Citations:


🌐 Web query:

@google/generative-ai PR #420 merged date when released version number

💡 Result:

The pull request #420 in the deprecated @google/generative-ai JavaScript SDK repository (formerly google-gemini/deprecated-generative-ai-js) was released as part of version 0.24.1 [1][2]. Version 0.24.1 was published on April 29, 2025 [1][3]. Note that there are multiple repositories associated with Google's generative AI tools. Pull request #420 in the deprecated @google/generative-ai repository specifically addressed an issue where subsequent calls would fail after an API error [1][4][5]. Other repositories, such as GoogleCloudPlatform/generative-ai, also contain pull requests numbered #420, but those are unrelated to the @google/generative-ai package versioning [6].

Citations:


0.21.0 버전의 ChatSession이 재시도 시 실제로 API를 호출하지 못합니다.

@google/generative-ai 버전 0.21.0(2024년 10월 출시)에는 ChatSession의 내부 _sendPromise가 에러 발생 후 reject 상태로 유지되는 문제가 있습니다. 이로 인해 네트워크 장애나 429/503 같은 일시적 오류 발생 시 다음 sendMessage 호출이 새로운 네트워크 요청을 시도하지 않고 직전 에러를 즉시 다시 throw하게 됩니다. 결과적으로 재시도 루프는 sleep만 반복하다 같은 에러로 종료됩니다.

두 가지 해결 방법이 있습니다:

  1. 매 시도마다 새 chat 세션 생성 (현재 제시된 방식): 루프 내부에서 model.startChat()을 호출하여 매번 새로운 ChatSession 인스턴스를 만들면 이 문제를 회피할 수 있습니다.

  2. 의존성 업그레이드: @google/generative-ai 버전 0.24.1 이상으로 업그레이드하면 PR #420에서 수정된 _sendPromise 에러 처리가 포함되어 재시도 로직이 정상 작동합니다.

🐛 제안 diff — chat 생성을 루프 내부로 이동
-  const chat = model.startChat({
-    history: [
-      { role: 'user', parts: [{ text: '시스템 설정' }] },
-      { role: 'model', parts: [{ text: SYSTEM_PROMPT }] },
-      ...history.map((msg) => ({
-        role: msg.role === 'user' ? 'user' as const : 'model' as const,
-        parts: [{ text: msg.content }],
-      })),
-    ],
-  })
-
   let lastError: unknown

   for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
     try {
+      const chat = model.startChat({
+        history: [
+          { role: 'user', parts: [{ text: '시스템 설정' }] },
+          { role: 'model', parts: [{ text: SYSTEM_PROMPT }] },
+          ...history.map((msg) => ({
+            role: msg.role === 'user' ? 'user' as const : 'model' as const,
+            parts: [{ text: msg.content }],
+          })),
+        ],
+      })
       const result = await chat.sendMessage(userMessage)
       return result.response.text()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/ai/chatbot.ts` around lines 75 - 96, The retry loop in the function
uses the same chat session instance across all retry attempts, which fails to
properly retry due to a bug in `@google/generative-ai` version 0.21.0 where
ChatSession's _sendPromise gets stuck in a reject state after an error. To fix
this, move the chat session creation (the model.startChat() call) inside the for
loop that starts with `for (let attempt = 0; attempt <= MAX_RETRIES;
attempt++)`, so that a fresh ChatSession instance is created for each retry
attempt instead of reusing the same chat object throughout the retry loop.

Comment thread public/images/about/1.png Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이 이미지들은 랜딩관련 이미지들인데 왜 다시 올리시는건가요?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

클로드가 착각했나 봅니다 수정하겠슴다~

evenif99 and others added 2 commits June 23, 2026 14:39
Gemini API 기반 FAQ 챗봇을 마이페이지에 플로팅 버튼으로 추가.
GSAP 애니메이션 적용 (열기/닫기, 메시지 등장, 캐릭터 바운스).
대화 내역 DB 저장/조회 기능 포함.

- 챗봇 UI 컴포넌트 (플로팅 버튼 + 채팅 화면 + 커스텀 메뉴)
- Gemini 2.5 Flash 연동 FAQ 응답 (서비스 안내 범위 제한)
- ChatSession/ChatMessage Prisma 모델 추가
- 챗봇 API Route (POST/GET/DELETE)
- contact 페이지 및 관련 참조 삭제
- middleware에서 contact 인증 예외 경로 제거

Co-Authored-By: Claude <noreply@anthropic.com>
일시적 서버 과부하(503) 및 요청 제한(429) 발생 시
최대 2회 재시도하여 응답 안정성 향상.

Co-Authored-By: Claude <noreply@anthropic.com>
- role 대소문자 불일치 수정 (toLowerCase 비교)
- Gemini 실패(502), 타임아웃(504) 에러 유형 분리
- 30초 요청 타임아웃 추가
- 히스토리 조회 최근 30개로 제한
- DELETE 실패 시 사용자 안내 메시지 추가
- 히스토리 로드 실패 시 재시도 가능하도록 수정

Co-Authored-By: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/chatbot/Chatbot.tsx (1)

210-219: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

히스토리 응답이 현재 대화를 덮어쓸 수 있습니다.

Line 217에서 히스토리를 상태에 치환해 넣고 있어서, 사용자가 히스토리 로드 완료 전에 보낸 메시지가 유실될 수 있습니다. 최소한 기존 대화가 이미 시작된 경우에는 치환을 막아야 합니다.

🔧 제안 수정안
   const loadHistory = useCallback(async () => {
     if (historyLoaded) return
     try {
       const res = await fetch('/api/chatbot')
       if (!res.ok) return
       const data = await res.json()
-      if (data.success && data.data.messages.length > 0) {
-        setMessages([WELCOME_MESSAGE, ...data.data.messages])
-      }
-      setHistoryLoaded(true)
+      if (!data.success) return
+      setMessages((prev) => {
+        // 이미 사용자가 대화를 시작했다면 히스토리로 덮어쓰지 않음
+        if (prev.length > 1) return prev
+        return data.data.messages.length > 0
+          ? [WELCOME_MESSAGE, ...data.data.messages]
+          : prev
+      })
+      setHistoryLoaded(true)
     } catch {
       // 히스토리 로드 실패 시 웰컴 메시지만 유지, 다음 열기 시 재시도
     }
   }, [historyLoaded])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/chatbot/Chatbot.tsx` around lines 210 - 219, The loadHistory
function in Chatbot.tsx is unconditionally replacing all messages when history
is loaded, which can lose user messages sent before the history finishes
loading. Before calling setMessages with the history data, check if
messages.length is greater than 1 (indicating user messages already exist beyond
the WELCOME_MESSAGE). Only set the messages with the history data if no user
messages have been added yet. If the user has already started the conversation,
skip setting the history to preserve their existing messages.
♻️ Duplicate comments (1)
src/components/chatbot/Chatbot.tsx (1)

311-425: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

모달 접근성(대화창 시맨틱/키보드 제어)이 아직 누락되어 있습니다.

현재 대화창은 dialog 시맨틱, aria-modal, 초기 포커스 이동, Esc 닫기, 포커스 트랩이 없어 키보드 사용자가 배경 UI로 이탈할 수 있습니다. 접근성 기준상 머지 전 보완이 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/chatbot/Chatbot.tsx` around lines 311 - 425, The chatbot modal
container needs proper accessibility features. First, add the dialog role and
aria-modal="true" attribute to the container div referenced by containerRef.
Second, add a keyboard event listener in a useEffect hook to detect the Escape
key press and call handleClose when pressed. Third, implement initial focus
management by using useEffect to focus on the input element (referenced by the
input with className styles.bottomBar__input) or the close button when the modal
opens (when isOpen becomes true). Finally, implement focus trap by adding a
keydown handler to prevent Tab key navigation from escaping the modal
boundaries, ensuring focus cycles between the last focusable element and the
first focusable element within the container.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/ai/chatbot.ts`:
- Around line 68-74: The withTimeout function has a memory leak where setTimeout
timers are not cleaned up when the original promise completes before the
timeout. Capture the timeout ID returned from setTimeout, and add a cleanup
mechanism (such as using .finally() on the original promise or wrapping the
logic to clear the timeout) to ensure clearTimeout is called whenever the
original promise settles, whether it resolves or rejects. This prevents
unnecessary timer accumulation under high traffic that would burden the event
loop and consume memory.

---

Outside diff comments:
In `@src/components/chatbot/Chatbot.tsx`:
- Around line 210-219: The loadHistory function in Chatbot.tsx is
unconditionally replacing all messages when history is loaded, which can lose
user messages sent before the history finishes loading. Before calling
setMessages with the history data, check if messages.length is greater than 1
(indicating user messages already exist beyond the WELCOME_MESSAGE). Only set
the messages with the history data if no user messages have been added yet. If
the user has already started the conversation, skip setting the history to
preserve their existing messages.

---

Duplicate comments:
In `@src/components/chatbot/Chatbot.tsx`:
- Around line 311-425: The chatbot modal container needs proper accessibility
features. First, add the dialog role and aria-modal="true" attribute to the
container div referenced by containerRef. Second, add a keyboard event listener
in a useEffect hook to detect the Escape key press and call handleClose when
pressed. Third, implement initial focus management by using useEffect to focus
on the input element (referenced by the input with className
styles.bottomBar__input) or the close button when the modal opens (when isOpen
becomes true). Finally, implement focus trap by adding a keydown handler to
prevent Tab key navigation from escaping the modal boundaries, ensuring focus
cycles between the last focusable element and the first focusable element within
the container.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 62ce5902-bbd8-45ec-841f-ec8f2ee70550

📥 Commits

Reviewing files that changed from the base of the PR and between 4f3e1ba and e596f49.

📒 Files selected for processing (4)
  • src/app/api/chatbot/route.ts
  • src/components/chatbot/Chatbot.tsx
  • src/domains/common/chatbot.service.ts
  • src/lib/ai/chatbot.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/api/chatbot/route.ts
  • src/domains/common/chatbot.service.ts

Comment thread src/lib/ai/chatbot.ts
Comment on lines +68 to +74
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new ChatbotTimeoutError()), ms)
),
])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

withTimeout 타이머 정리를 추가해주세요.

현재 Promise.race에서 원본 Promise가 먼저 끝나도 setTimeout이 해제되지 않아, 요청당 타이머가 ms 동안 불필요하게 남습니다. 고트래픽에서 타이머 누적으로 이벤트 루프/메모리 부담이 생길 수 있습니다.

🐛 제안 diff
 function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
-  return Promise.race([
-    promise,
-    new Promise<never>((_, reject) =>
-      setTimeout(() => reject(new ChatbotTimeoutError()), ms)
-    ),
-  ])
+  let timer: ReturnType<typeof setTimeout> | undefined
+  const timeoutPromise = new Promise<never>((_, reject) => {
+    timer = setTimeout(() => reject(new ChatbotTimeoutError()), ms)
+  })
+
+  return Promise.race([promise, timeoutPromise]).finally(() => {
+    if (timer) clearTimeout(timer)
+  })
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/ai/chatbot.ts` around lines 68 - 74, The withTimeout function has a
memory leak where setTimeout timers are not cleaned up when the original promise
completes before the timeout. Capture the timeout ID returned from setTimeout,
and add a cleanup mechanism (such as using .finally() on the original promise or
wrapping the logic to clear the timeout) to ensure clearTimeout is called
whenever the original promise settles, whether it resolves or rejects. This
prevents unnecessary timer accumulation under high traffic that would burden the
event loop and consume memory.

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