diff --git a/apps/web/e2e/layout-visual-gate.spec.ts b/apps/web/e2e/layout-visual-gate.spec.ts index 3792f54..a2c9668 100644 --- a/apps/web/e2e/layout-visual-gate.spec.ts +++ b/apps/web/e2e/layout-visual-gate.spec.ts @@ -577,6 +577,11 @@ test.describe("layout visual gate @single-run", () => { await expect(page.locator('[data-learner-overview-row="true"]')).toHaveCount(1); await expect(page.locator('[data-learner-analysis-panel="true"]')).toHaveCount(0); await expect(page.getByLabel("학습자 검색")).toBeVisible(); + const expandButton = page.getByRole("button", { name: /분석 검증 학습자 요약/ }); + if ((await expandButton.getAttribute("aria-expanded")) !== "true") { + await expandButton.click(); + } + await expect(page.locator('[data-learner-expanded-row="true"]')).toBeVisible(); }); }); diff --git a/apps/web/e2e/teacher.spec.ts b/apps/web/e2e/teacher.spec.ts index 53bb5b3..7c43009 100644 --- a/apps/web/e2e/teacher.spec.ts +++ b/apps/web/e2e/teacher.spec.ts @@ -651,7 +651,11 @@ test.describe("teacher console", () => { .filter({ hasText: "사용자 B" }); await expect(learnerBRow).toHaveCount(1); await expect(learnerBRow.getByText(learnerB)).toBeVisible(); - await learnerBRow.getByRole("button", { name: "사용자 B 상세 분석 열기" }).click(); + await expect(learnerBRow.getByRole("button", { name: "사용자 B", exact: true })).toBeVisible(); + await learnerBRow.getByRole("button", { name: "사용자 B 요약 펼치기" }).click(); + await expect(page.locator('[data-learner-expanded-row="true"]')).toBeVisible(); + await expect(page.locator('[data-learner-expanded-row="true"]').getByText("회기별 추이")).toBeVisible(); + await learnerBRow.getByRole("button", { name: "사용자 B 상세 보기" }).click(); await expect(page).toHaveURL(new RegExp(`/teach/analysis\\?learner=${learnerB}$`)); const analysisPanel = page.locator('[data-learner-analysis-panel="true"]'); diff --git a/apps/web/src/pages/Professor.tsx b/apps/web/src/pages/Professor.tsx index ad34a71..f375653 100644 --- a/apps/web/src/pages/Professor.tsx +++ b/apps/web/src/pages/Professor.tsx @@ -833,6 +833,7 @@ function LearnerOverviewTable({ onSort: (value: LearnerSort) => void; onOpen: (learnerId: string) => void; }) { + const [expandedLearnerId, setExpandedLearnerId] = useState(null); const sortOptions: Array<{ key: LearnerSort; label: string }> = [ { key: "latest", label: "최근순" }, { key: "sessions", label: "회기순" }, @@ -840,6 +841,12 @@ function LearnerOverviewTable({ { key: "delta", label: "변화순" }, ]; + useEffect(() => { + if (expandedLearnerId && !learners.some((learner) => learner.learner_id === expandedLearnerId)) { + setExpandedLearnerId(null); + } + }, [expandedLearnerId, learners]); + return (
@@ -890,67 +897,128 @@ function LearnerOverviewTable({ - {learners.map((learner) => ( -
-
- {learner.learner_label} - {learner.learner_id} -
-
- - {learner.ended_sessions}/{learner.sessions} - - {formatDateTime(learner.latest_at)} -
-
-
- {(learner.points ?? []).map((point) => ( - - ))} -
- - {trendLabel(learner.trend)} - -
-
- {formatScore(learner.latest_score)} -
-
- {formatDelta(learner.score_delta)} -
-
- {formatRapport(learner.avg_rapport)} -
-
- {(learner.top_techniques ?? []).slice(0, 3).map((tag) => ( - {tag} - ))} - {(learner.top_techniques ?? []).length === 0 ? 기법 부족 : null} -
-
- +
+ + {learner.learner_id} +
+
+ + {learner.ended_sessions}/{learner.sessions} + + {formatDateTime(learner.latest_at)} +
+
+ {formatScore(learner.latest_score)} +
+
+ {formatDelta(learner.score_delta)} +
+
+ {formatRapport(learner.avg_rapport)} +
+
+ + {trendLabel(learner.trend)} + +
+
+ {topTechniques[0] ?? "기법 부족"} + {topTechniques.length > 1 ? 외 {topTechniques.length - 1}개 : null} +
+
+ +
+
+ +
+
+ {isExpanded ? ( +
+
+ 회기별 추이 + {(learner.points ?? []).length > 0 ? ( +
+ {(learner.points ?? []).map((point) => ( + + ))} +
+ ) : ( + 저장된 점수 없음 + )} +
+
+ 점수 변화 + + {formatScore(learner.first_score)} → {formatScore(learner.latest_score)} + + {formatDelta(learner.score_delta)} +
+
+ 최근 기록 + {formatDateTime(learner.latest_at)} + + 완료 {learner.ended_sessions} / 전체 {learner.sessions} + +
+
+ 기법 태그 +
+ {topTechniques.slice(0, 5).map((tag) => ( + {tag} + ))} + {topTechniques.length === 0 ? 기법 부족 : null} +
+
+
+ ) : null}
- - ))} + ); + })} )} diff --git a/apps/web/src/pages/professor.css b/apps/web/src/pages/professor.css index d34816d..e9bda18 100644 --- a/apps/web/src/pages/professor.css +++ b/apps/web/src/pages/professor.css @@ -405,22 +405,25 @@ min-width:0; display:flex; flex-direction:column; + overflow-x:auto; + scrollbar-gutter:stable; } .pf-learner-table__head, .pf-learner-row{ display:grid; grid-template-columns: - minmax(170px,1.25fr) + minmax(178px,1.3fr) minmax(86px,.55fr) - minmax(164px,.9fr) minmax(76px,.48fr) minmax(64px,.42fr) minmax(76px,.48fr) - minmax(118px,.75fr) - minmax(96px,.42fr); + minmax(70px,.42fr) + minmax(112px,.72fr) + 48px + minmax(104px,.5fr); gap:9px; align-items:center; - min-width:0; + min-width:1000px; } .pf-learner-table__head{ position:sticky; @@ -434,19 +437,24 @@ font-weight:800; white-space:nowrap; } -.pf-learner-row{ - padding:11px 12px; +.pf-learner-record{ + min-width:1000px; border-top:1px solid var(--paper-2); } -.pf-learner-table__head + .pf-learner-row{ +.pf-learner-table__head + .pf-learner-record{ border-top:0; } -.pf-learner-row:hover{ +.pf-learner-row{ + padding:10px 12px; +} +.pf-learner-record:hover .pf-learner-row, +.pf-learner-record.is-expanded .pf-learner-row{ background:color-mix(in srgb,var(--accent-tint) 14%,transparent); } .pf-learner-row__id, .pf-learner-row__metric, -.pf-learner-row__spark, +.pf-learner-row__trend, +.pf-learner-row__tag-summary, .pf-learner-row__tags{ min-width:0; } @@ -454,13 +462,36 @@ display:grid; gap:3px; } -.pf-learner-row__id b{ +.pf-learner-name-button{ + width:max-content; + max-width:100%; + padding:0; + border:0; color:var(--text-strong); + background:transparent; + font:inherit; font-size:var(--fs-sm); + font-weight:820; line-height:1.35; + text-align:left; + cursor:pointer; + overflow:hidden; + text-overflow:ellipsis; + white-space:nowrap; +} +.pf-learner-name-button:hover, +.pf-learner-name-button:focus-visible{ + color:var(--accent-deep); + text-decoration:underline; + text-underline-offset:3px; +} +.pf-learner-name-button:focus-visible{ + outline:2px solid var(--accent); + outline-offset:2px; } .pf-learner-row__id span, -.pf-learner-row__metric span{ +.pf-learner-row__metric span, +.pf-learner-row__tag-summary span{ color:var(--text-muted); font-size:11px; line-height:1.3; @@ -473,7 +504,8 @@ gap:3px; } .pf-learner-row__metric::before, -.pf-learner-row__spark::before, +.pf-learner-row__trend::before, +.pf-learner-row__tag-summary::before, .pf-learner-row__tags::before{ display:none; content:attr(data-label); @@ -488,11 +520,24 @@ line-height:1.15; white-space:nowrap; } -.pf-learner-row__spark{ - display:grid; - grid-template-columns:minmax(0,1fr) auto; - gap:8px; +.pf-learner-row__trend{ + display:flex; align-items:center; + min-height:30px; +} +.pf-learner-row__tag-summary{ + display:grid; + gap:3px; +} +.pf-learner-row__tag-summary b{ + min-width:0; + color:var(--text-body); + font-size:var(--fs-xs); + font-weight:760; + line-height:1.25; + overflow:hidden; + text-overflow:ellipsis; + white-space:nowrap; } .pf-learner-spark{ min-width:0; @@ -529,14 +574,88 @@ text-overflow:ellipsis; white-space:nowrap; } +.pf-learner-row__expand{ + display:flex; + justify-content:center; +} +.pf-icon-action{ + width:34px; + height:34px; + display:inline-grid; + place-items:center; + border:1px solid var(--paper-2); + border-radius:var(--radius-sm); + color:var(--text-muted); + background:var(--bg-surface-2); + cursor:pointer; +} +.pf-icon-action:hover, +.pf-icon-action:focus-visible, +.pf-icon-action[aria-expanded="true"]{ + color:var(--accent-deep); + border-color:var(--accent); + background:var(--accent-tint); +} +.pf-icon-action:focus-visible{ + outline:2px solid var(--accent); + outline-offset:2px; +} .pf-learner-row__action{ display:flex; justify-content:flex-end; } .pf-learner-row__action .vg-btn{ - min-width:92px; + min-width:96px; padding-inline:10px; } +.pf-learner-row-detail{ + min-width:1000px; + display:grid; + grid-template-columns:minmax(260px,1.4fr) minmax(140px,.72fr) minmax(170px,.85fr) minmax(190px,1fr); + gap:0; + padding:0 12px 12px; + background:color-mix(in srgb,var(--accent-tint) 12%,transparent); +} +.pf-learner-detail-item{ + min-width:0; + display:grid; + align-content:start; + gap:5px; + padding:11px 12px; + border-top:1px solid var(--hair); + border-left:1px solid var(--hair); + background:color-mix(in srgb,var(--bg-surface) 90%,var(--bg-surface-2)); +} +.pf-learner-detail-item:first-child{ + border-left:0; +} +.pf-learner-detail-item > span{ + color:var(--text-muted); + font-size:var(--fs-xs); + font-weight:800; + line-height:1.25; +} +.pf-learner-detail-item > b{ + min-width:0; + color:var(--text-strong); + font-family:var(--font-num); + font-size:14px; + line-height:1.2; + overflow:hidden; + text-overflow:ellipsis; + white-space:nowrap; +} +.pf-learner-detail-item > small{ + color:var(--text-muted); + font-size:11px; + line-height:1.3; +} +.pf-learner-detail-item--trend{ + gap:8px; +} +.pf-learner-detail-item--tags .pf-learner-row__tags{ + align-self:center; +} .pf-recent__review-state{ color:var(--text-muted); font-size:12px; @@ -1247,47 +1366,6 @@ .pf-learner-table{ background:var(--bg-surface-2); } - .pf-learner-table__head{ - display:none; - } - .pf-learner-row{ - margin:10px; - padding:12px; - border:1px solid var(--paper-2); - border-radius:var(--radius); - background:var(--bg-surface); - grid-template-columns:repeat(3,minmax(0,1fr)); - gap:10px 12px; - } - .pf-learner-row + .pf-learner-row{ - margin-top:0; - } - .pf-learner-row__id, - .pf-learner-row__spark, - .pf-learner-row__tags, - .pf-learner-row__action{ - grid-column:1 / -1; - } - .pf-learner-row__metric::before, - .pf-learner-row__spark::before, - .pf-learner-row__tags::before{ - display:block; - } - .pf-learner-row__spark, - .pf-learner-row__tags{ - display:grid; - grid-template-columns:minmax(76px,.28fr) minmax(0,1fr); - align-items:center; - } - .pf-learner-row__tags{ - display:flex; - } - .pf-learner-row__tags::before{ - flex:0 0 76px; - } - .pf-learner-row__action{ - justify-content:flex-start; - } .pf-analysis__top{ grid-template-columns:1fr; } @@ -1507,21 +1585,6 @@ .pf-sort-segment button:nth-child(n+3){ border-top:1px solid var(--hair); } - .pf-learner-row{ - grid-template-columns:repeat(2,minmax(0,1fr)); - margin:8px; - } - .pf-learner-row__spark, - .pf-learner-row__tags{ - grid-template-columns:1fr; - gap:6px; - } - .pf-learner-row__tags::before{ - flex-basis:100%; - } - .pf-learner-row__action .vg-btn{ - width:100%; - } .pf-analysis__metrics{ grid-template-columns:1fr; } diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 8d96233..e2a9560 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -11,7 +11,7 @@ - 공개 웹: `https://vignette.chanpaca.net` - 공개 API: `https://api-vignette.chanpaca.net` - 최신 앱 배포 소스: 2026-07-01 `origin/master` manual deploy snapshot. -- 최신 Cloudflare Pages production deploy: 2026-07-01 manual deploy, branch `main`, custom domain assets `assets/index-DgN8JVBw.js` + `assets/index--s3xqgO5.css`. +- 최신 Cloudflare Pages production deploy: 2026-07-01 manual deploy, branch `main`, custom domain assets `assets/index-Cz4PBoO3.js` + `assets/index-DtVTvS9j.css`. - Google OAuth 허용 이메일 도메인: `hs.ac.kr`, `twentyoz.kr` - 최신 백엔드 회귀: `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -m pytest app/ -q` → `305 passed` - X1 재귀학습 export 1차: `scripts/export-recursive-dataset.py` 기본 read-only dry-run, `--write-dataset` 명시 시에만 `ds.*` write, approved export는 steward/legal/IAA gate 없으면 거부. @@ -33,7 +33,7 @@ - `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`로 떠 있다. -- 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-01 KST 재확인 대상: `https://vignette.chanpaca.net/login?deploy=20260701` 200 with `assets/index-DgN8JVBw.js` + `assets/index--s3xqgO5.css`, `https://api-vignette.chanpaca.net/health` prod/db/engine true, public unauth `GET /personas` 401. +- 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-01 KST 재확인 대상: `https://vignette.chanpaca.net/login?deploy=20260701` 200 with `assets/index-Cz4PBoO3.js` + `assets/index-DtVTvS9j.css`, `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`에도 포함했다. @@ -170,7 +170,7 @@ - Public web deployment: - `wrangler pages deploy dist --project-name vignette --branch main --commit-dirty=true` - 2026-06-29 manual deployment, preview `https://eb2ed257.vignette-b1q.pages.dev` - - `https://vignette.chanpaca.net/` serves `assets/index-DgN8JVBw.js` and `assets/index--s3xqgO5.css` + - `https://vignette.chanpaca.net/` serves `assets/index-Cz4PBoO3.js` and `assets/index-DtVTvS9j.css` - `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"` diff --git a/docs/dev_dashboard.html b/docs/dev_dashboard.html index 68fb13a..ebd3395 100644 --- a/docs/dev_dashboard.html +++ b/docs/dev_dashboard.html @@ -518,7 +518,7 @@

82차 적용(2026-06-29): refactor-governance P8로 Session.tsx의 음성/세션 UI 파생 상태 계산을 sessionVoiceStatusView() helper로 분리했다. 마이크 busy/label, 세션 상태 label, transcript live label, 텍스트 입력 차단, 응답/음성 상태 chip 계산을 named view-model 경계에서 유지한다. API/DB 계약, 세션 시작·전송·종료, voice WebSocket/TTS lifecycle, live-coach payload, DOM/CSS는 변경하지 않았다. 검증: npm run typecheck, npm run build, session-mvp 1 passed, voice-success 2 passed, session-layout desktop/mobile 8 passed.

83차 적용(2026-06-29): refactor-governance P3로 학습자/교수자 성장 지표가 턴 평가 기법 라벨을 읽는 경계를 session_metrics.turn_technique_label()로 명시했다. DB 정규화 후 재수화되는 TechniqueTag.label_ko를 우선 사용하고, 기존 label/name/id/code legacy fallback은 유지한다. alternative_utterances는 future adapter 추출 전에 문자열 배열과 text/suggestion dict 배열을 모두 row로 정규화하는 호환 테스트로 고정했다. 검증: py_compile, evaluation/learner/teacher focused 59 passed.

84차 적용(2026-06-29): refactor-governance P2로 Session.tsx의 음성 guard/view-model 경계를 한 번 더 줄였다. isVoiceStatusBusy()가 마이크 busy 판정을 소유하고, sessionVoiceStatusView()가 마이크 disabled와 aria label까지 반환해 JSX와 toggleMic()의 중복 조건을 제거했다. recording은 계속 busy가 아니므로 두 번째 마이크 클릭으로 발화 전송이 가능하다. API/DB 계약, WebSocket/TTS lifecycle, STT/TTS provider 동작, live-coach payload, DOM/CSS는 변경하지 않았다. 검증: npm run typecheck, npm run build, session-mvp 1 passed, voice-success 2 passed, session-layout desktop/mobile 8 passed.

-

85차 적용(2026-06-29): refactor-governance P3로 Node 전환 계약 검증의 gateway-default sentinel 중복을 줄였다. scripts/check-engine-gateway-contract.mjs는 더 이상 sentinel 문자열을 자체 상수로 들고 있지 않고, engine_gateway_schema.v1.jsonx-engine-gateway-default-model-sentinel extension을 읽어 schema artifact를 SSOT로 검증한다. 공개 런타임 SSOT도 재확인해 custom domain asset을 assets/index-DgN8JVBw.js/assets/index--s3xqgO5.css로 갱신했고, OAuth start는 동작하지만 인증된 /turn proof는 여전히 owner storageState gate로 남겼다. vnet.18ka.net/api-vnet.18ka.net DNS A record는 0건이라 DONE 처리하지 않는다.

+

85차 적용(2026-06-29): refactor-governance P3로 Node 전환 계약 검증의 gateway-default sentinel 중복을 줄였다. scripts/check-engine-gateway-contract.mjs는 더 이상 sentinel 문자열을 자체 상수로 들고 있지 않고, engine_gateway_schema.v1.jsonx-engine-gateway-default-model-sentinel extension을 읽어 schema artifact를 SSOT로 검증한다. 공개 런타임 SSOT도 재확인해 custom domain asset을 assets/index-Cz4PBoO3.js/assets/index-DtVTvS9j.css로 갱신했고, OAuth start는 동작하지만 인증된 /turn proof는 여전히 owner storageState gate로 남겼다. vnet.18ka.net/api-vnet.18ka.net DNS A record는 0건이라 DONE 처리하지 않는다.

86차 적용(2026-06-29): refactor-governance P2/P3로 Session mic capture를 AudioWorklet-first 구조로 전환하고 기존 MediaRecorder를 fallback으로 남겼다. Worklet은 mono PCM16 chunk를 /voice/ws binary frame으로 보내고, control packet은 format=pcm, sample_rate, channels, sample_width를 명시한다. 백엔드는 pcm 업로드를 STT 호출 직전 RIFF/WAV로 정규화해 raw PCM을 audio/wav로 오인하던 계약 틈을 닫았다. 실제 Deepgram streaming, 물리 마이크, public WSS, 50분 양방향 실측은 계속 외부/실측 gate다. 검증: app/test_voice_ws.py 11 passed, npm run typecheck, voice-success chromium-single-run 2 passed.

87차 적용(2026-06-29): refactor-governance P3로 /voice/ws에 provider-neutral stt_result control을 추가해 EOT helper를 route 경계에 연결했다. 새 control은 text, final, silence_ms, provider_events를 받아 assess_end_of_turn()이 ready를 판단하고, pending이면 턴 실행 없이 state=listening으로 돌아간다. ready일 때만 transcript를 emit하고 _run_turn_and_speak()로 진행한다. 기존 audio_start→binary→audio_end, text_turn, ping/close 계약은 유지했다. 실제 Deepgram interim/final WSS 연결과 public WSS/물리 마이크/50분 실측은 여전히 남은 gate다. 검증: py_compile, app/test_voice_ws.py app/test_voice_service.py 27 passed.

88차 적용(2026-06-29): refactor-governance P3로 Node conformance runner의 stream event/payload 검증을 schema-driven으로 바꿨다. scripts/check-engine-gateway-contract.mjs는 더 이상 token/done/error 이벤트 목록과 payload 필드를 로컬 상수·분기로 들고 있지 않고, engine_gateway_schema.v1.jsonStreamPacket.oneOf, payload $ref, x-engine-gateway-sse-events를 읽어 golden fixture를 검증한다. raw token text fallback과 data: [DONE] compatibility line은 유지했다. 검증: node scripts\check-engine-gateway-contract.mjs --json, 당시 gateway/backend focused suite 통과, 이후 최신 L1 focused 120 passed로 재검증.

@@ -548,6 +548,7 @@

113차 적용(2026-07-01): 학습자 /learn/practice 페르소나 리스트가 긴 한글 주호소 요약에서 카드 내부를 클립하던 문제를 수정했다. .lh-list-pane의 viewport 고정 높이와 내부 스크롤을 풀고, .lh-personas/.lh-persona가 콘텐츠 높이를 그대로 만들게 했으며, .lh-persona__summary 3줄 clamp를 제거했다. learner.spec.ts에는 각 페르소나 카드의 summary/body/card scroll overflow가 0인지 재는 regression guard를 추가했다. 검증: npm run typecheck, npm run build, 브라우저 rect smoke 1095px/1440px에서 카드 overflow 0 및 문서 가로 overflow 0. 현재 로컬 127.0.0.1:8000/health가 timeout이라 real API 기반 전체 learner.spec.ts는 이번 패스에서 미실행.

114차 적용(2026-07-01): 회기 종료 후 background deep 평가가 실제 durable DB row로 저장되는지 fixture 없이 검증했다. _schedule_session_evaluation()은 생성한 task를 반환하고 done callback에서 crash/cancel을 로그로 관찰하며, 평가 결과·timeout/error 저장이 durable store에 닿지 않으면 명시 error 로그를 남긴다. 교수자 명시 재평가 API도 평가 산출물이 생성됐지만 저장 실패하면 503으로 표면화한다. GET /eval/sessions/{id}/evaluationstatus/error/durable을 노출해 캐시 착시와 DB 저장 증거를 구분한다. 새 DB-backed E2E는 학습자 세션 생성→턴→종료→teacher 로그인→/eval/.../evaluation polling→교수자 리뷰 UI 평가 완료까지 실제 API/DB/엔진으로 확인한다. 검증: API 전체 309 passed, npm run check:api-types, npm run typecheck, PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 7 passed, PLAYWRIGHT_PORT=5203 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 16 passed, 현재 Playwright 수집 기준 162 tests in 18 files.

115차 적용(2026-07-01): 교수자 학생 분석을 /teach 대시보드에서 분리해 좌측 메뉴의 /teach/analysis 전용 작업면으로 이동했다. 교수 콘솔은 검토 큐·위기 알림·페르소나 검수·최근 회기 triage만 맡고, 학생 분석은 학습자 목록 레일, 사용자별 추이, 단계 분포, 전체 회기 타임라인, 회기 리뷰 드릴다운 구조를 맡는다. 회귀 방지로 teacher.spec.ts는 콘솔에 분석 패널이 없고 메뉴를 통해 학생 분석 페이지로 이동하는지 확인하며, layout-visual-gate.spec.ts는 학생 분석 페이지를 7개 폭에서 별도 캡처·검사한다. 검증: npm run typecheck, npm run build, npm run check:api-types, PLAYWRIGHT_PORT=5211 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1 9 passed, PLAYWRIGHT_PORT=5214 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1 11 passed, SSOT checker PASS, 현재 Playwright 수집 기준 163 tests / 18 files.

+

116차 적용(2026-07-01): 원천문서 갭 E2E 목표에 맞춰 C1/C2 silent failure를 추가로 닫았다. C1은 학습자가 저장한 사례개념화 워크시트가 app.case_worksheet를 거쳐 교수자 검토 화면 read-only 값으로 복원되고, 교수자 수정요청 검수 메모가 teacherReview.worksheetStatus/worksheetNote로 재조회되는지 실제 브라우저/API/DB E2E로 고정했다. C2는 app.safety_events가 runtime bootstrap/healthcheck에서 빠져 DB 정상처럼 보일 수 있던 구멍과, jsonb codec 위에 이미 직렬화한 문자열을 넘겨 detail이 JSON string으로 저장되던 구멍을 수정했다. record_safety_event()는 RLS용 ai_context=true로 저장하고, alert list 실패는 non-dev에서 fail-closed한다. 검증: python -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_runtime_policy.py -q 59 passed, PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet|crisis safety" 2 passed, 현재 Playwright 수집 기준 166 tests in 18 files.

20차 적용(2026-06-28): 신규 Google/SAML 사용자는 account_status=pending으로 시작하고 승인 전에는 /pending 안내 화면만 본다. yunchan@twentyoz.kr는 슈퍼 관리자 allowlist로 admin+approved를 받으며, /admin/users는 가입 승인 탭에서 pending 계정을 승인 또는 보류 처리한다.

21차 적용(2026-06-28): 관리자 페이지 진입권을 기본 역할과 분리해 app_user.admin_access로 저장한다. AUTH_SUPER_ADMIN_EMAILS 기본값은 yunchan@twentyoz.kr, hoonjungkoo@hs.ac.kr이며, 슈퍼 관리자는 학습자·교수자·관리자 공간 전환과 관리자 권한 부여/회수를 할 수 있다. 학생·교수 계정도 admin_access=true면 우측 상단 관리자 진입이 노출된다. 구성 슈퍼 관리자의 권한 회수와 계정 비활성화는 차단한다.

25차 적용(2026-06-28): /admin/users에서 허용 도메인 밖 이메일도 정확한 계정 단위로 강제 등록할 수 있다. Google/SAML/dev-login은 미리 등록된 이메일만 도메인 게이트 예외로 통과시키고, provider 로그인 시 기존 관리 row의 역할·코호트·승인 상태를 이어받는다. 미등록 외부 도메인 로그인은 계속 차단한다.

@@ -918,7 +919,7 @@ 범위명령 / 확인 대상결과 Web typechecknpm run typecheckPassed - Full Playwright E2E baselinenpx playwright test --list현재 수집 기준 162 tests / 18 files. 최신 변경은 focused gate 행으로 별도 검증한다. + Full Playwright E2E baselinenpx playwright test --list현재 수집 기준 166 tests / 18 files. 최신 변경은 focused gate 행으로 별도 검증한다. API typegen SSOTnpm run check:api-typesPassed; FastAPI OpenAPI → src/lib/api.gen.ts stale check Contract SSOT aggregate DTOpy -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 / npm run check:api-types / npm run typecheck / npm run build50 backend passed; learner sessions, session review/worksheet, teacher dashboard, session start/detail DTOs use generated ApiSchema aliases with UI fallback. Stage responses are OpenAPI enum unions, including review phase key/label, reached phase, evaluation summary/trigger responses, and teacher session/growth stage. .github/workflows/api-contract.yml runs npm run check:api-types on API/Web contract changes. SEO/GEO share cardspytest app/test_session_share.py app/test_session_turn_persistence.py -q / npm run generate:api-types / npm run typecheck21 passed; session share creates hashed-token public unfurl payload without raw transcript, revoked token returns 404, OpenAPI generated share DTOs, review screen share button typechecks. Static robots.txt/sitemap.xml/llms.txt added. @@ -931,10 +932,10 @@ L1 session read-model splitpython -B -m py_compile app/session_read_model.py app/routes/sessions.py / python -B -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_evaluation_persistence.py app/test_session_share.py app/test_learner_dashboard.py app/test_rbac_idor.py app/test_teacher_dashboard.py -q / npm run check:api-types / npm run typecheckpy_compile passed; focused backend 48 passed; OpenAPI generated API type check and web typecheck passed. app/session_read_model.py now owns session list/dashboard/detail/review/share DTOs and deterministic builders while routes/sessions.py keeps auth, RLS-backed reads, persistence, and route decorators. L1 persona read/generation splitpy -3.11 -X utf8 -B -m py_compile app/persona_generation_contract.py app/persona_read_model.py app/routes/personas.py app/test_persona_generation_contract.py app/test_persona_review.py / py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_persona_generation_contract.py app/test_persona_review.py -q / npm run check:api-types / npm run typecheckpy_compile passed; persona contract+review 39 passed; OpenAPI generated API type check and web typecheck passed. app/persona_read_model.py owns persona catalog/review/draft/source/evidence DTOs and mappers, while app/persona_generation_contract.py owns draft schema, prompt bundle, GenerateResponse payload extraction, and generated draft defaults/coercion. routes/personas.py keeps auth, teacher/admin gates, DB repository calls, RAG source registration, engine invocation, provenance assembly, and HTTP error mapping. M1 provider_events preservationpython -X utf8 -m pytest -p no:cacheprovider app/test_runtime_policy.py app/test_session_turn_persistence.py app/test_voice_ws.py app/test_voice_service.py -q77 passed; voice control/STT provider events are allowlisted, size-limited, and tagged with internal event_type/category taxonomy into app.turns.provider_events. 인증된 review nonverbal은 정규화 taxonomy 일부만 파생 칩으로 노출하며 raw transcript/text/provider/source/raw type은 응답하지 않는다. Runtime readiness now requires all five turn voice metadata columns. - C1 case worksheetpy -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 / scripts/check-case-worksheet-rubric.py --json / npm run check:api-types / npm run typecheck / npm run build / session-review.spec.ts / local API smoke40 passed for worksheet persistence/RBAC/teacher review plus rubric scaffold; session review exposes transcript-grounded caseWorksheet, learner-edited saved_by_learner worksheet persists through app.case_worksheet, teacher/admin teacherReview exposes manual worksheetStatus/worksheetNote/worksheetReviewedAt, and case-worksheet-rubric@scaffold-2026-06-28 validates 5 sections/28 items with scoring_enabled=false. Local smoke returned saved_by_learner:local smoke saved worksheet. Stage labels are normalized at response boundaries. + C1 case worksheetpy -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 / scripts/check-case-worksheet-rubric.py --json / PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet" / local API smoke40 passed for worksheet persistence/RBAC/teacher review plus rubric scaffold; session review exposes transcript-grounded caseWorksheet, learner-edited saved_by_learner worksheet persists through app.case_worksheet, teacher/admin teacherReview exposes manual worksheetStatus/worksheetNote/worksheetReviewedAt, and full browser+API+DB E2E now verifies learner save → teacher read-only review → 수정요청 persistence. case-worksheet-rubric@scaffold-2026-06-28 still validates 5 sections/28 items with scoring_enabled=false. M3 auth claim mappingpython -B -m pytest -p no:cacheprovider app/test_auth_providers.py -q30 passed; Google/SAML cohort maps, provider external_id, provider error reason, PKCE/state, HttpOnly cookie, SAML fixture ACS covered Auth managed-user upsert boundarypy_compile / py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_auth_providers.py app/test_admin_ops.py app/test_rbac_idor.py app/test_runtime_policy.py app/test_session_turn_persistence.py app/test_learner_dashboard.py app/test_teacher_dashboard.py -q / npm run check:api-typesManagedUserUpsertInput owns managed-user create/reactivate inputs; create_session(), admin user create, and direct auth regression calls no longer pass long keyword bags. ManagedUserMemoryInput remains DB→memory fallback/store sync, and ManagedUserPatch remains partial profile/admin update. Focused backend 103 passed; API type drift check passed. - CrisisGate 109python -m pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q20 passed; real crisis stops before engine, returns 109 resource, safety event DB insert payload covered + CrisisGate 109python -m pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q / PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "crisis safety"real crisis stops before engine, returns 109 resource, and DB insert uses ai_context=true. db.healthcheck() now requires app.safety_events; alert detail handles legacy double-encoded JSON string rows; full browser+API+DB E2E verifies safety_events reaches the DB-backed teacher safety queue with resource 109. Professor review workflowpython -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q / PLAYWRIGHT_PORT=5211 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1 / PLAYWRIGHT_PORT=5214 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1 / npm run check:api-types / npm run typecheck / npm run build5 backend passed + 9 E2E passed + strict visual gate 11 passed; /teach now keeps only triage queues, and /teach/analysis returns a selected learner's full session timeline, unrestricted trend points, stage breakdown, and review counts in a separate student analysis page. Learner/teacher growth metricspy -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_session_turn_persistence.py app/test_rbac_idor.py app/test_teacher_dashboard.py app/test_learner_dashboard.py -q59 backend passed; shared session_metrics now prefers rehydrated label_ko for technique labels while preserving label/name/id/code fallback. alternative_utterances string and text/suggestion dict inputs are pinned before any future evaluator adapter extraction. Teacher review statuspytest app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_learner_dashboard.py -q / npm run typecheck / npm run build / teacher.spec.ts12 backend passed + 4 E2E passed; app.session_review_status, teacher dashboard review_status/review_note/reviewed_at, worksheet worksheet_status/worksheet_note/worksheet_reviewed_at, and PUT /teacher/sessions/{session_id}/review-status are documented. 검토 큐 visible 회귀와 교수자 전용 review grid 영역도 테스트로 고정했다. @@ -946,7 +947,7 @@ P1/H4 masking gateapp/test_pii_masking_eval.py app/test_orchestrator_masking.py app/test_evaluation_persistence.py app/test_session_turn_persistence.py47 passed; 최신 보정 회귀 59 passed. phone/email/RRN 및 한국어 NAME/ORG raw 값이 generate/stream/evaluator payload와 client text_masked에 남지 않음. Optional ko recognizer fake span은 [NAME]/[ORG]로 마스킹되고 같은 문장의 phone은 후단 regex가 처리하며, adapter 실패 시에도 regex fallback이 유지된다. Synthetic ko fixture 16/16 pass, 자연 발화형 이름 라벨·자기소개와 negative control 포함, input/report schema validation, summary-only report(evidence_text_included=false), entity recall 1.0, forbidden substring removal 1.0, unexpected entity violations 0. P2a RBAC/audit/visibilityapp.test_rbac_idor11 tests OK; other learner 403, read_session audit, evaluator-only hidden, teacher session review read allowed, super_admin learner review principal is promoted to admin for evaluation/worksheet/review-status loaders, admin_access-only learner remains blocked, and learner worksheet write remains 403 Admin access delegationpython -B -m pytest -p no:cacheprovider app/test_auth_providers.py app/test_runtime_policy.py app/test_rbac_idor.py app/test_teacher_dashboard.py app/test_learner_dashboard.py -q / npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 / npm run check:api-types / npm run typecheck / npm run build70 backend focused passed + admin/auth/runtime 64 passed + admin E2E 8 passed. 실제 admin role은 학습자·교수자·관리자 공간을 모두 열고, admin_access만 받은 비관리자 계정은 관리자 콘솔 진입권만 얻는다. 관리자 sidebar는 운영 홈/사용자/권한/티켓과 교수자·학습자 작업 공간 링크를 함께 노출한다. - Session read-model DB readinesspython scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets / https://api-vignette.chanpaca.net/health2026-06-29 prod 503 원인은 운영 DB의 app.turns.provider_events 컬럼 누락이었다. 운영 DB hotfix 후 /teacher/dashboard code path는 source=database로 복구됐다. 현재 db.healthcheck(), runtime table readiness, deploy preflight DB mode는 app.turns 음성 메타 컬럼 5개(audio_ref, silence_ms, speech_rate, barge_in, provider_events)와 app.session_review_status worksheet 컬럼을 함께 검증한다. 2026-06-30에는 Docker Desktop/DB 중단으로 public API 530/error code 1033이 재발했지만, Docker Desktop/DB 재기동 뒤 public health가 environment=prod, db=true, engine=true로 복구됐다. + Session read-model DB readinesspython scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets / https://api-vignette.chanpaca.net/health2026-06-29 prod 503 원인은 운영 DB의 app.turns.provider_events 컬럼 누락이었다. 운영 DB hotfix 후 /teacher/dashboard code path는 source=database로 복구됐다. 현재 db.healthcheck(), runtime table readiness, deploy preflight DB mode는 app.turns 음성 메타 컬럼 5개(audio_ref, silence_ms, speech_rate, barge_in, provider_events), app.session_review_status worksheet 컬럼, app.safety_events 필수 컬럼을 함께 검증한다. 2026-06-30에는 Docker Desktop/DB 중단으로 public API 530/error code 1033이 재발했지만, Docker Desktop/DB 재기동 뒤 public health가 environment=prod, db=true, engine=true로 복구됐다. Admin usage persistencepython -m pytest app/test_admin_ops.py app/test_runtime_policy.py -q / authenticated local public-API smoke26 passed; /admin/usage returns 200 with source=database, durable=true. Cloudflare blocked raw Python public smoke with 1010, so app-level HTTP was verified against 127.0.0.1:8001 using the same prod process. Synthetic health samplerrecord_admin_health_sample(), record-admin-health-sample.py, install-health-sampler-task.ps1Backend focused 31 passed. Python compile/help passed; PowerShell parser + -PrintOnly passed. Local one-shot appended 5 service rows to app.admin_health_event: status ok, engine_mode claude_cli. This remains sample history, not an SLA claim. Health retention/rollupadmin_health_daily_rollup, maintain-admin-health-events.py, /admin/uptimeBackend focused 36 passed. Python compile/help passed. Local dry-run returned rollup_event_count=0, prunable_event_count=0. Uptime summary now combines raw samples with daily rollups when raw rows have been pruned; detailed events stay raw-only. diff --git a/docs/guides/source-docs-and-gaps.md b/docs/guides/source-docs-and-gaps.md index 5a224a6..83e1a4b 100644 --- a/docs/guides/source-docs-and-gaps.md +++ b/docs/guides/source-docs-and-gaps.md @@ -51,8 +51,8 @@ | ID | 갭 | 현재상태 | 권고 | 근거 | |---|---|---|---|---| -| **C1** | 사례개념화·치료계획 산출물 저장형 구조 4차 구현·루브릭 외부화 scaffold·수동 교수자 검수 골격 완료·채점/정책 잔여 | `SessionReviewResponse.caseWorksheet`가 축어록 근거 기반 초안을 제공하고, 리뷰 화면에서 학습자가 편집한 워크시트를 `PUT /sessions/{id}/review/worksheet`로 `app.case_worksheet`에 저장한다. 이후 `GET /review`는 `saved_by_learner` 저장본을 자동 초안보다 우선 반환한다. 3차에서는 `data/rubrics/case-worksheet-rubric.json`, Draft 2020-12 schema, `app.services.case_worksheet_rubric`, `scripts/check-case-worksheet-rubric.py`를 추가해 임상팀 확정 루브릭을 코드 수정 없이 받을 scaffold를 만들었다. 현재 rubric은 `scaffold_only`/`scoring_enabled=false`이며 서버 생성 워크시트 5개 section/28개 item key와 일치하는지만 검증한다. 템플릿 key source는 `CASE_WORKSHEET_SECTION_SPECS`와 `case_worksheet_template_item_keys()`로 명시해 validator/CLI/test가 더미 턴 없이 같은 생성 spec을 참조한다. 4차에서는 기존 `app.session_review_status`에 `worksheet_status`/`worksheet_note`/`worksheet_reviewed_at`을 붙이고, `PUT /teacher/sessions/{id}/review-status`와 교수자 `SessionReview` 카드에서 승인·수정요청·반려 수동 판정을 저장/표시한다. CCD는 숨은 정답키로 남고 저장본에 점수로 노출하지 않는다. | 임상팀 확정 루브릭 콘텐츠(level anchor, 가중치, 컷오프, 예시 답안), AI 채점 적용·calibration, 승인 후 잠금·재제출 정책, 재귀학습 데이터셋 approved 연계. | doc1·doc4·doc5 | -| **C2** | 위기개입 프로토콜·생명유지서약·에스컬레이션 1차 배선 완료·임상 고도화 필요 | 실제 자해·자살 신호는 LLM/엔진 호출 전 중단하고 109 리소스와 `conversation_stopped`를 REST/SSE/voice 응답에 싣는다. `crisis.risk_level`은 상태머신 `ideation_observed`로 전달하고, escalate 시 `app.safety_events` detail 적재 및 교수자 대시보드 안전 알림 큐로 연결한다. | 남은 것은 임상팀 콘텐츠: 비밀보장 예외고지→단계적 탐색→생명유지서약 스크립트, 실시간 push/메일 알림 정책, 위기탐색 누락 시 회기리뷰 감점 루브릭. | doc1·doc5(핵심 시나리오)·doc4(IRB 전제) | +| **C1** | 사례개념화·치료계획 산출물 저장형 구조 4차 구현·루브릭 외부화 scaffold·수동 교수자 검수 골격 완료·채점/정책 잔여 | `SessionReviewResponse.caseWorksheet`가 축어록 근거 기반 초안을 제공하고, 리뷰 화면에서 학습자가 편집한 워크시트를 `PUT /sessions/{id}/review/worksheet`로 `app.case_worksheet`에 저장한다. 이후 `GET /review`는 `saved_by_learner` 저장본을 자동 초안보다 우선 반환한다. 3차에서는 `data/rubrics/case-worksheet-rubric.json`, Draft 2020-12 schema, `app.services.case_worksheet_rubric`, `scripts/check-case-worksheet-rubric.py`를 추가해 임상팀 확정 루브릭을 코드 수정 없이 받을 scaffold를 만들었다. 현재 rubric은 `scaffold_only`/`scoring_enabled=false`이며 서버 생성 워크시트 5개 section/28개 item key와 일치하는지만 검증한다. 템플릿 key source는 `CASE_WORKSHEET_SECTION_SPECS`와 `case_worksheet_template_item_keys()`로 명시해 validator/CLI/test가 더미 턴 없이 같은 생성 spec을 참조한다. 4차에서는 기존 `app.session_review_status`에 `worksheet_status`/`worksheet_note`/`worksheet_reviewed_at`을 붙이고, `PUT /teacher/sessions/{id}/review-status`와 교수자 `SessionReview` 카드에서 승인·수정요청·반려 수동 판정을 저장/표시한다. 2026-07-01 E2E는 학습자 저장본이 교수자 read-only 검토 화면으로 복원되고 `수정요청` 검수 메모가 DB-backed `teacherReview`로 재조회되는지 검증한다. CCD는 숨은 정답키로 남고 저장본에 점수로 노출하지 않는다. | 임상팀 확정 루브릭 콘텐츠(level anchor, 가중치, 컷오프, 예시 답안), AI 채점 적용·calibration, 승인 후 잠금·재제출 정책, 재귀학습 데이터셋 approved 연계. | doc1·doc4·doc5 | +| **C2** | 위기개입 프로토콜·생명유지서약·에스컬레이션 1차 배선 완료·임상 고도화 필요 | 실제 자해·자살 신호는 LLM/엔진 호출 전 중단하고 109 리소스와 `conversation_stopped`를 REST/SSE/voice 응답에 싣는다. `crisis.risk_level`은 상태머신 `ideation_observed`로 전달하고, escalate 시 `app.safety_events` detail 적재 및 교수자 대시보드 안전 알림 큐로 연결한다. 2026-07-01 E2E는 실제 브라우저 위기 발화가 `app.safety_events`를 거쳐 DB-backed 교수자 안전 큐의 109 알림으로 표시되는지 검증한다. `db.healthcheck()`는 이제 `app.safety_events` schema를 요구하고, JSONB detail double-encoding legacy row도 안전하게 읽는다. | 남은 것은 임상팀 콘텐츠: 비밀보장 예외고지→단계적 탐색→생명유지서약 스크립트, 실시간 push/메일 알림 정책, 위기탐색 누락 시 회기리뷰 감점 루브릭. | doc1·doc5(핵심 시나리오)·doc4(IRB 전제) | | **C3** | 이론모드 2차 배선·학습자 명시 선택 UI 완료·CBT 콘텐츠 잔여 | `TurnContext`/`prepare_turn`/sessions/voice/evaluator에 `theory_mode`가 전달되고, `build_turn_messages`도 인간중심·CBT·통합 이론 프레이밍을 엔진 메시지에 넣는다. 프론트는 `persona.theory_target` 기준 기본값을 잡되, 세션 시작 전 `humanistic`/`cbt`/`integrative` segmented control로 학습자가 명시 선택하고 `POST /sessions`의 `theory_mode`로 전송한다. | 임상팀이 확정한 CBT 단계 프롬프트 체인, 이론부합 채점 루브릭. 이번 UI는 임상 문안·루브릭·백엔드 enum을 확장하지 않았다. | doc4(humanistic+CBT 필수)·doc2/doc5 | ### High (4) @@ -85,8 +85,8 @@ ### A. 즉시 착수 가능 (내부 코드, 외부 합의 불요) -- **C1 4차 완료**: `caseWorksheet` 응답 구조, 리뷰 화면 편집 UI, `app.case_worksheet` 저장/재조회 경로, 외부 루브릭 schema/loader/validation scaffold, 교수자 수동 워크시트 검수 상태 저장/표시. 워크시트 템플릿 key source는 `CASE_WORKSHEET_SECTION_SPECS`/`case_worksheet_template_item_keys()`로 명시했다. 후속은 임상팀 확정 루브릭 콘텐츠, AI 채점 보정, 승인 후 잠금·재제출 정책. -- **C2 1차 완료**: 위기 신호는 엔진 전 중단, 109 안내, `safety_events` 적재, 교수자 알림 큐, `ideation_observed` 전달까지 배선했다. 후속은 임상 스크립트·서약 문안·감점 루브릭. +- **C1 4차 완료 + E2E 고정**: `caseWorksheet` 응답 구조, 리뷰 화면 편집 UI, `app.case_worksheet` 저장/재조회 경로, 외부 루브릭 schema/loader/validation scaffold, 교수자 수동 워크시트 검수 상태 저장/표시를 구현했고, 실제 브라우저/API/DB E2E가 learner-save→teacher-review→수정요청 재조회를 검증한다. 워크시트 템플릿 key source는 `CASE_WORKSHEET_SECTION_SPECS`/`case_worksheet_template_item_keys()`로 명시했다. 후속은 임상팀 확정 루브릭 콘텐츠, AI 채점 보정, 승인 후 잠금·재제출 정책. +- **C2 1차 완료 + E2E 고정**: 위기 신호는 엔진 전 중단, 109 안내, `safety_events` 적재, 교수자 알림 큐, `ideation_observed` 전달까지 배선했다. 실제 브라우저/API/DB E2E가 crisis stream→`app.safety_events`→교수자 안전 큐 109를 검증한다. 후속은 임상 스크립트·서약 문안·감점 루브릭. - **C3 2차 완료**: `theory_mode`가 세션·평가·생성 프롬프트까지 흐르고, 학습자는 세션 시작 전 기존 3개 모드 중 하나를 명시 선택해 `POST /sessions`로 보낸다. 후속은 임상팀 CBT 체인·이론부합 루브릭. - **H2 1차+라이브 코칭+리뷰 3열 workbench 완료**: `make_eval_hook`과 stream 평가가 턴 파이프라인에 붙고 정규화 테이블로 적재·복원된다. 대안발화도 `app.alternative_utterance`로 정규화한다. 라이브 코칭은 0615 워크북·DSM·공식 지침 요약/RAG 근거로 코칭 아바타 말풍선·근거 모달·발화별 이력 오버레이까지 연결했다. `POST /kb/live-coach/source-packs/sync`는 공용 source pack sync service를 통해 evaluator 전용 RAG에 증분 색인하고, hash 변경 시 document version을 최신+1로 올린다. 관리자 sync/CLI는 source pack manifest 생성 전 process-local cache를 refresh해 현재 repo JSON 기준으로 비교한다. 학습자 `SessionReview` 데스크톱은 요약/흐름, 축어록, 평가 rail의 3열 workbench와 하단 워크시트로 재배치했고, 교수자/모바일 레이아웃은 기존 규칙을 유지한다. 후속은 골든셋·임상팀 루브릭·source pack 임상 검수 상태 운영. - **H3 10차 완료**: 페르소나 저작 CRUD(draft→review), P4~P7 저장소 JSON 로드, RAG source 기반 draft generation, prompt bundle id/version/hash provenance와 seed/version materializer runner가 붙었다. seed runner는 dry-run/JSON manifest를 제공하고, `--apply`에서만 DB pool을 초기화해 기존 materializer를 호출한다. repo-managed source pack runner는 DB-backed dry-run/apply를 제공하며 active `content_hash`가 바뀐 문서만 최신 version+1로 sync한다. `kb.raw_source_artifact` hash-only 레코드와 `rag.index_document()` raw/sensitivity=3 fail-closed guard를 추가해 raw 원문이 `kb.chunk`/embedding/FTS로 들어가지 않게 했고, `app/persona_read_model.py`로 persona DTO/mapper 경계를 분리했다. PersonaStudio의 자동사고·회기 시나리오·말투 목록·역린/금기 목록은 행 추가/삭제 UI로 바꾸고 기존 배열 payload 계약을 유지한다. 이번 패스에서 프롬프트 미리보기 raw JSON textarea를 라벨형 검토 섹션으로 교체했다. 후속은 루브릭·이론 콘텐츠 외부화, 임상팀 최종 검수 evidence, 암호화 blob/vault 기반 원문 실저장. diff --git a/docs/guides/testing.md b/docs/guides/testing.md index 4d5a71f..83dd5ed 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -20,7 +20,7 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타 | API 타입 생성 체크 | `apps/web` | `npm run check:api-types` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass | | 웹 타입체크 | `apps/web` | `npm run typecheck` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass | | 웹 빌드 | `apps/web` | `npm run build` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass | -| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 162 tests / 18 files | +| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 166 tests / 18 files | 핵심 원칙: **단위 테스트(pytest)와 타입체크/빌드는 외부 서비스 없이 단독 실행된다.** **E2E만 풀스택(DB+API+웹+브라우저)을 요구한다.** 아래 각 절에서 근거와 절차를 설명한다. @@ -258,14 +258,15 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API ### 3.6 실측 테스트 개수 (현재) -`npx playwright test --list` 기준 **총 162 tests / 18 files**. +`npx playwright test --list` 기준 **총 166 tests / 18 files**. - **병렬 시나리오**: 130 tests (desktop 65 + mobile 65) -- **`@single-run` 직렬 시나리오**: 32 tests (DB 영속화·세션 MVP·음성 성공경로·회기말 평가 저장·교수자 사용자별 분석 등) +- **`@single-run` 직렬 시나리오**: 36 tests (DB 영속화·세션 MVP·음성 성공경로·회기말 평가 저장·교수자 사용자별 분석 등) - `e2e/voice-success.spec.ts`는 직접 `/voice/ws` 캐스케이드와 Session 마이크 UI를 함께 검증하며, 브라우저 `