์•ˆ๋…•ํ•˜์„ธ์š”? ์ด๋ฒˆ ๊ธ€์€ Bluemix๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ฐ„๋‹จํ•œ ํด๋ผ์šฐ๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋””์ž์ธํ•˜๊ณ  ๋งŒ๋“ค์–ด ๊ฐ€๋Š” ๊ณผ์ •์„ ๋‚ด์šฉ์œผ๋กœ ์—ฐ์žฌ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์€ ์ˆœ์„œ๋กœ ์ง„ํ–‰๋˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ ์ฐธ๊ณ  ๋ถ€ํƒ ๋“œ๋ฆฝ๋‹ˆ๋‹ค.


  1. ํด๋ผ์šฐ๋“œ ํ™˜๊ฒฝ ์ดํ•ด
  2. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ์ƒ ๋ฐ ์š”๊ฑด ์ •์˜
  3. ์š”๊ฑด์— ๋Œ€ํ•œ Usecase ๋ฐ Wireframe ์ž‘์„ฑ
  4. ๋งˆ์ดํฌ๋กœ ์„œ๋น„์Šค ์•„ํ‚คํ…์ณ ์„ค๊ณ„
  5. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋ฒ„ ํ™˜๊ฒฝ ์ค€๋น„
  6. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์ค€๋น„
  7. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ”„๋กœํ† ํƒ€์ž… ์ž‘์„ฑ Part1
  8. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ”„๋กœํ† ํƒ€์ž… ์ž‘์„ฑ Part2
  9. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ”„๋กœํ† ํƒ€์ž… ์ž‘์„ฑ Part3
  10. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ DevOps ํ™˜๊ฒฝ ๊ตฌ์„ฑ

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ”„๋กœํ† ํƒ€์ž… ์ž‘์„ฑ Part3

์ง€๋‚œ ๊ธ€์—์„œ Cloudant๋ฅผ ์ด์šฉํ•œ ๋ฐฉ๋ฌธ ์˜ˆ์•ฝ ์ •๋ณด ์„œ๋น„์Šค๋ฅผ ๊ตฌ์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ์ด์–ด์„œ ๋‚จ์€ API ์„œ๋น„์Šค์ธ ์ž„์‹œ ์ถœ์ž…์ฆ ์ •๋ณด ์„œ๋น„์Šค(smv-badge)์™€ ์‚ฌ์šฉ์ž ์ •๋ณด ์„œ๋น„์Šค(smv-userinfo) ๊ทธ๋ฆฌ๊ณ  ์‚ฌ์šฉ์ž UI๋ฅผ ๋‹ด๋‹นํ•˜๋Š” smv-ui-app์„ ์ž‘์„ฑํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์ž„์‹œ ์ถœ์ž…์ฆ ์ •๋ณด ์„œ๋น„์Šค ํ”„๋กœํ† ํƒ€์ž… ๊ตฌํ˜„

์ž„์‹œ ์ถœ์ž…์ฆ ์ •๋ณด ์„œ๋น„์Šค๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์€ ์•ž์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ”„๋กœํ† ํƒ€์ž… ์ž‘์„ฑ Part2์—์„œ ์ž‘์„ฑํ•œ ๋ฐฉ๋ฌธ ์˜ˆ์•ฝ ์ •๋ณด ์„œ๋น„์Šค(smv-visit)์™€ ์œ ์‚ฌํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋ฏ€๋กœ ์ด๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

์„œ๋น„์Šค Handler ํ™˜๊ฒฝ ๊ตฌ์„ฑ

๋ฐฉ๋ฌธ ์ •๋ณด ๊ด€๋ฆฌ ์„œ๋น„์Šค(smv-visit)์—์„œ ์ฒ˜๋Ÿผ controllers๋ผ๋Š” ํด๋”๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•ด๋‹น ์„œ๋น„์Šค๋ฅผ ๋…ธ์ถœํ•˜๋Š” SMVBadgeController.js ํŒŒ์ผ์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

'use strict';

const BASE_PATH = '/api/smv/v1/badge';

function newBadgeInfo(req, res) {

  // TODO
  res.statusCode = 200;
  res.end('OK');
}

function getBadgeInfo(req, res) {
  var docid = req.params.id;

  console.log(`getBadgeInfo: badge id ${docid}`);
  
  // TODO
  res.statusCode = 200;
  res.end('OK');
}

function updateBadgeInfo(req, res) {
  var docid = req.params.id;
  
  console.log(`updateBadgeInfo: badge id ${docid}`);

  // TODO
  res.statusCode = 200;
  res.end('OK');
}

function deleteBadgeInfo(req, res) {
  var docid = req.params.id;
  
  console.log(`deleteBadgeInfo: badge id ${docid}`);

  // TODO
  res.statusCode = 200;
  res.end('OK');
}

function searchBadgeInfos(req, res) {

  var type = req.query.type;
  var keyword = req.query.keyword;
  var page = Number(req.query.page);
  var size = Number(req.query.size);

  console.log(`searchBadgeInfos: type=${type}, keyword=${keyword}, page=${page}, size=${size}`);
  
  // TODO
  res.statusCode = 200;
  res.end('OK');
}

module.exports = function(app, options) {
  app.post(BASE_PATH, authenticatedFilter, newBadgeInfo);

  // Expose search option ahead of general document getting function
  app.post(BASE_PATH, newBadgeInfo);
  app.get(BASE_PATH + '/search', searchBadgeInfos);
  app.get(BASE_PATH + '/:id', getBadgeInfo);
  app.put(BASE_PATH + '/:id', updateBadgeInfo);
  app.delete(BASE_PATH + '/:id', deleteBadgeInfo);
};

๊ทธ๋ฆฌ๊ณ  app.js์—์„œ SMVBadgeController ๋ชจ๋“ˆ์„ ๋กœ๋”ฉํ•˜๊ณ  ์ดˆ๊ธฐํ™”ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

...
// Expose the SMVBadgeController
var SMVBadgeController = require('./controllers/SMVBadgeController');
SMVBadgeController(app);
...

์‚ฌ์šฉ์ž ์ธ์ฆ ๊ณตํ†ต ๋ชจ๋“ˆ ์ ์šฉ

์•ž์„œ ์‚ฌ์šฉ์ž ์ธ์ฆ ์„œ๋น„์Šค์—์„œ ๊ตฌ์„ฑํ•œ ๊ณตํ†ต ๋ชจ๋“ˆ์ธ SMVAuthTokenHelper.js๋ฅผ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค. NPM package๋กœ ๋“ฑ๋ก๋˜์–ด ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ์ด์šฉํ•  ์ˆ˜๋„ ์žˆ๊ฒ ์œผ๋‚˜ ๊ฐ๊ฐ์˜ ๊ตฌ๋ถ„๋œ ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋ฏ€๋กœ ์ด ๋ถ€๋ถ„์€ ์ง์ ‘ ๋ณต์ œ๋ฅผ ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ , SMVBadgeController.js์— ์‚ฌ์šฉ์ž ์ธ์ฆ ํ•„ํ„ฐ๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

const AUTH_TOKEN_KEY = 'X-AUTH-TOKEN';

var SMVAuthTokenHelper = require('./SMVAuthTokenHelper');

function extractAuthToken(req) {
  var token = req.headers[AUTH_TOKEN_KEY] || req.headers[AUTH_TOKEN_KEY.toLowerCase()];
  if (!token) {
    console.error(`${AUTH_TOKEN_KEY} is not in the header as key`);
  }
  return token;
}

function authenticatedFilter(req, res, next) {
  var token = extractAuthToken(req);
  SMVAuthTokenHelper.isValidAuthToken(token, function(valid) {
    if (valid) {
      // go next
      return next();
    }

    res.statusCode = 401;
    res.end('Unauthorized');
  });
}

...

module.exports = function(app, options) {
  app.post(BASE_PATH, authenticatedFilter, newBadgeInfo);
  app.get(BASE_PATH + '/search', authenticatedFilter, searchBadgeInfos);
  app.get(BASE_PATH + '/:id', authenticatedFilter, getBadgeInfo);
  app.put(BASE_PATH + '/:id', authenticatedFilter, updateBadgeInfo);
  app.delete(BASE_PATH + '/:id', authenticatedFilter, deleteBadgeInfo);
};

์„œ๋น„์Šค Handler ๊ตฌํ˜„

์•ž์„œ ๊ตฌ์„ฑํ•ด ๋†“์€ API Handler๋ฅผ Cloudant๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ๋ฐฉ๋ฌธ ์ •๋ณด ์„œ๋น„์Šค์™€ ๋™์ผํ•œ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋‚˜ ์ €์žฅํ•˜๋Š” ์ •๋ณด๊ฐ€ ๋‹ค๋ฅด๋ฏ€๋กœ ์ด์— ๋Œ€ํ•œ ๋‚ด์šฉ๋งŒ ๊ฐ„๋‹จํžˆ ์ •๋ฆฌํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

Document ์ƒ์„ฑ

์ž„์‹œ ์ถœ์ž…์ฆ์— ๋Œ€ํ•œ BadgeObject ์ •๋ณด๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ object๋กœ ์„ค๊ณ„๋ฅผ ํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  BadgeObject:
    type: object
    properties:
      id:
        type: string
        description: ์ž„์‹œ ์ถœ์ž…์ฆ ID
        example: 12392391239
      type:
        type: string
        description: ์ž„์‹œ ์ถœ์ž…์ฆ ์ข…๋ฅ˜
        enum:
          - VISITOR
      number:
        type: string
        description: ์ž„์‹œ ์ถœ์ž…์ฆ ๋ฒˆํ˜ธ
        example: 01
      updated:
        type: integer
        format: int64
        description: '์ •๋ณด ๊ฐฑ์‹  Timestamp(๋ฐ€๋ฆฌ์ดˆ), ์ž๋™์ƒ์„ฑ'
        example: 1493349000000

์ด๋ฅผ json์œผ๋กœ ํ‘œํ˜„ํ•œ ์˜ˆ์ œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

{
  'type': 'VISITOR',
  'number': '01',
  'updated': 1493349000000
}

์ด JSON ๋ฐ์ดํ„ฐ๋ฅผ Cloudant์— ์ €์žฅํ•˜๋ฉด ๋˜๋Š”๋ฐ, ์•ž์„œ cloudant๋ฅผ ๊ฐ๊ฐ ์„œ๋น„์Šค๊ฐ€ ๊ณต์œ ํ•ด์„œ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ์ด๋ฅผ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ๋„๋ก type์ด๋ผ๋Š” ์ด๋ฆ„์˜ properity๋ฅผ ์‚ฌ์šฉํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, API๋กœ ๋…ธ์ถœํ•  ๋•Œ๋Š” type ์ด๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ์ œ๊ณตํ•˜์ง€๋งŒ, ์‹ค์ œ Cloudant์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ๋ถˆ๋Ÿฌ ์˜ค๋Š” ๊ฒฝ์šฐ๋Š” ๋‹ค๋ฅธ์ด๋ฆ„์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

newBadgeInfo, updateBadgeInfo ๊ทธ๋ฆฌ๊ณ  searchBadgeInfo์—์„œ type์œผ๋กœ ์ž…๋ ฅ ๋ฐ›์€ ์ •๋ณด๋ฅผ badgetype์ด๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์ €์žฅํ•˜๊ณ  Cloudant ์ €์žฅ์— ํ•„์š”ํ•œ ์ •๋ณด๋กœ ์žฌ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

...
  var json = Object.assign({}, req.body);
  if (json.hasOwnProperty('id')) {
    json['_id'] = String(json.id);
    json['id'] = undefined;
  }
  json['type'] = DOC_TYPE;
  json['badgetype'] = req.body.type;
  json['number'] = req.body.number;
  json['updated'] = new Date().getTime();
...
}

๋˜ํ•œ, cloudant๋ฅผ ํ†ตํ•ด ์–ป์–ด์ง„ BadgeObject์˜ ๊ฒฝ์šฐ๋„ API๋กœ ๋…ธ์ถœ๋˜๋Š” ์ •๋ณด๋กœ ์ฒ˜๋ฆฌํ•˜๋„๋ก cleanseBadgeObject๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ด๋ฅผ getBadgeInfo์™€ searchBadgeInfo์—์„œ ์ด์šฉํ•˜๋„๋ก ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

function cleanseBadgeObject(badge) {
  // 
  var id = badge._id || badge.id;
  //var rev = badge._rev || badge.rev;
  //var type = badge.type;
  var json = Object.assign({}, badge);
  json['id'] = id;
  json['_id'] = undefined;
  json['_rev'] = undefined;
  json['badgetype'] = undefined;
  json['type'] = badge.badgetype;

  return json;
}

๋‹ค์Œ์€ API๋กœ ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋„˜๊ฒจ ๋ฐ›์•„ cloudant document๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

function newBadgeInfo(req, res) {
  console.log(`newBadgeInfo: badge id ${req.body.id} for ${req.body.type} with ${req.body.number}`);

  var json = Object.assign({}, req.body);
  if (json.hasOwnProperty('id')) {
    json['_id'] = String(json.id);
    json['id'] = undefined;
  }
  json['type'] = DOC_TYPE;
  json['badgetype'] = req.body.type;
  json['number'] = req.body.number;
  json['updated'] = new Date().getTime();

  mydb.insert(json, function(err, doc) {
    if (err) {
      console.error(err);
      // Error 
      res.statusCode = 500;
      res.end('Internel Server Error');
    } else {
      res.json(cleanseBadgeObject(Object.assign(json, doc)));
      res.end();
    }
  });
}

Document ์ฝ๊ธฐ

Read์— ๋Œ€ํ•œ ๋ถ€๋ถ„์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌํ˜„๋ฉ๋‹ˆ๋‹ค.

function getBadgeInfo(req, res) {
  var docid = req.params.id;

  console.log(`getBadgeInfo: badge id ${docid}`);

  mydb.get(docid, function(err, doc) {
    if (err) {
      console.error(err);
      // Error 
      res.statusCode = 500;
      res.end('Internel Server Error');
    } else {
      res.json(cleanseBadgeObject(doc));
      res.end();
    }
  });
}

Document ๊ฐฑ์‹ 

์ž„์‹œ ์ถœ์ž…์ฆ์— ๋Œ€ํ•œ ์ •๋ณด๋Š” ํƒ€์ž…๊ณผ ๋ฒˆํ˜ธ๋งŒ ๊ฐฑ์‹  ๊ฐ€๋Šฅํ•œ ํ˜•ํƒœ๊ฐ€ ๋˜์–ด์žˆ๋Š”๋ฐ, ์ด ๋•Œ ๊ฐฑ์‹ ๋˜๋Š” ์ •๋ณด๊ฐ€ type์ธ ๊ฒฝ์šฐ ์‹ค์ œ ์ €์žฅ๋˜๋Š” ์ •๋ณด๋Š” type์ด ์•„๋‹Œ badgetype์ด์–ด์•ผ ํ•˜๋Š” ๊ฒƒ์— ์ฃผ์˜ํ•ฉ๋‹ˆ๋‹ค.

const UPDATABLE_PROPS = ['type', 'number'];
...
function updateBadgeInfo(req, res) {
  var docid = req.params.id;
  
  console.log(`updateBadgeInfo: badge id ${docid}`);

  mydb.get(docid, function(err, doc) {
    if (err) {
      console.error(err);
      // Error 
      res.statusCode = 500;
      res.end('Internel Server Error');
    } else {
      var jsondoc = Object.assign({}, doc);
      //console.log(`key:${key}`);
      // Select field to update
      for (var idx in UPDATABLE_PROPS) {
        var key = UPDATABLE_PROPS[idx];
        console.log(`key:${key}`);
        if (req.body.hasOwnProperty(key)) {
          if (key == 'type') {
            jsondoc['badgetype'] = req.body['type'];
          } else {
            jsondoc[key] = req.body[key]; 
          }
        }
      }

      jsondoc['updated'] = new Date().getTime();

      mydb.insert(jsondoc, function(err, doc) {
        if (err) {
          console.error(err);
          // Error 
          res.statusCode = 500;
          res.end('Internel Server Error');
        } else {
          // new jsondoc is saved well
          res.json(cleanseBadgeObject(jsondoc));
          res.end();
        }
      });
    }
  });
}

Document ์‚ญ์ œ

function deleteBadgeInfo(req, res) {
  var docid = req.params.id;
  
  console.log(`deleteBadgeInfo: badge id ${docid}`);

  mydb.get(docid, function(err, doc) {
    if (err) {
      console.error(err);
      // Error 
      res.statusCode = 500;
      res.end('Internel Server Error');
    } else {
      // Delete
      mydb.destroy(doc._id, doc._rev, function(err, doc) {
        if (err) {
          console.error(err);
          // Error 
          res.statusCode = 500;
          res.end('Internel Server Error');
        } else {
          res.end('OK');
        }
      });
    }
  });
}

Document ์กฐํšŒ

์ž„์‹œ ์ถœ์ž…์ฆ์€ ์ข…๋ฅ˜์™€ ๋ฒˆํ˜ธ๋กœ ์กฐํšŒ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์ด ๋•Œ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ type์ด ์•„๋‹Œ badgetype์œผ๋กœ ์กฐํšŒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

function searchBadgeInfos(req, res) {

  var type = req.query.type;
  var keyword = req.query.keyword;
  var page = Number(req.query.page);
  var size = Number(req.query.size);

  console.log(`searchBadgeInfos: type=${type}, keyword=${keyword}, page=${page}, size=${size}`);

  if (size && Number.isNaN(size)) {
    // Error 
    res.statusCode = 400;
    res.end('Invalid Query Parameter: the size is NaN');
    return;
  }
  if (page && Number.isNaN(page)) {
    // Error 
    res.statusCode = 400;
    res.end('Invalid Query Parameter: the page is NaN');
    return;
  }
  if (size <= 0) {
    // Error 
    res.statusCode = 400;
    res.end('Invalid Query Parameter: the size is 0 or negative value');
    return;
  }
  if (page <= 0) {
    // Error 
    res.statusCode = 400;
    res.end('Invalid Query Parameter: the page is 0 or negative value');
    return;
  }

  var selector = {
    '$and':[
      {'type': DOC_TYPE}
    ]
  };
  var queryIndex;
  if (type) {
    var keytype = type.toLowerCase();
    if (keytype == 'type') keytype = 'badgetype';
    var ekeyword = keyword.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');

    var searchByType = {};
    searchByType[`${keytype}`] = {'$regex': `(?i)${ekeyword}`}; 

    selector['$and'].push(searchByType);
    //queryIndex = ['_design/smv-badge-indexes', `badge-${keytype}`];
  }

  mydb.find({
    selector: selector,
    fields: ['type'],
    use_index: queryIndex
  }, function(err, totalInfo) {
    if (err) {
      console.error(err);
      // Error 
      res.statusCode = 500;
      res.end('Internel Server Error');
    } else {
      if (totalInfo.warning) {
        console.error(totalInfo.warning);
      }

      console.log(`total count : ${totalInfo.docs.length}`);
      var total = totalInfo.docs.length;

      var limit, skip;
      if (size > 0) {
        limit = size;
        
        // Set the page as last
        var maxpage = Math.floor(total/size-1);
        page = page || 0; // defaul page index is 0

        if (maxpage < page) {
          page = maxpage;
        }
        //console.log(`page:${page}, size:${size}`);
        skip = page * size;
      } else {
        size = undefined;
        page = 0;
      }

      mydb.find({
        selector: selector,
        limit: limit,
        skip: skip,
        use_index: queryIndex
      }, function(err, result) {

        if (err) {
          console.error(err);
          // Error 
          res.statusCode = 500;
          res.end('Internel Server Error');
        } else {

          console.log(`searched count : ${result.docs.length}`);
          var resObject = {
            result: result.docs.map(function(item){
              return cleanseBadgeObject(item);
            }),
            paging: {
              page: page,
              size: limit,
              total: total
            }
          };

          res.json(resObject);
          res.end();
        }
      });
    }
  });
}

์ด ๋ถ€๋ถ„์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ Github https://github.com/mc500/smv/blob/master/projects/smv-visit/controllers/SMVVisitController.js ์ฝ”๋“œ๋ฅผ ์ฐธ๊ณ ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž ์ •๋ณด ์„œ๋น„์Šค ํ”„๋กœํ† ํƒ€์ž… ๊ตฌํ˜„

์‚ฌ์šฉ์ž ์ •๋ณด ์„œ๋น„์Šค(smv-userinfo)๋Š” ์กฐ์ง ์ •๋ณด์™€ ์—ฐ๊ณ„๋˜์–ด ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. LDAP์ด๋‚˜ ์‚ฌ๋‚ด ์ •๋ณด ์‹œ์Šคํ…œ๊ณผ ์—ฐ๊ณ„ํ•˜๊ธฐ๋„ ํ•˜์ง€๋งŒ ๋ณธ ๊ธ€์—์„œ๋Š” ํ•˜๋“œ์ฝ”๋”ฉ์œผ๋กœ ์ž„์˜์˜ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ˜•ํƒœ๋กœ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

API๋กœ ์กฐํšŒ ๊ฐ€๋Šฅํ•œ ์‚ฌ์šฉ์ž๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

Name ID Email Role
Jone Doe CN=John Doe/OU=ACME/O=IBM john.doe@acme.ibm.com ESCORT
Sally Doe CN=Sally Doe/OU=ACME/O=IBM sally.doe@acme.ibm.com RECEPTION
Lisa Doe CN=Lisa Doe/OU=ACME/O=IBM lisa.doe@acme.ibm.com ADMIN

์‚ฌ์‹ค ์ด ๋ถ€๋ถ„์€ ์•ž์„œ ์ž‘์„ฑํ•œ ์‚ฌ์šฉ์ž ์ธ์ฆ ์„œ๋น„์Šค(smv-userauth)์™€๋„ ๊ด€๊ณ„๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฒ˜์Œ ๋กœ๊ทธ์ธ ๊ฐ€๋Šฅํ•œ ์‚ฌ์šฉ์ž๋ฅผ ์ง€์ •ํ•  ๋•Œ๋Š” jone.doe@acme.ibm.com๋งŒ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ–ˆ์ง€๋งŒ ์œ„์™€ ๊ฐ™์ด ์ถ”๊ฐ€๋œ ์‚ฌ์šฉ์ž๋„ ๋กœ๊ทธ์ธ ํ•  ์ˆ˜ ์žˆ๋„๋ก smv-userauth/controllers/SMVUserAuthController.js์˜ ์ผ๋ถ€ ์ฝ”๋“œ๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.

์šฐ์„  ์˜ˆ์ œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

...
var USER_INFO_SAMPLES = [
  {
    'role' : 'ESCORT',
    'serial' : '1234567890',
    'phone' : '+82-2-1234-0000',
    'name' : 'John Doe',
    'mobile' : '+82-10-1234-0000',
    'userid' : 'CN=John Doe/OU=ACME/O=IBM',
    'email' : 'john.doe@acme.ibm.com',
    'dept': 'Client Innovation Lab',
    'passwd': 'passw0rd'
  },
  {
    'role' : 'RECEPTION',
    'serial' : '1000000000',
    'phone' : '+82-2-1234-1111',
    'name' : 'Jay Doe',
    'mobile' : '+82-10-1234-1111',
    'userid' : 'CN=Sally Doe/OU=ACME/O=IBM',
    'email' : 'sally.doe@acme.ibm.com',
    'dept': 'Client Service',
    'passwd': 'passw0rd'
  },
  {
    'role' : 'ADMIN',
    'serial' : '9000000000',
    'phone' : '+82-2-1234-9999',
    'name' : 'Lisa Doe',
    'mobile' : '+82-10-1234-9999',
    'userid' : 'CN=Lisa Doe/OU=ACME/O=IBM',
    'email' : 'lisa.doe@acme.ibm.com',
    'dept': 'Client Service',
    'passwd': 'passw0rd'
  }
];
...

๊ทธ๋ฆฌ๊ณ  module.exports.loginPOST ํ•จ์ˆ˜์—์„œ id/pw๋ฅผ ํ™•์ธํ•˜๊ณ  ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ถ€๋ถ„์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.

...
  var sampleUserInfo;
  var email = args.email.value;
  var passwd = args.passwd.value;
  var isFound = USER_INFO_SAMPLE.some(function(userinfo) {
    if (userinfo.email == email) {
      sampleUserInfo = JSON.stringify(userinfo);
      return true;
    }
  });

  if (isFound && passwd === 'passw0rd') {
    SMVAuthTokenHelper.generateAuthToken(function(token) {
      // Set the key of user
      SMVAuthTokenHelper.setAuthTokenValue(token, 'userinfo', sampleUserInfo, function(result){
        if (result) {
          res.setHeader(AUTH_TOKEN_KEY, token);
          res.setHeader('Content-Type', 'application/json');
          res.end(sampleUserInfo);
        } else {
          // Error 
          res.statusCode = 500;
          res.end('Internel Server Error');
        }
      });
    });

    return;
  }
...

์ด์ œ smv-userinfo๋กœ ๋˜๋Œ์•„์™€์„œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

์„œ๋น„์Šค Handler ํ™˜๊ฒฝ ๊ตฌ์„ฑ

์‚ฌ์šฉ์ž ์ธ์ฆ ์„œ๋น„์Šค์—์„œ์—์„œ ์ฒ˜๋Ÿผ REST ์„œ๋น„์Šค๋ฅผ ์œ„ํ•œ Handler ์ฝ”๋“œ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์ธ์ฆ ์„œ๋น„์Šค์™€ ์œ ์‚ฌํ•œ ๊ตฌ์กฐ๋กœ ํ•˜๊ธฐ ์œ„ํ•ด controllers๋ผ๋Š” ํด๋”๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•ด๋‹น ์„œ๋น„์Šค๋ฅผ ๋…ธ์ถœํ•˜๋Š” SMVUserInfoController.js ํŒŒ์ผ์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

'use strict';

const BASE_PATH = '/api/smv/v1/userinfo';

function getUserInfo(req, res) {
  var userid = req.params.id;

  // TODO
  res.statusCode = 200;
  res.end('OK');
}

function updateUserRole(req, res) {
  var userid = req.params.id;
  var newrole = req.params.role;
  

  // TODO
  res.statusCode = 200;
  res.end('OK');
}

module.exports = function(app, options) {

  // Expose search option ahead of general document getting function
  app.get(BASE_PATH + '/:id', getUserInfo);
  app.put(BASE_PATH + '/:id/role/:role', updateUserRole);
};

๊ทธ๋ฆฌ๊ณ  app.js์—์„œ SMVUserInfoController ๋ชจ๋“ˆ์„ ๋กœ๋”ฉํ•˜๊ณ  ์ดˆ๊ธฐํ™”ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

...
// Expose the SMVUserInfoController
var SMVUserInfoController = require('./controllers/SMVUserInfoController');
SMVUserInfoController(app);
...

์‚ฌ์šฉ์ž ์ธ์ฆ ๊ณตํ†ต ๋ชจ๋“ˆ ์ ์šฉ

์•ž์„œ ์‚ฌ์šฉ์ž ์ธ์ฆ ์„œ๋น„์Šค์—์„œ ๊ตฌ์„ฑํ•œ ๊ณตํ†ต ๋ชจ๋“ˆ์ธ SMVAuthTokenHelper.js๋ฅผ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค. NPM package๋กœ ๋“ฑ๋ก๋˜์–ด ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ์ด์šฉํ•  ์ˆ˜๋„ ์žˆ๊ฒ ์œผ๋‚˜ ๊ฐ๊ฐ์˜ ๊ตฌ๋ถ„๋œ ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋ฏ€๋กœ ์ด ๋ถ€๋ถ„์€ ์ง์ ‘ ๋ณต์ œ๋ฅผ ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ , SMVUserInfoController.js์— ์‚ฌ์šฉ์ž ์ธ์ฆ ํ•„ํ„ฐ๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

const AUTH_TOKEN_KEY = 'X-AUTH-TOKEN';

var SMVAuthTokenHelper = require('./SMVAuthTokenHelper');

function extractAuthToken(req) {
  var token = req.headers[AUTH_TOKEN_KEY] || req.headers[AUTH_TOKEN_KEY.toLowerCase()];
  if (!token) {
    console.error(`${AUTH_TOKEN_KEY} is not in the header as key`);
  }
  return token;
}

function authenticatedFilter(req, res, next) {
  var token = extractAuthToken(req);
  SMVAuthTokenHelper.isValidAuthToken(token, function(valid) {
    if (valid) {
      // go next
      return next();
    }

    res.statusCode = 401;
    res.end('Unauthorized');
  });
}

...

module.exports = function(app, options) {
  app.get(BASE_PATH + '/:id', authenticatedFilter, getUserInfo);
  app.put(BASE_PATH + '/:id/role/:role', authenticatedFilter, updateUserRole);
};

์„œ๋น„์Šค Handler ๊ตฌํ˜„

์•ž์„œ ๋‹ค๋ฅธ ์„œ๋น„์Šค API์™€ ๋‹ฌ๋ฆฌ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์ž„์˜์˜ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฏ€๋กœ ์•ž์„œ ์ž‘์„ฑํ–ˆ๋˜ ์‚ฌ์šฉ์ž ์ธ์ฆ ์„œ๋น„์Šค์˜ smv-userauth/controllers/SMVUserAuthController.js ์˜ ๋‚ด์šฉ์„ ์ฐธ์กฐํ•˜์—ฌ ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉ์ž ์ •๋ณด USER_INFO_SAMPLES ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ, updateUserRole ํ•จ์ˆ˜ ๊ฐ™์€ ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž์˜ ์—ญํ• (role)์„ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•œ ๋ชฉ์ ์ด๋ฏ€๋กœ ์‹ค์ œ ๋กœ๊ทธ์ธ ํ•œ ์‚ฌ์šฉ์ž์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์™ธ๋ถ€์— ๋ณ„๋„ ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌ์„ฑํ•ด์•ผ ํ•˜๋‚˜ ๋‚˜์ค‘์— ๊ธฐํšŒ๊ฐ€ ๋˜๋ฉด ์—…๋ฐ์ดํŠธ ํ•˜๊ธฐ๋กœ ํ•˜๊ณ  ์šฐ์„ ์€ ์ž„์‹œ๋กœ ๊ตฌ์„ฑํ•ด ๋†“์€ ํ˜•ํƒœ์ด๋‹ˆ ์ฐธ๊ณ  ๋ฐ”๋ž๋‹ˆ๋‹ค.

...

var USER_INFO_SAMPLES = [
  {
    'role' : 'ESCORT',
    'serial' : '1234567890',
    'phone' : '+82-2-1234-0000',
    'name' : 'John Doe',
    'mobile' : '+82-10-1234-0000',
    'userid' : 'CN=John Doe/OU=ACME/O=IBM',
    'email' : 'john.doe@acme.ibm.com',
    'dept': 'Client Innovation Lab'
  },
  {
    'role' : 'RECEPTION',
    'serial' : '1000000000',
    'phone' : '+82-2-1234-1111',
    'name' : 'Jay Doe',
    'mobile' : '+82-10-1234-1111',
    'userid' : 'CN=Sally Doe/OU=ACME/O=IBM',
    'email' : 'sally.doe@acme.ibm.com',
    'dept': 'Client Service'
  },
  {
    'role' : 'ADMIN',
    'serial' : '9000000000',
    'phone' : '+82-2-1234-9999',
    'name' : 'Lisa Doe',
    'mobile' : '+82-10-1234-9999',
    'userid' : 'CN=Lisa Doe/OU=ACME/O=IBM',
    'email' : 'lisa.doe@acme.ibm.com',
    'dept': 'Client Service'
  }
];

var USER_ROLES = ['ESCORT', 'RECEPTION', 'ADMIN'];

function getUserInfo(req, res) {
  var userid = req.params.id;

  console.log(`getUserInfo: user id ${userid}`);

  var isFound = USER_INFO_SAMPLES.some(function(userinfo) {
    if (userinfo.userid == userid) {
      res.json(userinfo);
      res.end();
      return true;
    }
  });

  if (isFound) return;

  // Not Found
  console.error('User Not Found');

  // Error 
  res.statusCode = 404;
  res.end('User Not Found');
}

function updateUserRole(req, res) {
  var userid = req.params.id;
  var newrole = req.params.role;
  
  console.log(`updateUserRole: user id ${userid}, ${newrole}`);

  if (!userid) {
    console.error('User\'s id is empty');
    // Error 
    res.statusCode = 400;
    res.end('Invalid Argument');
  }

  // Find roles
  if (USER_ROLES.indexOf(newrole) < 0) {
    // Not Found
    console.error(`Unknown role code : ${newrole}`);
    // Error 
    res.statusCode = 400;
    res.end('Invalid Argument');
  }

  // Find user info
  var foundUserInfo = undefined;
  USER_INFO_SAMPLES.some(function(userinfo) {
    if (userinfo.userid == userid) {
      foundUserInfo = Object.assign({}, userinfo);
      foundUserInfo.role = newrole;
      return true;
    }
  });

  if (foundUserInfo) {
    res.json(foundUserInfo);
    res.end();
    return ;
  }

  // Not Found
  console.error('User Not Found');

  // Error 
  res.statusCode = 404;
  res.end('User Not Found');
}

SMV-UI API ํ†ตํ•ฉ ํ”„๋กœํ† ํƒ€์ž… ๊ตฌํ˜„

UI ์•ฑ์ด๋ผ ํ•˜๋”๋ผ๋„ ๋‹จ์ˆœํžˆ ํ™”๋ฉด๋งŒ ํ•„์š”ํ˜„ ํ˜•ํƒœ๊ฐ€ ์•„๋‹ˆ๋ผ ๋กœ๊ทธ์ธ๊ณผ ๋กœ๊ทธ์•„์›ƒ ๊ทธ๋ฆฌ๊ณ  ํ™”๋ฉด(view ๋˜๋Š” page)์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, ์•ž์„œ ์„œ๋น„์Šค๋“ค๊ณผ ๊ฐ™์ด Controller ์ •๋ณด๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

์„œ๋น„์Šค Handler ๊ตฌ์„ฑ

๋‹ค๋ฅธ ์„œ๋น„์Šค์™€ ์œ ์‚ฌํ•œ ๊ตฌ์กฐ๋กœ ํ•˜๊ธฐ ์œ„ํ•ด controllers๋ผ๋Š” ํด๋”๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•ด๋‹น ์„œ๋น„์Šค๋ฅผ ๋…ธ์ถœํ•˜๋Š” SMVUIAppController.js ํŒŒ์ผ์„ ์•„๋ž˜์™€ ๊ฐ™์ด ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์— ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ชจ๋“ˆ๋„ ์ถ”๊ฐ€๋œ ํ˜•ํƒœ์ž…๋‹ˆ๋‹ค.

'use strict';

const BASE_PATH = '';
const AUTH_TOKEN_KEY = 'X-AUTH-TOKEN';

var SMVAuthTokenHelper = require('./SMVAuthTokenHelper');

function extractAuthToken(req) {
  var token = req.headers[AUTH_TOKEN_KEY] || req.headers[AUTH_TOKEN_KEY.toLowerCase()];
  if (!token) {
    console.error(`${AUTH_TOKEN_KEY} is not in the header as key`);
  }
  return token;
}

function buildAuthHeaders(authtoken) {
  var authheaders = {};
  authheaders[AUTH_TOKEN_KEY] = authtoken;
  return authheaders;
}

function login(req, res, next) {
  // Login

  // TODO
  res.statusCode = 200;
  res.end('OK');
}

function logout(req, res, next) {
  // Logout
  
  // TODO
  res.statusCode = 200;
  res.end('OK');
}

module.exports = function(app, options) {
  app.post(BASE_PATH + '/api/login', login);
  app.get(BASE_PATH + '/api/logout', logout);
};

์„œ๋น„์Šค Handler ๊ตฌํ˜„

๋กœ๊ทธ์ธ API์˜ ๊ฒฝ์šฐ smv-userauth ์„œ๋น„์Šค๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜๋ฏ€๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์ด request ๋ชจ๋“ˆ์„ ์ด์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

...
const SMV_USERAUTH_BASE_URL = process.env['SMV_USERAUTH_BASE_URL'];
const EXPIRES_IN_SECS = 3600; // an hour

var request = require('request');

...

function login(req, res, next) {
  // Login
  request.post({
    url: SMV_USERAUTH_BASE_URL+'/api/smv/v1/auth/login',
    form: req.body
  }, function(error, response, body) {
    if (error) {
      console.log(error);
      //return res.end(error);
      // Redirect to visiting list
      res.redirect(BASE_PATH + '/login?error='+error);
      return;
    }

    if (response.statusCode != 200) {
      console.error(`response code is ${response.statusCode}`);
      res.redirect(BASE_PATH + '/login?error='+response.body);
      return;
    }

    var authtoken = extractAuthToken(response);
    if (!authtoken) {
      res.redirect(BASE_PATH + '/login?error=authtoken is empty');
      return; 
    }

    // keep authtoken as cookie
    res.cookie(AUTH_TOKEN_KEY, authtoken, {
      maxAge: EXPIRES_IN_SECS*1000 // in milliseconds
    });
      
    // Redirect to visiting list
    res.redirect(BASE_PATH + '/visiting/list');
  });
}

๋กœ๊ทธ์ธ์ด ์„ฑ๊ณตํ•˜๋ฉด smv-userauth๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์ธ์ฆ ํ† ํฐ์„ ์–ป๊ฒŒ๋ฉ๋‹ˆ๋‹ค. ์–ป์–ด์ง„ token์„ Web Browser์— API ํ˜ธ์ถœ ๊ฒฐ๊ณผ๋กœ ์ „๋‹ฌ ํ•  ๋•Œ cookie ๋ฐฉ์‹์œผ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์ธ์ฆ ์„œ๋น„์Šค์—์„œ ์œ ํšจ ์‹œ๊ฐ„์„ 1์‹œ๊ฐ„์œผ๋กœ ํ•ด ๋†“์•˜๊ธฐ์— cookie์—์„œ๋„ 1์‹œ๊ฐ„์œผ๋กœ ํ•ด ๋†“์•˜์Šต๋‹ˆ๋‹ค. ์ด๋Š” ํ•„์š”์— ๋”ฐ๋ผ์„œ ์กฐ์ ˆ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋กœ๊ทธ์•„์›ƒ์€ cookie์—์„œ ์ธ์ฆ ํ† ํฐ์„ ์–ป์–ด ์ฒ˜๋ฆฌํ•˜๋„๋ก ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค

function logout(req, res, next) {
  // Logout
  var authtoken = req.cookies[AUTH_TOKEN_KEY];
  if (!authtoken) {
    // Redirect to visiting list
    res.redirect(BASE_PATH + '/');
  }

  request.get({
    url: SMV_USERAUTH_BASE_URL+'/api/smv/v1/auth/logout',
    headers: buildAuthHeaders(authtoken)
  }, function(error, response, body) {
    console.log('logout done');

    // Remove Cookie
    res.clearCookie(AUTH_TOKEN_KEY);
    
    // Redirect to visiting list
    res.redirect(BASE_PATH + '/');
  });
}

๊ทธ๋ฆฌ๊ณ , app.js์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„œ๋น„์Šค๋ฅผ ๋กœ๋”ฉํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

...
// Expose the SMVUIAppController
var SMVUIAppController = require('./controllers/SMVUIAppController');
SMVUIAppController(app);
...

์„œ๋น„์Šค API๊ฐ€ ์ค€๋น„๋˜์—ˆ์œผ๋ฏ€๋กœ ์ด์ œ ์‚ฌ์šฉ์ž UI๋ฅผ ๊ตฌ์„ฑ ํ•  ์ฐจ๋ก€์ž…๋‹ˆ๋‹ค.

express view(๋˜๋Š” page)์™€ data์˜ ๊ด€๊ณ„

smv-ui-app์€ Bluemix Boilerplate๋กœ ๊ตฌ์„ฑ๋˜์–ด Node.js ์™€ Cloudant๋ฅผ ์ด์šฉํ•œ Web UI Application์œผ๋กœ์„œ ejs๋ฅผ express์˜ rendering ์—”์ง„์œผ๋กœ ์‚ฌ์šฉํ•œ ํ˜•ํƒœ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Š” app.js์˜ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ํ™•์ธ ํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

...
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.engine('html', require('ejs').renderFile);
...

์ด๋Š” express๊ฐ€ view๋ฅผ ์œ„ํ•ด /views ๋””๋ ‰ํ† ๋ฆฌ ์•„๋ž˜์— ์žˆ๋Š” ๋ฆฌ์†Œ์Šค๋ฅผ ์‚ฌ์šฉํ•˜๊ฒ ์œผ๋ฉฐ view engine์œผ๋กœ ejs ๋ฐฉ์‹์„ ์„ ํƒํ•˜๊ณ  html ํŒŒ์ผ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ๋•Œ ejs์˜ renderFile์„ ํ†ตํ•ด ์ฒ˜๋ฆฌํ•˜๊ฒ ๋‹ค๋Š” ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.

๋˜ํ•œ ์•„๋ž˜์™€ ๊ฐ™์€ ๋ถ€๋ถ„๋„ ๋ณด์ด๋Š”๋ฐ

...
var express = require('express'),
    routes = require('./routes'),
...
app.get('/', routes.index);
...

/routes ๋””๋ ‰ํ† ๋ฆฌ์— ์žˆ๋Š” index.js๋ฅผ ๋กœ๋”ฉํ•˜๊ณ  index.js์— ๋…ธ์ถœ๋˜์–ด ์žˆ๋Š” index ํ•จ์ˆ˜๋ฅผ / ๊ฒฝ๋กœ๋กœ ์—ฐ๊ฒฐํ•˜๋Š” ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

์‹ค์ œ /routes/index.js ํŒŒ์ผ์„ ๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

exports.index = function(req, res){
  res.render('index.html', { title: 'Cloudant Boiler Plate' });
};

/views ๋””๋ ‰ํ† ๋ฆฌ์— ์žˆ๋Š” index.html์„ ๋ Œํ„ฐ๋งํ•˜๋ฉฐ ์ด ๋•Œ title ๊ฐ’์„ 'Cloudant Boiler Plate'๋กœ ์ „๋‹ฌํ•˜์—ฌ ํ™”๋ฉด์— ์ƒ์„ฑ๋˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

๋‹ค๋งŒ /views/index.html ํŒŒ์ผ์„ ๋ณด๋ฉด title์— ๋Œ€์‘๋˜๋Š” ejs ํ‘œํ˜„์ด ์—†๋Š”๋ฐ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑ๋œ <title> tag๋ฅผ

...
<title>Favorites Organizer powered by Cloudant</title>
...

์•„๋ž˜์™€ ๊ฐ™์ด ๋ณ€๊ฒฝํ•˜๋ฉด

...
<title><%=title%></title>
...

์ถœ๋ ฅ๋˜๋Š” title์ด Favorites Organizer powered by Cloudant๊ฐ€ ์•„๋‹Œ Cloudant Boiler Plate๊ฐ€ ๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

express์™€ ejs์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์•„๋ž˜ ๋งํฌ๋ฅผ ์ฐธ๊ณ  ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

ํ™”๋ฉด ๊ตฌ์„ฑ (์ž„์ง์›์šฉ)

๋ณดํ†ต ํ”„๋กœํ†  ํƒ€์ž…์„ ๋งŒ๋“ค๋•Œ ํ•œ๋ฒˆ์— ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์ผ์ •ํ•œ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค๊ณ  ์ด๋ฅผ ๊ฐœ๋ฐœ ์ผ์ •์— ๋งž์ถฐ ์—ฌ๋Ÿฌ๋ฒˆ์˜ ๋ฐ˜๋ณต๋œ ๊ฐœ์„  ์ž‘์—…์„ ํ†ตํ•ด ์™„์„ฑ๋„๋ฅผ ๋†’์—ฌ๊ฐ€๋Š” ๋ฐฉ์‹์œผ๋กœ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค. ๋ณธ ๊ธ€์—์„œ๋Š” ์œ„์— ์–ธ๊ธ‰๋œ view ์ค‘์—์„œ ์ž„์ง์›์ด ๊ณ ๊ฐ ๋ฐฉ๋ฌธ ์˜ˆ์•ฝ์„ ์œ„ํ•œ ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ๋‚ด์šฉ์„ ์œ„์ฃผ๋กœ ์ง„ํ–‰ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

smv-ui-app์ด ํ•„์š”ํ•œ ํ™”๋ฉด์€ ์•ž์„œ ์ž‘์„ฑ ํ–ˆ๋˜ Wireframe๊ณผ Usecase์˜ ์ •๋ณด๋ฅผ ์ฐธ๊ณ  ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. https://developer.ibm.com/kr/cloud/bluemix/cf-applications/2017/04/21/develop-cloud-app-step3/

ํ•ด๋‹น ํ™”๋ฉด์„ view๋ผ๊ณ  ํ–ˆ์„ ๋•Œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์—ฐ๊ฒฐํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ™”๋ฉด ์ด๋ฆ„ HTML ํŒŒ์ผ ํ˜ธ์ถœ ๊ฒฝ๋กœ ์ฐธ๊ณ 
๋กœ๊ทธ์ธ views/login.html /login SMV-UCS-000-001-์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ
๋ฐฉ๋ฌธ์ž ๋“ฑ๋ก views/visiting/register.html /visiting/register SMV-UCS-100-001-๋ฐฉ๋ฌธ ์˜ˆ์ • ์ •๋ณด ์ถ”๊ฐ€
๋ฐฉ๋ฌธ ์ผ์ • ์กฐํšŒ views/visiting/list.html /visiting/list?query&paging SMV-UCS-100-002-๋ฐฉ๋ฌธ ์˜ˆ์ • ์ •๋ณด ์กฐํšŒ
๋ฐฉ๋ฌธ์ž ์ƒ์„ธ ์ •๋ณด views/visiting/detail.html /visiting/detail SMV-UCS-100-002-๋ฐฉ๋ฌธ ์˜ˆ์ • ์ •๋ณด ์กฐํšŒ

์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ

์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ์€ smv-ui-app์— ๋กœ๊ทธ์ธ ๋˜์ง€ ์•Š์€ ์ƒํƒœ์ธ ๊ฒฝ์šฐ ๋ฌด์กฐ๊ฑด ๋ณด์—ฌ์ฃผ๋Š” ํŽ˜์ด์ง€์ž…๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ์ด ์„ฑ๊ณตํ•˜๋ฉด ์ดˆ๊ธฐ ํ™”๋ฉด์ธ ๋ฐฉ๋ฌธ ์˜ˆ์ • ์ •๋ณด ํ™”๋ฉด์ด ๋ณด์ด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ, ๋‹ค์Œ๊ณผ ๊ฐ™์ด views/login.html ํŒŒ์ผ์„ ์ค€๋น„ํ•ฉ๋‹ˆ๋‹ค.

<!DOCTYPE html>
<html>
<head>
    <%- include('common-head.html'); %>
</head>
<body>
    <div class = "container login">
        ๋กœ๊ทธ์ธ
        <form method="post" action="/login">
            <input name="email" id="email" placeholder="email" value="john.doe@acme.ibm.com"><br>
            <input name="passwd" id="passwd" placeholder="password" type="password" value="passw0rd"><br>
            <input name="savepw" id="savepw" type="checkbox">๋น„๋ฐ€๋ฒˆํ˜ธ ์ €์žฅ<br>
            <button id="submit" type="submit">๋กœ๊ทธ์ธ</button>
        </form>
    </div>
    <script type="text/javascript" src="/scripts/login.js"></script>
</body>
</html>

๋กœ๊ทธ์ธ ํ™”๋ฉด์€ ์‚ฌ์šฉ์ž๋กœ ๋ถ€ํ„ฐ email๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅ ๋ฐ›์•„ /login์œผ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” SMVUIAppController์—์„œ ์ž‘์„ฑํ–ˆ๋˜ login ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ , ์•ž์„œ index.html์„ /router/index.js์—์„œ ์—ฐ๊ฒฐ ํ–ˆ๋˜ ๊ฒƒ๊ณผ ๊ฐ™์ด login.html๊ณผ '/login'์œผ๋กœ ์—ฐ๊ฒฐํ•˜๋Š” ๋ถ€๋ถ„์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” SMVUIAppController์— ์ด๋ฅผ ๊ตฌํ˜„ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

...
function renderFunction(req, res, view, obj) {
  obj = obj || {};
  obj['SMV_VISIT_BASE_URL'] = SMV_VISIT_BASE_URL;

  var authtoken = extractViewAuthToken(req);
  SMVAuthTokenHelper.getAuthTokenValue(authtoken, 'userinfo', (result)=>{
      if (result) {
        obj['userinfo'] = JSON.parse(result); 
      }
      return res.render(view, obj);
  });
}

function loginView(req, res) {
  renderFunction(req, res, 'login.html', {
    title: '๋กœ๊ทธ์ธ'
  });
}
...
  // Page view
  app.get(BASE_PATH + '/login', loginView);
...

Web Browser์—์„œ /login URL๋กœ ์ ‘๊ทผํ•˜๋ฉด GET ๋ฐฉ์‹์ด๋ฏ€๋กœ POST ๋ฐฉ์‹ ์ ‘๊ทผ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ login() ๋Œ€์‹  loginView()๊ฐ€ ํ˜ธ์ถœ ๋ฉ๋‹ˆ๋‹ค. loginView()์—์„œ๋Š” renderFunction์ด๋ผ๋Š” ๊ณตํ†ต ํ•จ์ˆ˜์— ๋ณด์—ฌ์ฃผ์–ด์•ผ ํ•  HTML๊ณผ Data Object๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์ „๋‹ฌ๋œ ์ •๋ณด์™€ ๊ณตํ†ต์œผ๋กœ ์ „๋‹ฌ๋˜์–ด์•ผ ํ•˜๋Š” Data๋ฅผ ํฌํ•จํ•˜์—ฌ response.render ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ฐธ๊ณ ๋กœ, ์ „๋‹ฌํ•˜๋Š” Data Object์— title ๊ฐ’์ด ์žˆ๋Š”๋ฐ login.html์— ejs ๋ฌธ๋ฒ•์„ ์ด์šฉํ•˜์—ฌ title ๊ฐ’์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

![]((https://developer.ibm.com/kr/wp-content/uploads/sites/98//loginview.jpg)

๋กœ๊ทธ์ธ์ด ์„ฑ๊ณตํ–ˆ๋‹ค๋ฉด visiting/list๋กœ ์ด๋™ํ•˜๋„๋ก ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ๋ฐฉ๋ฌธ ์˜ˆ์ • ๋ชฉ๋ก์„ ๊ตฌ์„ฑํ•ด ๋ณด๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

๋ฐฉ๋ฌธ ์˜ˆ์ • ๋ชฉ๋ก

๋กœ๊ทธ์ธ์ด ์„ฑ๊ณตํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ € ๊ธฐ์ค€ ์˜ค๋Š˜ ๋‚ ์งœ์— ๋งž์ถฐ ๋ฐฉ๋ฌธ ์˜ˆ์ • ์ •๋ณด ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

๋ฐฉ๋ฌธ ์ •๋ณด ๋ชฉ๋ก View

๋ฐฉ๋ฌธ ์˜ˆ์ • ๋ชฉ๋ก์„ ์œ„ํ•ด visiting/list.html๋ฅผ ์ค€๋น„ํ•ฉ๋‹ˆ๋‹ค.

<!DOCTYPE html>
<html>
<head>
    <%- include('../common-head.html'); %>
</head>
<body>
    <%- include('../common-header.html'); %>
    <div class = "container visiting list">
        <!-- ๋ฐฉ๋ฌธ ์ผ์ • ์กฐํšŒ -->
        <div>
            <input id="date" type="date" value="<%=date%>">
            <input id="keyword" placeholder="๊ฒ€์ƒ‰๋‚ด์šฉ" value="<%=keyword%>">
            <select id="type">
                <option value="NAME" <% if(type == 'NAME' || type == undefined) { %>selected<% } %> >๋ฐฉ๋ฌธ์ž๋ช…</option>
                <option value="EMAIL" <% if(type == 'EMAIL') { %>selected<% } %> >์ด๋ฉ”์ผ</option>
                <option value="CONTACT" <% if(type == 'CONTACT') { %>selected<% } %> >์—ฐ๋ฝ์ฒ˜</option>
            </select>
            <button id="search">๊ฒ€์ƒ‰</button>
            <button id="register" onclick="window.location='/visiting/register';">๋ฐฉ๋ฌธ๋“ฑ๋ก</button>
        </div>
<% if (typeof error != "undefined") { %>
        <div class="error">
            ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: <%=error%>
        </div>
<% } else { %>
        <table>
            <tbody>
                <tr>
                    <th>#</th>
                    <th>์ผ์‹œ</th>
                    <th>์ด๋ฆ„</th>
                    <th>์ง์œ„</th>
                    <th>์—ฐ๋ฝ์ฒ˜</th>
                    <th>ํšŒ์‚ฌ</th>
                    <th>์ƒํƒœ</th>
                    <th>์นด๋“œ์ข…๋ฅ˜</th>
                    <th>์นด๋“œ๋ฒˆํ˜ธ</th>
                    <th>์ž„์ง์›</th>
                    <th>์—ฐ๋ฝ์ฒ˜</th>
                    <th>๋ถ€์„œ</th>
                </tr>
                <% for (var i=0 ; i < result.length ; i++) {%>
                <tr class="visiting" data-id="<%=result[i].id%>" data-escort-id="<%=result[i].escort.id%>">
                    <td><%=result[i].idx%></td>
                    <td><%=result[i].ndate%></td>
                    <td><%=result[i].visitor.name%></td>
                    <td><%=result[i].visitor.title%></td>
                    <td><%=result[i].visitor.contact%></td>
                    <td><%=result[i].visitor.company%></td>
                    <td><% if (result[i].agreement) { %>
                        ํ™•์ธ
                    <% } else { %>
                        ๋ฏธ๋™์˜
                    <% } %></td>
                    <% if (result[i].badge) { %>
                    <td><%=result[i].badge.type%></td>
                    <td><%=result[i].badge.number%></td>
                    <% } else { %>
                    <td colspan="2">๋ฏธํ• ๋‹น</td>
                    <% } %>
                    <td><%=result[i].escort.name%></td>
                    <td><%=result[i].escort.contact%></td>
                    <td><%=result[i].escort.dept%></td>
                </tr>
                <% } %>

                <% if (result.length == 0) {%>
                <tr>
                    <td></td>
                    <td colspan="11">๋ฐฉ๋ฌธ ๋“ฑ๋ก ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</td>
                </tr>
                <% } %>
            </tbody>
        </table>
        <%- include('../common-paging.html'); %>
<% } %>
    </div>
    <script type="text/javascript" src="/scripts/visiting/list.js"></script>
    <%- include('../common-footer.html'); %>
</body>
</html>

ejs์—์„œ๋Š” ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋‚ด์šฉ์ธ ๊ฒฝ์šฐ ๋ณ„๋„๋กœ ๋ถ„๋ฆฌํ•˜๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก <%- include(); %> ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒฝ์šฐ list.html์˜ ์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ์— ์žˆ๋Š” common-head.html ์ด๋ผ๋Š” ํŒŒ์ผ์ด ์ž๋™์œผ๋กœ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

    <%- include('../common-head.html'); %>

common-head.html์€ HTML head์— ๊ณตํ†ต์œผ๋กœ ํ•„์š”ํ•œ ๋ถ€๋ถ„ ๋˜๋Š” ๋ฐ˜๋ณต๋˜๋Š” ๋ถ€๋ถ„์„ ๋ถ„๋ฆฌํ•ด ๋†“์€ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค. ์•ž์„œ Data Object๋กœ ์ „๋‹ฌํ–ˆ๋˜ title ๊ฐ’๋„ ์ด ํŒŒ์ผ์—์„œ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no"/>
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <link rel="stylesheet" href="/style/style.css" />
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script type="text/javascript">
        function getSmvVisitBase(path) {
            path = path || '';
            return `<%=SMV_VISIT_BASE_URL%>${path}`;
        }
    </script>
    <% if (typeof title != "undefined") { %>
    <title><%=title%></title>
    <% } %>

๋˜ํ•œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” HTML์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์ž์„ธํ•œ ๋‚ด์šฉ์€ GitHub https://github.com/mc500/smv/tree/master/projects/smv-ui-app/views๋ฅผ ์ฐธ๊ณ  ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

์ด์ œ SMVUIAppController.js ํŒŒ์ผ์— visiting/list์— ๋Œ€ํ•œ ์„œ๋น„์Šค ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

...
function extractViewAuthToken(req) {
  //var token = req.headers[AUTH_TOKEN_KEY] || req.headers[AUTH_TOKEN_KEY.toLowerCase()];
  var token = req.cookies[AUTH_TOKEN_KEY];
  if (!token) {
    console.error(`${AUTH_TOKEN_KEY} is not in cookies`);
  }
  return token;
}

function authenticatedViewFilter(req, res, next) {
  var token = extractViewAuthToken(req);
  SMVAuthTokenHelper.isValidAuthToken(token, function(valid) {
    //valid = true;
    if (valid) {
      // go next
      return next();
    }

  // redirect to login
    res.redirect(BASE_PATH + '/login');
  });
}
...

function visitinglistView(req, res) {
  console.log(`visiting list`);

  var authtoken = extractViewAuthToken(req);
  
  // TODO
  res.statusCode = 200;
  res.end('OK');
}
...
app.get(BASE_PATH + '/visiting/list', authenticatedViewFilter, visitinglistView);
...

authenticatedViewFilter๋Š” ์•ž์„œ API ์„œ๋น„์Šค์—์„œ ์ž‘์„ฑํ•œ ๋‚ด์šฉ๊ณผ ๊ฑฐ์˜ ๋™์ผํ•˜์ง€๋งŒ Header๋กœ ์ „๋‹ฌ๋˜๋Š” ์ธ์ฆํ† ํฐ์ด ์•„๋‹Œ cookie๋กœ ์ „๋‹ฌ๋˜๋Š” ๊ฒƒ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

๋ฐฉ๋ฌธ ์ •๋ณด ๋ชฉ๋ก Controller

visitinglistView ํ•จ์ˆ˜๋Š” Cloudant๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ฐฉ๋ฌธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š”๋ฐ URL paramter๋กœ ์กฐํšŒ ์ •๋ณด๋ฅผ ์ž…๋ ฅ๋ฐ›๊ณ  ์ด๋ฅผ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

parameter ์„ค๋ช…
date ์กฐํšŒ ๋‚ ์งœ, ํ˜•์‹(2017-01-01)
keyword ์กฐํšŒ ํ•  ๋‚ด์šฉ
type ์กฐํšŒ ํ•  ์ข…๋ฅ˜ (๋ฐฉ๋ฌธ์ž๋ช…, ์ด๋ฉ”์ผ, ์—ฐ๋ฝ์ฒ˜)
page ํ™”๋ฉด์— ํ‘œ์‹œํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (๊ธฐ๋ณธ๊ฐ’ 1)
size ํ™”๋ฉด์— ํ‘œ์‹œํ•  ํŽ˜์ด์ง€ size (๊ธฐ๋ณธ๊ฐ’ 10)
  var date = new Date(req.query.date ? req.query.date : getDateString());
  var keyword = req.query.keyword;
  var type = req.query.type;
  var page = Number(req.query.page);
  var size = req.query.size || PAGE_SIZE;

๊ทธ๋ฆฌ๊ณ  smv-visit API๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ •๋ณด๋ฅผ ์–ป์Šต๋‹ˆ๋‹ค.

...
  var query = {
    date: date,
    type: type,
    keyword: keyword,
    page: page-1, // page starts with 0
    size: size
  };

  // search 
  request.get({
    url: `${SMV_VISIT_BASE_URL}/api/smv/v1/visit/search`,
    headers: buildAuthHeaders(authtoken),
    qs: query
  }, function(error, response, body) {
...

API ํ˜ธ์ถœ ๊ฒฐ๊ณผ๋ฅผ body๋กœ ์ „๋‹ฌ๋ฐ›๊ณ  ์ด๋ฅผ ๋ฐฐ์—ด๋กœ ๋งŒ๋“ค์–ด์„œ ํ™”๋ฉด์— ํ…Œ์ด๋ธ” ํ˜•ํƒœ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.

...
    var json = body ? JSON.parse(body) : [];
...
    for(var i=0; i< len; i++) {
      var pagenum = off+i+1;
      pageinfo.pages.push({
        page: pagenum,
        query: querystring.stringify({
          date: getDateString(date),
          type: type,
          keyword: keyword,
          page: pagenum,
          size: size
        })
      });
    }

    renderFunction(req, res, 'visiting/list.html', {
      title: '๋ฐฉ๋ฌธ ์ผ์ • ์กฐํšŒ',
      date: getDateString(date),
      datetime: getDateTimeString(date),
      keyword: keyword,
      type: type,
      result: json.result.map((item, idx)=>{        
        var ret = Object.assign({}, item);
        ret.idx = (page - 1) * pageSize + idx + 1;
        // date
        ret.ndate = getDateTimeString(new Date(item.date));
        return ret;
      }),
      pageinfo : pageinfo
    });
...

๋ฐฉ๋ฌธ ์ •๋ณด ๋ชฉ๋ก Web UI Controller

๋ฐฉ๋ฌธ ์˜ˆ์ • ๋ชฉ๋ก ํ™”๋ฉด์ด ์•ž์„œ ๋กœ๊ทธ์ธ ํ™”๋ฉด๊ณผ ์กฐ๊ธˆ ๋‹ค๋ฅธ ๋ถ€๋ถ„์ด ์žˆ๋Š”๋ฐ ๋ฐ”๋กœ Web Browser์˜ JavaScript๋ฅผ ์ด์šฉํ•˜๋Š” ์ ์ž…๋‹ˆ๋‹ค. ์•ฝ๊ฐ„ ํ˜ผ๋™์ด ์˜ฌ ์ˆ˜ ์žˆ๊ฒ ๋Š”๋ฐ Node.js์šฉ ์„œ๋ฒ„ ์ฝ”๋“œ๋กœ ์‹คํ–‰๋˜๋Š” JavaScript์™€ Web Browser์—์„œ ์‹คํ–‰๋˜๋Š” JavaScript๋Š” ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณธ ๊ธ€์—์„œ๋Š” Web UI๋ฅผ ์ œ์–ดํ•˜๊ธฐ ์œ„ํ•ด JavaScript๋ฅผ ์ด์šฉํ•˜๋ฉฐ ํŠนํžˆ jQuery Library๋ฅผ ์ด์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๊ณตํ†ต HTML์ธ common-head.html ํŒŒ์ผ์„ ๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด jquery ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

...
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
...

๋˜ํ•œ, visiting/list.html์—์„œ๋„ ํ•ด๋‹น view๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋กœ๋”ฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

...
<script type="text/javascript" src="/scripts/visiting/list.js"></script>
...

smv-ui-app์—์„œ๋Š” public์ด๋ผ๋Š” ๋””๋ ‰ํ† ๋ฆฌ์— ์ด๋ฏธ์ง€ ๋ฆฌ์†Œ์Šค๋‚˜ ์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ์„ ๋‘๋ฉด Web Browser์—์„œ ์ ‘๊ทผ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. list.js JavaScript ํŒŒ์ผ์„ public/scripts/visiting ๋””๋ ‰ํ† ๋ฆฌ์— ๋‘๊ณ  ์œ„์™€ ๊ฐ™์ด HTML์„ ์ž‘์„ฑํ•˜๋ฉด ํ•ด๋‹น ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋กœ๋”ฉ๋˜์–ด ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// visiting/list.js
'use strict';

(function() {
  //
  $('#search').on('click', ()=>{
    //
    var date = $('#date').val();
    var type = $('#type').val();
    var keyword = $('#keyword').val();
    var query = [];
    var url = '/visiting/list';
    if (date) query.push(`date=${date}`);
    if (type) query.push(`type=${type}`);
    if (keyword) query.push(`keyword=${keyword}`);
    if (query.length > 0) {
      url += '?'+query.join('&');
    }

    window.location.href = url;
    return false;
  });

  $('table tr.visiting').on('click', (evt)=>{
    var $visiting = $(evt.currentTarget);
    //var $target = $(evt.target);

    //alert('click! '+$visiting.attr('data-id'));
    window.location.href = '/visiting/detail/'+$visiting.attr('data-id');

    return false;
  });

})();

list.js์—๋Š” ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ์ด click ์ด๋ฒคํŠธ๋ฅผ ์ธ์‹ํ•˜์—ฌ URL์„ ์กฐํ•ฉํ•˜๋Š” ๊ธฐ๋Šฅ๊ณผ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ๋ฌธ ์˜ˆ์ • ์ •๋ณด๋ฅผ ์„ ํƒํ•˜์—ฌ ์ƒ์„ธ ์ •๋ณด ํŽ˜์ด์ง€ view๋กœ ์ด๋™ํ•˜๋Š” ๋‘ ๊ฐ€์ง€์˜ ๊ธฐ๋Šฅ์ด ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

jQuery์— ๋Œ€ํ•œ ๋‚ด์šฉ์€ ๋‹ค์Œ ๋งํฌ๋ฅผ ์ฐธ๊ณ  ๋ฐ”๋ž๋‹ˆ๋‹ค.

์ดˆ๊ธฐ์—๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ฑ๋ก๋˜์–ด ์žˆ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์™€ ๊ฐ™์ด ํ•ญ๋ชฉ์ด ์—†๋Š” ํ™”๋ฉด์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐฉ๋ฌธ ์ •๋ณด ๋ชฉ๋ก ์ •๋ ฌ

์‚ฌ์‹ค ์•ž์„œ ์ž‘์„ฑํ•ด ๋†“์•˜๋˜ smv-visit ์„œ๋น„์Šค์—์„œ๋Š” ํ•œ๊ฐ€์ง€ ๋น ์ง„ ๋ถ€๋ถ„์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ”๋กœ ์กฐํšŒ๋œ ๋ชฉ๋ก์— ๋ณด์ด๋Š” ํ•ญ๋ชฉ๋“ค์ด ์•„์ดํ…œ ์ƒ์„ฑ ๋˜๋Š” ์—…๋ฐ์ดํŠธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ๋˜์–ด ์žˆ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์ธ ๊ฒฝ์šฐ ๋‚ ์งœ์™€ ๊ฐ™์€ ํŠน์ • ํ•ญ๋ชฉ์„ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌํ•˜๋Š”๋ฐ Cloudant์˜ ๊ฒฝ์šฐ๋Š” ์ €์žฅ๋œ document์˜ key๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ index๋ฅผ ์ƒ์„ฑํ•˜์ง€ ์•Š์ด๋ฏ€๋กœ find ๋ช…๋ น์œผ๋กœ ๋ฌธ์„œ๋ฅผ ์ฐพ์„ ๋•Œ sort ์˜ต์…˜์œผ๋กœ ํ˜ธ์ถœํ•˜๋ฉด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, ๋‹ค์Œ๊ณผ ๊ฐ™์ด Cloudant Dashboard์˜ Cloudant Query ๋ฉ”๋‰ด๋กœ ์ง„์ž…ํ•˜์—ฌ index๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

{
  "index": {
    "fields": [
      "date:number"
    ]
  },
  "type": "json"
}

์ธ๋ฑ์Šค๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ๋‹ค๋ฉด Cloudant Query ๋ฉ”๋‰ด์— Queryable indexes: ํ•ญ๋ชฉ์— json:number๋ผ๋Š” ํ˜•์‹์œผ๋กœ ์ถ”๊ฐ€๋œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฒ€์ƒ‰ ์˜ต์…˜์œผ๋กœ sort ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋‚ ์งœ ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ๋˜๋„๋ก ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

...
function searchVisits(req, res) {
...
      mydb.find({
        selector: selector,
        sort:[{'date:number': 'asc'}],
        limit: limit,
        skip: skip,
        use_index: queryIndex
      }, function(err, result) {
...

๋ฐฉ๋ฌธ ์ •๋ณด ๋“ฑ๋ก

๋ฐฉ๋ฌธ ์ •๋ณด ๋“ฑ๋ก ํ™”๋ฉด์€ ์ž„์ง์›(ESCORT)์ด ๋ฐฉ๋ฌธ ์˜ˆ์ •์ž ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜๋Š” ํ™”๋ฉด์ž…๋‹ˆ๋‹ค.

๋ฐฉ๋ฌธ ์ •๋ณด ๋“ฑ๋ก View

๋ฐฉ๋ฌธ ์ •๋ณด ๋ชฉ๋ก ํ™”๋ฉด์—์„œ ๋ฐฉ๋ฌธ๋“ฑ๋ก ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ์ง„์ž… ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฉ๋ฌธ ์ •๋ณด ๋ชฉ๋ก์šฉ list.js์—์„œ Script๋กœ ๋กœ๋”ฉํ•˜๋Š” ๋ฐฉ์‹์ด ์•„๋‹Œ ๋งํฌ๋ฅผ ์ œ๊ณตํ•˜์—ฌ ํŽ˜์ด์ง€ view๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

...
<button id="register" onclick="window.location='/visiting/register';">๋ฐฉ๋ฌธ๋“ฑ๋ก</button>
...

์ด์ œ ๋ฐฉ๋ฌธ ์ •๋ณด ๋“ฑ๋ก์„ ์œ„ํ•ด visiting/register.html๋ฅผ ์ค€๋น„ํ•ฉ๋‹ˆ๋‹ค.

<!DOCTYPE html>
<html>
<head>
    <%- include('../common-head.html'); %>
</head>
<body>
    <%- include('../common-header.html'); %>
    <div class = "container visiting register">
        <!-- ๋ฐฉ๋ฌธ์ž ๋“ฑ๋ก -->
        <div class="hidden">
            <input id="euserid" hidden value="<%=userinfo.userid%>">
            <input id="eemail" hidden value="<%=userinfo.email%>">
        </div>
        <table>
            <tbody>
                <tr>
                    <td>์ผ์‹œ</td>
                    <td><input id="datetime" type="datetime-local" value="<%=datetime%>"></td>
                    <td>์ž„์ง์›</td>
                    <td><input id="ename" disabled value="<%=userinfo.name%>"></td>
                </tr>
                <tr>
                    <td>์ด๋ฆ„</td>
                    <td><input id="name" value="Jane Doe"></td>
                    <td>์—ฐ๋ฝ์ฒ˜</td>
                    <td><input id="emobile" disabled value="<%=userinfo.mobile%>"></td>
                </tr>
                <tr>
                    <td>์ง์œ„</td>
                    <td><input id="title" value="Dr."></td>
                    <td>๋ถ€์„œ</td>
                    <td><input id="edept" disabled value="<%=userinfo.dept%>"></td>
                </tr>
                <tr>
                    <td>์—ฐ๋ฝ์ฒ˜</td>
                    <td><input id="contact" value="+82-1-1234-0678"></td>
                </tr>
                <tr>
                    <td>์ด๋ฉ”์ผ</td>
                    <td><input id="email" value="jane.doe@customer.com"></td>
                </tr>
                <tr>
                    <td>ํšŒ์‚ฌ</td>
                    <td><input id="company" value="Customers Inc"></td>
                </tr>
            </tbody>
        </table>
        <div>
            <button id="register">๋“ฑ๋ก</button>
            <button id="cancel" onclick="history.back(1);">์ทจ์†Œ</button>
        </div>
    </div>
    <script type="text/javascript" src="/scripts/visiting/register.js"></script>
    <%- include('../common-footer.html'); %>
</body>
</html>

๋ฐฉ๋ฌธ์ž ๋“ฑ๋ก ํŽ˜์ด์ง€์—๋Š” ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์ž„์ง์›์ด ESCORT๋กœ ์ž๋™์œผ๋กœ ์ž…๋ ฅ๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด์ธ userinfo๋ผ๋Š” ํ•ญ๋ชฉ ์ •๋ณด๋ฅผ ์ด์šฉํ•˜์—ฌ ์ž‘์„ฑ๋œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐฉ๋ฌธ ์ •๋ณด ๋“ฑ๋ก Controller

๋ฐฉ๋ฌธ ์ •๋ณด ๋“ฑ๋ก ํ™”๋ฉด์€ ์ž„์ง์›(ESCORT)์ด ๋ฐฉ๋ฌธ ์˜ˆ์ •์ž ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜๋Š” ํ™”๋ฉด์ž…๋‹ˆ๋‹ค. ๋ฐฉ๋ฌธ ์ •๋ณด ๋ชฉ๋ก ํ™”๋ฉด์—์„œ ๋ฐฉ๋ฌธ๋“ฑ๋ก ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ์ง„์ž… ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด์ œ SMVUIAppController.js ํŒŒ์ผ์— visiting/register์— ๋Œ€ํ•œ ์„œ๋น„์Šค ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

...
function registerView(req, res) {
  renderFunction(req, res, 'visiting/register.html', {
    title: '๋ฐฉ๋ฌธ์ž ๋“ฑ๋ก',
    datetime: getNextDateTimeString()
  });
}
...
  app.get(BASE_PATH + '/visiting/register', authenticatedViewFilter, registerView);
...

๊ฐ„๋‹จํ•˜๊ฒŒ visiting/register.html์ด ์‚ฌ์šฉ๋˜๋ฉฐ ๋“ฑ๋ก ๋‚ ์งœ ๋ฐ ์‹œ๊ฐ„ ์ •๋ณด๋ฅผ ์œ„ํ•ด getNextDateTimeString ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ •๋ณด๋ฅผ ์–ป์Šต๋‹ˆ๋‹ค. getNextDateTimeString ํ•จ์ˆ˜๋Š” ํ˜„์žฌ ์ž…๋ ฅ ์‹œ๊ฐ„์„ ๊ธฐ์ค€์œผ๋กœ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๋‚ ์งœ๋ฅผ ์–ป๋Š” ํ•จ์ˆ˜์ธ๋ฐ, ์šฐ์„ ์€ ๋‹ค์Œ๋‚  9์‹œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‚˜์ค‘์— ์ผ์ •ํ•œ ๊ทœ์น™์„ ๋‘๊ณ  ํ•ด๋‹น ๋‚ด์šฉ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

...
function getNextDateTimeString() {
  var date = new Date();
  date.setHours(9);
  date.setMinutes(0);
  date.setSeconds(0);
  date.setMilliseconds(0);
  // next day
  date.setDate(date.getDate()+1);

  return getDateTimeString(date);
}
...

๊ทธ๋ฆฌ๊ณ , HTML์— userinfo๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์ž„์ง์› ์ •๋ณด๊ฐ€ ์ž…๋ ฅ๋œ๋‹ค๊ณ  ํ–ˆ๋Š”๋ฐ ์‹ค์ œ renderFunction์— ์ „๋‹ฌ๋˜๋Š” ์ •๋ณด์—๋Š” ํ•ด๋‹น ์ •๋ณด๊ฐ€ ์—†๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” renderFunction ๋‚ด๋ถ€์— ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ์ฝ”๋“œ์—์„œ ์ž๋™์œผ๋กœ ์ž…๋ ฅ๋˜๋Š” ํ˜•ํƒœ๋ผ ๋ณ„๋‹ค๋ฅธ ์ •๋ณด ์ž…๋ ฅ์ด ์—†์–ด๋„ userinfo๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

...
function renderFunction(req, res, view, obj) {
  obj = obj || {};
  obj['SMV_VISIT_BASE_URL'] = SMV_VISIT_BASE_URL;

  var authtoken = extractViewAuthToken(req);
  SMVAuthTokenHelper.getAuthTokenValue(authtoken, 'userinfo', (result)=>{
      if (result) {
        obj['userinfo'] = JSON.parse(result); 
      }
      return res.render(view, obj);
  });
}
...

๋ฐฉ๋ฌธ ์ •๋ณด ๋“ฑ๋ก Web UI Controller

๋ฐฉ๋ฌธ ์˜ˆ์ • ๋ชฉ๋ก์ฒ˜๋Ÿผ ๋ฐฉ๋ฌธ ์ •๋ณด ๋“ฑ๋ก ํ™”๋ฉด์„ ์œ„ํ•œ JavaScript ์ฝ”๋“œ๋กœ public/scripts/visiting/register.js ํŒŒ์ผ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

register๋ผ๋Š” id๋ฅผ ๊ฐ€์ง„ ๋ฒ„ํŠผ์ด ํด๋ฆญ๋˜๋ฉด ํ˜„์žฌ ํ™”๋ฉด์— ์žˆ๋Š” ๋ชจ๋“  input์˜ ๊ฐ’์„ ์ˆ˜์ง‘ํ•˜๊ณ  ๋งŒ์•ฝ ๊ฐ’์ด ๋น„์–ด ์žˆ๋Š” ๊ฒฝ์šฐ๋Š” ์ง„ํ–‰ํ•˜์ง€ ์•Š๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ๋ฐฉ๋ฌธ์ž์™€ ์ž„์ง์› ์ •๋ณด๊ฐ€ ๋ชจ๋‘ ์ž…๋ ฅ๋œ ๊ฒƒ์„ ํ™•์ธํ•˜๋ฉด smv-visit API๋ฅผ XHR(XML HTTP Request)๋กœ ํ˜ธ์ถœํ•˜์—ฌ ์ƒˆ๋กœ์šด ๋ฐฉ๋ฌธ ์˜ˆ์ • ์ •๋ณด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. $.ajax๋Š” jQuery์—์„œ ์ œ๊ณตํ•˜๋Š” XHR ํ˜ธ์ถœ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

// visiting/register.js
'use strict';

(function() {
  // 
  $('#register').on('click', ()=>{
    // check fields
    var checked = Array.prototype.some.call($('input'), function(field){
      return !$(field).val();
    });
    if (checked) {
      alert('์ž…๋ ฅ๋˜์ง€ ์•Š์€ ์ •๋ณด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.');
      return false;
    }

    // visitor info
    var datetime = $('#datetime').val();
    var name = $('#name').val();
    var title = $('#title').val();
    var contact = $('#contact').val();
    var email = $('#email').val();
    var company = $('#company').val();

    // employee info
    var euserid = $('#euserid').val();
    var ename = $('#ename').val();
    var eemail = $('#eemail').val();
    var emobile = $('#emobile').val();
    var edept = $('#edept').val();

    console.log(`${datetime}, ${name}, ${title}, ${contact}, ${company}`);
    console.log(`${euserid}, ${ename}, ${eemail}, ${emobile}, ${edept}`);
    
    var visitObject = {
      'date': new Date(datetime).toString(),
      'visitor': {
        'name': name,
        'title': title,
        'contact': contact,
        'email': email,
        'company': company
      },
      'escort': {
        'id': euserid,
        'name': ename,
        'email': eemail,
        'dept': edept,
        'mobile': emobile
      },
      'agreement': undefined,
      'badge': undefined
    };

    $.ajax({
      type: 'POST',
      url: getSmvVisitBase('/api/smv/v1/visit'),
      xhrFields: {
        withCredentials: true
      },
      data: visitObject,
      success: function (data) {
        console.log(data);
        alert('๋“ฑ๋ก ์„ฑ๊ณต');
        window.location.href = '/visiting/list';
      },
      error: function (err) {
        console.log(err);
        if (err.status == 401) {
          // redirect to login
          window.location.href = '/login?error='+err.responseText;
        } else {
          alert('error:'+err.responseText);
        }
      }
    });

    return false;
  });

})();

์™„์„ฑ๋œ ํ™”๋ฉด์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋ฐฉ๋ฌธ ์ •๋ณด๋ฅผ ๋“ฑ๋กํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฐฉ๋ฌธ ์ •๋ณด ๋ฆฌ์ŠคํŠธ์—์„œ ํ•ญ๋ชฉ์ด ๋‚˜ํƒ€๋‚˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐฉ๋ฌธ ์ •๋ณด ์ƒ์„ธ

๋ฐฉ๋ฌธ ์ •๋ณด ์ƒ์„ธ ํ™”๋ฉด์€ ์ด๋ฏธ ๋“ฑ๋ก๋œ ์ •๋ณด๋ฅผ ํ™•์ธ, ๋ณ€๊ฒฝ ๋˜๋Š” ์‚ญ์ œ๋ฅผ ์œ„ํ•œ ํ™”๋ฉด์ž…๋‹ˆ๋‹ค.

๋ฐฉ๋ฌธ ์ •๋ณด ์ƒ์„ธ View

๋ฐฉ๋ฌธ ์˜ˆ์ • ๋ชฉ๋ก ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•˜๋ฉด ํ•ด๋‹น ์˜ˆ์ • ์ •๋ณด์˜ ์ƒ์„ธ ํ™”๋ฉด์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ๋ฐฉ๋ฌธ ์ •๋ณด ๋“ฑ๋ก ํ™”๋ฉด๊ณผ ์ƒ๋‹นํžˆ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค. list.js์—์„œ JavaScript๋ฅผ ํ†ตํ•ด detail ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.

...
  $('table tr.visiting').on('click', (evt)=>{
    var $visiting = $(evt.currentTarget);
    //var $target = $(evt.target);

    //alert('click! '+$visiting.attr('data-id'));
    window.location.href = '/visiting/detail/'+$visiting.attr('data-id');

    return false;
  });
...

์ด์ œ ๋ฐฉ๋ฌธ ์ •๋ณด ๋“ฑ๋ก์„ ์œ„ํ•ด visiting/detail.html๋ฅผ ์ค€๋น„ํ•ฉ๋‹ˆ๋‹ค.

<!DOCTYPE html>
<html>
<head>
    <%- include('../common-head.html'); %>
</head>
<body>
    <%- include('../common-header.html'); %>
    <div class = "container visiting detail">
        <!-- ๋ฐฉ๋ฌธ์ž ์ƒ์„ธ ์ •๋ณด -->

<% if (typeof error != "undefined") { %>
        <div class="error">
            ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: <%=error%>
        </div>
        <div>
            <button id="cancel" onclick="history.back(1);">๋’ค๋กœ</button>
        </div>
<% } else { %>
        <div class="hidden">
            <input id="euserid" hidden value="<%=escort.id%>">
            <input id="eemail" hidden value="<%=escort.email%>">
            <input id="visitingid" hidden value="<%=visitingid%>">
        </div>
        <table>
            <tbody>
                <tr>
                    <td>์ผ์‹œ</td>
                    <td><input id="datetime" type="datetime-local" value="<%=ndate%>"></td>
                    <td>์ž„์ง์›</td>
                    <td><input id="ename" disabled value="<%=escort.name%>"></td>
                </tr>
                <tr>
                    <td>์ด๋ฆ„</td>
                    <td><input id="name" value="<%=visitor.name%>"></td>
                    <td>์—ฐ๋ฝ์ฒ˜</td>
                    <td><input id="emobile" disabled value="<%=escort.mobile%>"></td>
                </tr>
                <tr>
                    <td>์ง์œ„</td>
                    <td><input id="title" value="<%=visitor.title%>"></td>
                    <td>๋ถ€์„œ</td>
                    <td><input id="edept" disabled value="<%=escort.dept%>"></td>
                </tr>
                <tr>
                    <td>์—ฐ๋ฝ์ฒ˜</td>
                    <td><input id="contact" value="<%=visitor.contact%>"></td>
                    <td>์นด๋“œ๋ฒˆํ˜ธ</td>
                    <td><input id="cardnumber" disabled></td>
                </tr>
                <tr>
                    <td>ํšŒ์‚ฌ</td>
                    <td><input id="company" value="<%=visitor.company%>"></td>
                    <td>๋ณด์•ˆ์„œ๋ช…</td>
                    <td><span><a onclick="alert('๋Œ€๊ธฐ์ค‘');">๋Œ€๊ธฐ</a></span></td>
                </tr>
            </tbody>
        </table>
        <div>
            <button id="update">๋ณ€๊ฒฝ</button>
            <button id="delete">์‚ญ์ œ</button>
            <button id="cancel" onclick="history.back(1);">๋’ค๋กœ</button>
        </div>
<% } %>
    </div>
    <script type="text/javascript" src="/scripts/visiting/detail.js"></script>
    <%- include('../common-footer.html'); %>
</body>
</html>

๋ฐฉ๋ฌธ ์ •๋ณด ์ƒ์„ธ Controller

์ด์ œ SMVUIAppController.js ํŒŒ์ผ์— visiting/detail์— ๋Œ€ํ•œ ์„œ๋น„์Šค ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

...
function detailView(req, res) {
  console.log(`visiting detail with id : ${req.params.id}`);

  var authtoken = extractViewAuthToken(req);
  var visitingid = req.params.id;

  // get visit item with id
  request.get({
    url: `${SMV_VISIT_BASE_URL}/api/smv/v1/visit/${visitingid}`,
    headers: buildAuthHeaders(authtoken)
  }, function(error, response, body) {
    if (error) {
      console.log(error);
      return renderFunction(req, res, 'visiting/detail.html', {
        title: '๋ฐฉ๋ฌธ์ž ์ƒ์„ธ ์ •๋ณด',
        error: error
      });
    }

    // Check Error Condition
    if (response.statusCode != 200) {
      return renderFunction(req, res, 'visiting/detail.html', {
        title: '๋ฐฉ๋ฌธ์ž ์ƒ์„ธ ์ •๋ณด',
        error: `Response with code ${response.statusCode}, ${body}`
      });
    }

    var json = body ? JSON.parse(body) : {};
    console.log(json);

    json.title = '๋ฐฉ๋ฌธ์ž ์ƒ์„ธ ์ •๋ณด';
    json.visitingid = visitingid;
    json.ndate = getDateTimeString(new Date(json.date));
    renderFunction(req, res, 'visiting/detail.html', json);
  });
}
...
  app.get(BASE_PATH + '/visiting/detail/:id', authenticatedViewFilter, detailView);
...

๋ฐฉ๋ฌธ ์ •๋ณด ์ƒ์„ธ Web UI Controller

๋ฐฉ๋ฌธ ์˜ˆ์ • ๋“ฑ๋ก์ฒ˜๋Ÿผ ๋ฐฉ๋ฌธ ์ •๋ณด ์ƒ์„ธ ํ™”๋ฉด์„ ์œ„ํ•œ JavaScript ์ฝ”๋“œ๋กœ public/scripts/visiting/detail.js ํŒŒ์ผ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ , smv-visit API๋„ XHR ๋ฐฉ์‹์œผ๋กœ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ์กด ์ •๋ณด๋ฅผ ํŽธ์ง‘ํ•˜๋ ค๋ฉด PUT ๋ฐฉ์‹์„ ์ด์šฉํ•ฉ๋‹ˆ๋‹ค.

  $('#update').on('click', ()=>{
    // check fields
    var checked = Array.prototype.some.call($('input.visitor'), function(field){
      return !$(field).val();
    });
    if (checked) {
      alert('์ž…๋ ฅ๋˜์ง€ ์•Š์€ ์ •๋ณด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.');
      return false;
    }

    // 
    var visitingid = $('#visitingid').val();
    var datetime = $('#datetime').val();

    // visitor info
    var name = $('#name').val();
    var title = $('#title').val();
    var contact = $('#contact').val();
    var email = $('#email').val();
    var company = $('#company').val();

    console.log(`${datetime}, ${name}, ${title}, ${contact}, ${company}`);

    var visitObject = {
      'date': new Date(datetime).toString(),
      'visitor': {
        'name': name,
        'title': title,
        'contact': contact,
        'email': email,
        'company': company
      },
      //'agreement': undefined,
      //'badge': undefined
    };

    $.ajax({
      type: 'PUT',
      url: getSmvVisitBase(`/api/smv/v1/visit/${visitingid}`),
      xhrFields: {
        withCredentials: true
      },
      data: visitObject,
      success: function (data) {
        console.log(data);
        alert('๋ณ€๊ฒฝ ์„ฑ๊ณต');
        //window.location.href = '/visiting/list';
      },
      error: function (err) {
        console.log(err);
        if (err.status == 401) {
          // redirect to login
          window.location.href = '/login?error='+err.responseText;
        } else {
          alert('error:'+err.responseText);
        }
      }
    });

    return false;
  });

๊ทธ๋ฆฌ๊ณ , ์‚ญ์ œ๋ฅผ ํ•˜๊ธฐ์œ„ํ•ด์„œ๋Š” DELETE ๋ฐฉ์‹์„ ์ด์šฉํ•ฉ๋‹ˆ๋‹ค.

  $('#delete').on('click', ()=>{

    var visitingid = $('#visitingid').val();
    
    $.ajax({
      type: 'DELETE',
      url: getSmvVisitBase(`/api/smv/v1/visit/${visitingid}`),
      xhrFields: {
        withCredentials: true
      },
      success: function (data) {
        console.log(data);
        alert('์‚ญ์ œ ์„ฑ๊ณต');
        //window.location.href = '/visiting/list';
        history.back(1);
      },
      error: function (err) {
        console.log(err);
        if (err.status == 401) {
          // redirect to login
          window.location.href = '/login?error='+err.responseText;
        } else {
          alert('error:'+err.responseText);
        }
      }
    });

    return false;
  });

์™„์„ฑ๋œ ํ™”๋ฉด์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

Local test ํ™˜๊ฒฝ์—์„œ Bluemix ์„œ๋ฒ„๋กœ ๋ฐฐํฌ์‹œ ์ด์Šˆ ์ •๋ฆฌ

XSRF Cross Domin ์ด์Šˆ

์‚ฌ์‹ค Server Code์—์„œ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ ๋ฌธ์ œ๊ฐ€ ๋˜์ง€ ์•Š์ง€๋งŒ Web Browser์—์„œ API๋ฅผ ํ˜ธ์ถœ ํ•  ๋•Œ ์„œ๋ฒ„์˜ domain name๊ณผ ๋‹ค๋ฅธ ๊ฒฝ์šฐ cross-domain ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฐ ๊ฒฝ์šฐ API๋ฅผ ์ œ๊ณตํ•˜๋Š” ์„œ๋ฒ„์—์„œ Cross Domin์„ ํ—ˆ์šฉํ•˜๋Š” ์„œ๋ฒ„์˜ domain name์„ ์ž…๋ ฅํ•˜๋„๋ก ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณธ ๊ธ€์—์„œ๋Š” smv-visit ์„œ๋น„์Šค๊ฐ€ ๋Œ€์ƒ์ด ๋˜๋Š”๋ฐ ํ•ด๋‹น ์ •๋ณด๋Š” smv-ui-app์—์„œ๋งŒ ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜๋ฏ€๋กœ SMV_UI_APP_BASE_URL ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ •๋ณด๋ฅผ ํ™œ์šฉํ•˜์—ฌ app.js์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•ด ์ฃผ๋Š” ๊ฒƒ์œผ๋กœ ํ•ด๊ฒฐ ํ•ฉ๋‹ˆ๋‹ค.

...
const SMV_UI_APP_BASE_URL = process.env['SMV_UI_APP_BASE_URL'];

// to be placed before api service handlers
app.use(function (req, res, next) {

  // Website you wish to allow to connect
  res.setHeader('Access-Control-Allow-Origin', SMV_UI_APP_BASE_URL);

  // Request methods you wish to allow
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');

  // Request headers you wish to allow
  res.setHeader('Access-Control-Allow-Headers', 'X-AUTH-TOKEN,applicaiton/json');

  // Set to true if you need the website to include cookies in the requests sent
  // to the API (e.g. in case you use sessions)
  res.setHeader('Access-Control-Allow-Credentials', true);

  // Pass to next layer of middleware
  next();
});
...

์ธ์ฆ ํ† ํฐ์„ XHR Cookie๋กœ ์ „๋‹ฌ

์ดˆ๊ธฐ ์„œ๋น„์Šค API ์„ค๊ณ„ ์‹œ ์ธ์ฆ ํ† ํฐ์€ Request Header๋ฅผ ํ†ตํ•ด ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๊ตฌ์ƒํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, UI App์˜ ๊ฒฝ์šฐ Page์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ธ์ฆ ์ •๋ณด ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด token ์ •๋ณด๋ฅผ cookie๋กœ ๊ด€๋ฆฌ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์•ž์„œ XHR์˜ Cross Domain ์ด์Šˆ๋Š” ์„œ๋ฒ„ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ํ•ด๊ฒฐ์„ ํ–ˆ๋Š”๋ฐ cookie๋Š” UI App๊ณผ Service API์˜ domain ์ด๋ฆ„์„ ๋™์ผํ•œ ๊ฒƒ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ด์ƒ ๋ฌธ์ œ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ๋กœ์„œ bluemix์— ๋ฐฐํฌ์‹œ ์–ป๋Š” domain์€ .mybluemix.net์ด๋ฏ€๋กœ ์ด๋ฅผ cookie domain์œผ๋กœ ์„ค์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ๋งŒ์•ฝ ๋ฌธ์ œ๊ฐ€ ๋œ๋‹ค๋ฉด cookie ์ •๋ณด๋ฅผ ์ง์ ‘ ์ฝ์–ด API ํ˜ธ์ถœ header์— ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ด์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋จผ์ € cookie domain์ด๋ฆ„์„ .mybluemix.net๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒฝ์šฐ๋Š” smv-ui-app์˜ SMVUIAppController.js์—์„œ cookie ์ƒ์„ฑ ์ฝ”๋“œ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

...
    // keep authtoken as cookie
    res.cookie(AUTH_TOKEN_KEY, authtoken, {
      domain: '.mybluemix.net',
      maxAge: EXPIRES_IN_SECS*1000 // in milliseconds
    });
...

๊ทธ๋ฆฌ๊ณ , API์—์„œ๋„ ์ธ์ฆ token ์ˆ˜์ง‘ ์‹œ cookie์—์„œ๋„ ์–ป์„ ์ˆ˜ ์žˆ๋„๋ก SMVVisitController.js์˜ extractAuthToken ํ•จ์ˆ˜๋ฅผ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.

function extractAuthToken(req) {
  var token = req.headers[AUTH_TOKEN_KEY] || req.headers[AUTH_TOKEN_KEY.toLowerCase()] || req.cookies[AUTH_TOKEN_KEY];

  if (!token) {
    console.error(`${AUTH_TOKEN_KEY} is not in headers or cookies`);
  }
  console.log(token);
  return token;
}

XHR Header๋กœ ์ธ์ฆ ํ† ํฐ ์ „๋‹ฌ

XHR ํ˜ธ์ถœ ์‹œ ์ „๋‹ฌํ•˜๋Š” ์ธ์ฆ ํ† ํฐ์„ Header๋กœ ์ „๋‹ฌํ•˜๋Š” ๊ฒฝ์šฐ common-head.html์—์„œ ์ธ์ฆ ํ† ํฐ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๋Š” JavaScript ํ•จ์ˆ˜๋ฅผ ๊ตฌ์„ฑํ•˜์—ฌ ์ด๋ฅผ ํ™œ์šฉ ํ•˜๋„๋ก ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

...
        function getAuthTokenHeader(header) {
            header = header || {};
            var obj = Object.assign({}, header);
<% if (typeof AUTH_TOKEN_VALUE != "undefined") { %>
            obj[`<%=AUTH_TOKEN_KEY%>`] = `<%=AUTH_TOKEN_VALUE%>`;
<% } %>
            return obj;
        }
...

๊ทธ๋ฆฌ๊ณ  XHR ํ˜ธ์ถœ ์‹œ getAuthTokenHeader ๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

...
var headersObject = getAuthTokenHeader();

    $.ajax({
      type: 'POST',
      url: getSmvVisitBase('/api/smv/v1/visit'),
      xhrFields: {
        withCredentials: true
      },
      headers: headersObject,
...

Timezone ์ฐจ์ด

๊ธฐ๋ณธ์ ์œผ๋กœ ์›น ๋ธŒ๋ผ์šฐ์ €๋Š” ์‹œ์Šคํ…œ์ด ์ œ๊ณตํ•˜๋Š” TimeZone์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ PC๋˜๋Š” Server์— ์„ค์ •๋œ Timezone์ด ํ•œ๊ตญ์ธ ๊ฒฝ์šฐ KST(Korea Standard Timezone)์œผ๋กœ GMT+09:00๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฏ€๋กœ Local์—์„œ ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ์™€ ์ด๋ฅผ ์„œ๋ฒ„๋กœ ๋ฐฐํฌ ํ›„ ์‹คํ–‰ํ•  ๋•Œ ์ฐจ์ด๊ฐ€ ๋ฐœ์ƒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ, Web Browser์—์„œ ์‚ฌ์šฉํ•˜๋Š” Timezone๊ณผ ์„œ๋ฒ„์™€ ๋‹ค๋ฅผ ๊ฒฝ์šฐ๋Š” ๋”์šฑ ๋ฌธ์ œ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฐ๊ฒฝ์šฐ ์›น ๋ธŒ๋ผ์šฐ์ €์™€ ์„œ๋ฒ„์— ์ƒ๊ด€ ์—†์ด ์ ˆ๋Œ€์‹œ๊ฐ„ ์ •๋ณด๋ฅผ ์ด์šฉํ•˜๊ธฐ๋„ ํ•˜์ง€๋งŒ, ๋ณธ ๊ธ€์—์„œ์™€ ๊ฐ™์ด ์›น ๋ธŒ๋ผ์šฐ์ € ๊ธฐ์ค€ ๋‚ ์งœ๋กœ ๊ฒ€์ƒ‰ํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ์‹ค์ œ ์ „๋‹ฌ๋˜์–ด์•ผ ํ•  ์ •๋ณด๋Š” ๋‚ ์งœ์™€ ๋”๋ถˆ์–ด Timezone ์ •๋ณด (๋˜๋Š” offset)๋„ ๊ฐ™์ด ์ „๋‹ฌ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

smv-visit์—์„œ ์กฐํšŒ๋ฅผ ์œ„ํ•œ searchVisits ํ•จ์ˆ˜์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”๋“œ๊ฐ€ ๋“ค์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

...
    // Convert date in msec
    var d = new Date(date);
    var sdate = (d.getTime()-1000*(d.getSeconds()+d.getMinutes()*60+d.getHours()*3600)-d.getMilliseconds());
...

์˜๋„ ํ–ˆ๋˜ ๊ฒƒ์€ ์ ˆ๋Œ€ ์‹œ๊ฐ„์œผ๋กœ ์ž…๋ ฅ๋ฐ›์€ ์ •๋ณด์—์„œ ๋‚ ์งœ ์ •๋ณด๋งŒ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์‹œ,๋ถ„,์ดˆ, ๋ฐ€๋ฆฌ์ดˆ ์ •๋ณด๋ฅผ 0์œผ๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด์—ˆ๋Š”๋ฐ ์ด๋Š” Server์˜ Timezone์„ ๊ธฐ์ค€์œผ๋กœ ์ฒ˜๋ฆฌ๊ฐ€ ๋˜๋Š” ๊ฒƒ์ด๋ฏ€๋กœ ์˜ค๋™์ž‘์„ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. API ์ƒ์œผ๋กœ ์ œ๊ณต๋˜๋Š” ๋ถ€๋ถ„์ด๋ฏ€๋กœ Web Browser ์‹œ๊ฐ„์„ ๊ธฐ์ค€์œผ๋กœ 0์‹œ 0๋ถ„ 0์ดˆ๋ฅผ ์ž‘์„ฑํ•˜์—ฌ API๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ํ•ด๋‹น ์‹œ๊ฐ„์„ ๊ธฐ์ค€์œผ๋กœ +24์‹œ๊ฐ„ ๋™์•ˆ์˜ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š” ํ˜•ํƒœ๋กœ ์ œ๊ณต๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

...
    // Convert date in msec
    var d = new Date(date);
    var sdate = d.getTime();
...

๋˜ํ•œ, smv-ui-app์—์„œ query๋กœ ์ „๋‹ฌํ•˜๋Š” ๊ฒฝ์šฐ๋„ text ๊ฐ’์ด ์•„๋‹Œ ๋ฐ€๋ฆฌ์ดˆ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ํ˜•ํƒœ์˜ ์‹œ๊ฐ„ ์ •๋ณด๋กœ ์ „๋‹ฌ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค๋งŒ, ์„œ๋ฒ„์—์„œ ์‚ฌ์šฉํ•˜๋Š” Timezone ์ •๋ณด๋ฅผ KST๋กœ ๊ณ ์ •ํ•˜๊ณ  ์ด๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ํ˜•ํƒœ๋กœ ์ œ๊ณตํ•˜๋Š” ๊ฒฝ์šฐ๋„ ๋งŽ์Šต๋‹ˆ๋‹ค. KST์˜ TimeZone ์ด๋ฆ„์€ Asia/Seoul ์ด๋ฉฐ, GMT+9์‹œ๊ฐ„์˜ ์ฐจ์ด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฏ€๋กœ ์ด๋ฅผ ์ด์šฉ ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ ๋‚ ์งœ์™€ ์‹œ๊ฐ„ ๋“ฑ์„ ์–ป๊ธฐ์œ„ํ•œ ๊ณ„์‚ฐ์„ ํ•ด์•ผ ํ•˜๋ฏ€๋กœ ์ด๋ฅผ ์ง€์›ํ•˜๋Š” module์„ ์‚ฌ์šฉํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

์ด ๋ชจ๋“ˆ์„ ์„ค์น˜ํ•˜๊ณ  ์ด๋ฅผ ์ด์šฉํ•œ ๋‚ ์งœ ๋ฐ ์‹œ๊ฐ„ ์ •๋ณด๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.

...
const TIMEZONE_NAME = 'Asia/Seoul';
const DATE_FORMAT = 'YYYY-MM-DD';
const DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm';
...
var moment = require('moment-timezone');
...
function getDateString(date) {
  return moment.tz(date, TIMEZONE_NAME).format(DATE_FORMAT);
}

function getDateTimeString(date) {
  return moment.tz(date, TIMEZONE_NAME).format(DATETIME_FORMAT);
}

function visitinglistView(req, res) {
...
  var dateText = req.query.date ? req.query.date : moment.tz(new Date(), TIMEZONE_NAME).format(DATE_FORMAT);

  // converted
  var date = moment.tz(dateText, TIMEZONE_NAME).toDate();
...

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฉ๋ฌธ์ž ๊ด€๋ฆฌ ์„œ๋น„์Šค๋“ค๊ณผ ์ด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” Web UI ์•ฑ์— ๋Œ€ํ•œ ํ”„๋กœํ† ํƒ€์ž…์„ ๊ตฌ์„ฑํ•ด ๋ณด์•˜์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๊ธ€์—์„œ๋Š” ํ˜„์žฌ ๊ฐœ๋ฐœ๋œ ์ฝ”๋“œ๋ฅผ Bluemix Cloud๋กœ ๋ฐฐํฌํ•˜๊ณ  ์ด์— ๋Œ€ํ•œ DevOps ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋‚ด์šฉ์œผ๋กœ ์ง„ํ–‰ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  1. ํด๋ผ์šฐ๋“œ ํ™˜๊ฒฝ ์ดํ•ด
  2. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ์ƒ ๋ฐ ์š”๊ฑด ์ •์˜
  3. ์š”๊ฑด์— ๋Œ€ํ•œ Usecase ๋ฐ Wireframe ์ž‘์„ฑ
  4. ๋งˆ์ดํฌ๋กœ ์„œ๋น„์Šค ์•„ํ‚คํ…์ณ ์„ค๊ณ„
  5. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋ฒ„ ํ™˜๊ฒฝ ์ค€๋น„
  6. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์ค€๋น„
  7. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ”„๋กœํ† ํƒ€์ž… ์ž‘์„ฑ Part1
  8. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ”„๋กœํ† ํƒ€์ž… ์ž‘์„ฑ Part2
  9. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ”„๋กœํ† ํƒ€์ž… ์ž‘์„ฑ Part3
  10. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ DevOps ํ™˜๊ฒฝ ๊ตฌ์„ฑ

์ฐธ๊ณ 

ํ† ๋ก  ์ฐธ๊ฐ€

์ด๋ฉ”์ผ์€ ๊ณต๊ฐœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•„์ˆ˜ ์ž…๋ ฅ์ฐฝ์€ * ๋กœ ํ‘œ์‹œ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.