From a731ab19e8b9366c41f865f770381e9f8d7ecad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=98=81?= Date: Mon, 26 May 2025 12:19:48 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BB=AC=EB=A0=89=EC=85=98=20=EB=B0=8F=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=EA=B4=80=EB=A0=A8=20=EC=9C=A0=ED=8B=B8?= =?UTF-8?q?=EB=A6=AC=ED=8B=B0=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/main/java/egovframework/util/CollectionUtil.java: - 컬렉션 관련 유틸리티 메소드 추가 - Null 체크(isEmpty, isNotEmpty) 및 크기 반환(size) 메소드 - 변환 메소드(toList, toSet) - 필터링, 매핑, 정렬과 같은 스트림 처리 메소드(filter, map, sort 등) - 합집합, 교집합, 차집합 및 그룹화 기능(union, intersection, difference, groupBy 등) src/main/java/egovframework/util/DateUtil.java: - 날짜 및 시간 관련 유틸리티 메소드 추가 - 현재 날짜/시간 반환 메소드(getCurrentYearMonth, getCurrentDateTime 등) - 형식 변환 및 파싱 메소드(parseLocalDate, formatLocalDate 등) - 날짜 추가/계산(addDays, daysBetween 등) 및 범위 처리(periodBetween 등) - 요일, 월의 첫날 및 마지막 날 계산(getDayOfWeekName, getFirstDayOfMonth 등) - 공통적으로 null 안전성 및 유효성 검사에 초점. --- qodana.yaml | 31 ++ .../config/FileUploadProperties.java | 47 ++ .../egovframework/util/CollectionUtil.java | 440 ++++++++++++++++++ .../java/egovframework/util/DateUtil.java | 410 ++++++++++++++++ .../java/egovframework/util/FileUtil.java | 108 ++++- .../java/egovframework/util/NumberUtil.java | 352 ++++++++++++++ .../java/egovframework/util/StringUtil.java | 218 +++++++++ .../egovframework/util/ValidationUtil.java | 340 ++++++++++++++ .../service/impl/BbsNoticeServiceImpl.java | 4 +- .../service/impl/HtmlEditorServiceImpl.java | 8 +- src/main/resources/application-dev.yml | 7 +- src/main/resources/application-local.yml | 7 +- src/main/resources/application-prd.yml | 7 +- 13 files changed, 1954 insertions(+), 25 deletions(-) create mode 100644 qodana.yaml create mode 100644 src/main/java/egovframework/config/FileUploadProperties.java create mode 100644 src/main/java/egovframework/util/CollectionUtil.java create mode 100644 src/main/java/egovframework/util/DateUtil.java create mode 100644 src/main/java/egovframework/util/NumberUtil.java create mode 100644 src/main/java/egovframework/util/StringUtil.java create mode 100644 src/main/java/egovframework/util/ValidationUtil.java diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..b804e63 --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,31 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +projectJDK: "8" #(Applied in CI/CD pipeline) + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-jvm:2025.1 diff --git a/src/main/java/egovframework/config/FileUploadProperties.java b/src/main/java/egovframework/config/FileUploadProperties.java new file mode 100644 index 0000000..fce7fff --- /dev/null +++ b/src/main/java/egovframework/config/FileUploadProperties.java @@ -0,0 +1,47 @@ +package egovframework.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +/** + * packageName : egovframework.config + * fileName : FileUploadProperties + * author : 시스템 관리자 + * date : 25. 5. 23. + * description : 파일 업로드 관련 설정 속성 + * =========================================================== + * DATE AUTHOR NOTE + * ----------------------------------------------------------- + * 25. 5. 23. 시스템 관리자 최초 생성 + */ +@Setter +@Getter +@Configuration +@ConfigurationProperties(prefix = "file.upload") +public class FileUploadProperties { + + /** 파일 저장 기본 경로 */ + private String path; + + /** 최대 파일 크기 (단일 파일) - 기본값 10MB */ + private long maxSize; + + /** 최대 총 파일 크기 - 기본값 50MB */ + private long maxTotalSize; + + /** 허용된 파일 확장자 */ + private String allowedExtensions; + + /** 최대 파일 개수 - 기본값 10개 */ + private int maxFiles; + + /** 실제 파일 삭제 여부 - 기본값 true */ + private boolean realFileDelete; + + /** 하위 디렉토리 설정 */ + private Map subDirs; +} \ No newline at end of file diff --git a/src/main/java/egovframework/util/CollectionUtil.java b/src/main/java/egovframework/util/CollectionUtil.java new file mode 100644 index 0000000..0d145b5 --- /dev/null +++ b/src/main/java/egovframework/util/CollectionUtil.java @@ -0,0 +1,440 @@ +package egovframework.util; + +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * 컬렉션 관련 공통 유틸리티 클래스 + */ +@Component +public class CollectionUtil { + + /** + * 컬렉션이 null이거나 비어있는지 확인 + * @param collection 검사할 컬렉션 + * @return null이거나 비어있으면 true, 그렇지 않으면 false + */ + public static boolean isEmpty(Collection collection) { + return collection == null || collection.isEmpty(); + } + + /** + * 컬렉션이 null이 아니고 비어있지 않은지 확인 + * @param collection 검사할 컬렉션 + * @return null이 아니고 비어있지 않으면 true, 그렇지 않으면 false + */ + public static boolean isNotEmpty(Collection collection) { + return !isEmpty(collection); + } + + /** + * 맵이 null이거나 비어있는지 확인 + * @param map 검사할 맵 + * @return null이거나 비어있으면 true, 그렇지 않으면 false + */ + public static boolean isEmpty(Map map) { + return map == null || map.isEmpty(); + } + + /** + * 맵이 null이 아니고 비어있지 않은지 확인 + * @param map 검사할 맵 + * @return null이 아니고 비어있지 않으면 true, 그렇지 않으면 false + */ + public static boolean isNotEmpty(Map map) { + return !isEmpty(map); + } + + /** + * 배열이 null이거나 비어있는지 확인 + * @param array 검사할 배열 + * @return null이거나 비어있으면 true, 그렇지 않으면 false + */ + public static boolean isEmpty(T[] array) { + return array == null || array.length == 0; + } + + /** + * 배열이 null이 아니고 비어있지 않은지 확인 + * @param array 검사할 배열 + * @return null이 아니고 비어있지 않으면 true, 그렇지 않으면 false + */ + public static boolean isNotEmpty(T[] array) { + return !isEmpty(array); + } + + /** + * 컬렉션의 크기 반환 (null 안전) + * @param collection 검사할 컬렉션 + * @return 컬렉션의 크기, null이면 0 반환 + */ + public static int size(Collection collection) { + return collection == null ? 0 : collection.size(); + } + + /** + * 맵의 크기 반환 (null 안전) + * @param map 검사할 맵 + * @return 맵의 크기, null이면 0 반환 + */ + public static int size(Map map) { + return map == null ? 0 : map.size(); + } + + /** + * 배열의 크기 반환 (null 안전) + * @param array 검사할 배열 + * @return 배열의 크기, null이면 0 반환 + */ + public static int size(T[] array) { + return array == null ? 0 : array.length; + } + + /** + * 컬렉션이 null이면 빈 컬렉션 반환, 그렇지 않으면 원래 컬렉션 반환 + * @param collection 처리할 컬렉션 + * @return null이면 빈 컬렉션, 그렇지 않으면 원래 컬렉션 + */ + public static Collection emptyIfNull(Collection collection) { + return collection == null ? Collections.emptyList() : collection; + } + + /** + * 리스트가 null이면 빈 리스트 반환, 그렇지 않으면 원래 리스트 반환 + * @param list 처리할 리스트 + * @return null이면 빈 리스트, 그렇지 않으면 원래 리스트 + */ + public static List emptyIfNull(List list) { + return list == null ? Collections.emptyList() : list; + } + + /** + * 맵이 null이면 빈 맵 반환, 그렇지 않으면 원래 맵 반환 + * @param map 처리할 맵 + * @return null이면 빈 맵, 그렇지 않으면 원래 맵 + */ + public static Map emptyIfNull(Map map) { + return map == null ? Collections.emptyMap() : map; + } + + /** + * 배열을 리스트로 변환 (null 안전) + * @param array 변환할 배열 + * @return 변환된 리스트, null이면 빈 리스트 반환 + */ + public static List toList(T[] array) { + if (array == null) { + return Collections.emptyList(); + } + return Arrays.asList(array); + } + + /** + * 컬렉션을 리스트로 변환 (null 안전) + * @param collection 변환할 컬렉션 + * @return 변환된 리스트, null이면 빈 리스트 반환 + */ + public static List toList(Collection collection) { + if (collection == null) { + return Collections.emptyList(); + } + return new ArrayList<>(collection); + } + + /** + * 컬렉션을 Set으로 변환 (null 안전) + * @param collection 변환할 컬렉션 + * @return 변환된 Set, null이면 빈 Set 반환 + */ + public static Set toSet(Collection collection) { + if (collection == null) { + return Collections.emptySet(); + } + return new HashSet<>(collection); + } + + /** + * 배열을 Set으로 변환 (null 안전) + * @param array 변환할 배열 + * @return 변환된 Set, null이면 빈 Set 반환 + */ + public static Set toSet(T[] array) { + if (array == null) { + return Collections.emptySet(); + } + Set set = new HashSet<>(); + Collections.addAll(set, array); + return set; + } + + /** + * 컬렉션에서 조건에 맞는 요소만 필터링 + * @param collection 필터링할 컬렉션 + * @param predicate 필터링 조건 + * @return 필터링된 리스트 + */ + public static List filter(Collection collection, Predicate predicate) { + if (isEmpty(collection)) { + return Collections.emptyList(); + } + return collection.stream() + .filter(predicate) + .collect(Collectors.toList()); + } + + /** + * 컬렉션의 요소를 변환 + * @param collection 변환할 컬렉션 + * @param mapper 변환 함수 + * @return 변환된 리스트 + */ + public static List map(Collection collection, Function mapper) { + if (isEmpty(collection)) { + return Collections.emptyList(); + } + return collection.stream() + .map(mapper) + .collect(Collectors.toList()); + } + + /** + * 컬렉션을 정렬 + * @param collection 정렬할 컬렉션 + * @param comparator 정렬 기준 + * @return 정렬된 리스트 + */ + public static List sort(Collection collection, Comparator comparator) { + if (isEmpty(collection)) { + return Collections.emptyList(); + } + List list = new ArrayList<>(collection); + list.sort(comparator); + return list; + } + + /** + * 컬렉션을 자연 순서로 정렬 (Comparable 구현 필요) + * @param collection 정렬할 컬렉션 + * @return 정렬된 리스트 + */ + public static > List sort(Collection collection) { + if (isEmpty(collection)) { + return Collections.emptyList(); + } + List list = new ArrayList<>(collection); + Collections.sort(list); + return list; + } + + /** + * 컬렉션에서 중복 제거 + * @param collection 중복 제거할 컬렉션 + * @return 중복이 제거된 리스트 + */ + public static List distinct(Collection collection) { + if (isEmpty(collection)) { + return Collections.emptyList(); + } + return collection.stream() + .distinct() + .collect(Collectors.toList()); + } + + /** + * 컬렉션을 특정 키를 기준으로 그룹화 + * @param collection 그룹화할 컬렉션 + * @param keyMapper 그룹화 키 추출 함수 + * @return 그룹화된 맵 + */ + public static Map> groupBy(Collection collection, Function keyMapper) { + if (isEmpty(collection)) { + return Collections.emptyMap(); + } + return collection.stream() + .collect(Collectors.groupingBy(keyMapper)); + } + + /** + * 컬렉션을 특정 키를 기준으로 맵으로 변환 + * @param collection 변환할 컬렉션 + * @param keyMapper 키 추출 함수 + * @return 변환된 맵 + */ + public static Map toMap(Collection collection, Function keyMapper) { + if (isEmpty(collection)) { + return Collections.emptyMap(); + } + return collection.stream() + .collect(Collectors.toMap(keyMapper, Function.identity(), (a, b) -> a)); + } + + /** + * 컬렉션을 특정 키와 값을 기준으로 맵으로 변환 + * @param collection 변환할 컬렉션 + * @param keyMapper 키 추출 함수 + * @param valueMapper 값 추출 함수 + * @return 변환된 맵 + */ + public static Map toMap(Collection collection, Function keyMapper, Function valueMapper) { + if (isEmpty(collection)) { + return Collections.emptyMap(); + } + return collection.stream() + .collect(Collectors.toMap(keyMapper, valueMapper, (a, b) -> a)); + } + + /** + * 두 컬렉션의 합집합 반환 + * @param collection1 첫 번째 컬렉션 + * @param collection2 두 번째 컬렉션 + * @return 합집합 리스트 + */ + public static List union(Collection collection1, Collection collection2) { + Set set = new HashSet<>(); + if (isNotEmpty(collection1)) { + set.addAll(collection1); + } + if (isNotEmpty(collection2)) { + set.addAll(collection2); + } + return new ArrayList<>(set); + } + + /** + * 두 컬렉션의 교집합 반환 + * @param collection1 첫 번째 컬렉션 + * @param collection2 두 번째 컬렉션 + * @return 교집합 리스트 + */ + public static List intersection(Collection collection1, Collection collection2) { + if (isEmpty(collection1) || isEmpty(collection2)) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + Set set = new HashSet<>(collection2); + + for (T item : collection1) { + if (set.contains(item)) { + result.add(item); + } + } + + return result; + } + + /** + * 두 컬렉션의 차집합 반환 (collection1 - collection2) + * @param collection1 첫 번째 컬렉션 + * @param collection2 두 번째 컬렉션 + * @return 차집합 리스트 + */ + public static List difference(Collection collection1, Collection collection2) { + if (isEmpty(collection1)) { + return Collections.emptyList(); + } + if (isEmpty(collection2)) { + return new ArrayList<>(collection1); + } + + List result = new ArrayList<>(collection1); + result.removeAll(new HashSet<>(collection2)); + return result; + } + + /** + * 컬렉션을 지정된 크기의 하위 리스트로 분할 + * @param collection 분할할 컬렉션 + * @param size 하위 리스트의 크기 + * @return 분할된 하위 리스트의 리스트 + */ + public static List> partition(Collection collection, int size) { + if (isEmpty(collection)) { + return Collections.emptyList(); + } + if (size <= 0) { + throw new IllegalArgumentException("Size must be greater than 0"); + } + + List> result = new ArrayList<>(); + List list = new ArrayList<>(collection); + int total = list.size(); + + for (int i = 0; i < total; i += size) { + result.add(list.subList(i, Math.min(total, i + size))); + } + + return result; + } + + /** + * 컬렉션에서 첫 번째 요소 반환 (null 안전) + * @param collection 처리할 컬렉션 + * @return 첫 번째 요소, 없으면 null 반환 + */ + public static T getFirst(Collection collection) { + if (isEmpty(collection)) { + return null; + } + return collection.iterator().next(); + } + + /** + * 리스트에서 마지막 요소 반환 (null 안전) + * @param list 처리할 리스트 + * @return 마지막 요소, 없으면 null 반환 + */ + public static T getLast(List list) { + if (isEmpty(list)) { + return null; + } + return list.get(list.size() - 1); + } + + /** + * 맵에서 키에 해당하는 값 반환 (null 안전) + * @param map 처리할 맵 + * @param key 키 + * @param defaultValue 기본값 + * @return 키에 해당하는 값, 없으면 기본값 반환 + */ + public static V getOrDefault(Map map, K key, V defaultValue) { + if (isEmpty(map)) { + return defaultValue; + } + return map.getOrDefault(key, defaultValue); + } + + /** + * 두 맵을 병합 + * @param map1 첫 번째 맵 + * @param map2 두 번째 맵 + * @return 병합된 맵 + */ + public static Map merge(Map map1, Map map2) { + Map result = new HashMap<>(); + + if (isNotEmpty(map1)) { + result.putAll(map1); + } + + if (isNotEmpty(map2)) { + result.putAll(map2); + } + + return result; + } +} diff --git a/src/main/java/egovframework/util/DateUtil.java b/src/main/java/egovframework/util/DateUtil.java new file mode 100644 index 0000000..c208a7e --- /dev/null +++ b/src/main/java/egovframework/util/DateUtil.java @@ -0,0 +1,410 @@ +package egovframework.util; + +import org.springframework.stereotype.Component; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Period; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.TextStyle; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; +import java.util.Date; +import java.util.Locale; + +/** + * 날짜 관련 공통 유틸리티 클래스 + */ +@Component +public class DateUtil { + + /** + * 현재 날짜의 년월 정보를 "yyyyMM" 형식으로 반환 + * @return 년월 문자열 (예: "202405") + */ + public String getCurrentYearMonth() { + LocalDate now = LocalDate.now(); + return now.format(DateTimeFormatter.ofPattern("yyyyMM")); + } + + /** + * 현재 날짜를 지정된 형식으로 반환 + * @param pattern 날짜 형식 (예: "yyyy-MM-dd", "yyyyMMdd", "yyyy/MM/dd") + * @return 형식화된 날짜 문자열 + */ + public String getCurrentDate(String pattern) { + LocalDate now = LocalDate.now(); + return now.format(DateTimeFormatter.ofPattern(pattern)); + } + + /** + * 현재 날짜와 시간을 지정된 형식으로 반환 + * @param pattern 날짜 형식 (예: "yyyy-MM-dd HH:mm:ss", "yyyyMMddHHmmss") + * @return 형식화된 날짜 문자열 + */ + public String getCurrentDateTime(String pattern) { + LocalDateTime now = LocalDateTime.now(); + return now.format(DateTimeFormatter.ofPattern(pattern)); + } + + /** + * 문자열을 LocalDate 객체로 변환 + * @param dateStr 날짜 문자열 + * @param pattern 날짜 형식 (예: "yyyy-MM-dd", "yyyyMMdd") + * @return 변환된 LocalDate 객체, 변환 실패 시 null 반환 + */ + public static LocalDate parseLocalDate(String dateStr, String pattern) { + if (dateStr == null || pattern == null) { + return null; + } + try { + return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(pattern)); + } catch (Exception e) { + return null; + } + } + + /** + * 문자열을 LocalDateTime 객체로 변환 + * @param dateTimeStr 날짜 시간 문자열 + * @param pattern 날짜 시간 형식 (예: "yyyy-MM-dd HH:mm:ss", "yyyyMMddHHmmss") + * @return 변환된 LocalDateTime 객체, 변환 실패 시 null 반환 + */ + public static LocalDateTime parseLocalDateTime(String dateTimeStr, String pattern) { + if (dateTimeStr == null || pattern == null) { + return null; + } + try { + return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern(pattern)); + } catch (Exception e) { + return null; + } + } + + /** + * LocalDate 객체를 문자열로 변환 + * @param date LocalDate 객체 + * @param pattern 날짜 형식 (예: "yyyy-MM-dd", "yyyyMMdd") + * @return 변환된 날짜 문자열, 변환 실패 시 null 반환 + */ + public static String formatLocalDate(LocalDate date, String pattern) { + if (date == null || pattern == null) { + return null; + } + return date.format(DateTimeFormatter.ofPattern(pattern)); + } + + /** + * LocalDateTime 객체를 문자열로 변환 + * @param dateTime LocalDateTime 객체 + * @param pattern 날짜 시간 형식 (예: "yyyy-MM-dd HH:mm:ss", "yyyyMMddHHmmss") + * @return 변환된 날짜 시간 문자열, 변환 실패 시 null 반환 + */ + public static String formatLocalDateTime(LocalDateTime dateTime, String pattern) { + if (dateTime == null || pattern == null) { + return null; + } + return dateTime.format(DateTimeFormatter.ofPattern(pattern)); + } + + /** + * Date 객체를 LocalDate 객체로 변환 + * @param date Date 객체 + * @return 변환된 LocalDate 객체, 변환 실패 시 null 반환 + */ + public static LocalDate toLocalDate(Date date) { + if (date == null) { + return null; + } + return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + } + + /** + * Date 객체를 LocalDateTime 객체로 변환 + * @param date Date 객체 + * @return 변환된 LocalDateTime 객체, 변환 실패 시 null 반환 + */ + public static LocalDateTime toLocalDateTime(Date date) { + if (date == null) { + return null; + } + return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); + } + + /** + * LocalDate 객체를 Date 객체로 변환 + * @param localDate LocalDate 객체 + * @return 변환된 Date 객체, 변환 실패 시 null 반환 + */ + public static Date toDate(LocalDate localDate) { + if (localDate == null) { + return null; + } + return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } + + /** + * LocalDateTime 객체를 Date 객체로 변환 + * @param localDateTime LocalDateTime 객체 + * @return 변환된 Date 객체, 변환 실패 시 null 반환 + */ + public static Date toDate(LocalDateTime localDateTime) { + if (localDateTime == null) { + return null; + } + return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); + } + + /** + * 지정된 날짜에 일수를 더하거나 뺀 날짜 반환 + * @param date 기준 날짜 + * @param days 더하거나 뺄 일수 (양수: 더하기, 음수: 빼기) + * @return 계산된 날짜 + */ + public static LocalDate addDays(LocalDate date, int days) { + if (date == null) { + return null; + } + return date.plusDays(days); + } + + /** + * 지정된 날짜에 월수를 더하거나 뺀 날짜 반환 + * @param date 기준 날짜 + * @param months 더하거나 뺄 월수 (양수: 더하기, 음수: 빼기) + * @return 계산된 날짜 + */ + public static LocalDate addMonths(LocalDate date, int months) { + if (date == null) { + return null; + } + return date.plusMonths(months); + } + + /** + * 지정된 날짜에 연수를 더하거나 뺀 날짜 반환 + * @param date 기준 날짜 + * @param years 더하거나 뺄 연수 (양수: 더하기, 음수: 빼기) + * @return 계산된 날짜 + */ + public static LocalDate addYears(LocalDate date, int years) { + if (date == null) { + return null; + } + return date.plusYears(years); + } + + /** + * 두 날짜 사이의 일수 계산 + * @param startDate 시작 날짜 + * @param endDate 종료 날짜 + * @return 두 날짜 사이의 일수 (endDate - startDate) + */ + public static long daysBetween(LocalDate startDate, LocalDate endDate) { + if (startDate == null || endDate == null) { + return 0; + } + return ChronoUnit.DAYS.between(startDate, endDate); + } + + /** + * 두 날짜 사이의 월수 계산 + * @param startDate 시작 날짜 + * @param endDate 종료 날짜 + * @return 두 날짜 사이의 월수 (endDate - startDate) + */ + public static long monthsBetween(LocalDate startDate, LocalDate endDate) { + if (startDate == null || endDate == null) { + return 0; + } + return ChronoUnit.MONTHS.between(startDate, endDate); + } + + /** + * 두 날짜 사이의 연수 계산 + * @param startDate 시작 날짜 + * @param endDate 종료 날짜 + * @return 두 날짜 사이의 연수 (endDate - startDate) + */ + public static long yearsBetween(LocalDate startDate, LocalDate endDate) { + if (startDate == null || endDate == null) { + return 0; + } + return ChronoUnit.YEARS.between(startDate, endDate); + } + + /** + * 두 날짜 사이의 기간 계산 + * @param startDate 시작 날짜 + * @param endDate 종료 날짜 + * @return 두 날짜 사이의 기간 (년, 월, 일) + */ + public static Period periodBetween(LocalDate startDate, LocalDate endDate) { + if (startDate == null || endDate == null) { + return Period.ZERO; + } + return Period.between(startDate, endDate); + } + + /** + * 지정된 날짜의 요일 이름 반환 (한글) + * @param date 날짜 + * @return 요일 이름 (예: "월요일", "화요일") + */ + public static String getDayOfWeekName(LocalDate date) { + if (date == null) { + return null; + } + return date.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.KOREAN); + } + + /** + * 지정된 날짜의 요일 이름 반환 (영문) + * @param date 날짜 + * @return 요일 이름 (예: "Monday", "Tuesday") + */ + public static String getDayOfWeekNameInEnglish(LocalDate date) { + if (date == null) { + return null; + } + return date.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.ENGLISH); + } + + /** + * 지정된 날짜의 요일 이름 반환 (한글 약자) + * @param date 날짜 + * @return 요일 이름 (예: "월", "화") + */ + public static String getDayOfWeekShortName(LocalDate date) { + if (date == null) { + return null; + } + return date.getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.KOREAN); + } + + /** + * 지정된 날짜가 주말인지 확인 + * @param date 날짜 + * @return 주말이면 true, 평일이면 false + */ + public static boolean isWeekend(LocalDate date) { + if (date == null) { + return false; + } + DayOfWeek dayOfWeek = date.getDayOfWeek(); + return dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY; + } + + /** + * 지정된 날짜가 평일인지 확인 + * @param date 날짜 + * @return 평일이면 true, 주말이면 false + */ + public static boolean isWeekday(LocalDate date) { + return !isWeekend(date); + } + + /** + * 지정된 날짜가 오늘인지 확인 + * @param date 날짜 + * @return 오늘이면 true, 아니면 false + */ + public static boolean isToday(LocalDate date) { + if (date == null) { + return false; + } + return date.equals(LocalDate.now()); + } + + /** + * 지정된 날짜의 월의 첫 날 반환 + * @param date 날짜 + * @return 월의 첫 날 + */ + public static LocalDate getFirstDayOfMonth(LocalDate date) { + if (date == null) { + return null; + } + return date.with(TemporalAdjusters.firstDayOfMonth()); + } + + /** + * 지정된 날짜의 월의 마지막 날 반환 + * @param date 날짜 + * @return 월의 마지막 날 + */ + public static LocalDate getLastDayOfMonth(LocalDate date) { + if (date == null) { + return null; + } + return date.with(TemporalAdjusters.lastDayOfMonth()); + } + + /** + * 지정된 날짜의 연도의 첫 날 반환 + * @param date 날짜 + * @return 연도의 첫 날 + */ + public static LocalDate getFirstDayOfYear(LocalDate date) { + if (date == null) { + return null; + } + return date.with(TemporalAdjusters.firstDayOfYear()); + } + + /** + * 지정된 날짜의 연도의 마지막 날 반환 + * @param date 날짜 + * @return 연도의 마지막 날 + */ + public static LocalDate getLastDayOfYear(LocalDate date) { + if (date == null) { + return null; + } + return date.with(TemporalAdjusters.lastDayOfYear()); + } + + /** + * 지정된 날짜의 다음 특정 요일 반환 + * @param date 날짜 + * @param dayOfWeek 요일 (예: DayOfWeek.MONDAY) + * @return 다음 특정 요일 + */ + public static LocalDate getNextDayOfWeek(LocalDate date, DayOfWeek dayOfWeek) { + if (date == null || dayOfWeek == null) { + return null; + } + return date.with(TemporalAdjusters.next(dayOfWeek)); + } + + /** + * 지정된 날짜의 이전 특정 요일 반환 + * @param date 날짜 + * @param dayOfWeek 요일 (예: DayOfWeek.MONDAY) + * @return 이전 특정 요일 + */ + public static LocalDate getPreviousDayOfWeek(LocalDate date, DayOfWeek dayOfWeek) { + if (date == null || dayOfWeek == null) { + return null; + } + return date.with(TemporalAdjusters.previous(dayOfWeek)); + } + + /** + * 지정된 날짜에 시간을 결합하여 LocalDateTime 객체 생성 + * @param date 날짜 + * @param hour 시 + * @param minute 분 + * @param second 초 + * @return 생성된 LocalDateTime 객체 + */ + public static LocalDateTime combineDateTime(LocalDate date, int hour, int minute, int second) { + if (date == null) { + return null; + } + return LocalDateTime.of(date, LocalTime.of(hour, minute, second)); + } +} diff --git a/src/main/java/egovframework/util/FileUtil.java b/src/main/java/egovframework/util/FileUtil.java index f3d43b2..59af6da 100644 --- a/src/main/java/egovframework/util/FileUtil.java +++ b/src/main/java/egovframework/util/FileUtil.java @@ -1,7 +1,7 @@ package egovframework.util; +import egovframework.config.FileUploadProperties; import go.kr.project.common.model.FileVO; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; @@ -15,11 +15,13 @@ import java.io.OutputStream; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; @@ -29,39 +31,62 @@ import java.util.stream.Collectors; @Component public class FileUtil { + /** 파일 업로드 설정 */ + private final FileUploadProperties fileUploadProperties; + + /** 날짜 유틸리티 */ + private final DateUtil dateUtil; + /** 파일 저장 기본 경로 */ - @Value("${file.upload.path:D:\\xit-framework-file}") private String uploadPath; /** 최대 파일 크기 (단일 파일) - 기본값 10MB */ - @Value("${file.upload.max-size:10}") private long maxFileSize; /** 최대 총 파일 크기 - 기본값 50MB */ - @Value("${file.upload.max-total-size:50}") private long maxTotalSize; /** 허용된 파일 확장자 */ - @Value("${file.upload.allowed-extensions:jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,ppt,pptx,txt}") private String allowedExtensions; + /** 허용된 파일 확장자 목록 */ + private List allowedExtensionList; + /** 최대 파일 개수 - 기본값 10개 */ - @Value("${file.upload.max-files:10}") private int maxFiles; /** 실제 파일 삭제 여부 - 기본값 true */ - @Value("${file.upload.real-file-delete:true}") private boolean realFileDelete; - /** 허용된 파일 확장자 목록 */ - private List allowedExtensionList; + /** 하위 디렉토리 설정 */ + private Map subDirs; + + /** + * 생성자 + * @param fileUploadProperties 파일 업로드 설정 + * @param dateUtil 날짜 유틸리티 + */ + public FileUtil(FileUploadProperties fileUploadProperties, DateUtil dateUtil) { + this.fileUploadProperties = fileUploadProperties; + this.dateUtil = dateUtil; + } /** * 초기화 메서드 - * allowedExtensions 문자열을 파싱하여 allowedExtensionList를 초기화 + * FileUploadProperties에서 설정 값을 가져와 초기화 */ @PostConstruct public void init() { + // 설정 값 초기화 + this.uploadPath = fileUploadProperties.getPath(); + this.maxFileSize = fileUploadProperties.getMaxSize(); + this.maxTotalSize = fileUploadProperties.getMaxTotalSize(); + this.allowedExtensions = fileUploadProperties.getAllowedExtensions(); + this.maxFiles = fileUploadProperties.getMaxFiles(); + this.realFileDelete = fileUploadProperties.isRealFileDelete(); + this.subDirs = fileUploadProperties.getSubDirs(); + + // 허용된 파일 확장자 목록 초기화 allowedExtensionList = Arrays.stream(allowedExtensions.split(",")) .map(String::trim) .map(String::toLowerCase) @@ -76,12 +101,42 @@ public class FileUtil { * @throws IOException 잘못된 경로일 경우 예외 발생 */ private void validatePath(String path, boolean isFileName) throws IOException { - if (path == null || path.contains("..") || path.contains("/") || path.contains("\\")) { - String type = isFileName ? "파일명" : "디렉토리 경로"; - throw new IOException("잘못된 " + type + "입니다: " + path); + if (path == null) { + throw new IOException("경로가 null입니다."); + } + + // 경로 정규화 시도 + try { + Path normalizedPath = Paths.get(path).normalize(); + String normalizedPathStr = normalizedPath.toString(); + + // 정규화 후에도 '..'가 남아있는지 확인 + if (normalizedPathStr.contains("..")) { + String type = isFileName ? "파일명" : "디렉토리 경로"; + throw new IOException("잘못된 " + type + "입니다: " + path); + } + + // 추가 경로 검증 로직 + if (isFileName) { + // 파일명에는 경로 구분자가 없어야 함 + if (normalizedPathStr.contains("/") || normalizedPathStr.contains("\\")) { + throw new IOException("파일명에 경로 구분자가 포함되어 있습니다: " + path); + } + } else { + // 디렉토리 경로 검증 로직 (필요에 따라 추가) + } + + // 허용된 문자만 포함되어 있는지 검증 (정규식 사용) + //if (!normalizedPathStr.matches("[a-zA-Z0-9_\\-\\.가-힣]+")) { + // String type = isFileName ? "파일명" : "디렉토리 경로"; + // throw new IOException("허용되지 않은 문자가 포함된 " + type + "입니다: " + path); + //} + } catch (InvalidPathException e) { + throw new IOException("유효하지 않은 경로입니다: " + path, e); } } + /** * 파일 확장자 유효성 검증 * @param fileExt 검증할 파일 확장자 @@ -91,6 +146,19 @@ public class FileUtil { return allowedExtensionList.contains(fileExt.toLowerCase()); } + /** + * 설정된 하위 디렉토리 경로 조회 + * @param key 하위 디렉토리 키 (예: bbs-notice, html-editor) + * @return 설정된 하위 디렉토리 경로 + * @throws IOException 설정된 경로가 없을 경우 예외 발생 + */ + public String getSubDir(String key) throws IOException { + if (!subDirs.containsKey(key)) { + throw new IOException("설정된 하위 디렉토리가 없습니다: " + key); + } + return subDirs.get(key); + } + /** * 브라우저별 파일명 인코딩 처리 * @param fileName 원본 파일명 @@ -133,6 +201,7 @@ public class FileUtil { return fileVO; } + /** * 파일 업로드 처리 * @param files 업로드할 파일 목록 @@ -147,12 +216,21 @@ public class FileUtil { // 디렉토리 경로 검증 validatePath(subDir, false); + // 년월 정보 추출 + String yearMonth = dateUtil.getCurrentYearMonth(); + + // 년월 디렉토리를 포함한 경로 생성 + String yearMonthPath = subDir + File.separator + yearMonth; + + // 디렉토리 경로 검증 + validatePath(yearMonthPath, false); + // 디렉토리 생성 - String uploadDir = uploadPath + File.separator + subDir; + String uploadDir = uploadPath + File.separator + yearMonthPath; createDirectoryIfNotExists(uploadDir); // 파일 업로드 처리 - return processFileUploads(files, subDir, uploadDir); + return processFileUploads(files, yearMonthPath, uploadDir); } /** diff --git a/src/main/java/egovframework/util/NumberUtil.java b/src/main/java/egovframework/util/NumberUtil.java new file mode 100644 index 0000000..a76b118 --- /dev/null +++ b/src/main/java/egovframework/util/NumberUtil.java @@ -0,0 +1,352 @@ +package egovframework.util; + +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Locale; + +/** + * 숫자 관련 공통 유틸리티 클래스 + */ +@Component +public class NumberUtil { + + /** + * 문자열을 정수(int)로 변환 + * @param str 변환할 문자열 + * @param defaultValue 변환 실패 시 반환할 기본값 + * @return 변환된 정수 또는 기본값 + */ + public static int toInt(String str, int defaultValue) { + if (StringUtil.isEmpty(str)) { + return defaultValue; + } + try { + return Integer.parseInt(str.trim()); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + /** + * 문자열을 정수(long)로 변환 + * @param str 변환할 문자열 + * @param defaultValue 변환 실패 시 반환할 기본값 + * @return 변환된 정수 또는 기본값 + */ + public static long toLong(String str, long defaultValue) { + if (StringUtil.isEmpty(str)) { + return defaultValue; + } + try { + return Long.parseLong(str.trim()); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + /** + * 문자열을 실수(double)로 변환 + * @param str 변환할 문자열 + * @param defaultValue 변환 실패 시 반환할 기본값 + * @return 변환된 실수 또는 기본값 + */ + public static double toDouble(String str, double defaultValue) { + if (StringUtil.isEmpty(str)) { + return defaultValue; + } + try { + return Double.parseDouble(str.trim()); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + /** + * 문자열을 BigDecimal로 변환 + * @param str 변환할 문자열 + * @param defaultValue 변환 실패 시 반환할 기본값 + * @return 변환된 BigDecimal 또는 기본값 + */ + public static BigDecimal toBigDecimal(String str, BigDecimal defaultValue) { + if (StringUtil.isEmpty(str)) { + return defaultValue; + } + try { + return new BigDecimal(str.trim()); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + /** + * 숫자를 천 단위 구분 기호가 포함된 문자열로 변환 + * @param number 변환할 숫자 + * @return 천 단위 구분 기호가 포함된 문자열 + */ + public static String formatWithComma(long number) { + return NumberFormat.getNumberInstance(Locale.KOREA).format(number); + } + + /** + * 숫자를 천 단위 구분 기호가 포함된 문자열로 변환 + * @param number 변환할 숫자 + * @return 천 단위 구분 기호가 포함된 문자열 + */ + public static String formatWithComma(double number) { + return NumberFormat.getNumberInstance(Locale.KOREA).format(number); + } + + /** + * 숫자를 천 단위 구분 기호가 포함된 문자열로 변환 + * @param number 변환할 숫자 + * @return 천 단위 구분 기호가 포함된 문자열 + */ + public static String formatWithComma(BigDecimal number) { + if (number == null) { + return ""; + } + return NumberFormat.getNumberInstance(Locale.KOREA).format(number); + } + + /** + * 숫자를 원화 형식의 문자열로 변환 + * @param number 변환할 숫자 + * @return 원화 형식의 문자열 (예: "₩1,000") + */ + public static String formatKRW(long number) { + return NumberFormat.getCurrencyInstance(Locale.KOREA).format(number); + } + + /** + * 숫자를 원화 형식의 문자열로 변환 + * @param number 변환할 숫자 + * @return 원화 형식의 문자열 (예: "₩1,000.50") + */ + public static String formatKRW(double number) { + return NumberFormat.getCurrencyInstance(Locale.KOREA).format(number); + } + + /** + * 숫자를 원화 형식의 문자열로 변환 + * @param number 변환할 숫자 + * @return 원화 형식의 문자열 (예: "₩1,000.50") + */ + public static String formatKRW(BigDecimal number) { + if (number == null) { + return ""; + } + return NumberFormat.getCurrencyInstance(Locale.KOREA).format(number); + } + + /** + * 숫자를 달러 형식의 문자열로 변환 + * @param number 변환할 숫자 + * @return 달러 형식의 문자열 (예: "$1,000") + */ + public static String formatUSD(long number) { + return NumberFormat.getCurrencyInstance(Locale.US).format(number); + } + + /** + * 숫자를 달러 형식의 문자열로 변환 + * @param number 변환할 숫자 + * @return 달러 형식의 문자열 (예: "$1,000.50") + */ + public static String formatUSD(double number) { + return NumberFormat.getCurrencyInstance(Locale.US).format(number); + } + + /** + * 숫자를 달러 형식의 문자열로 변환 + * @param number 변환할 숫자 + * @return 달러 형식의 문자열 (예: "$1,000.50") + */ + public static String formatUSD(BigDecimal number) { + if (number == null) { + return ""; + } + return NumberFormat.getCurrencyInstance(Locale.US).format(number); + } + + /** + * 숫자를 지정된 소수점 자리수로 반올림 + * @param number 반올림할 숫자 + * @param scale 소수점 자리수 + * @return 반올림된 숫자 + */ + public static double round(double number, int scale) { + return BigDecimal.valueOf(number) + .setScale(scale, RoundingMode.HALF_UP) + .doubleValue(); + } + + /** + * BigDecimal을 지정된 소수점 자리수로 반올림 + * @param number 반올림할 숫자 + * @param scale 소수점 자리수 + * @return 반올림된 숫자 + */ + public static BigDecimal round(BigDecimal number, int scale) { + if (number == null) { + return null; + } + return number.setScale(scale, RoundingMode.HALF_UP); + } + + /** + * 숫자를 지정된 소수점 자리수로 내림 + * @param number 내림할 숫자 + * @param scale 소수점 자리수 + * @return 내림된 숫자 + */ + public static double floor(double number, int scale) { + return BigDecimal.valueOf(number) + .setScale(scale, RoundingMode.FLOOR) + .doubleValue(); + } + + /** + * BigDecimal을 지정된 소수점 자리수로 내림 + * @param number 내림할 숫자 + * @param scale 소수점 자리수 + * @return 내림된 숫자 + */ + public static BigDecimal floor(BigDecimal number, int scale) { + if (number == null) { + return null; + } + return number.setScale(scale, RoundingMode.FLOOR); + } + + /** + * 숫자를 지정된 소수점 자리수로 올림 + * @param number 올림할 숫자 + * @param scale 소수점 자리수 + * @return 올림된 숫자 + */ + public static double ceil(double number, int scale) { + return BigDecimal.valueOf(number) + .setScale(scale, RoundingMode.CEILING) + .doubleValue(); + } + + /** + * BigDecimal을 지정된 소수점 자리수로 올림 + * @param number 올림할 숫자 + * @param scale 소수점 자리수 + * @return 올림된 숫자 + */ + public static BigDecimal ceil(BigDecimal number, int scale) { + if (number == null) { + return null; + } + return number.setScale(scale, RoundingMode.CEILING); + } + + /** + * 숫자를 지정된 패턴으로 포맷팅 + * @param number 포맷팅할 숫자 + * @param pattern 패턴 (예: "#,###.##", "0.00") + * @return 포맷팅된 문자열 + */ + public static String format(double number, String pattern) { + DecimalFormat df = new DecimalFormat(pattern); + return df.format(number); + } + + /** + * 숫자를 지정된 패턴으로 포맷팅 + * @param number 포맷팅할 숫자 + * @param pattern 패턴 (예: "#,###.##", "0.00") + * @return 포맷팅된 문자열 + */ + public static String format(BigDecimal number, String pattern) { + if (number == null) { + return ""; + } + DecimalFormat df = new DecimalFormat(pattern); + return df.format(number); + } + + /** + * 두 숫자 중 최소값 반환 + * @param a 첫 번째 숫자 + * @param b 두 번째 숫자 + * @return 최소값 + */ + public static int min(int a, int b) { + return Math.min(a, b); + } + + /** + * 두 숫자 중 최대값 반환 + * @param a 첫 번째 숫자 + * @param b 두 번째 숫자 + * @return 최대값 + */ + public static int max(int a, int b) { + return Math.max(a, b); + } + + /** + * 숫자가 지정된 범위 내에 있는지 확인 + * @param number 확인할 숫자 + * @param min 최소값 + * @param max 최대값 + * @return 범위 내에 있으면 true, 그렇지 않으면 false + */ + public static boolean isBetween(int number, int min, int max) { + return number >= min && number <= max; + } + + /** + * 숫자가 지정된 범위 내에 있는지 확인 + * @param number 확인할 숫자 + * @param min 최소값 + * @param max 최대값 + * @return 범위 내에 있으면 true, 그렇지 않으면 false + */ + public static boolean isBetween(double number, double min, double max) { + return number >= min && number <= max; + } + + /** + * 숫자가 양수인지 확인 + * @param number 확인할 숫자 + * @return 양수이면 true, 그렇지 않으면 false + */ + public static boolean isPositive(int number) { + return number > 0; + } + + /** + * 숫자가 음수인지 확인 + * @param number 확인할 숫자 + * @return 음수이면 true, 그렇지 않으면 false + */ + public static boolean isNegative(int number) { + return number < 0; + } + + /** + * 숫자가 0인지 확인 + * @param number 확인할 숫자 + * @return 0이면 true, 그렇지 않으면 false + */ + public static boolean isZero(int number) { + return number == 0; + } + + /** + * 숫자가 0인지 확인 (부동소수점 오차 고려) + * @param number 확인할 숫자 + * @return 0이면 true, 그렇지 않으면 false + */ + public static boolean isZero(double number) { + return Math.abs(number) < 0.000001; + } +} \ No newline at end of file diff --git a/src/main/java/egovframework/util/StringUtil.java b/src/main/java/egovframework/util/StringUtil.java new file mode 100644 index 0000000..db0186d --- /dev/null +++ b/src/main/java/egovframework/util/StringUtil.java @@ -0,0 +1,218 @@ +package egovframework.util; + +import org.springframework.stereotype.Component; + +/** + * 문자열 관련 공통 유틸리티 클래스 + */ +@Component +public class StringUtil { + + /** + * 문자열이 null이거나 빈 문자열인지 확인 + * @param str 검사할 문자열 + * @return null이거나 빈 문자열이면 true, 그렇지 않으면 false + */ + public static boolean isEmpty(String str) { + return str == null || str.length() == 0; + } + + /** + * 문자열이 null이 아니고 빈 문자열이 아닌지 확인 + * @param str 검사할 문자열 + * @return null이 아니고 빈 문자열이 아니면 true, 그렇지 않으면 false + */ + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + /** + * 문자열이 null이거나 빈 문자열이거나 공백 문자로만 이루어져 있는지 확인 + * @param str 검사할 문자열 + * @return null이거나 빈 문자열이거나 공백 문자로만 이루어져 있으면 true, 그렇지 않으면 false + */ + public static boolean isBlank(String str) { + if (isEmpty(str)) { + return true; + } + for (int i = 0; i < str.length(); i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return false; + } + } + return true; + } + + /** + * 문자열이 null이 아니고 빈 문자열이 아니고 공백 문자로만 이루어져 있지 않은지 확인 + * @param str 검사할 문자열 + * @return null이 아니고 빈 문자열이 아니고 공백 문자로만 이루어져 있지 않으면 true, 그렇지 않으면 false + */ + public static boolean isNotBlank(String str) { + return !isBlank(str); + } + + /** + * 문자열의 앞뒤 공백을 제거 + * @param str 처리할 문자열 + * @return 앞뒤 공백이 제거된 문자열, null이면 null 반환 + */ + public static String trim(String str) { + return str == null ? null : str.trim(); + } + + /** + * 문자열이 null이면 빈 문자열 반환, 그렇지 않으면 원래 문자열 반환 + * @param str 처리할 문자열 + * @return null이면 빈 문자열, 그렇지 않으면 원래 문자열 + */ + public static String nullToEmpty(String str) { + return str == null ? "" : str; + } + + /** + * 문자열이 null이거나 빈 문자열이면 기본값 반환, 그렇지 않으면 원래 문자열 반환 + * @param str 처리할 문자열 + * @param defaultValue 기본값 + * @return null이거나 빈 문자열이면 기본값, 그렇지 않으면 원래 문자열 + */ + public static String defaultIfEmpty(String str, String defaultValue) { + return isEmpty(str) ? defaultValue : str; + } + + /** + * 문자열이 null이거나 빈 문자열이거나 공백 문자로만 이루어져 있으면 기본값 반환, 그렇지 않으면 원래 문자열 반환 + * @param str 처리할 문자열 + * @param defaultValue 기본값 + * @return null이거나 빈 문자열이거나 공백 문자로만 이루어져 있으면 기본값, 그렇지 않으면 원래 문자열 + */ + public static String defaultIfBlank(String str, String defaultValue) { + return isBlank(str) ? defaultValue : str; + } + + /** + * 문자열의 좌측에서 지정된 길이만큼 문자열 추출 + * @param str 처리할 문자열 + * @param len 추출할 길이 + * @return 추출된 문자열, null이면 null 반환 + */ + public static String left(String str, int len) { + if (str == null) { + return null; + } + if (len < 0) { + return ""; + } + if (str.length() <= len) { + return str; + } + return str.substring(0, len); + } + + /** + * 문자열의 우측에서 지정된 길이만큼 문자열 추출 + * @param str 처리할 문자열 + * @param len 추출할 길이 + * @return 추출된 문자열, null이면 null 반환 + */ + public static String right(String str, int len) { + if (str == null) { + return null; + } + if (len < 0) { + return ""; + } + if (str.length() <= len) { + return str; + } + return str.substring(str.length() - len); + } + + /** + * 문자열에서 특정 문자열을 다른 문자열로 모두 치환 + * @param str 처리할 문자열 + * @param searchStr 찾을 문자열 + * @param replaceStr 치환할 문자열 + * @return 치환된 문자열, null이면 null 반환 + */ + public static String replace(String str, String searchStr, String replaceStr) { + if (isEmpty(str) || isEmpty(searchStr) || replaceStr == null) { + return str; + } + return str.replace(searchStr, replaceStr); + } + + /** + * 문자열 내의 HTML 특수 문자를 이스케이프 처리 + * @param str 처리할 문자열 + * @return 이스케이프 처리된 문자열, null이면 null 반환 + */ + public static String escapeHtml(String str) { + if (str == null) { + return null; + } + return str.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + /** + * 문자열 내의 줄바꿈 문자를 HTML
태그로 변환 + * @param str 처리할 문자열 + * @return 변환된 문자열, null이면 null 반환 + */ + public static String nl2br(String str) { + if (str == null) { + return null; + } + return str.replace("\r\n", "
") + .replace("\n", "
") + .replace("\r", "
"); + } + + /** + * 문자열이 지정된 접두사로 시작하는지 확인 + * @param str 검사할 문자열 + * @param prefix 접두사 + * @return 지정된 접두사로 시작하면 true, 그렇지 않으면 false + */ + public static boolean startsWith(String str, String prefix) { + return str != null && prefix != null && str.startsWith(prefix); + } + + /** + * 문자열이 지정된 접미사로 끝나는지 확인 + * @param str 검사할 문자열 + * @param suffix 접미사 + * @return 지정된 접미사로 끝나면 true, 그렇지 않으면 false + */ + public static boolean endsWith(String str, String suffix) { + return str != null && suffix != null && str.endsWith(suffix); + } + + /** + * 문자열을 지정된 길이로 자르고 생략 부호를 추가 + * @param str 처리할 문자열 + * @param maxLength 최대 길이 + * @param suffix 생략 부호 (예: "...") + * @return 처리된 문자열, null이면 null 반환 + */ + public static String abbreviate(String str, int maxLength, String suffix) { + if (str == null) { + return null; + } + if (str.length() <= maxLength) { + return str; + } + if (suffix == null) { + suffix = ""; + } + int suffixLength = suffix.length(); + if (maxLength <= suffixLength) { + return suffix; + } + return str.substring(0, maxLength - suffixLength) + suffix; + } +} \ No newline at end of file diff --git a/src/main/java/egovframework/util/ValidationUtil.java b/src/main/java/egovframework/util/ValidationUtil.java new file mode 100644 index 0000000..cc94344 --- /dev/null +++ b/src/main/java/egovframework/util/ValidationUtil.java @@ -0,0 +1,340 @@ +package egovframework.util; + +import org.springframework.stereotype.Component; + +import java.util.regex.Pattern; + +/** + * 데이터 유효성 검증 관련 공통 유틸리티 클래스 + */ +@Component +public class ValidationUtil { + + /** 이메일 주소 정규식 패턴 */ + private static final Pattern EMAIL_PATTERN = + Pattern.compile("^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"); + + /** 한국 휴대폰 번호 정규식 패턴 (010-XXXX-XXXX 또는 010XXXXXXXX 형식) */ + private static final Pattern MOBILE_PHONE_PATTERN = + Pattern.compile("^01(?:0|1|[6-9])(?:-?\\d{3,4})?(?:-?\\d{4})$"); + + /** 한국 일반 전화번호 정규식 패턴 (지역번호-국번-번호 형식) */ + private static final Pattern PHONE_PATTERN = + Pattern.compile("^(?:(?:\\d{2,3})|(?:\\d{2,3}-))(?:\\d{3,4}-\\d{4})$"); + + /** 한국 우편번호 정규식 패턴 (5자리) */ + private static final Pattern ZIPCODE_PATTERN = + Pattern.compile("^\\d{5}$"); + + /** 한국 주민등록번호 정규식 패턴 (XXXXXX-XXXXXXX 형식) */ + private static final Pattern RESIDENT_REGISTRATION_NUMBER_PATTERN = + Pattern.compile("^\\d{6}-?[1-4]\\d{6}$"); + + /** 한국 사업자등록번호 정규식 패턴 (XXX-XX-XXXXX 형식) */ + private static final Pattern BUSINESS_REGISTRATION_NUMBER_PATTERN = + Pattern.compile("^\\d{3}-?\\d{2}-?\\d{5}$"); + + /** IP 주소 정규식 패턴 (IPv4) */ + private static final Pattern IPV4_PATTERN = + Pattern.compile("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"); + + /** URL 정규식 패턴 */ + private static final Pattern URL_PATTERN = + Pattern.compile("^(https?|ftp)://[^\\s/$.?#].[^\\s]*$"); + + /** 날짜 정규식 패턴 (YYYY-MM-DD 형식) */ + private static final Pattern DATE_PATTERN = + Pattern.compile("^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$"); + + /** 시간 정규식 패턴 (HH:MM:SS 형식) */ + private static final Pattern TIME_PATTERN = + Pattern.compile("^([01]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$"); + + /** 한글 정규식 패턴 */ + private static final Pattern KOREAN_PATTERN = + Pattern.compile("^[가-힣]+$"); + + /** 영문자 정규식 패턴 */ + private static final Pattern ENGLISH_PATTERN = + Pattern.compile("^[a-zA-Z]+$"); + + /** 영문자 및 숫자 정규식 패턴 */ + private static final Pattern ALPHANUMERIC_PATTERN = + Pattern.compile("^[a-zA-Z0-9]+$"); + + /** 숫자 정규식 패턴 */ + private static final Pattern NUMERIC_PATTERN = + Pattern.compile("^[0-9]+$"); + + /** + * 이메일 주소 유효성 검증 + * @param email 검증할 이메일 주소 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isValidEmail(String email) { + if (StringUtil.isEmpty(email)) { + return false; + } + return EMAIL_PATTERN.matcher(email).matches(); + } + + /** + * 한국 휴대폰 번호 유효성 검증 + * @param mobilePhone 검증할 휴대폰 번호 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isValidMobilePhone(String mobilePhone) { + if (StringUtil.isEmpty(mobilePhone)) { + return false; + } + return MOBILE_PHONE_PATTERN.matcher(mobilePhone).matches(); + } + + /** + * 한국 일반 전화번호 유효성 검증 + * @param phone 검증할 전화번호 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isValidPhone(String phone) { + if (StringUtil.isEmpty(phone)) { + return false; + } + return PHONE_PATTERN.matcher(phone).matches(); + } + + /** + * 한국 우편번호 유효성 검증 + * @param zipcode 검증할 우편번호 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isValidZipcode(String zipcode) { + if (StringUtil.isEmpty(zipcode)) { + return false; + } + return ZIPCODE_PATTERN.matcher(zipcode).matches(); + } + + /** + * 한국 주민등록번호 유효성 검증 (형식만 검증, 실제 유효성은 검증하지 않음) + * @param rrn 검증할 주민등록번호 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isValidResidentRegistrationNumber(String rrn) { + if (StringUtil.isEmpty(rrn)) { + return false; + } + return RESIDENT_REGISTRATION_NUMBER_PATTERN.matcher(rrn).matches(); + } + + /** + * 한국 사업자등록번호 유효성 검증 (형식만 검증, 실제 유효성은 검증하지 않음) + * @param brn 검증할 사업자등록번호 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isValidBusinessRegistrationNumber(String brn) { + if (StringUtil.isEmpty(brn)) { + return false; + } + return BUSINESS_REGISTRATION_NUMBER_PATTERN.matcher(brn).matches(); + } + + /** + * IP 주소(IPv4) 유효성 검증 + * @param ipAddress 검증할 IP 주소 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isValidIpAddress(String ipAddress) { + if (StringUtil.isEmpty(ipAddress)) { + return false; + } + return IPV4_PATTERN.matcher(ipAddress).matches(); + } + + /** + * URL 유효성 검증 + * @param url 검증할 URL + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isValidUrl(String url) { + if (StringUtil.isEmpty(url)) { + return false; + } + return URL_PATTERN.matcher(url).matches(); + } + + /** + * 날짜 유효성 검증 (YYYY-MM-DD 형식) + * @param date 검증할 날짜 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isValidDate(String date) { + if (StringUtil.isEmpty(date)) { + return false; + } + return DATE_PATTERN.matcher(date).matches(); + } + + /** + * 시간 유효성 검증 (HH:MM:SS 또는 HH:MM 형식) + * @param time 검증할 시간 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isValidTime(String time) { + if (StringUtil.isEmpty(time)) { + return false; + } + return TIME_PATTERN.matcher(time).matches(); + } + + /** + * 한글 유효성 검증 + * @param text 검증할 문자열 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isKorean(String text) { + if (StringUtil.isEmpty(text)) { + return false; + } + return KOREAN_PATTERN.matcher(text).matches(); + } + + /** + * 영문자 유효성 검증 + * @param text 검증할 문자열 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isEnglish(String text) { + if (StringUtil.isEmpty(text)) { + return false; + } + return ENGLISH_PATTERN.matcher(text).matches(); + } + + /** + * 영문자 및 숫자 유효성 검증 + * @param text 검증할 문자열 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isAlphanumeric(String text) { + if (StringUtil.isEmpty(text)) { + return false; + } + return ALPHANUMERIC_PATTERN.matcher(text).matches(); + } + + /** + * 숫자 유효성 검증 + * @param text 검증할 문자열 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isNumeric(String text) { + if (StringUtil.isEmpty(text)) { + return false; + } + return NUMERIC_PATTERN.matcher(text).matches(); + } + + /** + * 비밀번호 복잡성 검증 (영문 대소문자, 숫자, 특수문자 포함 8자 이상) + * @param password 검증할 비밀번호 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isValidPassword(String password) { + if (StringUtil.isEmpty(password) || password.length() < 8) { + return false; + } + + boolean hasUpperCase = false; + boolean hasLowerCase = false; + boolean hasDigit = false; + boolean hasSpecialChar = false; + + for (char c : password.toCharArray()) { + if (Character.isUpperCase(c)) { + hasUpperCase = true; + } else if (Character.isLowerCase(c)) { + hasLowerCase = true; + } else if (Character.isDigit(c)) { + hasDigit = true; + } else if (!Character.isLetterOrDigit(c)) { + hasSpecialChar = true; + } + } + + return hasUpperCase && hasLowerCase && hasDigit && hasSpecialChar; + } + + /** + * 문자열 길이 검증 + * @param text 검증할 문자열 + * @param minLength 최소 길이 + * @param maxLength 최대 길이 + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isValidLength(String text, int minLength, int maxLength) { + if (text == null) { + return minLength <= 0; + } + int length = text.length(); + return length >= minLength && length <= maxLength; + } + + /** + * 주민등록번호 유효성 검증 (체크섬 포함) + * @param rrn 검증할 주민등록번호 (XXXXXX-XXXXXXX 형식) + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isValidResidentRegistrationNumberWithChecksum(String rrn) { + if (!isValidResidentRegistrationNumber(rrn)) { + return false; + } + + // 하이픈 제거 + rrn = rrn.replace("-", ""); + + // 가중치 배열 + int[] weights = {2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5}; + + // 합계 계산 + int sum = 0; + for (int i = 0; i < 12; i++) { + sum += (rrn.charAt(i) - '0') * weights[i]; + } + + // 체크섬 계산 + int checksum = (11 - (sum % 11)) % 10; + + // 마지막 자리와 체크섬 비교 + return checksum == (rrn.charAt(12) - '0'); + } + + /** + * 사업자등록번호 유효성 검증 (체크섬 포함) + * @param brn 검증할 사업자등록번호 (XXX-XX-XXXXX 형식) + * @return 유효하면 true, 그렇지 않으면 false + */ + public static boolean isValidBusinessRegistrationNumberWithChecksum(String brn) { + if (!isValidBusinessRegistrationNumber(brn)) { + return false; + } + + // 하이픈 제거 + brn = brn.replace("-", ""); + + // 가중치 배열 + int[] weights = {1, 3, 7, 1, 3, 7, 1, 3, 5}; + + // 합계 계산 + int sum = 0; + for (int i = 0; i < 9; i++) { + sum += (brn.charAt(i) - '0') * weights[i]; + } + + // 체크섬 계산 + sum += ((brn.charAt(8) - '0') * 5) / 10; + int checksum = (10 - (sum % 10)) % 10; + + // 마지막 자리와 체크섬 비교 + return checksum == (brn.charAt(9) - '0'); + } +} \ No newline at end of file diff --git a/src/main/java/go/kr/project/bbs/notice/service/impl/BbsNoticeServiceImpl.java b/src/main/java/go/kr/project/bbs/notice/service/impl/BbsNoticeServiceImpl.java index 84fb7bd..9472e33 100644 --- a/src/main/java/go/kr/project/bbs/notice/service/impl/BbsNoticeServiceImpl.java +++ b/src/main/java/go/kr/project/bbs/notice/service/impl/BbsNoticeServiceImpl.java @@ -201,7 +201,9 @@ public class BbsNoticeServiceImpl extends EgovAbstractServiceImpl implements Bbs // FileUtil을 사용하여 파일 업로드 List uploadedFiles; try { - uploadedFiles = fileUtil.uploadFiles(validFiles, "bbs/notice"); + // 설정 파일에서 하위 디렉토리 경로 가져오기 + String subDir = fileUtil.getSubDir("bbs-notice"); + uploadedFiles = fileUtil.uploadFiles(validFiles, subDir); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/main/java/go/kr/project/common/service/impl/HtmlEditorServiceImpl.java b/src/main/java/go/kr/project/common/service/impl/HtmlEditorServiceImpl.java index b13d347..6379745 100644 --- a/src/main/java/go/kr/project/common/service/impl/HtmlEditorServiceImpl.java +++ b/src/main/java/go/kr/project/common/service/impl/HtmlEditorServiceImpl.java @@ -202,7 +202,7 @@ public class HtmlEditorServiceImpl extends EgovAbstractServiceImpl implements Ht files.add(file); List uploadedFiles = uploadHtmlEditorFiles(files, moduleId, fileType, rgtr); - + if (uploadedFiles.isEmpty()) { throw new RuntimeException("파일 업로드에 실패했습니다."); } @@ -243,7 +243,9 @@ public class HtmlEditorServiceImpl extends EgovAbstractServiceImpl implements Ht // FileUtil을 사용하여 파일 업로드 List uploadedFiles; try { - uploadedFiles = fileUtil.uploadFiles(validFiles, "common/html_editor"); + // 설정 파일에서 하위 디렉토리 경로 가져오기 + String subDir = fileUtil.getSubDir("html-editor"); + uploadedFiles = fileUtil.uploadFiles(validFiles, subDir); } catch (IOException e) { throw new RuntimeException(e); } @@ -286,4 +288,4 @@ public class HtmlEditorServiceImpl extends EgovAbstractServiceImpl implements Ht public int updateHtmlEditorFile(HtmlEditorFileVO vo) { return htmlEditorMapper.updateHtmlEditorFile(vo); } -} \ No newline at end of file +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index a596bee..2b98c20 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -47,5 +47,8 @@ file: max-size: 10 # 단일 파일 최대 크기 (MB) max-total-size: 50 # 총 파일 최대 크기 (MB) max-files: 10 # 최대 파일 개수 - allowed-extensions: jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,ppt,pptx,txt,zip - real-file-delete: true # 실제 파일 삭제 여부 \ No newline at end of file + allowed-extensions: hwp,jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,ppt,pptx,txt,zip + real-file-delete: true # 실제 파일 삭제 여부 + sub-dirs: + bbs-notice: bbs/notice # 게시판 공지사항 파일 저장 경로 + html-editor: common/html_editor # HTML 에디터 파일 저장 경로 diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index f804ecb..778d148 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -54,5 +54,8 @@ file: max-size: 10 # 단일 파일 최대 크기 (MB) max-total-size: 50 # 총 파일 최대 크기 (MB) max-files: 10 # 최대 파일 개수 - allowed-extensions: jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,ppt,pptx,txt,zip - real-file-delete: true # 실제 파일 삭제 여부 \ No newline at end of file + allowed-extensions: hwp,jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,ppt,pptx,txt,zip + real-file-delete: true # 실제 파일 삭제 여부 + sub-dirs: + bbs-notice: bbs/notice # 게시판 공지사항 파일 저장 경로 + html-editor: common/html_editor # HTML 에디터 파일 저장 경로 diff --git a/src/main/resources/application-prd.yml b/src/main/resources/application-prd.yml index bbf73d9..c149fc2 100644 --- a/src/main/resources/application-prd.yml +++ b/src/main/resources/application-prd.yml @@ -47,5 +47,8 @@ file: max-size: 10 # 단일 파일 최대 크기 (MB) max-total-size: 50 # 총 파일 최대 크기 (MB) max-files: 10 # 최대 파일 개수 - allowed-extensions: jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,ppt,pptx,txt,zip - real-file-delete: true # 실제 파일 삭제 여부 \ No newline at end of file + allowed-extensions: hwp,jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,ppt,pptx,txt,zip + real-file-delete: true # 실제 파일 삭제 여부 + sub-dirs: + bbs-notice: bbs/notice # 게시판 공지사항 파일 저장 경로 + html-editor: common/html_editor # HTML 에디터 파일 저장 경로