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


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

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

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

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

๋ฐฉ๋ฌธ ์ •๋ณด ์„œ๋น„์Šค๋Š” ์‚ฌ์šฉ์ž ์ธ์ฆ ์„œ๋น„์Šค์™€ ๋‹ฌ๋ฆฌ SDK for Node.js Runtime
์—์„œ ์ƒ์„ฑํ•œ ์ฝ”๋“œ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

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

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

'use strict';

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

function newVisit(req, res) {

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

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

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

function updateVisit(req, res) {
  var id = req.params.id;
  
  console.log(`visit id ${id}`);

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

function deleteVisit(req, res) {
  var id = req.params.id;
  
  console.log(`visit id ${id}`);

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

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

module.exports = function(app, options) {
  app.post(BASE_PATH, newVisit);
  app.get(BASE_PATH + '/:id', getVisit);
  app.put(BASE_PATH + '/:id', updateVisit);
  app.delete(BASE_PATH + '/:id', deleteVisit);

  app.get(BASE_PATH + '/search', searchVisits);
};

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

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

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

์•ž์„œ ์‚ฌ์šฉ์ž ์ธ์ฆ ์„œ๋น„์Šค์—์„œ ๊ตฌ์„ฑํ•œ ๊ณตํ†ต ๋ชจ๋“ˆ์ธ SMVAuthTokenHelper.js๋ฅผ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค. NPM package๋กœ ๋“ฑ๋ก๋˜์–ด ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ์ด์šฉํ•  ์ˆ˜๋„ ์žˆ๊ฒ ์œผ๋‚˜ ๊ฐ๊ฐ์˜ ๊ตฌ๋ถ„๋œ ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋ฏ€๋กœ ์ด ๋ถ€๋ถ„์€ ์ง์ ‘ ๋ณต์ œ๋ฅผ ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ , SMVVisitController.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, newVisit);
  app.get(BASE_PATH + '/:id', authenticatedFilter, getVisit);
  app.put(BASE_PATH + '/:id', authenticatedFilter, updateVisit);
  app.delete(BASE_PATH + '/:id', authenticatedFilter, deleteVisit);

  app.get(BASE_PATH + '/search', authenticatedFilter, searchVisits);
};

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์„ฑ

์‚ฌ์šฉ์ž ์ธ์ฆ ์„œ๋น„์Šค์—์„œ์™€ ๊ฐ™์ด MochaJS๋ฅผ ์ด์šฉํ•œ JavaScript ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

$ npm install mocha --save-dev

๊ธฐ์กด ์‚ฌ์šฉ์ž ์ธ์ฆ ์„œ๋น„์Šค์—์„œ ์‚ฌ์šฉํ•˜๋˜ test-authtokenhelper.js๋ฅผ test ํด๋”์— ๋ณต์‚ฌํ•˜๊ณ  ์ •์ƒ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ ํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ง€ ์ธ์ฆ ์„œ๋น„์Šค์™€ ๋‹ฌ๋ฆฌ ์‹ค์ œ ๋…ธ์ถœ๋˜๋Š” REST API ์„œ๋น„์Šค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœํ•œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ๋งŒ๋“ค์–ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

๋จผ์ € request ๋ชจ๋“ˆ์„ ๋‹ค์Œ ๋ช…๋ น์œผ๋กœ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

$ npm install request --save-dev

๊ทธ๋ฆฌ๊ณ  test/test-smv-visit.js ํŒŒ์ผ์„ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‚ด์šฉ์œผ๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

#!/usr/local/bin/node

/* eslint-env mocha */

var appEnv = require('cfenv').getAppEnv();

const BASE_PATH = appEnv.url+'/api/smv/v1/visit';
const TEST_TIMEOUT = 60*1000; // 60 seconds

var assert = require('assert'),
  SMVAuthTokenHelper = require('../controllers/SMVAuthTokenHelper'),
  request = require('request');

describe('[SMVVisit API Test]', function() {
  var authtoken,
    testid = 1234;

  before(function () {
    //console.log('>>>>>>>>>>>>>>> before');
  });

  after(function () {
    //console.log('after <<<<<<<<<<<<<<<<');
  });

  this.timeout(TEST_TIMEOUT);

  describe('POST /', function() {
    it('returns status code 200', function(done) {
      request.post(BASE_PATH, function(error, response, body) {

        assert.equal(200, response.statusCode);
        done();
      });
    });
  });

  describe('GET /{id}', function() {
    it('returns status code 200', function(done) {
      request.get(`${BASE_PATH}/${testid}`, function(error, response, body) {

        assert.equal(200, response.statusCode);
        done();
      });
    });
  });

  describe('PUT /{id}', function() {
    it('returns status code 200', function(done) {
      request.put(`${BASE_PATH}/${testid}`, function(error, response, body) {

        assert.equal(200, response.statusCode);
        done();
      });
    });
  });

  describe('DELETE /{id}', function() {
    it('returns status code 200', function(done) {
      request.delete(`${BASE_PATH}/${testid}`, function(error, response, body) {

        assert.equal(200, response.statusCode);
        done();
      });
    });
  });

  describe('GET /search', function() {
    it('returns status code 200', function(done) {
      request.get(`${BASE_PATH}/search`, function(error, response, body) {

        assert.equal(200, response.statusCode);
        done();
      });
    });
  });
});

์ด ์ƒํƒœ์—์„œ mocha test๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

$ ./test.sh 

> SMVVisitService@0.0.3 test /Project/DevWorks/smv/projects/smv-visit
> mocha



  [SMVAuthTokenHelper Unit Test]
    โœ“ AuthToken generation (712ms)
    โœ“ AuthToken validation (176ms)
    โœ“ AuthToken validation negative test (175ms)
    โœ“ AuthToken set value (175ms)
    โœ“ AuthToken get value (175ms)
    โœ“ AuthToken invalidate (349ms)

  [SMVVisit API Test]
    POST /
      1) returns status code 200
    GET /{id}
      2) returns status code 200
    PUT /{id}
      3) returns status code 200
    DELETE /{id}
      4) returns status code 200
    GET /search
      5) returns status code 200


  6 passing (3s)
  5 failing

  1) [SMVVisit API Test] POST / returns status code 200:

      Uncaught AssertionError: 200 == 401
      + expected - actual

      -200
      +401
      
      at Request._callback (test/test-smv-visit.js:32:16)
      at Request.self.callback (node_modules/request/request.js:188:22)
      at Request.<anonymous> (node_modules/request/request.js:1171:10)
      at IncomingMessage.<anonymous> (node_modules/request/request.js:1091:12)
      at endReadableNT (_stream_readable.js:974:12)
      at _combinedTickCallback (internal/process/next_tick.js:80:11)
      at process._tickCallback (internal/process/next_tick.js:104:9)
...

์•ž์„œ ์ž‘์„ฑํ•œ authenticatedFilter ํ•จ์ˆ˜์—์„œ ์‚ฌ์šฉ์ž ์ธ์ฆ ํ† ํฐ์ด ์—†์–ด 401 Unauthorized๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์ƒํ™ฉ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์ธ์ฆ ํ† ํฐ ๋ฐœ๊ธ‰์„ ์œ„ํ•ด ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์—์„œ ๋กœ๊ทธ์ธ API๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์ด๋ฅผ header๋ฅผ ํ†ตํ•ด ์ „๋‹ฌํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

const AUTH_TOKEN_KEY = 'X-AUTH-TOKEN';
const BASE_PATH = appEnv.url+'/api/smv/v1/visit';
const TEST_TIMEOUT = 60*1000; // 60 seconds
const SMV_USERAUTH_BASE_URL = process.env['SMV_USERAUTH_BASE_URL'];

...

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

...

describe('[SMVVisit API Test]', function() {
  var authtoken,
    testid;

  this.timeout(TEST_TIMEOUT);

  before(function (done) {
    // Login
    request.post({
      url: SMV_USERAUTH_BASE_URL+'/api/smv/v1/auth/login',
      form: {
        email: 'john.doe@acme.ibm.com',
        passwd: 'passw0rd'
      }
    }, function(error, response, body) {
      authtoken = extractAuthToken(response);
      assert(authtoken);
      done();
    });
  });

...

  describe('GET /{id}', function() {
    it('returns status code 401', function(done) {
      request.get(`${BASE_PATH}/${testid}`, function(error, response, body) {
        assert.equal(401, response.statusCode);
        done();
      });
    });

    it('returns status code 200', function(done) {
      request.get({
        url: `${BASE_PATH}/${testid}`,
        headers: buildAuthHeaders(authtoken)
      }, function(error, response, body) {
        assert.equal(200, response.statusCode);
        done();
      });
    });
  });

...

API์— ๋Œ€ํ•œ ๊ธฐ๋ณธ์ ์ธ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์™„์„ฑํ•˜๋ฉด ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ API ์ƒ์„ธ๋ฅผ ๊ตฌํ˜„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

function newVisit(req, res) {

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

๋ฐฉ๋ฌธ์ž ์ •๋ณด ์„œ๋น„์Šค๋Š” ์˜๊ตฌ ๋ฐ์ดํ„ฐ ์ €์žฅ์„ ์œ„ํ•ด Cloudant ์„œ๋น„์Šค๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์„ค๊ณ„ ํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค. UI ์„œ๋น„์Šค์—์„œ ์—ฐ๊ฒฐํ–ˆ๋˜ Cloudant ์„œ๋น„์Šค๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ Bluemix ๋Œ€์‹œ๋ณด๋“œ์—์„œ ํ•ด๋‹น ์„œ๋น„์Šค๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ์กด ํ•ญ๋ชฉ ์—ฐ๊ฒฐ์„ ์„ ํƒํ•˜์—ฌ ์—ฐ๊ฒฐ ํ•  ์„œ๋น„์Šค๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

์„œ๋น„์Šค ์—ฐ๊ฒฐ์ด ์™„๋ฃŒ๋˜๋ฉด local ์‹คํ–‰์„ ์œ„ํ•ด ์„œ๋น„์Šค ์‹ ์ž„ ์ •๋ณด๋ฅผ vcal-loca.json ํŒŒ์ผ๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

๋ฐฉ๋ฌธ์ž ์ •๋ณด ์ €์žฅ์„ ์œ„ํ•œ ์„œ๋น„์Šค๊ฐ€ Cloudant ์ฝ”๋“œ์ด๋ฏ€๋กœ ์ด๋ฅผ ์œ„ํ•œ ๋ชจ๋“ˆ์„ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

$ npm install cloudant --save

๊ทธ๋ฆฌ๊ณ , cloudant๋ฅผ ์ด์šฉํ•œ ์ •๋ณด ์ €์žฅ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด SMVCloudantHelper.js๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ชจ๋“ˆ์€ SMV UI ์•ฑ์˜ app.js์— ํฌํ•จ๋œ cloudant ์ •๋ณด ์ดˆ๊ธฐํ™” ์ฝ”๋“œ๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.

'use strict';

const CLOUDANT_SERVICE_NAME = 'smv-ui-app-cloudantNoSQLDB';
const CLOUDANT_SMV_DATABASE = 'smv';

const appEnv = require('cfenv').getAppEnv();
var dbCredentials = appEnv.getServiceCreds(CLOUDANT_SERVICE_NAME);

// Initialize 
const cloudant = require('cloudant')(dbCredentials.url);

function createDatabase(dbName) {
  // check if DB exists if not create
  cloudant.db.create(dbName, function(err, res) {
    if (err) {
      console.log('Could not create new db: ' + dbName + ', it might already exist.');
    }
  });
}

// ----------------------------------------------------------------------------

module.exports = {
  'cloudant': cloudant,
  'initDB': function (dbName) {
    var name = dbName || CLOUDANT_SMV_DATABASE;

    // check if DB exists if not create
    createDatabase(name);

    return cloudant.use(name);
  }
};

app.js๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ์ดˆ๊ธฐํ™” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ ํ–ˆ์ง€๋งŒ ํ–ฅํ›„ ํ•„์š”์— ๋”ฐ๋ผ์„œ ๋‹ค์–‘ํ•œ ์˜ต์…˜์„ ์„ ํƒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. cloudant ๋ชจ๋“ˆ์— ๋Œ€ํ•œ ์ž์„ธํ•œ ์ •๋ณด๋Š” https://www.npmjs.com/package/cloudant๋ฅผ ์ฐธ๊ณ ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ๋งŒ๋“  SMVCloudantHelper.js์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ƒ์„ฑํ•˜๊ณ  ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์ผ ์ด๋ฆ„์€ test-cloudanthelper.js ์ž…๋‹ˆ๋‹ค.

#!/usr/local/bin/node

/* eslint-env mocha */

const TEST_TIMEOUT = 60*1000; // 60 seconds

var assert = require('assert'),
  SMVCloudantHelper = require('../controllers/SMVCloudantHelper');

describe('[SMVCloudantHelper Unit Test]', function() {
  before(function () {
    //console.log('>>>>>>>>>>>>>>> before');
  });

  after(function () {
    //console.log('after <<<<<<<<<<<<<<<<');
  });

  this.timeout(TEST_TIMEOUT);

  describe('smv database test', function() {
    var mydb = SMVCloudantHelper.initDB('smv'),
      docid = 'test',
      doctype = 'test',
      jsondoc,
      lastrev;

    const json = {
      _id: docid,
      type: doctype,
      text: 'hello'
    };

    it('CREATE', function (done) {
      assert(mydb);
      mydb.insert(json, function(err, doc) {
        if (err) {
          done(err);
        } else {
          assert(doc.id == docid);
          done();
        }
      });
    });

    it('READ', function (done) {
      assert(mydb);
      mydb.get(docid, function(err, doc) {
        if (err) {
          done(err);
        } else {
          jsondoc = Object.assign({}, doc);
          lastrev = doc._rev;
          assert(doc._id == docid);
          assert(doc.type == doctype);
          done();
        }
      });
    });

    it('UPDATE', function (done) {
      assert(mydb);
      assert(jsondoc);
      jsondoc.text = 'world';
      mydb.insert(jsondoc, function(err, doc) {
        if (err) {
          done(err);
        } else {
          lastrev = doc.rev;
          assert(lastrev);
          assert(doc.id == docid);
          done();
        }
      });
    });

    it('DELETE', function (done) {
      assert(mydb);
      assert(docid);
      assert(lastrev);
      mydb.destroy(docid, lastrev, function(err, doc) {
        if (err) {
          done(err);
        } else {
          assert(doc.id == docid);
          done();
        }
      });
    });
  });
});

๋‹ค์‹œ newVisit ํ•จ์ˆ˜๋กœ ๋˜๋Œ์•„ ์™€์„œ newVisit ํ•จ์ˆ˜๊ฐ€ ์ „๋‹ฌ ๋ฐ›์€ ์ •๋ณด๋ฅผ ํ™•์ธํ•˜๋ฉด request์—์„œ ์ •๋ณด๋ฅผ ์–ป๋Š” ๊ณผ์ •์„ ๊ฑฐ์ณ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ newVisit๊ณผ ๊ฐ™์€ POST ๋ฉ”์†Œ๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ body์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด์•ผ ํ•˜๋Š”๋ฐ, body-parser ๋ชจ๋“ˆ์€ ์ด์™€ ๊ฐ™์ธ ๊ธฐ๋Šฅ์„ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด ์ฃผ๋ฏ€๋กœ ์ด๋ฅผ ํ™œ์šฉํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ช…๋ น์œผ๋กœ body-parser ๋ชจ๋“ˆ์„ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

$ npm install body-parser --save

๊ทธ๋ฆฌ๊ณ , app.js์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด body-parser ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

...
var app = express();

var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({
    extended: true
}));
app.use(bodyParser.json());
...

newVisit ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด req.body object๋ฅผ ํ†ตํ•ด ์ „๋‹ฌ ๋ฐ›์€ ์ •๋ณด๋ฅผ ํ™•์ธ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

function newVisit(req, res) {
  console.log(req.body);

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

newVisit์ด ๋„˜๊ฒจ ๋ฐ›์€ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

{ id: 10103023023010,
  date: '2017-04-28 12:10:00 GMT+09:00',
  updated: 1493349000000,
  visitor: 
   { name: 'Jane Doe',
     title: 'Dr.',
     contact: '+82-1-1234-0678',
     email: 'jane.doe@customer.com',
     nationality: 'South Korea',
     company: 'Customers Inc' },
  escort: 
   { id: 'CN=John Doe/OU=ACME/O=IBM',
     name: 'John Doe',
     email: 'john.doe@acme.ibm.com',
     dept: 'Client Innovation Lab' },
  agreement: 
   { templateid: '238a8a7d77f7bf',
     agreement: 'User agreement blabla',
     date: 1493349000000,
     signature: 'data:image/jpeg;base64,R0lGODlhDwAOAOYAAAAAAP' },
  badge: { id: 12392391239, type: 'VISITOR', number: 1, returnYN: 'YES' } }

Cloudant DB ๊ตฌ์„ฑ

์ด์ œ Cloudant์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ์ด๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ๊ตฌ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ๋จผ์ € Cloudant์— ๋Œ€ํ•œ ๋ช‡ ๊ฐ€์ง€ ํŠน์ง•์„ ์•Œ๊ณ  ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  • ์ €์žฅ๋˜๋Š” ๋ฐ์ดํ„ฐ๋Š” JSON ํ˜•์‹์ด๋‹ค
  • ์ €์žฅ๋˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ๋ฌธ์„œ๋กœ ์ทจ๊ธ‰ํ•œ๋‹ค
  • ๋ฌธ์„œ๋Š” ์ตœ์ดˆ ์ƒ์„ฑ ์‹œ ์ค‘๋ณต๋˜์ง€ ์•Š๋Š” ID๋กœ ๊ตฌ๋ถ„ํ•œ๋‹ค
  • ์ •๋ณด๊ฐ€ ๊ฐฑ์‹ ๋˜๋ฉด ๋ฌธ์„œ ID๋Š” ๋ณ€ํ•˜์ง€ ์•Š์œผ๋‚˜, ๋ฌธ์„œ revisioin์ด ๋ณ€๊ฒฝ๋œ๋‹ค.
  • ๊ฒ€์ƒ‰์„ ์œ„ํ•œ KEY๋ฅผ ์ง€์ •ํ•˜์—ฌ View๋ฅผ ํ†ตํ•ด ์ •๋ณด๋ฅผ ์ ‘๊ทผํ•œ๋‹ค
  • ๊ด€๊ณ„ํ˜• DB์™€ ๋‹ฌ๋ฆฌ ์ฐธ์กฐ๊ฐ€ ์—†๋‹ค

์‚ฌ์‹ค ๋งˆ์ง€๋ง‰ ์ฐธ์กฐ๊ฐ€ ์—†๋‹ค๋Š”์— ๋Œ€ํ•œ ๋ถ€๋ถ„์ด ๊ธฐ์กด ๊ด€๊ณ„ํ˜•DB์™€ ๊ฐ€์žฅ ๋‹ค๋ฅธ์ ์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด RDB๋Š” ํŠน์ • ID๋ฅผ ๊ฐ–๋Š” ์ •๋ณด๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ๋Š” ํ…Œ์ด๋ธ”๊ณผ, ์ด๋ฅผ Foreign Key๋กœ ๊ฐ€์ง„ ํ…Œ์ด๋ธ”๊ณผ joinํ•ด์„œ ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ ์˜ฌ ์ˆ˜ ์žˆ์œผ๋ฉฐ ๊ทธ๋ ‡๊ธฐ์— ํ•˜๋‚˜์˜ ํ…Œ์ด๋ธ”์˜ ์ •๋ณด๋ฅผ ๋ณ€๊ฒฝํ•ด๋„ ํ•ด๋‹น ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์‹œ์ ์— ๋ณ€๊ฒฝ๋œ ๋‚ด์šฉ์ด ๋ฐ˜์˜๋œ ๊ฒฐ๊ณผ๋ฅผ ์–ป๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. Cloudant์˜ ๊ฒฝ์šฐ ์ €์žฅ๋˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ๋ฌธ์„œ ๋‹จ์œ„์ž…๋‹ˆ๋‹ค. ํ•˜๋‚˜์˜ ๊ณ ์œ ํ•œ ID๋ฅผ ๊ฐ–๋Š” ๋ฌธ์„œ์— ๋ฐ์ดํ„ฐ๊ฐ€ ํฌํ•จ๋˜๊ณ  ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, ํ…Œ์ด๋ธ”์—์„œ์ฒ˜๋Ÿผ column์ด๋‚˜ ๋ฐ์ดํ„ฐ ํƒ€์ž…์— ๋Œ€ํ•œ ์ œํ•œ์ด ์—†์œผ๋ฏ€๋กœ ์ƒ๋‹นํžˆ ์œ ์—ฐํ•œ ํ˜•ํƒœ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ, Cloudant ์‚ฌ์šฉ์‹œ์—๋Š” ๋ฌธ์„œ์˜ ์ข…๋ฅ˜๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ณ  ๊ทธ ๋ฌธ์„œ๊ฐ€ ๊ฐ€์ง€๋Š” ๋ฐ์ดํ„ฐ ์˜์—ญ์ด ์–ผ๋งˆ๋‚˜ ๋˜๋Š”์ง€๋ฅผ ๋จผ์ € ๊ฒฐ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ฐฉ๋ฌธ์ž API๋ฅผ ์˜ˆ๋ฅผ ๋“ ๋‹ค๋ฉด visitor์™€ escort ์— ๋Œ€ํ•œ ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. ๋ณธ ๊ธ€์—์„œ๋Š” ํ•˜๋‚˜๋กœ ํ•ฉ์น˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ ํƒํ•˜๋ฉฐ ํ•„์š”์— ๋”ฐ๋ผ view๋ฅผ ์ •์˜ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์ง„ํ–‰ ํ•ฉ๋‹ˆ๋‹ค.

๋ฌผ๋ก  ํ•˜๋‚˜์˜ ๋ฌธ์„œ์— ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋˜๋Š” ๊ฒฝ์šฐ ๋ฐ˜๋ณต๋˜๋Š” ๋‚ด์šฉ์ด ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ค‘๋ณต๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ƒ๋‹นํžˆ ๋งŽ๋‹ค๊ณ  ํŒ๋‹จ๋˜๊ณ , ์ •๋ณด ์กฐํšŒ๋ฅผ ์œ„ํ•ด ์ถ”๊ฐ€ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์ด ๋” ๋‚ซ๋‹ค๊ณ  ํŒ๋‹จ๋˜๋ฉด ๋ถ„๋ฆฌ๋œ ํ˜•ํƒœ๋กœ ๊ตฌ์„ฑํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค๋งŒ, ๋ณธ ๊ธ€์—์„œ๋Š” ๊ทธ๋ ‡์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ํ•˜๋‚˜๋กœ ํ•ฉ์ณ์„œ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

๋ฐฉ๋ฌธ ์ •๋ณด์˜ id๋Š” Cloudant์—์„œ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜๋„๋กํ•˜๋ฉฐ ๋‚˜๋จธ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‘ ์ €์žฅํ•˜๋Š” ํ˜•์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, ์ž„์‹œ ๋ฐฉ๋ฌธ์ฆ ์ •๋ณด์™€ ๊ฐ™์€ ๋‚ด์šฉ๊ณผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด type์ด๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ๋ฌธ์„œ ์ข…๋ฅ˜๋ฅผ ๊ตฌ๋ถ„ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

์ด์ œ Bluemix ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ํ†ตํ•ด Cloudant ๊ด€๋ฆฌ ํ™”๋ฉด์œผ๋กœ ๋“ค์–ด๊ฐ€๋ณผ ์ฐจ๋ก€์ž…๋‹ˆ๋‹ค.

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋Š” ์„œ๋น„์Šค ์ค‘ Cloudant ์„œ๋น„์Šค๋ฅผ ์„ ํƒํ•˜๋ฉด Cloudant ์„œ๋น„์Šค๋กœ ์—ฐ๊ฒฐ์— ํ•„์š”ํ•œ ๋งํฌ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ™”๋ฉด ์˜ค๋ฅธ์ชฝ์˜ LAUNCH ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ๋ณ„๋„๋กœ ๊ตฌ์„ฑ๋œ Cloudant Web Dashboard ํ™”๋ฉด์œผ๋กœ ์ž๋™์œผ๋กœ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค.

๋Œ€์‹œ๋ณด๋“œ์—์„œ ํ˜„์žฌ ์ƒ์„ฑ๋œ database๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋Š”๋ฐ smv-ui ์•ฑ์—์„œ ์‚ฌ์šฉํ•˜๋Š” my_sample_db์™€ smv-visit์—์„œ ์‚ฌ์šฉํ•˜๋Š” smv๊ฐ€ ์ƒ์„ฑ๋œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ smv๊ฐ€ ๋ณด์ด์ง€ ์•Š๋Š” ๊ฒฝ์šฐ SMVCloudantHelper.js ๋ชจ๋“ˆ์ผ ์‹คํ–‰๋˜์–ด database๋ฅผ ์ƒ์„ฑํ•œ ์ ์ด ์—†๋Š” ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๊ฒฝ์šฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•˜๊ฑฐ๋‚˜ ์•„๋‹ˆ๋ฉด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ฑฐ๋‚˜, Cloudant ๋Œ€์‹œ๋ณด๋“œ์˜ ์˜ค๋ฅธ์ชฝ ์ƒ๋‹จ์˜ Create Database๋ฅผ ํด๋ฆญํ•˜์—ฌ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ƒ์„ฑ ํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ์ƒˆ๋กญ๊ฒŒ ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค๋ฅผ ์ž‘์„ฑ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

์•ž์„œ ๊ตฌ์„ฑํ•ด ๋†“์€ API Handler์— ๋Œ€ํ•ด Cloudant๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ๋ณธ ๊ธ€์—์„œ๋Š” Cloudant ์‚ฌ์šฉ์— ๋Œ€ํ•œ ๊ฐ„๋žตํ•œ ์„ค๋ช…๋งŒ ํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ, Cloudant Query์— ๋Œ€ํ•œ ์ƒ์„ธ ๋ถ€๋ถ„์€ https://docs.cloudant.com/cloudant_query.html๋‚˜ Bluemix์˜ https://console.ng.bluemix.net/docs/services/Cloudant/api/cloudant_query.html์—์„œ ํ™•์ธ ํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Document ์ƒ์„ฑ

Cloudant์—์„œ๋Š” document๋ฅผ ์ƒ์„ฑ ํ•  ๋•Œ id๋ฅผ ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. smv-visit API๋ฅผ ํ†ตํ•ด ์ „๋‹ฌ๋˜๋Š” id๋ฅผ cloudant์˜ id ์ •๋ณด๋กœ ํ™œ์šฉ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, Cloudant์™€ API๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌ๋˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ๋™์ผํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ด์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

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

function newVisit(req, res) {
  var json = req.body;
  if (json.hasOwnProperty('id')) {
    json['_id'] = String(json.id);
    json['id'] = undefined;
  }
  json['type'] = 'VISIT';
  json['date'] = new Date(json.date).getTime();
  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(cleanseVisitObject(Object.assign(json, doc)));
      res.end();
    }
  });
}

cleanseVisitObject ํ•จ์ˆ˜๋Š” ์ €์žฅ๋œ API์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ smv-visit API์— ๋งž์ถฐ ๋ณ€๊ฒฝํ•˜๋Š” ์ฝ”๋“œ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

function cleanseVisitObject(visit) {
  // 
  var id = visit._id || visit.id;
  var rev = visit._rev || visit.rev;
  var type = visit.type;
  
  var json = Object.assign(visit, {
    id: id
  });

  json['_id'] = undefined;
  json['_rev'] = undefined;
  json['type'] = undefined;
  json['date'] = new Date(visit.date).toString();

  return json;
}

Document ์ฝ๊ธฐ

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

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

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

Document ๊ฐฑ์‹ 

smv-visit API์—์„œ๋Š” ์ˆ˜์ •์„ ์›ํ•˜๋Š” ๋ถ€๋ถ„๋งŒ ์ž…๋ ฅํ•˜๊ณ  ํ•ด๋‹น ๋ถ€๋ถ„์— ๋Œ€ํ•œ ๋‚ด์šฉ์„ ์—…๋ฐ์ดํŠธ ํ•˜๋Š” ํ˜•ํƒœ๋กœ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค๋งŒ, Cloudant์˜ ๊ฒฝ์šฐ ์ผ๋ถ€๋ถ„์— ๋Œ€ํ•œ ๋ณ€๊ฒฝ์— ๋Œ€ํ•œ ์ผ์ง€๋ผ๋„ ์ „์ฒด document์˜ ๋‚ด์šฉ์ด ๋ฐ˜์˜๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, id์— ๋Œ€์‘ํ•˜๋Š” document๋ฅผ ๋จผ์ € ์ฝ์€ ํ›„ ๋ณ€๊ฒฝ์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์„ ํ™•์ธํ•˜๊ณ  ์ด๋ฅผ ์ ์šฉํ•œ ์ตœ์ข… document๋ฅผ ๋งŒ๋“ค์–ด ์—…๋ฐ์ดํŠธ ํ•˜๋Š” ๊ณผ์ •์„ ๊ฑฐ์น˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

function updateVisit(req, res) {
  var docid = req.params.id;
  
  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];
        if (req.body.hasOwnProperty(key)) {
          jsondoc[key] = Object.assign(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(cleanseVisitObject(jsondoc));
          res.end();
        }
      });
    }
  });
}

Document ์‚ญ์ œ

cloudant document๋ฅผ ์‚ญ์ œํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” id ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ตœ์‹  ์ €์žฅ๋œ revision id๊ฐ€ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, update์—์„œ์™€ ๊ฐ™์ด id์— ๋Œ€ํ•œ document๋ฅผ ์ฝ์€ ํ›„ ํ•ด๋‹น document์— ํฌํ•จ๋œ rev ์ •๋ณด๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ document๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.

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

  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 ์กฐํšŒ

์‚ฌ์‹ค ์กฐํšŒ์— ๋Œ€ํ•œ ๋ถ€๋ถ„์ด ์ œ์ผ ๊นŒ๋‹ค๋กญ์Šต๋‹ˆ๋‹ค. RDB์˜ SQL๊ณผ ๊ฐ™์€ ํ˜•ํƒœ์˜ Query๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์•ฝ๊ฐ„ ํ˜ผ๋ž€์Šค๋Ÿฌ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค๋งŒ, ๊ธฐ๋ณธ์ ์ธ ๋™์ž‘์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด find ๋ช…๋ น์œผ๋กœ ์กฐ๊ฑด์— ๋งž๋Š” document๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค.

function searchVisits(req, res) {
  var date = req.query.date;
  var type = req.query.type;
  var keyword = req.query.keyword;
  var page = Number(req.query.page);
  var size = Number(req.query.size);
  
...

  var selector = {
    '$and':[
      {'type': 'VISIT'}
    ]
  };

...

  mydb.find({
    selector: selector,
    limit: limit,
    skip: skip
  }, function(err, result) {
    if (err) {
      console.error(err);
      // Error 
      res.statusCode = 500;
      res.end('Internel Server Error');
    } else {

...

    }
  }

selector์— ํ•„์š”ํ•œ ์กฐ๊ฑด์„ ์ž…๋ ฅํ•˜๋ฉด ๊ทธ์— ๋”ฐ๋ฅธ document ๋ฆฌ์ŠคํŠธ๋ฅผ ์–ป๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. smv-visitor API์—์„œ๋Š” page ๊ธฐ๋Šฅ์„ ์œ„ํ•ด result์™€ total ์ด๋ž€ property๋ฅผ ์ง€์ •ํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ์ œ๊ณตํ•˜๋„๋ก ํ–ˆ๋Š”๋ฐ, cloudant API์—์„œ ์ œ๊ณตํ•˜๋Š” limit, skip ์˜ต์…˜์„ ์ด์šฉํ•˜์—ฌ pagingation ์ •๋ณด๋ฅผ ๊ตฌ์„ฑ ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ฌผ๋ก  ํ˜„์žฌ ๊ฒ€์ƒ‰ ์กฐ๊ฑด์œผ๋กœ ์–ป์„ ์ˆ˜ ์žˆ๋Š” ์ „์ฒด ๋ฌธ์„œ ๊ฐฏ์ˆ˜์— ๋Œ€ํ•œ ์ •๋ณด๋Š” ์ œ๊ณตํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๊ฐฏ์ˆ˜๋ฅผ ์กฐํšŒํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ์–ป๋Š” ๋ถ€๋ถ„๋„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. search์˜ ๊ฒฝ์šฐ cloudant API๊ฐ€ ๋‘ ๋ฒˆ ํ˜ธ์ถœ๋˜๊ฒŒ ๋˜๋Š” ์…ˆ์ž…๋‹ˆ๋‹ค. ๋Œ€์‹  ์ „์ฒด ์ •๋ณด๋ฅผ ์–ป์„ ํ•„์š” ์—†์ด ์ตœ์†Œํ•œ์˜ ์ •๋ณด๋งŒ ์–ป๊ธฐ ์œ„ํ•ด fields๋ผ๋Š” ์˜ต์…˜์œผ๋กœ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

...
      mydb.find({
        selector: selector,
        fields: ['type'],
        use_index: queryIndex
      }, function(err, totalInfo) {
        console.log(`total count : ${totalInfo.docs.length}`);
        resObject.total['total'] = totalInfo.docs.length;

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

๊ทธ๋ฆฌ๊ณ , ํ•œ๊ฐ€์ง€ ์ฃผ์˜ ํ•  ๋ถ€๋ถ„์€ API ํ˜ธ์ถœ ์‹œ ํ•ด๋‹น ์กฐ๊ฑด์— ๋Œ€ํ•œ ์ „์ฒด document ๊ฐฏ์ˆ˜๋ฅผ page์— ๋Œ€ํ•œ total size๋ฅผ ์•Œ ์ˆ˜ ์—†๋Š” ์ƒํƒœ์—์„œ ์กฐํšŒํ•˜๋Š” page ์ •๋ณด๊ฐ€ ์ตœ๋Œ€ page ๋ณด๋‹ค ํฌ๊ฒŒ ์ž…๋ ฅ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ถ€๋ถ„์— ๋Œ€ํ•œ ๋ฐฉ์–ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์—ฌ ํŽ˜์ด์ง€ ์ •๋ณด๋ฅผ ์•Œ ์ˆ˜ ์—†๋Š” ์ƒํƒœ๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๋„๋ก ์ฃผ์˜ ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, API ๊ตฌ์„ฑ ์‹œ total size๋ฅผ ๋จผ์ € ์–ป์€ ํ›„ page size๋ฅผ ๊ณ„์‚ฐํ•ด์„œ ์ตœ๋Œ€ page index ์ •๋ณด๋ฅผ ์–ป๋Š” ์ฝ”๋“œ๊ฐ€ ์ฒ˜๋ฆฌ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

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

์ง€๊ธˆ๊นŒ์ง€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋น„์Šค ์ค‘ ๋ฐฉ๋ฌธ์ž ์ •๋ณด ์„œ๋น„์Šค์— ๋Œ€ํ•œ ๋ถ€๋ถ„์„ ํ”„๋กœํ†  ํƒ€์ž…์œผ๋กœ ๋งŒ๋“ค์–ด ๋ณด์•˜์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๊ธ€์—์„œ๋Š” ๋‚จ์€ API ์„œ๋น„์Šค๋“ค์— ๋Œ€ํ•œ ํ”„๋กœํ† ํƒ€์ž…์„ ๊ตฌ์„ฑํ•˜๋Š” ๋‚ด์šฉ์œผ๋กœ ์ง„ํ–‰ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.


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

์ฐธ๊ณ 

ํ† ๋ก  ์ฐธ๊ฐ€

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