70 Commits

Author SHA1 Message Date
972cf808a9 Merge pull request '학습데이터 관리 목록 순서 변경, 주석추가' (#123) from feat/infer_dev_260211 into develop
Reviewed-on: #123
2026-02-27 18:54:29 +09:00
086eb20e8d 학습데이터 관리 목록 순서 변경, 주석추가 2026-02-27 18:54:03 +09:00
c3c194c067 Merge pull request '국유인 실태조사 bulk 확인' (#122) from feat/infer_dev_260211 into develop
Reviewed-on: #122
2026-02-27 17:02:22 +09:00
0acaeaac09 국유인 실태조사 bulk 확인 2026-02-27 17:01:54 +09:00
53ce735bca Merge pull request '국유인 체크 url 수정' (#121) from feat/infer_dev_260211 into develop
Reviewed-on: #121
2026-02-27 16:24:11 +09:00
a341be7ed6 국유인 체크 url 수정 2026-02-27 16:23:50 +09:00
9f6dc2b3c6 Merge pull request '국유인 연동 url 수정' (#120) from feat/infer_dev_260211 into develop
Reviewed-on: #120
2026-02-27 16:01:32 +09:00
cc46315e3a 국유인 연동 url 수정 2026-02-27 16:01:05 +09:00
ca631d5d58 Merge pull request '변화지도 도엽 목록에 도엽별bbox 추가' (#119) from feat/infer_dev_260211 into develop
Reviewed-on: #119
2026-02-27 15:31:52 +09:00
5d0590bd3c 변화지도 도엽 목록에 도엽별bbox 추가 2026-02-27 15:31:33 +09:00
3aed3cf1ec Merge pull request 'feat/infer_dev_260211' (#118) from feat/infer_dev_260211 into develop
Reviewed-on: #118
2026-02-27 15:14:37 +09:00
396e76c362 국유인 연동 outputdir 경로 변경 2026-02-27 15:14:18 +09:00
9f379c6dc3 영상관리, 도엽관리 API만 우선 정리함 2026-02-27 14:07:44 +09:00
d069981c8f 패스워드 변경 권한 수정 2026-02-27 14:01:54 +09:00
ece70f1d68 Merge remote-tracking branch 'origin/feat/infer_dev_260211' into feat/infer_dev_260211 2026-02-27 14:01:21 +09:00
b7470d11d4 추론 주석 추가 2026-02-27 14:01:14 +09:00
a7108c44f4 Merge pull request '비밀번호 변경 security 설정 수정' (#117) from feat/infer_dev_260211 into develop
Reviewed-on: #117
2026-02-27 14:00:11 +09:00
5039dd0f51 비밀번호 변경 security 설정 수정 2026-02-27 13:59:47 +09:00
9cb3a100aa Merge pull request '주석 추가, 패스워드변경 권한 수정' (#116) from feat/infer_dev_260211 into develop
Reviewed-on: #116
2026-02-27 13:41:32 +09:00
dc42baf91a 주석 추가, 패스워드변경 권한 수정 2026-02-27 13:41:17 +09:00
9d36208845 Merge pull request '국유인 반영 부분 막기 해제' (#115) from feat/infer_dev_260211 into develop
Reviewed-on: #115
2026-02-27 11:41:57 +09:00
ae4c1c61e8 국유인 반영 부분 막기 해제 2026-02-27 11:40:59 +09:00
369f303f6c Merge pull request 'feat/infer_dev_260211' (#114) from feat/infer_dev_260211 into develop
Reviewed-on: #114
2026-02-27 10:09:04 +09:00
96d4bb4af3 주석 제거 2026-02-27 10:08:39 +09:00
7b55204ae1 추론 api 호출시 대용량이면 3초 timeout 문제
60초 long restTemplate 추가
2026-02-27 10:06:39 +09:00
f54655c191 추론 수정 2026-02-27 09:25:53 +09:00
f4cbb48aa2 Merge pull request '추론 수정' (#113) from feat/infer_dev_260211 into develop
Reviewed-on: #113
2026-02-27 09:15:56 +09:00
fbad8d1cd3 추론 수정 2026-02-27 09:15:38 +09:00
6e4682dad6 Merge pull request '추론 수정' (#112) from feat/infer_dev_260211 into develop
Reviewed-on: #112
2026-02-27 08:29:22 +09:00
4629715443 추론 수정 2026-02-27 08:28:59 +09:00
5ba7f9ccfc Merge pull request '추론 수정' (#111) from feat/infer_dev_260211 into develop
Reviewed-on: #111
2026-02-27 00:03:26 +09:00
264dae3ba9 추론 수정 2026-02-27 00:03:03 +09:00
fab5b02211 Merge pull request '추론 수정' (#110) from feat/infer_dev_260211 into develop
Reviewed-on: #110
2026-02-26 23:55:53 +09:00
38f70b933f 추론 수정 2026-02-26 23:55:29 +09:00
a4b1db462b Merge pull request '추론 수정' (#109) from feat/infer_dev_260211 into develop
Reviewed-on: #109
2026-02-26 23:42:09 +09:00
2bf7c42a3f 추론 수정 2026-02-26 23:41:49 +09:00
a1be4e9faf Merge pull request '추론 수정' (#108) from feat/infer_dev_260211 into develop
Reviewed-on: #108
2026-02-26 23:18:50 +09:00
8904de0e3d 추론 수정 2026-02-26 23:18:31 +09:00
a21df9d018 중복 경고 2026-02-26 18:38:56 +09:00
85cad2dd28 중복 경고 2026-02-26 18:34:29 +09:00
7b8bf8726b 중복 경고 2026-02-26 18:29:03 +09:00
c841d460aa 중복 경고 2026-02-26 18:26:40 +09:00
3a8ac3a24f Merge pull request 'ensureAccepted 함수 InferenceCommonService 생성후 공통으로 생성' (#107) from feat/infer_dev_260211 into develop
Reviewed-on: #107
2026-02-26 16:56:04 +09:00
046f4f06d3 기준년도 없을때 throw 추가 2026-02-26 16:53:29 +09:00
5c9f33d210 Merge branch 'develop' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-cd-api into feat/infer_dev_260211 2026-02-26 16:31:33 +09:00
ea7e98d28e ensureAccepted 함수 InferenceCommonService 생성후 공통으로 생성 2026-02-26 16:30:52 +09:00
3e78f744a4 Merge pull request 'shp파일 a 링크로 다운로드할때 이력 저장 변경' (#106) from feat/infer_dev_260211 into develop
Reviewed-on: #106
2026-02-26 15:27:17 +09:00
cea1f01ed9 shp파일 a 링크로 다운로드할때 이력 저장 변경 2026-02-26 15:26:21 +09:00
d7f2d22b93 Merge branch 'develop' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-cd-api into feat/infer_dev_260211 2026-02-26 15:25:21 +09:00
eccdfb17e6 shp파일 a 링크로 다운로드할때 이력 저장 변경 2026-02-26 15:25:10 +09:00
d2fa86a89f Merge pull request 'merge develop_add_log' (#105) from feat/dean/add_log into develop
Reviewed-on: #105
2026-02-26 13:22:05 +09:00
32d56cf8fe merge develop_add_log 2026-02-26 13:20:11 +09:00
c3b7daebb7 Merge pull request '미사용 쿼리 삭제, 추론 주석 추가, api/inference/download/ 권한 제거 수정' (#104) from feat/infer_dev_260211 into develop
Reviewed-on: #104
2026-02-26 12:36:40 +09:00
2188d426d4 미사용 쿼리 삭제, 추론 주석 추가, api/inference/download/ 권한 제거 수정 2026-02-26 12:36:02 +09:00
5c2ee0974b Merge pull request '추론실행 수정, develop pull 반영, 국유인 파일경로 dir 하드코딩 수정' (#103) from feat/infer_dev_260211 into develop
Reviewed-on: #103
2026-02-26 12:21:06 +09:00
7980fe1d42 InferenceRunService 미사용 삭제 및 추론 실행
추론제외, 이전연도 사용 별로 분기처리
2026-02-26 12:16:48 +09:00
c10141e915 Merge branch 'develop' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-cd-api into feat/infer_dev_260211
# Conflicts:
#	src/main/java/com/kamco/cd/kamcoback/gukyuin/service/GukYuinApiService.java
2026-02-26 12:12:50 +09:00
97565c5369 Merge pull request 'feat/dean/test2_temp' (#102) from feat/dean/test2_temp into develop
Reviewed-on: #102
2026-02-26 11:58:48 +09:00
ba562261c3 /api/inference/download 로그인 없이 접근 가능하도록 수정 2026-02-26 11:56:21 +09:00
a084c80715 Merge remote-tracking branch 'origin/feat/infer_dev_260211' into feat/infer_dev_260211 2026-02-26 11:53:01 +09:00
a44e93c234 추론 실행 수정 및 추석 추가 작업중 2026-02-26 11:52:56 +09:00
7b15e5bb8c Merge branch 'develop' into feat/infer_dev_260211
# Conflicts:
#	src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceRunService.java
2026-02-26 11:36:40 +09:00
001ad73de7 국유인 파일경로 dir 하드코딩 수정 2026-02-26 11:34:02 +09:00
cefacb291b develop pull 2026-02-26 09:54:43 +09:00
744cbb55a9 Merge branch 'develop' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-cd-api into feat/infer_dev_260211
# Conflicts:
#	src/main/java/com/kamco/cd/kamcoback/inference/dto/InferenceSendDto.java
#	src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceRunService.java
2026-02-26 09:53:11 +09:00
4863091406 추론 run 수정 2026-02-25 22:50:38 +09:00
70c28e0b54 추론 run 수정 2026-02-25 22:40:30 +09:00
9197819340 추론 run 수정 2026-02-25 22:39:49 +09:00
f2500c33e6 Merge remote-tracking branch 'origin/feat/infer_dev_260211' into feat/infer_dev_260211 2026-02-25 22:30:46 +09:00
18dc831b05 추론 run 수정 2026-02-25 22:30:36 +09:00
61 changed files with 2250 additions and 1109 deletions

View File

@@ -19,9 +19,9 @@ import org.springframework.web.filter.OncePerRequestFilter;
public class JwtAuthenticationFilter extends OncePerRequestFilter { public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(); private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
// JWT 필터를 타지 않게 할 URL 패턴들
private static final String[] EXCLUDE_PATHS = { private static final String[] EXCLUDE_PATHS = {
// "/api/auth/signin", "/api/auth/refresh", "/api/auth/logout", "/api/members/*/password" "/api/auth/signin", "/api/auth/refresh", "/api/auth/logout", "/api/members/*/password"
"/api/auth/signin", "/api/auth/refresh", "/api/auth/logout"
}; };
private final JwtTokenProvider jwtTokenProvider; private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService; private final UserDetailsService userDetailsService;
@@ -31,8 +31,10 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
// HTTP 요청 헤더에서 JWT 토큰 꺼내기
String token = resolveToken(request); String token = resolveToken(request);
// JWT 토큰을 검증하고, 인증된 사용자로 SecurityContext에 등록
if (token != null && jwtTokenProvider.isValidToken(token)) { if (token != null && jwtTokenProvider.isValidToken(token)) {
String username = jwtTokenProvider.getSubject(token); String username = jwtTokenProvider.getSubject(token);
@@ -58,7 +60,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
return false; return false;
} }
// /api/members/{memberId}/password // HTTP 요청 헤더에서 JWT 토큰 꺼내기
private String resolveToken(HttpServletRequest request) { private String resolveToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization"); String bearer = request.getHeader("Authorization");
if (bearer != null && bearer.startsWith("Bearer ")) { if (bearer != null && bearer.startsWith("Bearer ")) {

View File

@@ -11,6 +11,7 @@ import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** 토큰 생성 */
@Component @Component
public class JwtTokenProvider { public class JwtTokenProvider {
@@ -31,10 +32,12 @@ public class JwtTokenProvider {
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
} }
// Access Token 생성
public String createAccessToken(String subject) { public String createAccessToken(String subject) {
return createToken(subject, accessTokenValidityInMs); return createToken(subject, accessTokenValidityInMs);
} }
// Refresh Token 생성
public String createRefreshToken(String subject) { public String createRefreshToken(String subject) {
return createToken(subject, refreshTokenValidityInMs); return createToken(subject, refreshTokenValidityInMs);
} }

View File

@@ -6,6 +6,7 @@ import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations; import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** redis token handler */
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class RefreshTokenService { public class RefreshTokenService {
@@ -13,16 +14,33 @@ public class RefreshTokenService {
private final StringRedisTemplate redisTemplate; private final StringRedisTemplate redisTemplate;
private static final String PREFIX = "RT:"; private static final String PREFIX = "RT:";
/**
* Refresh Token 저장
*
* @param username 사용자 식별값 (보통 username or userId)
* @param refreshToken 발급된 Refresh Token
* @param ttlMillis 토큰 만료 시간 (밀리초 단위)
*/
public void save(String username, String refreshToken, long ttlMillis) { public void save(String username, String refreshToken, long ttlMillis) {
ValueOperations<String, String> ops = redisTemplate.opsForValue(); ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set(PREFIX + username, refreshToken, Duration.ofMillis(ttlMillis)); ops.set(PREFIX + username, refreshToken, Duration.ofMillis(ttlMillis));
} }
/**
* Refresh Token 검증
*
* <p>1. Redis에 저장된 값 조회 2. 클라이언트가 보낸 refreshToken과 비교 3. 동일하면 true
*/
public boolean validate(String username, String refreshToken) { public boolean validate(String username, String refreshToken) {
String stored = redisTemplate.opsForValue().get(PREFIX + username); String stored = redisTemplate.opsForValue().get(PREFIX + username);
return stored != null && stored.equals(refreshToken); return stored != null && stored.equals(refreshToken);
} }
/**
* Refresh Token 삭제
*
* <p>로그아웃 시 호출 Redis에서 해당 사용자 토큰 제거
*/
public void delete(String username) { public void delete(String username) {
redisTemplate.delete(PREFIX + username); redisTemplate.delete(PREFIX + username);
} }

View File

@@ -1,6 +1,9 @@
package com.kamco.cd.kamcoback.changedetection.dto; package com.kamco.cd.kamcoback.changedetection.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.kamcoback.common.utils.enums.CodeExpose; import com.kamco.cd.kamcoback.common.utils.enums.CodeExpose;
import com.kamco.cd.kamcoback.common.utils.enums.EnumType; import com.kamco.cd.kamcoback.common.utils.enums.EnumType;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@@ -143,6 +146,23 @@ public class ChangeDetectionDto {
private String mapSheetNum; private String mapSheetNum;
private String mapSheetName; private String mapSheetName;
private String alias; private String alias;
@JsonIgnore private String bboxStr;
private JsonNode bbox;
public MapSheetList(String mapSheetNum, String mapSheetName, String alias, String bboxStr) {
this.mapSheetNum = mapSheetNum;
this.mapSheetName = mapSheetName;
this.alias = alias;
if (bboxStr != null) {
ObjectMapper mapper = new ObjectMapper();
try {
this.bbox = mapper.readTree(bboxStr);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
} }
@Schema(name = "PolygonFeatureList", description = "Geometry 리턴 객체") @Schema(name = "PolygonFeatureList", description = "Geometry 리턴 객체")
@@ -262,6 +282,7 @@ public class ChangeDetectionDto {
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public static class ChangeDetectionMapDto { public static class ChangeDetectionMapDto {
private Integer compareYyyy; private Integer compareYyyy;
private Integer targetYyyy; private Integer targetYyyy;
private String cdObjectId; private String cdObjectId;

View File

@@ -5,7 +5,9 @@ import com.kamco.cd.kamcoback.common.download.dto.DownloadAuditEvent;
import com.kamco.cd.kamcoback.menu.dto.MenuDto; import com.kamco.cd.kamcoback.menu.dto.MenuDto;
import com.kamco.cd.kamcoback.menu.service.MenuService; import com.kamco.cd.kamcoback.menu.service.MenuService;
import com.kamco.cd.kamcoback.postgres.entity.AuditLogEntity; import com.kamco.cd.kamcoback.postgres.entity.AuditLogEntity;
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
import com.kamco.cd.kamcoback.postgres.repository.log.AuditLogRepository; import com.kamco.cd.kamcoback.postgres.repository.log.AuditLogRepository;
import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository;
import java.util.Comparator; import java.util.Comparator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@@ -24,6 +26,7 @@ import org.springframework.transaction.annotation.Transactional;
public class DownloadAuditEventListener { public class DownloadAuditEventListener {
private final AuditLogRepository auditLogRepository; private final AuditLogRepository auditLogRepository;
private final MembersRepository membersRepository;
private final MenuService menuService; private final MenuService menuService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@@ -42,9 +45,23 @@ public class DownloadAuditEventListener {
return; return;
} }
Long userId = ev.userId();
if (userId == null) {
// a 링크로 들어온 download는 사번으로 파라미터가 전달 되므로 사번으로 user id 조회 하기
MemberEntity memberEntity =
membersRepository.findByEmployeeNo(ev.employeeNo()).orElse(null);
if (memberEntity == null) {
return; // 매핑 실패 시 로그 저장 안 함
}
userId = memberEntity.getId();
}
AuditLogEntity logEntity = AuditLogEntity logEntity =
AuditLogEntity.forFileDownload( AuditLogEntity.forFileDownload(
ev.userId(), ev.requestUri(), menuUid, ev.ip(), ev.status(), ev.downloadUuid()); userId, ev.requestUri(), menuUid, ev.ip(), ev.status(), ev.downloadUuid());
auditLogRepository.save(logEntity); auditLogRepository.save(logEntity);

View File

@@ -14,23 +14,56 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component; 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 @Component
public class RangeDownloadResponder { public class RangeDownloadResponder {
/**
* ZIP(또는 바이너리) 파일 다운로드 응답을 생성한다.
*
* @param filePath 실제 서버 파일 경로
* @param downloadFileName 사용자에게 노출될 다운로드 파일명
* @param request Range 헤더 확인용 HttpServletRequest
* @return Range 유무에 따라 200(전체) 또는 206(부분) ResponseEntity 반환
* @throws IOException 파일 접근/조회 실패 시
*/
public ResponseEntity<?> buildZipResponse( public ResponseEntity<?> buildZipResponse(
Path filePath, String downloadFileName, HttpServletRequest request) throws IOException { Path filePath, String downloadFileName, HttpServletRequest request) throws IOException {
// 1) 파일 존재/정상 파일 여부 확인
// - 일반 파일(regular file)이 아니면 404 반환 (디렉토리/없는 파일/특수 파일 등 방지)
if (!Files.isRegularFile(filePath)) { if (!Files.isRegularFile(filePath)) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
// 2) 파일 전체 크기 및 Spring Resource 래핑
// - Files.size: 전체 파일 크기(Content-Range 계산/검증에 필요)
// - FileSystemResource: 스프링이 스트리밍 형태로 파일을 응답 바디로 내려줄 수 있게 함
long totalSize = Files.size(filePath); long totalSize = Files.size(filePath);
Resource resource = new FileSystemResource(filePath); Resource resource = new FileSystemResource(filePath);
// 3) 다운로드 강제(Content-Disposition)
// - attachment; filename="xxx.zip" 형태로 브라우저가 저장 대화상자/다운로드로 처리
String disposition = "attachment; filename=\"" + downloadFileName + "\""; String disposition = "attachment; filename=\"" + downloadFileName + "\"";
// 4) Range 헤더 조회
// - Range: bytes=0-1023 (일부 구간 요청)
// - Range가 없으면 전체 다운로드로 처리
String rangeHeader = request.getHeader(HttpHeaders.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.BodyBuilder base =
ResponseEntity.ok() ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM) .contentType(MediaType.APPLICATION_OCTET_STREAM)
@@ -38,10 +71,15 @@ public class RangeDownloadResponder {
.header(HttpHeaders.ACCEPT_RANGES, "bytes") .header(HttpHeaders.ACCEPT_RANGES, "bytes")
.header("X-Accel-Buffering", "no"); .header("X-Accel-Buffering", "no");
// 6) Range 헤더가 없으면 전체 파일 반환 (200 OK)
if (rangeHeader == null || rangeHeader.isBlank()) { if (rangeHeader == null || rangeHeader.isBlank()) {
// Content-Length를 전체 크기로 설정하고 Resource를 그대로 바디에 담아 스트리밍
return base.contentLength(totalSize).body(resource); return base.contentLength(totalSize).body(resource);
} }
// 7) Range 헤더 파싱
// - 잘못된 Range 헤더 형식이면 parseRanges에서 IllegalArgumentException 발생 가능
// - RFC에 따라 416 응답 + Content-Range: bytes */{total} 형태로 알려줌
List<HttpRange> ranges; List<HttpRange> ranges;
try { try {
ranges = HttpRange.parseRanges(rangeHeader); ranges = HttpRange.parseRanges(rangeHeader);
@@ -52,11 +90,18 @@ public class RangeDownloadResponder {
.build(); .build();
} }
// 8) 다중 Range 요청이 와도(예: bytes=0-99,200-299) 여기서는 첫 번째 Range만 처리
// - 대부분 브라우저는 단일 Range를 사용
// - 멀티파트/byteranges 처리를 하려면 별도 구현 필요
HttpRange range = ranges.get(0); HttpRange range = ranges.get(0);
// 9) 실제 시작/끝 범위 계산
// - bytes=500- : end를 파일 끝으로 해석
// - bytes=-500 : 마지막 500바이트로 해석
long start = range.getRangeStart(totalSize); long start = range.getRangeStart(totalSize);
long end = range.getRangeEnd(totalSize); long end = range.getRangeEnd(totalSize);
// 10) 시작점이 파일 크기 이상이면 만족 불가 -> 416
if (start >= totalSize) { if (start >= totalSize) {
return ResponseEntity.status(416) return ResponseEntity.status(416)
.header(HttpHeaders.CONTENT_RANGE, "bytes */" + totalSize) .header(HttpHeaders.CONTENT_RANGE, "bytes */" + totalSize)
@@ -64,9 +109,18 @@ public class RangeDownloadResponder {
.build(); .build();
} }
// 11) 요청 구간 길이 계산
// - end/start는 inclusive이므로 +1 필요
long regionLength = end - start + 1; long regionLength = end - start + 1;
// 12) ResourceRegion 생성
// - resource의 start부터 regionLength 만큼만 응답으로 내려줄 수 있게 함
// - 파일 전체를 메모리에 올리지 않고 필요한 부분만 스트리밍
ResourceRegion region = new 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) return ResponseEntity.status(206)
.contentType(MediaType.APPLICATION_OCTET_STREAM) .contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, disposition) .header(HttpHeaders.CONTENT_DISPOSITION, disposition)

View File

@@ -4,6 +4,7 @@ import java.util.UUID;
public record DownloadAuditEvent( public record DownloadAuditEvent(
Long userId, Long userId,
String employeeNo,
String requestUri, String requestUri,
String normalizedUri, String normalizedUri,
String ip, String ip,

View File

@@ -0,0 +1,111 @@
package com.kamco.cd.kamcoback.common.inference.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient;
import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient.ExternalCallResult;
import com.kamco.cd.kamcoback.inference.dto.InferenceSendDto;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Log4j2
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class InferenceCommonService {
@Value("${spring.profiles.active}")
private String profile;
@Value("${inference.url}")
private String inferenceUrl;
private final ObjectMapper objectMapper;
private final ExternalHttpClient externalHttpClient;
/**
* 추론 AI API 호출 batch id를 리턴
*
* @param dto
* @return
*/
public Long ensureAccepted(InferenceSendDto dto) {
if (dto == null) {
log.warn("not InferenceSendDto dto");
throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST);
}
log.info("");
log.info("========================================================");
log.info("[SEND INFERENCE] Inference request dto= {}", dto);
log.info("========================================================");
log.info("");
// 1) 요청 로그
try {
log.debug("Inference request dto={}", objectMapper.writeValueAsString(dto));
} catch (JsonProcessingException e) {
log.warn("Failed to serialize inference dto", e);
}
// 2) local 환경 임시 처리
// if ("local".equals(profile)) {
// if (dto.getPred_requests_areas() == null) {
// throw new IllegalStateException("pred_requests_areas is null");
// }
//
// dto.getPred_requests_areas().setInput1_scene_path("/kamco-nfs/requests/2023_local.geojson");
//
// dto.getPred_requests_areas().setInput2_scene_path("/kamco-nfs/requests/2024_local.geojson");
// }
// 3) HTTP 호출
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
// 4) 추론 실행 API 호출
ExternalCallResult<String> result =
externalHttpClient.callLong(inferenceUrl, HttpMethod.POST, dto, headers, String.class);
if (result.statusCode() < 200 || result.statusCode() >= 300) {
log.error("Inference API failed. status={}, body={}", result.statusCode(), result.body());
throw new CustomApiException("BAD_GATEWAY", HttpStatus.BAD_GATEWAY);
}
// 5) 응답 파싱
try {
List<Map<String, Object>> list =
objectMapper.readValue(result.body(), new TypeReference<>() {});
if (list.isEmpty()) {
throw new CustomApiException(
"NOT_FOUND", HttpStatus.NOT_FOUND, "Inference response is empty");
}
Object batchIdObj = list.get(0).get("batch_id");
if (batchIdObj == null) {
throw new CustomApiException(
"NOT_FOUND", HttpStatus.NOT_FOUND, "batch_id not found in response");
}
return Long.valueOf(batchIdObj.toString());
} catch (Exception e) {
log.error("Failed to parse inference response. body={}", result.body(), e);
throw new CustomApiException("INVALID_INFERENCE_RESPONSE", HttpStatus.BAD_GATEWAY);
}
}
}

View File

@@ -0,0 +1,264 @@
package com.kamco.cd.kamcoback.common.inference.utils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
/**
* GeoJSON 파일의 "features[].properties.scene_id" 값들이 "요청한 도엽번호 목록(requestedMapSheetNums)"과 정확히 일치하는지
* 검증하는 유틸.
*
* <p>핵심 목적: - 요청한 도엽번호를 기반으로 GeoJSON을 생성했는데, 실제 결과 파일에 누락/추가/중복/빈값(scene_id 없음) 등이 발생했는지 빠르게 잡아내기.
*
* <p>검증 실패 시: - 404: 파일 자체가 없음 - 400: 파일이 비어있거나(0 byte), features 구조가 이상하거나, 요청 목록이 비어있음 - 500: 파일
* IO/파싱 자체가 실패(읽기 실패 등) - 422: 정합성(요청 vs 결과)이 맞지 않음 (누락/추가/중복/빈 scene_id 존재)
*/
public class GeoJsonValidator {
/** GeoJSON 파싱용 ObjectMapper (정적 1개로 재사용) */
private static final ObjectMapper om = new ObjectMapper();
/** 로그 출력용 */
private static final Logger log = LogManager.getLogger(GeoJsonValidator.class);
/**
* @param geojsonPath GeoJSON 파일 경로(문자열)
* @param requestedMapSheetNums "요청한 도엽번호" 리스트 (중복/공백/NULL 포함 가능)
* <p>동작 개요: 1) 파일 존재/크기 검증 2) 요청 도엽번호 목록 정리(Trim + 공백 제거 + 중복 제거) 3) GeoJSON 파싱 후 features 배열
* 확보 4) features에서 scene_id 추출하여 유니크 set 구성 5) requested vs found 비교: - missing: requested -
* found - extra : found - requested - duplicates: GeoJSON 내부에서 scene_id 중복 등장 - nullIdCount:
* scene_id가 null/blank 인 feature 개수 6) 이상 있으면 422로 실패 처리
*/
public static void validateWithRequested(String geojsonPath, List<String> requestedMapSheetNums) {
// 문자열 경로를 Path로 변환 (Files API 사용 목적)
Path path = Path.of(geojsonPath);
// =========================================================
// 1) 파일 기본 검증
// - 파일이 존재하는지
// - 파일 크기가 0인지(비어있으면 생성 실패/오류 가능성)
// =========================================================
try {
// 파일 존재 여부 체크 (없으면 404)
if (!Files.exists(path)) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "GeoJSON 파일이 존재하지 않습니다: " + geojsonPath);
}
// 파일 사이즈 체크 (0 byte면 400)
if (Files.size(path) == 0) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "GeoJSON 파일이 비어있습니다: " + geojsonPath);
}
} catch (IOException e) {
// 파일 사이즈/상태 확인 중 IO 오류면 서버오류로 처리
log.error("GeoJSON 파일 상태 확인 실패: path={}", path, e);
throw new ResponseStatusException(
HttpStatus.INTERNAL_SERVER_ERROR, "GeoJSON 파일 상태 확인 실패: " + geojsonPath, e);
}
// =========================================================
// 2) 요청 도엽 리스트 유효성 검증
// - 요청 목록 자체가 null/empty면 검증할 기준이 없으므로 400
// =========================================================
if (requestedMapSheetNums == null || requestedMapSheetNums.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "requestedMapSheetNums 가 비어있습니다.");
}
// =========================================================
// 2-1) 요청 도엽 Set 정리 (중복/공백/NULL 제거)
// - null 제거
// - trim 적용
// - 빈 문자열 제거
// - LinkedHashSet 사용: "중복 제거 + 원래 입력 순서 유지"
// =========================================================
Set<String> requested =
requestedMapSheetNums.stream()
.filter(Objects::nonNull) // null 제거
.map(String::trim) // 앞뒤 공백 제거
.filter(s -> !s.isEmpty()) // "" 제거
.collect(Collectors.toCollection(LinkedHashSet::new)); // 중복 제거 + 순서 유지
// 정리 결과가 비어있으면(전부 null/공백)이므로 400
if (requested.isEmpty()) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "requestedMapSheetNums 가 공백/NULL만 포함합니다.");
}
// =========================================================
// 3) GeoJSON 파싱
// 기대 구조:
// {
// "type": "FeatureCollection",
// "features": [ ... ]
// }
//
// - features가 없거나 배열이 아니면 "유효하지 않은 GeoJSON" (400)
// - 파일 읽기/파싱 IO 문제는 500
// - JSON 자체가 깨진 경우는 400
// =========================================================
final JsonNode features;
try {
// JSON 파일을 트리 형태로 파싱
JsonNode root = om.readTree(path.toFile());
// GeoJSON FeatureCollection의 핵심은 features 배열
features = root.get("features");
// features가 없거나 배열이 아니면 GeoJSON 구조가 이상한 것
if (features == null || !features.isArray()) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "유효하지 않은 GeoJSON: features가 없거나 배열이 아닙니다.");
}
} catch (ResponseStatusException e) {
// 위에서 직접 던진 에러는 그대로 전달
throw e;
} catch (IOException e) {
// 읽기/파싱 과정에서 IO 문제가 터지면 서버오류
log.error("GeoJSON 파일 읽기/파싱 실패: path={}", path, e);
throw new ResponseStatusException(
HttpStatus.INTERNAL_SERVER_ERROR, "GeoJSON 파일 읽기/파싱 실패: " + geojsonPath, e);
} catch (Exception e) {
// JSON 문법 오류/예상치 못한 파싱 오류는 클라이언트 입력/파일 자체 문제로 400 처리
log.error("GeoJSON 파싱 오류(비정상 JSON): path={}", path, e);
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "GeoJSON 파싱 오류(비정상 JSON): " + geojsonPath, e);
}
// =========================================================
// 4) 검증 로직
// - featureCount: 전체 feature 수 (중복 포함)
// - foundUnique: GeoJSON에 등장한 유니크 scene_id 집합
// - duplicates: GeoJSON 내부에서 scene_id가 중복된 목록(샘플 출력용)
// - nullIdCount: scene_id가 없거나 빈 값인 feature 개수
// =========================================================
int featureCount = features.size();
// 유니크 scene_id를 담는 Set (중복 판단을 위해 add 결과를 사용)
Set<String> foundUnique = new HashSet<>();
// 중복된 scene_id 목록 (샘플 로그 출력용이라 순서 유지 가능한 LinkedHashSet 사용)
Set<String> duplicates = new LinkedHashSet<>();
// scene_id가 null 또는 blank인 feature의 개수 (데이터 이상)
int nullIdCount = 0;
// ---------------------------------------------------------
// features를 돌면서 feature.properties.scene_id를 추출한다.
//
// 기대 구조(일반적):
// features[i] = {
// "type": "Feature",
// "properties": {
// "scene_id": "도엽번호"
// },
// "geometry": {...}
// }
// ---------------------------------------------------------
for (JsonNode feature : features) {
JsonNode props = feature.get("properties");
// properties가 있고 scene_id가 null이 아니면 텍스트로 읽음
// 없으면 null 처리
String sceneId =
(props != null && props.hasNonNull("scene_id")) ? props.get("scene_id").asText() : null;
// scene_id가 없거나 빈값이면 "정상적으로 도엽번호가 들어오지 않은 feature"로 카운트
if (sceneId == null || sceneId.isBlank()) {
nullIdCount++; // 도엽번호가 없으면 증가
continue;
}
// foundUnique.add(sceneId)가 false면 "이미 같은 값이 있었다"는 뜻 => 중복
if (!foundUnique.add(sceneId)) {
duplicates.add(sceneId);
}
}
// =========================================================
// 4-1) requested vs found 비교(set 차집합)
//
// missing = requested - found
// : 요청은 했는데 결과 GeoJSON에 없는 도엽번호
//
// extra = found - requested
// : 요청하지 않았는데 결과 GeoJSON에 들어간 도엽번호
// =========================================================
// missing: requested를 복사한 뒤(foundUnique에 있는 값들을 제거) => 남은 것이 누락분
Set<String> missing = new LinkedHashSet<>(requested);
missing.removeAll(foundUnique);
// extra: foundUnique를 복사한 뒤(requested에 있는 값들을 제거) => 남은 것이 추가분
Set<String> extra = new LinkedHashSet<>(foundUnique);
extra.removeAll(requested);
// =========================================================
// 5) 로그 출력
// - 운영에서 문제 생겼을 때 "요청 vs 생성 결과"를 한 눈에 보게
// - sample 로그는 너무 길어질 수 있으므로 limit 걸어줌
// =========================================================
log.info(
"""
===== GeoJSON Validation =====
file: {}
features(total): {}
requested(unique): {}
found(unique scene_id): {}
scene_id null/blank: {}
duplicates(scene_id): {}
missing(requested - found): {}
extra(found - requested): {}
==============================
""",
geojsonPath,
featureCount, // 중복 포함한 전체 feature 수
requested.size(), // 요청 도엽 유니크 수
foundUnique.size(), // GeoJSON에서 발견된 scene_id 유니크 수
nullIdCount, // scene_id가 비어있는 feature 수
duplicates.size(), // 중복 scene_id 종류 수
missing.size(), // 요청했지만 빠진 도엽 수
extra.size()); // 요청하지 않았는데 들어온 도엽 수
// 중복/누락/추가 항목은 전체를 다 찍으면 로그 폭발하므로 샘플만
if (!duplicates.isEmpty())
log.warn("duplicates sample: {}", duplicates.stream().limit(20).toList());
if (!missing.isEmpty()) log.warn("missing sample: {}", missing.stream().limit(50).toList());
if (!extra.isEmpty()) log.warn("extra sample: {}", extra.stream().limit(50).toList());
// =========================================================
// 6) 실패 조건 판정
//
// 아래 중 하나라도 있으면 "요청 대비 결과 정합성이 깨졌다"로 보고 실패 처리(422):
// - missing 존재: 요청했는데 결과에 없음
// - extra 존재 : 요청 안했는데 결과에 있음
// - duplicates 존재: 동일 도엽이 중복 생성됨
// - nullIdCount > 0: scene_id가 비어있는 feature가 있음(데이터 이상)
//
// 422(Unprocessable Entity):
// - 요청 문법은 맞지만(파일은 있고 JSON도 읽힘),
// 내용(정합성)이 요구사항을 만족하지 못하는 경우에 적합.
// =========================================================
if (!missing.isEmpty() || !extra.isEmpty() || !duplicates.isEmpty() || nullIdCount > 0) {
throw new ResponseStatusException(
HttpStatus.UNPROCESSABLE_ENTITY,
String.format(
"GeoJSON validation failed: missing=%d, extra=%d, duplicates=%d, nullId=%d",
missing.size(), extra.size(), duplicates.size(), nullIdCount));
}
// 모든 조건을 통과하면 정상
log.info("GeoJSON validation OK");
}
}

View File

@@ -13,6 +13,7 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
/** 파일 다운로드 log 저장 */
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -30,9 +31,17 @@ public class FileDownloadInteceptor implements HandlerInterceptor {
if (request.getDispatcherType() != DispatcherType.REQUEST) return; if (request.getDispatcherType() != DispatcherType.REQUEST) return;
Long userId; Long userId;
String employeeNo = "";
try { try {
// a 링크 다운로드일경우 userId가 없으므로 전달받은 사번을 넣는다
userId = userUtil.getId(); userId = userUtil.getId();
if (userId == null) return; // userId null 불가면 스킵 if (userId == null) {
employeeNo = request.getParameter("employeeNo");
if (employeeNo == null) {
return;
}
}
} catch (Exception e) { } catch (Exception e) {
log.warn("Download audit userId resolve failed. uri={}, err={}", uri, e.toString()); log.warn("Download audit userId resolve failed. uri={}, err={}", uri, e.toString());
return; return;
@@ -48,8 +57,9 @@ public class FileDownloadInteceptor implements HandlerInterceptor {
return; // downloadUuid null 불가 -> 스킵 return; // downloadUuid null 불가 -> 스킵
} }
// log저장 DownloadAuditEventListener 클래스 호출
publisher.publishEvent( publisher.publishEvent(
new DownloadAuditEvent(userId, uri, normalizedUri, ip, status, downloadUuid)); new DownloadAuditEvent(userId, employeeNo, uri, normalizedUri, ip, status, downloadUuid));
} }
private UUID extractUuidFromUri(String uri) { private UUID extractUuidFromUri(String uri) {

View File

@@ -0,0 +1,35 @@
package com.kamco.cd.kamcoback.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "file")
public class FileProperties {
private String root;
private String nfs;
private String syncRootDir;
private String syncTmpDir;
private String syncFileExtention;
private String datasetDir;
private String datasetTmpDir;
private String modelDir;
private String modelTmpDir;
private String modelFileExtention;
private String ptPath;
private String datasetResponse;
private TrainingData trainingData;
private String outputDir;
@Getter
@Setter
public static class TrainingData {
private String geojsonDir;
}
}

View File

@@ -0,0 +1,20 @@
package com.kamco.cd.kamcoback.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "inference")
public class InferenceProperties {
private String nfs;
private String url;
private String batchUrl;
private String geojsonDir;
private String jarPath;
private String inferenceServerName;
}

View File

@@ -77,7 +77,7 @@ public class SecurityConfig {
// 다운로드는 인증 필요 // 다운로드는 인증 필요
.requestMatchers(HttpMethod.GET, DownloadPaths.PATTERNS) .requestMatchers(HttpMethod.GET, DownloadPaths.PATTERNS)
.authenticated() .permitAll()
// 메뉴 등록 ADMIN만 가능 // 메뉴 등록 ADMIN만 가능
.requestMatchers(HttpMethod.POST, "/api/menu/auth") .requestMatchers(HttpMethod.POST, "/api/menu/auth")
@@ -102,6 +102,7 @@ public class SecurityConfig {
"/api/upload/file-chunk-upload", "/api/upload/file-chunk-upload",
"/api/upload/chunk-upload-complete", "/api/upload/chunk-upload-complete",
"/api/change-detection/**", "/api/change-detection/**",
"/api/members/*/password",
"/api/layer/map/**", "/api/layer/map/**",
"/api/layer/tile-url", "/api/layer/tile-url",
"/api/layer/tile-url-year", "/api/layer/tile-url-year",
@@ -111,7 +112,6 @@ public class SecurityConfig {
.requestMatchers( .requestMatchers(
"/api/user/**", "/api/user/**",
"/api/my/menus", "/api/my/menus",
"/api/members/*/password",
"/api/training-data/label/**", "/api/training-data/label/**",
"/api/training-data/review/**") "/api/training-data/review/**")
.authenticated() .authenticated()

View File

@@ -16,6 +16,8 @@ public class StartupLogger {
private final Environment environment; private final Environment environment;
private final DataSource dataSource; private final DataSource dataSource;
private final FileProperties fileProperties;
private final InferenceProperties inferenceProperties;
@EventListener(ApplicationReadyEvent.class) @EventListener(ApplicationReadyEvent.class)
public void logStartupInfo() { public void logStartupInfo() {
@@ -79,6 +81,26 @@ public class StartupLogger {
│ DDL Auto : %s │ DDL Auto : %s
│ JDBC Batch Size : %s │ JDBC Batch Size : %s
│ Fetch Batch Size : %s │ Fetch Batch Size : %s
╠════════════════════════════════════════════════════════════════════════════════╣
║ FILE CONFIGURATION ║
╠────────────────────────────────────────────────────────────────────────────────╣
│ Root Directory : %s
│ NFS Mount Path : %s
│ Sync Root Dir : %s
│ Sync Tmp Dir : %s
│ Dataset Dir : %s
│ Model Dir : %s
│ PT Path : %s
│ Output Dir : %s
╠════════════════════════════════════════════════════════════════════════════════╣
║ INFERENCE CONFIGURATION ║
╠────────────────────────────────────────────────────────────────────────────────╣
│ NFS Mount Path : %s
│ Inference URL : %s
│ Batch URL : %s
│ GeoJSON Dir : %s
│ JAR Path : %s
│ Server Names : %s
╚════════════════════════════════════════════════════════════════════════════════╝ ╚════════════════════════════════════════════════════════════════════════════════╝
""", """,
profileInfo, profileInfo,
@@ -89,7 +111,25 @@ public class StartupLogger {
showSql, showSql,
ddlAuto, ddlAuto,
batchSize, batchSize,
batchFetchSize); batchFetchSize,
fileProperties.getRoot() != null ? fileProperties.getRoot() : "N/A",
fileProperties.getNfs() != null ? fileProperties.getNfs() : "N/A",
fileProperties.getSyncRootDir() != null ? fileProperties.getSyncRootDir() : "N/A",
fileProperties.getSyncTmpDir() != null ? fileProperties.getSyncTmpDir() : "N/A",
fileProperties.getDatasetDir() != null ? fileProperties.getDatasetDir() : "N/A",
fileProperties.getModelDir() != null ? fileProperties.getModelDir() : "N/A",
fileProperties.getPtPath() != null ? fileProperties.getPtPath() : "N/A",
fileProperties.getOutputDir() != null ? fileProperties.getOutputDir() : "N/A",
inferenceProperties.getNfs() != null ? inferenceProperties.getNfs() : "N/A",
inferenceProperties.getUrl() != null ? inferenceProperties.getUrl() : "N/A",
inferenceProperties.getBatchUrl() != null ? inferenceProperties.getBatchUrl() : "N/A",
inferenceProperties.getGeojsonDir() != null
? inferenceProperties.getGeojsonDir()
: "N/A",
inferenceProperties.getJarPath() != null ? inferenceProperties.getJarPath() : "N/A",
inferenceProperties.getInferenceServerName() != null
? inferenceProperties.getInferenceServerName()
: "N/A");
log.info(startupMessage); log.info(startupMessage);
} }

View File

@@ -173,8 +173,9 @@ public class ApiResponseDto<T> {
+ "To reset your password again, please submit a new request through \"Forgot" + "To reset your password again, please submit a new request through \"Forgot"
+ " Password.\""), + " Password.\""),
PAYLOAD_TOO_LARGE("업로드 용량 제한을 초과했습니다."), PAYLOAD_TOO_LARGE("업로드 용량 제한을 초과했습니다."),
NOT_FOUND_TARGET_YEAR("기준도 도엽을 찾을 수 없습니다."), NOT_FOUND_TARGET_YEAR("기준도 도엽을 찾을 수 없습니다."),
NOT_FOUND_COMPARE_YEAR("비교도 도엽을 찾을 수 없습니다."), NOT_FOUND_COMPARE_YEAR("비교도 도엽을 찾을 수 없습니다."),
NOT_FOUND_MAP_SHEET_NUM("추론 가능한 도엽이 없습니다."),
FAIL_SAVE_MAP_SHEET("도엽 저장 중 오류가 발생했습니다."), FAIL_SAVE_MAP_SHEET("도엽 저장 중 오류가 발생했습니다."),
FAIL_CREATE_MAP_SHEET_FILE("도엽 설정파일 생성 중 오류가 발생했습니다."), FAIL_CREATE_MAP_SHEET_FILE("도엽 설정파일 생성 중 오류가 발생했습니다."),
; ;

View File

@@ -3,8 +3,8 @@ package com.kamco.cd.kamcoback.config.resttemplate;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@@ -14,54 +14,86 @@ import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
@RequiredArgsConstructor
@Component @Component
@Log4j2 @Log4j2
public class ExternalHttpClient { public class ExternalHttpClient {
private final RestTemplate restTemplate; private final RestTemplate restTemplate; // short (@Primary)
private final RestTemplate restTemplateLong; // long
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
public ExternalHttpClient(
RestTemplate restTemplate,
@Qualifier("restTemplateLong") RestTemplate restTemplateLong,
ObjectMapper objectMapper) {
this.restTemplate = restTemplate;
this.restTemplateLong = restTemplateLong;
this.objectMapper = objectMapper;
}
/** 기본(짧은 timeout) 호출 */
public <T> ExternalCallResult<T> call( public <T> ExternalCallResult<T> call(
String url, HttpMethod method, Object body, HttpHeaders headers, Class<T> responseType) { String url, HttpMethod method, Object body, HttpHeaders headers, Class<T> responseType) {
// responseType 기반으로 Accept 동적 세팅 return doCall(restTemplate, url, method, body, headers, responseType);
}
/** 추론/대용량 전용 (긴 timeout) */
public <T> ExternalCallResult<T> callLong(
String url, HttpMethod method, Object body, HttpHeaders headers, Class<T> responseType) {
return doCall(restTemplateLong, url, method, body, headers, responseType);
}
private <T> ExternalCallResult<T> doCall(
RestTemplate rt,
String url,
HttpMethod method,
Object body,
HttpHeaders headers,
Class<T> responseType) {
HttpHeaders resolvedHeaders = resolveHeaders(headers, responseType); HttpHeaders resolvedHeaders = resolveHeaders(headers, responseType);
logRequestBody(body); logRequestBody(body);
HttpEntity<Object> entity = new HttpEntity<>(body, resolvedHeaders); HttpEntity<Object> entity = new HttpEntity<>(body, resolvedHeaders);
try { try {
// String: raw bytes -> UTF-8 string
// String 응답은 raw byte로 받아 UTF-8 변환
if (responseType == String.class) { if (responseType == String.class) {
ResponseEntity<byte[]> res = restTemplate.exchange(url, method, entity, byte[].class); ResponseEntity<byte[]> res = rt.exchange(url, method, entity, byte[].class);
String raw = String raw =
(res.getBody() == null) ? null : new String(res.getBody(), StandardCharsets.UTF_8); (res.getBody() == null) ? null : new String(res.getBody(), StandardCharsets.UTF_8);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
T casted = (T) raw; T casted = (T) raw;
return new ExternalCallResult<>(res.getStatusCodeValue(), true, casted, null); return new ExternalCallResult<>(res.getStatusCodeValue(), true, casted, null);
} }
// byte[]: raw bytes로 받고, JSON이면 에러로 처리 // byte[] 응답 처리
if (responseType == byte[].class) { if (responseType == byte[].class) {
ResponseEntity<byte[]> res = restTemplate.exchange(url, method, entity, byte[].class); ResponseEntity<byte[]> res = rt.exchange(url, method, entity, byte[].class);
MediaType ct = res.getHeaders().getContentType(); MediaType ct = res.getHeaders().getContentType();
byte[] bytes = res.getBody(); byte[] bytes = res.getBody();
// JSON이면 에러로 간주
if (isJsonLike(ct)) { if (isJsonLike(ct)) {
String err = (bytes == null) ? null : new String(bytes, StandardCharsets.UTF_8); String err = (bytes == null) ? null : new String(bytes, StandardCharsets.UTF_8);
return new ExternalCallResult<>(res.getStatusCodeValue(), false, null, err); return new ExternalCallResult<>(res.getStatusCodeValue(), false, null, err);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
T casted = (T) bytes; T casted = (T) bytes;
return new ExternalCallResult<>(res.getStatusCodeValue(), true, casted, null); return new ExternalCallResult<>(res.getStatusCodeValue(), true, casted, null);
} }
// DTO 등: 일반 역직렬화 // DTO 응답
ResponseEntity<T> res = restTemplate.exchange(url, method, entity, responseType); ResponseEntity<T> res = rt.exchange(url, method, entity, responseType);
return new ExternalCallResult<>(res.getStatusCodeValue(), true, res.getBody(), null); return new ExternalCallResult<>(res.getStatusCodeValue(), true, res.getBody(), null);
} catch (HttpStatusCodeException e) { } catch (HttpStatusCodeException e) {
@@ -70,29 +102,28 @@ public class ExternalHttpClient {
} }
} }
// 기존 resolveJsonHeaders를 "동적"으로 교체 /** Accept / Content-Type 자동 처리 */
private HttpHeaders resolveHeaders(HttpHeaders headers, Class<?> responseType) { private HttpHeaders resolveHeaders(HttpHeaders headers, Class<?> responseType) {
// 원본 headers를 그대로 쓰면 외부에서 재사용할 때 사이드이펙트 날 수 있어서 복사 권장
HttpHeaders h = (headers == null) ? new HttpHeaders() : new HttpHeaders(headers); HttpHeaders h = (headers == null) ? new HttpHeaders() : new HttpHeaders(headers);
// 요청 바디 기본은 JSON이라고 가정 (필요하면 호출부에서 덮어쓰기) // 기본 Content-Type
if (h.getContentType() == null) { if (h.getContentType() == null) {
h.setContentType(MediaType.APPLICATION_JSON); h.setContentType(MediaType.APPLICATION_JSON);
} }
// 호출부에서 Accept를 명시했으면 존중 // Accept 이미 있으면 존중
if (h.getAccept() != null && !h.getAccept().isEmpty()) { if (h.getAccept() != null && !h.getAccept().isEmpty()) {
return h; return h;
} }
// responseType Accept 자동 지정 // 응답 타입 Accept 자동 지정
if (responseType == byte[].class) { if (responseType == byte[].class) {
h.setAccept( h.setAccept(
List.of( List.of(
MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_OCTET_STREAM,
MediaType.valueOf("application/zip"), MediaType.valueOf("application/zip"),
MediaType.APPLICATION_JSON // 실패(JSON 에러 바디) 대비 MediaType.APPLICATION_JSON));
));
} else { } else {
h.setAccept(List.of(MediaType.APPLICATION_JSON)); h.setAccept(List.of(MediaType.APPLICATION_JSON));
} }
@@ -100,12 +131,15 @@ public class ExternalHttpClient {
return h; return h;
} }
/** JSON 응답 여부 체크 */
private boolean isJsonLike(MediaType ct) { private boolean isJsonLike(MediaType ct) {
if (ct == null) return false; if (ct == null) return false;
return ct.includes(MediaType.APPLICATION_JSON) return ct.includes(MediaType.APPLICATION_JSON)
|| "application/problem+json".equalsIgnoreCase(ct.toString()); || "application/problem+json".equalsIgnoreCase(ct.toString());
} }
/** 요청 바디 로그 */
private void logRequestBody(Object body) { private void logRequestBody(Object body) {
try { try {
if (body != null) { if (body != null) {

View File

@@ -4,6 +4,7 @@ import lombok.extern.log4j.Log4j2;
import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.client.BufferingClientHttpRequestFactory; import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
@@ -13,10 +14,20 @@ import org.springframework.web.client.RestTemplate;
public class RestTemplateConfig { public class RestTemplateConfig {
@Bean @Bean
@Primary
public RestTemplate restTemplate(RestTemplateBuilder builder) { public RestTemplate restTemplate(RestTemplateBuilder builder) {
return build(builder, 2000, 3000);
}
@Bean("restTemplateLong")
public RestTemplate restTemplateLong(RestTemplateBuilder builder) {
return build(builder, 2000, 60000);
}
private RestTemplate build(RestTemplateBuilder builder, int connectTimeoutMs, int readTimeoutMs) {
SimpleClientHttpRequestFactory baseFactory = new SimpleClientHttpRequestFactory(); SimpleClientHttpRequestFactory baseFactory = new SimpleClientHttpRequestFactory();
baseFactory.setConnectTimeout(2000); baseFactory.setConnectTimeout(connectTimeoutMs);
baseFactory.setReadTimeout(3000); baseFactory.setReadTimeout(readTimeoutMs);
RestTemplate rt = RestTemplate rt =
builder builder
@@ -24,10 +35,8 @@ public class RestTemplateConfig {
.additionalInterceptors(new RetryInterceptor()) .additionalInterceptors(new RetryInterceptor())
.build(); .build();
// byte[] 응답은 무조건 raw로 읽게 강제 (Jackson이 끼어들 여지 제거)
rt.getMessageConverters() rt.getMessageConverters()
.add(0, new org.springframework.http.converter.ByteArrayHttpMessageConverter()); .add(0, new org.springframework.http.converter.ByteArrayHttpMessageConverter());
return rt; return rt;
} }
} }

View File

@@ -322,4 +322,23 @@ public class GukYuinApiController {
@PathVariable String chnDtctObjtId) { @PathVariable String chnDtctObjtId) {
return ApiResponseDto.ok(gukYuinApiService.findRlbDtctObject(chnDtctObjtId)); return ApiResponseDto.ok(gukYuinApiService.findRlbDtctObject(chnDtctObjtId));
} }
@Operation(summary = "실태조사 적합여부 업데이트", description = "실태조사 적합여부 업데이트")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "등록 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = DetectMastReq.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/rlb/objt/{chnDtctObjtId}/stblt/{stbltYn}")
public ApiResponseDto<ChngDetectContDto.ResultLabelDto> updateStbltObjtYn(
@PathVariable String chnDtctObjtId, @PathVariable String stbltYn) {
return ApiResponseDto.ok(gukYuinApiService.updateStbltObjtYn(chnDtctObjtId, stbltYn, ""));
}
} }

View File

@@ -69,8 +69,11 @@ public class GukYuinApiService {
@Value("${file.nfs}") @Value("${file.nfs}")
private String nfs; private String nfs;
// @Value("${file.dataset-dir}") @Value("${file.output-dir}")
// private String datasetDir; private String outputDir;
@Value("${file.dataset-dir}")
private String datasetDir;
@Transactional @Transactional
public ChngDetectMastDto.RegistResDto regist( public ChngDetectMastDto.RegistResDto regist(
@@ -246,9 +249,9 @@ public class GukYuinApiService {
return GukYuinLinkFailCode.NOT_FOUND; return GukYuinLinkFailCode.NOT_FOUND;
} }
if (f.isPartScope()) { // if (f.isPartScope()) {
return GukYuinLinkFailCode.SCOPE_PART_NOT_ALLOWED; // return GukYuinLinkFailCode.SCOPE_PART_NOT_ALLOWED;
} // }
if (f.hasRunningInference()) { if (f.hasRunningInference()) {
return GukYuinLinkFailCode.HAS_RUNNING_INFERENCE; return GukYuinLinkFailCode.HAS_RUNNING_INFERENCE;
@@ -456,10 +459,9 @@ public class GukYuinApiService {
return new ResponseObj(ApiResponseCode.DUPLICATE_DATA, "이미 국유인 연동을 한 회차입니다."); return new ResponseObj(ApiResponseCode.DUPLICATE_DATA, "이미 국유인 연동을 한 회차입니다.");
} }
// String kamconfsDatasetExportPathfsDatasetExportPath = "/kamco-nfs/dataset/export/"; // 추론 shp 파일 생성되는 위치
String kamconfsDatasetExportPathfsDatasetExportPath = log.info("datasetDir path : " + datasetDir + info.getUid());
String.format("%s%s", nfs, "/dataset/export/"); if (!Files.isDirectory(Path.of(datasetDir + info.getUid()))) {
if (!Files.isDirectory(Path.of(kamconfsDatasetExportPathfsDatasetExportPath + info.getUid()))) {
return new ResponseObj( return new ResponseObj(
ApiResponseCode.NOT_FOUND_DATA, "파일 경로에 회차 실행 파일이 생성되지 않았습니다. 확인 부탁드립니다."); ApiResponseCode.NOT_FOUND_DATA, "파일 경로에 회차 실행 파일이 생성되지 않았습니다. 확인 부탁드립니다.");
} }
@@ -469,6 +471,9 @@ public class GukYuinApiService {
gukyuinCoreService.findMapSheetLearnYearStage(info.getCompareYyyy(), info.getTargetYyyy()); gukyuinCoreService.findMapSheetLearnYearStage(info.getCompareYyyy(), info.getTargetYyyy());
// reqDto 셋팅 // reqDto 셋팅
// 마운트된 추론 shp 파일 생성되는 위치
log.info("outputDir path : " + outputDir + info.getUid());
String kamconfsDatasetExportPathfsDatasetExportPath = outputDir;
ChnDetectMastReqDto reqDto = new ChnDetectMastReqDto(); ChnDetectMastReqDto reqDto = new ChnDetectMastReqDto();
reqDto.setCprsYr(String.valueOf(info.getCompareYyyy())); reqDto.setCprsYr(String.valueOf(info.getCompareYyyy()));
reqDto.setCrtrYr(String.valueOf(info.getTargetYyyy())); reqDto.setCrtrYr(String.valueOf(info.getTargetYyyy()));
@@ -586,4 +591,41 @@ public class GukYuinApiService {
result.body() != null && result.body().getSuccess()); result.body() != null && result.body().getSuccess());
return result.body(); return result.body();
} }
public ChngDetectContDto.ResultLabelDto updateStbltObjtYn(
String chnDtctObjtId, String stbltYn, String batchYn) {
String url = gukyuinCdiUrl + "/rlb/objt/" + chnDtctObjtId + "/stblt/" + stbltYn;
ReqInfo info = new ReqInfo();
info.setReqIp(myip);
info.setReqEpno("Y".equals(batchYn) ? "BATCH" : userUtil.getEmployeeNo());
ExternalCallResult<ChngDetectContDto.ResultLabelDto> result =
externalHttpClient.call(
url,
HttpMethod.POST,
info,
netUtils.jsonHeaders(),
ChngDetectContDto.ResultLabelDto.class);
this.insertGukyuinAuditLog(
EventType.MODIFIED.getId(),
netUtils.getLocalIP(),
userUtil.getId(),
url.replace(gukyuinUrl, ""),
null,
result.body().getSuccess());
return result.body();
}
public void stbltBulkUpdate(List<String> objectIds) {
for (String objectId : objectIds) {
this.updateStbltObjtYn(objectId, "Y", "");
}
}
public List<String> findStbltObjectIds(String uid, String mapSheetNum) {
return gukyuinCoreService.findStbltObjectIds(uid, mapSheetNum);
}
} }

View File

@@ -0,0 +1,43 @@
package com.kamco.cd.kamcoback.inference;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto;
import com.kamco.cd.kamcoback.inference.service.InferenceManualService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "추론결과 데이터 생성", description = "추론결과 데이터 생성 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/inference/manual")
public class InferenceManualApiController {
private final InferenceManualService inferenceManualService;
@Operation(summary = "추론 결과로 추론 목록 및 shp 생성", description = "추론 결과로 추론 목록 및 shp 생성")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "데이터 저장 성공",
content =
@Content(
mediaType = "application/json",
schema =
@Schema(implementation = InferenceResultShpDto.InferenceCntDto.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/save")
public void saveTesting(List<Long> batchIds) {
inferenceManualService.saveResultsTesting(batchIds);
}
}

View File

@@ -18,7 +18,6 @@ import com.kamco.cd.kamcoback.model.dto.ModelMngDto;
import com.kamco.cd.kamcoback.model.service.ModelMngService; import com.kamco.cd.kamcoback.model.service.ModelMngService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@@ -58,7 +57,8 @@ public class InferenceResultApiController {
private final ModelMngService modelMngService; private final ModelMngService modelMngService;
private final RangeDownloadResponder rangeDownloadResponder; private final RangeDownloadResponder rangeDownloadResponder;
@Operation(summary = "추론관리 목록", description = "어드민 홈 > 추론관리 > 추론관리 > 추론관리 목록") /** 추론관리 목록 화면에서 호출 */
@Operation(summary = "추론관리 목록", description = "추론관리 > 추론관리 목록 ")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -90,7 +90,8 @@ public class InferenceResultApiController {
return ApiResponseDto.ok(analResList); return ApiResponseDto.ok(analResList);
} }
@Operation(summary = "추론 진행 여부 확인", description = "어드민 홈 > 추론관리 > 추론관리 > 추론관리 목록") /** 추론관리 목록 화면에서 호출 */
@Operation(summary = "추론 진행 여부 확인", description = "추론관리 > 추론관리 목록")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -112,7 +113,8 @@ public class InferenceResultApiController {
return ApiResponseDto.ok(inferenceResultService.getProcessing()); return ApiResponseDto.ok(inferenceResultService.getProcessing());
} }
@Operation(summary = "년도 목록 조회", description = "어드민 홈 > 추론관리 > 추론목록 > 변화탐지 실행 정보 입력 > 년도 목록 조회") /** 추론관리 목록 화면에서 호출 */
@Operation(summary = "년도 목록 조회", description = "추론관리 > 추론목록 > 변화탐지 실행 정보 입력 > 년도 목록 조회")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -130,7 +132,8 @@ public class InferenceResultApiController {
return ApiResponseDto.ok(mapSheetMngService.findMapSheetMngDoneYyyyList()); return ApiResponseDto.ok(mapSheetMngService.findMapSheetMngDoneYyyyList());
} }
@Operation(summary = "변화탐지 실행 정보 입력", description = "어드민 홈 > 추론관리 > 추론목록 > 변화탐지 실행 정보 입력") /** 변화탐지 실행 정보 입력화면에서 호출 */
@Operation(summary = "변화탐지 실행 정보 입력, 추론실행", description = "추론관리 > 추론목록 > 변화탐지 실행 정보 입력")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -155,7 +158,8 @@ public class InferenceResultApiController {
return ApiResponseDto.ok(uuid); return ApiResponseDto.ok(uuid);
} }
@Operation(summary = "추론 종료", description = "추론 종료") /** 추론진행 현황 화면에서 호출 */
@Operation(summary = "추론 종료", description = "추론관리 > 추론목록 > 추론진행 현황")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -174,7 +178,8 @@ public class InferenceResultApiController {
return ApiResponseDto.ok(uuid); return ApiResponseDto.ok(uuid);
} }
@Operation(summary = "분석 모델 선택 조회", description = "변화탐지 실행 정보 입력 모델선택 팝업 ") /** 변화탐지 실행 정보 입력화면에서 호출 */
@Operation(summary = "분석 모델 선택 조회", description = "추론관리 > 추론목록 > 변화탐지 실행 정보 입력 > 모델선택 팝업 ")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -205,7 +210,8 @@ public class InferenceResultApiController {
return ApiResponseDto.ok(result); return ApiResponseDto.ok(result);
} }
@Operation(summary = "추론관리 추론진행 서버 현황", description = "추론관리 추론진행 서버 현황") /** 추론진행 현황 화면에서 호출 */
@Operation(summary = "추론관리 추론진행 서버 현황", description = "추론관리 > 추론목록 > 추론진행 현황")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -224,7 +230,8 @@ public class InferenceResultApiController {
return ApiResponseDto.ok(inferenceResultService.getInferenceServerStatusList()); return ApiResponseDto.ok(inferenceResultService.getInferenceServerStatusList());
} }
@Operation(summary = "추론관리 진행현황 상세", description = "어드민 홈 > 추론관리 > 추론관리 > 진행현황 상세") /** 추론진행 현황 화면에서 호출 */
@Operation(summary = "추론관리 진행현황 상세", description = "추론관리 > 추론진행 현황")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -248,7 +255,8 @@ public class InferenceResultApiController {
return ApiResponseDto.ok(inferenceResultService.getInferenceStatus(uuid)); return ApiResponseDto.ok(inferenceResultService.getInferenceStatus(uuid));
} }
@Operation(summary = "추론결과 기본정보", description = "추론결과 기본정보") /** 추론결과 화면에서 호출 */
@Operation(summary = "추론결과 기본정보", description = "추론관리 > 추론결과")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -269,7 +277,8 @@ public class InferenceResultApiController {
return ApiResponseDto.ok(inferenceResultService.getInferenceResultInfo(uuid)); return ApiResponseDto.ok(inferenceResultService.getInferenceResultInfo(uuid));
} }
@Operation(summary = "추론결과 분류별 탐지 건수", description = "추론결과 분류별 탐지 건수") /** 추론결과 화면에서 호출 */
@Operation(summary = "추론결과 분류별 탐지 건수", description = "추론관리 > 추론결과")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -290,6 +299,7 @@ public class InferenceResultApiController {
return ApiResponseDto.ok(inferenceResultService.getInferenceClassCountList(uuid)); return ApiResponseDto.ok(inferenceResultService.getInferenceClassCountList(uuid));
} }
/** 추론결과 화면에서 호출 */
@Operation(summary = "추론관리 분석결과 상세 목록", description = "추론관리 분석결과 상세 목록 geojson 데이터 조회") @Operation(summary = "추론관리 분석결과 상세 목록", description = "추론관리 분석결과 상세 목록 geojson 데이터 조회")
@ApiResponses( @ApiResponses(
value = { value = {
@@ -329,26 +339,14 @@ public class InferenceResultApiController {
return ApiResponseDto.ok(geomList); return ApiResponseDto.ok(geomList);
} }
@Operation( /** 추론결과 화면에서 호출 */
summary = "shp 파일 다운로드", /** 다운로드는 a 링크로 받는걸로 변경되어 사번을 파라미터로 받아서 로그에 저장하는걸로 변경함 */
description = "추론관리 분석결과 shp 파일 다운로드", @Operation(summary = "shp 파일 다운로드", description = "추론관리 분석결과 shp 파일 다운로드")
parameters = {
@Parameter(
name = "kamco-download-uuid",
in = ParameterIn.HEADER,
required = true,
description = "다운로드 요청 UUID",
schema =
@Schema(
type = "string",
format = "uuid",
example = "69c4e56c-e0bf-4742-9225-bba9aae39052"))
})
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "shp zip파일 다운로드", description = "shp 파일 다운로드",
content = content =
@Content( @Content(
mediaType = "application/octet-stream", mediaType = "application/octet-stream",
@@ -357,13 +355,16 @@ public class InferenceResultApiController {
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
}) })
@GetMapping("/download/{uuid}") @GetMapping("/download/{uuid}")
public ResponseEntity<?> download(@PathVariable UUID uuid, HttpServletRequest request) public ResponseEntity<?> download(
@PathVariable UUID uuid,
@Parameter(description = "사번", example = "123456") @RequestParam String employeeNo,
HttpServletRequest request)
throws IOException { throws IOException {
String path; String path;
String uid; String uid;
try { try {
// 추론결과 shp zip 파일 확인하여 다운로드 경로 생성
Map<String, Object> map = inferenceResultService.shpDownloadPath(uuid); Map<String, Object> map = inferenceResultService.shpDownloadPath(uuid);
path = String.valueOf(map.get("path")); path = String.valueOf(map.get("path"));
uid = String.valueOf(map.get("uid")); uid = String.valueOf(map.get("uid"));
@@ -373,10 +374,10 @@ public class InferenceResultApiController {
Path zipPath = Path.of(path); Path zipPath = Path.of(path);
// Range + 200/206/416 공통 처리 (추가 헤더 포함)
return rangeDownloadResponder.buildZipResponse(zipPath, uid + ".zip", request); return rangeDownloadResponder.buildZipResponse(zipPath, uid + ".zip", request);
} }
/** 추론결과 화면에서 호출 */
@Operation(summary = "shp 파일 다운로드 이력 조회", description = "추론관리 분석결과 shp 파일 다운로드 이력 조회") @Operation(summary = "shp 파일 다운로드 이력 조회", description = "추론관리 분석결과 shp 파일 다운로드 이력 조회")
@GetMapping(value = "/download-audit/{uuid}") @GetMapping(value = "/download-audit/{uuid}")
@ApiResponses( @ApiResponses(
@@ -419,7 +420,8 @@ public class InferenceResultApiController {
return ApiResponseDto.ok(inferenceResultService.getDownloadAudit(searchReq, downloadReq)); return ApiResponseDto.ok(inferenceResultService.getDownloadAudit(searchReq, downloadReq));
} }
@Operation(summary = "추론 실행중인 도엽 목록", description = "추론관리 실행중인 도엽명 5k 목록") /** 추론진행 현황 화면에서 호출, 분석도엽 부분 옵션일때 분석중인 도엽 확인용 */
@Operation(summary = "추론관리 분석중인 도엽명 5k 목록", description = "추론관리 분석중인 도엽명 50k 목록")
@ApiResponses({ @ApiResponses({
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",

View File

@@ -1,7 +1,5 @@
package com.kamco.cd.kamcoback.inference; package com.kamco.cd.kamcoback.inference;
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.Scene;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto; import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto;
import com.kamco.cd.kamcoback.inference.service.InferenceResultShpService; import com.kamco.cd.kamcoback.inference.service.InferenceResultShpService;
@@ -12,15 +10,11 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -59,32 +53,8 @@ public class InferenceResultShpApiController {
@PostMapping("/shp/{uuid}") @PostMapping("/shp/{uuid}")
public ApiResponseDto<Void> createShp( public ApiResponseDto<Void> createShp(
@Parameter(example = "feb2ec0b-a0f7-49ca-95e4-98b2231bdaae") @PathVariable UUID uuid) { @Parameter(example = "feb2ec0b-a0f7-49ca-95e4-98b2231bdaae") @PathVariable UUID uuid) {
// shp 파일 수동생성
inferenceResultShpService.createShp(uuid); inferenceResultShpService.createShp(uuid);
return ApiResponseDto.createOK(null); return ApiResponseDto.createOK(null);
} }
@Operation(summary = "추론실행에 필요한 geojson 파일 생성", description = "추론실행에 필요한 geojson 파일 생성")
@PostMapping("/geojson/{yyyy}/{mapSheetScope}/{detectOption}")
public ApiResponseDto<Scene> createGeojson(
@Schema(description = "년도") @PathVariable String yyyy,
@Schema(description = "전체(ALL),부분(PART)", example = "PART") @PathVariable
String mapSheetScope,
@Schema(description = "추론제외(EXCL),이전 년도 도엽 사용(PREV)", example = "EXCL") @PathVariable
String detectOption,
@Schema(description = "5k도엽번호", example = MAP_ID) @RequestBody Map<String, Object> body) {
Object raw = body.get("mapIds");
if (raw == null) {
throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST);
}
@SuppressWarnings("unchecked")
List<String> mapIds = (List<String>) raw;
Scene scene =
inferenceResultShpService.createGeojson(yyyy, mapSheetScope, detectOption, mapIds);
return ApiResponseDto.createOK(scene);
}
} }

View File

@@ -1,57 +0,0 @@
package com.kamco.cd.kamcoback.inference;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.inference.service.InferenceRunService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "추론 실행", description = "추론 실행")
@Log4j2
@RequestMapping("/api/inference/run")
@RequiredArgsConstructor
@RestController
public class InferenceRunController {
private final InferenceRunService inferenceRunService;
@Operation(summary = "추론 진행 여부 확인", description = "어드민 홈 > 추론관리 > 추론관리 > 추론관리 목록")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "검색 성공",
content =
@Content(
mediaType = "application/json",
schema =
@Schema(
description = "진행 여부 (UUID 있으면 진행중)",
type = "UUID",
example = "44709877-2e27-4fc5-bacb-8e0328c69b64"))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping
public ApiResponseDto<Void> getProcessing(
@Parameter(description = "비교년도", example = "2021") @RequestParam(required = false)
Integer compareYear,
@Parameter(description = "기준년도", example = "2022") @RequestParam(required = false)
Integer targetYear,
@Parameter(description = "모델 uuid") @RequestParam(required = false) UUID modelUuid) {
inferenceRunService.run(compareYear, targetYear, modelUuid);
return ApiResponseDto.ok(null);
}
}

View File

@@ -246,15 +246,15 @@ public class InferenceResultDto {
@NotBlank @NotBlank
private String title; private String title;
@Schema(description = "G1", example = "b40e0f68-c1d8-49fc-93f9-a36270093861") @Schema(description = "G1", example = "643adead-f3d2-4f10-9037-862bee919399")
@NotNull @NotNull
private UUID model1Uuid; private UUID model1Uuid;
@Schema(description = "G2", example = "ec92b7d2-b5a3-4915-9bdf-35fb3ca8ad27") @Schema(description = "G2", example = "dd86b4ef-28e3-4e3d-9ee4-f60d9cb54e13")
@NotNull @NotNull
private UUID model2Uuid; private UUID model2Uuid;
@Schema(description = "G3", example = "37f45782-8ccf-4cf6-911c-a055a1510d39") @Schema(description = "G3", example = "58c1153e-dec6-4424-82a1-189083a9d9dc")
@NotNull @NotNull
private UUID model3Uuid; private UUID model3Uuid;
@@ -676,4 +676,13 @@ public class InferenceResultDto {
private Long m2ModelBatchId; private Long m2ModelBatchId;
private Long m3ModelBatchId; private Long m3ModelBatchId;
} }
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class MapSheetFallbackYearDto {
private String mapSheetNum;
private Integer mngYyyy;
}
} }

View File

@@ -1,10 +0,0 @@
package com.kamco.cd.kamcoback.inference.service;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/inference/manual")
public class InferenceManualApiController {}

View File

@@ -1,22 +1,61 @@
package com.kamco.cd.kamcoback.inference.service; package com.kamco.cd.kamcoback.inference.service;
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultsTestingDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultsTestingDto;
import com.kamco.cd.kamcoback.postgres.core.InferenceResultCoreService; import com.kamco.cd.kamcoback.postgres.core.InferenceResultCoreService;
import com.kamco.cd.kamcoback.postgres.core.ModelMngCoreService;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class InferenceManualService { public class InferenceManualService {
private final InferenceResultCoreService inferenceResultCoreService; private final InferenceResultCoreService inferenceResultCoreService;
private final ModelMngCoreService modelMngCoreService;
public void getResultsTesting(List<Long> batchIds) { public void saveResultsTesting(List<Long> batchIds) {
List<InferenceResultsTestingDto.Basic> resultList = // 배치 id로 추론 결과 testing 테이블에서 조회
inferenceResultCoreService.getInferenceResults(batchIds); List<InferenceResultsTestingDto.Basic> resultInfoList =
inferenceResultCoreService.getInferenceResultGroupList(batchIds);
if (resultList.isEmpty()) {} if (resultInfoList.isEmpty()) {
throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND);
}
// Long compareYear = resultInfoList.getFirst().getBeforeYear();
// Long targetYear = resultInfoList.getFirst().getAfterYear();
// String title = compareYear + "-" + targetYear + "변화탐지";
//
// InferenceResultDto.RegReq inferenceDto = new InferenceResultDto.RegReq();
// inferenceDto.setTitle(title);
// inferenceDto.setCompareYyyy(Integer.valueOf(compareYear));
// inferenceDto.setTargetYyyy(targetYear);
// // 추론 기본정보 저장
// for (InferenceResultsTestingDto.Basic result : resultInfoList) {
//
// if (result.getModelVersion().startsWith(ModelType.G1.getId()) ||
// result.getModelVersion().startsWith("M1")) {
// ModelMngDto.Basic model =
// modelMngCoreService.findByModelVer(result.getModelVersion());
// inferenceDto.setModel1Uuid(model.getUuid());
//
// } else if (result.getModelVersion().startsWith("G2") ||
// result.getModelVersion().startsWith("M2")) {
// ModelMngDto.Basic model =
// modelMngCoreService.findByModelVer(result.getModelVersion());
// inferenceDto.setModel2Uuid(model.getUuid());
//
// } else if (result.getModelVersion().startsWith("G3") ||
// result.getModelVersion().startsWith("M3")) {
// ModelMngDto.Basic model =
// modelMngCoreService.findByModelVer(result.getModelVersion());
// inferenceDto.setModel3Uuid(model.getUuid());
// }
//
// System.out.println(result);
// }
for (InferenceResultsTestingDto.Basic result : resultList) {}
} }
} }

View File

@@ -1,11 +1,11 @@
package com.kamco.cd.kamcoback.inference.service; package com.kamco.cd.kamcoback.inference.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.kamcoback.common.exception.CustomApiException; import com.kamco.cd.kamcoback.common.exception.CustomApiException;
import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter;
import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.ImageFeature; import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.ImageFeature;
import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.Scene; import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.Scene;
import com.kamco.cd.kamcoback.common.inference.service.InferenceCommonService;
import com.kamco.cd.kamcoback.common.inference.utils.GeoJsonValidator;
import com.kamco.cd.kamcoback.common.utils.UserUtil; import com.kamco.cd.kamcoback.common.utils.UserUtil;
import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient; import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient;
import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient.ExternalCallResult; import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient.ExternalCallResult;
@@ -21,7 +21,7 @@ import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.DetectOption;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.InferenceLearnDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.InferenceLearnDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.InferenceServerStatusDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.InferenceServerStatusDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.InferenceStatusDetailDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.InferenceStatusDetailDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetNumDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetScope;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.ResultList; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.ResultList;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.SaveInferenceAiDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.SaveInferenceAiDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Status; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Status;
@@ -39,6 +39,8 @@ import com.kamco.cd.kamcoback.postgres.core.InferenceResultCoreService;
import com.kamco.cd.kamcoback.postgres.core.MapSheetMngCoreService; import com.kamco.cd.kamcoback.postgres.core.MapSheetMngCoreService;
import com.kamco.cd.kamcoback.postgres.core.ModelMngCoreService; import com.kamco.cd.kamcoback.postgres.core.ModelMngCoreService;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@@ -64,6 +66,7 @@ import org.springframework.http.MediaType;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
/** 추론 관리 */
@Service @Service
@Log4j2 @Log4j2
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -74,14 +77,11 @@ public class InferenceResultService {
private final MapSheetMngCoreService mapSheetMngCoreService; private final MapSheetMngCoreService mapSheetMngCoreService;
private final ModelMngCoreService modelMngCoreService; private final ModelMngCoreService modelMngCoreService;
private final AuditLogCoreService auditLogCoreService; private final AuditLogCoreService auditLogCoreService;
private final InferenceCommonService inferenceCommonService;
private final ExternalHttpClient externalHttpClient; private final ExternalHttpClient externalHttpClient;
private final ObjectMapper objectMapper;
private final UserUtil userUtil; private final UserUtil userUtil;
@Value("${inference.url}")
private String inferenceUrl;
@Value("${inference.batch-url}") @Value("${inference.batch-url}")
private String batchUrl; private String batchUrl;
@@ -92,7 +92,10 @@ public class InferenceResultService {
private String datasetDir; private String datasetDir;
@Value("${spring.profiles.active}") @Value("${spring.profiles.active}")
private String profile; private String activeEnv;
@Value("${inference.geojson-dir}")
private String inferenceDir;
/** /**
* 추론관리 목록 * 추론관리 목록
@@ -105,7 +108,7 @@ public class InferenceResultService {
} }
/** /**
* 추론 진행중인지 확인 * 추론 진행중인지 확인, 변화탐지 설정 등록 버튼 활성화 여부에 필요함
* *
* @return * @return
*/ */
@@ -117,40 +120,93 @@ public class InferenceResultService {
return dto.getUuid(); return dto.getUuid();
} }
/**
* 추론 실행 - 추론제외, 이전연도 도엽 사용 분기
*
* @param req
* @return
*/
@Transactional @Transactional
public UUID run(InferenceResultDto.RegReq req) { public UUID run(InferenceResultDto.RegReq req) {
List<MngListDto> targetDtoList = mapSheetMngCoreService.getHstMapSheetList(req); if (req.getDetectOption().equals(DetectOption.EXCL.getId())) {
List<String> compareList = mapSheetMngCoreService.getMapSheetMngHst(req.getCompareYyyy()); // 추론 제외 일때 EXCL
List<String> targetList = return runExcl(req);
mapSheetMngCoreService.getHstMapSheetList(req).stream() }
// 이전연도 도엽 사용 일때 PREV
return runPrev(req);
}
/**
* 변화탐지 옵션 추론제외 실행
*
* @param req
* @return
*/
public UUID runExcl(InferenceResultDto.RegReq req) {
// TODO 쿼리로 한번에 할수 있게 수정해야하나..
// 기준연도 실행가능 도엽 조회
List<MngListDto> targetMngList =
mapSheetMngCoreService.getMapSheetMngHst(
req.getTargetYyyy(), req.getMapSheetScope(), req.getMapSheetNum());
// List<MngListDto> mngList =
// mapSheetMngCoreService.findExecutableSheets(
// req.getCompareYyyy(),
// req.getTargetYyyy(),
// req.getMapSheetScope(),
// req.getMapSheetNum());
if (targetMngList == null || targetMngList.isEmpty()) {
throw new CustomApiException("NOT_FOUND_MAP_SHEET_NUM", HttpStatus.NOT_FOUND);
}
// 비교연도 실행가능 도엽 조회
List<MngListDto> compareMngList =
mapSheetMngCoreService.getMapSheetMngHst(
req.getCompareYyyy(), req.getMapSheetScope(), req.getMapSheetNum());
if (compareMngList == null || compareMngList.isEmpty()) {
throw new CustomApiException("NOT_FOUND_COMPARE_YEAR", HttpStatus.NOT_FOUND);
}
// compare 도엽번호 Set 구성
Set<String> compareSet =
compareMngList.stream()
.map(MngListDto::getMapSheetNum)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 기준년도 비교년도 동일한 도엽번호만 담기
List<MngListDto> intersectionList =
targetMngList.stream()
.filter(dto -> dto.getMapSheetNum() != null)
.filter(dto -> compareSet.contains(dto.getMapSheetNum()))
.toList();
log.info("target size = {}", targetMngList.size());
log.info("compare size = {}", compareMngList.size());
log.info("intersection size = {}", intersectionList.size());
// 비교 연도 도엽번호를 꺼내와서 최종 추론 대상 도엽번호를 담기
List<String> mapSheetNums =
intersectionList.stream()
.map(MngListDto::getMapSheetNum) .map(MngListDto::getMapSheetNum)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.distinct() .distinct()
.toList(); .toList();
log.info( int targetTotal = targetMngList.size();
"hst list count compareList = {}, targetList = {}", compareList.size(), targetList.size()); int compareTotal = compareMngList.size();
int intersection = intersectionList.size();
Set<String> compareSet = new HashSet<>(compareList); // ===== MapSheet Year Comparison =====
Set<String> targetSet = new HashSet<>(targetList); // target Total : 기준연도 실행가능 전체 도엽 수
// compare Total : 비교연도 실행가능 전체 도엽 수
long intersectionCount = // Intersection : 양 연도에 모두 존재하는 도엽 수 (최종 추론 대상)
targetSet.stream() // target Only (Excluded) : 기준연도에만 존재하고 비교연도에는 없는 도엽 수
.distinct() // compare Only : 비교연도에만 존재하고 기준연도에는 없는 도엽 수
.filter(compareSet::contains) // ====================================
.count(); // compare와 target에 공통으로 존재하는 도협 수
long excludedTargetCount =
targetSet.stream()
.distinct()
.filter(s -> !compareSet.contains(s))
.count(); // target 에만 존재하는 도협 수 (compare 에는 없음)
long onlyCompareCount =
compareSet.stream()
.distinct()
.filter(s -> !targetSet.contains(s))
.count(); // compare 에만 존재하는 도협 수 (target 에는 없음)
log.info( log.info(
""" """
@@ -158,69 +214,242 @@ public class InferenceResultService {
target Total: {} target Total: {}
compare Total: {} compare Total: {}
Intersection: {} Intersection: {}
target Only (Excluded from compare): {} target Only (Excluded): {}
compare Only: {} compare Only: {}
==================================== ====================================
""", """,
targetSet.size(), targetTotal,
compareSet.size(), compareTotal,
intersectionCount, intersection,
excludedTargetCount, targetTotal - intersection,
onlyCompareCount); compareTotal - intersection);
List<String> filteredTargetList = if (mapSheetNums.isEmpty()) {
targetSet.stream() // target 기준으로 // 추론 가능한 도엽이 없습니다.
.filter(compareSet::contains) // compare에 있는 도협만 남김 throw new CustomApiException("NOT_FOUND_MAP_SHEET_NUM", HttpStatus.NOT_FOUND);
.toList();
Scene modelComparePath =
getSceneInference(req.getCompareYyyy().toString(), filteredTargetList, "", "");
Scene modelTargetPath =
getSceneInference(req.getTargetYyyy().toString(), filteredTargetList, "", "");
// 작은 쪽 기준으로 탐지건수/파일생성 리스트 결정
List<ImageFeature> imageFeatureList;
if (modelComparePath.getFeatures().size() <= modelTargetPath.getFeatures().size()) {
imageFeatureList = modelComparePath.getFeatures();
} else {
imageFeatureList = modelTargetPath.getFeatures();
} }
// imageFeatureList 기준 sceneId Set // compare geojson 파일 생성
Set<String> sceneIdSet = Scene compareScene =
imageFeatureList.stream() getSceneInference(
.map(ImageFeature::getSceneId) req.getCompareYyyy().toString(), // 기준년도
mapSheetNums, // 최종 추론 대상
req.getMapSheetScope(), // ALL / 부분
req.getDetectOption()); // EXCL / PREV
// target geojson 파일 생성
Scene targetScene =
getSceneInference(
req.getTargetYyyy().toString(), // 대상년도
mapSheetNums, // 최종 추론 대상
req.getMapSheetScope(),
req.getDetectOption());
log.info("비교년도 geojson 파일 validation ===== {}", compareScene.getFilePath());
GeoJsonValidator.validateWithRequested(compareScene.getFilePath(), mapSheetNums);
log.info("기준년도 geojson 파일 validation ===== {}", targetScene.getFilePath());
GeoJsonValidator.validateWithRequested(targetScene.getFilePath(), mapSheetNums);
// 추론 실행
return executeInference(
req,
intersectionList, // 전체 target 목록
mapSheetNums, // 최종 추론 대상
compareScene, // compare geojson
targetScene // target geojson
);
}
/**
* 변화탐지 옵션 이전 년도 도엽 사용 실행
*
* @param req
* @return
*/
@Transactional
public UUID runPrev(InferenceResultDto.RegReq req) {
// TODO 쿼리로 한번에 할수 있게 수정해야하나..
// 기준연도 실행가능 도엽 조회
List<MngListDto> targetMngList =
mapSheetMngCoreService.getMapSheetMngHst(
req.getTargetYyyy(), req.getMapSheetScope(), req.getMapSheetNum());
if (targetMngList == null || targetMngList.isEmpty()) {
throw new CustomApiException("NOT_FOUND_TARGET_YEAR", HttpStatus.NOT_FOUND);
}
// 비교연도 실행가능 도엽 조회
List<MngListDto> compareMngList =
mapSheetMngCoreService.getMapSheetMngHst(
req.getCompareYyyy(), req.getMapSheetScope(), req.getMapSheetNum());
if (compareMngList == null || compareMngList.isEmpty()) {
throw new CustomApiException("NOT_FOUND_COMPARE_YEAR", HttpStatus.NOT_FOUND);
}
log.info("targetMngList size = {}", targetMngList.size());
log.info("compareMngList size = {}", compareMngList.size());
log.info("Difference in count = {}", targetMngList.size() - compareMngList.size());
// 로그용 원본 카운트 (이전도엽 추가 전)
int targetTotal = targetMngList.size();
int compareTotalBeforeFallback = compareMngList.size();
// 기준연도 기준 비교연도 구해서 이전년도로 compare 보완 하기위해서 도엽번호만 정리
Set<String> compareSet0 =
compareMngList.stream()
.map(MngListDto::getMapSheetNum)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
// targetList(List<MngListDto>) 리턴용으로 필터링 // 기준연도 기준 비교연도에 도협번호가 없으면 이전연도 조회해서 compare 보완, 없는거 담기
List<String> targetOnlyMapSheetNums =
targetMngList.stream()
.map(MngListDto::getMapSheetNum)
.filter(Objects::nonNull)
.filter(num -> !compareSet0.contains(num))
.toList();
log.info("targetOnlyMapSheetNums in count = {}", targetOnlyMapSheetNums.size());
// 이전연도 초회 추가
compareMngList.addAll(
mapSheetMngCoreService.findFallbackCompareYearByMapSheets(
req.getCompareYyyy(), targetOnlyMapSheetNums));
log.info("fallback compare size= {}", compareMngList.size());
// 이전연도 추가 후 compare 총 개수
int compareTotalAfterFallback = compareMngList.size();
// 이전연도 추가한 기준연도 값 도협번호만 담기
Set<String> compareSet1 =
compareMngList.stream()
.map(MngListDto::getMapSheetNum)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 기준연도 기준으로 비교연도에 있는것만 담기 (도협번호) 결국 비교년도와 개수가 같아짐
List<String> mapSheetNums =
targetMngList.stream()
.map(MngListDto::getMapSheetNum)
.filter(Objects::nonNull)
.filter(compareSet1::contains)
.toList();
int intersection = mapSheetNums.size();
Set<String> intersectionSet = new HashSet<>(mapSheetNums);
// 비교연도 같은거 담기(dto list)
compareMngList =
compareMngList.stream()
.filter(c -> c.getMapSheetNum() != null)
.filter(c -> intersectionSet.contains(c.getMapSheetNum()))
.toList();
// 기준연도 같은거 담기(dto list)
List<MngListDto> filteredTargetMngList =
targetMngList.stream()
.filter(t -> t.getMapSheetNum() != null)
.filter(t -> intersectionSet.contains(t.getMapSheetNum()))
.toList();
// 로그
int targetOnlyExcluded = targetTotal - intersection;
int compareOnly = compareTotalAfterFallback - intersection;
log.info(
"""
===== MapSheet Year Comparison =====
target Total: {}
compare Total(before fallback): {}
compare Total(after fallback): {}
Intersection: {}
target Only (Excluded): {}
compare Only: {}
====================================
""",
targetTotal,
compareTotalBeforeFallback,
compareTotalAfterFallback,
intersection,
targetOnlyExcluded,
compareOnly);
if (mapSheetNums.isEmpty()) {
throw new CustomApiException("NOT_FOUND_MAP_SHEET_NUM", HttpStatus.NOT_FOUND);
}
// compare 기준 geojson 생성
Scene compareScene =
getSceneInference(
compareMngList,
req.getCompareYyyy().toString(),
req.getMapSheetScope(),
req.getDetectOption());
// target 기준 geojson 생성
Scene targetScene =
getSceneInference(
req.getTargetYyyy().toString(),
mapSheetNums,
req.getMapSheetScope(),
req.getDetectOption());
log.info("비교년도 geojson 파일 validation ===== {}", compareScene.getFilePath());
GeoJsonValidator.validateWithRequested(compareScene.getFilePath(), mapSheetNums);
log.info("기준년도 geojson 파일 validation ===== {}", targetScene.getFilePath());
GeoJsonValidator.validateWithRequested(targetScene.getFilePath(), mapSheetNums);
// 추론 실행
return executeInference(req, filteredTargetMngList, mapSheetNums, compareScene, targetScene);
}
/**
* learn 테이블 저장 및 AI 추론 API 호출
*
* @param req
* @param targetDtoList
* @param filteredTargetList
* @param modelComparePath
* @param modelTargetPath
* @return
*/
private UUID executeInference(
InferenceResultDto.RegReq req,
List<MngListDto> targetDtoList,
List<String> filteredTargetList,
Scene modelComparePath,
Scene modelTargetPath) {
Set<String> filteredSet = new HashSet<>(filteredTargetList);
List<MngListDto> newTargetList = List<MngListDto> newTargetList =
targetDtoList.stream() targetDtoList.stream()
.filter(m -> m.getMapSheetNum() != null) .filter(m -> m.getMapSheetNum() != null)
.filter(m -> sceneIdSet.contains(m.getMapSheetNum())) .filter(m -> filteredSet.contains(m.getMapSheetNum()))
.toList(); .toList();
// 목록 및 추론 대상 도엽정보 저장 // 추론 실행 목록 테이블 저장, 도엽목록별 상태 체크 테이블 저장
UUID uuid = inferenceResultCoreService.saveInferenceInfo(req, newTargetList); UUID uuid = inferenceResultCoreService.saveInferenceInfo(req, newTargetList);
// ai 서버에 전달 파라미터 생성 // 추론 AI 전달 파라미터 생성
pred_requests_areas predRequestsAreas = new pred_requests_areas(); pred_requests_areas predRequestsAreas = new pred_requests_areas();
predRequestsAreas.setInput1_year(req.getCompareYyyy()); predRequestsAreas.setInput1_year(req.getCompareYyyy());
predRequestsAreas.setInput2_year(req.getTargetYyyy()); predRequestsAreas.setInput2_year(req.getTargetYyyy());
predRequestsAreas.setInput1_scene_path(modelComparePath.getFilePath()); predRequestsAreas.setInput1_scene_path(modelComparePath.getFilePath());
predRequestsAreas.setInput2_scene_path(modelTargetPath.getFilePath()); predRequestsAreas.setInput2_scene_path(modelTargetPath.getFilePath());
// 모델정보 조회 dto 생성 후 반환
InferenceSendDto m1 = this.getModelInfo(req.getModel1Uuid()); InferenceSendDto m1 = this.getModelInfo(req.getModel1Uuid());
m1.setPred_requests_areas(predRequestsAreas); m1.setPred_requests_areas(predRequestsAreas);
log.info("[INFERENCE] Start m1 = {}", m1); log.info("[INFERENCE] Start m1 = {}", m1);
m1.setPred_requests_areas(predRequestsAreas);
// ai 추론 실행 api 호출 // AI 호출
Long batchId = ensureAccepted(m1); Long batchId = inferenceCommonService.ensureAccepted(m1);
// ai 추론 실행후 응답값 update
SaveInferenceAiDto saveInferenceAiDto = new SaveInferenceAiDto(); SaveInferenceAiDto saveInferenceAiDto = new SaveInferenceAiDto();
saveInferenceAiDto.setUuid(uuid); saveInferenceAiDto.setUuid(uuid);
saveInferenceAiDto.setBatchId(batchId); saveInferenceAiDto.setBatchId(batchId);
@@ -230,13 +459,15 @@ public class InferenceResultService {
saveInferenceAiDto.setModelComparePath(modelComparePath.getFilePath()); saveInferenceAiDto.setModelComparePath(modelComparePath.getFilePath());
saveInferenceAiDto.setModelTargetPath(modelTargetPath.getFilePath()); saveInferenceAiDto.setModelTargetPath(modelTargetPath.getFilePath());
saveInferenceAiDto.setModelStartDttm(ZonedDateTime.now()); saveInferenceAiDto.setModelStartDttm(ZonedDateTime.now());
// AI 호출 하고 리턴 받은 정보 추론 실행 목록 테이블에 업데이트
inferenceResultCoreService.update(saveInferenceAiDto); inferenceResultCoreService.update(saveInferenceAiDto);
return uuid; return uuid;
} }
/** /**
* 변화탐지 실행 정보 생성 * 변화탐지 실행 정보 생성 TODO 미사용, 새로운 추론실행 로직 테스트후 삭제 해야합니다.
* *
* @param req * @param req
*/ */
@@ -244,7 +475,7 @@ public class InferenceResultService {
public UUID saveInferenceInfo(InferenceResultDto.RegReq req) { public UUID saveInferenceInfo(InferenceResultDto.RegReq req) {
// 변화탐지 실행 가능 기준 년도 조회 // 변화탐지 실행 가능 기준 년도 조회
List<MngListDto> targetList = mapSheetMngCoreService.getHstMapSheetList(req); List<MngListDto> targetList = null; // mapSheetMngCoreService.getHstMapSheetList(req);
if (targetList.isEmpty()) { if (targetList.isEmpty()) {
throw new CustomApiException("NOT_FOUND_TARGET_YEAR", HttpStatus.NOT_FOUND); throw new CustomApiException("NOT_FOUND_TARGET_YEAR", HttpStatus.NOT_FOUND);
@@ -265,31 +496,31 @@ public class InferenceResultService {
List<TotalListDto> totalNumList = new ArrayList<>(); List<TotalListDto> totalNumList = new ArrayList<>();
// if (DetectOption.EXCL.getId().equals(req.getDetectOption())) { if (DetectOption.EXCL.getId().equals(req.getDetectOption())) {
// // "추론제외" 일때 전년도 이전 값이 있어도 전년도 도엽이 없으면 비교 안함 // "추론제외" 일때 전년도 이전 값이 있어도 전년도 도엽이 없으면 비교 안함
// for (MngListCompareDto dto : compareList) { for (MngListCompareDto dto : compareList) {
// if (Objects.equals(dto.getBeforeYear(), req.getCompareYyyy())) { if (Objects.equals(dto.getBeforeYear(), req.getCompareYyyy())) {
// TotalListDto totalDto = new TotalListDto(); TotalListDto totalDto = new TotalListDto();
// totalDto.setBeforeYear(dto.getBeforeYear() == null ? 0 : dto.getBeforeYear()); totalDto.setBeforeYear(dto.getBeforeYear() == null ? 0 : dto.getBeforeYear());
// totalDto.setMapSheetNum(dto.getMapSheetNum()); totalDto.setMapSheetNum(dto.getMapSheetNum());
// totalNumList.add(totalDto); totalNumList.add(totalDto);
// } }
// } }
// } else if (DetectOption.PREV.getId().equals(req.getDetectOption())) { } else if (DetectOption.PREV.getId().equals(req.getDetectOption())) {
// // "이전 년도 도엽 사용" 이면 전년도 이전 도엽도 사용 // "이전 년도 도엽 사용" 이면 전년도 이전 도엽도 사용
// for (MngListCompareDto dto : compareList) { for (MngListCompareDto dto : compareList) {
// if (dto.getBeforeYear() != 0) { if (dto.getBeforeYear() != 0) {
// TotalListDto totalDto = new TotalListDto(); TotalListDto totalDto = new TotalListDto();
// totalDto.setBeforeYear(dto.getBeforeYear() == null ? 0 : dto.getBeforeYear()); totalDto.setBeforeYear(dto.getBeforeYear() == null ? 0 : dto.getBeforeYear());
// totalDto.setMapSheetNum(dto.getMapSheetNum()); totalDto.setMapSheetNum(dto.getMapSheetNum());
// totalNumList.add(totalDto); totalNumList.add(totalDto);
// } }
// } }
// } }
//
// if (totalNumList.isEmpty()) { if (totalNumList.isEmpty()) {
// throw new CustomApiException("NOT_FOUND_COMPARE_YEAR", HttpStatus.NOT_FOUND); throw new CustomApiException("NOT_FOUND_COMPARE_YEAR", HttpStatus.NOT_FOUND);
// } }
// 사용할 영상파일 년도 기록 및 추론에 포함되는지 설정 // 사용할 영상파일 년도 기록 및 추론에 포함되는지 설정
for (MngListDto target : targetList) { for (MngListDto target : targetList) {
@@ -362,7 +593,7 @@ public class InferenceResultService {
m1.setPred_requests_areas(predRequestsAreas); m1.setPred_requests_areas(predRequestsAreas);
// ai 추론 실행 api 호출 // ai 추론 실행 api 호출
Long batchId = ensureAccepted(m1); Long batchId = inferenceCommonService.ensureAccepted(m1);
// ai 추론 실행후 응답값 update // ai 추론 실행후 응답값 update
SaveInferenceAiDto saveInferenceAiDto = new SaveInferenceAiDto(); SaveInferenceAiDto saveInferenceAiDto = new SaveInferenceAiDto();
@@ -379,142 +610,6 @@ public class InferenceResultService {
return uuid; return uuid;
} }
// 비교년도 탐지 제이터 옵션 별로 조회하여 req에 적용
private List<MapSheetNumDto> createdMngDto(
InferenceResultDto.RegReq req, List<MngListDto> targetList) {
List<String> mapTargetIds = new ArrayList<>();
targetList.forEach(
hstMapSheet -> {
// 비교년도는 target 년도 기준으로 가져옴 파라미터 만들기
mapTargetIds.add(hstMapSheet.getMapSheetNum());
});
// 비교년도 조회
List<String> mapCompareIds = new ArrayList<>();
List<MngListCompareDto> compareList =
mapSheetMngCoreService.getByHstMapSheetCompareList(req.getCompareYyyy(), mapTargetIds);
for (MngListCompareDto dto : compareList) {
// 추론 제외일때 이전년도 파일이 없으면 제외
if (req.getDetectOption().equals(DetectOption.EXCL.getId())) {
int targetYear = req.getTargetYyyy() - 1;
if (dto.getBeforeYear() != targetYear) {
continue;
}
}
// 비교년도는 target 년도 기준으로 가져옴
mapCompareIds.add(dto.getMapSheetNum());
}
Set<String> compareSet =
mapCompareIds.stream()
.filter(Objects::nonNull)
.map(String::trim) // 공백/개행 방지
.collect(Collectors.toSet());
// target 기준 compare 비교하여 서로 있는것만 저장
List<String> commonIds =
mapTargetIds.stream()
.filter(Objects::nonNull)
.map(String::trim)
.filter(compareSet::contains)
.toList();
Set<String> commonIdSet =
commonIds.stream().filter(Objects::nonNull).map(String::trim).collect(Collectors.toSet());
// 저장하기위해 파라미터 다시 구성
List<MapSheetNumDto> mapSheetNum =
targetList.stream()
.filter(dto -> dto.getMapSheetNum() != null)
.filter(dto -> commonIdSet.contains(dto.getMapSheetNum().trim()))
.map(
dto -> {
MapSheetNumDto mapSheetNumDto = new MapSheetNumDto();
mapSheetNumDto.setMapSheetNum(dto.getMapSheetNum());
mapSheetNumDto.setMapSheetName(dto.getMapSheetName());
return mapSheetNumDto;
})
.toList();
return mapSheetNum;
}
/**
* 추론 AI API 호출
*
* @param dto
*/
// 같은함수가 왜 두개지
private Long ensureAccepted(InferenceSendDto dto) {
if (dto == null) {
log.warn("not InferenceSendDto dto");
throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST);
}
// [중복]운영환경일때 경로수정 dean 260226
if (profile != null && profile.equals("prod")) {
log.info("========================================================");
log.info("[CHANGE INFERENCE] profile = {} Inforence req", profile);
log.info("========================================================");
log.info("");
}
// 1) 요청 로그
try {
log.debug("Inference request dto={}", objectMapper.writeValueAsString(dto));
} catch (JsonProcessingException e) {
log.warn("Failed to serialize inference dto", e);
}
// 2) local 환경 임시 처리
// if ("local".equals(profile)) {
// if (dto.getPred_requests_areas() == null) {
// throw new IllegalStateException("pred_requests_areas is null");
// }
//
// dto.getPred_requests_areas().setInput1_scene_path("/kamco-nfs/requests/2023_local.geojson");
//
// dto.getPred_requests_areas().setInput2_scene_path("/kamco-nfs/requests/2024_local.geojson");
// }
// 3) HTTP 호출
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
ExternalCallResult<String> result =
externalHttpClient.call(inferenceUrl, HttpMethod.POST, dto, headers, String.class);
if (result.statusCode() < 200 || result.statusCode() >= 300) {
log.error("Inference API failed. status={}, body={}", result.statusCode(), result.body());
throw new CustomApiException("BAD_GATEWAY", HttpStatus.BAD_GATEWAY);
}
// 4) 응답 파싱
try {
List<Map<String, Object>> list =
objectMapper.readValue(result.body(), new TypeReference<>() {});
if (list.isEmpty()) {
throw new IllegalStateException("Inference response is empty");
}
Object batchIdObj = list.get(0).get("batch_id");
if (batchIdObj == null) {
throw new IllegalStateException("batch_id not found in response");
}
return Long.valueOf(batchIdObj.toString());
} catch (Exception e) {
log.error("Failed to parse inference response. body={}", result.body(), e);
throw new CustomApiException("INVALID_INFERENCE_RESPONSE", HttpStatus.BAD_GATEWAY);
}
}
/** /**
* 모델정보 조회 dto 생성 후 반환 * 모델정보 조회 dto 생성 후 반환
* *
@@ -523,6 +618,7 @@ public class InferenceResultService {
*/ */
private InferenceSendDto getModelInfo(UUID uuid) { private InferenceSendDto getModelInfo(UUID uuid) {
// 모델정보 조회
Basic modelInfo = modelMngCoreService.findByModelUuid(uuid); Basic modelInfo = modelMngCoreService.findByModelUuid(uuid);
String cdModelPath = ""; String cdModelPath = "";
@@ -576,8 +672,36 @@ public class InferenceResultService {
*/ */
private Scene getSceneInference( private Scene getSceneInference(
String yyyy, List<String> mapSheetNums, String mapSheetScope, String detectOption) { String yyyy, List<String> mapSheetNums, String mapSheetScope, String detectOption) {
return mapSheetMngCoreService.getSceneInference(
yyyy, mapSheetNums, mapSheetScope, detectOption); // geojson 생성시 필요한 영상파일 정보 조회
List<ImageFeature> features =
mapSheetMngCoreService.loadSceneInferenceBySheets(yyyy, mapSheetNums);
if (features == null || features.isEmpty()) {
log.warn(
"NOT_FOUND_MAP_SHEET_NUM : yyyy={}, scenesSize={}",
yyyy,
mapSheetNums == null ? 0 : mapSheetNums.size());
throw new CustomApiException("NOT_FOUND_MAP_SHEET_NUM", HttpStatus.NOT_FOUND);
}
return writeSceneGeoJson(yyyy, mapSheetScope, detectOption, features);
}
/**
* 년도 별로 조회하여 geojson 파일 생성
*
* @param yearDtos
* @param yyyy
* @param mapSheetScope
* @param detectOption
* @return
*/
private Scene getSceneInference(
List<MngListDto> yearDtos, String yyyy, String mapSheetScope, String detectOption) {
List<ImageFeature> features =
mapSheetMngCoreService.loadSceneInferenceByFallbackYears(yearDtos);
return writeSceneGeoJson(yyyy, mapSheetScope, detectOption, features);
} }
/** /**
@@ -656,11 +780,17 @@ public class InferenceResultService {
return inferenceResultCoreService.listGetScenes5k(id); return inferenceResultCoreService.listGetScenes5k(id);
} }
/**
* 추론 서버 현황 cpu, gpu 확인
*
* @return 서버 정보
*/
public List<InferenceServerStatusDto> getInferenceServerStatusList() { public List<InferenceServerStatusDto> getInferenceServerStatusList() {
String[] serverNames = inferenceServerName.split(","); String[] serverNames = inferenceServerName.split(",");
int serveCnt = serverNames.length; int serveCnt = serverNames.length;
// 서버정보 조회
List<InferenceServerStatusDto> dtoList = List<InferenceServerStatusDto> dtoList =
inferenceResultCoreService.getInferenceServerStatusList(); inferenceResultCoreService.getInferenceServerStatusList();
int size = dtoList.size(); int size = dtoList.size();
@@ -670,6 +800,7 @@ public class InferenceResultService {
System.out.println("size =" + size); System.out.println("size =" + size);
if (size == 0) { if (size == 0) {
// 서버 정보가 없을때
for (int k = 0; k < serveCnt; k++) { for (int k = 0; k < serveCnt; k++) {
InferenceServerStatusDto dto = new InferenceServerStatusDto(); InferenceServerStatusDto dto = new InferenceServerStatusDto();
dto.setServerName(serverNames[k]); dto.setServerName(serverNames[k]);
@@ -737,17 +868,35 @@ public class InferenceResultService {
return inferenceResultCoreService.getInferenceResultInfo(uuid); return inferenceResultCoreService.getInferenceResultInfo(uuid);
} }
/**
* 분류별 탐지건수 조회
*
* @param uuid 추론 uuid
* @return 분류별 탐지건수 정보
*/
public List<Dashboard> getInferenceClassCountList(UUID uuid) { public List<Dashboard> getInferenceClassCountList(UUID uuid) {
return inferenceResultCoreService.getInferenceClassCountList(uuid); return inferenceResultCoreService.getInferenceClassCountList(uuid);
} }
/**
* 추론결과 geom 목록 조회
*
* @param uuid 추론 uuid
* @param searchGeoReq 추론 결과 상세화면 geom 조회 조건
* @return geom 목록 정보
*/
public Page<Geom> getInferenceGeomList(UUID uuid, SearchGeoReq searchGeoReq) { public Page<Geom> getInferenceGeomList(UUID uuid, SearchGeoReq searchGeoReq) {
return inferenceResultCoreService.getInferenceGeomList(uuid, searchGeoReq); return inferenceResultCoreService.getInferenceGeomList(uuid, searchGeoReq);
} }
/** 추론 종료 */ /**
* 추론 종료
*
* @return 호출한 uuid
*/
@Transactional @Transactional
public UUID deleteInferenceEnd() { public UUID deleteInferenceEnd() {
// 추론 진행중인지 확인
SaveInferenceAiDto dto = inferenceResultCoreService.getProcessing(); SaveInferenceAiDto dto = inferenceResultCoreService.getProcessing();
if (dto == null) { if (dto == null) {
throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND); throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND);
@@ -759,13 +908,15 @@ public class InferenceResultService {
headers.setContentType(MediaType.APPLICATION_JSON); headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(List.of(MediaType.APPLICATION_JSON)); headers.setAccept(List.of(MediaType.APPLICATION_JSON));
// 종료 api 호출
ExternalCallResult<String> result = ExternalCallResult<String> result =
externalHttpClient.call(url, HttpMethod.DELETE, dto, headers, String.class); externalHttpClient.callLong(url, HttpMethod.DELETE, dto, headers, String.class);
if (!result.success()) { if (!result.success()) {
throw new CustomApiException("BAD_GATEWAY", HttpStatus.BAD_GATEWAY); throw new CustomApiException("BAD_GATEWAY", HttpStatus.BAD_GATEWAY);
} }
// 추론 정보 테이블 update
SaveInferenceAiDto request = new SaveInferenceAiDto(); SaveInferenceAiDto request = new SaveInferenceAiDto();
request.setStatus(Status.FORCED_END.getId()); request.setStatus(Status.FORCED_END.getId());
request.setUuid(dto.getUuid()); request.setUuid(dto.getUuid());
@@ -780,14 +931,17 @@ public class InferenceResultService {
} }
/** /**
* 추론결과 shp zip 파일 다운로드 경로 생성 * 추론결과 shp zip 파일 확인하여 다운로드 경로 생성
* *
* @param uuid * @param uuid 추론 uuid
* @return * @return 32자 추론 uid, shp 파일 경로
*/ */
public Map<String, Object> shpDownloadPath(UUID uuid) { public Map<String, Object> shpDownloadPath(UUID uuid) {
// 추론정보 조회
InferenceLearnDto dto = inferenceResultCoreService.getInferenceUid(uuid); InferenceLearnDto dto = inferenceResultCoreService.getInferenceUid(uuid);
String uid = dto.getUid(); String uid = dto.getUid();
// 파일 경로 생성
Path path = Path.of(datasetDir).resolve(uid).resolve("merge").resolve(uid + ".zip"); Path path = Path.of(datasetDir).resolve(uid).resolve("merge").resolve(uid + ".zip");
Map<String, Object> downloadMap = new HashMap<>(); Map<String, Object> downloadMap = new HashMap<>();
@@ -809,7 +963,7 @@ public class InferenceResultService {
} }
/** /**
* 실행중인 추론 도엽명 목록 * 분석중인 추론 도엽명 목록
* *
* @param uuid uuid * @param uuid uuid
* @return * @return
@@ -817,4 +971,69 @@ public class InferenceResultService {
public List<String> getInferenceRunMapId(UUID uuid) { public List<String> getInferenceRunMapId(UUID uuid) {
return inferenceResultCoreService.getInferenceRunMapId(uuid); return inferenceResultCoreService.getInferenceRunMapId(uuid);
} }
/**
* 파일 경로/이름 , 파일 생성 , 도엽번호 반환
*
* @param yyyy
* @param mapSheetScope
* @param detectOption
* @param sceneInference
* @return Scene
*/
private Scene writeSceneGeoJson(
String yyyy, String mapSheetScope, String detectOption, List<ImageFeature> sceneInference) {
boolean isAll = MapSheetScope.ALL.getId().equals(mapSheetScope);
String optionSuffix = buildOptionSuffix(detectOption);
String targetDir =
"local".equals(activeEnv) ? System.getProperty("user.home") + "/geojson" : inferenceDir;
// 파일명 생성
String filename =
isAll
? String.format("%s_%s_ALL%s.geojson", yyyy, activeEnv, optionSuffix)
: String.format("%s_%s%s.geojson", yyyy, activeEnv, optionSuffix);
Path outputPath = Paths.get(targetDir, filename);
try {
log.info("create Directories outputPath: {}", outputPath);
log.info(
"activeEnv={}, inferenceDir={}, targetDir={}, filename={}",
activeEnv,
inferenceDir,
targetDir,
filename);
log.info("outputPath={}, parent={}", outputPath.toAbsolutePath(), outputPath.getParent());
Files.createDirectories(outputPath.getParent());
new GeoJsonFileWriter()
.exportToFile(sceneInference, "scene_inference_" + yyyy, 5186, outputPath.toString());
Scene scene = new Scene();
scene.setFeatures(sceneInference);
scene.setFilePath(outputPath.toString());
return scene;
} catch (IOException e) {
log.error(
"FAIL_CREATE_MAP_SHEET_FILE: yyyy={}, isAll={}, path={}", yyyy, isAll, outputPath, e);
throw new CustomApiException("INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, e);
}
}
/**
* geojson 파일명 Suffix
*
* @param detectOption
* @return
*/
private String buildOptionSuffix(String detectOption) {
if (DetectOption.EXCL.getId().equals(detectOption)) return "_EXCL";
if (DetectOption.PREV.getId().equals(detectOption)) return "_PREV";
return "";
}
} }

View File

@@ -1,13 +1,11 @@
package com.kamco.cd.kamcoback.inference.service; package com.kamco.cd.kamcoback.inference.service;
import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.Scene;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.InferenceLearnDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.InferenceLearnDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto;
import com.kamco.cd.kamcoback.postgres.core.InferenceResultCoreService; import com.kamco.cd.kamcoback.postgres.core.InferenceResultCoreService;
import com.kamco.cd.kamcoback.postgres.core.InferenceResultShpCoreService; import com.kamco.cd.kamcoback.postgres.core.InferenceResultShpCoreService;
import com.kamco.cd.kamcoback.postgres.core.MapSheetMngCoreService; import com.kamco.cd.kamcoback.postgres.core.MapSheetMngCoreService;
import com.kamco.cd.kamcoback.scheduler.service.ShpPipelineService; import com.kamco.cd.kamcoback.scheduler.service.ShpPipelineService;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -66,21 +64,4 @@ public class InferenceResultShpService {
// shp 파일 비동기 생성 // shp 파일 비동기 생성
shpPipelineService.runPipeline(jarPath, datasetDir, batchId, dto.getUid()); shpPipelineService.runPipeline(jarPath, datasetDir, batchId, dto.getUid());
} }
/**
* 추론 실행전 geojson 파일 생성
*
* @param yyyy
* @param mapSheetScope
* @param detectOption
* @param mapIds
* @return
*/
public Scene createGeojson(
String yyyy, String mapSheetScope, String detectOption, List<String> mapIds) {
Scene getSceneInference =
mapSheetMngCoreService.getSceneInference(yyyy, mapIds, mapSheetScope, detectOption);
log.info("getSceneInference: {}", getSceneInference);
return getSceneInference;
}
} }

View File

@@ -1,224 +0,0 @@
package com.kamco.cd.kamcoback.inference.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.Scene;
import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient;
import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient.ExternalCallResult;
import com.kamco.cd.kamcoback.inference.dto.InferenceSendDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceSendDto.pred_requests_areas;
import com.kamco.cd.kamcoback.model.dto.ModelMngDto.Basic;
import com.kamco.cd.kamcoback.model.dto.ModelMngDto.ModelType;
import com.kamco.cd.kamcoback.postgres.core.MapSheetMngCoreService;
import com.kamco.cd.kamcoback.postgres.core.ModelMngCoreService;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
@Service
@Log4j2
@RequiredArgsConstructor
public class InferenceRunService {
private final ExternalHttpClient externalHttpClient;
private final MapSheetMngCoreService mapSheetMngCoreService;
private final ModelMngCoreService modelMngCoreService;
private final ObjectMapper objectMapper;
@Value("${spring.profiles.active}")
private String profile;
@Value("${inference.url}")
private String inferenceUrl;
// TODO 이거 쓰는건가?
public void run(Integer compareYear, Integer targetYear, UUID modelUuid) {
List<String> compareList = mapSheetMngCoreService.getMapSheetMngHst(compareYear);
List<String> targetList = mapSheetMngCoreService.getMapSheetMngHst(targetYear);
log.info(
"hst list count compareList = {}, targetList = {}", compareList.size(), targetList.size());
Set<String> compareSet = new HashSet<>(compareList);
Set<String> targetSet = new HashSet<>(targetList);
long intersectionCount =
targetSet.stream()
.distinct()
.filter(compareSet::contains)
.count(); // compare와 target에 공통으로 존재하는 도협 수
long excludedTargetCount =
targetSet.stream()
.distinct()
.filter(s -> !compareSet.contains(s))
.count(); // target 에만 존재하는 도협 수 (compare 에는 없음)
long onlyCompareCount =
compareSet.stream()
.distinct()
.filter(s -> !targetSet.contains(s))
.count(); // compare 에만 존재하는 도협 수 (target 에는 없음)
log.info(
"""
===== MapSheet Year Comparison =====
target Total: {}
compare Total: {}
Intersection: {}
target Only (Excluded from compare): {}
compare Only: {}
====================================
""",
targetSet.size(),
compareSet.size(),
intersectionCount,
excludedTargetCount,
onlyCompareCount);
List<String> filteredTargetList =
targetSet.stream() // target 기준으로
.filter(compareSet::contains) // compare에 있는 도협만 남김
.toList();
Scene modelComparePath = getSceneInference(compareYear.toString(), filteredTargetList, "", "");
Scene modelTargetPath = getSceneInference(targetYear.toString(), filteredTargetList, "", "");
// ai 서버에 전달할 파라미터 생성
pred_requests_areas predRequestsAreas = new pred_requests_areas();
predRequestsAreas.setInput1_year(compareYear);
predRequestsAreas.setInput2_year(targetYear);
predRequestsAreas.setInput1_scene_path(modelComparePath.getFilePath());
predRequestsAreas.setInput2_scene_path(modelTargetPath.getFilePath());
InferenceSendDto m1 = this.getModelInfo(modelUuid);
m1.setPred_requests_areas(predRequestsAreas);
// ai 추론 실행 api 호출
Long batchId = ensureAccepted(m1);
}
private Scene getSceneInference(
String yyyy, List<String> mapSheetNums, String mapSheetScope, String detectOption) {
return mapSheetMngCoreService.getSceneInference(
yyyy, mapSheetNums, mapSheetScope, detectOption);
}
/**
* 추론 AI API 호출
*
* @param dto
*/
private Long ensureAccepted(InferenceSendDto dto) {
if (dto == null) {
log.warn("not InferenceSendDto dto");
throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST);
}
// 1) 요청 로그
try {
log.info("Inference request dto={}", objectMapper.writeValueAsString(dto));
} catch (JsonProcessingException e) {
log.warn("Failed to serialize inference dto", e);
}
// 3) HTTP 호출
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
ExternalCallResult<String> result =
externalHttpClient.call(inferenceUrl, HttpMethod.POST, dto, headers, String.class);
if (result.statusCode() < 200 || result.statusCode() >= 300) {
log.error("Inference API failed. status={}, body={}", result.statusCode(), result.body());
throw new CustomApiException("BAD_GATEWAY", HttpStatus.BAD_GATEWAY);
}
// 4) 응답 파싱
try {
List<Map<String, Object>> list =
objectMapper.readValue(result.body(), new TypeReference<>() {});
if (list.isEmpty()) {
throw new IllegalStateException("Inference response is empty");
}
Object batchIdObj = list.get(0).get("batch_id");
if (batchIdObj == null) {
throw new IllegalStateException("batch_id not found in response");
}
return Long.valueOf(batchIdObj.toString());
} catch (Exception e) {
log.error("Failed to parse inference response. body={}", result.body(), e);
throw new CustomApiException("INVALID_INFERENCE_RESPONSE", HttpStatus.BAD_GATEWAY);
}
}
/**
* 모델정보 조회 dto 생성 후 반환
*
* @param uuid
* @return
*/
private InferenceSendDto getModelInfo(UUID uuid) {
Basic modelInfo = modelMngCoreService.findByModelUuid(uuid);
String cdModelPath = "";
String cdModelConfigPath = "";
String cdClsModelPath = "";
if (modelInfo.getCdModelPath() != null && modelInfo.getCdModelFileName() != null) {
cdModelPath =
Paths.get(modelInfo.getCdModelPath(), modelInfo.getCdModelFileName()).toString();
}
if (modelInfo.getCdModelConfig() != null && modelInfo.getCdModelConfigFileName() != null) {
cdModelConfigPath =
Paths.get(modelInfo.getCdModelConfig(), modelInfo.getCdModelConfigFileName()).toString();
}
if (modelInfo.getClsModelPath() != null && modelInfo.getClsModelFileName() != null) {
cdClsModelPath =
Paths.get(modelInfo.getClsModelPath(), modelInfo.getClsModelFileName()).toString();
}
String modelType = "";
if (modelInfo.getModelType().equals(ModelType.G1.getId())) {
modelType = ModelType.G1.getId();
} else if (modelInfo.getModelType().equals(ModelType.G2.getId())) {
modelType = ModelType.G2.getId();
} else {
modelType = ModelType.G3.getId();
}
InferenceSendDto sendDto = new InferenceSendDto();
sendDto.setModel_version(modelInfo.getModelVer());
sendDto.setCd_model_path(cdModelPath);
sendDto.setCd_model_config(cdModelConfigPath);
sendDto.setCls_model_path(cdClsModelPath);
sendDto.setCls_model_version(modelInfo.getModelVer());
sendDto.setCd_model_type(modelType);
sendDto.setPriority(5.0);
return sendDto;
}
}

View File

@@ -1,14 +1,10 @@
package com.kamco.cd.kamcoback.mapsheet; package com.kamco.cd.kamcoback.mapsheet;
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto; import com.kamco.cd.kamcoback.code.dto.CommonCodeDto;
import com.kamco.cd.kamcoback.code.service.CommonCodeService;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto; import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.FilesDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.FoldersDto; import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.FoldersDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.SrchFilesDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.SrchFoldersDto; import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.SrchFoldersDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngYyyyDto;
import com.kamco.cd.kamcoback.mapsheet.service.MapSheetMngService; import com.kamco.cd.kamcoback.mapsheet.service.MapSheetMngService;
import com.kamco.cd.kamcoback.model.dto.ModelMngDto.ModelUploadResDto; import com.kamco.cd.kamcoback.model.dto.ModelMngDto.ModelUploadResDto;
import com.kamco.cd.kamcoback.upload.dto.UploadDto; import com.kamco.cd.kamcoback.upload.dto.UploadDto;
@@ -42,7 +38,6 @@ import org.springframework.web.multipart.MultipartFile;
@RequestMapping({"/api/imagery/dataset"}) @RequestMapping({"/api/imagery/dataset"})
public class MapSheetMngApiController { public class MapSheetMngApiController {
private final CommonCodeService commonCodeService;
private final MapSheetMngService mapSheetMngService; private final MapSheetMngService mapSheetMngService;
@Value("${file.sync-root-dir}") @Value("${file.sync-root-dir}")
@@ -51,7 +46,7 @@ public class MapSheetMngApiController {
@Value("${file.sync-tmp-dir}") @Value("${file.sync-tmp-dir}")
private String syncRootTmpDir; private String syncRootTmpDir;
@Operation(summary = "영상 데이터 관리 목록 조회", description = "영상 데이터 관리 목록 조회") @Operation(summary = "영상데이터관리 > 목록 조회", description = "영상데이터관리 > 목록 조회")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -70,7 +65,7 @@ public class MapSheetMngApiController {
return ApiResponseDto.ok(mapSheetMngService.findMapSheetMngList()); return ApiResponseDto.ok(mapSheetMngService.findMapSheetMngList());
} }
@Operation(summary = "영상 데이터 관리 상세", description = "영상 데이터 관리 상세") @Operation(summary = "영상데이터관리 > 상세 조회", description = "영상데이터관리 > 상세 조회")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -89,7 +84,7 @@ public class MapSheetMngApiController {
return ApiResponseDto.ok(mapSheetMngService.findMapSheetMng(mngYyyy)); return ApiResponseDto.ok(mapSheetMngService.findMapSheetMng(mngYyyy));
} }
@Operation(summary = "영상관리 > 데이터 등록", description = "영상관리 > 데이터 등록") @Operation(summary = "영상데이터관리 > 데이터 등록", description = "영상데이터관리 > 데이터 등록")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -109,26 +104,7 @@ public class MapSheetMngApiController {
return ApiResponseDto.ok(mapSheetMngService.mngDataSave(AddReq)); return ApiResponseDto.ok(mapSheetMngService.mngDataSave(AddReq));
} }
@Operation(summary = "영상관리 > 작업완료", description = "영상관리 > 작업완료") @Operation(summary = "영상데이터관리 > 데이터 등록 > 연도 선택 목록", description = "영상데이터관리 > 데이터 등록 > 연도 선택 목록")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "작업완료 처리 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PutMapping("/mng-complete")
public ApiResponseDto<MapSheetMngDto.DmlReturn> mngComplete(@RequestParam @Valid int mngYyyy) {
return ApiResponseDto.ok(mapSheetMngService.mngComplete(mngYyyy));
}
@Operation(summary = "영상 데이터 관리 년도 목록", description = "영상 데이터 관리 년도 목록")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -147,7 +123,7 @@ public class MapSheetMngApiController {
return ApiResponseDto.ok(mapSheetMngService.findMapSheetMngYyyyList()); return ApiResponseDto.ok(mapSheetMngService.findMapSheetMngYyyyList());
} }
@Operation(summary = "영상 데이터 관리 오류 목록", description = "영상 데이터 관리 오류 목록") @Operation(summary = "영상데이터관리 > 상세 > 오류 처리 내역", description = "영상데이터관리 > 상세 > 오류 처리 내역")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -166,42 +142,9 @@ public class MapSheetMngApiController {
return ApiResponseDto.ok(mapSheetMngService.findMapSheetErrorList(searchReq)); return ApiResponseDto.ok(mapSheetMngService.findMapSheetErrorList(searchReq));
} }
/* @Operation(
@Operation(summary = "오류데이터 팝업 > 업로드 처리", description = "오류데이터 팝업 > 업로드 처리") summary = "영상데이터관리 > 상세 > 오류 처리 내역 > 업로드 (페어 파일 저장)",
@ApiResponses( description = "영상데이터관리 > 상세 > 오류 처리 내역 > 업로드 (페어 파일 저장)")
value = {
@ApiResponse(
responseCode = "201",
description = "업로드 처리 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PutMapping("/upload-process")
public ApiResponseDto<MapSheetMngDto.DmlReturn> uploadProcess(
@RequestBody @Valid List<Long> hstUidList) {
return ApiResponseDto.ok(mapSheetMngService.uploadProcess(hstUidList));
}
*/
/*
@Operation(summary = "오류데이터 팝업 > 추론 제외", description = "오류데이터 팝업 > 추론 제외")
@PutMapping("/except-inference")
public ApiResponseDto<MapSheetMngDto.DmlReturn> updateExceptUseInference(
@RequestBody @Valid List<Long> hstUidList) {
return ApiResponseDto.ok(mapSheetMngService.updateExceptUseInference(hstUidList));
}
*/
@Operation(summary = "페어 파일 업로드", description = "TFW/TIF 두 파일을 쌍으로 업로드 및 검증")
@PostMapping(value = "/upload-pair", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @PostMapping(value = "/upload-pair", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponseDto<MapSheetMngDto.DmlReturn> uploadPair( public ApiResponseDto<MapSheetMngDto.DmlReturn> uploadPair(
@RequestPart("tfw") MultipartFile tfwFile, @RequestPart("tfw") MultipartFile tfwFile,
@@ -213,7 +156,9 @@ public class MapSheetMngApiController {
mapSheetMngService.uploadPair(tfwFile, tifFile, hstUid, tifFileSize)); mapSheetMngService.uploadPair(tfwFile, tifFile, hstUid, tifFileSize));
} }
@Operation(summary = "영상관리 > 파일조회", description = "영상관리 > 파일조회") @Operation(
summary = "영상데이터관리 > 상세 > 오류 처리 내역 > 중복제거 > 팝업 내 해당 파일조회",
description = "영상데이터관리 > 상세 > 오류 처리 내역 > 중복제거 > 팝업 내 해당 파일조회")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -234,13 +179,13 @@ public class MapSheetMngApiController {
} }
@Operation( @Operation(
summary = "영상관리 > 파일사용설정 및 중복제거", summary = "영상데이터관리 > 상세 > 오류 처리 내역 > 중복제거 업데이트",
description = "영상관리 >파일사용설정 및 중복제거(중복파일제거 및 선택파일사용설정)") description = "영상데이터관리 > 상세 > 오류 처리 내역 > 중복제거 업데이트")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
responseCode = "201", responseCode = "201",
description = "파일사용설정 처리 성공", description = "중복제거 업데이트 처리 성공",
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
@@ -255,7 +200,7 @@ public class MapSheetMngApiController {
return ApiResponseDto.ok(mapSheetMngService.setUseByFileUidMngFile(fileUids)); return ApiResponseDto.ok(mapSheetMngService.setUseByFileUidMngFile(fileUids));
} }
@Operation(summary = "폴더 조회", description = "폴더 조회 (ROOT:/app/original-images 이하로 경로입력)") @Operation(summary = "영상데이터관리 > 데이터 등록 > NAS 폴더 선택", description = "영상데이터관리 > 데이터 등록 > NAS 폴더 선택")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
@@ -274,45 +219,9 @@ public class MapSheetMngApiController {
return ApiResponseDto.createOK(mapSheetMngService.getFolderAll(srchDto)); return ApiResponseDto.createOK(mapSheetMngService.getFolderAll(srchDto));
} }
@Operation(summary = "지정폴더내 파일목록 조회", description = "지정폴더내 파일목록 조회") @Operation(
@ApiResponses( summary = "영상데이터관리 > 상세 > 오류 처리 내역 > 업로드 (영상 tif 대용량 파일 분할 전송)",
value = { description = "영상데이터관리 > 상세 > 오류 처리 내역 > 업로드 (영상 tif 대용량 파일 분할 전송)")
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/file-list")
public ApiResponseDto<FilesDto> getFiles(@RequestBody SrchFilesDto srchDto) {
return ApiResponseDto.createOK(mapSheetMngService.getFilesAll(srchDto));
}
@Operation(summary = "영상 데이터 관리 완료 년도 목록 조회", description = "영상 데이터 관리 완료 년도 목록 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/mng-done-yyyy-list")
public ApiResponseDto<List<MngYyyyDto>> findMapSheetMngDoneYyyyList() {
return ApiResponseDto.ok(mapSheetMngService.findMapSheetMngDoneYyyyList());
}
@Operation(summary = "영상 tif 대용량 파일 분할 전송", description = "영상 tif 파일 대용량 파일을 청크 단위로 전송합니다.")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse(responseCode = "200", description = "청크 업로드 성공", content = @Content), @ApiResponse(responseCode = "200", description = "청크 업로드 성공", content = @Content),

View File

@@ -59,10 +59,20 @@ public class MapSheetMngService {
@Value("${file.sync-file-extention}") @Value("${file.sync-file-extention}")
private String syncFileExtention; private String syncFileExtention;
/**
* 영상데이터관리 > 목록 조회
*
* @return
*/
public List<MngDto> findMapSheetMngList() { public List<MngDto> findMapSheetMngList() {
return mapSheetMngCoreService.findMapSheetMngList(); return mapSheetMngCoreService.findMapSheetMngList();
} }
/**
* 영상데이터관리 > 데이터 등록 > 연도 선택 목록
*
* @return
*/
public List<Integer> findMapSheetMngYyyyList() { public List<Integer> findMapSheetMngYyyyList() {
return mapSheetMngCoreService.findMapSheetMngYyyyList(); return mapSheetMngCoreService.findMapSheetMngYyyyList();
} }
@@ -77,6 +87,12 @@ public class MapSheetMngService {
.toList(); .toList();
} }
/**
* 영상데이터관리 > 상세 조회
*
* @param mngYyyy
* @return
*/
public MngDto findMapSheetMng(int mngYyyy) { public MngDto findMapSheetMng(int mngYyyy) {
return mapSheetMngCoreService.findMapSheetMng(mngYyyy); return mapSheetMngCoreService.findMapSheetMng(mngYyyy);
} }
@@ -89,6 +105,12 @@ public class MapSheetMngService {
return new DmlReturn("success", "작업완료 처리되었습니다."); return new DmlReturn("success", "작업완료 처리되었습니다.");
} }
/**
* 영상데이터관리 > 상세 > 오류 처리 내역
*
* @param searchReq
* @return
*/
public Page<ErrorDataDto> findMapSheetErrorList(@Valid ErrorSearchReq searchReq) { public Page<ErrorDataDto> findMapSheetErrorList(@Valid ErrorSearchReq searchReq) {
return mapSheetMngCoreService.findMapSheetErrorList(searchReq); return mapSheetMngCoreService.findMapSheetErrorList(searchReq);
} }
@@ -97,12 +119,20 @@ public class MapSheetMngService {
return mapSheetMngCoreService.findMapSheetError(hstUid); return mapSheetMngCoreService.findMapSheetError(hstUid);
} }
/**
* 영상데이터관리 > 데이터 등록
*
* @param addReq
* @return
*/
@Transactional @Transactional
public DmlReturn mngDataSave(AddReq addReq) { public DmlReturn mngDataSave(AddReq addReq) {
addReq.setCreatedUid(userUtil.getId()); addReq.setCreatedUid(userUtil.getId());
int execCnt = mapSheetMngCoreService.mngDataSave(addReq); int execCnt =
mapSheetMngCoreService.mngDataSave(addReq); // tb_map_sheet_mng_hst 테이블에 insert된 개수 리턴됨
return new MapSheetMngDto.DmlReturn("success", addReq.getMngYyyy() + "년, " + execCnt + "건 생성"); return new MapSheetMngDto.DmlReturn("success", addReq.getMngYyyy() + "년, " + execCnt + "건 생성");
} }
@@ -110,6 +140,15 @@ public class MapSheetMngService {
return mapSheetMngCoreService.updateExceptUseInference(hstUidList); return mapSheetMngCoreService.updateExceptUseInference(hstUidList);
} }
/**
* 영상데이터관리 > 상세 > 오류 처리 내역 > 업로드 (페어 파일 저장)
*
* @param tfwFile
* @param tifFile
* @param hstUid
* @param tifFileSize
* @return
*/
@Transactional @Transactional
public DmlReturn uploadPair( public DmlReturn uploadPair(
MultipartFile tfwFile, String tifFile, Long hstUid, Long tifFileSize) { MultipartFile tfwFile, String tifFile, Long hstUid, Long tifFileSize) {
@@ -118,6 +157,7 @@ public class MapSheetMngService {
DmlReturn dmlReturn = new DmlReturn("success", "UPLOAD COMPLETE"); DmlReturn dmlReturn = new DmlReturn("success", "UPLOAD COMPLETE");
// hst 테이블에 선택한 hstUid row 정보 조회
ErrorDataDto errDto = mapSheetMngCoreService.findMapSheetError(hstUid); ErrorDataDto errDto = mapSheetMngCoreService.findMapSheetError(hstUid);
if (errDto == null) { if (errDto == null) {
@@ -135,33 +175,14 @@ public class MapSheetMngService {
return dmlReturn; return dmlReturn;
} }
// TODO 삭제? // ★ tif 파일은 대용량 업로드에서 아래 정보들을 체크하기 때문에 체크 로직에서 제외됨
MngDto mngDto = mapSheetMngCoreService.findMapSheetMng(errDto.getMngYyyy());
// 중복체크 -> 도엽50k/uuid 경로에 업로드 할 거라 overwrite 되지 않음
// if (!overwrite) {
// dmlReturn =
// this.duplicateFile(
// errDto.getMngYyyy(), tfwFile.getOriginalFilename(), tifFile.getOriginalFilename());
// if (dmlReturn.getFlag().equals("duplicate")) {
// return dmlReturn;
// }
// }
// 멀티파트 파일 tmp폴더 저장(파일형식 체크를 위해) // 멀티파트 파일 tmp폴더 저장(파일형식 체크를 위해)
String tfwTmpPath = tmpPath + tfwFile.getOriginalFilename(); String tfwTmpPath = tmpPath + tfwFile.getOriginalFilename();
// String tifTmpPath = tmpPath + tifFile;
if (!FIleChecker.multipartSaveTo(tfwFile, tfwTmpPath)) { if (!FIleChecker.multipartSaveTo(tfwFile, tfwTmpPath)) {
return new DmlReturn("fail", "UPLOAD ERROR"); return new DmlReturn("fail", "UPLOAD ERROR");
} }
// if (!FIleChecker.multipartSaveTo(tifFile, tifTmpPath)) {
// return new DmlReturn("fail", "UPLOAD ERROR");
// }
// if (!FIleChecker.cmmndGdalInfo(tifTmpPath)) {
// return new DmlReturn("fail", "TIF TYPE ERROR");
// }
if (!FIleChecker.checkTfw(tfwTmpPath)) { if (!FIleChecker.checkTfw(tfwTmpPath)) {
return new DmlReturn("fail", "TFW TYPE ERROR"); return new DmlReturn("fail", "TFW TYPE ERROR");
} }
@@ -175,7 +196,6 @@ public class MapSheetMngService {
} }
Path tfwTmpSavePath = Paths.get(tfwTmpPath); Path tfwTmpSavePath = Paths.get(tfwTmpPath);
// Path tifTmpSavePath = Paths.get(tifTmpPath);
Path tfwTargetPath = null; Path tfwTargetPath = null;
Path tifTargetPath = null; Path tifTargetPath = null;
@@ -222,7 +242,7 @@ public class MapSheetMngService {
try { try {
Files.move(tfwTmpSavePath, tfwTargetPath, StandardCopyOption.REPLACE_EXISTING); Files.move(tfwTmpSavePath, tfwTargetPath, StandardCopyOption.REPLACE_EXISTING);
// Files.move(tifTmpSavePath, tifTargetPath, StandardCopyOption.REPLACE_EXISTING); // tif파일은 대용량 업로드에서 이미 업로드 했기 때문에 완료된 상태
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@@ -234,7 +254,7 @@ public class MapSheetMngService {
updReqSyncCheckState.setFilePath(uploadPath); updReqSyncCheckState.setFilePath(uploadPath);
updReqSyncCheckState.setSyncCheckTfwFileName(tfwFile.getOriginalFilename()); updReqSyncCheckState.setSyncCheckTfwFileName(tfwFile.getOriginalFilename());
updReqSyncCheckState.setSyncCheckTifFileName(tifFile); updReqSyncCheckState.setSyncCheckTifFileName(tifFile);
updReqSyncCheckState.setSyncCheckState("DONE"); updReqSyncCheckState.setSyncCheckState("DONE"); // 오류처리 완료
mapSheetMngCoreService.updateMapSheetMngHstSyncCheckState(updReqSyncCheckState); mapSheetMngCoreService.updateMapSheetMngHstSyncCheckState(updReqSyncCheckState);
// 파일정보 업데이트 // 파일정보 업데이트
mapSheetMngCoreService.deleteByHstUidMngFile(hstUid); mapSheetMngCoreService.deleteByHstUidMngFile(hstUid);
@@ -339,7 +359,12 @@ public class MapSheetMngService {
public FoldersDto getFolderAll(SrchFoldersDto srchDto) { public FoldersDto getFolderAll(SrchFoldersDto srchDto) {
String dirPath = syncRootDir + srchDto.getDirPath(); // "경로중복"
String dirPath =
(srchDto.getDirPath() == null || srchDto.getDirPath().isEmpty())
? syncRootDir
: srchDto.getDirPath();
// String dirPath = syncRootDir + srchDto.getDirPath();
log.info("[FIND_FOLDER] DIR : {}", dirPath); log.info("[FIND_FOLDER] DIR : {}", dirPath);
List<FIleChecker.Folder> folderList = List<FIleChecker.Folder> folderList =
@@ -381,6 +406,11 @@ public class MapSheetMngService {
mapSheetMngCoreService.getSceneInference(yyyy); mapSheetMngCoreService.getSceneInference(yyyy);
} }
/**
* 연도 목록 조회
*
* @return
*/
public List<MngYyyyDto> findMapSheetMngDoneYyyyList() { public List<MngYyyyDto> findMapSheetMngDoneYyyyList() {
List<MngDto> mngList = mapSheetMngCoreService.findMapSheetMngList(); List<MngDto> mngList = mapSheetMngCoreService.findMapSheetMngList();

View File

@@ -67,6 +67,7 @@ public class ModelMngDto {
private String clsModelVersion; private String clsModelVersion;
private Double priority; private Double priority;
private String memo; private String memo;
private UUID uuid;
public Basic( public Basic(
Long modelUid, Long modelUid,
@@ -89,7 +90,8 @@ public class ModelMngDto {
String clsModelFileName, String clsModelFileName,
String clsModelVersion, String clsModelVersion,
double priority, double priority,
String memo) { String memo,
UUID uuid) {
this.modelUid = modelUid; this.modelUid = modelUid;
this.modelVer = modelVer; this.modelVer = modelVer;
this.createCompleteDttm = createCompleteDttm; this.createCompleteDttm = createCompleteDttm;
@@ -111,6 +113,7 @@ public class ModelMngDto {
this.clsModelVersion = clsModelVersion; this.clsModelVersion = clsModelVersion;
this.priority = priority; this.priority = priority;
this.memo = memo; this.memo = memo;
this.uuid = uuid;
} }
} }

View File

@@ -41,6 +41,16 @@ public class ModelMngService {
@Value("${file.pt-FileName}") @Value("${file.pt-FileName}")
private String ptFileName; private String ptFileName;
/**
* 모델조회
*
* @param searchReq 페이징
* @param startDate 시작날짜
* @param endDate 종료날짜
* @param modelType 모델 타입 G1, G2, G3
* @param searchVal 모델 ver
* @return 모델 목록
*/
public Page<ModelMngDto.ModelList> findModelMgmtList( public Page<ModelMngDto.ModelList> findModelMgmtList(
ModelMngDto.searchReq searchReq, ModelMngDto.searchReq searchReq,
LocalDate startDate, LocalDate startDate,

View File

@@ -46,6 +46,13 @@ public class AuditLogCoreService
return auditLogRepository.findLogByAccount(searchRange, searchValue); return auditLogRepository.findLogByAccount(searchRange, searchValue);
} }
/**
* 다운로드 이력 조회
*
* @param searchReq 페이징 파라미터
* @param downloadReq 다운로드 이력 팝업 검색 조건
* @return 다운로드 이력 정보 목록
*/
public Page<AuditLogDto.DownloadRes> findLogByAccount( public Page<AuditLogDto.DownloadRes> findLogByAccount(
AuditLogDto.searchReq searchReq, DownloadReq downloadReq) { AuditLogDto.searchReq searchReq, DownloadReq downloadReq) {
return auditLogRepository.findDownloadLog(searchReq, downloadReq); return auditLogRepository.findDownloadLog(searchReq, downloadReq);

View File

@@ -77,4 +77,8 @@ public class GukYuinCoreService {
public void updateMapSheetInferenceLabelEndStatus(Long learnId) { public void updateMapSheetInferenceLabelEndStatus(Long learnId) {
gukYuinRepository.updateMapSheetInferenceLabelEndStatus(learnId); gukYuinRepository.updateMapSheetInferenceLabelEndStatus(learnId);
} }
public List<String> findStbltObjectIds(String uid, String mapSheetNum) {
return gukYuinRepository.findStbltObjectIds(uid, mapSheetNum);
}
} }

View File

@@ -79,12 +79,13 @@ public class InferenceResultCoreService {
} }
/** /**
* 변화탐지 실행 정보 생성 * 변화탐지 실행 정보 생성 - 추론 실행 목록 테이블 저장, 도엽목록별 상태 체크 테이블 저장
* *
* @param req * @param req 추론 실행 목록 uuid
*/ */
public UUID saveInferenceInfo(InferenceResultDto.RegReq req, List<MngListDto> targetList) { public UUID saveInferenceInfo(InferenceResultDto.RegReq req, List<MngListDto> targetList) {
// 대표 도엽명 외 N 건 실행 문구 만들기 위해 Null, 중복 제거
List<MngListDto> distinctList = List<MngListDto> distinctList =
targetList.stream() targetList.stream()
.filter(dto -> dto.getMapSheetName() != null && !dto.getMapSheetName().isBlank()) .filter(dto -> dto.getMapSheetName() != null && !dto.getMapSheetName().isBlank())
@@ -124,17 +125,13 @@ public class InferenceResultCoreService {
mapSheetLearnEntity.setDetectingCnt(0L); mapSheetLearnEntity.setDetectingCnt(0L);
mapSheetLearnEntity.setTotalJobs((long) targetList.size()); mapSheetLearnEntity.setTotalJobs((long) targetList.size());
// 회차는 국유인 반영할때 update로 변경됨
// mapSheetLearnEntity.setStage(
// mapSheetLearnRepository.getLearnStage(req.getCompareYyyy(), req.getTargetYyyy()));
// learn 테이블 저장 // learn 테이블 저장
MapSheetLearnEntity savedLearn = mapSheetLearnRepository.save(mapSheetLearnEntity); MapSheetLearnEntity savedLearn = mapSheetLearnRepository.save(mapSheetLearnEntity);
final int CHUNK = 1000; final int CHUNK = 1000;
List<MapSheetLearn5kEntity> buffer = new ArrayList<>(CHUNK); List<MapSheetLearn5kEntity> buffer = new ArrayList<>(CHUNK);
// learn 도엽별 저장 // learn 도엽별 저장, 도엽수가 많으므로 1000개 씩 저장함
for (MngListDto mngDto : targetList) { for (MngListDto mngDto : targetList) {
MapSheetLearn5kEntity entity = new MapSheetLearn5kEntity(); MapSheetLearn5kEntity entity = new MapSheetLearn5kEntity();
entity.setLearn(savedLearn); entity.setLearn(savedLearn);
@@ -145,12 +142,15 @@ public class InferenceResultCoreService {
buffer.add(entity); buffer.add(entity);
if (buffer.size() == CHUNK) { if (buffer.size() == CHUNK) {
// 도엽별 저장 learn 5k 테이블
flushChunk(buffer); flushChunk(buffer);
buffer.clear(); buffer.clear();
} }
} }
// chunk 남은거 처리
if (!buffer.isEmpty()) { if (!buffer.isEmpty()) {
// 도엽별 저장 learn 5k 테이블
flushChunk(buffer); flushChunk(buffer);
buffer.clear(); buffer.clear();
} }
@@ -159,9 +159,9 @@ public class InferenceResultCoreService {
} }
/** /**
* 도엽별 저장 * 도엽별 저장 learn 5k 테이블
* *
* @param buffer * @param buffer 저장 정보
*/ */
private void flushChunk(List<MapSheetLearn5kEntity> buffer) { private void flushChunk(List<MapSheetLearn5kEntity> buffer) {
@@ -283,7 +283,7 @@ public class InferenceResultCoreService {
.distinct() // 중복 방지 (선택) .distinct() // 중복 방지 (선택)
.toList(); .toList();
// testing 추론결과 테이블 조회하여 탐지 개수 업데이트 // testing 테이블 추론결과 테이블 조회하여 탐지 개수 업데이트
Long testing = getInferenceResultCnt(batchIds); Long testing = getInferenceResultCnt(batchIds);
// 공통 영역 업데이트 // 공통 영역 업데이트
@@ -369,6 +369,12 @@ public class InferenceResultCoreService {
return mapSheetLearnRepository.getInferenceServerStatusList(); return mapSheetLearnRepository.getInferenceServerStatusList();
} }
/**
* 진행중 배치 조회
*
* @param status
* @return
*/
public InferenceBatchSheet getInferenceResultByStatus(String status) { public InferenceBatchSheet getInferenceResultByStatus(String status) {
MapSheetLearnEntity entity = MapSheetLearnEntity entity =
mapSheetLearnRepository.getInferenceResultByStatus(status).orElse(null); mapSheetLearnRepository.getInferenceResultByStatus(status).orElse(null);
@@ -403,6 +409,12 @@ public class InferenceResultCoreService {
return mapSheetLearnRepository.getInferenceAiResultById(id, modelUuid); return mapSheetLearnRepository.getInferenceAiResultById(id, modelUuid);
} }
/**
* 추론 진행 현황 상세
*
* @param uuid
* @return
*/
public InferenceStatusDetailDto getInferenceStatus(UUID uuid) { public InferenceStatusDetailDto getInferenceStatus(UUID uuid) {
return mapSheetLearnRepository.getInferenceStatus(uuid); return mapSheetLearnRepository.getInferenceStatus(uuid);
} }
@@ -410,7 +422,7 @@ public class InferenceResultCoreService {
/** /**
* 추론 진행중인지 확인 * 추론 진행중인지 확인
* *
* @return * @return 추론 실행중인 추론 uuid, batch id
*/ */
public SaveInferenceAiDto getProcessing() { public SaveInferenceAiDto getProcessing() {
MapSheetLearnEntity entity = mapSheetLearnRepository.getProcessing(); MapSheetLearnEntity entity = mapSheetLearnRepository.getProcessing();
@@ -441,17 +453,30 @@ public class InferenceResultCoreService {
* @return * @return
*/ */
public AnalResultInfo getInferenceResultInfo(UUID uuid) { public AnalResultInfo getInferenceResultInfo(UUID uuid) {
// 추론 결과 정보조회
AnalResultInfo resultInfo = mapSheetLearnRepository.getInferenceResultInfo(uuid); AnalResultInfo resultInfo = mapSheetLearnRepository.getInferenceResultInfo(uuid);
// bbox, point 조회
BboxPointDto bboxPointDto = mapSheetLearnRepository.getBboxPoint(uuid); BboxPointDto bboxPointDto = mapSheetLearnRepository.getBboxPoint(uuid);
resultInfo.setBboxGeom(bboxPointDto.getBboxGeom()); resultInfo.setBboxGeom(bboxPointDto.getBboxGeom());
resultInfo.setBboxCenterPoint(bboxPointDto.getBboxCenterPoint()); resultInfo.setBboxCenterPoint(bboxPointDto.getBboxCenterPoint());
return resultInfo; return resultInfo;
} }
/**
* 분류별 탐지건수 조회
*
* @param uuid 추론 uuid
* @return 분류별 탐지건수 정보
*/
public List<Dashboard> getInferenceClassCountList(UUID uuid) { public List<Dashboard> getInferenceClassCountList(UUID uuid) {
return mapSheetLearnRepository.getInferenceClassCountList(uuid); return mapSheetLearnRepository.getInferenceClassCountList(uuid);
} }
/**
* @param uuid 추론 uuid
* @param searchGeoReq 추론 결과 상세화면 geom 조회 조건
* @return geom 목록 정보
*/
public Page<Geom> getInferenceGeomList(UUID uuid, SearchGeoReq searchGeoReq) { public Page<Geom> getInferenceGeomList(UUID uuid, SearchGeoReq searchGeoReq) {
return mapSheetLearnRepository.getInferenceGeomList(uuid, searchGeoReq); return mapSheetLearnRepository.getInferenceGeomList(uuid, searchGeoReq);
} }
@@ -510,15 +535,21 @@ public class InferenceResultCoreService {
return list.stream().map(InferenceResultsTestingEntity::toDto).toList(); return list.stream().map(InferenceResultsTestingEntity::toDto).toList();
} }
/**
* 테스팅 테이블 조회하여 탐지건수 조회
*
* @param batchIds batchIds
* @return batchIds 조회 count 수
*/
public Long getInferenceResultCnt(List<Long> batchIds) { public Long getInferenceResultCnt(List<Long> batchIds) {
return inferenceResultsTestingRepository.getInferenceResultCnt(batchIds); return inferenceResultsTestingRepository.getInferenceResultCnt(batchIds);
} }
/** /**
* uid 조회 * 추론 정보 조회 하여 batch id, 32자 uid 리턴
* *
* @param uuid * @param uuid 추론 uuid
* @return * @return 추론정보
*/ */
public InferenceLearnDto getInferenceUid(UUID uuid) { public InferenceLearnDto getInferenceUid(UUID uuid) {
MapSheetLearnEntity entity = inferenceResultRepository.getInferenceUid(uuid).orElse(null); MapSheetLearnEntity entity = inferenceResultRepository.getInferenceUid(uuid).orElse(null);
@@ -535,7 +566,7 @@ public class InferenceResultCoreService {
} }
/** /**
* 실행중인 추론 도엽명 목록 * 분석중인 추론 도엽명 목록
* *
* @param uuid 추론 실행중인 uuid * @param uuid 추론 실행중인 uuid
* @return * @return
@@ -544,11 +575,37 @@ public class InferenceResultCoreService {
return mapSheetLearn5kRepository.getInferenceRunMapId(uuid); return mapSheetLearn5kRepository.getInferenceRunMapId(uuid);
} }
/**
* 실패 처리되어야 할 목록 중에 아직 실패로 표시되지 않은 ID 조회
*
* @param uuid 추론 uuid
* @param failMapIds AI API 연결하여 조회한 실패 job id
* @param type 모델 타입
* @return job id
*/
public List<Long> findFail5kList(UUID uuid, List<Long> failMapIds, String type) { public List<Long> findFail5kList(UUID uuid, List<Long> failMapIds, String type) {
return mapSheetLearn5kRepository.findFail5kList(uuid, failMapIds, type); return mapSheetLearn5kRepository.findFail5kList(uuid, failMapIds, type);
} }
/**
* 완료된 것으로 들어온 목록 중 실제로 존재하는 5k jobId 조회
*
* @param uuid 추론 uuid
* @param completedIds AI API 연결하여 조회한 성공 job id
* @param type 모델 타입
* @return job id
*/
public List<Long> findCompleted5kList(UUID uuid, List<Long> completedIds, String type) { public List<Long> findCompleted5kList(UUID uuid, List<Long> completedIds, String type) {
return mapSheetLearn5kRepository.findCompleted5kList(uuid, completedIds, type); return mapSheetLearn5kRepository.findCompleted5kList(uuid, completedIds, type);
} }
/**
* testing 테이블 결과로 기본정보 조회
*
* @param batchIds batch id
* @return batch id, model ver, year 정보
*/
public List<InferenceResultsTestingDto.Basic> getInferenceResultGroupList(List<Long> batchIds) {
return inferenceResultsTestingRepository.getInferenceResultGroupList(batchIds);
}
} }

View File

@@ -1,13 +1,8 @@
package com.kamco.cd.kamcoback.postgres.core; package com.kamco.cd.kamcoback.postgres.core;
import com.kamco.cd.kamcoback.common.enums.MngStateType; import com.kamco.cd.kamcoback.common.enums.MngStateType;
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter; import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter;
import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.ImageFeature; import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.ImageFeature;
import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.Scene;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.DetectOption;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetScope;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngListCompareDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngListCompareDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngListDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngListDto;
@@ -21,21 +16,21 @@ import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -53,13 +48,20 @@ public class MapSheetMngCoreService {
@Value("${file.sync-root-dir}") @Value("${file.sync-root-dir}")
private String syncRootDir; private String syncRootDir;
@Value("${inference.geojson-dir}") /**
private String inferenceDir; * 영상데이터관리 > 목록 조회
*
* @return
*/
public List<MapSheetMngDto.MngDto> findMapSheetMngList() { public List<MapSheetMngDto.MngDto> findMapSheetMngList() {
return mapSheetMngRepository.findMapSheetMngList(); return mapSheetMngRepository.findMapSheetMngList();
} }
/**
* 영상데이터관리 > 데이터 등록 > 연도 선택 목록
*
* @return
*/
public List<Integer> findMapSheetMngYyyyList() { public List<Integer> findMapSheetMngYyyyList() {
return mapSheetMngRepository.findMapSheetMngYyyyList(); return mapSheetMngRepository.findMapSheetMngYyyyList();
} }
@@ -76,6 +78,12 @@ public class MapSheetMngCoreService {
return mapSheetMngRepository.getYears(req); return mapSheetMngRepository.getYears(req);
} }
/**
* 영상데이터관리 > 상세 조회
*
* @param mngYyyy
* @return
*/
public MapSheetMngDto.MngDto findMapSheetMng(int mngYyyy) { public MapSheetMngDto.MngDto findMapSheetMng(int mngYyyy) {
return mapSheetMngRepository.findMapSheetMng(mngYyyy); return mapSheetMngRepository.findMapSheetMng(mngYyyy);
} }
@@ -103,15 +111,33 @@ public class MapSheetMngCoreService {
mapSheetMngRepository.updateMapSheetMngHstSyncCheckState(reqDto); mapSheetMngRepository.updateMapSheetMngHstSyncCheckState(reqDto);
} }
/**
* 영상데이터관리 > 상세 > 오류 처리 내역
*
* @param searchReq 오류 검색 조건
* @return
*/
public Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList( public Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList(
MapSheetMngDto.@Valid ErrorSearchReq searchReq) { MapSheetMngDto.@Valid ErrorSearchReq searchReq) {
return mapSheetMngRepository.findMapSheetErrorList(searchReq); return mapSheetMngRepository.findMapSheetErrorList(searchReq);
} }
/**
* hst 테이블에 선택한 hstUid row 정보 조회
*
* @param hstUid
* @return
*/
public MapSheetMngDto.ErrorDataDto findMapSheetError(Long hstUid) { public MapSheetMngDto.ErrorDataDto findMapSheetError(Long hstUid) {
return mapSheetMngRepository.findMapSheetError(hstUid); return mapSheetMngRepository.findMapSheetError(hstUid);
} }
/**
* 파일 목록으로 업로드 경로 확인
*
* @param hstUid
* @return
*/
public List<MapSheetMngDto.MngFilesDto> findByHstUidMapSheetFileList(Long hstUid) { public List<MapSheetMngDto.MngFilesDto> findByHstUidMapSheetFileList(Long hstUid) {
return mapSheetMngRepository.findByHstUidMapSheetFileList(hstUid); return mapSheetMngRepository.findByHstUidMapSheetFileList(hstUid);
} }
@@ -154,12 +180,19 @@ public class MapSheetMngCoreService {
entity.setCreatedUid(addReq.getCreatedUid()); entity.setCreatedUid(addReq.getCreatedUid());
entity.setUpdatedUid(addReq.getCreatedUid()); entity.setUpdatedUid(addReq.getCreatedUid());
// 같은 년도로 저장된 데이터가 있다면 삭제
// tb_map_sheet_mng, tb_map_sheet_hst, tb_map_sheet_mng_files, tb_map_sheet_mng_tile
mapSheetMngRepository.deleteByMngYyyyMngAll(addReq.getMngYyyy()); mapSheetMngRepository.deleteByMngYyyyMngAll(addReq.getMngYyyy());
// tb_map_sheet_mng 엔티티 저장
MapSheetMngEntity saved = mapSheetMngRepository.save(entity); MapSheetMngEntity saved = mapSheetMngRepository.save(entity);
// 5k 도엽 기준으로 tb_map_sheet_hst 테이블에 먼저 insert 하기
int hstCnt = int hstCnt =
mapSheetMngRepository.insertMapSheetOrgDataToMapSheetMngHst( mapSheetMngRepository.insertMapSheetOrgDataToMapSheetMngHst(
saved.getMngYyyy(), saved.getMngPath()); saved.getMngYyyy(), saved.getMngPath());
// tb_year 에 해당 년도 완료로 업데이트
mapSheetMngRepository.updateYearState(saved.getMngYyyy(), "DONE"); mapSheetMngRepository.updateYearState(saved.getMngYyyy(), "DONE");
// 년도별 Tile 정보 등록 // 년도별 Tile 정보 등록
@@ -233,94 +266,70 @@ public class MapSheetMngCoreService {
} }
/** /**
* 추론 실행에 필요한 geojson 파일 생성 * geojson 생성시 필요한 영상파일 정보 조회
* *
* @param yyyy 영상관리 파일별 년도 * @param yyyy
* @param scenes 5k 도엽 번호 리스트 * @param mapSheetNums
* @param mapSheetScope EXCL : 추론제외, PREV 이전 년도 도엽 사용 * @return ImageFeature
* @return
*/ */
public Scene getSceneInference( public List<ImageFeature> loadSceneInferenceBySheets(String yyyy, List<String> mapSheetNums) {
String yyyy, List<String> scenes, String mapSheetScope, String detectOption) {
Map<String, Object> result = new HashMap<>(); if (mapSheetNums == null || mapSheetNums.isEmpty()) {
boolean isAll = MapSheetScope.ALL.getId().equals(mapSheetScope); return List.of();
String optionSuffix = "";
if (DetectOption.EXCL.getId().equals(detectOption)) {
optionSuffix = "_EXCL";
} else if (DetectOption.PREV.getId().equals(detectOption)) {
optionSuffix = "_PREV";
} }
// 1) 경로/파일명 결정 // CHUNK_SIZE 단위로 나누어 여러 번 조회한다.
String targetDir = final int CHUNK_SIZE = 1000;
"local".equals(activeEnv) ? System.getProperty("user.home") + "/geojson" : inferenceDir; List<ImageFeature> features = new ArrayList<>();
String filename = // i부터 CHUNK_SIZE만큼 잘라서 조회
isAll // 마지막 구간은 남은 개수만큼만 처리하기 위해 Math.min 사용
? String.format("%s_%s_ALL%s.geojson", yyyy, activeEnv, optionSuffix) for (int i = 0; i < mapSheetNums.size(); i += CHUNK_SIZE) {
: String.format("%s_%s%s.geojson", yyyy, activeEnv, optionSuffix); List<String> chunk = mapSheetNums.subList(i, Math.min(i + CHUNK_SIZE, mapSheetNums.size()));
features.addAll(mapSheetMngRepository.getSceneInference(yyyy, chunk));
Path outputPath = Paths.get(targetDir, filename);
// 2) ALL일 때만 재사용
// if (isAll && Files.exists(outputPath)) {
// return outputPath.toString();
// }
// 3) 데이터 조회
List<ImageFeature> sceneInference = mapSheetMngRepository.getSceneInference(yyyy, scenes);
if (sceneInference == null || sceneInference.isEmpty()) {
log.warn(
"NOT_FOUND_TARGET_YEAR: yyyy={}, isAll={}, scenesSize={}",
yyyy,
isAll,
scenes == null ? 0 : scenes.size());
throw new CustomApiException("NOT_FOUND_TARGET_YEAR", HttpStatus.NOT_FOUND);
}
// 4) 파일 생성
try {
log.info("create Directories outputPath: {}", outputPath);
log.info(
"activeEnv={}, inferenceDir={}, targetDir={}, filename={}",
activeEnv,
inferenceDir,
targetDir,
filename);
log.info("outputPath={}, parent={}", outputPath.toAbsolutePath(), outputPath.getParent());
Files.createDirectories(outputPath.getParent());
new GeoJsonFileWriter()
.exportToFile(sceneInference, "scene_inference_" + yyyy, 5186, outputPath.toString());
log.info("GeoJsonFileWriter: {}", "scene_inference_" + yyyy);
Scene scene = new Scene();
scene.setFeatures(sceneInference);
scene.setFilePath(outputPath.toString());
return scene;
} catch (IOException e) {
log.error(
"FAIL_CREATE_MAP_SHEET_FILE: yyyy={}, isAll={}, path={}", yyyy, isAll, outputPath, e);
throw new CustomApiException("INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, e);
} }
return features;
} }
/** /**
* 변화탐지 실행 가능 기준 년도 조회 * 년도별로 나눠 조회
* *
* @param req * @param yearDtos
* @return * @return ImageFeature
*/ */
public List<MngListDto> getHstMapSheetList(InferenceResultDto.RegReq req) { public List<ImageFeature> loadSceneInferenceByFallbackYears(List<MngListDto> yearDtos) {
return mapSheetMngRepository.findByHstMapSheetTargetList(req); if (yearDtos == null || yearDtos.isEmpty()) {
return List.of();
} }
public List<MngListDto> getHstMapSheetList(int mngYyyy, List<String> mapIds) { // 년도 별로 루프를 돌리기위해 년도별 정리
return mapSheetMngRepository.findByHstMapSheetTargetList(mngYyyy, mapIds); Map<Integer, List<MngListDto>> groupedByYear =
yearDtos.stream()
.filter(d -> d.getMngYyyy() != 0 && d.getMapSheetNum() != null)
.collect(Collectors.groupingBy(MngListDto::getMngYyyy));
List<ImageFeature> sceneInference = new ArrayList<>();
for (Map.Entry<Integer, List<MngListDto>> entry : groupedByYear.entrySet()) {
Integer year = entry.getKey();
// 년도별 mapSheetNum 만들기
List<String> sheetNums =
entry.getValue().stream()
.map(MngListDto::getMapSheetNum)
.filter(Objects::nonNull)
.distinct()
.toList();
// tif파일 정보 조회
List<ImageFeature> temp = loadSceneInferenceBySheets(year.toString(), sheetNums);
if (temp != null && !temp.isEmpty()) {
sceneInference.addAll(temp);
}
}
return sceneInference;
} }
public void updateMapSheetMngHstUploadId(Long hstUid, UUID uuid, String uploadId) { public void updateMapSheetMngHstUploadId(Long hstUid, UUID uuid, String uploadId) {
@@ -343,8 +352,32 @@ public class MapSheetMngCoreService {
return mapSheetMngYearRepository.findByHstMapSheetCompareList(mngYyyy, mapId); return mapSheetMngYearRepository.findByHstMapSheetCompareList(mngYyyy, mapId);
} }
public List<String> getMapSheetMngHst(Integer year) { public List<MngListDto> getMapSheetMngHst(
List<MapSheetMngHstEntity> entity = mapSheetMngRepository.getMapSheetMngHst(year); Integer year, String mapSheetScope, List<String> mapSheetNum) {
return entity.stream().map(MapSheetMngHstEntity::getMapSheetNum).toList(); return mapSheetMngRepository.getMapSheetMngHst(year, mapSheetScope, mapSheetNum);
}
/**
* 이전 년도 도엽 조회 조건이 많을 수 있으므로 chunk 줘서 끊어서 조회
*
* @param year
* @param mapIds
* @return
*/
public List<MngListDto> findFallbackCompareYearByMapSheets(Integer year, List<String> mapIds) {
if (mapIds == null || mapIds.isEmpty()) {
return Collections.emptyList();
}
int chunkSize = 1000;
List<MngListDto> result = new ArrayList<>();
for (int i = 0; i < mapIds.size(); i += chunkSize) {
List<String> chunk = mapIds.subList(i, Math.min(i + chunkSize, mapIds.size()));
result.addAll(mapSheetMngRepository.findFallbackCompareYearByMapSheets(year, chunk));
}
return result;
} }
} }

View File

@@ -17,6 +17,16 @@ public class ModelMngCoreService {
private final ModelMngRepository modelMngRepository; private final ModelMngRepository modelMngRepository;
/**
* 모델조회
*
* @param searchReq 페이징
* @param startDate 시작날짜
* @param endDate 종료날짜
* @param modelType 모델 타입 G1, G2, G3
* @param searchVal 모델 ver
* @return 모델 목록
*/
public Page<ModelMngDto.ModelList> findModelMgmtList( public Page<ModelMngDto.ModelList> findModelMgmtList(
ModelMngDto.searchReq searchReq, ModelMngDto.searchReq searchReq,
LocalDate startDate, LocalDate startDate,
@@ -99,4 +109,18 @@ public class ModelMngCoreService {
.orElseThrow(() -> new EntityNotFoundException("모델 정보가 없습니다.")); .orElseThrow(() -> new EntityNotFoundException("모델 정보가 없습니다."));
return entity.toDto(); return entity.toDto();
} }
/**
* 모델 버전명으로 조회
*
* @param ver 모델버전
* @return 모델정보
*/
public ModelMngDto.Basic findByModelVer(String ver) {
ModelMngEntity entity =
modelMngRepository
.findByModelVer(ver)
.orElseThrow(() -> new EntityNotFoundException("모델 정보가 없습니다."));
return entity.toDto();
}
} }

View File

@@ -117,6 +117,7 @@ public class ModelMngEntity extends CommonDateEntity {
this.clsModelFileName, this.clsModelFileName,
this.clsModelVersion, this.clsModelVersion,
this.priority, this.priority,
this.memo); this.memo,
this.uuid);
} }
} }

View File

@@ -16,5 +16,11 @@ public interface InferenceResultRepositoryCustom {
Long getInferenceLearnIdByUuid(UUID uuid); Long getInferenceLearnIdByUuid(UUID uuid);
/**
* 추론 정보 조회
*
* @param uuid 추론 uuid
* @return 추론 정보
*/
Optional<MapSheetLearnEntity> getInferenceUid(UUID uuid); Optional<MapSheetLearnEntity> getInferenceUid(UUID uuid);
} }

View File

@@ -1,11 +1,32 @@
package com.kamco.cd.kamcoback.postgres.repository.Inference; package com.kamco.cd.kamcoback.postgres.repository.Inference;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultsTestingDto;
import com.kamco.cd.kamcoback.postgres.entity.InferenceResultsTestingEntity; import com.kamco.cd.kamcoback.postgres.entity.InferenceResultsTestingEntity;
import java.util.List; import java.util.List;
public interface InferenceResultsTestingRepositoryCustom { public interface InferenceResultsTestingRepositoryCustom {
/**
* 추론 결과 조회
*
* @param batchIds batch id
* @return 추론 결과 목록
*/
List<InferenceResultsTestingEntity> getInferenceResultList(List<Long> batchIds); List<InferenceResultsTestingEntity> getInferenceResultList(List<Long> batchIds);
/**
* 테스팅 테이블 조회하여 탐지건수 조회
*
* @param batchIds batchIds
* @return batchIds 조회 count 수
*/
Long getInferenceResultCnt(List<Long> batchIds); Long getInferenceResultCnt(List<Long> batchIds);
/**
* testing 테이블 결과로 기본정보 조회
*
* @param batchIds batch id
* @return batch id, model ver, year 정보
*/
List<InferenceResultsTestingDto.Basic> getInferenceResultGroupList(List<Long> batchIds);
} }

View File

@@ -2,7 +2,9 @@ package com.kamco.cd.kamcoback.postgres.repository.Inference;
import static com.kamco.cd.kamcoback.postgres.entity.QInferenceResultsTestingEntity.inferenceResultsTestingEntity; import static com.kamco.cd.kamcoback.postgres.entity.QInferenceResultsTestingEntity.inferenceResultsTestingEntity;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultsTestingDto;
import com.kamco.cd.kamcoback.postgres.entity.InferenceResultsTestingEntity; import com.kamco.cd.kamcoback.postgres.entity.InferenceResultsTestingEntity;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory; import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -47,4 +49,29 @@ public class InferenceResultsTestingRepositoryImpl
return cnt == null ? 0L : cnt; return cnt == null ? 0L : cnt;
} }
@Override
public List<InferenceResultsTestingDto.Basic> getInferenceResultGroupList(List<Long> batchIds) {
return queryFactory
.select(
Projections.constructor(
InferenceResultsTestingDto.Basic.class,
inferenceResultsTestingEntity.batchId,
inferenceResultsTestingEntity.modelVersion.max(),
inferenceResultsTestingEntity.beforeYear.max(),
inferenceResultsTestingEntity.afterYear.max()))
.from(inferenceResultsTestingEntity)
.where(
inferenceResultsTestingEntity
.batchId
.in(batchIds)
.and(inferenceResultsTestingEntity.afterC.isNotNull())
.and(inferenceResultsTestingEntity.afterP.isNotNull()))
.groupBy(
inferenceResultsTestingEntity.batchId,
inferenceResultsTestingEntity.modelVersion,
inferenceResultsTestingEntity.beforeYear,
inferenceResultsTestingEntity.afterYear)
.fetch();
}
} }

View File

@@ -6,13 +6,49 @@ import java.util.UUID;
public interface MapSheetLearn5kRepositoryCustom { public interface MapSheetLearn5kRepositoryCustom {
/**
* 추론 실행 실패 정보 저장
*
* @param uuid 추론 uuid
* @param jobDto AI API에서 조회한 Job 정보
* @param type 모델 타입
*/
void saveFail5k(UUID uuid, JobStatusDto jobDto, String type); void saveFail5k(UUID uuid, JobStatusDto jobDto, String type);
/**
* 추론 실행중인 Job id 저장
*
* @param uuid 추론 uuid
* @param jobDto AI API에서 조회한 Job 정보
* @param type 모델 타입
*/
void saveJobId(UUID uuid, JobStatusDto jobDto, String type); void saveJobId(UUID uuid, JobStatusDto jobDto, String type);
/**
* 실패 처리되어야 할 목록 중에 아직 실패로 표시되지 않은 ID 조회
*
* @param uuid 추론 uuid
* @param failIds AI API 연결하여 조회한 실패 job id
* @param type 모델 타입
* @return 실패로 표시되지 않은 ID
*/
List<Long> findFail5kList(UUID uuid, List<Long> failIds, String type); List<Long> findFail5kList(UUID uuid, List<Long> failIds, String type);
/**
* 완료된 것으로 들어온 목록 중 실제로 존재하는 5k jobId 조회
*
* @param uuid 추론 uuid
* @param completedIds AI API 연결하여 조회한 실패 job id
* @param type 모델 타입
* @return 성공한 job id
*/
List<Long> findCompleted5kList(UUID uuid, List<Long> completedIds, String type); List<Long> findCompleted5kList(UUID uuid, List<Long> completedIds, String type);
/**
* 추론 실행중일때 분석중인 도엽명 목록 조회
*
* @param uuid 추론 uuid
* @return 도엽명+50K 도엽번호
*/
List<String> getInferenceRunMapId(UUID uuid); List<String> getInferenceRunMapId(UUID uuid);
} }

View File

@@ -18,29 +18,99 @@ import org.springframework.data.domain.Page;
public interface MapSheetLearnRepositoryCustom { public interface MapSheetLearnRepositoryCustom {
/**
* 추론 관리 목록 조회
*
* @param req 추론관리 목록 화면 조회 조건
* @return 추론 관리 목록
*/
Page<MapSheetLearnEntity> getInferenceMgnResultList(InferenceResultDto.SearchListReq req); Page<MapSheetLearnEntity> getInferenceMgnResultList(InferenceResultDto.SearchListReq req);
/**
* uuid 조건으로 추론 실행 정보 조회
*
* @param uuid uuid
* @return 추론 실행 정보
*/
Optional<MapSheetLearnEntity> getInferenceResultByUuid(UUID uuid); Optional<MapSheetLearnEntity> getInferenceResultByUuid(UUID uuid);
/**
* 추론 실행중 서버정보 조회 cpu, gpu
*
* @return cpu, gpu 정보
*/
List<InferenceServerStatusDto> getInferenceServerStatusList(); List<InferenceServerStatusDto> getInferenceServerStatusList();
/**
* 추론 실행 목록 진행 상태별 조회
*
* @param status 추론 진행 상태
* @return 추론 실행 정보
*/
Optional<MapSheetLearnEntity> getInferenceResultByStatus(String status); Optional<MapSheetLearnEntity> getInferenceResultByStatus(String status);
/**
* 등록된 추론 실행목록 및 등록된 모델 정보 조회
*
* @param id 추론 실행 테이블 id
* @param modelUuid 모델 uuid
* @return 모델 정보
*/
InferenceProgressDto getInferenceAiResultById(Long id, UUID modelUuid); InferenceProgressDto getInferenceAiResultById(Long id, UUID modelUuid);
/**
* 진행중인 추론 정보 상세 조회
*
* @param uuid 추론진행 uuid
* @return 진행중인 추론정보 상세 정보
*/
InferenceStatusDetailDto getInferenceStatus(UUID uuid); InferenceStatusDetailDto getInferenceStatus(UUID uuid);
/**
* 진행중인 추론이 있는지 조회
*
* @return 진행중인 추론 정보
*/
MapSheetLearnEntity getProcessing(); MapSheetLearnEntity getProcessing();
Integer getLearnStage(Integer compareYear, Integer targetYear); /**
* 추론 결과 정보 조회
*
* @param uuid 추론 uuid
* @return 추론 결과 및 사용 모델 정보
*/
AnalResultInfo getInferenceResultInfo(UUID uuid); AnalResultInfo getInferenceResultInfo(UUID uuid);
/**
* 추론 결과 bbox, point 조회
*
* @param uuid 추론 uuid
* @return bbox, pont 정보
*/
BboxPointDto getBboxPoint(UUID uuid); BboxPointDto getBboxPoint(UUID uuid);
/**
* 분류별 탐지건수 조회
*
* @param uuid 추론 uuid
* @return 분류별 탐지건수 정보
*/
List<Dashboard> getInferenceClassCountList(UUID uuid); List<Dashboard> getInferenceClassCountList(UUID uuid);
/**
* 추론 결과 상세 geom 목록 조회
*
* @param uuid 추론 uuid
* @param searchGeoReq 추론 결과 상세화면 geom 조회 조건
* @return geom 목록 정보
*/
Page<Geom> getInferenceGeomList(UUID uuid, SearchGeoReq searchGeoReq); Page<Geom> getInferenceGeomList(UUID uuid, SearchGeoReq searchGeoReq);
/**
* 국유in연동 가능여부 확인 조회
*
* @param uuid 추론 uuid
* @return 추론 존재여부, 부분도엽 여부, 추론 진행중 여부, 국유인 작업 진행중 여부
*/
GukYuinLinkFacts findLinkFacts(UUID uuid); GukYuinLinkFacts findLinkFacts(UUID uuid);
} }

View File

@@ -291,24 +291,6 @@ public class MapSheetLearnRepositoryImpl implements MapSheetLearnRepositoryCusto
.fetchOne(); .fetchOne();
} }
@Override
public Integer getLearnStage(Integer compareYear, Integer targetYear) {
Integer stage =
queryFactory
.select(mapSheetLearnEntity.stage)
.from(mapSheetLearnEntity)
.where(
mapSheetLearnEntity
.compareYyyy
.eq(compareYear)
.and(mapSheetLearnEntity.targetYyyy.eq(targetYear)))
.orderBy(mapSheetLearnEntity.id.desc())
.limit(1)
.fetchOne();
return stage == null ? 1 : stage + 1;
}
@Override @Override
public AnalResultInfo getInferenceResultInfo(UUID uuid) { public AnalResultInfo getInferenceResultInfo(UUID uuid) {
QModelMngEntity m1 = new QModelMngEntity("m1"); QModelMngEntity m1 = new QModelMngEntity("m1");
@@ -528,6 +510,7 @@ public class MapSheetLearnRepositoryImpl implements MapSheetLearnRepositoryCusto
@Override @Override
public GukYuinLinkFacts findLinkFacts(UUID uuid) { public GukYuinLinkFacts findLinkFacts(UUID uuid) {
// 해당 추론 있는지 확인
MapSheetLearnEntity learn = MapSheetLearnEntity learn =
queryFactory queryFactory
.selectFrom(QMapSheetLearnEntity.mapSheetLearnEntity) .selectFrom(QMapSheetLearnEntity.mapSheetLearnEntity)
@@ -538,12 +521,14 @@ public class MapSheetLearnRepositoryImpl implements MapSheetLearnRepositoryCusto
return new GukYuinLinkFacts(false, false, false, false); return new GukYuinLinkFacts(false, false, false, false);
} }
// 부분 도엽 실행인지 확인
boolean isPartScope = MapSheetScope.PART.getId().equals(learn.getMapSheetScope()); boolean isPartScope = MapSheetScope.PART.getId().equals(learn.getMapSheetScope());
QMapSheetAnalInferenceEntity inf = QMapSheetAnalInferenceEntity.mapSheetAnalInferenceEntity; QMapSheetAnalInferenceEntity inf = QMapSheetAnalInferenceEntity.mapSheetAnalInferenceEntity;
QMapSheetLearnEntity learn2 = new QMapSheetLearnEntity("learn2"); QMapSheetLearnEntity learn2 = new QMapSheetLearnEntity("learn2");
QMapSheetLearnEntity learnQ = QMapSheetLearnEntity.mapSheetLearnEntity; QMapSheetLearnEntity learnQ = QMapSheetLearnEntity.mapSheetLearnEntity;
// 실행중인 추론 있는지 확인
boolean hasRunningInference = boolean hasRunningInference =
queryFactory queryFactory
.selectOne() .selectOne()
@@ -557,6 +542,7 @@ public class MapSheetLearnRepositoryImpl implements MapSheetLearnRepositoryCusto
.fetchFirst() .fetchFirst()
!= null; != null;
// 국유인 작업 진행중 있는지 확인
boolean hasOtherUnfinishedGukYuin = boolean hasOtherUnfinishedGukYuin =
queryFactory queryFactory
.selectOne() .selectOne()

View File

@@ -341,7 +341,10 @@ public class ChangeDetectionRepositoryImpl extends QuerydslRepositorySupport
mapInkx5kEntity.mapidcdNo, mapInkx5kEntity.mapidcdNo,
mapInkx5kEntity.mapidNm, mapInkx5kEntity.mapidNm,
Expressions.stringTemplate( Expressions.stringTemplate(
"concat({0}, ' ', {1})", mapInkx5kEntity.mapidNm, mapInkx5kEntity.mapidcdNo))) "concat({0}, ' ', {1})", mapInkx5kEntity.mapidNm, mapInkx5kEntity.mapidcdNo),
Expressions.stringTemplate(
"ST_AsGeoJSON(ST_Transform({0}, 5186))", mapInkx5kEntity.geom)
.as("bbox")))
.from(mapSheetAnalInferenceEntity) .from(mapSheetAnalInferenceEntity)
.innerJoin(mapSheetAnalDataInferenceEntity) .innerJoin(mapSheetAnalDataInferenceEntity)
.on(mapSheetAnalInferenceEntity.id.eq(mapSheetAnalDataInferenceEntity.analUid)) .on(mapSheetAnalInferenceEntity.id.eq(mapSheetAnalDataInferenceEntity.analUid))
@@ -361,7 +364,10 @@ public class ChangeDetectionRepositoryImpl extends QuerydslRepositorySupport
mapInkx50kEntity.mapidcdNo, mapInkx50kEntity.mapidcdNo,
mapInkx50kEntity.mapidNm, mapInkx50kEntity.mapidNm,
Expressions.stringTemplate( Expressions.stringTemplate(
"concat({0}, ' ', {1})", mapInkx50kEntity.mapidNm, mapInkx50kEntity.mapidcdNo))) "concat({0}, ' ', {1})", mapInkx50kEntity.mapidNm, mapInkx50kEntity.mapidcdNo),
Expressions.stringTemplate(
"ST_AsGeoJSON(ST_Transform({0}, 5186))", mapInkx50kEntity.geom)
.as("bbox")))
.from(mapSheetAnalInferenceEntity) .from(mapSheetAnalInferenceEntity)
.innerJoin(mapSheetAnalDataInferenceEntity) .innerJoin(mapSheetAnalDataInferenceEntity)
.on(mapSheetAnalInferenceEntity.id.eq(mapSheetAnalDataInferenceEntity.analUid)) .on(mapSheetAnalInferenceEntity.id.eq(mapSheetAnalDataInferenceEntity.analUid))
@@ -369,7 +375,7 @@ public class ChangeDetectionRepositoryImpl extends QuerydslRepositorySupport
.on(mapSheetAnalDataInferenceEntity.mapSheetNum.stringValue().eq(mapInkx5kEntity.mapidcdNo)) .on(mapSheetAnalDataInferenceEntity.mapSheetNum.stringValue().eq(mapInkx5kEntity.mapidcdNo))
.innerJoin(mapInkx5kEntity.mapInkx50k, mapInkx50kEntity) .innerJoin(mapInkx5kEntity.mapInkx50k, mapInkx50kEntity)
.where(mapSheetAnalInferenceEntity.uuid.eq(uuid)) .where(mapSheetAnalInferenceEntity.uuid.eq(uuid))
.groupBy(mapInkx50kEntity.mapidcdNo, mapInkx50kEntity.mapidNm) .groupBy(mapInkx50kEntity.mapidcdNo, mapInkx50kEntity.mapidNm, mapInkx50kEntity.geom)
.orderBy(mapInkx50kEntity.mapidcdNo.asc()) .orderBy(mapInkx50kEntity.mapidcdNo.asc())
.fetch(); .fetch();
} }

View File

@@ -45,4 +45,6 @@ public interface GukYuinRepositoryCustom {
void updateMapSheetLearnGukyuinEndStatus(Long learnId); void updateMapSheetLearnGukyuinEndStatus(Long learnId);
void updateMapSheetInferenceLabelEndStatus(Long learnId); void updateMapSheetInferenceLabelEndStatus(Long learnId);
List<String> findStbltObjectIds(String uid, String mapSheetNum);
} }

View File

@@ -1,6 +1,7 @@
package com.kamco.cd.kamcoback.postgres.repository.gukyuin; package com.kamco.cd.kamcoback.postgres.repository.gukyuin;
import static com.kamco.cd.kamcoback.postgres.entity.QLabelingAssignmentEntity.labelingAssignmentEntity; import static com.kamco.cd.kamcoback.postgres.entity.QLabelingAssignmentEntity.labelingAssignmentEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataInferenceEntity.mapSheetAnalDataInferenceEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataInferenceGeomEntity.mapSheetAnalDataInferenceGeomEntity; import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataInferenceGeomEntity.mapSheetAnalDataInferenceGeomEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalInferenceEntity.mapSheetAnalInferenceEntity; import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalInferenceEntity.mapSheetAnalInferenceEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetLearnEntity.mapSheetLearnEntity; import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetLearnEntity.mapSheetLearnEntity;
@@ -318,6 +319,23 @@ public class GukYuinRepositoryImpl implements GukYuinRepositoryCustom {
.execute(); .execute();
} }
@Override
public List<String> findStbltObjectIds(String uid, String mapSheetNum) {
return queryFactory
.select(mapSheetAnalDataInferenceGeomEntity.resultUid)
.from(mapSheetLearnEntity)
.innerJoin(mapSheetAnalInferenceEntity)
.on(mapSheetLearnEntity.id.eq(mapSheetAnalInferenceEntity.learnId))
.innerJoin(mapSheetAnalDataInferenceEntity)
.on(mapSheetAnalInferenceEntity.id.eq(mapSheetAnalDataInferenceEntity.analUid))
.innerJoin(mapSheetAnalDataInferenceGeomEntity)
.on(mapSheetAnalDataInferenceEntity.id.eq(mapSheetAnalDataInferenceGeomEntity.dataUid))
.where(
mapSheetLearnEntity.uid.eq(uid),
mapSheetAnalDataInferenceGeomEntity.mapSheetNum.eq(Long.valueOf(mapSheetNum)))
.fetch();
}
@Override @Override
@Transactional @Transactional
public void updateGukYuinApplyStateComplete(Long id, GukYuinStatus status) { public void updateGukYuinApplyStateComplete(Long id, GukYuinStatus status) {

View File

@@ -4,6 +4,7 @@ import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetLearnEntity.mapShe
import com.kamco.cd.kamcoback.common.enums.ImageryFitStatus; import com.kamco.cd.kamcoback.common.enums.ImageryFitStatus;
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InspectState; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InspectState;
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelMngState;
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelState; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelState;
import com.kamco.cd.kamcoback.label.dto.LabelWorkDto; import com.kamco.cd.kamcoback.label.dto.LabelWorkDto;
import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.LabelWorkMng; import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.LabelWorkMng;
@@ -166,6 +167,19 @@ public class LabelWorkRepositoryImpl implements LabelWorkRepositoryCustom {
.from(labelingAssignmentEntity) .from(labelingAssignmentEntity)
.where(labelingAssignmentEntity.analUid.eq(mapSheetAnalInferenceEntity.id)); .where(labelingAssignmentEntity.analUid.eq(mapSheetAnalInferenceEntity.id));
/** 순서 ING 진행중 ASSIGNED 작업할당 PENDING 작업대기 FINISH 종료 */
NumberExpression<Integer> stateOrder =
new CaseBuilder()
.when(mapSheetAnalInferenceEntity.analState.eq(LabelMngState.ING.getId()))
.then(0)
.when(mapSheetAnalInferenceEntity.analState.eq(LabelMngState.ASSIGNED.getId()))
.then(1)
.when(mapSheetAnalInferenceEntity.analState.eq(LabelMngState.PENDING.getId()))
.then(2)
.when(mapSheetAnalInferenceEntity.analState.eq(LabelMngState.FINISH.getId()))
.then(3)
.otherwise(99);
List<LabelWorkMng> foundContent = List<LabelWorkMng> foundContent =
queryFactory queryFactory
.select( .select(
@@ -251,6 +265,7 @@ public class LabelWorkRepositoryImpl implements LabelWorkRepositoryCustom {
mapSheetLearnEntity.uid, mapSheetLearnEntity.uid,
mapSheetLearnEntity.uuid) mapSheetLearnEntity.uuid)
.orderBy( .orderBy(
stateOrder.asc(),
mapSheetAnalInferenceEntity.targetYyyy.desc(), mapSheetAnalInferenceEntity.targetYyyy.desc(),
mapSheetAnalInferenceEntity.compareYyyy.desc(), mapSheetAnalInferenceEntity.compareYyyy.desc(),
mapSheetAnalInferenceEntity.stage.desc()) mapSheetAnalInferenceEntity.stage.desc())

View File

@@ -1,7 +1,6 @@
package com.kamco.cd.kamcoback.postgres.repository.mapsheet; package com.kamco.cd.kamcoback.postgres.repository.mapsheet;
import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.ImageFeature; import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.ImageFeature;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.AddReq; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.AddReq;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngListDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngListDto;
@@ -16,16 +15,39 @@ import org.springframework.data.domain.Page;
public interface MapSheetMngRepositoryCustom { public interface MapSheetMngRepositoryCustom {
/**
* 영상데이터관리 > 목록 조회
*
* @return
*/
List<MapSheetMngDto.MngDto> findMapSheetMngList(); List<MapSheetMngDto.MngDto> findMapSheetMngList();
/**
* 영상데이터관리 > 데이터 등록 > 연도 선택 목록
*
* @return
*/
List<Integer> findMapSheetMngYyyyList(); List<Integer> findMapSheetMngYyyyList();
/**
* 영상데이터관리 > 상세 조회
*
* @param mngYyyy
* @return
*/
MapSheetMngDto.MngDto findMapSheetMng(int mngYyyy); MapSheetMngDto.MngDto findMapSheetMng(int mngYyyy);
void MapSheetMngComplete(int mngYyyy); void MapSheetMngComplete(int mngYyyy);
Optional<MapSheetMngHstEntity> findMapSheetMngHstInfo(Long hstUid); Optional<MapSheetMngHstEntity> findMapSheetMngHstInfo(Long hstUid);
/**
* 5k 도엽 기준으로 tb_map_sheet_hst 테이블에 먼저 insert 하기
*
* @param mngYyyy
* @param mngPath
* @return
*/
int insertMapSheetOrgDataToMapSheetMngHst(int mngYyyy, String mngPath); int insertMapSheetOrgDataToMapSheetMngHst(int mngYyyy, String mngPath);
List<MapSheetMngDto.MngFilesDto> findHstUidToMapSheetFileList(Long hstUid); List<MapSheetMngDto.MngFilesDto> findHstUidToMapSheetFileList(Long hstUid);
@@ -34,6 +56,11 @@ public interface MapSheetMngRepositoryCustom {
MapSheetMngDto.MngFilesDto findYyyyToMapSheetFilePathRefer(int mngYyyy); MapSheetMngDto.MngFilesDto findYyyyToMapSheetFilePathRefer(int mngYyyy);
/**
* 같은 년도로 저장된 데이터가 있다면 삭제
*
* @param mngYyyy
*/
void deleteByMngYyyyMngAll(int mngYyyy); void deleteByMngYyyyMngAll(int mngYyyy);
void deleteByMngYyyyMng(int mngYyyy); void deleteByMngYyyyMng(int mngYyyy);
@@ -54,19 +81,39 @@ public interface MapSheetMngRepositoryCustom {
void deleteByNotInFileUidMngFile(Long hstUid, List<Long> fileUids); void deleteByNotInFileUidMngFile(Long hstUid, List<Long> fileUids);
/**
* tb_year 에 해당 년도 완료로 업데이트
*
* @param yyyy
* @param status
*/
void updateYearState(int yyyy, String status); void updateYearState(int yyyy, String status);
/**
* 영상데이터관리 > 상세 > 오류 처리 내역
*
* @param searchReq 오류 검색 조건
* @return
*/
Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList( Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList(
MapSheetMngDto.@Valid ErrorSearchReq searchReq); MapSheetMngDto.@Valid ErrorSearchReq searchReq);
/**
* hst 테이블에 선택한 hstUid row 정보 조회
*
* @param hstUid
* @return
*/
MapSheetMngDto.ErrorDataDto findMapSheetError(Long hstUid); MapSheetMngDto.ErrorDataDto findMapSheetError(Long hstUid);
/**
* 파일 목록으로 업로드 경로 확인
*
* @param hstUid
* @return
*/
List<MapSheetMngDto.MngFilesDto> findByHstUidMapSheetFileList(Long hstUid); List<MapSheetMngDto.MngFilesDto> findByHstUidMapSheetFileList(Long hstUid);
List<MngListDto> findByHstMapSheetTargetList(InferenceResultDto.RegReq req);
List<MngListDto> findByHstMapSheetTargetList(int mngYyyy, List<String> mapIds);
MapSheetMngDto.MngFilesDto findByFileUidMapSheetFile(Long fileUid); MapSheetMngDto.MngFilesDto findByFileUidMapSheetFile(Long fileUid);
void updateHstFileSizes(Long hstUid, long tifSizeBytes, long tfwSizeBytes, long totalSizeBytes); void updateHstFileSizes(Long hstUid, long tifSizeBytes, long tfwSizeBytes, long totalSizeBytes);
@@ -75,11 +122,38 @@ public interface MapSheetMngRepositoryCustom {
Page<YearEntity> getYears(YearSearchReq req); Page<YearEntity> getYears(YearSearchReq req);
/**
* 도엽 영상파일 정보 조회
*
* @param yyyy 연도
* @param mapSheetNums 도엽목록
* @return 도엽 영상파일 정보
*/
List<ImageFeature> getSceneInference(String yyyy, List<String> mapSheetNums); List<ImageFeature> getSceneInference(String yyyy, List<String> mapSheetNums);
void updateMapSheetMngHstUploadId(Long hstUid, UUID uuid, String uploadId); void updateMapSheetMngHstUploadId(Long hstUid, UUID uuid, String uploadId);
/**
* 년도별 Tile 정보 등록
*
* @param addReq
*/
void insertMapSheetMngTile(@Valid AddReq addReq); void insertMapSheetMngTile(@Valid AddReq addReq);
List<MapSheetMngHstEntity> getMapSheetMngHst(Integer year); /**
* 연도 조건으로 도엽번호 조회
*
* @param year 연도
* @return 추론 가능한 도엽 정보
*/
List<MngListDto> getMapSheetMngHst(Integer year, String mapSheetScope, List<String> mapSheetNum);
/**
* 비교연도 사용 가능한 이전도엽을 조회한다.
*
* @param year 연도
* @param mapIds 도엽목록
* @return 사용 가능한 이전도엽목록
*/
List<MngListDto> findFallbackCompareYearByMapSheets(Integer year, List<String> mapIds);
} }

View File

@@ -10,13 +10,15 @@ import static com.querydsl.core.types.dsl.Expressions.nullExpression;
import com.kamco.cd.kamcoback.common.enums.CommonUseStatus; import com.kamco.cd.kamcoback.common.enums.CommonUseStatus;
import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.ImageFeature; import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.ImageFeature;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetScope; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetScope;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.AddReq; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.AddReq;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngListDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngListDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.YearSearchReq; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.YearSearchReq;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetMngHstEntity; import com.kamco.cd.kamcoback.postgres.entity.MapSheetMngHstEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMapInkx5kEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngFileEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngHstEntity;
import com.kamco.cd.kamcoback.postgres.entity.QYearEntity; import com.kamco.cd.kamcoback.postgres.entity.QYearEntity;
import com.kamco.cd.kamcoback.postgres.entity.YearEntity; import com.kamco.cd.kamcoback.postgres.entity.YearEntity;
import com.querydsl.core.BooleanBuilder; import com.querydsl.core.BooleanBuilder;
@@ -26,6 +28,7 @@ import com.querydsl.core.types.dsl.CaseBuilder;
import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.core.types.dsl.StringExpression; import com.querydsl.core.types.dsl.StringExpression;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory; import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
@@ -56,16 +59,17 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
this.queryFactory = queryFactory; this.queryFactory = queryFactory;
} }
/**
* 영상데이터관리 > 목록 조회
*
* @return
*/
@Override @Override
public List<MapSheetMngDto.MngDto> findMapSheetMngList() { public List<MapSheetMngDto.MngDto> findMapSheetMngList() {
// Pageable pageable = searchReq.toPageable(); // Pageable pageable = searchReq.toPageable();
BooleanBuilder whereBuilder = new BooleanBuilder(); BooleanBuilder whereBuilder = new BooleanBuilder();
// if (searchReq.getMngYyyy() != null) {
// whereBuilder.and(mapSheetMngEntity.mngYyyy.eq(searchReq.getMngYyyy()));
// }
NumberExpression<Long> totalCount = mapSheetMngHstEntity.count().as("syncTotCnt"); NumberExpression<Long> totalCount = mapSheetMngHstEntity.count().as("syncTotCnt");
NumberExpression<Long> doneCount = NumberExpression<Long> doneCount =
@@ -224,6 +228,11 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
return foundContent; return foundContent;
} }
/**
* 영상데이터관리 > 데이터 등록 > 연도 선택 목록
*
* @return List<Integer>
*/
public List<Integer> findMapSheetMngYyyyList() { public List<Integer> findMapSheetMngYyyyList() {
List<Integer> foundContent = List<Integer> foundContent =
@@ -242,6 +251,12 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
return foundContent; return foundContent;
} }
/**
* tb_year 에 해당 년도 완료로 업데이트
*
* @param yyyy
* @param status
*/
public void updateYearState(int yyyy, String status) { public void updateYearState(int yyyy, String status) {
long execCount = long execCount =
queryFactory queryFactory
@@ -251,6 +266,12 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
.execute(); .execute();
} }
/**
* 영상데이터관리 > 상세 조회
*
* @param mngYyyy
* @return
*/
public MapSheetMngDto.MngDto findMapSheetMng(int mngYyyy) { public MapSheetMngDto.MngDto findMapSheetMng(int mngYyyy) {
BooleanBuilder whereBuilder = new BooleanBuilder(); BooleanBuilder whereBuilder = new BooleanBuilder();
@@ -396,6 +417,12 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
return foundContent; return foundContent;
} }
/**
* 영상데이터관리 > 상세 > 오류 처리 내역
*
* @param searchReq 오류 검색 조건
* @return
*/
@Override @Override
public Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList( public Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList(
MapSheetMngDto.@Valid ErrorSearchReq searchReq) { MapSheetMngDto.@Valid ErrorSearchReq searchReq) {
@@ -404,6 +431,10 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
BooleanBuilder whereBuilder = new BooleanBuilder(); BooleanBuilder whereBuilder = new BooleanBuilder();
whereBuilder.and(mapSheetMngHstEntity.mngYyyy.eq(searchReq.getMngYyyy())); whereBuilder.and(mapSheetMngHstEntity.mngYyyy.eq(searchReq.getMngYyyy()));
// syncState : 동기화 상태
// hst 테이블의 syncState 가 DONE, NOTYET 이 아닌 것 -> 오류
// (오류인 상태 SyncStateType : DUPLICATE, NOFILE, NOTPAIR, TYPEERROR)
whereBuilder.and( whereBuilder.and(
mapSheetMngHstEntity.syncState.ne("DONE").and(mapSheetMngHstEntity.syncState.ne("NOTYET"))); mapSheetMngHstEntity.syncState.ne("DONE").and(mapSheetMngHstEntity.syncState.ne("NOTYET")));
@@ -411,10 +442,12 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
whereBuilder.and(mapSheetMngHstEntity.syncState.eq(searchReq.getSyncState())); whereBuilder.and(mapSheetMngHstEntity.syncState.eq(searchReq.getSyncState()));
} }
// syncCheckState : 동기화 이후, 에러 처리 상태 - 처리 DONE / 미처리 NOTYET
if (searchReq.getSyncCheckState() != null && !searchReq.getSyncCheckState().isEmpty()) { if (searchReq.getSyncCheckState() != null && !searchReq.getSyncCheckState().isEmpty()) {
whereBuilder.and(mapSheetMngHstEntity.syncCheckState.eq(searchReq.getSyncCheckState())); whereBuilder.and(mapSheetMngHstEntity.syncCheckState.eq(searchReq.getSyncCheckState()));
} }
// 도엽 검색 조건 있을 때 whereBuilder에 추가
if (searchReq.getSearchValue() != null && !searchReq.getSearchValue().isEmpty()) { if (searchReq.getSearchValue() != null && !searchReq.getSearchValue().isEmpty()) {
whereBuilder.and( whereBuilder.and(
mapSheetMngHstEntity mapSheetMngHstEntity
@@ -472,7 +505,8 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
mapSheetMngHstEntity mapSheetMngHstEntity
.mapSheetNum .mapSheetNum
.eq(mapInkx5kEntity.mapidcdNo) .eq(mapInkx5kEntity.mapidcdNo)
.and(mapInkx5kEntity.useInference.eq(CommonUseStatus.USE))) .and(
mapInkx5kEntity.useInference.eq(CommonUseStatus.USE))) // 도엽 사용인 것만 오류 내역 표기
.where(whereBuilder) .where(whereBuilder)
.orderBy(mapSheetMngHstEntity.createdDate.desc()) .orderBy(mapSheetMngHstEntity.createdDate.desc())
.offset(pageable.getOffset()) .offset(pageable.getOffset())
@@ -488,13 +522,20 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
mapSheetMngHstEntity mapSheetMngHstEntity
.mapSheetNum .mapSheetNum
.eq(mapInkx5kEntity.mapidcdNo) .eq(mapInkx5kEntity.mapidcdNo)
.and(mapInkx5kEntity.useInference.eq(CommonUseStatus.USE))) .and(
mapInkx5kEntity.useInference.eq(CommonUseStatus.USE))) // 도엽 사용인 것만 오류 내역 표기
.where(whereBuilder) .where(whereBuilder)
.fetchOne(); .fetchOne();
return new PageImpl<>(foundContent, pageable, countQuery); return new PageImpl<>(foundContent, pageable, countQuery);
} }
/**
* hst 테이블에 선택한 hstUid row 정보 조회
*
* @param hstUid
* @return
*/
@Override @Override
public MapSheetMngDto.ErrorDataDto findMapSheetError(Long hstUid) { public MapSheetMngDto.ErrorDataDto findMapSheetError(Long hstUid) {
@@ -570,87 +611,6 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
return foundContent; return foundContent;
} }
/**
* 기준년도 추론 실행 가능 도엽 조회
*
* @param req
* @return
*/
@Override
public List<MngListDto> findByHstMapSheetTargetList(InferenceResultDto.RegReq req) {
BooleanBuilder whereBuilder = new BooleanBuilder();
whereBuilder.and(mapSheetMngHstEntity.dataState.eq("DONE"));
whereBuilder.and(
mapSheetMngHstEntity
.syncState
.eq("DONE")
.or(mapSheetMngHstEntity.syncCheckState.eq("DONE")));
whereBuilder.and(mapSheetMngHstEntity.useInference.eq("USE"));
whereBuilder.and(mapSheetMngHstEntity.mngYyyy.eq(req.getTargetYyyy()));
BooleanBuilder likeBuilder = new BooleanBuilder();
if (MapSheetScope.PART.getId().equals(req.getMapSheetScope())) {
List<String> list = req.getMapSheetNum();
if (list == null || list.isEmpty()) {
return List.of();
}
for (String prefix : list) {
if (prefix == null || prefix.isBlank()) {
continue;
}
likeBuilder.or(mapSheetMngHstEntity.mapSheetNum.like(prefix.trim() + "%"));
}
}
if (likeBuilder.hasValue()) {
whereBuilder.and(likeBuilder);
}
return queryFactory
.select(
Projections.constructor(
MngListDto.class,
mapSheetMngHstEntity.mngYyyy,
mapSheetMngHstEntity.mapSheetNum,
mapSheetMngHstEntity.mapSheetName,
nullExpression(Integer.class),
nullExpression(Boolean.class)))
.from(mapSheetMngHstEntity)
.innerJoin(mapInkx5kEntity)
.on(
mapInkx5kEntity
.mapidcdNo
.eq(mapSheetMngHstEntity.mapSheetNum)
.and(mapInkx5kEntity.useInference.eq(CommonUseStatus.USE)))
.where(whereBuilder)
.fetch();
}
@Override
public List<MngListDto> findByHstMapSheetTargetList(int mngYyyy, List<String> mapIds) {
BooleanBuilder whereBuilder = new BooleanBuilder();
whereBuilder.and(mapSheetMngHstEntity.mngYyyy.eq(mngYyyy));
whereBuilder.and(mapSheetMngHstEntity.mapSheetNum.in(mapIds));
whereBuilder.and(mapSheetMngHstEntity.dataState.eq("DONE"));
whereBuilder.and(mapSheetMngHstEntity.syncState.eq("DONE")); // TODO 싱크체크 or조건 추가
whereBuilder.and(mapSheetMngHstEntity.useInference.eq("USE"));
return queryFactory
.select(
Projections.constructor(
MngListDto.class,
mapSheetMngHstEntity.mngYyyy,
mapSheetMngHstEntity.mapSheetNum,
mapSheetMngHstEntity.mapSheetName))
.from(mapSheetMngHstEntity)
.where(whereBuilder)
.fetch();
}
@Override @Override
public List<MapSheetMngDto.MngFilesDto> findHstUidToMapSheetFileList(Long hstUid) { public List<MapSheetMngDto.MngFilesDto> findHstUidToMapSheetFileList(Long hstUid) {
BooleanBuilder whereBuilder = new BooleanBuilder(); BooleanBuilder whereBuilder = new BooleanBuilder();
@@ -758,6 +718,11 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
.execute(); .execute();
} }
/**
* 같은 년도로 저장된 데이터가 있다면 삭제
*
* @param mngYyyy
*/
@Override @Override
public void deleteByMngYyyyMngAll(int mngYyyy) { public void deleteByMngYyyyMngAll(int mngYyyy) {
@@ -778,6 +743,12 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
.delete(mapSheetMngEntity) .delete(mapSheetMngEntity)
.where(mapSheetMngEntity.mngYyyy.eq(mngYyyy)) .where(mapSheetMngEntity.mngYyyy.eq(mngYyyy))
.execute(); .execute();
long deletedTileCount =
queryFactory
.delete(mapSheetMngTileEntity)
.where(mapSheetMngTileEntity.mngYyyy.eq(mngYyyy))
.execute();
} }
@Override @Override
@@ -908,6 +879,13 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
.execute(); .execute();
} }
/**
* 5k 도엽 기준으로 tb_map_sheet_hst 테이블에 먼저 insert 하기
*
* @param mngYyyy
* @param mngPath
* @return
*/
@Override @Override
public int insertMapSheetOrgDataToMapSheetMngHst(int mngYyyy, String mngPath) { public int insertMapSheetOrgDataToMapSheetMngHst(int mngYyyy, String mngPath) {
@@ -1044,6 +1022,11 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
.execute(); .execute();
} }
/**
* 년도별 Tile 정보 등록
*
* @param addReq
*/
@Override @Override
public void insertMapSheetMngTile(AddReq addReq) { public void insertMapSheetMngTile(AddReq addReq) {
long execute = long execute =
@@ -1101,22 +1084,119 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
} }
@Override @Override
public List<MapSheetMngHstEntity> getMapSheetMngHst(Integer year) { public List<MngListDto> getMapSheetMngHst(
return queryFactory Integer year, String mapSheetScope, List<String> mapSheetNum) {
.select(mapSheetMngHstEntity) BooleanBuilder whereBuilder = new BooleanBuilder();
.from(mapSheetMngHstEntity)
.innerJoin(mapSheetMngFileEntity) whereBuilder.and(mapSheetMngHstEntity.dataState.eq("DONE"));
.on(mapSheetMngFileEntity.hstUid.eq(mapSheetMngHstEntity.hstUid)) whereBuilder.and(
.where(
mapSheetMngHstEntity
.mngYyyy
.eq(year)
.and(
mapSheetMngHstEntity mapSheetMngHstEntity
.syncState .syncState
.eq("DONE") .eq("DONE")
.or(mapSheetMngHstEntity.syncCheckState.eq("DONE"))) .or(mapSheetMngHstEntity.syncCheckState.eq("DONE")));
.and(mapSheetMngFileEntity.fileExt.eq("tif"))) whereBuilder.and(mapSheetMngHstEntity.useInference.eq("USE"));
whereBuilder.and(mapSheetMngHstEntity.mngYyyy.eq(year));
whereBuilder.and(
JPAExpressions.selectOne()
.from(mapSheetMngFileEntity)
.where(
mapSheetMngFileEntity
.hstUid
.eq(mapSheetMngHstEntity.hstUid) // FK 관계에 맞게 유지
.and(mapSheetMngFileEntity.fileExt.eq("tif"))
.and(mapSheetMngFileEntity.fileState.eq("DONE")))
.exists());
BooleanBuilder likeBuilder = new BooleanBuilder();
if (MapSheetScope.PART.getId().equals(mapSheetScope)) {
List<String> list = mapSheetNum;
if (list == null || list.isEmpty()) {
return List.of();
}
for (String prefix : list) {
if (prefix == null || prefix.isBlank()) {
continue;
}
likeBuilder.or(mapSheetMngHstEntity.mapSheetNum.like(prefix.trim() + "%"));
}
}
if (likeBuilder.hasValue()) {
whereBuilder.and(likeBuilder);
}
return queryFactory
.select(
Projections.constructor(
MngListDto.class,
mapSheetMngHstEntity.mngYyyy,
mapSheetMngHstEntity.mapSheetNum,
mapSheetMngHstEntity.mapSheetName,
nullExpression(Integer.class),
nullExpression(Boolean.class)))
.from(mapSheetMngHstEntity)
.innerJoin(mapInkx5kEntity)
.on(
mapInkx5kEntity
.mapidcdNo
.eq(mapSheetMngHstEntity.mapSheetNum)
.and(mapInkx5kEntity.useInference.eq(CommonUseStatus.USE)))
.where(whereBuilder)
.fetch();
}
@Override
public List<MngListDto> findFallbackCompareYearByMapSheets(Integer year, List<String> mapIds) {
QMapSheetMngHstEntity h = QMapSheetMngHstEntity.mapSheetMngHstEntity;
QMapSheetMngHstEntity h2 = new QMapSheetMngHstEntity("h2");
QMapSheetMngFileEntity f = QMapSheetMngFileEntity.mapSheetMngFileEntity;
QMapSheetMngFileEntity f2 = new QMapSheetMngFileEntity("f2");
QMapInkx5kEntity inkk = QMapInkx5kEntity.mapInkx5kEntity;
return queryFactory
.select(
Projections.constructor(
MngListDto.class,
h.mngYyyy,
h.mapSheetNum,
h.mapSheetName,
nullExpression(Integer.class),
nullExpression(Boolean.class)))
.from(h)
.innerJoin(inkk)
.on(inkk.mapidcdNo.eq(h.mapSheetNum).and(inkk.useInference.eq(CommonUseStatus.USE)))
.where(
h.mapSheetNum.in(mapIds),
h.mngYyyy.lt(year),
h.dataState.eq("DONE"),
h.useInference.eq("USE"),
h.syncState.eq("DONE").or(h.syncCheckState.eq("DONE")),
JPAExpressions.selectOne()
.from(f)
.where(f.hstUid.eq(h.hstUid), f.fileExt.eq("tif"), f.fileState.eq("DONE"))
.exists(),
// mapSheetNum별 최대 mngYyyy인 행만 남김
h.mngYyyy.eq(
JPAExpressions.select(h2.mngYyyy.max())
.from(h2)
.where(
h2.mapSheetNum.eq(h.mapSheetNum),
h2.mngYyyy.lt(year),
h2.dataState.eq("DONE"),
h2.useInference.eq("USE"),
h2.syncState.eq("DONE").or(h2.syncCheckState.eq("DONE")),
JPAExpressions.selectOne()
.from(f2)
.where(
f2.hstUid.eq(h2.hstUid),
f2.fileExt.eq("tif"),
f2.fileState.eq("DONE"))
.exists())))
.fetch(); .fetch();
} }
} }

View File

@@ -13,6 +13,12 @@ public interface MembersRepositoryCustom {
boolean existsByEmployeeNo(String employeeNo); boolean existsByEmployeeNo(String employeeNo);
/**
* 사번으로 사용자 조회
*
* @param employeeNo 사번
* @return 사용자 정보 조회
*/
Optional<MemberEntity> findByEmployeeNo(String employeeNo); Optional<MemberEntity> findByEmployeeNo(String employeeNo);
Optional<MemberEntity> findByUserId(String userId); Optional<MemberEntity> findByUserId(String userId);

View File

@@ -9,6 +9,16 @@ import org.springframework.data.domain.Page;
public interface ModelMngRepositoryCustom { public interface ModelMngRepositoryCustom {
/**
* 모델조회
*
* @param searchReq 페이징
* @param startDate 시작날짜
* @param endDate 종료날짜
* @param modelType 모델 타입 G1, G2, G3
* @param searchVal 모델 ver
* @return 모델 목록
*/
Page<ModelMngDto.ModelList> findModelMgmtList( Page<ModelMngDto.ModelList> findModelMgmtList(
ModelMngDto.searchReq searchReq, ModelMngDto.searchReq searchReq,
LocalDate startDate, LocalDate startDate,
@@ -29,4 +39,12 @@ public interface ModelMngRepositoryCustom {
Optional<ModelMngEntity> findByModelId(Long id); Optional<ModelMngEntity> findByModelId(Long id);
Optional<ModelMngEntity> findByModelId(UUID id); Optional<ModelMngEntity> findByModelId(UUID id);
/**
* 버전명으로 모델 조회
*
* @param ver 모델버전
* @return 모델정보
*/
Optional<ModelMngEntity> findByModelVer(String ver);
} }

View File

@@ -246,4 +246,14 @@ public class ModelMngRepositoryImpl extends QuerydslRepositorySupport
return Optional.ofNullable( return Optional.ofNullable(
queryFactory.selectFrom(modelMngEntity).where(modelMngEntity.uuid.eq(uuid)).fetchOne()); queryFactory.selectFrom(modelMngEntity).where(modelMngEntity.uuid.eq(uuid)).fetchOne());
} }
@Override
public Optional<ModelMngEntity> findByModelVer(String ver) {
return Optional.ofNullable(
queryFactory
.selectFrom(modelMngEntity)
.where(modelMngEntity.modelVer.eq(ver))
.limit(1)
.fetchOne());
}
} }

View File

@@ -59,6 +59,8 @@ public class MapInkxMngApiController {
mapInkxMngService.findMapInkxMngList(searchReq, useInference, searchVal)); mapInkxMngService.findMapInkxMngList(searchReq, useInference, searchVal));
} }
// 미사용 : 기획에서 도엽정보 등록 로직 제거됨
@Hidden
@Operation(summary = "저장", description = "도엽정보를 저장 합니다.") @Operation(summary = "저장", description = "도엽정보를 저장 합니다.")
@ApiResponses( @ApiResponses(
value = { value = {

View File

@@ -2,6 +2,7 @@ package com.kamco.cd.kamcoback.scheduler;
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto; import com.kamco.cd.kamcoback.code.dto.CommonCodeDto;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto; import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.gukyuin.service.GukYuinApiService;
import com.kamco.cd.kamcoback.scheduler.service.GukYuinApiLabelJobService; import com.kamco.cd.kamcoback.scheduler.service.GukYuinApiLabelJobService;
import com.kamco.cd.kamcoback.scheduler.service.GukYuinApiPnuJobService; import com.kamco.cd.kamcoback.scheduler.service.GukYuinApiPnuJobService;
import com.kamco.cd.kamcoback.scheduler.service.GukYuinApiStatusJobService; import com.kamco.cd.kamcoback.scheduler.service.GukYuinApiStatusJobService;
@@ -16,9 +17,13 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -37,6 +42,7 @@ public class SchedulerApiController {
private final TrainingDataReviewJobService trainingDataReviewJobService; private final TrainingDataReviewJobService trainingDataReviewJobService;
private final MemberInactiveJobService memberInactiveJobService; private final MemberInactiveJobService memberInactiveJobService;
private final MapSheetMngFileJobController mapSheetMngFileJobController; private final MapSheetMngFileJobController mapSheetMngFileJobController;
private final GukYuinApiService gukYuinApiService;
@Operation(summary = "국유인 탐지객체 조회 PNU 업데이트 스케줄링", description = "국유인 탐지객체 조회 PNU 업데이트 스케줄링") @Operation(summary = "국유인 탐지객체 조회 PNU 업데이트 스케줄링", description = "국유인 탐지객체 조회 PNU 업데이트 스케줄링")
@GetMapping("/gukyuin/pnu") @GetMapping("/gukyuin/pnu")
@@ -60,7 +66,21 @@ public class SchedulerApiController {
return ApiResponseDto.ok(null); return ApiResponseDto.ok(null);
} }
@Operation(summary = "국유인 실태조사 적합여부 업데이트 스케줄링", description = "국유인 실태조사 적합여부 업데이트 스케줄링") @Operation(summary = "국유인 실태조사 적합여부 전송가능한 객체 ids", description = "국유인 실태조사 적합여부 전송가능한 객체 ids")
@GetMapping("/gukyuin/stblt-object-ids/{uid}/{mapSheetNum}")
public ApiResponseDto<List<String>> findStbltObjectIds(
@PathVariable String uid, @PathVariable String mapSheetNum) {
return ApiResponseDto.ok(gukYuinApiService.findStbltObjectIds(uid, mapSheetNum));
}
@Operation(summary = "국유인 라벨 완료 전송 스케줄링", description = "국유인 라벨 완료 전송 스케줄링")
@PostMapping("/gukyuin/stblt-update")
public ApiResponseDto<Void> stbltBulkUpdate(@RequestBody List<String> objectIds) {
gukYuinApiService.stbltBulkUpdate(objectIds);
return ApiResponseDto.ok(null);
}
@Operation(summary = "국유인 실태조사 적합여부 조회 스케줄링", description = "국유인 실태조사 적합여부 조회 스케줄링")
@GetMapping("/gukyuin/stblt") @GetMapping("/gukyuin/stblt")
public ApiResponseDto<Void> findGukYuinEligibleForSurvey( public ApiResponseDto<Void> findGukYuinEligibleForSurvey(
@RequestParam(required = false) LocalDate baseDate) { @RequestParam(required = false) LocalDate baseDate) {

View File

@@ -7,19 +7,76 @@ import org.springframework.stereotype.Component;
@Component @Component
public class ShpKeyLock { public class ShpKeyLock {
/**
* key별 Lock 객체를 저장하는 맵
*
* <p>- key: 예) shp 파일 경로, uuid, 도엽번호 등 - value: 해당 key 전용 ReentrantLock
*
* <p>ConcurrentHashMap을 사용하여 멀티스레드 환경에서도 안전하게 접근 가능
*/
private final ConcurrentHashMap<String, ReentrantLock> locks = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, ReentrantLock> locks = new ConcurrentHashMap<>();
/**
* 특정 key에 대한 Lock 시도
*
* @param key 동시성 제어 대상 식별자
* @return true → 락 획득 성공 false → 이미 다른 스레드가 사용 중
*/
public boolean tryLock(String key) { public boolean tryLock(String key) {
/**
* computeIfAbsent 설명:
*
* <p>- 해당 key가 이미 존재하면 기존 Lock 반환 - 없으면 새 ReentrantLock 생성 후 저장
*
* <p>동시성 환경에서도 원자적으로 실행됨
*/
ReentrantLock lock = locks.computeIfAbsent(key, k -> new ReentrantLock()); ReentrantLock lock = locks.computeIfAbsent(key, k -> new ReentrantLock());
/**
* tryLock():
*
* <p>- 대기하지 않고 즉시 시도 - 이미 다른 스레드가 점유 중이면 false 반환
*
* <p>→ 다운로드 중복 방지에 적합
*/
return lock.tryLock(); return lock.tryLock();
} }
/**
* 특정 key에 대한 Lock 해제
*
* <p>반드시 tryLock 성공한 동일 스레드에서 호출해야 함
*/
public void unlock(String key) { public void unlock(String key) {
// 해당 key에 등록된 Lock 조회
ReentrantLock lock = locks.get(key); ReentrantLock lock = locks.get(key);
/**
* isHeldByCurrentThread():
*
* <p>- 현재 스레드가 해당 락을 보유 중인지 확인 - 다른 스레드가 unlock 호출하는 것 방지
*/
if (lock != null && lock.isHeldByCurrentThread()) { if (lock != null && lock.isHeldByCurrentThread()) {
// 실제 락 해제
lock.unlock(); lock.unlock();
// 메모리 누수 방지(락이 비어있으면 제거)
/**
* 메모리 누수 방지 처리
*
* <p>hasQueuedThreads(): - 현재 이 락을 기다리는 스레드가 있는지 확인
*
* <p>대기 스레드가 없다면 locks 맵에서 제거하여 불필요한 Lock 객체 정리
*/
if (!lock.hasQueuedThreads()) { if (!lock.hasQueuedThreads()) {
/**
* remove(key, lock):
*
* <p>- 현재 맵에 등록된 값이 'lock'일 경우에만 제거 - 동시성 안전 (CAS 방식)
*/
locks.remove(key, lock); locks.remove(key, lock);
} }
} }

View File

@@ -1,9 +1,8 @@
package com.kamco.cd.kamcoback.scheduler.service; package com.kamco.cd.kamcoback.scheduler.service;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.kamcoback.common.exception.CustomApiException; import com.kamco.cd.kamcoback.common.inference.service.InferenceCommonService;
import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient; import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient;
import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient.ExternalCallResult; import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient.ExternalCallResult;
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.InferenceBatchSheet; import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.InferenceBatchSheet;
@@ -19,7 +18,6 @@ import java.nio.file.Paths;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@@ -31,7 +29,6 @@ import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -43,6 +40,7 @@ public class MapSheetInferenceJobService {
private final InferenceResultCoreService inferenceResultCoreService; private final InferenceResultCoreService inferenceResultCoreService;
private final ShpPipelineService shpPipelineService; private final ShpPipelineService shpPipelineService;
private final InferenceCommonService inferenceCommonService;
private final ExternalHttpClient externalHttpClient; private final ExternalHttpClient externalHttpClient;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@@ -357,7 +355,7 @@ public class MapSheetInferenceJobService {
m.setPriority(5d); m.setPriority(5d);
log.info("[BEFORE INFERENCE] BEFORE SendDto={}", m); log.info("[BEFORE INFERENCE] BEFORE SendDto={}", m);
// 추론 실행 api 호출 // 추론 실행 api 호출
Long batchId = ensureAccepted(m); Long batchId = inferenceCommonService.ensureAccepted(m);
SaveInferenceAiDto saveInferenceAiDto = new SaveInferenceAiDto(); SaveInferenceAiDto saveInferenceAiDto = new SaveInferenceAiDto();
saveInferenceAiDto.setUuid(uuid); saveInferenceAiDto.setUuid(uuid);
@@ -369,73 +367,6 @@ public class MapSheetInferenceJobService {
inferenceResultCoreService.update(saveInferenceAiDto); inferenceResultCoreService.update(saveInferenceAiDto);
} }
/**
* api 호출
*
* @param dto
* @return
*/
// 같은함수가 왜 두개지
private Long ensureAccepted(InferenceSendDto dto) {
if (dto == null) {
log.warn("not InferenceSendDto dto");
throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST);
}
// 1) 요청 로그
log.info("");
log.info("========================================================");
log.info("[SEND INFERENCE] Inference request dto= {}", dto);
log.info("========================================================");
log.info("");
// 2) local 환경 임시 처리
// if ("local".equals(profile)) {
// if (dto.getPred_requests_areas() == null) {
// throw new IllegalStateException("pred_requests_areas is null");
// }
//
// dto.getPred_requests_areas().setInput1_scene_path("/kamco-nfs/requests/2023_local.geojson");
//
// dto.getPred_requests_areas().setInput2_scene_path("/kamco-nfs/requests/2024_local.geojson");
// }
// 3) HTTP 호출
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
// TODO 어떤 URL로 어떤파리티러로 요청한 로딩해야지
ExternalCallResult<String> result =
externalHttpClient.call(inferenceUrl, HttpMethod.POST, dto, headers, String.class);
if (result.statusCode() < 200 || result.statusCode() >= 300) {
log.error("Inference API failed. status={}, body={}", result.statusCode(), result.body());
throw new CustomApiException("BAD_GATEWAY", HttpStatus.BAD_GATEWAY);
}
// 4) 응답 파싱
try {
List<Map<String, Object>> list =
objectMapper.readValue(result.body(), new TypeReference<>() {});
if (list.isEmpty()) {
// 어떤 URL로 어떤파리티러로 요청한 정보를 봐야 재현을 할듯하지요
throw new IllegalStateException("Inference response is empty");
}
Object batchIdObj = list.get(0).get("batch_id");
if (batchIdObj == null) {
throw new IllegalStateException("batch_id not found in response");
}
return Long.valueOf(batchIdObj.toString());
} catch (Exception e) {
log.error("Failed to parse inference response. body={}", result.body(), e);
throw new CustomApiException("INVALID_INFERENCE_RESPONSE", HttpStatus.BAD_GATEWAY);
}
}
/** /**
* 실행중인 profile * 실행중인 profile
* *
@@ -492,6 +423,7 @@ public class MapSheetInferenceJobService {
.map(Long::valueOf) .map(Long::valueOf)
.toList(); .toList();
// 추론 실행 도엽별 정보 조회
List<Long> jobIds = inferenceResultCoreService.findFail5kList(uuid, failedIds, type); List<Long> jobIds = inferenceResultCoreService.findFail5kList(uuid, failedIds, type);
Set<Long> jobIdSet = new HashSet<>(jobIds); Set<Long> jobIdSet = new HashSet<>(jobIds);

View File

@@ -101,6 +101,7 @@ inference:
batch-url: http://192.168.2.183:8000/batches batch-url: http://192.168.2.183:8000/batches
jar-path: ${inference.nfs}/repo/jar/shp-exporter.jar jar-path: ${inference.nfs}/repo/jar/shp-exporter.jar
inference-server-name: server1,server2,server3,server4 inference-server-name: server1,server2,server3,server4
output-dir: ${inference.nfs}/model_output/export
gukyuin: gukyuin:
#url: http://localhost:8080 #url: http://localhost:8080

View File

@@ -95,6 +95,7 @@ inference:
batch-url: http://172.16.4.56:8000/batches batch-url: http://172.16.4.56:8000/batches
jar-path: ${inference.nfs}/repo/jar/shp-exporter.jar jar-path: ${inference.nfs}/repo/jar/shp-exporter.jar
inference-server-name: server1,server2,server3,server4 inference-server-name: server1,server2,server3,server4
output-dir: ${inference.nfs}/model_output/export
gukyuin: gukyuin:
url: http://127.0.0.1:5301 url: http://127.0.0.1:5301

View File

@@ -82,6 +82,8 @@ file:
dataset-response: ${file.nfs}/dataset/response/ dataset-response: ${file.nfs}/dataset/response/
training-data: training-data:
geojson-dir: ${file.nfs}/dataset/request/ geojson-dir: ${file.nfs}/dataset/request/
output-dir: ${file.nfs}/dataset/export/ # 마운트 경로 : 국유인 연계 등록할 추론 shp 파일
inference: inference:
nfs: /kamco-nfs nfs: /kamco-nfs
geojson-dir: ${inference.nfs}/requests/ # 추론실행을 위한 파일생성경로 geojson-dir: ${inference.nfs}/requests/ # 추론실행을 위한 파일생성경로