/* ============================================================================ * 서명전에 — TDS Loader 컴포넌트 * · 데이터/작업 로딩 중 시각 피드백을 주는 회전 스피너 + 옵션 라벨. * · SVG 두 원으로 그려요: * track : 옅은 베이스 원 (stroke-opacity 0.16) * arc : 1/4 호가 회전 — strokeDasharray 로 "보이는 길이" 만 1/4 * · primary 는 파란색, dark 는 짙은 회색, light 는 흰색이에요. * light 는 어두운 배경에서만 보이므로 호출자가 배경을 깔아 줘야 해요. * * · props (LoaderProps): * size "small" | "medium" | "large" (기본 "medium") * type "primary" | "dark" | "light" (기본 "primary") * label string — 스피너 아래 텍스트. \n 줄바꿈 지원 * style, className, id ... 루트 div 패스스루 * aria-label, role 등도 패스스루 (기본 role="status") * ========================================================================== */ const __LOADER_SIZE_PX = { small: 20, medium: 32, large: 48, }; const __LOADER_STROKE = { small: 2.5, medium: 3, large: 4, }; function Loader({ size = "medium", type = "primary", label, className = "", style, id, ...rest }) { // 사이즈 검증 if (!__LOADER_SIZE_PX[size]) { if (window.console && console.warn) { console.warn( `[Loader] \`size\` 는 "small" | "medium" | "large" 중 하나여야 해요. 받은 값: ${size}` ); } } const safeSize = __LOADER_SIZE_PX[size] ? size : "medium"; // 타입 검증 const validTypes = { primary: 1, dark: 1, light: 1 }; if (!validTypes[type]) { if (window.console && console.warn) { console.warn( `[Loader] \`type\` 은 "primary" | "dark" | "light" 중 하나여야 해요. 받은 값: ${type}` ); } } const safeType = validTypes[type] ? type : "primary"; const px = __LOADER_SIZE_PX[safeSize]; const stroke = __LOADER_STROKE[safeSize]; // 24×24 viewBox 기준으로 반지름 = 12 - stroke/2 // (stroke 가 박스 안쪽으로 그려지지 않고 양옆 절반씩 걸치기 때문) const r = 12 - stroke / 2; const circumference = 2 * Math.PI * r; const arcLen = circumference / 4; // 1/4 호만 보이게 const dashArray = `${arcLen} ${circumference - arcLen}`; const cls = [ "tds-loader", `tds-loader--size-${safeSize}`, `tds-loader--type-${safeType}`, className, ].filter(Boolean).join(" "); // role="status" + aria-live="polite" 이 기본 — 호출자가 다르게 줄 수 있게 패스스루 const a11yProps = { role: rest.role || "status", "aria-live": rest["aria-live"] || "polite", "aria-label": rest["aria-label"] || (typeof label === "string" && label) || "로딩 중", }; // role/aria-live/aria-label 은 위에서 처리했으니 rest 에서 빼고 나머지만 패스스루 const { role: _r, ["aria-live"]: _al, ["aria-label"]: _albl, ...restPass } = rest; return (
{label}
)}