docs: Sync documentation from private repository

This commit is contained in:
Documentation Bot 2025-12-08 07:27:49 +00:00
parent ab36bba761
commit 4caac03e62
4 changed files with 722 additions and 19 deletions

View File

@ -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 # ✅ 팀 주제 관리

View File

@ -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** |
### 🚧 진행 중

View File

@ -169,6 +169,76 @@ export default createSystem(defaultConfig, {
- 다크 모드 자동 대응
- 일관된 시각적 통일성
**Props**:
- `glow`: 호버 시 그라데이션 글로우 효과
- `isClickable`: 클릭 가능 상태 (커서 변경, 호버 효과)
### Glassmorphism Dialog 패턴
UserSettingsDialog와 TopicSelector에서 사용하는 고급 glassmorphism 패턴:
```tsx
<Dialog.Root open={open} onOpenChange={onOpenChange} size="xl">
<Portal>
<Dialog.Backdrop
bg="blackAlpha.600"
backdropFilter="blur(8px)"
/>
<Dialog.Positioner>
<Dialog.Content
p={0}
overflow="hidden"
bg="bg/85"
backdropFilter="blur(5px)"
borderWidth="1px"
borderColor="whiteAlpha.200"
borderRadius="2em"
boxShadow="0 8px 32px 0 rgba(0, 0, 0, 0.37)"
css={{
background: 'linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%)',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
width: '400px',
height: '400px',
background: 'radial-gradient(circle at top left, rgba(59, 130, 246, 0.3) 0%, rgba(59, 130, 246, 0.15) 25%, transparent 60%)',
pointerEvents: 'none',
borderTopLeftRadius: 'inherit',
zIndex: 0,
},
'&::after': {
content: '""',
position: 'absolute',
bottom: 0,
right: 0,
width: '400px',
height: '400px',
background: 'radial-gradient(circle at bottom right, rgba(255, 107, 157, 0.2) 0%, rgba(255, 107, 157, 0.1) 30%, transparent 70%)',
pointerEvents: 'none',
borderBottomRightRadius: 'inherit',
zIndex: 0,
}
}}
>
{/* Content with sidebar tabs */}
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
```
**주요 효과**:
- **멀티레이어 glassmorphism**: 베이스 그라데이션 + 청색 악센트(좌상단) + 분홍색 악센트(우하단)
- **Backdrop blur**: 배경 8px 블러 처리
- **Content blur**: 컨텐츠 5px 블러 + 85% 투명도
- **Radial gradients**: 대화상자에 깊이감과 프리미엄 느낌 부여
**사용 컴포넌트**:
- `UserSettingsDialog.tsx` - 사용자 설정 (프로필, 환경설정 탭)
- `TopicSelector.tsx` - 주제 선택 (내 주제 + 팀별 탭, 미리보기 패널)
### 버튼 스타일
```tsx

View File

@ -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 && (
<Box w="full" p={2} bg="bg.muted" borderRadius="md" textAlign="center">
<Text fontSize="sm" fontWeight="bold" color="brand.fg">
{team.code}
</Text>
</Box>
)}
```
#### 타입 안전성 처리
**필수 코드 사용처**:
- `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<PlanType, number>
```
**planLimits.ts** (상수):
```typescript
// 크레딧 전환
convertKRWToCredits(krw: number): number
// AI 기능 비용
AI_FEATURE_COSTS
// 환율
CREDIT_EXCHANGE_RATE
```
#### UI 표시
**UserSettingsDialog (구독 탭)**:
```tsx
<SettingBox>
<HStack justify="space-between">
<Text>AI 크레딧</Text>
<Badge colorPalette="orange">
<LuSparkles /> 950 크레딧
</Badge>
</HStack>
<Text color="muted">
플랜 업그레이드 시 환불받은 크레딧입니다.
월 제한을 초과해도 크레딧으로 AI 기능을 사용할 수 있어요.
</Text>
</SettingBox>
```
#### 향후 확장 (크레딧 차감)
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) - 프로젝트 구조