1011 lines
157 KiB
HTML
1011 lines
157 KiB
HTML
<!doctype html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Vignette 개발 커맨드센터</title>
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/variable/pretendardvariable.min.css">
|
||
<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>위기게이트 109</em>와 <em>교수자 성장 UI</em>는 1차 구현 완료.</h1>
|
||
<p class="pulse-state">코어(저항·반응 조절, 상태머신, RAG, 이론모드, 가드레일, 평가 JSON Schema)는 <b>잘 정렬</b>됐고 회귀도 녹색이다. 막힌 건 두 종류 — <b>내가 실증/결정해야 풀리는 외부 의존</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 Playwright <b>72</b> 통과</span>
|
||
<span class="mchip good">Backend pytest <b>118</b> 통과</span>
|
||
<span class="mchip good">Layout 시각게이트 <b>7/7</b> accept</span>
|
||
<span class="mchip">Pages prod <b>2a58ca49</b></span>
|
||
<span class="mchip good">Public API <b>prod OK</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>
|
||
<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>내담자 선택, 주호소·난도·접근·음성 정보, 새 회기 시작, 기존 회기 이어가기</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>서비스 상태, 엔진·DB·음성·리뷰 리소스, 사용자 등록·수정·비활성화</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/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>서비스 상태와 사용자 권한 관리를 운영자가 바로 판단할 수 있는 밀도로 정리.</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>, 기법 태그를 학습자별로 집계한다. 교수 콘솔은 학습자별 성장 카드, 회기별 막대 추이, 평균 라포, 변화폭, 최근 회기 점검 포인트를 표시한다. 평가가 없는 회기는 "평가 부족"으로 남겨 가짜 곡선을 만들지 않는다.</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/ -q</code> 119 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code> passed.</p></div><div class="kv k-warn"><b>잔여</b><p>자기효능감·기술숙련도·수련만족도 사전사후(H1)와 교수자 코멘트/검수 워크플로 확장은 별도 평가설계·임상팀 문항 확정이 필요하다.</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%절감. 현재 claude_cli 단일 — 벤더중립 인터페이스는 있음. ② PDF는 <b>Node.js 우선</b>(팀 보유기술·5개월 일정)/Spring 대안. 현재 <b>Python/FastAPI</b> — 재작성은 일정·인력 리스크.</p></div><div class="kv k-warn"><b>권고</b><p>2-tier는 ENGINE_MODE 라우팅으로 점진 도입. 백엔드 언어는 <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">백엔드 118/118 + E2E 42 + 시각게이트 7/7 통과. 스택 라이브.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>페르소나 격리(캐릭터 붕괴 버그)·RAG 비차단(세션생성 블로킹 회귀)·역린/misconduct·init_state 파라미터객체·데드코드(LogHook·tier·RMS)·OAuth state 복구·X1 dataset export dry-run — <b>백엔드 118/118 + E2E 42(데스크27·모바일11·아바타4) + 시각게이트 7/7</b> 통과. 스택 라이브(web·api·gateway·voice·RAG).</p></div><div class="kv"><b>다음</b><p>남은 원천문서 갭과 임상 콘텐츠 적재를 순서대로 닫는다.</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 1차 DONE</span><span class="scard-mid"><span class="scard-title">사례개념화·치료계획 워크시트 1차 구조<span class="critbadge">critical</span></span><span class="scard-sum">리뷰 API <code>caseWorksheet</code>와 read-only UI 카드로 구조 전무를 해소했다. 임상 루브릭은 외부 의존.</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>로 노출한다. 서버는 저장된 축어록만 근거로 규칙 기반 초안을 만들고, 프론트 리뷰 화면은 근거 턴으로 점프 가능한 read-only 워크시트 카드를 표시한다.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_session_turn_persistence.py -q</code> 15 passed, <code>pytest app/ -q</code> 119 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>session-review.spec.ts</code> 1 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>편집·DB 저장형 워크시트, 임상팀 루브릭, 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="doing" data-cat="교수자·평가" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-doing">H2 2차 DONE</span><span class="scard-mid"><span class="scard-title">턴별 회기 리뷰(상호작용 분석) — 평가 SSOP 적재 확대</span><span class="scard-sum">submit/stream/voice 경로의 fast-loop 평가를 DB 정규화 테이블에 적재·복원.</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>TurnRecord.evaluation</code>에만 남기지 않고 <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한다. 원시 평가 테이블은 evaluator/admin RLS로 제한하고 서버가 evaluator 컨텍스트로 적재한다. <code>app.turns</code> insert도 <code>RETURNING id</code> 확인으로 0-row silent success를 차단. 검증: <code>pytest app/test_evaluation_persistence.py -q</code> 6 passed, <code>pytest app/test_session_turn_persistence.py app/test_evaluation_persistence.py -q</code> 21 passed, <code>pytest app/</code> 119 passed + <code>pytest engine_gateway/</code> 7 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>더 큰 <code>finalize_turn</code>/<code>SessionRepository</code> 경계 · few-shot 골든셋 기본 OFF · 회기리뷰 2열 UI(축어록 타임라인+비언어) 프론트 강화 · 원천 축어록 골든셋 적재(임상팀).</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 1차 DONE</span><span class="scard-mid"><span class="scard-title">임상팀 콘텐츠 입력 경로(페르소나 저작 CRUD) + P4~P7 적재</span><span class="scard-sum">draft 생성·조회·편집·검수요청 API와 교수 콘솔 JSON 초안 패널 연결. 실제 P4~P7 임상 콘텐츠는 잔여.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p><code>POST/GET/PUT /personas/drafts</code>를 추가해 teacher/admin이 페르소나 카드를 draft로 생성·조회·편집하고 review 상태로 올릴 수 있다. 교수 콘솔에는 JSON 초안 패널과 검수 큐의 편집 불러오기를 연결했다. 승인된 카드만 학습자 catalog에 노출되는 기존 경계는 유지.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_persona_review.py -q</code> 19 passed, <code>pytest app/ -q</code> 119 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>P4~P7 실제 임상 콘텐츠 적재, JSON 대신 항목형 저작 UI, 루브릭·이론 콘텐츠 외부화는 후속. 임상팀이 제공하지 않은 페르소나 내용을 임의 생성하지 않는다.</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 적재. 전체 API 119.</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 본문은 저장하지 않는다. 검증: 마스킹/감사 5 passed, 관련 회귀 27 passed, 전체 API 119 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>이름/기관명은 NER 필요(Presidio ko 모델·spaCy ko, 무거움 — TODO 명시), 실제 운영 로그인 <code>/turn</code> 후 live DB row proof, 미성년/guardian 동의 수집·게이트·철회.</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차는 구현 완료. 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)를 표시한다. 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 후속은 캐싱·저비용 모델 분기, L1은 스택 정합 또는 변경사유 거버넌스 기록.</p></div></div>
|
||
</article>
|
||
</div>
|
||
<div class="detail-blocks">
|
||
<div class="detail-block"><h4>즉시 착수 가능(내부 코드, 외부 합의 불요)</h4><ul><li>C1/C2/C3/H2/H3은 1차 구조 완료. 후속은 저장형 워크시트, 임상 스크립트, CBT 체인, 2열 리뷰 UI, P4~P7 콘텐츠 적재처럼 데이터·정책·UI 고도화 영역이다.</li><li>H4(부분): 한국어 정규식 마스킹과 metadata-only LLM call audit 경로는 완료. 남은 내부 후보는 이름/기관 NER와 동의 게이트 골격이고, live row proof는 운영 로그인 증거가 필요하다. X1 dry-run export 1차와 X2 비용 관측·예산 경고 1차는 완료, X2 캐싱은 후속. 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> 필수값 누락도 경고한다. dev-login + seed 페르소나. 로그 <code>.devlogs/</code>(gitignore). <b>검증</b>: 3포트 health OK + 실제 AI 턴 생성·회기리뷰 동작.</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로 존재하지만 현재 API startup DDL은 owner 권한이 필요해 런타임 app-role 분리는 후속이다. <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 1차 DONE</span><span class="scard-mid"><span class="scard-title">턴 처리 파이프라인 중복(REST/WS) 제거</span><span class="scard-sum"><code>turn_runtime.py</code>로 세션 로드·권한·턴 append·상태 갱신 공통화.</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>routes/sessions.py</code>의 submit/stream과 <code>routes/voice.py</code>가 같은 저장·위기 이벤트 경로를 사용하고, stream fast-loop 평가 누락도 닫았다. voice 경로도 세션 recall cache를 사용한다. 검증: 관련 회귀 45 passed, 전체 API 119 passed, gateway 7 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-doing">REFACTOR 2 1차 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> 생성 + stale 체크 게이트. <code>crisis_kind</code>/<code>end_state</code> 드리프트 해소.</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>TurnResponse</code>/<code>SessionEndResponse</code>는 생성 타입 alias를 사용한다. 기존 누락 필드 <code>crisis_kind</code>, <code>end_state</code>, <code>crisis_resource</code>, <code>safety_alerts</code>, <code>learner_growth</code>, <code>PersonaDraftPayload</code>는 생성 타입에 포함된다. 검증: <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>pytest app/</code> 119 passed, <code>pytest engine_gateway/</code> 7 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>나머지 수기 미러 타입(<code>PersonaSummary</code>, review/admin/teacher/user DTO 등)을 생성 타입 alias로 점진 전환하고, 실제 CI 워크플로에 <code>npm run check:api-types</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">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 힌트·P4~P7·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>로 옮기고 턴은 캐시만 읽음(비차단). 재검증: session-layout 4/4 + <b>핵심 E2E 27/27·11/11·아바타 4/4</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>2a58ca49</code>이 현재 워킹트리 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 계약은 회귀화했다. claude_cli와 Messages API 폴백 동일성 검증은 남아 있다.</p></div><div><b>산출물</b><p><code>token</code>, <code>done</code>, <code>error</code>, cost 이벤트 계약과 contract test</p></div><div><b>검증</b><p>stream error 미저장 + stream/generate client turn telemetry 저장</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>상주 <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 1667–4199ms, 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-doing">CHECK</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 본문은 저장하지 않는다. 실제 운영 로그인 <code>/turn</code> 후 live DB row proof는 남아 있다.</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 119 passed</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.</p></div><div><b>산출물</b><p><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</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-doing">CHECK</span></div><div><b>OpenAI TTS voice preset 추상화</b><p>P1/P2/P3 persona voice preset, invalid preset fallback, speed clamp, instructions payload 계약을 회귀화했다. DB <code>voice_persona_map</code>과 live TTS 호출은 남아 있다.</p></div><div><b>산출물</b><p><code>resolve_voice</code>, <code>build_tts_payload</code>, provider-neutral params test</p></div><div><b>검증</b><p>P1/P2/P3 voice 분리, fallback, fake-client streaming payload OK</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-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를 문서화했다. 실제 참여자 완주 증거는 아직 필요하다.</p></div><div><b>산출물</b><p><code>docs/phase3/pilot-runbook.md</code>, evidence root layout, operator gates</p></div><div><b>검증</b><p><code>scripts/check-phase3-artifacts.py --help</code>; 실제 evidence root validation은 파일럿 시점에 실행</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-plan">GATE</span></div><div><b>효과성/KPI 측정</b><p>SUS, 자기효능감, κ/ICC, 환각률, 피드백 타당도 기준과 report schema를 고정했다. 실제 수치 충족 여부는 파일럿 데이터 이후 판단한다.</p></div><div><b>산출물</b><p><code>docs/phase3/kpi-measurement-plan.md</code>, KPI report schema</p></div><div><b>검증</b><p>checker가 <code>prepost_measures</code>, <code>sus_responses</code>, <code>kpi_report.json</code> evidence를 요구</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 차단 게이트를 구현했다. 실제 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></p></div><div><b>검증</b><p><code>pytest app/test_dataset_export.py</code> 5 passed, DB dry-run 5 rows → <code>technical_dry_run</code> manifest + sha256 생성, PII finding 시 approved 불가</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 OK</span><span>Live2D: 404</span></div></div>
|
||
<div class="task-list">
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>운영 웹 asset 확인</b><p>커스텀 도메인이 최신 JS/CSS asset을 서빙한다.</p></div><div><b>산출물</b><p><code>index-BzQmv-jU.js</code>, <code>index-CCZ1bDA9.css</code></p></div><div><b>검증</b><p><code>Invoke-WebRequest https://vignette.chanpaca.net/login?probe=deploy-20260627</code></p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>공개 API prod health</b><p>터널은 prod API 8001을 보고, API는 DB와 engine을 모두 확인한다.</p></div><div><b>산출물</b><p><code>environment:"prod"</code>, <code>db:true</code>, <code>engine:true</code></p></div><div><b>검증</b><p><code>https://api-vignette.chanpaca.net/health</code></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에서는 public OAuth redirect_uri가 로컬/Tailnet 세션이 아니라 공개 API 세션으로 돌아가므로 Google 버튼과 직접 Google 시작 URL을 <code>local_oauth_unavailable</code>로 차단하고 dev-login만 사용한다. Vite proxy는 원래 프론트 host/proto를 <code>X-Forwarded-Host</code>/<code>X-Forwarded-Proto</code>로 넘긴다.</p></div><div><b>산출물</b><p><code>scripts/start-tailscale-runtime.ps1</code>, <code>AUTH_DEV_LOGIN_EXTRA_ORIGINS</code>, <code>VITE_ALLOWED_HOSTS</code>, <code>Login.tsx</code>, <code>vite.config.ts</code>, <code>auth.py</code></p></div><div><b>검증</b><p><code>https://alpaca-home.taile93291.ts.net/login</code> 200, <code>/api/auth/config</code> dev_login true, dev-login 후 <code>/api/auth/me</code> 200, direct Google start → <code>/login?oauth=local_oauth_unavailable</code>, Tailnet auth/dev-login E2E 2 passed.</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-plan">MINOR</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>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>Backend pytest</td><td><code>python -m pytest app/ -q</code></td><td>119 passed</td></tr>
|
||
<tr><td>C1 case worksheet</td><td><code>python -m pytest app/test_session_turn_persistence.py -q</code> / <code>session-review.spec.ts</code></td><td>14 passed; session review exposes transcript-grounded <code>caseWorksheet</code> and UI evidence jump</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 learner growth</td><td><code>python -m pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q</code></td><td>21 passed; teacher dashboard exposes learner_growth and safety alert queue</td></tr>
|
||
<tr><td>Persona authoring CRUD</td><td><code>python -m pytest app/test_persona_review.py -q</code></td><td>19 passed; teacher/admin draft create/read/update/review submit, 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>7 tests OK; live session_id reuse and ephemeral close fixed</td></tr>
|
||
<tr><td>P1 masking gate</td><td><code>app.test_orchestrator_masking</code></td><td>4 tests OK; phone/email/RRN absent from generate/stream payloads</td></tr>
|
||
<tr><td>P2a RBAC/audit/visibility</td><td><code>app.test_rbac_idor</code></td><td>7 tests OK; other learner 403, read_session audit, evaluator-only hidden</td></tr>
|
||
<tr><td>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>Persona review workflow</td><td><code>app.test_persona_review</code></td><td>19 tests OK; approved-only catalog, teacher/admin queue, draft authoring, approve/reject audit, learner 403</td></tr>
|
||
<tr><td>Voice WS contract</td><td><code>app.test_voice_ws</code></td><td>5 tests OK; auth guard, audio_start/chunks/audio_end, text_turn, ping, max audio cap</td></tr>
|
||
<tr><td>Voice preset/EOT</td><td><code>app.test_voice_service</code></td><td>11 tests OK; TTS payload, preset 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 review UI E2E</td><td><code>npx playwright test e2e/teacher.spec.ts --project=chromium-single-run</code></td><td>1 passed; teacher approves pending persona from console</td></tr>
|
||
<tr><td>Voice UI synthetic E2E</td><td><code>npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run</code></td><td>2 passed; direct WS cascade plus Session mic button path with synthetic browser audio</td></tr>
|
||
<tr><td>Voice s2s decision memo</td><td><code>docs/decisions/voice-s2s-poc.md</code></td><td>criteria recorded; keep/drop decision remains owner DECIDE</td></tr>
|
||
<tr><td>Hanshin data/SSO gate</td><td><code>docs/ops/hanshin-data-governance-gate.md</code></td><td>artifact created; written external evidence still required</td></tr>
|
||
<tr><td>Resistance engine</td><td><code>app.test_state_machine_resistance</code></td><td>2 tests OK; empathy vs advice-jump curve fixed</td></tr>
|
||
<tr><td>Python compile</td><td><code>python -m compileall app engine_gateway</code></td><td>Passed</td></tr>
|
||
<tr><td>Phase 3 artifact gates</td><td><code>python scripts\check-phase3-artifacts.py --help</code></td><td>Passed; runbook, KPI plan, dataset manifest, privacy audit checker documented</td></tr>
|
||
<tr><td>Web build</td><td><code>npm run build</code></td><td>Passed</td></tr>
|
||
<tr><td>Pages production deploy</td><td><code>wrangler pages deploy dist --project-name vignette --branch main --commit-dirty=true</code></td><td><code>2a58ca49</code>, source <code>6bd91b0d</code> + dirty worktree</td></tr>
|
||
<tr><td>Custom domain assets</td><td><code>https://vignette.chanpaca.net/login?probe=deploy-20260627</code></td><td><code>index-BzQmv-jU.js</code>, <code>index-CCZ1bDA9.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</td><td><code>docker compose -f infra\docker-compose.yml --env-file infra\.env.example config --quiet</code></td><td>Passed with dummy required env</td></tr>
|
||
<tr><td>Focused E2E</td><td><code>admin + db-persistence + voice-success</code></td><td>10 passed</td></tr>
|
||
<tr><td>Admin manage-users</td><td><code>admin.spec.ts --grep manage real server-known users</code></td><td>desktop/mobile 2 passed</td></tr>
|
||
<tr><td>Full E2E</td><td><code>PLAYWRIGHT_HOST=127.0.0.1 npx playwright test</code></td><td>72 passed</td></tr>
|
||
<tr><td>Public auth discovery</td><td><code>E2E_PUBLIC_AUTH=1 npx playwright test --list --project=chromium-public-auth</code></td><td>2 tests listed</td></tr>
|
||
<tr><td>Public readiness</td><td><code>public-auth-turn.spec.ts --grep production-safe</code></td><td>1 passed</td></tr>
|
||
<tr><td>Persona auth boundary</td><td><code>GET /personas</code></td><td>verified local/public unauth 401</td></tr>
|
||
<tr><td>Public login</td><td><code>auth.spec.ts --grep public login</code></td><td>1 passed</td></tr>
|
||
<tr><td>Public runtime scripts</td><td><code>start/watch/install-public-runtime*.ps1</code></td><td>parser OK; watchdog check-only 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>http://localhost:5173/login</code> / <code>https://alpaca-home.taile93291.ts.net/login</code></td><td>public login 200, local/Tailnet login 200, public Google auth redirect 302 with HttpOnly state cookie, cookie-bound signed OAuth state survives API restart(fake code → <code>token_exchange_failed</code>, not <code>invalid_state</code>), provider callback error → <code>oauth=access_denied</code>, OAuth callback 실패는 <code>api.public.err.log</code>에 reason/status만 비밀값 없이 남김, 로그인 화면은 reason code 표시, local/Tailnet Google direct → <code>local_oauth_unavailable</code>, local auth E2E 7 passed, Tailnet auth/dev-login E2E 2 passed</td></tr>
|
||
<tr><td>Local 5175 login</td><td><code>PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 auth.spec.ts</code></td><td>desktop/mobile passed</td></tr>
|
||
<tr><td>Learner/readiness E2E</td><td><code>learner.spec.ts + readiness.spec.ts desktop/mobile</code></td><td>14 passed</td></tr>
|
||
<tr><td>Learner screenshots</td><td><code>learn-empty-desktop.png / learn-empty-mobile-compact.png</code></td><td>document overflow 0, empty history visible</td></tr>
|
||
<tr><td>Session layout/turn</td><td><code>session-layout.spec.ts desktop/mobile</code></td><td>8 passed</td></tr>
|
||
<tr><td>Voice/session focused</td><td><code>voice + voice-success + session-layout</code></td><td>15 passed</td></tr>
|
||
<tr><td>Layout redesign handoff</td><td><code>docs/ops/layout-redesign-handoff-2026-06-26.md</code></td><td>subagent scopes, files, verification, remaining visual review recorded</td></tr>
|
||
<tr><td>Layout redesign focused E2E</td><td><code>npx playwright test e2e/learner.spec.ts e2e/session-layout.spec.ts e2e/session-review.spec.ts e2e/admin.spec.ts e2e/settings.spec.ts e2e/teacher.spec.ts --project=chromium-desktop --project=chromium-mobile</code></td><td>54 passed after isolated Admin tablet flake rerun</td></tr>
|
||
<tr><td>Strict layout visual gate</td><td><code>npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run</code></td><td>7 passed; 7 screens × 7 widths(390/720/861/900/1024/1280/1440), horizontal overflow 0 + control clip 0, 49 full-page screenshots</td></tr>
|
||
<tr><td>Parallel visual quality review</td><td><code>workflow layout-visual-quality-review (7 agents)</code></td><td>49 screenshots read; per-screen findings(critical 1, major 8+, minor) → fix verdicts</td></tr>
|
||
<tr><td>Parallel layout fixes</td><td><code>6 agent team: learner-home/session/review/professor/admin/settings (disjoint files)</code></td><td>all fixes applied; <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 1667–4199ms; cost 누적 0.068→0.123(turns 1→2)</td></tr>
|
||
<tr><td>Postgres RLS/audit smoke (live)</td><td><code>VIGNETTE_RLS_AUDIT_DSN=…vignette_app… check-postgres-rls-audit.py --write-fixtures</code></td><td>5 checks PASS: preflight, learner 세션/턴 격리, teacher cohort 스코프, visible_to evaluator-only 숨김, teacher/admin read audit insert (NOBYPASSRLS app role, fixtures 자동 정리)</td></tr>
|
||
<tr><td>Turn cost telemetry (live DB)</td><td><code>SELECT … FROM app.turns WHERE actor_kind='client_ai'</code></td><td>13 client_ai turns 적재: llm_provider=claude_cli, model=gateway-default, cost_usd 실값(합 $1.22, 평균 $0.094), visible_to 배열. 토큰은 CLI 게이트웨이 특성상 0, cost는 산출</td></tr>
|
||
<tr><td>Cosmetic polish re-verify</td><td><code>5 polish agents → typecheck + gate + layout E2E re-run</code></td><td>typecheck OK; gate 7 passed; layout focused E2E 54 passed; session-layout 8/8; regression 0</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="tab-subhead"><h4>다음 실행 명령</h4><p>남은 public OAuth <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 1667–4199ms, cost 누적 0.068→0.123. ③ 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>(owner 지시로 보류) · <code>vnet.18ka.net</code>/<code>api-vnet.18ka.net</code> 공개 런타임(DNS 미해결, Cloudflare 18ka.net zone 권한 불일치, Google OAuth redirect URI 등록 필요) · 한신대 데이터/SSO 거버넌스(owner 지시로 보류) · 음성 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 사례개념화 워크시트 1차 구조를 리뷰 API와 화면에 붙여 축어록 근거 기반 read-only 초안을 제공한다. 공개 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>
|