docs: Sync documentation from private repository
This commit is contained in:
parent
ab36bba761
commit
4caac03e62
@ -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`)
|
- ⚙️ **사용자 설정 다이얼로그** (`UserSettingsDialog.tsx`)
|
||||||
- **프로필 정보 수정**: 이름, 프로필 사진 업로드 (드래그앤드롭, Canvas 리사이즈)
|
- **프로필 정보 수정**: 이름, 프로필 사진 업로드 (드래그앤드롭, Canvas 리사이즈)
|
||||||
- **환경 설정**: 테마 (라이트/다크), 주간 목표 (1~10개)
|
- **환경 설정**: 테마 (라이트/다크), 주간 목표 (1~10개)
|
||||||
@ -146,7 +205,7 @@
|
|||||||
- 기존 단순 배지 → 전체 UI 테마로 업그레이드
|
- 기존 단순 배지 → 전체 UI 테마로 업그레이드
|
||||||
- 🔒 **수정 모드 주제 변경 차단**
|
- 🔒 **수정 모드 주제 변경 차단**
|
||||||
- TopicSelector에 `readonly` prop 추가
|
- TopicSelector에 `readonly` prop 추가
|
||||||
- 수정 모드에서는 Select 드롭다운 숨김, "현재 주제" 라벨만 표시
|
- 수정 모드에서는 Dialog 트리거 버튼 숨김, "현재 주제" 라벨만 표시
|
||||||
- 데이터 무결성 보장 (템플릿 덮어쓰기 방지)
|
- 데이터 무결성 보장 (템플릿 덮어쓰기 방지)
|
||||||
- AI 설정/실시간 모니터링 혼선 방지
|
- AI 설정/실시간 모니터링 혼선 방지
|
||||||
- 다국어 지원: `readonlyNote`, `currentTopic` (ko, en, ja)
|
- 다국어 지원: `readonlyNote`, `currentTopic` (ko, en, ja)
|
||||||
@ -326,7 +385,7 @@
|
|||||||
- 📝 다중 글조각 관리 시스템 (DraftManager, SavedDraftsDialog)
|
- 📝 다중 글조각 관리 시스템 (DraftManager, SavedDraftsDialog)
|
||||||
- 💾 강화된 자동 저장 (2초 debounce, 저장 상태 표시)
|
- 💾 강화된 자동 저장 (2초 debounce, 저장 상태 표시)
|
||||||
- 🎨 테마 슬롯 레시피 추가 (Dialog, Select 자동 배경색)
|
- 🎨 테마 슬롯 레시피 추가 (Dialog, Select 자동 배경색)
|
||||||
- 📋 TopicSelector 그룹핑 (자유/팀/개인 주제 구분, 팀 이름 표시)
|
- 📋 TopicSelector Dialog 리디자인 (glassmorphism, 탭 기반 그룹핑, 미리보기 7:3 레이아웃)
|
||||||
- 🔐 5단계 보안 레벨 시스템 (팀별 보안 정책 선택)
|
- 🔐 5단계 보안 레벨 시스템 (팀별 보안 정책 선택)
|
||||||
- 📦 User 타입 분리 (FirestoreUser / User)
|
- 📦 User 타입 분리 (FirestoreUser / User)
|
||||||
- 🏷️ 닉네임 저장 위치 변경 (team.members[uid].nickname)
|
- 🏷️ 닉네임 저장 위치 변경 (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):
|
**주요 기능** (2025-12-03):
|
||||||
- ✅ 프로필 정보 수정
|
- ✅ 프로필 정보 수정
|
||||||
@ -537,7 +598,7 @@
|
|||||||
| 컴포넌트 | 파일명 | 설명 | 상태 |
|
| 컴포넌트 | 파일명 | 설명 | 상태 |
|
||||||
|---------|--------|------|------|
|
|---------|--------|------|------|
|
||||||
| **WritingEditor** | `WritingEditor.tsx` | Tiptap 기반 순수 텍스트 에디터 (하이라이트 통합) | ✅ 완료 |
|
| **WritingEditor** | `WritingEditor.tsx` | Tiptap 기반 순수 텍스트 에디터 (하이라이트 통합) | ✅ 완료 |
|
||||||
| **TopicSelector** | `TopicSelector.tsx` | 주제 선택 드롭다운 (그룹핑, 팀 이름 표시) | ✅ 완료 |
|
| **TopicSelector** | `TopicSelector.tsx` | 🔄 **주제 선택 Dialog** (glassmorphism, 탭 기반 그룹핑, 미리보기 패널, 7:3 레이아웃) | ✅ 완료 |
|
||||||
| **EditorTooltip** | `EditorTooltip.tsx` | 🆕 **인터랙티브 툴팁** (맞춤법/감각 단어 클릭 시 표시) | ✅ 완료 |
|
| **EditorTooltip** | `EditorTooltip.tsx` | 🆕 **인터랙티브 툴팁** (맞춤법/감각 단어 클릭 시 표시) | ✅ 완료 |
|
||||||
| **WritingPatternDialog** | `WritingPatternDialog.tsx` | 🆕 **글 작성 패턴 분석 다이얼로그** (최근 10개 글 분석) | ✅ 완료 |
|
| **WritingPatternDialog** | `WritingPatternDialog.tsx` | 🆕 **글 작성 패턴 분석 다이얼로그** (최근 10개 글 분석) | ✅ 완료 |
|
||||||
| **WritingPatternDisplay** | `WritingPatternDisplay.tsx` | 🆕 **패턴 분석 결과 표시** (종합 평가, 발전 추이, 강점/약점, 추천) | ✅ 완료 |
|
| **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 제한) | ✅ 완료 |
|
| **TeamCoverImageUploader** | `TeamCoverImageUploader.tsx` | 🆕 **팀 커버 이미지 업로더** (드래그앤드롭, 미리보기, 16:9 AspectRatio, 5MB 제한) | ✅ 완료 |
|
||||||
| **TeamTopicManager** | `TeamTopicManager.tsx` | 팀 주제 목록 및 생성/삭제 UI | ✅ 완료 |
|
| **TeamTopicManager** | `TeamTopicManager.tsx` | 팀 주제 목록 및 생성/삭제 UI | ✅ 완료 |
|
||||||
|
| **TeamAISettings** | `TeamAISettings.tsx` | 🆕 **팀 AI 설정 컴포넌트** (2단계 계층: 팀 AI 마스터 스위치 + AI 글쓰기 도우미, 플랜 제한 체크, 3가지 상태 피드백, VStack 레이아웃) | ✅ 완료 |
|
||||||
| **AIConfigDialog** | `AIConfigDialog.tsx` | 🆕 **AI 도우미 고급 설정 Dialog** (Slider, 커스텀 CheckboxCard) | ✅ 완료 |
|
| **AIConfigDialog** | `AIConfigDialog.tsx` | 🆕 **AI 도우미 고급 설정 Dialog** (Slider, 커스텀 CheckboxCard) | ✅ 완료 |
|
||||||
| **SecurityLevelSelector** | `SecurityLevelSelector.tsx` | 5단계 보안 레벨 선택 (RadioCard, framer-motion 애니메이션) | ✅ 완료 |
|
| **SecurityLevelSelector** | `SecurityLevelSelector.tsx` | 5단계 보안 레벨 선택 (RadioCard, framer-motion 애니메이션) | ✅ 완료 |
|
||||||
| **TopicMemberAnalysisSection** | `TopicMemberAnalysisSection.tsx` | 🆕 **주제별 학생 글쓰기 분석** (Accordion, 주제별 학생 목록, 글 개수, by-topic 분석 연동) | ✅ 완료 |
|
| **TopicMemberAnalysisSection** | `TopicMemberAnalysisSection.tsx` | 🆕 **주제별 학생 글쓰기 분석** (Accordion, 주제별 학생 목록, 글 개수, by-topic 분석 연동) | ✅ 완료 |
|
||||||
@ -792,6 +854,9 @@
|
|||||||
| **WritingSessionManager** | `WritingSessionManager.ts` | 🆕 **실시간 글쓰기 세션 관리** (Firebase Realtime DB 작업) | ✅ 완료 |
|
| **WritingSessionManager** | `WritingSessionManager.ts` | 🆕 **실시간 글쓰기 세션 관리** (Firebase Realtime DB 작업) | ✅ 완료 |
|
||||||
| **WritingManager** | `WritingManager.ts` | 글쓰기 관련 비즈니스 로직 (CRUD, 통계) | ✅ 완료 |
|
| **WritingManager** | `WritingManager.ts` | 글쓰기 관련 비즈니스 로직 (CRUD, 통계) | ✅ 완료 |
|
||||||
| **TopicManager** | `TopicManager.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 | ✅ 완료 |
|
| **index.ts** | `index.ts` | 모든 매니저 export | ✅ 완료 |
|
||||||
| **LevelManager** | `LevelManager.ts` | 레벨/경험치 관리 | ❌ 미구현 |
|
| **LevelManager** | `LevelManager.ts` | 레벨/경험치 관리 | ❌ 미구현 |
|
||||||
| **StickerManager** | `StickerManager.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 도우미 설정) | ✅ 완료 |
|
| **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 생성 이미지) | ✅ 완료 |
|
| **Writing 타입** | `writing.ts` | 글 데이터 모델, 🆕 **WritingAnalysis** (AI 분석 결과, contentHash 기반 재사용), **SpellingError** (맞춤법 오류), 🆕 **AIAssistanceRecord** (AI 도움 이력), 🆕 **GeneratedImage** (AI 생성 이미지) | ✅ 완료 |
|
||||||
| **Scene 타입** | `scene.ts` | 🆕 **장면 데이터 모델** (Scene, SceneExtractionResponse) | ✅ 완료 |
|
| **Scene 타입** | `scene.ts` | 🆕 **장면 데이터 모델** (Scene, SceneExtractionResponse) | ✅ 완료 |
|
||||||
| **Draft 타입** | `draft.ts` | 글조각 데이터 모델 (Draft, DraftListItem, **AnalysisHistoryItem**, **syncStatus**: 'local'\|'synced'\|'syncing') | ✅ 완료 |
|
| **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로 대체) |
|
| ~~**Student API 타입**~~ | ~~`api/student.ts`~~ | ~~학생 API Request/Response~~ | ⚠️ Deprecated (api/user.ts로 대체) |
|
||||||
| **Topic API 타입** | `api/topic.ts` | 주제 API Request/Response (9개 엔드포인트, 팀 주제 포함) | ✅ 완료 |
|
| **Topic API 타입** | `api/topic.ts` | 주제 API Request/Response (9개 엔드포인트, 팀 주제 포함) | ✅ 완료 |
|
||||||
|
|
||||||
|
**타입 테스트** (`src/types/__tests__/`):
|
||||||
|
- 🆕 `plan.test.ts` - PlanType/BillingCycle/PlanSource/AIFeatureType Enum 검증, 타입 호환성 테스트 (70개 통과)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 📁 `src/utils/` - 유틸리티 함수
|
### 📁 `src/utils/` - 유틸리티 함수
|
||||||
@ -950,7 +1019,7 @@ firebase functions:log --only cleanupExpiredReservations
|
|||||||
|
|
||||||
| API | 경로 | 메서드 | 설명 | 상태 |
|
| API | 경로 | 메서드 | 설명 | 상태 |
|
||||||
|-----|------|--------|------|------|
|
|-----|------|--------|------|------|
|
||||||
| **텍스트 분석** | `/api/analyze-text` | POST | **Gemini 기반 텍스트 분석** (Delta 지원, 캐싱, 히스토리 인식) | ✅ 완료 |
|
| **텍스트 분석** | `/api/analyze-text` | POST | **Gemini 기반 텍스트 분석** (Delta 지원, 캐싱, 히스토리 인식, 🆕 **선택적 인증 + 플랜 검증**, 비로그인 IP 해싱) | ✅ 완료 |
|
||||||
| **패턴 분석** | `/api/analyze-pattern` | POST | **글 작성 패턴 분석** (3가지 타입, contentHash 기반 3단계 캐싱, 변경 감지) | ✅ 완료 |
|
| **패턴 분석** | `/api/analyze-pattern` | POST | **글 작성 패턴 분석** (3가지 타입, contentHash 기반 3단계 캐싱, 변경 감지) | ✅ 완료 |
|
||||||
| **맞춤법 검사** | `/api/spelling/check` | POST | **Gemini 기반 맞춤법 검사** (초등학생 눈높이) | ✅ 완료 |
|
| **맞춤법 검사** | `/api/spelling/check` | POST | **Gemini 기반 맞춤법 검사** (초등학생 눈높이) | ✅ 완료 |
|
||||||
| **AI 글쓰기 도우미** | `/api/writing-assistance` | POST | 🆕 **AI 힌트 생성** (4단계, 주제 맥락, 팀 설정 검증) | ✅ 완료 |
|
| **AI 글쓰기 도우미** | `/api/writing-assistance` | POST | 🆕 **AI 힌트 생성** (4단계, 주제 맥락, 팀 설정 검증) | ✅ 완료 |
|
||||||
@ -967,7 +1036,9 @@ firebase functions:log --only cleanupExpiredReservations
|
|||||||
| **이메일 관리** | `/api/team/[teamId]/allowed-emails` | POST, DELETE | 허용 이메일 추가/제거 | ✅ 완료 |
|
| **이메일 관리** | `/api/team/[teamId]/allowed-emails` | POST, DELETE | 허용 이메일 추가/제거 | ✅ 완료 |
|
||||||
| **팀 AI 설정** | `/api/team/[teamId]/ai-config` | GET, PUT | 🆕 **AI 도우미 설정 조회/업데이트** | ✅ 완료 |
|
| **팀 AI 설정** | `/api/team/[teamId]/ai-config` | GET, PUT | 🆕 **AI 도우미 설정 조회/업데이트** | ✅ 완료 |
|
||||||
| **AI 장면 추출** | `/api/extract-scenes` | POST | 🆕 **글에서 주요 장면 추출** (Gemini Flash, 3~5개 장면, 각 장면별 이미지 프롬프트 자동 생성) | ✅ 완료 |
|
| **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개 엔드포인트) | ✅ 완료 |
|
| **주제 CRUD** | `/api/topic` | GET, POST, PUT, DELETE | 주제 생성/조회/수정/삭제 (9개 엔드포인트) | ✅ 완료 |
|
||||||
| **주제별 작성자** | `/api/topic/[topicId]/writers` | GET | 🆕 **주제로 글 쓴 학생 목록** (글 개수, Firebase Auth 결합, 글 개수 내림차순) | ✅ 완료 |
|
| **주제별 작성자** | `/api/topic/[topicId]/writers` | GET | 🆕 **주제로 글 쓴 학생 목록** (글 개수, Firebase Auth 결합, 글 개수 내림차순) | ✅ 완료 |
|
||||||
| **계정 병합** | `/api/auth/merge-account` | POST | 🆕 **익명 계정 데이터 병합** (Firestore + Realtime DB 마이그레이션, Batch/Transaction, 통계 반환) | ✅ 완료 |
|
| **계정 병합** | `/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**)
|
- 🆕 `writing.ts` - 글 Firestore CRUD (createWriting, getWriting, updateWriting, deleteWriting, getUserWritings, getRecentWritings, isWritingOwner, 🆕 **getTopicWriters**)
|
||||||
- 🆕 `patternAnalysis.ts` - 패턴 분석 결과 Firestore 저장/조회 (contentHash 키, 영구 저장)
|
- 🆕 `patternAnalysis.ts` - 패턴 분석 결과 Firestore 저장/조회 (contentHash 키, 영구 저장)
|
||||||
- 🆕 `teamCodeReservation.ts` - **팀 코드 예약 시스템** (Realtime DB transaction, atomic 예약, 5분 TTL, race condition 방지)
|
- 🆕 `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]) - 🆕 전체 번역 완료
|
│ │ ├── page.tsx # ✅ 랜딩 페이지 (/[locale]) - 🆕 전체 번역 완료
|
||||||
│ │ ├── home/
|
│ │ ├── home/
|
||||||
│ │ │ └── page.tsx # ✅ 유저 홈 페이지 (/[locale]/home) - 🆕 전체 번역 완료
|
│ │ │ └── page.tsx # ✅ 유저 홈 페이지 (/[locale]/home) - 🆕 전체 번역 완료
|
||||||
│ │ ├── write/
|
│ │ ├── write/ # ✅ 글쓰기 페이지 (라우트 분리)
|
||||||
│ │ │ └── page.tsx # ✅ 글쓰기 페이지 (/[locale]/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/ # ✅ 글 상세보기
|
│ │ ├── writing/ # ✅ 글 상세보기
|
||||||
│ │ │ └── [writingId]/
|
│ │ │ └── [writingId]/
|
||||||
│ │ │ └── page.tsx # 🆕 글 상세 페이지 (Server Component, SEO 최적화)
|
│ │ │ └── page.tsx # 🆕 글 상세 페이지 (Server Component, SEO 최적화)
|
||||||
@ -1098,7 +1185,7 @@ project_w/
|
|||||||
│ ├── ui/ # ✅ Chakra UI 기본
|
│ ├── ui/ # ✅ Chakra UI 기본
|
||||||
│ ├── writing/ # ✅ 글쓰기 에디터
|
│ ├── writing/ # ✅ 글쓰기 에디터
|
||||||
│ │ ├── WritingEditor.tsx # 순수 텍스트 에디터 (포맷팅 없음)
|
│ │ ├── WritingEditor.tsx # 순수 텍스트 에디터 (포맷팅 없음)
|
||||||
│ │ ├── TopicSelector.tsx # ✅ 주제 선택 컴포넌트 (팀/개인 배지)
|
│ │ ├── TopicSelector.tsx # 🔄 주제 선택 Dialog (glassmorphism, 탭 기반, 미리보기)
|
||||||
│ │ └── CreateTopicDialog.tsx # ✅ 통합 주제 생성 (개인/팀 공용)
|
│ │ └── CreateTopicDialog.tsx # ✅ 통합 주제 생성 (개인/팀 공용)
|
||||||
│ ├── team/ # ✅ 팀 관련
|
│ ├── team/ # ✅ 팀 관련
|
||||||
│ │ ├── TeamTopicManager.tsx # ✅ 팀 주제 관리
|
│ │ ├── TeamTopicManager.tsx # ✅ 팀 주제 관리
|
||||||
|
|||||||
12
ROADMAP.md
12
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** |
|
| **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** |
|
| **사용자 설정 다이얼로그** | **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** |
|
| **프로필 사진 업로드** | **드래그앤드롭 이미지 업로드, 미리보기 기능, 파일 유효성 검사 (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** |
|
||||||
|
|
||||||
### 🚧 진행 중
|
### 🚧 진행 중
|
||||||
|
|
||||||
|
|||||||
@ -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
|
```tsx
|
||||||
|
|||||||
546
TECH_STACK.md
546
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 패턴 적용)
|
### 3. 글쓰기 및 저장 로직 (Manager 패턴 적용)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
🆕 Write 페이지 라우트 구조 (2025-12-04 분리)
|
||||||
|
/write → 모드 선택 화면
|
||||||
|
/write/text → 글 먼저 모드 (글쓰기 → 이미지)
|
||||||
|
/write/image → 그림 먼저 모드 (이미지 → 글쓰기)
|
||||||
|
/write/edit/[id] → 수정 모드
|
||||||
|
|
||||||
1. 사용자가 /write 페이지 접근
|
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)
|
├─> LocalStorage에서 임시 저장된 글 불러오기 (DraftManager)
|
||||||
└─> 에디터에 복원
|
└─> 에디터에 복원
|
||||||
|
|
||||||
@ -995,7 +1009,9 @@ interface Draft {
|
|||||||
- `src/app/api/spelling/check/route.ts` - 맞춤법 검사 API
|
- `src/app/api/spelling/check/route.ts` - 맞춤법 검사 API
|
||||||
- ~~`src/components/writing/ScoreDisplay.tsx`~~ - ❌ 삭제됨 (하이라이트로 대체)
|
- ~~`src/components/writing/ScoreDisplay.tsx`~~ - ❌ 삭제됨 (하이라이트로 대체)
|
||||||
- ~~`src/components/writing/SpellingErrorDisplay.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
|
- `src/types/draft.ts` - Draft, AnalysisHistoryItem
|
||||||
@ -1138,7 +1154,8 @@ interface Draft {
|
|||||||
- 성공/실패 시 기존 loading toast 제거 후 새 toast 표시
|
- 성공/실패 시 기존 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/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`
|
- `src/managers/WritingManager.ts` - `CreateWritingParams.analysis`
|
||||||
|
|
||||||
#### 비용 절감 효과
|
#### 비용 절감 효과
|
||||||
@ -1561,7 +1579,7 @@ const improvementRate = calculateImprovementRate(spellingErrorsHistory);
|
|||||||
**타입**: `src/types/writing.ts`
|
**타입**: `src/types/writing.ts`
|
||||||
**유틸**: `src/utils/contentHash.ts`
|
**유틸**: `src/utils/contentHash.ts`
|
||||||
**서버**: `src/lib/server/writing.ts`, `src/app/api/analyze-pattern/route.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) - 프로젝트 구조
|
- [PROJECT_STRUCTURE.md](./PROJECT_STRUCTURE.md) - 프로젝트 구조
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user