Stabilize runtime auth and E2E coverage

This commit is contained in:
Yun Chan 2026-06-26 14:47:00 +09:00
parent 6a3e3b541c
commit 188e899394
133 changed files with 55987 additions and 6775 deletions

View file

@ -1,602 +1,378 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vignette — 개발 오버뷰 (PM 보드)</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/variable/pretendardvariable.min.css">
<style>
/* =====================================================================
Vignette — 개발 오버뷰 대시보드 (내부 PM 보드, 앱 화면 아님)
토큰: DESIGN_CONCEPT.md §3 그대로 계승. accent = 세이지-틸(학습자 톤).
철칙: border-left 0 · 이모지 0(아이콘 inline SVG) · 카드덤프 0 ·
순흑/순백 금지 · 강조는 weight/tint/kicker/dot로만 · 한 화면 한 목적.
빨강은 리스크에만.
===================================================================== */
:root{
/* 중성 베이스 (따뜻한 종이 + 잉크 네이비-그린) */
--paper:#FBFAF8; --paper-2:#F4F2EE; --surface:#FFFFFF;
--neutral-100:#ECEFF1; --neutral-150:#E5E1DA; --neutral-200:#D5DBDE; --neutral-400:#98A4A9;
--ink:#1C2A2A; --ink-2:#5A6663; --ink-3:#93A09C; --hair:#E5E1DA;
/* 악센트: 세이지-틸 (프로젝트 톤) */
--accent:#3E7A6E; --accent-deep:#316257; --accent-tint:#EEF4F2; --accent-bright:#5F968B;
/* 보조 온기 (테라코타) — 화면당 절제 */
--clay:#B0735C; --clay-deep:#94604C; --clay-tint:#F6F0EC;
/* 의미색 */
--pos-text:#2C6E49; --pos-solid:#3B8C5E; --pos-tint:#E8F3EC;
--warn-text:#8A5A00; --warn-solid:#C28A2C; --warn-tint:#FAF1E0;
--crit-text:#A8352C; --crit-solid:#C4453A; --crit-tint:#FAEAE8;
--info-text:#2E5A7A; --info-solid:#3C7299; --info-tint:#EAF1F6;
--neutral-sig:#8895A2;
/* 별칭 */
--text-on-accent:#FBFAF8;
--focus-ring:rgba(62,122,110,.45);
/* 폰트 */
--font-sans:"Pretendard Variable",Pretendard,-apple-system,"Apple SD Gothic Neo","Noto Sans KR",system-ui,sans-serif;
--font-num:"Pretendard Variable","JetBrains Mono",ui-monospace,monospace;
/* spacing */
--sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:24px; --sp-6:32px; --sp-7:48px; --sp-8:64px;
--radius:8px; --radius-sm:6px; --radius-lg:12px;
--maxw:1200px;
--shadow-sm:0 1px 2px rgba(28,42,42,.05);
--shadow-md:0 4px 14px rgba(28,42,42,.07);
--ease-out:cubic-bezier(.22,.61,.36,1);
}
*{margin:0;padding:0;box-sizing:border-box;}
html,body{height:100%;}
body{
font-family:var(--font-sans); background:var(--paper); color:var(--ink);
-webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
letter-spacing:-0.01em; line-height:1.6;
}
.tab-num{font-variant-numeric:tabular-nums; letter-spacing:-0.01em;}
/* ============ 공통 앱 셸: 톱바 ============ */
.topbar{
height:56px; background:var(--surface); border-bottom:1px solid var(--hair);
display:flex; align-items:center; gap:var(--sp-6); padding:0 var(--sp-6);
position:sticky; top:0; z-index:20;
}
.brand{display:flex; align-items:center; gap:10px;}
.brand .mark{display:flex; align-items:center; justify-content:center; width:26px; height:26px;}
.brand .mark svg{width:26px; height:26px;}
.brand .wm{font-size:17px; font-weight:700; letter-spacing:-0.02em; color:var(--ink);}
.brand .wm .v{color:var(--accent);}
/* 컨텍스트 라벨 (mono accent, 조용한 신호) */
.rolelabel{
font-family:var(--font-num); font-size:13px; font-weight:600;
letter-spacing:0.04em; color:var(--accent);
padding-left:var(--sp-4); margin-left:var(--sp-2);
border-left:1px solid var(--hair); /* 셸 구획 디바이더(강조선 아님) */
}
.topbar .spacer{flex:1;}
.repo{
font-family:var(--font-num); font-size:12.5px; color:var(--ink-3);
display:inline-flex; align-items:center; gap:7px;
}
.repo svg{width:14px; height:14px; stroke:var(--ink-3); fill:none;}
.repo .priv{
font-size:11px; font-weight:600; color:var(--ink-2); background:var(--paper-2);
padding:2px 8px; border-radius:100px; letter-spacing:0.02em;
}
.topbar .when{
display:flex; align-items:center; gap:8px; font-size:13px; color:var(--ink-2);
font-weight:500; margin-left:var(--sp-2);
}
.topbar .when .dot{width:7px; height:7px; border-radius:50%; background:var(--accent);}
.topbar .when .d{font-family:var(--font-num); font-variant-numeric:tabular-nums; color:var(--ink); font-weight:600;}
/* ============ 본문 래퍼 ============ */
.wrap{max-width:var(--maxw); margin:0 auto; padding:var(--sp-7) var(--sp-6) var(--sp-8);}
.kicker{
font-family:var(--font-num); font-size:12px; font-weight:600;
letter-spacing:0.08em; color:var(--accent);
display:flex; align-items:center; gap:8px;
}
.kicker .dot{width:6px; height:6px; border-radius:50%; background:var(--accent-bright);}
/* 페이지 헤드라인 (한 화면 한 메시지) */
.head{margin-bottom:var(--sp-7);}
.head .kicker{margin-bottom:var(--sp-3);}
.head h1{font-size:30px; font-weight:700; letter-spacing:-0.02em; color:var(--ink); line-height:1.3;}
.head h1 em{font-style:normal; color:var(--accent-deep);}
.head .sub{font-size:15px; color:var(--ink-3); margin-top:8px; max-width:680px;}
/* 헤드라인 옆 요약 스탯 라인 */
.headstats{
display:flex; align-items:center; flex-wrap:wrap; gap:var(--sp-5);
margin-top:var(--sp-5); padding-top:var(--sp-5); border-top:1px solid var(--hair);
}
.headstats .stat{display:inline-flex; align-items:baseline; gap:7px;}
.headstats .stat .num{font-size:20px; font-weight:700; color:var(--ink); font-variant-numeric:tabular-nums; letter-spacing:-0.02em;}
.headstats .stat .lab{font-size:13.5px; color:var(--ink-3);}
.headstats .vsep{width:1px; height:18px; background:var(--hair);}
/* 섹션 헤더 */
.sechead{margin-bottom:var(--sp-5);}
.sechead .kicker{margin-bottom:6px;}
.sechead .desc{font-size:13px; color:var(--ink-3);}
.block{margin-bottom:var(--sp-8);}
/* ============ 1. 로드맵 4단계 진행 막대 ============ */
.roadmap{
background:var(--surface); border:1px solid var(--hair); border-radius:var(--radius-lg);
box-shadow:var(--shadow-sm); padding:var(--sp-6) var(--sp-6) var(--sp-5);
}
.phase-grid{display:grid; grid-template-columns:repeat(4,1fr); gap:var(--sp-5);}
.phase .ph-top{display:flex; align-items:baseline; justify-content:space-between; margin-bottom:9px;}
.phase .ph-id{font-family:var(--font-num); font-size:12px; font-weight:700; letter-spacing:0.04em; color:var(--ink-3);}
.phase .ph-pct{font-family:var(--font-num); font-size:13px; font-weight:700; font-variant-numeric:tabular-nums; color:var(--ink-2);}
.phase.is-active .ph-pct{color:var(--accent-deep);}
.phase .ph-name{font-size:15px; font-weight:600; color:var(--ink); letter-spacing:-0.01em; line-height:1.4; margin-bottom:3px;}
.phase .ph-weeks{font-size:12px; color:var(--ink-3); font-variant-numeric:tabular-nums; margin-bottom:11px;}
.phase .track{height:8px; background:var(--paper-2); border-radius:4px; overflow:hidden;}
.phase .track .fill{height:100%; border-radius:4px; background:var(--neutral-150);}
.phase.is-active .track .fill{background:var(--accent);}
.phase .ph-state{
margin-top:11px; display:inline-flex; align-items:center; gap:7px;
font-size:12.5px; font-weight:500;
}
.phase .ph-state .d{width:7px; height:7px; border-radius:50%; background:var(--neutral-200);}
.phase .ph-state.active{color:var(--accent-deep); font-weight:600;}
.phase .ph-state.active .d{background:var(--accent);}
.phase .ph-state.wait{color:var(--ink-3);}
.phase .ph-note{font-size:11.5px; color:var(--ink-3); margin-top:7px; line-height:1.5;}
/* 단계 사이 연결 디바이더 (시각 흐름) */
.phase{position:relative;}
.phase:not(:first-child)::before{
content:""; position:absolute; left:calc(-1 * var(--sp-5) / 2 - 0.5px); top:7px; bottom:7px;
width:1px; background:var(--hair);
}
/* ============ 2단 본문 (좌: 완료/다음 · 우: 결정/리스크) ============ */
.cols{display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-7); align-items:start;}
/* 완료 체크리스트 */
.panel{
background:var(--surface); border:1px solid var(--hair); border-radius:var(--radius-lg);
box-shadow:var(--shadow-sm); padding:var(--sp-6);
}
.panel + .panel{margin-top:var(--sp-6);}
.panel .ph{font-size:16px; font-weight:600; color:var(--ink); letter-spacing:-0.01em; margin-bottom:var(--sp-2);}
.panel .pd{font-size:12.5px; color:var(--ink-3); margin-bottom:var(--sp-5);}
.checks{display:flex; flex-direction:column;}
.chk-group + .chk-group{margin-top:var(--sp-5);}
.chk-glabel{
font-family:var(--font-num); font-size:11px; font-weight:600; letter-spacing:0.06em;
color:var(--ink-3); text-transform:uppercase; margin-bottom:10px;
}
.chk{display:flex; align-items:flex-start; gap:11px; padding:7px 0;}
.chk + .chk{border-top:1px solid var(--paper-2);}
.chk .mk{
flex:none; width:18px; height:18px; border-radius:50%; margin-top:1px;
background:var(--pos-tint); display:flex; align-items:center; justify-content:center;
}
.chk .mk svg{width:11px; height:11px; stroke:var(--pos-solid); fill:none; stroke-width:2.6;}
.chk .ct{font-size:13.5px; color:var(--ink); line-height:1.5;}
.chk .ct b{font-weight:600;}
.chk .ct .tag{
display:inline-block; margin-left:6px; font-family:var(--font-num); font-size:10.5px; font-weight:600;
letter-spacing:0.02em; color:var(--accent-deep); background:var(--accent-tint);
padding:1px 7px; border-radius:100px; vertical-align:middle;
}
.chk .ct .verified{color:var(--pos-text); background:var(--pos-tint);}
/* 진행중 / 다음 (도트 리스트) */
.nextlist{display:flex; flex-direction:column;}
.nx{display:flex; align-items:flex-start; gap:11px; padding:10px 0;}
.nx + .nx{border-top:1px solid var(--paper-2);}
.nx .nd{flex:none; width:8px; height:8px; border-radius:50%; margin-top:7px;}
.nx .nd.doing{background:var(--accent);}
.nx .nd.queued{background:var(--neutral-200);}
.nx .nt{font-size:13.5px; color:var(--ink); line-height:1.5;}
.nx .nt b{font-weight:600;}
.nx .nt .st{
display:inline-block; margin-left:6px; font-size:11px; font-weight:600;
padding:1px 8px; border-radius:100px; vertical-align:middle;
}
.nx .nt .st.doing{color:var(--accent-deep); background:var(--accent-tint);}
.nx .nt .st.blocked{color:var(--crit-text); background:var(--crit-tint);}
.nx .nt .st.queued{color:var(--ink-3); background:var(--paper-2);}
.nx .nt .why{display:block; font-size:12px; color:var(--ink-3); margin-top:3px; line-height:1.5;}
/* 확정 결정 */
.decisions{display:flex; flex-direction:column;}
.dc{display:grid; grid-template-columns:96px 1fr; align-items:baseline; gap:var(--sp-4); padding:11px 0;}
.dc + .dc{border-top:1px solid var(--paper-2);}
.dc .dk{font-size:12.5px; font-weight:600; color:var(--ink-3); letter-spacing:-0.01em;}
.dc .dv{font-size:13.5px; color:var(--ink); line-height:1.5;}
.dc .dv b{font-weight:600; color:var(--accent-deep);}
.dc.span{grid-template-columns:1fr;}
.dc.span .dk{
font-family:var(--font-num); font-size:11px; letter-spacing:0.06em; text-transform:uppercase;
color:var(--ink-3); margin-bottom:8px;
}
.dc .stack-chips{display:flex; flex-wrap:wrap; gap:7px; margin-top:3px;}
.dc .stack-chips .c{
font-family:var(--font-num); font-size:11.5px; font-weight:500; color:var(--ink-2);
background:var(--paper-2); padding:4px 10px; border-radius:100px;
}
.dc .stack-chips .c.key{color:var(--accent-deep); background:var(--accent-tint); font-weight:600;}
/* 리스크 · 블로커 (빨강은 여기에만) */
.risks{display:flex; flex-direction:column;}
.rk{display:flex; align-items:flex-start; gap:12px; padding:13px 0;}
.rk + .rk{border-top:1px solid var(--paper-2);}
.rk .sev{
flex:none; margin-top:3px; display:inline-flex; align-items:center; gap:7px;
font-family:var(--font-num); font-size:10.5px; font-weight:700; letter-spacing:0.04em;
width:64px;
}
.rk .sev .d{width:9px; height:9px; border-radius:50%;}
.rk .sev.high{color:var(--crit-text);} .rk .sev.high .d{background:var(--crit-solid);}
.rk .sev.mid{color:var(--warn-text);} .rk .sev.mid .d{background:var(--warn-solid);}
.rk .rbody .rt{font-size:13.5px; font-weight:600; color:var(--ink); line-height:1.45;}
.rk .rbody .rd{font-size:12.5px; color:var(--ink-2); line-height:1.55; margin-top:3px;}
.rk .rbody .rd .ext{
font-size:11px; font-weight:600; color:var(--clay-deep); background:var(--clay-tint);
padding:1px 7px; border-radius:100px; margin-left:4px;
}
/* ============ 3. 산출물 인벤토리 (테이블) ============ */
.inv-tbl{width:100%; border-collapse:collapse; font-size:14px;}
.inv-tbl thead th{
font-size:12px; font-weight:600; color:var(--ink-3); letter-spacing:0.02em;
text-align:left; padding:0 var(--sp-3) var(--sp-3); border-bottom:1px solid var(--neutral-200);
white-space:nowrap;
}
.inv-tbl tbody td{padding:var(--sp-4) var(--sp-3); border-bottom:1px solid var(--paper-2); vertical-align:top;}
.inv-tbl tbody tr:last-child td{border-bottom:none;}
.inv-tbl .area{font-weight:600; color:var(--ink); white-space:nowrap;}
.inv-tbl .area .kd{
display:inline-flex; align-items:center; gap:8px;
}
.inv-tbl .area .kd .d{width:7px; height:7px; border-radius:50%; background:var(--accent-bright);}
.inv-tbl .what{color:var(--ink-2); line-height:1.55;}
.inv-tbl .path{
font-family:var(--font-num); font-size:12px; color:var(--ink-3); line-height:1.7;
white-space:nowrap;
}
.inv-tbl .path code{
background:var(--paper-2); color:var(--ink-2); padding:2px 7px; border-radius:var(--radius-sm);
font-family:var(--font-num); display:inline-block; margin:1px 4px 1px 0;
}
.inv-tbl .stt{white-space:nowrap;}
.invtag{display:inline-flex; align-items:center; gap:7px; font-size:12.5px; font-weight:500;}
.invtag .d{width:7px; height:7px; border-radius:50%;}
.invtag.done{color:var(--pos-text);} .invtag.done .d{background:var(--pos-solid);}
.invtag.partial{color:var(--warn-text);} .invtag.partial .d{background:var(--warn-solid);}
/* 푸터 메타 */
.foot{
margin-top:var(--sp-6); display:flex; align-items:center; flex-wrap:wrap; gap:var(--sp-4);
font-size:12px; color:var(--ink-3); font-variant-numeric:tabular-nums;
}
.foot .fdot{width:5px; height:5px; border-radius:50%; background:var(--neutral-200);}
@media (prefers-reduced-motion: reduce){
*,*::before,*::after{animation-duration:.01ms!important; transition-duration:.01ms!important;}
}
@media (max-width:980px){
.phase-grid{grid-template-columns:repeat(2,1fr); gap:var(--sp-6);}
.phase:not(:first-child)::before{display:none;}
.cols{grid-template-columns:1fr; gap:var(--sp-6);}
.repo,.topbar .when{display:none;}
.inv-tbl .path{white-space:normal;}
}
@media (max-width:560px){
.phase-grid{grid-template-columns:1fr;}
}
</style>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vignette 개발 대시보드</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/variable/pretendardvariable.min.css">
<style>
:root {
--paper:#FBFAF8;
--paper-2:#F4F2EE;
--surface:#FFFFFF;
--hair:#E5E1DA;
--ink:#1C2A2A;
--ink-2:#5A6663;
--ink-3:#93A09C;
--accent:#3E7A6E;
--accent-deep:#316257;
--accent-tint:#EEF4F2;
--clay:#B0735C;
--pos:#2C6E49;
--warn:#8A5A00;
--crit:#A8352C;
--pos-bg:#E8F3EC;
--warn-bg:#FAF1E0;
--crit-bg:#FAEAE8;
--info-bg:#EAF1F6;
--font:"Pretendard Variable", Pretendard, -apple-system, "Apple SD Gothic Neo", "Noto Sans KR", system-ui, sans-serif;
--num:"Pretendard Variable", "JetBrains Mono", ui-monospace, monospace;
--radius:8px;
}
* { box-sizing:border-box; }
body {
margin:0;
background:var(--paper);
color:var(--ink);
font-family:var(--font);
line-height:1.55;
letter-spacing:0;
}
header {
position:sticky;
top:0;
z-index:2;
display:flex;
align-items:center;
justify-content:space-between;
gap:24px;
min-height:58px;
padding:0 32px;
background:rgba(255,255,255,.92);
border-bottom:1px solid var(--hair);
backdrop-filter:blur(10px);
}
.brand { display:flex; align-items:center; gap:10px; font-weight:760; }
.mark { width:24px; height:24px; border:2px solid var(--accent); border-radius:50%; position:relative; }
.mark:after { content:""; width:6px; height:6px; border-radius:50%; background:var(--clay); position:absolute; left:7px; top:6px; }
.stamp { color:var(--ink-3); font:600 12px/1 var(--num); }
main { width:min(1180px, calc(100% - 40px)); margin:0 auto; padding:42px 0 64px; }
.hero { display:grid; grid-template-columns:minmax(0,1.2fr) minmax(280px,.8fr); gap:28px; align-items:start; margin-bottom:34px; }
.kicker { display:flex; align-items:center; gap:8px; color:var(--accent); font:700 12px/1.3 var(--num); letter-spacing:.08em; text-transform:uppercase; }
.kicker:before { content:""; width:6px; height:6px; border-radius:50%; background:var(--accent); }
h1 { margin:12px 0 0; font-size:30px; line-height:1.28; letter-spacing:0; }
h1 em { color:var(--accent-deep); font-style:normal; }
.lead { margin:12px 0 0; color:var(--ink-2); max-width:760px; font-size:15px; }
.summary {
display:grid;
grid-template-columns:repeat(2,minmax(0,1fr));
gap:1px;
overflow:hidden;
border:1px solid var(--hair);
border-radius:12px;
background:var(--hair);
}
.metric { min-width:0; padding:18px; background:var(--surface); }
.metric span { display:block; color:var(--ink-3); font-size:12px; font-weight:650; }
.metric b { display:block; margin-top:6px; color:var(--ink); font:760 28px/1 var(--num); }
section { margin-top:30px; }
.section-head { display:flex; align-items:end; justify-content:space-between; gap:18px; margin-bottom:14px; }
.section-head h2 { margin:0; font-size:20px; line-height:1.3; }
.section-head p { margin:4px 0 0; color:var(--ink-3); font-size:13px; }
.grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:16px; }
.panel {
min-width:0;
background:var(--surface);
border:1px solid var(--hair);
border-radius:12px;
padding:20px;
box-shadow:0 1px 2px rgba(28,42,42,.05);
}
.panel h3 { margin:0 0 10px; font-size:16px; line-height:1.35; }
.panel p { margin:0; color:var(--ink-2); font-size:13px; }
.tag { display:inline-flex; align-items:center; gap:6px; margin-bottom:12px; padding:3px 9px; border-radius:999px; font-size:12px; font-weight:680; }
.tag:before { content:""; width:7px; height:7px; border-radius:50%; }
.done { color:var(--pos); background:var(--pos-bg); }
.done:before { background:var(--pos); }
.doing { color:var(--warn); background:var(--warn-bg); }
.doing:before { background:var(--warn); }
.risk { color:var(--crit); background:var(--crit-bg); }
.risk:before { background:var(--crit); }
.info { color:#2E5A7A; background:var(--info-bg); }
.info:before { background:#3C7299; }
.list { display:grid; gap:10px; margin:0; padding:0; list-style:none; }
.list li { display:grid; grid-template-columns:10px minmax(0,1fr); gap:10px; color:var(--ink-2); font-size:13px; }
.list li:before { content:""; width:6px; height:6px; margin-top:8px; border-radius:50%; background:var(--accent); }
code { font-family:var(--num); font-size:12px; background:var(--paper-2); padding:2px 6px; border-radius:6px; }
.wide { grid-column:1 / -1; }
.commands { display:grid; gap:10px; }
pre { margin:0; overflow:auto; padding:14px; border-radius:8px; background:#1E2724; color:#F6F0EC; font:12px/1.6 var(--num); }
table { width:100%; border-collapse:collapse; background:var(--surface); border:1px solid var(--hair); border-radius:12px; overflow:hidden; display:table; }
th,td { padding:12px 14px; border-bottom:1px solid var(--hair); text-align:left; vertical-align:top; font-size:13px; }
th { color:var(--ink-3); font-weight:700; background:var(--paper-2); }
tr:last-child td { border-bottom:0; }
@media (max-width:900px) {
header { padding:0 20px; }
main { width:min(100% - 28px, 1180px); padding-top:28px; }
.hero { grid-template-columns:1fr; }
.grid { grid-template-columns:1fr; }
}
@media (max-width:520px) {
.summary { grid-template-columns:1fr; }
h1 { font-size:24px; }
.stamp { display:none; }
}
</style>
</head>
<body>
<header>
<div class="brand"><span class="mark" aria-hidden="true"></span><span>Vignette 개발 대시보드</span></div>
<div class="stamp">2026-06-26 14:43 KST</div>
</header>
<!-- ============ 공통 앱 셸: 톱바 ============ -->
<header class="topbar">
<div class="brand">
<span class="mark">
<!-- 워드마크: 비네트(조리개) — inline SVG -->
<svg viewBox="0 0 26 26" fill="none" aria-hidden="true">
<circle cx="13" cy="13" r="11" stroke="#3E7A6E" stroke-width="2"/>
<path d="M8 14.5c1.4 1.7 3 2.5 5 2.5s3.6-.8 5-2.5" stroke="#3E7A6E" stroke-width="2" stroke-linecap="round"/>
<circle cx="13" cy="9" r="1.6" fill="#B0735C"/>
</svg>
</span>
<span class="wm"><span class="v">Vignette</span></span>
</div>
<span class="rolelabel">개발 오버뷰</span>
<span class="spacer"></span>
<span class="repo">
<svg viewBox="0 0 24 24" stroke-width="1.7"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" stroke-linecap="round" stroke-linejoin="round"/></svg>
yunchan8804-blip/vignette
<span class="priv">private</span>
</span>
<span class="when"><span class="dot"></span>오늘 <span class="d">2026-06-25</span></span>
</header>
<!-- ============ 본문 ============ -->
<div class="wrap">
<!-- 페이지 헤드라인 -->
<div class="head">
<div class="kicker"><span class="dot"></span>한신대 산학협력 · 20주 · 매칭금 500만</div>
<h1>Phase 0 기반정렬 막바지, <em>엔진 실동작까지 검증</em>됐습니다.</h1>
<div class="sub">설계 4종·모노레포·디자인 7종·P0 코드 골격이 오늘 자로 완료됐고, 남은 블로커는 Docker 기동 후 DB 실마이그레이션과 외부 데이터 수급입니다.</div>
<div class="headstats">
<span class="stat"><span class="num">4</span><span class="lab">로드맵 단계</span></span>
<span class="vsep"></span>
<span class="stat"><span class="num">1</span><span class="lab">진행중 (P0)</span></span>
<span class="vsep"></span>
<span class="stat"><span class="num">6</span><span class="lab">확정 결정</span></span>
<span class="vsep"></span>
<span class="stat"><span class="num">3</span><span class="lab">열린 리스크</span></span>
</div>
</div>
<!-- 1. 로드맵 4단계 진행 막대 -->
<section class="block">
<div class="sechead">
<div class="kicker"><span class="dot"></span>로드맵 · 20주</div>
<div class="desc">단계별 진행률과 상태. 진행중인 단계만 색으로 강조, 대기 단계는 무채로 둡니다.</div>
</div>
<div class="roadmap">
<div class="phase-grid">
<!-- Phase 0 -->
<div class="phase is-active">
<div class="ph-top"><span class="ph-id">PHASE 0</span><span class="ph-pct">70%</span></div>
<div class="ph-name">기반정렬</div>
<div class="ph-weeks">1~2주차</div>
<div class="track"><div class="fill" style="width:70%"></div></div>
<div class="ph-state active"><span class="d"></span>진행중</div>
<div class="ph-note">스캐폴드·DB 스키마·엔진 PoC 거의 완료</div>
</div>
<!-- Phase 1 -->
<div class="phase">
<div class="ph-top"><span class="ph-id">PHASE 1</span><span class="ph-pct">0%</span></div>
<div class="ph-name">텍스트 상담 MVP</div>
<div class="ph-weeks">3~8주차</div>
<div class="track"><div class="fill" style="width:0%"></div></div>
<div class="ph-state wait"><span class="d"></span>대기</div>
<div class="ph-note">상담 루프 + 상태머신 + 실시간 피드백</div>
</div>
<!-- Phase 2 -->
<div class="phase">
<div class="ph-top"><span class="ph-id">PHASE 2</span><span class="ph-pct">0%</span></div>
<div class="ph-name">음성 + 3역할 + 페르소나</div>
<div class="ph-weeks">9~15주차</div>
<div class="track"><div class="fill" style="width:0%"></div></div>
<div class="ph-state wait"><span class="d"></span>대기</div>
<div class="ph-note">음성 캐스케이드 · 슈퍼바이저/내담자/평가</div>
</div>
<!-- Phase 3 -->
<div class="phase">
<div class="ph-top"><span class="ph-id">PHASE 3</span><span class="ph-pct">0%</span></div>
<div class="ph-name">효과검증 파일럿</div>
<div class="ph-weeks">16~20주차</div>
<div class="track"><div class="fill" style="width:0%"></div></div>
<div class="ph-state wait"><span class="d"></span>대기</div>
<div class="ph-note">IRB 면제 확정 (교육용)</div>
</div>
<main>
<div class="hero">
<div>
<div class="kicker">team lead review</div>
<h1>Phase 0는 <em>검증 가능한 운영 안전장치와 E2E 안정화</em>까지 마무리됐다.</h1>
<p class="lead">
이번 세션은 seed persona 결합 제거, 비개발 fail-closed 설정, compose OAuth 주입,
admin 모바일 overflow 수정, persona 인증 경계 보강, 전체 Playwright 재통과, 공개 API prod 전환까지 완료했다.
이후 로컬 5175 런타임 재기동, admin 사용자 관리 E2E 안정화, 학습자 홈 0회 기록 UX,
active session 실제 SSE 스트림 전환, 텍스트/음성 실패 턴 미저장, 직접 URL persona 우회 차단, Live2D demo fallback 제거,
AI 운영 fail-closed 보강까지 전체 Playwright로 확인했다.
2026-06-26 14:43 KST 기준 공개 API 터널을 복구한 뒤 전체 Playwright 72개가 desktop/mobile/single-run 모두 통과했다.
공개 Google OAuth 실제 `/turn` 증명만 storageState 캡처 후 남아 있다.
</p>
</div>
<div class="summary" aria-label="현재 검증 요약">
<div class="metric"><span>Full Playwright</span><b>72 passed</b></div>
<div class="metric"><span>Public API</span><b>prod OK</b></div>
<div class="metric"><span>Backend unittest</span><b>20 OK</b></div>
<div class="metric"><span>Decision</span><b>조건부 GO</b></div>
</div>
</div>
</section>
<!-- 2단 본문 -->
<div class="cols">
<!-- 좌측: 오늘 완료 + 진행중/다음 -->
<div>
<section class="block">
<div class="sechead">
<div class="kicker"><span class="dot"></span>오늘 완료 · 2026-06-25</div>
<div class="desc">P0 기반정렬에서 오늘 자로 닫은 항목들입니다.</div>
<section>
<div class="section-head">
<div>
<h2>팀장 판정</h2>
<p>이 HTML만 보고 판단해야 하는 항목</p>
</div>
</div>
<div class="grid">
<article class="panel">
<span class="tag done">go</span>
<h3>로컬 회귀와 공개 prod-safe 게이트 통과</h3>
<p>백엔드 unittest 20개, Python compile, 웹 production build, 전체 Playwright 72개가 통과했다. 공개 API도 `environment:"prod"`, `db:true`, `engine:true`로 응답한다.</p>
</article>
<article class="panel">
<span class="tag doing">conditional</span>
<h3>운영 반영 전 남은 증거 1개</h3>
<p>실제 Google 로그인 쿠키가 필요한 공개 `/turn` smoke는 아직 storageState가 없어 미완이다. 로그인 가능한 계정으로 한 번 캡처하면 바로 검증 가능하다.</p>
</article>
<article class="panel">
<span class="tag info">mock audit</span>
<h3>런타임 목업 제거 기준</h3>
<p>프론트 페이지는 실제 API/DB 경로를 탄다. 남은 `mock` 표기는 테스트 코드의 `unittest.mock` 또는 의도적 fixture이며, seed fallback은 prod/staging에서 fail-closed 된다.</p>
</article>
</div>
</section>
<div class="panel">
<div class="checks">
<!-- 설계 -->
<div class="chk-group">
<div class="chk-glabel">설계 4종</div>
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">마스터플랜</span></div>
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">적대검증 — red team <b>40개 결함</b> 도출 후 재설계</span></div>
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">메모리 · 페르소나 설계</span></div>
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">디자인 컨셉</span></div>
</div>
<!-- 인프라 -->
<div class="chk-group">
<div class="chk-glabel">인프라</div>
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">모노레포 repo 생성</span></div>
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">Docker 이식 구성 <span class="ct" style="color:var(--ink-3)">— compose + Caddy + 엔진 어댑터 분리</span></span></div>
</div>
<!-- 디자인 -->
<div class="chk-group">
<div class="chk-glabel">디자인</div>
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">화면 <b>7종</b> 시안 <span class="tag">login · learner-home · session · session-review · professor · admin · settings</span></span></div>
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">아바타 <b>Rive</b> 채택</span></div>
</div>
<!-- 엔진 -->
<div class="chk-group">
<div class="chk-glabel">엔진</div>
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct"><code style="font-family:var(--font-num);font-size:12px;background:var(--paper-2);padding:1px 6px;border-radius:5px;">claude -p</code> 상주 멀티턴 게이트웨이 <span class="tag verified">실동작 검증 완료</span><br><span style="font-size:12px;color:var(--ink-3);">컨텍스트 유지 + 캐시 재사용 확인</span></span></div>
</div>
<!-- P0 코드 -->
<div class="chk-group">
<div class="chk-glabel">P0 코드 · 구문검증 통과</div>
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">DB 스키마 <b>35테이블</b> <span style="color:var(--ink-3);font-variant-numeric:tabular-nums;">(727줄)</span></span></div>
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">FastAPI 골격</span></div>
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">taxonomy <span style="color:var(--ink-3);font-variant-numeric:tabular-nums;">(285줄)</span></span></div>
</div>
</div>
<section>
<div class="section-head">
<div>
<h2>완료</h2>
<p>새 세션에서 반복하지 않아도 되는 작업</p>
</div>
</section>
</div>
<div class="grid">
<article class="panel">
<span class="tag done">E2E persona decoupled</span>
<h3>DB persona 기반 테스트</h3>
<p>`session-persistence`, `teacher`, `voice`는 `/api/personas`에서 실제 DB persona를 가져온다.</p>
</article>
<article class="panel">
<span class="tag done">fail-closed</span>
<h3>비개발 런타임 보호</h3>
<p>prod/staging에서 dev login, seed fallback, OAuth 누락, 기본 secret, localhost CORS를 거부한다.</p>
</article>
<article class="panel">
<span class="tag done">infra</span>
<h3>compose OAuth 주입</h3>
<p>`ENVIRONMENT`, Google OAuth vars, `CORS_ORIGINS`, `OPENAI_BASE_URL`을 API 컨테이너에 명시했다.</p>
</article>
<article class="panel">
<span class="tag done">admin UX</span>
<h3>모바일 overflow 수정</h3>
<p>긴 readiness detail을 안정화했고, 사용자 변경은 DB 저장소가 durable할 때만 허용한다. AI 운영은 실제 어댑터명과 health 상태를 표시한다.</p>
</article>
<article class="panel">
<span class="tag done">test isolation</span>
<h3>global engine config 분리</h3>
<p>`db-persistence`는 전역 engine config를 바꾸므로 `@single-run`으로 분리했다.</p>
</article>
<article class="panel">
<span class="tag done">docs</span>
<h3>handoff 갱신</h3>
<p>`docs/HANDOFF.md`, `docs/dev_dashboard.html`, `apps/web/e2e/README.md`를 최신 상태로 맞췄다.</p>
</article>
<article class="panel">
<span class="tag done">public prod</span>
<h3>공개 API prod 전환</h3>
<p>`cloudflared` ingress를 `127.0.0.1:8001` prod API로 돌렸고 public readiness gate가 통과했다.</p>
</article>
<article class="panel">
<span class="tag done">ops script</span>
<h3>공개 런타임 재기동</h3>
<p>`scripts/start-public-runtime.ps1`와 `scripts/stop-public-runtime.ps1`로 API 8001과 cloudflared를 재현 가능하게 만들었다.</p>
</article>
<article class="panel">
<span class="tag done">auth boundary</span>
<h3>persona catalog 인증화</h3>
<p>`/personas`는 이제 비로그인 401이다. 로컬과 공개 API 모두 직접 확인했고 E2E 회귀를 추가했다.</p>
</article>
<article class="panel">
<span class="tag done">public web</span>
<h3>API base fallback 보강</h3>
<p>public 웹에서 `/api/auth/config`가 SPA HTML로 떨어지지 않도록 public API origin fallback을 추가했다.</p>
</article>
<article class="panel">
<span class="tag done">learner UX</span>
<h3>0회 기록 상태 보강</h3>
<p>학습자 홈은 이제 fake 기록 없이 실제 서버 세션 0개를 `기존 회기` 빈 상태와 0 카운트로 표시한다.</p>
</article>
<article class="panel">
<span class="tag done">local login</span>
<h3>5175 로컬 재기동 검증</h3>
<p>`127.0.0.1:5175` web, `8000` API, `9099` engine을 재기동하고 dev-login 서버 세션과 `/learn` redirect를 확인했다.</p>
</article>
<article class="panel">
<span class="tag done">session UX</span>
<h3>dense viewport 보정</h3>
<p>375px급 active session에서 stage 내부가 1px 넘게 잘리던 문제를 avatar/padding 조정으로 제거했다.</p>
</article>
<article class="panel">
<span class="tag done">real stream</span>
<h3>텍스트 턴 SSE 전환</h3>
<p>`POST /sessions/{id}/stream` token 이벤트로만 내담자 자막을 쌓고, HTTP 거절과 SSE error 모두 실패 턴을 transcript와 저장소에 남기지 않는다.</p>
</article>
<article class="panel">
<span class="tag done">voice turn</span>
<h3>음성 실패 턴 미저장</h3>
<p>음성 회기도 엔진 응답 생성 성공 후에만 learner/client turn을 저장한다. 실패한 transcript는 pending 상태에서 제거한다.</p>
</article>
<article class="panel">
<span class="tag done">ops copy</span>
<h3>비영구 상태 문구 정리</h3>
<p>admin health와 voice degraded reason은 내부 fallback/seed 표현 대신 비영구 런타임 상태를 사용자용 문구로 표시한다.</p>
</article>
<article class="panel">
<span class="tag done">route guard</span>
<h3>직접 URL persona 차단</h3>
<p>`/learn/session/:code`로 직접 들어와도 DB 원본 persona가 아니면 회기 시작을 막는다. degraded/seed fallback 회귀를 추가했다.</p>
</article>
<article class="panel">
<span class="tag done">avatar</span>
<h3>demo Live2D fallback 제거</h3>
<p>Mao/Haru 샘플은 공용 fallback으로 쓰지 않는다. 실제 persona별 `live2dModelUrl`이 있을 때만 Live2D를 렌더링한다.</p>
</article>
</div>
</section>
<section class="block">
<div class="sechead">
<div class="kicker"><span class="dot"></span>진행중 · 다음</div>
<div class="desc">P0 마무리와 P1 진입 작업.</div>
<section>
<div class="section-head">
<div>
<h2>검증</h2>
<p>마지막으로 직접 실행한 결과</p>
</div>
</div>
<table>
<thead><tr><th>범위</th><th>명령</th><th>결과</th></tr></thead>
<tbody>
<tr><td>Web typecheck</td><td><code>npm run typecheck</code></td><td>Passed</td></tr>
<tr><td>Backend</td><td><code>python -m unittest app.test_runtime_policy app.test_session_turn_persistence engine_gateway.test_gateway_model</code></td><td>20 tests OK</td></tr>
<tr><td>Compile</td><td><code>python -m compileall app engine_gateway</code></td><td>Passed</td></tr>
<tr><td>Web build</td><td><code>npm run build</code></td><td>Passed</td></tr>
<tr><td>Compose</td><td><code>docker compose --env-file .env.example config --quiet</code></td><td>Passed with dummy required env</td></tr>
<tr><td>Focused E2E</td><td><code>admin + db-persistence + voice-success</code></td><td>10 passed</td></tr>
<tr><td>Admin manage-users</td><td><code>admin.spec.ts --grep manage real server-known users</code></td><td>desktop/mobile 2 passed</td></tr>
<tr><td>Full E2E</td><td><code>PLAYWRIGHT_HOST=127.0.0.1 npx playwright test</code></td><td>72 passed</td></tr>
<tr><td>Public auth discovery</td><td><code>E2E_PUBLIC_AUTH=1 npx playwright test --list --project=chromium-public-auth</code></td><td>2 tests listed</td></tr>
<tr><td>Public readiness</td><td><code>public-auth-turn.spec.ts --grep production-safe</code></td><td>1 passed</td></tr>
<tr><td>Persona auth boundary</td><td><code>GET /personas</code></td><td>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 script</td><td><code>scripts\start-public-runtime.ps1</code></td><td>API 8001 + cloudflared running</td></tr>
<tr><td>Public API health</td><td><code>https://api-vignette.chanpaca.net/health</code></td><td>prod, db true, engine true</td></tr>
<tr><td>Local 5175 login</td><td><code>PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 auth.spec.ts</code></td><td>desktop/mobile passed</td></tr>
<tr><td>Learner/readiness E2E</td><td><code>learner.spec.ts + readiness.spec.ts desktop/mobile</code></td><td>14 passed</td></tr>
<tr><td>Learner screenshots</td><td><code>learn-empty-desktop.png / learn-empty-mobile-compact.png</code></td><td>document overflow 0, empty history visible</td></tr>
<tr><td>Session layout/turn</td><td><code>session-layout.spec.ts desktop/mobile</code></td><td>8 passed</td></tr>
<tr><td>Voice/session focused</td><td><code>voice + voice-success + session-layout</code></td><td>15 passed</td></tr>
<tr><td>Latest focused E2E</td><td><code>learner + session-layout + settings + admin</code></td><td>37 passed</td></tr>
</tbody>
</table>
</section>
<div class="panel">
<div class="nextlist">
<div class="nx">
<span class="nd doing"></span>
<span class="nt">DB 실제 마이그레이션 <span class="st blocked">블로커</span>
<span class="why">Docker Desktop 띄워 psql 적용 — Docker 꺼져 있어 미완</span>
</span>
</div>
<div class="nx">
<span class="nd queued"></span>
<span class="nt">상담 루프 연결 <span class="st queued">P1</span>
<span class="why">엔진 + 상태머신 + 실시간 피드백</span>
</span>
</div>
<div class="nx">
<span class="nd queued"></span>
<span class="nt">Rive 아바타 통합 <span class="st queued">P1</span></span>
</div>
<div class="nx">
<span class="nd queued"></span>
<span class="nt">OpenAI 음성 캐스케이드 <span class="st queued">P1</span></span>
</div>
<div class="nx">
<span class="nd queued"></span>
<span class="nt">React 프론트 연결 <span class="st queued">P1</span></span>
</div>
</div>
<section>
<div class="section-head">
<div>
<h2>남은 리스크</h2>
<p>다음 세션 첫 작업</p>
</div>
</section>
</div>
</div>
<div class="grid">
<article class="panel">
<span class="tag risk">blocker</span>
<h3>public `/turn` proof 없음</h3>
<p>2026-06-26 14:43 KST 기준 Google 로그인 storageState가 없어 공개 API `/turn` 실제 성공 증거가 아직 없다.</p>
</article>
<article class="panel">
<span class="tag doing">ops</span>
<h3>공개 API 프로세스 유지</h3>
<p>터널은 `127.0.0.1:8001` prod API를 본다. 재부팅 후에는 prod API와 cloudflared를 같이 재기동해야 한다.</p>
</article>
<article class="panel">
<span class="tag doing">check before deploy</span>
<h3>engine config 재확인</h3>
<p>E2E가 admin engine config를 건드린다. 배포 전 의도한 model/url/mode인지 다시 확인해야 한다.</p>
</article>
</div>
</section>
<!-- 우측: 확정 결정 + 리스크/블로커 -->
<div>
<section class="block">
<div class="sechead">
<div class="kicker"><span class="dot"></span>확정 결정</div>
<div class="desc">되돌리지 않는 아키텍처·운영 기준선.</div>
<section>
<div class="section-head">
<div>
<h2>다음 명령</h2>
<p>공개 OAuth smoke</p>
</div>
</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
<div class="panel">
<div class="decisions">
<div class="dc">
<span class="dk">엔진</span>
<span class="dv">로컬 <b>claude -p</b> 상주풀 <span style="color:var(--ink-3);">(API 폴백)</span></span>
</div>
<div class="dc">
<span class="dk">아바타</span>
<span class="dv"><b>Rive</b></span>
</div>
<div class="dc span">
<span class="dk">기술 스택</span>
<span class="dv">
<span class="stack-chips">
<span class="c key">React 19</span>
<span class="c key">FastAPI</span>
<span class="c key">PG 16 + pgvector</span>
<span class="c">Docker</span>
</span>
</span>
</div>
<div class="dc span">
<span class="dk">운영 디폴트 6종</span>
<span class="dv" style="font-size:13px;color:var(--ink-2);line-height:1.65;">
s2s 2차 · 평가 AI 로컬 · 재귀학습 few-shot · 자유연습 ON (교수 토글) · 스케일업 compose
</span>
</div>
</div>
$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>
</section>
<section>
<div class="section-head">
<div>
<h2>주의</h2>
<p>작업 방식</p>
</div>
</section>
<section class="block">
<div class="sechead">
<div class="kicker"><span class="dot"></span>리스크 · 블로커</div>
<div class="desc">빨강은 데이터·외부 의존, 주황은 검증 미완.</div>
</div>
<div class="panel">
<div class="risks">
<div class="rk">
<span class="sev high"><span class="d"></span>HIGH</span>
<span class="rbody">
<span class="rt">데이터 n=1</span>
<span class="rd">0615 단일 축어록뿐. 추가 사례 수급은 한신대 협의 필요 <span class="ext">외부 블로커</span></span>
</span>
</div>
<div class="rk">
<span class="sev mid"><span class="d"></span>MID</span>
<span class="rbody">
<span class="rt">DB 실마이그레이션 미검증</span>
<span class="rd">Docker Desktop 기동 후 psql 적용 필요 — 현재 Docker 꺼짐</span>
</span>
</div>
<div class="rk">
<span class="sev mid"><span class="d"></span>MID</span>
<span class="rbody">
<span class="rt">미성년 사례데이터 활용동의</span>
<span class="rd">한신대 공문 필요 <span class="ext">외부 협의</span></span>
</span>
</div>
</div>
</div>
</section>
</div>
</div>
<!-- 3. 산출물 인벤토리 -->
<section class="block">
<div class="sechead">
<div class="kicker"><span class="dot"></span>산출물 인벤토리</div>
<div class="desc">repo 경로 기준. 카드가 아니라 표로 — 전체를 한 번에 훑습니다.</div>
</div>
<table class="inv-tbl">
<thead>
<tr>
<th style="width:120px;">영역</th>
<th>내용</th>
<th>repo 경로</th>
<th style="width:88px;">상태</th>
</tr>
</thead>
<tbody>
<tr>
<td class="area"><span class="kd"><span class="d"></span>설계 문서</span></td>
<td class="what">마스터플랜 · 적대검증(red team) · 메모리·페르소나 설계 · 디자인 컨셉</td>
<td class="path"><code>docs/MASTERPLAN.md</code><code>docs/redteam/</code><code>docs/MEMORY_KNOWLEDGE_PERSONA_DESIGN.md</code><code>docs/DESIGN_CONCEPT.md</code></td>
<td class="stt"><span class="invtag done"><span class="d"></span>완료</span></td>
</tr>
<tr>
<td class="area"><span class="kd"><span class="d"></span>디자인 시안</span></td>
<td class="what">화면 7종 목업 (login · learner-home · session · session-review · professor · admin · settings)</td>
<td class="path"><code>docs/mockups/</code> <span style="color:var(--ink-3);font-family:var(--font-num);">(7)</span></td>
<td class="stt"><span class="invtag done"><span class="d"></span>완료</span></td>
</tr>
<tr>
<td class="area"><span class="kd"><span class="d"></span>인프라</span></td>
<td class="what">Docker 이식 구성 — compose + Caddy + 엔진 어댑터 분리</td>
<td class="path"><code>infra/</code></td>
<td class="stt"><span class="invtag partial"><span class="d"></span>구성 완료 · 마이그레이션 미완</span></td>
</tr>
<tr>
<td class="area"><span class="kd"><span class="d"></span>엔진 · 백엔드</span></td>
<td class="what">claude -p 상주 게이트웨이 (실동작 검증) + FastAPI 골격 + DB 스키마 35테이블 + taxonomy</td>
<td class="path"><code>apps/api/</code></td>
<td class="stt"><span class="invtag partial"><span class="d"></span>골격 · 구문검증 통과</span></td>
</tr>
</tbody>
</table>
<div class="foot">
<span>repo · github.com/yunchan8804-blip/vignette (private)</span>
<span class="fdot"></span>
<span>기준일 2026-06-25</span>
<span class="fdot"></span>
<span>Vignette 개발팀 내부 PM 보드</span>
</div>
</section>
</div>
</div>
<ul class="list">
<li>`frontenddesign` 스킬은 현재 세션에 없었다. 다음 세션에서 보이면 먼저 사용하고, 없으면 `docs/DESIGN_CONCEPT.md`를 따른다.</li>
<li>학습자 홈과 active session UX는 사용자가 강하게 불만을 말한 영역이다. fake 기록/통계 금지, 0회 기록은 실제 서버 세션 0개 기반 빈 상태로 유지, active session 전체화면/무문서스크롤 유지.</li>
<li>worktree는 원래부터 dirty다. 관련 없는 변경을 되돌리지 말 것.</li>
<li>OAuth secret은 출력하지 말 것. 실제 secret은 환경 파일/인프라 설정에만 둔다.</li>
</ul>
</section>
</main>
</body>
</html>