/* ============================================================================ * 서명전에 — TDS NumericSpinner 컴포넌트 * · 정수 입력 — - / + 버튼으로 1 씩 증감. * · 4 size: tiny / small / medium / large (CSS 에 별도 변형) * · controlled (number + onNumberChange) / uncontrolled (defaultNumber) * · min/max 도달 시 해당 버튼만 자동 비활성화 * · disable=true → 컴포넌트 전체 비활성 * · 숫자는 aria-live="polite" 로 변경 자동 안내 * · decreaseAriaLabel / increaseAriaLabel 로 상황 맞춤 라벨 (기본 "빼기"/"더하기") * * · props (NumericSpinnerProps): * size * "tiny" | "small" | "medium" | "large" (필수) * number number — controlled 현재 값 * defaultNumber number — uncontrolled 초기 값 * minNumber number — 최솟값 (기본 0) * maxNumber number — 최댓값 (기본 999) * disable boolean — true 면 전체 비활성 (기본 false) * onNumberChange (number: number) => void — 값 변경 콜백 * decreaseAriaLabel string — 감소 버튼 aria-label (기본 "빼기") * increaseAriaLabel string — 증가 버튼 aria-label (기본 "더하기") * className, style, id 패스스루 * ========================================================================== */ const __NUMSPIN_VALID_SIZES = { tiny: 1, small: 1, medium: 1, large: 1 }; function NumericSpinner({ size, number, defaultNumber, minNumber = 0, maxNumber = 999, disable = false, onNumberChange, decreaseAriaLabel = "빼기", increaseAriaLabel = "더하기", className = "", style, id, ...rest }) { // size 필수 — 검증 if (!__NUMSPIN_VALID_SIZES[size]) { if (window.console && console.warn) { console.warn( `[NumericSpinner] \`size\` 는 필수예요 — "tiny" | "small" | "medium" | "large" 중 하나. 받은 값: ${size}` ); } } const safeSize = __NUMSPIN_VALID_SIZES[size] ? size : "medium"; // min/max 정합성 보정 const safeMin = Number.isFinite(minNumber) ? minNumber : 0; const safeMax = Number.isFinite(maxNumber) ? maxNumber : 999; const realMin = Math.min(safeMin, safeMax); const realMax = Math.max(safeMin, safeMax); const isControlled = typeof number === "number"; // uncontrolled 초기값 — defaultNumber 가 있으면 그걸로, 없으면 realMin 으로 const initial = (function () { const seed = typeof defaultNumber === "number" ? defaultNumber : realMin; return Math.min(realMax, Math.max(realMin, Math.round(seed))); })(); const [internal, setInternal] = React.useState(initial); // 외부 number 가 범위 밖이어도 그대로 보여줘요 (단, 버튼은 clamp 후 emit) const current = isControlled ? number : internal; const setValue = function (next) { const clamped = Math.min(realMax, Math.max(realMin, Math.round(next))); if (typeof onNumberChange === "function") onNumberChange(clamped); if (!isControlled) setInternal(clamped); }; const canDec = !disable && current > realMin; const canInc = !disable && current < realMax; const handleDec = function () { if (canDec) setValue(current - 1); }; const handleInc = function () { if (canInc) setValue(current + 1); }; const cls = [ "tds-numspin", `tds-numspin--size-${safeSize}`, disable ? "tds-numspin--disabled" : "", className, ].filter(Boolean).join(" "); return (
{current}
); } window.NumericSpinner = NumericSpinner; Object.assign(window, { NumericSpinner });