UNPKG

korean-search-utils

Version:

한글 초성 검색 및 React 훅 지원 라이브러리

120 lines (109 loc) 6.06 kB
<!DOCTYPE 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>