Merge pull request 'feat/dev_251201' (#52) from feat/dev_251201 into develop

Reviewed-on: https://kamco.gitea.gs.dabeeo.com/dabeeo/kamco-dabeeo-backoffice/pulls/52
This commit is contained in:
2025-12-15 11:23:42 +09:00
55 changed files with 1128 additions and 11451 deletions

0
login.json Normal file
View File

View File

@@ -11,7 +11,6 @@ import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
@Component
@@ -19,7 +18,6 @@ import org.springframework.stereotype.Component;
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final MembersRepository membersRepository;
private final UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
@@ -32,24 +30,21 @@ public class CustomAuthenticationProvider implements AuthenticationProvider {
.findByUserId(username)
.orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND));
// 미사용 상태
if (member.getStatus().equals(StatusType.INACTIVE.getId())) {
throw new CustomApiException(AuthErrorCode.INACTIVE_ID);
}
// jBCrypt + 커스텀 salt 로 저장된 패스워드 비교
if (!BCrypt.checkpw(rawPassword, member.getPassword())) {
// 실패 카운트 저장
int cnt = member.getLoginFailCount() + 1;
if (cnt >= 5) {
member.setStatus(StatusType.INACTIVE.getId());
}
member.setLoginFailCount(cnt);
membersRepository.save(member);
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
}
// 삭제 상태
if (member.getStatus().equals(StatusType.DELETED.getId())) {
throw new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND);
}
// 패스워드 실패 횟수 체크
// 로그인 실패 체크
if (member.getLoginFailCount() >= 5) {
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED);
}

View File

@@ -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) {

View File

@@ -15,6 +15,9 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class CommonCodeDto {
@@ -132,6 +135,33 @@ public class CommonCodeDto {
}
}
@Schema(name = "SearchReq", description = "검색 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
// 검색 조건
private String name;
// 페이징 파라미터
private int page = 0;
private int size = 20;
private String sort;
public Pageable toPageable() {
if (sort != null && !sort.isEmpty()) {
String[] sortParams = sort.split(",");
String property = sortParams[0];
Sort.Direction direction =
sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC;
return PageRequest.of(page, size, Sort.by(direction, property));
}
return PageRequest.of(page, size);
}
}
@Getter
public static class Clazzes {

View File

@@ -8,8 +8,8 @@ import lombok.Getter;
@AllArgsConstructor
public enum StatusType implements EnumType {
ACTIVE("활성"),
INACTIVE("비활성"),
DELETED("삭제");
INACTIVE("미사용"),
PENDING("보류");
private final String desc;

View File

@@ -10,7 +10,9 @@ public enum AuthErrorCode implements ErrorCode {
LOGIN_PASSWORD_MISMATCH("LOGIN_PASSWORD_MISMATCH", HttpStatus.UNAUTHORIZED),
LOGIN_PASSWORD_EXCEEDED("LOGIN_PASSWORD_EXCEEDED", HttpStatus.UNAUTHORIZED);
LOGIN_PASSWORD_EXCEEDED("LOGIN_PASSWORD_EXCEEDED", HttpStatus.UNAUTHORIZED),
INACTIVE_ID("INACTIVE_ID", HttpStatus.UNAUTHORIZED);
private final String code;
private final HttpStatus status;

View File

@@ -0,0 +1,7 @@
package com.kamco.cd.kamcoback.common.exception;
public class DuplicateFileException extends RuntimeException {
public DuplicateFileException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package com.kamco.cd.kamcoback.common.exception;
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,32 @@
package com.kamco.cd.kamcoback.common.utils;
import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator;
import java.util.regex.Pattern;
import org.mindrot.jbcrypt.BCrypt;
public class CommonStringUtils {
/**
* 영문, 숫자, 특수문자를 모두 포함하여 8~20자 이내의 비밀번호
*
* @param password 벨리데이션 필요한 패스워드
* @return
*/
public static boolean isValidPassword(String password) {
String passwordPattern =
"^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$";
return Pattern.matches(passwordPattern, password);
}
/**
* 패스워드 암호화
*
* @param password 암호화 필요한 패스워드
* @param employeeNo salt 생성에 필요한 사원번호
* @return
*/
public static String hashPassword(String password, String employeeNo) {
String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(employeeNo.trim());
return BCrypt.hashpw(password.trim(), salt);
}
}

View File

@@ -142,34 +142,11 @@ public class FIleChecker {
return false;
}
String resStr = "";
boolean hasDriver = false;
// 리눅스/맥용
// ProcessBuilder pb = new ProcessBuilder("sh", "-c", "gdalinfo "+filePath+" | grep -i 'Geo'");
List<String> command = new ArrayList<>();
// 윈도우용
/*
command.add("cmd.exe"); // 윈도우 명령 프롬프트 실행
command.add("/c"); // 명령어를 수행하고 종료한다는 옵션
command.add("gdalinfo");
command.add(filePath);
command.add("|");
command.add("findstr");
command.add("/i");
command.add("Geo");
*/
command.add("sh"); // 리눅스,맥 명령 프롬프트 실행
command.add("-c"); // 명령어를 수행하고 종료한다는 옵션
command.add("gdalinfo");
command.add(filePath);
command.add("|");
command.add("grep");
command.add("-i");
command.add("Geo");
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.redirectErrorStream(true);
@@ -177,19 +154,17 @@ public class FIleChecker {
try {
Process process = processBuilder.start();
// 인코딩은 윈도우 한글 환경에 맞게 MS949로 지정
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
// System.out.println(line);
if (line.contains("Driver: GTiff/GeoTIFF")) {
hasDriver = true;
break;
}
}
int exitCode = process.waitFor();
process.waitFor();
} catch (Exception e) {
e.printStackTrace();

View File

@@ -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<String> 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<String> 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<String> 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<String> handlerEntityNotFoundException(
@@ -105,27 +161,6 @@ public class GlobalExceptionHandler {
errorLog.getId());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ApiResponseDto<String> 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)
public ApiResponseDto<String> handlerDataIntegrityViolationException(
@@ -435,23 +470,38 @@ public class GlobalExceptionHandler {
}
@ExceptionHandler(CustomApiException.class)
public ResponseEntity<ApiResponseDto<String>> handleCustomApiException(
public ApiResponseDto<String> handleCustomApiException(
CustomApiException e, HttpServletRequest request) {
log.warn("[CustomApiException] resource : {}", e.getMessage());
String codeName = e.getCodeName();
HttpStatus status = e.getStatus();
String message = e.getMessage() == null ? ApiResponseCode.getMessage(codeName) : e.getMessage();
ApiResponseCode apiCode = ApiResponseCode.getCode(codeName);
// String message = e.getMessage() == null ? ApiResponseCode.getMessage(codeName) :
// e.getMessage();
//
// ApiResponseCode apiCode = ApiResponseCode.getCode(codeName);
//
// ErrorLogEntity errorLog =
// saveErrorLogData(
// request, apiCode, status, ErrorLogDto.LogErrorLevel.WARNING, e.getStackTrace());
//
// ApiResponseDto<String> body =
// ApiResponseDto.createException(apiCode, message, status, errorLog.getId());
ErrorLogEntity errorLog =
saveErrorLogData(
request, apiCode, status, ErrorLogDto.LogErrorLevel.WARNING, e.getStackTrace());
request,
ApiResponseCode.getCode(codeName),
HttpStatus.valueOf(status.value()),
ErrorLogDto.LogErrorLevel.WARNING,
e.getStackTrace());
ApiResponseDto<String> body =
ApiResponseDto.createException(apiCode, message, status, errorLog.getId());
return ApiResponseDto.createException(
ApiResponseCode.getCode(codeName),
ApiResponseCode.getMessage(codeName),
HttpStatus.valueOf(status.value()),
errorLog.getId());
return new ResponseEntity<>(body, status);
// return new ResponseEntity<>(body, status);
}
}

View File

@@ -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")
@@ -54,6 +62,8 @@ public class SecurityConfig {
// ADMIN, REVIEWER 접근
.requestMatchers("/api/test/review")
.hasAnyRole("ADMIN", "REVIEWER")
.requestMatchers("/error")
.permitAll()
.requestMatchers(HttpMethod.OPTIONS, "/**")
.permitAll() // preflight 허용
.requestMatchers(
@@ -98,4 +108,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/**"));
}
}

View File

@@ -56,37 +56,38 @@ public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
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<MenuDto.Basic> result =
list.stream()
.map(
@@ -111,7 +112,6 @@ public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
servletRequest.getRequestURI(),
requestBody,
apiResponse.getErrorLogUid());
// tb_audit_log 테이블 저장
auditLogRepository.save(log);
}

View File

@@ -167,6 +167,7 @@ public class ApiResponseDto<T> {
LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."),
LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."),
LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."),
INACTIVE_ID("미사용 아이디 입니다."),
INVALID_EMAIL_TOKEN(
"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"

View File

@@ -26,18 +26,6 @@ public class MapSheetMngApiController {
private final CommonCodeService commonCodeService;
private final MapSheetMngService mapSheetMngService;
/**
* 오류데이터 목록 조회
*
* @param searchReq
* @return
*/
@PostMapping("/error-list")
public ApiResponseDto<Page<MapSheetMngDto.ErrorDataDto>> findMapSheetErrorList(
@RequestBody @Valid MapSheetMngDto.searchReq searchReq) {
return ApiResponseDto.ok(mapSheetMngService.findMapSheetErrorList(searchReq));
}
@Operation(summary = "영상데이터관리목록 조회", description = "영상데이터관리목록 조회")
@ApiResponses(
value = {
@@ -53,7 +41,7 @@ public class MapSheetMngApiController {
})
@PostMapping("/mng-list")
public ApiResponseDto<Page<MapSheetMngDto.MngDto>> findMapSheetMngList(
@RequestBody @Valid MapSheetMngDto.searchReq searchReq) {
@RequestBody MapSheetMngDto.MngSearchReq searchReq) {
return ApiResponseDto.ok(mapSheetMngService.findMapSheetMngList(searchReq));
}
@@ -77,6 +65,18 @@ public class MapSheetMngApiController {
return ApiResponseDto.ok(mapSheetMngService.mngDataSave(AddReq));
}
/**
* 오류데이터 목록 조회
*
* @param searchReq
* @return
*/
@PostMapping("/error-list")
public ApiResponseDto<Page<MapSheetMngDto.ErrorDataDto>> findMapSheetErrorList(
@RequestBody @Valid MapSheetMngDto.ErrorSearchReq searchReq) {
return ApiResponseDto.ok(mapSheetMngService.findMapSheetErrorList(searchReq));
}
/**
* @param hstUidList
* @return
@@ -86,7 +86,7 @@ public class MapSheetMngApiController {
value = {
@ApiResponse(
responseCode = "201",
description = "공통코드 저장 성공",
description = "업로드 처리 성공",
content =
@Content(
mediaType = "application/json",

View File

@@ -15,10 +15,14 @@ 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 lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
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;
@Tag(name = "영상 관리", description = "영상 관리 API")
@RestController
@@ -67,6 +71,45 @@ public class MapSheetMngFileCheckerApiController {
return ApiResponseDto.createOK(mapSheetMngFileCheckerService.getFilesAll(srchDto));
}
@Operation(summary = "파일 업로드", description = "파일 업로드 및 TIF 검증")
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponseDto<String> uploadFile(
@RequestPart("file") MultipartFile file,
@RequestParam("targetPath") String targetPath,
@RequestParam(value = "overwrite", required = false, defaultValue = "false")
boolean overwrite,
@RequestParam(value = "hstUid", required = false) Long hstUid) {
return ApiResponseDto.createOK(
mapSheetMngFileCheckerService.uploadFile(file, targetPath, overwrite, hstUid));
}
@Operation(summary = "페어 파일 업로드", description = "TFW/TIF 두 파일을 쌍으로 업로드 및 검증")
@PostMapping(value = "/upload-pair", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponseDto<String> uploadPair(
@RequestPart("tfw") MultipartFile tfwFile,
@RequestPart("tif") MultipartFile tifFile,
@RequestParam("targetPath") String targetPath,
@RequestParam(value = "overwrite", required = false, defaultValue = "false")
boolean overwrite,
@RequestParam(value = "hstUid", required = false) Long hstUid) {
return ApiResponseDto.createOK(
mapSheetMngFileCheckerService.uploadPair(tfwFile, tifFile, targetPath, overwrite, hstUid));
}
@Operation(summary = "파일 삭제", description = "중복 파일 등 파일 삭제")
@PostMapping("/delete")
public ApiResponseDto<Boolean> deleteFile(@RequestBody SrchFoldersDto dto) {
return ApiResponseDto.createOK(mapSheetMngFileCheckerService.deleteFile(dto.getDirPath()));
}
@Operation(summary = "중복 파일 삭제", description = "중복 데이터 발견 시 기존 데이터를 삭제")
@PostMapping(value = "/delete-file")
public ApiResponseDto<String> deleteDuplicateFile(
@RequestParam("filePath") String filePath, @RequestParam("fileName") String fileName) {
return ApiResponseDto.createOK(
mapSheetMngFileCheckerService.deleteDuplicate(filePath, fileName));
}
/*
@Operation(summary = "지정폴더(하위폴더포함) 파일목록 조회", description = "지정폴더(하위폴더포함) 파일목록 조회")
@ApiResponses(

View File

@@ -14,12 +14,78 @@ import org.springframework.data.domain.Sort;
public class MapSheetMngDto {
@Schema(name = "searchReq", description = "영상관리 오류데이터 검색 요청")
@Schema(name = "MngSearchReq", description = "영상관리 검색 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class searchReq {
public static class MngSearchReq {
// 페이징 파라미터
@Schema(description = "페이지 번호 (0부터 시작) ", example = "0")
private int page = 0;
@Schema(description = "페이지 크기", example = "20")
private int size = 20;
@Schema(description = "년도", example = "2025")
private Integer mngYyyy;
public Pageable toPageable() {
return PageRequest.of(page, size);
}
}
@Schema(name = "MngAddReq", description = "영상관리 생성 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class AddReq {
@Schema(description = "관리년도", example = "2022")
private int mngYyyy;
@Schema(description = "선택폴더경로", example = "D:\\app\\original-images\\2022")
private String mngPath;
}
@Schema(name = "DeleteFileReq", description = "파일 삭제 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class DeleteFileReq {
@Schema(description = "파일 경로", example = "/app/original-images/2024/00000001.tif")
private String filePath;
}
@Schema(name = "MngDto", description = "영상관리 검색 리턴")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class MngDto {
private int rowNum;
private int mngYyyy;
private String mngState;
private String syncState;
private String syncCheckState;
private Long syncTotCnt;
private Long syncStateDoneCnt;
private Long syncCheckStateDoneCnt;
private Long syncNotFileCnt;
private Long syncTypeErrorCnt;
private Long syncSizeErrorCnt;
@JsonFormatDttm private ZonedDateTime rgstStrtDttm;
@JsonFormatDttm private ZonedDateTime rgstEndDttm;
}
@Schema(name = "ErrorSearchReq", description = "영상관리 오류데이터 검색 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ErrorSearchReq {
// 페이징 파라미터
@Schema(description = "페이지 번호 (0부터 시작) ", example = "0")
@@ -64,40 +130,6 @@ public class MapSheetMngDto {
private DataState dataState;
}
@Schema(name = "MngAddReq", description = "영상관리 생성 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class AddReq {
@Schema(description = "관리년도", example = "2022")
private int mngYyyy;
@Schema(description = "선택폴더경로", example = "D:\\app\\original-images\\2022")
private String mngPath;
}
@Schema(name = "MngDto", description = "영상관리 검색 리턴")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class MngDto {
private int rowNum;
private int mngYyyy;
private String mngState;
private String syncState;
private String syncCheckState;
private Long syncTotCnt;
private Long syncStateDoneCnt;
private Long syncCheckStateDoneCnt;
private Long syncNotFileCnt;
private Long syncTypeErrorCnt;
private Long syncSizeErrorCnt;
@JsonFormatDttm private ZonedDateTime rgstStrtDttm;
@JsonFormatDttm private ZonedDateTime rgstEndDttm;
}
@Schema(name = "DmlReturn", description = "영상관리 DML 수행 후 리턴")
@Getter
@Setter

View File

@@ -2,6 +2,9 @@ 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;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto;
@@ -13,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;
@@ -32,6 +37,7 @@ import lombok.RequiredArgsConstructor;
import org.apache.commons.io.FilenameUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@Service
@RequiredArgsConstructor
@@ -40,6 +46,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) {
@@ -304,4 +314,283 @@ public class MapSheetMngFileCheckerService {
public ImageryDto.SyncReturn syncProcess(ImageryDto.searchReq searchReq) {
return mapSheetMngFileCheckerCoreService.syncProcess(searchReq);
}
@Transactional
public String uploadFile(MultipartFile file, String targetPath, boolean overwrite, Long hstUid) {
try {
Path path = Paths.get(targetPath);
if (Files.isDirectory(path)) {
path = path.resolve(file.getOriginalFilename());
}
if (path.getParent() != null) {
Files.createDirectories(path.getParent());
}
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");
// DB 중복 체크
String parentPathStr = path.getParent() != null ? path.getParent().toString() : "";
boolean dbExists =
mapSheetMngFileRepository.existsByFileNameAndFilePath(filename, parentPathStr);
// boolean fileExists = Files.exists(path); // 파일 시스템 존재 여부는 체크하지 않음 (DB 기준)
// 이미 존재하는 경우 처리 (DB에만 있는 경우 체크)
if (!overwrite && dbExists) {
throw new DuplicateFileException("동일한 파일이 이미 존재합니다 (DB): " + path.getFileName());
}
// 덮어쓰기인 경우 기존 DB 데이터 삭제 (새로 저장하기 위함)
if (overwrite && dbExists) {
mapSheetMngFileRepository.deleteByFileNameAndFilePath(filename, parentPathStr);
}
// 업로드 파일 저장(덮어쓰기 허용 시 replace)
file.transferTo(path.toFile());
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, hstUid);
return "TFW 업로드 성공 (매칭되는 TIF가 아직 없습니다).";
}
// TIF가 존재하면 쌍 요건 충족
saveUploadMeta(path, hstUid);
return "TFW 업로드 성공";
}
if ("tif".equals(ext) || "tiff".equals(ext)) {
// GDAL 검증 (항상 수행)
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, hstUid);
return "TIF 업로드 성공";
}
// 기타 확장자: 저장만 하고 메타 기록
saveUploadMeta(path, hstUid);
return "업로드 성공";
} catch (IOException e) {
throw new IllegalArgumentException("파일 I/O 처리 실패: " + e.getMessage());
}
}
@Transactional
public String uploadPair(
MultipartFile tfwFile,
MultipartFile tifFile,
String targetPath,
boolean overwrite,
Long hstUid) {
try {
Path basePath = Paths.get(targetPath);
if (Files.isDirectory(basePath)) {
// 디렉토리인 경우 파일명 기준으로 경로 생성
Path tfwPath = basePath.resolve(tfwFile.getOriginalFilename());
Path tifPath = basePath.resolve(tifFile.getOriginalFilename());
// 동일 베이스명 확인
String tfwBase = FilenameUtils.getBaseName(tfwPath.getFileName().toString());
String tifBase = FilenameUtils.getBaseName(tifPath.getFileName().toString());
if (!tfwBase.equalsIgnoreCase(tifBase)) {
throw new ValidationException("TFW/TIF 파일명이 동일한 베이스가 아닙니다.");
}
// 디렉토리 생성
if (tfwPath.getParent() != null) Files.createDirectories(tfwPath.getParent());
if (tifPath.getParent() != null) Files.createDirectories(tifPath.getParent());
// DB 중복 체크 및 overwrite 처리 (각 파일별)
String parentPathStr = basePath.toString();
String tfwName = tfwPath.getFileName().toString();
String tifName = tifPath.getFileName().toString();
boolean tfwDbExists =
mapSheetMngFileRepository.existsByFileNameAndFilePath(tfwName, parentPathStr);
boolean tifDbExists =
mapSheetMngFileRepository.existsByFileNameAndFilePath(tifName, parentPathStr);
if (!overwrite && (tfwDbExists || tifDbExists)) {
throw new DuplicateFileException("동일한 파일이 이미 존재합니다 (DB): " + tfwName + ", " + tifName);
}
if (overwrite) {
if (tfwDbExists)
mapSheetMngFileRepository.deleteByFileNameAndFilePath(tfwName, parentPathStr);
if (tifDbExists)
mapSheetMngFileRepository.deleteByFileNameAndFilePath(tifName, parentPathStr);
}
// 파일 저장
tfwFile.transferTo(tfwPath.toFile());
tifFile.transferTo(tifPath.toFile());
// 검증
boolean tfwOk = FIleChecker.checkTfw(tfwPath.toString());
if (!tfwOk) {
Files.deleteIfExists(tfwPath);
Files.deleteIfExists(tifPath);
throw new ValidationException("유효하지 않은 TFW 파일입니다 (6줄 숫자 형식 검증 실패): " + tfwName);
}
boolean isValidTif = FIleChecker.cmmndGdalInfo(tifPath.toString());
if (!isValidTif) {
Files.deleteIfExists(tfwPath);
Files.deleteIfExists(tifPath);
throw new ValidationException("유효하지 않은 TIF 파일입니다 (GDAL 검증 실패): " + tifName);
}
// 메타 저장 (두 파일 각각 저장)
saveUploadMeta(tfwPath, hstUid);
saveUploadMeta(tifPath, hstUid);
return "TFW/TIF 페어 업로드 성공";
} else {
throw new ValidationException("targetPath는 디렉토리여야 합니다.");
}
} catch (IOException e) {
throw new IllegalArgumentException("파일 I/O 처리 실패: " + e.getMessage());
}
}
private void saveUploadMeta(Path savedPath, Long hstUid) {
String fullPath = savedPath.toAbsolutePath().toString();
String fileName = savedPath.getFileName().toString();
String ext = FilenameUtils.getExtension(fileName);
MapSheetMngFileEntity entity = new MapSheetMngFileEntity();
if (hstUid != null) {
// 히스토리에서 메타 가져오기
var hstOpt = mapSheetMngFileCheckerCoreService.findHstByUid(hstUid);
hstOpt.ifPresent(
hst -> {
entity.setHstUid(hst.getHstUid());
entity.setMngYyyy(hst.getMngYyyy());
entity.setMapSheetNum(hst.getMapSheetNum());
entity.setRefMapSheetNum(hst.getRefMapSheetNum());
});
} else {
// 기존 추정 로직 유지
Integer mngYyyy = extractYearFromPath(fullPath);
String mapSheetNum = extractMapSheetNumFromFileName(fileName);
String refMapSheetNum = null;
if (mapSheetNum != null && !mapSheetNum.isEmpty()) {
try {
long num = Long.parseLong(mapSheetNum);
refMapSheetNum = String.valueOf(num / 1000);
} catch (NumberFormatException ignored) {
}
}
entity.setMngYyyy(mngYyyy);
entity.setMapSheetNum(mapSheetNum);
entity.setRefMapSheetNum(refMapSheetNum);
}
entity.setFilePath(savedPath.getParent() != null ? savedPath.getParent().toString() : "");
entity.setFileName(fileName);
entity.setFileExt(ext);
// 파일 크기 설정
try {
long size = Files.size(savedPath);
entity.setFileSize(size);
} catch (IOException e) {
entity.setFileSize(0L);
}
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 {
Path path = Paths.get(filePath);
return Files.deleteIfExists(path);
} catch (IOException e) {
throw new RuntimeException("파일 삭제 실패: " + e.getMessage());
}
}
@Transactional(readOnly = true)
public List<MapSheetMngFileEntity> findRecentFiles(int limit) {
// 간단히 전체를 불러 정렬/제한 (운영에선 Page 요청으로 변경 권장)
List<MapSheetMngFileEntity> 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;
}
@Transactional
public String deleteDuplicate(String filePath, String fileName) {
try {
Path path = Paths.get(filePath, fileName);
boolean deleted = Files.deleteIfExists(path);
// DB에서도 삭제
mapSheetMngFileRepository.deleteByFileNameAndFilePath(fileName, filePath);
return deleted ? "파일 및 DB 레코드 삭제 완료" : "DB 레코드 삭제 완료 (파일 미존재)";
} catch (IOException e) {
throw new RuntimeException("중복 파일 삭제 실패: " + e.getMessage());
}
}
}

View File

@@ -213,19 +213,30 @@ public class MapSheetMngService {
}
public Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList(
MapSheetMngDto.@Valid searchReq searchReq) {
MapSheetMngDto.@Valid ErrorSearchReq searchReq) {
return mapSheetMngCoreService.findMapSheetErrorList(searchReq);
}
public Page<MapSheetMngDto.MngDto> findMapSheetMngList(
MapSheetMngDto.@Valid searchReq searchReq) {
public Page<MapSheetMngDto.MngDto> findMapSheetMngList(MapSheetMngDto.MngSearchReq searchReq) {
return mapSheetMngCoreService.findMapSheetMngList(searchReq);
}
public MapSheetMngDto.DmlReturn mngDataSave(@Valid MapSheetMngDto.AddReq AddReq) {
@Transactional
public MapSheetMngDto.DmlReturn mngDataSave(MapSheetMngDto.AddReq AddReq) {
return mapSheetMngCoreService.mngDataSave(AddReq);
}
/*
public MapSheetMngDto.DmlReturn uploadFile(MultipartFile file, Long hstUid) {
return mapSheetMngCoreService.uploadFile(file, hstUid);
}
public MapSheetMngDto.DmlReturn deleteFile(MapSheetMngDto.DeleteFileReq req) {
return mapSheetMngCoreService.deleteFile(req);
}
*/
public MapSheetMngDto.DmlReturn uploadProcess(@Valid List<Long> hstUidList) {
return mapSheetMngCoreService.uploadProcess(hstUidList);
}

View File

@@ -12,7 +12,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
@@ -87,24 +86,4 @@ public class AdminApiController {
adminService.updateMembers(uuid, updateReq);
return ApiResponseDto.createOK(UUID.randomUUID());
}
@Operation(summary = "관리자 계정 미사용 처리", description = "관리자 계정 미사용 처리")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "관리자 계정 미사용 처리",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/delete/{uuid}")
public ApiResponseDto<UUID> deleteAccount(@PathVariable UUID uuid) {
adminService.deleteAccount(uuid);
return ApiResponseDto.createOK(uuid);
}
}

View File

@@ -4,8 +4,6 @@ import com.kamco.cd.kamcoback.auth.CustomUserDetails;
import com.kamco.cd.kamcoback.auth.JwtTokenProvider;
import com.kamco.cd.kamcoback.auth.RefreshTokenService;
import com.kamco.cd.kamcoback.common.enums.StatusType;
import com.kamco.cd.kamcoback.common.enums.error.AuthErrorCode;
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.members.dto.MembersDto;
import com.kamco.cd.kamcoback.members.dto.SignInRequest;
@@ -112,20 +110,13 @@ public class AuthController {
Authentication authentication = null;
MembersDto.Member member = new MembersDto.Member();
// 비활성 상태면 임시패스워드를 비교함
if (StatusType.INACTIVE.getId().equals(status)) {
if (!authService.isTempPasswordValid(request)) {
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
}
} else {
authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(), request.getPassword()));
}
authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
// INACTIVE 비활성 상태(새로운 패스워드 입력 해야함), DELETED 탈퇴
if (!StatusType.ACTIVE.getId().equals(status)) {
// PENDING 비활성 상태(새로운 패스워드 입력 해야함)
if (StatusType.PENDING.getId().equals(status)) {
member.setEmployeeNo(request.getUsername());
return ApiResponseDto.ok(new TokenResponse(status, null, null, member));
}

View File

@@ -15,7 +15,6 @@ import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Page;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -70,10 +69,6 @@ public class MembersApiController {
@PatchMapping("/{memberId}/password")
public ApiResponseDto<String> resetPassword(
@PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword()));
membersService.resetPassword(memberId, initReq);
return ApiResponseDto.createOK(memberId);
}

View File

@@ -28,9 +28,7 @@ public class MembersDto {
private String userRole;
private String userRoleName;
private String name;
private String userId;
private String employeeNo;
private String tempPassword;
private String status;
private String statusName;
@JsonFormatDttm private ZonedDateTime createdDttm;
@@ -43,9 +41,7 @@ public class MembersDto {
UUID uuid,
String userRole,
String name,
String userId,
String employeeNo,
String tempPassword,
String status,
ZonedDateTime createdDttm,
ZonedDateTime updatedDttm,
@@ -56,9 +52,7 @@ public class MembersDto {
this.userRole = userRole;
this.userRoleName = getUserRoleName(userRole);
this.name = name;
this.userId = userId;
this.employeeNo = employeeNo;
this.tempPassword = tempPassword;
this.status = status;
this.statusName = getStatusName(status);
this.createdDttm = createdDttm;
@@ -108,32 +102,28 @@ public class MembersDto {
@Schema(description = "관리자 유형", example = "ADMIN")
@NotBlank
@EnumValid(enumClass = RoleType.class, message = "userRole은 ADMIN, LABELER, REVIEWER만 가능합니다.")
@EnumValid(enumClass = RoleType.class, message = "userRole은 ADMIN, LABELER, REVIEWER 만 가능합니다.")
private String userRole;
@Schema(description = "사번", example = "K20251212001")
@Size(max = 50)
private String employeeNo;
@Schema(description = "이름", example = "홍길동")
@NotBlank
@Size(min = 2, max = 100)
private String name;
@Schema(description = "ID", example = "gildong")
@NotBlank
@Size(min = 2, max = 50)
private String userId;
@Schema(description = "패스워드", example = "")
@Size(max = 255)
private String password;
@Schema(description = "임시 비밀번호", example = "q!w@e#r4")
private String tempPassword;
@Schema(description = "사번", example = "123456")
private String employeeNo;
public AddReq(
String userRole, String name, String userId, String tempPassword, String employeeNo) {
public AddReq(String userRole, String employeeNo, String name, String password) {
this.userRole = userRole;
this.name = name;
this.userId = userId;
this.tempPassword = tempPassword;
this.employeeNo = employeeNo;
this.name = name;
this.password = password;
}
}
@@ -141,22 +131,22 @@ public class MembersDto {
@Setter
public static class UpdateReq {
@Schema(description = "사번, 패스워드 변경시 필수 값", example = "11111")
@Size(max = 50)
private String employeeNo;
@Schema(description = "이름", example = "홍길동")
@Size(min = 2, max = 100)
private String name;
@Schema(description = "상태", example = "ACTIVE")
@EnumValid(enumClass = StatusType.class, message = "status는 ACTIVE, INACTIVE, DELETED 만 가능합니다.")
private String status;
@Schema(description = "패스워드", example = "")
@Size(max = 255)
private String tempPassword;
private String password;
public UpdateReq(String employeeNo, String name, String tempPassword) {
this.employeeNo = employeeNo;
public UpdateReq(String name, String status, String password) {
this.name = name;
this.tempPassword = tempPassword;
this.status = status;
this.password = password;
}
}
@@ -164,14 +154,15 @@ public class MembersDto {
@Setter
public static class InitReq {
@Schema(description = "변경 패스워드", example = "")
@Schema(description = "기존 패스워드", example = "")
@Size(max = 255)
@NotBlank
private String password;
private String oldPassword;
@Schema(description = "초기 패스워드", example = "")
@Schema(description = "신규 패스워드", example = "")
@Size(max = 255)
@NotBlank
private String tempPassword;
private String newPassword;
}
@Getter

View File

@@ -11,6 +11,7 @@ public class MemberException {
public enum Field {
USER_ID,
EMPLOYEE_NO,
DEFAULT
}

View File

@@ -1,9 +1,12 @@
package com.kamco.cd.kamcoback.members.service;
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
import com.kamco.cd.kamcoback.common.utils.CommonStringUtils;
import com.kamco.cd.kamcoback.members.dto.MembersDto;
import com.kamco.cd.kamcoback.postgres.core.MembersCoreService;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -22,6 +25,10 @@ public class AdminService {
*/
@Transactional
public Long saveMember(MembersDto.AddReq addReq) {
if (!CommonStringUtils.isValidPassword(addReq.getPassword())) {
throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST);
}
return membersCoreService.saveMembers(addReq);
}
@@ -35,14 +42,4 @@ public class AdminService {
public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) {
membersCoreService.updateMembers(uuid, updateReq);
}
/**
* 관리자 계정 미사용 처리
*
* @param uuid
*/
@Transactional
public void deleteAccount(UUID uuid) {
membersCoreService.deleteAccount(uuid);
}
}

View File

@@ -1,10 +1,10 @@
package com.kamco.cd.kamcoback.members.service;
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
import com.kamco.cd.kamcoback.common.utils.CommonStringUtils;
import com.kamco.cd.kamcoback.members.dto.MembersDto;
import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic;
import com.kamco.cd.kamcoback.postgres.core.MembersCoreService;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
@@ -37,21 +37,9 @@ public class MembersService {
@Transactional
public void resetPassword(String id, MembersDto.InitReq initReq) {
if (!isValidPassword(initReq.getPassword())) {
if (!CommonStringUtils.isValidPassword(initReq.getNewPassword())) {
throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST);
}
membersCoreService.resetPassword(id, initReq);
}
/**
* 영문, 숫자, 특수문자를 모두 포함하여 8~20자 이내의 비밀번호
*
* @param password
* @return
*/
private boolean isValidPassword(String password) {
String passwordPattern =
"^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$";
return Pattern.matches(passwordPattern, password);
}
}

View File

@@ -2,12 +2,12 @@ package com.kamco.cd.kamcoback.postgres.core;
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto;
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto.Basic;
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto.SearchReq;
import com.kamco.cd.kamcoback.common.service.BaseCoreService;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto.ApiResponseCode;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto.ResponseObj;
import com.kamco.cd.kamcoback.postgres.entity.CommonCodeEntity;
import com.kamco.cd.kamcoback.postgres.repository.code.CommonCodeRepository;
import com.kamco.cd.kamcoback.zoo.dto.AnimalDto.SearchReq;
import jakarta.persistence.EntityNotFoundException;
import java.util.List;
import java.util.Optional;

View File

@@ -33,12 +33,12 @@ public class MapSheetMngCoreService {
private String activeEnv;
public Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList(
MapSheetMngDto.@Valid searchReq searchReq) {
MapSheetMngDto.@Valid ErrorSearchReq searchReq) {
return mapSheetMngRepository.findMapSheetErrorList(searchReq);
}
public Page<MapSheetMngDto.MngDto> findMapSheetMngList(
MapSheetMngDto.@Valid searchReq searchReq) {
MapSheetMngDto.@Valid MngSearchReq searchReq) {
return mapSheetMngRepository.findMapSheetMngList(searchReq);
}
@@ -52,10 +52,7 @@ public class MapSheetMngCoreService {
.findMapSheetMngHstInfo(hstUid)
.orElseThrow(EntityNotFoundException::new));
// TODO: local TEST 시 각자 경로 수정하기
// TODO: application.yml 에 active profile : local 로 임시 변경하여 테스트
String localPath = "";
// String localPath = "C:\\Users\\gypark\\Desktop\\file";
String rootDir = ORIGINAL_IMAGES_PATH + "/" + entity.get().getMngYyyy();
if (activeEnv.equals("local")) {
rootDir = localPath + rootDir;
@@ -68,6 +65,59 @@ public class MapSheetMngCoreService {
count += 1;
}
// 파일 크기 계산 및 저장
try (Stream<Path> paths = Files.walk(Paths.get(rootDir))) {
List<Path> matched =
paths
.filter(Files::isRegularFile)
.filter(
p -> {
String name = p.getFileName().toString();
return name.equals(filename + ".tif") || name.equals(filename + ".tfw");
})
.collect(Collectors.toList());
long tifSize =
matched.stream()
.filter(p -> p.getFileName().toString().endsWith(".tif"))
.mapToLong(
p -> {
try {
return Files.size(p);
} catch (IOException e) {
return 0L;
}
})
.sum();
long tfwSize =
matched.stream()
.filter(p -> p.getFileName().toString().endsWith(".tfw"))
.mapToLong(
p -> {
try {
return Files.size(p);
} catch (IOException e) {
return 0L;
}
})
.sum();
entity.get().setTifSizeBytes(tifSize);
entity.get().setTfwSizeBytes(tfwSize);
entity.get().setTotalSizeBytes(tifSize + tfwSize);
// 엔터티 저장 -> 커스텀 업데이트로 변경
mapSheetMngRepository.updateHstFileSizes(
entity.get().getHstUid(), tifSize, tfwSize, tifSize + tfwSize);
} catch (IOException e) {
// 크기 계산 실패 시 0으로 저장
entity.get().setTifSizeBytes(0L);
entity.get().setTfwSizeBytes(0L);
entity.get().setTotalSizeBytes(0L);
mapSheetMngRepository.updateHstFileSizes(entity.get().getHstUid(), 0L, 0L, 0L);
}
/*
MapSheetMngDto.DataState dataState =
flag ? MapSheetMngDto.DataState.SUCCESS : MapSheetMngDto.DataState.FAIL;
@@ -132,7 +182,10 @@ public class MapSheetMngCoreService {
entity.setMngYyyy(addReq.getMngYyyy());
entity.setMngPath(addReq.getMngPath());
mapSheetMngRepository.deleteByMngYyyyMngAll(addReq.getMngYyyy());
MapSheetMngEntity saved = mapSheetMngRepository.save(entity);
int hstCnt = mapSheetMngRepository.insertMapSheetOrgDataToMapSheetMngHst(saved.getMngYyyy());
return new MapSheetMngDto.DmlReturn("success", saved.getMngYyyy().toString());
}

View File

@@ -2,7 +2,10 @@ package com.kamco.cd.kamcoback.postgres.core;
import com.kamco.cd.kamcoback.common.utils.FIleChecker;
import com.kamco.cd.kamcoback.mapsheet.dto.ImageryDto;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetMngHstEntity;
import com.kamco.cd.kamcoback.postgres.repository.mapsheet.MapSheetMngFileCheckerRepository;
import com.kamco.cd.kamcoback.postgres.repository.mapsheet.MapSheetMngRepository;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
@@ -13,6 +16,7 @@ import org.springframework.stereotype.Service;
public class MapSheetMngFileCheckerCoreService {
private final MapSheetMngFileCheckerRepository mapSheetMngFileCheckerRepository;
private final MapSheetMngRepository mapSheetMngRepository;
private static final String ORIGINAL_IMAGES_PATH = "/app/original-images";
@@ -50,4 +54,8 @@ public class MapSheetMngFileCheckerCoreService {
return new ImageryDto.SyncReturn(flag, syncCnt, tfwErrCnt, tifErrCnt);
}
public Optional<MapSheetMngHstEntity> findHstByUid(Long hstUid) {
return mapSheetMngRepository.findMapSheetMngHstInfo(hstUid);
}
}

View File

@@ -2,6 +2,9 @@ package com.kamco.cd.kamcoback.postgres.core;
import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator;
import com.kamco.cd.kamcoback.common.enums.StatusType;
import com.kamco.cd.kamcoback.common.enums.error.AuthErrorCode;
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
import com.kamco.cd.kamcoback.common.utils.CommonStringUtils;
import com.kamco.cd.kamcoback.common.utils.UserUtil;
import com.kamco.cd.kamcoback.members.dto.MembersDto;
import com.kamco.cd.kamcoback.members.dto.MembersDto.AddReq;
@@ -18,6 +21,7 @@ import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
@Service
@@ -34,23 +38,23 @@ public class MembersCoreService {
* @return
*/
public Long saveMembers(AddReq addReq) {
if (membersRepository.existsByUserId(addReq.getUserId())) {
throw new DuplicateMemberException(Field.USER_ID, addReq.getUserId());
if (membersRepository.existsByEmployeeNo(addReq.getEmployeeNo())) {
throw new DuplicateMemberException(Field.EMPLOYEE_NO, addReq.getEmployeeNo());
}
// salt 생성, 사번이 salt
String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(addReq.getEmployeeNo().trim());
// 패스워드 암호화, 초기 패스워드 고정
String hashedPassword = BCrypt.hashpw(addReq.getTempPassword(), salt);
String hashedPassword = BCrypt.hashpw(addReq.getPassword(), salt);
MemberEntity memberEntity = new MemberEntity();
memberEntity.setUserId(addReq.getUserId());
memberEntity.setUserId(addReq.getEmployeeNo());
memberEntity.setUserRole(addReq.getUserRole());
memberEntity.setTempPassword(addReq.getTempPassword().trim()); // 임시 패스워드는 암호화 하지 않음
memberEntity.setPassword(hashedPassword);
memberEntity.setName(addReq.getName());
memberEntity.setEmployeeNo(addReq.getEmployeeNo());
memberEntity.setRgstrUidl(userUtil.getId());
memberEntity.setStatus(StatusType.PENDING.getId());
return membersRepository.save(memberEntity).getId();
}
@@ -69,38 +73,25 @@ public class MembersCoreService {
memberEntity.setName(updateReq.getName());
}
// 임시 패스워드는 암호화 하지 않음
if (StringUtils.isNotBlank(updateReq.getTempPassword())) {
// 임시 패스워드가 기존과 다르면 패스워드 변경으로 처리함
// 상태 INACTIVE로 변경하여 사용자가 로그인할때 패스워드 변경하게함
// 패스워드 리셋이므로 로그인 실패카운트 초기화처리함
if (!memberEntity.getTempPassword().equals(updateReq.getTempPassword().trim())) {
memberEntity.setStatus(StatusType.INACTIVE.getId());
memberEntity.setLoginFailCount(0);
if (StringUtils.isNotBlank(updateReq.getStatus())) {
memberEntity.changeStatus(updateReq.getStatus());
}
if (StringUtils.isNotBlank(updateReq.getPassword())) {
// 패스워드 유효성 검사
if (!CommonStringUtils.isValidPassword(updateReq.getPassword())) {
throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST);
}
memberEntity.setTempPassword(updateReq.getTempPassword().trim());
String password =
CommonStringUtils.hashPassword(updateReq.getPassword(), memberEntity.getEmployeeNo());
memberEntity.setStatus(StatusType.PENDING.getId());
memberEntity.setLoginFailCount(0);
memberEntity.setPassword(password);
memberEntity.setPwdResetYn("Y");
}
if (StringUtils.isNotBlank(memberEntity.getEmployeeNo())) {
memberEntity.setEmployeeNo(updateReq.getEmployeeNo());
}
memberEntity.setUpdtrUid(userUtil.getId());
membersRepository.save(memberEntity);
}
/**
* 관리자 계정 삭제 처리
*
* @param uuid
*/
public void deleteAccount(UUID uuid) {
MemberEntity memberEntity =
membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException());
memberEntity.setStatus(StatusType.DELETED.getId());
memberEntity.setUpdatedDttm(ZonedDateTime.now());
memberEntity.setUpdtrUid(userUtil.getId());
membersRepository.save(memberEntity);
}
@@ -112,17 +103,21 @@ public class MembersCoreService {
*/
public void resetPassword(String id, MembersDto.InitReq initReq) {
MemberEntity memberEntity =
membersRepository.findByUserId(id).orElseThrow(() -> new MemberNotFoundException());
membersRepository.findByEmployeeNo(id).orElseThrow(() -> new MemberNotFoundException());
String salt =
BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim());
// 패스워드 암호화
String hashedPassword = BCrypt.hashpw(initReq.getPassword(), salt);
// 기존 패스워드 확인
if (!BCrypt.checkpw(initReq.getOldPassword(), memberEntity.getPassword())) {
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
}
memberEntity.setPassword(hashedPassword);
memberEntity.setStatus("ACTIVE");
String password =
CommonStringUtils.hashPassword(initReq.getOldPassword(), memberEntity.getEmployeeNo());
memberEntity.setPassword(password);
memberEntity.setStatus(StatusType.ACTIVE.getId());
memberEntity.setUpdatedDttm(ZonedDateTime.now());
memberEntity.setUpdtrUid(memberEntity.getId());
memberEntity.setPwdResetYn("N");
membersRepository.save(memberEntity);
}

View File

@@ -0,0 +1,53 @@
package com.kamco.cd.kamcoback.postgres.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "tb_map_sheet_mng_files")
public class MapSheetMngFileEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "file_uid", nullable = false)
private Long fileUid;
@NotNull
@Column(name = "mng_yyyy", nullable = false)
private Integer mngYyyy;
@NotNull
@Column(name = "map_sheet_num", nullable = false)
private String mapSheetNum;
@Column(name = "ref_map_sheet_num")
private String refMapSheetNum;
@Size(max = 255)
@Column(name = "file_path")
private String filePath;
@Size(max = 100)
@Column(name = "file_name", length = 100)
private String fileName;
@Size(max = 20)
@Column(name = "file_ext", length = 20)
private String fileExt;
@Column(name = "hst_uid")
private Long hstUid;
@Column(name = "file_size")
private Long fileSize;
}

View File

@@ -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;
@@ -78,4 +78,13 @@ public class MapSheetMngHstEntity extends CommonDateEntity {
@Column(name = "sync_check_end_dttm")
private ZonedDateTime syncCheckEndDttm;
@Column(name = "tif_size_bytes")
private Long tifSizeBytes;
@Column(name = "tfw_size_bytes")
private Long tfwSizeBytes;
@Column(name = "total_size_bytes")
private Long totalSizeBytes;
}

View File

@@ -1,53 +0,0 @@
package com.kamco.cd.kamcoback.postgres.entity;
import jakarta.persistence.Column;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;
@Getter
@Setter
@Entity
@Table(name = "tb_member_archived")
public class MemberArchivedEntity {
@EmbeddedId private MemberArchivedEntityId id;
@Size(max = 50)
@Column(name = "employee_no", length = 50)
private String employeeNo;
@Size(max = 100)
@NotNull
@Column(name = "name", nullable = false, length = 100)
private String name;
@Size(max = 255)
@NotNull
@Column(name = "password", nullable = false)
private String password;
@Size(max = 100)
@NotNull
@Column(name = "email", nullable = false, length = 100)
private String email;
@Size(max = 20)
@Column(name = "status", length = 20)
private String status;
@NotNull
@Column(name = "created_dttm", nullable = false)
private ZonedDateTime createdDttm;
@NotNull
@ColumnDefault("now()")
@Column(name = "archived_dttm", nullable = false)
private ZonedDateTime archivedDttm;
}

View File

@@ -1,44 +0,0 @@
package com.kamco.cd.kamcoback.postgres.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Objects;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.Hibernate;
@Getter
@Setter
@Embeddable
public class MemberArchivedEntityId implements Serializable {
private static final long serialVersionUID = -7102800377481389036L;
@NotNull
@Column(name = "user_id", nullable = false)
private Long userId;
@NotNull
@Column(name = "uuid", nullable = false)
private UUID uuid;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) {
return false;
}
MemberArchivedEntityId entity = (MemberArchivedEntityId) o;
return Objects.equals(this.userId, entity.userId) && Objects.equals(this.uuid, entity.uuid);
}
@Override
public int hashCode() {
return Objects.hash(userId, uuid);
}
}

View File

@@ -9,11 +9,14 @@ import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.sql.Types;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.JdbcTypeCode;
@Getter
@Setter
@@ -62,9 +65,8 @@ public class MemberEntity {
private String password;
@Size(max = 20)
@ColumnDefault("'INACTIVE'")
@Column(name = "status", length = 20)
private String status = StatusType.INACTIVE.getId();
private String status = StatusType.PENDING.getId();
@NotNull
@ColumnDefault("now()")
@@ -90,4 +92,20 @@ public class MemberEntity {
@Column(name = "updtr_uid")
private Long updtrUid;
@Column(name = "status_chg_dttm")
private ZonedDateTime statusChgDttm;
@JdbcTypeCode(Types.CHAR)
@Column(name = "pwd_reset_yn", columnDefinition = "CHAR(1)")
private String pwdResetYn;
public void changeStatus(String newStatus) {
// 같은 값 보내도 무시
if (Objects.equals(this.status, newStatus)) {
return;
}
this.status = newStatus;
this.statusChgDttm = ZonedDateTime.now();
}
}

View File

@@ -0,0 +1,10 @@
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<MapSheetMngFileEntity, Long> {
boolean existsByFileNameAndFilePath(String fileName, String filePath);
void deleteByFileNameAndFilePath(String fileName, String filePath);
}

View File

@@ -8,10 +8,22 @@ import org.springframework.data.domain.Page;
public interface MapSheetMngRepositoryCustom {
Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList(
MapSheetMngDto.@Valid searchReq searchReq);
Page<MapSheetMngDto.MngDto> findMapSheetMngList(MapSheetMngDto.@Valid searchReq searchReq);
Page<MapSheetMngDto.MngDto> findMapSheetMngList(MapSheetMngDto.MngSearchReq searchReq);
Optional<MapSheetMngHstEntity> findMapSheetMngHstInfo(Long hstUid);
int insertMapSheetOrgDataToMapSheetMngHst(int mngYyyy);
void deleteByMngYyyyMngAll(int mngYyyy);
void deleteByMngYyyyMng(int mngYyyy);
void deleteByMngYyyyMngHst(int mngYyyy);
void deleteByMngYyyyMngFile(int mngYyyy);
Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList(
MapSheetMngDto.@Valid ErrorSearchReq searchReq);
void updateHstFileSizes(Long hstUid, long tifSizeBytes, long tfwSizeBytes, long totalSizeBytes);
}

View File

@@ -3,6 +3,7 @@ package com.kamco.cd.kamcoback.postgres.repository.mapsheet;
import static com.kamco.cd.kamcoback.postgres.entity.QMapInkx50kEntity.mapInkx50kEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QMapInkx5kEntity.mapInkx5kEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngEntity.mapSheetMngEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngFileEntity.mapSheetMngFileEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngHstEntity.mapSheetMngHstEntity;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto;
@@ -15,10 +16,13 @@ import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.core.types.dsl.StringExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.hibernate.query.Query;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
@@ -31,67 +35,15 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
private final JPAQueryFactory queryFactory;
private final StringExpression NULL_STRING = Expressions.stringTemplate("cast(null as text)");
@PersistenceContext private EntityManager em;
public MapSheetMngRepositoryImpl(JPAQueryFactory queryFactory) {
super(MapSheetMngHstEntity.class);
this.queryFactory = queryFactory;
}
@Override
public Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList(
MapSheetMngDto.@Valid searchReq searchReq) {
Pageable pageable = PageRequest.of(searchReq.getPage(), searchReq.getSize());
List<MapSheetMngDto.ErrorDataDto> foundContent =
queryFactory
.select(
Projections.constructor(
MapSheetMngDto.ErrorDataDto.class,
mapSheetMngHstEntity.hstUid,
rowNum(),
Expressions.stringTemplate(
"concat({0}, {1})",
mapSheetMngHstEntity.mapSheetName, mapInkx50kEntity.mapidcdNo),
Expressions.stringTemplate(
"concat({0}, substring({1}, {2}, {3}))",
mapSheetMngHstEntity.mapSheetName, mapSheetMngHstEntity.mapSheetNum, 6, 8),
mapSheetMngHstEntity.mapSheetCodeSrc,
Expressions.stringTemplate(
"to_char({0}, 'YYYY-MM-DD')", mapSheetMngHstEntity.createdDate),
mapSheetMngHstEntity.dataState))
.from(mapSheetMngHstEntity)
.innerJoin(mapInkx5kEntity)
.on(mapSheetMngHstEntity.mapSheetCode.eq(mapInkx5kEntity.fid))
.leftJoin(mapInkx50kEntity)
.on(mapInkx5kEntity.fidK50.eq(mapInkx50kEntity.fid.longValue()))
.where(
mapSheetMngHstEntity.mngYyyy.eq(searchReq.getMngYyyy()),
// mapSheetMngHstEntity.dataState.eq(MapSheetMngDto.DataState.FAIL), // 오류만 검색
mapSheetErrorSearchValue(searchReq))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(mapSheetMngHstEntity.createdDate.desc())
.fetch();
Long countQuery =
queryFactory
.select(mapSheetMngHstEntity.hstUid.count())
.from(mapSheetMngHstEntity)
.innerJoin(mapInkx5kEntity)
.on(mapSheetMngHstEntity.mapSheetCode.eq(mapInkx5kEntity.fid))
.leftJoin(mapInkx50kEntity)
.on(mapInkx5kEntity.fidK50.eq(mapInkx50kEntity.fid.longValue()))
.where(
mapSheetMngHstEntity.mngYyyy.eq(searchReq.getMngYyyy()),
// mapSheetMngHstEntity.dataState.eq(MapSheetMngDto.DataState.FAIL), // 오류만 검색
mapSheetErrorSearchValue(searchReq))
.fetchOne();
return new PageImpl<>(foundContent, pageable, countQuery);
}
@Override
public Page<MapSheetMngDto.MngDto> findMapSheetMngList(
MapSheetMngDto.@Valid searchReq searchReq) {
public Page<MapSheetMngDto.MngDto> findMapSheetMngList(MapSheetMngDto.MngSearchReq searchReq) {
Pageable pageable = searchReq.toPageable();
BooleanBuilder whereBuilder = new BooleanBuilder();
@@ -100,27 +52,6 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
whereBuilder.and(mapSheetMngEntity.mngYyyy.eq(searchReq.getMngYyyy()));
}
/*
QMapSheetMngEntity m = mapSheetMngEntity;
QMapSheetMngHstEntity h = mapSheetMngHstEntity;
List<MapSheetSummaryDto> summaryContent =
queryFactory
.select(
Projections.constructor(
MapSheetSummaryDto.class,
mapSheetMngHstEntity.mngYyyy,
mapSheetMngHstEntity.mngYyyy.count().as("syncTotCnt"),
new CaseBuilder()
.when(mapSheetMngHstEntity.syncState.eq("DONE")).then(1L).otherwise(0L)
.sum().as("syncStateDoneCnt")
))
.from(mapSheetMngHstEntity)
.groupBy(mapSheetMngHstEntity.mngYyyy) // mng_yyyy 별로 그룹핑
.fetch();
*/
List<MapSheetMngDto.MngDto> foundContent =
queryFactory
.select(
@@ -183,6 +114,149 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
return new PageImpl<>(foundContent, pageable, countQuery);
}
@Override
public Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList(
MapSheetMngDto.@Valid ErrorSearchReq searchReq) {
Pageable pageable = PageRequest.of(searchReq.getPage(), searchReq.getSize());
List<MapSheetMngDto.ErrorDataDto> foundContent =
queryFactory
.select(
Projections.constructor(
MapSheetMngDto.ErrorDataDto.class,
mapSheetMngHstEntity.hstUid,
rowNum(),
Expressions.stringTemplate(
"concat({0}, {1})",
mapSheetMngHstEntity.mapSheetName, mapInkx50kEntity.mapidcdNo),
Expressions.stringTemplate(
"concat({0}, substring({1}, {2}, {3}))",
mapSheetMngHstEntity.mapSheetName, mapSheetMngHstEntity.mapSheetNum, 6, 8),
mapSheetMngHstEntity.mapSheetCodeSrc,
Expressions.stringTemplate(
"to_char({0}, 'YYYY-MM-DD')", mapSheetMngHstEntity.createdDate),
mapSheetMngHstEntity.dataState))
.from(mapSheetMngHstEntity)
.innerJoin(mapInkx5kEntity)
.on(mapSheetMngHstEntity.mapSheetCode.eq(mapInkx5kEntity.fid))
.leftJoin(mapInkx50kEntity)
.on(mapInkx5kEntity.fidK50.eq(mapInkx50kEntity.fid.longValue()))
.where(
mapSheetMngHstEntity.mngYyyy.eq(searchReq.getMngYyyy()),
// mapSheetMngHstEntity.dataState.eq(MapSheetMngDto.DataState.FAIL), // 오류만 검색
mapSheetErrorSearchValue(searchReq))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(mapSheetMngHstEntity.createdDate.desc())
.fetch();
Long countQuery =
queryFactory
.select(mapSheetMngHstEntity.hstUid.count())
.from(mapSheetMngHstEntity)
.innerJoin(mapInkx5kEntity)
.on(mapSheetMngHstEntity.mapSheetCode.eq(mapInkx5kEntity.fid))
.leftJoin(mapInkx50kEntity)
.on(mapInkx5kEntity.fidK50.eq(mapInkx50kEntity.fid.longValue()))
.where(
mapSheetMngHstEntity.mngYyyy.eq(searchReq.getMngYyyy()),
// mapSheetMngHstEntity.dataState.eq(MapSheetMngDto.DataState.FAIL), // 오류만 검색
mapSheetErrorSearchValue(searchReq))
.fetchOne();
return new PageImpl<>(foundContent, pageable, countQuery);
}
@Override
public void deleteByMngYyyyMngAll(int mngYyyy) {
long deletedFileCount =
queryFactory
.delete(mapSheetMngFileEntity)
.where(mapSheetMngFileEntity.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();
}
@Override
public void deleteByMngYyyyMng(int mngYyyy) {
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();
}
@Override
public void deleteByMngYyyyMngFile(int mngYyyy) {
long deletedFileCount =
queryFactory
.delete(mapSheetMngFileEntity)
.where(mapSheetMngFileEntity.mngYyyy.eq(mngYyyy))
.execute();
}
@Override
public int insertMapSheetOrgDataToMapSheetMngHst(int mngYyyy) {
String sql =
"""
INSERT INTO tb_map_sheet_mng_hst
(
mng_yyyy
,map_sheet_code
,map_sheet_num
,map_sheet_name
,map_sheet_code_src
,scale_ratio
,ref_map_sheet_num
,use_inference
)
select
:mngYyyy as mng_yyyy
,fid as map_sheet_code
,mapidcd_no::INTEGER as map_sheet_num
,mapid_nm as map_sheet_name
,fid as map_sheet_code_src
,5000 as scale_ratio
,((mapidcd_no::INTEGER)/1000) as ref_map_sheet_num
,use_inference
from
tb_map_inkx_5k
""";
// Native Query 생성 및 실행
Query query = (Query) em.createNativeQuery(sql);
query.setParameter("mngYyyy", mngYyyy);
int exeCnt = query.executeUpdate(); // 실행 (영향받은 행의 개수 반환)
return exeCnt;
}
@Override
public Optional<MapSheetMngHstEntity> findMapSheetMngHstInfo(Long hstUid) {
return Optional.ofNullable(
@@ -192,12 +266,25 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
.fetchOne());
}
@Override
public void updateHstFileSizes(
Long hstUid, long tifSizeBytes, long tfwSizeBytes, long totalSizeBytes) {
String sql =
"UPDATE tb_map_sheet_mng_hst SET tif_size_bytes = :tif, tfw_size_bytes = :tfw, total_size_bytes = :tot WHERE hst_uid = :uid";
Query query = (Query) em.createNativeQuery(sql);
query.setParameter("tif", tifSizeBytes);
query.setParameter("tfw", tfwSizeBytes);
query.setParameter("tot", totalSizeBytes);
query.setParameter("uid", hstUid);
query.executeUpdate();
}
private NumberExpression<Integer> rowNum() {
return Expressions.numberTemplate(
Integer.class, "row_number() over(order by {0} desc)", mapSheetMngHstEntity.createdDate);
}
private BooleanExpression mapSheetErrorSearchValue(MapSheetMngDto.searchReq searchReq) {
private BooleanExpression mapSheetErrorSearchValue(MapSheetMngDto.ErrorSearchReq searchReq) {
if (Objects.isNull(searchReq.getSearchValue())) {
return null;
}

View File

@@ -1,7 +0,0 @@
package com.kamco.cd.kamcoback.postgres.repository.members;
import com.kamco.cd.kamcoback.postgres.entity.MemberArchivedEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MembersArchivedRepository
extends JpaRepository<MemberArchivedEntity, Long>, MembersArchivedRepositoryCustom {}

View File

@@ -1,3 +0,0 @@
package com.kamco.cd.kamcoback.postgres.repository.members;
public interface MembersArchivedRepositoryCustom {}

View File

@@ -1,6 +0,0 @@
package com.kamco.cd.kamcoback.postgres.repository.members;
import org.springframework.stereotype.Repository;
@Repository
public class MembersArchivedRepositoryImpl implements MembersArchivedRepositoryCustom {}

View File

@@ -11,6 +11,10 @@ public interface MembersRepositoryCustom {
boolean existsByUserId(String userId);
boolean existsByEmployeeNo(String employeeNo);
Optional<MemberEntity> findByEmployeeNo(String employeeNo);
Optional<MemberEntity> findByUserId(String userId);
Optional<MemberEntity> findByUUID(UUID uuid);

View File

@@ -40,6 +40,22 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom {
!= null;
}
/**
* 사용자 사번 조회
*
* @param employeeNo
* @return
*/
@Override
public boolean existsByEmployeeNo(String employeeNo) {
return queryFactory
.selectOne()
.from(memberEntity)
.where(memberEntity.employeeNo.eq(employeeNo))
.fetchFirst()
!= null;
}
/**
* 사용자 조회 user id
*
@@ -52,6 +68,21 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom {
queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne());
}
/**
* 사용자 조회 employeed no
*
* @param employeeNo
* @return
*/
@Override
public Optional<MemberEntity> findByEmployeeNo(String employeeNo) {
return Optional.ofNullable(
queryFactory
.selectFrom(memberEntity)
.where(memberEntity.employeeNo.eq(employeeNo))
.fetchOne());
}
/**
* 회원정보 목록 조회
*

View File

@@ -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

View File

@@ -6,7 +6,7 @@ spring:
jpa:
show-sql: true
hibernate:
ddl-auto: validate # 스키마 검증만 수행, 자동 변경하지 않음
ddl-auto: validate # 로컬만 완화(시킬려면 update으로 변경)
properties:
hibernate:
default_batch_fetch_size: 100 # ✅ 성능 - N+1 쿼리 방지
@@ -16,7 +16,7 @@ spring:
datasource:
url: jdbc:postgresql://192.168.2.127:15432/kamco_cds
#url: jdbc:postgresql://localhost:5432/kamco_cds
#url: jdbc:postgresql://localhost:15432/kamco_cds
username: kamco_cds
password: kamco_cds_Q!W@E#R$
hikari:

View File

@@ -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

View File

@@ -1,66 +0,0 @@
-- Fix timestamp column type conversion issue
-- Run this if the Hibernate automatic schema update still fails
-- For tb_map_sheet_anal_data
ALTER TABLE tb_map_sheet_anal_data
ALTER COLUMN anal_end_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING anal_end_dttm::TIMESTAMP WITH TIME ZONE;
ALTER TABLE tb_map_sheet_anal_data
ALTER COLUMN anal_strt_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING anal_strt_dttm::TIMESTAMP WITH TIME ZONE;
ALTER TABLE tb_map_sheet_anal_data
ALTER COLUMN created_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING created_dttm::TIMESTAMP WITH TIME ZONE;
ALTER TABLE tb_map_sheet_anal_data
ALTER COLUMN updated_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING updated_dttm::TIMESTAMP WITH TIME ZONE;
ALTER TABLE tb_map_sheet_anal_data
ALTER COLUMN data_state_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING data_state_dttm::TIMESTAMP WITH TIME ZONE;
-- For tb_map_sheet_learn_data
ALTER TABLE tb_map_sheet_learn_data
ALTER COLUMN anal_end_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING anal_end_dttm::TIMESTAMP WITH TIME ZONE;
ALTER TABLE tb_map_sheet_learn_data
ALTER COLUMN anal_strt_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING anal_strt_dttm::TIMESTAMP WITH TIME ZONE;
ALTER TABLE tb_map_sheet_learn_data
ALTER COLUMN created_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING created_dttm::TIMESTAMP WITH TIME ZONE;
ALTER TABLE tb_map_sheet_learn_data
ALTER COLUMN updated_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING updated_dttm::TIMESTAMP WITH TIME ZONE;
ALTER TABLE tb_map_sheet_learn_data
ALTER COLUMN data_state_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING data_state_dttm::TIMESTAMP WITH TIME ZONE;
ALTER TABLE tb_map_sheet_learn_data
ALTER COLUMN gukuin_used_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING gukuin_used_dttm::TIMESTAMP WITH TIME ZONE;
-- For tb_map_sheet_learn_data_geom
ALTER TABLE tb_map_sheet_learn_data_geom
ALTER COLUMN created_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING created_dttm::TIMESTAMP WITH TIME ZONE;
ALTER TABLE tb_map_sheet_learn_data_geom
ALTER COLUMN updated_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING updated_dttm::TIMESTAMP WITH TIME ZONE;
-- For tb_map_sheet_anal_data_geom
ALTER TABLE tb_map_sheet_anal_data_geom
ALTER COLUMN created_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING created_dttm::TIMESTAMP WITH TIME ZONE;
ALTER TABLE tb_map_sheet_anal_data_geom
ALTER COLUMN updated_dttm SET DATA TYPE TIMESTAMP WITH TIME ZONE
USING updated_dttm::TIMESTAMP WITH TIME ZONE;

View File

@@ -1,97 +0,0 @@
-- GeoJSON 모니터링 시스템을 위한 필수 테이블 생성 스크립트
-- dump-kamco_cds-202511201730.sql에서 추출
-- 1. 시퀀스 생성
CREATE SEQUENCE IF NOT EXISTS public.tb_map_sheet_learn_data_data_uid
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
-- 2. tb_map_sheet_learn_data 테이블 생성
CREATE TABLE IF NOT EXISTS public.tb_map_sheet_learn_data (
data_uid bigint DEFAULT nextval('public.tb_map_sheet_learn_data_data_uid'::regclass) NOT NULL,
data_name character varying(128),
data_path character varying(255),
data_type character varying(128),
data_crs_type character varying(128),
data_crs_type_name character varying(255),
created_dttm timestamp without time zone DEFAULT now(),
created_uid bigint,
updated_dttm timestamp without time zone DEFAULT now(),
updated_uid bigint,
compare_yyyy integer,
data_yyyy integer,
data_json json,
data_state character varying(20),
data_state_dttm timestamp without time zone DEFAULT now(),
data_title character varying(255),
anal_map_sheet character varying(255),
anal_strt_dttm timestamp without time zone,
anal_end_dttm time without time zone,
anal_sec bigint,
gukuin_used character varying(20),
gukuin_used_dttm timestamp without time zone,
anal_state character varying(20),
CONSTRAINT tb_map_sheet_learn_data_pkey PRIMARY KEY (data_uid)
);
-- 3. 시퀀스 생성 (Geometry용)
CREATE SEQUENCE IF NOT EXISTS public.tb_map_sheet_learn_data_geom_geom_uid
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
-- 4. tb_map_sheet_learn_data_geom 테이블 생성
CREATE TABLE IF NOT EXISTS public.tb_map_sheet_learn_data_geom (
geo_uid bigint DEFAULT nextval('public.tb_map_sheet_learn_data_geom_geom_uid'::regclass) NOT NULL,
cd_prob double precision,
class_before_name character varying(100),
class_before_prob double precision,
class_after_name character varying(100),
class_after_prob double precision,
map_sheet_num bigint,
before_yyyy integer,
after_yyyy integer,
area double precision,
geom public.geometry,
geo_type character varying(100),
data_uid bigint,
created_dttm timestamp without time zone,
created_uid bigint,
updated_dttm timestamp without time zone,
updated_uid bigint,
CONSTRAINT tb_map_sheet_learn_data_geom_pkey PRIMARY KEY (geo_uid)
);
-- 5. 외래 키 제약 조건
ALTER TABLE ONLY public.tb_map_sheet_learn_data_geom
ADD CONSTRAINT fk_learn_data_geom_data_uid
FOREIGN KEY (data_uid) REFERENCES public.tb_map_sheet_learn_data(data_uid) ON DELETE CASCADE;
-- 6. 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_tb_map_sheet_learn_data_data_state ON public.tb_map_sheet_learn_data(data_state);
CREATE INDEX IF NOT EXISTS idx_tb_map_sheet_learn_data_anal_state ON public.tb_map_sheet_learn_data(anal_state);
CREATE INDEX IF NOT EXISTS idx_tb_map_sheet_learn_data_data_path ON public.tb_map_sheet_learn_data(data_path);
CREATE INDEX IF NOT EXISTS idx_tb_map_sheet_learn_data_geom_data_uid ON public.tb_map_sheet_learn_data_geom(data_uid);
CREATE INDEX IF NOT EXISTS idx_tb_map_sheet_learn_data_geom_geo_type ON public.tb_map_sheet_learn_data_geom(geo_type);
-- 7. 테이블 코멘트
COMMENT ON TABLE public.tb_map_sheet_learn_data IS '학습데이터';
COMMENT ON COLUMN public.tb_map_sheet_learn_data.data_uid IS '식별키';
COMMENT ON COLUMN public.tb_map_sheet_learn_data.data_name IS '데이타명';
COMMENT ON COLUMN public.tb_map_sheet_learn_data.data_path IS '경로';
COMMENT ON COLUMN public.tb_map_sheet_learn_data.data_type IS '타입';
COMMENT ON COLUMN public.tb_map_sheet_learn_data.data_state IS '처리상태';
COMMENT ON COLUMN public.tb_map_sheet_learn_data.anal_state IS '분석상태';
COMMENT ON TABLE public.tb_map_sheet_learn_data_geom IS '학습데이터GEOM정보';
COMMENT ON COLUMN public.tb_map_sheet_learn_data_geom.geo_uid IS '식별키';
COMMENT ON COLUMN public.tb_map_sheet_learn_data_geom.geom IS 'geometry정보';
COMMENT ON COLUMN public.tb_map_sheet_learn_data_geom.data_uid IS '데이터식별키';
-- 완료 메시지
SELECT 'GeoJSON 모니터링 시스템 테이블 생성 완료' as message;

View File

@@ -1,32 +0,0 @@
-- PostGIS extension 및 기본 설정 확인
-- 이 스크립트를 PostgreSQL에서 실행하여 PostGIS가 설치되어 있는지 확인
-- 1. PostGIS extension 설치 (이미 설치되어 있다면 무시됨)
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;
-- 2. 현재 설치된 확장 확인
SELECT name, default_version, installed_version
FROM pg_available_extensions
WHERE name LIKE '%postgis%';
-- 3. Geometry 타입이 사용 가능한지 확인
SELECT typname
FROM pg_type
WHERE typname = 'geometry';
-- 4. 테스트용 geometry 컬럼 생성 확인
DO $$
BEGIN
-- 임시 테스트 테이블로 geometry 타입 확인
DROP TABLE IF EXISTS temp_geom_test;
CREATE TEMP TABLE temp_geom_test (
id serial,
test_geom geometry(Point, 4326)
);
RAISE NOTICE 'PostGIS geometry 타입이 정상적으로 작동합니다.';
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'PostGIS 설정에 문제가 있습니다: %', SQLERRM;
END
$$;

View File

@@ -1,41 +0,0 @@
-- Fix geometry column type in tb_map_sheet_learn_data_geom table
-- The table was incorrectly created with 'bytea' type instead of 'geometry' type
-- 1. First ensure PostGIS is enabled
CREATE EXTENSION IF NOT EXISTS postgis;
-- 2. Check if column needs to be recreated (only if it's bytea type)
-- Only clear data if the column type is incorrect and needs conversion
DO $$
BEGIN
-- Only delete data if geom column exists and is bytea type
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'tb_map_sheet_learn_data_geom'
AND column_name = 'geom'
AND data_type = 'bytea'
) THEN
DELETE FROM public.tb_map_sheet_learn_data_geom WHERE geom IS NOT NULL;
RAISE NOTICE 'Cleared incorrect bytea geometry data for conversion';
ELSE
RAISE NOTICE 'Geometry column is already correct type, skipping data deletion';
END IF;
END $$;
-- 3. Drop and recreate the geom column with correct PostGIS geometry type (only if needed)
ALTER TABLE public.tb_map_sheet_learn_data_geom DROP COLUMN IF EXISTS geom;
ALTER TABLE public.tb_map_sheet_learn_data_geom ADD COLUMN geom geometry(Polygon, 5186);
-- 4. Create spatial index for performance
CREATE INDEX IF NOT EXISTS idx_tb_map_sheet_learn_data_geom_spatial
ON public.tb_map_sheet_learn_data_geom USING GIST (geom);
-- 5. Update column comment
COMMENT ON COLUMN public.tb_map_sheet_learn_data_geom.geom IS 'PostGIS geometry 정보 (Polygon, EPSG:5186)';
-- 6. Verify the column type is correct
SELECT column_name, data_type, udt_name
FROM information_schema.columns
WHERE table_name = 'tb_map_sheet_learn_data_geom' AND column_name = 'geom';
SELECT 'Geometry column type fixed successfully' as message;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long