From 250787cb18eaee223530374a6e66685b12f28790 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sat, 27 Jun 2026 18:34:36 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8B=A4=ED=81=AC=20=ED=85=8C=EB=A7=88=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/Settings.tsx | 3 +- apps/web/src/pages/settings/settings.css | 126 +++++++++++++++++++++++ docs/DESIGN_CONCEPT.md | 2 + docs/HANDOFF.md | 4 +- docs/dev_dashboard.html | 4 +- docs/ops/backlog-2026-06-26.md | 4 +- 6 files changed, 136 insertions(+), 7 deletions(-) diff --git a/apps/web/src/pages/Settings.tsx b/apps/web/src/pages/Settings.tsx index fc9afe2..51f2d8a 100644 --- a/apps/web/src/pages/Settings.tsx +++ b/apps/web/src/pages/Settings.tsx @@ -22,6 +22,7 @@ import { type UserProfileResponse, type VoicePresetResponse, } from "../lib/api"; +import { readInitialTheme } from "../lib/theme"; import { Toggle } from "./settings/Toggle"; import { useTheme } from "./settings/useTheme"; import "./settings/settings.css"; @@ -214,7 +215,7 @@ export default function Settings() { } setPreferences(nextPrefs); setPreferencesReady(true); - setDark(nextPrefs.theme === "dark"); + setDark(nextPrefs.theme === "dark" || (nextPrefs.theme !== "light" && readInitialTheme() === "dark")); setVoicePresets(presets); if (nextEngine) { engineConfigRef.current = nextEngine; diff --git a/apps/web/src/pages/settings/settings.css b/apps/web/src/pages/settings/settings.css index 5444f70..1adcc52 100644 --- a/apps/web/src/pages/settings/settings.css +++ b/apps/web/src/pages/settings/settings.css @@ -1537,3 +1537,129 @@ body[data-page="settings"] .vg-set__pill { min-height: 0; } } + +html[data-theme="dark"] body[data-page="settings"] .vg-shell { + background: + radial-gradient(circle at 12% 0%, rgba(111, 179, 164, 0.18), transparent 34%), + radial-gradient(circle at 92% 12%, rgba(205, 154, 116, 0.12), transparent 32%), + linear-gradient(135deg, rgba(11, 25, 22, 0.98), rgba(18, 30, 27, 0.96)), + var(--asset-warm-elements) center / cover no-repeat; +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set__rail, +html[data-theme="dark"] body[data-page="settings"] .vg-set__group { + border-color: rgba(203, 227, 220, 0.13); + background: + linear-gradient(180deg, rgba(28, 44, 39, 0.93), rgba(16, 29, 26, 0.95)), + rgba(14, 27, 24, 0.88); + box-shadow: + 0 24px 58px rgba(0, 0, 0, 0.26), + inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set__rail-title h1, +html[data-theme="dark"] body[data-page="settings"] .vg-set__group-title, +html[data-theme="dark"] body[data-page="settings"] .vg-set__profile strong, +html[data-theme="dark"] body[data-page="settings"] .vg-set__label, +html[data-theme="dark"] body[data-page="settings"] .vg-set__opt-main, +html[data-theme="dark"] body[data-page="settings"] .vg-set__voice-title { + color: rgba(241, 247, 245, 0.96); +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set__rail-title p, +html[data-theme="dark"] body[data-page="settings"] .vg-set__group-desc, +html[data-theme="dark"] body[data-page="settings"] .vg-set__profile span, +html[data-theme="dark"] body[data-page="settings"] .vg-set__hint, +html[data-theme="dark"] body[data-page="settings"] .vg-set__opt-sub, +html[data-theme="dark"] body[data-page="settings"] .vg-set__voice-meta, +html[data-theme="dark"] body[data-page="settings"] .vg-set__foot, +html[data-theme="dark"] body[data-page="settings"] .vg-set__callout { + color: rgba(214, 228, 224, 0.72); +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set__nav-item { + color: rgba(205, 232, 225, 0.82); +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set__nav-item:hover { + background: rgba(111, 179, 164, 0.12); + color: rgba(239, 248, 245, 0.96); +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set__nav-item.is-active { + background: linear-gradient(135deg, rgba(111, 179, 164, 0.9), rgba(62, 136, 121, 0.94)); + color: #061511; + box-shadow: 0 14px 28px rgba(7, 20, 17, 0.34); +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set__nav-item.is-active svg { + color: #061511; +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set__profile, +html[data-theme="dark"] body[data-page="settings"] .vg-set__field, +html[data-theme="dark"] body[data-page="settings"] .vg-set__control-block, +html[data-theme="dark"] body[data-page="settings"] .vg-set__opt, +html[data-theme="dark"] body[data-page="settings"] .vg-set__range-row, +html[data-theme="dark"] body[data-page="settings"] .vg-set__seg, +html[data-theme="dark"] body[data-page="settings"] .vg-set__pill, +html[data-theme="dark"] body[data-page="settings"] .vg-set__callout, +html[data-theme="dark"] body[data-page="settings"] .vg-badge { + border-color: rgba(203, 227, 220, 0.11); + background: rgba(255, 255, 255, 0.055); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035); +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set__voice, +html[data-theme="dark"] body[data-page="settings"] .vg-set__select, +html[data-theme="dark"] body[data-page="settings"] .vg-input { + border-color: rgba(203, 227, 220, 0.12); + background: rgba(11, 24, 21, 0.78); + color: rgba(241, 247, 245, 0.94); +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set__voice.is-active, +html[data-theme="dark"] body[data-page="settings"] .vg-set__seg-btn.is-active { + border-color: rgba(111, 179, 164, 0.48); + background: rgba(111, 179, 164, 0.2); + color: rgba(239, 248, 245, 0.98); +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set__seg-btn { + color: rgba(205, 232, 225, 0.76); +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set__seg-btn:not(.is-active):hover, +html[data-theme="dark"] body[data-page="settings"] .vg-set__voice:not(.is-active):hover { + background: rgba(111, 179, 164, 0.11); + color: rgba(239, 248, 245, 0.96); +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set__range { + accent-color: rgba(111, 179, 164, 0.95); +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set__avatar { + background: linear-gradient(135deg, rgba(111, 179, 164, 0.92), rgba(49, 116, 102, 0.95)); + color: #061511; +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set .vg-btn--primary { + border-color: rgba(111, 179, 164, 0.48); + background: linear-gradient(135deg, rgba(111, 179, 164, 0.88), rgba(73, 145, 130, 0.9)); + color: #061511; +} + +html[data-theme="dark"] body[data-page="settings"] .vg-set .vg-btn--primary:hover:not(:disabled) { + background: linear-gradient(135deg, rgba(129, 202, 186, 0.94), rgba(84, 160, 143, 0.96)); +} + +@media (max-width: 720px) { + html[data-theme="dark"] body[data-page="settings"] .vg-set__rail { + border-bottom-color: rgba(203, 227, 220, 0.12); + background: + linear-gradient(180deg, rgba(22, 36, 32, 0.98), rgba(13, 26, 23, 0.96)), + rgba(13, 26, 23, 0.96); + box-shadow: 0 12px 26px rgba(0, 0, 0, 0.28); + } +} diff --git a/docs/DESIGN_CONCEPT.md b/docs/DESIGN_CONCEPT.md index fe338f9..61dba97 100644 --- a/docs/DESIGN_CONCEPT.md +++ b/docs/DESIGN_CONCEPT.md @@ -186,6 +186,7 @@ CSS 변수는 primitive(원시값) → semantic(의미 별칭) 2계층. 컴포 ### 3.5 다크 모드 (gloom 회피 — 충분한 밝은 면 + 절제된 악센트) 배경은 한색 슬레이트(순흑 아님). 악센트는 명도를 올려 가독 확보. +운영 기본 표면은 dark이며, 저장 theme 값이 없거나 API preference가 `system`이면 light로 떨어지지 않고 앱 초기 dark 값을 따른다. dark 화면 안의 대형 카드/섹션은 순백·밝은 베이지 면을 쓰지 않는다. ```css [data-theme="dark"] { @@ -621,6 +622,7 @@ LEFT 패널 세로 트랙커 + 상단 바 가로 미니. **현재 단계만 또 ### 5.7 회기 종료 후 리뷰 화면 (진짜 학습이 일어나는 곳) 실시간에선 일부러 참았던 정밀 평가를 여기서 푼다. 구조: **타임라인 + 기법 라벨 + 슈퍼바이저 코멘트 + 개선점.** +리뷰도 라이브 세션과 같은 dark 작업면을 유지한다. 저장된 발화가 없는 빈 상태에서도 흰 카드로 빠지지 않고, 근거 대기/평가 대기/워크시트 대기를 어두운 패널로 보여준다. ``` ┌──────────────────────────────────────────────────────────────────────────┐ diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 21b2752..04c8759 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,6 +1,6 @@ # Vignette Handoff -> Updated: 2026-06-27 18:25 KST. 새 세션은 이 문서와 `docs/DESIGN_CONCEPT.md`를 먼저 읽고 이어가면 된다. +> Updated: 2026-06-27 18:26 KST. 새 세션은 이 문서와 `docs/DESIGN_CONCEPT.md`를 먼저 읽고 이어가면 된다. ## 현재 상태 @@ -24,7 +24,7 @@ - 계약 SSOT 3차: `apps/web/src/lib/api.ts`의 수기 DTO 중 auth, persona, admin/user/engine, review leaf, teacher safety/growth leaf, learner sessions, session review/worksheet aggregate, teacher dashboard aggregate 타입을 `apps/web/src/lib/api.gen.ts`의 `ApiSchema` alias로 전환했다. generated optional/default 차이는 화면 form state, notification default helper, 배열 렌더링 fallback으로 흡수한다. CI 연결과 SessionDetail/SessionStart 계열 stage enum 정리는 후속. 검증: `npm run check:api-types`, `npm run typecheck`, `npm run build`, learner/session-review/teacher/layout focused E2E 32 passed. - 턴 런타임 리팩터 2차: `turn_runtime.finalize_completed_turn`으로 REST submit/stream/voice WS의 `record_completed_turn` + `record_safety_event` 호출쌍을 공통화했다. gateway `/v1/stream` route 레벨에서 `token/done/error` SSE 프레임 contract tests를 추가했다. 검증: focused backend 40 passed. - Phase3 artifact checker 강화: `scripts/check-phase3-artifacts.py`가 CSV enum, KPI report 필수 field, approved export의 PII pass·κ/ICC·withdrawn exclusion·consent scope·file sha256을 검증한다. `app/test_phase3_artifact_checker.py` 5 tests 추가. 실제 파일럿 evidence와 steward/legal/IAA gate는 그대로 외부 의존이다. -- 세션 종료 UX/theme SSOT 정리: 드래그형 `SlideToEnd`를 명시 확인 다이얼로그로 교체하고, Topbar/Settings theme 토글을 `lib/theme.ts` 단일 경로로 통합했다. 저장값이 없으면 dark를 기본으로 두고 앱 부팅 시 theme를 먼저 적용해 초기 flash를 줄인다. 검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts` 15 passed. +- 세션 종료 UX/dark theme SSOT 정리: 드래그형 `SlideToEnd`를 명시 확인 다이얼로그로 교체하고, Topbar/Settings theme 토글을 `lib/theme.ts` 단일 경로로 통합했다. 저장값이 없으면 dark를 기본으로 두고 앱 부팅 시 theme를 먼저 적용해 초기 flash를 줄인다. API 기본 preference `system`은 Settings에서 light로 오해하지 않고 현재 초기 테마를 따른다. 회기 리뷰·설정 포함 주요 페이지의 흰 섹션 잔재도 dark 작업면으로 맞췄다. 검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts e2e/settings.spec.ts e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run --workers=1` 22 passed. - M3 인증 claim 1차: Google/SAML/dev-login이 설정 기반 cohort map과 SAML cohort claim을 `cohort_ids`로 넘기고, 관리 사용자 `external_id`는 provider subject 기반(`google:`/`saml:`/`dev:`)으로 저장한다. 운영 SAML 서명검증·기관 claim schema·deprovisioning audit은 후속. - Google OAuth 진단 1차: provider callback error는 `access_denied`/`provider_error`로 분리하고, 로그인 화면은 실패 reason code를 함께 표시한다. 실제 Google 계정 완료 proof는 아직 owner 로그인/storageState가 필요하다. - `frontenddesign` 스킬은 현재 세션의 사용 가능 스킬 목록에 없었다. 대신 `docs/DESIGN_CONCEPT.md`를 SSOT로 사용했다. diff --git a/docs/dev_dashboard.html b/docs/dev_dashboard.html index 4e19a8a..eaf0932 100644 --- a/docs/dev_dashboard.html +++ b/docs/dev_dashboard.html @@ -774,7 +774,7 @@
DONE
레이아웃 감사팀 / 전면 레이아웃 개선

서브에이전트 전담으로 1차 overflow/clipping 수리 후 2차 전면 재설계를 진행했고, 이번 라운드에서 7개 권장 너비 시각 수용까지 닫았다. 세션, 학습자 홈/리뷰, 교수 콘솔, 관리자, 설정 화면을 작업 화면 기준으로 재배치했고, 과한 빈칸·튀어나감·버튼/텍스트 잘림·모바일 테이블 사용성을 줄였다. 엄격한 시각 게이트 + 화면별 적대적 시각 재검수에서 7개 화면 모두 accept(critical/major 0, 회귀 0)를 받아 DONE으로 닫는다.

산출물

docs/ops/layout-redesign-handoff-2026-06-26.md, page-by-page redesign patches, apps/web/e2e/layout-visual-gate.spec.ts

검증

npm run typecheck OK, 엄격 시각 게이트 7 passed, layout focused E2E desktop/mobile 54 passed, 적대적 시각 재검수 7/7 accept·회귀 0.

DONE
엄격한 레이아웃 시각 게이트 구축 · 병렬 수정 · 적대적 재검수

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

산출물

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

검증

게이트 7 passed(재스크린샷). 해소된 핵심 결함: 학습자홈 1280/1440 3열 가운데 컬럼 붕괴(critical → 2열 분기 상향 + word-break:keep-all), 교수 'API 404' raw 배너 제거, 리뷰 빈상태 위계 역전, 관리자 스켈레톤, 설정 와이드 좌측 데드존 제거, 세션 모바일 44px 터치타깃. 적대적 재검수 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, 시각 게이트 7 passed, 레이아웃 E2E desktop/mobile 54 passed, session-layout 8/8 — 회귀 0.

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

세션 하단의 드래그형 종료 컴포넌트를 명시 확인 다이얼로그로 바꿔 모바일 오발동과 제스처 실패를 줄였다. Topbar/Settings의 theme 저장·초기화는 lib/theme.ts 단일 경로로 합치고, 저장값이 없으면 dark 기본값을 앱 부팅 전에 적용한다. API 기본 preference system은 Settings에서 light로 오해하지 않고 현재 초기 테마를 따르며, 회기 리뷰·설정 포함 주요 페이지의 흰 섹션 잔재를 dark 작업면으로 맞췄다.

산출물

Session.tsx, session.css, session-review.css, Settings.tsx, settings.css, lib/theme.ts, settings/useTheme.ts, session-layout.spec.ts

검증

npm run typecheck, npm run build, npx playwright test e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts e2e/settings.spec.ts e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run --workers=1 30 passed. 추가 시각 감사: 7개 주요 페이지 × PC/태블릿/모바일 캡처에서 theme=dark, horizontal overflow 0, large bright surfaces 0.

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

세션 하단의 드래그형 종료 컴포넌트를 명시 확인 다이얼로그로 바꿔 모바일 오발동과 제스처 실패를 줄였다. Topbar/Settings의 theme 저장·초기화는 lib/theme.ts 단일 경로로 합치고, 저장값이 없으면 dark 기본값을 앱 부팅 전에 적용한다. API 기본 preference system은 Settings에서 light로 오해하지 않고 현재 초기 테마를 따르며, 회기 리뷰·설정 포함 주요 페이지의 흰 섹션 잔재를 dark 작업면으로 맞췄다.

산출물

Session.tsx, session.css, session-review.css, Settings.tsx, settings.css, lib/theme.ts, settings/useTheme.ts, session-layout.spec.ts

검증

npm run typecheck, npm run build, npx playwright test e2e/layout-visual-gate.spec.ts e2e/settings.spec.ts e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run --workers=1 22 passed.

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회 시각 확인 권장.

검증 실행 상세 표

요약하지 않고 실행 범위, 명령, 결과를 행 단위로 남긴다.

@@ -831,7 +831,7 @@ Learner/readiness E2Elearner.spec.ts + readiness.spec.ts desktop/mobile14 passed Learner screenshotslearn-empty-desktop.png / learn-empty-mobile-compact.pngdocument overflow 0, empty history visible Session layout/turnsession-layout.spec.ts desktop/mobile8 passed - Session end + dark theme UXnpm run typecheck, npm run build, layout-visual-gate, session-layout, settings, session-reviewtypecheck/build passed; Playwright 묶음 30 passed(layout visual gate 7, session-layout desktop/mobile 8, settings desktop/mobile+single-run 13, session-review desktop/mobile 2). 7개 주요 페이지 × PC/태블릿/모바일 수동 캡처 감사에서 theme=dark, horizontal overflow 0, large bright surfaces 0. + Session end + dark theme UXnpm run typecheck, npm run build, layout-visual-gate, settings, session-reviewtypecheck/build passed; Playwright 묶음 22 passed(layout visual gate 7, settings desktop/mobile+single-run 13, session-review desktop/mobile 2). Settings now treats API system preference as current initial theme instead of forcing light. Voice/session focusedvoice + voice-success + session-layout15 passed Layout redesign handoffdocs/ops/layout-redesign-handoff-2026-06-26.mdsubagent scopes, files, verification, remaining visual review recorded Layout redesign focused E2Enpx playwright test e2e/learner.spec.ts e2e/session-layout.spec.ts e2e/session-review.spec.ts e2e/admin.spec.ts e2e/settings.spec.ts e2e/teacher.spec.ts --project=chromium-desktop --project=chromium-mobile54 passed after isolated Admin tablet flake rerun diff --git a/docs/ops/backlog-2026-06-26.md b/docs/ops/backlog-2026-06-26.md index feec6fe..3adbb93 100644 --- a/docs/ops/backlog-2026-06-26.md +++ b/docs/ops/backlog-2026-06-26.md @@ -13,11 +13,11 @@ ## B1. 비차단 코스메틱 폴리시 - [x] **공용 셸 사이드바 구분선 전체 높이** — (2026-06-26 처리) `.vg-nav border-right`(sticky·뷰포트 높이)를 제거하고, `.vg-shell__body` 컨테이너 배경 하어라인(`linear-gradient` 1px×100%, `--nav-cur`로 그리드·구분선 폭 동기화)으로 본문 그리드 전체 높이를 따라가는 세로 구분선을 그린다. 1024 축소폭은 `--nav-cur: var(--nav-w-collapsed)`로, 720 이하(상단 가로바 전환)는 `background-image:none`으로 정합. `components/shell/shell.css`. **검증: `npm run typecheck` PASS + `vite build` PASS.** 시각 회귀 게이트(gate 7/7 + 포커스 E2E + session-layout 8/8)는 web+api+DB 스택(Docker 미기동·secrets 부재)으로 이 환경 미실행 — 스택 가용 시 1회 시각 확인 권장. -- [x] **세션 종료 UX와 theme SSOT** — (2026-06-27 처리) 오발동과 모바일 조작 불안을 줄이기 위해 세션 하단의 드래그형 `SlideToEnd`를 명시 확인 다이얼로그로 교체했다. Topbar/Settings의 `vignette.theme` 읽기·저장·`data-theme` 적용은 `apps/web/src/lib/theme.ts` 단일 경로로 통합했고, 저장값이 없으면 dark 기본값을 부팅 전에 적용한다. **검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts` 15 passed.** +- [x] **세션 종료 UX와 dark theme SSOT** — (2026-06-27 처리) 오발동과 모바일 조작 불안을 줄이기 위해 세션 하단의 드래그형 `SlideToEnd`를 명시 확인 다이얼로그로 교체했다. Topbar/Settings의 `vignette.theme` 읽기·저장·`data-theme` 적용은 `apps/web/src/lib/theme.ts` 단일 경로로 통합했고, 저장값이 없으면 dark 기본값을 부팅 전에 적용한다. API 기본 preference `system`은 Settings에서 light로 오해하지 않고 현재 초기 테마를 따른다. 회기 리뷰·설정 포함 주요 페이지의 흰 섹션 잔재도 dark 작업면으로 맞췄다. **검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts e2e/settings.spec.ts e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run --workers=1` 22 passed.** - [ ] **빈상태 컬럼 높이 여백** — session-review 1280 masonry 우측 컬럼, session-prestart 우측 '진행 초점' 컬럼, learner-home 1440 하단 등 데이터 0건 빈상태에서만 두드러지는 컬럼 높이 불균형/여백. 실데이터면 채워진다. **(미착수, 의도적)** 0건 전용 시각 균형이라 러닝 앱에서의 시각 반복 없이 블라인드 수정 시 악화 위험 → 스택 가용 시 시각 확인하며 처리. 빈상태 전용 균형 레이아웃 채택 자체가 별도 폴리시 결정. - [x] **learner-home 로딩 스켈레톤 밀도(390)** — (2026-06-26 처리) '연습 대상' 로딩 스켈레톤을 빈 div 단일 셰이머에서 실제 카드 구조(마크 박스 + 이름/메타/요약 2줄 플레이스홀더)를 모사하도록 밀도 보강. `LearnerHome.tsx` (`.lh-skel__box`/`.lh-skel__line*`), reduced-motion 분기 동반 갱신. **검증: `npm run typecheck` PASS + `vite build` PASS.** -검증 기준: 변경 후 `npm run typecheck`, `e2e/layout-visual-gate.spec.ts`(7/7), 레이아웃 포커스 E2E(54), `e2e/session-layout.spec.ts`(8/8) 무회귀. (E2E 게이트는 web+api+DB 스택 필요 — 현재 워크스테이션은 Docker 미기동·API secrets 부재로 미실행. 코드 변경은 typecheck+vite build로 1차 검증함.) +검증 기준: 변경 후 `npm run typecheck`, `e2e/layout-visual-gate.spec.ts`(7/7), 레이아웃 포커스 E2E(54), `e2e/session-layout.spec.ts`(8/8) 무회귀. 이번 dark theme 라운드는 로컬 web+api+DB 스택 가용 상태에서 typecheck/build와 Playwright 묶음 22 passed까지 확인했다. ---