vignette/docs/HANDOFF.md
2026-07-31 09:27:51 +09:00

37 KiB
Raw Blame History

Vignette Handoff

Updated: 2026-07-31 KST. 새 세션은 이 문서와 docs/DESIGN_CONCEPT.md를 먼저 읽고 이어가면 된다.

현재 상태

  • Workspace: D:\workspace\vignette
  • 로컬 웹 기본 포트: http://localhost:5173
  • 로컬 API 기본 포트: http://127.0.0.1:8000
  • 로컬 engine gateway 기본 포트: http://127.0.0.1:9099
  • 공개 웹: https://vignette.chanpaca.net
  • 공개 API: https://api-vignette.chanpaca.net
  • 최신 앱 배포 소스: commit 12bdc594970be8fbd8727800c720d14d7d5b4e25 (master, origin push 완료). 사용자 제공 1536×1024 세션 시안 기반의 326px 컨텍스트 레일, 중앙 인물 무대·자막, 357px 라이브 코칭 레일, 116px 제어바와 투명 WebP 식물 장식을 포함한다.
  • 최신 Cloudflare Pages production deploy: 2026-07-31 guarded manual deploy https://ed0cddb4.vignette-b1q.pages.dev, branch main, custom domain entry assets/index-CzevzoM7.js. 세션 청크 Session-BWMFTqgE.js·Session-D7DZVnh0.csssession-botanical/leaf-1.webp가 커스텀 도메인에서 올바른 MIME으로 200을 반환한다. 공개 로그인 실렌더, Google 버튼 2개 enabled, boot diagnostic 0, 브라우저 warning/error 0을 확인했다.
  • 최신 학습 세션 런타임: 실시간 내담자 응답은 VIGNETTE_LIVE_CLIENT_PROVIDER=claude_cli 전용 lane으로 evaluator와 분리하고, 응답 저장·SSE done 뒤 fast-loop 평가는 백그라운드 처리한다. 입력창은 생성·TTS 중에도 초안 작성과 보존이 가능하다. 로컬 -UseHiggsVoice는 설치된 higgs-audio-v3-tts-4b를 P1에 사용하지만 연구/비상업 라이선스 때문에 dev 전용이며, 공개 /voice/healthopenai / gpt-4o-mini-tts로 유지한다.
  • 관리자 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 432 passed, engine gateway 44 passed; 프론트 typecheck·API type drift·production build와 세션 stream 1/1, voice draft/TTS 1/1 focused E2E가 통과했다.
  • X1 재귀학습 export 1차: scripts/export-recursive-dataset.py 기본 read-only dry-run, --write-dataset 명시 시에만 ds.* write, approved export는 steward/legal/IAA gate 없으면 거부.
  • C1 사례개념화 워크시트 2차: SessionReviewResponse.caseWorksheet와 리뷰 화면 편집 카드가 축어록 근거 기반 초안을 제공한다. 학습자 저장본은 PUT /sessions/{id}/review/worksheetapp.case_worksheet에 저장되고, 이후 GET /reviewsaved_by_learner 저장본을 자동 초안보다 우선 반환한다. 검증: focused backend 25 passed, API typecheck/build, 로컬 API smoke saved_by_learner:local smoke saved worksheet. 임상 루브릭, AI 추출/채점, 교수자 검수는 후속.
  • C3 이론모드 1차: theory_mode가 세션·평가·생성 프롬프트까지 흐르고, build_turn_messages는 인간중심/CBT/통합 프레이밍을 엔진 메시지에 넣는다. CBT 체인·이론부합 루브릭·명시적 선택 UI는 후속.
  • H2 평가 정규화: 턴 평가의 대안발화도 app.alternative_utterance에 적재하고 리뷰 hydrate 시 alternative_utterances로 복원한다.
  • X2 예산 경고/저비용 평가 라우팅: ADMIN_USAGE_BUDGET_USD가 0보다 크면 /admin/usage가 budget 상태(ok/warn/exceeded)를 반환하고 /admin이 예산 배너를 표시한다. EVALUATOR_FAST_MODEL/EVALUATOR_DEEP_MODEL을 설정하면 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달한다. 비우면 기존 gateway default 라우팅을 유지한다.
  • 2026-06-28 11:17 KST: /admin/usage prod 503 원인은 provider/model breakdown 쿼리의 ORDER BY tokens_in + tokens_out가 집계 alias가 아니라 원본 컬럼 참조로 해석된 PostgreSQL GroupingError였다. SUM(tokens_in)+SUM(tokens_out) 집계식 정렬로 수정했고, public API 프로세스 127.0.0.1:8001의 인증 HTTP smoke에서 source=database, durable=true, 200을 확인했다.
  • H4 LLM call audit + 동의 게이트: 상담 생성(generate/stream)과 fast/deep 평가의 외부 LLM 호출 직후 audit.llm_call_log에 provider/model/token/cost/inference_geo/latency metadata만 적재한다. prompt/completion 본문은 저장하지 않는다. 로컬 dev-login 실제 /turn smoke에서 audit.llm_call_log가 8→11로 3행 증가했다. 이번 패스에서 app_user.consent_at 기반 learner 동의 수락/철회 API와 세션 시작/voice dev persona 시작 하드게이트를 추가했다. 한국어 이름/기관 NER와 guardian/legal 서명 evidence는 후속.
  • RAG warm 동시성: E2E가 여러 세션을 빠르게 만들 때 BGE-M3 embedder가 동시에 지연 로드되어 tqdm lock 예외와 API health/dev-login timeout이 반복됐다. rag.py embedder load/encode를 process-wide RLock으로 직렬화하고, sessions.py warm task를 semaphore 1개로 제한했다. 최신 검증: layout-visual-gate + session-layout 11 passed.
  • 계약 SSOT 4차: apps/web/src/lib/api.ts의 수기 DTO 중 auth, persona, admin/user/engine, review leaf, teacher safety/growth leaf, learner sessions, session review/worksheet aggregate, teacher dashboard aggregate에 이어 SessionStartResponse, SessionDetailResponse, SessionDetailTurn까지 apps/web/src/lib/api.gen.tsApiSchema alias로 전환했다. 백엔드 세션 응답 stage는 StageLabel enum(라포|탐색|개입|정리)으로 고정했고, generated optional/default 차이는 화면 form state, notification default helper, 배열 렌더링 fallback으로 흡수한다. .github/workflows/api-contract.yml은 PR/master push에서 npm run check:api-types로 FastAPI OpenAPI와 generated type 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/session-review/teacher/layout focused E2E 32 passed.
  • 턴 런타임 리팩터 2차: turn_runtime.finalize_completed_turn으로 REST submit/stream/voice WS의 record_completed_turn + record_safety_event 호출쌍을 공통화했다. gateway /v1/stream route 레벨에서 token/done/error SSE 프레임 contract tests를 추가했다. 검증: focused backend 40 passed.
  • Phase3 artifact checker 강화: scripts/check-phase3-artifacts.py가 CSV enum, KPI report 필수 field, approved export의 PII pass·κ/ICC·withdrawn exclusion·consent scope·file sha256을 검증한다. app/test_phase3_artifact_checker.py 5 tests 추가. 실제 파일럿 evidence와 steward/legal/IAA gate는 그대로 외부 의존이다.
  • 세션 종료 UX/dark theme SSOT 정리: 드래그형 SlideToEnd를 명시 확인 다이얼로그로 교체하고, Topbar/Settings theme 토글을 lib/theme.ts 단일 경로로 통합했다. 저장값이 없으면 dark를 기본으로 두고 앱 부팅 시 theme를 먼저 적용해 초기 flash를 줄인다. API 기본 preference system은 Settings에서 light로 오해하지 않고 현재 초기 테마를 따른다. 회기 리뷰·설정 포함 주요 페이지의 흰 섹션 잔재도 dark 작업면으로 맞췄다. 검증: npm run typecheck, npm run build, npx playwright test e2e/layout-visual-gate.spec.ts e2e/settings.spec.ts e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run --workers=1 22 passed.
  • 아바타 렌더 기준: 사용자 피드백에 따라 서연 P1과 P4~P7의 생성 이미지/PSD 래스터 파츠 연결을 전부 끊고, 모든 실제 화면과 dev 미리보기에서 기존 SVG 도형 기반 파라미터 리그만 렌더링한다. RasterBust와 래스터 원본·파츠는 재검토용으로 보존하지만 앱에서 로드하지 않는다. active session 데스크톱 스테이지의 상태 라벨 / 아바타 / 현재 상태 3행 구조는 유지하며, 사용자 제공 수채화 잎 5장은 인물 파츠가 아닌 독립 배경 장식으로만 사용한다.
  • 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은 후속.
  • 페르소나 저작/P4P7 적재 2차: data/personas/P4.jsonP7.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 /personasP1..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만 보이는 빈 화면을 만들지 않는다. 최초 /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 f323a41dindex-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 buildnpm 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:8010ENVIRONMENT=dev로 떠 있다.
  • 2026-06-27 05:34:32 UTC 모바일 502는 cloudflared 로그상 127.0.0.1:8001 origin connection refused와 일치한다. 현재 public API는 복구됐고, watchdog 기본 검사에서 아직 DNS가 없는 api-vnet.18ka.net을 제외해 향후 설치 시 불필요한 restart loop를 막았다. 2026-07-03 KST 재확인 대상: https://vignette.chanpaca.net/admin/users?deploy=b67fd5d4 200 with assets/index-R5KK7hZI.js + assets/index-DzdAkRsn.css and vignette-boot-diagnostic HTML, https://api-vignette.chanpaca.net/health prod/db/engine true, public unauth GET /personas 401.
  • GET /personas는 이제 인증 필요다. 검증 당시 로컬 http://127.0.0.1:8000/personas와 현재 공개 https://api-vignette.chanpaca.net/personas 모두 비로그인 401 확인 완료.
  • 로컬 웹이 http://127.0.0.1:5175처럼 다른 Vite 포트로 떠도 로그인은 로컬 테스트 계정으로 계속 버튼을 사용한다. Google OAuth 버튼은 로컬에서는 disabled로 둔다. 현재 OAuth callback이 공개 API로 돌아가기 때문에 로컬 Google OAuth는 로컬 세션에 붙을 수 없다.
  • 현재 확인용 프로세스는 127.0.0.1:9099 engine gateway, 127.0.0.1:8001 prod public API, 127.0.0.1:8000 dev API, 127.0.0.1:8010 Tailnet dev API, 127.0.0.1:5173 Vite dev web, 127.0.0.1:5174 public preview, cloudflared tunnel 1개다. Tailnet URL은 https://alpaca-home.taile93291.ts.net/login이다. stale Vite allowedHosts로 403이 났던 상태는 재검증 시 login 200, /api/health dev/db/engine true, /api/auth/config 200이었다. alpaca-home.taile93291.ts.net는 Vite 기본 allowedHosts에도 포함했다.
  • scripts/dev-up.ps1 -NoGateway -NoWeb는 이제 gateway/web stale 정리를 건너뛰고 지정 -ApiPort의 API만 재기동한다. 검증 당시 8000 API-only 재기동 후 8001/8010/5173/9099/20241 listener가 보존됐다.
  • Docker vignette-dev-db는 실행 중이고 DB는 accepting connections다. 단, 기존 컨테이너라 healthcheck가 없고 POSTGRES_USER=vignette 기반이다. vignette_app role은 NOBYPASSRLS로 존재하지만 현재 API startup DDL이 owner 권한을 요구하므로 런타임 app-role 전환은 마이그레이션 owner/런타임 role 분리 후 진행한다.
  • 공개 런타임 재기동 스크립트:
    • start: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1
    • stop: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\stop-public-runtime.ps1

이번 세션 완료

  • 남아 있던 E2E seed persona 결합을 제거했다.
    • session-persistence, teacher, voice 스펙은 /api/personas에서 실제 DB persona를 가져와 사용한다.
    • voice-success만 DB-offline fixture라 ALLOW_SEED_PERSONA_FALLBACK=trueP1을 의도적으로 유지하고 주석을 남겼다.
  • 운영 설정 fail-closed를 강화했다.
    • ENVIRONMENT != dev에서 dev login, seed fallback, 기본 session secret, OAuth 누락, localhost frontend/CORS를 설정 검증에서 거부한다.
    • API lifespan은 DB 초기화 실패 시 dev에서만 degraded fallback을 허용하고, staging/prod에서는 시작 실패로 둔다.
    • SettingsConfigDict(populate_by_name=True)를 추가해 테스트/코드에서 field name 기반 설정 생성이 가능하게 했다.
  • infra/docker-compose.yml을 깨끗한 ASCII compose로 재작성했다.
    • ENVIRONMENT, Google OAuth client id/secret, OAUTH_REDIRECT_URI, CORS_ORIGINS, OPENAI_BASE_URL을 API 컨테이너에 명시 주입한다.
    • 기존 손상된 줄 때문에 OPENAI_API_KEY가 주석/중복으로 처리되던 문제를 제거했다.
    • 기본 FRONTEND_BASE_URLhttps://vignette.chanpaca.net로 수정했다.
  • infra/.env.example에 운영 필수 값을 보강했다.
    • ENVIRONMENT=prod, OAUTH_GOOGLE_CLIENT_ID, OAUTH_GOOGLE_CLIENT_SECRET
    • 기본 CORS에서 localhost 제거. 로컬 개발은 주석대로 ENVIRONMENT=dev와 localhost CORS를 별도 추가.
  • admin UI 안정화.
    • health card의 긴 engine readiness JSON이 모바일 폭을 밀어내지 않도록 줄바꿈 처리.
    • backend admin health는 동적인 raw JSON detail 대신 안정적인 Engine readiness failed 문구를 내려준다.
    • 전역 engine config를 바꾸는 db-persistence E2E는 @single-run으로 분리했다.
    • 사용자 생성 후 UI가 서버 목록 reload를 끝내기 전에 카드를 찾던 E2E 타이밍을 안정화했다. 생성한 이메일로 검색한 뒤 실제 서버 응답 카드만 검증한다.
    • 사용자 관리 UI는 DB 사용자 저장소가 durable하지 않으면 생성/수정/비활성화를 막는다.
    • AI 운영 라벨은 Claude CLI 게이트웨이, Anthropic API, OpenAI 호환, Solar처럼 실제 어댑터명으로 표시한다.
    • 설정의 AI 운영 패널은 /admin/health 기반 응답 생성 상태를 함께 보여주되, health 지연이 설정 폼 로딩을 막지 않도록 분리했다.
    • 비개발 환경에서 admin_engine_config singleton row가 없으면 runtime default로 내려가지 않고 fail-closed 한다.
    • 비개발 DB 장애 상태는 임시 기록이 아니라 저장소 중단으로 표시한다.
  • public Google OAuth smoke 문서를 보강했다.
    • E2E_PUBLIC_AUTH=1에서는 로컬 Vite 서버를 띄우지 않고 공개 웹을 대상으로 한다.
    • storageState는 인증 쿠키를 포함하므로 node_modules/.tmp 아래 민감 파일로 취급한다.
    • API session TTL은 현재 8시간이다.
  • 공개 API prod 전환을 완료했다.
    • cloudflared ingress를 127.0.0.1:8000에서 127.0.0.1:8001로 변경했다.
    • https://api-vignette.chanpaca.net/healthenvironment:"prod", db:true, engine:true를 반환한다.
    • public production-safe Playwright 게이트가 통과한다.
  • 공개 런타임 start/stop 스크립트를 추가했다.
    • scripts/start-public-runtime.ps1는 prod-safe env override로 API 8001을 띄우고 health를 검증한다.
    • 기본 실행 시 cloudflared도 vignette config 기준으로 재시작한다.
    • scripts/stop-public-runtime.ps1는 8001 API를 정리한다. -StopCloudflared를 붙이면 터널도 같이 정리한다.
  • 공개 API 보안 경계를 보강했다.
    • apps/api/app/routes/personas.pyCurrentPrincipal을 붙여 persona catalog를 authenticated-only로 전환했다.
    • 로컬 E2E auth.spec.ts에 비로그인 /api/personas => 401 회귀를 추가했다.
    • public readiness gate는 빈 API request context로 비로그인 /personas => 401을 확인한다. storageState가 붙은 request fixture를 쓰지 않도록 처리했다.
  • 공개 웹 API base fallback을 보강했다.
    • apps/web/src/lib/api.tsVITE_API_BASE가 없고 host가 vignette.chanpaca.net 또는 *.pages.dev이면 https://api-vignette.chanpaca.net을 기본 API origin으로 사용한다.
    • 이 변경으로 public web의 /api/auth/config가 SPA HTML로 떨어져 Google 버튼이 disabled 되는 문제를 피한다.
  • 로컬 로그인 UX를 보강했다.
    • apps/web/src/pages/Login.tsx는 로컬 origin에서 OAuth redirect URI가 공개 API이면 Google 버튼을 disabled 처리하고 로컬 테스트 계정으로 로그인 안내를 보여준다.
    • auth.spec.ts에 로컬 테스트 계정 로그인 후 /learn redirect 회귀를 추가했다.
  • 학습자 홈 기록 UX를 보강했다.
    • apps/web/src/pages/LearnerHome.tsx는 세션이 0개인 사용자에게도 실제 서버 세션 0개를 기반으로 기존 회기 빈 상태와 0 카운트를 보여준다.
    • 기록이 있을 때만 has-session-records 압축 레이아웃을 쓰도록 분리했다. 0회 사용자 모바일에서도 선택 내담자 정보, 새 회기 CTA, 기록 빈 상태가 문서 스크롤 없이 보인다.
    • apps/web/e2e/readiness.spec.ts는 fake 12회/최근 8회 부재뿐 아니라 서버 기반 0회 빈 상태를 기대하도록 갱신했다.
    • history의 반복 CTA는 다시 시도 대신 다시 연습으로 바꿔 기록 회고와 새 회기 시작 의미를 분리했다.
  • 직접 세션 URL의 persona 우회 경로를 막았다.
    • LearnerHome에서만 막던 degraded 또는 non-DB persona 차단을 Session 직접 진입 경로에도 적용했다.
    • /learn/session/:personaCode로 직접 들어와도 DB 원본 persona가 아니면 회기 시작 버튼이 비활성화된다.
    • desktop/mobile E2E에 degraded seed_fallback persona route mock 회귀를 추가했다.
  • active session dense viewport를 보정했다.
    • apps/web/src/pages/session/session.css에서 380px 이하/낮은 화면의 stage padding과 avatar 크기를 줄여 stage 내부 clipping을 없앴다.
    • session-layout.spec.ts dense viewport desktop/mobile 회귀와 전체 Playwright에서 확인했다.
  • active session 텍스트 턴 저장/표시 흐름을 실제 SSE 계약에 맞췄다.
    • 프론트 sessionApi.stream은 실제 백엔드 계약인 POST /sessions/{id}/stream을 fetch stream으로 소비한다.
    • /stream 요청이 거절되거나 SSE가 열린 뒤 error 이벤트로 끝나면 학습자 발화와 부분 내담자 응답을 transcript에 남기지 않고 입력값을 복원한다.
    • 내담자 자막은 로컬 typewriter가 아니라 서버 token 이벤트로만 쌓는다.
    • 백엔드 /turn/stream은 엔진 성공 전 learner turn/state를 append하지 않도록 순서를 바꿨고, app.test_session_turn_persistence 회귀로 검증했다.
  • 음성 회기 실패 턴 저장/표시도 같은 기준으로 맞췄다.
    • voice route는 엔진 응답 생성 성공 후에만 learner/client turn을 저장한다.
    • 프론트는 음성 transcript를 pending으로 표시하고, 엔진 실패 시 pending transcript를 제거한다.
    • 음성 degraded reason의 내부 seed_fallback/runtime store 표현은 사용자용 한국어 상태 문구로 바꿨다.
  • admin health의 dev DB fallback 문구를 임시 기록이 아니라 비영구 런타임 기록으로 정리했다.
  • Live2D demo fallback과 샘플 자산을 제거했다.
    • Mao/Haru 샘플 모델과 Pixi/Cubism 런타임은 공개 배포물에서 제거했다.
    • 세션 화면은 SVG persona avatar만 렌더링한다.
    • Cloudflare edge에 남은 기존 /live2d/* 캐시를 막기 위해 Pages Function apps/web/functions/live2d/[[path]].js가 404를 반환한다.

검증 결과

  • Web typecheck:
    • cd apps/web; fnm env --use-on-cd | Out-String | Invoke-Expression; fnm use 22.22.3; npm run typecheck
    • Passed
  • Backend unit/compile:
    • cd apps/api; C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -m unittest app.test_runtime_policy app.test_session_turn_persistence engine_gateway.test_gateway_model
    • 20 tests OK
    • python -m compileall app engine_gateway
    • Passed
  • Web production build:
    • cd apps/web; fnm use 22.22.3; npm run build
    • Passed
  • Design/theme verification:
    • docs/design-verification/full-pages/01..07-*-{desktop,tablet,mobile}.png
    • 7개 주요 화면 x 3뷰포트 = 21장 캡처 갱신. 자동 검사 결과: horizontal overflow 0, 큰 흰 CSS 배경면 0. 세션 active는 seoyeon-live2d-psd-v2 래스터 로드, 현재 상태 배지와 아바타 겹침 없음.
    • npx playwright test e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts e2e/avatar-expression.spec.ts e2e/session-review.spec.ts e2e/settings.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run --workers=1
    • 36 passed
  • Docker compose config:
    • With dummy required env vars, docker compose -f infra\docker-compose.yml --env-file infra\.env.example config --quiet
    • Passed
  • Focused Playwright after fixes:
    • npx playwright test e2e/admin.spec.ts e2e/db-persistence.spec.ts e2e/voice-success.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run
    • 10 passed
  • Admin manage-users focused rerun:
    • PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/admin.spec.ts --project=chromium-desktop --project=chromium-mobile --grep "manage real server-known users"
    • 2 passed
  • Full Playwright:
    • cd apps/web; PLAYWRIGHT_HOST=127.0.0.1 npx playwright test
    • 72 passed after public API/cloudflared was restored
    • 참고: tunnel을 내린 직후 첫 전체 실행은 public login gate 2개만 Cloudflare 1033으로 실패하고 나머지 70 passed였다. scripts\start-public-runtime.ps1로 prod public API와 cloudflared를 복구한 뒤 전체 재실행에서 72 passed.
  • Public auth project discovery:
    • E2E_PUBLIC_AUTH=1 npx playwright test --list --project=chromium-public-auth
    • 2 tests: public production-safe config + real OAuth /turn
  • Public production-safe gate:
    • E2E_PUBLIC_AUTH=1 npx playwright test e2e/public-auth-turn.spec.ts --project=chromium-public-auth --grep "production-safe"
    • 1 passed on rerun
  • Admin/settings focused E2E:
    • npx playwright test e2e/admin.spec.ts e2e/settings.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run
    • Latest focused reruns: admin.spec.ts 8 passed, settings.spec.ts 9 passed
  • Auth/readiness focused E2E:
    • npx playwright test e2e/auth.spec.ts e2e/readiness.spec.ts --project=chromium-desktop --project=chromium-mobile
    • 14 passed
  • Persona auth boundary:
    • local unauth http://127.0.0.1:8000/personas -> 401
    • public unauth https://api-vignette.chanpaca.net/personas -> 401
  • Public runtime script:
    • powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1
    • API 8001 health and public https://api-vignette.chanpaca.net/health both returned environment:"prod", db:true, engine:true
  • Public web deployment:
    • wrangler pages deploy dist --project-name vignette --branch main --commit-dirty=true
    • 2026-07-03 manual deployment, preview https://8f6aba1c.vignette-b1q.pages.dev
    • https://vignette.chanpaca.net/admin/users?deploy=b67fd5d4 serves assets/index-R5KK7hZI.js, assets/index-DzdAkRsn.css, and boot diagnostic HTML
    • https://vignette.chanpaca.net/live2d/mao/Mao.model3.json, Haru model, and Cubism core routes return 404
  • Public login screen:
    • npx playwright test e2e/auth.spec.ts --project=chromium-desktop --grep "public login"
    • 1 passed
    • OAuth state is now HMAC-signed and bound to a HttpOnly state cookie; fake callback with state cookie now reaches oauth=token_exchange_failed instead of oauth=invalid_state, provider callback error redirects to oauth=access_denied, and callback failures log non-secret reason/status details in api.public.err.log.
    • Login UI maps known OAuth/SAML failure reasons and displays 오류 코드: ...; auth focused E2E is now 7 passed.
  • Public runtime watchdog:
    • 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 /healthprod/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
    • PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/auth.spec.ts --project=chromium-mobile
    • 6 passed
    • focused local dev-login redirect: 1 passed
  • Learner home/history focused verification:
    • cd apps/web; npm run typecheck
    • Passed
    • PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/learner.spec.ts e2e/readiness.spec.ts --project=chromium-desktop --project=chromium-mobile
    • 14 passed
    • Visual audit screenshots: apps/web/node_modules/.tmp/ui-audit/learn-empty-desktop.png, apps/web/node_modules/.tmp/ui-audit/learn-empty-mobile-compact.png
    • Screenshot metrics confirmed desktop/mobile document overflow x:0, y:0, empty history visible, and real stats area present.
  • Session dense viewport verification:
    • PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile
    • 8 passed
  • Latest voice/session focused E2E:
    • PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/voice.spec.ts e2e/voice-success.spec.ts e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run
    • 15 passed
  • Latest learner/session/settings/admin focused E2E:
    • PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/learner.spec.ts e2e/session-layout.spec.ts e2e/settings.spec.ts e2e/admin.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run
    • 37 passed

아직 못 끝낸 것

  • 공개 Google OAuth 실제 /turn proof는 아직 없다.
    • 이유: E2E_PUBLIC_STORAGE_STATE=apps/web/node_modules/.tmp/public-auth.json가 아직 확보되지 않았다.
    • 사용자가 Playwright codegen 브라우저에서 Google 로그인 후 storageState를 저장해야 한다.
    • 2026-06-26 15:18 KST 기준 apps/web/node_modules/.tmp/public-auth.json은 없다. codegen 창/프로세스가 남아 있으면 로그인 완료 후 창을 닫아 저장을 완료하면 된다.
  • 공개 API environment:"dev" 문제는 이번 세션에서 해결했다. cloudflared가 prod API인 127.0.0.1:8001을 보도록 변경했고 public gate로 확인했다.
  • Google Console 설정은 브라우저 로그인 세션이 필요하다. 막히면 사용자가 로그인해 주기로 했다.
  • 현재 worktree에는 이번 디자인 패스의 아바타/세션 화면 변경이 남아 있다(Session.tsx, session.css, apps/web/public/avatar/seoyeon-live2d-psd-v2/, avatar-expression.spec.ts, 관련 문서). 과거 임시 크롭 apps/web/public/avatar/p1-concept/는 P1 기본값으로 쓰지 않는다. API 쪽 변경(apps/api/app/persona_repository.py, apps/api/app/test_persona_review.py)은 이번 디자인 패스 범위에서 수정하지 않았으므로 별도 변경으로 취급하고 되돌리지 말 것.

다음 세션 우선순위

  1. Google OAuth Console을 확인한다.
    • Authorized redirect URI: https://api-vignette.chanpaca.net/auth/callback
    • Authorized JavaScript origin: https://vignette.chanpaca.net
    • 앱/도메인 설정은 hs.ac.kr, twentyoz.kr 계정만 서버 claim 검증으로 허용한다. Google Console의 authorized domains와 이메일 도메인 제한은 다른 개념이다.
  2. storageState를 캡처하고 public /turn smoke를 실행한다.
    • cd D:\workspace\vignette\apps\web
    • npx playwright codegen https://vignette.chanpaca.net/login --save-storage=.\node_modules\.tmp\public-auth.json
    • $env:E2E_PUBLIC_AUTH="1"
    • $env:E2E_PUBLIC_STORAGE_STATE=".\node_modules\.tmp\public-auth.json"
    • .\node_modules\.bin\playwright.cmd test e2e/public-auth-turn.spec.ts --project=chromium-public-auth
  3. UI/UX는 계속 docs/DESIGN_CONCEPT.md를 따른다.
    • learner home은 빈 사용자에게 fake 통계/기록을 보여주지 않는다. 0회 상태는 실제 서버 기록 0개로 기존 회기 빈 상태를 보여주는 현재 구현을 유지한다.
    • active learning session은 전체 화면, 문서 스크롤 없음이 원칙이다.
    • 실제 session history는 기존 회기, 이어하기, 기록/리뷰, 다시 연습, 새 회기 시작 흐름으로 유지한다.
  4. 다음 배포 전 admin/engine-config를 다시 확인한다.
    • 의도된 현재값이 claude_cli / http://127.0.0.1:9099 / gateway-default인지 확인.
    • E2E가 engine config를 건드리므로 배포 전 실제 운영값을 반드시 재확인한다.

로컬 실행 명령

Engine gateway:

cd D:\workspace\vignette\apps\api
C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -m uvicorn engine_gateway.gateway:app --host 127.0.0.1 --port 9099

API:

cd D:\workspace\vignette\apps\api
$env:ENGINE_URL="http://127.0.0.1:9099"
$env:ENGINE_MODE="claude_cli"
$env:ENVIRONMENT="dev"
$env:AUTH_DEV_LOGIN_ENABLED="true"
C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -m uvicorn app.main:app --host 127.0.0.1 --port 8000

Public API process:

cd D:\workspace\vignette
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1

Web:

cd D:\workspace\vignette\apps\web
fnm env --use-on-cd | Out-String | Invoke-Expression
fnm use 22.22.3
npm run dev -- --host localhost --port 5173

최근 주요 변경 파일

  • apps/web/e2e/support.ts
  • apps/web/e2e/auth.spec.ts
  • apps/web/e2e/learner.spec.ts
  • apps/web/e2e/session-layout.spec.ts
  • apps/web/e2e/session-review.spec.ts
  • apps/web/e2e/session-persistence.spec.ts
  • apps/web/e2e/teacher.spec.ts
  • apps/web/e2e/voice.spec.ts
  • apps/web/e2e/voice-success.spec.ts
  • apps/web/e2e/public-auth-turn.spec.ts
  • apps/web/e2e/db-persistence.spec.ts
  • apps/web/e2e/README.md
  • apps/web/playwright.config.ts
  • apps/web/src/pages/Admin.tsx
  • apps/web/src/pages/Login.tsx
  • apps/web/src/pages/Session.tsx
  • apps/web/src/pages/Settings.tsx
  • apps/web/src/pages/session/session.css
  • apps/web/src/components/avatar/ClientAvatar.tsx
  • apps/web/src/components/avatar/persona.ts
  • apps/api/app/config.py
  • apps/api/app/main.py
  • apps/api/app/routes/admin.py
  • apps/api/app/routes/personas.py
  • apps/api/app/routes/users.py
  • apps/api/app/routes/voice.py
  • apps/api/app/test_session_turn_persistence.py
  • apps/api/app/test_runtime_policy.py
  • infra/docker-compose.yml
  • infra/.env.example
  • docs/HANDOFF.md
  • docs/dev_dashboard.html
  • apps/web/src/lib/api.ts