feat: clean review-instrument restyle with bundled Pretendard font

- Bundle Pretendard Variable woff2 locally (air-gapped safe, no CDN)
  and switch UI/stamp font stacks to it; preload in index.html
- Replace the forensic-dossier paper theme with a flat neutral cool
  palette: single teal accent, white cards, no noise texture, and
  zero linear/radial gradients (per design contract)
- Restore the product-purpose top-bar block and its CSS, drop the
  unused global search form, and strip the stray UTF-8 BOM
- Re-skin queue hover/selection, eyebrows, nav rail, chips, and
  empty states to the neutral palette; tabular numerals for numbers
- Regenerate ui-overhaul final audit artifacts: zero horizontal
  overflow across 8 views at 1440x900 and 390x844, Pretendard active

Design spec: docs/superpowers/specs/2026-06-11-operator-console-clean-review-ui-design.md
Plan: docs/plans/2026-06-11-001-feat-operator-console-clean-review-ui-plan.md
Tests: 358 passed (full suite incl. browser smoke)
This commit is contained in:
유창욱 2026-06-11 10:31:16 +09:00
parent 3f7b3a9cf2
commit ed701bd436
6 changed files with 1065 additions and 492 deletions

View file

@ -0,0 +1,52 @@
---
status: active
created: 2026-06-11
type: quality
title: Operator console clean review UI + Pretendard font plan
spec: docs/superpowers/specs/2026-06-11-operator-console-clean-review-ui-design.md
---
# Operator Console Clean Review UI Plan
목표: 전체적 UI 점검 및 폰트 변경, 깔끔하게 잘 떨어지는 심사 프로그램의 목적에 맞게 개선.
## Phase 0 — Baseline (done)
- [x] 전 뷰 데스크톱/모바일 스크린샷 + 오버플로 감사 (`data/logs/ui-font-baseline-*`)
- [x] 정적 테스트 베이스라인: `test_workbench_shell_exposes_all_internal_operator_views` 실패
(product-purpose 블록/CSS 삭제됨), index.html BOM 유입 확인
- [x] Pretendard Variable woff2 v1.3.9 오프라인 번들 확보
(`web/operator-gui/assets/fonts/PretendardVariable.woff2`)
## Phase 1 — Font integration
- [ ] `styles.css` 상단에 `@font-face` (Pretendard Variable, weight 45 920, swap)
- [ ] `--font-ui`, `--font-stamp` 토큰을 Pretendard 스택으로 교체 (mono 유지)
- [ ] `index.html`에 woff2 preload 링크 추가, BOM 제거
- [ ] 점수/쿼터/시간 표기에 `font-variant-numeric: tabular-nums`
## Phase 2 — Clean re-skin (token-driven)
- [ ] `:root` 팔레트를 종이톤 → 중성 쿨톤으로 교체 (spec 3.2)
- [ ] body 노이즈 텍스처 + radial-gradient 제거 (플랫 서피스)
- [ ] :root 밖 하드코딩 색상(37 hex, 36 rgba) 중 종이톤 잔재 일괄 정리
- [ ] 그림자/포커스 링을 쿨톤으로 정돈
- [ ] 모든 테스트 계약 문자열 보존 (spec 3.4 목록)
## Phase 3 — Contract restoration
- [ ] top-bar에 `product-purpose` 블록 복원 (HTML + CSS)
- [ ] 큐 오버플로 미세 정리: 데스크톱 `strong`, 모바일 `provider-chip`
## Phase 4 — Verification (사용자 반응성 검토)
- [ ] `pytest tests/operator_gui` 전체 통과
- [ ] 라이브 서버 Playwright 감사: 8개 뷰 × 데스크톱/모바일, docW <= vw
- [ ] `data/logs/ui-overhaul-final-results.json` + 계약 스크린샷 8종 재생성
- [ ] Pretendard 적용 여부 런타임 확인 (document.fonts)
- [ ] 오프라인 검증: GUI 내 외부 fetch 참조 0건
- [ ] `pytest` 전체 회귀
## Phase 5 — Commit
- [ ] 디자인 문서 + 구현 + 산출물 커밋 (feat/chore 분리 없이 단일 feat 커밋)

View file

@ -0,0 +1,114 @@
# Operator Console Clean Review UI — Design Spec
- Date: 2026-06-11
- Status: approved (autonomous goal session)
- Goal: 전체적 UI 점검 및 폰트 변경, 깔끔하게 잘 떨어지는 심사 프로그램의 목적에 맞게 개선
## 1. Problem
The current working-tree restyle ("Forensic Dossier") gives the operator console a
vintage paper-document look: beige paper background with an SVG noise texture,
ochre/teal dual accents, a "stamp" display font (Bahnschrift), and warm hairlines.
That direction conflicts with the product's purpose — a fast, trustworthy,
**clean review instrument** an operator stares at for hours.
Additional defects found during the baseline audit (2026-06-11):
- `tests/operator_gui/test_static_workbench.py::test_workbench_shell_exposes_all_internal_operator_views`
fails: the uncommitted change deleted the `product-purpose` top-bar block and its CSS.
- `index.html` gained a UTF-8 BOM (``).
- Typography relies on Malgun Gothic, which renders Korean UI text loosely and
inconsistently across weights.
- Minor horizontal overflow offenders: `strong` inside desktop queue rows,
`span.provider-chip` on mobile queue (inside the intentionally scrollable table shell).
Baseline artifacts: `data/logs/ui-font-baseline-*.png`, `data/logs/ui-font-baseline-results.json`.
## 2. Approaches Considered
1. **Token re-skin (chosen).** Keep the stylesheet's structure, selectors, and all
test-contract strings; replace the design tokens (`:root`), the body texture,
the font stack, and sweep hardcoded decorative colors. Lowest risk: every
class hook and fixed contract string (queue grid template, audit widths,
floating panel geometry, media queries) survives untouched.
2. Full stylesheet rewrite. Cleanest end state but high risk of dropping one of
the ~30 fixed contract strings the static tests pin; harder to review.
3. Revert to the committed styles and start over. Discards legitimate layout
work already in the working tree (queue grid, floating decision panel).
## 3. Design Decisions
### 3.1 Typography (폰트 변경)
- Bundle **Pretendard Variable** (`web/operator-gui/assets/fonts/PretendardVariable.woff2`,
v1.3.9, 2.06 MB) — downloaded at build time, served locally. **No CDN reference**
at runtime (air-gapped rule).
- `@font-face` with `font-display: swap`, weight range 45920.
- Token changes:
- `--font-ui: "Pretendard Variable", Pretendard, "Malgun Gothic", "Apple SD Gothic Neo", "Segoe UI", system-ui, sans-serif`
- `--font-stamp` (labels, IDs, numeric stamps): same Pretendard stack —
differentiation now comes from weight (650750), size, and `letter-spacing`,
not from a second display face. Bahnschrift is dropped.
- `--font-mono` unchanged (Cascadia Mono / Consolas) for hashes, IDs, code.
- `index.html` gets `<link rel="preload" as="font" type="font/woff2" crossorigin>`
for the woff2.
- Numeric UI (scores, quotas, timestamps) uses `font-variant-numeric: tabular-nums`.
### 3.2 Surface and color (깔끔하게 잘 떨어지는 톤)
- Remove the SVG noise texture and both radial gradient washes from `body`
flat, calm surface.
- Palette shifts from warm paper to neutral cool:
- `--paper` `#f3f5f7` (workspace background), `--paper-2` `#e9edf0`
- `--card` `#ffffff`, `--card-raised` `#ffffff`, `--card-sunk` `#f6f8fa`
- Ink scale: `--ink #1a2128`, `--ink-soft #51606b`, `--ink-faint #8794a1`
- Hairlines: cool grays (`#dde3e8` / `#e7ecf0` / `#b9c4cd`)
- Single primary accent: deep teal-blue (`--teal #1f6f8b` family). The ochre
family stays defined (components reference it) but is re-tuned toward a
restrained amber used only for "hold/attention" semantics, not branding.
- Risk semantics keep their class names and stay high-contrast:
red=high, amber=medium, green=low/approve, plus failed/pending neutrals.
- Shadows get smaller and cooler; radii stay (6/9/14px). No `linear-gradient`
(test contract), and the remaining radial gradients are removed for flatness.
- The dark nav rail stays dark (orientation anchor) but moves to a neutral
slate so the single accent color reads clearly.
### 3.3 Contract restorations
- Re-add the `product-purpose` block to the top bar (`class="product-purpose"`,
`aria-label="제품 목적"`, copy: "이미지 저작권 위험 심사" /
"제출 이미지, 외부 검색 근거, 내부 기준 DB를 한 화면에서 검토합니다.") and its
`.product-purpose` CSS, styled to fit the clean top bar.
- Remove the BOM from `index.html`.
- The deleted global-search form and operator chip stay deleted (no test
references them; the queue has its own search field).
### 3.4 Explicitly preserved test-contract strings in `styles.css`
`grid-template-columns: 28px 64px minmax(104px, 0.68fr) 72px minmax(126px, 0.58fr) minmax(360px, 1.5fr) 82px 76px 90px`,
`--audit-object-width: 24%`, `.audit-table th:nth-child(4)/(5)` with
`width: var(--audit-object-width)`, `.floating-decision-panel` with
`position: fixed` / `bottom: 24px` / `padding-right: 334px` /
`padding-bottom: 260px`, `data-workbench-panel="evidence"` selector,
`@media (max-width: 980px)`, `@media (max-width: 680px)`, `:focus-visible`,
`.risk-high/.risk-medium/.risk-low/.risk-failed`,
`.source-naver/.source-google/.source-llm/.source-internal`,
`.queue-row td:nth-child(5)/(6)`, `white-space: nowrap`, `flex-wrap: nowrap`,
`.queue-provider-strip`, `.coverage-tabs`/`.coverage-tab`, no `linear-gradient`,
no `orb`, obsolete selectors (`.coverage-main`, `.coverage-badges`,
`.coverage-provider-grid`, `.coverage-mini-line`, `.queue-table th:nth-child`)
stay absent.
## 4. Verification Plan (사용자 반응성 검토)
1. `node --check` on touched JS (none expected) and full
`pytest tests/operator_gui` static contracts.
2. Live-server Playwright audit at 1440×900 and 390×844 across all 8 views:
zero horizontal document overflow (`docW <= vw`), regenerate
`data/logs/ui-overhaul-final-results.json` and the 8 contract screenshots.
3. Interaction smoke: existing `test_browser_smoke.py` (suggested-query fill,
real upload flow) must pass against the restyled DOM.
4. Visual review of screenshots: font rendering (Pretendard active), flat
surfaces, risk colors legible, floating decision panel unobstructed.
5. Offline check: grep the GUI for `http://`/`https://` font or CSS fetches —
none allowed.

View file

@ -3239,36 +3239,6 @@ async function emergencyDisableExternalProviders() {
function handleGlobalSearch(event) {
event.preventDefault();
const query = document.getElementById("global-search-input").value.trim().toLowerCase();
if (!query) return;
const match = submissions.find(
(submission) => submission.id.toLowerCase().includes(query) || submission.title.toLowerCase().includes(query),
);
if (match) {
selectCase(match.id);
return;
}
switchView("knowledge");
renderAll();
}
function bindEvents() {
document.querySelectorAll(".nav-button").forEach((button) => {
@ -3411,8 +3381,6 @@ function bindEvents() {
document.getElementById("knowledge-image").addEventListener("change", updateKnowledgeImageName);
document.getElementById("global-search-form").addEventListener("submit", handleGlobalSearch);
document.getElementById("reload-submissions").addEventListener("click", reloadSubmissions);
document.getElementById("submission-image").addEventListener("change", updateSubmissionImageName);
document.getElementById("upload-submission-image").addEventListener("click", uploadSubmissionImage);

Binary file not shown.

View file

@ -1,9 +1,10 @@
<!doctype html>
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>권리 검수 콘솔</title>
<link rel="preload" href="assets/fonts/PretendardVariable.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="styles.css">
<script src="operator-labels.js" defer></script>
<script src="submission-import.js" defer></script>
@ -58,21 +59,13 @@
<div class="workspace">
<header class="top-bar">
<form class="global-search" id="global-search-form" role="search">
<label class="sr-only" for="global-search-input">전체 검색</label>
<input id="global-search-input" type="search" autocomplete="off" placeholder="제출 ID, 엔티티, 도메인 검색">
</form>
<div class="product-purpose" aria-label="제품 목적">
<strong>이미지 저작권 위험 심사</strong>
<span>제출 이미지, 외부 검색 근거, 내부 기준 DB를 한 화면에서 검토합니다.</span>
</div>
<div class="queue-health" id="queue-health" aria-live="polite"></div>
<div class="coverage-tabs" id="coverage-tabs" aria-label="검색 근거 필터" aria-live="polite"></div>
<div class="provider-pulse" id="provider-pulse" aria-label="외부 검색 tool 활용 상태"></div>
<div class="operator-chip">
<span>운영자</span>
<strong>rights.ops</strong>
</div>
<div class="coverage-tabs" id="coverage-tabs" aria-label="검색 근거 필터" aria-live="polite"></div>
</header>
<main class="main-surface" id="app-main">

File diff suppressed because it is too large Load diff