package com.kamco.cd.training.common.utils;

import static java.lang.String.CASE_INSENSITIVE_ORDER;

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.config.api.ApiResponseDto.ApiResponseCode;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.gce.geotiff.GeoTiffReader;
import org.springframework.http.HttpStatus;
import org.springframework.util.FileSystemUtils;
import org.springframework.web.multipart.MultipartFile;

@Slf4j
public class FIleChecker {

  static SimpleDateFormat dttmFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  public static boolean isValidFile(String pathStr) {

    Path path = Paths.get(pathStr);

    if (!Files.exists(path)) {
      return false;
    }

    if (!Files.isRegularFile(path)) {
      return false;
    }

    if (!Files.isReadable(path)) {
      return false;
    }

    try {
      if (Files.size(path) <= 0) {
        return false;
      }
    } catch (IOException e) {
      return false;
    }

    return true;
  }

  public static boolean verifyFileIntegrity(Path path, String expectedHash)
      throws IOException, NoSuchAlgorithmException {

    // 1. 알고리즘 선택 (SHA-256 권장, MD5는 보안상 비추천)
    MessageDigest digest = MessageDigest.getInstance("SHA-256");

    try (InputStream fis = Files.newInputStream(path)) {
      byte[] buffer = new byte[8192]; // 8KB 버퍼
      int bytesRead;
      while ((bytesRead = fis.read(buffer)) != -1) {
        digest.update(buffer, 0, bytesRead);
      }
    }

    // 3. 계산된 바이트 배열을 16진수 문자열로 변환
    StringBuilder sb = new StringBuilder();
    for (byte b : digest.digest()) {
      sb.append(String.format("%02x", b));
    }
    String actualHash = sb.toString();

    return actualHash.equalsIgnoreCase(expectedHash);
  }

  public static boolean checkTfw(String filePath) {

    File file = new File(filePath);

    if (!file.exists()) {
      return false;
    }

    // 1. 파일의 모든 라인을 읽어옴
    List<Double> lines = new ArrayList<>();
    try (BufferedReader br = new BufferedReader(new FileReader(file))) {
      String line;
      while ((line = br.readLine()) != null) {
        if (!line.trim().isEmpty()) { // 빈 줄 제외
          lines.add(Double.parseDouble(line.trim()));
        }
      }
    } catch (IOException ignored) {
      return false;
    }

    // 2. 6줄이 맞는지 확인
    if (lines.size() < 6) {
      // System.out.println("유효하지 않은 TFW 파일입니다. (데이터 부족)");
      return false;
    }

    return true;
  }

  public static boolean checkGeoTiff(String filePath) {

    File file = new File(filePath);

    if (!file.exists()) {
      return false;
    }

    GeoTiffReader reader = null;
    try {
      // 1. 파일 포맷 및 헤더 확인
      reader = new GeoTiffReader(file);

      // 2. 실제 데이터 로딩 (여기서 파일 깨짐 여부 확인됨)
      // null을 넣으면 전체 영역을 읽지 않고 메타데이터 위주로 체크하여 빠름
      GridCoverage2D coverage = reader.read(null);

      if (coverage == null) return false;

      // 3. GIS 필수 정보(좌표계)가 있는지 확인
      // if (coverage.getCoordinateReferenceSystem() == null) {
      // GeoTIFF가 아니라 일반 TIFF일 수도 있음(이미지는 정상이지만, 좌표계(CRS) 정보가 없습니다.)
      // }

      return true;

    } catch (Exception e) {
      System.err.println("손상된 TIF 파일입니다: " + e.getMessage());
      return false;
    } finally {
      // 리소스 해제 (필수)
      if (reader != null) reader.dispose();
    }
  }

  public static Boolean cmmndGdalInfo(String filePath) {

    File file = new File(filePath);

    if (!file.exists()) {
      System.err.println("파일이 존재하지 않습니다: " + filePath);
      return false;
    }

    boolean hasDriver = false;

    // 운영체제 감지
    String osName = System.getProperty("os.name").toLowerCase();
    boolean isWindows = osName.contains("win");
    boolean isMac = osName.contains("mac");
    boolean isUnix = osName.contains("nix") || osName.contains("nux") || osName.contains("aix");

    // gdalinfo 경로 찾기 (일반적인 설치 경로 우선 확인)
    String gdalinfoPath = findGdalinfoPath();
    if (gdalinfoPath == null) {
      System.err.println("gdalinfo 명령어를 찾을 수 없습니다. GDAL이 설치되어 있는지 확인하세요.");
      System.err.println("macOS: brew install gdal");
      System.err.println("Ubuntu/Debian: sudo apt-get install gdal-bin");
      System.err.println("CentOS/RHEL: sudo yum install gdal");
      return false;
    }

    List<String> command = new ArrayList<>();

    if (isWindows) {
      // 윈도우용
      command.add("cmd.exe"); // 윈도우 명령 프롬프트 실행
      command.add("/c"); // 명령어를 수행하고 종료한다는 옵션
      command.add("gdalinfo");
      command.add(filePath);
      command.add("|");
      command.add("findstr");
      command.add("/i");
      command.add("Geo");
    } else if (isMac || isUnix) {
      // 리눅스, 맥용
      command.add("sh");
      command.add("-c");
      command.add(gdalinfoPath + " \"" + filePath + "\" | grep -i Geo");
    } else {
      System.err.println("지원하지 않는 운영체제: " + osName);
      return false;
    }

    ProcessBuilder processBuilder = new ProcessBuilder(command);
    processBuilder.redirectErrorStream(true);

    Process process = null;
    BufferedReader reader = null;
    try {
      System.out.println("gdalinfo 명령어 실행 시작: " + filePath);
      process = processBuilder.start();

      reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

      String line;
      while ((line = reader.readLine()) != null) {
        // System.out.println("gdalinfo 출력: " + line);
        if (line.contains("Driver: GTiff/GeoTIFF")) {
          hasDriver = true;
          break;
        }
      }

      int exitCode = process.waitFor();
      System.out.println("gdalinfo 종료 코드: " + exitCode);

      // 프로세스가 정상 종료되지 않았고 Driver를 찾지 못한 경우
      if (exitCode != 0 && !hasDriver) {
        System.err.println("gdalinfo 명령 실행 실패. Exit code: " + exitCode);
      }

    } catch (IOException e) {
      System.err.println("gdalinfo 실행 중 I/O 오류 발생: " + e.getMessage());
      e.printStackTrace();
      return false;
    } catch (InterruptedException e) {
      System.err.println("gdalinfo 실행 중 인터럽트 발생: " + e.getMessage());
      Thread.currentThread().interrupt();
      return false;
    } catch (Exception e) {
      System.err.println("gdalinfo 실행 중 예상치 못한 오류 발생: " + e.getMessage());
      e.printStackTrace();
      return false;
    } finally {
      // 리소스 정리
      if (reader != null) {
        try {
          reader.close();
        } catch (IOException e) {
          System.err.println("BufferedReader 종료 중 오류: " + e.getMessage());
        }
      }
      if (process != null) {
        process.destroy();
      }
    }

    return hasDriver;
  }

  public static boolean mkDir(String dirPath) {
    Path uploadTargetPath = Paths.get(dirPath);
    try {
      Files.createDirectories(uploadTargetPath);
    } catch (IOException e) {
      return false;
    }

    return true;
  }

  public static List<Folder> getFolderAll(String dirPath, String sortType, int maxDepth) {

    Path startPath = Paths.get(dirPath);

    List<Folder> folderList = List.of();

    try (Stream<Path> stream = Files.walk(startPath, maxDepth)) {

      folderList =
          stream
              .filter(Files::isDirectory)
              .filter(p -> !p.toString().equals(dirPath))
              .map(
                  path -> {
                    int depth = path.getNameCount();

                    String folderNm = path.getFileName().toString();
                    String parentFolderNm = path.getParent().getFileName().toString();
                    String parentPath = path.getParent().toString();
                    String fullPath = path.toAbsolutePath().toString();

                    boolean isValid =
                        !NameValidator.containsKorean(folderNm)
                            && !NameValidator.containsWhitespaceRegex(folderNm);

                    File file = new File(fullPath);
                    int childCnt = getChildFolderCount(file);
                    String lastModified = getLastModified(file);

                    return new Folder(
                        folderNm,
                        parentFolderNm,
                        parentPath,
                        fullPath,
                        depth,
                        childCnt,
                        lastModified,
                        isValid);
                  })
              .collect(Collectors.toList());

      if (sortType.equals("name") || sortType.equals("name asc")) {
        folderList.sort(
            Comparator.comparing(
                Folder::getFolderNm, CASE_INSENSITIVE_ORDER // 대소문자 구분 없이
                ));
      } else if (sortType.equals("name desc")) {
        folderList.sort(
            Comparator.comparing(
                    Folder::getFolderNm, CASE_INSENSITIVE_ORDER // 대소문자 구분 없이
                    )
                .reversed());
      } else if (sortType.equals("dttm desc")) {
        folderList.sort(
            Comparator.comparing(
                    Folder::getLastModified, CASE_INSENSITIVE_ORDER // 대소문자 구분 없이
                    )
                .reversed());
      } else {
        folderList.sort(
            Comparator.comparing(
                Folder::getLastModified, CASE_INSENSITIVE_ORDER // 대소문자 구분 없이
                ));
      }

    } catch (IOException e) {
      throw new RuntimeException(e);
    }

    return folderList;
  }

  public static List<Folder> getFolderAll(String dirPath) {
    return getFolderAll(dirPath, "name", 1);
  }

  public static List<Folder> getFolderAll(String dirPath, String sortType) {
    return getFolderAll(dirPath, sortType, 1);
  }

  public static int getChildFolderCount(String dirPath) {
    File directory = new File(dirPath);
    File[] childFolders = directory.listFiles(File::isDirectory);

    int childCnt = 0;
    if (childFolders != null) {
      childCnt = childFolders.length;
    }

    return childCnt;
  }

  public static int getChildFolderCount(File directory) {
    File[] childFolders = directory.listFiles(File::isDirectory);

    int childCnt = 0;
    if (childFolders != null) {
      childCnt = childFolders.length;
    }

    return childCnt;
  }

  public static String getLastModified(String dirPath) {
    File file = new File(dirPath);
    return dttmFormat.format(new Date(file.lastModified()));
  }

  public static String getLastModified(File file) {
    return dttmFormat.format(new Date(file.lastModified()));
  }

  public static List<Basic> getFilesFromAllDepth(
      String dir,
      String targetFileNm,
      String extension,
      int maxDepth,
      String sortType,
      int startPos,
      int endPos) {

    Path startPath = Paths.get(dir);
    String dirPath = dir;

    int limit = endPos - startPos + 1;

    Set<String> targetExtensions = createExtensionSet(extension);

    List<Basic> fileList = new ArrayList<>();
    SimpleDateFormat dttmFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    Predicate<Path> isTargetName =
        p -> {
          if (targetFileNm == null
              || targetFileNm.trim().isEmpty()
              || targetFileNm.trim().equals("*")) {
            return true; // 전체 파일 허용
          }
          return p.getFileName().toString().contains(targetFileNm);
        };

    try (Stream<Path> stream = Files.walk(startPath, maxDepth)) {

      fileList =
          stream
              .filter(Files::isRegularFile)
              .filter(
                  p ->
                      extension == null
                          || extension.equals("")
                          || extension.equals("*")
                          || targetExtensions.contains(extractExtension(p)))
              .sorted(getFileComparator(sortType))
              .filter(isTargetName)
              .skip(startPos)
              .limit(limit)
              .map(
                  path -> {
                    // int depth = path.getNameCount();

                    String fileNm = path.getFileName().toString();
                    String ext = FilenameUtils.getExtension(fileNm);
                    String parentFolderNm = path.getParent().getFileName().toString();
                    String parentPath = path.getParent().toString();
                    String fullPath = path.toAbsolutePath().toString();

                    File file = new File(fullPath);
                    long fileSize = file.length();
                    String lastModified = dttmFormat.format(new Date(file.lastModified()));

                    return new Basic(
                        fileNm, parentFolderNm, parentPath, fullPath, ext, fileSize, lastModified);
                  })
              .collect(Collectors.toList());

    } catch (IOException e) {
      System.err.println("파일 I/O 오류 발생: " + e.getMessage());
    }

    return fileList;
  }

  public static List<Basic> getFilesFromAllDepth(
      String dir, String targetFileNm, String extension) {

    return FIleChecker.getFilesFromAllDepth(dir, targetFileNm, extension, 100, "name", 0, 100);
  }

  public static int getFileCountFromAllDepth(String dir, String targetFileNm, String extension) {

    List<FIleChecker.Basic> basicList =
        FIleChecker.getFilesFromAllDepth(dir, targetFileNm, extension);

    return (int)
        basicList.stream().filter(dto -> dto.getExtension().toString().equals(extension)).count();
  }

  public static Long getFileTotSize(List<FIleChecker.Basic> files) {

    Long fileTotSize = 0L;
    if (files != null || files.size() > 0) {
      fileTotSize = files.stream().mapToLong(FIleChecker.Basic::getFileSize).sum();
    }

    return fileTotSize;
  }

  public static boolean multipartSaveTo(MultipartFile mfile, String targetPath) {
    Path tmpSavePath = Paths.get(targetPath);

    boolean fileUpload = true;
    try {
      mfile.transferTo(tmpSavePath);
    } catch (IOException e) {
      // throw new RuntimeException(e);
      return false;
    }

    return true;
  }

  public static boolean multipartChunkSaveTo(
      MultipartFile mfile, String targetPath, int chunkIndex) {

    try {
      File dir = new File(targetPath);
      log.info("targetPath={}", targetPath);
      log.info("absolute targetPath={}", dir.getAbsolutePath());

      if (!dir.exists()) {
        dir.mkdirs();
      }

      File dest = new File(dir, String.valueOf(chunkIndex));
      log.info("real save path = {}", dest.getAbsolutePath());

      log.info("chunkIndex={}, uploadSize={}", chunkIndex, mfile.getSize());
      log.info("savedSize={}", dest.length());
      // ⭐ 핵심
      if (dest.exists()) {
        dest.delete();
      }

      log.info("after delete={}", dest.length());
      mfile.transferTo(dest);

      log.info("after transfer size={}", dest.length());
      log.info("after transfer exists={}", dest.exists());

      return true;
    } catch (IOException e) {
      log.error("chunk save error", e);
      return false;
    }
  }

  public static boolean deleteFolder(String path) {
    return FileSystemUtils.deleteRecursively(new File(path));
  }

  public static boolean validationMultipart(MultipartFile mfile) {
    // 파일 유효성 검증
    if (mfile == null || mfile.isEmpty() || mfile.getSize() == 0) {
      return false;
    }

    return true;
  }

  public static boolean checkExtensions(String fileName, String ext) {
    if (fileName == null) return false;

    if (!fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase().equals(ext)) {
      return false;
    }

    return true;
  }

  public static Set<String> createExtensionSet(String extensionString) {
    if (extensionString == null || extensionString.isBlank()) {
      return Set.of();
    }

    // "java, class" -> ["java", " class"] -> [".java", ".class"]
    return Arrays.stream(extensionString.split(","))
        .map(ext -> ext.trim())
        .filter(ext -> !ext.isEmpty())
        .map(ext -> "." + ext.toLowerCase())
        .collect(Collectors.toSet());
  }

  public static String extractExtension(Path path) {
    String filename = path.getFileName().toString();
    int lastDotIndex = filename.lastIndexOf('.');

    // 확장자가 없거나 파일명이 .으로 끝나는 경우
    if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) {
      return ""; // 빈 문자열 반환
    }

    // 확장자 추출 및 소문자 변환
    return filename.substring(lastDotIndex).toLowerCase();
  }

  public static Comparator<Path> getFileComparator(String sortType) {

    // 파일 이름 비교 기본 Comparator (대소문자 무시)
    Comparator<Path> nameComparator =
        Comparator.comparing(path -> path.getFileName().toString(), CASE_INSENSITIVE_ORDER);

    Comparator<Path> dateComparator =
        Comparator.comparing(
            path -> {
              try {
                return Files.getLastModifiedTime(path);
              } catch (IOException e) {
                return FileTime.fromMillis(0);
              }
            });

    if ("name desc".equalsIgnoreCase(sortType)) {
      return nameComparator.reversed();
    } else if ("date".equalsIgnoreCase(sortType)) {
      return dateComparator;
    } else if ("date desc".equalsIgnoreCase(sortType)) {
      return dateComparator.reversed();
    } else {
      return nameComparator;
    }
  }

  private static String findGdalinfoPath() {
    // 일반적인 설치 경로 확인
    String[] possiblePaths = {
      "/usr/local/bin/gdalinfo", // Homebrew (macOS)
      "/opt/homebrew/bin/gdalinfo", // Homebrew (Apple Silicon macOS)
      "/usr/bin/gdalinfo", // Linux
      "gdalinfo" // PATH에 있는 경우
    };

    for (String path : possiblePaths) {
      if (isCommandAvailable(path)) {
        return path;
      }
    }

    return null;
  }

  private static boolean isCommandAvailable(String command) {
    try {
      ProcessBuilder pb = new ProcessBuilder(command, "--version");
      pb.redirectErrorStream(true);
      Process process = pb.start();

      // 프로세스 완료 대기 (최대 5초)
      boolean finished = process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS);

      if (!finished) {
        process.destroy();
        return false;
      }

      // 종료 코드가 0이면 정상 (일부 명령어는 --version에서 다른 코드 반환할 수 있음)
      return process.exitValue() == 0 || process.exitValue() == 1;
    } catch (Exception e) {
      return false;
    }
  }

  @Schema(name = "Folder", description = "폴더 정보")
  @Getter
  public static class Folder {
    private final String folderNm;
    private final String parentFolderNm;
    private final String parentPath;
    private final String fullPath;
    private final int depth;
    private final long childCnt;
    private final String lastModified;
    private final Boolean isValid;

    public Folder(
        String folderNm,
        String parentFolderNm,
        String parentPath,
        String fullPath,
        int depth,
        long childCnt,
        String lastModified,
        Boolean isValid) {
      this.folderNm = folderNm;
      this.parentFolderNm = parentFolderNm;
      this.parentPath = parentPath;
      this.fullPath = fullPath;
      this.depth = depth;
      this.childCnt = childCnt;
      this.lastModified = lastModified;
      this.isValid = isValid;
    }
  }

  @Schema(name = "File Basic", description = "파일 기본 정보")
  @Getter
  public static class Basic {

    private final String fileNm;
    private final String parentFolderNm;
    private final String parentPath;
    private final String fullPath;
    private final String extension;
    private final long fileSize;
    private final String lastModified;

    public Basic(
        String fileNm,
        String parentFolderNm,
        String parentPath,
        String fullPath,
        String extension,
        long fileSize,
        String lastModified) {
      this.fileNm = fileNm;
      this.parentFolderNm = parentFolderNm;
      this.parentPath = parentPath;
      this.fullPath = fullPath;
      this.extension = extension;
      this.fileSize = fileSize;
      this.lastModified = lastModified;
    }
  }

  public static void unzip(String fileName, String destDirectory) throws IOException {
    String zipFilePath = destDirectory + File.separator + fileName;

    log.info("fileName : {}", fileName);
    log.info("destDirectory : {}", destDirectory);
    log.info("zipFilePath : {}", zipFilePath);
    // zip 이름으로 폴더 생성 (확장자 제거)
    String folderName =
        fileName.endsWith(".zip") ? fileName.substring(0, fileName.length() - 4) : fileName;
    log.info("folderName : {}", folderName);

    File destDir = new File(destDirectory, folderName);
    log.info("destDir : {}", destDir);

    // 동일 폴더가 이미 있으면 삭제
    log.info("111 destDir.exists() : {}", destDir.exists());
    if (destDir.exists()) {
      deleteDirectoryRecursively(destDir.toPath());
    }

    log.info("222 destDir.exists() : {}", destDir.exists());
    if (!destDir.exists()) {
      log.info("mkdirs : {}", destDir.exists());
      destDir.mkdirs();
    }

    try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFilePath))) {
      ZipEntry zipEntry = zis.getNextEntry();

      while (zipEntry != null) {
        File newFile = newFile(destDir, zipEntry);

        if (zipEntry.isDirectory()) {
          if (!newFile.isDirectory() && !newFile.mkdirs()) {
            throw new IOException("디렉토리 생성 실패: " + newFile);
          }

        } else {

          // 상위 디렉토리가 없는 경우 생성
          File parent = newFile.getParentFile();
          if (!parent.exists() && !parent.mkdirs()) {
            throw new IOException("상위 디렉토리 생성 실패: " + parent);
          }

          // 실제 파일 쓰기
          try (FileOutputStream fos = new FileOutputStream(newFile)) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = zis.read(buffer)) > 0) {
              fos.write(buffer, 0, len);
            }
          }
        }
        zipEntry = zis.getNextEntry();
      }
      zis.closeEntry();
    } catch (IOException e) {
      throw new CustomApiException(
          ApiResponseCode.INTERNAL_SERVER_ERROR.getId(),
          HttpStatus.INTERNAL_SERVER_ERROR,
          "압축 해제 중 오류가 발생했습니다: " + e.getMessage());
    }
  }

  public static File newFile(File destinationDir, ZipEntry zipEntry) throws IOException {
    File destFile = new File(destinationDir, zipEntry.getName());

    String destDirPath = destinationDir.getCanonicalPath();
    String destFilePath = destFile.getCanonicalPath();

    if (!destFilePath.startsWith(destDirPath + File.separator)) {
      throw new IOException("엔트리가 대상 디렉토리를 벗어남: " + zipEntry.getName());
    }

    return destFile;
  }

  public static List<String> execCommandAndReadLines(String command) {

    List<String> result = new ArrayList<>();

    String host = "192.168.2.86";
    String user = "kcomu";
    String password = "Kamco2025!";

    Session session = null;
    ChannelExec channel = null;

    try {
      JSch jsch = new JSch();

      session = jsch.getSession(user, host, 22);
      session.setPassword(password);

      Properties config = new Properties();
      config.put("StrictHostKeyChecking", "no");
      session.setConfig(config);

      session.connect(10_000);

      channel = (ChannelExec) session.openChannel("exec");
      channel.setCommand(command);
      channel.setInputStream(null);

      InputStream in = channel.getInputStream();
      channel.connect();

      try (BufferedReader br = new BufferedReader(new InputStreamReader(in))) {
        String line;
        while ((line = br.readLine()) != null) {
          result.add(line);
        }
      }

      return result;

    } catch (Exception e) {
      throw new RuntimeException("remote command failed : " + command, e);

    } finally {
      if (channel != null) channel.disconnect();
      if (session != null) session.disconnect();
    }
  }

  /** ✅ 폴더 재귀 삭제 */
  private static void deleteDirectoryRecursively(Path path) throws IOException {
    if (!Files.exists(path)) return;

    // 하위부터 지워야 하므로 reverse order
    Files.walk(path)
        .sorted(Comparator.reverseOrder())
        .forEach(
            p -> {
              try {
                Files.deleteIfExists(p);
              } catch (IOException e) {
                // 여기서 바로 RuntimeException으로 올려서 상위 catch(IOException)로 잡히게 함
                throw new UncheckedIOException("폴더 삭제 실패: " + p.toAbsolutePath(), e);
              }
            });
  }
}
