PageSourceSearch

https://www.aladin.co.kr/js/aladin.search.autocomplete.js?ver=202609241726

js aladin.co.kr collected 2026-09-24 08:27:03 UTC 40,389 bytes, 1,041 lines download raw bytes

1var aladin = aladin || {};
2
3(function ($) {
4    aladin.search = {
5        _initialized: false,
6
7        init: function () {
8            if (this._initialized) return;
9
10            this.field = $(".search_field_area");
11            this.form = this.field.find("#QuickSearch");
12            this.baselayer = this.field.find("#Search_Layer");
13            this.defaultlayer = this.field.find("#SearchDefaultLayer");
14            this.completelayer = this.field.find("#SearchCompleteLayer");
15            this.input = this.field.find("#SearchWord");
16            this.submitBtn = this.field.find("#SearchSubmit");
17            this.clearBtn = this.field.find("#search-clear-btn");
18            this.isDirectDelivery = this.form.find("#IsDirectDelivery").val();
19
20            //검색어, 진행중인 요청 캐시
21            this.cache = new Map();
22            this.itemcache = new Map();
23            this.requestCache = new Map();
24            this.timer = null;
25            this.lastKeyword = '';
26
27            // 자동완성 네비게이션 변수
28            this.item_count = 0;
29            this.item_cursor_idx = -1;
30
31            this.searchTargetInit();
32            this.searchBannerInit();
33            this.searchLayerInit();
34            this.searchInputInit();
35            this.jpKeyboardInit();
36
37            this._setupEventDelegation();
38            this._initialized = true;
39        },
40
41        // 상수 및 설정
42        maxCacheSize: 100,
43        debounceDelay: 180,
44        timeouts: {
45            search: 1000, //자동완성검색API
46            keyword: 2000, //최근검색어, 추천키워드, 지금베스트API
47            viewitem: 2000, //미리보기API
48            banner: 2000 //배너API
49        },
50
51        /*######################
52        Private 메서드들
53        #######################*/
54
55        _setupEventDelegation: function () {
56            $(document).on('click', '.open-popup-full', (e) => {
57                if (!$(e.target).parents(".search_field_area fieldset").length) {
58                    $.publish("search-target.hide");
59                    $.publish("mode-search.hide");
60                    $.publish("jpKeyboard.hide");
61                }
62            });
63        },
64
65        _manageCacheSize: function (cacheMap) {
66            while (cacheMap.size > this.maxCacheSize) {
67                const firstKey = cacheMap.keys().next().value;
68                cacheMap.delete(firstKey);
69            }
70        },
71
72        _createElement: function (tag, attributes, content, isHTML = false) {
73            const element = document.createElement(tag);
74
75            if (attributes) {
76                Object.entries(attributes).forEach(([attr, value]) => {
77                    element.setAttribute(attr, value);
78                });
79            }
80
81            if (content) {
82                if (isHTML) {
83                    element.innerHTML = this._sanitizeSearchTitle(content);
84                } else {
85                    element.textContent = content;
86                }
87            }
88
89            return element;
90        },
91
92        _sanitizeSearchTitle: function (title) {
93            return title
94                .replace(/<(?!\/?(em)\b)[^<>]*>/gi, '')
95                .replace(/&lt;/g, '<')
96                .replace(/&gt;/g, '>')
97                .replace(/&amp;/g, '&');
98        },
99
100        _deduplicateRequest: function (key, requestFn) {
101            if (this.requestCache.has(key)) {
102                return this.requestCache.get(key);
103            }
104
105            const promise = requestFn().finally(() => {
106                this.requestCache.delete(key);
107            });
108
109            this.requestCache.set(key, promise);
110            return promise;
111        },
112
113        _handleAjaxError: function (xhr, textStatus, error, context) {
114            const errorInfo = {
115                context,
116                status: xhr.status,
117                statusText: xhr.statusText,
118                error: error,
119                timestamp: Date.now(),
120                responseText: xhr.responseText || null
121            };
122
123            const userMessages = {
124                0: '네트워크 연결을 확인해주세요.',
125                404: '요청한 페이지를 찾을 수 없습니다.',
126                timeout: '요청 시간이 초과되었습니다.',
127                default: '일시적인 오류가 발생했습니다.'
128            };
129
130            const message = xhr.status >= 500 ? '서버 점검 중입니다.' :
131                userMessages[xhr.status] ||
132                userMessages[textStatus] ||
133                userMessages.default;
134
135            if (window.console && console.warn) {
136                console.warn(
137                    `Ajax Error [${context}]`,
138                    '\nUser Message:', message,
139                    '\nError Details:', errorInfo,
140                    '\nXHR Object:', xhr
141                );
142            }
143        },
144
145        _makeRequest: function (options) {
146            const defaults = {
147                type: "POST",
148                async: true,
149                cache: false,
150                dataType: "json",
151                timeout: this.timeouts[options.context] || 3000,
152                beforeSend: () => options.onStart?.(),
153                complete: () => options.onComplete?.(),
154                error: (xhr, textStatus, error) => {
155                    this._handleAjaxError(xhr, textStatus, error, options.context);
156                    options.onError?.(xhr, textStatus, error);
157                }
158            };
159
160            // jQuery Ajax를 네이티브 Promise로 래핑
161            return new Promise((resolve, reject) => {
162                const jqXHR = $.ajax({ ...defaults, ...options });
163                jqXHR.done(resolve).fail((xhr, textStatus, error) => {
164                    // 에러 핸들러가 이미 호출되었으므로 reject만 수행
165                    reject({ xhr, textStatus, error });
166                });
167            });
168        },
169
170        _saveRecentData: function (type, value) {
171            const params = new URLSearchParams({ method: 'add' });
172
173            if (type === 'keyword') {
174                params.set('keyword', encodeURIComponent(value));
175            } else if (type === 'item') {
176                params.set('itemid', value);
177            } else {
178                console.warn('Invalid save type:', type);
179                return;
180            }
181
182            if (navigator.sendBeacon) {
183                navigator.sendBeacon("/search/v2/recent-keyword.ashx", params);
184            } else {
185                this._makeRequest({
186                    url: "/search/v2/recent-keyword.ashx",
187                    data: Object.fromEntries(params.entries()),
188                    timeout: 1500,
189                    context: `saveRecent${type.charAt(0).toUpperCase()}${type.slice(1)}`
190                }).catch(() => { });
191            }
192        },
193
194        /*######################
195        검색 배너 초기화
196        #######################*/
197        searchBannerInit: function () {
198            const $input = this.input;
199            const $submit = this.submitBtn;
200            const branchtype = this.form.data("branchtype");
201
202            if ($input.val().length > 0 || this.form.data('banner-active') == 0) {
203                return;
204            }
205
206            this._makeRequest({
207                url: "/banner/search-banner.ashx",
208                data: { branchtype: branchtype },
209                timeout: this.timeouts.banner,
210                context: 'searchBanner'
211            }).then(data => {
212                if (data?.ResultCode == 1 && data.banner) {
213                    $input.attr('placeholder', data.banner.AdText).addClass("ad");
214
215                    const redirect = '/banner/wadbanner_redirect.aspx?PubBannerId=' + data.banner.PubBannerId;
216                    $submit.off("click.banner").on("click.banner", (e) => {
217                        e.preventDefault();
218                        location.href = redirect;
219                    });
220                }
221            }).catch(error => {
222                console.warn('Banner loading failed:', error);
223            });
224        },
225
226        /*######################
227        일본어 키보드 초기화
228        #######################*/
229        jpKeyboardInit: function () {
230            const $jpBtn = this.form.find(".jp_word_btn");
231            const $jpLayer = this.form.find(".jp_word_layer_positon");
232            const $jpSearchWord = $jpLayer.find("#JP-SearchWord");
233            const $jpSubmit = $jpLayer.find(".jp_word_submit");
234            const $jpClose = $jpLayer.find(".layer_close");
235
236            $.subscribe("jpKeyboard.hide", () => {
237                $jpLayer.fullpop("hide");
238            });
239
240            $jpBtn.on("click", (e) => {
241                e.preventDefault();
242                if ($jpLayer.is(":visible")) {
243                    $jpLayer.fullpop("hide");
244                } else {
245                    $.publish("mode-search.hide");
246                    $jpLayer.fullpop("show");
247                    this.defaultlayer.hide();
248                }
249            });
250
251            $jpLayer.find(".jp_word_table td a").on("click", (e) => {
252                e.preventDefault();
253                const val = $(e.currentTarget).text();
254                if (val.length > 0) {
255                    $jpSearchWord.val($jpSearchWord.val() + val);
256                }
257            });
258
259            $jpSubmit.on("click", (e) => {
260                e.preventDefault();
261                const jsword = $jpSearchWord.val();
262                const length = jsword.length;
263
264                $jpSearchWord.val("");
265                this.input.val(jsword);
266                $jpLayer.fullpop("hide");
267
268                setTimeout(() => {
269                    this.input[0].focus();
270                    this.input[0].setSelectionRange(length, length);
271                }, 100);
272            });
273
274            $jpClose.on("click", (e) => {
275                e.preventDefault();
276                $jpLayer.fullpop("hide");
277            });
278        },
279
280        /*######################
281        검색 타겟 초기화
282        #######################*/
283        searchTargetInit: function () {
284            const $target = this.form.find("#Search-Target-Desc");
285            const $targetList = this.form.find("#Search-Target-List");
286            const isUsedStore = this.form.find("input[name='KeyTag']").length > 0;
287            const $jpBtn = this.form.find(".jp_word_btn");
288            const $searchTarget = this.form.find("input[name='SearchTarget']");
289
290            $.subscribe("search-target.show", () => {
291                if ($('body').hasClass('mode-search')) {
292                    $.publish("mode-search.hide");
293                }
294                this.form.find("#Search-Target-List").fullpop("show");
295                $target.addClass("active");
296            });
297
298            $.subscribe("search-target.hide", () => {
299                this.form.find("#Search-Target-List").fullpop("hide");
300                $target.removeClass("active");
301            });
302
303            $target.off("click.searchtarget keypress.searchtarget")
304                .on("click.searchtarget keypress.searchtarget", (e) => {
305                    if (e.type === "click" || (e.type === "keypress" && e.which === 13)) {
306                        e.preventDefault();
307                        if (e.originalEvent.pointerType === "mouse" || e.which === 13) {
308                            $targetList.is(":visible") ?
309                                $.publish("search-target.hide") :
310                                $.publish("search-target.show");
311                        }
312                    }
313                });
314
315            $targetList.find("button").off("click.searchtarget keypress.searchtarget")
316                .on("click.searchtarget keypress.searchtarget", (e) => {
317                    e.preventDefault();
318                    $targetList.fullpop("hide");
319
320                    const element = $(e.currentTarget);
321                    $target.text(element.text());
322
323                    if (isUsedStore) {
324                        this.form.find("input[name='KeyTag']").val(element.data('target'));
325                    } else {
326                        $target.attr('data-target', element.data('target'));
327                        this.searchTargetChangeSet(element.data('target'));
328                        this.form.find("input[name='SearchTarget']").val(element.data('target'));
329
330                        if (element.data('target') === 'Foreign') {
331                            $jpBtn.show();
332                            this.clearBtn.addClass("jp");
333                        } else {
334                            $jpBtn.hide();
335                            this.clearBtn.removeClass("jp");
336                        }
337                    }
338
339                    // 캐시 초기화
340                    this.cache?.clear();
341
342                    setTimeout(() => {
343                        this.input[0].focus();
344                    }, 200);
345                });
346
347            if ($searchTarget.val() === 'Foreign') {
348                $jpBtn.show();
349                this.clearBtn.addClass("jp");
350            }
351
352            if ($target.length > 0) {
353                this.searchTargetChangeSet($target.data('target'));
354            } else {
355                this.searchTargetChangeSet($searchTarget.val());
356            }
357        },
358
359        searchTargetChangeSet: function (target) {
360            const mode = this.form.attr('data-site-mode');
361
362            if (target === "UsedStore" || target === "Talktalk") {
363                if (target === "UsedStore" && this.isDirectDelivery == 2) {
364                    this.form.attr('data-active', 1);
365                } else {
366                    this.form.attr('data-active', 0);
367                }
368                this.input.attr('data-autocomplete-active', 0);
369            }
370            else if (target === "Coffee") {
371                this.form.attr('data-active', 1);
372                this.input.attr('data-autocomplete-active', 0);
373            }
374            else if (target === "Used" && this.form.find("input[name='Ke
374yTag']").length > 0) {
375                this.form.attr('data-active', 0);
376                this.input.attr('data-autocomplete-active', 0);
377            }
378            else if (target === undefined && mode == 1) {
379                this.form.attr('data-active', 0);
380                this.input.attr('data-autocomplete-active', 0);
381            }
382            else {
383                this.form.attr('data-active', 1);
384                this.input.attr('data-autocomplete-active', 1);
385            }
386        },
387
388        /*######################
389        검색 입력 초기화
390        #######################*/
391        searchInputInit: function () {
392            let debounceTimer = null;
393
394            const debouncedSearch = (keyword, searchTarget, mode) => {
395                clearTimeout(debounceTimer);
396                debounceTimer = setTimeout(() => {
397                    this._performSearch(keyword, searchTarget, mode);
398                }, this.debounceDelay);
399            };
400
401            $(document).off("keydown.input keyup.input focusin.input mousedown.input propertychange.input input.input")
402                .on("keydown.input keyup.input focusin.input mousedown.input propertychange.input input.input", "#SearchWord", (e) => {
403                    const keyword = this.input.val();
404
405                    if (e.type === 'mousedown') {
406                        if (window.getSelection) {
407                            window.getSelection().removeAllRanges();
408                        }
409                        if (document.selection) {
410                            document.selection.empty();
411                        }
412                    }
413
414                    // keydown에서만 처리할 이벤트들
415                    if (e.type === 'keydown') {
416                        // 자동완성 네비게이션
417                        if (e.which === 40 || e.which === 38) {
418                            e.preventDefault();
419                            if (this.item_count === 0 || !this.completelayer.is(":visible")) {
420                                return;
421                            }
422                            this._handleAutoCompleteNavigation(e);
423                            return;
424                        }
425
426                        // 엔터키 처리
427                        if (e.which === 13) {
428                            e.preventDefault();
429                            if (this.item_cursor_idx > -1) {
430                                const $autoComplete = this.completelayer.find(".auto_complete");
431                                const itemid = $autoComplete.find("li").eq(this.item_cursor_idx).data("itemid");
432                                this.viewItem(itemid, true);
433                            } else {
434                                this.searchResult(keyword, true);
435                            }
436                            return;
437                        }
438                    }
439
440                    // 검색어 입력 처리
441                    if (e.type === 'input' || e.type === 'keyup' || e.type === 'focusin' || e.type === 'propertychange' || e.type === 'mousedown') {
442                        if (e.which === 40 || e.keyCode === 40 || e.which === 38 || e.keyCode === 38 || e.which === 13 || e.keyCode === 13) {
443                            return;
444                        }
445                        this._handleSearchInput(keyword, debouncedSearch);
446                    }
447                });
448        },
449
450        _handleAutoCompleteNavigation: function (e) {
451            const $auto_complete = this.completelayer.find(".auto_complete");
452
453            if (e.which === 40) { // Down key
454                this.item_cursor_idx = this.item_cursor_idx < this.item_count - 1 ?
455                    this.item_cursor_idx + 1 : 0;
456            } else if (e.which === 38) { // Up key
457                this.item_cursor_idx = this.item_cursor_idx > 0 ?
458                    this.item_cursor_idx - 1 : -1;
459            }
460
461            if (this.item_cursor_idx === -1) {
462                $auto_complete.find("li").removeClass("on");
463            } else {
464                const $obj = $auto_complete.find("li").eq(this.item_cursor_idx);
465                if ($obj.length) {
466                    $obj.addClass("on").siblings().removeClass("on");
467                    const itemid = $obj.data("itemid");
468                    if (itemid) {
469                        this.searchItemPreview(itemid, this.item_cursor_idx);
470                    }
471                }
472            }
473        },
474
475        _handleSearchInput: function (keyword, debouncedSearch) {
476            // 이전 키워드와 동일한 경우 스킵
477            if (keyword === this.lastKeyword && keyword.length > 0) {
478                return;
479            }
480            this.lastKeyword = keyword;
481
482            const active = this.input.attr('data-autocomplete-active');
483            const mode = this.form.attr('data-site-mode');
484            const searchTarget = this.form.find("input[name='SearchTarget']").val();
485
486            this.clearBtn.css('visibility', keyword ? 'visible' : 'hidden');
487
488            if (!$('body').hasClass('mode-search')) {
489                this.input.attr('placeholder', '');
490                $.publish("mode-submit-btn.normal");
491                if (this.form.attr('data-active') == 1) {
492                    $.publish("mode-search.show");
493                }
494            }
495
496            this.item_cursor_idx = -1;
497
498            if (active == 0) {
499                keyword ? this.defaultlayer.hide() : this.defaultlayer.show();
500                return;
501            }
502
503            if (!keyword) {
504                const $autoComplete = this.completelayer.find(".auto_complete");
505                const $productInfo = this.completelayer.find(".product_info");
506                $autoComplete.empty();
507                $productInfo.empty();
508                this.completelayer.hide();
509                this.defaultlayer.show();
510            } else {
511                if (this.defaultlayer.is(":visible")) {
512                    setTimeout(() => this.defaultlayer.hide(), 300);
513                }
514                debouncedSearch(keyword, searchTarget, mode);
515            }
516        },
517
518        /*######################
519        검색 수행 로직
520        #######################*/
521        _performSearch: function (keyword, searchTarget, mode) {
522            const $autoComplete = this.completelayer.find(".auto_complete");
523            const $completelayer = this.completelayer;
524
525            // 캐시 확인
526            if (this.cache.has(keyword)) {
527                const cachedHtml = this.cache.get(keyword);
528                $autoComplete.html(cachedHtml);
529                $completelayer.show();
530
531                const $firstItem = $autoComplete.find("li").eq(0);
532                if ($firstItem.length) {
533                    this.searchItemPreview($firstItem.data("itemid"));
534                }
535                return Promise.resolve();
536            }
537
538            const requestKey = `search_${keyword}`;
539            return this._deduplicateRequest(requestKey, () => {
540                return this._makeRequest({
541                    url: "/search/v2/search-items.ashx",
542                    data: {
543                        q: encodeURIComponent(keyword),
544                        t: searchTarget,
545                        mode: mode
546                    },
547                    timeout: this.timeouts.search,
548                    context: 'performSearch'
549                });
550            }).then(data => {
551                if (data?.ResultCode === 1 && Array.isArray(data.List)) {
552                    this.item_count = data.List.length;
553                    if (data.List.length === 0) {
554                        $completelayer.hide();
555                        return;
556                    }
557
558                    const html = this._buildAutoCompleteHTML(data.List);
559                    $autoComplete.html(html);
560                    $completelayer.show();
561
562                    // 캐시 저장
563                    this._manageCacheSize(this.cache);
564                    this.cache.set(keyword, html);
565
566                    if (data.List[0]?.ItemId) {
567                        this.searchItemPreview(data.List[0].ItemId);
568                    }
569                } else if (data?.ResultCode === 0) {
570                    this.input.attr('data-autocomplete-active', 0);
571                    $completelayer.hide();
572                    if (this.form.attr('data-active') == 1) {
573                        this.defaultlayer.show();
574                    }
575                }
576            }).catch(error => {
577                //console.warn('Search request failed:', error);
578                $completelayer.hide();
579            });
580        },
581
582        _buildAutoCompleteHTML: function (items) {
583            const ul = this._createElement('ul');
584            const fragment = document.createDocumentFragment();
585
586            items.forEach(item => {
587                if (item?.ItemId && item?.Title) {
588                    const li = this._createElement('li', { 'data-itemid': item.ItemId });
589                    const a = this._createElement('a', {
590                        'href': `javascript:aladin.search.viewItem(${item.ItemId}, true);`,
591                        'onfocus': `aladin.search.searchItemPreview(${item.ItemId});`,
592                        'onmouseover': `aladin.search.searchItemPreview(${item.ItemId});`
593                    });
594
595                    a.innerHTML = this._sanitizeSearchTitle(item.Title);
596                    li.appendChild(a);
597                    fragment.appendChild(li);
598                }
599            });
600
601            ul.appendChild(fragment);
602            return ul.outerHTML;
603        },
604
605        /*######################
606        아이템 미리보기
607        #######################*/
608        searchItemPreview: function (itemid, cursor_idx) {
609            if (!itemid) return;
610
611            const $productInfo = this.completelayer.find(".product_info");
612            const $autoComplete = this.completelayer.find(".auto_complete");
613
614            if (cursor_idx === undefined) {
615                $autoComplete.find("li").removeClass("on");
616                this.item_cursor_idx = -1;
617            }
618
619            if (this.itemcache.has(itemid)) {
620                $productInfo.html(this.itemcache.get(itemid));
621                return;
622            }
623
624            $productInfo.empty();
625
626            this._makeRequest({
627                url: "/search/v2/search-item-preview.ashx",
628                data: { itemid: itemid },
629                timeout: this.timeouts.viewitem,
630                context: 'searchItemPreview'
631            }).then(data => {
632                if (data?.ResultCode == 1) {
633                    const html = this._buildPreviewHTML(data, itemid);
634                    $productInfo.html(html);
635
636                    // 캐시 저장
637                    this._manageCacheSize(this.itemcache);
638                    this.itemcache.set(itemid, html);
639                }
640            }).catch(error => {
641                console.warn('Preview request failed:', error);
642            });
643        },
644
645        _buildPreviewHTML: function (data, itemid) {
646            const url = `javascript:aladin.search.viewItem(${itemid}, true);`;
647            let html = `<div class="cover"><a href="${url}"><img src="${data.Cover}"></a>`;
648
649            if (data.IsBasketable) {
650                html += `<button type="button" class="add_basket" onclick="aladin.search.addBasket(${data.ItemId},event)">장바구니 담기</button>`;
651            }
652            html += '</div>';
653
654            html += `<div class="txt"><div class="book_tit"><a href="${url}">${data.Title}</a></div>`;
655            html += '<div class="book_info">';
656
657            if (data.Author?.length > 0) html += `<span>${data.Author}</span>`;
658            if (data.Publisher?.length > 0) html += `<span>${data.Publisher}</span>`;
659            if (data.PublishDate?.length > 0) html += `<span>${data.PublishDate}</span>`;
660
661            html += `</div><div class="book_price">${data.Price}</div></div>`;
662
663            return html;
664        },
665
666        /*######################
667        검색 레이어 초기화
668        #######################*/
669        searchLayerInit: function () {
670            this._setupSearchLayerEvents();
671            this._setupSearchSubscriptions();
672            this._setupInputEvents();
673        },
674
675        _setupSearchLayerEvents: function () {
676            $.subscribe("mode-search.show", () => {
677                $("body").addClass("mode-search");
678                this.baselayer.fullpop("show");
679                $.publish("search-target.hide");
680
681                // 비동기 데이터 로딩
682                Promise.allSettled([
683                    this._loadRecentKeywords(),
684                    this._loadRecommendKeywords(),
685                    this._loadNowBest(),
686                    //this._loadRecentViewItems()
687                ]);
688            });
689
690            $.subscribe("mode-search.hide", () => {
691                this.baselayer.fullpop('hide');
692                this.item_cursor_idx = -1;
693                $.publish("mode-autocomplet.clear");
694                setTimeout(() => $("body").removeClass("mode-search"), 100);
695            });
696
697            $.subscribe("mode-autocomplet.clear", () => {
698                const $completelayer = this.completelayer;
699                $completelayer.find(".auto_complete").html("");
700                $completelayer.find(".product_info").html("");
701                $completelayer.hide();
702                this.lastKeyword = "";
703            });
704
705            $.subscribe("mode-submit-btn.normal", () => {
706                this.submitBtn.off("click").on("click", (e) => {
707                    e.preventDefault();
708                    this.searchResult(this.input.val(), true);
709                });
710            });
711        },
712
713        _setupInputEvents: function () {
714            this.input.on("click", (e) => {
715                e.preventDefault();
716                if ($('body').hasClass('mode-search')) return;
717
718                $(e.currentTarget).attr('placeholder', '').removeClass("ad");
719                $.publish("mode-submit-btn.normal");
720
721                if (this.form.attr('data-active') == 1) {
722                    $.publish("mode-search.show");
723                    if (this.form.find(".jp_word_layer_positon").is(":visible")) {
724                        $.publish("jpKeyboard.hide");
725                    }
726                } else {
727                    $.publish("search-target.hide");
728                }
729            });
730
731            this.clearBtn.on("click", (e) => {
732                e.preventDefault();
733                this.input.val('');
734                setTimeout(() => this.input[0].focus(), 100);
735            });
736
737            this.form.find(".layer_close").on("click", (e) => {
738                e.preventDefault();
739                $.publish("mode-search.hide");
740            });
741
742            if (this.input.val().length > 0) {
743                this.clearBtn.css('visibility', 'visible');
744            }
745        },
746
747        /*######################
748        최근 검색어 로딩
749        #######################*/
750        _loadRecentKeywords: function () {
751            const $section = $("#recent-keyword-section");
752            if ($section.hasClass("on")) return Promise.resolve();
753
754            $section.empty();
755
756            return this._makeRequest({
757                url: "/search/v2/recent-keyword.ashx",
758                timeout: this.timeouts.keyword,
759                context: 'recent_keyword_bind'
760            }).then(data => {
761                if (data?.ResultCode == 1 && Array.isArray(data.List)) {
762                    this._renderRecentKeywords($section, data);
763                } else {
764                    this._renderEmptyRecentKeywords($section);
765                }
766            }).catch(error => {
767                console.warn('Failed to load recent keywords:', error);
768            });
769        },
770
771        _renderRecentKeywords: function ($section, data) {
772            const items = data.List.map(item => {
773                const fn = item.ItemId > 0
774                    ? `aladin.search.viewItem(${item.ItemId}, undefined, 'pcsearch_recen');`
775                    : `aladin.search.changeKeyword('${encodeURIComponent(item.Keyword)}')`;
776
777                return `<li><a href="javascript:${fn}">${item.Keyword}</a>
778                        <button type="button" class="del_btn" onclick="aladin.search.deleteRecentKeyword('delete', ${item.Seq}, event)">삭제</button></li>`;
779            }).join('');
780
781            const html = `
782                <div class="tit">
783                    <h3 tabindex="0">최근 검색어</h3>
784                    <button type="button" class="all_del" onclick="aladin.search.deleteRecentKeyword('delete_all',0,event)">전체삭제</button>
785                </div>
786                <ul>${items}</ul>`;
787
788            $section.append(html).show().addClass("on");
789        },
790
791        _renderEmptyRecentKeywords: function ($section) {
792            const html = `
793                <div class="tit"><h3 tabindex="0">최근 검색어</h3></div>
794                <div class="none_box"><p>최근 검색어가 없습니다.</p></div>`;
795            $section.append(html).show().addClass("on");
796        },
797
798        /*######################
799        추천 키워드 로딩
800        #######################*/
801        _loadRecommendKeywords: function () {
802            const $section = $("#recommand-keyword-section");
803            if ($section.hasClass("on")) return Promise.resolve();
804
805            const mode = this.form.attr('data-site-mode');
806
807            return this._makeRequest({
808                url: "/search/v2/recommand-keyword.ashx",
809                data: { mode: mode },
810                timeout: this.timeouts.keyword,
811                context: 'recommand_keyword_bind'
812            }).then(data => {
813                if (data?.ResultCode == 1 && Array.isArray(data.List)) {
814                    const keywords = data.List.map(item => {
815                        const url = item.Link + (item.Link.includes("?") ? "&" : "?") + "start=pcsearch_mdkey";
816                        return `<a href="${url}">${item.Keyword}</a>`;
817                    }).join('');
818
819                    const html = `
820                        <div class="tit"><h3 tabindex="0">추천 키워드</h3></div>
821                        <div class="keyword_box">${keywords}</div>`;
822
823                    $section.append(html).show().addClass("on");
824                }
825            }).catch(error => {
826                console.warn('Failed to load recommend keywords:', error);
827            });
828        },
829
830        /*######################
831        지금 베스트 로딩
832        #######################*/
833        _loadNowBest: function () {
834            const $section = $("#now-best-section");
835            if ($section.hasClass("on")) return Promise.resolve();
836
837            const mode = this.form.attr('data-site-mode');
838
839            return this._makeRequest({
840                url: "/search/v2/now-best.ashx",
841                data: { mode: mode },
842                timeout: this.timeouts.keyword,
843                context: 'now_best_bind'
844            }).then(data => {
845                if (data?.ResultCode == 1 && Array.isArray(data.List)) {
846                    this._renderNowBest($section, data);
847                }
848            }).catch(error => {
849                console.warn('Failed to load now best:', error);
850            });
851        },
852
853        _renderNowBest: function ($section, data) {
854            let items = "";
855
856            for (let i = 0; i < data.List.length; i++) {
857                if (i % 5 === 0) {
858                    if (i > 0) items += '</ol></div>';
859                    items += '<div class="swiper-slide"><ol>';
860                }
861
862                const item = data.List[i];
863                items += `<li><a href="/shop/wproduct.aspx?ItemId=${item.ItemId}&start=pcsearch_beitem">
864                    <div class="cover"><img src="${item.Cover}"/></div>
865                    <div class="txt"><em>${item.Rank}.</em> ${item.Title}</div>
866                </a></li>`;
867
868                if (i === data.List.length - 1) {
869                    items += '</ol></div>';
870                }
871            }
872
873            const header = `
874                <div class="tit">
875                    <h3><a href="/shop/common/wbest.aspx?BestType=NowBest&start=pcsearch_beitem">지금 베스트</a></h3>
876                    <div>
877                        <div class="time" tabindex="0">${data.CalcTime}</div>
878                        <div class="now_best_btn">
879                            <button type="button" class="now_best-prev" tabindex="-1" title="1위~5위 목록 보기">이전</button>
880                            <button type="button" tabindex="-1" class="now_best-next" title="6위~10위 목록 보기">다음</button>
881                        </div>
882                    </div>
883                </div>`;
884
885            const swiperHtml = `<div class="swiper now_best_list"><div class="swiper-wrapper">${items}</div></div>`;
886
887            $section.append(header + swiperHtml).show().addClass("on");
888
889            // Swiper 초기화
890            requestAnimationFrame(() => {
891                this._initNowBestSwiper();
892            });
893        },
894
895        _initNowBestSwiper: function () {
896            try {
897                new Swiper(".now_best_list", {
898                    slidesPerView: 1,
899                    observer: true,
900                    observeParents: true,
901                    navigation: {
902                        nextEl: ".now_best_btn .now_best-next",
903                        prevEl: ".now_best_btn .now_best-prev",
904                    },
905                    touchRatio: 0,
906                    allowTouchMove: false,
907                    on: {
908                        afterInit: function () {
909                            $('.now_best-next, .now_best-prev').attr('tabindex', '-1');
910                        },
911                        slideChange: function () {
912                            $('.now_best-next, .now_best-prev').attr('tabindex', '-1');
913                        },
914                    },
915                });
916            } catch (error) {
917                // Swiper 라이브러리가 없는 경우 동적 로드
918                $.getScript('/ucl_editor/js/swiper.min.js', () => {
919                    this._initNowBestSwiper();
920                });
921            }
922        },
923
924        /*######################
925        최근 본 상품 로딩 (구독 방식 유지)
926        #######################*/
927        _loadRecentViewItems: function () {
928            return new Promise((resolve) => {
929                $.publish("recent-viewitem.bind");
930                resolve();
931            });
932        },
933
934        _setupSearchSubscriptions: function () {
935            // 최근 본 상품 구독 (기존 로직 유지)
936            $.subscribe("recent-viewitem.bind", function () {
937                const $section = $("#recent-viewitem-section");
938                if ($section.hasClass("on")) return;
939
940                aladin.search._makeRequest({
941                    url: "/search/v2/recent-viewitem.ashx",
942                    timeout: aladin.search.timeouts.viewitem,
943                    context: 'recent_viewitem_bind'
944                }).then(data => {
945                    if (data?.ResultCode == 1 && Array.isArray(data.List)) {
946                        // 최근 본 상품 렌더링 로직...
947                        $section.addClass("on");
948                    }
949                }).catch(error => {
950                    console.warn('Failed to load recent view items:', error);
951                });
952            });
953        },
954
955        /*######################
956        검색 결과 처리
957        #######################*/
958        searchResult: function (keyword, needsave) {
959            keyword = keyword.trim();
960
961            if (!keyword) {
962                alert('검색어를 입력하세요.');
963                this.input.focus();
964                return;
965            }
966
967            if (needsave) {
968                this._saveRecentData('keyword', keyword);
969            }
970
971            this.form.submit();
972        },
973
974        /*######################
975        아이템 관련 메서드
976        #######################*/
977        addBasket: function (itemid, e) {
978            e.preventDefault();
979            if (itemid) {
980                location.href = '/shop/wbasket.aspx?addbook=' + itemid + '&start=slayer';
981            }
982        },
983
984        viewItem: function (itemid, needsave, param) {
985            if (!itemid) return false;
986
987            param = param || "pcsearch_auto";
988
989            if (needsave === undefined) {
990                this._navigateToItem(itemid, param);
991            } else {
992                this._saveRecentData('item', Math.abs(itemid));
993                this._navigateToItem(itemid, param);
994            }
995
996            return false;
997        },
998
999        _navigateToItem: function (itemid, param) {
1000            const url = itemid < 0
1001                ? `/shop/usedshop/wuseditemall.aspx?ItemId=${Math.abs(itemid)}&start=${param}`
1002                : `/shop/wproduct.aspx?ItemId=${itemid}&start=${param}`;
1003            location.href = url;
1004        },
1005
1006        /*######################
1007        키워드 관련 메서드
1008        #######################*/
1009        changeKeyword: function (keyword) {
1010            this.input.val(decodeURIComponent(keyword));
1011            this.form.append('<input type="hidden" name="start" value="pcsearch_recen" />');
1012            this.searchResult(decodeURIComponent(keyword));
1013        },
1014
1015        deleteRecentKeyword: function (method, seq, e) {
1016            e.preventDefault();
1017
1018            return this._makeRequest({
1019                url: "/search/v2/recent-keyword.ashx",
1020                data: { method: method, Seq: seq },
1021                timeout: this.timeouts.keyword,
1022                context: 'deleteRecentKeyword'
1023            }).then(data => {
1024                if (data?.ResultCode == 1) {
1025                    $("#recent-keyword-section").removeClass("on");
1026                    return this._loadRecentKeywords();
1027                }
1028            }).catch(error => {
1029                console.warn('Failed to delete recent keyword:', error);
1030            });
1031        },
1032
1033        /*######################
1034        유틸리티 메서드들
1035        #######################*/
1036        searchLayerClose: function () {
1037            this.input.blur();
1038            $.publish("mode-search.hide");
1039        },
1040    };
1041})(jQuery);

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.