Files
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

426 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>폐쇄망 콘솔 — 재디자인 미리보기</title>
<!-- FOUC 방지 + 테마 복원 (실제 base.html 의 인라인 스크립트와 동일 동작) -->
<script>
(function () {
var t = localStorage.getItem("theme");
if (t) document.documentElement.setAttribute("data-theme", t);
})();
</script>
<link rel="stylesheet" href="pico-shim.css" />
<link rel="stylesheet" href="app.css" />
<style>
/* 미리보기 하니스 전용 (배포 CSS 아님) */
.demo-note {
max-width: 1040px;
margin: 28px auto 0;
padding: 0 20px;
}
.demo-note .callout {
background: var(--surface-2);
color: var(--accent-hover);
border-radius: 12px;
padding: 14px 18px;
font-size: 14px;
line-height: 1.6;
}
.state-grid { display: grid; gap: 18px; }
hr.sep { border: none; border-top: 1px solid var(--border); margin: 0 0 28px; }
</style>
</head>
<body>
<!-- 데모 배너 -->
<div class="demo-banner">
<span class="demo-dot"></span>
<span><strong>데모 모드</strong> · 가짜 저장소 데이터입니다. 실제 Pulp 서버와 연동되어 있지 않습니다.</span>
</div>
<!-- 상단 내비 -->
<nav class="app-nav">
<div class="container">
<p class="brand"><span class="brand-mark">P</span>Pulp <span class="accent">Console</span></p>
<button class="theme-toggle" id="theme-toggle" aria-label="테마 전환">
<svg class="icon moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
<svg class="icon sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
</div>
</nav>
<div class="demo-note">
<div class="callout">
<b>재디자인 미리보기 (확정안).</b> 요약 카드 = 화이트+호버 리프트, 저장소 표 = 제브라, 배포 모달 = 중앙 경고형.
동기화 버튼은 흰 배경·파란 글씨로, 새로고침(중립 아웃라인)과 구분됩니다. 우상단 아이콘으로 라이트·다크를 전환해 보세요.
</div>
</div>
<main class="container">
<!-- ═══════════ 대시보드 ═══════════ -->
<section>
<div class="status-row" style="margin-bottom:24px">
<span class="status-label">연결 상태</span>
<span class="status-line status-ok">Pulp 서버 연결됨</span>
<span class="status-line status-demo">데모 데이터</span>
</div>
<div class="summary">
<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"><path d="M4 7h16M4 12h16M4 17h16"/></svg></span>
</div>
<div class="value is-accent">18</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"><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">2</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"><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">16</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"><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">48,120</div>
</div>
</div>
<div class="section-head">
<h2>저장소</h2>
<button class="ghost btn-sm btn-icon" id="refresh-btn">
<svg class="icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
<div class="list-toolbar">
<svg class="search-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input class="repo-search" type="search" placeholder="저장소 이름으로 검색…" />
</div>
<div class="table-card" id="repo-table-wrap">
<table>
<thead>
<tr>
<th><a class="th-sort is-active" href="#">저장소 <span class="sort-ind"></span></a></th>
<th>검증</th>
<th><a class="th-sort" href="#">최신 버전</a></th>
<th class="num"><a class="th-sort" href="#">패키지</a></th>
<th><a class="th-sort" href="#">마지막 동기화</a></th>
<th></th>
</tr>
</thead>
<tbody id="repo-list">
<tr>
<td class="repo-name"><a href="#versions">rhel8-baseos</a></td>
<td><span class="badge badge-pass">GPG 통과</span></td>
<td class="mono">v142</td>
<td class="num">9,204</td>
<td class="mono">2026-07-09 04:12</td>
<td>
<div class="row-action">
<button class="secondary btn-sm btn-icon" onclick="document.getElementById('modal').showModal()">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
<tr>
<td class="repo-name"><a href="#versions">rhel8-appstream</a></td>
<td><span class="badge badge-pass">GPG 통과</span></td>
<td class="mono">v98</td>
<td class="num">12,880</td>
<td class="mono">동기화 중…</td>
<td>
<div class="row-action">
<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>
</div>
</td>
</tr>
<tr>
<td class="repo-name"><a href="#versions">ubuntu2204-main</a></td>
<td><span class="badge badge-warn">서명 확인 필요</span></td>
<td class="mono">v51</td>
<td class="num">6,412</td>
<td class="mono">2026-07-08 23:40</td>
<td>
<div class="row-action">
<button class="secondary btn-sm btn-icon" onclick="document.getElementById('modal').showModal()">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
<tr>
<td class="repo-name"><a href="#versions">epel8-extras</a></td>
<td><span class="badge badge-deployed">운영 배포됨</span></td>
<td class="mono">v220</td>
<td class="num">3,051</td>
<td class="mono">2026-07-09 01:05</td>
<td>
<div class="row-action">
<button class="secondary btn-sm btn-icon" onclick="document.getElementById('modal').showModal()">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
<tr>
<td class="repo-name"><a href="#versions">centos-stream9</a></td>
<td><span class="badge badge-unset">미검증</span></td>
<td class="mono">v7</td>
<td class="num">1,120</td>
<td class="mono">2026-07-07 18:22</td>
<td>
<div class="row-action">
<button class="secondary btn-sm btn-icon" onclick="document.getElementById('modal').showModal()">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
</tbody>
</table>
<div class="pager">
<button class="ghost btn-sm" disabled>이전</button>
<span class="mono">15 / 18</span>
<button class="ghost btn-sm">다음</button>
</div>
</div>
</section>
<hr class="sep" />
<!-- ═══════════ 상태 · 인터랙션 데모 ═══════════ -->
<section>
<div class="section-head"><h2>상태 · 인터랙션 데모</h2></div>
<div class="state-grid">
<div class="flash-banner">
<span class="flash-check"><svg viewBox="0 0 24 24"><path d="m5 13 4 4L19 7"/></svg></span>
<span>운영 배포 완료 · <span class="mono">rhel8-baseos v142</span></span>
<span class="flash-sub">3,204개 패키지 반영</span>
<span class="flash-warn">되돌리기: 이전 버전 재배포</span>
</div>
<div class="table-card" style="padding:20px">
<div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap">
<button class="secondary" id="sync-demo-btn">동기화 시작 (진행 애니메이션)</button>
<div class="sync-progress" id="sync-demo" style="width:220px;display:none">
<div class="sync-head"><small id="sync-demo-label">준비 중</small><small class="sync-pct" id="sync-demo-pct">0%</small></div>
<progress id="sync-demo-bar" value="0" max="100"></progress>
</div>
<button class="secondary" style="width:auto" id="loading-demo">로딩 상태</button>
</div>
</div>
<div class="table-card">
<div class="empty-note">
<span class="empty-ico"><svg class="icon" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg></span>
<div class="empty-title">검색 결과가 없습니다</div>
<div class="empty-sub"><span class="mono">"nginx"</span> 와 일치하는 저장소가 없어요. 다른 키워드로 검색해 보세요.</div>
</div>
</div>
<div class="error-note">
<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"><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>Pulp 서버에 연결할 수 없습니다. 네트워크 및 서비스 상태를 확인하세요.</span>
</div>
<div class="table-card" style="padding:18px">
<div style="display:flex;flex-direction:column;gap:14px">
<div class="skeleton skeleton-row" style="width:40%"></div>
<div class="skeleton skeleton-row" style="width:70%"></div>
<div class="skeleton skeleton-row" style="width:55%"></div>
</div>
</div>
</div>
</section>
<hr class="sep" />
<!-- ═══════════ 버전 목록 (저장소 상세 — 저장소 이름 클릭 시 화면) ═══════════ -->
<section id="versions">
<a href="#" role="button" class="ghost btn-sm btn-icon back-btn">
<svg class="icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
대시보드
</a>
<div class="section-head">
<h2 class="mono">rhel8-baseos</h2>
<button class="secondary btn-sm btn-icon" onclick="document.getElementById('modal').showModal()">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
<div class="verify-detail">
<span class="badge badge-pass">GPG 통과</span>
<small>서명 검증 통과 · 2026-07-09 04:12 · 키 0x3A9F&hellip;C21</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">2026-07-08 22:03 · 9,180 pkgs · <span class="delta-add">+40</span> <span class="delta-del">16</span></span>
<span class="spacer"></span>
<button class="secondary btn-sm" onclick="document.getElementById('modal').showModal()">이 버전 배포</button>
</div>
<div class="version-item">
<span class="ver-no">v140</span>
<span class="ver-meta">2026-07-08 09:15 · 9,001 pkgs · <span class="delta-add">+112</span></span>
<span class="spacer"></span>
<button class="secondary btn-sm" onclick="document.getElementById('modal').showModal()">이 버전 배포</button>
</div>
<div class="version-item">
<span class="ver-no">v139</span>
<span class="ver-meta">2026-07-07 20:41 · 8,889 pkgs</span>
<span class="spacer"></span>
<button class="secondary btn-sm" onclick="document.getElementById('modal').showModal()">이 버전 배포</button>
</div>
</div>
</section>
</main>
<!-- ═══════════ 배포 확인 모달 (중앙 경고형) ═══════════ -->
<dialog id="modal">
<article class="deploy-modal">
<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"><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>
</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 viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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">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" />
<svg class="confirm-state ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><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"><path d="M18 6 6 18M6 6l12 12"/></svg>
</div>
</label>
<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>
<script>
// 테마 토글 (실제 base.html 동작과 동일)
document.getElementById("theme-toggle").addEventListener("click", function () {
var el = document.documentElement;
var next = el.getAttribute("data-theme") === "dark" ? "light" : "dark";
el.setAttribute("data-theme", next);
localStorage.setItem("theme", next);
});
// 타입-투-컨펌: 입력값이 data-confirm 과 일치하면 확정 버튼 활성 + 시각 상태
(function () {
var field = document.querySelector("#modal .confirm-field");
var input = field.querySelector("input");
var target = field.getAttribute("data-confirm");
var btn = document.getElementById("deploy-confirm-btn");
input.addEventListener("input", function () {
var v = input.value.trim();
field.classList.remove("is-valid", "is-invalid");
if (v === "") { btn.disabled = true; return; }
if (v === target) {
field.classList.add("is-valid");
btn.disabled = false;
} else {
field.classList.add("is-invalid");
btn.disabled = true;
}
});
btn.addEventListener("click", function () {
if (btn.disabled) return;
btn.classList.add("is-loading");
btn.textContent = "배포 중";
setTimeout(function () {
btn.closest("dialog").close();
btn.classList.remove("is-loading");
btn.textContent = "운영에 배포";
btn.disabled = true;
field.classList.remove("is-valid", "is-invalid");
input.value = "";
}, 1400);
});
})();
// 진행바 데모
document.getElementById("sync-demo-btn").addEventListener("click", function () {
var wrap = document.getElementById("sync-demo");
var bar = document.getElementById("sync-demo-bar");
var pct = document.getElementById("sync-demo-pct");
var label = document.getElementById("sync-demo-label");
wrap.style.display = "flex";
wrap.classList.add("indeterminate");
label.textContent = "연결 중…";
var v = 0;
setTimeout(function () {
wrap.classList.remove("indeterminate");
label.textContent = "패키지 내려받는 중";
var t = setInterval(function () {
v += Math.max(1, Math.round((100 - v) * 0.12));
if (v >= 100) { v = 100; clearInterval(t); label.textContent = "완료"; }
bar.value = v;
pct.textContent = v + "%";
}, 240);
}, 900);
});
// 로딩 버튼 데모
document.getElementById("loading-demo").addEventListener("click", function () {
var b = this;
b.classList.add("is-loading");
setTimeout(function () { b.classList.remove("is-loading"); }, 1600);
});
</script>
</body>
</html>