GitHub
https://github.com/bsorryman/recipe-bag
개요
기존에는 '레시피 상세 보기 기능'을 위해, DB에 바이너리 타입으로 들어있는 파일을 압축 해제(Decompress)하여 텍스트 파일을 보여주도록 했다. 해당 기능을 수정하고 마크업 디자인 적용 및 검색어 Highlight 기능을 구현한다.
JavaScript 코드
selectRecipeByIdAndKeyword(keyword, id) {
if (this.db == null)
return '';
let query = '';
if (keyword==null || keyword=='') {
query = `
SELECT id, title, ingredients, instructions, image_name, image_file
FROM tb_recipe
WHERE id = ${id}
`;
} else {
query = `
SELECT a.id, b.title as title_no_mark, a.title, a.ingredients, a.instructions, b.image_name, b.image_file
FROM (
SELECT id,
highlight(tb_fts_recipe, 0, '<mark>', '</mark>') AS title,
highlight(tb_fts_recipe, 1, '<mark>', '</mark>') AS ingredients,
highlight(tb_fts_recipe, 2, '<mark>', '</mark>') AS instructions
FROM tb_fts_recipe
WHERE tb_fts_recipe MATCH '${keyword}'
AND id = ${id}
) a INNER JOIN (
SELECT id, title, image_name, image_file
FROM tb_recipe
WHERE id = ${id}
) b ON a.id = b.id;
`;
}
const result = this.db.prepare(query).all();
return result;
}
rcp_sqlite_db.js -> selectRecipeByIdAndKeyword(keyword, id)
기존에는 압축 파일 안의 텍스트 파일만 사용했지만, Highlight 기능 및 변경된 디자인을 위해 DB 내 데이터를 직접 가져와야한다. 따라서 SQL 쿼리문 함수를 새로 작성했다.
검색이 아닌 'All Recipe' 기능으로 전체 레시피 목록을 조회한 경우에는, 검색어 Highlight가 필요 없으므로 해당 경우에서 필요한 쿼리문을 분기하여 사용한다. (keyword = null)
검색 결과 목록에서 상세보기 기능을 수행할 경우에는 fts 테이블의 highlight 함수를 활용한다.
highlight(table, column_number, front_tag, front_tag, end_tag)
기본 형식은 위와 같다.
table: 테이블 이름을 넣는다
column_number: Where 조건에 걸린 검색어를 강조하고 싶은 컬럼의 Index를 넣는다. (첫번째 컬럼일 경우 0)
front_tag, end_tag: 강조하는 검색어의 앞 뒤로 넣고 싶은 문자열을 넣는다. (웹의 경우 HTML tag로 활용가능)
fts 테이블에서 highlight 기능을 이용해 조회하고, title, image 등을 함께 쓰기 위해 원본 recipe 테이블과 JOIN 한다.
(title은 팝업 창의 타이틀 바를 위해 Highlight가 없는 버전을 쓰기 위해서 추가적으로 조회한다.)
req_rcpView: (keyword, id) => ipcRenderer.send("req_rcpView", keyword, id),
resp_rcpView: (viewResult) => ipcRenderer.on("resp_rcpView", viewResult),
preload.js
renderer 단에서 사용할 ipc 함수를 preload.js에 선언한다.
addRcpView() {
ipcMain.on('req_rcpView', (event, keyword, id) => {
if (keyword != '') {
keyword = Utils.replaceSpecial(keyword);
}
let data = {
keyword: keyword,
id: id
};
this.rcpWorker.postMessageToWorker('selectRecipeByIdAndKeyword', data);
});
}
rcp_ipc.js -> addRcpView()
main 단에서 사용할 ipc 함수를 rcp_ipc.js를 통해 앱 실행 시 등록하도록 한다. renderer 단에서 req_rcpView 채널을 통해 요청을 하면, main 프로세스는 다시 worker thread로 쿼리문 호출을 요청한다.
const { parentPort, workerData } = require('worker_threads');
import RcpSqliteDB from '../rcp_sqlite_db';
const { dbPath } = workerData;
let db = null;
function dbOpen() {
try {
console.log('worker db open');
db = new RcpSqliteDB(dbPath);
db.open();
} catch (error) {
console.log('db open in worker: ' + error);
}
}
function chooseQuery(msgId, data) {
let dbResult;
let replyId;
switch (msgId) {
.
. (생략)
.
case 'selectRecipeByIdAndKeyword':
let viewKeyword = data.keyword;
let viewId = data.id;
dbResult = db.selectRecipeByIdAndKeyword(viewKeyword, viewId);
replyId = 'resp_rcpView';
break;
}
let result = {
replyId: replyId,
dbResult: dbResult
}
return result;
}
parentPort.on('message', (_workerData) => {
const { msgId, data } = _workerData;
try {
if (data.keyword == 'worker_error') {
if (process.env.NODE_ENV != 'production') {
parentPort.error(); // forced error. (test)
}
}
if (msgId == 'close') {
db.close();
parentPort.close();
return;
}
let workerResult = chooseQuery(msgId, data);
parentPort.postMessage(workerResult);
} catch (e) {
console.log('[worker] Catch Error in Worker: ');
console.log(e);
parentPort.close();
}
});
worker.js
요청받은 worker는 rcp_sqlite_db.js의 selectRecipeByIdAndKeyword(keyword, id) 함수를 통해 쿼리문을 독립적이고 빠르게 처리하고 DB 결과값을 main 단으로 전송한다.
setWorkerOnListener() {
this.workerOnListener = this.worker.on('message', (workerResult) => {
.
. (생략)
.
} else if (workerResult.replyId == 'resp_rcpView') {
let viewResult = workerResult.dbResult[0];
let imageFile = nativeImage.createFromBuffer(viewResult.image_file).toDataURL();
viewResult.image_file = imageFile;
this.showRcpView(this.modalWindow);
this.modalWindow.webContents.postMessage('resp_rcpView', viewResult);
}
this.mainWindow.webContents.postMessage(workerResult.replyId, workerResult.dbResult);
});
}
showRcpView(modalWindow) {
// Center the modalWindow in the mainWindow
const parentBounds = (modalWindow.getParentWindow()).getBounds();
const x = parentBounds.x + Math.floor((parentBounds.width - 823) / 2);
const y = parentBounds.y + Math.floor((parentBounds.height - 600) / 2);
modalWindow.setPosition(x, y);
modalWindow.setOpacity(0);
modalWindow.show();
// fade in effect
let opacity = 0;
const interval = setInterval(() => {
opacity += 0.25;
modalWindow.setOpacity(opacity);
if (opacity >= 1) {
clearInterval(interval);
}
}, 25);
return;
}
rcp_worker.js
worker의 응답을 수신하는 리스너에서 응답 ID를 식별하여, renderer 단에 새로운 modal window 팝업창을 띄우고, 팝업 창에 표시할 데이터를 전송한다.
PS.
이전의 '상세보기 기능' 로직에서는 압축 파일을 해제하고, 해제된 파일에서 텍스트 파일을 찾아 텍스트만 renderer로 전달하는 등 다소 복잡하고 쓸데 없는 방식이었다. 이는 단순히 앱 최초 실행시에 자동으로 DB를 구성하면서 바이너리 컬럼에 넣었던 압축 파일 데이터를 활용하고 싶어서였다.. 하지만 하나의 긴 String으로 이뤄져있기 때문에 디자인에도 제약이 걸리고, 활용하려해도 번거로운 파싱 작업이 끼는 문제가 있었다. 같은 기능을 두번 작업하게 되었지만, 결과적으로는 Highlight 기능도 활용할 수 있었다.
'토이 프로젝트 > 레시피 일렉트론 앱 (완)' 카테고리의 다른 글
[Electron] 17.앱 빌드 및 패키징 - 레시피 일렉트론 앱 (0) | 2024.07.06 |
---|---|
[Electron] 16.최근 검색어 저장 - 레시피 일렉트론 앱 (0) | 2024.07.05 |
[Electron] 14.디자인 마크업 적용 및 무한 스크롤 기능 추가 - 레시피 일렉트론 앱 (0) | 2024.06.26 |
[Electron] 13.멀티스레딩 구현 with worker(2) - 레시피 일렉트론 앱 (0) | 2024.02.12 |
[Electron] 12.멀티스레딩 구현 with worker(1) - 레시피 일렉트론 앱 (0) | 2024.02.11 |