vignette/docs/dev_dashboard.html
Yun Chan 085460b5e0 대시보드 폴드아웃/드릴다운 정리 + 페르소나 역린·misconduct 반응 + 게이트웨이 격리·RAG 비차단 수정
SSOT 대시보드:
- 한신대 기술분석 PDF(19쪽) 정합성 분석 + 이번 세션 발견 섹션 추가
- 섹션 폴드아웃(접기)·상단 목차(드릴다운)·모두 펼치기/접기 — 내용 보존, 레이아웃만 정리

페르소나 반응 강화('저항·반응 조절' 핵심 차별):
- PersonaCard.triggers(역린) 필드 + CCD 핵심상처 파생 역린 블록
- L0에 무례·모욕·조롱 시 현실적 동맹 균열 반응 지침

버그·성능 수정(라이브/E2E로 포착):
- 게이트웨이 페르소나 격리: --append-system-prompt를 --system-prompt(교체)로 + --exclude-dynamic-system-prompt-sections (내담자 캐릭터 붕괴·개발맥락 누출 차단)
- RAG: 임베더 동기 로드(약 7-13초)를 _warm_rag_caches 백그라운드 warm으로(세션 생성 블로킹 회귀 수정)
- voice TTS RMS 데드힌트 제거, init_state OpennessParams 파라미터객체화
- 한국어 PII(날짜·금액·주소) 마스킹 보강
- 레이아웃 시각 게이트: 폼 컨트롤 값 스크롤 오탐 제외(7/7)

검증: 백엔드 84/84, E2E 42(데스크톱 27·모바일 11·아바타 4), 시각 게이트 7/7
2026-06-27 02:30:46 +09:00

1184 lines
102 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vignette 개발 대시보드</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/variable/pretendardvariable.min.css">
<style>
:root {
--paper:#FBFAF8;
--paper-2:#F4F2EE;
--surface:#FFFFFF;
--hair:#E5E1DA;
--ink:#1C2A2A;
--ink-2:#5A6663;
--ink-3:#93A09C;
--accent:#3E7A6E;
--accent-deep:#316257;
--accent-tint:#EEF4F2;
--clay:#B0735C;
--pos:#2C6E49;
--warn:#8A5A00;
--crit:#A8352C;
--pos-bg:#E8F3EC;
--warn-bg:#FAF1E0;
--crit-bg:#FAEAE8;
--info-bg:#EAF1F6;
--font:"Pretendard Variable", Pretendard, -apple-system, "Apple SD Gothic Neo", "Noto Sans KR", system-ui, sans-serif;
--num:"Pretendard Variable", "JetBrains Mono", ui-monospace, monospace;
--radius:8px;
}
* { box-sizing:border-box; }
body {
margin:0;
background:var(--paper);
color:var(--ink);
font-family:var(--font);
line-height:1.55;
letter-spacing:0;
}
header {
position:sticky;
top:0;
z-index:2;
display:flex;
align-items:center;
justify-content:space-between;
gap:24px;
min-height:58px;
padding:0 32px;
background:rgba(255,255,255,.92);
border-bottom:1px solid var(--hair);
backdrop-filter:blur(10px);
}
.brand { display:flex; align-items:center; gap:10px; font-weight:760; }
.mark { width:24px; height:24px; border:2px solid var(--accent); border-radius:50%; position:relative; }
.mark:after { content:""; width:6px; height:6px; border-radius:50%; background:var(--clay); position:absolute; left:7px; top:6px; }
.stamp { color:var(--ink-3); font:600 12px/1 var(--num); }
main { width:min(1180px, calc(100% - 40px)); margin:0 auto; padding:42px 0 64px; }
.hero { display:grid; grid-template-columns:minmax(0,1.2fr) minmax(280px,.8fr); gap:28px; align-items:start; margin-bottom:34px; }
.kicker { display:flex; align-items:center; gap:8px; color:var(--accent); font:700 12px/1.3 var(--num); letter-spacing:.08em; text-transform:uppercase; }
.kicker:before { content:""; width:6px; height:6px; border-radius:50%; background:var(--accent); }
h1 { margin:12px 0 0; font-size:30px; line-height:1.28; letter-spacing:0; }
h1 em { color:var(--accent-deep); font-style:normal; }
.lead { margin:12px 0 0; color:var(--ink-2); max-width:760px; font-size:15px; }
.summary {
display:grid;
grid-template-columns:repeat(2,minmax(0,1fr));
gap:1px;
overflow:hidden;
border:1px solid var(--hair);
border-radius:12px;
background:var(--hair);
}
.metric { min-width:0; padding:18px; background:var(--surface); }
.metric span { display:block; color:var(--ink-3); font-size:12px; font-weight:650; }
.metric b { display:block; margin-top:6px; color:var(--ink); font:760 28px/1 var(--num); }
section { margin-top:30px; }
.section-head { display:flex; align-items:end; justify-content:space-between; gap:18px; margin-bottom:14px; }
.section-head h2 { margin:0; font-size:20px; line-height:1.3; }
.section-head p { margin:4px 0 0; color:var(--ink-3); font-size:13px; }
.grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:16px; }
.panel {
min-width:0;
background:var(--surface);
border:1px solid var(--hair);
border-radius:12px;
padding:20px;
box-shadow:0 1px 2px rgba(28,42,42,.05);
}
.panel h3 { margin:0 0 10px; font-size:16px; line-height:1.35; }
.panel p { margin:0; color:var(--ink-2); font-size:13px; }
.tag { display:inline-flex; align-items:center; gap:6px; margin-bottom:12px; padding:3px 9px; border-radius:999px; font-size:12px; font-weight:680; }
.tag:before { content:""; width:7px; height:7px; border-radius:50%; }
.done { color:var(--pos); background:var(--pos-bg); }
.done:before { background:var(--pos); }
.doing { color:var(--warn); background:var(--warn-bg); }
.doing:before { background:var(--warn); }
.risk { color:var(--crit); background:var(--crit-bg); }
.risk:before { background:var(--crit); }
.info { color:#2E5A7A; background:var(--info-bg); }
.info:before { background:#3C7299; }
.list { display:grid; gap:10px; margin:0; padding:0; list-style:none; }
.list li { display:grid; grid-template-columns:10px minmax(0,1fr); gap:10px; color:var(--ink-2); font-size:13px; }
.list li:before { content:""; width:6px; height:6px; margin-top:8px; border-radius:50%; background:var(--accent); }
.roadmap {
display:grid;
grid-template-columns:repeat(5,minmax(0,1fr));
gap:12px;
align-items:stretch;
}
.phase-card {
min-width:0;
background:var(--surface);
border:1px solid var(--hair);
border-radius:12px;
padding:16px;
box-shadow:0 1px 2px rgba(28,42,42,.05);
}
.phase-card.current { border-color:color-mix(in srgb, var(--accent) 42%, var(--hair)); background:linear-gradient(180deg, var(--accent-tint), var(--surface) 58%); }
.phase-card.next { border-color:color-mix(in srgb, var(--warn) 35%, var(--hair)); }
.phase-top { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:10px; }
.phase-id { font:760 13px/1 var(--num); color:var(--accent-deep); }
.phase-card h3 { margin:0; font-size:15px; line-height:1.35; }
.phase-card p { margin:8px 0 0; color:var(--ink-2); font-size:12px; }
.phase-list { display:grid; gap:6px; margin:12px 0 0; padding:0; list-style:none; }
.phase-list li { color:var(--ink-2); font-size:12px; line-height:1.45; }
.phase-list li b { color:var(--ink); font-weight:700; }
.source-note {
margin-top:14px;
padding:14px 16px;
border:1px solid var(--hair);
border-radius:12px;
background:var(--paper-2);
color:var(--ink-2);
font-size:13px;
}
.tabs {
border:1px solid var(--hair);
border-radius:12px;
background:var(--surface);
overflow:hidden;
box-shadow:0 1px 2px rgba(28,42,42,.05);
}
.tabbar {
display:flex;
gap:0;
overflow-x:auto;
border-bottom:1px solid var(--hair);
background:var(--paper-2);
}
.tab-button {
appearance:none;
border:0;
border-right:1px solid var(--hair);
background:transparent;
color:var(--ink-2);
padding:13px 16px;
font:700 13px/1.2 var(--font);
white-space:nowrap;
cursor:pointer;
}
.tab-button:hover { background:rgba(62,122,110,.08); color:var(--ink); }
.tab-button[aria-selected="true"] { background:var(--surface); color:var(--accent-deep); box-shadow:inset 0 -2px 0 var(--accent); }
.tab-panel { padding:20px; }
.tab-panel[hidden] { display:none; }
.detail-head { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:18px; align-items:start; margin-bottom:16px; }
.detail-head h3 { margin:0; font-size:18px; line-height:1.35; }
.detail-head p { margin:6px 0 0; color:var(--ink-2); font-size:13px; }
.mini-metrics { display:flex; flex-wrap:wrap; gap:8px; justify-content:flex-end; }
.mini-metrics span { display:inline-flex; align-items:center; min-height:28px; padding:4px 9px; border:1px solid var(--hair); border-radius:999px; background:var(--paper); color:var(--ink-2); font:650 12px/1 var(--num); }
.task-list { display:grid; border:1px solid var(--hair); border-radius:12px; overflow:hidden; background:var(--hair); gap:1px; }
.task-row { display:grid; grid-template-columns:108px minmax(220px,1fr) minmax(180px,.8fr) minmax(190px,.9fr); gap:0; background:var(--surface); }
.task-row > div { padding:13px 14px; min-width:0; }
.task-row b { display:block; margin-bottom:4px; font-size:13px; color:var(--ink); }
.task-row p { margin:0; color:var(--ink-2); font-size:12px; line-height:1.45; }
.task-status { display:inline-flex; align-items:center; justify-content:center; min-width:76px; min-height:28px; padding:4px 8px; border-radius:999px; font:760 12px/1 var(--num); }
.s-done { background:var(--pos-bg); color:var(--pos); }
.s-doing { background:var(--warn-bg); color:var(--warn); }
.s-risk { background:var(--crit-bg); color:var(--crit); }
.s-plan { background:var(--info-bg); color:#2E5A7A; }
.detail-blocks { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; margin-top:14px; }
.detail-block { border:1px solid var(--hair); border-radius:12px; padding:14px; background:var(--paper); }
.detail-block h4 { margin:0 0 8px; font-size:13px; }
.detail-block ul { margin:0; padding-left:18px; color:var(--ink-2); font-size:12px; line-height:1.6; }
.detail-block li + li { margin-top:4px; }
.tab-subhead { margin:18px 0 10px; }
.tab-subhead h4 { margin:0; font-size:14px; line-height:1.35; }
.tab-subhead p { margin:4px 0 0; color:var(--ink-3); font-size:12px; }
code { font-family:var(--num); font-size:12px; background:var(--paper-2); padding:2px 6px; border-radius:6px; }
.wide { grid-column:1 / -1; }
.commands { display:grid; gap:10px; }
pre { margin:0; overflow:auto; padding:14px; border-radius:8px; background:#1E2724; color:#F6F0EC; font:12px/1.6 var(--num); }
table { width:100%; border-collapse:collapse; background:var(--surface); border:1px solid var(--hair); border-radius:12px; overflow:hidden; display:table; }
th,td { padding:12px 14px; border-bottom:1px solid var(--hair); text-align:left; vertical-align:top; font-size:13px; }
th { color:var(--ink-3); font-weight:700; background:var(--paper-2); }
tr:last-child td { border-bottom:0; }
@media (max-width:900px) {
header { padding:0 20px; }
main { width:min(100% - 28px, 1180px); padding-top:28px; }
.hero { grid-template-columns:1fr; }
.grid { grid-template-columns:1fr; }
.roadmap { grid-template-columns:1fr; }
.detail-head { grid-template-columns:1fr; }
.mini-metrics { justify-content:flex-start; }
.task-row { grid-template-columns:1fr; }
.task-row > div { padding:12px 14px; border-bottom:1px solid var(--hair); }
.task-row > div:last-child { border-bottom:0; }
.detail-blocks { grid-template-columns:1fr; }
}
@media (max-width:520px) {
.summary { grid-template-columns:1fr; }
h1 { font-size:24px; }
.stamp { display:none; }
}
/* ── 폴드아웃(섹션 접기) + 목차 드릴다운 — 내용 보존, 레이아웃만 정리 ── */
.dash-toc { position:sticky; top:58px; z-index:1; display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin:0 0 22px; padding:11px 13px; background:rgba(255,255,255,.93); border:1px solid var(--hair); border-radius:12px; backdrop-filter:blur(8px); box-shadow:0 1px 2px rgba(28,42,42,.05); }
.dash-toc .toc-label { font:700 11px/1 var(--num); letter-spacing:.07em; text-transform:uppercase; color:var(--ink-3); margin-right:2px; }
.dash-toc a { display:inline-flex; align-items:center; gap:6px; max-width:300px; padding:5px 11px; border:1px solid var(--hair); border-radius:999px; background:var(--paper); color:var(--ink-2); font-size:12px; font-weight:650; text-decoration:none; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.dash-toc a:hover { background:var(--accent-tint); color:var(--accent-deep); border-color:color-mix(in srgb,var(--accent) 32%,var(--hair)); }
.dash-toc .toc-spacer { flex:1 1 12px; }
.dash-toc button { appearance:none; border:1px solid var(--hair); background:var(--paper); color:var(--ink-2); border-radius:999px; padding:5px 11px; font:650 12px/1 var(--font); cursor:pointer; white-space:nowrap; }
.dash-toc button:hover { background:var(--accent-tint); color:var(--accent-deep); border-color:color-mix(in srgb,var(--accent) 32%,var(--hair)); }
.fold-head { cursor:pointer; user-select:none; position:relative; padding-left:26px; border-radius:8px; }
.fold-head:hover h2 { color:var(--accent-deep); }
.fold-head:before { content:""; position:absolute; left:4px; top:9px; width:8px; height:8px; border-right:2px solid var(--ink-3); border-bottom:2px solid var(--ink-3); transform:rotate(45deg); transition:transform .18s ease; }
section.folded .fold-head:before { transform:rotate(-45deg); }
.fold-head:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }
section.folded .fold-head { margin-bottom:0; }
section.folded .fold-body { display:none; }
@media (max-width:520px) { .dash-toc a { max-width:160px; } }
</style>
</head>
<body>
<header>
<div class="brand"><span class="mark" aria-hidden="true"></span><span>Vignette 개발 대시보드</span></div>
<div class="stamp">2026-06-26 22:10 KST</div>
</header>
<main>
<div class="hero">
<div>
<div class="kicker">team lead review</div>
<h1>전체 로드맵 기준 현재 위치는 <em>Phase 0 종료 직전</em>이다.</h1>
<p class="lead">
이번 세션은 seed persona 결합 제거, 비개발 fail-closed 설정, compose OAuth 주입,
admin 모바일 overflow 수정, persona 인증 경계 보강, 전체 Playwright 재통과, 공개 API prod 전환까지 완료했다.
이번 이어받기에서는 내담자 AI 생성 턴의 provider/model/token/cost 텔레메트리를 `/turn`, SSE stream, 음성 생성 경로에서 DB `app.turns`까지 적재하도록 연결했고,
gateway stream `done/error` 계약을 구조화했으며, P1 상담 시작→텍스트 stream→종료→리뷰 피드백 UI E2E와 저항 엔진 정량 회귀 테스트를 추가했다.
이어서 PII 마스킹 게이트 우회 차단, RBAC/IDOR·감사 insert 경로, 음성 paralinguistic 메타 저장, 공개 런타임 watchdog 스크립트와 runbook을 보강했다.
추가로 상주 engine session reuse, SAML provider 스캐폴드, `visible_to` 런타임 필터, EOT/TTS payload 계약까지 회귀화했다.
백엔드 회귀 테스트는 84개로 재검증했고, 페르소나 승인/반려 API와 교수 콘솔 검수 UI proof를 추가했다.
Postgres RLS/audit live smoke checker와 s2s 2차 PoC 판단 메모도 준비했지만, live DB 실행과 s2s 채택 결정은 별도 증거가 필요하다.
이후 로컬 5175 런타임 재기동, admin 사용자 관리 E2E 안정화, 학습자 홈 0회 기록 UX,
active session 실제 SSE 스트림 전환, 텍스트/음성 실패 턴 미저장, 직접 URL persona 우회 차단, Live2D demo 자산 제거,
AI 운영 fail-closed 보강까지 전체 Playwright로 확인했다.
2026-06-26 15:18 KST 기준 공개 API 터널을 복구한 뒤 전체 Playwright 72개가 desktop/mobile/single-run 모두 통과했다.
Cloudflare Pages production 배포 `0fe8f3cc`도 완료했고, 커스텀 도메인이 새 asset을 서빙하는 것을 확인했다.
과거 Mao/Haru Live2D demo 자산은 repo와 번들에서 제거했고, `/live2d/*` 운영 경로는 404로 차단했다.
로컬 프로세스는 prod API 8001, engine gateway 9099, cloudflared tunnel 1개만 남기고 dev API와 임시 Vite preview를 내렸다.
이번 라운드에서는 레이아웃 작업을 서브에이전트 전담으로 분리해 세션, 학습자 홈/리뷰, 관리자/교수자, 설정 화면을 overflow 수리 수준이 아니라 실제 작업 화면 재설계 수준으로 갱신했다.
통합 레이아웃 E2E는 desktop/mobile 54개를 최종 통과했고, 자세한 인수인계는 `docs/ops/layout-redesign-handoff-2026-06-26.md`에 남겼다.
이번 이어받기에서는 핸드오프가 남긴 "실기기 시각 수용"을 엄격한 자동 게이트로 닫았다. 7개 권장 너비(390/720/861/900/1024/1280/1440)에서 7개 재설계 화면의 가로 overflow 0·컨트롤 클립 0을 강제하는 `layout-visual-gate.spec.ts`(7 passed, 49장 스크린샷)를 만들고, 화면별 1에이전트 병렬 시각 리뷰 → 6개 병렬 수정 에이전트 → 화면별 적대적 재검수까지 돌려 7/7 accept(critical/major 0, 회귀 0)를 확정했다. 학습자홈 1280 3열 가운데 컬럼 붕괴(critical)와 교수 'API 404' raw 배너를 포함한 결함을 모두 해소했고, 레이아웃 항목을 DONE으로 닫았다.
다만 이 HTML은 지금까지 Phase 0 상태판처럼 쓰였고, 아래에 Phase 1 이후 전체 계획을 보강했다.
공개 Google OAuth 실제 `/turn` 증명만 storageState 캡처 후 남아 있다.
</p>
</div>
<div class="summary" aria-label="현재 검증 요약">
<div class="metric"><span>Full Playwright</span><b>72 passed</b></div>
<div class="metric"><span>Pages Production</span><b>0fe8f3cc</b></div>
<div class="metric"><span>Public API</span><b>prod OK</b></div>
<div class="metric"><span>Backend unittest</span><b>84 OK</b></div>
<div class="metric"><span>Layout visual gate</span><b>7/7 accept</b></div>
<div class="metric"><span>원천문서 갭</span><b>critical 3</b></div>
<div class="metric"><span>Decision</span><b>P0 조건부</b></div>
</div>
</div>
<section>
<div class="section-head">
<div>
<h2>한신대 기술분석 PDF 정합성 + 이번 세션 발견 (2026-06-27)</h2>
<p>원천 기술분석 PDF(트웬티온스×한신대 산학협력, 19쪽)를 전수 정독해 원안과 현 구현을 대조. 결론: <b>코어는 잘 정렬</b>됐으나(저항·반응 조절=PDF "우리만의 핵심 기술", 상태머신 4단계·RAG·이론모드·가드레일 분리·SSE·React19·평가 JSON스키마 강제), <b>"통과·납품 필수조건"인 위기 안전게이트</b><b>핵심 가치인 교수자의 학습자-이력/성장 추적 UI</b>가 비어 있다. 사용자 라이브 테스트로 페르소나 반응 갭(모욕·조롱을 공손히 흡수)을 발견·1차 수정.</p>
</div>
</div>
<div class="task-list">
<div class="task-row">
<div><span class="task-status s-done">정합 ✓</span></div>
<div><b>원안과 잘 맞는 코어(유지)</b><p>PDF가 차별 기술로 꼽은 <b>'저항·반응 조절'</b>(AI가 너무 착하지 않게, 실제 환자처럼)·상태머신(라포→탐색→개입→정리)·RAG(자료검색)·이론모드(인간중심/CBT)·답변과 분리된 가드레일·SSE 스트리밍·React19·<b>피드백 JSON Schema 강제</b>(벤더중립)가 이미 구현·정렬됨. Stanford RCT(피드백 없는 연습=역효과)도 H2 턴평가로 충족 방향.</p></div>
<div><b>권고</b><p>이 코어를 '교육용(비의료)' 포지션으로 유지. 평가는 교수자 검토용 보조지표로 표기.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">신규 DONE</span></div>
<div><b>역린(逆鱗)·트리거 + 수련생 misconduct 반응(2026-06-27)</b><p>라이브 테스트에서 수련생이 내담자를 "수동적 패배자"라 비난·조롱("웃음이 나오십니까")해도 민재가 공손히 흡수 → 훈련 가치 훼손. 수정: ① L0에 무례·모욕·조롱·인신공격 시 <b>현실적 동맹 균열</b>(위축·방어·따지기·종결의향) 지침 ② <b>PersonaCard.triggers(역린)</b> 필드 + CCD 핵심상처 파생 역린 블록(상담자가 핵심상처를 조롱·확정·강요·평가절하 시 가장 강한 반응). PDF '저항·반응 조절' 핵심 차별 강화. 무회귀 77/77.</p></div>
<div><b>잔여</b><p>P1~P3 명시 triggers는 임상팀 정밀 저작(현재 CCD 파생 기본값으로 작동). 라이브 재현 검증.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">CRITICAL</span></div>
<div><b>위기 안전게이트 — PDF "납품 필수조건·법적 생존선"인데 미구현</b><p>PDF(9·16·18쪽): 자해·자살 감지 시 <b>대화 중단 + 자살예방상담 109 안내 + 지도교수 자동 알림</b>, 안전은 <b>LLM 밖 별도 CrisisGate</b>. 현 코드: 위기 분류·safety_events 적재만, <b>핫라인 번호가 코드 어디에도 없음</b>(109는 2024.1 1393·1577 통합 — 최신 번호). 대화중단·109안내·교수자 알림 UI 전무.</p></div>
<div><b>권고</b><p>CrisisGate: LEARNER_REAL 위기 시 턴 중단 + 109/지역 자원 카드 + 교수자 알림(safety_events→대시보드/메일). 답변 생성과 분리.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">CRITICAL UX</span></div>
<div><b>교수자의 '학습자 이력·성장 추적' UI/UX — 핵심 가치인데 못 담음</b><p>PDF 핵심 루프: 학습자가 반복 연습 → <b>교수자가 학습자별 이력·성장곡선(이론부합도·자신감·숙련도, '연습 전·후' 비교)을 검수</b>. 현 교수자 콘솔은 회기 목록 수준 — 학습자별 성장·평가 이력·항목별(공감·질문·정상화·이론부합) 추이를 못 보여줌. 학습 데이터는 쌓이나 <b>가시화·검수 동선 부재</b>.</p></div>
<div><b>권고</b><p>교수자: 학습자별 대시보드(회기 타임라인·4항목 추이·전후 비교·역린/위기 플래그). 학습자: 성장곡선 + 회기 리뷰 누적.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">중기</span></div>
<div><b>2-tier 모델 전략 + 백엔드 언어(원안 차이)</b><p>① PDF: 고성능(Claude·GPT=내담자발화·실시간평가)/저비용 한국어(Solar=단순응답·사후요약) 2단계 + 캐싱 90%절감. 현재 claude_cli 단일 — 벤더중립 인터페이스는 있음. ② PDF는 <b>Node.js 우선</b>(팀 보유기술·5개월 일정)/Spring 대안. 현재 <b>Python/FastAPI</b> — 재작성은 일정·인력 리스크.</p></div>
<div><b>권고</b><p>2-tier는 ENGINE_MODE 라우팅으로 점진 도입. 백엔드 언어는 <b>소유자 결정</b>(팀 역량·유지보수 vs 재작성 비용).</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">검증 ✓</span></div>
<div><b>이번 세션 무회귀 검증 종합</b><p>페르소나 격리(캐릭터 붕괴 버그)·RAG 비차단(세션생성 블로킹 회귀)·역린/misconduct·init_state 파라미터객체·데드코드(LogHook·tier·RMS) — <b>백엔드 77~84/84 + E2E 42(데스크27·모바일11·아바타4) + 시각게이트 7/7</b> 통과. 스택 라이브(web·api·gateway·voice·RAG).</p></div>
<div><b>다음</b><p>위기 안전게이트(109) → 교수자/학습자 성장 UI 순으로 착수.</p></div>
</div>
</div>
</section>
<section>
<div class="section-head">
<div>
<h2>원천문서 갭 분석 (2026-06-26) — 신규 우선순위 입력</h2>
<p>한신대 산학협력 원천문서 5종(상담 프로토콜·인간중심 사례·회의록·신청서·사례개념화 워크북)을 시각 정독하고 코드베이스를 교차 조사해 도출한 "우리가 부족한 부분". 상세 근거: `docs/ops/source-docs-gap-analysis-2026-06-26.md`. critical/high 핵심 5건은 코드 grep으로 직접 재확인(✓). 콘텐츠(루브릭·이론·워크시트 문안)는 doc3 R&R상 임상팀(구훈정·어유경) 소유 — 코드는 '편집 가능 구조'만 선제 구축.</p>
</div>
<div class="mini-metrics"><span>critical 3</span><span>high 4</span><span>medium+ 6</span></div>
</div>
<div class="task-list">
<div class="task-row">
<div><span class="task-status s-risk">CRIT C1</span></div>
<div><b>사례개념화·치료계획 산출물 구조 전면 부재</b><p>doc1/4/5가 1급 산출물로 요구하는 워크시트(11탐색항목·호소 5영역·인지삼제·1·2차감정·보호/방해 4사분면·생물심리사회 목표)가 코드·스키마에 전무(grep 0건 ✓직접확인). CCD는 숨은 정답키일 뿐 학습자 산출물 폼이 아님.</p></div>
<div><b>권고</b><p>워크시트 스키마 입력 폼 + AI 축어록 초안 추출 + 규칙 기반 채점. 루브릭은 임상팀 외부 정의 가능하게 외부화.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">C2 구조 DONE</span></div>
<div><b>위기 에스컬레이션·safety_events 배선(2026-06-26, 라이브 검증)</b><p>prepare_turn→state_machine.evolve에 crisis.risk_level을 ideation_observed로 전달(위기 관측 상태머신 반영). escalate=True 시 `app.safety_events` 적재(graceful 비차단). <b>라이브</b>: "저 지금 죽고싶어요"→learner_real, safety_events 행 적재 확인. 84/84.</p></div>
<div><b>잔여(임상팀)</b><p>비밀보장 예외고지·단계적 탐색·생명유지서약 스크립트 콘텐츠 + 실시간 교수자 push(레코드가 1차 알림원) + 위기탐색 누락 회기리뷰 감점.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">C3 구조 DONE</span></div>
<div><b>이론모드 스레딩(2026-06-26)</b><p>TurnContext+prepare_turn에 theory_mode, sessions/voice가 sess.theory_mode 전달, evaluator._theory_mode가 세션 이론(학습자 선택) 우선. 프론트 Session.tsx 'humanistic' 하드코딩 제거→persona.theory_target 기준(CBT 페르소나는 cbt). 84/84+typecheck.</p></div>
<div><b>잔여(임상팀)</b><p>CBT 이론별 단계 프롬프트 체인 콘텐츠 + 이론부합 채점 루브릭 + 프론트 이론 선택 UI(현재 persona 설계 이론 자동).</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">HIGH H1</span></div>
<div><b>계약 평가 KPI(자기효능감·기술숙련도·수련만족도 사전사후) 수집·집계 전무</b><p>doc4가 20명 실험/통제군·단회기 50분·3척도 pre-post를 못박았으나 코드 0건. Phase3 KPI도 report shape만, 계산 코드 0줄. (κ/ICC·환각률은 doc4 미명시 — 평가설계 문서 확정 필요.)</p></div>
<div><b>권고</b><p>3척도 pre-post 폼·실험/통제군 배정·자동누적 대시보드·추이 시각화·검정 계산 코드. 평가설계는 소유자 결정.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">H2 부분 DONE</span></div>
<div><b>턴별 회기 리뷰(상호작용 분석) — 텍스트 경로 가동(2026-06-26)</b><p>텍스트 `/turn` 경로에 `eval_hook=make_eval_hook(engine_client)` 주입, fast-loop 턴 평가를 학습자 TurnRecord(`evaluation` 신규 필드)에 영속화, 리뷰가 per-turn 기법칩(category→empathy/explore/reflect/confront/closed)·적절성/의도이탈 노트를 노출하도록 배선. <b>라이브 검증</b>: "많이 힘드셨겠어요…" → techniques [공감/empathy, 촉진질문/explore] + note '적절한 개입'. 백엔드 84/84 pass.</p></div>
<div><b>잔여</b><p>음성 경로(`voice.py:307`) eval_hook 미주입(코드품질 연구 확인) · few-shot 골든셋 기본 OFF · 회기리뷰 2열 UI(축어록 타임라인+비언어) 프론트 강화 · 원천 축어록 골든셋 적재(임상팀).</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">HIGH H3</span></div>
<div><b>임상팀 콘텐츠 입력 경로(페르소나 저작 CRUD) 부재 + P4~P7 미적재</b><p>personas 라우트에 검수 승인/반려만, draft 생성·편집 API 없음. persona_repository는 in-code SEED(P1~P3)만 materialize, 외부 JSON 미로드(✓직접확인). doc3 R&R상 콘텐츠는 임상팀 산출물인데 작성 경로가 막힘.</p></div>
<div><b>권고</b><p>페르소나 저작 CRUD(draft→review) + P4~P7 적재, 루브릭·이론 콘텐츠를 임상팀 편집 가능 데이터로 외부화.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">H4 부분 DONE</span></div>
<div><b>한국어 PII 마스킹 보강(2026-06-26)</b><p>정규식 폴백에 한국어 고위험 패턴 추가: 구체 날짜(2001.4.18→[DATE])·금액(1200원→[MONEY])·행정구역 주소(서울시 강남구 역삼동→[ADDR]). 과다마스킹 없음 검증("그냥 힘들어요" 보존), 마스킹·저항 6/6+전체 84/84.</p></div>
<div><b>잔여</b><p>이름/기관명은 NER 필요(Presidio ko 모델·spaCy ko, 무거움 — TODO 명시) · audit.llm_call_log 런타임 적재 · 미성년/guardian 동의 수집·게이트·철회.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">MED M1~M3</span></div>
<div><b>비언어 임상이벤트(M1) · 다회기 케이스 아크(M2) · SSO claim/식별자/감사(M3)</b><p>M1: voice.py가 EOT용 silence_ms만, 침묵·한숨·울음 타임스탬프 이벤트 캡처·리뷰 표시 전무(서버 RMS 힌트 프론트 dead). M2: case_state·build_recall_context가 빈 컨텍스트 반환(다회기 미구동). M3: cohort_ids=[] 하드코딩·email 파생 식별자·SAML 서명검증 미구현·role변경/삭제 audit 미기록. (분석.)</p></div>
<div><b>권고</b><p>M1 비언어 메타 이벤트 보존·시각화, M2 case_state 런타임·회상 주입, M3 claim→role/cohort 매핑+안정 식별자+서명검증+deprovisioning audit(기관 의존).</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TRACK X1·X2·L1</span></div>
<div><b>재귀학습 export(X1) · API 비용 관측(X2) · 스택 정합성·일정(L1)</b><p>X1: ds.* 스키마만, read/write·JSONL export·IAA 게이트 미코딩. X2: 게이트웨이 토큰 텔레메트리 0 고정·cost 모니터링 부재(doc3가 비용을 운영 리스크로 명시). L1: doc4 신청서 스택(Spring Boot 3/Node.js·TimescaleDB) vs 실제 FastAPI/Python 불일치, 20주 단기일정·9월 저작권 등재 압박.</p></div>
<div><b>권고</b><p>X1 export 잡·IAA 게이트(κ≥0.6/ICC≥0.75), X2 토큰 텔레메트리·캐싱·비용 대시보드, L1 스택 정합 또는 변경사유 거버넌스 기록.</p></div>
</div>
</div>
<div class="detail-blocks">
<div class="detail-block">
<h4>즉시 착수 가능(내부 코드, 외부 합의 불요)</h4>
<ul>
<li>C2 배선: escalate 시 safety_events insert + ideation_observed 전달.</li>
<li>C3 골격: theory 인자 + Session.tsx 하드코딩 제거 + 프론트 선택 UI.</li>
<li>H2 배선: eval_hook을 turn 파이프라인 주입(이미 정의·export).</li>
<li>H3: 페르소나 저작 CRUD + P4~P7 로드.</li>
<li>H4(부분)·M1(부분)·M2·X1·X2: 마스킹·메타 이벤트·회상·export·텔레메트리.</li>
</ul>
</div>
<div class="detail-block">
<h4>소유자 결정 / 외부(임상팀·기관) 의존</h4>
<ul>
<li>임상팀 산출물: C1 워크시트 항목·루브릭, C3 CBT 콘텐츠·체인, C2 위기 스크립트·서약 문안, H2 골든셋 코딩.</li>
<li>소유자 평가설계: H1 실험/통제군·3척도 문항·50분 흐름, κ/ICC·환각률 목표(doc4 미명시).</li>
<li>기관(한신 IT): M3 SSO IdP·test tenant·claim 스키마·인증서, 거버넌스 증거.</li>
<li>거버넌스: L1 스택 정합성 방향·저작권 등재 문서화·IP 협의.</li>
</ul>
</div>
<div class="detail-block">
<h4>판독 한계 (정직성)</h4>
<ul>
<li>핵심 축어록 2단 표는 PDF 시각 판독이 1차 근거(txt 일부 누락), OCR 잔재 존재.</li>
<li>doc4 κ/ICC·환각률은 신청서 본문 미명시 — 계약 확정 지표로 단정 금지.</li>
<li>'현재상태'는 grep/코드 사실 기반. C1~C3·H2·H3은 직접 재확인(✓), 그 외는 착수 전 코드 1차 재확인 권고.</li>
</ul>
</div>
</div>
</section>
<section>
<div class="section-head">
<div>
<h2>코드품질 연구 + 로컬 개발 스택 (2026-06-26)</h2>
<p>리팩토링 연구(데드코드·과다파라미터·중복·인터페이스SSOT·추상화) 상세: `docs/ops/code-quality-research-2026-06-26.md`. 로컬 실행 가이드 `docs/guides/local-development.md`.</p>
</div>
<div class="mini-metrics"><span>리팩터 Top3</span><span>dev 스택 1-커맨드</span></div>
</div>
<div class="task-list">
<div class="task-row">
<div><span class="task-status s-done">DEV STACK</span></div>
<div><b>로컬 개발 스택 1-커맨드 기동 — `scripts/dev-up.ps1` / `dev-down.ps1`</b><p>게이트웨이(9099 claude_cli)+API(8000, DB 미가용 시 in-memory degraded)+웹(5173)을 커맨드라인 기준 정리 후 결정론적 기동(`--reload` 워처 불안정 회피, uvicorn 있는 python 자동 해석). dev-login + seed 페르소나. 로그 `.devlogs/`(gitignore). <b>검증</b>: 3포트 health OK + 실제 AI 턴 생성·회기리뷰 동작.</p></div>
<div><b>비고</b><p>AI 턴은 로컬 claude CLI 사용(사용량 발생). 진입점 http://localhost:5173. <b>주의(무DB 한정)</b>: DB 없이 degraded면 페르소나가 `source:seed_fallback·degraded:true`라 프론트 `isUsablePersona`(`!degraded && source==="database"`)가 막아 "회기 시작" 버튼이 비활성 → UI 세션 시작엔 실제 Postgres(`127.0.0.1:55432`)가 필요. (백엔드 API는 seed로도 세션 생성 가능.)</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">VOICE 작동</span></div>
<div><b>음성 채팅 "바로 꺼짐" 해결 — provider 키 부재였음(코드 아님)</b><p>직접 WS 재현으로 근본원인 규명: 키 없으면 백엔드가 `degraded`+1011 즉시 close → 마이크 깜빡·녹음 미도달. STT(gpt-4o-transcribe)·TTS(gpt-4o-mini-tts) 모두 OpenAI. <b>실 OPENAI_API_KEY를 apps/api/.env(gitignore)에 설정 → /voice/health available:true, WS가 ready→listening 정상 진입</b>(degraded-close 사라짐). 백로그 B2 음성 캐스케이드가 로컬에서 실작동.</p></div>
<div><b>부수 수정(2026-06-26 라이브)</b><p>① 미설정 시 마이크 버튼 사전 비활성+명확 안내(`/voice/health` 게이팅) ② <b>TTS 지문 제거</b>: (고개 끄덕)·(한숨)·[침묵] 등 무대지시를 음성으로 안 읽음(`speakable_text`, 자막엔 유지) ③ <b>아바타 애니메이션</b>: 텍스트 턴 speaking 시 입 정지(0) 버그 수정(의사 발화 모션) + thinking 표현 보강(시선/고개/눈썹). typecheck+voice테스트 PASS.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">페르소나 격리</span></div>
<div><b>AI 내담자 캐릭터 붕괴·개발맥락 누출 버그 수정(2026-06-27, 라이브)</b><p>증상: 라이브 회기에서 내담자(민재)가 "저는 Claude Code인데… Vignette 코드베이스… git status…"로 캐릭터를 깨고 개발 맥락 노출. 원인: ENGINE_MODE=claude_cli가 로컬 claude(=Claude Code)를 호출하는데 게이트웨이가 페르소나를 `--append-system-prompt`로 코딩 어시스턴트 정체성에 <b>덧붙이고</b>, cwd=repo라 CLAUDE.md·git status·env가 시스템프롬프트에 포함됨.</p></div>
<div><b>수정·검증</b><p>`engine_gateway/gateway.py`: `--append-system-prompt`→`--system-prompt`(페르소나만 <b>교체</b> 주입) + `--exclude-dynamic-system-prompt-sections`(cwd/env/git/메모리 섹션 제거). 라이브: 깨던 입력 그대로 → 민재 완벽 인캐릭터, 누출 0. 게이트웨이 7/7 + 전체 84/84.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">REFACTOR 1</span></div>
<div><b>턴 처리 파이프라인 중복(REST/WS) 제거</b><p>`routes/sessions.py`(submit/stream)·`routes/voice.py`가 세션로드→오너십→prepare_turn→run_turn_generate→TurnRecord append→state update를 복제. 실드리프트 존재: stage 표기 채널별 상이(한글 vs enum), 음성 경로 eval_hook 미주입·빈 RecallContext. 공통 `record_completed_turn`/`SessionRepository`로 추출.</p></div>
<div><b>안전</b><p>순수 리팩터 단독 PR + 골든 테스트(test_session_turn_persistence·test_voice_ws) 동등성 확인.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">REFACTOR 2</span></div>
<div><b>백엔드 Pydantic ↔ 프론트 `lib/api.ts` 계약 SSOT화</b><p>~14개 응답 모델이 TS에 수기 미러(드리프트 실증: `crisis_kind`·`end_state` 누락). OpenAPI→openapi-typescript 단방향 생성으로 전환(타입만, 저위험). [[interface-ssot]]</p></div>
<div><b>안전</b><p>CI에 "재생성 후 git diff 없음" 게이트.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">REFACTOR 3 일부</span></div>
<div><b>죽은 확장점/계약 정리 — LogHook·tier 제거 적용(2026-06-26, 84/84)</b><p>`LogHook`/`log_hook`(caller 0) 타입·파라미터·분기·export 제거 · 게이트웨이/엔진클라 `tier` dead-contract 제거(write-only, gateway는 model만 사용). 잔여(RMS 힌트·P4~P7·Live2D 고아 자산·파라미터객체 init_state/prepare_turn/upsert)는 `docs/ops/refactor-rag-patches-2026-06-26.{md,json}`에 스테이징(단계 적용).</p></div>
<div><b>참고</b><p>일괄 적용 대신 라이브 스택 보호 위해 그룹별 적용+pytest 검증.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">RAG 활성화</span></div>
<div><b>"좋은 RAG" 마스터플랜대로 구현돼 있었으나 미배선 → 라이브 활성화(2026-06-26)</b><p>임베더 BGE-M3 검증+버전핀(1.4.0 dtype버그→1.2.11/transformers 4.44.2). `embed_query`를 asyncio.to_thread로 offload(이벤트루프 비차단). 턴 파이프라인 배선: `retrieve_persona_memory`→episodic recall, `search_kb`→kb_behavior_cues(CLIENT 정책, 전구간 graceful degradation). KB seed 3청크 적재. <b>검증</b>: search_kb가 behavior_cue 의미매칭 반환, 라이브 세션 13s(임베더로드)+턴 정상. 색인/회수 첫 실행으로 드러난 rag.py latent 버그 3건(sparse·meta jsonb) 수정. GraphRAG는 마스터플랜상 2차 보류(맞음).</p></div>
<div><b>성능 회귀 수정(2026-06-27, E2E로 포착)</b><p>최초 RAG 배선이 회기 시작/턴에 임베더 로드(~7-13s)를 <b>동기 await</b>해 `POST /sessions`가 막힘 → session-layout E2E 4건이 5s 타임아웃 실패("여는 중…" 멈춤). 수정: 회상·KB 회수를 `_warm_rag_caches` <b>백그라운드 task</b>로 옮기고 턴은 캐시만 읽음(비차단, warm 전엔 빈 단서 graceful). 재검증: session-layout 4/4 + <b>핵심 E2E 27/27(데스크톱)·11/11(모바일)·아바타 4/4</b> 통과. KB 스타터 3청크(임상 정밀은 임상팀).</p></div>
<div><b>시각 게이트 7/7 정제(2026-06-27)</b><p>admin 1280 input "text-clipped-x" 1건은 폼 input의 <b>값 스크롤(설계상 정상·커서로 전부 도달)</b>을 오탐한 것 — 가로 overflow·clipped-by-ancestor는 원래 통과(레이아웃 contained 확인). ellipsis/line-clamp 제외와 동일 원리로 form 컨트롤(input/textarea/select)을 text-clip 판정에서 제외 → <b>게이트 7/7</b>(실 결함 검사는 유지, 미은폐).</p></div>
</div>
</div>
</section>
<section>
<div class="section-head">
<div>
<h2>전체 개발 로드맵</h2>
<p>`docs/MASTERPLAN.md`와 `docs/redteam/MASTERPLAN_REVISIONS.md`를 합친 팀장 판단용 단계표</p>
</div>
</div>
<div class="roadmap">
<article class="phase-card current">
<div class="phase-top"><span class="phase-id">Phase 0</span><span class="tag doing">closing</span></div>
<h3>기반 정렬 / 운영 안전장치</h3>
<p>인프라, 인증 경계, fail-closed, 공개 배포, 회귀 테스트 안정화.</p>
<ul class="phase-list">
<li><b>상태:</b> 대부분 완료, 공개 `/turn` proof만 남음</li>
<li><b>검증:</b> build + backend 84 + Playwright 72</li>
<li><b>운영:</b> prod API 8001, engine 9099, tunnel 1개</li>
</ul>
</article>
<article class="phase-card next">
<div class="phase-top"><span class="phase-id">Phase 1</span><span class="tag doing">next</span></div>
<h3>MVP 코어 루프</h3>
<p>1 페르소나 상담, 실제 턴 저장, 회기말 피드백, 저항 엔진 검증.</p>
<ul class="phase-list">
<li><b>DoD:</b> 로그인 → P1 상담 완주 → 피드백 수신</li>
<li><b>추가:</b> 음성 왕복 1턴 PoC 포함</li>
<li><b>증거:</b> cost/turn DB 적재 + openness 곡선 대조</li>
</ul>
</article>
<article class="phase-card">
<div class="phase-top"><span class="phase-id">Phase 2a</span><span class="tag info">planned</span></div>
<h3>3역할 / OAuth / 페르소나</h3>
<p>학습자, 교수자, 관리자 권한 분리와 페르소나 확장.</p>
<ul class="phase-list">
<li><b>DoD:</b> 타인 session 직접 접근 403</li>
<li><b>범위:</b> OAuth BFF, RBAC, 교수 검수 루프</li>
<li><b>주의:</b> visible_to와 인간 권한은 별도 레이어</li>
</ul>
</article>
<article class="phase-card">
<div class="phase-top"><span class="phase-id">Phase 2b</span><span class="tag info">planned</span></div>
<h3>음성 캐스케이드</h3>
<p>STT → 엔진 → TTS 왕복, WSS/터널 안정성, barge-in 로깅.</p>
<ul class="phase-list">
<li><b>DoD:</b> 50분 WSS 끊김 실측</li>
<li><b>범위:</b> Deepgram STT, OpenAI TTS, voice preset</li>
<li><b>리스크:</b> 실시간 레이턴시는 실측 전 완료 표기 금지</li>
</ul>
</article>
<article class="phase-card">
<div class="phase-top"><span class="phase-id">Phase 3</span><span class="tag info">planned</span></div>
<h3>교육용 파일럿 / 효과성</h3>
<p>수련생 파일럿, 사전사후 효과 측정, 재귀학습 데이터셋 산출.</p>
<ul class="phase-list">
<li><b>DoD:</b> 20명 완주 + KPI 충족</li>
<li><b>측정:</b> SUS, 자기효능감, κ/ICC, 환각률</li>
<li><b>정정:</b> IRB 게이트가 아니라 데이터 활용동의/개인정보 게이트</li>
</ul>
</article>
</div>
<div class="source-note">
정정 기준: `MASTERPLAN` 원본의 IRB 신청/승인 게이트는 `MASTERPLAN_REVISIONS` 기준으로 제거한다. Phase 2는 2a(3역할/OAuth/페르소나)와 2b(음성 캐스케이드)로 분리하고, Phase 1 DoD에는 얇은 음성 왕복 1턴 PoC를 포함한다.
</div>
</section>
<section aria-labelledby="detail-tabs-title">
<div class="section-head">
<div>
<h2 id="detail-tabs-title">상세 페이지</h2>
<p>탭별 할 일, 산출물, 검증 기준. 팀장 리뷰는 여기서 다음 작업을 고르면 된다.</p>
</div>
</div>
<div class="tabs">
<div class="tabbar" role="tablist" aria-label="개발 상세 탭">
<button class="tab-button" id="tab-p0" role="tab" aria-selected="true" aria-controls="panel-p0" data-tab="p0">P0 마감</button>
<button class="tab-button" id="tab-p1" role="tab" aria-selected="false" aria-controls="panel-p1" data-tab="p1">P1 MVP</button>
<button class="tab-button" id="tab-p2a" role="tab" aria-selected="false" aria-controls="panel-p2a" data-tab="p2a">P2a 권한/페르소나</button>
<button class="tab-button" id="tab-p2b" role="tab" aria-selected="false" aria-controls="panel-p2b" data-tab="p2b">P2b 음성</button>
<button class="tab-button" id="tab-p3" role="tab" aria-selected="false" aria-controls="panel-p3" data-tab="p3">P3 파일럿</button>
<button class="tab-button" id="tab-ops" role="tab" aria-selected="false" aria-controls="panel-ops" data-tab="ops">운영/검증</button>
<button class="tab-button" id="tab-decisions" role="tab" aria-selected="false" aria-controls="panel-decisions" data-tab="decisions">결정 필요</button>
</div>
<div class="tab-panel" id="panel-p0" role="tabpanel" aria-labelledby="tab-p0">
<div class="detail-head">
<div>
<h3>Phase 0 마감 상세</h3>
<p>지금 단계는 구현을 더 벌리는 단계가 아니라, 운영 증거를 닫고 P1 진입 조건을 고정하는 단계다.</p>
</div>
<div class="mini-metrics"><span>상태: 조건부</span><span>우선순위: P0</span><span>블로커: public /turn</span></div>
</div>
<div class="task-list">
<div class="task-row">
<div><span class="task-status s-risk">BLOCK</span></div>
<div><b>공개 Google OAuth 실제 `/turn` proof 확보</b><p>로그인 가능한 계정으로 storageState를 저장한 뒤 public-auth-turn E2E를 실행한다.</p></div>
<div><b>산출물</b><p>`apps/web/node_modules/.tmp/public-auth.json`, public `/turn` 성공 로그</p></div>
<div><b>검증</b><p>`E2E_PUBLIC_AUTH=1` + `chromium-public-auth` 1회 통과</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>engine config 운영값 재확인</b><p>`GET /admin/engine-config`로 운영값을 직접 확인했다. `engine_mode=claude_cli`, `engine_url=http://127.0.0.1:9099`, `model=gateway-default`, `durable=true`, `source=database`로 의도값과 일치한다.</p></div>
<div><b>산출물</b><p>config API 응답(durable, db source)</p></div>
<div><b>검증</b><p>`claude_cli / http://127.0.0.1:9099 / gateway-default` 의도값 대조 완료</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>과거 Live2D demo 자산 제거</b><p>Mao/Haru 샘플, Pixi/Cubism 런타임, 공개 `/live2d/*` 캐시 잔여 접근을 차단했다.</p></div>
<div><b>산출물</b><p>`apps/web/functions/live2d/[[path]].js`, SVG-only avatar</p></div>
<div><b>검증</b><p>운영 `/live2d/mao/*`, Cubism core `404`</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>프로세스 난립 정리</b><p>임시 Vite preview와 dev API를 내리고 운영 확인용 프로세스만 유지한다.</p></div>
<div><b>산출물</b><p>9099 engine, 8001 prod API, cloudflared tunnel 1개</p></div>
<div><b>검증</b><p>`Get-NetTCPConnection`에서 8000/517x/5537 비어 있음</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">GATE</span></div>
<div><b>한신대 공문/데이터 거버넌스 게이트 정리</b><p>SSO 클레임, 추가 축어록 수급, 미성년 원본 활용동의, 개인정보 처리방침을 P1 진입 전 외부 의존성으로 명확히 둔다. 로컬 문서 골격은 준비됐지만 한신대/데이터 steward의 written evidence는 아직 필요하다.</p></div>
<div><b>산출물</b><p>`docs/ops/hanshin-data-governance-gate.md`, 공문 질의 항목, 동의 범위 체크리스트, SSO claim mapping 표</p></div>
<div><b>검증</b><p>문서 artifact 작성 완료; IRB 게이트가 아니라 데이터/SSO 게이트로 외부 증거 필요 상태 유지</p></div>
</div>
</div>
<div class="source-note">
<b>팀장 판정:</b> 로컬 회귀와 공개 prod-safe 게이트는 통과했다. Cloudflare Pages production `0fe8f3cc`이 `543d48c` 소스로 배포됐고, 운영 반영 전 남은 핵심 증거는 공개 Google OAuth 실제 `/turn` smoke다. 런타임 mock/demo 자산은 운영 번들에서 제거하고 테스트 fixture만 남긴다.
</div>
<div class="tab-subhead">
<h4>Phase 0 완료 상세 목록</h4>
<p>이전 “완료” 카드 내용을 탭 안에서 항목별로 보존한다.</p>
</div>
<div class="task-list">
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>DB persona 기반 테스트</b><p>`session-persistence`, `teacher`, `voice`는 `/api/personas`에서 실제 DB persona를 가져온다.</p></div>
<div><b>산출물</b><p>seed persona 직접 결합 제거, 실제 persona catalog 기반 E2E</p></div>
<div><b>검증</b><p>persona 관련 E2E가 DB-backed persona로 통과</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>비개발 런타임 보호</b><p>prod/staging에서 dev login, seed fallback, OAuth 누락, 기본 secret, localhost CORS를 거부한다.</p></div>
<div><b>산출물</b><p>fail-closed runtime config validation</p></div>
<div><b>검증</b><p>backend runtime policy unittest 통과</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>compose OAuth 주입</b><p>`ENVIRONMENT`, Google OAuth vars, `CORS_ORIGINS`, `OPENAI_BASE_URL`을 API 컨테이너에 명시했다.</p></div>
<div><b>산출물</b><p>compose 환경 주입 정리</p></div>
<div><b>검증</b><p>`docker compose --env-file .env.example config --quiet` 통과</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>admin 모바일 overflow 수정</b><p>긴 readiness detail을 안정화했고, 사용자 변경은 DB 저장소가 durable할 때만 허용한다.</p></div>
<div><b>산출물</b><p>admin responsive layout, durable store gate, AI health copy</p></div>
<div><b>검증</b><p>admin manage-users desktop/mobile 2 passed</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>global engine config 테스트 분리</b><p>`db-persistence`는 전역 engine config를 바꾸므로 `@single-run`으로 분리했다.</p></div>
<div><b>산출물</b><p>Playwright single-run tagging</p></div>
<div><b>검증</b><p>전체 Playwright 72 passed</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>handoff와 문서 갱신</b><p>`docs/HANDOFF.md`, `docs/dev_dashboard.html`, `apps/web/e2e/README.md`를 최신 상태로 맞췄다.</p></div>
<div><b>산출물</b><p>handoff, dashboard, E2E README</p></div>
<div><b>검증</b><p>최신 커밋 기준 상태/검증/운영 포트 일치</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>공개 API prod 전환</b><p>`cloudflared` ingress를 `127.0.0.1:8001` prod API로 돌렸고 public readiness gate가 통과했다.</p></div>
<div><b>산출물</b><p>prod API 8001, tunnel config</p></div>
<div><b>검증</b><p>`https://api-vignette.chanpaca.net/health` prod/db/engine OK</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>공개 런타임 재기동 스크립트</b><p>`scripts/start-public-runtime.ps1`와 `scripts/stop-public-runtime.ps1`로 API 8001과 cloudflared를 재현 가능하게 만들었다.</p></div>
<div><b>산출물</b><p>public runtime start/stop scripts</p></div>
<div><b>검증</b><p>engine 9099 + API 8001 + cloudflared only</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>persona catalog 인증화</b><p>`/personas`는 이제 비로그인 401이다. 로컬과 공개 API 모두 직접 확인했고 E2E 회귀를 추가했다.</p></div>
<div><b>산출물</b><p>authenticated persona catalog boundary</p></div>
<div><b>검증</b><p>local/public unauth `GET /personas` 401</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>public API base fallback 보강</b><p>public 웹에서 `/api/auth/config`가 SPA HTML로 떨어지지 않도록 public API origin fallback을 추가했다.</p></div>
<div><b>산출물</b><p>public API origin fallback</p></div>
<div><b>검증</b><p>public login screen E2E 1 passed</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>학습자 홈 0회 기록 상태</b><p>학습자 홈은 fake 기록 없이 실제 서버 세션 0개를 `기존 회기` 빈 상태와 0 카운트로 표시한다.</p></div>
<div><b>산출물</b><p>empty history UX, zero count, no fake stats</p></div>
<div><b>검증</b><p>learner screenshots: document overflow 0, empty history visible</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>로컬 5175 로그인 재기동 검증</b><p>`127.0.0.1:5175` web, `8000` API, `9099` engine을 재기동하고 dev-login 서버 세션과 `/learn` redirect를 확인했다.</p></div>
<div><b>산출물</b><p>local dev-login flow proof</p></div>
<div><b>검증</b><p>desktop/mobile auth flow passed</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>active session dense viewport 보정</b><p>375px급 active session에서 stage 내부가 1px 넘게 잘리던 문제를 avatar/padding 조정으로 제거했다.</p></div>
<div><b>산출물</b><p>session layout responsive adjustment</p></div>
<div><b>검증</b><p>session-layout desktop/mobile 8 passed</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>텍스트 턴 SSE 전환</b><p>`POST /sessions/{id}/stream` token 이벤트로만 내담자 자막을 쌓고, HTTP 거절과 SSE error 모두 실패 턴을 transcript와 저장소에 남기지 않는다.</p></div>
<div><b>산출물</b><p>real SSE stream turn path, failed turn discard</p></div>
<div><b>검증</b><p>session layout/turn E2E passed</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>음성 실패 턴 미저장</b><p>음성 회기도 엔진 응답 생성 성공 후에만 learner/client turn을 저장한다. 실패한 transcript는 pending 상태에서 제거한다.</p></div>
<div><b>산출물</b><p>voice failure persistence guard</p></div>
<div><b>검증</b><p>voice + voice-success + session-layout 15 passed</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>비영구 상태 문구 정리</b><p>admin health와 voice degraded reason은 내부 fallback/seed 표현 대신 비영구 런타임 상태를 사용자용 문구로 표시한다.</p></div>
<div><b>산출물</b><p>operator-facing copy cleanup</p></div>
<div><b>검증</b><p>settings/admin focused E2E 통과</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>직접 URL persona 차단</b><p>`/learn/session/:code`로 직접 들어와도 DB 원본 persona가 아니면 회기 시작을 막는다.</p></div>
<div><b>산출물</b><p>route guard, degraded/seed fallback regression</p></div>
<div><b>검증</b><p>unavailable persona code E2E 통과</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>demo Live2D 자산 제거</b><p>Mao/Haru 샘플과 Pixi/Cubism 런타임을 공개 배포물에서 제거했다. 세션 화면은 SVG persona avatar만 렌더링한다.</p></div>
<div><b>산출물</b><p>SVG-only avatar, legacy `/live2d/*` 404 function</p></div>
<div><b>검증</b><p>Mao/Haru/Cubism core routes 404, no Live2D/Pixi chunks</p></div>
</div>
</div>
</div>
<div class="tab-panel" id="panel-p1" role="tabpanel" aria-labelledby="tab-p1" hidden>
<div class="detail-head">
<div>
<h3>Phase 1 MVP 코어 루프 상세</h3>
<p>목표는 “P1 내담자와 실제 상담 1회 완주 후 피드백을 받는 공개 시연 가능 상태”다.</p>
</div>
<div class="mini-metrics"><span>우선순위: P0</span><span>범위: 텍스트+음성 1턴</span><span>완료보고: E2E 증거 후</span></div>
</div>
<div class="task-list">
<div class="task-row">
<div><span class="task-status s-doing">CHECK</span></div>
<div><b>LLMEngine 정규화 계약 고정</b><p>gateway SSE `token/done/error`와 generate cost 계약은 회귀화했다. claude_cli와 Messages API 폴백 동일성 검증은 남아 있다.</p></div>
<div><b>산출물</b><p>`token`, `done`, `error`, cost 이벤트 계약과 contract test</p></div>
<div><b>검증</b><p>stream error 미저장 + stream/generate client turn telemetry 저장</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>상주 `claude -p` 엔진풀 실증</b><p>`session_id`가 살아 있는 gateway session을 재사용하고 missing session은 ephemeral로 닫히는 구조를 회귀화했고, live 게이트웨이(9099)로 실측까지 마쳤다. `probe-engine-gateway.py`가 단일 session_id로 2회 reused stream을 돌려 TTFT 16674199ms, cost 누적 0.068→0.123(turns 1→2), engine=claude_p, model=opus-4-8을 측정했다. (프로세스 RSS는 별도 OS 계측 항목으로 남김.)</p></div>
<div><b>산출물</b><p>`engine_gateway/test_gateway_model.py`, `scripts/probe-engine-gateway.py`, live probe JSON</p></div>
<div><b>검증</b><p>session reuse 7 tests OK; live probe 2 reused streams, TTFT/cost 실측, session_id 재사용 확인</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-doing">CHECK</span></div>
<div><b>마스킹 게이트 강제</b><p>current turn, recall, pinned fact, recent turns가 Presidio/가명처리 후에만 engine request로 들어가게 막았다. 운영 외부-send 관측 증거는 남아 있다.</p></div>
<div><b>산출물</b><p>`app/test_orchestrator_masking.py`, context 입력 재마스킹</p></div>
<div><b>검증</b><p>phone/email/RRN raw 값이 generate/stream payload에 없음</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-doing">CHECK</span></div>
<div><b>P1 상담 완주 플로우</b><p>통제된 UI E2E로 P1 선택, SSE 상담, 종료, 회기말 리뷰 피드백을 한 번에 통과시켰다. 실제 운영 로그인 proof는 P0 blocker와 함께 남아 있다.</p></div>
<div><b>산출물</b><p>`e2e/session-mvp.spec.ts`, turn telemetry 저장 경로</p></div>
<div><b>검증</b><p>`chromium-single-run` P1 MVP UI E2E 1 passed</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-doing">CHECK</span></div>
<div><b>저항 엔진 정량 검증</b><p>공감 발화와 조언점프 발화 5턴 세트를 순수 상태머신 회귀 테스트로 비교했다. DB 쿼리 기반 운영 증거는 남아 있다.</p></div>
<div><b>산출물</b><p>`app/test_state_machine_resistance.py`</p></div>
<div><b>검증</b><p>공감 시 탐색 전이/개방도 상승, 조언점프 시 라포 단계 유지/개방도 0</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-doing">CHECK</span></div>
<div><b>음성 왕복 1턴 PoC</b><p>Session UI 마이크 버튼 경로에서 synthetic `getUserMedia`/`MediaRecorder`로 실제 `/voice/ws`를 열고 audio_start, binary chunk, audio_end, transcript, reply, TTS 재생 상태까지 검증했다. 물리 마이크, 공개 WSS, live STT/TTS provider 증거는 아직 별도다.</p></div>
<div><b>산출물</b><p>`apps/web/e2e/voice-success.spec.ts`, controlled OpenAI/engine providers, browser voice UI probe</p></div>
<div><b>검증</b><p>`npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run` 2 passed; 실패 턴 미저장은 backend persistence 회귀로 별도 확인</p></div>
</div>
</div>
</div>
<div class="tab-panel" id="panel-p2a" role="tabpanel" aria-labelledby="tab-p2a" hidden>
<div class="detail-head">
<div>
<h3>Phase 2a 권한, OAuth, 페르소나 상세</h3>
<p>음성과 섞지 않고 3역할, 권한격리, 페르소나 확장을 독립 트랙으로 닫는다.</p>
</div>
<div class="mini-metrics"><span>주차: 9-13</span><span>우선순위: P1</span><span>IDOR 검증 필수</span></div>
</div>
<div class="task-list">
<div class="task-row">
<div><span class="task-status s-doing">CHECK</span></div>
<div><b>OAuth BFF / HttpOnly 세션 완성</b><p>Auth Code + PKCE/state와 opaque HttpOnly/Secure cookie 계약을 회귀화했다. 공개 Google OAuth 실제 `/turn` proof는 P0 blocker로 남아 있다.</p></div>
<div><b>산출물</b><p>`app/test_auth_providers.py`, PKCE state store, OAuth callback cookie path</p></div>
<div><b>검증</b><p>auth URL secret 미노출, browser token 미저장, dev/prod cookie 속성 분리</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>학습자/교수자/관리자 권한 격리</b><p>runtime fallback에서 타 learner session 직접 접근 403과 learner list 필터를 회귀화했고, teacher/admin durable read audit insert 경로를 추가했다. live Postgres(NOBYPASSRLS `vignette_app` 역할)로 RLS/audit smoke를 실행해 운영 증거를 확보했다: learner 세션/턴 격리, teacher cohort 스코프, teacher/admin read_session audit insert가 실제 정책 하에서 PASS. (smoke 스크립트의 cleanup RLS 컨텍스트 버그도 수정.)</p></div>
<div><b>산출물</b><p>`app/test_rbac_idor.py`, `audit.audit_log` read_session insert, `scripts/check-postgres-rls-audit.py`(live PASS)</p></div>
<div><b>검증</b><p>타인 `session_id` 직접 입력 403(unit), live RLS smoke 5 checks PASS(learner isolation, teacher cohort, evaluator-only hidden, audit insert)</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>`visible_to`와 인간 권한 레이어 분리</b><p>learner detail/review와 client engine history에서 evaluator-only turn을 제외한다. live Postgres smoke가 evaluator-only DB 정책을 실제로 검사해 PASS: evaluator-only turn이 learner/client/counselor AI에는 숨겨지고 evaluator AI에만 보인다. live DSN 실행 완료.</p></div>
<div><b>산출물</b><p>`TurnRecord.visible_to`, learner/client/evaluator filters, `docs/ops/postgres-rls-audit-smoke.md`</p></div>
<div><b>검증</b><p>evaluator-only turn 숨김 unit proof + live RLS smoke `visible_to.evaluator_only_hidden` PASS</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>페르소나 P2/P3 교수 검수 루프</b><p>approved catalog와 draft/review 교수 검수 큐를 분리하고, teacher/admin 승인·반려 mutation과 교수 콘솔 UI proof를 추가했다. 교수 콘솔의 404 raw 노출도 제거(404→정상 빈상태)했다. live Postgres RLS/audit smoke 실행으로 DB 권한/감사 레이어가 실제 정책 하에서 동작함을 확인했다.</p></div>
<div><b>산출물</b><p>`/personas/review`, `POST /personas/review/{persona_id}`, professor review panel</p></div>
<div><b>검증</b><p>`app.test_persona_review` 14 tests; teacher UI single-run E2E 1 passed; live RLS smoke PASS</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-doing">CHECK</span></div>
<div><b>SAML SP 스캐폴드 선반영</b><p>Google OIDC와 별도 provider 상태를 `/auth/config`에 노출하고, Redirect-binding AuthnRequest와 dev-only unsigned fixture ACS를 backend proof 수준으로 구현했다. 서명 검증이 필요한 운영 SAML ACS는 아직 미구현이며 fingerprint 설정 시 거부한다.</p></div>
<div><b>산출물</b><p>`AUTH_SAML_ENABLED`, `SAML_SP_ENTITY_ID`, `SAML_SSO_URL`, RelayState store, `/auth/saml/acs` fixture endpoint</p></div>
<div><b>검증</b><p>AuthnRequest issuer/ACS/destination, RelayState 검증, opaque HttpOnly cookie, bad RelayState/signature-required reject unit test</p></div>
</div>
</div>
</div>
<div class="tab-panel" id="panel-p2b" role="tabpanel" aria-labelledby="tab-p2b" hidden>
<div class="detail-head">
<div>
<h3>Phase 2b 음성 캐스케이드 상세</h3>
<p>음성은 “되게 보이는 UI”가 아니라, 전송, STT, EOT, 엔진, TTS, 저장까지 끊기지 않아야 한다.</p>
</div>
<div class="mini-metrics"><span>주차: 13-17</span><span>WSS 50분</span><span>실측 전 완료 금지</span></div>
</div>
<div class="task-list">
<div class="task-row">
<div><span class="task-status s-doing">CHECK</span></div>
<div><b>voice gateway WebSocket</b><p>`/voice/ws` backend contract를 fake WebSocket으로 회귀화했고, Session UI의 MediaRecorder 기반 브라우저 경로도 synthetic audio로 한 턴 통과시켰다. 실제 AudioWorklet 전환, public WSS, 50분 양방향 실측은 남아 있다.</p></div>
<div><b>산출물</b><p>`app/test_voice_ws.py`, `apps/web/e2e/voice-success.spec.ts`, audio_start/binary/audio_end/text_turn/ping contract</p></div>
<div><b>검증</b><p>5 backend tests OK; voice-success chromium-single-run 2 passed; auth guard, chunk concat, max audio cap, TTS chunk envelope</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-doing">CHECK</span></div>
<div><b>Deepgram STT + EOT</b><p>provider-neutral EOT helper로 final transcript와 silence threshold readiness를 회귀화했다. 실제 Deepgram interim/final 스트림 연결은 남아 있다.</p></div>
<div><b>산출물</b><p>`assess_end_of_turn`, `EndOfTurnDecision`, EOT silence threshold</p></div>
<div><b>검증</b><p>final pending, empty transcript, silence threshold 케이스 unit test</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-doing">CHECK</span></div>
<div><b>OpenAI TTS voice preset 추상화</b><p>P1/P2/P3 persona voice preset, invalid preset fallback, speed clamp, instructions payload 계약을 회귀화했다. DB `voice_persona_map`과 live TTS 호출은 남아 있다.</p></div>
<div><b>산출물</b><p>`resolve_voice`, `build_tts_payload`, provider-neutral params test</p></div>
<div><b>검증</b><p>P1/P2/P3 voice 분리, fallback, fake-client streaming payload OK</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-doing">CHECK</span></div>
<div><b>barge-in / paralinguistic logging</b><p>음성 learner turn에 audio_ref, silence_ms, speech_rate, barge_in을 nullable 메타로 저장한다. 실제 장시간 WSS/마이크 증거는 남아 있다.</p></div>
<div><b>산출물</b><p>`turns` nullable 확장, voice route 메타 산출</p></div>
<div><b>검증</b><p>오디오 STT→엔진→TTS 더블 테스트에서 voice turn log에 메타 저장</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-doing">CHECK</span></div>
<div><b>s2s 2차 PoC 판단</b><p>캐스케이드 1차 원칙과 s2s 유지/폐기 증거 기준을 문서화했다. 채택 여부 결정은 윤찬 DECIDE로 유지한다.</p></div>
<div><b>산출물</b><p>`docs/decisions/voice-s2s-poc.md`</p></div>
<div><b>검증</b><p>transcript, safety, audit, latency, failure, privacy 기준 명시</p></div>
</div>
</div>
</div>
<div class="tab-panel" id="panel-p3" role="tabpanel" aria-labelledby="tab-p3" hidden>
<div class="detail-head">
<div>
<h3>Phase 3 교육용 파일럿 / 효과성 상세</h3>
<p>IRB 승인 게이트가 아니라 교육 활용 파일럿과 데이터/개인정보 게이트로 관리한다.</p>
</div>
<div class="mini-metrics"><span>주차: 18-20+</span><span>표본: 20명</span><span>first measure</span></div>
</div>
<div class="task-list">
<div class="task-row">
<div><span class="task-status s-plan">GATE</span></div>
<div><b>20명 교육용 파일럿 운영</b><p>20명 파일럿을 실행하기 위한 roster, consent, session completion evidence gate를 문서화했다. 실제 참여자 완주 증거는 아직 필요하다.</p></div>
<div><b>산출물</b><p>`docs/phase3/pilot-runbook.md`, evidence root layout, operator gates</p></div>
<div><b>검증</b><p>`scripts/check-phase3-artifacts.py --help`; 실제 evidence root validation은 파일럿 시점에 실행</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">GATE</span></div>
<div><b>효과성/KPI 측정</b><p>SUS, 자기효능감, κ/ICC, 환각률, 피드백 타당도 기준과 report schema를 고정했다. 실제 수치 충족 여부는 파일럿 데이터 이후 판단한다.</p></div>
<div><b>산출물</b><p>`docs/phase3/kpi-measurement-plan.md`, KPI report schema</p></div>
<div><b>검증</b><p>checker가 `prepost_measures`, `sus_responses`, `kpi_report.json` evidence를 요구</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">GATE</span></div>
<div><b>재귀학습 데이터셋 1차 산출</b><p>anonymized JSONL export manifest와 누수/식별자 점검 기준을 문서화했다. 실제 export 승인과 데이터 steward 검수는 남아 있다.</p></div>
<div><b>산출물</b><p>`docs/phase3/dataset-export-manifest.md`, `03-export/export_manifest.json` contract</p></div>
<div><b>검증</b><p>source/hash/consent/withdrawal/anonymization fields 요구, raw identifier 금지</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">GATE</span></div>
<div><b>개인정보/동의 감사</b><p>동의, 철회, privacy audit, 해외 전송 고지 evidence checklist를 만들었다. 실제 법무/개인정보 검토 서명은 파일럿 전 gate로 남아 있다.</p></div>
<div><b>산출물</b><p>`docs/phase3/privacy-consent-audit.md`, withdrawal log contract</p></div>
<div><b>검증</b><p>동의 철회 후 session/export 제외 evidence를 checker 입력으로 요구</p></div>
</div>
</div>
</div>
<div class="tab-panel" id="panel-ops" role="tabpanel" aria-labelledby="tab-ops" hidden>
<div class="detail-head">
<div>
<h3>운영 / 검증 상세</h3>
<p>실제 운영 도메인에 반영됐는지, 서버가 하나로 정리됐는지, 완료라고 말할 증거가 있는지 확인하는 탭이다.</p>
</div>
<div class="mini-metrics"><span>웹: 200</span><span>API: prod OK</span><span>Live2D: 404</span></div>
</div>
<div class="task-list">
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>운영 웹 asset 확인</b><p>커스텀 도메인이 최신 JS/CSS asset을 서빙한다.</p></div>
<div><b>산출물</b><p>`index-D1Z5DH5J.js`, `index-BA9-xxfn.css`</p></div>
<div><b>검증</b><p>`Invoke-WebRequest https://vignette.chanpaca.net/`</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>공개 API prod health</b><p>터널은 prod API 8001을 보고, API는 DB와 engine을 모두 확인한다.</p></div>
<div><b>산출물</b><p>`environment:"prod"`, `db:true`, `engine:true`</p></div>
<div><b>검증</b><p>`https://api-vignette.chanpaca.net/health`</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>서버 정리 상태</b><p>운영 확인용 프로세스만 유지하고 임시 dev server는 내린다.</p></div>
<div><b>산출물</b><p>8001 API, 9099 engine, 20241 cloudflared</p></div>
<div><b>검증</b><p>`Get-NetTCPConnection` 대상 포트 확인</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-doing">CHECK</span></div>
<div><b>재부팅 후 런타임 재기동</b><p>engine gateway, prod API, cloudflared를 health-check하고 복구하는 watchdog과 Scheduled Task installer를 추가했다. 실제 재부팅 후 public `/turn` smoke는 남아 있다.</p></div>
<div><b>산출물</b><p>`watch-public-runtime.ps1`, `install-public-runtime-task.ps1`, ops runbook</p></div>
<div><b>검증</b><p>PowerShell parser OK, check-only watchdog 실행</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>레이아웃 감사팀 / 전면 레이아웃 개선</b><p>서브에이전트 전담으로 1차 overflow/clipping 수리 후 2차 전면 재설계를 진행했고, 이번 라운드에서 7개 권장 너비 시각 수용까지 닫았다. 세션, 학습자 홈/리뷰, 교수 콘솔, 관리자, 설정 화면을 작업 화면 기준으로 재배치했고, 과한 빈칸·튀어나감·버튼/텍스트 잘림·모바일 테이블 사용성을 줄였다. 엄격한 시각 게이트 + 화면별 적대적 시각 재검수에서 7개 화면 모두 accept(critical/major 0, 회귀 0)를 받아 DONE으로 닫는다. 잔여는 cosmetic minor(아래 백로그).</p></div>
<div><b>산출물</b><p>`docs/ops/layout-redesign-handoff-2026-06-26.md`, page-by-page redesign patches, `apps/web/e2e/layout-visual-gate.spec.ts`</p></div>
<div><b>검증</b><p>`npm run typecheck` OK, 엄격 시각 게이트 7 passed, layout focused E2E desktop/mobile 54 passed, 적대적 시각 재검수 7/7 accept·회귀 0.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>엄격한 레이아웃 시각 게이트 구축 · 병렬 수정 · 적대적 재검수</b><p>핸드오프가 요구한 7개 권장 너비(390/720/861/900/1024/1280/1440) 시각 수용을 자동 게이트로 고정했다. 7개 재설계 화면을 각 너비에서 렌더링해 가로 overflow 0, 컨트롤 하드클립/텍스트클립 0을 강제하고 화면당 7장 총 49장 풀페이지 스크린샷을 남긴다. 캐러셀(overflow-x 스크롤)·의도적 말줄임/line-clamp는 결함에서 제외해 false positive를 제거했다. 밀집 실데이터(긴 학습자명, 3회 누적 기록, 종료 회기 리뷰)로 진입. 화면별 1에이전트 병렬 시각 리뷰로 49장 직접 판독 → 6개 병렬 수정 에이전트(파일 비중첩)가 결함 수정 → 화면별 적대적 재검수로 해소·무회귀 확정.</p></div>
<div><b>산출물</b><p>`apps/web/e2e/layout-visual-gate.spec.ts`, `node_modules/.tmp/layout-gate/*.png` 49장, 2× workflow(review/verify)·6× fix agent</p></div>
<div><b>검증</b><p>게이트 7 passed(재스크린샷). 해소된 핵심 결함: 학습자홈 1280/1440 3열 가운데 컬럼 붕괴(critical → 2열 분기 1180→1400 상향 + word-break:keep-all), 교수 'API 404' raw 배너 제거(404→정상 빈상태/실장애만 재시도 카드), 리뷰 빈상태 검정카드 위계 역전(filled 조건부 강조), 관리자 빈/로딩 박스 shimmer 스켈레톤·플레이스홀더, 설정 와이드 좌측 데드존 제거(레일 슬림+본문 중앙정렬)·운영방식 2x2, 세션 모바일 컨트롤바 44px 터치타깃+'밀어서 종료' 라벨. 적대적 재검수 7/7 accept, regression 0.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-done">DONE</span></div>
<div><b>레이아웃 cosmetic minor 폴리시 일괄 처리</b><p>적대적 재검수 잔여 cosmetic minor를 5개 병렬 폴리시 에이전트로 처리했다. 학습자홈 헬퍼문구 고아 글자(word-break:keep-all+text-wrap:balance), 세션 1024 일시정지 아이콘-only 정리 + 보내기 버튼 활성/비활성 대비 강화, 설정 2x2 칩 행 높이 균일 + AI 운영 2열 높이 정렬, 교수 검토 큐 카드 갭 제거, 관리자 사용자 관리 2열 높이 동기화를 적용했다.</p></div>
<div><b>산출물</b><p>page-by-page polish patches(LearnerHome/Session/Settings/Professor/Admin)</p></div>
<div><b>검증</b><p>`npm run typecheck` OK, 시각 게이트 7 passed(재스크린샷), 레이아웃 E2E desktop/mobile 54 passed, session-layout 8/8 — 회귀 0.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">MINOR</span></div>
<div><b>잔여(비차단) — 공용 셸 단일 항목 → 처리(2026-06-26)</b><p>축소 사이드바 세로 구분선이 본문 전체 높이까지 닿지 않던 건을 `components/shell/shell.css`에서 처리했다. `.vg-nav border-right`(sticky·뷰포트 높이) 제거 후 `.vg-shell__body` 컨테이너 배경 하어라인(linear-gradient 1px×100%)으로 본문 그리드 전체 높이를 따라가는 구분선을 그리고, 그리드·구분선 폭을 `--nav-cur`로 동기화(1024 축소폭/720 가로바 전환 정합). learner-home 로딩 스켈레톤 밀도도 실제 카드 구조 모사로 보강했다. 데이터 0건 빈상태 컬럼 높이 여백은 의도적 미착수(0건 전용 시각 균형이라 러닝 앱 시각 확인 필요).</p></div>
<div><b>판정</b><p>구현 완료. 검증: `npm run typecheck` PASS + `vite build` PASS. 전 페이지 시각 회귀 게이트(7/7 + 포커스 E2E + session-layout 8/8)는 web+api+DB 스택(Docker 미기동·secrets 부재)으로 이 워크스테이션 미실행 — 스택 가용 시 1회 시각 확인 권장.</p></div>
</div>
</div>
<div class="tab-subhead">
<h4>검증 실행 상세 표</h4>
<p>요약하지 않고 실행 범위, 명령, 결과를 행 단위로 남긴다.</p>
</div>
<table>
<thead><tr><th>범위</th><th>명령 / 확인 대상</th><th>결과</th></tr></thead>
<tbody>
<tr><td>Web typecheck</td><td><code>npm run typecheck</code></td><td>Passed</td></tr>
<tr><td>Backend unittest</td><td><code>python -m unittest app.test_runtime_policy app.test_session_turn_persistence app.test_state_machine_resistance app.test_orchestrator_masking app.test_rbac_idor app.test_voice_service app.test_auth_providers app.test_voice_ws app.test_persona_review engine_gateway.test_gateway_model</code></td><td>84 tests OK</td></tr>
<tr><td>P1 cost telemetry</td><td><code>app.test_session_turn_persistence.SessionTurnPersistenceTest</code></td><td>Passed; generate/stream client AI turns store provider/model/tokens/cost</td></tr>
<tr><td>Engine stream contract</td><td><code>gateway token/done/error parsing</code></td><td>Passed; gateway error event does not create saved turns</td></tr>
<tr><td>Engine session reuse</td><td><code>engine_gateway.test_gateway_model</code></td><td>7 tests OK; live session_id reuse and ephemeral close fixed</td></tr>
<tr><td>P1 masking gate</td><td><code>app.test_orchestrator_masking</code></td><td>4 tests OK; phone/email/RRN absent from generate/stream payloads</td></tr>
<tr><td>P2a RBAC/audit/visibility</td><td><code>app.test_rbac_idor</code></td><td>7 tests OK; other learner 403, read_session audit, evaluator-only hidden</td></tr>
<tr><td>Postgres RLS/audit smoke checker</td><td><code>python scripts\check-postgres-rls-audit.py --help</code></td><td>py_compile/help/safe default OK; live DSN run still required for proof</td></tr>
<tr><td>OAuth/BFF cookie security</td><td><code>app.test_auth_providers</code></td><td>12 tests OK; PKCE/state, opaque HttpOnly/Secure cookie, no browser tokens, SAML AuthnRequest/dev-only fixture ACS</td></tr>
<tr><td>Persona review workflow</td><td><code>app.test_persona_review</code></td><td>14 tests OK; approved-only catalog, teacher/admin queue, approve/reject audit, learner 403</td></tr>
<tr><td>Voice WS contract</td><td><code>app.test_voice_ws</code></td><td>5 tests OK; auth guard, audio_start/chunks/audio_end, text_turn, ping, max audio cap</td></tr>
<tr><td>Voice preset/EOT</td><td><code>app.test_voice_service</code></td><td>9 tests OK; TTS payload, preset fallback, EOT readiness</td></tr>
<tr><td>Voice metadata</td><td><code>app.test_session_turn_persistence</code></td><td>audio_ref/silence_ms/speech_rate/barge_in persisted on learner voice turn</td></tr>
<tr><td>P1 MVP UI E2E</td><td><code>npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run</code></td><td>1 passed</td></tr>
<tr><td>Persona review UI E2E</td><td><code>npx playwright test e2e/teacher.spec.ts --project=chromium-single-run</code></td><td>1 passed; teacher approves pending persona from console</td></tr>
<tr><td>Voice UI synthetic E2E</td><td><code>npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run</code></td><td>2 passed; direct WS cascade plus Session mic button path with synthetic browser audio</td></tr>
<tr><td>Voice s2s decision memo</td><td><code>docs/decisions/voice-s2s-poc.md</code></td><td>criteria recorded; keep/drop decision remains owner DECIDE</td></tr>
<tr><td>Hanshin data/SSO gate</td><td><code>docs/ops/hanshin-data-governance-gate.md</code></td><td>artifact created; written external evidence still required</td></tr>
<tr><td>Resistance engine</td><td><code>app.test_state_machine_resistance</code></td><td>2 tests OK; empathy vs advice-jump curve fixed</td></tr>
<tr><td>Python compile</td><td><code>python -m compileall app engine_gateway</code></td><td>Passed</td></tr>
<tr><td>Phase 3 artifact gates</td><td><code>python scripts\check-phase3-artifacts.py --help</code></td><td>Passed; runbook, KPI plan, dataset manifest, privacy audit checker documented</td></tr>
<tr><td>Web build</td><td><code>npm run build</code></td><td>Passed</td></tr>
<tr><td>Pages production deploy</td><td><code>wrangler pages deploy dist --project-name vignette --branch main</code></td><td>`0fe8f3cc`, source `543d48c`</td></tr>
<tr><td>Custom domain assets</td><td><code>https://vignette.chanpaca.net/</code></td><td>`index-D1Z5DH5J.js`, `index-BA9-xxfn.css`</td></tr>
<tr><td>Legacy Live2D routes</td><td><code>/live2d/mao/* / /live2d/haru/* / /live2d/live2dcubismcore.min.js</code></td><td>404</td></tr>
<tr><td>Compose</td><td><code>docker compose --env-file .env.example config --quiet</code></td><td>Passed with dummy required env</td></tr>
<tr><td>Focused E2E</td><td><code>admin + db-persistence + voice-success</code></td><td>10 passed</td></tr>
<tr><td>Admin manage-users</td><td><code>admin.spec.ts --grep manage real server-known users</code></td><td>desktop/mobile 2 passed</td></tr>
<tr><td>Full E2E</td><td><code>PLAYWRIGHT_HOST=127.0.0.1 npx playwright test</code></td><td>72 passed</td></tr>
<tr><td>Public auth discovery</td><td><code>E2E_PUBLIC_AUTH=1 npx playwright test --list --project=chromium-public-auth</code></td><td>2 tests listed</td></tr>
<tr><td>Public readiness</td><td><code>public-auth-turn.spec.ts --grep production-safe</code></td><td>1 passed</td></tr>
<tr><td>Persona auth boundary</td><td><code>GET /personas</code></td><td>verified local/public unauth 401</td></tr>
<tr><td>Public login</td><td><code>auth.spec.ts --grep public login</code></td><td>1 passed</td></tr>
<tr><td>Public runtime scripts</td><td><code>start/watch/install-public-runtime*.ps1</code></td><td>parser OK; watchdog check-only path verified</td></tr>
<tr><td>Public API health</td><td><code>https://api-vignette.chanpaca.net/health</code></td><td>prod, db true, engine true</td></tr>
<tr><td>Local 5175 login</td><td><code>PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 auth.spec.ts</code></td><td>desktop/mobile passed</td></tr>
<tr><td>Learner/readiness E2E</td><td><code>learner.spec.ts + readiness.spec.ts desktop/mobile</code></td><td>14 passed</td></tr>
<tr><td>Learner screenshots</td><td><code>learn-empty-desktop.png / learn-empty-mobile-compact.png</code></td><td>document overflow 0, empty history visible</td></tr>
<tr><td>Session layout/turn</td><td><code>session-layout.spec.ts desktop/mobile</code></td><td>8 passed</td></tr>
<tr><td>Voice/session focused</td><td><code>voice + voice-success + session-layout</code></td><td>15 passed</td></tr>
<tr><td>Layout redesign handoff</td><td><code>docs/ops/layout-redesign-handoff-2026-06-26.md</code></td><td>subagent scopes, files, verification, remaining visual review recorded</td></tr>
<tr><td>Layout redesign focused E2E</td><td><code>npx playwright test e2e/learner.spec.ts e2e/session-layout.spec.ts e2e/session-review.spec.ts e2e/admin.spec.ts e2e/settings.spec.ts e2e/teacher.spec.ts --project=chromium-desktop --project=chromium-mobile</code></td><td>54 passed after isolated Admin tablet flake rerun</td></tr>
<tr><td>Strict layout visual gate</td><td><code>npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run</code></td><td>7 passed; 7 screens × 7 widths(390/720/861/900/1024/1280/1440), horizontal overflow 0 + control clip 0, 49 full-page screenshots</td></tr>
<tr><td>Parallel visual quality review</td><td><code>workflow layout-visual-quality-review (7 agents)</code></td><td>49 screenshots read; per-screen findings(critical 1, major 8+, minor) → fix verdicts</td></tr>
<tr><td>Parallel layout fixes</td><td><code>6 agent team: learner-home/session/review/professor/admin/settings (disjoint files)</code></td><td>all fixes applied; `npm run typecheck` OK; no cross-file conflicts</td></tr>
<tr><td>Strict gate re-run (post-fix)</td><td><code>npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run</code></td><td>7 passed; re-screenshotted 49</td></tr>
<tr><td>Layout focused E2E re-run (post-fix)</td><td><code>learner/session-layout/session-review/admin/settings/teacher × desktop+mobile</code></td><td>54 passed; no regression</td></tr>
<tr><td>Adversarial visual verify</td><td><code>workflow layout-visual-verify (7 agents)</code></td><td>7/7 accept; critical/major resolved, regression 0; minor polish backlog recorded</td></tr>
<tr><td>Engine config 운영값</td><td><code>GET /admin/engine-config</code></td><td>claude_cli / http://127.0.0.1:9099 / gateway-default, durable, source=database (의도값 일치)</td></tr>
<tr><td>Engine gateway probe (live)</td><td><code>scripts/probe-engine-gateway.py --base-url http://127.0.0.1:9099 --reuse-runs 2</code></td><td>claude_p / opus-4-8; 단일 session_id 2회 reused stream; TTFT 16674199ms; cost 누적 0.068→0.123(turns 1→2)</td></tr>
<tr><td>Postgres RLS/audit smoke (live)</td><td><code>VIGNETTE_RLS_AUDIT_DSN=…vignette_app… check-postgres-rls-audit.py --write-fixtures</code></td><td>5 checks PASS: preflight, learner 세션/턴 격리, teacher cohort 스코프, visible_to evaluator-only 숨김, teacher/admin read audit insert (NOBYPASSRLS app role, fixtures 자동 정리)</td></tr>
<tr><td>Turn cost telemetry (live DB)</td><td><code>SELECT … FROM app.turns WHERE actor_kind='client_ai'</code></td><td>13 client_ai turns 적재: llm_provider=claude_cli, model=gateway-default, cost_usd 실값(합 $1.22, 평균 $0.094), visible_to 배열. 토큰은 CLI 게이트웨이 특성상 0, cost는 산출</td></tr>
<tr><td>Cosmetic polish re-verify</td><td><code>5 polish agents → typecheck + gate + layout E2E re-run</code></td><td>typecheck OK; gate 7 passed; layout focused E2E 54 passed; session-layout 8/8; regression 0</td></tr>
</tbody>
</table>
<div class="tab-subhead">
<h4>다음 실행 명령</h4>
<p>남은 public OAuth `/turn` proof 확보용 명령이다.</p>
</div>
<div class="commands">
<pre>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</pre>
<pre>cd D:\workspace\vignette
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1</pre>
</div>
<div class="tab-subhead">
<h4>이번 세션 live 운영 증거 종합</h4>
<p>OAuth `/turn` proof와 한신대 거버넌스를 제외한 나머지 중 이 환경에서 실증 가능한 항목을 닫았다.</p>
</div>
<div class="source-note">
<b>닫은 항목(live 증거):</b> ① engine config 운영값(`claude_cli / 9099 / gateway-default`, durable, db). ② 상주 엔진풀 probe — 단일 session_id 2회 reused stream, TTFT 16674199ms, cost 누적 0.068→0.123. ③ Postgres RLS/audit smoke 5 checks PASS(learner 격리·teacher cohort·evaluator-only 숨김·audit insert; NOBYPASSRLS `vignette_app` 역할 프로비저닝, smoke cleanup RLS 버그 수정). ④ turn cost telemetry — `app.turns` client_ai 13행에 provider/model/cost_usd/visible_to 실적재(합 $1.22). ⑤ 레이아웃 시각 게이트 7/7 + 적대적 재검수 7/7 accept.
</div>
<div class="source-note">
<b>환경 제약으로 이 세션에서 못 닫는 항목(정직 표기):</b> 공개 Google OAuth 실제 `/turn`(owner 지시로 보류) · 한신대 데이터/SSO 거버넌스(owner 지시로 보류) · 음성 live STT/TTS provider·물리 마이크·공개 WSS 50분(provider 키/하드웨어 부재) · 저항엔진 openness 곡선 DB 실증(turn_client_state 0행, 대량 실턴 필요 — 단위테스트로 증명) · claude_cli↔Messages API 폴백 동일성(ANTHROPIC_API_KEY 부재) · 재부팅 후 watchdog smoke(재부팅 불가) · Phase 3 파일럿 게이트(실참여자 필요) · decisions 5건(owner 결정). 운영 원칙상 가짜 증거로 DONE 표기하지 않는다. 전체 추적 목록은 `docs/ops/backlog-2026-06-26.md`(B1 폴리시 · B2 환경제약 · B3 소유자결정 · B4 거버넌스).
</div>
</div>
<div class="tab-panel" id="panel-decisions" role="tabpanel" aria-labelledby="tab-decisions" hidden>
<div class="detail-head">
<div>
<h3>윤찬 결정 필요 상세</h3>
<p>람다가 단독으로 정하면 월권인 항목이다. 결정되기 전에는 “완료”가 아니라 조건부로 둔다.</p>
</div>
<div class="mini-metrics"><span>결정 5개</span><span>조건부 GO 사유</span></div>
</div>
<div class="task-list">
<div class="task-row">
<div><span class="task-status s-risk">DECIDE</span></div>
<div><b>s2s 2차 PoC 채택 여부</b><p>캐스케이드 1차는 확정. `voice-s2s-poc.md` 기준표에 따라 비교 리포트가 나온 뒤 유지/폐기를 정한다.</p></div>
<div><b>선택지</b><p>캐스케이드만 / s2s 2차 PoC 유지</p></div>
<div><b>영향</b><p>음성 아키텍처, 전송 비용, 로그 보존 방식. 람다 단독 결정 금지.</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-risk">DECIDE</span></div>
<div><b>fast-loop 외부 API 허용 여부</b><p>로컬 상주 분류기를 기본으로 하되, 외부 Haiku/Solar API 폴백을 허용할지 정한다.</p></div>
<div><b>선택지</b><p>로컬 전용 / 외부 폴백 허용</p></div>
<div><b>영향</b><p>데이터주권, 레이턴시, 장애 대응</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-risk">DECIDE</span></div>
<div><b>재귀학습 fine-tuning 범위</b><p>1차는 few-shot 자동갱신만 할지, fine-tuning까지 포함할지 결정한다.</p></div>
<div><b>선택지</b><p>few-shot only / fine-tuning 포함</p></div>
<div><b>영향</b><p>동의서, 데이터셋 품질, 운영 리스크</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-risk">DECIDE</span></div>
<div><b>자유연습 기본값</b><p>수정안은 기본 ON + 피드백 권장 배지 + 교수자 토글을 제안한다.</p></div>
<div><b>선택지</b><p>기본 ON / 교수자 승인 후 ON</p></div>
<div><b>영향</b><p>학습자 자율성, 평가 품질, 안전 정책</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-risk">DECIDE</span></div>
<div><b>추가 축어록 수급 규모/일정</b><p>n=1 시드의 한계를 해소하려면 추가 사례 수급 규모와 일정을 정해야 한다.</p></div>
<div><b>선택지</b><p>N건 목표/일정 확정</p></div>
<div><b>영향</b><p>평가 타당도, 페르소나 다양성, Phase 3 측정력</p></div>
</div>
</div>
<div class="detail-blocks">
<div class="detail-block">
<h4>완료라고 말하는 기준</h4>
<ul>
<li>실제 로그인 기반 E2E 증거가 있어야 한다.</li>
<li>DB 쿼리나 로그로 저장/권한/비용이 확인되어야 한다.</li>
<li>시각 확인만으로 기능 완료를 선언하지 않는다.</li>
</ul>
</div>
<div class="detail-block">
<h4>문서 기준</h4>
<ul>
<li>`MASTERPLAN`은 원본 보존 문서다.</li>
<li>`MASTERPLAN_REVISIONS`가 Phase/IRB/음성 정정 기준이다.</li>
<li>`dev_dashboard.html`은 팀장 판단용 현재판이다.</li>
</ul>
</div>
<div class="detail-block">
<h4>운영 원칙</h4>
<ul>
<li>mock/demo 자산은 운영 번들에서 제거한다.</li>
<li>ignored runtime 파일은 커밋하지 않는다.</li>
<li>서버는 목적별 1개씩만 유지한다.</li>
<li>`frontenddesign` 스킬이 있으면 먼저 쓰고, 없으면 `DESIGN_CONCEPT`를 SSOT로 따른다.</li>
<li>UX는 `docs/DESIGN_CONCEPT.md`를 기준으로 하고, fake 기록/통계는 금지한다.</li>
<li>OAuth secret은 출력하지 않고 환경 파일/인프라 설정에만 둔다.</li>
</ul>
</div>
</div>
</div>
</div>
</section>
</main>
<script>
const tabButtons = Array.from(document.querySelectorAll("[role='tab'][data-tab]"));
const tabPanels = Array.from(document.querySelectorAll("[role='tabpanel']"));
function activateTab(nextButton) {
tabButtons.forEach((button) => {
button.setAttribute("aria-selected", String(button === nextButton));
button.tabIndex = button === nextButton ? 0 : -1;
});
tabPanels.forEach((panel) => {
panel.hidden = panel.id !== nextButton.getAttribute("aria-controls");
});
}
tabButtons.forEach((button, index) => {
button.addEventListener("click", () => activateTab(button));
button.addEventListener("keydown", (event) => {
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
event.preventDefault();
let nextIndex = index;
if (event.key === "ArrowLeft") nextIndex = (index - 1 + tabButtons.length) % tabButtons.length;
if (event.key === "ArrowRight") nextIndex = (index + 1) % tabButtons.length;
if (event.key === "Home") nextIndex = 0;
if (event.key === "End") nextIndex = tabButtons.length - 1;
tabButtons[nextIndex].focus();
activateTab(tabButtons[nextIndex]);
});
});
// ── 폴드아웃(섹션 접기/펼치기) + 목차(드릴다운) — DOM만 재배치, 텍스트는 그대로 보존 ──
(function () {
const main = document.querySelector("main");
const hero = main ? main.querySelector(".hero") : null;
const sections = Array.from(document.querySelectorAll("main > section"));
if (!main || !sections.length) return;
const toc = document.createElement("nav");
toc.className = "dash-toc";
toc.setAttribute("aria-label", "대시보드 목차");
const label = document.createElement("span");
label.className = "toc-label";
label.textContent = "목차";
toc.appendChild(label);
const entries = [];
sections.forEach((section, i) => {
const head = section.querySelector(":scope > .section-head");
if (!head) return;
const h2 = head.querySelector("h2");
const title = h2 ? h2.textContent.trim() : "섹션 " + (i + 1);
const id = section.id || "dash-sec-" + (i + 1);
section.id = id;
// 섹션-헤더 이후 모든 형제를 fold-body 로 이동(텍스트 변경 없음, 노드 이동만)
const body = document.createElement("div");
body.className = "fold-body";
let node = head.nextElementSibling;
while (node) { const next = node.nextElementSibling; body.appendChild(node); node = next; }
section.appendChild(body);
head.classList.add("fold-head");
head.setAttribute("role", "button");
head.setAttribute("tabindex", "0");
head.setAttribute("aria-expanded", "true");
const setFolded = (folded) => {
section.classList.toggle("folded", folded);
head.setAttribute("aria-expanded", String(!folded));
};
head.addEventListener("click", () => setFolded(!section.classList.contains("folded")));
head.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setFolded(!section.classList.contains("folded")); }
});
const a = document.createElement("a");
a.href = "#" + id;
a.textContent = title;
a.title = title;
a.addEventListener("click", () => setFolded(false));
toc.appendChild(a);
entries.push(setFolded);
});
const spacer = document.createElement("span");
spacer.className = "toc-spacer";
toc.appendChild(spacer);
const expandAll = document.createElement("button");
expandAll.type = "button";
expandAll.textContent = "모두 펼치기";
expandAll.addEventListener("click", () => entries.forEach((fn) => fn(false)));
const collapseAll = document.createElement("button");
collapseAll.type = "button";
collapseAll.textContent = "모두 접기";
collapseAll.addEventListener("click", () => entries.forEach((fn) => fn(true)));
toc.appendChild(expandAll);
toc.appendChild(collapseAll);
if (hero && hero.nextSibling) main.insertBefore(toc, hero.nextSibling);
else main.insertBefore(toc, main.firstChild);
// 기본: 최신 2개 섹션(이번 세션 발견·갭 분석)만 펼치고 나머지는 접어 한눈에 보이게
entries.forEach((fn, i) => fn(i >= 2));
})();
</script>
</body>
</html>