feat: P1 풀빌드 — React 프론트 7화면 + 백엔드 상담루프·평가·음성·RAG

web (Vite+React19+TS, Cloudflare Pages 배포):
- 디자인토큰(세이지틸/테라코타 SSOT), 앱셸, 공통 UI 프리미티브
- 7화면: 로그인/학습자홈/상담세션/회기리뷰/교수자/관리자/설정
- ClientAvatar: SVG 반구상 흉상 4상태 + RMS 립싱크 + 6파라미터 정서
- 회기리뷰는 외부 레퍼런스 디자인을 Vignette 토큰으로 리스킨

api (FastAPI):
- 게이트웨이 /v1/generate·/v1/stream 어댑터(상주풀/EngineSession 보존)
- services: 페르소나 L0~L6 빌더 / 결정론 상태머신 / 가드레일 /
  턴 오케스트레이터 / 회기간 메모리 / 평가AI / 음성 / RAG
- store: DB off 폴백(in-memory), sessions 실구현

검증:
- web: node22 tsc+vite build 통과(node23 segfault 회피), Pages 배포 200
- api: app.main import 통과
- 핫픽스: Topbar initials undefined-safe (undefined.trim 크래시)
- E2E: 서연(P1) 상담 1턴 — 좋은/나쁜 상담에 차등 반응 실증
This commit is contained in:
Yun Chan 2026-06-25 23:37:22 +09:00
parent 859ab26314
commit 24b1b7a6e1
84 changed files with 19645 additions and 107 deletions

View file

@ -0,0 +1,332 @@
/* =====================================================================
UI 프리미티브 스타일 토큰 변수만 참조(tokens.css).
철칙: border-left 강조선 0 · 카드 상단 강조선 0 · 알약 radius 금지(원형은 dot/오브/아바타만).
강조 = weight + accent 텍스트 + accent-tint 배경 + kicker(+dot).
===================================================================== */
/* ── Kicker (상단 소제목, 양산형 좌측바 대체 위계 신호) §3.6 ── */
.vg-kicker {
font-family: var(--font-num);
font-size: var(--fs-kicker);
font-weight: 600;
letter-spacing: 0.08em;
color: var(--accent);
display: inline-flex;
align-items: center;
gap: 8px;
text-transform: none;
}
.vg-kicker__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent-bright);
flex: none;
}
/* ── SectionHead ── */
.vg-sechead {
margin-bottom: var(--sp-5);
}
.vg-sechead__kicker {
margin-bottom: 6px;
}
.vg-sechead__title {
font-size: var(--fs-h2);
font-weight: 600;
letter-spacing: -0.015em;
color: var(--text-strong);
line-height: 1.35;
}
.vg-sechead__desc {
font-size: var(--fs-xs);
color: var(--text-muted);
margin-top: 4px;
}
/* ── Button (위계 3단 + danger). radius 8px, 알약 금지 §7.1 ── */
.vg-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font: 600 var(--fs-sm) / 1 var(--font-sans);
padding: 10px 18px;
border-radius: var(--radius);
border: 1px solid transparent;
transition:
background var(--dur-base) var(--ease-out),
border-color var(--dur-base) var(--ease-out),
transform var(--dur-fast) var(--ease-out);
cursor: pointer;
white-space: nowrap;
user-select: none;
}
.vg-btn:active {
transform: translateY(0.5px);
}
.vg-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.vg-btn--block {
width: 100%;
}
.vg-btn--sm {
padding: 7px 13px;
font-size: var(--fs-xs);
}
.vg-btn--lg {
padding: 13px 22px;
font-size: var(--fs-body);
}
.vg-btn--primary {
background: var(--accent);
color: var(--text-on-accent);
}
.vg-btn--primary:hover:not(:disabled) {
background: var(--accent-deep);
}
.vg-btn--secondary {
background: var(--bg-surface);
color: var(--accent);
border-color: var(--accent);
}
.vg-btn--secondary:hover:not(:disabled) {
background: var(--accent-tint);
}
.vg-btn--ghost {
background: transparent;
color: var(--text-body);
}
.vg-btn--ghost:hover:not(:disabled) {
background: var(--bg-surface-2);
color: var(--text-strong);
}
.vg-btn--danger {
background: var(--crit-solid);
color: #fff;
}
.vg-btn--danger:hover:not(:disabled) {
background: var(--crit-text);
}
/* ── Card / Panel §7.3 (헤어라인 + 미세 그림자, 상단 강조선 금지) ── */
.vg-card {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius);
padding: var(--sp-5);
box-shadow: var(--shadow-sm);
}
.vg-card--flat {
box-shadow: none;
}
.vg-card--tint {
background: var(--bg-tint);
border-color: transparent;
box-shadow: none;
}
.vg-panel {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
padding: var(--sp-6);
box-shadow: var(--shadow-sm);
}
.vg-panel--flat {
box-shadow: none;
}
.vg-panel--tint {
background: var(--bg-tint);
border-color: transparent;
box-shadow: none;
}
/* ── Badge (배경 틴트 + 텍스트, 좌측바 없음) ── */
.vg-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: var(--font-num);
font-size: var(--fs-xs);
font-weight: 600;
letter-spacing: 0.01em;
padding: 2px 9px;
border-radius: 100px; /* 칩/배지만 100px 관용 (DESIGN_CONCEPT 데모 일치) */
line-height: 1.5;
}
.vg-badge--neutral {
color: var(--ink-2);
background: var(--paper-2);
}
.vg-badge--accent {
color: var(--accent-deep);
background: var(--accent-tint);
}
.vg-badge--pos {
color: var(--pos-text);
background: var(--pos-tint);
}
.vg-badge--warn {
color: var(--warn-text);
background: var(--warn-tint);
}
.vg-badge--crit {
color: var(--crit-text);
background: var(--crit-tint);
}
.vg-badge--info {
color: var(--info-text);
background: var(--info-tint);
}
/* ── Dot (상태 점). 원형 예외 허용 대상 ── */
.vg-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
flex: none;
background: var(--neutral-200);
}
.vg-dot--accent {
background: var(--accent);
}
.vg-dot--pos {
background: var(--pos-solid);
}
.vg-dot--warn {
background: var(--warn-solid);
}
.vg-dot--crit {
background: var(--crit-solid);
}
.vg-dot--info {
background: var(--info-solid);
}
.vg-dot--muted {
background: var(--ink-3);
}
/* ── StatLine (요약 스탯, tabular) ── */
.vg-statline {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--sp-5);
}
.vg-statline__item {
display: inline-flex;
align-items: baseline;
gap: 7px;
}
.vg-statline__num {
font-size: 20px;
font-weight: 700;
color: var(--text-strong);
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
.vg-statline__lab {
font-size: var(--fs-xs);
color: var(--text-muted);
}
.vg-statline__sep {
width: 1px;
height: 18px;
background: var(--hair);
}
/* ── ProgressBar (가로 게이지, 라운드 없는 사각 트랙) §5.5/§6.4 ── */
.vg-progress {
width: 100%;
height: 8px;
background: var(--paper-2);
border-radius: 4px;
overflow: hidden;
}
.vg-progress--slim {
height: 4px;
}
.vg-progress__fill {
height: 100%;
border-radius: 4px;
background: var(--accent);
transition: width var(--dur-slow) var(--ease-out);
}
.vg-progress__fill--muted {
background: var(--neutral-200);
}
.vg-progress__fill--warn {
background: var(--warn-solid);
}
.vg-progress__fill--clay {
background: var(--clay);
}
/* ── Field / Input §7.2 (radius 6px, 포커스 보더색+ring, 좌측바 X) ── */
.vg-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.vg-field__label {
font-size: var(--fs-sm);
font-weight: 600;
color: var(--text-strong);
}
.vg-field__hint {
font-size: var(--fs-xs);
color: var(--text-muted);
}
.vg-field__error {
font-size: var(--fs-xs);
color: var(--crit-text);
}
.vg-input {
width: 100%;
background: var(--bg-surface-2);
color: var(--text-strong);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
padding: 10px 12px;
font: var(--fs-body) / 1.5 var(--font-sans);
transition:
border-color var(--dur-fast) var(--ease-out),
box-shadow var(--dur-fast) var(--ease-out);
}
.vg-input::placeholder {
color: var(--neutral-400);
}
.vg-input:focus {
border-color: var(--border-focus);
outline: none;
box-shadow: 0 0 0 3px var(--focus-ring);
}
.vg-input[aria-invalid="true"] {
border-color: var(--crit-solid);
}
/* ── 빈/준비중 상태 (스텁 페이지 공통) ── */
.vg-empty {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--sp-3);
max-width: var(--maxw-read);
}
.vg-empty__title {
font-size: var(--fs-h1);
font-weight: 700;
letter-spacing: -0.02em;
color: var(--text-strong);
line-height: 1.3;
}
.vg-empty__desc {
font-size: var(--fs-lead);
color: var(--text-body);
line-height: 1.6;
}