궤도
[백엔드] Node.js + Sequelize + MySQL 검색 기능을 만들어보자 본문
교재 검색 기능을 만들 것이다.
우리 앱에서 검색창을 열람할 수 있는 방법이 대충 5개?정도 되는데 다행히 그 경로 전부 같은 검색창을 공유한다.
api를 1개만 만들어도 된다는 뜻이다. 다행이다.
뭐 이렇게 검색어를 입력하면 제목에 해당 검색어가 포함된 교재들을 불러올 것이다.
그 교재들에 대해 내 문제집 또는 학원 문제집으로 추가할 수 있게도 해야한다.
각 기능에 대한 자세한 설명은 아래에서 하도록 하고 대충 이런 기능을 구현할 것이라는 것만 언급하고 넘어간다.
교재 검색
검색 기능을 구현할 때 신경써야 할 부분은 2가지이다.
1. 제목에 검색어가 포함된 교재를 잘 불러오기
2. 사용자가 이미 가지고 있는 책은 검색 결과에서 제외하기
1번이야 당연한 말이고...2번은 초기 기획에서 약간 수정된 부분이다.
처음에는 검색어에 해당하는 모든 교재를 불러오고, 그 중 사용자가 이미 보유하고 있는 교재라면 '추가'버튼을 비활성화 하는 것으로 기획했다. 그런데 생각해보니 굳이 그럴 필요가 있나..? 싶어서 이미 보유하고 있는 교재는 검색 결과에서 제외하기로 했다.
프론트에서는 검색을 진행하는 사용자의 stu_id와 사용자의 검색어를 넘겨줄 것이다.
나는 사용자가 입력한 검색어와 검색 결과를 반환할 것이다.
routes/search.js
router.get('/', async function (req, res, next) {
const search = req.query.title; //검색어
const input_stu_id = req.query.stu_id;
let result = await models.student.findOne({
where: {
stu_id: input_stu_id
}
});
//사용자가 보유한 책의 workbook_sn 끌올
let user_books = await models.stu_workbook.findAll({
attributes: ['workbook_sn'],
where: {
stu_sn: result.dataValues.stu_sn
}
});
var book_list = new Array(); //끌올한 workbook_sn을 book_list에 배열로 저장
for(var i in user_books){
book_list.push(user_books[i].dataValues.workbook_sn);
}
let search_result = await models.workbook.findAll({
where: {
workbook_title: { //제목에 검색어가 포함됐나?
[Op.like]: `%${search}%`
},
workbook_sn: { //사용자가 보유한 책이 아닌가?
[Op.notIn]: book_list
},
workbook_publisher: { //학평 제외
[Op.not]: 'Gyoyuk'
}
}
});
if (search_result.length != 0) { //교재 있냐?
try {
res.send({ //교재 정보 넘김
message: "Search results",
status: 'success',
data: {
search,
search_result
}
});
} catch (err) { //무언가 문제가 생김
res.send({
message: "ERROR",
status: 'fail'
})
}
}
else { //검색결과에 해당하는 교재 없거나 실패한 것임
res.send({
message: "No results or fail",
status: 'null'
});
}
});
하나하나 뜯어보자
const search = req.query.title; //검색어
const input_stu_id = req.query.stu_id;
let result = await models.student.findOne({
where: {
stu_id: input_stu_id
}
});
일단 프론트에서 넘겨 받은 값이 stu_sn이 아닌 stu_id이기 때문에 student 테이블에서 해당하는 사용자를 가져온다.
//사용자가 보유한 책의 workbook_sn 끌올
let user_books = await models.stu_workbook.findAll({
attributes: ['workbook_sn'],
where: {
stu_sn: result.dataValues.stu_sn
}
});
var book_list = new Array(); //끌올한 workbook_sn을 book_list에 배열로 저장
for(var i in user_books){
book_list.push(user_books[i].dataValues.workbook_sn);
}
받아온 stu_sn을 가지고 stu_workbook 테이블로 가서 사용자가 가지고 있는 교재의 workbook_sn을 모두 가져온다.
이대로 사용하기엔 dataValues.workbook_sn 해가면서 귀찮게 참고해야하니 그냥 book_list라는 배열을 하나 만들고 그 안에 전부 넣어준다.
let search_result = await models.workbook.findAll({
where: {
workbook_title: { //제목에 검색어가 포함됐나?
[Op.like]: `%${search}%`
},
workbook_sn: { //사용자가 보유한 책이 아닌가?
[Op.notIn]: book_list
},
workbook_publisher: { //학평 제외
[Op.not]: 'Gyoyuk'
}
}
});
여기가 핵심이다.
이전까지 사용하지 않은 3개의 sequelize 문법이 사용된다. Op.like, Op.notIn, Op.not이다.
Op.like는 MySQL의 like와 같다. `${search}%`는 해당 검색어로 시작하는 결과, `%${search}`는 해당 검색어로 끝나는 결과, `%${search}%`는 위치 상관없이 해당 검색어를 포함하는 결과이니 마지막 것을 사용한다.
Op.notIn은 특정 배열에 대해 해당 배열의 원소를 포함하지 않은 결과를 반환하게 한다. 반대로 특정 배열의 원소를 포함한 결과를 얻고 싶다면 Op.in을 사용하면 된다. 아까 사용자가 보유한 책의 workbook_sn을 book_list 배열에 넣었으니 not.In 조건에 해당 배열을 넣어주자.
Op.not은 Op.notIn과 달리 배열이 아니라 특정 데이터 하나에 대한 not 계산을 한다. 검색결과에서 교육청 모의고사를 제외하기 위해 사용해주자.
이 과정을 통해 얻어낸 검색 결과는 search_result에 저장한다.
if (search_result.length != 0) { //교재 있냐?
try {
res.send({ //교재 정보 넘김
message: "Search results",
status: 'success',
data: {
search,
search_result
}
});
} catch (err) { //무언가 문제가 생김
res.send({
message: "ERROR",
status: 'fail'
})
}
}
else { //검색결과에 해당하는 교재 없거나 실패한 것임
res.send({
message: "No results or fail",
status: 'null'
});
}
마지막으로 데이터를 반환하기 전에 확인해야 할 것이 있다. 일단 search_result에 값이 있는지 확인해야 한다. 사용자가 엉뚱한 검색어를 입력하거나, 해당하는 검색어의 책을 사용자가 모두 갖고 있다거나 등등 검색 결과가 없을 경우는 다양하기 때문이다. 그래서 search_result.length로 값의 존재를 확인한 뒤 결과를 반환하면 되겠다.
localhost:3001/api/search?title=수능&stu_id=samdol
검색어에 대한 검색 결과가 잘 나온다.
사용자가 보유하고 있는 교재가 잘 제외된 것인지 의문을 품을 수도 있으니 사용자의 교재 목록을 같이 첨부한다.
교재 추가
교재 추가시 신경쓸 것은 별로 없고 그냥 뭐 굳이 따지자면 1개 있다.
이 알림창이다.
메인화면 구현할 때 언급했지만, 우린 일반 교재와 학원 교재를 workbook_sn으로 구분짓고 있다. 그니까 stu_workbook에 교재를 추가할 때 추가되는 교재의 workbook_sn을 보고 이게 일반 교재인지 학원 교재인지만 알려주면 된다.
routes/search.js
router.post('/', async function (req, res, next) {
const search = req.query.title; //검색어
let body = req.body;
let isWorkbook=true;
const input_stu_id = req.query.stu_id;
let result = await models.student.findOne({
where: {
stu_id: input_stu_id
}
});
const user = result.dataValues.stu_sn;
//body로 넘겨준 workbook_sn에 해당하는 문제집 찾기. body로 workbook_sn만 넘겨주면 됨
let selected_book = await models.workbook.findOne({
where : {
workbook_sn: body.workbook_sn
}
});
//해당 책이 일반 교재인지 학원 교재인지 판단. isWorkbook이 true면 일반 교재고, 아니면 학원 교재임
if(body.workbook_sn>=1000000){
isWorkbook = false;
}
//workbook_sn, stu_sn으로 stu_workbook 테이블에 데이터 넣기
models.stu_workbook.create({
workbook_sn: selected_book.dataValues.workbook_sn,
stu_sn: user
})
.then(result => {
res.send({
message: 'Inserted in DB',
status:'success',
data:{ //result는 그냥 내가 postman에서 보려고 넣은거라 실제로 쓸 때는 빼고 isWorkbook만 가져가도 괜찮음.
result,
isWorkbook
}
})
})
.catch(err => {
res.send({
message:
err.message || "Some error occurred while insert data.",
status:'fail'
});
});
});
이것도 하나하나 뜯어보자
const input_stu_id = req.query.stu_id;
let result = await models.student.findOne({
where: {
stu_id: input_stu_id
}
});
const user = result.dataValues.stu_sn;
이건 아까도 한 stu_id를 stu_sn으로 바꾸는 과정...
//body로 넘겨준 workbook_sn에 해당하는 문제집 찾기. body로 workbook_sn만 넘겨주면 됨
let selected_book = await models.workbook.findOne({
where : {
workbook_sn: body.workbook_sn
}
});
프론트에서 workbook_sn을 넘겨줄텐데 일단 그 workbook_sn에 해당하는 문제집이 있는지 workbook 테이블에서 찾아본다. 근데 사실...굳이 지운다면야 필요없는 부분이긴 하다. 검색결과 창에서 넘어온 값인데 당연히 있는 교재겠지...
//해당 책이 일반 교재인지 학원 교재인지 판단. isWorkbook이 true면 일반 교재고, 아니면 학원 교재임
if(body.workbook_sn>=1000000){
isWorkbook = false;
}
workbook_sn이 1000000 이상이면 학원교재라는 뜻이니 그렇다면 isWorkbook을 false로 놓고, 아니라면 초기값대로 true로 들어갈 것이다.
//workbook_sn, stu_sn으로 stu_workbook 테이블에 데이터 넣기
models.stu_workbook.create({
workbook_sn: selected_book.dataValues.workbook_sn,
stu_sn: user
})
.then(result => {
res.send({
message: 'Inserted in DB',
status:'success',
data:{ //result는 그냥 내가 postman에서 보려고 넣은거라 실제로 쓸 때는 빼고 isWorkbook만 가져가도 괜찮음.
result,
isWorkbook
}
})
})
.catch(err => {
res.send({
message:
err.message || "Some error occurred while insert data.",
status:'fail'
});
});
stu_workbook 테이블에 값을 넣는 과정이다. 아까도 말했듯이 selected_book.dataValues.workbook_sn를 그냥 body.workbook_sn으로 바꾸고 selected_book 관련 코드를 지워도 무방하다. 결과와 함께 isWorkbook 변수도 넘겨주자.
localhost:3001/api/search?title=수능&stu_id=samdol
잘 들어왔다.
정말인지 확인하고 싶을 수도 있으니까...