Merge pull request '영상관리 추가 수정' (#66) from feat/dev_251201 into develop

Reviewed-on: https://kamco.gitea.gs.dabeeo.com/dabeeo/kamco-dabeeo-backoffice/pulls/66
This commit is contained in:
2025-12-17 11:32:36 +09:00
15 changed files with 617 additions and 167 deletions

View File

@@ -152,55 +152,97 @@ public class FIleChecker {
File file = new File(filePath); File file = new File(filePath);
if (!file.exists()) { if (!file.exists()) {
System.err.println("파일이 존재하지 않습니다: " + filePath);
return false; return false;
} }
boolean hasDriver = false; boolean hasDriver = false;
// 운영체제 감지
String osName = System.getProperty("os.name").toLowerCase();
boolean isWindows = osName.contains("win");
boolean isMac = osName.contains("mac");
boolean isUnix = osName.contains("nix") || osName.contains("nux") || osName.contains("aix");
// gdalinfo 경로 찾기 (일반적인 설치 경로 우선 확인)
String gdalinfoPath = findGdalinfoPath();
if (gdalinfoPath == null) {
System.err.println("gdalinfo 명령어를 찾을 수 없습니다. GDAL이 설치되어 있는지 확인하세요.");
System.err.println("macOS: brew install gdal");
System.err.println("Ubuntu/Debian: sudo apt-get install gdal-bin");
System.err.println("CentOS/RHEL: sudo yum install gdal");
return false;
}
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
// 윈도우용
command.add("cmd.exe"); // 윈도우 명령 프롬프트 실행 if (isWindows) {
command.add("/c"); // 명령어를 수행하고 종료한다는 옵션 // 윈도우용
command.add("gdalinfo"); command.add("cmd.exe");
command.add(filePath); command.add("/c");
command.add("|"); command.add(gdalinfoPath + " \"" + filePath + "\" | findstr /i Geo");
command.add("findstr"); } else if (isMac || isUnix) {
command.add("/i"); // 리눅스, 맥용
command.add("Geo"); command.add("sh");
command.add("-c");
/* command.add(gdalinfoPath + " \"" + filePath + "\" | grep -i Geo");
command.add("sh"); // 리눅스,맥 명령 프롬프트 실행 } else {
command.add("-c"); // 명령어를 수행하고 종료한다는 옵션 System.err.println("지원하지 않는 운영체제: " + osName);
command.add("gdalinfo"); return false;
command.add(filePath); }
command.add("|");
command.add("grep");
command.add("-i");
command.add("Geo");
*/
ProcessBuilder processBuilder = new ProcessBuilder(command); ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.redirectErrorStream(true); processBuilder.redirectErrorStream(true);
Process process = null;
BufferedReader reader = null;
try { try {
Process process = processBuilder.start(); System.out.println("gdalinfo 명령어 실행 시작: " + filePath);
process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line; String line;
while ((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) {
System.out.println("line == " + line); System.out.println("gdalinfo 출력: " + line);
if (line.contains("Driver: GTiff/GeoTIFF")) { if (line.contains("Driver: GTiff/GeoTIFF")) {
hasDriver = true; hasDriver = true;
break; break;
} }
} }
process.waitFor(); int exitCode = process.waitFor();
System.out.println("gdalinfo 종료 코드: " + exitCode);
} catch (Exception e) { // 프로세스가 정상 종료되지 않았고 Driver를 찾지 못한 경우
if (exitCode != 0 && !hasDriver) {
System.err.println("gdalinfo 명령 실행 실패. Exit code: " + exitCode);
}
} catch (IOException e) {
System.err.println("gdalinfo 실행 중 I/O 오류 발생: " + e.getMessage());
e.printStackTrace(); e.printStackTrace();
return false;
} catch (InterruptedException e) {
System.err.println("gdalinfo 실행 중 인터럽트 발생: " + e.getMessage());
Thread.currentThread().interrupt();
return false;
} catch (Exception e) {
System.err.println("gdalinfo 실행 중 예상치 못한 오류 발생: " + e.getMessage());
e.printStackTrace();
return false;
} finally {
// 리소스 정리
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.err.println("BufferedReader 종료 중 오류: " + e.getMessage());
}
}
if (process != null) {
process.destroy();
}
} }
return hasDriver; return hasDriver;
@@ -349,4 +391,54 @@ public class FIleChecker {
return nameComparator; return nameComparator;
} }
} }
/**
* gdalinfo 실행 파일 경로를 찾습니다.
*
* @return gdalinfo 경로 (찾지 못하면 null)
*/
private static String findGdalinfoPath() {
// 일반적인 설치 경로 확인
String[] possiblePaths = {
"/usr/local/bin/gdalinfo", // Homebrew (macOS)
"/opt/homebrew/bin/gdalinfo", // Homebrew (Apple Silicon macOS)
"/usr/bin/gdalinfo", // Linux
"gdalinfo" // PATH에 있는 경우
};
for (String path : possiblePaths) {
if (isCommandAvailable(path)) {
return path;
}
}
return null;
}
/**
* 명령어가 사용 가능한지 확인합니다.
*
* @param command 명령어 경로
* @return 사용 가능 여부
*/
private static boolean isCommandAvailable(String command) {
try {
ProcessBuilder pb = new ProcessBuilder(command, "--version");
pb.redirectErrorStream(true);
Process process = pb.start();
// 프로세스 완료 대기 (최대 5초)
boolean finished = process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS);
if (!finished) {
process.destroy();
return false;
}
// 종료 코드가 0이면 정상 (일부 명령어는 --version에서 다른 코드 반환할 수 있음)
return process.exitValue() == 0 || process.exitValue() == 1;
} catch (Exception e) {
return false;
}
}
} }

View File

@@ -32,6 +32,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
@Slf4j @Slf4j
@Order(value = 1) @Order(value = 1)
@@ -396,7 +397,8 @@ public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
public ApiResponseDto<String> handlerException(Exception e, HttpServletRequest request) { public ApiResponseDto<String> handlerException(Exception e, HttpServletRequest request) {
log.warn("[Exception] resource :{} ", e.getMessage()); log.error("[Exception] resource: {}, message: {}", request.getRequestURI(), e.getMessage());
log.error("Exception stacktrace: ", e);
String codeName = "INTERNAL_SERVER_ERROR"; String codeName = "INTERNAL_SERVER_ERROR";
ErrorLogEntity errorLog = ErrorLogEntity errorLog =
@@ -504,4 +506,22 @@ public class GlobalExceptionHandler {
// return new ResponseEntity<>(body, status); // return new ResponseEntity<>(body, status);
} }
@ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE)
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ApiResponseDto<String> handleMaxUploadSizeExceeded(
MaxUploadSizeExceededException e, HttpServletRequest request) {
log.warn("[MaxUploadSizeExceededException] resource :{} ", e.getMessage());
ApiResponseCode code = ApiResponseCode.PAYLOAD_TOO_LARGE;
ErrorLogEntity errorLog =
saveErrorLogData(
request,
code,
HttpStatus.PAYLOAD_TOO_LARGE,
ErrorLogDto.LogErrorLevel.WARNING,
e.getStackTrace());
return ApiResponseDto.createException(
code, code.getText(), HttpStatus.PAYLOAD_TOO_LARGE, errorLog.getId());
}
} }

View File

@@ -172,6 +172,7 @@ public class ApiResponseDto<T> {
"You can only reset your password within 24 hours from when the email was sent.\n" "You can only reset your password within 24 hours from when the email was sent.\n"
+ "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("업로드 용량 제한을 초과했습니다."),
; ;
// @formatter:on // @formatter:on
private final String message; private final String message;

View File

@@ -40,12 +40,28 @@ public class MapSheetMngApiController {
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
}) })
@PostMapping("/mng-list") @PostMapping("/mng-list")
public ApiResponseDto<Page<MapSheetMngDto.MngDto>> findMapSheetMngList( public ApiResponseDto<List<MapSheetMngDto.MngDto>> findMapSheetMngList() {
@RequestBody MapSheetMngDto.MngSearchReq searchReq) {
System.out.println("kkkkkkkkkkkkkkkkkkkkkkkkk"); return ApiResponseDto.ok(mapSheetMngService.findMapSheetMngList());
}
return ApiResponseDto.ok(mapSheetMngService.findMapSheetMngList(searchReq)); @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)
})
@GetMapping("/mng")
public ApiResponseDto<MapSheetMngDto.MngDto> findMapSheetMng(@RequestParam int mngYyyy) {
return ApiResponseDto.ok(mapSheetMngService.findMapSheetMng(mngYyyy));
} }
@Operation(summary = "영상관리 > 데이터 등록", description = "영상관리 > 데이터 등록") @Operation(summary = "영상관리 > 데이터 등록", description = "영상관리 > 데이터 등록")
@@ -71,40 +87,9 @@ public class MapSheetMngApiController {
/** /**
* 오류데이터 목록 조회 * 오류데이터 목록 조회
* *
* <p>도엽파일 동기화 시 발생한 오류 데이터를 조회합니다. * @param searchReq
* * @return
* <p>오류 타입:
*
* <ul>
* <li>NOFILE - 파일없음
* <li>NOTPAIR - 페어없음 (tif/tfw 중 하나만 존재)
* <li>DUPLICATE - 중복 (동일한 파일이 여러개 존재)
* <li>SIZEERROR - size 0 (파일 크기가 0)
* <li>TYPEERROR - 형식오류 (tfw 검증 실패 또는 GeoTIFF 검증 실패)
* </ul>
*
* <p>sync_check_strt_dttm과 sync_check_end_dttm은 동일한 값으로 설정됩니다.
*
* @param searchReq 검색 조건 (년도, 검색어, syncStateFilter 등)
* @return 오류 데이터 목록
*/ */
@Operation(
summary = "오류데이터 목록 조회",
description =
"도엽파일 동기화 시 발생한 오류 데이터를 조회합니다. "
+ "오류 타입: NOFILE(파일없음), NOTPAIR(페어없음), DUPLICATE(중복), SIZEERROR(size 0), TYPEERROR(형식오류)")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = MapSheetMngDto.ErrorDataDto.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/error-list") @PostMapping("/error-list")
public ApiResponseDto<Page<MapSheetMngDto.ErrorDataDto>> findMapSheetErrorList( public ApiResponseDto<Page<MapSheetMngDto.ErrorDataDto>> findMapSheetErrorList(
@RequestBody @Valid MapSheetMngDto.ErrorSearchReq searchReq) { @RequestBody @Valid MapSheetMngDto.ErrorSearchReq searchReq) {

View File

@@ -122,17 +122,18 @@ public class MapSheetMngDto {
@Schema(description = "정렬", example = "id desc") @Schema(description = "정렬", example = "id desc")
private String sort; private String sort;
@Schema(description = "오류종류(페어누락:NOTPAIR,중복파일:DUPLICATE,손상파일:FAULT)", example = "NOTPAIR")
private String syncState;
@Schema(description = "처리유형(처리:DONE,미처리:NOTYET)", example = "DONE")
private String syncCheckState;
@Schema(description = "검색어", example = "부산3959") @Schema(description = "검색어", example = "부산3959")
private String searchValue; private String searchValue;
@Schema(description = "년도", example = "2025") @Schema(description = "년도", example = "2025")
private Integer mngYyyy; private Integer mngYyyy;
@Schema(
description = "동기화 상태 필터 (NOFILE, NOTPAIR, DUPLICATE, SIZEERROR, TYPEERROR)",
example = "NOFILE")
private String syncStateFilter;
public Pageable toPageable() { public Pageable toPageable() {
if (sort != null && !sort.isEmpty()) { if (sort != null && !sort.isEmpty()) {
String[] sortParams = sort.split(","); String[] sortParams = sort.split(",");
@@ -149,27 +150,57 @@ public class MapSheetMngDto {
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor
public static class ErrorDataDto { public static class ErrorDataDto {
// private Integer rowNum;
private Long hstUid; private Long hstUid;
private Integer rowNum;
private String map50kName; private String map50kName;
private String map5kName; private String map5kName;
private String mapSrcName;
private Integer mapCodeSrc; private Integer mapCodeSrc;
private String createdDttm; @JsonFormatDttm private ZonedDateTime createdDttm;
private DataState dataState;
@Schema(description = "동기화 상태 (NOFILE, NOTPAIR, DUPLICATE, SIZEERROR, TYPEERROR)")
private String syncState; private String syncState;
@Schema(description = "동기화 체크 상태")
private String syncCheckState; private String syncCheckState;
@Schema(description = "동기화 체크 시작 시간") // private Long fileUid;
private java.time.LocalDateTime syncCheckStrtDttm; private String tfwFileName;
private String tifFileName;
@Schema(description = "동기화 체크 종료 시간") // private List<MngFIleDto> fileArray;
private java.time.LocalDateTime syncCheckEndDttm;
public ErrorDataDto(
Long hstUid,
String map50kName,
String map5kName,
String mapSrcName,
Integer mapCodeSrc,
ZonedDateTime createdDttm,
String syncState,
String syncCheckState,
String tfwFileName,
String tifFileName) {
this.hstUid = hstUid;
this.map50kName = map50kName;
this.map5kName = map5kName;
this.mapSrcName = mapSrcName;
this.mapCodeSrc = mapCodeSrc;
this.createdDttm = createdDttm;
this.syncState = syncState;
this.syncCheckState = syncCheckState;
this.tfwFileName = tfwFileName;
this.tifFileName = tifFileName;
}
}
@Schema(name = "MngFIleDto", description = "관리파일정보")
@Getter
@Setter
public static class MngFIleDto {
private Long fileUid;
private String filePath;
private String fileName;
private Long fileSize;
private String fileState;
private Long hstUid;
} }
@Schema(name = "DmlReturn", description = "영상관리 DML 수행 후 리턴") @Schema(name = "DmlReturn", description = "영상관리 DML 수행 후 리턴")
@@ -241,27 +272,4 @@ public class MapSheetMngDto {
return desc; return desc;
} }
} }
@Getter
@AllArgsConstructor
public enum SyncErrorState implements EnumType {
NOFILE("파일없음"),
NOTPAIR("페어없음"),
DUPLICATE("중복"),
SIZEERROR("size 0"),
TYPEERROR("형식오류"),
DONE("정상");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
} }

View File

@@ -34,11 +34,13 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Transactional(readOnly = true) @Transactional(readOnly = true)
@@ -318,11 +320,34 @@ public class MapSheetMngFileCheckerService {
@Transactional @Transactional
public String uploadFile(MultipartFile file, String targetPath, boolean overwrite, Long hstUid) { public String uploadFile(MultipartFile file, String targetPath, boolean overwrite, Long hstUid) {
try { try {
// 파일 유효성 검증
if (file == null || file.isEmpty()) {
throw new ValidationException("업로드 파일이 비어있습니다.");
}
if (file.getOriginalFilename() == null || file.getOriginalFilename().isEmpty()) {
throw new ValidationException("파일명이 유효하지 않습니다.");
}
Path path = Paths.get(targetPath); Path path = Paths.get(targetPath);
if (Files.isDirectory(path)) {
// targetPath가 존재하지 않으면 파일 경로로 가정하고 부모 디렉토리 생성
if (!Files.exists(path)) {
// 경로가 확장자로 끝나면 파일로 간주
if (targetPath.matches(".*\\.[a-zA-Z]{3,4}$")) {
if (path.getParent() != null) {
Files.createDirectories(path.getParent());
}
} else {
// 확장자가 없으면 디렉토리로 간주
Files.createDirectories(path);
path = path.resolve(file.getOriginalFilename());
}
} else if (Files.isDirectory(path)) {
path = path.resolve(file.getOriginalFilename()); path = path.resolve(file.getOriginalFilename());
} }
if (path.getParent() != null) {
// 최종 파일의 부모 디렉토리 생성
if (path.getParent() != null && !Files.exists(path.getParent())) {
Files.createDirectories(path.getParent()); Files.createDirectories(path.getParent());
} }
@@ -403,8 +428,13 @@ public class MapSheetMngFileCheckerService {
saveUploadMeta(path, hstUid); saveUploadMeta(path, hstUid);
return "업로드 성공"; return "업로드 성공";
} catch (ValidationException | DuplicateFileException e) {
// 비즈니스 예외는 그대로 던짐
throw e;
} catch (IOException e) { } catch (IOException e) {
throw new IllegalArgumentException("파일 I/O 처리 실패: " + e.getMessage()); throw new IllegalArgumentException("파일 I/O 처리 실패: " + e.getMessage(), e);
} catch (Exception e) {
throw new IllegalArgumentException("파일 업로드 처리 중 오류 발생: " + e.getMessage(), e);
} }
} }
@@ -416,8 +446,45 @@ public class MapSheetMngFileCheckerService {
boolean overwrite, boolean overwrite,
Long hstUid) { Long hstUid) {
try { try {
log.info(
"uploadPair 시작 - targetPath: {}, overwrite: {}, hstUid: {}",
targetPath,
overwrite,
hstUid);
// 파일 유효성 검증
if (tfwFile == null || tfwFile.isEmpty()) {
throw new ValidationException("TFW 파일이 비어있습니다.");
}
if (tifFile == null || tifFile.isEmpty()) {
throw new ValidationException("TIF 파일이 비어있습니다.");
}
if (tfwFile.getOriginalFilename() == null || tfwFile.getOriginalFilename().isEmpty()) {
throw new ValidationException("TFW 파일명이 유효하지 않습니다.");
}
if (tifFile.getOriginalFilename() == null || tifFile.getOriginalFilename().isEmpty()) {
throw new ValidationException("TIF 파일명이 유효하지 않습니다.");
}
log.info(
"파일명 - TFW: {}, TIF: {}", tfwFile.getOriginalFilename(), tifFile.getOriginalFilename());
Path basePath = Paths.get(targetPath); Path basePath = Paths.get(targetPath);
// targetPath가 존재하지 않으면 디렉토리로 생성
if (!Files.exists(basePath)) {
log.info("대상 경로가 존재하지 않아 디렉토리 생성: {}", basePath);
Files.createDirectories(basePath);
}
// 파일인 경우 부모 디렉토리를 basePath로 사용
if (Files.isRegularFile(basePath)) {
log.info("대상 경로가 파일이므로 부모 디렉토리 사용");
basePath = basePath.getParent();
}
if (Files.isDirectory(basePath)) { if (Files.isDirectory(basePath)) {
log.info("디렉토리 확인됨: {}", basePath);
// 디렉토리인 경우 파일명 기준으로 경로 생성 // 디렉토리인 경우 파일명 기준으로 경로 생성
Path tfwPath = basePath.resolve(tfwFile.getOriginalFilename()); Path tfwPath = basePath.resolve(tfwFile.getOriginalFilename());
Path tifPath = basePath.resolve(tifFile.getOriginalFilename()); Path tifPath = basePath.resolve(tifFile.getOriginalFilename());
@@ -427,9 +494,9 @@ public class MapSheetMngFileCheckerService {
if (!tfwBase.equalsIgnoreCase(tifBase)) { if (!tfwBase.equalsIgnoreCase(tifBase)) {
throw new ValidationException("TFW/TIF 파일명이 동일한 베이스가 아닙니다."); throw new ValidationException("TFW/TIF 파일명이 동일한 베이스가 아닙니다.");
} }
// 디렉토리 생성 // 디렉토리는 이미 생성되었으므로 추가 생성 불필요
if (tfwPath.getParent() != null) Files.createDirectories(tfwPath.getParent()); // if (tfwPath.getParent() != null) Files.createDirectories(tfwPath.getParent());
if (tifPath.getParent() != null) Files.createDirectories(tifPath.getParent()); // if (tifPath.getParent() != null) Files.createDirectories(tifPath.getParent());
// DB 중복 체크 및 overwrite 처리 (각 파일별) // DB 중복 체크 및 overwrite 처리 (각 파일별)
String parentPathStr = basePath.toString(); String parentPathStr = basePath.toString();
@@ -450,32 +517,51 @@ public class MapSheetMngFileCheckerService {
} }
// 파일 저장 // 파일 저장
log.info("파일 저장 시작 - TFW: {}, TIF: {}", tfwPath, tifPath);
tfwFile.transferTo(tfwPath.toFile()); tfwFile.transferTo(tfwPath.toFile());
tifFile.transferTo(tifPath.toFile()); tifFile.transferTo(tifPath.toFile());
log.info("파일 저장 완료");
// 검증 // 검증
log.info("TFW 파일 검증 시작: {}", tfwPath);
boolean tfwOk = FIleChecker.checkTfw(tfwPath.toString()); boolean tfwOk = FIleChecker.checkTfw(tfwPath.toString());
if (!tfwOk) { if (!tfwOk) {
log.warn("TFW 파일 검증 실패: {}", tfwName);
Files.deleteIfExists(tfwPath); Files.deleteIfExists(tfwPath);
Files.deleteIfExists(tifPath); Files.deleteIfExists(tifPath);
throw new ValidationException("유효하지 않은 TFW 파일입니다 (6줄 숫자 형식 검증 실패): " + tfwName); throw new ValidationException("유효하지 않은 TFW 파일입니다 (6줄 숫자 형식 검증 실패): " + tfwName);
} }
log.info("TFW 파일 검증 성공");
log.info("TIF 파일 검증 시작: {}", tifPath);
boolean isValidTif = FIleChecker.cmmndGdalInfo(tifPath.toString()); boolean isValidTif = FIleChecker.cmmndGdalInfo(tifPath.toString());
if (!isValidTif) { if (!isValidTif) {
log.warn("TIF 파일 검증 실패: {}", tifName);
Files.deleteIfExists(tfwPath); Files.deleteIfExists(tfwPath);
Files.deleteIfExists(tifPath); Files.deleteIfExists(tifPath);
throw new ValidationException("유효하지 않은 TIF 파일입니다 (GDAL 검증 실패): " + tifName); throw new ValidationException("유효하지 않은 TIF 파일입니다 (GDAL 검증 실패): " + tifName);
} }
log.info("TIF 파일 검증 성공");
// 메타 저장 (두 파일 각각 저장) // 메타 저장 (두 파일 각각 저장)
log.info("메타 데이터 저장 시작");
saveUploadMeta(tfwPath, hstUid); saveUploadMeta(tfwPath, hstUid);
saveUploadMeta(tifPath, hstUid); saveUploadMeta(tifPath, hstUid);
log.info("메타 데이터 저장 완료");
return "TFW/TIF 페어 업로드 성공"; return "TFW/TIF 페어 업로드 성공";
} else { } else {
throw new ValidationException("targetPath는 디렉토리여야 합니다."); throw new ValidationException("targetPath는 디렉토리여야 합니다.");
} }
} catch (ValidationException | DuplicateFileException e) {
// 비즈니스 예외는 그대로 던짐
log.warn("업로드 비즈니스 예외 발생: {}", e.getMessage());
throw e;
} catch (IOException e) { } catch (IOException e) {
throw new IllegalArgumentException("파일 I/O 처리 실패: " + e.getMessage()); log.error("파일 I/O 처리 실패: {}", e.getMessage(), e);
throw new IllegalArgumentException("파일 I/O 처리 실패: " + e.getMessage(), e);
} catch (Exception e) {
log.error("파일 업로드 처리 중 예상치 못한 오류 발생: {}", e.getMessage(), e);
throw new IllegalArgumentException("파일 업로드 처리 중 오류 발생: " + e.getMessage(), e);
} }
} }

View File

@@ -212,15 +212,19 @@ public class MapSheetMngService {
} }
} }
public List<MapSheetMngDto.MngDto> findMapSheetMngList() {
return mapSheetMngCoreService.findMapSheetMngList();
}
public MapSheetMngDto.MngDto findMapSheetMng(int mngYyyy) {
return mapSheetMngCoreService.findMapSheetMng(mngYyyy);
}
public Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList( public Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList(
MapSheetMngDto.@Valid ErrorSearchReq searchReq) { MapSheetMngDto.@Valid ErrorSearchReq searchReq) {
return mapSheetMngCoreService.findMapSheetErrorList(searchReq); return mapSheetMngCoreService.findMapSheetErrorList(searchReq);
} }
public Page<MapSheetMngDto.MngDto> findMapSheetMngList(MapSheetMngDto.MngSearchReq searchReq) {
return mapSheetMngCoreService.findMapSheetMngList(searchReq);
}
@Transactional @Transactional
public MapSheetMngDto.DmlReturn mngDataSave(MapSheetMngDto.AddReq AddReq) { public MapSheetMngDto.DmlReturn mngDataSave(MapSheetMngDto.AddReq AddReq) {
return mapSheetMngCoreService.mngDataSave(AddReq); return mapSheetMngCoreService.mngDataSave(AddReq);

View File

@@ -37,9 +37,12 @@ public class MapSheetMngCoreService {
return mapSheetMngRepository.findMapSheetErrorList(searchReq); return mapSheetMngRepository.findMapSheetErrorList(searchReq);
} }
public Page<MapSheetMngDto.MngDto> findMapSheetMngList( public List<MapSheetMngDto.MngDto> findMapSheetMngList() {
MapSheetMngDto.@Valid MngSearchReq searchReq) { return mapSheetMngRepository.findMapSheetMngList();
return mapSheetMngRepository.findMapSheetMngList(searchReq); }
public MapSheetMngDto.MngDto findMapSheetMng(int mngYyyy) {
return mapSheetMngRepository.findMapSheetMng(mngYyyy);
} }
public MapSheetMngDto.DmlReturn uploadProcess(@Valid List<Long> hstUidList) { public MapSheetMngDto.DmlReturn uploadProcess(@Valid List<Long> hstUidList) {

View File

@@ -3,12 +3,15 @@ package com.kamco.cd.kamcoback.postgres.repository.mapsheet;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetMngHstEntity; import com.kamco.cd.kamcoback.postgres.entity.MapSheetMngHstEntity;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
public interface MapSheetMngRepositoryCustom { public interface MapSheetMngRepositoryCustom {
Page<MapSheetMngDto.MngDto> findMapSheetMngList(MapSheetMngDto.MngSearchReq searchReq); List<MapSheetMngDto.MngDto> findMapSheetMngList();
MapSheetMngDto.MngDto findMapSheetMng(int mngYyyy);
Optional<MapSheetMngHstEntity> findMapSheetMngHstInfo(Long hstUid); Optional<MapSheetMngHstEntity> findMapSheetMngHstInfo(Long hstUid);

View File

@@ -43,14 +43,14 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
} }
@Override @Override
public Page<MapSheetMngDto.MngDto> findMapSheetMngList(MapSheetMngDto.MngSearchReq searchReq) { 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) { // if (searchReq.getMngYyyy() != null) {
whereBuilder.and(mapSheetMngEntity.mngYyyy.eq(searchReq.getMngYyyy())); // whereBuilder.and(mapSheetMngEntity.mngYyyy.eq(searchReq.getMngYyyy()));
} // }
NumberExpression<Long> totalCount = mapSheetMngHstEntity.count().as("syncTotCnt"); NumberExpression<Long> totalCount = mapSheetMngHstEntity.count().as("syncTotCnt");
@@ -151,9 +151,9 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
.leftJoin(mapSheetMngHstEntity) .leftJoin(mapSheetMngHstEntity)
.on(mapSheetMngEntity.mngYyyy.eq(mapSheetMngHstEntity.mngYyyy)) .on(mapSheetMngEntity.mngYyyy.eq(mapSheetMngHstEntity.mngYyyy))
.where(whereBuilder) .where(whereBuilder)
.offset(pageable.getOffset()) // .offset(pageable.getOffset())
.limit(pageable.getPageSize()) // .limit(pageable.getPageSize())
.orderBy(mapSheetMngEntity.createdDttm.desc()) .orderBy(mapSheetMngEntity.mngYyyy.desc())
.groupBy(mapSheetMngEntity.mngYyyy) .groupBy(mapSheetMngEntity.mngYyyy)
.fetch(); .fetch();
@@ -164,7 +164,107 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
.where(whereBuilder) .where(whereBuilder)
.fetchOne(); .fetchOne();
return new PageImpl<>(foundContent, pageable, countQuery); return foundContent;
}
public MapSheetMngDto.MngDto findMapSheetMng(int mngYyyy) {
BooleanBuilder whereBuilder = new BooleanBuilder();
whereBuilder.and(mapSheetMngEntity.mngYyyy.eq(mngYyyy));
MapSheetMngDto.MngDto foundContent =
queryFactory
.select(
Projections.constructor(
MapSheetMngDto.MngDto.class,
Expressions.numberTemplate(
Integer.class,
"row_number() over(order by {0} desc)",
mapSheetMngEntity.createdDttm),
mapSheetMngEntity.mngYyyy,
mapSheetMngEntity.mngState,
mapSheetMngEntity.syncState,
mapSheetMngEntity.syncCheckState,
mapSheetMngHstEntity.count(),
new CaseBuilder()
.when(mapSheetMngHstEntity.dataState.eq("DONE"))
.then(1L)
.otherwise(0L)
.sum()
.as("syncStateDoneCnt"),
new CaseBuilder()
.when(mapSheetMngHstEntity.syncState.ne("NOTYET"))
.then(1L)
.otherwise(0L)
.sum(),
new CaseBuilder()
.when(
mapSheetMngHstEntity
.syncState
.eq("NOFILE")
.or(mapSheetMngHstEntity.syncState.eq("NOTPAIR")))
.then(1L)
.otherwise(0L)
.sum(),
new CaseBuilder()
.when(
mapSheetMngHstEntity
.syncCheckState
.eq("DONE")
.and(
mapSheetMngHstEntity
.syncState
.eq("NOFILE")
.or(mapSheetMngHstEntity.syncState.eq("NOTPAIR"))))
.then(1L)
.otherwise(0L)
.sum(),
new CaseBuilder()
.when(mapSheetMngHstEntity.syncState.eq("DUPLICATE"))
.then(1L)
.otherwise(0L)
.sum(),
new CaseBuilder()
.when(
mapSheetMngHstEntity
.syncCheckState
.eq("DONE")
.and(mapSheetMngHstEntity.syncState.eq("DUPLICATE")))
.then(1L)
.otherwise(0L)
.sum(),
new CaseBuilder()
.when(
mapSheetMngHstEntity
.syncState
.eq("TYPEERROR")
.or(mapSheetMngHstEntity.syncState.eq("SIZEERROR")))
.then(1L)
.otherwise(0L)
.sum(),
new CaseBuilder()
.when(
mapSheetMngHstEntity
.syncCheckState
.eq("DONE")
.and(
mapSheetMngHstEntity
.syncState
.eq("TYPEERROR")
.or(mapSheetMngHstEntity.syncState.eq("SIZEERROR"))))
.then(1L)
.otherwise(0L)
.sum(),
mapSheetMngEntity.createdDttm,
mapSheetMngHstEntity.syncEndDttm.max()))
.from(mapSheetMngEntity)
.leftJoin(mapSheetMngHstEntity)
.on(mapSheetMngEntity.mngYyyy.eq(mapSheetMngHstEntity.mngYyyy))
.where(whereBuilder)
.groupBy(mapSheetMngEntity.mngYyyy)
.fetchOne();
return foundContent;
} }
@Override @Override
@@ -172,28 +272,50 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
MapSheetMngDto.@Valid ErrorSearchReq searchReq) { MapSheetMngDto.@Valid ErrorSearchReq searchReq) {
Pageable pageable = PageRequest.of(searchReq.getPage(), searchReq.getSize()); Pageable pageable = PageRequest.of(searchReq.getPage(), searchReq.getSize());
BooleanBuilder whereBuilder = new BooleanBuilder(); BooleanBuilder whereBuilder = new BooleanBuilder();
whereBuilder.and(mapSheetMngHstEntity.mngYyyy.eq(searchReq.getMngYyyy()));
// syncStateFilter 조건 추가 whereBuilder.and(mapSheetMngHstEntity.mngYyyy.eq(searchReq.getMngYyyy()));
if (searchReq.getSyncStateFilter() != null && !searchReq.getSyncStateFilter().isEmpty()) { whereBuilder.and(
whereBuilder.and(mapSheetMngHstEntity.syncState.eq(searchReq.getSyncStateFilter())); mapSheetMngHstEntity.syncState.ne("DONE").and(mapSheetMngHstEntity.syncState.ne("NOTYET")));
} else {
// 기본: 오류 상태만 조회 (NOFILE, NOTPAIR, DUPLICATE, SIZEERROR, TYPEERROR) if (searchReq.getSyncState() != null && !searchReq.getSyncState().isEmpty()) {
whereBuilder.and( if (searchReq.getSyncState().equals("NOTPAIR")) {
mapSheetMngHstEntity whereBuilder.and(
.syncState mapSheetMngHstEntity
.eq("NOFILE") .syncState
.or(mapSheetMngHstEntity.syncState.eq("NOTPAIR")) .eq("NOTPAIR")
.or(mapSheetMngHstEntity.syncState.eq("DUPLICATE")) .or(mapSheetMngHstEntity.syncState.eq("NOFILE")));
.or(mapSheetMngHstEntity.syncState.eq("SIZEERROR")) } else if (searchReq.getSyncState().equals("FAULT")) {
.or(mapSheetMngHstEntity.syncState.eq("TYPEERROR"))); whereBuilder.and(
mapSheetMngHstEntity
.syncState
.eq("SIZEERROR")
.or(mapSheetMngHstEntity.syncState.eq("TYPEERROR")));
} else {
whereBuilder.and(mapSheetMngHstEntity.syncState.eq(searchReq.getSyncState()));
}
}
if (searchReq.getSyncCheckState() != null && !searchReq.getSyncCheckState().isEmpty()) {
whereBuilder.and(mapSheetMngHstEntity.syncCheckState.eq(searchReq.getSyncCheckState()));
} }
// 검색어 조건 추가
if (searchReq.getSearchValue() != null && !searchReq.getSearchValue().isEmpty()) { if (searchReq.getSearchValue() != null && !searchReq.getSearchValue().isEmpty()) {
whereBuilder.and(mapSheetErrorSearchValue(searchReq)); whereBuilder.and(
mapSheetMngHstEntity
.mapSheetNum
.eq(searchReq.getSearchValue())
.or(mapSheetMngHstEntity.refMapSheetNum.eq(searchReq.getSearchValue()))
.or(
Expressions.stringTemplate(
"concat({0},substring({1}, 0, 6))",
mapInkx5kEntity.mapidNm, mapSheetMngHstEntity.mapSheetNum)
.likeIgnoreCase("%" + searchReq.getSearchValue() + "%"))
.or(
Expressions.stringTemplate(
"concat({0},substring({1}, 6, 8))",
mapInkx5kEntity.mapidNm, mapSheetMngHstEntity.mapSheetNum)
.likeIgnoreCase("%" + searchReq.getSearchValue() + "%")));
} }
List<MapSheetMngDto.ErrorDataDto> foundContent = List<MapSheetMngDto.ErrorDataDto> foundContent =
@@ -202,30 +324,38 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
Projections.constructor( Projections.constructor(
MapSheetMngDto.ErrorDataDto.class, MapSheetMngDto.ErrorDataDto.class,
mapSheetMngHstEntity.hstUid, mapSheetMngHstEntity.hstUid,
rowNum(),
Expressions.stringTemplate( Expressions.stringTemplate(
"concat({0}, {1})", "concat({0},substring({1}, 0, 6))",
mapSheetMngHstEntity.mapSheetName, mapInkx50kEntity.mapidcdNo), mapInkx5kEntity.mapidNm, mapSheetMngHstEntity.mapSheetNum)
.as("map50kName"),
Expressions.stringTemplate( Expressions.stringTemplate(
"concat({0}, substring({1}, {2}, {3}))", "concat({0},substring({1}, 6, 8))",
mapSheetMngHstEntity.mapSheetName, mapSheetMngHstEntity.mapSheetNum, 6, 8), mapInkx5kEntity.mapidNm, mapSheetMngHstEntity.mapSheetNum)
mapSheetMngHstEntity.mapSheetCodeSrc, .as("map5kName"),
Expressions.stringTemplate( Expressions.stringTemplate(
"to_char({0}, 'YYYY-MM-DD')", mapSheetMngHstEntity.createdDate), "concat({0},substring({1}, 6, 8))",
mapSheetMngHstEntity.dataState, mapInkx5kEntity.mapidNm, mapSheetMngHstEntity.mapSheetNum)
.as("mapSrcName"),
mapInkx5kEntity.fid,
mapSheetMngHstEntity.createdDate,
mapSheetMngHstEntity.syncState, mapSheetMngHstEntity.syncState,
mapSheetMngHstEntity.syncCheckState, mapSheetMngHstEntity.syncCheckState,
mapSheetMngHstEntity.syncCheckStrtDttm, Expressions.stringTemplate(
mapSheetMngHstEntity.syncCheckEndDttm)) "MAX(CASE WHEN {0} = 'tfw' THEN {1} END)",
mapSheetMngFileEntity.fileExt, mapSheetMngFileEntity.fileName),
Expressions.stringTemplate(
"MAX(CASE WHEN {0} = 'tif' THEN {1} END)",
mapSheetMngFileEntity.fileExt, mapSheetMngFileEntity.fileName)))
.from(mapSheetMngHstEntity) .from(mapSheetMngHstEntity)
.innerJoin(mapInkx5kEntity) .innerJoin(mapInkx5kEntity)
.on(mapSheetMngHstEntity.mapSheetCode.eq(mapInkx5kEntity.fid)) .on(mapSheetMngHstEntity.mapSheetNum.eq(mapInkx5kEntity.mapidcdNo))
.leftJoin(mapInkx50kEntity) .leftJoin(mapSheetMngFileEntity)
.on(mapInkx5kEntity.fidK50.eq(mapInkx50kEntity.fid.longValue())) .on(mapSheetMngHstEntity.hstUid.eq(mapSheetMngFileEntity.hstUid))
.where(whereBuilder) .where(whereBuilder)
.groupBy(mapSheetMngHstEntity.hstUid, mapInkx5kEntity.fid, mapInkx5kEntity.mapidNm)
.orderBy(mapSheetMngHstEntity.createdDate.desc())
.offset(pageable.getOffset()) .offset(pageable.getOffset())
.limit(pageable.getPageSize()) .limit(pageable.getPageSize())
.orderBy(mapSheetMngHstEntity.createdDate.desc())
.fetch(); .fetch();
Long countQuery = Long countQuery =

View File

@@ -38,13 +38,14 @@ spring:
servlet: servlet:
multipart: multipart:
enabled: true enabled: true
max-file-size: 1024MB max-file-size: 4GB
max-request-size: 2048MB max-request-size: 4GB
file-size-threshold: 10MB file-size-threshold: 10MB
server: server:
tomcat: tomcat:
max-swallow-size: 2097152000 # 약 2GB max-swallow-size: 4GB
max-http-form-post-size: 4GB
jwt: jwt:
secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b" secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b"
@@ -73,4 +74,3 @@ mapsheet:
upload: upload:
skipGdalValidation: true skipGdalValidation: true

View File

@@ -29,6 +29,18 @@ spring:
port: 6379 port: 6379
password: 1234 password: 1234
servlet:
multipart:
enabled: true
max-file-size: 4GB
max-request-size: 4GB
file-size-threshold: 10MB
server:
tomcat:
max-swallow-size: 4GB
max-http-form-post-size: 4GB
jwt: jwt:
secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b" secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b"
access-token-validity-in-ms: 86400000 # 1일 access-token-validity-in-ms: 86400000 # 1일
@@ -41,7 +53,3 @@ token:
springdoc: springdoc:
swagger-ui: swagger-ui:
persist-authorization: true # 스웨거 새로고침해도 토큰 유지, 로컬스토리지에 저장 persist-authorization: true # 스웨거 새로고침해도 토큰 유지, 로컬스토리지에 저장

View File

@@ -5,7 +5,7 @@ spring:
application: application:
name: kamco-change-detection-api name: kamco-change-detection-api
profiles: profiles:
active: dev # 사용할 프로파일 지정 (ex. dev, prod, test) active: local # 사용할 프로파일 지정 (ex. dev, prod, test)
datasource: datasource:
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver

View File

@@ -0,0 +1,110 @@
package com.kamco.cd.kamcoback.common.utils;
import static org.junit.jupiter.api.Assertions.*;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.io.TempDir;
class FIleCheckerTest {
@Test
void testOsDetection() {
// 운영체제 감지 테스트
String osName = System.getProperty("os.name").toLowerCase();
System.out.println("현재 운영체제: " + osName);
boolean isWindows = osName.contains("win");
boolean isMac = osName.contains("mac");
boolean isUnix = osName.contains("nix") || osName.contains("nux") || osName.contains("aix");
// 최소한 하나의 OS는 감지되어야 함
assertTrue(isWindows || isMac || isUnix, "지원되는 운영체제를 감지해야 합니다");
System.out.println("Windows: " + isWindows);
System.out.println("Mac: " + isMac);
System.out.println("Unix/Linux: " + isUnix);
}
@Test
void testCheckTfw_ValidFile(@TempDir File tempDir) throws IOException {
// 임시 유효한 TFW 파일 생성
File tfwFile = new File(tempDir, "test.tfw");
try (FileWriter writer = new FileWriter(tfwFile)) {
writer.write("0.25\n"); // pixel size x
writer.write("0.0\n"); // rotation x
writer.write("0.0\n"); // rotation y
writer.write("-0.25\n"); // pixel size y
writer.write("127.5\n"); // upper left x
writer.write("37.5\n"); // upper left y
}
boolean result = FIleChecker.checkTfw(tfwFile.getAbsolutePath());
assertTrue(result, "유효한 TFW 파일은 true를 반환해야 합니다");
}
@Test
void testCheckTfw_InvalidFile(@TempDir File tempDir) throws IOException {
// 잘못된 TFW 파일 생성 (5줄만)
File tfwFile = new File(tempDir, "invalid.tfw");
try (FileWriter writer = new FileWriter(tfwFile)) {
writer.write("0.25\n");
writer.write("0.0\n");
writer.write("0.0\n");
writer.write("-0.25\n");
writer.write("127.5\n");
// 6번째 줄 누락
}
boolean result = FIleChecker.checkTfw(tfwFile.getAbsolutePath());
assertFalse(result, "잘못된 TFW 파일은 false를 반환해야 합니다");
}
@Test
void testCheckTfw_NonExistentFile() {
boolean result = FIleChecker.checkTfw("/non/existent/path/file.tfw");
assertFalse(result, "존재하지 않는 파일은 false를 반환해야 합니다");
}
@Test
void testCmmndGdalInfo_MethodExists() throws NoSuchMethodException {
// cmmndGdalInfo 메서드가 존재하는지 확인
assertNotNull(FIleChecker.class.getMethod("cmmndGdalInfo", String.class));
System.out.println("cmmndGdalInfo 메서드 존재 확인");
}
@Test
void testCmmndGdalInfo_NonExistentFile() {
// 존재하지 않는 파일은 false를 반환해야 함
boolean result = FIleChecker.cmmndGdalInfo("/non/existent/path/file.tif");
assertFalse(result, "존재하지 않는 파일은 false를 반환해야 합니다");
}
@Test
@EnabledOnOs(OS.WINDOWS)
void testWindowsCommand() {
System.out.println("Windows OS 감지됨 - GDAL 명령어 형식 확인");
String osName = System.getProperty("os.name").toLowerCase();
assertTrue(osName.contains("win"));
}
@Test
@EnabledOnOs(OS.MAC)
void testMacCommand() {
System.out.println("Mac OS 감지됨 - GDAL 명령어 형식 확인");
String osName = System.getProperty("os.name").toLowerCase();
assertTrue(osName.contains("mac"));
}
@Test
@EnabledOnOs(OS.LINUX)
void testLinuxCommand() {
System.out.println("Linux OS 감지됨 - GDAL 명령어 형식 확인");
String osName = System.getProperty("os.name").toLowerCase();
assertTrue(osName.contains("nux") || osName.contains("nix"));
}
}