Character Runtime MVP
상세 개발 계획

12개 모듈, 100+ 태스크, 평가셋, 스키마, 의존관계를 정리한 실행 계획

1개발 원칙

1.1 설계 원칙

원칙설명
Package-firstCharacter Package 스펙을 먼저 확정하고, 모든 모듈이 이 스펙에 맞춰 구현
Schema-drivenTurnPlan, ActionPlan, Memory 등 핵심 데이터는 JSON Schema로 선정의
LLM은 제안자LLM은 TurnPlan을 제안할 뿐, 직접 DB 쓰기·파라미터 접근 불가
Adapter 분리LLM, TTS, Vector DB, MCP 프로토콜은 모두 Adapter 패턴으로 교체 가능
의미 기반 제어Live2D 파라미터를 직접 다루지 않고 semantic control을 통해 간접 제어
테넌트 격리 필수character_instance_id 기반 격리, 사용자 간 기억 누출 0건이 통과 조건
Idempotency모든 쓰기 API에 idempotency_key 필수

1.2 기술 결정 가이드

결정 항목권장안대안결정 기준
모노레포 도구TurborepoNx설정 단순성, TS 생태계
API 프레임워크FastifyNestJSMVP 속도 vs 구조화
ORM/쿼리Drizzle ORMPrisma, Kysely타입 안전성 + SQL 제어력
벡터 검색pgvectorQdrant, Pinecone인프라 단순화 (PG 하나로)
작업 큐BullMQ (Redis)pg-bossRedis를 캐시·큐 양쪽 활용
실시간ws (raw WebSocket)Socket.io프로토콜 제어력
프론트엔드React + ViteNext.jsPlayer는 SPA
인증Lucia Auth + OAuthClerk, Auth.js자체 구현 제어력
오브젝트 저장Cloudflare R2S3기존 CF 계정, egress 무료
컨테이너 배포Fly.io / RailwayAWS ECSMVP 비용 최소화

2모노레포 구조

character-platform/
├─ apps/
│  ├─ api/                    # Fastify — Character Runtime API
│  ├─ web-player/             # React+Vite — Live2D Player
│  ├─ mcp-server/             # MCP TS SDK — Gateway
│  ├─ admin-console/          # 관리자 콘솔
│  └─ worker/                 # BullMQ 비동기 Worker
├─ packages/
│  ├─ character-schema/       # JSON Schema + TS 타입
│  ├─ character-package/      # Package 로더/검증
│  ├─ llm-adapter/            # LLM Provider Adapter
│  ├─ tts-adapter/            # TTS Provider Adapter
│  ├─ memory-engine/          # 기억 검색/저장/갱신
│  ├─ action-engine/          # ActionPlan → Live2D 이벤트
│  ├─ db/                     # Drizzle 스키마 + 마이그레이션
│  └─ observability/          # 로깅/메트릭
├─ characters/
│  └─ luna-v1/                # MVP 캐릭터 패키지
├─ tests/
│  ├─ fixtures/               # 평가셋 (기억 30개, 동작 20개)
│  ├─ e2e/
│  ├─ contract/               # MCP 계약 테스트
│  └─ security/
└─ infra/

초기 세팅 태스크

#태스크산출물완료 기준
S-1모노레포 초기화 (Turborepo + pnpm)turbo.json, pnpm-workspace.yamlturbo build 성공
S-2TypeScript 공통 설정tsconfig.base.jsonstrict 모드, path alias
S-3ESLint + Prettier설정 파일lint/format 동작
S-4Docker Compose (PG16+pgvector, Redis 7)docker-compose.ymlDB 접속 확인
S-5환경변수 관리.env.exampleenv 검증
S-6CI 파이프라인GitHub Actionspush 시 자동 실행

3Phase 0 — 기술 검증 스파이크

전체 MVP를 만들기 전에 가장 위험한 8개 연결을 최소한으로 검증한다. 완성도 있는 코드가 아니라 연결이 가능한지만 확인하는 throwaway prototype이다.

#스파이크검증 대상Go 기준
SP-1Live2D Web SDK 모델 로딩브라우저에서 .moc3 렌더링모델 표시 + 표정 3종 + 모션 3종
SP-2LLM Structured Output → TurnPlanClaude/GPT → JSON TurnPlan10회 중 9회+ 유효 Schema 통과
SP-3ActionPlan ValidatorSP-2 출력을 capability manifest 대비 검증범위 초과·미지원 모두 차단
SP-4TTS → 볼륨 립싱크Web Audio API 분석, 입 파라미터 반영발화 중 입 움직임, 묵음 시 닫힘
SP-5WebSocket 이벤트 전달서버 → Player 이벤트 수신순서 보존 확인
SP-6pgvector 기억 저장/검색메모리 3개 저장, 유사도 검색다른 표현으로 재호출 성공
SP-7MCP character.turnMCP Inspector에서 Tool 호출호출 성공, Schema 일치
SP-8사용자 격리A/B 교차 검색A의 기억이 B에게 0건 노출

스파이크 통합 시나리오

  1. 브라우저에서 Live2D 모델 로딩 (SP-1)
  2. 텍스트 입력 → API → LLM → TurnPlan (SP-2)
  3. Validator가 TurnPlan 검증 (SP-3)
  4. WebSocket으로 Player에 이벤트 전달 (SP-5)
  5. Player가 Expression + Motion 실행 (SP-1)
  6. TTS 오디오 재생 + 립싱크 (SP-4)
  7. 대화 내용에서 기억 저장 (SP-6)
  8. 새 세션에서 기억 재호출 (SP-6)
  9. MCP Inspector에서 같은 호출 (SP-7)
  10. 다른 사용자로 같은 질문 → 기억 격리 (SP-8)

Go 판정: 10단계 중 10단계 모두 성공해야 Phase 1 진행.

스파이크 산출물 (Phase 1으로 이관)

4Phase 1 — MVP 본개발

모듈 개요

모듈앱/패키지핵심 의존
M1. 기반 인프라packages/db없음
M2. Package & Schemapackages/character-schema, character-package없음
M3. Live2D Playerapps/web-playerM2
M4. Runtime APIapps/apiM1, M2
M5. Orchestratorapps/api/servicesM2, M4, M6, M7
M6. Memory Enginepackages/memory-engineM1
M7. Action Enginepackages/action-engineM2
M8. TTS & Lip-syncpackages/tts-adapterM4
M9. Realtime Gatewayapps/api WSM4, M3
M10. MCP Serverapps/mcp-serverM4, M5, M6
M11. Admin Consoleapps/admin-consoleM4, M6
M12. Observabilitypackages/observabilityM4

M1. 기반 인프라

#태스크산출물완료 기준
M1-1Drizzle ORM + PG 연결packages/db/src/client.ts연결 + 쿼리 동작
M1-2pgvector 확장마이그레이션 SQLCREATE EXTENSION vector 성공
M1-3스키마 — 사용자users, sessionsup/down 동작
M1-4스키마 — 캐릭터character_templates, instances 등 4개 테이블FK 관계 정상
M1-5스키마 — 대화conversations, messages, turns, turn_context_snapshots인덱스 포함
M1-6스키마 — 상태character_state, relationship_state, state_eventsinstance_id 인덱스
M1-7스키마 — 기억memories, memory_embeddings (vector column)벡터 인덱스 (HNSW)
M1-8스키마 — 애니메이션expressions, motion_assets, action_events
M1-9스키마 — 감사audit_logs, mcp_tokens
M1-10Redis + BullMQ 큐Worker 큐 초기화enqueue/dequeue 동작
M1-11R2 오브젝트 저장S3 호환 클라이언트업로드/서명 URL 생성
M1-12인증 (Lucia + Google OAuth)로그인/세션OAuth 콜백, 쿠키 발급
M1-13Idempotency 미들웨어middleware/idempotency.ts중복 키 → 같은 응답

M2. Character Package & Schema

#태스크완료 기준
M2-1TurnPlan JSON Schema 확정ajv로 유효/무효 각 10개 검증
M2-2ActionPlan JSON Schema 확정유효/무효 검증
M2-3CapabilityManifest Schema 확정
M2-4Memory Schema 확정
M2-5CharacterContextBundle Schema 확정
M2-6Character Event Schema 확정WS 이벤트 타입 정의
M2-7TypeScript 타입 자동 생성스키마↔타입 동기화
M2-8Character Package Loadermanifest.yaml 읽고 해석
M2-9Character Package Validator필수 필드·참조 파일 검출
M2-10MVP 캐릭터 (Luna) 패키지 작성Validator 통과

M3. Live2D Web Player

#태스크완료 기준
M3-1Vite + React 프로젝트dev server 기동
M3-2Live2D Cubism SDK 통합Canvas 렌더링
M3-3Model Loadermodel3.json 경로로 로딩
M3-4Expression ControllersetExpression('happy') API
M3-5Motion ControllerplayMotion('greet') API
M3-6Parameter Overlay Controllersemantic → live2d parameter 오버레이
M3-7Capability Manifest 연동manifest에 없는 제어 → 무시
M3-8Idle / Eye Blink / Breathing입력 없이도 자연스러운 상태
M3-9Event Queue (우선순위)높은 우선순위 선점
M3-10채팅 UI입력, 스트리밍 표시
M3-11연결 끊김 → 안전한 Idle끊겨도 모델 정상

M4. Character Runtime API

#태스크완료 기준
M4-1Fastify 서버 + 플러그인health check 동작
M4-2인증 미들웨어비인증 → 401
M4-3POST /v1/characters/{id}/turnsOrchestrator 호출 → 응답
M4-4GET /v1/characters/{id}/state상태 조회
M4-5GET /v1/characters/{id}/memories기억 목록 (페이징)
M4-6POST /v1/characters/{id}/memories수동 기억 추가
M4-7DELETE /v1/characters/{id}/memories/{mid}soft delete
M4-8GET /v1/characters/{id}/capabilitiesManifest 반환
M4-9POST /v1/characters/{id}/playersPlayer Session 생성
M4-10Idempotency 적용POST/DELETE에 적용
M4-11에러 핸들링 (RFC 7807)표준 에러 응답
M4-12인스턴스 소유권 검사타 사용자 → 403

M5. 대화 Orchestrator

#태스크완료 기준
M5-1LLM Adapter 인터페이스generateTurnPlan(bundle): TurnPlan
M5-2Claude AdapterStructured Output → TurnPlan
M5-3OpenAI Adapter (대안)function calling → TurnPlan
M5-4Prompt BuilderContextBundle → System/User 메시지
M5-5Context BuilderIdentity+Persona+Memories+Capabilities 조립
M5-6Turn ValidatorSchema + Capability 범위 검증
M5-7State Enginestate_updates 적용, revision++
M5-8Orchestrator 메인 로직전체 턴 파이프라인
M5-9텍스트 스트리밍 (SSE/WS)첫 토큰 빠르게 전달
M5-10오류 재시도 정책LLM 실패 → 1회 재시도, TurnPlan 무효 → fallback
M5-11Context Budget토큰 초과 시 기억/lore 축소

M6. Memory Engine

#태스크완료 기준
M6-1임베딩 생성 AdapterOpenAI embedding API 호출
M6-2Memory Retrieval (복합 점수)semantic×0.45 + recency×0.15 + importance×0.20 + relationship×0.10 + pin×0.10
M6-3Memory Extraction (LLM 기반)대화에서 사실 추출
M6-4Memory Proposal → 저장중복 감지, confidence 부여
M6-5기억 정정 (Supersede)모순 시 기존 superseded + 새 active
M6-6기억 삭제soft delete, 검색 제외
M6-7Session Summary 생성N턴마다 요약
M6-8기억 격리 테스트다른 instance_id 기억 반환 0건

기억 검색 SQL

WITH scored AS (
  SELECT m.*, me.embedding,
    1 - (me.embedding <=> $1::vector) AS semantic_sim,
    1.0 / (1.0 + EXTRACT(EPOCH FROM (NOW() - m.created_at)) / 86400) AS recency,
    CASE WHEN m.type = 'pinned' THEN 1.0 ELSE 0.0 END AS pin_score
  FROM memories m
  JOIN memory_embeddings me ON me.memory_id = m.id
  WHERE m.character_instance_id = $2 AND m.status = 'active'
)
SELECT *,
  (semantic_sim * 0.45 + recency * 0.15 + importance * 0.20
   + confidence * 0.10 + pin_score * 0.10) AS total_score
FROM scored ORDER BY total_score DESC LIMIT $3;

M7. Action Engine & Validator

#태스크완료 기준
M7-1ActionPlan Validatorexpression/motion manifest 존재 확인, overlay 범위 검사
M7-2Semantic → Live2D Mapperhead_yawParamAngleX
M7-3Priority Resolver충돌 파라미터 우선순위 결정
M7-4Velocity Limitermax_velocity 초과 시 클램핑
M7-5ActionPlan → Player Events 변환WebSocket 이벤트 배열 생성

M8. TTS & Lip-sync

#태스크완료 기준
M8-1TTS Adapter 인터페이스synthesize(text, config): AudioResult
M8-2ElevenLabs Adapter텍스트 → mp3/wav
M8-3Google TTS Adapter (대안)
M8-4TTS Worker (BullMQ)큐 → TTS → R2 → 서명 URL → WS audio.ready
M8-5Web Audio 볼륨 분석기AudioContext + AnalyserNode → 0~1
M8-6Lip-sync Controller볼륨 → ParamMouthOpenY
M8-7오디오 + 립싱크 동기화재생 시작·종료 연동

M9. Realtime Event Gateway

#태스크완료 기준
M9-1WebSocket 서버연결/인증/해제
M9-2Player Session 인증무효 세션 거부
M9-3이벤트 전송 인터페이스send(sessionId, events)
M9-4이벤트 타입별 처리모든 이벤트 타입 수신 확인
M9-5재연결 + 놓친 이벤트 버퍼재연결 후 마지막 턴 수신
M9-6Heartbeat / Ping-pong좀비 연결 정리

이벤트 타입

turn.started    │ text.delta       │ emotion.changed
expression.play │ motion.play      │ parameter.overlay
audio.ready     │ audio.sync       │ state.updated
turn.completed  │ turn.failed

M10. MCP Server

#태스크완료 기준
M10-1MCP TS SDK 통합server/discover 응답
M10-2Streamable HTTP TransportMCP Inspector 연결
M10-3OAuth 2.1 + PKCE비인증 거부
M10-4Tool: character.turn입출력 Schema 통과
M10-5Tool: character.get_state
M10-6Tool: character.get_context
M10-7Tool: character.search_memory
M10-8Tool: character.remember
M10-9Tool: character.forget
M10-10Tool: character.play_motionPlayer 이벤트 발생
M10-11Tool: character.set_expression
M10-12Tool: character.list_capabilities
M10-13~15Resources (profile, state, memory/index)
M10-16최신/레거시 호환 Adapter두 버전 Inspector 통과
M10-17Scope 기반 권한Scope 부족 시 거부
M10-18MCP Inspector 계약 테스트CI 자동 실행

M11. Admin Console

#태스크완료 기준
M11-1프로젝트 세팅dev server
M11-2캐릭터 설정 조회/편집Persona 읽기/수정
M11-3기억 관리목록/검색/삭제
M11-4상태 모니터감정, 관계, revision
M11-5테스트 채팅관리자용 대화
M11-6모션 매핑 편집semantic ↔ parameter 수정
M11-7비용 대시보드LLM/TTS/턴 집계

M12. Observability

#태스크완료 기준
M12-1구조화 로거 (pino)JSON 로그, request_id
M12-2턴별 비용 기록usage_ledger 테이블 기록
M12-3응답 지연 메트릭p50/p95/p99
M12-4기억 적중률 메트릭로그 기반 추정
M12-5ActionPlan 유효율통과/실패 비율

5모듈 의존관계 & 권장 구현 순서

M2 (Schema/Package) ───────────────────────────────────┐
    │                                                    │
    ├── M3 (Live2D Player) ◄── M9 (Realtime Gateway)    │
    │         │                      │                   │
    │         └──────── M8 (TTS/Lipsync) ◄──┐           │
    │                                        │           │
M1 (인프라/DB) ──┬── M4 (API) ──── M5 (Orchestrator)    │
                 │       │              │   │   │        │
                 │       │              │   │   └─ M7 (Action Engine) ◄─ M2
                 │       │              │   │
                 │       │              │   └── M6 (Memory Engine) ◄── M1
                 │       │              │
                 │       │              └── M8 (TTS Worker)
                 │       │
                 │       ├── M10 (MCP Server)
                 │       └── M11 (Admin Console)
                 │
                 └── M12 (Observability)

권장 구현 순서

Layer모듈비고
0 (병렬)M1 + M2스키마와 인프라가 모든 것의 기반
1 (병렬)M3 + M6 + M7 + M12서로 독립 개발 가능
2M4Layer 1 패키지를 조합
3 (병렬)M5 + M8 + M9모든 엔진 결합, TTS와 WS
4 (병렬)M10 + M11API 완성 후

6DB 상세 설계

RLS (Row Level Security) — 기억 누출 방지

ALTER TABLE memories ENABLE ROW LEVEL SECURITY;

CREATE POLICY memories_instance_isolation ON memories
  USING (character_instance_id = current_setting('app.current_instance_id')::text);

-- 애플리케이션에서 매 요청마다:
-- SET LOCAL app.current_instance_id = 'ci_abc123';

인덱스 전략

-- 기억 검색
CREATE INDEX idx_memories_active ON memories (character_instance_id, status)
  WHERE status = 'active';

-- 벡터 검색 (HNSW)
CREATE INDEX idx_memory_embeddings_hnsw ON memory_embeddings
  USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64);

-- 최근 대화
CREATE INDEX idx_messages_conv ON messages (conversation_id, created_at DESC);

-- 상태 이벤트
CREATE INDEX idx_state_events ON state_events (character_instance_id, created_at DESC);

-- 감사 로그
CREATE INDEX idx_audit ON audit_logs (character_instance_id, created_at DESC);

7API 계약 상세

POST /v1/characters/{instanceId}/turns

Request

{
  "conversation_id": "conv_...",
  "message": "오늘 기분 어때?",
  "player_session_id": "player_...",
  "locale": "ko-KR",
  "response_mode": "text_audio_action",
  "idempotency_key": "idem_..."
}

Response (200)

{
  "turn_id": "turn_...",
  "text": "오늘은 네가 와서 꽤 좋아졌어.",
  "emotion": { "primary": "joy", "intensity": 0.7 },
  "action_plan": {
    "expression": { "id": "happy", "weight": 0.85, "fade_ms": 250 },
    "base_motion": { "id": "greet", "priority": 60 },
    "overlays": []
  },
  "state_revision": 143,
  "audio_pending": true
}

response_mode 옵션: text_only | text_action | text_audio_action

8MCP Tool/Resource 계약

character.turn

{
  "name": "character.turn",
  "description": "캐릭터에게 대화 메시지를 보내고 응답을 받는다.",
  "inputSchema": {
    "type": "object",
    "required": ["character_instance_id", "message"],
    "properties": {
      "character_instance_id": { "type": "string" },
      "conversation_id": { "type": "string" },
      "message": { "type": "string", "maxLength": 4000 },
      "player_session_id": { "type": "string" },
      "locale": { "type": "string", "default": "ko-KR" },
      "response_mode": { "enum": ["text_only","text_action","text_audio_action"] }
    }
  }
}

character.search_memory

{
  "name": "character.search_memory",
  "description": "캐릭터 인스턴스의 기억을 검색한다.",
  "inputSchema": {
    "type": "object",
    "required": ["character_instance_id", "query"],
    "properties": {
      "character_instance_id": { "type": "string" },
      "query": { "type": "string" },
      "limit": { "type": "integer", "default": 5, "maximum": 20 },
      "type_filter": { "enum": ["all","user_preference","episodic","semantic"] }
    }
  }
}

MCP Resources

character://instances/{id}/profile
character://instances/{id}/state
character://instances/{id}/relationship
character://instances/{id}/capabilities
character://instances/{id}/memory/index
character://instances/{id}/memory/{memory_id}
character://instances/{id}/motion-library
character://templates/{template_id}/lore/{page}

Auth Scopes

character:profile:read    character:state:read
character:chat            character:memory:read
character:memory:write    character:animation:control
character:instance:export character:admin

9Character Package v0.1

MVP Expressions (8종)

ID용도
neutral기본
happy기쁨/만족
sad슬픔
angry화남
surprised놀람
shy수줍음
thinking생각 중
tired피곤

MVP Motions (10종)

ID용도우선순위
idle_01기본 대기10
idle_02대기 변형10
greet인사60
nod끄덕임60
shake_head고개 젓기60
wave손 흔들기70
laugh웃음60
think생각 포즈50
look_away시선 회피60
celebrate기뻐하기70

Semantic Controls

ControlLive2D ParamMinMaxMax Vel
head_yawParamAngleX-303060/s
head_pitchParamAngleY-202040/s
head_rollParamAngleZ-151530/s
gaze_xParamEyeBallX-113/s
gaze_yParamEyeBallY-113/s
body_yawParamBodyAngleX-101020/s
body_pitchParamBodyAngleY-5510/s

10테스트 계획

기억 평가셋 (30개)

#카테고리예시
1-5직접 사실 회상"내가 좋아하는 음료가 뭐야?"
6-10표현 변형 질문"내 취미 알아?" (게임 좋아한다고 저장 후)
11-15오래된 기억20턴 전 내용 회상
16-18정정된 기억"서울" → "부산으로 이사" 후 질문
19-21삭제된 기억삭제 정보를 확정 사실로 안 써야
22-24교차 사용자A 기억이 B에게 0건 노출
25-27유사 정보 구분A "고양이" vs B "강아지"
28-30Lore vs 사용자 기억설정과 사용자 말 충돌 시

자연어 동작 평가셋 (20개, 발췌)

#입력기대 action_plan
1"웃어줘"expression: happy, motion: laugh
3"삐진 것처럼 고개 돌려"expression: angry, motion: look_away, head_yaw -16
11"좌측을 봐"gaze_x -0.8, head_yaw -10
19"천천히 오른쪽을 봐"head_yaw 15 duration 1000ms

테스트 디렉터리 구성

tests/
├─ unit/                 # 순수 로직
├─ integration/          # DB/Redis 포함
├─ contract/             # MCP 계약
├─ e2e/                  # 브라우저 E2E
├─ eval/                 # LLM 평가 (비결정적)
└─ security/             # 인젝션, 격리

11배포 전략

컴포넌트배포 대상
API ServerFly.io / Railway (컨테이너)
Worker같은 플랫폼, 별도 프로세스
MCP Server같은 플랫폼, 별도 포트
Web PlayerCloudflare Pages
Admin ConsoleCloudflare Pages
PostgreSQLNeon (Serverless, pgvector)
RedisUpstash (Serverless)
오브젝트 저장Cloudflare R2
CDNCloudflare

12Go/No-Go 체크리스트

Phase 0 → Phase 1

MVP 완료

13선행 과제 (비기술)

#과제담당의존
BIZ-1Live2D Expandable Application 사전 문의사업/법무Phase 0 시작 전
BIZ-2MVP 비공개 검증과 상용 공개의 라이선스 경계사업/법무BIZ-1
BIZ-3MVP용 Live2D 모델 확보기획/아트BIZ-1 후
BIZ-4TTS 상업 이용 라이선스사업TTS 공급자 선정 후
BIZ-5이용약관 초안 (기억 저장 동의, 삭제 권리)법무M6 설계 후
BIZ-6개인정보 처리방침 초안법무BIZ-5

Character Runtime MVP — 상세 개발 계획

2026-08-22