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

Publication

Category

Recent Post

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 윤영식
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 윤영식
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 윤영식
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 윤영식
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 윤영식
2013. 11. 9. 07:08 My Services/PlayHub

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





1. Express-Train 설치하기 

  - 설치 : npm install -g express-train 

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

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

$ train new palette.frontend.node 

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

$ train run 

  - 디렉토리 구조 

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

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

    + test : Mocha를 이용한다

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

app

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

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

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

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

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

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

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


bin                   -- executable scripts : 

doc                  -- documentation

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

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

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

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

{

  "cookie_secret": "boggle at the situation",

  "session":{

    "key": "boggle at the situation",

    "secret": "boggle at the situation"

  },

  "client_port": 3000,

  "request_timeout": 1000,

  // RESTful 호출 접두사 Restanuglar 참조 

  "restful_api_version": "/api/v1",

  "server_name": "first",

  // REDIS 연결 Client Library 환경 설정

  "data_broker": {

    "port" : 6379,

    "host" : "localhost",

    "auth" : ""

  },

  // MongoDB 환경 설정

  "mongodb":{

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

  },

  "playhub_channel_file": "channels.json",

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

  "winston_mongo": {

    "level": "error",

    "db": "playhub_palette",

    "host": "localhost",

    "port": 27017,

    "collection": "sd_logs"

  },

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

  "winston_file": {

    "filename": "sd.log",

    "exception": "sd_exception.log",

    "maxSize": 20480,

    "maxFiles": 12,

    "colorize": true

  },

  "character_encoding":"utf-8"

}



2. Express-Train 파일 사용하기 

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

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

// app.js 내역중 일부 

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

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

var LOCATIONS = createApplication.locations = {

    pkg:{

        path: '../package.json'

    },

    config:{

        path: '../config'

    },

    logs:{

        path: '../logs'

    },

    models: {

        path: 'models',

        autoinject: true,

        aggregateOn: 'models'

    },

    views: {

        path: 'views'

    },

    lib: {

        path: 'lib',

        autoinject: true

    },

    controllers: {

        path: 'controllers',

        autoinject: true

    },

    pub: {

        path: 'public'

    },

    middleware: {

        path: 'middleware',

        autoinject: true

    }, 

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

    services: {

       path: 'services',

       autoinject: true

    },

}; 


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

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

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

function createApplication(dir, locs) {

    catchErrors();


    var app = express(),

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

        tree = new nject.Tree();


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

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


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

    tree.constant('config', config);


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

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

    });


    //allow override of app

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

        tree.constant('app', app);  

    }


    //log path 하나 추가함 

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

    tree.constant('logPath', logPath);


    return tree.resolve();

}


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

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

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

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

////////

// 형식

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

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

}


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

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

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


var winston = require('winston'),

  path = require('path');


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


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

  require('winston-mongodb').MongoDB;


  var dbOpts = {

    level: config.winston_mongo.level,

    db: config.winston_mongo.db,

    collection: config.winston_mongo.collection,

    host: config.winston_mongo.host,

    port: config.winston_mongo.port

  };

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


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

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

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

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


  var fileOpts = {

    filename: fileName,

    maxsize: config.winston_file.maxSize,

    maxFiles: config.winston_file.maxFiles,

    colorize: config.winston_file.colorize

  }


  var exceptionOpts = {

    filename: exceptionFileName,

    maxsize: config.winston_file.maxSize,

    maxFiles: config.winston_file.maxFiles,

    colorize: config.winston_file.colorize

  }


  var logger = new winston.Logger({

    transports: [

      new winston.transports.Console(),

      new winston.transports.File(fileOpts),

      new winston.transports.MongoDB(dbOpts)

    ],

    exceptionHandlers: [

      new winston.transports.Console(),

      new winston.transports.File(exceptionOpts),

      new winston.transports.MongoDB(dbOpts)

    ]

  });


//  testing

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

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

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

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


  return logger;

}


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

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

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

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

module.exports = function (applogger) {

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

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

};




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

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

    + SPA 저장소 : palette.frontend

    + Node저장소 : palette.frontend.node 

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

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

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

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

// 폴더구조 

playhub

  |_ palette.frontend         (별도 git repository)

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


// Angular 기반 frontend 파일 

playhub$ cd palette.frontend

playhub$ grunt build 


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

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

playhub$ rm -rf *

playhub$ cd ../views

playhub$ rm -rf *


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

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

  - 테스트 하기 

$ cd palette.frontend.node

train run 

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



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

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

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

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

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

$ cd palette.frontend.node


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

$ cd node_modules/

$ cd express-train/

$ ls -arlt

total 80

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

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

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

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

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

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

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

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

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

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

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

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

$ mv bin ../../


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

$ train run

module.js:340

    throw err;

          ^

Error: Cannot find module 'commander'

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

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

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

    at require (module.js:380:17)

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

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

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

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

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

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


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

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

$ npm install underscore --save

$ npm install handlebars --save

$ npm install boilerplate --save-dev

$ cd bin && train run 

module.js:340

    throw err;

          ^

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

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

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

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

    at require (module.js:380:17)

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

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

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

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

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

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


// 에러 다시 수정 하기 

$ cd ../node_modules/express-train/

$ ls -alrt

total 80

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

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

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

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

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

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

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

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

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

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


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

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

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

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


// express-train 은 삭제한다 

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

$ cd ../../bin/

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

$ ls -alrt

total 32

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

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

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

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

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

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

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


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

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


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

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

// 수정 내역 

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


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

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

// 수정 내역

// module.exports = {

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

// };


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

module.exports = paletteApp(__dirname);


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

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

  

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

$ cd bin && vi app.js

//위치 변경하기 

var LOCATIONS = createApplication.locations = {

    pkg:{

        path: '../package.json'

    },

    config:{

        path: '../config'

    },

    logs:{

        path: '../logs'

    },

    models: {

        path: '../api/models',

        autoinject: true,

        aggregateOn: 'models'

    },

    views: {

        path: '../api/views'

    },

    lib: {

        path: '../api/lib',

        autoinject: true

    },

    controllers: {

        path: '../api/controllers',

        autoinject: true

    },

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

    pub: {

        path: '../public'

    },

    middleware: {

        path: '../api/middleware',

        autoinject: true

    }

};


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

$ mv app/public .


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

$ train run 

[express train application listening on 4000]


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

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

$ mv package.json packate.json.node


// public 폴더는 삭제한다 

$ rm -rf public 


// 옮기기 

$ mv * ../palette.frontend/

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

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

$ cd node_modules

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


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

$ cd ../../palette.frontend

$ mv dist public 

$ cd bin

$ train run

[express train application listening on 4000]


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

$ cd .. && rm -rf public 


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

$ vi Gruntfile.js

// 변경내역 

 grunt.initConfig({

    yeoman: {

      // configurable paths

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

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

    },


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

$ grunt build 

$ cd bin && train run 

[express train application listening on 4000]


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

  - api : Node 서버 

  - app : SPA Angular

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


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

{

  "name": "palette",

  "version": "0.0.1",

  "dependencies": {

    "express": "3.2.x",

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

    "connect-timeout": "latest",

    "connect-mongodb": "latest",

    "mongoose": "latest",

    "async": "latest",

    "express-hbs": "latest",

    "passport": "latest",

    "winston": "latest",

    "commander": "~2.0.0",

    "underscore": "~1.5.2",

    "handlebars": "~1.1.2",

    "nject": "~0.1.6"

  },

  "devDependencies": {

    "grunt": "~0.4.1",

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

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

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

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

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

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

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

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

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

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

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

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

    "grunt-autoprefixer": "~0.2.0",

    "grunt-usemin": "~0.1.11",

    "grunt-svgmin": "~0.2.0",

    "grunt-rev": "~0.1.0",

    "grunt-concurrent": "~0.3.0",

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

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

    "grunt-ngmin": "~0.0.2",

    "time-grunt": "~0.1.0",

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

    "grunt-karma": "~0.6.2",

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

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

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

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

    "karma-jasmine": "~0.1.3",

    "karma-requirejs": "~0.1.0",

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

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

    "karma": "~0.10.4",

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

    "mocha": "latest"

  },

  "engines": {

    "node": ">=0.8.0"

  },

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

  "scripts": {

    "test": "grunt test",

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

    "server-test": "mocha test"

  }

}


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

$ npm start

[express train application listening on 4000]


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

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

$ train run

env: node\r: No such file or directory


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

$ brew install dos2unix

==> Installing dos2unix dependency: gettext

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

.. 중략 ..


$ dos2unix train

dos2unix: converting file train to Unix format ...

$ train run 

[express train application listening on 4000]



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

  - Express + Angular의 통합

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

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

// FullStack generator 설치 

$ sudo npm install -g generator-angular-fullstack

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

$ yo angular-fullstack FullStackTest

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

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

$ grunt build 

... 중략 ...

Elapsed time

useminPrepare:html                   25ms

concurrent:dist                          5s

autoprefixer:dist                        113ms

concat:public/scripts/modules.js  23ms

copy:dist                                  640ms

cdnify:dist                                 21ms

ngmin:dist                                263ms

cssmin:public/styles/main.css     33ms

uglify:dist                                  28ms

uglify:public/scripts/plugins.js      373ms

uglify:public/scripts/modules.js   134ms

usemin:html                             482ms

usemin:css                              152ms

Total                                        8s



<참조> 

  - Angular-Express-Train-Seed

  - Yeoman : generator-fullstack with angular and express

  - dos2unix 설치하여 사용하기

posted by 윤영식
2013. 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 윤영식
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 윤영식
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 윤영식
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 윤영식
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 윤영식
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 윤영식
2013. 10. 20. 21:12 Testing, TDD/Test First

AngularJS 기반하에 TDD를 수행하거나 테스트 코드를 작성할 때 적합한 툴과 개념에 대해 이해를 하자



1. Unit Testing

  - 단위 기능 테스트 

  - 추천 단위 테스트 툴

    + Mocha : 단순하며 유연한 테스트 프레임워크 (추천)

    + Jasmine : Behavior-Driven Development Framework for testing Javascript (추천)

    + QUnit : jQuery에서 사용

  - 단위 테스트 만들기 

    + 사용자 스토리 기반으로 만들어 보자 

       "사용자로써 나의 앱에 로그인을 하면 대시보드 페이지를 본다"

    + Story -> Features -> Units 로 구분을 한다 

       "로그인 폼"을 하나의 기능으로 선택한다

       하나의 스토리는 여러 기능의 합집합이며, 각 기능의 정상작동 검증(Verification)을 거쳐서 하나의 스토리는 완성된다 

    + 로그인 폼에 대한 Spec을 만든다 (BDD)

describe('user login form', function() {


    // critical : 기능의 검증

    it('ensure invalid email addresses are caught', function() {});

    it('ensure valid email addresses pass validation', function() {});

    it('ensure submitting form changes path', function() { });


    // nice-to-haves

    it('ensure client-side helper shown for empty fields', function() { });

    it('ensure hitting enter on password field submits form', function() { });


});

   

  - 위의 테스트 Spec을 AngularJS로 단위 테스트 만들기 

    + 파일 : angular.js, angular-mocks.js, app.js(모듈명-App), loginController.js(LoginCtrl),  loginService.js(LoginService)

    + beforeEach에서 angular.mock.module 통해 모듈을 셋업한다 

       beforeEach에서 angular.mock.inject 통해 controller에서 $scope 셋업한다 (참조) 

    + it 에서 inject 통하여 service를 셋업한다 (참조)

describe("Unit Testing Examples", function() {


  beforeEach(angular.mock.module('App')); 


  it('should have a LoginCtrl controller', function() {

    expect(App.LoginCtrl).toBeDefined();

  });


  it('should have a working LoginService service', inject(['LoginService',

    function(LoginService) {

      expect(LoginService.isValidEmail).not.to.equal(null);


      // test cases - testing for success

      // 검증을 위한 테스트 데이터 

      var validEmails = [

        'test@test.com',

        'test@test.co.uk',

        'test734ltylytkliytkryety9ef@jb-fe.com'

      ];


      // test cases - testing for failure

      var invalidEmails = [

        'test@testcom',

        'test@ test.co.uk',

        'ghgf@fe.com.co.',

        'tes@t@test.com',

        ''

      ];


      // you can loop through arrays of test cases like this

      for (var i in validEmails) {

        var valid = LoginService.isValidEmail(validEmails[i]);

        expect(valid).toBeTruthy();

      }

      for (var i in invalidEmails) {

        var valid = LoginService.isValidEmail(invalidEmails[i]);

        expect(valid).toBeFalsy();

      }


    }]) // end inject 

  ); // end it 

}); // end describe



2. Integration Testing 

  - 통합 테스트는 end-to-end 테스트(E2E)이고, 수행순서대로 차례로 수행해 보는 것이다. 

     그리고 애플리케이션과 UI의 상태를 검증한다. 심지어 Visual적인 부분에 대한 검증도 가능하다 

  - 추천 도구 

    + Karma : AngularJS에서 공식사용 (추천)

    + CasperJS : headless browser로 사용 

    + PhantomJS : headles brower

  - 통합 테스트 만들기 

    + "사용자로써 나의 앱에 로그인하여 대시보드 페이지를 본다"

describe('user login', function() {


  // 수행 및 결과 상태의 검증

  it('ensures user can log in', function() {

    // expect current scope to contain username

  });

  it('ensures path has changed', function() {

    // expect path to equal '/dashboard'

  });


});


  - 위의 테스트 Spec을 AngularJS로 통합 테스트 만들기 (참조)

    + 최근에는 Angular Scenario Test Runner와 같은 Protractor E2E test framework이 새롭게 소개되고 있다 

describe("Integration/E2E Testing", function() {


  // start at root before every test is run

  beforeEach(function() {

    browser().navigateTo('/');

  });


  // test default route

  it('should jump to the /home path when / is accessed', function() {

   // 루트 패스로 이동 - 로그인 화면 

    browser().navigateTo('#/');

    expect(browser().location().path()).toBe("/login");

  });


  it('ensures user can log in', function() {

    // 로그인 화면인지 체크

    browser().navigateTo('#/login');

    expect(browser().location().path()).toBe("/login");


    // assuming inputs have ng-model specified, and this conbination will successfully login

    // 테스트 데이터를 통해 브라우져 로그인 submit 버튼 click 이벤트 발생시킴 

    input('email').enter('test@test.com');

    input('password').enter('password');

    element('submit').click();


    // logged in route

    // 로그인이 성공했을 경우 대시보드 패스에 있는지 검증

    expect(browser().location().path()).toBe("/dashboard");


    // my dashboard page has a label for the email address of the logged in user

    // DIV의 id="email"의 html 값이 로그인시 입력한 값인지 검증 

    expect(element('#email').html()).toContain('test@test.com');

  });


  it('should keep invalid logins on this page', function() {

    // 로그인 화면 인지 검증

    browser().navigateTo('#/login');

    expect(browser().location().path()).toBe("/login");


    // assuming inputs have ng-model specified, and this conbination will successfully login

    // 잘 못된 값을 넣었을 가지고 로그인 버튼 클릭 

    input('email').enter('invalid@test.com');

    input('password').enter('wrong password');

    element('submit').click();


    // DIV id="message"에 failed메세지 있는지 검증 

    expect(element('#message').html().toLowerCase()).toContain('failed');


    // logged out route

    // 제대로 로그인을 못했으므로 로그인 패스 그대로인지 검증 

    expect(browser().location().path()).toBe("/login");

  }); // end it 

}); // end describe 



3. Mocking Services and Modules in AngularJS

  - 목(Mock) 서비스를 이용한 테스트 방법 

  - 목 오브젝트를 사용하는 이유는 외부적인 요소 의존적인 부분을 제거하고 테스트 하기 위해서 이다 

  - ngMock.$httpBackend : service에서 http login 호출을 하면 응답 JSON 객체를 넘겨주는 mock 객체를 만든다 

it('should get login success',

  inject(function(LoginService, $httpBackend) {


    $httpBackend.expect('POST', 'https://api.mydomain.com/login')

      .respond(200, "[{ success : 'true', id : 123 }]");


    LoginService.login('test@test.com', 'password')

      .then(function(data) {

        expect(data.success).toBeTruthy();

    });


  $httpBackend.flush();

});



<참조>

  - 원문 : Best Practices in AngularJS

  - AngularJS Controller Unit Test 방법

  - AngularJS Service Unit Test 방법

  - Full Spectrum Testing(E2E) in Angular with Karma (필독)

  - Introducing E2E Testing in Angular 동영상

    

posted by 윤영식
2013. 10. 10. 11:19 MongoDB/Concept

MongoDB가 나오게 된 이유와 개념/구성에 대하여 알아보자



어느 방향을 선택할 것인가

  - 500만 : trend

  - 1000만 : culture

  - 2000만 : 안끼면 소외된다 

  소셜네트워크가 IT와 만나고 다시 스마트폰을 만나면서 새로운 방향을 제시하였다. 이런 방향을 지원하기 위하여 많은 OSS(Open Source Software)가 나왔고, HTTP 통신(Stateless)을 통하여 유입되는 어마어마한 사용자를 수용하기(규모의 확장) 위하여 사람들의 생각을 저장하고 활용해야 하는 필요성이 커졌다. 사람의 생각을 Key=Value로 많은 데이터를(BigData) 저장할 수 있어야 한다. 이러한 사상은 예전의 Client-Server 사상이 아닌 HTTP Web상의 통합과 유입을 처리하는 SNS(Social Network Service), SND(Social Network Data), SNG(Social Network Game) 사상으로 가고 있다. SNS는 HTTP 프로토콜을 통하여 Big Data Stream을 만들어 Real Time으로 반응하는 것이다. 



기술의 흐름에 따른 선택

1) 전통적 웹기술

  - 전통 pc application 개발 기술

  - static 기술

  - dynamic 기술


3) 모바일 기술

  - embedded 기술 (고립된 기술) : iphone, android 등 mobile application 개발 기술 또는 Sencha 기술 

  - open api 기술 : web services 기술 


4) 웹앱(WebApp) 기술 : 3세대 웹

  - SPA(스타) : single page application 기술 (javascript, json)

  - MVC framework을 client 사이트로 내림 = Fluent MVC = Stream

    (스트림은 융합을 쉽게 이루어지게 한다. 예) 디지털TV, 무인자동차) 

    + client Controller는 없다. View가 Controller를 대신한다 

    + MOV : Model + Operation + View가 있다. 여기서 Operation = Functional 이 된다. (Functional Language, Async) 

    + DRY : Don't Repeatly Yourself 반복하지 않는 프레임워크    

    



  - SNS : Modern Web = Smart Device (TV, Phone등), 반대개념 일반 Web 임

  - Real Time 기술 : websocket 기술이 들어감 

  - Javascript가 소통 언어가 되고 JSON이 데이터 포멧이 되어 end-to-end에서 커뮤니케이션의 실체가 된다. 

    + 기존 ORM-Object Relational Mapping- 기술은 사용하지 말자~~

    + client : 메모리, 브라우져 사용

    + server : 모듈 서비스. 일반 모듈의 대표는 Node.js 이고, special module = DB module    

  - Stream의 통합 : Edge 통합

    + Facebook이 초창기 사용하였으나 지금은 웹앱, mongodb를 사용하지 않는다 (초기 기술 적용에 대한 실패일까?)


  - 최종 모습

    + client : Backbone, Handlebar(mustache)

    + server : Node.js, Redis(맵데이터=view테이블), MongoDB(MapReducing, NoSQL)

      MapReduce를 통하면 BI(Business Intelegence)를 구현한다

    + bigdata : 큰단위 - Hadoop + HBase,  작은단위 - Redis + MongoDB로 보아도 된다

    + Agile 개발 : Schemaless 로 에자일 개발 프로세스에 적합하다. 작은 변화에도 기만하게 반응할 수 있다

     


MongoDB 이해하기 

  - 기존 RDB

    + join = projection 

    + 성능 optimization(최적화)를 위하여 schema와 join이 중요함 : 정규화의 필요(normalization)   

    

 

  - 현재 상황 

    + 성능 최적화를 위하여 찢어 놓았던(normalization)것을 고객 서비스를 위하여 다시 합쳐야 한다

    + SQL문이 8Kbytes 가 넘어 가고 현재 무한히 SQL을 쓸 수 있지만 늪이 된다. (튜닝을 위하여 컨설턴트가 필요해짐)

   - MongoDB

    + Table = Collection

    + Tuple = Document    

    

    


MongoDB 구성

  - Replica Set을 기본으로 가져가자 : HTTP를 통하여 묶는다 (Primary-Secondary)

  - Fail-Over를 통하여 High Availability(HA)를 충족한다 

    

  

  - Scale-Out 확장을 통하여 무한히 대응할 수 있다

    + Sharding을 통하여 데이터를 분산하여 처리하고 그안에 Replica-Set이 구성된다 

    + mongos는 router 이다     

    


  - 전체 MongoDB 구조 

    + Config Server : Replica-set, Shard 구성

    + Replica-set : Fail-Over (primary, secondary)

    + Sharding : Data Scale-out 

    

  


SNS는 이제 RealTime Stream Data 시대

  - 하나로 보이지만 여러곳에서 데이터가 통합되어 온다. 

    하기 그림에서 영상 화면 하나가 여러 곳에서 데이터가 모여져서 보여지듯이.

  - mongoDB의 GridFS가 된다 

  

   


MongoDB 개념 및 용어 정리 


MongoDB는 Service로 접근해야 한다! (애플리케이션 접근 옳지 않아~~)



<참조>

  - .Net Framework Frontier Facebook Group

  - KTH  MongoDB 어떻게 사용할 것인가?

posted by 윤영식
2013. 10. 8. 09:06 Lean Agile Culture/Lean Startup

장강일님 페이스북 글 발췌 


"더 나은 의사결정을 내리는 7단계"

1. Set a deadline.

의사결정의 반대는 지지부진 의사결정을 미루는 것이다. 우유부단함(indecision)은 다른 사람들이 당신을 대신해 의사결정내릴 때까지 망설이게 만든다. 그러므로, 중요한 의사결정은 마감일(deadline)이 있어야 한다. 당신이 "자율적으로(will)"으로 결정을 할 수 있는 최종기한을 뜻한다.

2. Discover the knowns.

최선의 판단을 하기 위해선, 의사결정에 필요한 정보들을 획득해야한다. 그런데, 정보를 취합할 때, 수확체감(diminishing returns)의 법칙이 적용된다는 것을 상기하라. 너무 세세한 디테일들에 과도하게 사로잡히기 시작할 때, 정보 수집을 멈추는 것이 좋다.

3. Gather relevant inputs.

의사 결정 이후에, 다른 사람들이 그 결정된 내용을 실행해야 한다면, "반드시" 그 사람들의 관점을 의사결정 내리기 전에 반영하라. 이 과정을 빠뜨리게 되면, 그 사람들은 당신이 내린 의사결정을 자신들의 것으로 수용(own)하지 않을 것이다. 당연히 매끄럽게 실행되기도 힘들다.

4. Decide.

결정하다(decide)의 어원은 라틴 말로 절단(cut off)에서 나왔다. 결정은 논쟁을 중단시키는 것이며, 다른 결정을 내릴 가능성들을 쳐내는 것이다. 일단 결정을 내리면, 바로 실행으로 나아가라. 그렇지 않으면, 당신이 내린건 진짜 의사결정이 아니다.

5. Explain your reasoning.

(3단계에서) 다른 사람들이 자신들의 관점을 피력할 기회를 주었다면, 이제 그들에게 왜 다른 옵션을 취하지 않고 이러한 의사결정을 내렸는지 설명하라. 그들은 기꺼이 당신의 의사결정에 따를 준비가 되어 있을 것이다.

6. Never second guess.

일단 의사결정을 내린 뒤에는 그 결정에 의구심을 갖지도 말고, 회의적인 시각을 지닌 사람들에게 심각하게 귀 기울이지 마라. 실제 실행에 온 힘을 기울이고, 결과를 얻기 전까지는 말이다. 이미 내려진 판단에 의심을 가질수록 우유부단함에 물들게 되며, 자연히 실행력도 떨어지게 된다.

7. Observe the results

정말로 진실되게 그리고 갖은 힘을 다해 실행에 주력했다면, 이제 한 걸음 뒤로 물러나 그 결과를 엄정하게 살펴보라. 기대하는 성과를 거두었다면 축하하라. 만약 그렇지 않다면, 8단계로 넘어가라.

8. Adjust the decision

명심하라!!! 꽉 막힌 완고함(Bullheadedness)은 우유부단함 못지 않게 당신을 대책없는 사람으로 전락시킨다. 오히려, 더 많은 자원과 노력을 낭비하게 만들면서 말이다. 그러므로, 기대하는 결과를 얻지 못했다면, 1단계로 다시 돌아가라. 그간 실행하면서 터득한 경험을 활용해 더 다듬고, 더 나은 의사결정을 내려라.



<참조>

  - 장강일 페이스북

posted by 윤영식
2013. 10. 6. 21:09 Protocols/OAuth

플랫폼의 핵심 기능중 하나가 Open API이다. 해당 기능을 이용하려면 인증(Authetication)하고 권한 부여(Authorization)할 수 있는 약속을(프로토콜) 새롭게 만들어야 했고 그 결과물이 OAuth이다. 초기 1.0에서 보안결함을 해결한 1.0a 와 최신 2.0 버전을 가장 많이 사용을 하고 있다. 




개념

예전의 OpenID방식은 인증(로그인)만을 처리하는 것이고, RESTful Open API 호출시 권한 체크를 하지 않으므로 이에 대한 권한체크도 필요하다. OAuth는 두개의 역할을 다 수행한다 

  - 인증 : 우리가 흔히 사용하는 Login시 ID/Password 를 통하여 인증을 받는다 - Request_Token

  - 권한 : 권한이 있는 기능만을 호출할 수 있다 - Access_Token



버전

  - v1.0 : 보안 결함 발견 v1.0a가 IETF 표준 프로토콜, 웹만 인증 가능

  - v2.0 : v1.0의 key_signature 복잡함등의 제거, 웹뿐만 아니라 애플리케이션도 인증 가능 



V1.0 

  - User : 특정 서비스에 사용하는 유저 예) Smart Visualization 사용자

    Consumer : 특정 서비스이면서 OAuth 제공자(서비스 제공자)에게 인증과 권한을 요청 예) Smart Visualization

    Service Provider : OAuth 제공자 예) 트위터

  


  - Request Token 으로 인증을 처리하고 Access Token으로 권한

  - 장점

1. 컨슈머가 아이디/패스워드를 가지지 않고 API를 사용할 수 있음

2. 필요한 API에만 제한적으로 접근할 수 있도록 권한 제어 가능

3. 사용자가 서비스 프로바이더의 관리 페이지에서 권한 취소 가능

4. 패스워드 변경 시에도 인증 토큰은 계속 유효함.

  - 총정리

  
- 동영상 강좌 
  


  

V2.0 

  - 3가지 종류 존재 : bearer token 방식 사용. 나머지 두가지는 계속 수정중 

  - 간단해 졌고, 더 많은 인증 방법을 제공, 대형 서비스로 확장을 지원한다 

  


사용하기 

  - 다양한 언어별 라이브러리 존재 



<참조>

  - OAuth v2.0 만능 도구상자 (KTH)

  - OAuth v1.0와 춤을 (NHN)

'Protocols > OAuth' 카테고리의 다른 글

[OAuth] EveryAuth 이용해서 Twitter 인증하기  (0) 2013.10.05
posted by 윤영식
2013. 10. 5. 20:28 Protocols/OAuth

OAuth 트위터에서 Consumer/Acces Key를 생성한 후, Node.js 상에서 EveryAuth를 이용하여 인증방법에 대해서 알아보자 



Twitter 키 생성하기 

  - https://dev.twitter.com/apps/new  에서 생성함

  - 이름, 설명, url -예, http://sv.mobiconsof.co.kr -, 약관체크, CAPCHA등록하고 submit을 하면 키생성 화면이 나온다 

  - 추가로 "access token"을 생성할 수도 있다

     단 여기서 중요한 것은 "Callback URL"을 입력하는 것이다. (sv.mobiconsoft.co.kr은 /etc/hosts파일에 설정한 테스트 도메인)

  - 만들어 놓은 자신의 키들은 https://dev.twitter.com/apps 에서 애플리케이션 별로 확인을 할 수 있다 



Twitter 키 사용하기 

  - Node.js에서 everyauth 모듈을 통하여 사용하는 예를 보자

  - Key 생성시 url로 "http://sv.mobiconsoft.co.kr" 를 입력하였다면 - 각자의 public 또는 test url을 입력하면 된다 - hosts 파일에 등록하고 dig 와 ping 명령으로 확인을 한다 

$ sudo vi /etc/hosts

127.0.0.1    sv.mobiconsoft.co.kr


// ping 성공

$ ping sv.mobiconsoft.co.kr

PING sv.mobiconsoft.co.kr (127.0.0.1): 56 data bytes

64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.044 ms

64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.051 ms

 

  - Express.js, Angular.js 와 Bootstrap을 이용한 코드를 사용할 것이다. 

$ git clone https://github.com/ganarajpr/express-angular.git && cd express-angular

$ 소스 수정하기

  - app.js 안에서 트위터에 대한 consumerKey와 consumerSecret을 입력한다 

everyauth

    .twitter

    .consumerKey('2yQ.....img')

    .consumerSecret('TbhKnP.....fQ1gyvUO4')

    .findOrCreateUser( function (sess, accessToken, accessSecret, twitUser) {

        console.log('twitter User data is', twitUser);

        return usersByTwitId[twitUser.id] || (usersByTwitId[twitUser.id] = addUser('twitter', twitUser));

    })

    .redirectPath('/');

  - Node.js 를 수행한다 

$ sudo node app.js

  - 브라우져에서 호출하기 : 우측 "Login"에서 "Sign in with Twitter" 버튼을 클릭한다 

 - 트위터 승인 화면이 나온다 : "애플리케이션 승인" 버튼을 클릭하면 다시 원래 화면으로 돌아온다 

 - Node.js 콘솔에 뿌려진 사용자 정보를 보자 : 해당 정보를 저장하여 애플리케이션에서 사용을 하면 된다 

$ sudo node app.js

starting step - getRequestToken

...finished step

starting step - storeRequestToken

...finished step

starting step - redirectToProviderAuth

...finished step

starting step - extractTokenAndVerifier

...finished step

starting step - getSession

...finished step

starting step - rememberTokenSecret

...finished step

starting step - getAccessToken

...finished step

starting step - fetchOAuthUser

...finished step

starting step - assignOAuthUserToSession

...finished step

starting step - findOrCreateUser

twitter User data is { id: 48553254,

  id_str: '48553254',

  name: 'Software 행복공동체 키우기',

  screen_name: 'nulpulum',

  location: 'Seoul Korea',

  description: '자건거타기 실용주의프로그래머 에자일정신 글로벌소프트웨어만들기 행복공통체키우기 지식나눔운동',

  url: 'http://t.co/wunTkx6q',

  entities: { url: { urls: [Object] }, description: { urls: [] } },

  protected: false,

  followers_count: 98,

  friends_count: 533,

  listed_count: 0,

  created_at: 'Fri Jun 19 00:44:39 +0000 2009',

  favourites_count: 12,

  utc_offset: 32400,

  time_zone: 'Seoul',

  geo_enabled: true,

  verified: false,

  statuses_count: 506,

  lang: 'ko',

  status:

   { created_at: 'Fri Oct 04 12:15:34 +0000 2013',

     id: 386102292232937500,

     id_str: '386102292232937472',

     text: 'I signed up for Human-Computer Interaction from @ucsandiego on @Coursera! https://t.co/FdQc2h654C #hci',

     source: '<a href="http://twitter.com/tweetbutton" rel="nofollow">Tweet Button</a>',

     truncated: false,

     in_reply_to_status_id: null,

     in_reply_to_status_id_str: null,

     in_reply_to_user_id: null,

     in_reply_to_user_id_str: null,

     in_reply_to_screen_name: null,

     geo: null,

     coordinates: null,

     place: null,

     contributors: null,

     retweet_count: 0,

     favorite_count: 0,

     entities:

      { hashtags: [Object],

        symbols: [],

        urls: [Object],

        user_mentions: [Object] },

     favorited: false,

     retweeted: false,

     possibly_sensitive: false,

     lang: 'en' },

  contributors_enabled: false,

  is_translator: false,

  profile_background_color: '9AE4E8',

  profile_background_image_url: 'http://abs.twimg.com/images/themes/theme16/bg.gif',

  profile_background_image_url_https: 'https://abs.twimg.com/images/themes/theme16/bg.gif',

  profile_background_tile: false,

  profile_image_url: 'http://a0.twimg.com/profile_images/2956822008/b20249749c11917f4ce9e29263ba1b92_normal.jpeg',

  profile_image_url_https: 'https://si0.twimg.com/profile_images/2956822008/b20249749c11917f4ce9e29263ba1b92_normal.jpeg',

  profile_banner_url: 'https://pbs.twimg.com/profile_banners/48553254/1355144673',

  profile_link_color: '0084B4',

  profile_sidebar_border_color: 'BDDCAD',

  profile_sidebar_fill_color: 'DDFFCC',

  profile_text_color: '333333',

  profile_use_background_image: true,

  default_profile: false,

  default_profile_image: false,

  following: false,

  follow_request_sent: false,

  notifications: false }

...finished step

starting step - compileAuth

...finished step

starting step - addToSession

...finished step

starting step - sendResponse

...finished step



<참조> 

  - How to get twitter key

  - Express에서 EveryAuth 사용하기 

  - Facebook에서 EveryAuth 적용하기

'Protocols > OAuth' 카테고리의 다른 글

[OAuth] 인증과 권한 개념잡기  (0) 2013.10.06
posted by 윤영식
2013. 10. 5. 15:42 My Services/Smart Visualization

디자인이라고 말하면 일반 개발자들은 포토샵과 일러스트레이터를 다루는 그래픽 디자이너를 생각하게 된다. 하지만 특정 도메인을 표현하는 제품 입장에서 디자이너와 함께 일하기 위하여 일반적으로 기대하는 수준은 사뭇 다르다. 나 또한 B2B 솔루션을 3개 정도 런칭해 보았지만 일반 그래픽만 다루던 디자이너와 일했을 때 도메인 컨텍스트를 이해시키고 설득하는데 많은 시간과 노력 그리고 스트레스를 어떻게 받게 되었는지 잘 안다. 어떻게 하면 훌륭한 디자인을 할 수 있는지? 자신의 Golden Path는 어떻게 만들 수 있는지 알아보자 



프러덕트 디자이너가 작업전 알아야 할 것은?

  - 프러덕트 디자인을 하기전 디자이너는 회사의 미션과 비젼을 알아야 한다 (참조)

  - 프러덕트의 아키텍쳐를 알아야 한다 

    아키텍쳐는 기술적 표현보다는 좀 더 추상화되고 구조화된 표현이다 

    

   + Facebook 예

   

  + 구조화된 표현

   



프러덕트 디자인의 접근법

  - 디자인시 4개의 층이 있고 Top->Down으로 접근한다 

  - Outcome -> Structure -> Interaction -> Visual

    최종 결과물의 모습 : 우리가 해결하길 원하는 사람들의 문제는 무엇인가? 서비스(프러덕트)를 정확히 정의한다 (서비스 참조)

    레이아웃 구조 : 의도한 시스템 구축을 위해 필요한 컴포넌트와 그들의 관계에 대해서 맵핑하는 디자인을 한다  

    화면 상호작용 : 애니메이션효과 화면전환의 흐름등의 상호작용을 자세히 디자인한다

    시안의 시각화 : 아름다운 그리드, 색감, 타이포그라피, 아이콘등의 look and feel의 아름다움을 디자인한다 

  



2013년 트랜드인 Flat Design 관점 

  - 애플 iOS7에서 플랫디자인을 선보이고, 구글은 전사 서비스를 플랫디자인으로 도배하고 있다. 

  - 프러덕트 디자이너가 Visual 단계에서 바라본 플랫디자인의 요소 5가지 (참조)

     + Use of simple elements 

        심플한 아이콘 : 사각, 원형의 단순 모형에 배치, 컬러, 형태로 표현 -> 사용자가 UI 를 쉽게 인식하고 사용토록 만든다 

     + Absence of depth 

        배제된 효과 : 그림자, 입체감, 그라데이션등의 현실감있게 하는 3D적 효과를 배제하여 직관적으로 만든다  

     + Typography

        타이포그래피 : 화려한 폰트를 배제하고 간결한 폰트를 사용한다. 예) 산세리프 

     + Color

        단순한 칼라 : 2~3가지 또는 5~6가지의 색상만을 사용하여 간결하고 과감하고 표현한다. 핑크, 그린, 블루가 대세  

     + Minimalism

        미니멀리즘 : 미니멀리즘과 부합 - 간단하고 심플하게 표현으로 다양한 웹, 모바일에 적응이 쉽다  

    



서비스 디자인이라는 관점

  - 서비스을 만들 때 우리는 어떻게 접근하여야 할까 단지 디자인이 문제가 아니라 UX라는 사용자 경험을 고려하여 사람과 컴퓨터의 상호작용을 생각해 보아야 하고, 우리가 만들려는 프로덕트나 서비스에 어떻게 녹아 있어야 하는지 연구하고 새롭게 창조하는 과정에 대해 알아보자 

  - 서비스 디자인 이란 무엇인가? - 개념 이해


  - 서비스 디자인을 어떻게 하는지 알아보자 - 접근 방법 


- 서비스 디자인 프로세스 
  Discover -> Define -> Develop -> Deliver

  



InfoGraphic 에서 바라본 디자인 관점 예  

  - 정보를 시각화 하는 인포그래픽에 대한 접근법은 어떻게 될까? 10단계에 대해 알아보자 (참조)

  1) 자료 모으기 - Gathering Data

  2) 모든 것을 알기 - Reading Everything

  3) 서사구조를 찾아내기 - Finding the narrative

  4) 문제를 확인하기 - Identifying Problems

  5) 계층을 만들기 - Creating Hierarchy

  6) 뼈대를 만들기 - Building a Wireframe

  7) 표현형식 고르기 - Choosing a Fromat

  8) 시각적 접근법 결정하기 - Determining a Visual Approach

  9) 개선과 시험 : Refinement and Testing

  10) 세상에 출시하기 : Releasing into the World 

  


  - 프러덕트 디자이너 관점에 인포그래픽을 바라 본 다면 다음과 같이 나눌 수 있지 않을까 개인적으로 작위적 분류를 해본다 

     (특성상 상호작용은 제외함)

     1) ~ 4) == OutCome

     5) ~ 6) == Structure

     7) ~ 9) == Visual 

  - 서비스 다지안 관점에서도 작위적으로 분류해 본다 

     1) ~ 4) == 탐색 

     5) ~ 6) == 정의 

     7) ~ 9) == 만들기

     10) == 출시하기  



프러덕트 디자인을 한다는 것은 이제 예전의 단순 그래픽 디자인에서 -> 결과물(프러덕트)의 전반적인 미션과 비젼을 이해하고 -> 어떻게 사람과 컴퓨터가 상호작용해야 하는지 이해한 후 사용자 경험을 극대화 하는 작업으로 변하였다. 최근은 모바일기기의 확대로 그 경험의 비쥬얼에 플랫디자인이 대세를 이루고 있으며 이는 사용에게 궁극적으로 감정적 만족감과 행복감을 주는 방향으로 가고 있다.



<참조> 

  - 원문 : 디자인의 드리블화 

  - 플랫 디자인의 특징

  - 몇가지 플랫 디자인 예들

  - 놀라운 인포그라픽을 만드는 10단계

  - 빅데이터 관점의 서비스 디자인

  - HCI Cousera 강좌 : Human Computer Interaction

posted by 윤영식
2013. 9. 30. 16:57 My Services/Smart Visualization

차트는 많은 정보를 내포한다. 사람들끼리 차트를 공유하고 싶을 경우 보통은 엑셀에서 작업을 하고, 보고서를 MS-Word 또는 HWP로 만들어서 메일을 보내거나 채팅창으로 파일을 전송하게 된다. 여러 복잡한 과정을 통하여 자신의 데이터에 대하여 의미를 내포한 차트로 공유하기까지의 과정이 너무 길고 많은 시간이 소요된다. 그리고 지속적인 관심을 가진 데이터라면 이들에 대한 히스토리는 개별 문서마다 보관이 될 것이고, 합쳐서 보고자 할 때 번거로운 재작업을 거쳐야 한다. 우리가 사용하는 페이스북이나 기존 SNS 사용방식처럼 데이터를 시각화한 차트에 대해 공유하여 볼 수 있고, 코멘트를 달고 관련 차트끼리 연결지어서 더 낳은 정보로 만들어 갈 수 있으면 얼마나 재미있을까? 관련성있는 차트들을 모아 보면서 회의에 사용하거나 바로 스마트 기기에서 확인함으로써 업무 효율화를 가져올 수도 있다


이러한 가치하에서 Smart Visualization에 대한 목업화면을 그려 보았다. 



Share Place 

  - Search 입력에 어느 그룹, 코멘트, 의견등의 내용을 검색하여 관련있는 차트 목록을 볼 수 있다

  - 공유하는 공간이다. 차트별로 상단에 자신의 것인지, 어느 그룹과 공유되는 것인지 보이고 등록 시간이 나온다 

     Place : personal 또는 group-01

  - 상단 Place 또는 시간을 클릭하면 해당 차트에 대한 상세 내역을 보여주는 화면이 나온다 

  - 차트 하단에는 해당 차트에 대한 Comment와 관련 차트(Related Chart)를 연결지을 수 있다 

  - 상단 우측에는 현재 채팅진행중이 확인되지 않은 내역 건수와 메세지 건수를 Badge 로 보여준다



Register Place

  - 일정 형식의 .csv 파일을 업로드한다 

  - 해당 차트의 Place (속할 그룹)과 차트 종류를 선택하고 등록한다 



Slide Menu

  - "Smart Visualization" 좌측의 아이콘을 클릭하면 메뉴가 나온다 

  - 개인 또는 그룹을 검색하여 친구맺기를 할 수 있고, 상대가 응답을 해주면 상호 연결이 되어 Share Place에서 차트를 공유한다 

  - 기존 대화목록이나 메세지를 열람할 수 있다

  - 메뉴 상단에 "사용자"검색이나 "그룹" 검색이 들어가겠다

데이터 시각화 차트의 공유에 벗어나는 레이아웃은 제거할 것이다.



* 개념이 유사한 사이트

  https://plot.ly/


  https://infogr.am/


posted by 윤영식
2013. 9. 28. 17:45 Languages/JavaScript

자바스크립트의 상속과 성능향상을 위하여서는 Prototype Pattern을 갖는 Prototypal Inheritance를 이용한다.




개념

  - ECMAScript 5에서 Object.create 로 가능하다 

var myCar = { name: "Ford Escort", drive: function () { console.log( "Weeee. I'm driving!" ); }, panic: function () { console.log( "Wait. How do you stop this thing?" ); } }; // Use Object.create to instantiate a new car var yourCar = Object.create( myCar ); // Now we can see that one is a prototype of the other 

console.log( yourCar.name );

 

  - 오브젝트 확장할 때 

var vehicle = { getModel: function () { console.log( "The model of this vehicle is.." + this.model ); } };

// 두번째 인자에서 확장한다 Mixin... var car = Object.create(vehicle, { "id": { value: MY_GLOBAL.nextId(), // writable:false, configurable:false by default enumerable: true }, "model": { value: "Ford", enumerable: true } }); 


  - Object.create를 사용하지 않고 Construct Function의 prototype 프로퍼티를 이용하여 prototypal inheritance 구현 

var vehiclePrototype = { init: function ( carModel ) { this.model = carModel; }, getModel: function () { console.log( "The model of this vehicle is.." + this.model); } }; function vehicle( model ) { function F() {};  // 일급 클래스 펑션의 prototype을 vehiclePrototype객체로 정의 

// 상단 이미지에서 Foo의 프로퍼티인 propertype 변수에 Foo.prototype 객체를 참조한다

F.prototype = vehiclePrototype;

// new를 하면 vechiclePrototype객체가 f 변수에 할당 된다 var f = new F(); f.init( model ); return f; } var car = vehicle( "Ford Escort" ); 

console.log(car.getModel());

다른 방법으로 proto 전달 

var beget = (function () { function F() {} return function ( proto ) { F.prototype = proto; return new F(); }; 

})();



다양한 Object creation으로 메소드 확장하기

  - 메소드안에 정의 

    + getInfo는 Car 생성때 마다 생성되므로 메모리를 차지한다 (안좋음) 

function Car(make, model, level, color, warranty) {
    this.make     = make;
    this.model    = model;
    this.level    = level;
    this.color    = color;
    this.warranty = warranty;
    
    this.getInfo = function() {
        return this.make + ', ' + this.model + ', ' + this.level + ', '+ this.color + ', ' + this.warranty;
    };
}
 
var aCar = new Car('Acura', 'TL', 'Sport', 'blue', 5);

console.log(aCar.getInfo());  //displays Acura, TL, Sport, blue, 5 


  - 메소드를 외부에서 Prototype에 지정하기 

    + 필요한 시점에 메소드를 추가할 수 있다 예) Car.prototype.accelerate 

function Car(make, model, level, color, warranty) {
    this.make     = make;
    this.model    = model;
    this.level    = level;
    this.color    = color;
    this.warranty = warranty;
}
 
Car.prototype.getInfo = function() {
  return this.make +','+ this.model +','+ this.level +','+ this.color +','+ this.warranty;
};
 
var aCar = new Car('Acura', 'TL', 'Sport', 'blue', 5);
alert(aCar.getInfo());  //displays Acura, TL, Sport, blue, 5
 
Car.prototype.accelerate = function() {
    return 'How fast?';
};

console.log(aCar.accelerate()); //displays "How fast?"


  - Prototype Pattern을 이용

    + prototype에 필요한 메소드를 Literal 객체로 지정한다 

function Car(make, model, level, color, warranty) {
    this.make     = make;
    this.model    = model;
    this.level    = level;
    this.color    = color;
    this.warranty = warranty;
}
 
Car.prototype = {
    getInfo: function () {
      return this.make +','+ this.model +','+ this.level +','+ this.color +','+ this.warranty;
    }

};


  - Revealing Prototype Pattern을 이용

    + prototype에 객체를 생성하여 할당한다 

var UsedCar = function(mileage) {
    //Define a variable unique to each instance of UsedCar
    this.mileage = mileage;
};
 

UsedCar.prototype = new Car('Honda', 'Civic', 'LX', 'gray', 2);


// Revealing Module Pattern 형식

UsedCar.prototype = function () { }();



<참조>

  - 원문 : The Prototype Pattern

  - Some Prototype Pattern 사용하기 

  - Revealing Prototype Pattern 사용하기

  - 드미트리 소스시코브의 JavaScript Core

posted by 윤영식