디자인 핸드오프(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>
14 KiB
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. 최종 색/타이포/간격/인터랙션이 확정된 목업입니다. 아래 값 그대로 구현하세요.
절대 지켜야 할 기술 제약
- 빌드 단계 없음. React/Vue/Vite/npm/Tailwind 금지. 순수 HTML + HTMX + Jinja2 +
Pico.css v2 + 커스텀 CSS(
app.css)만. - 폐쇄망. 런타임에 외부 네트워크(구글폰트/CDN/원격 아이콘/fetch) 참조 금지. 모든 자원 로컬 동봉.
새
@import(원격)/CDN<link>/<script>추가 금지. 아이콘은 인라인 SVG. - 폰트는 이미 로컬 동봉: 본문
Pretendard Variable, 코드/숫자JetBrains Mono(@font-face로/static/fonts/*.woff2로드). 새 폰트 추가 금지. (app.css에 @font-face 포함됨) - 크롬에 장식용 이모지 금지. 상태는 색 있는 chip/dot으로.
- 뷰는 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-field에is-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(+tnumtabular). 헤더 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에게)
- 기존
app/static/app.css를 이 번들의app.css로 교체. - 위 "컴포넌트별 변경 + 필요 마크업"에 따라 각 Jinja2 템플릿에 새 요소(.card-top/.card-ico, .search-ico, .modal-head/.warn-ico/.pv-box/.confirm-state 등)를 추가. HTMX 훅·id·data 속성은 그대로.
preview.html을 브라우저로 열어 렌더 기준과 대조(라이트/다크 토글).PULP_DEMO=true로 데모 데이터 띄워 표/검색/정렬/모달 실동작 확인.- 회귀 확인:
pytest -q(현행 58 통과),ruff check app.
Files
app.css— 실제 산출물 (drop-in).preview.html— 렌더 참고 하니스 (앱에 넣지 말 것).pico-shim.css— 미리보기 전용 Pico 최소 흉내 (앱에 넣지 말 것).