2025-11-23 09:28:41 +00:00

65 KiB
Raw Blame History

라온누리 - 기술 스택 및 개발 환경

최종 업데이트: 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 컴파일러 최적화

개발 명령어

주요 명령어

# 개발 서버 시작 (포트 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.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/ - 작성한 글
    {
      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/ - 글쓰기 주제 (팀 주제 + 개인 주제)
    {
      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;
      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, 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 기능:

// 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);

참고 문서:

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 패턴입니다.

통합 Dialog 설계:

  • TopicFormData export: 입력 데이터만 수집하여 반환
  • onSubmit 콜백 패턴: 부모 컴포넌트가 팀/개인 주제 생성 결정
  • 관심사 분리: Dialog는 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/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 - 텍스트 분석 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

// 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', '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 - 번역 메시지 로더:

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 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 (
  <Menu.Root positioning={{placement: "bottom-end"}}>
    <Menu.Trigger asChild>
      <Button>
        <LuGlobe /> {currentLocale.flag} {currentLocale.code.toUpperCase()}
      </Button>
    </Menu.Trigger>
    <Menu.Content>
      {LOCALES.map((loc) => (
        <Menu.Item onClick={() => handleLocaleChange(loc.code)}>
          {loc.flag} {loc.name}
          {locale === loc.code && <LuCheck />}
        </Menu.Item>
      ))}
    </Menu.Content>
  </Menu.Root>
);

동작:

  • 드롭다운 메뉴에서 언어 선택
  • 국기 이모지로 시각적 구분
  • 현재 언어에 체크 마크 표시
  • 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 설정

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 기반 분석 결과 저장 + 변경 감지

데이터 구조

// 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가 실제 데이터로 채워짐:

// 패턴 분석 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)
   ↓
<HintDisplay>
   ├─> 생성된 이미지 미리보기
   ├─> 사용된 프롬프트 표시
   └─> "장면 변경" / "닫기" 버튼

프롬프트 최적화 시스템

2단계 프로세스:

  1. AI 키워드 추출 (Gemini Flash):

    // 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 실패 시):

    // 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() 실행
   └─> <InactivityPrompt isVisible={true} />
       (플로팅 버튼: "막히셨나요?")

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⃣ 힌트 표시
   ↓
<HintDisplay
  level={1~4}
  topicTitle={selectedTopic.title}
  content={hint.content}
  encouragement={hint.encouragement}
  onNext={() => handleRequestHint(level+1)}
/>
   ├─> Level 1, 2, 4: 단일 텍스트 (Box)
   └─> Level 3: RadioCard.Root (선택지 3개)

데이터 모델

// 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)


참고 문서


© 2024 BlueNovaLab. All rights reserved.