통합검색 기능 추가

통합검색 기능 추가

라이믹스 레이아웃에 통합검색 기능을 구현하는 방법을 학습합니다.

기본 통합검색 구현

검색 폼 구조

<!-- 통합검색 폼 -->
<form action="{getUrl()}" method="get" class="integrated-search" onsubmit="return checkSearch(this)">
    <input type="hidden" name="vid" value="{$vid}" />
    <input type="hidden" name="mid" value="{$mid}" />
    <input type="hidden" name="act" value="IS" />

    <div class="search-wrapper">
        <!-- 검색 대상 선택 -->
        <select name="search_target" class="search-target">
            <option value="title_content">제목+내용</option>
            <option value="title">제목</option>
            <option value="content">내용</option>
            <option value="user_name">작성자</option>
            <option value="user_id">아이디</option>
            <option value="tag">태그</option>
        </select>

        <!-- 검색어 입력 -->
        <input type="search" 
               name="search_keyword" 
               value="{$search_keyword}" 
               placeholder="검색어를 입력하세요" 
               class="search-input"
               required />

        <!-- 검색 버튼 -->
        <button type="submit" class="search-button">
            <i class="xi-search"></i>
            <span class="sr-only">검색</span>
        </button>
    </div>
</form>

<script>
function checkSearch(form) {
    var keyword = form.search_keyword.value.trim();
    if(keyword.length < 2) {
        alert('검색어는 2글자 이상 입력해주세요.');
        form.search_keyword.focus();
        return false;
    }
    return true;
}
</script>

고급 통합검색

모듈별 검색

<!-- 모듈별 통합검색 -->
<div class="advanced-search">
    <form action="{getUrl()}" method="get" id="advancedSearchForm">
        <input type="hidden" name="act" value="IS" />

        <!-- 검색 범위 선택 -->
        <div class="search-scope">
            <h4>검색 범위</h4>
            <label>
                <input type="radio" name="search_scope" value="all" checked="checked" />
                전체 검색
            </label>
            <label>
                <input type="radio" name="search_scope" value="current" />
                현재 게시판만
            </label>
            <label>
                <input type="radio" name="search_scope" value="custom" />
                선택한 게시판
            </label>
        </div>

        <!-- 게시판 선택 (커스텀 검색시) -->
        <div class="module-selection" style="display:none;">
            {@
                // 검색 가능한 모듈 목록 가져오기
                $oModuleModel = getModel('module');
                $module_list = $oModuleModel->getModuleList();

                $searchable_modules = array();
                foreach($module_list as $module) {
                    if(in_array($module->module, array('board', 'bodex', 'marketplace'))) {
                        $searchable_modules[] = $module;
                    }
                }
            }

            <div class="module-checkboxes">
                <label loop="$searchable_modules=>$module">
                    <input type="checkbox" name="target_modules[]" value="{$module->module_srl}" />
                    {$module->browser_title ?: $module->mid}
                </label>
            </div>
        </div>

        <!-- 검색 옵션 -->
        <div class="search-options">
            <h4>검색 옵션</h4>

            <!-- 기간 설정 -->
            <div class="date-range">
                <label>기간</label>
                <select name="search_date_range">
                    <option value="">전체</option>
                    <option value="1">1일</option>
                    <option value="7">1주일</option>
                    <option value="30">1개월</option>
                    <option value="90">3개월</option>
                    <option value="365">1년</option>
                </select>
            </div>

            <!-- 정렬 순서 -->
            <div class="sort-order">
                <label>정렬</label>
                <select name="sort_index">
                    <option value="regdate">최신순</option>
                    <option value="voted_count">추천순</option>
                    <option value="readed_count">조회순</option>
                    <option value="comment_count">댓글순</option>
                </select>
            </div>
        </div>

        <!-- 검색어 입력 -->
        <div class="search-input-area">
            <input type="text" 
                   name="search_keyword" 
                   placeholder="검색어를 입력하세요" 
                   class="search-keyword" />
            <button type="submit" class="btn-search">검색</button>
        </div>
    </form>
</div>

<script>
jQuery(function($) {
    // 검색 범위 변경시
    $('input[name="search_scope"]').change(function() {
        if($(this).val() == 'custom') {
            $('.module-selection').show();
        } else {
            $('.module-selection').hide();
        }
    });

    // 폼 제출시 처리
    $('#advancedSearchForm').submit(function() {
        var scope = $('input[name="search_scope"]:checked').val();

        if(scope == 'current') {
            // 현재 모듈만 검색
            $('<input>').attr({
                type: 'hidden',
                name: 'target_modules[]',
                value: window.current_module_srl
            }).appendTo(this);
        } else if(scope == 'custom') {
            // 선택한 모듈이 있는지 확인
            if($('input[name="target_modules[]"]:checked').length == 0) {
                alert('검색할 게시판을 선택해주세요.');
                return false;
            }
        }

        return true;
    });
});
</script>

실시간 검색

자동완성 기능

<!-- 실시간 검색 자동완성 -->
<div class="realtime-search">
    <input type="text" 
           id="realtimeSearchInput" 
           placeholder="검색어를 입력하세요" 
           autocomplete="off" />

    <!-- 자동완성 결과 -->
    <div class="autocomplete-results" style="display:none;">
        <ul class="result-list"></ul>
    </div>
</div>

<script>
(function($) {
    var searchTimer = null;
    var currentRequest = null;

    $('#realtimeSearchInput').on('input', function() {
        var keyword = $(this).val().trim();
        var $results = $('.autocomplete-results');

        // 타이머 초기화
        clearTimeout(searchTimer);

        // 이전 요청 취소
        if(currentRequest) {
            currentRequest.abort();
        }

        if(keyword.length < 2) {
            $results.hide();
            return;
        }

        // 300ms 디바운싱
        searchTimer = setTimeout(function() {
            currentRequest = $.ajax({
                url: request_uri,
                type: 'POST',
                data: {
                    module: 'integration_search',
                    act: 'getSearchAutocomplete',
                    search_keyword: keyword
                },
                dataType: 'json',
                success: function(response) {
                    if(response.error != 0) return;

                    var html = '';
                    if(response.data && response.data.length > 0) {
                        $.each(response.data, function(i, item) {
                            html += '<li>' +
                                    '<a href="' + item.url + '">' +
                                    '<span class="title">' + highlightKeyword(item.title, keyword) + '</span>' +
                                    '<span class="module">' + item.module_name + '</span>' +
                                    '</a></li>';
                        });
                    } else {
                        html = '<li class="no-result">검색 결과가 없습니다.</li>';
                    }

                    $('.result-list').html(html);
                    $results.show();
                }
            });
        }, 300);
    });

    // 키워드 하이라이트
    function highlightKeyword(text, keyword) {
        var regex = new RegExp('(' + keyword + ')', 'gi');
        return text.replace(regex, '<mark>$1</mark>');
    }

    // 외부 클릭시 닫기
    $(document).click(function(e) {
        if(!$(e.target).closest('.realtime-search').length) {
            $('.autocomplete-results').hide();
        }
    });
})(jQuery);
</script>

검색 결과 페이지

커스텀 검색 결과

<!-- 통합검색 결과 페이지 -->
<div class="search-results" cond="$act == 'IS'">
    <div class="search-header">
        <h2>
            '<strong>{$search_keyword}</strong>' 검색 결과
            <span class="result-count">({number_format($total_count)}건)</span>
        </h2>

        <!-- 검색 필터 -->
        <div class="search-filters">
            <a href="{getUrl('search_target', '')}" class="active"|cond="!$search_target">전체</a>
            <a href="{getUrl('search_target', 'title')}" class="active"|cond="$search_target=='title'">제목</a>
            <a href="{getUrl('search_target', 'content')}" class="active"|cond="$search_target=='content'">내용</a>
            <a href="{getUrl('search_target', 'title_content')}" class="active"|cond="$search_target=='title_content'">제목+내용</a>
            <a href="{getUrl('search_target', 'tag')}" class="active"|cond="$search_target=='tag'">태그</a>
        </div>
    </div>

    <!-- 모듈별 검색 결과 -->
    <div class="module-results">
        {@
            // 모듈별로 검색 결과 그룹화
            $grouped_results = array();
            foreach($search_result as $document) {
                $module_srl = $document->get('module_srl');
                if(!isset($grouped_results[$module_srl])) {
                    $module_info = getModel('module')->getModuleInfoByModuleSrl($module_srl);
                    $grouped_results[$module_srl] = array(
                        'module_info' => $module_info,
                        'documents' => array()
                    );
                }
                $grouped_results[$module_srl]['documents'][] = $document;
            }
        }

        <section loop="$grouped_results=>$module_srl,$group" class="module-section">
            <h3>
                {$group['module_info']->browser_title ?: $group['module_info']->mid}
                <span class="count">({count($group['documents'])})</span>
            </h3>

            <ul class="result-list">
                <li loop="$group['documents']=>$document" class="result-item">
                    <h4 class="title">
                        <a href="{getUrl('', 'mid', $group['module_info']->mid, 'document_srl', $document->document_srl)}">
                            {highlightSearchKeyword($document->getTitle(), $search_keyword)}
                        </a>
                    </h4>

                    <p class="excerpt">
                        {highlightSearchKeyword($document->getSummary(200), $search_keyword)}
                    </p>

                    <div class="meta">
                        <span class="author">{$document->getNickName()}</span>
                        <span class="date">{$document->getRegdate('Y.m.d')}</span>
                        <span class="comment" cond="$document->getCommentCount()">
                            댓글 {$document->getCommentCount()}
                        </span>
                    </div>
                </li>
            </ul>

            <!-- 더보기 -->
            <a href="{getUrl('', 'mid', $group['module_info']->mid, 'search_keyword', $search_keyword)}" 
               class="more-link" 
               cond="count($group['documents']) >= 5">
                {$group['module_info']->browser_title}에서 더 많은 결과 보기 →
            </a>
        </section>
    </div>

    <!-- 페이지네이션 -->
    <div class="pagination">
        {$page_navigation}
    </div>
</div>

<!-- 검색 키워드 하이라이트 함수 -->
{@
    function highlightSearchKeyword($text, $keyword) {
        if(!$keyword) return $text;

        $keywords = explode(' ', $keyword);
        foreach($keywords as $word) {
            if(strlen($word) < 2) continue;
            $text = preg_replace('/(' . preg_quote($word, '/') . ')/iu', '<mark>$1</mark>', $text);
        }

        return $text;
    }
}

인기 검색어

검색어 순위 표시

<!-- 인기 검색어 위젯 -->
<div class="popular-keywords-widget">
    <h3>인기 검색어</h3>

    {@
        // 인기 검색어 가져오기 (캐시 사용)
        $cache_key = 'popular_keywords_' . date('YmdH');
        $popular_keywords = Rhymix\Framework\Cache::get($cache_key);

        if(!$popular_keywords) {
            $args = new stdClass();
            $args->list_count = 10;
            $args->date = date('Ymd', strtotime('-1 day'));

            $output = executeQuery('integration_search.getPopularKeywords', $args);
            $popular_keywords = $output->data;

            Rhymix\Framework\Cache::set($cache_key, $popular_keywords, 3600);
        }
    }

    <ol class="keyword-list">
        <li loop="$popular_keywords=>$idx,$keyword" class="keyword-item">
            <span class="rank">{$idx + 1}</span>
            <a href="{getUrl('', 'act', 'IS', 'search_keyword', $keyword->search_keyword)}">
                {$keyword->search_keyword}
            </a>

            <!-- 순위 변동 -->
            <span class="change" cond="$keyword->rank_change">
                <i cond="$keyword->rank_change > 0" class="xi-arrow-up"></i>
                <i cond="$keyword->rank_change < 0" class="xi-arrow-down"></i>
                <span cond="$keyword->rank_change == 0">-</span>
            </span>
        </li>
    </ol>

    <!-- 실시간 업데이트 -->
    <div class="update-time">
        {date('H:i')} 기준
    </div>
</div>

<style>
.keyword-list {
    list-style: none;
    padding: 0;
}

.keyword-item {
    display: flex;
    align-items: center;
    padding: 8px 0;
    border-bottom: 1px solid #eee;
}

.rank {
    width: 30px;
    font-weight: bold;
    color: #666;
}

.keyword-item:nth-child(-n+3) .rank {
    color: #ff6b6b;
}

.change {
    margin-left: auto;
    font-size: 12px;
}

.change .xi-arrow-up { color: #ff6b6b; }
.change .xi-arrow-down { color: #4dabf7; }
</style>

검색 최적화

검색 인덱싱

<!-- 검색 성능 최적화 설정 -->
{@
    // 검색 설정
    $search_config = array(
        'min_keyword_length' => 2,           // 최소 검색어 길이
        'max_keyword_length' => 20,          // 최대 검색어 길이
        'search_cache_time' => 300,          // 검색 결과 캐시 시간 (초)
        'autocomplete_limit' => 10,          // 자동완성 결과 수
        'highlight_color' => '#ffeb3b',      // 하이라이트 색상
        'use_stemming' => true,              // 형태소 분석 사용
        'excluded_words' => array('은', '는', '이', '가', '을', '를') // 불용어
    );

    Context::set('search_config', $search_config);
}

<!-- 검색어 전처리 -->
<script>
function preprocessSearchKeyword(keyword) {
    // 특수문자 제거
    keyword = keyword.replace(/[^\w\s가-힣]/g, ' ');

    // 연속 공백 제거
    keyword = keyword.replace(/\s+/g, ' ').trim();

    // 불용어 제거
    var excluded = {$search_config['excluded_words']|@json_encode};
    var words = keyword.split(' ');
    words = words.filter(function(word) {
        return excluded.indexOf(word) === -1 && word.length >= 2;
    });

    return words.join(' ');
}

// 검색 폼 제출 전 전처리
$('.integrated-search').submit(function() {
    var keyword = $(this).find('input[name="search_keyword"]').val();
    var processed = preprocessSearchKeyword(keyword);

    if(processed.length < {$search_config['min_keyword_length']}) {
        alert('검색어는 {$search_config['min_keyword_length']}글자 이상 입력해주세요.');
        return false;
    }

    $(this).find('input[name="search_keyword"]').val(processed);
    return true;
});
</script>

모범 사례

  1. 성능 최적화: 검색 결과 캐싱 활용
  2. 사용자 경험: 자동완성, 검색어 제안 제공
  3. 보안: 검색어 입력값 검증 철저히
  4. 접근성: 키보드 네비게이션 지원
  5. 반응형: 모바일 검색 UI 최적화

다음 단계

통합검색 기능을 구현했다면, 로그인 시스템을 학습하세요.