diff --git a/apps/web/e2e/full-sweep-session.spec.ts b/apps/web/e2e/full-sweep-session.spec.ts index 46d66c3..3b865dd 100644 --- a/apps/web/e2e/full-sweep-session.spec.ts +++ b/apps/web/e2e/full-sweep-session.spec.ts @@ -673,7 +673,8 @@ test.describe("full sweep — counseling session", () => { await routeSessionFixtureApi(page); await startFixtureSession(page); - await expect(page.locator(".sx-signal__rest")).toContainText("아직 표시할 신호가 없어요"); + await expect(page.locator(".sx-signal__rest")).toHaveCount(0); + await expect(page.locator(".sx-coach-card")).toBeVisible(); await page.getByLabel("학습자 발화 입력").fill(learnerText); await page.getByRole("button", { name: "보내기" }).click(); @@ -702,6 +703,7 @@ test.describe("full sweep — counseling session", () => { test("shows the coach nudge for a pending turn and requests coaching immediately when opened", async ({ page, }) => { + await page.setViewportSize({ width: 1366, height: 640 }); const api = await routeSessionFixtureApi(page); await startFixtureSession(page); @@ -716,12 +718,175 @@ test.describe("full sweep — counseling session", () => { expect(api.liveCoachRequests).toHaveLength(0); await nudge.click(); - await expect(page.locator(".sx-coach-card").getByText("감정 반영이 선명합니다")).toBeVisible(); + const coachCard = page.locator(".sx-coach-card"); + await expect(coachCard.getByText("감정 반영이 선명합니다")).toBeVisible(); + await expect(page.locator(".sx-signal__wave")).toHaveCount(0); + + const railLayout = await page.evaluate(() => { + const panel = document.querySelector(".sx-signal"); + const coach = document.querySelector(".sx-coach-card"); + const signal = document.querySelector(".sx-signal__one"); + if (!panel || !coach || !signal) return null; + const panelRect = panel.getBoundingClientRect(); + const coachRect = coach.getBoundingClientRect(); + const signalRect = signal.getBoundingClientRect(); + return { + coachBeforeSignal: coachRect.top < signalRect.top, + coachInsidePanel: + coachRect.top >= panelRect.top - 1 && coachRect.bottom <= panelRect.bottom + 1, + coachInsideViewport: coachRect.bottom <= window.innerHeight + 1, + }; + }); + expect(railLayout).toEqual({ + coachBeforeSignal: true, + coachInsidePanel: true, + coachInsideViewport: true, + }); + + await page.setViewportSize({ width: 1024, height: 640 }); + await expect(coachCard).toBeVisible(); + await expect(page.locator(".sx-page--active .sx-col-right")).toBeVisible(); + await expect(page.locator(".sx-signal__one")).toBeHidden(); + const compactCoachInsideViewport = await coachCard.evaluate((element) => { + const rect = element.getBoundingClientRect(); + return rect.top >= -1 && rect.bottom <= window.innerHeight + 1; + }); + expect(compactCoachInsideViewport).toBe(true); + await expect(nudge).toHaveCount(0); expect(api.liveCoachRequests).toHaveLength(1); expect(api.liveCoachRequests[0]).toMatchObject({ learner_text: learnerText }); }); + test("uses the global light theme tokens throughout an active session", async ({ page }) => { + await page.addInitScript(() => { + localStorage.setItem("vignette.theme", "light"); + }); + await routeSessionFixtureApi(page); + + await startFixtureSession(page); + await expect(page.locator("html")).toHaveAttribute("data-theme", "light"); + await page.getByRole("button", { name: /코칭 모드, 남은 기회/ }).click(); + await expect(page.locator(".sx-coach-bubble")).toBeVisible(); + + const themeState = await page.evaluate(() => { + const rootStyle = getComputedStyle(document.documentElement); + const session = document.querySelector(".sx-page--active"); + const coachBubble = document.querySelector(".sx-coach-bubble"); + if (!session || !coachBubble) return null; + const sessionStyle = getComputedStyle(session); + const coachStyle = getComputedStyle(coachBubble); + return { + rootSurface: rootStyle.getPropertyValue("--bg-surface").trim(), + sessionSurface: sessionStyle.getPropertyValue("--bg-surface").trim(), + rootText: rootStyle.getPropertyValue("--text-strong").trim(), + sessionText: sessionStyle.getPropertyValue("--text-strong").trim(), + colorScheme: sessionStyle.colorScheme, + coachInsetSurface: + coachBubble.classList.contains("vg-surface--inset") && + coachStyle.getPropertyValue("--glass-surface-inset").trim() === + rootStyle.getPropertyValue("--glass-surface-inset").trim(), + }; + }); + + expect(themeState).not.toBeNull(); + expect(themeState!.sessionSurface).toBe(themeState!.rootSurface); + expect(themeState!.sessionText).toBe(themeState!.rootText); + expect(themeState!.colorScheme).toBe("light"); + expect(themeState!.coachInsetSurface).toBe(true); + }); + + test("matches the 1536px botanical session workspace composition", async ({ + page, + }, testInfo) => { + await page.setViewportSize({ width: 1536, height: 1024 }); + await page.addInitScript(() => { + localStorage.setItem("vignette.theme", "light"); + }); + await routeSessionFixtureApi(page); + + await startFixtureSession(page); + await expect(page.getByText("라이브 코칭", { exact: true })).toBeVisible(); + await expect(page.locator(".sx-coach-card")).toBeVisible(); + await expect(page.getByRole("button", { name: "상태 신호" })).toHaveClass(/is-on/); + + const layout = await page.evaluate(async () => { + const pageRoot = document.querySelector(".sx-page--active"); + const bar = document.querySelector(".sx-sessionbar"); + const grid = document.querySelector(".sx-grid"); + const left = document.querySelector(".sx-col-left"); + const center = document.querySelector(".sx-col-center"); + const right = document.querySelector(".sx-col-right"); + const controls = document.querySelector(".sx-controlbar"); + const mic = document.querySelector(".sx-mic"); + const micText = document.querySelector(".sx-mic-block__ms"); + if (!pageRoot || !bar || !grid || !left || !center || !right || !controls || !mic || !micText) { + return null; + } + const rect = (element: HTMLElement) => { + const value = element.getBoundingClientRect(); + return { + left: Math.round(value.left), + top: Math.round(value.top), + right: Math.round(value.right), + bottom: Math.round(value.bottom), + width: Math.round(value.width), + height: Math.round(value.height), + }; + }; + const leafResponse = await fetch("/session-botanical/leaf-1.webp"); + return { + viewport: { width: window.innerWidth, height: window.innerHeight }, + page: rect(pageRoot), + bar: rect(bar), + grid: rect(grid), + left: rect(left), + center: rect(center), + right: rect(right), + controls: rect(controls), + mic: rect(mic), + micText: rect(micText), + pageLeaf: getComputedStyle(pageRoot, "::before").backgroundImage, + stageLeaf: getComputedStyle( + document.querySelector(".sx-orb-wrap")!, + "::before", + ).backgroundImage, + leafAsset: { + ok: leafResponse.ok, + contentType: leafResponse.headers.get("content-type"), + }, + }; + }); + + expect(layout).not.toBeNull(); + expect(layout!.viewport).toEqual({ width: 1536, height: 1024 }); + expect(layout!.page).toMatchObject({ left: 0, top: 0, width: 1536, height: 1024 }); + expect(layout!.bar.width).toBeGreaterThanOrEqual(1400); + expect(layout!.bar.width).toBeLessThanOrEqual(1410); + expect(layout!.grid.width).toBeGreaterThanOrEqual(1400); + expect(layout!.grid.width).toBeLessThanOrEqual(1410); + expect(layout!.left.width).toBeGreaterThanOrEqual(324); + expect(layout!.left.width).toBeLessThanOrEqual(328); + expect(layout!.right.width).toBeGreaterThanOrEqual(355); + expect(layout!.right.width).toBeLessThanOrEqual(359); + expect(layout!.center.left).toBeGreaterThan(layout!.left.right); + expect(layout!.right.left).toBeGreaterThan(layout!.center.right); + expect(layout!.controls.width).toBeGreaterThanOrEqual(1460); + expect(layout!.controls.width).toBeLessThanOrEqual(1470); + expect(layout!.controls.bottom).toBeLessThanOrEqual(1024); + expect(layout!.micText.left).toBeGreaterThanOrEqual(layout!.mic.right + 8); + expect(layout!.pageLeaf).toContain("leaf-5.webp"); + expect(layout!.stageLeaf).toContain("leaf-1.webp"); + expect(layout!.leafAsset).toEqual({ ok: true, contentType: "image/webp" }); + + const screenshotPath = testInfo.outputPath("session-botanical-1536x1024.png"); + await page.screenshot({ path: screenshotPath, animations: "disabled" }); + await testInfo.attach("session-botanical-1536x1024", { + path: screenshotPath, + contentType: "image/png", + }); + }); + // checklist: session-meters-collapse // 모바일(≤880px) 압축 레이아웃은 관찰 게이지 패널을 의도적으로 숨긴다(session.css) — // 데스크톱 전용 UI라 desktop 프로젝트에서만 검증한다. diff --git a/apps/web/public/session-botanical/leaf-1.webp b/apps/web/public/session-botanical/leaf-1.webp new file mode 100644 index 0000000..58119d1 Binary files /dev/null and b/apps/web/public/session-botanical/leaf-1.webp differ diff --git a/apps/web/public/session-botanical/leaf-2.webp b/apps/web/public/session-botanical/leaf-2.webp new file mode 100644 index 0000000..7d5f355 Binary files /dev/null and b/apps/web/public/session-botanical/leaf-2.webp differ diff --git a/apps/web/public/session-botanical/leaf-3.webp b/apps/web/public/session-botanical/leaf-3.webp new file mode 100644 index 0000000..a51d3e9 Binary files /dev/null and b/apps/web/public/session-botanical/leaf-3.webp differ diff --git a/apps/web/public/session-botanical/leaf-4.webp b/apps/web/public/session-botanical/leaf-4.webp new file mode 100644 index 0000000..0c55d7b Binary files /dev/null and b/apps/web/public/session-botanical/leaf-4.webp differ diff --git a/apps/web/public/session-botanical/leaf-5.webp b/apps/web/public/session-botanical/leaf-5.webp new file mode 100644 index 0000000..9cec8d3 Binary files /dev/null and b/apps/web/public/session-botanical/leaf-5.webp differ diff --git a/apps/web/src/components/ui/Icon.tsx b/apps/web/src/components/ui/Icon.tsx index eab7090..5bab762 100644 --- a/apps/web/src/components/ui/Icon.tsx +++ b/apps/web/src/components/ui/Icon.tsx @@ -9,6 +9,9 @@ import type { ReactNode, SVGProps } from "react"; export type IconName = | "home" + | "clock" + | "hourglass" + | "spark" | "session" | "review" | "users" @@ -50,6 +53,24 @@ export interface IconProps extends Omit, "name"> { // 각 아이콘의 path/children (24x24 viewBox 기준). google 만 멀티컬러 예외. const PATHS: Record = { + clock: ( + <> + + + + ), + hourglass: ( + <> + + + + ), + spark: ( + <> + + + + ), refresh: ( <> diff --git a/apps/web/src/pages/Session.tsx b/apps/web/src/pages/Session.tsx index af711e1..ca1dba1 100644 --- a/apps/web/src/pages/Session.tsx +++ b/apps/web/src/pages/Session.tsx @@ -1959,8 +1959,8 @@ export default function Session() { voiceAvailable, micOn, }); - const elapsedLabel = formatElapsed(elapsed); - const remainingLabel = formatElapsed(remainingSeconds); + const elapsedLabel = formatTimecode(elapsed); + const remainingLabel = formatTimecode(remainingSeconds); const limitMinutesLabel = Math.round(sessionLimitSeconds / 60); const warningMinutesLabel = Math.max(1, Math.round(sessionWarningSeconds / 60)); const selectedTheoryOption = @@ -2544,15 +2544,24 @@ export default function Session() {
{elapsedLabel} - 회기 시간 + + + 경과 시간 + {timeUp ? "정리 시간" : remainingLabel} - {limitMinutesLabel}분 회기 · 남은 시간 + + + 남은 시간 + {turnCount}턴 - 저장된 발화 + + + 저장된 발화 +
{goalsAchieved && !timeUp && !sessionEnded ? ( @@ -2610,6 +2619,7 @@ export default function Session() {
+ 지금: {stageStateText[avatarState]} {avatarExpressionLabel}
@@ -2783,8 +2793,7 @@ export default function Session() { {feedbackMode !== "immersive" ? (
- {/* D1 — 신호 상태는 아래 sx-signal__one-dot 이 색으로 전달 → 라벨 dot 제거 */} - 라이브 신호 + 라이브 코칭
{liveSignal ? ( @@ -2811,148 +2820,135 @@ export default function Session() { 방금 )} - ) : ( -

- 아직 표시할 신호가 없어요. 대화가 이어지면 여기에 짧게 비춰 드릴게요. -

- )} + ) : null} - - -
+
- 음성 입력 + + 음성 {voiceInputStatus} - 응답 상태 + + AI 응답 {responseStatus} - - 화면 모드 - - {feedbackMode === "ambient" - ? "상태 신호" - : feedbackMode === "coached" - ? "코칭" - : "몰입"} - -
-
- 최근 흐름 - - {signalSeq.map((t, i) => ( - - ))} - -
- - {feedbackMode === "coached" ? ( -
- -
-
- {coachIsDegraded ? "대체 코칭" : "AI 코치"} - {coachStatusText} -
- {coachIsDegraded ? ( -

{coachDegradedNote}

- ) : null} - {coachSyncWarning ? ( -

0 ? ( +

+ 최근 흐름 + + {signalSeq.map((t, i) => ( + - {coachSyncWarning} -

- ) : null} -
- 코칭 기회 - - - {coachQuotaRemaining}/{coachQuotaMax} - -
- {coachCreditPulseText ? ( -
- {coachCreditPulseText} -
- ) : null} - {coachLoading ? ( -

방금 턴과 근거 자료를 대조하는 중입니다.

- ) : coachError ? ( -

{coachError}

- ) : coachSuggestion ? ( - <> - {coachSuggestion.title} -

{coachSuggestion.message}

- {coachSuggestion.next_utterance ? ( -
{coachSuggestion.next_utterance}
- ) : null} -
- - -
- - ) : coachQuotaRemaining <= 0 ? ( -

코칭 기회를 모두 사용했습니다. 좋은 발화로 내담자가 열리면 다시 1개가 충전됩니다.

- ) : ( -

코칭 모드에서는 방금 발화의 강점과 조정점을 근거와 함께 바로 짚습니다.

- )} -
+ /> + ))} +
) : null} +
+ +
+
+ {coachIsDegraded ? "대체 코칭" : "AI 코치"} + {coachStatusText} +
+
+ 코칭 기회 + + + {coachQuotaRemaining}/{coachQuotaMax} + +
+
+
+ {coachIsDegraded ? ( +

{coachDegradedNote}

+ ) : null} + {coachSyncWarning ? ( +

+ {coachSyncWarning} +

+ ) : null} + {coachCreditPulseText ? ( +
+ {coachCreditPulseText} +
+ ) : null} + {coachLoading ? ( +

방금 턴과 근거 자료를 대조하는 중입니다.

+ ) : coachError ? ( +

{coachError}

+ ) : coachSuggestion ? ( + <> + {coachSuggestion.title} +

{coachSuggestion.message}

+ {coachSuggestion.next_utterance ? ( +
{coachSuggestion.next_utterance}
+ ) : null} +
+ + +
+ + ) : coachQuotaRemaining <= 0 ? ( +

+ 코칭 기회를 모두 사용했습니다. 좋은 발화로 내담자가 열리면 다시 1개가 + 충전됩니다. +

+ ) : ( +

코칭 모드에서는 방금 발화의 강점과 조정점을 근거와 함께 바로 짚습니다.

+ )} +
+
+
{feedbackMode === "coached" ? ( <> diff --git a/apps/web/src/pages/session/session.css b/apps/web/src/pages/session/session.css index f1eba14..a38e4c7 100644 --- a/apps/web/src/pages/session/session.css +++ b/apps/web/src/pages/session/session.css @@ -48,36 +48,21 @@ padding-bottom: var(--sp-5); } .sx-page--active { - /* 2026-07-14 채도 완화: 회기 화면 국소 팔레트를 마른 세이지 톤으로 (한신대 피드백) */ - --paper-2: #1a2723; - --bg-surface: #141f1c; - --bg-surface-2: #1c2925; - --bg-stage: #0d1512; - --text-strong: #ecf2ef; - --text-body: #c7d2ce; - --text-muted: #8d9c96; - --text-on-accent: #0a1310; - --border-subtle: rgba(178, 199, 191, 0.14); - --border-strong: rgba(178, 199, 191, 0.22); - --border-focus: #83a89b; - --focus-ring: rgba(131, 168, 155, 0.3); - --accent: #83a89b; - --accent-deep: #a3bfb4; - --accent-tint: rgba(131, 168, 155, 0.14); - --accent-bright: #8db1a4; - --clay: #c98f76; - --clay-deep: #e0ac93; - --clay-tint: rgba(201, 143, 118, 0.14); height: 100vh; height: 100dvh; grid-template-rows: min-content minmax(0, 1fr) min-content; padding: 10px 14px 14px; gap: 10px; background: - radial-gradient(circle at 18% 8%, rgba(131, 168, 155, 0.09), transparent 28%), - radial-gradient(circle at 86% 12%, rgba(201, 143, 118, 0.07), transparent 24%), - linear-gradient(135deg, #0b1210 0%, #0e1513 48%, #101a16 100%); + radial-gradient(circle at 18% 8%, color-mix(in srgb, var(--accent) 11%, transparent), transparent 28%), + radial-gradient(circle at 86% 12%, color-mix(in srgb, var(--clay) 8%, transparent), transparent 24%), + var(--glass-canvas); color: var(--text-strong); +} +[data-theme="light"] .sx-page--active { + color-scheme: light; +} +[data-theme="dark"] .sx-page--active { color-scheme: dark; } .sx-page--active .sx-grid { @@ -98,21 +83,21 @@ align-items: center; justify-content: space-between; gap: 10px; - border: 1px solid rgba(169, 215, 204, 0.16); + border: 1px solid var(--glass-border); border-radius: var(--radius); - background: rgba(13, 24, 21, 0.92); - color: rgba(238, 244, 242, 0.88); - box-shadow: 0 12px 34px rgba(4, 12, 10, 0.26); + background: var(--glass-specular), var(--glass-surface-strong); + color: var(--text-body); + box-shadow: var(--glass-edge-shadow), var(--glass-shadow); backdrop-filter: blur(12px); min-width: 0; } .sx-sessionbar button { min-height: 32px; padding: 7px 10px; - border: 1px solid rgba(255, 255, 255, 0.12); + border: 1px solid var(--glass-inset-border); border-radius: var(--radius-sm); - background: rgba(255, 255, 255, 0.06); - color: #eef4f2; + background: var(--glass-specular-inset), var(--glass-surface-inset); + color: var(--text-strong); display: inline-flex; align-items: center; justify-content: center; @@ -122,7 +107,7 @@ cursor: pointer; } .sx-sessionbar button:hover { - background: rgba(255, 255, 255, 0.11); + background: var(--accent-tint); } .sx-sessionbar__back { flex: none; @@ -143,14 +128,14 @@ gap: 2px; } .sx-sessionbar__meta b { - color: #f4faf7; + color: var(--text-strong); font-size: 13px; line-height: 1.15; } /* D3 — "이름 · 단계 · 경과" 한 줄(· 2개)을 "이름 · 단계" + "경과" 두 칸으로 분리했다. 두 칸을 gap 으로 띄워 가운뎃점 없이도 구분되게 한다. */ .sx-sessionbar__meta small { - color: rgba(238, 244, 242, 0.55); + color: var(--text-muted); font: 600 11px/1.2 var(--font-sans); display: flex; align-items: baseline; @@ -190,9 +175,9 @@ gap: 7px; } .sx-sessionbar .sx-sessionbar__review { - border-color: rgba(151, 175, 166, 0.3); - background: rgba(151, 175, 166, 0.14); - color: #d0ddd7; + border-color: color-mix(in srgb, var(--accent) 36%, var(--glass-inset-border)); + background: var(--accent-tint); + color: var(--accent-deep); } /* ── 페이지 헤드라인 + 가로 회기 단계 미니 트랙 ── */ @@ -357,12 +342,9 @@ } .sx-page--active .sx-panel, .sx-page--active .sx-mobile-context { - background: - linear-gradient(180deg, rgba(23, 39, 35, 0.94), rgba(13, 25, 22, 0.96)); - border-color: var(--border-subtle); - box-shadow: - 0 18px 42px rgba(2, 8, 7, 0.3), - inset 0 1px 0 rgba(237, 247, 244, 0.045); + background: var(--glass-specular), var(--glass-surface); + border-color: var(--glass-border); + box-shadow: var(--glass-edge-shadow), var(--glass-shadow); color: var(--text-body); } @@ -628,21 +610,25 @@ } .sx-page--active .sx-stage { background: - radial-gradient(circle at 50% 47%, rgba(131, 168, 155, 0.22), transparent 36%), - radial-gradient(circle at 50% 82%, rgba(208, 139, 112, 0.12), transparent 24%), - linear-gradient(145deg, rgba(11, 22, 19, 0.98), rgba(20, 34, 30, 0.96) 56%, rgba(10, 19, 17, 0.98)); - border-color: rgba(169, 215, 204, 0.18); - box-shadow: - 0 22px 52px rgba(2, 8, 7, 0.42), - inset 0 1px 0 rgba(237, 247, 244, 0.05); + radial-gradient(circle at 50% 47%, color-mix(in srgb, var(--accent) 20%, transparent), transparent 36%), + radial-gradient(circle at 50% 82%, color-mix(in srgb, var(--clay) 12%, transparent), transparent 24%), + linear-gradient(145deg, color-mix(in srgb, var(--bg-stage) 92%, var(--bg-surface)), var(--bg-stage)); + border-color: var(--glass-border); + box-shadow: var(--glass-edge-shadow), var(--glass-shadow); grid-template-columns: minmax(184px, 256px) minmax(0, 1fr); padding: 20px 24px; } +[data-theme="light"] .sx-page--active .sx-stage { + background: + radial-gradient(circle at 50% 47%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 36%), + radial-gradient(circle at 50% 82%, color-mix(in srgb, var(--clay) 10%, transparent), transparent 24%), + linear-gradient(145deg, var(--paper), var(--paper-2)); +} .sx-page--active .sx-stage::before { content: ""; position: absolute; inset: 12px; - border: 1px solid rgba(169, 215, 204, 0.1); + border: 1px solid var(--glass-inset-border); border-radius: calc(var(--radius-lg) - 2px); pointer-events: none; } @@ -780,7 +766,7 @@ .sx-stage__client p { max-width: 42ch; margin: 0; - color: rgba(237, 247, 244, 0.9); + color: var(--text-strong); font-size: 16px; font-weight: 650; line-height: 1.58; @@ -855,9 +841,9 @@ width: min(100%, 600px); max-width: calc(100% - 48px); padding: 7px 10px; - border: 1px solid rgba(169, 215, 204, 0.15); + border: 1px solid var(--glass-inset-border); border-radius: var(--radius-sm); - background: rgba(7, 18, 15, 0.62); + background: var(--glass-specular-inset), var(--glass-surface-inset); color: var(--text-strong); text-align: center; white-space: nowrap; @@ -897,12 +883,9 @@ overflow: hidden; } .sx-page--active .sx-transcript { - background: - linear-gradient(180deg, rgba(19, 34, 30, 0.97), rgba(10, 22, 19, 0.98)); - border-color: rgba(169, 215, 204, 0.16); - box-shadow: - 0 18px 44px rgba(2, 8, 7, 0.34), - inset 0 1px 0 rgba(237, 247, 244, 0.04); + background: var(--glass-specular), var(--glass-surface); + border-color: var(--glass-border); + box-shadow: var(--glass-edge-shadow), var(--glass-shadow); color: var(--text-body); } .sx-transcript__head { @@ -1241,14 +1224,21 @@ /* ── RIGHT: 라이브 신호 ── */ .sx-signal { - padding: 14px; - overflow: hidden; + min-height: 0; + padding: 12px; + overflow-x: hidden; + overflow-y: auto; + scrollbar-width: thin; + display: flex; + flex-direction: column; } .sx-signal__head { - margin-bottom: 12px; + order: 0; + margin-bottom: 9px; } /* 앰비언트 도트 1개 + 한 단어 (6초 페이드) */ .sx-signal__one { + order: 2; display: flex; align-items: center; gap: 10px; @@ -1263,6 +1253,7 @@ } /* 신호가 아직 없을 때 — 빈 패널 대신 의도된 안내(가짜 데이터 아님) */ .sx-signal__rest { + order: 2; margin: 0; padding: 12px; border-radius: var(--radius); @@ -1308,63 +1299,39 @@ .sx-feedback-ambient .sx-signal__one-when { margin-left: 0; } -.sx-signal__wave { - height: 26px; - margin-top: 11px; +.sx-signal__status { + order: 3; display: grid; - grid-template-columns: repeat(8, minmax(0, 1fr)); - align-items: center; - gap: 4px; - color: var(--accent-bright); + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 7px; + margin-top: 8px; } -.sx-signal__wave i { - display: block; - height: 7px; - border-radius: 999px; - background: currentColor; - opacity: 0.55; -} -.sx-signal__wave i:nth-child(2), -.sx-signal__wave i:nth-child(7) { - height: 12px; -} -.sx-signal__wave i:nth-child(3), -.sx-signal__wave i:nth-child(6) { - height: 18px; - opacity: 0.8; -} -.sx-signal__wave i:nth-child(4), -.sx-signal__wave i:nth-child(5) { - height: 24px; - opacity: 0.95; -} -.sx-signal__rows { - display: grid; - gap: 0; - margin-top: 10px; -} -.sx-signal__rows span { +.sx-signal__status span { min-width: 0; display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: baseline; - gap: 10px; - padding: 8px 0; - border-top: 1px solid var(--paper-2); + gap: 2px; + padding: 7px 8px; + border: 1px solid var(--glass-inset-border); + border-radius: var(--radius-sm); + background: var(--glass-specular-inset), var(--glass-surface-inset); } -.sx-signal__rows b { +.sx-signal__status b { color: var(--text-body); - font-size: 12.5px; + font-size: 10.5px; font-weight: 650; } -.sx-signal__rows small { +.sx-signal__status small { + min-width: 0; color: var(--accent-deep); - font-size: 12px; + font-size: 11.5px; font-weight: 750; - text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } /* 최근 흐름 시퀀스 (클릭 가능) */ .sx-signal__seq { + order: 4; display: flex; align-items: center; gap: 7px; @@ -1397,6 +1364,7 @@ box-shadow: 0 0 0 3px var(--accent-tint); } .sx-signal__defer { + order: 5; font-size: 12px; color: var(--text-muted); line-height: 1.55; @@ -1414,10 +1382,11 @@ display: none; } .sx-coach-card { + order: 1; display: grid; grid-template-columns: 42px minmax(0, 1fr); gap: 10px; - margin-top: 12px; + margin: 0 0 9px; align-items: start; } .sx-coach-avatar { @@ -1425,24 +1394,24 @@ width: 42px; height: 42px; border-radius: 50%; - background: #e8edf0; + background: var(--bg-surface-2); border: 1px solid color-mix(in srgb, var(--text-muted) 24%, transparent); - box-shadow: 0 8px 20px rgba(28, 35, 42, 0.12); + box-shadow: var(--shadow-md); } .sx-coach-card.is-pos .sx-coach-avatar { - background: #e5f1eb; + background: color-mix(in srgb, var(--pos-tint) 82%, var(--bg-surface-2)); border-color: color-mix(in srgb, var(--pos-solid) 38%, transparent); } .sx-coach-card.is-warn .sx-coach-avatar { - background: #f4eadc; + background: color-mix(in srgb, var(--warn-tint) 82%, var(--bg-surface-2)); border-color: color-mix(in srgb, var(--warn-solid) 45%, transparent); } .sx-coach-avatar__lens { position: absolute; inset: 6px; border-radius: 50%; - border: 1px solid rgba(35, 45, 54, 0.12); - background: rgba(255, 255, 255, 0.58); + border: 1px solid var(--glass-inset-border); + background: var(--glass-specular-inset), var(--glass-surface-inset); } .sx-coach-avatar__face { position: absolute; @@ -1627,6 +1596,40 @@ .sx-coach-bubble button:hover { border-color: color-mix(in srgb, var(--accent-deep) 42%, transparent); } +/* 낮은 데스크톱에서도 코치의 판단과 행동 버튼을 먼저 보존한다. 발화 예시와 + 보조 흐름 설명은 세로 여유가 생길 때만 노출한다. */ +@media (min-width: 881px) and (max-height: 700px) { + .sx-feedback-coached .sx-coach-card { + gap: 8px; + margin-bottom: 6px; + } + .sx-feedback-coached .sx-coach-avatar { + width: 36px; + height: 36px; + } + .sx-feedback-coached .sx-coach-avatar__face { + left: 9px; + right: 9px; + top: 13px; + } + .sx-feedback-coached .sx-coach-bubble { + gap: 5px; + padding: 8px 10px; + } + .sx-feedback-coached .sx-coach-quota { + min-height: 22px; + padding: 4px 6px; + } + .sx-coach-bubble blockquote, + .sx-feedback-coached .sx-signal__seq, + .sx-feedback-coached .sx-signal__defer { + display: none; + } + .sx-feedback-coached .sx-signal__one, + .sx-feedback-coached .sx-signal__rest { + padding-block: 8px; + } +} .sx-coach-card.is-loading .sx-coach-avatar__lens { animation: sxCoachThinking 1100ms var(--ease-in-out) infinite; } @@ -2013,87 +2016,43 @@ .sx-page--active .sx-controlbar { width: min(100%, 1460px); margin: 0 auto; - background: rgba(11, 22, 19, 0.96); - border-color: rgba(169, 215, 204, 0.16); - box-shadow: - 0 16px 40px rgba(2, 8, 7, 0.34), - inset 0 1px 0 rgba(237, 247, 244, 0.04); - color: rgba(238, 244, 242, 0.86); -} -.sx-page--active .sx-mic-block__l, -.sx-page--active .sx-seg-block__label { - color: #eef4f2; -} -.sx-page--active .sx-mic-block__h { - color: rgba(238, 244, 242, 0.58); -} -.sx-page--active .sx-segmented { - background: rgba(237, 247, 244, 0.07); - border: 1px solid rgba(169, 215, 204, 0.1); -} -.sx-page--active .sx-segmented button { - color: rgba(238, 244, 242, 0.68); -} -.sx-page--active .sx-segmented button:hover { - color: var(--text-strong); - background: rgba(237, 247, 244, 0.08); -} -.sx-page--active .sx-segmented button.is-on { - background: rgba(121, 200, 183, 0.24); - color: var(--text-strong); -} -.sx-page--active .sx-cb-sep { - background: rgba(255, 255, 255, 0.12); -} -.sx-page--active .sx-pause { - background: rgba(237, 247, 244, 0.07); - border-color: rgba(169, 215, 204, 0.14); - color: #eef4f2; -} -.sx-page--active .sx-pause:hover { - background: rgba(237, 247, 244, 0.11); -} -.sx-page--active .sx-review-button { - background: rgba(151, 175, 166, 0.14); - border-color: rgba(151, 175, 166, 0.3); - color: #d0ddd7; -} -.sx-page--active .sx-review-button:hover { - background: rgba(151, 175, 166, 0.2); - color: #edf3f0; + background: var(--glass-specular), var(--glass-surface-strong); + border-color: var(--glass-border); + box-shadow: var(--glass-edge-shadow), var(--glass-shadow); + color: var(--text-body); } .sx-page--active .sx-ctx__row, .sx-page--active .sx-compose, .sx-page--active .sx-meter + .sx-meter, .sx-page--active .sx-safety { - border-color: rgba(203, 227, 220, 0.1); + border-color: var(--glass-inset-border); } .sx-page--active .sx-chip, .sx-page--active .sx-signal__one, .sx-page--active .sx-signal__rest, .sx-page--active .sx-mobile-context__row span, .sx-page--active .sx-mobile-context__brief span { - background: rgba(237, 247, 244, 0.055); - border-color: rgba(169, 215, 204, 0.09); - color: rgba(234, 240, 241, 0.76); + background: var(--glass-specular-inset), var(--glass-surface-inset); + border-color: var(--glass-inset-border); + color: var(--text-body); } .sx-page--active .sx-chip.is-clay, .sx-page--active .sx-ctx__pf { - background: rgba(204, 143, 119, 0.16); - color: #e5ad95; + background: var(--clay-tint); + color: var(--clay-deep); } .sx-page--active .sx-ctx__nm, .sx-page--active .sx-vstep.is-cur .sx-vstep__label, .sx-page--active .sx-mobile-context__row b, .sx-page--active .sx-mobile-context__brief b { - color: #eaf0f1; + color: var(--text-strong); } .sx-page--active .sx-ctx__rv, .sx-page--active .sx-vstep.is-done .sx-vstep__label, .sx-page--active .sx-meter__label, .sx-page--active .sx-signal__txt { - color: rgba(234, 240, 241, 0.74); + color: var(--text-body); } .sx-page--active .sx-ctx__mt, .sx-page--active .sx-ctx__rl, @@ -2102,47 +2061,7 @@ .sx-page--active .sx-mobile-context__row small, .sx-page--active .sx-mobile-context__brief small, .sx-page--active .sx-safety__desc { - color: rgba(234, 240, 241, 0.52); -} -.sx-page--active .sx-utt.is-client .sx-utt__line { - background: rgba(204, 143, 119, 0.12); - color: #edf5f2; -} -.sx-page--active .sx-utt.is-thinking .sx-utt__line { - color: rgba(234, 240, 241, 0.62); - border-color: rgba(229, 173, 149, 0.22); -} -.sx-page--active .sx-utt.is-learner .sx-utt__line { - background: rgba(125, 162, 148, 0.13); - color: #edf5f2; -} -.sx-page--active .sx-utt.is-client .sx-utt__spk { - color: #e5ad95; -} -.sx-page--active .sx-utt.is-learner .sx-utt__spk { - color: #a4bab2; -} -.sx-page--active .sx-compose textarea { - background: rgba(7, 18, 15, 0.58); - color: #eef6f3; - border-color: rgba(169, 215, 204, 0.22); -} -.sx-page--active .sx-compose textarea::placeholder { - color: rgba(234, 240, 241, 0.36); -} -.sx-page--active .sx-compose .vg-btn--primary:disabled { - background: rgba(237, 247, 244, 0.055); - color: rgba(234, 240, 241, 0.38); - border-color: rgba(169, 215, 204, 0.14); -} -.sx-page--active .sx-compose .vg-btn--primary:not(:disabled) { - background: var(--accent); - color: var(--text-on-accent); - border-color: transparent; - box-shadow: 0 0 0 1px rgba(237, 247, 244, 0.08), 0 10px 22px rgba(3, 12, 10, 0.24); -} -.sx-page--active .sx-compose .vg-btn--primary:hover:not(:disabled) { - background: #93b3a7; + color: var(--text-muted); } /* 마이크 (주 컨트롤, 음성 호흡 펄스) — 원형 예외 허용 */ .sx-mic-block { @@ -2231,16 +2150,6 @@ border-color: var(--border-focus); color: var(--text-strong); } -.sx-page--active .sx-voice-skip { - background: rgba(255, 255, 255, 0.08); - border-color: rgba(255, 255, 255, 0.14); - color: rgba(238, 244, 242, 0.86); -} -.sx-page--active .sx-voice-skip:hover { - background: rgba(255, 255, 255, 0.12); - color: #ffffff; -} - .sx-cb-sep { width: 1px; height: 34px; @@ -2416,17 +2325,6 @@ cursor: not-allowed; opacity: 0.6; } -.sx-page--active .sx-end-button { - background: rgba(216, 100, 89, 0.13); - border-color: rgba(232, 144, 134, 0.32); - color: #f1b2aa; -} -.sx-page--active .sx-end-button:hover:not(:disabled) { - background: rgba(216, 100, 89, 0.19); - border-color: rgba(232, 144, 134, 0.5); - color: #ffd1cc; -} - .sx-coach-modal { position: fixed; inset: 0; @@ -3105,6 +3003,53 @@ .sx-page--active .sx-col-right { display: none; } + /* 코칭 모드는 우측 전체 진단 레일을 숨기더라도 코치 카드 자체는 보존한다. + 881~1180px 구간에서 코칭 탭을 눌러도 아무것도 나타나지 않던 사각지대를 닫는다. */ + .sx-page--active.sx-feedback-coached .sx-grid { + grid-template-rows: auto minmax(0, 1fr) auto; + } + .sx-page--active.sx-feedback-coached .sx-col-right { + display: flex; + grid-column: 1 / -1; + grid-row: 3; + min-height: 0; + overflow: visible; + } + .sx-page--active.sx-feedback-coached .sx-col-right .sx-meters, + .sx-page--active.sx-feedback-coached .sx-col-right .sx-safety--ok, + .sx-page--active.sx-feedback-coached .sx-signal__head, + .sx-page--active.sx-feedback-coached .sx-signal__one, + .sx-page--active.sx-feedback-coached .sx-signal__rest, + .sx-page--active.sx-feedback-coached .sx-signal__status, + .sx-page--active.sx-feedback-coached .sx-signal__seq, + .sx-page--active.sx-feedback-coached .sx-signal__defer { + display: none; + } + .sx-page--active.sx-feedback-coached .sx-col-right .sx-signal { + padding: 8px; + overflow: visible; + } + .sx-page--active.sx-feedback-coached .sx-coach-card { + grid-template-columns: 34px minmax(0, 1fr); + gap: 8px; + margin: 0; + } + .sx-page--active.sx-feedback-coached .sx-coach-avatar { + width: 34px; + height: 34px; + } + .sx-page--active.sx-feedback-coached .sx-coach-avatar__face { + left: 9px; + right: 9px; + top: 12px; + } + .sx-page--active.sx-feedback-coached .sx-coach-bubble { + padding: 8px 10px; + gap: 5px; + } + .sx-page--active.sx-feedback-coached .sx-coach-bubble blockquote { + display: none; + } /* 위기 신호가 감지되면 어떤 화면 모드에서도 안전 패널(109 리소스 포함)을 노출한다. 압축 레이아웃이 안전 안내를 가리던 결함 수정 (2026-07-15). */ .sx-page--active.sx-has-safety .sx-col-right { @@ -3385,8 +3330,7 @@ .sx-page--active.sx-feedback-coached .sx-signal__head, .sx-page--active.sx-feedback-coached .sx-signal__one, .sx-page--active.sx-feedback-coached .sx-signal__rest, - .sx-page--active.sx-feedback-coached .sx-signal__wave, - .sx-page--active.sx-feedback-coached .sx-signal__rows, + .sx-page--active.sx-feedback-coached .sx-signal__status, .sx-page--active.sx-feedback-coached .sx-signal__seq, .sx-page--active.sx-feedback-coached .sx-signal__defer { display: none; @@ -4377,3 +4321,910 @@ .sx-safety .sx-panel-toggle .sx-safety__badge { margin-left: auto; } + +.vg-main:has(.sx-page--active) { + overflow: hidden; + scrollbar-gutter: auto; +} + +/* ── 2026-07-31 botanical session workspace ────────────────────────── + 사용자 제공 1536×1024 시안의 정보 위계를 데스크톱 세션에 고정한다. + 기능 DOM은 유지하고 좌·우 레일을 각각 하나의 연속 표면으로 보이게 합친다. */ +.sx-coach-summary { + min-width: 0; + display: grid; + gap: 8px; + align-self: center; +} +.sx-coach-card > .sx-coach-bubble { + grid-column: 1 / -1; +} + +.sx-page--active { + position: relative; + isolation: isolate; +} +.sx-page--active::before, +.sx-page--active::after { + content: ""; + position: absolute; + z-index: 0; + top: 0; + bottom: 0; + width: clamp(150px, 13vw, 220px); + pointer-events: none; + opacity: 0.48; + background-repeat: no-repeat; + filter: saturate(0.72); +} +.sx-page--active::before { + left: 0; + background-image: + url("/session-botanical/leaf-5.webp"), + url("/session-botanical/leaf-2.webp"), + url("/session-botanical/leaf-3.webp"); + background-size: 330px auto, 245px auto, 260px auto; + background-position: -142px 5px, -128px 48%, -132px 94%; +} +.sx-page--active::after { + right: 0; + background-image: + url("/session-botanical/leaf-1.webp"), + url("/session-botanical/leaf-4.webp"); + background-size: 330px auto, 350px auto; + background-position: -132px 33%, -148px 88%; + transform: scaleX(-1); +} +.sx-page--active > * { + position: relative; + z-index: 1; +} +[data-theme="dark"] .sx-page--active::before, +[data-theme="dark"] .sx-page--active::after { + opacity: 0.19; + filter: saturate(0.5) brightness(0.85); +} + +@media (min-width: 1280px) { + .sx-page--active { + grid-template-rows: 80px minmax(0, 1fr) 116px; + gap: 12px; + padding: 14px 34px 30px; + background: + radial-gradient(circle at 51% 14%, rgba(255, 255, 255, 0.82), transparent 31%), + radial-gradient(circle at 8% 94%, color-mix(in srgb, var(--accent) 8%, transparent), transparent 25%), + #f8f7f3; + overflow: hidden; + } + [data-theme="dark"] .sx-page--active { + background: + radial-gradient(circle at 51% 14%, rgba(255, 255, 255, 0.035), transparent 31%), + radial-gradient(circle at 8% 94%, color-mix(in srgb, var(--accent) 9%, transparent), transparent 25%), + var(--glass-canvas); + } + + .sx-page--active .sx-sessionbar { + width: min(100%, 1408px); + height: 80px; + margin: 0 auto; + padding: 0 25px; + border-color: rgba(159, 142, 111, 0.2); + border-radius: 12px; + background: rgba(255, 255, 255, 0.62); + box-shadow: 0 12px 34px rgba(72, 62, 44, 0.055); + backdrop-filter: blur(18px); + } + [data-theme="dark"] .sx-page--active .sx-sessionbar { + border-color: var(--glass-border); + background: var(--glass-specular), var(--glass-surface-strong); + box-shadow: var(--glass-edge-shadow), var(--glass-shadow); + } + .sx-page--active .sx-sessionbar button { + min-height: 42px; + padding: 0 16px; + border-color: rgba(159, 142, 111, 0.23); + border-radius: 14px; + background: rgba(255, 255, 255, 0.42); + color: #202521; + font-size: 15px; + } + [data-theme="dark"] .sx-page--active .sx-sessionbar button { + border-color: var(--glass-inset-border); + background: var(--glass-specular-inset), var(--glass-surface-inset); + color: var(--text-strong); + } + .sx-page--active .sx-sessionbar__meta { + gap: 3px; + } + .sx-page--active .sx-sessionbar__meta b { + color: #161b18; + font-size: 20px; + letter-spacing: -0.035em; + } + .sx-page--active .sx-sessionbar__meta small { + justify-content: center; + color: #6b706b; + font-size: 14px; + } + [data-theme="dark"] .sx-page--active .sx-sessionbar__meta b { + color: var(--text-strong); + } + [data-theme="dark"] .sx-page--active .sx-sessionbar__meta small { + color: var(--text-muted); + } + .sx-page--active .sx-sessionbar__dot { + width: 11px; + height: 11px; + box-shadow: 0 0 0 4px rgba(48, 148, 93, 0.13); + } + + .sx-page--active .sx-grid { + width: min(100%, 1408px); + margin: 0 auto; + grid-template-columns: 326px minmax(500px, 1fr) 357px; + gap: 12px; + } + .sx-page--active .sx-col { + gap: 12px; + } + .sx-page--active .sx-col-left, + .sx-page--active .sx-col-right { + gap: 0; + padding: 20px 22px; + border: 1px solid rgba(159, 142, 111, 0.2); + border-radius: 12px; + background: rgba(255, 255, 255, 0.58); + box-shadow: 0 12px 32px rgba(72, 62, 44, 0.045); + backdrop-filter: blur(16px); + overflow: auto; + scrollbar-width: thin; + } + [data-theme="dark"] .sx-page--active .sx-col-left, + [data-theme="dark"] .sx-page--active .sx-col-right { + border-color: var(--glass-border); + background: var(--glass-specular), var(--glass-surface); + box-shadow: var(--glass-edge-shadow), var(--glass-shadow); + } + .sx-page--active .sx-col-left > .sx-panel, + .sx-page--active .sx-col-right > .sx-panel { + width: 100%; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + backdrop-filter: none; + } + + .sx-page--active .sx-ctx { + order: 1; + flex: 0 0 auto; + padding: 0 0 18px; + overflow: visible; + border-bottom: 1px solid rgba(159, 142, 111, 0.2); + } + .sx-page--active .sx-ctx__head { + min-height: 24px; + margin: 0; + } + .sx-page--active .sx-ctx .sx-panel-toggle__hint { + display: none; + } + .sx-page--active .sx-track { + order: 2; + flex: 1 0 auto; + display: flex; + flex-direction: column; + padding: 18px 0 20px; + overflow: visible; + border-bottom: 1px solid rgba(159, 142, 111, 0.2); + } + .sx-page--active .sx-track > .sx-vstep { + flex: 1 1 0; + min-height: 62px; + } + .sx-page--active .sx-track__head { + margin-bottom: 16px; + } + .sx-page--active .sx-vstep { + grid-template-columns: 18px minmax(0, 1fr); + column-gap: 12px; + } + .sx-page--active .sx-vstep__node { + width: 17px; + height: 17px; + background: #fbfaf7; + border-color: #d9d2c4; + } + .sx-page--active .sx-vstep.is-cur .sx-vstep__node { + border-color: #2f806b; + box-shadow: 0 0 0 4px rgba(47, 128, 107, 0.12); + } + .sx-page--active .sx-vstep.is-cur .sx-vstep__node::after { + inset: 3px; + background: #2f806b; + } + .sx-page--active .sx-vstep__line { + width: 1px; + min-height: 31px; + background: #d8d0c2; + } + .sx-page--active .sx-vstep__body { + padding-bottom: 18px; + } + .sx-page--active .sx-vstep__label { + color: #1e2420; + font-size: 15px; + font-weight: 720; + } + .sx-page--active .sx-vstep__t { + color: #737871; + font-size: 12px; + } + .sx-page--active .sx-vstep.is-cur .sx-vstep__t { + color: #2b8069; + } + .sx-page--active .sx-vstep__desc { + margin-top: 5px; + color: #676b66; + font-size: 13px; + line-height: 1.45; + } + .sx-page--active .sx-vstep__goal { + margin-right: auto; + margin-left: 8px; + border: 0; + background: #e7f0e9; + color: #2a7563; + font-size: 11px; + } + [data-theme="dark"] .sx-page--active .sx-vstep__node { + background: var(--bg-surface); + border-color: var(--border-strong); + } + [data-theme="dark"] .sx-page--active .sx-vstep__label, + [data-theme="dark"] .sx-page--active .sx-vstep.is-cur .sx-vstep__label { + color: var(--text-strong); + } + [data-theme="dark"] .sx-page--active .sx-vstep__desc, + [data-theme="dark"] .sx-page--active .sx-vstep__t { + color: var(--text-muted); + } + + .sx-page--active .sx-session-progress { + order: 3; + flex: 0 0 270px; + padding: 17px 0 0; + overflow: visible; + } + .sx-page--active .sx-session-progress__head { + margin-bottom: 10px; + } + .sx-page--active .sx-session-progress__grid { + gap: 0; + } + .sx-page--active .sx-session-progress__grid span { + grid-template-columns: minmax(0, 1fr) auto; + padding: 11px 0; + border-color: rgba(159, 142, 111, 0.16); + } + .sx-page--active .sx-session-progress__grid small { + grid-column: 1; + grid-row: 1; + display: inline-flex; + align-items: center; + gap: 12px; + color: #686d68; + font-size: 13px; + text-align: left; + } + .sx-page--active .sx-session-progress__grid small svg { + color: #287967; + } + .sx-page--active .sx-session-progress__grid b { + grid-column: 2; + grid-row: 1; + color: #161b18; + font-size: 15px; + } + [data-theme="dark"] .sx-page--active .sx-session-progress__grid small { + color: var(--text-muted); + } + [data-theme="dark"] .sx-page--active .sx-session-progress__grid b { + color: var(--text-strong); + } + + .sx-page--active .sx-col-center { + grid-template-rows: 360px minmax(0, 1fr); + gap: 12px; + } + .sx-page--active .sx-stage, + .sx-page--active .sx-transcript { + border-color: rgba(159, 142, 111, 0.2); + border-radius: 12px; + background: rgba(255, 255, 255, 0.6); + box-shadow: 0 12px 32px rgba(72, 62, 44, 0.045); + backdrop-filter: blur(16px); + } + [data-theme="dark"] .sx-page--active .sx-stage, + [data-theme="dark"] .sx-page--active .sx-transcript { + border-color: var(--glass-border); + background: var(--glass-specular), var(--glass-surface); + box-shadow: var(--glass-edge-shadow), var(--glass-shadow); + } + .sx-page--active .sx-stage { + grid-template-columns: minmax(245px, 0.92fr) minmax(280px, 1.08fr); + grid-template-rows: minmax(0, 1fr) 62px; + column-gap: 28px; + padding: 20px 28px 14px; + } + .sx-page--active .sx-stage::before { + inset: 12px; + border-color: rgba(159, 142, 111, 0.13); + border-radius: 10px; + } + .sx-page--active .sx-stage__top { + display: none; + } + .sx-page--active .sx-orb-wrap { + grid-column: 1; + grid-row: 1; + place-self: end center; + width: 238px; + height: 238px; + z-index: 1; + } + .sx-page--active .sx-orb-wrap::before { + content: ""; + position: absolute; + z-index: -1; + left: -86px; + bottom: -20px; + width: 190px; + height: 248px; + background: url("/session-botanical/leaf-1.webp") center / contain no-repeat; + opacity: 0.72; + transform: rotate(-11deg); + pointer-events: none; + } + .sx-page--active .sx-orb { + inset: -3px; + border-radius: 50% 50% 46% 54% / 46% 48% 52% 54%; + background: radial-gradient(circle at 50% 42%, #f4f6ee 0%, #e6ede2 68%, #dce6db 100%); + box-shadow: inset 0 0 36px rgba(97, 122, 99, 0.08); + } + [data-theme="dark"] .sx-page--active .sx-orb { + background: radial-gradient(circle at 50% 42%, rgba(139, 166, 150, 0.24), rgba(34, 50, 44, 0.54)); + } + .sx-page--active .sx-stage .vg-avatar, + .sx-page--active .sx-stage .vg-avatar__svg { + width: 232px !important; + height: 232px !important; + } + .sx-page--active .sx-stage .vg-avatar__stage { + height: 232px !important; + } + .sx-page--active .sx-stage__client { + grid-column: 2; + grid-row: 1; + align-self: center; + gap: 13px; + } + .sx-page--active .sx-stage__client-kicker { + color: #131915; + font-size: 34px; + font-weight: 820; + letter-spacing: -0.055em; + } + .sx-page--active .sx-stage__client p { + max-width: 33ch; + color: #262b27; + font-size: 16px; + font-weight: 560; + line-height: 1.55; + } + [data-theme="dark"] .sx-page--active .sx-stage__client-kicker, + [data-theme="dark"] .sx-page--active .sx-stage__client p { + color: var(--text-strong); + } + .sx-page--active .sx-stage__now { + grid-column: 1 / -1; + grid-row: 2; + place-self: stretch; + width: auto; + max-width: none; + min-height: 50px; + padding: 0 18px; + border-color: rgba(159, 142, 111, 0.18); + border-radius: 11px; + background: rgba(255, 255, 255, 0.45); + color: #222824; + font-size: 15px; + } + .sx-page--active .sx-stage__now > svg { + flex: none; + color: #987b4c; + } + .sx-page--active .sx-stage__now small { + padding: 4px 10px; + border-radius: 999px; + background: #e5efe7; + color: #29705f; + } + [data-theme="dark"] .sx-page--active .sx-stage__now { + border-color: var(--glass-inset-border); + background: var(--glass-specular-inset), var(--glass-surface-inset); + color: var(--text-strong); + } + + .sx-page--active .sx-transcript { + padding: 19px 18px 13px; + } + .sx-page--active .sx-transcript__head { + margin-bottom: 12px; + } + .sx-page--active .sx-transcript__scroll { + padding: 0; + } + .sx-page--active .sx-transcript__empty { + padding: 13px 2px; + } + .sx-page--active .sx-transcript__empty b { + color: #202622; + font-size: 16px; + } + .sx-page--active .sx-transcript__empty span { + margin-top: 8px; + color: #7a7d79; + font-size: 13px; + } + [data-theme="dark"] .sx-page--active .sx-transcript__empty b { + color: var(--text-strong); + } + [data-theme="dark"] .sx-page--active .sx-transcript__empty span { + color: var(--text-muted); + } + .sx-page--active .sx-compose { + gap: 14px; + margin-top: 12px; + padding-top: 12px; + border-color: rgba(159, 142, 111, 0.16); + } + .sx-page--active .sx-compose textarea { + min-height: 56px; + padding: 16px 18px; + border-color: rgba(159, 142, 111, 0.24); + border-radius: 12px; + background: rgba(255, 255, 255, 0.54); + color: #262b27; + font-size: 14px; + } + .sx-page--active .sx-compose .vg-btn { + min-width: 134px; + min-height: 56px; + border-radius: 12px; + background: linear-gradient(145deg, #3d947d, #267762); + box-shadow: 0 8px 18px rgba(43, 119, 97, 0.2); + font-size: 15px; + } + .sx-page--active .sx-compose .vg-btn:disabled { + background: linear-gradient(145deg, #4b9d88, #2d806a); + color: #fff; + opacity: 0.82; + } + + .sx-page--active .sx-col-right { + padding-inline: 20px; + } + .sx-page--active .sx-signal { + order: 1; + flex: 0 0 auto; + padding: 0 0 18px; + overflow: visible; + border-bottom: 1px solid rgba(159, 142, 111, 0.2); + } + .sx-page--active .sx-signal__head { + margin-bottom: 16px; + } + .sx-page--active .sx-coach-card { + grid-template-columns: 58px minmax(0, 1fr); + gap: 11px 13px; + margin: 0; + } + .sx-page--active .sx-coach-avatar { + width: 58px; + height: 58px; + background: #f6f5ef; + border-color: #d7dfd5; + box-shadow: none; + } + .sx-page--active .sx-coach-avatar__lens { + inset: 8px; + background: rgba(255, 255, 255, 0.64); + } + .sx-page--active .sx-coach-avatar__face { + left: 15px; + right: 15px; + top: 21px; + } + .sx-page--active .sx-coach-summary { + gap: 10px; + } + .sx-page--active .sx-coach-bubble__meta { + padding-bottom: 8px; + border-bottom: 1px solid rgba(159, 142, 111, 0.16); + } + .sx-page--active .sx-coach-bubble__meta b { + color: #171d19; + font-size: 17px; + } + .sx-page--active .sx-coach-bubble__meta span { + color: #666b66; + font-size: 12px; + } + .sx-page--active .sx-coach-quota { + min-height: 25px; + padding: 0; + border: 0; + background: transparent; + } + .sx-page--active .sx-coach-quota > span:first-child { + color: #666b66; + font-size: 12px; + } + .sx-page--active .sx-coach-quota b { + color: #1d6f5d; + font-size: 14px; + } + .sx-page--active .sx-coach-quota__dots i { + width: 10px; + height: 10px; + } + .sx-page--active .sx-coach-bubble { + gap: 7px; + padding: 13px 14px; + border: 1px solid #d8e1d9; + border-radius: 9px; + background: rgba(239, 245, 239, 0.72); + color: #2a302b; + box-shadow: none; + } + .sx-page--active .sx-coach-bubble::after { + content: "코칭 포인트"; + order: -1; + color: #296f5e; + font-size: 12px; + font-weight: 760; + } + .sx-page--active .sx-coach-bubble::before { + display: none; + } + .sx-page--active .sx-coach-bubble p { + color: #444a45; + font-size: 13px; + line-height: 1.65; + } + [data-theme="dark"] .sx-page--active .sx-coach-bubble__meta b, + [data-theme="dark"] .sx-page--active .sx-coach-bubble p { + color: var(--text-strong); + } + [data-theme="dark"] .sx-page--active .sx-coach-bubble { + border-color: var(--glass-inset-border); + background: var(--glass-specular-inset), var(--glass-surface-inset); + } + .sx-page--active .sx-signal__status { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + margin-top: 16px; + } + .sx-page--active .sx-signal__status span { + min-height: 61px; + grid-template-columns: auto minmax(0, 1fr); + grid-template-rows: auto auto; + place-content: center; + column-gap: 9px; + row-gap: 2px; + padding: 10px 12px; + border-color: rgba(159, 142, 111, 0.22); + border-radius: 11px; + background: rgba(255, 255, 255, 0.42); + text-align: left; + } + .sx-page--active .sx-signal__status span > svg { + grid-column: 1; + grid-row: 1 / span 2; + align-self: center; + color: #1f7c65; + } + .sx-page--active .sx-signal__status b { + grid-column: 2; + grid-row: 1; + color: #565b56; + font-size: 13px; + } + .sx-page--active .sx-signal__status small { + grid-column: 2; + grid-row: 2; + color: #1f7c65; + font-size: 13px; + } + .sx-page--active .sx-signal__one, + .sx-page--active .sx-signal__seq { + margin-top: 11px; + } + .sx-page--active.sx-feedback-ambient .sx-signal__defer, + .sx-page--active.sx-feedback-coached .sx-signal__defer { + display: none; + } + .sx-page--active .sx-meters { + order: 2; + flex: 1 0 auto; + padding: 17px 0 14px; + overflow: visible; + border-bottom: 1px solid rgba(159, 142, 111, 0.2); + } + .sx-page--active .sx-meter { + padding: 10px 0; + } + .sx-page--active .sx-meter__label { + color: #595e59; + font-size: 13px; + } + .sx-page--active .sx-meter__val { + font-size: 13px; + } + .sx-page--active .sx-meters__note { + color: #828681; + font-size: 10.5px; + line-height: 1.55; + } + .sx-page--active .sx-safety { + order: 3; + flex: 0 0 auto; + padding: 14px 0 0; + } + .sx-page--active .sx-safety__head { + min-height: 28px; + } + .sx-page--active .sx-safety__badge { + color: #4c524d; + } + [data-theme="dark"] .sx-page--active .sx-meter__label, + [data-theme="dark"] .sx-page--active .sx-safety__badge { + color: var(--text-body); + } + + .sx-page--active .sx-controlbar { + width: min(100%, 1468px); + height: 116px; + margin: 0 auto; + padding: 18px 26px; + border-color: rgba(159, 142, 111, 0.2); + border-radius: 12px; + background: rgba(255, 255, 255, 0.64); + box-shadow: 0 12px 34px rgba(72, 62, 44, 0.055); + backdrop-filter: blur(18px); + } + [data-theme="dark"] .sx-page--active .sx-controlbar { + border-color: var(--glass-border); + background: var(--glass-specular), var(--glass-surface-strong); + box-shadow: var(--glass-edge-shadow), var(--glass-shadow); + } + .sx-page--active .sx-mic { + width: 70px; + height: 70px; + border-color: rgba(159, 142, 111, 0.24); + background: rgba(255, 255, 255, 0.5); + } + .sx-page--active .sx-mic-block__l { + color: #181e1a; + font-size: 16px; + } + .sx-page--active .sx-mic-block__h { + color: #767a76; + font-size: 12px; + } + .sx-page--active .sx-cb-sep { + height: 72px; + background: rgba(159, 142, 111, 0.2); + } + .sx-page--active .sx-segmented { + padding: 4px; + border-color: rgba(159, 142, 111, 0.18); + border-radius: 11px; + background: rgba(245, 244, 239, 0.9); + } + .sx-page--active .sx-segmented button { + min-height: 42px; + padding-inline: 16px; + color: #5f645f; + font-size: 13px; + } + .sx-page--active .sx-segmented button.is-on { + border-radius: 8px; + background: rgba(255, 255, 255, 0.96); + color: #237460; + box-shadow: 0 3px 11px rgba(72, 62, 44, 0.1); + } + .sx-page--active .sx-pause, + .sx-page--active .sx-end-button { + min-height: 60px; + border-radius: 12px; + font-size: 14px; + } + .sx-page--active .sx-pause { + min-width: 138px; + border-color: rgba(159, 142, 111, 0.22); + background: rgba(255, 255, 255, 0.48); + color: #232824; + } + .sx-page--active .sx-end-button { + min-width: 150px; + border-color: #eb8c84; + background: rgba(255, 255, 255, 0.42); + color: #d8342b; + } + [data-theme="dark"] .sx-page--active .sx-mic-block__l, + [data-theme="dark"] .sx-page--active .sx-pause { + color: var(--text-strong); + } +} + +/* 1366×640/768 검증 lane: 같은 위계를 유지하되 높이만 광학적으로 압축한다. */ +@media (min-width: 1280px) and (max-height: 820px) { + .sx-page--active { + grid-template-rows: 52px minmax(0, 1fr) 82px; + gap: 8px; + padding: 8px 18px 10px; + } + .sx-page--active .sx-sessionbar { + width: min(100%, 1320px); + height: 52px; + padding-inline: 14px; + } + .sx-page--active .sx-sessionbar button { + min-height: 36px; + padding-inline: 12px; + } + .sx-page--active .sx-sessionbar__meta b { + font-size: 16px; + } + .sx-page--active .sx-grid { + width: min(100%, 1320px); + grid-template-columns: 276px minmax(470px, 1fr) 310px; + gap: 8px; + } + .sx-page--active .sx-col-left, + .sx-page--active .sx-col-right { + padding: 11px 14px; + } + .sx-page--active .sx-ctx { + padding-bottom: 9px; + } + .sx-page--active .sx-track { + padding: 9px 0; + } + .sx-page--active .sx-track__head, + .sx-page--active .sx-session-progress__head, + .sx-page--active .sx-signal__head { + margin-bottom: 7px; + } + .sx-page--active .sx-vstep__body { + padding-bottom: 7px; + } + .sx-page--active .sx-track > .sx-vstep { + min-height: 34px; + } + .sx-page--active .sx-vstep__desc, + .sx-page--active .sx-track__note { + display: none; + } + .sx-page--active .sx-session-progress { + flex-basis: 142px; + padding-top: 8px; + } + .sx-page--active .sx-session-progress__grid span { + padding: 6px 0; + } + .sx-page--active .sx-col-center { + grid-template-rows: minmax(182px, 0.46fr) minmax(0, 1fr); + gap: 8px; + } + .sx-page--active .sx-stage { + grid-template-columns: minmax(176px, 0.8fr) minmax(250px, 1.2fr); + grid-template-rows: minmax(0, 1fr) 43px; + padding: 10px 18px 8px; + } + .sx-page--active .sx-orb-wrap { + width: 158px; + height: 158px; + } + .sx-page--active .sx-orb-wrap::before { + left: -56px; + bottom: -15px; + width: 122px; + height: 165px; + } + .sx-page--active .sx-stage .vg-avatar, + .sx-page--active .sx-stage .vg-avatar__svg { + width: 154px !important; + height: 154px !important; + } + .sx-page--active .sx-stage .vg-avatar__stage { + height: 154px !important; + } + .sx-page--active .sx-stage__client-kicker { + font-size: 25px; + } + .sx-page--active .sx-stage__client p { + font-size: 13px; + } + .sx-page--active .sx-stage__now { + min-height: 38px; + padding-inline: 10px; + font-size: 12px; + } + .sx-page--active .sx-transcript { + padding: 10px 12px 8px; + } + .sx-page--active .sx-compose { + gap: 8px; + margin-top: 7px; + padding-top: 7px; + } + .sx-page--active .sx-compose textarea, + .sx-page--active .sx-compose .vg-btn { + min-height: 40px; + } + .sx-page--active .sx-signal { + padding-bottom: 8px; + } + .sx-page--active .sx-coach-card { + grid-template-columns: 42px minmax(0, 1fr); + gap: 6px 9px; + } + .sx-page--active .sx-coach-avatar { + width: 42px; + height: 42px; + } + .sx-page--active .sx-coach-bubble { + padding: 8px 9px; + } + .sx-page--active .sx-coach-bubble blockquote, + .sx-page--active .sx-signal__seq, + .sx-page--active .sx-meters__note { + display: none; + } + .sx-page--active .sx-signal__status { + gap: 7px; + margin-top: 7px; + } + .sx-page--active .sx-signal__status span { + min-height: 42px; + padding: 5px 7px; + } + .sx-page--active .sx-meters { + padding: 8px 0 6px; + } + .sx-page--active .sx-meter { + padding: 5px 0; + } + .sx-page--active .sx-safety { + padding-top: 7px; + } + .sx-page--active .sx-controlbar { + width: min(100%, 1330px); + height: 82px; + padding: 10px 16px; + } + .sx-page--active .sx-mic { + width: 52px; + height: 52px; + } + .sx-page--active .sx-cb-sep { + height: 52px; + } + .sx-page--active .sx-pause, + .sx-page--active .sx-end-button { + min-height: 48px; + } +} diff --git a/docs/DESIGN_CONCEPT.md b/docs/DESIGN_CONCEPT.md index 3a9a419..7c9aa2f 100644 --- a/docs/DESIGN_CONCEPT.md +++ b/docs/DESIGN_CONCEPT.md @@ -514,27 +514,32 @@ function useAvatarMotion(state, affect, analyser) { ``` ┌──────────────────────────────────────────────────────────────────────────┐ -│ 상단 바 (56px) 좌:세션 제목/시나리오 중앙:회기 단계 우:경과시간·종료 │ +│ 상단 바 (80px) 좌:기록 중앙:회기 상태·단계·시간 우:학습 홈 │ ├──────────────────┬──────────────────────────────────┬──────────────────────┤ -│ LEFT (320px) │ CENTER (flexible) │ RIGHT (300px) │ +│ LEFT (326px) │ CENTER (flexible) │ RIGHT (357px) │ │ ───────────── │ ───────────────── │ ───────────── │ -│ 회기 단계 트랙커 │ 내담자 아바타 / 음성 STAGE │ 피드백 신호 패널 │ -│ (라포→탐색→ │ (어두운 배경, 중앙 집중) │ (은은한, 평시 접힘) │ -│ 개입→정리) │ │ │ -│ 내담자 컨텍스트 │ [ 아바타 + 음성 오브 ] │ ── 내담자 상태 ── │ -│ 카드 │ │ ── 라이브 신호 ── │ -│ │ 실시간 자막 (아래→위 흐름) │ (최대 1개, 페이드) │ -│ │ │ ── 셀프 노트 ── │ +│ 내담자 컨텍스트 │ [ 아바타 + 이름·사례 요약 ] │ AI 코치·코칭 기회 │ +│ 회기 단계 트랙커 │ [ 현재 표정·발화 상태 ] │ 코칭 포인트 │ +│ (라포→탐색→ │ │ 음성·AI 응답 상태 │ +│ 개입→정리) │ 실시간 자막 (아래→위 흐름) │ ── 관찰 신호 ── │ +│ 세션 진행 │ │ 방어·개방·라포 게이지 │ +│ 시간·남은시간·턴 │ │ ── 안전 점검 ── │ ├──────────────────┴──────────────────────────────────┴──────────────────────┤ -│ 하단 컨트롤 바 (80px) 마이크 · 일시정지 · [몰입│은은│코칭] · 회기 종료 확인 │ +│ 하단 컨트롤 바 (116px) 마이크 · [몰입│상태 신호│코칭] · 일시정지 · 회기 종료 │ └──────────────────────────────────────────────────────────────────────────┘ ``` **비율 의도: 시각적 무게 = 주의 배분.** 내담자 stage 60%, 평가 패널 15%(평시 대부분 접힘). 화면은 대시보드가 아니라 상담실이다. +활성 세션도 전역 `data-theme`와 `tokens.css` 의미 토큰을 그대로 따른다. 세션 전용 로컬 팔레트로 `--bg-surface`·`--text-*`·`--accent`를 다시 정의하지 않으며, 패널과 AI 코치 인셋은 공통 `Surface`가 소유한다. 라이트에서는 종이·글래스 무대, 다크에서는 차콜·세이지 무대로 함께 전환한다. + +라이트 데스크톱의 시각 기준은 1536×1024 보태니컬 워크스페이스다. 좌·우 레일은 작은 카드를 쌓지 않고 각각 하나의 연속 표면으로 보이며, 중앙만 인물 무대와 자막으로 분리한다. 장식은 `apps/web/public/session-botanical/leaf-1.webp`~`leaf-5.webp`의 투명 수채화 자산을 좌우 캔버스와 아바타 뒤에 배치한다. 자산은 상호작용과 정보보다 뒤에 놓고 `pointer-events:none`을 유지하며, 다크 테마에서는 밝기·채도를 낮춘다. + **반응형:** -- 태블릿(768~1279px): RIGHT 패널이 오버레이 drawer(평시 숨김, 우상단 신호 도트만). LEFT 280px. -- 모바일/RN(<768px): 단일 컬럼. stage 상단 고정(40vh), 자막 스크롤, 단계는 상단 가는 진행선, 평가는 하단 시트(bottom sheet). 컨트롤 바 하단 고정. +- 중간 폭(881~1180px): 단계·현재 신호는 상단 회기 요약으로 압축한다. 평시 RIGHT 진단 레일은 숨기되 코칭 모드에서는 AI 코치 카드만 하단 전폭으로 보존한다. +- 모바일(≤880px): 단일 컬럼. stage·자막을 우선하고 단계·상태는 상단 요약으로 압축한다. 코칭 모드에서는 진단·장식을 제외한 AI 코치 카드만 본문 아래에 유지한다. +- 넓지만 짧은 화면(≥1280px, 높이 ≤820px): 스테이지를 약 182px까지 압축해 자막을 주 작업영역으로 보존하고, 발화 예시·보조 흐름 설명을 먼저 접는다. +- 그 외 짧은 높이(≤700px): AI 코치의 판단·본문·근거/이력 액션을 먼저 보존하고 발화 예시와 보조 흐름 설명을 접는다. ### 5.2 중앙 STAGE — 음성 오브 (Voice Orb, 4상태) @@ -634,6 +639,9 @@ LEFT 패널 세로 트랙커 + 상단 바 가로 미니. **현재 단계만 또 [ 알겠어요 ] ← 가볍게 dismiss ``` - `--accent-tint` 배경 틴트 블록(border 없음), 13~14px. 예시 문장 `--ink-2` 이탤릭. 한 번에 1개, 새 힌트 오면 이전 것 교체(stack 안 함). +- 우측 레일의 순서는 **AI 코치 → 현재 신호 → 연결 상태**다. 코치가 장식성 신호나 운영 상태 아래로 밀려 뷰포트 밖에서 잘리면 안 된다. +- 연결 상태는 `음성`·`AI 응답` 두 값만 짧게 표시한다. 화면 모드는 하단 토글이 이미 소유하므로 우측에서 중복하지 않는다. +- 라이브 신호 영역에는 의미 없는 equalizer/막대 파형을 두지 않는다. 최근 흐름 도트는 보조 정보이며 코치 가시성을 침범하면 먼저 접는다. - **위험 신호만 예외적 즉시 표시(모드 무관):** 자해·위기 언급을 학습자가 놓치면 모드 무관하게 우측에 차분한(빨강 아닌 `--warn-solid`) 알림 — "안전 점검: 방금 위기 신호가 있었어요." 교육적으로 놓치면 안 되는 지점이라 항상 노출. #### 내담자 상태 미터 (우측 상단) — "반응 읽기 연습"의 거울 diff --git a/docs/design-concepts/generated/15-session-active-botanical-workspace.png b/docs/design-concepts/generated/15-session-active-botanical-workspace.png new file mode 100644 index 0000000..6968bfb Binary files /dev/null and b/docs/design-concepts/generated/15-session-active-botanical-workspace.png differ diff --git a/docs/dev_dashboard.html b/docs/dev_dashboard.html index 6035725..7fad26f 100644 --- a/docs/dev_dashboard.html +++ b/docs/dev_dashboard.html @@ -606,6 +606,8 @@

153차 적용(2026-07-30): 관리자 AI 엔진 설정을 자유 문자열 입력에서 게이트웨이 capability 기반 선택으로 바꿨다. claude_cli·claude_api·codex_cli·agy_cli·openai·solar 6종을 같은 계약으로 관리하고, GET /v1/capabilitiesGET /admin/engine-capabilities가 설치·인증 상태, 사용 가능 모델, 모델별 추론 강도, 기본값을 전달한다. 관리자 콘솔과 설정은 provider·모델·추론 강도를 드롭다운으로 제한하며 Codex 기본은 gpt-5.6-terra / medium, Agy 기본은 gemini-3.6-flash-high / high다. 연결 주소를 바꾸면 이전 주소의 catalog를 즉시 폐기하고, 편집 중인 새 URL을 engine_url query로 넘겨 목록을 다시 확인하기 전에는 저장할 수 없다. PATCH도 제안된 게이트웨이의 capability를 다시 확인해 연결 불가·존재하지 않는 모델·지원하지 않는 강도를 422로 막고 durable 설정을 보존한다. 기존에는 engine_mode가 사실상 설정 표시값에 가까웠으나 이제 generate/stream/readiness가 실제 provider 실행기로 라우팅되며 reasoning_effort도 DB·API·게이트웨이까지 영속된다. Codex app-server model/list에서 실제 모델을 조회하고 Terra/Medium 생성 응답 OK, Agy models에서 실제 모델을 조회하고 Gemini 3.6 Flash/High 생성 응답 VIGNETTE_ISOLATED_OK를 격리 작업 디렉터리에서 실측했다. Anthropic API는 현재 키가 없어 의도대로 선택 불가이며 live 동일성은 B2에 남긴다. 임시 신규 게이트웨이/API 연결에서는 Codex·Agy 선택 저장과 Claude CLI 원복을 확인했고, backend 전체 466 passed, web build, 관리자 AI desktop/mobile 1+1, admin AI 전수 7, settings 레이아웃·모바일 3이 통과했다. 현재 로컬 9099도 신규 게이트웨이 코드로 구동되어 Codex 7개 / Terra / medium, Agy 11개 / Gemini 3.6 Flash High / high live catalog를 반환하고, 로컬 API의 관리자 capability 라우트는 미인증 요청을 401로 차단한다. 121차의 “고장 URL을 저장해 실패 row 생성” E2E는 새 fail-closed 계약과 충돌해 저장 거부·기존 설정 보존 증거로 대체했고, 평가 재시도 UI 회귀는 route fixture spec이 계속 소유한다. 현재 상태: 로컬 작업트리·게이트웨이 검증 완료, 공개 웹 배포 전이다.

154차 적용(2026-07-30): 153차 AI 엔진 capability 변경을 공개 런타임까지 반영했다. API PID 11788과 gateway PID 44488를 신규 코드로 교체하고 Cloudflare Pages production 2f52f8e3-f992-441f-ada4-f6396cbb1a2b를 게시했다. 최종 custom domain은 index-B54GMBSf.jsAdminAi-ClAJvCvx.jsapplication/javascript 200으로 제공한다. 최초 production a3819e6e 검증에서 5세대 전 엔트리 index-Cwtfyfq5.js의 보존 누락을 발견했기 때문에, scripts/preserve-pages-assets.ps1가 최근 production preview의 HTML·JS·CSS 의존 그래프를 재귀적으로 수집하고 MIME을 검사해 현재 dist에 병합하도록 고친 뒤 최종 배포로 교체했다. custom domain에서 현재와 이전 4세대 엔트리가 모두 JavaScript 200이며, lazy 청크 첫 요청 강제 실패도 문서 재로드 1회 뒤 1/1 복구됐다. 실제 Google 슈퍼 관리자 사용자 행에 연결한 15분 임시 세션으로 공개 /admin/ai에서 공급자 6개, Codex 7개 / gpt-5.6-terra / medium, Agy 11개 / gemini-3.6-flash-high / high를 확인했고 저장 없이 세션을 즉시 삭제했다. 공개 health는 status=ok, environment=prod, db=true, engine=true이며 auth config 200, 비인증 personas·admin capability 401, watchdog LastTaskResult=0다. 153차의 공개 배포 전 상태 표기는 이 증거로 대체한다.

155차 적용(2026-07-31): 세션 텍스트 입력이 AI 생성·TTS 종료까지 잠기고 답변 표시 뒤에도 evaluator가 끝날 때까지 다음 전송이 약 24초 묶이던 학습 UX를 실제 브라우저와 gateway/API 계량으로 분해했다. textarea는 회기 종료·일시정지에서만 잠기며 생성·음성 준비·재생 중에는 다음 질문 초안을 계속 작성·보존하고, 내담자 응답 저장 직후에는 음성 재생과 무관하게 다음 전송을 허용한다. 관리자 DB의 Agy/Gemini 설정은 evaluator/review에 유지하되 실시간 client 역할은 VIGNETTE_LIVE_CLIENT_PROVIDER=claude_cli 전용 lane으로 분리해 회기별 claude -p 프로세스를 재사용한다. stream learner/client turn과 결정론 상태를 먼저 durable 저장하고 SSE done을 보낸 뒤 fast-loop 평가는 백그라운드에서 같은 learner turn normalized row에 붙이며 평가 기반 코칭 충전도 한 번만 적용한다. Agy fallback도 stream-json delta를 실제 SSE token으로 전달한다. 로컬 음성은 -UseHiggsVoice로 설치된 higgs-audio-v3-tts-4b를 loopback 상주시켜 P1 실제 답변을 24kHz WAV로 합성하며 저장소의 무참조 synthetic seed만 reference로 허용하고 non-dev는 fail-closed한다. 실증: 변경 전 full API 완료 24.17/24.57초, 변경 후 실제 UI 첫 응답·전송 해제 9.58초와 상주 연속 턴 6.96초, 생성 중 초안 enabled/보존, TTS 중 보내기 enabled. Higgs full API 362,924 bytes/7.56초 WAV, provider/model 헤더 확인. backend 432/432, gateway 44/44, typecheck·API types·build, session stream 1/1, voice skip/draft 1/1 통과. 배포·공개 실증: commit c7883434를 origin/master에 push하고 Cloudflare Pages production 505155c4-01f3-4eac-ad59-c2d323a0bb51로 게시했다. custom/preview 모두 index-BU5JjRnR.js를 JavaScript 200으로 서빙하고 최근 5개 배포의 의존 자산을 보존한다. 공개 API는 prod·db=true·engine=true, auth config Google configured/dev login disabled, 비인증 personas·voice speech 401, Google 시작 302다. 실제 공개 로그인 1280×720 렌더에서 버튼 2개 enabled, 브라우저 warning/error 0을 확인했다. 공개 음성은 라이선스 경계상 openai / gpt-4o-mini-tts를 유지한다.

+

156차 적용(2026-07-31): 활성 상담 세션이 전역 테마와 무관하게 .sx-page--active에서 다크 팔레트를 다시 정의해, 라이트 설정에서도 화면은 다크인데 공통 Surface inset인 AI 코치만 밝게 뜨는 혼합 테마 결함을 수정했다. 세션 전용 --bg-surface/--text-*/--accent 덮어쓰기를 제거하고 전역 data-theme·tokens.css·공통 Surface를 그대로 사용해 라이트는 종이·글래스, 다크는 차콜·세이지로 함께 전환한다. 우측 레일은 AI 코치 → 현재 신호 → 음성·AI 응답 상태 순으로 재배치하고 의미 없는 equalizer 막대와 중복 화면 모드 행을 제거했다. 700px 이하 높이에서는 발화 예시·보조 흐름을 먼저 접고 코치 판단과 근거/이력 액션을 보존하며, 기존 사각지대였던 881~1180px에서도 코칭 모드에 AI 코치 카드만 전폭으로 남긴다. 검증: npm run check:design-ssot, typecheck, build 통과, 세션 전수 desktop/mobile 30 passed / 2 skipped, session-layout 8/8, 7폭 layout-visual-gate 15/15, 라이트 1366×640 코치·1024×640 압축 코치 및 다크 1440/1024/390 캡처 직접 확인. 로컬 작업트리 기준이며 배포 전이다.

+

157차 적용(2026-07-31): 사용자 제공 1536×1024 활성 세션 시안을 실제 세션 화면에 반영했다. 데스크톱을 좌측 단일 컨텍스트 레일 326px / 중앙 인물 무대+자막 / 우측 단일 라이브 코칭 레일 357px / 하단 116px 제어바로 재구성하고, 상태 신호 모드에서도 AI 코치의 개입 시점·남은 기회·코칭 포인트를 미리 이해할 수 있게 했다. 제공된 수채화 잎 5장은 BEN2로 투명 분리한 뒤 600×800 이하 알파 WebP로 최적화해 합계 약 158KB로 줄였고, 좌우 캔버스와 아바타 뒤에 장식 계층으로 연결했다. 새 E2E는 1408px 상단/본문, 1468px 제어바, 326/357px 레일, WebP 응답과 computed background 연결을 1536×1024에서 고정한다. 1366×768 이하에서는 스테이지를 압축해 자막이 주 작업영역을 잃지 않게 했다. 검증: npm run check:design-ssot, typecheck, build 통과, 세션 전수 desktop/mobile 32 passed / 2 skipped, session-layout 8/8, 7폭 layout-visual-gate 15/15, 1536×1024 라이트 캡처 직접 확인. 로컬 작업트리 기준이며 배포 전이다.

래스터만 사용이미지 생성 도구 산출물은 PNG 기반 시안이다. SVG·벡터·와이어프레임·로고 시트로 해석하지 않는다.
기능 우선메인 라우트의 실제 액션과 정보 구조를 먼저 반영한다. 장식은 기능을 가리지 않는 수준에서만 쓴다.
@@ -648,7 +650,7 @@
라이브 상담 세션 PC 태블릿 모바일 다크 테마 디자인 시안 -
라이브 상담 세션내담자 무대, 자막, 입력, 라이브 신호, 안전 상태를 다크 몰입형 훈련 화면으로 구성.03-session-responsive-v2-dark-unified.png
+
라이브 상담 세션내담자 무대, 자막, 입력, AI 코치, 안전 상태를 전역 라이트·다크 테마와 같은 토큰으로 구성.03-session-responsive-v2-dark-unified.png
회기 리뷰 PC 태블릿 모바일 다크 테마 디자인 시안 @@ -955,7 +957,7 @@
DONE
엄격한 레이아웃 시각 게이트 구축 · 병렬 수정 · 적대적 재검수

핸드오프가 요구한 7개 권장 너비(390/720/861/900/1024/1280/1440) 시각 수용을 자동 게이트로 고정했다. 현재는 빈 회기리뷰 전용 화면까지 9개 화면을 각 너비에서 렌더링해 가로 overflow 0, 컨트롤 하드클립/텍스트클립 0, 다크 테마 캡처를 강제하고 화면당 7장 총 63장 풀페이지 스크린샷을 남긴다. 2026-06-28 당시 화면별 1에이전트 병렬 시각 리뷰로 49장 직접 판독 → 6개 병렬 수정 에이전트(파일 비중첩)가 결함 수정 → 화면별 적대적 재검수로 해소·무회귀를 확정했다.

산출물

apps/web/e2e/layout-visual-gate.spec.ts, node_modules/.tmp/layout-gate/*.png 63장, 2× workflow(review/verify)·6× fix agent

검증

최신 게이트 9 passed(재스크린샷). 해소된 핵심 결함: 학습자홈 1280/1440 3열 가운데 컬럼 붕괴(critical → 2열 분기 상향 + word-break:keep-all), 교수 'API 404' raw 배너 제거, 리뷰 빈상태 위계 역전과 1280 sparse third-column, 관리자 스켈레톤, 설정 와이드 좌측 데드존 제거, 세션 모바일 44px 터치타깃. 2026-06-28 적대적 재검수 7/7 accept, regression 0.

DONE
레이아웃 cosmetic minor 폴리시 일괄 처리

적대적 재검수 잔여 cosmetic minor를 5개 병렬 폴리시 에이전트로 처리했다. 학습자홈 헬퍼문구 고아 글자, 세션 1024 일시정지 아이콘 정리 + 보내기 버튼 대비 강화, 설정 2x2 칩 행 높이 균일, 교수 검토 큐 카드 갭 제거, 관리자 2열 높이 동기화를 적용했다.

산출물

page-by-page polish patches(LearnerHome/Session/Settings/Professor/Admin)

검증

npm run typecheck OK, 당시 cosmetic 시각 게이트 7 passed, 최신 통합 시각 게이트 9 passed, 레이아웃 E2E desktop/mobile 54 passed, session-layout 8/8 — 회귀 0.

DONE
회기 아카이브 저장/복원 API

/learn/history보관됨을 실제 학습자별 저장 상태로 연결했다. 종료 회기는 POST /sessions/{id}/archive/restore로 보관·복원하고, app.session_archive_state는 보기 상태만 저장한다. 진행 중 회기는 보관하지 못하며, 보관은 transcript/review/share/audit evidence를 삭제하지 않는다.

산출물

SessionArchiveResponse, LearnerSessionSummary.archived, LearnerDashboardOverview.archived_sessions, session_archive_state RLS, LearnerHome 보관/복원 버튼

검증

python -B -m pytest app/ -q 178 passed, python -B -m pytest engine_gateway/ -q 11 passed, npm run check:api-types, npm run typecheck, npm run build, npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1 6 passed.

-
DONE
세션 종료 UX와 다크 테마 SSOT

세션 하단의 드래그형 종료 컴포넌트를 명시 확인 다이얼로그로 바꿔 모바일 오발동과 제스처 실패를 줄였다. Topbar/Settings의 theme 저장·초기화는 lib/theme.ts 단일 경로로 합치고, 저장값이 없으면 dark 기본값을 앱 부팅 전에 적용한다. API 기본 preference system은 Settings에서 light로 오해하지 않고 현재 초기 테마를 따른다. active session 스테이지 상태 배지는 아바타와 겹치지 않는 3행 구조로 정리했다.

산출물

Session.tsx, AvatarPreview.tsx, session.css, avatar-expression.spec.ts, session-layout.spec.ts

검증

세션 종료·다크 테마·3행 스테이지 구조를 시각 회귀 묶음으로 확인했다. 래스터 파츠 실험 자산은 후속 결정 전 보존 상태다.

+
DONE
세션 종료 UX와 테마 SSOT

세션 하단의 드래그형 종료 컴포넌트를 명시 확인 다이얼로그로 바꿔 모바일 오발동과 제스처 실패를 줄였다. 테마 저장·초기화·OS 추종은 lib/theme.ts 단일 경로가 소유하며, 활성 세션도 전역 data-themetokens.css 의미 토큰을 그대로 따른다. 세션 전용 다크 팔레트 재정의는 제거했고 active session 스테이지 상태 배지는 아바타와 겹치지 않는 3행 구조로 유지한다.

산출물

Session.tsx, AvatarPreview.tsx, session.css, full-sweep-session.spec.ts, session-layout.spec.ts

검증

라이트·다크 세션, 종료 UX, 짧은 높이·중간 폭 AI 코치 가시성, 3행 스테이지 구조를 시각 회귀 묶음으로 확인했다.

DONE
래스터 아바타 비활성화·SVG 리그 복귀

사용자 시각 피드백에 따라 서연 P1과 P4~P7의 생성 이미지/PSD 파츠 연결을 전부 제거했다. 제품·학습자 홈·세션·dev 미리보기는 기존 SVG 도형 기반 파라미터 리그만 렌더링한다.

산출물

ClientAvatar.tsx, persona.ts, Session.tsx, LearnerHome.tsx, AvatarPreview.tsx, avatar-expression.spec.ts

검증

typecheck/build 통과, 아바타·세션 desktop/mobile 18 passed, 전체 레이아웃 시각 게이트 12 passed, 커스텀 도메인 P1 공개 번들 desktop/mobile 2 passed. data-render-mode="svg", 래스터 DOM 0개, SVG primitive·neck·표정 전환을 확인했다. 래스터 렌더러와 자산은 재검토용으로 삭제하지 않았다.

DONE
잔여(비차단) — 공용 셸 단일 항목 → 처리

축소 사이드바 세로 구분선이 본문 전체 높이까지 닿지 않던 건을 components/shell/shell.css에서 처리했다. .vg-nav border-right 제거 후 .vg-shell__body 컨테이너 배경 하어라인으로 본문 그리드 전체 높이 구분선을 그리고, 그리드·구분선 폭을 --nav-cur로 동기화. learner-home 로딩 스켈레톤 밀도도 실제 카드 구조 모사로 보강했다.

판정

구현 완료. 검증: npm run typecheck PASS + vite build PASS. 전 페이지 시각 회귀 게이트는 web+api+DB 스택으로 이 워크스테이션 미실행 — 스택 가용 시 1회 시각 확인 권장.

DONE
디자인 SSOT 리팩터링 · 인증 화면 복구

테마 알림은 lib/theme.ts/lib/useTheme.ts, 앱 크롬은 AppShell, 글래스 표면은 Surface가 각각 단독 소유한다. 로그인·온보딩·승인대기는 AuthShell의 100vw/100dvh 캔버스를 공유하고, 학습·세션·리뷰·교수자·관리자·페르소나·설정의 카드/인셋은 공통 Surface variant로 이관했다. shell.css의 페이지 클래스 나열과 !important 표면 덮어쓰기는 제거했다. 라이트 Surface도 2중 반투명 광원, 24px backdrop blur, 굴절 hairline을 공통 토큰으로 사용하고 모바일 셸의 보태니컬 배경을 유지한다.

검증

npm run check:design-ssot, typecheck/build 통과. 인증 라이트·다크 × 390/1280 시각 게이트 1 passed, 14개 화면 × 7개 폭 레이아웃 게이트 14 passed, 학습·세션·리뷰 desktop/mobile 46 passed, 로그인→온보딩→학습 desktop/mobile 2 passed. 라이트 글래스의 gradient/blur/hairline/shadow와 모바일 배경 자산은 computed style 단언으로 고정했다.

diff --git a/docs/guides/testing.md b/docs/guides/testing.md index 8863fc1..e739b3c 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -321,17 +321,18 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API - 2026-07-01 focused 검증: `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q` **10 passed**. 교수자 대시보드 API는 `session_persistence.list_all_sessions()`로 전체 담당 세션을 읽고, 학습자 일반 목록은 `list_recent_sessions()`로 최근 100개 제한을 명시해 한 학생의 최신 회기가 다른 학생 분석 목록을 밀어내지 않는지 검증한다. 또한 ended session summary가 `app.session_evaluation`의 `evaluation_status`, `review_ready`, `supervisor_state`, `evaluation_error`를 별도로 싣고, 평가 실패가 수동 review status와 섞이지 않는지 검증한다. - 2026-07-01 focused 검증: `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 --grep "approved admin without onboarding"` **2 passed**. 승인된 `role=admin` 사용자의 세션에 `admin_access=false`, `onboarding_completed_at=null`이 남아도 `/admin`이 `/onboarding`으로 우회하지 않고 관리 콘솔의 `/admin/*` API를 호출하는지 fixture로 고정한다. - 2026-07-15 보태니컬 글래스 UI·SSOT 검증: `npm run check:design-ssot` + `npm run typecheck` + `npm run build` 통과, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --reporter=line` 시각 게이트 **15/15**, `npx playwright test e2e/auth-visual.spec.ts --project=chromium-single-run --reporter=line` **1/1**, `npx playwright test e2e/session-layout.spec.ts e2e/learner.spec.ts e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile` **46/46**, 로그인→온보딩 focused desktop/mobile **2/2**. 공통 AppShell GNB, Theme store, Surface variant의 소유권과 전 폭 대시보드/리뷰 탭, 로그인·온보딩 라이트/다크 390/1280px, 고해상도 보태니컬 자산을 함께 고정한다. 라이트 테마도 패널당 복수 굴절 그라데이션, backdrop blur, 헤어라인, 그림자를 computed style로 단언하며 모바일 셸이 보태니컬 배경을 제거하지 않는지 검사한다. 관리자 사용자 표는 semantic table, 정렬 헤더, 1440px 최소 폭과 표 전용 가로 스크롤을 desktop/mobile에서 검증한다. +- 2026-07-31 활성 세션 보태니컬 워크스페이스 검증: `full-sweep-session.spec.ts`의 1536×1024 계약이 좌 326px·우 357px 레일, 1408px 상단/본문, 1468px 하단 제어바, 좌우/스테이지 보태니컬 WebP 연결과 라이트 테마를 실측한다. 세션 전수 desktop/mobile **32 passed / 2 skipped**, `session-layout` **8/8**, 7폭 `layout-visual-gate` **15/15**, `check:design-ssot`·typecheck·build를 통과했다. 1366×768 이하는 스테이지보다 자막이 작아지지 않게 별도 압축 계약을 적용한다. 레이아웃·시각 회귀 게이트(핵심 합격선): | 게이트 | 스펙 | 구성 | 개수 | |---|---|---|---| | 세션 레이아웃 | `e2e/session-layout.spec.ts` | 4 테스트 × (desktop+mobile) | **8 / 8** | -| 시각 레이아웃 게이트 | `e2e/layout-visual-gate.spec.ts` | `@single-run`, 14개 화면 × 7개 폭 검사 + 다크 테마 assertion + 학습 대시보드 라이트 자산 연결 | **14 / 14** | +| 시각 레이아웃 게이트 | `e2e/layout-visual-gate.spec.ts` | `@single-run`, 15개 화면 계약 × 7개 폭 검사 + 다크 테마 assertion + 학습 대시보드 라이트 자산 연결 | **15 / 15** | | 인증 테마 게이트 | `e2e/auth-visual.spec.ts` | 로그인·온보딩 × light/dark × 390/1280px, 100vw/100dvh 및 overflow 검사 | **1 / 1** | | 레이아웃 포커스(재설계 화면) | `session-layout`·`session-review`·`admin`·`learner`·`settings`·`teacher`, `@single-run` 제외 | desktop+mobile 병렬 | **54** | -> `layout-visual-gate`는 7개 폭(390/720/861/900/1024/1280/1440)에서 14개 핵심 화면(페르소나 운영·작성 단계 포함)의 가로 +> `layout-visual-gate`는 7개 폭(390/720/861/900/1024/1280/1440)에서 15개 핵심 화면 계약(페르소나 운영·작성 단계 포함)의 가로 > 오버플로·잘린 컨트롤·다크 테마 적용을 검사하고 전체 페이지 스크린샷을 > `node_modules/.tmp/layout-gate/`에 남긴다. 학습 대시보드는 추가로 라이트 테마의 카드·사이드바· > 우상단 배경 자산이 실제 computed style에 연결됐는지 확인하고 1200px·390px 라이트 캡처를 남긴 뒤 다크로 복귀한다.