#281 (리뷰/메모 UI): - Stars 컴포넌트 신규 (lib 분리 가능한 공통 별점) — 0.5 단위 절반 채우기 시각 구분 - ReviewSection/MemoSection의 StarDisplay 제거 → 공통 Stars 사용 (시각 일관성) - StarSelector: role='radiogroup'/role='radio' + aria-checked, 44×44px 터치 영역, 반쪽 별 '⯨' 표시로 시각 차별화 - ReviewSection/MemoSection: API 실패 try/catch + alert 사용자 피드백 - MyReviewsList: Math.round 별점 → Stars 0.5단위 정확 렌더 #283 (로그인 메뉴): - LoginMenu: useEscapeKey + useFocusTrap + useBodyScrollLock 적용 - role='dialog' / aria-modal / aria-labelledby / aria-label='로그인 창 닫기' - onError 콘솔만 → 인라인 role='alert' 메시지로 사용자 피드백 - max-w-xs → max-w-sm (위젯 260px + 패딩 24px = 308px 안전 수용) 후속 분리: - #343 (next/image + ARIA Tabs + Stars 테스트) - #344 (z-index 토큰 + i18n) Refs: #281 #283
38 lines
1.3 KiB
TypeScript
38 lines
1.3 KiB
TypeScript
// #281 공통 별점 컴포넌트 — ReviewSection/MemoSection/MyReviewsList 재사용.
|
|
// 0.5 단위 시각 구분: 빈 별 위에 황색 절반 별을 절대배치 + clip으로 표시.
|
|
|
|
interface StarsProps {
|
|
rating: number; // 0~5, 0.5 단위
|
|
size?: "sm" | "md";
|
|
showNumber?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
export default function Stars({ rating, size = "sm", showNumber = false, className = "" }: StarsProps) {
|
|
const r = Math.max(0, Math.min(5, rating));
|
|
const textSize = size === "md" ? "text-base" : "text-sm";
|
|
return (
|
|
<span className={`inline-flex items-center gap-0.5 ${textSize} ${className}`} aria-label={`${r}점`}>
|
|
{[1, 2, 3, 4, 5].map((i) => {
|
|
const full = r >= i;
|
|
const half = !full && r >= i - 0.5;
|
|
return (
|
|
<span key={i} className="relative inline-block leading-none">
|
|
<span className="text-gray-300">★</span>
|
|
{(full || half) && (
|
|
<span
|
|
aria-hidden="true"
|
|
className="absolute inset-0 text-yellow-500 overflow-hidden"
|
|
style={{ width: full ? "100%" : "50%" }}
|
|
>
|
|
★
|
|
</span>
|
|
)}
|
|
</span>
|
|
);
|
|
})}
|
|
{showNumber && r > 0 && <span className="text-xs text-yellow-600 font-medium ml-1">{r}</span>}
|
|
</span>
|
|
);
|
|
}
|