아파트 온도, 습도, 전력 실시간 데이터 관리 프로젝트 마무리하며.

아파트 실시간 데이터 관리 프로젝트를 마무리하며 느꼈던점

Node.js를 기반으로한 협업을 통해? 사실 현업에 계신 분 2명과 취준생 2명이 모인 스터디라 현업 개발자 분들 께서 많은 정보와 팁을 알려주신 재능기부 현장느낌이였다. 하지마 어디서도 들을 수 없는… 그런 정보들을 얻는 기회였다고 생각한다. 물론 프로젝트를 그분들께 의존하지는 않았다. 기본적인 세팅과 아이디어를 제공해주시고 거의 2명이 만든 그런 느낌이였다.

Mqtt라는 통신기법에 대해 처음 알게되었고 어떤 기능을 위해 여러가지 고려하여 다양한 선택을 하는 생각의 폭을 넓힐 수 있는 좋은 경험으로 생각된다. 무턱대고 기능을 위해 내가 아는 것만 활용하여 물론 아는것이 많지도 않다. 하지만 틀에 갖혀서 개발하는 것보다 기능을 위해 많은 것을 생각하고 고려하면 또다른 방향으로도 더 좋은 결과를 낳을 수 있다는 것을 느끼는 프로젝트 였다.

또한 과연 내가 백엔드 개발자를 꿈꾸지만 그에 맞는 준비를 하고 있었는가 그냥 무턱대고 코드만 치며 내것이 되었다라고 생각하던 것은 아니였을까 라는 의문을 나에게 던지는 값진 시간이 되었다.

좋은 결과물을 위해서는 많은 공부와 시간을 투자할 준비가 되어있어야 한다. 새로운 것에 대한 도전 그리고 알아가기 위해 노력하는 자세 또 그것을 내 것으로 만들 준비 이러한 덕목이 앞으로 개발자가 되기 위해 내가 준비하는 과정에서 가장 중요하지 않을까 라는 생각이 들었던 프로젝트였다.

지난 프로젝트들을 정리하며 그때 내가 생각했던것을 정리하고 다시 생각을 해보며 그때 생각한 것을 나는 지키고 있는가. 모든 다짐을 지키지는 못했지만 이때 다짐했던 하나의 결과를 위해 고민을 수없이 하고 의문을 가지고 기능 구현에 안주하지 않는 자세를 지금까지 갖고 있었구나 라는 생각을 하게 된다. 어쩌면 프로젝트를 진행하는 과정에서 사용하는 기술, 언어, 모듈에 대한 이해도 모두 중요하지만 이러한 과정에서 나를 발전시킨 것이 아니였을까. 또 아직 더 발전해야 하는 나의 모습을 더욱 앞으로 나아가게 하는 과정이라고 생각된다.

아파트 온도, 습도, 전력 실시간 데이터 관리 프로젝트 Part 2.

스케줄러를 통한 이메일 보내기 기능 구현

사용자의 실시간 데이터를 서버로 전송받고 데이터베이스에 저장하여 쌓인 데이터를 연산하여 사용자에게 특정한 일자에 보내도록 하는 기능을 제공하기로 했다.

가장 먼저 생각한 방식이자 이상적인 방법이였던 카카오톡으로 전송하기는 토이 프로젝트이기에 사업자 번호 등 카카오에서 요구하는 조건에 부딧혀 포기하고 구글 이메일 보내기로 방향을 틀게 되었다.

구상하는 방식은 이렇다.

  1. 매월 1일 자정에 모든 동 호수의 데이터를 그래프로 그린다.
  2. 각 호수별 사용자이자 입주민이라는 가정하에 그래프 이미지를 이메일로 매월 1일 00시 15분에 발송한다.

스케줄러 활용

우리는 node-schedule모듈을 활용하여 스케줄러를 작성하여 메일을 발송하기로 했다.

  1. 그래프 이미지 그리기
  • 모든 집의 데이터를 각각 그래프로 그려 이미지로 파일로 저장
그래프 그리기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
const get_month = () => {
const today = new Date();
return today.getMonth() + 1;
}

// 아파트 단지, 동, 호 일별 데이터 가져오기
const sensorData = async (Complex, Dong, Ho) => {
let exData = await apt_Info.sensorFind(Complex, Dong, Ho);
return exData;
};

// 아파트의 모든 단지 동 호 정보 가져오기
const basename = async () => {
const aptInfo = await apt_Info.aptFind();

for(let i=0; i < aptInfo.length; i++) {
// 파일 이름 지정 => ex) 단지 동 호.png
let fileName = `${aptInfo[i].AptDong.apt_complex}${aptInfo[i].AptDong.apt_dong}${aptInfo[i].apt_ho}`;
// 단지, 동, 호의 센서 데이터 받아오기.
let data = await sensorData(aptInfo[i].AptDong.apt_complex, aptInfo[i].AptDong.apt_dong, aptInfo[i].apt_ho);

// 차트를 그리기위한 날짜, 데이터 별 배열 분리
let dateArray = [];
let humiArray = [];
let tempArray = [];
let wattArray = [];
data.map((sensor) => {
let newDate = new Date(sensor.dataValues.date);
dateArray.push(dateAndTime.format(newDate, 'MM-DD'));
tempArray.push(sensor.dataValues.humidityAVG);
humiArray.push(sensor.dataValues.temperatureAVG);
wattArray.push(sensor.dataValues.electricitySUM);
});

// 차트 그리기 함수로 파일 이름과 데이터 배열 전달
humiChart(fileName, humiArray, dateArray);
tempChart(fileName, tempArray, dateArray);
wattChart(fileName, wattArray, dateArray);
};
console.log("차트 그리기 시작");
logger.info('create chart');
};

// 아파트 모든 세대의 전력 차트 그리기
const wattChart = (fileName, data, date) => {
const filewatt = fileName + "watt.png";
saveChart(filewatt, "Watt", data, date);
};

// 아파트 모든 세대 온도 차트 그리기
const tempChart = (fileName, data, date) => {
const filetemp = fileName + "temp.png";
saveChart(filetemp, "Temp", data, date);
};

// 아파트 모든 세대 습도 차트 그리기
const humiChart = (fileName, data, date) => {
const filehumi = fileName + "humi.png";
saveChart(filehumi, "Humi", data, date);
};

//차트그리기 스케줄러
const drawChart = () => {
// 매달 자정에 파일 생성
const jobs = schedule.scheduleJob('0 0 01 * *', function() {
basename();
});
};

이미지 그리기 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const ChartJsImage = require('chartjs-to-image');
const fs = require('fs');

const chart = new ChartJsImage();

const days = function(month,year) {
return new Date(year, month, 0).getDate();
};

const get_Month = () => {
const today = new Date();

return today.getMonth() + 1;
}

const makeFolder = (dir) => {
try {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
} catch (err) {
console.error(err)
}
}

const saveChart = async (filename, type, data, date) => {
// 차트 label 지정
const labels = date;
// 차트 그리기
chart.setConfig({
type: 'line',
data: { labels: labels, datasets: [{ label: type, data }] },
});

// 월별 / 데이터별 파일 구분하기
const folderName = './data/' + (get_Month() - 1);
const monthPath = folderName + '/' + type;
// Write file to disk
makeFolder(folderName);
makeFolder(monthPath);
await chart.toFile(monthPath + "/" + filename);
}

module.exports = saveChart;

각각의 파일은 월별 / 데이터 종류로 구분되어 저장되고 저장된 이미지를 사용자에게 전송한다.

  1. 이메일 보내기
이메일 보내기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 이메일 보내기 스케줄러 시간 지정 
const mailResult = async () => {
const userinfo = await User.findAll({
attributes: ['uemail', 'apt_ho'],
include: {
model: AptHo,
include: {
model: AptDong,
}
},
});
userinfo.map((user) => {
let emailParam = {
toEmail: `${user.uemail}`,
subject: `${get_month() -1}월 사용량입니다.`,
text: `${get_month() - 1}월 사용량입니다.`,
// ex)1단지101동101호
name: `${user.AptHo.AptDong.apt_complex}${user.AptHo.AptDong.apt_dong}${user.AptHo.apt_ho}`,
month: get_month() - 1,
};

// const rule = new schedule.RecurrenceRule();
// const m = 50;
// rule.minute = m;

// 매달 1일 0시 15분 이메일 보내기 실행
const j = schedule.scheduleJob('13 13 02 * *', function() {
// const j = schedule.scheduleJob(rule, async function() {
console.log("send mail");
// await basename();
logger.info('send mail');
mailSender.sendGmail(emailParam);
});
});
};

테스트를 위해 시간을 조정하여 진행하였고 스케줄러 시간 부분만 원하는 시간으로 조정하면 정상적으로 전송이 된다.

결과



아파트 온도, 습도, 전력 실시간 데이터 관리 프로젝트 Part 1.

아파트 실시간 데이터 관리 프로젝트

아파트 실시간 데이터를 전송받아 저장하고 사용자에게 데이터를 제공하는 서비스를 만들어보았다.
Node.js, express를 기반으로 하여 서버를 구성하고 사용자가 데이터를 볼 수 있는 View는 기본적인 html로 구성하여 백엔드 구축을 중점으로 한 프로젝트이다.

실시간 데이터를 전송받기 위해 선택한 통신방법은 MQTT통신을 사용하였다.
소켓 통신을 통해 실시간 데이터 처리를 하려했던 과정에서 MQTt통신으로 방법을 변경하게 된 이유는 다음과 같은 이유였다.

  1. Socket 과 Mqtt 통신 모두 실시간으로 데이터를 전송 할 수 있지만 Socket은 데이터의 전송을 하고 응답을 기다리고 있다.
    하지만 우리의 프로젝트는 실시간 데이터를 전송만 받을 뿐 다시 응답을 돌려 주지 않기 때문에 필요없는 자원이 소모된다. 그러나 Mqtt통신은 데이터의 전송을 하지만 응답을 대기하지 않는다.
  2. 실시간 통신을 위한 서버와의 연결이 비정상적으로 해제 되었을 때 Socket은 재 연결 까지 전송 받아야 하는 데이터가 유실된다. 반면 Mqtt통신은 연결이 해제 되어도 데이터를 연결이 될 때까지 보관 후 연결시 순차적으로 전송해주기 때문에 유실 가능성이 적다.
  3. 프로젝트에서 가공하는 데이터가 온도, 습도, 전력 과 같은 데이터이기 때문에 아두이노 같은 IOT기기에 적합한 통신 방법을 사용해 보기 위해서

그렇다면 WebSocket과 Mqtt는 무엇이 다른가???

  1. WebSocket이란 TCP기반 소켓 통신을 대체할 목적에서 등장한 양방향 통신기법이다.
  2. Mqtt란 저전력, 신뢰할 수 없는 네트워크 등의 상황에서 사용하는 메시징 프로토콜이다.

두가지 모두 실시간 통신이 가능하지만 두 방식을 직접 적으로 비교할 수는 없다고 한다. 이유를 알아보니 통신 레벨의 차이와 mqtt는 통신 프로토콜에 의존하지 않으며 패킷 구조로 데이터를 전송하는 차이점이 있다. 간단하게 두가지의 차이점을 알아보고 우리는 왜 Mqtt를 선택하여 실시간 온도 등의 데이터를 전송받기로 했는가에 대한 이유를 알아보자.

자세히 보기

Nodejs express로 에어비앤비 클론코딩 해보기 5.

도시별 숙소 보여주기

도시별 숙소 라우터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
router.get('/city', async (req, res, next) => {
try {

let hosts;
let count;
let city;
let category
...
hosts = await Host.findAll({
where: {
hostaddress: {
[Op.like]: "%" + req.query.city + "%"
},
},
include: {
model: Image,
attributes: ['src'],
},
order: [['id', 'DESC']],
offset: offset,
limit: 12,

});
count = await Host.count({
where: {
city: {
[Op.like]: "%" + req.query.city + "%"
},
},
});
if(req.query.city) {
city = req.query.city;
} else {
city = "전체"
}
res.render('hosts', {hosts, count, city});
}

} catch (error) {
console.error(error);
next(error);
}
});

숙소의 주소 중 도시 컬럼의 값을 비교하여 해당 도시의 숙소를 가져오는 방식이다.

자세히 보기

Nodejs express로 에어비앤비 클론코딩 해보기 4.

local 계정 회원가입 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 프론트 코드
document.getElementById('join__form').addEventListener('submit', (e) => {
e.preventDefault();
const joinData = {
email: e.target.email.value,
password: e.target.password.value,
name: e.target.name.value,
nickname: e.target.nickname.value,
phone: e.target.firstNum.value + e.target.middleNum.value + e.target.lastNum.value,
}
console.log(joinData);
if(joinData.email === "" ) {
return alert('이메일을 입력해주세요.');
}
if(joinData.password < 4) {
return alert('비밀번호는 4글자 이상입니다.');
}
if(joinData.name === "" || joinData.nickname === "" || joinData.phone === "" || joinData.phone.length > 11) {
return alert('잘못된 정보입니다.');
}

axios.post('/auth/join', joinData)
.then((response) => {
if(response.data.joinsuccess){
alert(response.data.message);
return window.location = "/login";
}
alert(response.data.message);
return location.reload();
})
.catch((error) => {
console.log(error);
});


e.target.email.value = '';
e.target.password.value = '';
e.target.name.value = '';
e.target.nickname.value = '';
e.target.firstNum.value = '';
e.target.middleNum.value = '';
e.target.lastNum.value = '';
});
// 서버 코드
router.post('/join', isNotLoggedIn, async (req, res, next) => {
try {
const {
email,
password,
name,
nickname,
phone,
} = req.body;
console.log(req.body);
//const phone = firstNum + middleNum + lastNum;
const user = await User.findOne({ where: {email} });
if(user) {
return res.json({ joinsuccess: false, message: "입력하신 이메일로 가입한 계정이 존재합니다..." });
}
const hash = await bcrypt.hash(password, 12);
const newUser = await User.create({
email,
password: hash,
name,
nickname,
phone,
});
return res.json({ joinsuccess: true, message: "회원가입 성공입니다."});

} catch (error) {
console.error(error);
return res.json({ joinsuccess: false, message: "데이터 베이스 오류 입니다 관리자에게 문의 하세요..."});
}
});

회원 가입 페이지에서 가입 정보를 받아온 후 User모델에 새로운 데이터를 생성하여 회원을 생성한다.

가입 전 회원을 구분할 수 있는 이메일로 기존의 데이터와 일치한 정보가 있는지 확인을 하고 없을 때 생성하는 방식으로 진행했다…

결과

회원 가입을 할 정보를 입력한다.

서버에서 처리 후 json형식으로 보낸 데이터에 따라 성공 실패 여부를 확인 한다.

입력한 데이터가 없는 회원이기 때문에 성공이라는 메시지를 띄웠고 성공했다는 것을 알수있다..✌✌

users 테이블을 확인하면 아까 입력한 데이터가 성공적으로 생성 된 것을 확인 할 수 있다. 👍

문제점

로컬 계정 회원 가입 처리 코드 작성은 간단하게 구현해서 어렵게 느껴지지는 않았지만… 페이지에서 데이터를 보낼 때 검사하는 방법이 너무 많고 어려운게 많아서 간단하게 검사 후 서버로

데이터를 보내는 방식으로 진행했다…

자바 스크립트를 이용해서 데이터를 검사해서 보내야 하는 방법에 치여서 조금 어려움을 느꼈다 자바스크립트의 공부가 필요하다는 것을 느끼는 과정 이였다.

보완점

혼자 하는 프로젝트라서 많은 것을 축소해서 진행 하려 하다 보니 회원 가입 과정이 너무 단순하다는 생각이 들었다. 많은 사이트들을 참고해서 비슷한 과정으로 만들고 싶었지만

본인 인증 부분을 제외 시킨 것이 조금 아쉽다.

본인 인증 구현도 공부해서 사용해봐야겠다.

Nodejs express로 에어비앤비 클론코딩 해보기 3.

숙소 업로드 구현

숙소 업로드 라우터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
try {
fs.readdirSync('uploads');
} catch (error) {
console.error('uploads 폴더가 없어 uploads 폴더를 생성 합니다.');
fs.mkdirSync('uploads');
}

const upload = multer({
storage: multer.diskStorage({
destination(req, file, cb) {
cb(null, 'uploads/');
},
filename(req, file, cb) {
const ext = path.extname(file.originalname);
cb(null, path.basename(file.originalname, ext) + Date.now() + ext);
},
}),
});

// 게시글 업로드
router.post('/host', isLoggedIn, upload.array('img'), async (req, res, next) => {
const hostaddress = req.body.firstCity.concat(" ", req.body.middleCity," ", req.body.hostaddress);

try{
const newHost = await Host.create({
title: req.body.title,
hostaddress,
city: req.body.firstCity,
person: req.body.person,
roominfo_room: req.body.room,
roominfo_bed: req.body.bed,
roominfo_cook: req.body.cook,
roominfo_bathroom: req.body.bathroom,
hostinfo: req.body.hostinfo,
hosttype: req.body.hosttype,
UserId: req.user.id,
});
for(i=0; i<req.files.length; i++){
let newImage = await Image.create({
src: "img/" + req.files[i].filename,
HostId: newHost.id,
});

}
console.log("success");
res.redirect('/');
} catch (error) {
console.log(error);
next(error);
}
});

숙소 업로드 라우터는 숙소 정보를 담는 Host모델과 숙소의 사진을 담는 Image모델을 사용하여 업로드를 진행한다.

사진 파일들은 multer 모델을 사용하여 업로드했고 이미지의 파일 이름을 Image모델에 담아 주었다.

이미지 업로드는 이미지의 갯수 만큼 생성하도록 반복문을 사용하여 진행했는데 더 좋은 방법이 있을 수 도 있지만 간단하게 구현 했다.

자세히 보기

Nodejs express로 에어비앤비 클론코딩 해보기 2.

Sequelize Model 구축

Mysql 데이터베이스를 사용하며 Sequelize를 통해 데이터 관리를 했다. 시쿼라이즈를 통한 데이터베이스 사용을 위해 모델을 구축하는 과정을 기록합니다.

DB 관계

User 1 : N Host
User 1 : N Review
User 1 : N Reservation
User 1 : N Favorite

Host 1 : N Image
Host 1 : N Favorite
Host 1 : N Reservation
Host 1 : N Review

많은 모델을 설계하지는 않았지만 각각 필요한 모델이라고 생각하는 모델을 구축했다.
기본적으로 유저와 게시글을 기준하여 예약 기능에 필요한 모델 찜 모델, 리뷰 모델 정도로 구성했다.

자세한 코드는 깃헙에 올렸으니 기본적인 관계 설정과 모델 구조는 깃에서 확인하면 될 것 같다.
Model file : github

Nodejs express로 에어비앤비 클론코딩 해보기 1.

에어비앤비 클론코딩 해보기 1.

Nodejs를 학습하고 학습한 내용을 바탕으로 무언가를 만들어봐야 겠다는 마음 하나로 무턱대고 혼자 진행했던 프로젝트입니다.
실제 서비스까지의 구현을 목표로 하지 않고 node.js와 express를 이용한 앱을 만들어보고자 진행했던 프로젝트라 많이 힘들었고 좋은 결과물이 나오지는 못했지만 처음으로 스스로 처음부터 만들어본 것 이기 때문에 이후 다른 어떤 프로젝트에서도 기본적인 개념을 알고 진행하는데 있어 충분한 학습 효과가 있었던 프로젝트 입니다.

Github : https://github.com/hanjuren/Node

프로젝트의 전체적인 구조

  • Nodejs기반 express앱
  • mysql 데이터 베이스 사용 및 sequelize 사용
  • view를 위해 nunjucks 템플릿 엔진 사용
  • passport를 통한 사용자 인증 기능 구현

완성 결과

사용한 패키지

  • express
  • express-session
    • express-mysql-session
  • passport
    • passport-local
    • passport-kakao
    • passport-naver
    • passport-facebook
  • bcrypt
  • cookie-parser
  • dotenv
  • morgan
  • multer
  • nunjucks
  • mysql2
  • sequelize
    • sequelize-cli
  • nodemon

프로젝트를 진행하며 느낀점

node.js의 기본적인 학습만 진행하고 바로 작업을 진행하며 하나씩 붙여나가는 과정이 스스로 무언가를 만들어낸다는 것에 의미가 큰 과정이였습니다.
비록 작업을 계속 해나가며 프로젝트의 과정이 설계를 하지 않고 혼자 추가해보고 싶은 기능을 구현하다보니 두서 없는 과정으로 진행되었지만 기능 하나하나 스스로 만들며
막히는 부분을 해결해 나가는 과정이 의미있었습니다.
하지만 이 과정을 겪고서 작고 사소한 프로젝트라도 시작 전 설계의 중요성과 구조를 잡고 시작해야하는 중요성을 크게 느끼게 되었고 무턱대고 작업을 한다고 해서 그저 돌아가기만 하는 기능을 만드는 것이
무의미 하다는 것도 느끼게된 프로젝트 였습니다.
아쉬웠던 점은 클론코딩을 하다보니 스스로 생각해내는 기능은 적고 클론코딩의 대상이 된 웹의 기능을 어떤식으로 구현해볼지에 대해 고민을 하던 시간이 대부분이였던 것에 아쉬움을 느꼈습니다.
스스로 만들어나가는 과정은 뿌듯했지만 새로운 것을 만들어 낸다는 느낌보다는 기능을 가능한 수준에서 모방하기만 한다는 느낌이 컷던 만큼 이후 프로젝트에서는 하나의 기능을 만들더라도
많은 고민과 보다 좋은 기능을 만들 수 있는 측면까지 고민하며 작업해야겠다라고 생각하게 되었습니다.

또한 이 프로젝트의 결과물에 대해 다시 한번 글을 작성하며 이런 부분은 지금보니 이렇게 개선하면 되겠다는 부분을 생각을 많이 하게되었던 프로젝트 입니다.
이 프로젝트에서 했던 내용을 기록하며 다시 공부한다는 마음으로 진행 부분을 기록하려 합니다.