package cokr.xit.fims.sndb.web;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

import org.apache.poi.ss.usermodel.CellStyle;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import cokr.xit.applib.Print;
import cokr.xit.base.code.CommonCode;
import cokr.xit.base.docs.xls.CellDef;
import cokr.xit.base.docs.xls.Format;
import cokr.xit.base.docs.xls.Style;
import cokr.xit.base.docs.xls.XLSWriter;
import cokr.xit.base.file.service.FileQuery;
import cokr.xit.base.file.service.bean.FileBean;
import cokr.xit.base.web.ApplicationController;
import cokr.xit.fims.base.service.bean.OgdpBean;
import cokr.xit.fims.cmmn.CmmnQuery;
import cokr.xit.fims.cmmn.FimsPrintOption;
import cokr.xit.fims.cmmn.OtptStngQuery;
import cokr.xit.fims.cmmn.OutsourcingFileMaker;
import cokr.xit.fims.cmmn.OutsourcingStngQuery;
import cokr.xit.fims.cmmn.dao.OtptStngMapper;
import cokr.xit.fims.cmmn.hwp.format.SvbtcList;
import cokr.xit.fims.cmmn.pdf.print.format.Advntce;
import cokr.xit.fims.cmmn.pdf.print.format.Nht;
import cokr.xit.fims.cmmn.service.bean.OutsourcingStngBean;
import cokr.xit.fims.cmmn.service.bean.StngBean;
import cokr.xit.fims.cmmn.xls.FormatMaker;
import cokr.xit.fims.cmmn.xls.StyleMaker;
import cokr.xit.fims.crdn.Crdn;
import cokr.xit.fims.mngt.service.bean.TaskBean;
import cokr.xit.fims.mngt.service.bean.TaskProcessor;
import cokr.xit.fims.sndb.SndbQuery;
import cokr.xit.fims.sndb.Sndng;
import cokr.xit.fims.sndb.SndngDtl;
import cokr.xit.fims.sndb.Svbtc;
import cokr.xit.fims.sndb.service.SndngDtlService;
import cokr.xit.fims.sndb.service.SndngService;
import cokr.xit.fims.sndb.service.SvbtcService;
import cokr.xit.fims.sndb.service.bean.SvbtcBean;
import cokr.xit.fims.sprt.SprtQuery;
import cokr.xit.fims.sprt.dao.IntegrationSearchMapper;
import cokr.xit.fims.sprt.service.IntegrationSearchService;
import cokr.xit.fims.sprt.service.bean.MediaBean;
import cokr.xit.fims.task.Task;
import cokr.xit.foundation.UserInfo;
import cokr.xit.foundation.data.DataObject;
import cokr.xit.foundation.data.DataProc;

import cokr.xit.fims.levy.LevyQuery;
import cokr.xit.fims.levy.Levy;
import cokr.xit.fims.levy.service.LevyService;
import cokr.xit.fims.levy.service.bean.LevyBean;

/**발송대장 서비스의 웹 컨트롤러
*
* <p>상세 설명:
*
* <pre>
* ============ 변경 이력 ============
* 2023-08-24    JoJH 최초 작성
* ================================
* </pre>
*/
@Controller
@RequestMapping(name="발송반송", value=Sndb01Controller.CLASS_URL)
public class Sndb01Controller extends ApplicationController {

    public static final String CLASS_URL = "/sndb/sndb01";

    public class METHOD_URL {
        public static final String
              warningTargetMain                     = "/010/main.do"                    // 계고장 발송 대상 메인 화면
            , getWarningTargetList                  = "/010/list.do"                    // 계고장 발송 대상 목록 조회
            , getWarningTargetInfo                  = "/020/info.do"                    // 계고장 발송 대상 조회
            , createWarningSendingList              = "/020/createList.do"              // 계고장 발송 대상 목록의 발송 등록
            , warningSendingMain                    = "/030/main.do"                    // 계고장 발송 현황 메인 화면
            , getWarningSendingList                 = "/030/list.do"                    // 계고장 발송 현황 목록 조회
            , removeWarningSending                  = "/030/remove.do"                  // 계고장 발송 현황 삭제
            , getWarningSendingDetailInfo           = "/040/info.do"                    // 계고장 발송 현황 상세 화면
            , getWarningSendingDetailList           = "/040/list.do"                    // 계고장 발송 현황 상세 목록 조회
            , advanceNoticeTargetMain               = "/050/main.do"                    // 사전통지 발송 대상 메인 화면
            , getAdvanceNoticeTargetList            = "/050/list.do"                    // 사전통지 발송 대상 목록 조회
            , getAdvanceNoticeTargetInfo            = "/060/info.do"                    // 사전통지 발송 대상 조회
            , createAdvanceNoticeSendingList        = "/060/createList.do"              // 사전통지 발송 대상 목록의 발송 등록
            , advanceNoticeSendingMain              = "/070/main.do"                    // 사전통지 발송 현황 메인 화면
            , getAdvanceNoticeSendingList           = "/070/list.do"                    // 사전통지 발송 현황 목록 조회
            , removeAdvanceNoticeSending            = "/070/remove.do"                  // 사전통지 발송 현황 삭제
            , getAdvanceNoticeSendingDetailInfo     = "/080/info.do"                    // 사전통지 발송 현황 상세 화면
            , getAdvanceNoticeSendingDetailList     = "/080/list.do"                    // 사전통지 발송 현황 상세 목록 조회
            , createSendingLink                     = "/080/createSndngLink.do"         // 사전통지 발송 현황 상세 목록의 우편통합 발송 연계 등록
            , makeAdvanceNoticePdf                  = "/080/makeAdvntcePdf.do"          // 사전통지 발송 현황 사전통지서 출력(PDF)
            , makeAdvanceNoticeOutsourcing          = "/080/makeAdvntceOutsourcing.do"  // 사전통지 발송 현황 사전통지서 출력(외주파일)
            , noticeSheetTargetMain                 = "/090/main.do"                    // 고지서 발송 대상 메인 화면
            , getNoticeSheetTargetList              = "/090/list.do"                    // 고지서 발송 대상 목록 조회
            , getNoticeSheetTargetInfo              = "/100/info.do"                    // 고지서 발송 대상 조회
            , createNoticeSheetSendingList          = "/100/createList.do"              // 고지서 발송 대상 목록의 발송 등록
            , noticeSheetSendingMain                = "/110/main.do"                    // 고지서 발송 현황 메인 화면
            , getNoticeSheetSendingList             = "/110/list.do"                    // 고지서 발송 현황 상세 목록 조회
            , removeNoticeSheetSending              = "/110/remove.do"                  // 고지서 발송 현황 삭제
            , getNoticeSheetSendingDetailInfo       = "/120/info.do"                    // 고지서 발송 현황 화면
            , getNoticeSheetSendingDetailList       = "/120/list.do"                    // 고지서 발송 현황 상세 목록 조회
            , makeNoticeSheetPdf                    = "/120/makeNhtPdf.do"              // 고지서 발송 현황 사전통지서 출력(PDF)
            , makeNoticeSheetOutsourcing            = "/120/makeNhtOutsourcing.do"      // 고지서 발송 현황 사전통지서 출력(외주파일)
            , sendingDetailInfo                     = "/210/info.do"                    // 수기 반송 등록 상세
            , getSendingDetailList                  = "/210/list.do"                    // 수기 반송 등록 조회
            , sendBackMain                          = "/220/main.do"                    // 반송 현황 메인 화면
            , getSendBackList                       = "/220/list.do"                    // 반송 현황 목록 조회
            , removeSendBack                        = "/220/remove.do"                  // 반송 삭제
            , getSendBackInfo                       = "/230/info.do"                    // 반송 정보 조회
            , createSendBackList                    = "/230/createSndbkList.do"         // 사전통지 발송 상세 목록의 반송 확인(등록)
            , createSendBack                        = "/230/create.do"                  // 반송 등록
            , updateSendBack                        = "/230/update.do"                  // 반송 수정
            , serviceByPublicNoticeMain             = "/240/main.do"                    // 공시송달 현황 메인 화면
            , getServiceByPublicNoticeList          = "/240/list.do"                    // 공시송달 현황 목록 조회
            , removeServiceByPublicNotice           = "/240/remove.do"                  // 공시송달 삭제
            , makeSvbtcFileFromHwpFormat            = "/240/makeSvbtcHwp.do"            // 공시송달 한글 파일 출력
            , getServiceByPublicNoticeDetailInfo    = "/250/info.do"                    // 공시송달 상세 화면
            , getServiceByPublicNoticeDetailList    = "/250/list.do"                    // 공시송달 상세 목록 조회
            , makeSvbtcDetailsFileFromHwpFormat     = "/250/makeSvbtcDtlHwp.do"         // 공시송달 상세 한글 파일 출력
            , removeServiceByPublicNoticeEach       = "/250/remove.do"                  // 공시송달 개별 삭제
            , getServiceByPublicNoticeInfo          = "/260/info.do"                    // 공시송달 정보
            , createServiceByPublicNotice           = "/260/create.do"                  // 공시송달 등록
            , updateServiceByPublicNotice           = "/260/update.do"                  // 공시송달 수정

            , setNonTaxReceiptIndividualA01List     = "/991/nxrpA01List.do"             // 과태료대장 단속정보 등록(실시간) A01
            , getNonTaxReceiptIndividualA03List     = "/991/nxrpA03List.do"             // 과태료대장 상세조회(실시간) A03
            , getNonTaxReceiptSpecialB01List        = "/991/nxrpB01List.do"             // 체납상세세목정보(실시간) B01
        ;
    }

    /**발송대장 서비스*/
    @Resource(name="sndngService")
    private SndngService sndngService;
    /**발송 상세 서비스*/
    @Resource(name="sndngDtlService")
    private SndngDtlService sndngDtlService;
    /**공시송달 대장 서비스*/
    @Resource(name="svbtcService")
    private SvbtcService svbtcService;
    /**업무 정보 서비스*/
    @Resource(name="taskBean")
    private TaskBean taskBean;
    /**부과 서비스*/
    @Resource(name="levyService")
    private LevyService levyService;    
    /**통합 조회 서비스*/
    @Resource(name="integrationSearchService")
    private IntegrationSearchService integrationSearchService;
    @Resource(name="stngBean")
    private StngBean stngBean;
    @Resource(name="fileBean")
    private FileBean fileBean;
    @Resource(name = "ogdpBean")
    private OgdpBean ogdpBean;
    @Resource(name="svbtcBean")
    private SvbtcBean svbtcBean;
    @Resource(name="mediaBean")
    private MediaBean mediaBean;
    @Resource(name="otptStngMapper")
    private OtptStngMapper otptStngMapper;
    @Resource(name="outsourcingStngBean")
    protected OutsourcingStngBean outsourcingStngBean;
    @Resource(name="integrationSearchMapper")
    private IntegrationSearchMapper integrationSearchMapper;

    /**계고장 발송 대상 자료 메인화면(sndb/sndb01/010-main)을 연다.
     * @return /sndb/sndb01/010-main
     */
    @RequestMapping(name = "계고장 발송 대상 메인", value = METHOD_URL.warningTargetMain)
    public ModelAndView warningTargetMain() {
        // View(jsp)에서 사용할 공통코드를 조회
    	List<String> cdGrps = TaskProcessor.get().getVltnCdGrps();
    	cdGrps.add("FIM022"); // 부과 제외 사유 코드(LEVY_EXCL_RSN_CD)
    	cdGrps.add("FIM054"); // 업무구분 코드(TASE_SE_CD)
    	String[] groupIDs = cdGrps.toArray(new String[cdGrps.size()]);
        Map<String, List<CommonCode>> commonCodes = getCodesOf(groupIDs);

        ModelAndView mav = new ModelAndView("fims/sndb/sndb01010-main")
			.addObject("pageName", "sndb01010")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
			.addObject("prefixUrl", CLASS_URL)                              // prefixUrl
			.addObject("sggCd", currentUser().getOrgID())                        // 시군구 코드(SGG_CD)
			.addObject("taskListForSgg", stngBean.filterTaskSectionCodeForSgg(commonCodes.get("FIM054")));   // 시군구에서 사용하는 업무 목록

        commonCodes.forEach((grpID, codes) -> mav.addObject(grpID + "List", codes));

        return addCodes(commonCodes, mav, groupIDs);
    }

    /**계고장 발송 대상 목록을 조회하여 반환한다.
     * {@link SndngService#getSndngList(SndngQuery)} 참고
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "wrngSndngTrgtList": [계고장 발송 대상 목록]
     *     "wrngSndngTrgtStart": 계고장 발송 대상 목록 시작 인덱스
     *     "wrngSndngTrgtFetch": 한 번에 가져오는 계고장 발송 대상 목록 수
     *     "wrngSndngTrgtTotal": 조회 결과 찾은 전체 계고장 발송 대상 수
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="계고장 발송 대상 목록 조회", value=METHOD_URL.getWarningTargetList)
    public ModelAndView getWarningTargetList(SndbQuery req) {
        if (!"xls".equals(req.getDownload())) {
            List<?> result = sndngService.getWrngTrgtList(setFetchSize(req));
            return setPagingInfo(new ModelAndView("jsonView"), result, "");
        } else {
            List<CellDef> cellDefs = fromJson(req.getCellDefs(), CellDef.listType());

            XLSWriter xlsx = new XLSWriter().worksheet(0);
            Format format = new Format(xlsx);
            CellStyle center = format.cellStyle(Style.CENTER);
            CellStyle numeric = format.n_nn0();
            CellStyle dateYMD = format.yyyy_mm_dd();
            CellStyle dateDT = format.yyyy_mm_dd_hh_mm_ss();

            List<DataObject> list = sndngService.getWrngTrgtList(req.setFetchSize(0));

            Map<String,Object> valueMap = new HashMap<String,Object>();
            valueMap.put("업무구분", format.of("TASK_SE_NM").style(center));
            valueMap.put("계고일자", FormatMaker.yyyy_mm_dd(format, "LEVY_EXCL_YMD").style(dateYMD));
            valueMap.put("계고사유", format.of("LEVY_EXCL_RSN_NM"));
            valueMap.put("기타내용", format.of("ETC_CN"));
            valueMap.put("민원신청번호", format.of("CVLCPT_APLY_NO").style(center));
            valueMap.put("민원접수번호", format.of("CVLCPT_RCPT_NO").style(center));
            valueMap.put("민원접수일자", FormatMaker.yyyy_mm_dd(format, "CVLCPT_RCPT_YMD").style(dateYMD));
            valueMap.put("민원처리담당자", format.of("CVLCPT_PRCS_PIC_NM").style(center));
            valueMap.put("민원처리결과", format.of("CVLCPT_PRCS_NM").style(center));
            valueMap.put("단속일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "CRDN_YMD_TM").style(dateDT));
            valueMap.put("차량번호", format.of("VHRNO"));
            valueMap.put("위반항목", format.of("VLTN_ARTCL"));
            valueMap.put("단속법정동", format.of("CRDN_STDG_NM"));
            valueMap.put("단속장소", format.of("CRDN_PLC"));
            valueMap.put("상세단속장소", format.of("DTL_CRDN_PLC"));
            valueMap.put("단속원금", format.of("FFNLG_CRDN_AMT").style(numeric));
            valueMap.put("처리상태", format.of("CRDN_STTS_NM"));
            valueMap.put("납부자명", format.of("RTPYR_NM"));
            valueMap.put("납부자생일", format.of("RTPYR_BRDT_MASK").style(center));
            valueMap.put("우편번호", format.of("ZIP").style(center));
            valueMap.put("주소", format.of("ADDR"));
            valueMap.put("상세주소", format.of("DTL_ADDR"));
            valueMap.put("단속특별구역", format.of("CRDN_SPAREA_NM").style(center));
            valueMap.put("사용연료", format.of("USE_FUEL_NM").style(center));
            valueMap.put("주차가능여부", format.of("PRK_PSBLTY_RSLT_NM").style(center));
            valueMap.put("등록일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "REG_DT").style(dateDT));
            valueMap.put("등록사용자", format.of("RGTR_NM").style(center));
            valueMap.put("수정일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "MDFCN_DT").style(dateDT));
            valueMap.put("수정사용자", format.of("MDFR_NM").style(center));

            CellDef.setValues(cellDefs, valueMap);

            xlsx.cell(0, 0).value("계고장 발송 대상").value(center).merge(0, cellDefs.size() - 1)
                .cell(3, 0).rowValues(CellDef.header(cellDefs, () -> StyleMaker.headerStyle(xlsx)))
                .cell(4, 0).values(list, CellDef.values(cellDefs.stream().map(i -> {if (i.getValue() instanceof Format) {i.setField(null);}; return i;}).toList()));

            return new ModelAndView("downloadView")
                .addObject("download", xlsx.getDownloadable().setFilename("계고장발송대상" + "_목록_" + now() + ".xlsx"));
        }
    }

    private String now() {
        return dateFormats.format("yyyyMMdd_HHmmss", System.currentTimeMillis());
    }

    /**계고장 발송 대상 정보 화면(sndb/sndb01/020-info)을 연다.
     * @param crdnIds 단속 Ids
     * @return fims/sndb/sndb01020-info 또는 jsonView
     * <pre>{
     *     "wrngSndngTrgts": [계고장 발송 대상 목록]
     * }</pre>
     */
    @Task
    @RequestMapping(name="계고장 발송 대상 상세조회", value=METHOD_URL.getWarningTargetInfo)
    public ModelAndView getWarningTargetInfo(HttpServletRequest hReq, SndbQuery req) {
        ModelAndView mav = getWarningTargetList(req);

        if (jsonResponse()) {
            mav.setViewName("jsonView");
        } else {
        	String egp002 = "EGP002"; // 우편취급 구분
    		Map<String, List<CommonCode>> commonCodes = getCodesOf(egp002);
            mav.setViewName("fims/sndb/sndb01020-info");

            mav.addObject("callPurpose", req.getCallPurpose())                  // 호출 용도
                .addObject("pageName", "sndb01020")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
                .addObject("prefixUrl", CLASS_URL)                              // prefixUrl
                .addObject("sggCd", req.getSggCd())                             // 시군구 코드(SGG_CD)
                .addObject("taskSeCd", req.getTaskSeCd())                       // 업무 구분 코드(TASK_SE_CD)
                .addObject("vltnCd", req.getSchVltnCd())                        // 위반 코드(VLTN_CD)
                .addObject("mainQuery", toJson(req))                            // 검색 조건
                .addObject("List", toJson(mav.getModel().get("List")))          // 데이터
                .addObject("Paging", toJson(mav.getModel().get("Paging")))      // 페이징
                .addObject(egp002, toJson(commonCodes.get(egp002)))
            ;
        }

        return mav;
    }

    /**계고장 발송 대상을 조회하여 발송대장에 등록한다.
     * @param req 계고장 발송 대상 조회, sndng 발송대장
     * @return jsonView
     * <pre><code> {
     *     "saved": 등록되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="계고장 발송 등록", value=METHOD_URL.createWarningSendingList)
    public ModelAndView createWarningSendingList(SndbQuery req, Sndng sndng) {
    	DataProc result = sndngService.createWrngSndngList(req, sndng);

        return new ModelAndView("jsonView")
	        .addObject("saved", result.isSuccess())
	        .addObject("sndng", result.getTarget())
	        .addObject("rtnMsg", result.getMessage());
    }

    /**계고장 발송 현황 메인화면(sndb/sndb01/030-main)을 연다.
     * @return /sndb/sndb01/030-main
     */
    @RequestMapping(name = "계고장 발송 현황 메인", value = METHOD_URL.warningSendingMain)
    public ModelAndView warningSendingMain() {
        // View(jsp)에서 사용할 공통코드를 조회
        Map<String, List<CommonCode>> commonCodes = getCodesOf("FIM054", "FIM067");

        return new ModelAndView("fims/sndb/sndb01030-main")
            .addObject("pageName", "sndb01030")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
            .addObject("prefixUrl", CLASS_URL)                              // prefixUrl
            .addObject("sggCd", currentUser().getOrgID())                        // 시군구 코드(SGG_CD)
            .addObject("taskListForSgg", stngBean.filterTaskSectionCodeForSgg(commonCodes.get("FIM054")))    // 시군구에서 사용하는 업무 목록
            .addObject("FIM067List", commonCodes.get("FIM067"))             // 발송 등록 구분 코드(SNDNG_REG_SE_CD)
        ;
    }

    /**계고장 발송 현황 목록을 조회하여 반환한다.
     * {@link SndngService#getWarningSendingList(SndngQuery)} 참고
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "sndngList": [계고장 발송 현황 목록]
     *     "sndngStart": 계고장 발송 현황 목록 시작 인덱스
     *     "sndngFetch": 한 번에 가져오는 계고장 발송 현황 목록 수
     *     "sndngTotal": 조회 결과 찾은 전체 계고장 발송 현황 수
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="계고장 발송 현황 목록 조회", value=METHOD_URL.getWarningSendingList)
    public ModelAndView getWarningSendingList(SndbQuery req) {
        if (!"xls".equals(req.getDownload())) {
            List<?> result = sndngService.getWrngSndngList(setFetchSize(req));
            return setPagingInfo(new ModelAndView("jsonView"), result, "");
        } else {
            List<CellDef> cellDefs = fromJson(req.getCellDefs(), CellDef.listType());

            XLSWriter xlsx = new XLSWriter().worksheet(0);
            Format format = new Format(xlsx);
            CellStyle center = format.cellStyle(Style.CENTER);
            CellStyle numeric = format.n_nn0();
            CellStyle dateYMD = format.yyyy_mm_dd();
            CellStyle dateDT = format.yyyy_mm_dd_hh_mm_ss();

            List<DataObject> list = sndngService.getWrngSndngList(req.setFetchSize(0));

            Map<String,Object> valueMap = new HashMap<String,Object>();
            valueMap.put("업무구분", format.of("TASK_SE_NM").style(center));
            valueMap.put("등록구분", format.of("SNDNG_REG_SE_NM").style(center));
            valueMap.put("위반항목", format.of("VLTN_ARTCL"));
            valueMap.put("발송일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_YMD").style(dateYMD));
            valueMap.put("납기일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_END_YMD").style(dateYMD));
            valueMap.put("총건수", format.of("TNOCS").style(numeric));
            valueMap.put("총금액", format.of("GRAMT").style(numeric));
            valueMap.put("문서번호", format.of("DOC_NO"));
            valueMap.put("제목", format.of("TTL_NM"));
            valueMap.put("등록일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "REG_DT").style(dateDT));
            valueMap.put("등록사용자", format.of("RGTR_NM").style(center));
            valueMap.put("수정일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "MDFCN_DT").style(dateDT));
            valueMap.put("수정사용자", format.of("MDFR_NM").style(center));

            CellDef.setValues(cellDefs, valueMap);

            xlsx.cell(0, 0).value("계고장 발송 현황").value(center).merge(0, cellDefs.size() - 1)
                .cell(3, 0).rowValues(CellDef.header(cellDefs, () -> StyleMaker.headerStyle(xlsx)))
                .cell(4, 0).values(list, CellDef.values(cellDefs.stream().map(i -> {if (i.getValue() instanceof Format) {i.setField(null);}; return i;}).toList()));

            return new ModelAndView("downloadView")
                .addObject("download", xlsx.getDownloadable().setFilename("계고장발송현황" + "_목록_" + now() + ".xlsx"));
        }
    }

    /**계고장 발송 현황 - 발송상세 화면(sndb/sndb01/040-info)을 연다.
     * @param sndngId 발송 ID
     * @return fims/sndb/sndb01040-info 또는 jsonView
     * <pre>{
     *     "sndngDtls": [계고장 발송 상세 목록]
     * }</pre>
     */
    @Task
    @RequestMapping(name="계고장 발송 현황 상세 화면", value=METHOD_URL.getWarningSendingDetailInfo)
    public ModelAndView getWarningSendingDetailInfo(HttpServletRequest hReq, SndbQuery req) {
        ModelAndView mav = getWarningSendingDetailList(req);

        if (jsonResponse()) {
            mav.setViewName("jsonView");
        } else {
            mav.setViewName("fims/sndb/sndb01040-info");

            mav.addObject("callPurpose", req.getCallPurpose())                 // 호출 용도
                .addObject("pageName", "sndb01040")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
                .addObject("prefixUrl", CLASS_URL)                              // prefixUrl
                .addObject("sggCd", req.getSggCd())                             // 시군구 코드(SGG_CD)
                .addObject("taskSeCd", req.getTaskSeCd())                       // 업무 구분 코드(TASK_SE_CD)
                .addObject("sndngId", req.getSndngId())                         // 발송 ID(SNDNG_ID)
                .addObject("mainQuery", toJson(req))                            // 검색 조건
                .addObject("List", toJson(mav.getModel().get("List")))          // 데이터
                .addObject("Paging", toJson(mav.getModel().get("Paging")))      // 페이징
            ;
        }

        return mav;
    }

    /**계고장 발송 현황 - 발송상세 목록을 조회하여 반환한다.
     * {@link SndngService#getSndngList(SndngQuery)} 참고
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "sndngDtlList": [발송상세 목록]
     *     "sndngDtlStart": 발송상세 목록 시작 인덱스
     *     "sndngDtlFetch": 한 번에 가져오는 발송상세 목록 수
     *     "sndngDtlTotal": 조회 결과 찾은 전체 발송상세 수
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="계고장 발송 현황 상세 목록 조회", value=METHOD_URL.getWarningSendingDetailList)
    public ModelAndView getWarningSendingDetailList(SndbQuery req) {
        if (!"xls".equals(req.getDownload())) {
            List<?> result = sndngDtlService.getSndngDtlList(setFetchSize(req));
            return setPagingInfo(new ModelAndView("jsonView"), result, "");
        } else {
            List<DataObject> list = sndngDtlService.getSndngDtlList(req.setFetchSize(0));
            List<CellDef> cellDefs = fromJson(req.getCellDefs(), CellDef.listType());

            XLSWriter xlsx = new XLSWriter().worksheet(0);
            Format format = new Format(xlsx);
            CellStyle center = format.cellStyle(Style.CENTER);
            CellStyle numeric = format.n_nn0();
            CellStyle dateYMD = format.yyyy_mm_dd();
            CellStyle dateDT = format.yyyy_mm_dd_hh_mm_ss();

            Map<String,Object> valueMap = new HashMap<String,Object>();
            valueMap.put("발송상태", format.of("SNDNG_DTL_STTS_NM").style(center));
            valueMap.put("등기번호", format.of("RG_NO").style(center));
            valueMap.put("발송일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_YMD").style(dateYMD));
            valueMap.put("납기일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_END_YMD").style(dateYMD));
            valueMap.put("성명", format.of("RCPN_NM"));
            valueMap.put("우편번호", format.of("RCPN_ZIP").style(center));
            valueMap.put("주소", format.of("RCPN_ADDR"));
            valueMap.put("상세주소", format.of("RCPN_DTL_ADDR"));
            valueMap.put("배달일자", FormatMaker.yyyy_mm_dd(format, "DLVR_YMD").style(dateYMD));
            valueMap.put("배달시각", format.of("DLVR_TM").style(center));
            valueMap.put("미배달사유", format.of("UNDLVR_RSN_NM"));
            valueMap.put("수령인", format.of("ACTL_RCPN_NM"));
            valueMap.put("수령인관계", format.of("RCPN_REL_NM"));
            valueMap.put("단속일시", format.of("CRDN_DT").style(center));
            valueMap.put("차량번호", format.of("VHRNO"));
            valueMap.put("단속법정동", format.of("CRDN_STDG_NM"));
            valueMap.put("단속장소", format.of("CRDN_PLC"));
            valueMap.put("과태료금액", format.of("FFNLG_AMT").style(numeric));
            valueMap.put("등록일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "REG_DT").style(dateDT));
            valueMap.put("등록사용자", format.of("RGTR_NM").style(center));
            valueMap.put("수정일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "MDFCN_DT").style(dateDT));
            valueMap.put("수정사용자", format.of("MDFR_NM").style(center));

            CellDef.setValues(cellDefs, valueMap);

            xlsx.cell(0, 0).value("계고장 발송 상세").value(center).merge(0, cellDefs.size() - 1)
                .cell(3, 0).rowValues(CellDef.header(cellDefs, () -> StyleMaker.headerStyle(xlsx)))
                .cell(4, 0).values(list, CellDef.values(cellDefs.stream().map(i -> {if (i.getValue() instanceof Format) {i.setField(null);}; return i;}).toList()));

            return new ModelAndView("downloadView")
                .addObject("download", xlsx.getDownloadable().setFilename("계고장 발송상세" + "_목록_" + now() + ".xlsx"));
        }
    }

    /**지정한 계고장 발송대장을 제거한다.
     * @param sndng 발송대장
     * @return jsonView
     * <pre><code> {
     *     "saved": 등록되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="계고장 발송대장 제거", value=METHOD_URL.removeWarningSending)
    public ModelAndView removeWarningSending(Sndng sndng) {
        String rtnMsg = "";

        // 발송 ID 단건 삭제
        if (isEmpty(sndng.getSndngIds())) {
            rtnMsg = sndngService.removeWrngSndng(sndng);
        } else {
            // 발송 IDs 만큼 발송 삭제 반복..
            for (String sndngId : sndng.getSndngIds()) {
                Sndng deltSndng = new Sndng();
                deltSndng.setSndngId(sndngId);
                deltSndng.setDelRsn(sndng.getDelRsn());

                // 발송 삭제 호출
                rtnMsg = sndngService.removeWrngSndng(deltSndng);
                if (!rtnMsg.contains("[S]"))                                    // 오류가 발생하였으면 종료..
                    break;
            }
        }

        return new ModelAndView("jsonView")
            .addObject("saved", rtnMsg.contains("[S]"))
            .addObject("rtnMsg", rtnMsg);
    }

    /**사전통지 발송 대상 자료 메인화면(sndb/sndb01/050-main)을 연다.
     * @return /sndb/sndb01/050-main
     */
    @RequestMapping(name = "사전통지 발송 대상 메인", value = METHOD_URL.advanceNoticeTargetMain)
    public ModelAndView advanceNoticeTargetMain() {
        // View(jsp)에서 사용할 공통코드를 조회
    	List<String> cdGrps = TaskProcessor.get().getVltnCdGrps();
    	cdGrps.add("FIM054");
    	String[] groupIDs = cdGrps.toArray(new String[cdGrps.size()]);
        Map<String, List<CommonCode>> commonCodes = getCodesOf(groupIDs);

        ModelAndView mav = new ModelAndView("fims/sndb/sndb01050-main")
			.addObject("pageName", "sndb01050")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
			.addObject("prefixUrl", CLASS_URL)                              // prefixUrl
			.addObject("sggCd", currentUser().getOrgID())                        // 시군구 코드(SGG_CD)
			.addObject("taskListForSgg", stngBean.filterTaskSectionCodeForSgg(commonCodes.get("FIM054")));    // 시군구에서 사용하는 업무 목록
        commonCodes.forEach((grpID, codes) -> mav.addObject(grpID + "List", codes));
        return addCodes(commonCodes, mav, groupIDs);
    }

    /**사전통지 발송 대상 목록을 조회하여 반환한다.
     * {@link SndngService#getSndngList(SndngQuery)} 참고
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "sndngList": [사전통지 발송 대상 목록]
     *     "sndngStart": 사전통지 발송 대상 목록 시작 인덱스
     *     "sndngFetch": 한 번에 가져오는 사전통지 발송 대상 목록 수
     *     "sndngTotal": 조회 결과 찾은 전체 사전통지 발송 대상 수
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="사전통지 발송 대상 목록 조회", value=METHOD_URL.getAdvanceNoticeTargetList)
    public ModelAndView getAdvanceNoticeTargetList(SndbQuery req) {
        if (!"xls".equals(req.getDownload())) {
            List<?> result = sndngService.getAdvntceTrgtList(setFetchSize(req));
            return setPagingInfo(new ModelAndView("jsonView"), result, "");
        } else {
            List<CellDef> cellDefs = fromJson(req.getCellDefs(), CellDef.listType());

            XLSWriter xlsx = new XLSWriter().worksheet(0);
            Format format = new Format(xlsx);
            CellStyle center = format.cellStyle(Style.CENTER);
            CellStyle numeric = format.n_nn0();
            CellStyle dateYMD = format.yyyy_mm_dd();
            CellStyle dateDT = format.yyyy_mm_dd_hh_mm_ss();

            List<DataObject> list = sndngService.getAdvntceTrgtList(req.setFetchSize(0));

            Map<String,Object> valueMap = new HashMap<String,Object>();
            valueMap.put("업무구분", format.of("TASK_SE_NM").style(center));
            valueMap.put("민원신청번호", format.of("CVLCPT_APLY_NO").style(center));
            valueMap.put("민원접수번호", format.of("CVLCPT_RCPT_NO").style(center));
            valueMap.put("민원접수일자", FormatMaker.yyyy_mm_dd(format, "CVLCPT_RCPT_YMD").style(dateYMD));
            valueMap.put("민원처리담당자", format.of("CVLCPT_PRCS_PIC_NM").style(center));
            valueMap.put("민원처리결과", format.of("CVLCPT_PRCS_NM").style(center));
            valueMap.put("단속일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "CRDN_YMD_TM").style(dateDT));
            valueMap.put("차량번호", format.of("VHRNO"));
            valueMap.put("위반항목", format.of("VLTN_ARTCL"));
            valueMap.put("단속법정동", format.of("CRDN_STDG_NM"));
            valueMap.put("단속장소", format.of("CRDN_PLC"));
            valueMap.put("상세단속장소", format.of("DTL_CRDN_PLC"));
            valueMap.put("과태료금액", format.of("FFNLG_AMT").style(numeric));
            valueMap.put("사전통지금액", format.of("ADVNTCE_AMT").style(numeric));
            valueMap.put("처리상태", format.of("CRDN_STTS_NM"));
            valueMap.put("납부자명", format.of("RTPYR_NM"));
            valueMap.put("납부자생일", format.of("RTPYR_BRDT_MASK").style(center));
            valueMap.put("우편번호", format.of("ZIP").style(center));
            valueMap.put("주소", format.of("ADDR"));
            valueMap.put("상세주소", format.of("DTL_ADDR"));
            valueMap.put("단속특별구역", format.of("CRDN_SPAREA_NM").style(center));
            valueMap.put("사용연료", format.of("USE_FUEL_NM").style(center));
            valueMap.put("주차가능여부", format.of("PRK_PSBLTY_RSLT_NM").style(center));
            valueMap.put("등록일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "REG_DT").style(dateDT));
            valueMap.put("등록사용자", format.of("RGTR_NM").style(center));
            valueMap.put("수정일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "MDFCN_DT").style(dateDT));
            valueMap.put("수정사용자", format.of("MDFR_NM").style(center));

            CellDef.setValues(cellDefs, valueMap);

            xlsx.cell(0, 0).value("사전통지 발송 대상").value(center).merge(0, cellDefs.size() - 1)
                .cell(3, 0).rowValues(CellDef.header(cellDefs, () -> StyleMaker.headerStyle(xlsx)))
                .cell(4, 0).values(list, CellDef.values(cellDefs.stream().map(i -> {if (i.getValue() instanceof Format) {i.setField(null);}; return i;}).toList()));

            return new ModelAndView("downloadView")
                .addObject("download", xlsx.getDownloadable().setFilename("사전통지발송대상" + "_목록_" + now() + ".xlsx"));
        }
    }

    /**사전통지 발송 대상 정보 화면(sndb/sndb01/060-info)을 연다.
     * @param crdnIds 단속 Ids
     * @return fims/sndb/sndb01060-info 또는 jsonView
     * <pre>{
     *     "advntceSndngTrgs": [사전통지 발송 대상 목록]
     * }</pre>
     */
    @Task
    @RequestMapping(name="사전통지 발송 대상 상세조회", value=METHOD_URL.getAdvanceNoticeTargetInfo)
    public ModelAndView getAdvanceNoticeTargetInfo(HttpServletRequest hReq, SndbQuery req) {
        ModelAndView mav = getAdvanceNoticeTargetList(req);

        if (jsonResponse()) {
            mav.setViewName("jsonView");
        } else {
        	String egp002 = "EGP002"; // 우편취급 구분
    		Map<String, List<CommonCode>> commonCodes = getCodesOf(egp002);
            mav.setViewName("fims/sndb/sndb01060-info");

            // 사전통지 납기일자 조회
            CmmnQuery cmmnReq = new CmmnQuery()
            	.setSggCd(req.getSggCd())
            	.setTaskSeCd(req.getTaskSeCd());
            DataObject advntceInfo = taskBean.getAdvntceYmdInfo(cmmnReq);

            mav.addObject("callPurpose", req.getCallPurpose())                  // 호출 용도
                .addObject("pageName", "sndb01060")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
                .addObject("prefixUrl", CLASS_URL)                              // prefixUrl
                .addObject("sggCd", req.getSggCd())                             // 시군구 코드(SGG_CD)
                .addObject("taskSeCd", req.getTaskSeCd())                       // 업무 구분 코드(TASK_SE_CD)
                .addObject("vltnCd", req.getSchVltnCd())                        // 위반 코드(VLTN_CD)
                .addObject("mainQuery", toJson(req))                            // 검색 조건
                .addObject(egp002, toJson(commonCodes.get(egp002)))
                .addObject("advntceInfo", toJson(advntceInfo))                  // 사전통지 기간 정보
                .addObject("List", toJson(mav.getModel().get("List")))          // 데이터
                .addObject("Paging", toJson(mav.getModel().get("Paging")))      // 페이징
            ;
        }

        return mav;
    }

    /**사전통지 발송 대상을 등록한다.
     * @param sndng 발송 정보
     * @return jsonView
     * <pre><code> {
     *     "saved": 등록되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="사전통지 발송 등록", value=METHOD_URL.createAdvanceNoticeSendingList)
    public ModelAndView createAdvanceNoticeSendingList(SndbQuery req, Sndng sndng) {
        DataProc result = sndngService.createAdvntceSndngList(req, sndng);

        return new ModelAndView("jsonView")
            .addObject("saved", result.isSuccess())
            .addObject("sndng", result.getTarget())
            .addObject("rtnMsg", result.getMessage());
    }

    /**사전통지 발송 현황 메인화면(sndb/sndb01/070-main)을 연다.
     *
     * @return /sndb/sndb01/070-main
     */
    @RequestMapping(name = "사전통지 발송 현황 메인", value = METHOD_URL.advanceNoticeSendingMain)
    public ModelAndView advanceNoticeSendingMain() {
        // View(jsp)에서 사용할 공통코드를 조회
        Map<String, List<CommonCode>> commonCodes = getCodesOf("FIM054", "FIM067");

        return new ModelAndView("fims/sndb/sndb01070-main")
            .addObject("pageName", "sndb01070")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
            .addObject("prefixUrl", CLASS_URL)                              // prefixUrl
            .addObject("sggCd", currentUser().getOrgID())                        // 시군구 코드(SGG_CD)
            .addObject("taskListForSgg", stngBean.filterTaskSectionCodeForSgg(commonCodes.get("FIM054")))   // 시군구에서 사용하는 업무 목록
            .addObject("FIM067List", commonCodes.get("FIM067"))             // 발송 등록 구분 코드(SNDNG_REG_SE_CD)
        ;
    }

    /**사전통지 발송 현황 목록을 조회하여 반환한다.
     * {@link SndngService#getSndngList(SndngQuery)} 참고
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "sndngList": [사전통지 발송 현황 목록]
     *     "sndngStart": 사전통지 발송 현황 목록 시작 인덱스
     *     "sndngFetch": 한 번에 가져오는 사전통지 발송 현황 목록 수
     *     "sndngTotal": 조회 결과 찾은 전체 사전통지 발송 현황 수
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="사전통지 발송 현황 목록 조회", value=METHOD_URL.getAdvanceNoticeSendingList)
    public ModelAndView getAdvanceNoticeSendingList(SndbQuery req) {
        if (!"xls".equals(req.getDownload())) {
            List<?> result = sndngService.getAdvntceSndngList(setFetchSize(req));
            return setPagingInfo(new ModelAndView("jsonView"), result, "");
        } else {
            List<CellDef> cellDefs = fromJson(req.getCellDefs(), CellDef.listType());

            XLSWriter xlsx = new XLSWriter().worksheet(0);
            Format format = new Format(xlsx);
            CellStyle center = format.cellStyle(Style.CENTER);
            CellStyle numeric = format.n_nn0();
            CellStyle dateYMD = format.yyyy_mm_dd();
            CellStyle dateDT = format.yyyy_mm_dd_hh_mm_ss();

            List<DataObject> list = sndngService.getAdvntceSndngList(req.setFetchSize(0));

            Map<String,Object> valueMap = new HashMap<String,Object>();
            valueMap.put("업무구분", format.of("TASK_SE_NM").style(center));
            valueMap.put("등록구분", format.of("SNDNG_REG_SE_NM").style(center));
            valueMap.put("위반항목", format.of("VLTN_ARTCL"));
            valueMap.put("발송일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_YMD").style(dateYMD));
            valueMap.put("납기일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_END_YMD").style(dateYMD));
            valueMap.put("총건수", format.of("TNOCS").style(numeric));
            valueMap.put("총금액", format.of("GRAMT").style(numeric));
            valueMap.put("문서번호", format.of("DOC_NO"));
            valueMap.put("제목", format.of("TTL_NM"));
            valueMap.put("등록일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "REG_DT").style(dateDT));
            valueMap.put("등록사용자", format.of("RGTR_NM").style(center));
            valueMap.put("수정일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "MDFCN_DT").style(dateDT));
            valueMap.put("수정사용자", format.of("MDFR_NM").style(center));

            CellDef.setValues(cellDefs, valueMap);

            xlsx.cell(0, 0).value("사전통지 발송 현황").value(center).merge(0, cellDefs.size() - 1)
                .cell(3, 0).rowValues(CellDef.header(cellDefs, () -> StyleMaker.headerStyle(xlsx)))
                .cell(4, 0).values(list, CellDef.values(cellDefs.stream().map(i -> {if (i.getValue() instanceof Format) {i.setField(null);}; return i;}).toList()));

            return new ModelAndView("downloadView")
                .addObject("download", xlsx.getDownloadable().setFilename("사전통지발송현황" + "_목록_" + now() + ".xlsx"));
        }
    }

    /**지정한 사전통지 발송대장을 제거한다.
     * @param sndng 발송대장
     * @return jsonView
     * <pre><code> {
     *     "saved": 삭제되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="사전통지 발송대장 제거", value=METHOD_URL.removeAdvanceNoticeSending)
    public ModelAndView removeAdvanceNoticeSending(Sndng sndng) {
        String rtnMsg = "";

        // 발송 ID 단건 삭제
        if (isEmpty(sndng.getSndngIds())) {
            rtnMsg = sndngService.removeAdvntceSndng(sndng);
        } else {
            // 발송 IDs 만큼 발송 삭제 반복..
            for (String sndngId : sndng.getSndngIds()) {
                Sndng deltSndng = new Sndng();
                deltSndng.setSndngId(sndngId);
                deltSndng.setDelRsn(sndng.getDelRsn());

                // 발송 삭제 호출
                rtnMsg = sndngService.removeAdvntceSndng(deltSndng);
                if (!rtnMsg.contains("[S]"))                                    // 오류가 발생하였으면 종료..
                    break;
            }
        }

        return new ModelAndView("jsonView")
	        .addObject("saved", rtnMsg.contains("[S]"))
	        .addObject("rtnMsg", rtnMsg);
    }

    /**사전통지 발송 현황 - 발송상세 화면(sndb/sndb01/080-info)을 연다.
     * @param sndngId 발송 ID
     * @return fims/sndb/sndb01080-info 또는 jsonView
     * <pre>{
     *     "sndngDtls": [사전통지 발송 상세 목록]
     * }</pre>
     */
    @Task
    @RequestMapping(name="사전통지 발송 현황 상세 화면", value=METHOD_URL.getAdvanceNoticeSendingDetailInfo)
    public ModelAndView getAdvanceNoticeSendingDetailInfo(HttpServletRequest hReq, SndbQuery req) {
        ModelAndView mav = getAdvanceNoticeSendingDetailList(req);

        if (jsonResponse()) {
            mav.setViewName("jsonView");
        } else {
            mav.setViewName("fims/sndb/sndb01080-info");

            mav.addObject("callPurpose", req.getCallPurpose())                  // 호출 용도
                .addObject("pageName", "sndb01080")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
                .addObject("prefixUrl", CLASS_URL)                              // prefixUrl
                .addObject("sggCd", req.getSggCd())                             // 시군구 코드(SGG_CD)
                .addObject("taskSeCd", req.getTaskSeCd())                       // 업무 구분 코드(TASK_SE_CD)
                .addObject("sndngId", req.getSndngId())                         // 발송 ID(SNDNG_ID)
                .addObject("mainQuery", toJson(req))                            // 검색 조건
                .addObject("List", toJson(mav.getModel().get("List")))          // 데이터
                .addObject("Paging", toJson(mav.getModel().get("Paging")))      // 페이징
            ;
        }

        return mav;
    }

    /**사전통지 발송 현황 - 발송상세 목록을 조회하여 반환한다.
     * {@link SndngService#getSndngList(SndngQuery)} 참고
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "sndngDtlList": [발송상세 목록]
     *     "sndngDtlStart": 발송상세 목록 시작 인덱스
     *     "sndngDtlFetch": 한 번에 가져오는 발송상세 목록 수
     *     "sndngDtlTotal": 조회 결과 찾은 전체 발송상세 수
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="사전통지 발송 현황 상세 목록 조회", value=METHOD_URL.getAdvanceNoticeSendingDetailList)
    public ModelAndView getAdvanceNoticeSendingDetailList(SndbQuery req) {

        if (!"xls".equals(req.getDownload())) {
            List<?> result = sndngDtlService.getSndngDtlList(setFetchSize(req));
            return setPagingInfo(new ModelAndView("jsonView"), result, "");
        } else {
            List<CellDef> cellDefs = fromJson(req.getCellDefs(), CellDef.listType());

            XLSWriter xlsx = new XLSWriter().worksheet(0);
            Format format = new Format(xlsx);
            CellStyle center = format.cellStyle(Style.CENTER);
            CellStyle numeric = format.n_nn0();
            CellStyle dateYMD = format.yyyy_mm_dd();
            CellStyle dateDT = format.yyyy_mm_dd_hh_mm_ss();

            List<DataObject> list = sndngDtlService.getSndngDtlList(req.setFetchSize(0));

            Map<String,Object> valueMap = new HashMap<String,Object>();
            valueMap.put("발송상태", format.of("SNDNG_DTL_STTS_NM").style(center));
            valueMap.put("등기번호", format.of("RG_NO").style(center));
            valueMap.put("발송일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_YMD").style(dateYMD));
            valueMap.put("발송종료일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_END_YMD").style(dateYMD));
            valueMap.put("성명", format.of("RCPN_NM"));
            valueMap.put("우편번호", format.of("RCPN_ZIP").style(center));
            valueMap.put("주소", format.of("RCPN_ADDR"));
            valueMap.put("상세주소", format.of("RCPN_DTL_ADDR"));
            valueMap.put("배달일자", FormatMaker.yyyy_mm_dd(format, "DLVR_YMD").style(dateYMD));
            valueMap.put("배달시각", format.of("DLVR_TM").style(center));
            valueMap.put("미배달사유", format.of("UNDLVR_RSN_NM"));
            valueMap.put("수령인", format.of("ACTL_RCPN_NM"));
            valueMap.put("수령인관계", format.of("RCPN_REL_NM"));
            valueMap.put("단속일시", format.of("CRDN_DT").style(center));
            valueMap.put("차량번호", format.of("VHRNO"));
            valueMap.put("단속법정동", format.of("CRDN_STDG_NM"));
            valueMap.put("단속장소", format.of("CRDN_PLC"));
            valueMap.put("회계연도", format.of("FYR").style(center));
            valueMap.put("부과번호", format.of("LEVY_NO").style(center));
            valueMap.put("부과일자", FormatMaker.yyyy_mm_dd(format, "LEVY_YMD").style(dateYMD));
            valueMap.put("납기일자", FormatMaker.yyyy_mm_dd(format, "DUDT_YMD").style(dateYMD));
            valueMap.put("과태료금액", format.of("FFNLG_AMT").style(numeric));
            valueMap.put("본세", format.of("PCPTAX").style(numeric));
            valueMap.put("가산금", format.of("ADAMT").style(numeric));
            valueMap.put("최종금액", format.of("LAST_AMT").style(numeric));
            valueMap.put("등록일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "REG_DT").style(dateDT));
            valueMap.put("등록사용자", format.of("RGTR_NM").style(center));
            valueMap.put("수정일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "MDFCN_DT").style(dateDT));
            valueMap.put("수정사용자", format.of("MDFR_NM").style(center));

            CellDef.setValues(cellDefs, valueMap);

            xlsx.cell(0, 0).value("사전통지 발송 상세").value(center).merge(0, cellDefs.size() - 1)
                .cell(3, 0).rowValues(CellDef.header(cellDefs, () -> StyleMaker.headerStyle(xlsx)))
                .cell(4, 0).values(list, CellDef.values(cellDefs.stream().map(i -> {if (i.getValue() instanceof Format) {i.setField(null);}; return i;}).toList()));

            return new ModelAndView("downloadView")
                .addObject("download", xlsx.getDownloadable().setFilename("사전통지 발송상세" + "_목록_" + now() + ".xlsx"));
        }
    }

    /**고지서 발송 대상 자료 메인화면(sndb/sndb01/090-main)을 연다.
     * @return /sndb/sndb01/090-main
     */
    @RequestMapping(name = "고지서 발송 대상 메인", value = METHOD_URL.noticeSheetTargetMain)
    public ModelAndView noticeSheetTargetMain() {
        // View(jsp)에서 사용할 공통코드를 조회
        Map<String, List<CommonCode>> commonCodes = getCodesOf("FIM004", "FIM005", "FIM006", "FIM054", "FIM061", "FIM064");

        return addCodes(commonCodes, new ModelAndView("fims/sndb/sndb01090-main"), "FIM004", "FIM005", "FIM006", "FIM061", "FIM064")
            .addObject("pageName", "sndb01090")                                // View(jsp)에서 사용할 id 뒤에 붙일 suffix
            .addObject("infoPrefix", "nhtTrgt")                                // prefix
            .addObject("infoPrefixUrl", CLASS_URL)                            // prefixUrl
            .addObject("sggCd", currentUser().getOrgID())                        // 시군구 코드(SGG_CD)
            .addObject("taskListForSgg", stngBean.filterTaskSectionCodeForSgg(commonCodes.get("FIM054")))    // 시군구에서 사용하는 업무 목록
            .addObject("FIM004List", commonCodes.get("FIM004"))                // 주정차위반 내역 코드(VLTN_CD)
            .addObject("FIM005List", commonCodes.get("FIM005"))                // 전용차로위반 내역 코드(VLTN_CD)
            .addObject("FIM006List", commonCodes.get("FIM006"))                // 장애인주차위반 내역 코드(VLTN_CD)
            .addObject("FIM061List", commonCodes.get("FIM061"))                // 전기차 충전구역 및 충전방해 위반 코드(VLTN_CD)
            .addObject("FIM064List", commonCodes.get("FIM064"))                // 화물자동차 밤샘주차 위반 코드(VLTN_CD)
        ;
    }

    /**고지서 발송 대상 목록을 조회하여 반환한다.
     * {@link SndngService#getSndngList(SndngQuery)} 참고
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "sndngList": [사전통지 발송 대상 목록]
     *     "sndngStart": 사전통지 발송 대상 목록 시작 인덱스
     *     "sndngFetch": 한 번에 가져오는 사전통지 발송 대상 목록 수
     *     "sndngTotal": 조회 결과 찾은 전체 사전통지 발송 대상 수
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="고지서 발송 대상 목록 조회", value=METHOD_URL.getNoticeSheetTargetList)
    public ModelAndView getNoticeSheetTargetList(SndbQuery req) {
        if (!"xls".equals(req.getDownload())) {
            List<?> result = sndngService.getNhtTrgtList(setFetchSize(req));
            return setCollectionInfo(new ModelAndView("jsonView"), result, "nhtTrgt");
        } else {
            List<CellDef> cellDefs = fromJson(req.getCellDefs(), CellDef.listType());

            XLSWriter xlsx = new XLSWriter().worksheet(0);
            Format format = new Format(xlsx);
            CellStyle center = format.cellStyle(Style.CENTER);
            CellStyle numeric = format.n_nn0();
            CellStyle dateYMD = format.yyyy_mm_dd();
            CellStyle dateDT = format.yyyy_mm_dd_hh_mm_ss();

            List<DataObject> list = sndngService.getNhtTrgtList(req.setFetchSize(0));

            Map<String,Object> valueMap = new HashMap<String,Object>();
            valueMap.put("업무구분", format.of("TASK_SE_NM").style(center));
            valueMap.put("민원신청번호", format.of("CVLCPT_APLY_NO").style(center));
            valueMap.put("민원접수번호", format.of("CVLCPT_RCPT_NO").style(center));
            valueMap.put("민원접수일자", FormatMaker.yyyy_mm_dd(format, "CVLCPT_RCPT_YMD").style(dateYMD));
            valueMap.put("민원처리담당자", format.of("CVLCPT_PRCS_PIC_NM").style(center));
            valueMap.put("민원처리결과", format.of("CVLCPT_PRCS_NM").style(center));
            valueMap.put("단속일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "CRDN_YMD_TM").style(dateDT));
            valueMap.put("차량번호", format.of("VHRNO"));
            valueMap.put("위반항목", format.of("VLTN_ARTCL"));
            valueMap.put("단속법정동", format.of("CRDN_STDG_NM"));
            valueMap.put("단속장소", format.of("CRDN_PLC"));
            valueMap.put("상세단속장소", format.of("DTL_CRDN_PLC"));
            valueMap.put("과태료금액", format.of("FFNLG_AMT").style(numeric));
            valueMap.put("사전통지금액", format.of("ADVNTCE_AMT").style(numeric));
            valueMap.put("처리상태", format.of("CRDN_STTS_NM"));
            valueMap.put("납부자명", format.of("RTPYR_NM"));
            valueMap.put("납부자생일", format.of("RTPYR_BRDT_MASK").style(center));
            valueMap.put("우편번호", format.of("ZIP").style(center));
            valueMap.put("주소", format.of("ADDR"));
            valueMap.put("상세주소", format.of("DTL_ADDR"));
            valueMap.put("단속특별구역", format.of("CRDN_SPAREA_NM").style(center));
            valueMap.put("사용연료", format.of("USE_FUEL_NM").style(center));
            valueMap.put("주차가능여부", format.of("PRK_PSBLTY_RSLT_NM").style(center));
            valueMap.put("등록일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "REG_DT").style(dateDT));
            valueMap.put("등록사용자", format.of("RGTR_NM").style(center));
            valueMap.put("수정일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "MDFCN_DT").style(dateDT));
            valueMap.put("수정사용자", format.of("MDFR_NM").style(center));

            CellDef.setValues(cellDefs, valueMap);

            xlsx.cell(0, 0).value("사전통지 발송 대상").value(center).merge(0, cellDefs.size() - 1)
                .cell(3, 0).rowValues(CellDef.header(cellDefs, () -> StyleMaker.headerStyle(xlsx)))
                .cell(4, 0).values(list, CellDef.values(cellDefs.stream().map(i -> {if (i.getValue() instanceof Format) {i.setField(null);}; return i;}).toList()));

            return new ModelAndView("downloadView")
                .addObject("download", xlsx.getDownloadable().setFilename("사전통지발송대상" + "_목록_" + now() + ".xlsx"));
        }
    }

    /**고지서 발송 대상 현황 - 발송 대상 상세 화면(sndb/sndb01/100-info)을 연다.
     * @param sndngId 발송 ID
     * @return fims/sndb/sndb01100-info 또는 jsonView
     * <pre>{
     *     "sndngDtls": [고지서 발송 대상 상세 목록]
     * }</pre>
     */
    @Task
    @RequestMapping(name="고지서 발송 대상 상세 화면", value=METHOD_URL.getNoticeSheetTargetInfo)
    public ModelAndView getNoticeSheetTargetInfo(HttpServletRequest hReq, SndbQuery req) {
        // View(jsp)에서 사용할 공통코드를 조회
    	List<String> cdGrpList = TaskProcessor.get().getVltnCdGrps();
    	cdGrpList.add("EGP002");
    	cdGrpList.add("FIM047");
    	String[] cdGrps = cdGrpList.toArray(new String[cdGrpList.size()]);
        Map<String, List<CommonCode>> commonCodes = getCodesOf(cdGrps);

        return addCodes(commonCodes, new ModelAndView("fims/sndb/sndb01100-info"), cdGrps)
            .addObject("openerPageName", hReq.getParameter("openerPageName"))    // 호출한 Jsp PageName
            .addObject("callPurpose", req.getCallPurpose())                        // 호출 용도
            .addObject("pageName", "sndb01100")                                    // View(jsp)에서 사용할 id 뒤에 붙일 suffix
            .addObject("infoPrefix", "nhtTrgt")                                    // prefix
            .addObject("infoPrefixUrl", CLASS_URL)                                // prefixUrl
            .addObject("sggCd", req.getSggCd())                                    // 시군구 코드(SGG_CD)
            .addObject("taskSeCd", req.getTaskSeCd())                            // 업무 구분 코드(TASK_SE_CD)
            .addObject("FIM047List", commonCodes.get("FIM047"))                    // 발송 구분 코드(SNDNG_SE_CD)
            .addObject("mainQuery", toJson(req))                            // 검색 조건
        ;
    }

    /**고지서 발송 대상을 등록한다.
     * @param sndng 발송 정보
     * @return jsonView
     * <pre><code> {
     *     "saved": 등록되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="고지서 발송대장 등록", value=METHOD_URL.createNoticeSheetSendingList)
    public ModelAndView createNoticeSheetSendingList(SndbQuery req, Sndng sndng) {
        DataProc result = sndngService.createNhtSndngList(req, sndng);

        return new ModelAndView("jsonView")
            .addObject("saved", result.isSuccess())
	        .addObject("sndng", result.getTarget())
            .addObject("rtnMsg", result.getMessage());
    }

    /**고지서 발송 현황 메인화면(sndb/sndb01/110-main)을 연다.
     *
     * @return /sndb/sndb01/110-main
     */
    @RequestMapping(name = "고지서 발송 현황 메인", value = METHOD_URL.noticeSheetSendingMain)
    public ModelAndView noticeSheetSendingMain() {
        // View(jsp)에서 사용할 공통코드를 조회
        Map<String, List<CommonCode>> commonCodes = getCodesOf("FIM047", "FIM054", "FIM067");

        return addCodes(commonCodes, new ModelAndView("fims/sndb/sndb01110-main"), "FIM047")
            .addObject("pageName", "sndb01110")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
            .addObject("prefixUrl", CLASS_URL)                              // prefixUrl
            .addObject("sggCd", currentUser().getOrgID())                        // 시군구 코드(SGG_CD)
            .addObject("taskListForSgg", stngBean.filterTaskSectionCodeForSgg(commonCodes.get("FIM054")))   // 시군구에서 사용하는 업무 목록
            .addObject("FIM047List", commonCodes.get("FIM047"))             // 발송 구분 코드(SNDNG_SE_CD)
            .addObject("FIM067List", commonCodes.get("FIM067"))             // 발송 등록 구분 코드(SNDNG_REG_SE_CD)
        ;
    }

    /**고지서 발송 현황 목록을 조회하여 반환한다.
     * {@link SndngService#getSndngList(SndngQuery)} 참고
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "sndngList": [고지서 발송 현황 목록]
     *     "sndngStart": 고지서 발송 현황 목록 시작 인덱스
     *     "sndngFetch": 한 번에 가져오는 고지서 발송 현황 목록 수
     *     "sndngTotal": 조회 결과 찾은 전체 고지서 발송 현황 수
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="고지서 발송 현황 목록 조회", value=METHOD_URL.getNoticeSheetSendingList)
    public ModelAndView getNoticeSheetSendingList(SndbQuery req) {
        if (!"xls".equals(req.getDownload())) {
            List<?> result = sndngService.getSndngList(setFetchSize(req));
            return setPagingInfo(new ModelAndView("jsonView"), result, "");
        } else {
            List<CellDef> cellDefs = fromJson(req.getCellDefs(), CellDef.listType());

            XLSWriter xlsx = new XLSWriter().worksheet(0);
            Format format = new Format(xlsx);
            CellStyle center = format.cellStyle(Style.CENTER);
            CellStyle numeric = format.n_nn0();
            CellStyle dateYMD = format.yyyy_mm_dd();
            CellStyle dateDT = format.yyyy_mm_dd_hh_mm_ss();

            List<DataObject> list = sndngService.getSndngList(req.setFetchSize(0));

            Map<String,Object> valueMap = new HashMap<String,Object>();
            valueMap.put("업무구분", format.of("TASK_SE_NM").style(center));
            valueMap.put("등록구분", format.of("SNDNG_REG_SE_NM").style(center));
            valueMap.put("위반항목", format.of("VLTN_ARTCL"));
            valueMap.put("발송일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_YMD").style(dateYMD));
            valueMap.put("납기일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_END_YMD").style(dateYMD));
            valueMap.put("총건수", format.of("TNOCS").style(numeric));
            valueMap.put("총금액", format.of("GRAMT").style(numeric));
            valueMap.put("문서번호", format.of("DOC_NO"));
            valueMap.put("제목", format.of("TTL_NM"));
            valueMap.put("등록일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "REG_DT").style(dateDT));
            valueMap.put("등록사용자", format.of("RGTR_NM").style(center));
            valueMap.put("수정일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "MDFCN_DT").style(dateDT));
            valueMap.put("수정사용자", format.of("MDFR_NM").style(center));

            CellDef.setValues(cellDefs, valueMap);

            xlsx.cell(0, 0).value("고지서 발송 현황").value(center).merge(0, cellDefs.size() - 1)
                .cell(3, 0).rowValues(CellDef.header(cellDefs, () -> StyleMaker.headerStyle(xlsx)))
                .cell(4, 0).values(list, CellDef.values(cellDefs.stream().map(i -> {if (i.getValue() instanceof Format) {i.setField(null);}; return i;}).toList()));

            return new ModelAndView("downloadView")
                .addObject("download", xlsx.getDownloadable().setFilename("고지서발송현황" + "_목록_" + now() + ".xlsx"));
        }
    }

    /****지정한 고지서 발송대장을 제거한다.
     * @param sndng 발송대장
     * @return jsonView
     * <pre><code> {
     *     "saved": 저장되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="고지서 발송대장 삭제", value=METHOD_URL.removeNoticeSheetSending)
    public ModelAndView removeNoticeSheetSending(Sndng sndng) {
        String rtnMsg = "";

        // 발송(sndngId) 단건 삭제
        if (isEmpty(sndng.getSndngIds())) {
            rtnMsg = sndngService.removeSndng(sndng);
        } else {
            // 발송 IDs 만큼 삭제 반복..
            for (String sndngId : sndng.getSndngIds()) {
                Sndng deltSndng = new Sndng();
                deltSndng.setSndngId(sndngId);
                deltSndng.setDelRsn(sndng.getDelRsn());

                // 발송 삭제 호출
                rtnMsg = sndngService.removeSndng(deltSndng);
                if (!rtnMsg.contains("[S]"))                                    // 오류가 발생하였으면 종료..
                    break;
            }
        }

        return new ModelAndView("jsonView")
            .addObject("saved", rtnMsg.contains("[S]"))
            .addObject("rtnMsg", rtnMsg);
    }

    /**고지서 발송 현황 - 발송상세 화면(sndb/sndb01/120-info)을 연다.
     * @param sndngId 발송 ID
     * @return fims/sndb/sndb01120-info 또는 jsonView
     * <pre>{
     *     "sndngDtls": [고지서 발송 상세 목록]
     * }</pre>
     */
    @Task
    @RequestMapping(name="고지서 발송 현황 상세 화면", value=METHOD_URL.getNoticeSheetSendingDetailInfo)
    public ModelAndView getNoticeSheetSendingDetailInfo(HttpServletRequest hReq, SndbQuery req) {
        boolean json = jsonResponse();
        ModelAndView mav = getNoticeSheetSendingDetailList(req);
        if (json) {
            mav.setViewName("jsonView");
        } else {
            mav.setViewName("fims/sndb/sndb01120-info");

            mav.addObject("callPurpose", req.getCallPurpose())                  // 호출 용도
                .addObject("pageName", "sndb01120")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
                .addObject("prefixUrl", CLASS_URL)                              // prefixUrl
                .addObject("sggCd", req.getSggCd())                             // 시군구 코드(SGG_CD)
                .addObject("taskSeCd", req.getTaskSeCd())                       // 업무 구분 코드(TASK_SE_CD)
                .addObject("sndngId", req.getSndngId())                         // 발송 ID(SNDNG_ID)
                .addObject("mainQuery", toJson(req))                            // 검색 조건
                .addObject("List", toJson(mav.getModel().get("List")))          // 데이터
                .addObject("Paging", toJson(mav.getModel().get("Paging")))      // 페이징
            ;
        }

        return mav;
    }

    /**고지서 발송 현황 - 발송상세 목록을 조회하여 반환한다.
     * {@link SndngService#getSndngList(SndngQuery)} 참고
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "sndngDtlList": [발송상세 목록]
     *     "sndngDtlStart": 발송상세 목록 시작 인덱스
     *     "sndngDtlFetch": 한 번에 가져오는 발송상세 목록 수
     *     "sndngDtlTotal": 조회 결과 찾은 전체 발송상세 수
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="고지서 발송 현황 상세 목록 조회", value=METHOD_URL.getNoticeSheetSendingDetailList)
    public ModelAndView getNoticeSheetSendingDetailList(SndbQuery req) {
        if (!"xls".equals(req.getDownload())) {
            List<?> result = sndngDtlService.getSndngDtlList(setFetchSize(req));
            return setPagingInfo(new ModelAndView("jsonView"), result, "");
        } else {
            List<CellDef> cellDefs = fromJson(req.getCellDefs(), CellDef.listType());

            XLSWriter xlsx = new XLSWriter().worksheet(0);
            Format format = new Format(xlsx);
            CellStyle center = format.cellStyle(Style.CENTER);
            CellStyle numeric = format.n_nn0();
            CellStyle dateYMD = format.yyyy_mm_dd();
            CellStyle dateDT = format.yyyy_mm_dd_hh_mm_ss();

            List<DataObject> list = sndngDtlService.getSndngDtlList(req.setFetchSize(0));

            Map<String,Object> valueMap = new HashMap<String,Object>();
            valueMap.put("발송상태", format.of("SNDNG_DTL_STTS_NM").style(center));
            valueMap.put("등기번호", format.of("RG_NO").style(center));
            valueMap.put("발송일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_YMD").style(dateYMD));
            valueMap.put("발송종료일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_END_YMD").style(dateYMD));
            valueMap.put("성명", format.of("RCPN_NM"));
            valueMap.put("우편번호", format.of("RCPN_ZIP").style(center));
            valueMap.put("주소", format.of("RCPN_ADDR"));
            valueMap.put("상세주소", format.of("RCPN_DTL_ADDR"));
            valueMap.put("배달일자", FormatMaker.yyyy_mm_dd(format, "DLVR_YMD").style(dateYMD));
            valueMap.put("배달시각", format.of("DLVR_TM").style(center));
            valueMap.put("미배달사유", format.of("UNDLVR_RSN_NM"));
            valueMap.put("수령인", format.of("ACTL_RCPN_NM"));
            valueMap.put("수령인관계", format.of("RCPN_REL_NM"));
            valueMap.put("단속일시", format.of("CRDN_DT").style(center));
            valueMap.put("차량번호", format.of("VHRNO"));
            valueMap.put("단속법정동", format.of("CRDN_STDG_NM"));
            valueMap.put("단속장소", format.of("CRDN_PLC"));
            valueMap.put("회계연도", format.of("FYR").style(center));
            valueMap.put("부과번호", format.of("LEVY_NO").style(center));
            valueMap.put("부과일자", FormatMaker.yyyy_mm_dd(format, "LEVY_YMD").style(dateYMD));
            valueMap.put("납기일자", FormatMaker.yyyy_mm_dd(format, "DUDT_YMD").style(dateYMD));
            valueMap.put("과태료금액", format.of("FFNLG_AMT").style(numeric));
            valueMap.put("본세", format.of("PCPTAX").style(numeric));
            valueMap.put("가산금", format.of("ADAMT").style(numeric));
            valueMap.put("최종금액", format.of("LAST_AMT").style(numeric));
            valueMap.put("등록일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "REG_DT").style(dateDT));
            valueMap.put("등록사용자", format.of("RGTR_NM").style(center));
            valueMap.put("수정일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "MDFCN_DT").style(dateDT));
            valueMap.put("수정사용자", format.of("MDFR_NM").style(center));

            CellDef.setValues(cellDefs, valueMap);

            xlsx.cell(0, 0).value("고지서 발송 상세").value(center).merge(0, cellDefs.size() - 1)
                .cell(3, 0).rowValues(CellDef.header(cellDefs, () -> StyleMaker.headerStyle(xlsx)))
                .cell(4, 0).values(list, CellDef.values(cellDefs.stream().map(i -> {if (i.getValue() instanceof Format) {i.setField(null);}; return i;}).toList()));

            return new ModelAndView("downloadView")
                .addObject("download", xlsx.getDownloadable().setFilename("고지서발송상세" + "_목록_" + now() + ".xlsx"));
        }
    }

    /**우편통합 발송 대상을 등록한다.
     * @param sndng 발송 정보
     * @return jsonView
     * <pre><code> {
     *     "saved": 등록되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="우편통합 발송 연계 등록", value=METHOD_URL.createSendingLink)
    public ModelAndView createSendingLink(Sndng sndng) {
        String rtnMsg = sndngService.createSndngLink(sndng);

        return new ModelAndView("jsonView")
            .addObject("saved", rtnMsg.contains("[S]"))
            .addObject("rtnMsg", rtnMsg);
    }

    /**수기 반송 등록 정보(sndb/sndb01/210-info)을 연다.
     *
     * @return /sndb/sndb01/210-info
     */
    @Task
    @RequestMapping(name = "수기 반송 등록 정보 조회", value = METHOD_URL.sendingDetailInfo)
    public ModelAndView sendingDetailInfo(HttpServletRequest hReq, SndbQuery req) {
        // View(jsp)에서 사용할 공통코드를 조회
        Map<String, List<CommonCode>> commonCodes = getCodesOf("FIM047", "FIM054", "FIM081");

        return new ModelAndView("fims/sndb/sndb01210-info")
            .addObject("callPurpose", req.getCallPurpose())                 // 호출 용도
            .addObject("pageName", "sndb01210")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
            .addObject("infoPrefixUrl", CLASS_URL)                              // prefixUrl
            .addObject("sggCd", req.getSggCd())                             // 시군구 코드(SGG_CD)
            .addObject("taskSeCd", req.getTaskSeCd())                       // 업무 구분 코드(TASK_SE_CD)
            .addObject("FIM047List", commonCodes.get("FIM047"))             // 발송 구분 코드(SNDNG_SE_CD)
            .addObject("FIM081List", commonCodes.get("FIM081"))             // 미배달 사유 코드(UNDLVR_RSN_CD)
        ;
    }

    /**발송 상세 대장 목록을 조회하여 반환한다
     * {@link SndngService#getSndngList(SndngQuery)} 참고
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "sndngDtlList": [발송상세 목록]
     *     "sndngDtlStart": 발송상세 목록 시작 인덱스
     *     "sndngDtlFetch": 한 번에 가져오는 발송상세 목록 수
     *     "sndngDtlTotal": 조회 결과 찾은 전체 발송상세 수
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="반송 대상 목록 조회", value=METHOD_URL.getSendingDetailList)
    public ModelAndView getSendingDetailList(SndbQuery req) {
        if (!"xls".equals(req.getDownload())) {
            List<?> result = sndngDtlService.getSndngDtlList(setFetchSize(req));
            return setPagingInfo(new ModelAndView("jsonView"), result, "");
        } else {
            List<CellDef> cellDefs = fromJson(req.getCellDefs(), CellDef.listType());

            XLSWriter xlsx = new XLSWriter().worksheet(0);
            Format format = new Format(xlsx);
            CellStyle center = format.cellStyle(Style.CENTER);
            CellStyle numeric = format.n_nn0();
            CellStyle dateYMD = format.yyyy_mm_dd();
            CellStyle dateDT = format.yyyy_mm_dd_hh_mm_ss();

            List<DataObject> list = sndngDtlService.getSndngDtlList(req.setFetchSize(0));

            Map<String,Object> valueMap = new HashMap<String,Object>();
            valueMap.put("발송상태", format.of("SNDNG_DTL_STTS_NM").style(center));
            valueMap.put("등기번호", format.of("RG_NO").style(center));
            valueMap.put("발송일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_YMD").style(dateYMD));
            valueMap.put("발송종료일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_END_YMD").style(dateYMD));
            valueMap.put("성명", format.of("RCPN_NM"));
            valueMap.put("우편번호", format.of("RCPN_ZIP").style(center));
            valueMap.put("주소", format.of("RCPN_ADDR"));
            valueMap.put("상세주소", format.of("RCPN_DTL_ADDR"));
            valueMap.put("배달일자", FormatMaker.yyyy_mm_dd(format, "DLVR_YMD").style(dateYMD));
            valueMap.put("배달시각", format.of("DLVR_TM").style(center));
            valueMap.put("미배달사유", format.of("UNDLVR_RSN_NM"));
            valueMap.put("수령인", format.of("ACTL_RCPN_NM"));
            valueMap.put("수령인관계", format.of("RCPN_REL_NM"));
            valueMap.put("단속일시", format.of("CRDN_DT").style(center));
            valueMap.put("차량번호", format.of("VHRNO"));
            valueMap.put("단속법정동", format.of("CRDN_STDG_NM"));
            valueMap.put("단속장소", format.of("CRDN_PLC"));
            valueMap.put("회계연도", format.of("FYR").style(center));
            valueMap.put("부과번호", format.of("LEVY_NO").style(center));
            valueMap.put("부과일자", FormatMaker.yyyy_mm_dd(format, "LEVY_YMD").style(dateYMD));
            valueMap.put("납기일자", FormatMaker.yyyy_mm_dd(format, "DUDT_YMD").style(dateYMD));
            valueMap.put("과태료금액", format.of("FFNLG_AMT").style(numeric));
            valueMap.put("본세", format.of("PCPTAX").style(numeric));
            valueMap.put("가산금", format.of("ADAMT").style(numeric));
            valueMap.put("최종금액", format.of("LAST_AMT").style(numeric));
            valueMap.put("등록일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "REG_DT").style(dateDT));
            valueMap.put("등록사용자", format.of("RGTR_NM").style(center));
            valueMap.put("수정일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "MDFCN_DT").style(dateDT));
            valueMap.put("수정사용자", format.of("MDFR_NM").style(center));

            CellDef.setValues(cellDefs, valueMap);

            xlsx.cell(0, 0).value("사전통지 발송 상세").value(center).merge(0, cellDefs.size() - 1)
                .cell(3, 0).rowValues(CellDef.header(cellDefs, () -> StyleMaker.headerStyle(xlsx)))
                .cell(4, 0).values(list, CellDef.values(cellDefs.stream().map(i -> {if (i.getValue() instanceof Format) {i.setField(null);}; return i;}).toList()));

            return new ModelAndView("downloadView")
                .addObject("download", xlsx.getDownloadable().setFilename("발송 상세" + "_목록_" + now() + ".xlsx"));
        }
    }

    /**발송상세 목록의 반송 정보를 등록한다.
     * @param sndngDtl 발송상세 정보
     * @return jsonView
     * <pre><code> {
     *     "saved": 수정되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="발송상세 목록의 반송 정보 등록", value=METHOD_URL.createSendBackList)
    public ModelAndView createSendBackList(SndbQuery req) {
        String rtnMsg = sndngDtlService.createSndbkList(req);

        return new ModelAndView("jsonView")
            .addObject("saved", rtnMsg.contains("[S]"))
            .addObject("rtnMsg", rtnMsg);
    }

    /**발송상세 정보의 반송 정보를 등록한다.
     * @param sndngDtl 발송상세 정보
     * @return jsonView
     * <pre><code> {
     *     "saved": 수정되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="발송상세의 반송 정보 등록", value=METHOD_URL.createSendBack)
    public ModelAndView createSendBack(SndngDtl sndngDtl) {
        String rtnMsg = sndngDtlService.createSndbk(sndngDtl);

        return new ModelAndView("jsonView")
            .addObject("saved", rtnMsg.contains("[S]"))
            .addObject("rtnMsg", rtnMsg);
    }

    /**발송상세 정보의 반송 정보를 수정한다.
     * @param sndngDtl 발송상세 정보
     * @return jsonView
     * <pre><code> {
     *     "saved": 수정되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="발송상세의 반송 정보 수정", value=METHOD_URL.updateSendBack)
    public ModelAndView updateSendBack(SndngDtl sndngDtl) {
        String rtnMsg = sndngDtlService.updateSndbk(sndngDtl);

        return new ModelAndView("jsonView")
            .addObject("saved", rtnMsg.contains("[S]"))
            .addObject("rtnMsg", rtnMsg);
    }

    /**발송상세 정보의 반송 정보를 삭제한다.
     * @param sndngDtl 발송상세 정보
     * @return jsonView
     * <pre><code> {
     *     "saved": 수정되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="발송 상세의 반송 정보 삭제", value=METHOD_URL.removeSendBack)
    public ModelAndView removeSendBack(SndngDtl sndngDtl) {
        String rtnMsg = "";

        // 발송상세의 반송 정보 삭제
        if (isEmpty(sndngDtl.getSndngDtlIds())) {
            rtnMsg = sndngDtlService.removeSndbk(sndngDtl);
        } else {
            // 발송상세 IDs 만큼 반송 삭제 반복..
            for (String sndngDtlId : sndngDtl.getSndngDtlIds()) {
                SndngDtl deltSndngDtl = new SndngDtl();
                deltSndngDtl.setSndngDtlId(sndngDtlId);

                // 반송 삭제 호출
                rtnMsg = sndngDtlService.removeSndbk(deltSndngDtl);
                if (!rtnMsg.contains("[S]"))                                    // 오류가 발생하였으면 종료..
                    break;
            }
        }

        return new ModelAndView("jsonView")
            .addObject("saved", rtnMsg.contains("[S]"))
            .addObject("rtnMsg", rtnMsg);
    }

    /**반송 현황 메인화면(sndb/sndb01/220-main)을 연다.
     *
     * @return /sndb/sndb01/220-main
     */
    @RequestMapping(name = "반송 현황 메인", value = METHOD_URL.sendBackMain)
    public ModelAndView sendBackMain() {
        // View(jsp)에서 사용할 공통코드를 조회
        Map<String, List<CommonCode>> commonCodes = getCodesOf("FIM047", "FIM054", "FIM081");

        return new ModelAndView("fims/sndb/sndb01220-main")
            .addObject("pageName", "sndb01220")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
            .addObject("prefixUrl", CLASS_URL)                              // prefixUrl
            .addObject("sggCd", currentUser().getOrgID())                   // 시군구 코드(SGG_CD)
            .addObject("taskListForSgg", stngBean.filterTaskSectionCodeForSgg(commonCodes.get("FIM054")))   // 시군구에서 사용하는 업무 목록
            .addObject("FIM047List", commonCodes.get("FIM047"))             // 발송 구분 코드(SNDNG_SE_CD)
            .addObject("FIM081List", commonCodes.get("FIM081"))             // 미배달 사유 코드(UNDLVR_RSN_CD)
        ;
    }

    /**반송 현황 목록을 조회하여 반환한다.
     * {@link SndngService#getSndngList(SndngQuery)} 참고
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "sndngList": [반송 현황 목록]
     *     "sndngStart": 반송 현황 목록 시작 인덱스
     *     "sndngFetch": 한 번에 가져오는 반송 현황 목록 수
     *     "sndngTotal": 조회 결과 찾은 전체 반송 현황 수
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="반송 목록 조회", value=METHOD_URL.getSendBackList)
    public ModelAndView getSendBackList(SndbQuery req) {
        if (!"xls".equals(req.getDownload())) {
            List<?> result = sndngDtlService.getSndbkList(setFetchSize(req));
            return setPagingInfo(new ModelAndView("jsonView"), result, "");
        } else {
            List<CellDef> cellDefs = fromJson(req.getCellDefs(), CellDef.listType());

            XLSWriter xlsx = new XLSWriter().worksheet(0);
            Format format = new Format(xlsx);
            CellStyle center = format.cellStyle(Style.CENTER);
            CellStyle numeric = format.n_nn0();
            CellStyle dateYMD = format.yyyy_mm_dd();
            CellStyle dateDT = format.yyyy_mm_dd_hh_mm_ss();

            List<DataObject> list = sndngDtlService.getSndbkList(req.setFetchSize(0));

            Map<String,Object> valueMap = new HashMap<String,Object>();
            valueMap.put("업무구분", format.of("TASK_SE_NM").style(center));
            valueMap.put("고지번호", format.of("GOJI_NO").style(center));
            valueMap.put("등기번호", format.of("RG_NO").style(center));
            valueMap.put("배달일자", FormatMaker.yyyy_mm_dd(format, "DLVR_YMD").style(dateYMD));
            valueMap.put("배달시간", format.of("DLVR_TM"));
            valueMap.put("미배달사유", format.of("UNDLVR_RSN_NM"));
            valueMap.put("수령인", format.of("ACTL_RCPN_NM"));
            valueMap.put("수령인관계", format.of("RCPN_REL_NM"));
            valueMap.put("발송구분", format.of("SNDNG_SE_NM").style(center));
            valueMap.put("발송일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_YMD").style(dateYMD));
            valueMap.put("과태료금액", format.of("FFNLG_AMT").style(numeric));
            valueMap.put("성명", format.of("RCPN_NM"));
            valueMap.put("우편번호", format.of("RCPN_ZIP").style(center));
            valueMap.put("주소", format.of("RCPN_ADDR"));
            valueMap.put("상세주소", format.of("RCPN_DTL_ADDR"));
            valueMap.put("단속일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "CRDN_YMD_TM").style(dateDT));
            valueMap.put("차량번호", format.of("VHRNO"));
            valueMap.put("납부자구분", format.of("RTPYR_SE_NM").style(center));
            valueMap.put("납부자번호", format.of("RTPYR_NO").style(center));
            valueMap.put("납부자명", format.of("RTPYR_NM"));
            valueMap.put("단속법정동", format.of("CRDN_STDG_NM").style(center));
            valueMap.put("단속장소", format.of("CRDN_PLC"));
            valueMap.put("처리상태", format.of("CRDN_STTS_NM"));
            valueMap.put("등록일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "REG_DT").style(dateDT));
            valueMap.put("등록사용자", format.of("RGTR_NM").style(center));
            valueMap.put("수정일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "MDFCN_DT").style(dateDT));
            valueMap.put("수정사용자", format.of("MDFR_NM").style(center));

            CellDef.setValues(cellDefs, valueMap);

            xlsx.cell(0, 0).value("반송현황").value(center).merge(0, cellDefs.size() - 1)
                .cell(3, 0).rowValues(CellDef.header(cellDefs, () -> StyleMaker.headerStyle(xlsx)))
                .cell(4, 0).values(list, CellDef.values(cellDefs.stream().map(i -> {if (i.getValue() instanceof Format) {i.setField(null);}; return i;}).toList()));

            return new ModelAndView("downloadView")
                .addObject("download", xlsx.getDownloadable().setFilename("반송현황" + "_목록_" + now() + ".xlsx"));
        }
    }

    /**반송등록 (sndb/sndb01/230-info)을 연다.
     * @param sndngId 발송 ID
     * @return fims/sndb/sndb01230-info 또는 jsonView
     * <pre>{
     *     "sndngDtls":
     * }</pre>
     */
    @Task
    @RequestMapping(name="발송상세의 반송(미배달) 정보 조회", value=METHOD_URL.getSendBackInfo)
    public ModelAndView getSendBackInfo(HttpServletRequest hReq, SndbQuery req) {
        DataObject info = sndngDtlService.getSndbkInfo(req);

        // 등록을 호출하였지만 등록된 미배달 사유가 있는지 확인.
        if ("create".equals(req.getCallPurpose()) && !isEmpty(info.get("UNDLVR_RSN_NM"))) {
            return new ModelAndView("jsonView")
            	.addObject("pageName", "sndb01230")
                .addObject("rtnMsg", "이미 등록된 미배달 사유가 있습니다.");
        } else {
            boolean json = jsonResponse();
            // View(jsp)에서 사용할 공통코드를 조회
            Map<String, List<CommonCode>> commonCodes = getCodesOf("FIM054", "FIM081");

            return new ModelAndView(json ? "jsonView" : "fims/sndb/sndb01230-info")
                .addObject("openerPageName", hReq.getParameter("openerPageName"))    // 호출한 Jsp PageName
                .addObject("callPurpose", req.getCallPurpose())                        // 호출 용도
                .addObject("pageName", "sndb01230")                                    // jsp pageName
                .addObject("infoPrefix", "sndbk")                                    // prefix
                .addObject("infoPrefixUrl", CLASS_URL)                                // prefixUrl
                .addObject("FIM054List", commonCodes.get("FIM054"))                    // 업무 구분 코드(TASK_SE_CD)
                .addObject("FIM081List", commonCodes.get("FIM081"))                    // 미배달 사유 코드(UNDLVR_RSN_CD)
                .addObject("sndbkInfo", json ? info : toJson(info))                    // 반송 정보
            ;
        }
    }

    /**공시송달 현황 메인화면(sndb/sndb01/240-main)을 연다.
     *
     * @return /sndb/sndb01/240-main
     */
    @RequestMapping(name = "공시송달 현황 메인", value = METHOD_URL.serviceByPublicNoticeMain)
    public ModelAndView serviceByPublicNoticeMain() {
        // View(jsp)에서 사용할 공통코드를 조회
        Map<String, List<CommonCode>> commonCodes = getCodesOf("FIM047", "FIM054");

        return new ModelAndView("fims/sndb/sndb01240-main")
            .addObject("pageName", "sndb01240")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
            .addObject("prefixUrl", CLASS_URL)                              // prefixUrl
            .addObject("sggCd", currentUser().getOrgID())                        // 시군구 코드(SGG_CD)
            .addObject("taskListForSgg", stngBean.filterTaskSectionCodeForSgg(commonCodes.get("FIM054")))   // 시군구에서 사용하는 업무 목록
            .addObject("FIM047List", commonCodes.get("FIM047"))             // 발송 구분 코드(SNDNG_SE_CD)
        ;
    }

    /**공시송달 현황 목록을 조회하여 반환한다.
     * {@link SndngService#getSndngList(SndngQuery)} 참고
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "sndngList": [공시송달 현황 목록]
     *     "sndngStart": 공시송달 현황 목록 시작 인덱스
     *     "sndngFetch": 한 번에 가져오는 공시송달 현황 목록 수
     *     "sndngTotal": 조회 결과 찾은 전체 공시송달 현황 수
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="공시송달 목록 조회", value=METHOD_URL.getServiceByPublicNoticeList)
    public ModelAndView getServiceByPublicNoticeList(SndbQuery req) {
        if (!"xls".equals(req.getDownload())) {
            List<?> result = svbtcService.getSvbtcList(setFetchSize(req));
            return setPagingInfo(new ModelAndView("jsonView"), result, "");
        } else {
            List<CellDef> cellDefs = fromJson(req.getCellDefs(), CellDef.listType());

            XLSWriter xlsx = new XLSWriter().worksheet(0);
            Format format = new Format(xlsx);
            CellStyle center = format.cellStyle(Style.CENTER);
            CellStyle numeric = format.n_nn0();
            CellStyle dateYMD = format.yyyy_mm_dd();
            CellStyle dateDT = format.yyyy_mm_dd_hh_mm_ss();

            List<DataObject> list = svbtcService.getSvbtcList(req.setFetchSize(0));

            Map<String,Object> valueMap = new HashMap<String,Object>();
            valueMap.put("업무구분", format.of("TASK_SE_NM").style(center));
            valueMap.put("발송구분", format.of("SNDNG_SE_NM").style(center));
            valueMap.put("공시송달일자", FormatMaker.yyyy_mm_dd(format, "SVBTC_YMD").style(dateYMD));
            valueMap.put("공시송달종료일자", FormatMaker.yyyy_mm_dd(format, "SVBTC_END_YMD").style(dateYMD));
            valueMap.put("총건수", format.of("TNOCS").style(numeric));
            valueMap.put("문서번호", format.of("DOC_NO"));
            valueMap.put("제목", format.of("TTL_NM"));
            valueMap.put("기타내용", format.of("ETC_CN"));
            valueMap.put("등록일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "REG_DT").style(dateDT));
            valueMap.put("등록사용자", format.of("RGTR_NM").style(center));
            valueMap.put("수정일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "MDFCN_DT").style(dateDT));
            valueMap.put("수정사용자", format.of("MDFR_NM").style(center));

            CellDef.setValues(cellDefs, valueMap);

            xlsx.cell(0, 0).value("공시송달현황").value(center).merge(0, cellDefs.size() - 1)
                .cell(3, 0).rowValues(CellDef.header(cellDefs, () -> StyleMaker.headerStyle(xlsx)))
                .cell(4, 0).values(list, CellDef.values(cellDefs.stream().map(i -> {if (i.getValue() instanceof Format) {i.setField(null);}; return i;}).toList()));

            return new ModelAndView("downloadView")
                    .addObject("download", xlsx.getDownloadable().setFilename("공시송달현황" + "_목록_" + now() + ".xlsx"));
        }
    }

    /**공시송달 발송 현황 - 공시송달 상세 정보 화면(sndb/sndb01/250-info)을 연다.
     * @param svbtcId 공시송달 ID
     * @return fims/sndb/sndb01250-info 또는 jsonView
     * <pre>{
     *     "sndngDtls": [발송 상세 목록]
     * }</pre>
     */
    @Task
    @RequestMapping(name="공시송달 상세 화면", value=METHOD_URL.getServiceByPublicNoticeDetailInfo)
    public ModelAndView getServiceByPublicNoticeDetailInfo(HttpServletRequest hReq, SndbQuery req) {
        ModelAndView mav = getServiceByPublicNoticeDetailList(req);

        if (jsonResponse()) {
            mav.setViewName("jsonView");
        } else {
            mav.setViewName("fims/sndb/sndb01250-info");

            mav.addObject("callPurpose", req.getCallPurpose())                  // 호출 용도
                .addObject("pageName", "sndb01250")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
                .addObject("prefixUrl", CLASS_URL)                              // prefixUrl
                .addObject("sggCd", req.getSggCd())                             // 시군구 코드(SGG_CD)
                .addObject("taskSeCd", req.getTaskSeCd())                       // 업무 구분 코드(TASK_SE_CD)
                .addObject("svbtcId", req.getSvbtcId())                         // 업무 구분 코드(TASK_SE_CD)
                .addObject("mainQuery", toJson(req))                            // 검색 조건
                .addObject("List", toJson(mav.getModel().get("List")))          // 데이터
                .addObject("Paging", toJson(mav.getModel().get("Paging")))      // 페이징
            ;
        }

        return mav;
    }

    /**공시송달 발송 현황 - 공시송달 상세 목록을 조회하여 반환한다.
     * {@link SndngService#getSndngList(SndngQuery)} 참고
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "sndngDtlList": [공시송달 상세 목록]
     *     "sndngDtlStart": 공시송달 상세 목록 시작 인덱스
     *     "sndngDtlFetch": 한 번에 가져오는 공시송달 상세 목록 수
     *     "sndngDtlTotal": 조회 결과 찾은 전체 공시송달 상세 수
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="공시송달 상세 목록 조회", value=METHOD_URL.getServiceByPublicNoticeDetailList)
    public ModelAndView getServiceByPublicNoticeDetailList(SndbQuery req) {
        if (!"xls".equals(req.getDownload())) {
            List<?> result = svbtcService.getSvbtcDtlList(setFetchSize(req));
            return setPagingInfo(new ModelAndView("jsonView"), result, "");
        } else {
            List<CellDef> cellDefs = fromJson(req.getCellDefs(), CellDef.listType());

            XLSWriter xlsx = new XLSWriter().worksheet(0);
            Format format = new Format(xlsx);
            CellStyle center = format.cellStyle(Style.CENTER);
            CellStyle numeric = format.n_nn0();
            CellStyle dateYMD = format.yyyy_mm_dd();
            CellStyle dateDT = format.yyyy_mm_dd_hh_mm_ss();

            List<DataObject> list = svbtcService.getSvbtcDtlList(req.setFetchSize(0));

            Map<String,Object> valueMap = new HashMap<String,Object>();
            valueMap.put("발송상태", format.of("SNDNG_DTL_STTS_NM").style(center));
            valueMap.put("공시송달일자", FormatMaker.yyyy_mm_dd(format, "SVBTC_YMD").style(dateYMD));
            valueMap.put("등기번호", format.of("RG_NO").style(center));
            valueMap.put("발송일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_YMD").style(dateYMD));
            valueMap.put("발송종료일자", FormatMaker.yyyy_mm_dd(format, "SNDNG_END_YMD").style(dateYMD));
            valueMap.put("성명", format.of("RCPN_NM"));
            valueMap.put("우편번호", format.of("RCPN_ZIP").style(center));
            valueMap.put("주소", format.of("RCPN_ADDR"));
            valueMap.put("상세주소", format.of("RCPN_DTL_ADDR"));
            valueMap.put("배달일자", FormatMaker.yyyy_mm_dd(format, "DLVR_YMD").style(dateYMD));
            valueMap.put("배달시각", format.of("DLVR_TM").style(center));
            valueMap.put("미배달사유", format.of("UNDLVR_RSN_NM"));
            valueMap.put("수령인", format.of("ACTL_RCPN_NM"));
            valueMap.put("수령인관계", format.of("RCPN_REL_NM"));
            valueMap.put("단속일시", format.of("CRDN_DT").style(center));
            valueMap.put("차량번호", format.of("VHRNO"));
            valueMap.put("단속법정동", format.of("CRDN_STDG_NM"));
            valueMap.put("단속장소", format.of("CRDN_PLC"));
            valueMap.put("회계연도", format.of("FYR").style(center));
            valueMap.put("부과번호", format.of("LEVY_NO").style(center));
            valueMap.put("부과일자", FormatMaker.yyyy_mm_dd(format, "LEVY_YMD").style(dateYMD));
            valueMap.put("납기일자", FormatMaker.yyyy_mm_dd(format, "DUDT_YMD").style(dateYMD));
            valueMap.put("과태료금액", format.of("FFNLG_AMT").style(numeric));
            valueMap.put("본세", format.of("PCPTAX").style(numeric));
            valueMap.put("가산금", format.of("ADAMT").style(numeric));
            valueMap.put("등록일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "REG_DT").style(dateDT));
            valueMap.put("등록사용자", format.of("RGTR_NM").style(center));
            valueMap.put("수정일시", FormatMaker.yyyy_mm_dd_hh_mm_ss(format, "MDFCN_DT").style(dateDT));
            valueMap.put("수정사용자", format.of("MDFR_NM").style(center));

            CellDef.setValues(cellDefs, valueMap);

            xlsx.cell(0, 0).value("공시송달 상세").value(center).merge(0, cellDefs.size() - 1)
                .cell(3, 0).rowValues(CellDef.header(cellDefs, () -> StyleMaker.headerStyle(xlsx)))
                .cell(4, 0).values(list, CellDef.values(cellDefs.stream().map(i -> {if (i.getValue() instanceof Format) {i.setField(null);}; return i;}).toList()));

            return new ModelAndView("downloadView")
                .addObject("download", xlsx.getDownloadable().setFilename("공시송달상세" + "_목록_" + now() + ".xlsx"));
        }
    }

    /** 공시송달 한글 파일로 작성한다.
     * @return 공시송달 hwp파일
     */
    @Task
    @RequestMapping(name="공시송달 한글 저장", value=METHOD_URL.makeSvbtcFileFromHwpFormat)
    public ModelAndView makeSvbtcFileFromHwpFormat(SndbQuery req) {
        UserInfo userInfo = currentUser();
        ogdpBean.initUserInfo(userInfo);
        Print print = new Print();
        print.setPrintRequestUserInfo(userInfo);

        FimsPrintOption printOption = new FimsPrintOption();

        req.setSggCd((String)userInfo.getInfo().get("sggCd"))
        	.setDeptCd((String)userInfo.getInfo().get("deptCd"))
        	.setOrderBy("SV.SVBTC_ID, SD.SNDNG_DTL_ID");
        List<DataObject> dataObjectList = svbtcBean.getSvbtcPrint(req);

        SvbtcList hwpFormat = new SvbtcList(print, printOption, dataObjectList);
        HashMap<String, Object> result = hwpFormat.makeFile().andDownload();
        return new ModelAndView("downloadView")
        	.addAllObjects(result);
    }

    /**지정한 ID의 공시송달 정보를 반환한다.
     * @param svbtcId 공시송달 ID
     * @return fims/sndb/sndb01260-info 또는 jsonView
     * <pre>{
     *     "svbtcInfo": 공시송달 정보
     * }</pre>
     */
    @Task
    @RequestMapping(name="공시송달 정보 상세조회", value=METHOD_URL.getServiceByPublicNoticeInfo)
    public ModelAndView getServiceByPublicNoticeInfo(HttpServletRequest hReq, SndbQuery req) {
        boolean json = jsonResponse();

        // 공시송달 정보 조회
        DataObject svbtcInfo = new DataObject();
        if (req.getSvbtcId() == null) {
            svbtcInfo.set("SGG_CD", req.getSggCd());                            // 시군구 코드
            svbtcInfo.set("TASK_SE_CD", req.getTaskSeCd());                     // 업무 구분 코드
            svbtcInfo.set("SNDNG_SE_CD", req.getSchSndngSeCd());                // 발송 구분 코드
            svbtcInfo.set("TNOCS", req.getTnocs());                             // 총건수
        } else {
            svbtcInfo = svbtcService.getSvbtcInfo(req);
        }

        // View(jsp)에서 사용할 공통코드를 조회
        Map<String, List<CommonCode>> commonCodes = getCodesOf("FIM047", "FIM054");
        return addCodes(commonCodes, new ModelAndView(json ? "jsonView" : "fims/sndb/sndb01260-info"), "FIM047", "FIM054")
            .addObject("callPurpose", req.getCallPurpose())                 // 호출 용도
            .addObject("pageName", "sndb01260")                             // View(jsp)에서 사용할 id 뒤에 붙일 suffix
            .addObject("prefixUrl", CLASS_URL)                              // prefixUrl
            .addObject("mainQuery", json ? req : toJson(req))               // mainQuery
            .addObject("Info", json ? svbtcInfo : toJson(svbtcInfo))        // 공시송달 정보
        ;
    }

    /**공시송달 대장에 공시송달 정보를 등록한다.
     * @param svbtc 공시송달 정보
     * @return jsonView
     * <pre><code> {
     *     "saved": 수정되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="공시송달 등록", value=METHOD_URL.createServiceByPublicNotice)
    public ModelAndView createServiceByPublicNotice(SndbQuery req, Svbtc svbtc) {
        String rtnMsg = svbtcService.createSvbtc(req, svbtc);

        return new ModelAndView("jsonView")
            .addObject("saved", rtnMsg.contains("[S]"))
            .addObject("rtnMsg", rtnMsg);
    }

    /**공시송달 대장에 공시송달 정보를 수정한다.
     * @param svbtc 공시송달 정보
     * @return jsonView
     * <pre><code> {
     *     "saved": 수정되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="공시송달 수정", value=METHOD_URL.updateServiceByPublicNotice)
    public ModelAndView updateServiceByPublicNotice(Svbtc svbtc) {
        String rtnMsg = svbtcService.updateSvbtc(svbtc);

        return new ModelAndView("jsonView")
            .addObject("saved", rtnMsg.contains("[S]"))
            .addObject("rtnMsg", rtnMsg);
    }

    /**지정한 공시송달 ID에 해당하는 공시송달 대장을 제거한다.
     * @param svbtcId 공시송달 ID
     * @return jsonView
     * <pre><code> {
     *     "affected": 저장된 정보수
     *     "saved": 저장되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="공시송달 삭제", value=METHOD_URL.removeServiceByPublicNotice)
    public ModelAndView removeServiceByPublicNotice(Svbtc svbtc) {
        String rtnMsg = "";

        if (isEmpty(svbtc.getSvbtcIds())) {
            rtnMsg = svbtcService.removeSvbtc(svbtc);
        } else {
            // 공시송달 Ids 만큼 삭제 반복..
            for (String svbtcId : svbtc.getSvbtcIds()) {
                Svbtc deltSvbtc = new Svbtc();
                deltSvbtc.setSvbtcId(svbtcId);
                deltSvbtc.setDelRsn(svbtc.getDelRsn());

                // 공시송달 삭제 호출
                rtnMsg = svbtcService.removeSvbtc(deltSvbtc);
                if (!rtnMsg.contains("[S]"))                                    // 오류가 발생하였으면 종료..
                    break;
            }
        }

        return new ModelAndView("jsonView")
            .addObject("saved", rtnMsg.contains("[S]"))
            .addObject("rtnMsg", rtnMsg);
    }

    /**지정한 공시송달 ID에 해당하는 공시송달 대장을 제거한다.
     * @param svbtcId 공시송달 ID
     * @return jsonView
     * <pre><code> {
     *     "affected": 저장된 정보수
     *     "saved": 저장되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="공시송달 개별 삭제", value=METHOD_URL.removeServiceByPublicNoticeEach)
    public ModelAndView removeServiceByPublicNoticeEach(Svbtc svbtc) {
        String rtnMsg = svbtcService.removeSvbtcEach(svbtc);

        return new ModelAndView("jsonView")
            .addObject("saved", rtnMsg.contains("[S]"))
            .addObject("rtnMsg", rtnMsg);
    }

    /** 사전통지서를 PDF 파일로 작성한다.
     * @return 사전통지서 PDF파일
     */
    @Task
    @RequestMapping(name="사전통지서 pdf 파일 생성", value=METHOD_URL.makeAdvanceNoticePdf)
    public ModelAndView makeAdvanceNoticePdf(SndbQuery req, FimsPrintOption printOption) {
        List<DataObject> list = sndngDtlService.getSndngTrgtDtlIds(req);
        String[] crdnIds = list.stream()
        	.map(row -> row.string("CRDN_ID"))
        	.toList()
        	.toArray(new String[list.size()]);

        List<DataObject> dataObjectList = new ArrayList<DataObject>();
        if (crdnIds.length == 1 && crdnIds[0].equals("sample")) {
            DataObject sample = new Advntce().createSampleData();
            dataObjectList.add(sample);
        } else {
            dataObjectList = integrationSearchMapper.selectIntegrationDataList(new SprtQuery().setCrdnIds(crdnIds));
            Map<String, List<DataObject>> fileList = fileBean.getFileList(
            		new FileQuery().setInfoType(Crdn.INF_TYPE).setInfoKeys(crdnIds)
            	).stream()
            	.collect(Collectors.groupingBy(row -> row.string("INF_ID")));

            dataObjectList.forEach(row -> {
            	String crdnId = row.string("CRDN_ID");
            	List<DataObject> files = fileList.get(crdnId);
            	if (isEmpty(files)) return;

            	for (int i = 0; i < files.size(); ++i) {
                    DataObject fileInfo = files.get(i);
					row.set("CRDN_PHOTO"+(i+1), fileInfo.string("URL"));
                    String fileID = fileInfo.string("FILE_ID");
					row.set("CRDN_PHOTO"+(i+1)+"KEY", fileID);

                    List<DataObject> mosSet = mediaBean.getMosaicList(fileID);
                    if (mosSet != null && !mosSet.isEmpty()) {
                        row.set("MOS"+fileID, mosSet);
                    }
            	}
            });
        }

        Print print = new Print();
        UserInfo userInfo = currentUser();
        ogdpBean.initUserInfo(userInfo);
        print.setPrintRequestUserInfo(userInfo);

        //출력물 기본 설정
        OtptStngQuery otptStngQuery = new OtptStngQuery();

        otptStngQuery.setSggCd((String)userInfo.getInfo().get("sggCd"));
        String paperSeCd = printOption.getPaperSeCd();
        String taskSeCd = printOption.getTaskSeCd();
        otptStngQuery.setTaskSeCd(taskSeCd);
        otptStngQuery.setSndngSeCd("02");
        otptStngQuery.setPaperSeCd(paperSeCd);
        DataObject otptBscStng = ifEmpty(otptStngMapper.selectOtptBscStng(otptStngQuery), DataObject::new);

        //출력 요소별 설정
        List<DataObject> otptArtclStngList = otptStngMapper.selectOtptArtclStngList(otptStngQuery);

        DataObject sggStngInfo = stngBean.getStng("sgg");
        DataObject deptStngInfo = stngBean.getStng("dept");
        //
        Map<String, String> globalVariable = printOption.getGlobalVariable();
        if (globalVariable == null) {
            globalVariable = new HashMap<String, String>();
        }
        globalVariable.put("offcsFilePath", sggStngInfo.string("OFFCS_FILE_PATH"));
        globalVariable.put("logoFilePath", sggStngInfo.string("LOGO_FILE_PATH"));
        globalVariable.put("instAddr", sggStngInfo.string("INST_ADDR"));
        globalVariable.put("instDaddr", sggStngInfo.string("INST_DADDR"));
        globalVariable.put("instZip", sggStngInfo.string("INST_ZIP"));
        globalVariable.put("deptTelno", deptStngInfo.string("DEPT_TELNO"));
        globalVariable.put("deptFxno", deptStngInfo.string("DEPT_FXNO"));
        if (!otptBscStng.string("BCRN_IMG_PATH").equals("")) {
            globalVariable.put("bcrnImgPath", otptBscStng.string("BCRN_IMG_PATH"));
        }
        printOption.setGlobalVariable(globalVariable);

        Advntce pdfFormat = new Advntce(print,printOption,otptBscStng,otptArtclStngList,paperSeCd,dataObjectList);
        Map<String, Object> result = pdfFormat.makeFile();

        return new ModelAndView("downloadView")
        	.addAllObjects(result);
    }

    /** 사전통지 정보로 txt파일,이미지파일을 생성하고 압축파일로 반환한다.
     * @return 사전통지서 압축파일
     */
    @Task
    @RequestMapping(name="사전통지서 외주 파일 생성", value=METHOD_URL.makeAdvanceNoticeOutsourcing)
    public ModelAndView makeAdvanceNoticeOutsourcing(SndbQuery req, FimsPrintOption printOption) {
        List<DataObject> list = sndngDtlService.getSndngTrgtDtlIds(req);
        String[] crdnIds = list.stream().map(row -> row.string("CRDN_ID")).toList().toArray(new String[list.size()]);
        SprtQuery sprtQuery = new SprtQuery()
        	.setCrdnIds(crdnIds);
        List<DataObject> dataObjectList = integrationSearchMapper.selectIntegrationDataList(sprtQuery);
        for (DataObject row: dataObjectList) {
			DataObject sndngDtl = integrationSearchMapper.selectLatestSndngDtlInfo(row.string("CRDN_ID"));
            row.set("SNDNG_DTL_ID", sndngDtl.string("SNDNG_DTL_ID"));
            row.set("SNDNG_YMD", sndngDtl.string("SNDNG_YMD"));
            row.set("OPNN_RANGE", row.string("SNDNG_YMD")+"~"+row.string("DUDT_YMD"));
            row.set("RG_NO",sndngDtl.string("RG_NO"));
        }

       String taskSeCd = "";
      for (int i = 0; i < dataObjectList.size(); i++) {
            DataObject dataObject = dataObjectList.get(i);

            if (taskSeCd.isEmpty())
                taskSeCd = dataObject.string("TASK_SE_CD");

            FileQuery fileQuery = new FileQuery()
            	.setInfoType(Crdn.INF_TYPE)
            	.setInfoKeys(dataObject.string("CRDN_ID"));
            List<DataObject> fileInfoList = fileBean.getFileList(fileQuery);
            int crdnPhotoCnt = fileInfoList.size();

            for (int j=0; j < fileInfoList.size(); j++) {
                DataObject fileInfo = fileInfoList.get(j);

                dataObject
                	.set("CRDN_PHOTO"+(j+1), fileInfo.string("FILE_PATH"))
                	.set("CRDN_PHOTO"+(j+1)+"KEY", fileInfo.string("FILE_ID"));
                List<DataObject> mosSet = mediaBean.getMosaicList(fileInfo.string("FILE_ID"));
                if (mosSet != null) {
                    for (int k=0; k < mosSet.size(); k++) {
                        dataObject.set("MOS"+fileInfo.string("FILE_ID"), mosSet);
                    }
                }
            }
            dataObject.set("CRDN_PHOTO_CNT", crdnPhotoCnt);

        }

        OutsourcingStngQuery stngQuery = new OutsourcingStngQuery()
        	.setSndngSeCd("02")
        	.setSggCd(currentUser().getOrgID())
        	.setTaskSeCd(taskSeCd);
        List<DataObject> printStngItems = outsourcingStngBean.getArtclStngList(stngQuery);

        Map<String, Object> result = new OutsourcingFileMaker("사전통지", printOption, printStngItems, dataObjectList).makeFile();
        return new ModelAndView("downloadView")
        	.addAllObjects(result);
    }

    /** 고지서를 PDF 파일로 작성한다.
     * @return 고지서 PDF파일
     */
    @Task
    @RequestMapping(name="고지서 pdf 파일 생성", value=METHOD_URL.makeNoticeSheetPdf)
    public ModelAndView makeNoticeSheetPdf(SndbQuery req, FimsPrintOption printOption) {
        // 고지서 출력 대상 자료 조회
        List<DataObject> list = sndngDtlService.getSndngTrgtDtlIds(req);
        String[] crdnIds = list.stream().map(row -> row.string("CRDN_ID")).toList().toArray(new String[list.size()]);

        // 단속 대장 조회
        List<DataObject> crdnList = integrationSearchMapper.selectIntegrationDataList(new SprtQuery().setCrdnIds(crdnIds));
        Map<String, List<DataObject>> crdnFiles = fileBean.getFileList(new FileQuery().setInfoType(Crdn.INF_TYPE).setInfoKeys(crdnIds)).stream()
        	.collect(Collectors.groupingBy(row -> row.string("INF_KEY")));

        for (DataObject dataObject: crdnList) {
            List<DataObject> fileList = crdnFiles.get(dataObject.string("CRDN_ID"));
            if (isEmpty(fileList)) continue;

            for (int i = 0; i < fileList.size(); i++) {
                DataObject fileInfo = fileList.get(i);
				dataObject.set("CRDN_PHOTO"+(i+1), fileInfo.string("URL"));
                String fileID = fileInfo.string("FILE_ID");
				dataObject.set("CRDN_PHOTO"+(i+1)+"KEY", fileID);

                List<DataObject> mosSet = mediaBean.getMosaicList(fileID);
                if (!isEmpty(mosSet))
                    dataObject.set("MOS"+fileID, mosSet);
            }
        }

        UserInfo userInfo = currentUser();
        ogdpBean.initUserInfo(userInfo);
        Print print = new Print();
        print.setPrintRequestUserInfo(userInfo);

        //출력물 기본 설정
        String paperSeCd = printOption.getPaperSeCd();
        String taskSeCd = printOption.getTaskSeCd();
        OtptStngQuery otptStngQuery = new OtptStngQuery()
           	.setSndngSeCd("03")
           	.setPaperSeCd(paperSeCd)
        	.setSggCd((String)userInfo.getInfo().get("sggCd"))
        	.setTaskSeCd(taskSeCd);
        DataObject otptBscStng = ifEmpty(otptStngMapper.selectOtptBscStng(otptStngQuery), DataObject::new);
        //출력 요소별 설정
        List<DataObject> otptArtclStngList = otptStngMapper.selectOtptArtclStngList(otptStngQuery);

        DataObject sggStngInfo = stngBean.getStng("sgg");
        DataObject deptStngInfo = stngBean.getStng("dept");
        //
        Map<String, String> globalVariable = ifEmpty(printOption.getGlobalVariable(), HashMap::new);
        globalVariable.put("offcsFilePath", sggStngInfo.string("OFFCS_FILE_PATH"));
        globalVariable.put("logoFilePath", sggStngInfo.string("LOGO_FILE_PATH"));
        globalVariable.put("instAddr", sggStngInfo.string("INST_ADDR"));
        globalVariable.put("instDaddr", sggStngInfo.string("INST_DADDR"));
        globalVariable.put("instZip", sggStngInfo.string("INST_ZIP"));
        globalVariable.put("deptTelno", deptStngInfo.string("DEPT_TELNO"));
        globalVariable.put("deptFxno", deptStngInfo.string("DEPT_FXNO"));
        if (!isEmpty(otptBscStng.string("BCRN_IMG_PATH"))) {
            globalVariable.put("bcrnImgPath", otptBscStng.string("BCRN_IMG_PATH"));
        }
        printOption.setGlobalVariable(globalVariable);
        Map<String, Object> result = new Nht(print,printOption,otptBscStng,otptArtclStngList,paperSeCd,crdnList).makeFile();

        return new ModelAndView("downloadView")
        	.addAllObjects(result);
    }

    /** 고지서 정보로 txt파일,이미지파일을 생성하고 압축파일로 반환한다.
     * @return 고지서 압축파일
     */
    @Task
    @RequestMapping(name="고지서 외주 파일 생성", value=METHOD_URL.makeNoticeSheetOutsourcing)
    public ModelAndView makeNoticeSheetOutsourcing(SndbQuery req, FimsPrintOption printOption) {
        // 고지서 출력 대상 자료 조회
        List<DataObject> list = sndngDtlService.getSndngTrgtDtlIds(req);
        String[] crdnIds = list.stream().map(row -> row.string("CRDN_ID")).toList().toArray(new String[list.size()]);
        UserInfo userInfo = currentUser();
        ogdpBean.initUserInfo(userInfo);

        String taskSeCd = req.getTaskSeCd();
        // 단속 대장 조회
        SprtQuery sprtQuery = new SprtQuery()
        	.setCrdnIds(crdnIds);
        List<DataObject> dataObjectList = integrationSearchMapper.selectIntegrationDataList(sprtQuery);

        for (DataObject dataObject: dataObjectList) {
            if (taskSeCd.isEmpty()) {
                taskSeCd = dataObject.string("TASK_SE_CD");
            }

            FileQuery fileQuery = new FileQuery()
            	.setInfoType(Crdn.INF_TYPE)
            	.setInfoKeys(dataObject.string("CRDN_ID"));
            List<DataObject> fileInfoList = fileBean.getFileList(fileQuery);
            int crdnPhotoCnt = fileInfoList.size();

            for (int j = 0; j < crdnPhotoCnt; j++) {
                DataObject fileInfo = fileInfoList.get(j);
                dataObject.set("CRDN_PHOTO"+(j + 1), fileInfo.string("FILE_PATH"));
                String fileID = fileInfo.string("FILE_ID");
				dataObject.set("CRDN_PHOTO"+(j + 1)+"KEY", fileID);

                List<DataObject> mosSet = mediaBean.getMosaicList(fileID);
                if (mosSet != null) {
                    for (int k=0; k < mosSet.size(); k++) {
                        dataObject.set("MOS"+fileID, mosSet);
                    }
                }
            }
            dataObject.set("CRDN_PHOTO_CNT", crdnPhotoCnt);
        }

        OutsourcingStngQuery stngQuery = new OutsourcingStngQuery()
           	.setSndngSeCd("03")
        	.setSggCd(currentUser().getOrgID())
        	.setTaskSeCd(taskSeCd);
        List<DataObject> printStngItems = outsourcingStngBean.getArtclStngList(stngQuery);
        Map<String, Object> result = new OutsourcingFileMaker("고지서", printOption, printStngItems, dataObjectList).makeFile();

        return new ModelAndView("downloadView").addAllObjects(result);
    }

    /**과태료대장 단속정보 등록를 등록한다.
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "saved": 등록되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task({"ECA","DPV","PES","DVS"})
    @RequestMapping(name="세외수입 과태료대장 단속정보 등록", value=METHOD_URL.setNonTaxReceiptIndividualA01List)
    public ModelAndView setNonTaxReceiptIndividualA01List(SndbQuery req, Sndng sndng) {
    	if ("lntris".equals(TaskProcessor.get().getNtrisInterface(req.getTaskSeCd()))) { // 지방 세외수입
            boolean saved = false;
            String rtnMsg = "";

            SndngDtl sndngDtl = new SndngDtl();
            sndngDtl.setTaskSeCd(req.getTaskSeCd());
            // 발송상세 Ids 만큼 반복..
            for (DataObject dtl: sndngDtlService.getSndngTrgtDtlIds(req)) {
    			sndngDtl.setSndngDtlId(dtl.string("SNDNG_DTL_ID"));
                sndngDtl.setCrdnId(dtl.string("CRDN_ID"));
                sndngDtl.setLinkTrgtCd(dtl.string("LINK_TRGT_CD"));
                sndngDtl.setLinkMngKey(dtl.string("LINK_MNG_KEY"));
                sndngDtl.setSndngDtlSttsCd(dtl.string("SNDNG_DTL_STTS_CD"));
                sndngDtl.setDelYn("N");
                sndngDtl.setSndngYmd(sndng.getSndngYmd());

                rtnMsg = sndngDtlService.setNxrpIndivA01List(sndngDtl);
                saved = rtnMsg.contains("[S]");
            }

            return new ModelAndView("jsonView")
                .addObject("saved", saved)
                .addObject("rtnMsg", rtnMsg);
    	} else { // 서울 세외수입
    		if (!"xls".equals(req.getDownload())) {
        		String resp = sndngDtlService.registerPreNotice(req);
                return new ModelAndView("jsonView")
                    .addObject("saved", true)
                    .addObject("rtnMsg", resp);
    		} else {
    			return new ModelAndView("downloadView")
    				.addObject("download", sndngDtlService.downloadPreNotices(req));
    		}
    	}
    }

    /**과태료대장 상세조회(실시간)한다.
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "saved": 등록되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task({"ECA","DPV","PES","DVS"})
    @RequestMapping(name="세외수입 과태료대장 상세조회", value=METHOD_URL.getNonTaxReceiptIndividualA03List)
    public ModelAndView getNonTaxReceiptIndividualA03List(SndbQuery req) {
        boolean saved = false;
        List<DataObject> dtlIds = sndngDtlService.getSndngTrgtDtlIds(req);
        String rtnMsg = dtlIds.isEmpty() ? "[F] 자료를 찾지못했습니다." : "";

        boolean lntris = "lntris".equals(TaskProcessor.get().getNtrisInterface(req.getTaskSeCd()));
        for (DataObject dtl: dtlIds) {
            SndngDtl sndngDtl = new SndngDtl();
			sndngDtl.setSndngDtlId(dtl.string("SNDNG_DTL_ID"));
            sndngDtl.setCrdnId(dtl.string("CRDN_ID"));
            sndngDtl.setLinkTrgtCd(dtl.string("LINK_TRGT_CD"));
            sndngDtl.setLinkMngKey(dtl.string("LINK_MNG_KEY"));
            sndngDtl.setSndngDtlSttsCd(dtl.string("SNDNG_DTL_STTS_CD"));
            sndngDtl.setDelYn("N");

            rtnMsg = lntris ? sndngDtlService.getNxrpIndivA03List(sndngDtl) : sndngDtlService.updatePreNotice(sndngDtl);
            saved = rtnMsg.contains("[S]");
        }

        return new ModelAndView("jsonView")
			.addObject("saved", saved)
			.addObject("rtnMsg", rtnMsg);
    }

    /**과태료대장 상세조회(실시간)한다.
     * @param req 발송대장 조회 조건
     * @return jsonView
     * <pre><code> {
     *     "saved": 등록되었으면 true, 그렇지 않으면 false
     * }</code></pre>
     */
    @Task
    @RequestMapping(name="세외수입 체납상세 세목정보 조회", value=METHOD_URL.getNonTaxReceiptSpecialB01List)
    public ModelAndView getNonTaxReceiptSpecialB01List(SndbQuery req) {
        boolean saved = false;
        
        LevyQuery levyQuery = new LevyQuery().setSchLevyYmdFrom(req.getSchLevyYmdFrom())
				  .setSchLevyYmdTo(req.getSchLevyYmdTo())
				  .setSchFrstDudtYmdFrom(req.getSchFrstDudtYmdFrom())
				  .setSchFrstDudtYmdTo(req.getSchFrstDudtYmdTo())
				  .setSchFyr(req.getSchFyr());
        
    	List<Levy> levys = levyService.getLevyForGoji(levyQuery);
        
        ModelAndView mav = new ModelAndView("jsonView");
    	String rtnMsg = "";
        
    	if (levys.isEmpty()) {
    		rtnMsg = "[F] 자료를 찾지못했습니다.";
    		return mav.addObject("saved", saved)
    			.addObject("rtnMsg", rtnMsg);
    	}
        
        for (Levy levy: levys) {
            rtnMsg = sndngDtlService.updatePreNotice((String) levy.getNxrpAcbKey());
            saved = rtnMsg.contains("[S]");
        }
                
        if (saved) {
        	levys = levyService.getLevyForGoji(levyQuery);
        	mav.addObject("saved", saved)
        	   .addObject("tnocs", levys.size())
               .addObject("gramt", levys.stream().mapToInt(Levy::getLastAmt).sum())
            ;
        }        
        
        return mav;
        
       /*
    	List<SndngDtl> sndngDtls = sndngDtlService.getSndngDetails(req.setSndngDtlSttsCds(List.of(
    		"01", "02", "03","04", "05", "06", "07"
    	)));
    	
        String rtnMsg = sndngDtls.isEmpty() ? "[F] 자료를 찾지못했습니다." : "";
        boolean lntris = "lntris".equals(TaskProcessor.get().getNtrisInterface(req.getTaskSeCd()));

        for (SndngDtl sndngDtl: sndngDtls) {
            rtnMsg = lntris ? sndngDtlService.getNxrpIndivA03List(sndngDtl) : sndngDtlService.updatePreNotice(sndngDtl);
            saved = rtnMsg.contains("[S]");
        }

        ModelAndView mav = new ModelAndView("jsonView")
			.addObject("saved", saved)
			.addObject("rtnMsg", rtnMsg);
        if (saved) {
        	sndngDtls = sndngDtlService.getSndngDetails(req);
            mav.addObject("tnocs", sndngDtls.size())
               .addObject("gramt", sndngDtls.stream().mapToInt(SndngDtl::getLastAmt).sum());
        }

   	
/*
        boolean saved = false;
        String rtnMsg = "";

        List<DataObject> dtlList = sndngDtlService.getSndngTrgtDtlIds(req);

        // 발송상세 Ids 만큼 반복..
        for (int iLoop = 0; iLoop < dtlList.size(); iLoop++) {
            SndngDtl sndngDtl = new SndngDtl();

            sndngDtl.setSndngDtlId(dtlList.get(iLoop).string("SNDNG_DTL_ID"));
            sndngDtl.setCrdnId(dtlList.get(iLoop).string("CRDN_ID"));
            sndngDtl.setLinkTrgtCd(dtlList.get(iLoop).string("LINK_TRGT_CD"));
            sndngDtl.setLinkMngKey(dtlList.get(iLoop).string("LINK_MNG_KEY"));
            sndngDtl.setSndngDtlSttsCd(dtlList.get(iLoop).string("SNDNG_DTL_STTS_CD"));
            sndngDtl.setDelYn("N");

            rtnMsg = sndngDtlService.getNxrpIndivA03List(sndngDtl);
            if (rtnMsg.contains("[S]")) {
                saved = true;
            } else {
                saved = false;
            }
        }
        saved = true;
        rtnMsg = "[S]";

        return new ModelAndView("jsonView")
			.addObject("saved", saved)
			.addObject("rtnMsg", rtnMsg);
*/
    }
}