package incheon.ags.mrb.analysis.service.impl;

import com.all4land.sa.analysis.impl.*;
import com.all4land.sa.option.ClusteringOption;
import com.all4land.sa.option.aggregatepoints.AggregatePointsOption;
import com.all4land.sa.option.buffer.BufferOption;
import com.all4land.sa.option.clip.ClipOption;
import com.all4land.sa.option.density.DensityOption;
import com.all4land.sa.option.dissolve.DissolveOption;
import com.all4land.sa.option.extract.ExtractOption;
import com.all4land.sa.option.hotspot.HotspotOption;
import com.all4land.sa.option.intersect.IntersectOption;
import com.all4land.sa.option.joinFeatures.JoinFeaturesOption;
import com.all4land.sa.option.merge.MergeOption;
import com.all4land.sa.option.summarizeNearby.SummarizeNearbyOption;
import com.all4land.sa.option.summarizeWithin.SummarizeWithinOption;
import com.all4land.sa.option.union.UnionOption;
import incheon.ags.mrb.analysis.AnalysisValidationUtils;
import incheon.ags.mrb.analysis.domain.AnalysisKind;
import incheon.ags.mrb.analysis.service.AnalysisHistoryService;
import incheon.ags.mrb.analysis.vo.*;
import incheon.ags.mrb.analysis.domain.AnalysisStatus;
import incheon.ags.mrb.analysis.mapper.SpatialAnalysisMapper;
import incheon.ags.mrb.analysis.service.SpatialAnalysisService;
import incheon.ags.mrb.analysis.vo.request.*;
import incheon.cmm.g2f.layer.domain.LayerType;
import incheon.cmm.g2f.util.CoordinateUtils;
import incheon.cmm.g2f.util.GisServerRestUtils;
import incheon.com.security.vo.LoginVO;
import org.apache.commons.lang3.StringUtils;
import org.geotools.api.data.DataStore;
import org.geotools.api.data.Query;
import org.geotools.api.data.SimpleFeatureSource;
import org.geotools.api.feature.simple.SimpleFeature;
import org.geotools.api.feature.simple.SimpleFeatureType;
import org.geotools.api.feature.type.AttributeDescriptor;
import org.geotools.api.filter.Filter;
import org.geotools.api.referencing.FactoryException;
import org.geotools.api.referencing.operation.TransformException;
import org.geotools.data.postgis.PostgisNGDataStoreFactory;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.filter.text.cql2.CQL;
import org.geotools.filter.text.cql2.CQLException;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.jdbc.JDBCDataStore;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.TopologyException;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKBWriter;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.operation.buffer.BufferParameters;
import org.mybatis.spring.SqlSessionTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ResponseStatusException;

import javax.sql.DataSource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

import static incheon.ags.mrb.analysis.mapper.SpatialAnalysisSqlProvider.toGeomSubtype;

/**
 * 공간 분석 서비스 구현체
 */
@Service
public class SpatialAnalysisServiceImpl implements SpatialAnalysisService {

    private static final Logger logger = LoggerFactory.getLogger(SpatialAnalysisServiceImpl.class);

    private final ThreadPoolExecutor executor;
    private final DataSource dataSource;
    private final TransactionTemplate tx;
    private final RestTemplate restTemplate;
    private final SqlSessionTemplate batchSqlSessionTemplate;

    private final SpatialAnalysisMapper mapper;
    private final SpatialAnalysisMapper batchMapper;
    private final AnalysisHistoryService analysisHistoryService;

    private static final String GEOMETRY_ID = "gid";
    private static final String GEOMETRY_COLUMN = "geom";
    private static final int CHUNK_SIZE = 1000;
    private static final String RESULT_SCHEMA = "icmrb";

    private final ConcurrentMap<String, Set<String>> registry = new ConcurrentHashMap<>();

    @Value("${gis.server.url}")
    private String baseUrl;

    @Value("${gis.server.prefix}")
    private String prefix;

    /**
     * SpatialAnalysisServiceImpl 생성자
     *
     * @param executor   분석 ThreadPoolExecutor
     * @param dataSource 내부 DataSource
     * @param mapper     SpatialAnalysisMapper 매퍼
     * @param tx         TransactionTemplate
     */
    public SpatialAnalysisServiceImpl(
            @Qualifier("analysisExecutor") ThreadPoolExecutor executor,
            @Qualifier("dataSource") DataSource dataSource,
            TransactionTemplate tx,
            RestTemplate restTemplate,
            @Qualifier("batchSqlSessionTemplate") SqlSessionTemplate batchSqlSessionTemplate,
            SpatialAnalysisMapper mapper,
            AnalysisHistoryService analysisHistoryService) {
        this.executor = executor;
        this.dataSource = dataSource;
        tx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        this.tx = tx;
        this.restTemplate = restTemplate;
        this.batchSqlSessionTemplate = batchSqlSessionTemplate;
        this.mapper = mapper;
        this.batchMapper = batchSqlSessionTemplate.getMapper(SpatialAnalysisMapper.class);
        this.analysisHistoryService = analysisHistoryService;
    }

    /**
     * 포인트 집계 분석을 시작한다.
     *
     * @param request 집계 분석 요청 DTO
     * @return 등록된 분석 이력 VO
     */
    @Override
    public AnlsHstryVO startAggregatePoints(AggregatePointsRequestDTO request) {
        try {
            assertLayerNameNotDuplicated(getCurrentUserId(), request.getLayerName());

            AnalysisLayerInfoDTO sourceLayerInfoDTO = getAnalysisLayerInfo(request.getSourceLyrId(), request.getSourceLayerType());
            AnalysisLayerInfoDTO boundaryLayerInfoDTO = getAnalysisLayerInfo(request.getBoundaryLyrId(), request.getBoundaryLayerType());

            if (!StringUtils.defaultString(sourceLayerInfoDTO.getSpceTy()).toUpperCase().contains("POINT")) {
                throw new IllegalArgumentException("잘못된 소스 레이어 공간 유형입니다. 허용: POINT/MULTIPOINT");
            }
            if (!StringUtils.defaultString(boundaryLayerInfoDTO.getSpceTy()).toUpperCase().contains("POLYGON")) {
                throw new IllegalArgumentException("잘못된 경계 레이어 공간 유형입니다. 허용: POLYGON/MULTIPOLYGON");
            }

            if (isColumnExists(boundaryLayerInfoDTO, request.getCountField())) {
                throw new IllegalArgumentException("경계 레이어에 같은 이름의 컬럼이 이미 존재합니다.");
            }

            AnlsHstryVO anlsHstryVO = tx.execute(status -> {
                AnlsHstryVO vo = analysisHistoryService.insertAnlsHstry(AnalysisKind.AGGREGATE_POINTS);
                analysisHistoryService.insertAnlsHstryDtl(vo.getAnlsHstryId(), sourceLayerInfoDTO.getLyrNm(), boundaryLayerInfoDTO.getLyrNm(), request.toJsonString());
                return vo;
            });
            assert anlsHstryVO != null;
            final int anlsHstryId = anlsHstryVO.getAnlsHstryId();
            final UserLyrVO userLyrVO = makeUserLayerVO(request);

            executor.submit(() -> {
                try {
                    String sourceSchema;
                    String sourceTable;
                    if (sourceLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = sourceLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        sourceSchema = parts[0];
                        sourceTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + sourceLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    String boundarySchema;
                    String boundaryTable;
                    if (boundaryLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = boundaryLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        boundarySchema = parts[0];
                        boundaryTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + boundaryLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    // 상태 업데이트 : RUNNING
                    tx.execute(status -> {
                        analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.RUNNING, null, null, userLyrVO.getUserId());
                        return null;
                    });

                    // DataStore 생성
                    DataStore sourceDataStore = createDataStore(sourceSchema);
                    DataStore boundaryDataStore;
                    if (sourceSchema.equals(boundarySchema)) {
                        boundaryDataStore = sourceDataStore;
                    } else {
                        boundaryDataStore = createDataStore(boundarySchema);
                    }

                    try {
                        // SimpleFeatureCollection 가져오기
                        SimpleFeatureCollection boundaryFeatureCollection = getSimpleFeatureCollection(boundaryDataStore, boundaryTable, null);
                        SimpleFeatureCollection sourceFeatureCollection = getSimpleFeatureCollection(sourceDataStore, sourceTable, null);

                        // 좌표계를 EPSG:3857로 맞추기
                        boundaryFeatureCollection = CoordinateUtils.reprojectTo3857(boundaryFeatureCollection);
                        sourceFeatureCollection = CoordinateUtils.reprojectTo3857(sourceFeatureCollection);

                        // 분석 실행
                        AggregatePointsProcessor aggregatePointsProcessor = new AggregatePointsProcessor();
                        AggregatePointsOption aggregatePointsOption = new AggregatePointsOption(request.getCountField(), request.getStatisticsFields());
                        SimpleFeatureCollection analysisResult = aggregatePointsProcessor.run(boundaryFeatureCollection, sourceFeatureCollection, aggregatePointsOption);

                        postSpatialAnalysis(anlsHstryId, analysisResult, userLyrVO);
                    } finally {
                        if (sourceDataStore != null) {
                            sourceDataStore.dispose();
                        }
                        if (boundaryDataStore != null) {
                            boundaryDataStore.dispose();
                        }
                    }
                } catch (TopologyException e) {
                    String msg = "원본 데이터의 지오메트리가 유효하지 않아 분석을 수행할 수 없습니다.";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } catch (Exception e) {
                    String msg = "공간 분석 처리 중 예외가 발생했습니다";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } finally {
                    unregister(userLyrVO.getUserId(), userLyrVO.getUserLyrNm());
                }
            });
            return anlsHstryVO;
        } catch (Exception e) {
            unregister(getCurrentUserId(), request.getLayerName());
            throw e;
        }
    }

    /**
     * 버퍼 분석을 시작한다.
     *
     * @param request 버퍼 분석 요청 DTO
     * @return 등록된 분석 이력 VO
     */
    @Override
    public AnlsHstryVO startBuffer(BufferRequestDTO request) {

        try {
            assertLayerNameNotDuplicated(getCurrentUserId(), request.getLayerName());

            AnalysisLayerInfoDTO sourceLayerInfoDTO = getAnalysisLayerInfo(request.getSourceLyrId(), request.getSourceLayerType());

            AnlsHstryVO anlsHstryVO = tx.execute(status -> {
                AnlsHstryVO vo = analysisHistoryService.insertAnlsHstry(AnalysisKind.BUFFER);
                analysisHistoryService.insertAnlsHstryDtl(vo.getAnlsHstryId(), sourceLayerInfoDTO.getLyrNm(), null, request.toJsonString());
                return vo;
            });
            assert anlsHstryVO != null;
            final int anlsHstryId = anlsHstryVO.getAnlsHstryId();
            final UserLyrVO userLyrVO = makeUserLayerVO(request);

            executor.submit(() -> {
                try {
                    String schema;
                    String table;
                    if (sourceLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = sourceLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        schema = parts[0];
                        table = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + sourceLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    // 상태 업데이트 : RUNNING
                    tx.execute(status -> {
                        analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.RUNNING, null, null, userLyrVO.getUserId());
                        return null;
                    });

                    // DataStore 생성
                    DataStore dataStore = createDataStore(schema);
                    try {
                        // SimpleFeatureCollection 가져오기
                        SimpleFeatureCollection sourceFeatureCollection = getSimpleFeatureCollection(dataStore, table, null);

                        // 좌표계를 EPSG:3857로 맞추기
                        sourceFeatureCollection = CoordinateUtils.reprojectTo3857(sourceFeatureCollection);

                        // 분석 실행
                        BufferProcessor bufferProcessor = new BufferProcessor();
                        BufferOption bufferOption = new BufferOption(request.getDistance(), request.getResolution(), request.getEndCapStyle(), request.getJoinStyle(), BufferParameters.DEFAULT_MITRE_LIMIT);
                        SimpleFeatureCollection analysisResult = bufferProcessor.run(sourceFeatureCollection, bufferOption);

                        postSpatialAnalysis(anlsHstryId, analysisResult, userLyrVO);
                    } finally {
                        if (dataStore != null) {
                            dataStore.dispose();
                        }
                    }
                } catch (TopologyException e) {
                    String msg = "원본 데이터의 지오메트리가 유효하지 않아 분석을 수행할 수 없습니다.";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } catch (Exception e) {
                    String msg = "공간 분석 처리 중 예외가 발생했습니다";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } finally {
                    unregister(userLyrVO.getUserId(), userLyrVO.getUserLyrNm());
                }
            });
            return anlsHstryVO;
        } catch (Exception e) {
            unregister(getCurrentUserId(), request.getLayerName());
            throw e;
        }
    }

    /**
     * 유니온 분석을 시작한다.
     *
     * @param request 유니온 분석 요청 DTO
     * @return 등록된 분석 이력 VO
     */
    @Override
    public AnlsHstryVO startUnion(UnionRequestDTO request) {

        try {
            assertLayerNameNotDuplicated(getCurrentUserId(), request.getLayerName());

            AnalysisLayerInfoDTO sourceLayerInfoDTO = getAnalysisLayerInfo(request.getSourceLyrId(), request.getSourceLayerType());
            AnalysisLayerInfoDTO boundaryLayerInfoDTO = getAnalysisLayerInfo(request.getBoundaryLyrId(), request.getBoundaryLayerType());

            AnalysisValidationUtils.assertSameGeometryType(sourceLayerInfoDTO.getSpceTy(), boundaryLayerInfoDTO.getSpceTy(), false);

            AnlsHstryVO anlsHstryVO = tx.execute(status -> {
                AnlsHstryVO vo = analysisHistoryService.insertAnlsHstry(AnalysisKind.UNION);
                analysisHistoryService.insertAnlsHstryDtl(vo.getAnlsHstryId(), sourceLayerInfoDTO.getLyrNm(), boundaryLayerInfoDTO.getLyrNm(), request.toJsonString());
                return vo;
            });
            assert anlsHstryVO != null;
            final int anlsHstryId = anlsHstryVO.getAnlsHstryId();
            final UserLyrVO userLyrVO = makeUserLayerVO(request);

            executor.submit(() -> {
                try {
                    String sourceSchema;
                    String sourceTable;
                    if (sourceLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = sourceLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        sourceSchema = parts[0];
                        sourceTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + sourceLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    String boundarySchema;
                    String boundaryTable;
                    if (boundaryLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = boundaryLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        boundarySchema = parts[0];
                        boundaryTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + boundaryLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    // 상태 업데이트 : RUNNING
                    tx.execute(status -> {
                        analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.RUNNING, null, null, userLyrVO.getUserId());
                        return null;
                    });

                    // DataStore 생성
                    DataStore sourceDataStore = createDataStore(sourceSchema);
                    DataStore boundaryDataStore;
                    if (sourceSchema.equals(boundarySchema)) {
                        boundaryDataStore = sourceDataStore;
                    } else {
                        boundaryDataStore = createDataStore(boundarySchema);
                    }

                    try {
                        // SimpleFeatureCollection 가져오기
                        SimpleFeatureCollection boundaryFeatureCollection = getSimpleFeatureCollection(boundaryDataStore, boundaryTable, null);
                        SimpleFeatureCollection sourceFeatureCollection = getSimpleFeatureCollection(sourceDataStore, sourceTable, null);

                        // 좌표계를 EPSG:3857로 맞추기
                        boundaryFeatureCollection = CoordinateUtils.reprojectTo3857(boundaryFeatureCollection);
                        sourceFeatureCollection = CoordinateUtils.reprojectTo3857(sourceFeatureCollection);

                        // 분석 실행
                        UnionProcessor unionProcessor = new UnionProcessor();
                        UnionOption unionOption = new UnionOption(request.getAttributeJoinMode());
                        SimpleFeatureCollection analysisResult = unionProcessor.run(boundaryFeatureCollection, sourceFeatureCollection, unionOption);

                        postSpatialAnalysis(anlsHstryId, analysisResult, userLyrVO);
                    } finally {
                        if (sourceDataStore != null) {
                            sourceDataStore.dispose();
                        }
                        if (boundaryDataStore != null) {
                            boundaryDataStore.dispose();
                        }
                    }
                } catch (TopologyException e) {
                    String msg = "원본 데이터의 지오메트리가 유효하지 않아 분석을 수행할 수 없습니다.";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } catch (Exception e) {
                    String msg = "공간 분석 처리 중 예외가 발생했습니다";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } finally {
                    unregister(userLyrVO.getUserId(), userLyrVO.getUserLyrNm());
                }
            });
            return anlsHstryVO;
        } catch (Exception e) {
            unregister(getCurrentUserId(), request.getLayerName());
            throw e;
        }
    }

    /**
     * 클립 분석을 시작한다.
     *
     * @param request 클립 분석 요청 DTO
     * @return 등록된 분석 이력 VO
     */
    @Override
    public AnlsHstryVO startClip(ClipRequestDTO request) {

        try {
            assertLayerNameNotDuplicated(getCurrentUserId(), request.getLayerName());

            AnalysisLayerInfoDTO sourceLayerInfoDTO = getAnalysisLayerInfo(request.getSourceLyrId(), request.getSourceLayerType());
            AnalysisLayerInfoDTO boundaryLayerInfoDTO = getAnalysisLayerInfo(request.getBoundaryLyrId(), request.getBoundaryLayerType());

            AnalysisValidationUtils.assertClipCompatible(boundaryLayerInfoDTO.getSpceTy(), sourceLayerInfoDTO.getSpceTy());

            AnlsHstryVO anlsHstryVO = tx.execute(status -> {
                AnlsHstryVO vo = analysisHistoryService.insertAnlsHstry(AnalysisKind.CLIP);
                analysisHistoryService.insertAnlsHstryDtl(vo.getAnlsHstryId(), sourceLayerInfoDTO.getLyrNm(), boundaryLayerInfoDTO.getLyrNm(), request.toJsonString());
                return vo;
            });
            assert anlsHstryVO != null;
            final int anlsHstryId = anlsHstryVO.getAnlsHstryId();
            final UserLyrVO userLyrVO = makeUserLayerVO(request);

            executor.submit(() -> {
                try {
                    String sourceSchema;
                    String sourceTable;
                    if (sourceLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = sourceLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        sourceSchema = parts[0];
                        sourceTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + sourceLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    String boundarySchema;
                    String boundaryTable;
                    if (boundaryLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = boundaryLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        boundarySchema = parts[0];
                        boundaryTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + boundaryLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    // 상태 업데이트 : RUNNING
                    tx.execute(status -> {
                        analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.RUNNING, null, null, userLyrVO.getUserId());
                        return null;
                    });

                    // DataStore 생성
                    DataStore sourceDataStore = createDataStore(sourceSchema);
                    DataStore boundaryDataStore;
                    if (sourceSchema.equals(boundarySchema)) {
                        boundaryDataStore = sourceDataStore;
                    } else {
                        boundaryDataStore = createDataStore(boundarySchema);
                    }

                    try {
                        // SimpleFeatureCollection 가져오기
                        SimpleFeatureCollection boundaryFeatureCollection = getSimpleFeatureCollection(boundaryDataStore, boundaryTable, null);
                        SimpleFeatureCollection sourceFeatureCollection = getSimpleFeatureCollection(sourceDataStore, sourceTable, null);

                        // 좌표계를 EPSG:3857로 맞추기
                        boundaryFeatureCollection = CoordinateUtils.reprojectTo3857(boundaryFeatureCollection);
                        sourceFeatureCollection = CoordinateUtils.reprojectTo3857(sourceFeatureCollection);

                        // 분석 실행
                        ClipProcessor clipProcessor = new ClipProcessor();
                        ClipOption clipOption = new ClipOption(request.getTolerance());
                        SimpleFeatureCollection analysisResult = clipProcessor.run(sourceFeatureCollection, boundaryFeatureCollection, clipOption);

                        postSpatialAnalysis(anlsHstryId, analysisResult, userLyrVO);
                    } finally {
                        if (sourceDataStore != null) {
                            sourceDataStore.dispose();
                        }
                        if (boundaryDataStore != null) {
                            boundaryDataStore.dispose();
                        }
                    }
                } catch (TopologyException e) {
                    String msg = "원본 데이터의 지오메트리가 유효하지 않아 분석을 수행할 수 없습니다.";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } catch (Exception e) {
                    String msg = "공간 분석 처리 중 예외가 발생했습니다";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } finally {
                    unregister(userLyrVO.getUserId(), userLyrVO.getUserLyrNm());
                }
            });
            return anlsHstryVO;
        } catch (Exception e) {
            unregister(getCurrentUserId(), request.getLayerName());
            throw e;
        }
    }

    /**
     * 클러스터링 분석을 시작한다.
     *
     * @param request 클러스터링 분석 요청 DTO
     * @return 등록된 분석 이력 VO
     */
    @Override
    public AnlsHstryVO startClustering(ClusteringRequestDTO request) {

        try {
            assertLayerNameNotDuplicated(getCurrentUserId(), request.getLayerName());

            AnalysisLayerInfoDTO sourceLayerInfoDTO = getAnalysisLayerInfo(request.getSourceLyrId(), request.getSourceLayerType());

            if (!request.getAsCircle() && isColumnExists(sourceLayerInfoDTO, request.getTargetField())) {
                throw new IllegalArgumentException("레이어에 같은 이름의 컬럼이 이미 존재합니다.");
            }

            AnlsHstryVO anlsHstryVO = tx.execute(status -> {
                AnlsHstryVO vo = analysisHistoryService.insertAnlsHstry(AnalysisKind.CLUSTERING);
                analysisHistoryService.insertAnlsHstryDtl(vo.getAnlsHstryId(), sourceLayerInfoDTO.getLyrNm(), null, request.toJsonString());
                return vo;
            });
            assert anlsHstryVO != null;
            final int anlsHstryId = anlsHstryVO.getAnlsHstryId();
            final UserLyrVO userLyrVO = makeUserLayerVO(request);

            executor.submit(() -> {
                try {
                    String sourceSchema;
                    String sourceTable;
                    if (sourceLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = sourceLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        sourceSchema = parts[0];
                        sourceTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + sourceLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    // 상태 업데이트 : RUNNING
                    tx.execute(status -> {
                        analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.RUNNING, null, null, userLyrVO.getUserId());
                        return null;
                    });

                    // DataStore 생성
                    DataStore sourceDataStore = createDataStore(sourceSchema);

                    try {
                        // SimpleFeatureCollection 가져오기
                        SimpleFeatureCollection sourceFeatureCollection = getSimpleFeatureCollection(sourceDataStore, sourceTable, null);

                        // 좌표계를 EPSG:3857로 맞추기
                        sourceFeatureCollection = CoordinateUtils.reprojectTo3857(sourceFeatureCollection);

                        // 분석 실행
                        ClusteringProcessor clusteringProcessor = new ClusteringProcessor();
                        ClusteringOption clusteringOption = new ClusteringOption(request.getTargetField(), request.getNumberOfClusters(), request.getAsCircle());
                        SimpleFeatureCollection analysisResult = clusteringProcessor.run(sourceFeatureCollection, clusteringOption);

                        postSpatialAnalysis(anlsHstryId, analysisResult, userLyrVO);
                    } finally {
                        if (sourceDataStore != null) {
                            sourceDataStore.dispose();
                        }
                    }
                } catch (TopologyException e) {
                    String msg = "원본 데이터의 지오메트리가 유효하지 않아 분석을 수행할 수 없습니다.";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } catch (Exception e) {
                    String msg = "공간 분석 처리 중 예외가 발생했습니다";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } finally {
                    unregister(userLyrVO.getUserId(), userLyrVO.getUserLyrNm());
                }
            });
            return anlsHstryVO;
        } catch (Exception e) {
            unregister(getCurrentUserId(), request.getLayerName());
            throw e;
        }
    }

    /**
     * 디졸브 분석을 시작한다.
     *
     * @param request 디졸브 분석 요청 DTO
     * @return 등록된 분석 이력 VO
     */
    @Override
    public AnlsHstryVO startDissolve(DissolveRequestDTO request) {

        try {
            assertLayerNameNotDuplicated(getCurrentUserId(), request.getLayerName());

            AnalysisLayerInfoDTO sourceLayerInfoDTO = getAnalysisLayerInfo(request.getSourceLyrId(), request.getSourceLayerType());

            AnlsHstryVO anlsHstryVO = tx.execute(status -> {
                AnlsHstryVO vo = analysisHistoryService.insertAnlsHstry(AnalysisKind.DISSOLVE);
                analysisHistoryService.insertAnlsHstryDtl(vo.getAnlsHstryId(), sourceLayerInfoDTO.getLyrNm(), null, request.toJsonString());
                return vo;
            });
            assert anlsHstryVO != null;
            final int anlsHstryId = anlsHstryVO.getAnlsHstryId();
            final UserLyrVO userLyrVO = makeUserLayerVO(request);

            executor.submit(() -> {
                try {
                    String sourceSchema;
                    String sourceTable;
                    if (sourceLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = sourceLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        sourceSchema = parts[0];
                        sourceTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + sourceLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    // 상태 업데이트 : RUNNING
                    tx.execute(status -> {
                        analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.RUNNING, null, null, userLyrVO.getUserId());
                        return null;
                    });

                    // DataStore 생성
                    DataStore sourceDataStore = createDataStore(sourceSchema);

                    try {
                        // SimpleFeatureCollection 가져오기
                        SimpleFeatureCollection sourceFeatureCollection = getSimpleFeatureCollection(sourceDataStore, sourceTable, null);

                        // 좌표계를 EPSG:3857로 맞추기
                        sourceFeatureCollection = CoordinateUtils.reprojectTo3857(sourceFeatureCollection);

                        // 분석 실행
                        DissolveProcessor dissolveProcessor = new DissolveProcessor();
                        DissolveOption dissolveOption = new DissolveOption(request.getDissolveField(), request.getStatisticsFields(), request.getUseMultiPart());
                        SimpleFeatureCollection analysisResult = dissolveProcessor.run(sourceFeatureCollection, dissolveOption);

                        postSpatialAnalysis(anlsHstryId, analysisResult, userLyrVO);
                    } finally {
                        if (sourceDataStore != null) {
                            sourceDataStore.dispose();
                        }
                    }
                } catch (TopologyException e) {
                    String msg = "원본 데이터의 지오메트리가 유효하지 않아 분석을 수행할 수 없습니다.";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } catch (Exception e) {
                    String msg = "공간 분석 처리 중 예외가 발생했습니다";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } finally {
                    unregister(userLyrVO.getUserId(), userLyrVO.getUserLyrNm());
                }
            });
            return anlsHstryVO;
        } catch (Exception e) {
            unregister(getCurrentUserId(), request.getLayerName());
            throw e;
        }
    }

    /**
     * 추출 분석을 시작한다.
     *
     * @param request 추출 분석 요청 DTO
     * @return 등록된 분석 이력 VO
     */
    @Override
    public AnlsHstryVO startExtract(ExtractRequestDTO request) {

        try {
            assertLayerNameNotDuplicated(getCurrentUserId(), request.getLayerName());

            Filter filter;
            try {
                filter = CQL.toFilter(request.getFilterString());
            } catch (CQLException e) {
                throw new IllegalArgumentException("잘못된 CQL 필터 형식입니다: " + e.getMessage(), e);
            }
            if (filter == null) {
                throw new IllegalArgumentException("잘못된 CQL 필터 형식입니다: " + request.getFilterString());
            }

            AnalysisLayerInfoDTO sourceLayerInfoDTO = getAnalysisLayerInfo(request.getSourceLyrId(), request.getSourceLayerType());

            AnlsHstryVO anlsHstryVO = tx.execute(status -> {
                AnlsHstryVO vo = analysisHistoryService.insertAnlsHstry(AnalysisKind.EXTRACT);
                analysisHistoryService.insertAnlsHstryDtl(vo.getAnlsHstryId(), sourceLayerInfoDTO.getLyrNm(), null, request.toJsonString());
                return vo;
            });
            assert anlsHstryVO != null;
            final int anlsHstryId = anlsHstryVO.getAnlsHstryId();
            final UserLyrVO userLyrVO = makeUserLayerVO(request);

            executor.submit(() -> {
                try {
                    String sourceSchema;
                    String sourceTable;
                    if (sourceLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = sourceLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        sourceSchema = parts[0];
                        sourceTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + sourceLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    // 상태 업데이트 : RUNNING
                    tx.execute(status -> {
                        analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.RUNNING, null, null, userLyrVO.getUserId());
                        return null;
                    });

                    // DataStore 생성
                    DataStore sourceDataStore = createDataStore(sourceSchema);

                    try {
                        // SimpleFeatureCollection 가져오기
                        SimpleFeatureCollection sourceFeatureCollection = getSimpleFeatureCollection(sourceDataStore, sourceTable, filter);

                        // 좌표계를 EPSG:3857로 맞추기
                        sourceFeatureCollection = CoordinateUtils.reprojectTo3857(sourceFeatureCollection);

                        // 분석 실행
                        ExtractProcessor extractProcessor = new ExtractProcessor();

                        // 추출 컬럼 옵션에서 GEOMETRY_COLUMN 강제 추가 및 GEOMETRY_ID 제거
                        String names = request.getAttributeNames();
                        if (names != null && !names.isBlank()) {
                            List<String> list = Arrays.stream(names.split(","))
                                    .map(String::trim)
                                    .filter(s -> !s.equalsIgnoreCase(GEOMETRY_ID))
                                    .collect(Collectors.toList());

                            if (!list.contains(GEOMETRY_COLUMN)) {
                                list.add(GEOMETRY_COLUMN);
                            }
                            request.setAttributeNames(String.join(",", list));
                        }

                        ExtractOption extractOption = new ExtractOption(filter, request.getAttributeNames());
                        SimpleFeatureCollection analysisResult = extractProcessor.run(sourceFeatureCollection, extractOption);

                        postSpatialAnalysis(anlsHstryId, analysisResult, userLyrVO);
                    } finally {
                        if (sourceDataStore != null) {
                            sourceDataStore.dispose();
                        }
                    }
                } catch (TopologyException e) {
                    String msg = "원본 데이터의 지오메트리가 유효하지 않아 분석을 수행할 수 없습니다.";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } catch (Exception e) {
                    String msg = "공간 분석 처리 중 예외가 발생했습니다";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } finally {
                    unregister(userLyrVO.getUserId(), userLyrVO.getUserLyrNm());
                }
            });
            return anlsHstryVO;
        } catch (Exception e) {
            unregister(getCurrentUserId(), request.getLayerName());
            throw e;
        }
    }

    /**
     * 핫스팟 분석을 시작한다.
     *
     * @param request 핫스팟 분석 요청 DTO
     * @return 등록된 분석 이력 VO
     */
    @Override
    public AnlsHstryVO startHotSpot(HotSpotRequestDTO request) {

        try {
            assertLayerNameNotDuplicated(getCurrentUserId(), request.getLayerName());

            AnalysisLayerInfoDTO sourceLayerInfoDTO = getAnalysisLayerInfo(request.getSourceLyrId(), request.getSourceLayerType());

            AnlsHstryVO anlsHstryVO = tx.execute(status -> {
                AnlsHstryVO vo = analysisHistoryService.insertAnlsHstry(AnalysisKind.HOTSPOT);
                analysisHistoryService.insertAnlsHstryDtl(vo.getAnlsHstryId(), sourceLayerInfoDTO.getLyrNm(), null, request.toJsonString());
                return vo;
            });
            assert anlsHstryVO != null;
            final int anlsHstryId = anlsHstryVO.getAnlsHstryId();
            final UserLyrVO userLyrVO = makeUserLayerVO(request);

            executor.submit(() -> {
                try {
                    String sourceSchema;
                    String sourceTable;
                    if (sourceLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = sourceLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        sourceSchema = parts[0];
                        sourceTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + sourceLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    // 상태 업데이트 : RUNNING
                    tx.execute(status -> {
                        analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.RUNNING, null, null, userLyrVO.getUserId());
                        return null;
                    });

                    // DataStore 생성
                    DataStore sourceDataStore = createDataStore(sourceSchema);

                    try {
                        // SimpleFeatureCollection 가져오기
                        SimpleFeatureCollection sourceFeatureCollection = getSimpleFeatureCollection(sourceDataStore, sourceTable, null);

                        // 좌표계를 EPSG:3857로 맞추기
                        sourceFeatureCollection = CoordinateUtils.reprojectTo3857(sourceFeatureCollection);

                        // 분석 실행
                        HotSpotProcessor hotSpotProcessor = new HotSpotProcessor();

                        HotspotOption hotSpotOption = new HotspotOption(
                                request.getInputField(),
                                request.getSpatialConcept(),
                                request.getDistanceMethod(),
                                request.getStandardization(),
                                request.getSearchDistance(),
                                request.getModel()
                        );

                        SimpleFeatureCollection analysisResult = hotSpotProcessor.run(sourceFeatureCollection, hotSpotOption);

                        postSpatialAnalysis(anlsHstryId, analysisResult, userLyrVO);
                    } finally {
                        if (sourceDataStore != null) {
                            sourceDataStore.dispose();
                        }
                    }
                } catch (TopologyException e) {
                    String msg = "원본 데이터의 지오메트리가 유효하지 않아 분석을 수행할 수 없습니다.";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } catch (Exception e) {
                    String msg = "공간 분석 처리 중 예외가 발생했습니다";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } finally {
                    unregister(userLyrVO.getUserId(), userLyrVO.getUserLyrNm());
                }
            });
            return anlsHstryVO;
        } catch (Exception e) {
            unregister(getCurrentUserId(), request.getLayerName());
            throw e;
        }
    }


    /**
     * 교차 분석을 시작한다.
     *
     * @param request 교차 분석 요청 DTO
     * @return 등록된 분석 이력 VO
     */
    @Override
    public AnlsHstryVO startIntersect(IntersectRequestDTO request) {

        try {
            assertLayerNameNotDuplicated(getCurrentUserId(), request.getLayerName());

            AnalysisLayerInfoDTO sourceLayerInfoDTO = getAnalysisLayerInfo(request.getSourceLyrId(), request.getSourceLayerType());
            AnalysisLayerInfoDTO boundaryLayerInfoDTO = getAnalysisLayerInfo(request.getBoundaryLyrId(), request.getBoundaryLayerType());

            AnlsHstryVO anlsHstryVO = tx.execute(status -> {
                AnlsHstryVO vo = analysisHistoryService.insertAnlsHstry(AnalysisKind.INTERSECT);
                analysisHistoryService.insertAnlsHstryDtl(vo.getAnlsHstryId(), sourceLayerInfoDTO.getLyrNm(), boundaryLayerInfoDTO.getLyrNm(), request.toJsonString());
                return vo;
            });
            assert anlsHstryVO != null;
            final int anlsHstryId = anlsHstryVO.getAnlsHstryId();
            final UserLyrVO userLyrVO = makeUserLayerVO(request);

            executor.submit(() -> {
                try {
                    String sourceSchema;
                    String sourceTable;
                    if (sourceLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = sourceLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        sourceSchema = parts[0];
                        sourceTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + sourceLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    String boundarySchema;
                    String boundaryTable;
                    if (boundaryLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = boundaryLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        boundarySchema = parts[0];
                        boundaryTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + boundaryLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    // 상태 업데이트 : RUNNING
                    tx.execute(status -> {
                        analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.RUNNING, null, null, userLyrVO.getUserId());
                        return null;
                    });

                    // DataStore 생성
                    DataStore sourceDataStore = createDataStore(sourceSchema);
                    DataStore boundaryDataStore;
                    if (sourceSchema.equals(boundarySchema)) {
                        boundaryDataStore = sourceDataStore;
                    } else {
                        boundaryDataStore = createDataStore(boundarySchema);
                    }

                    try {
                        // SimpleFeatureCollection 가져오기
                        SimpleFeatureCollection boundaryFeatureCollection = getSimpleFeatureCollection(boundaryDataStore, boundaryTable, null);
                        SimpleFeatureCollection sourceFeatureCollection = getSimpleFeatureCollection(sourceDataStore, sourceTable, null);

                        // 좌표계를 EPSG:3857로 맞추기
                        boundaryFeatureCollection = CoordinateUtils.reprojectTo3857(boundaryFeatureCollection);
                        sourceFeatureCollection = CoordinateUtils.reprojectTo3857(sourceFeatureCollection);

                        // 분석 실행
                        IntersectProcessor intersectProcessor = new IntersectProcessor();
                        IntersectOption intersectOption = new IntersectOption(request.getAttributeJoinMode(), request.getOutputType(), request.getTolerance());
                        SimpleFeatureCollection analysisResult = intersectProcessor.run(sourceFeatureCollection, boundaryFeatureCollection, intersectOption);

                        postSpatialAnalysis(anlsHstryId, analysisResult, userLyrVO);
                    } finally {
                        if (sourceDataStore != null) {
                            sourceDataStore.dispose();
                        }
                        if (boundaryDataStore != null) {
                            boundaryDataStore.dispose();
                        }
                    }
                } catch (TopologyException e) {
                    String msg = "원본 데이터의 지오메트리가 유효하지 않아 분석을 수행할 수 없습니다.";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } catch (Exception e) {
                    String msg = "공간 분석 처리 중 예외가 발생했습니다";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } finally {
                    unregister(userLyrVO.getUserId(), userLyrVO.getUserLyrNm());
                }
            });
            return anlsHstryVO;
        } catch (Exception e) {
            unregister(getCurrentUserId(), request.getLayerName());
            throw e;
        }
    }

    /**
     * 병합 분석을 시작한다.
     *
     * @param request 병합 분석 요청 DTO
     * @return 등록된 분석 이력 VO
     */
    @Override
    public AnlsHstryVO startMerge(MergeRequestDTO request) {

        try {
            assertLayerNameNotDuplicated(getCurrentUserId(), request.getLayerName());

            AnalysisLayerInfoDTO sourceLayerInfoDTO = getAnalysisLayerInfo(request.getSourceLyrId(), request.getSourceLayerType());
            AnalysisLayerInfoDTO boundaryLayerInfoDTO = getAnalysisLayerInfo(request.getBoundaryLyrId(), request.getBoundaryLayerType());

            if (!AnalysisValidationUtils.toGeomFamily(sourceLayerInfoDTO.getSpceTy()).equals(AnalysisValidationUtils.toGeomFamily(boundaryLayerInfoDTO.getSpceTy()))) {
                throw new IllegalArgumentException(
                        "Source 레이어와 Boundary 레이어의 공간 유형이 서로 다릅니다. 동일한 공간 유형의 레이어만 분석할 수 있습니다."
                );
            }

            AnlsHstryVO anlsHstryVO = tx.execute(status -> {
                AnlsHstryVO vo = analysisHistoryService.insertAnlsHstry(AnalysisKind.MERGE);
                analysisHistoryService.insertAnlsHstryDtl(vo.getAnlsHstryId(), sourceLayerInfoDTO.getLyrNm(), boundaryLayerInfoDTO.getLyrNm(), request.toJsonString());
                return vo;
            });
            assert anlsHstryVO != null;
            final int anlsHstryId = anlsHstryVO.getAnlsHstryId();
            final UserLyrVO userLyrVO = makeUserLayerVO(request);

            executor.submit(() -> {
                try {
                    String sourceSchema;
                    String sourceTable;
                    if (sourceLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = sourceLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        sourceSchema = parts[0];
                        sourceTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + sourceLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    String boundarySchema;
                    String boundaryTable;
                    if (boundaryLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = boundaryLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        boundarySchema = parts[0];
                        boundaryTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + boundaryLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    // 상태 업데이트 : RUNNING
                    tx.execute(status -> {
                        analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.RUNNING, null, null, userLyrVO.getUserId());
                        return null;
                    });

                    // DataStore 생성
                    DataStore sourceDataStore = createDataStore(sourceSchema);
                    DataStore boundaryDataStore;
                    if (sourceSchema.equals(boundarySchema)) {
                        boundaryDataStore = sourceDataStore;
                    } else {
                        boundaryDataStore = createDataStore(boundarySchema);
                    }

                    try {
                        // SimpleFeatureCollection 가져오기
                        SimpleFeatureCollection boundaryFeatureCollection = getSimpleFeatureCollection(boundaryDataStore, boundaryTable, null);
                        SimpleFeatureCollection sourceFeatureCollection = getSimpleFeatureCollection(sourceDataStore, sourceTable, null);

                        // 좌표계를 EPSG:3857로 맞추기
                        boundaryFeatureCollection = CoordinateUtils.reprojectTo3857(boundaryFeatureCollection);
                        sourceFeatureCollection = CoordinateUtils.reprojectTo3857(sourceFeatureCollection);

                        // 분석 실행
                        MergeProcessor mergeProcessor = new MergeProcessor();
                        MergeOption mergeOption = new MergeOption(request.getAddSourceField(), request.getUnionFields(), request.getFieldMappings());
                        SimpleFeatureCollection analysisResult = mergeProcessor.run(sourceFeatureCollection, boundaryFeatureCollection, mergeOption);

                        postSpatialAnalysis(anlsHstryId, analysisResult, userLyrVO);
                    } finally {
                        if (sourceDataStore != null) {
                            sourceDataStore.dispose();
                        }
                        if (boundaryDataStore != null) {
                            boundaryDataStore.dispose();
                        }
                    }
                } catch (TopologyException e) {
                    String msg = "원본 데이터의 지오메트리가 유효하지 않아 분석을 수행할 수 없습니다.";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } catch (Exception e) {
                    String msg = "공간 분석 처리 중 예외가 발생했습니다";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } finally {
                    unregister(userLyrVO.getUserId(), userLyrVO.getUserLyrNm());
                }
            });
            return anlsHstryVO;
        } catch (Exception e) {
            unregister(getCurrentUserId(), request.getLayerName());
            throw e;
        }
    }

    /**
     * 주변 요약 분석을 시작한다.
     *
     * @param request 주변 요약 분석 요청 DTO
     * @return 등록된 분석 이력 VO
     */
    @Override
    public AnlsHstryVO startSummarizeNearby(SummarizeNearbyRequestDTO request) {

        try {
            assertLayerNameNotDuplicated(getCurrentUserId(), request.getLayerName());

            AnalysisLayerInfoDTO sourceLayerInfoDTO = getAnalysisLayerInfo(request.getSourceLyrId(), request.getSourceLayerType());

            AnlsHstryVO anlsHstryVO = tx.execute(status -> {
                AnlsHstryVO vo = analysisHistoryService.insertAnlsHstry(AnalysisKind.SUMMARIZE_NEARBY);
                analysisHistoryService.insertAnlsHstryDtl(vo.getAnlsHstryId(), sourceLayerInfoDTO.getLyrNm(), null, request.toJsonString());
                return vo;
            });
            assert anlsHstryVO != null;
            final int anlsHstryId = anlsHstryVO.getAnlsHstryId();
            final UserLyrVO userLyrVO = makeUserLayerVO(request);

            Geometry geometry;
            try {
                geometry = new WKTReader().read(request.getGeometry());
            } catch (ParseException e) {
                throw new IllegalArgumentException("유효하지 않은 WKT 형식입니다: " + request.getGeometry(), e);
            }

            executor.submit(() -> {
                try {
                    String sourceSchema;
                    String sourceTable;
                    if (sourceLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = sourceLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        sourceSchema = parts[0];
                        sourceTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + sourceLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    // 상태 업데이트 : RUNNING
                    tx.execute(status -> {
                        analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.RUNNING, null, null, userLyrVO.getUserId());
                        return null;
                    });

                    // DataStore 생성
                    DataStore sourceDataStore = createDataStore(sourceSchema);

                    try {
                        // SimpleFeatureCollection 가져오기
                        SimpleFeatureCollection sourceFeatureCollection = getSimpleFeatureCollection(sourceDataStore, sourceTable, null);

                        // 좌표계를 EPSG:3857로 맞추기
                        sourceFeatureCollection = CoordinateUtils.reprojectTo3857(sourceFeatureCollection);

                        // 분석 실행
                        SummarizeNearbyProcessor summarizeNearbyProcessor = new SummarizeNearbyProcessor();

                        SummarizeNearbyOption summarizeNearbyOption = new SummarizeNearbyOption(
                                geometry,
                                request.getBufferSize(),
                                request.getIncludeGeometrySummary(),
                                request.getStatisticsFields()
                        );

                        SimpleFeatureCollection analysisResult = summarizeNearbyProcessor.run(sourceFeatureCollection, summarizeNearbyOption);

                        postSpatialAnalysis(anlsHstryId, analysisResult, userLyrVO);
                    } finally {
                        if (sourceDataStore != null) {
                            sourceDataStore.dispose();
                        }
                    }
                } catch (TopologyException e) {
                    String msg = "원본 데이터의 지오메트리가 유효하지 않아 분석을 수행할 수 없습니다.";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } catch (Exception e) {
                    String msg = "공간 분석 처리 중 예외가 발생했습니다";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } finally {
                    unregister(userLyrVO.getUserId(), userLyrVO.getUserLyrNm());
                }
            });
            return anlsHstryVO;
        } catch (Exception e) {
            unregister(getCurrentUserId(), request.getLayerName());
            throw e;
        }
    }

    /**
     * 범위 요약 분석을 시작한다.
     *
     * @param request 범위 요약 분석 요청 DTO
     * @return 등록된 분석 이력 VO
     */
    @Override
    public AnlsHstryVO startSummarizeWithin(SummarizeWithinRequestDTO request) {

        try {
            assertLayerNameNotDuplicated(getCurrentUserId(), request.getLayerName());

            AnalysisLayerInfoDTO sourceLayerInfoDTO = getAnalysisLayerInfo(request.getSourceLyrId(), request.getSourceLayerType());
            AnalysisLayerInfoDTO boundaryLayerInfoDTO = getAnalysisLayerInfo(request.getBoundaryLyrId(), request.getBoundaryLayerType());

            AnlsHstryVO anlsHstryVO = tx.execute(status -> {
                AnlsHstryVO vo = analysisHistoryService.insertAnlsHstry(AnalysisKind.SUMMARIZE_WITHIN);
                analysisHistoryService.insertAnlsHstryDtl(vo.getAnlsHstryId(), sourceLayerInfoDTO.getLyrNm(), boundaryLayerInfoDTO.getLyrNm(), request.toJsonString());
                return vo;
            });
            assert anlsHstryVO != null;
            final int anlsHstryId = anlsHstryVO.getAnlsHstryId();
            final UserLyrVO userLyrVO = makeUserLayerVO(request);

            executor.submit(() -> {
                try {
                    String sourceSchema;
                    String sourceTable;
                    if (sourceLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = sourceLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        sourceSchema = parts[0];
                        sourceTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + sourceLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    String boundarySchema;
                    String boundaryTable;
                    if (boundaryLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = boundaryLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        boundarySchema = parts[0];
                        boundaryTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + boundaryLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    // 상태 업데이트 : RUNNING
                    tx.execute(status -> {
                        analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.RUNNING, null, null, userLyrVO.getUserId());
                        return null;
                    });

                    // DataStore 생성
                    DataStore sourceDataStore = createDataStore(sourceSchema);
                    DataStore boundaryDataStore;
                    if (sourceSchema.equals(boundarySchema)) {
                        boundaryDataStore = sourceDataStore;
                    } else {
                        boundaryDataStore = createDataStore(boundarySchema);
                    }

                    try {
                        // SimpleFeatureCollection 가져오기
                        SimpleFeatureCollection boundaryFeatureCollection = getSimpleFeatureCollection(boundaryDataStore, boundaryTable, null);
                        SimpleFeatureCollection sourceFeatureCollection = getSimpleFeatureCollection(sourceDataStore, sourceTable, null);

                        // 좌표계를 EPSG:3857로 맞추기
                        boundaryFeatureCollection = CoordinateUtils.reprojectTo3857(boundaryFeatureCollection);
                        sourceFeatureCollection = CoordinateUtils.reprojectTo3857(sourceFeatureCollection);

                        // 분석 실행
                        SummarizeWithinProcessor summarizeWithinProcessor = new SummarizeWithinProcessor();
                        // GroupByField 기능 미구현
                        SummarizeWithinOption summarizeWithinOption = new SummarizeWithinOption(
                                request.getIncludeGeometrySummary(),
                                request.getStatisticsFields(),
                                null);
                        SimpleFeatureCollection analysisResult = summarizeWithinProcessor.run(boundaryFeatureCollection, sourceFeatureCollection, summarizeWithinOption);

                        postSpatialAnalysis(anlsHstryId, analysisResult, userLyrVO);
                    } finally {
                        if (sourceDataStore != null) {
                            sourceDataStore.dispose();
                        }
                        if (boundaryDataStore != null) {
                            boundaryDataStore.dispose();
                        }
                    }
                } catch (TopologyException e) {
                    String msg = "원본 데이터의 지오메트리가 유효하지 않아 분석을 수행할 수 없습니다.";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } catch (Exception e) {
                    String msg = "공간 분석 처리 중 예외가 발생했습니다";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } finally {
                    unregister(userLyrVO.getUserId(), userLyrVO.getUserLyrNm());
                }
            });
            return anlsHstryVO;
        } catch (Exception e) {
            unregister(getCurrentUserId(), request.getLayerName());
            throw e;
        }
    }

    /**
     * 밀도 분석을 시작한다.
     *
     * @param request 밀도 분석 요청 DTO
     * @return 등록된 분석 이력 VO
     */
    @Override
    public AnlsHstryVO startDensity(DensityRequestDTO request) {

        try {
            assertLayerNameNotDuplicated(getCurrentUserId(), request.getLayerName());

            AnalysisLayerInfoDTO sourceLayerInfoDTO = getAnalysisLayerInfo(request.getSourceLyrId(), request.getSourceLayerType());

            AnlsHstryVO anlsHstryVO = tx.execute(status -> {
                AnlsHstryVO vo = analysisHistoryService.insertAnlsHstry(AnalysisKind.DENSITY);
                analysisHistoryService.insertAnlsHstryDtl(vo.getAnlsHstryId(), sourceLayerInfoDTO.getLyrNm(), null, request.toJsonString());
                return vo;
            });
            assert anlsHstryVO != null;
            final int anlsHstryId = anlsHstryVO.getAnlsHstryId();
            final UserLyrVO userLyrVO = makeUserLayerVO(request);

            executor.submit(() -> {
                try {
                    String sourceSchema;
                    String sourceTable;
                    if (sourceLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = sourceLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        sourceSchema = parts[0];
                        sourceTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + sourceLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    // 상태 업데이트 : RUNNING
                    tx.execute(status -> {
                        analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.RUNNING, null, null, userLyrVO.getUserId());
                        return null;
                    });

                    // DataStore 생성
                    DataStore sourceDataStore = createDataStore(sourceSchema);

                    try {
                        // SimpleFeatureCollection 가져오기
                        SimpleFeatureCollection sourceFeatureCollection = getSimpleFeatureCollection(sourceDataStore, sourceTable, null);

                        // 좌표계를 EPSG:3857로 맞추기
                        sourceFeatureCollection = CoordinateUtils.reprojectTo3857(sourceFeatureCollection);

                        // 분석 실행
                        DensityProcessor densityProcessor = new DensityProcessor();

                        DensityOption densityOption = getDensityOption(request, sourceFeatureCollection);
                        SimpleFeatureCollection analysisResult = densityProcessor.run(sourceFeatureCollection, densityOption);

                        postSpatialAnalysis(anlsHstryId, analysisResult, userLyrVO);
                    } finally {
                        if (sourceDataStore != null) {
                            sourceDataStore.dispose();
                        }
                    }
                } catch (TopologyException e) {
                    String msg = "원본 데이터의 지오메트리가 유효하지 않아 분석을 수행할 수 없습니다.";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } catch (Exception e) {
                    String msg = "공간 분석 처리 중 예외가 발생했습니다";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } finally {
                    unregister(userLyrVO.getUserId(), userLyrVO.getUserLyrNm());
                }
            });
            return anlsHstryVO;
        } catch (Exception e) {
            unregister(getCurrentUserId(), request.getLayerName());
            throw e;
        }
    }

    /**
     * 조인 피처 분석을 시작한다.
     *
     * @param request 조인 피처 분석 요청 DTO
     * @return 등록된 분석 이력 VO
     */
    @Override
    public AnlsHstryVO startJoinFeatures(JoinFeaturesRequestDTO request) {

        try {
            assertLayerNameNotDuplicated(getCurrentUserId(), request.getLayerName());

            AnalysisLayerInfoDTO sourceLayerInfoDTO = getAnalysisLayerInfo(request.getSourceLyrId(), request.getSourceLayerType());
            AnalysisLayerInfoDTO joinLayerInfoDTO = getAnalysisLayerInfo(request.getBoundaryLyrId(), request.getBoundaryLayerType());

            /*
             * if (request.getJoinType().equals(Join.Type.OUTER) &&
             * !AnalysisValidationUtils.toGeomFamily(sourceLayerInfoDTO.getSpceTy())
             * .equals(AnalysisValidationUtils.toGeomFamily(JoinLayerInfoDTO.getSpceTy())))
             * { throw new IllegalArgumentException(
             * "OUTER 조인 시에는 Source 레이어와 Join 레이어의 공간 유형이 동일해야 합니다." ); }
             */

            AnlsHstryVO anlsHstryVO = tx.execute(status -> {
                AnlsHstryVO vo = analysisHistoryService.insertAnlsHstry(AnalysisKind.JOIN_FEATURES);
                analysisHistoryService.insertAnlsHstryDtl(vo.getAnlsHstryId(), sourceLayerInfoDTO.getLyrNm(), joinLayerInfoDTO.getLyrNm(), request.toJsonString());
                return vo;
            });
            assert anlsHstryVO != null;
            final int anlsHstryId = anlsHstryVO.getAnlsHstryId();
            final UserLyrVO userLyrVO = makeUserLayerVO(request);

            // joinGeometry는 선택사항 (null이면 전체 영역 조인)
            Geometry joinGeometry = null;
            if (request.getJoinGeometry() != null && !request.getJoinGeometry().trim().isEmpty()) {
                try {
                    joinGeometry = new WKTReader().read(request.getJoinGeometry());
                    joinGeometry = CoordinateUtils.reprojectFrom3857(joinGeometry, joinLayerInfoDTO.getCntm());
                } catch (ParseException e) {
                    throw new IllegalArgumentException("유효하지 않은 WKT 형식입니다: " + request.getJoinGeometry(), e);
                } catch (FactoryException | TransformException e) {
                    throw new IllegalArgumentException(e.getMessage(), e);
                }
            }
            final Geometry finalJoinGeometry = joinGeometry;

            executor.submit(() -> {
                try {
                    String sourceSchema;
                    String sourceTable;
                    if (sourceLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = sourceLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        sourceSchema = parts[0];
                        sourceTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + sourceLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    String boundarySchema;
                    String boundaryTable;
                    if (joinLayerInfoDTO.getLyrPhysNm().contains(".")) {
                        String[] parts = joinLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
                        boundarySchema = parts[0];
                        boundaryTable = parts[1];
                    } else {
                        throw new IllegalArgumentException("잘못된 레이어 물리명 형식입니다: '" + joinLayerInfoDTO.getLyrPhysNm() + "'. 기대되는 형식은 <스키마명>.<테이블명> 입니다.");
                    }

                    // 상태 업데이트 : RUNNING
                    tx.execute(status -> {
                        analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.RUNNING, null, null, userLyrVO.getUserId());
                        return null;
                    });

                    // DataStore 생성
                    DataStore sourceDataStore = createDataStore(sourceSchema);
                    DataStore boundaryDataStore;
                    if (sourceSchema.equals(boundarySchema)) {
                        boundaryDataStore = sourceDataStore;
                    } else {
                        boundaryDataStore = createDataStore(boundarySchema);
                    }

                    try {
                        // 조인 레이어의 SimpleFeatureCollection 가져오기
                        // geometry가 지정된 경우에만 필터 적용
                        Filter filter = null;
                        if (finalJoinGeometry != null) {
                            String cqlFilter = "INTERSECTS(" + GEOMETRY_COLUMN + ", " + finalJoinGeometry.toText() + ")";
                            filter = CQL.toFilter(cqlFilter);
                        }
                        SimpleFeatureCollection boundaryFeatureCollection = getSimpleFeatureCollection(boundaryDataStore, boundaryTable, filter);

                        // 소스 레이어의 SimpleFeatureCollection 가져오기 (전체 데이터)
                        SimpleFeatureCollection sourceFeatureCollection = getSimpleFeatureCollection(sourceDataStore, sourceTable, null);

                        // 좌표계를 EPSG:3857로 맞추기
                        boundaryFeatureCollection = CoordinateUtils.reprojectTo3857(boundaryFeatureCollection);
                        sourceFeatureCollection = CoordinateUtils.reprojectTo3857(sourceFeatureCollection);

                        // 분석 실행
                        JoinFeaturesProcessor JoinFeaturesProcessor = new JoinFeaturesProcessor();
                        JoinFeaturesOption JoinFeaturesOption = new JoinFeaturesOption(
                                request.getInputField(),
                                request.getJoinField(),
                                request.getJoinType());
                        SimpleFeatureCollection analysisResult = JoinFeaturesProcessor.run(boundaryFeatureCollection, sourceFeatureCollection, JoinFeaturesOption);

                        postSpatialAnalysis(anlsHstryId, analysisResult, userLyrVO);
                    } finally {
                        if (sourceDataStore != null) {
                            sourceDataStore.dispose();
                        }
                        if (boundaryDataStore != null) {
                            boundaryDataStore.dispose();
                        }
                    }
                } catch (TopologyException e) {
                    String msg = "원본 데이터의 지오메트리가 유효하지 않아 분석을 수행할 수 없습니다.";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } catch (Exception e) {
                    String msg = "공간 분석 처리 중 예외가 발생했습니다";
                    logger.error(msg, e);
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, msg, null, userLyrVO.getUserId());
                } finally {
                    unregister(userLyrVO.getUserId(), userLyrVO.getUserLyrNm());
                }
            });
            return anlsHstryVO;
        } catch (Exception e) {
            unregister(getCurrentUserId(), request.getLayerName());
            throw e;
        }
    }

    private static DensityOption getDensityOption(DensityRequestDTO request, SimpleFeatureCollection sourceFeatureCollection) {
        ReferencedEnvelope boundingBox = sourceFeatureCollection.getBounds();
        double cellSize = Math.min(boundingBox.getWidth(), boundingBox.getHeight()) / 250.0;
        double searchRadius = Math.min(boundingBox.getWidth(), boundingBox.getHeight()) / 30.0;
        return new DensityOption(
                request.getKernelType(),
                request.getPopulationField(),
                cellSize,
                searchRadius,
                boundingBox,
                request.getMethod(),
                request.getNumClasses(),
                request.getNoData(),
                request.getDissolve()
        );
    }

    /**
     * 공간 분석 수행 후 후처리
     *
     * @param request 분석 요청 DTO
     * @return 사용자 레이어 VO
     */
    private UserLyrVO makeUserLayerVO(AbstractAnalysisRequestDTO request) {
        UserLyrVO userLyrVO = new UserLyrVO();
        userLyrVO.setUserId(getCurrentUserId());
        userLyrVO.setUserLyrNm(request.getLayerName());
        userLyrVO.setUserLyrExpln(request.getLayerExpln());
        userLyrVO.setLyrSrvcPrefix(prefix);
        userLyrVO.setLyrGroupCd(request.getLayerGroupCode());
        userLyrVO.setCntm((short) 3857);
        userLyrVO.setCrtId(getCurrentUserId());
        userLyrVO.setChgId(getCurrentUserId());

        return userLyrVO;
    }

    /**
     * 공간 분석 수행 후 후처리
     *
     * @param anlsHstryId    분석 이력 ID
     * @param analysisResult 공간 분석 결과 FeatureCollection
     */
    private void postSpatialAnalysis(int anlsHstryId, SimpleFeatureCollection analysisResult, UserLyrVO userLyrVO) throws Exception {

        if (analysisResult.isEmpty()) {
            throw new IllegalStateException("분석 결과가 비어 있습니다, 분석 옵션을 조정한 뒤 다시 시도해 주세요.");
        }

        String tableName = tx.execute(status -> {
            // Batch Mapper 사용
            String tb = createAnalysisTable(anlsHstryId, analysisResult);
            createGeometryIdIndex(RESULT_SCHEMA, tb, GEOMETRY_ID);
            createSpatialIndex(RESULT_SCHEMA, tb, GEOMETRY_COLUMN);
            return tb;
        });
        try {
            tx.execute(status -> {
                userLyrVO.setLyrSrvcNm(tableName);
                userLyrVO.setLyrPhysNm(RESULT_SCHEMA + "." + tableName);
                userLyrVO.setSpceTy(toGeomSubtype(analysisResult.getSchema().getGeometryDescriptor().getType().getBinding()));
                int userLyrId = insertUserLayer(userLyrVO);
                userLyrVO.setUserLyrId(userLyrId);
                return null;
            });
        } catch (Exception e) {
            logger.error("사용자 레이어 등록 실패", e);
            dropAnalysisTableQuietly(tableName);
            throw e;
        }


        try {
            GisServerRestUtils gisServerUtils = new GisServerRestUtils(restTemplate, baseUrl);
            boolean ok = gisServerUtils.publishLayer(prefix, tableName, RESULT_SCHEMA, tableName, 3857, null, null, null, null);
            if (ok) {
                tx.execute(s -> {
                    analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.SUCCESS, null, userLyrVO.getUserLyrId(), userLyrVO.getUserId());
                    return null;
                });
            } else {
                compensate(tableName, anlsHstryId, userLyrVO.getUserLyrId(), "레이어 등록 실패", userLyrVO.getUserId());
                throw new Exception("레이어 등록 실패");
            }
        } catch (Exception e) {
            compensate(tableName, anlsHstryId, userLyrVO.getUserLyrId(), "레이어 등록 실패", userLyrVO.getUserId());
            throw e;
        }
    }

    private void compensate(String tableName, int anlsHstryId, int userLyrId, String reason, String userId) {
        tx.execute(s -> {
            dropAnalysisTableQuietly(tableName);
            deleteUserLayer(userLyrId);
            analysisHistoryService.updateAnlsHstry(anlsHstryId, AnalysisStatus.FAILED, reason, null, userId);
            return null;
        });
    }

    /**
     * 분석 레이어를 삭제한다.
     *
     * @param tableName 테이블 이름
     */
    private void dropAnalysisTableQuietly(String tableName) {
        try {
            mapper.dropTableIfExists(RESULT_SCHEMA, tableName);
            logger.info("dropped table {}.{}", RESULT_SCHEMA, tableName);
        } catch (Exception e) {
            logger.error("failed to drop table {}.{}: {}", RESULT_SCHEMA, tableName, e.getMessage(), e);
        }
    }

    /**
     * 레이어 ID와 타입을 기반으로 분석 대상 레이어 정보를 조회한다.
     *
     * @param layerId   레이어 ID
     * @param layerType 레이어 타입 (TASK/USER)
     * @return 분석 레이어 정보 DTO
     */
    private AnalysisLayerInfoDTO getAnalysisLayerInfo(int layerId, LayerType layerType) {
        AnalysisLayerInfoDTO analysisLayerInfoDTO = null;
        if (layerType.equals(LayerType.TASK)) {
            analysisLayerInfoDTO = mapper.selectTaskLayerInfo(layerId);
        } else {
            analysisLayerInfoDTO = mapper.selectUserLayerInfo(layerId);
        }
        if (analysisLayerInfoDTO == null) {
            throw new IllegalArgumentException(String.format("레이어 정보를 찾을 수 없습니다. layerId=%d, layerType=%s", layerId, layerType));
        }
        return analysisLayerInfoDTO;
    }

    /**
     * PostGIS DataStore를 생성한다.
     *
     * @return DataStore 인스턴스
     * @throws IOException 생성 실패 시 발생
     */
    private DataStore createDataStore(String schema) throws IOException {
        Map<String, Object> params = new HashMap<>();
        params.put(PostgisNGDataStoreFactory.DBTYPE.key, "postgis");
        params.put(PostgisNGDataStoreFactory.DATASOURCE.key, dataSource);
        params.put(PostgisNGDataStoreFactory.SCHEMA.key, schema);
        PostgisNGDataStoreFactory factory = new PostgisNGDataStoreFactory();
        JDBCDataStore jdbcDataStore = factory.createDataStore(params);
        ;
        jdbcDataStore.setExposePrimaryKeyColumns(true);
        return jdbcDataStore;
    }

    /**
     * SimpleFeatureCollection을 조회한다.
     *
     * @param table  조회할 테이블 이름
     * @param filter 적용할 필터 (null이면 전체 조회)
     * @return 조회된 SimpleFeatureCollection
     * @throws IOException 조회 실패 시 발생
     */
    private SimpleFeatureCollection getSimpleFeatureCollection(DataStore dataStore, String table, Filter filter) throws IOException {
        SimpleFeatureSource featureSource = dataStore.getFeatureSource(table);
        Query query = new Query(table, filter != null ? filter : Filter.INCLUDE);
        return featureSource.getFeatures(query);
    }

    /**
     * 분석 결과(SimpleFeatureCollection)를 기반으로 결과 테이블을 생성한다.
     *
     * @param anlsHstryId 분석 이력 ID
     * @param result      분석 결과 FeatureCollection
     */
    private String createAnalysisTable(int anlsHstryId, SimpleFeatureCollection result) {
        final String tableName = createAnalysisTableName(anlsHstryId);

        final SimpleFeatureType type = result.getSchema();
        batchMapper.createTable(type, RESULT_SCHEMA, tableName, GEOMETRY_ID, GEOMETRY_COLUMN, toGeomSubtype(type.getGeometryDescriptor().getType().getBinding()), 3857);

        final List<AttributeDescriptor> nonGeomDescs = new ArrayList<>();
        for (AttributeDescriptor d : type.getAttributeDescriptors()) {
            if (!Geometry.class.isAssignableFrom(d.getType().getBinding())) {
                nonGeomDescs.add(d);
            }
        }

        final WKBWriter wkbWriter = new WKBWriter();
        List<FeatureRowDTO> rows = new ArrayList<>(CHUNK_SIZE);

        try (SimpleFeatureIterator it = result.features()) {
            while (it.hasNext()) {
                SimpleFeature f = it.next();

                Geometry geom = (Geometry) f.getDefaultGeometry();
                byte[] wkb = (geom != null) ? wkbWriter.write(geom) : null;

                List<Object> attrs = new ArrayList<>(nonGeomDescs.size());
                for (AttributeDescriptor d : nonGeomDescs) {
                    attrs.add(f.getAttribute(d.getLocalName()));
                }

                rows.add(new FeatureRowDTO(wkb, attrs));

                if (rows.size() >= CHUNK_SIZE) {
                    flushChunk(rows, type, RESULT_SCHEMA, tableName);
                }
            }
        }

        if (!rows.isEmpty()) {
            flushChunk(rows, type, RESULT_SCHEMA, tableName);
        }

        return tableName;
    }

    /**
     * 유니크 인덱스 생성
     *
     * @param schema 스키마명
     * @param table  테이블명
     * @param column 유니크 제약을 걸 컬럼명
     */
    private void createGeometryIdIndex(String schema, String table, String column) {
        String idxName = ("ux_" + table + "_" + column).toLowerCase();
        batchMapper.createUniqueIndex(schema, table, column, idxName);
    }

    /**
     * 공간 인덱스(GiST) 생성
     *
     * @param schema     스키마명
     * @param table      테이블명
     * @param geomColumn 지오메트리 컬럼명
     */
    private void createSpatialIndex(String schema, String table, String geomColumn) {
        String idxName = ("sx_" + table + "_" + geomColumn).toLowerCase();
        batchMapper.createSpatialIndex(schema, table, geomColumn, idxName);
    }

    /**
     * 배치 버퍼(rows)를 한 번에 DB로 밀어 넣고, 원본 버퍼를 비운다.
     * <p>
     * 파라미터
     *
     * @param rows       배치 적재 대기 중인 레코드 버퍼(가변 리스트)
     * @param type       삽입 대상 테이블에 매핑할 SimpleFeatureType(스키마/컬럼 메타)
     * @param schemaName 대상 스키마명
     * @param tableName  대상 테이블명
     */
    private void flushChunk(List<FeatureRowDTO> rows, SimpleFeatureType type, String schemaName, String tableName) {
        if (rows == null || rows.isEmpty()) return;
        List<FeatureRowDTO> snapshot = new ArrayList<>(rows);
        for (FeatureRowDTO feature : snapshot) {
            batchMapper.rowInsert(feature, type, schemaName, tableName);
        }
        rows.clear();
        batchSqlSessionTemplate.flushStatements();
    }

    /**
     * 분석 결과 테이블 이름을 생성한다.
     *
     * @param anlsHstryId 분석 이력 ID
     * @return "ul_" prefix를 가진 고유 테이블 이름
     */
    private String createAnalysisTableName(int anlsHstryId) {
        final String TABLE_PREFIX = "ul_";

        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] digest = md.digest(
                    String.valueOf(anlsHstryId).getBytes(StandardCharsets.UTF_8)
            );
            StringBuilder hex = new StringBuilder();
            for (int i = 0; i < 4; i++) {
                hex.append(String.format("%02x", digest[i]));
            }
            return TABLE_PREFIX + hex.toString();
        } catch (NoSuchAlgorithmException e) {
            return TABLE_PREFIX + UUID.randomUUID().toString()
                    .replace("-", "")
                    .substring(0, 8);
        }
    }

    /**
     * 현재 로그인된 사용자 ID를 조회한다.
     *
     * @return 사용자 ID, 없으면 null
     */
    private String getCurrentUserId() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.getPrincipal() instanceof LoginVO loginVO) {
            return loginVO.getUserId();
        }
        return null;
    }

    /**
     * 사용자 레이어를 등록한다.
     *
     * @param userLyrVO 사용자 레이어 VO
     */
    private int insertUserLayer(UserLyrVO userLyrVO) {
        mapper.insertUserLayer(userLyrVO);
        return userLyrVO.getUserLyrId();
    }

    /**
     * 사용자 레이어를 삭제한다.
     *
     * @param userLyrId 사용자 레이어 ID
     */
    private void deleteUserLayer(int userLyrId) {
        mapper.deleteUserLayer(userLyrId);
    }

    /**
     * 사용자 레이어명 중복 검사.
     *
     * @param userId    현재 사용자 ID
     * @param layerName 만들려는 레이어 표시명
     * @throws IllegalArgumentException 파라미터가 비었을 때
     * @throws IllegalStateException    동일 이름이 이미 존재할 때
     */
    private void assertLayerNameAvailableOrThrow(String userId, String layerName) {
        if (userId == null || userId.isBlank()) {
            throw new IllegalArgumentException("사용자 ID가 비어있습니다.");
        }
        if (layerName == null || layerName.isBlank()) {
            throw new IllegalArgumentException("레이어 이름이 비어있습니다.");
        }

        String normalized = layerName.trim();
        UserLyrVO existing = mapper.selectUserLayer(userId, normalized);
        if (existing != null) {
            throw new IllegalStateException("이미 존재하는 레이어명입니다: '" + normalized + "'");
        }
    }

    /**
     * 리플리카(읽기 노드)에서 새로 생성된 테이블이 보일 때까지 대기한다.
     *
     * @param schema          스키마명
     * @param table           테이블명
     * @param timeout         전체 대기 타임아웃 (예: 10초)
     * @param initialInterval 최초 재시도 간격 (예: 150ms)
     * @param maxInterval     재시도 간격 상한 (예: 1초)
     */
    private void waitUntilTableVisible(String schema, String table, Duration timeout, Duration initialInterval, Duration maxInterval) throws InterruptedException {
        long deadline = System.nanoTime() + timeout.toNanos();
        long interval = initialInterval.toMillis();

        while (System.nanoTime() < deadline) {
            try {
                if (mapper.existsTable(schema, table)) {
                    return;
                }
            } catch (Exception e) {
                logger.debug("existsTable check failed: {}.{} ({})", schema, table, e.getMessage());
            }

            long jitter = ThreadLocalRandom.current().nextLong(Math.max(1, interval / 5));
            Thread.sleep(Math.min(interval + jitter, maxInterval.toMillis()));
            interval = Math.min(interval * 2, maxInterval.toMillis()); // 2배씩 증가
        }
        throw new IllegalStateException("Replica still doesn't see table: " + schema + "." + table + " within " + timeout.toMillis() + "ms");
    }

    /**
     * 사용자별 레이어 이름 중복 체크
     *
     * @param userId    사용자 ID
     * @param layerName 레이어 이름
     */
    private void assertLayerNameNotDuplicated(String userId, String layerName) {
        if (!registerIfAbsent(userId, layerName)) {
            throw new ResponseStatusException(HttpStatus.CONFLICT, "이미 진행 중인 레이어명입니다: '" + layerName + "'");
        }

        try {
            assertLayerNameAvailableOrThrow(userId, layerName);
        } catch (IllegalStateException e) {
            unregister(userId, layerName);
            throw new ResponseStatusException(HttpStatus.CONFLICT, "이미 존재하는 레이어명입니다: '" + layerName + "'");
        }
    }

    /**
     * 특정 사용자의 레이어 이름을 등록합니다.
     *
     * @param userId    사용자 ID
     * @param layerName 레이어 이름
     * @return 등록 성공 여부 (true: 등록됨, false: 이미 존재)
     */
    public boolean registerIfAbsent(String userId, String layerName) {
        registry.putIfAbsent(userId, new ConcurrentSkipListSet<>());
        Set<String> userSet = registry.get(userId);
        synchronized (userSet) {
            if (userSet.contains(layerName)) {
                return false;
            }
            userSet.add(layerName);
            return true;
        }
    }

    /**
     * 특정 사용자의 레이어 이름 등록을 해제합니다.
     *
     * @param userId    사용자 ID
     * @param layerName 제거할 레이어 이름
     */
    public void unregister(String userId, String layerName) {
        Set<String> userSet = registry.get(userId);
        if (userSet != null) {
            synchronized (userSet) {
                userSet.remove(layerName);
                if (userSet.isEmpty()) {
                    registry.remove(userId);
                }
            }
        }
    }

    /**
     * 테이블의 컬럼 이름 목록을 조회합니다.
     *
     * @param schema    스키마 이름
     * @param tableName 테이블 이름
     */
    private List<String> getColumnNames(String schema, String tableName) {
        return mapper.getColumnNames(schema, tableName);
    }

    /**
     * 특정 스키마/테이블에 지정한 컬럼명이 존재하는지 확인한다.
     *
     * @param analysisLayerInfoDTO 스키마.테이블 정보를 가진 DTO
     * @param columnName           검사할 컬럼명
     * @return 컬럼이 존재하면 true, 존재하지 않으면 false
     */
    private boolean isColumnExists(AnalysisLayerInfoDTO analysisLayerInfoDTO, String columnName) {

        String schema;
        String tableName;
        if (analysisLayerInfoDTO.getLyrPhysNm().contains(".")) {
            String[] parts = analysisLayerInfoDTO.getLyrPhysNm().split("\\.", 2);
            schema = parts[0];
            tableName = parts[1];
        } else {
            throw new IllegalArgumentException(
                    "잘못된 물리명 형식입니다: '" + analysisLayerInfoDTO.getLyrPhysNm()
                            + "'. 형식은 <스키마명>.<테이블명> 이어야 합니다."
            );
        }
        List<String> columnNames = getColumnNames(schema, tableName);

        for (String name : columnNames) {
            if (name.equalsIgnoreCase(columnName)) {
                return true;
            }
        }
        return false;
    }
}
