2021년 2월 23일 화요일

오픈소스 기반 IoT, MongoDB, BIM과 Autodesk Forge 연결 개발 방법

이 글은 IoT(Internet Of Things), NoSQL MongoDB, BIM(Building Information Modeling)과 Autodesk Forge 상호연결 방법에 대한 간단한 설명이다. 개발 환경은 node.js, express, bootstrap을 사용해 앱 서버로서 실행된다. 이 글에서 소개하는 기술은 디지털 트윈 개념 구현에 참고할 수 있으며, 손쉽고 저렴한 기술 개발을 위해, 오픈소스를 적극 사용한다. 동기 부여를 위해, 이 글에서 개발될 시스템명은 DTB-BMS(가칭 Digital Twin and BIM based Building Management System)으로 한다.

쉬운 개발을 위해, IoT 센서 데이터 취득 및 DB 연결은 node-red를 사용한다, 굳이 성능 최적화가 중요하다면, 파이썬 등 다른 언어를 사용해 코딩한다. 참고로, 이 글은 간단히 메이크하는 것을 지향하므로, 굳이 어려운 방법을 사용하지는 않는다.

이 글의 개발 파일 및 소스 코드는 블로그에 싣기에는 많고 복잡할 수 있다. 그러므로, 주요 개발 컨셉과 코드를 중심으로 설명한다. 관련 코드들을 이 github링크의 해당 폴더들을 참고하길 바란다. git을 설치하면, 간편하게 개발된 코드를 다운로드할 수 있다. 참고로, 개발 결과 화면은 다음과 같다. 이와 관련된 Mongo DB, node.js, IoT 장치 개발 등 다른 내용들이 궁금하다면, 이 블로그의 관련 글들을 참고한다. 
DTB-BMS(Digital Twin and BIM based Building Management System) APP server screen and database REST API(Application Program Interface)

DTB-BMS 시스템 요구사항 및 패키지 디자인
막코딩이 싫다면 목적에 맞게 시스템 아키텍처를 설계하는 것은 중요하다. 이 시스템의 핵심 요구사항은 다음과 같다.
  • BIM 모델의 각 공간에 설치한 IoT 센서 데이터 모니터링
  • 분석을 위한 각 공간별 센서 데이터 취득 지원 
  • 소형화된 센서의 간편한 설치
  • 공간별 센서 데이터의 주요 부분 그래프 지원
  • 데이터 모델 확장성이 쉬운 구조
  • 유지보수성 고려한 DB(Database), API, 비지니스 로직 분리
  • 50만원 이하 매우 저렴한 비용으로 앱, DB, API 서버 구현
시스템 패키지 구조는 다음과 같다. 
디지털 트윈 시스템 기본 아키텍처(UML)

IoT 센서 개발
아두이노 나노 센스를 이용해, BIM 공간 관리를 위한 데이터를 얻는다. 핵심 아두이노 코드는 다음과 같다. Serial port로 출력하며, 출력 목적지는 초기 설정에 따라 WiFi, BLE, 유선이 가능하도록 한다.
void sensingNano(void) {
  display.clearDisplay();

  float temperature = HTS.readTemperature();
  float humidity    = HTS.readHumidity();
  float pressure = BARO.readPressure();

  // print each of the sensor values
  Serial.print(temperature);
  Serial.print(", ");
  Serial.print(humidity);
  Serial.print(", ");
  Serial.print(pressure);
  
  display.setCursor(0, 24);
  String data = String("THP: ") + String(temperature, 1) + ", " + String(humidity, 1) + ", " + String(pressure, 1);
  display.println(data);
  ...
}

구현 코드는 다음 링크를 참고한다.
구현 결과는 다음과 같다. 

관련해 자세한 개발 방법은 아래 링크를 참고한다.
참고로, BLE 버전은 센서가 많으나 블루투스만 지원한다. 데이터를 몽고디비에 저장하기 위해서 인터넷 연결이 필요하다. 이는 Serial port를 통해 node-red에서 데이터를 받아 전송하거나, WiFi회로를 만들어 데이터를 전송한다. WiFi는 다음 예를 참고해 ESP8266를 사용한다.
노드레드 활용 Mongo DB에 IoT 데이터 전송
IoT 장치에서 보낸 데이터를 MongoDB에 전송하기 위해 node-red를 사용한다. node-red에서 다음과 같이 function노드를 만들어 프로그래밍한다.
노드레드 기반 센서 데이터베이스 저장을 위해 코딩된 그래프

프로그래밍 된 코드는 다음 링크를 참고한다.
참고로, 노드래드 그래프에서 첫번째 함수는 아두이노와 연결된 COM 포트에서 콤마 형식으로 구분된 IoT 데이터들(예. "608.02, 910.13, 7203.23, ...")을 개별 토큰(token)으로 분리한다. 두번째 함수는 토큰으로 분리된 값들을 각각 mongo DB 노드로 전송한다.

첫번째 함수 노드 스크립트는 다음과 같다.
var output = msg.payload.split(",");

var msgList = [];
output.forEach(function(v, index)
{
    var msgIoT = {payload: parseFloat(v)};
    msgList.push(msgIoT);
});

return msgList;

두번째 함수 노드 스크립크를 다음과 같이 코딩한다.
var d = new Date();
var date = d.getFullYear() + '-' + d.getMonth() + '-' 
        + d.getDate() + '-' + d.getHours() + ':' + d.getMinutes()
        + ':' + d.getSeconds();
            
var newMsg = {
            'sensor': 'temp',
            'area' : 'Skinner',
            'date' : date,                    
            'value': msg.payload
            };
 
return newMsg;

다음과 같이 노드레드에서 Mongo DB 노드를 생성해, 데이터베이스와 연결하면 된다. 
node-red의 MongoDB 설정 화면

데이터베이스 및 BIM 정의
먼저 MongoDB를 설치한다(MongoDB에 대한 사용법은 이 링크를 참고한다).
센서 데이터베이스 모델링을 위해, 터미널에서 mongo를 실행하고, 다음과 같이 정의한다.
show dbs
use sensor
db.records.insert({sensor:'light', area:'UNF', date:'2021-1-1', value:31})
Mongo DB 터미널에서 데이터베이스 정의 결과

MongoDB compass에서 확인한 결과는 다음과 같다. 

현재는 온도만 저장하고 있으나, 센서 유형별로 다양한 데이터 값을 저장하기 위해 sensor 컬럼을 추가하였다. 테스트에서는 온도 센서값만 저장하고 있다. 1년동안 서비스한다는 가정하에 어느 정도 저장 용량 장치가 필요하지 확인하기 위해, 다음과 같이 저장 용량을 계산한다.
데이터베이스 향후 저장 용량 계산

1년을 저장한다면, 7,388.94 Mb 저장 용량이 필요하다. 센서 유형을 온도 이외에 습도, 조도, 사운드 추가시 약 30 Gb 저장 용량이면 된다. 1년 간 데이터 백업을 고려하면 60 Gb이다.  저장소가 부족하 경우, 압축이나 데이터 취득 간격 조정을 고려한다. 참고로, IoT장치에서 1초에 2번씩 데이터를 샘플링하므로, 1초로 줄일 경우, 계산된 저장용량의 절반 정도면 충분할 것이다. 참고로, 신호처리이론(Nyquist–Shannon sampling theorem, Sampling and sampling density)에 의해 데이터 처리 간격이 1/1초라면, 데이터 손상 없는 재현을 위해, 최소 두배(2.56) 간격으로 샘플링하는 것이 좋다. 아울러, 샘플링은 센서 스펙도 함께 고려한다(예. Arduino Nano BLE sense 33. Sensor output rate = 104 Hz).

각 레코드는 IoT 데이터와 연결을 위해서는 모델 객체 별로 연결 데이터 처리 작업이 필요하다. BIM 모델링은 Autodesk Revit을 이용한다. 이 과정은 모델 용량에 따라 매우 많은 노가다가 필요할 수 있다. BIM 객체에 센서 데이터 연결을 위한 ID같은 연결점을 모델링한다(마법은 없다. 눈으로 객체를 확인하며 손으로 직접 입력 수정한다). 상호 연결을 위해 객체 속성 GUID, 분류체계, 태그 ID 등을 생성하고 이를 이용해 DB, 비지니스 로직과 연결한다.
Autodesk Revit의 Room 속성 예시

참고로, 이 글에서는 테스트를 위해, Autodesk 제공 예제 파일을 이용한다. 

MongoDB CRUD REST API 서버 개발
몽고디비 리소스는 보안 등의 문제로 인터넷 브라우저에서 직접 접속할 수 없다. 데이터를 얻기 위해, REST API서버를 개발한다. 주요 코드는 다음과 같다.
// sensor.mode.js 파일
const mongoose = require('mongoose');  // 몽고DB 연결을 위해 mongoose node 패키지 사용

var Schema = mongoose.Schema, ObjectId = Schema.ObjectId;

// 저장되는 몽고DB 스키마 구조 정의
const RecordSchema = new Schema({
    _id : ObjectId, 
    sensor : String,
    area : String,
    date : String,
    value : Number
    // _msgid : String
}); 

const record = mongoose.model('record', RecordSchema);
module.exports = record;

// sensor.routes.js 파일
module.exports = (app) => {
    const sensors = require('../controllers/sensor.controller.js');

    // Retrieve all sensors
    app.get('/sensors', sensors.findAll);  // 센서 데이터를 얻는 URL GET 함수
    ...

// sensor.controll.js 파일
const Record = require('../models/sensor.model.js');

// 데이터베이스에서 데이터 검색해 리턴.
exports.findAll = (req, res) => {
    Record.find()
    .then(records => {
        res.send(records);
    }).catch(err => {
        res.status(500).send({
            message: err.message || "Some error occurred while retrieving Record."
        });
    });
};
...

실제 구현 코드는 아래 링크를 참고한다.
관련 내용의 자세한 설명은 다음 링크를 참고한다.
개발 결과는 다음과 같다.
센서 데이터 REST API 결과

Postman 도구를 이용하면, 개발된 REST API를 쉽게 테스트할 수 있다(참고. Postman 설치). Postman에서 GET URL을 http://localhost:4000/sensors로 입력한다. 다음 그림과 같은 결과가 출력되면 성공한것이다.
Postman REST API 테스트 결과

참고로, REST API서버는 이를 이용하는 포지 앱 서버와 서로 CORS 설정이 되어 있어야, 에러 없이 리소스에 접근할 수 있다. 구현 코드는 이를 기본 옵션으로 설정해 두었다. 관련 자세한 내용은 다음 링크를 참고한다. 
Autodesk Forge와 Mongo DB 연결 및 데쉬보드 개발
이제, Autodesk Forge와 Mongo DB를 연결하고, 데쉬보드 개발한다. BIM Forge view 개발 방법은 다음을 참고한다.
실제, 구현 코드는 다음 링크를 참고한다.
구현 코드의 폴더 구조는 다음과 같다.
구현 코드에서 포지 앱과 몽고 DB연결은 앞서 개발한 API 서버를 사용한다. 주요 코드는 다음과 같다.
//  viewer.html 파일
<!DOCTYPE html>
<html>
...
<head>
</head>
...
<body>
    <script type="text/javascript">
        function renderSensorTable(responseText) {
            var htmlElement= document.getElementById('sensorDataTable'); 
            htmlElement.innerHTML = '';

            var table = document.createElement("table");
            table.setAttribute("class", "table table-striped table-sm table-responsive");

            var head = "<thead><tr><th>No</th><th>Area</th><th>Sensor</th><th>Time</th><th>Value</th></tr></thead>";
            table.innerHTML = head;   // 테이블 헤더 정의

            var tbody = document.createElement("tbody");
                           
            var records = JSON.parse(responseText);  // REST API 리턴 데이터를 JSON 포맷으로 변환
            for (var i = 0; i < records.length; i++) {    // 센서 데이터 레코드 행 추가
                var record = records[i];

                var recValues = [];
                recValues.push(i);
                recValues.push(record.area);
                recValues.push(record.sensor);
                recValues.push(record.date);
                recValues.push(record.value);               

                var row = document.createElement("tr");

                for (var j = 0; j < recValues.length; j++)  // 센서 데이터 레코드 값 추가
                {
                    var c = document.createElement("td");
                    var t = document.createTextNode(recValues[j]);
                    c.appendChild(t);
                    row.appendChild(c);                    
                }

                tbody.appendChild(row);
            } 
            table.appendChild(tbody);
            htmlElement.appendChild(table);
        }

        function renderSensorData() {  // 센서 데이터 렌더링
            // Create a new ajax requst
            var oReq = new XMLHttpRequest();  // REST API 요청

            oReq.onreadystatechange = function ()  // 비동기 데이터 처리
            {
                if (oReq.readyState == 4 && oReq.status == 200)
                    renderSensorTable(oReq.responseText);  // IoT 데이터 전송 시 처리
            };

            // Create the connection to our API
            oReq.open("GET", "http://localhost:4000/sensors");  // REST API를 통한 IoT 센서 데이터 요청
            oReq.setRequestHeader('Content-Type', 'application/json');
            oReq.send();    // Fire the request
        }

        /**
        * Autodesk.Viewing.Document.load() success callback.
        * Proceeds with model initialization.
        */
        function onDocumentLoadSuccess(doc) {
            // Load the default viewable geometry into the viewer.
            // Using the doc, we have access to the root BubbleNode,
            // which references the root node of a graph that wraps each object from the Manifest JSON.
            var viewable = doc.getRoot().getDefaultGeometry();
            if (viewable) {
                viewer.loadDocumentNode(doc, viewable).then(function(result) { // 3D 뷰어 로딩시 발생 이벤트
                    console.log('Viewable Loaded!');
                    viewer.loadExtension("NestedViewerExtension", { filter: ["2d"], crossSelection: true });  

                    renderSensorData();  // 센서 데이터 렌더링
                }).catch(function(err) {
                    console.log('Viewable failed to load.');
                    console.log(err);
                }
              )
            }
        }
  </script>
</body>
</html>

데쉬보드 그래픽은 Bootstrap을 사용하며, 뷰어 우측과 아래에 차트 그래프(forgeViewer div 요소 뒤에 추가됨), 테이블(sensorDataTable)로 동적 구성된다. 동적 구성될 요소는  viewer.html의 body에서 다음과 같이 정의되어 있다.
<body>
    <!-- Custom script -->
    <h4>Digital Twin based Building Monitoring and Data Analysis</h4>
    <div class="container-fluid fill">
        <div class="row fill">               
            <div class="col-sm-7 fill">          
                <div id="forgeViewer"> 
                </div>
            </div> 
        </div>
    </div>        
    <div class="container-fluid fill">
        <div class="row">       
            <div class="col-sm-12 fill">
                <button id="reflashSensorData" onclick="renderSensorData()">Reflash IoT sensor data</button>                  <div id="sensorDataTable" class="table-responsive"></div> 
            </div>             
        </div>
    </div>

그래프 동적 구성을 위한 주요 코드는 다음과 같다. 데쉬보드 div 요소가 추가되고, 데쉬보드 div 요소 안에 바차트, 파이차트 div 요소가 추가된다.
// dashboard.js
$(document).ready(function () {
    $(document).on('DOMNodeInserted', function (e) {
        if ($(e.target).hasClass('orbit-gizmo')) {
            new Dashboard(NOP_VIEWER, [new BarChart('Light'), new PieChart('Temp')]);  // HTML DOM ready 이벤트 발생 시 데쉬보드 추가.    
        }
    });
})
class Dashboard {
    adjustLayout() {
        var row = $(".row").children();
        if(row.length > 0)
            $(row[0]).removeClass('col-sm-7').addClass('col-sm-9 transition-width').after('<div class="col-sm-3 transition-width" id="dashboard"></div>');  // Forge viewer div 아래에 차트 그래프 요소 추가
    }
}

// Dashboard panel base
class DashboardPanel {
    load(parentDivId, divId, viewer) {
        this.divId = divId;
        this.viewer = viewer;
        $('#' + parentDivId).append('<div id="' + divId + '" class="dashboardPanel"></div>');  // 데쉬보드 요소 추가
    }
}

// Dashboard panels for charts
class DashboardPanelChart extends DashboardPanel {
    load(parentDivId, divId, viewer) {
        divId = this.propertyToUse.replace(/[^A-Za-z0-9]/gi, '') + divId; // div name = property + chart type
        super.load(parentDivId, divId, viewer);
        this.canvasId = divId + 'Canvas';
        $('#' + divId).append('<canvas id="' + this.canvasId + '" width="400" height="400"></canvas>');  // 차트 그래프 요소 추가
        return true;
    }
}

// 바차트 데이터 렌더링 구현
class BarChart extends DashboardPanelChart {
    drawChart() {
    ..
    }
}

// 파이차트 데이터 렌더링 구현
class PieChart extends DashboardPanelChart {
    drawChart() {
   }
}

이 코드의 실행 결과로 다음과 같이 HTML DOM(document object model)이 구성된다. 앱 화면 레이아웃은 목적에 맞게 CSS파일의 해당 요소 스타일을 적절히 수정한다.
DOM 구조
데쉬보드 화면

BIM 객체 선택 시 처리(object selection override)는 다음과 같이 Forge viewer에 이벤트 핸들러를 등록해 수행한다.
            ...
            viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, onSelectionChanged);
        }

        function onSelectionChanged(event) {
            if (event.dbIdArray.length === 1) {
                viewer.getProperties(event.dbIdArray[0], function(data) {
                    console.log(data.name);
                    if(data.name.indexOf("Column") >= 0)
                    {
                        var instanceTree = viewer.model.getData().instanceTree;
                        var parentId = instanceTree.getNodeParentId(event.dbIdArray[0])
                        viewer.select([parentId]);
                    }
                });
            }
        }
        ...

마무리
이제 BIM 공간 선택 시 해당 데이터를 차트 그래프와 연결하고, IoT 센서 장치를 각 공간에 설치하면, 현실과 디지털 공간과 연결한 소위 디지털 트윈이란 개념을 구현한 것이다. 개발해 본 결과를 보면, 기존 센서 데이터를 디지털 모델과 연계하거나 시뮬레이션했던 시스템들과 큰 차이가 없다. 이런 접근이 브랜드로 발전하고 대중화된 것이다. 디지털 트윈을 구현하는 방법은 다양할 수 있다. 구현 방법은 요구사항과 개발 전략에 따라 달라질 수 있다. 만약, GIS map을 사용한다면, Leaflet(리플릿), mapbox(맵박스), 3D Cesuim(세슘) 등 다양한 오픈소스 도구를 사용할 수 있다. 도면만 필요하다면, DWG/DXF 캐드 뷰어만 사용해도 된다. 데이터 모니터링만 필요하다면, 테이블 및 차트만 지원되는 데쉬보드 형태의 HCI(Human Communication Interface)도 충분하다.
오픈소스 기반 GIS 공간정보 뷰어 예시 

굳이, 3D 모델이 필요없이 효율적인 업무가 가능하다면, 디지털 트윈 타이틀이 붙은 과제라도 3D 모델 뷰어를 구현할 필요가 없다. 앞서 개발하는 과정을 보면 알겠지만, 디지털 트윈 개념을 서비스하기 위해서는 많은 비용과 노력이 필요하다. DATABASE, REST API, 보안, VIEWER, DASHBOARD, BUSINESS LOGIC, IoT, 서버, 데이터 모델링 등 욕심을 내다 보면 끝도 없다. 

보통, 사용자는 유지보수 비용을 크게 간과하는 경향이 있다. 실제 서비스 중인 소프트웨어는 유지보수 비용이 80~90%를 상회한다.

소프트웨어 공학 관점에서 보았을 때 불필요한 모델, 데이터의 구현은 유저의 시스템 사용을 매우 불편하게 할 뿐아니라 유지보수, 성능에 나쁜 영향을 준다. 이 시점에서 모든 것을 막연한 짐작으로 디지털 트윈이나 BIM으로 구현하면 편리하다는 주장은 근거가 매우 빈약하며, 쉽게 납득하기도 어렵다(발주자가 진정 돈을 아끼고 싶다면, 화려하고 복잡한 PPT는 조심해야 한다). 물리적 데이터를 디지털 공간에 맵핑하는 것이 문제라면, 그 요구사항과 구현이 유저 입장인지가 중요하다.

레퍼런스
추신 - 이 정도까지 진행하는 데 Full M/M로 대략 1개월(Not including holiday) 걸린 것 같다. 이 정도로도 물리적 공간에 붙착된 데이터를 가상으로 모니터링하고 분석용 데이터를 획득할 수 있다. 이제 좀 더 편리한 인터페이스 구현을 위해, 다음과 같은 부분을 처리한다. 
  • BIM 공간과 그래프 연결. 이벤트 처리
  • IoT 센서 장치를 인터넷에 연결
  • IoT 센서 장치를 각 공간에 설치
  • 모니터링된 데이터에서 이상 패턴 감지를 위한 딥러닝 학습 모듈 개발
여기서, 다른 일을 하며 진행하므로, 개발 효율이 30~50% 정도... 그럼 앞으로 약 두달 정도해야 정리될 듯하다. 시간이 빡빡하기는 하지만, 센서 데이터와 글은 병행해 정리하기로 한다.

추신 - 오토데스크 포지 제공 API가 원하는 모든 기능을 제공하지는 않는다(직접 해봐야 안다). 제공되지 않는 기능은 직접 구현해야 하며, 가끔은 DOM 해킹, 3차원 기하 계산 등을 코딩해야 할 수 있다. 참고로, 이런 노가다 코딩이 인공지능으로 자동화될 수 있다고 착각하지 말자(한참 멀었다). 

2021년 2월 16일 화요일

Arduino nano BLE, Node-red기반 IoT 데이터 MongoDB 저장 및 모니터링하기

이 글은 Arduino nano BLE, Node-red기반 IoT 데이터 MongoDB 저장 및 모니터링하는 방법을 간략히 공유한다. 이 글에서 개발되는 시스템은 nano의 모든 센서 데이터를 획득해, node-red로 전달하고, node-red에서 mongodb로 저장한다. 이 글을 따라하면 다음과 같은 결과를 얻을 수 있다.

mongodb에 저장되면, 이를 통해, 데쉬보드, 오토데스크 포지 뷰어 등에서 모니터링할 수 있을 것이다.

아키텍처 디자인
아키텍처는 간단히 다음과 같이 디자인한다. Forge와 DT app은 이전 블로그에서 개발한 오토데스크 포지 기반 앱을 사용한다.

아두이노 nano sense 코딩
다음과 같이 코딩하고 업로드한다. 업로드가 제대로 되지 않으면, 코드 에러, 나노 보드 회로 연결이 제대로 된 것인지, 연결된 부품이 제대로 동작되는 것인지 확인한다. SSD1306 OLED 및 센서 라이브러리 설치 관련 내용은 이전 블로그 글을 참고한다.
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Arduino_LSM9DS1.h>
#include <Arduino_HTS221.h>
#include <Arduino_LPS22HB.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels

// Declaration for SSD1306 display connected using software SPI (default case):
#define OLED_RESET  -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void setup() {
  Serial.begin(9600);
  Serial.println("Sensing");

  if (!IMU.begin()) {
    Serial.println("Failed to initialize IMU!");
    while (1);
  }
  Serial.print("Gyroscope sample rate = ");
  Serial.print(IMU.gyroscopeSampleRate());
  Serial.println(" Hz");
  Serial.println();
  Serial.println("Gyroscope in degrees/second");
  Serial.println("X\tY\tZ");  

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }

  // Show initial display buffer contents on the screen --
  // the library initializes this with an Adafruit splash screen.
  display.display();
  delay(1000); 

  // Clear the buffer
  display.clearDisplay();
  display.setTextSize(1); // Draw 2X-scale text
  display.setTextColor(SSD1306_WHITE);
  // display.setFont(ArialMT_Plain_10);

  if (!HTS.begin()) {
    Serial.println("Failed to initialize humidity temperature sensor!");
    while (1);
  }
  
  if (!BARO.begin()) {
    Serial.println("Failed to initialize pressure sensor!");
    while (1);
  }
  testdrawline();      // Draw many lines
}

void loop() {
  sensingNano();    
}

void testdrawline() {
  int16_t i;
  display.clearDisplay(); // Clear display buffer

  for(i=0; i<display.width(); i+=4) {
    display.drawLine(0, 0, i, display.height()-1, SSD1306_WHITE);
    display.display(); // Update screen with each newly-drawn line
    delay(1);
  }
  for(i=0; i<display.height(); i+=4) {
    display.drawLine(0, 0, display.width()-1, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(250);
}

void sensingNano(void) {
  display.clearDisplay();

  float temperature = HTS.readTemperature();
  float humidity    = HTS.readHumidity();
  float pressure = BARO.readPressure();

  // print each of the sensor values
  Serial.print(temperature);
  Serial.print(", ");
  Serial.print(humidity);
  Serial.print(", ");
  Serial.print(pressure);
  
  display.setCursor(0, 24);
  String data = String("THP: ") + String(temperature, 1) + ", " + String(humidity, 1) + ", " + String(pressure, 1);
  display.println(data);


  float x, y, z;
  if (IMU.gyroscopeAvailable()) {
    IMU.readGyroscope(x, y, z);

    Serial.print(", ");
    Serial.print(x);
    Serial.print(", ");
    Serial.print(y);
    Serial.print(", ");
    Serial.print(z);

    display.setCursor(0, 0);
    String data = String("G: ") + String(x, 1) + ", " + String(y, 1) + ", " + String(z, 1);
    display.println(data);
  }

  if (IMU.accelerationAvailable()) {
    IMU.readAcceleration(x, y, z);

    Serial.print(", ");
    Serial.print(x);
    Serial.print(", ");
    Serial.print(y);
    Serial.print(", ");
    Serial.print(z);

    display.setCursor(0, 8);
    String data = String("A: ") + String(x, 1) + ", " + String(y, 1) + ", " + String(z, 1);
    display.println(data);    
  }

  if (IMU.magneticFieldAvailable()) {
    IMU.readMagneticField(x, y, z);

    Serial.print(", ");
    Serial.print(x);
    Serial.print(", ");
    Serial.print(y);
    Serial.print(", ");
    Serial.print(z);

    display.setCursor(0, 16);
    String data = String("M: ") + String(x, 1) + ", " + String(y, 1) + ", " + String(z, 1);
    display.println(data);       
  }

  Serial.println(" ");

  display.display();      // Show initial text
  delay(500);
}

시리얼 모니터에서 센서 데이터들이 제대로 출력되면 성공한 것이다.

mongo DB 설정
mongodb를 설치한다. 몽고디비 데이터 저장 폴더를 만들고, 경로를 다음과 같이 지정한다.
mkdir c:\data
cd data
mkdir db
export MONGO_PATH=/usr/local/mongodb
export PATH=$PATH:$MONGO_PATH/bin

node-red 를 실행한다. 노드래드의 팔레트 관리자 메뉴에서 node-red-node-mongodb2 패키지를 설치한다. 
npm install node-red-node-mongodb

몽고디비를 실행한다.
mongod

몽고디비 실행후 MongoDB compass 프로그램에서 sensor 데이터베이스와 data 컬랙션을 생성하거나, 다음과 같이 몽고디비 콘솔창을 실행해 명령을 입력한다(mongo).
use sensor
db.data.insert({sensor: "temp", date: "2021-2.19", value: "20.9"})

node-red와 센서 연결
node-red를 다음과 같이 디자인한다. 시리얼 노드를 더블클릭해, 연결된 아두이노 나노 보드의 COM 포트를 입력한다.
serial 노드를 생성해, function 노드와 연결한다. 그리고, function 노드를 더블클릭해 다음과 같이 코딩한다. function 출력은 12개로 설정한다. 시리얼에서 문자열로 가져온 메시지 데이터는 콤마 형식으로 구분되어 있다. 이를 분리해 배열로 만들고, 실수값으로 변환해 메시지 리스트를 만든다. 이를 리턴한다.

var output = msg.payload.split(",");

var msgList = [];
output.forEach(function(v, index)
{
    var msgIoT = {payload: parseFloat(v)};
    msgList.push(msgIoT);
});

return msgList;


node-red와 MongoDB 연결
디버깅을 위해 msg.payload, text 노드를 생성해 연결한다. mongodb out 노드를 생성해, 함수와 연결한다.

다음과 같이 몽고디비에 저장할 데이터베이스, 유저, 암호 등 연결 정보를 설정한다.


배포를 하면, 다음과 같이 몽고디비에 저장되는 센서 데이터 값을 확인할 수 있다.
show dbs
use sensor
db.data.find()

아울러, 노드레드 데쉬보드에서 다음과 같이 센서값들을 확인할 수 있다.

센서 값에 타임스탬프를 추가하기 위해, 함수를 하나 더 추가하고, 처음 만든 함수의 첫번째 출력과 노드를 연결한다. 함수의 출력은 기존에 만든 몽고디비 노드 입력과 연결한다. 

그리고, 새로 만든 함수는 다음과 같이 코딩한다.
var d = new Date();
var date = d.getFullYear() + '-' + d.getMonth() + '-' 
        + d.getDate() + '-' + d.getHours() + ':' + d.getMinutes()
        + ':' + d.getSeconds();

var newMsg = {
                payload: {
                    'sensor': 'temp',
                    'date' : date,                    
                    'value': msg.payload
                }
            };

return newMsg;

노드레드 프로그램 배포 전에 다음과 같이 mongodb collection data를 모두 삭제한다. 
db.data.remove({})

이제 배포하면, 다음과 같이 몽고디비에 저장된 결과를 확인할 수 있다.

몽고디비에 센서 값들을 이런 방식으로 저장할 수 있다. 참고로, 지금까지 개발된 노드레드 프로그램 코드는 다음과 같다. 참고로, 노드레드 import 메뉴를 이용해, 코드를 로딩할 수 있다.
[{"id":"4e59f461.a56f0c","type":"tab","label":"플로우 1","disabled":false,"info":""},{"id":"63af438.5058bbc","type":"serial in","z":"4e59f461.a56f0c","name":"","serial":"af5253fc.09b97","x":150,"y":160,"wires":[["c62c93a8.af08d","d243c665.c31f28","71fe46c0.dfaa18"]]},{"id":"d243c665.c31f28","type":"debug","z":"4e59f461.a56f0c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":330,"y":60,"wires":[]},{"id":"19e8218a.5b6f8e","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":2,"width":0,"height":0,"name":"s2","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":140,"wires":[]},{"id":"c62c93a8.af08d","type":"function","z":"4e59f461.a56f0c","name":"","func":"var output = msg.payload.split(\",\");\n\nvar msgList = [];\noutput.forEach(function(v, index)\n{\n    var msgIoT = {payload: parseFloat(v)};\n    msgList.push(msgIoT);\n});\n\nreturn msgList;","outputs":12,"noerr":0,"initialize":"","finalize":"","x":350,"y":300,"wires":[["1e8b1389.ebbb5c","eb4101fb.ecc5a"],["19e8218a.5b6f8e","42b4327e.a5e49c"],["fedf546e.b92f38","4156ebd0.2fada4"],["7f000ff8.72b0c"],["7b969ff5.17729"],["723708ed.d0fe68"],["cc30198b.56e498"],["a62c4830.afe078"],["22e28692.dae18a"],["758c4be3.100204"],["3804bcdf.9caf44"],["29d226c9.8c1c4a"]]},{"id":"1e8b1389.ebbb5c","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":1,"width":0,"height":0,"name":"s1","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":100,"wires":[]},{"id":"fedf546e.b92f38","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":3,"width":0,"height":0,"name":"s3","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":180,"wires":[]},{"id":"7f000ff8.72b0c","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":4,"width":0,"height":0,"name":"s4","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":220,"wires":[]},{"id":"7b969ff5.17729","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":5,"width":0,"height":0,"name":"s5","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":260,"wires":[]},{"id":"723708ed.d0fe68","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":6,"width":0,"height":0,"name":"s6","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":300,"wires":[]},{"id":"cc30198b.56e498","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":7,"width":0,"height":0,"name":"s7","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":340,"wires":[]},{"id":"a62c4830.afe078","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":8,"width":0,"height":0,"name":"s8","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":380,"wires":[]},{"id":"22e28692.dae18a","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":9,"width":0,"height":0,"name":"s9","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":420,"wires":[]},{"id":"758c4be3.100204","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":10,"width":0,"height":0,"name":"s10","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":460,"wires":[]},{"id":"3804bcdf.9caf44","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":11,"width":0,"height":0,"name":"s11","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":500,"wires":[]},{"id":"29d226c9.8c1c4a","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":12,"width":0,"height":0,"name":"s12","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":540,"wires":[]},{"id":"71fe46c0.dfaa18","type":"ui_text","z":"4e59f461.a56f0c","group":"c65726f5.12d638","order":1,"width":0,"height":0,"name":"","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":310,"y":100,"wires":[]},{"id":"eb4101fb.ecc5a","type":"function","z":"4e59f461.a56f0c","name":"","func":"var d = new Date();\nvar date = d.getFullYear() + '-' + d.getMonth() + '-' \n        + d.getDate() + '-' + d.getHours() + ':' + d.getMinutes()\n        + ':' + d.getSeconds();\n\nvar newMsg = {\n                payload: {\n                    'sensor': 'temp',\n                    'date' : date,                    \n                    'value': msg.payload\n                }\n            };\n\nreturn newMsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":540,"y":40,"wires":[["5325fecc.91cdc","40d4c998.b69328"]]},{"id":"5325fecc.91cdc","type":"mongodb out","z":"4e59f461.a56f0c","mongodb":"129644e9.2c45fb","name":"","collection":"data","payonly":false,"upsert":false,"multi":false,"operation":"insert","x":750,"y":40,"wires":[]},{"id":"40d4c998.b69328","type":"ui_text","z":"4e59f461.a56f0c","group":"c65726f5.12d638","order":1,"width":0,"height":0,"name":"db","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":730,"y":100,"wires":[]},{"id":"4156ebd0.2fada4","type":"ui_chart","z":"4e59f461.a56f0c","name":"","group":"c65726f5.12d638","order":4,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"x":750,"y":200,"wires":[[]]},{"id":"42b4327e.a5e49c","type":"ui_gauge","z":"4e59f461.a56f0c","name":"","group":"c65726f5.12d638","order":3,"width":0,"height":0,"gtype":"gage","title":"gauge","label":"units","format":"{{value}}","min":0,"max":10,"colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":740,"y":160,"wires":[]},{"id":"af5253fc.09b97","type":"serial-port","serialport":"COM5","serialbaud":"9600","databits":"8","parity":"none","stopbits":"1","waitfor":"","dtr":"none","rts":"none","cts":"none","dsr":"none","newline":"\\n","bin":"false","out":"char","addchar":"","responsetimeout":"10000"},{"id":"2b0b67e5.8c6878","type":"ui_group","name":"Group 1","tab":"ad2f4438.b053a8","order":1,"disp":true,"width":"6","collapse":false},{"id":"c65726f5.12d638","type":"ui_group","name":"Group 2","tab":"ad2f4438.b053a8","order":2,"disp":true,"width":6},{"id":"129644e9.2c45fb","type":"mongodb","hostname":"127.0.0.1","topology":"direct","connectOptions":"","port":"27017","db":"sensor","name":""},{"id":"ad2f4438.b053a8","type":"ui_tab","name":"Tab 1","icon":"dashboard","order":1}]

2021년 2월 11일 목요일

NodeJS, Express, Bootstrap, pug 기반 간단한 웹 서비스 개발 방법

이 글은 NodeJS, Express, Bootstrap, pug 기반 웹 서비스 개발 방법을 이야기한다. 

이 글은 NodeJS, Express, Bootstrap, pug가 무엇인지 알고 있다는 가정에서 진행한다. 각 패키지 설명은 아래 링크를 참고한다.

NodeJS, Express 기반 간단한 웹 페이지 만들기

순서는 nodejs 앱 폴더 구조를 만들고, npm package파일을 생성한다. 그리고, 코딩한다. 터미널에서 다음 절차로 따라한다.

1. 폴더를 다음과 같이 생성

mkdir expressApp
cd expressApp
mkdir views

2. Package.json 파일 생성
{
  "name" : "website-using-express",
  "version" : "0.0.1",
  "scripts" : {
    "start" : "node Server.js"
  },
  "dependencies" : {
    "express" : "latest"
  }
}

3. 패키지 설치
npm install 

4. server.js 코딩. 라우터에서 각 URL마다 해당 웹페이지를 지정함.
var express = require("express");
var app = express();
var router = express.Router();
var path = __dirname + '/views/';

router.use(function (req,res,next) {
  console.log("/" + req.method);
  next();
});

router.get("/",function(req,res){
  res.sendFile(path + "index.html");
});

router.get("/about",function(req,res){
  res.sendFile(path + "about.html");
});

router.get("/contact",function(req,res){
  res.sendFile(path + "contact.html");
});

app.use("/",router);

app.use("*",function(req,res){
  res.sendFile(path + "404.html");
});

app.listen(3000,function(){
  console.log("Live at Port 3000");
});

5. Views 폴더내 index.html 파일 생성. 메뉴바를 navbar스타일로 정의. 텍스트 박스는 jumbotron스타일 사용.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Single page web app using Angularjs</title>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.3/angular.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.3/angular-route.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div>
<div>
<nav class="navbar navbar-inverse" role="navigation" style="padding-left:130px;">
<ul class="nav navbar-nav">
<li class="active"><a href="/">Home<span class="sr-only">(current)</span></a></li>
<li><a href="/about">About us</a></li>
<li><a href="/contact">Contact us</a></li>
</ul>
</nav>
</div>
<br/>
<div class="jumbotron"> <p>
Building information modeling (BIM) is a process supported by various tools, technologies and contracts involving the generation and management of digital ...
</p></div>
</div>
</body>
</html>

6. Views 폴더내 about.html 파일 생성
<html>
  <head>
    <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
  </head>
  <body>
    <div>
    <div>
    <nav class="navbar navbar-inverse" role="navigation" style="padding-left:130px;">
      <ul class="nav navbar-nav">
        <li><a href="/">Home</a></li>
        <li class="active"><a href="/about">About us<span class="sr-only">(current)</span></a></li>
        <li><a href="/contact">Contact us</a></li>
      </ul>
    </nav>
    </div>
    <br/>
    <div class="jumbotron">
      <p>
       it's not a tool or system. In academic, it can be traced back to the 1960s and since then it was researched and developed by various names. <a href='https://sites.google.com/site/bimprinciple/'>BIM principle</a>
      </p>
    </div>
    </div>
  </body>
</html>

7. Views 폴더내 contract.html 파일 생성
<html>
    <head>
        <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
        <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
    </head>
    <body>
        <div>
        <div>
            <nav class="navbar navbar-inverse" role="navigation" style="padding-left:130px;">
            <ul class="nav navbar-nav">
            <li><a href="/">Home</a></li>
            <li><a href="/about">About us</a></li>
            <li class="active"><a href="/contact">Contact us<span class="sr-only">(current)</span></a></li>
            </ul>
            </nav>
        </div>
        <br/>
        <form class="form-horizontal" role="form" style="width: 50%;">
            <div class="form-group">
                <label for="name" class="col-sm-2 control-label">Name</label>
                <div class="col-sm-10">
                <input type="text" class="form-control" id="name" name="name" placeholder="First & Last Name" value="">
                </div>
            </div>
            <div class="form-group">
                <label for="email" class="col-sm-2 control-label">Email</label>
                <div class="col-sm-10">
                <input type="email" class="form-control" id="email" name="email" placeholder="example@domain.com" value="">
                </div>
            </div>
            <div class="form-group">
                <label for="message" class="col-sm-2 control-label">Message</label>
                <div class="col-sm-10">
                <textarea class="form-control" rows="4" name="message"></textarea>
                </div>
            </div>
            <div class="form-group">
                <label for="human" class="col-sm-2 control-label">3D + time = ?</label>
                <div class="col-sm-10">
                <input type="text" class="form-control" id="human" name="human" placeholder="Your Answer">
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-10 col-sm-offset-2">
                <input id="submit" name="submit" type="submit" value="Send" class="btn btn-primary">
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-10 col-sm-offset-2">
                <! Will be used to display an alert to the user>
                </div>
            </div>
        </form>
        </div>
    </body>
</html>

8. Views 폴더내 404.html 파일 생성
<html>
<head>
</head>
<body>
<h3>
Wrong Pages.
</h3>
</body>
</html>

9. 다음과 같이 실행하고, http://localhost:3000 에 접속한다. 화면이 보이면 성공한것이다.
npm start


PUG기반 BIM 카탈로그 블로그 만들기
앞에서 작업했던 Express, Bootstrap에 PUG를 추가한 BIM 카탈로그 블로그를 만들어본다(github code download).

1. 폴더 생성
mkdir catBIM
cd catBIM
mkdir views
cd views
mkdir includes
mkdir mixins
cd ..

2. package.json 파일 생성
{
  "name": "BIM catalog",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"    
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.21.1",
    "express": "^4.17.1",
    "pug": "^3.0.0"
  }
}

3. 패키지 설치
npm init -y
npm install express pug axios

4. index.js 코딩
const express = require("express");
const axios = require("axios");
const app = express();

app.set("view engine", "pug");  // view engine 으로 pug 형식 지정.

app.get("/", async (req, res) => {
  const query = await axios.get("https://randomuser.me/api/?results=9");
  res.render("index", { users: query.data.results });
});

const PORT = 3000;  // 서버 실행
app.listen(PORT, () => {
  console.log(`Listening on port ${PORT}...`);
});

5. views/index.pug 코딩
extends layout.pug
include mixins/_thumbCard

block content
  .album.py-5.bg-light
    .container
      .row
        each user in users
          +thumbCard(user)

6. views/layout.pug 코딩
doctype html
html
  head
    title BIM Example
    link(rel='stylesheet' href='https://getbootstrap.com/docs/4.4/dist/css/bootstrap.min.css')
    style.
      .bd-placeholder-img {
        font-size: 1.125rem;
        text-anchor: middle;
        -webkit-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
      }

      footer {
        padding-top: 3rem;
        padding-bottom: 3rem;
      }

      footer p {
        margin-bottom: .25rem;
      }

      @media (min-width: 768px) {
        .bd-placeholder-img-lg {
          font-size: 3.5rem;
        }
      }

  body
    include includes/header.pug

    main#main
      include includes/jumbotron.pug

      block content

    include includes/footer.pug
    
    script(src='https://code.jquery.com/jquery-3.4.1.slim.min.js')
    script(src='https://getbootstrap.com/docs/4.4/dist/js/bootstrap.bundle.min.js')

7. views/mixins/_thumbCard.pug 코딩
mixin thumbCard(user)
  .col-md-4
    .card.mb-4.shadow-sm
      svg.bd-placeholder-img.card-img-top&attributes({"width": "100%"}, {"height": "30"}, {"focusable": "false"}, {"role": "img"})
        title #{user.name.first} #{user.name.last}
        rect(width="100%" height="100%" fill="#55595c")
        text(x="50%" y="50%" fill="#eceeef" dy=".3em") #{user.name.first} #{user.name.last}
        img.ui.mini.rounded.image(src='https://sites.google.com/site/bimprinciple/_/rsrc/1355538518737/in-the-news/bimforum-bimtofmlaxiyagi/LAX2.PNG?height=200&width=400')
      .card-body
        .card-text #{user.location.street.number} #{user.location.street.name}
        .card-text #{user.location.city}, #{user.location.state} #{user.location.postcode}
        .card-text.mb-4 #{user.location.country}
        .d-flex.justify-content-between.align-items-center
          .btn-group
            button.btn.btn-sm.btn-outline-secondary View
            button.btn.btn-sm.btn-outline-secondary Edit
          small.text-muted 9 mins

10. views/includes/header.pug 코딩
header
  .collapse.bg-dark#navbarHeader
    .container
      .row
        .col-sm-8.col-md-7.py-4
          h4.text-white Hidden Content
          p.text-muted Until now, we look at one side of 2-dimension framework force in the various engineering information which has been designed. Consequently, the information in the framework is transformed from the original design concept.
        .col-sm-4.offset-md-1.py-4
          h4.text-white Contact
          ul.list-unstyled
            li
              a.text-white(href='#') Facebook
            li
              a.text-white(href='#') Twitter
            li
              a.text-white(href='#') Instagram
  nav.navbar.navbar-dark.bg-dark.shadow-sm
    .container.d-flex.justify-content-between
      a.navbar-brand.d-flex.align-items-center
        //- Including the svg logo
        include logo.svg
        strong BIM Example
      button.navbar-toggler.collapsed&attributes({'data-toggle': 'collapse'}, {'data-target': '#navbarHeader'})
        span.navbar-toggler-icon

11. views/includes/jumbotron.pug 코딩
section.jumbotron.text-center
    .container
        h1 BIM Example
        p.lead.text-muted The fact that the concept of BIM is now new. Of course, it's not a tool or system. In academic, it can be traced back to the 1960s and since then it was researched and developed by various names.
        p
          a.btn.btn-primary.m-2(href="#") Main call to action
          a.btn.btn-secondary.m-2(href="#") Secondary action

12. views/includes/footer.pug 코딩
footer.text-muted
  .container
    p.float-right
      a(href="#") Back to top
    p BIM example is © Bootstrap, but please download and customize it for yourself!
    p New to Bootstrap? #[a(href="https://getbootstrap.com/") Visit the homepage] or read our #[a(href="https://getbootstrap.com/docs/4.4/getting-started/introduction/") getting started guide].

13. 웹서버 실행
npm start

localhost:3000에 접속하면 다음 화면을 볼 수 있다.


레퍼런스