유명현

Merge branch 'feature/line-bot-v2' into 'main'

Feature/line bot v2

## [V2]line bot
- 3사 중고매장 검색 기능, 통합 검색 기능 구현
- Refactor: app.js로 부터 line bot 관련 코드 분리
- Clean up some unessential code

See merge request !18
const express = require("express");
const line = require("@line/bot-sdk");
const setFlexMessage = require("./apis/setFlexMessage");
const fs = require("fs");
const { sequelize } = require('./models')
const { handleEvent } = require("./chatbot/index");
const { sequelize } = require("./models");
const database = require("./apis/database");
// Initialize DB connection
sequelize.sync({ force: false })
sequelize
.sync({ force: false })
.then(() => {
console.log('database connection complete');
database.addKeyword("rtx3060", "junseok")
database.getKeywordsByUserId("junseok")
database.deleteKeyword("phobyjun", "rtx3080")
database.getAllUsers()
database.getUsersByKeyword("rtx3060")
database.getAllKeywords()
console.log("database connection complete");
database.addKeyword("rtx3060", "junseok");
database.getKeywordsByUserId("junseok");
database.deleteKeyword("phobyjun", "rtx3080");
database.getAllUsers();
database.getUsersByKeyword("rtx3060");
database.getAllKeywords();
})
.catch((err) => {
console.log('database connection failed');
console.log("database connection failed");
});
// Load .env configuration
......@@ -33,144 +33,11 @@ const app = express();
// Create post request handler for chatbot
app.post("/webhook", line.middleware(config), (req, res) => {
Promise.all(req.body.events.map(handleEvent)).then((result) =>
res.json(result)
);
Promise.all(req.body.events.map(handleEvent)).then((result) => {
res.json(result);
});
});
const client = new line.Client(config);
let waitNewMamulList = []; // 매물 키워드 입력 기다리는 목록
function handleEvent(event) {
if (event.type !== "message" || event.message.type !== "text") {
console.log(event);
if (event.type == "postback") {
if (event.postback.data == "new") {
var found = waitNewMamulList.indexOf(event.source.userId);
if (found == -1) {
waitNewMamulList.push(event.source.userId);
console.log(waitNewMamulList);
return Promise.resolve(
client.replyMessage(event.replyToken, {
type: "text",
text: "등록할 매물 키워드를 알려주세요!",
})
);
} else {
return Promise.resolve(
client.replyMessage(event.replyToken, {
type: "text",
text: "등록할 매물 키워드를 알려주세요!",
})
);
}
} else if (event.postback.data == "check") {
return Promise.resolve(
client.replyMessage(event.replyToken, {
type: "flex",
altText: "등록된 매물",
contents: setFlexMessage(
"daangn",
"RTX 3080",
"1000000",
"https://dnvefa72aowie.cloudfront.net/origin/article/202205/94cdd237258671d5806a70f64ab2b3c7dcd790da0384b394ef5809fe10c08ced.webp?q=95&s=1440x1440&t=inside",
"https://www.daangn.com/articles/403755360"
),
})
);
}
}
return Promise.resolve(null);
} else {
console.log(event);
var found = waitNewMamulList.indexOf(event.source.userId);
if (found == -1) {
return Promise.resolve(
client.replyMessage(event.replyToken, {
type: "text",
text: "왼쪽 하단 메뉴버튼(☰)을 클릭해 상호작용 해주세요!",
})
);
} else {
// TODO: 서버에 키워드 등록하는 api
waitNewMamulList.splice(found, 1);
console.log(waitNewMamulList[found]);
return Promise.resolve(
client.replyMessage(event.replyToken, {
type: "text",
text: "매물이 등록되었습니다!\n등록된 매물: " + event.message.text,
})
);
}
}
}
const port = 1231;
app.listen(port);
console.log(`listening...\nport : ${port}`);
/*Push Message*/
// client.pushMessage(event.source.userId, {
// type: "flex",
// altText: "새로운 매물이 왔어요!",
// contents: setFlexMessage(
// "daangn",
// "RTX 3080",
// "1000000",
// "https://dnvefa72aowie.cloudfront.net/origin/article/202205/94cdd237258671d5806a70f64ab2b3c7dcd790da0384b394ef5809fe10c08ced.webp?q=95&s=1440x1440&t=inside",
// "https://www.daangn.com/articles/403755360"
// ),
// })
/*리치메뉴 설정*/
// let richMenu = {
// size: {
// width: 2500,
// height: 843,
// },
// selected: false,
// name: "Nice richmenu",
// chatBarText: "Tap to open",
// areas: [
// {
// bounds: {
// x: 0,
// y: 0,
// width: 1250,
// height: 843,
// },
// action: {
// type: "postback",
// label: "new",
// data: "new",
// displayText: "키워드 등록",
// inputOption: "openKeyboard",
// fillInText: "",
// },
// },
// {
// bounds: {
// x: 1250,
// y: 0,
// width: 1250,
// height: 843,
// },
// action: {
// type: "postback",
// label: "check",
// data: "check",
// displayText: "최신 매물 확인",
// inputOption: "openKeyboard",
// fillInText: "",
// },
// },
// ],
// };
//// 등록
// client.createRichMenu(richMenu).then((richMenuId) => console.log(richMenuId));
// client.setRichMenuImage(
// "richmenu-183eff606f059b8244f0a625b54bddf1",
// fs.createReadStream("./static/img/richMenu.jpg")
// );
// client.setDefaultRichMenu("richmenu-183eff606f059b8244f0a625b54bddf1");
......
const line = require("@line/bot-sdk");
const setFlexMessage = require("./message/setFlexMessage");
const setCarouselMessage = require("./message/setCarouselMessage");
const fs = require("fs");
const { daangnSingleSearch } = require("./search/daangnSearch");
const { daangnMultiSearch } = require("./search/daangnSearch");
const { joongnaSingleSearch } = require("./search/joongnaSearch");
const { joongnaMultiSearch } = require("./search/joongnaSearch");
const { bunjangSingleSearch } = require("./search/bunjangSearch");
const { bunjangMultiSearch } = require("./search/bunjangSearch");
const { marketMultiSearch } = require("./search/marketSearch");
require("dotenv").config({ path: __dirname + "/../.env" });
const config = {
channelAccessToken: process.env.channelAccessToken,
channelSecret: process.env.channelSecret,
};
const client = new line.Client(config);
let waitNewMamulList = []; // 매물 키워드 입력 기다리는 목록
function handleEvent(event) {
if (event.type !== "message" || event.message.type !== "text") {
console.log(event);
if (event.type == "postback") {
if (event.postback.data == "new") {
var found = waitNewMamulList.indexOf(event.source.userId);
if (found == -1) {
waitNewMamulList.push(event.source.userId);
console.log(waitNewMamulList);
return Promise.resolve(
client.replyMessage(event.replyToken, {
type: "text",
text: "등록할 매물 키워드를 알려주세요!",
})
);
} else {
return Promise.resolve(
client.replyMessage(event.replyToken, {
type: "text",
text: "등록할 매물 키워드를 알려주세요!",
})
);
}
} else if (event.postback.data == "check") {
return Promise.resolve(
client.replyMessage(event.replyToken, {
type: "flex",
altText: "등록된 매물",
contents: setFlexMessage(
"daangn",
"RTX 3080",
"1000000",
"https://dnvefa72aowie.cloudfront.net/origin/article/202205/94cdd237258671d5806a70f64ab2b3c7dcd790da0384b394ef5809fe10c08ced.webp?q=95&s=1440x1440&t=inside",
"https://www.daangn.com/articles/403755360",
"test설명"
),
})
);
}
}
return Promise.resolve(null);
} else {
console.log(event);
var found = waitNewMamulList.indexOf(event.source.userId);
if (found == -1) {
return Promise.resolve(
marketMultiSearch(event.message.text).then((res) => {
client.pushMessage(event.source.userId, setCarouselMessage(res));
})
);
} else {
// TODO: 서버에 키워드 등록하는 api
waitNewMamulList.splice(found, 1);
console.log(waitNewMamulList[found]);
return Promise.resolve(
client.replyMessage(event.replyToken, {
type: "text",
text: "매물이 등록되었습니다!\n등록된 매물: " + event.message.text,
})
);
}
}
}
module.exports = { handleEvent, config };
/*Reply Message*/
// client.replyMessage(event.replyToken, {
// type: "text",
// text: "왼쪽 하단 메뉴버튼(☰)을 클릭해 상호작용 해주세요!",
// })
/*Push Message*/
// client.pushMessage(event.source.userId, {
// type: "flex",
// altText: "새로운 매물이 왔어요!",
// contents: setFlexMessage(
// "daangn",
// "RTX 3080",
// "1000000",
// "https://dnvefa72aowie.cloudfront.net/origin/article/202205/94cdd237258671d5806a70f64ab2b3c7dcd790da0384b394ef5809fe10c08ced.webp?q=95&s=1440x1440&t=inside",
// "https://www.daangn.com/articles/403755360"
// ),
// })
/*Carousel Message (flex message 여러개)*/
// client.pushMessage(event.source.userId, {
// type: "carousel",
// contents: [
// setFlexMessage(
// "daangn",
// "RTX 3080",
// "1220000",
// "https://dnvefa72aowie.cloudfront.net/origin/article/202205/a6ed5583ba1c0b206c264a96afcf5c736a1f055ad899c14d8087d0f7cd9e4805.webp?q=95&s=1440x1440&t=inside",
// "https://www.daangn.com/articles/408099984"
// ),
// setFlexMessage(
// "joongna",
// "RTX 2080",
// "1000000",
// "https://cafeptthumb-phinf.pstatic.net/MjAyMjA1MjRfMTM1/MDAxNjUzMzY4MjU1MDUx.D2xeHlHLzhF3BhMD83GAMN7dAiu7YcArtpwL1AVnPR0g.dRILQe9D5XVBPbAtNKimAmYwgG1CKcr-rnSx3CeyFQIg.JPEG/%EA%B0%A4%EB%9F%AD%EC%8B%9C_GTX1060_3G.jpg?type=s3",
// "https://cafe.naver.com/joonggonara/918947018"
// ),
// setFlexMessage(
// "bunjang",
// "RTX 3080",
// "1059800",
// "https://media.bunjang.co.kr/product/179119900_1_1652919446_w856.jpg",
// "https://m.bunjang.co.kr/products/179119900"
// ),
// ],
// });
/*리치메뉴 설정*/
// let richMenu = {
// size: {
// width: 2500,
// height: 843,
// },
// selected: false,
// name: "Nice richmenu",
// chatBarText: "Tap to open",
// areas: [
// {
// bounds: {
// x: 0,
// y: 0,
// width: 1250,
// height: 843,
// },
// action: {
// type: "postback",
// label: "new",
// data: "new",
// displayText: "키워드 등록",
// inputOption: "openKeyboard",
// fillInText: "",
// },
// },
// {
// bounds: {
// x: 1250,
// y: 0,
// width: 1250,
// height: 843,
// },
// action: {
// type: "postback",
// label: "check",
// data: "check",
// displayText: "최신 매물 확인",
// inputOption: "openKeyboard",
// fillInText: "",
// },
// },
// ],
// };
//// 등록
// client.createRichMenu(richMenu).then((richMenuId) => console.log(richMenuId));
// client.setRichMenuImage(
// "richmenu-183eff606f059b8244f0a625b54bddf1",
// fs.createReadStream("./static/img/richMenu.jpg")
// );
// client.setDefaultRichMenu("richmenu-183eff606f059b8244f0a625b54bddf1");
const setFlexMessage = require("./setFlexMessage");
function setCarouselMessage(mamuls) {
let flexMessages = [];
let flexMessage = {};
if (
mamuls[0] == undefined &&
mamuls[1] == undefined &&
mamuls[2] == undefined
) {
let nonMamulMessage = {
type: "flex",
altText: "매물 검색 에러",
contents: setFlexMessage(
"-",
"매물이 없습니다!",
"0",
"https://upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Error.svg/515px-Error.svg.png",
"https://upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Error.svg/515px-Error.svg.png",
"-"
),
};
return nonMamulMessage;
}
for (i = 0; i < mamuls.length; i++) {
if (mamuls[i] == undefined) {
continue;
}
try {
if (
mamuls[i]["platform"] === "bunjang" ||
mamuls[i]["platform"] === "번개장터"
) {
mamuls[i]["thumbnailUrl"] = mamuls[i]["thumbnailUrl"].replace(
"{",
"%7B"
);
mamuls[i]["thumbnailUrl"] = mamuls[i]["thumbnailUrl"].replace(
"}",
"%7D"
);
}
if (
mamuls[i]["thumbnailUrl"] == undefined ||
mamuls[i]["thumbnailUrl"] == ""
) {
mamuls[i]["thumbnailUrl"] =
"https://upload.wikimedia.org/wikipedia/commons/5/5f/Grey.PNG";
}
if (mamuls[i]["extraInfo"] == undefined || mamuls[i]["extraInfo"] == "") {
mamuls[i]["extraInfo"] = "없음";
} else if (mamuls[i]["extraInfo"].length > 150) {
mamuls[i]["extraInfo"] = mamuls[i]["extraInfo"].slice(0, 150) + "\n...";
}
flexMessage = setFlexMessage(
mamuls[i]["platform"],
mamuls[i]["name"],
mamuls[i]["price"],
mamuls[i]["thumbnailUrl"],
mamuls[i]["itemUrl"],
mamuls[i]["extraInfo"]
);
flexMessages.push(flexMessage);
} catch (err) {
console.log(err);
continue;
}
}
let carouselMessage = {
type: "flex",
altText: "Carousel mamul message",
contents: {
type: "carousel",
contents: flexMessages,
},
};
return carouselMessage;
}
module.exports = setCarouselMessage;
......@@ -2,14 +2,21 @@ function priceToString(price) {
return price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
function setFlexMessage(platform, name, price, thumbnailUrl, itemUrl) {
function setFlexMessage(
platform,
name,
price,
thumbnailUrl,
itemUrl,
extraInfo
) {
let koreanPlatformName = "";
if (platform === "daangn") {
koreanPlatformName = "당근";
} else if (platform === "joongna") {
if (platform === "daangn" || platform === "당근마켓") {
koreanPlatformName = "당근마켓";
} else if (platform === "joongna" || platform === "중고나라") {
koreanPlatformName = "중고나라";
} else if (platform === "bunjang") {
koreanPlatformName = "번개나라";
} else if (platform === "bunjang" || platform === "번개장터") {
koreanPlatformName = "번개장터";
} else {
koreanPlatformName = "Unknown";
}
......@@ -127,6 +134,28 @@ function setFlexMessage(platform, name, price, thumbnailUrl, itemUrl) {
},
],
},
{
type: "box",
layout: "baseline",
spacing: "sm",
contents: [
{
type: "text",
text: "정보",
color: "#aaaaaa",
size: "sm",
flex: 1,
},
{
type: "text",
text: extraInfo,
wrap: true,
color: "#666666",
size: "sm",
flex: 5,
},
],
},
],
},
],
......
const axios = require("axios").default;
const bunjangSingleSearch = (keyword) => {
return Promise.resolve(
axios
.get(
`http://43.200.35.46:18082/api/v2/bunjang/${encodeURIComponent(
keyword
)}`
)
.then((res) => res.data[0])
.catch((e) => undefined)
);
};
const bunjangMultiSearch = (keyword) => {
return Promise.resolve(
axios
.get(
`http://43.200.35.46:18082/api/v2/bunjang/${encodeURIComponent(
keyword
)}`
)
.then((res) => res.data)
.catch((e) => undefined)
);
};
module.exports = { bunjangSingleSearch, bunjangMultiSearch };
const axios = require("axios").default;
const daangnSingleSearch = (keyword) => {
return Promise.resolve(
axios
.get(
`http://43.200.35.46:18080/api/v2/daangn/${encodeURIComponent(keyword)}`
)
.then((res) => res.data["items"][0])
.catch((e) => undefined)
);
};
const daangnMultiSearch = (keyword) => {
return Promise.resolve(
axios
.get(
`http://43.200.35.46:18080/api/v2/daangn/${encodeURIComponent(keyword)}`
)
.then((res) => res.data["items"])
.catch((e) => undefined)
);
};
module.exports = { daangnSingleSearch, daangnMultiSearch };
const axios = require("axios").default;
const joongnaSingleSearch = (keyword) => {
return Promise.resolve(
axios
.get(
`http://43.200.35.46:18081/api/v2/joongna/${encodeURIComponent(
keyword
)}`
)
.then((res) => res.data[0])
.catch((e) => undefined)
);
};
const joongnaMultiSearch = (keyword) => {
return Promise.resolve(
axios
.get(
`http://43.200.35.46:18081/api/v2/joongna/${encodeURIComponent(
keyword
)}`
)
.then((res) => res.data)
.catch((e) => undefined)
);
};
module.exports = { joongnaSingleSearch, joongnaMultiSearch };
const { daangnSingleSearch } = require("./daangnSearch");
const { bunjangSingleSearch } = require("./bunjangSearch");
const { joongnaSingleSearch } = require("./joongnaSearch");
const setCarouselMessage = require("../message/setCarouselMessage");
const marketMultiSearch = (keyword) => {
const result = [];
return new Promise((resolve, reject) => {
daangnSingleSearch(keyword).then((res) => {
result.push(res);
bunjangSingleSearch(keyword).then((res) => {
result.push(res);
joongnaSingleSearch(keyword).then((res) => {
result.push(res);
resolve(result);
});
});
});
});
};
module.exports = { marketMultiSearch };
......@@ -10,6 +10,7 @@
"license": "MIT",
"dependencies": {
"@line/bot-sdk": "^7.5.0",
"axios": "^0.27.2",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"mysql2": "^2.3.3",
......@@ -33,6 +34,14 @@
"node": ">=10"
}
},
"node_modules/@line/bot-sdk/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/@sindresorhus/is": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
......@@ -167,11 +176,25 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"dependencies": {
"follow-redirects": "^1.14.0"
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/balanced-match": {
......@@ -2259,6 +2282,16 @@
"body-parser": "^1.20.0",
"file-type": "^15.0.0",
"form-data": "^3.0.0"
},
"dependencies": {
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
}
}
},
"@sindresorhus/is": {
......@@ -2374,11 +2407,24 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"requires": {
"follow-redirects": "^1.14.0"
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
},
"dependencies": {
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"balanced-match": {
......
......@@ -16,6 +16,7 @@
"license": "MIT",
"dependencies": {
"@line/bot-sdk": "^7.5.0",
"axios": "^0.27.2",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"mysql2": "^2.3.3",
......