function newExcl02010Control(pageName, doctx="[data-doctx='excl02010']") {
    let ctrl = new DatasetControl({
          doctx: doctx
        , prefix: "opnn"
        , prefixName: "의견제출"
        , dataGetter: obj => obj["List"]
        , keys: ["OPNN_ID"]
        , appendData: true
        , infoSize: "xl"
        , urls: {
              load: "/list.do"
            , getInfo: "/info.do"
            , remove: "/remove.do"
        }
        , formats: {
              RCPT_YMD: dateFormat
            , ANS_YMD: dateFormat
            , ANS_YMD_TM: datetimeFormat
            , CRDN_YMD_TM: datetimeFormat
            , FFNLG_CRDN_AMT: numberFormat
            , FFNLG_AMT: numberFormat
            , REG_DT: datetimeFormat
            , MDFCN_DT: datetimeFormat
        }
    });

    // 검색 조건
    ctrl.query = { pageNum: 1, fetchSize: DEFAULT_FETCH_SIZE };                 // 1 페이지당 자료 건수 index.jsp에서 확인
    // paging
    ctrl.pagingSupport = new FimsPagingSupport({
          ctrl: ctrl                                                            // DatasetControl
        , ignoreCtrlPrefix: true                                                // ctrl.prefix 가 있더라도 무시
        , linkContainer: "[name='dataPaging']"                                  // <ul class="pagination pagination-primary" name="dataPaging">
    });

    /**************************************************************************
     * DatasetControl Events
     **************************************************************************/
    // Dataset 변경 이벤트
    ctrl.onDatasetChange = (obj, option) => {
        // 데이터 셋
        let dataList = ctrl.dataset;
        let empty = dataList.empty;
        let clsForTask = $(ctrl.find("[name='frmSearch']")).find("[name='taskSeCd']:checked").val().toLowerCase();

        // 업무별 dataTables(그리드) tr, td
        let foundContent = ctrl.find("[name='tmpltRows']").content;
        let foundTr = $(foundContent).find("tr")[0].cloneNode(false);
        let foundTds = $(foundContent).find("." + clsForTask + ",.cmn");

        foundTds.each(function() {
            foundTr.appendChild(this.cloneNode(true));
        });

        let replacer = (str, dataItem) => str
            .replace(/{onclick}/gi, "pageObject['" + pageName + "'].dataTableClick('{data-index}');")
            .replace(/{ondblclick}/gi, "pageObject['" + pageName + "'].dataTableDblClick('{data-index}');");

        let trs = empty ?
                      [ctrl.find("[name='tmpltNotFound']").content.querySelector("." + clsForTask + ",.cmn").outerHTML]
                    : dataList.inStrings(foundTr.outerHTML, replacer);

        let noMore = (dataList.length >= obj["Paging"]?.totalSize);             // 전체 건수
        let initScroll = (ctrl.query.pageNum < 2);

        if (option != null && option.reloaded) {
            initScroll = false;
        }

        // dataTables(그리드)에 자료 표시 td
        ctrl.find("[name='tableRspns']").changeContent(trs.join(), initScroll, noMore);

        // checkbox 체크 해제
        $(ctrl.find("[name='gridAllCheckbox']")).prop("checked", false);

        // dataTables(그리드)에 페이지 표시 pagingInfo
        ctrl.pagingSupport.setPagingInfo(obj);

        // 보안모드
        fn_securityModeToggle($("#securityMode--top").is(":checked"));
    };

    // 현재 선택 자료 변경 이벤트
    ctrl.onCurrentChange = (item) => {
        Apply.fromDataset.currentRow(ctrl.dataset, item, ctrl.find("[name='tbodyList']"));
    };

    // 선택(체크) 변경 이벤트
    ctrl.onSelectionChange = (selectedArr) => {
        // gridCheckbox
        Apply.fromDataset.gridCheckbox(ctrl.dataset, ctrl.find("[name='tbodyList']"), 1, selectedArr);
        // gridButton
        Apply.fromDataset.gridButton(ctrl.dataset, ctrl.find("[name='btnRemove']"), selectedArr);           // 삭제 버튼
        Apply.fromDataset.gridButton(ctrl.dataset, ctrl.find("[name='btnPrintDlbrDecsn']"), selectedArr);   // 심의의결서 출력 버튼
    };

    // 상세 정보 dialog
    ctrl.getInfo = (params) => {
        if (!params) return;

        ajax.get({
              url: ctrl.urls.getInfo
            , data: params || {}
            , success: (resp) => {
                let dialogName = ctrl.prefixed("Dialog");
                let dialogId = dialogName + "-" + uuid();

                dialog.open({
                      id: dialogId
                    , title: params.btnTitle
                    , size: ctrl.infoSize
                    , content: resp
                    , init: () => {
                        $("#" + dialogId).attr("name", dialogName);
                        $("#" + dialogId).attr("data-ref-doctx", pageName);

                        AppSupport.setDialogZindex();
                    }
                    , onClose: () => { ctrl.reload({all : true}); }             // 자료 재조회
                });
            }
        });
    }

    // 삭제 callback
    ctrl.onRemove = (params, resp) => {
        let showMessage = resp.rtnMsg.replace(/[S]|[F]/g, params.btnTitle);

        // 메시지 출력
        dialog.alert({
              content: showMessage
            , init: () => { AppSupport.setDialogZindex(); }
            , onOK: resp.saved ? undefined : () => { }
            , onClose: () => {
                if (resp.saved) { ctrl.reload({ all: true }); }                 // 자료 재조회
            }
        });
    }

    // 삭제
    ctrl.remove = (params) => {
        if (!params) return;

        ajax.post({
              url: ctrl.urls.remove
            , data: params || {}
            , success: (resp) => { ctrl.onRemove(params, resp); }
        });
    }

    return ctrl;
}
