RankingTabs.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. 'use client';
  2. import { useCallback, useEffect, useRef } from 'react';
  3. import Link from 'next/link';
  4. import { Trophy, Users, Heart, TrendingUp, Crown, ChevronLeft, ChevronRight } from 'lucide-react';
  5. import useDragScroll from '@/hooks/useDragScroll';
  6. type RankingTabKey = 'comprehensive'|'creator'|'donor'|'rising'|'hall-of-fame';
  7. interface Props {
  8. active: RankingTabKey;
  9. }
  10. const SCROLL_AMOUNT = 120;
  11. const TABS: { key: RankingTabKey; href: string; label: string; icon: React.ElementType }[] = [
  12. { key: 'hall-of-fame', href: '/ranking/hall-of-fame', label: '명예의 전당', icon: Crown },
  13. { key: 'comprehensive', href: '/ranking', label: '종합', icon: Trophy },
  14. { key: 'creator', href: '/ranking/creator', label: '크리에이터', icon: Users },
  15. { key: 'donor', href: '/ranking/donor', label: '후원자', icon: Heart },
  16. { key: 'rising', href: '/ranking/rising', label: '급상승', icon: TrendingUp }
  17. ];
  18. export default function RankingTabs({ active }: Props) {
  19. const dragScroll = useDragScroll<HTMLElement>();
  20. const activeItemRef = useRef<HTMLAnchorElement>(null);
  21. // 활성 탭(active) 변경 시 뷰포트 중앙으로 스크롤해 모바일에서 현재 위치 가시화
  22. useEffect(() => {
  23. const target = activeItemRef.current;
  24. if (!target) {
  25. return;
  26. }
  27. const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  28. target.scrollIntoView({
  29. behavior: prefersReduced ? 'auto' : 'smooth',
  30. inline: 'center',
  31. block: 'nearest'
  32. });
  33. }, [active]);
  34. const scroll = useCallback((dir: 'left'|'right') => {
  35. const el = dragScroll.ref.current;
  36. if (!el) {
  37. return;
  38. }
  39. const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  40. el.scrollBy({
  41. left: dir === 'left' ? -SCROLL_AMOUNT : SCROLL_AMOUNT,
  42. behavior: prefersReduced ? 'auto' : 'smooth'
  43. });
  44. }, [dragScroll.ref]);
  45. return (
  46. <div className="ranking__tabs-wrap">
  47. <button
  48. type="button"
  49. className="ranking__tabs-arrow ranking__tabs-arrow--left"
  50. onClick={() => scroll('left')}
  51. aria-label="이전 탭으로 스크롤"
  52. >
  53. <ChevronLeft size={18} />
  54. </button>
  55. <nav
  56. className="ranking__tabs"
  57. aria-label="순위 탭"
  58. ref={dragScroll.ref}
  59. onMouseDown={dragScroll.onMouseDown}
  60. onMouseMove={dragScroll.onMouseMove}
  61. onMouseUp={dragScroll.onMouseUp}
  62. onMouseLeave={dragScroll.onMouseLeave}
  63. >
  64. {TABS.map((tab) => {
  65. const Icon = tab.icon;
  66. const isActive = tab.key === active;
  67. return (
  68. <Link
  69. key={tab.key}
  70. href={tab.href}
  71. className={`ranking__tabs-item ${isActive ? 'ranking__tabs-item--active' : ''}`}
  72. aria-current={isActive ? 'page' : undefined}
  73. ref={isActive ? activeItemRef : undefined}
  74. draggable={false}
  75. >
  76. <Icon size={18} />
  77. <span>{tab.label}</span>
  78. </Link>
  79. );
  80. })}
  81. </nav>
  82. <button
  83. type="button"
  84. className="ranking__tabs-arrow ranking__tabs-arrow--right"
  85. onClick={() => scroll('right')}
  86. aria-label="다음 탭으로 스크롤"
  87. >
  88. <ChevronRight size={18} />
  89. </button>
  90. </div>
  91. );
  92. }