블로그 이미지
윤영식
Full Stacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

Category

Recent Post

2013. 11. 9. 07:08 My Services/PlayHub

Angular 기반 SPA를 위한 Push 환경은 Node 기반의 Socket.io를 사용한다. Angular.js의 장점중 하나가 DI(Dependency Injection)이고 이를 Node에서 구현한 프레임워크인 express-train (축약, train)을 사용한다. train은 특정 폴더에 .js 파일을 놓으면 자동으로 DI되어 사용을 할 수 있게 해준다. 일정한 규약만을 지키면 개발 코드를 모듈화 하여 관리할 수 있고 테스트 할 수 있다. train은 express에 DI 기능을 접목해 놓은 것으로 보면 된다





1. Express-Train 설치하기 

  - 설치 : npm install -g express-train 

  - 새로운 Node 환경 프로젝트 생성

// train이 사용하는 기본 템플릿을 기반으로 자동으로 palette.fronend.node 디렉토리에 기본 폴더와 파일을 생성해 준다 

$ train new palette.frontend.node 

// 수행 http://localhost:4000 호출 

$ train run 

  - 디렉토리 구조 

    + app : auto-injected 로 표현된 것 .js 파일을 해당 디렉토리에 놓으면 다른 곳에서 파일 명칭으로 불러와서 (function parameter DI 방식) 사용을 할 수 있다 

    + bin : 이것은 node_modules/express-train 밑에 존재한다 

    + test : Mocha를 이용한다

    + public : Angular.js 기반으로 작성한 모든 static 파일들이 이곳에 놓일 것이다. 엄밀히 따지면 Distributed된 Frontend파일이 놓인다

app

  /controllers      -- application controllers (**autoinjected**)

  /lib                 -- application specific modules (**autoinjected**)

  /middleware    -- application middleware (**autoinjected**)

  /models          -- application models (**autoinjected**)

  /public            -- static content (html, js, css, etc) : SPA모든 파일을 해당 폴더 밑에 복사할 것이다  

  /views            -- view templates (loaded into express' view engine) : 서버단의 파일 jade, ejs, hbs 같은 서버 template 파일

  index.js           -- index file exports the express train application : train run 시에 수행되는 파일 based on Node.js


bin                   -- executable scripts : 

doc                  -- documentation

config              -- environmental configuration files : train 환경 설정 파일 

test                 -- tests : 서버 테스트 파일

package.json    -- npm package.json (needs to have express-train as a dependency)

  - train의 환경파일 : development와 production 환경파일을 구분함 

{

  "cookie_secret": "boggle at the situation",

  "session":{

    "key": "boggle at the situation",

    "secret": "boggle at the situation"

  },

  "client_port": 3000,

  "request_timeout": 1000,

  // RESTful 호출 접두사 Restanuglar 참조 

  "restful_api_version": "/api/v1",

  "server_name": "first",

  // REDIS 연결 Client Library 환경 설정

  "data_broker": {

    "port" : 6379,

    "host" : "localhost",

    "auth" : ""

  },

  // MongoDB 환경 설정

  "mongodb":{

    "uri": "mongodb://localhost/playhub_palette"

  },

  "playhub_channel_file": "channels.json",

  // Winston 에러 로그가 쌓일 MongoDB 정보 

  "winston_mongo": {

    "level": "error",

    "db": "playhub_palette",

    "host": "localhost",

    "port": 27017,

    "collection": "sd_logs"

  },

  // Winston 로그 파일의 명칭 설정

  "winston_file": {

    "filename": "sd.log",

    "exception": "sd_exception.log",

    "maxSize": 20480,

    "maxFiles": 12,

    "colorize": true

  },

  "character_encoding":"utf-8"

}



2. Express-Train 파일 사용하기 

  - auto-injected 되는 폴더에 모듈로 운용하고 싶은 .js 파일을 놓는다. 주로 MVC 파일과 Utility 또는 Service 파일들이다 

  - auto-inject 환경내역은 node_modules/express-train/lib/app.js 파일안에 이미 설정되어 있다.

// app.js 내역중 일부 

// autoinject라는 옵션을 주면 해당 지정된 폴더의 .js파일을 메모리에 로딩하고 DI를 한다 

// 이때 주의할 것은 상호 참조가 되지 않도록 하기 위한 폴더 전략이 중요하다. 위에서 부터 아래순서로 로딩되어 DI 된다 

var LOCATIONS = createApplication.locations = {

    pkg:{

        path: '../package.json'

    },

    config:{

        path: '../config'

    },

    logs:{

        path: '../logs'

    },

    models: {

        path: 'models',

        autoinject: true,

        aggregateOn: 'models'

    },

    views: {

        path: 'views'

    },

    lib: {

        path: 'lib',

        autoinject: true

    },

    controllers: {

        path: 'controllers',

        autoinject: true

    },

    pub: {

        path: 'public'

    },

    middleware: {

        path: 'middleware',

        autoinject: true

    }, 

   // 만약 app밑에 services폴더를 만든다면! 

    services: {

       path: 'services',

       autoinject: true

    },

}; 


// app.js 에서 express를 생성하고 views와 public 폴더를 강제 설정하고 있다

// _.each에서 autoinject 폴더의 모든 파일을 읽고 DI를 수행해 준다. 이때 DI되는 .js 파일은 Singleton 패턴이다 

// tree.constant로 저장된 객체를 DI 받을 수 있다

function createApplication(dir, locs) {

    catchErrors();


    var app = express(),

        locations = _.defaults((locs || {}), LOCATIONS);

        tree = new nject.Tree();


    app.set('views', path.join(dir, locations.views.path));

    app.set('public', path.join(dir, locations.pub.path));


    var config = loadConfig(path.join(dir, locations.config.path));

    tree.constant('config', config);


    _.each(_.where(locations, {autoinject: true}), function(location) {

        traverseAndRegister(path.join(dir, location.path), tree, location.aggregateOn)

    });


    //allow override of app

    if(!tree.isRegistered('app')) {

        tree.constant('app', app);  

    }


    //log path 하나 추가함 

    var logPath = path.join(dir, locations.logs.path);

    tree.constant('logPath', logPath);


    return tree.resolve();

}


  - 폴더 전략 : app.js에 설정된 순서에-위에서 아래로- 따라 로딩됨, 참조가 많은 것은 주로 service가 되지 않을까 함

    + lib/1level : 가장 기본적인 파일 참조하는 것이 없는 것들 - util, logger, db connection, constant

    + lib/2level : express-train의 파일들  - route, views, middleware 설정

    + lib/3level : 1, 2level의 .js를 사용하는 파일들 

////////

// 형식

module.exports = function(app, config, logger, ...) {

  // app, config, logger 등 파일이름을 대소문자 구분하여 넣으면 자동 DI를 해준다. 객체는 train에서 Singleton으로 관리한다 

}


/////////////////////////

// 예) lib/1level/logger.js

// MongoDB에 winston logger를 이용하여 에러발생 내역을 저장하거나 파일로 떨굼 


var winston = require('winston'),

  path = require('path');


module.exports = function(app, config, logPath) {


  winston.info('[1level: log config] init');

  require('winston-mongodb').MongoDB;


  var dbOpts = {

    level: config.winston_mongo.level,

    db: config.winston_mongo.db,

    collection: config.winston_mongo.collection,

    host: config.winston_mongo.host,

    port: config.winston_mongo.port

  };

  winston.info('[1level: log config] DB option is ' + JSON.stringify(dbOpts) );


  var fileName = path.join(logPath, config.winston_file.filename);

  winston.info('[1level: log config] general log : ' + fileName);

  var exceptionFileName = path.join(logPath, config.winston_file.exception);

  winston.info('[1level: log config] exception log : ' + exceptionFileName);


  var fileOpts = {

    filename: fileName,

    maxsize: config.winston_file.maxSize,

    maxFiles: config.winston_file.maxFiles,

    colorize: config.winston_file.colorize

  }


  var exceptionOpts = {

    filename: exceptionFileName,

    maxsize: config.winston_file.maxSize,

    maxFiles: config.winston_file.maxFiles,

    colorize: config.winston_file.colorize

  }


  var logger = new winston.Logger({

    transports: [

      new winston.transports.Console(),

      new winston.transports.File(fileOpts),

      new winston.transports.MongoDB(dbOpts)

    ],

    exceptionHandlers: [

      new winston.transports.Console(),

      new winston.transports.File(exceptionOpts),

      new winston.transports.MongoDB(dbOpts)

    ]

  });


//  testing

// logger.log(config.winston_mongo.level, '[log config] Test logging module completely');

//  logger.info('==============>> hi info');

//  logger.warn('==============>> hi warn');

//  logger.error('==============>> hi error');  <- MongoDB에도 쌓인다 


  return logger;

}


////////////////////////////////////////////////

// 예) lib/2level/ 또는 lib/2level에서 logger사용하기 

// lib/2level/views.js 파일에서 

// logger.js 파일에서 파일명칭을 - 대소문자 구분 - Function Parameter DI 해준다 

module.exports = function (applogger) {

  logger.info('[2level: view engine - jade] init');

  app.set('view engine', 'jade');

};




방법-1. Angular 프로젝트 배포후 Express-Train 프로젝트로 통합 

  - 엄밀히 따지면 합치는 시점은 개발이 다 된후 수행한다. 즉, SPA와 Node 파트의 Git 저장소를 틀리게 가져감을 전제로 한다 

    + SPA 저장소 : palette.frontend

    + Node저장소 : palette.frontend.node 

  - 개발이 된후 SPA와 Node 통합은 다음 순서로 하자 

    + Yeoman을 통하여 생성된 Angular 스케폴딩 파일은 별도로 개발한다 -> grunt build 수행 -> dist 폴더에 배포파일이 생성된다 

    + palette.frontend.node/app/public/ 와 palette.frontend.node/app/views 폴더 밑의 모든 파일을 삭제한다

    + palette.frontend.node/app/public/ 밑으로 dist/* 모든 파일을 복사한다 

// 폴더구조 

playhub

  |_ palette.frontend         (별도 git repository)

  |_ palette.frontend.node (별도 git repository)


// Angular 기반 frontend 파일 

playhub$ cd palette.frontend

playhub$ grunt build 


// Express-train에서 사용하지 않을 파일 삭제  

playhub$ cd ../palette.frontend.node/app/public

playhub$ rm -rf *

playhub$ cd ../views

playhub$ rm -rf *


// Angular 기반 SPA frontend파일을 Express-train의 app/public/ 폴더 밑으로 복사 

playhub$ cp -rf ../../../palette.frontend/dist/* ../public/

  - 테스트 하기 

$ cd palette.frontend.node

train run 

// http://localhost:4000/ 으로 호출한다 



방법-2. Express-Train 프로젝트를 Angular 프로젝트로 통합

  - Angular 프로젝트(palette.frontend)의 grunt, bower, yo, karma 기능을 그대로 사용한다 

  - Express-Train 프로젝트(palette.frontend.node)는 express-train기반으로 이미 스케폴딩된 서버단 애플리케이션이다. 

    즉 palette.frontend.node의 디렉토리 구조를 변경시켜보자  

// express-train 프로젝트로 이동하기 

$ cd palette.frontend.node


// express-train의 bin 디렉토리를 palette.frontend.node로 이동하기  

$ cd node_modules/

$ cd express-train/

$ ls -arlt

total 80

drwxr-xr-x   3 nulpulum  staff    102  9 17 17:20 test

-rw-r--r--   1 nulpulum  staff     90  9 17 17:20 index.js

-rw-r--r--   1 nulpulum  staff     82  9 17 17:20 .npmignore

-rw-r--r--   1 nulpulum  staff  13393  9 26 19:13 ReadMe.md

drwxr-xr-x   4 nulpulum  staff    136 11  8 14:14 bin

drwxr-xr-x   5 nulpulum  staff    170 11  8 14:14 boilerplates

-rw-r--r--   1 nulpulum  staff  14976 11  8 14:14 package.json

drwxr-xr-x   4 nulpulum  staff    136 11  8 14:14 lib

drwxr-xr-x  12 nulpulum  staff    408 11  8 14:14 node_modules

drwxr-xr-x  11 nulpulum  staff    374 11  8 14:14 .

drwxr-xr-x  13 nulpulum  staff    442 11  8 14:14 ..

// bin을 palette.frontend.node 루트로 이동시킨다 

$ mv bin ../../


// bin으로 들어가서 train run 명령을 수행 - 내부적으로 commander 모듈을 사용한다 

$ train run

module.js:340

    throw err;

          ^

Error: Cannot find module 'commander'

    at Function.Module._resolveFilename (module.js:338:15)

    at Function.Module._load (module.js:280:25)

    at Module.require (module.js:364:17)

    at require (module.js:380:17)

    at Object.<anonymous> (/Users/nulpulum/development/playground/playhub/palette.frontend.node/bin/train:3:15)

    at Module._compile (module.js:456:26)

    at Object.Module._extensions..js (module.js:474:10)

    at Module.load (module.js:356:32)

    at Function.Module._load (module.js:312:12)

    at Function.Module.runMain (module.js:497:10)


// 에러가 발생하므로 commander, underscore, handlebars, boilerplate 모듈을 설치한다 

$ cd .. && npm install commander --save 

$ npm install underscore --save

$ npm install handlebars --save

$ npm install boilerplate --save-dev

$ cd bin && train run 

module.js:340

    throw err;

          ^

Error: Cannot find module '../../lib/app'

    at Function.Module._resolveFilename (module.js:338:15)

    at Function.Module._load (module.js:280:25)

    at Module.require (module.js:364:17)

    at require (module.js:380:17)

    at Object.<anonymous>(/Users/nulpulum/development/playground/playhub/palette.frontend.node/bin/commands/console.js:4:13)

    at Module._compile (module.js:456:26)

    at Object.Module._extensions..js (module.js:474:10)

    at Module.load (module.js:356:32)

    at Function.Module._load (module.js:312:12)

    at Module.require (module.js:364:17)


// 에러 다시 수정 하기 

$ cd ../node_modules/express-train/

$ ls -alrt

total 80

drwxr-xr-x   3 nulpulum  staff    102  9 17 17:20 test

-rw-r--r--   1 nulpulum  staff     90  9 17 17:20 index.js

-rw-r--r--   1 nulpulum  staff     82  9 17 17:20 .npmignore

-rw-r--r--   1 nulpulum  staff  13393  9 26 19:13 ReadMe.md

drwxr-xr-x   5 nulpulum  staff    170 11  8 14:14 boilerplates

-rw-r--r--   1 nulpulum  staff  14976 11  8 14:14 package.json

drwxr-xr-x   4 nulpulum  staff    136 11  8 14:14 lib

drwxr-xr-x  12 nulpulum  staff    408 11  8 14:14 node_modules

drwxr-xr-x  10 nulpulum  staff    340 11  9 15:19 .

drwxr-xr-x  16 nulpulum  staff    544 11  9 15:29 ..


// express-train의 lib밑의 모든파일을 bin 밑으로 이동시킨다 

$ mv lib/* ../../bin/

// index.js 파일을 이동한 bin 디렉토리에 같이 놓는다 

$ mv index.js ../../bin/


// express-train 은 삭제한다 

$ cd .. && rm -rf express-train

$ cd ../../bin/

// 최종 lib이 bin 밑으로 들어갔다 

$ ls -alrt

total 32

-rw-r--r--   1 nulpulum  staff    64  9 17 17:20 runner.js

-rw-r--r--   1 nulpulum  staff  3594  9 27 14:06 app.js

drwxr-xr-x  11 nulpulum  staff   374 11  9 15:19 ..

-rw-r--r--   1 nulpulum  staff    89 11  9 15:56 index.js

-rwxr-xr-x   1 nulpulum  staff   785 11  9 16:02 train

drwxr-xr-x   7 nulpulum  staff   238 11  9 16:02 .

drwxr-xr-x   3 nulpulum  staff   102 11  9 16:08 commands


// commands 폴더에서 runt.js를 제외한 사용하지 않는 나머지 파일은 삭제하거나 나중을 위해서 다른 위치로 백업한다 

$ cd commands && rm console.js cycle.js new.js boilerplate.js


// commands 폴더의 run.js 안에 내역을 수정 한다 

// 기존 내역 : var appPath = path.join(process.cwd(), '../app/index.js');

// 수정 내역 

var appPath = path.join(process.cwd(), './index.js');


// lib밑에 있는 index.js가 가르키는 파일 수정 

// 기존 내역 : app: require('./lib/app')

// 수정 내역

// module.exports = {

//   app: require('./app.js')

// };


var paletteApp = require('./app.js');

module.exports = paletteApp(__dirname);


// palette.frontend.node 루트의 app 디렉토리명칭을 api 라고 변경한다 

$ cd ../../ && mv app api 

  

// 다음으로 직접 app.js안의 인식하는 디렉토리 경로를 상대경로로 변경한다 

$ cd bin && vi app.js

//위치 변경하기 

var LOCATIONS = createApplication.locations = {

    pkg:{

        path: '../package.json'

    },

    config:{

        path: '../config'

    },

    logs:{

        path: '../logs'

    },

    models: {

        path: '../api/models',

        autoinject: true,

        aggregateOn: 'models'

    },

    views: {

        path: '../api/views'

    },

    lib: {

        path: '../api/lib',

        autoinject: true

    },

    controllers: {

        path: '../api/controllers',

        autoinject: true

    },

    // 향후 palette.frontend의 grunt build시에 dist 폴더 밑으로 되는 것을 public 폴더로 바꿀 것이다 

    pub: {

        path: '../public'

    },

    middleware: {

        path: '../api/middleware',

        autoinject: true

    }

};


// api 디렉토리 밑에 있는 public 디렉토리를 루트로 옮겨서 테스트 해본다 

$ mv app/public .


// bin 디렉토리 밑에 있는 train 쉘 실행하기 

$ train run 

[express train application listening on 4000]


  - Express-Train기반의 서버구조를 만들었고, 이제 이것을 palette.frontend 쪽으로 옮겨보자 

// palette.frontend.node 프로젝트의 package.json 명칭을 변경 - 나중에 palette.frontend쪽의 package.json과 합치기 위함

$ mv package.json packate.json.node


// public 폴더는 삭제한다 

$ rm -rf public 


// 옮기기 

$ mv * ../palette.frontend/

mv: rename node_modules to ../palette.frontend/node_modules: Directory not empty

mv: rename test to ../palette.frontend/test: Directory not empty

$ cd node_modules

$ mv * ../../palette.frontend/node_modules/


// palette.frontend 에서 dist를 public으로 명칭 변경하여 테스트 

$ cd ../../palette.frontend

$ mv dist public 

$ cd bin

$ train run

[express train application listening on 4000]


// 이제 grunt build를 수행할 때 dist 디렉토리를 만드는 것이 아니라 public 으로 만들어 보자 

$ cd .. && rm -rf public 


// grunt 환경파일의 내역 수정 

$ vi Gruntfile.js

// 변경내역 

 grunt.initConfig({

    yeoman: {

      // configurable paths

      app: require('./bower.json').appPath || 'app',

      dist: 'public'  // 'dist'를 'public'으로 변경한다 

    },


// 다시 grunt로 빌드를 하면 public 폴더가 생성된다 

$ grunt build 

$ cd bin && train run 

[express train application listening on 4000]


// 최종 palette.frontend의 합친 구조

  - api : Node 서버 

  - app : SPA Angular

  - public : app 배포 디렉토리이면서 bin/train run 수행시 인식되는 서비스 폴더


// palette.frontend git 저장소에 push하기전에 package.json.node의 내용을 package.json에 통합한다 

{

  "name": "palette",

  "version": "0.0.1",

  "dependencies": {

    "express": "3.2.x",

    "express-train": "1.0.x",

    "connect-timeout": "latest",

    "connect-mongodb": "latest",

    "mongoose": "latest",

    "async": "latest",

    "express-hbs": "latest",

    "passport": "latest",

    "winston": "latest",

    "commander": "~2.0.0",

    "underscore": "~1.5.2",

    "handlebars": "~1.1.2",

    "nject": "~0.1.6"

  },

  "devDependencies": {

    "grunt": "~0.4.1",

    "grunt-contrib-copy": "~0.4.1",

    "grunt-contrib-concat": "~0.3.0",

    "grunt-contrib-coffee": "~0.7.0",

    "grunt-contrib-uglify": "~0.2.0",

    "grunt-contrib-compass": "~0.5.0",

    "grunt-contrib-jshint": "~0.6.0",

    "grunt-contrib-cssmin": "~0.6.0",

    "grunt-contrib-connect": "~0.5.0",

    "grunt-contrib-clean": "~0.5.0",

    "grunt-contrib-htmlmin": "~0.1.3",

    "grunt-contrib-imagemin": "~0.2.0",

    "grunt-contrib-watch": "~0.5.2",

    "grunt-autoprefixer": "~0.2.0",

    "grunt-usemin": "~0.1.11",

    "grunt-svgmin": "~0.2.0",

    "grunt-rev": "~0.1.0",

    "grunt-concurrent": "~0.3.0",

    "load-grunt-tasks": "~0.1.0",

    "grunt-google-cdn": "~0.2.0",

    "grunt-ngmin": "~0.0.2",

    "time-grunt": "~0.1.0",

    "karma-ng-scenario": "~0.1.0",

    "grunt-karma": "~0.6.2",

    "karma-script-launcher": "~0.1.0",

    "karma-chrome-launcher": "~0.1.0",

    "karma-firefox-launcher": "~0.1.0",

    "karma-html2js-preprocessor": "~0.1.0",

    "karma-jasmine": "~0.1.3",

    "karma-requirejs": "~0.1.0",

    "karma-coffee-preprocessor": "~0.1.0",

    "karma-phantomjs-launcher": "~0.1.0",

    "karma": "~0.10.4",

    "karma-ng-html2js-preprocessor": "~0.1.0",

    "mocha": "latest"

  },

  "engines": {

    "node": ">=0.8.0"

  },

  "main": "./app/index.js",

  "scripts": {

    "test": "grunt test",

    "start": "cd bin && train run",

    "server-test": "mocha test"

  }

}


// npm 명령으로 scripts 설정 내역 호출하기 

$ npm start

[express train application listening on 4000]


  - 최종 완성본 저장소 : MyPlayHub GitHub Repository

// git clone 하여 train run 수행시 다음과 같이 오류가 발생할 경우 

$ train run

env: node\r: No such file or directory


// dos2unix 패키지를 설치하여 수정하여 준다 

$ brew install dos2unix

==> Installing dos2unix dependency: gettext

==> Downloading http://ftpmirror.gnu.org/gettext/gettext-0.18.3.tar.gz

.. 중략 ..


$ dos2unix train

dos2unix: converting file train to Unix format ...

$ train run 

[express train application listening on 4000]



방법-3. Yeoman Generator FullStack을 통한 통합

  - Express + Angular의 통합

  - Angular는 클라이언트의 파일 전송과 암호화를 위하여 Lint와 압축을 위해 Grunt를 사용함 

  - Express는 서버단이므로 압축이 필요없음 

// FullStack generator 설치 

$ sudo npm install -g generator-angular-fullstack

// FullStackTest라는 애플리케이션 명칭으로 스케폴딩 생성

$ yo angular-fullstack FullStackTest

// 빌드하여 클라이언트 파일 Lint 및 압축 

// public 폴더밑으로 배포본이 만들어짐 

$ grunt build 

... 중략 ...

Elapsed time

useminPrepare:html                   25ms

concurrent:dist                          5s

autoprefixer:dist                        113ms

concat:public/scripts/modules.js  23ms

copy:dist                                  640ms

cdnify:dist                                 21ms

ngmin:dist                                263ms

cssmin:public/styles/main.css     33ms

uglify:dist                                  28ms

uglify:public/scripts/plugins.js      373ms

uglify:public/scripts/modules.js   134ms

usemin:html                             482ms

usemin:css                              152ms

Total                                        8s



<참조> 

  - Angular-Express-Train-Seed

  - Yeoman : generator-fullstack with angular and express

  - dos2unix 설치하여 사용하기

posted by 윤영식
2013. 7. 10. 15:42 MongoDB/Prototyping

Mongoose를 사용할 경우 몇가지 유용한 API를 제공하고 있다. 이중 virtual, method, pre에 대한 사용법을 알아보자. 예제는 로그인시 패스워드를 암호화 하는 코드를 가지고 파악해 본다



1. Schemas

  - http://mongoosejs.com/docs/guide.html 의 Schemas에 대한 정의와 플러그인 확장이 가능하다

  - 스키마 인스턴스를 만들었다면 여기에 methods, statics, indexes, virtuals, options등을 설정할 수 있다

  - 플로그인들은 http://plugins.mongoose.com에서 검색가능하다. 현재 나는 _id에 대한 auto increase 기능을 잘 쓰고 있다 

// 1) 스키마 만들기 : 도큐먼트 구조의 정의

var ComponentSchema = new mongoose.Schema({
    _id         : { type: Number },
    name        : { type: String },
    type        : { type: String },
    config      : { type: String },
    updated_on  : { type: Date, default: Date.now }
  });

  // 2) 플러그인 확장 & methods, statics, indexes, virtuals, options등 설정
  ComponentSchema.plugin(autoinc.plugin, {
    model : 'sd_component',
    field : '_id',
    start : 200000,
    step  : 1
  });


// 3) 모델은 스키마 정의에 대한 컴파일된 생성자이다. Model 인스턴스를 통해서 몽고디비로부터 데이터를 CRUD한다

var Component = mongoose.model('sd_component', ComponentSchema);


  - 샘플로 UserSchema 생성하기

  var UserSchema = new mongoose.Schema({
    username : {type:String, required:true , unique:true},
    hashed_password : {type:String, required:true},
    salt : {type:String, required:true}
  });



2. Instance Methods

  - 스키마가 만들어지고 난후 기본적으로 설정된 메소드를 상속받아 사용한다 예) findById

  - 사용자 커스텀 메소드를 정의할 수 있다. 처리 결과를 return 한다.

  - statics는 callback을 파라미터로 넘기고, return이 없다. (참조)

// 메소드안에서 메소드 호출이 가능하다  

UserSchema.method('authenticate', function(plainText) {
    console.log('authenticate called:')
    console.log('plain text = ' + plainText)
    console.log('hashed = ' + this.encryptPassword(plainText))
    console.log('db password= ' + this.hashed_password)
    return this.encryptPassword(plainText) === this.hashed_password;
  });

  UserSchema.method('makeSalt', function() {
    return Math.round((new Date().valueOf() * Math.random())) + '';
  });


  // crypto 모듈 사용하여 암호화  

  UserSchema.method('encryptPassword', function(password) {
    return crypto.createHmac('sha1', this.salt).update(password).digest('hex');
  });

  UserSchema.method('generateToken', function() {
    return crypto.createHash('md5').update(this.username + Date().toString()).digest("hex");
  });



3. Virtual

  - MongoDB에 저장되지 않는 편리한 Attribute이다 (참조)

  - set/get 펑션을 정의한다

  UserSchema.virtual('password')
    .set(function(password) {
      this._password = password;
      this.salt = this.makeSalt(); // 사용자정의 메소드 호출
      this.hashed_password = this.encryptPassword(password); // 사용자정의 메소드 호출
    })
    .get(function() { return this._password; });


// 모델 생성

var User = mongoose.model('User', UserSchema);

User.password = '1234';     // set

console.log(User.password); // get



4. Pre

  - 몽구스의 middleware기능이다

  - init, validate, save, remove 메소드 수행시 처리되는 미들웨어 펑션이다 

  - 복잡한 유효성검사, 트리거 이벤트 처리등. 예로 사용자를 삭제하면 사용자 관련 블로그포스트도 삭제하기같은 경우 사용

     또는 에러 핸들링

  UserSchema.pre('save', function(next) {
    this.token = this.generateToken();
    if (!validatePresenceOf(this.password || this.hashed_password)) {
      next(new Error('Invalid password'));
    } else {
      next();
    }
  });


function validatePresenceOf(value) {
  return value && value.length;
}


// ex)

User.save(function(err) { console.log(err.message); } );


  - post : 사후처리



5. 전체 소스 코드

  - User Model을 모듈로 만든다.  User.js

  - 애플리케이션에서는 User.js를 Model 인스턴스를 리턴받아서 사용한다 

  - Express의 Middleware에서 사용자 로그인시에 User.authenticate을 호출하여 패스워드를 암호화 한다

var mongoose = require('mongoose'),
  crypto = require('crypto');

module.exports = function () {

  var UserSchema = new mongoose.Schema({
    username : {type:String, required:true , unique:true},
    hashed_password : {type:String, required:true},
    salt : {type:String, required:true}
  });

  UserSchema.virtual('password')
    .set(function(password) {
      this._password = password;
      this.salt = this.makeSalt();
      this.hashed_password = this.encryptPassword(password);
    })
    .get(function() { return this._password; });

  UserSchema.method('authenticate', function(plainText) {
    console.log('authenticate called:')
    console.log('plain text = ' + plainText)
    console.log('hashed = ' + this.encryptPassword(plainText))
    console.log('db password= ' + this.hashed_password)
    return this.encryptPassword(plainText) === this.hashed_password;
  });

  UserSchema.method('makeSalt', function() {
    return Math.round((new Date().valueOf() * Math.random())) + '';
  });

  UserSchema.method('encryptPassword', function(password) {
    return crypto.createHmac('sha1', this.salt).update(password).digest('hex');
  });

  UserSchema.method('generateToken', function() {
    return crypto.createHash('md5').update(this.username + Date().toString()).digest("hex");
  });

  UserSchema.pre('save', function(next) {
    this.token = this.generateToken();
    if (!validatePresenceOf(this.password || this.hashed_password)) {
      next(new Error('Invalid password'));
    } else {
      next();
    }
  });

  return mongoose.model('User', UserSchema);
}

function validatePresenceOf(value) {
  return value && value.length;
}



<참조>

  - mongoosse virtual 사용법

  - Action function과 Callback function의 이해 

  - 샘플소스의 GitHub 저장소

posted by 윤영식
prev 1 next