HomeBannerCarousel.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. 'use client';
  2. import { useEffect, useState } from 'react';
  3. import Autoplay from 'embla-carousel-autoplay';
  4. import {
  5. Carousel,
  6. CarouselApi,
  7. CarouselContent,
  8. CarouselItem,
  9. CarouselNext,
  10. CarouselPrevious,
  11. } from '@/components/ui/carousel';
  12. import type { BannerItem } from '@/types/response/banner/active-list';
  13. type Props = {
  14. items: BannerItem[];
  15. };
  16. export default function HomeBannerCarousel({ items }: Props)
  17. {
  18. const [api, setApi] = useState<CarouselApi|null>(null);
  19. const [selectedIndex, setSelectedIndex] = useState(0);
  20. const [slideCount, setSlideCount] = useState(0);
  21. useEffect(() => {
  22. if (!api) {
  23. return;
  24. }
  25. setSlideCount(api.scrollSnapList().length);
  26. setSelectedIndex(api.selectedScrollSnap());
  27. const handleSelect = () => {
  28. setSelectedIndex(api.selectedScrollSnap());
  29. };
  30. api.on('select', handleSelect);
  31. api.on('reInit', handleSelect);
  32. return () => {
  33. api.off('select', handleSelect);
  34. api.off('reInit', handleSelect);
  35. };
  36. }, [api]);
  37. if (items.length === 0) {
  38. return null;
  39. }
  40. return (
  41. <section
  42. className="home__banner"
  43. aria-label="프로모션 배너"
  44. >
  45. <Carousel
  46. setApi={setApi}
  47. opts={{ loop: items.length > 1, align: 'start' }}
  48. plugins={items.length > 1 ? [Autoplay({ delay: 5000, stopOnInteraction: false, stopOnMouseEnter: true })] : []}
  49. className="relative"
  50. >
  51. <CarouselContent className="-ml-0">
  52. {items.map((banner, idx) => (
  53. <CarouselItem key={banner.id} className="pl-0 basis-full">
  54. <BannerSlide banner={banner} index={idx} total={items.length} />
  55. </CarouselItem>
  56. ))}
  57. </CarouselContent>
  58. {items.length > 1 && (
  59. <>
  60. <CarouselPrevious
  61. className="left-3 top-1/2 -translate-y-1/2 size-10 bg-black/40 hover:bg-black/60 border-0 text-white"
  62. aria-label="이전 배너"
  63. />
  64. <CarouselNext
  65. className="right-3 top-1/2 -translate-y-1/2 size-10 bg-black/40 hover:bg-black/60 border-0 text-white"
  66. aria-label="다음 배너"
  67. />
  68. <div
  69. className="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-2 z-10"
  70. role="tablist"
  71. aria-label="배너 인디케이터"
  72. >
  73. {Array.from({ length: slideCount }).map((_, idx) => (
  74. <button
  75. key={idx}
  76. type="button"
  77. onClick={() => api?.scrollTo(idx)}
  78. aria-label={`${idx + 1}번째 배너로 이동`}
  79. aria-selected={idx === selectedIndex}
  80. role="tab"
  81. className={`h-2 rounded-full transition-all ${idx === selectedIndex ? 'w-6 bg-white' : 'w-2 bg-white/50 hover:bg-white/70'}`}
  82. />
  83. ))}
  84. </div>
  85. </>
  86. )}
  87. </Carousel>
  88. </section>
  89. );
  90. }
  91. type SlideProps = {
  92. banner: BannerItem;
  93. index: number;
  94. total: number;
  95. };
  96. function BannerSlide({ banner, index, total }: SlideProps)
  97. {
  98. const desktop = banner.desktopImage;
  99. const mobile = banner.mobileImage ?? banner.desktopImage;
  100. const content = (
  101. <picture className="home__banner-picture">
  102. {mobile && (
  103. <source media="(max-width: 768px)" srcSet={mobile} />
  104. )}
  105. {desktop && (
  106. // eslint-disable-next-line @next/next/no-img-element
  107. <img
  108. src={desktop}
  109. alt={banner.subject}
  110. loading={index === 0 ? 'eager' : 'lazy'}
  111. decoding="async"
  112. className="home__banner-image"
  113. />
  114. )}
  115. </picture>
  116. );
  117. const ariaLabel = `${banner.subject} (${index + 1}/${total})`;
  118. if (banner.link) {
  119. return (
  120. <a
  121. href={banner.link}
  122. target={banner.link.startsWith('http') ? '_blank' : undefined}
  123. rel={banner.link.startsWith('http') ? 'noopener noreferrer' : undefined}
  124. aria-label={ariaLabel}
  125. className="home__banner-slide"
  126. >
  127. {content}
  128. </a>
  129. );
  130. }
  131. return (
  132. <div aria-label={ariaLabel} className="home__banner-slide">
  133. {content}
  134. </div>
  135. );
  136. }