uxtry

cp*: 배너 슬라이드 본문

★ Vue 컴포넌트 (cp*:)

cp*: 배너 슬라이드

uxtry 2024. 10. 30. 17:42

아래는 Vue에서 좌우 이동과 마우스 드래그가 가능한 배너 슬라이드 컴포넌트를 작성한 코드입니다. 각 슬라이드 이미지는 드래그 및 클릭으로 제어할 수 있으며, 코드에 있는 내용을 그대로 복사해 .vue 파일로 저장하면 작동합니다.

 

 

Slider.vue

<template>
  <div
    class="banner-slider"
    @mousedown="startDrag"
    @mousemove="onDrag"
    @mouseup="endDrag"
    @mouseleave="endDrag"
    @touchstart="startDrag"
    @touchmove="onDrag"
    @touchend="endDrag"
  >
    <!-- 슬라이드 이미지들 -->
    <div
      class="slides"
      :style="{ transform: `translateX(-${(currentIndex + 1) * 100}%)`, transitionDuration: isTransitioning ? '0.3s' : '0s' }"
      @transitionend="handleTransitionEnd"
    >
      <!-- 마지막 슬라이드를 맨 앞으로 복제 -->
      <div class="slide" :style="{ backgroundImage: `url(${slides[slides.length - 1].url})` }">
        <div class="slide-content">
          <h2>{{ slides[slides.length - 1].title }}</h2>
          <p>{{ slides[slides.length - 1].text }}</p>
        </div>
      </div>
      <!-- 원래 슬라이드들 -->
      <div
        v-for="(slide, index) in slides"
        :key="index"
        class="slide"
        :style="{ backgroundImage: `url(${slide.url})` }"        
      >
        <div class="slide-content" @click="goToLink(slide.link)" >
          <h2>{{ slide.title }}</h2>
          <p>{{ slide.text }}</p>
        </div>
      </div>
      <!-- 첫 번째 슬라이드를 맨 뒤로 복제 -->
      <div class="slide" :style="{ backgroundImage: `url(${slides[0].url})` }">
        <div class="slide-content">
          <h2>{{ slides[0].title }}</h2>
          <p>{{ slides[0].text }}</p>
        </div>
      </div>
    </div>

    <!-- 좌우 이동 버튼 -->
    <button @click="prevSlide" class="prev-btn">
      <i class="fa fa-chevron-left" aria-hidden="true"></i>
    </button>
    <button @click="nextSlide" class="next-btn">
      <i class="fa fa-chevron-right" aria-hidden="true"></i>
    </button>

    <!-- 하단 인디케이터 -->
    <div class="indicators">
      <span
        v-for="(slide, index) in slides"
        :key="index"
        :class="{ active: index === currentIndex }"
        @click="goToSlide(index)"
      ></span>
    </div>

    <!-- 하단 썸네일 목록 -->
    <div class="thumbnails">
      <div
        v-for="(slide, index) in slides"
        :key="index"
        class="thumbnail"
        :class="{ active: index === currentIndex }"
        :style="{ backgroundImage: `url(${slide.url})` }"
        @click="goToSlide(index)"
      ></div>
    </div>
  </div>
</template>

<script>
import { ref, onMounted, onBeforeUnmount } from "vue";

export default {
  name: "Slider",
  props: {
    slides: {
      type: Array,
      required: true, // 부모 컴포넌트에서 슬라이드 데이터 전달
    },
  },
  setup(props) {
    const currentIndex = ref(0);
    const isDragging = ref(false);
    const startX = ref(0);
    const currentTranslate = ref(0);
    const prevTranslate = ref(0);
    const isTransitioning = ref(false);
    let intervalId = null;

    const prevSlide = () => {
      isTransitioning.value = true;
      currentIndex.value = currentIndex.value - 1;
    };

    const nextSlide = () => {
      isTransitioning.value = true;
      currentIndex.value = currentIndex.value + 1;
    };

    const goToSlide = (index) => {
      isTransitioning.value = true;
      currentIndex.value = index;
    };

    const goToLink = (link) => {
      if (link) {
        //window.location.href = link;  // 링크로 이동
        window.open(link); 
      }
    };    

    const startAutoSlide = () => {
      intervalId = setInterval(nextSlide, 3000);
    };

    const stopAutoSlide = () => {
      clearInterval(intervalId);
    };

    const handleTransitionEnd = () => {
      isTransitioning.value = false;
      if (currentIndex.value < 0) {
        currentIndex.value = props.slides.length - 1;
      } else if (currentIndex.value >= props.slides.length) {
        currentIndex.value = 0;
      }
    };

    const startDrag = (e) => {
      stopAutoSlide();
      isDragging.value = true;
      startX.value = e.type.includes("mouse") ? e.pageX : e.touches[0].clientX;
      prevTranslate.value = currentTranslate.value;
    };

    const onDrag = (e) => {
      if (!isDragging.value) return;
      const currentPosition = e.type.includes("mouse")
        ? e.pageX
        : e.touches[0].clientX;
      currentTranslate.value =
        prevTranslate.value + currentPosition - startX.value;
    };

    const endDrag = () => {
      if (!isDragging.value) return;
      isDragging.value = false;
      const movedBy = currentTranslate.value - prevTranslate.value;
      if (movedBy < -100) nextSlide();
      else if (movedBy > 100) prevSlide();
      currentTranslate.value = 0;
      prevTranslate.value = 0;
      startAutoSlide();
    };

    onMounted(() => {
      startAutoSlide();
    });

    onBeforeUnmount(() => {
      stopAutoSlide();
    });

    return {
      currentIndex,
      isTransitioning,
      prevSlide,
      nextSlide,
      goToSlide,
      goToLink, 
      startDrag,
      onDrag,
      endDrag,
      handleTransitionEnd,
    };
  },
};
</script>

<style scoped>
.banner-slider {
  position: relative;
  width: 800px;
  height: 400px;
  overflow: hidden;
  margin: auto;
  cursor: grab;
}

.slides {
  display: flex;
  height: 100%;
  transition: transform 0.3s ease;
}

.slide {
  min-width: 100%;
  height: 100%;
  background-size: cover;
  background-position: center;
  position: relative;
}

.slide-content {
  position: absolute;
  top: 20px;
  left: 20px;
  color: white;
  text-shadow: 1px 1px 5px rgba(0, 0, 0, 0.7);
  z-index: 2;
}

.slide-content h2 {
  font-size: 24px;
  margin-bottom: 10px;
}

.slide-content p {
  font-size: 16px;
}

.prev-btn,
.next-btn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(0, 0, 0, 0.5);
  color: white;
  border: none;
  padding: 10px;
  cursor: pointer;
  z-index: 1;
}

.prev-btn {
  left: 10px;
}

.next-btn {
  right: 10px;
}

.indicators {
  position: absolute;
  bottom: 70px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
}

.indicators span {
  width: 10px;
  height: 10px;
  background: rgba(255, 255, 255, 0.5);
  margin: 0 5px;
  border-radius: 50%;
  cursor: pointer;
  transition: background 0.3s;
}

.indicators .active {
  background: white;
}

.thumbnails {
  display: flex;
  justify-content: center;
  position: absolute;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
}

.thumbnail {
  width: 60px;
  height: 40px;
  background-size: cover;
  background-position: center;
  margin: 0 5px;
  cursor: pointer;
  border: 2px solid transparent;
  transition: border 0.3s;
}

.thumbnail.active {
  border-color: white;
}
</style>

 

App.vue

<template>
  <div>
    <h1>Vue 3 Slider</h1>
    <Slider :slides="slides" />
  </div>
</template>

<script>
import Slider from "./Slider.vue";

export default {
  components: {
    Slider,
  },
  data() {
    return {
      slides: [
        {
          url: "https://picsum.photos/800/400/?image=1",
          title: "First Slide",
          text: "This is the first slide description.",
          link: 'https://www.example1.com',
        },
        {
          url: "https://picsum.photos/800/400/?image=2",
          title: "Second Slide",
          text: "This is the second slide description.",
          link: 'https://www.example2.com',
        },
        {
          url: "https://picsum.photos/800/400/?image=3",
          title: "Third Slide",
          text: "This is the third slide description.",
          link: 'https://www.example3.com',
        },
      ],
    };
  },
};
</script>

<style>
@import "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css"
</style>


<style>
@import "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css"
</style>

 

★ Vue SFC Playground 코드 실행 →

코드 설명

1. 슬라이드 데이터: slides 배열에 각 슬라이드 이미지의 URL을 저장합니다.

2. 좌우 버튼: prevSlidenextSlide 함수를 통해 이전/다음 슬라이드로 이동할 수 있습니다.

3. 드래그 이벤트

  • startDrag: 드래그 시작 시 호출, 드래그 시작 위치를 설정합니다.
  • onDrag: 드래그 중일 때 위치를 계산하여 슬라이드를 이동합니다.
  • endDrag: 드래그가 끝나면 이동 거리에 따라 다음 또는 이전 슬라이드로 이동합니다.

4. 자동 슬라이드:

  • startAutoSlide 함수에서 setInterval을 사용하여 3초마다 nextSlide 함수를 호출해 자동으로 슬라이드가 넘어갑니다.
  • stopAutoSlide는 드래그 또는 클릭 시 자동 슬라이드를 중단하고, 사용자가 슬라이드를 드래그로 제어한 뒤에는 다시 startAutoSlide로 재개됩니다.

5. 인디케이터:

  • 하단 indicators div에 각 슬라이드에 해당하는 인디케이터가 표시됩니다.
  • 현재 슬라이드 위치에 따라 active 클래스를 적용해 인디케이터의 스타일이 변경됩니다.
  • 인디케이터를 클릭 시 해당 슬라이드로 이동합니다.

6. 썸네일 추가:

  • thumbnails div에서 각 슬라이드에 해당하는 썸네일 이미지를 표시합니다.
  • 썸네일 이미지가 현재 활성 슬라이드와 일치할 때 active 클래스가 추가되어 테두리가 강조됩니다.
  • 썸네일을 클릭하면 해당 슬라이드로 이동하도록 goToSlide 메서드를 사용합니다.

7. 슬라이드 배열 구조:

  • slides 배열의 각 항목은 이미지 URL을 포함한 객체 형태로 수정되었습니다. 이를 통해 향후 추가 정보나 속성을 슬라이드에 더 쉽게 추가할 수 있습니다.

8. 가상 슬라이드 추가:

  • 원래 slides 배열의 첫 번째와 마지막 슬라이드를 각각 맨 뒤와 맨 앞으로 복제하여, 실제로 첫 번째 슬라이드와 마지막 슬라이드로 자연스럽게 넘어가도록 했습니다.
  • handleTransitionEnd 함수: 슬라이드 이동이 끝난 후 가상 슬라이드에서 실제 슬라이드로 인덱스를 재설정해 무한 루프를 완성했습니다.

 

 

'★ Vue 컴포넌트 (cp*:)' 카테고리의 다른 글

cp*: 알림창(alert)  (0) 2024.11.29
cp*: 레이어팝업  (0) 2024.10.31