47 4. 웹 애플리케이션 작성
hanmj edited this page 3 months ago

개요

웹 애플리케이션은 SPA(Single Page Application)로 동작하며 jsp로 작성한다.

웹 애플리케이션의 메인 페이지의 html의 구조는 다음과 같다.

<html>

<head>
<!-- 폰트, css 등의 정적 리소스 링크 -->
</head>

<body>
<!-- 공통 컨텐트
     기능별 컨텐트
-->

<!-- 스크립트 링크 -->
<script src="..."></script>
<script src="..."></script>
<!-- 페이지 스크립트 -->
<script>
...
$(function() {
...
});
</script>
</body>

</html>
  • 정적 리소스의 링크는 <head ../> 에 위치시킨다.
  • <script../>는 <body../>의 가장 마지막에 위치시킨다.
  • 기능별 화면은 <body../>의 기능별 컨텐트를 jsp로 구현하여 제공한다.
  • 기능별 컨텐트를 구현하는 jsp에서도 <script../>는 가장 마지막에 위치시킨다.

서비스와의 통신

서비스와 http(s) 통신을 하려면 다음과 같은 function들을 사용한다.

  • ajax(...)
  • json(...)
  • upload(...)

위 function들은 다음 설정을 파라미터로 받는다.

{
  url: "...", // 서비스 url
  data: {...}, // 파라미터
  success: function(resp) {} // 성공시 실행하는 함수
}

ajax

  • GET 방식으로 접속하려면 ajax.get({...})
  • POST 방식으로 접속하려면 ajax.post({...})

json

  • GET 방식으로 접속하려면 json.get({...})
  • POST 방식으로 접속하려면 json.post({...})

upload

데이터와 파일을 업로드하려면 upload({...})를 사용한다.

데이터 바인딩

서비스에서 받은 데이터를 페이지에 표시하고 바인딩할 때는

  • Dataset
  • DatasetControl

을 사용한다.

Dataset

Dataset은

  • 내부에 데이터를 갖는다.
  • 데이터 설정, 선택, 추가, 수정, 제거를 위한 메소드를 실행하면
  • 해당 메소드 실행 후 현재 상태를 알려주는 이벤트 핸들러를 실행한다.
  • 주로 사용되는 이벤트 핸들러는 다음과 같다.
    • onDatasetChange
    • onCurrentChange
    • onSelectionChange

DatasetControl

DatasetControl은

  • 내부에 dataset을 갖는다.
  • 서비스에 접속해 데이터를 받거나 전달하고
  • 사용자의 데이터 변경을 dataset에 전달한다.
  • 그 결과 발생하는 dataset의 이벤트를 자신의 이벤트 핸들러로 전달한다.
  • 그 중 주로 사용되는 이벤트 핸들러는 다음과 같다.
    • onDatasetChange
    • onCurrentChange
    • onSelectionChange
  • 개발자는 DatasetControl의 이벤트 핸들러를 override하여 화면을 업데이트한다.

데이터 목록 표시

데이터 목록을 <table../>로 표시하려면

  • DatasetControl.onDatasetChange 이벤트 핸들러에서
  • <template ../>의 컨텐트를 한 행(row)의 템플릿으로 하여
  • 데이터 한 행의 값들로 html 컨텐트를 만든 후
  • 대상 테이블의 <tbody../>를 업데이트 한다.

테이블

다음은 사용자 목록을 표시할 table의 예다.

<table>
  <thead>
    <tr><th><input onchange="userControl.select(this.checked);" type="checkbox"></th>
      <th>계정</th>
      <th>이름</th>
      <th>이메일</th>
      <th>전화번호(무선)</th>
      <th>등록일자</th>
    </tr>
  </thead>
  <tbody id="userList">
  </tbody>
  <template id="userRow">
  <tr data-key="{USER_ID}">
     <td><input value="{USER_ID}" onchange="userControl.select('{USER_ID}', this.checked);" type="checkbox"></td>
     <td {onclick} {ondblclick}>{USER_ACNT}</td>
     <td {onclick} {ondblclick}>{USER_NM}</td>
     <td {onclick} {ondblclick}>{EML_ADRS}</td>
     <td {onclick} {ondblclick}>{MBL_TELNO}</td>
     <td {onclick} {ondblclick}>{REG_DT}</td>
  </tr>
  </template>
  <template id="userNotFound">
  <tr><td valign="top" colspan="6" class="dataTables_empty text-center">사용자 정보를 찾지 못했습니다.</td>
  </tr>
  </template>
</table>
  • template#userRow의 <tr data-key="...">는 목록 중 현재 행이 달라졌을 때 찾기 위한 값을 갖는 애트리뷰트다.
  • template의 컨텐트 중 {...}로 표시된 부분은 데이터의 필드값으로 치환할 부분을 나타낸다.
  • {onclick}, {ondblclick}은 DatasetControl의 이벤트 핸들러에서 직접 치환할 부분을 나타내는 커스텀 마킹이다.

다음은 위 table에 데이터를 표시하는 스크립트 예다.

var userControl = new UserControl();
...
userControl.setData({
  userList:${userList},
  userStart:${userStart},
  userFetch:${userFetch},
  userTotal:${userTotal}
});
...
userControl.onDatasetChange = obj => {
  let userList = userControl.dataset,
      trs = userList.empty ?
          [document.getElementById("userNotFound").innerHTML] :
          userList.inStrings(
              document.getElementById("userRow").innerHTML,
              (str, dataItem) => {
                  let userID = dataItem.getValue("USER_ID");
                  return str
                      .replace(/{onclick}=""/gi, 'onclick="userControl.setCurrent(\'{USER_ID}\');"')
                      .replace(/{ondblclick}=""/gi, 'ondblclick="userControl.getInfo(\'{USER_ID}\')"')
              }
          );
  $("#userList").html(trs.join());
  $("th input[type='checkbox']").prop("checked", false);
};
  • userControl의 setData(...)를 호출하면 onDatasetChange(...) 이벤트 핸들러를 실행한다.
  • userControl의 onDatasetChange(...) 메소드를 override한다.
  • Dataset.inString(...) 메소드는 주어진 문자열에서
    • 커스텀 마킹({onclick}, {ondblclick})을 처리하는 함수를 실행하고
    • '{필드이름}' 부분을 치환한다.

페이징

페이징을 위한 컨텐트 생성도 onDatasetChange 핸들러에서 처리한다.

<ul id="userPaging" class="pagination pagination-primary">
</ul>

userControl.onDatasetChange = obj => {
  ...
  $("#userPaging").setPaging({
    list: userControl.dataset,
    prefix: userControl.prefix,
    start: obj.userStart,
    totalSize: obj.userTotal,
    fetchSize: obj.userFetch,
    func: "userControl.load({index})"
  });
};

현재 데이터행 표시

  • 데이터 목록 중 사용자가 보는 현재 데이터 행을 변경시키려면 DatasetControl.setCurrent(...) 메소드를 호출한다.
  • 현재 데이터 행이 변경되면 DatasetControl.onCurrentChange(...) 핸들러가 호출된다.
  • 변경된 현재 데이터 행을 화면에 업데이트하려면 DatasetControl.onCurrentChange를 override한다.

다음은 사용자 목록의 현재 데이터 목록이 변경되면 테이블의 하이라이트 행을 업데이트하는 예다.

userControl.onCurrentChange = item => {
  if (!item) return;
  
  let key = item.data.USER_ID;
  $("#userList").setCurrentRow(key);
};

데이터행 선택(체크)

  • 데이터 목록 중 데이터행(들)을 선택(체크)하려면 DatasetControl.select(...) 메소드를 호출한다.
  • 데이터행을 선택하면 DatasetControl.onSelectionChange(...) 핸들러가 호출된다.
  • 데이터행 선택을 화면에 업데이트하려면 DatasetControl.onSelectionChange를 override한다.

다음은 사용자 목록의 데이터 행들을 선택하면 테이블의 해당 행의 체크박스를 업데이트하는 예다.

userControl.onSelectionChange = selected => {
  let userList = userControl.dataset;
  let keys = selected.map(e => userList.getKey(e));
  $("#userList input[type='checkbox']").each(function() {
    let checkbox = $(this);
    checkbox.prop("checked", keys.includes(checkbox.val()));
  });
};

데이터 폼 바인딩

데이터 목록 중 특정 행의 데이터를 폼으로 표시하거나 사용자의 데이터 변경을 포착하려면

데이터 행의 필드값을 표시할 요소(input, select, textarea...) 태그에

  • 'id'나 'name' 애트리뷰트는 데이터 소유 객체의 프로퍼티 이름을 명시한다.
  • data-map="컬럼이름" 형식의 커스텀 애트리뷰트를 명시한다.

다음은 사용자 데이터를 표시하는 폼의 예다.

<form id="userForm">
  <div>
    <label for="account">계정</label>
    <div>
      <input name="account" type="text" required data-map="USER_ACNT" placeholder="사용자 계정" />
    </div>
  </div>
  <div>
    <label for="name">이름</label>
    <div>
      <input name="name" type="text" required  data-map="USER_NM" placeholder="사용자 이름" />
    </div>
  </div>
  <div>
  <div>
    <label for="birthday">생년월일</label>
    <div>
      <input name="birthday" type="text" data-map="BRDT" placeholder="YYYY-MM-DD"/>
    </div>
  </div>
  <div>
    <label for="gender">성별</label>
    <div>
      <div>
        <input name="gender" value="M" type="radio" data-map="GENDER"/>
        <label for="male">남자</label>
      </div>
      <div>
        <input name="gender" value="F" type="radio" data-map="GENDER"/>
        <label for="female">여자</label>
      </div>
    </div>
  </div>
  <div>
    <label for="emailAddress">이메일 주소</label>
    <div>
      <input name="emailAddress" type="email" required data-map="EML_ADRS" placeholder="이메일 주소" />
    </div>
  </div>
</form>

다음 코드는 사용자 데이터를 위 폼에 바인딩하는 예다.

var userFields = new FormFields("#user-form");
...
userControl.setInfo = obj => {
  ...
  userFields.set(userControl, obj);
  ..
};

폼에 설정된 데이터를 객체로 변환할 때는 FormFields.get() 메소드를 사용한다.

var userObj = userFields.get();

유효성 체크

데이터를 서비스로 보내기 전에 유효성을 체크하려면 폼의 요소(input, select, textarea...) 태그에

  • type="...", required, pattern, maxLength, max, min, step 등의 애트리뷰트를 명시한다.
  • 해당 요소를 대상으로 하는 `' 태그를 명시한다.
  • 데이터를 서비스로 보내기 전에 `$("selector").validInputs()'를 호출하여 유효성을 체크한다.

다음은 위 사용자 폼의 유효성을 체크하는 코드다.

function saveUser() {
  if (!$("#user-form").validInputs()) return;
  ...
}

위 코드의 결과 유효하지 않은 데이터가 있으면 다음과 같은 다이얼로그를 표시한다.

  • validInputs()는 기본적인 유효성 체크만 실행한다.
  • 보다 복잡한 유효성 체크는 개발자가 직접 구현한다.

DatasetSupport

Dataset의 데이터에 대한 바인딩을 지원하는 DatasetSupport를 참고한다.

다이얼로그

dialog.open(conf)

  • 다이얼로그(팝업) 창을 표시한다.
  • 간단한 메시지, 확인 메시지나 데이터 폼 등을 표시하는데 사용된다.

설정 파라미터

dialog.open(conf)의 conf는 다이얼로그의 설정 파라미터로

  • id: 다이얼로그의 아이디
  • title: 다이얼로그의 제목
  • content: 다이얼로그의 컨텐트(필수)
  • init: 다이얼로그가 로드된 후 최초 실행할 function
  • onOK: 사용자가 '확인' 버튼을 클릭했을 때 실행할 function
  • onClose: 다이얼로그를 닫을 때 실행할 function
  • size: 다이얼로그 크기(sm || lg || xl)
  • timeout: 다이얼로그가 보여질 시간. 0이면 사용자가 닫을 때까지 대기

간단 메시지

간단한 메시지를 표시하려면 다음과 같이 한다.

dialog.alert("간단한 메시지");

확인 메시지

확인 메시지를 표시하려면 다음과 같이 한다.

dialog.alert({
  content: "변경된 내용을 저장하시겠습니까?",
  onOK: function() {...}
});