diff --git a/src/main/java/com/kamco/cd/kamcoback/common/download/RangeDownloadResponder.java b/src/main/java/com/kamco/cd/kamcoback/common/download/RangeDownloadResponder.java
index 01cd0d24..755b62a5 100644
--- a/src/main/java/com/kamco/cd/kamcoback/common/download/RangeDownloadResponder.java
+++ b/src/main/java/com/kamco/cd/kamcoback/common/download/RangeDownloadResponder.java
@@ -14,23 +14,56 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
+/**
+ * Range(부분 다운로드) 지원 파일 다운로드 응답 생성기.
+ *
+ *
브라우저/다운로드 매니저가 Range 헤더를 보내면 206 Partial Content로 일부 구간만 내려주고, Range 헤더가 없으면 200 OK로 전체 파일을
+ * 내려준다.
+ *
+ *
대용량 ZIP(또는 바이너리) 파일 다운로드 시: - 메모리에 파일 전체를 올리지 않고(Resource/FileSystemResource 스트리밍) -
+ * 이어받기(Resume) 및 병렬 다운로드(일부 클라이언트) 지원 - 잘못된 Range에 대해 416 Range Not Satisfiable 처리
+ */
@Component
public class RangeDownloadResponder {
+ /**
+ * ZIP(또는 바이너리) 파일 다운로드 응답을 생성한다.
+ *
+ * @param filePath 실제 서버 파일 경로
+ * @param downloadFileName 사용자에게 노출될 다운로드 파일명
+ * @param request Range 헤더 확인용 HttpServletRequest
+ * @return Range 유무에 따라 200(전체) 또는 206(부분) ResponseEntity 반환
+ * @throws IOException 파일 접근/조회 실패 시
+ */
public ResponseEntity> buildZipResponse(
Path filePath, String downloadFileName, HttpServletRequest request) throws IOException {
+ // 1) 파일 존재/정상 파일 여부 확인
+ // - 일반 파일(regular file)이 아니면 404 반환 (디렉토리/없는 파일/특수 파일 등 방지)
if (!Files.isRegularFile(filePath)) {
return ResponseEntity.notFound().build();
}
+ // 2) 파일 전체 크기 및 Spring Resource 래핑
+ // - Files.size: 전체 파일 크기(Content-Range 계산/검증에 필요)
+ // - FileSystemResource: 스프링이 스트리밍 형태로 파일을 응답 바디로 내려줄 수 있게 함
long totalSize = Files.size(filePath);
Resource resource = new FileSystemResource(filePath);
+ // 3) 다운로드 강제(Content-Disposition)
+ // - attachment; filename="xxx.zip" 형태로 브라우저가 저장 대화상자/다운로드로 처리
String disposition = "attachment; filename=\"" + downloadFileName + "\"";
+
+ // 4) Range 헤더 조회
+ // - Range: bytes=0-1023 (일부 구간 요청)
+ // - Range가 없으면 전체 다운로드로 처리
String rangeHeader = request.getHeader(HttpHeaders.RANGE);
- // 🔥 공통 헤더 (여기 고정)
+ // 5) 공통 헤더(전체/부분 다운로드 공통으로 넣을 것)
+ // - Content-Type: 바이너리(필요시 application/zip 으로 바꿔도 됨)
+ // - Content-Disposition: 다운로드 강제
+ // - Accept-Ranges: bytes -> 서버가 Range(이어받기/부분요청) 지원함을 알림
+ // - X-Accel-Buffering: no -> Nginx 사용 시 버퍼링 비활성화(스트리밍/대용량에 유리)
ResponseEntity.BodyBuilder base =
ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
@@ -38,10 +71,15 @@ public class RangeDownloadResponder {
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
.header("X-Accel-Buffering", "no");
+ // 6) Range 헤더가 없으면 전체 파일 반환 (200 OK)
if (rangeHeader == null || rangeHeader.isBlank()) {
+ // Content-Length를 전체 크기로 설정하고 Resource를 그대로 바디에 담아 스트리밍
return base.contentLength(totalSize).body(resource);
}
+ // 7) Range 헤더 파싱
+ // - 잘못된 Range 헤더 형식이면 parseRanges에서 IllegalArgumentException 발생 가능
+ // - RFC에 따라 416 응답 + Content-Range: bytes */{total} 형태로 알려줌
List ranges;
try {
ranges = HttpRange.parseRanges(rangeHeader);
@@ -52,11 +90,18 @@ public class RangeDownloadResponder {
.build();
}
+ // 8) 다중 Range 요청이 와도(예: bytes=0-99,200-299) 여기서는 첫 번째 Range만 처리
+ // - 대부분 브라우저는 단일 Range를 사용
+ // - 멀티파트/byteranges 처리를 하려면 별도 구현 필요
HttpRange range = ranges.get(0);
+ // 9) 실제 시작/끝 범위 계산
+ // - bytes=500- : end를 파일 끝으로 해석
+ // - bytes=-500 : 마지막 500바이트로 해석
long start = range.getRangeStart(totalSize);
long end = range.getRangeEnd(totalSize);
+ // 10) 시작점이 파일 크기 이상이면 만족 불가 -> 416
if (start >= totalSize) {
return ResponseEntity.status(416)
.header(HttpHeaders.CONTENT_RANGE, "bytes */" + totalSize)
@@ -64,9 +109,18 @@ public class RangeDownloadResponder {
.build();
}
+ // 11) 요청 구간 길이 계산
+ // - end/start는 inclusive이므로 +1 필요
long regionLength = end - start + 1;
+
+ // 12) ResourceRegion 생성
+ // - resource의 start부터 regionLength 만큼만 응답으로 내려줄 수 있게 함
+ // - 파일 전체를 메모리에 올리지 않고 필요한 부분만 스트리밍
ResourceRegion region = new ResourceRegion(resource, start, regionLength);
+ // 13) 206 Partial Content로 응답 구성
+ // - Content-Range: bytes start-end/total
+ // - Content-Length: regionLength(부분 크기)
return ResponseEntity.status(206)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, disposition)
diff --git a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java
index 1db8aec8..83585d18 100644
--- a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java
+++ b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java
@@ -111,7 +111,6 @@ public class SecurityConfig {
.requestMatchers(
"/api/user/**",
"/api/my/menus",
- "/api/members/*/password",
"/api/training-data/label/**",
"/api/training-data/review/**")
.authenticated()
diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java b/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java
index 7356469f..13ee5d54 100644
--- a/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java
+++ b/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java
@@ -340,6 +340,7 @@ public class InferenceResultApiController {
}
/** 추론결과 화면에서 호출 */
+ /** 다운로드는 a 링크로 받는걸로 변경되어 사번을 파라미터로 받아서 로그에 저장하는걸로 변경함 */
@Operation(summary = "shp 파일 다운로드", description = "추론관리 분석결과 shp 파일 다운로드")
@ApiResponses(
value = {
@@ -373,7 +374,6 @@ public class InferenceResultApiController {
Path zipPath = Path.of(path);
- // Range + 200/206/416 공통 처리 (추가 헤더 포함)
return rangeDownloadResponder.buildZipResponse(zipPath, uid + ".zip", request);
}
diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java b/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java
index 0d4011f9..4091b7d2 100644
--- a/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java
+++ b/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java
@@ -780,11 +780,17 @@ public class InferenceResultService {
return inferenceResultCoreService.listGetScenes5k(id);
}
+ /**
+ * 추론 서버 현황 cpu, gpu 확인
+ *
+ * @return 서버 정보
+ */
public List getInferenceServerStatusList() {
String[] serverNames = inferenceServerName.split(",");
int serveCnt = serverNames.length;
+ // 서버정보 조회
List dtoList =
inferenceResultCoreService.getInferenceServerStatusList();
int size = dtoList.size();
@@ -794,6 +800,7 @@ public class InferenceResultService {
System.out.println("size =" + size);
if (size == 0) {
+ // 서버 정보가 없을때
for (int k = 0; k < serveCnt; k++) {
InferenceServerStatusDto dto = new InferenceServerStatusDto();
dto.setServerName(serverNames[k]);
@@ -861,17 +868,35 @@ public class InferenceResultService {
return inferenceResultCoreService.getInferenceResultInfo(uuid);
}
+ /**
+ * 분류별 탐지건수 조회
+ *
+ * @param uuid 추론 uuid
+ * @return 분류별 탐지건수 정보
+ */
public List getInferenceClassCountList(UUID uuid) {
return inferenceResultCoreService.getInferenceClassCountList(uuid);
}
+ /**
+ * 추론결과 geom 목록 조회
+ *
+ * @param uuid 추론 uuid
+ * @param searchGeoReq 추론 결과 상세화면 geom 조회 조건
+ * @return geom 목록 정보
+ */
public Page getInferenceGeomList(UUID uuid, SearchGeoReq searchGeoReq) {
return inferenceResultCoreService.getInferenceGeomList(uuid, searchGeoReq);
}
- /** 추론 종료 */
+ /**
+ * 추론 종료
+ *
+ * @return 호출한 uuid
+ */
@Transactional
public UUID deleteInferenceEnd() {
+ // 추론 진행중인지 확인
SaveInferenceAiDto dto = inferenceResultCoreService.getProcessing();
if (dto == null) {
throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND);
@@ -883,13 +908,15 @@ public class InferenceResultService {
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
+ // 종료 api 호출
ExternalCallResult result =
- externalHttpClient.call(url, HttpMethod.DELETE, dto, headers, String.class);
+ externalHttpClient.callLong(url, HttpMethod.DELETE, dto, headers, String.class);
if (!result.success()) {
throw new CustomApiException("BAD_GATEWAY", HttpStatus.BAD_GATEWAY);
}
+ // 추론 정보 테이블 update
SaveInferenceAiDto request = new SaveInferenceAiDto();
request.setStatus(Status.FORCED_END.getId());
request.setUuid(dto.getUuid());
@@ -910,8 +937,11 @@ public class InferenceResultService {
* @return 32자 추론 uid, shp 파일 경로
*/
public Map shpDownloadPath(UUID uuid) {
+ // 추론정보 조회
InferenceLearnDto dto = inferenceResultCoreService.getInferenceUid(uuid);
String uid = dto.getUid();
+
+ // 파일 경로 생성
Path path = Path.of(datasetDir).resolve(uid).resolve("merge").resolve(uid + ".zip");
Map downloadMap = new HashMap<>();
diff --git a/src/main/java/com/kamco/cd/kamcoback/mapsheet/service/MapSheetMngService.java b/src/main/java/com/kamco/cd/kamcoback/mapsheet/service/MapSheetMngService.java
index 383584f6..7e742d7e 100644
--- a/src/main/java/com/kamco/cd/kamcoback/mapsheet/service/MapSheetMngService.java
+++ b/src/main/java/com/kamco/cd/kamcoback/mapsheet/service/MapSheetMngService.java
@@ -387,6 +387,8 @@ public class MapSheetMngService {
}
/**
+ * 연도 목록 조회
+ *
* @return
*/
public List findMapSheetMngDoneYyyyList() {
diff --git a/src/main/java/com/kamco/cd/kamcoback/model/service/ModelMngService.java b/src/main/java/com/kamco/cd/kamcoback/model/service/ModelMngService.java
index ae3e2ff7..6818158b 100644
--- a/src/main/java/com/kamco/cd/kamcoback/model/service/ModelMngService.java
+++ b/src/main/java/com/kamco/cd/kamcoback/model/service/ModelMngService.java
@@ -41,6 +41,16 @@ public class ModelMngService {
@Value("${file.pt-FileName}")
private String ptFileName;
+ /**
+ * 모델조회
+ *
+ * @param searchReq 페이징
+ * @param startDate 시작날짜
+ * @param endDate 종료날짜
+ * @param modelType 모델 타입 G1, G2, G3
+ * @param searchVal 모델 ver
+ * @return 모델 목록
+ */
public Page findModelMgmtList(
ModelMngDto.searchReq searchReq,
LocalDate startDate,
diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java
index cdcb9d1b..a65930bc 100644
--- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java
+++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java
@@ -453,17 +453,30 @@ public class InferenceResultCoreService {
* @return
*/
public AnalResultInfo getInferenceResultInfo(UUID uuid) {
+ // 추론 결과 정보조회
AnalResultInfo resultInfo = mapSheetLearnRepository.getInferenceResultInfo(uuid);
+ // bbox, point 조회
BboxPointDto bboxPointDto = mapSheetLearnRepository.getBboxPoint(uuid);
resultInfo.setBboxGeom(bboxPointDto.getBboxGeom());
resultInfo.setBboxCenterPoint(bboxPointDto.getBboxCenterPoint());
return resultInfo;
}
+ /**
+ * 분류별 탐지건수 조회
+ *
+ * @param uuid 추론 uuid
+ * @return 분류별 탐지건수 정보
+ */
public List getInferenceClassCountList(UUID uuid) {
return mapSheetLearnRepository.getInferenceClassCountList(uuid);
}
+ /**
+ * @param uuid 추론 uuid
+ * @param searchGeoReq 추론 결과 상세화면 geom 조회 조건
+ * @return geom 목록 정보
+ */
public Page getInferenceGeomList(UUID uuid, SearchGeoReq searchGeoReq) {
return mapSheetLearnRepository.getInferenceGeomList(uuid, searchGeoReq);
}
diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MapSheetMngCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MapSheetMngCoreService.java
index 3b1670ec..4222092f 100644
--- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MapSheetMngCoreService.java
+++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MapSheetMngCoreService.java
@@ -291,40 +291,6 @@ public class MapSheetMngCoreService {
return sceneInference;
}
- /**
- * 변화탐지 실행 가능 기준 년도 조회
- *
- * @param compareYear 비교 연도
- * @param targetYear 기준 연도
- * @param mapSheetScope 부분실행, 전체실행
- * @param mapSheetNums 도엽번호 목록
- * @return 실행가능한 도엽번호 목록
- */
- public List findExecutableSheets(
- Integer compareYear, Integer targetYear, String mapSheetScope, List mapSheetNums) {
- return mapSheetMngRepository.findExecutableSheets(
- compareYear, targetYear, mapSheetScope, mapSheetNums);
- }
-
- public List fetchBaseWithCompare(
- Integer compareYear, Integer targetYear, String mapSheetScope, List mapSheetNums) {
- return mapSheetMngRepository.fetchBaseWithCompare(
- compareYear, targetYear, mapSheetScope, mapSheetNums);
- }
-
- /**
- * 실행가능 도엽 count
- *
- * @param year 연도
- * @param mapSheetScope 부분, 전체 구분
- * @param mapSheetNums 도엽 목록
- * @return count
- */
- public int countExecutableSheetsDistinct(
- Integer year, String mapSheetScope, List mapSheetNums) {
- return mapSheetMngRepository.countExecutableSheetsDistinct(year, mapSheetScope, mapSheetNums);
- }
-
public void updateMapSheetMngHstUploadId(Long hstUid, UUID uuid, String uploadId) {
mapSheetMngRepository.updateMapSheetMngHstUploadId(hstUid, uuid, uploadId);
}
diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/ModelMngCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/ModelMngCoreService.java
index 989e4fba..f1ba67e6 100644
--- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/ModelMngCoreService.java
+++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/ModelMngCoreService.java
@@ -17,6 +17,16 @@ public class ModelMngCoreService {
private final ModelMngRepository modelMngRepository;
+ /**
+ * 모델조회
+ *
+ * @param searchReq 페이징
+ * @param startDate 시작날짜
+ * @param endDate 종료날짜
+ * @param modelType 모델 타입 G1, G2, G3
+ * @param searchVal 모델 ver
+ * @return 모델 목록
+ */
public Page findModelMgmtList(
ModelMngDto.searchReq searchReq,
LocalDate startDate,
diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryCustom.java
index 3dd13cae..a9cbcc15 100644
--- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryCustom.java
+++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryCustom.java
@@ -62,40 +62,6 @@ public interface MapSheetMngRepositoryCustom {
List findByHstUidMapSheetFileList(Long hstUid);
- /**
- * 추론 실행 가능 도엽 조회 (추론 제외)
- *
- * @param compareYear 비교 연도
- * @param targetYear 기준 연도
- * @param mapSheetScope 부분실행, 전체실행
- * @param mapSheetNums 도엽번호 목록
- * @return 실행가능한 도엽번호 목록
- */
- List findExecutableSheets(
- Integer compareYear, Integer targetYear, String mapSheetScope, List mapSheetNums);
-
- /**
- * 추론 실행 가능 도엽 조회 (이전년도 사용가능)
- *
- * @param targetYear
- * @param compareYear
- * @param mapSheetScope
- * @param mapSheetNums
- * @return
- */
- List fetchBaseWithCompare(
- Integer targetYear, Integer compareYear, String mapSheetScope, List mapSheetNums);
-
- /**
- * 실행가능 도엽 count
- *
- * @param year 연도
- * @param mapSheetScope 부분, 전체 구분
- * @param mapSheetNums 도엽 목록
- * @return count
- */
- int countExecutableSheetsDistinct(Integer year, String mapSheetScope, List mapSheetNums);
-
MapSheetMngDto.MngFilesDto findByFileUidMapSheetFile(Long fileUid);
void updateHstFileSizes(Long hstUid, long tifSizeBytes, long tfwSizeBytes, long totalSizeBytes);
diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java
index 04101d00..599900e7 100644
--- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java
+++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java
@@ -22,7 +22,6 @@ import com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngHstEntity;
import com.kamco.cd.kamcoback.postgres.entity.QYearEntity;
import com.kamco.cd.kamcoback.postgres.entity.YearEntity;
import com.querydsl.core.BooleanBuilder;
-import com.querydsl.core.Tuple;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.CaseBuilder;
@@ -35,11 +34,8 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.validation.Valid;
import java.time.ZonedDateTime;
-import java.util.ArrayList;
import java.util.Comparator;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
@@ -577,204 +573,6 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
return foundContent;
}
- @Override
- public List findExecutableSheets(
- Integer compareYear, Integer targetYear, String mapSheetScope, List mapSheetNums) {
-
- QMapSheetMngHstEntity t = new QMapSheetMngHstEntity("t");
- QMapSheetMngHstEntity c = new QMapSheetMngHstEntity("c");
-
- QMapInkx5kEntity ti = new QMapInkx5kEntity("ti");
- QMapInkx5kEntity ci = new QMapInkx5kEntity("ci");
-
- QMapSheetMngFileEntity tf = new QMapSheetMngFileEntity("tf");
- QMapSheetMngFileEntity cf = new QMapSheetMngFileEntity("cf");
-
- BooleanBuilder whereBuilderere = new BooleanBuilder();
- whereBuilderere.and(t.dataState.eq("DONE"));
- whereBuilderere.and(t.syncState.eq("DONE").or(t.syncCheckState.eq("DONE")));
- whereBuilderere.and(t.useInference.eq("USE"));
- whereBuilderere.and(t.mngYyyy.eq(targetYear));
-
- // target: tif + DONE 파일 존재
- whereBuilderere.and(
- JPAExpressions.selectOne()
- .from(tf)
- .where(tf.hstUid.eq(t.hstUid).and(tf.fileExt.eq("tif")).and(tf.fileState.eq("DONE")))
- .exists());
-
- // PART scope면 prefix like 조건 추가
- BooleanBuilder likeBuilder = new BooleanBuilder();
-
- if (MapSheetScope.PART.getId().equals(mapSheetScope)) {
- if (mapSheetNums == null || mapSheetNums.isEmpty()) {
- return List.of();
- }
- for (String prefix : mapSheetNums) {
- if (prefix == null || prefix.isBlank()) continue;
- likeBuilder.or(t.mapSheetNum.like(prefix.trim() + "%"));
- }
- if (likeBuilder.hasValue()) {
- whereBuilderere.and(likeBuilder);
- }
- }
-
- // compare 조건은 EXISTS 서브쿼리 안에서 체크
- BooleanExpression compareExists =
- JPAExpressions.selectOne()
- .from(c)
- .join(ci)
- .on(ci.mapidcdNo.eq(c.mapSheetNum).and(ci.useInference.eq(CommonUseStatus.USE)))
- .where(
- c.mapSheetNum
- .eq(t.mapSheetNum) // 핵심: 같은 도엽번호
- .and(c.dataState.eq("DONE"))
- .and(c.syncState.eq("DONE").or(c.syncCheckState.eq("DONE")))
- .and(c.useInference.eq("USE"))
- .and(c.mngYyyy.eq(compareYear))
- .and(
- JPAExpressions.selectOne()
- .from(cf)
- .where(
- cf.hstUid
- .eq(c.hstUid)
- .and(cf.fileExt.eq("tif"))
- .and(cf.fileState.eq("DONE")))
- .exists()))
- .exists();
-
- return queryFactory
- .select(
- Projections.constructor(
- MngListDto.class,
- t.mngYyyy, // int mngYyyy
- t.mapSheetNum, // String mapSheetNum
- t.mapSheetName, // String mapSheetName
- Expressions.nullExpression(Integer.class),
- Expressions.nullExpression(Boolean.class)))
- .from(t)
- .join(ti)
- .on(ti.mapidcdNo.eq(t.mapSheetNum).and(ti.useInference.eq(CommonUseStatus.USE)))
- .where(whereBuilderere.and(compareExists))
- .distinct()
- .fetch();
- }
-
- @Override
- public List fetchBaseWithCompare(
- Integer compareYear, // 예: 2024 (비교 상한)
- Integer targetYear, // 예: 2026 (기준 고정)
- String mapSheetScope,
- List mapSheetNums) {
-
- QMapSheetMngHstEntity h = QMapSheetMngHstEntity.mapSheetMngHstEntity; // base
- QMapSheetMngHstEntity h2 = new QMapSheetMngHstEntity("h2"); // cmp
- QMapInkx5kEntity i = QMapInkx5kEntity.mapInkx5kEntity;
- QMapSheetMngFileEntity f = QMapSheetMngFileEntity.mapSheetMngFileEntity;
- QMapSheetMngFileEntity f2 = new QMapSheetMngFileEntity("f2");
-
- // -----------------------
- // 공통: tif DONE exists
- // -----------------------
- BooleanExpression baseTifExists =
- JPAExpressions.selectOne()
- .from(f)
- .where(f.hstUid.eq(h.hstUid), f.fileExt.eq("tif"), f.fileState.eq("DONE"))
- .exists();
-
- BooleanExpression cmpTifExists =
- JPAExpressions.selectOne()
- .from(f2)
- .where(f2.hstUid.eq(h2.hstUid), f2.fileExt.eq("tif"), f2.fileState.eq("DONE"))
- .exists();
-
- // -----------------------
- // 1) base 조회 (CTE base)
- // -----------------------
- BooleanBuilder baseWhere =
- new BooleanBuilder()
- .and(h.mngYyyy.eq(targetYear))
- .and(h.dataState.eq("DONE"))
- .and(h.useInference.eq("USE"))
- .and(h.syncState.eq("DONE").or(h.syncCheckState.eq("DONE")))
- .and(baseTifExists);
-
- // PART 조건
- if (MapSheetScope.PART.getId().equals(mapSheetScope)) {
- if (mapSheetNums == null || mapSheetNums.isEmpty()) return List.of();
-
- BooleanBuilder like = new BooleanBuilder();
- for (String prefix : mapSheetNums) {
- if (prefix == null || prefix.isBlank()) continue;
- like.or(h.mapSheetNum.like(prefix.trim() + "%"));
- }
- if (!like.hasValue()) return List.of();
- baseWhere.and(like);
- }
-
- // base: distinct map_sheet_num, mng_yyyy, map_sheet_name
- List baseTuples =
- queryFactory
- .select(h.mapSheetNum, h.mngYyyy, h.mapSheetName)
- .distinct()
- .from(h)
- .join(i)
- .on(i.mapidcdNo.eq(h.mapSheetNum), i.useInference.eq(CommonUseStatus.USE))
- .where(baseWhere)
- .fetch();
-
- if (baseTuples.isEmpty()) return List.of();
-
- List baseNums = baseTuples.stream().map(t -> t.get(h.mapSheetNum)).toList();
-
- // -----------------------
- // 2) cmp 집계 (CTE cmp)
- // map_sheet_num별 max(mng_yyyy <= 2024)
- // -----------------------
- List cmpTuples =
- queryFactory
- .select(h2.mapSheetNum, h2.mngYyyy.max())
- .from(h2)
- .where(
- h2.mapSheetNum.in(baseNums),
- h2.mngYyyy.loe(compareYear),
- h2.dataState.eq("DONE"),
- h2.useInference.eq("USE"),
- h2.syncState.eq("DONE").or(h2.syncCheckState.eq("DONE")),
- cmpTifExists)
- .groupBy(h2.mapSheetNum)
- .fetch();
-
- Map beforeYearMap = new HashMap<>(cmpTuples.size());
- for (Tuple t : cmpTuples) {
- String num = t.get(h2.mapSheetNum);
- Integer beforeYear = t.get(h2.mngYyyy.max());
- beforeYearMap.put(num, beforeYear);
- }
-
- // -----------------------
- // 3) base JOIN cmp (메모리)
- // - SQL에서 join cmp c on c.map_sheet_num = b.map_sheet_num 동일
- // -----------------------
- List result = new ArrayList<>();
- for (Tuple bt : baseTuples) {
- String num = bt.get(h.mapSheetNum);
- Integer beforeYear = beforeYearMap.get(num);
- if (beforeYear == null) continue; // SQL의 inner join cmp 효과
-
- result.add(
- new MngListDto(
- bt.get(h.mngYyyy), // 2026
- num,
- bt.get(h.mapSheetName),
- beforeYear, // <=2024 max
- null // isSuccess
- ));
- }
-
- return result;
- }
-
@Override
public List findHstUidToMapSheetFileList(Long hstUid) {
BooleanBuilder whereBuilder = new BooleanBuilder();
@@ -1340,57 +1138,4 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
.exists())))
.fetch();
}
-
- @Override
- public int countExecutableSheetsDistinct(
- Integer year, String mapSheetScope, List mapSheetNum) {
-
- QMapSheetMngHstEntity h = QMapSheetMngHstEntity.mapSheetMngHstEntity;
- QMapInkx5kEntity i = QMapInkx5kEntity.mapInkx5kEntity;
- QMapSheetMngFileEntity f = QMapSheetMngFileEntity.mapSheetMngFileEntity;
-
- BooleanBuilder where = new BooleanBuilder();
-
- // 실행가능 조건
- where.and(h.dataState.eq("DONE"));
- where.and(h.syncState.eq("DONE").or(h.syncCheckState.eq("DONE")));
- where.and(h.useInference.eq("USE"));
- where.and(h.mngYyyy.eq(year));
-
- // tif + DONE 파일 존재 조건 AND EXISTS
- where.and(
- JPAExpressions.selectOne()
- .from(f)
- .where(f.hstUid.eq(h.hstUid).and(f.fileExt.eq("tif")).and(f.fileState.eq("DONE")))
- .exists());
-
- // PART scope prefix 조건
- if (MapSheetScope.PART.getId().equals(mapSheetScope)) {
- if (mapSheetNum == null || mapSheetNum.isEmpty()) {
- return 0;
- }
-
- BooleanBuilder likeBuilder = new BooleanBuilder();
- for (String prefix : mapSheetNum) {
- if (prefix == null || prefix.isBlank()) continue;
- likeBuilder.or(h.mapSheetNum.like(prefix.trim() + "%"));
- }
-
- if (likeBuilder.hasValue()) {
- where.and(likeBuilder);
- }
- }
-
- // DISTINCT mapSheetNum 기준 카운트
- Long count =
- queryFactory
- .select(h.mapSheetNum.countDistinct())
- .from(h)
- .join(i)
- .on(i.mapidcdNo.eq(h.mapSheetNum).and(i.useInference.eq(CommonUseStatus.USE)))
- .where(where)
- .fetchOne();
-
- return count != null ? count.intValue() : 0;
- }
}
diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/model/ModelMngRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/model/ModelMngRepositoryCustom.java
index e6268a39..0e52cf46 100644
--- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/model/ModelMngRepositoryCustom.java
+++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/model/ModelMngRepositoryCustom.java
@@ -9,6 +9,16 @@ import org.springframework.data.domain.Page;
public interface ModelMngRepositoryCustom {
+ /**
+ * 모델조회
+ *
+ * @param searchReq 페이징
+ * @param startDate 시작날짜
+ * @param endDate 종료날짜
+ * @param modelType 모델 타입 G1, G2, G3
+ * @param searchVal 모델 ver
+ * @return 모델 목록
+ */
Page findModelMgmtList(
ModelMngDto.searchReq searchReq,
LocalDate startDate,
diff --git a/src/main/java/com/kamco/cd/kamcoback/scheduler/config/ShpKeyLock.java b/src/main/java/com/kamco/cd/kamcoback/scheduler/config/ShpKeyLock.java
index 11958fea..8ece0d05 100644
--- a/src/main/java/com/kamco/cd/kamcoback/scheduler/config/ShpKeyLock.java
+++ b/src/main/java/com/kamco/cd/kamcoback/scheduler/config/ShpKeyLock.java
@@ -7,19 +7,76 @@ import org.springframework.stereotype.Component;
@Component
public class ShpKeyLock {
+ /**
+ * key별 Lock 객체를 저장하는 맵
+ *
+ * - key: 예) shp 파일 경로, uuid, 도엽번호 등 - value: 해당 key 전용 ReentrantLock
+ *
+ *
ConcurrentHashMap을 사용하여 멀티스레드 환경에서도 안전하게 접근 가능
+ */
private final ConcurrentHashMap locks = new ConcurrentHashMap<>();
+ /**
+ * 특정 key에 대한 Lock 시도
+ *
+ * @param key 동시성 제어 대상 식별자
+ * @return true → 락 획득 성공 false → 이미 다른 스레드가 사용 중
+ */
public boolean tryLock(String key) {
+
+ /**
+ * computeIfAbsent 설명:
+ *
+ * - 해당 key가 이미 존재하면 기존 Lock 반환 - 없으면 새 ReentrantLock 생성 후 저장
+ *
+ *
동시성 환경에서도 원자적으로 실행됨
+ */
ReentrantLock lock = locks.computeIfAbsent(key, k -> new ReentrantLock());
+
+ /**
+ * tryLock():
+ *
+ *
- 대기하지 않고 즉시 시도 - 이미 다른 스레드가 점유 중이면 false 반환
+ *
+ *
→ 다운로드 중복 방지에 적합
+ */
return lock.tryLock();
}
+ /**
+ * 특정 key에 대한 Lock 해제
+ *
+ *
반드시 tryLock 성공한 동일 스레드에서 호출해야 함
+ */
public void unlock(String key) {
+
+ // 해당 key에 등록된 Lock 조회
ReentrantLock lock = locks.get(key);
+
+ /**
+ * isHeldByCurrentThread():
+ *
+ *
- 현재 스레드가 해당 락을 보유 중인지 확인 - 다른 스레드가 unlock 호출하는 것 방지
+ */
if (lock != null && lock.isHeldByCurrentThread()) {
+
+ // 실제 락 해제
lock.unlock();
- // 메모리 누수 방지(락이 비어있으면 제거)
+
+ /**
+ * 메모리 누수 방지 처리
+ *
+ *
hasQueuedThreads(): - 현재 이 락을 기다리는 스레드가 있는지 확인
+ *
+ *
대기 스레드가 없다면 locks 맵에서 제거하여 불필요한 Lock 객체 정리
+ */
if (!lock.hasQueuedThreads()) {
+
+ /**
+ * remove(key, lock):
+ *
+ *
- 현재 맵에 등록된 값이 'lock'일 경우에만 제거 - 동시성 안전 (CAS 방식)
+ */
locks.remove(key, lock);
}
}