package incheon.ags.dtm.service.impl;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.util.Base64;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.DataFormat;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;

import incheon.ags.dtm.mapper.DtmMapper;
import incheon.ags.dtm.service.DtmService;
import incheon.ags.dtm.util.DtmZipUtil;
import incheon.ags.dtm.vo.DmapDownloadVO;
import incheon.ags.dtm.vo.DtmAddressVO;
import incheon.ags.dtm.vo.DtmApplyItemVO;
import incheon.ags.dtm.vo.DtmApplyListVO;
import incheon.ags.dtm.vo.DtmApplySearchVO;
import incheon.ags.dtm.vo.DtmApplyVO;
import incheon.ags.dtm.vo.DtmApproveVO;
import incheon.ags.dtm.vo.DtmComboVO;
import incheon.ags.dtm.vo.DtmDmapDtVO;
import incheon.ags.dtm.vo.DtmDmapHistDtVO;
import incheon.ags.mrb.analysis.web.SpatialAnalysisController;
import incheon.cmm.g2f.layer.vo.TaskLayerSearchRequestDTO;
import incheon.cmm.g2f.layer.vo.TaskLayerVO;
import incheon.com.security.vo.LoginVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service("dtmService")
@RequiredArgsConstructor
public class DtmServiceImpl extends EgovAbstractServiceImpl implements DtmService {

	private static final Logger logger = LoggerFactory.getLogger(SpatialAnalysisController.class);
	private final DtmMapper dtmMapper;

	// OS별 절대 경로 주입
	@Value("${dtm.upload.path.win}")
	private String winFilePath;
	@Value("${dtm.resource.path.linux}")
	private String linuxFilePath;
	
	private String BASE_PATH; //dtm폴더
	private String DOWNLOAD_PATH; //dtm/download
	private String DTM_PATH;	//dtm/
	private String CERT_PATH;
	private String REQ_PATH;

	@PostConstruct
	private void initPaths() {
		this.BASE_PATH = getOsDependentBasePath();
		this.DTM_PATH = this.BASE_PATH + "/digitalmap";
		this.CERT_PATH = this.BASE_PATH + "/certfile";
		this.REQ_PATH = this.BASE_PATH + "/reqfile";
		this.DOWNLOAD_PATH = this.BASE_PATH + "/download";
	}
	
	
	@Override
	public List<TaskLayerVO> getLayerList(TaskLayerSearchRequestDTO searchVO) throws Exception {
		return dtmMapper.getLayerList(searchVO);
	}

	@Override
	public List<DtmDmapDtVO> selectDmapDtList(DtmDmapDtVO dtmDmapDtVO) throws Exception {
		return dtmMapper.selectDmapDtList(dtmDmapDtVO);
	}

	@Override
	public List<DtmDmapDtVO> selectDmapDtListBySig(DtmDmapDtVO dtmDmapDtVO) throws Exception {
		return dtmMapper.selectDmapDtListBySig(dtmDmapDtVO);
	}

	@Override
	public List<DtmDmapDtVO> selectDmapDtListByEmd(DtmDmapDtVO dtmDmapDtVO) throws Exception {
		return dtmMapper.selectDmapDtListByEmd(dtmDmapDtVO);
	}

	

	@Override
	public List<DtmDmapHistDtVO> searchDmapDtDetail(DtmDmapDtVO dtmDmapDtVO) throws Exception {
		List<DtmDmapHistDtVO> list = dtmMapper.searchDmapDtDetail(dtmDmapDtVO);
		if (list == null || list.isEmpty()) {
			return list;
		}

		DtmDmapHistDtVO first = list.get(0);
		String filePath = first.getFlpth().replace("\\", "/");

		Path path = Paths.get(DTM_PATH,filePath, first.getFileNm()).normalize();
		log.info("[searchDmapDtDetail] actual path={}", path.toAbsolutePath());
		
		boolean imageFound = false;
		
		// 클라이언트용 Base64 PNG 처리
		if (Files.exists(path)) {
			try (ZipFile zipFile = new ZipFile(path.toFile())) {
				// ZIP 안 PNG 파일 첫 번째만 추출
				ZipEntry pngEntry = zipFile.stream().filter(e -> e.getName().endsWith(".png")).findFirst().orElse(null);
				if (pngEntry != null) {
					try (InputStream is = zipFile.getInputStream(pngEntry)) {
						byte[] pngBytes = is.readAllBytes();
						String base64 = Base64.getEncoder().encodeToString(pngBytes);
						first.setImageData("data:image/png;base64," + base64);
						imageFound = true;
					}
				} else {
					first.setImageData(null);
				}
			} catch (Exception e) {
				log.error("ZIP에서 PNG 읽기 실패", e);
			}
		}
		
		
		if (!imageFound) {
			String zipName = first.getFileNm();
			String pngName = zipName.replaceAll("\\.[^.]+$", ".png");

			Path pngPath = Paths.get(DTM_PATH,filePath, pngName).normalize();
			log.info("[searchDmapDtDetail] png path={}", pngPath.toAbsolutePath());

			if (Files.exists(pngPath)) {
				try {
					byte[] pngBytes = Files.readAllBytes(pngPath);
					String base64 = Base64.getEncoder().encodeToString(pngBytes);
					first.setImageData("data:image/png;base64," + base64);
					imageFound = true;
				} catch (Exception e) {
					log.error("PNG 파일 읽기 실패", e);
				}
			}
		}
		
		if (!imageFound) {
			first.setImageData(null);
		}

		return list;
	}
	
	
	

	@Override
	public List<DtmDmapDtVO> selectApplyDmapDtList(DtmDmapDtVO dtmDmapDtVO) throws Exception {
		return dtmMapper.selectApplyDmapDtList(dtmDmapDtVO);
	}

	/**
	 * OS에 따라 저장 경로 결정
	 */
	private String getOsDependentBasePath() {
		String rawOsName = System.getProperty("os.name");
		String osName = (rawOsName != null) ? rawOsName.toLowerCase() : "linux";
		if (osName.contains("win")) {
			return this.winFilePath + "/ags/dtm";
		} else {
			return this.linuxFilePath;
		}
	}

	@Override
	public void downloadFile(HttpServletResponse response) throws Exception {
		// 파일명 생성
		String fileName = "수치지형도 자료제공 신청서식(신청서, 보안각서, 인수서 등).hwpx";

		// 파일 저장
		Path filePath = Paths.get(DOWNLOAD_PATH,fileName).normalize();
		log.info("downloadFile - filePath = {}", filePath.toString());

		File file = filePath.toFile();
		if (!file.exists()) {
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			response.setContentType("text/plain; charset=UTF-8");
			response.getWriter().write("요청하신 파일이 존재하지 않습니다.");
			return;
		}

		// 2️⃣ 응답 헤더 설정
		response.setContentType("application/octet-stream");
		response.setContentLengthLong(file.length());

		// 한글 파일명 대응
		String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20");

		response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName);

		// 3️⃣ 파일 스트림 복사
		try (InputStream in = new BufferedInputStream(new FileInputStream(file));
				OutputStream out = response.getOutputStream()) {

			FileCopyUtils.copy(in, out);
			out.flush();
		}
	}


	/* 사업명 */
	@Override
	public List<DtmComboVO> selectBizList() {
		return dtmMapper.selectBizList();
	}

	private Path createExcelFile(Path tempDir, String fileName, List<Map<String, Object>> data,
			Map<String, String> columnMap) throws IOException {
		if (data == null || data.isEmpty()) {
			throw new RuntimeException("Excel로 변환할 데이터가 없습니다.");
		}

		Path excelFile = tempDir.resolve(fileName + ".xlsx");

		try (XSSFWorkbook workbook = new XSSFWorkbook()) {
			XSSFSheet sheet = workbook.createSheet();

			// 0. 스타일
			DataFormat format = workbook.createDataFormat();

			CellStyle floatStyle = workbook.createCellStyle();
			floatStyle.setDataFormat(format.getFormat("#,##0.#"));

			CellStyle intStyle = workbook.createCellStyle();
			intStyle.setDataFormat(format.getFormat("#,##0"));

			CellStyle headerStyle = workbook.createCellStyle();
			headerStyle.setAlignment(HorizontalAlignment.CENTER);

			// 1. 컬럼 정리
			Set<String> dataKey = data.get(0).keySet();
			List<String> columnKeyList = new ArrayList<>(dataKey); // 컬럼맵이 없는 경우에 데이터의 모든 키를 헤더로 사용

			if (columnMap != null) {
				// 컬럼 키
				columnKeyList = new ArrayList<>(columnMap.keySet());

				// 존재하지 않는 컬럼 체크
				Set<String> availableColumns = dataKey;
				List<String> invalidColumns = columnKeyList.stream().filter(col -> !availableColumns.contains(col))
						.collect(Collectors.toList());

				if (!invalidColumns.isEmpty()) {
					throw new RuntimeException("존재하지 않는 컬럼: " + String.join(", ", invalidColumns));
				}
			}

			// 2. 헤더 생성
			XSSFRow headerRow = sheet.createRow(0);
			for (int i = 0; i < columnKeyList.size(); i++) {
				XSSFCell cell = headerRow.createCell(i);
				String columnName = columnMap != null ? columnMap.get(columnKeyList.get(i)) : columnKeyList.get(i);
				cell.setCellValue(columnName);
				cell.setCellStyle(headerStyle);
			}

			// 3. 데이터 작성
			for (int i = 0; i < data.size(); i++) {
				Map<String, Object> rowData = data.get(i);
				XSSFRow row = sheet.createRow(i + 1);
				for (int j = 0; j < columnKeyList.size(); j++) {
					XSSFCell cell = row.createCell(j);
					Object value = rowData.get(columnKeyList.get(j));

					if (value instanceof Number) {

						double num = ((Number) value).doubleValue();
						cell.setCellValue(num);

						if (num == Math.floor(num)) {
							cell.setCellStyle(intStyle);
						} else {
							cell.setCellStyle(floatStyle);
						}
					} else {
						cell.setCellValue(value != null ? value.toString() : "");
					}
				}
			}

			// 4. 열 너비 조정
			int maxWidth = 255 * 256; // 최대 폭
			for (int i = 0; i < columnKeyList.size(); i++) {
				String key = columnKeyList.get(i);

				int maxLength = columnMap != null ? columnMap.get(key).length() : key.length(); // 헤더 포함
				for (Map<String, Object> row : data) {
					Object val = row.get(key);
					if (val != null) {
						int len = val.toString().length();
						if (maxLength < len) {
							maxLength = len;
						}
					}
				}

				int width = (int) ((maxLength + 4) * 256 * 1.3);
				sheet.setColumnWidth(i, Math.min(width, maxWidth));
			}

			// 5. 파일 쓰기
			try (OutputStream os = Files.newOutputStream(excelFile)) {
				workbook.write(os);
			}
		}

		return excelFile;
	}

	private ResponseEntity<Resource> createDownloadResponse(Path filePath, String fileName) throws IOException {
		if (!Files.exists(filePath)) {
			throw new RuntimeException("다운로드할 파일이 존재하지 않습니다: " + filePath);
		}

		// 파일 크기 로그
		long fileSize = Files.size(filePath);
		Resource resource = new FileSystemResource(filePath);

		// 파일명 인코딩
		String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20");
		HttpHeaders headers = new HttpHeaders();
		headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFileName);
		headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
		headers.add(HttpHeaders.CACHE_CONTROL, "no-cache");

		return ResponseEntity.ok().headers(headers).contentLength(fileSize).body(resource);
	}

	private void cleanupDownloadFiles(Path tempDir) {
		try {
			Files.walk(tempDir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
		} catch (IOException e) {
			logger.error("시스템 오류가 발생했습니다: {}", "임시 파일 정리", e);
		}
	}

	//사용안하는것같음
	@Override
	public ResponseEntity<Resource> downLoadExcel(List<Map<String, Object>> data, Map<String, String> columnMap,
			String fileName) {
		Path tempDir = null;
		try {
			tempDir = Files.createTempDirectory("excel_");
			Path excelFilePath = createExcelFile(tempDir, fileName, data, columnMap);
			String downloadFileName = fileName + "_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date())
					+ ".xlsx";
			ResponseEntity<Resource> response = createDownloadResponse(excelFilePath, downloadFileName);

			return response;
		} catch (Exception e) {
			logger.error("시스템 오류가 발생했습니다: {}", "목록 엑셀 다운로드", e);

			if (tempDir != null) {
				try {
					cleanupDownloadFiles(tempDir);
				} catch (Exception cleanupEx) {
					logger.error("시스템 오류가 발생했습니다: {}", "엑셀 정리 실패", cleanupEx);
				}
			}

			throw new RuntimeException("다운로드 실패: " + e.getMessage(), e);
		}

	}

	@Override
	public List<DtmAddressVO> getAddrCodeByPoi(DtmAddressVO filterVO) {
		return dtmMapper.selectAddrCodeByPoint(filterVO);
	}

	/*
	 * ===================================================== 
	 * 수치지형도 다운로드 신청
	 * =====================================================
	 */
	/**
	 * 수치지형도 신청 등록
	 */
	@Override
	@Transactional(rollbackFor = Exception.class)
	public void insertApply(String useprps, String rlsYn, String fileTypeNm, List<DtmApplyItemVO> items,
			List<MultipartFile> files, HttpSession session) throws Exception {

		/* 0. 기본 검증 */
		if (items == null || items.isEmpty()) {
			throw new IllegalArgumentException("신청 도엽 목록이 없습니다.");
		}

		if (files == null || files.isEmpty()) {
			throw new IllegalArgumentException("신청서 파일이 없습니다.");
		}

		log.info("[insertApply] START");
		log.info("[insertApply] useprps={}, rlsYn={}, fileTypeNm={}", useprps, rlsYn, fileTypeNm);
		log.info("[insertApply] items.size={}", items.size());
		
		LoginVO loginVO = (LoginVO) session.getAttribute("loginVO");
		/* 1. req_sn 생성 (MAX + 1) */
		Long aplySn = dtmMapper.selectNextAplySn();
		log.info("[insertApply] generated aplySn={}", aplySn);

		/* 2. 마스터 저장 */
		DtmApplyVO master = new DtmApplyVO();
		master.setAplySn(aplySn);
		master.setUseprps(useprps);
		master.setRlsYn(rlsYn);
		master.setRtrcnYn("N");
		master.setFileTypeNm(fileTypeNm);
		master.setUserId(loginVO.getUserUnqId());

		log.info("[insertApply] insertApplyMaster aplySn={}", master.getAplySn());
		dtmMapper.insertApplyMaster(master);

		/* 3. 도엽 리스트 저장 */
		int idx = 0;
		for (DtmApplyItemVO item : items) {
			log.info("[insertApply][ITEM BEFORE] idx={}, mapSn={}, filePath={}", idx, item.getMapSn(),
					item.getFlpth());

			item.setAplySn(aplySn);
			item.setUserId(loginVO.getUserUnqId());

			log.info("[insertApply][ITEM INSERT] idx={}, aplySn={}, mapSn={}, filePath={}, userId={}", idx,
					item.getAplySn(), item.getMapSn(), item.getFlpth(), item.getUserId());

			dtmMapper.insertApplyItem(item);
			idx++;
		}

		/* 4. 신청서 파일 업로드 */
		int year = LocalDate.now().getYear();
		Path uploadDir = Paths.get(REQ_PATH, String.valueOf(year), String.valueOf(aplySn));
		Files.createDirectories(uploadDir);
		for (MultipartFile file : files) {
			if (file == null || file.isEmpty()) continue;
			String originalName = Paths.get(file.getOriginalFilename()).getFileName().toString();
			Path savePath = uploadDir.resolve(originalName);
			file.transferTo(savePath.toFile());
			log.info("[insertApply] files upload complete path={}", savePath);
	
			/* 5. 신청서 파일을 도엽 리스트(map_sn=0)로 저장 */
			DtmApplyItemVO fileItem = new DtmApplyItemVO();
			fileItem.setAplySn(aplySn);
			fileItem.setMapSn(0L);
			fileItem.setFlpth(savePath.toString());
			fileItem.setUserId(loginVO.getUserUnqId());
	
			log.info("[insertApply][FILE ITEM INSERT] aplySn={}, mapSn={}, filePath={}, userId={}", fileItem.getAplySn(),
					fileItem.getMapSn(), fileItem.getFlpth(), fileItem.getUserId());
	
			dtmMapper.insertApplyItem(fileItem);
		}

		log.info("[insertApply] END aplySn={}", aplySn);
	}

	@Override
	public List<DtmApplyListVO> selectApplyList(DtmApplySearchVO vo, HttpSession session) {
		LoginVO loginVO = (LoginVO) session.getAttribute("loginVO");
		vo.setUserId(loginVO.getUserUnqId());
		if(vo.isAdmin()) {
			return dtmMapper.selectApplyAdminList(vo);
		}
		else {
			return dtmMapper.selectApplyList(vo);
		}
	}

	private static final Logger log = LoggerFactory.getLogger(DtmServiceImpl.class);

	// 관리자 인수증 업로드
	@Transactional(rollbackFor = Exception.class)
	@Override
	public void uploadCertFile(MultipartFile file, String aplySn, String reqYear) throws Exception {

		log.info("=== uploadCertFile START ===");
		log.info("aplySn={}, reqYear={}", aplySn, reqYear);

		// 1. 기본 검증
		if (file == null || file.isEmpty()) {
			log.error("파일 없음");
			throw new IllegalStateException("파일이 존재하지 않습니다.");
		}

		String originalName = file.getOriginalFilename();
		log.info("upload file name={}", originalName);

		// 2. 확장자 검증 (필요 시 조정)
		String ext = originalName.substring(originalName.lastIndexOf('.') + 1).toLowerCase();
		if (!List.of("pdf", "pptx", "xlsx", "hwp", "hwpx").contains(ext)) {
			throw new IllegalArgumentException("허용되지 않은 파일 형식입니다.");
		}

		// 3. 저장 경로 생성
		Path dirPath = Paths.get(CERT_PATH, reqYear, aplySn);
		File dir = dirPath.toFile();

		if (!dir.exists()) {
			boolean mk = dir.mkdirs();
			log.info("dir create={}", mk);
		}

		// 4. 파일 저장
		File target = dirPath.resolve(originalName).toFile();
		file.transferTo(target);
		log.info("file saved: {}", target.getAbsolutePath());

		String fileFath = target.getAbsolutePath();

		// 5. DB 존재 여부 확인
		int cnt = dtmMapper.checkCertExists(aplySn);
		log.info("checkCertExists cnt={}", cnt);

		if (cnt == 0) {
			log.info("insertCertFile 실행");
			dtmMapper.insertCertFile(aplySn, fileFath);
		} else {
			log.info("updateCertFile 실행");
			dtmMapper.updateCertFile(aplySn, fileFath);
		}

		log.info("=== uploadCertFile END ===");
	}


	@Transactional(rollbackFor = Exception.class)
	@Override
	public void approveDigitalMap(DtmApproveVO vo, String basePath ,HttpSession session) throws Exception {
		LoginVO loginVO = (LoginVO) session.getAttribute("loginVO");
		vo.setUserId(loginVO.getUserUnqId());

		String aplySn = vo.getAplySn();
		log.info("=== approveDigitalMap START ===");
		log.info("[PARAM] aplySn={}, basePath={}", aplySn, basePath);

		/* 1. 공개 여부 확인 */
		String rlsYn = dtmMapper.selectRlsYn(aplySn);
		log.info("[STEP1] rlsYn 조회 완료. aplySn={}, rlsYn={}", aplySn, rlsYn);

		/* 2. 비공개일 경우 인수증 필수 */
		if (!"Y".equals(rlsYn)) {
			log.info("[STEP2] 비공개 건 → 인수증 존재 여부 확인 시작. aplySn={}", aplySn);

			int certCnt = dtmMapper.checkCertExists(aplySn);
			log.info("[STEP2] 인수증 조회 결과. aplySn={}, certCnt={}", aplySn, certCnt);

			if (certCnt < 1) {
				log.error("[ERROR] 인수증 미존재. 승인 불가. aplySn={}", aplySn);
				throw new IllegalStateException("인수증이 업로드되지 않았습니다.");
			}
		} else {
			log.info("[STEP2] 공개 건 → 인수증 체크 생략. aplySn={}", aplySn);
		}

		/* 3. DB 파일 목록 조회 */
		log.info("[STEP3] ZIP 대상 파일 목록 조회 시작. aplySn={}", aplySn);

		List<String> fileNames = dtmMapper.selectMapFiles(aplySn);
		log.info("[STEP3] 조회된 파일 수. aplySn={}, count={}", aplySn, fileNames == null ? 0 : fileNames.size());

		if (fileNames == null || fileNames.isEmpty()) {
			log.error("[ERROR] ZIP 대상 파일 없음. aplySn={}", aplySn);
			throw new IllegalStateException("압축할 파일이 없습니다.");
		}

		/* 4. ZIP 대상 File 객체 생성 */
		List<File> zipFiles = new ArrayList<>();
		log.info("[STEP4] File 객체 생성 시작. basePath={}", BASE_PATH);

		for (String fileName : fileNames) {
			fileName = fileName.replace("\\", File.separator);
			File file = new File(DTM_PATH, fileName);

			if (file.exists() && file.isFile()) {
				zipFiles.add(file);
				log.debug("[STEP4] 파일 추가됨: {}", file.getAbsolutePath());
			} else {
				log.warn("[STEP4] 파일 존재하지 않음 또는 파일 아님: {}", file.getAbsolutePath());
			}
		}

		log.info("[STEP4] 최종 ZIP 대상 파일 수: {}", zipFiles.size());

		if (zipFiles.isEmpty()) {
			log.error("[ERROR] 유효한 ZIP 대상 파일 없음. aplySn={}", aplySn);
			throw new IllegalStateException("유효한 파일이 존재하지 않습니다.");
		}

		/* 5. ZIP 생성 */
		// String zipDir = basePath + File.separator + "download";
		String zipPath = DOWNLOAD_PATH + File.separator + aplySn + ".zip";
		Path zipDownPath = Paths.get(DOWNLOAD_PATH, aplySn + ".zip"); 

		log.info("[STEP5] ZIP 생성 시작. zipPath={}", zipPath.toString());

		DtmZipUtil.create(zipDownPath.toString(), zipFiles);

		log.info("[STEP5] ZIP 생성 완료. zipPath={}, fileCount={}", zipPath, zipFiles.size());

		/* 6. 승인 처리 */
		log.info("[STEP6] 승인 DB 업데이트 시작. userId={}, aplySn={}, zipPath={}", vo.getUserId(), aplySn, zipPath);

		dtmMapper.updateApprove(vo.getUserId(), zipPath, aplySn);

		log.info("[STEP6] 승인 DB 업데이트 완료. aplySn={}", aplySn);

		/* 7. 대장 자동 등록 */
		log.info("[STEP7] 디지털맵 대장 등록 시작. aplySn={}", aplySn);

		dtmMapper.insertDigitRegister(aplySn);

		log.info("[STEP7] 디지털맵 대장 등록 완료. aplySn={}", aplySn);

		log.info("=== approveDigitalMap SUCCESS === aplySn={}", aplySn);
	}

	@Override
	@Transactional
	public void applyCancel(DtmApproveVO vo ,HttpSession session) {
		LoginVO loginVO = (LoginVO) session.getAttribute("loginVO");
		vo.setUserId(loginVO.getUserUnqId());
		int cnt = dtmMapper.applyCancel(vo);

		if (cnt <= 0) {
			throw new RuntimeException("반려 처리에 실패했습니다.");
		}
	}

	@Override
	public DmapDownloadVO selectDownloadInfo(long aplySn) {
		return dtmMapper.selectDownloadInfo(aplySn);
	}

	@Override
	public File createRequestZip(long aplySn, String reqYear) throws Exception {
		Path reqFolderPath = Paths.get(REQ_PATH, reqYear, String.valueOf(aplySn)).normalize(); 
		Path downPath = Paths.get(REQ_PATH).normalize(); 

		File reqFolder = reqFolderPath.toFile();
		if (!reqFolder.exists()) {
			throw new RuntimeException("신청서 폴더가 존재하지 않습니다.");
		}

		List<String> filenames = new ArrayList<>();
		List<String> filepathes = new ArrayList<>();

		for (File f : reqFolder.listFiles()) {
			if (f.isFile()) {
				filenames.add(f.getName());
				filepathes.add(f.getAbsolutePath());
			}
		}

		if (filenames.isEmpty()) {
			throw new RuntimeException("신청서 파일이 없습니다.");
		}

		// 기존 ZIP 정리
		File downDir = downPath.toFile(); 
		for (File f : downDir.listFiles()) {
			if (f.isFile() && f.getName().startsWith("REQ_")) {
				f.delete();
			}
		}

		Path zipFilePath = downPath.resolve("REQ_" + aplySn + ".zip").normalize();

		DtmZipUtil.create(zipFilePath.toString(), filenames, filepathes); 

		return zipFilePath.toFile(); 
	}

}