PageSourceSearch

https://dwvil.com/js/showcase_product_event_tracker.js?1782109849

js dwvil.com collected 2026-10-02 00:31:23 UTC 11,574 bytes, 363 lines download raw bytes

1(function(window, document) {
2	'use strict';
3
4	// 기획전(shop_showcase) 위젯 brandscope 이벤트 트래커.
5	// 쇼핑 위젯 트래커(brand_product_event_tracker.js)와 동일한 호출 메커니즘
6	// (카드별 IntersectionObserver 0.8 + 500ms, 클릭 backfill)을 재사용하되,
7	// 이벤트명/where/속성 세트는 기획전 전용(events.yaml: *_showcase_widget)을 따른다.
8	if (typeof window.WeakSet === 'undefined' || typeof window.WeakMap === 'undefined' || typeof window.Set === 'undefined') {
9		try {
10			if (window.console && typeof window.console.error === 'function') {
11				window.console.error('[ShowcaseProductEventTracker]', {
12					reason: 'unknown_error',
13					stage: 'init',
14					sourceEvent: '',
15					widgetCode: '',
16					prodCode: '',
17					error: null
18				});
19			}
20		} catch (_error) {
21		}
22		return;
23	}
24
25	class ShowcaseProductEventTracker {
26		static init() {
27			if (ShowcaseProductEventTracker.initialized) {
28				return;
29			}
30
31			ShowcaseProductEventTracker.initialized = true;
32			ShowcaseProductEventTracker.ensureObserver();
33			ShowcaseProductEventTracker.scan();
34			ShowcaseProductEventTracker.flushQueue();
35		}
36
37		static scan(widgetCode) {
38			try {
39				const root = ShowcaseProductEventTracker.getScanRoot(widgetCode);
40				if (!root) {
41					return;
42				}
43
44				const cards = root.querySelectorAll('._shop_item[data-showcase-product-event-properties]');
45				cards.forEach(function(card) {
46					ShowcaseProductEventTracker.observeCard(card);
47				});
48			} catch (error) {
49				ShowcaseProductEventTracker.logFailure('scan_error', 'scan', '', widgetCode || '', '', error);
50			}
51		}
52
53		static getScanRoot(widgetCode) {
54			if (!widgetCode) {
55				return document;
56			}
57
58			return document.getElementById('container_' + widgetCode);
59		}
60
61		static observeCard(card) {
62			if (!card || ShowcaseProductEventTracker.observedCards.has(card)) {
63				return;
64			}
65
66			// 슬라이드(owl carousel) 모드는 무한 루프용으로 카드를 복제(.owl-item.cloned)한다.
67			// 복제 카드는 동일 상품의 중복 DOM이므로 노출·클릭 집계에서 제외한다(카드 1개당 1회 원칙).
68			if (ShowcaseProductEventTracker.isClonedCard(card)) {
69				return;
70			}
71
72			ShowcaseProductEventTracker.observedCards.add(card);
73			card.addEventListener('click', function(event) {
74				ShowcaseProductEventTracker.handleClick(card, event);
75			}, true);
76
77			const observer = ShowcaseProductEventTracker.ensureObserver();
78			if (observer) {
79				try {
80					observer.observe(card);
81				} catch (error) {
82					ShowcaseProductEventTracker.logFailure('observer_error', 'view', ShowcaseProductEventTracker.VIEW_EVENT, '', ShowcaseProductEventTracker.getProdCode(card), error);
83				}
84			}
85		}
86
87		static ensureObserver() {
88			if (ShowcaseProductEventTracker.observer) {
89				return ShowcaseProductEventTracker.observer;
90			}
91
92			if (typeof window.IntersectionObserver === 'undefined') {
93				ShowcaseProductEventTracker.logFailure('intersection_observer_unavailable', 'view', ShowcaseProductEventTracker.VIEW_EVENT, '', '', null);
94				return null;
95			}
96
97			try {
98				ShowcaseProductEventTracker.observer = new window.IntersectionObserver(function(entries, observer) {
99					entries.forEach(function(entry) {
100						ShowcaseProductEventTracker.handleIntersection(entry, observer);
101					});
102				}, {
103					threshold: 0.8
104				});
105			} catch (error) {
106				ShowcaseProductEventTracker.logFailure('observer_error', 'view', ShowcaseProductEventTracker.VIEW_EVENT, '', '', error);
107				ShowcaseProductEventTracker.observer = null;
108			}
109
110			return ShowcaseProductEventTracker.observer;
111		}
112
113		static handleIntersection(entry, observer) {
114			const card = entry.target;
115			try {
116				if (ShowcaseProductEventTracker.viewedCards.has(card)) {
117					observer.unobserve(card);
118					return;
119				}
120
121				if (entry.isIntersecting) {
122					if (ShowcaseProductEventTracker.intersectionTimers.has(card)) {
123						return;
124					}
125
126					const timer = window.setTimeout(function() {
127						ShowcaseProductEventTracker.intersectionTimers.delete(card);
128						ShowcaseProductEventTracker.fireView(card, ShowcaseProductEventTracker.IMPRESSION_SOURCE_VIEWPORT);
129						observer.unobserve(card);
130					}, ShowcaseProductEventTracker.VIEW_DELAY);
131
132					ShowcaseProductEventTracker.intersectionTimers.set(card, timer);
133					return;
134				}
135
136				ShowcaseProductEventTracker.clearIntersectionTimer(card);
137			} catch (error) {
138				ShowcaseProductEventTracker.logFailure('observer_error', 'view', ShowcaseProductEventTracker.VIEW_EVENT, '', ShowcaseProductEventTracker.getProdCode(card), error);
139			}
140		}
141
142		static handleClick(card, event) {
143			try {
144				if (!ShowcaseProductEventTracker.isProductDetailLinkClick(card, event.target)) {
145					return;
146				}
147
148				if (!ShowcaseProductEventTracker.viewedCards.has(card)) {
149					ShowcaseProductEventTracker.fireView(card, ShowcaseProductEventTracker.IMPRESSION_SOURCE_CLICK);
150				}
151
152				ShowcaseProductEventTracker.trackCardEvent(card, ShowcaseProductEventTracker.CLICK_EVENT, 'click');
153			} catch (error) {
154				ShowcaseProductEventTracker.logFailure('unknown_error', 'click', ShowcaseProductEventTracker.CLICK_EVENT, '', ShowcaseProductEventTracker.getProdCode(card), error);
155			}
156		}
157
158		static isClonedCard(card) {
159			if (!card || typeof card.closest !== 'function') {
160				return false;
161			}
162
163			return card.closest('.owl-item.cloned') !== null;
164		}
165
166		static isProductDetailLinkClick(card, target) {
167			if (!target || typeof target.closest !== 'function') {
168				return false;
169			}
170
171			const productLink = target.closest('a');
172			if (!productLink || !card.contains(productLink)) {
173				return false;
174			}
175
176			return (productLink.getAttribute('href') || '').indexOf('idx=') !== -1;
177		}
178
179		static fireView(card, impressionSource) {
180			if (ShowcaseProductEventTracker.viewedCards.has(card)) {
181				return true;
182			}
183
184			const tracked = ShowcaseProductEventTracker.trackCardEvent(card, ShowcaseProductEventTracker.VIEW_EVENT, 'view', {
185				extraProperties: {
186					impression_source: impressionSource || ShowcaseProductEventTracker.IMPRESSION_SOURCE_VIEWPORT
187				}
188			});
189			if (tracked) {
190				ShowcaseProductEventTracker.viewedCards.add(card);
191			}
192
193			return tracked;
194		}
195
196		static trackCardEvent(card, sourceEvent, stage, options) {
197			const properties = ShowcaseProductEventTracker.getEventProperties(card, stage, sourceEvent);
198			if (!properties) {
199				return false;
200			}
201			const eventProperties = ShowcaseProductEventTracker.getEventPropertiesWithBrandScopeMeta(properties, stage, options);
202
203			if (typeof window.BrandScope === 'undefined' || typeof window.BrandScope.track !== 'function') {
204				ShowcaseProductEventTracker.logFailure('brand_scope_unavailable', stage, sourceEvent, eventProperties.widget_code || '', eventProperties.prod_code || '', null);
205				return false;
206			}
207
208			try {
209				window.BrandScope.track(sourceEvent, eventProperties);
210				return true;
211			} catch (error) {
212				ShowcaseProductEventTracker.logFailure('track_error', stage, sourceEvent, eventProperties.widget_code || '', eventProperties.prod_code || '', error);
213			}
214
215			return false;
216		}
217
218		static getEventPropertiesWithBrandScopeMeta(properties, stage, options) {
219			const eventOptions = options || {};
220			const eventProperties = Object.assign({}, properties, {
221				action: stage,
222				content: eventOptions.content || 'product',
223				where: 'showcase_widget'
224			});
225
226			if (stage === 'view') {
227				eventProperties.target = 'component';
228			}
229
230			if (eventOptions.extraProperties) {
231				Object.assign(eventProperties, eventOptions.extraProperties);
232			}
233
234			return eventProperties;
235		}
236
237		static getEventProperties(card, stage, sourceEvent) {
238			let properties;
239			try {
240				properties = JSON.parse(card.getAttribute('data-showcase-product-event-properties') || '');
241			} catch (error) {
242				ShowcaseProductEventTracker.logFailure('invalid_properties_json', stage, sourceEvent, '', ShowcaseProductEventTracker.getProdCode(card), error);
243				return null;
244			}
245
246			if (!properties || typeof properties !== 'object') {
247				ShowcaseProductEventTracker.logFailure('missing_required_properties', stage, sourceEvent, '', '', null);
248				return null;
249			}
250
251			const requiredKeys = [
252				'prod_code',
253				'menu_code',
254				'widget_code',
255				'showcase_code',
256				'sort_type',
257				'slot_index',
258				'page_no',
259				'ranking_source',
260				'ranking_fallback_reason',
261				'policy_snapshot_id'
262			];
263
264			for (let i = 0; i < requiredKeys.length; i++) {
265				if (!Object.prototype.hasOwnProperty.call(properties, requiredKeys[i])) {
266					ShowcaseProductEventTracker.logFailure('missing_required_properties', stage, sourceEvent, properties.widget_code || '', properties.prod_code || '', null);
267					return null;
268				}
269			}
270
271			if (properties.ranking_fallback_reason === null) {
272				properties.ranking_fallback_reason = '';
273			}
274
275			return properties;
276		}
277
278		static clearIntersectionTimer(card) {
279			if (!ShowcaseProductEventTracker.intersectionTimers.has(card)) {
280				return;
281			}
282
283			window.clearTimeout(ShowcaseProductEventTracker.intersectionTimers.get(card));
284			ShowcaseProductEventTracker.intersectionTimers.delete(card);
285		}
286
287		static flushQueue() {
288			const queue = window.__showcaseProductEventTrackerQueue;
289			if (!Array.isArray(queue) || queue.length === 0) {
290				window.__showcaseProductEventTrackerQueue = [];
291				return;
292			}
293
294			window.__showcaseProductEventTrackerQueue = [];
295			queue.forEach(function(item) {
296				if (!item || item.type !== 'scan') {
297					return;
298				}
299
300				ShowcaseProductEventTracker.scan(item.widgetCode);
301			});
302		}
303
304		static getProdCode(card) {
305			try {
306				const properties = JSON.parse(card.getAttribute('data-showcase-product-event-properties') || '{}');
307				return properties && properties.prod_code ? properties.prod_code : '';
308			} catch (error) {
309				return '';
310			}
311		}
312
313		static logFailure(reason, stage, sourceEvent, widgetCode, prodCode, error) {
314			try {
315				const key = [reason, stage, sourceEvent, widgetCode].join(':');
316				if (ShowcaseProductEventTracker.loggedFailures.has(key)) {
317					return;
318				}
319
320				ShowcaseProductEventTracker.loggedFailures.add(key);
321				if (window.console && typeof window.console.error === 'function') {
322					window.console.error('[ShowcaseProductEventTracker]', {
323						reason: reason,
324						stage: stage,
325						sourceEvent: sourceEvent,
326						widgetCode: widgetCode,
327						prodCode: prodCode,
328						error: error
329					});
330				}
331			} catch (_error) {
332			}
333		}
334	}
335
336	ShowcaseProductEventTracker.VIEW_EVENT = 'view_component_product_showcase_widget';
337	ShowcaseProductEventTracker.CLICK_EVENT = 'click_product_showcase_widget';
338	ShowcaseProductEventTracker.IMPRESSION_SOURCE_VIEWPORT = 'viewport';
339	ShowcaseProductEventTracker.IMPRESSION_SOURCE_CLICK = 'click';
340	ShowcaseProductEventTracker.VIEW_DELAY = 500;
341	ShowcaseProductEventTracker.initialized = false;
342	ShowcaseProductEventTracker.observedCards = new WeakSet();
343	ShowcaseProductEventTracker.viewedCards = new WeakSet();
344	ShowcaseProductEventTracker.intersectionTimers = new WeakMap();
345	ShowcaseProductEventTracker.loggedFailures = new Set();
346	ShowcaseProductEventTracker.observer = null;
347
348	window.ShowcaseProductEventTracker = ShowcaseProductEventTracker;
349
350	function bootstrapShowcaseProductEventTracker() {
351		try {
352			ShowcaseProductEventTracker.init();
353		} catch (error) {
354			ShowcaseProductEventTracker.logFailure('unknown_error', 'init', '', '', '', error);
355		}
356	}
357
358	if (document.readyState === 'loading') {
359		document.addEventListener('DOMContentLoaded', bootstrapShowcaseProductEventTracker);
360	} else {
361		bootstrapShowcaseProductEventTracker();
362	}
363})(window, document);

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.