/* ============================================================================ * 서명전에 — TDS Rating 컴포넌트 * · 사용자가 항목/콘텐츠에 대한 평가를 제공하거나(interactive) 결과를 * 읽기 전용으로 표시(readOnly)하는 별점 UI. * * · 두 모드: * readOnly=false → max 개의 별 버튼, 클릭 시 onValueChange * readOnly=true → 평가 결과 표시. variant 로 형태 변경 가능 * * · size: * readOnly=false → "medium" | "large" | "big" (3종) * readOnly=true → "tiny" | "small" | "medium" | "large" | "big" (5종) * * · variant (readOnly=true 전용): * "full" — max 개의 별 + 숫자 값 * "compact" — 별 1개 + 숫자 값 * "iconOnly" — max 개의 별만 (숫자 없음) * * · RatingProps: * readOnly * boolean (필수) * value * number (0 ~ max, 자동 클램프) * size * 위 size 테이블 * variant * 위 variant 테이블 (readOnly=true 일 때만) * max number (기본 5) * onValueChange (v:number)=>void (readOnly=false 일 때만 호출) * disabled boolean (readOnly=false 일 때만) * aria-label 기본 "별점 평가" * aria-valuetext 기본 "max점 만점 중 value점" * * · 접근성: * · interactive 모드: 시각적으로 숨긴 포함 * → 키보드 ← → 로 값 변경, 스크린 리더는 슬라이더 의미 읽음 * · 별 SVG 는 aria-hidden="true" — 값은 aria-valuetext 로 전달 * · 루트에 role + aria-label + aria-valuetext 부착 * ========================================================================== */ const __RATING_INTERACTIVE_SIZES = { medium: 1, large: 1, big: 1 }; const __RATING_READONLY_SIZES = { tiny: 1, small: 1, medium: 1, large: 1, big: 1 }; const __RATING_VARIANTS = { full: 1, compact: 1, iconOnly: 1 }; // ----- 별 SVG (5각성, currentColor 로 색 제어) ----- function __RatingStarSvg() { return ( ); } function Rating({ readOnly, value, size, variant, max = 5, onValueChange, disabled = false, className = "", style, id, "aria-label": ariaLabel, "aria-valuetext": ariaValuetext, ...rest }) { // readOnly 필수 if (typeof readOnly !== "boolean") { if (window.console && console.warn) { console.warn( `[Rating] \`readOnly\` 는 필수 boolean prop 이에요. 받은 값: ${readOnly}` ); } } const isReadOnly = readOnly === true; // size 검증 — readOnly 여부에 따라 허용 값이 달라짐 const sizeMap = isReadOnly ? __RATING_READONLY_SIZES : __RATING_INTERACTIVE_SIZES; if (!sizeMap[size]) { if (window.console && console.warn) { const allowed = isReadOnly ? '"tiny" | "small" | "medium" | "large" | "big"' : '"medium" | "large" | "big"'; console.warn( `[Rating] readOnly=${isReadOnly} 일 때 \`size\` 는 ${allowed} 중 하나여야 해요. 받은 값: ${size}` ); } } const safeSize = sizeMap[size] ? size : "medium"; // variant 검증 — readOnly=true 일 때만 의미 if (!isReadOnly && variant != null) { if (window.console && console.warn) { console.warn( "[Rating] `variant` 는 `readOnly=true` 일 때만 사용할 수 있어요. 무시돼요." ); } } if (isReadOnly && variant != null && !__RATING_VARIANTS[variant]) { if (window.console && console.warn) { console.warn( `[Rating] \`variant\` 는 "full" | "compact" | "iconOnly" 중 하나여야 해요. 받은 값: ${variant}` ); } } const safeVariant = isReadOnly ? (__RATING_VARIANTS[variant] ? variant : "full") : null; // disabled / onValueChange — readOnly=true 일 때 경고 if (isReadOnly && disabled) { if (window.console && console.warn) { console.warn( "[Rating] `disabled` 는 `readOnly=true` 일 때 사용할 수 없어요. 무시돼요." ); } } if (isReadOnly && typeof onValueChange === "function") { if (window.console && console.warn) { console.warn( "[Rating] `onValueChange` 는 `readOnly=true` 일 때 호출되지 않아요." ); } } const safeDisabled = !isReadOnly && disabled === true; // max 정규화 — 1 이상 정수 const safeMax = typeof max === "number" && Number.isFinite(max) && max >= 1 ? Math.max(1, Math.round(max)) : 5; // value 정규화 — [0, safeMax] 클램프 + 정수 const rawValue = typeof value === "number" && Number.isFinite(value) ? value : 0; const safeValueInt = Math.max( 0, Math.min(safeMax, Math.round(rawValue)) ); const finalAriaLabel = ariaLabel != null ? ariaLabel : "별점 평가"; const finalValuetext = ariaValuetext != null ? ariaValuetext : `${safeMax}점 만점 중 ${safeValueInt}점`; const cls = [ "tds-rating", `tds-rating--size-${safeSize}`, isReadOnly ? "tds-rating--readonly" : "tds-rating--interactive", safeVariant ? `tds-rating--variant-${safeVariant}` : "", safeDisabled ? "is-disabled" : "", className, ] .filter(Boolean) .join(" "); // ----------------------------- // Interactive 모드 // ----------------------------- if (!isReadOnly) { const handleClick = function (next) { if (safeDisabled) return; if (typeof onValueChange === "function") onValueChange(next); }; const handleSliderChange = function (e) { if (safeDisabled) return; const next = Math.max( 0, Math.min(safeMax, Math.round(Number(e.target.value) || 0)) ); if (typeof onValueChange === "function") onValueChange(next); }; const stars = []; for (let i = 0; i < safeMax; i++) { const filled = i < safeValueInt; stars.push( ); } return ( {stars} ); } // ----------------------------- // ReadOnly — full / iconOnly // ----------------------------- if (safeVariant === "full" || safeVariant === "iconOnly") { const stars = []; for (let i = 0; i < safeMax; i++) { const filled = i < safeValueInt; stars.push( ); } return ( {safeVariant === "full" ? ( ) : null} ); } // ----------------------------- // ReadOnly — compact (별 1개 + 값) // ----------------------------- return ( ); } window.Rating = Rating; Object.assign(window, { Rating });