이 강좌의 결과를 확인하시려면 여기를 클릭하시면 샘플프로그램을 실행해 볼수 있습니다. 단지 post전송은 git에 의해 막혀있어 처리되지 않습니다. 참고하세요.

eui프레임웍은 자체 그리드를 통해 CRUD를 쉽게 처리할 수 있도록 지원합니다. 이번 강좌에서는 euigrid의 명령버튼을 포함시키고 로우를 추가,삭제,수정 등의 기능을 구현해 보도록 하겠습니다.

eui-sample 앱은 universal app로 생성하였으므로 뷰는 classic폴더에 포함됨을 알고 있자.

Step 1 : 신규 클래스 정의 및 사용할 eui클래스.

파일명 경로 기능 설명
Eui.sample.model.Base eui-sample/app Model클래스
Eui.sample.model.Message eui-sample/app Model클래스
Eui.sample.view.grid.BasicController eui-sample/app ViewController
Eui.sample.view.grid.BasicModel eui-sample/app ViewModel
Eui.sample.view.grid.Basic eui-sample/classic 메인클래스
Eui.sample.view.grid.RecordForm eui-sample/classic 입력 및 수정폼 클래스
eui.form.Panel eui-core 테이블레이아웃용 폼패널
eui.toolbar.Command eui-core 명령버튼 제공
eui.grid.Panel eui-core CRUD등 기능제공
eui.form.Label eui-core 폼레이블
eui.form.field.Text eui-core 폼텍스트필드

Step 2 : 클래스 작성하기.

  • Eui.sample.view.grid.Basic 클래스는 우리가 작성할 샘플프로그램의 메인 클래스이다. item으로 grid를 포함하고 있다.
Ext.define('Eui.sample.view.grid.Basic', {
    extend: 'Ext.panel.Panel',
    xtype: 'sample-basic-grid',
    
    requires: [
        'Eui.sample.view.grid.RecordForm',      // 등록 및 수정 폼
        'Eui.sample.view.grid.BasicModel',      // 뷰모델 클래스
        'Eui.sample.view.grid.BasicController'  // 뷰컨트롤러 클래스
    ],
    controller: 'sample-basic-grid',
    viewModel: 'sample-basic-grid',
    layout: 'fit',
    items: [
        {
            xtype: 'grid',
            plugins: {  
                ptype: 'cellediting',   // 셀에디터를 추가.
                clicksToEdit: 2         // 더블클릭을 통해 에디터로 변환됨.
            },
            selModel: {     // 그리로우를 클릭시 체크박스를 통해 선택되며 체크와 체크해제
                mode: 'SIMPLE',
                selType: 'checkboxmodel'
            },
           
            bind: {
                store: '{mystore}'      // ViewModel클래스에 정의됨.
            },
            
            columns: [
                {
                    text: '#{행추가2}',
                    width: 100,
                    dataIndex: 'MSG_ID',
                    editor: {
                        bind: "{messageRecord.MSG_ID}", //뷰 모델 내부에 정의됨.
                        xtype: 'textfield'
                    }
                },
                {
                    text: 'MSG_LABEL',
                    flex: 1,
                    dataIndex: 'MSG_LABEL',
                    editor: {
                        bind: "{messageRecord.MSG_LABEL}",
                        xtype: 'textfield'
                    }
                }
            ]
        }
    ]
});
  • Eui.sample.view.grid.BasicController 클래스를 작성한다. 이 클래스는 몸체만 있다. 구체적 구현은 이후에 하도록 한다.
Ext.define('Eui.sample.view.grid.BasicController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.sample-basic-grid'
});
  • Eui.sample.view.grid.BasicModel 클래스를 작성한다. 이 클래스는 뷰모델 클래스로 그리드에서 사용할 스토어와 그 외 formulas등의 구현이 포함되어 있다.
    • messageRecord : 그리드 로우를 선택시 선택된 레코드 정보를 담을 뷰모델 변수다.
    • mystore : Eui.sample.model.Message 모델클래스를 기반으로 하는 스토어를 정의했다.
Ext.define('Eui.sample.view.grid.BasicModel', {
    extend : "Ext.app.ViewModel",
    alias: 'viewmodel.sample-basic-grid',
    requires: ['Eui.sample.model.Message'],
    formulas : {
        messageRecord: {
            bind: {
                // reference가 myGrid
                bindTo: "{myGrid.selection}",
                deep: true
            },
            get: function (b) {
                return b
            }
        }
    },
    stores: {
        mystore: {
            model: 'Eui.sample.model.Message',
            autoLoad: true
        }
    }
});
  • Eui.sample.model.Message 모델 클래스를 정의하자. 이 클래스는 proxy CRUD를 위한 프록시 정보를 담고 있다. 주의 깊게 봐야할 것은 Eui.sample.model.Base모델을 확장하고 있고 Base모델 클래스에는 Message모델에 없는 fields요소가 정의되어 있음을 기억하고 이후 추가하도록 하자.
Ext.define('Eui.sample.model.Message', {
    extend: 'Eui.sample.model.Base',
    identifier: {
        type: 'sequential',
        prefix: 'MSG'
    },
    proxy: {
        type: 'ajax',
        actionMethods: {
            read: 'GET',
            create: 'POST',
            update: 'POST',
            destroy: 'POST'
        },

        api: {
            create: 'resources/data/mygridadddata.json?create',
            read: 'resources/data/mygriddata.json?read',
            update: 'resources/data/mygridadddata.json?update',
            destroy: 'resources/data/mygriddata.json?destroy'
        },
        reader: {
            type: 'json',
            rootProperty: 'data'
        },
        writer: {
            type: 'json',
            allowSingle: false,
            writeAllFields: true
        },
        listeners: {
            exception: function (proxy, response, operation) {
                var json = Ext.decode(response.responseText);
                if (json) {
                    Ext.MessageBox.show({
                        title: 'Save Failed',
                        msg: json.message,
                        icon: Ext.MessageBox.ERROR,
                        buttons: Ext.Msg.OK
                    });
                } else
                    Ext.MessageBox.show({
                        title: 'EXCEPTION',
                        msg: operation.getError(),
                        icon: Ext.MessageBox.ERROR,
                        buttons: Ext.Msg.OK
                    });
            }
        }
    }
});
  • Eui.sample.model.Base 클래스를 작성하자. 이 모델 클래스의 목적은 필드를 정의하고 각 필드별로 validators를 추가하여 각 필드가 조건에 맞는 값을 가질수 있도록 하기 위함이며 이 클래스를 확장해 다른 모델 클래스를 작성하기 위함이다.
Ext.define('Eui.sample.model.Base', {
    extend: 'Ext.data.Model',
    requires: ['Ext.data.validator.Length'],
    fields: [
        {
            name: "MSG_ID",
            type: "string",
            validators: [
                {
                    type: "length",
                    min: 10,
                    minOnlyMessage: "MSG_ID must have at least 3 characters"
                }
            ]
        },
        {
            name: "MSG_LABEL",
            type: "string",
            validators: [
                {
                    type: "length",
                    min: 4,
                    minOnlyMessage: "MSG_LABEL must have at least 3 characters"
                }
            ]
        }
    ]
});
  • 이제 Message 모델 클래스 proxy api에서 사용할 데이터 파일을 정의하자. 이 파일은 서버시이드 역할을 할 것이다. 위치는 resources/data/mygriddata.json이다.
{
    "success": "true",
    "message": "Error message goes here.",
    "data": [
        {
            "id":"111", 
            "MSG_ID": "F000000122", 
            "MSG_LABEL": "신청일자를 입력해 주세요."
        },
        {
            "id":"112", 
            "MSG_ID": "F000000129", 
            "MSG_LABEL": "시간은 0~23 사이만 입력 가능합니다."
        },
        {
            "id":"113", 
            "MSG_ID": "F000000130", 
            "MSG_LABEL": "성명을 입력해 주시기 바랍니다."
        },
        {
            "id":"114", 
            "MSG_ID": "F000000131", 
            "MSG_LABEL": "날짜 형식을 yymmdd 형식으로 입력해 주세요."
        }
    ]
}
  • 코딩이 끝났다면 실행해보도록 하자. 여기까지는 일반적인 ExtJS를 위한 코드이다.

Alt text

Step 3 : euigrid를 이용한 CRUD 코딩하기.

  • 이제 평범한 그리드를 euigrid로 변형시키고 crud기능을 위한 commandtoolbar를 추가하도록 하겠다. 아래 추가한 코드는 모두 eui프레임웍에서 제공하는 기능이다.
Ext.define('Eui.sample.view.grid.Basic', {
    extend: 'Ext.panel.Panel',
    xtype: 'sample-basic-grid',
    title: '#{행추가2}',
    requires: [
        'eui.grid.Panel',       // Ext.grid.Panel클래스를 확장한 eui-core용 그리드 클래스.
        'eui.toolbar.Command',  // 명령버튼 제공
        ...
    ],
    controller: 'sample-basic-grid',
    viewModel: 'sample-basic-grid',
    layout: 'fit',
    items: [
        {
            xtype: 'euigrid',   // 기존 grid를 수정.
            ..
            usePagingToolbar: true, // 그리드에 페이징 툴바를 추가.
            tbar: [
                {
                    showRowAddBtn: true,    // 행추가 버튼 활성화
                    showRowDelBtn: true,    // 행삭제 버튼 활성화
                    showRegBtn: true,       // 등록 버튼 활성화
                    showModBtn: true,       // 수정 버튼 활성화
                    showSaveBtn: true,      // 저장 버튼 활성화
                    showReloadBtn: true,    // 조회 버튼 활성화
                    xtype: 'commandtoolbar' // eui.toolbar.Command 클래스
                }
            ],
            
            ..
            // commandtoolbar 내부 버튼들이 발생시키는 이벤트 처리.
            listeners: {                // ViewController클래스에 정의됨.
                select: 'onGridSelect',
                regBtnClick: 'onRowReg',
                rowDeleteBtnClick: 'onRowDelete',
                modBtnClick: 'onRowMod',
                rowAddBtnClick: 'onRowAdd',
                saveBtnClick: 'onRowSave'
            },
            ...
        }
    ]
});
  • 이제 다시 브라우저를 통해 실행해보자. 아래 그림처럼 여러 종류의 버튼들이 활성화 되는 것을 볼 수 있을 것이다.

Alt text

  • 행추가 기능 구현하기.

행추가 버튼을 클릭하게 되면 commandtoolbar클래스는 자신을 포함하고 있는 grid 또는 form클래스에 rowAddBtnClick 이벤트 리스너가 존재하는 지 확인하고 존재한다면 rowAddBtnClick 이벤트를 발생시킨다. rowAddBtnClick이벤트가 없다면 grid의 onRowAdd 메소드를 호출하도록 되어 있다.

// eui.toolbar.Command
{
    xtype: 'euibutton',
    text: '#{행추가}',
    iconCls: '#{행추가아이콘}',
    scope: me,
    showText: me.getShowText(),
    hidden: !me.getShowRowAddBtn(),
    listeners: {
        click: function () {
            if (owner.hasListeners['rowAddBtnClick'.toLowerCase()]) {
                owner.fireEvent('rowAddBtnClick', owner);
            } else {
                owner.onRowAdd(owner, {
                    randomInt: Ext.Number.randomInt(1, 1000000000000)
                }, 0, null);
            }
        }
    }
},

이 메소드는 euigrid 내부에 구현되어 있으며 그리드가 가진 store에 빈레코드를 추가하는 코드를 구현하고 있다. 즉 우리는 행추가 버튼클릭을 위한 이벤트 리스너를 작성하지 않아도 기본적인 로우 추가는 가능하다는 것이다.

우리는 이미 rowAddBtnClick 이벤트 리스너를 구현하였으나 뷰컨트롤러 내부에 메소드를 구현하지는 않았다. BasicController클래스 내부에 아래처럼 onRowAdd 메소드를 구현하도록 하자.

아래 코드처럼 onRowAdd 메소드 내부에서는 grid.onRowAdd메소드를 다시 호출하고 있다. 여기서 grid는 euigrid를 의미하며 euigrid내부에는 이미 onRowAdd메소드가 구현되어 있기때문에 실행이 가능하다.

Ext.define('Eui.sample.view.grid.BasicController', {
    extend: 'Ext.app.ViewController',

    ...
    onRowAdd: function (grid) {
        //euigrid 내부 메소드
        grid.onRowAdd(grid, {
            MSG_ID: 'M'+Ext.Number.randomInt(1, 1000000000)
        }, 0, function () {    // callback이 필요할 경우 구현한다.
            console.log(' 콜백처리...', arguments)
        });
    }
});

여기서 euigrid의 onRowAdd메소드의 인자를 설명하고 가자.

- grid : euigrid
- data : 모델에 추가될 필드별 값이 설정된 오브젝트를 의미하며 형식은 
        { name: '홍길동', address: '서울시..' }와 같다.
- idx   : 0일 경우 store.add({})를 호출하여 store의 맨뒤에 추가되며 
          0이상일 경우 store.insert(idx, {})를 호출하여 특정 위치에 데이터를 추가할 수 있다. 
- callback  : 콜백은 데이터 입력 이후 후처리 함수를 지정할 수 있다.
/***
 * 행추가 처리.
 * @param grid
 * @param data  : 추가할 모델 데이터 
 * @param idx   : 원하는 위치가 있다면 위치정보를 설정.
 * @param callback  : 로우를 추가한 다음 처리할 로직
 ***/
onRowAdd(grid, data, idx, callback)

이제 브라우저를 통해 실행하고 결과를 확인하자. 아래 그림처럼 로우가 추가되고 BasicController의 onRowAdd내부에 구현된 것 처럼 MSG_ID에 M이 포함된 문자열이 표시되는 것을 볼수 있을 것이다.

Alt text

  • 행삭제 기능 구현

행삭제 버튼을클릭하면 commandtoolbar는 대상 그리드에 rowDeleteBtnClick이벤트를 발생시킨다. 뷰컨트롤러에 onRowDelete메소드를 구현하도록 하자. onRowDelete메소드 내부에서 euigrid의 onRowDelete메소드를 호출하는 것으로 간단히 처리하고 있다.

Ext.define('Eui.sample.view.grid.BasicController', {
    extend: 'Ext.app.ViewController',

    ...
    
     onRowDelete: function (grid) {
         //euigrid 내부 메소드
         grid.onRowDelete(grid, function (store, records) {
             store.remove(records);
             store.sync({
                 success: function () {
                     Ext.Msg.alert(Util.getLocaleValue('CONFIRM'),  Util.getLocaleValue('M90004'));
                 }
             });
         }, grid);
     },
});

여기서 euigrid의 onRowDelete메소드의 인자를 설명하고 가자. 첫번째 인자는 그리드 자체를 의미하고 두번째인자인 callback은 "삭제하시겠습니까?" 메시지가 출력되고 "예"를 클릭하면 이후 처리는 사용자가 정의한 콜백 메소드대로 처리되는 것이다. callback메소드의 인자는 store와 그리드에 선택한 로우집합을 전달하게 되므로 사용자는 이 두개의 인자를 활용해 후 처리를 진행하는 것이다. records 변수는 현재 그리드에 선택된 한개 이상의 로우를 의미하며 store.remove메소드에 전달해 스토어에서 제거하게된다. 이후 store.sync메소드를 호출해 삭제된 로우를 서버사이드에 전달하여 삭제처리할 수 있도록 통신한다.

/***
 * 행삭제 처리.
 * @param grid
 * @param callback  : 로우를 추가한 다음 처리할 로직
 ***/
onRowDelete: function (grid, callback, scope)
  • 등록 기능 구현하기.

등록 버튼을 클릭하면 commandtoolbar는 그리드에 regBtnClick이벤트를 호출하게 된다. 일반적으로 등록 기능은 그리드 내부에 처리하기 보다는 등록용 팝업을 띄우는 용도로 사용된다. 리스터를 통해 onRowReg메소드를 호출하게 되고 컨트롤러 내부에 구현체를 아래와 같이 작성하자.

onRowReg: function () {
    // 뷰모델에 messageRecord라는 이름으로 모델을 생성하는 코드다.
    var record = Ext.create('Eui.sample.model.Message', {
        MSG_ID: 'M' + Ext.Number.randomInt(1, 1000000000)
    })
    this.getViewModel().set('messageRecord', record);

    // Util클래스의 팝업용 메소드를 이용해 Eui.sample.view.grid.RecordForm이라는 
    // 클래스를 윈도우에 포함시켜 윈도우 팝업을 호출하는 코드다. 
    Util.commonPopup(
        this.getView(), // 1. Eui.sample.view.grid.Basic클래스를 의미
        Util.getLocaleValue('M90003'),  // 2. 윈도우의 title설정.
        'Eui.sample.view.grid.RecordForm',  // 3. 팝업으로 사용할 클래스.
        530,    // 4. 너비
        200,    // 5. 높이
        null,   // 6. params
        {       // 7. 윈도우 옵션
            modal: true
        }, 
        true    // 8. true일 경우 (1)클래스의 자식으로 추가되어 
                //      컨트롤로와 뷰모델을 같이 사용하게된다.
    ).show();
},

등록에 필요한 입력 폼 클래스를 작성하자. 위의 코드에서 언급한 Eui.sample.view.grid.RecordForm 다.

Ext.define('Eui.sample.view.grid.RecordForm', {
    extend: 'eui.form.Panel',
    xtype: 'sample-basic-recordform',
    requires: [
        'eui.form.Label',
        'eui.form.field.Text'
    ],
    hiddenHeader: true, // 윈도우 내부에 자식으로 들어가므로 header는 없앴다.
    tableColumns: 2,    // eui form클래스는 기본이 table레이아웃이므로 컬럼 수를 지정한다.
    margin: 5,

    tbar: [
        '->',
        {
            showSaveBtn: true,  // 저장버튼
            showCloseBtn: true, // 닫기버튼
            xtype: 'commandtoolbar'
        }
    ],

    listeners: {    // 저장버튼 클릭.
        saveBtnClick: 'onSaveForm'
    },
    items: [
        {
            xtype: 'euilabel',
            text: '메시지 코드',
            allowBlank: false   // 레이블에 필수 체크.
        },
        {
            xtype: 'euitext',
            bind: '{messageRecord.MSG_ID}'  // 바인드 변수를 사용한다.
        },
        {
            xtype: 'euilabel',
            text: '메시지 내용',
            allowBlank: false
        },
        {
            xtype: 'euitext',
            bind: '{messageRecord.MSG_LABEL}'
        }
    ]
});

결과를 확인하자. euitext는 textfield를 확장한 클래스이다. 바인드를 이용해 MSG_ID와 MSG_LABEL필드를 설정했다. 아래 그림처럼 메시지 내용이 붉은 색으로 입력을 기다리는 것은 이 필드에 validator가 설정되어 있기 때문이다.

Alt text

이제 메시지 내용을 채워 Eui.sample.model.Base모델에 설정된 validator를 만족시켜 입력한다. validator를 만족시키면 disabled되었던 "저장"버튼은 enabled될 것이다.

저장 버튼을 클릭하면 RecordForm클래스 내부 리스너에 의해 onSaveForm메소드를 호출할 것이다. 이 메소드는 BaseController에 구현하면 될 것이다. BaseController에 구현하는 것은 팝업을 띄울때 Util.commonPopup메소드의 첫번째 인자와 마지막 인자에 의해 가능하다. 그렇지 않을 경우 RecordForm클래스는 별도의 컨트롤러를 작성하여 사용해야 한다.

onSaveForm 메소드를 구현하자.

onSaveForm: function (form) {
    var me = this;
    // 모델을 통해 save메소드를 호출한다. Message모델 내부 proxy정보를 참고하자.
    this.getViewModel().get("messageRecord").save({
        success: function (rec) {
            // 저장이 성공하면 모델을 그리드에 추가한다.
            me.getViewModel().getStore('mystore').add(rec);
            // 입력폼을 닫는다.
            form.up('window').close();
        },
        callback: function () {

        }
    });
}
  • 수정기능 구현하기

수정기능 또한 등록 기능과 동일한 형태이다. 수정 버튼이 클릭되면 modBtnClick 이벤트를 발생시켜 onRowMod메소드를 실행시키도록 코딩하였다.

listeners: {                // ViewController클래스에 정의됨.
    ..
    modBtnClick: 'onRowMod',
    ..
},

컨트롤러에 onRowMod메소드를 구현하자. 그리드의 수정기능은 로우를 선택하고 "수정"버튼을 클릭하면 해당 로우의 데이터를 입력시 사용한 폼을 통해 수정할 수 있도록 하는 것이다. 이때 체크해야할 것은 로우를 선택했는지와 한개 이상의 로우를 선택할 경우 경고하는 것이다.

onRowMod: function (grid) {
    var records = grid.getSelection();  // 선택된 로우를 구한다.
    if (records.length == 0) {  // 로우가 선택되지 않은 경우 경고한다.
        Ext.Msg.alert(Util.getLocaleValue('CONFIRM'), 
                    Util.getLocaleValue('M90001'));
        return;
    }
    if (records.length > 1) {   // 로우가 한개 이상일 경우 경고한다.
        Ext.Msg.alert(Util.getLocaleValue('M90002'), 
        '한건의 로우만 선택하세요.');
        return;
    }
    // 입력시와 동일하게 폼을 윈도우를 통해 호출한다.
    Util.commonPopup(this.getView(), 
        Util.getLocaleValue('M90003'), 
        'Eui.sample.view.grid.RecordForm', 
        530, 
        200, 
        null, 
        {
            modal: truep
        }, 
        true
    ).show();
},

이후 수정 폼이 윈도우를 통해 보여지고 "저장"버튼을 클릭하면 이전의 "등록"기능과 동일하다. Alt text

Alt text

  • 저장 기능 구현하기

저장버튼을 클릭하면 saveBtnClick 이벤트가 발생하고 이 이벤트를 컨트롤러에 구현하도록 하자. 메소드는 onRowSave이다. 저장 기능은 등록과 수정과 달리 그리드에 추가,수정,삭제 된 로우정보를 store의 sync를 이용해 서버사이드로 전달하는 간단한 기능이다. 우리는 추가기능이 포함된 eui 메소드인 checkSync를 통해 처리전 모델의 validation을 충족하지 못하는 필드에 대한 경고를 할 수 있다.

onRowSave: function (grid) {
    Ext.Msg.show({
        title: Util.getLocaleValue('CONFIRM'),
        buttons: Ext.Msg.YESNO,
        icon: Ext.Msg.QUESTION,
        message: Util.getLocaleValue('M90005'),
        fn: function (btn) {
            if (btn === 'yes') {
                // store.sync() 대신 사용한다.(eui 기능)
                grid.store.checkSync({
                    success: function () {
                        Ext.Msg.alert(Util.getLocaleValue('CONFIRM'), Util.getLocaleValue('M90006'));
                    }
                });
            }
        }
    });
},

이제 아래 그림처럼 로우를 더블 클릭해 MSG_LABEL필드를 수정하고 저장을 클릭해 위의 onRowSave메소드를 실행시키자.

Alt text

Alt text

"Yes"버튼을 클릭하면 아래 그림처럼 모델의 필드에 설정한 validation의 메시지를 출력하며 저장처리가 진행되지 않을 것이다. 이 처럼 그리드의 수정내용이 적합한지 판단하기 위해 모델을 정의하고 모델필드별로 validator를 설정하게 되면 여러모로 편리한 작업이 가능하게 된다.

Alt text

위 그림의 메시지는 아래의 Base모델의 MSG_LABEL필드에 설정된 length 용 valitor의 메시지 임을 확인할 수 있다.

Ext.define('Eui.sample.model.Base', {
    extend: 'Ext.data.Model',
    requires: ['Ext.data.validator.Length'],
    fields: [
       ...
        {
            name: "MSG_LABEL",
            type: "string",
            validators: [
                {
                    type: "length",
                    min: 4,
                    minOnlyMessage: "MSG_LABEL must have at least 3 characters"
                }
            ]
        }
    ]
});