korean-search-utils
Version:
한글 초성 검색 및 React 훅 지원 라이브러리
120 lines (109 loc) • 6.06 kB
HTML
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>korean-search-utils v1.1.0 테스트</title>
<script type="importmap">{"imports": {"react": "https://esm.sh/react@19"}}</script>
<style>
body { font-family: -apple-system, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; background: #f5f5f5; }
h1 { font-size: 1.4rem; color: #333; }
.version { background: #0070f3; color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; }
input { width: 100%; padding: 10px 14px; font-size: 1rem; border: 2px solid #ddd; border-radius: 8px; box-sizing: border-box; margin: 12px 0; }
input:focus { outline: none; border-color: #0070f3; }
.results { background: white; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,.08); overflow: hidden; min-height: 48px; }
.item { padding: 12px 16px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; gap: 12px; }
.item:last-child { border-bottom: none; }
.emoji { font-size: 1.3rem; }
mark { background: #fff3cd; color: #333; border-radius: 2px; padding: 0 1px; font-weight: 700; }
.empty { padding: 24px; text-align: center; color: #999; }
.section { margin: 28px 0; }
h2 { font-size: 1rem; color: #555; margin-bottom: 6px; }
.test-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.test-case { background: white; padding: 10px 14px; border-radius: 6px; font-size: 0.88rem; }
.test-case .label { color: #888; font-size: 0.78rem; margin-bottom: 2px; }
.pass { border-left: 3px solid #22c55e; }
.fail { border-left: 3px solid #ef4444; }
.result-val { font-weight: 600; }
.pass .result-val { color: #16a34a; }
.fail .result-val { color: #dc2626; }
.hint { font-size: 0.82rem; color: #888; margin-top: -4px; margin-bottom: 4px; }
</style>
</head>
<body>
<h1>korean-search-utils <span class="version">v1.1.0</span></h1>
<div class="section">
<h2>🔍 과일 검색 데모</h2>
<p class="hint">ㅅㄱ, 딸기, 바나, banana 등으로 검색해보세요</p>
<input id="searchInput" type="text" placeholder="과일 이름 검색..." autofocus />
<div class="results" id="results"></div>
</div>
<div class="section">
<h2>✅ 단위 테스트</h2>
<div class="test-grid" id="tests"></div>
</div>
<script type="module">
import { isMatch, disassembleHangul, getMatchStartIndex, getMatchScore, getMatchSegments, filterAndSort }
from '/index.js';
const fruits = [
{ name: "사과", english: "apple", emoji: "🍎" },
{ name: "바나나", english: "banana", emoji: "🍌" },
{ name: "오렌지", english: "orange", emoji: "🍊" },
{ name: "포도", english: "grape", emoji: "🍇" },
{ name: "딸기", english: "strawberry",emoji: "🍓" },
{ name: "수박", english: "watermelon",emoji: "🍉" },
{ name: "복숭아", english: "peach", emoji: "🍑" },
{ name: "망고", english: "mango", emoji: "🥭" },
{ name: "키위", english: "kiwi", emoji: "🥝" },
{ name: "체리", english: "cherry", emoji: "🍒" },
{ name: "파인애플", english: "pineapple",emoji: "🍍" },
{ name: "블루베리", english: "blueberry",emoji: "🫐" },
{ name: "레몬", english: "lemon", emoji: "🍋" },
{ name: "멜론", english: "melon", emoji: "🍈" },
{ name: "자두", english: "plum", emoji: "🍑" },
];
function renderResults(query) {
const el = document.getElementById('results');
const list = query.trim()
? filterAndSort(fruits, query, f => f.name + ' ' + f.english)
: fruits;
if (query.trim() && !list.length) {
el.innerHTML = '<div class="empty">검색 결과 없음</div>';
return;
}
el.innerHTML = list.map(fruit => {
const segs = query.trim()
? getMatchSegments(fruit.name, query)
: [{ text: fruit.name, matched: false }];
const highlighted = segs.map(s => s.matched ? `<mark>${s.text}</mark>` : s.text).join('');
return `
<div class="item">
<span class="emoji">${fruit.emoji}</span>
<span>${highlighted}</span>
<span style="color:#bbb;font-size:.82rem">${fruit.english}</span>
</div>`;
}).join('');
}
document.getElementById('searchInput').addEventListener('input', e => renderResults(e.target.value));
renderResults('');
const testCases = [
{ desc: 'isMatch("사과", "ㅅㄱ")', fn: () => isMatch("사과", "ㅅㄱ"), expect: true },
{ desc: 'isMatch("딸기", "딸기")', fn: () => isMatch("딸기", "딸기"), expect: true },
{ desc: 'isMatch("바나나", "ㅂㄴ")', fn: () => isMatch("바나나", "ㅂㄴ"), expect: true },
{ desc: 'isMatch("수박", "포도")', fn: () => isMatch("수박", "포도"), expect: false },
{ desc: 'getMatchStartIndex("사과","ㅅ")', fn: () => getMatchStartIndex("사과","ㅅ"), expect: 0 },
{ desc: 'getMatchScore no match=Infinity', fn: () => isFinite(getMatchScore("사과","포도")), expect: false },
{ desc: 'filterAndSort("딸기") 결과', fn: () => filterAndSort(fruits,"딸기",f=>f.name).map(f=>f.name), expect: ["딸기"] },
{ desc: 'getMatchSegments("사과","ㅅ") 매칭 포함', fn: () => getMatchSegments("사과","ㅅ").some(s=>s.matched), expect: true },
];
document.getElementById('tests').innerHTML = testCases.map(tc => {
let result, pass;
try { result = tc.fn(); pass = JSON.stringify(result) === JSON.stringify(tc.expect); }
catch(e) { result = e.message; pass = false; }
return `<div class="test-case ${pass?'pass':'fail'}">
<div class="label">${tc.desc}</div>
<div class="result-val">${pass ? '✅ PASS' : `❌ FAIL (${JSON.stringify(result)})`}</div>
</div>`;
}).join('');
</script>
</body>
</html>