package cokr.xit.fims.sndb.service.bean;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.function.Consumer;

import javax.annotation.Resource;

import org.springframework.stereotype.Component;

import cokr.xit.base.file.FileInfo;
import cokr.xit.base.file.service.bean.FileBean;
import cokr.xit.base.user.service.bean.UserBean;
import cokr.xit.fims.cmmn.CmmnUtil;
import cokr.xit.fims.cmmn.CrdnSttsHstry;
import cokr.xit.fims.cmmn.service.bean.CrdnSttsHstryBean;
import cokr.xit.fims.crdn.Crdn;
import cokr.xit.fims.crdn.dao.CrdnUpdtMapper;
import cokr.xit.fims.epst.EpostRcptDtl;
import cokr.xit.fims.epst.EpostRcptReg;
import cokr.xit.fims.epst.service.bean.EpostRcptDtlBean;
import cokr.xit.fims.epst.service.bean.EpostRcptRegBean;
import cokr.xit.fims.levy.Levy;
import cokr.xit.fims.levy.service.bean.LevyBean;
import cokr.xit.fims.mngt.VltnQuery;
import cokr.xit.fims.mngt.dao.VltnMapper;
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.dao.SndngDtlMapper;
import cokr.xit.fims.sndb.dao.SndngMapper;
import cokr.xit.foundation.component.AbstractBean;
import cokr.xit.foundation.data.DataObject;

/**발송 대장 정보 관리 Bean
 *
 * <p>상세 설명:
 *
 * <pre>
 * ============ 변경 이력 ============
 * 2023-08-25    JoJH 최초 작성
 * ================================
 * </pre>
 */
@Component("sndngBean")
public class SndngBean extends AbstractBean {
    /** 발송 대장 정보 DAO */
    @Resource(name = "sndngMapper")
    private SndngMapper sndngMapper;
    /** 발송 상세 정보 DAO */
    @Resource(name = "sndngDtlMapper")
    private SndngDtlMapper sndngDtlMapper;
    /** 사용자 정보 DAO */
    @Resource(name="userBean")
    protected UserBean userBean;
    /** 위반 정보 정보 DAO */
    @Resource(name = "vltnMapper")
    private VltnMapper vltnMapper;
    /** 단속 정보 DAO */
    @Resource(name = "crdnUpdtMapper")
    private CrdnUpdtMapper crdnUpdtMapper;
    /** 단속 상태 이력 정보 Bean */
    @Resource(name = "crdnSttsHstryBean")
    private CrdnSttsHstryBean crdnSttsHstryBean;
    /** 부과 대장 정보 Bean */
    @Resource(name = "levyBean")
    private LevyBean levyBean;
    /** 전자우편 접수 등록 정보 Bean */
    @Resource(name = "epostRcptRegBean")
    private EpostRcptRegBean epostRcptRegBean;
    /** 전자우편 접수 상세 정보 Bean */
    @Resource(name = "epostRcptDtlBean")
    private EpostRcptDtlBean epostRcptDtlBean;
    /** 파일 Bean */
    @Resource(name="fileBean")
    private FileBean fileBean;

    /**지정한 조건에 따라 계고장 발송 대상 목록을 조회하여 반환한다.
     * @param req 발송 대장 조회 조건
     * @return 계고장 발송 대상 목록
     */
    public List<DataObject> getWrngTrgtList(SndbQuery req) {
    	String[] crdnIds = req.getCrdnIds();
    	String crdnId = req.getCrdnId();
    	if (!isEmpty(crdnIds) || !isEmpty(crdnId)) {
    		req = new SndbQuery().setCrdnIds(crdnIds).setCrdnId(crdnId);
    	}
        // 삭제 여부 확인
        if (req.getDelYn() == null) {
            req.setDelYn("N");
        }
        // 정렬 확인
        if (req.getOrderBy() == null) {
            if (req.getBy() == null) {
                req.setOrderBy("REG_DT");
            } else {
                req.setOrderBy(CmmnUtil.convertCamelCaseToSnakeCase(req.getBy()));
            }
        }

        return sndngMapper.selectWrngTrgtList(req);
    }

    /**지정한 조건에 따라 발송 대상 객체들을 반환한다.
     * @param req 발송 대장 조회 조건
     * @return 계고장 발송 대상 객체 목록
     */
    public List<DataObject> getWrngTrgts(SndbQuery req) {
        // 삭제 여부 확인
        if (req.getDelYn() == null) {
            req.setDelYn("N");
        }
        if (req.getOrderBy() == null) {
            req.setOrderBy("CRDN_YMD_TM");
        }

        return sndngMapper.selectWrngTrgts(req);
    }

    /**계고장 발송 상세 정보를 등록한다.
     * @param sndngDtl 발송 상세
     * @return 저장 여부
     * <ul><li>저장됐으면 true</li>
     *     <li>그렇지 않으면 false</li>
     * </ul>
     */
    public String createWrngSndngDtl(Sndng sndng, String crdnId) {
        // 단속, 납부자 대장을 조회 한다.
        DataObject crdnPayerInfo = sndngMapper.selectCrdnPayerInfo(crdnId);
        if (crdnPayerInfo == null)
            return "[F] 발송상세 등록 작업중 단속 정보가 확인되지 않았습니다."
            	 + "<p>" + "단속 ID : " + crdnId + "</p>";
        if (crdnPayerInfo.string("RTPYR_ID").equals(""))
            return "[F] 발송상세 등록 작업중 납부자 정보가 확인되지 않았습니다."
				 + "<p>"  + "단속ID : " + crdnId
				 + "<br>" + "단속일시 : " + crdnPayerInfo.string("CRDN_YMD_TM_MASK")
				 + "<br>" + "차량번호 : " + crdnPayerInfo.string("VHRNO")
				 + "</p>";

        // 발송 상세(TB_SNDNG_DTL)
        SndngDtl sndngDtl = new SndngDtl(sndng).setPayerInfo(crdnPayerInfo);
        int rtnNocs = sndngDtlMapper.insertSndngDtl(sndngDtl);
        if (rtnNocs != 1)
            throw new RuntimeException("발송상세 등록에 실패하였습니다.");

        // 단속 상태 코드를 수정한다.
        if ("83".equals(crdnPayerInfo.string("CRDN_STTS_CD"))) {
            CrdnSttsHstry crdnSttsHstry = new CrdnSttsHstry();
            crdnSttsHstry.setCrdnId(crdnPayerInfo.string("CRDN_ID"));
            crdnSttsHstry.setBfrSttsCd(crdnPayerInfo.string("CRDN_STTS_CD"));
            crdnSttsHstry.setBfrSttsChgDt(crdnPayerInfo.string("CRDN_STTS_CHG_DT"));
            crdnSttsHstry.setCrdnSttsCd("84");
            crdnSttsHstry.setTaskDtlId(sndngDtl.getSndngDtlId());

            // 단속 상태 이력(TB_CRDN_STTS_HSTRY) 대장에 등록하고, 단속(TB_CRDN) 대장을 수정한다.
            boolean rtnScs = crdnSttsHstryBean.createHstryUpdateCrdnSttsCd(crdnSttsHstry);
            if (!rtnScs)
                throw new RuntimeException("발송상세 등록 중 단속대장의 상태변경에 실패하였습니다.");
        }

        return "[S] 작업이 정상 처리 되었습니다.";
    }

    /**계고장 발송 대상을 조회하여 발송 대장에 등록한다.
     * @param req 계고장 발송 대상 조회, sndng 발송 대장
     * @return 저장 여부
     * <ul><li>저장됐으면 true</li>
     *     <li>그렇지 않으면 false</li>
     * </ul>
     */
    public String createWrngSndngList(SndbQuery req, Sndng sndng) {
        // 단속 IDs로 계고장 발송 대상 정보 조회
    	req.setOrderBy("C.REG_DT");
        List<DataObject> trgtList = sndngMapper.selectWrngTrgtCrdnIds(req);
        // 조회 건수 확인
        if (isEmpty(trgtList))
            return "[F] 작업 중 대상 자료가 존재하지 않습니다.";
        if (sndng.getTnocs() != trgtList.size())
            return "[F] 조회된 총 건수와 대상 건수가 다릅니다.";

        // 위반 ID 조회
        String vltnId = "";
        if (req.getVltnCd() != null) {
            VltnQuery vltnReq = new VltnQuery()
               	.setVltnCd(req.getVltnCd())
            	.setSggCd(sndng.getSggCd())
            	.setTaskSeCd(sndng.getTaskSeCd());

            DataObject vltnInfo = vltnMapper.selectVltnInfo(vltnReq);
            vltnId = vltnInfo.string("VLTN_ID");
        }

        // 사용자 정보를 조회한다.
        DataObject userInfo = userBean.getUserInfo(currentUser().getId());
        // 발송(TB_SNDNG) 대장에 등록한다.
        sndng.setDeptCd(userInfo.string("DEPT_CD"));                            // 부서 코드
        sndng.setVltnId(vltnId);                                                // 위반 ID
        sndng.setSndngEndYmd(null);                                             // 발송 종료 일자

        if (sndng.getSndngRegSeCd() == null)
            sndng.setSndngRegSeCd("02");                                        // 발송 등록 구분 코드 - 개별
        if (sndng.getRsndYn() == null)
            sndng.setRsndYn("N");                                               // 재발송 여부
        if (sndng.getDelYn() == null)
            sndng.setDelYn("N");                                                // 삭제 여부
        sndng.setSndngSttsCd("00");                                             // 발송 상태 코드(FIM049) - 00 발송 준비

        // 발송(TB_SNDNG) 대장을 등록한다.
        int rtnNocs = sndngMapper.insertSndng(sndng);
        if (rtnNocs != 1)
            throw new RuntimeException("계고장 발송 등록 작업에 실패하였습니다.");

        // 발송 상세(TB_SNDNG_DTL) 대장을 등록한다.
        for (DataObject trgt: trgtList) {
            String rtnMsg = createWrngSndngDtl(sndng, trgt.string("CRDN_ID"));
            if (!rtnMsg.contains("[S]"))
                throw new RuntimeException(rtnMsg.replace("[F]", ""));
        }

        return "[S] 작업이 정상 처리 되었습니다.";
    }

    /**계고장 발송 대장 정보를 삭제한다.
     * @param sndng 발송 대장
     * @return 저장 여부
     * <ul><li>저장됐으면 true</li>
     *     <li>그렇지 않으면 false</li>
     * </ul>
     */
    public String removeWrngSndng(Sndng sndng) {
        // 발송 상세 대장 조회
        SndbQuery req = new SndbQuery()
        	.setSndngId(sndng.getSndngId())
        	.setDelYn("N");
        List<DataObject> sndngDtlList = sndngDtlMapper.selectSndngDtls(req);

        // 발송 상세 상태 코드 검증 - 00(처리전)
        for (DataObject info : sndngDtlList) {
            if (!"00".equals(info.string("SNDNG_DTL_STTS_CD")))
                throw new RuntimeException("[F] 발송 상세 자료중 상태코드가 '처리전'이 아닌 자료가 있습니다.");

            CrdnSttsHstry crdnSttsHstry = new CrdnSttsHstry();
            crdnSttsHstry.setCrdnId(info.string("CRDN_ID"));                    // 단속 ID
            crdnSttsHstry.setCrdnSttsCd(info.string("CRDN_STTS_CD"));           // 단속 상태 코드
            crdnSttsHstry.setEtcCn("계고장 발송 현황 메뉴에서 발송 삭제 처리"); // 기타 내용
            crdnSttsHstry.setUseYn("N");

            // 단속 상태 이력(TB_CRDN_STTS_HSTRY) 대장의 사용 여부를 "N"으로 처리하고, 단속(TB_CRDN) 대장의 단속 상태 코드를 수정한다.
            boolean rtnScs = crdnSttsHstryBean.removeHstryUpdateCrdnSttsCd(crdnSttsHstry);
            if (!rtnScs)
                throw new RuntimeException("[F] 발송상세 삭제 작업중 단속 대장의 단속상태 변경에 실패하였습니다.");
        }

        // 발송(TB_SNDNG), 발송 상세(TB_SNDNG_DTL) 대장을 삭제한다.
        String rtnMsg = removeSndng(sndng);
        // 메시지 확인
        if (!rtnMsg.contains("[S]"))
            throw new RuntimeException(rtnMsg.replace("[F]", ""));

        return "[S] 작업이 정상 처리 되었습니다.";
    }

    /**지정한 조건에 따라 사전통지 발송 대상 목록을 조회하여 반환한다.
     * @param req 발송 대장 조회 조건
     * @return 사전통지 발송 대상 목록
     */
    public List<DataObject> getAdvntceTrgtList(SndbQuery req) {
        // 삭제 여부 확인
        if (req.getDelYn() == null) {
            req.setDelYn("N");
        }
        // 정렬 확인
        if (req.getOrderBy() == null) {
            if (req.getBy() == null) {
                req.setOrderBy("REG_DT");
            } else {
                req.setOrderBy(CmmnUtil.convertCamelCaseToSnakeCase(req.getBy()));
            }
        }

        return sndngMapper.selectAdvntceTrgtList(req);
    }

    /**지정한 조건에 따라 사전통지 발송 대상 객체들을 반환한다.
     * @param req 발송 대장 조회 조건
     * @return 사전통지 발송 대상 객체 목록
     */
    public List<DataObject> getAdvntceTrgts(SndbQuery req) {
        // 삭제 여부 확인
        if (req.getDelYn() == null) {
            req.setDelYn("N");
        }
        // 정렬 확인
        if (req.getOrderBy() == null) {
            req.setOrderBy("CRDN_YMD_TM");
        }

        return sndngMapper.selectAdvntceTrgts(req);
    }

    /**사전통지 발송 대상을 조회하여 발송 대장에 등록한다.
     * @param req 사전통지 발송 대상 조회, sndng 발송 대장
     * @return 저장 여부
     * <ul><li>저장됐으면 true</li>
     *     <li>그렇지 않으면 false</li>
     * </ul>
     */
    public String createAdvntceSndngList(SndbQuery req, Sndng sndng) {
        // 단속 Ids로 사전통지 발송 대상 정보 조회
        List<DataObject> trgtList = sndngMapper.selectAdvntceTrgtCrdnIds(req);
        // 조회 건수 확인
        if (isEmpty(trgtList))
            return "[F] 작업 중 대상 자료가 존재하지 않습니다.";
        if (sndng.getTnocs() != trgtList.size())
            return "[F] 조회된 총 건수와 대상 건수가 다릅니다.";

        // 사용자 정보를 조회한다.
        DataObject userInfo = userBean.getUserInfo(currentUser().getId());
        // 위반 ID 조회
        String vltnId = "";
        if (req.getVltnCd() != null) {
            VltnQuery vltnReq = new VltnQuery()
            	.setVltnCd(req.getVltnCd())
            	.setSggCd(sndng.getSggCd())
            	.setTaskSeCd(sndng.getTaskSeCd());
            DataObject vltnInfo = vltnMapper.selectVltnInfo(vltnReq);

            vltnId = vltnInfo.string("VLTN_ID");
        }

        // 발송 대장에 등록한다.
        sndng.setDeptCd(userInfo.string("DEPT_CD"));                            // 부서 코드
        sndng.setVltnId(vltnId);                                                // 위반 ID

        if (sndng.getSndngRegSeCd() == null)
            sndng.setSndngRegSeCd("02");                                        // 발송 등록 구분 코드(FIM067) - 02: 개별
        if (sndng.getRsndYn() == null)
            sndng.setRsndYn("N");                                               // 재발송 여부
        if (sndng.getDelYn() == null)
            sndng.setDelYn("N");                                                // 삭제 여부
        sndng.setSndngSttsCd("00");                                             // 발송 상태 코드(FIM049) - 00: 발송 준비

        // 발송(TB_SNDNG) 대장을 등록한다.
        int rtnNocs = sndngMapper.insertSndng(sndng);
        if (rtnNocs != 1)
            throw new RuntimeException("사전통지 발송 등록에 실패하였습니다.");

        // 발송 상세 대장에 등록한다.
        for (int i = 0; i < trgtList.size(); i++) {
            String rtnMsg = createAdvntceSndngDtl(sndng, trgtList.get(i).string("CRDN_ID"));
            if (!rtnMsg.contains("[S]"))
                throw new RuntimeException(rtnMsg.replace("[F]", ""));
        }

        return "[S] 작업이 정상 처리 되었습니다.";
    }

    /**사전통지 발송 상세 정보를 등록한다.
     * @param sndngDtl 발송 상세
     * @return 저장 여부
     * <ul><li>저장됐으면 true</li>
     *     <li>그렇지 않으면 false</li>
     * </ul>
     */
    public String createAdvntceSndngDtl(Sndng sndng, String crdnId) {
        // 단속(TB_CRDN), 납부자(TB_PAYER), 부과(TB_LEVY) 대장을 조회 한다.
        DataObject crdnPayerInfo = sndngMapper.selectCrdnPayerInfo(crdnId);
        if (crdnPayerInfo == null)
            return "[F] 발송상세 등록 작업중 단속 정보가 확인되지 않았습니다."
                 + "<p>" + "단속 ID : " + crdnId + "</p>";
        if (isEmpty(crdnPayerInfo.get("RTPYR_ID")))
            return "[F] 발송상세 등록 작업중 납부자 정보가 확인되지 않았습니다."
                 + "<p>"  + "단속ID : " + crdnId
                 + "<br>" + "단속일시 : " + crdnPayerInfo.string("CRDN_YMD_TM_MASK")
                 + "<br>" + "차량번호 : " + crdnPayerInfo.string("VHRNO")
                 + "</p>";

        String rtnMsg = "";
		String sggCd = crdnPayerInfo.string("SGG_CD");
        String taskSeCd = crdnPayerInfo.string("TASK_SE_CD");
		if (TaskProcessor.get().isPreImposable(taskSeCd)) {
            // 사전 감경부과가 되지 않았다면.. 감경부과를 등록한다.
            if (isEmpty(crdnPayerInfo.get("LEVY_ID"))) {
                Levy levy = new Levy();
                levy.setSggCd(sggCd);                  // 시군구 코드
                levy.setTaskSeCd(taskSeCd);                                     // 업무 구분 코드
                levy.setCrdnId(crdnPayerInfo.string("CRDN_ID"));                // 단속 ID
                levy.setLevyYmd(sndng.getSndngYmd());                           // 부과 일자
                levy.setFrstDudtYmd(sndng.getSndngEndYmd());                    // 최초 납기 일자
                levy.setDudtYmd(sndng.getSndngEndYmd());                        // 납기 일자

                // 부과(TB_LEVY) 대장을 등록한다.
                rtnMsg = levyBean.createRdctLevy(levy);

                // 단속(TB_CRDN), 납부자(TB_PAYER), 부과(TB_LEVY) 대장을 조회 한다.
                crdnPayerInfo = sndngMapper.selectCrdnPayerInfo(crdnId);
            } else {
                // 부과 가산금 확인
                if (toInt(crdnPayerInfo.get("LEVY_ADAMT")) != 0)
                    return "[F] 발송상세 등록 작업중 부과 가산금액이 0원이 아닙니다."
                         + "<p>"  + "단속ID : " + crdnPayerInfo.string("CRDN_ID")
                         + "<br>" + "단속일시 : " + crdnPayerInfo.string("CRDN_YMD_TM_MASK")
                         + "<br>" + "차량번호 : " + crdnPayerInfo.string("VHRNO")
                         + "</p>";
                // 사전 감경금액과 부과 본세금액 확인
                if (toInt(crdnPayerInfo.get("ADVNTCE_AMT")) != toInt(crdnPayerInfo.get("LEVY_PCPTAX")))
                    return "[F] 발송상세 등록 작업중 사전 감경금액과 부과 본세금액이 다릅니다."
                         + "<p>"  + "단속ID : " + crdnPayerInfo.string("CRDN_ID")
                         + "<br>" + "단속일시 : " + crdnPayerInfo.string("CRDN_YMD_TM_MASK")
                         + "<br>" + "차량번호 : " + crdnPayerInfo.string("VHRNO")
                         + "<br>" + "사전 감경금액 : " + crdnPayerInfo.string("ADVNTCE_AMT")
                         + "<br>" + "부과 본세금액 : " + crdnPayerInfo.string("LEVY_PCPTAX")
                         + "</p>";
                // 부과 본세와 합계 금액이 같은지 확인
                if (toInt(crdnPayerInfo.get("LEVY_PCPTAX")) != toInt(crdnPayerInfo.get("LAST_AMT")))
                    return "[F] 발송상세 등록 작업중 본세금액과 최종금액이 다릅니다."
                         + "<p>"  + "단속ID : " + crdnPayerInfo.string("CRDN_ID")
                         + "<br>" + "단속일시 : " + crdnPayerInfo.string("CRDN_YMD_TM_MASK")
                         + "<br>" + "차량번호 : " + crdnPayerInfo.string("VHRNO")
                         + "<br>" + "본세금액 : " + crdnPayerInfo.string("LEVY_PCPTAX")
                         + "<br>" + "최종금액 : " + crdnPayerInfo.string("LAST_AMT")
                         + "</p>";

                Levy levy = new Levy();
                levy.setLevyId(crdnPayerInfo.string("LEVY_ID"));                    // 부과 ID
                levy.setLevyYmd(sndng.getSndngYmd());                               // 부과 일자
                levy.setFrstDudtYmd(sndng.getSndngEndYmd());                        // 최초 납기 일자
                levy.setDudtYmd(sndng.getSndngEndYmd());                            // 납기 일자
                levy.setFfnlgAmt(toInt(crdnPayerInfo.get("FFNLG_AMT")));     // 과태료 금액
                levy.setLevyPcptax(toInt(crdnPayerInfo.get("ADVNTCE_AMT"))); // 본세 금액
                levy.setLastAmt(toInt(crdnPayerInfo.get("ADVNTCE_AMT")));    // 최종 금액

                // 부과(TB_LEVY) 대장에 부과일자, 최초납기일자, 납기일자를 수정한다.
                rtnMsg = levyBean.updateRdctLevyYmd(levy);
                if (!rtnMsg.contains("[S]"))
                    throw new RuntimeException(rtnMsg.replace("[F]", ""));
            }
        }

        // 발송 상세(TB_SNDNG_DTL)
        SndngDtl sndngDtl = new SndngDtl(sndng).setPayerInfo(crdnPayerInfo);
        sndngDtl.setLevyYmd(sndng.getSndngYmd());                               // 부과 일자
        sndngDtl.setDudtYmd(sndng.getSndngEndYmd());                            // 납기 일자
        if (isEmpty(crdnPayerInfo.string("PCPTAX"))) {
            sndngDtl.setPcptax(toInt(crdnPayerInfo.get("ADVNTCE_AMT"))); // 본세 = 사전통지 금액
        } else {
            sndngDtl.setPcptax(toInt(crdnPayerInfo.get("PCPTAX")));      // 본세
        }
        sndngDtl.setAdamt(0);                                                   // 가산금 = 0
        if (isEmpty(crdnPayerInfo.string("LAST_AMT"))) {
            sndngDtl.setLastAmt(toInt(crdnPayerInfo.get("ADVNTCE_AMT")));    // 최종 금액 = 사전통지 금액
        } else {
            sndngDtl.setLastAmt(toInt(crdnPayerInfo.get("LAST_AMT")));       // 최종 금액
        }
        sndngDtl.setDudtAftrAmt(0);                                             // 납기 후 금액

        // 발송 상세(TB_SNDNG_DTL) 대장을 등록한다.
        int rtnNocs = sndngDtlMapper.insertSndngDtl(sndngDtl);
        if (rtnNocs != 1)
            throw new RuntimeException("발송상세 등록에 실패하였습니다.");

        // 단속 대장에 사전통지 일자를 수정한다.
        Crdn crdn = new Crdn();
        crdn.setCrdnId(crdnPayerInfo.string("CRDN_ID"));
        crdn.setSggCd(sggCd);
        crdn.setTaskSeCd(taskSeCd);
        crdn.setAdvntceBgngYmd(sndng.getSndngYmd());
        crdn.setAdvntceDudtYmd(sndng.getSndngEndYmd());
        crdn.setDelYn("N");

        // 단속(TB_CRDN) 대장을 수정한다.
        rtnNocs = crdnUpdtMapper.updateAdvntceYmd(crdn);
        if (rtnNocs != 1)
            throw new RuntimeException("단속 대장에 사전통지일자 수정에 실패하였습니다.");

        // 단속 상태 이력(TB_CRDN_STTS_HSTRY)을 등록한다.
        CrdnSttsHstry crdnSttsHstry = new CrdnSttsHstry();
        crdnSttsHstry.setCrdnId(crdnPayerInfo.string("CRDN_ID"));
        crdnSttsHstry.setBfrSttsCd(crdnPayerInfo.string("CRDN_STTS_CD"));
        crdnSttsHstry.setBfrSttsChgDt(crdnPayerInfo.string("CRDN_STTS_CHG_DT"));
        crdnSttsHstry.setCrdnSttsCd("42");
        crdnSttsHstry.setTaskDtlId(sndngDtl.getSndngDtlId());
        crdnSttsHstry.setUseYn("Y");

        boolean rtnScs = false;
        if ("21,22,23".contains(crdnPayerInfo.string("CRDN_STTS_CD"))) {
            // 단속 상태 이력(TB_CRDN_STTS_HSTRY) 대장에 등록하고, 단속(TB_CRDN) 대장을 수정한다.
            rtnScs = crdnSttsHstryBean.createHstryUpdateCrdnSttsCd(crdnSttsHstry);
            if (!rtnScs)
                throw new RuntimeException("발송상세 등록 작업중 단속 대장의 단속상태 변경에 실패하였습니다.");
        } else if (crdnPayerInfo.string("CRDN_STTS_CD").equals("31")) {
            // 단속 상태 이력(TB_CRDN_STTS_HSTRY) 대장에 등록한다.
            rtnScs = crdnSttsHstryBean.create(crdnSttsHstry);
            if (!rtnScs)
                throw new RuntimeException("발송상세 등록 작업중 단속 대장의 단속상태 변경에 실패하였습니다.");
        }

        return "[S] 작업이 정상 처리 되었습니다.";
    }

    /**사전통지 발송 대장 정보를 삭제한다.
     * @param sndng 발송 대장
     * @return 저장 여부
     * <ul><li>저장됐으면 true</li>
     *     <li>그렇지 않으면 false</li>
     * </ul>
     */
    public String removeAdvntceSndng(Sndng sndng) {
        // 발송 상세 대장 조회
        SndbQuery req = new SndbQuery()
        	.setSndngId(sndng.getSndngId())
        	.setDelYn("N");

        // 발송 상세 상태 코드 검증 - 00(처리전)
        for (DataObject info: sndngDtlMapper.selectSndngDtls(req)) {
            if (!"00".equals(info.string("SNDNG_DTL_STTS_CD")))
                throw new RuntimeException("[F] 발송 상세 자료중 상태코드가 '처리전'이 아닌 자료가 있습니다.");

            CrdnSttsHstry crdnSttsHstry = new CrdnSttsHstry();
            crdnSttsHstry.setCrdnId(info.string("CRDN_ID"));                    // 단속 ID
            crdnSttsHstry.setCrdnSttsCd(info.string("CRDN_STTS_CD"));           // 단속 상태 코드
            crdnSttsHstry.setEtcCn("사전통지 발송 현황 메뉴에서 발송 삭제 처리"); // 기타 내용
            crdnSttsHstry.setUseYn("N");

            // 단속 상태 이력(TB_CRDN_STTS_HSTRY) 대장의 사용 여부를 "N"으로 처리하고, 단속(TB_CRDN) 대장의 단속 상태 코드를 수정한다.
            boolean rtnScs = crdnSttsHstryBean.removeHstryUpdateCrdnSttsCd(crdnSttsHstry);
            if (!rtnScs)
                throw new RuntimeException("[F] 발송상세 삭제 작업중 단속 대장의 단속상태 변경에 실패하였습니다.");
        }

        // 발송(TB_SNDNG), 발송 상세(TB_SNDNG_DTL) 대장을 삭제한다.
        String rtnMsg = removeSndng(sndng);
        // 메시지 확인
        if (!rtnMsg.contains("[S]"))
            throw new RuntimeException(rtnMsg.replace("[F]", ""));

        return "[S] 작업이 정상 처리 되었습니다.";
    }

    /**지정한 조건에 따라 고지서 발송 대상 목록을 조회하여 반환한다.
     * @param req 발송 대장 조회 조건
     * @return 고지서 발송 대상 목록
     */
    public List<DataObject> getNhtTrgtList(SndbQuery req) {
        // 삭제 여부 확인
        if (req.getDelYn() == null) {
            req.setDelYn("N");
        }
        // 정렬 확인
        if (req.getOrderBy() == null) {
            if (req.getBy() == null) {
                req.setOrderBy("REG_DT");
            } else {
                req.setOrderBy(CmmnUtil.convertCamelCaseToSnakeCase(req.getBy()));
            }
        }

        return sndngMapper.selectNhtTrgtList(req);
    }

    /**지정한 조건에 따라 사전 통보 발송 대상 객체들을 반환한다.
     * @param req 발송 대장 조회 조건
     * @return 고지서 발송 대상 객체 목록
     */
    public List<DataObject> getNhtTrgts(SndbQuery req) {
        // 삭제 여부 확인
        if (req.getDelYn() == null) {
            req.setDelYn("N");
        }
        // 정렬 확인
        if (req.getOrderBy() == null) {
            req.setOrderBy("CRDN_YMD_TM");
        }

        return sndngMapper.selectNhtTrgts(req);
    }

    /**고지서 발송 대상을 조회하여 발송 대장에 등록한다.
     * @param req 고지서 발송 대상 조회, sndng 발송 대장
     * @return 저장 여부
     * <ul><li>저장됐으면 true</li>
     *     <li>그렇지 않으면 false</li>
     * </ul>
     */
    public String createNhtSndngList(SndbQuery req, Sndng sndng) {
        // 단속 Ids로 고지서 발송 대상 정보 조회
        List<DataObject> nhtTrgtList = sndngMapper.selectNhtTrgtCrdnIds(req);
        // 조회 건수 확인
        if (isEmpty(nhtTrgtList))
            return "[F] 작업 중 대상 자료가 존재하지 않습니다.";
        if (sndng.getTnocs() != nhtTrgtList.size())
            return "[F] 조회된 총 건수와 대상 건수가 다릅니다.";

        // 사용자 정보를 조회한다.
        DataObject userInfo = userBean.getUserInfo(currentUser().getId());
        // 발송 대장에 등록한다.
        sndng.setDeptCd(userInfo.string("DEPT_CD"));                            // 부서 코드
        if (sndng.getSndngRegSeCd() == null)
            sndng.setSndngRegSeCd("02");                                        // 발송 등록 구분 코드(FIM067) - 02: 개별
        if (sndng.getTnocs() == null)
            sndng.setTnocs(nhtTrgtList.size());                                 // 총건수
        if (sndng.getGramt() == null)
            sndng.setGramt(toLong(nhtTrgtList.get(0).get("GRAMT")));     // 총금액
        if (sndng.getRsndYn() == null)
            sndng.setRsndYn("N");                                               // 재발송 여부
        if (sndng.getDelYn() == null)
            sndng.setDelYn("N");                                                // 삭제 여부
        sndng.setSndngSttsCd("00");                                             // 발송 상태 코드(FIM049) - 00 발송 준비

        // 발송(TB_SNDNG) 대장을 등록한다.
        int rtnNocs = sndngMapper.insertSndng(sndng);
        if (rtnNocs != 1)
            throw new RuntimeException("고지서 발송 등록에 실패하였습니다.");

        // 발송 상세 대장에 등록한다.
        for (DataObject nhtTrgt: nhtTrgtList) {
            String rtnMsg = createNhtSndngDtl(sndng, nhtTrgt.string("CRDN_ID"));
            if (!rtnMsg.contains("[S]"))
                throw new RuntimeException(rtnMsg.replace("[F]", ""));
        }

        return "[S] 작업이 정상 처리 되었습니다.";
    }

    /**고지서 발송 상세 정보를 등록한다.
     * @param sndngDtl 발송 상세
     * @return 저장 여부
     * <ul><li>저장됐으면 true</li>
     *     <li>그렇지 않으면 false</li>
     * </ul>
     */
    public String createNhtSndngDtl(Sndng sndng, String crdnId) {
        // 단속, 납부자 대장을 조회 한다.
        DataObject crdnPayerInfo = sndngMapper.selectCrdnPayerInfo(crdnId);
        if (crdnPayerInfo == null)
            return "[F] 발송상세 등록 작업중 단속 정보가 확인되지 않았습니다."
                 + "<p>" + "단속 ID : " + crdnId    + "</p>";
        if (isEmpty(crdnPayerInfo.get("RTPYR_ID")))
            return "[F] 발송상세 등록 작업중 납부자 정보가 확인되지 않았습니다."
                 + "<p>"  + "단속 ID : " + crdnId
                 + "<br>" + "단속일시 : " + crdnPayerInfo.string("CRDN_YMD_TM_MASK")
                 + "<br>" + "차량번호 : " + crdnPayerInfo.string("VHRNO")
                 + "</p>";
        if (isEmpty(crdnPayerInfo.get("LEVY_ID")))
            return "[F] 발송상세 등록 작업중 부과 정보가 확인되지 않았습니다."
                 + "<p>"  + "단속 ID : " + crdnId
                 + "<br>" + "단속일시 : " + crdnPayerInfo.string("CRDN_YMD_TM_MASK")
                 + "<br>" + "차량번호 : " + crdnPayerInfo.string("VHRNO")
                 + "</p>";

        // 발송 상세(TB_SNDNG_DTL)
        SndngDtl sndngDtl = new SndngDtl(sndng).setPayerInfo(crdnPayerInfo);
        int rtnNocs = sndngDtlMapper.insertSndngDtl(sndngDtl);
        if (rtnNocs != 1)
            throw new RuntimeException("발송상세 등록에 실패하였습니다.");

        return "[S] 작업이 정상 처리 되었습니다.";
    }

    /**지정한 조건에 따라 발송 대장 목록을 조회하여 반환한다.
     * @param req 발송 대장 조회 조건
     * @return 발송 대장 목록
     */
    public List<DataObject> getSndngList(SndbQuery req) {
        // 삭제 여부 확인
        if (req.getDelYn() == null) {
            req.setDelYn("N");
        }
        // 정렬 확인
        if (req.getOrderBy() == null) {
            if (req.getBy() == null) {
                req.setOrderBy("REG_DT");
            } else {
                req.setOrderBy(CmmnUtil.convertCamelCaseToSnakeCase(req.getBy()));
            }
        }

        return sndngMapper.selectSndngList(req);
    }

    /**지정한 조건에 따라 발송 대장 객체들을 반환한다.
     * @param req 발송 대장 조회 조건
     * @return 발송 대장 객체 목록
     */
    public List<Sndng> getSndngs(SndbQuery req) {
        // 삭제 여부 확인
        if (req.getDelYn() == null) {
            req.setDelYn("N");
        }
        // 정렬 확인
        if (req.getOrderBy() == null) {
            if (req.getBy() == null) {
                req.setOrderBy("REG_DT");
            } else {
                req.setOrderBy(CmmnUtil.convertCamelCaseToSnakeCase(req.getBy()));
            }
        }
        return sndngMapper.selectSndngs(req);
    }

    /**발송 대장 정보를 삭제한다.
     * @param sndng 발송 대장
     * @return 저장 여부
     * <ul><li>저장됐으면 true</li>
     *     <li>그렇지 않으면 false</li>
     * </ul>
     */
    public String removeSndng(Sndng sndng) {
        // 발송 상세(TB_SNDNG_DTL) 대장을 삭제한다.
        SndngDtl sndngDtl = new SndngDtl(sndng);
        sndngDtl.setDelRsn(sndng.getDelRsn());                                  // 삭제 사유
        int rtnNocs = sndngDtlMapper.deleteSndngDtl(sndngDtl);
        if (rtnNocs < 1)
            throw new RuntimeException("발송상세 정보 삭제에 실패하였습니다.");

        // 발송(TB_SNDNG) 대장을 삭제한다.
        rtnNocs = sndngMapper.deleteSndng(sndng);
        if (rtnNocs != 1)
            throw new RuntimeException("발송 정보 삭제에 실패하였습니다.");

        return "[S] 작업이 정상 처리 되었습니다.";
    }

    /**전자우편 발송 대장 정보를 등록한다.
     * @param sndng 발송 대장
     * @return 저장 여부
     * <ul><li>저장됐으면 true</li>
     *     <li>그렇지 않으면 false</li>
     * </ul>
     */
    public String createEPost(Sndng sndng) {
    	//test/resources/우편통합 cs소스.txt 참고
    	SndbQuery sndbQuery = new SndbQuery().setSndngId(sndng.getSndngId()).setDelYn("N");
        // 1. 발송 대장 조회
        DataObject sndngInfo = sndngMapper.selectSndngInfo(sndbQuery);
        // 2. 사용자 정보를 조회한다.
        DataObject userInfo = userBean.getUserInfo(currentUser().getId());
        // 3. 자치단체 및 부서 정보를 조회한다.
        DataObject deptSggInfo = epostRcptRegBean.getDeptSggInfo(sndngInfo.string("DEPT_CD"));
        // 4. 위반 정보를 조회한다.
		String sggCd = sndngInfo.string("SGG_CD");
        String taskSeCd = sndngInfo.string("TASK_SE_CD");
		VltnQuery vltnQuery = new VltnQuery()
        	.setSggCd(sggCd)
        	.setTaskSeCd(taskSeCd);
        if (isEmpty(sndngInfo.string("VLTN_ID"))) {       // 위반 ID가 없다면..
            vltnQuery.setVltnCd("01");
        } else {
            vltnQuery.setVltnId(sndngInfo.string("VLTN_ID"));
        }
        DataObject vltnInfo = vltnMapper.selectVltnInfo(vltnQuery);

        TaskProcessor taskProcessor = TaskProcessor.get();
        String sndngSeCd = sndngInfo.string("SNDNG_SE_CD");
        String sealCd3 = taskProcessor.getSealCd3(taskSeCd, sndngSeCd);
        if (isEmpty(sealCd3))
        	return "[F] 작업중 지정되지 않았습니다.";

        // 발송 구분
        String vltnCd = vltnInfo.string("VLTN_CD");
        String jobCd = switch (sndngSeCd) {
        case "01", "11" -> vltnCd + "01"; // 계고장
        case "02" -> vltnCd + "02"; // 사전통지
        case "03" -> vltnCd + "03"; // 부과
        case "04" -> vltnCd + "04"; // 독촉
        case "05" -> vltnCd + "05"; // 압류예고
        case "06" -> vltnCd + "06"; // 압류통지
        case "09" -> vltnCd + "09"; // 체납분
        default -> null;
        };
        if (isEmpty(jobCd))
        	return "[F] 작업중 발송 구분이 지정되지 않았습니다.";

        String epostSenderRegSeq = epostRcptRegBean.getEpostSenderRegSeq(sndngInfo.string("TODAY"), sndngInfo.string("DEPT_CD"));
        int seqNo = 0;
        String postSndngSeCd = sndng.getPostSndngSeCd();
		if ("0".equals(postSndngSeCd))
			seqNo = epostRcptDtlBean.getEpostSenderDetailSeq(sndngInfo.string("TODAY"));
		// 포맷: 등록일(REG_YMD)-발송기관코드(RECEV_SENDER_ORG_CODE)-업무코드(WORK_ID)-우편물구분(POST_SE)일련번호(RECEV_SEQ_NO)
		// 예시: 20170719-40504370000-003-1001
		String conKey = sndngInfo.string("TODAY") + "-" + sndngInfo.string("DEPT_CD") + "0000" + "-" + epostSenderRegSeq;

        // 5. 전자우편 접수 내역을 등록한다.
        EpostRcptReg epostRcptReg = new EpostRcptReg();
        epostRcptReg.setConKey(conKey);                              // 외부연계식별키
        epostRcptReg.setConOrg(deptSggInfo.string("EGP_CON_ORG"));   // 외부기관구분코드
        epostRcptReg.setRceptId(deptSggInfo.string("EGP_RCEPT_ID")); // 접수우체국국기호
        epostRcptReg.setDataCd("00");                                // 접수코드

        // 취급구분(DIV_KB) 000=일반 001=등기 201=선택등기
        if ("1".equals(postSndngSeCd)) {
            if ("11500".equals(sggCd)) {
                epostRcptReg.setDivKb("00" + postSndngSeCd);
                epostRcptReg.setSealCd("1" + "2" + sealCd3);
            } else {
                epostRcptReg.setDivKb("20" + postSndngSeCd);
                epostRcptReg.setSealCd("E" + "2" + sealCd3);
            }
        } else {
            epostRcptReg.setDivKb("00" + postSndngSeCd);
            epostRcptReg.setSealCd(postSndngSeCd + "2" + sealCd3);
        }

        epostRcptReg.setDfpayyn("001");                          // 결재방법(DFPAYYN)
        epostRcptReg.setRcptKb("020");                           // 내용문 유형(RCPT_KB)
        epostRcptReg.setWordKb("001");                           // 내용문 종류(WORD_KB)
        epostRcptReg.setRecevCnt(toInt(sndngInfo.get("TNOCS"))); // 수취인 수 -> 건수 확인 필요하다. /////////////////////////////
        epostRcptReg.setEnvCd("003");                            // 봉투 종류 - 접착식

        String ctpvCode = sggCd.substring(0, 2);                 // 시도 코드
        boolean seoul = "11".equals(ctpvCode);
		if (seoul) {                                             // 서울특별시
            epostRcptReg.setColorYn("Y");
            epostRcptReg.setMailCnt(2);
            epostRcptReg.setFlexCd(sealCd3.equals("E") ? "N" : "Y");
        } else {
            if (sggCd.equals("44200")) {                         // 아산시청
                if (sndng.getSndngSeCd().equals("02")) {         // 사전통지는 컬러로..
                    epostRcptReg.setColorYn("Y");
                } else {
                    epostRcptReg.setColorYn("N");
                }
            } else if (sggCd.equals("50110")) {    // 제주시청
            	epostRcptReg.setColorYn("02,01,10".contains(sndng.getSndngSeCd()) ? "Y" : "N"); // 사전통지, 계고 컬러로
            } else {
                epostRcptReg.setColorYn("N");                              // 칼라우편물/흑백우편물(COLOR_YN)
            }

            epostRcptReg.setFlexCd("N");                                   // 이면구분(FLEX_CD)
            epostRcptReg.setMailCnt(1);                                    // 내용문 매수(MAIL_CNT)
        }

        epostRcptReg.setMmYn("Y");                                         // 메일머지 플래그(MM_YN)
        epostRcptReg.setDmCnt(0);                                          // 동봉물 개수(DM_CNT)
        epostRcptReg.setSbFg("Y");                                         // 반송불필요 여부(SB_FG)
        epostRcptReg.setApvlNb(deptSggInfo.string("EGP_APVL_NB"));         // 후납계약 승인번호(APVL_NB)
        epostRcptReg.setSendDate(sndngInfo.string("NOW").substring(0, 8)); // 신청 일자(SEND_DATE)
        epostRcptReg.setSendTime(sndngInfo.string("NOW").substring(8));    // 전송 시간(SEND_TIME)
        String relorsectCd = taskProcessor.getRelorsectCd(                 // 연계기관결제부서코드(RELORSECT_CD)
        	new DataObject()
            	.set("taskSeCd", taskSeCd)
            	.set("sndngDept", sndngInfo.get("DEPT_CD"))
            	.set("deptCd", deptSggInfo.string("DEPT_CD"))
        );
        epostRcptReg.setRelorsectCd(relorsectCd);

        epostRcptReg.setRecevSenderOrgCode(deptSggInfo.string("DEPT_CD") + "0000"); // 발송기관코드(RECEV_SENDER_ORG_CODE)
        epostRcptReg.setRecevSenderNm(deptSggInfo.string("INST_NM") + " " + deptSggInfo.string("DEPT_NM")); // 발송인명(SENDER_NM)
        epostRcptReg.setRecevSenderZipcode(deptSggInfo.string("INST_ZIP"));         // 우편번호(SENDER_ZIPCODE)
        epostRcptReg.setRecevSenderAddr(deptSggInfo.string("INST_ADDR"));           // 주소(SENDER_ADDR)
        epostRcptReg.setRecevSenderDetailaddr(deptSggInfo.string("INST_DADDR"));    // 상세주소(SENDER_DTAILADDR)
        epostRcptReg.setRecevSenderDepartTel(deptSggInfo.string("DEPT_TELNO"));     // 부과부서 전화번호(RECEV_SENDER_DEPART_TEL)
        epostRcptReg.setRecevSenderDepartNm(deptSggInfo.string("DEPT_NM"));         // 부과부서(RECEV_SENDER_DEPART_NM)
        epostRcptReg.setRecevDivCd(postSndngSeCd);                       // 등기구분(RECEV_DIV_CD)
        epostRcptReg.setRecevPrintDt(sndngInfo.string("TODAY_MASK"));               // 출력일자(RECEV_PRINT_DT)
        epostRcptReg.setRecevPrintYear(sndngInfo.string("TODAY").substring(0, 4));  // 출력연도(RECEV_PRINT_YEAR)
        epostRcptReg.setRecevPrintMonth(sndngInfo.string("TODAY").substring(4, 6)); // 출력월(RECEV_PRINT_MONTH)
        epostRcptReg.setRecevPrintDay(sndngInfo.string("TODAY").substring(6, 8));   // 출력일(RECEV_PRINT_DAY)
        epostRcptReg.setRecevSenderFax(deptSggInfo.string("DEPT_FXNO"));            // 부과부서 팩스번호(RECEV_SENDER_FAX)
        epostRcptReg.setRecevSerderStaff(userInfo.string("USER_NM"));               // 부과부서 담당자(RECEV_SERDER_STAFF)
        epostRcptReg.setRecevSenderEmail(userInfo.string("EML_ADRS"));              // 부과부서 이메일(RECEV_SENDER_EMAIL)
        epostRcptReg.setJobCd(jobCd);                                               // 작업 코드(JOB_CD)
        epostRcptReg.setPostPrcsSttsCd("01");                                       // 우편 처리 상태 코드(POST_PRCS_STTS_CD)
        epostRcptReg.setDelYn("N");                                                 // 삭제 여부
        epostRcptReg.setRceptYmd(sndngInfo.string("TODAY"));                        // 접수 일자
        epostRcptReg.setSndngId(sndngInfo.string("SNDNG_ID"));                      // 발송 ID

        // 6. 전자우편 접수 등록
        boolean success = epostRcptRegBean.create(epostRcptReg);
        if (!success)
            throw new RuntimeException("우편통합 발송 작업중 전자우편 접수 등록에 실패하였습니다.");

        // 7. 발송 내역을 수정한다.
        Sndng updtSndng = new Sndng();
        updtSndng.setConKey(epostRcptReg.getConKey()); // 외부연계식별키
        updtSndng.setDivKb(epostRcptReg.getDivKb());   // 취급 구분
//        updtSndng.setEpostNoticeId();                // 전자우편 안내문 ID
        updtSndng.setSndngSttsCd("01");                // 발송 상태 코드(FIM049) - 01 발송 대기
        updtSndng.setSndngId(sndng.getSndngId());      // 발송 ID

        int affected = sndngMapper.updateEPostSndng(updtSndng); // 전자우편 발송 내역 수정
        if (affected != 1)
            throw new RuntimeException("우편통합 발송 작업중 발송 내역 수정에 실패하였습니다.");

        // 8. 전자우편 접수 상세 내역을 등록한다.
        String infoSndngSeNm = switch (sndngSeCd) { // 약식 발송 구분 명
        case "01" -> "계고"; // 계고
        case "02" -> "사전"; // 사전통지서
        case "03" -> "부과"; // 부과고지서
        case "04" -> "독촉"; // 독촉고지서
        case "05" -> "예고"; // 압류예고통지서
        case "06" -> "압류"; // 압류통지
        case "07" -> "사전"; // 사전통지서(개별)
        case "08" -> "체납"; // 체납고지서(개별)
        case "09" -> "체납"; // 체납고지서
        default -> "";
        };

        // 문서종류
        DataObject recevDoc = taskProcessor.getRecevDocCd(
        	new DataObject()
            	.set("taskSeCd", taskSeCd)
            	.set("taskSeNm", vltnInfo.get("TASK_SE_NM"))
            	.set("infoSndngSeNm", infoSndngSeNm)
            	.set("vltn", vltnInfo.get("VLTN_ARTCL"))
            	.set("taxItem", sndngInfo.get("TXITM_NM"))
        );
        String recevDocCd1 = recevDoc.string("docCd1");
        String recevDocCd2 = recevDoc.string("docCd2");
        String recevDocCd3 = recevDoc.string("docCd3");
        String recevDocCd4 = recevDoc.string("docCd4");
        String recevDocCd5 = recevDoc.string("docCd5");

        // 과목명
        String recevSubj1 = "과태료";
        String recevSubj2 = "01,02".contains(sndngSeCd) ? "" : "가산금"; // 계고, 사전통지서
        String recevSubj3 = deptSggInfo.string("SGG_NM");

        // 9. 전자우편 접수 상세 조회
        for (DataObject sndngDtlInfo: sndngDtlMapper.selectSndngDtlList(sndbQuery)) {
//        for (DataObject sndngTrgt: sndngDtlMapper.selectSndngTrgtDtlIds(sndbQuery)) {
            // 발송 상세 및 단속, 납부자, 부과 정보 조회
//            DataObject sndngDtlInfo = sndngDtlMapper.selectSndngDtlInfo(new SndbQuery().setSndngDtlId(sndngTrgt.string("SNDNG_DTL_ID")));

            // 가상계좌취득 자료가 아니면 다음 자료로..
            if (!"02".equals(sndngDtlInfo.string("SNDNG_DTL_STTS_CD"))) {
//                continue;
                throw new RuntimeException("우편통합 발송 작업중 발송상세 상태코드가 가상계좌취득이 아닌 자료를 발견되었습니다.");
            }

            String dtlSndngSeCd = sndngDtlInfo.string("SNDNG_SE_CD");
            String becevBillNumber = "01,02".contains(dtlSndngSeCd) ? "" : "고지번호:" + sndngDtlInfo.string("FYR") + "-" + sndngDtlInfo.string("LEVY_NO");; // 고지번호
            String rgstNmbr = ""; // 등기번호
            if (postSndngSeCd.equals("1")) {
                rgstNmbr = epostRcptRegBean.getEpostRgNo(sggCd, taskSeCd);
            } else {
                seqNo++;
                rgstNmbr = sndngInfo.string("TODAY") + String.format("%05d", seqNo + 1);
            }

            int pcptax = 0;         // 본세
            int adamt = 0;          // 가산금
            int lastAmt = 0;        // 최종 금액
            int dudtAftrPcptax = 0; // 납기후 본세
            int dudtAftrAdamt = 0;  // 납기후 가산금
            int dudtAftrAmt = 0;    // 납기후 합계 금액

            // 계고장에는 최초 과태료 금액을 출력한다.
            if ("11,12".contains(dtlSndngSeCd)) {
                pcptax = toInt(sndngDtlInfo.get("FFNLG_AMT"));  // 본세 = 과태료 단속 금액
                adamt = 0;                                      // 가산금 = 0
                lastAmt = toInt(sndngDtlInfo.get("FFNLG_AMT")); // 합계 금액 = 과태료 단속 금액
                dudtAftrPcptax = 0;                             // 납기후 본세
                dudtAftrAdamt = 0;                              // 납기후 가산금
                dudtAftrAmt = 0;                                // 납기후 합계 금액
            } else {
                pcptax = toInt(sndngDtlInfo.get("PCPTAX"));    // 본세
                adamt = toInt(sndngDtlInfo.get("ADAMT"));      // 가산금
                lastAmt = toInt(sndngDtlInfo.get("LAST_AMT")); // 최종 금액

                if (sndngDtlInfo.string("SGG_CD").equals("50110")) {
                    dudtAftrPcptax = 0; // 납기후 본세
                    dudtAftrAdamt = 0;  // 납기후 가산금
                    dudtAftrAmt = 0;    // 납기후 합계 금액
                } else {
                    dudtAftrAmt = toInt(sndngDtlInfo.get("DUDT_AFTR_AMT"));  // 납기후 금액
                    if (dudtAftrAmt == 0) {
                        dudtAftrPcptax = 0; // 납기후 본세
                        dudtAftrAdamt = 0;  // 납기후 가산금
                        dudtAftrAmt = 0;    // 납기후 금액
                    } else {
                        dudtAftrPcptax = toInt(sndngDtlInfo.get("PCPTAX"));  // 납기후 본세
                        if (dudtAftrAmt > dudtAftrPcptax) {                         // 납기후 금액이 본세보다 크다면..
                            dudtAftrAdamt = toInt(sndngDtlInfo.get("DUDT_AFTR_AMT")) - toInt(sndngDtlInfo.get("PCPTAX"));
                        } else if (dudtAftrAmt < dudtAftrPcptax) {                  // 납기후 금액이 본세보다 작다면 오류..
                            throw new RuntimeException("우편통합 발송 작업중 납기후 금액이 본세보다 작은 자료가 발견되었습니다.");
                        } else {
                            dudtAftrAdamt = 0;
                        }
                    }
                }
            }

            // OCR Band
            String ocr0 = "";
            String ocr1 = "";
            String ocr2 = "";
            if ("11,12".contains(dtlSndngSeCd)) {         // 계고장
                ocr0 = "";
                ocr1 = "";
                ocr2 = "";
            } else if (seoul) {
                ocr0 = sndngDtlInfo.string("TAX_NO");
                ocr1 = "";
                ocr2 = "";
            } else {
                // TAX_NO = 부서코드(7) + 특별회계사업코드(4) + 회계연도(4) + 회계구분(2) + 세목코드(6) + 고지번호(6) + 분납순번(2)
                ocr0 = sndngDtlInfo.string("TAX_NO") + CmmnUtil.getOcrVrfcCode("12357", sndngDtlInfo.string("TAX_NO"), 31);
                ocr1 = CmmnUtil.getOcrband2(ocr0, sndngDtlInfo.string("LEVY_SE_CD"), lastAmt, sndngDtlInfo.string("DUDT_YMD"));
                ocr2 = CmmnUtil.getOcrband3(ocr1, lastAmt, sndngDtlInfo.string("DUDT_YMD"), dudtAftrAmt);
            }

            // 전자우편 접수 상세 /////////////////////////////////////////////////////
            EpostRcptDtl epostRcptDtl = new EpostRcptDtl();
            epostRcptDtl.setConKey(conKey);                                          // CON_KEY
            epostRcptDtl.setRgstNmbr(rgstNmbr);                                      // 등기번호
            epostRcptDtl.setRecevSeq(sndngDtlInfo.string("CRDN_ID"));                // 일련번호
            epostRcptDtl.setRecevCode(sndngDtlInfo.string("SNDNG_DTL_ID"));          // 발송코드
            epostRcptDtl.setRecevBar1d(deptSggInfo.string("SGG_LEADER"));            // 바코드
            epostRcptDtl.setRecevBar2d("");                                          // 이차원바코드
            epostRcptDtl.setRecevBillNumber(becevBillNumber);                        // 고지번호
            epostRcptDtl.setRecevDocCd1(recevDocCd1);                                // 문서종류1
            epostRcptDtl.setRecevDocCd2(recevDocCd2);                                // 문서종류2
            epostRcptDtl.setRecevDocCd3(recevDocCd3);                                // 문서종류3
            epostRcptDtl.setRecevDocCd4(recevDocCd4);                                // 문서종류4
            epostRcptDtl.setRecevDocCd5(recevDocCd5);                                // 문서종류5
            epostRcptDtl.setRecevSubj1(recevSubj1);                                  // 과목명1
            epostRcptDtl.setRecevSubj2(recevSubj2);                                  // 과목명2
            epostRcptDtl.setRecevSubj3(recevSubj3);                                  // 과목명3
            epostRcptDtl.setRecevViolateDt(sndngDtlInfo.string("CRDN_YMD_TM_MASK")); // 위반일시
            epostRcptDtl.setRecevViolateDong(sndngDtlInfo.string("CRDN_STDG_NM"));   // 위반동

            String crdnPlc = sndngDtlInfo.string("CRDN_PLC"); // 위반장소 - 80
            if (crdnPlc.length() > 80) {
                crdnPlc = sndngDtlInfo.string("CRDN_PLC").substring(0, 80);
                epostRcptDtl.setRecevViolatePlace(crdnPlc);
            } else {
                epostRcptDtl.setRecevViolatePlace(crdnPlc);
            }
            // 법조항
            epostRcptDtl.setRecevLaws(vltnInfo.string("VLTN_LAW_NM") + " " + vltnInfo.string("VLTN_LAW1") + " " + vltnInfo.string("VLTN_LAW2"));
            // 납부번호
            if (isEmpty(ocr0)) {
                epostRcptDtl.setRecevPayNumber1("");                                                // 납부번호1
                epostRcptDtl.setRecevPayNumber2("");                                                // 납부번호2
            } else {
                epostRcptDtl.setRecevPayNumber1(sndngDtlInfo.string("TAX_NO").substring(0, 17));    // 납부번호1
                epostRcptDtl.setRecevPayNumber2(sndngDtlInfo.string("TAX_NO").substring(17));       // 납부번호2
            }
            epostRcptDtl.setRecevOcr0(ocr0);
            epostRcptDtl.setRecevOcr1(ocr1);
            epostRcptDtl.setRecevOcr2(ocr2);
            epostRcptDtl.setRecevOnlinePayNumber(sndngDtlInfo.string("EPAYNO")); // 전자 납부번호

            // 가상계좌
			epostRcptDtl.setVractno(seoul ? sndngDtlInfo.string("VR_ACTNO") : sndngDtlInfo.string("BANK_NM") + " " + sndngDtlInfo.string("VR_ACTNO"));
			epostRcptDtl.setVractno2(seoul ? sndngDtlInfo.string("VR_ACTNO2") : sndngDtlInfo.string("BANK_NM2") + " " + sndngDtlInfo.string("VR_ACTNO2"));
			epostRcptDtl.setVractno3(seoul ? sndngDtlInfo.string("VR_ACTNO3") : sndngDtlInfo.string("BANK_NM3") + " " + sndngDtlInfo.string("VR_ACTNO3"));
			epostRcptDtl.setVractno4(seoul ? sndngDtlInfo.string("VR_ACTNO4") : sndngDtlInfo.string("BANK_NM4") + " " + sndngDtlInfo.string("VR_ACTNO4"));
			epostRcptDtl.setVractno5(seoul ? sndngDtlInfo.string("VR_ACTNO5") : sndngDtlInfo.string("BANK_NM5") + " " + sndngDtlInfo.string("VR_ACTNO5"));
			epostRcptDtl.setVractno6(seoul ? sndngDtlInfo.string("VR_ACTNO6") : sndngDtlInfo.string("BANK_NM6") + " " + sndngDtlInfo.string("VR_ACTNO6"));
			epostRcptDtl.setVractno7(seoul ? sndngDtlInfo.string("VR_ACTNO7") : sndngDtlInfo.string("BANK_NM7") + " " + sndngDtlInfo.string("VR_ACTNO7"));
			epostRcptDtl.setVractno8(seoul ? sndngDtlInfo.string("VR_ACTNO8") : sndngDtlInfo.string("BANK_NM8") + " " + sndngDtlInfo.string("VR_ACTNO8"));
			epostRcptDtl.setVractno9(seoul ? sndngDtlInfo.string("VR_ACTNO9") : sndngDtlInfo.string("BANK_NM9") + " " + sndngDtlInfo.string("VR_ACTNO9"));
			epostRcptDtl.setVractno10(seoul ? sndngDtlInfo.string("VR_ACTNO10") : sndngDtlInfo.string("BANK_NM10") + " " + sndngDtlInfo.string("VR_ACTNO10"));
			epostRcptDtl.setVractno11(seoul ? sndngDtlInfo.string("VR_ACTNO11") : sndngDtlInfo.string("BANK_NM11") + " " + sndngDtlInfo.string("VR_ACTNO11"));

            epostRcptDtl.setRecevFinePrice(sndngDtlInfo.string("FFNLG_AMT"));           // 과태료금액
            epostRcptDtl.setRecevPayPrice(String.valueOf(lastAmt));                     // 납부금액
            epostRcptDtl.setRecevPayDt(sndngDtlInfo.string("DUDT_YMD_MASK_KOR"));       // 납부기한
            epostRcptDtl.setRecevPayInDate(sndngDtlInfo.string("DUDT_YMD_MASK"));       // 납기내기한
            epostRcptDtl.setRecevPayInPrice(String.valueOf(pcptax));                    // 납기내본세
            epostRcptDtl.setRecevPayInAddPrice(String.valueOf(adamt));                  // 납기내가산금
            epostRcptDtl.setRecevPayInDefaultPrice(String.valueOf(lastAmt));            // 납기내금액
            epostRcptDtl.setRecevPayOutDate(sndngDtlInfo.string("DUDT_AFTR_YMD_MASK")); // 납기후기한
            epostRcptDtl.setRecevPayOutPrice(String.valueOf(dudtAftrPcptax));           // 납기후본세
            epostRcptDtl.setRecevPayOutAddPrice(String.valueOf(dudtAftrAdamt));         // 납기후가산금
            epostRcptDtl.setRecevPayOutDefaultPrice(String.valueOf(dudtAftrAmt));       // 납기후금액
            epostRcptDtl.setRecevCarNumber(sndngDtlInfo.string("VHRNO"));               // 차량번호
            epostRcptDtl.setRecevCarOwnerZipcode(sndngDtlInfo.string("ZIP"));           // 우편번호
            epostRcptDtl.setRecevCarOwnerAddr(sndngDtlInfo.string("ADDR"));             // 주소(시, 구)
            epostRcptDtl.setRecevCarOwnerDetailaddr(sndngDtlInfo.string("DTL_ADDR"));   // 상세주소
            epostRcptDtl.setRecevCarOwnerNm(sndngDtlInfo.string("RTPYR_NM"));           // 소유자명
            epostRcptDtl.setRecevCarOwnerSsn(sndngDtlInfo.string("RTPYR_BRDT"));        // 주민번호(납부자 생년월일)
            epostRcptDtl.setRecevEvidenceNumber(sndngDtlInfo.string("SNDNG_DTL_ID"));   // 증거번호
            epostRcptDtl.setRecevPhotoCnt("0");      // 사진장수
            epostRcptDtl.setRecevPhotoFilename1(""); // 사진파일명1
            epostRcptDtl.setRecevPhotoFilename2(""); // 사진파일명2
            epostRcptDtl.setRecevPhotoFilename3(""); // 사진파일명3
            epostRcptDtl.setRecevPhotoFilename4(""); // 사진파일명4

            // 계고장(11,12), 사전통지서(01,02)에는 사진을 출력한다.
            if ("01,02,11,12".contains(dtlSndngSeCd)) {
                // 단속 첨부 파일 조회
                List<FileInfo> fileInfo = fileBean.getFilesOf(Crdn.INF_TYPE, sndngDtlInfo.string("CRDN_ID"));
                int fileCount = fileInfo.size();
				if (fileCount > 0) {
                    epostRcptDtl.setRecevPhotoCnt(String.valueOf(fileCount)); // 사진장수

                    List<Consumer<String>> setPhotoFilename = List.of(
                    	epostRcptDtl::setRecevPhotoFilename1,
                    	epostRcptDtl::setRecevPhotoFilename2,
                    	epostRcptDtl::setRecevPhotoFilename3,
                    	epostRcptDtl::setRecevPhotoFilename4
                    );
                    // 사진파일명이 중복이 되면 안되다.. 중복을 피하기 위해 서버에 저장된 파일명으로 지정하였다.
                    for (int i = 0; i < fileCount; i++) {
                        // 서버 OS에 따라.. separator 지정.
                        String strSeparator = deptSggInfo.string("SRVR_OS").contains("Windows") ? "\\" : "/";
                        // 파일 명
                        String fileName = fileInfo.get(i).getPath();
                        fileName = fileName.substring(fileName.lastIndexOf(strSeparator) + 1);
                        // 파일이 저장되는 경로 확인. file-job.conf 에서 확인. epost 전자우편 신청 전송 "attachmentDir"
                        File fPath = ensureDir(new File("").getAbsolutePath() + "/files/interface/attachment/epost/");

                        try (
                            FileInputStream fInputStream = (FileInputStream) fileInfo.get(i).getInputStream();
                            FileOutputStream fOutputStream = new FileOutputStream(fPath + File.separator + fileName);
                        ) {
                            byte[] buffer = new byte[1024];
                            int length;

                            while ((length = fInputStream.read(buffer)) > 0) {
                                fOutputStream.write(buffer, 0, length);
                            }

                            if (fileCount < 4)
                            	setPhotoFilename.get(i).accept(fileName);
                        } catch (IOException e) {
                            throw new RuntimeException("우편통합 발송 작업중 전자우편 단속 사진 등록에 실패하였습니다.");
                        }
                    }
                }
            }

            // 10. 전자우편 접수 상세
            success = epostRcptDtlBean.create(epostRcptDtl);
            if (!success)
                throw new RuntimeException("우편통합 발송 작업중 전자우편 상세 등록에 실패하였습니다."); // 예외를 발생시켜서 DB Rollback

            // 11. 발송상세 내역을 수정한다.
            SndngDtl sndngDtl = new SndngDtl();
            sndngDtl.setConKey(conKey);                                         // 외부연계식별키
            sndngDtl.setRgNo(rgstNmbr);                                         // 등기 번호
            sndngDtl.setSndngDtlSttsCd("03");                                   // 발송 상세 상태 코드(FIM050) - 03 발송
            sndngDtl.setSndngDtlId(sndngDtlInfo.string("SNDNG_DTL_ID"));

            affected = sndngDtlMapper.updateSndngDtlEPost(sndngDtl);             // 전자우편 발송상세 내역 수정
            if (affected != 1)
                throw new RuntimeException("우편통합 발송 작업중 전자우편 상세 등록에 실패하였습니다.");
        }

        return "[S] 작업이 정상 처리 되었습니다.";
    }
}