2025-11-11 02:41:58 +00:00

27 KiB
Raw Blame History

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

최종 업데이트: 2025-11-11 (실시간 피드백 시스템, Multi-Region Failover)


기술 스택

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 컴포넌트 라이브러리
Emotion 11.14.0 CSS-in-JS
Framer Motion 12.23.24 애니메이션 라이브러리
React Icons 5.5.0 아이콘 세트
Tiptap latest 리치 텍스트 에디터

Backend & Database

기술 버전 용도
Firebase 12.4.0 BaaS (Backend as a Service)
Firebase Auth - 사용자 인증
Firestore - NoSQL 데이터베이스 (글 저장)
Vertex AI latest 🆕 AI 텍스트 분석 (Gemini 2.5 Flash)
Redis - Cache 데이터 베이스 (예정)

Utilities

기술 버전 용도
use-debounce latest 🆕 React debounce hook (5초 API 호출 제한)

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 규칙 적용

환경 변수

.env.local 파일 구조

# Firebase 설정 (필수)
NEXT_PUBLIC_FIREBASE_API_KEY=your_api_key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your_project_id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your_project.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_sender_id
NEXT_PUBLIC_FIREBASE_APP_ID=your_app_id

# 사이트 URL (프로덕션)
NEXT_PUBLIC_URL=https://raonnuri.com

# API Base URL (선택적, 기본값: /api)
NEXT_PUBLIC_API_URL=/api

환경 변수 사용 예시

// src/config/firebase.ts
const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  // ...
};

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, 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. 상태 관리 원칙

  • 전역 상태: Zustand 사용 (인증, 사용자 진행 상황, 알림)
  • 로컬 상태: useState 사용 (폼 입력, UI 토글, 에디터 내용)
  • 로컬 저장소: LocalStorage (임시 저장 글)
  • 서버 상태: Firestore 직접 호출 (React Query는 나중에 고려)

5. 태그 입력 필드 패턴 (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 아키텍처)

핵심 개념

목적: Vertex AI로 초등학생 글쓰기를 실시간 평가하면서 비용 최적화

문제:

  • 매번 전체 텍스트 전송 = 비용 폭증
  • 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    │
│                                         │
│ 역할:                                   │
│ - 프롬프트 생성 (평가 기준 정의)         │
│ - JSON 파싱 (AI 응답 처리)              │
│ - 점수 제한 (최대 10점)                 │
│ - vertexAI 호출                         │
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│ Layer 3: Infrastructure                │
│ src/services/vertexAI.ts                │
│                                         │
│ 역할:                                   │
│ - Vertex AI 클라이언트 관리              │
│ - Multi-region failover                 │
│ - Retry with exponential backoff        │
│ - regionHealthManager 연동              │
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│ 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)

참고 파일

서비스 레이어:

  • src/services/vertexAI.ts - Vertex AI 범용 래퍼
  • src/services/textAnalysisService.ts - 텍스트 분석 비즈니스 로직
  • src/services/regionHealthManager.ts - Region 상태 관리

API & 컴포넌트:

  • src/app/api/analyze-text/route.ts - 텍스트 분석 API
  • src/components/writing/ScoreDisplay.tsx - 점수 표시 UI
  • src/app/write/page.tsx - Delta 추적 + 통합

유틸리티:

  • src/utils/koreanWordList.ts - 감각 동사/형용사 목록

문서:

  • TECHNICAL_IMPLEMENTATION.md - 상세 기술 구현 가이드
  • SERVICE_DIRECTION.md - 서비스 방향성 논의 (8차)

참고 문서


© 2024 BlueNovaLab. All rights reserved.