vignette/docs/dev_dashboard.html

747 lines
45 KiB
HTML

<!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; }
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; }
}
</style>
</head>
<body>
<header>
<div class="brand"><span class="mark" aria-hidden="true"></span><span>Vignette 개발 대시보드</span></div>
<div class="stamp">2026-06-26 16:04 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 전환까지 완료했다.
이후 로컬 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를 내렸다.
다만 이 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>20 OK</b></div>
<div class="metric"><span>Roadmap</span><b>P0-P3</b></div>
<div class="metric"><span>Decision</span><b>P0 조건부</b></div>
</div>
</div>
<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 20 + 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-doing">CHECK</span></div>
<div><b>engine config 운영값 재확인</b><p>E2E가 admin engine config를 만질 수 있으므로 배포 전 `mode/url/model` 값을 확인한다.</p></div>
<div><b>산출물</b><p>admin AI 운영 화면 캡처 또는 config API 응답</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 진입 전 외부 의존성으로 명확히 둔다.</p></div>
<div><b>산출물</b><p>공문 질의서, 동의 범위 체크리스트, SSO claim 표</p></div>
<div><b>검증</b><p>IRB 게이트가 아니라 데이터/SSO 게이트로 문서 전수 정합</p></div>
</div>
</div>
<div class="detail-blocks">
<div class="detail-block">
<h4>팀장 판정</h4>
<ul>
<li>로컬 회귀와 공개 prod-safe 게이트는 통과했다.</li>
<li>Cloudflare Pages production `0fe8f3cc`이 `543d48c` 소스로 배포됐다.</li>
<li>운영 반영 전 남은 핵심 증거는 공개 Google OAuth 실제 `/turn` smoke다.</li>
<li>런타임 mock/demo 자산은 운영 번들에서 제거하고, 테스트 fixture만 남긴다.</li>
</ul>
</div>
<div class="detail-block">
<h4>제품/UX 완료</h4>
<ul>
<li>학습자 홈 0회 기록 상태는 fake 기록 없이 실제 서버 세션 0개로 표시한다.</li>
<li>active session은 실제 SSE token 이벤트로 자막을 쌓는다.</li>
<li>텍스트/음성 실패 턴은 transcript와 저장소에 남기지 않는다.</li>
<li>375px급 active session viewport overflow를 보정했다.</li>
<li>직접 URL persona 우회는 DB 원본 persona가 아니면 차단한다.</li>
<li>세션 화면은 SVG persona avatar만 렌더링한다.</li>
</ul>
</div>
<div class="detail-block">
<h4>운영/테스트 완료</h4>
<ul>
<li>seed persona 결합을 제거하고 DB persona 기반 E2E로 전환했다.</li>
<li>비개발 런타임은 dev login, seed fallback, 기본 secret, localhost CORS를 fail-closed 한다.</li>
<li>compose OAuth/CORS/OpenAI base 설정을 API 컨테이너에 명시했다.</li>
<li>persona catalog는 비로그인 401로 인증 경계를 가진다.</li>
<li>admin 모바일 overflow, AI 운영 상태 문구, global engine config 테스트 격리를 정리했다.</li>
<li>handoff, E2E README, 이 대시보드를 최신 상태로 맞췄다.</li>
</ul>
</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-plan">TODO</span></div>
<div><b>LLMEngine 정규화 계약 고정</b><p>claude_cli와 Messages API 폴백이 같은 이벤트 스키마와 cost 규약을 반환하게 한다.</p></div>
<div><b>산출물</b><p>`delta.text`, `result`, `cost` 이벤트 계약과 contract test</p></div>
<div><b>검증</b><p>동일 입력 → 두 어댑터 정상화 출력 동일성 테스트</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>상주 `claude -p` 엔진풀 실증</b><p>`--input-format stream-json` 상주 프로세스를 회기당 1회 기동하는 구조로 검증한다.</p></div>
<div><b>산출물</b><p>TTFT p50/p95, 동시 4회기 RSS/OOM 로그</p></div>
<div><b>검증</b><p>실측 전 “빠름/검증됨” 표기 금지</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>마스킹 게이트 강제</b><p>claude_cli 경로도 Presidio/가명처리 후에만 stdin으로 들어가게 막는다.</p></div>
<div><b>산출물</b><p>엔진 어댑터 전단 마스킹 미들웨어, 우회 차단 테스트</p></div>
<div><b>검증</b><p>민감 텍스트 raw 외부 전송 경로 0개</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>P1 상담 완주 플로우</b><p>로그인, P1 선택, SSE 상담, 종료, 회기말 리뷰를 한 번에 통과시킨다.</p></div>
<div><b>산출물</b><p>turn DB 적재, session review, cost telemetry</p></div>
<div><b>검증</b><p>Playwright로 실제 로그인 → 상담 완주 → 피드백 수신</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>저항 엔진 정량 검증</b><p>공감 발화와 조언점프 발화 세트를 넣어 openness 곡선을 비교한다.</p></div>
<div><b>산출물</b><p>공감 5턴 vs 조언 5턴 DB 쿼리 결과</p></div>
<div><b>검증</b><p>공감 시 개방도 상승, 조언점프 시 하락 증거</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>음성 왕복 1턴 PoC</b><p>Phase 1에서 음성을 완전히 뒤로 미루지 않고, 얇은 1턴 왕복 증거를 만든다.</p></div>
<div><b>산출물</b><p>STT transcript, TTS audio, voice turn log</p></div>
<div><b>검증</b><p>실제 마이크/오디오 1턴 + 실패 시 턴 미저장</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-plan">TODO</span></div>
<div><b>OAuth BFF / HttpOnly 세션 완성</b><p>브라우저에 토큰을 저장하지 않는 BFF 세션을 역할 페이지 전체에 통일한다.</p></div>
<div><b>산출물</b><p>Auth Code + PKCE, stateful session, role claim mapping</p></div>
<div><b>검증</b><p>비로그인 401, 역할 없는 접근 403, 쿠키 보안 속성 확인</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>학습자/교수자/관리자 권한 격리</b><p>RBAC와 cohort 소유권 게이트를 모든 session/review/admin API에 적용한다.</p></div>
<div><b>산출물</b><p>권한 미들웨어, audit log, role route map</p></div>
<div><b>검증</b><p>타인 `session_id` 직접 입력 → 403 스크린샷</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>`visible_to`와 인간 권한 레이어 분리</b><p>AI 정보비대칭과 사용자 권한을 섞지 않는다. 인간 조회는 RBAC와 cohort AND 게이트다.</p></div>
<div><b>산출물</b><p>RLS 정책, enforcement middleware, 테스트 matrix</p></div>
<div><b>검증</b><p>AI-only hidden field가 인간 조회 권한을 열지 않음</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>페르소나 P2/P3 교수 검수 루프</b><p>P1 단일 시드에서 P2/P3로 확장하되 교수 검수 상태와 공개 가능 상태를 분리한다.</p></div>
<div><b>산출물</b><p>persona review workflow, versioning, approval state</p></div>
<div><b>검증</b><p>미승인 persona는 학습자 시작 불가</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>SAML SP 스캐폴드 선반영</b><p>1차는 Google OIDC로 가되 한신대 SSO 전환 지점을 Authlib provider로 남긴다.</p></div>
<div><b>산출물</b><p>provider abstraction, SAML placeholder config</p></div>
<div><b>검증</b><p>Google OIDC 경로 회귀 없이 provider 교체 가능</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-plan">TODO</span></div>
<div><b>voice gateway WebSocket</b><p>브라우저 AudioWorklet PCM을 WSS로 보내고, TTS 오디오 청크를 내려받는다.</p></div>
<div><b>산출물</b><p>`voice.chanpaca.net` 또는 단일 도메인 WSS 경로</p></div>
<div><b>검증</b><p>50분 WSS 양방향 스트림 끊김 실측</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>Deepgram STT + EOT</b><p>interim/final transcript와 상담 튜닝 침묵 임계, semantic EOT를 연결한다.</p></div>
<div><b>산출물</b><p>transcript timestamp, silence_ms, final segment</p></div>
<div><b>검증</b><p>침묵/말 끊김/짧은 대답 케이스 회귀</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>OpenAI TTS voice preset 추상화</b><p>persona별 voice_id와 prosody instruction을 저장하고 교체 가능하게 한다.</p></div>
<div><b>산출물</b><p>`voice_persona_map`, provider-agnostic params</p></div>
<div><b>검증</b><p>P1/P2/P3 voice preset 분리, fallback 정상</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>barge-in / paralinguistic logging</b><p>중간 끼어들기, 침묵, 말속도 같은 음성 메타를 턴 로그와 분리 저장한다.</p></div>
<div><b>산출물</b><p>`turn_paralinguistic` 또는 turns nullable 확장</p></div>
<div><b>검증</b><p>barge-in 케이스가 audit 가능하게 남음</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>s2s 2차 PoC 판단</b><p>캐스케이드는 1차로 확정하고, s2s는 transcript 보존/몰입감 기준으로 별도 판단한다.</p></div>
<div><b>산출물</b><p>캐스케이드 vs s2s 비교 메모</p></div>
<div><b>검증</b><p>윤찬 결정 항목으로 기록, 람다 단독 폐기 금지</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">TODO</span></div>
<div><b>20명 교육용 파일럿 운영</b><p>수련생이 실제 계정으로 세션을 완주하고 사전/사후 설문을 남긴다.</p></div>
<div><b>산출물</b><p>참여자별 session, pre/post, consent state</p></div>
<div><b>검증</b><p>20명 완주율, 중도중단 사유, 세션 로그 대조</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>효과성/KPI 측정</b><p>SUS, 자기효능감, κ/ICC, 환각률, 피드백 타당도를 파일럿에서 처음 측정한다.</p></div>
<div><b>산출물</b><p>KPI report, metric query, anonymized export</p></div>
<div><b>검증</b><p>SUS≥80, κ≥0.6/ICC≥0.75 같은 기준 충족 여부</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>재귀학습 데이터셋 1차 산출</b><p>라벨링/검수/κ 누적은 영속 워크플로우로 관리하고 fine-tuning 여부는 별도 결정한다.</p></div>
<div><b>산출물</b><p>export_manifest, JSONL, annotation checkpoint</p></div>
<div><b>검증</b><p>train/test 누수 없음, 원문/가명본 경계 확인</p></div>
</div>
<div class="task-row">
<div><span class="task-status s-plan">TODO</span></div>
<div><b>개인정보/동의 감사</b><p>미성년 원본 활용동의, 가명처리, 해외 전송 고지, 삭제/철회 절차를 실제 운영 기준으로 점검한다.</p></div>
<div><b>산출물</b><p>privacy checklist, audit log, withdrawal runbook</p></div>
<div><b>검증</b><p>동의 철회 후 접근/학습 사용 차단 재현</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-risk">WATCH</span></div>
<div><b>재부팅 후 런타임 재기동</b><p>Windows 재부팅/업데이트 후에는 engine gateway와 prod API, cloudflared를 같이 올려야 한다.</p></div>
<div><b>산출물</b><p>runbook 또는 scheduled task</p></div>
<div><b>검증</b><p>재기동 후 public health와 `/turn` smoke 재확인</p></div>
</div>
</div>
<div class="detail-blocks">
<div class="detail-block">
<h4>검증 실행 결과</h4>
<ul>
<li>Web typecheck: `npm run typecheck` passed.</li>
<li>Backend unittest: `app.test_runtime_policy`, `app.test_session_turn_persistence`, `engine_gateway.test_gateway_model` 20 OK.</li>
<li>Python compile: `python -m compileall app engine_gateway` passed.</li>
<li>Web build: `npm run build` passed.</li>
<li>Full E2E: `PLAYWRIGHT_HOST=127.0.0.1 npx playwright test` 72 passed.</li>
<li>Latest focused E2E: learner + session-layout + settings + admin 37 passed.</li>
</ul>
</div>
<div class="detail-block">
<h4>공개 운영 증거</h4>
<ul>
<li>Pages deploy: `0fe8f3cc`, source `543d48c`, branch `main`.</li>
<li>Custom domain assets: `index-D1Z5DH5J.js`, `index-BA9-xxfn.css`.</li>
<li>Public readiness: `public-auth-turn.spec.ts --grep production-safe` 1 passed.</li>
<li>Public login: `auth.spec.ts --grep public login` 1 passed.</li>
<li>Legacy Live2D routes: Mao/Haru/Cubism core 404.</li>
<li>Persona auth boundary: local/public unauth `GET /personas` 401.</li>
</ul>
</div>
<div class="detail-block">
<h4>다음 실행 명령</h4>
<ul>
<li>`npx playwright codegen https://vignette.chanpaca.net/login --save-storage=.\node_modules\.tmp\public-auth.json`</li>
<li>`$env:E2E_PUBLIC_AUTH="1"`</li>
<li>`$env:E2E_PUBLIC_STORAGE_STATE=".\node_modules\.tmp\public-auth.json"`</li>
<li>`.\node_modules\.bin\playwright.cmd test e2e/public-auth-turn.spec.ts --project=chromium-public-auth`</li>
<li>`powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1`</li>
</ul>
</div>
</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차는 확정. s2s는 몰입감과 transcript 보존 기준으로 유지/폐기를 정한다.</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]);
});
});
</script>
</body>
</html>