컬렉션 및 날짜 관련 유틸리티 클래스 추가

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 안전성 및 유효성 검사에 초점.
multiDB
박성영 7 months ago
parent 8aac5e0bfa
commit a731ab19e8

@ -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: <SomeEnabledInspectionId>
#Disable inspections
#exclude:
# - name: <SomeDisabledInspectionId>
# paths:
# - <path/where/not/run/inspection>
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> #(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

@ -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<String, String> subDirs;
}

@ -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 <T> boolean isEmpty(T[] array) {
return array == null || array.length == 0;
}
/**
* null
* @param array
* @return null true, false
*/
public static <T> 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 <T> int size(T[] array) {
return array == null ? 0 : array.length;
}
/**
* null ,
* @param collection
* @return null ,
*/
public static <T> Collection<T> emptyIfNull(Collection<T> collection) {
return collection == null ? Collections.emptyList() : collection;
}
/**
* null ,
* @param list
* @return null ,
*/
public static <T> List<T> emptyIfNull(List<T> list) {
return list == null ? Collections.emptyList() : list;
}
/**
* null ,
* @param map
* @return null ,
*/
public static <K, V> Map<K, V> emptyIfNull(Map<K, V> map) {
return map == null ? Collections.emptyMap() : map;
}
/**
* (null )
* @param array
* @return , null
*/
public static <T> List<T> toList(T[] array) {
if (array == null) {
return Collections.emptyList();
}
return Arrays.asList(array);
}
/**
* (null )
* @param collection
* @return , null
*/
public static <T> List<T> toList(Collection<T> collection) {
if (collection == null) {
return Collections.emptyList();
}
return new ArrayList<>(collection);
}
/**
* Set (null )
* @param collection
* @return Set, null Set
*/
public static <T> Set<T> toSet(Collection<T> collection) {
if (collection == null) {
return Collections.emptySet();
}
return new HashSet<>(collection);
}
/**
* Set (null )
* @param array
* @return Set, null Set
*/
public static <T> Set<T> toSet(T[] array) {
if (array == null) {
return Collections.emptySet();
}
Set<T> set = new HashSet<>();
Collections.addAll(set, array);
return set;
}
/**
*
* @param collection
* @param predicate
* @return
*/
public static <T> List<T> filter(Collection<T> collection, Predicate<T> predicate) {
if (isEmpty(collection)) {
return Collections.emptyList();
}
return collection.stream()
.filter(predicate)
.collect(Collectors.toList());
}
/**
*
* @param collection
* @param mapper
* @return
*/
public static <T, R> List<R> map(Collection<T> collection, Function<T, R> mapper) {
if (isEmpty(collection)) {
return Collections.emptyList();
}
return collection.stream()
.map(mapper)
.collect(Collectors.toList());
}
/**
*
* @param collection
* @param comparator
* @return
*/
public static <T> List<T> sort(Collection<T> collection, Comparator<? super T> comparator) {
if (isEmpty(collection)) {
return Collections.emptyList();
}
List<T> list = new ArrayList<>(collection);
list.sort(comparator);
return list;
}
/**
* (Comparable )
* @param collection
* @return
*/
public static <T extends Comparable<? super T>> List<T> sort(Collection<T> collection) {
if (isEmpty(collection)) {
return Collections.emptyList();
}
List<T> list = new ArrayList<>(collection);
Collections.sort(list);
return list;
}
/**
*
* @param collection
* @return
*/
public static <T> List<T> distinct(Collection<T> collection) {
if (isEmpty(collection)) {
return Collections.emptyList();
}
return collection.stream()
.distinct()
.collect(Collectors.toList());
}
/**
*
* @param collection
* @param keyMapper
* @return
*/
public static <T, K> Map<K, List<T>> groupBy(Collection<T> collection, Function<T, K> keyMapper) {
if (isEmpty(collection)) {
return Collections.emptyMap();
}
return collection.stream()
.collect(Collectors.groupingBy(keyMapper));
}
/**
*
* @param collection
* @param keyMapper
* @return
*/
public static <T, K> Map<K, T> toMap(Collection<T> collection, Function<T, K> 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 <T, K, V> Map<K, V> toMap(Collection<T> collection, Function<T, K> keyMapper, Function<T, V> 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 <T> List<T> union(Collection<T> collection1, Collection<T> collection2) {
Set<T> 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 <T> List<T> intersection(Collection<T> collection1, Collection<T> collection2) {
if (isEmpty(collection1) || isEmpty(collection2)) {
return Collections.emptyList();
}
List<T> result = new ArrayList<>();
Set<T> 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 <T> List<T> difference(Collection<T> collection1, Collection<T> collection2) {
if (isEmpty(collection1)) {
return Collections.emptyList();
}
if (isEmpty(collection2)) {
return new ArrayList<>(collection1);
}
List<T> result = new ArrayList<>(collection1);
result.removeAll(new HashSet<>(collection2));
return result;
}
/**
*
* @param collection
* @param size
* @return
*/
public static <T> List<List<T>> partition(Collection<T> collection, int size) {
if (isEmpty(collection)) {
return Collections.emptyList();
}
if (size <= 0) {
throw new IllegalArgumentException("Size must be greater than 0");
}
List<List<T>> result = new ArrayList<>();
List<T> 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> T getFirst(Collection<T> collection) {
if (isEmpty(collection)) {
return null;
}
return collection.iterator().next();
}
/**
* (null )
* @param list
* @return , null
*/
public static <T> T getLast(List<T> list) {
if (isEmpty(list)) {
return null;
}
return list.get(list.size() - 1);
}
/**
* (null )
* @param map
* @param key
* @param defaultValue
* @return ,
*/
public static <K, V> V getOrDefault(Map<K, V> map, K key, V defaultValue) {
if (isEmpty(map)) {
return defaultValue;
}
return map.getOrDefault(key, defaultValue);
}
/**
*
* @param map1
* @param map2
* @return
*/
public static <K, V> Map<K, V> merge(Map<K, V> map1, Map<K, V> map2) {
Map<K, V> result = new HashMap<>();
if (isNotEmpty(map1)) {
result.putAll(map1);
}
if (isNotEmpty(map2)) {
result.putAll(map2);
}
return result;
}
}

@ -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));
}
}

@ -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<String> allowedExtensionList;
/** 최대 파일 개수 - 기본값 10개 */
@Value("${file.upload.max-files:10}")
private int maxFiles;
/** 실제 파일 삭제 여부 - 기본값 true */
@Value("${file.upload.real-file-delete:true}")
private boolean realFileDelete;
/** 허용된 파일 확장자 목록 */
private List<String> allowedExtensionList;
/** 하위 디렉토리 설정 */
private Map<String, String> 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);
}
/**

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

@ -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("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
/**
* HTML <br>
* @param str
* @return , null null
*/
public static String nl2br(String str) {
if (str == null) {
return null;
}
return str.replace("\r\n", "<br>")
.replace("\n", "<br>")
.replace("\r", "<br>");
}
/**
*
* @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;
}
}

@ -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');
}
}

@ -201,7 +201,9 @@ public class BbsNoticeServiceImpl extends EgovAbstractServiceImpl implements Bbs
// FileUtil을 사용하여 파일 업로드
List<FileVO> 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);
}

@ -202,7 +202,7 @@ public class HtmlEditorServiceImpl extends EgovAbstractServiceImpl implements Ht
files.add(file);
List<HtmlEditorFileVO> 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<FileVO> 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);
}
}
}

@ -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 # 실제 파일 삭제 여부
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 에디터 파일 저장 경로

@ -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 # 실제 파일 삭제 여부
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 에디터 파일 저장 경로

@ -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 # 실제 파일 삭제 여부
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 에디터 파일 저장 경로

Loading…
Cancel
Save