/* ============================================================================
* 서명전에 — 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(
<__RatingStarSvg />
);
}
return (
{stars}
{safeVariant === "full" ? (
{safeValueInt}
) : null}
);
}
// -----------------------------
// ReadOnly — compact (별 1개 + 값)
// -----------------------------
return (
<__RatingStarSvg />
{safeValueInt}
);
}
window.Rating = Rating;
Object.assign(window, { Rating });