- 공통 서클 양식을 로딩 스피너(.ui-spinner) 기준으로 통일하고 overlay 옵션 신설 (컨테이너 정중앙 배치를 페이지별 CSS 없이 처리) - B05 3D: 위쪽 18% → 뷰포트 정중앙 (하단 패널에 가려져도 무방) - B04 포인트클라우드 뷰어: setLoading() 신설, render() 시 자동 해제 - B04 지형 뷰어: GLTF/PLY 로더 진행 이벤트로 실제 바이트 진행률 표시 - B04 2D 지도: 도엽 레이어 n/10 진행률 - B05 종단면 그래프: body-wrap으로 감싸 서클 유지, 자료 조회 전후로 토글 - B05 배수유역도: 배경도 → 도엽 레이어 → 세부유역 산정 단계 진행률
92 lines
3.6 KiB
TypeScript
92 lines
3.6 KiB
TypeScript
import "./ui_template_progress.css";
|
|
|
|
/* =============================================================================
|
|
* ui_template_progress.ts
|
|
* 공통 프로그레스 서클 (원형 진행 표시)
|
|
*
|
|
* 뷰포트 위에 얹어 "지금 무엇을 얼마나 불러왔는지"를 보여 주는 용도.
|
|
* 진행률을 모르는 구간은 set()에 ratio를 주지 않으면 회전 애니메이션으로 표시한다.
|
|
* 색상/치수는 theme.css 변수만 사용한다.
|
|
* ========================================================================== */
|
|
|
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
/** viewBox 기준 반지름 — 실제 크기는 CSS(--ui-progress-size)로 정한다. */
|
|
const RADIUS = 42;
|
|
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
|
|
|
export interface ProgressCircleOptions {
|
|
/** 지름(px). 기본 72(공통 로딩 스피너와 같은 무게감). */
|
|
size?: number;
|
|
/** 서클 아래 안내 문구. */
|
|
label?: string;
|
|
/** 컨테이너 정중앙에 띄우는 오버레이로 만든다(컨테이너는 position: relative 여야 한다). */
|
|
overlay?: boolean;
|
|
}
|
|
|
|
export interface ProgressCircleHandle {
|
|
root: HTMLDivElement;
|
|
/** 진행률(0~1)과 문구 갱신. ratio가 null이면 진행률 미상(회전 표시). */
|
|
set: (ratio: number | null, label?: string) => void;
|
|
/** 화면에서 제거. */
|
|
remove: () => void;
|
|
}
|
|
|
|
export function createProgressCircle(options: ProgressCircleOptions = {}): ProgressCircleHandle {
|
|
const root = document.createElement("div");
|
|
root.className = "ui-progress-circle" + (options.overlay ? " ui-progress-circle--overlay" : "");
|
|
root.setAttribute("role", "status");
|
|
root.setAttribute("aria-live", "polite");
|
|
if (options.size) root.style.setProperty("--ui-progress-size", `${options.size}px`);
|
|
|
|
const svg = document.createElementNS(SVG_NS, "svg");
|
|
svg.setAttribute("class", "ui-progress-circle__svg");
|
|
svg.setAttribute("viewBox", "0 0 100 100");
|
|
const track = document.createElementNS(SVG_NS, "circle");
|
|
track.setAttribute("class", "ui-progress-circle__track");
|
|
track.setAttribute("cx", "50");
|
|
track.setAttribute("cy", "50");
|
|
track.setAttribute("r", String(RADIUS));
|
|
const bar = document.createElementNS(SVG_NS, "circle");
|
|
bar.setAttribute("class", "ui-progress-circle__bar");
|
|
bar.setAttribute("cx", "50");
|
|
bar.setAttribute("cy", "50");
|
|
bar.setAttribute("r", String(RADIUS));
|
|
bar.setAttribute("stroke-dasharray", String(CIRCUMFERENCE));
|
|
bar.setAttribute("stroke-dashoffset", String(CIRCUMFERENCE));
|
|
svg.append(track, bar);
|
|
|
|
const percent = document.createElement("span");
|
|
percent.className = "ui-progress-circle__percent";
|
|
const label = document.createElement("span");
|
|
label.className = "ui-progress-circle__label";
|
|
label.textContent = options.label ?? "";
|
|
|
|
const dial = document.createElement("div");
|
|
dial.className = "ui-progress-circle__dial";
|
|
dial.append(svg, percent);
|
|
root.append(dial, label);
|
|
|
|
function set(ratio: number | null, nextLabel?: string): void {
|
|
if (nextLabel !== undefined) label.textContent = nextLabel;
|
|
if (ratio === null) {
|
|
// 진행률 미상 — 4분의 1 호를 돌려 "돌아가는 중"만 알린다.
|
|
root.classList.add("is-indeterminate");
|
|
bar.setAttribute("stroke-dashoffset", String(CIRCUMFERENCE * 0.75));
|
|
percent.textContent = "";
|
|
return;
|
|
}
|
|
const clamped = Math.min(1, Math.max(0, ratio));
|
|
root.classList.remove("is-indeterminate");
|
|
bar.setAttribute("stroke-dashoffset", String(CIRCUMFERENCE * (1 - clamped)));
|
|
percent.textContent = `${Math.round(clamped * 100)}%`;
|
|
}
|
|
|
|
set(0, options.label);
|
|
|
|
return {
|
|
root,
|
|
set,
|
|
remove: () => root.remove(),
|
|
};
|
|
}
|