PageSourceSearch

https://landing.sixshop.com/resources/js/require/home/landing-menu-tracking.js?v=09231009

js sixshop.com collected 2026-10-01 10:42:09 UTC 13,435 bytes, 393 lines download raw bytes

1/**
2 * 랜딩 메뉴 클릭·호버 GTM 트래킹.
3 *
4 * 이벤트
5 *   click_landing_menu / hover_landing_menu
6 *
7 * 매개변수
8 *   area        desktop_header | desktop_footer | mobile_header | mobile_side_menu | mobile_footer
9 *   menu_depth  1, 2, ...
10 *   parent_menu 상위 메뉴 이름 (depth 1 은 빈 값)
11 *   menu_name   해당 메뉴 이름
12 *   link_url    클릭 후 이동 URL
13 *
14 * 메뉴 항목마다 data 속성을 붙이면 메뉴가 늘 때마다 누락이 생기므로,
15 * DOM 위치에서 area·depth·상위 메뉴를 역산한다. 마크업은 건드리지 않는다.
16 */
17(function (root, factory) {
18    var api = factory();
19
20    if (typeof module !== 'undefined' && module.exports) {
21        module.exports = api;
22    }
23
24    root.SixshopLandingMenuTracking = api;
25})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
26    var CLICK_EVENT = 'click_landing_menu';
27    var HOVER_EVENT = 'hover_landing_menu';
28
29    /* 푸터는 데스크톱·모바일이 같은 마크업이라 뷰포트로 가른다.
30       home-common.css 의 모바일 분기와 같은 768px 를 쓴다. */
31    var MOBILE_BREAKPOINT = 768;
32
33    function isPresent(value) {
34        return value !== undefined && value !== null && value !== '';
35    }
36
37    function hasClass(element, name) {
38        if (!element || !element.classList) {
39            return false;
40        }
41        return element.classList.contains(name);
42    }
43
44    function closestBySelector(element, predicate) {
45        var node = element;
46        while (node && node.nodeType === 1) {
47            if (predicate(node)) {
48                return node;
49            }
50            node = node.parentElement;
51        }
52        return null;
53    }
54
55    function isInside(element, predicate) {
56        return !!closestBySelector(element, predicate);
57    }
58
59    /* 이름에 섞이면 안 되는 자식들.
60       1뎁스 메뉴(li)를 대상으로 잡으면 그 안의 펼침 패널까지 딸려오므로 패널을 통째로 뺀다. */
61    var NAME_EXCLUDED_CLASSES = [
62        'mega-menu-wrapper',
63        'sub-menu-wrapper',
64        'nav-mobile-sublist',
65        'mega-menu-desc',
66    ];
67
68    function isExcludedFromName(node) {
69        for (var i = 0; i < NAME_EXCLUDED_CLASSES.length; i += 1) {
70            if (hasClass(node, NAME_EXCLUDED_CLASSES[i])) {
71                return true;
72            }
73        }
74        return false;
75    }
76
77    function collectName(source) {
78        var text = '';
79
80        if (!source.childNodes || !source.childNodes.length) {
81            return source.textContent || '';
82        }
83
84        for (var i = 0; i < source.childNodes.length; i += 1) {
85            var node = source.childNodes[i];
86            if (node.nodeType === 3) {
87                text += node.nodeValue;
88                continue;
89            }
90            if (node.nodeType !== 1) {
91                continue;
92            }
93            var tag = (node.tagName || '').toLowerCase();
94            /* 화살표 아이콘·배지 이미지는 이름이 아니다 */
95            if (tag === 'i' || tag === 'img') {
96                continue;
97            }
98            if (isExcludedFromName(node)) {
99                continue;
100            }
101            text += collectName(node);
102        }
103
104        return text;
105    }
106
107    /**
108     * 메뉴 라벨만 남긴다.
109     * 아이콘형 메가 메뉴는 strong 에 라벨, 그 뒤에 설명이 붙으므로 strong 을 우선한다.
110     * 단 strong 우선은 메가 메뉴 항목에만 적용한다. 1뎁스 li 에 그대로 쓰면
111     * 펼침 패널 안쪽 strong 이 잡혀 상위 메뉴 이름이 엉뚱해진다.
112     */
113    function readMenuName(element) {
114        if (!element) {
115            return '';
116        }
117
118        var source = element;
119        if (hasClass(element, 'mega-menu-item') && typeof element.querySelector === 'function') {
120            source = element.querySelector('strong') || element;
121        }
122
123        return collectName(source).replace(/\s+/g, ' ').trim();
124    }
125
126    function readLinkUrl(element) {
127        if (!element) {
128            return '';
129        }
130
131        /* href 프로퍼티는 브라우저가 절대 URL 로 정규화해 준다. */
132        var href = element.href;
133        if (isPresent(href) && typeof href === 'string') {
134            return href;
135        }
136        if (typeof element.getAttribute === 'function') {
137            var attr = element.getAttribute('href');
138            if (isPresent(attr)) {
139                return attr;
140            }
141        }
142
143        /* 1뎁스 메뉴는 li 를 대상으로 잡으므로 링크가 자식 a 에 있다.
144           펼침 패널 안쪽 링크를 물어오지 않도록 직속 자식만 본다.
145           토글 전용 버튼처럼 정말 이동이 없는 메뉴는 빈 값으로 남는다. */
146        if (element.childNodes) {
147            for (var i = 0; i < element.childNodes.length; i += 1) {
148                var child = element.childNodes[i];
149                if (child.nodeType !== 1 || (child.tagName || '').toLowerCase() !== 'a') {
150                    continue;
151                }
152                if (isPresent(child.href) && typeof child.href === 'string') {
153                    return child.href;
154                }
155                if (typeof child.getAttribute === 'function') {
156                    var childAttr = child.getAttribute('href');
157                    if (isPresent(childAttr)) {
158                        return childAttr;
159                    }
160                }
161            }
162        }
163
164        return '';
165    }
166
167    function isMobileViewport(scope) {
168        if (!scope) {
169            return false;
170        }
171        var width = scope.innerWidth;
172        if (typeof width !== 'number') {
173            return false;
174        }
175        return width <= MOBILE_BREAKPOINT;
176    }
177
178    /** 요소가 어느 메뉴 영역에 속하는지 판별한다. 못 찾으면 빈 문자열. */
179    function resolveArea(element, scope) {
180        if (!element) {
181            return '';
182        }
183
184        if (isInside(element, function (node) { return hasClass(node, 'mobile-nav-header'); })) {
185            return 'mobile_header';
186        }
187        if (isInside(element, function (node) { return hasClass(node, 'nav-mobile'); })) {
188            return 'mobile_side_menu';
189        }
190        if (isInside(element, function (node) { return hasClass(node, 'footer'); })) {
191            return isMobileViewport(scope) ? 'mobile_footer' : 'desktop_footer';
192        }
193        if (isInside(element, function (node) { return hasClass(node, 'navi-list'); })) {
194            return 'desktop_header';
195        }
196        return '';
197    }
198
199    /**
200     * 클릭·호버 대상이 될 메뉴 요소를 찾는다.
201     * 아이콘이나 span 을 눌러도 메뉴 단위로 잡히도록 위로 거슬러 올라간다.
202     */
203    function resolveMenuElement(element) {
204        return closestBySelector(element, function (node) {
205            return hasClass(node, 'mega-menu-item')
206                || hasClass(node, 'sub-menu-item')
207                || hasClass(node, 'nav-mobile-btn')
208                || hasClass(node, 'footer-menu-btn')
209                || hasClass(node, 'mobile-nav-login')
210                || (node.parentElement
211                    && node.parentElement.classList
212                    && node.parentElement.classList.contains('navi-list')
213                    && (node.tagName || '').toLowerCase() === 'li');
214        });
215    }
216
217    function resolveMenuDepth(menuElement, area) {
218        if (!menuElement) {
219            return 0;
220        }
221
222        if (area === 'desktop_header') {
223            /* 메가 메뉴·서브 메뉴 항목은 2뎁스, navi-list 직속은 1뎁스 */
224            if (hasClass(menuElement, 'mega-menu-item') || hasClass(menuElement, 'sub-menu-item')) {
225                return 2;
226            }
227            return 1;
228        }
229
230        if (area === 'mobile_side_menu') {
231            /* 펼쳐지는 하위 목록(nav-mobile-sublist) 안이면 2뎁스 */
232            var inSublist = isInside(menuElement, function (node) {
233                return hasClass(node, 'nav-mobile-sublist');
234            });
235            return inSublist ? 2 : 1;
236        }
237
238        return 1;
239    }
240
241    /** depth 2 항목의 상위 메뉴 이름. 1뎁스면 빈 값. */
242    function resolveParentMenu(menuElement, area, depth) {
243        if (!menuElement || depth < 2) {
244            return '';
245        }
246
247        if (area === 'desktop_header') {
248            var li = closestBySelector(menuElement, function (node) {
249                return node.parentElement
250                    && node.parentElement.classList
251                    && node.parentElement.classList.contains('navi-list');
252            });
253            if (!li || typeof li.querySelector !== 'function') {
254                return '';
255            }
256            return readMenuName(li.querySelector('a'));
257        }
258
259        if (area === 'mobile_side_menu') {
260            var sublist = closestBySelector(menuElement, function (node) {
261                return hasClass(node, 'nav-mobile-sublist');
262            });
263            if (!sublist || !sublist.parentElement || typeof sublist.parentElement.querySelector !== 'function') {
264                return '';
265            }
266            return readMenuName(sublist.parentElement.querySelector('.nav-mobile-btn'));
267        }
268
269        return '';
270    }
271
272    function buildLandingMenuPayload(eventName, params) {
273        var source = params || {};
274        var payload = { event: eventName };
275
276        if (isPresent(source.area)) {
277            payload.area = source.area;
278        }
279        if (isPresent(source.menuDepth)) {
280            payload.menu_depth = source.menuDepth;
281        }
282        if (isPresent(source.parentMenu)) {
283            payload.parent_menu = source.parentMenu;
284        }
285        if (isPresent(source.menuName)) {
286            payload.menu_name = source.menuName;
287        }
288        if (isPresent(source.linkUrl)) {
289            payload.link_url = source.linkUrl;
290        }
291
292        return payload;
293    }
294
295    /** 이벤트 대상 요소에서 GTM 매개변수를 뽑는다. 메뉴가 아니면 null. */
296    function resolveMenuParams(element, scope) {
297        var menuElement = resolveMenuElement(element);
298        if (!menuElement) {
299            return null;
300        }
301
302        var area = resolveArea(menuElement, scope);
303        if (!area) {
304            return null;
305        }
306
307        var depth = resolveMenuDepth(menuElement, area);
308        var menuName = readMenuName(menuElement);
309        if (!isPresent(menuName)) {
310            return null;
311        }
312
313        return {
314            area: area,
315            menuDepth: depth,
316            parentMenu: resolveParentMenu(menuElement, area, depth),
317            menuName: menuName,
318            linkUrl: readLinkUrl(menuElement),
319            element: menuElement,
320        };
321    }
322
323    function pushLandingMenuEvent(eventName, params, targetWindow) {
324        var scope = targetWindow || (typeof window !== 'undefined' ? window : undefined);
325        var payload = buildLandingMenuPayload(eventName, params);
326
327        if (!scope || !scope.dataLayer || typeof scope.dataLayer.push !== 'function') {
328            return payload;
329        }
330
331        scope.dataLayer.push(payload);
332        return payload;
333    }
334
335    /**
336     * 문서 전체에 위임으로 한 번만 건다. 메뉴가 추가돼도 코드를 고칠 필요가 없다.
337     * 호버는 같은 메뉴를 다시 지나갈 때 중복 발송되지 않도록 직전 대상을 기억한다.
338     */
339    function initLandingMenuTracking(doc, targetWindow) {
340        var scope = targetWindow || (typeof window !== 'undefined' ? window : undefined);
341        var document_ = doc || (typeof document !== 'undefined' ? document : undefined);
342
343        if (!document_ || typeof document_.addEventListener !== 'function') {
344            return false;
345        }
346
347        document_.addEventListener('click', function (event) {
348            var params = resolveMenuParams(event.target, scope);
349            if (params) {
350                pushLandingMenuEvent(CLICK_EVENT, params, scope);
351            }
352        }, true);
353
354        var lastHovered = null;
355
356        document_.addEventListener('mouseover', function (event) {
357            var params = resolveMenuParams(event.target, scope);
358            if (!params) {
359                return;
360            }
361            if (lastHovered === params.element) {
362                return;
363            }
364            lastHovered = params.element;
365            pushLandingMenuEvent(HOVER_EVENT, params, scope);
366        }, true);
367
368        document_.addEventListener('mouseout', function (event) {
369            var params = resolveMenuParams(event.target, scope);
370            if (params && lastHovered === params.element) {
371                lastHovered = null;
372            }
373        }, true);
374
375        return true;
376    }
377
378    return {
379        CLICK_EVENT: CLICK_EVENT,
380        HOVER_EVENT: HOVER_EVENT,
381        MOBILE_BREAKPOINT: MOBILE_BREAKPOINT,
382        buildLandingMenuPayload: buildLandingMenuPayload,
383        readMenuName: readMenuName,
384        readLinkUrl: readLinkUrl,
385        resolveArea: resolveArea,
386        resolveMenuElement: resolveMenuElement,
387        resolveMenuDepth: resolveMenuDepth,
388        resolveParentMenu: resolveParentMenu,
389        resolveMenuParams: resolveMenuParams,
390        pushLandingMenuEvent: pushLandingMenuEvent,
391        initLandingMenuTracking: initLandingMenuTracking,
392    };
393});

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.