feat: 운영 안정성과 세션 음성 경험 개선

This commit is contained in:
Yun Chan 2026-07-31 00:13:08 +09:00
parent facc4ad2d9
commit c788343467
95 changed files with 8431 additions and 1785 deletions

View file

@ -15,20 +15,22 @@ docker compose up -d
## 2. 핵심 — "엔진"만 컨테이너 밖 (이식성의 열쇠)
우리 엔진은 로컬 `claude -p`(네 PC의 claude CLI/OAuth에 묶임)라, 컨테이너에 가두면 못 옮긴다. 그래서 **엔진은 어댑터 뒤에 두고 `ENGINE_URL`/`ENGINE_MODE`로 가리킨다.** 옮길 때 이 두 줄만 바꾸면 됨 — 코드 무수정.
AI provider는 모두 호스트 엔진 게이트웨이 뒤에 둔다. 게이트웨이가 Claude CLI·Anthropic API·Codex CLI·Agy CLI의 모델 탐색과 실행을 맡고, 컨테이너 API는 **`ENGINE_URL`/`ENGINE_MODE`로 게이트웨이와 provider를 선택한다.** CLI는 호스트 인증을 재사용하고 API 키는 게이트웨이 호스트에만 주입한다.
설정 경로 **3가지(사용자 선택)**:
1. **yml / .env**`infra/.env``ENGINE_URL`·`ENGINE_MODE` (기본·운영)
2. **docker compose**`docker compose run -e ENGINE_MODE=messages_api ...` 즉석 오버라이드
3. **설정 페이지(관리자 UI)** — 런타임에 엔진/모델 전환 (DB에 저장, 재기동 불필요)
2. **docker compose**`docker compose run -e ENGINE_MODE=claude_api ...` 즉석 오버라이드
3. **설정 페이지(관리자 UI)** — 런타임에 provider·연결 주소·사용 가능 모델·추론 강도를 선택한다. 게이트웨이가 확인하지 못한 조합은 저장하지 않는다.
| 시나리오 | ENGINE_MODE | ENGINE_URL |
|---|---|---|
| **네 PC 운영** (요구사항: 로컬 claude -p) | `claude_p` | `http://host.docker.internal:9099` (호스트의 claude -p 게이트웨이) |
| **클라우드/타호스트 이전** | `messages_api` | (불필요, ANTHROPIC_API_KEY 사용) |
| **원격 엔진 PC** | `claude_p` | `http://<엔진PC-IP>:9099` |
| **네 PC 운영** (Claude CLI) | `claude_cli` | `http://host.docker.internal:9099` |
| **네 PC 운영** (Codex CLI, 기본 Terra/Medium) | `codex_cli` | `http://host.docker.internal:9099` |
| **네 PC 운영** (Agy CLI, 기본 Gemini 3.6 Flash/High) | `agy_cli` | `http://host.docker.internal:9099` |
| **클라우드/타호스트 이전** | `claude_api` | `http://host.docker.internal:9099` (`ANTHROPIC_API_KEY`는 게이트웨이 호스트에 주입) |
| **원격 엔진 PC** | 위 provider 중 설치·인증된 항목 | `http://<엔진PC-IP>:9099` |
> 로컬 claude -p 게이트웨이 = 호스트에서 도는 작은 프로세스(상주 멀티턴 풀, `claude -p --input-format stream-json`). 컨테이너 api 가 `host.docker.internal:9099`로 호출. 이 게이트웨이는 `apps/api/engine_gateway/`에 둔다(호스트 실행, 컨테이너 밖).
> 호스트 게이트웨이는 `apps/api/engine_gateway/`에 있으며 컨테이너 밖에서 실행한다. Claude CLI는 상주 멀티턴 풀을, Codex/Agy는 각 CLI를, `claude_api`는 Anthropic API를 호출한다. 관리자 UI의 모델 목록은 이 게이트웨이의 `GET /v1/capabilities`가 소유한다.
## 3. 환경변수 (.env)
@ -39,8 +41,8 @@ docker compose up -d
| `AUTH_ALLOWED_EMAIL_DOMAINS` | Google 로그인 허용 이메일 도메인(JSON 배열). 기본 `["hs.ac.kr","twentyoz.kr"]`. Google Console authorized domains가 아니라 서버에서 `email`/`hd` claim으로 강제 |
| `FRONTEND_ORIGIN_MAP` | API callback host → frontend origin 매핑(JSON 객체). 기본 `api-vignette.chanpaca.net → vignette.chanpaca.net`, `api-vnet.18ka.net → vnet.18ka.net` |
| `OPENAI_API_KEY` | 음성(STT/TTS) |
| `ANTHROPIC_API_KEY` | 엔진 폴백/클라우드 모드 |
| `ENGINE_MODE` / `ENGINE_URL` | 엔진 위치(위 표) |
| `ANTHROPIC_API_KEY` | `claude_api` provider용. API 컨테이너가 아니라 엔진 게이트웨이 호스트에 주입 |
| `ENGINE_MODE` / `ENGINE_URL` | provider와 엔진 게이트웨이 위치(위 표) |
| `SITE_ADDRESS` | 외부노출 도메인(예: `vignette.chanpaca.net`). 비우면 로컬 :80 |
| `ACME_EMAIL` | 외부노출 시 Let's Encrypt 자동 TLS용 |
| `HTTP_PORT`/`HTTPS_PORT` | 호스트 포트 매핑 |
@ -66,7 +68,7 @@ docker compose up -d
- **수직**: compose 그대로, 호스트 사양만 키움.
- **수평**: `docker compose up --scale api=3` (api 무상태 설계 전제, 세션은 DB/Redis). proxy가 라운드로빈.
- **이전**: `pgdata`·`ragmodels` 볼륨만 새 호스트로 옮기고 `.env` 채워 `up`. (rag 모델 캐시 볼륨 덕에 재다운로드 0)
- **클라우드**: 동일 compose. 단 엔진을 `messages_api`로 바꾸거나 엔진 PC를 `ENGINE_URL`로 원격 지정.
- **클라우드**: 동일 compose. `ENGINE_MODE=claude_api`와 게이트웨이 주소를 지정하고 게이트웨이 호스트에 `ANTHROPIC_API_KEY`를 주입한다.
## 6. 데이터 주권 / 보안

View file

@ -1,6 +1,6 @@
# Vignette Handoff
> Updated: 2026-07-15 KST. 새 세션은 이 문서와 `docs/DESIGN_CONCEPT.md`를 먼저 읽고 이어가면 된다.
> Updated: 2026-07-30 KST. 새 세션은 이 문서와 `docs/DESIGN_CONCEPT.md`를 먼저 읽고 이어가면 된다.
## 현재 상태
@ -10,8 +10,9 @@
- 로컬 engine gateway 기본 포트: `http://127.0.0.1:9099`
- 공개 웹: `https://vignette.chanpaca.net`
- 공개 API: `https://api-vignette.chanpaca.net`
- 최신 앱 배포 소스: 2026-07-15 전 저장소 리팩터 거버넌스 P1~P8, 계약/런타임/디자인 SSOT와 대형 화면 구조 분리, 회기·관리자·설정 회귀 수정까지 포함한 `master` 커밋 `3dfddcac`.
- 최신 Cloudflare Pages production deploy: 2026-07-15 manual deploy `https://9fa684e5.vignette-b1q.pages.dev`, branch `main`, custom domain assets `assets/index-FVj73yvV.js` + `assets/index-B3HPjbqc.css`. 커스텀 도메인과 preview가 같은 엔트리 번들을 서빙하고, 공개 API는 prod/db/engine 200, 비인증 `/personas` 401, Google OAuth 시작 302다.
- 최신 앱 배포 소스: base commit `facc4ad2`의 현재 dirty worktree를 `--commit-dirty=true`로 표시해 빌드했다. AI provider capability 변경과 작업트리의 현재 웹 변경이 포함됐으며 별도 commit/push는 하지 않았다.
- 최신 Cloudflare Pages production deploy: 2026-07-30 guarded manual deploy `https://2f52f8e3.vignette-b1q.pages.dev` (`2f52f8e3-f992-441f-ada4-f6396cbb1a2b`), branch `main`, custom domain assets `assets/index-B54GMBSf.js` + `assets/AdminAi-ClAJvCvx.js`. 현재와 이전 4세대 엔트리를 JavaScript 200으로 보존했고, 공개 API는 prod/db/engine 정상이다.
- 관리자 AI 엔진 capability: 공급자 6종을 드롭다운으로 선택하고 설치·인증 상태에 따라 live 모델과 추론 강도만 선택한다. 공개 인증 E2E에서 Codex 7개 모델의 기본 `gpt-5.6-terra / medium`, Agy 11개 모델의 기본 `gemini-3.6-flash-high / high`를 확인했으며 설정은 저장하지 않았다. Anthropic API는 키가 없어 fail-closed unavailable이다.
- Google OAuth 허용 이메일 도메인: `hs.ac.kr`, `twentyoz.kr`
- 최신 백엔드 회귀: API `400 passed`, engine gateway `29 passed`; 프론트 전체 Playwright fixture `166/166` + DB/engine/provider 직렬 `49/49`로 합계 `215/215 passed`.
- X1 재귀학습 export 1차: `scripts/export-recursive-dataset.py` 기본 read-only dry-run, `--write-dataset` 명시 시에만 `ds.*` write, approved export는 steward/legal/IAA gate 없으면 거부.
@ -30,7 +31,10 @@
- M3 인증 claim 1차: Google/SAML/dev-login이 설정 기반 cohort map과 SAML cohort claim을 `cohort_ids`로 넘기고, 관리 사용자 `external_id`는 provider subject 기반(`google:`/`saml:`/`dev:`)으로 저장한다. 운영 SAML 서명검증·기관 claim schema·deprovisioning audit은 후속.
- 페르소나 저작/P4~P7 적재 2차: `data/personas/P4.json`~`P7.json`을 저장소 관리 `PersonaCard`로 읽어 `materialize_seed_personas()`와 seed fallback catalog에 포함했다. 검증: `python -m pytest app/test_persona_review.py -q` 22 passed, 로컬 dev DB materialize 결과 `approved_codes=P1,P2,P3,P4,P5,P6,P7`, 인증된 로컬 `GET /personas``P1..P7` 반환.
- Google OAuth 진단 1차: provider callback error는 `access_denied`/`provider_error`로 분리하고, 로그인 화면은 실패 reason code를 함께 표시한다. 실제 Google 계정 완료 proof는 아직 owner 로그인/storageState가 필요하다.
- 관리자 기본 진입 경로와 blank diagnostics: `initialPathForUser()`가 pending/onboarding/admin-entitled landing을 소유한다. 관리자 콘솔 접근권이 있는 계정은 primary role이 learner/teacher여도 `/` 또는 로그인 완료 뒤 `/admin`으로 들어간다. `App.tsx`는 pathname 변경마다 document scroll을 0으로 복원해 긴 관리자 화면에서 짧은 `/admin/access`로 이동해도 sticky shell만 보이는 빈 화면을 만들지 않는다. 관리자 화면은 `/admin/users``/admin/tickets` payload 계약 위반을 안전하게 정규화하고 `관리자 데이터 진단`으로 깨진 필드/path/asset을 표시한다. 앱 route-level error boundary도 렌더 예외를 full white page 대신 진단 화면으로 대체하며, 0-height main도 진단 패널로 노출한다. `index.html`에는 React/module 실행 자체가 실패해도 3.5초 뒤 `Vignette 화면 진단`을 body에 직접 띄우는 boot watchdog이 있고, 같은 진단을 `/client-diagnostics`로 보내 API 로그에 남긴다. 검증: `npm run typecheck`, `npm run build`, `python -X utf8 -B -m pytest -p no:cacheprovider app/test_client_diagnostics.py -q`, `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 --grep "admin route guards"` 5 passed, dist file JS-missing watchdog smoke passed, custom domain 새 asset 확인.
- 관리자 기본 진입 경로와 blank diagnostics: `initialPathForUser()`가 pending/onboarding/admin-entitled landing을 소유한다. 관리자 콘솔 접근권이 있는 계정은 primary role이 learner/teacher여도 `/` 또는 로그인 완료 뒤 `/admin`으로 들어간다. `App.tsx`는 pathname 변경마다 document scroll을 0으로 복원해 긴 관리자 화면에서 짧은 `/admin/access`로 이동해도 sticky shell만 보이는 빈 화면을 만들지 않는다. 최초 `/auth/me`는 요청별 5초 상한과 짧은 3회 재시도를 사용하며, 재부팅 중 연결 실패는 로그아웃/권한 상실로 오인하지 않고 `AuthRestoreGate`에서 같은 세션으로 다시 연결한다. 설정 기반 슈퍼 관리자/관리자 entitlement는 로그인과 세션 복원 시 `app.app_user.admin_access`에도 영속화해 재기동 뒤 설정 판정만 의존하지 않는다. OAuth upsert의 nullable 관리자 플래그는 PostgreSQL `boolean`으로 명시 캐스팅하며 non-dev 저장 실패는 원래 DB 예외를 로그에 남기고 503으로 닫는다. primary role 화면의 사이드바에도 `운영 콘솔` 진입점을 유지해 학습자/교수자 작업면으로 이동한 뒤 관리자 페이지가 사라지지 않는다. 관리자 화면은 `/admin/users``/admin/tickets` payload 계약 위반을 안전하게 정규화하고 `관리자 데이터 진단`으로 깨진 필드/path/asset을 표시한다. 앱 route-level error boundary도 렌더 예외를 full white page 대신 진단 화면으로 대체하며, 0-height main도 진단 패널로 노출한다. `index.html`에는 React/module 실행 자체가 실패해도 3.5초 뒤 `Vignette 화면 진단`을 body에 직접 띄우는 boot watchdog이 있고, 같은 진단을 `/client-diagnostics`로 보내 API 로그에 남긴다. 2026-07-30 Cloudflare Pages production `1332ccbe`는 새 엔트리 `index-D4Twx0A1.js`와 직전 3세대 자산을 함께 게시했다. 실제 Google 슈퍼 관리자 사용자 행에 연결한 15분 임시 세션으로 관리자 5경로, primary learner 화면의 운영 콘솔 복귀, restored-tab 복원을 공개 사이트에서 4 passed로 확인했고 임시 세션은 즉시 삭제했다. 이어 관리자 권한 영속화 SQL의 타입 추론 회귀를 수정해 API PID `40992`로 재시작했고, 같은 실제 사용자 행으로 신규 세션 생성→공개 `/auth/me` 200(`admin_access=true`, `super_admin=true`, approved)까지 재검증한 뒤 세션을 폐기했다.
- 2026-07-30 관리자 실제 빈 본문 후속: 실제 스크롤 소유자는 document가 아니라 `.vg-main`이므로 `AppShell`이 mount·pathname 변경·`pageshow`마다 내부 스크롤을 직접 0으로 복원한다. 이전 E2E는 `window.scrollY`만 조작해 잘못된 대상을 통과시켰고, 정확한 `.vg-main.scrollTop=900` 재현은 수정 전 2 failed·수정 후 desktop/mobile 8 passed였다. 진단 visible node도 `.vg-main` viewport 교차를 요구한다. Cloudflare Pages production `f323a41d``index-Dun9umCY.js`와 현재 포함 직전 4세대 자산을 서빙한다. 실제 Google 슈퍼 관리자 사용자 행 기반 공개 5경로·learner→운영 콘솔·내부 scrollTop 복원 4 passed와 관리자 홈 픽셀 캡처를 직접 확인하고 임시 세션을 폐기했다. API PID는 `40992`, 공개 health는 prod·db=true·engine=true다.
- 2026-07-30 관리자 광고 차단기 blank 근본 수정: 실제 사용자 캡처와 같은 셸-only 화면은 권한/API/스크롤이 아니라 공식 EasyList의 `.ad-root`·`.ad-section` cosmetic rule이 관리자 `ad-*` 클래스를 광고로 오인한 결과였다. 같은 `display:none!important` 규칙을 주입해 첨부 화면을 그대로 RED 재현한 뒤 관리자 DOM/CSS 487곳을 `vgops-*`로 교체했다. 루트/진단은 `data-vignette-admin-*` marker를 쓰고 HTML watchdog은 marker의 실제 computed visibility까지 검사한다. 로컬 EasyList 회귀 1/1, admin guards 12/12, admin full-sweep 20/20, admin layout 3/3, session-layout 8/8, typecheck가 통과했다. production `0a339b77` 배포 뒤 실제 Google 슈퍼 관리자 사용자 행의 임시 세션에서 같은 EasyList 규칙을 켠 채 운영 홈·AI 운영·사용자·권한·티켓 5경로가 모두 visible, legacy `ad-*` class 0, boot/admin diagnostic 0, 브라우저 오류 0임을 확인하고 세션을 즉시 폐기했다.
- 같은 장애의 재유입 차단: `apps/web/scripts/check-cosmetic-filter-safety.mjs`가 프로덕션 소스 81개와 `index.html`을 검사하고 `ad-*`/`ads-*`/`advert*`/`sponsor*` UI namespace를 발견하면 `npm run build``npm run lint`를 실패시킨다. 탐지기 자체의 RED/GREEN fixture도 매 실행에 포함된다. 로컬 EasyList E2E는 첫 watchdog 판정 뒤에도 실제 root/heading이 보이고 legacy class가 0인지 검사하며, 공개 E2E는 같은 필터 아래 관리자 5경로를 모두 검증하도록 승격했다. 이 gate를 통과한 격리 빌드를 production `1b3b6d5b`로 다시 게시했고 custom/preview entry 일치, Admin JS SHA-256 일치, `vgops-root` 존재, `ad-root` 부재를 공개 응답 바이트로 확인했다.
- `frontenddesign` 스킬은 현재 세션의 사용 가능 스킬 목록에 없었다. 대신 `docs/DESIGN_CONCEPT.md`를 SSOT로 사용했다.
- 공개 API 터널은 현재 `C:\Users\encep\.cloudflared\vignette-config.yml`에서 `http://127.0.0.1:8001`을 본다.
- 공개용 API 프로세스는 `127.0.0.1:8001`에서 `ENVIRONMENT=prod`로 떠 있다. 로컬 개발 API `127.0.0.1:8000`과 Tailnet 개발 API `127.0.0.1:8010``ENVIRONMENT=dev`로 떠 있다.
@ -182,6 +186,8 @@
- `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\watch-public-runtime.ps1 -CheckOnly`
- healthy: `engine, api, web-preview, cloudflared, public-api`
- `api-vnet.18ka.net` is not part of the default watchdog checks while DNS is not live; add it later with `-AdditionalPublicHealthUrls`.
- 2026-07-30부터 엔진 readiness와 관리자·인증 제어면을 분리한다. 엔진 단독 장애는 API·웹·터널 재시작을 유발하지 않는다. 실운영에서 API PID `36824` 유지, 예약 작업 `LastTaskResult=0`, failcount `0`을 확인했다.
- API 코드 배포는 `start-public-runtime.ps1 -ForceApiRestart -SkipEngineRestart -SkipWebRestart -SkipCloudflaredRestart`로 API만 교체한다. 2026-07-30 관리자 entitlement 수정 배포 뒤 public `/health``prod/db/engine=true`다.
- Local login on `127.0.0.1:5175`:
- `PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/auth.spec.ts --project=chromium-desktop`
- `6 passed`

View file

@ -22,7 +22,7 @@
- [ ] **재부팅 후 watchdog smoke** — 실제 Windows 재부팅 후 엔진/API/터널 자동 복구 + public `/turn` 실측.
DNS 개통 후 `api-vnet.18ka.net``-AdditionalPublicHealthUrls`로 명시 추가.
- [ ] **음성 캐스케이드 live** — 실제 Deepgram interim/final WSS, 물리 마이크, 공개 WSS, 50분 양방향 장시간 실측.
- [ ] **claude_cli ↔ Messages API 폴백 동일성** — `ANTHROPIC_API_KEY` 필요.
- [ ] **claude_cli ↔ Anthropic API live 동일성** — provider 라우팅·모델 탐색·설정 저장 경로는 구현 완료. 남은 게이트는 연구팀/기관 `ANTHROPIC_API_KEY`를 게이트웨이 호스트에 주입한 뒤 같은 프롬프트의 live 응답·계량·오류 표면화를 비교하는 것이다.
## B. 외부 거버넌스 [외부]
> 출처: 백로그 B4 · `ops/hanshin-data-governance-gate.md` · `ops/source-docs-gap-analysis-2026-06-26.md` · `MASTERPLAN.md`
@ -45,15 +45,18 @@
- [ ] **s2s 2차 비교 PoC 구현·실측**`decisions/voice-s2s-poc.md` 기준표(전사·안전·감사·지연)로 20턴 비교 리포트 → 최종 채택/폐기.
- [ ] **fast-loop 외부 폴백 구현** — 로컬 상주 분류기 우선 + Haiku/Solar 폴백, 전송 가드(데이터주권·로그·동의)·관측.
- [ ] **대체 TTS provider 선정·통합** — 상업 라이선스 명확한 provider. dev sample(`VIGNETTE_VOICE_POC_SAMPLE_TTS`)과 서비스 경로 분리 유지.
- [ ] **대체 TTS provider 선정·통합** — 상업 라이선스 명확한 provider. 로컬 Higgs 실제 문장 합성은
`-UseHiggsVoice` 개발 전용 경로로 완료했으며, 무참조 synthetic seed만 사용한다. 서비스 배포 provider와
dev sample/Higgs 경로 분리는 계속 유지한다.
- [ ] **재귀학습 fine-tuning 파이프라인** — 동의/데이터셋 게이트 충족 후. few-shot 자동갱신은 선행 가능.
- [ ] **헬스 retention 적용·검증** — raw 90일 / rollup 365일. escalation/그룹 자동화는 미도입 고정.
- [x] **한신대 실사용 피드백 1차 개선팩**`ops/hanshin-feedback-improvement-plan-2026-07-03.md`,
`redteam/hanshin-feedback-plan-redteam-2026-07-03.md`, `redteam/hanshin-feedback-plan-counter-redteam-2026-07-03.md`
기준 P0~P4 1차 완료. 완료 범위: role mapping + client-only stateless history injection, review UI 표시 자연어화,
raw error display mapper, fast/deep 라벨, 명백한 출력 결함 차단, SSE full-response buffer, 교수자 설명 패널.
검증 수치는 SSOT 대시보드만 소유한다. 남은 후속은 저장형 fallback/synthetic 계약, 지연을 줄이는 partial stream buffer,
fast/deep reconciliation, resident session namespace, 임상팀 루브릭/CBT/약물·평가 문구 확정.
검증 수치는 SSOT 대시보드만 소유한다. partial stream, client 전용 resident session, fast-loop 사후 저장은
구현 완료했다. 남은 후속은 저장형 fallback/synthetic 계약, fast/deep reconciliation,
임상팀 루브릭/CBT/약물·평가 문구 확정.
## D-2. 전수 E2E 순회 소유자 결정 — **전건 결정 완료(2026-07-27)** → 구현 후속 [구현]
> 출처: `ops/e2e-full-sweep-2026-07-27.md` 발견 결함 로그 · `ops/backlog-2026-06-26.md` B3-신규.

File diff suppressed because one or more lines are too long

View file

@ -126,6 +126,9 @@ DB readiness는 auth/admin 테이블뿐 아니라 세션 read-model 핵심 테
- **`run_turn_stream(ctx, engine, *, log_hook=None)`** (4~8, 기본 UX 경로):
게이트웨이 SSE 원시 라인을 받아 `token | done | safety | error`로 재방출.
출력 가드레일은 *누적 텍스트* 기준으로 수단정보를 스캔하고, 발견 시 `safety` 이벤트 + 안전 대체로 종결한다.
세션 라우트는 client 응답과 결정론 상태를 먼저 영속화하고 `done`을 방출한다. fast-loop evaluator는
백그라운드 태스크로 실행해 같은 learner turn의 normalized 평가 row를 교체 저장하며, 평가 기반 코칭 충전도
이 태스크가 한 번만 적용한다. 따라서 보이지 않는 평가 지연이 다음 학습자 전송을 잠그지 않는다.
`TurnContext`/`TurnResult`/`StreamEvent` dataclass가 파이프라인을 관통한다. `EvalHook`/`LogHook`
`Callable[[TurnContext, str], Awaitable[...]]` 타입으로 주입된다.
@ -300,7 +303,7 @@ RBAC×AIView로 차단된다. 이 모듈은 평가 신호만 산출한다.
### 2.8 음성 캐스케이드 — `app/services/voice.py` + `app/routes/voice.py`
OpenAI STT/TTS 어댑터(순수 변환 + voice preset 매핑). 상담 로직은 라우트가 조립한다.
STT/TTS 어댑터(순수 변환 + voice preset 매핑). 상담 로직은 라우트가 조립한다.
- STT: `/audio/transcriptions` (gpt-4o-transcribe → 404 시 whisper-1 폴백), 언어 힌트 `ko`.
- TTS: `/audio/speech` (gpt-4o-mini-tts → tts-1 폴백). 음성 선택 우선순위는 명시 query preset →
@ -309,12 +312,16 @@ OpenAI STT/TTS 어댑터(순수 변환 + voice preset 매핑). 상담 로직은
OpenAI가 아닌 provider row는 현재 live OpenAI TTS로 보내지 않고 기존 fallback을 사용한다.
dev 런타임 스키마 보강은 기존 DB의 `app.persona_voice_map` 누락도 복구해 seed materializer와
`/voice/ws` 바인딩이 같은 테이블을 사용하게 한다.
- 로컬 개발에서 `VIGNETTE_VOICE_TTS_PROVIDER=higgs`이면 P1만 loopback Higgs 서버로 합성한다.
`scripts/higgs-tts-server.py`는 설치된 `higgs-audio-v3-tts-4b`를 다운로드 없이 한 번만 GPU에 올리고,
저장소의 무참조 synthetic seed만 reference로 쓴다. `/health`는 model/load time/reference policy를,
`/tts`는 24kHz WAV와 provider/model 헤더를 반환한다. 이 경로는 dev 전용이며 non-dev 설정은 fail-closed다.
프론트는 마이크 클릭과 텍스트 발화 전송 시 `AudioContext`를 먼저 resume해 재생 권한을 확보하고,
TTS Blob을 Web Audio buffer source로 재생하면서 `AnalyserNode`로 립싱크 RMS를 산출한다. 디코딩 실패 시
`<audio>` 재생으로 fallback한다.
- 텍스트 턴의 AI 내담자 응답은 인증된 `POST /voice/speech``session_id`/`turn_seq`로 소유 회기를 다시
로드하고, 이미 저장된 client-visible 내담자 응답만 MP3로 합성한다. 브라우저가 임의 문장을 보내는 유료
TTS 프록시가 아니며, 마이크 WebSocket과 같은 voice map/OpenAI TTS 어댑터를 공유한다.
로드하고, 이미 저장된 client-visible 내담자 응답만 선택 provider의 오디오로 합성한다. 브라우저가 임의
문장을 보내는 TTS 프록시가 아니며, 마이크 WebSocket과 같은 voice map/TTS 어댑터를 공유한다.
- 키 없으면 명확히 degraded(`is_available()=False`, `VoiceUnavailable`). 라우트가 503/WS close로 변환.
`/voice/ws` WebSocket 캐스케이드(`routes/voice.py`):
@ -362,7 +369,8 @@ session lifecycle을 유지하며, future Node read API는 이 read-model contra
학습자 발화 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 타임아웃 회피).
done 시점에 학습자 발화 + 누적 내담자 응답 + 상태를 먼저 영속화하고 즉시 완료한다. fast-loop 평가는
응답 경로 밖에서 같은 learner turn에 사후 저장한다. `sse_heartbeat_seconds`마다 ping(Cloudflare 타임아웃 회피).
- `POST /sessions/{id}/live-coach` — 방금 완료된 상담자 발화를 워크북 요약/RAG/evaluator fast-loop 신호와 대조해
라이브 코칭 카드 1개를 반환하고 `app.live_coach_events``use/-1`로 저장한다. 저장 payload는 마스킹
excerpt와 코칭 구조화 JSON이며, 잔여 기회가 없으면 409로 차단한다.
@ -550,9 +558,9 @@ HTTP error mapping을 유지한다.
## 3. 엔진 게이트웨이 — `apps/api/engine_gateway/gateway.py`
컨테이너 밖(호스트)에서 도는 **별도 서비스**. `claude -p`(Opus 4.8) 상주 멀티턴 풀을 흡수한다.
회기당 1 `EngineSession` = `claude -p` 프로세스 1개 상주 → 페르소나 system 프롬프트 고정 + 발화마다
stdin 주입(턴 간 컨텍스트 유지 + prompt caching 재사용).
컨테이너 밖(호스트)에서 도는 **별도 서비스**. Claude CLI 상주 멀티턴 풀과 Anthropic API,
Codex CLI, Agy CLI의 모델 탐색·실행 차이를 흡수한다. Claude CLI는 회기당 1 `EngineSession` 프로세스를
상주시켜 페르소나 system 프롬프트와 prompt caching을 유지한다. 나머지 공급자는 격리된 stateless 호출로 실행한다.
실행:
```
@ -570,11 +578,20 @@ cd apps/api && uvicorn engine_gateway.gateway:app --host 0.0.0.0 --port 9099
- `POST /v1/stream``turn_stream()`이 assistant 텍스트 델타를 즉시 yield → SSE
`event: token` / `event: done`(provider/model/cost 메타) / `event: error` 프레이밍.
- `session_id`가 살아있으면 풀 재사용, 아니면 1회성(ephemeral) 세션 생성 후 close(`_resolve_session`).
- **공급자 capability** `GET /v1/capabilities` — Codex app-server `model/list`, `agy models`, Anthropic
`GET /v1/models` 또는 Claude CLI 공식 alias를 공통 `EngineModelOption`으로 정규화한다. 60초 캐시와
관리자 강제 새로고침을 지원한다.
- `GET /ready``/health`(얕은 프로세스 liveness)와 달리 실제 1턴 생성을 시도해
"설치됐지만 미인증" CLI 상태를 학습자 도달 전에 잡는다(TTL 캐시).
선택한 provider/model/reasoning_effort의 "설치됐지만 미인증" 상태를 학습자 도달 전에 잡는다(TTL 캐시).
provider 라우팅 모드는 `ENGINE_MODE`(`app/config.py`)로 선택: `claude_api`(기본, Anthropic Messages API
직결) / `claude_cli`(로컬 상주 풀) / `openai`(폴백/평가 보조) / `solar`(국내, PII 민감구간 inference_geo:kr).
provider 라우팅 모드는 `ENGINE_MODE`(`app/config.py`) 또는 DB의 관리자 설정으로 선택한다.
실행 가능 공급자는 `claude_cli`, `claude_api`, `codex_cli`, `agy_cli`다. Codex 기본은
`gpt-5.6-terra` / Medium, Agy 기본은 `gemini-3.6-flash-high` / High다. `openai``solar`
기존 계약값을 보존하지만 실행 어댑터가 없어 capability에서 unavailable로 fail-closed한다.
`AdminEngineConfigResponse`는 provider·URL·model에 `reasoning_effort`를 더해 DB에 저장한다. PATCH는
제안된 게이트웨이에서 capability를 강제 재조회한 뒤 실제 목록에 없는 모델·추론 강도, 미인증 공급자,
접속 불가 URL을 422로 거부하고 저장 성공 후 `EngineClient` 런타임 설정을 함께 교체한다.
> 포트 주의: 게이트웨이 docstring 예시는 `:9099`이고, `app/config.py`의 bare Settings fallback은
> legacy compose 서비스명 기반 `http://engine:8100`이다. 현재 지원 compose/local runtime은
@ -602,7 +619,7 @@ React 19 + Vite. 라우팅은 `apps/web/src/App.tsx`(react-router-dom).
| `/teach/personas` | PersonaStudio(페르소나 저작·검수) | teacher/admin |
| `/teach/session/:sessionId/review` | SessionReview(교수자 읽기 전용 회기 검토) | teacher/admin |
| `/admin` | Admin(운영 홈) | admin |
| `/admin/ai` | AdminAi(AI 엔진 설정, 기간별 DB 비용·토큰·계량 커버리지, provider/model 원장, 평가 캐시 효율) | admin |
| `/admin/ai` | AdminAi(AI 공급자·실시간 모델·추론 강도 드롭다운, 기간별 DB 비용·토큰·계량 커버리지, provider/model 원장, 평가 캐시 효율) | admin |
| `/admin/users` | Admin(사용자 관리) | admin |
| `/admin/access` | Admin(접근 권한) | admin |
| `/admin/tickets` | Admin(운영 티켓 처리 큐: 접수, 필터, 카테고리 큐, 우선순위, 수동 상태 변경 감사) | admin |
@ -612,6 +629,13 @@ React 19 + Vite. 라우팅은 `apps/web/src/App.tsx`(react-router-dom).
- `App.tsx`는 모든 역할 페이지를 `lazy()`로 로드하고 공통 `Suspense` 부트 경계를 사용한다. 페이지별 순수 표시 계산은
`pages/*/model.ts`, 브라우저 음성 캡처는 `pages/session/voiceCapture.ts`, 페르소나 이름·난도·아바타 팔레트는
`lib/personaViewModel.ts`가 소유해 라우트 컴포넌트의 API/상태/렌더 책임과 분리한다.
- Pages 배포 전환 중 열린 탭이 삭제된 lazy 청크를 요청하면 `lib/chunkRecovery.ts`가 Vite
`vite:preloadError`를 받아 현재 경로에서 문서 재로드를 1회만 수행한다. 정상 라우트 렌더 뒤 재시도 표식을
지우고, 같은 경로의 연속 실패는 무한 재로드하지 않고 `RouteErrorBoundary`의 수동 복구 액션으로 넘긴다.
- 최초 `/auth/me` 복원은 요청별 5초 상한과 짧은 3회 재시도를 둔다. 재부팅 직후 API가 늦게 올라와도
세션을 즉시 로그아웃 처리하지 않으며, 끝내 연결되지 않으면 무한 `불러오는 중…` 대신 같은 HttpOnly
세션으로 다시 확인할 수 있는 `AuthRestoreGate` 복구 화면을 표시한다. `401`만 정상 로그아웃 상태로
확정하고 네트워크/5xx 실패는 권한 상실과 구분한다.
- API 클라이언트 `src/lib/api.ts`:
- `apiFetch`/`api`(get/post/put/del) — 표준 JSON, 에러는 `ApiError`로 정규화, 항상 `credentials:"include"`.
- SSE: `openSessionStream(sessionId, text, handlers)``fetch` 스트림을 직접 라인 파싱해
@ -769,18 +793,42 @@ DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
- 프론트의 최초 진입 경로(`initialPathForUser`)는 pending이면 `/pending`, 온보딩 미완료 일반 사용자는
`/onboarding`, 관리자 콘솔 접근권이 있는 사용자는 기본 역할이 learner/teacher여도 `/admin`을 우선한다.
역할 전환용 `roleHomePath`는 그대로 역할별 홈(`/learn`, `/teach`, `/admin`)만 소유한다.
- SPA 라우트 전환은 `App.tsx``pathname` 변경마다 문서 스크롤을 맨 위로 복원한다. 또한 브라우저
`history.scrollRestoration``manual`로 두고 `pageshow`에서도 다시 복원해, 재부팅 뒤 기존 관리자 탭을
되살릴 때 이전 `scrollY` 때문에 sticky 셸만 보이고 본문이 화면 위로 밀리는 빈 화면을 막는다.
- SPA 라우트 전환은 `App.tsx``pathname` 변경마다 문서 스크롤을 맨 위로 복원하고 브라우저
`history.scrollRestoration``manual`로 둔다. 실제 세로 스크롤 소유자는 document가 아니라
`.vg-main`이므로 `AppShell`이 mount·pathname 변경·`pageshow`마다 내부 `scrollTop/scrollLeft`
직접 0으로 되돌린다. 재부팅 뒤 기존 관리자 탭이나 긴 관리자 하위 페이지에서 이동한 화면에 sticky 셸만
남고 본문이 화면 밖에 머무르는 빈 화면을 막는다.
- 관리자 콘솔은 shell만 남고 본문이 비는 silent failure를 허용하지 않는다. `/admin/users` 응답의
`cohort_ids`, `active_sessions`, `created_at``/admin/tickets` 응답의 `summary`, `by_status`,
`by_priority` 같은 필드가 런타임 계약과 다르면 프론트가 안전한 기본값으로 정규화하고 `관리자 데이터 진단`
패널에 깨진 필드와 asset/path를 표시한다. 앱 route-level error boundary는 관리자 컴포넌트 바깥에서
터진 렌더 예외도 full white page 대신 진단 화면으로 노출하고, 본문 0-height 상태도 route-scoped
diagnostic panel로 표시해 운영자가 원인 없이 빈칸만 보지 않게 한다.
- 관리자 DOM/CSS는 광고 차단기의 generic cosmetic selector와 충돌하는 `ad-*` 접두사를 쓰지 않고
`vgops-*` namespace를 쓴다. EasyList에는 `.ad-root``.ad-section`이 실제 광고 숨김 규칙으로
등록되어 있어, 옛 접두사는 API가 모두 정상이어도 관리자 본문 전체를 `display:none`으로 만들었다.
관리자 루트와 진단 marker는 class명이 아닌 `data-vignette-admin-*` 속성으로 식별한다.
- 이 계약은 관례가 아니라 빌드 게이트다. `scripts/check-cosmetic-filter-safety.mjs`가 프로덕션 TS/TSX/CSS와
`index.html`의 class/id 후보를 정적으로 검사하고, 광고 의미의 위험 namespace를 발견하면 `build`
`lint`를 실패시킨다. 검사기는 내장된 위험/안전 fixture를 매번 먼저 실행해 탐지 로직이 무력화된 채
통과하지 못하게 한다. 공개 관리자 E2E는 동일 EasyList 규칙 아래 5경로의 실제 픽셀 가시성을 별도로
검증하므로 정적 검사와 런타임 검사가 서로 다른 실패면을 막는다.
- `index.html`은 React module 실행 자체가 실패하는 경우도 full white page로 두지 않는다. 부트스트랩 watchdog은
3.5초/8초 시점에 body/root visible content와 관리자 본문 marker를 검사하고, 실패 시 `Vignette 화면 진단`
패널을 React root 바깥 body에 직접 추가해 path, asset, bodyText, visibleNodes, global error를 표시한다.
패널을 React root 바깥 body에 직접 추가해 path, asset, bodyText, visibleNodes, mainScrollTop, global error를
표시한다. visible node와 관리자 marker는 `.vg-main` viewport 교차, rect, computed display/visibility/opacity를
함께 검사하므로 화면 밖 DOM이나 광고 차단기에 숨은 DOM을 정상 픽셀로 오인하지 않는다.
- 공개 런타임은 관리자·인증 제어면(`environment=prod`, `db=true`)과 AI 엔진 readiness를 별도 게이트로
감시한다. 엔진만 실패하면 `start-public-runtime.ps1`가 살아 있는 API·웹·cloudflared를 유지하고 엔진만
복구한다. 부팅 작업도 엔진이 늦더라도 관리자·인증 API가 준비되면 성공으로 끝내며 degraded 엔진은 별도
경고로 남긴다. 따라서 엔진 probe 실패가 API 재시작과 관리자 세션 복원 공백으로 전파되지 않는다.
- 설정 기반 슈퍼 관리자/관리자 이메일이 로그인하거나 기존 세션을 복원하면 유효한 관리자 접근권을
`app.app_user.admin_access`에도 영속화한다. primary role은 learner/teacher로 유지할 수 있지만,
재기동 뒤 환경설정 판정만으로 권한을 복원하지 않으며 기본 역할 사이드바에도 `운영 콘솔` 링크를 노출한다.
관리자 작업면에서는 `navRole="admin"`으로 전체 관리자 5경로를 항상 표시한다.
- OAuth 로그인에서 이메일 기반 기존 사용자와 provider external id를 연결할 때 nullable
`admin_access` SQL 인자는 명시적으로 `boolean` 캐스팅한다. non-dev의 managed-user 저장 실패는
메모리 폴백으로 숨기지 않고 원래 DB 예외를 서버 로그에 남긴 뒤 503으로 닫는다.
- OAuth/SAML callback은 저장된 `next`가 일반 진입 경로(`/`, `/learn`, `/teach`, `/login`, `/onboarding`)이고
로그인 사용자가 관리자 콘솔 접근권을 가지면 `/admin`으로 정규화한다. 단, `/learn/session/...` 같은 깊은
링크는 사용자가 의도적으로 연 URL일 수 있으므로 보존한다.
@ -845,13 +893,15 @@ 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)
### 7.2 엔진 게이트웨이 (AI 턴 생성, 다중 공급자)
```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)
```
`claude_api`는 게이트웨이 프로세스에 `ANTHROPIC_API_KEY`, CLI 모드는 해당 호스트에 로그인된
`claude`/`codex`/`agy` 실행 파일이 필요하다. 관리자 화면의 목록 조회가 unavailable이면 저장도 차단된다.
게이트웨이가 없으면 `/health``engine:false`이고 턴 생성은 실패하지만, UI/네비/로그인/페르소나/
세션생성(in-memory)은 동작한다.

View file

@ -26,6 +26,8 @@ DB가 없으면 인메모리 degraded 폴백으로 기동한다.
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-up.ps1
# 로컬 Higgs Audio v3 P1 음성까지 함께 연결
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-up.ps1 -UseHiggsVoice
# 종료: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-down.ps1
# DB 컨테이너까지 멈출 때만: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-down.ps1 -Db
```
@ -33,7 +35,8 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-up.ps1
- 진입점 **http://localhost:5173** → 로그인 페이지에서 **dev-login**(아무 `@hs.ac.kr`, role learner/teacher/admin).
- Docker가 있으면 기본적으로 `127.0.0.1:55432` DB 컨테이너를 사용한다. 새 컨테이너 생성 시 `POSTGRES_USER=vignette_owner`, API용 앱 role은 `DATABASE_URL` 사용자로 분리해 RLS 검증 기반을 보존한다. Docker가 없거나 `-NoDb`를 쓰면 in-memory degraded로 뜬다.
- 로그는 `.devlogs/`(gitignore). 코드 수정 후에는 dev-up을 다시 실행해 재기동(reload 미사용).
- 옵션: `-NoGateway`(기존 gateway 보존), `-NoWeb`(기존 web 보존, API만 재기동), `-NoDb`(DB 컨테이너 보장 건너뜀).
- 옵션: `-NoGateway`(기존 gateway 보존), `-NoWeb`(기존 web 보존, API만 재기동), `-NoDb`(DB 컨테이너 보장 건너뜀),
`-UseHiggsVoice`(설치된 `higgs-audio-v3-tts-4b`를 127.0.0.1:9881에 상주시켜 P1 TTS로 연결).
- `-NoGateway`/`-NoWeb`를 쓰면 해당 컴포넌트의 stale 정리도 건너뛰고, API 정리는 지정한 `-ApiPort` listener만 대상으로 한다. public `8001`, Tailnet `8010`, local `8000`을 나눠 띄운 상태에서 API-only 재기동할 때 다른 포트를 건드리지 않는다.
- `dev-down.ps1`은 기본적으로 DB 컨테이너를 보존한다. 컨테이너도 멈추려면 `-Db`를 명시한다.
@ -90,7 +93,10 @@ DB·엔진 없이도 UI/로그인/페르소나/세션 생성까지는 동작한
- **Python 3.11** (운영 스크립트가 Python 3.11 기준). 가상환경 권장.
- **Node.js 22 + npm** — CI와 같은 기준. newer LTS는 별도 재검증 전까지 기준선으로 쓰지 않는다.
- (선택) **Docker Desktop**`infra/docker-compose.yml` 전체 스택을 띄울 때만.
- (선택) **`claude` CLI** — `ENGINE_MODE=claude_cli`로 실제 턴 생성을 할 때. 설치 후 로그인되어 있어야 한다.
- (선택) **`claude` CLI** — `claude_cli` 실행. 설치 후 로그인되어 있어야 한다.
- (선택) **Codex CLI**`codex_cli` 실행과 `model/list` 탐색. 기본 선택은 `gpt-5.6-terra` / Medium.
- (선택) **Agy CLI**`agy_cli` 실행과 `agy models` 탐색. 기본 선택은 `gemini-3.6-flash-high` / High.
- (선택) **`ANTHROPIC_API_KEY`** — `claude_api`의 모델 목록과 Messages API 실행.
### Python 의존성 설치 (`apps/api`)
@ -130,7 +136,8 @@ npm install
| `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 라우팅 |
| `ENGINE_MODE` | `claude_cli` | `claude_cli` / `claude_api` / `codex_cli` / `agy_cli` 공급자 라우팅. 실제 운영 변경은 관리자 드롭다운이 DB에 저장 |
| `VIGNETTE_LIVE_CLIENT_PROVIDER` | `claude_cli` | 실시간 내담자 AI 전용 lane. 관리자에서 선택한 evaluator/review 공급자와 분리해 회기별 Claude 상주 세션을 재사용 |
| `AUTH_DEV_LOGIN_ENABLED` | `true` | dev-login 엔드포인트 활성화 |
| `AUTH_ALLOWED_EMAIL_DOMAINS` | `["hs.ac.kr","twentyoz.kr"]` | 기본 로그인 허용 이메일 도메인(dev-login 포함 검증). `/admin/users`에 미리 등록된 정확한 이메일은 도메인 밖이어도 예외로 로그인 가능 |
| `AUTH_SUPER_ADMIN_EMAILS` | `["yunchan@twentyoz.kr","hoonjungkoo@hs.ac.kr"]` | 학습자·교수자·관리자 공간 접근과 승인 상태를 부여할 슈퍼 관리자 이메일 |
@ -139,6 +146,8 @@ npm install
| `AUTH_EMAIL_COHORT_MAP` | `{}` | 특정 이메일을 cohort id로 매핑한다. 값은 comma-separated 문자열도 허용 |
| `AUTH_DOMAIN_COHORT_MAP` | `{}` | 이메일/Google hosted domain을 cohort id로 매핑한다. Google/SAML/dev-login 세션 `cohort_ids`에 반영 |
| `VIGNETTE_VOICE_POC_SAMPLE_TTS` | `false` | P1 무참조 샘플 음성을 `/voice/ws` TTS에 연결하는 개발 전용 플래그. 마이크/STT는 `OPENAI_API_KEY` 필요, 프로덕션 금지 |
| `VIGNETTE_VOICE_TTS_PROVIDER` | `openai` 또는 `higgs` | TTS 공급자. `higgs`는 dev + P1에서만 허용하며 다른 환경은 설정 검증에서 차단 |
| `VIGNETTE_HIGGS_TTS_URL` | `http://127.0.0.1:9881` | 로컬 Higgs 상주 서버. 저장소의 무참조 synthetic seed만 화자 참조로 사용 |
| `EVALUATOR_SEMANTIC_CACHE_ENABLED` | `true` | fast/deep evaluator structured 결과 인메모리 캐시 활성화. 원문 prompt/completion은 저장하지 않음 |
| `EVALUATOR_SEMANTIC_CACHE_TTL_SECONDS` | `900` | evaluator cache TTL(초). 0 이하면 비활성 |
| `EVALUATOR_SEMANTIC_CACHE_MAX_ENTRIES` | `256` | evaluator cache LRU 최대 엔트리 수. 0 이하면 비활성 |
@ -407,9 +416,15 @@ 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개로 컨텍스트·프롬프트 캐시를 재사용한다.
실제 발화 생성은 `ENGINE_MODE`와 무관하게 `engine_gateway`가 담당한다. 게이트웨이는
**컨테이너 밖(호스트)** 9099 포트에서 Claude CLI 상주 풀, Anthropic Messages API, Codex CLI,
Agy CLI를 공통 계약으로 라우팅한다. 관리자는 `/admin/ai` 또는 `/settings`의 AI 운영 섹션에서 공급자를
고르고, 게이트웨이가 반환한 모델·추론 강도만 드롭다운으로 저장할 수 있다.
실시간 내담자 발화는 `VIGNETTE_LIVE_CLIENT_PROVIDER=claude_cli`일 때 관리자 기본 공급자와 별도의
회기별 상주 Claude lane을 쓴다. Agy를 evaluator/review 기본값으로 유지해도 내담자 답변이 Agy CLI의
매 호출 도구 스키마 부팅 비용을 기다리지 않는다. 스트림의 learner/client turn과 상태는 먼저 저장하고
`done`을 보낸 뒤 fast-loop 평가는 백그라운드에서 같은 learner turn에 durable하게 붙인다.
### 5.1 실행
@ -420,24 +435,29 @@ python -m uvicorn engine_gateway.gateway:app --host 127.0.0.1 --port 9099
관련 환경변수(`gateway.py`):
- `CLAUDE_BIN` — claude 실행 파일 경로(기본 `claude`). PATH에 없으면 절대경로 지정.
- `CODEX_BIN`, `AGY_BIN` — 각 CLI 경로. Windows Codex는 npm shim 아래 native exe를 자동 탐색한다.
- `ANTHROPIC_API_KEY`, `ANTHROPIC_API_BASE` — Anthropic 모델 목록·Messages API.
- `ENGINE_CLI_CWD` — Codex/Agy를 저장소 밖에서 실행할 격리 cwd.
- `ENGINE_CAPABILITY_CACHE_TTL_SECONDS`(기본 60), `ENGINE_CLI_TIMEOUT_SECONDS`(기본 300).
- `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).
설치는 됐지만 로그인 안 된 상태를 여기서 잡는다.
- `GET /health` — 얕은 게이트웨이 프로세스 liveness. `{"ok": true, "engine": "claude_cli", ...}`
- `GET /ready?provider=&model=&reasoning_effort=` — 선택 조합으로 **실제 1회 생성**을 돌려 인증·동작까지 증명.
- `GET /v1/capabilities?provider=&force=` — 현재 계정에서 선택 가능한 모델·추론 강도를 반환.
```powershell
Invoke-RestMethod http://127.0.0.1:9099/health
Invoke-RestMethod http://127.0.0.1:9099/ready
Invoke-RestMethod 'http://127.0.0.1:9099/v1/capabilities?provider=codex_cli&force=true'
Invoke-RestMethod 'http://127.0.0.1:9099/ready?provider=codex_cli&model=gpt-5.6-terra&reasoning_effort=medium'
```
API의 `/health`게이트웨이의 `/ready`를 호출(404면 `/health`로 폴백)해 `engine` 필드를 채운다
(`engine_client.health_detail`). 즉 게이트웨이가 떠 있어도 `claude`가 인증 안 됐으면 `/ready`가 503이라
API `/health``engine: false`가 된다.
API의 `/health`현재 DB 설정의 provider/model/reasoning_effort를 게이트웨이 `/ready`에 전달하고
(구형 게이트웨이는 `/health`로 폴백) `engine` 필드를 채운다. 따라서 프로세스만 떠 있고 선택한 CLI/API가
미인증이거나 모델 조합을 실행할 수 없으면 API `/health``engine: false`가 된다.
### 5.3 프로빙 스크립트 (선택)
@ -488,8 +508,8 @@ RAG 임베딩/리랭커 의존성은 기본 이미지에 설치하지 않는다.
```powershell
# 백엔드 (apps/api)
cd apps\api
python -m pytest app/ -q # 백엔드 기준선 400 pass
python -m pytest engine_gateway\ -q # 현재 29 pass
python -m pytest app/ -q # 백엔드 기준선 432 pass
python -m pytest engine_gateway\ -q # 현재 44 pass
# 웹 (apps/web)
cd apps\web

View file

@ -15,8 +15,8 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
| 검증 | 작업 디렉터리 | 명령 | DB | API(8000) | 웹(5173) | 엔진GW(9099) | 브라우저 | 현재 통과 |
|---|---|---|---|---|---|---|---|---|
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-07-15 전체 실행 400 passed |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 현재 29 pass |
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-07-30 전체 실행 432 passed |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-07-30 전체 실행 44 passed |
| API 타입 생성 체크 | `apps/web` | `npm run check:api-types` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 타입체크 | `apps/web` | `npm run typecheck` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 빌드 | `apps/web` | `npm run build` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
@ -42,15 +42,15 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
```sh
# apps/api
python -m pytest app/ -q # 앱 단위 테스트: 2026-07-15 전체 실행 400 passed
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트 현재 29 pass
python -m pytest app/ -q # 앱 단위 테스트: 2026-07-30 전체 실행 432 passed
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트: 2026-07-30 전체 실행 44 passed
```
수집만 빠르게 확인하려면:
```sh
python -m pytest app/ --collect-only -q # 현재: "400 tests collected" (2026-07-15)
python -m pytest engine_gateway/ --collect-only -q # 현재: "29 tests collected"
python -m pytest app/ --collect-only -q # 현재: "432 tests collected" (2026-07-30)
python -m pytest engine_gateway/ --collect-only -q # 현재: "44 tests collected" (2026-07-30)
```
> 참고: 실행 중 `PendingDeprecationWarning: Please use 'import python_multipart'`
@ -80,6 +80,7 @@ python -m pytest engine_gateway/ --collect-only -q # 현재: "29 tests collected
| 파일 | 검증 영역 |
|---|---|
| `engine_gateway/test_gateway_model.py` | 게이트웨이 모델 선택·공유 engine contract·SSE 요청/응답 계약(ENGINE_MODE별) |
| `engine_gateway/test_provider_registry.py` | Claude/Anthropic/Codex/Agy capability 정규화, 기본값, CLI 인자, 잘못된 모델·추론 강도 차단 |
---
@ -131,7 +132,7 @@ python scripts/smoke-resistance-openness-db.py \
npm install # 최초 1회 (devDependencies: typescript, vite, @playwright/test 등)
npm run check:api-types # FastAPI OpenAPI ↔ src/lib/api.gen.ts 동기화 확인
npm run typecheck # tsc -b — 타입 오류 0 확인
npm run build # tsc -b && vite build — 프로덕션 번들 생성까지 확인
npm run build # cosmetic-filter namespace gate → tsc -b → vite build
```
API DTO를 바꿨다면 먼저 생성 산출물을 갱신한다.
@ -279,6 +280,14 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API
`POST /voice/speech`, Session 마이크 UI를 함께 검증한다. 브라우저 `<audio>.play()`가 차단된 조건에서도
Web Audio buffer source 재생이 시작되는지와 Session 마이크 UI가 `audio_end`에 browser voice
activity/silence 메타를 싣는지 확인한다.
- 2026-07-30 세션 음성/지연 focused 검증: 실제 로컬 P1 브라우저에서 응답 생성 중 textarea가 enabled이고
다음 질문 초안이 보존되며, 음성 준비/재생 중에도 보내기가 enabled인 것을 확인했다. 이전 full API 2턴은
fast-loop evaluator를 직렬로 기다려 완료가 24.17/24.57초였지만, 사후 평가 분리 뒤 실제 UI 응답·전송 해제는
첫 턴 9.58초, 상주 세션 연속 턴 6.96초였다. `test_session_turn_persistence.py`는 느린 평가 훅이 끝나기 전에
SSE `done`이 반환되고 이후 normalized 평가가 learner turn에 붙는 계약을 고정한다. Higgs API smoke는
`higgs-audio-v3-tts-4b`, 24kHz mono WAV 362,924 bytes/7.56초, provider/model 헤더와 synthetic-seed-only
reference policy를 확인했다. 검증: backend 432 passed, gateway 44 passed, typecheck/API types/build,
session text stream 1 passed, voice skip/draft 1 passed.
- 2026-07-13 focused 검증: `PLAYWRIGHT_PORT=5269 npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run --workers=1 --grep "drives one voice turn"` **1 passed**. 같은 로그인에서 텍스트 턴 저장→소유 회기/턴 기반 `/voice/speech`→OpenAI speech 요청→Web Audio buffer 재생 시작을 확인하고, 별도 새 회기에서 마이크 PCM/STT→AI reply→TTS chunk/`tts_end` 경로가 유지되는지 검증한다. 백엔드 voice focused는 **32 passed**이며, 운영 키 직접 smoke는 `gpt-4o-mini-tts`가 98,133-byte MP3(11.68초)를 반환했다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` **7 passed**. 실제 브라우저 `openSessionStream()``/sessions/{id}/stream` → DB-backed `/review` 축어록 저장 경로, AI 튜터 코칭 이력 저장/재로딩, WebSocket `stt_result` 음성 비언어 메타데이터, Session 마이크 UI가 생성한 voice activity/silence 메타, Phase 3 pre/post 점수의 DB-backed 저장/재조회, 그리고 세션 종료 background deep 평가가 durable DB row로 저장되어 교수자 리뷰가 `평가 완료`로 전환되는지 검증한다. AI 튜터 코칭 이력 검증은 `POST /live-coach` 응답과 DB-backed history payload의 `status=ready`, `latency_ms>0`도 확인해 규칙 기반 `degraded` fallback 200 응답이 정상 AI 코칭으로 통과하지 못하게 한다.
- 2026-07-01 추가 DB-backed 검증: `PLAYWRIGHT_PORT=5238 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "finishes session end evaluation"` **1 passed**. 같은 자동 종료 평가 row가 `/teacher/dashboard``recent_sessions`에서도 `evaluation_status=ready`, `review_ready=true`, `supervisor_state=평가 완료`로 반영되는지 실제 DB/API/엔진으로 검증한다.
@ -334,12 +343,37 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API
`e2e/public-auth-turn.spec.ts``e2e/public-admin-visual.spec.ts`는 공개 사이트
(`https://vignette.chanpaca.net`)를 직접 타격하는 옵트인 스모크로, 로컬 웹 서버를 띄우지 않는다.
관리자 시각 스모크는 운영 홈·사용자·권한·티켓의 본문 노출과 브라우저 `pageshow` 탭 복원 뒤
스크롤/heading 가시성을 함께 검증한다. 절차는 `apps/web/e2e/README.md` 참고
관리자 시각 스모크는 운영 홈·AI 운영·사용자·권한·티켓의 본문 노출, primary learner 화면의
`운영 콘솔` 복귀, 브라우저 `pageshow` 탭 복원 뒤 실제 내부 `.vg-main.scrollTop=0`과 heading 가시성을
함께 검증한다. document `window.scrollY`만 검사하면 관리자 셸의 실제 스크롤 잔류를 놓치므로 금지한다.
로컬 `admin.spec.ts``EasyList cosmetic filters` 회귀는 공식 목록에 있는 옛 `.ad-root`
`.ad-section` 숨김 규칙을 그대로 주입한 뒤에도 중립 `[data-vignette-admin-root]`와 운영 홈 heading이
보이는지 확인한다. API 200이나 DOM 존재만으로 이 검증을 대신하면 광고 차단기 silent blank를 놓친다.
`npm run check:cosmetic-filter-safety``src/**/*.{ts,tsx,css}``index.html`을 검사해
`ad-*`, `ads-*`, `advert*`, `sponsor*` class/id namespace가 프로덕션 코드에 다시 들어오면 빌드를
즉시 실패시킨다. 탐지기 자체의 양성·음성 fixture는 매 실행마다 먼저 검증하며,
`npm run check:cosmetic-filter-safety:self-test`로 따로 실행할 수도 있다. 이 검사는 `npm run build`
`npm run lint` 앞단에 포함되므로 `scripts/start-public-runtime.ps1`의 실제 웹 빌드도 우회하지 못한다.
공개 `public-admin-visual.spec.ts` 역시 같은 EasyList 규칙을 첫 paint부터 적용한 상태로 관리자 5경로의
heading/root 실제 rect·computed visibility, legacy class 0, watchdog/console 오류 0을 확인한다.
절차는 `apps/web/e2e/README.md` 참고
(`E2E_PUBLIC_AUTH=1`, `npx playwright codegen ... --save-storage`로 인증 상태 캡처 후
`E2E_PUBLIC_STORAGE_STATE` 재사용). 캡처한 storage state에는 API 세션 쿠키가 들어 있으니
민감 정보로 취급한다.
### 3.8 배포 전환 청크 복구 스모크(선택)
`e2e/chunk-recovery-preview.spec.ts`는 프로덕션 빌드의 로그인 lazy 청크 첫 요청을 강제로 실패시켜
문서가 정확히 한 번만 다시 로드되고 로그인 라우트가 정상 렌더되는지 검증한다. 로컬 `vite preview` 또는
공개 도메인을 `PLAYWRIGHT_BASE_URL`로 지정하고 아래처럼 실행한다.
```powershell
$env:E2E_PREVIEW_BUILD = "1"
$env:PLAYWRIGHT_SKIP_WEB_SERVER = "1"
$env:PLAYWRIGHT_BASE_URL = "http://127.0.0.1:5262" # 또는 https://vignette.chanpaca.net
npx playwright test e2e/chunk-recovery-preview.spec.ts --project=chromium-single-run --workers=1
```
---
## 4. 전체 검증 순서 권장안 (로컬)
@ -356,9 +390,15 @@ cd apps/web && npm run typecheck && npm run build
cd apps/web && npm run e2e
```
엔진 턴 생성까지 보려면(음성/세션 MVP 등 일부 `@single-run`) 별도로 엔진 게이트웨이를
포트 9099에 띄운다(`ENGINE_MODE=claude_cli`). 게이트웨이가 없으면 `/health``engine:false`
이고, 실제 턴 생성 시나리오는 실패한다. 레이아웃/시각 게이트는 게이트웨이 없이도 통과한다.
엔진 턴 생성이나 관리자 모델 저장까지 보려면(음성/세션 MVP 등 일부 `@single-run`) 현재 코드의
엔진 게이트웨이를 포트 9099에 띄운다. DB의 `admin_engine_config.engine_url`이 오래 떠 있던 구형
게이트웨이를 가리키면 `/admin/engine-capabilities`가 404/503이므로 프로세스를 새 코드로 재기동해야 한다.
게이트웨이가 없으면 `/health``engine:false`이고 실제 턴 생성·모델 저장 시나리오는 실패한다.
레이아웃/시각 게이트는 게이트웨이 없이도 통과한다.
공급자 실증은 capability만 확인하지 말고 최소 한 번 실제 고유 응답까지 확인한다. Codex는
`gpt-5.6-terra` / Medium, Agy는 `gemini-3.6-flash-high` / High가 목록 기본값과 실행 결과 양쪽에서
일치해야 한다. Anthropic은 실제 키가 없는 환경에서 unavailable·저장 거부가 정상이다.
---

View file

@ -35,14 +35,14 @@
상세: `docs/ops/tailscale-vnet-runtime-2026-06-27.md`.
- [ ] **실배포 `infra/.env` owner-secret fill-in + real preflight** — 필수 소유자 비밀값: `APP_DB_PASSWORD`,
`OAUTH_GOOGLE_CLIENT_ID`, `OAUTH_GOOGLE_CLIENT_SECRET`, `OPENAI_API_KEY`, `SESSION_SECRET`, production-safe
engine/voice flags. 로컬 stray 값(`ENGINE_MODE=claude_p`, prod sample TTS flag 등)은 운영값으로 복사 금지.
engine/voice flags. 로컬 stray 값(`ENGINE_MODE=claude_cli`, prod sample TTS flag 등)은 운영값으로 복사 금지.
실제 secret 주입 후 `scripts/check-deploy-preflight.py` DB 포함 모드 + 배포지 health를 별도 증거로 닫는다.
- [ ] **공개 Google OAuth 실제 `/turn` proof** — 로그인 가능한 계정으로 `storageState` 캡처 후 `E2E_PUBLIC_AUTH=1`
+ `chromium-public-auth` 1회 통과 필요. (소유자 지시로 보류 중.) 실행 명령은 대시보드 "다음 실행 명령" 참조.
- [ ] **음성 캐스케이드 live** — 로컬 API 실제 OpenAI STT/TTS 경로는 live WS smoke 통과. 남은 범위: 실제 Deepgram
interim/final WSS, 물리 마이크, 공개 WSS, 50분 양방향 장시간 실측.
- [ ] **claude_cli ↔ Messages API 폴백 동일성**`ANTHROPIC_API_KEY`가 있어야 Messages API 경로 동일성 검증 가능.
claude_cli 경로는 게이트웨이 probe로 live 실측 완료.
- [ ] **음성 캐스케이드 live** — 로컬 API 실제 OpenAI STT/TTS와 P1 Higgs 실제 문장 WAV 합성 경로는 live smoke 통과.
Higgs는 무참조 synthetic seed·dev-only로 유지한다. 남은 범위: 실제 Deepgram interim/final WSS, 물리 마이크,
공개 WSS, 50분 양방향 장시간 실측과 상업 라이선스가 명확한 운영 TTS provider 통합.
- [ ] **claude_cli ↔ Anthropic API live 동일성** — provider 라우팅·Anthropic `/v1/models` 탐색·지원 추론 강도·관리자 fail-closed 저장 경로는 구현 완료. 남은 범위는 연구팀/기관 `ANTHROPIC_API_KEY`를 게이트웨이 호스트에 주입한 live 응답·계량·오류 표면화 비교다. Claude CLI·Codex CLI(Terra/Medium)·Agy CLI(Gemini 3.6 Flash/High)는 로컬 live probe를 통과했다.
- [ ] **재부팅 후 watchdog smoke**`watch-public-runtime.ps1` + Scheduled Task가 재부팅 후 엔진/API/터널을 복구하고
public `/turn`이 통과하는지 실측. 재부팅 불가로 미실행(parser/check-only 경로는 확인). DNS 개통 후
`api-vnet.18ka.net``-AdditionalPublicHealthUrls`로 명시 추가. 상세: `docs/ops/public-runtime-watchdog.md`.

View file

@ -529,8 +529,8 @@
- [x] `admin-ai-guard-pending-approval` PendingApprovalGate 리다이렉트 — 미승인 사용자가 /admin/ai에 오면 /pending으로 리다이렉트한다 · 검증: 신규 spec 필요
- [x] `admin-ai-guard-onboarding-bypass` OnboardingGate 관리자 경로 우회 — 온보딩 미완료여도 admin 권한 보유 시 /admin/* 경로를 통과시킨다 · 검증: 기존 e2e/admin.spec.ts
- [x] `admin-ai-shell-sidebar-nav` AppShell 관리자 사이드바 내비게이션 — navRole=admin 셸로 관리자 메뉴 링크를 제공하고 현재 페이지를 활성 표시한다 · 검증: 기존 e2e/admin.spec.ts
- [x] `admin-ai-initial-load` 초기 데이터 병렬 로드 — usage·health·engine-config를 allSettled로 호출해 성공분만 반영하고 실패 메시지는 중복 제거해 배너에 잇는다 · 검증: 기존 e2e/admin.spec.ts
- [x] `admin-ai-refresh-button` 새로고침 버튼 — 세 가지 데이터를 모두 다시 불러오며 로딩 중 비활성화된다 · 검증: 신규 spec 필요
- [x] `admin-ai-initial-load` 초기 데이터 병렬 로드 — usage·health·engine-config·engine-capabilities를 allSettled로 호출해 성공분만 반영하고 실패 메시지는 중복 제거해 배너에 잇는다 · 검증: e2e/admin.spec.ts + e2e/full-sweep-admin-ai.spec.ts
- [x] `admin-ai-refresh-button` 새로고침 버튼 — 사용량·헬스·설정·provider capability를 모두 다시 불러오며 로딩 중 비활성화된다 · 검증: e2e/full-sweep-admin-ai.spec.ts
- [x] `admin-ai-error-alert` 오류 배너(role=alert) — 로드·저장 실패 시 오류 메시지를 role=alert 배너로 띄우고 다음 요청 시작 시 초기화한다 · 검증: 신규 spec 필요
- [x] `admin-ai-aria-busy` 로딩 중 aria-busy 표시 — 페이지 컨테이너에 로딩 동안 aria-busy=true를 설정한다 · 검증: 신규 spec 필요
- [x] `admin-ai-window-toggle` 계량 기간 토글(7/30/90일) — 기간을 바꾸면 usage만 재조회하고 늦게 도착한 응답은 active 플래그로 무시한다 · 검증: 신규 spec 필요
@ -543,11 +543,12 @@
- [x] `admin-ai-provider-table` Provider·모델별 사용 원장 테이블 — by_provider 행을 비용 내림차순으로 정렬해 호출·토큰·비용·비중을 표로 보여준다 · 검증: 기존(부분) e2e/admin.spec.ts + 탐색 순회 · 탐색 GREEN(2026-07-27 실순회)
- [x] `admin-ai-provider-table-empty` 모델 원장 빈 상태 — 계량 행이 없으면 '모델별 계량 행이 아직 없습니다.'를 보여준다 · 검증: 신규 spec 필요
- [x] `admin-ai-engine-status-pill` 엔진 런타임 상태 배지 — health.services의 engine 서비스를 정상/중단/제한 운영으로 표시하고 미확인 시 '확인 불가'를 보여준다 · 검증: 기존(부분) e2e/settings.spec.ts + 탐색 순회 · 탐색 GREEN(2026-07-27 실브라우저 순회)
- [x] `admin-ai-engine-mode-radiogroup` AI 운영 방식 라디오그룹(4종) — claude_cli·claude_api·openai·solar를 role=radio 버튼으로 제공하며 저장 버튼을 눌러야 서버에 반영된다 · 검증: 기존 e2e/admin.spec.ts + e2e/db-persistence.spec.ts
- [x] `admin-ai-engine-url-input` 연결 주소 입력 — engine_url을 제어 컴포넌트로 편집하며 게이트웨이·API base URL 힌트를 제공한다 · 검증: 기존 e2e/admin.spec.ts
- [x] `admin-ai-engine-model-input` 기본 모델 입력 — 기본 model 문자열을 편집하며 평가 모델 override가 없을 때 적용된다고 안내한다 · 검증: 기존 e2e/db-persistence.spec.ts
- [x] `admin-ai-engine-provider-select` AI provider 드롭다운(6종) — Claude CLI·Anthropic API·Codex CLI·Agy CLI·OpenAI·Solar를 select로 제공하고, 설치·인증되어 모델을 조회할 수 있는 provider만 저장 가능하다 · 검증: e2e/admin.spec.ts + e2e/full-sweep-admin-ai.spec.ts
- [x] `admin-ai-engine-url-input` 연결 주소 입력 — engine_url을 편집하면 이전 주소의 model catalog를 즉시 무효화하고, 새 주소를 query로 전달한 목록 새로고침이 끝나기 전에는 저장할 수 없다 · 검증: e2e/admin.spec.ts desktop/mobile
- [x] `admin-ai-engine-model-select` 기본 모델 드롭다운 — 선택 provider의 live capability 목록만 표시하고 Codex는 Terra, Agy는 Gemini 3.6 Flash High를 기본 선택한다 · 검증: e2e/admin.spec.ts + e2e/full-sweep-admin-ai.spec.ts
- [x] `admin-ai-engine-effort-select` 추론 강도 드롭다운 — 선택 모델이 실제 지원하는 강도만 표시하고 Codex Terra는 Medium, Agy Flash는 High를 기본 선택한다 · 검증: e2e/admin.spec.ts
- [x] `admin-ai-engine-config-meta` 엔진 설정 메타 정보 — 저장 원천(DB 영구/런타임)·현재 소스·최근 변경 시각·변경자를 dl로 표시한다 · 검증: 신규 spec 필요
- [x] `admin-ai-engine-save-button` 운영 설정 저장 버튼 — 모드·URL·모델을 PATCH 저장하고 health를 재조회하며 2.4초간 '저장됨' 배지를 띄운다 · 검증: 기존 e2e/admin.spec.ts + e2e/db-persistence.spec.ts
- [x] `admin-ai-engine-save-button` 운영 설정 저장 버튼 — provider·URL·모델·추론 강도를 PATCH 저장한다. 서버는 제안된 게이트웨이 capability를 다시 검증하고 성공 후 health를 재조회하며 2.4초간 '저장됨' 배지를 띄운다 · 검증: e2e/admin.spec.ts + e2e/db-persistence.spec.ts
- [x] `admin-ai-engine-loading-empty` 엔진 설정 로딩/실패 빈 상태 — engine-config가 없으면 '불러오는 중입니다' 문구를 표시한다 · 검증: 신규 spec 필요
- [x] `admin-ai-cache-panel` 평가 캐시 효율 패널 — enabled 배지·hit-rate·요청/적중/미스/저장/엔트리/축출 카운트와 원문 미저장 안내를 표시한다 · 검증: 신규 spec 필요
- [x] `admin-ai-integrity-panel` DB 계량 상태(무결성) 패널 — 계량 원천·durable 여부·미계량 턴 수와 '감사 필요/누락 없음' 판정을 표시한다 · 검증: 기존(부분) e2e/readiness.spec.ts + 탐색 순회 · 탐색 GREEN(2026-07-27 실순회)
@ -560,8 +561,8 @@
- `admin-ai-aria-busy`: aria-busy 외에 시각적 로딩 스피너/스켈레톤이 없어 데이터 미도착 시 지표가 '—'로만 보인다.
- `admin-ai-window-toggle`: 기간 변경 실패 시 이전 기간의 usage 데이터가 화면에 그대로 남은 채 오류 배너만 떠서 기간 라벨(예: '90일 DB 집계')과 실제 표시 데이터가 불일치할 수 있다.
- `admin-ai-daily-chart`: 막대별 상세가 title 속성에만 있어 키보드·스크린리더로는 개별 일자 값에 접근할 수 없다(전체 aria-label만 존재).
- `admin-ai-engine-mode-radiogroup`: ENGINE_MODE_LABEL에는 messages_api 라벨이 있지만 선택지(ENGINE_MODES)에는 없어, 서버가 engine_mode='messages_api'를 반환하면 어떤 라디오도 선택 표시되지 않는다. 또 role=radio이지만 방향키 이동 등 라디오 키보드 패턴이 구현되어 있지 않다.
- `admin-ai-engine-url-input`: URL 형식 검증이 없어(공백만 아니면 저장 가능) 잘못된 주소도 그대로 PATCH된다.
- `admin-ai-engine-provider-select`: **해결(2026-07-30)** — 옛 4종 라디오와 `messages_api` 별칭을 없애고 6종 provider select와 capability 상태를 단일 계약으로 연결했다.
- `admin-ai-engine-url-input`: **해결(2026-07-30)** — 서버가 제안된 URL의 capability를 조회한 뒤 연결 실패·모델/강도 불일치를 422로 거부하므로 잘못된 주소가 durable 설정을 덮지 않는다.
- `admin-ai-engine-save-button`: 저장 성공 후 health 재조회 실패는 catch(() => health)로 조용히 무시되며 이때 스테일 클로저의 이전 health를 쓴다. window.setTimeout(2400ms)을 언마운트 시 정리하지 않아 페이지 이탈 후 setState가 호출될 수 있다. 저장 확인(confirm) 없이 즉시 운영 엔진이 교체된다.
- `admin-ai-engine-loading-empty`: GET 실패 시에도 같은 '불러오는 중' 문구가 영구 표시되어 실패 상태를 로딩으로 오인하게 한다(패널 내 재시도 수단 없음).
@ -570,7 +571,7 @@
- [x] `settings-guard-require-auth` RequireAuth 가드 (미인증 → /login) — 로딩 중 BootScreen을 띄우고 미인증이면 state.from을 담아 /login으로 보낸다(역할 제한 없음) · 검증: 신규 spec 필요
- [x] `settings-guard-pending-approval` PendingApprovalGate (미승인 → /pending) — 미승인 사용자가 /settings에 접근하면 /pending으로 리다이렉트한다 · 검증: 신규 spec 필요
- [x] `settings-guard-onboarding` OnboardingGate (온보딩 미완료 → /onboarding) — onboardingCompletedAt이 null이면 /onboarding으로 보낸다(/settings는 관리자 예외 경로가 아님) · 검증: 신규 spec 필요
- [x] `settings-initial-load` 설정 데이터 일괄 로드 — 프로필·환경설정·지원 티켓·음성 프리셋을 병렬 조회하고 관리자면 엔진 설정·헬스도 추가 조회한다 · 검증: 기존 e2e/settings.spec.ts
- [x] `settings-initial-load` 설정 데이터 일괄 로드 — 프로필·환경설정·지원 티켓·음성 프리셋을 병렬 조회하고 관리자면 엔진 설정·헬스·provider capability도 추가 조회한다 · 검증: e2e/settings.spec.ts
- [x] `settings-load-error-callout` 로드 실패 경고 콜아웃 — 로드 실패 시 화면 상단에 role=alert 경고 콜아웃으로 오류 메시지를 표시한다 · 검증: 신규 spec 필요
- [x] `settings-section-skeletons` 섹션별 로딩 스켈레톤 — 데이터 도착 전 role=status 스켈레톤을 띄우고 스크린리더용 텍스트를 숨김 제공한다 · 검증: 기존 e2e/settings.spec.ts
- [x] `settings-rail-nav-buttons` 좌측 레일 섹션 내비게이션 버튼 — 계정·지원·(관리자 AI 운영)·테마·알림·음성 버튼으로 해당 섹션에 스크롤하고 aria-current를 표시한다 · 검증: 신규 spec 필요
@ -583,10 +584,11 @@
- [x] `settings-support-ticket-list` 내 지원 요청 티켓 목록 — 내가 접수한 티켓을 상태·카테고리·우선순위 배지, 담당 그룹, 해결 메모와 함께 나열한다 · 검증: 기존 e2e/settings.spec.ts
- [x] `settings-support-error-state` 지원 요청 로드 실패 상태 — 티켓 조회만 실패하면 페이지를 깨뜨리지 않고 섹션 안에 role=alert 경고를 표시한다 · 검증: 신규 spec 필요
- [x] `settings-support-empty-state` 지원 요청 빈 상태 — 0건이면 '접수한 지원 요청이 없습니다', null이면 '저장소에 표시할 항목이 없습니다'를 표시한다 · 검증: 신규 spec 필요
- [x] `settings-engine-mode-radiogroup` AI 운영 방식 세그먼트 라디오 (관리자) — 4가지 연결 방식 버튼 중 하나를 골라 로컬 상태에 반영한다 · 검증: 기존 e2e/settings.spec.ts
- [x] `settings-engine-url-input` AI 연결 주소 입력 (관리자) — 응답 생성 서비스 연결 URL을 로컬 상태와 ref에 반영한다 · 검증: 기존(부분) e2e/settings.spec.ts + 탐색 순회 · 탐색 GREEN(2026-07-27 실브라우저 순회)
- [x] `settings-engine-model-input` 기본 모델 입력 (관리자) — 기본 모델 이름을 입력받고 최근 변경자·저장 상태 메타를 함께 표시한다 · 검증: 기존 e2e/settings.spec.ts
- [x] `settings-engine-save-button` 운영 설정 저장 버튼 (관리자) — 운영 방식·주소·모델을 PATCH 저장한 뒤 헬스를 재조회해 상태 칩을 갱신한다 · 검증: 기존 e2e/settings.spec.ts + e2e/db-persistence.spec.ts
- [x] `settings-engine-provider-select` AI provider 드롭다운 (관리자) — 6가지 provider 중 하나를 고르고 설치·인증·모델 조회 상태를 함께 표시한다 · 검증: e2e/settings.spec.ts
- [x] `settings-engine-url-input` AI 연결 주소 입력 (관리자) — URL을 로컬 상태에 반영하면서 이전 주소의 catalog를 무효화하고 새 주소 모델 조회 전 저장을 막는다 · 검증: e2e/settings.spec.ts + admin.spec.ts의 동일 계약
- [x] `settings-engine-model-select` 기본 모델 드롭다운 (관리자) — provider capability에 있는 모델만 선택하고 최근 변경자·저장 상태 메타를 함께 표시한다 · 검증: e2e/settings.spec.ts
- [x] `settings-engine-effort-select` 추론 강도 드롭다운 (관리자) — 선택 모델이 지원하는 강도만 고를 수 있다 · 검증: e2e/settings.spec.ts
- [x] `settings-engine-save-button` 운영 설정 저장 버튼 (관리자) — provider·주소·모델·추론 강도를 PATCH 저장한 뒤 헬스를 재조회해 상태 칩을 갱신한다 · 검증: e2e/settings.spec.ts + e2e/db-persistence.spec.ts
- [x] `settings-engine-health-chip` 응답 생성 헬스 상태 칩 (관리자) — engine 서비스 상태를 정상/제한 운영/중단/확인 중 텍스트와 detail·metric으로 표시한다 · 검증: 기존(부분) e2e/settings.spec.ts + 탐색 순회 · 탐색 GREEN(2026-07-27 실브라우저 순회)
- [x] `settings-theme-dark-toggle` 다크 모드 토글 — role=switch 토글로 즉시 data-theme을 바꾸고 localStorage에 저장해 전역 구독자와 동기화한다 · 검증: 신규 spec 필요
- [x] `settings-theme-save-button` 테마 저장 버튼 — 현재 다크 여부를 theme으로 서버 환경설정에 PATCH 저장하고 '저장됨'을 표시한다 · 검증: 신규 spec 필요
@ -611,9 +613,9 @@
- `settings-account-save-button`: saveProfile에 try/catch가 없어 실패 시 unhandled rejection — 사용자에게 오류 표시가 전혀 없고 저장 실패를 알 수 없음. 저장 중 busy 상태도 없어 연타 가능.
- `settings-support-ticket-list`: 읽기 전용 목록만 있고 이 화면에서 새 지원 요청을 접수하는 UI가 없음.
- `settings-support-empty-state`: 빈 상태가 로딩용 SettingsLoadingState(점멸 dot, role=status) 컴포넌트를 재사용해 영구 로딩 중처럼 오인될 수 있음. 두 빈 상태가 같은 testId(settings-support-empty)를 공유.
- `settings-engine-mode-radiogroup`: role=radio 버튼 그룹에 화살표 키 이동(roving tabindex)이 없어 키보드 접근성 미흡. 라벨 맵에는 messages_api가 있으나 선택지에는 없어 서버가 그 값을 반환하면 아무 라디오도 체크되지 않음.
- `settings-engine-url-input`: URL 형식 검증이 전혀 없음(공백만 아니면 저장 가능).
- `settings-engine-save-button`: 선택 모드를 React 상태 대신 document.querySelector('[data-engine-mode][aria-checked=true]') DOM 조회로 읽는 안티패턴 — 마크업 변경에 취약. PATCH 실패 시 try/catch 없음(unhandled rejection, 사용자 피드백 없음).
- `settings-engine-provider-select`: **해결(2026-07-30)** — 옛 DOM 조회 라디오를 제어 select 상태로 교체하고 6종 provider capability와 연결했다.
- `settings-engine-url-input`: **해결(2026-07-30)** — 저장 전 서버 capability 검증으로 잘못된 URL을 durable 설정에 반영하지 않는다.
- `settings-engine-save-button`: **해결(2026-07-30)** — React 상태에서 provider·모델·강도를 직접 전송하고 저장 실패를 화면 오류로 표면화한다.
- `settings-engine-health-chip`: 헬스 조회 실패를 조용히 삼키고 null 처리 — 실패해도 '확인 중'으로만 남아 장애와 로딩을 구분할 수 없음. 기본 status fallback이 'degraded'라 로딩 중에도 경고색 칩으로 렌더됨.
- `settings-theme-save-button`: savePreferences에 try/catch 없음(실패 무통보). 저장 안 하면 서버 theme이 남아 다음 /settings 방문 로드 시 로컬 토글값을 되돌림 — 토글 즉시 반영과 서버 저장이 이중 상태.
- `settings-voice-preset-radiogroup`: 카드 우측 SpeakerIcon(재생 삼각형)이 role=presentation·aria-hidden 장식일 뿐 미리듣기 기능이 없음 — 재생 버튼처럼 보이는 죽은 어포던스. 화살표 키 라디오 이동도 없음.
@ -639,14 +641,14 @@
- [x] `shell-appshell-focus-modes` 셸 레이아웃 변형(hideNav/hideTopbar/bleed/wide) — 페이지 props로 사이드바 숨김·전체화면·풀블리드·넓은 작업폭을 제공하고 미인증 시 사이드바를 자동 숨긴다 · 검증: 기존 e2e/session-layout.spec.ts
- [x] `shell-appshell-role-theming` body[data-role] 역할 테마 적용 — navRole에 따라 body에 data-role을 설정해 역할별 팔레트를 적용하고 언마운트 시 복원한다 · 검증: 신규 spec 필요
- [x] `shell-auth-bootstrap-loading` 인증 부트스트랩 로딩 화면 — 앱 시작 시 세션을 복원하고 완료 전까지 모든 가드가 중립 BootScreen을 표시한다 · 검증: 신규 spec 필요
- [x] `shell-suspense-fallback` lazy 라우트 청크 로딩 fallback — 모든 페이지가 lazy import이므로 청크 로딩 동안 Suspense fallback으로 BootScreen을 표시한다 · 검증: 신규 spec 필요
- [x] `shell-suspense-fallback` lazy 라우트 청크 로딩 fallback — 모든 페이지가 lazy import이므로 청크 로딩 동안 Suspense fallback으로 BootScreen을 표시한다. 배포 후 stale 청크 실패는 문서 재로드 1회로 최신 배포를 받고 연속 실패 시 ErrorBoundary로 넘긴다 · 검증: `e2e/full-sweep-shell.spec.ts` + `e2e/chunk-recovery-preview.spec.ts`, 공개 강제 실패 포함 GREEN(2026-07-28)
- [x] `shell-guard-require-auth` RequireAuth 미인증 리다이렉트 — 보호 라우트에서 미인증이면 원래 경로를 state.from에 담아 /login으로 replace 리다이렉트한다 · 검증: 기존 e2e/learner.spec.ts
- [x] `shell-guard-role-restriction` RequireAuth 역할 제한 리다이렉트 — 역할 불일치 시 빈 화면 대신 자기 역할 홈으로 보내며 admin·adminAccess 예외를 적용한다 · 검증: 기존 e2e/admin.spec.ts + e2e/teacher.spec.ts
- [x] `shell-guard-pending-approval` PendingApprovalGate 승인 대기 리다이렉트 — 미승인 사용자는 어떤 경로든 /pending으로 보내고 승인 사용자가 /pending에 오면 초기 경로로 되돌린다 · 검증: 신규 spec 필요
- [x] `shell-guard-onboarding` OnboardingGate 온보딩 리다이렉트 — 온보딩 미완료자는 /onboarding으로(admin의 /admin*만 예외), 완료자가 /login·/onboarding에 오면 initialPathForUser로 되돌린다 · 검증: 기존(부분) e2e/auth.spec.ts + 탐색 순회 · 탐색 GREEN(2026-07-27 실순회)
- [x] `shell-root-redirect` 루트 경로 역할별 분기 — / 접근 시 미승인은 /pending, 인증 사용자는 initialPathForUser, 미인증은 /login으로 replace 이동한다 · 검증: 기존 e2e/admin.spec.ts
- [x] `shell-unknown-route-redirect` 미정의 경로 리다이렉트 — 정의되지 않은 모든 경로는 404 화면 없이 /로 replace되어 역할 홈으로 재분기된다 · 검증: 신규 spec 필요
- [x] `shell-route-error-boundary` 라우트 렌더 오류 fallback — 렌더 예외 시 role=alert 진단 화면을 띄우고 경로가 바뀌면 오류 상태를 자동 리셋한다 · 검증: 기존(부분) e2e/admin.spec.ts + 탐색 순회 · 탐색 GREEN(2026-07-27 실브라우저 순회)
- [x] `shell-route-error-boundary` 라우트 렌더 오류 fallback — 렌더 예외 시 role=alert 진단 화면을 띄우고 경로가 바뀌면 오류 상태를 자동 리셋한다. 최신 버전 재로드·홈 이동 복구 액션을 제공한다 · 검증: 기존(부분) e2e/admin.spec.ts + 탐색 순회 + chunk recovery focused GREEN(2026-07-28)
- [x] `shell-scroll-reset-on-navigate` 경로 변경 시 스크롤 최상단 리셋 — scrollRestoration을 manual로 두고 경로 변경·bfcache 복원 시 스크롤을 최상단으로 되돌린다 · 검증: 기존 e2e/admin.spec.ts
- [x] `shell-auth-expired-listener` 세션 만료 이벤트 자동 로그아웃 — API 계층이 401에서 발행하는 auth-expired 이벤트를 받아 사용자 상태를 비워 /login으로 유도한다 · 검증: 기존 e2e/learner.spec.ts
- [x] `shell-avatar-expression-board` Live2D 페르소나 표정 보드 — P4~P7 모델별 공유 표정 라이브러리 전체를 모델 경로·모션·페이드 메타와 함께 정적 렌더하는 QA 보드다 · 검증: 기존 e2e/avatar-expression-lab.spec.ts
@ -657,11 +659,11 @@
- `shell-topbar-space-switcher`: 일반 학습자·교수자에게는 아예 미노출(의도). 컨테이너가 nav가 아닌 div에 aria-label만 부여됐고, accessibleRolesFor 결과를 canAccessRole로 재필터링하는 중복 로직이 있음.
- `shell-sidebar-admin-nav`: 12개 항목이 역할 그룹 구분(구분선·소제목) 없이 한 열에 나열되고 review 아이콘이 5개 항목에 중복 사용되어 스캔성이 낮음.
- `shell-auth-bootstrap-loading`: me 호출 실패 사유(네트워크 오류 vs 미인증)를 구분하지 않고 catch에서 조용히 user=null 처리해 오프라인 시 로그인 화면으로 떨어짐.
- `shell-suspense-fallback`: 청크 로드 실패(배포 후 stale 해시)는 Suspense가 아닌 ErrorBoundary로 전파되는데 별도 새로고침 유도 UI 없이 진단 표만 노출됨.
- `shell-suspense-fallback`: ~~청크 로드 실패(배포 후 stale 해시)는 Suspense가 아닌 ErrorBoundary로 전파되는데 별도 새로고침 유도 UI 없이 진단 표만 노출됨.~~ **FIXED(2026-07-28)**`vite:preloadError`에서 경로별 자동 새로고침을 1회만 수행하고 정상 라우트 렌더 뒤 잠금을 해제한다. 개발/production preview/공개 도메인 강제 청크 실패 E2E GREEN.
- `shell-guard-onboarding`: 온보딩 미완료 admin은 /admin*만 예외라 /settings·/teach 접근 시 /onboarding으로 튕김. 미인증 방문자의 /dev/avatar-preview는 통과되지만 로그인된 미승인·미온보딩 사용자는 이 dev 페이지도 게이트에 걸려 접근 불가.
- `shell-root-redirect`: onboardingCompletedAt==null 분기와 최종 분기가 동일하게 initialPathForUser를 호출해 사실상 죽은 중복 분기.
- `shell-unknown-route-redirect`: 전용 404 페이지가 없어 오타 URL이 조용히 홈으로 이동, 사용자가 링크 오류를 인지하지 못함.
- `shell-route-error-boundary`: 진단 화면에 componentStack 등 내부 정보가 그대로 노출되며(개발 편의 의도로 보임) 재시도/홈으로 버튼이 없어 사용자는 URL을 직접 바꿔야 복구됨.
- `shell-route-error-boundary`: 진단의 componentStack은 운영 식별 증거로 유지한다. ~~재시도/홈 버튼이 없어 URL을 직접 바꿔야 함.~~ **FIXED(2026-07-28)**`최신 버전 다시 불러오기``홈으로 이동` 액션을 추가했다.
- `shell-avatar-expression-board`: 학습자 사이드바 등 어떤 GNB에도 링크가 없어 URL 직접 입력으로만 접근 가능한 숨은 라우트. 콘텐츠가 QA용인데 learner 라우트(/learn/*)에 위치.
- `shell-avatar-preview-dev-page`: 인증 가드 없는 dev 라우트가 프로덕션 라우터에 그대로 포함되어 누구나 페르소나 리그를 열람 가능(코드 주석상 의도지만 배포 노출 여부 재검토 필요).

View file

@ -60,8 +60,19 @@ Use this when you want to force a runtime restore immediately:
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1
```
`start-public-runtime.ps1` now verifies or starts the engine gateway before it
starts the prod API and cloudflared.
`start-public-runtime.ps1`는 관리자·인증 제어면과 엔진을 분리한다. 이미
`environment=prod`, `db=true`인 API와 정상 웹·터널은 유지하고, 엔진만 실패한 경우
엔진만 복구한다. 엔진이 늦게 준비돼도 관리자·인증 API 기동을 막지 않는다.
API 코드 변경을 운영 프로세스에 반영할 때는 다른 표면을 유지한 채 API만 명시적으로 교체한다.
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1 `
-ForceApiRestart `
-SkipEngineRestart `
-SkipWebRestart `
-SkipCloudflaredRestart
```
## Health Checks
@ -113,6 +124,11 @@ and `C:\Users\<user>\.cloudflared\vignette-config.yml`.
If engine health fails, verify that `claude` runs for the same Windows user that
owns the scheduled task and that the user has completed Claude CLI login.
엔진 장애 중에도 `http://127.0.0.1:8001/health``environment=prod`, `db=true`
유지되면 관리자·인증 제어면은 정상이다. 이때 watchdog은 API·웹·터널을 재시작하지
않는다. 예약 작업 확인 기준은 `VignettePublicRuntime``LastTaskResult=0`
`public-runtime-watchdog.failcount=0`이다.
If API health fails with `environment`, `db`, or auth configuration errors,
inspect `apps/api/.env`; do not copy secret values into scripts or task
arguments.

View file

@ -39,13 +39,13 @@
```dotenv
ENGINE_MODE=claude_api
ANTHROPIC_API_KEY=sk-ant-... # 연구팀/기관 발급 키
# ENGINE_URL 은 claude_api 모드에서 사용하지 않음 (host.docker.internal 라인 무시됨)
ENGINE_URL=http://host.docker.internal:9099
# ANTHROPIC_API_KEY는 엔진 게이트웨이 호스트에 연구팀/기관 발급 키로 주입
```
- 코드는 `claude_api`**기본값으로 지원**한다(`apps/api/app/config.py``engine_mode` 기본이 `claude_api`). 별도 개발 없이 env 전환으로 동작한다.
- 코드는 `claude_api`게이트웨이 provider로 지원한다. 관리자는 `/admin/ai` 또는 `/settings`에서 게이트웨이가 Anthropic API로 조회한 모델과 지원 추론 강도만 선택할 수 있다. 키가 없거나 조회가 실패하면 해당 provider는 선택 불가 상태가 되고 잘못된 설정은 저장되지 않는다.
- 비용: 회의 기록상 실사용 지난달 ~$10, 20명 액티브 시 최대 $100 이내 추정. 과금은 `ANTHROPIC_API_KEY` 계정으로 발생 → 연구팀/기관 계정 사용 권장. 관리자 UI에서 `ADMIN_USAGE_BUDGET_USD`로 예산 표시 가능.
- `claude_cli` 게이트웨이(`apps/api/engine_gateway/`), 로컬 `claude -p` 상주풀, `ENGINE_READY_TTL_SECONDS` 튜닝은 **모두 불필요**해진다.
- 개인 `claude` CLI와 상주풀은 불필요해지지만 `apps/api/engine_gateway/` 자체는 Anthropic API 모델 탐색·실행을 위해 계속 필요하다. 학교 서버 또는 별도 엔진 호스트에 게이트웨이를 실행하고 API 키는 그 프로세스에만 주입한다.
---

View file

@ -98,7 +98,7 @@ claude -p \
### 1.5 어댑터 정규화 계약 [F-15 해소]
- `LLMEngine` 인터페이스: 정규화 이벤트 스키마(`delta.text` / `result` / `cost`) + **정규화 cost 규약**(정액=토큰×참조단가 shadow cost, 종량제=실 total_cost_usd). 두 경로 cost 의미 통일 → 재귀학습·텔레메트리 일관성(요구5).
- **contract test DoD화:** 동일 입력 → claude_cli 경로와 messages_api 경로가 동일 정규화 출력. Phase 0~1에 둘 다 구현.
- **contract test DoD화:** 동일 입력 → claude_cli 경로와 claude_api 경로가 동일 정규화 출력. Phase 0~1에 둘 다 구현.
### 1.6 역할별 인증/플래그 분기 [F-39 해소]
| AI | 엔진 | 인증/모드 |