PageSourceSearch

https://static.danawa.com/globaljs/com/danawa/common/uxlog/priceCompareUxlog.js?v=1.0.4

js danawa.com collected 2026-09-24 08:48:32 UTC 25,384 bytes, 678 lines download raw bytes

1/**
2 * DSAC_V2 로깅을 간편하게 사용하기 위한 래퍼 스크립트 (페이로드 기반)
3 * @author Danawa
4 * @version 2.0.0
5 * @description JSON + Base64 인코딩 방식으로 안전한 로깅 데이터 전송
6 */
7
8(function (window) {
9    'use strict';
10
11    // 스크립트 로드 시점에 버전 파라미터 캡처 (document.currentScript는 즉시 실행될 때만 유효)
12    const currentScript = document.currentScript || document.querySelector('script[src*="priceCompareUxlog.js"]');
13    const scriptSrc = currentScript?.src || '';
14    const versionMatch = scriptSrc.match(/[?&]v=([^&]+)/);
15    const SCRIPT_VERSION = versionMatch ? versionMatch[1] : '';
16
17    /**
18     * @typedef {Object} LogPayload
19     * @property {'pv'|'c'} action - 액션 타입
20     * @property {string} [page] - 페이지 코드 (예: 'AAA')
21     * @property {string} [mid] - 멤버 ID
22     * @property {string} [area] - 영역 코드 (클릭 로깅 시 필수, 예: 'aa1', 'button', 'link')
23     * @property {Params} [params] - 유동 파라미터
24     */
25
26    /**
27     * @typedef {Object} Params
28     * @property {string} [click_name] - 내부클릭이벤트명
29     * @property {string|number} [productId] - 상품 ID
30     * @property {number} [category1_seq] - 물리카테고리 대분류 코드
31     * @property {number} [category2_seq] - 물리카테고리 중분류 코드
32     * @property {number} [category3_seq] - 물리카테고리 소분류 코드
33     * @property {number} [category4_seq] - 물리카테고리 세분류 코드
34     */
35
36    /**
37     * PriceCompareUxlog 클래스
38     * DSAC_V2 로깅을 간편하게 사용할 수 있도록 도와주는 유틸리티
39     */
40    function PriceCompareUxlog() {
41        this.defaultMemberCode = null;
42        this.defaultTrackingId = null;
43        this.defaultPageCode = null;
44        this.isDebugMode = false;
45        this.isInitialized = false;
46        this.eventDelegationEnabled = false;
47        this.eventListenerAttached = false;
48        this.eventListeners = []; // 이벤트 리스너 저장용
49        this.delegationContainer = null; // 위임 컨테이너 저장용
50        this.scriptVersion = SCRIPT_VERSION; // 스크립트 버전 저장
51    }
52
53    /**
54     * 기본 멤버 코드 설정
55     * @param {number|string} memberCode 멤버 코드
56     */
57    PriceCompareUxlog.prototype.setDefaultMemberCode = function (memberCode) {
58        this.defaultMemberCode = memberCode ? parseInt(memberCode) : null;
59        return this;
60    };
61
62    /**
63     * 기본 트래킹 ID 설정
64     * @param {string} trackingId 트래킹 ID
65     */
66    PriceCompareUxlog.prototype.setDefaultTrackingId = function (trackingId) {
67        this.defaultTrackingId = typeof trackingId === 'string' ? trackingId : null;
68        return this;
69    };
70
71    /**
72     * 기본 페이지 코드 설정
73     * @param {string} pageCode 페이지 코드
74     */
75    PriceCompareUxlog.prototype.setDefaultPageCode = function (pageCode) {
76        this.defaultPageCode = pageCode || null;
77        return this;
78    };
79
80    /**
81     * 디버그 모드 설정
82     * @param {boolean} debug 디버그 모드 여부
83     */
84    PriceCompareUxlog.prototype.setDebugMode = function (debug) {
85        this.isDebugMode = !!debug;
86        return this;
87    };
88    // DSAC 로드 상태 관리 (모듈 레벨 변수)
89    let dsacLoadingPromise = null;
90
91    /**
92     * DSAC_V2 동적 로드 및 초기화
93     */
94    PriceCompareUxlog.prototype.loadDSAC = function () {
95        const self = this;
96
97        // 이미 로드되어 있는 경우
98        if (typeof DSAC_V2 === 'object' && DSAC_V2 !== null) {
99            self.isInitialized = true;
100            return Promise.resolve(true);
101        }
102
103        // 이미 로드 중인 경우 동일한 Promise 반환 (중복 호출 방지)
104        if (dsacLoadingPromise) {
105            if (self.isDebugMode) {
106                console.log('PriceCompareUxlog: DSAC_V2 로드 중 - 기존 Promise 반환');
107            }
108            return dsacLoadingPromise;
109        }
110
111        // 새로운 로드 Promise 생성
112        dsacLoadingPromise = new Promise((resolve) => {
113            // 스크립트 로드 시점에 캡처된 버전 사용
114            const versionParam = self.scriptVersion ? '?v=' + self.scriptVersion : '';
115
116            // 환경에 따른 DSAC URL 결정
117            const isDevEnvironment =
118                window.location.hostname.includes('localhost') ||
119                window.location.hostname.includes('-t') ||
120                window.location.hostname.includes('-local');
121            const isStagingEnvironment =
122                window.location.hostname.includes('-s') || window.location.hostname.includes('-stg');
123
124            const dsacUrl = isDevEnvironment
125                ? 'https://static-t.danawa.com/globaljs/static/bdl/dsac.min.js' + versionParam
126                : isStagingEnvironment
127                  ? 'https://static-s.danawa.com/globaljs/static/bdl/dsac.min.js' + versionParam
128                  : 'https://static.danawa.com/globaljs/static/bdl/dsac.min.js' + versionParam;
129
130            if (self.isDebugMode) {
131                console.log('PriceCompareUxlog: DSAC_V2 로드 시작:', {
132                    url: dsacUrl,
133                    version: self.scriptVersion || '(버전 없음)'
134                });
135            }
136
137            // 동적 스크립트 로드
138            const script = document.createElement('script');
139            script.src = dsacUrl;
140            script.async = true;
141
142            script.onload = function () {
143                // 로드 완료 후 DSAC_V2 객체 확인
144                let attempts = 0;
145                const maxAttempts = 20; // 2초 대기
146                const checkInterval = setInterval(() => {
147                    attempts++;
148                    if (typeof DSAC_V2 === 'object' && DSAC_V2 !== null) {
149                        self.isInitialized = true;
150                        clearInterval(checkInterval);
151                        dsacLoadingPromise = null; // Promise 완료 후 초기화
152                        if (self.isDebugMode) {
153                            console.log('PriceCompareUxlog: DSAC_V2 로드 완료');
154                        }
155                        resolve(true);
156                    } else if (attempts >= maxAttempts) {
157                        clearInterval(checkInterval);
158                        dsacLoadingPromise = null; // Promise 완료 후 초기화
159                        if (self.isDebugMode) {
160                            console.warn('PriceCompareUxlog: DSAC_V2 객체 초기화 타임아웃');
161                        }
162                        resolve(false);
163                    }
164                }, 100);
165            };
166
167            script.onerror = function () {
168                dsacLoadingPromise = null; // Promise 완료 후 초기화
169                if (self.isDebugMode) {
170                    console.error('PriceCompareUxlog: DSAC_V2 스크립트 로드 실패:', dsacUrl);
171                }
172                resolve(false);
173            };
174
175            document.head.appendChild(script);
176        });
177
178        return dsacLoadingPromise;
179    };
180
181    /**
182     * DSAC_V2 초기화 대기 (하위 호환성을 위한 별칭)
183     * @deprecated loadDSAC() 사용 권장
184     */
185    PriceCompareUxlog.prototype.waitForDSAC = function () {
186        return this.loadDSAC();
187    };
188    /**
189     * DSAC_V2 로깅 실행 (Base64 페이로드 디코딩)
190     * @param {LogPayload|string} payload Base64 인코딩된 페이로드 문자열 또는 페이로드 객체
191     */
192    PriceCompareUxlog.prototype.log = function (payload) {
193        if (!payload) {
194            if (this.isDebugMode) console.warn('PriceCompareUxlog: 로깅 페이로드가 필요합니다.');
195            return false;
196        }
197
198        const self = this;
199
200        // DSAC_V2가 없으면 자동으로 로드 후 재시도
201        if (typeof DSAC_V2 !== 'object' || DSAC_V2 === null) {
202            if (this.isDebugMode) console.log('PriceCompareUxlog: DSAC_V2 자동 로드 시작');
203
204            this.loadDSAC()
205                .then((loaded) => {
206                    if (loaded) {
207                        // 로드 완료 후 다시 로깅 시도
208                        self._executeLog(payload);
209                    } else {
210                        if (self.isDebugMode) console.warn('PriceCompareUxlog: DSAC_V2 로드 실패 - 로깅 건너뜀');
211                    }
212                })
213                .catch((error) => {
214                    if (self.isDebugMode) console.error('PriceCompareUxlog: DSAC_V2 로드 오류:', error);
215                });
216
217            return true; // 비동기 처리 중이므로 true 반환
218        }
219
220        // DSAC_V2가 이미 로드된 경우 바로 실행
221        return this._executeLog(payload);
222    };
223
224    /**
225     * 실제 로깅 실행 함수 (내부용)
226     * @param {LogPayload|string} payload 페이로드
227     */
228    PriceCompareUxlog.prototype._executeLog = function (payload) {
229        if (typeof DSAC_V2 !== 'object' || DSAC_V2 === null) {
230            if (this.isDebugMode) console.warn('PriceCompareUxlog: DSAC_V2 객체가 없습니다.');
231            return false;
232        }
233
234        let finalPayload;
235
236        try {
237            // payload가 문자열이면 Base64 디코딩, 객체면 그대로 사용
238            if (typeof payload === 'string') {
239                finalPayload = this.decodePayload(payload);
240
241                if (!finalPayload) {
242                    if (this.isDebugMode) console.warn('PriceCompareUxlog: 페이로드 디코딩 실패');
243                    return false;
244                }
245
246                if (this.isDebugMode) {
247                    console.log('PriceCompareUxlog: Base64 페이로드 디코딩:', {
248                        base64: payload,
249                        decoded: finalPayload
250                    });
251                }
252            } else if (typeof payload === 'object') {
253                finalPayload = { ...payload };
254            } else {
255                if (this.isDebugMode) console.warn('PriceCompareUxlog: 잘못된 페이로드 형식입니다.');
256                return false;
257            }
258            // 기본 페이지 코드가 설정되어 있다면 payload 값 무시하고 기본 페이지 코드 사용
259            if (this.defaultPageCode) {
260                finalPayload.page = this.defaultPageCode;
261            } else if (!finalPayload.page) {
262                if (this.isDebugMode) console.warn('PriceCompareUxlog: page 값이 없습니다.');
263                return false;
264            }
265
266            // 기본 트래킹 ID가 설정되어 있고 mid가 없으면 추가
267            if (typeof this.defaultTrackingId === 'string' && !finalPayload.mid) {
268                finalPayload.mid = this.defaultTrackingId;
269            }
270
271            // 기존 방식으로 DSAC_V2.execute 호출
272            DSAC_V2.execute(finalPayload);
273
274            if (this.isDebugMode) {
275                console.log('PriceCompareUxlog V2 sent:', finalPayload);
276            }
277            return true;
278        } catch (error) {
279            if (this.isDebugMode) {
280                console.error('PriceCompareUxlog V2 error:', error);
281            }
282            return false;
283        }
284    };
285    /**
286     * 페이지뷰 로깅 (간편 메서드)
287     * @param {object} payload 페이지뷰 로깅 페이로드
288     */
289    PriceCompareUxlog.prototype.logPageView = function (payload = {}) {
290        if (this.isDebugMode) {
291            console.log('PriceCompareUxlog.logPageView 호출:', {
292                payload: payload,
293                defaultPageCode: this.defaultPageCode
294            });
295        }
296
297        const finalPayload = {
298            action: 'pv',
299            ...payload
300        };
301
302        return this.log(finalPayload);
303    };
304    /**
305     * 클릭 로깅 (간편 메서드)
306     * @param {object} payload 클릭 로깅 페이로드 (area는 필수)
307     */
308    PriceCompareUxlog.prototype.logClick = function (payload = {}) {
309        if (!payload.area) {
310            if (this.isDebugMode) {
311                console.warn('PriceCompareUxlog: 클릭 로깅에는 area가 필요합니다.');
312            }
313            return false;
314        }
315
316        const finalPayload = {
317            action: 'c',
318            ...payload
319        };
320
321        return this.log(finalPayload);
322    };
323    /**
324     * 조건부 로깅 (조건을 만족할 때만 로깅)
325     * @param {function|boolean} condition 조건 (함수 또는 boolean)
326     * @param {object} payload 로깅 페이로드
327     */
328    PriceCompareUxlog.prototype.logIf = function (condition, payload) {
329        let shouldLog = false;
330
331        if (typeof condition === 'function') {
332            try {
333                shouldLog = condition();
334            } catch (error) {
335                if (this.isDebugMode) {
336                    console.error('PriceCompareUxlog condition error:', error);
337                }
338                return false;
339            }
340        } else {
341            shouldLog = !!condition;
342        }
343
344        if (shouldLog) {
345            return this.log(payload);
346        }
347
348        return false;
349    };
350
351    /**
352     * 지연 로깅 (일정 시간 후 로깅)
353     * @param {number} delay 지연 시간 (밀리초)
354     * @param {object} payload 로깅 페이로드
355     */
356    PriceCompareUxlog.prototype.logDelay = function (delay, payload) {
357        const self = this;
358        const timeoutId = setTimeout(() => {
359            self.log(payload);
360        }, delay || 0);
361
362        // 취소할 수 있도록 timeout ID 반환
363        return timeoutId;
364    };
365    /**
366     * 배치 로깅 (여러 로그를 한번에)
367     * @param {Array} payloads 페이로드 배열
368     */
369    PriceCompareUxlog.prototype.logBatch = function (payloads) {
370        if (!Array.isArray(payloads)) {
371            if (this.isDebugMode) {
372                console.warn('PriceCompareUxlog: payloads는 배열이ì
372–´ì•¼ 합니다.');
373            }
374            return false;
375        }
376
377        let successCount = 0;
378
379        payloads.forEach((payload) => {
380            if (payload && typeof payload === 'object') {
381                if (this.log(payload)) {
382                    successCount++;
383                }
384            }
385        });
386
387        return successCount;
388    };
389
390    /**
391     * 멤버 로그인 상태에 따른 로깅
392     * @param {object} loggedInPayload 로그인 시 로깅 페이로드
393     * @param {object} loggedOutPayload 비로그인 시 로깅 페이로드 (선택사항)
394     */
395    PriceCompareUxlog.prototype.logByLoginStatus = function (loggedInPayload, loggedOutPayload = null) {
396        if (this.defaultMemberCode) {
397            return this.log(loggedInPayload);
398        } else if (loggedOutPayload) {
399            return this.log(loggedOutPayload);
400        }
401        return false;
402    };
403    /**
404     * 간편 페이지뷰 로깅 (기본 페이지 코드 사용)
405     * @param {object} payload 추가 페이로드 (기본값: {})
406     */
407    PriceCompareUxlog.prototype.logPV = function (payload = {}) {
408        if (this.isDebugMode) {
409            console.log('PriceCompareUxlog.logPV 호출:', {
410                payload: payload,
411                defaultPageCode: this.defaultPageCode
412            });
413        }
414        return this.logPageView(payload);
415    };
416
417    /**
418     * 간편 클릭 로깅 (기본 페이지 코드 사용)
419     * @param {string} area 영역 코드
420     * @param {object} additionalPayload 추가 페이로드
421     */
422    PriceCompareUxlog.prototype.logC = function (area, additionalPayload = {}) {
423        if (!area) {
424            if (this.isDebugMode) {
425                console.warn('PriceCompareUxlog: area가 필요합니다.');
426            }
427            return false;
428        }
429
430        return this.logClick({
431            area: area,
432            ...additionalPayload
433        });
434    };
435
436    /**
437     * 간편 로깅 (기본 페이지 코드 사용)
438     * @param {string} action 액션
439     * @param {object} additionalPayload 추가 페이로드
440     */
441    PriceCompareUxlog.prototype.quickLog = function (action, additionalPayload = {}) {
442        return this.log({
443            action: action,
444            ...additionalPayload
445        });
446    };
447    /**
448     * 중앙집중식 이벤트 위임 활성화
449     * HTML 요소에 data-uxlog-payload 속성으로 Base64 인코딩된 로깅 정보를 설정하면 자동으로 로깅됩니다
450     * @param {string|Element} container 이벤트를 위임할 컨테이너 (기본: document)
451     */
452    PriceCompareUxlog.prototype.enableEventDelegation = function (container = document) {
453        // 이미 활성화되어 있으면 중복 방지
454        if (this.eventDelegationEnabled && this.eventListeners.length > 0) {
455            if (this.isDebugMode) {
456                console.log('PriceCompareUxlog: 이벤트 위임이 이미 활성화되어 있습니다.');
457            }
458            return this;
459        }
460
461        // 기존 이벤트 리스너 정리
462        this.disableEventDelegation();
463
464        this.eventDelegationEnabled = true;
465        const targetContainer = typeof container === 'string' ? document.querySelector(container) : container;
466
467        if (!targetContainer) {
468            if (this.isDebugMode) {
469                console.warn('PriceCompareUxlog: 컨테이너를 찾을 수 없습니다.');
470            }
471            return this;
472        }
473
474        this.delegationContainer = targetContainer;
475
476        // 클릭 이벤트만 등록 (mousedown은 제거 - 중복 방지)
477        const clickHandler = (event) => {
478            if (!this.eventDelegationEnabled) return; // 비활성화 상태 체크
479            this._handleDelegatedEvent(event, 'click');
480        };
481
482        targetContainer.addEventListener('click', clickHandler);
483
484        // 이벤트 리스너 저장 (나중에 제거하기 위해)
485        this.eventListeners.push({
486            element: targetContainer,
487            event: 'click',
488            handler: clickHandler
489        });
490
491        if (this.isDebugMode) {
492            console.log('PriceCompareUxlog: 이벤트 위임이 활ì„
492±í™”되었습니다.', {
493                container: targetContainer,
494                eventListeners: this.eventListeners.length
495            });
496        }
497
498        return this;
499    };
500    /**
501     * 위임된 이벤트 처리
502     * @param {Event} event 이벤트 객체
503     * @param {string} eventType 이벤트 타입
504     */
505    PriceCompareUxlog.prototype._handleDelegatedEvent = function (event, eventType) {
506        // 이벤트 위임이 비활성화되어 있으면 무시
507        if (!this.eventDelegationEnabled) return;
508
509        const target = event.target.closest('[data-uxlog-payload]');
510        if (!target) return;
511
512        // 중복 호출 방지를 위한 플래그 체크
513        if (target._uxlogProcessing) return;
514        target._uxlogProcessing = true;
515
516        // 짧은 시간 후 플래그 해제 (동일한 이벤트 중복 방지)
517        setTimeout(() => {
518            delete target._uxlogProcessing;
519        }, 50);
520        try {
521            // data-uxlog-payload에서 Base64 디코딩 후 JSON 파싱
522            const base64Payload = target.dataset.uxlogPayload;
523            if (!base64Payload) return;
524
525            const payload = this.decodePayload(base64Payload);
526            if (!payload) {
527                if (this.isDebugMode) {
528                    console.error('PriceCompareUxlog: 이벤트 위임 페이로드 디코딩 실패:', target);
529                }
530                return;
531            }
532
533            if (this.isDebugMode) {
534                console.log(`PriceCompareUxlog: ${eventType} 이벤트 위임 로깅:`, {
535                    base64: base64Payload,
536                    payload: payload,
537                    target: target
538                });
539            }
540
541            this.logClick(payload);
542        } catch (error) {
543            if (this.isDebugMode) {
544                console.error('PriceCompareUxlog: 이벤트 위임 처리 오류:', error, target);
545            }
546        }
547    };
548    /**
549     * 이벤트 위임 비활성화
550     */
551    PriceCompareUxlog.prototype.disableEventDelegation = function () {
552        this.eventDelegationEnabled = false;
553
554        // 저장된 이벤트 리스너들을 모두 제거
555        this.eventListeners.forEach((listener) => {
556            if (listener.element && typeof listener.element.removeEventListener === 'function') {
557                listener.element.removeEventListener(listener.event, listener.handler);
558                if (this.isDebugMode) {
559                    console.log(`PriceCompareUxlog: ${listener.event} 이벤트 리스너 제거됨`);
560                }
561            }
562        });
563
564        // 이벤트 리스너 배열 초기화
565        this.eventListeners = [];
566        this.delegationContainer = null;
567        if (this.isDebugMode) {
568            console.log('PriceCompareUxlog: 이벤트 위임이 비활성화되었습니다.');
569        }
570
571        return this;
572    };
573    /**
574     * 페이로드를 Base64로 인코딩하는 유틸리티 함수 (유니코드 안전)
575     * @param {LogPayload} payload 페이로드 객체
576     * @returns {string} Base64 인코딩된 문자열
577     */
578    PriceCompareUxlog.prototype.encodePayload = function (payload) {
579        try {
580            const jsonPayload = JSON.stringify(payload);
581            // 유니코드 안전 인코딩: UTF-8 바이트 시퀀스로 변환 후 Base64 인코딩
582            const utf8Bytes = new TextEncoder().encode(jsonPayload);
583            const binaryString = Array.from(utf8Bytes, (byte) => String.fromCharCode(byte)).join('');
584            return btoa(binaryString);
585        } catch (error) {
586            if (this.isDebugMode) {
587                console.error('PriceCompareUxlog: 페이로드 인코딩 오류:', error);
588            }
589            return null;
590        }
591    };
592
593    /**
594     * Base64 인코딩된 문자열을 페이로드로 디코딩하는 유틸리티 함수 (유니코드 안전)
595     * @param {string} base64String Base64 인코딩된 문자열
596     * @returns {object|null} 디코딩된 페이로드 객체
597     */
598    PriceCompareUxlog.prototype.decodePayload = function (base64String) {
599        try {
600            // 유니코드 안전 디코딩: Base64 디코딩 후 UTF-8 바이트 시퀀스로 복
600원
601            const binaryString = atob(base64String);
602            const utf8Bytes = new Uint8Array(binaryString.length);
603            for (let i = 0; i < binaryString.length; i++) {
604                utf8Bytes[i] = binaryString.charCodeAt(i);
605            }
606            const jsonPayload = new TextDecoder().decode(utf8Bytes);
607            return JSON.parse(jsonPayload);
608        } catch (error) {
609            if (this.isDebugMode) {
610                console.error('PriceCompareUxlog: 페이로드 디코딩 오류:', error);
611            }
612            return null;
613        }
614    };
615    /**
616     * 초기화 및 자동 설정
617     */
618    PriceCompareUxlog.prototype.init = function () {
619        // 전역 설정에서 멤버 코드 자동 설정
620        if (typeof window.oGlobalSetting === 'object' && window.oGlobalSetting && window.oGlobalSetting.memberSeq) {
621            this.setDefaultMemberCode(window.oGlobalSetting.memberSeq);
622        }
623
624        // 전역 설정에서 트래킹 ID 자동 설정
625        if (typeof window.oGlobalSetting === 'object' && window.oGlobalSetting && window.oGlobalSetting.trackingId) {
626            this.setDefaultMemberCode(window.oGlobalSetting.trackingId);
627        }
628
629        // 전역 설정에서 페이지 코드 자동 설정
630        if (typeof window.oGlobalSetting === 'object' && window.oGlobalSetting && window.oGlobalSetting.pageCode) {
631            this.setDefaultPageCode(window.oGlobalSetting.pageCode);
632        }
633
634        // 개발 환경에서는 디버그 모드 활성화
635        if (
636            typeof window.location === 'object' &&
637            window.location.hostname &&
638            (window.location.hostname.includes('localhost') ||
639                window.location.hostname.includes('-t') ||
640                window.location.hostname.includes('-local'))
641        ) {
642            this.setDebugMode(true);
643        }
644
645        // DSAC_V2 로드 대기
646        this.waitForDSAC().then((loaded) => {
647            if (loaded && this.isDebugMode) {
648                console.log('PriceCompareUxlog: DSAC_V2 초기화 완료');
649                console.log('PriceCompareUxlog 상태:', {
650                    defaultPageCode: this.defaultPageCode,
651                    defaultMemberCode: this.defaultMemberCode,
652                    defaultTrackingId: this.defaultTrackingId,
653                    eventDelegationEnabled: this.eventDelegationEnabled
654                });
655            }
656        });
657
658        // 자동으로 이벤트 위임 활성화 (data-uxlog-auto="true"인 경우)
659        if (document.documentElement.dataset.uxlogAuto === 'true') {
660            this.enableEventDelegation();
661        }
662    };
663
664    // 전역 인스턴스 생성
665    window.PriceCompareUxlog = PriceCompareUxlog;
666
667    // 기본 인스턴스 생성 (바로 사용 가능)
668    window.pcUxlog = new PriceCompareUxlog();
669
670    // 자동 초기화
671    if (document.readyState === 'loading') {
672        document.addEventListener('DOMContentLoaded', () => {
673            window.pcUxlog.init();
674        });
675    } else {
676        window.pcUxlog.init();
677    }
678})(window);

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.