vignette/docs/dev_dashboard.html
2026-06-28 11:31:07 +09:00

1074 lines
197 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">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root{
--paper:#FBFAF8; --paper-2:#F1EDE6; --paper-3:#ECE7DE;
--surface:#FFFFFF; --hair:#E5E1DA; --hair-2:#D8D2C8;
--ink:#1C2A2A; --ink-2:#566260; --ink-3:#93A09C;
--accent:#3E7A6E; --accent-deep:#2B5249; --accent-tint:#EBF2F0;
--clay:#B0735C; --clay-deep:#8F5641; --clay-tint:#F6EBE4;
--pos:#2C6E49; --warn:#8A5A00; --crit:#A8352C; --plan:#3C7299;
--pos-bg:#E7F2EB; --warn-bg:#F8EFDD; --crit-bg:#F9E7E4; --plan-bg:#E8F0F6;
--font:"Pretendard Variable",Pretendard,-apple-system,"Apple SD Gothic Neo","Noto Sans KR",system-ui,sans-serif;
--mono:"JetBrains Mono",ui-monospace,"SFMono-Regular",monospace;
}
*{box-sizing:border-box;}
html{scroll-padding-top:76px;}
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--font);line-height:1.55;-webkit-font-smoothing:antialiased;}
a{color:var(--accent-deep);}
/* header */
header{position:sticky;top:0;z-index:20;display:flex;align-items:center;justify-content:space-between;gap:18px;min-height:56px;padding:0 26px;background:rgba(251,250,248,.9);border-bottom:1px solid var(--hair);backdrop-filter:blur(12px);}
.brand{display:flex;align-items:center;gap:11px;font-weight:780;letter-spacing:-.01em;}
.mark{width:24px;height:24px;border:2px solid var(--accent);border-radius:50%;position:relative;flex:none;}
.mark:after{content:"";position:absolute;left:7px;top:6px;width:6px;height:6px;border-radius:50%;background:var(--clay);}
.brand small{color:var(--ink-3);font:600 11px/1 var(--mono);letter-spacing:.04em;}
.head-right{display:flex;align-items:center;gap:12px;}
.owner-pill{display:inline-flex;align-items:center;gap:8px;padding:6px 13px;border-radius:999px;background:var(--clay-tint);border:1px solid color-mix(in srgb,var(--clay) 34%,var(--hair));color:var(--clay-deep);font:700 12.5px/1 var(--font);text-decoration:none;}
.owner-pill b{font:800 13px/1 var(--mono);}
.owner-pill .d{width:8px;height:8px;border-radius:50%;background:var(--clay);}
.stamp{color:var(--ink-3);font:600 11.5px/1 var(--mono);}
main{width:min(1240px,calc(100% - 40px));margin:0 auto;padding:30px 0 80px;}
.eyebrow{display:inline-flex;align-items:center;gap:7px;color:var(--accent);font:700 11px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;}
.eyebrow:before{content:"";width:6px;height:6px;border-radius:50%;background:var(--accent);}
h1{margin:11px 0 0;font-size:27px;line-height:1.25;letter-spacing:-.015em;}
h1 em{font-style:normal;color:var(--accent-deep);}
.sec-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin:0 0 14px;}
.sec-head h2{margin:0;font-size:18px;line-height:1.3;letter-spacing:-.01em;}
.sec-head .sub{margin:5px 0 0;color:var(--ink-3);font-size:12.5px;max-width:780px;}
section.block{margin-top:34px;scroll-margin-top:76px;}
/* PULSE / hero */
.pulse{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(330px,.95fr);gap:22px;align-items:stretch;margin-top:22px;}
.pulse-main{background:var(--surface);border:1px solid var(--hair);border-radius:16px;padding:22px 24px;box-shadow:0 1px 2px rgba(28,42,42,.05);}
.pulse-main h1{margin-top:9px;}
.pulse-state{margin:13px 0 0;color:var(--ink-2);font-size:13.5px;max-width:640px;}
.pulse-state b{color:var(--ink);}
.phase-rail{display:grid;grid-template-columns:repeat(5,1fr);gap:7px;margin-top:18px;}
.pr-seg{position:relative;}
.pr-bar{height:7px;border-radius:999px;background:var(--paper-3);overflow:hidden;}
.pr-fill{height:100%;border-radius:999px;background:linear-gradient(90deg,var(--accent),var(--accent-deep));}
.pr-seg.cur .pr-fill{background:linear-gradient(90deg,var(--warn),#a86b00);}
.pr-seg.idle .pr-fill{background:var(--plan);opacity:.5;}
.pr-meta{display:flex;justify-content:space-between;align-items:baseline;margin-top:7px;}
.pr-id{font:700 10.5px/1 var(--mono);color:var(--ink-2);}
.pr-pct{font:700 11px/1 var(--mono);color:var(--ink-3);}
.pr-name{display:block;margin-top:3px;color:var(--ink-3);font-size:10.5px;line-height:1.3;}
.pr-seg.cur .pr-id{color:var(--warn);}
/* KPI cards */
.kpis{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
.kpi{position:relative;background:var(--surface);border:1px solid var(--hair);border-radius:14px;padding:16px 17px;box-shadow:0 1px 2px rgba(28,42,42,.05);display:flex;flex-direction:column;justify-content:space-between;min-height:108px;text-decoration:none;color:inherit;}
.kpi .lab{display:flex;align-items:center;gap:7px;color:var(--ink-3);font-size:11.5px;font-weight:680;}
.kpi .lab .dot{width:8px;height:8px;border-radius:50%;}
.kpi .num{font:800 34px/1 var(--mono);letter-spacing:-.02em;margin-top:auto;}
.kpi .num small{font:700 14px/1 var(--mono);color:var(--ink-3);margin-left:3px;}
.kpi .hint{margin-top:5px;color:var(--ink-3);font-size:11px;}
.kpi.k-done .num{color:var(--pos);} .kpi.k-done .dot{background:var(--pos);}
.kpi.k-doing .num{color:var(--warn);} .kpi.k-doing .dot{background:var(--warn);}
.kpi.k-plan .num{color:var(--plan);} .kpi.k-plan .dot{background:var(--plan);}
.kpi.k-owner{background:linear-gradient(165deg,var(--clay-tint),var(--surface) 62%);border-color:color-mix(in srgb,var(--clay) 32%,var(--hair));}
.kpi.k-owner .num{color:var(--clay-deep);} .kpi.k-owner .dot{background:var(--clay);} .kpi.k-owner .lab{color:var(--clay-deep);}
.kpi.k-owner:after{content:"→";position:absolute;right:15px;top:14px;color:var(--clay);font-weight:800;}
.kpi-badge{position:absolute;right:13px;top:12px;display:none;align-items:center;gap:5px;font:800 10px/1 var(--mono);letter-spacing:.02em;color:#fff;background:var(--crit);padding:5px 8px;border-radius:7px;box-shadow:0 1px 3px rgba(168,53,44,.35);}
.kpi.k-plan{border-color:color-mix(in srgb,var(--crit) 18%,var(--hair));}
/* metric strip */
.mstrip{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px;}
.mchip{display:inline-flex;align-items:center;gap:7px;padding:6px 11px;border:1px solid var(--hair);border-radius:999px;background:var(--paper);font-size:11.5px;color:var(--ink-2);}
.mchip b{font:700 12px/1 var(--mono);color:var(--ink);}
.mchip.good b{color:var(--pos);}
.mchip.crit b{color:var(--crit);}
/* OWNER board */
.owner-board{background:linear-gradient(180deg,var(--clay-tint),transparent 120px);border:1px solid color-mix(in srgb,var(--clay) 26%,var(--hair));border-radius:16px;padding:20px 22px 22px;}
.owner-board .sec-head h2{color:var(--clay-deep);}
.owner-board .sec-head h2:before{content:"";display:inline-block;width:9px;height:9px;border-radius:50%;background:var(--clay);margin-right:9px;vertical-align:middle;}
.ocols{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;}
.ocol-h{display:flex;align-items:center;gap:8px;margin:2px 0 11px;font:700 12px/1 var(--font);color:var(--ink-2);}
.ocol-h .ct{font:700 11px/1 var(--mono);color:var(--clay-deep);background:var(--clay-tint);border:1px solid color-mix(in srgb,var(--clay) 26%,var(--hair));border-radius:999px;padding:2px 7px;}
.ostack{display:grid;gap:10px;}
.ocard{background:var(--surface);border:1px solid var(--hair);border-left:3px solid var(--clay);border-radius:11px;padding:13px 14px;box-shadow:0 1px 2px rgba(28,42,42,.04);}
.ocard .otop{display:flex;align-items:center;gap:7px;margin-bottom:7px;}
.otag{font:700 9.5px/1 var(--mono);letter-spacing:.05em;text-transform:uppercase;padding:3px 7px;border-radius:5px;}
.otag.block{background:var(--crit-bg);color:var(--crit);}
.otag.decide{background:var(--clay-tint);color:var(--clay-deep);}
.otag.ext{background:var(--plan-bg);color:var(--plan);}
.oblocks{font-size:10.5px;color:var(--ink-3);}
.ocard h3{margin:0 0 6px;font-size:13.5px;line-height:1.35;}
.ocard p{margin:0;color:var(--ink-2);font-size:12px;line-height:1.5;}
.ocard p+p{margin-top:5px;}
.ocard .ask{color:var(--ink);}
.ocard .why{color:var(--clay-deep);}
.ocard .opt{display:inline-block;margin-top:7px;font:600 11px/1.4 var(--mono);color:var(--ink-2);background:var(--paper-2);border-radius:6px;padding:4px 8px;}
code{font-family:var(--mono);font-size:11.5px;background:var(--paper-2);padding:1px 5px;border-radius:5px;color:var(--accent-deep);}
/* GRAPHS */
.graphs{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);gap:18px;}
.gcard{background:var(--surface);border:1px solid var(--hair);border-radius:14px;padding:18px 20px;box-shadow:0 1px 2px rgba(28,42,42,.05);}
.gcard h3{margin:0 0 4px;font-size:13.5px;}
.gcard .gsub{margin:0 0 15px;color:var(--ink-3);font-size:11.5px;}
.stackbar{display:flex;height:30px;border-radius:8px;overflow:hidden;border:1px solid var(--hair);}
.stackbar i{display:flex;align-items:center;justify-content:center;font:700 11px/1 var(--mono);color:#fff;min-width:0;transition:flex-basis .4s ease;}
.sb-done{background:var(--pos);} .sb-doing{background:var(--warn);} .sb-plan{background:var(--plan);}
.glegend{display:flex;flex-wrap:wrap;gap:14px;margin-top:12px;}
.glegend span{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;color:var(--ink-2);}
.glegend i{width:9px;height:9px;border-radius:3px;display:inline-block;}
.tracks{display:grid;gap:9px;}
.track{display:grid;grid-template-columns:96px 1fr 46px;align-items:center;gap:10px;}
.track .tname{font-size:11.5px;color:var(--ink-2);font-weight:650;}
.track .tbar{height:9px;border-radius:999px;background:var(--paper-3);overflow:hidden;}
.track .tbar i{display:block;height:100%;border-radius:999px;background:linear-gradient(90deg,var(--accent),var(--accent-deep));}
.track .tnum{font:700 11px/1 var(--mono);color:var(--ink-3);text-align:right;}
/* MAP */
.map-wrap{background:var(--surface);border:1px solid var(--hair);border-radius:16px;padding:16px;box-shadow:0 1px 2px rgba(28,42,42,.05);overflow-x:auto;}
.map-wrap svg{width:100%;min-width:760px;height:auto;display:block;}
.map-legend{display:flex;flex-wrap:wrap;gap:16px;margin:12px 4px 2px;}
.map-legend span{display:inline-flex;align-items:center;gap:7px;font-size:11.5px;color:var(--ink-2);}
.map-legend i{width:13px;height:13px;border-radius:4px;display:inline-block;border:1.5px solid;}
.ml-owner i{background:var(--clay-tint);border-color:var(--clay);}
.ml-crit i{background:#fff;border-color:var(--crit);}
.ml-gate i{background:var(--accent-tint);border-color:var(--accent);}
/* DESIGN GUIDE */
.dg-shell{background:linear-gradient(180deg,var(--surface),var(--paper) 100%);border:1px solid var(--hair);border-radius:16px;padding:18px;box-shadow:0 1px 2px rgba(28,42,42,.05);}
.dg-link{display:inline-flex;align-items:center;justify-content:center;min-height:34px;padding:8px 12px;border:1px solid color-mix(in srgb,var(--accent) 34%,var(--hair));border-radius:999px;background:var(--accent-tint);color:var(--accent-deep);font-size:12px;font-weight:750;text-decoration:none;white-space:nowrap;}
.dg-note{margin:0 0 14px;padding:12px 13px;border:1px dashed color-mix(in srgb,var(--clay) 38%,var(--hair));border-radius:11px;background:var(--clay-tint);color:var(--clay-deep);font-size:12.5px;line-height:1.55;}
.dg-principles{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:0 0 16px;}
.dg-principles div{background:var(--surface);border:1px solid var(--hair);border-radius:12px;padding:12px 13px;}
.dg-principles b{display:block;margin-bottom:4px;font-size:12.5px;color:var(--ink);}
.dg-principles span{display:block;color:var(--ink-2);font-size:11.5px;line-height:1.5;}
.dg-table-wrap{overflow-x:auto;margin:0 0 16px;}
.dg-table{min-width:760px;margin:0;}
.dg-gallery{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;}
.dg-card{min-width:0;background:var(--surface);border:1px solid var(--hair);border-radius:13px;overflow:hidden;box-shadow:0 1px 2px rgba(28,42,42,.04);}
.dg-card img{display:block;width:100%;aspect-ratio:1672/941;object-fit:cover;background:var(--paper-2);border-bottom:1px solid var(--hair);}
.dg-card-body{padding:12px 13px 13px;}
.dg-card b{display:block;margin-bottom:4px;font-size:13px;color:var(--ink);}
.dg-card span{display:block;color:var(--ink-2);font-size:11.5px;line-height:1.45;}
.dg-card code{display:inline-block;margin-top:8px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
/* BOARD */
.filterbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:0 0 16px;}
.filterbar .fl{font:700 10.5px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);margin-right:4px;}
.fbtn{appearance:none;border:1px solid var(--hair);background:var(--surface);color:var(--ink-2);border-radius:999px;padding:6px 13px;font:650 12px/1 var(--font);cursor:pointer;display:inline-flex;align-items:center;gap:6px;}
.fbtn .fn{font:700 10.5px/1 var(--mono);color:var(--ink-3);}
.fbtn:hover{background:var(--accent-tint);color:var(--accent-deep);border-color:color-mix(in srgb,var(--accent) 30%,var(--hair));}
.fbtn[aria-pressed="true"]{background:var(--ink);color:#fff;border-color:var(--ink);}
.fbtn[aria-pressed="true"] .fn{color:rgba(255,255,255,.7);}
.fbtn.f-owner[aria-pressed="true"]{background:var(--clay-deep);border-color:var(--clay-deep);}
.track-group{margin-top:22px;}
.track-group:first-of-type{margin-top:0;}
.tg-head{display:flex;align-items:baseline;gap:10px;margin:0 0 11px;}
.tg-head h3{margin:0;font-size:14.5px;letter-spacing:-.01em;}
.tg-head .tg-note{color:var(--ink-3);font-size:11.5px;}
.scards{display:grid;gap:9px;}
.scard{background:var(--surface);border:1px solid var(--hair);border-radius:12px;overflow:hidden;box-shadow:0 1px 2px rgba(28,42,42,.04);}
.scard[data-owner="1"]{border-left:3px solid var(--clay);}
.scard.hide{display:none;}
.scard-head{width:100%;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:13px;padding:13px 15px;background:none;border:0;text-align:left;cursor:pointer;font-family:inherit;}
.scard-head:hover{background:var(--paper);}
.scard-head:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;}
.chip{display:inline-flex;align-items:center;gap:6px;white-space:nowrap;font:700 10.5px/1 var(--mono);letter-spacing:.02em;padding:5px 9px;border-radius:6px;}
.chip:before{content:"";width:6px;height:6px;border-radius:50%;}
.c-done{background:var(--pos-bg);color:var(--pos);} .c-done:before{background:var(--pos);}
.c-doing{background:var(--warn-bg);color:var(--warn);} .c-doing:before{background:var(--warn);}
.c-plan{background:var(--plan-bg);color:var(--plan);} .c-plan:before{background:var(--plan);}
.c-block{background:var(--crit-bg);color:var(--crit);} .c-block:before{background:var(--crit);}
.scard-mid{min-width:0;}
.scard-title{display:block;font-size:13.5px;font-weight:700;color:var(--ink);line-height:1.4;}
.scard-title .critbadge{display:inline-block;margin-left:7px;font:800 9px/1 var(--mono);letter-spacing:.06em;color:var(--crit);background:var(--crit-bg);padding:3px 6px;border-radius:5px;vertical-align:middle;}
.scard-title .ownbadge{display:inline-block;margin-left:7px;font:800 9px/1 var(--mono);letter-spacing:.06em;color:var(--clay-deep);background:var(--clay-tint);padding:3px 6px;border-radius:5px;vertical-align:middle;}
.scard-sum{display:block;margin-top:3px;color:var(--ink-2);font-size:12px;line-height:1.45;}
.caret{flex:none;width:9px;height:9px;border-right:2px solid var(--ink-3);border-bottom:2px solid var(--ink-3);transform:rotate(-45deg);transition:transform .18s ease;margin-right:3px;}
.scard.open .caret{transform:rotate(45deg);}
.scard-body{display:none;padding:2px 16px 16px 16px;border-top:1px solid var(--hair);}
.scard.open .scard-body{display:block;}
.kv{display:grid;grid-template-columns:64px 1fr;gap:12px;padding:11px 0;border-bottom:1px dashed var(--hair);}
.kv:last-child{border-bottom:0;}
.kv b{font:700 11px/1.5 var(--mono);color:var(--accent-deep);text-transform:uppercase;letter-spacing:.04em;}
.kv.k-warn b{color:var(--clay-deep);}
.kv p{margin:0;color:var(--ink-2);font-size:12.5px;line-height:1.6;}
.kv p b{font:inherit;color:var(--ink);text-transform:none;letter-spacing:0;}
/* DEEP (folded reference) + legacy */
.deep-note{margin:0 0 4px;color:var(--ink-3);font-size:12px;}
.fold{margin-top:14px;border:1px solid var(--hair);border-radius:14px;background:var(--surface);overflow:hidden;}
.fold>summary{list-style:none;cursor:pointer;padding:15px 18px;display:flex;align-items:center;gap:12px;font-weight:700;font-size:14px;}
.fold>summary::-webkit-details-marker{display:none;}
.fold>summary:hover{background:var(--paper);}
.fold>summary .fcaret{width:9px;height:9px;border-right:2px solid var(--ink-3);border-bottom:2px solid var(--ink-3);transform:rotate(-45deg);transition:transform .18s ease;}
.fold[open]>summary .fcaret{transform:rotate(45deg);}
.fold>summary .fmeta{margin-left:auto;font:650 11px/1 var(--mono);color:var(--ink-3);}
.fold-inner{padding:6px 18px 20px;border-top:1px solid var(--hair);}
.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;}
.roadmap{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:11px;margin-top:6px;}
.phase-card{min-width:0;background:var(--paper);border:1px solid var(--hair);border-radius:12px;padding:14px;}
.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:8px;margin-bottom:9px;}
.phase-id{font:760 12px/1 var(--mono);color:var(--accent-deep);}
.phase-card h3{margin:0;font-size:14px;line-height:1.35;}
.phase-card p{margin:7px 0 0;color:var(--ink-2);font-size:11.5px;}
.phase-list{display:grid;gap:5px;margin:11px 0 0;padding:0;list-style:none;}
.phase-list li{color:var(--ink-2);font-size:11.5px;line-height:1.45;}
.phase-list li b{color:var(--ink);}
.tag{display:inline-flex;align-items:center;gap:6px;padding:3px 9px;border-radius:999px;font-size:11px;font-weight:680;}
.tag:before{content:"";width:7px;height:7px;border-radius:50%;}
.tag.doing{color:var(--warn);background:var(--warn-bg);} .tag.doing:before{background:var(--warn);}
.tag.info{color:var(--plan);background:var(--plan-bg);} .tag.info:before{background:var(--plan);}
.source-note{margin-top:13px;padding:13px 15px;border:1px solid var(--hair);border-radius:11px;background:var(--paper-2);color:var(--ink-2);font-size:12.5px;}
.tabs{border:1px solid var(--hair);border-radius:12px;background:var(--surface);overflow:hidden;margin-top:10px;}
.tabbar{display:flex;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:12px 15px;font:700 12.5px/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:18px;} .tab-panel[hidden]{display:none;}
.detail-head{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:16px;align-items:start;margin-bottom:14px;}
.detail-head h3{margin:0;font-size:16px;} .detail-head p{margin:5px 0 0;color:var(--ink-2);font-size:12.5px;}
.mini-metrics{display:flex;flex-wrap:wrap;gap:7px;justify-content:flex-end;}
.mini-metrics span{display:inline-flex;align-items:center;min-height:26px;padding:4px 9px;border:1px solid var(--hair);border-radius:999px;background:var(--paper);color:var(--ink-2);font:650 11px/1 var(--mono);}
.task-list{display:grid;border:1px solid var(--hair);border-radius:11px;overflow:hidden;background:var(--hair);gap:1px;}
.task-row{display:grid;grid-template-columns:108px minmax(200px,1fr) minmax(170px,.8fr) minmax(180px,.9fr);background:var(--surface);}
.task-row.r3{grid-template-columns:108px minmax(220px,1.4fr) minmax(200px,1fr);}
.task-row>div{padding:12px 13px;min-width:0;}
.task-row b{display:block;margin-bottom:4px;font-size:12.5px;color:var(--ink);}
.task-row p{margin:0;color:var(--ink-2);font-size:11.5px;line-height:1.45;}
.task-status{display:inline-flex;align-items:center;justify-content:center;min-width:72px;min-height:26px;padding:4px 8px;border-radius:999px;font:760 11px/1 var(--mono);}
.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(--plan-bg);color:var(--plan);}
.detail-blocks{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:11px;margin-top:13px;}
.detail-block{border:1px solid var(--hair);border-radius:11px;padding:13px;background:var(--paper);}
.detail-block h4{margin:0 0 7px;font-size:12.5px;} .detail-block ul{margin:0;padding-left:17px;color:var(--ink-2);font-size:11.5px;line-height:1.55;}
.detail-block li+li{margin-top:4px;}
.tab-subhead{margin:17px 0 9px;} .tab-subhead h4{margin:0;font-size:13.5px;} .tab-subhead p{margin:4px 0 0;color:var(--ink-3);font-size:11.5px;}
.commands{display:grid;gap:9px;}
pre{margin:0;overflow:auto;padding:13px;border-radius:8px;background:#1E2724;color:#F6F0EC;font:11.5px/1.6 var(--mono);}
table{width:100%;border-collapse:collapse;background:var(--surface);border:1px solid var(--hair);border-radius:11px;overflow:hidden;}
th,td{padding:10px 12px;border-bottom:1px solid var(--hair);text-align:left;vertical-align:top;font-size:12px;}
th{color:var(--ink-3);font-weight:700;background:var(--paper-2);}
tr:last-child td{border-bottom:0;}
.wide{grid-column:1/-1;}
@media (max-width:1000px){
.pulse{grid-template-columns:1fr;}
.graphs{grid-template-columns:1fr;}
.ocols{grid-template-columns:1fr;}
.dg-principles,.dg-gallery{grid-template-columns:repeat(2,minmax(0,1fr));}
}
@media (max-width:760px){
header{padding:0 16px;} main{width:calc(100% - 28px);}
.kpis{grid-template-columns:1fr 1fr;}
.grid,.roadmap,.detail-head,.detail-blocks{grid-template-columns:1fr;}
.dg-principles,.dg-gallery{grid-template-columns:1fr;}
.task-row,.task-row.r3{grid-template-columns:1fr;}
.task-row>div{border-bottom:1px solid var(--hair);} .task-row>div:last-child{border-bottom:0;}
.brand small{display:none;}
}
@media (max-width:460px){ .kpis{grid-template-columns:1fr;} h1{font-size:22px;} .stamp{display:none;} }
@media (prefers-reduced-motion:reduce){ *{transition:none!important;} }
</style>
</head>
<body>
<header>
<div class="brand"><span class="mark" aria-hidden="true"></span><span>Vignette 커맨드센터<small>팀장 리뷰 · dev status</small></span></div>
<div class="head-right">
<a class="owner-pill" href="#owner"><span class="d" aria-hidden="true"></span>내 차례 <b id="pill-owner">0</b></a>
<span class="stamp">2026-06-27 KST</span>
</div>
</header>
<main>
<!-- ░░ PULSE ░░ -->
<section class="pulse">
<div class="pulse-main">
<span class="eyebrow">project pulse</span>
<h1>지금 위치: <em>Phase 0 마감 직전</em> → Phase 1 진입 대기.<br><em>학습자 개인화 대시보드</em><em>교수자 검토 상태</em>는 1차 구현·검증 완료.</h1>
<p class="pulse-state">코어(저항·반응 조절, 상태머신, RAG, 이론모드, 가드레일, 평가 JSON Schema)는 <b>잘 정렬</b>됐고, 이번 점검에서 학습자 홈·교수자 리뷰·공개/Tailnet 런타임 스모크도 녹색이다. 막힌 건 두 종류 — <b>실제 계정·외부 DNS·기관 결정이 있어야 풀리는 게이트</b>와, <b>임상 루브릭·콘텐츠·운영 증거처럼 아직 남은 제품 갭</b>이다. 이 판은 그 둘을 분리해서 보여준다.</p>
<div class="phase-rail" id="phase-rail" aria-label="Phase 진행 추정"></div>
<div class="mstrip" aria-label="검증 지표">
<span class="mchip good">Full E2E <b>113</b> 통과</span>
<span class="mchip good">Backend focused <b>12</b> 통과</span>
<span class="mchip good">Layout 시각게이트 <b>7/7</b> accept</span>
<span class="mchip">Tailnet <b>alpaca-home</b> OK</span>
<span class="mchip good">Public API <b>prod OK</b></span>
<span class="mchip crit">Compose env <b>BLOCK</b></span>
<span class="mchip crit">원천문서 갭 <b>critical 3</b></span>
<span class="mchip">Decision <b>P0 조건부</b></span>
</div>
</div>
<div class="kpis" aria-label="상태 요약">
<a class="kpi k-done" href="#board"><span class="lab"><span class="dot"></span>완료</span><span class="num" id="kpi-done">0</span><span class="hint" id="kpi-pct">전체의 0%</span></a>
<a class="kpi k-doing" href="#board"><span class="lab"><span class="dot"></span>진행 중</span><span class="num" id="kpi-doing">0</span><span class="hint">검증/배선 진행</span></a>
<a class="kpi k-plan" href="#board"><span class="kpi-badge" id="kpi-critbadge" aria-hidden="true"></span><span class="lab"><span class="dot"></span>계획·블로커</span><span class="num" id="kpi-plan">0</span><span class="hint" id="kpi-crit">납품 필수 포함</span></a>
<a class="kpi k-owner" href="#owner"><span class="lab"><span class="dot"></span>내 차례</span><span class="num" id="kpi-owner">0</span><span class="hint">결정·실증·외부조율</span></a>
</div>
</section>
<!-- ░░ OWNER BOARD ░░ -->
<section class="block owner-board" id="owner">
<div class="sec-head">
<div>
<h2>당신 차례 — 이걸 풀어주면 진행이 열린다</h2>
<p class="sub">람다가 단독으로 못 정하거나(월권), 실제 계정·하드웨어·외부 합의가 필요한 항목. 각 카드는 <b>무엇을 막는지 · 무엇이 필요한지 · 왜 당신인지</b>를 담았다. 팀장이 우선순위를 골라 하나씩 풀어주면 된다.</p>
</div>
</div>
<div class="ocols">
<div class="ocol">
<div class="ocol-h">🚧 GO를 막는 블로커 <span class="ct" id="ct-block">0</span></div>
<div class="ostack" data-owner-col="block">
<article class="ocard" data-owner="1"><div class="otop"><span class="otag block">블로커</span><span class="oblocks">Phase 0 마감 · P1 진입</span></div><h3>공개 Google OAuth 실제 <code>/turn</code> 증명</h3><p class="ask">필요: 로그인 가능한 계정으로 storageState 캡처 → <code>public-auth-turn</code> E2E 1회 통과.</p><p class="why">왜 당신인가: 실제 Google 계정 인터랙티브 로그인이라 자동화 불가. (owner 지시로 현재 보류 상태)</p><span class="opt">증거: public-auth.json + public /turn 성공 로그</span></article>
<article class="ocard" data-owner="1"><div class="otop"><span class="otag block">게이트</span><span class="oblocks">Phase 3 파일럿 · 데이터</span></div><h3>한신대 데이터/SSO 거버넌스</h3><p class="ask">필요: SSO claim 스키마, 추가 축어록 수급, 미성년 원본 활용동의, 개인정보 처리방침에 대한 한신대/데이터 steward의 written evidence.</p><p class="why">왜 당신인가: 기관 공문·협의 영역. 로컬 문서 골격은 준비됨(<code>hanshin-data-governance-gate.md</code>).</p></article>
</div>
</div>
<div class="ocol">
<div class="ocol-h">🧭 소유자 결정 (DECIDE) <span class="ct" id="ct-decide">0</span></div>
<div class="ostack" data-owner-col="decide">
<article class="ocard" data-owner="1"><div class="otop"><span class="otag decide">결정</span><span class="oblocks">아키텍처·일정</span></div><h3>백엔드 언어 (Python 유지 vs Node/Spring)</h3><p class="ask">PDF는 Node.js 우선(팀 보유기술·5개월 일정)/Spring 대안. 현재 Python/FastAPI — 재작성은 일정·인력 리스크.</p><p class="why">영향: 팀 역량·유지보수 vs 재작성 비용. 람다 단독 결정 금지.</p></article>
<article class="ocard" data-owner="1"><div class="otop"><span class="otag decide">결정</span><span class="oblocks">Phase 3 측정</span></div><h3>평가설계 (H1: 실험/통제군·3척도·κ/ICC)</h3><p class="ask">doc4가 20명 실험/통제군·단회기 50분·3척도 pre-post를 명시. κ/ICC·환각률 목표는 doc4 미명시 — 확정 필요.</p><p class="why">영향: 평가 타당도, Phase 3 측정력. 평가설계는 소유자 결정.</p></article>
<article class="ocard" data-owner="1"><div class="otop"><span class="otag decide">결정</span><span class="oblocks">음성 아키텍처</span></div><h3>s2s 2차 PoC 채택 여부</h3><p class="ask">캐스케이드 1차는 확정. <code>voice-s2s-poc.md</code> 기준표로 비교 후 유지/폐기.</p><p class="opt">캐스케이드만 / s2s 2차 PoC 유지</p></article>
<article class="ocard" data-owner="1"><div class="otop"><span class="otag decide">결정</span><span class="oblocks">페르소나 음성</span></div><h3>서비스용 음성 모델·권리 정책</h3><p class="ask">Higgs급 무참조 synthetic P1 샘플은 생성 완료, dev-only sample TTS provider 연결 완료. 실존 인물/성우 ref 사용 금지 확정. 서비스 배포 전 상업 라이선스·동의 ref·대체 provider 중 결정 필요.</p><p class="opt">Higgs 라이선스 / 동의 ref / 대체 TTS</p></article>
<article class="ocard" data-owner="1"><div class="otop"><span class="otag decide">결정</span><span class="oblocks">데이터주권·레이턴시</span></div><h3>fast-loop 외부 API 허용 여부</h3><p class="ask">로컬 상주 분류기 기본 + 외부 Haiku/Solar 폴백 허용 여부.</p><p class="opt">로컬 전용 / 외부 폴백 허용</p></article>
<article class="ocard" data-owner="1"><div class="otop"><span class="otag decide">결정</span><span class="oblocks">동의서·데이터셋</span></div><h3>재귀학습 fine-tuning 범위</h3><p class="ask">1차 few-shot 자동갱신만 vs fine-tuning 포함.</p><p class="opt">few-shot only / fine-tuning 포함</p></article>
<article class="ocard" data-owner="1"><div class="otop"><span class="otag decide">결정</span><span class="oblocks">안전 정책</span></div><h3>자유연습 기본값</h3><p class="ask">수정안: 기본 ON + 피드백 권장 배지 + 교수자 토글.</p><p class="opt">기본 ON / 교수자 승인 후 ON</p></article>
</div>
</div>
<div class="ocol">
<div class="ocol-h">🤝 외부·임상팀 조율 <span class="ct" id="ct-ext">0</span></div>
<div class="ostack" data-owner-col="ext">
<article class="ocard" data-owner="1"><div class="otop"><span class="otag ext">조율</span><span class="oblocks">위기게이트·평가·이론</span></div><h3>임상팀 콘텐츠 (구훈정·어유경)</h3><p class="ask">필요: C1 워크시트 항목·루브릭, CBT 이론별 단계 프롬프트 체인, 위기 스크립트·생명유지서약 문안, H2 평가 골든셋.</p><p class="why">왜 당신인가: doc3 R&R상 콘텐츠는 임상팀 산출물. 코드는 '편집 가능 구조'만 선제 구축 → 조율·수급 필요.</p></article>
<article class="ocard" data-owner="1"><div class="otop"><span class="otag ext">수급</span><span class="oblocks">평가 타당도·다양성</span></div><h3>추가 축어록 수급 규모/일정</h3><p class="ask">n=1 시드 한계 해소를 위한 추가 사례 수급 규모·일정 확정.</p><p class="why">영향: 평가 타당도, 페르소나 다양성, Phase 3 측정력.</p></article>
<article class="ocard" data-owner="1"><div class="otop"><span class="otag ext">거버넌스</span><span class="oblocks">계약·일정</span></div><h3>스택 정합성·저작권 등재 (L1)</h3><p class="ask">doc4 신청서 스택(Spring/Node·TimescaleDB) vs 실제 FastAPI/Python 불일치. 20주 단기일정·9월 저작권 등재 압박.</p><p class="why">권고: 스택 정합 또는 변경사유 거버넌스 기록.</p></article>
</div>
</div>
</div>
</section>
<!-- ░░ GRAPHS ░░ -->
<section class="block" id="progress">
<div class="sec-head">
<div>
<h2>진행 현황</h2>
<p class="sub">아래 "상태 보드"의 작업 카드에서 자동 집계. 완료/진행/계획 비율과 트랙별 완료도. (Phase 진행률은 단계별 증거 기반 추정치 — 정밀 수치 아님)</p>
</div>
</div>
<div class="graphs">
<div class="gcard">
<h3>상태 분포</h3>
<p class="gsub">추적 작업 <b id="g-total">0</b>건 기준</p>
<div class="stackbar" id="stackbar" aria-label="상태 분포 막대"></div>
<div class="glegend">
<span><i style="background:var(--pos)"></i>완료 <b id="lg-done" style="font-family:var(--mono)">0</b></span>
<span><i style="background:var(--warn)"></i>진행 <b id="lg-doing" style="font-family:var(--mono)">0</b></span>
<span><i style="background:var(--plan)"></i>계획·블로커 <b id="lg-plan" style="font-family:var(--mono)">0</b></span>
</div>
</div>
<div class="gcard">
<h3>트랙별 완료도</h3>
<p class="gsub">카테고리별 완료 카드 / 전체 카드</p>
<div class="tracks" id="tracks"></div>
</div>
</div>
</section>
<!-- ░░ RELATIONSHIP MAP ░░ -->
<section class="block" id="map">
<div class="sec-head">
<div>
<h2>관계도 — 무엇이 무엇을 막는가</h2>
<p class="sub">왼쪽 <b style="color:var(--clay-deep)">내 차례(clay)</b> 노드를 풀면 가운데 게이트/작업이 열리고, 오른쪽 <b style="color:var(--crit)">납품·마일스톤</b>으로 이어진다. 화살표를 따라가면 "내가 지금 풀어야 임계경로가 뚫리는 것"이 보인다.</p>
</div>
</div>
<div class="map-wrap">
<svg viewBox="0 0 1040 560" role="img" aria-label="의존 관계도: 내 차례 노드가 게이트와 납품을 막는 구조">
<defs>
<marker id="arw" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#9aa6a2"/></marker>
<marker id="arwc" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#c79a86"/></marker>
</defs>
<!-- column labels -->
<text x="140" y="24" text-anchor="middle" font-family="JetBrains Mono" font-size="12" font-weight="700" fill="#8F5641">내 차례 (OWNER)</text>
<text x="520" y="24" text-anchor="middle" font-family="JetBrains Mono" font-size="12" font-weight="700" fill="#2B5249">게이트 · 작업</text>
<text x="900" y="24" text-anchor="middle" font-family="JetBrains Mono" font-size="12" font-weight="700" fill="#A8352C">납품 · 마일스톤</text>
<!-- edges (drawn first, under nodes) -->
<g fill="none" stroke="#c79a86" stroke-width="1.8" marker-end="url(#arwc)">
<path d="M240 64 C330 64 330 64 420 64"/>
<path d="M240 154 C330 160 340 168 420 168" stroke-dasharray="5 4"/>
<path d="M240 270 C330 270 330 270 420 270"/>
<path d="M240 270 C330 300 340 352 420 360"/>
<path d="M240 380 C330 410 340 452 420 460"/>
<path d="M240 466 C420 540 660 548 802 482" stroke-dasharray="5 4"/>
</g>
<g fill="none" stroke="#9aa6a2" stroke-width="1.8" marker-end="url(#arw)">
<path d="M620 64 C710 70 720 96 802 110"/>
<path d="M620 168 C710 150 720 124 802 116"/>
<path d="M620 270 C710 285 720 300 802 315"/>
<path d="M620 360 C710 345 720 330 802 321"/>
<path d="M620 460 C710 460 720 460 802 460"/>
<path d="M900 343 L900 432" stroke-dasharray="5 4"/>
</g>
<!-- owner nodes (clay) -->
<g font-family="Pretendard Variable" font-size="13" font-weight="700">
<g><rect x="40" y="40" width="200" height="48" rx="10" fill="#F6EBE4" stroke="#B0735C" stroke-width="1.6"/><text x="140" y="69" text-anchor="middle" fill="#8F5641">OAuth /turn 증명</text></g>
<g><rect x="40" y="130" width="200" height="48" rx="10" fill="#F6EBE4" stroke="#B0735C" stroke-width="1.6"/><text x="140" y="159" text-anchor="middle" fill="#8F5641">백엔드 언어 결정</text></g>
<g><rect x="40" y="246" width="200" height="48" rx="10" fill="#F6EBE4" stroke="#B0735C" stroke-width="1.6"/><text x="140" y="275" text-anchor="middle" fill="#8F5641">임상팀 콘텐츠</text></g>
<g><rect x="40" y="356" width="200" height="48" rx="10" fill="#F6EBE4" stroke="#B0735C" stroke-width="1.6"/><text x="140" y="385" text-anchor="middle" fill="#8F5641">평가설계 (H1)</text></g>
<g><rect x="40" y="442" width="200" height="48" rx="10" fill="#F6EBE4" stroke="#B0735C" stroke-width="1.6"/><text x="140" y="471" text-anchor="middle" fill="#8F5641">한신대 거버넌스</text></g>
</g>
<!-- mid nodes -->
<g font-family="Pretendard Variable" font-size="13" font-weight="700">
<g><rect x="420" y="40" width="200" height="48" rx="10" fill="#EBF2F0" stroke="#3E7A6E" stroke-width="1.6"/><text x="520" y="69" text-anchor="middle" fill="#2B5249">Phase 0 마감</text></g>
<g><rect x="420" y="144" width="200" height="48" rx="10" fill="#EBF2F0" stroke="#3E7A6E" stroke-width="1.6"/><text x="520" y="173" text-anchor="middle" fill="#2B5249">Phase 1 MVP</text></g>
<g><rect x="420" y="246" width="200" height="48" rx="10" fill="#fff" stroke="#A8352C" stroke-width="1.8"/><text x="520" y="268" text-anchor="middle" fill="#A8352C">위기게이트 109</text><text x="520" y="284" text-anchor="middle" font-size="10" font-weight="600" fill="#A8352C">1차 구현 · 검증</text></g>
<g><rect x="420" y="336" width="200" height="48" rx="10" fill="#fff" stroke="#A8352C" stroke-width="1.8"/><text x="520" y="358" text-anchor="middle" fill="#A8352C">교수자 성장 UI</text><text x="520" y="374" text-anchor="middle" font-size="10" font-weight="600" fill="#A8352C">1차 구현 · 검증</text></g>
<g><rect x="420" y="436" width="200" height="48" rx="10" fill="#EBF2F0" stroke="#3E7A6E" stroke-width="1.6"/><text x="520" y="465" text-anchor="middle" fill="#2B5249">평가 KPI (H1)</text></g>
</g>
<!-- right nodes -->
<g font-family="Pretendard Variable" font-size="13" font-weight="700">
<g><rect x="800" y="88" width="200" height="48" rx="10" fill="#EBF2F0" stroke="#3E7A6E" stroke-width="1.6"/><text x="900" y="117" text-anchor="middle" fill="#2B5249">Phase 1 GO</text></g>
<g><rect x="800" y="291" width="200" height="52" rx="10" fill="#F9E7E4" stroke="#A8352C" stroke-width="2"/><text x="900" y="313" text-anchor="middle" fill="#A8352C">납품 필수조건</text><text x="900" y="329" text-anchor="middle" font-size="10" font-weight="600" fill="#A8352C">법적 생존선</text></g>
<g><rect x="800" y="436" width="200" height="48" rx="10" fill="#E8F0F6" stroke="#3C7299" stroke-width="1.6"/><text x="900" y="465" text-anchor="middle" fill="#3C7299">Phase 3 파일럿</text></g>
</g>
</svg>
<div class="map-legend">
<span class="ml-owner"><i></i>내 차례 — 윤찬이 풀어야 열림</span>
<span class="ml-crit"><i></i>납품 필수 — 미구현 임계작업</span>
<span class="ml-gate"><i></i>게이트·마일스톤</span>
<span style="color:var(--ink-3);font-size:11px">··· 점선 = 약한/아키텍처 의존</span>
</div>
</div>
</section>
<!-- ░░ DESIGN DIRECTION ░░ -->
<section class="block" id="design-direction">
<div class="sec-head">
<div>
<h2>디자인 시안 지침 — 기능 추출 기반 이미지 생성</h2>
<p class="sub">2026-06-27 기준 주요 페이지 기능을 추출해 PC·태블릿·모바일 반응형 시안 7장과 온화한 이미지 요소 보드 2장을 만들었다. 결과물은 전부 PNG 래스터 이미지이며, 구현 확정안이 아니라 레이아웃·톤·이미지 요소 판단 기준이다.</p>
</div>
<a class="dg-link" href="design-concepts/prompts/vignette-design-prompts-2026-06-27.md">프롬프트 문서</a>
</div>
<div class="dg-shell">
<p class="dg-note">적용 원칙: 생성 이미지 안의 문구는 참고하지 말고 실제 카피는 코드/기획서 기준으로 다시 쓴다. 세션 아바타는 기존 서연/P1 아바타 핸드오프와 `rasterArtSet` 지침을 우선한다. 교수·관리자 내비게이션은 현재 `AppShell`·`Sidebar` 구조를 유지하고, 배경 일러스트·패턴은 빈 상태·히어로·세션 주변 질감에 소량만 쓴다.</p>
<p class="dg-note">1차 적용(2026-06-27): 온화한 이미지 요소 보드는 구현 참고용으로만 두고, 앱 배경에는 전용 래스터 텍스처 `apps/web/public/design-elements/clinical-paper-ambient.png`를 연결했다. 보드·타일셋·누끼 시트 형태 이미지는 직접 UI 배경으로 쓰지 않는다.</p>
<p class="dg-note">2차 적용(2026-06-27): 생성된 관리자/설정/리뷰 시안 기준으로 admin 역할의 상단바·사이드바를 어두운 운영 콘솔 크롬으로 전환했고, 관리자 사용자 관리 행을 조밀한 운영 테이블 밀도로 낮췄다. 회기 리뷰는 헤더·인사이트 카드·피드백 스테이지에 따뜻한 래스터 질감과 낮은 채도 틴트를 적용했다.</p>
<p class="dg-note">3차 적용(2026-06-27): 상담 세션 시작/진행 화면을 시안 기준의 어두운 상담 스테이지로 재정렬했다. 시작 전 화면은 내담자 무대·브리핑·진행 초점 3영역으로 유지하고, 진행 중 화면은 어두운 캔버스 위에 중앙 스테이지와 자막/컨트롤을 배치한다.</p>
<p class="dg-note">4차 적용(2026-06-27): 교수 콘솔은 시안의 triage 콘솔 구조에 맞춰 KPI를 전폭 6칸으로 재배치하고, 종료 회기 검토 큐와 최근 회기 기록을 첫 화면 2열 핵심 영역으로 올렸다. 성장 추적·페르소나 저작·검수 보조 큐는 하단 보조 업무로 낮췄고, 교수자 사이드바는 좁은 teal rail로 전환했다.</p>
<p class="dg-note">5차 적용(2026-06-27): 회기 리뷰는 시안의 좌·중·우 작업대 구조에 맞춰 요약/액션 레일, 감정·흐름 중앙 영역, 우측 인사이트 패널로 재배치했다. 모바일에서는 트랜스크립트를 워크시트보다 먼저 노출하고, 사례개념화 워크시트는 제한 높이의 보조 패널로 낮춰 긴 문서처럼 보이지 않게 했다.</p>
<p class="dg-note">6차 적용(2026-06-27): 설정 화면은 글로벌 셸을 숨기고 설정 전용 좌측 레일을 주 내비로 승격했다. 계정, AI 운영, 테마, 음성, 알림 카드를 시안의 차분한 설정 워크스페이스 흐름에 맞춰 단일 컬럼으로 재배치했고, 모바일은 상단 고정 섹션 탭으로 모든 항목이 잘리지 않게 정리했다.</p>
<p class="dg-note">7차 적용(2026-06-27): 로그인 화면은 별도 생성한 래스터 상담실 배경(<code>login-counseling-room.png</code>)을 왼쪽 브랜드 패널에 적용해 시안의 실제 상담실 분위기를 반영했다. 학습자 홈은 모바일 grid 행 계산 때문에 생기던 리스트-상세 간 큰 공백을 제거하고, 회기 준비 보드가 한 흐름으로 이어지게 정리했다.</p>
<p class="dg-note">8차 적용(2026-06-27): 다크 테마가 라이트 primitive 토큰과 역할 accent에 덮여 흰 섹션이 섞이던 문제를 수정했다. 앱 부트 시 테마를 먼저 적용하고, 다크 모드에서 <code>paper/surface/hair/ink</code> primitive까지 스왑한다. 상담 세션 진행 화면은 테마 토글과 무관하게 어두운 콘솔 패널로 고정했고, 드래그형 종료 제스처는 명시적 <code>회기 종료</code> 버튼 + 확인 다이얼로그로 교체했다. 회기 리뷰도 동일한 다크 카드/축어록/워크시트 재질로 보정했다.</p>
<p class="dg-note">9차 적용(2026-06-27): 관리자 콘솔 IA를 <code>/admin</code> 운영 홈과 <code>/admin/users</code> 사용자 관리, <code>/admin/access</code> 접근 권한, <code>/admin/tickets</code> 운영 티켓 메뉴로 분리했다. 운영 홈은 실제 API가 있는 교육 진행·온라인 사용자·헬스·AI 비용·서비스 리소스를 우선 표시하고, 티켓/업타임 이력은 <code>app.support_ticket</code>·<code>app.admin_health_event</code> 기반 1차 저장 모델로 연결했다. 세분 그룹 권한 enforcement는 아직 정책 화면이며 가짜 권한 효과를 표시하지 않는다.</p>
<p class="dg-note">17차 적용(2026-06-28): 운영 홈의 티켓 요약은 <code>resolved</code>/<code>closed</code> 이력을 진행 중 위험으로 세지 않는다. <code>high_priority_count</code>와 미니 티켓 목록은 미해결 티켓 기준이며, E2E 운영 티켓 테스트는 제목·본문에 테스트임을 명시하고 실패 시에도 cleanup patch로 해결 처리한다.</p>
<p class="dg-note">19차 적용(2026-06-28): <code>/admin/tickets</code>는 현재 범위를 미해결 큐 목록과 상태 변경으로 좁혔다. 정적 분류/인사이트 탭은 제거하고, 카드에 카테고리·신고자·경로·접수/갱신 시각을 표시한다. 해결된 항목은 최근 해결 이력으로 내려간다. 필터·카테고리 큐·Claude Recipe headless 자동 처리·이슈 등록은 백로그 <code>운영 티켓 자동 분류·처리 2차</code>로 남겼다.</p>
<p class="dg-note">20차 적용(2026-06-28): 신규 Google/SAML 사용자는 <code>account_status=pending</code>으로 시작하고 승인 전에는 <code>/pending</code> 안내 화면만 본다. <code>yunchan@twentyoz.kr</code>는 슈퍼 관리자 allowlist로 admin+approved를 받으며, <code>/admin/users</code>는 가입 승인 탭에서 pending 계정을 승인 또는 보류 처리한다.</p>
<p class="dg-note">21차 적용(2026-06-28): 관리자 페이지 진입권을 기본 역할과 분리해 <code>app_user.admin_access</code>로 저장한다. <code>AUTH_SUPER_ADMIN_EMAILS</code> 기본값은 <code>yunchan@twentyoz.kr</code>, <code>hoonjungkoo@hs.ac.kr</code>이며, 슈퍼 관리자는 학습자·교수자·관리자 공간 전환과 관리자 권한 부여/회수를 할 수 있다. 학생·교수 계정도 <code>admin_access=true</code>면 우측 상단 관리자 진입이 노출된다. 구성 슈퍼 관리자의 권한 회수와 계정 비활성화는 차단한다.</p>
<p class="dg-note">25차 적용(2026-06-28): <code>/admin/users</code>에서 허용 도메인 밖 이메일도 정확한 계정 단위로 강제 등록할 수 있다. Google/SAML/dev-login은 미리 등록된 이메일만 도메인 게이트 예외로 통과시키고, provider 로그인 시 기존 관리 row의 역할·코호트·승인 상태를 이어받는다. 미등록 외부 도메인 로그인은 계속 차단한다.</p>
<p class="dg-note">10차 적용(2026-06-27): 학습자 영역을 <code>/learn</code> 대시보드, <code>/learn/practice</code> 연습 대상, <code>/learn/history</code> 기록으로 분리했다. 홈은 최근/진행 회기를 기준으로 Live2D 아바타·감정 힌트·마지막 내담자 반응·학습자 마지막 발화·AI 코치 문장을 보여주는 세션 리캡 중심 대시보드로 바꾸고, 새 회기 시작은 학습 탭으로 낮췄다.</p>
<p class="dg-note">11차 적용(2026-06-27): 현재 <code>/learn</code> 화면을 다시 캡처한 뒤 UX 원칙(사용자 필요, progressive disclosure, 정보 향, adaptive layout)을 기준으로 이미지 생성 시안 3장을 만들었다. UI 지침에는 <code>11-learner-dashboard-redesign-concept-b.png</code>, <code>12-learner-dashboard-redesign-concept-b-alt.png</code>만 채택하고, 상담실 배경으로 나온 <code>10-learner-dashboard-redesign-concept-a.png</code>는 폐기했다. 구현은 회기 리캡과 AI 코치/이어하기 CTA를 홈 첫 블록으로 묶고, 독립 <code>다음 행동</code> 카드는 제거했다.</p>
<p class="dg-note">16차 적용(2026-06-28): 생성 시안과 더 맞추기 위해 <code>/learn</code> 하단을 <code>리뷰 대기</code>, <code>다음 연습 추천</code>, <code>최근 기록</code> 3분할 카드로 재배치했다. AI 코치 패널에서는 진행/리뷰 카운트와 보조 링크를 제거하고, 코칭 문장·3개 체크 힌트·단일 <code>이어하기</code> CTA만 남겼다. 최신 검증 캡처는 <code>docs/design-verification/learner-home-redesign/final-*.png</code>다.</p>
<p class="dg-note">18차 적용(2026-06-28): <code>/learn/history</code>는 표형 기존 회기 목록을 제거하고 <code>전체</code>, <code>진행 중</code>, <code>리뷰 필요</code>, <code>보관됨</code> 상태 필터와 검색, 카드형 회기 기록 작업대로 재구성했다. 설계 지침은 <code>docs/design-concepts/prompts/learner-history-archive-redesign-2026-06-28.md</code>, 검증 캡처는 <code>docs/design-verification/learner-history-redesign/history-*.png</code>다.</p>
<p class="dg-note">22차 적용(2026-06-28): 회기 아카이브를 실제 저장 상태로 연결했다. 학습자 종료 회기는 <code>POST /sessions/{id}/archive</code>/<code>restore</code>로 보관·복원하고, <code>app.session_archive_state</code>는 삭제가 아니라 학습자별 보기 상태만 저장한다. 회기·턴·리뷰·공유 링크·연구/감사 증거는 유지하며, 보관된 회기는 <code>/learn/history</code><code>보관됨</code> 필터와 dashboard <code>archived_sessions</code>에 반영되고 리뷰 대기 행동 큐에서는 제외된다.</p>
<p class="dg-note">24차 적용(2026-06-28): 운영 콘솔 <code>/admin/usage</code>의 DB 집계 쿼리에서 <code>ORDER BY tokens_in + tokens_out</code>가 PostgreSQL 원본 컬럼 참조로 해석되어 prod에서 503 fail-closed 되던 문제를 수정했다. provider/model별 정렬은 <code>SUM(tokens_in)+SUM(tokens_out)</code> 집계식 기준으로 바꿨고, public API 프로세스의 실제 HTTP smoke는 <code>source=database</code>, <code>durable=true</code>로 200을 반환한다.</p>
<p class="dg-note">23차 적용(2026-06-28): <code>app.persona_voice_map</code>을 실제 <code>/voice/ws</code> TTS voice 선택에 연결했다. 명시 query preset이 최우선이고, 없으면 세션의 <code>persona_id/persona_version</code> 또는 dev persona catalog의 voice map을 읽으며, unsupported provider는 기존 persona-code fallback으로 fail-safe 처리한다. seed materializer는 기본 OpenAI voice map row를 <code>ON CONFLICT DO NOTHING</code>으로 생성하고, dev 런타임 스키마 보강은 기존 DB의 <code>persona_voice_map</code> 누락도 함께 복구한다.</p>
<p class="dg-note">12차 적용(2026-06-27): 회기 리뷰 데스크톱 레이아웃은 sticky 요약 레일을 유지하되 트랜스크립트가 같은 컬럼을 공유하지 않도록 재배치했다. 왼쪽 요약 문장은 보조 브리핑 밀도로 낮추고, 축어록은 중앙 근거 작업대에 고정해 스크롤 중 레일이 발화 내용을 가리지 않게 했다.</p>
<p class="dg-note">13차 적용(2026-06-27): 회기 리뷰의 <code>적절한 개입</code> 등 평가 콜아웃은 제한 markdown을 기본 렌더링한다. <code>effective_openness</code><code>effective openness(유효 개방도)</code>로 학술 용어화하고, 상담자 질문 발화는 <code>ReviewNote.quote</code> 인용 블록으로 분리해 카드 내부에서 시각화한다.</p>
<p class="dg-note">14차 적용(2026-06-27): 활성 상담 세션은 시작 직후 실제 <code>session_id</code> URL로 고정해 새로고침 후에도 같은 회기 상세를 복원한다. 전역 topbar는 몰입형 화면에서 계속 숨기되, 내부 세션바에 <code>기록으로</code>/<code>학습 홈</code> 이동을 추가했다. 종료된 세션 상세도 곧바로 리뷰로 밀어내지 않고 읽기 전용 기록 화면에 머물며, 리뷰 이동은 명시 버튼으로만 수행한다.</p>
<p class="dg-note">15차 적용(2026-06-28): 학습자·교수자·운영·페르소나 스튜디오 화면을 같은 작업형 폭 정책으로 정렬했다. 교수 콘솔은 검토 대기·위기 알림·페르소나 검수·저작실 순서가 DOM과 화면 모두 일치하고, 페르소나 스튜디오는 SSOT 첨부/RAG 생성/항목 편집/검증 큐가 한 작업면에서 이어지는 구조로 유지한다. 적용 리포트와 대표 캡처는 <code>docs/ops/layout-research-2026-06-28/dashboard-layout-alignment-report.md</code>에 남겼다.</p>
<div class="dg-principles" aria-label="디자인 생성 가드레일">
<div><b>래스터만 사용</b><span>이미지 생성 도구 산출물은 PNG 기반 시안이다. SVG·벡터·와이어프레임·로고 시트로 해석하지 않는다.</span></div>
<div><b>기능 우선</b><span>메인 라우트의 실제 액션과 정보 구조를 먼저 반영한다. 장식은 기능을 가리지 않는 수준에서만 쓴다.</span></div>
<div><b>온화하지만 임상적</b><span>종이 질감, 낮은 채도, 차분한 빛은 허용하되 상담 훈련 도구의 신뢰감을 먼저 세운다.</span></div>
<div><b>반응형 세트</b><span>각 핵심 화면은 한 장 안에 PC·태블릿·모바일을 함께 배치해 전환 규칙을 비교한다.</span></div>
</div>
<div class="dg-table-wrap">
<table class="dg-table">
<thead>
<tr><th>화면</th><th>추출한 핵심 기능</th><th>디자인 의도</th></tr>
</thead>
<tbody>
<tr><td>로그인/진입</td><td>Google OAuth, 허용 도메인 안내, 로컬 역할 선택, 교육용 비치료 고지</td><td>신뢰·보안이 먼저 보이는 조용한 진입 화면</td></tr>
<tr><td>학습자 홈</td><td>학습 요약, 진행/리뷰 가능 회기, 마지막 세션 리캡, Live2D 감정 힌트, AI 코치, 학습·기록 드릴다운</td><td>오늘 이어갈 회기와 학습 상태가 먼저 보이는 개인 대시보드</td></tr>
<tr><td>상담 세션</td><td>회기 단계, 내담자 컨텍스트, 아바타, 음성 오브, 실시간 자막, 텍스트 폴백, 안전 점검</td><td>화면 중심은 내담자, 피드백은 조용한 신호</td></tr>
<tr><td>회기 리뷰</td><td>요약, 감정 밸런스, 회기 흐름, 축어록, 기법 태그, 개선점, 오디오/PDF</td><td>점수 폭격이 아니라 근거 기반 회고</td></tr>
<tr><td>교수 콘솔</td><td>리뷰 대기 큐, 코호트 현황, 페르소나 검수, 종료 회기 목록</td><td>awareness보다 triage에 가까운 교수자 작업대</td></tr>
<tr><td>관리자 콘솔</td><td>운영 홈: 교육 진행·온라인 사용자·헬스 샘플 이력·AI 비용·운영 티켓 요약. 하위 메뉴: 사용자 관리, 접근 권한, 운영 티켓 처리 큐</td><td>상태 관측과 관리 작업을 분리한 실제 운영 콘솔</td></tr>
<tr><td>설정</td><td>계정, 관리자 AI 운영 설정, 테마, 음성 프리셋, 속도, 알림</td><td>역할별 설정만 보이는 조용한 환경 관리</td></tr>
<tr><td>이미지 요소</td><td>종이 질감, 상담실 책상, 노트, 식물, 부드러운 빛, 패턴, 아바타 분위기</td><td>UI 뒤에서만 느껴지는 따뜻함과 반구상 내담자 톤</td></tr>
</tbody>
</table>
</div>
<div class="dg-gallery" aria-label="생성된 디자인 시안 갤러리">
<article class="dg-card">
<img src="design-concepts/generated/01-login-responsive.png" alt="로그인 화면 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
<div class="dg-card-body"><b>로그인/진입</b><span>OAuth, 허용 도메인, 로컬 역할 선택, 교육용 고지를 신뢰 중심으로 정리한 진입 화면.</span><code>01-login-responsive.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/02-learner-home-responsive.png" alt="학습자 홈 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
<div class="dg-card-body"><b>학습자 홈</b><span>내담자 선택, 회기 시작, 이전 회기 이어가기, 리뷰 가능 상태를 작업대처럼 배치.</span><code>02-learner-home-responsive.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/11-learner-dashboard-redesign-concept-b.png" alt="학습자 대시보드 회기 리캡 중심 재설계 시안" loading="lazy">
<div class="dg-card-body"><b>학습자 대시보드 재설계</b><span>회기 리캡, AI 코치, 이어하기 CTA를 첫 화면의 단일 결정 영역으로 묶음.</span><code>11-learner-dashboard-redesign-concept-b.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/12-learner-dashboard-redesign-concept-b-alt.png" alt="학습자 대시보드 대안 재설계 시안" loading="lazy">
<div class="dg-card-body"><b>학습자 대시보드 대안</b><span>마지막 내담자 반응과 내 마지막 반응을 더 강하게 드러낸 대안 레이아웃.</span><code>12-learner-dashboard-redesign-concept-b-alt.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/03-session-responsive.png" alt="라이브 상담 세션 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
<div class="dg-card-body"><b>라이브 상담 세션</b><span>내담자 무대, 자막, 입력, 라이브 신호, 안전 상태를 몰입형 훈련 화면으로 구성.</span><code>03-session-responsive.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/04-session-review-responsive.png" alt="회기 리뷰 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
<div class="dg-card-body"><b>회기 리뷰</b><span>감정 흐름, 축어록, 기법 태그, 교수자 콜아웃을 근거 기반 회고 화면으로 묶음.</span><code>04-session-review-responsive.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/05-professor-responsive.png" alt="교수 콘솔 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
<div class="dg-card-body"><b>교수 콘솔</b><span>리뷰 대기, 코호트 현황, 페르소나 검수, 종료 회기를 triage 중심으로 배치.</span><code>05-professor-responsive.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/06-admin-responsive.png" alt="관리자 콘솔 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
<div class="dg-card-body"><b>관리자 콘솔</b><span>운영 홈은 서비스 상태 관측에 집중하고 사용자·권한·티켓은 별도 메뉴로 분리. 티켓과 헬스 샘플은 DB 기반 1차 모델을 사용.</span><code>06-admin-responsive.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/07-settings-responsive.png" alt="설정 화면 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
<div class="dg-card-body"><b>설정</b><span>계정, AI 운영, 테마, 음성, 알림을 역할별로 노출하는 차분한 설정 화면.</span><code>07-settings-responsive.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/08-warm-visual-elements.png" alt="온화한 배경과 패턴 이미지 요소 보드" loading="lazy">
<div class="dg-card-body"><b>온화한 이미지 요소</b><span>종이 질감, 상담실 책상, 노트, 식물, 낮은 채도의 빛을 UI 보조 요소로 정리.</span><code>08-warm-visual-elements.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/09-avatar-pattern-elements.png" alt="반구상 아바타와 패턴 이미지 요소 보드" loading="lazy">
<div class="dg-card-body"><b>아바타/패턴 요소</b><span>반구상 내담자 분위기와 배경 패턴 방향. 실제 페르소나 아이덴티티는 별도 제작 기준을 따른다.</span><code>09-avatar-pattern-elements.png</code></div>
</article>
</div>
</div>
</section>
<!-- ░░ STATUS BOARD ░░ -->
<section class="block" id="board">
<div class="sec-head">
<div>
<h2>상태 보드</h2>
<p class="sub">현재 추적 중인 작업을 카드로. <b>요약만 노출</b>되고, 상세 근거·검증 로그는 카드를 펼쳐야 보인다. 필터로 "내 차례"·블로커만 추려 볼 수 있다.</p>
</div>
</div>
<div class="filterbar" role="group" aria-label="상태 필터">
<span class="fl">필터</span>
<button class="fbtn" data-filter="all" aria-pressed="true">전체 <span class="fn" data-count="all">0</span></button>
<button class="fbtn" data-filter="done" aria-pressed="false">완료 <span class="fn" data-count="done">0</span></button>
<button class="fbtn" data-filter="doing" aria-pressed="false">진행 <span class="fn" data-count="doing">0</span></button>
<button class="fbtn" data-filter="planned" aria-pressed="false">계획·블로커 <span class="fn" data-count="planned">0</span></button>
<button class="fbtn f-owner" data-filter="owner" aria-pressed="false">오너 의존 <span class="fn" data-count="owner">0</span></button>
<button class="fbtn" data-filter="crit" aria-pressed="false">납품 필수 <span class="fn" data-count="crit">0</span></button>
</div>
<!-- TRACK 1 -->
<div class="track-group">
<div class="tg-head"><h3>한신대 PDF 정합성 · 이번 세션 발견</h3><span class="tg-note">원천 기술분석 PDF(트웬티온스×한신대, 19쪽) 전수 정독 대조 (2026-06-27)</span></div>
<div class="scards">
<article class="scard" data-status="done" data-cat="코어·차별" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">정합 ✓</span><span class="scard-mid"><span class="scard-title">원안과 잘 맞는 코어(유지)</span><span class="scard-sum">저항·반응 조절·상태머신·RAG·이론모드·가드레일·평가 JSON Schema가 이미 구현·정렬됨.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>PDF가 차별 기술로 꼽은 <b>'저항·반응 조절'</b>(AI가 너무 착하지 않게, 실제 환자처럼)·상태머신(라포→탐색→개입→정리)·RAG(자료검색)·이론모드(인간중심/CBT)·답변과 분리된 가드레일·SSE 스트리밍·React19·<b>피드백 JSON Schema 강제</b>(벤더중립)가 이미 구현·정렬됨. Stanford RCT(피드백 없는 연습=역효과)도 H2 턴평가로 충족 방향.</p></div><div class="kv"><b>권고</b><p>이 코어를 '교육용(비의료)' 포지션으로 유지. 평가는 교수자 검토용 보조지표로 표기.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="코어·차별" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">신규 DONE</span><span class="scard-mid"><span class="scard-title">역린(逆鱗)·트리거 + 수련생 misconduct 반응</span><span class="scard-sum">무례·모욕·조롱 시 현실적 동맹 균열 + 핵심상처 역린 블록 추가. 무회귀 77/77.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>라이브 테스트에서 수련생이 내담자를 "수동적 패배자"라 비난·조롱("웃음이 나오십니까")해도 민재가 공손히 흡수 → 훈련 가치 훼손. 수정: ① L0에 무례·모욕·조롱·인신공격 시 <b>현실적 동맹 균열</b>(위축·방어·따지기·종결의향) 지침 ② <b>PersonaCard.triggers(역린)</b> 필드 + CCD 핵심상처 파생 역린 블록(상담자가 핵심상처를 조롱·확정·강요·평가절하 시 가장 강한 반응). PDF '저항·반응 조절' 핵심 차별 강화. 무회귀 77/77.</p></div><div class="kv k-warn"><b>잔여</b><p>P1~P3 명시 triggers는 임상팀 정밀 저작(현재 CCD 파생 기본값으로 작동). 라이브 재현 검증.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="안전·위기" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">CRITICAL 1차 DONE</span><span class="scard-mid"><span class="scard-title">위기 안전게이트<span class="critbadge">납품 필수</span></span><span class="scard-sum">자해·자살 감지 시 LLM 호출 전 중단, 109 안내, safety_events 적재, 교수자 대시보드 알림 큐까지 연결.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>PDF(9·16·18쪽): 자해·자살 감지 시 <b>대화 중단 + 자살예방상담 109 안내 + 지도교수 자동 알림</b>, 안전은 <b>LLM 밖 별도 CrisisGate</b>. 구현: <code>CRISIS_HOTLINE_NUMBER=109</code>, <code>TurnResponse.crisis_resource</code>/<code>conversation_stopped</code>, REST·SSE·voice 경로의 엔진 호출 전 중단, <code>app.safety_events</code> detail 적재, 교수자 대시보드 안전 알림 큐.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> 20 passed, 안전 이벤트 DB insert payload 테스트 포함. <code>pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> 21 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>비밀보장 예외고지·생명유지서약 등 임상 문안, 실시간 push/메일 알림, 지역 자원 확장, 운영 정책 문구는 소유자·임상팀 확정 필요. 현재 교수자 알림은 <code>safety_events</code> 기반 대시보드 큐다.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="교수자·평가" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">CRITICAL UX 1차 DONE</span><span class="scard-mid"><span class="scard-title">교수자 '학습자 이력·성장 추적' UI/UX<span class="critbadge">핵심 가치</span></span><span class="scard-sum">교수 콘솔에 학습자별 회기 이력, 적절성 추이, 라포, 기법 태그, 최근 회기 포인트를 추가.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p><code>TeacherDashboardResponse.learner_growth</code>를 추가해 턴별 fast-loop 평가의 <code>appropriateness</code>, <code>rapport_signal</code>, 기법 태그를 학습자별로 집계한다. 교수 콘솔은 학습자별 성장 카드, 회기별 막대 추이, 평균 라포, 변화폭, 최근 회기 점검 포인트를 표시한다. 평가가 없는 회기는 "평가 부족"으로 남겨 가짜 곡선을 만들지 않는다. 종료 회기 리뷰 대기 행은 <code>/teach/session/{id}/review</code>로 진입해 같은 회기 리뷰를 교수자 읽기 전용으로 열고, 워크시트는 학습자 제출물 수정 없이 검토 전용으로 표시한다.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> 21 passed, <code>pytest app/test_rbac_idor.py app/test_teacher_dashboard.py -q</code> 9 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed. <code>npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code> 4 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>자기효능감·기술숙련도·수련만족도 사전사후(H1)와 교수자 코멘트/검수 워크플로 확장은 별도 평가설계·임상팀 문항 확정이 필요하다.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="교수자·평가" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">검토 상태 1차 DONE</span><span class="scard-mid"><span class="scard-title">교수자 회기 검토 상태 저장</span><span class="scard-sum"><code>app.session_review_status</code><code>PUT /teacher/sessions/{session_id}/review-status</code>로 검토 메모·완료 상태를 저장.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>교수자/관리자는 <code>/sessions/{id}/review</code>를 읽을 수 있고, 학습자 워크시트 저장은 learner 전용으로 유지한다. 교수자 검토 상태는 <code>app.session_review_status</code><code>review_status</code>, <code>review_note</code>, <code>reviewed_at</code>로 저장되며, teacher dashboard 응답과 <code>SessionReview</code>가 이 값을 표시한다. <code>PUT /teacher/sessions/{session_id}/review-status</code>는 교수자 메모 저장과 검토 완료 처리를 담당하고, 검토 완료된 회기는 pending queue에서 제외한다.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_learner_dashboard.py -q</code> 12 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>teacher.spec.ts</code> 4 passed. 교수 콘솔 검토 큐 노출과 학습자 리뷰의 교수자 전용 빈 grid 제거는 E2E/시각 게이트로 재검증했다.</p></div><div class="kv k-warn"><b>잔여</b><p>임상 루브릭·최종 검수 기준은 외부 정의 전까지 완료 처리하지 않는다.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="학습자·UX" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">개인화 1차 DONE</span><span class="scard-mid"><span class="scard-title">학습자 홈 대시보드 / 최근 회기 리캡</span><span class="scard-sum"><code>GET /sessions/dashboard</code><code>LearnerHome</code>가 실제 세션 기반 성장·피드백·페르소나 진행을 표시.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>교수자 성장 집계와 중복되던 점수·라포·기법 계산을 <code>app.services.session_metrics</code>로 공용화했다. <code>GET /sessions/dashboard</code>는 학습자 본인 세션만 집계해 <code>overview</code>, <code>growth</code>, <code>persona_progress</code>, <code>achievements</code>, <code>recent_feedback</code>를 반환하고, <code>LearnerHome</code>은 누적 회기·리뷰 대기·최근 평가·라포 흐름·페르소나별 진행·최근 피드백·마지막 세션 리캡을 실제 서버 데이터로 보여준다. 성취는 공식 수료/등급이 아니라 실제 연습 milestone만 사용한다.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_learner_dashboard.py app/test_teacher_dashboard.py app/test_rbac_idor.py -q</code> 12 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>learner.spec.ts</code> 6 passed, <code>session-review.spec.ts</code> 2 passed, <code>layout-visual-gate.spec.ts</code> 7 passed, <code>session-layout.spec.ts</code> 4 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>UX 감사 기준으로 2차에서는 카드 격자 비중을 줄이고 게이지·small multiples·테이블 중심으로 정리해야 한다. 모바일에서 임상 요약을 단순 숨김 처리하는 부분도 접힘/요약으로 바꾸는 게 맞다.</p></div></div>
</article>
<article class="scard" data-status="planned" data-cat="거버넌스·아키텍처" data-owner="1">
<button class="scard-head" aria-expanded="false"><span class="chip c-plan">중기</span><span class="scard-mid"><span class="scard-title">2-tier 모델 전략 + 백엔드 언어(원안 차이)<span class="ownbadge">내 차례</span></span><span class="scard-sum">2-tier는 ENGINE_MODE로 점진 도입 가능. 백엔드 언어는 소유자 결정.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>① PDF: 고성능(Claude·GPT=내담자발화·실시간평가)/저비용 한국어(Solar=단순응답·사후요약) 2단계 + 캐싱 90%절감. 현재 client AI는 claude_cli 기본 라우팅, fast/deep evaluator는 <code>EVALUATOR_FAST_MODEL</code>/<code>EVALUATOR_DEEP_MODEL</code> 설정 시 모델 override를 gateway에 전달한다. 벤더중립 인터페이스와 L0~L2 cache hint/session reuse는 있음. ② PDF는 <b>Node.js 우선</b>(팀 보유기술·5개월 일정)/Spring 대안. 현재 <b>Python/FastAPI</b> — 재작성은 일정·인력 리스크.</p></div><div class="kv k-warn"><b>권고</b><p>2-tier는 evaluator 모델 override 이후 client AI/요약 모델 정책과 semantic cache로 점진 도입. 백엔드 언어는 <b>소유자 결정</b>(팀 역량·유지보수 vs 재작성 비용).</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="코어·차별" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">검증 ✓</span><span class="scard-mid"><span class="scard-title">무회귀 검증 종합</span><span class="scard-sum">이번 전체 검증: Playwright E2E 113 + build/typecheck 통과.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>이번 점검 범위인 SEO/GEO 공유 카드, 온보딩 반영 E2E, 학습자 홈, 회기 시작/종료, 리뷰 공유 버튼, 음성 성공 경로, 레이아웃 회귀는 <b><code>npm run e2e</code> 113 passed + <code>npm run typecheck</code>/<code>npm run build</code></b>로 닫았다. 공개 API와 Tailnet 스모크도 녹색이고, 패키징 점검에서 더미 운영 env 기준 Docker compose fresh smoke도 통과했다. 과거 focused suite와 Phase 3 evidence는 별도 행의 과거 증거로 유지하되, 이번 완료 증거와 섞어 과장하지 않는다.</p></div><div class="kv"><b>다음</b><p>남은 원천문서 갭, 임상 루브릭/최종 평가설계, public Google OAuth 실제 <code>/turn</code>, 배포지 운영 비밀값 주입을 순서대로 닫는다.</p></div></div>
</article>
</div>
</div>
<!-- TRACK 2 -->
<div class="track-group">
<div class="tg-head"><h3>원천문서 갭 분석</h3><span class="tg-note">산학협력 원천문서 5종 정독 + 코드 교차조사 (2026-06-26). 근거: source-docs-gap-analysis-2026-06-26.md · critical 3 / high 4 / medium+ 6</span></div>
<div class="scards">
<article class="scard" data-status="done" data-cat="콘텐츠·산출물" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">C1 2차 DONE</span><span class="scard-mid"><span class="scard-title">사례개념화·치료계획 워크시트 저장형 구조<span class="critbadge">critical</span></span><span class="scard-sum">축어록 기반 초안을 학습자가 편집·저장하는 <code>app.case_worksheet</code> 경로까지 연결했다. 임상 루브릭은 외부 의존.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>doc1/4/5가 1급 산출물로 요구하는 워크시트 축(탐색 11항목·호소 5영역·인지삼제·1/2차 감정·보호/방해 4사분면·생물심리사회 목표)을 <code>SessionReviewResponse.caseWorksheet</code>로 노출한다. 서버는 저장된 축어록만 근거로 규칙 기반 초안을 만들고, 프론트 리뷰 화면은 근거 턴으로 점프 가능한 워크시트를 편집할 수 있다. 저장본은 <code>PUT /sessions/{id}/review/worksheet</code><code>app.case_worksheet</code>에 저장되고, 이후 <code>GET /review</code><code>saved_by_learner</code> 워크시트를 자동 초안보다 우선 반환한다.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_session_turn_persistence.py app/test_rbac_idor.py -q</code> 25 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed. 로컬 API smoke: 세션 생성→워크시트 저장→<code>GET /review</code><code>saved_by_learner:local smoke saved worksheet</code> 반환.</p></div><div class="kv k-warn"><b>잔여</b><p>임상팀 루브릭, AI 추출/채점 고도화, 교수자 검수 플로우는 후속이다. 현재 초안/저장본은 숨은 정답키나 임상 점수 없이 학습자 편집 산출물로만 취급한다.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="안전·위기" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">C2 구조 DONE</span><span class="scard-mid"><span class="scard-title">위기 에스컬레이션·safety_events 배선</span><span class="scard-sum">crisis.risk_level→ideation_observed 전달 + escalate 시 safety_events 적재. 라이브 검증.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>prepare_turn→state_machine.evolve에 crisis.risk_level을 ideation_observed로 전달(위기 관측 상태머신 반영). escalate=True 시 REST·SSE·voice가 엔진 호출 전 중단하고 <code>app.safety_events</code>에 109 resource와 <code>conversation_stopped</code>를 적재한다. 교수자 대시보드는 이 레코드를 안전 알림 큐로 표시한다. 안전 이벤트 insert payload는 <code>test_record_safety_event_writes_teacher_alert_payload</code>로 회귀화했다.</p></div><div class="kv k-warn"><b>잔여(임상팀)</b><p>비밀보장 예외고지·단계적 탐색·생명유지서약 스크립트 콘텐츠 + 실시간 교수자 push/메일 + 위기탐색 누락 회기리뷰 감점.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="교수자·평가" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">C3 구조 DONE</span><span class="scard-mid"><span class="scard-title">이론모드 스레딩(humanistic/CBT)</span><span class="scard-sum">TurnContext+prepare_turn+build_turn_messages에 theory_mode 배선, 프론트 하드코딩 제거.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>TurnContext+prepare_turn에 theory_mode, sessions/voice가 sess.theory_mode 전달, evaluator._theory_mode가 세션 이론(학습자 선택) 우선. <code>build_turn_messages</code><code>THEORY_MODE_GUIDANCE</code>를 통해 인간중심·CBT·통합 이론 프레이밍을 엔진 메시지에 넣는다. 프론트 Session.tsx 'humanistic' 하드코딩 제거→persona.theory_target 기준(CBT 페르소나는 cbt). 회귀: <code>test_prepare_turn_threads_theory_mode_into_engine_messages</code> 포함 focused 20 passed.</p></div><div class="kv k-warn"><b>잔여(임상팀)</b><p>CBT 이론별 단계 프롬프트 체인 콘텐츠 + 이론부합 채점 루브릭 + 프론트 이론 선택 UI(현재 persona 설계 이론 자동).</p></div></div>
</article>
<article class="scard" data-status="planned" data-cat="교수자·평가" data-owner="1">
<button class="scard-head" aria-expanded="false"><span class="chip c-plan">HIGH H1</span><span class="scard-mid"><span class="scard-title">계약 평가 KPI(자기효능감·기술숙련도·수련만족도 사전사후) 수집·집계 전무<span class="ownbadge">내 차례</span></span><span class="scard-sum">doc4가 3척도 pre-post를 명시했으나 코드 0건. 평가설계는 소유자 결정.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>doc4가 20명 실험/통제군·단회기 50분·3척도 pre-post를 못박았으나 코드 0건. Phase3 KPI도 report shape만, 계산 코드 0줄. (κ/ICC·환각률은 doc4 미명시 — 평가설계 문서 확정 필요.)</p></div><div class="kv k-warn"><b>권고</b><p>3척도 pre-post 폼·실험/통제군 배정·자동누적 대시보드·추이 시각화·검정 계산 코드. 평가설계는 소유자 결정.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="교수자·평가" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">H2 LIVE DONE</span><span class="scard-mid"><span class="scard-title">턴별 fast-loop + 라이브 코칭·이력</span><span class="scard-sum">평가 정규화에 더해 워크북·DSM·공식 지침/RAG 기반 코칭 아바타, 근거 모달, 발화별 이력 오버레이와 source pack RAG sync를 연결.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>텍스트 <code>/turn</code>·<code>/stream</code>·음성 경로에서 fast-loop 턴 평가를 <code>app.feedback_scores</code>, <code>app.turn_technique</code>, <code>app.turn_client_state</code>, <code>app.supervisor_comment</code>, <code>app.alternative_utterance</code>로 정규화 적재 후 리뷰 조회에서 hydrate한다. 이번 패스에서 <code>app/services/live_coach.py</code>, <code>data/kb/live_coaching_workbook_0615.json</code>, <code>data/kb/live_coaching_sources/*.json</code>, <code>POST /sessions/{id}/live-coach</code>, <code>GET /sessions/{id}/live-coach</code>, <code>POST /kb/live-coach/source-packs/sync</code>, <code>app.live_coach_events</code>를 추가했다. 세션 UI는 코칭 모드에서 AI 코치 아바타 말풍선으로 즉시 개입하고, "근거 보기" 모달과 학습자 발화 우측 <code>C</code> 마커를 통해 받은 코칭 이력을 채팅 위 스크롤 오버레이로 보여준다. 저장 payload는 PII 마스킹 excerpt + 코칭 구조화 JSON이며, 공식 자료는 허가된 source pack의 version/citation/summary로 넣고 장문 원문·공식 문항을 재현하지 않는다. 같은 source pack은 관리자 sync로 evaluator 전용 RAG에 증분 색인한다.</p></div><div class="kv"><b>검증</b><p><code>python -m compileall app -q</code>, <code>pytest app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q</code> 27 passed, <code>npm run generate:api-types</code>, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>npm run e2e -- e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts</code> 15 passed. 로컬 DB smoke: source pack sync 4 sources/15 chunks embedded, 재호출 <code>skipped_unchanged=4</code>, <code>official_suicide_risk_guidelines</code> source filter 검색 성공.</p></div><div class="kv k-warn"><b>잔여</b><p>임상팀 골든셋·라이브 코칭 루브릭 검수, 회기리뷰 2열 UI 고도화, 공식 지침/논문 source pack 추가와 버전 운영정책 보강이 남아 있다.</p></div></div>
</article>
<article class="scard" data-status="doing" data-cat="콘텐츠·산출물" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-doing">HIGH H3 4차 DONE</span><span class="scard-mid"><span class="scard-title">페르소나 스튜디오 + RAG 첨부 SSOT 생성</span><span class="scard-sum">JSON 패널을 별도 저작 스튜디오로 분리하고, 실제 기록·교재 자료를 KB 근거 문서로 등록한 뒤 초안을 생성한다.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>teacher/admin 전용 <code>/teach/personas</code>를 3열 스튜디오로 추가했다. 좌측은 저작 흐름·검수 큐·공개 카탈로그, 중앙은 RAG 자료 등록과 개요/임상/저항/회기/말투/안전/프롬프트 항목형 편집 탭, 우측은 검증·생성 근거·승인/반려 결정이다. 교수 콘솔은 진입점과 triage만 맡고 저작 본체는 스튜디오가 맡는다. 2026-06-27 UI 정리에서 중복 네이티브 파일 입력을 커스텀 SSOT 첨부 드롭존으로 교체하고, 저작 스테퍼·탭별 작성 가이드·검증 dot 정렬을 보강했다. 레이아웃 근거와 생성 시안은 <code>docs/ops/layout-research-2026-06-27/persona-dashboard-layout-guideline.md</code>에 묶었다. <code>POST /personas/sources</code>는 첨부/붙여넣기 자료를 PII 마스킹 후 <code>kb.source/document/chunk</code>에 evaluator 전용(<code>visible_to=['evaluator']</code>, <code>sensitivity=2</code>) 근거 문서로 등록한다. <code>POST /personas/drafts/generate</code><code>source_id</code> 기반 RAG 검색 결과만 엔진에 넘기고, draft <code>source_provenance</code>와 응답에 source id·doc/content hash·chunk id를 남긴다. 승인된 카드만 학습자 catalog에 노출되는 경계는 유지하되, P1~P7 시스템 페르소나는 DB 저작 카탈로그의 초기 부트스트랩으로 승격했다. 교수자는 공개 목록의 <code>수정</code>으로 승인본을 같은 <code>persona_id</code>의 다음 버전 draft로 복제해 편집하고, <code>삭제</code>는 기존 회기 FK를 지키기 위해 같은 code family를 <code>archived</code>로 보관 처리한다. 기동 materializer는 누락분만 insert하고 기존 편집/archived row를 덮어쓰거나 되살리지 않는다.</p></div><div class="kv"><b>검증</b><p><code>python -m pytest app/test_persona_review.py -q</code> 31 passed, <code>npm run generate:api-types</code>, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed. PersonaStudio 공개본 수정/삭제 E2E: <code>npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code> 6 passed, <code>layout-visual-gate.spec.ts</code> chromium-single-run 8 passed, <code>session-layout.spec.ts</code> desktop/mobile 8 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>원문 격리본(<code>sensitivity=3</code>) 별도 보관, source/version 전용 마이그레이션 runner, prompt-bundle version pinning, 임상팀 최종 검수/서면 evidence, 루브릭·이론 콘텐츠 외부화는 후속이다. 임상팀이 제공하지 않은 페르소나 내용을 최종 승인본처럼 확정하지 않는다.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="보안·PII" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">H4 부분 DONE</span><span class="scard-mid"><span class="scard-title">PII 마스킹 + LLM 호출 감사 + 프로필 온보딩·동의 게이트</span><span class="scard-sum">metadata-only llm_call_log, 단순 가입정보 온보딩, 약관 버전, 전역 URL 하드게이트. 전체 E2E 113.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>정규식 폴백에 한국어 고위험 패턴 추가: 구체 날짜(2001.4.18→[DATE])·금액(1200원→[MONEY])·행정구역 주소(서울시 강남구 역삼동→[ADDR]). 상담 생성(generate/stream)·fast/deep 평가의 외부 LLM 호출 직후 <code>audit.llm_call_log</code>에 provider/model/token/cost/inference_geo/latency만 적재한다. prompt/completion 본문은 저장하지 않는다. 로컬 dev-login 실제 <code>/turn</code> smoke에서 <code>audit.llm_call_log</code>가 8→11로 3행 증가했다. 이번 패스에서 <code>app_user</code>에 이름·소속·학과·학년/직위·연락처·주소/수령지·닉네임·자기소개·아바타 URL·약관/개인정보 동의 시각·버전을 추가했다. 로그인 후 <code>onboarding_completed_at</code>이 없으면 전역 <code>OnboardingGate</code><code>/onboarding</code> 외 URL(<code>/learn</code>, <code>/settings</code>, <code>/admin</code>, <code>/dev/avatar-preview</code>, <code>/login</code> 포함)을 모두 온보딩으로 돌리고, 온보딩 중에는 공용 셸 메뉴 없이 가입 직후 사용자 정보 입력 폼만 보여준다. learner 회기 시작도 온보딩 완료 전에는 막는다. <code>/users/me/avatar</code>는 PNG/JPG/WebP MIME·시그니처·3MB 제한 후 파일을 <code>USER_UPLOAD_DIR/profile-avatars</code>에 저장하고 URL만 보관한다. 약관·개인정보 처리방침 초안은 <code>/users/legal-docs</code>에서 제공한다. 관리자/교수자 권한은 온보딩에서 받지 않고 allowlist/관리자 사용자 관리로만 부여한다. 검증: <code>pytest app/ -q</code> 145 passed, <code>pytest engine_gateway/ -q</code> 9 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, auth E2E 7 passed, 최종 <code>PLAYWRIGHT_PORT=5174 npm run e2e</code> 113 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>이름/기관명은 NER 필요(Presidio ko 모델·spaCy ko, 무거움 — TODO 명시). 미성년/guardian 및 법무 검토가 필요한 최종 서명 동의서·개인정보 처리방침·약관 evidence는 외부 게이트다. 공개 Google OAuth 실제 <code>/turn</code> proof는 별도 운영 게이트.</p></div></div>
</article>
<article class="scard" data-status="planned" data-cat="리팩터·정리" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-plan">MED M1~M3</span><span class="scard-mid"><span class="scard-title">비언어 임상이벤트(M1)·다회기 케이스 아크(M2)·SSO claim/식별자/감사(M3)</span><span class="scard-sum">M1/M2 1차 완료. M3는 cohort map + provider external_id 1차 완료, 기관 SAML/거버넌스는 남음.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>M1: voice learner turn의 audio_ref, silence_ms, speech_rate, barge_in을 리뷰 API <code>nonverbal</code> 이벤트로 파생하고, 회기 리뷰 축어록에 침묵·발화 속도·끼어듦·음성 입력 칩으로 표시한다. 한숨·울음 감지와 live 마이크/STT/TTS 실측은 아직 별도다. M2: <code>case_profile</code>을 learner-persona 안정 case로 upsert하고, <code>session_no</code>를 트랜잭션 안에서 원자 증가시키며, 시작/턴/voice 경로가 직전 <code>session_summary</code> 기반 seed recall을 사용한다. episodic embedding writer와 case_digest/pinned_fact 실적재는 후속. M3: Google/SAML/dev-login이 <code>AUTH_EMAIL_COHORT_MAP</code>/<code>AUTH_DOMAIN_COHORT_MAP</code> 및 SAML cohort claim을 <code>cohort_ids</code>로 전달하고, DB <code>app_user.external_id</code>는 provider subject(<code>google:</code>/<code>saml:</code>/<code>dev:</code>) 기반으로 저장한다.</p></div><div class="kv k-warn"><b>권고</b><p>M1 한숨·울음/억양 등 provider 이벤트 보존, M2 session_summary digest/pinned_fact 압축·embedding writer·case_profile trajectory 갱신, M3 운영 SAML 서명검증·기관 claim schema/test tenant·deprovisioning audit는 외부 IdP 협의 후 진행.</p></div></div>
</article>
<article class="scard" data-status="planned" data-cat="거버넌스·아키텍처" data-owner="1">
<button class="scard-head" aria-expanded="false"><span class="chip c-plan">TRACK X1·X2 1차·L1</span><span class="scard-mid"><span class="scard-title">재귀학습 export(X1)·API 비용 관측(X2)·스택 정합성·일정(L1)<span class="ownbadge">내 차례</span></span><span class="scard-sum">X1/X2 1차와 evaluator 모델 override는 구현 완료. X1 approved export와 L1 거버넌스는 외부 gate가 남음.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>X1: <code>scripts/export-recursive-dataset.py</code><code>app.services.dataset_export</code>로 masked-text JSONL dry-run, PII scan, κ/ICC 계산, approved export 게이트를 구현했다. 기본은 <code>technical_dry_run</code>이고 <code>ds.*</code> write는 <code>--write-dataset</code> 명시 시에만 수행한다. X2: <code>app.turns</code>의 provider/model/tokens/cost를 최근 7일 기준으로 집계하는 <code>GET /admin/usage</code>를 추가했고, 운영 콘솔 <code>/admin</code>에 누적 비용·입출력 토큰·계량 커버리지·provider/model breakdown과 <code>ADMIN_USAGE_BUDGET_USD</code> 기반 예산 경고(ok/warn/exceeded)를 표시한다. <code>EVALUATOR_FAST_MODEL</code>/<code>EVALUATOR_DEEP_MODEL</code> 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 default 라우팅을 유지한다. DB 미가용 dev에서는 runtime store로 fallback하지만 prod에서는 fail-closed한다. L1: doc4 신청서 스택(Spring Boot 3/Node.js·TimescaleDB) vs 실제 FastAPI/Python 불일치, 20주 단기일정·9월 저작권 등재 압박.</p></div><div class="kv k-warn"><b>권고</b><p>X1 approved export·골든셋 승격은 steward/legal 승인, reviewer disposition, IAA 게이트(κ≥0.6/ICC≥0.75) 이후만 진행한다. X2 후속은 semantic cache·장기 비용 추이/한도 정책·운영 모델별 비용 검증, L1은 스택 정합 또는 변경사유 거버넌스 기록.</p></div></div>
</article>
</div>
<div class="detail-blocks">
<div class="detail-block"><h4>즉시 착수 가능(내부 코드, 외부 합의 불요)</h4><ul><li>C1은 저장형 워크시트까지 2차 완료. C2/C3/H2/H3은 1차 구조 완료. 후속은 임상 스크립트, CBT 체인, 2열 리뷰 UI, 페르소나 항목형 저작 UI처럼 데이터·정책·UI 고도화 영역이다.</li><li>H4(부분): 한국어 정규식 마스킹, metadata-only LLM call audit 경로, 로컬 live row proof, learner 동의 수락/철회/회기 시작 하드게이트 골격은 완료. 남은 내부 후보는 이름/기관 NER이고, guardian/legal 서명 동의 evidence는 외부 게이트다. X1 dry-run export 1차와 X2 비용 관측·예산 경고/evaluator 모델 override 1차는 완료, X2 semantic cache는 후속. M1/M2/M3 1차는 완료, provider 기반 한숨·울음 감지와 case_digest/pinned_fact 실적재, 운영 SAML 서명검증은 후속.</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>
</div>
<!-- TRACK 3 -->
<div class="track-group">
<div class="tg-head"><h3>코드품질 · 로컬 개발 스택</h3><span class="tg-note">리팩토링 연구 + 1-커맨드 dev 스택 (2026-06-26~27). code-quality-research-2026-06-26.md · local-development.md</span></div>
<div class="scards">
<article class="scard" data-status="done" data-cat="인프라·스택·RAG" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">DEV STACK</span><span class="scard-mid"><span class="scard-title">로컬 개발 스택 1-커맨드 기동 — <code>scripts/dev-up.ps1</code></span><span class="scard-sum">게이트웨이9099+API8000+웹5173+Docker DB 보장. 3포트 health OK + 실 AI 턴.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>게이트웨이(9099 claude_cli)+API(8000, DB 미가용 시 in-memory degraded)+웹(5173)을 커맨드라인 기준 정리 후 결정론적 기동(<code>--reload</code> 워처 불안정 회피, uvicorn 있는 python 자동 해석). Docker가 사용 가능하면 <code>vignette-dev-db</code> healthcheck/pg_isready/role safety를 점검하고, compose <code>infra\.env</code> 필수값 누락도 경고한다. <code>-NoGateway</code>/<code>-NoWeb</code>는 해당 컴포넌트 정리도 건너뛰고, API 정리는 지정 <code>-ApiPort</code>만 대상으로 한다. dev-login + seed 페르소나. 로그 <code>.devlogs/</code>(gitignore). <b>검증</b>: 3포트 health OK + 실제 AI 턴 생성·회기리뷰 동작, API-only 재기동 후 8001/8010/5173/9099/20241 보존.</p></div><div class="kv k-warn"><b>비고</b><p>AI 턴은 로컬 claude CLI 사용(사용량 발생). 진입점 http://localhost:5173. <code>dev-down.ps1</code>은 기본적으로 DB 컨테이너를 보존하고, <code>-Db</code> 지정 시 함께 중지한다. 기존 <code>vignette-dev-db</code>는 accepting connections이나 healthcheck가 없고, <code>vignette_app</code>은 NOBYPASSRLS로 존재한다. 이번 패키징 smoke에서 fresh compose DB는 owner/app role 분리 상태로 API healthy까지 통과했고, 기존 dev DB의 운영 role 전환은 별도 owner migration 확인 후 진행한다. <b>주의(무DB 한정)</b>: DB 없이 degraded면 페르소나가 <code>source:seed_fallback·degraded:true</code>라 프론트 <code>isUsablePersona</code>가 막아 "회기 시작" 버튼이 비활성 → UI 세션 시작엔 실제 Postgres(<code>127.0.0.1:55432</code>)가 필요.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="인프라·스택·RAG" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">VOICE 작동</span><span class="scard-mid"><span class="scard-title">음성 채팅 "바로 꺼짐" 해결 — provider 키 부재였음</span><span class="scard-sum">키 없으면 1011 즉시 close. .env에 OPENAI_API_KEY 설정→정상. TTS 지문 제거·아바타 모션 수정.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>직접 WS 재현으로 근본원인 규명: 키 없으면 백엔드가 <code>degraded</code>+1011 즉시 close → 마이크 깜빡·녹음 미도달. STT(gpt-4o-transcribe)·TTS(gpt-4o-mini-tts) 모두 OpenAI. <b>실 OPENAI_API_KEY를 apps/api/.env(gitignore)에 설정 → /voice/health available:true</b>(degraded-close 사라짐). 백로그 B2 음성 캐스케이드가 로컬에서 실작동.</p></div><div class="kv"><b>부수 수정</b><p>① 미설정 시 마이크 버튼 사전 비활성+명확 안내 ② <b>TTS 지문 제거</b>: (고개 끄덕)·(한숨)·[침묵] 등 무대지시를 음성으로 안 읽음(<code>speakable_text</code>, 자막엔 유지) ③ <b>아바타 애니메이션</b>: 텍스트 턴 speaking 시 입 정지(0) 버그 수정 + thinking 표현 보강. typecheck+voice테스트 PASS.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="코어·차별" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">페르소나 격리</span><span class="scard-mid"><span class="scard-title">AI 내담자 캐릭터 붕괴·개발맥락 누출 버그 수정</span><span class="scard-sum">--append→--system-prompt 교체 + dynamic 섹션 제외. 누출 0. 게이트웨이 7/7.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>증상</b><p>라이브 회기에서 내담자(민재)가 "저는 Claude Code인데… Vignette 코드베이스… git status…"로 캐릭터를 깨고 개발 맥락 노출. 원인: ENGINE_MODE=claude_cli가 로컬 claude(=Claude Code)를 호출하는데 게이트웨이가 페르소나를 <code>--append-system-prompt</code>로 코딩 어시스턴트 정체성에 <b>덧붙이고</b>, cwd=repo라 CLAUDE.md·git status·env가 시스템프롬프트에 포함됨.</p></div><div class="kv"><b>수정·검증</b><p><code>engine_gateway/gateway.py</code>: <code>--append-system-prompt</code><code>--system-prompt</code>(페르소나만 <b>교체</b> 주입) + <code>--exclude-dynamic-system-prompt-sections</code>(cwd/env/git/메모리 섹션 제거). 라이브: 깨던 입력 그대로 → 민재 완벽 인캐릭터, 누출 0. 게이트웨이 7/7 + 전체 API 119 passed.</p></div></div>
</article>
<article class="scard" data-status="doing" data-cat="리팩터·정리" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-doing">REFACTOR 1 2차 DONE</span><span class="scard-mid"><span class="scard-title">턴 처리 파이프라인 중복(REST/WS) 제거</span><span class="scard-sum"><code>turn_runtime.py</code>로 세션 로드·권한·턴 append·상태 갱신·안전 이벤트 finalize를 공통화.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p><code>app/turn_runtime.py</code>를 추가해 <code>load_owned_session</code>, <code>record_completed_turn</code>, <code>append_completed_turn</code>, <code>update_session_state</code>, <code>record_safety_event</code>를 공통화했다. 이번 패스에서 <code>finalize_completed_turn</code>을 추가해 <code>record_completed_turn</code> + <code>record_safety_event</code> 호출쌍을 REST submit/stream과 voice WS 라우트에서 제거했다. stream fast-loop 평가 누락, voice 세션 recall cache, visible_to/evaluator turn 분리는 보존.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_session_turn_persistence.py app/test_voice_ws.py engine_gateway/test_gateway_model.py app/test_phase3_artifact_checker.py app/test_dataset_export.py -q</code> 40 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>더 큰 <code>finalize_turn</code>/<code>SessionRepository</code> 경계, 장기적으로는 세션 저장소 인터페이스 SSOT화가 남아 있다.</p></div></div>
</article>
<article class="scard" data-status="doing" data-cat="리팩터·정리" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">REFACTOR 2 4차 DONE</span><span class="scard-mid"><span class="scard-title">백엔드 Pydantic ↔ 프론트 <code>lib/api.ts</code> 계약 SSOT화</span><span class="scard-sum">OpenAPI→<code>api.gen.ts</code> 생성 + CI stale 체크 게이트. 세션 계열 수기 DTO도 생성 타입 alias로 축소.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p><code>apps/api/scripts/export-openapi.py</code><code>apps/web/scripts/generate-api-types.mjs</code>로 FastAPI OpenAPI를 <code>apps/web/src/lib/api.gen.ts</code>에 생성한다. <code>npm run check:api-types</code>가 재생성 누락을 잡는다. <code>MeResponse</code>, <code>AuthConfigResponse</code>, <code>ConsentResponse</code>, <code>PersonaSummary</code>, <code>PersonaReviewSummary</code>, admin/user/engine DTO, review leaf DTO, <code>CrisisResourceResponse</code>, teacher safety/growth leaf DTO, <code>LearnerSessionsResponse</code>, <code>SessionReviewResponse</code>, worksheet aggregate, <code>TeacherDashboardResponse</code>, <code>TeacherSessionSummary</code>, <code>TeacherLearnerGrowth</code>에 이어 <code>SessionStartResponse</code>, <code>SessionDetailResponse</code>, <code>SessionDetailTurn</code><code>ApiSchema&lt;...&gt;</code> alias로 전환했다. 세션 응답 stage는 backend <code>StageLabel</code> enum으로 고정해 OpenAPI가 <code>라포|탐색|개입|정리</code> union을 생성한다. generated optional/default 차이는 화면 form state, notification default helper, 배열 렌더링 fallback으로 흡수한다. <code>.github/workflows/api-contract.yml</code>은 PR/master push에서 같은 stale check를 실행해 OpenAPI type drift를 CI에서 막는다.</p></div><div class="kv"><b>검증</b><p><code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed. <code>python -m pytest app/test_session_turn_persistence.py -q</code> 16 passed. 영향 범위 E2E: <code>learner.spec.ts</code> + <code>session-review.spec.ts</code> + <code>teacher.spec.ts</code> + <code>layout-visual-gate.spec.ts</code> 32 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>평가 조회·리뷰 도식·교수 콘솔 등 아직 자유 문자열로 남긴 stage-like 필드는 각 도메인 의미를 확인한 뒤 별도 축소한다.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="인프라·스택·RAG" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">SEO/GEO 공유</span><span class="scard-mid"><span class="scard-title">검색 메타 + 회기 리뷰 URL 공유 카드</span><span class="scard-sum">robots/sitemap/llms + OG/JSON-LD. 종료 회기는 토큰 공유 URL로 교수자에게 요약·썸네일 전달.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p><code>apps/web/index.html</code>에 canonical, description, Open Graph/Twitter Card, JSON-LD(WebApplication)를 추가했고, <code>public/robots.txt</code>, <code>sitemap.xml</code>, <code>llms.txt</code>로 공개 사이트와 AI 검색용 설명을 정리했다. 인증 내부 경로는 robots에서 차단한다. 학습자 회기 리뷰는 종료 후 <code>POST /sessions/{id}/share</code>로 공개 토큰을 만들고, <code>GET /share/session/{token}</code>이 서버 렌더 HTML/OG/JSON-LD를 반환한다. <code>app.session_share_link</code>에는 토큰 해시와 sanitized preview payload만 저장하며 원문 축어록·학습자 식별자는 저장하지 않는다.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_session_share.py app/test_session_turn_persistence.py -q</code> 21 passed, <code>python -m pytest app/ -q</code> 145 passed, <code>python -m pytest engine_gateway/ -q</code> 9 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed.</p></div><div class="kv k-warn"><b>판정</b><p>공개 제품 페이지는 색인 가능, 회기 공유 페이지는 교수자 전달용 unfurl 카드라 <code>noindex</code>와 API robots <code>Disallow: /share/</code>를 유지한다. 실제 공개 도메인 unfurl 검증은 배포 후 1회 필요하다.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="리팩터·정리" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">REFACTOR 3 일부</span><span class="scard-mid"><span class="scard-title">죽은 확장점/계약 정리 — LogHook·tier 제거 적용</span><span class="scard-sum">caller 0 타입·분기·export 제거. 잔여는 패치 문서에 스테이징. 전체 API 119.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p><code>LogHook</code>/<code>log_hook</code>(caller 0) 타입·파라미터·분기·export 제거 · 게이트웨이/엔진클라 <code>tier</code> dead-contract 제거(write-only, gateway는 model만 사용). 잔여(RMS 힌트·Live2D 고아 자산·파라미터객체 init_state/prepare_turn/upsert)는 <code>docs/ops/refactor-rag-patches-2026-06-26.{md,json}</code>에 스테이징(단계 적용).</p></div><div class="kv"><b>참고</b><p>일괄 적용 대신 라이브 스택 보호 위해 그룹별 적용+pytest 검증.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="인프라·스택·RAG" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">RAG 활성화</span><span class="scard-mid"><span class="scard-title">"좋은 RAG" 미배선 → 라이브 활성화 + 성능 회귀 수정</span><span class="scard-sum">BGE-M3 핀+배선, 임베더 동기로드 블로킹을 백그라운드 task로. E2E 27/27·11/11·4/4.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>임베더 BGE-M3 검증+버전핀(1.4.0 dtype버그→1.2.11/transformers 4.44.2). <code>embed_query</code>를 asyncio.to_thread로 offload. 턴 파이프라인 배선: <code>retrieve_persona_memory</code>→episodic recall, <code>search_kb</code>→kb_behavior_cues. KB seed 3청크 적재. rag.py latent 버그 3건 수정. GraphRAG는 2차 보류.</p></div><div class="kv"><b>성능 회귀 수정</b><p>최초 RAG 배선이 회기 시작/턴에 임베더 로드(~7-13s)를 <b>동기 await</b><code>POST /sessions</code>가 막힘 → session-layout E2E 4건 타임아웃. 수정: 회상·KB 회수를 <code>_warm_rag_caches</code> <b>백그라운드 task</b>로 옮기고 턴은 캐시만 읽음(비차단). 이번 동의 게이트 E2E 재검증 중 여러 세션 warm task가 BGE-M3를 동시에 지연 로드해 API health/dev-login timeout이 반복된 문제는 embedder load/encode <code>RLock</code>과 warm semaphore 1개로 안정화했다. 재검증: session-layout 4/4 + <b>핵심 E2E 27/27·11/11·아바타 4/4</b>, 최신 layout/session 조합 <b>11/11</b> 통과.</p></div><div class="kv"><b>시각 게이트</b><p>admin 1280 input "text-clipped-x" 1건은 폼 input 값 스크롤(설계상 정상) 오탐 — form 컨트롤을 text-clip 판정에서 제외 → <b>게이트 7/7</b>(실 결함 검사 유지).</p></div></div>
</article>
</div>
</div>
</section>
<!-- ░░ DEEP / FOLDED REFERENCE ░░ -->
<section class="block" id="deep">
<div class="sec-head">
<div>
<h2>심층 상세 · 검증 로그</h2>
<p class="sub">로드맵 단계표, 단계별 상세 작업·검증 명령 표, 이번 세션 내러티브. 기본 접힘 — 필요할 때만 펼쳐서 본다.</p>
</div>
</div>
<details class="fold" id="fold-roadmap">
<summary><span class="fcaret" aria-hidden="true"></span>전체 개발 로드맵 (5단계)<span class="fmeta">MASTERPLAN + REVISIONS</span></summary>
<div class="fold-inner">
<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> 대부분 완료, 공개 <code>/turn</code> 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">정정 기준: <code>MASTERPLAN</code> 원본의 IRB 신청/승인 게이트는 <code>MASTERPLAN_REVISIONS</code> 기준으로 제거한다. Phase 2는 2a(3역할/OAuth/페르소나)와 2b(음성 캐스케이드)로 분리하고, Phase 1 DoD에는 얇은 음성 왕복 1턴 PoC를 포함한다.</div>
</div>
</details>
<details class="fold" id="fold-tabs">
<summary><span class="fcaret" aria-hidden="true"></span>단계별 상세 작업 · 검증 명령 표<span class="fmeta">P0~결정 / 검증 50+행</span></summary>
<div class="fold-inner">
<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 실제 <code>/turn</code> proof 확보</b><p>로그인 가능한 계정으로 storageState를 저장한 뒤 public-auth-turn E2E를 실행한다.</p></div><div><b>산출물</b><p><code>apps/web/node_modules/.tmp/public-auth.json</code>, public <code>/turn</code> 성공 로그</p></div><div><b>검증</b><p><code>E2E_PUBLIC_AUTH=1</code> + <code>chromium-public-auth</code> 1회 통과</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>engine config 운영값 재확인</b><p><code>GET /admin/engine-config</code>로 운영값을 직접 확인했다. <code>engine_mode=claude_cli</code>, <code>engine_url=http://127.0.0.1:9099</code>, <code>model=gateway-default</code>, <code>durable=true</code>, <code>source=database</code>로 의도값과 일치한다.</p></div><div><b>산출물</b><p>config API 응답(durable, db source)</p></div><div><b>검증</b><p><code>claude_cli / http://127.0.0.1:9099 / gateway-default</code> 의도값 대조 완료</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 런타임, 공개 <code>/live2d/*</code> 캐시 잔여 접근을 차단했다.</p></div><div><b>산출물</b><p><code>apps/web/functions/live2d/[[path]].js</code>, P1 PSB raster avatar + SVG fallback</p></div><div><b>검증</b><p>운영 <code>/live2d/mao/*</code>, Cubism core <code>404</code></p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>프로세스 난립 정리</b><p>운영 확인용 프로세스와 로컬/Tailnet 검증용 dev 프로세스를 의도적으로 분리해 유지한다.</p></div><div><b>산출물</b><p>9099 engine, 8001 prod API, 8000/8010 dev API, 5173 web, cloudflared tunnel 1개</p></div><div><b>검증</b><p><code>Get-NetTCPConnection</code>에서 대상 포트별 listener 확인</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><code>docs/ops/hanshin-data-governance-gate.md</code>, 공문 질의 항목, 동의 범위 체크리스트, 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 <code>24c90784</code>가 현재 dirty local source dist로 배포됐고, 운영 반영 전 남은 핵심 증거는 공개 Google OAuth 실제 <code>/turn</code> 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><code>session-persistence</code>, <code>teacher</code>, <code>voice</code><code>/api/personas</code>에서 실제 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><code>ENVIRONMENT</code>, Google OAuth vars, <code>CORS_ORIGINS</code>, <code>OPENAI_BASE_URL</code>을 API 컨테이너에 명시했다.</p></div><div><b>산출물</b><p>compose 환경 주입 정리</p></div><div><b>검증</b><p><code>docker compose -f infra\docker-compose.yml --env-file infra\.env.example config --quiet</code> 더미 필수 env 세트로 통과</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><code>db-persistence</code>는 전역 engine config를 바꾸므로 <code>@single-run</code>으로 분리했다.</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><code>docs/HANDOFF.md</code>, <code>docs/dev_dashboard.html</code>, <code>apps/web/e2e/README.md</code>를 최신 상태로 맞췄다.</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><code>cloudflared</code> ingress를 <code>127.0.0.1:8001</code> prod API로 돌렸고 public readiness gate가 통과했다.</p></div><div><b>산출물</b><p>prod API 8001, tunnel config</p></div><div><b>검증</b><p><code>https://api-vignette.chanpaca.net/health</code> 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><code>scripts/start-public-runtime.ps1</code><code>scripts/stop-public-runtime.ps1</code>로 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><code>/personas</code>는 이제 비로그인 401이다. 로컬과 공개 API 모두 직접 확인했고 E2E 회귀를 추가했다.</p></div><div><b>산출물</b><p>authenticated persona catalog boundary</p></div><div><b>검증</b><p>local/public unauth <code>GET /personas</code> 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 웹에서 <code>/api/auth/config</code>가 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개를 <code>기존 회기</code> 빈 상태와 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><code>127.0.0.1:5175</code> web, <code>8000</code> API, <code>9099</code> engine을 재기동하고 dev-login 서버 세션과 <code>/learn</code> 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><code>POST /sessions/{id}/stream</code> 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><code>/learn/session/:code</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 런타임을 공개 배포물에서 제거했다. 세션 화면은 P1 PSB raster avatar와 SVG fallback을 렌더링한다.</p></div><div><b>산출물</b><p>P1 PSB raster avatar, SVG fallback, legacy <code>/live2d/*</code> 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 <code>token/done/error</code>와 generate cost 계약은 회귀화했다. 이번 패스에서 <code>/v1/stream</code> route 자체가 <code>event: token</code>, <code>event: done</code>, <code>event: error</code> 프레임을 내는지 fake session으로 직접 고정했다. claude_cli와 Messages API 폴백 동일성 검증은 라이브 키/환경 의존이라 남아 있다.</p></div><div><b>산출물</b><p><code>engine_gateway/test_gateway_model.py</code> stream frame contract tests</p></div><div><b>검증</b><p><code>pytest engine_gateway/test_gateway_model.py -q</code> 9 passed; focused backend 묶음 40 passed.</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>상주 <code>claude -p</code> 엔진풀 실증</b><p><code>session_id</code>가 살아 있는 gateway session을 재사용하고 missing session은 ephemeral로 닫히는 구조를 회귀화했고, live 게이트웨이(9099)로 실측까지 마쳤다. <code>probe-engine-gateway.py</code>가 단일 session_id로 2회 reused stream을 돌려 TTFT 16674199ms, cost 누적 0.068→0.123(turns 1→2), engine=claude_p, model=opus-4-8을 측정했다.</p></div><div><b>산출물</b><p><code>engine_gateway/test_gateway_model.py</code>, <code>scripts/probe-engine-gateway.py</code>, 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-done">DONE</span></div><div><b>마스킹 게이트 + LLM call audit</b><p>current turn, recall, pinned fact, recent turns가 Presidio/가명처리 후에만 engine request로 들어가게 막았다. 외부 LLM 호출은 <code>audit.llm_call_log</code>에 provider/model/token/cost/inference_geo/latency metadata만 남기고 prompt/completion 본문은 저장하지 않는다.</p></div><div><b>산출물</b><p><code>app/test_orchestrator_masking.py</code>, <code>app/test_evaluation_persistence.py</code>, <code>session_persistence.record_llm_call_audit</code></p></div><div><b>검증</b><p>phone/email/RRN raw 값이 generate/stream payload에 없음, 감사 payload 본문 미포함, 관련 회귀 27 passed, 전체 API 최신 130 passed, 로컬 live <code>/turn</code> smoke 후 <code>audit.llm_call_log</code> 8→11</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><code>e2e/session-mvp.spec.ts</code>, turn telemetry 저장 경로</p></div><div><b>검증</b><p><code>chromium-single-run</code> 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><code>app/test_state_machine_resistance.py</code></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 <code>getUserMedia</code>/<code>MediaRecorder</code>로 실제 <code>/voice/ws</code>를 열고 audio_start, binary chunk, audio_end, transcript, reply, TTS 재생 상태까지 검증했다. 물리 마이크, 공개 WSS, live STT/TTS provider 증거는 아직 별도다.</p></div><div><b>산출물</b><p><code>apps/web/e2e/voice-success.spec.ts</code>, controlled OpenAI/engine providers, browser voice UI probe</p></div><div><b>검증</b><p><code>npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run</code> 2 passed</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 계약을 회귀화했다. OAuth state는 HMAC 서명 토큰과 HttpOnly state cookie를 함께 검증해 API 재시작 뒤 콜백이 <code>invalid_state</code>로 떨어지지 않으면서도 브라우저 시작 흐름에 묶인다. Google provider error는 <code>access_denied</code>/<code>provider_error</code>로 분리하고 로그인 화면에 reason code를 표시한다. 공개 Google OAuth 실제 <code>/turn</code> proof는 P0 blocker로 남아 있다.</p></div><div><b>산출물</b><p><code>app/test_auth_providers.py</code>, cookie-bound signed PKCE state, OAuth callback cookie path, login reason UI</p></div><div><b>검증</b><p>auth URL secret 미노출, browser token 미저장, dev/prod cookie 속성 분리, state cookie 불일치 차단, 재시작 후 signed state 콜백 복구, provider error reason 분리</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 <code>vignette_app</code> 역할)로 RLS/audit smoke를 실행해 운영 증거를 확보했다: learner 세션/턴 격리, teacher cohort 스코프, teacher/admin read_session audit insert가 실제 정책 하에서 PASS. 이번 패스에서 역할(role)과 관리자 페이지 진입권(<code>admin_access</code>)을 분리했고, <code>AUTH_SUPER_ADMIN_EMAILS</code> 슈퍼 관리자만 관리자 권한을 부여·회수할 수 있게 했다. 슈퍼 관리자는 학습자/교수자 라우트도 통과할 수 있지만, 일반 <code>admin_access</code>는 관리자 콘솔 진입권으로만 쓰인다.</p></div><div><b>산출물</b><p><code>app_user.admin_access</code>, <code>require_admin_access()</code>, <code>/admin/users</code> 권한 위임 UI, <code>app/test_rbac_idor.py</code>, <code>audit.audit_log</code> read_session insert, <code>scripts/check-postgres-rls-audit.py</code>(live PASS)</p></div><div><b>검증</b><p>타인 <code>session_id</code> 직접 입력 403(unit), live RLS smoke 5 checks PASS, 관리자 권한 위임 회귀 <code>app/test_auth_providers.py app/test_admin_ops.py</code> 32 passed</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b><code>visible_to</code>와 인간 권한 레이어 분리</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에만 보인다.</p></div><div><b>산출물</b><p><code>TurnRecord.visible_to</code>, learner/client/evaluator filters, <code>docs/ops/postgres-rls-audit-smoke.md</code></p></div><div><b>검증</b><p>evaluator-only turn 숨김 unit proof + live RLS smoke <code>visible_to.evaluator_only_hidden</code> 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→정상 빈상태)했다.</p></div><div><b>산출물</b><p><code>/personas/review</code>, <code>POST /personas/review/{persona_id}</code>, professor review panel</p></div><div><b>검증</b><p><code>app.test_persona_review</code> 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 상태를 <code>/auth/config</code>에 노출하고, Redirect-binding AuthnRequest와 dev-only unsigned fixture ACS를 backend proof 수준으로 구현했다. 서명 검증이 필요한 운영 SAML ACS는 아직 미구현이며 fingerprint 설정 시 거부한다.</p></div><div><b>산출물</b><p><code>AUTH_SAML_ENABLED</code>, <code>SAML_SP_ENTITY_ID</code>, <code>SAML_SSO_URL</code>, RelayState store, <code>/auth/saml/acs</code> 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><code>/voice/ws</code> backend contract를 fake WebSocket으로 회귀화했고, Session UI의 MediaRecorder 기반 브라우저 경로도 synthetic audio로 한 턴 통과시켰다. 실제 AudioWorklet 전환, public WSS, 50분 양방향 실측은 남아 있다.</p></div><div><b>산출물</b><p><code>app/test_voice_ws.py</code>, <code>apps/web/e2e/voice-success.spec.ts</code>, 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><code>assess_end_of_turn</code>, <code>EndOfTurnDecision</code>, 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-done">DONE</span></div><div><b>OpenAI TTS voice preset 추상화</b><p>P1/P2/P3 persona voice preset, invalid preset fallback, speed clamp, instructions payload 계약을 회귀화했고, <code>app.persona_voice_map</code>을 live <code>/voice/ws</code> TTS 선택에 연결했다. 명시 preset은 DB map보다 우선하고, OpenAI가 아닌 provider row는 기존 fallback으로 안전하게 흡수한다.</p></div><div><b>산출물</b><p><code>resolve_voice</code>, <code>resolve_voice_from_map</code>, <code>build_tts_payload</code>, <code>get_session_voice_map</code>, seed voice map row, dev runtime schema guard</p></div><div><b>검증</b><p><code>pytest app/test_runtime_policy.py app/test_persona_review.py app/test_voice_service.py app/test_voice_ws.py -q</code> 75 passed; full API 175 passed + gateway 9 passed.</p></div></div>
<div class="task-row"><div><span class="task-status s-plan">POC</span></div><div><b>P1 서연 무참조 synthetic voice 샘플</b><p>Higgs v3 로컬 런타임에서 실존 reference voice 없이 synthetic seed를 먼저 만들고, 그 seed만 재참조해 우울·피로·불안·라포·회복감 5개 변주를 생성했다. 개발용 <code>VIGNETTE_VOICE_POC_SAMPLE_TTS</code> 플래그로 <code>/voice/ws</code> P1 sample TTS provider에도 연결했다.</p></div><div><b>산출물</b><p><code>docs/voice-art/bimec-persona-voice-poc-2026-06-27.md</code>, <code>docs/voice-art/p1-seoyeon-higgs-v3-20260627/manifest.json</code>, <code>apps/api/app/services/voice.py</code></p></div><div><b>검증</b><p>mp3/wav 6개(seed+5) 생성, 24kHz, 7.72~11.68초. live <code>/voice/ws</code> text_turn smoke: provider <code>p1-sample-poc</code>, binary 20 chunks/78,573 bytes. 샘플 provider는 TTS override만 담당하고 마이크/STT는 OpenAI 키가 필요하다. 프로덕션 탑재는 모델 라이선스·권리정책 결정 전 금지.</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><code>turns</code> nullable 확장, voice route 메타 산출, <code>ReviewNonverbalEvent</code>, <code>SessionReview</code> 칩 UI</p></div><div><b>검증</b><p>오디오 STT→엔진→TTS 더블 테스트에서 voice turn log에 메타 저장; session review E2E 1 passed; backend voice/session 17 passed</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><code>docs/decisions/voice-s2s-poc.md</code></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를 문서화했다. checker는 CSV enum(<code>withdrawal_state</code>, <code>completion_state</code>, <code>timepoint</code>, withdrawal <code>scope/status</code>)을 검증한다. 실제 참여자 완주 증거는 아직 필요하다.</p></div><div><b>산출물</b><p><code>docs/phase3/pilot-runbook.md</code>, evidence root layout, operator gates, <code>scripts/check-phase3-artifacts.py</code></p></div><div><b>검증</b><p><code>python scripts/check-phase3-artifacts.py --help</code>, <code>pytest app/test_phase3_artifact_checker.py -q</code> 5 passed. 실제 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를 고정했다. checker는 KPI report top-level keys와 metric 필수 필드(<code>value/threshold/pass/method/source_files</code> 등)를 error로 검증한다. 실제 수치 충족 여부는 파일럿 데이터 이후 판단한다.</p></div><div><b>산출물</b><p><code>docs/phase3/kpi-measurement-plan.md</code>, KPI report schema, checker tests</p></div><div><b>검증</b><p>checker가 <code>prepost_measures</code>, <code>sus_responses</code>, <code>kpi_report.json</code> evidence와 metric shape를 요구.</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>재귀학습 데이터셋 1차 산출</b><p>masked-text JSONL dry-run exporter, PII scan, κ/ICC 계산, approved export 차단 게이트를 구현했다. 이번 패스에서 artifact checker도 approved manifest false-positive를 막도록 강화했다. 실제 approved export와 데이터 steward 검수는 남아 있다.</p></div><div><b>산출물</b><p><code>scripts/export-recursive-dataset.py</code>, <code>app.services.dataset_export</code>, <code>docs/phase3/dataset-export-manifest.md</code>, <code>app/test_phase3_artifact_checker.py</code></p></div><div><b>검증</b><p><code>pytest app/test_dataset_export.py app/test_phase3_artifact_checker.py -q</code> 10 passed, DB dry-run 5 rows → <code>technical_dry_run</code> manifest + sha256 생성, approved manifest는 PII pass·κ/ICC·withdrawn exclusion·consent scope·file sha256을 요구.</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><code>docs/phase3/privacy-consent-audit.md</code>, 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/dev OK</span><span>Compose smoke: OK</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>커스텀 도메인의 로그인 페이지가 최신 production build asset을 정상 응답한다. 2026-06-28 11:05 KST 기준 Cloudflare Pages production deploy는 <code>24c90784</code>다.</p></div><div><b>산출물</b><p><code>https://vignette.chanpaca.net/login</code>, <code>assets/index-HjXyC0JT.js</code>, <code>assets/index-C8JKXhNP.css</code></p></div><div><b>검증</b><p><code>Invoke-WebRequest https://vignette.chanpaca.net/login</code> → 200 + latest assets</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>공개/API 런타임 health</b><p>터널은 prod API 8001을 보고, API는 DB와 engine을 모두 확인한다. 로컬 dev API 8010도 같은 엔진/DB 경로로 정상이다.</p></div><div><b>산출물</b><p><code>environment:"prod"</code>, <code>environment:"dev"</code>, <code>db:true</code>, <code>engine:true</code>, <code>engine_mode:"claude_cli"</code></p></div><div><b>검증</b><p><code>https://api-vignette.chanpaca.net/health</code>, <code>http://127.0.0.1:8001/health</code>, <code>http://127.0.0.1:8010/health</code> 모두 OK</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>Tailscale Tailnet 런타임</b><p>로컬 web/api/engine을 Tailscale Serve로 묶어 같은 Tailnet PC·모바일에서 HTTPS로 접근한다. Vite proxy와 API 포트가 어긋나지 않도록 <code>dev-up.ps1</code>에서 <code>VITE_API_PROXY_TARGET</code><code>ApiPort</code>와 동기화한다. Tailnet dev-origin에서는 공개 OAuth callback으로 세션이 섞이지 않게 dev-login을 기본 검증 경로로 둔다.</p></div><div><b>산출물</b><p><code>https://alpaca-home.taile93291.ts.net</code>, <code>scripts/start-tailscale-runtime.ps1</code>, <code>VITE_ALLOWED_HOSTS</code>, <code>vite.config.ts</code>, <code>auth.py</code></p></div><div><b>검증</b><p><code>/login</code> 200, <code>/api/auth/config</code> 200, <code>redirect_uri=https://api-vignette.chanpaca.net/auth/callback</code>, <code>dev_login_enabled:true</code>. Vite host-header block 재현 안 됨.</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>Docker compose 패키징 smoke</b><p>더미 운영 env 기준 fresh compose 기동을 통과했다. API 컨테이너 import 실패, 구현 없는 RAG 사이드카, 로컬 <code>apps/api/.env</code> 이미지 혼입, startup owner-DDL, 빈 <code>admin_engine_config</code> 행 때문에 prod가 죽던 경로를 수정했다. 루트 계산은 <code>app.paths.repo_root()</code>로 모았고, backend requirements는 exact pin으로 고정했다.</p></div><div><b>운영 주의</b><p>패키징은 OK지만 실제 이식 전에는 배포지 <code>infra/.env</code><code>APP_DB_PASSWORD</code>, OAuth client id/secret, <code>OPENAI_API_KEY</code>, <code>SESSION_SECRET</code> 등 소유자 비밀값을 채워야 한다. <code>data/kb</code> live-coaching source pack은 commit 또는 같은 경로 provision이 필요하다. 현재 로컬 env의 <code>ENGINE_MODE=claude_p</code>와 prod 금지 샘플 TTS 플래그는 운영값으로 쓰면 안 된다.</p></div><div><b>검증</b><p><code>docker compose -f infra/docker-compose.yml config --quiet</code>, API/Web image build, API image <code>.env*</code> 미포함, prod import smoke, fresh <code>docker compose up -d --build</code><code>/api/health</code> 200, preflight <code>OK requirements_pinned/live_coach_kb/env_file</code>.</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">BLOCKED</span></div><div><b>공개 vnet.18ka.net 런타임</b><p><code>vnet.18ka.net</code><code>api-vnet.18ka.net</code>을 정식 공개 도메인으로 허용하도록 코드 경로는 보강했다.</p></div><div><b>산출물</b><p><code>FRONTEND_ORIGIN_MAP</code>, 공개 host별 API 매핑, <code>scripts/start-public-runtime.ps1</code> vnet ingress 옵션</p></div><div><b>블로커</b><p>현재 DNS 미해결, Cloudflare credential의 18ka.net zone 권한 불일치, Google OAuth redirect URI 등록 필요.</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>서버 정리 상태</b><p>운영 API와 로컬/Tailnet 테스트 API를 구분해 유지한다.</p></div><div><b>산출물</b><p>8001 prod API, 8000 local API, 8010 Tailnet API, 5173 web, 9099 engine, 20241 cloudflared</p></div><div><b>검증</b><p><code>Get-NetTCPConnection</code> 대상 포트 확인</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를 추가했다. 아직 DNS가 없는 <code>api-vnet.18ka.net</code>은 기본 watchdog 검사에서 제외해 불필요한 restart loop를 막고, 실제 개통 후 <code>-AdditionalPublicHealthUrls</code>로 명시 추가한다. 실제 재부팅 후 public <code>/turn</code> smoke는 남아 있다.</p></div><div><b>산출물</b><p><code>watch-public-runtime.ps1</code>, <code>install-public-runtime-task.ps1</code>, ops runbook</p></div><div><b>검증</b><p>PowerShell parser OK, check-only watchdog healthy: engine/api/web-preview/cloudflared/public-api</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으로 닫는다.</p></div><div><b>산출물</b><p><code>docs/ops/layout-redesign-handoff-2026-06-26.md</code>, page-by-page redesign patches, <code>apps/web/e2e/layout-visual-gate.spec.ts</code></p></div><div><b>검증</b><p><code>npm run typecheck</code> 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장 풀페이지 스크린샷을 남긴다. 화면별 1에이전트 병렬 시각 리뷰로 49장 직접 판독 → 6개 병렬 수정 에이전트(파일 비중첩)가 결함 수정 → 화면별 적대적 재검수로 해소·무회귀 확정.</p></div><div><b>산출물</b><p><code>apps/web/e2e/layout-visual-gate.spec.ts</code>, <code>node_modules/.tmp/layout-gate/*.png</code> 49장, 2× workflow(review/verify)·6× fix agent</p></div><div><b>검증</b><p>게이트 7 passed(재스크린샷). 해소된 핵심 결함: 학습자홈 1280/1440 3열 가운데 컬럼 붕괴(critical → 2열 분기 상향 + word-break:keep-all), 교수 'API 404' raw 배너 제거, 리뷰 빈상태 위계 역전, 관리자 스켈레톤, 설정 와이드 좌측 데드존 제거, 세션 모바일 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개 병렬 폴리시 에이전트로 처리했다. 학습자홈 헬퍼문구 고아 글자, 세션 1024 일시정지 아이콘 정리 + 보내기 버튼 대비 강화, 설정 2x2 칩 행 높이 균일, 교수 검토 큐 카드 갭 제거, 관리자 2열 높이 동기화를 적용했다.</p></div><div><b>산출물</b><p>page-by-page polish patches(LearnerHome/Session/Settings/Professor/Admin)</p></div><div><b>검증</b><p><code>npm run typecheck</code> 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-done">DONE</span></div><div><b>회기 아카이브 저장/복원 API</b><p><code>/learn/history</code><code>보관됨</code>을 실제 학습자별 저장 상태로 연결했다. 종료 회기는 <code>POST /sessions/{id}/archive</code>/<code>restore</code>로 보관·복원하고, <code>app.session_archive_state</code>는 보기 상태만 저장한다. 진행 중 회기는 보관하지 못하며, 보관은 transcript/review/share/audit evidence를 삭제하지 않는다.</p></div><div><b>산출물</b><p><code>SessionArchiveResponse</code>, <code>LearnerSessionSummary.archived</code>, <code>LearnerDashboardOverview.archived_sessions</code>, <code>session_archive_state</code> RLS, <code>LearnerHome</code> 보관/복원 버튼</p></div><div><b>검증</b><p><code>python -B -m pytest app/ -q</code> 168 passed, <code>python -B -m pytest engine_gateway/ -q</code> 9 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1</code> 6 passed.</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>세션 종료 UX와 다크 테마 SSOT</b><p>세션 하단의 드래그형 종료 컴포넌트를 명시 확인 다이얼로그로 바꿔 모바일 오발동과 제스처 실패를 줄였다. Topbar/Settings의 theme 저장·초기화는 <code>lib/theme.ts</code> 단일 경로로 합치고, 저장값이 없으면 dark 기본값을 앱 부팅 전에 적용한다. API 기본 preference <code>system</code>은 Settings에서 light로 오해하지 않고 현재 초기 테마를 따른다. P1 실제 세션 기본 아바타는 사용자 제공 PSD 기반 <code>seoyeon-live2d-psd-v2</code>이며, sad 표정에서 울상 눈썹·우는 입·눈물 파츠를 합성한다. active session 스테이지 상태 배지는 아바타와 겹치지 않는 3행 구조로 정리했다.</p></div><div><b>산출물</b><p><code>Session.tsx</code>, <code>RasterBust.tsx</code>, <code>AvatarPreview.tsx</code>, <code>session.css</code>, <code>apps/web/public/avatar/seoyeon-live2d-psd-v2/parts/*.png</code>, <code>docs/avatar-art/seoyeon/live2d-psd-v2/</code>, <code>avatar-expression.spec.ts</code>, <code>session-layout.spec.ts</code></p></div><div><b>검증</b><p>PSD 파츠 추출 53개. <code>preview-sad.png</code>에서 눈물/우는 입/울상 눈썹 확인. 기존 시각 회귀 게이트는 후속 전체 검증에서 재실행한다.</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>잔여(비차단) — 공용 셸 단일 항목 → 처리</b><p>축소 사이드바 세로 구분선이 본문 전체 높이까지 닿지 않던 건을 <code>components/shell/shell.css</code>에서 처리했다. <code>.vg-nav border-right</code> 제거 후 <code>.vg-shell__body</code> 컨테이너 배경 하어라인으로 본문 그리드 전체 높이 구분선을 그리고, 그리드·구분선 폭을 <code>--nav-cur</code>로 동기화. learner-home 로딩 스켈레톤 밀도도 실제 카드 구조 모사로 보강했다.</p></div><div><b>판정</b><p>구현 완료. 검증: <code>npm run typecheck</code> PASS + <code>vite build</code> PASS. 전 페이지 시각 회귀 게이트는 web+api+DB 스택으로 이 워크스테이션 미실행 — 스택 가용 시 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>Full Playwright E2E</td><td><code>PLAYWRIGHT_PORT=5174 npm run e2e</code></td><td>113 passed; desktop/mobile 병렬 + <code>@single-run</code> DB 영속화·음성 성공 경로까지 전체 통과.</td></tr>
<tr><td>API typegen SSOT</td><td><code>npm run check:api-types</code></td><td>Passed; FastAPI OpenAPI → <code>src/lib/api.gen.ts</code> stale check</td></tr>
<tr><td>Contract SSOT aggregate DTO</td><td><code>npm run typecheck</code> / <code>npm run build</code> / <code>session persistence</code> / <code>learner + session-review + teacher + layout gate</code></td><td>Passed; learner sessions, session review/worksheet, teacher dashboard, session start/detail DTOs now use generated <code>ApiSchema</code> aliases with UI fallback. Stage responses are OpenAPI enum unions. Focused E2E 32 passed. <code>.github/workflows/api-contract.yml</code> now runs <code>npm run check:api-types</code> on API/Web contract changes.</td></tr>
<tr><td>SEO/GEO share cards</td><td><code>pytest app/test_session_share.py app/test_session_turn_persistence.py -q</code> / <code>npm run generate:api-types</code> / <code>npm run typecheck</code></td><td>21 passed; session share creates hashed-token public unfurl payload without raw transcript, revoked token returns 404, OpenAPI generated share DTOs, review screen share button typechecks. Static <code>robots.txt</code>/<code>sitemap.xml</code>/<code>llms.txt</code> added.</td></tr>
<tr><td>Backend pytest</td><td><code>python -m pytest app/ -q</code></td><td>145 passed</td></tr>
<tr><td>X2 evaluator model routing</td><td><code>python -m pytest app/test_evaluator_model_routing.py app/test_runtime_policy.py -q</code></td><td>23 passed; fast/deep evaluator model override and blank-default routing covered</td></tr>
<tr><td>C1 case worksheet</td><td><code>python -m pytest app/test_session_turn_persistence.py app/test_rbac_idor.py -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code> / local API smoke</td><td>25 passed; session review exposes transcript-grounded <code>caseWorksheet</code>, learner-edited <code>saved_by_learner</code> worksheet persists through <code>app.case_worksheet</code>, and evaluator-only turns stay hidden. Local smoke returned <code>saved_by_learner:local smoke saved worksheet</code>. Stage labels are normalized at response boundaries.</td></tr>
<tr><td>M3 auth claim mapping</td><td><code>python -m pytest app/test_auth_providers.py -q</code></td><td>22 passed; Google/SAML cohort maps, provider external_id, provider error reason covered</td></tr>
<tr><td>CrisisGate 109</td><td><code>python -m pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q</code></td><td>20 passed; real crisis stops before engine, returns 109 resource, safety event DB insert payload covered</td></tr>
<tr><td>Professor review workflow</td><td><code>python -m pytest app/test_rbac_idor.py app/test_teacher_dashboard.py -q</code> / <code>npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code></td><td>9 backend passed + 4 E2E passed; teacher dashboard exposes learner_growth/safety queues and review-pending/recent rows now open <code>/teach/session/:id/review</code> read-only with learner worksheet editing blocked for teachers.</td></tr>
<tr><td>Learner dashboard personalization</td><td><code>pytest app/test_learner_dashboard.py app/test_teacher_dashboard.py app/test_rbac_idor.py -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code></td><td>12 backend passed; <code>GET /sessions/dashboard</code> uses shared <code>session_metrics</code> and feeds LearnerHome overview/growth/persona_progress/recent_feedback/last-session recap. Focused learner/review/teacher/layout/session E2E evidence recorded in backlog.</td></tr>
<tr><td>Teacher review status</td><td><code>pytest app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_learner_dashboard.py -q</code> / <code>npm run typecheck</code> / <code>npm run build</code> / <code>teacher.spec.ts</code></td><td>12 backend passed + 4 E2E passed; <code>app.session_review_status</code>, teacher dashboard <code>review_status/review_note/reviewed_at</code>, and <code>PUT /teacher/sessions/{session_id}/review-status</code> are documented. 검토 큐 visible 회귀와 교수자 전용 review grid 영역도 테스트로 고정했다.</td></tr>
<tr><td>Persona authoring CRUD</td><td><code>python -m pytest app/test_persona_review.py -q</code></td><td>31 passed; teacher/admin draft create/read/update/review submit, approved persona revision clone, code-family archive delete, learner blocked, approved-only catalog maintained</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>9 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>8 tests OK; other learner 403, read_session audit, evaluator-only hidden, teacher session review read allowed while learner worksheet write remains 403</td></tr>
<tr><td>Admin access delegation</td><td><code>python -m pytest app/test_auth_providers.py app/test_admin_ops.py -q</code> / <code>npm run typecheck</code> / <code>npm run check:api-types</code></td><td>32 backend passed; <code>admin_access</code> and <code>super_admin</code> are exposed through auth/admin DTOs. Super admins can grant/revoke admin page access; configured super admins stay protected. Frontend role switching uses <code>canAccessRole</code>.</td></tr>
<tr><td>Admin usage persistence</td><td><code>python -m pytest app/test_admin_ops.py app/test_runtime_policy.py -q</code> / authenticated local public-API smoke</td><td>26 passed; <code>/admin/usage</code> returns 200 with <code>source=database</code>, <code>durable=true</code>. Cloudflare blocked raw Python public smoke with 1010, so app-level HTTP was verified against <code>127.0.0.1:8001</code> using the same prod process.</td></tr>
<tr><td>Turn evaluation SSOP</td><td><code>pytest app/test_session_turn_persistence.py app/test_evaluation_persistence.py app/test_voice_ws.py</code></td><td>20 tests OK; submit/stream/voice fast-loop evaluation normalized mapping + evaluator-only raw RLS + turn insert id 확인</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>20 tests OK; PKCE/state, cookie-bound signed state restart recovery, state cookie mismatch reject, opaque HttpOnly/Secure cookie, no browser tokens, SAML AuthnRequest/dev-only fixture ACS, dev/Tailnet OAuth guard, public-origin dev-login closed</td></tr>
<tr><td>H4 consent gate</td><td><code>pytest app/test_auth_providers.py app/test_session_turn_persistence.py app/test_persona_review.py app/test_voice_ws.py -q</code></td><td>64 passed; learner consent accept/withdraw, non-learner/unaccepted reject, no-consent session start 403 <code>consent_required</code>, voice dev persona consent guard, frontend generated API contract updated</td></tr>
<tr><td>Persona review workflow</td><td><code>app.test_persona_review</code></td><td>31 tests OK; approved-only catalog, teacher/admin queue, draft authoring, approved persona revision, archive audit, learner 403</td></tr>
<tr><td>Voice WS contract</td><td><code>app.test_voice_ws</code></td><td>8 tests OK; auth guard, audio_start/chunks/audio_end, text_turn, ping, max audio cap, DB persona voice map binding, explicit preset override</td></tr>
<tr><td>Voice preset/EOT</td><td><code>app.test_voice_service</code></td><td>14 tests OK; TTS payload, preset fallback, DB voice-map resolver, unsupported provider fallback, P1 sample TTS, 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; review <code>nonverbal</code> events exposed</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/session review UI E2E</td><td><code>npx playwright test e2e/teacher.spec.ts --project=chromium-single-run</code></td><td>2 passed; teacher approves pending persona and opens pending ended-session review detail 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> + <code>pytest app/test_phase3_artifact_checker.py -q</code></td><td>Passed; checker now enforces CSV enums, KPI metric required fields, approved export PII/agreement/consent/withdrawal/file-hash gates. Actual pilot evidence still external.</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 --commit-dirty=true</code></td><td><code>24c90784</code>, dirty local source deploy</td></tr>
<tr><td>Custom domain assets</td><td><code>https://vignette.chanpaca.net/login</code></td><td><code>index-HjXyC0JT.js</code>, <code>index-C8JKXhNP.css</code></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 template</td><td><code>docker compose -f infra\docker-compose.yml config --quiet</code></td><td>Template path is valid with dummy required env. API build context now uses repo root; stale <code>rag.server</code> sidecar removed.</td></tr>
<tr><td>Docker image smoke</td><td><code>docker build -f apps/api/Dockerfile .</code> / <code>docker run ... python -c "import app.main"</code> / <code>docker build -f apps/web/Dockerfile apps/web</code></td><td>API/Web image build and API import smoke pass after packaging cleanup. Web build uses npm lockfile and ignores host <code>node_modules</code>; API image excludes local <code>.env</code> files.</td></tr>
<tr><td>Deploy preflight</td><td><code>python scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets</code></td><td>Passed: exact-pinned API requirements, live coaching <code>data/kb</code> source pack, env template keys. DB mode can additionally check app-role DSN with <code>--require-app-role</code>.</td></tr>
<tr><td>Fresh compose smoke</td><td><code>docker compose -p vignette-packaging-smoke -f infra/docker-compose.yml up -d --build</code> + proxy <code>/api/health</code></td><td>Passed with dummy production-safe env: API healthy, DB healthy, web/proxy up, <code>http://localhost:18080/api/health</code> 200 with <code>db:true</code>, <code>engine:true</code>, <code>engine_mode:"claude_cli"</code>. Smoke volumes/network removed after run.</td></tr>
<tr><td>Actual deployment env</td><td><code>infra/.env</code> owner-secret fill-in</td><td>Remaining external step: deployment target must provide real <code>APP_DB_PASSWORD</code>, OAuth client id/secret, <code>OPENAI_API_KEY</code>, <code>SESSION_SECRET</code>, and production-safe engine/voice flags. Current local stray values such as <code>ENGINE_MODE=claude_p</code> and prod sample TTS must not be copied.</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_PORT=5174 npm run e2e</code></td><td>113 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 healthy. Default public checks exclude not-yet-live <code>api-vnet.18ka.net</code>; add future domains explicitly with <code>-AdditionalPublicHealthUrls</code>.</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>Public/local/Tailnet login recovery</td><td><code>https://vignette.chanpaca.net/login</code> / <code>https://api-vignette.chanpaca.net/health</code> / <code>https://alpaca-home.taile93291.ts.net/login</code></td><td>2026-06-28 current smoke: public login 200 (<code>assets/index-HjXyC0JT.js</code>, <code>assets/index-C8JKXhNP.css</code>), public API health prod/db/engine true, unauth public <code>/personas</code> 401, local 8001 prod health true, local 5174 public preview 200, cloudflared tunnel running. Vite host block/Cloudflare 502 not reproduced.</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>Session end + dark theme UX</td><td><code>npm run typecheck</code>, <code>npm run build</code>, <code>layout-visual-gate</code>, <code>session-layout</code>, <code>avatar-expression</code>, <code>settings</code>, <code>session-review</code></td><td>typecheck/build passed; Playwright 묶음 36 passed. P1 active session은 <code>rasterArtSet="seoyeon-live2d-psd-v2"</code>를 사용한다. sad 표정은 PSD 기반 눈물/우는 입/울상 눈썹 파츠를 렌더한다. Settings now treats API <code>system</code> preference as current initial theme instead of forcing light.</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; <code>npm run typecheck</code> 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>Engine gateway RSS (live)</td><td><code>docs/ops/engine-rss-smoke-2026-06-28.md</code></td><td>gateway session 1개, <code>health.sessions=1</code>; gateway PID 51592 RSS 16.2MB; resident <code>claude -p</code> child PID 51688 RSS 363.3MB / Private 628.8MB after one turn; plugin bun children RSS 178.6MB.</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>
<tr><td>Dashboard layout alignment</td><td><code>npm run typecheck</code> / <code>npm run build</code> / <code>layout-visual-gate</code> / <code>session-layout</code> / <code>learner+teacher+admin E2E</code></td><td>typecheck/build passed; layout gate 7 passed; session-layout desktop/mobile 8 passed; learner+teacher+admin chromium-desktop 17 passed. 대표 캡처는 <code>docs/ops/layout-research-2026-06-28/final-gate-*.png</code>.</td></tr>
<tr><td>Learner session archive</td><td><code>python -B -m pytest -p no:cacheprovider app/ -q</code> / <code>python -B -m pytest -p no:cacheprovider engine_gateway/ -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code> / <code>npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1</code></td><td>168 backend passed + 9 gateway passed; OpenAPI generated archive DTOs are current; learner history can archive/restore an ended session and keeps archived sessions out of the review queue while preserving records.</td></tr>
</tbody>
</table>
<div class="tab-subhead"><h4>다음 실행 명령</h4><p>남은 public OAuth <code>/turn</code> 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="source-note"><b>닫은 항목(live 증거):</b> ① engine config 운영값(<code>claude_cli / 9099 / gateway-default</code>, durable, db). ② 상주 엔진풀 probe — 단일 session_id 2회 reused stream, TTFT 16674199ms, cost 누적 0.068→0.123. ③ 상주 엔진풀 RSS — gateway session 1개에서 <code>claude -p</code> child RSS 363.3MB, gateway RSS 16.2MB. ④ Postgres RLS/audit smoke 5 checks PASS. ⑤ turn cost telemetry — <code>app.turns</code> client_ai 13행에 provider/model/cost_usd 실적재(합 $1.22). ⑥ 레이아웃 시각 게이트 7/7 + 적대적 재검수 7/7 accept.</div>
<div class="source-note"><b>환경 제약으로 이 세션에서 못 닫는 항목(정직 표기):</b> 공개 Google OAuth 실제 <code>/turn</code>(실제 계정 storageState 필요) · <code>infra/.env</code> compose 필수값(<code>APP_DB_PASSWORD</code>, <code>OAUTH_GOOGLE_CLIENT_ID</code>, <code>OAUTH_GOOGLE_CLIENT_SECRET</code>) · <code>vnet.18ka.net</code>/<code>api-vnet.18ka.net</code> 공개 런타임(DNS 미해결, Cloudflare 18ka.net zone 권한 불일치, Google OAuth redirect URI 등록 필요) · 한신대 데이터/SSO 거버넌스(owner/기관 written evidence 필요) · 음성 live STT/TTS provider·물리 마이크·공개 WSS 50분 · 저항엔진 openness 곡선 DB 실증 · claude_cli↔Messages API 폴백 동일성 · 재부팅 후 watchdog smoke · Phase 3 파일럿 게이트 · decisions 6건(owner 결정). 운영 원칙상 가짜 증거로 DONE 표기하지 않는다. 전체 추적: <code>docs/ops/backlog-2026-06-26.md</code>(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>결정 6개</span><span>조건부 GO 사유</span></div></div>
<div class="task-list">
<div class="task-row r3"><div><span class="task-status s-risk">DECIDE</span></div><div><b>s2s 2차 PoC 채택 여부</b><p>캐스케이드 1차는 확정. <code>voice-s2s-poc.md</code> 기준표에 따라 비교 리포트가 나온 뒤 유지/폐기를 정한다. 선택지: 캐스케이드만 / s2s 2차 PoC 유지.</p></div><div><b>영향</b><p>음성 아키텍처, 전송 비용, 로그 보존 방식. 람다 단독 결정 금지.</p></div></div>
<div class="task-row r3"><div><span class="task-status s-risk">DECIDE</span></div><div><b>서비스용 페르소나 음성 모델·권리 정책</b><p>Higgs v3 무참조 synthetic seed 방식으로 P1 샘플은 만들었고, 개발 플래그로 P1 sample TTS provider를 서비스에 연결했다. 하지만 실서비스는 모델 상업 라이선스와 reference voice 권리정책이 닫히기 전까지 탑재 금지다. 선택지: Higgs 라이선스 확보 / 동의 받은 ref만 사용 / 대체 TTS provider.</p></div><div><b>영향</b><p>법적 리스크, 음성 품질, 페르소나 확장 비용, 운영 GPU 요구량.</p></div></div>
<div class="task-row r3"><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>
<div class="task-row r3"><div><span class="task-status s-risk">DECIDE</span></div><div><b>재귀학습 fine-tuning 범위</b><p>1차는 few-shot 자동갱신만 할지, fine-tuning까지 포함할지 결정한다. 선택지: few-shot only / fine-tuning 포함.</p></div><div><b>영향</b><p>동의서, 데이터셋 품질, 운영 리스크</p></div></div>
<div class="task-row r3"><div><span class="task-status s-risk">DECIDE</span></div><div><b>자유연습 기본값</b><p>수정안은 기본 ON + 피드백 권장 배지 + 교수자 토글을 제안한다. 선택지: 기본 ON / 교수자 승인 후 ON.</p></div><div><b>영향</b><p>학습자 자율성, 평가 품질, 안전 정책</p></div></div>
<div class="task-row r3"><div><span class="task-status s-risk">DECIDE</span></div><div><b>추가 축어록 수급 규모/일정</b><p>n=1 시드의 한계를 해소하려면 추가 사례 수급 규모와 일정을 정해야 한다. 선택지: 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><code>MASTERPLAN</code>은 원본 보존 문서다.</li><li><code>MASTERPLAN_REVISIONS</code>가 Phase/IRB/음성 정정 기준이다.</li><li><code>dev_dashboard.html</code>은 팀장 판단용 현재판이다.</li></ul></div>
<div class="detail-block"><h4>운영 원칙</h4><ul><li>mock/demo 자산은 운영 번들에서 제거한다.</li><li>ignored runtime 파일은 커밋하지 않는다.</li><li>서버는 목적별 1개씩만 유지한다.</li><li><code>frontenddesign</code> 스킬이 있으면 먼저 쓰고, 없으면 <code>DESIGN_CONCEPT</code>를 SSOT로 따른다.</li><li>UX는 <code>docs/DESIGN_CONCEPT.md</code>를 기준으로 하고, fake 기록/통계는 금지한다.</li><li>OAuth secret은 출력하지 않고 환경 파일/인프라 설정에만 둔다.</li></ul></div>
</div>
</div>
</div>
</div>
</details>
<details class="fold" id="fold-narrative">
<summary><span class="fcaret" aria-hidden="true"></span>이번 세션 내러티브 (원문 보존)<span class="fmeta">team lead review</span></summary>
<div class="fold-inner">
<p style="color:var(--ink-2);font-size:13px;max-width:840px">
이번 세션은 seed persona 결합 제거, 비개발 fail-closed 설정, compose OAuth 주입, admin 모바일 overflow 수정, persona 인증 경계 보강, 전체 Playwright 재통과, 공개 API prod 전환까지 완료했다. 이번 이어받기에서는 내담자 AI 생성 턴의 provider/model/token/cost 텔레메트리를 <code>/turn</code>, SSE stream, 음성 생성 경로에서 DB <code>app.turns</code>까지 적재하도록 연결했고, gateway stream <code>done/error</code> 계약을 구조화했으며, P1 상담 시작→텍스트 stream→종료→리뷰 피드백 UI E2E와 저항 엔진 정량 회귀 테스트를 추가했다. 이어서 PII 마스킹 게이트 우회 차단, RBAC/IDOR·감사 insert 경로, 음성 paralinguistic 메타 저장, 공개 런타임 watchdog 스크립트와 runbook을 보강했다. 추가로 상주 engine session reuse, SAML provider 스캐폴드, <code>visible_to</code> 런타임 필터, 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 배포 <code>2a58ca49</code>도 완료했고, 커스텀 도메인이 새 asset을 서빙하는 것을 확인했다. 과거 Mao/Haru Live2D demo 자산은 repo와 번들에서 제거했고, <code>/live2d/*</code> 운영 경로는 404로 차단했다. 현재 런타임은 prod API 8001, dev API 8000/8010, web 5173, engine gateway 9099, cloudflared tunnel 1개를 의도적으로 유지한다. 이번 라운드에서는 레이아웃 작업을 서브에이전트 전담으로 분리해 세션, 학습자 홈/리뷰, 관리자/교수자, 설정 화면을 overflow 수리 수준이 아니라 실제 작업 화면 재설계 수준으로 갱신했다. 통합 레이아웃 E2E는 desktop/mobile 54개를 최종 통과했고, 자세한 인수인계는 <code>docs/ops/layout-redesign-handoff-2026-06-26.md</code>에 남겼다. 이번 이어받기에서는 핸드오프가 남긴 "실기기 시각 수용"을 엄격한 자동 게이트로 닫았다. 7개 권장 너비(390/720/861/900/1024/1280/1440)에서 7개 재설계 화면의 가로 overflow 0·컨트롤 클립 0을 강제하는 <code>layout-visual-gate.spec.ts</code>(7 passed, 49장 스크린샷)를 만들고, 화면별 1에이전트 병렬 시각 리뷰 → 6개 병렬 수정 에이전트 → 화면별 적대적 재검수까지 돌려 7/7 accept(critical/major 0, 회귀 0)를 확정했다. 학습자홈 1280 3열 가운데 컬럼 붕괴(critical)와 교수 'API 404' raw 배너를 포함한 결함을 모두 해소했고, 레이아웃 항목을 DONE으로 닫았다. 이번 로그인 복구 라운드에서는 dev/Tailnet OAuth direct start까지 <code>local_oauth_unavailable</code>로 막고, public OAuth state를 HttpOnly 쿠키에 묶인 서명 토큰으로 바꿔 API 재시작 후 콜백이 <code>invalid_state</code>로 실패하지 않게 했으며, callback 실패 reason/status 진단 로그와 화면 메시지 분기를 추가했다. 추가로 Google provider error를 <code>access_denied</code>/<code>provider_error</code>로 분리하고 로그인 화면에 reason code를 노출했다. 이어서 C1 사례개념화 워크시트는 리뷰 API·편집 UI·<code>app.case_worksheet</code> 저장/재조회 경로까지 2차로 연결했다. 공개 Google OAuth 실제 <code>/turn</code> 증명만 storageState 캡처 후 남아 있다.
</p>
</div>
</details>
</section>
</main>
<script>
(function(){
var $ = function(id){ return document.getElementById(id); };
var setTxt = function(id,v){ var e=$(id); if(e) e.textContent=v; };
// ── tabs ──
var tabButtons = Array.prototype.slice.call(document.querySelectorAll("[role='tab'][data-tab]"));
var tabPanels = Array.prototype.slice.call(document.querySelectorAll("[role='tabpanel']"));
function activateTab(btn){
tabButtons.forEach(function(b){ b.setAttribute("aria-selected", String(b===btn)); b.tabIndex = b===btn?0:-1; });
tabPanels.forEach(function(p){ p.hidden = p.id !== btn.getAttribute("aria-controls"); });
}
tabButtons.forEach(function(btn,i){
btn.addEventListener("click", function(){ activateTab(btn); });
btn.addEventListener("keydown", function(e){
if(["ArrowLeft","ArrowRight","Home","End"].indexOf(e.key)<0) return;
e.preventDefault(); var n=i;
if(e.key==="ArrowLeft") n=(i-1+tabButtons.length)%tabButtons.length;
if(e.key==="ArrowRight") n=(i+1)%tabButtons.length;
if(e.key==="Home") n=0; if(e.key==="End") n=tabButtons.length-1;
tabButtons[n].focus(); activateTab(tabButtons[n]);
});
});
// ── status card fold ──
Array.prototype.slice.call(document.querySelectorAll(".scard-head")).forEach(function(h){
h.addEventListener("click", function(){
var c=h.closest(".scard"); var open=c.classList.toggle("open");
h.setAttribute("aria-expanded", String(open));
});
});
// ── derive KPI / graphs from board cards ──
var scards = Array.prototype.slice.call(document.querySelectorAll(".scard"));
var total = scards.length;
var cnt = function(s){ return scards.filter(function(c){ return c.dataset.status===s; }).length; };
var done=cnt("done"), doing=cnt("doing"), planned=total-done-doing;
var critN = scards.filter(function(c){ return c.querySelector(".critbadge"); }).length;
var boardOwner = scards.filter(function(c){ return c.dataset.owner==="1"; }).length;
var owners = document.querySelectorAll(".ocard").length;
setTxt("kpi-done",done); setTxt("kpi-doing",doing); setTxt("kpi-plan",planned);
setTxt("g-total",total); setTxt("lg-done",done); setTxt("lg-doing",doing); setTxt("lg-plan",planned);
var pct = total? Math.round(done/total*100):0;
setTxt("kpi-pct","전체의 "+pct+"%");
setTxt("kpi-crit","납품 필수 "+critN+"건 포함");
var critBadge=$("kpi-critbadge");
if(critBadge && critN){ critBadge.textContent="● 납품 필수 "+critN; critBadge.style.display="inline-flex"; }
setTxt("kpi-owner",owners); setTxt("pill-owner",owners);
// owner column counts
["block","decide","ext"].forEach(function(k){
var col=document.querySelector('[data-owner-col="'+k+'"]');
var c = col? col.querySelectorAll(".ocard").length:0;
setTxt("ct-"+k, c);
});
// stacked bar
var sb=$("stackbar");
if(sb){
function seg(cls,v){ if(!v) return ""; return '<i class="'+cls+'" style="flex:'+v+' 1 0;">'+v+'</i>'; }
sb.innerHTML = seg("sb-done",done)+seg("sb-doing",doing)+seg("sb-plan",planned);
}
// tracks by category
var cats={}; var order=[];
scards.forEach(function(c){
var k=c.dataset.cat||"기타";
if(!cats[k]){ cats[k]={d:0,t:0}; order.push(k); }
cats[k].t++; if(c.dataset.status==="done") cats[k].d++;
});
order.sort(function(a,b){ return cats[b].t-cats[a].t; });
var tr=$("tracks");
if(tr){
tr.innerHTML = order.map(function(k){
var o=cats[k]; var p=Math.round(o.d/o.t*100);
return '<div class="track"><span class="tname">'+k+'</span><span class="tbar"><i style="width:'+p+'%"></i></span><span class="tnum">'+o.d+'/'+o.t+'</span></div>';
}).join("");
}
// ── filter ──
var counts = { all: total, done: done, doing: doing, planned: planned, owner: boardOwner, crit: critN };
Object.keys(counts).forEach(function(k){
var e=document.querySelector('[data-count="'+k+'"]'); if(e) e.textContent=counts[k];
});
var fbtns = Array.prototype.slice.call(document.querySelectorAll(".fbtn"));
function applyFilter(f){
scards.forEach(function(c){
var show = f==="all"
|| (f==="owner" ? c.dataset.owner==="1"
: (f==="crit" ? !!c.querySelector(".critbadge")
: c.dataset.status===f));
c.classList.toggle("hide", !show);
});
// hide empty track groups
Array.prototype.slice.call(document.querySelectorAll("#board .track-group")).forEach(function(g){
var any = Array.prototype.slice.call(g.querySelectorAll(".scard")).some(function(c){ return !c.classList.contains("hide"); });
g.style.display = any? "":"none";
});
}
fbtns.forEach(function(b){
b.addEventListener("click", function(){
fbtns.forEach(function(x){ x.setAttribute("aria-pressed", String(x===b)); });
applyFilter(b.dataset.filter);
});
});
// ── phase rail ──
var phases=[
{id:"P0",name:"기반·안전장치",pct:92,cls:"cur"},
{id:"P1",name:"MVP 코어루프",pct:62,cls:""},
{id:"P2a",name:"권한·페르소나",pct:74,cls:""},
{id:"P2b",name:"음성 캐스케이드",pct:40,cls:""},
{id:"P3",name:"파일럿·효과",pct:6,cls:"idle"}
];
var rail=$("phase-rail");
if(rail){
rail.innerHTML = phases.map(function(p){
return '<div class="pr-seg '+p.cls+'"><div class="pr-bar"><div class="pr-fill" style="width:'+p.pct+'%"></div></div>'
+'<div class="pr-meta"><span class="pr-id">'+p.id+'</span><span class="pr-pct">'+p.pct+'%</span></div>'
+'<span class="pr-name">'+p.name+'</span></div>';
}).join("");
}
})();
</script>
</body>
</html>