47 KiB
라온누리 - 기술 스택 및 개발 환경
최종 업데이트: 2025-11-13 (다국어 지원 시스템)
기술 스택
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 컴파일러 최적화 |
개발 명령어
주요 명령어
# 개발 서버 시작 (포트 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)
// React Compiler 활성화
const nextConfig = {
reactCompiler: true,
// ... 기타 설정
};
TypeScript 설정
- Path Alias:
@/*→./src/* - 모든 import는
@/경로 사용
// 예시
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에 직접 하드코딩되어 있습니다:
// 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 클라이언트 앱 패턴)
환경 변수
# 사이트 URL (프로덕션)
NEXT_PUBLIC_URL=https://raonnuri.com
# 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/✅ - 작성한 글{ userId: string; title: string; content: string; // HTML wordCount: number; charCount: number; status: 'draft' | 'published'; topicId?: string | null; // 주제 ID (null은 자유 주제) createdAt: Timestamp; updatedAt: Timestamp; }topics/✅ - 글쓰기 주제 (팀 주제 + 개인 주제){ 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/✅ - 팀 (팀 코드 시스템){ 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 기반){ firebaseUid: string; // Anonymous Auth UID linkedUserId?: string; // 연결된 정식 계정 (선택적, 1:1) name: string; pinHash?: string; // SHA-256 해시 classroomIds: string[]; // 다중 팀 지원 isAnonymous: true; createdAt: Timestamp; lastLoginAt: Timestamp; }users/🔜 - 사용자 프로필 및 진행 상황 (정식 계정){ 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, shadowselect: 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 기능:
// src/managers/ManagerBase.ts
abstract class BaseManager {
// 인증
protected async getIdToken(): Promise<string | null>
protected getCurrentUserId(): string | null
protected isAuthenticated(): boolean
// API 호출
protected async authenticatedFetch<T>(endpoint, options)
protected async ApiCall<Req, Res>(method, endpoint, data)
}
abstract class SingletonManager extends BaseManager {
// 캐싱
protected getCached<T>(key, ttl?): T | null
protected setCached<T>(key, data): void
protected invalidateCache(key): void
protected invalidateCachePattern(pattern): void
protected clearCache(): void
// API + 캐싱 통합
protected async callApiWithCache<Req, Res>(cacheKey, method, endpoint, data, ttl)
}
사용 예시:
// 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 명세서
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 구조:
{
"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):
{
"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 접근 가능)
- 통계 쓰기: 본인만
- 미리보기: 요청자와 대상자만
작성 속도 계산 로직 (클라이언트 측):
// 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 패턴입니다.
구현 방식:
// 상태 구조
type TemplatePart = {
id: string;
type: "text" | "placeholder";
value: string; // "{date}" 또는 일반 텍스트
label?: string; // "날짜" (placeholder인 경우)
};
const [templateParts, setTemplateParts] = useState<TemplatePart[]>([]);
const [currentInput, setCurrentInput] = useState("");
const [selectedPartIndex, setSelectedPartIndex] = useState<number | null>(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자 전체 전송 = 토큰 낭비
해결: 변경분만 전송
// 클라이언트
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 타입 확장:
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- 텍스트 분석 APIsrc/app/api/spelling/check/route.ts- 맞춤법 검사 API- ❌ 삭제됨 (하이라이트로 대체)src/components/writing/ScoreDisplay.tsx- ❌ 삭제됨 (하이라이트로 대체)src/components/writing/SpellingErrorDisplay.tsxsrc/app/write/page.tsx- Delta 추적 + 통합 + Toast 알림
타입 정의:
src/types/draft.ts- Draft, AnalysisHistoryItem
유틸리티:
src/utils/koreanWordList.ts- 감각 동사/형용사 목록
문서:
TECHNICAL_IMPLEMENTATION.md- 상세 기술 구현 가이드SERVICE_DIRECTION.md- 서비스 방향성 논의 (8차)
8. 글 작성 패턴 분석 시스템
핵심 개념
목적: 사용자의 여러 글을 분석하여 작성 패턴, 강점, 약점을 파악하고 맞춤형 피드백 제공
분석 항목:
- 작성 스타일: 평균 글자/단어 수, 선호하는 글 길이, 문장 구조 (단문/복문/혼합)
- 표현력 분석: 평균 점수, 강점/약점, 카테고리별 점수, 자주 쓰는 표현
- 맞춤법 경향: 자주 하는 실수, 개선율
- 발전 추이: 최근 5개 vs 이전 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- 패턴 분석 APIsrc/components/writing/WritingPatternDialog.tsx- 다이얼로그src/components/writing/WritingPatternDisplay.tsx- 표시 컴포넌트src/app/home/page.tsx- "작성 패턴 분석" 카드 추가
9. 실시간 하이라이트 시스템 (Tiptap Extensions)
핵심 개념
목적: 에디터에서 맞춤법 오류와 감각 단어를 실시간으로 시각적으로 표시
하이라이트 종류:
- 맞춤법 오류: 빨간 물결 밑줄 (
spelling-error클래스) - 감각 동사: 초록색 하이라이트 (
sensory-word클래스) - 감각 형용사: 파란색 하이라이트 (
emotion-word클래스) - 의성어/의태어: 보라색 하이라이트 (
onomatopoeia-word클래스)
Tiptap Extensions:
SpellingHighlight- 맞춤법 오류 하이라이트- data-original, data-correction, data-reason 속성
- Meta를 통한 강제 업데이트
SensoryWordHighlight- 감각 단어 하이라이트- data-word, data-type 속성
- 색상별 구분 (초록/파랑/보라)
DecorationSet 기반:
- ProseMirror Decoration을 사용한 효율적인 하이라이트
- 문서 변경 시 자동 업데이트
- 하이라이트 위치를 정확하게 추적
WritingEditor 통합:
spellingErrors,foundWordsprops 추가- Extension 옵션 실시간 업데이트
- 브라우저 기본 맞춤법 검사 비활성화 (
spellcheck="false")
참고 파일:
src/extensions/spelling-highlight.ts- 맞춤법 하이라이트 Extensionsrc/extensions/sensory-word-highlight.ts- 감각 단어 하이라이트 Extensionsrc/components/writing/WritingEditor.tsx- Extensions 통합
10. 인터랙티브 툴팁 시스템
핵심 개념
목적: 하이라이트된 단어를 클릭하면 상세 정보를 툴팁으로 표시
툴팁 종류:
- 맞춤법 오류 툴팁:
- 원본 → 수정 (취소선 → 굵은 글씨)
- 이유 설명 (초등학생 눈높이)
- 빨간색 테두리
- 감각 단어 툴팁:
- 단어 표시
- 단어 타입 (감각 동사/형용사/의성어)
- 격려 메시지 ("이렇게 구체적으로 표현하면 글이 더 생생해져요!")
- 색상별 테두리 (초록/파랑/보라)
기술 구현:
- 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 알림 시스템
핵심 개념
목적: 텍스트 분석 및 맞춤법 검사 진행 상태를 사용자에게 알림
알림 종류:
- 텍스트 분석:
- 시작: "글을 분석하고 있어요..." (loading, duration: Infinity)
- 완료: "분석 완료! 점수: X.X점" (success, 3초)
- 실패: "분석 실패, 다시 시도해주세요." (error, 3초)
- 맞춤법 검사:
- 시작: "맞춤법을 검사하고 있어요..." (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)
아키텍처
브라우저 요청 (/)
↓
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
// 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 - 라우팅 설정:
export const routing = defineRouting({
locales: ['ko', 'en'],
defaultLocale: 'ko',
localePrefix: 'always', // URL에 항상 표시 (/ko/*, /en/*)
localeDetection: true // 브라우저 언어 자동 감지
});
// next-intl 타입 안전 내비게이션 API
export const {Link, redirect, usePathname, useRouter} = createNavigation(routing);
i18n/request.ts - 번역 메시지 로더:
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 - 자동 언어 감지:
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: ['/', '/(ko|en)/:path*']
};
컴포넌트 사용 패턴
Server Component (기본):
import {useTranslations} from 'next-intl';
export default function Page() {
const t = useTranslations('namespace');
return <h1>{t('key')}</h1>;
}
Client Component:
"use client";
import {useTranslations} from 'next-intl';
export default function ClientComponent() {
const t = useTranslations('namespace');
return <p>{t('key')}</p>;
}
파라미터가 있는 번역:
const t = useTranslations('home');
// messages/ko.json: "welcome": "환영합니다, {name}님!"
<h1>{t('hero.welcome', {name: userName})}</h1>
// → "환영합니다, 홍길동님!"
타입 안전 Link (locale 자동 처리):
import {Link} from '@/i18n/routing';
<Link href="/home">홈으로</Link>
// 현재 locale이 ko면 → /ko/home
// 현재 locale이 en이면 → /en/home
언어 전환 버튼
LocaleSwitcher (src/components/navigation/LocaleSwitcher.tsx):
const locale = useLocale(); // 현재 언어
const router = useRouter();
const pathname = usePathname();
const toggleLocale = () => {
const nextLocale = locale === 'ko' ? 'en' : 'ko';
router.replace(pathname, {locale: nextLocale});
};
return (
<Button onClick={toggleLocale}>
<LuGlobe /> {locale.toUpperCase()}
</Button>
);
동작:
/ko/home에서 버튼 클릭 →/en/home으로 이동NEXT_LOCALE쿠키에 저장 (다음 방문 시 기억)
브라우저 언어 자동 감지
첫 방문 시나리오:
1. 사용자가 / 접속 (쿠키 없음)
2. Middleware가 Accept-Language 헤더 확인
- "en-US,en;q=0.9" → /en/으로 리다이렉트
- "ko-KR,ko;q=0.9" → /ko/로 리다이렉트
- 지원하지 않는 언어 → /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+ 항목) |
| Write | /[locale]/write |
🔜 예정 | - |
| Team | /[locale]/team/* |
🔜 예정 | - |
next.config.ts 설정
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 헤더
참고 문서
- PROJECT_STRUCTURE.md - 프로젝트 구조
- ROADMAP.md - 개발 로드맵
- CLAUDE.md - Claude Code 가이드
© 2024 BlueNovaLab. All rights reserved.