From 81108870888883d7161863761d18988c58750870 Mon Sep 17 00:00:00 2001 From: DanielLee <198891672+sanghyeonhd@users.noreply.github.com> Date: Fri, 12 Dec 2025 18:52:51 +0900 Subject: [PATCH] File Management API Upload ALL Success --- .../auth/JwtAuthenticationFilter.java | 3 +- .../exception/DuplicateFileException.java | 7 + .../common/exception/ValidationException.java | 7 + .../config/GlobalExceptionHandler.java | 75 ++++++-- .../cd/kamcoback/config/SecurityConfig.java | 43 +++-- .../config/api/ApiResponseAdvice.java | 36 ++-- .../MapSheetMngFileCheckerApiController.java | 16 +- .../MapSheetMngFileCheckerService.java | 163 ++++++++++++++++-- .../mapsheet/service/MapSheetMngService.java | 1 - .../entity/MapSheetMngFileEntity.java | 12 +- .../postgres/entity/MapSheetMngHstEntity.java | 2 +- .../mapsheet/MapSheetMngFileRepository.java | 6 + .../mapsheet/MapSheetMngRepositoryImpl.java | 55 +++--- src/main/resources/application-dev.yml | 22 +++ src/main/resources/application-local.yml | 20 +++ src/main/resources/application.yml | 2 +- test_data/fake.tif | 1 + 17 files changed, 368 insertions(+), 103 deletions(-) create mode 100644 src/main/java/com/kamco/cd/kamcoback/common/exception/DuplicateFileException.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/common/exception/ValidationException.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngFileRepository.java create mode 100644 test_data/fake.tif diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/JwtAuthenticationFilter.java b/src/main/java/com/kamco/cd/kamcoback/auth/JwtAuthenticationFilter.java index 6b1b84f1..e572e16e 100644 --- a/src/main/java/com/kamco/cd/kamcoback/auth/JwtAuthenticationFilter.java +++ b/src/main/java/com/kamco/cd/kamcoback/auth/JwtAuthenticationFilter.java @@ -43,9 +43,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { String path = request.getServletPath(); - // 여기에 JWT 필터를 타지 않게 할 URL 패턴들 작성 + // JWT 필터를 타지 않게 할 URL 패턴들 return path.startsWith("/api/auth/signin") || path.startsWith("/api/auth/refresh"); - // 필요하면 "/api/auth/logout" 도 추가 } private String resolveToken(HttpServletRequest request) { diff --git a/src/main/java/com/kamco/cd/kamcoback/common/exception/DuplicateFileException.java b/src/main/java/com/kamco/cd/kamcoback/common/exception/DuplicateFileException.java new file mode 100644 index 00000000..8ee3b671 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/exception/DuplicateFileException.java @@ -0,0 +1,7 @@ +package com.kamco.cd.kamcoback.common.exception; + +public class DuplicateFileException extends RuntimeException { + public DuplicateFileException(String message) { + super(message); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/common/exception/ValidationException.java b/src/main/java/com/kamco/cd/kamcoback/common/exception/ValidationException.java new file mode 100644 index 00000000..f9df75b7 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/exception/ValidationException.java @@ -0,0 +1,7 @@ +package com.kamco.cd.kamcoback.common.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/config/GlobalExceptionHandler.java b/src/main/java/com/kamco/cd/kamcoback/config/GlobalExceptionHandler.java index bc7cdb74..701a863f 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/GlobalExceptionHandler.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/GlobalExceptionHandler.java @@ -2,6 +2,8 @@ package com.kamco.cd.kamcoback.config; import com.kamco.cd.kamcoback.auth.CustomUserDetails; import com.kamco.cd.kamcoback.common.exception.CustomApiException; +import com.kamco.cd.kamcoback.common.exception.DuplicateFileException; +import com.kamco.cd.kamcoback.common.exception.ValidationException; import com.kamco.cd.kamcoback.config.api.ApiLogFunction; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; import com.kamco.cd.kamcoback.config.api.ApiResponseDto.ApiResponseCode; @@ -42,6 +44,60 @@ public class GlobalExceptionHandler { this.errorLogRepository = errorLogRepository; } + @ResponseStatus(HttpStatus.CONFLICT) + @ExceptionHandler(DuplicateFileException.class) + public ApiResponseDto handleDuplicateFileException( + DuplicateFileException e, HttpServletRequest request) { + log.warn("[DuplicateFileException] resource :{} ", e.getMessage()); + ApiResponseCode code = ApiResponseCode.CONFLICT; + ErrorLogEntity errorLog = + saveErrorLogData( + request, + code, + HttpStatus.CONFLICT, + ErrorLogDto.LogErrorLevel.WARNING, + e.getStackTrace()); + + return ApiResponseDto.createException( + code, e.getMessage(), HttpStatus.CONFLICT, errorLog.getId()); + } + + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + @ExceptionHandler(ValidationException.class) + public ApiResponseDto handleValidationException( + ValidationException e, HttpServletRequest request) { + log.warn("[ValidationException] resource :{} ", e.getMessage()); + ApiResponseCode code = ApiResponseCode.UNPROCESSABLE_ENTITY; + ErrorLogEntity errorLog = + saveErrorLogData( + request, + code, + HttpStatus.UNPROCESSABLE_ENTITY, + ErrorLogDto.LogErrorLevel.WARNING, + e.getStackTrace()); + + return ApiResponseDto.createException( + code, e.getMessage(), HttpStatus.UNPROCESSABLE_ENTITY, errorLog.getId()); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(IllegalArgumentException.class) + public ApiResponseDto handleIllegalArgumentException( + IllegalArgumentException e, HttpServletRequest request) { + log.warn("[IllegalArgumentException] resource :{} ", e.getMessage()); + ApiResponseCode code = ApiResponseCode.BAD_REQUEST; + ErrorLogEntity errorLog = + saveErrorLogData( + request, + code, + HttpStatus.BAD_REQUEST, + ErrorLogDto.LogErrorLevel.WARNING, + e.getStackTrace()); + + return ApiResponseDto.createException( + code, e.getMessage(), HttpStatus.BAD_REQUEST, errorLog.getId()); + } + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) @ExceptionHandler(EntityNotFoundException.class) public ApiResponseDto handlerEntityNotFoundException( @@ -105,26 +161,7 @@ public class GlobalExceptionHandler { errorLog.getId()); } - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(IllegalArgumentException.class) - public ApiResponseDto handlerIllegalArgumentException( - IllegalArgumentException e, HttpServletRequest request) { - log.warn("[handlerIllegalArgumentException] resource :{} ", e.getMessage()); - String codeName = "BAD_REQUEST"; - ErrorLogEntity errorLog = - saveErrorLogData( - request, - ApiResponseCode.getCode(codeName), - HttpStatus.valueOf(codeName), - ErrorLogDto.LogErrorLevel.WARNING, - e.getStackTrace()); - return ApiResponseDto.createException( - ApiResponseCode.getCode(codeName), - ApiResponseCode.getMessage(codeName), - HttpStatus.valueOf(codeName), - errorLog.getId()); - } @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) @ExceptionHandler(DataIntegrityViolationException.class) diff --git a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java index eca8fcf7..a1cebc5d 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java @@ -11,9 +11,13 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.security.web.firewall.StrictHttpFirewall; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -30,19 +34,23 @@ public class SecurityConfig { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.cors(cors -> cors.configurationSource(corsConfigurationSource())) - .csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화 - .sessionManagement( - sm -> - sm.sessionCreationPolicy( - SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음, 요청은 JWT 인증 - .formLogin(form -> form.disable()) // react에서 로그인 요청 관리 - .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용 - .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음 - .authenticationProvider( - customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 + .csrf(csrf -> csrf.disable()) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .formLogin(form -> form.disable()) + .httpBasic(basic -> basic.disable()) + .logout(logout -> logout.disable()) + .authenticationProvider(customAuthenticationProvider) .authorizeHttpRequests( auth -> auth + // 맵시트 영역 전체 허용 (우선순위 최상단) + .requestMatchers("/api/mapsheet/**") + .permitAll() + + // 업로드 명시적 허용 + .requestMatchers(HttpMethod.POST, "/api/mapsheet/upload") + .permitAll() + // ADMIN만 접근 .requestMatchers("/api/test/admin") .hasRole("ADMIN") @@ -98,4 +106,19 @@ public class SecurityConfig { source.registerCorsConfiguration("/**", config); // CORS 정책을 등록 return source; } + + @Bean + public HttpFirewall httpFirewall() { + StrictHttpFirewall firewall = new StrictHttpFirewall(); + firewall.setAllowUrlEncodedSlash(true); + firewall.setAllowUrlEncodedDoubleSlash(true); + firewall.setAllowUrlEncodedPercent(true); + firewall.setAllowSemicolon(true); + return firewall; + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().requestMatchers(new AntPathRequestMatcher("/api/mapsheet/**")); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java index e6d1f12f..034eccf3 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java @@ -56,37 +56,38 @@ public class ApiResponseAdvice implements ResponseBodyAdvice { ServerHttpResponse response) { HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); - ContentCachingRequestWrapper contentWrapper = (ContentCachingRequestWrapper) servletRequest; + ContentCachingRequestWrapper contentWrapper = null; + if (servletRequest instanceof ContentCachingRequestWrapper wrapper) { + contentWrapper = wrapper; + } if (body instanceof ApiResponseDto apiResponse) { - // ApiResponseDto에 설정된 httpStatus를 실제 HTTP 응답에 적용 response.setStatusCode(apiResponse.getHttpStatus()); String ip = ApiLogFunction.getClientIp(servletRequest); - Long userid = null; - /** - * servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth 이 요청이 - * JWT 인증을 통과한 요청인가? 그리고 Spring Security Authentication 객체가 - * UsernamePasswordAuthenticationToken 타입인가? 체크 - */ - /** - * auth.getPrincipal() instanceof CustomUserDetails customUserDetails principal 안에 들어있는 객체가 내가 - * 만든 CustomUserDetails 타입인가? 체크 - */ if (servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth && auth.getPrincipal() instanceof CustomUserDetails customUserDetails) { - - // audit 에는 long 타입 user_id가 들어가지만 토큰 sub은 uuid여서 user_id 가져오기 userid = customUserDetails.getMember().getId(); } - String requestBody = ApiLogFunction.getRequestBody(servletRequest, contentWrapper); - requestBody = maskSensitiveFields(requestBody); // 로그 저장전에 중요정보 마스킹 + String requestBody; + // 멀티파트 요청은 바디 로깅을 생략 (파일 바이너리로 인한 문제 예방) + MediaType reqContentType = null; + try { + String ct = servletRequest.getContentType(); + reqContentType = ct != null ? MediaType.valueOf(ct) : null; + } catch (Exception ignored) { + } + if (reqContentType != null && MediaType.MULTIPART_FORM_DATA.includes(reqContentType)) { + requestBody = "(multipart omitted)"; + } else { + requestBody = ApiLogFunction.getRequestBody(servletRequest, contentWrapper); + requestBody = maskSensitiveFields(requestBody); + } List list = menuService.getFindAll(); - List result = list.stream() .map( @@ -111,7 +112,6 @@ public class ApiResponseAdvice implements ResponseBodyAdvice { servletRequest.getRequestURI(), requestBody, apiResponse.getErrorLogUid()); - // tb_audit_log 테이블 저장 auditLogRepository.save(log); } diff --git a/src/main/java/com/kamco/cd/kamcoback/mapsheet/MapSheetMngFileCheckerApiController.java b/src/main/java/com/kamco/cd/kamcoback/mapsheet/MapSheetMngFileCheckerApiController.java index e702e106..ee87c0eb 100644 --- a/src/main/java/com/kamco/cd/kamcoback/mapsheet/MapSheetMngFileCheckerApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/mapsheet/MapSheetMngFileCheckerApiController.java @@ -14,13 +14,14 @@ 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.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; 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.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -74,9 +75,12 @@ public class MapSheetMngFileCheckerApiController { @Operation(summary = "파일 업로드", description = "파일 업로드 및 TIF 검증") @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ApiResponseDto uploadFile( - @RequestPart("file") MultipartFile file, - @RequestParam("targetPath") String targetPath) { - return ApiResponseDto.createOK(mapSheetMngFileCheckerService.uploadFile(file, targetPath)); + @RequestParam("file") MultipartFile file, + @RequestParam("targetPath") String targetPath, + @RequestParam(name = "overwrite", required = false, defaultValue = "true") + boolean overwrite) { + return ApiResponseDto.createOK( + mapSheetMngFileCheckerService.uploadFile(file, targetPath, overwrite)); } @Operation(summary = "파일 삭제", description = "중복 파일 등 파일 삭제") @@ -126,4 +130,8 @@ public class MapSheetMngFileCheckerApiController { */ + @PostMapping("/upload-test") + public String uploadTest(@RequestParam("name") String name) { + return "RECV:" + name; + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/mapsheet/service/MapSheetMngFileCheckerService.java b/src/main/java/com/kamco/cd/kamcoback/mapsheet/service/MapSheetMngFileCheckerService.java index fc38a92f..7b8cfd08 100644 --- a/src/main/java/com/kamco/cd/kamcoback/mapsheet/service/MapSheetMngFileCheckerService.java +++ b/src/main/java/com/kamco/cd/kamcoback/mapsheet/service/MapSheetMngFileCheckerService.java @@ -2,6 +2,8 @@ package com.kamco.cd.kamcoback.mapsheet.service; import static java.lang.String.CASE_INSENSITIVE_ORDER; +import com.kamco.cd.kamcoback.common.exception.DuplicateFileException; +import com.kamco.cd.kamcoback.common.exception.ValidationException; import com.kamco.cd.kamcoback.common.utils.FIleChecker; import com.kamco.cd.kamcoback.common.utils.NameValidator; import com.kamco.cd.kamcoback.config.FileConfig; @@ -14,6 +16,8 @@ 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.ImageryDto; import com.kamco.cd.kamcoback.postgres.core.MapSheetMngFileCheckerCoreService; +import com.kamco.cd.kamcoback.postgres.entity.MapSheetMngFileEntity; +import com.kamco.cd.kamcoback.postgres.repository.mapsheet.MapSheetMngFileRepository; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -31,6 +35,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.apache.commons.io.FilenameUtils; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -42,6 +47,10 @@ public class MapSheetMngFileCheckerService { private final MapSheetMngFileCheckerCoreService mapSheetMngFileCheckerCoreService; private final FileConfig fileConfig; + private final MapSheetMngFileRepository mapSheetMngFileRepository; + + @Value("${mapsheet.upload.skipGdalValidation:false}") + private boolean skipGdalValidation; public FoldersDto getFolderAll(SrchFoldersDto srchDto) { @@ -308,41 +317,145 @@ public class MapSheetMngFileCheckerService { } @Transactional - public String uploadFile(MultipartFile file, String targetPath) { + public String uploadFile(MultipartFile file, String targetPath, boolean overwrite) { try { Path path = Paths.get(targetPath); - // If targetPath is a directory, append the original filename if (Files.isDirectory(path)) { path = path.resolve(file.getOriginalFilename()); - } else if (Files.notExists(path) && Files.isDirectory(path.getParent())) { - // If path doesn't exist but parent is a directory, assume it's the full path - // (No change needed) } - - // Ensure parent directory exists if (path.getParent() != null) { Files.createDirectories(path.getParent()); } - // Save file + String filename = path.getFileName().toString(); + String ext = FilenameUtils.getExtension(filename).toLowerCase(); + String baseName = FilenameUtils.getBaseName(filename); + Path tfwPath = + path.getParent() == null + ? Paths.get(baseName + ".tfw") + : path.getParent().resolve(baseName + ".tfw"); + Path tifPath = + path.getParent() == null + ? Paths.get(baseName + ".tif") + : path.getParent().resolve(baseName + ".tif"); + + // 이미 존재하는 경우 처리 + if (Files.exists(path) && !overwrite) { + throw new DuplicateFileException("동일한 파일이 이미 존재합니다: " + path.getFileName()); + } + + // 업로드 파일 저장(덮어쓰기 허용 시 replace) file.transferTo(path.toFile()); - // Check TIF using Gdal functionality - String ext = FilenameUtils.getExtension(path.getFileName().toString()); - if ("tif".equalsIgnoreCase(ext) || "tiff".equalsIgnoreCase(ext)) { - boolean isValid = FIleChecker.cmmndGdalInfo(path.toString()); - if (!isValid) { - Files.delete(path); // Delete invalid file - throw new RuntimeException("유효하지 않은 TIF 파일입니다 (Gdal 검증 실패)."); + if ("tfw".equals(ext)) { + // TFW 검증 + boolean tfwOk = FIleChecker.checkTfw(path.toString()); + if (!tfwOk) { + Files.deleteIfExists(path); + throw new ValidationException( + "유효하지 않은 TFW 파일입니다 (6줄 숫자 형식 검증 실패): " + path.getFileName()); } + // 안내: 같은 베이스의 TIF가 없으면 추후 TIF 업로드 필요 + if (!Files.exists(tifPath)) { + // DB 메타 저장은 진행 (향후 쌍 검증 위해) + saveUploadMeta(path); + return "TFW 업로드 성공 (매칭되는 TIF가 아직 없습니다)."; + } + // TIF가 존재하면 쌍 요건 충족 + saveUploadMeta(path); + return "TFW 업로드 성공"; } + if ("tif".equals(ext) || "tiff".equals(ext)) { + // GDAL 검증 (플래그에 따라 스킵) + if (!skipGdalValidation) { + boolean isValidTif = FIleChecker.cmmndGdalInfo(path.toString()); + if (!isValidTif) { + Files.deleteIfExists(path); + throw new ValidationException("유효하지 않은 TIF 파일입니다 (GDAL 검증 실패): " + path.getFileName()); + } + } + // TFW 존재/검증 + if (!Files.exists(tfwPath)) { + Files.deleteIfExists(path); + throw new ValidationException("TFW 파일이 존재하지 않습니다: " + tfwPath.getFileName()); + } + boolean tfwOk = FIleChecker.checkTfw(tfwPath.toString()); + if (!tfwOk) { + Files.deleteIfExists(path); + throw new ValidationException( + "유효하지 않은 TFW 파일입니다 (6줄 숫자 형식 검증 실패): " + tfwPath.getFileName()); + } + saveUploadMeta(path); + return skipGdalValidation ? "TIF 업로드 성공(GDAL 검증 스킵)" : "TIF 업로드 성공"; + } + + // 기타 확장자: 저장만 하고 메타 기록 + saveUploadMeta(path); + return "업로드 성공"; } catch (IOException e) { - throw new RuntimeException("파일 업로드 실패: " + e.getMessage()); + throw new IllegalArgumentException("파일 I/O 처리 실패: " + e.getMessage()); } } + private void saveUploadMeta(Path savedPath) { + String fullPath = savedPath.toAbsolutePath().toString(); + String fileName = savedPath.getFileName().toString(); + String ext = FilenameUtils.getExtension(fileName); + + // 연도(mng_yyyy) 추출: 경로 내의 연도 폴더명을 찾음 (예: .../original-images/2022/2022_25cm/...) + Integer mngYyyy = extractYearFromPath(fullPath); + + // 도엽번호(map_sheet_num) 추정: 파일명 내 숫자 연속 부분을 추출해 정수화 + String mapSheetNum = extractMapSheetNumFromFileName(fileName); + + // ref_map_sheet_num: 1000으로 나눈 값(파일명 규칙에 따라 추정) + String refMapSheetNum = null; + if (mapSheetNum != null && !mapSheetNum.isEmpty()) { + try { + long num = Long.parseLong(mapSheetNum); + refMapSheetNum = String.valueOf(num / 1000); + } catch (NumberFormatException ignored) { + } + } + + MapSheetMngFileEntity entity = new MapSheetMngFileEntity(); + entity.setMngYyyy(mngYyyy); + entity.setMapSheetNum(mapSheetNum); + entity.setRefMapSheetNum(refMapSheetNum); + entity.setFilePath(savedPath.getParent() != null ? savedPath.getParent().toString() : ""); + entity.setFileName(fileName); + entity.setFileExt(ext); + + mapSheetMngFileRepository.save(entity); + } + + private Integer extractYearFromPath(String fullPath) { + // 경로에서 4자리 연도를 찾아 가장 근접한 값을 사용 + // 예시 경로: /Users/.../original-images/2022/2022_25cm/1/34602 + String[] parts = fullPath.split("/"); + for (String p : parts) { + if (p.matches("\\d{4}")) { + try { + return Integer.parseInt(p); + } catch (NumberFormatException ignored) { + } + } + } + return null; + } + + private String extractMapSheetNumFromFileName(String fileName) { + // 파일명에서 연속된 숫자를 최대한 찾아 사용 (예: 34602027.tif -> 34602027) + String base = FilenameUtils.getBaseName(fileName); + String digits = base.replaceAll("[^0-9]", ""); + if (!digits.isEmpty()) { + return digits; + } + return null; + } + @Transactional public Boolean deleteFile(String filePath) { try { @@ -352,4 +465,22 @@ public class MapSheetMngFileCheckerService { throw new RuntimeException("파일 삭제 실패: " + e.getMessage()); } } + + @Transactional(readOnly = true) + public List findRecentFiles(int limit) { + // 간단히 전체를 불러 정렬/제한 (운영에선 Page 요청으로 변경 권장) + List all = new ArrayList<>(); + mapSheetMngFileRepository.findAll().forEach(all::add); + all.sort( + (a, b) -> { + // fileUid 기준 내림차순 + long av = a.getFileUid() == null ? 0L : a.getFileUid(); + long bv = b.getFileUid() == null ? 0L : b.getFileUid(); + return Long.compare(bv, av); + }); + if (all.size() > limit) { + return all.subList(0, limit); + } + return all; + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/mapsheet/service/MapSheetMngService.java b/src/main/java/com/kamco/cd/kamcoback/mapsheet/service/MapSheetMngService.java index 40145c2e..e6daa08f 100644 --- a/src/main/java/com/kamco/cd/kamcoback/mapsheet/service/MapSheetMngService.java +++ b/src/main/java/com/kamco/cd/kamcoback/mapsheet/service/MapSheetMngService.java @@ -29,7 +29,6 @@ import org.apache.commons.io.FilenameUtils; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; @Service @RequiredArgsConstructor diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetMngFileEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetMngFileEntity.java index e3a1e930..df8757e6 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetMngFileEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetMngFileEntity.java @@ -28,10 +28,10 @@ public class MapSheetMngFileEntity { @NotNull @Column(name = "map_sheet_num", nullable = false) - private Integer mapSheetNum; + private String mapSheetNum; @Column(name = "ref_map_sheet_num") - private Integer refMapSheetNum; + private String refMapSheetNum; @Size(max = 255) @Column(name = "file_path") @@ -45,9 +45,9 @@ public class MapSheetMngFileEntity { @Column(name = "file_ext", length = 20) private String fileExt; - @Column(name = "mng_uid") - private Long mngUid; + // @Column(name = "mng_uid") + // private Long mngUid; - @Column(name = "hst_uid") - private Long hstUid; + // @Column(name = "hst_uid") + // private Long hstUid; } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetMngHstEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetMngHstEntity.java index 332fde9b..50c85e18 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetMngHstEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetMngHstEntity.java @@ -51,7 +51,7 @@ public class MapSheetMngHstEntity extends CommonDateEntity { private String mapSheetPath; @Column(name = "ref_map_sheet_num") - private Long refMapSheetNum; + private String refMapSheetNum; @Column(name = "created_uid") private Long createdUid; diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngFileRepository.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngFileRepository.java new file mode 100644 index 00000000..212ea91c --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngFileRepository.java @@ -0,0 +1,6 @@ +package com.kamco.cd.kamcoback.postgres.repository.mapsheet; + +import com.kamco.cd.kamcoback.postgres.entity.MapSheetMngFileEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MapSheetMngFileRepository extends JpaRepository {} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java index ddc1c4cb..c50ae12c 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java @@ -170,48 +170,53 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport @Override public void deleteByMngYyyyMngAll(int mngYyyy) { - long deletedFileCount = queryFactory - .delete(mapSheetMngFileEntity) - .where(mapSheetMngFileEntity.mngYyyy.eq(mngYyyy)) - .execute(); + long deletedFileCount = + queryFactory + .delete(mapSheetMngFileEntity) + .where(mapSheetMngFileEntity.mngYyyy.eq(mngYyyy)) + .execute(); - long deletedHisCount = queryFactory - .delete(mapSheetMngHstEntity) - .where(mapSheetMngHstEntity.mngYyyy.eq(mngYyyy)) - .execute(); + long deletedHisCount = + queryFactory + .delete(mapSheetMngHstEntity) + .where(mapSheetMngHstEntity.mngYyyy.eq(mngYyyy)) + .execute(); - long deletedMngCount = queryFactory - .delete(mapSheetMngEntity) - .where(mapSheetMngEntity.mngYyyy.eq(mngYyyy)) - .execute(); + long deletedMngCount = + queryFactory + .delete(mapSheetMngEntity) + .where(mapSheetMngEntity.mngYyyy.eq(mngYyyy)) + .execute(); } - @Override public void deleteByMngYyyyMng(int mngYyyy) { - long deletedMngCount = queryFactory - .delete(mapSheetMngEntity) - .where(mapSheetMngEntity.mngYyyy.eq(mngYyyy)) - .execute(); + long deletedMngCount = + queryFactory + .delete(mapSheetMngEntity) + .where(mapSheetMngEntity.mngYyyy.eq(mngYyyy)) + .execute(); } @Override public void deleteByMngYyyyMngHst(int mngYyyy) { - long deletedHisCount = queryFactory - .delete(mapSheetMngHstEntity) - .where(mapSheetMngHstEntity.mngYyyy.eq(mngYyyy)) - .execute(); + long deletedHisCount = + queryFactory + .delete(mapSheetMngHstEntity) + .where(mapSheetMngHstEntity.mngYyyy.eq(mngYyyy)) + .execute(); } @Override public void deleteByMngYyyyMngFile(int mngYyyy) { - long deletedFileCount = queryFactory - .delete(mapSheetMngFileEntity) - .where(mapSheetMngFileEntity.mngYyyy.eq(mngYyyy)) - .execute(); + long deletedFileCount = + queryFactory + .delete(mapSheetMngFileEntity) + .where(mapSheetMngFileEntity.mngYyyy.eq(mngYyyy)) + .execute(); } @Override diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 2013e5fb..a06fba7a 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -35,6 +35,16 @@ spring: port: 6379 password: kamco + servlet: + multipart: + enabled: true + max-file-size: 1024MB + max-request-size: 2048MB + file-size-threshold: 10MB + +server: + tomcat: + max-swallow-size: 2097152000 # 약 2GB jwt: secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b" @@ -47,3 +57,15 @@ token: refresh-cookie-name: kamco-dev # 개발용 쿠키 이름 refresh-cookie-secure: false # 로컬 http 테스트면 false +logging: + level: + org: + springframework: + security: DEBUG + org.springframework.security: DEBUG + +mapsheet: + upload: + skipGdalValidation: true + + diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 7b211531..a35ad602 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -29,6 +29,17 @@ spring: port: 6379 password: 1234 + servlet: + multipart: + enabled: true + max-file-size: 1024MB + max-request-size: 2048MB + file-size-threshold: 10MB + +server: + tomcat: + max-swallow-size: 2097152000 # 약 2GB + jwt: secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b" access-token-validity-in-ms: 86400000 # 1일 @@ -38,4 +49,13 @@ token: refresh-cookie-name: kamco-local # 개발용 쿠키 이름 refresh-cookie-secure: false # 로컬 http 테스트면 false +logging: + level: + org: + springframework: + security: DEBUG + org.springframework.security: DEBUG +mapsheet: + upload: + skipGdalValidation: true diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f8cb1814..289b414c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -41,6 +41,7 @@ logging: web: DEBUG security: DEBUG root: INFO + org.springframework.security: DEBUG # actuator management: health: @@ -77,4 +78,3 @@ geojson: - tar.gz - tgz max-file-size: 104857600 # 100MB - diff --git a/test_data/fake.tif b/test_data/fake.tif new file mode 100644 index 00000000..97513f08 --- /dev/null +++ b/test_data/fake.tif @@ -0,0 +1 @@ +fake tif content