From 4caac03e62475251eacaca54ae42ddc9053bb907 Mon Sep 17 00:00:00 2001 From: Documentation Bot Date: Mon, 8 Dec 2025 07:27:49 +0000 Subject: [PATCH] docs: Sync documentation from private repository --- PROJECT_STRUCTURE.md | 113 +++++++-- ROADMAP.md | 12 +- STYLE_GUIDE.md | 70 ++++++ TECH_STACK.md | 546 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 722 insertions(+), 19 deletions(-) diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index c52ba7e..55b5d2b 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -1,10 +1,69 @@ # 라온누리 - 프로젝트 구조 -> 최종 업데이트: 2025-12-03 (사용자 설정 기능 추가) +> 최종 업데이트: 2025-12-08 (AI 크레딧 환불 시스템, 구매 플로우, 구독 관리) 초등학생을 위한 창작 글쓰기 교육 플랫폼 -**최신 업데이트** (2025-12-03): +**최신 업데이트** (2025-12-08 오후): +- 💳 **AI 크레딧 환불 시스템** + - **업그레이드 환불**: Prorated 계산 → AI 크레딧 지급 + - **환율**: 100원 = 10 크레딧 + - **AI 기능 비용**: 분석 20, 이미지 100, 도우미 10 크레딧 + - **FirestoreUser.aiCredits**: 영구 사용, 월 제한 무시 + - **credits.ts**: calculateProratedRefund(), isUpgrade(), PLAN_MONTHLY_PRICES + - **planLimits.ts**: AI_FEATURE_COSTS, CREDIT_EXCHANGE_RATE, convertKRWToCredits() +- 🛒 **구매 플로우 구현** + - **PurchaseConfirmationDialog**: 플랜 정보, 가격 포맷팅, 월간/연간 배지 + - **POST /api/user/purchase**: Mock 승인 + 환불 계산 + 크레딧 지급 + 플랜 업데이트 (원자적) + - **UserManager.purchasePlan()**: creditsAdded 반환 + - **pricing 페이지**: 로그인 체크, 플랜별 분기, 크레딧 안내 toast + - **향후 Toss Payments 연동 준비**: 주석으로 연동 포인트 표시 +- 📊 **구독 관리 UI** + - **UserSettingsDialog 구독 탭**: 현재 플랜, 플랜 제한, 실시간 사용량, AI 크레딧 잔액 + - **SettingBox 컴포넌트**: 투명 배경, 얇은 테두리, 블러 효과 + - **SettingButton 컴포넌트**: 브랜드 테두리, 호버 그라데이션 + - **업그레이드 버튼**: 새 창, locale 포함 URL +- 🔗 **팀 API 통합** + - **GET /api/team/[teamId]**: 보안 로직 통합 (공개 팀 + 멤버 체크) + - **/public 엔드포인트 삭제**: 중복 제거 + - **TeamCard AI 배지**: team.aiEnabled 표시 +- 🌐 **다국어 지원**: 20+ 키 추가 (ko/en/ja) + +**최신 업데이트** (2025-12-08 오전): +- 🤖 **팀 AI 2단계 계층 구조** + - **마스터 스위치**: `team.aiEnabled` (팀 전체 AI 기능 활성화) + - **하위 옵션**: `team.aiAssistanceConfig.enabled` (글쓰기 도우미) + - **플랜 제한**: `maxAIEnabledTeams` (Free: 0개, Classroom: 1개, Academy: 5개, School: 무제한) + - **TeamAISettings 컴포넌트**: 2단계 UI (마스터 OFF 시 도우미 숨김) + - **3가지 상태 피드백**: 활성화 / 플랜 제한 / 비활성화 + - **API 권한 체크**: PUT /api/team/[teamId] aiEnabled 변경 시 플랜 검증 + - **다국어 지원**: team.manage.teamAI namespace (ko/en/ja 6개 키) +- 🎴 **TeamCard 컴포넌트 통합** + - **리디자인**: 내 팀 페이지 스타일 (bg.subtle, border, hover 효과) + - **커버 이미지**: team.coverImage 140px 높이 표시 + - **팀 설명**: team.description 2줄 lineClamp + - **Props**: isOwner, onClick 추가 + - **적용 범위**: `/team` (내 팀 목록), `/team/all` (공개 팀 목록) + - **인라인 함수 제거**: renderTeamCard → TeamCard 컴포넌트로 전환 +- 🔒 **공개 팀 코드 보안 강화** + - **Team.code**: `string` → `string?` (optional) + - **서버 API**: getPublicTeams() 전체 코드 제거, GET /api/team/[teamId]/public 멤버 아닌 경우 제거 + - **TeamCard**: 코드 없으면 숨김 (조건부 렌더링) + - **타입 안전성**: StudentLoginFlow, TeamInfoCard, firebaseAuth.ts 수정 + +**업데이트** (2025-12-04): +- 🔀 **글쓰기 페이지 라우트 분리** + - **기존**: `/write?mode=wrt`, `/write?mode=img`, `/write?id=xxx` (1,540줄 단일 파일) + - **변경**: 4개 독립 라우트로 분리 + - `/write` - 모드 선택 화면 (~140줄) + - `/write/text` - 글 먼저 모드 (~800줄) + - `/write/image` - 그림 먼저 모드 (~900줄) + - `/write/edit/[id]` - 수정 모드 (~700줄) + - **하위 호환성**: 기존 URL은 새 URL로 자동 리다이렉트 + - **ModeSelectionCard 업데이트**: 라우트 URL 변경 + - **WritingCard 업데이트**: 수정 링크 `/write/edit/${id}`로 변경 + +**업데이트** (2025-12-03): - ⚙️ **사용자 설정 다이얼로그** (`UserSettingsDialog.tsx`) - **프로필 정보 수정**: 이름, 프로필 사진 업로드 (드래그앤드롭, Canvas 리사이즈) - **환경 설정**: 테마 (라이트/다크), 주간 목표 (1~10개) @@ -146,7 +205,7 @@ - 기존 단순 배지 → 전체 UI 테마로 업그레이드 - 🔒 **수정 모드 주제 변경 차단** - TopicSelector에 `readonly` prop 추가 - - 수정 모드에서는 Select 드롭다운 숨김, "현재 주제" 라벨만 표시 + - 수정 모드에서는 Dialog 트리거 버튼 숨김, "현재 주제" 라벨만 표시 - 데이터 무결성 보장 (템플릿 덮어쓰기 방지) - AI 설정/실시간 모니터링 혼선 방지 - 다국어 지원: `readonlyNote`, `currentTopic` (ko, en, ja) @@ -326,7 +385,7 @@ - 📝 다중 글조각 관리 시스템 (DraftManager, SavedDraftsDialog) - 💾 강화된 자동 저장 (2초 debounce, 저장 상태 표시) - 🎨 테마 슬롯 레시피 추가 (Dialog, Select 자동 배경색) -- 📋 TopicSelector 그룹핑 (자유/팀/개인 주제 구분, 팀 이름 표시) +- 📋 TopicSelector Dialog 리디자인 (glassmorphism, 탭 기반 그룹핑, 미리보기 7:3 레이아웃) - 🔐 5단계 보안 레벨 시스템 (팀별 보안 정책 선택) - 📦 User 타입 분리 (FirestoreUser / User) - 🏷️ 닉네임 저장 위치 변경 (team.members[uid].nickname) @@ -424,7 +483,9 @@ | 컴포넌트 | 파일명 | 설명 | 상태 | |---------|--------|------|------| -| **UserSettingsDialog** | `UserSettingsDialog.tsx` | 사용자 설정 다이얼로그 (프로필/환경설정) | ✅ 완료 | +| **UserSettingsDialog** | `UserSettingsDialog.tsx` | 🆕 **사용자 설정 다이얼로그 (프로필/내 구독/환경설정)** - 3개 탭, 현재 플랜 표시, 플랜 제한 정보, AI 크레딧 잔액, 실시간 사용량, 업그레이드 버튼 (새 창, locale 포함) | ✅ 완료 | +| **SettingBox** | `SettingBox.tsx` | 🆕 **설정 박스 컴포넌트** - 투명 배경 (3% opacity), 얇은 테두리 (whiteAlpha.200), 블러 효과 (blur 10px), BoxProps 확장 | ✅ 완료 | +| **SettingButton** | `SettingButton.tsx` | 🆕 **설정 버튼 컴포넌트** - 투명 배경 (5% opacity), 브랜드 테두리, 호버 그라데이션, ButtonProps 확장 | ✅ 완료 | **주요 기능** (2025-12-03): - ✅ 프로필 정보 수정 @@ -537,7 +598,7 @@ | 컴포넌트 | 파일명 | 설명 | 상태 | |---------|--------|------|------| | **WritingEditor** | `WritingEditor.tsx` | Tiptap 기반 순수 텍스트 에디터 (하이라이트 통합) | ✅ 완료 | -| **TopicSelector** | `TopicSelector.tsx` | 주제 선택 드롭다운 (그룹핑, 팀 이름 표시) | ✅ 완료 | +| **TopicSelector** | `TopicSelector.tsx` | 🔄 **주제 선택 Dialog** (glassmorphism, 탭 기반 그룹핑, 미리보기 패널, 7:3 레이아웃) | ✅ 완료 | | **EditorTooltip** | `EditorTooltip.tsx` | 🆕 **인터랙티브 툴팁** (맞춤법/감각 단어 클릭 시 표시) | ✅ 완료 | | **WritingPatternDialog** | `WritingPatternDialog.tsx` | 🆕 **글 작성 패턴 분석 다이얼로그** (최근 10개 글 분석) | ✅ 완료 | | **WritingPatternDisplay** | `WritingPatternDisplay.tsx` | 🆕 **패턴 분석 결과 표시** (종합 평가, 발전 추이, 강점/약점, 추천) | ✅ 완료 | @@ -687,9 +748,10 @@ | 컴포넌트 | 파일명 | 설명 | 상태 | |---------|--------|------|------| -| **TeamCard** | `TeamCard.tsx` | 🆕 **공개 팀 카드** (글래스모피즘, 보안 레벨 배지, 멤버 수, 🆕 커버 이미지 표시) | ✅ 완료 | +| **TeamCard** | `TeamCard.tsx` | 🆕 **팀 카드 컴포넌트** (내 팀/공개 팀 공용, 커버 이미지 140px, 팀 설명 2줄, 팀 코드 조건부 표시, isOwner/onClick props) | ✅ 완료 | | **TeamCoverImageUploader** | `TeamCoverImageUploader.tsx` | 🆕 **팀 커버 이미지 업로더** (드래그앤드롭, 미리보기, 16:9 AspectRatio, 5MB 제한) | ✅ 완료 | | **TeamTopicManager** | `TeamTopicManager.tsx` | 팀 주제 목록 및 생성/삭제 UI | ✅ 완료 | +| **TeamAISettings** | `TeamAISettings.tsx` | 🆕 **팀 AI 설정 컴포넌트** (2단계 계층: 팀 AI 마스터 스위치 + AI 글쓰기 도우미, 플랜 제한 체크, 3가지 상태 피드백, VStack 레이아웃) | ✅ 완료 | | **AIConfigDialog** | `AIConfigDialog.tsx` | 🆕 **AI 도우미 고급 설정 Dialog** (Slider, 커스텀 CheckboxCard) | ✅ 완료 | | **SecurityLevelSelector** | `SecurityLevelSelector.tsx` | 5단계 보안 레벨 선택 (RadioCard, framer-motion 애니메이션) | ✅ 완료 | | **TopicMemberAnalysisSection** | `TopicMemberAnalysisSection.tsx` | 🆕 **주제별 학생 글쓰기 분석** (Accordion, 주제별 학생 목록, 글 개수, by-topic 분석 연동) | ✅ 완료 | @@ -792,6 +854,9 @@ | **WritingSessionManager** | `WritingSessionManager.ts` | 🆕 **실시간 글쓰기 세션 관리** (Firebase Realtime DB 작업) | ✅ 완료 | | **WritingManager** | `WritingManager.ts` | 글쓰기 관련 비즈니스 로직 (CRUD, 통계) | ✅ 완료 | | **TopicManager** | `TopicManager.ts` | 주제 관련 비즈니스 로직 (CRUD, 템플릿 처리) | ✅ 완료 | +| **AIUsageManager** | `AIUsageManager.ts` | 🆕 **AI 사용량 관리** (플랜 정보, 사용량 조회, 기능 사용 가능 여부 체크, 1분 캐싱) | ✅ 완료 | +| **OrganizationManager** | `OrganizationManager.ts` | 🆕 **조직 관리** (Organization CRUD, 멤버 조회, 2분 캐싱) | ✅ 완료 | +| **FeedManager** | `FeedManager.ts` | 피드 관련 API 호출 (영감, 주간 목표, 팀 활동) | ✅ 완료 | | **index.ts** | `index.ts` | 모든 매니저 export | ✅ 완료 | | **LevelManager** | `LevelManager.ts` | 레벨/경험치 관리 | ❌ 미구현 | | **StickerManager** | `StickerManager.ts` | 스티커 획득/관리 | ❌ 미구현 | @@ -889,8 +954,9 @@ firebase functions:log --only cleanupExpiredReservations | 타입 | 파일명 | 설명 | 상태 | |------|--------|------|------| +| **Plan 타입** | `plan.ts` | 🆕 **플랜 시스템 타입** (PlanType/BillingCycle/PlanSource/AIFeatureType Enum, UserPlan, Organization, AIUsage, PlanLimits, EffectivePlan, AIFeatureCheckResult) | ✅ 완료 | | **Team 타입** | `team.ts` | 팀 데이터 모델 (members Map), **TeamSecurityLevel Enum (1-5)**, 🆕 **AIAssistanceConfig** (AI 도우미 설정) | ✅ 완료 | -| **FirestoreUser 타입** | `firestoreUser.ts` | **FirestoreUser** (DB 저장용), **User** (UI용) 분리 | ✅ 완료 | +| **FirestoreUser 타입** | `firestoreUser.ts` | **FirestoreUser** (DB 저장용, 🆕 **plan/organizationId 필드 추가**), **User** (UI용) 분리 | ✅ 완료 | | **Writing 타입** | `writing.ts` | 글 데이터 모델, 🆕 **WritingAnalysis** (AI 분석 결과, contentHash 기반 재사용), **SpellingError** (맞춤법 오류), 🆕 **AIAssistanceRecord** (AI 도움 이력), 🆕 **GeneratedImage** (AI 생성 이미지) | ✅ 완료 | | **Scene 타입** | `scene.ts` | 🆕 **장면 데이터 모델** (Scene, SceneExtractionResponse) | ✅ 완료 | | **Draft 타입** | `draft.ts` | 글조각 데이터 모델 (Draft, DraftListItem, **AnalysisHistoryItem**, **syncStatus**: 'local'\|'synced'\|'syncing') | ✅ 완료 | @@ -906,6 +972,9 @@ firebase functions:log --only cleanupExpiredReservations | ~~**Student API 타입**~~ | ~~`api/student.ts`~~ | ~~학생 API Request/Response~~ | ⚠️ Deprecated (api/user.ts로 대체) | | **Topic API 타입** | `api/topic.ts` | 주제 API Request/Response (9개 엔드포인트, 팀 주제 포함) | ✅ 완료 | +**타입 테스트** (`src/types/__tests__/`): +- 🆕 `plan.test.ts` - PlanType/BillingCycle/PlanSource/AIFeatureType Enum 검증, 타입 호환성 테스트 (70개 통과) + --- ### 📁 `src/utils/` - 유틸리티 함수 @@ -950,7 +1019,7 @@ firebase functions:log --only cleanupExpiredReservations | API | 경로 | 메서드 | 설명 | 상태 | |-----|------|--------|------|------| -| **텍스트 분석** | `/api/analyze-text` | POST | **Gemini 기반 텍스트 분석** (Delta 지원, 캐싱, 히스토리 인식) | ✅ 완료 | +| **텍스트 분석** | `/api/analyze-text` | POST | **Gemini 기반 텍스트 분석** (Delta 지원, 캐싱, 히스토리 인식, 🆕 **선택적 인증 + 플랜 검증**, 비로그인 IP 해싱) | ✅ 완료 | | **패턴 분석** | `/api/analyze-pattern` | POST | **글 작성 패턴 분석** (3가지 타입, contentHash 기반 3단계 캐싱, 변경 감지) | ✅ 완료 | | **맞춤법 검사** | `/api/spelling/check` | POST | **Gemini 기반 맞춤법 검사** (초등학생 눈높이) | ✅ 완료 | | **AI 글쓰기 도우미** | `/api/writing-assistance` | POST | 🆕 **AI 힌트 생성** (4단계, 주제 맥락, 팀 설정 검증) | ✅ 완료 | @@ -967,7 +1036,9 @@ firebase functions:log --only cleanupExpiredReservations | **이메일 관리** | `/api/team/[teamId]/allowed-emails` | POST, DELETE | 허용 이메일 추가/제거 | ✅ 완료 | | **팀 AI 설정** | `/api/team/[teamId]/ai-config` | GET, PUT | 🆕 **AI 도우미 설정 조회/업데이트** | ✅ 완료 | | **AI 장면 추출** | `/api/extract-scenes` | POST | 🆕 **글에서 주요 장면 추출** (Gemini Flash, 3~5개 장면, 각 장면별 이미지 프롬프트 자동 생성) | ✅ 완료 | -| **AI 이미지 생성** | `/api/generate-image` | POST | 🆕 **장면 기반 이미지 생성** (🆕 **AI 프롬프트 최적화**, Imagen 3.0, Firebase Storage 저장, Writing 업데이트) | ✅ 완료 | +| **AI 이미지 생성** | `/api/generate-image` | POST | 🆕 **장면 기반 이미지 생성** (🆕 **AI 프롬프트 최적화**, Imagen 3.0, Firebase Storage 저장, Writing 업데이트, **플랜 검증**) | ✅ 완료 | +| **AI 사용량 조회** | `/api/ai-usage` | GET | 🆕 **AI 사용량 및 플랜 정보 조회** (userId 기반, usage + plan + features 반환) | ✅ 완료 | +| **조직 관리** | `/api/organization` | GET, POST, PUT, DELETE | 🆕 **Organization CRUD** (School Plan 보유, 멤버 조회, 플랜 정보) | ✅ 완료 | | **주제 CRUD** | `/api/topic` | GET, POST, PUT, DELETE | 주제 생성/조회/수정/삭제 (9개 엔드포인트) | ✅ 완료 | | **주제별 작성자** | `/api/topic/[topicId]/writers` | GET | 🆕 **주제로 글 쓴 학생 목록** (글 개수, Firebase Auth 결합, 글 개수 내림차순) | ✅ 완료 | | **계정 병합** | `/api/auth/merge-account` | POST | 🆕 **익명 계정 데이터 병합** (Firestore + Realtime DB 마이그레이션, Batch/Transaction, 통계 반환) | ✅ 완료 | @@ -980,6 +1051,15 @@ firebase functions:log --only cleanupExpiredReservations - 🆕 `writing.ts` - 글 Firestore CRUD (createWriting, getWriting, updateWriting, deleteWriting, getUserWritings, getRecentWritings, isWritingOwner, 🆕 **getTopicWriters**) - 🆕 `patternAnalysis.ts` - 패턴 분석 결과 Firestore 저장/조회 (contentHash 키, 영구 저장) - 🆕 `teamCodeReservation.ts` - **팀 코드 예약 시스템** (Realtime DB transaction, atomic 예약, 5분 TTL, race condition 방지) +- 🆕 `planLimits.ts` - **플랜별 제한 상수** (PLAN_LIMITS, 헬퍼 함수들) +- 🆕 `planResolver.ts` - **플랜 결정 로직** (getEffectivePlan, getGuestEffectivePlan, canUseAIFeature) +- 🆕 `aiUsage.ts` - **AI 사용량 추적** (getAIUsage, incrementAIUsage, hashIpAddress, getUsageDocId) +- 🆕 `organization.ts` - **Organization CRUD** (getOrganization, createOrganization, updateOrganization, deleteOrganization, isOrganizationPlanValid) + +**서버 레이어 테스트** (`src/lib/server/__tests__/`): +- 🆕 `planLimits.test.ts` - 플랜별 제한 상수 검증, 헬퍼 함수 테스트 +- 🆕 `aiUsage.test.ts` - IP 해싱, 문서 ID 생성, 년월 포맷 검증 (5개 todo) +- 🆕 `planResolver.test.ts` - 플랜 결정 로직, AI 기능 사용 가능 여부 체크 (Firebase 모킹) --- @@ -1066,8 +1146,15 @@ project_w/ │ │ ├── page.tsx # ✅ 랜딩 페이지 (/[locale]) - 🆕 전체 번역 완료 │ │ ├── home/ │ │ │ └── page.tsx # ✅ 유저 홈 페이지 (/[locale]/home) - 🆕 전체 번역 완료 -│ │ ├── write/ -│ │ │ └── page.tsx # ✅ 글쓰기 페이지 (/[locale]/write) - 🆕 실시간 모니터링 +│ │ ├── write/ # ✅ 글쓰기 페이지 (라우트 분리) +│ │ │ ├── page.tsx # 모드 선택 화면 (/[locale]/write) +│ │ │ ├── text/ +│ │ │ │ └── page.tsx # 글 먼저 모드 (/[locale]/write/text) +│ │ │ ├── image/ +│ │ │ │ └── page.tsx # 그림 먼저 모드 (/[locale]/write/image) +│ │ │ └── edit/ +│ │ │ └── [id]/ +│ │ │ └── page.tsx # 수정 모드 (/[locale]/write/edit/[id]) │ │ ├── writing/ # ✅ 글 상세보기 │ │ │ └── [writingId]/ │ │ │ └── page.tsx # 🆕 글 상세 페이지 (Server Component, SEO 최적화) @@ -1098,7 +1185,7 @@ project_w/ │ ├── ui/ # ✅ Chakra UI 기본 │ ├── writing/ # ✅ 글쓰기 에디터 │ │ ├── WritingEditor.tsx # 순수 텍스트 에디터 (포맷팅 없음) -│ │ ├── TopicSelector.tsx # ✅ 주제 선택 컴포넌트 (팀/개인 배지) +│ │ ├── TopicSelector.tsx # 🔄 주제 선택 Dialog (glassmorphism, 탭 기반, 미리보기) │ │ └── CreateTopicDialog.tsx # ✅ 통합 주제 생성 (개인/팀 공용) │ ├── team/ # ✅ 팀 관련 │ │ ├── TeamTopicManager.tsx # ✅ 팀 주제 관리 diff --git a/ROADMAP.md b/ROADMAP.md index aff0ed8..54c09d9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # 라온누리 - 개발 로드맵 -> 최종 업데이트: 2025-12-03 (사용자 설정 기능 추가) +> 최종 업데이트: 2025-12-08 (AI 크레딧 환불 시스템, 구매 플로우, 구독 관리) 초등학생을 위한 창작 글쓰기 교육 플랫폼 개발 계획 @@ -135,6 +135,16 @@ | **Tlab신영복체 폰트 적용** | **Tlab신영복체.ttf 폰트 파일 추가 (public/fonts/), globals.css에 @font-face 추가, write 페이지 제목 텍스트에 폰트 적용, WeeklyGoalCard 스켈레톤 UI 개선 (헤더 및 진행 상태 레이아웃 간격 조정), GlassCard 내부 패딩 조정** | **2025-12-02** | | **사용자 설정 다이얼로그** | **UserSettingsDialog 컴포넌트 신규 추가 (434줄), 프로필 정보 수정 (이름, 사진), 환경 설정 (테마, 언어, 주간 목표), UserProfileButton 설정 메뉴 통합, Firebase Auth + Firestore 업데이트 로직 분리, GlassCard glow 기능 개선, 다국어 지원 (components.settingsDialog namespace, 31개 키, ko/en/ja)** | **2025-12-03** | | **프로필 사진 업로드** | **드래그앤드롭 이미지 업로드, 미리보기 기능, 파일 유효성 검사 (JPEG/PNG/WebP, 5MB 제한), Canvas API 리사이즈 (800x800, 90% 품질), Firebase Storage 업로드, 토스트 알림 (성공/실패), Firebase Storage 규칙 업데이트 (profile_photos 경로 허용)** | **2025-12-03** | +| **TopicSelector Dialog 리디자인** | **드롭다운 → Dialog 기반 UI 전환 (UserSettingsDialog 패턴), Glassmorphism 스타일링 (멀티레이어, 그라데이션 악센트), 좌측 사이드바 탭 네비게이션 ("내 주제" + Separator + 동적 팀 탭들), 주제 카드 그리드 (GlassCard 컴포넌트, 선택 하이라이트), 미리보기 패널 (7:3 세로 비율, 가로 구분 레이아웃), 미리보기-확인 선택 플로우 (클릭 → 미리보기 → 선택 버튼), "자유 주제" 옵션 (null 선택, "내 주제" 탭 최상단), 팀별 주제 필터링 (각 팀 탭에서 해당 팀 주제만 표시), CreateTopicDialog 통합 ("내 주제" 탭), Footer 버튼 (GlassCard, 취소/선택), Readonly 모드 유지 (수정 모드에서 Dialog 숨김), 다국어 지원 (topicSelector namespace 확장, title/selectButton/cancelButton/myTopicsTab/previewTitle/previewPlaceholder/description 키 추가, ko/en/ja)** | **2025-12-04** | +| **글쓰기 페이지 라우트 분리** | **1,540줄 단일 파일 → 4개 독립 라우트 분리 (/write 모드 선택 ~140줄, /write/text 글 먼저 ~800줄, /write/image 그림 먼저 ~900줄, /write/edit/[id] 수정 ~700줄), 기존 URL 리다이렉트 (/write?mode=wrt → /write/text, /write?mode=img → /write/image, /write?id=xxx → /write/edit/xxx), ModeSelectionCard 라우트 URL 변경, WritingCard 수정 링크 업데이트** | **2025-12-04** | +| **AI 기능 플랜 기반 제한 시스템** | **4단계 플랜 시스템 (Free/Classroom/Academy/School), Organization 계층 구조 (학교 → 선생님), 플랜 우선순위 (Organization > User), AI 사용량 추적 (월별, userId/IP 기반), 익명 사용자 IP 해싱, PlanType/BillingCycle/PlanSource/AIFeatureType Enum, planLimits.ts (플랜별 제한 상수), planResolver.ts (getEffectivePlan, canUseAIFeature), aiUsage.ts (사용량 추적, hashIpAddress, incrementAIUsage), organization.ts (Organization CRUD), AI 기능 검증 (analyze-text 선택적 인증, generate-image/writing-assistance 플랜 체크), POST /api/ai-usage (사용량 조회), POST /api/organization (Organization CRUD), AIUsageManager (클라이언트 캐싱), OrganizationManager, 다국어 지원 (errors.ai, ai.limits namespace), 테스트 코드 4개 (plan.test.ts, planLimits.test.ts, aiUsage.test.ts, planResolver.test.ts, 70개 통과)** | **2025-12-08** | +| **팀 AI 2단계 계층 구조** | **팀 AI 마스터 스위치 + AI 글쓰기 도우미 분리 (team.aiEnabled 필드 추가, maxAIEnabledTeams 플랜 제한 적용), TeamAISettings 컴포넌트 리팩토링 (2단계 UI, 마스터 스위치 OFF 시 도우미 숨김), PUT /api/team/[teamId] 플랜 제한 체크 (aiEnabled 변경 시), TeamManager.updateAIEnabled() 메서드 추가, 3가지 상태별 피드백 메시지 (활성화/플랜 제한/비활성화), 다국어 지원 (team.manage.teamAI namespace, ko/en/ja 6개 키), 타입 체크 통과** | **2025-12-08** | +| **TeamCard 컴포넌트 통합** | **내 팀 페이지 스타일로 리디자인 (bg.subtle 배경, border, hover 효과), 커버 이미지 지원 (team.coverImage 140px 높이), 팀 설명 표시 (team.description 2줄 lineClamp), isOwner/onClick props 추가, 인라인 renderTeamCard 함수 제거, /team 페이지에서 TeamCard 사용 (소유자는 관리 페이지로, 멤버는 상세 페이지로 이동), /team/all 공개 팀 페이지에서 TeamCard 사용, 타입 체크 통과** | **2025-12-08** | +| **공개 팀 코드 보안 강화** | **Team.code를 optional로 변경 (code?: string), TeamCard에서 코드 없으면 숨김 (조건부 렌더링), getPublicTeams() 팀 코드 제거 (서버), GET /api/team/[teamId]/public 멤버 아닌 경우 코드 제거 (서버), StudentLoginFlow 팀 코드 검증 추가, TeamInfoCard 복사 함수 null 체크, firebaseAuth.ts Non-null assertion, 타입 에러 수정 완료** | **2025-12-08** | +| **팀 조회 API 통합** | **GET /api/team/[teamId]에 보안 로직 통합 (optionalAuth, canViewTeam 권한 체크, 멤버 아니면 팀 코드 제거), /api/team/[teamId]/public 엔드포인트 삭제 (중복 제거), TeamManager.getPublicTeam() 제거, GetPublicTeamResponse 타입 제거, 단일 엔드포인트로 통합, 타입 체크 통과** | **2025-12-08** | +| **TeamCard AI 활성화 배지** | **team.aiEnabled === true일 때 주황색 배지 표시, LuSparkles 아이콘 사용, "AI 활성화" 텍스트, 다국어 지원 (team.list.aiEnabled, ko/en/ja)** | **2025-12-08** | +| **UserSettingsDialog 구독 섹션** | **"내 구독" 탭 추가 (프로필/내 구독/환경설정), 현재 플랜 카드 (플랜 타입 배지, 출처 표시), 플랜 제한 정보 카드 (팀 AI 활성화, AI 분석, 글쓰기 도우미, 이미지 생성), 실시간 사용량 표시 (remaining/limit), Free 플랜 업그레이드 버튼 (새 창, locale 포함), SettingBox 컴포넌트 (투명 배경, 얇은 테두리, 블러 효과), SettingButton 컴포넌트 (브랜드 테두리, 호버 그라데이션), 로딩 상태 처리, 다국어 지원 (components.settingsDialog.subscription namespace, ko/en/ja 17개 키)** | **2025-12-08** | +| **AI 크레딧 환불 시스템** | **업그레이드 시 Prorated 환불 → AI 크레딧 지급, FirestoreUser.aiCredits 필드 추가 (영구 사용, 월 제한 무시), 환율 100원 = 10 크레딧, AI 기능 비용 (분석 20, 이미지 100, 도우미 10), credits.ts 유틸리티 (calculateProratedRefund, isUpgrade, PLAN_MONTHLY_PRICES), planLimits.ts 상수 추가 (AI_FEATURE_COSTS, CREDIT_EXCHANGE_RATE, convertKRWToCredits), POST /api/user/purchase API (Mock 승인 + 환불 계산 + 크레딧 지급 + 플랜 업데이트, 원자적 처리), UserManager.purchasePlan() (creditsAdded 반환), PurchaseConfirmationDialog 컴포넌트 (플랜 정보, 가격 포맷팅, 월간/연간 배지, Dialog.Root 패턴), pricing 페이지 구매 플로우 (로그인 체크, 플랜별 분기, 크레딧 안내 toast), UserSettingsDialog 크레딧 표시 (주황색 배지, LuSparkles), 다국어 지원 (components.purchaseDialog, pricing.purchaseSuccessWithCredits, subscription.aiCredits/credits/creditsDescription, ko/en/ja), 향후 Toss Payments 연동 준비 (주석 포함), 타입 체크 통과** | **2025-12-08** | ### 🚧 진행 중 diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index f6ca93d..2233e5b 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -169,6 +169,76 @@ export default createSystem(defaultConfig, { - 다크 모드 자동 대응 - 일관된 시각적 통일성 +**Props**: +- `glow`: 호버 시 그라데이션 글로우 효과 +- `isClickable`: 클릭 가능 상태 (커서 변경, 호버 효과) + +### Glassmorphism Dialog 패턴 + +UserSettingsDialog와 TopicSelector에서 사용하는 고급 glassmorphism 패턴: + +```tsx + + + + + + {/* Content with sidebar tabs */} + + + + +``` + +**주요 효과**: +- **멀티레이어 glassmorphism**: 베이스 그라데이션 + 청색 악센트(좌상단) + 분홍색 악센트(우하단) +- **Backdrop blur**: 배경 8px 블러 처리 +- **Content blur**: 컨텐츠 5px 블러 + 85% 투명도 +- **Radial gradients**: 대화상자에 깊이감과 프리미엄 느낌 부여 + +**사용 컴포넌트**: +- `UserSettingsDialog.tsx` - 사용자 설정 (프로필, 환경설정 탭) +- `TopicSelector.tsx` - 주제 선택 (내 주제 + 팀별 탭, 미리보기 패널) + ### 버튼 스타일 ```tsx diff --git a/TECH_STACK.md b/TECH_STACK.md index 70ae006..450fda6 100644 --- a/TECH_STACK.md +++ b/TECH_STACK.md @@ -1,6 +1,6 @@ # 라온누리 - 기술 스택 및 개발 환경 -> 최종 업데이트: 2025-11-27 (채점 시스템 개편) +> 최종 업데이트: 2025-12-08 (AI 크레딧 환불 시스템, 구매 플로우) --- @@ -350,7 +350,21 @@ const nickname = teamManager.getMemberNickname(team, uid, user?.name); ### 3. 글쓰기 및 저장 로직 (Manager 패턴 적용) ``` +🆕 Write 페이지 라우트 구조 (2025-12-04 분리) + /write → 모드 선택 화면 + /write/text → 글 먼저 모드 (글쓰기 → 이미지) + /write/image → 그림 먼저 모드 (이미지 → 글쓰기) + /write/edit/[id] → 수정 모드 + 1. 사용자가 /write 페이지 접근 + ├─> 모드 선택 화면 표시 (글 먼저 / 그림 먼저) + ├─> 기존 URL 호환성 리다이렉트: + │ ├─> /write?mode=wrt → /write/text + │ ├─> /write?mode=img → /write/image + │ └─> /write?id=xxx → /write/edit/xxx + └─> 사용자가 모드 선택 → 해당 라우트로 이동 + +1-1. 글쓰기 모드 진입 (/write/text 또는 /write/image) ├─> LocalStorage에서 임시 저장된 글 불러오기 (DraftManager) └─> 에디터에 복원 @@ -995,7 +1009,9 @@ interface Draft { - `src/app/api/spelling/check/route.ts` - 맞춤법 검사 API - ~~`src/components/writing/ScoreDisplay.tsx`~~ - ❌ 삭제됨 (하이라이트로 대체) - ~~`src/components/writing/SpellingErrorDisplay.tsx`~~ - ❌ 삭제됨 (하이라이트로 대체) -- `src/app/write/page.tsx` - Delta 추적 + 통합 + Toast 알림 +- `src/app/[locale]/write/text/page.tsx` - Delta 추적 + 통합 + Toast 알림 +- `src/app/[locale]/write/image/page.tsx` - 이미지 먼저 모드 + Toast 알림 +- `src/app/[locale]/write/edit/[id]/page.tsx` - 수정 모드 **타입 정의**: - `src/types/draft.ts` - Draft, AnalysisHistoryItem @@ -1138,7 +1154,8 @@ interface Draft { - 성공/실패 시 기존 loading toast 제거 후 새 toast 표시 **참고 파일**: -- `src/app/write/page.tsx` - Toast 알림 통합 +- `src/app/[locale]/write/text/page.tsx` - Toast 알림 통합 +- `src/app/[locale]/write/image/page.tsx` - Toast 알림 통합 --- @@ -1503,7 +1520,8 @@ interface WritingAnalysis { - `src/app/api/analyze-pattern/route.ts` - analysis 재사용 로직 **클라이언트**: -- `src/app/[locale]/write/page.tsx` - 저장 시 분석 수행 +- `src/app/[locale]/write/text/page.tsx` - 저장 시 분석 수행 +- `src/app/[locale]/write/image/page.tsx` - 저장 시 분석 수행 - `src/managers/WritingManager.ts` - `CreateWritingParams.analysis` #### 비용 절감 효과 @@ -1561,7 +1579,7 @@ const improvementRate = calculateImprovementRate(spellingErrorsHistory); **타입**: `src/types/writing.ts` **유틸**: `src/utils/contentHash.ts` **서버**: `src/lib/server/writing.ts`, `src/app/api/analyze-pattern/route.ts` -**클라이언트**: `src/app/[locale]/write/page.tsx`, `src/managers/WritingManager.ts` +**클라이언트**: `src/app/[locale]/write/{text,image,edit}/page.tsx`, `src/managers/WritingManager.ts` --- @@ -1949,6 +1967,524 @@ interface AIAssistanceRecord { --- +### 21. AI 기능 플랜 기반 제한 시스템 + +#### 핵심 개념 + +**목적**: AI 기능(글 분석, 이미지 생성, AI 도우미)을 사용자/조직의 플랜에 따라 제한하여 과금 모델 구현 + +#### 플랜 구조 + +| 플랜 | 단위 | AI 분석 | AI 도우미 | 이미지 생성 | +|------|------|---------|-----------|-------------| +| Free | User | 10회/월 | ❌ | ❌ | +| Classroom | User | 무제한 | ✅ | ✅ | +| Academy | User | 무제한 | ✅ | ✅ | +| School | Organization | 무제한 | ✅ | ✅ | + +#### 데이터 구조 + +``` +Organization (학교) → School Plan 보유 + ↓ +User (선생님) → 개인 플랜 또는 Organization 소속 + ↓ +Team (반) +``` + +**플랜 우선순위**: Organization(School Plan) > User 개인 플랜 + +#### Firestore 컬렉션 + +``` +organizations/{organizationId} + - id, name, plan, adminIds, createdAt, updatedAt, isActive + +aiUsage/{identifier}_{yearMonth} + - userId?: string (로그인 사용자) + - ipHash?: string (비로그인 - IP 해시) + - yearMonth, analysisCount, assistanceCount, imageGenerationCount, lastUpdatedAt +``` + +#### 아키텍처 플로우 + +``` +1. API 요청 (analyze-text, generate-image, writing-assistance) + ↓ +2. 플랜 검증 + - 로그인: getEffectivePlan(userId) + - 비로그인: getGuestEffectivePlan() → Free 플랜 + ↓ +3. 기능 사용 가능 여부 체크 + - canUseAIFeature(identifier, featureType) + - Free 플랜: 월별 사용량 확인 + - 유료 플랜: 기능 활성화 여부 확인 + ↓ +4. AI 처리 + ↓ +5. 사용량 증가 + - incrementAIUsage(identifier, featureType) + - Firestore 트랜잭션으로 동시성 보장 +``` + +#### 주요 특징 + +1. **Organization 계층 구조** 🏫: + - School Plan은 Organization 레벨에서 관리 + - User가 organizationId를 가지면 조직 플랜 자동 적용 + - 조직 플랜이 개인 플랜보다 우선 + +2. **익명 사용자 지원** 👤: + - IP 주소 해싱 (SHA-256, 16자) + - Free 플랜 제한 자동 적용 + - 세션 간 사용량 추적 + +3. **사용량 추적** 📊: + - 월별 집계 (`yearMonth: "2024-12"`) + - 기능별 카운터 (analysis, assistance, imageGeneration) + - Firestore 트랜잭션으로 race condition 방지 + +4. **플랜 결정 로직** 🎯: + ```typescript + async function getEffectivePlan(userId): EffectivePlan { + const user = await getUser(userId); + if (!user) return FREE_PLAN; + + // Organization 플랜 우선 + if (user.organizationId) { + const org = await getOrganization(user.organizationId); + if (org?.isActive && isOrganizationPlanValid(org)) { + return { + type: org.plan.type, + limits: PLAN_LIMITS[org.plan.type], + source: PlanSource.ORGANIZATION, + organizationId: org.id, + }; + } + } + + // 개인 플랜 fallback + if (user.plan && isPlanValid(user.plan)) { + return { + type: user.plan.type, + limits: PLAN_LIMITS[user.plan.type], + source: PlanSource.USER, + }; + } + + return FREE_PLAN; + } + ``` + +5. **API 레벨 검증** 🔒: + - `analyze-text`: 선택적 인증 (optionalAuth) + - `generate-image`: 필수 인증 + Classroom 이상 + - `writing-assistance`: 필수 인증 + Classroom 이상 + - 제한 초과 시 403 에러 + 업그레이드 안내 + +6. **클라이언트 캐싱** ⚡: + - AIUsageManager: 1분 TTL + - 플랜 변경 시 자동 캐시 무효화 + +#### Enum 타입 시스템 + +```typescript +export enum PlanType { + FREE = "free", + CLASSROOM = "classroom", + ACADEMY = "academy", + SCHOOL = "school", +} + +export enum AIFeatureType { + ANALYSIS = "analysis", + ASSISTANCE = "assistance", + IMAGE_GENERATION = "imageGeneration", +} + +export enum PlanSource { + USER = "user", + ORGANIZATION = "organization", +} +``` + +#### 참고 파일 + +**타입**: `src/types/plan.ts` (8개 enum/interface) +**서버 레이어**: +- `src/lib/server/planLimits.ts` - 플랜별 제한 상수 +- `src/lib/server/planResolver.ts` - 플랜 결정 로직 +- `src/lib/server/aiUsage.ts` - 사용량 추적 +- `src/lib/server/organization.ts` - Organization CRUD +**API**: +- `src/app/api/ai-usage/route.ts` - 사용량 조회 +- `src/app/api/organization/route.ts` - Organization 관리 +- `src/app/api/analyze-text/route.ts` - 텍스트 분석 (플랜 검증 추가) +- `src/app/api/generate-image/route.ts` - 이미지 생성 (플랜 검증 추가) +**Manager**: +- `src/managers/AIUsageManager.ts` - 클라이언트 사용량 관리 +- `src/managers/OrganizationManager.ts` - 클라이언트 조직 관리 +**테스트**: +- `src/types/__tests__/plan.test.ts` - Enum 검증 +- `src/lib/server/__tests__/planLimits.test.ts` - 제한 상수 검증 +- `src/lib/server/__tests__/aiUsage.test.ts` - IP 해싱, 문서 ID 생성 +- `src/lib/server/__tests__/planResolver.test.ts` - 플랜 결정 로직 (70개 통과) + +--- + +### 22. 팀 AI 2단계 계층 구조 + +#### 핵심 개념 + +**목적**: 팀 AI 기능을 마스터 스위치 + 하위 옵션으로 분리하여 플랜 제한 적용 명확화 + +#### 계층 구조 + +``` +[팀 AI 기능] ⬅️ 플랜 제한 적용 (maxAIEnabledTeams) + ↓ (켜져있을 때만 표시) + [AI 글쓰기 도우미] ⬅️ aiEnabled=true일 때만 활성화 + ↓ (켜져있을 때만 표시) + [상세 설정...] +``` + +#### 데이터 모델 + +```typescript +interface Team { + // 1단계: 마스터 스위치 (플랜 제한 적용) + aiEnabled?: boolean; // 팀 전체 AI 기능 활성화 + + // 2단계: 하위 옵션 (aiEnabled=true일 때만 유효) + aiAssistanceConfig?: { + enabled: boolean; // AI 글쓰기 도우미 + detectionTimeMinutes: number; + maxHintsPerWriting: number; + cooldownMinutes: number; + allowedHintLevels: number[]; + requireSelfEdit: boolean; + }; +} +``` + +#### 플랜별 제한 (maxAIEnabledTeams) + +| 플랜 | AI 활성화 가능 팀 개수 | +|------|---------------------| +| Free | 0개 (불가) | +| Pro | 0개 (개인 전용) | +| Classroom | 1개 | +| Academy | 5개 | +| School | 무제한 | + +#### API 검증 로직 + +```typescript +// PUT /api/team/[teamId] +if (data.aiEnabled !== undefined && data.aiEnabled !== team.aiEnabled) { + if (data.aiEnabled === true) { + const effectivePlan = await getEffectivePlan(userId); + const maxAllowed = getMaxAIEnabledTeams(effectivePlan.type); + + if (maxAllowed !== -1) { + const userTeams = await getAllUserTeams(userId); + const currentCount = userTeams.filter( + (t) => t.aiEnabled === true && t.id !== teamId + ).length; + + if (currentCount >= maxAllowed) { + return validationErrorResponse( + `플랜 제한: 최대 ${maxAllowed}개 팀까지만 AI를 활성화할 수 있습니다.` + ); + } + } + } +} +``` + +#### UI 상태 피드백 + +**TeamAISettings 컴포넌트** 3가지 상태: + +1. **활성화 상태** (teamAIEnabled=true) + - ✓ 팀 AI가 활성화되어 있습니다 (초록색) + - AI 글쓰기 도우미 토글 표시 + +2. **플랜 제한** (canToggleMaster=false) + - AI 활성화 제한 도달 (빨간색) + - 해결 방법 안내: "다른 팀의 AI를 비활성화하거나 플랜을 업그레이드하세요" + - [플랜 업그레이드] 버튼 + +3. **비활성화** (teamAIEnabled=false, canToggleMaster=true) + - 스위치를 켜서 팀 AI 기능을 활성화하세요 (회색) + +#### 주요 특징 + +1. **종속성 관리** 🔗: + - 마스터 스위치 OFF → 도우미 자동 비활성화 + - 마스터 스위치 OFF → 도우미 UI 숨김 + +2. **플랜 제한 분리** 🎯: + - `maxAIEnabledTeams`: 팀 AI 마스터 스위치에만 적용 + - AI 글쓰기 도우미: 플랜 제한 없음 (마스터만 체크) + +3. **명확한 사용자 피드백** 💬: + - 스위치 비활성화 이유 명시 + - 활성화 방법 안내 + - 즉시 해결 가능 (업그레이드 버튼) + +#### 참고 파일 + +**타입**: `src/types/team.ts` (Team.aiEnabled 필드 추가) +**Manager**: +- `src/managers/TeamManager.ts` - updateAIEnabled(), getAIEnabledTeamsCount() +**API**: +- `src/app/api/team/[teamId]/route.ts` - PUT 메서드 플랜 검증 +**컴포넌트**: +- `src/components/team/TeamAISettings.tsx` - 2단계 UI +**다국어**: `messages/*.json` - team.manage.teamAI namespace (ko/en/ja) + +--- + +### 23. 공개 팀 코드 보안 강화 + +#### 핵심 개념 + +**목적**: 공개 팀에서 팀 코드를 비멤버에게 노출하지 않아 무단 가입 방지 + +#### 보안 정책 + +``` +멤버인 경우 → 팀 코드 표시 ✅ +멤버 아닌 경우 → 팀 코드 숨김 🔒 +``` + +#### 구현 레이어 + +**1. 타입 레벨** (`src/types/team.ts`): +```typescript +interface Team { + code?: string; // optional로 변경 (공개 팀에서 제거 가능) +} +``` + +**2. 서버 API 레벨**: + +a) **공개 팀 목록** (`src/lib/server/team.ts - getPublicTeams`): +```typescript +const teams = querySnapshot.docs.slice(0, limit).map((doc) => { + const data = doc.data(); + const {code, ...teamDataWithoutCode} = data; // 🔒 코드 제거 + return { id: doc.id, ...teamDataWithoutCode }; +}) as Team[]; +``` + +b) **개별 공개 팀 조회** (`src/app/api/team/[teamId]/public/route.ts`): +```typescript +const isMember = uid && uid in team.members; +const teamData = isMember ? team : (() => { + const {code, ...teamWithoutCode} = team; // 🔒 멤버 아니면 제거 + return teamWithoutCode as typeof team; +})(); +``` + +**3. UI 컴포넌트 레벨** (`src/components/team/TeamCard.tsx`): +```tsx +{/* 팀 코드 (코드가 있을 때만 표시) */} +{team.code && ( + + + {team.code} + + +)} +``` + +#### 타입 안전성 처리 + +**필수 코드 사용처**: +- `StudentLoginFlow.tsx`: 팀 코드 null 체크 +- `TeamInfoCard.tsx`: 복사 함수 null 체크 +- `firebaseAuth.ts`: Non-null assertion (`team.code!`) + +#### 주요 특징 + +1. **계층적 보안** 🛡️: + - 서버에서 데이터 제거 (클라이언트 우회 불가) + - 타입 시스템으로 안전성 보장 + - UI에서 조건부 렌더링 + +2. **멤버 우대** 👥: + - 팀 멤버는 모든 정보 접근 가능 + - 공개 팀 페이지에서도 코드 확인 + +3. **사용자 경험** ✨: + - 코드 없어도 UI 깨지지 않음 + - 내 팀 페이지는 항상 코드 표시 + +#### 참고 파일 + +**타입**: `src/types/team.ts` (Team.code?: string) +**서버**: +- `src/lib/server/team.ts` - getPublicTeams() +- `src/app/api/team/[teamId]/public/route.ts` - GET +**컴포넌트**: `src/components/team/TeamCard.tsx` + +--- + +### 24. AI 크레딧 환불 시스템 + +#### 핵심 개념 + +**목적**: 플랜 업그레이드 시 남은 기간 환불액을 AI 크레딧으로 지급하여 사용자 손실 최소화 + +#### 크레딧 시스템 + +**환율**: +``` +100원 = 10 크레딧 +``` + +**AI 기능별 비용**: +- AI 텍스트 분석: 20 크레딧/회 +- AI 이미지 생성: 100 크레딧/회 +- AI 글쓰기 도우미: 10 크레딧/회 + +**정책**: +- ✅ 영구 사용 (만료 없음) +- ✅ 월 제한 초과 시 크레딧으로 사용 가능 +- ✅ 업그레이드 시에만 지급 (다운그레이드 X) + +#### Prorated 환불 계산 + +```typescript +// Example +현재: Classroom (월 19,000원, 15일 남음) +→ Academy 구매 (월 39,000원) + +계산: +- 남은 기간: 15일 +- 일할 환불: 19,000 × (15/30) = 9,500원 +- 크레딧 전환: 9,500 ÷ 10 = 950 크레딧 +- 결과: aiCredits += 950 +``` + +#### 데이터 모델 + +```typescript +// FirestoreUser 확장 +interface FirestoreUser { + aiCredits?: number; // AI 크레딧 잔액 (기본값: 0) +} + +// AI 기능 비용 상수 +export const AI_FEATURE_COSTS = { + ANALYSIS: 20, + IMAGE_GENERATION: 100, + ASSISTANCE: 10, +} as const; + +// 환율 +export const CREDIT_EXCHANGE_RATE = { + KRW_PER_CREDIT: 10, +} as const; +``` + +#### 구매 플로우 (서버 원자적 처리) + +``` +클라이언트: userManager.purchasePlan(type, cycle, amount) + ↓ +서버 (POST /api/user/purchase): + ├─ Mock 결제 승인 [1.5s] ⏳ + ├─ 기존 플랜 확인 (getUser) + ├─ 업그레이드인지 체크 (isUpgrade) + ├─ Prorated 환불 계산 (calculateProratedRefund) + ├─ 크레딧 전환 (convertKRWToCredits) + ├─ Firestore 업데이트 (원자적): + │ ├─ plan 업데이트 + │ └─ aiCredits increment + └─ creditsAdded 반환 + ↓ +클라이언트: Toast ("950 크레딧이 지급되었습니다") +``` + +#### 주요 함수 + +**credits.ts** (서버 유틸리티): +```typescript +// Prorated 환불 계산 +calculateProratedRefund(currentPlan: UserPlan): number + +// 업그레이드 여부 +isUpgrade(currentPlan: PlanType, newPlan: PlanType): boolean + +// 플랜별 월 가격 +PLAN_MONTHLY_PRICES: Record +``` + +**planLimits.ts** (상수): +```typescript +// 크레딧 전환 +convertKRWToCredits(krw: number): number + +// AI 기능 비용 +AI_FEATURE_COSTS + +// 환율 +CREDIT_EXCHANGE_RATE +``` + +#### UI 표시 + +**UserSettingsDialog (구독 탭)**: +```tsx + + + AI 크레딧 + + 950 크레딧 + + + + 플랜 업그레이드 시 환불받은 크레딧입니다. + 월 제한을 초과해도 크레딧으로 AI 기능을 사용할 수 있어요. + + +``` + +#### 향후 확장 (크레딧 차감) + +AI API들에서 플랜 제한 초과 시 크레딧 사용: +```typescript +// analyze-text, generate-image API +if (planLimitExceeded) { + if (user.aiCredits >= AI_FEATURE_COSTS.ANALYSIS) { + // 크레딧 차감 + await adminFbClient.collection("users").doc(userId).update({ + aiCredits: FieldValue.increment(-AI_FEATURE_COSTS.ANALYSIS), + }); + } else { + return error("크레딧 부족"); + } +} +``` + +#### 참고 파일 + +**타입**: `src/types/firestoreUser.ts` (aiCredits 필드) +**유틸리티**: +- `src/lib/server/credits.ts` - 환불 계산 +- `src/lib/server/planLimits.ts` - 크레딧 상수 +**API**: `src/app/api/user/purchase/route.ts` +**Manager**: `src/managers/UserManager.ts` - purchasePlan() +**컴포넌트**: +- `src/components/pricing/PurchaseConfirmationDialog.tsx` - 구매 확인 +- `src/components/settings/UserSettingsDialog.tsx` - 크레딧 표시 +**다국어**: `messages/*.json` - purchaseDialog, subscription.aiCredits + +--- + ## 참고 문서 - [PROJECT_STRUCTURE.md](./PROJECT_STRUCTURE.md) - 프로젝트 구조