PageSourceSearch

https://www.campingtalk.me/1790749436508/static/app/module/cua/cua-widget.js

js campingtalk.me collected 2026-09-30 11:36:17 UTC 19,154 bytes, 463 lines download raw bytes

1let pageViewRecorded = false;
2
3Vue.component('cua-widget', {
4    template: `
5        <div ref="widget">
6            <div v-show="!allImagesLoaded" v-html="createCuaSkeletonTemplate(currentCuaSpace)"></div>
7            <div v-show="allImagesLoaded">
8                <div :id="'swiper-container-' + currentCuaSpace" v-if="cuaType === 'swiper'" class="swiper">
9                    <div class="swiper-wrapper">
10                        <a :href="generatedUrl(cua)" v-for="cua in cuaList" :key="cua.cuaId" :data-cua-id="cua.cuaId" :data-space-id="cua.cuaSpaceId" class="swiper-slide">
11                            <div v-html="createCuaTemplate(spaceInfo, cua)"></div>
12                        </a>
13                    </div>
14                    <div v-show="cuaList?.length > 1" class="pagination-wrap">
15                        <div class="swiper-pagination"></div>
16                    </div>
17                </div>
18                <div v-else-if="cuaType === 'static'" :class="adStaticContainerClass">
19                    <a :href="generatedUrl(cua)" v-for="cua in cuaList" :key="cua.cuaId" class="content-wrap" :data-cua-id="cua.cuaId" :data-space-id="cua.cuaSpaceId">
20                        <div v-html="createCuaTemplate(spaceInfo, cua)"></div>
21                    </a>
22                </div>
23                <div v-else-if="cuaType === 'popup'" style="height: 0;">
24                    <button ref="triggerBtn" class="btn" type="button" data-bs-toggle="offcanvas" data-bs-target="#banner-offcanvas" 
25                    aria-controls="offcanvasBottom" style="width: 0; height: 0;"></button>
26                    <div class="offcanvas offcanvas-bottom" data-bs-scroll="false" tabindex="-1" id="banner-offcanvas" aria-labelledby="offcanvasBottomLabel" style="height: auto;">
27                        <div :id="'swiper-container-' + currentCuaSpace" class="swiper h-100 m-0">
28                            <div class="swiper-wrapper">
29                                <a :href="generatedUrl(cua)" v-for="cua in cuaList" :key="cua.cuaId" :data-cua-id="cua.cuaId" :data-space-id="cua.cuaSpaceId" class="offcanvas-body cursor p-0 swiper-slide">
30                                    <div v-html="createCuaTemplate(spaceInfo, cua)"></div>
31                                </a>
32                            </div>
33                            <div v-show="cuaList?.length > 1" class="pagination-wrap">
34                                <div class="swiper-pagination"></div>
35                            </div>
36                        </div>
37                        <div class="offcanvas-footer d-flex justify-content-between">
38                            <button @click.stop="hidePopup($event)" type="button" class="cpt-sub-title p-3" data-bs-dismiss="offcanvas">일주일 동안 보지 않기</button>
39                            <button @click.stop="closePopup($event)" type="button" class="p-3" data-bs-dismiss="offcanvas" aria-label="Close">닫기</button>
40                        </div>
41                    </div>
42                </div>
43            </div>
44        </div>
45    `,
46    data () {
47        return {
48            swiper: {},
49            cuaList: [],
50            spaceInfo: null,
51            useSwiper: null,
52            observer: null,
53            currentCuaSpace: null,
54            swiperLoopCondition: null,
55            allImagesLoaded: false,
56            isInViewport: false,
57            cuaType: null,
58        };
59    },
60    computed: {
61        generatedUrl() {
62            return (cua) => {
63                if (!cua.redirectUrl) return '#';
64
65                const baseUrl = `${_webRootUrl}/inbound/cua`;
66                const additionalParams = this.generateUrlParameter(cua);
67
68                if (!additionalParams) {
69                    return baseUrl;
70                }
71                return `${baseUrl}?${additionalParams}`;
72            }
73        },
74        adStaticContainerClass () {
75            return '';
76        },
77    },
78    mounted () {
79        const adContainer = this.$refs.widget;
80        this.currentCuaSpace = adContainer.getAttribute('data-widget-code');
81        if (this.currentCuaSpace) this.getSpaceInfo(this.currentCuaSpace);
82    },
83    beforeDestroy () {
84        this.observer?.disconnect();
85    },
86    methods: {
87        hidePopup(event) {
88            event.preventDefault();
89            event.stopPropagation();
90            setCookie('hidePopup', 'true', 10080);
91            this.deleteSlide();
92        },
93        closePopup(event) {
94            event.preventDefault();
95            this.deleteSlide();
96        },
97        deleteSlide() {
98            const swiperGroup = `swiper-group-${this.currentCuaSpace}`;
99            if (this.swiper[swiperGroup]) {
100                this.swiper[swiperGroup].autoplay.stop();
101            }
102        },
103        initObserver () {
104            this.observer = new IntersectionObserver(this.handleIntersect, {
105                root: null,
106                threshold: 0.1,
107            });
108            if (this.$refs.widget) this.observer.observe(this.$refs.widget);
109        },
110        getSpaceInfo (cuaSpaceCode) {
111            _get(`${_campinfoServer_}/info/v3/cua/space`, {
112                cuaSpaceCode: cuaSpaceCode
113            }, (res) => {
114                if (res.status === RestResponseStatus.OK) {
115                    this.spaceInfo = res.data.cuaSpace;
116                    this.determineRenderingMethod(cuaSpaceCode);
117                    this.getCuaList(cuaSpaceCode);
118                }
119            });
120        },
121
122        /**
123         * 렌더링 방식 결정
124         *
125         * @param cuaSpaceCode
126         */
127        determineRenderingMethod (cuaSpaceCode) {
128            switch (cuaSpaceCode) {
129                case 'MAIN_PAGE_BOTTOM':
130                    if (getCookie('hidePopup')) return null;
131                    this.useSwiper = true;
132                    this.cuaType = 'popup';
133                    break;
134                default:
135                    this.useSwiper = true;
136                    this.cuaType = 'swiper';
137                    break;
138            }
139        },
140
141        /**
142         * 지면에 게재 중인 CUA 목록
143         *
144         * @param cuaSpaceCode
145         */
146        getCuaList (cuaSpaceCode) {
147            _get(`${_campinfoServer_}/info/v3/cua/list`, {
148                cuaSpaceCode: cuaSpaceCode,
149                isInProgress: true,
150                isActivePeriod: true,
151                cuaAudience: this.getCuaAudience(),
152                sortIndex: 'sortOrder',
153                sortType: 'ASC'
154            }, (res) => {
155                if (res.status === RestResponseStatus.OK) {
156                    // imageUrl 또는 bannerImageUrl이 있는 광고만 필터링
157                    this.cuaList = res.data.cuaList.filter(cua => cua.imageUrl || cua.bannerImageUrl);
158                    this.swiperLoopCondition = this.cuaList.length > 1;
159
160                    if (this.cuaList.length > 0) {
161                        if (this.useSwiper) this.createSwiper();
162                        else this.initObserver();
163
164                        const cuaIds = this.cuaList.map(item => item.cuaId);
165                        const cuaSpaceIds = this.cuaList.map(item => item.cuaSpaceId);
166
167                        if (!pageViewRecorded) {
168                            this.statCua(cuaIds, cuaSpaceIds, 'pageView');
169                            pageViewRecorded = true;
170                        }
171
172                        if (cuaSpaceCode === 'MAIN_PAGE_BOTTOM' && !getCookie('hidePopup')) {
173                            this.$nextTick(() => {
174                                this.$refs.triggerBtn.click();
175                            });
176                        }
177                    }
178
179                    this.allImagesLoaded = true;
180                }
181            });
182        },
183
184        getCuaAudience () {
185            return memberManager.isMember ? 'MEMBER' : 'GUEST';
186        },
187
188        /**
189         * 스와이퍼 옵션 세팅
190         * @param cuaSpaceCode
191         * @returns
192         */
193        getSwiperOptions (cuaSpaceCode) {
194            switch (cuaSpaceCode) {
195                case 'MAIN_PAGE_TOP':
196                    return {
197                        autoplay: { delay: 4000, disableOnInteraction: false },
198                        pagination: { el: '.swiper-pagination', clickable: true, type: "fraction" },
199                        loop: this.swiperLoopCondition,
200                        spaceBetween: 16,
201                        slidesPerView: 'auto',
202                        centeredSlides: true,
203                    };
204                default:
205                    return {
206                        autoplay: { delay: 6000, disableOnInteraction: false },
207                        pagination: { el: '.swiper-pagination', type: 'fraction', clickable: true },
208                        loop: this.swiperLoopCondition,
209                    };
210            }
211        },
212
213        /**
214         * 스와이퍼 생성
215         */
216        createSwiper () {
217            const swiperGroup = `swiper-group-${this.currentCuaSpace}`;
218            const swiperId = `#swiper-container-${this.currentCuaSpace}`;
219            const swiperOptions = this.getSwiperOptions(this.currentCuaSpace);
220
221            setTimeout(() => {
222                this.swiper[swiperGroup] = new Swiper(swiperId, {
223                    ...swiperOptions,
224                    threshold: 10,
225                    observer: true,
226                    lazy: false,
227                    autoHeight: true,
228                    on: {
229                        init: () => { this.initObserver(); },
230                        slideNextTransitionEnd: () => {
231                            if (this.isInViewport && this.swiper[swiperGroup]) {
232                                const activeIndex = this.swiper[swiperGroup].realIndex;
233                                this.statCua([this.cuaList[activeIndex].cuaId], [this.cuaList[activeIndex].cuaSpaceId], 'cuaView');
234                            }
235                        }
236                    }
237                });
238            }, 200);
239        },
240
241        /**
242         * 광고 목록이 로드되기 전 표시할 skeleton ui
243         *
244         * @param code
245         * @returns {string}
246         */
247        createCuaSkeletonTemplate (code) {
248            const spaceInfo = this.spaceInfo;
249            let aspectRatioStyle = 'padding-top: 19.14%;';
250
251            if (spaceInfo && spaceInfo.bannerWidth > 0 && spaceInfo.bannerHeight > 0) {
252                const ratio = (spaceInfo.bannerHeight / spaceInfo.bannerWidth) * 100;
253                aspectRatioStyle = `padding-top: ${ratio}%;`;
254            }
255
256            if (code === 'MAIN_PAGE_TOP') {
257                return `
258                    <div class="skeleton-ui">
259                        <div class="skeleton-item"><div class="skeleton skeleton-image"></div></div>
260                        <div class="swiper-skeleton-pagination">
261                            <div class="skeleton skeleton-dot"></div>
262                            <div class="skeleton skeleton-dot"></div>
263                            <div class="skeleton skeleton-dot"></div>
264                        </div>
265                    </div>`;
266            }
267            
268            if (code === 'MAIN_PAGE_BOTTOM') return `<div></div>`;
269
270            return `
271                <div class="skeleton-ui" style="width: 100%; ${aspectRatioStyle} position: relative;">
272                    <div class="skeleton-item" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0;">
273                        <div class="skeleton skeleton-image" style="width: 100%; height: 100%;"></div>
274                    </div>
275                </div>`;
276        },
277
278        /**
279         * 광고 템플릿 생성
280         *
281         * @param spaceInfo
282         * @param templateType
283         * @param cua
284         * @param templateContent
285         * @returns {string}
286         */
287        createCuaTemplate (spaceInfo, cua) {
288            const { bannerWidth, bannerHeight } = spaceInfo;
289            const bannerImageUrl = cua.bannerImageUrl || '';
290
291            // imageUrl 또는 bannerImageUrl이 없으면 표시하지 않음
292            if (!cua.imageUrl && !bannerImageUrl) {
293                return '';
294            }
295
296            const imageUrl = cua.imageUrl
297                    ? `${_imageServerUrl_}${cua.imageUrl}?f=webp&q=100`
298                    : (bannerImageUrl.startsWith('http') ? bannerImageUrl : `${_staticResourceServer_}${bannerImageUrl}`);
299
300            let style = 'width: 100%; height: auto; object-fit: cover;';
301            if (bannerWidth && bannerHeight && bannerWidth > 0 && bannerHeight > 0) {
302                style += ` aspect-ratio: ${bannerWidth} / ${bannerHeight};`;
303            }
304
305            return `<img src="${imageUrl}" alt="${cua.cuaName}" style="${style}" />`;
306        },
307        /**
308         * URL 생성
309         *
310         * @param cua
311         * @returns {string}
312         */
313        generateUrlParameter (cua) {
314            const parsedUrlParameters = cua.redirectUrl === 'LOGIN' ? [] : JSON.parse(cua.urlParameter || '[]');
315            const cuaData = window.cuaData;
316            const clientParams = {};
317            const systemParams = {};
318
319            parsedUrlParameters.filter(param => param.type === 'client').forEach(item => {
320                const { key, value } = item;
321                if (value.includes('${')) {
322                    const placeholderKey = value.match(/\${(.*?)}/)[1];
323                    if (cuaData.hasOwnProperty(placeholderKey)) { clientParams[key] = cuaData[placeholderKey]; }
324                } else { clientParams[key] = value; }
325            });
326            parsedUrlParameters.filter(param => param.type === 'system').forEach(item => {
327                const key = item.value.match(/\${(.*?)}/)[1];
328                systemParams[key] = item.key;
329            });
330
331            const clientParamsString = Object.entries(clientParams)
332                .filter(([key, value]) => key && value)
333                .map(([key, value]) => `${key}=${value}`)
334                .join('&');
335
336            const systemParameter = {
337                cuaId: cua.cuaId,
338                referer: document.referrer,
339                destination: this.resolveDestination(cua),
340                cuaSpaceId: cua.cuaSpaceId,
341                friendKey: memberManager.member.memberInfo ? memberManager.member.memberInfo.friendCode : null,
342                actionType: 'click',
343                cuid: CUID,
344                isApp: clientEnv.isApp,
345                isMobile: clientEnv.isMobile,
346                ip: clientEnv.additional.network.ip,
347                requireInfos: systemParams,
348                isExternalLink: !!cua.isExternalLink,
349            };
350            const encodedSystemParams = encodeURIComponent(this.aes128Encode(CUA_KEY, CUA_IV, JSON.stringify(systemParameter)));
351            const systemParamsString = `x=${encodedSystemParams}`;
352
353            const allParams = [];
354            if (clientParamsString) {
355                allParams.push(clientParamsString);
356            }
357            allParams.push(systemParamsString); // Always add system params
358
359            return allParams.join('&');
360        },
361
362        resolveDestination (cua) {
363            if (cua.redirectUrl === 'LOGIN') {
364                // CUA tracking endpoint(/inbound/cua)는 로그인 후 다시 진입하면
365                // 동일한 LOGIN 광고를 재실행해 로그인 페이지로 반복 이동한다.
366                // 광고를 클릭한 원래 페이지를 destination으로 넘겨 tracking endpoint
367                // 대신 원래 페이지로 복귀시킨다.
368                const currentUrl = window.location.pathname + window.location.search;
369                return authManager.getSnsLoginUrl({ destinationUri: currentUrl });
370            }
371            return !cua.isExternalLink ? `${_webRootUrl}${cua.redirectUrl}` : cua.redirectUrl;
372        },
373
374        /**
375         * 뷰포트와 CUA 영역이 교차되었을 때 호출
376         * @param entries
377         */
378        handleIntersect (entries) {
379            const shownCuaIds = new Set();
380            const shownCuaSpaceIds = [];
381
382            entries.forEach(entry => {
383                if (entry.isIntersecting && this.cuaList.length > 0) {
384                    this.isInViewport = true;
385                    if (this.useSwiper) {
386                        let targetElement = entry.target.querySelectorAll('[data-cua-id]');
387                        if (targetElement) {
388                            const swiperGroup = `swiper-group-${this.currentCuaSpace}`;
389                            if (this.swiper[swiperGroup]) {
390                                const activeIndex = this.swiper[swiperGroup].realIndex;
391                                shownCuaIds.add([this.cuaList[activeIndex].cuaId]);
392                                shownCuaSpaceIds.push(this.cuaList[activeIndex].cuaSpaceId);
393                            }
394                        }
395                    } else {
396                        const cuaElements = entry.target.querySelectorAll('[data-cua-id]');
397                        cuaElements.forEach(cuaElement => {
398                            const cuaId = cuaElement.getAttribute('data-cua-id');
399                            const cuaSpaceId = cuaElement.getAttribute('data-space-id');
400                            shownCuaIds.add(cuaId);
401                            shownCuaSpaceIds.push(cuaSpaceId);
402                        });
403                    }
404
405                    if (shownCuaIds.size > 0) {
406                        this.statCua(Array.from(shownCuaIds).map(Number), shownCuaSpaceIds, 'cuaView');
407                    }
408                } else {
409                    this.isInViewport = false;
410                }
411            });
412        },
413
414        /**
415         * 뷰 파라미터 생성
416         *
417         * @param actionType
418         * @returns {{actionType, cuid: *, cuaIds: (string|*)[], ip: *}}
419         */
420        generateViewParam (actionType) {
421            return {
422                cuaIds: this.cuaList.map(item => item.cuaId),
423                actionType: actionType,
424                cuid: CUID,
425                ip: clientEnv.additional.debugInfo.cloudFlare.ip,
426            };
427        },
428
429        /**
430         * 암호화
431         *
432         * @param secretKey
433         * @param iv
434         * @param data
435         * @returns {string}
436         */
437        aes128Encode (secretKey, iv, data) {
438            const cipher = CryptoJS.AES.encrypt(data, CryptoJS.enc.Utf8.parse(secretKey), {
439                iv: CryptoJS.enc.Utf8.parse(iv),
440                padding: CryptoJS.pad.Pkcs7,
441                mode: CryptoJS.mode.CBC,
442            });
443            return cipher.toString();
444        },
445
446        /**
447         * CUA 집계
448         */
449        statCua (cuaIds, cuaPlaceIds, actionType) {
450            _post(`/rest/domain/cua/event/view`, {
451                cuaIds: cuaIds,
452                cuaSpaceIds: cuaPlaceIds,
453                actionType: actionType,
454                cuid: CUID,
455                ip: clientEnv.additional.network.ip,
456            }, (res) => {
457                if (res.status === RestResponseStatus.OK) {
458                    // Success
459                }
460            });
461        },
462    }
463});

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.