PageSourceSearch

https://atalkbizplus.co.kr/js/custom.js

js atalkbizplus.co.kr collected 2026-10-06 03:32:43 UTC 10,438 bytes, 307 lines download raw bytes

1// DOMContentLoaded 이후에 로드되는 경우(동적 include)도 즉시 실행되도록 보조 함수
2function onReady(fn) {
3    if (document.readyState === 'loading') {
4        document.addEventListener('DOMContentLoaded', fn, { once: true });
5    } else {
6        fn();
7    }
8}
9
10// -------------------------------------------------스크롤 시 애니메이션 실행
11onReady(() => {
12    const boxes = document.querySelectorAll('.animation');
13    // 뷰포트 너비에 따라 threshold 조절
14    const thresholdValue = window.innerWidth <= 767 ? 0.3 : 0.3;
15    const observer = new IntersectionObserver(
16        (entries, observer) => {
17            entries.forEach((entry) => {
18                if (entry.isIntersecting) {
19                    entry.target.classList.add('show');
20                    observer.unobserve(entry.target); // 애니메이션 한번만 실행
21                }
22            });
23        },
24        {
25            threshold: thresholdValue,
26        },
27    );
28    boxes.forEach((box) => observer.observe(box));
29});
30
31//-----------------------------------------------------------헤더고정
32onReady(function () {
33    const header = document.querySelector('#header');
34    if (!header) return; // #header 없으면 종료 (에러 방지)
35
36    window.addEventListener('scroll', function () {
37        header.classList.toggle('on', window.scrollY >= 100);
38    });
39});
40
41//-------------------------------------------------헤더 서브메뉴 열고닫기 (확정판)
42onReady(() => {
43    const nav = document.querySelector('#nav');
44    if (!nav) return;
45
46    const menus = nav.querySelectorAll('.sub_open');
47
48    const closeAll = () => {
49        nav.querySelectorAll('.sub.on').forEach((el) => el.classList.remove('on'));
50        nav.querySelectorAll('.sub_open.active').forEach((el) => el.classList.remove('active'));
51    };
52
53    menus.forEach((menu) => {
54        const sub = menu.querySelector('.sub');
55        if (!sub) return;
56
57        // 마우스 올리면 열기
58        menu.addEventListener('mouseenter', () => {
59            closeAll();
60            sub.classList.add('on');
61            menu.classList.add('active');
62        });
63
64        // 메뉴 + 서브메뉴 영역에서 마우스 빠지면 닫기
65        menu.addEventListener('mouseleave', () => {
66            sub.classList.remove('on');
67            menu.classList.remove('active');
68        });
69    });
70});
71
72//-------------------------------------------------맨 위로
73function goTop() {
74    $(window).scrollTop(0);
75}
76
77//-------------------------------------------------플로팅배너
78onReady(() => {
79    const floating = document.querySelector('.floating');
80    const footer = document.querySelector('footer'); // #footer 쓰시면 거기에 맞게 바꾸셔도 됩니다.
81
82    if (!floating || !footer) return; // 혹시 요소 없으면 조용히 종료
83
84    window.addEventListener('scroll', () => {
85        const scrollY = window.scrollY || window.pageYOffset;
86        const footerRect = footer.getBoundingClientRect();
87        const windowH = window.innerHeight;
88
89        // footer가 화면 안에 보이는지 체크
90        const footerInView = footerRect.top <= windowH && footerRect.bottom >= 0;
91
92        // 조건: 스크롤 500 이상 & footer 안 보일 때만 .on
93        if (scrollY >= 500 && !footerInView) {
94            floating.classList.add('on');
95        } else {
96            floating.classList.remove('on');
97        }
98    });
99});
100
101//-------------------------------------------------모바일 메뉴 열고닫기
102onReady(function () {
103    const menuBtn = document.querySelector('#header_m .menu_btn'); // 상단 메뉴 버튼
104    const mMenu = document.getElementById('m_menu'); // 전체 오버레이
105
106    // 필수 요소가 없으면 종료 (페이지마다 헤더/메뉴가 없을 수 있음)
107    if (!menuBtn || !mMenu) return;
108
109    const mMenuInner = mMenu.querySelector('.inner'); // 안쪽 박스
110    const closeBtn = mMenu.querySelector('.close'); // 닫기 버튼
111
112    // 1) 메뉴 버튼 클릭 -> 열기
113    menuBtn.addEventListener('click', function (e) {
114        e.preventDefault();
115        mMenu.classList.add('open');
116    });
117
118    // 2) 오버레이 배경 클릭 -> 닫기
119    mMenu.addEventListener('click', function () {
120        mMenu.classList.remove('open');
121    });
122
123    // 3) 내부 클릭 -> 닫히지 않게
124    if (mMenuInner) {
125        mMenuInner.addEventListener('click', function (e) {
126            e.stopPropagation();
127        });
128    }
129
130    // 4) 닫기 버튼 클릭 -> 닫기
131    if (closeBtn) {
132        closeBtn.addEventListener('click', function (e) {
133            e.preventDefault();
134            mMenu.classList.remove('open');
135        });
136    }
137});
138
139//-------------------------------------------------사이트 팝업
140onReady(() => {
141    if (window.__sitePopupInit) return;
142    window.__sitePopupInit = true;
143    if (/\/login(\.html)?$/.test(location.pathname)) return;
144
145    function escapeHtml(value) {
146        return String(value ?? '')
147            .replace(/&/g, '&amp;')
148            .replace(/</g, '&lt;')
149            .replace(/>/g, '&gt;')
150            .replace(/"/g, '&quot;')
151            .replace(/'/g, '&#39;');
152    }
153
154    function normalizePath(value) {
155        let p = String(value || '').trim();
156        if (!p) return '/';
157        try {
158            const u = new URL(p, location.origin);
159            p = u.pathname || '/';
160        } catch {}
161        if (!p.startsWith('/')) p = '/' + p;
162        if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
163        if (p === '/index.html') return '/';
164        return p || '/';
165    }
166
167    function hideKey(item) {
168        return `site-popup-hide-until:${item.id}`;
169    }
170
171    function isHidden(item) {
172        try {
173            const raw = localStorage.getItem(hideKey(item));
174            if (!raw) return false;
175            const until = Number(raw);
176            return Number.isFinite(until) && until > Date.now();
177        } catch {
178            return false;
179        }
180    }
181
182    function setHidden(item, days) {
183        try {
184            const until = Date.now() + Math.max(1, Number(days) || 1) * 24 * 60 * 60 * 1000;
185            localStorage.setItem(hideKey(item), String(until));
186        } catch {}
187    }
188
189    function createLayer(item) {
190        const layer = document.createElement('div');
191        layer.className = 'site-popup-layer';
192        layer.setAttribute('role', 'dialog');
193        layer.setAttribute('aria-modal', 'true');
194
195        const modal = document.createElement('div');
196        modal.className = 'site-popup-modal';
197        modal.dataset.popupId = String(item.id || '');
198
199        const mobileBp = Number(item.mobile_breakpoint || 768);
200        if (window.innerWidth <= mobileBp) {
201            modal.classList.add('is-mobile');
202        } else {
203            modal.classList.add('is-desktop');
204            const top = Number(item.top_px || 100);
205            const left = Number(item.left_px || 100);
206            modal.style.top = `${Math.max(0, top)}px`;
207            modal.style.left = `${Math.max(0, left)}px`;
208        }
209
210        const body = document.createElement('div');
211        body.className = 'site-popup-body';
212        body.innerHTML = String(item.popup_html || '')
213            .replace(/\{\{\$POPUP_IMAGE\}\}/g, escapeHtml(item.image_url || ''))
214            .replace(/\{\{\$POPUP_LINK\}\}/g, escapeHtml(item.link_url || '#'))
215            .replace(/\{\{\$POPUP_TARGET\}\}/g, escapeHtml(item.link_target || '_self'))
216            .replace(/\{\{\$POPUP_TITLE\}\}/g, escapeHtml(item.title || 'popup'));
217
218        const footer = document.createElement('div');
219        footer.className = 'site-popup-footer';
220
221        const closeBtn = document.createElement('button');
222        closeBtn.type = 'button';
223        closeBtn.className = 'site-popup-close';
224        closeBtn.textContent = '닫기';
225
226        const hideBtn = document.createElement('button');
227        hideBtn.type = 'button';
228        hideBtn.className = 'site-popup-hide';
229        const hideDays = Math.max(1, Number(item.hide_days) || 1);
230        hideBtn.textContent = hideDays === 1 ? '하루 동안 안보기' : `${hideDays}일간 안보기`;
231
232        let keyHandler = null;
233        const dismiss = () => {
234            if (keyHandler) {
235                document.removeEventListener('keydown', keyHandler);
236                keyHandler = null;
237            }
238            try {
239                layer.remove();
240            } catch {}
241            showNext();
242        };
243
244        closeBtn.addEventListener('click', dismiss);
245        hideBtn.addEventListener('click', () => {
246            setHidden(item, hideDays);
247            dismiss();
248        });
249
250        layer.addEventListener('click', (e) => {
251            if (e.target === layer) dismiss();
252        });
253        keyHandler = function escClose(e) {
254            if (e.key === 'Escape') dismiss();
255        };
256        document.addEventListener('keydown', keyHandler);
257
258        footer.appendChild(closeBtn);
259        footer.appendChild(hideBtn);
260        modal.appendChild(body);
261        modal.appendChild(footer);
262        layer.appendChild(modal);
263        return layer;
264    }
265
266    let queue = [];
267    let activeLayer = null;
268    let loading = false;
269
270    function showNext() {
271        if (activeLayer) {
272            try {
273                activeLayer.remove();
274            } catch {}
275            activeLayer = null;
276        }
277        while (queue.length && isHidden(queue[0])) {
278            queue.shift();
279        }
280        if (!queue.length) return;
281        const item = queue.shift();
282        activeLayer = createLayer(item);
283        document.body.appendChild(activeLayer);
284        requestAnimationFrame(() => {
285            activeLayer?.classList.add('is-open');
286        });
287    }
288
289    async function loadPopups() {
290        if (loading) return;
291        loading = true;
292        try {
293            const path = normalizePath(location.pathname || '/');
294            const siteApiBase = (window.__APP_CONFIG__ && window.__APP_CONFIG__.SITE_API_BASE) || '/api/site';
295            const resp = await fetch(`${siteApiBase}/popup/active?path=${encodeURIComponent(path)}`, {
296                credentials: 'include',
297            });
298            const json = await resp.json().catch(() => ({}));
299            if (!resp.ok || !json.success) return;
300            queue = Array.isArray(json.items) ? json.items.slice() : [];
301            if (!queue.length) return;
302            showNext();
303        } catch {}
304    }
305
306    loadPopups();
307});

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.