블로그 이미지
Peter Note
Web & LLM FullStacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

Category

Recent Post

2013. 11. 27. 04:20 My Projects/BI Dashboard

Angular.js에서 Controller는 View DOM객체의 표현을 담당한다면 Service 모듈은 View단에 표현되는 NonVisible한 비즈니스 업무 데이터를 담당한다. 이전 블로그까지 모든 데이터 핸들링은 Controller에 담겨 있었는데 이를 Service 모듈로 분리 래팩토링 해보자. 또한 모듈분리를 하면서 $http를 추상화한 $resource를 사용해 보자 




1. factory vs service 무엇을 쓸까? (참조)

  - <module>.factory는 객체를 new 해서 return 해 주어야 한다. 

    따라서 별도의 function을 정의하고 해당 펑션을 new해서 return하는 구조일 때 사용

  - <module>.service는 function의 내용을 정의하고 new한 객체를 return할 필요가 없다 

  - <module>.factory 는 공통된 것을 function으로 이미 만들어 놓은 것을 사용할 때. 즉, 공통 모듈 <Module>Svc 에 생성

  - <module>.service 는 업무적으로 $resource/$http 이용하여 RESTful API 호출할  때. 즉 업무 모듈 <Module>Biz 에 생성



2. $resource 사용하기 

  - $resource 객체는 RESTful 클라이언트 애플리케이션 개발을 위한 $http 상위 레벨의 서비스이다

  - http에 대한 보다 정교한 header configuration이 가능하다

  - resource interceptors 가 있다

  - 보다 향상된 promises api를 제공한다 

  - API

// 사용법

$resource(url[, paramDefaults][, actions]);


// 의미 

- url

  string 이고, /user/:username 처럼 :<param> 형태로 입력가능하다. 전체 url을 넣어도 됨 


- paramDefaults 

  url parameters object 이고, /path/:verb 일때 파라미터값이 {verb:'greet', name:'dowon'} 이면

  url 결과는 /path/greet?name=dowon 으로 간다. @가 앞에 붙으면 전달하는 객체(json)에서 값을 추출하여 대입한다

 예) 

  var User = $resource('/user/:userId', {userId:'@id'});

  var user = User.get({userId:123}, function() {

     user.abc = true;

     user.$save();

  });


- actions 

  object.<object> 이고, $http.config의 action 을 상속받으며 사용자 정의하여 action을 추가할 수 있다.

  형식)

  {action1: {method:?, params:?, isArray:?, headers:?, ...},

   action2: {method:?, params:?, isArray:?, headers:?, ...},

   ...}


- 리턴타입

  기본적으로 resource에서 제공하는 action과 사용자 정의 action을 담아서 리턴한다 

  기본 action)

{ 'get':    {method:'GET'},

  'save':   {method:'POST'},

  'query':  {method:'GET', isArray:true},

  'remove': {method:'DELETE'},

  'delete': {method:'DELETE'} };



3. $resource 공통 모듈 만들기

  - $resource의 기본 action을 보면 'update': {method:'PUT'} 이 존재하지 않는다. 이를 custom action으로 포함시키고 공통 모듈로 만들어 보자. 

  - 공통 모듈을 만들기 전에 ngResource를 추가해야 한다. 애플리케이션에 없다는 다음과 과정을 거친다 

// bower로 angular resource 설치 및 bower.json에 추가 

$ bower install angular-resource --save


// index.html 에 추가 

<!-- build:js scripts/core.js -->

    <script src="bower_components/jquery/jquery.js"></script>

    <script src="bower_components/jquery-migrate/jquery-migrate.min.js"></script>

    <script src="bower_components/angular/angular.js"></script>

    <script src="bower_components/angular-route/angular-route.js"></script>

    <script src="bower_components/angular-resource/angular-resource.js"></script>

    <script src="bower_components/angular-cookies/angular-cookies.js"></script>

    <script src="bower_components/angular-sanitize/angular-sanitize.js"></script>

<!-- endbuild --> 


// ngResource가 필요한 모듈에 추가하기  

var DashboardApp = angular.module('DasbhoardApp', [

  'ngRoute',                                                  

  'ngCookies',

  'ngResource',

  'ngSanitize',

  'DasbhoardApp.CommonCtrl',

  'DasbhoardApp.RestTestBiz',

]);

  - $resource를 이용하여 공통 모듈을 만들어 보자 

// common/services/RestfulSvc.js 파일을 생성한다 

'use strict';

// 모듈 명칭 정의

var RestfulSvc = angular.module('DasbhoardApp.RestfulSvc', []);


RestfulSvc.factory('RestfulSvcApi', ['$resource', function ($resource) {

/**

* Restful API version. it must be attached the called url.

*/

var prefixUrl = '/api/v1';

/**

* @domain : biz object ex) person

* @key : biz object id ex) persion id is 123

* @action : server action name. if it exist, you can define or not.

* ex) http://www.bennadel.com/blog/2433-Using-RESTful-Controllers-In-An-AngularJS-Resource.htm

* add a update action for method of PUT

*/

return $resource( 

                           // 호출하는 url 형식

    prefixUrl + '/:domain/:key/:action',

                           // 호출 url 형식의 :domain :key :action 에 동적으로 받게 되는 파라미터

   {

    domain: "@domain",

    key: "@key",

    action: "@action"

   },

                            // 추가 action들 one, all, update 

   {

     one: { method: 'GET', isArray: false },

     all: { method: 'GET', isArray: true },

      update: {method:'PUT'}

    }

   );

}]);


// 새롭게 서비스 공통 모듈을 생성하였으니 index.html 과 DashboardApp 모듈에 추가한다 
// index.html 에서 
    <script src="scripts/common/controllers/CommonCtrl.js"></script>
    <script src="scripts/common/services/RestfulSvc.js"></script>

// DashboardApp.js 에서 
var DashboardApp = angular.module('DasbhoardApp', [
  'ngRoute',                                                  
  'ngCookies',
  'ngResource',
  'ngSanitize',
  'DasbhoardApp.CommonCtrl',
  'DasbhoardApp.RestTestBiz',
  'DasbhoardApp.RestfulSvc'
]);

 


4. Controller에 Service 모듈의 서비스 호출하기 

  - $http 방식을 $resource에 대한 추상 모듈 서비스로 대체한다

  - 기존의 RestTestBiz.js의 "RestTestBiz.personCtrl" 컨트롤러 로직을 리팩토링한다 

'use strict';

// 의존하는 모듈 주입

var RestTestBiz = angular.module('DasbhoardApp.RestTestBiz', ['DasbhoardApp.RestfulSvc']);


// 사용할 모듈의 서비스 주입 

RestTestBiz.controller('RestTestBiz.personCtrl', ['$scope', 'RestfulSvcApi', function ($scope, RestfulSvcApi) {


  var bizDomain = 'person';

  var load = function () {

    console.log('before calling...');

       // 사용자 정의 액션 all 호출 

    RestfulSvcApi.all(

    // param key domain의 값인 bizDomain는 @domain에 맵핑된다 

    { domain: bizDomain },

    // success

    function (response) {

    $scope.persons = response;

    console.log('persons is ', $scope.persons);

    }, 

    // error

    function (response) {

    });

     };


  load();


  $scope.save = function () {

  console.log('Save Person : ', $scope.person);

  RestfulSvcApi.save(

                        // 파라미터를 열거하면 하나의 json 객체로 합쳐준다 

  { domain: bizDomain }, $scope.person,

  function(response) { load(); },

  function(response) {}

  );

  };


  $scope.update = function () {

  console.log('Update Person : ', $scope.person);

  RestfulSvcApi.update(

  { domain: bizDomain }, $scope.person ,

  function(response) { load(); },

  function(response) {}

  );

  };


  $scope.deletePerson = function(personId) {

  console.log('Delete Person : ', personId);

  RestfulSvcApi['delete'](

  { domain: bizDomain, key: personId }, 

  function(response) { load(); },

  function(response) {}

  );

  };


}]);

  - RestfulSvcApi 와 같은 모듈 서비스를 해주는 추상화 공개 모듈이 존재한다. (참조)

    + Restangular

    + ModelCore

    + Angular-ServiceStack

    + Angular-ActiveRecord

    + Breezejs


  * 소스 : https://github.com/ysyun/SPA_Angular_SpringFramework_Eclipse_ENV/tree/feature_angular_service



<참조>

  - Angular.js v1.2 에서 향상된 내역들

  - ngResource를 통하여 트위터 연동하기 예제

  - $resource를 통한 RESTful API 구현하기 (필독)

  - rest 스타일의 ORM 모듈들

posted by Peter Note
2013. 11. 25. 09:36 AngularJS/Start MEAN Stack

MEAN Stack을 가지고 서비스를 만드는데 사용을 하려면 어떤 순서로 익혀야 할까? 각각의 기술을 따로 배우는 것은 어차피 전문서적들이 있기 때문에 여기서 다시 상세히 설명하는 것은 의미가 없을 것 같고, 서비스를 기획, 개발, 런칭까지의 과정을 알아보자. 모든 과정의 내용은 웹 서비스쪽에 공개되어 공유될 것이다 




Sprint-1) Full Stack 개발자 되기

  - MEAN 개념과 개요

    + AngularJS

    + NodeJS + ExpressJS

    + MongoDB + Redis(Advanced)

  - MEAN 이외에 알아야 할 것들

    + JavaScript 

    + Bootstrap (or Zurb Foundation

    + NodeJS Modules

    + Mongoose



Sprint-1) 개발 준비 단계

  - 로컬 개발 환경 만들기 

    + Vagrant + Linux 환경 구성하기

    + 개발환경에 Node.js, MongoDB 설치하기 

    + Trello 에서 Scrum 개발하기 

  - MEAN Stack 개발도구 개발환경에 설치하고 익히기 

    + Yeoman : 클라이언트 라이브러리관리 및 배포 Grunt, Bower, Yo 설치하기 

    + NPM : 서버 라이브러리 관리

  - Git & GitHub 저장소 만들기 

    + Git Branch 전략 

    + GitHub 사용방법

  - 클라우드 환경에 배포하여 테스트 하기

    + Cloud Development(Nitrous.io) 를 통한 협업개발 및 테스트하기

    + Heroku같은 PaaS에 배포하여 테스트하기 

 


Sprint-1) MEANK Stack 개발 환경 구성하기

  - SRS 작성하기 

    + Software Request Requirement란 무엇인가?

    + Trello 에 소프트웨어 요구사항 정의서 작성하기 

    + Balsamiq 도구 사용하여 Wireframe & Mockup 그리기 화면 그리기

  - AngularJS Scaffolding 코드 사용하기 

    + angular-generator 알아보기 

    + angular 기본골격 만들기

    + NodeJS로 테스트 하기 

  - MEAN Stack 테스트 전략 

    + AngularJS에서의 테스트 방법 : Karma Framework

    + NodeJS에서의 테스트 방법 : Mocha 그외 



Sprint-2) 서비스 프로토타입 개발하기

  - 가장 중요한 화면 설계하기

    + 클라이언트 화면 Mockup 구체화 하기 

    + NodeJs에서 처리 할 RESTful API 설계하기 

    + MongoDB 에 저장 할 Document Schema 설계하기

  - MongoDB Store 개발 및 테스트

    + Dynamic Schema, Embedded vs Reference 에 대한 이해 

    + Mongoose 기반으로 Schema 만들기

    + Test Framework 이용한 Model 단위 테스트하기 

  - Node.js 기반 RESTful API 개발 및 테스트

    + RESTful API 코드 뼈대 만들기 in NodeJS

    + Test Framework 이용한 서비스 단위 테스트하기

    + RESTful Client 테스트 도구 또는 curl 로 호출 테스트하기

  - Angular.js 기반 화면 개발 및 테스트

    + AngularJS와 Bootstrap 이용하여 화면 구성하기

    + AngularJS 애플리케이션 구조 개발 및 RESTful API 서비스 개발 

    + Test Framework 이용한 화면 단위 테스트하기 

  - Heroku 같은 PaaS에 올려서 통합 테스트 하기 



Sprint-3) 서비스 개발 반복하기

  - 반복하기 

    + sprint-01에서 반복하였던 과정을 지속하기 

  - 메인 및 로그인 화면 만들기 

    + Bootstrap의 다양한 도구 활용

    + OAuth 기능 로그인 하기  

  - 채팅기능 만들기 

    + 채팅 Mockup 만들기

    + NodeJS Socket.io API 구현

    + MongoDB 채팅 Schema 설계

    + AngularJS Socket.io 멀티 케스팅 구현 

    + Bootstrap 통한 화면 구현 및 AngularJS 연동

 - Heroku 같은 PaaS에 올려서 프로토타입 테스트 하기 



Sprint-3) 서비스 런칭하기

  - 서비스 일일 빌드시스템 구성하기 

    + Travis (또는 Jenkins)기반 자동 배포 기능 만들기 

    + Travis 에서 빌드된 것을 클라우드시스템(PaaS) 자동 배포하기 

  - 런칭 페이지 만들기 

    + GitHub Pages를 이용한 런칭 만드는 방법

    + Bootstrap을 이용하여 런칭 페이지만들어 GitHub에 반영하기 

  - 모니터링 하기

    + 클라이언트 AngularJS Log 모니터링 하기
    + NodeJS Log 모니터링 하기 

    + MEAN Stack 운영 및 장애 모니터링 전략 



Next Project) MEAN Stack Advanced

  - 채팅기능 고도화하기 

    + SNS 서비스 아키텍쳐들 알아보기  

    + 대용량 데이터를 처리를 위한 아키텍쳐 설계하기

  - NodeJS + Redis + MongoDB 연동하기 

    + Redis 알아보기 

    + NodeJS-Redis 연동하기 

  - 정제된 데이터 RDBMS와 연동하기

    + NodeJS - MySQL 연동하기 

    + MongoDB의 MapReduce를 이용하여 데이터 마이닝하기 

    + NodeJS에서 MongoDB 마이닝 데이터 MySQL에 넣기 

  - SNS 서비스 운영 고도화

    + NodeJS와 Nginx 연동하기 

    + NodeJS를 Proxy 서버로 만들어 Clustering 하기

    + MongoDB Sharding 구조 만들기 

    + Redis Master/Slave구조 만들기 

  - 유지보수 전략

    + 테스트 환경과 프로덕션 환경의 재구성

    + NodeJS Scale-out 확장 전략

    + MongoDB Sharding 및 Data Backup 전략 

    + Redis Scale-out 전략 

  - 빅데이터의 활용

    + 개인화를 위한 데이터 활용 방안

    + MongoDB Integration Framework을 이용한 MapReducing


전체 서비스를 만들고 런칭하기까지 참 많은 것들을 익히고 개발해야 하지만, 누군가에게 의미있는 가치를 줄 수 있는 SNS(Social Network Service)를 할 수 있다는 것에 만족을 느낀다. 


posted by Peter Note
2013. 11. 22. 03:53 My Projects/BI Dashboard

Angular.js를 하면서 모듈단위로 개발을 할 수 있고, 여러명이 동시에 관련 모듈을 개발하여 합칠 수도 있다. 즉, 엔터프라이즈급 애플리케이션 개발 공수가 든다면 모듈을 나누고 어떻게 합쳐서 하나의 SPA로 만드는지 알아보자 




1. 모듈 단위 나누기

  - 기존의 하나의 애플리케이션 모듈로 진행되는 것을 여러 모듈로 나어 큰 규모의 애플리케이션을 만들자 (참조)

    + 하나의 애플리케이션 .js 파일을 하나의 모듈로 한다

    + 모듈은 앞글자가 대문자로 시작한다 (App, Biz / Ctrl, Svc, Flt, Drtv 등 접미어를 붙여서 구분한다)

    + 모듈파일이 많이 커질때는 Lazy Loading을 고려한다 (참조1, 참조2)

    + 일반 기능들의 명칭은 소문자로 시작한다 (Ctrl, Svc, Flt, Drtv 등 접미어를 붙인다)

    + 공통으로 사용되는 것은 $rootScope 또는 Factory를 이용한다 

    + 파일과 모듈이 분리되어 테스트 코드를 쉽게 만들어 테스트 가능하다

  - 업무 모듈 유형 

    + Application Main 모듈 : <ProjectName>App.js (주로 routing 관련 설정)

    + Biz 모듈 : <UI명칭>Biz.js (화면과 맵핑되는 모듈 = n controller + n service/factory 존재)

  - 공통 모듈 유형 

    + Controller 모듈 : CommonCtrl.js (메뉴, 로그인/로그아웃 UI 핸들링)

    + Service 모듈 : CommonSvc.js (메뉴, 로그인/로그아웃 서버통신 처리)

    + Directives 모듈 : CommonDrtv.js (일반적인 지시자들), UtilDrtv.js (유틸리티), UXDrtv.js (UI), <User Defined>Dtrv

    + Filters 모듈 : CommonFlt (공통), UtilFlt (유틸) 필터링 관련 모듈 

  - /src/main/scripts의 기능별 폴더를 생성하여 모듈을 분리한다. 또는 업무적으로 폴더를 분리할 수도 있다

// /src/main/scripts 에서 

// 기존 폴더

/controllers/main.js

app.js 


// 추가 폴더 및 명칭변경 

/controllers

/directives

/filters

/services

DashboardApp.js

  - 주로 많이 개발하는 것이 화면단위의 <BizName>Ctrl/Svc.js 가 될 것이다 

  - vendor가 제공하는 컴포넌트는 Bower를 통하여 설치 관리되며, 배포시에 Grunt를 통하여 minification 된다 



2. 공통 모듈 분리하기 

  - 폴더를 만들었으면 이제 .js 안에 모듈명을 지정하고, 의존관계를 설정한다 

  - 모듈 명칭은 namespace 구분을 위하여 자바의 패키지처럼 <ApplicatonName>.<구분>.<FileName> 이라고 대소문자 작명한다

  - 모듈 분리 순서

    + app.js 파일명칭을 DashboarApp.js 로 변경하고, 모듈명칭을 DashboardApp로 시작한다

    + controllers/main.js 파일명칭을 CommonCtrl.js 로 변경하고, 모듈명칭을 <ApplicationName>.CommonCtrl로 변경한다

// DashboardApp.js 에서 

// 두번째 파라미터 배열에서 의존관계에 있는 모듈을 정의한다

var DashboardApp = angular.module('DasbhoardApp', [

  'ngCookies',

  'ngResource',

  'ngSanitize',

  'DasbhoardApp.CommonCtrl'

]);


// CommonCtrl.js 에서 

var CommonCtrl = angular.module('DasbhoardApp.CommonCtrl', []);

    + 분리된 파일은 index.html 에 추가한다 : 향후 파일이 늘어나면 Lazy loading을 고려한다 

// index.html 에서 

<!-- build:js scripts/dashboard-spa.js -->

<script src="scripts/DashboardApp.js"></script>

<script src="scripts/controllers/CommonCtrl.js"></script>

.. 이곳에 추가를 한다 ..

<!-- endbuild -->



3. 업무 모듈 분리하기 

  - Person 테이블을 보여주는 restTest.html 파일에 대한 별도 모듈을 만든다. (resttest.html을 restTest.html로 명칭 변경했음)

  - 1 view : 1 module 로 구성하고, 1 module :  n controller + n service 를 넣는다. 즉 1 module : 1 biz 로 한다

// step-1 : 기존 scripts 에 나열 되었던 controllers, directives, filters, services 폴더를 common 폴더를 만들어 옮긴다 

// step-2 : 업무적인 폴더는 scripts 폴더에 정의한다. 여기서는 person 폴더를 생성함 

// step-3 : scripts/person 폴더 밑에 RestTestBiz.js 파일을 만든다 

// step-4 : index.html 내역을 다음과 같이 수정한다


<!-- build:js scripts/dashboard-spa.js -->

<script src="scripts/DashboardApp.js"></script>

<script src="scripts/common/controllers/CommonCtrl.js"></script>

<script src="scripts/person/RestTestBiz.js"></script>

<!-- endbuild -->


// 최종 모습 

  - CommonCtrl.js 에 들어있는 restTestCtrl 코드를 RestTestBiz.js 파일로 옮기고, 명명규칙에 따라 수정한다 

var RestTestBiz = angular.module('DasbhoardApp.RestTestBiz', []);


RestTestBiz.controller('RestTestBiz.personCtrl', ['$scope', '$http', function ($scope, $http) {

    .. 중략 ..

    $scope.deletePerson = function(id) {

    console.log('Delete Person : ', id);

        $http['delete'](actionUrl + id).success(function () {

            load();

        });

    };

  }]);


// RestTestBiz.service or RestTestBiz.factory 를 정의한다 

  - DashboardApp.RestTestBiz 모듈을 DashboardApp.js에서 의존성을 등록하고, Routing시의 controller 명칭도 수정 등록한다 

var DashboardApp = angular.module('DasbhoardApp', [

  'ngRoute',                                                  

  'ngCookies',

  'ngResource',

  'ngSanitize',

  'DasbhoardApp.CommonCtrl',

  'DasbhoardApp.RestTestBiz',

]);


DashboardApp.config(['$routeProvider', function ($routeProvider) {

    $routeProvider

      .when('/', {

        templateUrl: 'views/main.html'

      })

      .when('/resttest', {

        templateUrl: 'views/restTest.html',

        controller: 'RestTestBiz.personCtrl'

      })

      .otherwise({

        redirectTo: '/'

      });

  }]);



* Angular.js v1.2.1 업그레이드 

  - 가장 큰점은 Angular의 route 가 별도로 분리되었다는 것이다 (참조1, 참조2)

  - v1.2.1 버전을 설치해 보자 

// bower를 통하여 기존 모듈 제거 및 재설치 작업 수행

$ bower uninstall angular angular-resource angular-cookies angular-mocks angular-sanitize angular-scenario

bower uninstall     angular

bower uninstall     angular-resource

bower uninstall     angular-cookies

bower uninstall     angular-mocks

bower uninstall     angular-sanitize

bower uninstall     angular-scenario


// bower.json에서 버전 정보를 삭제한다 

// --save 설치 

$ bower install angular angular-route angular-resource angular-cookies angular-sanitize --save


// --save-dev 설치 

$ bower install angular-mocks angular-scenario --save-dev


// 설치 버전 의존성 정보

$ bower list

bower check-new     Checking for new versions of the project dependencies..

SolarDasbhoard#0.0.0 /Users/nulpulum/development/eclipse_workspace/lg_solar/SPA_ASE

├── angular#1.2.1

├─┬ angular-cookies#1.2.1

│ └── angular#1.2.1

├─┬ angular-mocks#1.2.1

│ └── angular#1.2.1

├─┬ angular-resource#1.2.1

│ └── angular#1.2.1

├─┬ angular-route#1.2.1

│ └── angular#1.2.1

├─┬ angular-sanitize#1.2.1

│ └── angular#1.2.1

├─┬ angular-scenario#1.2.1

│ └── angular#1.2.1

├─┬ bootstrap#3.0.2

│ └── jquery#1.9.1 (2.0.3 available)

├── es5-shim#2.0.12 (latest is 2.1.0)

├── jquery#1.9.1 (latest is 2.0.3)

├── jquery-migrate#1.2.1

├── json3#3.2.5

└── respond#1.3.0

  - routing 관련 설정 변경

// index.html 에서 angular.js 밑으로 angular-route.js 추가 

<script src="bower_components/angular/angular.js"></script>

<script src="bower_components/angular-route/angular-route.js"></script>


// DashboardApp.js 파일에서 ngRoute 모듈 추가 

var DashboardApp = angular.module('DasbhoardApp', [

  'ngRoute',                                                  

  'ngCookies',

  'ngResource',

  'ngSanitize',

  'DasbhoardApp.CommonCtrlMod'

]);



* HTML5 지원하는 모듈 넣기 

  - html5shiv를 포함한 modernizr를 설치하여 오랜된 브라우져에서는 HTML5가 지원토록 한다 (참조)

  - IE 9 이하에서 선택적으로 넣자 

// bower install

$ bower install modernizr --save


// index.html 에서 

    <!--[if lt IE 9]>

      <script src="bower_components/modernizr/modernizr.js"></script>

      <script src="bower_components/es5-shim/es5-shim.js"></script>

      <script src="bower_components/json3/lib/json3.min.js"></script>

    <![endif]-->



* 메뉴 동작방식 변경하기 

  - data-ng-click 방식으로도 변경해 본다

// index.html 에서 

// 기존 코드 

<div class="collapse navbar-collapse">

  <ul class="nav navbar-nav">

    <li data-ng-class="activeWhen(path()=='/')"><a href="#/">Home</a></li>

    <li data-ng-class="activeWhen(path()=='/resttest')"><a href="#/resttest">RESTTest</a></li>

  </ul>

</div>


// 변경 코드

<div class="collapse navbar-collapse">

  <ul class="nav navbar-nav">

    <li data-ng-class="activeWhen(path()=='/')">

      <a href="" data-ng-click="setRoute('/')">Home</a>

    </li>

    <li data-ng-class="activeWhen(path()=='/resttest')">

      <a href="" data-ng-click="setRoute('/resttest')">RESTTest</a>

    </li>

  </ul>

</div>

  - 모듈안에서 사용되는 controller, service, filter, directive의 명칭은 <Module명>.<기능명칭> 처음에는 소문자로 시작한다 

// index.html 에서 

<div class="navbar navbar-inverse navbar-fixed-top"  data-ng-controller="CommonCtrlMod.menuCtrl">

..중략..

</div> 

  - menuCtrl안에 setRoute 메소드를 정의한다 

CommonCtrlMod.controller('CommonCtrlMod.menuCtrl', ['$scope', '$location', function ($scope, $location) {

    $scope.activeWhen = function (value) {

    return value ? 'active' : '';

    };

    $scope.path = function () {

        return $location.url();

    };

    $scope.setRoute = function (url) {

        $location.path(url);

    }    

  }]);


* 소스 : https://github.com/ysyun/SPA_Angular_SpringFramework_Eclipse_ENV/tree/feature_angular_modules



<참조>

  - Angular.js 기반 큰규모 애플리케이션 구조 만들기

  - Angular.js v1.2 에서 변경 추가된 사항들-Outsider

  - Angular.js v1.2 에서 변경된 것들

  - HTML5 지원을 위한 Modernizr vs Html5shiv 에 대하여 

posted by Peter Note
2013. 11. 21. 06:08 My Projects/BI Dashboard

고객이 사용하는 브라우져중 IE는 v8이상만 지원토록 한다면, 이전의 블로그 소스는 IE8이상에서 제대로 보이지 않을 것이다. IE8 이상 및 Chrome, Safari, Firefox에 지원토록 애플리케이션 환경을 개선해 보자 (사실 IE8에서 돌아가면 다른 곳은 문제되지 않음)




1. Windows OS 에서 소스 셋업하기 

  - GitHub에서 환경파일을 클론하고 Windows 환경 설정이 없는 branch로 이동

  - npm install 수행

$ git clone git@github.com:ysyun/SPA_Angular_SpringFramework_Eclipse_ENV.git

$ git checkout feature_mybatis_mapper

$ npm install .

  - npm install 시에 imgmin 관련 오류가 발생한다. package.json과 Gruntfile.js 에서 이미지 압축설정을 제거한다 

// package.json 에서 두가지 설정 제거 (분홍색)

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

"grunt-svgmin": "~0.2.0",


// Gruntfile.js 에서 imagemin, svgmin 관련 설정 제거 (분홍색)

imagemin: {

.. 중략 ..

},

svgmin: {

.. 중략 ..

},

.. 중략 ..

dist: [

'coffee',

'copy:styles',

'imagemin',

'svgmin',

'htmlmin'

]

  - Mysql 설치하고 사용자 생성

    + WEB-INF/spring/dbpool.properties에서 Database를 생성하고 사용자를 생성한다 

    + 테스트 person table을 만든다 

mysql> GRANT ALL 

          on <DBName>.* 

          to <UserName>@localhost 

          identified by '<Password>' with grant option;

mysql> create database <DBName>;

mysql> flush privileges;

mysql> update mysql.user set password = password('<password>') 

           where host='localhost' and user='<UserName>';

mysql> flush privileges;

mysql> \q

$ mysql -u <UserName> -p <DBName>

Enter password : *****

mysql> create table person( id int, name varchar(30));

  - eclipse에서 Maven Project로 import를 해서 Tomcat 7 버전에서 수행하여 보면 아직은 제대로 나오지 않을 것이다. 



2. jQuery 1.9.* 이상 사용할 때 ie 객체 null 에러 해결 

  - Angular.js와 함께 사용시 jQuery 버전이 1.2 이상을 경우 IE8 에서 window.ie 객체를 찾으면서 오류가 발생한다 

  - 해당 오류를 해결하기 위하여 jquery-migrate을 추가한다 (참조)

// bower를 통하여 bower_components 밑으로 설치한다

$ bower install jquery-migrate --save


// index.html 에 추가한다 

<script src="bower_components/jquery/jquery.js"></script>

<script src="bower_components/jquery-migrate/jquery-migrate.min.js"></script>

<script src="bower_components/angular/angular.js"></script>



3. CDN 문제 해결

  - jquery 또는 angular와 같이 유명한 Javascript 모듈은 Grunt 빌드할 때 구글의 CDN 쪽으로 url을 자동 변경한다. 

  - Gruntfile.js에서 CDN 관련 내역을 삭제한다 

// 분홍색 제거 

cdnify: {

  dist: {

    html: ['<%= yeoman.dist %>/*.html']

  }

},


// 분홍색 제거 

grunt.registerTask('build', [

'clean:dist',

'useminPrepare',

'concurrent:dist',

'autoprefixer',

'concat',

'copy:dist',

'cdnify',



4. Bootstrap RWD 적용 문제 해결

  - Bootstrap v3을 사용할 경우 IE8 에서 MediaQuery를 지원하지 않는 관계로 반응형 웹 디자인(RWD)이 적용되지 않는다 (참조)

  - IE8 이상에서 MediaQuery 지원을 위하여 respond.js 를 설치한다 

// bower 설치

$ bower install respond --save


// index.html 설정 : respond.js 설정은 bootstrap 밑으로 넣는다

<!-- build:js scripts/plugins.js -->

<script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>

<script src="bower_components/respond/respond.src.js"></script>

<!-- endbuild -->



5. Minification 으로 인한 Angular.js 애플리케이션 파일 오류 문제 해결

  - Grunt 빌드를 통하여 main.js (controller 파일)이 minification 되면 $scope, $http등도 이름이 축약되어 IE에서 인식을 못 한다 

// index.html 원본 소스

// 하기 <!-- --> 주석처리 된것을 Grunt가 인식하여 scripts 폴더 밑으로 sciprts.js 파일명으로 minification 된다

<!-- build:js({.tmp,app}) scripts/scripts.js -->

<script src="scripts/app.js"></script>

<script src="scripts/controllers/main.js"></script>

<!-- endbuild -->


// dist/index.html 배포된 소스 

<script src="scripts/d41d8cd9.scripts.js"></script>

  - app.js 와 main.js 파일에서 $scope, $http 등과 같은 명칭이 축약되어 어떤것인지 알수 있게 배열 String(문자)로 명시해 준다 (참조)

// app.js 에서 

.config(['$routeProvider', function ($routeProvider) {

$routeProvider

  .when('/', {

    templateUrl: 'views/main.html',

    controller: 'MainCtrl'

  })

  .when('/resttest', {

    templateUrl: 'views/resttest.html',

    controller: 'RestTestCtrl'

  })

  .otherwise({

    redirectTo: '/'

  });

}]);


// main.js 에서 

angular.module('SolarDasbhoardApp')

  .controller('MenuCtrl', ['$scope', '$location', function ($scope, $location) {

    $scope.activeWhen = function (value) {

    return value ? 'active' : '';

    };


    $scope.path = function () {

        return $location.url();

    };

  }])

.. 하위도 동일 



6. HTML 검증 호환을 위해 html 태그 수정 

  - ng-* 로 되어 있는 속성을 전부 data-ng-*로 변경하여 HTML Validation 호환성을 확보한다 (참조

  - *.html 에서 data-를 전부 붙여준다 

// index.html 에서 

<body data-ng-app="SolarDasbhoardApp" data-ng-controller="MenuCtrl">


<ul class="nav navbar-nav">

<li data-ng-class="activeWhen(path()=='/')"><a href="#/">Home</a></li>

<li data-ng-class="activeWhen(path()=='/resttest')"><a href="#/resttest">RESTTest</a></li>

</ul>


<div class="container" data-ng-view=""></div>


// views/restest.html 에서 

<form accept-charset="UTF-8" data-role="form">

                    <fieldset>

      <div class="form-group">

        <input class="form-control" placeholder="ID" data-ng-model="person.id" type="text" value="99">

    </div>

    <div class="form-group">

    <input class="form-control" placeholder="Name" data-ng-model="person.name" type="text" value="dowon">

    </div>

    <input class="btn btn-lg btn-success btn-block" type="submit" value="Save" data-ng-click="save()">

    <input class="btn btn-lg btn-success btn-block" type="submit" value="Update" data-ng-click="update()">

    </fieldset>

</form>

.. 중략 ..

<tr data-ng-repeat="person in persons">

    <td>{{person.id}}</td>

    <td><a data-ng-click="delete(person.id)">{{person.name}}</a></td>

</tr>



7. IE에서 delete 예약어 문제 해결

  - IE에서 수행을 할려면 여전히 main.js 에서 오류가 있다고 나올 것이다

  - $http.delete에서 delete가 ie에서 예약어 이기 때문이다. 따라서 두군에 delete를 수정해 주어야 한다. 

// main.js 에서 

// 기존 코드 

 $scope.delete = function(id) {

    console.log('Delete Person : ', id);

        $http.delete(actionUrl + id).success(function () {

            load();

        });

    }


// 변경 코드 

 $scope.deletePerson = function(id) {

    console.log('Delete Person : ', id);

        $http['delete'](actionUrl + id).success(function () {

            load();

        });

    }


// views/resttest.html 에서 delete호출을 deletePerson 으로 변경

<td><a data-ng-click="deletePerson(person.id)">{{person.name}}</a></td>

 


8. ng-app 명명 문제 해결

  - IE8이상에서 ng-app의 위치와 선언방법을 변경한다 

// 형식

<html xmlns:ng="http://angularjs.org" id="ng-app" ng-app="optionalModuleName">


// 변경

<html class="ng-app:DasbhoardApp" id="ng-app" data-ng-app="DasbhoardApp" xmlns:ng="http://angularjs.org">


이제 Angular.js와 Bootstrap v3 을 IE8 버전 이상에서 사용해 보자 

* 소스 : https://github.com/ysyun/SPA_Angular_SpringFramework_Eclipse_ENV/tree/feature_windows_ie8



* 하루종일 삽질해서 알아낸 IE8 내용

  - 위의 소스를 IE8에서 수행하면 RestTest가 제대로 안될 것이다 (하기 내역은 feature_windows_ie8 에 첨부되지 않았음)

  - 문제 : IE 8 초기버전에서 console 객체가 undefined 라는 것 (참조)

// 이런 내용이 들어가야 한다 

/**

 * Protect window.console method calls, e.g. console is not defined on IE

 * unless dev tools are open, and IE doesn't define console.debug

 */

(function() {

  if (!window.console) {

    window.console = {};

  }

  // union of Chrome, FF, IE, and Safari console methods

  var m = [

    "log", "info", "warn", "error", "debug", "trace", "dir", "group",

    "groupCollapsed", "groupEnd", "time", "timeEnd", "profile", "profileEnd",

    "dirxml", "assert", "count", "markTimeline", "timeStamp", "clear"

  ];

  // define undefined methods as noops to prevent errors

  for (var i = 0; i < m.length; i++) {

    if (!window.console[m[i]]) {

      window.console[m[i]] = function() {};

    }    

  } 

})();


// bower 통하여 console-shim을 설치한다 

$ bower install console-shim --save


// index.html에 선택적으로 설정한다 

    <!--[if lt IE 9]>

      <script src="bower_components/modernizr/modernizr.js"></script>

      <script src="bower_components/es5-shim/es5-shim.js"></script>

      <script src="bower_components/json3/lib/json3.min.js"></script>

      <script src="bower_components/console-shim/console-shim.js"></script>

    <![endif]-->



<참조>

  - Angular.js 공식 IE 호환성 설정

  - IE 버전에 대한 조건부 주석처리하기

  - HTML5와 CSS3 지원을 위한 Modernizr 사용 이유


posted by Peter Note
2013. 11. 20. 10:58 AngularJS/Start MEAN Stack

갑작이 떠오른 아이디어를 모바일 서비스로 빠르게 만들고 싶다. 클라이언트/서버/스토어 이런 것을 해줄 사람은 없다. 나 홀로 만들어 보고 프로토타입핑해서 스타팅해보고 싶을 때 MEAN Stack을 사용하자 




1. 스프링같은 "WebApp Framework" 에 대한 고민

  클라이언트단의 Android 또는 iOS 네이티브 코드는 익히는데 시간은 없고, 다양한 스마트 기기에 대응을 했으면 한다. 그리고 JavaScript를 어느 정도 할 수 있다. 이때 생각할 수 있는 대안이 "웹앱 사이트" 또는 "모바일 웹앱"을 만드는 것이다. 그러나 예전 jQuery 코드를 생각해 보자. 조금만 복잡해 지면 쉽게 스파게티가 되고, 테스트 코드는 엄두도 못내며 결국 유지보수의 골칫덩어리로 남게 된다. 

그렇다면 자바에서 Spring Framework과 같이 DI가 되면서 MVC처럼 모듈화 개발을 가능하게 해주는 클라이언트단 프레임워크는 없을까? 처음엔 Backbone.js를 살펴 보았고, 예로 Trello와 같은 서비스에서 사용을 하고 있다. 하지만 코드의 길이가 길어지고 불필요한 중복코드의 남발이 야기되어 좀 더 간결하면서 예전 Flex Framework과 같은 것은 없을지 고민해 보았다. HTML을 확장해서 사용해도 자동 해석을 하여 브라우져가 이해할 수 있는 HTML로 바꾸어 주면 되는 그런 프레임워크 말이다. 역시 구글 형님들도 똑같은 고민을 하고 프로젝트에 적용하고 놀라운 효과를 본 새로운 프레임워크를 공식 런칭하게 되니 이름하여 AngularJS





2. 새로운 "I/O Machine" 에 대한 고민

  서비스에서 요하는 기능중 가장 중요한 부분은 Push 기능이 아닐까 싶다. 실시간으로 변화하는 정보를 수많은 유저에게 바로 Push 서비스해줄 수 있는 서버가 필요하다. 하지만 Push를 위하여 별도의 Port를 사용하긴 싫고 웹앱이므로 요청하였던 HTTP Port를 사용하고 싶다면 어떤 것을 선택해야 할까?

서버단이 Java라면 vert.x도 고려해 볼 만하다. 하지만 아직 레퍼런스와 문서가 부족하다. 하지만 여기에 우리에게 구세주와 같은 세로운 I/O 머신이 나왔으니 V8엔진을 기반으로 돌아가는 Node.js이다. Node.js를 사용하는 가장 큰 목적중 하나가 Socket.io와 같은 이벤트 기반의 Push 모듈들이 아닐까 생각이든다. Node.js는 모듈기반으로 필요한 것들을 추가하여 사용할 수 있다. 이를 위하여 NPM(Node Package Manager)이 존재하고 방대한 모듈과 레퍼런스들이 존재한다. 또한 Chrome에 적용된 V8 자바스크립트 해석기를 달고 있으므로 모든 코딩은 JavaScript 만으로 이루어진다





3. 스키마에 자유로운 "Store"에 대한 고민 

  서비스를 빠르게 만들고 싶은데 개발을 진행하다 보면 자주 테이블이 변경되고 이에 맞추어 서버코드들도 수정을 해주거나 새롭게 테이블을 생성해 주는 번거로운 작업들을 거친다. 따라서 스키마가 자유로운 데이터베이스를 원한다. 그리고 서비스이므로 사용자의 데이터를 많이 쌓아 두고 싶다. 이를 통해 향후 마케팅 용으로 사용하려 한다. 이왕이면 클라이언트/서버가 모두 자바스크립트이므로 데이터베이스 핸들링도 자바스크립트 이면 좋겠다. 이를 위해 새롭게 나온 NoSQL 이 있으니 이름하여 MongoDB. JSON형태로 데이터를 저장하고 정규화 없이 도큐먼트 형태로 저장을 할 수 있어서 스키마의 제약이 없다. 또한 테라급의 데이터 정도는 MongoDB를 Sharding구조로 만들어 MapReduce를 이용하여 처리할 수 있다. 그리고 Node.js 처럼 V8 해석기를 탑재하여 데이터 핸들링을 JavaScript로 할 수 있다. 마치 Oracle의 SqlPlus에서 Sql을 수행하듯 MongoDB가 정의한 형식의 쿼리를 수행할 수 있다. 





4. Node.js를 기업용 WAS(Web Application Server)로 만들기  

  초창기 Java를 떠올려 보자. 처음엔 Applet에 열광하여 업무를 애플릿으로 만든적이 있다. 그때 서버단은 Apache + PHP정도 였을 것이다. 그리고 SUN에서 J2EE 스팩을 발표하면서 Tomcat, JBoss, WebSphere, WebLogic 같은 WAS가 나왔으며, 이 또한 C계열의 TP-Monitor처럼 Java기반 I/O Machine 이 나오게 된 것이다. Java진영의 WAS는 나름의 공통된 스팩을 기반으로 웹을 이끌어 갈 수 있는 새로운 I/O 머신으로 떠올랐고, 기업에서 요구하는 Transaction 처리에 대한 스팩도 제안하여 많이 사용하게 되었다. 최근에는 Tomcat같은 Servlet 엔진만 갖춘 WAS에 Spring Framework를 사용하면 충분히 기업용 Transaction을 처리하는 업무를 개발할 수 있는 환경까지 오게 되었다. 그러나 Java의 WAS가 web1.0의 중심 머신이었다면 Node.js기반의 WAS가 web2.0의 중심 머신으로 되기 위한 조건은 무엇일까?

  결국 Node.js위에 올라가는 Java의 J2EE 스팩과 같은 견고한 스팩이 필요하다. 현재까지는 아직 그러한 스팩이 나오진 않으나 Java의 Servlet 스팩에 비견할 만한 프레임워크가 나왔으니 그것이 Express.js 이다. 이제 web2.0의 I/O 머신은 Node.js가 될 것이고, 여기에 웹에 대한 표준 스팩구현체는-사실 Node.js를 위한 공통된 스팩은 없다- 아니지만 가장 많이 사용하는 Express.js를 통하여 Node.js가 Tomcat과 같은 기능을 보유하게 되었다.




Angular.js -> Node.js + Express.js -> MongoDB 를 사용하게 되면 

  - JavaScript로 모두 개발을 한다. 클라이언트/서버 분리하여 개발할 필요가 없다. 그냥 FullStack JavaScript개발을 하면 된다

  - 클라이언트 개발이 MV* 개발이 되어 서버 개발에 익숙한 개발자가 쉽게 접근할 수 있다 

  - 서버에서 Push 기능을 쉽게 적용할 수 있으므로 다양한 서비스의 응용이 가능하다 

  - 자바의 서블릿 엔진 구현체인 Tomcat과 같이 충실한 RESTful I/O 머신을 쉽게 구현할 수 있다

  - 서버의 멀티 쓰레드 문제에서 해방될 수 있다. Asynch의 세상에서 놀자 

  - 데이터 스키마를 미리 정의하지 않고 에자일하게 개발을 진행할 수 있다

  - 큰 규모의 데이터를 저장하고 처리 할 수 있다  



그러나 우리에게 남은 과제는 이것들을 어떻게 배우고 사용하는지 잘 모른다는 것이다. 각자를 심도 있게 배우는 것은 별도의 가이드북을 따로 보길 바란다. 대신 여기서는 실전 서비스를 어떻게 하면 빠르게 만들 수 있을지 노하우를 적어보려한다. 낮에는 회사에서 업무를 보면서 향후 스타트업을 준비하는 개발자 또는 사람들에게 좀 더 이로운 가치를 전달해 주고 싶은 개발자가 3개월안에 개발할 수 있는 서비스를 이 책을 통해 런칭할 수 있는 것이 가능해 질 수 있길 바란다. 그래서 책을 쓰면서 동시에 작은 서비스를 만들어 보려 한다. 책은 가이드이고 그물일 뿐이다. 실제 돌아가는 서비스가 물고기이다. 즉, 실제 서비스를 낚지 못하면 이 책의 가치도 없다고 생각한다. 실제로 이것을 증명해 보이고 싶다. 



Getting Start MEAN

          


posted by Peter Note
2013. 11. 20. 09:11 My Projects/BI Dashboard

이전 블로그에서 myBatis를 사용하면서 DAO Interface를 만들고, DAO를 구현한 클래스를 사용하는 방법을 테스트해 보았다. DAO를 구현하지 않고 Interface의 메소드만 선언하면 myBatis에서 자동 구현되어 사용할 수 있게 하는 방법을 알아보자 




1. Mapper 인식하는 방식

  - Spring context xml에 설정하기 : UserMapper는 단순 Interface

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">

  <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" />

  <property name="sqlSessionFactory" ref="sqlSessionFactory" />

</bean>

  - Mapper scanner를 등록하는 방식 : component-scan과 유사하게 mapper를 검색해 준다 (안됨)

<beans xmlns="http://www.springframework.org/schema/beans"

  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"

  xsi:schemaLocation="

  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

  http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

 

  <mybatis:scan base-package="org.mybatis.spring.sample.mapper" />


</beans>

  - @MapperScan 애노테이션 사용 : java로 환경설정

@Configuration

@MapperScan("org.mybatis.spring.sample.mapper")

public class AppConfig {


  @Bean

  public DataSource dataSource() {

    return new EmbeddedDatabaseBuilder().addScript("schema.sql").build()

  }


  @Bean

  public SqlSessionFactory sqlSessionFactory() throws Exception {

    SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();

    sessionFactory.setDataSource(dataSource());

    return sessionFactory.getObject();

  }

}

  - MapperScannerConfigurer 사용 : 본 셋팅에는 해당 설정을 사용한다 

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">

  <property name="basePackage" value="com.mobiconsoft.dashboard.mapper" />

</bean>



2. pom.xml의 myBatis 버전 업그레이드 

  - mybatis-spring.jar 에서 1.2 이상의 최신 버전을 사용토록 한다 

<mybatis.spring.version>1.2.1</mybatis.spring.version>

<mysql.connector.version>5.1.27</mysql.connector.version>


<dependency>

<groupId>org.mybatis</groupId>

<artifactId>mybatis</artifactId>

<version>${mybatis.version}</version>

</dependency>

<dependency>

<groupId>org.mybatis</groupId>

<artifactId>mybatis-spring</artifactId>

<version>${mybatis.spring.version}</version>

</dependency>



3. myBatis Mapper 환경 설정

  - 위치를 webapp 폴더 밑으로 옮긴다 

    

  - dbpool-context.xml의 환경설정 변경 : id 명칭은 내부적으로 동일 이름을 사용하니 변경하지 말자 

<!-- mybatis sql session template -->

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

        <property name="dataSource" ref="dataSource" />

<property name="mapperLocations" value="classpath:mappers/*Mapper.xml" />

<property name="configLocation" value="/WEB-INF/mybatis-config.xml" />

</bean>


<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">

<constructor-arg ref="sqlSessionFactory" />

</bean>


<!-- mybatis mapper auto scanning -->

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">

<property name="basePackage" value="com.mobiconsoft.dashboard.mapper" />

</bean>

  - mybatis-config.xml 내역 수정

<configuration>

    <settings>

        <setting name="cacheEnabled" value="false" />

        <setting name="useGeneratedKeys" value="true" />

        <setting name="defaultExecutorType" value="REUSE" />

    </settings>

</configuration>

  - Person.xml 맵퍼 xml 파일명칭을 PersonMapper.xml 로 변경하고 WEB-INF/classes/mappers/ 폴더 밑으로 이동한다 

    + namespace는 반드시 PersonMapper interface 파일이 있는 위치를 명시한다 

    + resultType, parameterType 시에 full package + class 명칭을 명시한다 


<mapper namespace="com.mobiconsoft.dashboard.mapper.PersonMapper">

 

    <resultMap id="person" type="com.mobiconsoft.dashboard.domain.Person" >

        <result property="id" column="id"/>

        <result property="name" column="name"/>

    </resultMap>

 

    <select id="getPersons" resultType="com.mobiconsoft.dashboard.domain.Person">

        SELECT

        *

        FROM

        person

    </select>

 

    <select id="getPerson" parameterType="Integer" resultType="com.mobiconsoft.dashboard.domain.Person">

        SELECT

        *

        FROM

        person

        WHERE

        id=#{id}

    </select>

 

    <insert id="savePerson" parameterType="com.mobiconsoft.dashboard.domain.Person"

        INSERT INTO

        person(id, name)

        VALUES

        (#{id}, #{name})

    </insert>

    

    <update id="updatePerson" parameterType="com.mobiconsoft.dashboard.domain.Person"> 

        UPDATE 

        person 

        SET

        name=#{name}

        WHERE

        id=#{id} 

    </update>

 

    <delete id="deletePerson" parameterType="Integer">

        DELETE FROM

        person

        WHERE

        id=#{id}

    </delete>

 

</mapper>

  - 이제 PersonDAO.java interface를 PersonMapper.java로 바꾸어 보자 

    + PersonMapper.xml 에서 정의한 parameterType은 메소드의 파라미터 타입와 일치해야 한다

    + PersonMapper.xml 에서 정의한 resultType은 메소드의 리턴 타입과 일치해야 한다 

@Repository(value="personMapper")

public interface PersonMapper {

public List<Person> getPersons();

public Person getPerson(int id);

public void savePerson(Person person);

public void updatePerson(Person person);

public void deletePerson(int id);

}

  - grunt build 할 때 WEB-INF/ 밑의 모든 폴더/파일 dist 폴더에 copy하는 옵션 변경

copy: {

      dist: {

        files: [{

          expand: true,

          dot: true,

          cwd: '<%= yeoman.app %>',

          dest: '<%= yeoman.dist %>',

          src: [

            '*.{ico,png,txt,html}',

            '.htaccess',

            'bower_components/**/*',

            'images/{,*/}*.{gif,webp}',

            'styles/fonts/*',

            'WEB-INF/**/*'

          ]


  - PersonService Interface 제거하고 구현체를 PersonService로 변경하였음. 최종 소스 디렉토리 

    + controller : RESTful 요청 처리 

    + service : 실제 업무 트랜잭션 @Transactional 처리

    + mapper : myBatis mapper interface 

    + domain : 비즈니스 객체. 주로 테이블과 1:1 맵핑되는 Model

    

  - 실행

    

* 저장소 : https://github.com/ysyun/SPA_Angular_SpringFramework_Eclipse_ENV/tree/feature_mybatis_mapper



<참조>

  - myBatis Mapper 설정방법

posted by Peter Note
2013. 11. 20. 01:33 My Projects/BI Dashboard

데이터 저장소를 MySQL을 사용하며 CRUD를 위하여 MyBatis를 사용한다. Spring 프레임워크에서 사용하기 위한 설정과 사용법을 알아보자 



1. JDBC Pool 설정

  - tomcat 7을 사용하므로 tomcat의 jdbc-pool을 사용한다 (참조)

  - maven pom.xml : 이미 $TOMCAT_HOME/lib 밑에 라이브러리가 존재할 경우 provided 설정

<!-- spring jdbc dependency spring-tx -->

<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-jdbc</artifactId>

<version>${spring.version}</version>

</dependency>


<!-- tomcat jdbc -->

<dependency>

<groupId>org.apache.tomcat</groupId>

<artifactId>tomcat-jdbc</artifactId>

<version>${tomcat.jdbc.version}</version>

<!--scope>provided</scope-->

</dependency>

  - spring context 와 properties 설정하기 

// web.xml 에서 dbpool-context.xml을 인식하기 위하여 설정변경

    <!-- root-context.xml, mybatis-context.xml -->

    <context-param>

        <param-name>contextConfigLocation</param-name>

        <param-value>

            /WEB-INF/*-context.xml

        </param-value>

    </context-param>


// dbpool-context.xml 에서 properties 에서 환경값을 읽어서 설정한다 

// common-jdbc, c3po보다 tomcat의 jdbc-pool 성능이 더 좋다 

<bean

class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

<property name="locations">

<value>/WEB-INF/dbpool.properties</value>

</property>

</bean>


<bean id="dataSource" class="org.apache.tomcat.jdbc.pool.DataSource"

destroy-method="close">

<property name="driverClassName" value="${jdbc.driverClass}" />

<property name="url" value="${jdbc.url}" />

<property name="username" value="${jdbc.username}" />

<property name="password" value="${jdbc.password}" />

<property name="initialSize" value="${jdbc.min.size}" />

<property name="maxActive" value="${jdbc.max.size}" />

<property name="maxIdle" value="5" />

<property name="minIdle" value="2" />

</bean>


// dbpool.properties 내역 

jdbc.driverClass=com.mysql.jdbc.Driver

jdbc.url=jdbc:mysql://localhost:3306/SolarDB?autoReconnect=true

jdbc.username=admin

jdbc.password=admin

jdbc.min.size=2

jdbc.max.size=10



2. Transaction 설정

  - @Transactional 애노테이션 기반으로 트랜잭션을 관리하고자 할 경우 설정

  - DAO 보다는 가급적 DAO를 호출하는 Service Interface의 Insert, Update, Delete 메소드 선언할 때 @Transactional 설정 (참조)

// dbpool-context.xml 에서 dataSource를 TransactionManager에 할당한다 

<!-- transaction manager -->

<context:annotation-config/>

<tx:annotation-driven transaction-manager="transactionManager" />

<bean id="transactionManager"

class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

<property name="dataSource" ref="dataSource"></property>

</bean>

  - @Transactional 사용시 Propagation 동작 방식에 대해서 숙지하자 (참조)

  - Spring과 그외 관련 설정 파일을 WEB-INF/spring 폴더로 위치 변경함

    



3. myBatis 설정

  - DataSource와 Transaction에 대한 설정이 끝났다면 myBatis를 설정한다 

  - MySQL을 사용할 것이다

  - myBatis의 Resource인 config와 mapper xml 파일은 src/main/resource로 이동하자 (Person.xml, mybatis-config.xml)

    


// dbpool-context.xml 에서 dataSource를 설정한다

// SqlSessionTemplate 을 DAO에서 사용할 것이다 

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

<property name="dataSource" ref="dataSource" />

<property name="configLocation" value="classpath:mybatis-config.xml" />

</bean>

<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">

<constructor-arg ref="sqlSessionFactory" />

</bean>



// mybatis-config.xml 에서 mapper.xml의 위치를 설정 

// Person.xml 과 같은 mapper xml을 여러개 열거 할 수 있다 

<configuration>

    <settings>

        <setting name="cacheEnabled" value="false" />

<setting name="useGeneratedKeys" value="true" />

<setting name="defaultExecutorType" value="REUSE" />

    </settings>

    

    <typeAliases>

        <typeAlias alias="Person" type="com.mobiconsoft.dashboard.domain.Person"></typeAlias>

    </typeAliases>

    

    <mappers>

        <mapper resource="com/mobiconsoft/dashboard/mybatis/mapper/Person.xml"></mapper>

    </mappers>

</configuration>

  - Mysql을 설치 및 Person Table을 하나 만들고 Mapper xml을 설정한다. 여기서는 예제로 Person.xml 파일 하나만 설정함 

// mysql 에서 테이블 생성하기 (참조)

mysql> create table person(

    -> id INT,

    -> name VARCHAR(30));


// Person.xml 맵핑 xml 내역 

// 호출 방법 ID : <namespace>.<id> ex) com.mobiconsoft.dashboard.person

<mapper namespace="com.mobiconsoft.dashboard.Person">

 

    <resultMap type="Person" id="PersonResult">

        <id property="id" column="id"/>

        <result property="name" column="name"/>

    </resultMap>

 

    <select id="getPersons" resultMap="PersonResult">

        SELECT

        *

        FROM

        person

    </select>

 

    <select id="getPerson" parameterType="Integer" resultMap="PersonResult">

        SELECT

        *

        FROM

        person

        WHERE

        id=#{id}

    </select>

 

    <insert id="savePerson" parameterType="Person"> <!-- useGeneratedKeys="true" keyProperty="id"> -->

        INSERT INTO

        person(id, name)

        VALUES

        (#{id}, #{name})

    </insert>

    

    <update id="updatePerson" parameterType="Person"

        UPDATE 

        person 

        SET

        name=#{name}

        WHERE

        id=#{id} 

    </update>

 

    <delete id="deletePerson" parameterType="Integer">

        DELETE FROM

        person

        WHERE

        id=#{id}

    </delete>

</mapper>

  - DAO와 DAO를 구현한다. 

// DAO Interface 

public interface PersonDAO {

  public List<Person> getPersons();

  public Person getPerson(int id);

  public Person savePerson(Person person);

  public Person updatePerson(Person person);

  public void deletePerson(int id);

}


// DAO Implementation

@Repository

public class PersonDAOImpl implements PersonDAO {

@Autowired

private SqlSession sqlSession;

private static final Logger logger = LoggerFactory.getLogger(PersonDAOImpl.class);

// end must be point . 

private static final String NS = "com.mobiconsoft.dashboard.Person.";


@Override

public List<Person> getPersons() {

logger.info("dao select all");

return sqlSession.selectList(NS+"getPersons");

}


@Override

public Person getPerson(int id) {

logger.info("dao select one id is " + id);

return sqlSession.selectOne(NS+"getPerson");

}


@Override

public Person savePerson(Person person) {

logger.info("dao save person is " + person);

sqlSession.insert(NS+"savePerson", person);

return person;

}

@Override

public Person updatePerson(Person person) {

logger.info("dao update person is " + person);

sqlSession.update(NS+"updatePerson", person);

return person;

}


@Override

public void deletePerson(int id) {

logger.info("dao delete id is " + id);

sqlSession.delete(NS+"deletePerson", id);

}

}

  - 기존 PersonServiceImpl의 내역을 변경한다 : @Transactional을 통하여 트랜잭션을 Service 레벨에서 관리한다 

@Service

public class PersonServiceImpl implements PersonService {


@Autowired

PersonDAO personDAO;

@Override

public List<Person> getPersons() {

return personDAO.getPersons();

}


@Override

public Person getById(Integer id) {

return personDAO.getPerson(id);

}


@Override

@Transactional

public Person save(Person person) {

personDAO.savePerson(person);

return person;

}

@Override

@Transactional

public Person update(Person person) {

personDAO.updatePerson(person);

return person;

}

@Override

@Transactional

public void delete(Integer id) {

personDAO.deletePerson(id);

}

}


* 저장소 : https://github.com/ysyun/SPA_Angular_SpringFramework_Eclipse_ENV/tree/feature_mybatis



<참조>

  - Spring, myBatis 설정 방법

  - Tomcat 7일 경우 Spring에서 Tomcat JDBC-Pool 사용하기

  - Common-JDBC와 C3P0와 Tomcat JDBC-Pool 성능차이

  - Transaction Manager 선언적으로 사용하기

  - Transaction Propagation 동작 방식

  - Mac에서 기존 MySQL 삭제하기 

posted by Peter Note
2013. 11. 16. 05:46 My Projects/BI Dashboard

Spring MVC의 RESTful 방식을 사용하면서 AngularJS를 Eclipse에서 개발하기 위한 제반 설정 환경을 만들어 보도록 한다. 핵심은 Java를 위한 라이브러리 의존성 관리와 빌드를 위하여 Maven을 사용하고 JavaScript는 의존성 관리는 bower를 사용하고 빌드도구는 grunt를 사용하게 된다. 따로 따로 사용하지 않고 하나의 프로젝트로 합치는 과정을 생각해 보자 



1. Maven을 이용한 Spring MVC 환경 만들기 

  - 이미 Eclipse Juno 기반에서 Maven + Spring MVC 환경 설정 블로깅을 하였다

  - pom.xml 에 logback 관련 내용 추가 (참조)

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>com.ysyun</groupId>

<artifactId>DashboardTest</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>war</packaging>

<url>http://maven.apache.org</url>


<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<spring.version>3.2.0.RELEASE</spring.version>

<junit.version>4.11</junit.version>

<jdk.version>1.6</jdk.version>

</properties>


<dependencies>

<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-core</artifactId>

<version>${spring.version}</version>

<exclusions>

<exclusion>

<artifactId>commons-logging</artifactId>

<groupId>commons-logging</groupId>

</exclusion>

</exclusions>

</dependency>


<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-web</artifactId>

<version>${spring.version}</version>

</dependency>


<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-webmvc</artifactId>

<version>${spring.version}</version>

<exclusions>

<exclusion>

<groupId>commons-logging</groupId>

<artifactId>commons-logging</artifactId>

</exclusion>

</exclusions>

</dependency>


<!-- use logback logger -->

<dependency>

<groupId>ch.qos.logback</groupId>

<artifactId>logback-access</artifactId>

<version>1.0.13</version>

</dependency>

<dependency>

<groupId>ch.qos.logback</groupId>

<artifactId>logback-core</artifactId>

<version>1.0.13</version>

</dependency>

<dependency>

<groupId>ch.qos.logback</groupId>

<artifactId>logback-classic</artifactId>

<version>1.0.13</version>

</dependency>

<dependency>

<groupId>org.slf4j</groupId>

<artifactId>slf4j-api</artifactId>

<version>1.7.5</version>

</dependency>

<dependency>

<groupId>org.slf4j</groupId>

<artifactId>jcl-over-slf4j</artifactId>

<version>1.7.5</version>

</dependency>

<dependency>

<groupId>org.slf4j</groupId>

<artifactId>log4j-over-slf4j</artifactId>

<version>1.7.5</version>

</dependency>

<dependency>

<groupId>org.slf4j</groupId>

<artifactId>slf4j-jcl</artifactId>

<version>1.7.5</version>

</dependency>


<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>${junit.version}</version>

<scope>test</scope>

</dependency>

</dependencies>


<build>

<finalName>DashboardTest</finalName>

<plugins>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-compiler-plugin</artifactId>

<version>3.0</version>

<configuration>

<source>${jdk.version}</source>

<target>${jdk.version}</target>

</configuration>

</plugin>

</plugins>

</build>

</project>

  - 다음에 logback.xml 환경파일을 resource에 놓아 classpath에 잡아준다. 개발/배포 따로 구성한다 

<?xml version="1.0" encoding="UTF-8"?>

<configuration scan="true">

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">

        <encoder>

            <charset>utf-8</charset>

            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>

        </encoder>

    </appender>

    <logger name="ch.qos.logback" level="WARN"/>

    <logger name="org.apache" level="WARN"/>

    <logger name="org.springframework" level="WARN"/>

    <logger name="org.springframework.web" level="WARN"/>

    <logger name="org.springframework.security" level="WARN"/>

    <logger name="org.springframework.cache" level="WARN"/>


    <root level="DEBUG">

        <appender-ref ref="CONSOLE"/>

    </root>

</configuration>




2. SPA 환경 잡아주기 

  - Yeoman을 통하여 angular 프로젝트 만들기 (참조)

$ yo angular SolarDasbhoard


  - 위의 파일을 각각 다음과 같이 이동한다 

    + app/ 폴더에 있는 것을 src/main/webapp로 복사한다 

    + node_modules 및 test 폴더 그리고 나머지 모든 파일을 eclipse 프로젝트 root로 복사한다 

      여기서 node_modules안의 모듈은 grunt와 karma 테스트 모듈들로 운영에 배포될 필요가 없다. 

      운영 배포는 webapp/ 밑에 있는 파일들이고, 사용하는 모듈은 src/main/webapp/bower_components/ 밑에 위치한다 

      


  - app폴더 밑의 내용이 src/main/webapp로 이동하였으므로 관련 환경설정 내역을 수정한다 

// bower 설정파일인 .bowerrc  변경 

{

    "directory": "src/main/webapp/bower_components"

}


// karm.conf.js 파일 내역 수정

    files: [

      'src/main/webapp/bower_components/angular/angular.js',

      'src/main/webapp/bower_components/angular-mocks/angular-mocks.js',

      'src/main/webapp/bower_components/angular-resource/angular-resource.js',

      'src/main/webapp/bower_components/angular-cookies/angular-cookies.js',

      'src/main/webapp/bower_components/angular-sanitize/angular-sanitize.js',

      'src/main/webapp/scripts/*.js',

      'src/main/webapp/scripts/**/*.js',

      'test/mock/**/*.js',

      'test/spec/**/*.js'

    ],


// grunt build를 위한 Gruntfile.js 내역 수정 

// 소스의 위치와 grunt build시에 dist 폴더에 .html과 WEB-INF/* 파일까지도 copy 하도록 수정 

 yeoman: {

      // configurable paths

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

      dist: 'dist'

    },

... 중략 ...

 copy: {

      dist: {

        files: [{

          expand: true,

          dot: true,

          cwd: '<%= yeoman.app %>',

          dest: '<%= yeoman.dist %>',

          src: [

            '*.{ico,png,txt,html}',

            '.htaccess',

            'bower_components/**/*',

            'images/{,*/}*.{gif,webp}',

            'styles/fonts/*',

            'WEB-INF/*'

          ]

        }, {

          expand: true,

          cwd: '.tmp/images',

          dest: '<%= yeoman.dist %>/images',

          src: [

            'generated/*'

          ]

        }]

      },

      styles: {

        expand: true,

        cwd: '<%= yeoman.app %>/styles',

        dest: '.tmp/styles/',

        src: '{,*/}*.css'

      }

    },



3. .war 배포파일 만들기 

  - Maven(java 서버)와 Yo(javascript 클라이언트) 환경설정을 완료하였다면 빌드했을 때 최종파일을 .war파일로 묶어보자 

  - grunt build를 했을 경우 프로젝트의 root/dist 디렉토리가 신규 생성되어 src/main/webapp/* 파일이 minification 되어 들어있다

  - 해당 파일을 .war안에 묶기 위하여 pom.xml 에 플러그인 설정 추가

<build>

<finalName>DashboardTest</finalName>

<plugins>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-compiler-plugin</artifactId>

<version>3.0</version>

<configuration>

<source>${jdk.version}</source>

<target>${jdk.version}</target>

</configuration>

</plugin>

<!-- grunt build 수행하여서 생성되는 폴더인 dist 를 지정한다 -->

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-war-plugin</artifactId>

<version>2.4</version>

<configuration>

<warSourceDirectory>dist</warSourceDirectory>

</configuration>

</plugin>

</plugins>

</build>


  - 배포파일 war 만들기 순서

$ grunt build (if error ocurred, option --force)

$ mvn clean install

[INFO] Scanning for projects...

[INFO] ------------------------------------------------------------------------

[INFO] Building DashboardTest 0.0.1-SNAPSHOT

[INFO] ------------------------------------------------------------------------

.. 중략 ..

[INFO] ------------------------------------------------------------------------

[INFO] BUILD SUCCESS

[INFO] ------------------------------------------------------------------------

[INFO] Total time: 5.109s

[INFO] Finished at: Fri Nov 15 15:43:09 EST 2013

[INFO] Final Memory: 12M/81M

[INFO] ------------------------------------------------------------------------

  * eclipse 의 "Run As..." 의 "5 Maven Build"를 해도 같은 결과를 받음 


이제 eclipse 상에서 Spring Framework + AngularJS Framework 으로 개발을 해보자 

GitHub URL : https://github.com/ysyun/SPA_Angular_SpringFramework_Eclipse_ENV



<참조>

  - SLF4J를 통하여 logback 사용하기

  - Yeoman을 통한 AngularJS + Express 환경구성하기

  - Maven 기본 디렉토리 layout, pom.xml 에서 변경하는 방법

posted by Peter Note
2013. 11. 15. 04:42 My Projects/BI Dashboard

자바기반 서버 환경 구축을 위한 기반 기술 설정에 대하여 알아보자. 우선 빌드환경인 Maven과 Spring Framework을 설치하고 간단히 RESTful 호출 테스트를 해보자 



1. Eclipse 플러그인 설치 

  - Eclipse Juno를 다운로드하여 설치 및 JDK1.6 사용

  - Sublime text 사용에 익숙하다면 http://eclipsecolorthemes.org/ 플러그인을 설치후 에서 Sublime Theme으로 바꾸어 사용한다 

  - Eclipse Marketplace에서 

    + STS (Spring Tool Suite - juno 4.2)를 설치

    + Maven (Maven Integration for Eclipse - juno and newer)를 설치

    + Git 사용한다면 EGit - Git Team Provider를 설치

    + SVN 사용한다면 Subversive - SVN Team Provider 설치 (참조1, 참조2)

       > "Help/Install New Software..."에서 

       > SVN Connector - http://community.polarion.com/projects/subversive/download/eclipse/3.0/juno-site/ 통해 설치

          Mac의 HL 부분은 unselect 

       > SVN Kit 1.7.8 선택한다. 

      


  

2. Spring MVC 프로젝트 구성

  - Maven으로 Simple project를 만든다. jar 배포 프로젝트로 만듦

  - 프로퍼티에서 Project facets에서 "Dynamics Web Module"를 선택하고 "Further configuration available..."에서 webapp 설정

  - webapp를 설정하였으면 src/main 밑으로 강제 이동시킴 

    

  - 프로퍼티에서 Deployment Assembly에서 add... 하여 Maven Denpendencies... 라이브러리가 WEB-INF/lib에 포함되도록 설정

    (참조)

    


  - pom.xml 에서 <packaging>jar</packaging>에서  <packaging>war</packaging>로 바꾼다 

  - 하기 의존성 라이브러리를 추가한다 

    + Spring Framework : v3.2.0

    + JDK : v1.6

    + JUnit : v4.11

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>com.ysyun</groupId>

<artifactId>DashboardTest</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>war</packaging>


<properties>

<spring.version>3.2.0.RELEASE</spring.version>

<junit.version>4.11</junit.version>

<jdk.version>1.6</jdk.version>

</properties>


<dependencies>


<!-- Spring 3 dependencies -->

<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-core</artifactId>

<version>${spring.version}</version>

</dependency>


<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-web</artifactId>

<version>${spring.version}</version>

</dependency>


<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-webmvc</artifactId>

<version>${spring.version}</version>

</dependency>


<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>${junit.version}</version>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<finalName>DashboardTest</finalName>

<plugins>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-compiler-plugin</artifactId>

<version>3.0</version>

<configuration>

<source>${jdk.version}</source>

<target>${jdk.version}</target>

</configuration>

</plugin>

</plugins>

</build>

</project>



3. RESTful 테스트 하기 

  - Controller 추가하기 : RESTful uri 설정

package com.lge.dashboard;


import org.springframework.stereotype.Controller;

import org.springframework.ui.ModelMap;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;


@Controller

@RequestMapping("/")

public class HelloController {

 

@RequestMapping(value="/welcome", method = RequestMethod.GET)

public String welcome(ModelMap model) {

 

model.addAttribute("message", "Maven Web Project + Spring 3 MVC - welcome()");

 

//Spring uses InternalResourceViewResolver and return back index.jsp

return "index";

}

 

@RequestMapping(value="/welcome/{name}", method = RequestMethod.GET)

public String welcomeName(@PathVariable String name, ModelMap model) {

 

model.addAttribute("message", "Maven Web Project + Spring 3 MVC - " + name);

return "index";

 

}

 

}

  - web.xml 설정 : spring dispatcher를 설정한다 

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"

id="WebApp_ID" version="3.0">

<display-name>Counter Web Application</display-name>


<servlet>

<servlet-name>mvc-dispatcher</servlet-name>

<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>


<servlet-mapping>

<servlet-name>mvc-dispatcher</servlet-name>

<url-pattern>/</url-pattern>

</servlet-mapping>


<context-param>

<param-name>contextConfigLocation</param-name>

<param-value>/WEB-INF/mvc-dispatcher-servlet.xml</param-value>

</context-param>


<listener>

<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>

</listener>

</web-app>

  - mvc-dispathcer-servlet.xml 스프링 환경설정 

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:context="http://www.springframework.org/schema/context"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="

        http://www.springframework.org/schema/beans     

        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

        http://www.springframework.org/schema/context 

        http://www.springframework.org/schema/context/spring-context-3.0.xsd">

 

<context:component-scan base-package="com.lge.*" />

 

<bean

class="org.springframework.web.servlet.view.InternalResourceViewResolver">

<property name="prefix">

<value>/WEB-INF/pages/</value>

</property>

<property name="suffix">

<value>.jsp</value>

</property>

</bean>

 

</beans>

  - WEB-INF/pages폴더를 만들고 index.jsp 파일 생성 

<html>

<body>

<h2>Hello World!</h2>

 

<h4>Message : ${message}</h1>

</body>

</html>

  - 전체 설정 정보 

    

  - Tomcat v7.0 설정이 되었다면 Run As 에서 "Run on Server"로 DashboardTest 컨텍스트 추가하여 Tomcat 기동

    /<contextName>/welcome/<value> 넣어서 호출하면 하기와 같이 나온다 

    

  - maven Goal에 대한 명령을 수행할 경우 

Goal은 ant의 target과 같은 개념으로, mvn의 경우 pre-defined goal을 가지고 있다.

mvn compile : 컴파일 수행

mvn package : jar 파일로 패키징

mvn test : JUnit 테스트 수행

mvn install : local repository (PC내의 디렉토리)에 해당 jar 파일을 저장 

mvn deploy  : remote repository (Nexus)에 jar 파일 저장

mvn clean : 컴파일 내용 모두 삭제

mvn build : 위의 예제는 일반적은 spring-java 코드로 build goal을 제공하지 않는다. 

(Eclipse에서 run as하면 나와서 헷갈리게 만드는데) build goal을 지원하는 프로젝트를 만들거나 또는 pom.xml안에 인위적으로 build goal을 지정해줘야 한다. 보통 clean install 등 여러 명령을 스페이스로 구분하여 파라미터를 줄 수 있다 

  - 만들어진 pom.xml 파일 

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>com.ysyun</groupId>

<artifactId>DashboardTest</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>war</packaging>

<url>http://maven.apache.org</url>


<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<spring.version>3.2.0.RELEASE</spring.version>

<junit.version>4.11</junit.version>

<jdk.version>1.6</jdk.version>

</properties>


<dependencies>

<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-core</artifactId>

<version>${spring.version}</version>

</dependency>


<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-web</artifactId>

<version>${spring.version}</version>

</dependency>


<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-webmvc</artifactId>

<version>${spring.version}</version>

</dependency>


<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>${junit.version}</version>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<finalName>DashboardTest</finalName>

<plugins>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-compiler-plugin</artifactId>

<version>3.0</version>

<configuration>

<source>${jdk.version}</source>

<target>${jdk.version}</target>

</configuration>

</plugin>

</plugins>

</build>

</project>



4. Spring 에 대한 Enterprise 환경 설정 원칙 

  - Enterprise Spring Best Practices

  - Bean Post Processors를 등록하는 어노테이션

RequiredAnnotationBeanPostProcessor – (@Required)

CommonAnnotationBeanPostProcessor – (@PostConstruct, @PreDestroy, @Resource, etc.)

AutowiredAnnotationBeanPostProcessor – (@Autowired, @Value, @Inject, etc.)

PersistenceAnnotationBeanPostProcessor – (@PersistenceUnit, @PersistenceContext, etc.)

  - annotation-config 지시자 설정

<context:annotation-config/>

  - 애플리케이션 컴포넌트 scanning 지시자 설정

<context:component-scan base-package="com.gordondickens.enterprisespring" use-default-filters="false">

<context:exclude-filter 

   expression="org.springframework.stereotype.Controller"

   type="annotation"/>

</context:component-scan>

  - 스캐닝 원칙 

+ Component scanning should ONLY be configured in the bootstrap config file, not in every XML config file

+ Do NOT also include the <context:annotation-config/> directive, it is automatically included by component scan

+ Do NOT start scanning from “com” and/or “org”, as this will scan ALL sub packages in all of the project and jars for candidates!

+ Be as specific as possible with the packages

+ Do NOT cross application boundaries with component-scan

  + Create an applicationContext-services.xml for scanning services

  + Create an applicationContext-persistence.xml for persistence and entity beans

  + Create an applicationContext-webmvc.xml for persistence and entity beans

  + Create an applicationContext-webservice.xml for web service beans

  + Import these references into the applicationContext-bootstrap.xml to these elements


Why separate the discovery files into layer specific configuration?

  + Unit testing is easier as discovery of beans is more specific

  + Allows the project to be separated into multiple Jar/Wars

  + Lowers risk of widely scoped discovery issues, overscanning and beans being replaced by multiple scanners



<참조>

  - Eclipse Theme 사용방법

  - STS + myBatis + RESTful 설정 및 테스트

  - Eclipse Juno기반 Spring MVC with maven으로 만드는 과정

  - Maven Console 기반 webapp RESTful 서비스 만드는 과정

  - AngularJS + SpringMVC 샘플 프로젝트

  - Eclipse에서 Maven 개발환경 설정

  - Spring에서 Enterprise 환경 설정 원칙

posted by Peter Note
2013. 11. 12. 13:08 HTML5, CSS3/jQuery

jQuery UI 스타일로 작성된 내역을 AngularJS 스타일의 개발방식으로 변형하는 방법을 알아보자



1. AngularJS 길을 가다 

  - jQuery에서 UI 컴포넌트를 모아 놓은 것이 jQuery UI라면 AngularJS기반으로 UI 컴포넌트를 모아 놓은 것이 AngularJS UI 이다

    사실, Angular UI는 jQuery UI를 AngularJS를 가지고 확장한 것이다

  - jQuery UI를 통하여 DOM을 조작하여 화면을 보여주는 것과 AngularJS를 통하여 UI를 조작하는 방법은 틀리다

  - AngularJS Way를 먼저 살펴본다  

  - AngularJS를 사용하여 SoC(Separate Of Concern) 관심분리를 수행하여 테스트 및 모듈화가 가능해 진다 

    + Controller : $scope를 통한 view와 two way binding 

    + Service : view와 분리된 비즈니스 로직 구현

    + Directive : 재사용 가능 컴포넌트이며 model 값을 DOM 프러퍼티와 연결해주고, DOM 이벤트 반응하여 model 값을 업데이트한다  



2. jQuery UI Plugin 방식을 보자 

  - datepicker에 대한 사용을 본다 

<div>

    Date Of Birth:

    <input type="text" id="dateOfBirth">

    <br>

    Current user's date of birth:

    <span id="dateOfBirthDisplay"></span>

</div>

  - 자바스크립트 코드 : 비즈니스 로직과 DOM 조작이 합쳐져 있다. 날짜를 선택하면 해당 날짜의 값을 span에 표현함 


$(function () {

   var user = {

      dateOfBirth: new Date(1970, 0, 1)

   };


   var displayValue = function () {

      $("#dateOfBirthDisplay").text(new Date(user.dateOfBirth).toDateString());

   };


   var processChange = function() {

      user.dateOfBirth = $("#dateOfBirth").datepicker("getDate");

      displayValue();

   };


   $("#dateOfBirth").datepicker({

         inline: true,

         onClose: processChange,

         onSelect: processChange

      }

   );


   displayValue();


   // initial display of value in input-box

   $("#dateOfBirth").datepicker("setDate", user.dateOfBirth);


});



3. datepicker를 AngularJS 방식으로 SoC 분리해 보자

  - 위의 jQuery 방식을 angularjs 방식의 model, controller, directive로 분리해 본다 

  - AngularJS 접근법

   Step 1) Model을 먼저 생각하고 Service를 만든다

   Step 2) Model이 표현될 View에 대해 생각해 보고 template을 만들고, 필요하면 자동 모델 바인딩되는 Directive도 만든다.

   Step 3) 각 View 를 Controller에 연결한다 (ng-view and routing, ng-controller)

- Model : user.dateOfBirth

- Directive : input 태그를 my-datepicker 태그로 만든다

- Controller : $scope를 통해 model을 제어한다 

  - AngularJs 의 Step1), Step 2), Step 3) 수행

// Step 1, 3) model

function DemoController($scope) {

   $scope.user = {

      dateOfBirth: new Date(1970, 0, 1)

   }

}


// Step 2) directive in html 

<div ng-app="demo" ng-controller="DemoController">

    Date Of Birth:

    <my-datepicker type="text" ng-model="user.dateOfBirth" />

    <br>

    Current user's date of birth:

    <span id="dateOfBirthDisplay">{{user.dateOfBirth}}</span>

</div>

  


4. my-datepicker Directive 만들기 

  - AngularJS Directive 만들기에 따른 myDatepicker

    + compile 첫번째 phase : DOM 변경

    + compile 두번째 phase : DOM value 조작 

// demo 모듈에 directive 만들기 

angular.module("demo", []).directive('myDatepicker', function ($parse) {

   return {

      restrict: "E",

      replace: true,

      transclude: false,

      // compile은 최초 한번 호출되고, return 되는 펑션이 link 펑션이 된다

      compile: function (element, attrs) {

         // Phase 1 - Compile function

         // 엘러먼트의 어트리뷰트 express을 function으로 변환한다 

         // user.dateOfBirth에 대한 set, get이 가능토록 해준다 

         // attrs.ngModel === ng-model 과 같다 

         var modelAccessor = $parse(attrs.ngModel);


   // 엘러먼트 태그를 바꾸어 준다 

         var html = "<input type='text' id='" + attrs.id + "' ></input>";

         var newElem = $(html);

         element.replaceWith(newElem);


         // Phase 2 - Link function

         // - onClose, onSelect를 정의한다 

         // - 이벤트 발생시 콜백을 정의한다 

         // - 값이 변경되었을 때 Angular Context로 들어가서 watch가 불리고 최종적으로 값을 변경한다 

         return function (scope, element, attrs, controller) {


// jQuery 이벤트 콜백 펑션이 호출되면 $apply를 호출하여 Angular Context가 적용되어 $watch가 자동 호출토록 한다 

// compile, link, watch 이해하기  

            var processChange = function () {

               var date = new Date(element.datepicker("getDate"));


               // HTML element 값이 변경되었음을 $watch가 호출되도록 trigger 해주는 콜백펑션을 등록한다 

               scope.$apply(function (scope) {

                  // Change bound variable

                  modelAccessor.assign(scope, date);

               });

            };


// jQuery의 이벤트 콜백을 등록한다 

            element.datepicker({

               inline: true,

               onClose: processChange,

               onSelect: processChange

            });


// scope.$apply안엣 modelAccessor의 값이 변경되면 datepicker 의 setDate를 호출한다

            scope.$watch(modelAccessor, function (val) {

               var date = new Date(val);

               element.datepicker("setDate", date);

            });

         };

      }

   };

});



<참조>

  - 원문 : 존재하는 컴포넌트를 AngularJS의 Directives로 만들기

  - AngularJS Way

  - AngularJS Directive 만들기 

  - datepicker에서 $parse를 사용하게된 이유

posted by Peter Note
2013. 11. 12. 07:01 HTML5, CSS3/jQuery

jquery grid 플러그인 jqGrid를 사용하는 방법과 jquery-ui의 custom theme을 입히는 방법을 알아보자 



1. jqGrid 이해

  - jQuery Grid Plugin 이다 

  - Open Source, MIT License 이다. 즉, 상업적인 용도로 사용해도 된다 

  - jqGrid 설명을 보자



2. 테스트 환경 만들기 

  - Angular + Express 프레임워크를 Node.js 기반으로 테스트 할 것이다 (방법-3 Yeoman Generator FullStack 사용 통합)

// angular + express 프레임워크 기반의 코드를 만든다 

$ mkdir jqgrid && cd jqgrid

$ sudo npm install -g generator-angular-fullstack

$ yo angular-fullstack jqGridApp


// localhost:9000 포트의 크롬 브라우져가 자동으로 수행된다 

$ grunt server 


// 디렉토리 구조 

  - app : Angular.js SPA 폴더 

  - lib : Express.js 서버 폴더

  - public : app의 minification 배포 폴더 (grunt build시 자동 생성됨)

  - 기타 : grunt, bower, yo, karma 설정 파일들 



3. jqGrid 설정하기 

  - 먼저 jqGrid 를 다운로드 한다 (bower install jqgrid 로 설치할 수 있으나 이상하게 에러 발생하여 직접 다운로드 설치 사용함)

  - 다운로드한 파일은 app/bower_components/jqgrid 폴더로 풀어 놓는다 

    + css 는 jquery-ui의 ThemeRoller에서 선택한 css를 적용하면 jqGrid의 theme 이 변경된다 

    + jquery-migrate를 반드시 설정한다 $.browser.msie 체크 기능은 jquery 1.4*버전에만 존재하여 최신버전에 없어졌다

    + jquery-ui.js 를 설정하고 언어패키지와 jqGrid.js를 최종 설정하게 된다 

// app/index.html 내역 추가

// css 영역 파란색 추가  

        <!-- build:css(.tmp) styles/main.css -->

        <link rel="stylesheet" href="styles/bootstrap.css">

        <link rel="stylesheet" href="styles/main.css">

        <link rel="stylesheet" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css">

        <link rel="stylesheet" href="bower_components/jqgrid/css/ui.jqgrid.css">

        <!-- endbuild -->


// js 영역 파란색 추가 

        <script src="bower_components/jquery/jquery.js"></script>

        <script src="http://code.jquery.com/jquery-migrate-1.2.1.js"></script>


        <!-- The JQuery UI code -->

        <script src="http://code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>

        <script src="bower_components/jqgrid/js/i18n/grid.locale-en.js"></script>

        <script src="bower_components/jqgrid/js/jquery.jqGrid.min.js"></script>


  - app/view/main.html 수정

// 기존코드 주석처리 

<!--div class="hero-unit">

  <h1>'Allo, 'Allo!</h1>

  <p>You now have</p>

  <ul>

      <li ng-repeat="thing in awesomeThings">{{thing}}</li>

  </ul>

  <p>installed.</p>

  <h3>Enjoy coding! - Yeoman</h3>

</div-->


// jqGrid 표현 태그 추가 

<table id="Grid"></table>

<div id="GridPager"></div>

    

// jqGrid 자바스크립트 코드 추가 

<script>

  ... 중략 ...

</script>

  - jqGrid 자바스크립트 코드 내역 

// 데이터와 변수 설정 

var gidData = [

        { id: "1", orderdate: "2013-10-01", customer: "customer",  price: "200.00", vat: "10.00", completed: true, shipment: "TN", total: "210.00"},

        { id: "2", orderdate: "2013-10-01", customer: "customer2",  price: "300.00", vat: "20.00", completed: false, shipment: "FE", total: "320.00"},

        { id: "3", orderdate: "2011-07-30", customer: "customer3",  price: "400.00", vat: "30.00", completed: false, shipment: "FE", total: "430.00"},

        { id: "4", orderdate: "2013-10-04", customer: "customer4",  price: "200.00", vat: "10.00", completed: true, shipment: "TN", total: "210.00"},

        { id: "5", orderdate: "2013-11-31", customer: "customer5",  price: "300.00", vat: "20.00", completed: false, shipment: "FE", total: "320.00"},

        { id: "6", orderdate: "2013-09-06", customer: "customer6",  price: "400.00", vat: "30.00", completed: false, shipment: "FE", total: "430.00"},

        { id: "7", orderdate: "2011-08-30", customer: "customer7",  price: "200.00", vat: "10.00", completed: true, shipment: "TN", total: "210.00"},

        { id: "8", orderdate: "2013-10-03", customer: "customer8",  price: "300.00", vat: "20.00", completed: false, shipment: "FE", total: "320.00"},

        { id: "9", orderdate: "2013-09-01", customer: "customer9",  price: "400.00", vat: "30.00", completed: false, shipment: "TN", total: "430.00"},

        { id: "10", orderdate: "2013-09-08", customer: "customer10", price: "702.00", vat: "30.00", completed: true, shipment: "IN", total: "530.00"},

        { id: "11", orderdate: "2013-09-08", customer: "customer11",  price: "500.00", vat: "30.00", completed: false, shipment: "FE", total: "530.00"},

        { id: "12", orderdate: "2013-09-10", customer: "customer12",  price: "500.00", vat: "30.00", completed: false, shipment: "FE", total: "530.00"}

    ],

    theGrid = $("#Grid"),

    numberTemplate = {formatter: 'number', align: 'right', sorttype: 'number'};

 

// jqGrid 옵션 설정 

    theGrid.jqGrid({

        datatype: 'local',

        data: gidData,

        colNames: ['Customer', 'Date',  'Price', 'VAT', 'Total', 'Completed', 'Shipment'],

        colModel: [                  

            {name: 'customer', index: 'customer', width: 90, editable:true},

       {name: 'orderdate', index: 'orderdate', width: 100, align: 'center', sorttype: 'date',

           formatter: 'date', datefmt: 'd-M-Y'},

       {name: 'price', index: 'price', width: 55, template: numberTemplate},

       {name: 'vat', index: 'vat', width: 42, template: numberTemplate},

       {name: 'total', index: 'total', width: 50, template: numberTemplate},

       {name: 'completed', index: 'completed', width: 30, align: 'center', formatter: 'checkbox',

           edittype: 'checkbox', editoptions: {value: 'Yes:No', defaultValue: 'Yes'}},

       {name: 'shipment', index: 'shipment', width: 80, align: 'center', formatter: 'select',

          edittype: 'select', editoptions: {value: 'FE:FedEx;TN:TNT;IN:Intime;us:USPS', defaultValue: 'Intime'}}                  

        ],

        autowidth: true,

        gridview: true,             

        rownumbers: false,

        rowNum: 10,

        rowList: [5, 10, 15],

        pager: '#GridPager',

        sortname: 'Date',

        sortorder: 'asc',

        viewrecords: true,  

        caption: 'LG Solar Dashboard',

        height: '100%',

        width: 'auto',

        gridComplete :  function () {

                       var maxDate; 

                       var rowIDs = jQuery("#Grid").jqGrid('getDataIDs');

                       for (var i = 0; i < rowIDs.length ; i++) 

                       {

                           var rowID = rowIDs[i];

                           var row = jQuery('#Grid').jqGrid ('getRowData', rowID);


                           if(i==0)

                           {

                               maxDate = new Date(row.orderdate);

                           }

                           else

                           {

                               if(maxDate < new Date(row.orderdate))

                               {   

                                maxDate = new Date(row.orderdate);

                               }                                       

                           }       

                       }

                       $("#maxDateField").val(maxDate);

                        }

    });


// 수행

$ grunt server



<참조>

  - jqGrid Homepage

  - jquery-migrate 하기

posted by Peter Note
2013. 11. 12. 01:45 HTML5, CSS3/jQuery

jQueryUI는 jQuery를 기본으로 하면서 뷰를 표현하는 다양한 컴포넌트, 위젯을 모아놓은 User Interface를 모아 놓은 것이다. 어떻게 시작할 수 있는지 알아보자 




1. 사전 지식

  - 먼저 데모페이지에서 어떤 종류의 컴포넌트가 있는지 보자 

  - 여러 종류의 컴포넌트중 사용하길 원하는 부분만 Download Builder 를 통하여 다운로드한다



2. 파일 다운로드하기 

  - Download Builder에서 필요한 컴포넌트 -Core, Interactions, Widgets, Effects - 를 선택한다. 

  - 사용할 테마(Theme)를 선택한다. ThemeRoller 에서 사용자 정의도 가능하고 이미 만들어진 것을 선택할 수 있다

  - jQuery UI 버전 - 1.10.3/1.9.2 - 을 선택한다. jQuery 버전은 1.6 이상을 요구한다

  - 마지막으로 download 버튼을 클릭한다. 다운로드 파일 예) jquery-ui-1.10.3.custom.zip

// 압추을 풀면 폴더와 파일 존재한다. index.html 를 브라우져에서 확인해 볼 수 있다

/css

/development-bundle

/ js

index.html 

  - index.html 의 내용

// css를 추가

<link rel="stylesheet" href="css/themename/jquery-ui.custom.css" />

// jquery를 먼저 위치시킴

<script src="js/jquery.min.js"></script>

// jquery-ui를 위치시킴

<script src="js/jquery-ui.custom.min.js"></script>

  - smoothness theme을 선택했을 경우




3. 사용자 정의하기

  - JSON 형태로 옵션 객체를 전달한다. API를 확인하고 설정한다

$( "#mySliderDiv" ).slider({

    orientation: "vertical",

    min: 0,

    max: 150,

    value: 50

});

  - jQuery UI 개발에 참여하고 싶다면 Development Center로...



4. 동작방식 이해하기 

  - jQuery UI widgets은 공통 API를 제공하는 Widget Factory 기반으로 생성된다

  - Widget Life Cycle

    + Initialization : 플로그인을 펑션을 호출한다. 옵션이 없으면 default 옵션이 적용된다 

$( "#elem" ).progressbar();

또는

$( "#elem" ).progressbar({ value: 20 });

    + Methods : 최기화 된 후 상태를 조회하거나 액션을 수행할 수 있다. 메소드 이름 "value" 호출하여 40을 설정. 체이닝 호출 가능함.

$( "#elem" )

    .progressbar( "value", 90 )

    .addClass( "almost-done" );

    + Command Methods : option, disable, enable, destroy, widget 

$( "#elem" ).progressbar( "disable" );

$( "#elem" ).progressbar( "enable" );

$( "#elem" ).progressbar( "distroy" );

    + Events : 모든 동작에 대한 위젯별 이벤트 콜백 펑션을 설정한다. 공통 이벤트는 위젯 생성후 호출되는 create 이 존재함

// bind 호출 후 위젯명+이벤트명 연결하여 호출 

$( "#elem" ).bind( "progressbarchange", function() {

    alert( "The value has changed!" );

});


// 위젯 호출 후 change에 콜백 펑션 정의

$( "#elem" ).progressbar({

    change: function() {

        alert( "The value has changed!" );

    }

});

 


5. 테마 설정하기 

  - 기본적으로 jQuery UI CSS Framework - ui.theme.css - 와 plugin 별 CSS가 조합되어 만들어진다

    + ui.theme.css는 ThemeRoller 를 통하여 변경하는 경우

    + ui.theme.css 또는 widget plugin css를 직접 CSS를 변경하는 경우

    + 완전히 새로운 CSS를 만드는 경우 예) Twitter Bootstrap CSS를 jQuery UI에 적용 

    



6. CSS Framework API 이해 

  - ui.core.css 와 ui.theme.css 두개로 분리된다  

  - Layout Helpers : .ui-helper-*

  - Widget Containers : .ui-widget-*

  - Interaction States/Cues : .ui-state-*

  - Icon : .ui-icon 형식) .ui-icon-{icon type}-{icon sub description}-{direction}

  - Corner Radius : .ui-corner-*

  * jQuery UI Theme Base 깃헙 소스



7. 직접 Theme 만들기 

  - 디렉토리 구조 만들기 

// 1. themename 폴더 만들기 

/themename


// 2. 기본 css 파일 만들기

/themename/themename.css


// 3. 플러그인 css 파일 만들기 

/themename/themename.pluginname.css


// 4. 이미지 포함하기 

/themename/img.png

  - 스타일링 : .ui-themename 으로 한다면 하기와 같이 사용한다 

<html>

<head>

    <title>My Site</title>

    <link rel="stylesheet" href="themename/themename.css" />

    <link rel="stylesheet" href="othertheme/othertheme.css" />

    <link rel="stylesheet" href="othertheme/othertheme.dialog.css" />

</head>

<body class="ui-themename">

    <div class="ui-othertheme">

        <div class="ui-dialog">This is a modal dialog.</div>

    </div>

</body>

</html>



8. Widget Factory 사용하기 

  - 위젯 만드는 일관성을 보장하기 위함 

  - jQuery.widget 또는 $.widget 호출을 통하여 만든다

    + Context Object 이다. DOM은 아니다

    + Single Object로 생성된다 

// 1) namespace 는 한개의 depth만 허용하여 만들 수 있다. 여기서는 custom 이 namespace 이다 

$.widget( "custom.progressbar", {

 

   // 2) instance의 option내역 및 default 값 정의 

    options: {

        value: 0

    },

 

   // 3) 위젯 생성

    _create: function() {

        var progress = this.options.value + "%";

        // jQuery Object 

        this.element

            .addClass( "progressbar" )

            .text( progress );

    },

 

    // 4) 사용할 메소드 생성

    // Create a public method.

    value: function( value ) {

 

        // No value passed, act as a getter.

        if ( value === undefined ) {

            return this.options.value;

        }

 

        // Value passed, act as a setter.

        this.options.value = this._constrain( value );

        var progress = this.options.value + "%";

        this.element.text( progress );

    },

 

    // Create a private method. _ 즉 underscore를 하면 private 메소드가 된다 

    _constrain: function( value ) {

        if ( value > 100 ) {

            value = 100;

        }

        if ( value < 0 ) {

            value = 0;

        }

        return value;

    }

});

  - 사용하기 

var bar = $( "<div></div>" )

    .appendTo( "body" )

    .progressbar({ value: 20 });

 

// Get the current value.

alert( bar.progressbar( "value" ) );

 

// Update the value.

bar.progressbar( "value", 50 );

 

// Get the current value again.

alert( bar.progressbar( "value" ) );

  - 그외 Option 동작, Triggering CallBack Function, CleanUp with Destroy방법은 링크 참조한다



<참조>

  - jQuery UI Getting Start 

  - jQuery UI How to Work 

  - Theming jQuery UI

  - jQuery UI CSS Framework API

  - jQuery UI Theme Base 깃헙 소스

  - Twitter Bootstrap CSS를 jQuery UI에 적용

  - How to Use Widget Factory

posted by Peter Note
2013. 11. 9. 07:08 My Services/PlayHub

PlayHub는 다양한 협업도구를 묶어서 부족한 2%를 채워주기 위한 서비스이다. 단순 매쉬업을 넘어서 정보를 자체 저장하고 확장할 수 있는 기능을 가지고 있다. 일반 협업도구에 어떤 불편한 점을 개선하려는 것일까?



PlayHub Flow

  - 로그인 하기 

    + Facebook 과 Google+ 를 통하여 로그인을 한다. 

    + OAuth를 통하여 얻은 정보를 사용자 정보로 사용한다 

    



  - 최초 로그인을 했을 때의 빈 화면

    + 좌측의 메뉴가 최초에는 열려있게 만드는 것이 좋겠다. 최대한 클릭수를 줄이는 경험을 주어야 한다 (AirBnB 참조)

    


  - 상단 좌/우 메뉴 버튼

    + 메뉴는 Side Slider Menu이다 (Sidr 또는 jQueryMobile 참조)

    + 좌측 메뉴 : 페이스북과 유사하다. Team:Project = 1:1 기준이다. 1개팀은 1개프로젝트만 가능하다

    + 우측 메뉴 : 등록한 친구 목록과 친구들과 채팅한 내역들이 나온다 

    


  - 팀 협업도구 연결하기 

    + 협업을 할 프로젝트 협업도구의 연결(로그인)과 팀명칭을 등록한다  

    


  - 팀 활동 내역

    + 등록한 팀을 선택하면 협업도구에 등록된 항목이 보인다

    + 좌측은 나의 Task가 우측은 팀원들의 Task 카드가 보인다 

    + 카드를 클릭하면 상세내역을 볼 수 있다   

    


  - 카드 상세내역 

    + Google Doc, DropBox, Trello 별로 보여지는 UI가 틀리게 나온다 

    + Google Doc은 기획자가 DropBox는 디자이너가 Trello는 개발자가 주로 사용함을 전제로 한다 

    + 카드형태별 하단에는 코멘트(카드에 대한 요청사항)를 달 수 있고 답변 할 수 있다 

    


  - 채팅하기 

    + 선택한 멤버들과 채팅을 한다 

    + 채팅 멤버추가는 팀단위 멤버와 상관없이 사용자가 선택한 사람과 채팅하는 것이다 

    


  - 메세지 확인하기

    + 자신의 카드에 코멘드(요청사항)를 달아놓은 것을 메세지에서 본다 

    + 자신에게 채팅신청이 왔으나 놓친 채팅 목록을 본다 

    


본 디자인은 개인적인 아이데이션이고 팀과 함께 더 좋은 방안과 UX/UI를 협의하여 완성해 갈 예정이다. 사람들과 대화를 통하여 정반합의 모델을 만들어 가는 재미가 쏠쏠하다



<참조>

  - mash up의 유래 

  - Good UI 전략

  - UX Design for HTML/CSS Mobile WebApp

posted by Peter Note
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 Peter Note
2013. 10. 30. 14:31 My Services/PlayHub

프론트 개발을 위해 준비해야할 것들은 무엇이 있을까? 우선 개발 및 테스트 프레임워크와 라이브러리 관리, 테스트/코드커버리지/빌드 자동화 수행등 준비할 것들이 많다. 이것을 한번에 준비하고 자동화할 방법은 없을까?



1. Yeoman

  - yo : 코드 스케폴딩 역할 (프로젝트를 시작할 때 최초에 디렉토리구조와 템플릿 파일일 제공해 준다) generator-* 로 시작

  - grunt : test, lint, concatenate, build등 Java의 Ant와 같은 Task를 자동 수행

  - bower : 내부적으로 사용하는 컴포넌트의 버전 의존성 관리 및 설치/업데이트/삭제 관리

  - 참조 



2. Addy Osmani의 Frontend Workflow

  - 자동화는 반복적인 작업을 효율적으로 만드는 과정

  - Frontend Automation = Scaffolding + Download Libraries + Download Template + Download Framework



3. PlayHub Workflow 만들기 

  - 자신의 개발환경에 GitHub 저장소 설정하기

    + mac 또는 windows

    + nitrous.io 같은 PaaS 에도 설정가능 

// Node Version Manager 설치 

$ curl https://raw.github.com/creationix/nvm/master/install.sh | sh

$ exit

logout

Connection to 127.0.0.1 closed.


// 현재 CentOS에 설치된 Node version 정보가 나오고 원하는 버전정보를 선택하여 사용할 수 있다. 명령은 help 참조

$ nvm list

       N/A

current: v0.11.5


// playhub 디렉토리 만듦 - 해당 디렉토리 밑으로 playhub의 저장소를 놓을 것임 

$ mkdir playhub 


// playhub의 저장소를 git clone하기 위하여 자신의 ssh 공개키가 없다면 생성한다

// 생성방법 참조

$ cd .ssh

$ ssh-keygen -t rsa -C ysyun@yuwin.co.kr  // 이후 쭉 enter

Generating public/private rsa key pair.

Enter file in which to save the key (/home/vagrant/.ssh/id_rsa):

Enter passphrase (empty for no passphrase):

Enter same passphrase again:

Your identification has been saved in /home/vagrant/.ssh/id_rsa.

Your public key has been saved in /home/vagrant/.ssh/id_rsa.pub.

The key fingerprint is:

8a:32:de:ef:de:fd:22:8f:8d:38:70:b0:d7:56:e1:da ysyun@yuwin.co.kr

The key's randomart image is:

+--[ RSA 2048]---+

|                            |

|          .                 |

|         . .                |

|    .     o                |

|     o .S+               |

|    o.o.+ E              |

|  o .+..                  |

| . +  .o.=.              |

|  . .+=.+o+o.         |

+------------------+

$ ssh-add id_rsa

Could not open a connection to your authentication agent.


// 두개의 id_rsa 파일 생성 

$ ls -lart

-rw-rw-r--  1 vagrant vagrant  399 2013-10-30 05:59 id_rsa.pub

-rw-------  1 vagrant vagrant 1675 2013-10-30 05:59 id_rsa

drwx------  2 vagrant root    4096 2013-10-30 05:59 .


$ cat id_rsa.pub

ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAyvfJRNE/Kx+VwuGcQX8+PYbFadT6mCLZ1.. 중략 ..XS0U0uQ== ysyun@yuwin.co.kr


// GitHub -> "Account Settings" -> "SSH Keys"에서 SSH key값 등록



// 이제 GitHub에서 PlayHub의 Palette를 clone 해 보자 (git 기본 설치를 전제로 함)

$ cd ../playhub/

$ git clone git@github.com:playground-ko/palette.git

Initialized empty Git repository in /home/vagrant/playhub/palette/.git/

.. 중략 ..


$ ls - lart

drwxr-xr-x  5 vagrant vagrant 4096 2013-10-30 06:07 palette


// submodule에 대하여 초기화 하고 업데이트 한다 (Git 환경만들기 참조)

$ cd palette

$ git submodule init

Submodule 'backend' (git@github.com:playground-ko/palette.backend.git) registered for path 'backend'

Submodule 'frontend' (git@github.com:playground-ko/palette.frontend.git) registered for path 'frontend'

$ git submodule update

Initialized empty Git repository in /home/vagrant/playhub/palette/backend/.git/

.. 중략 ..

Submodule path 'backend': checked out 'f8276822e8a12a15f2af19e0c62cbd74a9172d84'

Initialized empty Git repository in /home/vagrant/playhub/palette/frontend/.git/

.. 중략 ..

Submodule path 'frontend': checked out 'e1b7201011321e1df2cc990c4eb2042c765c8a3c'


// playhub 디렉토리로 이동하여 bankend, frontend 저장소를 별도로 받아서 개발을 시작한다 

$ cd ..

$ git clone git@github.com:playground-ko/palette.backend.git

Initialized empty Git repository in /home/vagrant/playhub/palette.backend/.git/

.. 중략 ..

$ git clone git@github.com:playground-ko/palette.frontend.git

Initialized empty Git repository in /home/vagrant/playhub/palette.frontend/.git/

.. 중략 ..


  - yo/grunt/bower, generator-angular 를 설치 또는 업그레이드 하고, frontend 디렉토리로 이동하여 프로젝트를 만든다 

// yo를 설치하면 grunt와 bower가 같이 설치된다 

sudo npm install -g yo

// 현재 버전은 1.0.4 (2013.10.31 기준)

$ yo --version

1.0.0-rc.1.4

$ grunt --version

grunt-cli v0.1.9

grunt v0.4.1

$ bower --version

1.2.7


// 이미 설치되어 있다면 최신버전으로 업데이트  한다 

$ sudo npm update -g yo

$ sudo npm update -g grunt-cli

$ sudo npm update -g bower


// angular framework 설치

sudo npm install -g generator-angular


// angular 웹앱을 만든다 

$ cd  palette.frontend/

$ yo angular Palette


// 빌드하면 배포할 수 있는 dist 배포 디렉토리가 나온다. 

// dist는 온전한 html, css, js 파일들이고 이것은 어느 웹서버에 가져다 놓아도 구동이 된다

// 즉, 굳이 node.js위에 구동되지 않아도 됨 

// 기본적으로 build를 하면 관련 js 파일을 minification하여 사이즈를 줄여준다 

// Sublime Text에 Grunt build를 설정할 수 있다

$ grunt build

// karma 통하여 unit, e2e 테스트 수행

$ grunt test

// node.js 기반으로 서버가 열리면서 브라우져(Chrome)도 자동으로 열린다 

$ grunt server


// dist 배포본은 python을 통하여 간단히 테스트 해 볼 수도 있다 

// 브라우져에서 8000 port를 열어서 배포본이 정상적으로 작동하는지 아이체킹

$ cd dist 

python -m SimpleHTTPServer 8000


이제 Frontend를 Yeoman을 통하여 개발해 보자



4. BootFlat 적용하기 

  - yeoman의 generator-bootstrap은 v2.* 버전이 적용되어 있다. Bootstrap v3.* 버전이 적용된 BootFlat 확장판을 적용하기 위한 방법은 기존 블로깅을 참조한다. 

  - Twitter Bootstrap 고도화



<참조>

  - Yeoman을 통해 AngularJS 프로젝트 자동 구성하기

  - nvm 설치 방법

  - GitHub을 위한 SSH 키 생성방법

  - Sublime Text에 Grunt build 설정하기

  - BootFlat Homepage

posted by Peter Note
2013. 10. 28. 17:57 My Services/PlayHub

PlayHub는 Git Submodule을 사용하여 Main 밑으로 Frontend/Backend로 서브모듈단위로 나눌 것이다. 또한 아키텍쳐에 메세지 버스를 사용하므로 여러가지 모듈 분리가 필요할 것으로 예상된다 




아키텍쳐 

  - 구성 

    + 모바일 웹앱 : SPA(Single Page Application)으로 구성한다. AngularJS + BootFlat 사용예정 

    + 조회&푸쉬 서버 : Node.js를 이용하여 OAuth 로그인을 처리한다. Socket.io를 이용하여 Push를 한다 

    + 메세지 버스 : Redis를 사용하여 "조회&푸쉬 서버"와 "이슈정보 서버" 사이를 느슨하게 연결한다. PubSub 방식을 사용한다 

    + 이슈정보 서버 : Open API를 사용하여 Google Doc, DropBox, Trello, GitHub의 정보를 주기적으로 수집한다 

    + 스토어 서버 :  MongoDB 또는 MySql을 사용한다 

  - 장단점

    + 장점 : Backend의 Scale-out이 가능하고 다양한 언어를 통한 구현 및 Node.js를 통한 손쉬운 Push 구현이 가능하다 

    + 단점 : 관리 포인트가 늘어난다. 



Git 서브모듈 만들기 

  - git submodule <command> 를 사용한다 

  - 생성 순서 

    + GitHub에 Main 저장소를 만든다 

    + GitHub에 Submodule이 될 저장소를 만든다 

    + git clone <Main저장소 주소> playhub & cd playhub

    + git submodule add <Submodule 저장소 주소> <이름지정>

    + GitHub으로 git push 수행하기 

$ git clone https://github.com/ysyun/myplayhub.git

Cloning into 'myplayhub'...

remote: Counting objects: 3, done.

remote: Total 3 (delta 0), reused 0 (delta 0)

Unpacking objects: 100% (3/3), done.


$ cd myplayhub/

$ git submodule add https://github.com/ysyun/myplayhub-spa-webapp.git webapp

Cloning into 'webapp'...

remote: Counting objects: 3, done.

remote: Compressing objects: 100% (2/2), done.

remote: Total 3 (delta 0), reused 0 (delta 0)

Unpacking objects: 100% (3/3), done.

warning: LF will be replaced by CRLF in .gitmodules.

The file will have its original line endings in your working directory.


$ git status

# On branch master

# Changes to be committed:

#   (use "git reset HEAD <file>..." to unstage)

#

# new file:   .gitmodules

# new file:   webapp

#

$ git branch

* master


$ git commit -a -m "add submodule webapp"

[master 2fdaa1b] add submodule webapp

warning: LF will be replaced by CRLF in .gitmodules.

The file will have its original line endings in your working directory.

 2 files changed, 4 insertions(+)

 create mode 100644 .gitmodules

 create mode 160000 webapp


$ cat .gitmodules

[submodule "webapp"]

path = webapp

url = https://github.com/ysyun/myplayhub-spa-webapp.git


$ git push

Counting objects: 4, done.

Delta compression using up to 4 threads.

Compressing objects: 100% (3/3), done.

Writing objects: 100% (3/3), 404 bytes | 0 bytes/s, done.

Total 3 (delta 0), reused 0 (delta 0)

To https://github.com/ysyun/myplayhub.git

   0871412..2fdaa1b  master -> master



  - 전체 소스 받아오기 

    + git clone 으로 Main 저장소 받아오기 

    + git submodule init 명령으로 submodule 등록

    + git submodule update로 서브모듈에 대한 clone 작업 수행 

$ git clone https://github.com/ysyun/myplayhub.git

Cloning into 'myplayhub'...

remote: Counting objects: 6, done.

remote: Compressing objects: 100% (4/4), done.

remote: Total 6 (delta 0), reused 3 (delta 0)

Unpacking objects: 100% (6/6), done.


$ cd myplayhub/

$ git status

# On branch master

nothing to commit, working directory clean


$ git submodule init

Submodule 'webapp' (https://github.com/ysyun/myplayhub-spa-webapp.git) registered for path 'webapp'


$ git submodule update

Cloning into 'webapp'...

remote: Counting objects: 3, done.

remote: Compressing objects: 100% (2/2), done.

remote: Total 3 (delta 0), reused 0 (delta 0)

Unpacking objects: 100% (3/3), done.

Submodule path 'webapp': checked out '1bfbcac347a7d6ce4620900b8dad991a638f2763'


$ cd webapp/


~/myplayhub/webapp on  (detached from 1bfbcac)

$ ls

README.md

$ git status

# HEAD detached at 1bfbcac

nothing to commit, working directory clean


// 서브모듈이 현재 존재하지 않는 브랜치가 되었다

~/myplayhub/webapp on  (detached from 1bfbcac)

$ git branch

* (detached from 1bfbcac)

  master


// 해결을 위하여 branch swith를 해야하는데 "git checkout master"를 하면 된다 

$ git checkout master

Switched to branch 'master'


// 해결이 되었다 

~/myplayhub/webapp on  master

$ git branch

* master


// 이후 업데이트 할 내역이 있으면 

$ git pull 


  - 서브모듈 삭제

    + git submodule rm <submodule 디렉토리> 명령 없어졌음 

    + git rm <submodule 디렉토리> 명령으로 제거

    + .gitmodules 의 submodule관련 설정 3줄 제거 

$ git rm webapp

rm 'webapp'


$ vi .gitmodules

webapp submodule 삭제 


$ git status

# On branch master

# Changes to be committed:

#   (use "git reset HEAD <file>..." to unstage)

#

# deleted:    webapp

#

# Changes not staged for commit:

#   (use "git add <file>..." to update what will be committed)

#   (use "git checkout -- <file>..." to discard changes in working directory)

#

# modified:   .gitmodules


$ git commit -a -m "delete webapp submodule"

[master 8305c3c] delete webapp submodule

 2 files changed, 4 deletions(-)

 delete mode 160000 webapp


$ git status

# On branch master

# Your branch is ahead of 'origin/master' by 1 commit.

#   (use "git push" to publish your local commits)

#

nothing to commit, working directory clean


$ git push




Git 브랜치 전략

  - 브랜치 전략에 대하여 숙지한다

  - master 브랜치에 어느 정도 환경설정이 끝났다면 협업을 위하여 develop 브랜치를 만든다. 

    + master와 develop은 절대 삭제되지 않고 계속 유지 되는 브랜치로 둔다 

  - 기능을 구현하기 시작하면 develop브랜치에서 feature 브랜치를 만든다 

    + Story 단위로 만들어서 관리한다 

    + master에서 따오지 않고 develop에서 브랜치하고, remote origin에 push하지 않는다 

    + 필요없을시 삭제한다 

  - develop 브랜치에서 어느 정도 되었다 싶으면 release 브랜치를 만든다

    + release 번호를 달고 수정이 필요한 부분을 보완하고 tag를 단다 

    + master로 merge를 한다 

  - master에 문제가 발견되었을 때 hotfix 브랜치를 만든다 

    + hotfix는 develop과 master 양쪽에 반영을 한다 


   Master + Develop 브랜치 : 영원한 브랜치 

   Feature + Release + Hotfix 브랜치 : 보조 브랜치 (삭제가능)



<참조>

  - Git 서브모듈 명령어 설명

  - Git Submodule 관리 팁

  - GitHub SSH 만들기 

posted by Peter Note
2013. 10. 28. 16:49 My Services/PlayHub

SRS는 Software Requirements Specification 약어로 서비스를 만드는 내역의 처음과 끝이 담겨져 있다. "SRS의 중요성"이라는 블로그에서 SRS의 중요성을 인지하고, 몇번의 서비스을 하면서 SRS를 써오고 있다. 여기서 오해하지 말아야 할것은 SRS은 양식이나 포멧이 아니다. 즉, 한장으로 표현될 수도 있고, 몇장으로 정해진 양식에 따라서 써질 수도 있다. 처음은 문서-doc-파일에 정의해 보았으나 내용의 확인 및 업데이트가 불편하여 Trello에 SRS를 정의 하고 있다. 




Trello에 SRS 작성하기 

  - 보드를 하나 만든다 

  - 보드에 챕터별로 리스트를 만든다 

  - 챕터에 하위 Task를 만들어 정의된 내역을 편집하면 된다 

* 각 챕터에 대한 자세한 내역은 해당 파일을 참조한다 

SRS_iPhone_PIMSplus.doc

* 참조 URL : http://sep522.googlecode.com/svn-history/r88/trunk/ 의 doc/srs 밑에 문서가 있다  




  - Trello에 SRS를 작성하면 접근성과 유지보수성이 높아진다. SRS는 기획자, 디자이너, 개발자 누구나 접근하여 쉽게 찾고 업데이트 할 수 있어야 한다 

  - 한가지 아쉬운 점은 문서의 버전관리가 안된다는 점이다. 보완을 위하여 좀 더 자세한 내역은 Google Doc을 이용하고 링크를 걸어 놓는 방법을 사용하자 

  - Playground 멤버 PlayHub의 Trello SRS



<참조>

  - SRS의 중요성 - 전규현

  - PlayHub의 Trello SRS

posted by Peter Note
2013. 10. 24. 15:56 My Services/PlayHub

플레이허브(PlayHub, 이하 플브)는 요즘 제가 놀고 있는 플레이그라운드 Public 페북그룹의 첫번째 파일럿 프로젝트입니다. 어제 일을 잘 기억하지 못하는 습성으로 서비스 런칭과정을 하나하나 정리 해보려 합니다. "서비스디자인" 부터 "개발" "런칭"까지 진행 스토리와 개발 노하우를 공유할 수 있었으면 좋겠습니다. 




플레이그라운드 모임의 가치 

플레이그라운드는 플레이허브 파일럿 프로젝트를 수행하는 주체입니다. 모임의 가치는 다음과 같습니다

  - Fun
  - Interesting
  - Something Cool
  - Together

"재밌게 즐기면서 만들어 낸 서비스를 통해서 먼저 스스로 재미를 느끼고, 그렇게 만들어진 서비스를 통해서 수익을 만들어 내고, 덕분에 경제적 한계성에서 벗어나 재미없는 일로부터 벗어날 수 있는 환경을 만들고, 그러한 환경에서 보다 스스로에게 가치있고 의미있는 일을 해보자는 것"


  저는 여기에 하나의 명제를 더 던져보려 합니다. 

"이런 어지러운 세상의 문제점을 해결해 줄 수 있는 서비스를 사람들에게 제공하고 싶다"


  여하튼 이런 생각들에 동참하신 분이 초기에는 200명가량 되었는데, 적극적 참여의사를 밝힌 분들만 추려서 플레이그라운 Members 페북그룹이 결성되었습니다. 여기서 저는 앞으로 열심히 놀 계획을 가지고 있습니다. 참여를 원하시면 이곳을 참조하세요



플레이그라운드(Playground) 파일럿 프로젝트 

  파일럿 프로젝트의 의미는 "서로 알아가기""협업환경 익숙해지기" 입니다. 


  - 하지만 40명가량의 Member분들이 기획:디자인:개발 = 1:1:2 비율이고 의견을 조율해 본봐 서로간의 사용하는 협업도구가 상이하여 우리만의 통합 협업 허브 시스템이 필요하겠구나 생각했습니다. 

  - PlayHub는 PlayGround에서 사용하는 협업도구들의 중요항목을 모아서 기획,디자이너,개발자이 함께 사용할 수 있는 Hub 시스템입니다.

    + Trello : Agile Scrum 도구 

    + GitHub : Social Repository

    + Redmine or Mantis : Issue 시스템

    + DropBox : 파일 공유

    + Google Docs : 문서 공유 

   등에서 모은 데이터를 UX&UI 가미하여 시각화하는 작업을 거칠 것 같습니다. 


  저의 꿈은 "좋은 개발 협업 문화"를 만들고, "글로벌 소프트웨어"를 만드는 것입니다. 이것은 "소프트웨어 개발의 모든 것"라는 책을 통해 알게된 부분에 대한 결심과도 같습니다. 이제 우리에게 필요한 것은 이러한 문화를 만들어야 하고, 더 넓은 세상을 바라 보아야 한다고 생각합니다. 이것은 스마트폰으로 촉발되어 서비스 산업이 국지적인 것이 아니라 글로벌을 염두에 두고 진행되어야 하는 시대가 되었기 때문입니다. 



플레이허브(PlayHub)의 가치 

  좋은 개발 협업 문화 만들기를 위한 하나의 밑거름이 될 것이라 믿습니다

 

  - 이러한 가치는 적극적이고 자발적으로 참여하는 기획, 디자인, 개발자 분들의 노력으로 만들어 갈 수 있다고 믿습니다. 

  - 웹앱 서비스를 통하여 스마트기기 어디에서나 능동적 알람(Push)을 통하여 온라인 협업을 가능토록 해줄 것이라 믿습니다.  

  - 앞으로 이런 Software Engineer가 되어야 겠다 생각해 봅니다. 스스로의 실천이 꿈을 만들어 가리라 믿습니다 

1. 팀이 없는 것처럼 협업하라. 같이 일을 하게 되면 자리를 옮겨서 같이 해라.

2. 지시하지 말고 토론하라.

3. 자신이 무엇을 하고 있고, 무엇을 잘 하는지, 무엇을 하고 싶어하는지 알려라.

4. 핵심기능·기술에만 먼저 집중하여 작게 시작하여 완성하고, 자신을 성장시키며 제품도 같이 성장시켜라

5. 자신보다 더 똑똑한 사람을 뽑아라. 단 팀플레이어만.

6. 자신과 생각이 다른 사람들과 가까이 하라. 불편함을 우정으로 풀어라.

7. 빠른 성장과 진행을 위해 팀을 작게 만들어라.

8. 잘못되어 가는 것이 보이면 빨리 뒤집어라. 고칠것이 있으면 자신이 고쳐라.

9. 자신이 만들고 있는 것이 어떤 유저의 어떤 문제를 해결하는지 자신에게 물어라

10. 항상 유저를 찾고 그들과 소통하라

11. 지식 공유를 하지 않는다는 것은 자신이 성장하고 있지 않다는 이야기다.

12. 결코 어른이 되지 마라. 기술에 대한 열정과 마음은 그대로 남아 있어라.



<참조>

  - 개발자도 기획자다. 좋은 개발자 되기

posted by Peter Note
2013. 10. 23. 17:48 Testing, TDD/Test First

AngularJS를 위한 Test 수행 방법을 생각해 보고, 테스트 전략에 대해서 다른 개발자는 어떻게 하고 있는지 알아보자 


  



1. Angular Testing 에 대한 고찰

 1-1. 스토리 보드 만들기 

  - 프로젝트 진행이 Agile Scrum 이라면 Story를 먼저 도출한다 

  - Story가 도출되었다면 스토리안에 Task를 나눈다 그리고 Task 별로 Validation(검증) 및 Processing(처리)해야할 것을 명세한다

  - 명세는 스토리카드안에 간단히 적는다. 


 1-2. UX&UI 디자인

  - 스토리에 맞는 화면의 목업(Mockup or WireFrame)을 그린다. Balsamiq같은 목업툴을 이용한다 

  - 화면 목업이 스토리에 부합되는지 확인 되면 스토리 개발 우선순위를 결정한다

  - 결정된 화면의 HTML을 Bootstrap 또는 BootFlat을 이용하여 코드를 만든다. 저작툴이 Bootply를 이용해도 된다 


 1-3. TDD 수행하기 개발

  - AngularJS 기반의 개발시 다음과 같이 진행을 한다 (AngularWay 참조)

    + View  === AngularJS의 Directive가 포함된 HTML이다 

    + Model === $scope를 통하여 Controller에서 Two way binding을 자동 수행한다 

    + View와 Controller를 통하여 표현과 행위를 분리한다 (SoC)

    + AngularJS 애플리케이션 접근법

      Step 1) Model을 먼저 생각하고 Service를 만든다

      Step 2) Model이 표현될 View에 대해 생각해 보고 template을 만들고, 필요하면 자동 모델 바인딩되는 Directive도 만든다.

      Step 3) 각 View 를 Controller에 연결한다 (ng-view and routing, ng-controller)


  - E2E Testing

    + AngularJS의 Config의 Routing 에 대해 E2E Test 수행 

    + Model을 표현하는 template에 대해 E2E Test 수행 

    + Directives를 HTML에 코딩하였다면 E2E Test 수행 

    + Filter가 HTML에 표현되는 것이라면 E2E Test 수행

    (Protractor 또는 Angular Test Scenario Runner 사용)

  - Unit Testing

    + Model을 서버 또는 Controller로 부터 받으면 Service/Factory에 대하여 Validation과 XHR등 스팩에 대한 Unit Test 수행

    + Model을 가공 핸들링하여 View와 양방향 연동하는 Controller에 대한 Unit Test 수행

    + Directives/Filter 의 내부 로직에 대한 Unit Test 수행 

    (Mocha 기반의 Chai와 같은 expectation, assertion 사용)


 1-4. 개발 툴체인

  - Yeoman 을 통하여 AngularJS 스케폴딩 코딩을 한다 (참조)

  - 1-3의 E2E 와 Unit Testing을 위하여 AngularJS의 Karma Test Framework안에서 사용하면 된다. 단, Protractor사용시 예외



2. 테스트 전략예 동영상 

  - AngularJS에 대한 Test 전략에 대한 동영상을 보자 

    + testem을 통해 Unit Test를 수행

    + protractor를 통해 E2E Test를 수행

    + 제품 통합 테스트 수행을 위하여 Yeoman과 유사한 Lineman을 사용

    + Jasmine-given을 사용하여 Given-When-Then의 CoffeeScript 방식의 재미있는 테스팅도 소개함

     


  - 새롭게 나온 E2E Test Framework인 Protractor for angularJS 

    + Angular팀에서 새롭게 소개된 Protractor 느낌상 Karma E2E 보다는 더 간단하고 직관적인듯 하다.

     



<참조>

  - End-to-End Testing with AngularJS

  - Front javascript testing framework

posted by Peter Note
2013. 10. 21. 19:01 Testing, TDD/Test First

Karma 테스트 프레임워크를 통하여 AngularJS기반 애플리케이션을 어떻게 테스트 하는지 알아보자 



AngularJS의 두가지 테스트 타입 

  - Unit Testing : 작고 격리된 코드 유닛의 동작에 대한 검증 테스트 

  - E2E Testing : 애플리케이션 전영역에 대한 테스트. E2E 테스터는 로봇처럼 브라우져를 실행하여 페이지를 검증해 준다 

  - AngularJS에서 이야기하는 E2E테스트 시나리오 API : Angular Scenario Runner 

    + describe 

    + beforeEach

    + afterEach

    + it

describe('Buzz Client', function() {

 it('should filter results', function() {

   input('user').enter('jacksparrow');

   element(':button').click();

   expect(repeater('ul li').count()).toEqual(10);

   input('filterText').enter('Bees');

   expect(repeater('ul li').count()).toEqual(1);

 });

});


  - Testing의 유형을 나누어 본다 : Unit, Midway, E2E

    + Unit Testing

      코드레벨 테스트

      격리된 테스트

      Mock & Stubbing 요구, 빠름

    + Midway Testing

      애플리케이션/코드레벨 테스트

      애플리케이션의 모든 부분 접근 가능

      다른 애플리케이션 코드와 상호작용

      뷰(html) 내부의 테스트는 불가능능

      빠름, 단, XHR일 경우 느릴 수 있음 

    + E2E Testing

      웹레벨 테스트

      웹서버가 필요

      완벽한 통합 테스트 



샘플코드 설치 및 수행

  - 데모 페이지

  - 소스 : GitHub Repository

  - 설치 (참조)

$ git clone git://github.com/yearofmoo-articles/AngularJS-Testing-Article.git

Cloning into 'AngularJS-Testing-Article'...

remote: Counting objects: 536, done.

remote: Compressing objects: 100% (368/368), done.

remote: Total 536 (delta 130), reused 516 (delta 117)

Receiving objects: 100% (536/536), 1.28 MiB | 390.00 KiB/s, done.

Resolving deltas: 100% (130/130), done.


$ cd AngularJS-Testing-Article/ && npm install


// 만일 제대로 npm install시 에러가 발생하면 package.json의 버전을 바꾼다

$ vi package.json

   "grunt-karma": "latest",

    "karma": "latest",

  - 수행 : http://localhost:8000 으로 호출한다 

$ grunt server

Running "connect:server" (connect) task

Starting connect web server on localhost:8000.

Waiting forever...

  - 디렉토리 구조 

    + test 밑에 3가지의 karma 환경파일이 존재한다

    + grunt test 명령으로 test 프로그램을 수행할 수 있다 - grunt test, grunt karma:unit, grunt karma:midway, grunt test:e2e (참조)

       * 테스트 안될 경우 주의

        1) bower_components를 components 폴더로 명칭 변경

        2) ngMidwayTester 못 찾으면 : comoponents/ngMidwayTest/src 에서 src를 Source 폴더로 명칭 변경  

    + 각 파트별 수행 테스트 파일은 unit, midway, e2e 폴더로 나뉜다 

    

  - Karma안에서 사용한 Test Frameworks은 test/ 폴더 밑으로 karma-e2e/midway/unit/shared.conf.js 로 설정되어 있다 

    + Mocha.js : Unit Test

    + Chai.js : matchers and assertions

    + Jasmine.js : End-To-End Test(E2E) Framework으로 Angular Scenario Runner에서 사용


  * Midway Plugin에서 에러가 발생한다. 해결하면 500원



Mocha, Chai 그리고 Angular Scenario Runner

  - Unit 과 Midway를 테스트에는 테스트 스팩은 Mocha(모카)를 이용하고 Matcher/Assertion으로 Chai를 사용한다  

  - E2E 테스트시에는 Mocah, Chai를 사용할 수 없었다. 별도의 E2E 수행 문서를 제공한다

  - 테스트 시에 AngularJS는 XHR 요청도 Interceptors를 통해서 가로챌 수 있다. 

     그외 route, controller, service, directive까지도 가로채서 테스트 코드를 넣을 수 있다. 

  - XHR 가로채기의 비용이 크면 angular-mock.js를 통하여 Mock을 사용할 수도 있다 



1. Testing Module

  - AngularJS에서 모듈은 directives, controllers, templates, services 그리고 resources 생성을 위한 컨테이너 오브젝트이다

  - 따라서 테스트에 최초로 할 것이 해당 모듈이 있는지 체크하는 것이다 

  - Midway 테스트에서 수행한다 

  - midway/appSpec.js

//
// test/midway/appSpec.js
//
describe("Midway: Testing Modules", function() {
  describe("App Module:", function() {

    var module;
    before(function() {
      module = angular.module("App");
    });

    it("should be registered", function() {
      expect(module).not.to.equal(null);
    });

    // App이 의존하는 모듈들이 있는지 검증 
    describe("Dependencies:", function() {

      var deps;
      var hasModule = function(m) {
        return deps.indexOf(m) >= 0;
      };
      before(function() {
        deps = module.value('appName').requires;
      });

      //you can also test the module's dependencies
      it("should have App.Controllers as a dependency", function() {
        expect(hasModule('App.Controllers')).to.equal(true);
      });

      it("should have App.Directives as a dependency", function() {
        expect(hasModule('App.Directives')).to.equal(true);
      });

      it("should have App.Filters as a dependency", function() {
        expect(hasModule('App.Filters')).to.equal(true);
      });

      it("should have App.Routes as a dependency", function() {
        expect(hasModule('App.Routes')).to.equal(true);
      });

      it("should have App.Services as a dependency", function() {
        expect(hasModule('App.Services')).to.equal(true);
      });
    });
  }); 

});



2. Testing Routes

  - 애플리케이션의 페이지 라우팅이 제대로 되는지 테스트 한다 

  - Midway, E2E 테스트에서 수행한다 

  - Midway Testing (Midway Testser Plugin)

//
// test/midway/routesSpec.js
//
describe("Testing Routes", function() {

  var test;

  before(function(done) {
    test = new ngMidwayTester();
    test.register('App', done);
  });

  it("should have a videos_path", function() {
    expect(ROUTER.routeDefined('videos_path')).to.equal(true);
    var url = ROUTER.routePath('videos_path');
    expect(url).to.equal('/videos');
  });

  it("the videos_path should goto the VideosCtrl controller", function() {
    var route = ROUTER.getRoute('videos_path');
    route.params.controller.should.equal('VideosCtrl');
  });

  it("the home_path should be the same as the videos_path", function() {
    expect(ROUTER.routeDefined('home_path')).to.equal(true);
    var url1 = ROUTER.routePath('home_path');
    var url2 = ROUTER.routePath('videos_path');
    expect(url1).to.equal(url2);
  });
 

});

  - E2E Testing 

// test/e2e/routesSpec.js
//
describe("E2E: Testing Routes", function() {

  beforeEach(function() {
    browser().navigateTo('/');
  });

  it('should jump to the /videos path when / is accessed', function() {
    browser().navigateTo('#/');
    expect(browser().location().path()).toBe("/videos");
  });

  it('should have a working /videos route', function() {
    browser().navigateTo('#/videos');
    expect(browser().location().path()).toBe("/videos");
  });

  it('should have a working /wathced-videos route', function() {
    browser().navigateTo('#/watched-videos');
    expect(browser().location().path()).toBe("/watched-videos");
  });

  it('should have a working /videos/ID route', function() {
    browser().navigateTo('#/videos/10');
    expect(browser().location().path()).toBe("/videos/10");
  });

});



3. Testing Controllers

  - $scope를 통하여 template 페이지와 데이터 바인딩 되는 테스트 

  - Unit Testing 

// test/unit/controllers/controllersSpec.js
//
describe("Unit: Testing Controllers", function() {

  beforeEach(angular.mock.module('App'));

  it('should have a VideosCtrl controller', function() {
    // controller 모듈이 있는지 검증
    expect(App.VideosCtrl).not.to.equal(null);
  });

  it('should have a VideoCtrl controller', function() {
    expect(App.VideoCtrl).not.to.equal(null);
  });

  it('should have a WatchedVideosCtrl controller', function() {
    expect(App.WatchedVideosCtrl).not.to.equal(null);
  });

  // $httpBackend Mock 객체를 통해 단위 테스트 검증
  it('should have a properly working VideosCtrl controller', 
    inject(function($rootScope, $controller, $httpBackend) {
    var searchTestAtr = 'cars';
    var response = $httpBackend.expectJSONP('https://gdata.youtube.com/feeds/api/videos?q='
                     + searchTestAtr + '&v=2&alt=json&callback=JSON_CALLBACK');
    response.respond(null);

    var $scope = $rootScope.$new();
    var ctrl = $controller('VideosCtrl', {
      $scope : $scope,
      $routeParams : {
        q : searchTestAtr
      }
    });
  }));

  it('should have a properly working VideoCtrl controller', 
       inject(function($rootScope, $controller, $httpBackend) {
    var searchID = 'cars';
    var response = $httpBackend.expectJSONP('https://gdata.youtube.com/feeds/api/videos/' 
                     + searchID + '?v=2&alt=json&callback=JSON_CALLBACK');
    response.respond(null);

    var $scope = $rootScope.$new();
    var ctrl = $controller('VideoCtrl', {
      $scope : $scope,
      $routeParams : {
        id : searchID
      }
    });
  }));

  it('should have a properly working WatchedVideosCtrl controller', 
         inject(function($rootScope, $controller, $httpBackend) {
    var $scope = $rootScope.$new();

    //we're stubbing the onReady event
    $scope.onReady = function() { };
    var ctrl = $controller('WatchedVideosCtrl', {
      $scope : $scope
    });
  }));
 

});

  - Midway Testing

// test/midway/controllers/controllersSpec.js

describe("Midway: Testing Controllers", function() {

  var test, onChange;
  // Async Test로 done 객체 파라미터 전달 
  before(function(done) {
    ngMidwayTester.register('App', function(instance) {
      test = instance;
      done();
    });
  });

  // 라투트통하여 페이지 변경 성공 이벤트 발생하면 onChange() 호출
  before(function() {
    test.$rootScope.$on('$routeChangeSuccess', function() {
      if(onChange) onChange(); 
    });
  });

  beforeEach(function(done) {
    test.reset(done);
  });

  it('should load the VideosCtrl controller properly when /videos route is accessed', function() {
    onChange = function() {
      test.path().should.eq('/videos');
      var current = test.route().current;
      var controller = current.controller;
      var scope = current.scope;
      expect(controller).to.equal('VideosCtrl');
    };
    test.path('/videos'); // /videos 호출되면 자동으로 onChange() 호출되어 검증 
  });

  it('should load the WatchedVideosCtrl controller properly when /watched-videos route is accessed', function(done) {
    onChange = function() {
      test.path().should.eq('/watched-videos');
      var current = test.route().current;
      var controller = current.controller;
      var params = current.params;
      var scope = current.scope;

      expect(controller).to.equal('WatchedVideosCtrl');
      done();
    };
    test.path('/watched-videos');
  });

});

  - E2E Testing

// test/e2e/controllers/controllersSpec.js
//
describe("E2E: Testing Controllers", function() {

  // 첫 페이지 있는지 검증 
  beforeEach(function() {
    browser().navigateTo('/');
  });

  // videos의 html 화면에서 DIV id="ng-view"의 html 내역 안에 data-app-youtube-listings이 있는지 검증   
  it('should have a working videos page controller that applies the videos to the scope', function() {
    browser().navigateTo('#/');
    expect(browser().location().path()).toBe("/videos");
    expect(element('#ng-view').html()).toContain('data-app-youtube-listings');
  });

  it('should have a working video page controller that applies the video to the scope', function() {
    browser().navigateTo('#/videos/WuiHuZq_cg4');
    expect(browser().location().path()).toBe("/videos/WuiHuZq_cg4");
    expect(element('#ng-view').html()).toContain('app-youtube-embed');
  }); 

}); 



4. Testing Services/Factories 

  - 서비스는 테스트가 가장 쉬운 코들 블럭이다 

  - Service와 Factory에 대해서 테스트 하는 방법

  - Unit Testing

// test/unit/services/servicesSpec.js
//
describe("Unit: Testing Controllers", function() {

  beforeEach(angular.mock.module('App'));

  it('should contain an $appStorage service', inject(function($appStorage) {
    expect($appStorage).not.to.equal(null);
  }));

  // 서비스 존재 유무 검증 
  it('should contain an $appYoutubeSearcher service', inject(function($appYoutubeSearcher) {
    expect($appYoutubeSearcher).not.to.equal(null);
  }));

  it('should have a working $appYoutubeSearcher service', inject(['$appYoutubeSearcher',function($yt) {
    expect($yt.prefixKey).not.to.equal(null);
    expect($yt.resize).not.to.equal(null);
    expect($yt.prepareImage).not.to.equal(null);
    expect($yt.getWatchedVideos).not.to.equal(null);
  }]));

  // $appYoutubeSearcher Factory 검증 
  it('should have a working service that resizes dimensions', inject(['$appYoutubeSearcher',function($yt) {
    var w = 100;
    var h = 100;
    var mw = 50;
    var mh = 50;
    var sizes = $yt.resize(w,h,mw,mh);
    expect(sizes.length).to.equal(2);
    expect(sizes[0]).to.equal(50);
    expect(sizes[1]).to.equal(50);
  }]));

  // $appStorage Factory 검증 
  it('should store and save data properly', inject(['$appStorage',function($storage) {
    var key = 'key', value = 'value';
    $storage.enableCaching();
    $storage.put(key, value);
    expect($storage.isPresent(key)).to.equal(true);
    expect($storage.get(key)).to.equal(value);

    $storage.erase(key);
    expect($storage.isPresent(key)).to.equal(false);

    $storage.put(key, value);
    $storage.flush();
    expect($storage.isPresent(key)).to.equal(false);
  }]));
 

});

  - Midway Testing 

// test/midway/services/servicesSpec.js
//
describe("Midway: Testing Services", function() {

  var test, onChange, $injector;

  before(function(done) {
    ngMidwayTester.register('App', function(instance) {
      test = instance;
      done();
    });
  });

  // 화면 변경이 있을 경우 onChange() 호출 
  before(function() {
    test.$rootScope.$on('$routeChangeSuccess', function() {
      if(onChange) onChange(); 
    });
  });

  before(function() {
    $injector = test.injector();
  });

  beforeEach(function() {
    //test.reset();
  });

  // $appYoutubeSearcher 를 $injector로 부터 가져옴. findeVideo 호출함 
  it('should perform a JSONP operation to youtube and fetch data', function(done) {
    var $yt = $injector.get('$appYoutubeSearcher');
    expect($yt).not.to.equal(null);

    //this is the first video ever uploaded on youtube
    //so I doubt it will be removed anytime soon
    //and should be a good testing item
    var youtubeID = 'jNQXAC9IVRw';

    $yt.findVideo(youtubeID, false,
      function(q, data) {
        expect(data).not.to.equal(null);
        expect(data.id).to.equal(youtubeID);
        done();
      }
    );
  });
 

});



5. Testing Directives

  - AngularJS입장에서 HTML 코드안에서 컴포넌트 역할이다 

    HTML상에 반복되며 격리될 수 있는 것을 Directive로 만든다. 따라서 HTML 코드 내역은 최소화 되고 불필요한 반복을 줄일 수 있다.

  - Directive가 $scope와 DOM을 가지고 어떻게 작동할지 테스트 해야 한다 

  - XHR을 호출할 수도 있다. 따라서 Unit, Midway, E2E 전부분의 테스트를 수행한다 

  - Unit Testing 

// test/unit/directives/directivesSpec.js
//
describe("Unit: Testing Directives", function() {

  var $compile, $rootScope;

  beforeEach(angular.mock.module('App'));

  // 
  beforeEach(inject(
    ['$compile','$rootScope', function($c, $r) {
      $compile = $c;
      $rootScope = $r;
    }]
  ));

  // app-welcome Directive안에 Welcome 글자와 매치하는지 검증
  it("should display the welcome text properly", function() {
    var element = $compile('<div data-app-welcome>User</div>')($rootScope);
    expect(element.html()).to.match(/Welcome/i);
  })

});

  - Midway Testing

// test/midway/directives/directivesSpec.js
//
describe("Midway: Testing Directives", function() {

  var test, $injector;

  // $injector 얻어오기 
  before(function(done) {
    ngMidwayTester.register('App', function(instance) {
      test = instance;
      done();
    });
  });

  before(function() {
    $injector = test.$injector;
  });

  // $appYoutubeSearcher 통하여 응답을 받은후 app-youtube-listings Directive를 검증한다. XHR호출이므로 setTimeout 값으로 1초를 준다 
  it("should properly create the youtube listings with the directive in mind", function(done) {
    var $youtube = $injector.get('$appYoutubeSearcher');

    var html = '';
    html += '<div data-app-youtube-listings id="app-youtube-listings">';
    html += ' <div data-ng-include="\'templates/partials/youtube_listing_tpl.html\'" data-ng-repeat="video in videos"></div>';
    html += '</div>';

    var $scope = test.scope();
    var element = angular.element(html);

    $youtube.query('latest', false, function(q, videos) {
      $scope.videos = videos;

      test.directive(element, $scope, function(element) {
        setTimeout(function() {
          var klass = (element.attr('class') || '').toString();
          var hasClass = /app-youtube-listings/.exec(klass);
          expect(hasClass.length).to.equal(1);

          var kids = element.children('.app-youtube-listing');
          expect(kids.length > 0).to.equal(true);
          done();
        },1000);
      });
    });
  });

});

  - E2E Testing

// test/e2e/directives/directivesSpec.js
//
describe("E2E: Testing Directives", function() {

  beforeEach(function() {
    browser().navigateTo('/');
  });

  // 화면을 이동하였을 때 Directive가 표현하는 HTML 내역이 있는지 검증
  it('should have a working welcome directive apply it\'s logic to the page', function() {
    browser().navigateTo('#/videos');
    expect(browser().location().path()).toBe("/videos");
    expect(element('#app-welcome-text').html()).toContain('Welcome');
  });

  it('should have a working youtube listing directive that goes to the right page when clicked', function() {
    browser().navigateTo('#/videos');
    element('.app-youtube-listing').click();
    expect(browser().location().path()).toMatch(/\/videos\/.+/);
  });

});




6. Testing Templates, Partials & Views 

  - 격리된 HTML 코드이다 

  - 앵귤러에서는 ngView 또는 ngInlcude 통하여 렌더링된다 

  - 뷰는 $templateCache 서비스 통해서 캐싱되어 제공된다. 

    테스트시 templateUrl을 주면 캐싱된 mock template을 얻어와 사용할 수 있다 

  - Midway Testing

// test/midway/templates/templatesSpec.js
//
describe("Midway: Testing Requests", function() {

  var test, onChange;
  var $location, $injector, $params;

  // url path 변경시 onChange() 호출
  before(function(done) {
    ngMidwayTester.register('App', function(instance) {
      test = instance;
      test.$rootScope.$on('$routeChangeSuccess', function() {
        if(onChange) onChange(); 
      });
      done();
    });
  });

  before(function() {
    $injector = test.$injector;
    $location = test.$location;
    $params   = test.$routeParams;
  });

  beforeEach(function() {
    onChange = null;
  });

  // templateUrl을 test.path('XXX') 호출후 template 명칭이 맞는지 검증
  it("should load the template for the videos page properly", function(done) {
    onChange = function() {
      setTimeout(function() {
        var current = test.route().current;
        var controller = current.controller;
        var template = current.templateUrl;
        expect(template).to.equal('templates/views/videos/index_tpl.html');
        onChange = null
        done();
      },1000);
    };
    test.path('/videos?123');
  });
 

});

  - E2E Testing

// test/e2e/templates/templatesSpec.js
//
describe("E2E: Testing Templates", function() {

  beforeEach(function() {
    browser().navigateTo('/');
  });

  // 각 templateUrl 이동시 화면 내역을 검증
  it('should redirect and setup the videos page template on root', function() {
    browser().navigateTo('#/');
    expect(element('#ng-view').html()).toContain('youtube_listing');
  });

  it('should load the watched videos template into view', function() {
    browser().navigateTo('#/watched-videos');
    expect(element('#ng-view').html()).toContain('youtube_listing');
  });

  it('should load the watched video template into view', function() {
    browser().navigateTo('#/videos/123');
    expect(element('#ng-view').html()).toContain('profile');
  });

  it('should redirect back to the index page if anything fails', function() {
    browser().navigateTo('#/something/else');
    expect(element('#ng-view').html()).toContain('youtube_listing');
  });
 

});



7. Testing Filters

  - filter 테스트시에는 $injector 서비스가 필요하고, $injector로 부터 $filter를 얻어온다 

  - Unit Testing

// test/unit/filters/filtersSpec.js
//
describe("Unit: Testing Filters", function() {

  beforeEach(angular.mock.module('App'));

  it('should have a range filter', inject(function($filter) {
    expect($filter('range')).not.to.equal(null);
  }));

  // $filter 서비스를 받아서 range 필터를 얻는다 
  it('should have a range filter that produces an array of numbers', inject(function($filter) {
    var range = $filter('range')([], 5);
    expect(range.length).to.equal(5);
    expect(range[0]).to.equal(0);
    expect(range[1]).to.equal(1);
    expect(range[2]).to.equal(2);
    expect(range[3]).to.equal(3);
    expect(range[4]).to.equal(4);
  }));

  it('should return null when nothing is set', inject(function($filter) {
    var range = $filter('range')();
    expect(range).to.equal(null);
  }));

  // range filter의 input에 대한 검증
  it('should return the input when no number is set', inject(function($filter) {
    var range, input = [1];
    range = $filter('range')(input);
    expect(range).to.equal(input);

    range = $filter('range')(input, 0);
    expect(range).to.equal(input);

    range = $filter('range')(input, -1);
    expect(range).to.equal(input);

    range = $filter('range')(input, 'Abc');
    expect(range).to.equal(input);
  }));
 

});

  - Mideway Testing

// test/midway/filters/filtersSpec.js

//
describe("Midway: Testing Filters", function() {

  var test, onChange, $filter;

  // 최초 한번 미리 $filter 서비스를 얻어온다 
  before(function(done) {
    ngMidwayTester.register('App', function(instance) {
      test = instance;
      test.$rootScope.$on('$routeChangeSuccess', function() {
        if(onChange) onChange(); 
      });

      $filter = test.filter();

      done();
    });
  });

  beforeEach(function() {
    test.reset();
  });

  it('should have a working range filter', function() {
    expect($filter('range')).not.to.equal(null);
  });

  // html 템플릿 DOM을 만들어서 append 하고, Directives에 사용한 range에 대해서 검증
  it('should have a working filter that updates the DOM', function(done) {
    var id = 'temp-filter-test-element';
    var html = '<div id="' + id + '"><div class="repeated" ng-repeat="n in [] | range:10">...</div></div>';
    var element = angular.element(html);
    angular.element(document.body).append(html);

    test.directive(element, test.scope(), function(element) {
      var elm = element[0];
      setTimeout(function() {
        var kids = elm.getElementsByTagName('div');
        expect(kids.length).to.equal(10);
        done();
      },1000);
    });
  });

});

  - E2E Testing 

// test/e2e/filters/filtersSpec.js

//
describe("E2E: Testing Filters", function() {

  beforeEach(function() {
    browser().navigateTo('/');
  });

  // range 필터가 사용된 화면으로 이동하였을 경우 화면이 정상적으로 표시 되는지 검증
  it('should have a filter that expands the stars properly', function() {
    browser().navigateTo('#/videos/WuiHuZq_cg4');
    expect(repeater('#app-youtube-stars > .app-youtube-star').count()).toBeGreaterThan(0);
  });

});



<참조>

  - 원문 : Full Spectrum Test in AngularJS 

  - Javascript 테스트를 어렵게 하는 6가지

  - Service Test 전략 동영상

    


  - AngularJS를 위한 새로운 E2E Test Framework "Protractor"

  - Protractor로 E2E 테스트하기 : 기존 Karma E2E 보다 쉬워진 느낌이다. 30분 부터 보시라

    


posted by Peter Note