2021년 2월 23일 화요일

MongoDB와 Autodesk Forge 연결하기

이 글은 MongoDB와 Autodesk Forge 연결하는 방법에 대한 간단한 설명이다. 이와 관련된 다른 내용은 앞의 블로그 글을 참고한다. 이 글은 node.js 환경에서 실행된다.

준비
mongodb 패키지를 설치한다.
npm install mongoose

MongoDB 연결
start.js 자바스크립트 코드에 아래 명령을 추가한다.
const mongoose = require('mongoose');

다음과 같이 DB와 연결한다. URL에서 sensor는 데이터베이스 이름이다. 본인이 만든 DB이름을 여기에 입력한다.

var url = "mongodb://localhost:27017/sensor";  // user:pwd@localhost:
try {
await mongoose.connect(url);
console.log("Connected to Database");
} catch(err) {
console.log(err);
}

MongoDB CRUD Restful API 서버 개발

Dashboard 개발


MongoDB API와 Autodesk Forge 연결

레퍼런스

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에 접속하면 다음 화면을 볼 수 있다.


레퍼런스

2021년 2월 4일 목요일

방문 교수(Visiting Scholar) 미국 생활 준비 사항 및 절차

이 글은 Visiting Scholar 미국 생활 준비 사항 및 순서에 대한 글이다. 준비 사항은 크게 국내외 해외로 나뉜다. 2021년 COVID 상황을 고려해, 준비물을 함께 정리한다.

UNF 파견 기관

국내

공동 연구 내용, 파견 기관 초청 레터 및 관련 서류, 가족과 합의 및 비자 문제 정리되었다는 가정하에 다음을 준비한다. 이 과정은 짧게는 두달, 길게는 몇개월 걸린다.

1. 파견될 곳 근처 집 렌트: Zillow등을 통해 최소 두달전에 알아봐야 한다. 해외 파견지에는 신용이 없는 상태이므로 원하는 집 구하기 쉽지 않을 수 있다. 이 경우, 미리 선불을 주거나 렌트비를 미리 더 낼 마음이 있어야 한다. 현지 부동산 중계인을 통하는 경우가 좋다. 

2. 항공권 구입: 가능한 두달전에 항공권을 구입한다. 

3. 은행 해외 계좌 개설 및 달러 환전: 거래 은행에서 해외 거주자 확인 및 주거래자 처리. 해외에서 본인 통장과 연결해 이체 가능하도록 처리. 해외 집 렌트, 차량 구입, 보험, 세금 등 고려 1억 이상 준비(1인 5만달러 한도. 상황에 따라 1-2억 정도 이체 가능).

4. 차량 렌트: 가기 전에 파견지에서 몰고다닐 차를 렌트한다. 렌트 기간은 상황에 따라 다르나, 파견 기관에 오가는 데 거리가 있다면, 결국 차량을 구입해야 하므로, 구량할 때까지 렌트차를 몰고다닐 수 있는 기간 정도로 해야 한다. 본인의 경우 7일을 렌트하였다.

5. 현지 가족보험: 현대, 한화 보험 등 연락해 해외 체류자 신분 가족 보험에 가입. COVID 이슈 발생시 처리되는 보험으로 가입할 것.

6. 로밍할 휴대폰 처리: 해외에서 로밍 문자 및 전화 필요하면 별도 폰 준비 필요. 미국에서 휴대폰 구입할 예정 아니면, 국내에서 중고폰 구입해 가져감. 해외 전화 개통될때까지 사용할 유심칩 미리 구입

7. 아마존 프라임 가입: 프라임 가입해 렌트한 집에 필요한 가구 등을 미리 구입해 배송시키는 것이 가급적 편함. 아파트 렌트일 경우 가구 내장된 경우가 많아 이럴 필요는 없음.

8. 각종 서류 준비: 파견지에 집 렌트, 세금 처리, 아이들 학교 처리를 위해서, 국내에서 미리 준비해야 할 서류가 있다. 이는 반듯이 파견지까지 원본을 가져가야 한다. 미국 관련 기관에서는 원본 서류를 체크한다.

1) 여권, DS2019 및 비자

2) 파견 기관 초청 레터 등 원본 서류(가끔 해외 파견지 정부 기관 일볼때 확인함)

3) 자녀 영문 성적서 필요. 기타 자녀 생기부 기록 등 참고로 가져감(영문. 필요시 중요 부분은 번역할 것)

4) 자녀 백신 접종 기록. 보건서에 요청(학교 등록시 필수)

5) 해외 집 렌트 계약서(해외 유틸리티 계정 개설에 필수)

6) 한국 운전면허증(해외 운전면허증 발급시 필수)

7) 영문 가족관계증명서

8) 기타, 기관 파견 관련 원본 서류들

9. 국내 짐 정리. 국내 불필요한 통신, 세금, 유틸리티 해지처리. 국내 우편물 처리 방안 마련

10. 가족과 연락 방법 등 미리 준비 및 인사: 주로 카톡 전화 사용. 로밍폰 가져감

11. 짐싸기. 보통 1인 2개 수화물 가능. 해외 렌트한 집에 짐 붙일 수 있으면, 미리 처리함. 

12. COVID 준비물: 현지 상황 알아보고 미리 마스크, 필수적인 의약품 준비. 가족 중 투약해야할 약이 있다면 미리 병원에서 상황 설명 후 해외 통관시 증빙할 사유적힌 서류 요청 및 필참. 현지 파견지역이 COVID 검사 증빙있어야 하면 미리 예약 및 검사 후 서류 준비.

13. 임시 숙소 예약: 만약, 집 렌트 절차가 덜 끝났다면, 임시 거처로 호텔이나 AirBnB 예약해야 함. 참고로, 일반 주택은 가구 전혀 없고, 유틸리티 처리 전이라 수도 및 전기가 안들어오는 상태임을 감안해야 함 

해외 미국

미국에 도착하면, 바로 해야 할 것이 있다. 이 과정은 짧게 2주, 평균 한달, 길게는 두세달 걸린다. 일 처리 시 구글맵으로 관련 기관, 회사 연락처 확인 후 전화하여 예약 유무 확인하고 가거나 인터넷 홈페이지 예약 처리하고 가야 한다(그냥 가면 낭패. 특히, 지금은 COVID 상황임. 미국은 모든 업무 처리가 전화, 메일, 인터넷이다). 

1. 해외 렌트 차 획득. 짐 싣고 숙소로 이동 

2. 유틸리티 업체에 계좌개설 및 비용처리: 수도, 전기 등 유틸리티 서비스 개설해야 함. 관할 기관 확인 후 처리 필요. 이 경우, 집 렌트 계약서가 필요함. 집 주소 확인.

3. 현지 은행 계좌 개설 및 현금 카드 만들기: 여기서 백지수표(BLANK CHECK)도 5장 정도 미리 받음. 차량 구입 등 목적으로 백지수표 발행 요청해야 함.

4. 통신 및 인터넷 개설: AT&T와 같은 통신망 서비스 업체 방문 후 모바일폰 및 인터넷 개설. 

5. 운전 면허 처리: 미국에서는 차량이 없으면, 생활하기 어려움. 차량 구입을 위해서는 운전 면허 처리해야 함. 운전 면허 처리를 위해 DMV(Department of Motor Vehicles) 방문. 앞서 언급한 1), 5), 6) 원본 서류 필요.

6. 렌트 집 방문 및 집 키 받음: 방문 시 렌트한 차량 이용. 아파트일 경우 가구가 비치되어 있으나, 단독 주택일 경우 실내 잠자리 등 전혀 없으므로, 미리 아마존에서 메모리폼 매트릭스 및 침구 구입해 놓을 것.

7. 차량 구입: 파견 기관 출퇴근, 마켓 생필품 구매 및 자녀 픽업용으로 차량 필요. 운전면허증 필요. 차량 보험처리 해야함. 차량 보험처리를 위해 현금 카드 사용. 차량 구입은 CARMAX에서 진행. 차량 구입비는 백지수표 미리 준비.

8. 자녀 학교 등록: 앞서 준비한 서류 원본 1) 3), 4), 5) 필요함. 현지 백신 접종 이력 공증할 병원, 기관이나 업체(Carespot 등)가서 한국에서 가져온 백신 접종 이력 제출해 공증 받음. 성적표는 영문으로 전달해야 함(한글일 경우 번역 필요). 학교에 관련 서류 제출. COVID 상황에서 온라인으로 전환한 학교 많음. 전화로 미리 관련 내용 설명한 후 온라인으로 서류 제출하였음. 자녀 학교 등록 시 등록 학년을 명시해 알려주야 함. 해당 지역 교육청에서 학군에 따라 학교 자동 배정됨. 이슈가 있다면, 해당 교육청 담당자와 통화후 메일로 처리해야 함.

9. 파견 기관 입국 신고,시설물 출입 카드 획득 및 주차 티켓 구매: 파견 기관에 출입 및 파킹하려면, N-number(Network number)를 획득하고 주차 티켓을 구매해야 함.

10. 가구 및 생필품 구입: 가구는 이케아, 생필품은 월마트나 코스트코에서 구입. 중고품도 권장. 관련 가계 및 업체는 google map에서 검색하면 다 찾을 수 있음.

기타, 시간되면 Social number를 해당 지역 관공서에 가서 신청한다. 만약, 현지 운전면허증 발급 받았다면 굳이 안해도 된다. 참고로, 운전면허증이나 Social number 둘중은 하나가 있어야 현지에서 차량 구입, 코스트코 카드 구매 등 모든 일이 진행된다.

사전 준비해야할 원본 서류들은 매우 중요해, 해외에서 없을 경우 일처리 안되며, 이때는 비싼 배송료로 Fedex 등 해외 우편 서비스 사용해야 할 수도 있다. 이런 경우, 대비해 중요한 일처리가 있을 경우, 국내 가족들에게 본인 대리해 관공서 등 업무 처리할 수 있도록 준비해 놓고, 해외 출국하는 것도 좋다. 

이 정도 준비하면, 시행착오로 인한 불편과 시간 낭비를 막을 수 있을 것이다. 바빠서 직접 준비하기 어렵다면, 인터넷에서 관련 대행 서비스 업체들을 활용하는 것이 좋다. 주변 지인들에게 도움을 받아도, 본인이 바쁜 경우 모든 것을 대응하기 어렵다. 이 경우, 돈을 좀 쓰는 것이 훨씬 낫다.

파견 기관 COVID 프로토콜
캠퍼스 모습
도서관

Autodesk Forge Digital Twin 코드 분석 및 UML 구조 역설계

이 글은 Autodesk Forge Digital Twin 소스 예제를 간단히 분석한다. 쉬운 이해를 위해, 구조와 소스코드를 먼저 분석하고, 이를 UML로 표현해 구조를 역설계 한다. 이를 통해, 실무적 관점에서 플랫폼 기반 디지털 트윈 서비스 개발 절차 및 방법, 고려사항 등을 확인해 봅니다.

개요
이 프로그램은 디지털 트윈의 특징 중 물리모델과 디지털모델의 연결, 실시간 IoT 센서 정보 반영, 시뮬레이션, 클라우드와 같은 기능을 구현한 것이다. 오토데스크 포지에 대한 설명은 다음 링크를 참고한다.
오토데스크 포지 디지털트윈 소프트웨어 아키텍처 분석 방법은 먼저 소스코드와 GUI 뷰를 분석한 후, 객체 및 클래스 정적 구조, 시퀀스 동적 구조를 역설계하는 순서로 진행한다. 

컴포넌트 구조 및 실행 방법
이 소프트웨어는 Autodesk Forge, Node.JS 자바스크립트 런타임 플랫폼, express 웹서버, Bootstrap UI 테마, MongoDB 데이터베이스를 사용하고 있다.

Autodesk Forge에 가입하고, 앞에 언급된 컴포넌트를 모두 설치한 후 다음 명령을 실행해 웹서버를 구동한다. 
git clone https://github.com/Autodesk-Forge/forge-digital-twin
npm install
export FORGE_CLIENT_ID=<client-id>
export FORGE_CLIENT_SECRET=<client-secret>
export FORGE_MODEL_URN=<model-urn>
export MONGODB_URL=<mongodb-connection-string>
npm start

3차원 디지털 모델은 아래 예제를 다운받아 SVF포맷으로 변환한 후 앞의 FORGE_MODEL_URN에 URN형식으로 경로를 제공한다.
View 구조 분석
HTML 뷰 렌더링 구조는 index.pug 에서 시작한다. 참고로, PUG(Python User Group)는 파이썬과 유사한 문법으로 HTML을 기술하는 HTML 렌더링 템플릿 미들웨어로 HTML을 간략한 문법으로 정의하고, 데이터베이스 객체와 바인딩을 쉽게 처리한다.
pug파일은 views 폴더에 저장되어 있다. 해당 pug 뷰를 분석하면 다음과 같은 웹 레이아웃을 구성하고 있는 것을 알 수 있다.
뷰 레이아웃 구조 분석

각 뷰를 정의한 pug는 script 함수를 통해 js 코드를 실행하게 되고, 실행된 js 코드에 의해 database 객체 데이터가 획득되어, 화면에 데이터를 출력한다. 비지니스 로직 최초 진입 부분은 main.js 의 Autodesk.Viewing.Initializer() 콜백 함수이고, 여기서 3차원 모델 로딩, 데이터 로딩 및 렌더링, 이벤트 등록 함수 호출이 실행된다.

소프트웨어 아키텍처 역설계
1. 정적구조 역설계
소스코드 분석 내용을 바탕으로 구조를 UML로 역설계한다. 주요 구조는 다음과 같다. 전형적인 MVC 디자인 패턴 구조이다. 
Digital Twin MVC 구조

아래와 같이 DATABASE는 MONGODB 스키마에서 객체 속성값을 로드하고 필요한 곳에 제공하는 역할을 한다. 
데이터베이스 model 구조

script 는 비지니스 로직 역할을 수행한다. 데이터베이스와 UI를 중간에서 연결하며, CRUD에 대한 로직을 처리한다.
script 구조

routes는 URL에 따른 페이지 뷰를 렌더링할 때 라우팅하는 역할을 한다.
routes 구조

2. 동적구조 역설계
각 컴포넌트와 객체가 서로간에 어떤 순서로 호출을 처리하는 지 동적구조를 역설계한다. 호출방식은 Node.js 및 웹 특성상 웹서버나 미들웨어가 이벤트 기반 실행 처리하는 구조이므로, 직접적으로 드러나 있지는 않다. 분석 시 프로토콜대로 약속된 이벤트에서 호출된다는 것을 고려한다.

유스케이스 구조는 다음과 같다. 
유스케이스 구조

user가 login하면, authorize하고, 웹서버 실행해 접속을 listen한다. 접속되면, node.js 의 라우터가 index.pug를 렌더링한다. index는 header, viewer, sidebar, footer pug파일을 포함한다. viewer는 3D 모델(예. 기계, 건축, 건설, BIM 모델 파일 등)을 렌더링하는 3D viewer를 포함한다. sidebar는 데이터 계층구조를 나타내는 performance, maintenance 등을 렌더링한다. 도킹되는 패널은 footer.pug에서 포함된 main.js, performance.js, maintenance.js 등을 포함한다. 데이터는 database를 통해 fetch하여 가져온다. 이 중에 주요 시퀀스를 분석해 보자.

login의 server.js 시퀀스: express의 set함수로 웹페이지 렌더링 뷰는 pug로 설정하고, 라우팅을 위해, 기본 페이지 루트('/')를 index 페이지로 설정한다. 이외, 인증 페이지, 구매 및 유지보수 뷰 페이지 라우팅 URL을 설정하고, 서버를 실행(listen)한다. 다음 UML 시퀀스 구조는 이를 표현한다. 

main.js 시퀀스. 이 시퀀스는 다음과 같이 Autodesk 3차원 모델 뷰어를 생성하고 옵션을 설정한다. main.js의 실행 진입점은 Autodesk.Viewing.Initializer() callback 함수이다. 여기서, 모델, 데이터베이스 CRUD를 위한 함수 호출 및 이벤트 등록이 실행된다.

maintenance.js 시퀀스. 이 시퀀스는 탭 패널에 출력할 데이터를 얻어 값을 설정하고, click, active 등 이벤트가 발생되면, 다시 데이터를 업데이트하는 적절한 함수를 실행한다.  

나머지, performance.js 등도 이와 유사하게 동작한다.

소스 구조 분석
github 폴더 구조는 다음과 같다. 웹서버 시작은 server.js 코드에서 시작된다. 주요 소스는 model, views, public/script 폴더내의 /routes/auth.js, /routes/index.js, /routes/maintenance.js, /routes/procurement.js, /script/main.js, /script/maintenance.js, /script/performance.js, /script/procurement.js 이다.

server.js 에서 클라이언트 접속 listen 대기 후 로그인되면 웹페이지 파일 index.pug 이 렌더링되고, 이 파일에 포함된 header, viewer, footer등 pug파일이 렌더링된다. footer는 실행될 자바스크립트를 포함시켜 인터페이스에 필요한 모델, 데이터 로딩 함수를 호출한다. 다음은 그 주요 코드이다. 
// footer.pug
...
script(src='/scripts/performance.js')
script(src='/scripts/maintenance.js')
...

// main.js
Autodesk.Viewing.Initializer(options, () => {
    mainViewer = new Autodesk.Viewing.Private.GuiViewer3D();
    mainViewer.start();
    loadModel(DEMO_URN /* set by the server-side template engine */);
});

function loadModel(urn) {
    return new Promise(function(resolve, reject) {
    ...
                    initPerformanceTab(mainViewer);
                    initMaintenanceTab(mainViewer);
                    initProcurementTab(mainViewer);
                    initViewer(mainViewer);
                    resolve();
                })
    ...
    });
}

// performance.js
...
function initPerformanceTab(mainViewer) {
...
}

function createEngineSpeedChart() {
        const chart = new Chart(ctx, {
        }
}

server.js 분석
const express = require('express');  // 라이브러리 모듈 가져오기
const path = require('path');
const helmet = require('helmet');
const morgan = require('morgan');

const { FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, FORGE_MODEL_URN, MONGODB_URL } = process.env;  // 환경변수 설정

const db = require('./model/db');   // DB 설정
const app = express();

app.use('/', require('./routes/index'));   // 웹서버 라우팅 주소 설정
app.use('/api/auth', require('./routes/auth'));
app.use('/api/procurement', require('./routes/procurement'));
app.use('/api/maintenance', require('./routes/maintenance'));

const port = process.env.PORT || 3000;
db.connect()
  .then(() => app.listen(port, () => console.log(`Server listening on port ${port}`))) // 서버실행

routes/auth.js 분석
let router = express.Router();
let auth = new AuthenticationClient(process.env.FORGE_CLIENT_ID, process.env.FORGE_CLIENT_SECRET);  // Forge 인증

router.get('/token', async function(req, res, next) {
    try {
        const result = await auth.authenticate(['viewables:read']);
        res.json({ access_token: result });
    } 
});

routes/index.js 분석
router.get('/', function(req, res) {
    res.render('index', { analytics: process.env.GOOGLE_ANALYTICS_TAG, urn: process.env.FORGE_MODEL_URN });
});

routes/maintenance.js 분석
const { Part, Review, Issue } = require('../model/maintenance');  // 유지보수 부품, 리뷰, 이슈 객체 생성
const PartTableLimit = 2048; // note: the jet engine model has cca 1600 parts
const ReviewTableLimit = 256;
const IssueTableLimit = 256;

function countParts() {
    return new Promise(function(resolve, reject) {
        Part.count({}, (err, count) => {        });  // 부품 갯수 
    });
}

function countReviews() {
    return new Promise(function(resolve, reject) {
        Review.count({}, (err, count) => {     });  // 검토 갯수
    });
}

function countIssues() {
    return new Promise(function(resolve, reject) {
        Issue.count({}, (err, count) => { }  // 이슈 갯수
        });
    });
}

function findOrCreatePart(id) {
    return new Promise(function(resolve, reject) {
        Part.findOne({ id }, (err, part) => {
                resolve(part);  // 부품 검색
            }
        });
    });
}

router.get('/issues', function(req, res) {
    let query = {};
    Issue.find(query, (err, issues) => {
            res.json(issues);  // 이슈 검색
        }
    });
});

view/index.pug
pug 형식으로 HTML view 정의하였다. 
doctype html
head
  include partials/head.pug
#container
  #header
    img(src='/images/forge-logo-gray.png' alt='Autodesk Forge Digital Twin Demo')
    h Digital Twin Demo
  #viewer
  #sidebar
    include partials/sidebar.pug
  include partials/footer.pug

view/maintenance.pug
ul#maintenance-tabs.nav.nav-tabs.mb-3.nav-fill(role='tablist')
  li.nav-item
    a#maintenance-revisions-tab.nav-link.active(data-toggle='tab' href='#maintenance-revisions' role='tab' aria-controls='maintenance-revisions' aria-selected='true') Revisions
  li.nav-item
    a#maintenance-issues-tab.nav-link(data-toggle='tab' href='#maintenance-issues' role='tab' aria-controls='maintenance-issues' aria-selected='false') Issues
  li.nav-item
    a#maintenance-statistics-tab.nav-link(data-toggle='tab' href='#maintenance-statistics' role='tab' aria-controls='maintenance-statistics' aria-selected='false') Statistics
  li.nav-item
    a#maintenance-instructions-tab.nav-link(data-toggle='tab' href='#maintenance-instructions' role='tab' aria-controls='maintenance-instructions' aria-selected='false') Docs
#maintenance-tabs-content.tab-content
  #maintenance-revisions.tab-pane.fade.show.active(role='tabpanel' aria-labelledby='maintenance-revisions-tab')
    include ./maintenance-revisions.pug
  #maintenance-issues.tab-pane.fade(role='tabpanel' aria-labelledby='maintenance-issues-tab')
    include ./maintenance-issues.pug
  #maintenance-statistics.tab-pane.fade(role='tabpanel' aria-labelledby='maintenance-statistics-tab')
    include ./maintenance-stats.pug    
  #maintenance-instructions.tab-pane.fade(role='tabpanel' aria-labelledby='maintenance-instructions-tab')
    include ./maintenance-instructions.pug   

public/scripts/main.js 분석
let mainViewer = null;
Autodesk.Viewing.Initializer(options, () => {
    mainViewer = new Autodesk.Viewing.Private.GuiViewer3D(
        document.getElementById('viewer'),
        { extensions: ['HeatmapExtension', 'IssuesExtension', 'AnimationExtension'] }
    );
    mainViewer.start();  // 뷰어 시작
    loadModel(DEMO_URN /* set by the server-side template engine */);  // 모델 로딩
});

function loadModel(urn) {
    return new Promise(function(resolve, reject) {
        function onDocumentLoadSuccess(doc) {
            const node = doc.getRoot().getDefaultGeometry();
            mainViewer.loadDocumentNode(doc, node);
        }
        Autodesk.Viewing.Document.load('urn:' + urn, onDocumentLoadSuccess, onDocumentLoadFailure);  // 모델 로딩
    });
}

function initViewer(viewer) {
    viewer.setQualityLevel(/* ambient shadows */ false, /* antialiasing */ true);  // 뷰어 옵션 설정
    viewer.setGroundShadow(true);
    viewer.setGroundReflection(false);
    viewer.setGhosting(true);
    viewer.setEnvMapBackground(true);
    viewer.setLightPreset(5);
    viewer.setSelectionColor(new THREE.Color(0xEBB30B));
}

public/scripts/maintenance.js 분석
function initMaintenanceTab(mainViewer) {
    let maintenanceViewer = null;

    async function updateRevisions(reload, partIds) {
        updateRevisionsTable();
        updateRevisionsPagination();
    }

    function updateRevisionsTable() // 리비전 테이블 화면 갱신 
    {
        for (let i = currPage * pageSize; i < (currPage + 1) * pageSize && i < list.length; i++) {
            const review = list[i];
            const date = new Date(review.createdAt);
            const $row = $(`
                <tr>
                    <th scope="row">${date.toLocaleDateString()}</th>
                    <td><a href="#" class="part-link">${review.partId}</a></td>
                    <td>${review.author}</td>
                    <td style="color: ${review.passed ? 'green' : 'red'};">${review.passed ? 'Good' : 'Bad'}</td>
                </tr>
            `);
            $tbody.append($row);
        }
    }

    function updateIssuesTable() // 이슈 테이블 화면 갱신 
    {
        for (let i = currPage * pageSize; i < (currPage + 1) * pageSize && i < list.length; i++) {
            const issue = list[i];
            const date = new Date(issue.createdAt);
            const $row = $(`
                <tr>
                    <th scope="row">${date.toLocaleDateString()}</th>
                    <td><a href="#" class="part-link">${issue.partId}</a></td>
                    <td>${issue.author}</td>
                    <td>${issue.text}</td>
                </tr>
            `);
            $tbody.append($row);
        }
    }
}

model/db.js 분석
const mongoose = require('mongoose');  // 몽고 디비 획득

class Database {
    async connect() { 
        try {
            await mongoose.connect(process.env.MONGODB_URL);  // 몽고 디비 연결
        } catch(err) {
        }
    }
}

module.exports = new Database();

model/maintenance.js 분석
const mongoose = require('mongoose');
const validator = require('validator');

// 부품 스키마 정의
const partSchema = new mongoose.Schema({
    id: {
        type: Number,
        required: true
    },
    status: {
        type: String,
        validate: validatePartStatus
    },
    nextReview: {
        type: Date
    }
});

// 리뷰 스키마 정의
const reviewSchema = new mongoose.Schema({
    createdAt: {
        type: Date,
        required: true
    },
    author: {
        type: String,
        required: true,
        validate: validateAuthor
    },
    passed: {
        type: Boolean,
        required: true
    },
    description: {
        type: String,
        validate: validateRevisionDescription
    },
    partId: {
        type: Number,
        required: true
    }
});

module.exports = {
    Part: mongoose.model('Part', partSchema),
    Review: mongoose.model('Review', reviewSchema),
    Issue: mongoose.model('Issue', issueSchema)
};

마무리
Autodesk Forge Digital Twin 웹 서비스는 전형적인 MVC(Model View Control) 디자인 패턴 구조로, 각 폴더의 model, views, public/script는 각각 MVC에 대응된다. 

시퀀스를 보면, start.js는 라우팅 설정을 하고, 웹서버를 우선 실행한다. routes는 인터넷 주소 입력 시 라우팅된 각 뷰 페이지를 redirect해서 렌더링되게 한다. model에서 DB to Object 맵핑을 처리하고, 맵핑된 object를 통해 view에 내용을 렌더링한다. view의 이벤트는 scripts에서 받아 처리한다. 

결론적으로 Autodesk Forge 기반 디지털 트윈 서비스 개발 순서는 다음과 같다.
1. 이해당자사, 요구사항, 유스케이스 및 아키텍처 설계. 예) SRS, BEP, SAD, UML, 의사코드 등
2. 디지털 모델과 데이터베이스 정의, 설계 및 개발. 예) DB Schema, BIM modeling 등 
3. 디지털 데이터 소스 준비. 예) IoT, RFID, Sensor, Agent 등
4. 웹서버 HW 및 SW 컴포넌트 준비. 예) 서버, Node.JS, DB, Express, Dashboard, ETL 등 
5. Autodesk Forge App 생성
6. GUI 뷰 설계. 예) HTML, CSS, PUG 등
7. 비지니스 로직 코딩
1) 모델 로딩 함수
2) 데이터 로딩 및 GUI 출력 함수
3) 데이터 CRUD 함수
4) 각종 이벤트 처리 함수
8. DNS 설정 및 IP 연결
9. 서비스 배포 및 운영

관련 소스는 뷰어를 조작하는 Forge API 사용방법을 충분히 보여주고 있어, 좋은 예제 역할을 하고 있다. 이제, 이를 이용해 BIM 모델, 데이터베이스와 IoT 데이터를 서로 연결해 보면 되겠다.

부록: Forge HTML DOM 구조 분석 및 Forge 뷰어 레이아웃 조정
3차원 모델에 데쉬보드를 추가하기 위해, Forge 화면 DOM 레이아웃 구조를 수정해야 한다. 개발 목적에 따라 모델과 데쉬보드 형식은 모두 다르므로, 오토데스크에서 제공하는 템플릿 코드를 그대로 사용할 수가 없다. 이런 이유로 Forge HTML DOM 구조 분석이 필요하다. 기본 구조는 다음과 같다. 
    
DOM HTML layout

DOM element는 화면에 렌더링될 때 해당 웹페이지에 지정된 css, style에 따라 표시된다. 참고로, html페이지에 설정된 meta, link, script는 다음과 같이 순서 의존 관계가 있으므로, 순서가 달라지면 제대로 표시되지 않을 수 있다.

Revit 3D viewer 옆에 차트를 만들고, 아래에 표를 배치하기 위해, Bootstrap layout 규약에 맞게 class에 row와 col을 적절히 지정한 후, 포지 뷰어가 생성되면 dashboard element안에 차트 canvas를 스크립트에서 생성해 주었다.
     <div class="container-fluid fill">
        <div class="row fill">               
            <div class="col-sm-7 fill">          
                <div id="forgeViewer"> 
                </div>
            </div>   
            <div class="col-sm-12 fill">
                <h4>IoT dataset</h4>
                <div class="table-responsive">
                    <table></table>
                </div>
            </div>
        </div>
     </div>

     <script>
        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>');
     </script> 

하지만, 결과는 제대로 처리되지 않는다. 이유는 지정된 viewer div 엘리먼트 안에서 adsk-viewing-viewer를 Forge가 생성할 때, 스타일을 강제적으로 height, width를 100%로 설정해 버리기 때문이다. 크롬 디버깅을 통해 화면 DOM의 해당 element 스타일을 확인해 보면 다음과 같이, css나 style을 외부에서 정의해 주어도 초기화되는 것을 알 수 있다. 

이런 이유로, 자바스크립트에서 다음과 같이 forge 뷰어가 생성된 후, 해당 element에 강제적으로 높이와 폭을 지정해준다.
    var style3D = "height: 60%; width: 65%; overflow: hidden;";
    $('.adsk-viewing-viewer').attr('style', style3D);

그럼, 다음과 같이 레이아웃이 제대로 표시되는 것을 확인할 수 있다. 
이외 다양한 레이아웃은 레퍼런스를 참고한다.

레퍼런스
추신: Forge와 같이 외부에서 개발된 요소를 가져다 쓸때 DOM을 개발회사 개발자가 강제 설정해 처리하는 경우가 종종 있다. 이 경우에도 이틀 동안 삽질 했는 데, 본인이 Bootstrap 레이아웃 규정에 맞게 DOM element 스타일 등 속성을 정의했음에도 제대로 화면 출력이 안되었다. 결국 어디가 잘못되었는 지 확인하기 위해서, 오토데스크에서 제공되는 해당 오픈소스를 모두 분석해야 했다. 먼가 이상하면, 어디서 속성이 변경될 수 있다는 것을 의심하고, css, style 등을 확인해 보고, 디버깅하여 DOM이 어떻게 변경되는 지를 확인해 보는 것이 좋다.