따꿍의 프로젝트
[2025.09.06] 검색 기능 구현 본문

더미데이터
INSERT INTO Book
(title, genre, author, publisher, publish_date, summary, length, ISBN, bookmark_num)
VALUES
-- 제목 맨 앞 "과학..." → relevance 3
('과학의 역사', '과학', '김철수', '민음사', '2001-01-01', '과학 발전사', 300, '1111111111', 5),
('과학기술의 미래', '과학', '이영희', '사이언스북스', '2002-01-01', '과학과 기술의 전망', 280, '2222222222', 10),
('과학적 사고의 힘', '과학', '박민수', '까치', '2003-01-01', '과학적 사고법을 설명', 260, '3333333333', 8),
('과학과 종교', '과학', '최지훈', '문학사상', '2004-01-01', '과학과 종교의 대화', 240, '4444444444', 12),
('과학 혁명의 구조', '과학', '토머스 쿤', '사이언스북스', '2005-01-01', '패러다임 전환 설명', 400, '5555555555', 20),
-- 제목 중간 "...과학..." → relevance 2
('커피 한 잔의 과학', '취미', '박철수', '북하우스', '2020-11-11', '커피 맛을 과학적으로 설명', 220, '6666666666', 180),
('시간과 과학 이야기', '과학', '이영희', '사이언스북스', '2005-01-01', '시간과 과학을 다룬 책', 280, '7777777777', 10),
('음악 속의 과학', '예술', '홍길동', '음악출판사', '2015-05-05', '음악 현상을 과학적으로 설명', 310, '8888888888', 50),
('요리와 과학', '취미', '김지영', '푸드북스', '2018-09-09', '요리를 과학적으로 설명', 200, '9999999999', 70),
('영화 속의 과학', '예술', '최수현', '필름북스', '2017-03-03', '영화 장면 속 과학적 해석', 330, '1010101010', 90),
('자연 속의 과학', '과학', '박영호', '자연사', '2010-10-10', '자연현상을 과학적으로 설명', 280, '1111111112', 60),
('스포츠와 과학', '체육', '이준호', '스포츠북', '2012-12-12', '운동을 과학적으로 접근', 250, '1212121212', 40),
('패션과 과학', '예술', '김현정', '패션북', '2014-08-08', '패션에 숨어있는 과학 원리', 210, '1313131313', 30),
('심리학과 과학', '심리', '정유진', '심리북스', '2016-06-06', '심리학을 과학적으로 해석', 290, '1414141414', 25),
('문학 속의 과학', '문학', '오세훈', '문학사', '2011-11-11', '문학과 과학의 융합', 350, '1515151515', 15),
-- 저자명에 "과학" → relevance 1
('우주의 신비', '과학', '박과학', '까치', '2010-01-01', '우주와 과학적 발견', 400, '1616161616', 7),
('인류의 진화', '역사', '홍과학', '역사출판사', '2012-01-01', '인류 진화사', 370, '1717171717', 5),
('생명의 기원', '과학', '이과학', '생명사', '2013-03-03', '생명의 기원 설명', 390, '1818181818', 9),
('기후 변화', '과학', '최과학', '환경사', '2014-04-04', '지구 온난화와 기후변화', 320, '1919191919', 11),
('뇌와 마음', '과학', '정과학', '심리북스', '2015-05-05', '뇌 과학 연구', 280, '2020202020', 6),
('인공지능의 현재', '과학', 'AI과학', '테크북', '2018-08-08', 'AI 발전', 310, '2121212121', 14),
('바이러스의 비밀', '과학', '장과학', '메디북', '2019-09-09', '바이러스 연구', 260, '2222222222', 22),
('미래의 에너지', '과학', '신과학', '에너지북', '2020-10-10', '에너지 혁신', 330, '2323232323', 18),
('인터넷의 원리', '과학', '인터과학', 'IT북', '2021-11-11', '인터넷 기술', 300, '2424242424', 13),
('유전자와 삶', '과학', '유과학', '생명사', '2022-12-12', '유전자 과학', 350, '2525252525', 19);
controllers>search.ts
import { Request, Response, NextFunction } from 'express';
import { Op, Sequelize } from 'sequelize';
import Book from '../models/book';
import sequelize from '../sequelize';
export const searchBooks = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const q = (req.query.q as string | undefined)?.trim();
const page = Math.max(1, Number(req.query.page ?? 1));
const limit = 20;
const offset = (page - 1) * limit;
if (!q) {
return res.status(400).json({
success: false,
message: '검색어가 필요합니다.',
});
}
const likeAny = `%${q}%`;
const likePrefix = `${q}%`;
const relevanceLiteral = sequelize.literal(`
CASE
WHEN title LIKE ${sequelize.escape(likePrefix)} THEN 3
WHEN title LIKE ${sequelize.escape(likeAny)} THEN 2
WHEN author LIKE ${sequelize.escape(likeAny)} THEN 1
ELSE 0
END
`);
const { rows, count } = await Book.findAndCountAll({
where: {
[Op.or]: [
{ title: { [Op.like]: likeAny } },
{ author: { [Op.like]: likeAny } },
],
},
attributes: {
include: [[relevanceLiteral, 'relevance']],
},
order: [
[Sequelize.literal('relevance'), 'DESC'], //정확도 우선
['bookmark_num', 'DESC'], //동점이면 인기순
['book_id', 'DESC'], //또 동점이면 book_id 역순
],
limit,
offset,
});
const hasNext = offset + rows.length < count;
const nextPage = hasNext ? page + 1 : null;
return res.status(200).json({
success: true,
message: '도서 검색 결과입니다.',
data: {
items: rows,
pagination: {
total: count,
page,
limit,
hasNext,
nextPage,
nextUrl: hasNext
? `/search/books?q=${encodeURIComponent(q)}&page=${nextPage}`
: null,
},
},
});
} catch (err) {
next(err);
}
};
- Sets pagination limit: max 20 books per page.
- offset is how many rows to skip (e.g. if page=2, skip first 20).
- likeAny and likePrefix is creating SQL LIKE patterns for searching.
- likeAny = %q% → matches anywhere in the string.
- likePrefix = q% → matches only if title starts with the query.



http://localhost:3000/search/books?q=과학
을 했는데
{
"success": true,
"message": "도서 검색 결과입니다.",
"data": {
"items": [
{
"book_id": 10,
"title": "과학 혁명의 구조",
"image_url": null,
"genre": "과학",
"author": "토머스 쿤",
"translator": null,
"publisher": "사이언스북스",
"publish_date": "2005-01-01T00:00:00.000Z",
"summary": "패러다임 전환 설명",
"length": 400,
"ISBN": "5555555555",
"bookmark_num": 20,
"pdf_url": null,
"relevance": 3
},
{
"book_id": 9,
"title": "과학과 종교",
"image_url": null,
"genre": "과학",
"author": "최지훈",
"translator": null,
"publisher": "문학사상",
"publish_date": "2004-01-01T00:00:00.000Z",
"summary": "과학과 종교의 대화",
"length": 240,
"ISBN": "4444444444",
"bookmark_num": 12,
"pdf_url": null,
"relevance": 3
},
{
"book_id": 7,
"title": "과학기술의 미래",
"image_url": null,
"genre": "과학",
"author": "이영희",
"translator": null,
"publisher": "사이언스북스",
"publish_date": "2002-01-01T00:00:00.000Z",
"summary": "과학과 기술의 전망",
"length": 280,
"ISBN": "2222222222",
"bookmark_num": 10,
"pdf_url": null,
"relevance": 3
},
{
"book_id": 8,
"title": "과학적 사고의 힘",
"image_url": null,
"genre": "과학",
"author": "박민수",
"translator": null,
"publisher": "까치",
"publish_date": "2003-01-01T00:00:00.000Z",
"summary": "과학적 사고법을 설명",
"length": 260,
"ISBN": "3333333333",
"bookmark_num": 8,
"pdf_url": null,
"relevance": 3
},
{
"book_id": 6,
"title": "과학의 역사",
"image_url": null,
"genre": "과학",
"author": "김철수",
"translator": null,
"publisher": "민음사",
"publish_date": "2001-01-01T00:00:00.000Z",
"summary": "과학 발전사",
"length": 300,
"ISBN": "1111111111",
"bookmark_num": 5,
"pdf_url": null,
"relevance": 3
},
{
"book_id": 11,
"title": "커피 한 잔의 과학",
"image_url": null,
"genre": "취미",
"author": "박철수",
"translator": null,
"publisher": "북하우스",
"publish_date": "2020-11-11T00:00:00.000Z",
"summary": "커피 맛을 과학적으로 설명",
"length": 220,
"ISBN": "6666666666",
"bookmark_num": 180,
"pdf_url": null,
"relevance": 2
},
{
"book_id": 15,
"title": "영화 속의 과학",
"image_url": null,
"genre": "예술",
"author": "최수현",
"translator": null,
"publisher": "필름북스",
"publish_date": "2017-03-03T00:00:00.000Z",
"summary": "영화 장면 속 과학적 해석",
"length": 330,
"ISBN": "1010101010",
"bookmark_num": 90,
"pdf_url": null,
"relevance": 2
},
{
"book_id": 14,
"title": "요리와 과학",
"image_url": null,
"genre": "취미",
"author": "김지영",
"translator": null,
"publisher": "푸드북스",
"publish_date": "2018-09-09T00:00:00.000Z",
"summary": "요리를 과학적으로 설명",
"length": 200,
"ISBN": "9999999999",
"bookmark_num": 70,
"pdf_url": null,
"relevance": 2
},
{
"book_id": 16,
"title": "자연 속의 과학",
"image_url": null,
"genre": "과학",
"author": "박영호",
"translator": null,
"publisher": "자연사",
"publish_date": "2010-10-10T00:00:00.000Z",
"summary": "자연현상을 과학적으로 설명",
"length": 280,
"ISBN": "1111111112",
"bookmark_num": 60,
"pdf_url": null,
"relevance": 2
},
{
"book_id": 13,
"title": "음악 속의 과학",
"image_url": null,
"genre": "예술",
"author": "홍길동",
"translator": null,
"publisher": "음악출판사",
"publish_date": "2015-05-05T00:00:00.000Z",
"summary": "음악 현상을 과학적으로 설명",
"length": 310,
"ISBN": "8888888888",
"bookmark_num": 50,
"pdf_url": null,
"relevance": 2
},
{
"book_id": 17,
"title": "스포츠와 과학",
"image_url": null,
"genre": "체육",
"author": "이준호",
"translator": null,
"publisher": "스포츠북",
"publish_date": "2012-12-12T00:00:00.000Z",
"summary": "운동을 과학적으로 접근",
"length": 250,
"ISBN": "1212121212",
"bookmark_num": 40,
"pdf_url": null,
"relevance": 2
},
{
"book_id": 18,
"title": "패션과 과학",
"image_url": null,
"genre": "예술",
"author": "김현정",
"translator": null,
"publisher": "패션북",
"publish_date": "2014-08-08T00:00:00.000Z",
"summary": "패션에 숨어있는 과학 원리",
"length": 210,
"ISBN": "1313131313",
"bookmark_num": 30,
"pdf_url": null,
"relevance": 2
},
{
"book_id": 19,
"title": "심리학과 과학",
"image_url": null,
"genre": "심리",
"author": "정유진",
"translator": null,
"publisher": "심리북스",
"publish_date": "2016-06-06T00:00:00.000Z",
"summary": "심리학을 과학적으로 해석",
"length": 290,
"ISBN": "1414141414",
"bookmark_num": 25,
"pdf_url": null,
"relevance": 2
},
{
"book_id": 20,
"title": "문학 속의 과학",
"image_url": null,
"genre": "문학",
"author": "오세훈",
"translator": null,
"publisher": "문학사",
"publish_date": "2011-11-11T00:00:00.000Z",
"summary": "문학과 과학의 융합",
"length": 350,
"ISBN": "1515151515",
"bookmark_num": 15,
"pdf_url": null,
"relevance": 2
},
{
"book_id": 12,
"title": "시간과 과학 이야기",
"image_url": null,
"genre": "과학",
"author": "이영희",
"translator": null,
"publisher": "사이언스북스",
"publish_date": "2005-01-01T00:00:00.000Z",
"summary": "시간과 과학을 다룬 책",
"length": 280,
"ISBN": "7777777777",
"bookmark_num": 10,
"pdf_url": null,
"relevance": 2
},
{
"book_id": 27,
"title": "바이러스의 비밀",
"image_url": null,
"genre": "과학",
"author": "장과학",
"translator": null,
"publisher": "메디북",
"publish_date": "2019-09-09T00:00:00.000Z",
"summary": "바이러스 연구",
"length": 260,
"ISBN": "2222222222",
"bookmark_num": 22,
"pdf_url": null,
"relevance": 1
},
{
"book_id": 30,
"title": "유전자와 삶",
"image_url": null,
"genre": "과학",
"author": "유과학",
"translator": null,
"publisher": "생명사",
"publish_date": "2022-12-12T00:00:00.000Z",
"summary": "유전자 과학",
"length": 350,
"ISBN": "2525252525",
"bookmark_num": 19,
"pdf_url": null,
"relevance": 1
},
{
"book_id": 28,
"title": "미래의 에너지",
"image_url": null,
"genre": "과학",
"author": "신과학",
"translator": null,
"publisher": "에너지북",
"publish_date": "2020-10-10T00:00:00.000Z",
"summary": "에너지 혁신",
"length": 330,
"ISBN": "2323232323",
"bookmark_num": 18,
"pdf_url": null,
"relevance": 1
},
{
"book_id": 26,
"title": "인공지능의 현재",
"image_url": null,
"genre": "과학",
"author": "AI과학",
"translator": null,
"publisher": "테크북",
"publish_date": "2018-08-08T00:00:00.000Z",
"summary": "AI 발전",
"length": 310,
"ISBN": "2121212121",
"bookmark_num": 14,
"pdf_url": null,
"relevance": 1
},
{
"book_id": 29,
"title": "인터넷의 원리",
"image_url": null,
"genre": "과학",
"author": "인터과학",
"translator": null,
"publisher": "IT북",
"publish_date": "2021-11-11T00:00:00.000Z",
"summary": "인터넷 기술",
"length": 300,
"ISBN": "2424242424",
"bookmark_num": 13,
"pdf_url": null,
"relevance": 1
}
],
"pagination": {
"total": 25,
"page": 1,
"limit": 20,
"hasNext": true,
"nextPage": 2,
"nextUrl": "/search/books?q=%EA%B3%BC%ED%95%99&page=2"
}
}
}
아주 잘 돌아가는 것을 확인했다.
'웹프로젝트 > 졸작' 카테고리의 다른 글
| [2025.10.13] pdf에서 텍스트 추출 (0) | 2025.10.13 |
|---|---|
| [2025.09.06] 모달 코딩하기 (0) | 2025.09.06 |
| [2025.08.29] 바뀐 DB에 따라 기존 코드 수정 (0) | 2025.08.29 |
| [20205.08.25] UserBookProgress의 is_bookmarked column 삭제 후 UserBookBookmark 생성 (0) | 2025.08.29 |
| [2025.08.03] auth 이메일 인증, 이메일찾기, 아이디 찾기, 임시 비밀번호 전송, 비밀번호 재설정 기능 PR 확인 (0) | 2025.08.03 |
