PageSourceSearch

https://artveteran.com/assets/js/search/search.js?ver26.06.2&t1791501160

js artveteran.com collected 2026-10-08 23:13:28 UTC 12,218 bytes, 304 lines download raw bytes

1// 검색 기능 JavaScript
2$(document).ready(function () {
3    // 검색 컴포넌트 초기화
4    initializeSearchComponents();
5});
6
7function initializeSearchComponents() {
8    // 기본 검색 컴포넌트 초기화
9    initializeSearchComponent("basic-search-input", "search-results-dropdown", "search-results-list", "full");
10
11    // 키워드 검색 컴포넌트 초기화
12    initializeSearchComponent("basic-search-keyword-input", "search-keyword-results-dropdown", "search-keyword-results-list", "keyword");
13}
14
15function initializeSearchComponent(inputId, resultsId, listId, searchType) {
16    var searchTimeout;
17    var currentRequest = null;
18    var $searchInput = $("#" + inputId);
19    var $searchResults = $("#" + resultsId);
20    var $searchResultsList = $("#" + listId);
21    var lastKeyword = "";
22    var cachedResults = []; // 검색 결과 캐시 추가
23
24    // 검색어 입력 시 (실시간)
25    $searchInput.on("input", function () {
26        var keyword = $(this).val().trim();
27
28        clearTimeout(searchTimeout);
29
30        if (currentRequest) {
31            currentRequest.abort();
32        }
33
34        if (keyword.length < 1) {
35            $searchResults.hide();
36            cachedResults = []; // 캐시 초기화
37            return;
38        }
39
40        // 검색어가 있을 때는 결과를 숨기지 않음
41        searchTimeout = setTimeout(function () {
42            performSearch(keyword);
43        }, 100);
44    });
45
46    function performSearch(keyword) {
47        if (keyword === lastKeyword) {
48            return;
49        }
50
51        lastKeyword = keyword;
52
53        currentRequest = $.ajax({
54            url: "/search/basic_search",
55            method: "GET",
56            data: {
57                keyword: keyword,
58                search_type: searchType,
59            },
60            dataType: "json",
61            contentType: "application/x-www-form-urlencoded; charset=UTF-8",
62            success: function (response) {
63                if (response.keyword !== $searchInput.val().trim()) {
64                    return;
65                }
66
67                if (response.success && response.results.length > 0) {
68                    cachedResults = response.results; // 결과 캐시
69                    displayResults(response.results);
70                } else {
71                    cachedResults = []; // 캐시 초기화
72                    if (response.success && response.results.length === 0) {
73                        $searchResultsList.html('<div class="search-loading">검색 결과가 없습니다.</div>');
74                        $searchResults.show();
75                    } else {
76                        $searchResults.hide();
77                    }
78                }
79            },
80            error: function (xhr, status, error) {
81                if (status === "abort") {
82                    return;
83                }
84
85                console.error("Search error:", error);
86                console.error("Response:", xhr.responseText);
87                $searchResultsList.html('<div class="search-loading">검색 중 오류가 발생했습니다.</div>');
88            },
89            complete: function () {
90                currentRequest = null;
91            },
92        });
93    }
94
95    function displayResults(results) {
96        $searchResultsList.empty();
97        var currentKeyword = $searchInput.val().trim();
98
99        results.forEach(function (result) {
100            // 타입에 따른 표시 텍스트 설정
101            var typeText = "게시글"; // 기본값
102            if (result.type === "page") {
103                typeText = "페이지";
104            } else if (result.type === "board_name") {
105                typeText = "게시판";
106            }
107
108            var titleHtml = '<div class="search-result-title" title="' + result.title + '"><span class="search-result-type ' + result.type + '">' + typeText + "</span>";
109
110            // 게시글인 경우 게시판 명을 제목 앞에 추가 (키워드 검색이 아닐 때만)
111            if (result.type === "board" && result.description && searchType !== "keyword") {
112                var boardName = result.description.split(" - ")[0];
113                // 게시판 이름에도 키워드 하이라이트 처리
114                var highlightedBoardName = highlightKeyword(boardName, currentKeyword);
115                titleHtml += '<span class="search-result-board">[' + highlightedBoardName + "]</span> ";
116            }
117
118            // 제목에서 키워드 하이라이트 처리
119            var highlightedTitle = highlightKeyword(result.title, currentKeyword);
120            titleHtml += highlightedTitle + "</div>";
121
122            // 설명에서도 키워드 하이라이트 처리 (키워드 검색이 아닐 때만, 작ì
122„±ìž 이름 제외)
123            var highlightedDescription = "";
124            if (result.description && searchType !== "keyword") {
125                // 작성자 이름 부분 제거 (예: "게시판명 - 작성자명"에서 "게시판명"만 사용)
126                var descriptionParts = result.description.split(" - ");
127                if (descriptionParts.length > 1) {
128                    // 게시판 이름만 사용
129                    var boardNameOnly = descriptionParts[0];
130                    highlightedDescription = '<div class="search-result-description">' + highlightKeyword(boardNameOnly, currentKeyword) + "</div>";
131                } else {
132                    // 설명에 구분자가 없으면 그대로 사용
133                    highlightedDescription = '<div class="search-result-description">' + highlightKeyword(result.description, currentKeyword) + "</div>";
134                }
135            }
136
137            var $item = $("<div>")
138                .addClass("search-result-item")
139                .data("url", result.url) // URL을 data 속성에 저장
140                .html(titleHtml + highlightedDescription);
141
142            $item.on("click", function () {
143                window.location.href = result.url;
144            });
145
146            $searchResultsList.append($item);
147        });
148
149        $searchResults.show();
150
151        // 첫 번째 항목 자동 선택
152        var $firstItem = $searchResultsList.find(".search-result-item").first();
153        if ($firstItem.length > 0) {
154            $firstItem.addClass("active");
155        }
156    }
157
158    // 키워드 하이라이트 함수
159    function highlightKeyword(text, keyword) {
160        if (!keyword || keyword.length === 0) {
161            return text;
162        }
163
164        // 대소문자 구분 없이 검색하기 위해 소문자로 변환하여 비교
165        var lowerText = text.toLowerCase();
166        var lowerKeyword = keyword.toLowerCase();
167
168        if (lowerText.indexOf(lowerKeyword) === -1) {
169            return text;
170        }
171
172        // 원본 텍스트에서 키워드 위치 찾기
173        var result = "";
174        var lastIndex = 0;
175        var currentIndex = 0;
176
177        while ((currentIndex = lowerText.indexOf(lowerKeyword, lastIndex)) !== -1) {
178            // 키워드 이전 텍스트 추가
179            result += text.substring(lastIndex, currentIndex);
180            // 키워드 부분을 하이라이트 처리
181            result += '<strong class="search-highlight">' + text.substring(currentIndex, currentIndex + keyword.length) + "</strong>";
182
183            lastIndex = currentIndex + keyword.length;
184        }
185
186        // 남은 텍스트 추가
187        result += text.substring(lastIndex);
188
189        return result;
190    }
191
192    // 외부 클릭 시 드롭다운 닫기 (수정)
193    $(document).on("click", function (e) {
194        // 검색 입력창이나 결과 영역을 클릭한 경우가 아닐 때만 닫기
195        if (!$(e.target).closest(".basic-search-container, .basic-search-keyword-container").length) {
196            $searchResults.hide();
197        }
198    });
199
200    // 키보드 네비게이션 (엔터키 - 첫 번째 항목 또는 선택된 항목으로 이동)
201    $searchInput.on("keydown", function (e) {
202        var $items = $searchResultsList.find(".search-result-item");
203        var $activeItem = $items.filter(".active");
204
205        switch (e.keyCode) {
206            case 40: // 아래 화살표
207                e.preventDefault();
208                // 검색 결과가 숨겨져 있으면 다시 표시
209                if ($searchResults.is(":hidden") && cachedResults.length > 0) {
210                    displayResults(cachedResults);
211                    $items = $searchResultsList.find(".search-result-item"); // 아이템 다시 가져오기
212                }
213
214                if ($items.length === 0) return; // 아이템이 없으면 종료
215
216                if ($activeItem.length === 0) {
217                    // 활성 아이템이 없으면 첫 번째 아이템 선택
218                    $items.first().addClass("active");
219                } else {
220                    // 현재 활성 아이템의 다음 아이템 선택
221                    var $nextItem = $activeItem.next();
222                    if ($nextItem.length === 0) {
223                        // 마지막 아이템이면 첫 번째 아이템으로
224                        $items.first().addClass("active");
225                    } else {
226                        $nextItem.addClass("active");
227                    }
228                    $activeItem.removeClass("active");
229                }
230                break;
231
232            case 38: // 위 화살표
233                e.preventDefault();
234                // 검색 결과가 숨겨져 있으면 다시 표시
235                if ($searchResults.is(":hidden") && cachedResults.length > 0) {
236                    displayResults(cachedResults);
237                    $items = $searchResultsList.find(".search-result-item"); // 아이템 다시 가져오기
238                }
239
240                if ($items.length === 0) return; // 아이템이 없으면 종료
241
242                if ($activeItem.length === 0) {
243                    // 활성 아이템이 없으면 마지막 아이템 선택
244                    $items.last().addClass("active");
245                } else {
246                    // 현재 활성 아이템의 이전 아이템 선택
247                    var $prevItem = $activeItem.prev();
248                    if ($prevItem.length === 0) {
249                        // 첫 번째 아이템이면 마지막 아이템으로
250                        $items.last().addClass("active");
251                    } else {
252                        $prevItem.addClass("active");
253                    }
254                    $activeItem.removeClass("active");
255                }
256                break;
257
258            case 13: // 엔터키 - 첫 번째 항목 또는 선택된 항목으로 이동
259                e.preventDefault();
260                if ($items.length > 0) {
261                    var targetItem;
262                    if ($activeItem.length > 0) {
263                        // 활성 아이템이 있으면 해당 아이템으로 이동
264                        targetItem = $activeItem;
265                    } else {
266                        // 활성 아이템이 없으면 첫 번째 아이템으로 이동
267                        targetItem = $items.first();
268                    }
269
270                    var url = targetItem.data("url");
271                    if (url) {
272                        window.location.href = url;
273                    }
274                }
275                break;
276
277            case 27: // ESC
278                $searchResults.hide();
279                break;
280        }
281    });
282
283    // 포커스 시 결과 표시 (개선)
284    $searchInput.on("focus", function () {
285        var keyword = $(this).val().trim();
286        if (keyword.length >= 1) {
287            if (cachedResults.length > 0) {
288                // 캐시된 결과가 있으면 표시
289                displayResults(cachedResults);
290            } else if ($searchResultsList.children().length > 0) {
291                // 기존 결과가 있으면 표시
292                $searchResults.show();
293            }
294        }
295    });
296
297    // 블러 시 활성 아이템 제거 (수정)
298    $searchInput.on("blur", function () {
299        // 검색 결과를 숨기지 않고 활성 아이템만 제거
300        setTimeout(function () {
301            $searchResultsList.find(".search-result-item").removeClass("active");
302        }, 200);
303    });
304}

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.