feat(chatbot): 마이페이지 고객문의 챗봇 구현 - #125
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughPrisma 스키마에 Changes챗봇 기능 추가
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (3)
prisma/schema.prisma (1)
364-384: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winChatSession/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
⛔ Files ignored due to path filters (24)
package-lock.jsonis excluded by!**/package-lock.jsonpublic/images/about/1.pngis excluded by!**/*.pngpublic/images/about/10.pngis excluded by!**/*.pngpublic/images/about/11.pngis excluded by!**/*.pngpublic/images/about/12.pngis excluded by!**/*.pngpublic/images/about/13.pngis excluded by!**/*.pngpublic/images/about/14.pngis excluded by!**/*.pngpublic/images/about/2.pngis excluded by!**/*.pngpublic/images/about/3.pngis excluded by!**/*.pngpublic/images/about/5.pngis excluded by!**/*.pngpublic/images/about/6.pngis excluded by!**/*.pngpublic/images/about/7.pngis excluded by!**/*.pngpublic/images/about/8.pngis excluded by!**/*.pngpublic/images/about/animal01.pngis excluded by!**/*.pngpublic/images/about/animal02.pngis excluded by!**/*.pngpublic/images/about/animal03.pngis excluded by!**/*.pngpublic/images/about/mockup01.pngis excluded by!**/*.pngpublic/images/about/mockup02.pngis excluded by!**/*.pngpublic/images/about/mockup03.pngis excluded by!**/*.pngpublic/images/about/mockup04.pngis excluded by!**/*.pngpublic/images/about/s1_bg copy.svgis excluded by!**/*.svgpublic/images/about/s1_bg.svgis excluded by!**/*.svgpublic/images/characters-actions/Judge Owl-striking gavel down.gifis excluded by!**/*.gifpublic/images/characters-actions/Judge Owl-striking_gavel_down.pngis excluded by!**/*.png
📒 Files selected for processing (20)
.gitignorepackage.jsonprisma/schema.prismapublic/images/characters-actions/README (1).txtsrc/app/(page)/about/page.module.scsssrc/app/(page)/about/page.tsxsrc/app/(page)/contact/page.tsxsrc/app/(page)/landing/.gitkeepsrc/app/(page)/layout.tsxsrc/app/(page)/mypage/page.tsxsrc/app/api/chatbot/route.tssrc/app/layout.tsxsrc/components/about/SpriteAnimation.module.scsssrc/components/about/SpriteAnimation.tsxsrc/components/chatbot/Chatbot.module.scsssrc/components/chatbot/Chatbot.tsxsrc/domains/common/chatbot.service.tssrc/lib/ai/chatbot.tssrc/middleware.tssrc/styles/abstracts/_variables.scss
💤 Files with no reviewable changes (1)
- src/app/(page)/contact/page.tsx
| src/app/**/*.module.css | ||
| src/components/**/*.module.css |
There was a problem hiding this comment.
📐 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$' || trueRepository: I5-Project/TALKY-OWL
Length of output: 403
Lines 39-40의 ignore 패턴은 소스 CSS Module을 자동으로 제외하는 활성 규칙입니다.
검증 결과, src/app/**/*.module.css와 src/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.
| 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.
| const REVOLVING_WORDS = ['남친', '친구', '상사', '엄마']; | ||
| const REVOLVING_ORDER = [2, 3, 0, 1]; | ||
| const CYCLE_DURATION = 2000; |
There was a problem hiding this comment.
🎯 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
| const body = await req.json() | ||
| const { message } = body as { message: string } |
There was a problem hiding this comment.
🩺 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
| {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> | ||
| </> |
There was a problem hiding this comment.
🎯 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.
| 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 }, | ||
| }) |
There was a problem hiding this comment.
🗄️ 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.
| $font-family-base: | ||
| 'Pretendard', | ||
| -apple-system, | ||
| BlinkMacSystemFont, | ||
| 'Segoe UI', | ||
| sans-serif; |
There was a problem hiding this comment.
📐 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.
| $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
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/lib/ai/chatbot.ts (1)
83-88: 🩺 Stability & Availability | 🔵 Trivial상태 코드 기반 재시도 판별로 변경하기
error.message부분 문자열 매칭은 SDK 메시지 포맷 변경에 취약합니다.@google/generative-ai0.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
📒 Files selected for processing (1)
src/lib/ai/chatbot.ts
| 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 |
There was a problem hiding this comment.
🩺 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:
- 1: https://github.com/google-gemini/generative-ai-js/blob/main/src/methods/chat-session.ts
- 2: https://github.com/firebase/firebase-js-sdk/blob/a3837700/packages/ai/src/methods/chat-session.ts
- 3: After an API error, all subsequent calls fail with the same error google-gemini/deprecated-generative-ai-js#417
- 4: After an error on sendMessage, all subsequent calls fail with the same error googleapis/js-genai#550
- 5: google-gemini/deprecated-generative-ai-js@58f208d
- 6: fix: After an API error, all subsequent calls fail with the same error google-gemini/deprecated-generative-ai-js#420
- 7: https://github.com/google-gemini/generative-ai-js/blob/main/docs/reference/main/generative-ai.chatsession.sendmessage.md
- 8: https://deepwiki.com/googleapis/js-genai/12.4-retry-logic
🏁 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 -20Repository: 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`"'
fiRepository: 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:
- 1: https://github.com/google-gemini/deprecated-generative-ai-js/releases/tag/v0.21.0
- 2: https://newreleases.io/project/npm/@google/generative-ai/release/0.21.0
- 3: https://raw.githubusercontent.com/google/generative-ai-js/0c8d8cde00a33ea9da94e5d1bb033dc261d0588f/CHANGELOG.md
- 4: fix(core): GeminiChat.sendMessageStream deadlocks session and corrupts history when returned generator is never iterated google-gemini/gemini-cli#22521
- 5: After an error on sendMessage, all subsequent calls fail with the same error googleapis/js-genai#550
- 6: fix: After an error on sendMessage, all subsequent calls fail with the same error googleapis/js-genai#551
- 7:
sendMessageStreamis causing unhandled promise rejections when it errors, even if the errors are caught by the caller googleapis/js-genai#487 - 8: https://github.com/googleapis/js-genai/blob/v2.9.0-rc.0/src/chats.ts
🌐 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:
- 1: fix: After an API error, all subsequent calls fail with the same error google-gemini/deprecated-generative-ai-js#420
- 2: fix: After an API error, all subsequent calls fail with the same error google-gemini/deprecated-generative-ai-js#420
- 3: After an API error, all subsequent calls fail with the same error google-gemini/deprecated-generative-ai-js#417
- 4: google-gemini/deprecated-generative-ai-js@58f208d
- 5: https://deepwiki.com/googleapis/js-genai/12.4-retry-logic
- 6: https://docs.cloud.google.com/vertex-ai/generative-ai/docs/retry-strategy
🌐 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:
- 1: https://github.com/google-gemini/deprecated-generative-ai-js/blob/main/CHANGELOG.md
- 2: https://raw.githubusercontent.com/google/generative-ai-js/0c8d8cde00a33ea9da94e5d1bb033dc261d0588f/CHANGELOG.md
- 3: https://registry.npmjs.org/@google/generative-ai
- 4: After an API error, all subsequent calls fail with the same error google-gemini/deprecated-generative-ai-js#417
- 5: google-gemini/deprecated-generative-ai-js@58f208d
- 6: https://npmx.dev/package-changelog/@google/generative-ai/v/0.21.0
- 7: https://newreleases.io/project/npm/@google/generative-ai/release/0.21.0
🌐 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:
- 1: https://newreleases.io/project/npm/@google/generative-ai/release/0.24.1
- 2: https://github.com/google-gemini/deprecated-generative-ai-js/releases/tag/v0.24.1
- 3: https://registry.npmjs.org/@google/generative-ai
- 4: google-gemini/deprecated-generative-ai-js@v0.24.0...v0.24.1
- 5: https://github.com/google-gemini/deprecated-generative-ai-js/releases
- 6: feat: Add Grounding with Gemini Notebook GoogleCloudPlatform/generative-ai#420
0.21.0 버전의 ChatSession이 재시도 시 실제로 API를 호출하지 못합니다.
@google/generative-ai 버전 0.21.0(2024년 10월 출시)에는 ChatSession의 내부 _sendPromise가 에러 발생 후 reject 상태로 유지되는 문제가 있습니다. 이로 인해 네트워크 장애나 429/503 같은 일시적 오류 발생 시 다음 sendMessage 호출이 새로운 네트워크 요청을 시도하지 않고 직전 에러를 즉시 다시 throw하게 됩니다. 결과적으로 재시도 루프는 sleep만 반복하다 같은 에러로 종료됩니다.
두 가지 해결 방법이 있습니다:
-
매 시도마다 새
chat세션 생성 (현재 제시된 방식): 루프 내부에서model.startChat()을 호출하여 매번 새로운ChatSession인스턴스를 만들면 이 문제를 회피할 수 있습니다. -
의존성 업그레이드:
@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.
There was a problem hiding this comment.
이 이미지들은 랜딩관련 이미지들인데 왜 다시 올리시는건가요?
There was a problem hiding this comment.
클로드가 착각했나 봅니다 수정하겠슴다~
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>
67dbd68 to
4f3e1ba
Compare
- role 대소문자 불일치 수정 (toLowerCase 비교) - Gemini 실패(502), 타임아웃(504) 에러 유형 분리 - 30초 요청 타임아웃 추가 - 히스토리 조회 최근 30개로 제한 - DELETE 실패 시 사용자 안내 메시지 추가 - 히스토리 로드 실패 시 재시도 가능하도록 수정 Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
src/app/api/chatbot/route.tssrc/components/chatbot/Chatbot.tsxsrc/domains/common/chatbot.service.tssrc/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
| function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> { | ||
| return Promise.race([ | ||
| promise, | ||
| new Promise<never>((_, reject) => | ||
| setTimeout(() => reject(new ChatbotTimeoutError()), ms) | ||
| ), | ||
| ]) |
There was a problem hiding this comment.
🩺 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.
Summary
변경 사항
신규
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
🤖 Generated with Claude Code
Summary by CodeRabbit
릴리스 노트