SwiperBanner4.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. <template>
  2. <section class="swiper--banner--wrapper2" :data-type="type" :data-fit="fit">
  3. <div class="swiper--banner--container">
  4. <div class="type--connection lincoln" :class="type" v-if="type == 'exterior'">
  5. <svg
  6. xmlns="http://www.w3.org/2000/svg"
  7. width="30"
  8. height="30"
  9. viewBox="0 0 30 30"
  10. fill="none"
  11. >
  12. <path
  13. d="M15.1354 5H20.9482C21.5223 5 22.034 5.35992 22.2251 5.89813L25.1563 12.8077M25.1563 12.8077C26.1042 12.9423 28 13.6962 28 15.6346V16.8462M25.1563 12.8077H15.1354M25.1563 12.8077L27.7292 10.9231M28 16.8462V20.3462M28 16.8462L23.3958 18.5962M28 20.3462V23.8462C27.9097 24.5641 27.3229 26 25.6979 26C23.6674 26 23.396 25.3274 22.0431 22.503M28 20.3462C26.8663 21.4731 24.5199 22.1343 22.0431 22.503M22.0431 22.503C22.0426 22.502 22.0421 22.501 22.0417 22.5M22.0431 22.503C19.5496 22.8742 16.924 22.9489 15.2708 22.9038H14.7292C13.076 22.9489 10.4504 22.8742 7.9569 22.503M19.3333 22.5V19C19.3333 18.7026 19.0908 18.4615 18.7917 18.4615H11.2083C10.9092 18.4615 10.6667 18.7026 10.6667 19V22.5M14.8646 5H9.05184C8.4777 5 7.96596 5.35992 7.77488 5.89813L4.84375 12.8077M4.84375 12.8077C3.89583 12.9423 2 13.6962 2 15.6346V16.8462M4.84375 12.8077H14.8646M4.84375 12.8077L2.27083 10.9231M2 16.8462V20.3462M2 16.8462L6.60417 18.5962M2 20.3462V23.8462C2.09028 24.5641 2.67708 26 4.30208 26C6.33261 26 6.60398 25.3274 7.9569 22.503M2 20.3462C3.13367 21.4731 5.48012 22.1343 7.9569 22.503M7.9569 22.503C7.95738 22.502 7.95785 22.501 7.95833 22.5"
  14. stroke="white"
  15. stroke-width="2"
  16. stroke-linecap="round"
  17. /></svg
  18. >{{ type }}
  19. </div>
  20. <div class="type--connection lincoln" :class="type" v-else-if="type == 'interior'">
  21. <svg
  22. xmlns="http://www.w3.org/2000/svg"
  23. width="30"
  24. height="30"
  25. viewBox="0 0 30 30"
  26. fill="none"
  27. >
  28. <path
  29. d="M2.30762 22.3072H13.8931M20.7409 22.3072H26.6922C27.2445 22.3072 27.6922 21.8595 27.6922 21.3072V18.0765M2.30762 3.8457C2.78804 3.8457 4.52124 3.8457 7.61071 3.8457C11.4725 3.8457 13.8931 6.92263 16.8791 9.61493C19.2679 11.7688 21.2214 12.3072 21.8995 12.3072C23.4442 12.3072 25.9288 12.5214 27.0908 14.9995M2.30762 6.92263H7.61071C8.12562 6.92263 9.46439 7.1534 10.7002 8.07647C11.7257 8.84247 13.8858 10.8974 15.3269 12.3072M2.30762 19.6149C4.84768 19.6149 10.1595 19.6149 11.0864 19.6149C12.05 19.6149 12.2121 19.0828 12.6838 18.4611M6.45216 13.8457L7.8038 11.9226M9.15544 9.99955L7.8038 11.9226M7.8038 11.9226L9.92781 13.4611M9.92781 13.4611C10.1853 13.0765 10.9319 12.3072 11.8587 12.3072C12.7856 12.3072 14.557 12.3072 15.3269 12.3072M9.92781 13.4611C9.30991 14.3842 9.67035 15.3842 9.92781 15.7688L12.6838 18.4611M15.3269 12.3072C15.6224 12.5963 15.8877 12.8583 16.1067 13.0765C16.3642 13.3329 16.8791 13.9995 16.8791 14.6149C16.8791 15.3842 16.8791 16.2288 15.7206 16.538C14.562 16.8472 13.7896 17.3072 13.0173 18.0765C12.8873 18.2059 12.7792 18.3353 12.6838 18.4611M27.6922 18.0765C27.6922 16.8049 27.464 15.7955 27.0908 14.9995M27.6922 18.0765H24.6345C23.124 18.0765 21.8995 16.852 21.8995 15.3414C21.8995 15.1526 22.0526 14.9995 22.2414 14.9995H27.0908"
  30. stroke="white"
  31. stroke-width="2"
  32. stroke-linecap="round"
  33. />
  34. <path
  35. d="M17.2653 20.2305C18.6364 20.2305 19.7408 21.3367 19.7408 22.6924C19.7406 24.0479 18.6362 25.1533 17.2653 25.1533C15.8943 25.1533 14.7899 24.0478 14.7897 22.6924C14.7897 21.3368 15.8942 20.2305 17.2653 20.2305Z"
  36. stroke="white"
  37. stroke-width="2"
  38. stroke-linecap="round"
  39. /></svg
  40. >{{ type }}
  41. </div>
  42. <!-- Pagination with Progress and Fractions -->
  43. <div class="swiper--pagination--wrapper">
  44. <div class="swiper-pagination" ref="paginationRef"></div>
  45. <div class="swiper--progressbar" ref="progressRef">
  46. <div class="swiper--progressbar-fill" :style="{ width: progressWidth }"></div>
  47. </div>
  48. <div class="swiper--fraction" ref="fractionRef">
  49. <span class="current">{{ currentSlide + 1 }}</span>
  50. <span class="separator">/</span>
  51. <span class="total">{{ slides.length }}</span>
  52. </div>
  53. </div>
  54. <!-- 70% 영역: 단일 배너 -->
  55. <div class="swiper--banner--section">
  56. <div class="swiper--container" ref="swiperContainer">
  57. <div class="swiper-wrapper">
  58. <div v-for="(slide, index) in slides" :key="index" class="swiper-slide">
  59. <div class="slide--image" v-if="slide.image">
  60. <img
  61. :src="slide.image"
  62. :alt="slide.alt || 'Banner Image'"
  63. loading="lazy"
  64. />
  65. <!-- 확대 버튼 -->
  66. <button class="zoom--btn" @click="openLightbox(index)" aria-label="확대">
  67. Image Details
  68. <i>
  69. <svg
  70. xmlns="http://www.w3.org/2000/svg"
  71. width="12"
  72. height="12"
  73. viewBox="0 0 12 12"
  74. fill="none"
  75. >
  76. <path
  77. d="M2.5 6H9.5"
  78. stroke="white"
  79. stroke-width="1.5"
  80. stroke-linecap="round"
  81. stroke-linejoin="round"
  82. />
  83. <path
  84. d="M6 2.5L9.5 6L6 9.5"
  85. stroke="white"
  86. stroke-width="1.5"
  87. stroke-linecap="round"
  88. stroke-linejoin="round"
  89. />
  90. </svg>
  91. </i>
  92. </button>
  93. </div>
  94. <div class="slide--image" v-if="slide.video">
  95. <video
  96. autoplay
  97. muted
  98. loop
  99. playsinline
  100. :poster="slide.video"
  101. controlslist="nodownload noplaybackrate"
  102. disablepictureinpicture
  103. preload="metadata"
  104. >
  105. <source media="(min-width:320px)" :src="slide.video" type="video/mp4" />
  106. </video>
  107. </div>
  108. </div>
  109. </div>
  110. </div>
  111. </div>
  112. </div>
  113. <!-- Lightbox -->
  114. <Transition name="lightbox-fade">
  115. <div v-if="isLightboxOpen" class="lightbox--overlay" @click.self="closeLightbox">
  116. <button class="lightbox--close" @click="closeLightbox" aria-label="닫기">
  117. <svg
  118. xmlns="http://www.w3.org/2000/svg"
  119. width="32"
  120. height="32"
  121. viewBox="0 0 32 32"
  122. fill="none"
  123. >
  124. <path
  125. d="M24 8L8 24M8 8L24 24"
  126. stroke="white"
  127. stroke-width="2"
  128. stroke-linecap="round"
  129. />
  130. </svg>
  131. </button>
  132. <button class="lightbox--prev" @click="prevLightboxSlide" aria-label="이전">
  133. <svg
  134. xmlns="http://www.w3.org/2000/svg"
  135. width="40"
  136. height="40"
  137. viewBox="0 0 40 40"
  138. fill="none"
  139. >
  140. <path
  141. d="M25 30L15 20L25 10"
  142. stroke="white"
  143. stroke-width="3"
  144. stroke-linecap="round"
  145. stroke-linejoin="round"
  146. />
  147. </svg>
  148. </button>
  149. <div class="lightbox--content">
  150. <div class="lightbox--swiper" ref="lightboxSwiperContainer">
  151. <div class="swiper-wrapper">
  152. <div
  153. v-for="(slide, index) in slides"
  154. :key="`lightbox-${index}`"
  155. class="swiper-slide"
  156. >
  157. <img
  158. v-if="slide.image"
  159. :src="slide.image"
  160. :alt="slide.alt || 'Banner Image'"
  161. />
  162. </div>
  163. </div>
  164. </div>
  165. <!-- Lightbox Pagination Info -->
  166. <div class="lightbox--info">
  167. <span class="lightbox--fraction">
  168. {{ lightboxCurrentSlide + 1 }} / {{ slides.length }}
  169. </span>
  170. </div>
  171. </div>
  172. <button class="lightbox--next" @click="nextLightboxSlide" aria-label="다음">
  173. <svg
  174. xmlns="http://www.w3.org/2000/svg"
  175. width="40"
  176. height="40"
  177. viewBox="0 0 40 40"
  178. fill="none"
  179. >
  180. <path
  181. d="M15 30L25 20L15 10"
  182. stroke="white"
  183. stroke-width="3"
  184. stroke-linecap="round"
  185. stroke-linejoin="round"
  186. />
  187. </svg>
  188. </button>
  189. </div>
  190. </Transition>
  191. </section>
  192. </template>
  193. <script setup>
  194. import { Swiper } from "swiper";
  195. import { Navigation, Pagination, Autoplay, EffectFade } from "swiper/modules";
  196. import "swiper/css";
  197. import "swiper/css/navigation";
  198. import "swiper/css/pagination";
  199. import "swiper/css/effect-fade";
  200. const { getMediaUrl } = useImage();
  201. // Props 정의
  202. const props = defineProps({
  203. slides: {
  204. type: Array,
  205. default: () => [],
  206. },
  207. height: {
  208. type: String,
  209. default: "60%",
  210. },
  211. title: {
  212. type: String,
  213. default: "",
  214. },
  215. subtitle: {
  216. type: String,
  217. default: "",
  218. },
  219. smalldesc: {
  220. type: String,
  221. default: "",
  222. },
  223. cautiondesc: {
  224. type: String,
  225. default: "",
  226. },
  227. morelink: {
  228. type: String,
  229. default: "",
  230. },
  231. morelinktitle: {
  232. type: String,
  233. default: "자세히 보기",
  234. },
  235. morelinktarget: {
  236. type: String,
  237. default: "_self",
  238. },
  239. notice: {
  240. type: String,
  241. default: "",
  242. },
  243. autoplay: {
  244. type: [Boolean, Object],
  245. default: () => ({ delay: 5000 }),
  246. },
  247. loop: {
  248. type: Boolean,
  249. default: true,
  250. },
  251. type: {
  252. type: String,
  253. default: "", // 'horz' , 'vert'
  254. validator: (value) => ["exterior", "interior"].includes(value),
  255. },
  256. fit: {
  257. type: String,
  258. default: "cover",
  259. validator: (value) => ["cover", "contain"].includes(value),
  260. },
  261. });
  262. // Refs
  263. const swiperContainer = ref(null);
  264. const lightboxSwiperContainer = ref(null);
  265. const paginationRef = ref(null);
  266. const progressRef = ref(null);
  267. const fractionRef = ref(null);
  268. const prevRef = ref(null);
  269. const nextRef = ref(null);
  270. const textSlider = ref(null);
  271. let swiperInstance = null;
  272. let lightboxSwiperInstance = null;
  273. // 현재 슬라이드 인덱스
  274. const currentSlide = ref(0);
  275. const lightboxCurrentSlide = ref(0);
  276. const progressWidth = ref("0%");
  277. // 라이트박스 상태
  278. const isLightboxOpen = ref(false);
  279. // 슬라이드별 텍스트 반환 함수들
  280. const getSlideTitle = (index) => {
  281. return props.slides[index]?.title || props.title;
  282. };
  283. const getSlideSubtitle = (index) => {
  284. return props.slides[index]?.subtitle || props.subtitle;
  285. };
  286. const getSlideSmalldesc = (index) => {
  287. return props.slides[index]?.smalldesc || props.smalldesc;
  288. };
  289. const getSlideMorelink = (index) => {
  290. return props.slides[index]?.morelink || props.morelink;
  291. };
  292. const getSlideMorelinktitle = (index) => {
  293. return props.slides[index]?.morelinktitle || props.morelinktitle;
  294. };
  295. const getSlideTarget = (index) => {
  296. return props.slides[index]?.morelinktarget || props.morelinktarget;
  297. };
  298. const getSlideCautiondesc = (index) => {
  299. return props.slides[index]?.cautiondesc || props.cautiondesc;
  300. };
  301. // Progress bar 업데이트 함수
  302. const updateProgress = () => {
  303. const progress = ((currentSlide.value + 1) / props.slides.length) * 100;
  304. progressWidth.value = `${progress}%`;
  305. };
  306. // 라이트박스 열기
  307. const openLightbox = (index) => {
  308. isLightboxOpen.value = true;
  309. document.body.style.overflow = "hidden";
  310. document.querySelector(".g-layout").classList.add("active");
  311. nextTick(() => {
  312. initLightboxSwiper(index);
  313. });
  314. };
  315. // 라이트박스 닫기
  316. const closeLightbox = () => {
  317. isLightboxOpen.value = false;
  318. document.body.style.overflow = "";
  319. document.querySelector(".g-layout").classList.remove("active");
  320. if (lightboxSwiperInstance) {
  321. lightboxSwiperInstance.destroy(true, true);
  322. lightboxSwiperInstance = null;
  323. }
  324. };
  325. // 라이트박스 Swiper 초기화
  326. const initLightboxSwiper = (initialIndex = 0) => {
  327. if (!lightboxSwiperContainer.value) return;
  328. lightboxSwiperInstance = new Swiper(lightboxSwiperContainer.value, {
  329. modules: [Navigation],
  330. slidesPerView: 1,
  331. spaceBetween: 0,
  332. initialSlide: initialIndex,
  333. loop: false,
  334. keyboard: {
  335. enabled: true,
  336. },
  337. on: {
  338. slideChange: function () {
  339. lightboxCurrentSlide.value = this.activeIndex;
  340. // 메인 스와이퍼도 동일한 인덱스로 이동
  341. if (swiperInstance && !swiperInstance.destroyed) {
  342. swiperInstance.slideToLoop(this.activeIndex, 0);
  343. }
  344. },
  345. init: function () {
  346. lightboxCurrentSlide.value = this.activeIndex;
  347. },
  348. },
  349. });
  350. };
  351. // 라이트박스 이전 슬라이드
  352. const prevLightboxSlide = () => {
  353. if (lightboxSwiperInstance) {
  354. lightboxSwiperInstance.slidePrev();
  355. }
  356. };
  357. // 라이트박스 다음 슬라이드
  358. const nextLightboxSlide = () => {
  359. if (lightboxSwiperInstance) {
  360. lightboxSwiperInstance.slideNext();
  361. }
  362. };
  363. // ESC 키로 라이트박스 닫기
  364. const handleKeydown = (e) => {
  365. if (e.key === "Escape" && isLightboxOpen.value) {
  366. closeLightbox();
  367. }
  368. };
  369. onMounted(() => {
  370. // 키보드 이벤트 리스너 추가
  371. window.addEventListener("keydown", handleKeydown);
  372. // Swiper 인스턴스 초기화
  373. swiperInstance = new Swiper(swiperContainer.value, {
  374. modules: [Navigation, Pagination, Autoplay],
  375. slidesPerView: 1,
  376. spaceBetween: 0,
  377. loop: props.loop,
  378. autoplay: props.autoplay
  379. ? {
  380. delay:
  381. typeof props.autoplay === "object" ? props.autoplay.delay || 5000 : 5000,
  382. disableOnInteraction: false,
  383. pauseOnMouseEnter: true,
  384. }
  385. : false,
  386. navigation: {
  387. nextEl: nextRef.value,
  388. prevEl: prevRef.value,
  389. },
  390. pagination: {
  391. el: paginationRef.value,
  392. clickable: true,
  393. type: "bullets",
  394. // renderBullet: function (index, className) {
  395. // return '<span class="' + className + '">' + (index + 1) + "</span>";
  396. // },
  397. },
  398. effect: "fade",
  399. fadeEffect: {
  400. crossFade: true,
  401. },
  402. speed: 800,
  403. // 슬라이드 변경 이벤트
  404. on: {
  405. slideChange: function () {
  406. if (this && this.realIndex !== undefined) {
  407. currentSlide.value = this.realIndex;
  408. }
  409. // Progress bar 업데이트
  410. updateProgress();
  411. // 모든 비디오 일시정지
  412. const allVideos = this.el.querySelectorAll("video");
  413. allVideos.forEach((video) => {
  414. video.pause();
  415. video.currentTime = 0;
  416. });
  417. // 현재 슬라이드의 비디오 재생
  418. const currentSlideEl = this.slides[this.activeIndex];
  419. if (currentSlideEl) {
  420. const video = currentSlideEl.querySelector("video");
  421. if (video) {
  422. video.play().catch((err) => console.log("Video play failed:", err));
  423. }
  424. }
  425. },
  426. init: function () {
  427. if (this && this.realIndex !== undefined) {
  428. currentSlide.value = this.realIndex;
  429. }
  430. // 초기 Progress bar 업데이트 - nextTick으로 지연
  431. nextTick(() => {
  432. updateProgress();
  433. });
  434. // 초기 슬라이드의 비디오 재생
  435. const currentSlideEl = this.slides[this.activeIndex];
  436. if (currentSlideEl) {
  437. const video = currentSlideEl.querySelector("video");
  438. if (video) {
  439. video.play().catch((err) => console.log("Video play failed:", err));
  440. }
  441. }
  442. },
  443. },
  444. });
  445. });
  446. onUnmounted(() => {
  447. // 키보드 이벤트 리스너 제거
  448. window.removeEventListener("keydown", handleKeydown);
  449. // Swiper 인스턴스 정리
  450. if (swiperInstance) {
  451. swiperInstance.destroy(true, true);
  452. }
  453. if (lightboxSwiperInstance) {
  454. lightboxSwiperInstance.destroy(true, true);
  455. }
  456. // body overflow 복원
  457. document.body.style.overflow = "";
  458. });
  459. </script>