다크 UI 시안과 운영 문서 갱신

This commit is contained in:
Yun Chan 2026-06-28 23:53:58 +09:00
parent 30441bd93b
commit d9458d0e40
8 changed files with 269 additions and 41 deletions

View file

@ -6,7 +6,7 @@
분류: **B1 비차단 폴리시** · **B2 환경 제약(증거 생산 불가)** · **B3 소유자 결정** · **B4 외부 거버넌스**
> **B0. 원천문서 갭 분석 (2026-06-26 추가)** — 한신대 산학협력 원천문서 5종 정독으로 도출한 "부족한 부분"(critical 3 / high 4 / medium+ 6)은 **SSOT 대시보드** `docs/dev_dashboard.html` "원천문서 갭 분석" 섹션과 상세 `docs/ops/source-docs-gap-analysis-2026-06-26.md`에서 추적한다. C1 사례개념화 산출물은 저장형 워크시트와 외부 루브릭 scaffold까지 3차 구조를 만들었고, C2 위기개입 프로토콜은 1차 구조, C3 이론모드는 2차 명시 선택 UI까지 만들었다. 콘텐츠 정의는 임상팀(구훈정·어유경) 소유라 코드는 구조를 선제 구축하되 임상 문안과 평가기준은 외부 정의로 받는다.
> **B0. 원천문서 갭 분석 (2026-06-26 추가)** — 한신대 산학협력 원천문서 5종 정독으로 도출한 "부족한 부분"(critical 3 / high 4 / medium+ 6)은 **SSOT 대시보드** `docs/dev_dashboard.html` "원천문서 갭 분석" 섹션과 상세 `docs/ops/source-docs-gap-analysis-2026-06-26.md`에서 추적한다. C1 사례개념화 산출물은 저장형 워크시트, 외부 루브릭 scaffold, 교수자 수동 검수 상태 저장까지 4차 구조를 만들었고, C2 위기개입 프로토콜은 1차 구조, C3 이론모드는 2차 명시 선택 UI까지 만들었다. 콘텐츠 정의는 임상팀(구훈정·어유경) 소유라 코드는 구조를 선제 구축하되 임상 문안과 평가기준은 외부 정의로 받는다.
---
@ -21,6 +21,7 @@
- [x] **회기 아카이브 저장/복원 API** — (2026-06-28 처리) `/learn/history``보관됨`을 실제 학습자별 보기 상태로 연결했다. 종료 회기는 `POST /sessions/{id}/archive`/`restore`로 보관·복원하고, `app.session_archive_state`는 삭제가 아니라 `archived_at`/`updated_at`만 저장한다. `LearnerSessionSummary.archived`, `SessionArchiveResponse`, `LearnerDashboardOverview.archived_sessions`를 OpenAPI에 고정했고, 보관된 회기는 리뷰 대기 행동 큐에서 빠진다. 회기·턴·리뷰·공유 링크·연구/감사 증거는 삭제하지 않는다. **검증: `python -B -m pytest app/ -q` 178 passed, `python -B -m pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1` 6 passed.**
- [x] **OpenAI TTS voice preset DB map 연결** — (2026-06-28 처리) `app.persona_voice_map`의 OpenAI row를 `/voice/ws` TTS voice 선택에 연결했다. 명시 query preset은 DB map보다 우선하고, 세션 바인딩은 `app.sessions.persona_id/persona_version`으로 voice map을 찾으며, dev persona 생성은 catalog persona id/version map을 사용한다. seed materializer는 기본 OpenAI voice map을 `ON CONFLICT DO NOTHING`으로 생성하고, dev 런타임 스키마 보강은 기존 DB의 `persona_voice_map` 누락도 복구한다. OpenAI가 아닌 provider row는 기존 persona-code fallback으로 안전하게 흡수한다. **검증: `pytest app/test_runtime_policy.py app/test_persona_review.py app/test_voice_service.py app/test_voice_ws.py -q` 75 passed, `pytest app/ -q` 178 passed, `pytest engine_gateway/ -q` 11 passed.**
- [x] **빈상태 컬럼 높이 여백** — (2026-06-28 처리) session-review 0건 상태는 1280px 이상에서 3컬럼 masonry를 쓰지 않고 빈 리뷰 전용 2컬럼 순차 레이아웃으로 전환한다. 가짜 기록·장식 콘텐츠를 넣지 않고, `EMPTY_REVIEW_SESSION_ID` fixture와 `session-review-empty` visual gate로 390/720/861/900/1024/1280/1440 전 폭을 검증한다. session-prestart와 learner-home은 기존 gate 대상에 남겨 함께 무회귀 확인했다. **검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed, `npx playwright test e2e/session-layout.spec.ts e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 6 passed.**
- [ ] **다크 UI v2 시안 실제 적용 라운드** — (2026-06-28 PLAN) `03-session-responsive-v2-dark-unified.png`에서 확인한 단일 dark surface 방향을 실제 코드 UI로 옮긴다. 우선순위는 P1 라이브 상담 세션(`Session.tsx`/`session.css`) → P2 회기 리뷰(`SessionReview.tsx`/`session-review.css`) → P3 학습자 홈(`LearnerHome.tsx`/`learner-home.css`) 순서다. 생성 PNG는 앱에 직접 넣지 않고 레이아웃·밀도·톤 기준으로만 사용한다. 실행 계획과 검증 게이트는 `docs/ops/dark-ui-refresh-implementation-plan-2026-06-28.md`에 고정했다. **완료 조건: 각 화면의 실제 Playwright 캡처가 v2 방향과 맞고, `typecheck`, `build`, 관련 focused E2E, `layout-visual-gate` 통과. DONE 표기는 새 캡처와 테스트 증거 이후에만 가능.**
- [x] **운영 티켓·헬스 샘플 이력 저장 모델 1차**`/admin` 운영 홈과 `/admin/tickets``app.admin_health_event`·`app.support_ticket` 실제 DB 모델에 연결됐다. `/users/support-tickets`는 인증 사용자의 문제·불만·장애 접수를 저장하고, `/admin/tickets`는 미해결 큐·우선순위·상태 변경을 처리한다. `/admin/uptime`은 상시 SLA가 아니라 관리자 콘솔이 관측한 헬스 샘플 정상률/중단 이력을 반환한다. 가짜 티켓 수·가짜 다운타임 숫자는 계속 금지. **검증: `python -m pytest app/test_admin_ops.py -q` 3 passed, `npm run generate:api-types`, `npm run typecheck`.**
- [x] **사용자별 운영 티켓 조회 UI** — (2026-06-28 처리) 학습자 Settings에 `지원 요청` 섹션을 추가해 본인이 접수한 티켓의 상태·카테고리·우선순위·담당 그룹·해결 메모를 읽기 전용으로 확인한다. `GET /users/support-tickets``reporter_id = current user` 기준으로만 조회하고, 본문과 신고자 이메일/이름은 응답·감사 검증에서 제외한다. **검증: `python -B -m pytest app/test_user_support_tickets.py app/test_admin_ops.py app/test_runtime_policy.py -q` 29 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/settings.spec.ts --project=chromium-desktop --workers=1` 7 passed, `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 --grep "operation tickets"` 1 passed.**
- [x] **상시 synthetic monitor/cron 샘플러 1차** — (2026-06-28 처리) `/admin/health`와 같은 헬스 계산을 `record_admin_health_sample()`로 공통화하고, 브라우저 세션 없이 `scripts/record-admin-health-sample.py``app.admin_health_event`에 서비스별 샘플을 append한다. Windows 예약 실행은 `scripts/install-health-sampler-task.ps1``VignetteAdminHealthSampler` Scheduled Task로 설치한다. 이 값은 SLA가 아니라 synthetic sampler가 관측한 최근 샘플 이력이다. **검증: `python -B -m py_compile apps/api/app/routes/admin.py apps/api/app/test_admin_ops.py scripts/record-admin-health-sample.py`, `python scripts/record-admin-health-sample.py --help`, `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-health-sampler-task.ps1 -PrintOnly -IntervalMinutes 7`, `python -B -m pytest app/test_admin_ops.py app/test_runtime_policy.py app/test_user_support_tickets.py -q` 31 passed, 로컬 one-shot `recorded_count=5`, `status=ok`, `engine_mode=claude_cli`.**
@ -31,7 +32,7 @@
- [ ] **운영 티켓 자동 분류·처리 후속** — Claude Recipe headless 자동 수정 후보, 관리자 승인 후 이슈 등록·PR/작업 스레드 생성, 자동 담당 그룹 배정, 우선순위 escalation, 처리 결과 audit trail 확장은 아직 설계/승인 필요. 자동 수정은 운영자 승인 전까지 실행하지 않는다.
- [x] **learner-home 로딩 스켈레톤 밀도(390)** — (2026-06-26 처리) '연습 대상' 로딩 스켈레톤을 빈 div 단일 셰이머에서 실제 카드 구조(마크 박스 + 이름/메타/요약 2줄 플레이스홀더)를 모사하도록 밀도 보강. `LearnerHome.tsx` (`.lh-skel__box`/`.lh-skel__line*`), reduced-motion 분기 동반 갱신. **검증: `npm run typecheck` PASS + `vite build` PASS.**
검증 기준: 변경 후 `npm run typecheck`, `e2e/layout-visual-gate.spec.ts`(현재 9/9), 레이아웃 포커스 E2E(54), `e2e/session-layout.spec.ts`(8/8) 무회귀. 이번 후속 라운드는 빈상태 레이아웃(`typecheck`, `build`, layout gate 9/9, session-layout+session-review desktop 6/6), 사용자별 티켓 조회 UI(backend 29, settings 7, admin ticket 1), synthetic health sampler(backend 31, one-shot recorded_count 5), health retention/rollup(backend 36, dry-run OK), 티켓 중복 저장·수동 연결(backend 38, API types/typecheck/build OK, admin tickets E2E 2)을 확인했다.
검증 기준: 변경 후 `npm run typecheck`, `e2e/layout-visual-gate.spec.ts`(현재 9/9), 레이아웃 포커스 E2E(54), `e2e/session-layout.spec.ts`(8/8) 무회귀. 이번 후속 라운드는 빈상태 레이아웃(`typecheck`, `build`, layout gate 9/9, session-layout+session-review desktop 6/6), 사용자별 티켓 조회 UI(backend 29, settings 7, admin ticket 1), synthetic health sampler(backend 31, one-shot recorded_count 5), health retention/rollup(backend 36, dry-run OK), 티켓 중복 저장·수동 연결(backend 38, API types/typecheck/build OK, admin tickets E2E 2), M2 route-level seed recall 주입(py_compile, M2 focused 68)을 확인했다.
---
@ -81,21 +82,21 @@
- P1 서연 음성 아트 PoC: Higgs v3 무참조 synthetic seed + 5개 정서/속도 변주(mp3/wav) 생성. 실존 reference voice 미사용. `/voice/ws`에는 dev-only sample TTS provider로 연결했고 live text_turn smoke에서 `p1-sample-poc` binary 20 chunks/78,573 bytes 수신 확인. 이 provider는 TTS override만 담당한다(마이크/STT는 OpenAI 키 필요). 프로덕션 탑재는 라이선스/권리 결정 전 금지.
- 부수: RLS smoke 스크립트 cleanup RLS 컨텍스트 버그 수정, `vignette_app`(NOBYPASSRLS) dev 역할 프로비저닝.
- 평가 SSOP 2차: `TurnRecord.evaluation``app.feedback_scores`/`turn_technique`/`turn_client_state`/`supervisor_comment`/`alternative_utterance`로 정규화 적재·복원하고, submit/stream/voice 저장 경로를 `app.turn_runtime`으로 공통화했다. 원시 평가 테이블은 evaluator/admin RLS로 제한하고 서버가 evaluator 컨텍스트로 적재한다. `app.turns` insert는 `RETURNING id` 확인으로 silent success 제거. **검증: `pytest app/test_evaluation_persistence.py -q` 6 passed, 관련 회귀 45 passed, `pytest app/` 119 passed, `pytest engine_gateway/` 7 passed.**
- 라이브 코칭 1차 + 이력 UI/RAG sync + 학습자 리뷰 2열 UI: `app/services/live_coach.py`, `data/kb/live_coaching_workbook_0615.json`, `data/kb/live_coaching_sources/*.json`, `POST /sessions/{id}/live-coach`, `GET /sessions/{id}/live-coach`, `POST /kb/live-coach/source-packs/sync`, `app.live_coach_events`를 추가해 0615 사례개념화 워크북·DSM·공식 지침 요약 기반 코칭을 구조화했다. 세션 UI는 코칭 모드에서 AI 코치 아바타 말풍선, 근거 모달, 학습자 발화 우측 `C` 마커, 채팅 위 스크롤 이력 오버레이를 제공한다. 저장 payload는 PII 마스킹 excerpt와 코칭 구조화 JSON으로 제한한다. 공식 자료는 허가된 source pack의 version/citation/summary로만 코칭에 넣고 장문 원문·공식 문항 재현은 하지 않는다. 같은 source pack은 `app.services.source_pack_sync`로 evaluator 전용 RAG에 증분 색인하며, active `content_hash`가 바뀌면 `kb.document.version`을 최신+1로 올려 UNIQUE 충돌을 피한다. 학습자 `SessionReview` 데스크톱은 좌측 축어록 타임라인과 우측 요약·감정·흐름·루브릭·강점·개선점·pre/post·워크시트·피드백 작업열의 2열 구조로 재배치했고, 교수자/모바일 레이아웃은 기존 규칙을 유지한다. **검증: `python -m compileall app -q`, 과거 `pytest app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 27 passed, source sync `python -B -m pytest -p no:cacheprovider app/test_live_coach_sources.py -q` 4 passed, `py -3.11 scripts\sync-persona-sources.py --help`, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npm run e2e -- e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts` 15 passed. 2열 UI 검증: `npm run typecheck`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed, `session-review__1440-desktop.png` 직접 확인. 로컬 DB smoke: sync 4 sources/15 chunks embedded, 재호출 skipped_unchanged 4, `official_suicide_risk_guidelines` source_id 필터 검색 성공.**
- 계약 SSOT 4차: FastAPI OpenAPI를 `apps/web/src/lib/api.gen.ts`로 생성하는 `npm run generate:api-types`와 stale 체크용 `npm run check:api-types`를 유지한다. `TurnResponse`/`SessionEndResponse`에 이어 `MeResponse`, `AuthConfigResponse`, `ConsentResponse`, `PersonaSummary`, `PersonaReviewSummary`, admin/user/engine DTO, review leaf DTO, `CrisisResourceResponse`, teacher safety/growth leaf DTO, `LearnerSessionsResponse`, `LearnerDashboardResponse`, `SessionReviewResponse`, worksheet aggregate, `TeacherDashboardResponse`, `TeacherSessionSummary`, `TeacherLearnerGrowth`, `SessionStartResponse`, `SessionDetailResponse`, `SessionDetailTurn`를 생성 타입 alias로 전환했다. 세션 응답 stage는 `StageLabel` enum으로 OpenAPI에 고정했고, generated optional/default 차이는 UI form state, notification default helper, 배열 렌더링 fallback로 흡수한다. `.github/workflows/api-contract.yml`은 PR/master push에서 `npm run check:api-types` drift를 막는다. **검증: `npm run check:api-types`, `npm run typecheck`, `npm run build`, `python -m pytest app/test_session_turn_persistence.py -q` 16 passed, `learner.spec.ts + session-review.spec.ts + teacher.spec.ts + layout-visual-gate.spec.ts` 32 passed.**
- 라이브 코칭 1차 + 이력 UI/RAG sync + 학습자 리뷰 2열 UI: `app/services/live_coach.py`, `data/kb/live_coaching_workbook_0615.json`, `data/kb/live_coaching_sources/*.json`, `POST /sessions/{id}/live-coach`, `GET /sessions/{id}/live-coach`, `POST /kb/live-coach/source-packs/sync`, `app.live_coach_events`를 추가해 0615 사례개념화 워크북·DSM·공식 지침 요약 기반 코칭을 구조화했다. 세션 UI는 코칭 모드에서 AI 코치 아바타 말풍선, 근거 모달, 학습자 발화 우측 `C` 마커, 채팅 위 스크롤 이력 오버레이를 제공한다. 저장 payload는 PII 마스킹 excerpt와 코칭 구조화 JSON으로 제한한다. 공식 자료는 허가된 source pack의 version/citation/summary로만 코칭에 넣고 장문 원문·공식 문항 재현은 하지 않는다. 같은 source pack은 `app.services.source_pack_sync`로 evaluator 전용 RAG에 증분 색인하며, active `content_hash`가 바뀌면 `kb.document.version`을 최신+1로 올려 UNIQUE 충돌을 피한다. 2026-06-28 refactor-governance P4에서는 live turn의 프로세스 로컬 source pack snapshot은 유지하되 관리자 sync/CLI가 `refresh=True`로 캐시를 비우고 repo 파일을 다시 읽게 해 stale `content_hash` 비교를 막았다. 학습자 `SessionReview` 데스크톱은 좌측 축어록 타임라인과 우측 요약·감정·흐름·루브릭·강점·개선점·pre/post·워크시트·피드백 작업열의 2열 구조로 재배치했고, 교수자/모바일 레이아웃은 기존 규칙을 유지한다. **검증: `python -m compileall app -q`, 과거 `pytest app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 27 passed, source sync `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py -q` 9 passed, H2 related `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 44 passed, `py -3.11 -X utf8 scripts\sync-persona-sources.py --help`, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npm run e2e -- e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts` 15 passed. 2열 UI 검증: `npm run typecheck`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed, `session-review__1440-desktop.png` 직접 확인. 로컬 DB smoke: sync 4 sources/15 chunks embedded, 재호출 skipped_unchanged 4, `official_suicide_risk_guidelines` source_id 필터 검색 성공.**
- 계약 SSOT 7차: FastAPI OpenAPI를 `apps/web/src/lib/api.gen.ts`로 생성하는 `npm run generate:api-types`와 stale 체크용 `npm run check:api-types`를 유지한다. `TurnResponse`/`SessionEndResponse`에 이어 `MeResponse`, `AuthConfigResponse`, `ConsentResponse`, `PersonaSummary`, `PersonaReviewSummary`, admin/user/engine DTO, review leaf DTO, `CrisisResourceResponse`, teacher safety/growth leaf DTO, `LearnerSessionsResponse`, `LearnerDashboardResponse`, `SessionReviewResponse`, worksheet aggregate, `TeacherDashboardResponse`, `TeacherSessionSummary`, `TeacherLearnerGrowth`, `SessionStartResponse`, `SessionDetailResponse`, `SessionDetailTurn`를 생성 타입 alias로 전환했다. 세션 응답 stage는 `StageLabel` enum으로 OpenAPI에 고정했고, 69차에서는 `ReviewPhaseSegment.key/label`, `SessionReviewResponse.reachedPhase`, `EvaluationSummary.stage`, 평가 트리거 응답 stage, `TeacherSessionSummary.stage`, `TeacherGrowthPoint.stage`도 union/null 계약으로 축소했다. 71차에서는 공개 공유 응답 `PublicSessionShareResponse.reachedPhase`와 live-coach 이력 `LiveCoachEvent.stage``StageLabel | null` 계약으로 축소했다. 72차에서는 `app/stage_contract.py``StageLabel`, `ReviewPhaseKey`, legacy stage code 정규화, unknown→null 판단, 리뷰 phase key 매핑을 소유하게 해 `turn_runtime.py`, `session_read_model.py`, `routes/eval.py`의 중복 stage 규칙을 제거했다. generated optional/default 차이는 UI form state, notification default helper, 배열 렌더링 fallback, live-coach `단계 미상` 표시로 흡수한다. `.github/workflows/api-contract.yml`은 PR/master push에서 `npm run check:api-types` drift를 막는다. **검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_session_turn_persistence.py -q` 50 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_share.py app/test_live_coach_sources.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py -q` 43 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_stage_contract.py app/test_session_share.py app/test_session_turn_persistence.py app/test_evaluation_persistence.py app/test_teacher_dashboard.py -q` 42 passed, `py -3.11 -X utf8 -m py_compile apps/api/app/stage_contract.py apps/api/app/turn_runtime.py apps/api/app/session_read_model.py apps/api/app/routes/eval.py apps/api/app/routes/sessions.py apps/api/app/routes/share.py apps/api/app/services/live_coach.py`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, 과거 `learner.spec.ts + session-review.spec.ts + teacher.spec.ts + layout-visual-gate.spec.ts` 32 passed.**
- 위기게이트 109 1차: 실제 자해·자살 신호는 LLM/엔진 호출 전 중단하고 `crisis_resource(109)``conversation_stopped`를 REST/SSE/voice 응답에 싣는다. `app.safety_events` detail 적재와 교수자 대시보드 안전 알림 큐까지 연결했다. 안전 이벤트 DB insert payload도 회귀화했다. 임상 문안, 실시간 push/메일, 지역 자원 확장은 후속. **검증: `pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q` 20 passed, `pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q` 21 passed.**
- 교수자 성장 추적/리뷰 상세 진입 1차: `teacher_dashboard`가 정규화된 턴 평가를 기반으로 학습자별 적절성·라포·기술 사용 추이를 집계하고, 교수 콘솔에 최근 회기/항목별 성장 카드와 안전 알림 큐를 함께 표시한다. 종료 회기 리뷰 대기 행과 최근 회기 행은 `/teach/session/:sessionId/review`로 이어지고, 교수자는 회기 리뷰와 사례개념화 워크시트를 읽기 전용으로 검토한다. 학습자 워크시트 저장은 계속 learner 전용이다. **검증: `pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q` 21 passed, `pytest app/test_rbac_idor.py app/test_teacher_dashboard.py -q` 9 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 4 passed.**
- 교수자 검토 상태 1차: `app.session_review_status`에 교수자 회기 검토 상태를 저장하고, teacher dashboard 응답에 `review_status/review_note/reviewed_at`를 포함한다. `PUT /teacher/sessions/{session_id}/review-status`로 교수자 메모 저장과 검토 완료 처리를 수행하며, 완료된 회기는 pending queue에서 제외한다. 교수자/관리자의 `/sessions/{id}/review` 읽기 허용은 유지하고, 학습자 워크시트 저장은 learner 전용으로 계속 제한한다. 교수 콘솔 검토 큐는 숨김 처리하지 않고 상단 triage로 노출하며, 학습자 리뷰 화면에는 교수자 전용 grid 빈칸을 예약하지 않는다. **검증: `pytest app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_learner_dashboard.py -q` 12 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `teacher.spec.ts` 4 passed, `session-review.spec.ts` 2 passed, `layout-visual-gate.spec.ts` 7 passed.**
- 학습자 개인화 대시보드 1차: 교수자 성장 집계와 중복되던 점수/라포/기법 계산을 `app.services.session_metrics`로 공용화하고, `GET /sessions/dashboard`가 본인 세션 기반 `overview/growth/persona_progress/achievements/recent_feedback`를 반환한다. 학습자 홈은 누적 회기, 리뷰 대기, 최근 평가, 라포 흐름, 페르소나별 진행, 최근 피드백, 마지막 세션 리캡을 실제 서버 데이터로 표시한다. 성취는 공식 등급/수료가 아니라 실제 연습 milestone만 사용한다. **검증: `pytest app/test_learner_dashboard.py app/test_teacher_dashboard.py app/test_rbac_idor.py -q` 12 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `learner.spec.ts` 6 passed, `session-review.spec.ts` 2 passed, `teacher.spec.ts` 4 passed, `layout-visual-gate.spec.ts` 7 passed, `session-layout.spec.ts` 4 passed.**
- 페르소나 스튜디오/RAG 저작 10차: 교수 콘솔의 JSON 패널을 분리해 teacher/admin 전용 `/teach/personas` 3열 스튜디오(좌: 저작 흐름·카탈로그·검수 큐, 중앙: RAG 자료 등록·항목형 편집 탭, 우: 검증·RAG 근거·검수 결정)로 옮겼다. 2026-06-27 UI 정리에서 중복 네이티브 파일 입력을 커스텀 SSOT 첨부 드롭존으로 교체하고, 저작 스테퍼·탭별 작성 가이드·검증 dot 정렬을 보강했다. 2026-06-28 항목형 목록 UI 정리에서 자동사고, 회기 시나리오, 말투 filler/verbal tic/nonverbal cue, 역린·금기 응답·금기어를 행 추가/삭제 UI로 전환했고, 저장 payload는 기존 배열 schema를 유지하며 빈 항목만 저장 직전에 제거한다. 프롬프트 탭은 raw JSON textarea 대신 L1 카드·인적 범주·임상 배경·말투·수치 파라미터·역린·회기 시나리오·추가 계약 섹션으로 같은 draft 데이터를 라벨형 검토 UI에 표시한다. 레이아웃 근거와 생성 시안은 `docs/ops/layout-research-2026-06-27/persona-dashboard-layout-guideline.md`에 묶었다. `POST /personas/sources`는 첨부/붙여넣기 자료를 PII 마스킹 후 raw 원문 hash-only 증거를 `kb.raw_source_artifact`에 따로 기록하고, sanitized 파생본만 `kb.source/document/chunk`에 evaluator 전용(`visible_to=['evaluator']`, `sensitivity=2`) 근거 문서로 등록한다. `rag.index_document()``sensitivity=3` 또는 raw marker chunk를 DB 접근 전에 차단해 raw 원문이 `kb.chunk`/embedding/FTS에 들어가지 않게 한다. `POST /personas/drafts/generate``source_id` 기반 RAG 검색 결과만 생성 프롬프트에 넘긴다. 생성 응답과 draft `source_provenance`에는 source id, doc/content hash, chunk id, prompt bundle id/version/hash(`persona-draft-rag@2026-06-28.1`)를 남긴다. P1~P7 시스템 페르소나는 DB 저작 카탈로그의 초기 부트스트랩으로 승격했고, `materialize_seed_personas()`는 누락분만 insert해서 교수 편집본이나 `archived` 보관본을 덮어쓰지 않는다. `scripts/materialize-persona-seeds.py`는 같은 seed/version manifest를 dry-run 기본으로 보고하며, `--apply`일 때만 DB pool을 초기화한 뒤 기존 idempotent materializer를 호출한다. `scripts/sync-persona-sources.py``app.services.source_pack_sync`는 repo-managed source pack의 active `content_hash`를 DB에서 비교하고, 변경 시 `kb.document.version`을 최신+1로 색인한다. 교수자는 공개 목록에서 승인본을 다음 버전 draft로 복제해 수정하고, 불필요한 페르소나는 기존 회기 FK 보존을 위해 같은 code family 전체를 `archived`로 보관 처리한다. `app/persona_read_model.py`는 catalog/review/draft/source/evidence DTO와 mapper를 route에서 분리해 schema 이름과 `source/degraded` fail-closed 경계를 유지한다. 암호화 blob/vault 기반 원문 실저장, 임상팀 최종 검수/서면 evidence, 루브릭·이론 콘텐츠 외부화는 후속. **검증: `python -B -m py_compile app/services/rag.py app/routes/personas.py app/routes/kb.py app/test_persona_review.py app/test_live_coach_sources.py`, `python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_live_coach_sources.py -q` 38 passed, `python -B -m py_compile app/persona_read_model.py app/routes/personas.py`, `python -B -m pytest -p no:cacheprovider app/test_persona_review.py -q` 33 passed, `python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_session_turn_persistence.py -q` 55 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `py -3.11 scripts\materialize-persona-seeds.py --json` dry-run `manifest_count=7`, `py -3.11 scripts\sync-persona-sources.py --help`, `npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 8 passed, 프롬프트 탭 포함 `layout-visual-gate.spec.ts` 9 passed, `session-layout.spec.ts` desktop/mobile 8 passed.**
- M1 비언어 이벤트 4차: voice learner turn에 이미 저장하던 `audio_ref`/`silence_ms`/`speech_rate`/`barge_in`을 리뷰 API `nonverbal` 이벤트로 노출하고, 회기 리뷰 축어록에 침묵·발화 속도·끼어듦·음성 입력 칩을 표시했다. 2차에서는 `app.turns.provider_events JSONB``TurnRecord.provider_events`를 추가해 WebSocket control/STT provider 이벤트를 allowlist·size limit 후 보존했고, 3차에서는 저장 전 sanitizer에서 내부 taxonomy `event_type`/`category`를 붙인다. 4차에서는 인증된 회기 리뷰에 한숨·울음·웃음·호흡·운율·배경소음 계열만 한글 label/detail 칩으로 파생 노출한다. raw transcript/text/provider/source/raw type은 응답에서 제외하고, 공개 공유 카드에는 축어록과 provider raw를 싣지 않는다. 실제 provider 기반 한숨·울음 감지, 역량 지표화 정책, live 마이크/WSS 장시간 실측은 후속. **검증: `python -B -m py_compile app/routes/sessions.py app/test_session_turn_persistence.py`, `python -B -m pytest -p no:cacheprovider app/test_voice_ws.py app/test_session_turn_persistence.py app/test_voice_service.py -q` 45 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed.**
- M2 다회기 케이스 아크 6차: DB 세션 생성 시 `(persona_id, learner_id)` 기준 `case_profile`을 upsert하고, `session_no`를 트랜잭션 안에서 원자 증가시키며, `InProcSession.case_id`가 매회 새 `runtime_case_id`가 아니라 안정 `case_id`를 가리키게 수정했다. 시작/submit/stream/voice 경로는 case recall cache를 사용한다. 세션 종료 시 마스킹 축어록 기반 fallback `session_summary.digest``case_profile.case_digest`, `rapport_trajectory`, `alliance_level`을 갱신하고, 다음 회기 seed recall에서 `case_digest`·직전 `session_summary`·client-visible non-contradicted `pinned_fact`를 함께 조립한다. 3차에서는 마스킹된 client-visible 발화에서 `[NAME]`/`[ORG]` identity와 명시적 상담 약속만 보수적으로 `pinned_fact`에 upsert했고, 4차에서는 삽입 또는 값 변경 시 `pinned_fact_history`에 append-only 이력을 남긴다. 5차에서는 명시적 상담 약속 철회/부정만 기존 non-locked `agreement:counseling` fact를 `contradicted`로 격리하고 history reason `contradiction`을 남긴다. 6차에서는 세션 종료 저장 성공 뒤 마스킹된 client-visible 내담자 발화만 `app.turn_embedding`에 BGE-M3 dense/sparse로 `ON CONFLICT (turn_id) DO NOTHING` 색인한다. 새 contradicted fact는 임의 생성하지 않고, 같은 값 재확인은 history를 늘리지 않으며, `locked` fact는 건드리지 않는다. learner-owned `case_profile` 기준 RLS insert/update를 추가했고, 관계갈등·위기·임상 추론은 자동 pinning/모순 처리에서 제외했다. 관계·임상 fact 승격 기준과 LLM digest 압축은 후속. **검증: `python -B -m py_compile app/services/rag.py app/routes/sessions.py app/test_session_memory.py`, `python -B -m pytest -p no:cacheprovider app/test_session_memory.py -q` 14 passed, `python -B -m pytest -p no:cacheprovider app/test_session_memory.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py app/test_runtime_policy.py -q` 67 passed.**
- C1 사례개념화 워크시트 3차: `SessionReviewResponse.caseWorksheet`와 리뷰 화면 카드가 탐색 11항목·호소 5영역·인지삼제/감정·보호/방해·생물심리사회 목표 초안을 축어록 근거 기반으로 제공한다. 학습자가 편집한 저장본은 `PUT /sessions/{id}/review/worksheet``app.case_worksheet`에 저장되고, 이후 `GET /review``saved_by_learner` 저장본을 자동 초안보다 우선 반환한다. 2026-06-28에는 `data/rubrics/case-worksheet-rubric.json`, schema, loader/validator service, `scripts/check-case-worksheet-rubric.py`를 추가해 임상팀 확정 루브릭을 외부 JSON으로 받을 scaffold를 만들었다. 현재는 `status=scaffold_only`, `scoring_enabled=false`이며 5개 section/28개 item key와 sha256 provenance만 검증한다. 64차 refactor-governance 패스에서 워크시트 템플릿 key source를 `CASE_WORKSHEET_SECTION_SPECS`/`case_worksheet_template_item_keys()`로 명시해 CLI/test가 더미 턴 없이 같은 생성 spec을 검증하게 했다. 임상팀 확정 루브릭 콘텐츠, AI 추출/채점 적용·calibration, 교수자 승인/반려/수정요청 검수는 후속 gate다. **검증: `pytest app/test_session_turn_persistence.py app/test_rbac_idor.py -q` 25 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_case_worksheet_rubric.py -q` 4 passed, `py -3.11 -X utf8 scripts/check-case-worksheet-rubric.py --json` PASS, `py -3.11 -m py_compile apps/api/app/services/case_worksheet_rubric.py apps/api/app/test_case_worksheet_rubric.py scripts/check-case-worksheet-rubric.py`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, 로컬 API smoke 세션 생성→저장→`GET /review` `saved_by_learner:local smoke saved worksheet`.**
- M2 다회기 케이스 아크 8차: DB 세션 생성 시 `(persona_id, learner_id)` 기준 `case_profile`을 upsert하고, `session_no`를 트랜잭션 안에서 원자 증가시키며, `InProcSession.case_id`가 매회 새 `runtime_case_id`가 아니라 안정 `case_id`를 가리키게 수정했다. 시작/submit/stream/voice 경로는 case recall cache를 사용한다. 세션 종료 시 마스킹 축어록 기반 fallback `session_summary.digest``case_profile.case_digest`, `rapport_trajectory`, `alliance_level`을 갱신하고, 다음 회기 seed recall에서 `case_digest`·직전 `session_summary`·client-visible non-contradicted `pinned_fact`를 함께 조립한다. 3차에서는 마스킹된 client-visible 발화에서 `[NAME]`/`[ORG]` identity와 명시적 상담 약속만 보수적으로 `pinned_fact`에 upsert했고, 4차에서는 삽입 또는 값 변경 시 `pinned_fact_history`에 append-only 이력을 남긴다. 5차에서는 명시적 상담 약속 철회/부정만 기존 non-locked `agreement:counseling` fact를 `contradicted`로 격리하고 history reason `contradiction`을 남긴다. 6차에서는 세션 종료 저장 성공 뒤 마스킹된 client-visible 내담자 발화만 `app.turn_embedding`에 BGE-M3 dense/sparse로 `ON CONFLICT (turn_id) DO NOTHING` 색인한다. 7차에서는 submit/stream 공통 `_prepare_turn_context()` 경계를 만들고 DB seed recall·pinned fact가 다음 턴 EngineMessage L2/L4에 raw 이름 마스킹 상태로 주입되는 route-level 회귀를 추가했다. 8차에서는 `SessionDigestInput`/`SessionDigestResult`/`SessionSummaryWrite`로 종료 digest 입력·fallback 결과·DB write 인자 경계를 명시했다. future LLM digest worker 입력은 client-visible masked turn과 open thread로 제한하고, 종료 상태 수치·평가 payload·CCD·raw text는 압축 prompt에 넣지 않는다. `/sessions/{id}/end``digest_pending`은 CompressionJob 생성 여부를 알리는 비동기 압축 필요 신호로 유지한다. 새 contradicted fact는 임의 생성하지 않고, 같은 값 재확인은 history를 늘리지 않으며, `locked` fact는 건드리지 않는다. learner-owned `case_profile` 기준 RLS insert/update를 추가했고, 관계갈등·위기·임상 추론은 자동 pinning/모순 처리에서 제외했다. 관계·임상 fact 승격 기준과 LLM digest worker 실행/품질평가/재압축은 후속. **검증: `py -3.11 -X utf8 -m py_compile apps\api\app\services\memory.py apps\api\app\routes\sessions.py apps\api\app\session_persistence.py apps\api\app\test_session_memory.py`, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_memory.py -q` 17 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_memory.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py app/test_runtime_policy.py -q` 74 passed, `npm run check:api-types`, `npm run typecheck`.**
- C1 사례개념화 워크시트 4차: `SessionReviewResponse.caseWorksheet`와 리뷰 화면 카드가 탐색 11항목·호소 5영역·인지삼제/감정·보호/방해·생물심리사회 목표 초안을 축어록 근거 기반으로 제공한다. 학습자가 편집한 저장본은 `PUT /sessions/{id}/review/worksheet``app.case_worksheet`에 저장되고, 이후 `GET /review``saved_by_learner` 저장본을 자동 초안보다 우선 반환한다. 2026-06-28에는 `data/rubrics/case-worksheet-rubric.json`, schema, loader/validator service, `scripts/check-case-worksheet-rubric.py`를 추가해 임상팀 확정 루브릭을 외부 JSON으로 받을 scaffold를 만들었다. 현재는 `status=scaffold_only`, `scoring_enabled=false`이며 5개 section/28개 item key와 sha256 provenance만 검증한다. 64차 refactor-governance 패스에서 워크시트 템플릿 key source를 `CASE_WORKSHEET_SECTION_SPECS`/`case_worksheet_template_item_keys()`로 명시해 CLI/test가 더미 턴 없이 같은 생성 spec을 검증하게 했다. 73차에서는 기존 `app.session_review_status``worksheet_status`/`worksheet_note`/`worksheet_reviewed_at`을 추가하고, `PUT /teacher/sessions/{id}/review-status`와 교수자 `SessionReview` 카드에서 승인·수정요청·반려 수동 판정을 저장/표시한다. 임상팀 확정 루브릭 콘텐츠, AI 추출/채점 적용·calibration, 승인 후 잠금·재제출 정책, 재귀학습 데이터셋 approved 연계는 후속 gate다. **검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_case_worksheet_rubric.py -q` 40 passed, `py -3.11 -X utf8 scripts/check-case-worksheet-rubric.py --json` PASS, `py -3.11 -X utf8 -m py_compile` focused files, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed, 로컬 API smoke 세션 생성→저장→`GET /review` `saved_by_learner:local smoke saved worksheet`.**
- C3 이론모드 2차: `theory_mode``TurnContext`/sessions/voice/evaluator뿐 아니라 `build_turn_messages`의 엔진 메시지까지 전달된다. 프론트는 `persona.theory_target` 기준 기본값을 유지하되, 세션 시작 전 `humanistic`/`cbt`/`integrative` segmented control로 학습자가 명시 선택하고 `POST /sessions``theory_mode`로 보낸다. CBT 체인·이론부합 루브릭은 후속. **검증: `python -B -m pytest -p no:cacheprovider app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 31 passed, `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --workers=1` 4 passed, `npx playwright test e2e/session-layout.spec.ts --project=chromium-mobile --workers=1` 4 passed, `npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1` 1 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed.**
- M3 인증 claim 1차: Google/SAML/dev-login이 `AUTH_EMAIL_COHORT_MAP`·`AUTH_DOMAIN_COHORT_MAP` 및 SAML cohort claim을 `cohort_ids`로 전달하고, DB `app_user.external_id`는 provider subject(`google:`/`saml:`/`dev:`) 기반으로 저장한다. 운영 SAML 서명검증, 기관 claim schema/test tenant, deprovisioning audit은 후속. **검증: `pytest app/test_auth_providers.py -q` 30 passed, `pytest app/ -q` 178 passed.**
- X2 예산 경고/저비용 평가 라우팅/evaluator cache 관측·일별 비용 추이·모델별 비용 검증 리포트 2차: `ADMIN_USAGE_BUDGET_USD` 설정값을 기준으로 `GET /admin/usage``budget.status=disabled|ok|warn|exceeded`, 사용률, 잔여 예산을 반환하고 관리자 콘솔이 예산 상태 배너를 표시한다. 80% 이상 warn, 100% 이상 exceeded. `EVALUATOR_FAST_MODEL`/`EVALUATOR_DEEP_MODEL` 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 gateway default 라우팅을 유지한다. fast/deep evaluator structured 결과는 canonical request SHA-256 기반 인메모리 semantic cache로 재사용하며, 원문 prompt·completion은 저장하지 않고 성공 파싱 결과만 TTL/entry 제한 안에서 캐시한다. `/admin/usage`와 관리자 `AI 비용` 카드는 cache enabled/entries/hits/misses/stores/evictions/requests/hit_rate와 일별 `daily_cost` 추이를 표시하되 cache key·prompt·completion은 노출하지 않는다. `app.services.usage_report``scripts/report-ai-usage.py`는 동일 usage JSON에서 provider/model별 cost share, token share, cost/turn, cost/1k tokens, metered coverage, budget/cache warning을 산출한다. 자동 차단·한도 enforcement 정책은 후속. **검증: `python -B -m py_compile app/services/usage_report.py app/test_usage_report.py ..\..\scripts\report-ai-usage.py`, `python -B -m pytest -p no:cacheprovider app/test_usage_report.py app/test_evaluator_model_routing.py app/test_runtime_policy.py app/test_admin_ops.py -q` 42 passed, `scripts/report-ai-usage.py` sample CLI schema `vignette.ai_usage_model_cost_report.v1` 생성, prior `npm run check:api-types`, `npm run typecheck`, `npm run build`, admin E2E evidence remains valid because API response shape was not changed.**
- H4 LLM call audit/마스킹 2차: 상담 생성(generate/stream)·fast-loop 평가·deep-loop 평가의 외부 LLM 호출 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency metadata만 적재한다. prompt/completion 본문은 저장하지 않고 감사 실패는 상담 루프를 막지 않는다. 2026-06-28에는 한국어 이름/기관 로컬 휴리스틱 마스킹 1차를 추가했고, fast evaluator prompt의 내담자 응답과 client turn `text_masked`도 마스킹본을 쓰게 보강했다. 이어서 `app.services.pii_masking_eval`, `data/privacy/pii-masking-ko-fixtures.json`, `scripts/evaluate-pii-masking.py`로 합성 fixture 15케이스 평가 harness를 추가했고, `data/privacy/pii-masking-eval-input.schema.json``data/privacy/pii-masking-eval-report.schema.json`으로 source/category/severity metadata와 summary-only `technical_dry_run` report 계약을 고정했다. 66차에서는 `제 이름은 김서연입니다`, `보호자 이름은 박민수입니다`, `저는 최하늘입니다` 자연 발화형 이름 라벨·자기소개 케이스와 `이름은 중요하지 않다` negative control을 추가해 라벨 단어만 마스킹하고 실명을 남기던 구멍을 막았다. 기본 CLI JSON은 `masked_text`/`forbidden_remaining` 원문 증거를 제외하며, `소속`/`안내`/`이름` NAME 오탐도 stopword로 보정했다. 로컬 dev-login 실제 `/turn` smoke에서 `audit.llm_call_log`가 8→11로 3행 증가했다(session `2460d56c-a9cb-4a40-a175-9575d510a5e9`). 공개 Google OAuth 실제 `/turn` proof는 별도 B2 항목에 남긴다. **검증: `py -3.11 -X utf8 -m py_compile apps/api/app/services/guardrail.py apps/api/app/services/pii_masking_eval.py apps/api/app/test_pii_masking_eval.py scripts/evaluate-pii-masking.py`, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_pii_masking_eval.py app/test_orchestrator_masking.py app/test_evaluation_persistence.py app/test_session_turn_persistence.py -q` 43 passed, `py -3.11 -X utf8 scripts/evaluate-pii-masking.py --json` 15/15 pass, 과거 전체 기준 `pytest app/ -q` 119 passed.**
- H4 온보딩·동의 게이트 3차: `app_user`에 이름·소속·학과·학년/직위·전화번호·주소/수령지·닉네임·자기소개·아바타 URL·약관/개인정보 동의 시각·버전 필드를 추가했다. 로그인 직후 `/onboarding` 완료 전에는 전역 `OnboardingGate``/onboarding` 외 모든 앱 URL(`/learn`, `/settings`, `/admin`, `/dev/avatar-preview`, `/login` 포함)을 온보딩으로 이동시키고, 온보딩 중에는 공용 셸 메뉴 없이 가입 정보 입력 폼만 보여준다. learner `POST /sessions`와 voice dev persona 시작은 `onboarding_required`를 먼저 확인하고, 온보딩 저장 시 learner `consent_at`도 함께 세팅한다. 아바타는 `/users/me/avatar`에서 PNG/JPG/WebP MIME·시그니처·3MB 제한을 통과한 파일만 `USER_UPLOAD_DIR/profile-avatars`에 저장하고 DB에는 URL만 보관한다. 약관·개인정보 처리방침은 개인정보 보호법·처리방침 작성지침·안전성 확보조치 기준·약관규제법 취지를 반영한 개발 초안으로 `/users/legal-docs`에서 제공한다. 관리자/교수자 권한은 온보딩 화면에서 신청받지 않고 기존 `AUTH_ADMIN_EMAILS`/`AUTH_TEACHER_EMAILS` allowlist 및 관리자 사용자 관리 경로로만 부여한다. 한국어 이름/기관은 로컬 휴리스틱 마스킹과 합성 fixture 평가 harness 1차까지 완료했고, 모델 기반 정밀 ko NER, 실제 운영 말뭉치 기반 오탐/미탐 평가, guardian/legal 최종 서명 동의서 evidence, 공개 Google OAuth 실제 `/turn` proof는 후속. **검증: `pytest app/ -q` 178 passed, `pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/auth.spec.ts --project=chromium-desktop --workers=1` 7 passed, `PLAYWRIGHT_PORT=5174 npm run e2e` 113 passed.**
- H4 LLM call audit/마스킹 2차: 상담 생성(generate/stream)·fast-loop 평가·deep-loop 평가의 외부 LLM 호출 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency metadata만 적재한다. prompt/completion 본문은 저장하지 않고 감사 실패는 상담 루프를 막지 않는다. 2026-06-28에는 한국어 이름/기관 로컬 휴리스틱 마스킹 1차를 추가했고, fast evaluator prompt의 내담자 응답과 client turn `text_masked`도 마스킹본을 쓰게 보강했다. 이어서 `app.services.pii_masking_eval`, `data/privacy/pii-masking-ko-fixtures.json`, `scripts/evaluate-pii-masking.py`로 합성 fixture 15케이스 평가 harness를 추가했고, `data/privacy/pii-masking-eval-input.schema.json``data/privacy/pii-masking-eval-report.schema.json`으로 source/category/severity metadata와 summary-only `technical_dry_run` report 계약을 고정했다. 66차에서는 `제 이름은 김서연입니다`, `보호자 이름은 박민수입니다`, `저는 최하늘입니다` 자연 발화형 이름 라벨·자기소개 케이스와 `이름은 중요하지 않다` negative control을 추가해 라벨 단어만 마스킹하고 실명을 남기던 구멍을 막았다. 70차에서는 `guardrail.mask_pii()` 내부에 선택형 한국어 PII recognizer adapter 경계를 추가했다. adapter는 import-time hard dependency가 아니며 명시 등록 전에는 비활성이고, 실패해도 기존 regex fallback이 마지막 안전망으로 유지된다. fake adapter 테스트는 regex가 못 잡는 별명/기관 span을 `[NAME]`/`[ORG]`로 마스킹하고 같은 문장의 전화번호는 후단 regex가 `[PHONE]`으로 처리하는지, adapter 실패 시에도 fallback이 유지되는지 검증한다. span 중복 처리는 문자별 set 할당 대신 정렬된 `last_end` 비교로 정리했다. 기본 CLI JSON은 `masked_text`/`forbidden_remaining` 원문 증거를 제외하며, `소속`/`안내`/`이름` NAME 오탐도 stopword로 보정했다. 로컬 dev-login 실제 `/turn` smoke에서 `audit.llm_call_log`가 8→11로 3행 증가했다(session `2460d56c-a9cb-4a40-a175-9575d510a5e9`). 공개 Google OAuth 실제 `/turn` proof는 별도 B2 항목에 남긴다. **검증: `py -3.11 -X utf8 -m py_compile apps/api/app/services/guardrail.py apps/api/app/services/pii_masking_eval.py apps/api/app/test_pii_masking_eval.py apps/api/app/test_orchestrator_masking.py scripts/evaluate-pii-masking.py`, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_pii_masking_eval.py app/test_orchestrator_masking.py app/test_evaluation_persistence.py app/test_session_turn_persistence.py -q` 47 passed, `py -3.11 -X utf8 scripts/evaluate-pii-masking.py --json` 15/15 pass, 과거 전체 기준 `pytest app/ -q` 119 passed.**
- H4 온보딩·동의 게이트 3차: `app_user`에 이름·소속·학과·학년/직위·전화번호·주소/수령지·닉네임·자기소개·아바타 URL·약관/개인정보 동의 시각·버전 필드를 추가했다. 로그인 직후 `/onboarding` 완료 전에는 전역 `OnboardingGate``/onboarding` 외 모든 앱 URL(`/learn`, `/settings`, `/admin`, `/dev/avatar-preview`, `/login` 포함)을 온보딩으로 이동시키고, 온보딩 중에는 공용 셸 메뉴 없이 가입 정보 입력 폼만 보여준다. learner `POST /sessions`와 voice dev persona 시작은 `onboarding_required`를 먼저 확인하고, 온보딩 저장 시 learner `consent_at`도 함께 세팅한다. 아바타는 `/users/me/avatar`에서 PNG/JPG/WebP MIME·시그니처·3MB 제한을 통과한 파일만 `USER_UPLOAD_DIR/profile-avatars`에 저장하고 DB에는 URL만 보관한다. 약관·개인정보 처리방침은 개인정보 보호법·처리방침 작성지침·안전성 확보조치 기준·약관규제법 취지를 반영한 개발 초안으로 `/users/legal-docs`에서 제공한다. 관리자/교수자 권한은 온보딩 화면에서 신청받지 않고 기존 `AUTH_ADMIN_EMAILS`/`AUTH_TEACHER_EMAILS` allowlist 및 관리자 사용자 관리 경로로만 부여한다. 한국어 이름/기관은 로컬 휴리스틱 마스킹, optional ko recognizer adapter 배선, 합성 fixture 평가 harness까지 완료했고, 실제 ko recognizer 모델/provider 선정, 운영 말뭉치 기반 오탐/미탐 평가, guardian/legal 최종 서명 동의서 evidence, 공개 Google OAuth 실제 `/turn` proof는 후속. **검증: `pytest app/ -q` 178 passed, `pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/auth.spec.ts --project=chromium-desktop --workers=1` 7 passed, `PLAYWRIGHT_PORT=5174 npm run e2e` 113 passed.**
- RAG warm 동시성 안정화: layout/session E2E가 여러 세션을 빠르게 만들 때 BGE-M3 embedder가 동시에 여러 번 지연 로드되며 `tqdm` lock 예외와 API health/dev-login timeout이 반복됐다. `rag.py`에서 embedder load/encode를 process-wide `RLock`으로 직렬화하고, `sessions.py``_warm_rag_caches`를 semaphore 1개로 제한해 warm task가 상담 요청 경로를 막지 않도록 했다. **검증: `pytest app/ -q` 125 passed, `npx playwright test e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts --project=chromium-single-run --project=chromium-desktop --workers=1` 11 passed.**
- 공개/로컬/Tailnet 로그인 복구: public API 530 원인은 prod에서 개발 전용 `VIGNETTE_VOICE_POC_SAMPLE_TTS=true`가 fail-close된 것과 DB `app.admin_engine_config` 기본 행 부재였다. 2026-06-27 모바일 502는 cloudflared 로그의 `127.0.0.1:8001` origin refused와 일치했다. `scripts/start-public-runtime.ps1`는 public prod 기동 시 샘플 TTS를 강제로 끄고, 운영 DB에는 `claude_cli`/`127.0.0.1:9099` engine config 행을 복구했다. 추가로 dev/Tailnet에서는 public OAuth callback이 로컬/Tailnet 세션으로 붙지 않으므로 Google 버튼과 직접 시작 URL을 `local_oauth_unavailable`로 차단하고 dev-login만 사용한다. public OAuth state는 HttpOnly 쿠키에 묶인 HMAC 서명 토큰으로 복구해 API 재시작 뒤 콜백이 `invalid_state`로 떨어지지 않는다. callback 실패는 비밀값 없는 reason/status로 남기고, 로그인 화면도 token/state/provider/identity 실패 메시지와 reason code를 분리한다. 2026-06-27 18:57 KST에 Tailnet stale Vite `allowedHosts` 재발 상태를 재확인했고, `alpaca-home.taile93291.ts.net`를 Vite 기본 허용 host에도 포함해 수동 Vite 기동 시 403 재발 가능성을 낮췄다. **검증: `https://vignette.chanpaca.net/login` 200, `https://api-vignette.chanpaca.net/health` prod/db/engine true, public Google auth redirect 302 + HttpOnly state cookie, provider callback error → `/login?oauth=access_denied`, `https://alpaca-home.taile93291.ts.net/login` 200 + `/api/health` dev/db/engine true + `/api/auth/config` 200, env 없는 임시 Vite host-header smoke 200, local/Tailnet Google direct는 `local_oauth_unavailable`, local auth E2E 7 passed, Tailnet auth/dev-login E2E 2 passed.**
- Docker dev DB 안전장치: `scripts/dev-up.ps1``vignette-dev-db` healthcheck/pg_isready/role safety를 점검하고, 새 컨테이너 생성 시 `vignette_owner`와 앱 role을 분리해 healthcheck를 붙인다. `infra\.env` compose 필수값 누락도 사전 경고한다. `scripts/dev-down.ps1`은 기본 DB 보존, `-Db` 지정 시 컨테이너 중지로 정리했다. **검증: 현재 Docker DB accepting connections, `vignette_app:false:false`, 기존 컨테이너 healthcheck 없음. 패키징 smoke에서 fresh compose DB는 owner/app role 분리 상태로 API healthy까지 통과했다. 기존 dev DB는 `POSTGRES_USER=vignette` 기반이라 운영 role 전환은 별도 owner migration 확인 후 진행한다.**

View file

@ -0,0 +1,113 @@
# 다크 UI v2 실행 준비 계획 — 2026-06-28
목표: `docs/design-concepts/generated/*-v2-dark-unified.png` 시안을 실제 코드 기반 UI 개선 작업으로 전환한다. 생성 이미지는 방향성 기준일 뿐이며, 제품 UI는 React/CSS 컴포넌트로 구현한다.
## 0. 기준 자산
| 화면 | 기준 이미지 | 구현 기준 |
|---|---|---|
| 학습자 홈 | `docs/design-concepts/generated/02-learner-home-responsive-v2-dark-unified.png` | `apps/web/src/pages/LearnerHome.tsx`, `apps/web/src/pages/learner-home.css` |
| 라이브 상담 세션 | `docs/design-concepts/generated/03-session-responsive-v2-dark-unified.png` | `apps/web/src/pages/Session.tsx`, `apps/web/src/pages/session/session.css` |
| 회기 리뷰 | `docs/design-concepts/generated/04-session-review-responsive-v2-dark-unified.png` | `apps/web/src/pages/SessionReview.tsx`, `apps/web/src/pages/session-review/session-review.css` |
| 토큰 | `apps/web/src/styles/tokens.css` | dark 기본, sage-teal/clay/warn 배급 유지 |
| 프롬프트 근거 | `docs/design-concepts/prompts/vignette-dark-ui-refresh-2026-06-28.md` | UX 원칙과 금지 패턴 |
## 1. 적용 원칙
- 기존 기능을 새 이미지처럼 보이게 숨기지 않는다. 실제 API·세션 상태·저장된 리뷰·코칭 이력만 표시한다.
- generated PNG를 앱에 직접 박지 않는다. 이미지는 레이아웃/밀도/톤 기준으로만 쓴다.
- active 작업 화면은 단일 dark surface로 유지한다. 큰 white/beige 카드, 라이트 패널, 장식용 hero는 금지한다.
- 실시간 상담 중 피드백은 주변 신호다. 중앙 과업은 내담자 얼굴, 자막, 입력이다.
- 회기 리뷰는 점수 폭격이 아니라 근거 기반 회고다. 축어록·근거·워크시트 연결을 우선한다.
- 학습자 홈은 create/continue-first flow다. 첫 화면의 핵심 결정은 새 회기 시작 또는 이어가기다.
## 2. 구현 순서
### P1. 라이브 상담 세션
우선순위가 가장 높다. 사용자가 지적한 light/dark 혼합 문제가 여기서 시작됐고, 현재 학습 몰입도에 직접 영향을 준다.
변경 후보:
- `sx-page--active`, `sx-panel`, `sx-transcript`, `sx-controlbar`의 dark surface 명도 차이를 v2 기준으로 정리한다.
- 좌측 `내담자 컨텍스트``회기 진행`은 더 조밀하게 만들되, 단계/주호소/난도/접근 정보는 유지한다.
- 중앙 stage는 avatar/orb/현재 상태 문구 위계를 분명히 하고, transcript와 시각적으로 같은 dark 작업면으로 맞춘다.
- 우측 rail은 `라이브 신호`, `관찰 신호`, `안전 점검`의 순서를 유지하되 색을 낮추고 amber/red 남용을 막는다.
- mobile/tablet에서는 context strip, stage, transcript, input, controlbar가 한 흐름으로 내려오게 한다.
검증:
- `cd apps/web; npm run typecheck`
- `cd apps/web; npm run build`
- `cd apps/web; npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1`
- `cd apps/web; npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1`
완료 증거:
- `docs/design-verification/full-pages/03-session-active-desktop.png`
- `docs/design-verification/full-pages/03-session-active-tablet.png`
- `docs/design-verification/full-pages/03-session-active-mobile.png`
- 가로 overflow 0, 컨트롤/텍스트 클립 0, 큰 white CSS surface 0.
### P2. 회기 리뷰
세션 다음의 학습 전환 화면이다. v2처럼 dark review workbench로 톤을 맞추되, 빈 상태와 실제 저장본 상태를 분리해야 한다.
변경 후보:
- filled review는 transcript 중심 + insight rail + worksheet workbench 구조를 강화한다.
- empty review는 가짜 데이터 없이 `대기` 상태만 dark panel로 표현한다.
- audio replay, PDF export, supervisor/teacher review, worksheet manual review 상태를 한눈에 보이게 정리한다.
- mobile은 `요약 / 트랜스크립트 / 인사이트` 식의 progressive disclosure를 유지한다.
검증:
- `cd apps/web; npm run typecheck`
- `cd apps/web; npm run build`
- `cd apps/web; npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1`
- `cd apps/web; npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1`
완료 증거:
- filled/empty review 모두 dark surface 유지.
- worksheet, transcript, feedback cards가 서로 겹치지 않는다.
- 교사용 read-only 상태와 학습자 편집 상태가 깨지지 않는다.
### P3. 학습자 홈
세션 시작 전 의사결정 화면이다. 현재 구현을 유지하면서 persona 선택과 이어하기 CTA를 더 명확하게 만든다.
변경 후보:
- persona list는 코드, 이름, 나이/상황, 난도, 접근을 한 행에서 비교 가능하게 유지한다.
- selected-client workspace는 주호소, 현재 회기, 마지막 반응, `이어하기`/`새 회기 시작` CTA를 분명히 둔다.
- 기존 회기, 리뷰 대기, 다음 연습 추천은 실제 데이터 기반으로만 표시한다.
- mobile bottom nav와 compact card의 텍스트 클립을 다시 확인한다.
검증:
- `cd apps/web; npm run typecheck`
- `cd apps/web; npm run build`
- `cd apps/web; npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1`
- `cd apps/web; npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1`
완료 증거:
- create/continue primary action이 첫 화면에서 명확하다.
- 390/720/900/1440 폭에서 persona row, CTA, recent session row 텍스트가 잘리지 않는다.
## 3. 구현 전 체크
- 현재 작업트리가 dirty 상태이므로 UI 구현 전에는 변경 파일 범위를 좁힌다.
- `docs/dev_dashboard.html`에는 PLAN 상태로만 두고, 실제 구현/검증 전까지 DONE으로 바꾸지 않는다.
- API response shape를 바꾸지 않는 UI/CSS 라운드로 제한한다. 만약 DTO 변경이 생기면 `npm run check:api-types`를 추가한다.
- visual gate가 기존 9개 화면을 모두 보므로 한 화면 수정 후에도 전체 gate를 돌린다.
- 새 이미지와 실제 코드가 다를 때는 코드의 실제 기능을 우선한다. 이미지는 방향성이고 기능은 SSOT다.
## 4. 다음 실행 단위
1. `P1 라이브 상담 세션`만 먼저 구현한다.
2. 현재 `03-session-active-*` 캡처와 v2 이미지를 나란히 비교해 CSS delta를 최소화한다.
3. `Session.tsx` 구조 변경은 피하고, 가능한 한 `session.css`에서 surface/density/responsive만 조정한다.
4. 검증 통과 후 새 full-page 캡처를 남기고 SSOT를 DONE/VERIFY 상태로 갱신한다.
5. 그 다음 `P2 회기 리뷰`, `P3 학습자 홈` 순서로 반복한다.