"광주 문화예술 인문스토리 플랫폼2"의 두 판 사이의 차이
광주문화예술인문스토리플랫폼
1번째 줄: | 1번째 줄: | ||
{{#tag:html| | {{#tag:html| | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
<!-- 에디터 컨텐츠 등록 영역 --> | <!-- 에디터 컨텐츠 등록 영역 --> | ||
<div class="gwangju-main-wrap"> | <div class="gwangju-main-wrap"> | ||
709번째 줄: | 614번째 줄: | ||
<!-- //AR Heritage 영역 --> | <!-- //AR Heritage 영역 --> | ||
</div> | </div> | ||
− | < | + | <link rel="stylesheet" href="https://unpkg.com/swiper/swiper-bundle.min.css" /> |
− | + | <link | |
− | + | rel="stylesheet" | |
+ | href="https://cdn.jsdelivr.net/npm/keen-slider@6.8.5/keen-slider.min.css" | ||
+ | /> | ||
+ | <script src="https://unpkg.com/swiper/swiper-bundle.min.js"></script> | ||
+ | <script src="https://cdn.jsdelivr.net/npm/keen-slider@6.8.5/keen-slider.min.js"></script> | ||
+ | <script> | ||
+ | let swiperInstance = null; | ||
− | + | const initSwiper = () => { | |
− | + | // 모바일에서만 Swiper 초기화 | |
− | + | if (window.innerWidth < 768) { | |
− | + | swiperInstance = new Swiper('.main-visual-swiper', { | |
− | + | direction: 'horizontal', | |
− | + | slidesPerView: 1, | |
− | + | spaceBetween: 0, | |
− | + | pagination: { | |
− | + | el: '.swiper-fraction', | |
− | + | type: 'bullets', | |
− | + | clickable: true, // Bullet 클릭 가능 | |
− | + | }, | |
− | + | }); | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
} | } | ||
− | } | + | }; |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | const | + | const destroySwiper = () => { |
if (swiperInstance) { | if (swiperInstance) { | ||
swiperInstance.destroy(true, true); | swiperInstance.destroy(true, true); | ||
swiperInstance = null; | swiperInstance = null; | ||
} | } | ||
+ | }; | ||
− | + | // 초기화 | |
− | + | initSwiper(); | |
− | |||
− | |||
− | |||
− | + | // 윈도우 리사이즈 시 Swiper 재설정 | |
− | + | window.addEventListener('resize', () => { | |
− | + | if (window.innerWidth < 768) { | |
− | + | if (!swiperInstance) { | |
− | + | initSwiper(); | |
+ | } | ||
+ | } else if (window.innerWidth >= 768) { | ||
+ | if (swiperInstance) { | ||
+ | destroySwiper(); | ||
+ | } | ||
+ | } | ||
+ | }); | ||
+ | document.addEventListener('DOMContentLoaded', function () { | ||
+ | const storyLinks = document.querySelectorAll('.story-link-item'); | ||
+ | const imageContainer = document.querySelector('.image-scroll-container'); | ||
+ | const breakpoint = window.matchMedia('(max-width: 991px)'); | ||
+ | let swiperInstance; | ||
− | const containerRect = imageContainer.getBoundingClientRect(); | + | const initDesktop = () => { |
− | + | if (swiperInstance) { | |
− | + | swiperInstance.destroy(true, true); | |
− | + | swiperInstance = null; | |
− | + | } | |
− | + | ||
− | + | storyLinks.forEach(link => { | |
+ | link.addEventListener('click', function (e) { | ||
+ | e.preventDefault(); | ||
+ | storyLinks.forEach(l => l.classList.remove('active')); | ||
+ | document | ||
+ | .querySelectorAll('.swiper-slide') | ||
+ | .forEach(i => i.classList.remove('active')); | ||
+ | |||
+ | this.classList.add('active'); | ||
+ | const targetId = this.getAttribute('data-target'); | ||
+ | const targetImage = document.getElementById(targetId); | ||
+ | if (targetImage) { | ||
+ | targetImage.classList.add('active'); | ||
+ | |||
+ | const containerRect = imageContainer.getBoundingClientRect(); | ||
+ | const imageRect = targetImage.getBoundingClientRect(); | ||
+ | const scrollTop = | ||
+ | imageContainer.scrollTop + | ||
+ | (imageRect.top - containerRect.top) - | ||
+ | containerRect.height / 2 + | ||
+ | imageRect.height / 2; | ||
− | + | imageContainer.scrollTo({ | |
− | + | top: scrollTop + 0, | |
− | + | behavior: 'smooth', | |
− | + | }); | |
− | } | + | } |
+ | }); | ||
}); | }); | ||
− | + | }; | |
− | |||
− | + | const initMobile = () => { | |
− | + | swiperInstance = new Swiper('.image-scroll-container.swiper', { | |
− | + | direction: 'horizontal', | |
− | + | slidesPerView: 1.1, | |
− | + | spaceBetween: 16, | |
− | + | pagination: { | |
− | + | el: '.swiper-fraction', | |
− | + | type: 'fraction', | |
− | + | formatFractionCurrent: number => number, | |
− | + | formatFractionTotal: number => number, | |
− | |||
− | |||
− | |||
− | |||
}, | }, | ||
− | slideChange: () => { | + | on: { |
− | + | init: () => { | |
+ | updateProgress(); | ||
+ | }, | ||
+ | slideChange: () => { | ||
+ | updateProgress(); | ||
+ | }, | ||
}, | }, | ||
− | } | + | }); |
− | }); | + | }; |
− | + | ||
+ | const updateProgress = () => { | ||
+ | if (!swiperInstance) return; | ||
+ | const current = swiperInstance.realIndex + 1; | ||
+ | const total = swiperInstance.slides.length; | ||
+ | const percent = (current / total) * 100; | ||
+ | const progressBar = document.querySelector('.swiper-progressbar .progress-fill'); | ||
+ | if (progressBar) { | ||
+ | progressBar.style.width = `${percent}%`; | ||
+ | } | ||
+ | }; | ||
+ | |||
+ | const handleResize = () => { | ||
+ | if (breakpoint.matches) { | ||
+ | initMobile(); | ||
+ | } else { | ||
+ | initDesktop(); | ||
+ | } | ||
+ | }; | ||
− | + | breakpoint.addEventListener('change', handleResize); | |
− | + | handleResize(); | |
− | + | }); | |
− | + | document.addEventListener('DOMContentLoaded', function () { | |
− | + | // 공통 브레이크포인트 기준 | |
− | const | + | const breakpoint = window.matchMedia('(max-width: 991px)'); |
− | |||
− | |||
− | |||
− | |||
− | + | // Story Swiper (모바일 전용 터치) | |
− | + | let storySwiper; | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | const initStorySwiper = () => { | |
− | + | const isMobile = breakpoint.matches; | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | if (storySwiper) storySwiper.destroy(true, true); | |
− | |||
− | + | storySwiper = new Swiper('.story-swiper .swiper', { | |
− | + | slidesPerView: 1, | |
+ | spaceBetween: 24, | ||
+ | grabCursor: isMobile, | ||
+ | simulateTouch: isMobile, | ||
+ | allowTouchMove: isMobile, | ||
+ | pagination: { | ||
+ | el: '.swiper-pagination', | ||
+ | clickable: true, | ||
+ | }, | ||
+ | navigation: { | ||
+ | nextEl: '.swiper-button-next', | ||
+ | prevEl: '.swiper-button-prev', | ||
+ | }, | ||
+ | mousewheel: isMobile | ||
+ | ? { | ||
+ | forceToAxis: true, | ||
+ | invert: false, | ||
+ | } | ||
+ | : false, | ||
+ | on: { | ||
+ | progress: function (swiper, progress) { | ||
+ | const indicatorBar = swiper.el.querySelector('.indicator-bar'); | ||
+ | if (indicatorBar) { | ||
+ | indicatorBar.style.width = progress * 100 + '%'; | ||
+ | } | ||
+ | }, | ||
+ | }, | ||
+ | }); | ||
+ | }; | ||
− | + | breakpoint.addEventListener('change', initStorySwiper); | |
+ | initStorySwiper(); | ||
− | + | // Asset Swiper type01 | |
− | slidesPerView: | + | const swiper01 = new Swiper('.asset-swiper .type01', { |
− | spaceBetween: | + | loop: true, |
− | + | slidesPerView: 'auto', | |
− | + | spaceBetween: 8, | |
− | + | autoplay: { | |
+ | delay: 0, | ||
+ | disableOnInteraction: false, | ||
+ | }, | ||
+ | speed: 5000, | ||
+ | freeMode: true, | ||
+ | breakpoints: { | ||
+ | 991: { | ||
+ | spaceBetween: 28, | ||
+ | }, | ||
+ | }, | ||
+ | }); | ||
+ | |||
+ | // Asset Swiper type02 | ||
+ | const swiper02 = new Swiper('.asset-swiper .type02', { | ||
+ | loop: true, | ||
+ | slidesPerView: 'auto', | ||
+ | spaceBetween: 8, | ||
+ | autoplay: { | ||
+ | delay: 0, | ||
+ | disableOnInteraction: false, | ||
+ | reverseDirection: true, | ||
+ | }, | ||
+ | speed: 5000, | ||
+ | freeMode: true, | ||
+ | breakpoints: { | ||
+ | 991: { | ||
+ | spaceBetween: 28, | ||
+ | }, | ||
+ | }, | ||
+ | }); | ||
+ | }); | ||
+ | |||
+ | document.addEventListener('DOMContentLoaded', function () { | ||
+ | const progressBar = document.querySelector('.gwangju-main-section04 .progress-bar'); | ||
+ | const metaverseSlider = new Swiper('.gwangju-metaverse-slider', { | ||
+ | loop: true, | ||
+ | slidesPerView: 1.2, | ||
+ | spaceBetween: 15, | ||
pagination: { | pagination: { | ||
el: '.swiper-pagination', | el: '.swiper-pagination', | ||
− | + | type: 'fraction', | |
− | |||
− | |||
− | |||
− | |||
}, | }, | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
on: { | on: { | ||
− | + | slideChange: function () { | |
− | const | + | const totalSlides = this.slides.length - this.loopedSlides * 2; |
− | if ( | + | const progress = (this.realIndex + 1) / totalSlides; |
− | + | if (progressBar) { | |
+ | progressBar.style.width = progress * 100 + '%'; | ||
} | } | ||
+ | }, | ||
+ | }, | ||
+ | breakpoints: { | ||
+ | 769: { | ||
+ | slidesPerView: 3, | ||
+ | spaceBetween: 30, | ||
+ | }, | ||
+ | 1024: { | ||
+ | slidesPerView: 4, | ||
+ | spaceBetween: 30, | ||
}, | }, | ||
}, | }, | ||
}); | }); | ||
+ | }); | ||
+ | var animation = { | ||
+ | duration: (document.querySelectorAll('.keen-slider__slide').length / 2) * 1500, | ||
+ | easing: t => t, | ||
}; | }; | ||
− | + | // 변수 초기화 | |
− | + | let slider = null; | |
− | + | let slider2 = null; | |
− | // | + | let autoplayActive = true; |
− | + | let autoplayTimeoutId = null; | |
+ | let isSyncingSlider1 = false; | ||
+ | let isSyncingSlider2 = false; | ||
+ | let isMouseOverContainer = false; // 컨테이너 마우스 상태 추적 변수 | ||
+ | // 슬라이더 2 먼저 초기화 | ||
+ | slider2 = new KeenSlider('#my-keen-slider2', { | ||
loop: true, | loop: true, | ||
− | + | renderMode: 'performance', | |
− | + | mode: 'free', | |
− | + | rtl: true, | |
− | + | slides: { | |
− | + | perView: 'auto', | |
+ | spacing: 8, | ||
}, | }, | ||
− | |||
− | |||
breakpoints: { | breakpoints: { | ||
− | + | '(min-width: 500px)': { | |
− | + | slides: { | |
+ | perView: 'auto', | ||
+ | spacing: 28, | ||
+ | }, | ||
}, | }, | ||
+ | }, | ||
+ | created(s) { | ||
+ | if (!isMouseOverContainer) { | ||
+ | s.moveToIdx(5, true, animation); | ||
+ | } | ||
+ | }, | ||
+ | animationEnded(s) { | ||
+ | // if (!isMouseOverContainer) { | ||
+ | slider2.endTimer = setTimeout(() => { | ||
+ | slider?.moveToIdx(slider.track.details.abs + 4, true, animation); | ||
+ | s.moveToIdx(s.track.details.abs + 5, true, animation); | ||
+ | }, 1000); | ||
+ | // } | ||
+ | }, | ||
+ | detailsChanged(s) { | ||
+ | // 마우스가 컨테이너 위에 있을 때만 동기화 작동 | ||
+ | if (slider2?.endTimer) clearTimeout(slider2?.endTimer); | ||
+ | if (!isSyncingSlider1) { | ||
+ | if (slider) { | ||
+ | if (slider.track) { | ||
+ | slider.animator.stop(); | ||
+ | isSyncingSlider2 = true; | ||
+ | // 위치 동기화 | ||
+ | const position = s.track.details.position; | ||
+ | slider.track.to(position); | ||
+ | setTimeout(() => { | ||
+ | isSyncingSlider2 = false; | ||
+ | }, 30); | ||
+ | } | ||
+ | } | ||
+ | } | ||
}, | }, | ||
}); | }); | ||
− | // | + | // slider1 초기화 |
− | + | slider = new KeenSlider('#my-keen-slider', { | |
loop: true, | loop: true, | ||
− | + | renderMode: 'performance', | |
− | + | mode: 'free', | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
breakpoints: { | breakpoints: { | ||
− | + | '(min-width: 500px)': { | |
− | + | slides: { | |
+ | perView: 'auto', | ||
+ | spacing: 28, | ||
+ | }, | ||
}, | }, | ||
}, | }, | ||
− | + | slides: { | |
− | + | perView: 'auto', | |
− | + | spacing: 8, | |
− | + | }, | |
− | + | created(s) { | |
− | + | if (!isMouseOverContainer) { | |
− | + | s.moveToIdx(5, true, animation); | |
− | + | } | |
− | + | }, | |
− | + | animationEnded(s) { | |
− | + | // if (!isMouseOverContainer) { | |
− | + | slider.endTimer = setTimeout(() => { | |
− | + | slider2?.moveToIdx(slider2.track.details.abs + 4, true, animation); | |
+ | s.moveToIdx(s.track.details.abs + 5, true, animation); | ||
+ | }, 1000); | ||
+ | // } | ||
}, | }, | ||
− | + | detailsChanged(s) { | |
− | + | if (slider?.endTimer) clearTimeout(slider?.endTimer); | |
− | + | // 마우스가 컨테이너 위에 있을 때만 동기화 작동 | |
− | + | if (!isSyncingSlider2) { | |
− | + | if (slider2) { | |
− | + | if (slider2.track) { | |
+ | slider2.animator.stop(); | ||
+ | isSyncingSlider1 = true; | ||
+ | slider2.track.to(s.track.details.position); | ||
+ | setTimeout(() => { | ||
+ | isSyncingSlider1 = false; | ||
+ | }, 30); | ||
+ | } | ||
} | } | ||
− | } | + | } |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
}, | }, | ||
}); | }); | ||
− | + | ||
− | + | // 컨테이너 하나에만 마우스 이벤트 적용 | |
− | + | document.querySelector('.asset-swiper').addEventListener('pointerdown', () => { | |
− | + | // 타임아웃 삭제 | |
− | + | if (autoplayTimeoutId) clearTimeout(autoplayTimeoutId); | |
− | + | ||
− | + | // 현재 진행 중인 애니메이션 중단 | |
− | + | if (slider) { | |
− | + | if (slider.animator) { | |
− | + | slider.animator.stop(); | |
− | + | } | |
− | + | } | |
− | + | if (slider2) { | |
− | + | if (slider2.animator) { | |
− | + | slider2.animator.stop(); | |
− | + | } | |
− | + | } | |
− | + | }); | |
− | + | </script> | |
− | + | <!-- // 에디터 컨텐츠 등록 영역 -->}} | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | }} |
2025년 7월 18일 (금) 16:59 판
MEMORIAL SITE
장소로 알아보는
열흘 간의 항쟁 기록
그날의 기억이 머문 장소들을 따라가며, 시간 속에 남은 이야기를 천천히 만나보세요.
번호를 따라가며 클릭하면, 장소마다 담긴 역사적 의미와 당시의 이야기를 자세히 살펴볼
수 있습니다. 광주의 골목골목에 스며든 기억과 그날의 진실과 마주해보세요.





STORY DATA
카테고리별로 만나는
인문 스토리
PLATFORM
AR Heritage
광주의 근현대 역사와 문화를 엿볼 수 있는 유산들을 증강현실
광주의 역사적 명소와 문화예술 거점을 AR 콘텐츠로 체험할 수 있는 투어입니다.
실시간 위치 정보와 장소의 맥락을 제공하여 시민들이 지역 인문자원을 향유하도록
기획되었습니다.
AR을 통해 광주 시민들이 공유하는 기억과 감성을 생생하게 느낄 수 있습니다.