대시보드 폴드아웃/드릴다운 정리 + 페르소나 역린·misconduct 반응 + 게이트웨이 격리·RAG 비차단 수정
SSOT 대시보드:
- 한신대 기술분석 PDF(19쪽) 정합성 분석 + 이번 세션 발견 섹션 추가
- 섹션 폴드아웃(접기)·상단 목차(드릴다운)·모두 펼치기/접기 — 내용 보존, 레이아웃만 정리
페르소나 반응 강화('저항·반응 조절' 핵심 차별):
- PersonaCard.triggers(역린) 필드 + CCD 핵심상처 파생 역린 블록
- L0에 무례·모욕·조롱 시 현실적 동맹 균열 반응 지침
버그·성능 수정(라이브/E2E로 포착):
- 게이트웨이 페르소나 격리: --append-system-prompt를 --system-prompt(교체)로 + --exclude-dynamic-system-prompt-sections (내담자 캐릭터 붕괴·개발맥락 누출 차단)
- RAG: 임베더 동기 로드(약 7-13초)를 _warm_rag_caches 백그라운드 warm으로(세션 생성 블로킹 회귀 수정)
- voice TTS RMS 데드힌트 제거, init_state OpennessParams 파라미터객체화
- 한국어 PII(날짜·금액·주소) 마스킹 보강
- 레이아웃 시각 게이트: 폼 컨트롤 값 스크롤 오탐 제외(7/7)
검증: 백엔드 84/84, E2E 42(데스크톱 27·모바일 11·아바타 4), 시각 게이트 7/7
This commit is contained in:
parent
cb2aebd76c
commit
085460b5e0
327 changed files with 31226 additions and 1829 deletions
517
docs/guides/architecture.md
Normal file
517
docs/guides/architecture.md
Normal file
|
|
@ -0,0 +1,517 @@
|
|||
# 아키텍처 가이드
|
||||
|
||||
Vignette(AI 심리상담 시뮬레이션 훈련 플랫폼)의 시스템 아키텍처를 실제 코드 기준으로 정리한 문서다.
|
||||
모든 서술은 저장소의 실제 파일을 근거로 하며, 인용 경로는 저장소 루트(`D:/workspace/vignette`) 기준 상대경로다.
|
||||
|
||||
> 대상 독자: 백엔드/프론트 기여자, 신규 합류자, 운영자.
|
||||
> 같이 보면 좋은 문서: `docs/MASTERPLAN.md`, `docs/MEMORY_KNOWLEDGE_PERSONA_DESIGN.md`, DB DDL `infra/db/init/*.sql`.
|
||||
|
||||
---
|
||||
|
||||
## 1. 한눈에 보기
|
||||
|
||||
학습자(상담수련생)가 AI 가상내담자 페르소나와 상담 회기를 진행하고, 회기 종료 후 평가 AI가
|
||||
만든 리뷰 피드백을 받는다. 시스템은 **3종 AI 역할**을 분리한다.
|
||||
|
||||
- **내담자 AI(client)** — 페르소나를 연기. 내부 설정(CCD·진단 차원·상태 수치)은 *행동으로만* 드러냄.
|
||||
- **상담사 AI(counselor)** — 데모/self-play용(스키마·프로필 존재, 런타임 핵심 경로는 인간 학습자가 상담사 역할).
|
||||
- **평가 AI(evaluator)** — 슈퍼바이저. 학습자 발화를 4차원 태깅(fast-loop) + 회기말 정밀평가(deep-loop).
|
||||
|
||||
핵심 설계 원칙:
|
||||
|
||||
- **결정론과 LLM의 분리**: 단계 전이·개방도(openness)·저항 수치는 백엔드가 결정론으로 소유하고,
|
||||
LLM이 절대 만지지 않는다(`services/state_machine.py`). LLM은 "연기"와 "평가"만 한다.
|
||||
- **하드 안전 게이트**: PII 마스킹 후에만 외부 LLM 경로로 텍스트가 나가고(`services/guardrail.py`),
|
||||
자살 수단/방법 정보는 출력 가드레일이 차단한다.
|
||||
- **주입형(hook) 경계**: 오케스트레이터는 평가/로깅 함수를 *주입*받는다. `services/orchestrator.py`는
|
||||
`services/evaluator.py`를 import 하지 않는다(소유권 분리).
|
||||
- **degraded 폴백**: DB(단일 SoR)가 없어도 `app/store.py` in-memory 미러로 1턴이 돈다.
|
||||
|
||||
### 1.1 모노레포 레이아웃
|
||||
|
||||
```
|
||||
vignette/
|
||||
├─ apps/
|
||||
│ ├─ api/ FastAPI 백엔드 (Python)
|
||||
│ │ ├─ app/ 라우터·서비스·스토어·DB·인증
|
||||
│ │ └─ engine_gateway/ 별도 서비스: claude -p 상주 풀 / provider 어댑터
|
||||
│ └─ web/ React 19 + Vite + Playwright 프론트
|
||||
├─ infra/ docker-compose(db+api), DB 초기화 SQL
|
||||
├─ docs/ 설계·운영 문서 (이 파일 포함)
|
||||
└─ scripts/ 운영 스크립트(PowerShell 등)
|
||||
```
|
||||
|
||||
### 1.2 런타임 토폴로지
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
브라우저 │ apps/web (Vite dev :5173) │
|
||||
(학습자/교수/ │ /learn /teach /admin /settings │
|
||||
관리자) │ api.ts: fetch(credentials:include) + SSE │
|
||||
└───────────────┬──────────────────────────┘
|
||||
│ /api → 프록시(프리픽스 제거)
|
||||
▼
|
||||
┌──────────────────────────────────────────┐
|
||||
│ apps/api app.main:app (uvicorn :8000) │
|
||||
│ routes/ → services/ → engine_client │
|
||||
└───┬───────────────┬───────────────┬───────┘
|
||||
│ HTTP(ENGINE_URL)│ asyncpg 풀 │ httpx(OpenAI)
|
||||
▼ ▼ ▼
|
||||
engine_gateway Postgres16 OpenAI
|
||||
(:9099 등 host) + pgvector STT/TTS
|
||||
claude -p 상주풀 (app/audit/kb/ds) (voice 캐스케이드)
|
||||
│
|
||||
▼ subprocess
|
||||
claude CLI (Opus 4.8) — ENGINE_MODE=claude_cli
|
||||
```
|
||||
|
||||
- 프론트는 `apps/web/src/lib/api.ts`에서 모든 요청에 `credentials:"include"`를 붙여 BFF
|
||||
HttpOnly 쿠키(`__Host-vignette_sid`)를 전송한다. SSE는 `EventSource`가 아니라
|
||||
`fetch` 스트림으로 직접 파싱한다(`openSessionStream`).
|
||||
- 백엔드는 엔진 게이트웨이를 `ENGINE_URL`로 HTTP 호출만 한다(`app/engine_client.py`).
|
||||
게이트웨이가 provider 라우팅·캐싱·상주 프로세스 풀을 흡수한다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 백엔드(apps/api/app)
|
||||
|
||||
### 2.1 앱 엔트리포인트 — `app/main.py`
|
||||
|
||||
`lifespan`(startup/shutdown)에서:
|
||||
|
||||
1. `init_pool()` → DB 풀 생성, `ensure_runtime_tables()` / `ensure_review_tables()` 보장.
|
||||
2. `settings.auto_seed_personas` 면 `materialize_seed_personas()`로 시드 P1~P3을 `app.persona_card`에 upsert.
|
||||
3. `engine_client.startup()` / `voice_service.startup()`로 httpx 클라이언트 준비.
|
||||
4. **DB 초기화 실패 시** `environment == "dev"`이면 예외를 삼키고 경고만 남긴 채 degraded 기동한다
|
||||
(그 외 환경은 raise). `app/main.py:47-53`.
|
||||
|
||||
등록 라우터(`app/routes/`): `auth, admin, personas, sessions, teacher, users, eval, voice, kb`.
|
||||
|
||||
`GET /health`는 liveness + DB readiness(`db.healthcheck()`) + 엔진 게이트웨이 readiness
|
||||
(`engine_client.health_detail()`)를 합쳐 `{"status": "ok|degraded", db, engine, engine_mode, ...}`를 반환한다.
|
||||
|
||||
### 2.2 턴 오케스트레이터 — `app/services/orchestrator.py`
|
||||
|
||||
상담 1턴 파이프라인을 1~8단계로 조립한다. 모듈 상단 docstring에 단계가 그대로 명시돼 있다.
|
||||
|
||||
| 단계 | 내용 | 소유 |
|
||||
|---|---|---|
|
||||
| 1 | 입력 가드레일 — PII 마스킹 + 위기분류 | `guardrail` |
|
||||
| 2 | 상태머신 — `effective_openness` 결정론 계산 + 단계 전이 | `state_machine` |
|
||||
| 3 | 페르소나 컨텍스트 — L0~L6 messages 조립 | `persona` |
|
||||
| 4 | 내담자 AI 생성(CCD 비노출) | `engine_client` |
|
||||
| 5 | 출력 가드레일 — 자살수단 차단, ideation 상한 | `guardrail` |
|
||||
| 6 | 평가 훅(주입형) | `eval_hook` |
|
||||
| 7 | 상태 갱신(체크포인트는 호출부가 DB/store에 반영) | 호출부 |
|
||||
| 8 | 로깅 훅(주입형, turns insert/임베딩) | `log_hook` |
|
||||
|
||||
함수 경계:
|
||||
|
||||
- **`prepare_turn(...)`** (1~3단계, 순수함수, IO/LLM 없음): `TurnContext`를 만든다.
|
||||
- `guardrail.mask_pii(learner_text)`로 마스킹 → `ctx.learner_text_masked`.
|
||||
- `guardrail.classify_crisis(...)`로 위기 분류 → `ctx.crisis`.
|
||||
- 라포 신호는 `eval_rapport_signal`(평가 AI 신호)이 있으면 그것을, 없으면
|
||||
`state_machine.estimate_rapport_signal(...)` 휴리스틱을 쓴다.
|
||||
- `state_machine.evolve(...)`로 새 상태 산출 → `ctx.state_after`.
|
||||
- `persona.build_turn_messages(...)`로 L0~L6 `EngineMessage[]` 조립.
|
||||
- 회상/핀/직전 턴 등 *주입 텍스트도 전부 다시 마스킹*한다(`_mask_optional_text` 등).
|
||||
- **`run_turn_generate(ctx, engine, *, eval_hook=None, log_hook=None)`** (4~8, 동기/폴백/테스트 경로):
|
||||
`engine.generate()`로 응답 한 번에 수신 → `guardrail.sanitize_client_reply()` → 수단정보 누출 시
|
||||
안전 대체 응답("…(말을 잇지 못하고 잠시 침묵한다)")으로 치환 → eval/log 훅 순차 적용 → `TurnResult` 반환.
|
||||
훅 예외는 모두 비치명적으로 흡수(상담 루프를 막지 않음).
|
||||
- **`run_turn_stream(ctx, engine, *, log_hook=None)`** (4~8, 기본 UX 경로):
|
||||
게이트웨이 SSE 원시 라인을 받아 `token | done | safety | error`로 재방출.
|
||||
출력 가드레일은 *누적 텍스트* 기준으로 수단정보를 스캔하고, 발견 시 `safety` 이벤트 + 안전 대체로 종결한다.
|
||||
|
||||
`TurnContext`/`TurnResult`/`StreamEvent` dataclass가 파이프라인을 관통한다. `EvalHook`/`LogHook`은
|
||||
`Callable[[TurnContext, str], Awaitable[...]]` 타입으로 주입된다.
|
||||
|
||||
### 2.3 페르소나 메시지 빌더 — `app/services/persona.py` (L0~L6)
|
||||
|
||||
`build_turn_messages(card, state, learner_text_masked, ...)`가 한 턴의 `EngineMessage[]`를 조립한다.
|
||||
레이어 구조(논리 레이어, docstring):
|
||||
|
||||
| 레이어 | 내용 | cache_control | 비고 |
|
||||
|---|---|---|---|
|
||||
| L0 | 역할 + 안전 가드레일 + 도식노출금지(`L0_SAFETY`) | ✅ | 전 페르소나 공통, 캐시 대상 |
|
||||
| L1 | 페르소나 카드(정적, CCD 포함) | ✅ | `build_persona_system_text()`로 L0와 합쳐 1개 system |
|
||||
| L2 | RAG 임상청크 / 회상 + KB 행동단서 | ✅ | 회기 내 1회 로드(캐시 친화) |
|
||||
| L3 | 상태머신 주입(stage, openness, resistance, ideation) | ❌ | 수치는 내부용, 발화 표면화 금지 |
|
||||
| L4 | 메모리 버퍼(pinned fact hard-pin) | ❌ | "자기 기억"으로만 표현 |
|
||||
| L6 | 직전 K턴 맥락(히스토리) + 이번 발화(L5) | ❌ | 상담자=user, 내담자(자기)=assistant 매핑 |
|
||||
|
||||
메시지 순서: `system(L0+L1, cache)` → `system(L2, cache)` → `system(L3)` → `system(L4)` →
|
||||
assistant/user 히스토리(L6) → `user(이번 마스킹 발화, L5)`. 게이트웨이는 마지막 user를 stdin으로 주입한다.
|
||||
|
||||
**안전 불변식**(`L0_SAFETY`, docstring R4/R5/M6):
|
||||
|
||||
- CCD(core_belief/automatic_thought/coping)·DSM 차원·정답 라벨은 *행동으로만* 드러낸다.
|
||||
"제 핵심신념은…" 같은 메타 발화 절대 금지(추론 훈련 무력화 차단).
|
||||
- 자살·자해의 구체적 '방법/수단'은 절대 발화하지 않는다.
|
||||
- `_format_openness_directive()`가 `effective_openness` 수치를 연기 강도 지시문으로 환산한다(수치 자체는 비노출).
|
||||
|
||||
시드 페르소나(개발 부트스트랩): `SEED_PERSONAS = {P1, P2, P3}` — `get_seed_persona(code)`.
|
||||
|
||||
| 코드 | 인물 | 난이도 | 이론타깃 | 특징 |
|
||||
|---|---|---|---|---|
|
||||
| P1 | 서연(고2) | hard | humanistic | 우울/자살사고(ideation_stage=2), 비자발·고저항(base 0.7) |
|
||||
| P2 | 민재(32) | moderate | cbt | 범불안/신체화, 자살사고 없음(ideation 1) |
|
||||
| P3 | 지우(28) | moderate | humanistic | 미혼모 역할부담·소진, 라포/무조건적 존중 연습용 |
|
||||
|
||||
### 2.4 결정론 상태머신 — `app/services/state_machine.py`
|
||||
|
||||
LLM 아님. 순수함수 + 작은 dataclass `SessionState`.
|
||||
|
||||
- 단계(`Stage`): `라포 → 탐색 → 개입 → 정리` (선형, 역행 없음). `STAGE_ORDER`.
|
||||
- 단계 전이(`next_stage`): **최소 체류 턴(`STAGE_MIN_TURNS`) + 누적 라포 임계(`STAGE_ADVANCE_RAPPORT`)**
|
||||
동시 충족 시 다음 단계. `CLOSE`는 종착(명시 종료).
|
||||
- 개방도 공식(`compute_effective_openness`):
|
||||
`effective_openness = clamp(stage_base + rapport_credit*unlock_rate − resistance*decay, 0, 1)`.
|
||||
- 라포 신호 휴리스틱(`estimate_rapport_signal`): 공감/반영/타당화/개방질문 키워드는 +,
|
||||
조언점프/평가/유도/당위는 −. **경량 추정**일 뿐, 정밀 4차원 채점은 평가 AI 소유.
|
||||
- `evolve(state, rapport_signal, unlock_rate, decay_floor, ideation_observed=None)`:
|
||||
① rapport_credit 누적(부정 신호는 더 크게 차감 → "닫힘" 재현) ② resistance 완화/강화
|
||||
③ 개방도 재계산 ④ 단계 전이 판정 ⑤ **ideation_stage 보수적 유지(절대 내려가지 않음, 안전 R5)**.
|
||||
- `init_state(..., carry=None)`: 이전 회기 `end_state`가 있으면 결정론 carry-over
|
||||
(라포 ×0.7 이월, resistance drift, ideation 보수적 유지).
|
||||
|
||||
수치는 무손실로 carry-over 된다(`SessionState.snapshot()`). `app.session_state` DDL의 제약
|
||||
(`effective_openness 0~1`, `ideation_stage 1~5`)과 정합한다.
|
||||
|
||||
### 2.5 가드레일 — `app/services/guardrail.py`
|
||||
|
||||
세 가지 순수 함수 경계:
|
||||
|
||||
1. **입력 PII 마스킹** `mask_pii(text) -> MaskResult`: Presidio가 설치돼 있으면 우선 사용,
|
||||
미설치면 정규식 폴백(`_PII_PATTERNS`: 주민번호/휴대폰/전화/이메일/장문 숫자열). 마스킹본만
|
||||
저장·외부 LLM 전송에 쓴다(하드 게이트, F-03). Presidio는 지연 로드 캐시(`_try_load_presidio`).
|
||||
2. **위기 분류** `classify_crisis(text, speaker_is_persona_context=True) -> CrisisResult`:
|
||||
가상내담자의 자살사고 *연기*는 시뮬레이션 정상(`PERSONA_PLAY`, escalate=False). 그러나
|
||||
1인칭 실제 단서(`_FIRST_PERSON_NOW`)가 강하면 **수련생 본인의 실제 위기**(`LEARNER_REAL`,
|
||||
escalate=True)로 보수적 승격.
|
||||
3. **출력 가드레일** `sanitize_client_reply(text, ideation_stage) -> OutputGuardResult`:
|
||||
자살/자해 수단·방법 패턴(`_MEANS_TERMS`)이 있으면 `needs_regeneration=True`(차단·재생성 신호).
|
||||
`ideation_stage > IDEATION_STAGE_CAP(3)`이면 상한 위반 기록. `clamp_ideation()`로 안전 상한 강제.
|
||||
|
||||
> 위기분류 에스컬레이션은 DB `app.safety_events`(trigger_type/ko_risk_level/escalated)와 매핑된다.
|
||||
> 정밀화(Presidio MedicalNER, 한국어 자살콘텐츠 분류기)는 docstring의 Phase 2 TODO로 표기돼 있다.
|
||||
|
||||
### 2.6 평가 AI — `app/services/evaluator.py` (deep/fast-loop + make_eval_hook)
|
||||
|
||||
평가 AI는 *전부 봐도 된다*(CCD/정답/상태 수치 포함). 비노출은 client AI 책임이고, 학습자에겐
|
||||
RBAC×AIView로 차단된다. 이 모듈은 평가 신호만 산출한다.
|
||||
|
||||
- **fast-loop** `evaluate_turn(ctx, client_reply, engine) -> TurnEvaluation`: 턴 직후 경량 4차원
|
||||
① technique[] ② client_state_read[] ③ appropriateness(pos/warn/neutral) ④ intent_deviation
|
||||
(`{dimension, expected, actual, severity}`, "의도와 다른 부분" 1급 시민). `rapport_signal`도 함께
|
||||
산출해 상태머신에 주입 가능. `tier="feedback"`, `structured_schema=_fast_schema()`.
|
||||
- **deep-loop** `evaluate_session(...) -> SessionEvaluation`: 단계전환/회기말 정밀평가. 기법 분포
|
||||
(`aggregate_distribution`, 코드 결정론 집계) + strengths + improvements(최대 3) +
|
||||
supervisor rationale/critique + alternative_utterances. `_deep_schema()`.
|
||||
- 정답 라벨 enum은 `app/taxonomy.py`가 단일 원천(SoT). LLM 출력은 enum으로 안전 파싱(미지값 폐기,
|
||||
`_parse_technique`/`_parse_client_state`). 게이트웨이 structured 우선, 없으면 text에서 JSON 추출
|
||||
(`_structured_payload`, 코드펜스 관용).
|
||||
- **엔진/파싱 실패는 비치명적** — `error` 필드에 사유만 남기고 절대 raise 하지 않는다.
|
||||
- **주입 어댑터** `make_eval_hook(engine)`: orchestrator의 `EvalHook` 시그니처에 맞춘 클로저를 반환.
|
||||
세션 라우트가 `run_turn_generate(ctx, engine, eval_hook=make_eval_hook(engine_client))` 식으로 주입한다.
|
||||
|
||||
⚠️ 의존 방향: evaluator는 `taxonomy/engine_client/orchestrator/state_machine/persona`를 *읽기 전용*으로만
|
||||
의존하고, orchestrator는 evaluator를 import 하지 않는다(단방향).
|
||||
|
||||
### 2.7 회기 메모리 — `app/services/memory.py`
|
||||
|
||||
회기 라이프사이클 메모리(4계층 매핑: ① working / ② episodic / ③ summary / ④ semantic).
|
||||
|
||||
- 회기 시작: `build_recall_context(...) -> RecallContext` — 큰그림(case_digest) → 직전 요약 →
|
||||
episodic 단편 순으로 회상 조립. **회상 요약엔 CCD·정답·평가가 절대 들어가지 않는다**(내담자 뷰, M6).
|
||||
DB/RAG가 없으면 인자 None → 빈 RecallContext(첫 회기/in-proc 폴백).
|
||||
- 회기 종료: `make_carry_over(state, ...) -> CarryOver` — (A) `end_state = state.snapshot()`
|
||||
무손실 코드 복사(P4) (B) `rapport_delta` (C) 서사 digest는 `CompressionJob`으로 큐잉(LLM 비동기,
|
||||
여기선 페이로드만). 실제 LLM 호출은 호출부/백그라운드가 수행.
|
||||
|
||||
### 2.8 음성 캐스케이드 — `app/services/voice.py` + `app/routes/voice.py`
|
||||
|
||||
OpenAI STT/TTS 어댑터(순수 변환 + voice preset 매핑). 상담 로직은 라우트가 조립한다.
|
||||
|
||||
- STT: `/audio/transcriptions` (gpt-4o-transcribe → 404 시 whisper-1 폴백), 언어 힌트 `ko`.
|
||||
- TTS: `/audio/speech` (gpt-4o-mini-tts → tts-1 폴백). 페르소나 preset → OpenAI voice 매핑
|
||||
(`PRESET_TO_OPENAI_VOICE`, P1=coral / P2=ash / P3=shimmer). 스트림 청크마다 립싱크 RMS 힌트
|
||||
(`estimate_chunk_rms`, PCM 디코딩 없이 바이트 에너지 근사) 동봉.
|
||||
- 키 없으면 명확히 degraded(`is_available()=False`, `VoiceUnavailable`). 라우트가 503/WS close로 변환.
|
||||
|
||||
`/voice/ws` WebSocket 캐스케이드(`routes/voice.py`):
|
||||
```
|
||||
client: audio_start → [binary chunks] → audio_end
|
||||
server: ready → state(listening) → state(thinking) → transcript → reply
|
||||
→ state(speaking) → tts_chunk(메타) + [binary audio] → tts_end → state(idle)
|
||||
```
|
||||
- 인증: REST와 동일한 서버측 세션 쿠키를 WebSocket 쿠키에서 복원(`_principal_from_websocket`),
|
||||
learner만 허용.
|
||||
- 턴 실행은 동일하게 `orchestrator.prepare_turn` → `run_turn_generate`를 거치고, 응답 생성 *후에만*
|
||||
발화를 영속화한다(실패한 AI 턴이 학습자 단독 축어록을 남기지 않도록).
|
||||
- `text_turn` 경로는 접근성/결정론 테스트용 텍스트 전용 경로.
|
||||
|
||||
### 2.9 세션 라우트 — `app/routes/sessions.py` (실제 데이터 흐름)
|
||||
|
||||
학습자 전용. 모든 세션 작업은 소유권(learner_id)을 검사한다(`_load_session_or_404`).
|
||||
|
||||
핵심 엔드포인트:
|
||||
|
||||
- `POST /sessions` — `get_catalog_persona(code)`로 승인 카드 조회 → `memory.build_recall_context()` →
|
||||
`state_machine.init_state(...)` → `session_persistence.create_session(...)`. DB가 없으면
|
||||
`runtime_fallback_allowed()` 확인 후 `store.create(...)`로 in-proc 생성.
|
||||
- `POST /sessions/{id}/turn` — 동기 경로. `prepare_turn` → `run_turn_generate` →
|
||||
학습자 발화 turn(speaker=`counselor`, 원문 `text` + `text_masked`) + 내담자 응답 turn(speaker=`client`)
|
||||
순차 append → 상태 갱신. `EngineError`는 503으로 변환.
|
||||
- `POST /sessions/{id}/stream` — SSE 경로(`run_turn_stream`). token/done/ping/error를 흘리고,
|
||||
done 시점에 학습자 발화 + 누적 내담자 응답을 영속화. `sse_heartbeat_seconds`마다 ping(Cloudflare 타임아웃 회피).
|
||||
- `POST /sessions/{id}/end` — `memory.make_carry_over(...)` → 세션 종료 + carry 준비 →
|
||||
`_schedule_session_evaluation(sess)`로 deep-loop 평가를 비동기 태스크로 발사.
|
||||
- `GET /sessions/{id}/review` — 저장된 축어록 + 평가 AI 산출물로 학습자-안전 리뷰 구성
|
||||
(`_rubric_from_evaluation`, `_ai_review_points` 등). 평가 미완이면 `degraded/reviewReady=false`로 표기.
|
||||
|
||||
발화 가시성: 학습자에게는 `visible_to`에 `counselor`가 포함된 턴만 보여준다
|
||||
(`_learner_visible_turns` → `_LEARNER_VISIBLE_AI_ROLE="counselor"`). 평가 전용 데이터는 노출되지 않는다.
|
||||
|
||||
### 2.10 엔진 클라이언트 — `app/engine_client.py`
|
||||
|
||||
게이트웨이 HTTP 클라이언트(호출부만). 계약:
|
||||
|
||||
```
|
||||
POST {ENGINE_URL}/v1/generate — 단발 생성(평가 deep-loop, 회기종료 압축 등)
|
||||
POST {ENGINE_URL}/v1/stream — SSE 토큰 스트림(내담자 AI 응답)
|
||||
GET {ENGINE_URL}/ready|/health — readiness/liveness
|
||||
```
|
||||
|
||||
- `EngineMessage(role, content, cache)` — `cache`는 L0~L2 cache_control 힌트(게이트웨이가 해석).
|
||||
- `GenerateRequest(ai_role, messages, tier, model, max_tokens, temperature, structured_schema, session_id, metadata)`
|
||||
— `tier`: `client`(Sonnet/Solar) / `feedback`(Opus) / `fast`(Haiku) 라우팅 힌트.
|
||||
- `session_id`를 넘기면 게이트웨이가 상주 풀을 재사용해 멀티턴 prompt caching 이점을 살린다.
|
||||
- 장애는 `EngineError`로 전파 → 라우트가 503/SSE error 프레임으로 변환.
|
||||
- 앱 전역 싱글톤 `engine_client`(lifespan에서 startup/shutdown).
|
||||
|
||||
### 2.11 in-memory 스토어 — `app/store.py` (degraded 폴백)
|
||||
|
||||
`app.sessions / app.session_state / app.turns`의 최소 in-proc 미러. DB가 붙으면 라우트가 DB 경로로 전환한다.
|
||||
|
||||
- `InProcSession`(working + 메타 + 페르소나 핀), `TurnRecord`(append-only 발화, `visible_to` 기본
|
||||
`(client, counselor, evaluator)`).
|
||||
- `recent_turns(k, visible_to)`(L6 버퍼), `masked_turns()`(평가/압축 입력)는 항상 마스킹본을 쓴다.
|
||||
- 단일 프로세스(uvicorn) 가정의 단순 dict. 멀티워커에선 DB가 SoR이므로 무방.
|
||||
- 영속/폴백 분기는 `app/runtime_policy.py`(`runtime_fallback_allowed` / `require_runtime_fallback_allowed`)가
|
||||
게이트. 라우트는 `session_persistence`(DB) → 실패 시 store(in-proc) 순으로 시도한다.
|
||||
|
||||
### 2.12 페르소나 카탈로그 — `app/persona_repository.py`
|
||||
|
||||
DB `app.persona_card`가 승인 페르소나의 SoR. 이 모듈이 in-proc `PersonaCard`와 DB 행을 잇는 경계다.
|
||||
|
||||
- `materialize_seed_personas()` — 시드 P1~P3을 `status='approved'`로 upsert(admin 롤).
|
||||
- `get_approved_persona(code)` / `list_approved_personas()` — 승인된 최신 버전 조회(AI 컨텍스트).
|
||||
- `get_catalog_persona(code)` — 승인 카드 조회, 실패 시 `settings.allow_seed_persona_fallback`이면
|
||||
`seed_fallback_persona`(degraded=True)로 폴백.
|
||||
- 교수 검수: `list_persona_review_queue(role)` / `update_persona_review_status(action=approve|reject)`
|
||||
— 승인/반려 시 `audit.audit_log`에 감사 기록.
|
||||
|
||||
---
|
||||
|
||||
## 3. 엔진 게이트웨이 — `apps/api/engine_gateway/gateway.py`
|
||||
|
||||
컨테이너 밖(호스트)에서 도는 **별도 서비스**. `claude -p`(Opus 4.8) 상주 멀티턴 풀을 흡수한다.
|
||||
회기당 1 `EngineSession` = `claude -p` 프로세스 1개 상주 → 페르소나 system 프롬프트 고정 + 발화마다
|
||||
stdin 주입(턴 간 컨텍스트 유지 + prompt caching 재사용).
|
||||
|
||||
실행:
|
||||
```
|
||||
cd apps/api && uvicorn engine_gateway.gateway:app --host 0.0.0.0 --port 9099
|
||||
```
|
||||
|
||||
엔드포인트 2계열:
|
||||
|
||||
- **상주 풀** `/session`, `/session/{sid}/turn`, `/session/{sid}`(DELETE) — 회기 단위 프로세스.
|
||||
- **stateless 어댑터**(engine_client 계약과 정합):
|
||||
- `POST /v1/generate` — `EngineMessage[]` → `_split_messages()`로 (system_prompt, last_user) 분리
|
||||
→ `--append-system-prompt`로 주입, 마지막 user를 stdin content로. structured_schema는
|
||||
`_inject_schema()`로 system에 JSON 준수 지시 주입(claude -p는 response_format 미지원이라 차선).
|
||||
- `POST /v1/stream` — `turn_stream()`이 assistant 텍스트 델타를 즉시 yield → SSE
|
||||
`event: token` / `event: done`(provider/model/cost 메타) / `event: error` 프레이밍.
|
||||
- `session_id`가 살아있으면 풀 재사용, 아니면 1회성(ephemeral) 세션 생성 후 close(`_resolve_session`).
|
||||
- `GET /ready` — `/health`(얕은 프로세스 liveness)와 달리 실제 1턴 생성을 시도해
|
||||
"설치됐지만 미인증" CLI 상태를 학습자 도달 전에 잡는다(TTL 캐시).
|
||||
|
||||
provider 라우팅 모드는 `ENGINE_MODE`(`app/config.py`)로 선택: `claude_api`(기본, Anthropic Messages API
|
||||
직결) / `claude_cli`(로컬 상주 풀) / `openai`(폴백/평가 보조) / `solar`(국내, PII 민감구간 inference_geo:kr).
|
||||
|
||||
> 포트 주의: 게이트웨이 docstring 예시는 `:9099`이고, `app/config.py`의 `engine_url` 기본값은
|
||||
> compose 서비스명 기반 `http://engine:8100`이다. 로컬에서는 `ENGINE_URL`을 게이트웨이 실제 포트
|
||||
> (예: `http://127.0.0.1:9099`)로 맞춰 주입한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 프론트엔드(apps/web)
|
||||
|
||||
React 19 + Vite. 라우팅은 `apps/web/src/App.tsx`(react-router-dom).
|
||||
|
||||
| 경로 | 화면 | 역할 가드 |
|
||||
|---|---|---|
|
||||
| `/login` | Login (dev-login 경로 포함) | 공개 |
|
||||
| `/learn` | LearnerHome | learner |
|
||||
| `/learn/session/:sessionId` | Session(상담 화면) | learner |
|
||||
| `/learn/session/:sessionId/review` | SessionReview(회기 리뷰) | learner |
|
||||
| `/learn/avatar-expressions` | AvatarExpressionLab | learner |
|
||||
| `/teach` | Professor(교수자 대시보드) | teacher |
|
||||
| `/admin` | Admin | admin |
|
||||
| `/settings` | Settings | 3역할 공통 |
|
||||
|
||||
- `RequireAuth`가 `lib/auth`의 AuthContext로 가드하고, 권한 불일치 시 역할 홈(`roleHomePath`)으로 보낸다.
|
||||
- API 클라이언트 `src/lib/api.ts`:
|
||||
- `apiFetch`/`api`(get/post/put/del) — 표준 JSON, 에러는 `ApiError`로 정규화, 항상 `credentials:"include"`.
|
||||
- SSE: `openSessionStream(sessionId, text, handlers)` — `fetch` 스트림을 직접 라인 파싱해
|
||||
`token | done | safety | ping | error` 이벤트를 콜백으로 전달.
|
||||
- 도메인 헬퍼: `sessionApi`(list/get/start/turn/end/review/stream), `personaApi`, `personaReviewApi`,
|
||||
`adminApi`, `adminEngineApi`, `teacherApi`, `userApi`. 응답 타입은 백엔드 라우트의 pydantic 모델 미러.
|
||||
- dev 환경: `vite.config`가 `/api` → `http://127.0.0.1:8000` 프록시(`/api` 프리픽스 제거).
|
||||
배포 호스트(`vignette.chanpaca.net`, `*.pages.dev`)에서는 `api-vignette.chanpaca.net`을 직접 가리킨다.
|
||||
|
||||
세션 화면 흐름(요약): 학습자 발화 입력 → `sessionApi.stream(id, text)`(SSE) 또는 `turn`(동기) →
|
||||
토큰 누적 표시 → 회기 종료(`end`) → `/review`에서 `sessionApi.review(id)`로 리뷰 표시.
|
||||
|
||||
---
|
||||
|
||||
## 5. 데이터베이스 스키마 개요
|
||||
|
||||
DB는 PostgreSQL 16 + pgvector(단일 SoR). 초기화 SQL은 `infra/db/init/`에 순서대로 적용된다
|
||||
(`01_extensions → 02_schema → 03_kb → 04_audit_eval_rls → 05_runtime_auth → 06_session_evaluation`).
|
||||
스키마는 **4분할**: `app` / `kb` / `audit` / `ds`.
|
||||
|
||||
### 5.1 app 스키마 (`infra/db/init/02_schema.sql`)
|
||||
|
||||
- **인적 주체** `app.app_user` — RBAC 앵커(role: learner/instructor/admin, cohort, consent_at).
|
||||
- **페르소나** `app.persona_card` — 불변 버전드 카드(`UNIQUE(code, version)`, status draft→review→approved→archived,
|
||||
ccd/dsm5_dimensional/affect_baseline는 JSONB). `app.persona_voice_map`(provider-agnostic 음성),
|
||||
`app.counselor_profile`(상담사 AI).
|
||||
- **라벨 코드테이블**(taxonomy 3축) — `stage_def`, `technique_label_def`, `client_state_def`, `scale_def`.
|
||||
- **세션** `app.sessions` — case_id/persona_id/persona_version 핀, stage_path. `case_id`는
|
||||
(persona_id, learner_id) 복합 인스턴스 식별.
|
||||
- **발화** `app.turns` — ② EPISODIC append-only. `text`(마스킹 원문)/`text_masked`(외부전송용),
|
||||
`visible_to TEXT[]`(기본 `{client,counselor,evaluator}`), salience/is_pinned/contradicts,
|
||||
음성 paralinguistic(audio_ref/silence_ms/speech_rate/barge_in). `turn_technique`/`turn_client_state`
|
||||
다대다, `supervisor_comment`(rationale/critique + intent_deviation JSONB), `safety_events`.
|
||||
- **메모리 4계층**:
|
||||
- ① WORKING `app.session_state` — 상태머신 수치 체크포인트(매 턴 UPSERT, openness/ideation CHECK 제약).
|
||||
- ② EPISODIC 임베딩 `app.turn_embedding` — BGE-M3 dense `vector(1024)` + sparse, HNSW 인덱스.
|
||||
- ③ SUMMARY `app.session_summary` — (A) end_state 무손실 carry-over + (B) digest narrative.
|
||||
- ④ SEMANTIC `app.case_profile`(evolving, `UNIQUE(persona_id, learner_id)`) + `app.pinned_fact`
|
||||
(+`pinned_fact_history` append-only).
|
||||
|
||||
### 5.2 audit 스키마 + app 평가/종단 (`infra/db/init/04_audit_eval_rls.sql`)
|
||||
|
||||
- 평가: `app.feedback_scores`(발화별 점수, `visible_to='{evaluator}'`, loop fast/deep),
|
||||
`app.alternative_utterance`(코칭은 학습자 노출).
|
||||
- 종단: `app.learner_profile`(dim_ewma/dim_slope/persistent_gaps — 회기 거듭하며 나아지는지).
|
||||
- 감사(append-only): `audit.persona_drift_log`(임베딩 일관성/CCD 누출), `audit.audit_log`(인간 열람 추적),
|
||||
`audit.llm_call_log`(비용·inference_geo).
|
||||
|
||||
### 5.3 ds 스키마 — 재귀학습 파이프라인 (`infra/db/init/04_audit_eval_rls.sql` §4)
|
||||
|
||||
AI자동(1R) → 인간검수(2R) → IAA 게이트(κ≥0.6, ICC≥0.75) → 골든셋 → JSONL.
|
||||
`ds.dataset` → `ds.dataset_item` → `ds.annotation_round`(ai_auto/human_review) → `ds.annotation`
|
||||
→ `ds.export_manifest`(iaa_kappa/iaa_icc/jsonl_path).
|
||||
|
||||
### 5.4 정보비대칭 2-레이어 + RLS
|
||||
|
||||
DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
|
||||
|
||||
- **레이어1 (AI 정보비대칭)**: `app.ai_context='1'` + `app.current_ai_view`(client/counselor/evaluator)
|
||||
+ `app.current_sens_max` → `turns`/`kb.chunk`/`pinned_fact`의 `visible_to[]`/`sensitivity` WHERE 강제.
|
||||
AIView별 sensitivity 상한 기본값: client=1, counselor=0, evaluator=2(`db._default_sensitivity_max`).
|
||||
- **레이어2 (인간 RBAC×cohort)**: `app.current_role` + `app.current_uid` + `app.current_cohort`로
|
||||
RLS 정책(학습자=본인 / 교수자=담당 코호트 / 관리자=전부). `feedback_scores`는 학습자 직접열람 차단.
|
||||
|
||||
세션 변수는 트랜잭션 내 `SELECT set_config(..., true)`(SET LOCAL 의미)로 주입해 풀 재사용 누수를 막는다
|
||||
(`app/db.py:94-137`). 애플리케이션 측 RBAC는 `app/deps.py`(`Role`, `AIView`, `Principal`,
|
||||
`get_current_principal`, `require_role`, `db_for_human`, `db_for_ai_view`)가 담당한다.
|
||||
|
||||
---
|
||||
|
||||
## 6. degraded / 폴백 정책
|
||||
|
||||
| 미가용 대상 | 증상 | 동작 |
|
||||
|---|---|---|
|
||||
| DB(Postgres) | `/health` `db:false`, status `degraded` | `app/main.py` lifespan이 dev에서 예외 흡수, `store.py` in-proc로 1턴 동작 |
|
||||
| 엔진 게이트웨이 | `/health` `engine:false` | 턴 생성 실패(503/SSE error). UI/로그인/페르소나/세션생성(in-memory)은 동작 |
|
||||
| OpenAI 키 미설정 | `/voice/health` `degraded` | 음성 비활성, WS는 degraded 통지 후 정상 close |
|
||||
| Presidio 미설치 | — | 정규식 PII 마스킹 폴백 |
|
||||
| 승인 페르소나 조회 실패 | persona `degraded=true` | `ALLOW_SEED_PERSONA_FALLBACK=true`면 시드 카드로 폴백 |
|
||||
|
||||
폴백 허용 여부는 `app/runtime_policy.py`가 게이트한다(무분별한 in-proc 사용 방지).
|
||||
|
||||
---
|
||||
|
||||
## 7. 로컬 실행 (Windows 11 + PowerShell 기준)
|
||||
|
||||
> 한글/공백 경로 주의. `apps/api/.env`는 pydantic-settings가 자동 로드한다.
|
||||
|
||||
### 7.1 API (DB 없이 degraded 기동 가능)
|
||||
|
||||
```powershell
|
||||
cd D:\workspace\vignette\apps\api
|
||||
# 시드 페르소나를 in-proc로 쓰려면(개발용):
|
||||
$env:AUTO_SEED_PERSONAS = "true"; $env:ALLOW_SEED_PERSONA_FALLBACK = "true"
|
||||
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload
|
||||
# 확인: GET http://127.0.0.1:8000/health → {"status":"degraded","db":false,...} (DB 없을 때)
|
||||
```
|
||||
|
||||
dev 인증(`apps/api/.env`에 `AUTH_DEV_LOGIN_ENABLED=true`, `ENVIRONMENT=dev`):
|
||||
```
|
||||
POST /auth/dev-login {email, role: learner|teacher|admin, display_name}
|
||||
→ __Host-vignette_sid 쿠키 발급(웹은 Login.tsx에 dev-login 경로 존재)
|
||||
```
|
||||
|
||||
### 7.2 엔진 게이트웨이 (AI 턴 생성, ENGINE_MODE=claude_cli)
|
||||
|
||||
```powershell
|
||||
cd D:\workspace\vignette\apps\api
|
||||
python -m uvicorn engine_gateway.gateway:app --host 0.0.0.0 --port 9099
|
||||
# API의 ENGINE_URL을 게이트웨이 포트로 맞춘다(예: http://127.0.0.1:9099)
|
||||
```
|
||||
게이트웨이가 없으면 `/health`의 `engine:false`이고 턴 생성은 실패하지만, UI/네비/로그인/페르소나/
|
||||
세션생성(in-memory)은 동작한다.
|
||||
|
||||
### 7.3 웹
|
||||
|
||||
```powershell
|
||||
cd D:\workspace\vignette\apps\web
|
||||
npm run dev # http://localhost:5173, /api → 127.0.0.1:8000 프록시
|
||||
```
|
||||
|
||||
### 7.4 테스트
|
||||
|
||||
```powershell
|
||||
# 백엔드
|
||||
cd D:\workspace\vignette\apps\api
|
||||
python -m pytest app/ -q # 앱 단위 테스트
|
||||
python -m pytest engine_gateway/ -q # 게이트웨이 테스트
|
||||
# 웹
|
||||
cd D:\workspace\vignette\apps\web
|
||||
npm run typecheck
|
||||
npm run build # vite
|
||||
npm run e2e # Playwright (web+api+DB 스택 필요)
|
||||
```
|
||||
|
||||
### 7.5 Docker compose (DB + API)
|
||||
|
||||
`infra/docker-compose.yml`(db: pgvector pg16 + api). Docker Desktop 필요. DB 컨테이너 기동 시
|
||||
`infra/db/init/*.sql`이 순서대로 적용된다.
|
||||
|
||||
---
|
||||
|
||||
## 8. 안전 불변식 요약(설계 보증)
|
||||
|
||||
- 마스킹되지 않은 원문은 외부 LLM 경로로 절대 나가지 않는다(입력 가드레일 하드 게이트, F-03).
|
||||
- 내담자 AI는 CCD·진단 차원·정답 라벨·상태 수치를 *말로 설명하지 않는다*(L0 + 출력 가드레일 이중방어, R4).
|
||||
- 자살·자해 수단/방법 정보는 출력 가드레일이 차단·치환한다. `ideation_stage` 안전 상한은 3(R5).
|
||||
- 상태머신 수치(stage/openness/resistance/ideation)는 결정론 코드만 만지고 LLM에 위임하지 않으며,
|
||||
무손실 carry-over 된다(P2/P4).
|
||||
- 평가/정답/평가 전용 데이터는 RBAC×AIView(레이어1 `visible_to`) + RLS(레이어2)로 학습자에게서 차단된다.
|
||||
- 평가/로깅 훅 실패는 비치명적으로 흡수되어 상담 루프를 멈추지 않는다.
|
||||
```
|
||||
350
docs/guides/local-development.md
Normal file
350
docs/guides/local-development.md
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
# 로컬 개발 실행 가이드
|
||||
|
||||
Vignette(AI 심리상담 시뮬레이션 훈련 플랫폼)를 로컬에서 띄워서 테스트하기 위한 단계별 가이드다.
|
||||
주 환경은 **Windows 11 + PowerShell**을 기준으로 하되, 셸 차이가 중요한 곳은 별도로 표시한다.
|
||||
|
||||
- 작업 디렉터리(저장소 루트): `D:\workspace\vignette`
|
||||
- 모노레포 구성
|
||||
- `apps/api` — FastAPI 백엔드 (Python)
|
||||
- `apps/api/engine_gateway` — AI 턴 생성용 엔진 게이트웨이(별도 프로세스, 포트 9099)
|
||||
- `apps/web` — React 19 + Vite 프런트엔드
|
||||
- `infra` — Docker Compose 스택(pgvector pg16 + api + web + rag + proxy)
|
||||
- `scripts` — 운영/점검 스크립트
|
||||
|
||||
핵심 구성요소: 학습자(상담수련생)가 AI 내담자 페르소나와 회기를 진행하고, 종료 후 회기 리뷰 피드백을 받는다.
|
||||
백엔드는 페르소나 엔진 · 오케스트레이터 · 저항엔진 · 마스킹 게이트 · 음성 캐스케이드 · 회기 리뷰를 소유하고,
|
||||
실제 AI 발화 생성은 `engine_gateway`(ENGINE_MODE 라우팅)가 담당한다. 단일 SoR은 Postgres16+pgvector이며,
|
||||
DB가 없으면 인메모리 degraded 폴백으로 기동한다.
|
||||
|
||||
---
|
||||
|
||||
## 0. 가장 빠른 경로 — 한 커맨드 (권장)
|
||||
|
||||
게이트웨이(9099)+API(8000)+웹(5173)을 한 번에 깔끔히 (재)기동한다. 기존/고아 프로세스를
|
||||
커맨드라인 기준으로 정리한 뒤 결정론적으로 띄운다(`--reload` 워처 불안정 회피).
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-up.ps1
|
||||
# 종료: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-down.ps1
|
||||
```
|
||||
|
||||
- 진입점 **http://localhost:5173** → 로그인 페이지에서 **dev-login**(아무 `@hs.ac.kr`, role learner/teacher/admin).
|
||||
- DB 없이 in-memory degraded로 뜨고, seed 페르소나(P1~P3)·AI 턴 생성까지 동작.
|
||||
- 로그는 `.devlogs/`(gitignore). 코드 수정 후에는 dev-up을 다시 실행해 재기동(reload 미사용).
|
||||
- 옵션: `-NoGateway`(UI만), `-NoWeb`(API만).
|
||||
|
||||
> 스크립트는 uvicorn이 설치된 python을 자동 해석한다(시스템에 복수 python 공존 시 'python' 별칭이
|
||||
> uvicorn 없는 인터프리터를 가리킬 수 있음 — 이 함정 때문에 명시 해석함).
|
||||
|
||||
### 수동 기동(대안)
|
||||
|
||||
DB·엔진 없이도 UI/로그인/페르소나/세션 생성까지는 동작한다(턴 생성만 불가).
|
||||
|
||||
1. **API** — `apps/api`에서
|
||||
`python -m uvicorn app.main:app --host 127.0.0.1 --port 8000`
|
||||
2. **웹** — `apps/web`에서 `npm run dev` → http://localhost:5173
|
||||
3. (선택) **엔진 게이트웨이** — `apps/api`에서
|
||||
`python -m uvicorn engine_gateway.gateway:app --host 127.0.0.1 --port 9099`
|
||||
(실제 AI 턴 생성을 하려면 필요. `claude` CLI 설치+로그인 전제)
|
||||
|
||||
아래에서 각 단계를 자세히 설명한다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 사전 준비
|
||||
|
||||
- **Python 3.11** (운영 스크립트가 Python 3.11 기준). 가상환경 권장.
|
||||
- **Node.js**(최신 LTS) + npm — `apps/web`용.
|
||||
- (선택) **Docker Desktop** — `infra/docker-compose.yml` 전체 스택을 띄울 때만.
|
||||
- (선택) **`claude` CLI** — `ENGINE_MODE=claude_cli`로 실제 턴 생성을 할 때. 설치 후 로그인되어 있어야 한다.
|
||||
|
||||
### Python 의존성 설치 (`apps/api`)
|
||||
|
||||
```powershell
|
||||
# 저장소 루트에서
|
||||
python -m venv .venv
|
||||
.\.venv\Scripts\Activate.ps1 # PowerShell 활성화
|
||||
python -m pip install -r apps\api\requirements.txt
|
||||
```
|
||||
|
||||
`apps/api/requirements.txt`는 `fastapi`, `uvicorn[standard]`, `asyncpg`, `pydantic`,
|
||||
`pydantic-settings`, `python-multipart`, `httpx`, `sse-starlette`를 포함한다.
|
||||
엔진 게이트웨이도 `fastapi`+`uvicorn`만 쓰므로 위 설치로 함께 충족된다.
|
||||
(Presidio PII 마스킹은 선택 의존성이라 기본 제외 — 없으면 정규식 폴백으로 자동 degrade.)
|
||||
|
||||
### 웹 의존성 설치 (`apps/web`)
|
||||
|
||||
```powershell
|
||||
cd apps\web
|
||||
npm install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. API 서버 실행 (FastAPI)
|
||||
|
||||
### 2.1 환경설정(.env 자동 로드)
|
||||
|
||||
설정은 전부 env 주입이며 `pydantic-settings`가 **작업 디렉터리의 `.env`를 자동 로드**한다
|
||||
(`apps/api/app/config.py`의 `SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")`).
|
||||
즉 **`apps/api` 디렉터리에서 uvicorn을 실행**하면 `apps/api/.env`가 자동 반영된다.
|
||||
|
||||
로컬 dev에서 중요한 키(이미 `apps/api/.env`에 셋업되어 있거나, 없으면 `.env.example` 참고):
|
||||
|
||||
| 키 | 로컬 dev 값 | 의미 |
|
||||
| --- | --- | --- |
|
||||
| `ENVIRONMENT` | `dev` | dev여야 DB 폴백·dev-login·seed 폴백이 허용됨 |
|
||||
| `DATABASE_URL` | `postgresql://...@127.0.0.1:55432/vignette` | 미연결 시 degraded 폴백(dev 한정) |
|
||||
| `ENGINE_URL` | `http://127.0.0.1:9099` | 엔진 게이트웨이 베이스 URL |
|
||||
| `ENGINE_MODE` | `claude_cli` | 엔진 provider 라우팅 |
|
||||
| `AUTH_DEV_LOGIN_ENABLED` | `true` | dev-login 엔드포인트 활성화 |
|
||||
| `AUTH_ALLOWED_EMAIL_DOMAINS` | `["hs.ac.kr","twentyoz.kr"]` | 로그인 허용 이메일 도메인(dev-login 포함 검증) |
|
||||
|
||||
> 참고: 프로세스 환경변수(`$env:KEY`)는 `.env`보다 우선한다. 일회성 오버라이드에 쓸 수 있다.
|
||||
|
||||
### 2.2 실행
|
||||
|
||||
```powershell
|
||||
cd apps\api
|
||||
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload
|
||||
```
|
||||
|
||||
기동 로그에 `Application startup complete.`가 뜨면 정상이다.
|
||||
프록시(아래 웹)는 `/api` 프리픽스를 제거해 이 8000 포트로 붙는다.
|
||||
|
||||
### 2.3 seed 페르소나로 띄우기 (선택)
|
||||
|
||||
기본값은 seed 미적재(`AUTO_SEED_PERSONAS=false`)다. 내장 페르소나(P1~P3)를 메모리/DB에 올려서
|
||||
바로 회기를 만들고 싶으면 실행 전에 두 플래그를 켠다.
|
||||
|
||||
```powershell
|
||||
cd apps\api
|
||||
$env:AUTO_SEED_PERSONAS = "true"
|
||||
$env:ALLOW_SEED_PERSONA_FALLBACK = "true"
|
||||
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload
|
||||
```
|
||||
|
||||
`main.py` lifespan은 `settings.auto_seed_personas`가 켜져 있을 때만 `materialize_seed_personas()`를 호출한다.
|
||||
(또는 `apps/api/.env`에 두 키를 `true`로 적어도 된다.)
|
||||
|
||||
### 2.4 DB 없이 degraded 기동 (정상 동작)
|
||||
|
||||
DB 연결이 안 되어도 dev에서는 그대로 기동한다. `main.py` lifespan이 풀 초기화 예외를 잡고
|
||||
`store` 인메모리 폴백으로 degraded 기동하며, 다음 경고를 남긴다.
|
||||
|
||||
```
|
||||
DB 풀 초기화 실패 — store 인메모리 폴백으로 degraded 기동: ...
|
||||
```
|
||||
|
||||
이때 `/health`는 다음과 같이 응답한다(DB·엔진 미가용이면 `status: degraded`).
|
||||
|
||||
```json
|
||||
{ "status": "degraded", "db": false, "engine": false, "environment": "dev", "engine_mode": "claude_cli", ... }
|
||||
```
|
||||
|
||||
> 중요: dev에서 `db: false`는 **버그가 아니라 의도된 폴백**이다. 실제 DB가 필요하면 Postgres16+pgvector를
|
||||
> `DATABASE_URL`이 가리키는 곳(로컬 dev는 `127.0.0.1:55432`)에 띄우면 된다. `ENVIRONMENT`가 dev가 아니면
|
||||
> 폴백하지 않고 그대로 실패한다.
|
||||
|
||||
헬스 체크:
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod http://127.0.0.1:8000/health
|
||||
# 또는
|
||||
curl.exe http://127.0.0.1:8000/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. dev 로그인 (로컬/E2E용 서버 로그인)
|
||||
|
||||
프로덕션 경로는 Google OIDC지만, 로컬에서는 dev-login으로 쿠키 세션을 바로 발급받을 수 있다.
|
||||
조건: `ENVIRONMENT=dev` **그리고** `AUTH_DEV_LOGIN_ENABLED=true`, 그리고 요청 Origin/Host가 로컬이어야 한다
|
||||
(`routes/auth.py`의 `_dev_login_available`). 충족 못 하면 `404 dev login is disabled`.
|
||||
|
||||
- 엔드포인트: `POST /auth/dev-login`
|
||||
- 본문: `{ "email": ..., "role": "learner|teacher|admin", "display_name": ... }`
|
||||
- `role` 기본값은 `learner`.
|
||||
- 성공 시 `__Host-vignette_sid` 쿠키(및 dev 전용 `vignette_sid` 쿠키)를 세팅한다.
|
||||
|
||||
> 주의(이메일 도메인): dev-login도 `validate_google_identity_domain`을 거치므로 **이메일 도메인이
|
||||
> `AUTH_ALLOWED_EMAIL_DOMAINS`에 있어야 한다**. 예: `learner@hs.ac.kr`. 다른 도메인이면
|
||||
> `403 email domain is not allowed`.
|
||||
|
||||
### 3.1 PowerShell(권장) — Invoke-RestMethod + 세션 쿠키
|
||||
|
||||
PowerShell의 `curl`은 `Invoke-WebRequest` 별칭이라 JSON 본문·쿠키 다루기가 번거롭다.
|
||||
PowerShell에서는 `Invoke-RestMethod`가 가장 깔끔하다.
|
||||
|
||||
```powershell
|
||||
$body = '{"email":"learner@hs.ac.kr","role":"learner","display_name":"테스트 학습자"}'
|
||||
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8000/auth/dev-login `
|
||||
-ContentType 'application/json' -Body $body -SessionVariable s
|
||||
|
||||
# 발급된 쿠키로 현재 사용자 확인
|
||||
Invoke-RestMethod -Uri http://127.0.0.1:8000/auth/me -WebSession $s
|
||||
```
|
||||
|
||||
### 3.2 curl.exe(셸 무관) — JSON 본문은 파일로
|
||||
|
||||
Windows에서 따옴표 이스케이프 사고를 피하려면 본문을 파일에 넣고 `--data-binary @file`로 보낸다.
|
||||
(PowerShell에서는 반드시 `curl.exe`라고 적어 별칭이 아닌 실제 curl을 호출한다.)
|
||||
|
||||
```powershell
|
||||
# body.json 작성
|
||||
'{"email":"learner@hs.ac.kr","role":"learner","display_name":"테스트 학습자"}' |
|
||||
Out-File -Encoding ascii body.json
|
||||
|
||||
# 쿠키 저장(-c) + 본문은 파일(@)로
|
||||
curl.exe -i -X POST http://127.0.0.1:8000/auth/dev-login `
|
||||
-H "Content-Type: application/json" --data-binary "@body.json" -c cookies.txt
|
||||
|
||||
# 저장한 쿠키(-b)로 재호출
|
||||
curl.exe http://127.0.0.1:8000/auth/me -b cookies.txt
|
||||
```
|
||||
|
||||
### 3.3 웹 UI
|
||||
|
||||
`apps/web`의 로그인 화면(`Login.tsx`)에 dev-login 경로가 있다. 웹을 띄운 상태(5173)에서
|
||||
프록시를 통해 `/api/auth/dev-login`으로 동일하게 동작한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 웹(프런트엔드) 실행
|
||||
|
||||
```powershell
|
||||
cd apps\web
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- 접속: http://localhost:5173
|
||||
- 프록시: `apps/web/vite.config.ts`가 `/api` → `http://127.0.0.1:8000`로 보내며 **`/api` 프리픽스를 제거**한다.
|
||||
(`changeOrigin: true`, `ws: true`로 쿠키·WebSocket 전달.) 따라서 브라우저의 `/api/auth/me`는 백엔드의
|
||||
`/auth/me`에 도달한다.
|
||||
- API가 8000에서 떠 있어야 프록시가 의미가 있다(2장 먼저 실행).
|
||||
|
||||
기타 웹 스크립트(`apps/web`):
|
||||
|
||||
```powershell
|
||||
npm run typecheck # tsc -b
|
||||
npm run build # tsc -b && vite build
|
||||
npm run e2e # Playwright (web + api + DB 스택 필요)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 엔진 게이트웨이 (AI 턴 생성)
|
||||
|
||||
`ENGINE_MODE=claude_cli`에서는 실제 발화 생성을 `engine_gateway`가 담당한다.
|
||||
이 게이트웨이는 로컬 `claude -p`(Opus 4.8) 상주 프로세스 풀로, **컨테이너 밖(호스트)에서** 9099 포트로 실행한다.
|
||||
회기 1개 = `claude -p` 프로세스 1개로 컨텍스트·프롬프트 캐시를 재사용한다.
|
||||
|
||||
### 5.1 실행
|
||||
|
||||
```powershell
|
||||
cd apps\api
|
||||
python -m uvicorn engine_gateway.gateway:app --host 127.0.0.1 --port 9099
|
||||
```
|
||||
|
||||
관련 환경변수(`gateway.py`):
|
||||
- `CLAUDE_BIN` — claude 실행 파일 경로(기본 `claude`). PATH에 없으면 절대경로 지정.
|
||||
- `ENGINE_MODEL` — 비우면 CLI 기본(Opus 4.8), `ENGINE_FALLBACK_MODEL`, `SESSION_BUDGET_USD` 등.
|
||||
|
||||
### 5.2 헬스/레디 확인
|
||||
|
||||
게이트웨이는 두 가지 점검 엔드포인트를 제공한다.
|
||||
|
||||
- `GET /health` — 얕은 프로세스 liveness. `{"ok": true, "engine": "claude_p", ...}`
|
||||
- `GET /ready` — **실제로 1회 생성**을 돌려 `claude -p` 인증/동작까지 증명(성공 200, 실패 503).
|
||||
설치는 됐지만 로그인 안 된 상태를 여기서 잡는다.
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod http://127.0.0.1:9099/health
|
||||
Invoke-RestMethod http://127.0.0.1:9099/ready
|
||||
```
|
||||
|
||||
API의 `/health`는 게이트웨이의 `/ready`를 호출(404면 `/health`로 폴백)해 `engine` 필드를 채운다
|
||||
(`engine_client.health_detail`). 즉 게이트웨이가 떠 있어도 `claude`가 인증 안 됐으면 `/ready`가 503이라
|
||||
API `/health`의 `engine: false`가 된다.
|
||||
|
||||
### 5.3 프로빙 스크립트 (선택)
|
||||
|
||||
이미 떠 있는 게이트웨이의 스트리밍 TTFT/지연/세션 재사용을 측정한다.
|
||||
|
||||
```powershell
|
||||
python scripts\probe-engine-gateway.py --base-url http://127.0.0.1:9099
|
||||
python scripts\probe-engine-gateway.py --json # 기계 판독용
|
||||
```
|
||||
|
||||
기본 base-url은 `ENGINE_URL` 또는 `http://127.0.0.1:9099`. 이 스크립트는 실제 세션을 만들어 생성 호출을
|
||||
하므로(예산 소모) 자격증명은 넘기지 않고 **이미 떠 있는** 게이트웨이에만 붙는다.
|
||||
|
||||
> 게이트웨이가 없을 때: API `/health`의 `engine: false`이고 **턴 생성은 실패**한다. 다만
|
||||
> UI/네비게이션/로그인/페르소나 조회/세션 생성(in-memory)은 정상 동작한다.
|
||||
|
||||
---
|
||||
|
||||
## 6. (선택) Docker Compose 전체 스택
|
||||
|
||||
DB까지 포함한 통합 실행은 `infra/docker-compose.yml`(db pgvector pg16 + api + web + rag + proxy)을 쓴다.
|
||||
Docker Desktop이 필요하고, `infra/.env`(템플릿: `infra/.env.example`)에 `POSTGRES_PASSWORD`,
|
||||
`APP_DB_PASSWORD`, `SESSION_SECRET`, OAuth/OpenAI 키 등을 채워야 한다.
|
||||
|
||||
```powershell
|
||||
cd infra
|
||||
docker compose up -d db # DB만
|
||||
docker compose up -d # 전체
|
||||
```
|
||||
|
||||
엔진 게이트웨이(9099)는 컴포즈 밖 호스트에서 돌리고, api 컨테이너는
|
||||
`ENGINE_URL=http://host.docker.internal:9099`로 호출한다(compose 기본값).
|
||||
|
||||
---
|
||||
|
||||
## 7. 테스트
|
||||
|
||||
```powershell
|
||||
# 백엔드 (apps/api)
|
||||
cd apps\api
|
||||
python -m pytest app\ -q # 현재 77 pass
|
||||
python -m pytest engine_gateway\ -q # 7 pass
|
||||
|
||||
# 웹 (apps/web)
|
||||
cd apps\web
|
||||
npm run typecheck
|
||||
npm run build
|
||||
npm run e2e # Playwright — web+api+DB 스택 필요
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 트러블슈팅
|
||||
|
||||
- **포트 충돌(8000/5173/9099)**: 점유 프로세스 확인 후 종료.
|
||||
```powershell
|
||||
Get-NetTCPConnection -LocalPort 8000 | Select-Object OwningProcess
|
||||
Stop-Process -Id <PID> -Force
|
||||
```
|
||||
또는 uvicorn `--port`를 바꾼다(웹 프록시는 8000 가정이므로 바꾸면 `vite.config.ts` target도 함께 조정).
|
||||
|
||||
- **`/health`의 `db: false`**: dev에서는 정상 degraded 폴백(인메모리 store). 실제 DB가 필요하면
|
||||
`DATABASE_URL` 대상(로컬 dev `127.0.0.1:55432`)에 Postgres16+pgvector를 띄운다. `ENVIRONMENT`가 dev가
|
||||
아니면 폴백하지 않고 기동이 실패한다.
|
||||
|
||||
- **`/health`의 `engine: false` / 턴 생성 실패**: 게이트웨이(9099)가 안 떠 있거나 `claude` CLI가
|
||||
인증 안 됨. 게이트웨이 `GET /ready`의 `detail`을 보고 원인을 확인한다. 게이트웨이를 먼저 실행하라.
|
||||
|
||||
- **dev-login이 404 (`dev login is disabled`)**: `ENVIRONMENT=dev` + `AUTH_DEV_LOGIN_ENABLED=true`인지,
|
||||
요청이 로컬 Origin/Host인지 확인. `apps/api`에서 uvicorn을 실행해 `.env`가 로드됐는지도 확인.
|
||||
|
||||
- **dev-login이 403 (`email domain is not allowed`)**: 이메일 도메인이
|
||||
`AUTH_ALLOWED_EMAIL_DOMAINS`에 없음. `learner@hs.ac.kr` 같은 허용 도메인을 쓴다.
|
||||
|
||||
- **`/auth/me`가 401**: 쿠키가 전달되지 않음. curl은 `-c`/`-b`로 쿠키를 저장·재사용하고,
|
||||
PowerShell은 `-SessionVariable`/`-WebSession`을 쓴다. 브라우저는 프록시(5173) 경유로 호출해야
|
||||
`__Host-` 쿠키가 동일 출처로 붙는다.
|
||||
|
||||
- **PowerShell에서 `curl`이 이상하게 동작**: PowerShell `curl`은 `Invoke-WebRequest` 별칭이다.
|
||||
실제 curl을 쓰려면 `curl.exe`로 호출하고, JSON 본문은 `--data-binary "@body.json"`처럼 파일로 넘긴다.
|
||||
|
||||
- **한글/공백 경로**: 경로에 공백·한글이 있으면 큰따옴표로 감싼다. `.env`/JSON 파일은 UTF-8(또는 ascii)로 저장.
|
||||
161
docs/guides/source-docs-and-gaps.md
Normal file
161
docs/guides/source-docs-and-gaps.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# 원천문서·갭 로드맵 가이드
|
||||
|
||||
> 한신대 산학협력(구훈정 교수) **원천문서 5종**과 거기서 도출된 **갭(부족분)**의 인덱스/로드맵을 한 장으로 정리한 가이드다.
|
||||
> 갭 항목의 상세 근거는 `docs/ops/source-docs-gap-analysis-2026-06-26.md`에, 상태·결론의 권위 기준(SSOT)은 `docs/dev_dashboard.html`의 **"원천문서 갭 분석"** 섹션에 있다. 이 가이드는 그 두 산출물을 **요약·링크**한다.
|
||||
|
||||
---
|
||||
|
||||
## 0. 이 문서를 읽는 법 (3분)
|
||||
|
||||
- **무엇인가**: Vignette(AI 심리상담 시뮬레이션 훈련 플랫폼)가 산학협력 원천문서가 요구하는 임상 사양 대비 어디가 비어 있는지를 심각도(critical/high/medium+)로 분류한 인덱스다.
|
||||
- **왜 있는가**: 어떤 갭이 "코드만 고치면 되는 것"이고, 어떤 갭이 "임상팀/기관/소유자 결정이 선행돼야 하는 것"인지 구분해 착수 순서를 정하기 위함이다.
|
||||
- **권위 순서(SSOT)**:
|
||||
1. `docs/dev_dashboard.html` → "원천문서 갭 분석 (2026-06-26)" 섹션 = 상태·결론의 단일 진실원천(SSOT).
|
||||
2. `docs/ops/source-docs-gap-analysis-2026-06-26.md` = 그 SSOT 항목의 상세 근거(원천문서 요지·항목별 근거·검증 메모).
|
||||
3. 이 가이드 = 위 둘의 요약 + 빠른 진입 인덱스.
|
||||
- **핵심 R&R 한 줄**: **콘텐츠(워크시트 문항·채점 루브릭·이론 프롬프트·위기 스크립트·골든셋)는 임상팀(구훈정·어유경) 소유**, **기술팀은 "편집 가능한 구조"만 선제 구축**한다. (출처: doc3 회의록 R&R)
|
||||
|
||||
### 원천 산출물 바로가기
|
||||
|
||||
| 산출물 | 경로 | 역할 |
|
||||
|---|---|---|
|
||||
| 갭 상세 분석 | `docs/ops/source-docs-gap-analysis-2026-06-26.md` | 원천문서 요지·항목별 근거·직접검증 메모·판독 한계 |
|
||||
| SSOT 대시보드 | `docs/dev_dashboard.html` (`#` "원천문서 갭 분석" 섹션) | 상태·우선순위의 권위 기준, 갭 요약 카드 |
|
||||
| 데이터 거버넌스 게이트 | `docs/ops/hanshin-data-governance-gate.md` | H4/M3 관련 거버넌스 |
|
||||
| 백로그 | `docs/ops/backlog-2026-06-26.md` | 실행 작업 목록 |
|
||||
| 레이아웃 핸드오프 | `docs/ops/layout-redesign-handoff-2026-06-26.md` | UI 재설계 맥락 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 원천문서 5종 (한신대 산학협력 · 구훈정 교수)
|
||||
|
||||
5종 문서를 PDF로 변환해 시각 정독(에이전트 1/문서)하고 Vignette 코드베이스 4개 도메인을 교차 조사한 뒤, 1차 종합 → 적대적 비평 → 최종 재구성한 결과다(워크플로우 `source-docs-gap-analysis`, 에이전트 12, 토큰 약 1.11M).
|
||||
|
||||
| ID | 문서 | 핵심 함의(Vignette 그라운드 트루스) |
|
||||
|---|---|---|
|
||||
| **doc1** | 청소년·대학생 사례 워크북(3·4장) | 첫 회기 축어록→사례개념화→목표·전략 표준 분석틀 + 두 사례(비자발 청소년/자발 대학생). 페르소나 완성형 스펙, openness 곡선 정량 근거, 위기 프로토콜, 다회기 아크(2~10회기), 회기리뷰 주석 포맷의 기준점. |
|
||||
| **doc2** | 인간중심접근 사례·프로토콜(4장) | 미혼모 3회기 상담의 인간중심 사례개념화 정답지. `theory_mode=humanistic` 1급 필드, 공감·반영 정확도 기반 openness, 회기리뷰 채점 루브릭의 직접 근거. |
|
||||
| **doc3** | 산학협력 회의록(2026-06-08) | 3주체(임상팀 구훈정·어유경 / 트웬티온스 / 사업단 김시윤) 협력·거버넌스 확정. **R&R: 콘텐츠·평가기준=임상팀 소유, 구현=기술팀**. 평가 루브릭은 임상팀이 외부 정의·수정 가능해야 함. AI API 비용을 운영 리스크로 명시. |
|
||||
| **doc4** | 산학협력 신청서(공식 계약) | 기간(2026.5.18~9.30, 20주)·예산·평가지표·산출물·서약의 권위 원천. **humanistic+CBT 2종 필수 이론을 '단계적 프롬프트 체인'으로 계약**, 3척도 사전사후·20명 실험/통제군·단회기 50분, 9월 저작권 등재, IRB 100% 준수. 기술스택 명시(Spring Boot 3/Node.js·TimescaleDB — **실제 FastAPI/Python과 불일치**). |
|
||||
| **doc5** | 사례개념화 워크북(3장, 상호작용 분석 추가본) | 비자발 청소년(자살사고) 축어록(상1~63)에 기법 코딩. 회기리뷰 루브릭·기법 코딩 택소노미(κ/ICC 스킴)·페르소나 사양·종결 규칙의 직접 근거. |
|
||||
|
||||
> **주의(doc4 KPI 범위)**: κ/ICC·환각률은 **신청서 본문에 미명시**('정량 신뢰도 확보' 수준). 계약 확정 지표로 단정하지 말 것 — 별도 평가설계 문서로 확정 필요.
|
||||
|
||||
---
|
||||
|
||||
## 2. 갭 로드맵 — 심각도 우선순위
|
||||
|
||||
심각도 분류: **critical 3 / high 4 / medium+ 6**. `✓`는 작성자가 코드 grep으로 직접 재확인한 항목, `(분석)`은 분석 결과로 착수 전 코드 1차 재확인 권고.
|
||||
|
||||
### Critical (3)
|
||||
|
||||
| ID | 갭 | 현재상태 | 권고 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| **C1** | 사례개념화·치료계획 산출물 구조 전면 부재 | `cognitive_triad/case_conceptual/인지삼제/4사분면/quadrant` grep **0건** ✓. CCD는 숨은 정답키일 뿐 학습자 산출물 폼이 아님. | 워크시트 스키마(11탐색항목·호소 5영역·인지삼제·1·2차감정·보호/방해 4사분면·생물심리사회 목표) 입력 폼 + AI 축어록 초안 추출 + 규칙 기반 채점. 루브릭은 임상팀 외부 정의 가능하게 외부화. | doc1·doc4·doc5 |
|
||||
| **C2** | 위기개입 프로토콜·생명유지서약·에스컬레이션 미구현 | `escalate=True` 시 client stream 이벤트(`StreamEvent("safety")`)만, `safety_events` DB 적재·교수자 알림 코드 **없음** ✓. `prepare_turn`이 risk_level을 상태머신 `ideation_observed`로 미전달. | 위기 분기 상태(예외고지→단계적 탐색→서약)를 상태머신에 추가, escalate 시 safety_events insert+교수자 알림, 위기탐색 누락 시 회기리뷰 감점, ideation_observed 전달. | doc1·doc5(핵심 시나리오)·doc4(IRB 전제) |
|
||||
| **C3** | 이론모드 미주입 + CBT 콘텐츠 자체 부재 | `build_turn_messages` 시그니처에 `theory` 인자 **없음** + `apps/web/src/pages/Session.tsx:589` `sessionApi.start(personaCode, "humanistic")` 하드코딩 ✓. CBT 프롬프트 체인·이론부합 루브릭 전무. | theory 인자 + 이론별 단계 프롬프트 체인(공감·반영 / 인지재구조화·행동활성화) + 프론트 이론 선택 UI + 이론 부합도 루브릭. | doc4(humanistic+CBT 필수)·doc2/doc5 |
|
||||
|
||||
### High (4)
|
||||
|
||||
| ID | 갭 | 현재상태 | 권고 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| **H1** | 계약 평가 KPI(자기효능감·기술숙련도·수련만족도 사전사후) 수집·집계 전무 | `자기효능감/사전사후/수련만족/실험통제군` grep 0건. Phase3 KPI도 report shape만, 계산 코드 0줄. (분석) | 3척도 pre-post 폼·실험/통제군 배정·자동누적 대시보드·추이 시각화·검정 계산 코드. (κ/ICC·환각률은 doc4 미명시 → 평가설계 확정.) | doc4(20명 실험/통제군·단회기 50분·3척도 pre-post) |
|
||||
| **H2** | 턴별 회기 리뷰(상호작용 분석 2열) 미가동 | `make_eval_hook`은 `apps/api/app/services/evaluator.py:738`에 정의·export됐으나 `routes/`에서 주입 **0건** → 턴별 fast-loop 미동작(회기종료 deep-loop만) ✓. few-shot 골든셋 기본 OFF. | eval_hook을 turn 파이프라인에 주입. 회기리뷰 UI 좌(축어록 타임라인+비언어)/우(기법·적절성·대안반응·이론) 2열. 원천 축어록을 채점 few-shot 골든셋으로 적재. | doc2·doc5(골드 포맷) |
|
||||
| **H3** | 임상팀 콘텐츠 입력 경로(페르소나 저작 CRUD) 부재 + P4~P7 미적재 | personas 라우트에 검수 승인/반려만, draft 생성·편집 API 없음. `persona_repository.py`는 in-code `SEED_PERSONAS`(P1~P3)만 materialize, 외부 JSON 미로드 ✓. | 페르소나 저작 CRUD(draft→review) + P4~P7 적재, 루브릭·이론 콘텐츠를 임상팀 편집 가능 데이터로 외부화. | doc3(R&R)·doc4(페르소나=전문가 산출물) |
|
||||
| **H4** | PII 마스킹 한국어 공백 + 외부전송 관측 부재 | Presidio `language='en'` 고정으로 한국어 이름/주소/기관 미탐지(폴백은 번호·이메일만). `audit.llm_call_log` 0행(런타임 미관측). `consent_at` 컬럼만, 동의 수집/게이트/철회 엔드포인트 전무. (분석) | 한국어 PII 탐지 추가, 마스킹 미들웨어 하드게이트화, llm_call_log 적재로 런타임 관측, 미성년/guardian 동의 수집·게이트·철회. | doc1/2/5(실명·날짜·미성년·자살시도 다수)·doc4(IRB·개인정보) |
|
||||
|
||||
### Medium+ (6) — M1~M3, X1~X2, L1
|
||||
|
||||
| ID | 갭 | 현재상태 | 권고 |
|
||||
|---|---|---|---|
|
||||
| **M1** | 비언어/준언어 임상 이벤트 캡처·태깅 부재 | `voice.py`는 EOT용 `silence_ms`만, 침묵·한숨·울음 타임스탬프 이벤트 캡처·리뷰 표시 전무. 서버 RMS 힌트 프론트 미사용(dead). (분석) | 침묵·한숨·울음을 타임스탬프 메타 이벤트로 보존·시각화, '침묵 견디기'를 역량 지표화, 페르소나 의도적 침묵·비유창 한국어 렌더링. |
|
||||
| **M2** | 다회기 종단 케이스 아크·교차회기 사례개념화 미구동 | `case_state.ccd_estimate/presenting_arc/alliance_level` 스키마만, `build_recall_context()` 빈 컨텍스트 반환. 음성 경로 빈 RecallContext. (분석) | case_state 런타임 구동, build_recall_context 실제 회상/pinned_fact 주입, 접수면접→다회기 연속성·자기개념 진화. |
|
||||
| **M3** | SSO claim 매핑·식별자 안정성·deprovisioning 감사 미연결 | Google/SAML 콜백 `cohort_ids=[]` 하드코딩, role이 email allowlist, external_id가 `email:{}` 파생(불안정). SAML 서명검증 미구현. role변경/삭제 audit 미기록. (분석) | claim→role/cohort/institution_user_id 매핑+안정 식별자(sub/NameID), SAML 서명검증, deprovisioning audit, 한신 IdP 확정·외부 증거 수급. |
|
||||
| **X1** | 재귀학습·데이터셋 export 파이프라인 미구현 | `ds.*` 스키마(κ/ICC 컬럼)만, read/write 코드 0건. JSONL export·IAA 게이트·골든셋 승격 미코딩. (분석) | JSONL export 잡, IAA 게이트(κ≥0.6/ICC≥0.75), 골든셋 승격. |
|
||||
| **X2** | AI API 비용 관측 부재 | 게이트웨이 토큰 텔레메트리 0 고정, cost 모니터링·캐싱 부재. (분석) | 토큰 텔레메트리 실측, 저비용 모델 분기·캐싱, 비용 대시보드. |
|
||||
| **L1** | 기술스택 신청서-구현 불일치 및 단기일정 산출물 압박 | doc4 신청서 스택(Spring Boot 3/Node.js·TimescaleDB) vs 실제 FastAPI/Python 불일치, 20주 단기일정·9월 저작권 등재 압박. | 스택 정합 또는 변경 사유를 거버넌스 회의록으로, 9월 저작권 등재 문서화 수준을 일정 반영. |
|
||||
|
||||
> X2 근거: doc3 회의록이 'AI API 비용'을 운영 리스크로 명시.
|
||||
|
||||
---
|
||||
|
||||
## 3. R&R — 누가 무엇을 소유하는가
|
||||
|
||||
원칙(doc3 회의록): **콘텐츠는 임상팀 소유, 코드는 "편집 가능 구조"만 선제 구축.** 코드와 콘텐츠를 한 PR에 섞지 말 것 — 구조(스키마·폼·주입 경로)는 기술팀이 먼저 만들되, 그 안에 들어갈 임상 문안은 임상팀이 데이터로 채운다.
|
||||
|
||||
### A. 즉시 착수 가능 (내부 코드, 외부 합의 불요)
|
||||
|
||||
- **C2 배선**: escalate 시 `safety_events` insert + `ideation_observed` 전달.
|
||||
- **C3 골격**: `theory` 인자 추가 + `Session.tsx:589` 하드코딩 제거 + 프론트 이론 선택 UI 골격.
|
||||
- **H2 배선**: `make_eval_hook`을 turn 파이프라인에 주입(이미 정의·export됨, `evaluator.py:738`).
|
||||
- **H3**: 페르소나 저작 CRUD(draft→review) + P4~P7 로드 경로.
|
||||
- **H4(부분)·M1(부분)·M2·X1·X2**: 마스킹 한국어 설정·하드게이트, 비언어 메타 이벤트 보존, `build_recall_context` 구현, `ds.*` read/write·export, 토큰 텔레메트리.
|
||||
|
||||
### B. 소유자 결정 / 외부(임상팀·기관) 의존
|
||||
|
||||
- **임상팀(구훈정·어유경) 산출물**: C1 워크시트 항목·채점 루브릭, C3 CBT 이론 콘텐츠·프롬프트 체인, C2 위기 스크립트·서약 문안, H2 골든셋 코딩. → 코드는 '편집 가능 구조'만 선제 구축.
|
||||
- **소유자 평가설계 결정**: H1 실험/통제군 배정·3척도 문항·50분 흐름. κ/ICC·환각률 목표는 doc4 미명시 → 평가설계 문서 확정.
|
||||
- **기관(한신 IT) 의존**: M3 SSO IdP 프로토콜·test tenant·claim 스키마, SAML 인증서, 거버넌스 증거.
|
||||
- **거버넌스 결정**: L1 스택 정합성 처리 방향, 저작권 등재 문서화 수준, IP 협의.
|
||||
|
||||
---
|
||||
|
||||
## 4. 작업 시작 절차 (체크리스트)
|
||||
|
||||
특정 갭(예: C2)에 착수하기 전에:
|
||||
|
||||
1. **SSOT 확인**: `docs/dev_dashboard.html`의 "원천문서 갭 분석" 섹션에서 해당 항목의 최신 상태/우선순위를 본다.
|
||||
2. **상세 근거 확인**: `docs/ops/source-docs-gap-analysis-2026-06-26.md`의 동일 ID 항목에서 근거·권고·소유 구분(A/B)을 본다.
|
||||
3. **코드 1차 재확인**: `(분석)` 표기 항목은 착수 전 grep으로 현재상태를 직접 재확인한다(아래 예시).
|
||||
4. **소유 구분 판정**: 콘텐츠가 필요한가? → 임상팀 핸드오프 선행. 구조/배선만인가? → 즉시 착수(섹션 3-A).
|
||||
5. **검증 게이트 통과**: 변경 후 표준 테스트를 돌린다(아래 명령).
|
||||
|
||||
### 코드 재확인 grep 예시 (저장소 루트 `D:/workspace/vignette`)
|
||||
|
||||
```powershell
|
||||
# C1: 사례개념화 구조 존재 여부 (현재 0건이어야 함)
|
||||
rg -n "cognitive_triad|case_conceptual|인지삼제|4사분면|quadrant" apps
|
||||
|
||||
# C2: escalate 시 safety_events 적재 여부
|
||||
rg -n "safety_events|escalate|ideation_observed" apps/api/app/services/orchestrator.py
|
||||
|
||||
# C3: 이론모드 하드코딩 위치
|
||||
rg -n "humanistic" apps/web/src/pages/Session.tsx apps/api/app/services/persona.py
|
||||
|
||||
# H2: eval_hook 정의 vs 주입
|
||||
rg -n "make_eval_hook" apps/api/app
|
||||
|
||||
# H3: 페르소나 SEED(P1~P3) 한정 여부
|
||||
rg -n "SEED_PERSONAS" apps/api/app/persona_repository.py
|
||||
```
|
||||
|
||||
### 검증 명령 (변경 후)
|
||||
|
||||
```powershell
|
||||
# 백엔드 (작업 디렉터리 apps/api)
|
||||
python -m pytest app/ -q # 현재 77 pass
|
||||
python -m pytest engine_gateway/ -q # 현재 7 pass
|
||||
|
||||
# 프론트 (작업 디렉터리 apps/web)
|
||||
npm run typecheck
|
||||
npm run build # vite
|
||||
npm run e2e # Playwright — web+api+DB 스택 필요
|
||||
```
|
||||
|
||||
> 로컬 기동: API는 `apps/api`에서 `python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload`(pydantic-settings가 `apps/api/.env` 자동 로드, DB 미가용 시 in-memory degraded 폴백). 웹은 `apps/web`에서 `npm run dev`(http://localhost:5173, `/api`→127.0.0.1:8000 프록시). AI 턴 생성은 `ENGINE_MODE=claude_cli`일 때 engine_gateway가 포트 9099에 떠 있어야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 판독 한계 (정직성)
|
||||
|
||||
- 핵심 축어록 2단 표(doc5 상1~63, doc2 3회기 표)는 txt 추출 시 일부 누락 — **PDF 시각 판독이 1차 근거**.
|
||||
- OCR 잔재·오탈자 존재(doc1·doc5). 의미는 시각 보정했으나 일부 표현 불확실.
|
||||
- **doc4 κ/ICC·환각률은 신청서 본문 미명시** — 계약 확정 지표로 단정 금지.
|
||||
- doc4/doc3 행정 불일치(참여교수 1명 vs 2명, 서식 연도 '2025' 오기, 연구책임자 표기 불일치, 트웬티온스 성명 공란).
|
||||
- '현재상태'는 grep/코드 사실 기반 적대적 비평 인용. **C1·C2·C3·H2·H3은 작성자 직접 재확인(✓)**, 그 외(H1·H4·M1~M3·X1·X2·L1)는 착수 전 코드 1차 재확인 권고.
|
||||
|
||||
---
|
||||
|
||||
### 변경 이력
|
||||
|
||||
- 2026-06-26: 초판. `docs/ops/source-docs-gap-analysis-2026-06-26.md`와 SSOT 대시보드 "원천문서 갭 분석" 섹션을 요약·인덱스화.
|
||||
271
docs/guides/testing.md
Normal file
271
docs/guides/testing.md
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
# 테스트·검증 가이드
|
||||
|
||||
Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타입체크/빌드, Playwright E2E)을
|
||||
"무엇을 어떻게 실행하고, 어떤 의존이 필요한가" 기준으로 정리한다. 이 문서는 실제
|
||||
`apps/web/package.json`, `apps/web/playwright.config.ts`, `apps/web/e2e/`,
|
||||
`apps/api/app/test_*.py`, `apps/api/engine_gateway/test_*.py`,
|
||||
`infra/docker-compose.yml`을 읽고 작성했으며, 명령·경로는 그대로 따라 할 수 있다.
|
||||
|
||||
주 환경은 Windows 11 + PowerShell이다. 아래 명령은 셸 공통(`python -m ...`, `npm run ...`)
|
||||
형태로 적었고, 환경변수 지정은 PowerShell/Bash 양쪽 예시를 병기한다.
|
||||
|
||||
---
|
||||
|
||||
## 0. 한눈에 보는 검증 매트릭스
|
||||
|
||||
| 검증 | 작업 디렉터리 | 명령 | DB | API(8000) | 웹(5173) | 엔진GW(9099) | 브라우저 | 현재 통과 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 77 pass |
|
||||
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 7 pass |
|
||||
| 웹 타입체크 | `apps/web` | `npm run typecheck` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | — |
|
||||
| 웹 빌드 | `apps/web` | `npm run build` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | — |
|
||||
| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 102 tests / 17 files |
|
||||
|
||||
핵심 원칙: **단위 테스트(pytest)와 타입체크/빌드는 외부 서비스 없이 단독 실행된다.**
|
||||
**E2E만 풀스택(DB+API+웹+브라우저)을 요구한다.** 아래 각 절에서 근거와 절차를 설명한다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 백엔드 단위 테스트 (pytest)
|
||||
|
||||
### 1.1 대상과 구성
|
||||
|
||||
- 작업 디렉터리: `apps/api`
|
||||
- 테스트는 FastAPI `TestClient`와 인메모리 `store.py` 폴백으로 돌기 때문에 **Postgres/엔진
|
||||
게이트웨이가 없어도 통과한다.** 별도 `pytest.ini`/`pyproject.toml` 설정은 없고 기본 수집
|
||||
규칙(`test_*.py`)을 그대로 쓴다.
|
||||
- `pydantic-settings`가 `apps/api/.env`를 자동 로드한다(있으면). 단위 테스트는 `.env` 없이도
|
||||
돈다.
|
||||
|
||||
### 1.2 실행
|
||||
|
||||
```sh
|
||||
# apps/api
|
||||
python -m pytest app/ -q # 앱 단위 테스트 (현재 77 pass)
|
||||
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트 (현재 7 pass)
|
||||
```
|
||||
|
||||
수집만 빠르게 확인하려면:
|
||||
|
||||
```sh
|
||||
python -m pytest app/ --collect-only -q # → "77 tests collected"
|
||||
python -m pytest engine_gateway/ --collect-only -q # → "7 tests collected"
|
||||
```
|
||||
|
||||
> 참고: 실행 중 `PendingDeprecationWarning: Please use 'import python_multipart'`
|
||||
> 경고가 보일 수 있으나 무해하며 통과 결과에 영향을 주지 않는다.
|
||||
|
||||
### 1.3 `app/` 테스트 파일 (보안·상태머신·평가 중심)
|
||||
|
||||
| 파일 | 검증 영역 |
|
||||
|---|---|
|
||||
| `app/test_runtime_policy.py` | 런타임 정책(공개/dev 모드, dev-login 가드) |
|
||||
| `app/test_session_turn_persistence.py` | 세션 턴 영속화(DB/인메모리 양쪽) |
|
||||
| `app/test_orchestrator_masking.py` | 오케스트레이터 PII 마스킹 게이트 |
|
||||
| `app/test_state_machine_resistance.py` | 저항엔진 상태머신(openness 전이) |
|
||||
| `app/test_rbac_idor.py` | RBAC / IDOR 권한 경계 |
|
||||
| `app/test_auth_providers.py` | 인증 프로바이더(SSO/allowlist) |
|
||||
| `app/test_persona_review.py` | 페르소나 리뷰 워크플로 |
|
||||
| `app/test_voice_service.py` | 음성 캐스케이드 서비스(STT/TTS) |
|
||||
| `app/test_voice_ws.py` | 음성 WebSocket 경계 |
|
||||
|
||||
### 1.4 `engine_gateway/` 테스트
|
||||
|
||||
| 파일 | 검증 영역 |
|
||||
|---|---|
|
||||
| `engine_gateway/test_gateway_model.py` | 게이트웨이 모델 선택·요청/응답 계약(ENGINE_MODE별) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 웹 타입체크 / 빌드
|
||||
|
||||
### 2.1 대상과 구성
|
||||
|
||||
- 작업 디렉터리: `apps/web`
|
||||
- `package.json` 스크립트(실측):
|
||||
- `typecheck` → `tsc -b`
|
||||
- `lint` → `tsc -b` (현재 lint는 타입체크와 동일)
|
||||
- `build` → `tsc -b && vite build`
|
||||
- `dev` → `vite`
|
||||
- `e2e` → `playwright test`
|
||||
- 타입체크/빌드는 **순수 정적 검사**라 DB·API·브라우저가 필요 없다.
|
||||
|
||||
### 2.2 실행
|
||||
|
||||
```sh
|
||||
# apps/web
|
||||
npm install # 최초 1회 (devDependencies: typescript, vite, @playwright/test 등)
|
||||
npm run typecheck # tsc -b — 타입 오류 0 확인
|
||||
npm run build # tsc -b && vite build — 프로덕션 번들 생성까지 확인
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Playwright E2E (풀스택)
|
||||
|
||||
### 3.1 의존성 — 왜 풀스택인가
|
||||
|
||||
E2E는 Playwright route fixture로 앱 데이터를 대체하지 않고, **Vite `/api` 프록시를 통해 실제
|
||||
로컬 FastAPI를 호출한다**(`apps/web/e2e/README.md`). 특히 공용 헬퍼
|
||||
`apps/web/e2e/support.ts`의 `fetchAvailablePersonas()`는
|
||||
`source === "database" && !degraded` 페르소나만 사용 가능으로 간주한다.
|
||||
|
||||
```ts
|
||||
// support.ts
|
||||
const usable = personas.filter((p) => p.source === "database" && !p.degraded);
|
||||
expect(usable.length, ...).toBeGreaterThan(0);
|
||||
```
|
||||
|
||||
즉 **인메모리 degraded 페르소나로는 대부분의 E2E가 통과하지 못한다.** 따라서 E2E는:
|
||||
|
||||
1. **Postgres(pgvector pg16)** — 실제 DB가 떠 있어야 함
|
||||
2. **시드 페르소나** — `AUTO_SEED_PERSONAS=true`(필요 시 `ALLOW_SEED_PERSONA_FALLBACK=true`)
|
||||
3. **FastAPI** — `127.0.0.1:8000`에서 수동 기동
|
||||
4. **Vite 웹 서버** — Playwright가 자동 기동(아래 3.3)
|
||||
5. **Chromium** — `npx playwright install chromium`로 1회 설치
|
||||
6. **엔진 게이트웨이(9099)** — **일부 테스트만** 필요(3.5 참고). 레이아웃/시각 게이트는 불필요.
|
||||
|
||||
### 3.2 사전 준비
|
||||
|
||||
```sh
|
||||
# (1) 브라우저 바이너리 설치 — 최초 1회
|
||||
cd apps/web
|
||||
npx playwright install chromium
|
||||
|
||||
# (2) DB 기동 — Docker Desktop 필요. infra/docker-compose.yml의 db 서비스
|
||||
# (pgvector/pgvector:pg16). 전체 스택을 띄우려면:
|
||||
# docker compose -f infra/docker-compose.yml up -d db
|
||||
```
|
||||
|
||||
### 3.3 API 서버 기동 (시드 포함)
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
# apps/api
|
||||
$env:AUTH_DEV_LOGIN_ENABLED = "true" # dev-login 허용 (support.ts가 사용)
|
||||
$env:ENVIRONMENT = "dev"
|
||||
$env:AUTO_SEED_PERSONAS = "true" # DB에 SEED P1~P3 적재
|
||||
$env:ALLOW_SEED_PERSONA_FALLBACK = "true"
|
||||
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
```
|
||||
|
||||
Bash:
|
||||
|
||||
```sh
|
||||
cd apps/api
|
||||
AUTH_DEV_LOGIN_ENABLED=true ENVIRONMENT=dev AUTO_SEED_PERSONAS=true \
|
||||
ALLOW_SEED_PERSONA_FALLBACK=true \
|
||||
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
```
|
||||
|
||||
> `.env`에 동일 값을 넣어두면 매번 환경변수를 줄 필요가 없다. DB 연결이 실패하면 main.py
|
||||
> lifespan이 인메모리 degraded로 기동되지만(`/health` → `{"status":"degraded","db":false}`),
|
||||
> 이 상태에서는 E2E 페르소나 헬퍼가 실패하므로 **E2E용으로는 반드시 DB를 붙여야 한다.**
|
||||
|
||||
### 3.4 E2E 실행
|
||||
|
||||
웹 서버는 Playwright `webServer`가 자동으로 띄운다(`PLAYWRIGHT_BASE_URL` 미설정이고
|
||||
`PLAYWRIGHT_SKIP_WEB_SERVER`가 없을 때, `npm run dev -- --host localhost --port 5173`).
|
||||
|
||||
```sh
|
||||
# apps/web — API(8000)와 DB가 떠 있는 상태에서
|
||||
npm run e2e # = playwright test (전체)
|
||||
npm run e2e:headed # 브라우저 표시
|
||||
npm run e2e:ui # Playwright UI 모드
|
||||
```
|
||||
|
||||
특정 스펙/그룹만:
|
||||
|
||||
```sh
|
||||
npx playwright test e2e/session-layout.spec.ts
|
||||
npx playwright test --grep "@single-run" # 직렬 실행 시나리오만
|
||||
npx playwright test --grep-invert "@single-run" # 병렬 시나리오만
|
||||
npx playwright test --list # 실행하지 않고 목록·개수만
|
||||
```
|
||||
|
||||
유용한 오버라이드(`playwright.config.ts` 실측):
|
||||
|
||||
```sh
|
||||
PLAYWRIGHT_PORT=5174 npm run e2e # 웹 포트 변경
|
||||
PLAYWRIGHT_BASE_URL=http://localhost:5173 npm run e2e # 외부에 이미 뜬 웹 사용(자동기동 끔)
|
||||
VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API 직접 지정
|
||||
```
|
||||
|
||||
### 3.5 프로젝트(브라우저 프로파일) 구성
|
||||
|
||||
`playwright.config.ts`는 `testDir: ./e2e`, `timeout: 30s`, `expect.timeout: 5s`,
|
||||
`fullyParallel: true`로 다음 프로젝트를 정의한다.
|
||||
|
||||
| 프로젝트 | 뷰포트/디바이스 | 대상 grep |
|
||||
|---|---|---|
|
||||
| `chromium-desktop` | 1440×900 | `@single-run`·`@public-auth` 제외 전부 |
|
||||
| `chromium-mobile` | Pixel 5 | `@single-run`·`@public-auth` 제외 전부 |
|
||||
| `chromium-single-run` | 1280×800 | `@single-run`만(직렬) |
|
||||
| `chromium-public-auth` | 1440×900 | `E2E_PUBLIC_AUTH=1`일 때만 활성, 공개 사이트 대상 |
|
||||
|
||||
- CI(`process.env.CI`)에서는 `retries: 2`, `workers: 1`, `list`+`html` 리포터를 쓴다.
|
||||
- 실패 시 trace(첫 재시도)·스크린샷·비디오를 `node_modules/.tmp/`에 남긴다.
|
||||
|
||||
### 3.6 실측 테스트 개수 (현재)
|
||||
|
||||
`npx playwright test --list` 기준 **총 102 tests / 17 files**.
|
||||
|
||||
- **병렬 시나리오**: 88 tests (desktop + mobile)
|
||||
- **`@single-run` 직렬 시나리오**: 14 tests (DB 영속화·세션 MVP·음성 성공경로 등)
|
||||
|
||||
레이아웃·시각 회귀 게이트(핵심 합격선):
|
||||
|
||||
| 게이트 | 스펙 | 구성 | 개수 |
|
||||
|---|---|---|---|
|
||||
| 세션 레이아웃 | `e2e/session-layout.spec.ts` | 4 테스트 × (desktop+mobile) | **8 / 8** |
|
||||
| 시각 레이아웃 게이트 | `e2e/layout-visual-gate.spec.ts` | `@single-run`, 7개 화면 × 7개 폭 검사 | **7 / 7** |
|
||||
| 레이아웃 포커스(재설계 화면) | `session-layout`·`session-review`·`admin`·`learner`·`settings`·`teacher`, `@single-run` 제외 | desktop+mobile 병렬 | **54** |
|
||||
|
||||
> `layout-visual-gate`는 7개 폭(390/720/861/900/1024/1280/1440)에서 가로 오버플로·잘린
|
||||
> 컨트롤을 검사하고 전체 페이지 스크린샷을 `node_modules/.tmp/layout-gate/`에 남긴다.
|
||||
> `session-layout`은 회기 전/활성 화면이 뷰포트를 벗어나지 않는지, 우측 패널이 코어 영역을
|
||||
> 침범하지 않는지, 스트림 실패 시 미저장 전사가 남지 않는지를 검증한다.
|
||||
|
||||
### 3.7 공개 인증 스모크(선택)
|
||||
|
||||
`e2e/public-auth-turn.spec.ts`는 공개 사이트(`https://vignette.chanpaca.net`)를 직접 타격하는
|
||||
옵트인 스모크로, 로컬 웹 서버를 띄우지 않는다. 절차는 `apps/web/e2e/README.md` 참고
|
||||
(`E2E_PUBLIC_AUTH=1`, `npx playwright codegen ... --save-storage`로 인증 상태 캡처 후
|
||||
`E2E_PUBLIC_STORAGE_STATE` 재사용). 캡처한 storage state에는 API 세션 쿠키가 들어 있으니
|
||||
민감 정보로 취급한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 전체 검증 순서 권장안 (로컬)
|
||||
|
||||
```sh
|
||||
# 1) 외부 의존 없는 빠른 검증부터
|
||||
cd apps/api && python -m pytest app/ -q && python -m pytest engine_gateway/ -q
|
||||
cd apps/web && npm run typecheck && npm run build
|
||||
|
||||
# 2) 풀스택 E2E (DB+API 준비 후)
|
||||
# 터미널 A: docker compose -f infra/docker-compose.yml up -d db
|
||||
# 터미널 B: cd apps/api && (3.3의 env) python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
# 터미널 C:
|
||||
cd apps/web && npm run e2e
|
||||
```
|
||||
|
||||
엔진 턴 생성까지 보려면(음성/세션 MVP 등 일부 `@single-run`) 별도로 엔진 게이트웨이를
|
||||
포트 9099에 띄운다(`ENGINE_MODE=claude_cli`). 게이트웨이가 없으면 `/health`의 `engine:false`
|
||||
이고, 실제 턴 생성 시나리오는 실패한다. 레이아웃/시각 게이트는 게이트웨이 없이도 통과한다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 운영 원칙 — "가짜 DONE" 금지
|
||||
|
||||
검증 결과는 **실제로 실행해 통과한 명령**만 근거로 보고한다.
|
||||
|
||||
- "통과했다/완료다"라고 말하려면 그 자리에서 **실행 가능한 검증 명령과 관측된 결과(통과 개수)**를
|
||||
함께 제시한다. 예: `python -m pytest app/ -q` → `77 passed`.
|
||||
- 의존을 갖춘 검증을 우회하지 않는다. E2E를 DB/API 없이 돌려 놓고 "그린"이라고 보고하지
|
||||
않는다. 풀스택을 못 띄웠으면 **"E2E 미실행"**이라고 명시한다.
|
||||
- 인메모리 degraded 기동(`db:false`)에서 페르소나 헬퍼가 실패하는 것은 환경 문제이지 코드가
|
||||
통과한 것이 아니다. 환경을 고친 뒤 재실행한 결과로만 판단한다.
|
||||
- 수집(`--collect-only`)·목록(`--list`)은 "있다"는 근거일 뿐 "통과했다"는 근거가 아니다.
|
||||
통과 주장은 실제 실행 출력으로 뒷받침한다.
|
||||
Loading…
Add table
Add a link
Reference in a new issue