통합검색 기능 추가¶
라이믹스 레이아웃에 통합검색 기능을 구현하는 방법을 학습합니다.
기본 통합검색 구현¶
검색 폼 구조¶
<!-- 통합검색 폼 -->
<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>
모범 사례¶
- 성능 최적화: 검색 결과 캐싱 활용
- 사용자 경험: 자동완성, 검색어 제안 제공
- 보안: 검색어 입력값 검증 철저히
- 접근성: 키보드 네비게이션 지원
- 반응형: 모바일 검색 UI 최적화
다음 단계¶
통합검색 기능을 구현했다면, 로그인 시스템을 학습하세요.