ExtJS의 기초강좌가 아닌 지금까지 배운 내용과 별도의 기능을 적용하여 간단하게 엑셀의 기능을 벤치마킹해보는 시간을 가져보도록 하겠습니다.

    오늘 따라해보고자 하는 기능은 엑셀화면에서의 내용이 입력된 데이터를 수정 해본 후,

    Ctrl + Z 키를 누르면 원복을 하는 기능을 ExtJS의 그리드를 이용하여 제작해볼까 합니다.▼



    이번 예제의 흐름은 다음과 같습니다.

    1. DB테이블에 등록되어있는 데이터를 그리드에 출력

    2. CellEditing 플러그인을 이용하여 컬럼수정

    3. 수정이 되면 즉시, 데이트 Update

    4. Ctrl + Z 를 이용하여 이전 데이터로 원복

       (화면이 변경되는 동시에 3과 동일하게 이전데이터로 DB Update)


    DB연동없이 구현해보고자 하는 기능을 제작할 수 있겠지만,

    웹에서 동작이 되고, 그리드의 CellEditing을 이용하여 Update 처리를 이용하여 무언가를 제작하고자하여 생각한 끝에 위의 이미지처럼 동작되는 예제를 만들기로 결정하였습니다.

    예제를 구현하려다 보니, 지금까지는 다루지 않았던 Class 및 Config / Events 들이 다양하게 나올 것입니다.

    우선 전제적인 코드를 작성과 결과확인을 본 다음, 각 코드별로 설명을 진행하도록 하겠습니다.


    클라이언트코드

    
    // Global Variable
    // Ctrl + z -> before data set
    Ext.define('globalData', { 
        singleton: true, 
        temp: null 
    });  
    
    Ext.onReady(function(){
    	Ext.create('Ext.grid.Panel',{
    		plugins: [Ext.create('Ext.grid.plugin.CellEditing',{clicksToEdit: 1})],
    		height : 300,
    		title : 'ExtJS Example',
    		columns : [{
    			text : '번호',
    			flex : 1,
    			dataIndex : 'pk',
    			hidden : true
    		},{
    			text : '제목',
    			flex : 1,
    			dataIndex : 'title',
    			editor: {
                    allowBlank: false
                }
    		},{
    			text : '내용',
    			flex : 1,
    			dataIndex : 'contents',
    			editor: {
    				xtype : 'textarea',
                    allowBlank: false
                }
    		},{
    			text : '작성자',
    			flex : 1,
    			dataIndex : 'author',
    			editor: {
                    allowBlank: false
                }
    		},{
    			text : '등록일',
    			flex : 1,
    			dataIndex : 'create_date',
    			editor: {
    				allowBlank: false
                }
    		}],
    		store : Ext.create('Ext.data.Store',{
    			//add
    			autoSync : true,
    			autoLoad : true	,
    			fields : ['title','contents','author','create_date'],
    			proxy : {
    				type : 'ajax',
    				api : {
    					//add
    					update : './server2_update.jsp',
    					read : './server2_select.jsp'
    				},
    				reader : {
    					type : 'json',
    					rootProperty : 'data'
    				},
    				//add
    				writer : {
    					type : 'json',
    					writeAllFields : true,
    					encode : true,
    					rootProperty : 'data'
    				}
    			}
    		}),
    		listeners : {
    			//add
    			edit: function (editor, e, eOpts) {
    				if(globalData.temp == null) {
    					globalData.temp = []; 
    				}
    				globalData.temp.push([editor.context.rowIdx, editor.context.field, editor.context.originalValue]);
                }, 
                //add
                afterrender: function(obj, opt) {
                	new Ext.util.KeyMap({
                        target: document,
                        binding: [{
                            key: "z",
                            ctrl:true,
                            fn: function(){
                                 if(globalData.temp != null && globalData.temp.length > 0) {
                                	 var store = obj.getStore();
                                	 var temp = globalData.temp;
                                	 var length = temp.length-1;
                                	 
                                	 store.getData().getAt(temp[length][0]).set(temp[length][1],temp[length][2]);
                                	 globalData.temp.pop(length);
                                 } else {
                                	 return;
                                 }
                            }
                        }],
                        scope: this
                    }); 
                }
    		},
    		renderTo : Ext.getBody()
    	})
    })
    
    


    Update를 위한 서버코드(server2_update.jsp)

    
    Connection conn = null;
    PreparedStatement pstmt = null;
    ResultSet rs = null;
    try{
    	Object obj = JSONValue.parse(request.getParameter("data"));
    	JSONObject req=(JSONObject)obj;
    	JSONObject res = new JSONObject();
    	
    	/**
    	*	JDBC 정보 설정 (MySQL)
    	*/
    	String url = "jdbc:mysql://localhost:3306/test";
    	String id = "사용자아이디";
    	String pwd = "패스워드";                             
    	
    	Class.forName("com.mysql.jdbc.Driver");
    	conn=DriverManager.getConnection(url,id,pwd);
    	
    	String query = "UPDATE blog SET title = ?, contents = ?, create_date = ?, author = ? WHERE idx = ? ";
            System.out.println(query);
    	pstmt = conn.prepareStatement(query);
    	
    	pstmt.setString(1, (String)req.get("title"));
    	pstmt.setString(2, (String)req.get("contents"));
    	pstmt.setString(3, (String)req.get("create_date"));
    	pstmt.setString(4, (String)req.get("author"));
    	pstmt.setInt(5, Integer.parseInt((String)req.get("pk")));
    	
    	int count = pstmt.executeUpdate();
    	
    	res.put("success", true);
    	
    	response.setContentType("text/plain; charset=UTF-8");
    	PrintWriter pw = response.getWriter();
    	pw.print(res);
    	pw.flush();
    	pw.close();
    }catch(Exception e){
    	e.printStackTrace();
    }
    
    


    코드실행결과



    코드에서 현재까지 다루지 않았던 내용을 위주로 설명을 드리겠습니다.

    
    Ext.define('globalData', { 
        singleton: true, 
        temp: null 
    });  
    
    

    Ext.onReady 호출 전, 위처럼 코드를 정의해주었습니다.

    전역변수를 설정하기 위하여 'globalData'라는 명칭으로 define 해주었습니다.

    질문 :  일반 자바스크립트처럼 var globalData; 로 선언해도 되지않나요?

    답 : 지금은 문법을 배우기 위한 포스팅을 하기때문에 위처럼 선언하지 않아도 가능하지만, 이후 빌드작업을 거쳐서 웹어플리케이션을 제작할때는 위와같이 선언을 해주어야 한다는군요.

    한국 센차포럼의 운영자이신 '김종광님'의 내용을 참고하였습니다.

    선언문법이 다른거 같습니다. 우선은 참고하세요. ▼

    관련글 보기


    이전 기초강좌에서 다루었던 코드에 추가 config 및 코드들을 서술하였습니다.

    첫번째 컬럼을 보시면 hidden : true 속성이 추가되었는데, 그리드화면에 출력을 시키지 않기 위해서 hidden값을 주었습니다.

    dataIndex가 'pk' 는 변하지 않는 자동증가값이므로, 화면에 보여질 필요도 없고, DB Update 처리시에만 사용되는 값이기 때문입니다.

    나머지는 동일합니다.

    데이터 스토어부분으로 넘어가서, autoSync 라는 config가 추가되었습니다.

    이 속성은 그리드의 컬럼이 변경되는 즉시, 등록 또는 수정 또는 삭제를 하겠다라는 의미입니다.

    우리는 실시간으로 컬럼이 변경될때마다 데이터 수정을 해주기 위함이므로 autoSync : true를 선언해주었습니다.

    이어서, proxy → api 의 config를 보면 이전에는 read만 있었는데, update config가 추가되었습니다.

    api conifg는 총 4가지를 선언할 수 있습니다.

    create / read / update / destroy 

    자주 사용하는 CRUD 기능에 대한 URL 정의하는 CONFIG 값입니다.

    그리드에 발생되는 이벤트에따라 알아서 정의 된 URL을 호출합니다.

    그리드 셀이 수정될 경우 - update config에 정의된 URL 호출 및 수정된 데이터 전송

    그리드 로우가 삭제될 경우 - destory config에 정의된 URL 호출 및 삭제 데이터 전송

    그리드  로우가 신규추가될 경우 - create config에 정의된 URL 호출 및 추가된 데이터 전송

    그리드의 load 호출 시 - read config에 정의된 URL 호출


    우리는 그리드 행을 신규로 추가하지 않고, 삭제또한 하지 않으므로 필요한 CONFIG가 update와 read만을 선언하였습니다.

    create / update / destory url 호출시, 패키지형태의 설정이 필요합니다.

    read URL 호출할때 필요한 'reader' config처럼 create / update / destory에 필요한 config는 'writer' 입니다.

    'writer' config의 자식속성들을 확인하면, 4가지(type/writeAllFields/encode/rootProperty) 를 선언하였습니다.

    writeAllFields : 변경된 cell만 전달할 것인지, 아니면 변경된 셀의 row데이터들을 넘길것인지여부

    encode : 인코딩여부

    rootProperty : reader에서는 응답받은 json object의 root key값이었지만, writer에서는 서버에서 요청을 받기위해 정의하는 key값 

    ※ writer에서 넘겨주는 데이터를 JSON Object가 아닌, JSON 문자열로 넘어갑니다.


    rootProperty에 선언하는 값을 이해를 쉽게 하려면, 서버페이지에서 request.getParameter("data")를 보시면되겠습니다.

    바로 클라이언트로부터 넘어온 값을 받기위하여 rootProperty에 정의된 value값으로 받은 것입니다.

    HTML의 INPUT 태그는 name="data"로 서버에서 받는다면, ExtJS 그리드의 전송되는 값은 rootProperty로부터 정의해준 값이 되겠습니다.

    문자열로 넘어가니, JSON 객체로 변경하는 작업이 필요합니다.


    리스너를 제외한 나머지 코드만을 이용한다면, 실시간 데이터수정만 가능합니다.

    리스너에서 추가로 구현한 기능은 아래와 같습니다.

    전역변수에 이전 데이터를 배열형태로 Add해주는 기능

    KeyMap 클래스를 이용하여 단축키(ShortKey) 로 Ctrl + Z에 대한 이벤트 발생시 동작되는 기능 구현


    각 리스너 코드를 한번 보도록 하겠습니다.

    
    edit: function (editor, e, eOpts) {
    	if(globalData.temp == null) {
    		globalData.temp = []; 
    	}
    	globalData.temp.push([editor.context.rowIdx, editor.context.field, editor.context.originalValue]);
    }
    
    

    그리드패널 이벤트 중에서 'edit' 이벤트가 존재합니다.

    동작되는 시점은 셀이 수정되는 동시에 발생되는 이벤트입니다.

    내부에 정의 된 함수를 간단하게 설명해보자면,

    전역변수로 선언한 'globalData'의 temp 개체가 null일경우에는 초기에 배열선언을 해주고, 변경된 셀의 행과 dataIndex값 그리고 변경직전에 원본데이터를 배열에 담아 PUSH


    다음 코드를 보시겠습니다.


    
    afterrender: function(obj, opt) {
                	new Ext.util.KeyMap({
                        target: document,
                        binding: [{
                            key: "z",
                            ctrl:true,
                            fn: function(){
                                 if(globalData.temp != null && globalData.temp.length > 0) {
                                	 var store = obj.getStore();
                                	 var temp = globalData.temp;
                                	 var length = temp.length-1;
                                	 
                                	 store.getData().getAt(temp[length][0]).set(temp[length][1],temp[length][2]);
                                	 globalData.temp.pop(length);
                                 } else {
                                	 return;
                                 }
                            }
                        }],
                        scope: this
                    }); 
    }
    
    


    'afterrender' 이벤트는 그리드가 렌더링 된 이후 동작되는 기능을 구현하는 이벤트입니다.

    'Ext.util.KeyMap' 은 키 이벤트를 제어할때 사용되는 유틸성 클래스입니다.

    target은 document로 정의해주었고, binding config내에 ctrl 속성은 true / 동시에 눌리는 키는 'z'로 정의를 해주어서 ' Ctrl + Z ' 에대한 이벤트 발생시, fn config내에 정의된 기능을 동작시킵니다.

    fn에 정의한 내용은 전역변수의 남아있는 값이 존재하면 해당 데이터스토어에 존재하는 영역에 이전데이터를 적용시킨 후, 전역변수내에 정의되어있는 배열을 제거합니다.


    설명으로는 어려운 부분들이 많았지만, 

    listeners / global variable/ KeyMap 

    을 제외한 나머지는 이해가 되셨을거라 생각하겠습니다.

    상단에 3가지 기능은 추후 기초 강좌를 통하여 다시한번 알아보도록 하겠습니다.

    또한, 그리드 CRUD 전체를 이용하는 방식은 추후 ExtJS MVC 기반 또는 MVVM 구현할때 예제로 다루도록 하겠습니다.


    다음장은 트리에 대해서 알아보도록 하겠습니다.




    Posted by 몽고