Files
pulp-console/design_handoff_console_redesign/README.md
Hyemin Lee 9b686e35ba feat(ui): 콘솔 재디자인 적용 (Toss 스타일) + Pico v2 호환 수정
디자인 핸드오프(design_handoff_console_redesign)를 실제 앱에 반영.
CSS 드롭인 교체 + 최소 마크업 추가, HTMX 훅·id·data 속성은 모두 보존.

- app.css: 재디자인 토큰/컴포넌트로 교체(요약카드 호버 리프트, 표
  제브라, 중앙 경고형 배포 모달, secondary/ghost/danger 버튼 체계,
  진행률·상태칩·스켈레톤 등).
- base.html: 브랜드 로고 큐브(.brand-mark) 추가 + 데모 배너.
- repo_list: 요약 카드 .card-top/.card-ico + 검색 돋보기 아이콘.
- table_wrap: 회전형 정렬 인디케이터, 동기화 버튼 아이콘.
- progress: .sync-head/.sync-pct/.is-active/.indeterminate 구조.
- deploy_modal: 중앙 경고형 + 현재→대상 프리뷰 카드 + 타입-투-컨펌
  체크/X 시각 상태.
- deploy_success: 그려지는 체크 원 배너.
- version_list: 표 → .version-item 카드 리스트.

Pico v2 호환(미리보기는 pico-shim 기준이라 안 드러났던 것):
- Pico 의 `:is(...).secondary`(0,2,0)·hover(0,4,0) 특정성을 이기도록
  오버라이드를 :is() 형태로 상향 → 동기화 버튼 흰배경+파란글씨 유지,
  hover 시 어두워지던 문제 해결.
- `input[type=search]` 의 Pico 기본 돋보기 배경 제거(아이콘 중복).
- `td` 배경을 투명 처리해 tr 제브라/hover 가 보이도록(Pico 의
  `td{background:var(--pico-background-color)}` 가 덮던 문제), 줄무늬
  색을 --bg-alt 로 또렷하게.
- .row-action 을 40px 고정 → 버튼↔진행률↔배지 교체 시 높이 흔들림·
  위아래 테두리 잘림 해소.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 10:05:17 +09:00

14 KiB
Raw Blame History

Handoff: 폐쇄망 Pulp 운영 콘솔 — 시각 디자인 개선

Overview

한국은행 IT전략부 클라우드팀의 폐쇄망(인터넷 차단) Pulp 3 관리 웹 UI의 시각 디자인 개선입니다. 운영자가 저장소를 동기화 → 버전 스냅샷 확인 → 검증 → 운영 배포하는 도구로, "관공서 기본 폼" 느낌을 벗고 Toss 스타일의 신뢰감 있고 정돈된 운영 도구로 끌어올리는 것이 목표입니다.

기능/스택 변경 없이 CSS(토큰 + 컴포넌트) + 최소한의 마크업 추가만으로 개선했습니다.

About the Design Files

이 번들의 파일은 HTML로 만든 디자인 레퍼런스입니다. 다만 이 프로젝트는 특수하게도 타깃 환경이 이미 정해져 있고, 산출물이 그 환경에 그대로 들어가는 CSS입니다:

  • app.css실제 산출물. 기존 앱의 app/static/app.css를 이 파일로 교체하면 됩니다. (Pico.css v2 뒤에 로드되어 변수를 오버라이드하는 커스텀 스타일. 순수 CSS, 외부 의존 0.)
  • preview.html렌더링 참고용. 개선안이 실제로 어떻게 보이는지 확인하는 하니스입니다. 앱에 넣지 마세요.
  • pico-shim.css미리보기 전용, 앱에 넣지 마세요. 폐쇄망에서 CDN 없이 preview.html을 렌더링하려고 Pico의 최소 기본 스타일만 흉내 낸 파일입니다. 실제 앱은 진짜 Pico.css v2를 씁니다.

작업의 본질: app.css를 교체하고, 아래 "컴포넌트별 변경 + 필요 마크업"에 적힌 소량의 템플릿 마크업(Jinja2)을 추가/조정하는 것입니다. 프레임워크를 바꾸지 않습니다.

Fidelity

High-fidelity. 최종 색/타이포/간격/인터랙션이 확정된 목업입니다. 아래 값 그대로 구현하세요.

절대 지켜야 할 기술 제약

  1. 빌드 단계 없음. React/Vue/Vite/npm/Tailwind 금지. 순수 HTML + HTMX + Jinja2 + Pico.css v2 + 커스텀 CSS(app.css)만.
  2. 폐쇄망. 런타임에 외부 네트워크(구글폰트/CDN/원격 아이콘/fetch) 참조 금지. 모든 자원 로컬 동봉. 새 @import(원격)/CDN <link>/<script> 추가 금지. 아이콘은 인라인 SVG.
  3. 폰트는 이미 로컬 동봉: 본문 Pretendard Variable, 코드/숫자 JetBrains Mono (@font-face/static/fonts/*.woff2 로드). 새 폰트 추가 금지. (app.css에 @font-face 포함됨)
  4. 크롬에 장식용 이모지 금지. 상태는 색 있는 chip/dot으로.
  5. 뷰는 HTML 조각(fragment)을 반환, HTMX가 swap. 라이트 모드 기본, 다크는 토글 옵션.

보존해야 하는 훅 (바꾸면 기능 깨짐)

  • 모든 HTMX 속성: hx-get/hx-post/hx-target/hx-swap/hx-trigger/hx-include/hx-vals/hx-disabled-elt (값·타깃 셀렉터 유지).
  • element id: #repo-list, #repo-table-wrap, #repo-search, #modal, #flash, #sync-{uuid}, #deploy-confirm-btn.
  • data 속성: data-state(정렬), data-confirm(타입-투-컨펌), data-theme(테마).
  • 배포 확정 버튼의 oninput 비활성화 로직, 테마 토글/FOUC 방지 인라인 스크립트.
  • 클래스명을 리네임하려면 CSS와 템플릿 양쪽 다 맞춰 바꿀 것.

주: preview.html의 검색 input은 데모라 id가 없습니다. 실제 앱에서는 #repo-search를 그대로 두고 .repo-search 클래스만 유지하세요.


컴포넌트별 변경 + 필요 마크업

대부분은 기존 마크업 그대로 두고 CSS만 바뀌면 됩니다. 아래는 새 요소/구조가 필요한 부분만 정리했습니다. 각 스니펫은 preview.html에서 실제로 렌더되는 형태입니다.

0. 상단 내비 브랜드 마크 (선택)

차분한 단색 사각 로고 큐브. 인라인 SVG 없이 CSS만으로.

<p class="brand"><span class="brand-mark">P</span>Pulp <span class="accent">Console</span></p>

1. 요약 카드 (대시보드)

카드 상단에 라벨 + 아이콘 칩 한 줄(.card-top)을 추가. 값은 그대로 .value. .is-accent(전체 저장소=파랑), .is-success(GPG 통과=민트)로 강조.

<div class="card is-accent">
  <div class="card-top">
    <span class="label">전체 저장소</span>
    <span class="card-ico"><svg class="icon" viewBox="0 0 24 24" ...></svg></span>
  </div>
  <div class="value is-accent">18</div>
</div>

호버 시 살짝 떠오름(translateY -2px)+그림자. (reduced-motion에서 자동 비활성)

2. 검색 툴바

.list-toolbar를 relative 래퍼로 두고 왼쪽에 돋보기 인라인 SVG(.search-ico). input(#repo-search, .repo-search)의 왼쪽 패딩을 아이콘 자리만큼(40px) 확보(→ CSS가 처리).

<div class="list-toolbar">
  <svg class="search-ico" viewBox="0 0 24 24" ...><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
  <input id="repo-search" class="repo-search" type="search" placeholder="저장소 이름으로 검색…"
         hx-get="..." hx-target="#repo-table-wrap" hx-trigger="input changed delay:300ms" ...>
</div>

3. 저장소 표

  • 카드로 감싸기(.table-card) — 기존 유지.
  • 확정안 = 제브라 스트라이프: 짝수 행에 옅은 톤(CSS tbody tr:nth-child(even)), hover는 틴티드 블루.
  • 정렬 헤더: a.th-sort(활성 시 .is-active), 방향 인디케이터 .sort-ind(desc면 .th-sort.is-active.desc가 화살표 회전).
  • 검증 배지: .badge.badge-pass|warn|unset|deployed(dot + 라벨, 이모지 없음).
  • 저장소명 셀: .repo-name > a(모노, hover 시 accent).
  • 액션 셀: 폭·높이 고정 박스 .row-action(버튼↔진행률↔배지로 내용이 바뀌어도 레이아웃 안 흔들림).
<td><div class="row-action"> …버튼/진행률/배지… </div></td>

4. 버튼 색 체계 (중요)

  • 동기화(반복 노출) = .secondary흰 배경 + 파란 글씨 + 옅은 파란 테두리. 표에 여러 개 떠도 안 번잡.
  • 새로고침 등 유틸리티 = .ghost → 중립(투명 배경 + 회색 테두리). 동기화와 확실히 구분.
  • 위험(배포 확정) = .danger → 빨강 채움 + 흰 글씨.
  • 공통: press 시 scale(0.97), hover 시 옅은 그림자.
  • 로딩 상태: 버튼에 .is-loading 클래스를 붙이면 텍스트가 스피너로 대체됨. HTMX 요청 시 hx-disabled-elt와 함께 .is-loading을 토글하면 자연스럽다.
<button class="secondary btn-sm btn-icon" hx-post="/sync/{{ repo.id }}"
        hx-target="#sync-{{ repo.id }}" hx-disabled-elt="this">
  <svg class="icon" ...></svg> 동기화
</button>

5. 동기화 진행률

<div class="sync-progress is-active">
  <div class="sync-head"><small>동기화 중</small><small class="sync-pct">64%</small></div>
  <progress value="64" max="100"></progress>
</div>

값을 모르는 초기 단계엔 .sync-progress.indeterminate(움직이는 인디터미닛 바)를 쓸 수 있음.

6. 배포 확인 모달 (<dialog id="modal">) — 위험 강조 + 타입-투-컨펌

확정안 = 중앙 경고형. 헤더 밴드 대신 상단 중앙에 원형 경고 아이콘.

<dialog id="modal">
  <article class="deploy-modal">
    <div class="modal-head">
      <span class="warn-ico"><svg viewBox="0 0 24 24" ...>△!</svg></span>
      <div><h3>운영 배포 확인</h3><span class="head-sub">되돌리기 어려운 작업입니다</span></div>
    </div>
    <div class="modal-body">
      <p class="deploy-note" style="text-align:center"><span class="repo-hl">rhel8-baseos</span> 저장소를 운영에 배포합니다.</p>

      <!-- 현재 → 대상 카드형 프리뷰 -->
      <div class="deploy-preview">
        <div class="pv-box"><div class="pv-label">현재 (운영)</div><div class="pv-ver">v140</div><div class="pv-pkg">9,001 pkgs</div></div>
        <div class="pv-arrow"><svg ...></svg></div>
        <div class="pv-box pv-target"><div class="pv-label">대상</div><div class="pv-ver">v142</div><div class="pv-pkg">9,204 pkgs · <span class="delta-add">+203</span></div></div>
      </div>

      <!-- 타입-투-컨펌: 이름이 정확히 일치해야 확정 버튼 열림 -->
      <label class="confirm-label">확인을 위해 저장소 이름 <span class="repo-hl">rhel8-baseos</span> 을 입력하세요.
        <div class="confirm-field" data-confirm="rhel8-baseos">
          <input type="text" autocomplete="off" placeholder="rhel8-baseos"
                 oninput="document.getElementById('deploy-confirm-btn').disabled = (this.value.trim() !== this.closest('.confirm-field').dataset.confirm)">
          <svg class="confirm-state ok" ...></svg>
          <svg class="confirm-state bad" ...></svg>
        </div>
      </label>
      <!-- 힌트 문구 없음: 상태는 초록 체크/초록 테두리(일치) · 빨강 X/빨강 테두리(불일치)로만 표시 -->

      <div class="modal-actions">
        <button class="ghost" onclick="this.closest('dialog').close()">취소</button>
        <button class="danger" id="deploy-confirm-btn" disabled>운영에 배포</button>
      </div>
    </div>
  </article>
</dialog>

입력 상태 시각화는 .confirm-field.is-valid / .is-invalid 클래스로 제어됩니다.

  • 순수 오조작 방지만 필요하면 위 oninput처럼 #deploy-confirm-btn.disabled만 토글해도 됨.
  • 체크/X 아이콘·테두리 색까지 원하면 값 일치 여부에 따라 .confirm-fieldis-valid/is-invalid도 함께 토글(입력이 비면 둘 다 제거).
  • 열릴 때 배경 스크롤 잠금은 CSS html:has(dialog[open]){overflow:hidden} + 진입 애니메이션을 translate 없이 scale/opacity로만 처리해 순간 스크롤바 깜빡임이 없음.

7. 버전 목록 (저장소 상세 — 저장소명 클릭 시 화면)

<a href="{{ url_for('dashboard') }}" role="button" class="ghost btn-sm btn-icon back-btn">
  <svg class="icon" ...></svg> 대시보드
</a>
<div class="verify-detail"><span class="badge badge-pass">GPG 통과</span><small>서명 검증 통과 · … · 키 0x3A9F…</small></div>
<div class="version-list">
  <div class="version-item">
    <span class="ver-no">v142</span>
    <span class="badge badge-deployed">운영 중</span>
    <span class="ver-meta">2026-07-09 04:12 · 9,204 pkgs</span>
    <span class="spacer"></span>
    <button class="ghost btn-sm" disabled>현재 운영 버전</button>
  </div>
  <div class="version-item">
    <span class="ver-no">v141</span>
    <span class="ver-meta">… · <span class="delta-add">+40</span> <span class="delta-del">16</span></span>
    <span class="spacer"></span>
    <button class="secondary btn-sm" hx-get="/deploy_modal/{{ repo }}/{{ v }}" hx-target="#modal">이 버전 배포</button>
  </div>
</div>

8. 상태 조각들

  • 배포 완료 배너 #flash (.flash-banner): 성공 체크 원(.flash-check, 그려지는 SVG 체크) + 본문 + .flash-sub + .flash-warn(되돌리기 안내). 등장 시 페이드-업.
  • 데모 배너 (.demo-banner): 가짜 데이터 경고, 앰버 톤, 펄스 dot.
  • 연결 상태 칩 (.status-line.status-ok|bad|loading|demo): dot + 텍스트. ok는 펄스, loading은 blink.
  • 빈 상태 (.empty-note + .empty-ico/.empty-title/.empty-sub).
  • 에러 상태 (.error-note): 경고 아이콘 + 메시지, danger 톤.
  • 스켈레톤 (.skeleton.skeleton-row): 셔머 로딩. 표 초기/재조회 자리표시.
  • HTMX 스왑 중 페이드: .htmx-swapping{opacity:.4} 자동 적용(이미 CSS에 포함).

Design Tokens (라이트 / :root)

색은 app.css:root(라이트)와 [data-theme="dark"](다크) 두 곳에서 정의되고, 컴포넌트는 전부 이 변수를 참조하므로 토큰만 손보면 라이트/다크가 함께 따라옵니다.

  • 배경: --bg #ffffff · --bg-alt #f9fafb · --surface #f2f4f6 · --surface-2 #e8f3ff(틴티드 블루)
  • 텍스트: --text #191f28 · --text-secondary #333d4b · --text-muted #6b7684 · --text-dim #8b95a1
  • 테두리: --border #e5e8eb · --border-strong #d1d6db
  • 액센트(Toss Blue를 살짝 톤다운): --accent oklch(0.63 0.147 255) · --accent-hover oklch(0.55 0.16 255) · --accent-soft #e8f3ff
  • 상태: --success-text #00a37a · --warning-text #c5700a · --danger #f04452 (+ *-soft 반투명 배경)
  • 라운드: 카드 --radius-card 12px, 버튼 8px(--pico-border-radius)
  • 그림자: --shadow-card(얇게) · --shadow-card-hover · --shadow-modal(무겁게)
  • 모션: --ease cubic-bezier(0.32,0.72,0,1) · --dur-fast .12s · --dur .2s · --dur-slow .32s
  • 타이포: 본문 Pretendard Variable, 숫자/버전/시각/저장소명은 JetBrains Mono(+ tnum tabular). 헤더 th는 한글 라벨이라 모노 제외.

모든 애니메이션은 prefers-reduced-motion: reduce에서 자동 비활성(파일 맨 끝 미디어쿼리).

Interactions & Behavior

  • 버튼: hover 그림자, active scale(0.97). .is-loading으로 스피너 교체.
  • 표: 행 hover 배경 전환, 정렬 화살표 회전, 검증 dot(pass)·연결 dot(ok) 은은한 펄스.
  • 진행률: 결정형 <progress> width 트랜지션, 초기엔 인디터미닛 바.
  • 모달: 배경 페이드 + 본문 scale/opacity 진입, 배경 스크롤 잠금(스크롤바 깜빡임 방지), 타입-투-컨펌(이름 일치 시에만 확정 활성 + 초록 체크 / 불일치 시 빨강 X).
  • 성공 배너: 체크 SVG stroke-dash 그리기 애니메이션.

적용 순서 (Claude Code에게)

  1. 기존 app/static/app.css를 이 번들의 app.css로 교체.
  2. 위 "컴포넌트별 변경 + 필요 마크업"에 따라 각 Jinja2 템플릿에 새 요소(.card-top/.card-ico, .search-ico, .modal-head/.warn-ico/.pv-box/.confirm-state 등)를 추가. HTMX 훅·id·data 속성은 그대로.
  3. preview.html을 브라우저로 열어 렌더 기준과 대조(라이트/다크 토글).
  4. PULP_DEMO=true로 데모 데이터 띄워 표/검색/정렬/모달 실동작 확인.
  5. 회귀 확인: pytest -q(현행 58 통과), ruff check app.

Files

  • app.css — 실제 산출물 (drop-in).
  • preview.html — 렌더 참고 하니스 (앱에 넣지 말 것).
  • pico-shim.css — 미리보기 전용 Pico 최소 흉내 (앱에 넣지 말 것).