Anyframe jQuery Plugin

Version 1.0.1

본 문서의 저작권은 삼성SDS에 있으며 Anyframe 오픈소스 커뮤니티 활동의 목적하에서 자유로운 이용이 가능합니다. 본 문서를 복제, 배포할 경우에는 저작권자를 명시하여 주시기 바라며 본 문서를 변경하실 경우에는 원문과 변경된 내용을 표시하여 주시기 바랍니다. 원문과 변경된 문서에 대한 상업적 용도의 활용은 허용되지 않습니다. 본 문서에 오류가 있다고 판단될 경우 이슈로 등록해 주시면 적절한 조치를 취하도록 하겠습니다.


I. Introduction
II. jQuery
1. jqGrid
1.1. jqGrid를 위한 Spring MVC Controller 구현
1.1.1. JqGridVO 상속
1.1.2. BoardController.list()
1.2. jqGrid를 이용한 HTML 개발
1.2.1. jqGrid를 위한 Javascript 라이브러리 dependency
1.2.2. jqGrid Type 1
1.2.3. jqGrid Type 2
1.2.4. jqGrid와 jstree 연동
2. Quickpager
2.1. jqgrid와 quickpager 연동
3. jstree
3.1. jsTree의 활용
4. Upload
4.1. uploadify 소개
4.2. jqueryUpload.js
5. jquery-ui
5.1. Autocomplete
5.2. Tab widget
5.3. Dialog widget
5.4. Button widget
5.5. Theme
6. Validation

I.Introduction

jQuery Plugin은 AJAX를 활용한 Spring MVC 기반의 웹 어플리케이션 개발 사례를 제공할 목적으로 만들어졌다. jQuery Plugin은 JSON 형태의 데이터를 이용한 공통 Controller 클래스 및 jQuery Javascript 프레임워크 및 이를 바탕으로 개발된 오픈소스 UI Component를 활용하여 작성된 샘플 코드와 이를 활용하는데 필요한 참조 라이브러리들로 구성되어 있으며, Plugin 샘플 코드에 활용된 jQuery UI 컴포넌트들로는 jqgrid, quickpager, jstree, jquery-ui(button, dialog, tab, autocomplete), uploadify 등이 있다.

Installation

Command 창에서 다음과 같이 명령어를 입력하여 jquery plugin을 설치한다.

mvn anyframe:install -Dname=jquery

installed(mvn anyframe:installed) 혹은 jetty:run(mvn clean jetty:run) command를 이용하여 설치 결과를 확인해볼 수 있다.

Dependent Plugins

Plugin NameVersion Range
Query2.0.0 > *

Message Source 추가하기

jQuery 플러그인은 별도의 Message Source 파일을 가지고 있다. 플러그인을 설치할 때 파일은 다운로드 되었으니, context-message.properties 파일에 <value>classpath:message/message-jquery</value>를 추가하여 사용하도록 한다.

II.jQuery

1.jqGrid

jqGrid는 웹 상에서 tabular data를 표현하고 조작하기 위한 솔루션을 제공하는 AJAX 기반 자바스크립트 컨트롤이다.(http://www.trirand.com/jqgridwiki/doku.php)

1.1.jqGrid를 위한 Spring MVC Controller 구현

1.1.1.JqGridVO 상속

JqGridVO는 jqGrid 적용을 위한 Abstract Domain Class로서 jqGrid에서 parameter로 제공하는 page, sord, sidx를 member로 가지고 있으므로 이 클래스를 상속받아 Domain 객체를 구현하면 별도의 추가적인 코딩 없이 jqGrid 연동 Controller를 구현하는 것이 가능하다.

다음은 jqGrid 적용을 위해서 JqGridVO를 상속받은 Board Domain Class의 코드 일부이다.

package org.anyframe.plugin.jquery.domain;

public class Board extends JqGridVO implements Serializable{
...중략...
}

1.1.2.BoardController.list()

현재 jQuery plugin에서는 jQuery의 Grid 컴포넌트(jqgrid plugin)에서 Grid를 그릴 때 비즈니스 서비스 호출 후 반환되는 Page 객체를 바로 받을 수 있는 것이 아닌 Grid에서 인식할 수 있는 Key와 Value 쌍의 Map 형태로 Model 객체에 셋팅해줘야 한다. 다음은 BoardController 클래스의 일부이다.

@RequestMapping(params = "method=list")
public String list(
		@RequestParam(value = "searchKeyword", defaultValue = "") String searchKeyword,
		@RequestParam(value = "searchCondition", defaultValue = "") String searchCondition,
		@RequestParam(value = "communityId", required = false) String communityId,
		Board board, Model model, HttpServletRequest request)
		throws Exception {
		
...중략...

	Page resultPage = boardService.getPagingList(board);

	Map<String, Object> jsonModel = new HashMap<String, Object>();
	
	jsonModel.put("page", resultPage.getCurrentPage() + "");
	jsonModel.put("total", resultPage.getMaxPage() + "");
	jsonModel.put("records", resultPage.getTotalCount() + "");
	jsonModel.put("rows", resultPage.getList());
	
	model.addAllAttributes(jsonModel);
	return "jsonView";
}

위의 코드에서 볼 수 있듯이 비즈니스 서비스 수행후 Return 값이 org.anyframe.pagination.Page 타입일 경우 jQuery의 Grid에서 인식 할 수 있는 key값으로 jsonModel 객체를 셋팅해 주고있다.

1.2.jqGrid를 이용한 HTML 개발

1.2.1.jqGrid를 위한 Javascript 라이브러리 dependency

jqGrid를 사용하기 위해서는 jquery, jquery-ui, jqGrid 라이브러리가 필요하다.

<!-- jquery -->
<script type="text/javascript" src="<c:url value='/jquery/jquery/jquery-1.6.2.min.js'/>"></script>
<script type="text/javascript" src="<c:url value='/jquery/jquery/validation/jquery.validate.js'/>"></script>

<!-- jquery-ui -->
<script type="text/javascript" src="<c:url value='/jquery/jquery/jquery-ui/jquery-ui-1.8.16.custom.min.js'/>"></script>

<!-- jqGrid -->
<script type="text/javascript" src="<c:url value='/jquery/jquery/jqgrid/i18n/grid.locale-en.js'/>"></script>
<script type="text/javascript" src="<c:url value='/jquery/jquery/jqgrid/jquery.jqGrid.min.js'/>"></script>
<script type="text/javascript" src="<c:url value='/jquery/jquery/jqgrid/plugins/grid.setcolumns.js'/>"></script>

참고

※ 라이브러리 참조 선언의 순서에 주의하여야 한다.

※※ jqGrid의 경우 필요에 따라 추가적인 플러그인 라이브러리를 호출하여 사용할 수 있다.(jqGrid 위키사이트 참조 : http://www.trirand.com/jqgridwiki/doku.php)

jquery plugin 에서는 크게 두 가지 형태의 jqGrid 적용 type을 제공하고 있다.

1.2.2.jqGrid Type 1

  • i. : 커뮤티니의 게시물 list를 제공

  • ii. : row 단위로 select 할 수 있으며, quickpager 오픈소스 컴포넌트와 결합하여 page navigation을 제공

  • iii. : jquery-ui에서 제공하는 dialog widget과 연동하여 등록/수정 기능을 제공

  • iv. : jquery-ui에서 제공하는 autocomplete 와 연동하여 목록에 대한 검색기능을 제공

다음은 type 1 게시물 리스트를 출력하는 tabMain.jsp 파일의 일부이다.

function _createGridType1(id) {
	var gridId = '#grid_' + id;
	var paginationId = gridId + '_pagination';
	
	$(gridId).jqGrid({
		url: "<c:url value='/jqueryBoard.do?method=list' />",
		mtype:'POST',
		datatype : "json",
		postData : {'communityId' : id, 'searchKeyword' : '', 'searchCondition' : ''},
		colNames : [ '<spring:message code="board.id" />', '<spring:message code="category.name" />', '<spring:message code="board.title" />', 
		             '<spring:message code="board.contents" />', '<spring:message code="board.regId" />', 
		             '<spring:message code="board.regDate" />', '<spring:message code="community.id" />'],
		jsonReader: {repeatitems: false},
		colModel : [ 
			{key : true, name : 'postId', editable:false, hidden:true}, 
			{name : 'communityName', name : 'communityName', editable:false, hidden:true},
			{name : 'title', index : 'title', align : 'center', editable:true}, 
			{name : 'contents', index :'contents' , align : 'left', editable:true, hidden : false}, 
			{name : 'regId', index : 'regId' , align : 'center', editable:false, width:50}, 
			{name : 'regDate', index : 'regDate', align : 'center', sorttype:"date", editable:true,width:70},
			{name : 'communityId', name : 'communityId', editable:false, hidden:true}],
		autowidth : true,
		height : "auto",	
		viewrecords : true,	
		rowNum : 10, sortable : true,
		loadComplete : function(xhr) {
			$(paginationId).quickPager( {
	    		pageSize: "10",
	    		pageUnit: "10",
	    		pageIndexId: 'grid_' + id + "_pageIndex",
	    		searchButtonId: 'grid_' + id + "_btnSearch", 
	    		currentPage: $(gridId).getGridParam("page"),
	    		totalCount: $(gridId).getGridParam("records"),
	    		searchUrl: "#"
	    		});
	    },
	    gridComplete: function() { 
	    	$("#_empty",gridId).addClass("nodrag nodrop"); 
	    	//$("#grid").tableDnDUpdate(); 
	    	$(gridId).setGridWidth($('#right').width() - 40);
	    	$(window).bind('resize', function() {
	    	    $(gridId).setGridWidth($('#right').width() - 40);
	    	}).trigger('resize');
	    }, 
		loadError: function(xhr,st,err) {
			alert(err); 
		}
	});

	$("button", ".buttons").button();
	
	$(gridId + "_btnAdd").click(function() { 
		dialogMode = "add";
		AnyframeUpload.options.refId = '';
		$("#dialog-form").dialog( "open" );
	});

	$(gridId + "_btnEdit").click(function() { 
		var rowNum = $(gridId).jqGrid('getGridParam','selrow');
		if(rowNum == null || rowNum == ""){
			alert('<spring:message code="board.msg.delete.alert" />');
		}else{
			dialogMode = "edit";
			$.post(
			       "<c:url value='/jqueryBoard.do?method=get'/>", {postId : rowNum}, 
			       function(data) {
			    	   $("#boardPostId").val(data.board.postId);
			    	   $("#boardTitle").val(data.board.title);
			    	   $("#boardContents").val(data.board.contents);
			    	   $("#boardRegId").val(data.board.regId);
			    	   $("#boardRegDate").val(data.board.regDate);
			    	   $("#communities").val(data.board.communityName).selected;

			    	   AnyframeUpload.options.refId = data.board.postId;
			    	   AnyframeUpload.loadAttachedFileList('uploadPane');
			    	   
			    	   $( "#dialog-form" ).dialog( "open" );
		     }); 
		}
	});
	
	$(gridId + "_btnRemove").click(function() { 
		var rowNum = $(gridId).jqGrid('getGridParam','selrow');
		var postId = $(gridId).getCell(rowNum, 'postId');
		$(gridId).delGridRow( rowNum, {
			reloadAfterSubmit:true,
			msg:'<spring:message code="board.msg.delete.confirm" />',
			delData:{postId: postId},
			url:"<c:url value='/jqueryBoard.do?method=remove'/>"
		});
	});
	
	$(gridId + "_btnRefresh").click(function() { 
		$(gridId).trigger("reloadGrid");
	});
	
	$(gridId + "_btnSearch").click(function() {
		$(gridId).setGridParam({
			page: $(gridId + "_pageIndex").val(),
			postData: {
				searchKeyword:$(gridId + "_searchKeyword").val(), 
				searchCondition:$(gridId + "_searchCondition").val()
			}
		});
		$(gridId).setGridParam({url:"<c:url value='/jqueryBoard.do?method=list'/>"}).trigger("reloadGrid");
	});
	
	/* auto click by enter key */
	$(gridId + "_searchKeyword").keypress(function (e) {
		if (e.which == 13){
			$(gridId + "_pageIndex").val('1');
			$(gridId + "_btnSearch").trigger("click");
			return false;
		}
	});

	$(gridId + "_searchKeyword").autocomplete({
		source : function(request, response){
			logger.log('call');
			$.ajax({
				'url' : '<c:url value="/jqueryBoard.do?method=searchKeyword"/>',
				'type' : 'POST',
				'async' : false,
				'data' : 'searchKeyword=' + $(gridId + "_searchKeyword").val() + '&searchCondition=' + $(gridId + "_searchCondition").val() + '&communityId=' + _currentNodeId,
				'dataType' : 'json',
				'success' : function(data){
					logger.log('return:data.r.length=' + data.r.length);
					response(data.r);
				}
			});
		},
		minLength : 1,
		select : function(event, ui) {
			logger.log('autocomplete selected:' + ui.item.value);
			$(gridId + '_searchKeyword').val(ui.item.value);
			$(gridId + '_pageIndex').val('1');
			$(gridId + '_btnSearch').trigger("click");
			return false;
		}
	});

}

위와 같이 jqgrid로 구현된 리스트의 모습은 아래와 같다.

1.2.3.jqGrid Type 2

  • i. : 카테고리에 속한 커뮤니티 list를 제공

  • ii. : cell 단위로 select/edit 할 수 있으며, scroll down 방식의 page navigation을 제공

  • iii. : jquery-ui의 date-picker 컴포넌트와 연동하여 date 형식의 data 수정

  • iv. : jquery-ui에서 제공하는 autocomplete 와 연동하여 목록에 대한 검색기능을 제공

  • v. : 그리드 내 버튼을 통한 삭제 기능 제공

다음은 type 2 방식의 커뮤니티 리스트를 출력하는 tabMain.jsp 파일의 일부이다.

var isCellSaved;
var lastsel_row;
var lastsel_col;

function _createGridType2(id) {
	
	var gridId = '#grid_' + id;
	
	$(gridId).jqGrid({
		url: "<c:url value='/jqueryCommunity.do?method=list' />",
		mtype:'POST',
		datatype : "json",
		postData : {'categoryId' : id},
		colNames : ['<spring:message code="community.id" />','<spring:message code="community" /> <spring:message code="community.name" />', '<spring:message code="community.desc" />', 
		            '<spring:message code="community.redId" />', '<spring:message code="community.regDate" />', '<spring:message code="category.id" />', '<spring:message code="category.name" />',''],
		jsonReader: {repeatitems: false},
		colModel : [ 
		{key : true, name : 'communityId', editable:false, hidden:true}, 
		{name : 'communityName', index : 'communityName', align : 'center', editable:true}, 
		{name : 'communityDesc', index :'communityDesc' , align : 'left', editable:true}, 
		{name : 'regId', index : 'regId' , align : 'center', editable:false, width:75}, 
		{name : 'regDate', index : 'regDate', align : 'center', sorttype:"date", width:100, editable:true,
			editoptions: {
	              dataInit: function(element) {
           		    $(element).datepicker({ 
           		    	dateFormat: 'yy/mm/dd',
           		    	onSelect: function(dataText, inst){
           		    		$(gridId).jqGrid('saveCell',lastsel_row, lastsel_col); 
           		    	}
    		    	});
	              }
	          }	
		},
		{name : 'categoryId', name : 'categoryId', editable:false, hidden:true},
		{name : 'categoryName', name : 'categoryName', editable:false, hidden:true},
		{name: 'myac', width:40, fixed:true, sortable:false, resize:false, formatter:'actions', 
			formatoptions:{editbutton:false, keys:true,
				delOptions:{msg:'<spring:message code="board.msg.delete.confirm" />',
					onclickSubmit:function(rp_ge, rowid){
					$(gridId).delGridRow( rowid, {
						reloadAfterSubmit:true,
						delData:{communityId: rowid},
						url:"<c:url value='/jqueryCommunity.do?method=remove'/>",
						afterComplete : function (response, postdata, formid) {
							$('#${treeId}').jstree("remove","#" + rowid);
							return false;
						} 
					});
				}}
			}
		}],
		scroll : true,
		height : 220,
		multiselect : false, viewrecords : true,	
		rowNum : 10, sortable : true,
		cellEdit: true, cellsubmit:"remote",
		cellurl:"<c:url value='/jqueryCommunity.do?method=updateCell'/>",
		beforeEditCell: function(id,name,val,iRow,iCol){     
			lastsel_row = iRow;
			lastsel_col = iCol;
			isCellSaved = false;
		},
		beforeSaveCell: function(id,name,val,iRow,iCol){  
			isCellSaved = true;
		},
		afterSaveCell:function(rowid, cellname, value, iRow, iCol){
			if(cellname=="communityName"){
				$('#${treeId}').jstree('get_selected').find("#"+rowid+" a").html('<ins class="jstree-icon">&nbsp;</ins>' + value);
			}
		},
	    gridComplete: function() { 
	    	$("#_empty",gridId).addClass("nodrag nodrop"); 
	    	$(gridId).setGridWidth($('#right').width() - 40);
	    	$(window).bind('resize', function() {
	    	    $(gridId).setGridWidth($('#right').width() - 40);
	    	}).trigger('resize');
	    }, 
		loadError: function(xhr,st,err) {
			alert(err); 
		}
	});
	
	$(gridId + "_btnSearch").click( function() {
		$(gridId).setGridParam({
			postData: {
				searchKeyword:$(gridId + "_searchKeyword").val(), 
				searchCondition:$(gridId + "_searchCondition").val()
			}
		});
		$(gridId).setGridParam({url:"<c:url value='/jqueryCommunity.do?method=list'/>"}).trigger("reloadGrid");
		return false;
	});

	/* auto click by enter key */
	$(gridId + "_searchKeyword").keypress(function (e) {
		if (e.which == 13){
			$(gridId + "_btnSearch").trigger("click");
			return false;
		}
	});

	$(gridId + "_searchKeyword").autocomplete({
		source : function(request, response){
			logger.log('call');
			$.ajax({
				'url' : '<c:url value="/jqueryCommunity.do?method=searchKeyword"/>',
				'type' : 'POST',
				'async' : false,
				'data' : 'searchKeyword=' + $(gridId + "_searchKeyword").val() + '&searchCondition=' + $(gridId + "_searchCondition").val() + '&categoryId=' + id,
				'dataType' : 'json',
				'success' : function(data){
					logger.log('return:data.r.length=' + data.r.length);
					response(data.r);
				}
			});
		},
		minLength : 1,
		select : function(event, ui) {
			logger.log('autocomplete selected:' + ui.item.value);
			$(gridId + '_searchKeyword').val(ui.item.value);
			$(gridId + '_pageIndex').val('1');
			$(gridId + '_btnSearch').trigger("click");
			return false;
		}
	});

	//$(gridId).jqGrid('gridResize');
}

위와 같이 jqgrid로 구현된 리스트의 모습은 아래와 같다.

1.2.4.jqGrid와 jstree 연동

  • i. : 커뮤니티 리스트가 삭제되거나 수정되는 경우, jstree에도 이를 반영

$(gridId).jqGrid({
		...중략...
		{name: 'myac', width:40, fixed:true, sortable:false, resize:false, formatter:'actions', 
			formatoptions:{editbutton:false, keys:true,
				delOptions:{msg:'<spring:message code="board.msg.delete.confirm" />',
					onclickSubmit:function(rp_ge, rowid){
					$(gridId).delGridRow( rowid, {
						reloadAfterSubmit:true,
						delData:{communityId: rowid},
						url:"<c:url value='/jqueryCommunity.do?method=remove'/>",
						afterComplete : function (response, postdata, formid) {
							$('#${treeId}').jstree("remove","#" + rowid); // 커뮤니티 삭제 시 tree 반영
							return false;
						} 
					});
				}}
			}
		}],
		...중략...
		afterSaveCell:function(rowid, cellname, value, iRow, iCol){
			if(cellname=="communityName"){
				// 커뮤니티 수정 시 tree 반영
				$('#${treeId}').jstree('get_selected').find("#"+rowid+" a").html('<ins class="jstree-icon">&nbsp;</ins>' + value);
			}
		},
	    ...중략...
	});

참고

※ jqgrid를 사용하여 리스트를 작성할 때 너무 많은 양의 데이터를 한꺼번에 출력하려고 하면 리스트를 출력하는데 있어서 많은 시간이 걸리거나 브라우저가 멈추는 현상이 발생할 수 있다. 이에 한번에 출력하는 데이터의 건수는 100개 이내로 하며 데이터가 많을 경우 pager를 이용해 paging 처리 할 것을 권고한다.

2.Quickpager

jqgrid는 기본적으로 Paging 처리를 위한 Pager를 제공하고 있다. Anyframe에서는 pagenavigator와 유사한 Pager 출력을 위해 quickpager를 확장하여 사용하고 있다. quickpager를 사용하기 위해서는 리스트 Script내의 loadComplete 함수 안에 paging 정보를 셋팅 해주고 search 버튼을 클릭하는 이벤트를 발생 시키도록 한다.

2.1.jqgrid와 quickpager 연동

관련 jQuery 코드는 다음과 같다.

jQuery("#grid").jqGrid({
...중략...
	loadComplete : function(xhr) {
		$(paginationId).quickPager( {
		pageSize: "10",
		pageUnit: "10",
		pageIndexId: 'grid_' + id + "_pageIndex",
		searchButtonId: 'grid_' + id + "_btnSearch", 
		currentPage: $(gridId).getGridParam("page"),
		totalCount: $(gridId).getGridParam("records"),
		searchUrl: "#"
		});
	},
...중략...
});

$(gridId + "_btnSearch").click(function() {
	$(gridId).setGridParam({
		page: $(gridId + "_pageIndex").val(),
		postData: {
			searchKeyword:$(gridId + "_searchKeyword").val(), 
			searchCondition:$(gridId + "_searchCondition").val()
		}
	});
	$(gridId).setGridParam({url:"<c:url value='/jqueryBoard.do?method=list'/>"}).trigger("reloadGrid");
});

위와 같이 Script 코드가 작성 되면 pagenavigator 출력 부분에 아래와 같이 div 영역을 표시해준다.

<div id="${gridId}_boardNav">
	<div id="${gridId}_pagination" class="pagination"></div>
</div>

위와 같이 정의한 quickpager는 아래와 같은 pagenavigator를 출력하게 된다

jqgrid에서 제공하는 pager

jqgrid에서도 paging 처리를 위한 간편한 pager를 제공한다. 구현 방법은 아래와 같다.

//jqgrid 속성 설정 내에 정의
pager : jQuery('#pager')

<!-- JSP 내의 pager 출력 부분에 정의 -->
<div id="pager" class="scroll" style="text-align: center;"></div>

3.jstree

jstree는 계층적으로 조직된 데이타를 tree 형태로 보여주기 위해 제공되는 jQuery 기반의 오픈소스 UI 컴포넌트이다.

jsTree is a javascript based, cross browser tree component. It is packaged as a jQuery plugin(http://www.jstree.com)

3.1.jsTree의 활용

jstree는 0.9.9a 이후 1.0-rc3로 업그레이드 되면서 일부 api 및 사용법이 변경되었다. 여기서는 1.0-rc3 버전을 기준으로 설명한다.

jstree는 크게 html, json, xml 방식의 data loading 방식을 제공하며, 여기서는 html 방식을 제공한다. html은 JSTL을 사용하여 다음과 같이 표시 할 수 있다.

<!-- start of tree -->
<div id="tree">
	<span>listNode</span>
	<ul>
	<li id="ROOT" rel="root">
		<a href='#'>ROOT</a>
			<c:set var="prevDepth" value="-1"/>
			<c:forEach var="node" items="${treeList}">
				<c:if test="${node.depth > prevDepth}">
				<ul>
				</c:if>
				<c:if test="${prevDepth > node.depth}">
					<c:forEach begin="${node.depth}" end="${prevDepth - 1}" step="1">
					</ul></li>
					</c:forEach>
				</c:if>
				<li id="${node.nodeId}" parentId="${node.parentId}" depth="${node.depth}" rel="${node.type}">
					<a href='#'>${node.nodeName}</a>
				<c:if test="${node.hasChild == 0}">
				</li>
				</c:if>
				<c:set var="prevDepth" value="${node.depth}"/>
			</c:forEach>
		</li>
    </ul>
</div>
<!-- end of tree -->

위와 같은 html 트리 관련 jQuery 구현코드는 아래와 같다.

// tree definition
$(document).ready(function() {

	$('#${treeId}').jstree({
   		'plugins' : ["themes","html_data","ui","crrm","search","types","hotkeys","contextmenu"], //,"dnd"  ,"html_data" , 'checkbox', "cookies", 
   		'themes' : {
   			'theme' : 'default',
   			'dots' : false,
   			'icons' : true
		},
		'contextmenu' : {
			'items' : createContextMenu
		},
		'search' : {
			'case_insensitive' : true
		},
		'types' : {
			'valid_children' : ["root"],
			'types' : {
				'CA' : {},
				'CO' : {// change icon for community
					'icon' : {'image' : '<c:url value="/sample/images/tree_types/leaficons.png"/>'}
				}
			}
		},
		'core' : {
			'initially_open' : ['ROOT'],
			'animation' : 0
		}
	}).bind("select_node.jstree", function (e, data) { // event handling for node select
		logger.log('select_node:' + data.rslt.obj.attr("id"));
		if(data.rslt.obj.attr('id') == 'ROOT') { // Root is selected
			logger.log('root Selected');
			_currentNodeType = 'ROOT';
			_currentNodeId = 'ROOT';
			$tabs.tabs('select', '#tabs-0');
		}else if(data.rslt.obj.attr('rel') == 'CA') { // Category is selected
			logger.log('category Selected');
			_currentNodeType = data.rslt.obj.attr('rel');
			_currentNodeId = data.rslt.obj.attr('id');
			// commuity list load
			$.get("<c:url value='/jqueryCategory.do?method=get'/>", {'categoryId' : data.rslt.obj.attr('id')}, function(r) {
				addTab(r.category.categoryName, data.rslt.obj.attr('id'));
			});
		}else if(data.rslt.obj.attr('rel') == 'CO'){ // Community is selected
			logger.log('community Selected');
			_currentNodeType = data.rslt.obj.attr('rel');
			_currentNodeId = data.rslt.obj.attr('id');
			// community's board list load
			$.get("<c:url value='/jqueryCommunity.do?method=get'/>", {'communityId' : data.rslt.obj.attr('id')}, function(r) {
				addTab(r.community.communityName, data.rslt.obj.attr('id'));
			});
		}
		$('#community').val(data.rslt.obj.attr('id'));		
	}).bind("remove.jstree", function(e, data) { // event handling for node delete
		data.rslt.obj.each(function() {
			if($(data.rslt.obj).attr("rel") == 'CO') { // for community
				logger.log('community removed:' + $(data.rslt.obj).attr("id"));
				$.ajax({
					async : false,
					type : 'POST',
					url : '<c:url value="/jqueryCommunity.do?method=remove"/>',
					data : {
						"communityId" : $(data.rslt.obj).attr("id")
					},
					success : function(r) {
						logger.log('111');
						data.inst.refresh();
						logger.log('222');
						$tabs.tabs('remove', '#tabs-' + $(data.rslt.obj).attr("id"));
						logger.log('333');
						$("#community option[value='" + $(data.rslt.obj).attr("id") + "']").remove();
						logger.log('444');
					},
					error : function() {
						$.jstree.rollback(data.rlbk);
					}
				});
			}else if($(data.rslt.obj).attr("rel") == 'CA'){ // for category
				$.ajax({
					async : false,
					type : 'POST',
					url : '<c:url value="/jqueryCategory.do?method=remove"/>',
					data : {
						"categoryId" : $(data.rslt.obj).attr("id")
					},
					success : function(r) {
						data.inst.refresh();
						$tabs.tabs('remove', '#tabs-' + $(data.rslt.obj).attr("id"));
					},
					error : function() {
						$.jstree.rollback(data.rlbk);
					}
				});
			}
		});
	});

	// tree search event handler for button click case
	$('#treeSearch').click(function(e) {
		$('#${treeId}').jstree('search', $('#searchKeyword').val());
	});

	// tree search event handler for enter key down case
	$('#searchKeyword').keydown(function(e) {
		if(e.keyCode == '13') {
			$('#${treeId}').jstree('search', $('#searchKeyword').val());
			return false;
		}
	});

});

트리의 노드 타입에 따른 컨텍스트 메뉴 구성은 다음과 같이 구현한다.

/**
 * context menu generate for tree
 */
function createContextMenu(node) {

	var default_object = {
		'create' : {},
		'edit' : {},
		'remove' : {}
	};
	if(node.attr('id') == 'ROOT') {
		default_object = {
			create : {
				label : '<spring:message code="category.context.add" />',
				action : function(obj) {
					logger.log('create category : ' + obj.attr('id'));
					openCategoryForm(obj, 'create');
				}
			},
			edit : false,
			remove : false
		};
	}else if(node.attr('rel') == 'CA') {
		default_object = {
			create : {
				label : '<spring:message code="community.context.add" />',
				action : function(obj) {
					logger.log('create community : ' + obj.attr('id'));
					openCommunityForm(obj, 'create');
				}
			},
			edit : {
				label : '<spring:message code="category.context.edit" />',
				action : function(obj) {
					logger.log('edit category : ' + obj.attr('id'));
					openCategoryForm(obj, 'edit');
				}
			},
			remove : {
				label : '<spring:message code="category.context.delete" />',
				_disabled : node.children('ul').length > 0 ? true : false,
				action : function(obj) {
					logger.log('remove category : ' + obj.attr('id'));
					if(this.is_selected(obj)) { 
						this.remove(); 
					} else { 
						this.remove(obj); 
					} 
				}
			}
		};
	}else if(node.attr('rel') == 'CO') {
		default_object = {
			create : false,
			edit : {
				label : '<spring:message code="community.context.edit" />',
				action : function(obj) {
					logger.log('edit community : ' + obj.attr('id'));
					openCommunityForm(obj, 'edit');
				}
			},
			remove : {
				label : '<spring:message code="community.context.delete" />',
				action : function(obj) {
					logger.log('remove community : ' + obj.attr('id'));
					if(this.is_selected(obj)) { 
						logger.log('try remove 1: ' + obj.attr('id'));
						this.remove(); 
					} else { 
						logger.log('try remove 2: ' + obj.attr('id'));
						this.remove(obj); 
					} 
				}
			}
		};
	}
	return default_object;
}

트리 검색 기능 구현은 다음과 같이 한다.

// tree definition
$(document).ready(function() {

	// tree search event handler for button click case
	$('#treeSearch').click(function(e) {
		$('#${treeId}').jstree('search', $('#searchKeyword').val());
	});

	// tree search event handler for enter key down case
	$('#searchKeyword').keydown(function(e) {
		if(e.keyCode == '13') {
			$('#${treeId}').jstree('search', $('#searchKeyword').val());
			return false;
		}
	});
});

※ Tree 유틸을 사용한 데이타의 정렬

트리 형식으로 표시할 데이타를 표시 순서대로 정렬하기 위해서, Tree와 TreeNode 유틸클래스를 활용하여 정렬하는 것이 가능하다.(DB 에서 가져온 1차 데이타는 depth와 display order 순으로 정렬되어 있어야 함.)

다음은 카테고리와 커뮤니티의 데이타를 각각 가져와서 tree 형태로 정렬시킨 CommunityServiceImpl 클래스의 getTreeList() 메소드의 구현 코드이다.

public List<Map<String, String>> getTreeList() throws Exception {
	List<Map<String, String>> trees = new ArrayList<Map<String, String>>();
	List<Community> communities = (List<Community>) communityDao.getList(new Community());
	List<Category> categories = (List<Category>) categoryDao.getList(new Category());
	Tree tree = null;
	if (categories != null) {
		tree = new Tree();
		int cL = categories.size();
		for (int i = 0; i < cL; i++) {
			Category ca = (Category) categories.get(i);
			Map<String, String> item = new HashMap<String, String>();
			item.put("nodeId", ca.getCategoryId());
			item.put("parentId", "ROOT");
			item.put("depth", "0");
			item.put("displayOrder", Integer.toString(i));
			item.put("nodeName", ca.getCategoryName());
			item.put("type", "CA");
			tree.add(ca.getCategoryId(), "ROOT", item);
		}

		if (communities != null) {
			int ccL = communities.size();
			for (int i = 0; i < ccL; i++) {
				Community co = (Community) communities.get(i);
				Map<String, String> item = new HashMap<String, String>();
				item.put("nodeId", co.getCommunityId());
				item.put("parentId", co.getCategoryId());
				item.put("depth", "1");
				item.put("displayOrder", "0");
				item.put("nodeName", co.getCommunityName());
				item.put("type", "CO");
				tree.add(co.getCommunityId(), co.getCategoryId(), item);
			}
		}

		trees = (List<Map<String, String>>) tree.getList();
	}
	return trees;
}

다음은 jstree를 이용하여 Tree를 출력한 화면이다.

4.Upload

jQuery와 AJAX를 활용한 Multi file 첨부기능을 구현하여 제공하고 있다.

4.1.uploadify 소개

uploadify는 jquery와 flash Object를 통하여 간편하게 multi file 첨부를 구현할 수 있게 해주는 오픈소스 컴포넌트이다. 자세한 내용은 http://www.uploadify.com/ 사이트를 참조하기 바란다.

4.2.jqueryUpload.js

jquery plugin에서는 uploadify를 사용하여 파일첨부를 구현한 별도의 서브셋을 jqueryUpload.js 에 별도로 구현하였다. 이를 통해서 파일 첨부 관련 코드가 비즈니스 로직에 추가되는 것을 최소화하도록 의도된 것이다.

파일 업로드 컴포넌트는 다음과 같이 인스턴스화 시킨다.

$(document).ready(function() {
	$('#uploadPane').attachment({
		'contextRoot' : '${ctx}',
		'callBack' : function() {
			savePost(); // after file upload
		}
	});
});

위의 코드에서 'uploadPane', 즉, 첨부파일 UI가 표시될 영역은 게시물 등록 form 인 "dialog-form" 영역에 선언되어 있다.

<!-- board form start -->
<form:form id="dialog-form" name="dialog-form" title="Board Form">
	<fieldset>
		<input type="hidden" name="postId" id="boardPostId">
		<input type="hidden" name="regId" id="boardRegId">
		<input type="hidden" name="regDate" id="boardRegDate">
		<table summary="jquery" width="100%">
			... 중략 ...
			<tr>
				<td><label><spring:message code="board.attach" /></label></td>
				<td>
					<div id="uploadPane"></div>
					<input type="hidden" name="refId" id="refId" value=""/>
				</td>
			</tr>
		</table>
	</fieldset>
</form:form>
<!-- board form end -->

파일을 업로드한 후 실제 게시물을 등록시켜야 하므로 실제로 게시물을 저장하는 스크립트 함수인 savePost()를 callback으로 선언한다.

등록의 경우 첨부된 파일 첨부 대표 ID(Reference ID)는 AnyframeUpload.options.refId에 저장되어 있으므로 이를 파라메터로 전달하여 게시물 정보에 저장될 수 있도록 한다.

여기서는 Reference ID를 게시물 ID와 일치시키도록 하였으므로 BoardServiceImpl.create()에서 다음과 같이 처리하여야 한다.

@Service("jqueryBoardService")
@Transactional(rollbackFor = { Exception.class })
public class BoardServiceImpl implements BoardService{

	@Inject
	@Named("jqueryUploadInfoService")
	private UploadInfoService uploadService;

	public int create(Board board, String fileRefId) throws Exception {
		SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmssSSS", new Locale("ko", "KR"));
		String postId = "POST-" + formatter.format(new Date());
		board.setPostId(postId);
		board.setRegDate((new SimpleDateFormat("yyyy/MM/dd")).format(new Date()));
		int r = boardDao.create(board);
		if(r > 0) uploadService.updateFileRefId(fileRefId, postId); // reference id를 post id와 일치시킨다.
		return r;
	}

... 중략 ...
}

참고

※ jqueryUpload.js 내에 구현된 내용은 하나의 구현 사례이므로 구현 요건에 따라 자유롭게 재구성될 수 있다.

다음은 uploadify와 jqueryUpload.js를 활용하여 파일첨부 기능을 구현한 것이다.

5.jquery-ui

jQuery는 UI 플러그 인을 통해 UI컴포넌트를 추가적으로 제공하며, 테마 기능과 연동된 UI컴포넌트를 통해 강력한 User Inteface를 Pure-HTML 환경에서도 구현할 수 있도록 도와주고 있다.

jQuery 에서 제공하는 UI 컴포넌트에 대한 자세한 기능은 http://jqueryui.com 에서 확인하기 바란다.

Anyframe jQuery plugin에서는 jQuery ui 1.8.16 버전을 바탕으로 autocomplete, tab, dialog, button, theme 기능을 활용하여 제공하고 있다.

5.1.Autocomplete

autocomplete는 사용자가 입력한 prefix를 가지고 자동 완성 기능을 제공하는 UI컴포넌트이다.

아래 자바스크립 코드는 게시물 리스트의 검색에 autocomplete 기능을 적용한 내용이다. success 부분과 select 부분의 코드활용을 주목하기 바란다.

$(gridId + "_searchKeyword").autocomplete({
		source : function(request, response){
			logger.log('call');
			$.ajax({
				'url' : '<c:url value="/jqueryBoard.do?method=searchKeyword"/>',
				'type' : 'POST',
				'async' : false,
				'data' : 'searchKeyword=' + $(gridId + "_searchKeyword").val() + '&searchCondition=' + $(gridId + "_searchCondition").val() + '&communityId=' + _currentNodeId,
				'dataType' : 'json',
				'success' : function(data){
					logger.log('return:data.r.length=' + data.r.length);
					response(data.r);
				}
			});
		},
		minLength : 1,
		select : function(event, ui) {
			logger.log('autocomplete selected:' + ui.item.value);
			$(gridId + '_searchKeyword').val(ui.item.value);
			$(gridId + '_pageIndex').val('1');
			$(gridId + '_btnSearch').trigger("click");
			return false;
		}
	});

다음은 Autocomplete을 적용한 결과이다.

5.2.Tab widget

Tab 위젯은 동일한 목적을 가지고 있으나 성격이 상이한 UI를 분할하여 화면 복잡도를 낮추고 효율적인 User Inteface 구현을 도와주는 자바스크립트 기반의 UI 컴포넌트이다.

var _currentTabId = '';
var $tabs = null;

// 탭을 추가
function addTab(title, id) {
	if($('#tabs-' + id).length > 0) {
		$tabs.tabs("select", '#tabs-' + id);
	}else{
		$tabs.tabs("add", "#tabs-" + id, title );
	}
}

$(document).ready(function(){

	$tabs = $('#tabs').tabs({
		tabTemplate: '<li><a href="<%="#"%>{href}"><%="#"%>{label}</a><span class="ui-icon ui-icon-close">Remove Tab</span></li>',
		add: function( event, ui ) { // 탭이 추가되었을 때의 이벤트 핸들링
			var tab_content = '';
			tab_content = _currentNodeType + ':' + _currentNodeId;
			$(ui.panel).append(tab_content);
			$(ui.tab).attr('nodeId', _currentNodeId);
			$(ui.tab).click();
			if(_currentNodeType == 'CO') {
				$.get('<c:url value="/jqueryBoard.do?method=getGridType1"/>', {'gridId' : 'grid_' + _currentNodeId}, function(data) {
					$(ui.panel).html(data);
					_createGridType1(_currentNodeId);
				});
			}else if(_currentNodeType == 'CA'){
				$.get('<c:url value="/jqueryBoard.do?method=getGridType2"/>', {'gridId' : 'grid_' + _currentNodeId}, function(data) {
					$(ui.panel).html(data);
					_createGridType2(_currentNodeId);
				});
			}
		},
		select : function(event, ui) {
			logger.log('tab selected!!:' + $(ui.tab).attr('nodeId'));
			_currentTabId = $(ui.tab).attr('nodeId');
		}
	});

	// 탭의 'x' 버튼 클릭 시 해당 탭을 제거한다.
	$( "#tabs span.ui-icon-close" ).live( "click", function() {
		var index = $( "li", $tabs ).index( $( this ).parent() );
		$tabs.tabs( "remove", index );
	});
});

다음은 jstree의 구현 코드중 일부이며, 트리의 노드가 선택되었을 때 tab을 추가하도록 하는 부분을 주목하기 바란다.

...생략
	.bind("select_node.jstree", function (e, data) { // event handling for node select
		logger.log('select_node:' + data.rslt.obj.attr("id"));
		if(data.rslt.obj.attr('id') == 'ROOT') { // Root is selected
			logger.log('root Selected');
			_currentNodeType = 'ROOT';
			_currentNodeId = 'ROOT';
			$tabs.tabs('select', '#tabs-0'); // ROOT 선택시에는 탭을 추가하지 않고 포커스만 전환한다.
		}else if(data.rslt.obj.attr('rel') == 'CA') { // Category is selected
			logger.log('category Selected');
			_currentNodeType = data.rslt.obj.attr('rel');
			_currentNodeId = data.rslt.obj.attr('id');
			// commuity list load
			$.get("<c:url value='/jqueryCategory.do?method=get'/>", {'categoryId' : data.rslt.obj.attr('id')}, function(r) {
				addTab(r.category.categoryName, data.rslt.obj.attr('id')); // 커뮤니티 리스트를 위한 탭을 추가한다.
			});
		}else if(data.rslt.obj.attr('rel') == 'CO'){ // Community is selected
			logger.log('community Selected');
			_currentNodeType = data.rslt.obj.attr('rel');
			_currentNodeId = data.rslt.obj.attr('id');
			// community's board list load
			$.get("<c:url value='/jqueryCommunity.do?method=get'/>", {'communityId' : data.rslt.obj.attr('id')}, function(r) {
				addTab(r.community.communityName, data.rslt.obj.attr('id')); // 게시물 리스트를 위한 탭을 추가한다.
			});
		}
		$('#community').val(data.rslt.obj.attr('id'));
생략...

다음은 Tab widget이 적용된 화면이다. 왼쪽 트리 선택 시 해당화면이 우측에 탭을 활용하여 추가되고 또한 삭제가 가능하다. 또한 트리에서 커뮤니티나 카테고리가 삭제되는 경우 우측화면의 tab section도 같이 삭제되도록 구현되어 있다.

5.3.Dialog widget

Dialog 위젯은 Window Popup이나 브라우져의 Message Alert Box를 Layed된 html 요소를 활용하여 대체할 수 있도록 한 것이다.

jQuery UI에서 제공하는 Dialog 위젯은 아래 코드와 같이 다양한 옵션 및 이벤트 핸들링을 구성할 수 있다.

$(document).ready(function() {
... 생략
	// Dialog form definition for Category
	$( "#category-form" ).dialog({
		autoOpen: false,
		width: 400,
		height:"auto",
		modal: true,
		resizable:true,
		close: function() {
			clearCategoryDialog();
		}
	});
...생략
});

다음은 Dialog를 적용한 결과이다. Modal 형태의 Window로서 'Cancel' 버튼과 타이틀바의 'x' 버튼 및 ESC 키를 통해 창을 닫을 수 있으며, window의 사이즈 조절도 가능하도록 옵션처리 되었다.

5.4.Button widget

jQuery UI에서 제공하는 버튼 위젯의 특징은 <button>태그를 그대로 활용한 다는 점으로 웹 표준을 그대로 준수하고 있다는 점이다.

<div class="buttons">
	<button id="${gridId}_btnAdd">Add</button>
	<button id="${gridId}_btnEdit">Edit</button>
	<button id="${gridId}_btnRemove">Remove</button>
	<button id="${gridId}_btnRefresh">Refresh</button>
</div>

위의 버튼 tag들에 다음과 같이 간단한 코딩으로 위젯을 적용하는 것이 가능하다. 이벤트 핸들링 또한 위젯적용여부에 관계없이 핸들링 하고 있다.

$("button", ".buttons").button(); // 위젯 적용
	// 'Add' 버튼의 onclick 이벤트 핸들링
	$(gridId + "_btnAdd").click(function() { 
		dialogMode = "add";
		AnyframeUpload.options.refId = '';
		$("#dialog-form").dialog( "open" );
	});

다음은 게시물 리스트에서 Button 위젯을 적용한 결과이다.

5.5.Theme

jquery-ui 에서 제공하는 UI 컴포넌트와 이를 기반으로 작성된 jqGrid는 jquery-ui에서 제공하는 Theme 관련 feature를 통해 다양한 테마를 손쉽게 변경할 수 있다.

다음은 테마 변경에 대한 구현 코드이다.

// Theme Switcher
$(document).ready(function(){

	$('#themeSwitcher').change(function() {
		var cssUrl = '<c:url value="/jquery/jquery/jquery-ui/themes/"/>';
		var theme = $('#themeSwitcher').val();
		$('#uiTheme').attr('href',cssUrl + theme + '/jquery-ui-1.8.16.custom.css');
	});
	
});

다음은 여러가지 테마를 변경하여 적용한 화면이다.

  • Redmond(기본테마)

  • Blitzer

  • South Street

6.Validation

jQuery 에서는 validation plugin을 통해 jQuery를 적용한 AJAX 기반의 웹 어플리케이션의 form validation 수단을 제공한다.

validation plugin은 다양한 rule과 메시지 표시를 제공하지만, 예제에서는 가장 태그의 attribute를 통해 가장 간단히 구현하는 방법을 소개한다.

좀 더 자세한 내용은 http://docs.jquery.com/Plugins/validation을 참조하기 바란다.

* validation을 위한 css 설정

validation 메시지 표시를 위해 본 예제 코드에서는 admin_new.css 에 다음의 내용을 추가하였다.

/* validation */
label.error { width: 10em;float: none; color: red; padding-left: .5em; vertical-align: top; }
* { font-family: verdana", "돋움";font-size: 9pt }
p { clear: both; }
.submit { margin-left: 12em; }
em { font-weight: bold; padding-right: 1em; vertical-align: top; }

vallidation 적용을 위해서는 해당되는form을 다음과 같이 지정한다.

$('#dialog-form').validate();

해당 form의 html 은 아래와 같으며, class에 'required' 값을 부여함으로써 필수 필드로 지정된다. 또한 maxlength 값 지정을 통해 최대 길이를 제한할 수 있다.

<!-- board form start -->
<form:form id="dialog-form" name="dialog-form" title="Board Form">
	<fieldset>
		...생략 ...
		<table summary="jquery" width="100%">
			<tr>
				<td><spring:message code="board.title" /></td>
				<td><input type="text" name="title" id="boardTitle" class="dialog_text required" maxlength="25"></td>
			</tr>
			<tr>
				<td><spring:message code="board.contents" /></td>
				<td><textarea name="contents" id="boardContents" class="dialog_text required" maxlength="128"></textarea></td>
			</tr>
			...생략...
		</table>
	</fieldset>
</form:form>
<!-- board form end -->

실제 form 값들의 validation을 수행하기 위해서는 valid() 함수를 사용한다.

/**
 * Add/Modify for Post
 */
function savePost() {
	... 생략
	
	if(!$('#dialog-form').valid()) { // validation
		logger.log('dialog-form is invalid.');
		return false;
	}
	
	... 생략
}

다음은 validation을 게시물 등록/수정 form에 적용한 결과이다.