용인시 수지구청 자동차 검사 과태료 시스템

최초 등록
internalApi
박성영 1 month ago
commit f1ecbca216

44
.gitignore vendored

@ -0,0 +1,44 @@
# ---> Java
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
/.junie/
/logs/
/build/
/.idea/
/.gradle/
/.idea/
/src/main/UbiService/logs/
/src/main/UbiService/results/UBIHTML/
/src/main/webapp/ubi4/logs/
/.vscode/settings.json
/CLAUDE.md
/DEV-SERVER-REPORT-FILE/UbiService/logs/
/DEV-SERVER-REPORT-FILE/UbiService/results/UBIHTML/
/.claude/settings.local.json
/DEV-SERVER-REPORT-FILE/UbiService/results/
/gradle/wrapper/gradle-wrapper.properties
/gradlew
/gradlew.bat

@ -0,0 +1,4 @@
CREATE DEFINER=`root`@`%` FUNCTION `ibmsdb`.`ECL_DECRYPT`(PIN_JNO varchar(256)) RETURNS varchar(4000) CHARSET utf8
BEGIN
RETURN AES_DECRYPT(UNHEX(FROM_BASE64(PIN_JNO)),'Copyright(c)2015-xit.co.kr');
END

@ -0,0 +1,4 @@
CREATE DEFINER=`root`@`%` FUNCTION `ibmsdb`.`ECL_ENCRYPT`(PIN_JNO varchar(256)) RETURNS varchar(4000) CHARSET utf8
BEGIN
RETURN TO_BASE64(HEX(AES_ENCRYPT(PIN_JNO,'Copyright(c)2015-xit.co.kr')));
END

@ -0,0 +1,8 @@
CREATE SEQUENCE seq_file_id
START WITH 1000
INCREMENT BY 1
MINVALUE 1
MAXVALUE 9999999999999999
CACHE 1000
CYCLE;

@ -0,0 +1,7 @@
CREATE SEQUENCE seq_login_log_id
START WITH 1000
INCREMENT BY 1
MINVALUE 1
MAXVALUE 9999999999999999
CACHE 1000
CYCLE;

@ -0,0 +1,8 @@
CREATE SEQUENCE seq_menu_id
START WITH 1000
INCREMENT BY 1
MINVALUE 1
MAXVALUE 9999999999999999
CACHE 1000
CYCLE;

@ -0,0 +1,8 @@
CREATE SEQUENCE seq_user_id
START WITH 1000
INCREMENT BY 1
MINVALUE 1
MAXVALUE 9999999999999999
CACHE 1000
CYCLE;

@ -0,0 +1,23 @@
create table tb_cd_detail
(
CD_GROUP_ID varchar(20) not null comment '코드 그룹 ID',
CD_ID varchar(20) not null comment '코드 ID',
CD_NM varchar(100) not null comment '코드 이름',
CD_DC varchar(200) null comment '코드 설명',
SORT_ORDR int default 0 null comment '정렬 순서',
USE_YN varchar(1) not null comment '사용 여부',
ATTRIBUTE1 varchar(200) null comment '속성1',
ATTRIBUTE2 varchar(200) null comment '속성2',
ATTRIBUTE3 varchar(200) null comment '속성3',
ATTRIBUTE4 varchar(200) null comment '속성4',
ATTRIBUTE5 varchar(200) null comment '속성5',
REG_DTTM datetime null comment '등록 일시',
RGTR varchar(20) null comment '등록자',
MDFCN_DTTM datetime null comment '수정 일시',
MDFR varchar(20) null comment '수정자',
primary key (CD_GROUP_ID, CD_ID),
constraint fk_cd_detail_group
foreign key (CD_GROUP_ID) references tb_cd_group (CD_GROUP_ID)
)
comment '코드 상세';

@ -0,0 +1,14 @@
create table tb_cd_group
(
CD_GROUP_ID varchar(20) not null comment '코드 그룹 ID'
primary key,
CD_GROUP_NM varchar(100) not null comment '코드 그룹 이름',
CD_GROUP_DC varchar(200) null comment '코드 그룹 설명',
USE_YN varchar(1) not null comment '사용 여부',
REG_DTTM datetime null comment '등록 일시',
RGTR varchar(20) null comment '등록자',
MDFCN_DTTM datetime null comment '수정 일시',
MDFR varchar(20) null comment '수정자'
)
comment '코드 그룹';

@ -0,0 +1,15 @@
create table tb_group
(
GROUP_ID varchar(20) not null comment '그룹 ID'
primary key,
GROUP_NM varchar(100) not null comment '그룹 이름',
GROUP_DC varchar(200) null comment '그룹 설명',
SORT_ORDR int default 0 null comment '정렬 순서',
USE_YN varchar(1) not null comment '사용 여부',
REG_DTTM datetime null comment '등록 일시',
RGTR varchar(20) null comment '등록자',
MDFCN_DTTM datetime null comment '수정 일시',
MDFR varchar(20) null comment '수정자'
)
comment '그룹';

@ -0,0 +1,14 @@
create table tb_group_role
(
GROUP_ID varchar(20) not null comment '그룹 ID',
ROLE_ID varchar(20) not null comment '역할 ID',
REG_DTTM datetime null comment '등록 일시',
RGTR varchar(20) null comment '등록자',
primary key (GROUP_ID, ROLE_ID),
constraint fk_group_role_group
foreign key (GROUP_ID) references tb_group (GROUP_ID),
constraint fk_group_role_role
foreign key (ROLE_ID) references tb_role (ROLE_ID)
)
comment '그룹-역할 매핑';

@ -0,0 +1,27 @@
create table tb_login_log
(
LOG_ID varchar(20) not null comment '로그 ID - 시퀀스 기반'
primary key,
USER_ID varchar(20) null comment '사용자 ID',
USER_ACNT varchar(20) null comment '사용자 계정',
LOGIN_DTTM datetime not null comment '로그인 시간',
IP_ADDR varchar(50) null comment 'IP 주소',
SUCCESS_YN varchar(1) not null comment '성공 여부',
FAIL_REASON varchar(100) null comment '실패 사유',
LOGIN_TYPE varchar(20) default 'WEB' null comment '로그인 유형',
SESSION_ID varchar(100) null comment '세션 ID',
USER_AGENT varchar(500) null comment '사용자 에이전트',
DEVICE_INFO varchar(100) null comment '접속 디바이스 정보',
REG_DTTM datetime default current_timestamp() null comment '등록 일시'
)
comment '로그인 로그';
create index IDX_TB_LOGIN_LOG_LOGIN_DTTM
on tb_login_log (LOGIN_DTTM);
create index IDX_TB_LOGIN_LOG_SUCCESS_YN
on tb_login_log (SUCCESS_YN);
create index IDX_TB_LOGIN_LOG_USER_ID
on tb_login_log (USER_ID);

@ -0,0 +1,23 @@
create table tb_menu
(
MENU_ID varchar(20) not null comment '메뉴 ID'
primary key,
MENU_NM varchar(100) not null comment '메뉴 이름',
MENU_DC varchar(200) null comment '메뉴 설명',
UPPER_MENU_ID varchar(20) null comment '상위 메뉴 ID',
MENU_LEVEL int not null comment '메뉴 레벨',
SORT_ORDR int not null comment '정렬 순서',
MENU_URL varchar(200) null comment '메뉴 URL',
URL_PATTERN varchar(2000) null comment 'URL 패턴',
MENU_ICON varchar(100) null comment '메뉴 아이콘',
USE_YN varchar(1) not null comment '사용 여부',
VIEW_YN varchar(1) default 'Y' not null comment '화면 표시 여부',
REG_DTTM datetime null comment '등록 일시',
RGTR varchar(20) null comment '등록자',
MDFCN_DTTM datetime null comment '수정 일시',
MDFR varchar(20) null comment '수정자',
constraint fk_menu_upper
foreign key (UPPER_MENU_ID) references tb_menu (MENU_ID)
)
comment '메뉴';

@ -0,0 +1,15 @@
create table tb_role
(
ROLE_ID varchar(20) not null comment '역할 ID'
primary key,
ROLE_NM varchar(100) not null comment '역할 이름',
ROLE_DC varchar(200) null comment '역할 설명',
SORT_ORDR int default 0 null comment '정렬 순서',
USE_YN varchar(1) not null comment '사용 여부',
REG_DTTM datetime null comment '등록 일시',
RGTR varchar(20) null comment '등록자',
MDFCN_DTTM datetime null comment '수정 일시',
MDFR varchar(20) null comment '수정자'
)
comment '역할';

@ -0,0 +1,14 @@
create table tb_role_menu
(
ROLE_ID varchar(20) not null comment '역할 ID',
MENU_ID varchar(20) not null comment '메뉴 ID',
REG_DTTM datetime null comment '등록 일시',
RGTR varchar(20) null comment '등록자',
primary key (ROLE_ID, MENU_ID),
constraint fk_role_menu_menu
foreign key (MENU_ID) references tb_menu (MENU_ID),
constraint fk_role_menu_role
foreign key (ROLE_ID) references tb_role (ROLE_ID)
)
comment '역할-메뉴 매핑';

@ -0,0 +1,44 @@
create table tb_user
(
USER_ID varchar(11) not null comment '사용자 ID'
primary key,
USER_ACNT varchar(20) not null comment '사용자 계정',
USER_NM varchar(50) not null comment '사용자 이름',
PASSWD varchar(200) not null comment '비밀번호',
PASSWD_HINT varchar(100) null comment '비밀번호 힌트',
PASSWD_NSR varchar(100) null comment '비밀번호 힌트 답',
EMP_NO varchar(20) null comment '사원 번호',
GENDER varchar(1) null comment '성별',
ZIP varchar(6) null comment '우편번호',
ADDR varchar(150) null comment '주소',
DADDR varchar(150) null comment '상세주소',
AREA_NO varchar(10) null comment '지역 번호',
EML_ADDR varchar(50) null comment '이메일 주소',
ORG_CD varchar(20) null comment '조직 CD',
USER_GROUP_ID varchar(20) null comment '그룹 ID',
NSTT_CD varchar(8) not null comment '소속기관 코드',
POS_NM varchar(60) null comment '직위 이름',
CRTFC_DN varchar(20) null comment '인증 DN값',
USER_STATUS_CD varchar(20) not null comment '사용자 상태',
FXNO varchar(20) null comment '팩스번호',
TELNO varchar(20) null comment '전화번호',
MBL_TELNO varchar(20) null comment '휴대 전화번호',
BRDT varchar(20) null comment '생년월일',
DEPT_CD varchar(7) null comment '부서 코드',
USE_YN varchar(1) not null comment '사용 여부',
RSDNT_NO varchar(200) null comment '주민등록 번호',
PASSWD_INIT_YN varchar(1) default 'N' null comment '비밀번호 초기화 여부',
LOCK_YN varchar(1) null comment '잠김 여부',
LOCK_CNT int not null comment '잠김 횟수',
LOCK_DTTM datetime null comment '잠김 일시',
REG_DTTM datetime null comment '등록 일시',
RGTR varchar(20) null comment '등록자',
MDFCN_DTTM datetime null comment '수정 일시',
MDFR varchar(20) null comment '수정자',
constraint tb_user_unique
unique (USER_ACNT),
constraint fk_user_group
foreign key (USER_GROUP_ID) references tb_group (GROUP_ID)
)
comment '사용자';

@ -0,0 +1,19 @@
create table tb_user_session
(
SESSION_ID varchar(100) not null comment '세션 ID'
primary key,
USER_ID varchar(20) not null comment '사용자 ID',
USER_ACNT varchar(20) null comment '사용자 계정',
LOGIN_DTTM datetime default current_timestamp() not null comment '로그인 시간',
LAST_ACCESS_DTTM datetime default current_timestamp() not null comment '마지막 접속 시간',
IP_ADDR varchar(50) null comment 'IP 주소',
USER_AGENT varchar(500) null comment '사용자 에이전트'
)
comment '사용자 세션 정보';
create index IDX_TB_USER_SESSION_LOGIN_DTTM
on tb_user_session (LOGIN_DTTM);
create index IDX_TB_USER_SESSION_USER_ID
on tb_user_session (USER_ID);

@ -0,0 +1,195 @@
/**
* XIT Framework Gradle
*
* :
* - Spring Boot: 2.7.18 ( )
* - Java: 1.8 ( )
* - : 4.3.0
*/
plugins {
// -
id 'org.springframework.boot' version '2.7.18'
// -
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
// -
id 'java'
// WAR -
id 'war'
}
//
group = 'go.kr.project'
version = '0.0.1-SNAPSHOT'
// ( )
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
// Java
//tasks.withType(JavaCompile) {
// options.encoding = 'UTF-8'
// options.compilerArgs += [
// '-Xlint:deprecation',
// '-Xlint:unchecked'
// ]
//}
//
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
//
repositories {
mavenCentral() //
maven { url 'https://maven.egovframe.go.kr/maven/' } //
}
//
ext {
//
tomcatVersion = '9.0.78' // 9 (Servlet 3.1 )
//
tilesVersion = '3.0.8' //
mybatisVersion = '2.3.1' //
commonsTextVersion = '1.10.0' //
egovFrameVersion = '4.3.0' //
}
dependencies {
// ===== =====
// - MVC, REST,
implementation 'org.springframework.boot:spring-boot-starter-web'
// - Bean Validation API
implementation 'org.springframework.boot:spring-boot-starter-validation'
// AOP -
implementation 'org.springframework.boot:spring-boot-starter-aop'
// JDBC - JDBC (HikariCP)
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
//implementation 'org.springframework.boot:spring-boot-starter-data-jpa' /* 실제 사용 X, intellij plugin 사용을 위해 설정 */
// ===== =====
// -
implementation("org.egovframe.rte:org.egovframe.rte.fdl.cmmn:${egovFrameVersion}") {
exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl' /* 보안이슈 대응 후 쿼리로그 문제 발생, SLF4J 충돌 발생 */
}
// MVC - MVC
implementation("org.egovframe.rte:org.egovframe.rte.ptl.mvc:${egovFrameVersion}") {
exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl' /* 보안이슈 대응 후 쿼리로그 문제 발생, SLF4J 충돌 발생 */
}
// ===== =====
// Logback -
implementation 'ch.qos.logback:logback-classic'
implementation 'ch.qos.logback:logback-core'
// SLF4J -
implementation 'org.slf4j:slf4j-api'
// ===== 릿 & JSP =====
// JSTL - JSP
implementation 'javax.servlet:jstl'
// ===== =====
// 9 (Servlet 3.1 )
implementation "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}"
implementation "org.apache.tomcat.embed:tomcat-embed-el:${tomcatVersion}"
implementation "org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}"
// ===== =====
// - 릿 (JSP )
implementation "org.apache.tiles:tiles-jsp:${tilesVersion}"
implementation "org.apache.tiles:tiles-core:${tilesVersion}"
implementation "org.apache.tiles:tiles-api:${tilesVersion}"
implementation "org.apache.tiles:tiles-servlet:${tilesVersion}"
implementation "org.apache.tiles:tiles-el:${tilesVersion}"
// ===== =====
// MyBatis - SQL
implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:${mybatisVersion}"
// MariaDB JDBC - MariaDB
implementation 'org.mariadb.jdbc:mariadb-java-client'
// HikariCP - JDBC (spring-boot-starter-jdbc )
// ===== =====
// Apache Commons Text -
implementation "org.apache.commons:commons-text:${commonsTextVersion}"
// ===== EXCEL =====
implementation 'org.apache.poi:poi:5.3.0'
implementation 'org.apache.poi:poi-ooxml:5.3.0'
// ===== Swagger UI =====
implementation 'org.springdoc:springdoc-openapi-ui:1.7.0'
implementation 'org.springdoc:springdoc-openapi-webmvc-core:1.7.0'
// ===== sqlPaser =====
implementation 'com.github.jsqlparser:jsqlparser:4.5'
// ===== datasource-proxy =====
// SQL datasource-proxy
implementation 'net.ttddyy:datasource-proxy:1.10.1'
// ===== =====
// Lombok - (Getter, Setter, Builder )
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// - ,
developmentOnly 'org.springframework.boot:spring-boot-devtools'
// ===== =====
// - JUnit, Mockito
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// ===== =====
// 릿 API -
providedCompile "org.apache.tomcat:tomcat-servlet-api:${tomcatVersion}"
// -
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
}
// ===== =====
// application*.yml @@ gradle.properties
processResources {
// application*.yml @projectName@
filesMatching('application*.yml') {
filter { line ->
line.replaceAll('@projectName@', project.property('projectName').toString())
}
}
}
// ===== =====
// JUnit (JUnit 5 )
tasks.named('test') {
useJUnitPlatform()
}
// ===== WAR =====
// WAR (gradle.properties )
war {
archiveFileName = "${projectName}.war"
}
// ===== bootWar =====
// bootWar (gradle.properties )
bootWar {
archiveFileName = "${projectName}-BOOT.war"
}
// war ,
// ./build/exploded/{프로젝트명}/
tasks.register('exploded', Copy) {
dependsOn 'war'
from zipTree(tasks.war.archiveFile)
into layout.buildDirectory.dir("exploded/${project.name}")
doFirst {
layout.buildDirectory.dir("exploded/${project.name}").get().asFile.deleteDir()
}
}

@ -0,0 +1,10 @@
# ???? ?? ?? ??
# ?????? ????? ???? gradle? application*.yml?? ?? ??
# ????? ???? (gradle ??? Spring Boot ???? ?? ??)
projectName=VIPS
# Gradle ?? ??
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true

@ -0,0 +1,864 @@
package egovframework.config;
import com.zaxxer.hikari.HikariDataSource;
import net.sf.jsqlparser.expression.CaseExpression;
import net.sf.jsqlparser.expression.CastExpression;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.WhenClause;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.update.UpdateSet;
import net.ttddyy.dsproxy.ExecutionInfo;
import net.ttddyy.dsproxy.QueryInfo;
import net.ttddyy.dsproxy.listener.QueryExecutionListener;
import net.ttddyy.dsproxy.listener.logging.DefaultQueryLogEntryCreator;
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener;
import net.ttddyy.dsproxy.proxy.ParameterSetOperation;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import javax.sql.DataSource;
import java.util.List;
/**
* DataSource Proxy
*
* datasource-proxy SQL
* MyBatis include, foreach
*
* Environment DataSource
* Multi DB
*
* @author XIT Framework
*/
@Configuration
public class DataSourceProxyConfig {
@Autowired
private Environment environment;
/**
* Primary
* Environment HikariDataSource
*/
@Bean
public DataSource actualDataSource() {
return createHikariDataSource("spring.datasource");
}
/**
* Secondary ( )
* Multi DB DB
*
* :
* @Bean
* public DataSource actualSecondaryDataSource() {
* return createHikariDataSource("spring.datasource.secondary");
* }
*/
/**
* HikariDataSource
*
* @param prefix application.yml prefix
* @return HikariDataSource
*/
private HikariDataSource createHikariDataSource(String prefix) {
HikariDataSource dataSource = new HikariDataSource();
// application.yml에서 설정값 읽기
dataSource.setJdbcUrl(environment.getProperty(prefix + ".url"));
dataSource.setUsername(environment.getProperty(prefix + ".username"));
dataSource.setPassword(environment.getProperty(prefix + ".password"));
dataSource.setDriverClassName(environment.getProperty(prefix + ".driver-class-name"));
// HikariCP 설정
dataSource.setMaximumPoolSize(environment.getProperty(prefix + ".hikari.maximum-pool-size", Integer.class, 10));
dataSource.setMinimumIdle(environment.getProperty(prefix + ".hikari.minimum-idle", Integer.class, 5));
dataSource.setConnectionTimeout(environment.getProperty(prefix + ".hikari.connection-timeout", Long.class, 30000L));
dataSource.setIdleTimeout(environment.getProperty(prefix + ".hikari.idle-timeout", Long.class, 600000L));
dataSource.setMaxLifetime(environment.getProperty(prefix + ".hikari.max-lifetime", Long.class, 1800000L));
dataSource.setValidationTimeout(environment.getProperty(prefix + ".hikari.validation-timeout", Long.class, 60000L));
return dataSource;
}
/**
* Primary
* actualDataSource SQL
*
* @param actualDataSource
* @return
*/
@Bean
@Primary
public DataSource dataSource(@Qualifier("actualDataSource") DataSource actualDataSource) {
return createProxyDataSource(actualDataSource, "PRIMARY-DB");
}
/**
* Secondary ( )
* Multi DB DB
*
* :
* @Bean
* public DataSource secondaryDataSource(@Qualifier("actualSecondaryDataSource") DataSource actualSecondaryDataSource) {
* return createProxyDataSource(actualSecondaryDataSource, "SECONDARY-DB");
* }
*/
/**
*
*
* @param actualDataSource
* @param dataSourceName ( )
* @return
*/
private DataSource createProxyDataSource(DataSource actualDataSource, String dataSourceName) {
// SLF4J 쿼리 로깅 리스너 생성
SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener();
// 로그 레벨 설정 (DEBUG 레벨로 출력)
loggingListener.setLogLevel(SLF4JLogLevel.DEBUG);
// 로거 이름 설정 (쿼리 로그 식별을 위함)
loggingListener.setLogger("go.kr.project.sql");
// 쿼리 로그 엔트리 생성자 설정
DefaultQueryLogEntryCreator logEntryCreator = new DefaultQueryLogEntryCreator();
logEntryCreator.setMultiline(true); // 멀티라인으로 보기 좋게 출력
loggingListener.setQueryLogEntryCreator(logEntryCreator);
// 커스텀 파라미터 바인딩 리스너 생성
CustomParameterBindingListener customListener = new CustomParameterBindingListener();
// 프록시 데이터소스 빌더를 사용하여 프록시 데이터소스 생성
return ProxyDataSourceBuilder
.create(actualDataSource)
.name(dataSourceName) // 데이터소스 이름 설정
.listener(loggingListener) // 기본 로깅 리스너 추가
.listener(customListener) // 커스텀 파라미터 바인딩 리스너 추가
.asJson() // JSON 형태로 파라미터 바인딩된 쿼리 출력
.build();
}
/**
* SQL
*/
private static class CustomParameterBindingListener implements QueryExecutionListener {
private static final Logger logger = LoggerFactory.getLogger("go.kr.project.sql.binding");
@Override
public void beforeQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) {
// 쿼리 실행 전 처리 (필요시 구현)
}
@Override
public void afterQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) {
try {
// 쿼리 실행 후 파라미터가 바인딩된 SQL 출력
for (QueryInfo queryInfo : queryInfoList) {
String query = queryInfo.getQuery();
List<List<ParameterSetOperation>> parametersList = queryInfo.getParametersList();
// Mapper 경로 및 메서드명 추출
String mapperInfo = extractMapperInfo();
logger.info(" ========================== Mapper: {} ========================== ", mapperInfo);
// 쿼리 실행 시간 및 기본 정보
long executionTime = execInfo.getElapsedTime();
logger.debug("실행 시간: {}ms", executionTime);
// 파라미터 값 추출
List<Object> parameterValues = extractParameterValues(parametersList);
// 파라미터 개수 정보
int questionMarkCount = countQuestionMarks(query);
logger.debug("파라미터 정보: ? 플레이스홀더 {}개, 바인딩 값 {}개", questionMarkCount, parameterValues.size());
// 파라미터 값들 전체 출력 (인덱스 포함)
if (!parameterValues.isEmpty()) {
logger.debug("파라미터 값 목록 (총 {}개):", parameterValues.size());
for (int i = 0; i < parameterValues.size(); i++) {
Object param = parameterValues.get(i);
String paramValue;
if (param == null) {
paramValue = "NULL";
} else if (param instanceof String) {
paramValue = "'" + param.toString() + "'";
} else {
paramValue = param.toString();
}
// 파라미터 인덱스와 함께 출력
logger.debug(" (parameter {}) = {}", i + 1, paramValue);
}
}
// 파라미터 바인딩된 SQL 생성
String boundQuery = bindParameters(query, parameterValues);
// SQL 포맷팅 적용 (jsqlparser 사용)
String formattedQuery = formatSql(boundQuery);
logger.info("\n{}\n", formattedQuery);
}
} finally {
// ThreadLocal 정리 (메모리 누수 방지)
// 쿼리 로깅이 완료된 후 반드시 정리
SqlLoggingInterceptor.clearCurrentMapperInfo();
}
}
/**
* SQL ?
* ?
*
* @param query SQL
* @return ?
*/
private int countQuestionMarks(String query) {
int count = 0;
boolean inBlockComment = false; // /* */ 블록 주석 내부인지 여부
boolean inLineComment = false; // -- 라인 주석 내부인지 여부
boolean inStringLiteral = false; // ' ' 문자열 리터럴 내부인지 여부
for (int i = 0; i < query.length(); i++) {
char currentChar = query.charAt(i);
char nextChar = (i + 1 < query.length()) ? query.charAt(i + 1) : '\0';
// 문자열 리터럴 처리 (홑따옴표로 둘러싸인 문자열)
if (currentChar == '\'' && !inBlockComment && !inLineComment) {
inStringLiteral = !inStringLiteral;
continue;
}
// 블록 주석 시작 /* 검사
if (!inStringLiteral && !inLineComment && currentChar == '/' && nextChar == '*') {
inBlockComment = true;
i++; // 다음 문자도 건너뛰기
continue;
}
// 블록 주석 끝 */ 검사
if (!inStringLiteral && inBlockComment && currentChar == '*' && nextChar == '/') {
inBlockComment = false;
i++; // 다음 문자도 건너뛰기
continue;
}
// 라인 주석 시작 -- 검사
if (!inStringLiteral && !inBlockComment && currentChar == '-' && nextChar == '-') {
inLineComment = true;
i++; // 다음 문자도 건너뛰기
continue;
}
// 라인 주석 끝 (줄바꿈) 검사
if (inLineComment && (currentChar == '\n' || currentChar == '\r')) {
inLineComment = false;
continue;
}
// ? 플레이스홀더 카운트 (주석이나 문자열 리터럴 내부가 아닌 경우에만)
if (currentChar == '?' && !inBlockComment && !inLineComment && !inStringLiteral) {
count++;
}
}
return count;
}
/**
* MyBatis Interceptor Mapper
* SqlLoggingInterceptor ThreadLocal
*
* @return Mapper (: go.kr.project.login.mapper.LoginMapper.selectMenusByRoleIds)
*/
private String extractMapperInfo() {
try {
// SqlLoggingInterceptor에서 제공하는 정확한 Mapper 정보 사용
String mapperInfo = SqlLoggingInterceptor.getCurrentMapperInfo();
return mapperInfo != null ? mapperInfo : "Unknown Mapper";
} catch (Exception e) {
logger.warn("Mapper 정보 추출 실패: {}", e.getMessage());
return "Unknown Mapper";
}
}
/**
* ParameterSetOperation
* (Integer, String, Date ) null
*/
private List<Object> extractParameterValues(List<List<ParameterSetOperation>> parametersList) {
java.util.Map<Integer, Object> parameterMap = new java.util.TreeMap<>();
if (parametersList != null && !parametersList.isEmpty()) {
// 첫 번째 파라미터 세트를 사용 (일반적으로 PreparedStatement는 하나의 파라미터 세트를 가짐)
List<ParameterSetOperation> operations = parametersList.get(0);
if (operations != null) {
for (ParameterSetOperation operation : operations) {
Object[] args = operation.getArgs();
if (args != null && args.length >= 1) {
Integer paramIndex = (Integer) args[0];
Object paramValue = null;
// 리플렉션을 사용하여 실제 메서드명 추출
String methodName = extractMethodName(operation);
//logger.debug("[METHOD_INFO] 파라미터 {}번 - 메서드명: {}, args 길이: {}",
// paramIndex, methodName, args.length);
// 메서드명을 통한 정확한 null 처리
if ("setNull".equals(methodName)) {
// setNull 메서드 호출 - 확실하게 null 값으로 설정
paramValue = null;
//logger.debug("[NULL_SET] setNull 메서드 호출됨 - 파라미터 {}번을 null로 설정", paramIndex);
} else {
// 다른 모든 set 메서드들 (setString, setInt, setObject 등)
if (args.length >= 2) {
paramValue = args[1];
// args[1]이 null인 경우에도 null로 처리
if (paramValue == null) {
//logger.debug("[NULL_VALUE] 파라미터 {}번 값이 null임", paramIndex);
} else {
//logger.debug("[VALUE_SET] 파라미터 {}번 - 메서드: {}, 값: {} (타입: {})",
// paramIndex, methodName, paramValue, paramValue.getClass().getSimpleName());
}
} else {
logger.warn("[WARN] 파라미터 {}번 - 메서드: {}, args 길이 부족: {}",
paramIndex, methodName, args.length);
}
}
parameterMap.put(paramIndex, paramValue);
}
}
}
}
// TreeMap을 사용했으므로 자동으로 인덱스 순서대로 정렬됨
return new java.util.ArrayList<>(parameterMap.values());
}
/**
* ParameterSetOperation
*
*/
private String extractMethodName(ParameterSetOperation operation) {
try {
// ParameterSetOperation의 getMethod() 메서드 호출을 시도
java.lang.reflect.Method getMethodMethod = operation.getClass().getMethod("getMethod");
Object method = getMethodMethod.invoke(operation);
if (method instanceof java.lang.reflect.Method) {
return ((java.lang.reflect.Method) method).getName();
}
// getMethod가 없거나 실패한 경우 toString()에서 메서드명 추출 시도
String operationStr = operation.toString();
if (operationStr != null) {
// "MethodName(args...)" 형태에서 메서드명 추출
int openParen = operationStr.indexOf('(');
if (openParen > 0) {
String methodPart = operationStr.substring(0, openParen);
int lastDot = methodPart.lastIndexOf('.');
return lastDot >= 0 ? methodPart.substring(lastDot + 1) : methodPart;
}
}
} catch (Exception e) {
logger.debug("[METHOD_EXTRACT_ERROR] 메서드명 추출 실패: {}", e.getMessage());
}
return "unknown";
}
/**
* SQL .
* (/ * * /) .
*
* @param sql SQL
* @return SQL
*/
private String formatSql(String sql) {
// 주석이 포함된 경우 원본 SQL을 그대로 반환 (주석 위치 보존)
if (sql.contains("/*")) {
return sql;
}
// 주석이 없는 경우에만 jsqlparser로 포맷팅
try {
Statement statement = CCJSqlParserUtil.parse(sql);
if (statement instanceof Select) {
return formatSelectStatement((Select) statement);
} else if (statement instanceof Insert) {
return formatInsertStatement((Insert) statement);
} else if (statement instanceof Update) {
return formatUpdateStatement((Update) statement);
} else {
// SELECT, INSERT, UPDATE 외 다른 구문(DELETE 등)은 기본 포맷팅 적용
return applyBasicFormatting(statement.toString());
}
} catch (Exception e) {
// SQL 파싱 실패 시 원본 SQL 반환
logger.info("SQL 파싱 실패, 원본 SQL 반환: {}", e.getMessage());
return sql;
}
}
/**
* UPDATE AST .
*/
private String formatUpdateStatement(Update update) {
StringBuilder sb = new StringBuilder();
sb.append("UPDATE ").append(update.getTable());
// SET clause
if (update.getUpdateSets() != null && !update.getUpdateSets().isEmpty()) {
sb.append("\nSET");
List<UpdateSet> updateSets = update.getUpdateSets();
for (int i = 0; i < updateSets.size(); i++) {
if (i > 0) {
sb.append(",");
}
UpdateSet set = updateSets.get(i);
sb.append("\n ");
if (set.getColumns().size() > 1) sb.append("(");
for (int j = 0; j < set.getColumns().size(); j++) {
if (j > 0) sb.append(", ");
sb.append(set.getColumns().get(j));
}
if (set.getColumns().size() > 1) sb.append(")");
sb.append(" = ");
if (set.getExpressions().size() > 1) sb.append("(");
for (int j = 0; j < set.getExpressions().size(); j++) {
if (j > 0) sb.append(", ");
sb.append(formatComplexExpression(set.getExpressions().get(j), 2));
}
if (set.getExpressions().size() > 1) sb.append(")");
}
}
// FROM clause (for some dialects)
if (update.getFromItem() != null) {
sb.append("\nFROM ").append(update.getFromItem());
}
// JOIN clause (for some dialects)
if (update.getJoins() != null) {
for (Join join : update.getJoins()) {
sb.append("\n").append(formatJoin(join));
}
}
// WHERE clause
if (update.getWhere() != null) {
sb.append("\nWHERE");
appendExpression(update.getWhere(), sb, 1, false);
}
return sb.toString();
}
/**
* INSERT AST .
* .
* INSERT ... SELECT .
*/
private String formatInsertStatement(Insert insert) {
StringBuilder sb = new StringBuilder();
sb.append("INSERT INTO ").append(insert.getTable());
// 컬럼 목록 포맷팅
if (insert.getColumns() != null && !insert.getColumns().isEmpty()) {
sb.append("(");
for (int i = 0; i < insert.getColumns().size(); i++) {
sb.append("\n ").append(insert.getColumns().get(i).getColumnName());
if (i < insert.getColumns().size() - 1) {
sb.append(",");
}
}
sb.append("\n)");
}
// 값 목록 포맷팅
if (insert.getItemsList() != null) {
if (insert.getItemsList() instanceof SubSelect) {
// INSERT ... SELECT 구문인 경우
sb.append("\n");
SubSelect subSelect = (SubSelect) insert.getItemsList();
formatSelectBody(subSelect.getSelectBody(), sb, 0);
} else {
sb.append("\nVALUES\n(");
if (insert.getItemsList() instanceof ExpressionList) {
List<Expression> values = ((ExpressionList) insert.getItemsList()).getExpressions();
for (int i = 0; i < values.size(); i++) {
sb.append("\n ").append(formatComplexExpression(values.get(i), 2));
if (i < values.size() - 1) {
sb.append(",");
}
}
} else {
sb.append("\n ").append(insert.getItemsList().toString());
}
sb.append("\n)");
}
}
return sb.toString();
}
/**
* SELECT AST .
*
* @param select JSqlParser Select
* @return SELECT
*/
private String formatSelectStatement(Select select) {
StringBuilder sb = new StringBuilder();
SelectBody selectBody = select.getSelectBody();
// 재귀적으로 SelectBody를 처리 (UNION 등 복합 쿼리 지원)
formatSelectBody(selectBody, sb, 0);
return sb.toString();
}
/**
* SelectBody UNION .
*
* @param selectBody SelectBody
* @param sb StringBuilder
* @param indentLevel
*/
private void formatSelectBody(SelectBody selectBody, StringBuilder sb, int indentLevel) {
String indent = getIndent(indentLevel);
if (selectBody instanceof PlainSelect) {
PlainSelect plainSelect = (PlainSelect) selectBody;
// SELECT 절
sb.append(indent).append("SELECT");
if (plainSelect.getDistinct() != null) {
sb.append(" ").append(plainSelect.getDistinct());
}
appendSelectItems(plainSelect.getSelectItems(), sb, indentLevel + 1);
// FROM 절
if (plainSelect.getFromItem() != null) {
sb.append("\n").append(indent).append("FROM ").append(plainSelect.getFromItem());
}
// JOIN 절
if (plainSelect.getJoins() != null) {
for (Join join : plainSelect.getJoins()) {
sb.append("\n").append(indent).append(formatJoin(join));
}
}
// WHERE 절
if (plainSelect.getWhere() != null) {
sb.append("\n").append(indent).append("WHERE");
appendExpression(plainSelect.getWhere(), sb, indentLevel + 1, false);
}
// GROUP BY 절
if (plainSelect.getGroupBy() != null) {
sb.append("\n").append(indent).append("GROUP BY ").append(plainSelect.getGroupBy().toString());
}
// HAVING 절
if (plainSelect.getHaving() != null) {
sb.append("\n").append(indent).append("HAVING");
appendExpression(plainSelect.getHaving(), sb, indentLevel + 1, false);
}
// ORDER BY 절
if (plainSelect.getOrderByElements() != null && !plainSelect.getOrderByElements().isEmpty()) {
sb.append("\n").append(indent).append("ORDER BY ");
List<OrderByElement> orderByElements = plainSelect.getOrderByElements();
for (int i = 0; i < orderByElements.size(); i++) {
if (i > 0) {
sb.append(", ");
}
OrderByElement element = orderByElements.get(i);
sb.append(element.getExpression().toString());
if (element.isAsc()) {
sb.append(" ASC");
} else if (!element.isAsc() && element.toString().contains("DESC")) {
sb.append(" DESC");
}
}
}
// LIMIT 절
if (plainSelect.getLimit() != null) {
sb.append("\n").append(indent).append(plainSelect.getLimit());
}
} else if (selectBody instanceof SetOperationList) {
SetOperationList setOpList = (SetOperationList) selectBody;
for (int i = 0; i < setOpList.getSelects().size(); i++) {
if (i > 0) {
sb.append("\n").append(indent).append(setOpList.getOperations().get(i - 1)).append("\n");
}
formatSelectBody(setOpList.getSelects().get(i), sb, indentLevel);
}
} else {
// 기타 SelectBody 타입은 기본 toString() 사용
sb.append(indent).append(selectBody.toString());
}
}
/**
* SELECT .
*/
private void appendSelectItems(List<SelectItem> selectItems, StringBuilder sb, int indentLevel) {
String indent = getIndent(indentLevel);
for (int i = 0; i < selectItems.size(); i++) {
sb.append("\n").append(indent);
if (i > 0) {
sb.append(", ");
}
SelectItem item = selectItems.get(i);
if (item instanceof SelectExpressionItem) {
SelectExpressionItem exprItem = (SelectExpressionItem) item;
// CASE 표현식 등을 포함한 복잡한 표현식 포맷팅
sb.append(formatComplexExpression(exprItem.getExpression(), indentLevel));
if (exprItem.getAlias() != null) {
sb.append(" AS ").append(exprItem.getAlias().getName());
}
} else {
sb.append(item.toString());
}
}
}
/**
* (WHERE, HAVING) .
* AND, OR .
*/
private void appendExpression(Expression expression, StringBuilder sb, int indentLevel, boolean isNested) {
String indent = getIndent(indentLevel);
if (expression instanceof AndExpression) {
AndExpression and = (AndExpression) expression;
appendExpression(and.getLeftExpression(), sb, indentLevel, isNested);
sb.append("\n").append(indent).append("AND ");
appendExpression(and.getRightExpression(), sb, indentLevel, true);
} else if (expression instanceof OrExpression) {
OrExpression or = (OrExpression) expression;
appendExpression(or.getLeftExpression(), sb, indentLevel, isNested);
sb.append("\n").append(indent).append("OR ");
appendExpression(or.getRightExpression(), sb, indentLevel, true);
} else {
if (!isNested) {
sb.append("\n").append(indent);
}
sb.append(formatComplexExpression(expression, indentLevel));
}
}
/**
* CASE .
*/
private String formatComplexExpression(Expression expression, int indentLevel) {
if (expression instanceof CaseExpression) {
return formatCaseExpression((CaseExpression) expression, indentLevel);
}
if (expression instanceof CastExpression) {
return formatCastExpression((CastExpression) expression, indentLevel);
}
// 다른 복잡한 표현식들도 여기에 추가 가능
return expression.toString();
}
/**
* CASE .
*/
private String formatCaseExpression(CaseExpression caseExpr, int indentLevel) {
String indent = getIndent(indentLevel);
String innerIndent = getIndent(indentLevel + 1);
StringBuilder sb = new StringBuilder("CASE");
if (caseExpr.getSwitchExpression() != null) {
sb.append(" ").append(caseExpr.getSwitchExpression().toString());
}
for (WhenClause when : caseExpr.getWhenClauses()) {
sb.append("\n").append(innerIndent).append("WHEN ").append(when.getWhenExpression().toString());
sb.append(" THEN ").append(when.getThenExpression().toString());
}
if (caseExpr.getElseExpression() != null) {
sb.append("\n").append(innerIndent).append("ELSE ").append(caseExpr.getElseExpression().toString());
}
sb.append("\n").append(indent).append("END");
return sb.toString();
}
/**
* CAST .
*/
private String formatCastExpression(CastExpression castExpr, int indentLevel) {
return "CAST(" + formatComplexExpression(castExpr.getLeftExpression(), indentLevel) + " AS " + castExpr.getType().toString() + ")";
}
/**
* JOIN .
*/
private String formatJoin(Join join) {
String joinType = "";
if (join.isSimple()) joinType += " ";
if (join.isCross()) joinType += "CROSS ";
if (join.isFull()) joinType += "FULL ";
if (join.isInner()) joinType += "INNER ";
if (join.isLeft()) joinType += "LEFT ";
if (join.isNatural()) joinType += "NATURAL ";
if (join.isOuter()) joinType += "OUTER ";
if (join.isRight()) joinType += "RIGHT ";
if (join.isSemi()) joinType += "SEMI ";
return joinType + "JOIN " + join.getRightItem() + (join.getOnExpression() != null ? " ON " + join.getOnExpression() : "");
}
/**
* INSERT, UPDATE, DELETE .
*/
private String applyBasicFormatting(String sql) {
return sql.replaceAll("(?i)\\b(SET|VALUES|WHERE)\\b", "\\n$1");
}
/**
* .
*/
private String getIndent(int level) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < level; i++) {
sb.append(" "); // 4 spaces for each level
}
return sb.toString();
}
/**
* SQL ?
* ?
*/
private String bindParameters(String query, List<Object> parameters) {
if (parameters == null || parameters.isEmpty()) {
return query;
}
StringBuilder result = new StringBuilder();
int paramIndex = 0;
int queryIndex = 0;
boolean inBlockComment = false; // /* */ 블록 주석 내부인지 여부
boolean inLineComment = false; // -- 라인 주석 내부인지 여부
boolean inStringLiteral = false; // ' ' 문자열 리터럴 내부인지 여부
while (queryIndex < query.length()) {
char currentChar = query.charAt(queryIndex);
char nextChar = (queryIndex + 1 < query.length()) ? query.charAt(queryIndex + 1) : '\0';
// 문자열 리터럴 처리 (홑따옴표로 둘러싸인 문자열)
if (currentChar == '\'' && !inBlockComment && !inLineComment) {
inStringLiteral = !inStringLiteral;
result.append(currentChar);
queryIndex++;
continue;
}
// 블록 주석 시작 /* 검사
if (!inStringLiteral && !inLineComment && currentChar == '/' && nextChar == '*') {
inBlockComment = true;
result.append(currentChar);
result.append(nextChar);
queryIndex += 2;
continue;
}
// 블록 주석 끝 */ 검사
if (!inStringLiteral && inBlockComment && currentChar == '*' && nextChar == '/') {
inBlockComment = false;
result.append(currentChar);
result.append(nextChar);
queryIndex += 2;
continue;
}
// 라인 주석 시작 -- 검사
if (!inStringLiteral && !inBlockComment && currentChar == '-' && nextChar == '-') {
inLineComment = true;
result.append(currentChar);
result.append(nextChar);
queryIndex += 2;
continue;
}
// 라인 주석 끝 (줄바꿈) 검사
if (inLineComment && (currentChar == '\n' || currentChar == '\r')) {
inLineComment = false;
result.append(currentChar);
queryIndex++;
continue;
}
// ? 파라미터 플레이스홀더 처리 (주석이나 문자열 리터럴 내부가 아닌 경우에만)
if (currentChar == '?' && !inBlockComment && !inLineComment && !inStringLiteral) {
if (paramIndex < parameters.size()) {
// ? 플레이스홀더를 실제 파라미터 값으로 치환
Object param = parameters.get(paramIndex);
String paramValue;
if (param == null) {
paramValue = "NULL";
} else if (param instanceof String) {
paramValue = "'" + param.toString().replace("'", "''") + "'";
} else if (param instanceof java.util.Date || param instanceof java.time.LocalDateTime || param instanceof java.time.LocalDate) {
paramValue = "'" + param.toString() + "'";
} else {
paramValue = param.toString();
}
result.append(paramValue);
paramIndex++;
} else {
// 파라미터가 부족한 경우 ? 그대로 유지
result.append('?');
}
} else {
// 그 외의 모든 문자는 그대로 복사
result.append(currentChar);
}
queryIndex++;
}
return result.toString();
}
}
}

@ -0,0 +1,60 @@
package egovframework.config;
import org.egovframe.rte.fdl.cmmn.trace.LeaveaTrace;
import org.egovframe.rte.fdl.cmmn.trace.handler.DefaultTraceHandler;
import org.egovframe.rte.fdl.cmmn.trace.handler.TraceHandler;
import org.egovframe.rte.fdl.cmmn.trace.manager.DefaultTraceHandleManager;
import org.egovframe.rte.fdl.cmmn.trace.manager.TraceHandlerService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.util.AntPathMatcher;
@Configuration
public class EgovConfigCommon {
@Bean
public AntPathMatcher antPathMatcher() {
return new AntPathMatcher();
}
@Bean
public DefaultTraceHandler defaultTraceHandler() {
return new DefaultTraceHandler();
}
@Bean
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource = new ReloadableResourceBundleMessageSource();
reloadableResourceBundleMessageSource.setBasenames(
"classpath:/egovframework/message/message-common",
"classpath:/org/egovframe/rte/fdl/idgnr/messages/idgnr",
"classpath:/org/egovframe/rte/fdl/property/messages/properties");
reloadableResourceBundleMessageSource.setDefaultEncoding("UTF-8");
reloadableResourceBundleMessageSource.setCacheSeconds(60);
return reloadableResourceBundleMessageSource;
}
@Bean
public MessageSourceAccessor messageSourceAccessor() {
return new MessageSourceAccessor(this.messageSource());
}
@Bean
public DefaultTraceHandleManager traceHandlerService() {
DefaultTraceHandleManager defaultTraceHandleManager = new DefaultTraceHandleManager();
defaultTraceHandleManager.setReqExpMatcher(antPathMatcher());
defaultTraceHandleManager.setPatterns(new String[]{"*"});
defaultTraceHandleManager.setHandlers(new TraceHandler[]{defaultTraceHandler()});
return defaultTraceHandleManager;
}
@Bean
public LeaveaTrace leaveaTrace() {
LeaveaTrace leaveaTrace = new LeaveaTrace();
leaveaTrace.setTraceHandlerServices(new TraceHandlerService[]{traceHandlerService()});
return leaveaTrace;
}
}

@ -0,0 +1,42 @@
package egovframework.config;
import egovframework.configProperties.InterceptorProperties;
import egovframework.interceptor.AuthInterceptor;
import go.kr.project.login.service.LoginService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
*
*/
@Configuration
@RequiredArgsConstructor
public class EgovConfigInterceptor implements WebMvcConfigurer {
private final LoginService loginService;
private final InterceptorProperties interceptorProperties;
/**
*
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor())
.addPathPatterns("/**") // 모든 경로에 적용
.excludePathPatterns(interceptorProperties.getInterceptorExclude()); // 접근 제어 예외 URL 패턴 제외
}
/**
*
* @return AuthInterceptor
*/
@Bean
public AuthInterceptor authInterceptor() {
return new AuthInterceptor(loginService);
}
}

@ -0,0 +1,95 @@
package egovframework.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.interceptor.*;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.util.Collections;
import java.util.HashMap;
/**
* @ClassName : EgovConfigAppTransaction.java
* @Description : Transaction
*
* @author :
* @since : 2021. 7. 20
* @version : 1.0
*
* <pre>
* << (Modification Information) >>
*
*
* ------------- ------------ ---------------------
* 2021. 7. 20
* </pre>
*
*/
@Slf4j
@Configuration
public class EgovConfigTransaction {
@Autowired
DataSource dataSource;
@PostConstruct
public void init() {
log.info("Datasource type: {}", dataSource.getClass().getName());
}
@Bean
public DataSourceTransactionManager txManager() {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
// -------------------------------------------------------------
// TransactionAdvice 설정
// -------------------------------------------------------------
@Bean
public TransactionInterceptor txAdvice(DataSourceTransactionManager txManager) {
TransactionInterceptor txAdvice = new TransactionInterceptor();
txAdvice.setTransactionManager(txManager);
txAdvice.setTransactionAttributeSource(getNameMatchTransactionAttributeSource());
return txAdvice;
}
private NameMatchTransactionAttributeSource getNameMatchTransactionAttributeSource() {
NameMatchTransactionAttributeSource txAttributeSource = new NameMatchTransactionAttributeSource();
txAttributeSource.setNameMap(getRuleBasedTxAttributeMap());
return txAttributeSource;
}
private HashMap<String, TransactionAttribute> getRuleBasedTxAttributeMap() {
HashMap<String, TransactionAttribute> txMethods = new HashMap<String, TransactionAttribute>();
RuleBasedTransactionAttribute txAttribute = new RuleBasedTransactionAttribute();
txAttribute.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
txAttribute.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
txMethods.put("*", txAttribute);
return txMethods;
}
// -------------------------------------------------------------
// TransactionAdvisor 설정
// -------------------------------------------------------------
@Bean
public Advisor txAdvisor(DataSourceTransactionManager txManager) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(
"execution(* go.kr.project..impl.*Impl.*(..)) or execution(* egovframework.com..*Impl.*(..))");
return new DefaultPointcutAdvisor(pointcut, txAdvice(txManager));
}
}

@ -0,0 +1,112 @@
package egovframework.config;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@Setter
@Configuration
@RequiredArgsConstructor
public class EgovConfigWeb implements WebMvcConfigurer, ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/plugins/**")
.addResourceLocations("/resources/plugins/")
.setCacheControl(CacheControl.noCache().mustRevalidate());
registry.addResourceHandler("/css/**")
.addResourceLocations("/resources/css/")
.setCacheControl(CacheControl.noCache().mustRevalidate());
registry.addResourceHandler("/img/**")
.addResourceLocations("/resources/img/")
.setCacheControl(CacheControl.noCache().mustRevalidate());
registry.addResourceHandler("/js/**")
.addResourceLocations("/resources/js/")
.setCacheControl(CacheControl.noCache().mustRevalidate());
registry.addResourceHandler("/xit/**")
.addResourceLocations("/resources/xit/")
.setCacheControl(CacheControl.noCache().mustRevalidate());
registry.addResourceHandler("/font/**")
.addResourceLocations("/resources/font/")
.setCacheControl(CacheControl.noCache().mustRevalidate());
}
/**
* Interceptor
* : .
* js, jsp, css ,
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 중요로직: HTTP 캐시 방지 헤더 설정
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1
response.setHeader("Pragma", "no-cache"); // HTTP 1.0
response.setDateHeader("Expires", 0); // Proxies
return true;
}
}).addPathPatterns("/**"); // 모든 경로에 적용
}
/**
* Exception handling is now done using @ControllerAdvice in EgovExceptionAdvice class.
* The SimpleMappingExceptionResolver approach has been replaced with a more modern approach.
*
* @see egovframework.exception.EgovExceptionAdvice
*/
@Override
public void configureHandlerExceptionResolvers(@Nullable List< HandlerExceptionResolver> resolvers) {
// Exception handling is now done using @ControllerAdvice
}
@Bean
public UrlBasedViewResolver viewResolver() {
UrlBasedViewResolver tilesViewResolver = new UrlBasedViewResolver();
tilesViewResolver.setViewClass(TilesView.class);
tilesViewResolver.setOrder(1);
return tilesViewResolver;
}
@Bean
public TilesConfigurer tilesConfigurer() {
TilesConfigurer tilesConfigurer = new TilesConfigurer();
tilesConfigurer.setDefinitions(
"/WEB-INF/tiles/tiles.xml"
);
tilesConfigurer.setCheckRefresh(true);
return tilesConfigurer;
}
@Bean
public InternalResourceViewResolver internalResourceViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setOrder(2);
return resolver;
}
}

@ -0,0 +1,116 @@
package egovframework.config;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.ErrorPageRegistrar;
import org.springframework.boot.web.server.ErrorPageRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
/**
*
* 404, 500 HTTP .
*/
@Configuration
public class EgovErrorConfig {
/**
* ErrorPageRegistrar .
*
* @return ErrorPageRegistrar
*/
@Bean
public ErrorPageRegistrar errorPageRegistrar() {
return new ErrorPageRegistrar() {
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
// 404 에러 페이지 설정
ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/error/404");
// 403 에러 페이지 설정 (권한 없음)
ErrorPage error403Page = new ErrorPage(HttpStatus.FORBIDDEN, "/error/404");
// AccessDeniedException 예외 처리를 위한 에러 페이지 설정
ErrorPage accessDeniedPage = new ErrorPage(egovframework.exception.AccessDeniedException.class, "/error/404");
// 클라이언트 에러(4xx) 페이지 설정
// 400 에러 페이지 설정 (잘못된 요청)
ErrorPage error400Page = new ErrorPage(HttpStatus.BAD_REQUEST, "/error/404");
// 401 에러 페이지 설정 (인증 실패)
ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/error/404");
// 405 에러 페이지 설정 (허용되지 않는 메소드)
ErrorPage error405Page = new ErrorPage(HttpStatus.METHOD_NOT_ALLOWED, "/error/404");
// 406 에러 페이지 설정 (허용되지 않는 형식)
ErrorPage error406Page = new ErrorPage(HttpStatus.NOT_ACCEPTABLE, "/error/404");
// 408 에러 페이지 설정 (요청 시간 초과)
ErrorPage error408Page = new ErrorPage(HttpStatus.REQUEST_TIMEOUT, "/error/404");
// 409 에러 페이지 설정 (충돌)
ErrorPage error409Page = new ErrorPage(HttpStatus.CONFLICT, "/error/404");
// 410 에러 페이지 설정 (리소스 사라짐)
ErrorPage error410Page = new ErrorPage(HttpStatus.GONE, "/error/404");
// 413 에러 페이지 설정 (요청 엔티티가 너무 큼)
ErrorPage error413Page = new ErrorPage(HttpStatus.PAYLOAD_TOO_LARGE, "/error/404");
// 414 에러 페이지 설정 (URI가 너무 김)
ErrorPage error414Page = new ErrorPage(HttpStatus.URI_TOO_LONG, "/error/404");
// 415 에러 페이지 설정 (지원되지 않는 미디어 타입)
ErrorPage error415Page = new ErrorPage(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "/error/404");
// 429 에러 페이지 설정 (너무 많은 요청)
ErrorPage error429Page = new ErrorPage(HttpStatus.TOO_MANY_REQUESTS, "/error/404");
// 500 에러 페이지 설정
ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500");
// 기타 서버 에러 페이지 설정
ErrorPage error501Page = new ErrorPage(HttpStatus.NOT_IMPLEMENTED, "/error/500");
ErrorPage error502Page = new ErrorPage(HttpStatus.BAD_GATEWAY, "/error/500");
ErrorPage error503Page = new ErrorPage(HttpStatus.SERVICE_UNAVAILABLE, "/error/500");
ErrorPage error504Page = new ErrorPage(HttpStatus.GATEWAY_TIMEOUT, "/error/500");
ErrorPage error505Page = new ErrorPage(HttpStatus.HTTP_VERSION_NOT_SUPPORTED, "/error/500");
// 예외 처리를 위한 에러 페이지 설정
ErrorPage runtimeExceptionPage = new ErrorPage(RuntimeException.class, "/error/500");
// 추가 예외 처리를 위한 에러 페이지 설정
ErrorPage illegalArgumentExceptionPage = new ErrorPage(IllegalArgumentException.class, "/error/500");
ErrorPage nullPointerExceptionPage = new ErrorPage(NullPointerException.class, "/error/500");
ErrorPage illegalStateExceptionPage = new ErrorPage(IllegalStateException.class, "/error/500");
ErrorPage ioExceptionPage = new ErrorPage(java.io.IOException.class, "/error/500");
ErrorPage sqlExceptionPage = new ErrorPage(java.sql.SQLException.class, "/error/500");
ErrorPage dataAccessExceptionPage = new ErrorPage(org.springframework.dao.DataAccessException.class, "/error/500");
ErrorPage arithmeticExceptionPage = new ErrorPage(ArithmeticException.class, "/error/500");
ErrorPage classNotFoundExceptionPage = new ErrorPage(ClassNotFoundException.class, "/error/500");
ErrorPage noSuchMethodExceptionPage = new ErrorPage(NoSuchMethodException.class, "/error/500");
ErrorPage indexOutOfBoundsExceptionPage = new ErrorPage(IndexOutOfBoundsException.class, "/error/500");
ErrorPage concurrentModificationExceptionPage = new ErrorPage(java.util.ConcurrentModificationException.class, "/error/500");
// 에러 페이지 등록
registry.addErrorPages(
// 4xx 에러
error400Page, error401Page, error403Page, error404Page, error405Page,
error406Page, error408Page, error409Page, error410Page, error413Page,
error414Page, error415Page, error429Page,
// 5xx 에러
error500Page, error501Page, error502Page, error503Page, error504Page, error505Page,
// 예외 처리
runtimeExceptionPage, illegalArgumentExceptionPage, nullPointerExceptionPage,
illegalStateExceptionPage, ioExceptionPage, sqlExceptionPage,
dataAccessExceptionPage, arithmeticExceptionPage, classNotFoundExceptionPage,
noSuchMethodExceptionPage, indexOutOfBoundsExceptionPage, concurrentModificationExceptionPage,
accessDeniedPage
);
}
};
}
}

@ -0,0 +1,34 @@
package egovframework.config;
import com.fasterxml.jackson.core.SerializableString;
import com.fasterxml.jackson.core.io.CharacterEscapes;
import com.fasterxml.jackson.core.io.SerializedString;
import org.apache.commons.text.StringEscapeUtils;
public class HtmlCharacterEscapes extends CharacterEscapes {
private static final long serialVersionUID = -6353236148390563705L;
private final int[] asciiEscapes;
public HtmlCharacterEscapes() {
this.asciiEscapes = CharacterEscapes.standardAsciiEscapesForJSON();
this.asciiEscapes['<'] = CharacterEscapes.ESCAPE_CUSTOM;
this.asciiEscapes['>'] = CharacterEscapes.ESCAPE_CUSTOM;
this.asciiEscapes['\"'] = CharacterEscapes.ESCAPE_CUSTOM;
this.asciiEscapes['('] = CharacterEscapes.ESCAPE_CUSTOM;
this.asciiEscapes[')'] = CharacterEscapes.ESCAPE_CUSTOM;
this.asciiEscapes['#'] = CharacterEscapes.ESCAPE_CUSTOM;
this.asciiEscapes['\''] = CharacterEscapes.ESCAPE_CUSTOM;
}
@Override
public int[] getEscapeCodesForAscii() {
return asciiEscapes;
}
@Override
public SerializableString getEscapeSequence(int ch) {
return new SerializedString(StringEscapeUtils.escapeHtml4(Character.toString((char) ch)));
}
}

@ -0,0 +1,107 @@
package egovframework.config;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Properties;
/**
* MyBatis Interceptor SQL Mapper
*
* MappedStatement namespace(Mapper ) id()
* ThreadLocal DataSourceProxy .
*
* @author XIT Framework
*/
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class SqlLoggingInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(SqlLoggingInterceptor.class);
/**
* ThreadLocal Mapper
* DataSourceProxy Mapper
*/
private static final ThreadLocal<String> CURRENT_MAPPER_INFO = new ThreadLocal<>();
/**
* Mapper
*
* @return Mapper . (: go.kr.project.login.mapper.LoginMapper.selectMenusByRoleIds)
*/
public static String getCurrentMapperInfo() {
String mapperInfo = CURRENT_MAPPER_INFO.get();
return mapperInfo != null ? mapperInfo : "Unknown Mapper";
}
/**
* Mapper
*
* @param mapperInfo Mapper
*/
public static void setCurrentMapperInfo(String mapperInfo) {
CURRENT_MAPPER_INFO.set(mapperInfo);
}
/**
* Mapper
*/
public static void clearCurrentMapperInfo() {
CURRENT_MAPPER_INFO.remove();
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
// MappedStatement에서 Mapper 정보 추출
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
String statementId = mappedStatement.getId();
// statementId는 "namespace.methodId" 형태 (예: go.kr.project.login.mapper.LoginMapper.selectMenusByRoleIds)
String mapperInfo = statementId;
// ThreadLocal에 Mapper 정보 저장
setCurrentMapperInfo(mapperInfo);
logger.debug("MyBatis Interceptor - Mapper 정보 설정: {}", mapperInfo);
try {
// 실제 쿼리 실행
return invocation.proceed();
} catch (Exception e) {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
logger.error("MyBatis Interceptor 실행 중 오류 발생");
logger.error("Query ID: {}", ms.getId());
logger.error("Parameters: {}", parameter);
logger.error("SQL: {}", ms.getBoundSql(parameter).getSql());
logger.error("Error: {}", e.getMessage());
// 중요로직: 예외 전체 스택트레이스를 가독성 있게 출력하기 위해 logger.error("message", exception) 형태를 사용
logger.error("Error StackTrace: ", e);
throw e;
}
// 주의: ThreadLocal 정리는 DataSourceProxy의 afterQuery에서 수행
// finally 블록에서 정리하면 DataSourceProxy가 Mapper 정보를 사용하기 전에 제거될 수 있음
}
@Override
public Object plugin(Object target) {
// 모든 Executor에 대해 인터셉터 적용
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 설정 프로퍼티가 있다면 여기서 처리
// 현재는 특별한 설정이 필요없음
}
}

@ -0,0 +1,73 @@
package egovframework.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springdoc.core.GroupedOpenApi;
import org.springdoc.core.SpringDocUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Swagger UI
* OpenAPI 3.0 API .
* .
* @Controller .
*/
@Configuration
public class SwaggerConfig {
static {
// @Controller 어노테이션이 있는 클래스도 스캔하도록 설정
SpringDocUtils.getConfig().addAnnotationsToIgnore(RequestMapping.class);
}
/**
* API GroupedOpenApi .
*
* @return GroupedOpenApi
*/
@Bean
public GroupedOpenApi allApi() {
return GroupedOpenApi.builder()
.group("All")
.packagesToScan("go.kr.project")
.pathsToMatch("/**")
.build();
}
/**
* API GroupedOpenApi .
*
* @return GroupedOpenApi
*/
@Bean
public GroupedOpenApi loginApi() {
return GroupedOpenApi.builder()
.group("Login")
.packagesToScan("go.kr.project.login")
.pathsToMatch("/login/**")
.build();
}
/**
* OpenAPI .
* API (, , ) .
*
* @return OpenAPI
*/
@Bean
public OpenAPI openAPI() {
Info info = new Info()
.title("XIT Framework API")
.description("XIT Framework API 문서 - 세션 기반 인증 사용")
.version("v1.0.0")
.license(new License().name("Apache 2.0").url("http://springdoc.org"));
return new OpenAPI()
.components(new Components())
.info(info);
}
}

@ -0,0 +1,47 @@
package egovframework.configProperties;
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,31 @@
package egovframework.configProperties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* packageName : egovframework.config
* fileName : InterceptorProperties
* author :
* date : 25. 5. 19.
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 25. 5. 19.
*/
@Setter
@Getter
@Configuration
@ConfigurationProperties(prefix = "interceptor")
public class InterceptorProperties {
private List<String> interceptorExclude;
private List<String> refererExclude;
}

@ -0,0 +1,35 @@
package egovframework.configProperties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* packageName : egovframework.config
* fileName : LoginProperties
* author :
* date : 25. 5. 19.
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 25. 5. 19.
* 25. 5. 22.
*/
@Setter
@Getter
@Configuration
@ConfigurationProperties(prefix = "login")
public class LoginProperties {
private String url; // 로그인 페이지 URL
private Lock lock; // 로그인 잠금 설정
private boolean allowMultipleLogin = true; // 동시 접속 가능 여부 (기본값: true)
@Setter
@Getter
public static class Lock {
private int count; // 비밀번호 잠김 최종 카운트
}
}

@ -0,0 +1,113 @@
package egovframework.constant;
/**
* packageName : go.kr.project.common.constant
* fileName : BatchConstants
* author :
* date : 2025-06-10
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-06-10
*/
public class BatchConstants {
/**
* (TB_BATCH_JOB_INFO.STATUS)
*/
public static final String JOB_INFO_STATUS_ACTIVE = "ACTIVE";
public static final String JOB_INFO_STATUS_PAUSED = "PAUSED";
public static final String JOB_INFO_STATUS_DELETED = "DELETED";
/**
* (TB_BATCH_JOB_EXECUTION.STATUS)
* - ,,,
*/
public static final String JOB_EXECUTION_STATUS_STARTED = "STARTED";
public static final String JOB_EXECUTION_STATUS_COMPLETED = "COMPLETED";
public static final String JOB_EXECUTION_STATUS_FAILED = "FAILED";
public static final String JOB_EXECUTION_STATUS_VETOED = "VETOED";
/**
* (TB_BATCH_JOB_EXECUTION.EXIT_CODE)
*/
public static final String JOB_EXECUTION_EXIT_COMPLETED = "COMPLETED";
public static final String JOB_EXECUTION_EXIT_FAILED = "FAILED";
public static final String JOB_EXECUTION_EXIT_UNKNOWN = "UNKNOWN";
public static final String JOB_EXECUTION_EXIT_PARTIALLY_COMPLETED = "PARTIALLY_COMPLETED";
/**
*
*/
public static final String LOG_LEVEL_INFO = "INFO";
public static final String LOG_LEVEL_WARN = "WARN";
public static final String LOG_LEVEL_ERROR = "ERROR";
/**
*
*/
public static final String DATE_FORMAT_DEFAULT = "yyyy-MM-dd HH:mm:ss";
/**
* ()
*/
public static final long TIME_ITEM_PROCESS_DELAY = 100; // 0.1초
public static final long TIME_JOB_DELAY_TEST = 3000; // 3초 (테스트용)
public static final long TIME_JOB_DELAY_1_MIN = 60000; // 1분
public static final long TIME_JOB_DELAY_3_MIN = 180000; // 3분
public static final long TIME_JOB_DELAY_5_MIN = 300000; // 5분
/**
* JobDataMap
*/
public static final String JOB_DATA_EXECUTION_ID = "executionId";
/**
*
*/
public static final String JOB_NAME_SAMPLE = "sampleJob";
public static final String JOB_GROUP_SAMPLE = "sampleGroup";
public static final String JOB_TRIGGER_SUFFIX = "Trigger";
/**
* Cron
*/
public static final String CRON_EVERY_MINUTE = "0 * * * * ?";
public static final String CRON_EVERY_ONE_SECOND = "1 0 * * * ?";
/**
*
*/
public static final String JOB_DESC_SAMPLE = "샘플 배치 작업 (매 분마다 실행)";
/**
* 릿
*/
// 기본 배치 작업 로그 메시지
public static final String LOG_MSG_BATCH_JOB_START = "===== 배치 작업 시작: %s.%s - %s =====";
public static final String LOG_MSG_BATCH_JOB_COMPLETE = "===== 배치 작업 완료: %s.%s - %s =====";
public static final String LOG_MSG_BATCH_JOB_PARTIAL_COMPLETE = "===== 배치 작업 부분 완료: %s.%s - 총 %d개 파일 중 처리: %d개, 성공: %d개, 실패: %d개 (%s) =====";
public static final String LOG_MSG_BATCH_JOB_FAILED = "===== 배치 작업 실패: %s.%s - 총 %d개 파일 중 처리: %d개, 성공: %d개, 실패: %d개 (%s) =====";
public static final String LOG_MSG_BATCH_JOB_ERROR = "===== 배치 작업 실행 중 오류 발생: %s.%s - %s =====";
/**
*
*/
public static final String LOG_MSG_BATCH_INIT_SERVER_BOOT_DETECTED = "서버 부팅 완료 감지 - 배치 작업 초기화를 5초 후에 시작합니다.";
public static final String LOG_MSG_BATCH_INIT_START = "배치 작업 초기화 시작";
public static final String LOG_MSG_BATCH_INIT_COMPLETE = "배치 작업 초기화 완료";
public static final String LOG_MSG_BATCH_INIT_INTERRUPT_ERROR = "배치 작업 초기화 대기 중 인터럽트 발생: {}";
public static final String LOG_MSG_BATCH_INIT_ERROR = "배치 작업 초기화 중 오류 발생: %s";
public static final String LOG_MSG_BATCH_INIT_NO_JOBS = "등록된 배치 작업이 없습니다.";
public static final String LOG_MSG_BATCH_INIT_JOBS_COUNT = "총 {}개의 배치 작업 정보를 조회했습니다.";
public static final String LOG_MSG_BATCH_INIT_SKIP_DELETED = "삭제된 작업은 스케줄러에 등록하지 않습니다: {}.{}";
public static final String LOG_MSG_BATCH_INIT_SCHEDULE_SUCCESS = "배치 작업을 스케줄러에 등록했습니다: {}.{}";
public static final String LOG_MSG_BATCH_INIT_SCHEDULE_FAILED = "배치 작업 스케줄링에 실패했습니다: {}.{}";
public static final String LOG_MSG_BATCH_INIT_CLASS_NOT_FOUND = "배치 작업 클래스를 찾을 수 없습니다: {}";
public static final String LOG_MSG_BATCH_INIT_SCHEDULE_ERROR = "배치 작업 스케줄링 중 오류 발생: {}.{} - {}";
public static final String LOG_MSG_BATCH_INIT_DB_ERROR = "배치 작업 초기화 중 오류 발생: {}";
public static final String LOG_MSG_BATCH_INIT_REGISTER_COMPLETE = "배치 작업 정보 등록 완료: %s";
public static final String LOG_MSG_BATCH_INIT_REGISTER_ERROR = "샘플 배치 작업 등록 중 오류 발생: %s";
}

@ -0,0 +1,59 @@
package egovframework.constant;
/**
* packageName : egovframework.constant
* fileName : CrdnPrcsSttsConstants
* author :
* date : 2025-08-26
* description :
* : ,
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-08-26
*/
public class CrdnPrcsSttsConstants {
/**
* - 10:
*
*/
public static final String CRDN_PRCS_STTS_CD_10_CRDN = "10";
/**
* - 20:
*
*/
public static final String CRDN_PRCS_STTS_CD_20_DSPS_BFHD = "20";
/**
* - 30:
*
*/
public static final String CRDN_PRCS_STTS_CD_30_CRC_CMD = "30";
/**
* - 40:
*
*/
public static final String CRDN_PRCS_STTS_CD_40_CRC_URG = "40";
/**
* - 50:
*
*/
public static final String CRDN_PRCS_STTS_CD_50_LEVY_PRVNTC = "50";
/**
* - 60:
*
*/
public static final String CRDN_PRCS_STTS_CD_60_LEVY = "60";
/**
* - 70:
*
*/
public static final String CRDN_PRCS_STTS_CD_70_PAY_URG = "70";
}

@ -0,0 +1,65 @@
package egovframework.constant;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* packageName : go.kr.project.common.constant
* fileName : FileContentTypeConstants
* author :
* date : 2025-05-17
* description : MIME
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-05-17
*/
public class FileContentTypeConstants {
/**
* MIME ( )
*/
public static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
/**
* MIME
*/
private static final Map<String, String> CONTENT_TYPE_MAP;
static {
Map<String, String> map = new HashMap<>();
// 이미지 파일
map.put("jpg", "image/jpeg");
map.put("jpeg", "image/jpeg");
map.put("png", "image/png");
map.put("gif", "image/gif");
// 문서 파일
map.put("pdf", "application/pdf");
map.put("doc", "application/msword");
map.put("docx", "application/msword");
map.put("xls", "application/vnd.ms-excel");
map.put("xlsx", "application/vnd.ms-excel");
map.put("ppt", "application/vnd.ms-powerpoint");
map.put("pptx", "application/vnd.ms-powerpoint");
map.put("txt", "text/plain");
CONTENT_TYPE_MAP = Collections.unmodifiableMap(map);
}
/**
* MIME .
*
* @param fileExt ()
* @return MIME
*/
public static String getContentType(String fileExt) {
if (fileExt == null || fileExt.isEmpty()) {
return DEFAULT_CONTENT_TYPE;
}
return CONTENT_TYPE_MAP.getOrDefault(fileExt.toLowerCase(), DEFAULT_CONTENT_TYPE);
}
}

@ -0,0 +1,106 @@
package egovframework.constant;
/**
* packageName : egovframework.constant
* fileName : ImpltTaskSeConstants
* author :
* date : 2025-09-08
* description :
* : ,
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-09-08
*/
public class ImpltTaskSeConstants {
/**
* - 1:
*
*/
public static final String IMPLT_TASK_SE_CD_1_DSPS_BFHD = "1";
/**
* - 2:
*
*/
public static final String IMPLT_TASK_SE_CD_2_CRC_CMD = "2";
/**
* - 3:
*
*/
public static final String IMPLT_TASK_SE_CD_3_CRC_URG = "3";
/**
* - 4:
*
*/
public static final String IMPLT_TASK_SE_CD_4_LEVY_PRVNTC = "4";
/**
* - 5:
*
*/
public static final String IMPLT_TASK_SE_CD_5_LEVY = "5";
/**
* - 6:
*
*/
public static final String IMPLT_TASK_SE_CD_6_PAY_URG = "6";
/**
* -
*/
public static final String IMPLT_TASK_SE_CD_NM_DSPS_BFHD = "처분사전";
/**
* -
*/
public static final String IMPLT_TASK_SE_CD_NM_CRC_CMD = "시정명령";
/**
* -
*/
public static final String IMPLT_TASK_SE_CD_NM_CRC_URG = "시정촉구";
/**
* -
*/
public static final String IMPLT_TASK_SE_CD_NM_LEVY_PRVNTC = "부과예고";
/**
* -
*/
public static final String IMPLT_TASK_SE_CD_NM_LEVY = "부과";
/**
* -
*/
public static final String IMPLT_TASK_SE_CD_NM_PAY_URG = "납부촉구";
/**
*
*/
public static final String IMPLT_TASK_SE_CD_NM_UNKNOWN = "알수없음";
/**
* .
*
* @param impltTaskSeCd
* @return
*/
public static String getImpltTaskSeCdNm(String impltTaskSeCd) {
switch (impltTaskSeCd) {
case IMPLT_TASK_SE_CD_1_DSPS_BFHD: return IMPLT_TASK_SE_CD_NM_DSPS_BFHD;
case IMPLT_TASK_SE_CD_2_CRC_CMD: return IMPLT_TASK_SE_CD_NM_CRC_CMD;
case IMPLT_TASK_SE_CD_3_CRC_URG: return IMPLT_TASK_SE_CD_NM_CRC_URG;
case IMPLT_TASK_SE_CD_4_LEVY_PRVNTC: return IMPLT_TASK_SE_CD_NM_LEVY_PRVNTC;
case IMPLT_TASK_SE_CD_5_LEVY: return IMPLT_TASK_SE_CD_NM_LEVY;
case IMPLT_TASK_SE_CD_6_PAY_URG: return IMPLT_TASK_SE_CD_NM_PAY_URG;
default: return IMPLT_TASK_SE_CD_NM_UNKNOWN;
}
}
}

@ -0,0 +1,205 @@
package egovframework.constant;
/**
* packageName : go.kr.project.common.constant
* fileName : MessageConstants
* author :
* date : 2025-05-22
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-05-22
*/
public class MessageConstants {
/**
*
*/
public static class Login {
// 성공 메시지
public static final String SUCCESS = "로그인에 성공하였습니다.";
// 오류 메시지
public static final String ERROR_INVALID_CREDENTIALS = "아이디 또는 비밀번호가 일치하지 않습니다.";
public static final String ERROR_INACTIVE_ACCOUNT = "비활성화 계정입니다. 관리자에 문의하세요.";
public static final String ERROR_PROCESS = "로그인 처리 중 오류가 발생했습니다.";
}
/**
*
*/
public static class LoginFailure {
public static final String ABNORMAL_PATTERN_PREFIX = "비정상적인 로그인 패턴: ";
public static final String USER_NOT_FOUND = "사용자 계정 없음";
public static final String ACCOUNT_LOCKED = "계정 잠김";
public static final String PASSWORD_MISMATCH = "비밀번호 불일치";
public static final String ACCOUNT_INACTIVE = "계정 비활성화";
public static final String MESSAGE_ACCOUNT_LOCKED = "사용자 계정이 잠겼습니다.";
}
/**
*
*/
public static class Logout {
// 성공 메시지
public static final String SUCCESS = "로그아웃 되었습니다.";
// 오류 메시지
public static final String ERROR_PROCESS = "로그아웃 처리 중 오류가 발생했습니다.";
}
/**
*
*/
public static class Account {
// 계정 잠금 관련 메시지
public static final String UNLOCK_SUCCESS = "계정 잠금이 해제되었습니다.";
public static final String UNLOCK_FAIL = "계정 잠금 해제에 실패했습니다.";
public static final String UNLOCK_ERROR = "계정 잠금 해제 중 오류가 발생했습니다.";
}
/**
*
*/
public static class LoginLog {
// 목록 조회 관련 메시지
public static final String LIST_SUCCESS = "로그인 로그 목록을 조회하였습니다.";
public static final String LIST_ERROR = "로그인 로그 목록 조회 중 오류가 발생했습니다.";
// 상세 조회 관련 메시지
public static final String DETAIL_SUCCESS = "로그인 로그 상세 정보를 조회하였습니다.";
public static final String DETAIL_ERROR = "로그인 로그 상세 조회 중 오류가 발생했습니다.";
public static final String NOT_EXIST = "존재하지 않는 로그입니다.";
}
/**
*
*/
public static class Stats {
// 디바이스 유형별 통계
public static final String DEVICE_SUCCESS = "디바이스 유형별 로그인 통계를 조회하였습니다.";
public static final String DEVICE_ERROR = "디바이스 유형별 로그인 통계 조회 중 오류가 발생했습니다.";
// 성공/실패 비율 통계
public static final String SUCCESS_FAIL_SUCCESS = "로그인 성공/실패 비율 통계를 조회하였습니다.";
public static final String SUCCESS_FAIL_ERROR = "로그인 성공/실패 비율 통계 조회 중 오류가 발생했습니다.";
// 시간대별 통계
public static final String HOURLY_SUCCESS = "시간대별 로그인 시도 통계를 조회하였습니다.";
public static final String HOURLY_ERROR = "시간대별 로그인 시도 통계 조회 중 오류가 발생했습니다.";
}
/**
*
*/
public static class Auth {
// 메뉴-역할 관련 메시지
public static final String MENU_ROLE_ASSIGN_SUCCESS = "메뉴가 역할에 성공적으로 할당되었습니다.";
public static final String MENU_ROLE_REMOVE_SUCCESS = "메뉴가 역할에서 성공적으로 제거되었습니다.";
// 역할-그룹 관련 메시지
public static final String ROLE_GROUP_ASSIGN_SUCCESS = "역할이 그룹에 성공적으로 할당되었습니다.";
public static final String ROLE_GROUP_REMOVE_SUCCESS = "역할이 그룹에서 성공적으로 제거되었습니다.";
}
/**
*
*/
public static class Code {
// 코드 상세 관련 메시지
public static final String DETAIL_SAVE_SUCCESS = "코드 상세 정보가 성공적으로 저장되었습니다.";
// 그룹코드 관련 메시지
public static final String GROUP_SAVE_SUCCESS = "그룹코드 정보가 성공적으로 저장되었습니다.";
}
/**
*
*/
public static class Group {
// 그룹 저장 관련 메시지
public static final String SAVE_SUCCESS = "그룹이 성공적으로 저장되었습니다.";
}
/**
*
*/
public static class Menu {
// 성공 메시지
public static final String REGISTER_SUCCESS = "메뉴가 성공적으로 등록되었습니다.";
public static final String EDIT_SUCCESS = "메뉴 정보가 성공적으로 수정되었습니다.";
public static final String DELETE_SUCCESS = "메뉴가 성공적으로 삭제되었습니다.";
// 오류 메시지
public static final String ROOT_REGISTER_ERROR = "ROOT 메뉴 ID로 새 메뉴를 등록할 수 없습니다.";
public static final String UPPER_MENU_REQUIRED = "상위 메뉴를 선택해주세요.";
public static final String REGISTER_ERROR = "메뉴 등록에 실패했습니다.";
public static final String ROOT_EDIT_ERROR = "ROOT 메뉴는 수정할 수 없습니다.";
public static final String EDIT_ERROR = "메뉴 정보 수정에 실패했습니다.";
public static final String ROOT_DELETE_ERROR = "ROOT 메뉴는 삭제할 수 없습니다.";
public static final String CHILD_EXISTS_ERROR = "하위 메뉴가 존재하여 삭제할 수 없습니다.";
public static final String DELETE_ERROR = "메뉴 삭제에 실패했습니다.";
}
/**
*
*/
public static class Role {
// 성공 메시지
public static final String SAVE_SUCCESS = "역할이 성공적으로 저장되었습니다.";
// 오류 메시지
public static final String SAVE_ERROR = "역할 저장에 실패했습니다.";
}
/**
*
*/
public static class User {
// 성공 메시지
public static final String REGISTER_SUCCESS = "사용자가 성공적으로 등록되었습니다.";
public static final String EDIT_SUCCESS = "사용자 정보가 성공적으로 수정되었습니다.";
public static final String PASSWORD_RESET_SUCCESS = "비밀번호가 성공적으로 초기화되었습니다.";
public static final String UNLOCK_SUCCESS = "사용자 잠금이 성공적으로 해제되었습니다.";
// 오류 메시지
public static final String REGISTER_ERROR = "사용자 등록에 실패했습니다.";
public static final String EDIT_ERROR = "사용자 정보 수정에 실패했습니다.";
public static final String NOT_FOUND = "사용자 정보를 찾을 수 없습니다.";
public static final String PASSWORD_RESET_ERROR = "비밀번호 초기화에 실패했습니다.";
public static final String UNLOCK_ERROR = "사용자 잠금 해제에 실패했습니다.";
}
/**
*
*/
public static class UserGroup {
// 성공 메시지
public static final String GROUP_LIST_SUCCESS = "그룹 목록을 성공적으로 조회했습니다.";
public static final String ROLE_LIST_SUCCESS = "역할 목록을 성공적으로 조회했습니다.";
public static final String MENU_LIST_SUCCESS = "메뉴 목록을 성공적으로 조회했습니다.";
// 오류 메시지
public static final String GROUP_LIST_ERROR = "그룹 목록 조회 중 오류가 발생했습니다: ";
public static final String ROLE_LIST_ERROR = "역할 목록 조회 중 오류가 발생했습니다: ";
public static final String MENU_LIST_ERROR = "메뉴 목록 조회 중 오류가 발생했습니다: ";
}
/**
*
*/
public static class Common {
// 성공 메시지
public static final String SAVE_SUCCESS = "저장되었습니다.";
public static final String UPDATE_SUCCESS = "수정되었습니다.";
public static final String DELETE_SUCCESS = "삭제되었습니다.";
// 오류 메시지
public static final String SAVE_ERROR = "저장 중 오류가 발생했습니다.";
public static final String UPDATE_ERROR = "수정 중 오류가 발생했습니다.";
public static final String DELETE_ERROR = "삭제 중 오류가 발생했습니다.";
public static final String SYSTEM_ERROR = "시스템 오류가 발생했습니다.";
}
}

@ -0,0 +1,18 @@
package egovframework.constant;
/**
* packageName : egovframework.constant
* fileName : SEQConstants
* author :
* date : 25. 8. 26.
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 25. 8. 26.
*/
public class SEQConstants {
public static final String SEQ_CRDN = "seq_crdn_no_";
}

@ -0,0 +1,39 @@
package egovframework.constant;
/**
* packageName : go.kr.project.common.constant
* fileName : SessionConstants
* author :
* date : 2025-05-22
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-05-22
*/
public class SessionConstants {
/**
*
* SessionVO
*/
public static final String SESSION_KEY = "sessionVO";
/**
* ID
* ID
*/
public static final String VISITOR_ROLE_ID = "ROLE_VISITOR";
/**
*
*
*/
public static final String SESSION_EXPIRED = "SESSION_EXPIRED";
/**
*
* Referer
*/
public static final String INVALID_ACCESS = "INVALID_ACCESS";
}

@ -0,0 +1,23 @@
package egovframework.constant;
/**
* packageName : go.kr.project.common.constant
* fileName : TilesConstants
* author :
* date : 2025-05-22
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-05-22
*/
public class TilesConstants {
public static final String LOGIN = ".login";
public static final String BASE = ".base";
public static final String INNER = ".inner";
public static final String POPUP = ".popup";
}

@ -0,0 +1,27 @@
package egovframework.constant;
/**
* packageName : egovframework.constant
* fileName : TuiGridColorConstants
* author :
* date : 2025-10-21
* description : TUI Grid / CSS
* : TUI Grid _attributes.className.row CSS
* .
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-10-21 (tui-grid-custom-color* )
*/
public class TuiGridColorConstants {
// 공통: 그리드 행 배경색 지정용 CSS 클래스명 (xit-tui-grid.css 참고)
public static final String ROW_COLOR_RED = "tui-grid-custom-color-red";
public static final String ROW_COLOR_BLUE = "tui-grid-custom-color-blue";
public static final String ROW_COLOR_GRAY = "tui-grid-custom-color-gray";
public static final String ROW_COLOR_WHITE = "tui-grid-custom-color-white";
private TuiGridColorConstants() {
// 유틸 상수 클래스 - 인스턴스화 방지
}
}

@ -0,0 +1,23 @@
package egovframework.exception;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
*
*/
@Getter
@ResponseStatus(HttpStatus.FORBIDDEN)
public class AccessDeniedException extends RuntimeException {
private final String userAcnt;
private final String requestURI;
public AccessDeniedException(String userAcnt, String requestURI) {
super("접근 권한이 없습니다.");
this.userAcnt = userAcnt;
this.requestURI = requestURI;
}
}

@ -0,0 +1,21 @@
package egovframework.exception;
import lombok.Setter;
import org.aspectj.lang.JoinPoint;
import org.egovframe.rte.fdl.cmmn.aspect.ExceptionTransfer;
@Setter
//@Aspect
public class EgovAopExceptionTransfer {
private ExceptionTransfer exceptionTransfer;
//@Pointcut("execution(* go.kr.project..impl.*Impl.*(..)) or execution(* egovframework.com..*Impl.*(..))")
private void exceptionTransferService() {}
//@AfterThrowing(pointcut="exceptionTransferService()", throwing="ex")
public void doAfterThrowingExceptionTransferService(JoinPoint thisJoinPoint, Exception ex) throws Exception {
exceptionTransfer.transfer(thisJoinPoint, ex);
}
}

@ -0,0 +1,14 @@
package egovframework.exception;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.exception.handler.ExceptionHandler;
@Slf4j
public class EgovDefaultExcepHndlr implements ExceptionHandler {
@Override
public void occur(Exception ex, String packageName) {
log.debug("##### EgovServiceExceptionHandler Run...");
}
}

@ -0,0 +1,14 @@
package egovframework.exception;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.exception.handler.ExceptionHandler;
@Slf4j
public class EgovDefaultOthersExcepHndlr implements ExceptionHandler {
@Override
public void occur(Exception exception, String packageName) {
log.debug("##### EgovSampleOthersExcepHndlr Run...");
}
}

@ -0,0 +1,147 @@
package egovframework.exception;
import egovframework.configProperties.LoginProperties;
import egovframework.util.ApiResponseEntity;
import egovframework.util.HttpServletUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.TransactionException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* @ControllerAdvice
* SimpleMappingExceptionResolver
*
* AJAX (*.ajax) JSON ,
* egovError.jsp .
*/
@Slf4j
@ControllerAdvice
@RequiredArgsConstructor
public class EgovExceptionAdvice {
private final LoginProperties loginProperties;
/**
* DataAccessException
* .
*/
@ExceptionHandler(DataAccessException.class)
public Object handleDataAccessException(DataAccessException e, HttpServletRequest request) {
log.error("DataAccessException 발생: ", e);
return getModelAndView(e, request, HttpStatus.BAD_REQUEST);
}
/**
* TransactionException
* .
*/
@ExceptionHandler(TransactionException.class)
public Object handleTransactionException(TransactionException e, HttpServletRequest request) {
log.error("TransactionException 발생: ", e);
return getModelAndView(e, request, HttpStatus.BAD_REQUEST);
}
/**
* AccessDeniedException
* .
*/
@ExceptionHandler(AccessDeniedException.class)
public Object handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
log.warn("접근 권한 예외 발생 - 사용자: {}, URI: {}", e.getUserAcnt(), e.getRequestURI());
return getModelAndView(e, request, HttpStatus.FORBIDDEN);
}
/**
*
* HttpSessionRequiredException .
* AuthInterceptor AJAX .
*/
/**
* RuntimeException
* .
*/
@ExceptionHandler(RuntimeException.class)
public Object handleRuntimeException(RuntimeException e, HttpServletRequest request) {
log.error("RuntimeException 발생: ", e);
return getModelAndView(e, request, HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* Exception
* .
*/
@ExceptionHandler(Exception.class)
public Object handleException(Exception e, HttpServletRequest request) {
log.error("Exception 발생: ", e);
return getModelAndView(e, request, HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* IOException
* IO .
*/
@ExceptionHandler(IOException.class)
public Object handleIOException(Exception e, HttpServletRequest request) {
log.error("IOException 발생: ", e);
return getModelAndView(e, request, HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* Throwable
* Throwable .
*/
@ExceptionHandler(Throwable.class)
public Object handleThrowable(Throwable e, HttpServletRequest request) {
log.error("Throwable 발생: ", e);
return getModelAndView(e, request, HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* MessageException
* .
*/
@ExceptionHandler(MessageException.class)
public Object handleMessageException(MessageException e, HttpServletRequest request) {
log.warn("MessageException 발생: ", e);
return getModelAndView(e, request, HttpStatus.BAD_REQUEST);
}
/**
*
* AJAX ResponseEntity<ApiResponse<Void>> ,
* egovError ModelAndView .
* AJAX URL (*.ajax) HTTP .
*
* @param e
* @param request HTTP
* @param status HTTP
* @return AJAX ResponseEntity, ModelAndView
*/
private Object getModelAndView(Throwable e, HttpServletRequest request, HttpStatus status) {
// AJAX 요청인 경우 ApiResponse를 사용한 JSON 응답 반환
if (HttpServletUtil.isAjaxRequest(request) || HttpServletUtil.isRealAjaxRequest(request)) {
ApiResponseEntity<Void> responseBody = ApiResponseEntity.<Void>builder()
.result(false)
.message(e.getMessage())
.errorCode(e.getClass().getSimpleName())
.build();
return new ResponseEntity<>(responseBody, status);
}
// 일반 요청인 경우 egovError 페이지로 리다이렉트
ModelAndView mav = new ModelAndView("error/egovError");
mav.addObject("exceptionName", e.getClass().getSimpleName());
mav.addObject("exception", e);
return mav;
}
}

@ -0,0 +1,65 @@
package egovframework.exception;
import lombok.Getter;
/**
*
*
* .
* BasicException "MESSAGE" ,
* .
*/
@Getter
public class MessageException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
*
*/
private final String errorCode;
/**
*
*
* @param message
*/
public MessageException(String message) {
super(message);
this.errorCode = "MESSAGE_EXCEPTION";
}
/**
*
*
* @param message
* @param errorCode
*/
public MessageException(String message, String errorCode) {
super(message);
this.errorCode = errorCode;
}
/**
*
*
* @param message
* @param cause
*/
public MessageException(String message, Throwable cause) {
super(message, cause);
this.errorCode = "MESSAGE_EXCEPTION";
}
/**
*
*
* @param message
* @param errorCode
* @param cause
*/
public MessageException(String message, String errorCode, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
}

@ -0,0 +1,54 @@
package egovframework.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* packageName : egovframework.filter
* fileName : SessionRefreshFilter
* author :
* date : 25. 5. 21.
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 25. 5. 21.
*/
@Slf4j
@Component
public class SessionRefreshFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 배치 작업 경로는 세션 리프레시에서 제외
String requestURI = httpRequest.getRequestURI();
if (requestURI != null && requestURI.startsWith("/batch/")) {
chain.doFilter(request, response);
return;
}
try {
// 세션이 있으면 접근하여 갱신 (세션 타임아웃 연장)
httpRequest.getSession(false);
// 디버깅 용도 (필요시 주석 해제)
// if (httpRequest.getSession().getAttribute("sessionVO") != null) {
// log.debug("세션 리프레시 - 세션 정보 존재");
// } else {
// log.debug("세션 리프레시 - 세션 정보 없음");
// }
} catch (Exception e) {
// 세션 관련 오류 발생 시 로그만 남기고 계속 진행
log.debug("세션 리프레시 중 오류 발생: {}", e.getMessage());
}
chain.doFilter(request, response);
}
}

@ -0,0 +1,38 @@
package egovframework.filter;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* XSS
* XSS
*/
public class XssFilter extends OncePerRequestFilter {
private final XssUtil xssUtil;
public XssFilter(XssUtil xssUtil) {
this.xssUtil = xssUtil;
}
@Override
protected void doFilterInternal(@Nullable HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// XSS 방지를 위한 헤더 설정
response.setHeader("X-XSS-Protection", "1; mode=block");
response.setHeader("X-Content-Type-Options", "nosniff");
// 요청을 래핑하여 XSS 필터링 적용
XssRequestWrapper xssRequestWrapper = new XssRequestWrapper(request, xssUtil);
// 필터 체인 실행
filterChain.doFilter(xssRequestWrapper, response);
}
}

@ -0,0 +1,32 @@
package egovframework.filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* XSS
*/
@Configuration
public class XssFilterConfig {
private final XssUtil xssUtil;
public XssFilterConfig(XssUtil xssUtil) {
this.xssUtil = xssUtil;
}
/**
* XSS
* @return FilterRegistrationBean
*/
@Bean
public FilterRegistrationBean<XssFilter> xssFilterRegistration() {
FilterRegistrationBean<XssFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new XssFilter(xssUtil));
registrationBean.addUrlPatterns("/*"); // 모든 URL에 적용
registrationBean.setName("xssFilter");
registrationBean.setOrder(1); // 필터 순서 (낮은 숫자가 먼저 실행)
return registrationBean;
}
}

@ -0,0 +1,83 @@
package egovframework.filter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.HashMap;
import java.util.Map;
/**
* XSS HttpServletRequest
*/
public class XssRequestWrapper extends HttpServletRequestWrapper {
private final XssUtil xssUtil;
public XssRequestWrapper(HttpServletRequest request, XssUtil xssUtil) {
super(request);
this.xssUtil = xssUtil;
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return xssFilter(name, value);
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) {
return null;
}
String[] filteredValues = new String[values.length];
for (int i = 0; i < values.length; i++) {
filteredValues[i] = xssFilter(name, values[i]);
}
return filteredValues;
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> paramMap = super.getParameterMap();
Map<String, String[]> filteredParamMap = new HashMap<>();
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
String[] values = entry.getValue();
String[] filteredValues = new String[values.length];
for (int i = 0; i < values.length; i++) {
filteredValues[i] = xssFilter(entry.getKey(), values[i]);
}
filteredParamMap.put(entry.getKey(), filteredValues);
}
return filteredParamMap;
}
/**
* XSS
* @param name
* @param value
* @return
*/
private String xssFilter(String name, String value) {
if (value == null) {
return null;
}
// HTML 에디터 내용인 경우 cleanHtml 메서드 사용
//if (name != null && (name.contains("content") || name.contains("html") || name.contains("editor"))) {
if (name != null && (name.contains("html") || name.contains("editor"))) {
return xssUtil.cleanHtml(value);
}
// 파일명인 경우 sanitizeFilename 메서드 사용
if (name != null && (name.contains("filename") || name.contains("file"))) {
return xssUtil.sanitizeFilename(value);
}
// 일반 텍스트인 경우 escape 메서드 사용
//return xssUtil.escape(value);
// TODO : 임시 해제!! 추후 보안을 위해 다시 적용해야함!!
return value;
}
}

@ -0,0 +1,105 @@
package egovframework.filter;
import org.springframework.stereotype.Component;
import java.util.regex.Pattern;
/**
* XSS(Cross-Site Scripting)
*/
@Component
public class XssUtil {
/**
* HTML XSS
* @param value
* @return
*/
public String escape(String value) {
if (value == null) {
return null;
}
return value.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;")
.replaceAll("'", "&#x27;")
.replaceAll("/", "&#x2F;")
.replaceAll("\\(", "&#40;")
.replaceAll("\\)", "&#41;");
}
/**
*
* @param value
* @return
*/
public String unescape(String value) {
if (value == null) {
return null;
}
return value.replaceAll("&amp;", "&")
.replaceAll("&lt;", "<")
.replaceAll("&gt;", ">")
.replaceAll("&quot;", "\"")
.replaceAll("&#x27;", "'")
.replaceAll("&#x2F;", "/")
.replaceAll("&#40;", "(")
.replaceAll("&#41;", ")");
}
/**
* HTML
* @param value HTML
* @return HTML
*/
public String cleanHtml(String value) {
if (value == null) {
return null;
}
// 허용할 HTML 태그 패턴
String allowedTags = "<p>|<br>|<b>|<i>|<strong>|<em>|<u>|<ul>|<ol>|<li>|<h1>|<h2>|<h3>|<h4>|<h5>|<h6>|<table>|<tr>|<td>|<th>|<tbody>|<thead>|<img>|<a>|<div>|<span>";
// 스크립트 태그 및 이벤트 핸들러 패턴
String scriptPattern = "<script[^>]*>.*?</script>";
String eventPattern = " on\\w+=\".*?\"";
String stylePattern = " style=\".*?\"";
String inlineFramePattern = "<iframe[^>]*>.*?</iframe>";
String embedPattern = "<embed[^>]*>.*?</embed>";
String objectPattern = "<object[^>]*>.*?</object>";
// 스크립트 및 이벤트 핸들러 제거
value = Pattern.compile(scriptPattern, Pattern.DOTALL | Pattern.CASE_INSENSITIVE).matcher(value).replaceAll("");
value = Pattern.compile(eventPattern, Pattern.CASE_INSENSITIVE).matcher(value).replaceAll("");
value = Pattern.compile(inlineFramePattern, Pattern.DOTALL | Pattern.CASE_INSENSITIVE).matcher(value).replaceAll("");
value = Pattern.compile(embedPattern, Pattern.DOTALL | Pattern.CASE_INSENSITIVE).matcher(value).replaceAll("");
value = Pattern.compile(objectPattern, Pattern.DOTALL | Pattern.CASE_INSENSITIVE).matcher(value).replaceAll("");
// style 속성은 제한적으로 허용 (위험한 표현식 제거)
value = Pattern.compile("expression\\s*\\(.*?\\)", Pattern.DOTALL | Pattern.CASE_INSENSITIVE).matcher(value).replaceAll("");
value = Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE).matcher(value).replaceAll("");
value = Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE).matcher(value).replaceAll("");
// href 속성에서 javascript: 제거
value = Pattern.compile("href\\s*=\\s*['\"]\\s*javascript:.*?['\"]", Pattern.CASE_INSENSITIVE).matcher(value).replaceAll("href=\"#\"");
return value;
}
/**
* XSS
* @param filename
* @return
*/
public String sanitizeFilename(String filename) {
if (filename == null) {
return null;
}
// 파일명에서 위험한 문자 제거
return filename.replaceAll("[\\\\/:*?\"<>|]", "_");
}
}

@ -0,0 +1,413 @@
package egovframework.interceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
import egovframework.configProperties.InterceptorProperties;
import egovframework.configProperties.LoginProperties;
import egovframework.constant.SessionConstants;
import egovframework.exception.AccessDeniedException;
import egovframework.filter.XssUtil;
import egovframework.util.ApiResponseEntity;
import egovframework.util.HttpServletUtil;
import go.kr.project.login.mapper.LoginMapper;
import go.kr.project.login.model.SessionVO;
import go.kr.project.login.model.UserSessionVO;
import go.kr.project.login.service.LoginService;
import go.kr.project.system.menu.model.MenuVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* packageName : egovframework.interceptor
* fileName : AuthInterceptor
* author :
* date : 2025-05-13
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-05-13
*/
@Slf4j
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {
private final LoginService loginService;
// URL 패턴 매칭을 위한 AntPathMatcher
private final AntPathMatcher pathMatcher = new AntPathMatcher();
// XSS 방지 유틸리티
private final XssUtil xssUtil = new XssUtil();
// JSON 변환을 위한 ObjectMapper
private final ObjectMapper objectMapper = new ObjectMapper();
// Referer 검사에서 제외할 URL 패턴 목록 설정
@Autowired
private InterceptorProperties interceptorProperties;
// 로그인 관련 설정
@Autowired
private LoginProperties loginProperties;
// 로그인 매퍼
@Autowired
private LoginMapper loginMapper;
/**
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Object handler) throws Exception {
String requestURI = request.getRequestURI();
// 접근 제어 예외 URL 패턴은 WebMvcConfigurer에서 처리됨
// 이 메서드가 호출된다는 것은 이미 예외 패턴이 아니라는 의미
// Referer 헤더 검사 - URL을 브라우저 주소창에 직접 입력해서 접근하는 것을 방지
// 첫 페이지 접근이나 로그인 페이지 등 일부 URL은 Referer 검사에서 제외
if (!isRefererCheckExcluded(requestURI)) {
String referer = request.getHeader("Referer");
if (referer == null || referer.isEmpty()) {
// Referer 헤더가 없는 경우 (직접 URL 입력 또는 북마크 등)
log.warn("Referer 헤더 없음: {}", requestURI);
// AJAX 요청인 경우 JSON 응답 반환
if (isAjaxRequest(request)) {
handleRefererMissing(response);
return false;
}
// 일반 요청인 경우 로그인 페이지로 리다이렉트
redirectToLoginPageWithPopupCheck(request, response, "잘못된 접근입니다. 로그인 페이지로 이동합니다.");
return false;
}
}
try {
// 세션 정보 조회
SessionVO sessionVO = loginService.getSessionInfo(request);
// 세션이 있고 로그인 상태인 경우, 동시 접속 허용 불가일 때만 세션 정보를 DB에서 확인
if (sessionVO != null && sessionVO.isLogin() && !loginProperties.isAllowMultipleLogin()) {
HttpSession session = request.getSession(false);
if (session != null) {
String sessionId = session.getId();
String userId = sessionVO.getUser().getUserId();
// DB에서 세션 정보 조회
UserSessionVO userSessionVO = loginMapper.selectUserSessionBySessionId(sessionId);
if (userSessionVO == null) {
// DB에 세션 정보가 없으면 현재 세션 무효화
log.warn("DB에 세션 정보가 없음. 세션 무효화 (동시 접속 제한): {}", sessionId);
session.invalidate();
// AJAX 요청인 경우 JSON 응답 반환
if (isAjaxRequest(request)) {
handleAjaxSessionExpired(response);
return false;
}
// 일반 요청인 경우 로그인 페이지로 리다이렉트
redirectToLoginPageWithPopupCheck(request, response, "세션이 종료되었습니다. 로그인 페이지로 이동합니다.");
return false;
} else {
// DB에 세션 정보가 있으면 마지막 접속 시간 업데이트
userSessionVO.setLastAccessDttm(LocalDateTime.now());
loginMapper.updateUserSession(userSessionVO);
log.debug("세션 정보 업데이트 (동시 접속 제한): {}", sessionId);
}
}
}
// 세션이 없거나 로그인 상태가 아닌 경우
if (sessionVO == null || !sessionVO.isLogin()) {
// 방문자 권한 확인
if (sessionVO != null && sessionVO.isVisitor()) {
// 방문자 권한으로 접근 가능한지 확인
if (hasAccess(sessionVO, requestURI)) {
return true;
}
}
// AJAX 요청인 경우 JSON 응답 반환
if (isAjaxRequest(request)) {
handleAjaxSessionExpired(response);
return false;
}
// 일반 요청인 경우 로그인 페이지로 리다이렉트
redirectToLoginPageWithPopupCheck(request, response, "세션이 종료되었습니다. 로그인 페이지로 이동합니다.");
return false;
}
// 비밀번호 초기화 여부 확인
if ("Y".equals(sessionVO.getUser().getPasswdInitYn())) {
// 비밀번호 변경 페이지 또는 비밀번호 변경 처리 URL인 경우에만 접근 허용
if (requestURI.equals("/mypage/password.do") ||
requestURI.equals("/mypage/password.ajax") ||
requestURI.equals("/mypage/check-password.ajax") ||
requestURI.equals("/mypage/password-reset.do")) {
return true;
} else {
// 비밀번호 변경 페이지로 리다이렉트
if (isAjaxRequest(request)) {
// AJAX 요청인 경우 JSON 응답 반환
sendJsonResponse(response, HttpStatus.UNAUTHORIZED, false, "비밀번호를 변경해야 합니다.", "PASSWORD_RESET_REQUIRED");
return false;
} else {
// 일반 요청인 경우 비밀번호 변경 페이지로 리다이렉트
redirectToPage(request, response, "비밀번호를 변경해야 합니다.", "/mypage/password-reset.do");
return false;
}
}
}
// 로그인 상태인 경우 접근 권한 확인
if (hasAccess(sessionVO, requestURI)) {
return true;
}
// 접근 권한이 없는 경우 예외 발생
log.warn("접근 권한 없음: {} - {}", sessionVO.getUser().getUserAcnt(), requestURI);
throw new AccessDeniedException(sessionVO.getUser().getUserAcnt(), requestURI);
//response.sendError(HttpServletResponse.SC_FORBIDDEN, "접근 권한이 없습니다.");
//return false;
} catch (AccessDeniedException e) {
throw e;
} catch (Exception e) {
log.error("접근 제어 처리 중 오류 발생", e);
// AJAX 요청인 경우 JSON 응답 반환
if (isAjaxRequest(request)) {
try {
handleAjaxSessionExpired(response);
} catch (IOException ex) {
log.error("AJAX 응답 처리 중 오류 발생", ex);
}
return false;
}
// 일반 요청인 경우 로그인 페이지로 리다이렉트
try {
redirectToLoginPageWithPopupCheck(request, response, "세션이 종료되었습니다. 로그인 페이지로 이동합니다.");
} catch (IOException ex) {
log.error("리다이렉트 처리 중 오류 발생", ex);
}
return false;
}
}
/**
*
* @param sessionVO
* @param requestURI URI
* @return
*/
private boolean hasAccess(SessionVO sessionVO, String requestURI) {
// 메뉴 트리를 1차원 리스트로 변환
List<MenuVO> menus = flattenMenuTree(sessionVO.getMenus());
if (menus.isEmpty()) {
return false;
}
// 메뉴의 URL 패턴과 일치하는지 확인
for (MenuVO menu : menus) {
if (menu.getUrlPattern() != null && !menu.getUrlPattern().isEmpty()) {
// XSS 방지를 위한 이스케이프 처리된 URL 패턴을 원래대로 복원하고 쉼표로 구분된 패턴을 분리
String[] patterns = xssUtil.unescape(menu.getUrlPattern()).split(",");
for (String pattern : patterns) {
// 각 패턴과 요청 URI를 비교하여 일치하면 접근 허용
if (pathMatcher.match(pattern.trim(), requestURI)) {
return true;
}
}
}
}
// 일치하는 패턴이 없으면 접근 불가
return false;
}
/**
* 1
*
*
* @param menus
* @return 1
*/
private List<MenuVO> flattenMenuTree(List<MenuVO> menus) {
List<MenuVO> flatList = new ArrayList<>();
if (menus != null) {
for (MenuVO menu : menus) {
// 현재 메뉴를 리스트에 추가
flatList.add(menu);
// 하위 메뉴가 있으면 재귀적으로 처리하여 리스트에 추가
if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
flatList.addAll(flattenMenuTree(menu.getChildren()));
}
}
}
return flatList;
}
/**
* AJAX
* JSON
*
* @param response HTTP
* @throws IOException
*/
private void handleAjaxSessionExpired(HttpServletResponse response) throws IOException {
sendJsonResponse(response, HttpStatus.UNAUTHORIZED, false,
"세션이 만료되었습니다. 로그인 페이지로 이동합니다.",
SessionConstants.SESSION_EXPIRED);
}
/**
* Referer AJAX
* URL
*
* @param response HTTP
* @throws IOException
*/
private void handleRefererMissing(HttpServletResponse response) throws IOException {
sendJsonResponse(response, HttpStatus.FORBIDDEN, false,
"잘못된 접근입니다. 로그인 페이지로 이동합니다.",
SessionConstants.INVALID_ACCESS);
}
/**
* JSON
*
* @param response HTTP
* @param status HTTP
* @param result
* @param message
* @param errorCode
* @throws IOException
*/
private void sendJsonResponse(HttpServletResponse response, HttpStatus status, boolean result,
String message, String errorCode) throws IOException {
// 응답 상태 코드 및 컨텐츠 타입 설정
response.setStatus(status.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
// 응답 객체 생성
ApiResponseEntity<Void> responseBody = ApiResponseEntity.<Void>builder()
.result(result)
.message(message)
.errorCode(errorCode)
.build();
// JSON 변환 및 응답 출력
response.getWriter().write(objectMapper.writeValueAsString(responseBody));
}
/**
* Referer URL
* , URL
*
* @param requestURI URI
* @return Referer (true: , false: )
*/
private boolean isRefererCheckExcluded(String requestURI) {
// 설정된 제외 패턴과 요청 URI를 비교
for (String pattern : interceptorProperties.getRefererExclude()) {
if (pathMatcher.match(pattern, requestURI)) {
return true; // 제외 패턴에 해당하면 Referer 검사 제외
}
}
// 제외 패턴에 해당하지 않으면 Referer 검사 필요
return false;
}
/**
* AJAX
*
* @param request HTTP
* @return AJAX
*/
private boolean isAjaxRequest(HttpServletRequest request) {
return HttpServletUtil.isAjaxRequest(request) || HttpServletUtil.isRealAjaxRequest(request);
}
/**
*
*
* @param request HTTP
* @param response HTTP
* @param message
* @param targetUrl URL
* @throws IOException
*/
private void redirectToPage(HttpServletRequest request, HttpServletResponse response,
String message, String targetUrl) throws IOException {
response.setContentType("text/html; charset=UTF-8");
response.getWriter().write("<script type='text/javascript'>");
response.getWriter().write("alert('" + message + "');");
response.getWriter().write("location.href = '" + request.getContextPath() + targetUrl + "';");
response.getWriter().write("</script>");
}
/**
*
*
* @param request HTTP
* @param response HTTP
* @param message
* @throws IOException
*/
private void redirectToLoginPageWithPopupCheck(HttpServletRequest request, HttpServletResponse response,
String message) throws IOException {
try {
// 메시지를 request attribute로 설정
request.setAttribute("errorMessage", message);
// 401.jsp로 forward하여 handleSessionExpired() 함수 호출
request.getRequestDispatcher("/WEB-INF/views/error/401.jsp").forward(request, response);
} catch (Exception e) {
log.error("401 페이지 forward 실패, 기존 방식으로 처리", e);
// forward 실패 시 기존 방식으로 fallback
response.setContentType("text/html; charset=UTF-8");
response.getWriter().write("<script type='text/javascript'>");
response.getWriter().write("alert('" + message + "');");
response.getWriter().write("if(window.opener) {");
response.getWriter().write(" var topWindow = window;");
response.getWriter().write(" while(topWindow.opener && !topWindow.opener.closed) {");
response.getWriter().write(" topWindow = topWindow.opener;");
response.getWriter().write(" }");
response.getWriter().write(" topWindow.location.href = '" + request.getContextPath() + loginProperties.getUrl() + "';");
response.getWriter().write(" window.close();");
response.getWriter().write("} else {");
response.getWriter().write(" location.href = '" + request.getContextPath() + loginProperties.getUrl() + "';");
response.getWriter().write("}");
response.getWriter().write("</script>");
}
}
}

@ -0,0 +1,50 @@
package egovframework.util;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* packageName : egovframework.common.model
* fileName : ApiResponse
* author :
* date : 25. 5. 8.
* description : AJAX
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 25. 5. 8.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponseEntity<T> {
/**
*
*/
private boolean success;
/**
*
*/
private boolean result;
/**
*
*/
private String message;
/**
* ( )
*/
private T data;
/**
* ( )
*/
private String errorCode;
}

@ -0,0 +1,173 @@
package egovframework.util;
import go.kr.project.common.model.PagingVO;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.HashMap;
import java.util.Map;
/**
* packageName : egovframework.common.util
* fileName : ApiResponseUtil
* author :
* date : 25. 5. 8.
* description : AJAX
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 25. 5. 8.
*/
public class ApiResponseUtil {
/**
* ( )
* @param message
* @return ResponseEntity
*/
public static ResponseEntity<ApiResponseEntity<Void>> success(String message) {
ApiResponseEntity<Void> response = ApiResponseEntity.<Void>builder()
.success(true)
.result(true)
.message(message)
.build();
return ResponseEntity.ok(response);
}
/**
* ( )
* @param data
* @param message
* @param <T>
* @return ResponseEntity
*/
public static <T> ResponseEntity<ApiResponseEntity<T>> success(T data, String message) {
ApiResponseEntity<T> response = ApiResponseEntity.<T>builder()
.success(true)
.result(true)
.message(message)
.data(data)
.build();
return ResponseEntity.ok(response);
}
/**
* ( , )
* @param data
* @param <T>
* @return ResponseEntity
*/
public static <T> ResponseEntity<ApiResponseEntity<T>> success(T data) {
return success(data, "요청이 성공적으로 처리되었습니다.");
}
/**
*
* TOAST UI Grid
*
* TOAST UI Grid :
* {
* data: {
* contents: [...], // 실제 데이터 배열
* pagination: {
* page: number,
* perPage: number,
* totalCount: number
* }
* }
* }
*
* @param data
* @param pagingVO
* @param message
* @param <T>
* @return ResponseEntity
*/
public static <T> ResponseEntity<ApiResponseEntity<Map<String, Object>>> successWithGrid(T data, PagingVO pagingVO, String message) {
// TOAST UI Grid 형식에 맞는 데이터 구조 생성
Map<String, Object> gridData = new HashMap<>();
// contents에 실제 데이터 배열 설정
gridData.put("contents", data);
// pagination 정보 설정
if( "Y".equals(pagingVO.getPagingYn()) ){
Map<String, Object> paginationMap = new HashMap<>();
paginationMap.put("page", pagingVO.getPage());
paginationMap.put("perPage", pagingVO.getPerPage());
paginationMap.put("totalCount", pagingVO.getTotalCount());
paginationMap.put("totalPages", pagingVO.getTotalPages());
gridData.put("pagination", paginationMap);
}
// 응답 객체 생성
ApiResponseEntity<Map<String, Object>> response = ApiResponseEntity.<Map<String, Object>>builder()
.success(true)
.result(true)
.message(message)
.data(gridData)
.build();
return ResponseEntity.ok(response);
}
/**
* ( )
* @param data
* @param pagingVO
* @param <T>
* @return ResponseEntity
*/
public static <T> ResponseEntity<ApiResponseEntity<Map<String, Object>>> successWithGrid(T data, PagingVO pagingVO) {
return successWithGrid(data, pagingVO, "요청이 성공적으로 처리되었습니다.");
}
/**
*
* @param message
* @param errorCode
* @param status HTTP
* @return ResponseEntity
*/
public static ResponseEntity<ApiResponseEntity<Void>> error(String message, String errorCode, HttpStatus status) {
ApiResponseEntity<Void> response = ApiResponseEntity.<Void>builder()
.success(false)
.result(false)
.message(message)
.errorCode(errorCode)
.build();
return ResponseEntity.status(status).body(response);
}
/**
* ( HTTP : BAD_REQUEST)
* @param message
* @param errorCode
* @return ResponseEntity
*/
public static ResponseEntity<ApiResponseEntity<Void>> error(String message, String errorCode) {
return error(message, errorCode, HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* ( : "UNKNOWN_ERROR")
* @param message
* @param status HTTP
* @return ResponseEntity
*/
public static ResponseEntity<ApiResponseEntity<Void>> error(String message, HttpStatus status) {
return error(message, "UNKNOWN_ERROR", status);
}
/**
* ( HTTP : BAD_REQUEST, : "UNKNOWN_ERROR")
* @param message
* @return ResponseEntity
*/
public static ResponseEntity<ApiResponseEntity<Void>> error(String message) {
return error(message, "MESSAGE", HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@ -0,0 +1,107 @@
package egovframework.util;
import lombok.extern.slf4j.Slf4j;
/**
* packageName : egovframework.util
* fileName : BatchSessionUtil
* author :
* date : 2025-01-27
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-01-27
*/
@Slf4j
public class BatchSessionUtil {
/**
* ID
*/
public static final String BATCH_SYSTEM_USER_ID = "BATCH_SYSTEM";
/**
*
*/
public static final String BATCH_SYSTEM_USER_ACCOUNT = "BATCH_SYSTEM";
/**
*
*/
public static final String BATCH_SYSTEM_USER_NAME = "시스템 배치";
/**
* ID .
*
* @return ID
*/
public static String getBatchUserId() {
return BATCH_SYSTEM_USER_ID;
}
/**
* .
*
* @return
*/
public static String getBatchUserAccount() {
return BATCH_SYSTEM_USER_ACCOUNT;
}
/**
* .
*
* @return
*/
public static String getBatchUserName() {
return BATCH_SYSTEM_USER_NAME;
}
/**
* .
*
* @return true ( )
*/
public static boolean isBatchSystemUser() {
return true;
}
/**
* .
*
* @param jobName
* @param jobGroup
*/
public static void logBatchContext(String jobName, String jobGroup) {
log.info("배치 작업 실행 컨텍스트 - 작업: {}.{}, 사용자: {}, 계정: {}, 시스템권한: {}",
jobGroup, jobName, getBatchUserId(), getBatchUserAccount(), isBatchSystemUser());
}
/**
* .
*
* @param sessionAccessor
* @param defaultValue
* @param <T>
* @return
*/
public static <T> T safeSessionAccess(SessionAccessor<T> sessionAccessor, T defaultValue) {
try {
return sessionAccessor.access();
} catch (Exception e) {
log.debug("세션 접근 실패 (배치 컨텍스트), 기본값 사용: {}", e.getMessage());
return defaultValue;
}
}
/**
*
*
* @param <T>
*/
@FunctionalInterface
public interface SessionAccessor<T> {
T access() throws Exception;
}
}

@ -0,0 +1,255 @@
package egovframework.util;
import javax.servlet.http.HttpServletRequest;
/**
* packageName : egovframework.util
* fileName : ClientInfoUtil
* author :
* date : 2025-05-22
* description : (, OS, )
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-05-22
*/
public class ClientInfoUtil {
/**
* User-Agent .
*
* @param userAgent
* @return
*/
public static String extractDeviceInfo(String userAgent) {
if (userAgent == null || userAgent.isEmpty()) {
return "Unknown";
}
userAgent = userAgent.toLowerCase();
StringBuilder deviceInfo = new StringBuilder();
// OS 정보 추출
String osInfo = extractOsInfo(userAgent);
// 디바이스 유형 및 제조사 정보 추출
String deviceType = extractDeviceType(userAgent);
// 브라우저 정보 추출
String browserInfo = extractBrowserInfo(userAgent);
// 정보 조합
if (!deviceType.isEmpty()) {
deviceInfo.append(deviceType);
}
if (!osInfo.isEmpty()) {
if (deviceInfo.length() > 0) {
deviceInfo.append(" - ");
}
deviceInfo.append(osInfo);
}
// 모바일/태블릿이 아닌 경우에만 브라우저 정보 추가
if (!deviceType.contains("Mobile") && !deviceType.contains("Tablet") && !browserInfo.isEmpty()) {
if (deviceInfo.length() > 0) {
deviceInfo.append(" - ");
}
deviceInfo.append(browserInfo);
}
return deviceInfo.length() > 0 ? deviceInfo.toString() : "Unknown Device";
}
/**
* User-Agent OS .
*
* @param userAgent
* @return OS
*/
public static String extractOsInfo(String userAgent) {
// Windows 버전 확인
if (userAgent.contains("windows")) {
if (userAgent.contains("windows nt 10")) {
return "Windows 10";
} else if (userAgent.contains("windows nt 6.3")) {
return "Windows 8.1";
} else if (userAgent.contains("windows nt 6.2")) {
return "Windows 8";
} else if (userAgent.contains("windows nt 6.1")) {
return "Windows 7";
} else if (userAgent.contains("windows nt 6.0")) {
return "Windows Vista";
} else if (userAgent.contains("windows nt 5.1") || userAgent.contains("windows xp")) {
return "Windows XP";
} else {
return "Windows";
}
}
// Mac OS 버전 확인
if (userAgent.contains("macintosh") || userAgent.contains("mac os x")) {
if (userAgent.contains("mac os x 10_15")) {
return "macOS Catalina";
} else if (userAgent.contains("mac os x 10_14")) {
return "macOS Mojave";
} else if (userAgent.contains("mac os x 10_13")) {
return "macOS High Sierra";
} else if (userAgent.contains("mac os x 10_12")) {
return "macOS Sierra";
} else {
return "Mac OS";
}
}
// 기타 OS 확인
if (userAgent.contains("android")) {
// Android 버전 추출
int startIndex = userAgent.indexOf("android ");
if (startIndex != -1) {
int endIndex = userAgent.indexOf(";", startIndex);
if (endIndex != -1) {
return "Android " + userAgent.substring(startIndex + 8, endIndex).trim();
}
}
return "Android";
} else if (userAgent.contains("ios")) {
return "iOS";
} else if (userAgent.contains("iphone os") || userAgent.contains("cpu os")) {
// iOS 버전 추출
int startIndex = userAgent.contains("iphone os ") ? userAgent.indexOf("iphone os ") + 10 : userAgent.indexOf("cpu os ") + 7;
if (startIndex != -1) {
int endIndex = userAgent.indexOf(" ", startIndex);
if (endIndex != -1) {
String version = userAgent.substring(startIndex, endIndex).trim().replace("_", ".");
return "iOS " + version;
}
}
return "iOS";
} else if (userAgent.contains("linux")) {
return "Linux";
} else if (userAgent.contains("unix")) {
return "Unix";
}
return "";
}
/**
* User-Agent .
*
* @param userAgent
* @return
*/
public static String extractDeviceType(String userAgent) {
// 모바일 디바이스 확인
if (userAgent.contains("mobile")) {
if (userAgent.contains("iphone")) {
return "iPhone";
} else if (userAgent.contains("android")) {
// 안드로이드 제조사 확인
if (userAgent.contains("samsung")) {
return "Samsung Mobile";
} else if (userAgent.contains("lg")) {
return "LG Mobile";
} else if (userAgent.contains("huawei")) {
return "Huawei Mobile";
} else if (userAgent.contains("xiaomi")) {
return "Xiaomi Mobile";
} else if (userAgent.contains("oppo")) {
return "OPPO Mobile";
} else if (userAgent.contains("vivo")) {
return "Vivo Mobile";
} else if (userAgent.contains("oneplus")) {
return "OnePlus Mobile";
} else if (userAgent.contains("motorola")) {
return "Motorola Mobile";
} else if (userAgent.contains("nokia")) {
return "Nokia Mobile";
} else {
return "Android Mobile";
}
} else if (userAgent.contains("windows phone")) {
return "Windows Phone";
} else {
return "Mobile Device";
}
}
// 태블릿 확인
if (userAgent.contains("ipad")) {
return "iPad";
} else if (userAgent.contains("android") && !userAgent.contains("mobile")) {
// 안드로이드 태블릿 제조사 확인
if (userAgent.contains("samsung")) {
return "Samsung Tablet";
} else if (userAgent.contains("huawei")) {
return "Huawei Tablet";
} else if (userAgent.contains("lenovo")) {
return "Lenovo Tablet";
} else if (userAgent.contains("asus")) {
return "Asus Tablet";
} else if (userAgent.contains("amazon") || userAgent.contains("kindle")) {
return "Amazon Tablet";
} else {
return "Android Tablet";
}
} else if (userAgent.contains("tablet")) {
return "Tablet";
}
return "";
}
/**
* User-Agent .
*
* @param userAgent
* @return
*/
public static String extractBrowserInfo(String userAgent) {
if (userAgent.contains("edge/") || userAgent.contains("edg/")) {
return "Edge Browser";
} else if (userAgent.contains("chrome/")) {
return "Chrome Browser";
} else if (userAgent.contains("safari/") && !userAgent.contains("chrome/")) {
return "Safari Browser";
} else if (userAgent.contains("firefox/")) {
return "Firefox Browser";
} else if (userAgent.contains("opera/") || userAgent.contains("opr/")) {
return "Opera Browser";
} else if (userAgent.contains("msie") || userAgent.contains("trident/")) {
return "Internet Explorer";
}
return "";
}
/**
* IP .
*
* @param request HTTP
* @return IP
*/
public static String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}

@ -0,0 +1,431 @@
package egovframework.util;
import org.springframework.stereotype.Component;
import java.util.*;
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,567 @@
package egovframework.util;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.time.*;
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", "yyyyMMdd")
* @param days (: , : )
* @return
*/
public static String getCurrentDateAddDays(String pattern, int days) {
LocalDate now = LocalDate.now();
return now.plusDays(days).format(DateTimeFormatter.ofPattern(pattern));
}
/**
*
*
* @param pattern (: "yyyy-MM-dd", "yyyyMMdd")
* @param months (: , : )
* @return
*/
public static String getCurrentDateAddMonths(String pattern, int months) {
LocalDate now = LocalDate.now();
return now.plusMonths(months).format(DateTimeFormatter.ofPattern(pattern));
}
/**
*
* @param pattern (: "yyyy-MM-dd HH:mm:ss", "yyyyMMddHHmmss")
* @return
*/
public static String getCurrentDateTime(String pattern) {
LocalDateTime now = LocalDateTime.now();
return now.format(DateTimeFormatter.ofPattern(pattern));
}
/**
* 30
* @param pattern (: "yyyy-MM-dd HH:mm:ss", "yyyyMMddHHmmss")
* @return 30
*/
public static String getCurrentDateTimeRoundedToHalfHour(String pattern) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime adjusted;
int minute = now.getMinute();
if (minute < 30) {
// 0~29분이면 같은 시간의 30분으로 조정
adjusted = now.withMinute(30).withSecond(0).withNano(0);
} else {
// 30~59분이면 다음 시간의 00분으로 조정
adjusted = now.plusHours(1).withMinute(0).withSecond(0).withNano(0);
}
return adjusted.format(DateTimeFormatter.ofPattern(pattern));
}
/**
* 30 1
* @param pattern (: "yyyy-MM-dd HH:mm:ss", "yyyyMMddHHmmss")
* @return 30 1
*/
public static String getCurrentDateTimeRoundedToHalfHourPlusOneHour(String pattern) {
// 먼저 30분 단위로 조정된 시간을 구한 후
LocalDateTime adjusted = LocalDateTime.parse(
getCurrentDateTimeRoundedToHalfHour("yyyy-MM-dd'T'HH:mm:ss"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
);
// 1시간 추가
adjusted = adjusted.plusHours(1);
return adjusted.format(DateTimeFormatter.ofPattern(pattern));
}
/**
* 30
* @param dateTime LocalDateTime
* @return 30 LocalDateTime
*/
public static LocalDateTime roundToHalfHour(LocalDateTime dateTime) {
if (dateTime == null) {
return null;
}
int minute = dateTime.getMinute();
if (minute < 30) {
// 0~29분이면 같은 시간의 30분으로 조정
return dateTime.withMinute(30).withSecond(0).withNano(0);
} else {
// 30~59분이면 다음 시간의 00분으로 조정
return dateTime.plusHours(1).withMinute(0).withSecond(0).withNano(0);
}
}
/**
* 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;
}
}
/**
* yyyyMMdd yyyy-MM-dd
* @param dateStr (yyyyMMdd , : "20250825")
* @return (yyyy-MM-dd , : "2025-08-25"), null
*/
public static String formatDateString(String dateStr) {
if (dateStr == null || dateStr.length() != 8) {
return null;
}
try {
// yyyyMMdd 형식으로 파싱한 후 yyyy-MM-dd 형식으로 포맷
LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyyMMdd"));
return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
} 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(Object dateTime, String pattern) {
if (!(dateTime instanceof LocalDateTime) || pattern == null) {
return "";
}
return ((LocalDateTime) 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 pattern (: "yyyy-MM-dd", "yyyyMMdd")
* @param type (Y: , M: , D: )
* @param amount (: , : )
* @return
*/
public static String getCurrentDateAdd(String pattern, String type, int amount) {
LocalDate now = LocalDate.now();
LocalDate result;
if ("Y".equalsIgnoreCase(type) || "year".equalsIgnoreCase(type)) {
result = now.plusYears(amount);
} else if ("M".equalsIgnoreCase(type) || "month".equalsIgnoreCase(type)) {
result = now.plusMonths(amount);
} else if ("D".equalsIgnoreCase(type) || "day".equalsIgnoreCase(type)) {
result = now.plusDays(amount);
} else {
result = now;
}
return result.format(DateTimeFormatter.ofPattern(pattern));
}
/**
*
* @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));
}
/**
* "HH:mm:ss"
* @param request HttpServletRequest
* @param pattern (: "HH:mm:ss")
* @return (HH:mm:ss )
*/
public static String getSessionExpiryTime(HttpServletRequest request, String pattern) {
if (request == null) {
return "00:00:00";
}
HttpSession session = request.getSession(false);
if (session == null) {
return "00:00:00";
}
// 세션 최대 유효 시간(초)
int maxInactiveInterval = session.getMaxInactiveInterval();
// 시, 분, 초 계산
int hours = maxInactiveInterval / 3600;
int minutes = (maxInactiveInterval % 3600) / 60;
int seconds = maxInactiveInterval % 60;
// HH:mm:ss 형식으로 반환
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
}
}

@ -0,0 +1,161 @@
/**
* Class Name : EgovFileScrty.java
* Description : Base64/ / Business Interface class
* Modification Information
*
*
* ------- -------- ---------------------------
* 2009.02.04
*
* @author
* @since 2009. 02. 04
* @version 1.0
* @see
*
* Copyright (C) 2009 by MOPAS All right reserved.
*/
package egovframework.util;
//import org.apache.tomcat.util.codec.binary.Base64;
import org.apache.commons.codec.binary.Base64;
import java.security.MessageDigest;
/**
* @Class Name : EgovFileScrty.java
* @Description :
* @Modification Information
*
*
* ---------- ------- -------------------
* 2019.11.29 encryptPassword(String data) : KISA ( )
* 2022.11.16
*
* @author
* @since 2009.08.26
* @version 1.0
*/
public class EgovFileScrty {
/**
*
*
* @param byte[] data
* @return String result
* @exception Exception
*/
public static String encodeBinary(byte[] data) throws Exception {
if (data == null) {
return "";
}
return new String(Base64.encodeBase64(data));
}
/**
*
*
* @param String data
* @return String result
* @exception Exception
*/
@Deprecated
public static String encode(String data) throws Exception {
return encodeBinary(data.getBytes());
}
/**
*
*
* @param String data
* @return String result
* @exception Exception
*/
public static byte[] decodeBinary(String data) throws Exception {
return Base64.decodeBase64(data.getBytes());
}
/**
*
*
* @param String data
* @return String result
* @exception Exception
*/
@Deprecated
public static String decode(String data) throws Exception {
return new String(decodeBinary(data));
}
/**
* ( SHA-256 )
*
* @param password
* @param id salt ID
* @return
* @throws Exception
*/
public static String encryptPassword(String password, String id) throws Exception {
if (password == null) return "";
if (id == null) return ""; // KISA 보안약점 조치 (2018-12-11, 신용호)
byte[] hashValue; // 해쉬값
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.reset();
md.update(id.getBytes());
hashValue = md.digest(password.getBytes());
return new String(Base64.encodeBase64(hashValue));
}
/**
* ( SHA-256 )
* @param data
* @param salt Salt
* @return
* @throws Exception
*/
public static String encryptPassword(String data, byte[] salt) throws Exception {
if (data == null) {
return "";
}
byte[] hashValue; // 해쉬값
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.reset();
md.update(salt);
hashValue = md.digest(data.getBytes());
return new String(Base64.encodeBase64(hashValue));
}
/**
* (salt ).
*
* @param data
* @param encoded (Base64 )
* @return
* @throws Exception
*/
public static boolean checkPassword(String data, String encoded, byte[] salt) throws Exception {
byte[] hashValue; // 해쉬값
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.reset();
md.update(salt);
hashValue = md.digest(data.getBytes());
return MessageDigest.isEqual(hashValue, Base64.decodeBase64(encoded.getBytes()));
}
}

@ -0,0 +1,688 @@
package egovframework.util;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import egovframework.configProperties.FileUploadProperties;
import go.kr.project.common.model.FileVO;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
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.*;
import java.util.stream.Collectors;
/**
* /
*/
@Component
public class FileUtil {
/** 파일 업로드 설정 */
private final FileUploadProperties fileUploadProperties;
/** 날짜 유틸리티 */
private final DateUtil dateUtil;
/** 파일 저장 기본 경로 */
private String uploadPath;
/** 최대 파일 크기 (단일 파일) - 기본값 10MB */
private long maxFileSize;
/** 최대 총 파일 크기 - 기본값 50MB */
private long maxTotalSize;
/** 허용된 파일 확장자 */
private String allowedExtensions;
/** 허용된 파일 확장자 목록 */
private List<String> allowedExtensionList;
/** 최대 파일 개수 - 기본값 10개 */
private int maxFiles;
/** 실제 파일 삭제 여부 - 기본값 true */
private boolean realFileDelete;
/** 하위 디렉토리 설정 */
private Map<String, String> subDirs;
/**
*
* @param fileUploadProperties
* @param dateUtil
*/
public FileUtil(FileUploadProperties fileUploadProperties, DateUtil dateUtil) {
this.fileUploadProperties = fileUploadProperties;
this.dateUtil = dateUtil;
}
/**
*
* 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)
.collect(Collectors.toList());
}
/**
* <h3> </h3>
* : return "/" .
* @see #cleanPath(String, boolean)
*/
public static String cleanPath(String filePath) throws IllegalArgumentException {
// Java 는 파일 경로 구분자가 "\" 든 "/" 든 똑같이 대응하도록 되어 있다.
// 그러므로 useOsFileSeparatorOnResult=false 로 세팅한다.
return cleanPath(filePath, false);
}
/**
* <h3> </h3>
* @param filePath
* @param useOsFileSeparatorOnResult return OS true .
* true window "C:\sdf\some.txt" , false "C:/sdf/some.txt"
* @return
* @throws IllegalArgumentException , WEB Root .
*/
public static String cleanPath(String filePath, boolean useOsFileSeparatorOnResult) throws IllegalArgumentException {
if(filePath == null || filePath.trim().isEmpty()) {
return filePath;
}
filePath = filePath.trim();
/*
, "/" .
.
String OS [useOsFileSeparatorOnResult=true] .
*/
String sanitizedPath = filePath.replaceAll("\\\\+", "/");
// "../" 경로 제거
sanitizedPath = sanitizedPath.replaceAll("\\.\\.", "");
// "/./" 경로 제거
sanitizedPath = sanitizedPath.replaceAll("/\\./", "/");
// "&" 제거
sanitizedPath = sanitizedPath.replaceAll("&", "");
// Replace multiple consecutive slashes with a single slash
sanitizedPath = sanitizedPath.replaceAll("/{2,}", "/");
// 루트 경로 사용 검사 ("/" or "C:/"). 발견되면 예외를 던진다.
checkRootPathUsage(filePath, sanitizedPath);
// URL 경로 체크(= Web root 사용여부 검사). 발견되면 예외를 던진다.
checkUrlLikePathUsage(filePath, sanitizedPath);
// 최종적으로 운영체제에 맞게 문자열을 반환할지 분기처리하여 return 한다.
return useOsFileSeparatorOnResult ?
changeFileSeparatorDependOnOs(sanitizedPath) : sanitizedPath;
}
/**
* .
*/
private static void checkRootPathUsage(String filePath, String sanitizedPath) {
if (sanitizedPath.equals("/") || sanitizedPath.matches("[A-Za-z]:/$")) {
throw new IllegalArgumentException("Invalid path: " + filePath);
}
}
/**
* URL Path . Web Root .
*/
private static void checkUrlLikePathUsage(String filePath, String sanitizedPath) throws IllegalArgumentException {
Path path = Paths.get(sanitizedPath);
// Check if the path is a URL-like path (potentially a web root)
URI uri = path.toUri();
if (uri.getScheme() != null && uri.getHost() != null) {
throw new IllegalArgumentException("Path resembles a URL-like path. Error Occurred Path => " + filePath);
}
}
/**
* .
*/
public static String changeFileSeparatorDependOnOs(String path) {
String osName = System.getProperty("os.name").toLowerCase();
return osName.toLowerCase().contains("win") ? path.replace("/", File.separator) : path;
}
/**
* File File .
*/
public static File cleanPath(File file) {
String path = file.getPath();
return new File(cleanPath(path));
}
/**
* Path File .
*/
public static Path cleanPath(Path file) {
String path = file.toFile().getPath();
return Paths.get(cleanPath(path));
}
/**
*
* (Path Traversal)
* @param path
* @param isFileName (true: , false: )
* @throws IOException
*/
private void validatePath(String path, boolean isFileName) throws IOException {
if (path == null) {
throw new IOException("경로가 null입니다.");
}
// 경로 정규화 시도
try {
Path normalizedPath = Paths.get(cleanPath(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
* @return
*/
private boolean isAllowedExtension(String fileExt) {
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
* @param userAgent User-Agent
* @return
* @throws IOException
*/
private String getEncodedFilename(String fileName, String userAgent) throws IOException {
if (userAgent.contains("MSIE") || userAgent.contains("Trident") || userAgent.contains("Chrome")) {
// IE, Chrome
return URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
} else if (userAgent.contains("Firefox")) {
// Firefox
return "\"" + new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1) + "\"";
} else {
// 기타 브라우저
return URLEncoder.encode(fileName, "UTF-8");
}
}
/**
* FileVO
* @param originalFilename
* @param storedFilename
* @param subDir
* @param fileSize
* @param fileExt
* @param contentType
* @return FileVO
*/
private FileVO createFileVO(String originalFilename, String storedFilename, String subDir,
long fileSize, String fileExt, String contentType) {
FileVO fileVO = new FileVO();
fileVO.setOriginalFileNm(originalFilename);
fileVO.setStoredFileNm(storedFilename);
fileVO.setFilePath(subDir);
fileVO.setFileSize(fileSize);
fileVO.setFileExt(fileExt);
fileVO.setContentType(contentType);
return fileVO;
}
/**
*
* @param files
* @param subDir (notice, biz, qna )
* @return
* @throws IOException
*/
public List<FileVO> uploadFiles(List<MultipartFile> files, String subDir) throws IOException {
// 파일 유효성 검증
validateFiles(files);
// 디렉토리 경로 검증
validatePath(subDir, false);
// 년월 정보 추출
String yearMonth = dateUtil.getCurrentYearMonth();
// 년월 디렉토리를 포함한 경로 생성
String yearMonthPath = subDir + File.separator + yearMonth;
// 디렉토리 경로 검증
validatePath(yearMonthPath, false);
// 디렉토리 생성
String uploadDir = uploadPath + File.separator + yearMonthPath;
createDirectoryIfNotExists(uploadDir);
// 파일 업로드 처리
return processFileUploads(files, yearMonthPath, uploadDir);
}
/**
*
* @param files
* @throws IOException
*/
private void validateFiles(List<MultipartFile> files) throws IOException {
// 파일 개수 검증
int validFileCount = (int) files.stream()
.filter(file -> !file.isEmpty())
.count();
if (validFileCount > maxFiles) {
throw new IOException("파일 개수가 제한을 초과했습니다. 최대 " + maxFiles + "개까지 가능합니다.");
}
}
/**
*
* @param files
* @param subDir
* @param uploadDir
* @return
* @throws IOException
*/
private List<FileVO> processFileUploads(List<MultipartFile> files, String subDir, String uploadDir) throws IOException {
List<FileVO> uploadedFiles = new ArrayList<>();
long totalSize = 0;
for (MultipartFile file : files) {
if (file.isEmpty()) continue;
// 파일 크기 검증
validateFileSize(file, totalSize);
totalSize += file.getSize();
// 파일 저장 및 정보 생성
FileVO fileVO = saveFile(file, subDir, uploadDir);
uploadedFiles.add(fileVO);
}
return uploadedFiles;
}
/**
*
* @param file
* @param currentTotalSize
* @throws IOException
*/
private void validateFileSize(MultipartFile file, long currentTotalSize) throws IOException {
// 단일 파일 크기 검증
if (file.getSize() > maxFileSize * 1024 * 1024) {
throw new IOException("파일 크기가 제한을 초과했습니다. 최대 " + maxFileSize + "MB까지 가능합니다.");
}
// 총 파일 크기 검증
if (currentTotalSize + file.getSize() > maxTotalSize * 1024 * 1024) {
throw new IOException("총 파일 크기가 제한을 초과했습니다. 최대 " + maxTotalSize + "MB까지 가능합니다.");
}
}
/**
*
* @param file
* @param subDir
* @param uploadDir
* @return
* @throws IOException
*/
private FileVO saveFile(MultipartFile file, String subDir, String uploadDir) throws IOException {
// 원본 파일명 및 확장자 추출
String originalFilename = file.getOriginalFilename();
String fileExt = getFileExtension(originalFilename);
// 확장자 검증
if (!isAllowedExtension(fileExt)) {
throw new IOException("허용되지 않은 파일 형식입니다: " + fileExt);
}
// UUID를 이용한 저장 파일명 생성
String storedFilename = UUID.randomUUID() + "." + fileExt;
// 파일명 검증
validatePath(storedFilename, true);
// 파일 저장 경로 생성 및 검증
Path filePath = createAndValidateFilePath(uploadDir, storedFilename);
// 파일 저장
Files.write(filePath, file.getBytes());
// 파일 정보 생성 및 반환
return createFileVO(originalFilename, storedFilename, subDir,
file.getSize(), fileExt, file.getContentType());
}
/**
*
* @param uploadDir
* @param storedFilename
* @return
* @throws IOException
*/
private Path createAndValidateFilePath(String uploadDir, String storedFilename) throws IOException {
Path filePath = Paths.get(uploadDir).normalize().resolve(storedFilename).normalize();
// 생성된 경로가 업로드 디렉토리 내에 있는지 확인
File targetFile = filePath.toFile();
String canonicalUploadDir = new File(uploadDir).getCanonicalPath();
String canonicalTargetPath = targetFile.getCanonicalPath();
if (!canonicalTargetPath.startsWith(canonicalUploadDir)) {
throw new IOException("잘못된 파일 경로입니다.");
}
return filePath;
}
/**
*
* @param fileVO
* @param request HTTP
* @param response HTTP
* @throws IOException
*/
public void downloadFile(FileVO fileVO, HttpServletRequest request, HttpServletResponse response) throws IOException {
// 파일 정보 검증 및 파일 객체 생성
File file = validateAndGetFile(fileVO);
// 파일 확장자 검증
validateFileExtension(fileVO.getOriginalFileNm());
// 브라우저별 인코딩된 파일명 생성
String userAgent = request.getHeader("User-Agent");
String encodedFilename = getEncodedFilename(fileVO.getOriginalFileNm(), userAgent);
// 응답 헤더 설정
setResponseHeaders(response, fileVO, file, encodedFilename);
// 파일 전송
sendFile(file, response);
}
/**
*
* @param fileVO
* @return
* @throws IOException
*/
private File validateAndGetFile(FileVO fileVO) throws IOException {
// 파일 경로 생성
String filePath = uploadPath + File.separator + fileVO.getFilePath() + File.separator + fileVO.getStoredFileNm();
File file = new File(filePath);
// 파일 존재 여부 확인
if (!file.exists()) {
throw new IOException("파일을 찾을 수 없습니다: " + filePath);
}
// 보안 검사: 경로 검증 (경로 탐색 공격 방지)
String canonicalPath = file.getCanonicalPath();
if (!canonicalPath.startsWith(new File(uploadPath).getCanonicalPath())) {
throw new IOException("잘못된 파일 경로입니다.");
}
return file;
}
/**
*
* @param filename
* @throws IOException
*/
private void validateFileExtension(String filename) throws IOException {
String fileExt = getFileExtension(filename);
if (!isAllowedExtension(fileExt)) {
throw new IOException("허용되지 않은 파일 형식입니다: " + fileExt);
}
}
/**
*
* @param response HTTP
* @param fileVO
* @param file
* @param encodedFilename
*/
private void setResponseHeaders(HttpServletResponse response, FileVO fileVO, File file, String encodedFilename) {
// 컨텐츠 타입 및 길이 설정
response.setContentType(fileVO.getContentType());
response.setContentLength((int) file.length());
// 다운로드 관련 헤더 설정
response.setHeader("Content-Disposition", "attachment; filename=" + encodedFilename);
response.setHeader("Content-Transfer-Encoding", "binary");
// 캐시 관련 헤더 설정
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
// 보안 관련 헤더 설정
response.setHeader("X-Content-Type-Options", "nosniff");
}
/**
*
* @param file
* @param response HTTP
* @throws IOException
*/
private void sendFile(File file, HttpServletResponse response) throws IOException {
try (FileInputStream fis = new FileInputStream(file);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.flush();
}
}
/**
*
* @param fileVO
* @return
*/
public boolean deleteFile(FileVO fileVO) {
// real-file-delete 설정이 false인 경우 실제 파일 삭제하지 않고 true 반환
if (!realFileDelete) {
return true;
}
try {
// 파일 경로 검증 및 파일 객체 생성
File file = getValidatedFileForDelete(fileVO);
// 파일 존재 여부 확인 및 삭제
return file.exists() && file.delete();
} catch (IOException e) {
// 로그 기록 등의 처리를 추가할 수 있음
return false;
}
}
/**
*
* @param fileVO
* @return
* @throws IOException
*/
private File getValidatedFileForDelete(FileVO fileVO) throws IOException {
// 파일 경로 및 파일명 검증
String subDir = fileVO.getFilePath();
String storedFilename = fileVO.getStoredFileNm();
// 경로 검증
validatePath(subDir, false);
validatePath(storedFilename, true);
// 파일 경로 생성
String filePath = uploadPath + File.separator + subDir + File.separator + storedFilename;
File file = new File(filePath);
// 보안 검사: 경로 검증 (경로 탐색 공격 방지)
String canonicalPath = file.getCanonicalPath();
if (!canonicalPath.startsWith(new File(uploadPath).getCanonicalPath())) {
throw new IOException("잘못된 파일 경로입니다.");
}
return file;
}
/**
*
* @param directory
*/
@CanIgnoreReturnValue
private void createDirectoryIfNotExists(String directory) {
File dir = new File(directory);
if (!dir.exists()) {
dir.mkdirs();
}
}
/**
*
* @param filename
* @return ( )
*/
private String getFileExtension(String filename) {
if (filename == null || filename.isEmpty()) {
return "";
}
int lastDotIndex = filename.lastIndexOf(".");
if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) {
return "";
}
return filename.substring(lastDotIndex + 1).toLowerCase();
}
/**
* .
* .
*
* @param sourcePath
* @param targetPath
* @return
* @throws IOException
*/
public boolean copyFile(String sourcePath, String uploadDir, String targetPath) throws IOException {
if (sourcePath == null || targetPath == null || uploadDir == null) {
throw new IOException("원본 또는 대상 파일 경로가 null입니다.");
}
File sourceFile = new File(uploadPath + File.separator + sourcePath);
File targetFile = new File(uploadPath + File.separator + targetPath);
// 원본 파일 존재 여부 확인
if (!sourceFile.exists()) {
throw new IOException("원본 파일이 존재하지 않습니다: " + uploadPath + File.separator + sourcePath);
}
// 원본 파일이 일반 파일인지 확인
if (!sourceFile.isFile()) {
throw new IOException("원본이 일반 파일이 아닙니다: " + uploadPath + File.separator + sourcePath);
}
createDirectoryIfNotExists( uploadPath + File.separator + uploadDir);
// 대상 디렉토리 생성
File targetDir = targetFile.getParentFile();
if (targetDir != null && !targetDir.exists()) {
if (!targetDir.mkdirs()) {
throw new IOException("대상 디렉토리 생성 실패: " + targetDir.getAbsolutePath());
}
}
// 파일 복사
try {
Files.copy(sourceFile.toPath(), targetFile.toPath(),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
return true;
} catch (IOException e) {
throw new IOException("파일 복사 실패: " + sourcePath + " -> " + targetPath, e);
}
}
}

@ -0,0 +1,58 @@
package egovframework.util;
import javax.servlet.http.HttpServletRequest;
/**
* packageName : egovframework.util
* fileName : HttpServletUtil
* author :
* date : 25. 5. 8.
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 25. 5. 8.
*/
public class HttpServletUtil {
/**
* AJAX
* *.ajax URL AJAX .
*/
public static boolean isAjaxRequest(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return requestURI != null && requestURI.endsWith(".ajax");
}
/**
* AJAX
* HTTP AJAX .
*
* AJAX :
* 1. 'X-Requested-With: XMLHttpRequest'
* 2. 'Accept' 'application/json'
* 3. 'Content-Type' 'application/json'
*/
public static boolean isRealAjaxRequest(HttpServletRequest request) {
// 'X-Requested-With: XMLHttpRequest' 헤더 확인 (가장 일반적인 AJAX 요청 지표)
String requestedWith = request.getHeader("X-Requested-With");
if ("XMLHttpRequest".equals(requestedWith)) {
return true;
}
// 'Accept' 헤더가 'application/json'을 포함하는지 확인
String accept = request.getHeader("Accept");
if (accept != null && accept.contains("application/json")) {
return true;
}
// 'Content-Type' 헤더가 'application/json'인지 확인
String contentType = request.getHeader("Content-Type");
if (contentType != null && contentType.contains("application/json")) {
return true;
}
return false;
}
}

@ -0,0 +1,170 @@
package egovframework.util;
import lombok.extern.slf4j.Slf4j;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
/**
* packageName : go.kr.project.batch.util
* fileName : ImageValidationUtil
* author :
* date : 2025-01-27
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-01-27
*/
@Slf4j
public class ImageValidationUtil {
/**
*
*/
private static final List<String> SUPPORTED_IMAGE_EXTENSIONS = Arrays.asList(
"jpg", "jpeg", "png", "gif", "bmp", "tiff", "tif", "webp"
);
/**
* MIME
*/
private static final List<String> IMAGE_MIME_TYPES = Arrays.asList(
"image/jpeg", "image/jpg", "image/png", "image/gif",
"image/bmp", "image/tiff", "image/webp"
);
/**
* .
*
* @param fileName
* @return
*/
public static boolean isImageByExtension(String fileName) {
if (fileName == null || fileName.trim().isEmpty()) {
return false;
}
int lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex == -1 || lastDotIndex == fileName.length() - 1) {
return false;
}
String extension = fileName.substring(lastDotIndex + 1).toLowerCase();
return SUPPORTED_IMAGE_EXTENSIONS.contains(extension);
}
/**
* MIME .
*
* @param filePath
* @return
*/
public static boolean isImageByMimeType(Path filePath) {
try {
String mimeType = Files.probeContentType(filePath);
if (mimeType == null) {
return false;
}
return IMAGE_MIME_TYPES.contains(mimeType.toLowerCase());
} catch (IOException e) {
log.warn("MIME 타입 확인 중 오류 발생: {}", filePath, e);
return false;
}
}
/**
* .
*
* @param filePath
* @return (true: , false: )
*/
public static boolean isImageCorrupted(Path filePath) {
try {
File imageFile = filePath.toFile();
// 파일이 존재하지 않거나 크기가 0인 경우
if (!imageFile.exists() || imageFile.length() == 0) {
log.warn("이미지 파일이 존재하지 않거나 크기가 0입니다: {}", filePath);
return true;
}
// ImageIO를 사용하여 이미지 읽기 시도
BufferedImage image = ImageIO.read(imageFile);
// 이미지를 읽을 수 없는 경우 손상된 것으로 판단
if (image == null) {
log.warn("이미지 파일을 읽을 수 없습니다: {}", filePath);
return true;
}
// 이미지 크기가 유효하지 않은 경우
if (image.getWidth() <= 0 || image.getHeight() <= 0) {
log.warn("이미지 크기가 유효하지 않습니다: {} ({}x{})",
filePath, image.getWidth(), image.getHeight());
return true;
}
log.debug("이미지 파일 검증 성공: {} ({}x{})",
filePath, image.getWidth(), image.getHeight());
return false;
} catch (IOException e) {
log.warn("이미지 파일 검증 중 오류 발생: {}", filePath, e);
return true;
} catch (Exception e) {
log.error("이미지 파일 검증 중 예상치 못한 오류 발생: {}", filePath, e);
return true;
}
}
/**
* .
* ( + MIME )
*
* @param filePath
* @return
*/
public static boolean isImageFile(Path filePath) {
String fileName = filePath.getFileName().toString();
// 1차: 확장자 검사
boolean isImageByExt = isImageByExtension(fileName);
// 2차: MIME 타입 검사 (확장자가 이미지인 경우에만)
boolean isImageByMime = isImageByExt && isImageByMimeType(filePath);
log.debug("이미지 파일 검사 결과: {} - 확장자: {}, MIME: {}",
fileName, isImageByExt, isImageByMime);
return isImageByMime;
}
/**
* .
*
* @param filePath
* @return
*/
public static String getImageInfo(Path filePath) {
try {
BufferedImage image = ImageIO.read(filePath.toFile());
if (image == null) {
return "이미지 정보를 읽을 수 없음";
}
return String.format("크기: %dx%d, 타입: %d",
image.getWidth(), image.getHeight(), image.getType());
} catch (IOException e) {
log.warn("이미지 정보 조회 중 오류 발생: {}", filePath, e);
return "이미지 정보 조회 실패: " + e.getMessage();
}
}
}

@ -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,75 @@
package egovframework.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
/**
* Utility class for URL pattern matching
*/
@Slf4j
public class PathMatcherUtil {
private static final AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* Check if URL pattern matches the request URI
*
* @param pattern URL pattern
* @param uri Request URI
* @return true if matches, false otherwise
*/
/**
* URL URI
*
*
* @param pattern URL ( )
* @param uri URI
* @return
*/
public static boolean match(String pattern, String uri) {
if (pattern == null || uri == null) {
return false;
}
// 패턴 전처리 - 앞뒤 공백 제거
String trimmedPattern = pattern.trim();
// 패턴에 콤마가 포함된 경우 (여러 패턴이 하나의 문자열로 전달된 경우)
if (trimmedPattern.contains(",")) {
//log.debug("콤마로 구분된 여러 패턴이 발견되었습니다: [{}]", trimmedPattern);
// 콤마로 패턴을 분리하여 각각 처리
String[] patterns = trimmedPattern.split(",");
for (String singlePattern : patterns) {
String trimmedSinglePattern = singlePattern.trim();
if (!trimmedSinglePattern.isEmpty()) {
// 개별 패턴으로 재귀 호출
if (match(trimmedSinglePattern, uri)) {
return true;
}
}
}
return false;
}
// 단일 패턴 처리
try {
// AntPathMatcher는 패턴에 따라 다르게 동작함
// 1. 정확한 경로 매칭 (/system/user/list.do)
// 2. 경로 변수 매칭 (/system/user/{id}.do)
// 3. 와일드카드 매칭 (/system/user/*.do, /system/user/**/list.do)
boolean isMatch = pathMatcher.match(trimmedPattern, uri);
//log.debug("URL 패턴 매칭 시도: 패턴=[{}], URI=[{}], 결과=[{}]", trimmedPattern, uri, isMatch);
// 매칭되지 않았을 경우 추가 디버깅 정보
if (!isMatch && (trimmedPattern.contains("*") || trimmedPattern.contains("?"))) {
//log.debug("와일드카드 패턴 매칭 실패: 패턴=[{}], URI=[{}]", trimmedPattern, uri);
}
return isMatch;
} catch (Exception e) {
log.error("URL 패턴 매칭 중 오류 발생: 패턴=[{}], URI=[{}], 오류=[{}]", trimmedPattern, uri, e.getMessage(), e);
return false;
}
}
}

@ -0,0 +1,67 @@
package egovframework.util;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* packageName : egovframework.util
* fileName : PathUtil
* author :
* date : 2023-06-10
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2023-06-10
*/
public class PathUtil {
/**
* contextPath .
*
* @return contextPath (: "/app")
*/
public static String getContextPath() {
try {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request.getContextPath();
} catch (Exception e) {
// RequestContextHolder가 사용 불가능한 경우 (예: 배치 작업 등)
return "";
}
}
/**
* URL contextPath .
* contextPath URL URL .
*
* @param url contextPath URL
* @return contextPath URL
*/
public static String addContextPath(String url) {
if (url == null || url.isEmpty()) {
return "";
}
// 외부 URL인 경우 (http:// 또는 https://로 시작하는 경우)
if (url.startsWith("http://") || url.startsWith("https://")) {
return url;
}
String contextPath = getContextPath();
// URL이 이미 contextPath로 시작하는 경우
if (!contextPath.isEmpty() && url.startsWith(contextPath)) {
return url;
}
// URL이 /로 시작하지 않는 경우 /를 추가
if (!url.startsWith("/")) {
url = "/" + url;
}
return contextPath + url;
}
}

@ -0,0 +1,235 @@
package egovframework.util;
import egovframework.constant.SessionConstants;
import go.kr.project.login.model.LoginUserVO;
import go.kr.project.login.model.SessionVO;
import go.kr.project.system.group.model.GroupVO;
import go.kr.project.system.menu.model.MenuVO;
import go.kr.project.system.role.model.RoleVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.List;
/**
* packageName : egovframework.util
* fileName : SessionUtil
* author :
* date : 2025-05-15
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-05-15
*/
@Slf4j
public class SessionUtil {
// 세션 키는 SessionConstants 클래스에서 관리
/**
* HttpServletRequest .
* HTTP null .
*
* @return HttpServletRequest , null
*/
public static HttpServletRequest getRequest() {
try {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
return attr.getRequest();
} catch (IllegalStateException e) {
// 배치 작업 등에서 HTTP 요청 컨텍스트가 없는 경우
log.debug("HTTP 요청 컨텍스트가 없습니다. (배치 작업 또는 비웹 컨텍스트)");
return null;
}
}
/**
* HttpSession .
* HTTP null .
*
* @return HttpSession , null
*/
public static HttpSession getSession() {
HttpServletRequest request = getRequest();
return request != null ? request.getSession() : null;
}
/**
* SessionVO .
* HTTP null .
*
* @return SessionVO , null
*/
public static SessionVO getSessionVO() {
HttpSession session = getSession();
if (session != null) {
return (SessionVO) session.getAttribute(SessionConstants.SESSION_KEY);
}
return null;
}
/**
* .
* HTTP null .
*
* @return , null
*/
public static LoginUserVO getLoginUser() {
SessionVO sessionVO = getSessionVO();
if (sessionVO != null && sessionVO.isLogin()) {
return sessionVO.getUser();
}
return null;
}
/**
* ID .
* HTTP null .
*
* @return ID, null
*/
public static String getUserId() {
LoginUserVO user = getLoginUser();
if (user != null) {
return user.getUserId();
}
return null;
}
/**
* .
* HTTP null .
*
* @return , null
*/
public static String getUserName() {
LoginUserVO user = getLoginUser();
if (user != null) {
return user.getUserNm();
}
return null;
}
/**
* .
* HTTP null .
*
* @return , null
*/
public static String getUserAccount() {
LoginUserVO user = getLoginUser();
if (user != null) {
return user.getUserAcnt();
}
return null;
}
/**
* .
* HTTP null .
*
* @return , null
*/
public static GroupVO getUserGroup() {
SessionVO sessionVO = getSessionVO();
if (sessionVO != null && sessionVO.isLogin()) {
return sessionVO.getGroup();
}
return null;
}
/**
* .
* HTTP null .
*
* @return , null
*/
public static List<RoleVO> getUserRoles() {
SessionVO sessionVO = getSessionVO();
if (sessionVO != null) {
return sessionVO.getRoles();
}
return null;
}
/**
* .
* HTTP null .
*
* @return , null
*/
public static List<MenuVO> getUserMenus() {
SessionVO sessionVO = getSessionVO();
if (sessionVO != null) {
return sessionVO.getMenus();
}
return null;
}
/**
* .
* HTTP false .
*
* @return
*/
public static boolean isLogin() {
SessionVO sessionVO = getSessionVO();
return sessionVO != null && sessionVO.isLogin();
}
/**
* .
* HTTP false .
*
* @return
*/
public static boolean isVisitor() {
SessionVO sessionVO = getSessionVO();
return sessionVO != null && sessionVO.isVisitor();
}
/**
* .
* HTTP false .
*
* @return
*/
public static boolean isSystem() {
SessionVO sessionVO = getSessionVO();
return sessionVO != null && sessionVO.isSystem();
}
/**
* .
* .
*
* @return ( true)
*/
public static boolean isBatchSystem() {
// 배치 작업은 시스템 권한으로 실행
return true;
}
/**
* ID .
* .
*
* @return ID
*/
public static String getBatchUserId() {
return "BATCH_SYSTEM";
}
/**
* .
*
* @return
*/
public static String getBatchUserAccount() {
return "BATCH_SYSTEM";
}
}

@ -0,0 +1,245 @@
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.isEmpty();
}
/**
* 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;
}
/**
* UTF-8 .
* 3, / 1 .
*
* @param str
* @return , null 0
*/
public static int calculateUtf8ByteLength(String str) {
if (str == null || str.isEmpty()) {
return 0;
}
int byteLength = 0;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
// 한글 유니코드 범위 확인
if ((c >= 0xAC00 && c <= 0xD7A3) || (c >= 0x3131 && c <= 0x318E)) {
byteLength += 3; // 한글은 3바이트
} else {
byteLength += 1; // 영문/숫자는 1바이트
}
}
return byteLength;
}
}

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

@ -0,0 +1,588 @@
package egovframework.util.excel;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.EncryptionMode;
import org.apache.poi.poifs.crypt.Encryptor;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.security.GeneralSecurityException;
import java.util.List;
import static egovframework.util.excel.SuperClassReflectionUtils.getAllFieldsWithExcelColumn;
/**
* SXSSF(Streaming)
*
* <p> Apache POI SXSSF(Streaming) API .
* SXSSF flush .</p>
*
* <p><b> :</b></p>
* <ul>
* <li> ( )</li>
* <li> , , </li>
* <li> </li>
* <li> (AES Agile )</li>
* </ul>
*
* @see SxssfExcelFile
* @see SxssfMultiSheetExcelFile
* @author eGovFrame
*/
public abstract class BaseSxssfExcelFile implements ExcelFile {
// ==================== 상수 정의 ====================
/** SXSSF 워크북의 메모리 윈도우 크기 (메모리에 유지할 행의 개수) */
protected static final int ROW_ACCESS_WINDOW_SIZE = 10000;
/** 엑셀 행 시작 인덱스 */
protected static final int ROW_START_INDEX = 0;
/** 엑셀 컬럼 시작 인덱스 */
protected static final int COLUMN_START_INDEX = 0;
/** 제목 폰트 크기 (포인트) */
private static final short TITLE_FONT_SIZE = 22;
/** 제목 행 높이 (포인트) */
private static final float TITLE_ROW_HEIGHT = 33f;
/** 자동 조정 시 컬럼 최소 너비 (POI 단위: 1/256 문자) */
private static final int MIN_COLUMN_WIDTH = 3000;
/** 자동 조정 시 컬럼 최대 너비 (POI 단위: 1/256 문자) */
private static final int MAX_COLUMN_WIDTH = 15000;
/** 자동 조정 시 너비 증가 비율 (한글 폰트 보정) */
private static final double AUTO_SIZE_MULTIPLIER = 1.3;
/** POI 컬럼 너비 단위 (1 문자 = 256 POI 단위) */
private static final int POI_WIDTH_UNIT = 256;
// ==================== 필드 ====================
/** SXSSF 워크북 인스턴스 */
protected SXSSFWorkbook workbook;
/** 현재 작업 중인 시트 */
protected Sheet sheet;
/** 제목 행 오프셋 (제목이 있으면 1, 없으면 0) */
protected int titleRowOffset = 0;
// ==================== 생성자 ====================
/**
*
* <p>ROW_ACCESS_WINDOW_SIZE SXSSF .</p>
*/
public BaseSxssfExcelFile() {
this.workbook = new SXSSFWorkbook(ROW_ACCESS_WINDOW_SIZE);
}
// ==================== 렌더링 메서드 ====================
/**
* , , .
*
* <p> :</p>
* <ol>
* <li> ( )</li>
* <li> </li>
* <li> </li>
* <li> </li>
* </ol>
*
* @param data ( , , )
* @param metadata (, , )
*/
protected void renderSheetContent(ExcelSheetData data, ExcelMetadata metadata) {
// 1. 제목 행 렌더링 (있는 경우)
if (data.getTitle() != null && !data.getTitle().trim().isEmpty()) {
int columnCount = metadata.getDataFieldNames().size();
renderTitleRow(metadata.getSheetName(), data.getTitle(), columnCount);
}
// 2. 헤더 행 렌더링
renderHeaders(metadata);
// 3. 데이터 행 렌더링
renderDataLines(data, metadata);
}
/**
* .
*
* <p> , .
* 22 , , 33 .</p>
*
* @param sheetName
* @param title
* @param columnCount
*/
protected void renderTitleRow(String sheetName, String title, int columnCount) {
// 시트가 없으면 생성
if (sheet == null) {
sheet = workbook.createSheet(sheetName);
}
// 제목 행 생성
Row titleRow = sheet.createRow(ROW_START_INDEX);
titleRow.setHeightInPoints(TITLE_ROW_HEIGHT);
// 제목 스타일 생성
CellStyle titleStyle = createTitleCellStyle();
// 첫 번째 셀에 제목 입력
createCell(titleRow, COLUMN_START_INDEX, title, titleStyle);
// 모든 컬럼에 걸쳐 셀 병합
if (columnCount > 1) {
sheet.addMergedRegion(new CellRangeAddress(
ROW_START_INDEX, // 시작 행
ROW_START_INDEX, // 종료 행
COLUMN_START_INDEX, // 시작 컬럼
columnCount - 1 // 종료 컬럼
));
}
// 제목 행 오프셋 설정 (다음 행부터 헤더가 시작됨)
titleRowOffset = 1;
}
/**
* .
*
* <p> {@link ExcelColumn} .
* , .</p>
*
* <p> SXSSF autoSizeColumn .</p>
*
* @param excelMetadata (, , )
*/
protected void renderHeaders(ExcelMetadata excelMetadata) {
// 시트가 없으면 생성 (제목이 없는 경우)
if (sheet == null) {
sheet = workbook.createSheet(excelMetadata.getSheetName());
}
// 헤더 행 생성 (제목이 있으면 row 1, 없으면 row 0)
Row row = sheet.createRow(ROW_START_INDEX + titleRowOffset);
// 헤더 스타일 생성 (굵게, 가운데 정렬)
CellStyle headerStyle = createHeaderCellStyle();
// 각 필드의 헤더명 렌더링
int columnIndex = COLUMN_START_INDEX;
for (String fieldName : excelMetadata.getDataFieldNames()) {
createCell(row, columnIndex++, excelMetadata.getHeaderName(fieldName), headerStyle);
}
// SXSSF에서 autoSizeColumn 사용을 위한 컬럼 추적 등록
trackColumnsForAutoSizing(excelMetadata.getDataFieldNames().size());
}
/**
* .
*
* <p> {@link ExcelColumn}
* .</p>
*
* <p> :
* <ul>
* <li>headerWidth : </li>
* <li>headerWidth 0 : ( + )</li>
* </ul>
* </p>
*
* @param data ( , )
* @param metadata ( )
* @throws RuntimeException
*/
protected void renderDataLines(ExcelSheetData data, ExcelMetadata metadata) {
CellStyle dataStyle = createCellStyle(workbook, false);
// 숫자 서식 스타일 생성(정수/소수) - 기본 스타일을 복제하여 데이터 포맷만 부여
CellStyle integerNumberStyle = workbook.createCellStyle();
integerNumberStyle.cloneStyleFrom(dataStyle);
short intDf = workbook.createDataFormat().getFormat("#,##0");
integerNumberStyle.setDataFormat(intDf);
CellStyle decimalNumberStyle = workbook.createCellStyle();
decimalNumberStyle.cloneStyleFrom(dataStyle);
short decDf = workbook.createDataFormat().getFormat("#,##0.##");
decimalNumberStyle.setDataFormat(decDf);
// 한글 중요 주석: 정렬 옵션 지원을 위해 기본/숫자 스타일에 대해 좌/중앙/우 정렬 변형을 미리 생성해 재사용한다.
CellStyle baseLeft = workbook.createCellStyle();
baseLeft.cloneStyleFrom(dataStyle);
baseLeft.setAlignment(HorizontalAlignment.LEFT);
CellStyle baseCenter = workbook.createCellStyle();
baseCenter.cloneStyleFrom(dataStyle);
baseCenter.setAlignment(HorizontalAlignment.CENTER);
CellStyle baseRight = workbook.createCellStyle();
baseRight.cloneStyleFrom(dataStyle);
baseRight.setAlignment(HorizontalAlignment.RIGHT);
CellStyle intLeft = workbook.createCellStyle();
intLeft.cloneStyleFrom(integerNumberStyle);
intLeft.setAlignment(HorizontalAlignment.LEFT);
CellStyle intCenter = workbook.createCellStyle();
intCenter.cloneStyleFrom(integerNumberStyle);
intCenter.setAlignment(HorizontalAlignment.CENTER);
CellStyle intRight = workbook.createCellStyle();
intRight.cloneStyleFrom(integerNumberStyle);
intRight.setAlignment(HorizontalAlignment.RIGHT);
CellStyle decLeft = workbook.createCellStyle();
decLeft.cloneStyleFrom(decimalNumberStyle);
decLeft.setAlignment(HorizontalAlignment.LEFT);
CellStyle decCenter = workbook.createCellStyle();
decCenter.cloneStyleFrom(decimalNumberStyle);
decCenter.setAlignment(HorizontalAlignment.CENTER);
CellStyle decRight = workbook.createCellStyle();
decRight.cloneStyleFrom(decimalNumberStyle);
decRight.setAlignment(HorizontalAlignment.RIGHT);
// 데이터 시작 행 (제목이 있으면 row 2, 없으면 row 1)
int rowIndex = ROW_START_INDEX + titleRowOffset + 1;
List<Field> fields = getAllFieldsWithExcelColumn(data.getType());
// 각 데이터 객체를 행으로 변환
for (Object record : data.getDataList()) {
Row row = sheet.createRow(rowIndex++);
renderDataRow(row, record, fields, dataStyle, integerNumberStyle, decimalNumberStyle,
baseLeft, baseCenter, baseRight,
intLeft, intCenter, intRight,
decLeft, decCenter, decRight);
}
// 컬럼 너비 조정
adjustColumnWidths(fields, metadata);
}
// ==================== private 헬퍼 메서드 ====================
/**
* .
*
* @return CellStyle ( 22 , / )
*/
private CellStyle createTitleCellStyle() {
CellStyle titleStyle = workbook.createCellStyle();
titleStyle.setAlignment(HorizontalAlignment.LEFT);
titleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
Font titleFont = workbook.createFont();
titleFont.setFontHeightInPoints(TITLE_FONT_SIZE);
titleFont.setBold(true);
titleStyle.setFont(titleFont);
// 중요 로직(한글): 제목 셀에도 실선(THIN) 보더를 적용하여 표의 일관성 유지
titleStyle.setBorderTop(BorderStyle.THIN);
titleStyle.setBorderBottom(BorderStyle.THIN);
titleStyle.setBorderLeft(BorderStyle.THIN);
titleStyle.setBorderRight(BorderStyle.THIN);
// 중요 로직(한글): 제목 배경색 지정 - 요구사항에 따라 #be8e00 색상을 적용
// SXSSF에서는 내부적으로 XSSFCellStyle을 사용하므로 캐스팅하여 사용자 색상 설정
if (titleStyle instanceof org.apache.poi.xssf.usermodel.XSSFCellStyle) {
org.apache.poi.xssf.usermodel.XSSFCellStyle xssf = (org.apache.poi.xssf.usermodel.XSSFCellStyle) titleStyle;
xssf.setFillForegroundColor(new org.apache.poi.xssf.usermodel.XSSFColor(new java.awt.Color(0xBE, 0x8E, 0x00), null));
xssf.setFillPattern(FillPatternType.SOLID_FOREGROUND);
}
return titleStyle;
}
/**
* .
*
* @return CellStyle ( , )
*/
private CellStyle createHeaderCellStyle() {
CellStyle headerStyle = createCellStyle(workbook, true);
headerStyle.setAlignment(HorizontalAlignment.CENTER);
// 중요 로직(한글): 헤더 배경색 지정 - 요구사항에 따라 #fde598 색상을 적용
if (headerStyle instanceof org.apache.poi.xssf.usermodel.XSSFCellStyle) {
org.apache.poi.xssf.usermodel.XSSFCellStyle xssf = (org.apache.poi.xssf.usermodel.XSSFCellStyle) headerStyle;
xssf.setFillForegroundColor(new org.apache.poi.xssf.usermodel.XSSFColor(new java.awt.Color(0xFD, 0xE5, 0x98), null));
xssf.setFillPattern(FillPatternType.SOLID_FOREGROUND);
}
return headerStyle;
}
/**
* SXSSF autoSizeColumn .
*
* @param columnCount
*/
private void trackColumnsForAutoSizing(int columnCount) {
for (int i = 0; i < columnCount; i++) {
((org.apache.poi.xssf.streaming.SXSSFSheet) sheet)
.trackColumnForAutoSizing(COLUMN_START_INDEX + i);
}
}
/**
* .
*
* @param row
* @param record
* @param fields ExcelColumn
* @param baseStyle ( )
* @param integerNumberStyle (#,##0)
* @param decimalNumberStyle (#,##0.##)
* @throws RuntimeException
*/
private void renderDataRow(
Row row,
Object record,
List<Field> fields,
CellStyle baseStyle, CellStyle integerNumberStyle, CellStyle decimalNumberStyle,
CellStyle baseLeft, CellStyle baseCenter, CellStyle baseRight,
CellStyle intLeft, CellStyle intCenter, CellStyle intRight,
CellStyle decLeft, CellStyle decCenter, CellStyle decRight) {
int columnIndex = COLUMN_START_INDEX;
try {
for (Field field : fields) {
field.setAccessible(true);
Object value = field.get(record);
// 수식 처리: ExcelColumn에 formula 설정이 있는 경우 수식을 생성하여 설정
ExcelColumn excelColumn = field.getAnnotation(ExcelColumn.class);
ExcelColumn.Align align = (excelColumn != null) ? excelColumn.align() : ExcelColumn.Align.AUTO;
if (excelColumn != null && excelColumn.formula() && !excelColumn.formulaRefField().isEmpty()) {
String refFieldName = excelColumn.formulaRefField();
int refColumnIndex = findFieldColumnIndex(fields, refFieldName);
if (refColumnIndex >= 0) {
// 한글 중요 주석: 참조 대상(X열) 값이 null 또는 공백("")이면 수식을 적용하지 않고 빈 셀로 처리하여 컬럼 정렬 유지
Field refField = fields.get(refColumnIndex);
refField.setAccessible(true);
Object refValue = refField.get(record);
boolean isBlankRef = (refValue == null) || (refValue instanceof String && ((String) refValue).trim().isEmpty());
CellStyle baseAligned = baseStyle;
if (align == ExcelColumn.Align.LEFT) baseAligned = baseLeft;
else if (align == ExcelColumn.Align.CENTER) baseAligned = baseCenter;
else if (align == ExcelColumn.Align.RIGHT) baseAligned = baseRight;
if (isBlankRef) {
Cell cell = row.createCell(columnIndex++);
cell.setCellValue("");
cell.setCellStyle(baseAligned);
continue; // 다음 필드 처리
}
String refExcelColumn = toExcelColumnName(refColumnIndex + 1); // 1-based for Excel column letters
int excelRowNumber = row.getRowNum() + 1; // 1-based row number in Excel
String formula = String.format(excelColumn.formulaPattern(), refExcelColumn, excelRowNumber);
Cell cell = row.createCell(columnIndex++);
cell.setCellFormula(formula);
cell.setCellStyle(baseAligned);
continue; // 다음 필드 처리
}
}
// 기본 값 처리: 숫자 타입일 경우 천단위 콤마 서식 적용 + 정렬 옵션 반영
boolean isIntNum = (value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long);
boolean isDecNum = (value instanceof Float || value instanceof Double || value instanceof BigDecimal);
CellStyle styleToUse = baseStyle;
if (isIntNum) {
styleToUse = integerNumberStyle;
} else if (isDecNum) {
styleToUse = decimalNumberStyle;
}
if (align != ExcelColumn.Align.AUTO) {
if (isIntNum) {
if (align == ExcelColumn.Align.LEFT) styleToUse = intLeft;
else if (align == ExcelColumn.Align.CENTER) styleToUse = intCenter;
else if (align == ExcelColumn.Align.RIGHT) styleToUse = intRight;
} else if (isDecNum) {
if (align == ExcelColumn.Align.LEFT) styleToUse = decLeft;
else if (align == ExcelColumn.Align.CENTER) styleToUse = decCenter;
else if (align == ExcelColumn.Align.RIGHT) styleToUse = decRight;
} else {
if (align == ExcelColumn.Align.LEFT) styleToUse = baseLeft;
else if (align == ExcelColumn.Align.CENTER) styleToUse = baseCenter;
else if (align == ExcelColumn.Align.RIGHT) styleToUse = baseRight;
}
}
createCell(row, columnIndex++, value, styleToUse);
}
} catch (IllegalAccessException e) {
throw new RuntimeException("데이터 필드 접근 중 오류가 발생했습니다.", e);
}
}
/**
* .
* : ExcelColumn 0 .
*/
private int findFieldColumnIndex(List<Field> fields, String targetFieldName) {
for (int i = 0; i < fields.size(); i++) {
if (fields.get(i).getName().equals(targetFieldName)) {
return i;
}
}
return -1;
}
/**
* 1 (A, B, ... AA, AB ...) .
* : .
*/
private String toExcelColumnName(int columnNumber) {
StringBuilder sb = new StringBuilder();
int num = columnNumber;
while (num > 0) {
int rem = (num - 1) % 26;
sb.insert(0, (char) ('A' + rem));
num = (num - 1) / 26;
}
return sb.toString();
}
/**
* .
*
* <p>headerWidth ,
* .</p>
*
* @param fields ExcelColumn
* @param metadata ( )
*/
private void adjustColumnWidths(List<Field> fields, ExcelMetadata metadata) {
for (int i = 0; i < fields.size(); i++) {
int columnIndex = COLUMN_START_INDEX + i;
Field field = fields.get(i);
String fieldName = field.getName();
int headerWidth = metadata.getHeaderWidth(fieldName);
if (headerWidth > 0) {
// headerWidth가 지정된 경우: 지정된 값 사용
setColumnWidthInCharacters(columnIndex, headerWidth);
} else {
// headerWidth가 0인 경우: 자동 조정 + 한글 보정
autoSizeColumnWithKoreanFontCorrection(columnIndex);
}
}
}
/**
* .
*
* @param columnIndex
* @param widthInCharacters
*/
private void setColumnWidthInCharacters(int columnIndex, int widthInCharacters) {
sheet.setColumnWidth(columnIndex, widthInCharacters * POI_WIDTH_UNIT);
}
/**
* .
*
* <p>POI autoSizeColumn ,
* 1.3 / .</p>
*
* @param columnIndex
*/
private void autoSizeColumnWithKoreanFontCorrection(int columnIndex) {
sheet.autoSizeColumn(columnIndex);
int currentWidth = sheet.getColumnWidth(columnIndex);
int adjustedWidth = (int) (currentWidth * AUTO_SIZE_MULTIPLIER);
// 최소/최대 범위 적용
int finalWidth = Math.max(MIN_COLUMN_WIDTH, Math.min(adjustedWidth, MAX_COLUMN_WIDTH));
sheet.setColumnWidth(columnIndex, finalWidth);
}
// ==================== ExcelFile 인터페이스 구현 ====================
/**
* .
*
* @param stream
* @throws RuntimeException IO
*/
@Override
public void write(OutputStream stream) {
try {
workbook.write(stream);
} catch (IOException e) {
throw new RuntimeException("엑셀 파일 쓰기 중 오류가 발생했습니다.", e);
}
}
/**
* .
*
* <p>AES Agile .
* password null .</p>
*
* @param stream
* @param password (null )
* @throws RuntimeException IO
*/
@Override
public void writeWithEncryption(OutputStream stream, String password) {
try {
if (password == null) {
write(stream);
} else {
encryptAndWrite(stream, password);
}
workbook.close();
stream.close();
} catch (IOException e) {
throw new RuntimeException("암호화된 엑셀 파일 쓰기 중 오류가 발생했습니다.", e);
}
}
/**
* .
*
* @param stream
* @param password
* @throws IOException IO
*/
private void encryptAndWrite(OutputStream stream, String password) throws IOException {
POIFSFileSystem fileSystem = new POIFSFileSystem();
OutputStream encryptorStream = getEncryptorStream(fileSystem, password);
workbook.write(encryptorStream);
encryptorStream.close();
fileSystem.writeFilesystem(stream);
fileSystem.close();
}
/**
* POIFSFileSystem .
*
* @param fileSystem POI
* @param password
* @return
* @throws RuntimeException
*/
private OutputStream getEncryptorStream(POIFSFileSystem fileSystem, String password) {
try {
Encryptor encryptor = new EncryptionInfo(EncryptionMode.agile).getEncryptor();
encryptor.confirmPassword(password);
return encryptor.getDataStream(fileSystem);
} catch (IOException | GeneralSecurityException e) {
throw new RuntimeException("POIFSFileSystem에서 암호화 스트림 생성에 실패했습니다.", e);
}
}
}

@ -0,0 +1,45 @@
package egovframework.util.excel;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelColumn {
String headerName() default "";
int headerWidth() default 0; // 0이면 자동 조정, 0보다 크면 지정된 너비 사용
// ==================== 수식 지원 옵션 ====================
/**
*
* : true , .
*/
boolean formula() default false;
/**
*
* : ) "deadline" , .
*/
String formulaRefField() default "";
/**
* . %s (A, B, ...), %d (1) .
* : "=%s%d-TODAY()" , "참조셀 - 오늘" .
*/
String formulaPattern() default "=%s%d-TODAY()";
// ==================== 정렬 옵션(선택) ====================
/**
* ()
* : (AUTO) . LEFT/CENTER/RIGHT .
*/
Align align() default Align.AUTO;
/**
*
* : AUTO ( ) .
*/
enum Align { AUTO, LEFT, CENTER, RIGHT }
}

@ -0,0 +1,141 @@
package egovframework.util.excel;
import org.apache.poi.ss.usermodel.*;
import java.io.IOException;
import java.io.OutputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
/**
*
*
* <p> .
* , , .</p>
*
* <p><b> :</b></p>
* <ul>
* <li> </li>
* <li> (, )</li>
* <li> (/)</li>
* </ul>
*
* @see BaseSxssfExcelFile
* @author eGovFrame
*/
public interface ExcelFile {
// ==================== 파일 출력 메서드 ====================
/**
* .
*
* @param stream
* @throws IOException
*/
void write(OutputStream stream) throws IOException;
/**
* .
*
* <p>password null .</p>
*
* @param stream
* @param password (null )
* @throws IOException
*/
void writeWithEncryption(OutputStream stream, String password) throws IOException;
// ==================== 셀 생성 메서드 ====================
/**
* .
*
* <p> :
* <ul>
* <li> (Integer, Long, Double, Float): </li>
* <li> (Boolean): </li>
* <li>/ (LocalDateTime, LocalDate, LocalTime): </li>
* <li> : toString() </li>
* </ul>
* </p>
*
* <p><b>/ :</b></p>
* <ul>
* <li>LocalDateTime: "yyyy-MM-dd HH:mm:ss"</li>
* <li>LocalDate: "yyyy-MM-dd"</li>
* <li>LocalTime: "HH:mm:ss"</li>
* </ul>
*
* @param <T>
* @param row
* @param column (0 )
* @param value (null )
* @param style
*/
default <T> void createCell(Row row, int column, T value, CellStyle style) {
// null 값도 보더가 보이도록 빈 셀 생성 후 스타일 적용 (NPE 방지)
Cell cell = row.createCell(column);
if (value == null) {
cell.setCellValue("");
cell.setCellStyle(style);
return;
}
// 타입별로 적절한 셀 값 설정
if (value instanceof Integer) {
cell.setCellValue((Integer) value);
} else if (value instanceof Long) {
cell.setCellValue((Long) value);
} else if (value instanceof Double) {
cell.setCellValue((Double) value);
} else if (value instanceof Float) {
cell.setCellValue((Float) value);
} else if (value instanceof Boolean) {
cell.setCellValue((Boolean) value);
} else if (value instanceof LocalDateTime) {
LocalDateTime dateTime = (LocalDateTime) value;
cell.setCellValue(dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
} else if (value instanceof LocalDate) {
LocalDate date = (LocalDate) value;
cell.setCellValue(date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
} else if (value instanceof LocalTime) {
LocalTime time = (LocalTime) value;
cell.setCellValue(time.format(DateTimeFormatter.ofPattern("HH:mm:ss")));
} else {
cell.setCellValue(value.toString());
}
// 스타일 적용
cell.setCellStyle(style);
}
// ==================== 스타일 생성 메서드 ====================
/**
* .
*
* <p> , .</p>
*
* @param wb
* @param isBold (true: , false: )
* @return
*/
default CellStyle createCellStyle(Workbook wb, boolean isBold) {
CellStyle style = wb.createCellStyle();
Font font = wb.createFont();
font.setBold(isBold);
style.setFont(font);
// 중요 로직(한글): 생성되는 모든 셀에 대해 실선(THIN) 보더 적용
// 데이터 셀과 헤더 셀은 본 메서드를 통해 스타일이 생성되므로, 여기서 공통 보더를 지정한다.
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
return style;
}
}

@ -0,0 +1,115 @@
package egovframework.util.excel;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
@Slf4j
public class ExcelHandler<T> {
public List<T> handleExcelUpload(List<MultipartFile> mFiles, Class<T> clazz) {
List<T> dataList = new ArrayList<>();
mFiles.forEach(file -> {
try (InputStream inputStream = file.getInputStream()) {
dataList.addAll(parseExcel(inputStream, clazz));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
return dataList;
}
private List<T> parseExcel(InputStream inputStream, Class<T> clazz) throws IOException {
Workbook workbook = new XSSFWorkbook(inputStream);
Sheet sheet = workbook.getSheetAt(0); // 첫 번째 시트를 가져옴
// 헤더 정보 추출
Row headerRow = sheet.getRow(0);
List<String> headers = StreamSupport.stream(headerRow.spliterator(), false)
.map(Cell::getStringCellValue)
.collect(Collectors.toList());
List<T> dataList = StreamSupport.stream(sheet.spliterator(), false)
.skip(1) // 첫 번째 행은 헤더이므로 건너뜁니다.
.filter(this::isRowNotEmpty) // 빈 행이 아닌 경우에만 처리합니다.
.map(row -> mapRowToDto(row, clazz, headers))
.collect(Collectors.toList());
workbook.close();
return dataList;
}
private boolean isRowNotEmpty(Row row) {
Iterator<Cell> cellIterator = row.cellIterator();
while (cellIterator.hasNext()) {
Cell cell = cellIterator.next();
if (cell.getCellType() != CellType.BLANK) {
return true; // 빈 셀이 아닌 경우에만 true를 반환합니다.
}
}
return false; // 모든 셀이 비어 있으면 false를 반환합니다.
}
private T mapRowToDto(Row row, Class<T> clazz, List<String> excelHeaderList) {
T dataDto;
try {
dataDto = clazz.getDeclaredConstructor().newInstance();
Iterator<Cell> cellIterator = row.cellIterator();
while (cellIterator.hasNext()) {
Cell cell = cellIterator.next();
String excelHeaderName = excelHeaderList.get(cell.getColumnIndex());
//각 필드를 순회하며 커스텀 어노테이션인 ExcelHeader값에 맞게 값을 넣어줌
Field[] dtoFields = clazz.getDeclaredFields();
for (Field field : dtoFields) {
if (field.isAnnotationPresent(ExcelColumn.class)) {
ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
if (Objects.requireNonNull(annotation).headerName().equals(excelHeaderName)) {
field.setAccessible(true);
setFieldValue(field, dataDto, cell);
break;
}
}
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return dataDto;
}
private void setFieldValue(Field field, T dataDto, Cell cell) throws IllegalAccessException {
Class<?> fieldType = field.getType();
field.setAccessible(true);
if (fieldType == int.class || fieldType == Integer.class) {
field.set(dataDto, (int)cell.getNumericCellValue());
} else if (fieldType == long.class || fieldType == Long.class) {
field.set(dataDto, (long)cell.getNumericCellValue());
} else if (fieldType == double.class || fieldType == Double.class) {
field.set(dataDto, cell.getNumericCellValue());
} else if (fieldType == boolean.class || fieldType == Boolean.class) {
field.set(dataDto, cell.getBooleanCellValue());
} else {
// if (fieldType == String.class) {
DataFormatter formatter = new DataFormatter();
field.set(dataDto, formatter.formatCellValue(cell));
}
// 다른 타입에 따른 맵핑을 추가할 수 있습니다.
}
}

@ -0,0 +1,29 @@
package egovframework.util.excel;
import lombok.Getter;
import java.util.List;
import java.util.Map;
@Getter
public class ExcelMetadata {
private final Map<String, String> excelHeaderNames;
private final Map<String, Integer> excelHeaderWidths;
private final List<String> dataFieldNames;
private final String sheetName;
public ExcelMetadata(Map<String, String> excelHeaderNames, Map<String, Integer> excelHeaderWidths, List<String> dataFieldNames, String sheetName) {
this.excelHeaderNames = excelHeaderNames;
this.excelHeaderWidths = excelHeaderWidths;
this.dataFieldNames = dataFieldNames;
this.sheetName = sheetName;
}
public String getHeaderName(String fieldName) {
return excelHeaderNames.getOrDefault(fieldName, "");
}
public int getHeaderWidth(String fieldName) {
return excelHeaderWidths.getOrDefault(fieldName, 0);
}
}

@ -0,0 +1,146 @@
package egovframework.util.excel;
import java.lang.reflect.Field;
import java.util.*;
import static egovframework.util.excel.SuperClassReflectionUtils.getAllFields;
import static org.springframework.core.annotation.AnnotationUtils.getAnnotation;
/**
* ()
*
* <p> VO {@link ExcelColumn} {@link ExcelSheet}
* .</p>
*
* <p><b> :</b></p>
* <ul>
* <li> VO </li>
* <li>ExcelColumn </li>
* <li>ExcelSheet </li>
* <li> </li>
* </ul>
*
* <p><b> :</b></p>
* <pre>{@code
* ExcelMetadata metadata = ExcelMetadataFactory.getInstance()
* .createMetadata(UserVO.class);
*
* String sheetName = metadata.getSheetName();
* List<String> fieldNames = metadata.getDataFieldNames();
* }</pre>
*
* @see ExcelMetadata
* @see ExcelColumn
* @see ExcelSheet
* @author eGovFrame
*/
public class ExcelMetadataFactory {
// ==================== 싱글톤 구현 ====================
/**
* private ( )
*/
private ExcelMetadataFactory() {
}
/**
*
* <p>Bill Pugh Singleton (Thread-safe, Lazy Loading)</p>
*/
private static class SingletonHolder {
private static final ExcelMetadataFactory INSTANCE = new ExcelMetadataFactory();
}
/**
* .
*
* @return ExcelMetadataFactory
*/
public static ExcelMetadataFactory getInstance() {
return SingletonHolder.INSTANCE;
}
// ==================== 메타데이터 생성 ====================
/**
* VO .
*
* <p> VO {@link ExcelColumn}
* , .</p>
*
* <p><b> :</b></p>
* <ol>
* <li> </li>
* <li>ExcelColumn </li>
* <li>, , </li>
* <li>ExcelSheet </li>
* <li>ExcelMetadata </li>
* </ol>
*
* @param clazz VO
* @return
* @throws RuntimeException ExcelColumn
*/
public ExcelMetadata createMetadata(Class<?> clazz) {
// 헤더명, 컬럼 너비, 필드명을 저장할 컬렉션 (순서 유지)
Map<String, String> headerNamesMap = new LinkedHashMap<>();
Map<String, Integer> headerWidthsMap = new LinkedHashMap<>();
List<String> dataFieldNamesList = new ArrayList<>();
// 모든 필드를 탐색하여 ExcelColumn 어노테이션 정보 수집
for (Field field : getAllFields(clazz)) {
if (field.isAnnotationPresent(ExcelColumn.class)) {
ExcelColumn columnAnnotation = field.getAnnotation(ExcelColumn.class);
String fieldName = field.getName();
// 헤더명, 컬럼 너비, 필드명 저장
headerNamesMap.put(fieldName, Objects.requireNonNull(columnAnnotation).headerName());
headerWidthsMap.put(fieldName, columnAnnotation.headerWidth());
dataFieldNamesList.add(fieldName);
}
}
// ExcelColumn 어노테이션이 하나도 없으면 예외 발생
validateHasExcelColumns(clazz, headerNamesMap);
// 메타데이터 객체 생성 및 반환
return new ExcelMetadata(
headerNamesMap,
headerWidthsMap,
dataFieldNamesList,
extractSheetName(clazz)
);
}
// ==================== private 헬퍼 메서드 ====================
/**
* ExcelColumn .
*
* @param clazz
* @param headerNamesMap
* @throws RuntimeException ExcelColumn
*/
private void validateHasExcelColumns(Class<?> clazz, Map<String, String> headerNamesMap) {
if (headerNamesMap.isEmpty()) {
throw new RuntimeException(
String.format("클래스 %s에 @ExcelColumn 어노테이션이 하나도 없습니다.", clazz.getName())
);
}
}
/**
* .
*
* <p>{@link ExcelSheet} ,
* "Sheet1" .</p>
*
* @param clazz
* @return (ExcelSheet name "Sheet1")
*/
private String extractSheetName(Class<?> clazz) {
ExcelSheet annotation = getAnnotation(clazz, ExcelSheet.class);
return annotation != null ? annotation.name() : "Sheet1";
}
}

@ -0,0 +1,12 @@
package egovframework.util.excel;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelSheet {
String name() default "";
}

@ -0,0 +1,169 @@
package egovframework.util.excel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
/**
* DTO
*
* <p> .
* , VO , () .</p>
*
* <p><b> :</b></p>
* <ul>
* <li>(Immutable) - final</li>
* <li> - of() </li>
* <li> - </li>
* </ul>
*
* <p><b> 1: </b></p>
* <pre>{@code
* // VO 리스트 조회
* List<UserVO> userList = userService.selectUserList();
*
* // ExcelSheetData 생성 (제목 없음)
* ExcelSheetData sheetData = ExcelSheetData.of(userList, UserVO.class);
*
* // 엑셀 파일 생성
* new SxssfExcelFile(sheetData, request, response, "사용자목록.xlsx");
* }</pre>
*
* <p><b> 2: </b></p>
* <pre>{@code
* // VO 리스트 조회
* List<UserVO> userList = userService.selectUserList();
*
* // ExcelSheetData 생성 (제목 포함)
* ExcelSheetData sheetData = ExcelSheetData.of(
* userList,
* UserVO.class,
* "2024년 1월 사용자 목록" // 제목 행에 표시될 텍스트
* );
*
* // 엑셀 파일 생성 (첫 행에 제목이 표시됨)
* new SxssfExcelFile(sheetData, request, response, "사용자목록.xlsx");
* }</pre>
*
* <p><b> 3: </b></p>
* <pre>{@code
* // 시트 1: 사용자 목록
* List<UserVO> userList = userService.selectUserList();
* ExcelSheetData userSheet = ExcelSheetData.of(userList, UserVO.class, "사용자 목록");
*
* // 시트 2: 부서 목록
* List<DeptVO> deptList = deptService.selectDeptList();
* ExcelSheetData deptSheet = ExcelSheetData.of(deptList, DeptVO.class, "부서 목록");
*
* // 다중 시트 그룹 생성
* ExcelSheetDataGroup dataGroup = ExcelSheetDataGroup.of(
* List.of(userSheet, deptSheet)
* );
*
* // 다중 시트 엑셀 파일 생성
* new SxssfMultiSheetExcelFile(dataGroup, response);
* }</pre>
*
* @see SxssfExcelFile
* @see SxssfMultiSheetExcelFile
* @see ExcelSheetDataGroup
* @author eGovFrame
*/
@Getter
@AllArgsConstructor
public class ExcelSheetData {
// ==================== 필드 ====================
/**
*
*
* <p> {@link ExcelColumn} VO .
* (row) .</p>
*/
private final List<?> dataList;
/**
* (VO )
*
* <p> :
* <ul>
* <li>{@link ExcelColumn} - </li>
* <li>{@link ExcelSheet} - ()</li>
* </ul>
* </p>
*
* <p><b>:</b></p>
* <pre>{@code
* @ExcelSheet(name = "사용자")
* public class UserVO {
* @ExcelColumn(headerName = "이름", headerWidth = 20)
* private String userName;
*
* @ExcelColumn(headerName = "이메일")
* private String email;
* }
* }</pre>
*/
private final Class<?> type;
/**
* ()
*
* <p> null ,
* . ,
* 22 .</p>
*
* <p>null ,
* .</p>
*/
private final String title;
// ==================== 팩토리 메서드 ====================
/**
* ExcelSheetData .
*
* <p> .</p>
*
* <p><b> :</b></p>
* <pre>
* Row 0: [1] [2] [3] ...
* Row 1: [] [] [] ...
* Row 2: [] [] [] ...
* ...
* </pre>
*
* @param dataList (null )
* @param type (VO , null )
* @return ExcelSheetData
*/
public static ExcelSheetData of(List<?> dataList, Class<?> type) {
return new ExcelSheetData(dataList, type, null);
}
/**
* ExcelSheetData .
*
* <p> , , .
* .</p>
*
* <p><b> :</b></p>
* <pre>
* Row 0: [ - ]
* Row 1: [1] [2] [3] ...
* Row 2: [] [] [] ...
* Row 3: [] [] [] ...
* ...
* </pre>
*
* @param dataList (null )
* @param type (VO , null )
* @param title (null )
* @return ExcelSheetData
*/
public static ExcelSheetData of(List<?> dataList, Class<?> type, String title) {
return new ExcelSheetData(dataList, type, title);
}
}

@ -0,0 +1,35 @@
package egovframework.util.excel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ExcelSheetDataGroup { // (2)
private final List<ExcelSheetData> dataList;
private ExcelSheetDataGroup(List<ExcelSheetData> data) {
validateEmpty(data);
this.dataList = new ArrayList<>(data);
}
public List<ExcelSheetData> getExcelSheetData() {
return Collections.unmodifiableList(dataList);
}
public static ExcelSheetDataGroup of(ExcelSheetData... data) {
List<ExcelSheetData> list;
if (data == null) {
list = Collections.emptyList();
} else {
list = Arrays.asList(data);
}
return new ExcelSheetDataGroup(list);
}
private void validateEmpty(List<ExcelSheetData> data) {
if (data.isEmpty()) {
throw new IllegalArgumentException("lists must not be empty");
}
}
}

@ -0,0 +1,52 @@
package egovframework.util.excel;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class OutputExcelFile {
public void serveFile(HttpServletRequest request, HttpServletResponse response, Path filePath) throws IOException {
String fileName = filePath.getFileName().toString();
try {
String browser = request.getHeader("User-Agent");
String encodedFileName;
if (browser.contains("MSIE") || browser.contains("Trident")) {
// IE 브라우저 대응
encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
} else {
// IE 외 브라우저 대응 (크롬, 파이어폭스 등)
encodedFileName = new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
}
// Content-Disposition 설정
response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e.getMessage());
}
try (InputStream is = Files.newInputStream(filePath);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.flush();
}
}
public void generateExcelFile(ExcelSheetData data, Path filePath) throws IOException {
try (OutputStream os = Files.newOutputStream(filePath)) {
new SxssfExcelFile(data, os, null); // 엑셀 파일 생성 및 저장
}
}
}

@ -0,0 +1,66 @@
package egovframework.util.excel;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public final class SuperClassReflectionUtils {
private SuperClassReflectionUtils() {
}
public static List<Field> getAllFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
for (Class<?> clazzInClasses : getAllClassesIncludingSuperClasses(clazz, true)) {
fields.addAll(Arrays.asList(clazzInClasses.getDeclaredFields()));
}
return fields;
}
public static List<Field> getAllFieldsWithExcelColumn(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
for (Field field : getAllFields(clazz)) {
if (field.isAnnotationPresent(ExcelColumn.class)) {
fields.add(field);
}
}
return fields;
}
public static Annotation getAnnotation(Class<?> clazz, Class<? extends Annotation> targetAnnotation) {
for (Class<?> clazzInClasses : getAllClassesIncludingSuperClasses(clazz, false)) {
if (clazzInClasses.isAnnotationPresent(targetAnnotation)) {
return clazzInClasses.getAnnotation(targetAnnotation);
}
}
return null;
}
public static Field getField(Class<?> clazz, String name) throws Exception {
for (Class<?> clazzInClasses : getAllClassesIncludingSuperClasses(clazz, false)) {
for (Field field : clazzInClasses.getDeclaredFields()) {
if (field.getName().equals(name)) {
return clazzInClasses.getDeclaredField(name);
}
}
}
throw new NoSuchFieldException();
}
private static List<Class<?>> getAllClassesIncludingSuperClasses(Class<?> clazz, boolean fromSuper) {
List<Class<?>> classes = new ArrayList<>();
while (clazz != null) {
classes.add(clazz);
clazz = clazz.getSuperclass();
}
if (fromSuper) {
Collections.reverse(classes);
}
return classes;
}
}

@ -0,0 +1,188 @@
package egovframework.util.excel;
import org.checkerframework.checker.nullness.qual.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
*
*
* <p> {@link BaseSxssfExcelFile} .
* HTTP , OutputStream .</p>
*
* <p><b> 1: HTTP </b></p>
* <pre>{@code
* @GetMapping("/download.xlsx")
* public void downloadExcel(HttpServletRequest request, HttpServletResponse response) {
* List<UserVO> dataList = userService.selectUserList();
* ExcelSheetData sheetData = ExcelSheetData.of(dataList, UserVO.class, "사용자 목록");
* new SxssfExcelFile(sheetData, request, response, "사용자목록.xlsx");
* }
* }</pre>
*
* <p><b> 2: </b></p>
* <pre>{@code
* @GetMapping("/download-encrypted.xlsx")
* public void downloadEncryptedExcel(HttpServletRequest request, HttpServletResponse response) {
* List<UserVO> dataList = userService.selectUserList();
* ExcelSheetData sheetData = ExcelSheetData.of(dataList, UserVO.class, "사용자 목록");
* new SxssfExcelFile(sheetData, request, response, "사용자목록.xlsx", "password123");
* }
* }</pre>
*
* <p><b> 3: OutputStream </b></p>
* <pre>{@code
* try (FileOutputStream fos = new FileOutputStream("output.xlsx")) {
* List<UserVO> dataList = userService.selectUserList();
* ExcelSheetData sheetData = ExcelSheetData.of(dataList, UserVO.class);
* new SxssfExcelFile(sheetData, fos, null);
* }
* }</pre>
*
* <p><b>VO :</b></p>
* <pre>{@code
* @ExcelSheet(name = "사용자")
* public class UserVO {
* @ExcelColumn(headerName = "이름", headerWidth = 20)
* private String userName;
*
* @ExcelColumn(headerName = "이메일", headerWidth = 30)
* private String email;
*
* @ExcelColumn(headerName = "전화번호")
* private String phone;
* }
* }</pre>
*
* @see BaseSxssfExcelFile
* @see ExcelSheetData
* @see ExcelColumn
* @author eGovFrame
*/
public class SxssfExcelFile extends BaseSxssfExcelFile {
// ==================== 생성자 (HTTP 응답 다운로드) ====================
/**
* HTTP ( ).
*
* <p> HTTP .
* .</p>
*
* @param data ( , , )
* @param request HTTP ( )
* @param response HTTP
* @param fileName ( , : "사용자목록.xlsx")
* @throws RuntimeException
*/
public SxssfExcelFile(ExcelSheetData data, HttpServletRequest request, HttpServletResponse response,
String fileName) {
this(data, request, response, fileName, null);
}
/**
* HTTP ( ).
*
* <p> HTTP .
* .</p>
*
* @param data ( , , )
* @param request HTTP ( )
* @param response HTTP
* @param fileName ( , : "사용자목록.xlsx")
* @param password (null )
* @throws RuntimeException
*/
public SxssfExcelFile(ExcelSheetData data, HttpServletRequest request, HttpServletResponse response,
String fileName, @Nullable String password) {
try {
setDownloadHeaders(request, response, fileName);
ExcelMetadata metadata = ExcelMetadataFactory.getInstance().createMetadata(data.getType());
renderSheetContent(data, metadata);
writeWithEncryption(response.getOutputStream(), password);
} catch (IOException e) {
throw new RuntimeException("HTTP 응답으로 엑셀 파일 출력 중 오류가 발생했습니다.", e);
}
}
// ==================== 생성자 (OutputStream 출력) ====================
/**
* OutputStream .
*
* <p> OutputStream .
* .</p>
*
* @param data ( , , )
* @param outputStream
* @param password (null )
* @throws RuntimeException
*/
public SxssfExcelFile(ExcelSheetData data, OutputStream outputStream, @Nullable String password) {
ExcelMetadata metadata = ExcelMetadataFactory.getInstance().createMetadata(data.getType());
renderSheetContent(data, metadata);
writeWithEncryption(outputStream, password);
}
// ==================== private 헬퍼 메서드 ====================
/**
* HTTP .
*
* <p> Content-Disposition :
* <ul>
* <li>IE (MSIE/Trident): UTF-8 URL (+ )</li>
* <li> : UTF-8 ISO-8859-1 </li>
* </ul>
* </p>
*
* @param request HTTP (User-Agent )
* @param response HTTP
* @param fileName
* @throws RuntimeException
*/
private void setDownloadHeaders(HttpServletRequest request, HttpServletResponse response, String fileName) {
try {
String browser = request.getHeader("User-Agent");
String encodedFileName = encodeFileName(fileName, browser);
response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("파일명 인코딩 중 오류가 발생했습니다.", e);
}
}
/**
* .
*
* @param fileName
* @param userAgent User-Agent
* @return
* @throws UnsupportedEncodingException
*/
private String encodeFileName(String fileName, String userAgent) throws UnsupportedEncodingException {
if (isInternetExplorer(userAgent)) {
// IE: UTF-8 URL 인코딩 (+ -> 공백 처리)
return URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
} else {
// Chrome, Firefox 등: UTF-8 바이트를 ISO-8859-1로 변환
return new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
}
}
/**
* User-Agent Internet Explorer .
*
* @param userAgent User-Agent
* @return IE true, false
*/
private boolean isInternetExplorer(String userAgent) {
return userAgent != null && (userAgent.contains("MSIE") || userAgent.contains("Trident"));
}
}

@ -0,0 +1,129 @@
package egovframework.util.excel;
import org.checkerframework.checker.nullness.qual.Nullable;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
*
*
* <p> {@link BaseSxssfExcelFile} .
* .</p>
*
* <p><b> 1: </b></p>
* <pre>{@code
* @GetMapping("/download-multi.xlsx")
* public void downloadMultiSheetExcel(HttpServletResponse response) throws IOException {
* // 시트 1: 사용자 목록
* List<UserVO> userList = userService.selectUserList();
* ExcelSheetData userSheet = ExcelSheetData.of(userList, UserVO.class, "사용자 목록");
*
* // 시트 2: 부서 목록
* List<DeptVO> deptList = deptService.selectDeptList();
* ExcelSheetData deptSheet = ExcelSheetData.of(deptList, DeptVO.class, "부서 목록");
*
* // 다중 시트 그룹 생성
* ExcelSheetDataGroup dataGroup = ExcelSheetDataGroup.of(
* List.of(userSheet, deptSheet)
* );
*
* // 파일 다운로드
* response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
* response.setHeader("Content-Disposition", "attachment; filename=\"통합리포트.xlsx\"");
* new SxssfMultiSheetExcelFile(dataGroup, response);
* }
* }</pre>
*
* <p><b> 2: </b></p>
* <pre>{@code
* @GetMapping("/download-multi-encrypted.xlsx")
* public void downloadEncryptedMultiSheetExcel(HttpServletResponse response) throws IOException {
* ExcelSheetDataGroup dataGroup = createMultiSheetData();
*
* response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
* response.setHeader("Content-Disposition", "attachment; filename=\"통합리포트.xlsx\"");
* new SxssfMultiSheetExcelFile(dataGroup, response, "password123");
* }
* }</pre>
*
* <p><b>:</b></p>
* <ul>
* <li> VO .</li>
* <li> VO {@link ExcelSheet} .</li>
* <li> .</li>
* </ul>
*
* @see BaseSxssfExcelFile
* @see ExcelSheetDataGroup
* @see ExcelSheetData
* @author eGovFrame
*/
public class SxssfMultiSheetExcelFile extends BaseSxssfExcelFile {
// ==================== 생성자 ====================
/**
* HTTP ( ).
*
* <p> HTTP .</p>
*
* @param dataGroup
* @param response HTTP
* @throws IOException
*/
public SxssfMultiSheetExcelFile(ExcelSheetDataGroup dataGroup, HttpServletResponse response)
throws IOException {
this(dataGroup, response, null);
}
/**
* HTTP ( ).
*
* <p> HTTP .</p>
*
* @param dataGroup
* @param response HTTP
* @param password (null )
* @throws IOException
*/
public SxssfMultiSheetExcelFile(ExcelSheetDataGroup dataGroup, HttpServletResponse response,
@Nullable String password) throws IOException {
renderAllSheets(dataGroup);
writeWithEncryption(response.getOutputStream(), password);
}
// ==================== private 헬퍼 메서드 ====================
/**
* .
*
* <p> .
* , , .</p>
*
* @param dataGroup
*/
private void renderAllSheets(ExcelSheetDataGroup dataGroup) {
for (ExcelSheetData data : dataGroup.getExcelSheetData()) {
// 현재 시트 참조 초기화 (새 시트 생성 준비)
resetSheetContext();
// 메타데이터 생성
ExcelMetadata metadata = ExcelMetadataFactory.getInstance().createMetadata(data.getType());
// 시트 내용 렌더링 (제목, 헤더, 데이터)
renderSheetContent(data, metadata);
}
}
/**
* .
*
* <p> .
* renderSheetContent .</p>
*/
private void resetSheetContext() {
sheet = null;
titleRowOffset = 0;
}
}

@ -0,0 +1,12 @@
package go.kr.project;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(VipsApplication.class);
}
}

@ -0,0 +1,25 @@
package go.kr.project;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.ComponentScan;
@Slf4j
@ServletComponentScan
@SpringBootApplication
@ComponentScan(basePackages = {"go.kr.project", "egovframework"})
public class VipsApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(VipsApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(VipsApplication.class, args);
}
}

@ -0,0 +1,40 @@
package go.kr.project.common.controller;
import go.kr.project.common.service.AddressService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
*
*/
@RestController
@RequestMapping("/common/address")
@RequiredArgsConstructor
@Tag(name = "주소 검색", description = "외부 주소 API 연동 관련")
public class AddressController {
private final AddressService addressService;
/**
* API .
* @param keyword
* @param currentPage
* @param countPerPage
* @return API (JSON )
*/
@GetMapping(value = "/search.ajax", produces = "application/json; charset=UTF-8")
@Operation(summary = "주소 검색", description = "외부 도로명주소 API를 호출하여 결과를 반환합니다.")
public String searchAddress(
@Parameter(description = "검색어") @RequestParam("keyword") String keyword,
@Parameter(description = "현재 페이지") @RequestParam("currentPage") int currentPage,
@Parameter(description = "페이지당 결과 수") @RequestParam("countPerPage") int countPerPage) {
return addressService.searchAddress(keyword, currentPage, countPerPage);
}
}

@ -0,0 +1,80 @@
package go.kr.project.common.controller;
import egovframework.util.ApiResponseUtil;
import go.kr.project.common.model.CmmnCodeSearchVO;
import go.kr.project.common.service.CommonCodeService;
import go.kr.project.system.code.model.CodeDetailVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
/**
* packageName : go.kr.project.common.controller
* fileName : CommonCodeController
* author :
* date : 2025-05-10
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-05-10
*/
@RequestMapping("/common/code")
@Controller
@RequiredArgsConstructor
@Slf4j
@Tag(name = "공통 코드", description = "공통 코드 관련 API")
public class CommonCodeController {
private final CommonCodeService commonCodeService;
/**
* AJAX
*
* @param cdGroupId ID
* @return ResponseEntity
* @throws Exception
*/
@Operation(summary = "코드 상세 목록 조회", description = "특정 코드 그룹에 속한 코드 상세 목록을 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "코드 상세 목록 조회 성공"),
@ApiResponse(responseCode = "400", description = "코드 상세 목록 조회 실패"),
@ApiResponse(description = "오류로 인한 실패")
})
@GetMapping("/detail/listByGroupId.ajax")
public ResponseEntity<?> getCodeDetailListByGroupIdAjax(@RequestParam String cdGroupId) {
List<CodeDetailVO> codeDetailList = commonCodeService.selectCodeDetailListByGroupId(cdGroupId);
return ApiResponseUtil.success(codeDetailList);
}
/**
* AJAX
*
* @param searchVO VO
* @return ResponseEntity
* @throws Exception
*/
@Operation(summary = "코드 상세 목록 조회", description = "검색 조건을 담은 VO 객체를 통해 코드 상세 목록을 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "코드 상세 목록 조회 성공"),
@ApiResponse(responseCode = "400", description = "코드 상세 목록 조회 실패"),
@ApiResponse(description = "오류로 인한 실패")
})
@GetMapping("/detail/list.ajax")
public ResponseEntity<?> getCodeDetailListAjax(@ModelAttribute CmmnCodeSearchVO searchVO) {
List<CodeDetailVO> codeDetailList = commonCodeService.selectCodeDetailList(searchVO);
return ApiResponseUtil.success(codeDetailList);
}
}

@ -0,0 +1,54 @@
package go.kr.project.common.controller;
import go.kr.project.common.service.CommonHeaderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
@RequestMapping("/common/header")
@Controller
@RequiredArgsConstructor
@Slf4j
@Tag(name = "공통 헤더", description = "공통 헤더 관련 API")
public class CommonHeaderController {
private final CommonHeaderService commonHeaderService;
/**
* API
*
* @param state (collapsed, expanded, )
* @param session HTTP
* @return
*/
@PostMapping("/sidebar/state.ajax")
@Operation(summary = "사이드바 상태 저장", description = "사이드바 상태를 세션에 저장합니다.")
public ResponseEntity<Map<String, Object>> saveSidebarState(
@Parameter(description = "사이드바 상태 (sidebar-collapse, 또는 빈 문자열)")
@RequestParam(value = "state", required = false, defaultValue = "") String state,
HttpSession session) {
// 세션에 사이드바 상태 저장
session.setAttribute("sidebarState", state);
log.debug("사이드바 상태 저장: {}", state);
// 응답 데이터 구성
Map<String, Object> response = new HashMap<>();
response.put("result", true);
response.put("message", "사이드바 상태가 저장되었습니다.");
return ResponseEntity.ok(response);
}
}

@ -0,0 +1,75 @@
package go.kr.project.common.controller;
import egovframework.configProperties.LoginProperties;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
/**
* packageName : egovframework.config
* fileName : ConfigController
* author :
* date : 25. 5. 19.
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 25. 5. 19.
*/
@RequestMapping("/common/config")
@Controller
@RequiredArgsConstructor
@Slf4j
@Tag(name = "시스템 설정", description = "시스템 설정 관련 API")
public class ConfigController {
private final LoginProperties loginProperties;
/**
* URL
*
* @return
*/
@Operation(summary = "로그인 URL 설정값 자바스크립트 반환", description = "로그인 URL 설정값을 자바스크립트 코드로 반환합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "자바스크립트 반환 성공")
})
@GetMapping(value = "/login-url.do", produces = "application/javascript")
public ResponseEntity<String> getLoginUrlJs() {
String js = "var loginUrl = '" + loginProperties.getUrl() + "';";
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/javascript"))
.body(js);
}
/**
*
* .do, .ajax URL
*
* @param request HttpServletRequest
* @return
*/
@Operation(summary = "컨택스트 패스 설정값 자바스크립트 반환", description = "컨택스트 패스 설정값을 자바스크립트 코드로 반환합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "자바스크립트 반환 성공")
})
@GetMapping(value = "/context-path.do", produces = "application/javascript")
public ResponseEntity<String> getContextPathJs(HttpServletRequest request) {
// 컨택스트 패스를 가져와서 JavaScript 변수로 설정
String contextPath = request.getContextPath();
String js = "var contextPath = '" + contextPath + "';";
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/javascript"))
.body(js);
}
}

@ -0,0 +1,121 @@
package go.kr.project.common.controller;
import egovframework.util.ApiResponseUtil;
import egovframework.util.SessionUtil;
import go.kr.project.common.model.HtmlEditorFileVO;
import go.kr.project.common.service.HtmlEditorService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Objects;
/**
* packageName : go.kr.project.common.controller
* fileName : HtmlEditorController
* author :
* date : 2025-05-23
* description : HTML
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-05-23
*/
@RequestMapping("/common/htmlEditor")
@Controller
@RequiredArgsConstructor
@Slf4j
@Tag(name = "HTML 에디터", description = "HTML 에디터 관련 API")
public class HtmlEditorController {
private final HtmlEditorService htmlEditorService;
/**
* HTML
*
* @param image
* @param moduleId ID ()
* @param request HTTP
* @return URL ResponseEntity
*/
@Operation(summary = "에디터 이미지 업로드", description = "HTML 에디터에서 이미지를 업로드합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "이미지 업로드 성공"),
@ApiResponse(responseCode = "400", description = "이미지 업로드 실패"),
@ApiResponse(description = "오류로 인한 실패")
})
@PostMapping("/uploadImage.ajax")
public ResponseEntity<?> uploadImage(
@RequestParam("image") MultipartFile image,
@RequestParam(value = "moduleId", required = false) String moduleId,
HttpServletRequest request) {
try {
// 이미지 파일 검증
if (image.isEmpty()) {
return ApiResponseUtil.error("이미지 파일이 없습니다.");
}
// 이미지 파일 확장자 검증
String originalFilename = image.getOriginalFilename();
String fileExt = Objects.requireNonNull(originalFilename).substring(originalFilename.lastIndexOf(".") + 1).toLowerCase();
if (!fileExt.matches("jpg|jpeg|png|gif")) {
return ApiResponseUtil.error("허용되지 않은 이미지 형식입니다. (jpg, jpeg, png, gif만 가능)");
}
// 파일 업로드 처리
HtmlEditorFileVO uploadedFile = htmlEditorService.uploadHtmlEditorFile(
image, moduleId, "image", SessionUtil.getUserId());
// 이미지 URL 생성
String imageUrl = request.getContextPath() + "/common/htmlEditor/download.do?fileId=" + uploadedFile.getFileId();
// 응답 데이터 생성
HashMap<String, Object> data = new HashMap<>();
data.put("url", imageUrl);
data.put("alt", uploadedFile.getOriginalFileNm());
data.put("fileId", uploadedFile.getFileId());
return ApiResponseUtil.success(data);
} catch (Exception e) {
log.error("이미지 업로드 중 오류 발생", e);
return ApiResponseUtil.error("이미지 업로드 중 오류가 발생했습니다: " + e.getMessage());
}
}
/**
* .
*
* @param fileId ID
* @param request HTTP
* @param response HTTP
*/
@Operation(summary = "파일 다운로드", description = "HTML 에디터에서 사용된 파일을 다운로드합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "파일 다운로드 성공"),
@ApiResponse(responseCode = "400", description = "파일 다운로드 실패"),
@ApiResponse(description = "오류로 인한 실패")
})
@GetMapping("/download.do")
public void downloadFile(
@RequestParam String fileId,
HttpServletRequest request,
HttpServletResponse response) {
// 파일 다운로드 처리 - 서비스 계층으로 위임
htmlEditorService.downloadHtmlEditorFile(fileId, request, response);
}
}

@ -0,0 +1,40 @@
package go.kr.project.common.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* packageName : go.kr.project.common.controller
* fileName : SearchAddressController
* author :
* date : 25. 5. 13.
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 25. 5. 13.
*/
@RequestMapping("/common/address")
@Controller
@RequiredArgsConstructor
@Slf4j
@Tag(name = "주소 검색", description = "주소 검색 관련 API")
public class SearchAddressController {
@Operation(summary = "주소 검색 팝업", description = "주소 검색 관련 요청을 처리 페이지를 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "주소 검색 팝업 조회 성공")
})
@RequestMapping(value = "/search.do", method={RequestMethod.GET, RequestMethod.POST})
public String addressSearchPage() {
return "common/address/searchAddress";
}
}

@ -0,0 +1,59 @@
package go.kr.project.common.error;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* packageName : go.kr.project.common.error
* fileName : ErrorController
* author :
* date : 25. 6. 5.
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 25. 6. 5.
*/
@Controller
@Tag(name = "에러 페이지", description = "에러 페이지 관련 API")
public class ErrorController {
@Operation(summary = "404 에러 페이지", description = "404 에러 페이지를 반환합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "404 에러 페이지 반환 성공")
})
@GetMapping("/error/404")
public String handleError404() {
return "error/404";
}
@Operation(summary = "500 에러 페이지", description = "500 에러 페이지를 반환합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "500 에러 페이지 반환 성공")
})
@GetMapping("/error/500")
public String handleError500() {
return "error/500"; //
}
@Operation(summary = "일반 에러 페이지", description = "일반 에러 페이지를 반환합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "일반 에러 페이지 반환 성공")
})
@GetMapping("/error/error")
public String handleError() {
return "error/error";
}
@Operation(summary = "eGov 에러 페이지", description = "eGov 에러 페이지를 반환합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "eGov 에러 페이지 반환 성공")
})
@GetMapping("/error/egovError")
public String handleEgovError() {
return "error/egovError";
}
}

@ -0,0 +1,39 @@
package go.kr.project.common.mapper;
import go.kr.project.common.model.CmmnCodeSearchVO;
import go.kr.project.system.code.model.CodeDetailVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* packageName : go.kr.project.common.mapper
* fileName : CommonCodeMapper
* author :
* date : 2025-05-10
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-05-10
*/
@Mapper
public interface CommonCodeMapper {
/**
* .
*
* @param cdGroupId ID
* @return
*/
List<CodeDetailVO> selectCodeDetailListByGroupId(String cdGroupId);
/**
* .
*
* @param searchVO VO
* @return
*/
List<CodeDetailVO> selectCodeDetailList(CmmnCodeSearchVO searchVO);
}

@ -0,0 +1,19 @@
package go.kr.project.common.mapper;
import org.apache.ibatis.annotations.Mapper;
/**
* packageName : go.kr.project.common.mapper
* fileName : CommonCodeMapper
* author :
* date : 2025-05-10
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-05-10
*/
@Mapper
public interface CommonHeaderMapper {
}

@ -0,0 +1,92 @@
package go.kr.project.common.mapper;
import go.kr.project.common.model.HtmlEditorFileVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* packageName : go.kr.project.common.mapper
* fileName : HtmlEditorMapper
* author :
* date : 2025-05-23
* description : HTML Mapper
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-05-23
*/
@Mapper
public interface HtmlEditorMapper {
/**
* .
*
* @param fileId ID
* @return
*/
HtmlEditorFileVO selectHtmlEditorFile(String fileId);
/**
* .
*
* @param moduleId ID
* @return
*/
List<HtmlEditorFileVO> selectHtmlEditorFileListByModuleId(String moduleId);
/**
* .
*
* @param vo VO
* @return
*/
List<HtmlEditorFileVO> selectHtmlEditorFileList(HtmlEditorFileVO vo);
/**
* .
*
* @param vo VO
* @return
*/
int selectHtmlEditorFileListTotalCount(HtmlEditorFileVO vo);
/**
* ID .
*
* @return HEDF00000001 ID
*/
String generateFileId();
/**
* .
*
* @param vo VO
* @return
*/
int insertHtmlEditorFile(HtmlEditorFileVO vo);
/**
* .
*
* @param vo VO
* @return
*/
int updateHtmlEditorFile(HtmlEditorFileVO vo);
/**
* .
*
* @param fileId ID
* @return
*/
int deleteHtmlEditorFile(String fileId);
/**
* .
*
* @param moduleId ID
* @return
*/
int deleteHtmlEditorFileByModuleId(String moduleId);
}

@ -0,0 +1,53 @@
package go.kr.project.common.model;
import lombok.*;
/**
* packageName : go.kr.project.system.menu.model
* fileName : MenuVO
* author :
* date : 2023-06-01
* description : VO
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2023-06-01
*/
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CmmnCodeSearchVO {
private String searchCdGroupId;
private String searchCdId;
private String searchCdNm;
private String searchAttribute1;
private String searchAttribute2;
private String searchAttribute3;
private String searchAttribute4;
private String searchAttribute5;
/** 사용유무 */
private String searchUseYn;
/** 등록일시 시작일 */
private String searchRegDttmStart;
/** 등록일시 종료일 */
private String searchRegDttmEnd;
/** 등록자 */
private String searchRgtr;
/** 수정일시 시작일 */
private String searchMdfcnDttmStart;
/** 수정일시 종료일 */
private String searchMdfcnDttmEnd;
/** 수정자 */
private String searchMdfr;
/** 정렬컬럼 */
private String sortColumn;
/** 정렬구분 */
private Boolean sortAscending;
}

@ -0,0 +1,52 @@
package go.kr.project.common.model;
import lombok.Data;
import java.io.Serializable;
import java.util.Map;
/**
* packageName : go.kr.project.common
* fileName : DefaultVO
* author :
* date : 25. 5. 8.
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 25. 5. 8.
*/
@Data
public class DefaultVO implements Serializable {
/** 저장구분 : I(insert), U(update), D(delete) */
private String crudType;
/** checkbox */
private Integer chk;
/* 페이징여부 */
private String pagingYn;
/** 검색조건 */
private String searchCondition = "";
/** 검색Keyword */
private String searchKeyword = "";
/** TotalCount */
private Integer totalCount;
/** 사용유무 */
private String searchUseYn;
/** 정렬컬럼 */
private String sortColumn;
/** 정렬구분 */
private Boolean sortAscending;
/** TUI Grid 속성 (row class 등) */
private Map<String, Object> _attributes;
}

@ -0,0 +1,43 @@
package go.kr.project.common.model;
import lombok.Data;
/**
* VO
*/
@Data
public class FileVO {
/** 파일 ID */
private String fileId;
/** 참조 ID (공지사항 ID 등) */
private String refId;
/** 원본 파일명 */
private String originalFileNm;
/** 저장 파일명 */
private String storedFileNm;
/** 파일 경로 */
private String filePath;
/** 파일 크기 */
private long fileSize;
/** 파일 확장자 */
private String fileExt;
/** 등록 일시 */
private String regDttm;
/** 등록자 */
private String rgtr;
/** 파일 데이터 (MultipartFile에서 변환) */
private byte[] fileData;
/** 파일 컨텐츠 타입 */
private String contentType;
}

@ -0,0 +1,63 @@
package go.kr.project.common.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
* packageName : go.kr.project.common.model
* fileName : HtmlEditorFileVO
* author :
* date : 2025-05-23
* description : HTML VO
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-05-23
*/
@EqualsAndHashCode(callSuper=true)
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class HtmlEditorFileVO extends PagingVO {
private String fileId;
private String moduleId;
private String originalFileNm;
private String storedFileNm;
private String filePath;
private Long fileSize;
private String fileExt;
private String fileType;
private String useYn;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime regDttm;
private String rgtr;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime mdfcnDttm;
private String mdfr;
// 파일 크기 표시용 (KB, MB 등)
private String fileSizeStr;
// 다운로드 URL
private String downloadUrl;
}

@ -0,0 +1,89 @@
package go.kr.project.common.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* packageName : go.kr.project.common
* fileName : PagingVO
* author :
* date : 25. 5. 8.
* description : VO
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 25. 5. 8.
*/
@Data
@EqualsAndHashCode(callSuper=true)
public class PagingVO extends DefaultVO {
/** 현재 페이지 번호 */
private Integer page;
/** 페이지당 항목 수 */
private Integer perPage;
/** 시작 인덱스 */
private Integer startIndex;
/** 끝 인덱스 */
private Integer endIndex;
/** 전체 페이지 수 */
private Integer totalPages;
/** 기본 페이지당 항목 수 */
private static final int DEFAULT_PER_PAGE = 10;
/**
* .
*/
public void calculateIndex() {
if (this.page == null) {
this.page = 1;
}
if (this.perPage == null) {
this.perPage = DEFAULT_PER_PAGE;
}
this.startIndex = (this.page - 1) * this.perPage;
this.endIndex = this.startIndex + this.perPage;
}
/**
* .
*/
public void calculateTotalPages() {
if (this.perPage == null) {
this.perPage = DEFAULT_PER_PAGE;
}
if (this.getTotalCount() > 0) {
this.totalPages = (int) Math.ceil((double) this.getTotalCount() / this.perPage);
} else {
this.totalPages = 0;
}
}
/**
* .
* "Y" .
*
* @param pagingYn ("Y" "N")
*/
@Override
public void setPagingYn(String pagingYn) {
super.setPagingYn(pagingYn);
if ("Y".equals(pagingYn)) {
// 페이지 인덱스 계산
calculateIndex();
// 전체 페이지 수 계산
calculateTotalPages();
}
}
}

@ -0,0 +1,17 @@
package go.kr.project.common.service;
/**
*
*/
public interface AddressService {
/**
* API .
*
* @param keyword
* @param currentPage
* @param countPerPage
* @return API (JSON )
*/
String searchAddress(String keyword, int currentPage, int countPerPage);
}

@ -0,0 +1,36 @@
package go.kr.project.common.service;
import go.kr.project.common.model.CmmnCodeSearchVO;
import go.kr.project.system.code.model.CodeDetailVO;
import java.util.List;
/**
* packageName : go.kr.project.common.service
* fileName : CommonCodeService
* author :
* date : 2025-05-10
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-05-10
*/
public interface CommonCodeService {
/**
* .
*
* @param cdGroupId ID
* @return
*/
List<CodeDetailVO> selectCodeDetailListByGroupId(String cdGroupId);
/**
* .
*
* @param searchVO VO
* @return
*/
List<CodeDetailVO> selectCodeDetailList(CmmnCodeSearchVO searchVO);
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save