PageSourceSearch

https://cdn.monkeytravel.com/assets/js/modal_gallery_full_slier_init.js?a=20260916102204

js monkeytravel.com collected 2026-09-24 12:26:55 UTC 14,879 bytes, 422 lines download raw bytes

1let thumbSlider, originSlider;
2let lastProductId = null;
3let lastGalleryType = null;
4let lastReviewProductId = null;
5let lastReviewId = null;
6
7/** 풀스크린 슬라이더: 실제 URL 전 1×1 투명 자리표시 (대량 이미지 동시 로드로 모바일 탭 크래시 방지) */
8const FULL_SLIDER_LAZY_PLACEHOLDER =
9  'data:image/svg+xml;charset=utf-8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg"/>');
10
11/**
12 * OTA 이미지 URL 을 용도별 사이즈로 변환 (캐시에는 사이즈 미지정 base URL 저장됨 — hotelProduct.php 참고)
13 * - Expedia(travelapi.com): _t(70px)/_b(350px)/_z(1000px) suffix
14 * - Agoda(agoda.net): ?s=<width>x 가변 리사이즈, 미지정 시 원본이 내려오므로 반드시 지정
15 * - 그 외(자체 업로드 등)는 원본 유지
16 */
17window.resizeGalleryImageUrl = window.resizeGalleryImageUrl || function (url, size) {
18  if (!url || typeof url !== 'string') return url;
19  if (url.indexOf('travelapi.com') !== -1) {
20    const suffix = size === 'large' ? '_z.jpg' : '_b.jpg';
21    return url.replace(/_[tbz]\.jpg(\?.*)?$/, suffix + '$1');
22  }
23  if (url.indexOf('agoda.net') !== -1) {
24    const width = size === 'small' ? 312 : size === 'medium' ? 512 : 1000;
25    if (/[?&]s=\d+x/.test(url)) {
26      return url.replace(/([?&])s=\d+x/, '$1s=' + width + 'x');
27    }
28    return url + (url.indexOf('?') !== -1 ? '&' : '?') + 's=' + width + 'x';
29  }
30  return url;
31};
32
33let fullSliderThumbLazyIo = null;
34
35function disconnectFullSliderThumbLazyObserver() {
36  if (fullSliderThumbLazyIo) {
37    fullSliderThumbLazyIo.disconnect();
38    fullSliderThumbLazyIo = null;
39  }
40}
41
42/** 현재 슬라이드에서 이만큼 벗어난 원본 이미지는 placeholder 로 되돌려 메모리 상주를 제한 (iOS 메모리 회수 → 흰 화면 예방) */
43const ORIGIN_KEEP_RANGE = 6;
44const FULL_SLIDER_MAX_RETRY = 3;
45
46function loadFullSliderImage(img) {
47  const url = img?.dataset?.src;
48  if (!url) return;
49  img.decoding = 'async';
50  img.onload = () => {
51    img.onload = null;
52    img.onerror = null;
53    img.removeAttribute('data-retry');
54  };
55  img.onerror = () => {
56    // 로드 중단(화면 이탈로 요청 취소 등)/실패 시 placeholder 로 되돌려 다음 진입 때 재시도
57    img.onload = null;
58    img.onerror = null;
59    const retry = (parseInt(img.dataset.retry, 10) || 0) + 1;
60    if (retry > FULL_SLIDER_MAX_RETRY) return;
61    img.dataset.retry = String(retry);
62    img.src = FULL_SLIDER_LAZY_PLACEHOLDER;
63    img.dataset.src = url;
64    if (fullSliderThumbLazyIo && img.isConnected && img.closest('.thumb-slider')) {
65      fullSliderThumbLazyIo.observe(img);
66    }
67  };
68  img.src = url;
69  img.removeAttribute('data-src');
70}
71
72/** 로드된 원본 이미지를 placeholder 로 되돌려 디코딩 메모리를 해제 */
73function unloadFullSliderImage(img) {
74  if (!img || img.dataset.src) return;
75  const url = img.currentSrc || img.src;
76  if (!url || url.indexOf('data:') === 0) return;
77  img.onload = null;
78  img.onerror = null;
79  img.dataset.src = url;
80  img.src = FULL_SLIDER_LAZY_PLACEHOLDER;
81}
82
83/** origin-slider: 현재 슬라이드 ± range 만 실제 로드, ± ORIGIN_KEEP_RANGE 밖은 언로드 */
84function loadOriginNeighborImages(swiper, range = 2) {
85  if (!swiper || !swiper.slides || swiper.destroyed) return;
86  const idx = swiper.activeIndex;
87  const from = Math.max(0, idx - range);
88  const to = Math.min(swiper.slides.length - 1, idx + range);
89  for (let i = 0; i < swiper.slides.length; i++) {
90    const img = swiper.slides[i]?.querySelector('img');
91    if (!img) continue;
92    if (i >= from && i <= to) {
93      if (img.dataset.src) loadFullSliderImage(img);
94    } else if (i < idx - ORIGIN_KEEP_RANGE || i > idx + ORIGIN_KEEP_RANGE) {
95      unloadFullSliderImage(img);
96    }
97  }
98}
99
100/**
101 * 현재 슬라이드 주변의 로드된 원본 이미지를 캐시에서 다시 디코드
102 * (iOS 가 백그라운드 전환 중 디코딩 메모리를 회수하면 naturalWidth 는 정상인데 흰 화면으로 보이는 경우 복구)
103 */
104function refreshFullSliderActiveImages() {
105  const sliderEl = document.getElementById('fullscreenSlider');
106  if (!sliderEl || sliderEl.style.visibility !== 'visible') return;
107  if (!originSlider || originSlider.destroyed || !originSlider.slides) return;
108  const idx = originSlider.activeIndex;
109  const from = Math.max(0, idx - 1);
110  const to = Math.min(originSlider.slides.length - 1, idx + 1);
111  for (let i = from; i <= to; i++) {
112    const img = originSlider.slides[i]?.querySelector('img');
113    if (!img || img.dataset.src) continue;
114    const url = img.currentSrc || img.src;
115    if (!url || url.indexOf('data:') === 0) continue;
116    img.removeAttribute('src');
117    img.dataset.src = url;
118    img.removeAttribute('data-retry');
119    loadFullSliderImage(img);
120  }
121}
122
123/** 슬라이더를 닫고 그리드 모달로 돌아갈 때, 슬라이더 사용 중 회수됐을 수 있는 그리드 이미지 복구 */
124function refreshUnderlyingGalleryImages() {
125  if (typeof refreshModalGalleryVisibleImages === 'function') {
126    refreshModalGalleryVisibleImages();
127  }
128}
129
130/** 메모리 회수/로드 중단으로 깨진(흰 화면) 이미지를 찾아 재로드 */
131function recoverFullSliderImages() {
132  const sliderEl = document.getElementById('fullscreenSlider');
133  if (!sliderEl) return;
134  sliderEl.querySelectorAll('img').forEach((img) => {
135    if (img.dataset.src) return;
136    const url = img.currentSrc || img.src;
137    if (!url || url.indexOf('data:') === 0) return;
138    if (img.complete && img.naturalWidth === 0) {
139      img.dataset.src = url;
140      img.removeAttribute('data-retry');
141      loadFullSliderImage(img);
142    }
143  });
144}
145
146/** thumb-slider: 가로 스크롤 영역에 들어온 썸네일만 실제 로드 */
147function initLazyFullSliderThumbImages() {
148  disconnectFullSliderThumbLazyObserver();
149
150  const thumbRoot = document.querySelector('.modal-fullscreen-slider .thumb-slider');
151  const imgs = document.querySelectorAll('.modal-fullscreen-slider .thumb-slider img[data-src]');
152  if (!thumbRoot || imgs.length === 0) return;
153
154  if (!('IntersectionObserver' in window)) {
155    imgs.forEach(loadFullSliderImage);
156    return;
157  }
158
159  fullSliderThumbLazyIo = new IntersectionObserver(
160    (entries, obs) => {
161      entries.forEach((entry) => {
162        if (!entry.isIntersecting) return;
163        loadFullSliderImage(entry.target);
164        obs.unobserve(entry.target);
165      });
166    },
167    {
168      root: thumbRoot,
169      rootMargin: '0px 480px',
170      threshold: 0.01,
171    }
172  );
173
174  imgs.forEach((img) => fullSliderThumbLazyIo.observe(img));
175}
176
177// fullscreen slider 데이터 로드
178function screenGallery(product_id, gallery_type = "", imgIndex = 0, callback) {
179  fetchGallery(product_id, gallery_type, (data) => {
180    renderFullScreenGallery(data.imageList, imgIndex);
181    if (typeof callback === 'function') callback();
182  });
183}
184
185// 리뷰용 fullscreen slider 데이터 로드
186function reviewScreenGallery (product_id, review_id = null, imgIndex = 0, callback) {
187  fetchReviewGallery(product_id, review_id, (data) => {
188    renderFullScreenGallery(data.imageList, imgIndex);
189    if (typeof callback === 'function') callback();
190  });
191}
192
193// fullscreen 열기
194function openFullScreen(fullscreenSlider) {
195  // 먼저 슬라이더를 표시
196  fullscreenSlider.style.opacity = 1;
197  fullscreenSlider.style.visibility = "visible";
198
199  // 전체화면 요청을 안전하게 처리
200  const requestFullscreenSafely = () => {
201    try {
202      if (fullscreenSlider.requestFullscreen) {
203        fullscreenSlider.requestFullscreen().catch(err => {
204          console.warn("Fullscreen 요청 실패:", err.message || err);
205          // 전체화면 실패 (모달 형태)
206        });
207      } else if (fullscreenSlider.webkitRequestFullscreen) {
208        fullscreenSlider.webkitRequestFullscreen();
209      } else if (fullscreenSlider.msRequestFullscreen) {
210        fullscreenSlider.msRequestFullscreen();
211      } else {
212        console.warn("모달 형태로 표시");
213      }
214    } catch (err) {
215      console.warn("Fullscreen 요청 중 오류:", err.message || err);
216      // 오류 발생 시에도 슬라이더는 표시됨 (모달 형태)
217    }
218  };
219
220  // 전체화면을 시도 (실패하면 모달)
221  requestFullscreenSafely();
222}
223
224// fullscreen 닫기
225function closeFullScreen(fullscreenSlider) {
226  if (document.fullscreenElement && document.exitFullscreen) {
227    document.exitFullscreen();
228  } else {
229    fullscreenSlider.style.opacity = 0;
230    fullscreenSlider.style.visibility = "hidden";
231    refreshUnderlyingGalleryImages();
232  }
233}
234
235// fullscreen slider 만들기
236function renderFullScreenGallery(imageList, imgIndex = 0) {
237
238  disconnectFullSliderThumbLazyObserver();
239
240  const thumbWrapper = document.querySelector(".thumb-slider .swiper-wrapper");
241  const originWrapper = document.querySelector(".origin-slider .swiper-wrapper");
242
243  // 이전 렌더 이미지의 src 를 먼저 비워 디코딩 메모리를 즉시 해제 (반복 open 시 누적 방지)
244  [thumbWrapper, originWrapper].forEach((wrapper) => {
245    wrapper.querySelectorAll('img').forEach((img) => {
246      img.onload = null;
247      img.onerror = null;
248      img.removeAttribute('data-src');
249      img.src = FULL_SLIDER_LAZY_PLACEHOLDER;
250    });
251  });
252
253  thumbWrapper.innerHTML = "";
254  originWrapper.innerHTML = "";
255
256  imageList.forEach(item => {
257    const alt = item.alt ?? "이미지";
258    const imageUrl = item.webPath || item.link || "";
259
260    const thumbSlide = document.createElement("div");
261    thumbSlide.classList.add("swiper-slide");
262
263    const thumbImg = document.createElement("img");
264    if (imageUrl) {
265      thumbImg.src = FULL_SLIDER_LAZY_PLACEHOLDER;
266      thumbImg.dataset.src = window.resizeGalleryImageUrl(imageUrl, 'small');
267    }
268    thumbImg.alt = alt;
269    thumbSlide.appendChild(thumbImg);
270    thumbWrapper.appendChild(thumbSlide);
271
272    const originSlide = document.createElement("div");
273    originSlide.classList.add("swiper-slide");
274
275    const originImg = document.createElement("img");
276    if (imageUrl) {
277      originImg.src = FULL_SLIDER_LAZY_PLACEHOLDER;
278      originImg.dataset.src = window.resizeGalleryImageUrl(imageUrl, 'large');
279    }
280    originImg.alt = alt;
281    originSlide.appendChild(originImg);
282    originWrapper.appendChild(originSlide);
283  });
284
285  if (thumbSlider) thumbSlider.destroy(true, true);
286  if (originSlider) originSlider.destroy(true, true);
287
288  thumbSlider = new Swiper(".thumb-slider", {
289    spaceBetween: 8,
290    slidesPerView: "auto",
291    freeMode: true,
292    centerInsufficientSlides: true,
293    watchSlidesProgress: true,
294    watchSlidesVisibility: true,
295    slideToClickedSlide: true,
296  });
297
298  originSlider = new Swiper(".origin-slider", {
299    speed: 300,
300    effect: 'fade',
301    fadeEffect: {
302      crossFade: true
303    },
304    spaceBetween: 10,
305    navigation: {
306      nextEl: ".origin-slider .swiper-button-next",
307      prevEl: ".origin-slider .swiper-button-prev",
308    },
309    thumbs: {
310      swiper: thumbSlider,
311    },
312    on: {
313      afterInit() {
314        loadOriginNeighborImages(this);
315      },
316      slideChange() {
317        loadOriginNeighborImages(this);
318      },
319    },
320  });
321
322  initLazyFullSliderThumbImages();
323
324  setTimeout(() => {
325    originSlider.update();
326    originSlider.slideTo(imgIndex, 0);
327    loadOriginNeighborImages(originSlider);
328  }, 0);
329}
330
331document.addEventListener("DOMContentLoaded", () => {
332  const fullscreenSlider = document.getElementById("fullscreenSlider");
333
334  document.body.addEventListener("click", function (e) {
335    const btnFullSlider = e.target.closest('.js-show-full-slider');
336    const originSliderItem = e.target.closest(".origin-slider .swiper-slide");
337    const closeBtn = e.target.closest('.btn-fullscreen-close');
338    const btnReviewSlider = e.target.closest('.js-show-review-slider');
339
340    // 일반 상품용 슬라이더
341    if (btnFullSlider) {
342      const imgIndex = parseInt(btnFullSlider.dataset.index, 10) || 0;
343      const product_id = btnFullSlider.getAttribute("data-product_id") || btnFullSlider.getAttribute("product_id");
344      const gallery_type = btnFullSlider.getAttribute("data-gallery_type") || btnFullSlider.getAttribute("gallery_type") || "";
345
346      if (!product_id) return;
347
348      if (lastProductId === product_id && lastGalleryType === gallery_type) {
349        openFullScreen(fullscreenSlider);
350        setTimeout(() => {
351          originSlider.slideTo(imgIndex, 0);
352          loadOriginNeighborImages(originSlider);
353          recoverFullSliderImages();
354        }, 0);
355        return;
356      }
357      //console.log('일반용',product_id,gallery_type,imgIndex);
358
359      screenGallery(product_id, gallery_type, imgIndex, () => {
360        lastProductId = product_id;
361        lastGalleryType = gallery_type;
362        // 사용자 상호작용 컨텍스트를 유지하기 위해 즉시 실행
363        openFullScreen(fullscreenSlider);
364      });
365    }
366
367    // 리뷰용 슬라이더
368    if (btnReviewSlider) {
369      const imgIndex = parseInt(btnReviewSlider.dataset.index, 10) || 0;
370      const product_id = btnReviewSlider.getAttribute("data-product-id") || btnReviewSlider.getAttribute("product_id");
371      const review_id = btnReviewSlider.getAttribute("data-review-id") || btnReviewSlider.getAttribute("review_id") || null;
372
373      if (!product_id) return;
374
375      if (lastReviewProductId === product_id && lastReviewId === review_id) {
376        openFullScreen(fullscreenSlider);
377        setTimeout(() => {
378          originSlider.slideTo(imgIndex, 0);
379          loadOriginNeighborImages(originSlider);
380          recoverFullSliderImages();
381        }, 0);
382        return;
383      }
384      //console.log('리뷰용',product_id,review_id,imgIndex);
385      
386      reviewScreenGallery(product_id, review_id, imgIndex, () => {
387        lastReviewProductId = product_id;
388        lastReviewId = review_id;
389        openFullScreen(fullscreenSlider);
390        setTimeout(() => {
391          originSlider.slideTo(imgIndex, 0);
392        }, 0);
393      });
394    }
395
396    // 닫기 버튼 또는 슬라이드 이미지 클릭 시 닫기
397    if (closeBtn || (originSliderItem && document.fullscreenElement)) {
398      closeFullScreen(fullscreenSlider);
399    }
400  });
401
402  document.addEventListener("fullscreenchange", () => {
403    if (!document.fullscreenElement) {
404      fullscreenSlider.style.opacity = 0;
405      fullscreenSlider.style.visibility = "hidden";
406      refreshUnderlyingGalleryImages();
407    }
408  });
409
410  // 다른 앱/탭 다녀온 뒤: iOS 가 백그라운드에서 이미지 메모리를 회수한 경우 복구
411  document.addEventListener("visibilitychange", () => {
412    if (document.visibilityState !== "visible") return;
413    refreshFullSliderActiveImages();
414  });
415
416  // bfcache 복원(뒤로가기 등): 회수/중단으로 깨진 이미지 재로드
417  window.addEventListener("pageshow", (event) => {
418    if (!event.persisted) return;
419    recoverFullSliderImages();
420    refreshFullSliderActiveImages();
421  });
422});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.