PageSourceSearch

https://handure.net/assets/site.js

js handure.net collected 2026-10-06 04:29:36 UTC 13,062 bytes, 327 lines download raw bytes

1/* ══════════════════════════════════════════════════════════
2   너와두리 캠핑장 — 공통 스크립트
3   모든 페이지에서 함께 씁니다.
4   ══════════════════════════════════════════════════════════ */
5
6/* 스크롤 등장 애니메이션 */
7(function () {
8  const els = document.querySelectorAll('.reveal');
9  if (!els.length) return;
10  if (!('IntersectionObserver' in window)) { els.forEach(e => e.classList.add('is-in')); return; }
11  const io = new IntersectionObserver((entries) => {
12    entries.forEach((entry) => {
13      if (entry.isIntersecting) { entry.target.classList.add('is-in'); io.unobserve(entry.target); }
14    });
15  }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
16  els.forEach((el) => io.observe(el));
17})();
18
19/* 모바일 메뉴 */
20(function () {
21  const menu = document.getElementById('mobileMenu');
22  const open = document.getElementById('menuBtn');
23  const close = document.getElementById('menuClose');
24  if (!menu || !open) return;
25  const show = () => { menu.classList.remove('hidden'); document.body.style.overflow = 'hidden'; };
26  const hide = () => { menu.classList.add('hidden'); document.body.style.overflow = ''; };
27  open.addEventListener('click', show);
28  close && close.addEventListener('click', hide);
29  document.querySelectorAll('.mob-link').forEach(a => a.addEventListener('click', hide));
30  document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hide(); });
31})();
32
33/* 페이지 안 메뉴(서브내비) — 지금 보고 있는 구역을 표시
34   ※ 예전에는 여기서 scrollIntoView(behavior:'smooth') 를 썼는데,
35      스크롤 중에 계속 호출되면서 사용자의 휠 스크롤과 부딪혀
36      화면이 버벅였습니다. 지금은 메뉴 띠의 가로 위치만 직접 옮기고
37      페이지 스크롤에는 손대지 않습니다. */
38(function () {
39  const bar = document.querySelector('.subnav');
40  if (!bar) return;
41  const links = Array.from(bar.querySelectorAll('a[href^="#"]'));
42  if (!links.length) return;
43
44  const scroller = bar.querySelector('.no-bar') || bar;
45
46  const targets = links
47    .map(a => ({ a, el: document.getElementById(a.getAttribute('href').slice(1)) }))
48    .filter(x => x.el);
49  if (!targets.length) return;
50
51  let current = null;
52
53  // 선택된 항목이 띠 밖으로 잘렸을 때만 가로로 살짝 옮깁니다
54  function keepVisible(link) {
55    if (scroller.scrollWidth <= scroller.clientWidth + 1) return;
56    const left = link.offsetLeft;
57    const right = left + link.offsetWidth;
58    const viewLeft = scroller.scrollLeft;
59    const viewRight = viewLeft + scroller.clientWidth;
60    if (left < viewLeft) scroller.scrollLeft = left - 12;
61    else if (right > viewRight) scroller.scrollLeft = right - scroller.clientWidth + 12;
62  }
63
64  function mark(link) {
65    if (link === current) return;          // 바뀔 때만 손댑니다
66    current = link;
67    links.forEach(a => a.classList.toggle('is-current', a === link));
68    keepVisible(link);
69  }
70
71  if (!('IntersectionObserver' in window)) return;
72
73  const seen = new Map();
74  let queued = false;
75
76  function pick() {
77    queued = false;
78    let best = null, bestRatio = 0;
79    targets.forEach(({ a, el }) => {
80      const r = seen.get(el) || 0;
81      if (r > bestRatio) { bestRatio = r; best = a; }
82    });
83    if (best) mark(best);
84  }
85
86  const io = new IntersectionObserver((entries) => {
87    entries.forEach(e => seen.set(e.target, e.isIntersecting ? e.intersectionRatio : 0));
88    // 스크롤 도중 계산이 몰리지 않도록 한 프레임에 한 번만 처리합니다
89    if (!queued) { queued = true; requestAnimationFrame(pick); }
90  }, { rootMargin: '-45% 0px -45% 0px', threshold: [0, 0.01, 0.5, 1] });
91
92  targets.forEach(({ el }) => io.observe(el));
93})();
94
95/* 객실 탭 */
96(function () {
97  const list = document.getElementById('stayTabs');
98  if (!list) return;
99  const tabs = Array.from(list.querySelectorAll('[role="tab"]'));
100
101  function select(tab, focus) {
102    tabs.forEach((t) => {
103      const on = t === tab;
104      t.setAttribute('aria-selected', String(on));
105      t.tabIndex = on ? 0 : -1;
106      const panel = document.getElementById(t.getAttribute('aria-controls'));
107      if (panel) panel.hidden = !on;
108    });
109    if (focus) tab.focus();
110    // 탭 띠만 가로로 옮깁니다 (페이지 스크롤은 건드리지 않습니다)
111    if (list.scrollWidth > list.clientWidth + 1) {
112      const l = tab.offsetLeft, rgt = l + tab.offsetWidth;
113      if (l < list.scrollLeft) list.scrollLeft = l - 12;
114      else if (rgt > list.scrollLeft + list.clientWidth) list.scrollLeft = rgt - list.clientWidth + 12;
115    }
116  }
117
118  tabs.forEach((tab) => tab.addEventListener('click', () => select(tab, false)));
119  list.addEventListener('keydown', (e) => {
120    const i = tabs.indexOf(document.activeElement);
121    if (i < 0) return;
122    let n = null;
123    if (e.key === 'ArrowRight') n = (i + 1) % tabs.length;
124    if (e.key === 'ArrowLeft') n = (i - 1 + tabs.length) % tabs.length;
125    if (e.key === 'Home') n = 0;
126    if (e.key === 'End') n = tabs.length - 1;
127    if (n === null) return;
128    e.preventDefault();
129    select(tabs[n], true);
130  });
131})();
132
133
134/* 객실 사진 슬라이드
135   ┌────────────────────────────────────────────────────────┐
136   │ 사진을 넣고 빼려면 HTML 의 .rslide 안에서 <img> 를       │
137   │ 추가하거나 지우기만 하면 됩니다. 화살표·점·장수 표시는    │
138   │ 사진 수에 맞춰 자동으로 만들어집니다.                    │
139   └────────────────────────────────────────────────────────┘ */
140(function () {
141  document.querySelectorAll('.rslide').forEach(function (box) {
142    const imgs = Array.from(box.querySelectorAll(':scope > img'));
143    if (!imgs.length) return;
144
145    const track = document.createElement('div');
146    track.className = 'rslide-track';
147    imgs.forEach(im => track.appendChild(im));
148    box.appendChild(track);
149
150    let i = 0;
151    const many = imgs.length > 1;
152
153    const count = document.createElement('p');
154    count.className = 'rslide-count';
155    box.appendChild(count);
156
157    let dots = [];
158    if (many) {
159      const prev = document.createElement('button');
160      prev.type = 'button'; prev.className = 'rslide-btn prev';
161      prev.setAttribute('aria-label', '이전 사진');
162      prev.innerHTML = '<iconify-icon icon="solar:alt-arrow-left-linear" class="text-xl"></iconify-icon>';
163      const next = document.createElement('button');
164      next.type = 'button'; next.className = 'rslide-btn next';
165      next.setAttribute('aria-label', '다음 사진');
166      next.innerHTML = '<iconify-icon icon="solar:alt-arrow-right-linear" class="text-xl"></iconify-icon>';
167      box.append(prev, next);
168      prev.addEventListener('click', () => go(i - 1));
169      next.addEventListener('click', () => go(i + 1));
170
171      const dotWrap = document.createElement('div');
172      dotWrap.className = 'rslide-dots';
173      dots = imgs.map((_, n) => {
174        const d = document.createElement('button');
175        d.type = 'button';
176        d.setAttribute('aria-label', (n + 1) + '번째 사진 보기');
177        d.addEventListener('click', () => go(n));
178        dotWrap.appendChild(d);
179        return d;
180      });
181      box.appendChild(dotWrap);
182
183      // 좌우 넘기기 (손가락 · 키보드)
184      let x0 = null;
185      box.addEventListener('touchstart', (e) => { x0 = e.touches[0].clientX; }, { passive: true });
186      box.addEventListener('touchend', (e) => {
187        if (x0 === null) return;
188        const dx = e.changedTouches[0].clientX - x0;
189        if (Math.abs(dx) > 40) go(dx < 0 ? i + 1 : i - 1);
190        x0 = null;
191      });
192      box.tabIndex = 0;
193      box.addEventListener('keydown', (e) => {
194        if (e.key === 'ArrowLeft') { e.preventDefault(); go(i - 1); }
195        if (e.key === 'ArrowRight') { e.preventDefault(); go(i + 1); }
196      });
197    }
198
199    function go(n) {
200      i = (n + imgs.length) % imgs.length;
201      track.style.transform = 'translateX(-' + (i * 100) + '%)';
202      dots.forEach((d, k) => d.setAttribute('aria-current', String(k === i)));
203      count.textContent = (i + 1) + ' / ' + imgs.length;
204    }
205    go(0);
206  });
207})();
208
209/* ──────────────────────────────────────────────────────
210   배치도 크게 보기
211   배치도를 누르면 전체 화면으로 열리고,
212   단계별로 확대하거나 끌어서 이동할 수 있습니다.
213────────────────────────────────────────────────────── */
214(function () {
215  const dlg  = document.getElementById('mapDlg');
216  const open = document.getElementById('mapOpen');
217  if (!dlg || !open || !dlg.showModal) return;
218
219  const scroll = document.getElementById('mapScroll');
220  const img    = document.getElementById('mapZoomImg');
221  const pct    = document.getElementById('mapPct');
222
223  const STEPS = [1, 1.6, 2.4, 3.5, 5];   // 배율 단계
224  let zi = 0;
225  let dragged = false;
226
227  // 화면 너비에 딱 맞는 크기 (좌우 여백 p-3 = 12px 씩)
228  function fitWidth() {
229    return Math.max(80, scroll.clientWidth - 24);
230  }
231
232  function setZoom(next, focus) {
233    const oldW = img.offsetWidth || fitWidth();
234    zi = Math.max(0, Math.min(STEPS.length - 1, next));
235
236    const newW = fitWidth() * STEPS[zi];
237    const ratio = newW / oldW;
238    const fx = focus ? focus.x : scroll.clientWidth / 2;
239    const fy = focus ? focus.y : scroll.clientHeight / 2;
240
241    img.style.width = newW + 'px';
242    pct.textContent = Math.round(STEPS[zi] * 100) + '%';
243    scroll.style.cursor = zi >= STEPS.length - 1 ? 'zoom-out' : 'zoom-in';
244
245    // 보고 있던 지점이 그대로 남도록 스크롤 위치를 옮깁니다
246    scroll.scrollLeft = (scroll.scrollLeft + fx) * ratio - fx;
247    scroll.scrollTop  = (scroll.scrollTop  + fy) * ratio - fy;
248  }
249
250  function reset() {
251    zi = 0;
252    img.style.width = fitWidth() + 'px';
253    pct.textContent = '100%';
254    scroll.style.cursor = 'zoom-in';
255    scroll.scrollLeft = 0;
256    scroll.scrollTop  = 0;
257  }
258
259  open.addEventListener('click', () => {
260    dlg.showModal();
261    reset();                       // 창이 열린 뒤라야 실제 너비를 알 수 있습니다
262  });
263
264  dlg.addEventListener('click', (e) => {
265    const btn = e.target.closest('[data-map]');
266    if (!btn) return;
267    const act = btn.dataset.map;
268    if (act === 'close') dlg.close();
269    if (act === 'in')    setZoom(zi + 1);
270    if (act === 'out')   setZoom(zi - 1);
271  });
272
273  // 배치도를 누르면 그 지점을 중심으로 한 단계씩 확대
274  img.addEventListener('click', (e) => {
275    if (dragged) { dragged = false; return; }
276    const r = scroll.getBoundingClientRect();
277    setZoom(zi >= STEPS.length - 1 ? 0 : zi + 1, { x: e.clientX - r.left, y: e.clientY - r.top });
278  });
279
280  // 마우스로 끌어서 이동
281  let drag = null;
282  scroll.addEventListener('pointerdown', (e) => {
283    if (e.pointerType === 'touch') return;         // 손가락은 기본 스크롤 사용
284    drag = { x: e.clientX, y: e.clientY, l: scroll.scrollLeft, t: scroll.scrollTop };
285    dragged = false;
286    try { scroll.setPointerCapture(e.pointerId); } catch (err) {}
287  });
288  scroll.addEventListener('pointermove', (e) => {
289    if (!drag) return;
290    const dx = e.clientX - drag.x, dy = e.clientY - drag.y;
291    if (Math.abs(dx) > 3 || Math.abs(dy) > 3) dragged = true;
292    scroll.scrollLeft = drag.l - dx;
293    scroll.scrollTop  = drag.t - dy;
294  });
295  ['pointerup', 'pointercancel', 'pointerleave'].forEach((ev) =>
296    scroll.addEventListener(ev, () => { drag = null; })
297  );
298
299  // Ctrl + 휠로 확대·축소
300  scroll.addEventListener('wheel', (e) => {
301    if (!e.ctrlKey) return;
302    e.preventDefault();
303    const r = scroll.getBoundingClientRect();
304    setZoom(zi + (e.deltaY < 0 ? 1 : -1), { x: e.clientX - r.left, y: e.clientY - r.top });
305  }, { passive: false });
306
307  // 키보드
308  dlg.addEventListener('keydown', (e) => {
309    if (e.key === '+' || e.key === '=') { e.preventDefault(); setZoom(zi + 1); }
310    if (e.key === '-' || e.key === '_') { e.preventDefault(); setZoom(zi - 1); }
311    if (e.key === '0')                  { e.preventDefault(); reset(); }
312  });
313
314  // 화면 크기가 바뀌면 다시 맞춥니다
315  let rt;
316  window.addEventListener('resize', () => {
317    if (!dlg.open) return;
318    clearTimeout(rt);
319    rt = setTimeout(() => setZoom(zi), 150);
320  });
321})();
322
323/* 푸터 연도 */
324(function () {
325  const y = document.getElementById('year');
326  if (y) y.textContent = new Date().getFullYear();
327})();

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.