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>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -24,9 +24,16 @@
|
||||
<script src="/static/htmx.min.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
{% if demo_mode %}
|
||||
<div class="demo-banner" role="alert">
|
||||
<span class="demo-dot"></span>
|
||||
<span>데모 모드 — 화면의 저장소·버전·배포는 모두 <strong>가짜 데이터</strong>입니다.
|
||||
실제 Pulp 에 연결되어 있지 않습니다.</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<nav class="app-nav">
|
||||
<div class="container">
|
||||
<p class="brand"><span class="accent">Pulp</span> 패치 관리 콘솔</p>
|
||||
<p class="brand"><span class="brand-mark">P</span>Pulp <span class="accent">패치 관리 콘솔</span></p>
|
||||
<button class="theme-toggle" type="button" onclick="toggleTheme()"
|
||||
aria-label="라이트/다크 전환" title="라이트/다크 전환">
|
||||
<svg class="icon moon" viewBox="0 0 24 24" width="18" height="18" fill="none"
|
||||
|
||||
@@ -1,50 +1,65 @@
|
||||
<dialog open>
|
||||
<dialog open class="deploy-dialog">
|
||||
<article class="deploy-modal">
|
||||
<header>
|
||||
<h3>이 버전을 운영망에 배포</h3>
|
||||
</header>
|
||||
|
||||
{% if error %}
|
||||
<p class="status-line status-bad">{{ error }}</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="deploy-note">운영 서버(yum/dnf)가 실제로 내려받는 버전이 바뀝니다. 신중히 확인하세요.</p>
|
||||
|
||||
{% if preview %}
|
||||
<table class="deploy-preview">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>현재 운영</th>
|
||||
<td class="num">{% if preview.current_version is not none %}v{{ preview.current_version }} · {{ preview.current_count }}개{% else %}없음{% endif %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>배포 대상</th>
|
||||
<td class="num">v{{ preview.target_version }} · {{ preview.target_count }}개</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>순 변화</th>
|
||||
<td class="num">
|
||||
<span class="{{ 'delta-add' if preview.net_sign == '+' else 'delta-del' }}">{{ preview.net_sign }}{{ preview.net }}개</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<form hx-post="/repos/{{ uuid }}/deploy" hx-target="#modal" hx-swap="innerHTML">
|
||||
<input type="hidden" name="version_href" value="{{ version_href }}">
|
||||
<label class="confirm-label">
|
||||
확인을 위해 저장소 이름 <strong class="mono">{{ repo_name }}</strong> 을(를) 입력하세요
|
||||
<input type="text" name="confirm_name" autocomplete="off" required class="mono"
|
||||
data-confirm="{{ repo_name }}"
|
||||
oninput="document.getElementById('deploy-confirm-btn').disabled = (this.value !== this.dataset.confirm)">
|
||||
</label>
|
||||
{# TODO(auth): 로그인 도입 시 배포 비밀번호 재확인 입력 추가 #}
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="ghost"
|
||||
onclick="document.getElementById('modal').innerHTML=''">취소</button>
|
||||
<button type="submit" id="deploy-confirm-btn" class="danger" disabled>배포 확정</button>
|
||||
<div class="modal-head">
|
||||
<span class="warn-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/></svg></span>
|
||||
<div>
|
||||
<h3>운영 배포 확인</h3>
|
||||
<span class="head-sub">되돌리기 어려운 작업입니다</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
{% if error %}
|
||||
<p class="error-note" role="alert">
|
||||
<svg class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/></svg>
|
||||
<span>{{ error }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="deploy-note" style="text-align:center">
|
||||
<span class="repo-hl">{{ repo_name }}</span> 저장소를 운영망에 배포합니다.
|
||||
운영 서버(yum/dnf)가 실제로 내려받는 버전이 바뀝니다.
|
||||
</p>
|
||||
|
||||
{% if preview %}
|
||||
<div class="deploy-preview">
|
||||
<div class="pv-box">
|
||||
<div class="pv-label">현재 (운영)</div>
|
||||
{% if preview.current_version is not none %}
|
||||
<div class="pv-ver">v{{ preview.current_version }}</div>
|
||||
<div class="pv-pkg">{{ preview.current_count }} pkgs</div>
|
||||
{% else %}
|
||||
<div class="pv-ver">없음</div>
|
||||
<div class="pv-pkg">—</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pv-arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></div>
|
||||
<div class="pv-box pv-target">
|
||||
<div class="pv-label">대상</div>
|
||||
<div class="pv-ver">v{{ preview.target_version }}</div>
|
||||
<div class="pv-pkg">{{ preview.target_count }} pkgs · <span class="{{ 'delta-add' if preview.net_sign == '+' else 'delta-del' }}">{{ preview.net_sign }}{{ preview.net }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form hx-post="/repos/{{ uuid }}/deploy" hx-target="#modal" hx-swap="innerHTML">
|
||||
<input type="hidden" name="version_href" value="{{ version_href }}">
|
||||
<label class="confirm-label">확인을 위해 저장소 이름 <span class="repo-hl">{{ repo_name }}</span> 을(를) 입력하세요.
|
||||
<div class="confirm-field" data-confirm="{{ repo_name }}">
|
||||
<input type="text" name="confirm_name" autocomplete="off" required class="mono"
|
||||
placeholder="{{ repo_name }}"
|
||||
oninput="var f=this.closest('.confirm-field');var v=this.value.trim();var ok=(v===f.dataset.confirm);f.classList.toggle('is-valid',ok&&v!=='');f.classList.toggle('is-invalid',!ok&&v!=='');document.getElementById('deploy-confirm-btn').disabled=!ok;">
|
||||
<svg class="confirm-state ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 13 4 4L19 7"/></svg>
|
||||
<svg class="confirm-state bad" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||||
</div>
|
||||
</label>
|
||||
{# TODO(auth): 로그인 도입 시 배포 비밀번호 재확인 입력 추가 #}
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="ghost"
|
||||
onclick="document.getElementById('modal').innerHTML=''">취소</button>
|
||||
<button type="submit" id="deploy-confirm-btn" class="danger" disabled>배포 확정</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
</dialog>
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
|
||||
<div id="flash" hx-swap-oob="true">
|
||||
<div class="flash-banner">
|
||||
<span class="flash-dot"></span>
|
||||
<span>배포 완료 · <strong class="mono">v{{ to_version }}</strong>{% if from_version is not none %} <span class="flash-sub">(이전 v{{ from_version }})</span>{% endif %}</span>
|
||||
<span class="flash-check"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 13 4 4L19 7"/></svg></span>
|
||||
<span>배포 완료 · <strong class="mono">v{{ to_version }}</strong></span>
|
||||
{% if from_version is not none %}<span class="flash-sub">이전 v{{ from_version }} · 되돌리기: 이전 버전 재배포</span>{% endif %}
|
||||
{% if not audit_ok %}<span class="flash-warn">감사 로그 기록 실패</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,14 +9,21 @@
|
||||
{% elif progress.idle %}
|
||||
{# 추적 중인 작업 없음 — 빈 조각 #}
|
||||
{% else %}
|
||||
<div class="sync-progress">
|
||||
{% if progress.total %}
|
||||
<progress value="{{ progress.done }}" max="{{ progress.total }}"></progress>
|
||||
<small class="num">{{ progress.done }}/{{ progress.total }} · {{ progress.percent }}%</small>
|
||||
{% else %}
|
||||
<progress></progress>
|
||||
<small>준비 중…</small>
|
||||
{% endif %}
|
||||
{% if progress.total %}
|
||||
<div class="sync-progress is-active">
|
||||
<div class="sync-head">
|
||||
<small>동기화 중</small>
|
||||
<small class="sync-pct">{{ progress.percent }}%</small>
|
||||
</div>
|
||||
<progress value="{{ progress.done }}" max="{{ progress.total }}"></progress>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="sync-progress is-active indeterminate">
|
||||
<div class="sync-head">
|
||||
<small>준비 중…</small>
|
||||
</div>
|
||||
<progress></progress>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -3,14 +3,39 @@
|
||||
<p role="alert" class="status-line status-bad">저장소 목록을 불러오지 못했습니다: {{ error }}</p>
|
||||
{% else %}
|
||||
<div class="summary">
|
||||
<div class="card"><div class="value">{{ summary.total }}</div><div class="label">전체 저장소</div></div>
|
||||
<div class="card"><div class="value{% if summary.syncing %} is-accent{% endif %}">{{ summary.syncing }}</div><div class="label">동기화 중</div></div>
|
||||
<div class="card"><div class="value is-success">{{ summary.gpg_pass }}</div><div class="label">GPG 검증 통과</div></div>
|
||||
<div class="card"><div class="value">{{ summary.total_packages }}</div><div class="label">총 패키지</div></div>
|
||||
<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" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg></span>
|
||||
</div>
|
||||
<div class="value is-accent">{{ summary.total }}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-top">
|
||||
<span class="label">동기화 중</span>
|
||||
<span class="card-ico"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.5 9a9 9 0 0 1 14.9-3.4L23 10M1 14l4.6 4.4A9 9 0 0 0 20.5 15"/></svg></span>
|
||||
</div>
|
||||
<div class="value">{{ summary.syncing }}</div>
|
||||
</div>
|
||||
<div class="card is-success">
|
||||
<div class="card-top">
|
||||
<span class="label">GPG 검증 통과</span>
|
||||
<span class="card-ico"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2 4 5v6c0 5 3.4 8.5 8 10 4.6-1.5 8-5 8-10V5z"/><path d="m9 12 2 2 4-4"/></svg></span>
|
||||
</div>
|
||||
<div class="value is-success">{{ summary.gpg_pass }}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-top">
|
||||
<span class="label">총 패키지</span>
|
||||
<span class="card-ico"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 16V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><path d="m3.3 7 8.7 5 8.7-5M12 22V12"/></svg></span>
|
||||
</div>
|
||||
<div class="value">{{ summary.total_packages }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<!-- 검색창은 표 바깥에 둬서 입력 중 포커스 유지. 정렬 상태([data-state])를 함께 전송 -->
|
||||
<svg class="search-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><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" name="q" value="{{ q }}"
|
||||
placeholder="저장소 이름 검색"
|
||||
hx-get="/repos/table" hx-target="#repo-table-wrap" hx-swap="outerHTML"
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
|
||||
{% macro sorth(col, label, extra="") %}
|
||||
<th class="{{ extra }}">
|
||||
<a href="#" class="th-sort{% if sort == col %} is-active{% endif %}"
|
||||
<a href="#" class="th-sort{% if sort == col %} is-active{% if dir == 'desc' %} desc{% endif %}{% endif %}"
|
||||
hx-get="/repos/table" hx-target="#repo-table-wrap" hx-swap="outerHTML"
|
||||
hx-include="#repo-search"
|
||||
hx-vals='{"sort": "{{ col }}", "dir": "{{ 'desc' if (sort == col and dir == 'asc') else 'asc' }}"}'>
|
||||
{{ label }}{% if sort == col %} <span class="sort-ind">{{ '↑' if dir == 'asc' else '↓' }}</span>{% endif %}
|
||||
{{ label }}{% if sort == col %} <span class="sort-ind">▲</span>{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
{% endmacro %}
|
||||
@@ -41,11 +41,14 @@
|
||||
<td class="mono">{{ repo.last_sync or "—" }}</td>
|
||||
<td>
|
||||
<div id="sync-{{ repo.uuid }}" class="row-action">
|
||||
<button class="secondary btn-sm"
|
||||
<button class="secondary btn-sm btn-icon"
|
||||
hx-post="/repos/{{ repo.uuid }}/sync"
|
||||
hx-target="#sync-{{ repo.uuid }}"
|
||||
hx-swap="innerHTML"
|
||||
hx-disabled-elt="this">동기화</button>
|
||||
hx-disabled-elt="this">
|
||||
<svg class="icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.5 9a9 9 0 0 1 14.9-3.4L23 10M1 14l4.6 4.4A9 9 0 0 0 20.5 15"/></svg>
|
||||
동기화
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,46 +1,27 @@
|
||||
<div id="version-list" class="table-card"{% if oob %} hx-swap-oob="true"{% endif %}>
|
||||
<div id="version-list" class="version-list"{% if oob %} hx-swap-oob="true"{% endif %}>
|
||||
{% if versions %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="num">버전</th>
|
||||
<th>생성일시</th>
|
||||
<th class="num">패키지</th>
|
||||
<th class="num">증감</th>
|
||||
<th>검증</th>
|
||||
<th>배포 상태</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for v in versions %}
|
||||
<tr>
|
||||
<td class="num">v{{ v.number }}</td>
|
||||
<td class="mono">{{ v.created or "—" }}</td>
|
||||
<td class="num">{{ v.package_count }}</td>
|
||||
<td class="num">
|
||||
{%- if v.added %}<span class="delta-add">+{{ v.added }}</span>{% endif -%}
|
||||
{%- if v.removed %} <span class="delta-del">−{{ v.removed }}</span>{% endif -%}
|
||||
{%- if not v.added and not v.removed %}—{% endif -%}
|
||||
</td>
|
||||
<td>{% if v.gpg %}<span class="badge badge-{{ v.gpg.level }}">{{ v.gpg.label }}</span>{% endif %}</td>
|
||||
<td>{% if v.is_deployed %}<span class="badge badge-deployed">운영 배포 중</span>{% endif %}</td>
|
||||
<td>
|
||||
{% if v.is_deployed %}
|
||||
{# 배포 상태 칸에 배지 표시 #}
|
||||
{% elif v.deployable %}
|
||||
<button class="secondary btn-sm"
|
||||
hx-get="/repos/{{ uuid }}/deploy/confirm?version_href={{ v.href | urlencode }}"
|
||||
hx-target="#modal" hx-swap="innerHTML">이 버전 배포</button>
|
||||
{% else %}
|
||||
<button class="secondary btn-sm" disabled title="검증을 통과하지 못해 배포할 수 없습니다">이 버전 배포</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% for v in versions %}
|
||||
<div class="version-item">
|
||||
<span class="ver-no">v{{ v.number }}</span>
|
||||
{% if v.is_deployed %}<span class="badge badge-deployed">운영 배포 중</span>{% endif %}
|
||||
{% if v.gpg %}<span class="badge badge-{{ v.gpg.level }}">{{ v.gpg.label }}</span>{% endif %}
|
||||
<span class="ver-meta">{{ v.created or "—" }} · {{ v.package_count }} pkgs
|
||||
{%- if v.added %} · <span class="delta-add">+{{ v.added }}</span>{% endif -%}
|
||||
{%- if v.removed %} <span class="delta-del">−{{ v.removed }}</span>{% endif -%}
|
||||
</span>
|
||||
<span class="spacer"></span>
|
||||
{% if v.is_deployed %}
|
||||
<button class="ghost btn-sm" disabled>현재 운영 버전</button>
|
||||
{% elif v.deployable %}
|
||||
<button class="secondary btn-sm"
|
||||
hx-get="/repos/{{ uuid }}/deploy/confirm?version_href={{ v.href | urlencode }}"
|
||||
hx-target="#modal" hx-swap="innerHTML">이 버전 배포</button>
|
||||
{% else %}
|
||||
<button class="secondary btn-sm" disabled title="검증을 통과하지 못해 배포할 수 없습니다">이 버전 배포</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="padding:16px;">버전이 없습니다. 먼저 동기화를 실행하세요.</p>
|
||||
<p class="empty-note">버전이 없습니다. 먼저 동기화를 실행하세요.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user