Merge pull request '주석 추가, 패스워드변경 권한 수정' (#116) from feat/infer_dev_260211 into develop
Reviewed-on: #116
This commit was merged in pull request #116.
This commit is contained in:
@@ -14,23 +14,56 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Range(부분 다운로드) 지원 파일 다운로드 응답 생성기.
|
||||
*
|
||||
* <p>브라우저/다운로드 매니저가 Range 헤더를 보내면 206 Partial Content로 일부 구간만 내려주고, Range 헤더가 없으면 200 OK로 전체 파일을
|
||||
* 내려준다.
|
||||
*
|
||||
* <p>대용량 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<HttpRange> 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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -780,11 +780,17 @@ public class InferenceResultService {
|
||||
return inferenceResultCoreService.listGetScenes5k(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 추론 서버 현황 cpu, gpu 확인
|
||||
*
|
||||
* @return 서버 정보
|
||||
*/
|
||||
public List<InferenceServerStatusDto> getInferenceServerStatusList() {
|
||||
|
||||
String[] serverNames = inferenceServerName.split(",");
|
||||
int serveCnt = serverNames.length;
|
||||
|
||||
// 서버정보 조회
|
||||
List<InferenceServerStatusDto> 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<Dashboard> getInferenceClassCountList(UUID uuid) {
|
||||
return inferenceResultCoreService.getInferenceClassCountList(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 추론결과 geom 목록 조회
|
||||
*
|
||||
* @param uuid 추론 uuid
|
||||
* @param searchGeoReq 추론 결과 상세화면 geom 조회 조건
|
||||
* @return geom 목록 정보
|
||||
*/
|
||||
public Page<Geom> 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<String> 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<String, Object> 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<String, Object> downloadMap = new HashMap<>();
|
||||
|
||||
@@ -387,6 +387,8 @@ public class MapSheetMngService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 연도 목록 조회
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<MngYyyyDto> findMapSheetMngDoneYyyyList() {
|
||||
|
||||
@@ -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<ModelMngDto.ModelList> findModelMgmtList(
|
||||
ModelMngDto.searchReq searchReq,
|
||||
LocalDate startDate,
|
||||
|
||||
@@ -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<Dashboard> getInferenceClassCountList(UUID uuid) {
|
||||
return mapSheetLearnRepository.getInferenceClassCountList(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uuid 추론 uuid
|
||||
* @param searchGeoReq 추론 결과 상세화면 geom 조회 조건
|
||||
* @return geom 목록 정보
|
||||
*/
|
||||
public Page<Geom> getInferenceGeomList(UUID uuid, SearchGeoReq searchGeoReq) {
|
||||
return mapSheetLearnRepository.getInferenceGeomList(uuid, searchGeoReq);
|
||||
}
|
||||
|
||||
@@ -291,40 +291,6 @@ public class MapSheetMngCoreService {
|
||||
return sceneInference;
|
||||
}
|
||||
|
||||
/**
|
||||
* 변화탐지 실행 가능 기준 년도 조회
|
||||
*
|
||||
* @param compareYear 비교 연도
|
||||
* @param targetYear 기준 연도
|
||||
* @param mapSheetScope 부분실행, 전체실행
|
||||
* @param mapSheetNums 도엽번호 목록
|
||||
* @return 실행가능한 도엽번호 목록
|
||||
*/
|
||||
public List<MngListDto> findExecutableSheets(
|
||||
Integer compareYear, Integer targetYear, String mapSheetScope, List<String> mapSheetNums) {
|
||||
return mapSheetMngRepository.findExecutableSheets(
|
||||
compareYear, targetYear, mapSheetScope, mapSheetNums);
|
||||
}
|
||||
|
||||
public List<MngListDto> fetchBaseWithCompare(
|
||||
Integer compareYear, Integer targetYear, String mapSheetScope, List<String> 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<String> mapSheetNums) {
|
||||
return mapSheetMngRepository.countExecutableSheetsDistinct(year, mapSheetScope, mapSheetNums);
|
||||
}
|
||||
|
||||
public void updateMapSheetMngHstUploadId(Long hstUid, UUID uuid, String uploadId) {
|
||||
mapSheetMngRepository.updateMapSheetMngHstUploadId(hstUid, uuid, uploadId);
|
||||
}
|
||||
|
||||
@@ -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<ModelMngDto.ModelList> findModelMgmtList(
|
||||
ModelMngDto.searchReq searchReq,
|
||||
LocalDate startDate,
|
||||
|
||||
@@ -62,40 +62,6 @@ public interface MapSheetMngRepositoryCustom {
|
||||
|
||||
List<MapSheetMngDto.MngFilesDto> findByHstUidMapSheetFileList(Long hstUid);
|
||||
|
||||
/**
|
||||
* 추론 실행 가능 도엽 조회 (추론 제외)
|
||||
*
|
||||
* @param compareYear 비교 연도
|
||||
* @param targetYear 기준 연도
|
||||
* @param mapSheetScope 부분실행, 전체실행
|
||||
* @param mapSheetNums 도엽번호 목록
|
||||
* @return 실행가능한 도엽번호 목록
|
||||
*/
|
||||
List<MngListDto> findExecutableSheets(
|
||||
Integer compareYear, Integer targetYear, String mapSheetScope, List<String> mapSheetNums);
|
||||
|
||||
/**
|
||||
* 추론 실행 가능 도엽 조회 (이전년도 사용가능)
|
||||
*
|
||||
* @param targetYear
|
||||
* @param compareYear
|
||||
* @param mapSheetScope
|
||||
* @param mapSheetNums
|
||||
* @return
|
||||
*/
|
||||
List<MngListDto> fetchBaseWithCompare(
|
||||
Integer targetYear, Integer compareYear, String mapSheetScope, List<String> mapSheetNums);
|
||||
|
||||
/**
|
||||
* 실행가능 도엽 count
|
||||
*
|
||||
* @param year 연도
|
||||
* @param mapSheetScope 부분, 전체 구분
|
||||
* @param mapSheetNums 도엽 목록
|
||||
* @return count
|
||||
*/
|
||||
int countExecutableSheetsDistinct(Integer year, String mapSheetScope, List<String> mapSheetNums);
|
||||
|
||||
MapSheetMngDto.MngFilesDto findByFileUidMapSheetFile(Long fileUid);
|
||||
|
||||
void updateHstFileSizes(Long hstUid, long tifSizeBytes, long tfwSizeBytes, long totalSizeBytes);
|
||||
|
||||
@@ -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<MngListDto> findExecutableSheets(
|
||||
Integer compareYear, Integer targetYear, String mapSheetScope, List<String> 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<MngListDto> fetchBaseWithCompare(
|
||||
Integer compareYear, // 예: 2024 (비교 상한)
|
||||
Integer targetYear, // 예: 2026 (기준 고정)
|
||||
String mapSheetScope,
|
||||
List<String> 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<Tuple> 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<String> baseNums = baseTuples.stream().map(t -> t.get(h.mapSheetNum)).toList();
|
||||
|
||||
// -----------------------
|
||||
// 2) cmp 집계 (CTE cmp)
|
||||
// map_sheet_num별 max(mng_yyyy <= 2024)
|
||||
// -----------------------
|
||||
List<Tuple> 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<String, Integer> 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<MngListDto> 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<MapSheetMngDto.MngFilesDto> 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<String> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ModelMngDto.ModelList> findModelMgmtList(
|
||||
ModelMngDto.searchReq searchReq,
|
||||
LocalDate startDate,
|
||||
|
||||
@@ -7,19 +7,76 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
public class ShpKeyLock {
|
||||
|
||||
/**
|
||||
* key별 Lock 객체를 저장하는 맵
|
||||
*
|
||||
* <p>- key: 예) shp 파일 경로, uuid, 도엽번호 등 - value: 해당 key 전용 ReentrantLock
|
||||
*
|
||||
* <p>ConcurrentHashMap을 사용하여 멀티스레드 환경에서도 안전하게 접근 가능
|
||||
*/
|
||||
private final ConcurrentHashMap<String, ReentrantLock> locks = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 특정 key에 대한 Lock 시도
|
||||
*
|
||||
* @param key 동시성 제어 대상 식별자
|
||||
* @return true → 락 획득 성공 false → 이미 다른 스레드가 사용 중
|
||||
*/
|
||||
public boolean tryLock(String key) {
|
||||
|
||||
/**
|
||||
* computeIfAbsent 설명:
|
||||
*
|
||||
* <p>- 해당 key가 이미 존재하면 기존 Lock 반환 - 없으면 새 ReentrantLock 생성 후 저장
|
||||
*
|
||||
* <p>동시성 환경에서도 원자적으로 실행됨
|
||||
*/
|
||||
ReentrantLock lock = locks.computeIfAbsent(key, k -> new ReentrantLock());
|
||||
|
||||
/**
|
||||
* tryLock():
|
||||
*
|
||||
* <p>- 대기하지 않고 즉시 시도 - 이미 다른 스레드가 점유 중이면 false 반환
|
||||
*
|
||||
* <p>→ 다운로드 중복 방지에 적합
|
||||
*/
|
||||
return lock.tryLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 key에 대한 Lock 해제
|
||||
*
|
||||
* <p>반드시 tryLock 성공한 동일 스레드에서 호출해야 함
|
||||
*/
|
||||
public void unlock(String key) {
|
||||
|
||||
// 해당 key에 등록된 Lock 조회
|
||||
ReentrantLock lock = locks.get(key);
|
||||
|
||||
/**
|
||||
* isHeldByCurrentThread():
|
||||
*
|
||||
* <p>- 현재 스레드가 해당 락을 보유 중인지 확인 - 다른 스레드가 unlock 호출하는 것 방지
|
||||
*/
|
||||
if (lock != null && lock.isHeldByCurrentThread()) {
|
||||
|
||||
// 실제 락 해제
|
||||
lock.unlock();
|
||||
// 메모리 누수 방지(락이 비어있으면 제거)
|
||||
|
||||
/**
|
||||
* 메모리 누수 방지 처리
|
||||
*
|
||||
* <p>hasQueuedThreads(): - 현재 이 락을 기다리는 스레드가 있는지 확인
|
||||
*
|
||||
* <p>대기 스레드가 없다면 locks 맵에서 제거하여 불필요한 Lock 객체 정리
|
||||
*/
|
||||
if (!lock.hasQueuedThreads()) {
|
||||
|
||||
/**
|
||||
* remove(key, lock):
|
||||
*
|
||||
* <p>- 현재 맵에 등록된 값이 'lock'일 경우에만 제거 - 동시성 안전 (CAS 방식)
|
||||
*/
|
||||
locks.remove(key, lock);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user