docs: add project spec, build plan, and contributor/design guidance
- pulp-console-spec.md: 실행 스펙 (확정 스택, Pulp API, 안전 모델) - PLAN.md: 단계별 구현 계획 (체크박스 진행 추적) - CLAUDE.md: future Claude 세션용 아키텍처/제약 안내 - ref/design-ref.md: Toss 스타일 디자인 레퍼런스 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
115
CLAUDE.md
Normal file
115
CLAUDE.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What this is
|
||||
|
||||
A thin, self-hosted **web admin console for Pulp 3**, a Linux patch-repository
|
||||
server. Pulp has a full REST API (`/pulp/api/v3/`) but no usable official web UI,
|
||||
so this project builds a focused operator console on top of that API.
|
||||
|
||||
Context: it runs in an **air-gapped (closed) network** at the Bank of Korea IT
|
||||
Strategy Dept. cloud team. The console lets operators mirror, snapshot, verify,
|
||||
and deploy RPM/DEB patch repositories by clicking instead of running CLI commands.
|
||||
|
||||
The full implementation spec is **`pulp-console-spec.md`** (written in Korean,
|
||||
addressed to the implementing AI). It is the source of truth — read it before
|
||||
making design decisions. The codebase itself does not yet exist; the spec
|
||||
describes what to build and in what order.
|
||||
|
||||
## Stack (decided — do not substitute)
|
||||
|
||||
- **Backend / BFF:** FastAPI (Python), using `httpx` to call Pulp.
|
||||
- **Frontend:** HTMX + Jinja2 templates. **No build step.** Buttons trigger
|
||||
partial-HTML swaps; progress polling is done with `hx-trigger="every 3s"`.
|
||||
- **Styling:** Pico.css (preferred for air-gap simplicity) or Tailwind standalone.
|
||||
|
||||
## Hard constraints (these are the point of the project)
|
||||
|
||||
- **The browser never calls the Pulp API directly.** All traffic goes through
|
||||
the FastAPI BFF, which holds Pulp Basic Auth credentials and proxies requests.
|
||||
This protects credentials and avoids CORS exposure.
|
||||
- **No heavy frontend tooling** (React, Vite, npm build chains). HTML + HTMX only.
|
||||
- **Air-gap first:** minimize CDN dependence. Bundle CSS / HTMX JS under
|
||||
`static/` so the app runs with no internet access.
|
||||
- **View routes return HTML** (full pages or fragments), **not JSON** — HTMX
|
||||
consumes HTML.
|
||||
- **Self-signed CA support from day one:** the `httpx` client must accept a
|
||||
`verify=PULP_CA_FILE` option for the internal TLS CA (boknet CA family).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser (HTMX + Jinja) ──form/button──► FastAPI BFF ──Basic Auth──► Pulp REST API (/pulp/api/v3/)
|
||||
◄──── partial HTML ──── ◄──────── JSON ────────
|
||||
```
|
||||
|
||||
The BFF receives Pulp JSON, renders it into Jinja partial templates, and returns
|
||||
HTML fragments that HTMX swaps into the page.
|
||||
|
||||
### Pulp object model (essential to understand the flow)
|
||||
|
||||
`Remote (source def) → Repository (container) → RepositoryVersion (snapshot) →
|
||||
Publication (deployable form) → Distribution (public URL)`
|
||||
|
||||
The URL operators point `yum/dnf` `baseurl` at is the **Distribution**.
|
||||
|
||||
Two operations matter:
|
||||
1. **Sync** (`POST .../repositories/rpm/rpm/{uuid}/sync/`): pulls from an external
|
||||
mirror and creates a new RepositoryVersion snapshot. Relatively safe — does
|
||||
not change what production servers receive. Returns an async **task**.
|
||||
2. **Deploy** (create Publication → `PATCH` the Distribution's `publication`):
|
||||
**dangerous** — changes the version production actually fetches.
|
||||
|
||||
Pulp objects reference each other by `pulp_href` (a path string); **use href, not
|
||||
name, in code.** Sync and deploy return async **tasks** — capture `task_href` and
|
||||
poll `GET {task_href}` (`state`: waiting/running/completed/failed) for completion.
|
||||
RPM paths use `rpm/rpm`; the DEB equivalent swaps that segment for `deb/apt`.
|
||||
|
||||
## Safety model (deploy is gated)
|
||||
|
||||
- Deploy must go through a **confirmation modal** (`hx-confirm` or an explicit step).
|
||||
- The deploy button is **disabled for versions that aren't verified** (GPG /
|
||||
checksum passed). Verification comes from the Remote's `gpgkey`/`tls_validation`
|
||||
and the Repository `repo_config` (`gpgcheck: 1`, `repo-gpgcheck: 1`) — this is
|
||||
the basis for the "verified ✅" badge.
|
||||
- Every deploy is written to an **audit log** (who / when / which repo / which version).
|
||||
|
||||
## Planned route surface (BFF)
|
||||
|
||||
```
|
||||
GET / dashboard (full page)
|
||||
GET /repos repo list fragment
|
||||
POST /repos/{uuid}/sync start sync, return progress-bar fragment
|
||||
GET /repos/{uuid}/progress progress-bar fragment (polled every 3s)
|
||||
GET /repos/{uuid}/versions version list fragment/page
|
||||
POST /repos/{uuid}/deploy confirm deploy (publication + distribution), result fragment
|
||||
GET /healthz proxy Pulp status
|
||||
```
|
||||
|
||||
Pulp access is centralized in a `pulp_client.py` helper (`get`/`post`/`patch`
|
||||
with Basic Auth, `list_repos`, `sync_repo`, `list_versions`,
|
||||
`create_publication`, `update_distribution`, single task fetch — the *page* does
|
||||
the polling, not the client).
|
||||
|
||||
## Configuration (env vars)
|
||||
|
||||
`PULP_BASE_URL`, `PULP_USERNAME`, `PULP_PASSWORD` (secret), `PULP_VERIFY_TLS`,
|
||||
`PULP_CA_FILE` (path to internal CA pem).
|
||||
|
||||
## Build order (from the spec — build in working-screen increments)
|
||||
|
||||
1. Skeleton: FastAPI + Jinja2 + httpx, `pulp_client.py` stub, `/healthz` hitting Pulp status.
|
||||
2. Dashboard (read-only repo list/cards).
|
||||
3. Sync: `POST` sync → task_href → polling progress fragment.
|
||||
4. Version list + verification badges.
|
||||
5. Deploy: confirm modal → create publication → PATCH distribution → poll → audit log.
|
||||
6. Styling, bundle static assets for air-gap, containerize.
|
||||
|
||||
Each step should produce a working screen. Do not build everything at once.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Python 3.14 is installed locally. Once code exists, prefer a virtualenv and
|
||||
pin dependencies (`requirements.txt` / lockfile) so they can be carried into
|
||||
the air-gapped network via pip.
|
||||
173
PLAN.md
Normal file
173
PLAN.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# PLAN.md — Pulp 패치 관리 콘솔 구현 계획
|
||||
|
||||
`pulp-console-spec.md`를 근거로 한 단계별 작업 계획. 각 단계는 **"동작하는 화면/엔드포인트"가
|
||||
나오는 단위**로 끊는다. 한 번에 다 만들지 않는다. 각 단계 끝에 검증 방법과 커밋 포인트를 둔다.
|
||||
|
||||
원칙(스펙 §2, §5 재확인):
|
||||
- 브라우저는 Pulp API를 직접 호출하지 않는다. 모든 호출은 FastAPI(BFF) 경유.
|
||||
- 프론트 빌드 도구(React/Vite) 금지. HTMX + Jinja2만.
|
||||
- 정적 자산(Pico.css, htmx.min.js)은 `static/`에 동봉 — CDN 의존 금지(폐쇄망).
|
||||
- 화면용 라우트는 JSON이 아니라 **HTML(전체 페이지 또는 조각)** 을 반환.
|
||||
- 위험 동작(배포 확정)은 확인 + 검증 게이트 + 감사 로그.
|
||||
|
||||
---
|
||||
|
||||
## [x] 단계 0 — 프로젝트 골격 + 설정 + 헬스체크
|
||||
|
||||
**목표:** 서버가 뜨고 `/healthz`가 Pulp `status`를 프록시해 "연결됨/끊김"을 보여준다.
|
||||
|
||||
작업:
|
||||
1. 의존성 고정: `requirements.txt` (`fastapi`, `uvicorn[standard]`, `httpx`, `jinja2`,
|
||||
`python-multipart`, `pydantic-settings`). 폐쇄망 반입 위해 버전 핀.
|
||||
2. 디렉터리 구조 생성:
|
||||
```
|
||||
app/
|
||||
main.py # FastAPI 인스턴스, 라우터 등록, 정적/템플릿 마운트
|
||||
config.py # pydantic-settings로 환경변수 로드 (§8)
|
||||
pulp_client.py # Pulp 호출 래퍼 (스텁)
|
||||
routes/
|
||||
__init__.py
|
||||
dashboard.py
|
||||
templates/
|
||||
base.html
|
||||
partials/
|
||||
static/
|
||||
pico.min.css
|
||||
htmx.min.js
|
||||
tests/
|
||||
.env.example
|
||||
```
|
||||
3. `config.py`: `PULP_BASE_URL`, `PULP_USERNAME`, `PULP_PASSWORD`, `PULP_VERIFY_TLS`,
|
||||
`PULP_CA_FILE` 로드. `httpx` 클라이언트 생성 시 `verify=` 에 `PULP_CA_FILE`(있으면 경로,
|
||||
없고 VERIFY_TLS=false면 False) 적용 — 사내 self-signed CA 대응을 **처음부터** 넣는다.
|
||||
4. `pulp_client.py` 골격: `get/post/patch` (Basic Auth 포함, base_url 결합), `status()`.
|
||||
5. `GET /healthz`: `pulp_client.status()` 호출 결과를 JSON 또는 작은 HTML 배지로 반환.
|
||||
6. `.env.example` 작성. 실제 `.env`는 커밋 금지(`.gitignore`).
|
||||
|
||||
**검증:** `uvicorn app.main:app --reload` → `/healthz`가 Pulp 연결 상태 반영.
|
||||
(Pulp 미연결 환경이면 httpx 예외를 잡아 "연결 실패" 표시까지 확인.)
|
||||
|
||||
---
|
||||
|
||||
## [x] 단계 1 — 대시보드 (화면 1, 읽기 전용)
|
||||
|
||||
**목표:** 저장소 목록을 표/카드로 렌더. 조작 버튼은 아직 동작 안 해도 됨(자리만).
|
||||
|
||||
작업:
|
||||
1. `pulp_client.list_repos()` — `GET /pulp/api/v3/repositories/rpm/rpm/` 결과 파싱.
|
||||
2. 각 저장소에 대해 표시용 모델로 가공: 이름, 현재 운영 배포 버전(vN), 마지막 동기화 시각,
|
||||
패키지 수, GPG 검증 배지 근거(`repo_config`의 `gpgcheck`/`repo-gpgcheck`).
|
||||
- 현재 배포 버전은 Distribution → publication → repository_version 역추적이 필요할 수 있음.
|
||||
이 단계에선 가능한 범위까지만 표시하고 TODO 주석으로 남긴다.
|
||||
3. `GET /` → `base.html` + 대시보드 본문(전체 페이지).
|
||||
4. `GET /repos` → 저장소 목록 **조각**(새로고침/폴링용 partial). `/`는 이 조각을 include.
|
||||
5. 상단 요약 카드: 전체 저장소 수 / 동기화 중 수 / GPG 통과 수 / 총 패키지 수.
|
||||
6. 검증 배지 컴포넌트: ✅ 통과 / ⚠️ 경고 / ⚪ 미설정 3단계(화면 4 로직을 여기서 함수로).
|
||||
|
||||
**검증:** `/`에서 실제 저장소 목록과 배지가 보인다. `/repos` 조각만 단독 요청해도 렌더된다.
|
||||
|
||||
---
|
||||
|
||||
## [ ] 단계 2 — 동기화 실행 + 진행률 폴링 (화면 2)
|
||||
|
||||
**목표:** [동기화] 클릭 → sync task 시작 → 3초 폴링으로 진행률 갱신 → 완료/실패 표시.
|
||||
|
||||
작업:
|
||||
1. `pulp_client.sync_repo(uuid, remote_href)` — `POST .../repositories/rpm/rpm/{uuid}/sync/`
|
||||
body `{"remote": remote_href}` → 반환된 `task_href` 보관.
|
||||
- remote_href는 저장소에 연결된 remote를 조회해 결정(또는 repo 객체의 `remote` 필드).
|
||||
2. `POST /repos/{uuid}/sync` → sync 시작 후 **진행률 바 조각** 반환.
|
||||
이 조각은 `hx-get="/repos/{uuid}/progress" hx-trigger="every 3s"` 를 포함.
|
||||
- task_href를 클라이언트로 어떻게 넘길지: 진행률 조각/폴링 URL에 task_href를 쿼리로 싣거나,
|
||||
서버측에 `{uuid: task_href}` 임시 매핑(메모리 dict)으로 보관. 폐쇄망 단일 인스턴스이므로
|
||||
메모리 매핑으로 시작하고, 다중 워커 시 한계는 주석으로 명시.
|
||||
3. `pulp_client.get_task(task_href)` — `state`(waiting/running/completed/failed),
|
||||
`progress_reports[]`(done/total) 파싱.
|
||||
4. `GET /repos/{uuid}/progress` → 진행률 바 조각:
|
||||
- running: `n/m 패키지, NN%` + 진행률 바, 계속 폴링.
|
||||
- completed: "동기화 완료, 새 버전 vN 생성됨" 배지로 교체(폴링 중단 — `hx-trigger` 제거).
|
||||
- failed: 빨간 에러 + task의 `error` 필드 사유.
|
||||
5. 동기화는 비교적 안전(스펙 §5-1) → 확인 모달 없이 바로 실행 허용.
|
||||
|
||||
**검증:** 실제 sync 실행 시 진행률이 3초마다 갱신되고, 완료 시 새 버전 안내가 뜬다.
|
||||
폴링 종료(완료/실패 후 더 이상 요청 안 감) 확인.
|
||||
|
||||
---
|
||||
|
||||
## [ ] 단계 3 — 버전(스냅샷) 목록 + 검증 배지 (화면 3 전반)
|
||||
|
||||
**목표:** 저장소별 RepositoryVersion 목록을 최신순으로, 검증/배포 상태와 함께 표시.
|
||||
|
||||
작업:
|
||||
1. `pulp_client.list_versions(uuid)` — `GET .../repositories/rpm/rpm/{uuid}/versions/`.
|
||||
2. `GET /repos/{uuid}/versions` → 버전 목록 조각/페이지:
|
||||
- 각 버전: vN, 생성일시, 패키지 수(가능하면 증감), 검증 상태, **현재 운영 배포 여부 배지**.
|
||||
- 현재 배포 버전 판별: Distribution의 publication → repository_version 과 비교.
|
||||
3. 검증 상태(화면 4): GPG 서명, checksum 타입(sha256 등), `repo_config.gpgcheck` 값을
|
||||
✅/⚠️/⚪ 로. 단계 1의 배지 함수를 재사용.
|
||||
4. 배포 버튼 자리만 둔다(다음 단계에서 활성화 로직 연결).
|
||||
|
||||
**검증:** 한 저장소의 버전들이 최신순으로, 검증/배포 배지와 함께 보인다.
|
||||
|
||||
---
|
||||
|
||||
## [ ] 단계 4 — 배포 확정 + 감사 로그 (화면 3 후반, 위험 동작)
|
||||
|
||||
**목표:** 검증 완료 + 미배포 버전만 배포 가능. 확인 모달 → publication 생성 →
|
||||
distribution PATCH → task 폴링 → 배지 갱신 → 감사 로그 기록.
|
||||
|
||||
작업:
|
||||
1. 배포 버튼 활성화 게이트(스펙 §5-2):
|
||||
- "검증 완료(GPG/checksum 통과)"가 아닌 버전 → 버튼 **비활성화**.
|
||||
- 이미 현재 배포 중인 버전 → 버튼 숨김/비활성.
|
||||
2. 확인 단계: `hx-confirm` 또는 별도 확인 모달 조각("이 버전을 운영망에 배포합니다").
|
||||
3. `POST /repos/{uuid}/deploy` body: 선택한 `version_href`:
|
||||
- `pulp_client.create_publication(version_href)` → `POST /publications/rpm/rpm/`
|
||||
body `{"repository_version": version_href}` → task → 폴링으로 publication_href 확보.
|
||||
- `pulp_client.update_distribution(dist_href, publication_href)` →
|
||||
`PATCH {dist_href}` body `{"publication": publication_href}` → task 폴링.
|
||||
- 대상 distribution_href는 저장소에 매핑되는 distribution을 조회해 결정.
|
||||
4. 완료 시: 결과 조각 반환, 해당 버전에 "현재 운영 배포" 배지 갱신.
|
||||
5. **감사 로그**(스펙 §5-2): 누가 / 언제 / 어떤 repo / 어떤 버전 → 으로 배포.
|
||||
- 최소 구현: append-only 파일(JSONL) 또는 SQLite. 시각은 서버 시각.
|
||||
- "누가"는 인증 도입 전까지 placeholder(예: 단일 운영자) — §5-3 권한 분리 시 확장.
|
||||
|
||||
**검증:** 미검증 버전은 배포 버튼이 막혀 있다. 검증된 버전을 확인 모달 거쳐 배포하면
|
||||
운영 배포 배지가 그 버전으로 옮겨가고, 감사 로그에 1줄이 남는다.
|
||||
|
||||
---
|
||||
|
||||
## [ ] 단계 5 — 스타일 마감 + 폐쇄망 패키징
|
||||
|
||||
**목표:** 운영툴로서 보기 좋고, 인터넷 없이 그대로 구동/반입 가능.
|
||||
|
||||
작업:
|
||||
1. Pico.css(권장) 또는 Tailwind standalone으로 표·버튼·배지·진행률 바 정돈.
|
||||
모든 CSS/JS는 `static/`에 동봉, 템플릿은 로컬 경로만 참조(CDN 링크 0개 확인).
|
||||
2. 컨테이너화: `Dockerfile`(또는 단순 venv + 실행 스크립트). 의존성은 사전 다운로드한
|
||||
wheel/오프라인 인덱스로 설치 가능하게(litellm 반입 방식 참고, 스펙 §9).
|
||||
3. 실행 문서화: `README` 또는 CLAUDE.md "Conventions"에 기동 명령/환경변수 정리.
|
||||
4. (선택, §5-3) 읽기전용 / 배포권한 계정 분리 여지를 남긴 인증 훅 위치 표시.
|
||||
|
||||
**검증:** 네트워크 차단 상태에서 컨테이너/venv만으로 앱이 뜨고 모든 정적 자산이 로드된다.
|
||||
|
||||
---
|
||||
|
||||
## 교차 관심사 (전 단계 공통)
|
||||
|
||||
- **에러 처리:** httpx 타임아웃/연결 실패/4xx·5xx를 잡아 화면에 사람이 읽을 수 있는
|
||||
메시지로. Pulp task `failed` 시 `error` 필드 노출.
|
||||
- **href 기준:** Pulp 객체는 이름이 아니라 `pulp_href`로 참조(스펙 §4). 코드 전반 href 우선.
|
||||
- **비동기 task:** sync/deploy는 task 반환 → 반드시 폴링. 폴링 주체는 **화면(HTMX)**,
|
||||
`pulp_client`는 단발 조회만(스펙 §7).
|
||||
- **DEB 지원:** RPM 경로 `rpm/rpm` 자리를 `deb/apt`로. 1차 MVP는 RPM에 집중하되,
|
||||
`pulp_client`에서 content-type을 파라미터화해 확장 여지를 둔다.
|
||||
- **테스트:** `pulp_client`는 httpx mock(`respx` 등)으로 단위 테스트. 라우트는
|
||||
`TestClient`로 조각 HTML에 기대 요소(배지/진행률/버튼 비활성)가 있는지 검사.
|
||||
|
||||
## 권장 진행 순서 요약
|
||||
|
||||
0 골격·헬스체크 → 1 대시보드(읽기) → 2 동기화·폴링 → 3 버전목록·검증배지 →
|
||||
4 배포확정·감사로그 → 5 스타일·폐쇄망 패키징.
|
||||
|
||||
각 단계 완료 시 동작 확인 후 커밋. 단계 4 전까지는 운영망에 영향 없는 안전 구간.
|
||||
203
pulp-console-spec.md
Normal file
203
pulp-console-spec.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# Pulp 패치 관리 콘솔 — 개발 스펙 (Claude Code용)
|
||||
|
||||
> 이 문서는 Claude Code 세션에 그대로 넣어 개발을 시작하기 위한 실행 스펙이다.
|
||||
> 읽는 대상은 "사람"이 아니라 "코드를 짜는 AI"다. 따라서 모호한 표현 대신
|
||||
> 구체적인 동작·경로·데이터 형태를 명시한다.
|
||||
|
||||
---
|
||||
|
||||
## 0. 한 줄 정의
|
||||
|
||||
공식 관리 UI가 없는 Pulp 3을, 운영자가 명령어 없이 클릭만으로 다룰 수 있게 해주는
|
||||
**사내 폐쇄망용 리눅스 패치 저장소 관리 콘솔**.
|
||||
|
||||
핵심 가치: **"검증이 끝난 특정 버전(스냅샷)만 운영 서버에 배포되도록 사람이 통제하는 화면."**
|
||||
|
||||
---
|
||||
|
||||
## 1. 배경 / 우리 팀 컨텍스트
|
||||
|
||||
- 한국은행 IT전략국 클라우드팀. 폐쇄망(air-gapped) 환경, 퍼블릭 클라우드 MSP 미사용.
|
||||
- 인터넷/내부 서버관리망에 Pulp 기반 리눅스 패치 Repo 서버를 구축하는 인프라 개혁의 일부.
|
||||
- Pulp는 외부 미러(RHEL/CentOS/Rocky/Ubuntu)에서 패키지를 미러링하고, GPG 서명·checksum으로
|
||||
변조를 판별하며, repository version(스냅샷)으로 버전을 고정·배포하는 백엔드 역할을 이미 수행한다.
|
||||
- 문제: **Pulp에는 쓸 만한 공식 웹 UI가 없다.** 커뮤니티 `pulp-ui`는 사실상 방치 상태.
|
||||
반면 REST API(`/pulp/api/v3/`)는 전부 공개되어 있고 OpenAPI 스키마로 문서화되어 있다.
|
||||
- 따라서 우리 요구에 딱 맞는 얇은 관리 콘솔을 자체 제작한다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 기술 스택 (확정)
|
||||
|
||||
| 영역 | 선택 | 이유 |
|
||||
|---|---|---|
|
||||
| 백엔드/BFF | **FastAPI (Python)** | Pulp 인증을 서버가 쥐고 프록시. 폐쇄망 반입 간단(pip). |
|
||||
| 화면 | **HTMX + Jinja2 템플릿** | 빌드 단계 없음. "버튼→부분 갱신", "주기적 폴링"이 HTML 속성만으로 됨. |
|
||||
| 스타일 | **Tailwind CSS (CDN/standalone) 또는 Pico.css** | 운영툴은 깔끔한 표·버튼·상태표시면 충분. 폐쇄망이면 Pico.css가 더 편함. |
|
||||
| 비동기 작업 추적 | HTMX `hx-trigger="every 3s"` 폴링 | Pulp의 task를 주기적으로 조회해 진행률 갱신. |
|
||||
|
||||
**금지/주의:**
|
||||
- 브라우저가 Pulp API를 직접 호출하지 않는다. 반드시 FastAPI를 경유한다(인증정보·CORS 보호).
|
||||
- React/Vite 등 무거운 프론트 빌드 도구 사용하지 않는다.
|
||||
- 폐쇄망 반입을 전제로, CDN 의존은 최소화하고 정적 자산은 로컬에 둘 수 있게 한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 아키텍처
|
||||
|
||||
```
|
||||
[브라우저: HTMX + Jinja 템플릿]
|
||||
│ (HTTPS, 폼/버튼 → 부분 HTML 응답)
|
||||
▼
|
||||
[FastAPI BFF]
|
||||
- Pulp 인증정보(Basic Auth) 보관
|
||||
- Pulp REST API 호출 → 결과를 가공 → HTML 조각 렌더
|
||||
- 위험한 동작(배포 확정)에 확인·권한·로깅 적용
|
||||
│ (HTTP, Basic Auth)
|
||||
▼
|
||||
[Pulp REST API /pulp/api/v3/]
|
||||
```
|
||||
|
||||
- BFF는 Pulp API 응답(JSON)을 받아 Jinja 부분 템플릿(HTML 조각)으로 렌더해서 돌려준다.
|
||||
HTMX가 그 조각을 화면 일부에 끼워넣는다.
|
||||
|
||||
---
|
||||
|
||||
## 4. Pulp API — 알아야 할 핵심 (실제 경로)
|
||||
|
||||
Pulp의 객체 모델: **Remote(소스 정의) → Repository(그릇) → RepositoryVersion(스냅샷) →
|
||||
Publication(배포가능 형태) → Distribution(공개 URL)**.
|
||||
|
||||
운영자가 `yum/dnf`의 `baseurl`로 바라보는 주소가 바로 Distribution이다.
|
||||
|
||||
자주 쓰는 엔드포인트(RPM 기준; deb는 `rpm/rpm` 자리를 `deb/apt`로):
|
||||
|
||||
| 동작 | 메서드 / 경로 | 비고 |
|
||||
|---|---|---|
|
||||
| 상태 확인 | `GET /pulp/api/v3/status/` | 헬스체크 |
|
||||
| 저장소 목록 | `GET /pulp/api/v3/repositories/rpm/rpm/` | |
|
||||
| 저장소 동기화(스냅샷 생성) | `POST /pulp/api/v3/repositories/rpm/rpm/{uuid}/sync/` body: `{"remote": "<remote_href>"}` | 새 RepositoryVersion을 만든다. 비동기 → task 반환 |
|
||||
| 버전 목록 | `GET /pulp/api/v3/repositories/rpm/rpm/{uuid}/versions/` | 스냅샷 v1, v2, ... |
|
||||
| 작업 상태 | `GET {task_href}` | state: waiting/running/completed/failed, progress_reports[] |
|
||||
| Publication 생성 | `POST /pulp/api/v3/publications/rpm/rpm/` body: `{"repository_version": "<version_href>"}` | 특정 버전을 배포가능 형태로 |
|
||||
| Distribution 목록 | `GET /pulp/api/v3/distributions/rpm/rpm/` | 공개 URL들 |
|
||||
| Distribution 업데이트(배포 확정) | `PATCH {distribution_href}` body: `{"publication": "<publication_href>"}` | **이게 "이 버전 배포" 동작.** 운영망이 보는 URL을 특정 publication으로 교체 |
|
||||
|
||||
- Pulp 객체는 `pulp_href`(경로 문자열)로 서로를 참조한다. 이름과 href를 혼용 가능하나 코드에선 href 기준.
|
||||
- 동기화/배포는 **비동기 task**를 반환한다. 반드시 task_href를 받아 폴링으로 완료를 확인한다.
|
||||
- GPG 검증: Remote에 `gpgkey`/`tls_validation` 설정 시 sync 단계에서 서명을 검증한다.
|
||||
Repository의 `repo_config`에 `gpgcheck: 1, repo-gpgcheck: 1`이 들어간다 → UI에서 "검증됨" 배지 근거.
|
||||
|
||||
---
|
||||
|
||||
## 5. 권한·안전 모델 (풀 기능이므로 필수)
|
||||
|
||||
운영자가 "실제 조작"까지 하므로 배포 사고 방지가 설계의 일부다.
|
||||
|
||||
1. **동기화(sync)**: 비교적 안전. 외부 미러에서 받아 새 스냅샷을 만들 뿐, 운영망 배포는 아님.
|
||||
버튼 누르면 바로 실행 가능.
|
||||
2. **배포 확정(Distribution 교체)**: 위험. 운영 서버가 실제로 받게 되는 버전이 바뀐다.
|
||||
- 반드시 **확인 모달**(hx-confirm 또는 별도 확인 단계)을 거친다.
|
||||
- "검증 완료(GPG/checksum 통과) 상태"가 아닌 버전은 배포 버튼 자체를 비활성화한다.
|
||||
- 모든 배포 확정 행위는 **감사 로그**로 남긴다(누가/언제/어떤 repo를/어떤 버전으로).
|
||||
3. (선택) 향후 읽기전용 계정 / 배포권한 계정 분리 여지를 남긴다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 화면 명세 (MVP — 4개)
|
||||
|
||||
### 화면 1. 대시보드 (`GET /`)
|
||||
- 상단 요약 카드: 전체 저장소 수 / 동기화 중 수 / GPG 검증 통과 수 / 총 패키지 수
|
||||
- 저장소 목록(카드 또는 표). 각 행:
|
||||
- 저장소 이름, GPG 검증 배지(✅/⚠️), 현재 운영 배포 버전(vN), 마지막 동기화 시각, 패키지 수
|
||||
- [동기화] 버튼
|
||||
- 동기화 중인 항목은 진행률 바 + "n/m 패키지, NN%" 표시
|
||||
- 진행률 영역은 `hx-get="/repos/{uuid}/progress" hx-trigger="every 3s"`로 자동 폴링.
|
||||
|
||||
### 화면 2. 동기화 실행 + 진행률 (부분 갱신)
|
||||
- [동기화] 클릭 → `POST /repos/{uuid}/sync` → BFF가 Pulp sync 호출 → task_href 보관
|
||||
- 응답으로 진행률 바 조각 반환, 이후 3초마다 폴링하여 갱신
|
||||
- 완료 시: "동기화 완료, 새 버전 vN 생성됨" 배지로 교체
|
||||
- 실패 시: 빨간 에러 메시지 + 사유(task의 error 필드)
|
||||
|
||||
### 화면 3. 버전(스냅샷) 목록 + 배포 확정 (`GET /repos/{uuid}/versions`)
|
||||
- 해당 저장소의 RepositoryVersion 목록을 최신순으로
|
||||
- 각 버전: vN, 생성일시, 패키지 수(증감), 검증 상태, 현재 운영 배포 여부 배지
|
||||
- 검증 완료 + 미배포 버전에만 [이 버전 배포] 버튼 활성화
|
||||
- 클릭 → 확인 모달 → `POST /repos/{uuid}/deploy` body: 선택 version_href
|
||||
→ BFF가 (publication 생성 → distribution PATCH) 수행 → task 폴링 → 완료 시 배지 갱신
|
||||
|
||||
### 화면 4. 검증 상태 상세 (화면 1/3에 인라인으로 표시해도 됨)
|
||||
- GPG 서명 검증 결과, checksum 타입(sha256 등), repo_config의 gpgcheck 값 표시
|
||||
- 통과 ✅ / 경고 ⚠️ / 미설정 ⚪ 3단계
|
||||
|
||||
---
|
||||
|
||||
## 7. FastAPI 라우트 설계 (BFF)
|
||||
|
||||
화면용 라우트는 "전체 페이지" 또는 "HTML 조각"을 반환한다(JSON 아님, HTMX가 HTML을 받기 때문).
|
||||
|
||||
```
|
||||
GET / → 대시보드 전체 페이지
|
||||
GET /repos → 저장소 목록 조각 (새로고침용)
|
||||
POST /repos/{uuid}/sync → 동기화 시작, 진행률 바 조각 반환
|
||||
GET /repos/{uuid}/progress → 진행률 바 조각 (폴링 대상)
|
||||
GET /repos/{uuid}/versions → 버전 목록 조각/페이지
|
||||
POST /repos/{uuid}/deploy → 배포 확정(publication+distribution), 결과 조각
|
||||
GET /healthz → Pulp status 프록시
|
||||
```
|
||||
|
||||
내부 헬퍼:
|
||||
```
|
||||
pulp_client.py
|
||||
- get(path) / post(path, json) / patch(href, json) # Basic Auth 포함
|
||||
- wait_or_poll_task(task_href) # 단발 조회(폴링은 화면이 함)
|
||||
- list_repos() / sync_repo(uuid, remote_href)
|
||||
- list_versions(uuid) / create_publication(version_href)
|
||||
- update_distribution(dist_href, publication_href)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 설정 / 환경변수
|
||||
|
||||
```
|
||||
PULP_BASE_URL = http://repo.bok.or.kr:8080 # 또는 내부 주소
|
||||
PULP_USERNAME = admin
|
||||
PULP_PASSWORD = (시크릿; 환경변수/시크릿 파일)
|
||||
PULP_VERIFY_TLS = true/false # 사내 CA면 cafile 경로 지정
|
||||
PULP_CA_FILE = /path/boknet-ca.pem # 사내 TLS 검사 환경 대응
|
||||
```
|
||||
|
||||
- 사내망 TLS(self-signed CA) 이슈가 있을 수 있으므로, httpx 클라이언트에 `verify=PULP_CA_FILE`
|
||||
지정 옵션을 처음부터 넣어둔다. (LiteLLM 세팅 때와 동일한 boknet-SMSCENTER-CA 계열)
|
||||
|
||||
---
|
||||
|
||||
## 9. 폐쇄망 배포 방식
|
||||
|
||||
- 최종 산출물: FastAPI 앱(+Jinja 템플릿) + 정적 자산(Pico.css 등 로컬 포함)을 하나의
|
||||
컨테이너 이미지 또는 단순 pip 가상환경으로 패키징.
|
||||
- Python 의존성(pip)·정적 CSS는 인터넷 가능 PC에서 받아 반입하거나 사내 프록시(litellm 방식) 활용.
|
||||
- CDN 직접 의존을 피하고 CSS/HTMX 스크립트는 `static/`에 동봉한다.
|
||||
|
||||
---
|
||||
|
||||
## 10. 개발 순서 (Claude Code에게)
|
||||
|
||||
1. 프로젝트 골격: FastAPI + Jinja2 + httpx, `pulp_client.py` 스텁, `/healthz`로 Pulp status 확인
|
||||
2. 대시보드(화면 1) — 저장소 목록 읽어 표/카드 렌더 (읽기만)
|
||||
3. 동기화(화면 2) — sync POST → task_href → 진행률 폴링 조각
|
||||
4. 버전 목록(화면 3 전반) — versions 읽어 목록 렌더 + 검증 배지
|
||||
5. 배포 확정(화면 3 후반) — 확인 모달 → publication 생성 → distribution PATCH → 폴링 → 감사 로그
|
||||
6. 스타일 마감(Pico.css/Tailwind), 폐쇄망 정적 자산 동봉, 컨테이너화
|
||||
|
||||
각 단계는 "동작하는 화면"이 나오는 단위로 끊는다. 한 번에 다 만들지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 11. 참고
|
||||
|
||||
- Pulp RPM 튜토리얼(sync→publication→distribution 흐름): pulpproject.org pulp_rpm 문서
|
||||
- 커뮤니티 UI(참고용, 베이스로 쓰지 않음): github.com/pulp/pulp-ui
|
||||
- 우리는 pulp-ui 코드를 fork하지 않는다. API 호출 흐름만 참고하고, 위 스택으로 새로 만든다.
|
||||
115
ref/design-ref.md
Normal file
115
ref/design-ref.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Toss — Playful Fintech
|
||||
|
||||
Reference DESIGN.md for Korean-fintech playful: Toss Blue against bright white, Toss Product Sans paired with Noto Sans KR, generous radii, friendly mood, mobile-first density.
|
||||
|
||||
## 1. Visual Theme & Atmosphere
|
||||
|
||||
Playful fintech. The page reads like a friend explaining money — bright white surfaces, one confident blue, generous radii, illustrations of cards and coins rendered in flat vector. The brand is warm without being childish; type stays geometric, copy stays plainspoken, and every CTA invites a tap. Korean-first, but the system holds in Latin script.
|
||||
|
||||
Mood: friendly, approachable, confident, never sterile.
|
||||
|
||||
## 2. Color Palette & Roles
|
||||
|
||||
```
|
||||
--bg: #ffffff /* canvas */
|
||||
--bg-alt: #f9fafb
|
||||
--surface: #f2f4f6 /* cool gray surface lift */
|
||||
--surface-2: #e8f3ff /* tinted blue wash for callouts */
|
||||
--text: #191f28 /* near-ink */
|
||||
--text-secondary: #333d4b /* body copy */
|
||||
--text-muted: #6b7684
|
||||
--text-dim: #8b95a1
|
||||
--border: #e5e8eb
|
||||
--border-strong: #d1d6db
|
||||
|
||||
--accent: #3182f6 /* Toss Blue — primary action + brand */
|
||||
--accent-hover: #1b64da
|
||||
--accent-deep: #1e40af /* pressed / dark variant */
|
||||
--accent-soft: #e8f3ff /* tinted callouts, secondary button bg */
|
||||
|
||||
--success: #00c896 /* mint, positive balance */
|
||||
--warning: #ff9500 /* amber, attention */
|
||||
--danger: #f04452 /* coral red, decline / loss */
|
||||
--info: #3182f6
|
||||
```
|
||||
|
||||
Rule: Toss Blue is the only branded hue. Status colors are reserved for state (positive balance, attention, decline). Never reuse status hues for decoration. The tinted blue wash `--surface-2` carries callouts and secondary buttons — it is the brand's softening move.
|
||||
|
||||
## 3. Typography Rules
|
||||
|
||||
- **Headlines + display:** `Toss Product Sans`, fallback `Pretendard`, `-apple-system`, `BlinkMacSystemFont`, `Apple SD Gothic Neo`, `Noto Sans KR`, system. Weight 600–700, tight tracking (−1% at large sizes).
|
||||
- **Body + UI:** `Toss Product Sans`, fallback as above. Weight 400/500. Line-height 1.5–1.6. Korean glyphs rendered via `Noto Sans KR` and `Apple SD Gothic Neo` fallbacks.
|
||||
- **UI labels:** Toss Product Sans weight 500, 13–15px.
|
||||
- **Numerals:** tabular figures globally on balances, transaction lists, charts.
|
||||
- **Mono:** `JetBrains Mono`, fallback `SF Mono`. Used for transaction IDs, code samples, OTPs.
|
||||
|
||||
Scale: 12 / 14 / 15 / 17 / 20 / 24 / 32 / 40 / 50 / 66.
|
||||
|
||||
Hero headlines run 50–66px on desktop. Korean and Latin glyphs share metrics within Toss Product Sans.
|
||||
|
||||
## 4. Component Stylings
|
||||
|
||||
**Buttons**
|
||||
- Primary: Toss Blue fill `--accent`, white text `#f9fafb`, radius 7px, padding 14/20, weight 500. Hover: `--accent-hover`. Pressed: `--accent-deep`. No lift, no scale.
|
||||
- Secondary: tinted blue fill `--surface-2`, deep blue text `--accent-hover`, radius 7px. The two-step blue is Toss's signature hierarchy.
|
||||
- Tertiary / link: `--text-secondary`, blue underline on hover.
|
||||
- Destructive: `--danger` fill, white text, only on confirm modals.
|
||||
|
||||
**Cards / list items**
|
||||
- White fill, 1px `--border`, radius 12px, padding 20. Soft shadow `0 1px 3px rgba(25, 31, 40, 0.04)` only.
|
||||
- Transaction rows: 1px hairline divider, no card chrome, 64px row height for tap targets.
|
||||
- Hover: 1px `--border-strong`. No lift.
|
||||
|
||||
**Inputs**
|
||||
- `--surface` fill, no border at rest, radius 12px, padding 16/20 (large tap targets).
|
||||
- Focus: 2px `--accent` ring, no offset. Label floats above on focus.
|
||||
- Currency inputs use right-aligned tabular numerals.
|
||||
|
||||
**Nav**
|
||||
- Mobile bottom-tab nav primary, 56px tall, 5 icons max, blue active state.
|
||||
- Web top nav: white fill, 1px bottom `--border`, blue underline on active.
|
||||
|
||||
**Illustrations**
|
||||
- Flat vector. Cards, coins, characters. 2–3 hues per illustration drawn from blue + soft neutrals.
|
||||
- Avoid photorealism, avoid drop shadows on illustration art.
|
||||
|
||||
## 5. Layout Principles
|
||||
|
||||
- Mobile-first. 360–430px primary canvas. Web mirrors mobile column at 480px max content width inside a 1200px shell.
|
||||
- 4px base. 4 / 8 / 12 / 16 / 20 / 24 / 32 / 48.
|
||||
- Generous vertical rhythm between content blocks (32–48px).
|
||||
- Dense list views at 64px row height for tap-target compliance.
|
||||
|
||||
## 6. Depth & Elevation
|
||||
|
||||
Soft shadows allowed sparingly — single `0 1px 3px rgba(25, 31, 40, 0.04)` on cards, `0 8px 24px rgba(25, 31, 40, 0.08)` on modals and bottom sheets. No neumorphism. No stacked shadows. Tap targets get a brief scale(0.98) press feedback (96ms ease-out).
|
||||
|
||||
## 7. Do's and Don'ts
|
||||
|
||||
**Do**
|
||||
- Use Toss Blue as the single confident accent.
|
||||
- Pair primary blue with the tinted blue secondary — the two-step is the signature.
|
||||
- Ship bottom-sheet modals on mobile, not page-pushed dialogs.
|
||||
- Render every monetary value in tabular numerals.
|
||||
- Write copy in plainspoken Korean / English — short sentences, friendly verbs.
|
||||
|
||||
**Don't**
|
||||
- Introduce a second saturated brand hue.
|
||||
- Soften corners past 16px (Toss tops out at 12–16px on cards, 7px on buttons).
|
||||
- Use dark mode as the default canvas.
|
||||
- Fill backgrounds with gradients (flat fills only).
|
||||
- Mix more than two type weights per screen.
|
||||
|
||||
## 8. Responsive Behavior
|
||||
|
||||
- Mobile is canonical. Desktop mirrors the mobile column inside a centered 480px content lane.
|
||||
- Headlines scale 66 → 28 on mobile.
|
||||
- Bottom-tab nav on mobile becomes top nav with same 5 destinations on web.
|
||||
- Modals become full-screen sheets below 640px.
|
||||
- Lists become full-bleed at 360px viewport.
|
||||
|
||||
## 9. Agent Prompt Guide
|
||||
|
||||
Bias: bright white `#ffffff` canvas, single Toss Blue `#3182f6` accent paired with a tinted blue `#e8f3ff` secondary, Toss Product Sans with Noto Sans KR fallback, 7px radii on buttons and 12px on cards, mobile-first 480px content lane, 64px tap-target row heights, tabular numerals on every monetary value, flat-vector illustrations of cards / coins, bottom-sheet modals on mobile.
|
||||
|
||||
Reject: dark-mode marketing as default, multi-accent palettes, gradient fills, drop-shadow heavy cards, photorealistic imagery, soft-pill buttons over 16px radius, dense desktop-first dashboards, decorative emoji in chrome.
|
||||
Reference in New Issue
Block a user