전수 E2E 순회·소유자 결정 6건 구현·디자인 감사 반영

- 전수 순회: IA 전 라우트 412개 기능 인벤토리를 체크리스트 백로그로 관리
  (docs/ops/e2e-full-sweep-2026-07-27.md), 신규 full-sweep 스펙 10파일 추가.
  RED→GREEN으로 결함 12건 수정: 관리자 무한 렌더 프리즈(TanStack autoReset
  루프), 복수 코호트 저장 유실, 페르소나 보관 503(SQL 컬럼 모호성), PII 과잉
  마스킹, /admin/ai watchdog 오탐 오버레이, 모바일 겹침 2건, 설정 스크롤
  스파이, 온보딩 전화번호 무검증, 리뷰 조사·난도 라벨, pending 피드백 등.
- 소유자 결정 구현: 설정 아바타 변경, 동의 철회·재동의 전체 흐름, 신규
  학습자 기초 우선 추천, 학생 분석 테이블 가상화(@tanstack/react-virtual),
  저작 모드 죽은 레일 정리, 감정 밸런스 타임라인 차트(deep turn_valence +
  결정론 파생 폴백).
- 디자인 감사(142차): 라이트 팔레트 AA 대비, 다크 토큰 별칭 통일, 미정의
  CSS 변수 정리, 한글 keep-all 전역화, 탭 타깃 24px, LCP preconnect.
- 검증: npm run e2e 병렬 432 수집 GREEN + 직렬 49/49 exit 0, 백엔드 pytest
  421, gateway 29, typecheck/build/design-ssot/dead-code/중복 게이트 통과,
  layout-visual-gate 15/15, session-layout 8/8. 상세는 SSOT 대시보드
  142~144차 노트.
This commit is contained in:
Yun Chan 2026-07-27 14:25:24 +09:00
parent 0ae94499f2
commit 4511383cd9
55 changed files with 9333 additions and 573 deletions

View file

@ -2637,7 +2637,13 @@
.sx-prestart {
width: 100%;
display: grid;
grid-template-columns: minmax(220px, 0.82fr) minmax(360px, 1.72fr) minmax(260px, 0.98fr);
/* 트랙 최소치를 0으로 컨테이너가 좁아져도 트랙이 끝까지 축소되게 한다(구조적 넘침 불가).
이전 minmax(220px,) minmax(360px,) minmax(260px,) 최소 840px+gap 48px=888px
요구했는데, 뷰포트 1041~1240px 구간은 사이드바를 실제 컨테이너가 735~886px 뿐이라
3열이 축소되지 못하고 우측 "진행 초점" 패널이 최대 195px 잘려 보였다.
overflow-x:hidden 이라 스크롤도 되고 문서 오버플로도 0 이라 기존 게이트가 잡았다.
아래 2열 전환(1260px) 최소치 0 이중 안전망이다. (2026-07-27) */
grid-template-columns: minmax(0, 0.82fr) minmax(0, 1.72fr) minmax(0, 0.98fr);
align-items: center;
gap: var(--sp-5);
max-width: none;
@ -2794,13 +2800,16 @@
line-height: 1.6;
max-width: 58ch;
}
/* 사실 목록. repeat(3, 1fr) 고정이면 항목이 1개뿐인 상태(페르소나 로딩/폴백)에서도
칸이 1/3 쪼개져 값이 세로로 길게 잘렸다(결함 #2). auto-fit 으로 바꿔 남는 트랙을
접고, 항목 ·폭에 따라 1~3열로 스스로 접히게 한다. */
.sx-prestart__facts {
margin: var(--sp-4) 0 0;
padding: var(--sp-4) 0;
padding: var(--sp-3) 0;
border-top: 1px solid var(--hair);
border-bottom: 1px solid var(--hair);
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(auto-fit, minmax(min(100%, 96px), 1fr));
gap: var(--sp-4);
}
.sx-page--prestart .sx-prestart__facts {
@ -2814,16 +2823,27 @@
font-size: 11.5px;
font-weight: 700;
}
/* 값은 2줄 클램프로 잘라 쓰던 자리다. 폴백 문구("실제 연습 대상 정보를 확인하고
있습니다.") 320~421 등에서 113px 잘려 나가 클램프를 걷고 전문을 흘린다.
대신 데스크톱 화면 제약은 아래 min-width:1181px 블록에서 사실 블록 자체의
최대 높이 + 내부 스크롤로 지킨다. */
.sx-prestart__facts dd {
margin: 6px 0 0;
color: var(--text-strong);
font-size: 13.5px;
font-weight: 650;
line-height: 1.4;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
display: block;
overflow: visible;
overflow-wrap: anywhere;
}
/* 데스크톱은 문서 스크롤 없이 화면에 들어가야 하므로, 페르소나 요약이 아주 때만
사실 블록 안에서 스크롤되게 상한을 둔다(잘라내지 않는다). */
@media (min-width: 1181px) {
.sx-prestart__facts {
max-height: 152px;
overflow-y: auto;
}
}
.sx-prestart__chips {
display: flex;
@ -2847,9 +2867,12 @@
background: var(--clay-tint);
color: var(--clay-deep);
}
/* 아래 여백(이론모드 / 시작 버튼 ) dd 클램프를 걷어내면서 늘어난 세로
12px 되돌려 받는 자리다. 프리스타트는 1280x900 에서 문서 스크롤 없이 들어가야
한다( 파일 상단 .vg-main:has(.sx-prestart--...) 규칙과 같은 제약). */
.sx-theory {
width: min(100%, 520px);
margin: var(--sp-4) 0 0;
margin: var(--sp-3) 0 0;
padding: 0;
border: 0;
display: grid;
@ -2913,12 +2936,16 @@
font-size: 11px;
font-weight: 650;
}
/* 상태 안내문. 예전에는 목표 fieldset 과의 간격을 줄이려고 margin-top -12px
당겨 썼는데, .sx-goals 마진이 0 으로 끝나는 블록이라 -12px 그대로
fieldset 마지막 (목표 힌트) 위로 파고들었다. 320~1536 폭에서 88x12 겹침이
재현돼(프리스타트 결함 #1) 음수 마진을 걷어내고 양수 간격으로 되돌린다. */
.sx-prestart__note {
max-width: 460px;
font-size: 12.5px;
color: var(--text-muted);
line-height: 1.55;
margin-top: calc(var(--sp-3) * -1);
margin-top: var(--sp-2);
}
.sx-prestart__err {
font-size: var(--fs-sm);
@ -2956,7 +2983,7 @@
align-items: center;
gap: var(--sp-4);
flex-wrap: wrap;
margin-top: var(--sp-5);
margin-top: var(--sp-4);
}
.sx-prestart__actions span {
color: var(--text-muted);
@ -3036,7 +3063,10 @@
}
/* ── 반응형 ── */
@media (max-width: 1040px) {
/* 3열이 실제로 들어가는 최소 컨테이너는 888px 이고, 폭은 뷰포트 1241px 부터 확보된다.
기존 전환점 1040px 200px 짜리 사각지대(1041~1240) 남겨 우측 패널이 잘렸다.
안전 마진을 두고 1260px 까지 2열(사이드 패널은 하단 전체 ) 내린다. (2026-07-27) */
@media (max-width: 1260px) {
.sx-prestart {
grid-template-columns: minmax(180px, 230px) minmax(0, 1fr);
}
@ -3448,6 +3478,14 @@
.sx-page--active .sx-seg-block__label {
display: none;
}
/* 절대배치 넛지가 좁은 폭에서 음성 건너뛰기 위쪽 컨트롤을 덮어 클릭을
가로채던 문제(결함 로그 #18) 모바일에서는 플로우 배치로 전환한다. */
.sx-page--active .sx-coach-nudge {
position: static;
transform: none;
align-self: center;
margin: 0 auto 4px;
}
.sx-page--active .sx-segmented button {
font-size: 12px;
flex: 1 1 0;
@ -3590,23 +3628,22 @@
margin-top: 6px;
font-size: 18px;
}
/* 2줄 클램프를 쓰던 자리. 본문 열이 좁아지는 320~421 에서 문장이 73px 잘려
나갔다(결함 #3). 1180 이하는 어차피 세로 스크롤이 있는 구간이라 전문을 흘린다. */
.sx-page--prestart .sx-prestart__desc {
font-size: 12.5px;
line-height: 1.45;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
display: block;
overflow: visible;
}
.sx-page--prestart .sx-prestart__facts {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(auto-fit, minmax(min(100%, 132px), 1fr));
gap: 6px;
margin-top: 8px;
padding: 8px 0;
}
.sx-page--prestart .sx-prestart__facts dd {
font-size: 11.5px;
-webkit-line-clamp: 2;
}
.sx-page--prestart .sx-prestart__chips {
display: none;
@ -3766,6 +3803,19 @@
}
}
/* 프리스타트 아주 좁은 : 1180 이하 규칙이 아바타 열을 140px 고정해 두는데,
460px 아래에서는 본문 열에 128px 밖에 남지 않아 설명문·목표 버튼·사실값이 전부
짜부러졌다(결함 #2·#3 실제 원인). 구간만 세로 1열로 쌓는다. */
@media (max-width: 460px) {
.sx-page--prestart .sx-prestart {
grid-template-columns: minmax(0, 1fr);
gap: 8px;
}
.sx-page--prestart .sx-prestart__visual {
padding: 6px 8px;
}
}
@media (max-width: 1180px) and (max-height: 820px) {
.sx-sessionbar {
min-height: 38px;
@ -4090,6 +4140,12 @@
}
/* ── 시간 알람 바(10분 전 경고 · 시간 만료) — 강제 노출 배너 ── */
/* 시간 알람 바가 있으면 페이지 그리드가 4자식이 된다. 3행 템플릿 그대로면
타임바가 1fr 행을 차지하고 그리드가 min-content로 밀려 모바일에서
컨텍스트 패널이 타임바를 덮는 겹침이 생긴다(결함 로그 #18). */
.sx-page--active:has(> .sx-timebar) {
grid-template-rows: min-content min-content minmax(0, 1fr) min-content;
}
.sx-timebar {
display: flex;
align-items: center;