From dc42baf91ab0d055b6e641963050c53c56310dc3 Mon Sep 17 00:00:00 2001 From: teddy Date: Fri, 27 Feb 2026 13:41:17 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80,=20?= =?UTF-8?q?=ED=8C=A8=EC=8A=A4=EC=9B=8C=EB=93=9C=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../download/RangeDownloadResponder.java | 56 +++- .../cd/kamcoback/config/SecurityConfig.java | 1 - .../InferenceResultApiController.java | 2 +- .../service/InferenceResultService.java | 34 ++- .../mapsheet/service/MapSheetMngService.java | 2 + .../model/service/ModelMngService.java | 10 + .../core/InferenceResultCoreService.java | 13 + .../postgres/core/MapSheetMngCoreService.java | 34 --- .../postgres/core/ModelMngCoreService.java | 10 + .../mapsheet/MapSheetMngRepositoryCustom.java | 34 --- .../mapsheet/MapSheetMngRepositoryImpl.java | 255 ------------------ .../model/ModelMngRepositoryCustom.java | 10 + .../scheduler/config/ShpKeyLock.java | 59 +++- 13 files changed, 191 insertions(+), 329 deletions(-) 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 a28acf53..6fa89112 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 @@ -382,6 +382,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); } }