Skip to content

[FEAT] AI 독후감 구현#286

Merged
ljh130334 merged 8 commits into
developfrom
feat/aiwriting
Oct 29, 2025
Merged

[FEAT] AI 독후감 구현#286
ljh130334 merged 8 commits into
developfrom
feat/aiwriting

Conversation

@ljh130334
Copy link
Copy Markdown
Member

@ljh130334 ljh130334 commented Oct 28, 2025

#️⃣ 연관된 이슈

#285

📝 작업 내용

1️⃣ 플로팅 버튼 메뉴 추가

  • 기록장 페이지의 플로팅 버튼에 "AI 독서 감상문 생성" 메뉴 추가
  • 기존 "기록 작성", "투표 생성" 아래에 세 번째 옵션으로 배치
  • 클릭 시 확인 모달 표시

2️⃣ AI 독서감상문 생성 확인 모달

  • ConfirmModal 컴포넌트 활용
  • 제목: "AI 독서감상문 생성 (Beta)"
  • 설명: 기록 기반 생성 안내 + 잔여 이용횟수 표시 (n/5)
  • 버튼 텍스트 커스터마이징 기능 추가 (취소/확인)
  • 모달 외부 클릭 시 닫기 기능 구현

3️⃣ AI 독서감상문 로딩 페이지

  • /aiwrite/:roomId 라우트 추가
  • 5초 로딩 스피너 표시
  • "독서 감상문을 생성중이에요!" + "조금만 기다려주세요" 메시지
  • TitleHeader 사용하여 일관된 헤더 UI 구현

4️⃣ AI 독서감상문 결과 화면

  • 정보 배너: "내 기록과 총평을 바탕으로 생성된 감상문입니다."
  • 생성된 감상문: 스크롤 가능한 컨텐츠 영역
  • 클립보드에 복사 버튼: 하단 고정 (보라색)
    • 클립보드 복사 성공 시 "클립보드에 복사가 완료되었어요" 스낵바 표시
    • 복사 실패 시 에러 스낵바 표시

5️⃣ 뒤로가기 확인 모달

  • AI 독서감상문 페이지에서 뒤로가기 클릭 시 확인 모달 표시
  • "생성된 감상문은 다시 볼 수 없으며, 잔여 이용횟수는 차감돼요. 계속하시겠어요?"
  • 확인 시 기록장으로 이동, 취소/외부 클릭 시 페이지 유지

6️⃣ 목업 데이터 및 파일 구조

  • src/mocks/aiwrite.mock.ts 생성 - 레이 커즈와일 "특이점이 시작된다" 서평 형식
  • src/pages/aiwrite/AIWrite.tsx 생성 - 메인 페이지 컴포넌트

주요 변경 파일

  • src/components/memory/MemoryAddButton/MemoryAddButton.tsx - AI 생성 메뉴 추가
  • src/stores/usePopupStore.ts - ConfirmModal props에 버튼 텍스트 커스터마이징 추가
  • src/components/common/Modal/ConfirmModal.tsx - confirmText, cancelText props 추가
  • src/components/common/Modal/PopupContainer.tsx - 외부 클릭 시 모달 닫기 기능
  • src/pages/index.tsx - 라우팅 추가
  • src/mocks/aiwrite.mock.ts - 목업 데이터
  • src/pages/aiwrite/AIWrite.tsx - AI 독서감상문 페이지

스크린샷 (선택)

2025-10-28.9.30.15.mov

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • AI 독서 감상문 생성 기능 추가: 메모리 추가 버튼에서 AI 생성 옵션 선택 가능
    • 생성된 감상문을 클립보드에 복사 기능 추가
  • 개선사항

    • 확인 모달의 버튼 텍스트 커스터마이징 지원
    • 모달 배경 클릭 동작 개선으로 더 나은 사용성 제공

@ljh130334 ljh130334 requested review from heeeeyong and ho0010 October 28, 2025 12:31
@ljh130334 ljh130334 self-assigned this Oct 28, 2025
@ljh130334 ljh130334 added ✨ Feature 기능 개발 🎨 Html&css 마크업 & 스타일링 labels Oct 28, 2025
@vercel
Copy link
Copy Markdown

vercel Bot commented Oct 28, 2025

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

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Oct 28, 2025 0:31am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Oct 28, 2025

Walkthrough

AI 작문 생성 기능을 추가합니다. 사용자가 메모리 추가 버튼에서 "AI 독서 감상문 생성"을 선택하면 확인 모달이 나타나고, 확인 후 AIWrite 페이지로 이동하여 로딩 후 생성된 내용을 표시하고 클립보드에 복사할 수 있습니다. 또한 ConfirmModal의 버튼 텍스트를 커스터마이징 가능하게 변경했습니다.

Changes

Cohort / File(s) 변경 내용
ConfirmModal 커스터마이징
src/components/common/Modal/ConfirmModal.tsx, src/stores/usePopupStore.ts
confirmText/cancelText 선택 프로퍼티 추가로 버튼 텍스트 커스터마이징 가능. ConfirmModal에 container click 핸들러 추가하여 외부 클릭 전파 방지.
PopupContainer 오버레이 동작
src/components/common/Modal/PopupContainer.tsx
confirm-modal 렌더링 시 Wrapper의 onClick 핸들러로 popup 닫기 기능 추가.
MemoryAddButton AI 옵션
src/components/memory/MemoryAddButton/MemoryAddButton.tsx
드롭다운 메뉴에 "AI 독서 감상문 생성" 옵션 추가. usePopupActions 훅으로 openConfirm/closePopup 활용하여 확인 모달 표시 및 /aiwrite/{roomId}로 네비게이션.
AIWrite 페이지 및 라우팅
src/pages/aiwrite/AIWrite.tsx, src/pages/index.tsx
새로운 AIWrite 컴포넌트 추가로 5초 로딩 후 생성된 내용 표시. 클립보드 복사, 뒤로 가기 (확인 모달 포함) 기능 구현. pages/index.tsx에 "aiwrite/:roomId" 라우트 등록.
Mock 데이터
src/mocks/aiwrite.mock.ts
MOCK_AI_WRITING 상수 추가로 개발/테스트용 한국어 에세이 템플릿 제공.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MemoryAddButton
    participant PopupStore
    participant AIWrite as AIWrite Page
    participant Clipboard

    User->>MemoryAddButton: "AI 독서 감상문 생성" 선택
    MemoryAddButton->>PopupStore: openConfirm() 호출
    PopupStore->>User: 확인 모달 표시
    
    User->>PopupStore: 확인 클릭
    PopupStore->>PopupStore: closePopup()
    PopupStore->>AIWrite: /aiwrite/{roomId}로 네비게이션
    
    AIWrite->>AIWrite: 5초 로딩 상태 표시
    AIWrite->>AIWrite: 로딩 완료 후 결과 뷰 표시
    
    User->>AIWrite: "클립보드에 복사" 버튼 클릭
    AIWrite->>Clipboard: MOCK_AI_WRITING 복사
    Clipboard-->>AIWrite: 성공/실패 응답
    AIWrite->>User: 스낵바 메시지 표시

    User->>AIWrite: 뒤로 가기 클릭
    AIWrite->>PopupStore: openConfirm() 호출
    PopupStore->>User: 나가기 확인 모달 표시
    User->>PopupStore: 확인 클릭
    PopupStore->>AIWrite: /rooms/{roomId}/memory로 네비게이션
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 분

주의할 사항:

  • AIWrite.tsx: 새로운 페이지 컴포넌트의 로딩 상태 관리, Emotion 스타일 구현, MOCK_AI_WRITING 활용 등을 확인
  • MemoryAddButton.tsx: usePopupActions 훅 사용 및 네비게이션 로직이 정확하게 구현되었는지 검증
  • PopupContainer.tsx: confirm-modal의 click 핸들러 추가로 다른 모달 타입에 영향을 주지 않는지 확인
  • 라우팅: pages/index.tsx의 새로운 라우트가 기존 라우팅 구조와 충돌하지 않는지 검증

Possibly related issues

  • [FEAT] AI 독후감 - 지현 #285: AI 작문 생성 기능 구현 (AIWrite 페이지, MemoryAddButton 메뉴 추가, 라우팅, Mock 데이터) 및 ConfirmModal 커스터마이징이 이 이슈의 요구사항과 직접 대응

Poem

🐰 꼬물꼬물 AI의 손으로
감상문 술술 나오나니,
클립보드에 쏘옥 담고나면
텍스트는 반짝반짝 ✨
정성스런 수정과 함께라면
완성도 높아지리!

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed PR 제목 "[FEAT] AI 독후감 구현"은 변경 내용의 주요 목표를 명확하게 반영하고 있습니다. 제목은 AI 기반 독서감상문 생성 기능 추가라는 changeset의 핵심 변경사항을 직접 나타내며, 간결하고 구체적입니다. "FEAT" 접두사로 새 기능임을 명시하고, 불필요한 파일 목록이나 emoji 없이 깔끔하게 작성되었습니다. MemoryAddButton, ConfirmModal, AIWrite 페이지 등의 지원 변경사항들이 모두 이 주요 목표를 구현하기 위한 것으로, 제목이 changeset을 정확하게 요약합니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/aiwriting

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

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/components/common/Modal/PopupContainer.tsx (1)

44-44: 백드롭 클릭으로 모달이 닫히는 동작 확인 필요

WrapperonClick={closePopup}을 추가하여 모달 외부 클릭 시 닫히도록 구현했습니다. 일반적인 UX 패턴이지만, AIWrite 페이지의 뒤로가기 확인 모달처럼 중요한 경고("생성된 감상문은 다시 볼 수 없으며...")가 있는 경우 사용자가 실수로 백드롭을 클릭해 모달을 닫을 수 있습니다.

중요한 확인 모달의 경우 명시적인 버튼 클릭만 허용하는 것을 고려해보세요. 예를 들어, ConfirmModalPropsdisableBackdropClick?: boolean 옵션을 추가하거나, 중요한 작업의 경우 별도의 모달 타입을 사용하는 방법이 있습니다.

src/pages/aiwrite/AIWrite.tsx (2)

166-179: 모바일 세이프 에리어 고려 필요

CopyButton이 화면 하단에 고정(position: fixed; bottom: 0)되어 있지만, 최신 모바일 기기의 홈 인디케이터나 노치를 고려하지 않았습니다. 일부 기기에서 버튼이 시스템 UI에 가려질 수 있습니다.

CSS safe-area-inset 적용을 권장합니다:

 const CopyButton = styled.button`
   position: fixed;
   bottom: 0;
+  bottom: env(safe-area-inset-bottom);
+  padding-bottom: calc(env(safe-area-inset-bottom) / 2);
   width: 100%;
   height: 50px;

또는 height를 동적으로 조정:

 const CopyButton = styled.button`
   position: fixed;
   bottom: 0;
   width: 100%;
-  height: 50px;
+  height: calc(50px + env(safe-area-inset-bottom));
+  padding-bottom: env(safe-area-inset-bottom);
   background-color: ${colors.purple.main};

129-135: 모바일 키보드 환경 고려 (선택사항)

Line 133에서 height: calc(100vh - 56px)를 사용하고 있습니다. 모바일에서 키보드가 표시될 때 100vh는 키보드를 포함한 전체 높이를 참조하여, 일부 콘텐츠가 키보드에 가려질 수 있습니다.

현재는 overflow-y: auto로 스크롤이 가능하므로 큰 문제는 아니지만, 필요시 CSS dvh (Dynamic Viewport Height) 단위를 고려할 수 있습니다 (최신 브라우저에서 지원).

-  height: calc(100vh - 56px);
+  height: calc(100dvh - 56px); /* 또는 min-height 사용 */

브라우저 호환성 확인 후 적용하시기 바랍니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 154caf7 and 58352ae.

⛔ Files ignored due to path filters (2)
  • src/assets/common/infoIcon_white.svg is excluded by !**/*.svg
  • src/assets/memory/ai.svg is excluded by !**/*.svg
📒 Files selected for processing (7)
  • src/components/common/Modal/ConfirmModal.tsx (1 hunks)
  • src/components/common/Modal/PopupContainer.tsx (1 hunks)
  • src/components/memory/MemoryAddButton/MemoryAddButton.tsx (3 hunks)
  • src/mocks/aiwrite.mock.ts (1 hunks)
  • src/pages/aiwrite/AIWrite.tsx (1 hunks)
  • src/pages/index.tsx (2 hunks)
  • src/stores/usePopupStore.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/components/common/Modal/ConfirmModal.tsx (1)
src/stores/usePopupStore.ts (1)
  • ConfirmModalProps (12-19)
src/components/memory/MemoryAddButton/MemoryAddButton.tsx (2)
src/hooks/usePopupActions.ts (1)
  • usePopupActions (9-35)
src/components/memory/MemoryAddButton/MemoryAddButton.styled.ts (1)
  • DropdownItem (67-94)
src/pages/aiwrite/AIWrite.tsx (3)
src/hooks/usePopupActions.ts (1)
  • usePopupActions (9-35)
src/mocks/aiwrite.mock.ts (1)
  • MOCK_AI_WRITING (1-19)
src/styles/global/global.ts (2)
  • colors (4-53)
  • typography (56-77)
🪛 ast-grep (0.39.6)
src/components/common/Modal/ConfirmModal.tsx

[warning] 19-19: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
src/components/common/Modal/ConfirmModal.tsx

[error] 20-20: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🔇 Additional comments (8)
src/pages/index.tsx (1)

43-43: LGTM!

라우팅 설정이 올바르게 구현되었습니다. 기존 패턴과 일관성 있게 AIWrite 컴포넌트를 추가했습니다.

Also applies to: 68-68

src/stores/usePopupStore.ts (1)

17-18: LGTM!

ConfirmModalProps에 선택적 프로퍼티를 추가하여 모달 버튼 텍스트를 커스터마이징할 수 있게 했습니다. 기존 코드와의 하위 호환성이 유지됩니다.

src/components/memory/MemoryAddButton/MemoryAddButton.tsx (1)

90-93: LGTM!

AI 독서 감상문 생성 드롭다운 항목이 올바르게 추가되었습니다. 아이콘과 핸들러 연결이 적절합니다.

src/mocks/aiwrite.mock.ts (1)

1-19: LGTM!

AI 작문 기능의 목업 데이터가 적절하게 구현되었습니다. 개발 및 테스트에 사용하기에 충분한 길이와 내용을 가지고 있습니다.

src/components/common/Modal/ConfirmModal.tsx (1)

5-15: LGTM!

커스터마이징 가능한 버튼 텍스트와 stopPropagation 처리가 올바르게 구현되었습니다. 기본값('예', '아니요')도 적절하며, 모달 컨텐츠 클릭 시 백드롭 클릭 이벤트 전파를 차단하는 로직이 정확합니다.

Also applies to: 23-26

src/pages/aiwrite/AIWrite.tsx (3)

18-24: LGTM!

5초 로딩 타이머가 올바르게 구현되었으며, cleanup 함수로 메모리 누수를 방지하고 있습니다.


39-56: LGTM!

클립보드 복사 기능이 올바르게 구현되었습니다. 에러 처리와 사용자 피드백(스낵바)이 적절합니다. navigator.clipboard API는 HTTPS 환경에서 작동하므로, 로컬 개발 환경에서는 localhost를 사용하고 프로덕션은 HTTPS를 사용하면 됩니다.


58-86: 전반적으로 잘 구현되었습니다!

AIWrite 컴포넌트의 전체 구조와 로직이 깔끔하게 구현되었습니다. 로딩 상태 관리, 조건부 렌더링, 그리고 팝업 시스템 통합이 모두 적절합니다. 위에서 언급한 이용횟수 표시와 모바일 세이프 에리어 이슈만 개선하면 프로덕션에 적합할 것으로 보입니다.

<Container>
<Container onClick={handleContainerClick}>
<div className="title">{title}</div>
<div className="disc" dangerouslySetInnerHTML={{ __html: disc }} />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

XSS 취약점 방지를 위한 HTML 새니타이제이션 필요

Line 20에서 dangerouslySetInnerHTML을 사용하여 disc 프로퍼티를 렌더링하고 있습니다. 현재는 정적 문자열(<br/> 태그 포함)만 전달되므로 안전하지만, 향후 사용자 입력이나 외부 데이터가 포함될 경우 XSS(Cross-Site Scripting) 공격에 취약해질 수 있습니다.

DOMPurify 라이브러리를 사용하여 HTML을 새니타이징하는 것을 권장합니다:

+import DOMPurify from 'dompurify';
+
 const ConfirmModal = ({
   title,
   disc,
   onConfirm,
   onClose,
   confirmText = '예',
   cancelText = '아니요',
 }: ConfirmModalProps) => {
   const handleContainerClick = (e: React.MouseEvent) => {
     e.stopPropagation();
   };
+
+  const sanitizedDisc = DOMPurify.sanitize(disc);

   return (
     <Container onClick={handleContainerClick}>
       <div className="title">{title}</div>
-      <div className="disc" dangerouslySetInnerHTML={{ __html: disc }} />
+      <div className="disc" dangerouslySetInnerHTML={{ __html: sanitizedDisc }} />
       <ButtonContainer>

DOMPurify 설치:

npm install dompurify
npm install --save-dev @types/dompurify
🧰 Tools
🪛 Biome (2.1.2)

[error] 20-20: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🤖 Prompt for AI Agents
In src/components/common/Modal/ConfirmModal.tsx around line 20, rendering disc
via dangerouslySetInnerHTML without sanitization risks XSS if disc ever contains
user/external input; import DOMPurify, sanitize the disc value (e.g., const
safeDisc = DOMPurify.sanitize(disc)) and use the sanitized string in
dangerouslySetInnerHTML, ensuring DOMPurify is installed and types added as dev
dependency.

Comment on lines +58 to +72
const handleAIWrite = () => {
setIsOpen(false);
openConfirm({
title: 'AI 독서감상문 생성 (Beta)',
disc: '기록장에서 작성한 기록을 기반으로<br/>독서감상문을 생성하시겠어요?<br/>(서비스 내 잔여 이용횟수 : n/5)',
confirmText: '확인',
cancelText: '취소',
onConfirm: () => {
closePopup();
const currentRoomId = roomId || '1';
navigate(`/aiwrite/${currentRoomId}`);
console.log('AI 독서 감상문 생성 시작 - roomId:', currentRoomId);
},
});
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

하드코딩된 이용횟수를 동적 데이터로 대체 필요

Line 62에서 "(서비스 내 잔여 이용횟수 : n/5)"가 하드코딩되어 있습니다. 실제 사용자의 잔여 이용횟수를 서버나 상태 관리에서 가져와 동적으로 표시해야 합니다.

다음과 같이 수정할 수 있습니다:

+ // 사용자의 실제 이용횟수를 가져오는 hook/API 호출 필요
+ const { remainingCount, totalCount } = useAIWritingQuota(); // 예시

  const handleAIWrite = () => {
    setIsOpen(false);
    openConfirm({
      title: 'AI 독서감상문 생성 (Beta)',
-     disc: '기록장에서 작성한 기록을 기반으로<br/>독서감상문을 생성하시겠어요?<br/>(서비스 내 잔여 이용횟수 : n/5)',
+     disc: `기록장에서 작성한 기록을 기반으로<br/>독서감상문을 생성하시겠어요?<br/>(서비스 내 잔여 이용횟수 : ${remainingCount}/${totalCount})`,
      confirmText: '확인',
      cancelText: '취소',
      onConfirm: () => {

참고: Line 62의 disc는 ConfirmModal에서 dangerouslySetInnerHTML로 렌더링되므로, 사용자 입력이 포함된 데이터를 사용할 경우 XSS 방어를 위해 sanitize 처리가 필요합니다. 현재는 정적 문자열이므로 안전하지만, 동적 데이터 추가 시 주의하세요.

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

Comment on lines +26 to +37
const handleBackClick = () => {
openConfirm({
title: 'AI 독서감상문 생성 (Beta)',
disc: '생성된 감상문은 다시 볼 수 없으며, 잔여 이용횟수는 차감돼요. 계속하시겠어요?',
confirmText: '확인',
cancelText: '취소',
onConfirm: () => {
closePopup();
navigate(`/rooms/${roomId}/memory`);
},
});
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

이용횟수 관련 메시지 일관성 개선 필요

Line 29에서 "잔여 이용횟수는 차감돼요"라는 메시지를 표시하고 있지만, 실제 잔여 횟수를 보여주지 않습니다. MemoryAddButton에서는 "n/5" 형태로 표시하고 있어 일관성이 부족합니다.

사용자에게 현재 잔여 횟수를 명확히 알려주는 것이 더 나은 UX입니다.

  const handleBackClick = () => {
+   // 실제 이용횟수를 가져오는 hook/API 필요
+   const { remainingCount, totalCount } = useAIWritingQuota(); // 예시
+
    openConfirm({
      title: 'AI 독서감상문 생성 (Beta)',
-     disc: '생성된 감상문은 다시 볼 수 없으며, 잔여 이용횟수는 차감돼요. 계속하시겠어요?',
+     disc: `생성된 감상문은 다시 볼 수 없으며, 잔여 이용횟수가 차감됩니다 (현재: ${remainingCount}/${totalCount}). 계속하시겠어요?`,
      confirmText: '확인',

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

@ljh130334 ljh130334 merged commit 390eac5 into develop Oct 29, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발 🎨 Html&css 마크업 & 스타일링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant