using angular.js
ITWISE Consulting
Engineering center
선임 엔지니어 김태희






/* 
 * 어디는 이렇게 Server Side로 제목을 렌더링
 * ex) EL Exp
 */
<h3>${title}</h3>
/*
 * 어디는 이렇게 처리하고...
 * ex) scripting
 */ 
<h3 class="title"></h3>
// scripting
$('.title').text('title text');<c:forEach items="${articles}" var="article">
  <article>
    <div class="article-header">${article.header}</div>
    <div class="article-content">${article.content}</div>
    <!-- 엘리먼트 노출 여부를 Server Side Rendering에서 결정 -->
    <c:if test="${article.commentCount > 0}">
      <button class="comment-load-button">댓글 불러오기</button>
    </c:if>
    <!-- ajax 로딩 후 아래 영역에 html append -->
    <div class="article-comments"></div>
  </article>
</c:forEach>


'추가' 버튼 누르면 할 일이 추가
해야 할 일의 해당 항목을 클릭하면 완료된 일로 이동
해야 할 일의 삭제 버튼을 누르면 해당 일 삭제
할 일 갯수의 변화가 있을 때 마다 맨 아래 할 일 갯수 영역이 갱신되어야 한다.
현재는 localStorage 사용, 추후 RESTful API 연동 처리
<!DOCTYPE html>
<html>
  <head>
    <title>Sample Todo App</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div class="container">
      <header class="todo-header">
        <h3>Sample Todo App</h3>
      </header>
      <section class="panel">
        <h3>할 일 추가하기</h3>
        <form class="new-todo">
          <input type="text" id="todo-title" placeholder="할 일을 입력하세요!"> 
          <button class="btn btn-add pull-right">추가</button>
        </form>
      </section>
      <section class="panel">
        <h3>해야 할 일</h3>
        <ul class="todos-inprogress">
          <!-- 반복되는 부분 -->
          <li class="panel todo">
            <label><input type="checkbox">http 공부하기</label> 
            <button class="btn btn-remove pull-right">삭제</button>
          </li>
          <li class="panel todo">
            <label><input type="checkbox">ionic 정리</label> 
            <button class="btn btn-remove pull-right">삭제</button>
          </li>
        </ul>
      </section>
      <section class="panel">
        <h3>완료한 일<button class="btn btn-default inline pull-right todo-complete-clear-button">비우기</button></h3>
        <ul class="todos todos-completed">
          <!-- 반복되는 부분 -->
          <li class="panel todo">발표자료 준비</li>
        </ul>
      </section>
      <aside class="panel todo-info">
        <strong class="todo-total-count">3</strong>개 중 <strong class="todo-complete-count">2</strong>개를 완료했습니다.
      </aside>
    </div>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script src="script.js"></script>
  </body>
</html>'use strict';
(function($){
  var todos = [];
  
  // localStorage에서 저장된 todos 로딩
  if(window.localStorage && window.localStorage.todos !== undefined){
    todos = JSON.parse(window.localStorage.todos);
    console.log(localStorage.todos);
    console.log(todos.length)
  }
  
  function sync(){
    window.localStorage.todos = JSON.stringify(todos);  
    console.log(localStorage.todos);
  }
  
  function updateTodoInfo(){
    var todoCompleteCount = 0;
    for(var i = 0; i < todos.length; i++){
      if(todos[i].isComplete){
        todoCompleteCount = todoCompleteCount + 1;
      }
    }
    
    // dom 접근해서 값 변경
    $('.todo-complete-count').text(todoCompleteCount);
    $('.todo-total-count').text(todos.length);
  }
  
  $(document).ready(function(){
    // todos render
    for(var i = 0; i < todos.length; i++){
      if(todos[i].isComplete){
        $('.todos-completed').append(
        '<li class="panel todo" id="todo-' + todos[i].id + '">' +
          todos[i].title + 
        '</li>'
        );
      }else{
        $('.todos-inprogress').append(
          '<li class="panel todo" id="todo-' + todos[i].id + '">' +
            '<label>' +
              '<input type="checkbox" class="todo-complete">' + todos[i].title + 
            '</label>' +
            '<button class="btn btn-remove pull-right">삭제</button>' +
          '</li>'
          );  
      } 
    }
    updateTodoInfo();
    
    // add event binding
    $('.new-todo').on('submit', function(event){
      var $todoTitle = $('#todo-title');
      var todoTitle = $todoTitle.val();
      var todo = {
        title: todoTitle, 
        id: new Date().getTime(),
        isComplete: false
      };
      $('.todos-inprogress').append(
        '<li class="panel todo" id="todo-' + todo.id + '">' +
          '<label>' +
            '<input type="checkbox" class="todo-complete">' + todo.title + 
          '</label>' +
          '<button class="btn btn-remove pull-right">삭제</button>' +
        '</li>'  
      );
      $todoTitle.val('');
      
      todos.push(todo);
      sync();
      updateTodoInfo();
      
      event.preventDefault();
    });
    
    // todo complete event
    $('.todos-inprogress').on('click', '.todo-complete', function(){
      var $completeTodo = $(this).parents('.todo');
      var todoId = $completeTodo.attr('id').replace('todo-', '');
      var todo, i;
      
      // 완료 처리
      for(i = 0; i < todos.length; i++){
        todo = todos[i];
        if(todo.id === parseInt(todoId)){
          todo.isComplete = true;
          break;
        }
      }
      
      $completeTodo.find('input[type=checkbox]').remove();
      $completeTodo.find('.btn-remove').remove();
      $('.todos-completed').append($completeTodo);
      
      sync();
      updateTodoInfo();
    });
    
    // remove event binding
    $('.todos-inprogress').on('click', '.btn-remove', function(){
      var $deleteTodo = $(this).parents('.todo');
      var todoId = $deleteTodo.attr('id').replace('todo-', '');
      $(this).parents('.todo').remove();
      
      for(i = 0; i < todos.length; i++){
        todo = todos[i];
        if(todo.id === parseInt(todoId)){
          todos.splice(i, 1);
          break;
        }
      }
      
      sync();
      updateTodoInfo();
    });
   
    $('.todo').on('click', '.btn-remove', function(){
      var $removeTargetTodo = $(this).parents('.todo');
      todos.splice($removeTargetTodo.index(), 1);
      $removeTargetTodo.remove();
      
      sync();
      updateTodoInfo();      
    });
    // completed remove all event
    $('.todo-complete-clear-button').on('click', function(){
      var inprogressTodos = [], i;
      for(i = 0; i < todos.length; i++){
        if(!todos[i].isComplete){
          inprogressTodos.push(todos[i]);
        }
      }
      
      todos = inprogressTodos;
      
      // html 비우기
      $('.todos-completed').html('');
      
      sync();
      updateTodoInfo();
    });
  });
})(jQuery);JAVASCRIPT에 HTML markup이 하드코딩 되어있음
'와 " 처리
markup을 고쳐야 하면 JAVASCRIPT 코드를 고쳐야 함
화면 조작에 따라 매번 DOM을 세세하게 조작해야 함
public class HelloWorldExample extends HttpServlet {
   @Override
   public void doGet(HttpServletRequest request, HttpServletResponse response)
               throws IOException, ServletException {
      // Set the response message's MIME type.
      response.setContentType("text/html;charset=UTF-8");
      // Allocate a output writer to write the response message into the network socket.
      PrintWriter out = response.getWriter();
 
      // Write the response message, in an HTML document.
      try {
         out.println("<!DOCTYPE html>");  // HTML 5
         out.println("<html><head>");
         out.println("<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>");
         String title = "helloworld.title";
         out.println("<title>" + title + "</title></head>");
         out.println("<body>");
         out.println("<h1>" + title + "</h1>");  // Prints "Hello, world!"
         out.println("<a href='" + request.getRequestURI() + "'><img src='images/return.gif'></a>");
         out.println("</body></html>");
      } finally {
         out.close();  // Always close the output writer
      }
   }
}<!-- templates -->
<!-- todo complete template -->
<script type="text/x-custom-template" id="todo-complete">
  <li class="panel todo" id="todo-{id}">
      ✓ {title}
  </li>
</script>
<!-- todo inprogress template -->
<script type="text/x-custom-template" id="todo-inprogress">
  <li class="panel todo" id="todo-{id}">
    <label>
      <input type="checkbox" class="todo-complete">{title}
    </label>
    <button class="btn btn-remove pull-right">삭제</button>
  </li>
</script>
<!-- todo info template -->
<script type="text/x-custom-template" id="todo-info">
  <strong class="todo-total-count">{totalCount}</strong>개 중 
  <strong class="todo-complete-count">{completeCount}</strong>개를 완료했습니다.
</script>
var templateUtil = {
  mapper: {},
  init: function(){
    // script element 중 type이 text/x-custom-template인 것을 template으로 등록
    var that = this;
    $('script').each(function(){
      if($(this).attr('type') === 'text/x-custom-template'){
        var id = $(this).attr('id');
        that.mapper[id] = $(this).text();
      }
    });
  },
  render: function(id, data){
    // 정규표현식을 이용해 {key} 형태의 string을 value로 치환
    var template = this.mapper[id];
    
    if(template !== undefined){
      for(var key in data){
        var binding = new RegExp('{' + key +'}', 'g');
        template = template.replace(binding, data[key]);
      }  
      return template;
    }else{
      throw new Error(id + ' template은 존재하지 않습니다.');
    }
  }
};render 구간 분리
if(window.localStorage && window.localStorage.todos !== undefined){
  todos = JSON.parse(window.localStorage.todos);
}
function sync(){
  window.localStorage.todos = JSON.stringify(todos);  
}
  
function render(){
  var i;
  var todoCompleteCount = 0;
  
  // init html
  $('.todos-completed').html('');
  $('.todos-inprogress').html('');
    
  // todos render
  for(i = 0; i < todos.length; i++){
    if(todos[i].isComplete){
      todoCompleteCount = todoCompleteCount + 1;
      $('.todos-completed').append(
        templateUtil.render('todo-complete', todos[i])
      );
    }else{
      $('.todos-inprogress').append(
        templateUtil.render('todo-inprogress', todos[i])
      );  
    } 
  }
    
  $('.todo-info').html(templateUtil.render('todo-info', {
    totalCount: todos.length,
    completeCount: todoCompleteCount
  }));
}
  
function syncAndRender(){
  sync();
  render();
}
  
$(document).ready(function(){
  // template parsing
  templateUtil.init();
    
  render();
    
  // add event binding
  $('.new-todo').on('submit', function(event){
    var $todoTitle = $('#todo-title');
    var todoTitle = $todoTitle.val();
    var todo = {
      title: todoTitle, 
      id: new Date().getTime(),
      isComplete: false
    };
      
    $todoTitle.val('');
      
    todos.push(todo);
    
    syncAndRender();
    event.preventDefault();
  });
    
  // todo complete event
  $('.todos-inprogress').on('click', '.todo-complete', function(){
    var $completeTodo = $(this).parents('.todo');
    var todoId = $completeTodo.attr('id').replace('todo-', '');
    var todo, i;
      
    // 완료 처리
    for(i = 0; i < todos.length; i++){
      todo = todos[i];
      if(todo.id === parseInt(todoId)){
        todo.isComplete = true;
        break;
      }
    }
      
    syncAndRender();
  });
    
  // remove event binding
  $('.todos-inprogress').on('click', '.btn-remove', function(){
    var $deleteTodo = $(this).parents('.todo');
    var todoId = $deleteTodo.attr('id').replace('todo-', '');
    
    var todo, i;
    for(i = 0; i < todos.length; i++){
      todo = todos[i];
      if(todo.id === parseInt(todoId)){
        todos.splice(i, 1);
        break;
      }
    }
      
    syncAndRender();
  });
});




...and many more...
var todos = JSON.parse(window.localStorage.todos);
var localStorage = window.localStorage;
// 데이터를 추가해도
todos.push(todo);
localStorage.todos = JSON.stringify(todos);
// 데이터를 삭제해도
todos.splice(todos, 1);
localStorage.todos = JSON.stringify(todos);
// 데이터를 수정해도
todos[3].title = '변경';
// 새로고침하면 수정한 데이터 날라감!



...and many more...








<!DOCTYPE html>
<html data-ng-app="todoApp">
  <head>
    <title>Sample Todo App</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div class="container" data-ng-controller="todoController" data-ng-cloak>
      <header class="todo-header">
        <h3>Sample Todo App</h3>
      </header>
      <div class="panel">
        <h3>할 일 추가하기</h3>
        <form data-ng-submit="add($event)">
          <input type="text" id="todo-title" placeholder="할 일을 입력하세요!" 
            data-ng-model="todoTitle"/>
          <button class="btn btn-add pull-right">추가</button>
        </form>
      </div>
      <div class="panel">
        <h3>해야 할 일</h3>
        <ul class="todos todos-inprogress">
          <li class="panel todo" 
            data-ng-repeat="todo in todos"
            data-ng-if="!todo.isComplete">
            <label>
              <input type="checkbox" data-ng-click="complete(todo)">{{todo.title}}
            </label>  
            <button class="btn btn-remove pull-right" 
              data-ng-click="remove($index)">삭제</button>
          </li>
        </ul>
      </div>
      <div class="panel">
        <h3>완료한 일<button class="btn" data-ng-click="removeCompleteAll()">비우기</button></h3>
        <ul class="todos todos-completed">
          <li class="panel todo"
            data-ng-repeat="todo in todos"
            data-ng-if="todo.isComplete">
              {{todo.title}}
            </li>
        </ul>
      </div>
      <div class="panel todo-info">
        <strong class="todo-total-count">{{todoTotalCount}}</strong>개 중 
        <strong class="todo-complete-count">{{todoCompleteCount}}</strong>개를 완료했습니다.
      </div>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.7/angular.js"></script>
    <script src="script.js"></script>
  </body>
</html>

'use strict';
var todoApp = angular.module('todoApp', []);
todoApp.controller('todoController', function($scope){
  $scope.todos = [];
  
  if(window.localStorage && window.localStorage.todos !== undefined){
    $scope.todos = JSON.parse(window.localStorage.todos);    
  }
  
  $scope.todoTotalCount = $scope.todos.length;
  $scope.todoCompleteCount = 0;
  
  // todos 데이터 변경을 감지하여 총 갯수 알아내고 localStorage에 갱신시키기
  $scope.$watch('todos', function(){
    $scope.todoTotalCount = $scope.todos.length;
    var todoCompleteCount = 0;
    var i;
    for(i = 0; i < $scope.todos.length; i++){
      if($scope.todos[i].isComplete){
        todoCompleteCount = todoCompleteCount + 1;
      }
    }  
    $scope.todoCompleteCount = todoCompleteCount;
    
    window.localStorage.todos = angular.toJson($scope.todos);
  }, true);
  
  $scope.todoTitle = '';
  
  // todo functions
  $scope.add = function(){
    $scope.todos.push({
      id: new Date().getTime(),
      title: $scope.todoTitle
    });
    
    $scope.todoTitle = '';
  };
  
  $scope.complete = function(todo){
    todo.isComplete = true;
  };
  
  $scope.remove = function($index){
    $scope.todos.splice($index, 1);
  };
  $scope.removeCompleteAll = function(){
    var inprogressTodos = [];
    for(var i = 0; i < $scope.todos.length; i++){
      if(!$scope.todos[i].isComplete){
        inprogressTodos.push($scope.todos[i]);
      }
    }
    $scope.todos = inprogressTodos;
  };
});
  
with service & dependency injection
'use strict';
var todoApp = angular.module('todoApp', []);
// todoStore service 정의
todoApp.service('todoStore', function(){
  return {
    save: function(todos){
      window.localStorage.todos = JSON.stringify(todos);                
    },    
    findAll: function(){
      if(window.localStorage && window.localStorage.todos !== undefined){
        return JSON.parse(window.localStorage.todos);    
      }else{
        return [];
      }    
    }
  };
});
// 위에서 정의한 todoStore를 주입받는다.
// 이걸로 직접 localStorage에 접근하는 코드를 제거
todoApp.controller('todoController', function($scope, todoStore){
  $scope.todos = todoStore.findAll();
  
  $scope.todoTotalCount = $scope.todos.length;
  $scope.todoCompleteCount = 0;
  
  // todos 데이터 변경을 감지하여 총 갯수 알아내고 localStorage에 갱신시키기
  $scope.$watch('todos', function(){
    $scope.todoTotalCount = $scope.todos.length;
    var todoCompleteCount = 0;
    var i;
    for(i = 0; i < $scope.todos.length; i++){
      if($scope.todos[i].isComplete){
        todoCompleteCount = todoCompleteCount + 1;
      }
    }  
    $scope.todoCompleteCount = todoCompleteCount;
    
    todoStore.save(angular.toJson($scope.todos));
  }, true);
  
  $scope.todoTitle = '';
  
  // todo functions
  $scope.add = function(){
    $scope.todos.push({
      id: new Date().getTime(),
      title: $scope.todoTitle
    });
    
    $scope.todoTitle = '';
  };
  
  $scope.complete = function(todo){
    todo.isComplete = true;
  };
  
  $scope.remove = function($index){
    $scope.todos.splice($index, 1);
  };
  $scope.removeCompleteAll = function(){
    var inprogressTodos = [];
    for(var i = 0; i < $scope.todos.length; i++){
      if(!$scope.todos[i].isComplete){
        inprogressTodos.push($scope.todos[i]);
      }
    }
    $scope.todos = inprogressTodos;
  };
});