feat(D 예정노선): 영역 루틴 — 받기 끝 지형면 · 받은 범위 먼저 → 겹 다시(차례 번호) · 도엽 겹 서버 자름 · 위치 칸 제목 서클 하나 · 10 km 밖 알림 (PLAN 604 · 요구 475 · 476-2)

- 받기 끝 = 지형면 · 받은 범위 다 읽은 뒤 겹 판정(동시 읽기 = 옛 범위 판정 뿌리) · 되돌리기 · 다시하기도 영역 확인
- 겹 load(have) — 받은 범위 지문 · 위성 틀 바뀜 = 차례 +1 · 자료 · 받는 중 비움 · 켠 겹 모두 다시 · 옛 차례 답은 자료 · GL 에 안 씀
- 등고 · 세류 = 예정노선 겹 길(layers/sheet) · 도엽 겹 모두 서버가 받은 범위로 자른 사본(화면 상자 · 보관함 지문 걷음)
- 서클 = 위치 칸 제목 오른쪽 하나 · 들어간 일 수(지형 받기 · 지형면 · 겹 · 임도망 올리기) · 부품 상자 원에 제목 줄 꼴(inline) 덧 · 지도 귀퉁이 서클 걷음
- 기준점 10 km 밖 점 = 위치 칸 알림 한 줄
- 시험 553_4 서클 줄 = 새 자리로 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-30 21:43:20 +09:00
co-authored by Claude Opus 5.5
parent 20ed22709c
commit 64b35da265
8 changed files with 528 additions and 267 deletions
+12 -5
View File
@@ -5,6 +5,7 @@ import "./ui_template_progress.css";
* 부품 「진행 막대」(552-3 · 목록 30) — 막대 · 쌓임 막대 · 원.
* 꼴 = 막대(`createProgressBar` · 값 하나) · 쌓임 막대(`parts` · 갈래 셋 accent · soft · warn) · 원(`createProgressCircle` · 아래)
* · 원 자리 = 흐름(기본) · 가운데 덮음(`overlay`) · 오른쪽 아래 귀퉁이(`corner` · 글은 원 왼쪽)
* · 제목 줄 안(`inline` · 카드 제목 오른쪽 · 캐럿 왼쪽 · 작은 원 + 글 한 줄 · 진행률은 글 끝에)
* 크기 = 막대 굵기 6 · 8(`thick` → data-size) · 원 지름 72(기본)
* 상태 = 기본 · 오류(`.is-error` · 막대가 위험색) · 판정(`tone`: warn · bad)
* 쓰는 법 = `createProgressBar({ value: 0.4, label: "40 %" })` · `bar.set(0.8)` · `createProgressBar({ parts: [{ value: 3, tone: "accent" }, { value: 1, tone: "warn" }] })`
@@ -21,6 +22,7 @@ const SVG_NS = "http://www.w3.org/2000/svg";
/** viewBox 기준 반지름 — 실제 크기는 CSS(--ui-progress-size)로 정한다. */
const RADIUS = 42;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
const clamp01 = (v: number): number => Math.min(1, Math.max(0, v));
export interface ProgressCircleOptions {
/** 지름(px). 기본 72(공통 로딩 스피너와 같은 무게감). */
@@ -31,6 +33,8 @@ export interface ProgressCircleOptions {
overlay?: boolean;
/** 컨테이너 오른쪽 아래 귀퉁이에 작게 띄운다(가운데를 안 가림 · 컨테이너는 position: relative). */
corner?: boolean;
/** 제목 줄 안(패널 카드 제목 오른쪽 · 캐럿 왼쪽) — 작은 원 + 글 한 줄 · 진행률은 글 끝에 붙음. */
inline?: boolean;
}
export interface ProgressCircleHandle {
@@ -46,7 +50,8 @@ export function createProgressCircle(options: ProgressCircleOptions = {}): Progr
root.className =
"ui-progress-circle" +
(options.overlay ? " ui-progress-circle--overlay" : "") +
(options.corner ? " ui-progress-circle--corner" : "");
(options.corner ? " ui-progress-circle--corner" : "") +
(options.inline ? " ui-progress-circle--inline" : "");
root.setAttribute("role", "status");
root.setAttribute("aria-live", "polite");
if (options.size) root.style.setProperty("--ui-progress-size", `${options.size}px`);
@@ -86,15 +91,19 @@ export function createProgressCircle(options: ProgressCircleOptions = {}): Progr
dial.append(spin, percent);
root.append(dial, label);
let text = options.label ?? "";
function set(ratio: number | null, nextLabel?: string): void {
if (nextLabel !== undefined) label.textContent = nextLabel;
if (nextLabel !== undefined) text = label.textContent = nextLabel;
if (options.inline)
label.textContent =
ratio === null ? text : `${text} ${Math.round(clamp01(ratio) * 100)}%`.trim();
if (ratio === null) {
// 진행률 미상 — 4분의 1 호만 남긴다(회전은 껍데기가 늘 맡는다).
bar.setAttribute("stroke-dashoffset", String(CIRCUMFERENCE * 0.75));
percent.textContent = "";
return;
}
const clamped = Math.min(1, Math.max(0, ratio));
const clamped = clamp01(ratio);
bar.setAttribute("stroke-dashoffset", String(CIRCUMFERENCE * (1 - clamped)));
percent.textContent = `${Math.round(clamped * 100)}%`;
}
@@ -126,8 +135,6 @@ export interface ProgressBarHandle {
set: (value: number, label?: string) => void;
}
const clamp01 = (v: number): number => Math.min(1, Math.max(0, v));
/** 진행 막대 — 값 하나 또는 쌓임 */
export function createProgressBar(opts: ProgressBarOptions = {}): ProgressBarHandle {
const root = document.createElement("div");