feat(search): / 단축키 검색 모달 및 통합 검색 API 추가
- / 및 헤더 검색 클릭으로 모달을 열고 태그·게시물 검색을 제공. - 태그 검색 범위를 name/slug로 제한하고 IME 조합 입력 대응을 보강. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
12
server/api/search.get.js
Normal file
12
server/api/search.get.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { searchPublicContent } from '../repositories/content-repository'
|
||||
|
||||
/**
|
||||
* 공개 통합 검색 API(태그·게시물)
|
||||
* @param {import('h3').H3Event} event - 요청 이벤트
|
||||
* @returns {Promise<{ tags: Array<{ name: string, slug: string }>, posts: Array<{ slug: string, title: string, excerpt: string }> }>} 검색 결과
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const raw = getQuery(event).q
|
||||
const q = Array.isArray(raw) ? raw[0] : raw
|
||||
return searchPublicContent(typeof q === 'string' ? q : '')
|
||||
})
|
||||
@@ -548,6 +548,82 @@ export const listTags = async () => {
|
||||
return rows.map(mapTagRow)
|
||||
}
|
||||
|
||||
const SEARCH_TAG_LIMIT = 12
|
||||
const SEARCH_POST_LIMIT = 12
|
||||
|
||||
/**
|
||||
* 공개 검색: 태그·게시물 제목·요약·본문에서 부분 일치(대소문자 무시)
|
||||
* @param {string} rawQuery - 검색어
|
||||
* @returns {Promise<{ tags: Array<{ name: string, slug: string }>, posts: Array<{ slug: string, title: string, excerpt: string }> }>} 태그·게시물 요약 결과
|
||||
*/
|
||||
export const searchPublicContent = async (rawQuery) => {
|
||||
const q = String(rawQuery || '').trim()
|
||||
if (!q) {
|
||||
return { tags: [], posts: [] }
|
||||
}
|
||||
|
||||
const sql = getPostgresClient()
|
||||
|
||||
if (!sql) {
|
||||
const needle = q.toLowerCase()
|
||||
const posts = getSamplePosts()
|
||||
.filter((post) => {
|
||||
const hay = `${post.title}\n${post.excerpt || ''}\n${post.content || ''}`.toLowerCase()
|
||||
return hay.includes(needle)
|
||||
})
|
||||
.slice(0, SEARCH_POST_LIMIT)
|
||||
.map((post) => ({
|
||||
slug: post.slug,
|
||||
title: post.title,
|
||||
excerpt: post.excerpt || ''
|
||||
}))
|
||||
const tags = getSampleTags()
|
||||
.filter((tag) => {
|
||||
const hay = `${tag.name}\n${tag.slug}`.toLowerCase()
|
||||
return hay.includes(needle)
|
||||
})
|
||||
.slice(0, SEARCH_TAG_LIMIT)
|
||||
.map((tag) => ({
|
||||
name: tag.name,
|
||||
slug: tag.slug
|
||||
}))
|
||||
return { tags, posts }
|
||||
}
|
||||
|
||||
const tagRows = await sql`
|
||||
SELECT name, slug
|
||||
FROM tags
|
||||
WHERE
|
||||
position(lower(${q}) in lower(name)) > 0
|
||||
OR position(lower(${q}) in lower(slug)) > 0
|
||||
ORDER BY sort_order ASC, name ASC
|
||||
LIMIT ${SEARCH_TAG_LIMIT}
|
||||
`
|
||||
|
||||
const postRows = await sql`
|
||||
SELECT posts.slug, posts.title, posts.excerpt
|
||||
FROM posts
|
||||
WHERE posts.status = 'published'
|
||||
AND (posts.published_at IS NULL OR posts.published_at <= now())
|
||||
AND (
|
||||
position(lower(${q}) in lower(posts.title)) > 0
|
||||
OR position(lower(${q}) in lower(coalesce(posts.excerpt, ''))) > 0
|
||||
OR position(lower(${q}) in lower(posts.content)) > 0
|
||||
)
|
||||
ORDER BY posts.published_at DESC NULLS LAST, posts.created_at DESC
|
||||
LIMIT ${SEARCH_POST_LIMIT}
|
||||
`
|
||||
|
||||
return {
|
||||
tags: tagRows.map((row) => ({ name: row.name, slug: row.slug })),
|
||||
posts: postRows.map((row) => ({
|
||||
slug: row.slug,
|
||||
title: row.title,
|
||||
excerpt: row.excerpt || ''
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사이트 설정 조회
|
||||
* @returns {Promise<Object>} 사이트 설정
|
||||
|
||||
Reference in New Issue
Block a user