feat: 운영 안정성과 세션 음성 경험 개선
This commit is contained in:
parent
facc4ad2d9
commit
c788343467
95 changed files with 8431 additions and 1785 deletions
|
|
@ -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. 데이터 주권 / 보안
|
||||
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
11
docs/TODO.md
11
docs/TODO.md
|
|
@ -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
|
|
@ -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)은 동작한다.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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·저장 거부가 정상이다.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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 라우트가 프로덕션 라우터에 그대로 포함되어 누구나 페르소나 리그를 열람 가능(코드 주석상 의도지만 배포 노출 여부 재검토 필요).
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 키는 그 프로세스에만 주입한다.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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 | 엔진 | 인증/모드 |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue