# 라온누리 - 기술 스택 및 개발 환경 > 최종 업데이트: 2025-11-21 (홈 페이지 모듈화) --- ## 기술 스택 ### Core Framework | 기술 | 버전 | 용도 | |-----|------|------| | **Next.js** | 16.0.0 | React 프레임워크 (App Router) | | **React** | 19.2.0 | UI 라이브러리 | | **TypeScript** | 5.x | 타입 안전성 | ### UI & Styling | 기술 | 버전 | 용도 | |-----|------|------| | **Chakra UI** | v3.28.0 | 컴포넌트 라이브러리 | | **@chakra-ui/charts** | latest | 🆕 **차트 컴포넌트** (Sparkline, Area/Bar/Line 차트) | | **Recharts** | latest | 🆕 **차트 라이브러리** (Chakra Charts 내부 사용) | | **Emotion** | 11.14.0 | CSS-in-JS | | **Framer Motion** | 12.23.24 | 애니메이션 라이브러리 | | **React Icons** | 5.5.0 | 아이콘 세트 | | **Tiptap** | latest | 리치 텍스트 에디터 | | **next-intl** | latest | 🆕 **다국어 지원 (i18n)** | ### Backend & Database | 기술 | 버전 | 용도 | |-----|------|------| | **Firebase** | 12.4.0 | BaaS (Backend as a Service) | | **Firebase Auth** | - | 사용자 인증 | | **Firestore** | - | NoSQL 데이터베이스 (글 저장) | | **Firebase Realtime Database** | - | 🆕 **실시간 데이터 동기화** (글쓰기 모니터링) | | **@google/genai** | 1.29.0 | Google Gemini API SDK (텍스트 분석, 맞춤법 검사) | | **Redis** | - | Cache 데이터 베이스 (예정) | **AI 서비스**: - **Gemini 2.5 Flash-Lite**: 텍스트 분석 (오감/감정/대화/의성어 평가, Delta 전송) - **Gemini 2.5 Flash-Lite**: 맞춤법 검사 (초등학생 눈높이) - **Gemini 2.5 Flash-Lite**: 글 작성 패턴 분석 (최근 10개 글 종합 분석, AI 평가 및 맞춤형 추천) - **Vertex AI 모드**: Multi-region failover 지원 (`vertexai: true`) - **Response Schema**: JSON 응답 강제 (`Type.OBJECT`, `Type.ARRAY` 등) ### Utilities | 기술 | 버전 | 용도 | |-----|------|------| | **use-debounce** | latest | React debounce hook (5초 API 호출 제한) | ### Charts | 기술 | 버전 | 용도 | |-----|------|------| | **@chakra-ui/charts** | latest | 🆕 **Chakra UI 차트 컴포넌트** (실시간 모니터링 그래프) | | **recharts** | latest | 🆕 **차트 라이브러리** (Area, Line, Bar 차트) | ### State Management | 기술 | 버전 | 용도 | |-----|------|------| | **Zustand** | 5.0.8 | 전역 상태 관리 | ### Development Tools | 기술 | 버전 | 용도 | |-----|------|------| | **ESLint** | 9.x | 코드 린팅 | | **babel-plugin-react-compiler** | 1.0.0 | React 컴파일러 최적화 | --- ## 개발 명령어 ### 주요 명령어 ```bash # 개발 서버 시작 (포트 3001) npm run dev # 프로덕션 빌드 npm run build # 프로덕션 서버 시작 (포트 3001) npm start # ESLint 실행 npm run lint ``` ### 중요 사항 - **포트**: 개발/프로덕션 서버 모두 포트 `3001` 사용 (기본 3000이 아님) - **Webpack 모드**: `--webpack` 플래그 사용 (React Compiler 요구사항) - **Turbopack 미사용**: React Compiler와 호환성을 위해 webpack 모드 사용 --- ## 프로젝트 설정 ### Next.js 설정 (`next.config.ts`) ```typescript // React Compiler 활성화 const nextConfig = { reactCompiler: true, // ... 기타 설정 }; ``` ### TypeScript 설정 - **Path Alias**: `@/*` → `./src/*` - 모든 import는 `@/` 경로 사용 ```typescript // 예시 import { useAuthStore } from "@/store/authStore"; import { Navbar } from "@/components/navigation/Navbar"; ``` ### ESLint 설정 - Next.js 공식 ESLint 설정 사용 - React 19 및 Next.js 16 규칙 적용 --- ## Firebase 설정 ### Firebase Config Firebase 설정은 `src/config/firebase.ts`에 직접 하드코딩되어 있습니다: ```typescript // src/config/firebase.ts const firebaseConfig = { apiKey: "AIzaSyBXmSq9Sq81oNkEZsbcbc-YA9LO31URby8", authDomain: "raonnuri-84830.firebaseapp.com", databaseURL: "https://raonnuri-84830-default-rtdb.firebaseio.com", // 🆕 Realtime DB projectId: "raonnuri-84830", storageBucket: "raonnuri-84830.firebasestorage.app", messagingSenderId: "962894843507", appId: "1:962894843507:web:91d41427d4de819c47a406", measurementId: "G-E4VKK56B8G" }; export const fbAuth = getAuth(fbApp); export const fbClient = getFirestore(fbApp); export const fbRealtimeDb = getDatabase(fbApp); // 🆕 ``` **보안 참고**: - Public API Key는 클라이언트 SDK 표준 방식 (Firebase 프로젝트 설정에서 도메인 제한) - 환경변수 대신 코드에 포함 (일반적인 Firebase 클라이언트 앱 패턴) ### 환경 변수 ```bash # 사이트 URL (프로덕션) NEXT_PUBLIC_URL=https://raonnuri.life # API Base URL (선택적, 기본값: /api) NEXT_PUBLIC_API_URL=/api ``` --- ## Firebase 설정 ### 인증 제공자 | 제공자 | 상태 | 설정 위치 | 용도 | |-------|------|----------|------| | **이메일/비밀번호** | ✅ 활성화 | Firebase Console > Authentication | 정식 계정 (학부모/고학년) | | **Google OAuth** | ✅ 활성화 | Firebase Console > Authentication | 정식 계정 (소셜 로그인) | | **Anonymous** | ✅ 활성화 | Firebase Console > Authentication | **학생 팀 코드 로그인** | | **네이버** | 🔜 준비 중 | - | 정식 계정 (소셜 로그인) | | **카카오** | 🔜 준비 중 | - | 정식 계정 (소셜 로그인) | ### Firestore 데이터베이스 ``` 프로젝트 루트 └── firestore.rules # Firestore 보안 규칙 (예정) ``` **컬렉션 구조**: - `writings/` ✅ - 작성한 글 ```typescript { userId: string; title: string; content: string; // HTML wordCount: number; charCount: number; analysis?: { // 🆕 AI 분석 결과 (저장 시 자동 생성) score: number; breakdown: { sensory, emotion, dialogue, onomatopoeia }; foundWords: { sensory[], emotion[], onomatopoeia[] }; suggestions?: string[]; spellingErrors?: SpellingError[]; analyzedAt: Timestamp; contentHash: string; // SHA-256(content) }; status: 'draft' | 'published'; topicId?: string | null; // 주제 ID (null은 자유 주제) createdAt: Timestamp; updatedAt: Timestamp; } ``` - `topics/` ✅ - 글쓰기 주제 (팀 주제 + 개인 주제) ```typescript { title: string; description: string; category: TopicCategory; // Enum: daily | imagination | emotion | experience difficulty: TopicDifficulty; // Enum: easy | medium | hard ownerType: TopicOwnerType; // Enum: system | team | personal ownerId?: string; // 팀 주제: teamId, 개인 주제: userId keywords: string[]; examplePrompts: string[]; titleTemplate?: string; // 제목 템플릿 contentTemplate?: string; // 내용 템플릿 usageCount: number; createdAt: Timestamp; updatedAt: Timestamp; createdBy: string; isActive: boolean; } // 팀 주제: ownerId = teamId 직접 사용 (예: abc123) // 유틸 함수: getTeamOwnerId(teamId), extractTeamId(ownerId) - 단순 반환 ``` - `classrooms/` ✅ - **팀 (팀 코드 시스템)** ```typescript { code: string; // "춤추는 파란 사자" (한글 팀 코드) name: string; // "2학년 1반" ownerId: string; // 팀 소유자 UID securityMode: 'simple' | 'normal' | 'open'; requirePin: boolean; allowAnonymousJoin: boolean; studentIds: string[]; // students 컬렉션 참조 createdAt: Timestamp; updatedAt: Timestamp; isActive: boolean; } ``` - `students/` ✅ - **학생 계정 (독립적, Anonymous Auth 기반)** ```typescript { firebaseUid: string; // Anonymous Auth UID linkedUserId?: string; // 연결된 정식 계정 (선택적, 1:1) name: string; pinHash?: string; // SHA-256 해시 classroomIds: string[]; // 다중 팀 지원 isAnonymous: true; createdAt: Timestamp; lastLoginAt: Timestamp; } ``` - `users/` 🔜 - 사용자 프로필 및 진행 상황 (정식 계정) ```typescript { uid: string; email: string; ownedStudentIds: string[]; // students 컬렉션 ID 배열 role: 'student' | 'parent' | 'teacher'; // ... } ``` - `lessons/` 🔜 - 학습 레슨 - `stickers/` 🔜 - 스티커 마스터 데이터 - `userStickers/` 🔜 - 사용자별 스티커 획득 기록 --- ## Chakra UI v3 커스텀 테마 - **파일**: `src/theme/system.ts` - **브랜드 컬러**: 핑크(#FF6B9D), 오렌지(#FFA07A), 청록(#4ECDC4) - **다크모드**: 시맨틱 토큰으로 자동 전환 - **반응형 타이포그래피**: hero, heading, body 등 텍스트 스타일 정의 - **🆕 슬롯 레시피** (2025-11-10): - `menu`: 커스텀 메뉴 스타일 (애니메이션, hover 효과) - `dialog`: Dialog 자동 배경색, border, shadow - `select`: Select 드롭다운 자동 배경색, hover 효과 - **시맨틱 토큰**: - `bg`, `fg`, `border`: 전역 배경/전경/테두리 색상 - `brand.*`: 브랜드 컬러 시맨틱 토큰 - `navbar.*`, `menu.*`: 컴포넌트별 시맨틱 토큰 - `landing.*`: 랜딩 페이지 전용 토큰 --- ## 아키텍처 패턴 ### 1. Manager 패턴 + API 아키텍처 (3계층 구조) ``` UI Layer (Components/Pages) ↓ 매니저 호출 Manager Layer (비즈니스 로직 + 클라이언트 캐싱) ├─> TeamManager (싱글톤) │ ├─> createTeam() → POST /team │ ├─> getTeam() → GET /team/:id (5분 캐싱) │ ├─> getMyTeams() → GET /team/list (소유+참여 팀, 1분 캐싱) │ ├─> updateTeam() → PUT /team/:id │ ├─> deleteTeam() → DELETE /team/:id │ └─> generateUniqueTeamCode() → POST /team/generate-code │ ├─> UserManager (싱글톤) │ ├─> createUser() → POST /user │ ├─> getUser() → GET /user/:id (Firebase Auth + Firestore 자동 결합, 5분 캐싱) │ ├─> getUsersByTeam() → GET /user/by-team/:teamId (30초 캐싱) │ ├─> updateLastLogin() → POST /user/:uid/update-last-login │ ├─> findUserByNickname() → POST /user/find-by-nickname (Level 1용) │ └─> setUserNickname() → POST /user/:uid/nickname (DEPRECATED - 팀에서 관리) │ ├─> WritingManager (싱글톤) │ ├─> createWriting() │ ├─> getWriting() │ └─> getUserWritings() │ └─> TopicManager (싱글톤) ├─> getAvailableTopics() └─> createPersonalTopic() ↓ HTTP API 호출 API Layer (Next.js API Routes / Server Actions) - 구현 대기 ├─> /api/team/* (팀 관련 엔드포인트) ├─> /api/student/* (학생 관련 엔드포인트) └─> ID Token 검증, 권한 체크, Firestore 접근 ↓ Database Layer ├─> Firestore (영구 데이터) └─> Redis (캐싱, Rate Limiting) - 예정 ``` **Manager 패턴의 장점**: - ✅ **UI와 비즈니스 로직 완전 분리** - ✅ **싱글톤 패턴**으로 전역 인스턴스 관리 - ✅ **클라이언트 사이드 캐싱**: GET 요청 자동 캐싱 (TTL 기반) - ✅ **캐시 무효화**: 변경 작업 시 관련 캐시 자동 삭제 - ✅ **API 추상화**: HTTP 호출 로직을 BaseManager에서 처리 - ✅ **타입 안전성**: Request/Response 타입 완전 정의 - ✅ **테스트 용이성**: API 모킹으로 단위 테스트 가능 - ✅ **유연성**: Firestore 직접 접근 → API 호출로 전환 완료 **BaseManager 기능**: ```typescript // src/managers/ManagerBase.ts abstract class BaseManager { // 인증 protected async getIdToken(): Promise protected getCurrentUserId(): string | null protected isAuthenticated(): boolean // API 호출 protected async authenticatedFetch(endpoint, options) protected async ApiCall(method, endpoint, data) } abstract class SingletonManager extends BaseManager { // 캐싱 protected getCached(key, ttl?): T | null protected setCached(key, data): void protected invalidateCache(key): void protected invalidateCachePattern(pattern): void protected clearCache(): void // API + 캐싱 통합 protected async callApiWithCache(cacheKey, method, endpoint, data, ttl) } ``` **사용 예시**: ```typescript // team/page.tsx import { teamManager } from "@/managers"; // 팀 목록 조회 - 소유한 팀 + 참여한 팀 (1분간 캐싱됨) const teams = await teamManager.getMyTeams(); // 🆕 팀 생성 (5단계 보안 레벨) const teamId = await teamManager.createTeam({ name: "우리반", code: "춤추는파란사자", securityLevel: 2, // 1-5 (명단 기반) allowedNames: ["홍길동", "김철수"] }); // 🆕 보안 레벨 변경 await teamManager.updateSecurityLevel(teamId, 4, true); // Level 4, 자동 명단 생성 // 🆕 닉네임 조회 const nickname = teamManager.getMemberNickname(team, uid, user?.name); ``` **참고 문서**: - [API_SPEC.md](./API_SPEC.md) - 전체 API 명세서 ### 2. 인증 플로우 및 라우팅 (currentStudent 중심 아키텍처) ``` 1. AuthInitializer (클라이언트) └─> initializeAuth() 호출 (마운트 시) └─> onAuthStateChanged 리스너 등록 ├─> firebaseUser.isAnonymous ? │ └─> getStudentByFirebaseUid() → authStore.currentStudent 설정 └─> else ? └─> getStudentsByUserId() → authStore.ownedStudents 설정 2. authStore (Zustand) - 재설계됨 ├─> **currentStudent** (Student | null) - 현재 활동 중인 학생 (필수) ├─> user (User | null) - 정식 계정 (선택적) ├─> ownedStudents (Student[]) - 정식 계정이 소유한 학생들 ├─> isAnonymous (boolean) - Anonymous Auth 여부 ├─> isAuthenticated - 정식 계정 로그인 여부 ├─> isLoading └─> 액션: ├─> login/signup/loginWithGoogle (기존) ├─> **loginAsStudent(classCode, name, pin?)** - 팀 코드 로그인 ├─> **switchStudent(student)** - 학생 전환 ├─> **linkCurrentStudentWithEmail()** - 계정 연결 └─> **linkCurrentStudentWithGoogle()** - Google 계정 연결 3. 인증 기반 라우팅 ├─> 랜딩 페이지 (/) │ └─> 로그인 상태 확인 │ └─> isAuthenticated || currentStudent ? redirect(/home) : 랜딩 표시 │ └─> 유저 홈 (/home) └─> 인증 상태 확인 └─> !currentStudent ? redirect(/) : 대시보드 표시 4. 보호된 페이지 패턴 └─> useAuthStore()로 currentStudent 확인 └─> 미인증 시 redirect(/) 또는 openLoginDialog() ``` ### 3. 글쓰기 및 저장 로직 (Manager 패턴 적용) ``` 1. 사용자가 /write 페이지 접근 ├─> LocalStorage에서 임시 저장된 글 불러오기 (DraftManager) └─> 에디터에 복원 2. 주제 선택 ├─> 작성 중인 내용 없음: 바로 주제 변경 + 템플릿 적용 └─> 작성 중인 내용 있음: ├─> 🆕 **경고 Dialog 표시** │ ├─> "제목과 내용이 모두 초기화됩니다" │ └─> "임시 저장된 내용은 저장된 글조각에서 복구 가능" ├─> 사용자 선택: │ ├─> "취소": 주제 변경 취소 │ └─> "확인하고 초기화": 주제 변경 + 내용 초기화 └─> 확인 시 템플릿 미리채우기 (제목/내용) 3. 글 작성 중 ├─> 제목: Editable 컴포넌트 (인라인 편집) ├─> 본문: Tiptap 순수 텍스트 에디터 (포맷팅 비활성화) │ └─> 초등학생을 위한 단순한 텍스트 입력에 집중 ├─> 2초마다 LocalStorage에 자동 저장 (DraftManager, FIFO) ├─> 저장 상태 표시 (저장 중 → 저장됨 → 시간) └─> 하단 고정 버튼 (취소, 저장) 4. 저장 버튼 클릭 ├─> 미인증 시: 로그인 다이얼로그 표시 └─> 인증 시: └─> writingManager.createWriting() 호출 ├─> 유효성 검사 (제목, 내용) ├─> 텍스트 통계 계산 (글자 수, 단어 수) ├─> Firestore에 저장 └─> LocalStorage draft 삭제 후 /home 이동 5. WritingManager API ├─> createWriting() - 새 글 작성 ├─> getWriting() - 글 조회 ├─> getUserWritings() - 사용자 글 목록 ├─> getRecentWritings() - 최근 글 목록 ├─> updateWriting() - 글 수정 └─> deleteWriting() - 글 삭제 6. DraftManager (클라이언트 전용) ├─> saveDraft() - 글조각 저장 (최대 10개, FIFO) ├─> getDraft() - 글조각 조회 ├─> getAllDrafts() - 전체 글조각 목록 ├─> deleteDraft() - 글조각 삭제 ├─> setCurrentDraftId() - 현재 편집 중인 draft 설정 └─> migrateLegacyDraft() - 기존 단일 draft 마이그레이션 ``` ### 4. 실시간 글쓰기 모니터링 아키텍처 (Firebase Realtime Database) ``` 관리자 (팀 관리 페이지) ↓ 주제 선택 ↓ LiveWritingMonitor 컴포넌트 ├─> 주제 드롭다운 (teamTopics) └─> subscribeToTopic(teamId, topicId, callback) ↓ Firebase Realtime DB 구독 monitoring/{teamId}/{topicId}/{userId} ↑ 5초마다 업데이트 학생 (글쓰기 페이지) ├─> 팀 주제 선택 감지 └─> startMonitoring(teamId, topicId, getStats) ├─> 5초마다 통계 전송 (contentLength, wordCount) ├─> onDisconnect().remove() 설정 └─> 페이지 이탈 시 자동 정리 미리보기 요청-응답 플로우 관리자: requestPreview(userId) ↓ 요청 생성 previewRequests/{userId}/{requestId} ↓ 학생 리스너 감지 학생: listenForPreviewRequests(callback) ↓ 현재 글 내용 전송 previewResponses/{requestId} ↓ 관리자 구독 관리자: Promise 해결 → Dialog 표시 ``` **주요 구성 요소**: - **WritingSessionManager** (`src/managers/WritingSessionManager.ts`): - `startMonitoring(teamId, topicId, getStatsCallback)`: 5초 주기 통계 전송 - `stopMonitoring()`: 전송 중지 + DB 삭제 - `subscribeToTopic(teamId, topicId, callback)`: 실시간 구독 (관리자) - `requestPreview(userId)`: 미리보기 요청 (Promise 반환) - `listenForPreviewRequests(onRequestCallback)`: 미리보기 리스너 (학생) - 상세 디버그 로그 (전송/수신/에러 추적) - **LiveWritingMonitor** (`src/components/team/LiveWritingMonitor.tsx`): - 주제 선택 Select 컴포넌트 (Chakra UI Select) - **모든 팀 멤버 표시** (getUsersByTeam) - **3가지 상태 관리**: - 🟢 작성 중 (isActive: true, lastUpdated < 30초) - 초록 배지, 핑크 테두리 - 🟠 나감 (isActive: false, 마지막 통계 유지) - 주황 배지, 주황 테두리 - ⚪ 대기 중 (한 번도 작성 안 함) - 회색 배지, 투명도 60% - **정렬 순서**: 작성 중 → 나감 → 대기 중 - StudentMonitorCard 컴포넌트 (개별 학생 카드) - 유저 정보와 통계 자동 결합 (userManager 활용) - **작성 속도 실시간 계산** (클라이언트 측, 글자/분) - **Sparkline 그래프** (Area Chart, 최근 10개 히스토리) - **인터랙티브 툴팁** (속도 값 + 몇 초 전 데이터) - 미리보기 Dialog (작성 중인 학생만) - **30초 타임아웃**: 업데이트 없으면 "나감" 처리 - **마지막 통계 유지**: Firebase 삭제되어도 클라이언트 상태 유지 - 마지막 업데이트 시간 표시 ("N초 전") **Realtime DB 구조**: ```json { "monitoring": { "{teamId}": { "{topicId}": { "{userId}": { "userId": "abc123", "contentLength": 1500, "wordCount": 300, "topicId": "topic_123", "lastUpdated": 1731400800000 } } } }, "previewRequests": { "{userId}": { "{requestId}": { "requestedBy": "admin_uid", "timestamp": 1234567890, "requestId": "req_xyz" } } }, "previewResponses": { "{requestId}": { "content": "현재 작성 중인 글...", "timestamp": 1234567890, "requestId": "req_xyz" } } } ``` **Security Rules** (`database.rules.json`): ```json { "rules": { "monitoring": { "$teamId": { "$topicId": { ".read": "auth != null", "$userId": { ".write": "auth != null && auth.uid == $userId" } } } }, "previewRequests": { "$userId": { ".read": "auth != null && auth.uid == $userId", ".write": "auth != null" } }, "previewResponses": { "$requestId": { ".read": "auth != null", ".write": "auth != null" } } } } ``` **권한 정책**: - 통계 읽기: 인증된 모든 사용자 (팀 소유자만 UI 접근 가능) - 통계 쓰기: 본인만 - 미리보기: 요청자와 대상자만 **작성 속도 계산 로직** (클라이언트 측): ```typescript // 5초마다 데이터 수신 const charDiff = 현재글자수 - 이전글자수; const speed = charDiff * 12; // 5초 * 12 = 60초(1분) // 히스토리 저장 (최근 10개) speedHistory.push({ speed, timestamp: Date.now() }); if (speedHistory.length > 10) speedHistory.shift(); // Sparkline 그래프로 시각화 - Area Chart (면적 그래프) - Teal 색상, 투명도 30% - 툴팁: 마우스 오버 시 "N자/분" + "N초 전" 표시 - 0도 표시 (작성 멈춤 시각화) ``` **비용 효율성**: - Firebase Realtime DB Spark 플랫폼: 동시 접속 100명까지 **완전 무료** - GB 다운로드 기반 과금 (쓰기/읽기 횟수 무관) - 30명 × 1시간 수업 = ~1.5MB (무료 한도 1GB/day의 0.15%) ### 5. 상태 관리 원칙 - **전역 상태**: Zustand 사용 (인증, 사용자 진행 상황, 알림) - **로컬 상태**: `useState` 사용 (폼 입력, UI 토글, 에디터 내용) - **로컬 저장소**: LocalStorage (임시 저장 글) - **서버 상태**: Firestore 직접 호출 (React Query는 나중에 고려) ### 6. 태그 입력 필드 패턴 (Tag Input Field) CreateTopicDialog (개인/팀 공용)의 제목 템플릿 입력에 사용되는 고급 UI 패턴입니다. **통합 Dialog 설계**: - `TopicFormData` export: 입력 데이터만 수집하여 반환 - `onSubmit` 콜백 패턴: 부모 컴포넌트가 팀/개인 주제 생성 결정 - **관심사 분리**: Dialog는 UI만 담당, 비즈니스 로직은 부모에게 위임 **구현 방식**: ```typescript // 상태 구조 type TemplatePart = { id: string; type: "text" | "placeholder"; value: string; // "{date}" 또는 일반 텍스트 label?: string; // "날짜" (placeholder인 경우) }; const [templateParts, setTemplateParts] = useState([]); const [currentInput, setCurrentInput] = useState(""); const [selectedPartIndex, setSelectedPartIndex] = useState(null); ``` **주요 기능**: - ✅ 자동 플레이스홀더 감지: `{date}`, `{time}` 등 입력 시 자동으로 태그 변환 - ✅ 키보드 네비게이션: - `←` (왼쪽 화살표): 이전 part 선택 - `→` (오른쪽 화살표): 다음 part 선택 또는 입력 필드로 복귀 - `Backspace`: 선택된 part 삭제 (입력이 비어있으면 마지막 part 선택) - `Delete`: 선택된 part 삭제 (다음 part로 이동) - `Enter`: 현재 입력 확정 - ✅ 마우스 인터랙션: - part 클릭: 해당 part 선택 - × 버튼: 즉시 삭제 - ✅ 시각적 피드백: - 선택된 텍스트: 파란 배경 + 파란 테두리 - 선택된 태그: 진한 파란 테두리 + 투명도 감소 **사용 사례**: - 템플릿 입력 (제목/내용 템플릿) - 이메일 받는 사람 입력 (Gmail 스타일) - 태그 입력 (해시태그, 키워드) - 멘션 입력 (Slack, Discord 스타일) **참고 파일**: `src/components/writing/CreateTopicDialog.tsx:61-321` --- ### 6. 팀 코드 시스템 아키텍처 (초등 저학년 로그인 간소화) #### 핵심 개념 **문제**: 초등 저학년은 이메일/비밀번호 로그인이 어려움 **해결**: 팀 소유자가 팀 코드를 발급하고, 학생은 간단히 로그인 **아키텍처**: currentStudent 중심, 정식 계정은 선택사항 #### 계정 구조 ``` 정식 계정 (User) - 선택적 ├─ Firebase Auth: user456 (Email/Google) ├─ Firestore: users/user456 └─ ownedStudentIds: ["studentDoc1", "studentDoc2"] │ ├─> 학생 계정 1 (Student) - 독립적, 필수 │ ├─ Firebase Auth: anon123 (Anonymous) │ ├─ Firestore: students/studentDoc1 │ ├─ linkedUserId: user456 (1:1 관계) │ ├─ teamIds: ["team1", "team2"] │ └─ 모든 활동 데이터(writings, topics)는 studentId로 기록 │ └─> 학생 계정 2 (Student) └─ ... (동일 구조) ``` #### 팀 코드 생성 **한글 3단어 조합**: `[형용사/동사] + [색상] + [동물]` ``` 예시: "춤추는 파란 사자" 조합 수: - 형용사/동사: 100개 - 색상: 20개 - 동물: 50개 → 총 100,000가지 조합 (10만 개) 특징: ✅ 기억하기 쉬움 (이미지 연상) ✅ 구두 전달 가능 (말로 쉽게 전달) ✅ 타이핑 오타 최소화 (자동완성 가능) ✅ 초등 저학년도 이해 가능 ✅ 재미있고 친근함 (팀 정체성 형성) ``` **파일**: `src/data/classCodeWords.ts`, `src/utils/classCodeGenerator.ts` #### 학생 로그인 플로우 (개선됨 - 2025-11-06) ``` 1. 학생 로그인 (팀 코드 3단계) Step 1: 팀 코드 입력 ├─> "춤추는 파란 사자" 입력 ├─> Firestore teams 조회 ├─> 소유자 체크 (본인 팀이면 /manage로 리다이렉트) └─> Step 2로 진행 Step 2: 이름 입력 (선택 → 입력으로 개선) ├─> 이름 직접 입력 (예: "김민지") ├─> PIN 필요하면 Step 3 (PIN) └─> PIN 불필요하면 Step 3 (완료 화면) Step 3: PIN 입력 또는 완료 화면 ├─> [PIN 필요] 숫자 패드로 PIN 입력 → 검증 → 완료 화면 └─> [완료 화면] 참여/로그인 구분 ├─> 신규: 🎉 "환영합니다! {이름}님, {팀명}에 참여했어요" └─> 재로그인: 👋 "반가워요! {이름}님, 다시 돌아왔군요!" 2. 로그인 완료 ├─> Anonymous Auth 유지/생성 ├─> authStore.currentStudent 설정 └─> /team/[teamId] 멤버 페이지로 이동 2. 정식 계정 연결 (선택적, 학부모/고학년) ├─> 설정 → "내 계정 만들기" ├─> 이메일 회원가입 또는 Google 로그인 ├─> linkWithCredential() 호출 │ └─ Anonymous(anon123) → Email(user456) 전환 ├─> Firestore 연결: │ ├─ students/studentDoc.linkedUserId = user456 │ └─ users/user456.ownedStudentIds = [studentDoc] └─> 이후 user456으로 로그인 가능 (currentStudent 자동 설정) 3. 정식 계정 로그인 (학생 자동 선택) ├─> user456으로 로그인 ├─> Firestore users/user456 조회 ├─> ownedStudentIds로 students 조회 ├─> 학생이 1명: 자동 선택 ├─> 학생이 2명+: StudentPicker 표시 (누구로 활동할까요?) └─> authStore.currentStudent 설정 ``` #### 보안 모드 | 모드 | 인증 단계 | 사용 사례 | |------|----------|----------| | **simple** | 팀 코드 + 이름 | 교실 전용, 저학년 (1-2학년) | | **normal** | 팀 코드 + 이름 + PIN | 가정 학습 포함, 고학년 (3-4학년) | | **open** | 팀 코드 + 자유 가입 | 전학생, 체험 학생 허용 | #### Rate Limiting (학생 친화적) ``` 5회 실패: 💡 "어려우면 팀을 만든 사람에게 물어보세요!" 10회 실패: ⚠️ "입력을 확인해주세요. 띄어쓰기는 안 해도 괜찮아요!" 15회 실패: 🔒 "2분 후에 다시 시도해주세요. 팀 관리자에게 도움을 요청하세요." ``` #### 데이터 흐름 ``` 모든 활동 데이터는 studentId로 기록: writings/{writingId} ├─ studentId: "studentDoc1" ← 핵심! (userId 아님) ├─ title: "나의 하루" └─ content: "..." 조회 시: - 팀 코드 계정: getUserWritings(currentStudent.id) - 정식 계정: ownedStudents.map(s => getUserWritings(s.id)) ``` **참고 파일**: - `src/managers/TeamManager.ts` - 팀 관련 API 호출 + 캐싱 - `src/managers/StudentManager.ts` - 학생 관련 API 호출 + 캐싱 - `src/managers/ManagerBase.ts` - API 호출 및 캐싱 공통 로직 - `src/services/firebaseAuth.ts:125-316` - 학생 로그인 로직 - `src/store/authStore.ts` - currentStudent 중심 상태 관리 - `src/types/api/team.ts` - 팀 API 타입 정의 - `src/types/api/student.ts` - 학생 API 타입 정의 - `API_SPEC.md` - 전체 API 명세서 (23개 엔드포인트) --- ### 7. 실시간 피드백 시스템 (3-Layer 아키텍처) + 맞춤법 검사 #### 핵심 개념 **목적**: Gemini API로 초등학생 글쓰기를 실시간 평가하면서 비용 최적화 **문제**: - 매번 전체 텍스트 전송 = 비용 폭증 - Rate Limit (15 RPM) = 사용자 2명만 접속해도 초과 **해결**: Delta 전송 + 캐싱 + Multi-Region Failover + 분석 히스토리 #### 아키텍처 ``` ┌─────────────────────────────────────────┐ │ Layer 1: API Route │ │ src/app/api/analyze-text/route.ts │ │ │ │ 역할: │ │ - Delta 계산 (변경된 부분만 추출) │ │ - 서버 캐싱 (In-Memory LRU, TTL 1분) │ │ - textAnalysisService 호출 │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ Layer 2: Business Logic │ │ src/services/textAnalysisService.ts │ │ │ │ 역할: │ │ - 프롬프트 생성 (히스토리 포함) │ │ - Response Schema 정의 (Type.OBJECT) │ │ - JSON 파싱 (AI 응답 처리) │ │ - 점수 제한 (최대 10점) │ │ - vertexAI 호출 │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ Layer 3: Infrastructure │ │ src/services/vertexAI.ts │ │ │ │ 역할: │ │ - GoogleGenAI 클라이언트 관리 (SDK) │ │ - Response Schema 전달 (JSON 강제) │ │ - Multi-region failover │ │ - Retry with exponential backoff │ │ - regionHealthManager 연동 │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ 맞춤법 검사 (독립적) │ │ src/services/spellingService.ts │ │ │ │ 역할: │ │ - Gemini 기반 맞춤법 검사 │ │ - Response Schema (SpellingError[]) │ │ - 초등학생 눈높이 설명 생성 │ │ - 별도 debounce (5초) │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ Region Health Manager │ │ src/services/regionHealthManager.ts │ │ │ │ 역할: │ │ - Region별 과부하 상태 추적 │ │ - 429 에러 시 1분간 region 제외 │ │ - 자동 복구 (1분 경과 시) │ └─────────────────────────────────────────┘ ``` #### Multi-Region Strategy **사용 가능한 Regions** (우선순위 순): ``` 1. asia-northeast1 (도쿄) - 한국 최근접, ~50ms 🥇 2. asia-southeast1 (싱가포르) - 백업, ~100ms 🥈 3. us-central1 (미국) - 최종 대체, ~200ms 🥉 ``` **장애 시나리오**: ``` 요청 → 도쿄 region ↓ 429 Rate Limit 도쿄를 1분간 "과부하" 마킹 ↓ 다음 요청 → 싱가포르 (자동 전환) ↓ 성공 ✅ 계속 싱가포르 사용 ↓ 1분 후 도쿄 자동 복구 ↓ 다시 도쿄 우선 사용 (빠름) ``` #### Delta 전송 메커니즘 **문제**: 500자 전체 전송 = 토큰 낭비 **해결**: 변경분만 전송 ```typescript // 클라이언트 const previousText = "오늘 날씨가 좋다."; // 15자 const currentText = "오늘 날씨가 좋다. 하늘이 맑다."; // 24자 fetch('/api/analyze-text', { body: JSON.stringify({ text: currentText, previousText: previousText // Delta 계산용 }) }); // 서버 const delta = text.slice(previousText.length); // " 하늘이 맑다." (9자) // → 9자만 Vertex AI로 전송 (60% 절감) ``` #### 성능 최적화 **비용 절감**: ``` 순수 AI: $0.18/글 Debounce: $0.036/글 (80% 절감) Delta: $0.014/글 (92% 절감) Delta + Cache: $0.009/글 (95% 절감) ⭐ ``` **처리량 증가**: ``` Single Region: 15 RPM Multi-Region (3개): 45 RPM (3배) 동시 사용자: 1~2명 → 3~5명 ``` **가용성 향상**: ``` Single: 95% Multi-Region: 99.9% (자동 failover) ``` #### 새로운 평가 기준 (2025-11-11 개편) **총 10점 = 오감(4) + 감정(2) + 대화(2) + 의성어(2)** | 항목 | 배점 | 설명 | |------|------|------| | **오감 표현** | 0~4점 | 시각/청각/후각/미각/촉각, 1개당 +1점 | | **감정 표현** | 0~2점 | 기쁨/슬픔/놀람 등, 1개당 +1점 | | **대화 표현** | 0~2점 | 큰따옴표(" ") 사용 시 +2점 | | **의성어/의태어** | 0~2점 | 쿵쿵, 반짝반짝 등, 1개당 +1점 | **변경사항**: - ❌ `descriptive` (감각/감정 형용사, 0~3점) - ✅ `emotion` (감정 표현, 0~2점) - ✅ 오감과 감정 분리로 평가 명확화 #### 분석 히스토리 시스템 **Draft 타입 확장**: ```typescript interface AnalysisHistoryItem { version: number; // 1, 2, 3, ... content: string; // 해당 버전의 텍스트 timestamp: string; // ISO string analysis: { // 분석 결과 전체 score: number; breakdown: {...}; foundWords: {...}; suggestions: string[]; }; } interface Draft { // ... 기존 필드 analysisHistory?: AnalysisHistoryItem[]; // 최대 5개 } ``` **AI 피드백 개선**: - ✅ 이전 버전과 비교하여 개선점 칭찬 - ✅ 학생의 발전 과정 인정 - ✅ 제안 최소화 (0~1개, 정말 부족한 것만) - ✅ 7점 이상이면 칭찬만 #### 참고 파일 **서비스 레이어**: - `src/services/vertexAI.ts` - Gemini API 범용 래퍼 (`@google/genai`) - `src/services/textAnalysisService.ts` - 텍스트 분석 (히스토리 기반) - `src/services/spellingService.ts` - 🆕 맞춤법 검사 (독립적) - `src/services/regionHealthManager.ts` - Region 상태 관리 **API & 컴포넌트**: - `src/app/api/analyze-text/route.ts` - 텍스트 분석 API - `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/types/draft.ts` - Draft, AnalysisHistoryItem **유틸리티**: - `src/utils/koreanWordList.ts` - 감각 동사/형용사 목록 **문서**: - `TECHNICAL_IMPLEMENTATION.md` - 상세 기술 구현 가이드 - `SERVICE_DIRECTION.md` - 서비스 방향성 논의 (8차) --- ### 8. 글 작성 패턴 분석 시스템 #### 핵심 개념 **목적**: 사용자의 여러 글을 분석하여 작성 패턴, 강점, 약점을 파악하고 맞춤형 피드백 제공 **분석 항목**: 1. **작성 스타일**: 평균 글자/단어 수, 선호하는 글 길이, 문장 구조 (단문/복문/혼합) 2. **표현력 분석**: 평균 점수, 강점/약점, 카테고리별 점수, 자주 쓰는 표현 3. **맞춤법 경향**: 자주 하는 실수, 개선율 4. **발전 추이**: 최근 5개 vs 이전 5개 비교, 개선 영역, 주의 필요 영역 5. **AI 종합 평가**: 전반적 평가, 격려 메시지, 맞춤형 추천 3가지 **서비스 레이어**: - `patternAnalysisService.ts` - 10개 글 종합 분석, Gemini 기반 AI 평가, Response Schema 사용 **API**: - `POST /api/analyze-pattern` - 최근 10개 글 분석 (5분 캐싱) - 인증 필요 (ID Token) - Firestore에서 글 조회 → 각 글 AI 분석 (병렬) → 패턴 분석 → 캐싱 **컴포넌트**: - `WritingPatternDialog` - 패턴 분석 다이얼로그 (로딩/에러/성공 상태) - `WritingPatternDisplay` - 분석 결과 표시 (종합 평가, 발전 추이, 작성 스타일, 표현력, 강점/약점, 자주 쓰는 표현, 추천) **타입**: - `src/types/writingPattern.ts` - WritingPatternAnalysis, AnalyzePatternRequest/Response **참고 파일**: - `src/services/patternAnalysisService.ts` - 패턴 분석 로직 - `src/app/api/analyze-pattern/route.ts` - 패턴 분석 API - `src/components/writing/WritingPatternDialog.tsx` - 다이얼로그 - `src/components/writing/WritingPatternDisplay.tsx` - 표시 컴포넌트 - `src/app/home/page.tsx` - "작성 패턴 분석" 카드 추가 --- ### 9. 실시간 하이라이트 시스템 (Tiptap Extensions) #### 핵심 개념 **목적**: 에디터에서 맞춤법 오류와 감각 단어를 실시간으로 시각적으로 표시 **하이라이트 종류**: 1. **맞춤법 오류**: 빨간 물결 밑줄 (`spelling-error` 클래스) 2. **감각 동사**: 초록색 하이라이트 (`sensory-word` 클래스) 3. **감각 형용사**: 파란색 하이라이트 (`emotion-word` 클래스) 4. **의성어/의태어**: 보라색 하이라이트 (`onomatopoeia-word` 클래스) **Tiptap Extensions**: - `SpellingHighlight` - 맞춤법 오류 하이라이트 - data-original, data-correction, data-reason 속성 - Meta를 통한 강제 업데이트 - `SensoryWordHighlight` - 감각 단어 하이라이트 - data-word, data-type 속성 - 색상별 구분 (초록/파랑/보라) **DecorationSet 기반**: - ProseMirror Decoration을 사용한 효율적인 하이라이트 - 문서 변경 시 자동 업데이트 - 하이라이트 위치를 정확하게 추적 **WritingEditor 통합**: - `spellingErrors`, `foundWords` props 추가 - Extension 옵션 실시간 업데이트 - 브라우저 기본 맞춤법 검사 비활성화 (`spellcheck="false"`) **참고 파일**: - `src/extensions/spelling-highlight.ts` - 맞춤법 하이라이트 Extension - `src/extensions/sensory-word-highlight.ts` - 감각 단어 하이라이트 Extension - `src/components/writing/WritingEditor.tsx` - Extensions 통합 --- ### 10. 인터랙티브 툴팁 시스템 #### 핵심 개념 **목적**: 하이라이트된 단어를 클릭하면 상세 정보를 툴팁으로 표시 **툴팁 종류**: 1. **맞춤법 오류 툴팁**: - 원본 → 수정 (취소선 → 굵은 글씨) - 이유 설명 (초등학생 눈높이) - 빨간색 테두리 2. **감각 단어 툴팁**: - 단어 표시 - 단어 타입 (감각 동사/형용사/의성어) - 격려 메시지 ("이렇게 구체적으로 표현하면 글이 더 생생해져요!") - 색상별 테두리 (초록/파랑/보라) **기술 구현**: - Portal 사용 (z-index 문제 해결) - 클릭한 요소의 data attributes 읽기 - 외부 클릭/ESC 키로 닫기 - Fade-in 애니메이션 **WritingEditor 통합**: - DOM 클릭 이벤트 리스너 등록 - 하이라이트 클래스 확인 (spelling-error, sensory-word 등) - 툴팁 위치 계산 (getBoundingClientRect) - 여러 개 툴팁 동시 표시 가능 (배열 관리) **참고 파일**: - `src/components/writing/EditorTooltip.tsx` - 툴팁 컴포넌트 - `src/components/writing/WritingEditor.tsx` - 클릭 이벤트 처리 --- ### 11. Toast 알림 시스템 #### 핵심 개념 **목적**: 텍스트 분석 및 맞춤법 검사 진행 상태를 사용자에게 알림 **알림 종류**: 1. **텍스트 분석**: - 시작: "글을 분석하고 있어요..." (loading, duration: Infinity) - 완료: "분석 완료! 점수: X.X점" (success, 3초) - 실패: "분석 실패, 다시 시도해주세요." (error, 3초) 2. **맞춤법 검사**: - 시작: "맞춤법을 검사하고 있어요..." (loading, duration: Infinity) - 완료: "맞춤법 검사 완료! X개의 오류 발견" 또는 "맞춤법 오류 없음!" (success, 3초) - 실패: "맞춤법 검사 실패" (error, 3초) **기술 구현**: - Chakra UI Toaster 사용 - Toast ID 관리 (ref로 저장, dismiss 호출) - 로딩 상태 toast는 수동으로 dismiss - 성공/실패 시 기존 loading toast 제거 후 새 toast 표시 **참고 파일**: - `src/app/write/page.tsx` - Toast 알림 통합 --- ### 12. 다국어 지원 시스템 (i18n) #### 핵심 개념 **목적**: 한국어/영어 사용자 모두 접근 가능한 글로벌 플랫폼 구축 **라이브러리**: next-intl (Next.js App Router 표준 i18n 라이브러리) **지원 언어**: - 한국어 (ko) - 기본값 - 영어 (en) - 일본어 (ja) - 어린이 친화적 표현 (한자 최소화, ひらがな 우선) #### 아키텍처 ``` 브라우저 요청 (/) ↓ Middleware (src/middleware.ts) ├─> Accept-Language 헤더 확인 ├─> NEXT_LOCALE 쿠키 확인 └─> 적절한 locale로 리다이렉트 ├─> 한국어 우선: /ko └─> 영어 우선: /en ↓ [locale] 라우팅 (src/app/[locale]/*) ├─> layout.tsx (locale별 레이아웃) │ ├─> NextIntlClientProvider (번역 메시지 주입) │ ├─> Provider (Chakra UI) │ ├─> Navbar (다국어 메뉴) │ └─> AuthInitializer │ └─> page.tsx (각 페이지) └─> useTranslations('namespace') 훅 사용 └─> {t('key')} 형태로 번역 표시 ``` #### 번역 파일 구조 **파일 위치**: `messages/{locale}.json` ```json // messages/ko.json { "site": { "name": "라온누리", "tagline": "재미있게 글쓰기를 배워보자!", "subtitle": "친구들과 함께 신나는 글쓰기 모험을 떠나요" }, "navbar": { "home": "홈", "write": "글쓰기", "learn": "학습하기", "stickers": "스티커" }, "landing": { "hero": { "cta": "지금 시작하기", "teamCode": "팀 코드로 참여" }, "features": {...}, "howItWorks": {...} }, "home": { "hero": { "welcome": "환영합니다, {name}님!", // 파라미터 지원 "subtitle": "오늘도 멋진 글쓰기를 시작해볼까요?" }, "quickStart": {...} } } ``` #### 설정 파일 **i18n/routing.ts** - 라우팅 설정: ```typescript export const routing = defineRouting({ locales: ['ko', 'en', 'ja'], defaultLocale: 'ko', localePrefix: 'always', // URL에 항상 표시 (/ko/*, /en/*, /ja/*) localeDetection: true // 브라우저 언어 자동 감지 }); // next-intl 타입 안전 내비게이션 API export const {Link, redirect, usePathname, useRouter} = createNavigation(routing); ``` **i18n/request.ts** - 번역 메시지 로더: ```typescript export default getRequestConfig(async ({requestLocale}) => { let locale = await requestLocale; if (!locale || !routing.locales.includes(locale)) { locale = routing.defaultLocale; } return { locale, messages: (await import(`../../messages/${locale}.json`)).default }; }); ``` **middleware.ts** - 자동 언어 감지: ```typescript import createMiddleware from 'next-intl/middleware'; import {routing} from './i18n/routing'; export default createMiddleware(routing); export const config = { matcher: ['/', '/(ko|en)/:path*'] }; ``` #### 컴포넌트 사용 패턴 **Server Component** (기본): ```typescript import {useTranslations} from 'next-intl'; export default function Page() { const t = useTranslations('namespace'); return

{t('key')}

; } ``` **Client Component**: ```typescript "use client"; import {useTranslations} from 'next-intl'; export default function ClientComponent() { const t = useTranslations('namespace'); return

{t('key')}

; } ``` **파라미터가 있는 번역**: ```typescript const t = useTranslations('home'); // messages/ko.json: "welcome": "환영합니다, {name}님!"

{t('hero.welcome', {name: userName})}

// → "환영합니다, 홍길동님!" ``` **타입 안전 Link** (locale 자동 처리): ```typescript import {Link} from '@/i18n/routing'; 홈으로 // 현재 locale이 ko면 → /ko/home // 현재 locale이 en이면 → /en/home ``` #### 언어 전환 버튼 **LocaleSwitcher** (`src/components/navigation/LocaleSwitcher.tsx`): ```typescript const LOCALES = [ {code: 'ko', name: '한국어', flag: '🇰🇷'}, {code: 'en', name: 'English', flag: '🇺🇸'}, {code: 'ja', name: '日本語', flag: '🇯🇵'}, ]; const locale = useLocale(); const currentLocale = LOCALES.find(l => l.code === locale); return ( {LOCALES.map((loc) => ( handleLocaleChange(loc.code)}> {loc.flag} {loc.name} {locale === loc.code && } ))} ); ``` **동작**: - 드롭다운 메뉴에서 언어 선택 - 국기 이모지로 시각적 구분 - 현재 언어에 체크 마크 표시 - `NEXT_LOCALE` 쿠키에 저장 (다음 방문 시 기억) #### 브라우저 언어 자동 감지 **첫 방문 시나리오**: ``` 1. 사용자가 / 접속 (쿠키 없음) 2. Middleware가 Accept-Language 헤더 확인 - "en-US,en;q=0.9" → /en/으로 리다이렉트 - "ko-KR,ko;q=0.9" → /ko/로 리다이렉트 - "ja-JP,ja;q=0.9" → /ja/로 리다이렉트 - 지원하지 않는 언어 → /ko/ (기본값) 3. NEXT_LOCALE 쿠키 저장 다음 방문 시: 1. 쿠키에서 저장된 언어 확인 2. 해당 언어로 바로 리다이렉트 ``` #### 완성된 다국어 페이지 | 페이지 | 경로 | 상태 | 번역 항목 | |-------|------|------|-----------| | **Navbar** | 모든 페이지 | ✅ 완료 | 홈, 글쓰기, 학습하기, 스티커 (4개) | | **Landing** | `/[locale]` | ✅ 완료 | Hero(사이트명, 태그라인, CTA 버튼), Features(4개 카드), Steps(3단계), CTA 섹션, Footer (총 20+ 항목) | | **Home** | `/[locale]/home` | ✅ 완료 | 웰컴 메시지, QuickStart(9개 액션 카드), RecentActivity (총 15+ 항목) | | **Auth** | LoginDialog 등 | ✅ 완료 | LoginDialog, LoginForm, SignupForm, UserProfileButton, StudentLoginFlow, SavedDraftsDialog (총 50+ 항목) | | **Team** | `/[locale]/team/*` | ✅ 완료 | List, Create, Detail, Manage + SecurityLevelSelector (총 60+ 항목) | | **Write** | `/[locale]/write` | ✅ 완료 | 분석/저장 메시지, 버튼, 상태 표시 (총 20+ 항목) | **총 번역 키**: 220개 이상 (ko.json, en.json) #### next.config.ts 설정 ```typescript import createNextIntlPlugin from 'next-intl/plugin'; const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts'); const nextConfig = { reactCompiler: true, }; export default withNextIntl(nextConfig); ``` #### 참고 파일 **설정**: - `src/i18n/routing.ts` - 라우팅 설정 - `src/i18n/request.ts` - 번역 로더 - `src/middleware.ts` - 언어 감지 미들웨어 - `messages/ko.json`, `messages/en.json` - 번역 파일 **컴포넌트**: - `src/components/navigation/LocaleSwitcher.tsx` - 언어 전환 버튼 - `src/components/navigation/Navbar.tsx` - 다국어 메뉴 - `src/app/[locale]/page.tsx` - Landing 페이지 (전체 번역) - `src/app/[locale]/home/page.tsx` - Home 페이지 (전체 번역) **타입 안전성**: - next-intl은 타입 추론을 지원하지만, JSON 구조에 따라 자동 완성됨 - 없는 키 사용 시 런타임 에러 (개발 모드에서는 경고) #### 장점 ✅ **타입 안전**: useTranslations 훅으로 타입 체크 ✅ **Server/Client 지원**: RSC에서도 번역 가능 ✅ **자동 코드 스플리팅**: 필요한 번역만 로드 ✅ **SEO 친화적**: locale별 URL (/ko/*, /en/*) ✅ **쿠키 기반 기억**: 사용자 선택 언어 저장 ✅ **브라우저 자동 감지**: Accept-Language 헤더 --- ### 13. 개별 글 분석 결과 저장 및 재사용 시스템 (AI 비용 최적화) #### 핵심 개념 **목적**: 글마다 AI 분석 결과를 저장하여 패턴 분석 시 재분석 방지 **문제**: - 패턴 분석 시 매번 모든 글을 재분석 (10개 글 = 10회 AI 호출) - 글 1개만 수정해도 전체 재분석 (9개 글은 변경 없음) - AI 비용 폭증 (사용자당 $0.50/일) **해결**: contentHash 기반 분석 결과 저장 + 변경 감지 #### 데이터 구조 ```typescript // Writing 타입에 analysis 필드 추가 interface Writing { id: string; content: string; // ...기존 필드 analysis?: WritingAnalysis; // 🆕 AI 분석 결과 } interface WritingAnalysis { score: number; // AI 평가 점수 breakdown: { sensory: number; // 오감 표현 점수 emotion: number; // 감정 표현 점수 dialogue: number; // 대화 표현 점수 onomatopoeia: number; // 의성어 점수 }; foundWords: { sensory: string[]; emotion: string[]; onomatopoeia: string[]; }; suggestions?: string[]; spellingErrors?: SpellingError[]; // 맞춤법 오류 목록 analyzedAt: Date; contentHash: string; // SHA-256(content) } ``` #### 아키텍처 ``` 글 저장 플로우: 저장 버튼 클릭 ↓ AI 분석 + 맞춤법 검사 (병렬) ↓ WritingAnalysis 생성 ├─ score, breakdown, foundWords ├─ spellingErrors ├─ contentHash (SHA-256) └─ analyzedAt ↓ Firestore 저장 (writing + analysis) 패턴 분석 플로우: 패턴 분석 요청 ↓ writings 조회 (analysis 포함) ↓ 각 글마다 체크: ├─ analysis 있음? │ └─ contentHash 일치? │ ├─ YES → 재사용 ⚡ (0 AI 호출) │ └─ NO → 재분석 💰 (1 AI 호출) └─ analysis 없음? → 재분석 💰 ↓ 패턴 분석 수행 (종합) ``` #### 구현 파일 **타입**: - `src/types/writing.ts` - `WritingAnalysis`, `SpellingError` 타입 - `src/types/api/writing.ts` - `CreateWritingRequest.analysis` 추가 **유틸**: - `src/utils/contentHash.ts` - `generateWritingContentHash()` 추가 **서버**: - `src/lib/server/writing.ts` - `createWriting()` analysis 저장 - `src/app/api/writing/route.ts` - analysis 전달 - `src/app/api/analyze-pattern/route.ts` - analysis 재사용 로직 **클라이언트**: - `src/app/[locale]/write/page.tsx` - 저장 시 분석 수행 - `src/managers/WritingManager.ts` - `CreateWritingParams.analysis` #### 비용 절감 효과 **시나리오 1: 10개 글, 첫 패턴 분석** ``` 조회: 10 reads AI 분석: 10회 (analysis 없음) 비용: $0.20 ``` **시나리오 2: 10개 글, 재분석 (변경 없음)** ``` 조회: 10 reads AI 분석: 0회 (모든 contentHash 일치) 비용: $0.01 (Firestore만) 절감: 95% ✅ ``` **시나리오 3: 10개 글, 1개 수정 후 재분석** ``` 조회: 10 reads AI 분석: 1회 (9개는 contentHash 일치) 비용: $0.03 절감: 85% ✅ ``` **연간 비용** (사용자 1000명 기준): ``` 이전: $180,000/년 개선: $18,000/년 절감: $162,000/년 (90%) 💰 ``` #### 맞춤법 에러 히스토리 완성 이제 `spellingErrorsHistory`가 실제 데이터로 채워짐: ```typescript // 패턴 분석 API const spellingErrorsHistory = writings.map( (writing) => writing.analysis?.spellingErrors || [] ); // patternAnalysisService.ts에서 활용 const commonErrors = extractCommonErrors(spellingErrorsHistory); // → [{ error: "했읍니다", correction: "했습니다", frequency: 15 }, ...] const improvementRate = calculateImprovementRate(spellingErrorsHistory); // → 최근 5개 vs 이전 5개 에러 개수 비교 ``` #### 참고 파일 **타입**: `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` --- ### 10. AI 이미지 생성 시스템 (Vertex AI Imagen 4.0) #### 핵심 개념 **목적**: 학생이 작성한 글의 장면을 자동으로 추출하여 일관된 스타일의 삽화 이미지 생성 **기술 스택**: - **Vertex AI Imagen 4.0 Fast**: Google 최신 이미지 생성 모델 - **Gemini 2.5 Flash**: 장면 추출 및 프롬프트 최적화 - **Multi-region Failover**: us-east5 → us-south1 → us-central1 #### 일관된 스타일 가이드 (2025-11-20 개선) **화풍**: 따뜻한 디지털 일러스트, 애니메이션/만화 스타일 **색감**: 부드럽고 따뜻한 톤, 자연스러운 채도 **분위기**: 친근하고 밝은, 초등학생 눈높이에 맞는 **피해야 할 요소**: - ❌ 지나치게 귀여운 데포르메 - ❌ 과도한 사실주의 (semi-realistic, photorealistic) - ❌ 어두운 분위기, 미국식 스타일 **Negative Prompt** (강화): ``` text, words, letters, watermark, signature, overly cute, chibi style, excessive realism, photorealistic, semi-realistic, dark atmosphere, gloomy, oversaturated colors, low quality, blurry, distorted, poorly drawn ``` #### 아키텍처 플로우 ``` 1️⃣ 글 작성 완료 ↓ 저장된 글 상세 페이지 ├─> "이미지 생성" 버튼 클릭 └─> GenerateImageDialog 열기 2️⃣ 장면 추출 (Step 1: Extracting) ↓ POST /api/extract-scenes ├─> body: { title, content, locale } ├─> sceneExtractionService.extractScenes() │ ├─> Gemini 2.5 Flash로 장면 분석 │ ├─> 2-5개 주요 장면 추출 │ └─> Response Schema (Scene[]) │ └─> { scenes, totalScenes } 3️⃣ 장면 선택 (Step 2: Selecting) ↓ SceneSelector 컴포넌트 ├─> RadioCard.Root로 장면 카드 표시 ├─> 각 장면: 제목 + 내용 미리보기 └─> 사용자 선택 → selectedSceneId 저장 4️⃣ 이미지 생성 (Step 3: Generating) ↓ POST /api/generate-image ├─> body: { writingId, title (scene), content (scene), locale } ├─> Authorization: Bearer {idToken} │ ├─> 🔒 인증 & 권한 확인 │ ├─> verifyIdToken() │ └─> writing.userId === userId 체크 │ ├─> 🎨 프롬프트 최적화 │ └─> optimizePromptForImage(title, content, locale) │ ├─> Gemini Flash로 키워드 추출 (6-12개) │ │ - 주요 피사체, 행동, 배경, 시각적 디테일, 분위기 │ │ - "Korean elementary student" 국적 명시 │ │ - 일관된 스타일 키워드 자동 추가 │ │ │ └─> keywords.join(", ") │ 예: "Korean elementary student with bright smile, │ catching red dodgeball, │ sunny school playground in Korea, │ warm natural lighting, │ friendly digital illustration, │ anime-inspired art style" │ ├─> 🖼️ Imagen API 호출 │ └─> generateImage(optimizedPrompt, config) │ ├─> model: imagen-4.0-fast-generate-001 │ ├─> aspectRatio: 16:9 │ ├─> numberOfImages: 1 │ └─> negativePrompt (강화됨) │ ├─> 💾 Firebase Storage 업로드 │ └─> uploadGeneratedImage(writingId, dataUrl, 'png') │ ├─> 경로: generated-images/{writingId}/{timestamp}.png │ └─> 공개 URL 반환 │ └─> 📝 Firestore 업데이트 └─> writings/{writingId}.generatedImage = { url, prompt, generatedAt, modelName } 5️⃣ 결과 표시 (Step 4: Done) ↓ ├─> 생성된 이미지 미리보기 ├─> 사용된 프롬프트 표시 └─> "장면 변경" / "닫기" 버튼 ``` #### 프롬프트 최적화 시스템 **2단계 프로세스**: 1. **AI 키워드 추출** (Gemini Flash): ```typescript // promptOptimization.ts input: { sceneTitle: "막판 역전", sceneContent: "지훈이를 아웃시켰다...", locale: "ko" } output: { keywords: [ "Korean elementary student with confident expression", "catching red dodgeball mid-air", "elementary school playground during golden hour", "classmates running excitedly", "dynamic joyful pose", "warm natural lighting", "friendly digital illustration", "anime-inspired art style", "warm gentle colors", "clean composition" ] } ``` 2. **폴백 프롬프트** (AI 실패 시): ```typescript // imagenService.ts const prompt = [ "friendly digital illustration", "anime-inspired art style", "warm gentle colors", title, summary, "soft warm lighting", "cheerful atmosphere" ].join(", "); ``` #### 주요 특징 1. **일관된 스타일** 🎨: - 모든 이미지에 동일한 스타일 가이드 적용 - AI 프롬프트에 스타일 키워드 자동 포함 - Negative prompt로 원하지 않는 스타일 차단 2. **한국 문화권 고려** 🇰🇷: - "Korean elementary student" 명시 - "school playground in Korea" 등 한국 배경 - 아시아권 얼굴 특징 반영 3. **장면 자동 추출** ✂️: - Gemini가 글에서 시각화하기 좋은 장면 2-5개 추출 - 사용자가 원하는 장면 선택 - 다시 생성 가능 (장면 변경 버튼) 4. **비용 최적화** 💰: - 프롬프트 최적화로 토큰 절감 - Multi-region failover로 Rate Limit 회피 - 이미지당 평균 $0.04 5. **다국어 지원** 🌏: - 한국어/영어/일본어 프롬프트 생성 - locale에 따라 다른 스타일 가이드 #### 참고 파일 **프롬프트**: - `src/prompts/promptOptimization.ts` - AI 키워드 추출 프롬프트 (3개 언어) **서비스**: - `src/services/vertexAI.ts` - Imagen API 래퍼 (multi-region failover) - `src/services/imagenService.ts` - 이미지 생성 비즈니스 로직 - `src/services/sceneExtractionService.ts` - 장면 추출 로직 **API**: - `src/app/api/extract-scenes/route.ts` - 장면 추출 API - `src/app/api/generate-image/route.ts` - 이미지 생성 API **컴포넌트**: - `src/components/writing/GenerateImageDialog.tsx` - 4단계 플로우 다이얼로그 - `src/components/writing/SceneSelector.tsx` - 장면 선택 UI **유틸**: - `src/utils/imageStorage.ts` - Firebase Storage 업로드 --- ### 11. AI 글쓰기 도우미 시스템 #### 개요 학생이 글쓰기 중 막혔을 때 AI가 **주제 맥락을 고려한** 4단계 점진적 힌트를 제공하는 시스템 #### 아키텍처 플로우 ``` 1️⃣ 선생님 (팀 관리) ↓ 팀 관리 페이지 (/team/[teamId]/manage) ├─> Switch.Root (AI 도움 On/Off) ├─> handleAIToggle(checked) └─> teamManager.updateAIConfig(teamId, config) ↓ PUT API: /api/team/[teamId]/ai-config ↓ Firestore teams/{teamId}.aiAssistanceConfig = { enabled: true, detectionTimeMinutes: 5, maxHintsPerWriting: 5, cooldownMinutes: 3, allowedHintLevels: [1,2,3,4], requireSelfEdit: true } 2️⃣ 학생 (글쓰기) ↓ 글쓰기 페이지 (/write) ├─> 팀 주제 선택 (topicInfo.ownerType === "team") ├─> teamManager.getAIConfig(topicInfo.ownerId) │ ↓ GET (5분 캐싱) │ API: /api/team/[teamId]/ai-config │ ↓ Firestore │ teams/{teamId}.aiAssistanceConfig 조회 └─> setAiAssistEnabled(config?.enabled) 3️⃣ 작성 멈춤 감지 ↓ useWritingInactivityDetection({ detectionTimeMinutes: 5, enabled: aiAssistEnabled && !!selectedTopic, onInactive: () => setShowInactivityPrompt(true) }) ├─> 에디터 입력 → resetTimer() ├─> 5분간 입력 없음 → onInactive() 실행 └─> (플로팅 버튼: "막히셨나요?") 4️⃣ AI 힌트 요청 ↓ handleRequestHint(level) ├─> 클라이언트 검증 │ ├─> 주제 선택 확인 │ ├─> 제한 확인 (aiHintsUsed < maxHints) │ └─> 쿨다운 확인 (lastHintTime + cooldown) │ ├─> POST /api/writing-assistance │ body: { │ level: 1~4, │ currentContent: string, │ topicInfo: Topic, // 🔑 주제 정보 전달 │ locale: "ko" | "en" | "ja" │ } │ └─> 서버 처리 (writing-assistance/route.ts) │ ├─> 🔒 팀 설정 검증 │ ├─> getTeamAIConfig(topicInfo.ownerId) │ ├─> config.enabled === false → 403 "AI_DISABLED" │ └─> level ∉ config.allowedHintLevels → 403 "LEVEL_NOT_ALLOWED" │ └─> ✅ 검증 통과 ↓ generateHint(params) ├─> buildHintPrompt(level, content, topicInfo, locale) │ ├─> 주제 정보 활용 │ │ - title: "나의 여름방학" │ │ - keywords: ["여름", "가족"] │ │ - category: "daily" │ │ - examplePrompts: [...] │ │ │ └─> 레벨별 프롬프트 생성 │ - Level 1: "여름방학에서 가장 기억에 남는 순간은?" │ - Level 2: "그 순간의 감정을 자세히 표현해보세요" │ - Level 3: ["A. 가족", "B. 친구", "C. 혼자"] │ - Level 4: "예: 나는 여름 해변에서..." │ ├─> Vertex AI (Gemini 2.5 Flash) │ temperature: 0.8 │ schema: SINGLE_HINT_SCHEMA | CHOICE_HINT_SCHEMA │ └─> 서버 캐싱 (1분 TTL, 50개) key: `${level}-${topicId}-${contentHash}` 5️⃣ 힌트 표시 ↓ handleRequestHint(level+1)} /> ├─> Level 1, 2, 4: 단일 텍스트 (Box) └─> Level 3: RadioCard.Root (선택지 3개) ``` #### 데이터 모델 ```typescript // Team (Firestore) interface Team { // ... 기존 필드 aiAssistanceConfig?: { enabled: boolean; detectionTimeMinutes: number; // 5분 maxHintsPerWriting: number; // 5회 cooldownMinutes: number; // 3분 allowedHintLevels: number[]; // [1,2,3,4] requireSelfEdit: boolean; // true }; } // Writing (선택적) interface Writing { // ... 기존 필드 aiAssistanceHistory?: AIAssistanceRecord[]; } interface AIAssistanceRecord { timestamp: Timestamp; hintLevel: 1 | 2 | 3 | 4; topicId?: string; topicTitle?: string; context: string; // 마지막 50단어 hintProvided: string; wasUsed: boolean; } ``` #### 주요 특징 1. **주제 맥락 활용** 🔑: - AI 프롬프트에 주제 정보 전달 (title, keywords, category) - 맥락에 맞는 힌트 생성 ("여름방학"이면 여름 관련 질문) 2. **4단계 점진적 힌트** 📈: - Level 1 (질문): "주인공은 어떤 기분일까요?" - Level 2 (방향): "감정 변화를 써보세요" - Level 3 (선택): ["A. 친구", "B. 가족", "C. 혼자"] - Level 4 (예시): "예: 나는 용기를 내어..." 3. **서버 검증** 🔒: - 팀 설정 확인 (Firestore) - enabled=false → 403 에러 - 허용되지 않은 레벨 → 403 에러 4. **사용 제한** ⏱️: - 글당 최대 5회 - 힌트 간 3분 쿨다운 - 클라이언트 + 서버 이중 검증 5. **다국어 지원** 🌏: - 한국어/영어/일본어 프롬프트 - locale에 따라 다른 AI 응답 6. **캐싱 전략** ⚡: - 팀 AI 설정: 5분 캐싱 (TeamManager) - AI 힌트: 서버 메모리 1분 (동일 컨텍스트 재요청 방지) #### 참고 파일 **타입**: `src/types/team.ts` (AIAssistanceConfig), `src/types/writing.ts` (AIAssistanceRecord) **훅**: `src/hooks/useWritingInactivityDetection.ts` **서비스**: `src/services/writingAssistanceService.ts` **프롬프트**: `src/prompts/writingAssistance.ts` (4단계 × 3개 언어) **UI**: `src/components/writing/{InactivityPrompt, HintDisplay, AIAssistancePanel}.tsx` **API**: `src/app/api/writing-assistance/route.ts`, `src/app/api/team/[teamId]/ai-config/route.ts` **서버**: `src/lib/server/team.ts` (getTeamAIConfig, updateTeamAIConfig) **Manager**: `src/managers/TeamManager.ts` (getAIConfig, updateAIConfig) --- ## 참고 문서 - [PROJECT_STRUCTURE.md](./PROJECT_STRUCTURE.md) - 프로젝트 구조 - [ROADMAP.md](./ROADMAP.md) - 개발 로드맵 - [CLAUDE.md](./CLAUDE.md) - Claude Code 가이드 --- © 2024 BlueNovaLab. All rights reserved.