function newSprt02040ControlSD(pageName, doctx="[data-doctx='sprt02040']", mainPageName="sprt02010") {
    let ctrlSndngDtl = new DatasetControl({
          doctx: doctx
        , prefix: "sndngDtl"
        , prefixName: "발송반송"
        , dataGetter: obj => obj["sndngDtls"]
        , keys: ["SNDNG_DTL_ID"]
        , appendData: true
        , infoSize: "md"
        , urls: {
              load: "/list.do"
            , getInfo: "/info.do"
        }
        , formats: {
              SNDNG_YMD: dateFormat
            , DUDT_YMD: dateFormat
            , LAST_AMT: numberFormat
            , DLVR_YMD: dateFormat
            , DLVR_TM: timeFormat
            , REG_DT: datetimeFormat
            , MDFCN_DT: datetimeFormat
        }
    });

    /**************************************************************************
     * DatasetControl Events
     **************************************************************************/
    // Dataset 변경 이벤트
    ctrlSndngDtl.onDatasetChange = (obj, option) => {
        // 데이터 셋
        let dataList = ctrlSndngDtl.dataset;
        let empty = dataList.empty;

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

        let trs = empty ?
                      [ctrlSndngDtl.find("[name='tmpltNotFoundSndngDtl']").innerHTML]
                    : dataList.inStrings(ctrlSndngDtl.find("[name='tmpltSndngDtlRows']").innerHTML, replacer);

        // dataTables(그리드)에 자료 표시 td
        $(ctrlSndngDtl.find("[name='tbodySndngDtlList']")).html(trs.join());
    };

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

        if (!item) return;

        // 가상계좌번호
        let vrActno = Array.from({length: 20}, (x, i) => {
            let no = i + 1
              , bank = "BANK_NM" + (no > 1 ? no : "")
              , account = "VR_ACTNO" + (no > 1 ? no : "");

            return [bank, account]
        })
        .map(entry => {
            let bank = item.data[entry[0]]
              , account = item.data[entry[1]];

            return bank ? bank + " " + account : null;
        })
        .filter(txt => txt != null)
        .join("\n");

        $(ctrlSndngDtl.find("[name='vrActno']")).val(vrActno);                  // 가상 계좌번호
    };

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

        ajax.get({
              url: wctx.url("/sndb/sndb01/230/info.do")
            , data: params || {}
            , success: (resp) => {
                let dialogName = "sndbkDialog";
                let dialogId = dialogName + "-" + uuid();

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

                        AppSupport.setDialogZindex();
                    }
                    , onClose: () => { pageObject[pageName].fnRefresh(); }      // 정보 새로고침
                });
            }
        });
    }

    // 삭제 callback
    ctrlSndngDtl.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) { pageObject[pageName].fnRefresh(); }           // 정보 새로고침
            }
        });
    }

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

        ajax.post({
              url: wctx.url("/sndb/sndb01/220/remove.do")
            , data: params || {}
            , success: (resp) => { ctrlSndngDtl.onRemove(params, resp); }
        });
    }

    return ctrlSndngDtl;
}

function newSprt02040ControlSV(pageName, doctx="[data-doctx='sprt02040']", mainPageName="sprt02010") {
    let ctrlSvbtc = new DatasetControl({
          doctx: doctx
        , prefix: "svbtc"
        , prefixName: "공시송달"
        , dataGetter: obj => obj["svbtcDtls"]
        , keys: ["SNDNG_DTL_ID"]
        , appendData: true
        , infoSize: "md"
        , urls: {
              load: "/list.do"
            , getInfo: "/info.do"
        }
        , formats: {
              SNDNG_YMD: dateFormat
            , SVBTC_YMD: dateFormat
            , REG_DT: datetimeFormat
        }
    });

    /**************************************************************************
     * DatasetControl Events
     **************************************************************************/
    // Dataset 변경 이벤트
    ctrlSvbtc.onDatasetChange = (obj, option) => {
        // 데이터 셋
        let dataList = ctrlSvbtc.dataset;
        let empty = dataList.empty;

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

        let trs = empty ?
                      [ctrlSvbtc.find("[name='tmpltNotFoundSvbtc']").innerHTML]
                    : dataList.inStrings(ctrlSvbtc.find("[name='tmpltSvbtcRows']").innerHTML, replacer);

        // dataTables(그리드)에 자료 표시 td
        $(ctrlSvbtc.find("[name='tbodySvbtcList']")).html(trs.join());
    };

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

    return ctrlSvbtc;
}
