배치 전반적으로 수정,

ZIP 파일 압축해제 배치 추가 테스트
multiDB
박성영 5 months ago
parent 46b5c2df25
commit 8778338eef

@ -0,0 +1,6 @@
CREATE SEQUENCE seq_zip_file_detail_log_id
START WITH 1
INCREMENT BY 1
MINVALUE 1
MAXVALUE 99999999
CYCLE;

@ -0,0 +1,6 @@
CREATE SEQUENCE seq_zip_file_process_log_id
START WITH 1
INCREMENT BY 1
MINVALUE 1
MAXVALUE 99999999
CYCLE;

@ -0,0 +1,33 @@
create table tb_zip_file_detail_log
(
detail_log_id varchar(12) not null comment '상세로그ID'
primary key,
log_id varchar(12) not null comment '로그ID',
file_name varchar(255) not null comment '파일명',
file_path varchar(500) not null comment '파일경로',
file_size bigint default 0 null comment '파일크기(bytes)',
file_type varchar(50) null comment '파일타입',
is_image char(1) default 'N' null comment '이미지여부(Y/N)',
is_corrupted char(1) default 'N' null comment '손상여부(Y/N)',
process_status varchar(20) default 'SUCCESS' null comment '처리상태(SUCCESS/ERROR)',
error_message text null comment '에러메시지',
process_time datetime default current_timestamp() null comment '처리시간',
reg_date datetime default current_timestamp() null comment '등록일시',
reg_user_id varchar(20) default 'BATCH_SYSTEM' null comment '등록자ID',
constraint fk_zip_detail_log_id
foreign key (log_id) references tb_zip_file_process_log (log_id)
on delete cascade
)
comment 'ZIP파일처리상세로그' collate = utf8mb4_unicode_ci;
create index idx_log_id
on tb_zip_file_detail_log (log_id);
create index idx_file_name
on tb_zip_file_detail_log (file_name);
create index idx_process_status
on tb_zip_file_detail_log (process_status);
create index idx_is_image
on tb_zip_file_detail_log (is_image);

@ -0,0 +1,33 @@
create table tb_zip_file_process_log
(
log_id varchar(12) not null comment '로그ID'
primary key,
zip_file_name varchar(255) not null comment 'ZIP파일명',
zip_file_path varchar(500) not null comment 'ZIP파일경로',
extract_path varchar(500) null comment '압축해제경로',
archive_path varchar(500) null comment '원본보관경로',
total_files int default 0 null comment '총파일수',
success_files int default 0 null comment '성공파일수',
error_files int default 0 null comment '실패파일수',
image_files int default 0 null comment '이미지파일수',
non_image_files int default 0 null comment '비이미지파일수',
corrupted_files int default 0 null comment '손상파일수',
process_status varchar(20) default 'PROCESSING' null comment '처리상태(PROCESSING/SUCCESS/ERROR)',
error_message text null comment '에러메시지',
start_time datetime default current_timestamp() null comment '시작시간',
end_time datetime null comment '종료시간',
reg_date datetime default current_timestamp() null comment '등록일시',
reg_user_id varchar(20) default 'BATCH_SYSTEM' null comment '등록자ID',
mod_date datetime default current_timestamp() null on update current_timestamp() comment '수정일시',
mod_user_id varchar(20) default 'BATCH_SYSTEM' null comment '수정자ID'
)
comment 'ZIP파일처리로그' collate = utf8mb4_unicode_ci;
create index idx_zip_file_name
on tb_zip_file_process_log (zip_file_name);
create index idx_process_status
on tb_zip_file_process_log (process_status);
create index idx_start_time
on tb_zip_file_process_log (start_time);

@ -0,0 +1,135 @@
# ZIP 파일 처리 배치 작업 구현
## 개요
특정 디렉토리에 *.zip 파일이 들어오면 압축을 풀어서 특정 디렉토리로 이동시키고, 원본 *.zip 파일은 원본 디렉토리로 이동시키는 배치 작업을 구현했습니다. 이미지 파일 검증 기능과 정상/실패에 대한 로그를 테이블에 정확히 남기는 기능이 포함되어 있습니다.
## 구현된 기능
### 1. 데이터베이스 테이블
- **tb_zip_file_process_log**: ZIP 파일 처리 메인 로그 테이블
- **tb_zip_file_detail_log**: ZIP 파일 내 개별 파일 처리 상세 로그 테이블
- **seq_zip_file_process_log_id**: 메인 로그 ID 시퀀스
- **seq_zip_file_detail_log_id**: 상세 로그 ID 시퀀스
### 2. 주요 클래스
#### 모델 클래스
- `ZipFileProcessLogVO`: ZIP 파일 처리 로그 VO
- `ZipFileDetailLogVO`: ZIP 파일 처리 상세 로그 VO
#### 매퍼 클래스
- `ZipFileProcessLogMapper`: ZIP 파일 처리 로그 매퍼
- `ZipFileDetailLogMapper`: ZIP 파일 처리 상세 로그 매퍼
#### 서비스 클래스
- `ZipFileProcessLogService`: ZIP 파일 처리 로그 서비스
- `ZipFileDetailLogService`: ZIP 파일 처리 상세 로그 서비스
#### 유틸리티 클래스
- `ImageValidationUtil`: 이미지 파일 검증 유틸리티
#### 배치 작업 클래스
- `ZipFileProcessBatchJob`: ZIP 파일 처리 배치 작업 메인 클래스
## 주요 기능
### 1. ZIP 파일 처리 흐름
1. 소스 디렉토리에서 *.zip 파일 검색
2. ZIP 파일 압축 해제
3. 개별 파일 처리 및 검증
4. 이미지 파일 손상 여부 확인
5. 처리 결과 로그 저장
6. ZIP 파일을 아카이브 디렉토리로 이동
### 2. 이미지 파일 검증
- 확장자 기반 이미지 파일 판별
- MIME 타입 기반 이미지 파일 검증
- ImageIO를 이용한 이미지 손상 여부 확인
- 지원 이미지 형식: jpg, jpeg, png, gif, bmp, tiff, tif, webp
### 3. 로그 기능
- ZIP 파일별 처리 로그 (처리 상태, 파일 수, 성공/실패 통계)
- 개별 파일별 상세 로그 (파일 정보, 이미지 여부, 손상 여부)
- 배치 작업 실행 로그 (BatchJobLogUtil 활용)
## 설정
### application.yml 설정
```yaml
batch:
zip:
source:
directory: ${user.home}/batch/zip/source # ZIP 파일 소스 디렉토리
extract:
directory: ${user.home}/batch/zip/extract # ZIP 파일 압축 해제 디렉토리
archive:
directory: ${user.home}/batch/zip/archive # ZIP 파일 아카이브 디렉토리
```
## 디렉토리 구조
```
${user.home}/batch/zip/
├── source/ # ZIP 파일이 들어오는 디렉토리
├── extract/ # ZIP 파일 압축 해제 디렉토리
└── archive/ # 처리 완료된 ZIP 파일 보관 디렉토리
```
## 사용 방법
### 1. 데이터베이스 테이블 생성
```sql
-- 시퀀스 생성
source DB-DDL/maria/ddl/xitframework/seq_zip_file_process_log_id.sql
source DB-DDL/maria/ddl/xitframework/seq_zip_file_detail_log_id.sql
-- 테이블 생성
source DB-DDL/maria/ddl/xitframework/tb_zip_file_process_log.sql
source DB-DDL/maria/ddl/xitframework/tb_zip_file_detail_log.sql
```
### 2. 배치 작업 등록
Quartz 스케줄러를 통해 `ZipFileProcessBatchJob` 클래스를 등록하여 주기적으로 실행
### 3. ZIP 파일 처리
1. `${user.home}/batch/zip/source/` 디렉토리에 ZIP 파일 업로드
2. 배치 작업이 실행되면 자동으로 처리
3. 처리 결과는 데이터베이스 로그 테이블에서 확인
## 로그 확인
### 메인 처리 로그 조회
```sql
SELECT * FROM tb_zip_file_process_log
ORDER BY start_time DESC;
```
### 상세 처리 로그 조회
```sql
SELECT * FROM tb_zip_file_detail_log
WHERE log_id = 'ZPFL00000001'
ORDER BY process_time ASC;
```
### 이미지 파일 처리 결과 조회
```sql
SELECT
file_name,
is_image,
is_corrupted,
process_status,
error_message
FROM tb_zip_file_detail_log
WHERE log_id = 'ZPFL00000001'
AND is_image = 'Y';
```
## 에러 처리
- ZIP 파일 처리 중 오류 발생 시 로그 테이블에 오류 정보 저장
- 개별 파일 처리 실패 시에도 다른 파일 처리 계속 진행
- 손상된 이미지 파일 발견 시 ERROR 상태로 로그 저장
## 주의사항
1. 디렉토리 권한 확인 필요
2. 대용량 ZIP 파일 처리 시 메모리 사용량 모니터링 필요
3. 동일한 파일명의 ZIP 파일 처리 시 타임스탬프 추가하여 중복 방지
4. 배치 작업은 `@DisallowConcurrentExecution` 어노테이션으로 동시 실행 방지

@ -81,4 +81,55 @@ public class BatchConstants {
*
*/
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_ERROR_RETRY_JOB_START = "===== 에러 파일 재처리 배치 작업 시작: %s.%s - %s =====";
public static final String LOG_MSG_ERROR_RETRY_JOB_COMPLETE = "===== 에러 파일 재처리 배치 작업 완료: %s.%s - %s =====";
public static final String LOG_MSG_ERROR_RETRY_JOB_PARTIAL_COMPLETE = "===== 에러 파일 재처리 배치 작업 부분 완료: %s.%s - 총 %d개 파일 중 처리: %d개, 성공: %d개, 실패: %d개 (%s) =====";
public static final String LOG_MSG_ERROR_RETRY_JOB_FAILED = "===== 에러 파일 재처리 배치 작업 실패: %s.%s - 총 %d개 파일 중 처리: %d개, 성공: %d개, 실패: %d개 (%s) =====";
public static final String LOG_MSG_ERROR_RETRY_JOB_ERROR = "===== 에러 파일 재처리 배치 작업 실행 중 오류 발생: %s.%s - %s =====";
// ZIP 파일 처리 배치 작업 로그 메시지
public static final String LOG_MSG_ZIP_JOB_START = "===== ZIP 파일 처리 배치 작업 시작: %s.%s - %s =====";
public static final String LOG_MSG_ZIP_JOB_COMPLETE = "===== ZIP 파일 처리 배치 작업 완료: %s.%s - %s =====";
public static final String LOG_MSG_ZIP_JOB_PARTIAL_COMPLETE = "===== ZIP 파일 처리 배치 작업 부분 완료: %s.%s - %s (처리: %d, 성공: %d, 실패: %d) =====";
public static final String LOG_MSG_ZIP_JOB_FAILED = "===== ZIP 파일 처리 배치 작업 실패: %s.%s - %s (총: %d개, 처리: %d개, 성공: %d개, 실패: %d개, 오류: %s) =====";
public static final String LOG_MSG_ZIP_JOB_ERROR = "===== ZIP 파일 처리 배치 작업 오류: %s.%s =====";
/**
*
*/
public static final String LOG_MSG_STATUS_ALL_FAILED = "전체 파일에서 에러가 발생하여 실패 상태로 설정합니다. (총 %d개 파일 중 %d개 파일에서 에러 발생)";
public static final String LOG_MSG_STATUS_PARTIAL_FAILED = "일부 파일에서 에러가 발생하여 부분 완료 상태로 설정합니다. (총 %d개 파일 중 %d개 파일에서 에러 발생)";
public static final String LOG_MSG_STATUS_ALL_SUCCESS = "모든 파일이 정상적으로 처리되었습니다. 배치 작업 정상 완료.";
public static final String LOG_MSG_STATUS_ALL_RETRY_SUCCESS = "모든 파일이 정상적으로 재처리되었습니다. 배치 작업 정상 완료.";
/**
*
*/
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";
}

@ -40,13 +40,13 @@ public class BatchJobInitializer {
*/
@EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
log.info("서버 부팅 완료 감지 - 배치 작업 초기화를 5초 후에 시작합니다.");
log.info(BatchConstants.LOG_MSG_BATCH_INIT_SERVER_BOOT_DETECTED);
try {
// 서버 부팅 완료 후 5초 지연 - 이를 통해 서버가 완전히 안정화된 후 배치 작업을 초기화, 추후에 15초정도 늘릴예정
TimeUnit.SECONDS.sleep(5);
log.info("배치 작업 초기화 시작");
log.info(BatchConstants.LOG_MSG_BATCH_INIT_START);
// 샘플 배치 작업 등록 (초기 데이터가 없을 경우를 대비)
// 매 분마다 실행 (테스트용, 실제 운영에서는 적절한 주기로 변경)
@ -55,12 +55,12 @@ public class BatchJobInitializer {
// 배치 작업 정보 테이블에서 작업 조회 및 스케줄링
initializeJobsFromDatabase();
log.info("배치 작업 초기화 완료");
log.info(BatchConstants.LOG_MSG_BATCH_INIT_COMPLETE);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("배치 작업 초기화 대기 중 인터럽트 발생: {}", e.getMessage(), e);
log.error(BatchConstants.LOG_MSG_BATCH_INIT_INTERRUPT_ERROR, e.getMessage(), e);
} catch (Exception e) {
log.error(String.format("배치 작업 초기화 중 오류 발생: %s", e.getMessage()), e);
log.error(String.format(BatchConstants.LOG_MSG_BATCH_INIT_ERROR, e.getMessage()), e);
}
}
@ -75,18 +75,18 @@ public class BatchJobInitializer {
List<BatchJobInfoVO> jobList = batchJobService.getBatchJobInfoList(paramVO);
if (jobList == null || jobList.isEmpty()) {
log.info("등록된 배치 작업이 없습니다.");
log.info(BatchConstants.LOG_MSG_BATCH_INIT_NO_JOBS);
return;
}
log.info("총 {}개의 배치 작업 정보를 조회했습니다.", jobList.size());
log.info(BatchConstants.LOG_MSG_BATCH_INIT_JOBS_COUNT, jobList.size());
// 각 작업을 스케줄러에 등록
for (BatchJobInfoVO job : jobList) {
try {
// JOB_INFO_STATUS_DELETED가 아닌 작업만 등록
if (BatchConstants.JOB_INFO_STATUS_DELETED.equals(job.getStatus())) {
log.info("삭제된 작업은 스케줄러에 등록하지 않습니다: {}.{}", job.getJobGroup(), job.getJobName());
log.info(BatchConstants.LOG_MSG_BATCH_INIT_SKIP_DELETED, job.getJobGroup(), job.getJobName());
continue;
}
@ -108,18 +108,18 @@ public class BatchJobInitializer {
if (result) {
log.info("배치 작업을 스케줄러에 등록했습니다: {}.{}", job.getJobGroup(), job.getJobName());
log.info(BatchConstants.LOG_MSG_BATCH_INIT_SCHEDULE_SUCCESS, job.getJobGroup(), job.getJobName());
} else {
log.warn("배치 작업 스케줄링에 실패했습니다: {}.{}", job.getJobGroup(), job.getJobName());
log.warn(BatchConstants.LOG_MSG_BATCH_INIT_SCHEDULE_FAILED, job.getJobGroup(), job.getJobName());
}
} catch (ClassNotFoundException e) {
log.error("배치 작업 클래스를 찾을 수 없습니다: {}", job.getJobClass(), e);
log.error(BatchConstants.LOG_MSG_BATCH_INIT_CLASS_NOT_FOUND, job.getJobClass(), e);
} catch (Exception e) {
log.error("배치 작업 스케줄링 중 오류 발생: {}.{} - {}", job.getJobGroup(), job.getJobName(), e.getMessage(), e);
log.error(BatchConstants.LOG_MSG_BATCH_INIT_SCHEDULE_ERROR, job.getJobGroup(), job.getJobName(), e.getMessage(), e);
}
}
} catch (Exception e) {
log.error("배치 작업 초기화 중 오류 발생: {}", e.getMessage(), e);
log.error(BatchConstants.LOG_MSG_BATCH_INIT_DB_ERROR, e.getMessage(), e);
}
}
@ -143,10 +143,10 @@ public class BatchJobInitializer {
description
);
log.info(String.format("배치 작업 정보 등록 완료: %s",
log.info(String.format(BatchConstants.LOG_MSG_BATCH_INIT_REGISTER_COMPLETE,
jobInfo != null ? jobInfo.getJobId() : "unknown"));
} catch (Exception e) {
log.error(String.format("샘플 배치 작업 등록 중 오류 발생: %s", e.getMessage()), e);
log.error(String.format(BatchConstants.LOG_MSG_BATCH_INIT_REGISTER_ERROR, e.getMessage()), e);
}
}
}

@ -43,7 +43,7 @@ public class SampleBatchJob implements Job {
String jobGroup = context.getJobDetail().getKey().getGroup();
String currentTime = LocalDateTime.now().format(formatter);
String startMessage = String.format("===== 배치 작업 시작: %s.%s - %s =====", jobGroup, jobName, currentTime);
String startMessage = String.format(BatchConstants.LOG_MSG_BATCH_JOB_START, jobGroup, jobName, currentTime);
BatchJobLogUtil.info(context, startMessage);
try {
@ -53,10 +53,10 @@ public class SampleBatchJob implements Job {
// 실제 배치 작업 수행
processBatchJob(context);
String completeMessage = String.format("===== 배치 작업 완료: %s.%s - %s =====", jobGroup, jobName, currentTime);
String completeMessage = String.format(BatchConstants.LOG_MSG_BATCH_JOB_COMPLETE, jobGroup, jobName, currentTime);
BatchJobLogUtil.info(context, completeMessage);
} catch (Exception e) {
String errorMessage = String.format("배치 작업 실행 중 오류 발생: %s.%s - %s", jobGroup, jobName, e.getMessage());
String errorMessage = String.format(BatchConstants.LOG_MSG_BATCH_JOB_ERROR, jobGroup, jobName, e.getMessage());
BatchJobLogUtil.error(context, errorMessage, e);
throw new JobExecutionException(errorMessage, e, false);
}

@ -43,7 +43,7 @@ public class SampleBatchJob2 implements Job {
String jobGroup = context.getJobDetail().getKey().getGroup();
String currentTime = LocalDateTime.now().format(formatter);
String startMessage = String.format("===== 배치 작업 시작: %s.%s - %s =====", jobGroup, jobName, currentTime);
String startMessage = String.format(BatchConstants.LOG_MSG_BATCH_JOB_START, jobGroup, jobName, currentTime);
BatchJobLogUtil.info(context, startMessage);
try {
@ -53,10 +53,10 @@ public class SampleBatchJob2 implements Job {
// 실제 배치 작업 수행
processBatchJob(context);
String completeMessage = String.format("===== 배치 작업 완료: %s.%s - %s =====", jobGroup, jobName, currentTime);
String completeMessage = String.format(BatchConstants.LOG_MSG_BATCH_JOB_COMPLETE, jobGroup, jobName, currentTime);
BatchJobLogUtil.info(context, completeMessage);
} catch (Exception e) {
String errorMessage = String.format("배치 작업 실행 중 오류 발생: %s.%s - %s", jobGroup, jobName, e.getMessage());
String errorMessage = String.format(BatchConstants.LOG_MSG_BATCH_JOB_ERROR, jobGroup, jobName, e.getMessage());
BatchJobLogUtil.error(context, errorMessage, e);
throw new JobExecutionException(errorMessage, e, false);
}

@ -44,7 +44,7 @@ public class SampleBatchJob3 implements Job {
String jobGroup = context.getJobDetail().getKey().getGroup();
String currentTime = LocalDateTime.now().format(formatter);
String startMessage = String.format("===== 배치 작업 시작: %s.%s - %s =====", jobGroup, jobName, currentTime);
String startMessage = String.format(BatchConstants.LOG_MSG_BATCH_JOB_START, jobGroup, jobName, currentTime);
BatchJobLogUtil.info(context, startMessage);
try {
@ -56,21 +56,21 @@ public class SampleBatchJob3 implements Job {
// 결과에 따라 적절한 메시지 출력 및 상태 처리
if (result.getStatus().equals(BatchConstants.JOB_EXECUTION_STATUS_PARTIALLY_COMPLETED)) {
String partialCompleteMessage = String.format("배치 작업 부분 완료: %s.%s - 총 %d개 중 %d개 처리됨 (%s)",
jobGroup, jobName, result.getTotalItems(), result.getProcessedItems(), currentTime);
String partialCompleteMessage = String.format(BatchConstants.LOG_MSG_BATCH_JOB_PARTIAL_COMPLETE,
jobGroup, jobName, result.getTotalItems(), result.getProcessedItems(), result.getProcessedItems(), result.getErrorItems(), currentTime);
BatchJobLogUtil.info(context, partialCompleteMessage);
// 부분 완료 상태로 작업 종료 - 메시지로 상태 전달
throw new JobExecutionException(partialCompleteMessage, false);
} else {
String completeMessage = String.format("===== 배치 작업 완료: %s.%s - %s =====", jobGroup, jobName, currentTime);
String completeMessage = String.format(BatchConstants.LOG_MSG_BATCH_JOB_COMPLETE, jobGroup, jobName, currentTime);
BatchJobLogUtil.info(context, completeMessage);
}
} catch (JobExecutionException e) {
// JobExecutionException은 이미 처리된 예외이므로 그대로 던짐
throw e;
} catch (Exception e) {
String errorMessage = String.format("배치 작업 실행 중 오류 발생: %s.%s - %s", jobGroup, jobName, e.getMessage());
String errorMessage = String.format(BatchConstants.LOG_MSG_BATCH_JOB_ERROR, jobGroup, jobName, e.getMessage());
BatchJobLogUtil.error(context, errorMessage, e);
throw new JobExecutionException(errorMessage, e, false);
}
@ -184,6 +184,8 @@ public class SampleBatchJob3 implements Job {
.status(status)
.totalItems(TOTAL_ITEMS)
.processedItems(totalProcessedItems)
.successItems(totalProcessedItems)
.errorItems(TOTAL_ITEMS - totalProcessedItems)
.build();
}

@ -107,7 +107,7 @@ public class SampleErrorFileRetryBatchJob implements Job {
* .
*/
private void logJobStart(JobExecutionContext context, String jobGroup, String jobName, String currentTime) {
String startMessage = String.format("===== 에러 파일 재처리 배치 작업 시작: %s.%s - %s =====", jobGroup, jobName, currentTime);
String startMessage = String.format(BatchConstants.LOG_MSG_ERROR_RETRY_JOB_START, jobGroup, jobName, currentTime);
BatchJobLogUtil.info(context, startMessage);
}
@ -162,8 +162,8 @@ public class SampleErrorFileRetryBatchJob implements Job {
*/
private void handlePartialComplete(JobExecutionContext context, BatchJobResult result,
String jobGroup, String jobName, String currentTime) throws JobExecutionException {
String message = String.format("===== 에러 파일 재처리 배치 작업 부분 완료: %s.%s - 총 %d개 파일 중 %d개 파일 처리됨 (%s) =====",
jobGroup, jobName, result.getTotalItems(), result.getProcessedItems(), currentTime);
String message = String.format(BatchConstants.LOG_MSG_ERROR_RETRY_JOB_PARTIAL_COMPLETE,
jobGroup, jobName, result.getTotalItems(), result.getProcessedItems(), result.getSuccessItems(), result.getErrorItems(), currentTime);
BatchJobLogUtil.info(context, message);
throw new JobExecutionException(message, false);
}
@ -173,8 +173,8 @@ public class SampleErrorFileRetryBatchJob implements Job {
*/
private void handleJobFailed(JobExecutionContext context, BatchJobResult result,
String jobGroup, String jobName, String currentTime) throws JobExecutionException {
String message = String.format("===== 에러 파일 재처리 배치 작업 실패: %s.%s - 총 %d개 파일 모두 처리 실패 (%s) =====",
jobGroup, jobName, result.getTotalItems(), currentTime);
String message = String.format(BatchConstants.LOG_MSG_ERROR_RETRY_JOB_FAILED,
jobGroup, jobName, result.getTotalItems(), result.getProcessedItems(), result.getSuccessItems(), result.getErrorItems(), currentTime);
BatchJobLogUtil.error(context, message);
throw new JobExecutionException(message, false);
}
@ -183,7 +183,7 @@ public class SampleErrorFileRetryBatchJob implements Job {
* .
*/
private void handleJobComplete(JobExecutionContext context, String jobGroup, String jobName, String currentTime) {
String message = String.format("===== 에러 파일 재처리 배치 작업 완료: %s.%s - %s =====", jobGroup, jobName, currentTime);
String message = String.format(BatchConstants.LOG_MSG_ERROR_RETRY_JOB_COMPLETE, jobGroup, jobName, currentTime);
BatchJobLogUtil.info(context, message);
}
@ -191,7 +191,7 @@ public class SampleErrorFileRetryBatchJob implements Job {
* .
*/
private void handleJobError(JobExecutionContext context, String jobGroup, String jobName, Exception e) throws JobExecutionException {
String errorMessage = String.format("===== 에러 파일 재처리 배치 작업 실행 중 오류 발생: %s.%s - %s =====", jobGroup, jobName, e.getMessage());
String errorMessage = String.format(BatchConstants.LOG_MSG_ERROR_RETRY_JOB_ERROR, jobGroup, jobName, e.getMessage());
BatchJobLogUtil.error(context, errorMessage, e);
throw new JobExecutionException(errorMessage, e, false);
}
@ -228,7 +228,9 @@ public class SampleErrorFileRetryBatchJob implements Job {
return BatchJobResult.builder()
.status(status)
.totalItems(retryTargetFiles.size())
.processedItems(processingResult.getSuccessCount())
.processedItems(processingResult.getProcessedCount())
.successItems(processingResult.getSuccessCount())
.errorItems(processingResult.getErrorCount())
.build();
}
@ -241,6 +243,8 @@ public class SampleErrorFileRetryBatchJob implements Job {
.status(BatchConstants.JOB_EXECUTION_STATUS_COMPLETED)
.totalItems(0)
.processedItems(0)
.successItems(0)
.errorItems(0)
.build();
}
@ -480,6 +484,8 @@ public class SampleErrorFileRetryBatchJob implements Job {
String[] columns;
if ("|".equals(delimiter)) {
columns = line.split("\\|", -1);
} else if ("^".equals(delimiter)) {
columns = line.split("\\^", -1);
} else {
columns = line.split(delimiter, -1);
}

@ -103,7 +103,7 @@ public class SampleFileReadBatchJob implements Job {
* .
*/
private void logJobStart(JobExecutionContext context, String jobGroup, String jobName, String currentTime) {
String startMessage = String.format("===== 배치 작업 시작: %s.%s - %s =====", jobGroup, jobName, currentTime);
String startMessage = String.format(BatchConstants.LOG_MSG_BATCH_JOB_START, jobGroup, jobName, currentTime);
BatchJobLogUtil.info(context, startMessage);
}
@ -171,8 +171,8 @@ public class SampleFileReadBatchJob implements Job {
*/
private void handlePartialComplete(JobExecutionContext context, BatchJobResult result,
String jobGroup, String jobName, String currentTime) throws JobExecutionException {
String message = String.format("===== 배치 작업 부분 완료: %s.%s - 총 %d개 파일 중 %d개 파일 처리됨 (%s) =====",
jobGroup, jobName, result.getTotalItems(), result.getProcessedItems(), currentTime);
String message = String.format(BatchConstants.LOG_MSG_BATCH_JOB_PARTIAL_COMPLETE,
jobGroup, jobName, result.getTotalItems(), result.getProcessedItems(), result.getSuccessItems(), result.getErrorItems(), currentTime);
BatchJobLogUtil.info(context, message);
throw new JobExecutionException(message, false);
}
@ -182,8 +182,8 @@ public class SampleFileReadBatchJob implements Job {
*/
private void handleJobFailed(JobExecutionContext context, BatchJobResult result,
String jobGroup, String jobName, String currentTime) throws JobExecutionException {
String message = String.format("===== 배치 작업 실패: %s.%s - 총 %d개 파일 모두 처리 실패 (%s) =====",
jobGroup, jobName, result.getTotalItems(), currentTime);
String message = String.format(BatchConstants.LOG_MSG_BATCH_JOB_FAILED,
jobGroup, jobName, result.getTotalItems(), result.getProcessedItems(), result.getSuccessItems(), result.getErrorItems(), currentTime);
BatchJobLogUtil.error(context, message);
throw new JobExecutionException(message, false);
}
@ -192,7 +192,7 @@ public class SampleFileReadBatchJob implements Job {
* .
*/
private void handleJobComplete(JobExecutionContext context, String jobGroup, String jobName, String currentTime) {
String message = String.format("===== 배치 작업 완료: %s.%s - %s =====", jobGroup, jobName, currentTime);
String message = String.format(BatchConstants.LOG_MSG_BATCH_JOB_COMPLETE, jobGroup, jobName, currentTime);
BatchJobLogUtil.info(context, message);
}
@ -200,7 +200,7 @@ public class SampleFileReadBatchJob implements Job {
* .
*/
private void handleJobError(JobExecutionContext context, String jobGroup, String jobName, Exception e) throws JobExecutionException {
String errorMessage = String.format("===== 배치 작업 실행 중 오류 발생: %s.%s - %s =====", jobGroup, jobName, e.getMessage());
String errorMessage = String.format(BatchConstants.LOG_MSG_BATCH_JOB_ERROR, jobGroup, jobName, e.getMessage());
BatchJobLogUtil.error(context, errorMessage, e);
throw new JobExecutionException(errorMessage, e, false);
}
@ -235,7 +235,9 @@ public class SampleFileReadBatchJob implements Job {
return BatchJobResult.builder()
.status(status)
.totalItems(filesToProcess.size())
.processedItems(processingResult.getSuccessCount())
.processedItems(processingResult.getProcessedCount())
.successItems(processingResult.getSuccessCount())
.errorItems(processingResult.getErrorCount())
.build();
}
@ -248,6 +250,8 @@ public class SampleFileReadBatchJob implements Job {
.status(BatchConstants.JOB_EXECUTION_STATUS_COMPLETED)
.totalItems(0)
.processedItems(0)
.successItems(0)
.errorItems(0)
.build();
}
@ -433,6 +437,8 @@ public class SampleFileReadBatchJob implements Job {
String[] columns;
if ("|".equals(delimiter)) {
columns = line.split("\\|", -1);
} else if ("^".equals(delimiter)) {
columns = line.split("\\^", -1);
} else {
columns = line.split(delimiter, -1);
}

@ -0,0 +1,513 @@
package go.kr.project.batch.job;
import egovframework.constant.BatchConstants;
import egovframework.util.SessionUtil;
import go.kr.project.batch.model.BatchJobResult;
import go.kr.project.batch.model.ZipFileDetailLogVO;
import go.kr.project.batch.model.ZipFileProcessLogVO;
import go.kr.project.batch.service.ZipFileDetailLogService;
import go.kr.project.batch.service.ZipFileProcessLogService;
import go.kr.project.batch.util.BatchJobLogUtil;
import go.kr.project.batch.util.ImageValidationUtil;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* packageName : go.kr.project.batch.job
* fileName : ZipFileProcessBatchJob
* author :
* date : 2025-01-27
* description : ZIP
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-01-27
*/
@Slf4j
@Component
@DisallowConcurrentExecution // 동시 실행 방지
public class ZipFileProcessBatchJob implements Job {
// ===========================================================
// 상수 정의
// ===========================================================
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(BatchConstants.DATE_FORMAT_DEFAULT);
private static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final String BATCH_USER_ID = "BATCH_SYSTEM";
// ===========================================================
// 설정값 주입
// ===========================================================
@Value("${batch.zip.source-dir:/batch/zip/source}")
private String sourceDirectory;
@Value("${batch.zip.extract-dir:/batch/zip/extract}")
private String extractDirectory;
@Value("${batch.zip.archive-dir:/batch/zip/archive}")
private String archiveDirectory;
@Value("${batch.zip.create-date-subdir:false}")
private boolean createDateSubdir;
// ===========================================================
// 의존성 주입
// ===========================================================
@Autowired
private ZipFileProcessLogService zipFileProcessLogService;
@Autowired
private ZipFileDetailLogService zipFileDetailLogService;
// ===========================================================
// 메인 실행 메소드
// ===========================================================
/**
* .
*
* @param context
* @throws JobExecutionException
*/
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
String jobName = context.getJobDetail().getKey().getName();
String jobGroup = context.getJobDetail().getKey().getGroup();
String currentTime = LocalDateTime.now().format(formatter);
logJobStart(context, jobGroup, jobName, currentTime);
try {
logBatchSessionInfo();
BatchJobResult result = processZipFiles(context);
handleJobResult(context, result, jobGroup, jobName, currentTime);
} catch (Exception e) {
handleJobError(context, jobGroup, jobName, e);
}
}
// ===========================================================
// 배치 작업 로깅 메소드들
// ===========================================================
/**
* .
*/
private void logJobStart(JobExecutionContext context, String jobGroup, String jobName, String currentTime) {
String startMessage = String.format(BatchConstants.LOG_MSG_ZIP_JOB_START, jobGroup, jobName, currentTime);
BatchJobLogUtil.info(context, startMessage);
}
/**
* .
*/
private void logBatchSessionInfo() {
try {
String batchUserId = SessionUtil.getBatchUserId();
String batchUserAccount = SessionUtil.getBatchUserAccount();
boolean isBatchSystem = SessionUtil.isBatchSystem();
log.info("배치 세션 정보 - 사용자ID: {}, 계정: {}, 배치시스템여부: {}",
batchUserId, batchUserAccount, isBatchSystem);
} catch (Exception e) {
log.warn("배치 세션 정보 조회 중 오류 발생", e);
}
}
/**
* .
*/
private void handleJobResult(JobExecutionContext context, BatchJobResult result, String jobGroup, String jobName, String currentTime) {
if (result.getStatus().equals(BatchConstants.JOB_EXECUTION_STATUS_PARTIALLY_COMPLETED)) {
handlePartialComplete(context, result, jobGroup, jobName, currentTime);
} else if (result.getStatus().equals(BatchConstants.JOB_EXECUTION_STATUS_FAILED)) {
handleJobFailed(context, result, jobGroup, jobName, currentTime);
} else {
handleJobComplete(context, jobGroup, jobName, currentTime);
}
}
/**
*
*/
private void handlePartialComplete(JobExecutionContext context, BatchJobResult result, String jobGroup, String jobName, String currentTime) {
String partialCompleteMessage = String.format(BatchConstants.LOG_MSG_ZIP_JOB_PARTIAL_COMPLETE,
jobGroup, jobName, currentTime, result.getProcessedCount(), result.getSuccessCount(), result.getErrorCount());
BatchJobLogUtil.info(context, partialCompleteMessage);
}
/**
*
*/
private void handleJobFailed(JobExecutionContext context, BatchJobResult result, String jobGroup, String jobName, String currentTime) {
String failedMessage = String.format(BatchConstants.LOG_MSG_ZIP_JOB_FAILED,
jobGroup, jobName, currentTime, result.getTotalItems(), result.getProcessedCount(), result.getSuccessCount(), result.getErrorCount(), result.getErrorMessage());
BatchJobLogUtil.error(context, failedMessage);
}
/**
*
*/
private void handleJobComplete(JobExecutionContext context, String jobGroup, String jobName, String currentTime) {
String completeMessage = String.format(BatchConstants.LOG_MSG_ZIP_JOB_COMPLETE, jobGroup, jobName, currentTime);
BatchJobLogUtil.info(context, completeMessage);
}
/**
*
*/
private void handleJobError(JobExecutionContext context, String jobGroup, String jobName, Exception e) throws JobExecutionException {
String errorMessage = String.format(BatchConstants.LOG_MSG_ZIP_JOB_ERROR, jobGroup, jobName);
BatchJobLogUtil.error(context, errorMessage, e);
throw new JobExecutionException(e);
}
// ===========================================================
// ZIP 파일 처리 메소드들
// ===========================================================
/**
* ZIP .
*/
private BatchJobResult processZipFiles(JobExecutionContext context) {
BatchJobResult result = new BatchJobResult();
result.setStatus(BatchConstants.JOB_EXECUTION_STATUS_COMPLETED);
try {
// 디렉토리 생성
createDirectories();
// 처리할 ZIP 파일 목록 조회
List<Path> zipFiles = getZipFilesToProcess();
if (zipFiles.isEmpty()) {
BatchJobLogUtil.info(context, "처리할 파일이 없습니다.");
return result;
}
BatchJobLogUtil.info(context, String.format("처리할 ZIP 파일 수: %d개", zipFiles.size()));
result.setTotalItems(zipFiles.size());
// 각 ZIP 파일 처리
for (Path zipFile : zipFiles) {
try {
boolean success = processZipFile(context, zipFile);
result.incrementProcessed();
if (success) {
result.incrementSuccess();
} else {
result.incrementError();
result.setStatus(BatchConstants.JOB_EXECUTION_STATUS_PARTIALLY_COMPLETED);
}
} catch (Exception e) {
log.error("ZIP 파일 처리 중 오류 발생: {}", zipFile, e);
result.incrementError();
result.setStatus(BatchConstants.JOB_EXECUTION_STATUS_PARTIALLY_COMPLETED);
}
}
// 처리 결과 로깅
logProcessingSummary(context, result);
} catch (Exception e) {
log.error("ZIP 파일 처리 배치 작업 중 오류 발생", e);
result.setStatus(BatchConstants.JOB_EXECUTION_STATUS_FAILED);
result.setErrorMessage(e.getMessage());
}
return result;
}
/**
* .
*/
private void createDirectories() throws IOException {
Files.createDirectories(Paths.get(sourceDirectory));
Files.createDirectories(Paths.get(extractDirectory));
Files.createDirectories(Paths.get(archiveDirectory));
}
/**
* ZIP .
*/
private List<Path> getZipFilesToProcess() throws IOException {
List<Path> zipFiles = new ArrayList<>();
Path sourcePath = Paths.get(sourceDirectory);
if (!Files.exists(sourcePath)) {
return zipFiles;
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(sourcePath, "*.zip")) {
for (Path file : stream) {
if (Files.isRegularFile(file)) {
zipFiles.add(file);
}
}
}
return zipFiles;
}
/**
* ZIP .
*/
private boolean processZipFile(JobExecutionContext context, Path zipFile) {
String zipFileName = zipFile.getFileName().toString();
BatchJobLogUtil.info(context, String.format("ZIP 파일 처리 시작: %s", zipFileName));
// 처리 로그 생성
ZipFileProcessLogVO processLog = createProcessLog(zipFile);
String logId = zipFileProcessLogService.generateZipFileProcessLogId();
processLog.setLogId(logId);
zipFileProcessLogService.insertZipFileProcessLog(processLog);
try {
// ZIP 파일 압축 해제 및 파일 처리
List<ZipFileDetailLogVO> detailLogs = extractAndProcessZipFile(zipFile, logId);
// 상세 로그 저장
if (!detailLogs.isEmpty()) {
zipFileDetailLogService.insertZipFileDetailLogList(detailLogs);
}
// 처리 결과 집계
updateProcessLogSummary(processLog, detailLogs);
// ZIP 파일을 아카이브 디렉토리로 이동
moveZipFileToArchive(zipFile);
// 처리 로그 완료 처리
processLog.setProcessStatus("SUCCESS");
processLog.setEndTime(LocalDateTime.now());
zipFileProcessLogService.updateZipFileProcessLog(processLog);
BatchJobLogUtil.info(context, String.format("ZIP 파일 처리 완료: %s (총: %d, 성공: %d, 실패: %d)",
zipFileName, processLog.getTotalFiles(), processLog.getSuccessFiles(), processLog.getErrorFiles()));
return true;
} catch (Exception e) {
log.error("ZIP 파일 처리 중 오류 발생: {}", zipFileName, e);
// 처리 로그 오류 처리
processLog.setProcessStatus("ERROR");
processLog.setErrorMessage(e.getMessage());
processLog.setEndTime(LocalDateTime.now());
zipFileProcessLogService.updateZipFileProcessLog(processLog);
BatchJobLogUtil.error(context, String.format("ZIP 파일 처리 실패: %s", zipFileName), e);
return false;
}
}
/**
* .
*/
private ZipFileProcessLogVO createProcessLog(Path zipFile) {
ZipFileProcessLogVO processLog = new ZipFileProcessLogVO();
processLog.setZipFileName(zipFile.getFileName().toString());
processLog.setZipFilePath(zipFile.toString());
processLog.setExtractPath(extractDirectory);
processLog.setArchivePath(archiveDirectory);
processLog.setProcessStatus("PROCESSING");
processLog.setStartTime(LocalDateTime.now());
processLog.setRegUserId(BATCH_USER_ID);
processLog.setModUserId(BATCH_USER_ID);
// 초기값 설정
processLog.setTotalFiles(0);
processLog.setSuccessFiles(0);
processLog.setErrorFiles(0);
processLog.setImageFiles(0);
processLog.setNonImageFiles(0);
processLog.setCorruptedFiles(0);
return processLog;
}
/**
* ZIP .
*/
private List<ZipFileDetailLogVO> extractAndProcessZipFile(Path zipFile, String logId) throws IOException {
List<ZipFileDetailLogVO> detailLogs = new ArrayList<>();
Path extractPath = Paths.get(extractDirectory);
// 날짜별 하위 디렉토리 생성
if (createDateSubdir) {
String dateStr = LocalDateTime.now().format(dateFormatter);
extractPath = extractPath.resolve(dateStr);
}
// 추출 디렉토리 생성
if (!Files.exists(extractPath)) {
Files.createDirectories(extractPath);
log.info("압축 해제 디렉토리 생성: {}", extractPath);
}
try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(zipFile))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (!entry.isDirectory()) {
ZipFileDetailLogVO detailLog = processZipEntry(zis, entry, extractPath, logId);
detailLogs.add(detailLog);
}
zis.closeEntry();
}
}
return detailLogs;
}
/**
* ZIP ( ) .
*/
private ZipFileDetailLogVO processZipEntry(ZipInputStream zis, ZipEntry entry, Path extractPath, String logId) {
ZipFileDetailLogVO detailLog = new ZipFileDetailLogVO();
detailLog.setLogId(logId);
detailLog.setFileName(entry.getName());
detailLog.setFileSize(entry.getSize());
detailLog.setProcessTime(LocalDateTime.now());
detailLog.setRegUserId(BATCH_USER_ID);
try {
// 파일을 추출 디렉토리에 저장
Path targetFile = extractPath.resolve(entry.getName());
Files.createDirectories(targetFile.getParent());
Files.copy(zis, targetFile, StandardCopyOption.REPLACE_EXISTING);
detailLog.setFilePath(targetFile.toString());
detailLog.setFileSize(Files.size(targetFile));
// 파일 타입 확인
String mimeType = Files.probeContentType(targetFile);
detailLog.setFileType(mimeType != null ? mimeType : "unknown");
// 이미지 파일 검증
boolean isImage = ImageValidationUtil.isImageFile(targetFile);
detailLog.setIsImage(isImage ? "Y" : "N");
if (isImage) {
// 이미지 파일인 경우 손상 여부 확인
boolean isCorrupted = ImageValidationUtil.isImageCorrupted(targetFile);
detailLog.setIsCorrupted(isCorrupted ? "Y" : "N");
if (isCorrupted) {
detailLog.setProcessStatus("ERROR");
detailLog.setErrorMessage("손상된 이미지 파일");
log.warn("손상된 이미지 파일 발견: {}", entry.getName());
} else {
detailLog.setProcessStatus("SUCCESS");
log.debug("정상 이미지 파일 처리: {}", entry.getName());
}
} else {
detailLog.setProcessStatus("SUCCESS");
log.debug("비이미지 파일 처리: {}", entry.getName());
}
} catch (IOException e) {
log.error("ZIP 엔트리 처리 중 오류 발생: {}", entry.getName(), e);
detailLog.setProcessStatus("ERROR");
detailLog.setErrorMessage("파일 추출 실패: " + e.getMessage());
detailLog.setIsImage("N");
detailLog.setIsCorrupted("N");
}
return detailLog;
}
/**
* .
*/
private void updateProcessLogSummary(ZipFileProcessLogVO processLog, List<ZipFileDetailLogVO> detailLogs) {
int totalFiles = detailLogs.size();
int successFiles = 0;
int errorFiles = 0;
int imageFiles = 0;
int nonImageFiles = 0;
int corruptedFiles = 0;
for (ZipFileDetailLogVO detailLog : detailLogs) {
if ("SUCCESS".equals(detailLog.getProcessStatus())) {
successFiles++;
} else {
errorFiles++;
}
if ("Y".equals(detailLog.getIsImage())) {
imageFiles++;
if ("Y".equals(detailLog.getIsCorrupted())) {
corruptedFiles++;
}
} else {
nonImageFiles++;
}
}
processLog.setTotalFiles(totalFiles);
processLog.setSuccessFiles(successFiles);
processLog.setErrorFiles(errorFiles);
processLog.setImageFiles(imageFiles);
processLog.setNonImageFiles(nonImageFiles);
processLog.setCorruptedFiles(corruptedFiles);
}
/**
* ZIP .
*/
private void moveZipFileToArchive(Path zipFile) throws IOException {
Path archivePath = Paths.get(archiveDirectory);
// 날짜별 하위 디렉토리 생성
if (createDateSubdir) {
String dateStr = LocalDateTime.now().format(dateFormatter);
archivePath = archivePath.resolve(dateStr);
}
// 아카이브 디렉토리 생성
if (!Files.exists(archivePath)) {
Files.createDirectories(archivePath);
log.info("아카이브 디렉토리 생성: {}", archivePath);
}
Path targetPath = archivePath.resolve(zipFile.getFileName());
// 동일한 파일명이 있는 경우 타임스탬프 추가
if (Files.exists(targetPath)) {
String fileName = zipFile.getFileName().toString();
String baseName = fileName.substring(0, fileName.lastIndexOf('.'));
String extension = fileName.substring(fileName.lastIndexOf('.'));
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
targetPath = archivePath.resolve(baseName + "_" + timestamp + extension);
}
Files.move(zipFile, targetPath, StandardCopyOption.REPLACE_EXISTING);
log.info("ZIP 파일 아카이브 완료: {} -> {}", zipFile, targetPath);
}
/**
* .
*/
private void logProcessingSummary(JobExecutionContext context, BatchJobResult result) {
String summary = String.format("ZIP 파일 처리 결과 - 처리: %d개, 성공: %d개, 실패: %d개",
result.getProcessedCount(), result.getSuccessCount(), result.getErrorCount());
BatchJobLogUtil.info(context, summary);
}
}

@ -0,0 +1,93 @@
package go.kr.project.batch.mapper;
import go.kr.project.batch.model.ZipFileDetailLogVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* packageName : go.kr.project.batch.mapper
* fileName : ZipFileDetailLogMapper
* author :
* date : 2025-01-27
* description : ZIP
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-01-27
*/
@Mapper
public interface ZipFileDetailLogMapper {
/**
* ZIP ID .
*
* @return ID
*/
String generateZipFileDetailLogId();
/**
* ZIP .
*
* @param zipFileDetailLog ZIP
* @return
*/
int insertZipFileDetailLog(ZipFileDetailLogVO zipFileDetailLog);
/**
* ZIP .
*
* @param zipFileDetailLogList ZIP
* @return
*/
int insertZipFileDetailLogList(List<ZipFileDetailLogVO> zipFileDetailLogList);
/**
* ZIP .
*
* @param detailLogId ID
* @return
*/
int deleteZipFileDetailLog(@Param("detailLogId") String detailLogId);
/**
* ID ZIP .
*
* @param logId ID
* @return
*/
int deleteZipFileDetailLogByLogId(@Param("logId") String logId);
/**
* ZIP .
*
* @param detailLogId ID
* @return ZIP
*/
ZipFileDetailLogVO selectZipFileDetailLog(@Param("detailLogId") String detailLogId);
/**
* ID ZIP .
*
* @param logId ID
* @return ZIP
*/
List<ZipFileDetailLogVO> selectZipFileDetailLogListByLogId(@Param("logId") String logId);
/**
* ZIP .
*
* @param paramVO
* @return
*/
int selectZipFileDetailLogListTotalCount(ZipFileDetailLogVO paramVO);
/**
* ZIP .
*
* @param paramVO
* @return ZIP
*/
List<ZipFileDetailLogVO> selectZipFileDetailLogList(ZipFileDetailLogVO paramVO);
}

@ -0,0 +1,85 @@
package go.kr.project.batch.mapper;
import go.kr.project.batch.model.ZipFileProcessLogVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* packageName : go.kr.project.batch.mapper
* fileName : ZipFileProcessLogMapper
* author :
* date : 2025-01-27
* description : ZIP
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-01-27
*/
@Mapper
public interface ZipFileProcessLogMapper {
/**
* ZIP ID .
*
* @return ID
*/
String generateZipFileProcessLogId();
/**
* ZIP .
*
* @param zipFileProcessLog ZIP
* @return
*/
int insertZipFileProcessLog(ZipFileProcessLogVO zipFileProcessLog);
/**
* ZIP .
*
* @param zipFileProcessLog ZIP
* @return
*/
int updateZipFileProcessLog(ZipFileProcessLogVO zipFileProcessLog);
/**
* ZIP .
*
* @param logId ID
* @return
*/
int deleteZipFileProcessLog(@Param("logId") String logId);
/**
* ZIP .
*
* @param logId ID
* @return ZIP
*/
ZipFileProcessLogVO selectZipFileProcessLog(@Param("logId") String logId);
/**
* ZIP .
*
* @param paramVO
* @return
*/
int selectZipFileProcessLogListTotalCount(ZipFileProcessLogVO paramVO);
/**
* ZIP .
*
* @param paramVO
* @return ZIP
*/
List<ZipFileProcessLogVO> selectZipFileProcessLogList(ZipFileProcessLogVO paramVO);
/**
* ZIP .
*
* @param zipFileName ZIP
* @return ZIP
*/
ZipFileProcessLogVO selectProcessingZipFileLog(@Param("zipFileName") String zipFileName);
}

@ -21,34 +21,77 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
public class BatchJobResult {
/**
*
*/
private String status;
/**
*
*/
private int totalItems;
/**
*
*/
private int processedItems;
/**
* ( )
*/
private String errorMessage;
/**
*
*/
private int successItems;
/**
*
*/
private int errorItems;
/**
* .
*/
public void incrementProcessed() {
this.processedItems++;
}
/**
* .
*/
public void incrementSuccess() {
this.successItems++;
}
/**
* .
*/
public void incrementError() {
this.errorItems++;
}
/**
* .
*/
public int getProcessedCount() {
return this.processedItems;
}
/**
* .
*/
public int getSuccessCount() {
return this.successItems;
}
/**
* .
* .
*
* @return
* .
*/
public boolean isPartiallyCompleted() {
return processedItems > 0 && processedItems < totalItems;
public int getErrorCount() {
return this.errorItems;
}
}
}

@ -0,0 +1,88 @@
package go.kr.project.batch.model;
import go.kr.project.common.model.PagingVO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* packageName : go.kr.project.batch.model
* fileName : ZipFileDetailLogVO
* author :
* date : 2025-01-27
* description : ZIP VO
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-01-27
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ZipFileDetailLogVO extends PagingVO {
/**
* ID
*/
private String detailLogId;
/**
* ID
*/
private String logId;
/**
*
*/
private String fileName;
/**
*
*/
private String filePath;
/**
* (bytes)
*/
private Long fileSize;
/**
*
*/
private String fileType;
/**
* (Y/N)
*/
private String isImage;
/**
* (Y/N)
*/
private String isCorrupted;
/**
* (SUCCESS/ERROR)
*/
private String processStatus;
/**
*
*/
private String errorMessage;
/**
*
*/
private LocalDateTime processTime;
/**
*
*/
private LocalDateTime regDate;
/**
* ID
*/
private String regUserId;
}

@ -0,0 +1,118 @@
package go.kr.project.batch.model;
import go.kr.project.common.model.PagingVO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* packageName : go.kr.project.batch.model
* fileName : ZipFileProcessLogVO
* author :
* date : 2025-01-27
* description : ZIP VO
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-01-27
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ZipFileProcessLogVO extends PagingVO {
/**
* ID
*/
private String logId;
/**
* ZIP
*/
private String zipFileName;
/**
* ZIP
*/
private String zipFilePath;
/**
*
*/
private String extractPath;
/**
*
*/
private String archivePath;
/**
*
*/
private Integer totalFiles;
/**
*
*/
private Integer successFiles;
/**
*
*/
private Integer errorFiles;
/**
*
*/
private Integer imageFiles;
/**
*
*/
private Integer nonImageFiles;
/**
*
*/
private Integer corruptedFiles;
/**
* (PROCESSING/SUCCESS/ERROR)
*/
private String processStatus;
/**
*
*/
private String errorMessage;
/**
*
*/
private LocalDateTime startTime;
/**
*
*/
private LocalDateTime endTime;
/**
*
*/
private LocalDateTime regDate;
/**
* ID
*/
private String regUserId;
/**
*
*/
private LocalDateTime modDate;
/**
* ID
*/
private String modUserId;
}

@ -0,0 +1,90 @@
package go.kr.project.batch.service;
import go.kr.project.batch.model.ZipFileDetailLogVO;
import java.util.List;
/**
* packageName : go.kr.project.batch.service
* fileName : ZipFileDetailLogService
* author :
* date : 2025-01-27
* description : ZIP
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-01-27
*/
public interface ZipFileDetailLogService {
/**
* ZIP ID .
*
* @return ID
*/
String generateZipFileDetailLogId();
/**
* ZIP .
*
* @param zipFileDetailLog ZIP
* @return
*/
int insertZipFileDetailLog(ZipFileDetailLogVO zipFileDetailLog);
/**
* ZIP .
*
* @param zipFileDetailLogList ZIP
* @return
*/
int insertZipFileDetailLogList(List<ZipFileDetailLogVO> zipFileDetailLogList);
/**
* ZIP .
*
* @param detailLogId ID
* @return
*/
int deleteZipFileDetailLog(String detailLogId);
/**
* ID ZIP .
*
* @param logId ID
* @return
*/
int deleteZipFileDetailLogByLogId(String logId);
/**
* ZIP .
*
* @param detailLogId ID
* @return ZIP
*/
ZipFileDetailLogVO selectZipFileDetailLog(String detailLogId);
/**
* ID ZIP .
*
* @param logId ID
* @return ZIP
*/
List<ZipFileDetailLogVO> selectZipFileDetailLogListByLogId(String logId);
/**
* ZIP .
*
* @param paramVO
* @return
*/
int selectZipFileDetailLogListTotalCount(ZipFileDetailLogVO paramVO);
/**
* ZIP .
*
* @param paramVO
* @return ZIP
*/
List<ZipFileDetailLogVO> selectZipFileDetailLogList(ZipFileDetailLogVO paramVO);
}

@ -0,0 +1,82 @@
package go.kr.project.batch.service;
import go.kr.project.batch.model.ZipFileProcessLogVO;
import java.util.List;
/**
* packageName : go.kr.project.batch.service
* fileName : ZipFileProcessLogService
* author :
* date : 2025-01-27
* description : ZIP
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-01-27
*/
public interface ZipFileProcessLogService {
/**
* ZIP ID .
*
* @return ID
*/
String generateZipFileProcessLogId();
/**
* ZIP .
*
* @param zipFileProcessLog ZIP
* @return
*/
int insertZipFileProcessLog(ZipFileProcessLogVO zipFileProcessLog);
/**
* ZIP .
*
* @param zipFileProcessLog ZIP
* @return
*/
int updateZipFileProcessLog(ZipFileProcessLogVO zipFileProcessLog);
/**
* ZIP .
*
* @param logId ID
* @return
*/
int deleteZipFileProcessLog(String logId);
/**
* ZIP .
*
* @param logId ID
* @return ZIP
*/
ZipFileProcessLogVO selectZipFileProcessLog(String logId);
/**
* ZIP .
*
* @param paramVO
* @return
*/
int selectZipFileProcessLogListTotalCount(ZipFileProcessLogVO paramVO);
/**
* ZIP .
*
* @param paramVO
* @return ZIP
*/
List<ZipFileProcessLogVO> selectZipFileProcessLogList(ZipFileProcessLogVO paramVO);
/**
* ZIP .
*
* @param zipFileName ZIP
* @return ZIP
*/
ZipFileProcessLogVO selectProcessingZipFileLog(String zipFileName);
}

@ -0,0 +1,184 @@
package go.kr.project.batch.service.impl;
import go.kr.project.batch.mapper.ZipFileDetailLogMapper;
import go.kr.project.batch.model.ZipFileDetailLogVO;
import go.kr.project.batch.service.ZipFileDetailLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* packageName : go.kr.project.batch.service.impl
* fileName : ZipFileDetailLogServiceImpl
* author :
* date : 2025-01-27
* description : ZIP
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-01-27
*/
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ZipFileDetailLogServiceImpl extends EgovAbstractServiceImpl implements ZipFileDetailLogService {
private final ZipFileDetailLogMapper zipFileDetailLogMapper;
/**
* ZIP ID .
*
* @return ID
*/
@Override
public String generateZipFileDetailLogId() {
try {
return zipFileDetailLogMapper.generateZipFileDetailLogId();
} catch (Exception e) {
log.error("ZIP 파일 처리 상세 로그 ID 생성 중 오류 발생", e);
throw new RuntimeException("ZIP 파일 처리 상세 로그 ID 생성 실패", e);
}
}
/**
* ZIP .
*
* @param zipFileDetailLog ZIP
* @return
*/
@Override
@Transactional
public int insertZipFileDetailLog(ZipFileDetailLogVO zipFileDetailLog) {
try {
return zipFileDetailLogMapper.insertZipFileDetailLog(zipFileDetailLog);
} catch (Exception e) {
log.error("ZIP 파일 처리 상세 로그 등록 중 오류 발생: {}", zipFileDetailLog, e);
throw new RuntimeException("ZIP 파일 처리 상세 로그 등록 실패", e);
}
}
/**
* ZIP .
*
* @param zipFileDetailLogList ZIP
* @return
*/
@Override
@Transactional
public int insertZipFileDetailLogList(List<ZipFileDetailLogVO> zipFileDetailLogList) {
try {
if (zipFileDetailLogList == null || zipFileDetailLogList.isEmpty()) {
log.warn("등록할 ZIP 파일 처리 상세 로그 목록이 비어있습니다.");
return 0;
}
return zipFileDetailLogMapper.insertZipFileDetailLogList(zipFileDetailLogList);
} catch (Exception e) {
log.error("ZIP 파일 처리 상세 로그 목록 일괄 등록 중 오류 발생: size={}",
zipFileDetailLogList != null ? zipFileDetailLogList.size() : 0, e);
throw new RuntimeException("ZIP 파일 처리 상세 로그 목록 일괄 등록 실패", e);
}
}
/**
* ZIP .
*
* @param detailLogId ID
* @return
*/
@Override
@Transactional
public int deleteZipFileDetailLog(String detailLogId) {
try {
return zipFileDetailLogMapper.deleteZipFileDetailLog(detailLogId);
} catch (Exception e) {
log.error("ZIP 파일 처리 상세 로그 삭제 중 오류 발생: detailLogId={}", detailLogId, e);
throw new RuntimeException("ZIP 파일 처리 상세 로그 삭제 실패", e);
}
}
/**
* ID ZIP .
*
* @param logId ID
* @return
*/
@Override
@Transactional
public int deleteZipFileDetailLogByLogId(String logId) {
try {
return zipFileDetailLogMapper.deleteZipFileDetailLogByLogId(logId);
} catch (Exception e) {
log.error("로그 ID로 ZIP 파일 처리 상세 로그 삭제 중 오류 발생: logId={}", logId, e);
throw new RuntimeException("로그 ID로 ZIP 파일 처리 상세 로그 삭제 실패", e);
}
}
/**
* ZIP .
*
* @param detailLogId ID
* @return ZIP
*/
@Override
public ZipFileDetailLogVO selectZipFileDetailLog(String detailLogId) {
try {
return zipFileDetailLogMapper.selectZipFileDetailLog(detailLogId);
} catch (Exception e) {
log.error("ZIP 파일 처리 상세 로그 조회 중 오류 발생: detailLogId={}", detailLogId, e);
throw new RuntimeException("ZIP 파일 처리 상세 로그 조회 실패", e);
}
}
/**
* ID ZIP .
*
* @param logId ID
* @return ZIP
*/
@Override
public List<ZipFileDetailLogVO> selectZipFileDetailLogListByLogId(String logId) {
try {
return zipFileDetailLogMapper.selectZipFileDetailLogListByLogId(logId);
} catch (Exception e) {
log.error("로그 ID로 ZIP 파일 처리 상세 로그 목록 조회 중 오류 발생: logId={}", logId, e);
throw new RuntimeException("로그 ID로 ZIP 파일 처리 상세 로그 목록 조회 실패", e);
}
}
/**
* ZIP .
*
* @param paramVO
* @return
*/
@Override
public int selectZipFileDetailLogListTotalCount(ZipFileDetailLogVO paramVO) {
try {
return zipFileDetailLogMapper.selectZipFileDetailLogListTotalCount(paramVO);
} catch (Exception e) {
log.error("ZIP 파일 처리 상세 로그 목록 총 개수 조회 중 오류 발생: {}", paramVO, e);
throw new RuntimeException("ZIP 파일 처리 상세 로그 목록 총 개수 조회 실패", e);
}
}
/**
* ZIP .
*
* @param paramVO
* @return ZIP
*/
@Override
public List<ZipFileDetailLogVO> selectZipFileDetailLogList(ZipFileDetailLogVO paramVO) {
try {
return zipFileDetailLogMapper.selectZipFileDetailLogList(paramVO);
} catch (Exception e) {
log.error("ZIP 파일 처리 상세 로그 목록 조회 중 오류 발생: {}", paramVO, e);
throw new RuntimeException("ZIP 파일 처리 상세 로그 목록 조회 실패", e);
}
}
}

@ -0,0 +1,162 @@
package go.kr.project.batch.service.impl;
import go.kr.project.batch.mapper.ZipFileProcessLogMapper;
import go.kr.project.batch.model.ZipFileProcessLogVO;
import go.kr.project.batch.service.ZipFileProcessLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* packageName : go.kr.project.batch.service.impl
* fileName : ZipFileProcessLogServiceImpl
* author :
* date : 2025-01-27
* description : ZIP
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-01-27
*/
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ZipFileProcessLogServiceImpl extends EgovAbstractServiceImpl implements ZipFileProcessLogService {
private final ZipFileProcessLogMapper zipFileProcessLogMapper;
/**
* ZIP ID .
*
* @return ID
*/
@Override
public String generateZipFileProcessLogId() {
try {
return zipFileProcessLogMapper.generateZipFileProcessLogId();
} catch (Exception e) {
log.error("ZIP 파일 처리 로그 ID 생성 중 오류 발생", e);
throw new RuntimeException("ZIP 파일 처리 로그 ID 생성 실패", e);
}
}
/**
* ZIP .
*
* @param zipFileProcessLog ZIP
* @return
*/
@Override
@Transactional
public int insertZipFileProcessLog(ZipFileProcessLogVO zipFileProcessLog) {
try {
return zipFileProcessLogMapper.insertZipFileProcessLog(zipFileProcessLog);
} catch (Exception e) {
log.error("ZIP 파일 처리 로그 등록 중 오류 발생: {}", zipFileProcessLog, e);
throw new RuntimeException("ZIP 파일 처리 로그 등록 실패", e);
}
}
/**
* ZIP .
*
* @param zipFileProcessLog ZIP
* @return
*/
@Override
@Transactional
public int updateZipFileProcessLog(ZipFileProcessLogVO zipFileProcessLog) {
try {
return zipFileProcessLogMapper.updateZipFileProcessLog(zipFileProcessLog);
} catch (Exception e) {
log.error("ZIP 파일 처리 로그 수정 중 오류 발생: {}", zipFileProcessLog, e);
throw new RuntimeException("ZIP 파일 처리 로그 수정 실패", e);
}
}
/**
* ZIP .
*
* @param logId ID
* @return
*/
@Override
@Transactional
public int deleteZipFileProcessLog(String logId) {
try {
return zipFileProcessLogMapper.deleteZipFileProcessLog(logId);
} catch (Exception e) {
log.error("ZIP 파일 처리 로그 삭제 중 오류 발생: logId={}", logId, e);
throw new RuntimeException("ZIP 파일 처리 로그 삭제 실패", e);
}
}
/**
* ZIP .
*
* @param logId ID
* @return ZIP
*/
@Override
public ZipFileProcessLogVO selectZipFileProcessLog(String logId) {
try {
return zipFileProcessLogMapper.selectZipFileProcessLog(logId);
} catch (Exception e) {
log.error("ZIP 파일 처리 로그 조회 중 오류 발생: logId={}", logId, e);
throw new RuntimeException("ZIP 파일 처리 로그 조회 실패", e);
}
}
/**
* ZIP .
*
* @param paramVO
* @return
*/
@Override
public int selectZipFileProcessLogListTotalCount(ZipFileProcessLogVO paramVO) {
try {
return zipFileProcessLogMapper.selectZipFileProcessLogListTotalCount(paramVO);
} catch (Exception e) {
log.error("ZIP 파일 처리 로그 목록 총 개수 조회 중 오류 발생: {}", paramVO, e);
throw new RuntimeException("ZIP 파일 처리 로그 목록 총 개수 조회 실패", e);
}
}
/**
* ZIP .
*
* @param paramVO
* @return ZIP
*/
@Override
public List<ZipFileProcessLogVO> selectZipFileProcessLogList(ZipFileProcessLogVO paramVO) {
try {
return zipFileProcessLogMapper.selectZipFileProcessLogList(paramVO);
} catch (Exception e) {
log.error("ZIP 파일 처리 로그 목록 조회 중 오류 발생: {}", paramVO, e);
throw new RuntimeException("ZIP 파일 처리 로그 목록 조회 실패", e);
}
}
/**
* ZIP .
*
* @param zipFileName ZIP
* @return ZIP
*/
@Override
public ZipFileProcessLogVO selectProcessingZipFileLog(String zipFileName) {
try {
return zipFileProcessLogMapper.selectProcessingZipFileLog(zipFileName);
} catch (Exception e) {
log.error("처리 중인 ZIP 파일 로그 조회 중 오류 발생: zipFileName={}", zipFileName, e);
throw new RuntimeException("처리 중인 ZIP 파일 로그 조회 실패", e);
}
}
}

@ -0,0 +1,170 @@
package go.kr.project.batch.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();
}
}
}

@ -84,4 +84,9 @@ batch:
retry:
max-retry-count: 3 # 최대 재시도 횟수
retry-interval-hours: 1 # 재시도 간격 (시간)
enabled: true # 에러 파일 재처리 기능 활성화 여부
enabled: true # 에러 파일 재처리 기능 활성화 여부
zip:
source-dir: /data/xit-framework/batch/zip/source # ZIP 파일 소스 디렉토리
extract-dir: /data/xit-framework/batch/zip/extract # ZIP 파일 압축 해제 디렉토리
archive-dir: /data/xit-framework/batch/zip/archive # ZIP 파일 아카이브 디렉토리
create-date-subdir: true # yyyymmdd 하위 디렉토리 생성 여부

@ -89,4 +89,9 @@ batch:
retry:
max-retry-count: 3 # 최대 재시도 횟수
retry-interval-hours: 1 # 재시도 간격 (시간)
enabled: true # 에러 파일 재처리 기능 활성화 여부
enabled: true # 에러 파일 재처리 기능 활성화 여부
zip:
source-dir: d:/data/xit-framework/batch/zip/source # ZIP 파일 소스 디렉토리
extract-dir: d:/data/xit-framework/batch/zip/extract # ZIP 파일 압축 해제 디렉토리
archive-dir: d:/data/xit-framework/batch/zip/archive # ZIP 파일 아카이브 디렉토리
create-date-subdir: true # yyyymmdd 하위 디렉토리 생성 여부

@ -84,4 +84,9 @@ batch:
retry:
max-retry-count: 3 # 최대 재시도 횟수
retry-interval-hours: 1 # 재시도 간격 (시간)
enabled: true # 에러 파일 재처리 기능 활성화 여부
enabled: true # 에러 파일 재처리 기능 활성화 여부
zip:
source-dir: /data/xit-framework/batch/zip/source # ZIP 파일 소스 디렉토리
extract-dir: /data/xit-framework/batch/zip/extract # ZIP 파일 압축 해제 디렉토리
archive-dir: /data/xit-framework/batch/zip/archive # ZIP 파일 아카이브 디렉토리
create-date-subdir: true # yyyymmdd 하위 디렉토리 생성 여부

@ -114,7 +114,6 @@ query-executor:
transaction:
timeout-minutes: 5 # 트랜잭션 타임아웃 (분 단위, 기본값: 5분)
# Interceptor configuration
interceptor:
interceptorExclude:

@ -0,0 +1,197 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="go.kr.project.batch.mapper.ZipFileDetailLogMapper">
<!-- ZIP 파일 처리 상세 로그 ID 생성 -->
<select id="generateZipFileDetailLogId" resultType="String">
SELECT CONCAT('ZFDL', LPAD(NEXTVAL(seq_zip_file_detail_log_id), 8, '0'))
</select>
<!-- ZIP 파일 처리 상세 로그 등록 -->
<insert id="insertZipFileDetailLog" parameterType="go.kr.project.batch.model.ZipFileDetailLogVO">
<selectKey keyProperty="detailLogId" resultType="String" order="BEFORE">
SELECT CONCAT('ZFDL', LPAD(NEXTVAL(seq_zip_file_detail_log_id), 8, '0'))
</selectKey>
INSERT INTO tb_zip_file_detail_log (
detail_log_id,
log_id,
file_name,
file_path,
file_size,
file_type,
is_image,
is_corrupted,
process_status,
error_message,
process_time,
reg_date,
reg_user_id
) VALUES (
#{detailLogId},
#{logId},
#{fileName},
#{filePath},
#{fileSize},
#{fileType},
#{isImage},
#{isCorrupted},
#{processStatus},
#{errorMessage},
#{processTime},
NOW(),
#{regUserId}
)
</insert>
<!-- ZIP 파일 처리 상세 로그 목록 일괄 등록 -->
<insert id="insertZipFileDetailLogList" parameterType="java.util.List">
INSERT INTO tb_zip_file_detail_log (
detail_log_id,
log_id,
file_name,
file_path,
file_size,
file_type,
is_image,
is_corrupted,
process_status,
error_message,
process_time,
reg_date,
reg_user_id
) VALUES
<foreach collection="list" item="item" separator=",">
(
CONCAT('ZFDL', LPAD(NEXTVAL(seq_zip_file_detail_log_id), 8, '0')),
#{item.logId},
#{item.fileName},
#{item.filePath},
#{item.fileSize},
#{item.fileType},
#{item.isImage},
#{item.isCorrupted},
#{item.processStatus},
#{item.errorMessage},
#{item.processTime},
NOW(),
#{item.regUserId}
)
</foreach>
</insert>
<!-- ZIP 파일 처리 상세 로그 삭제 -->
<delete id="deleteZipFileDetailLog" parameterType="String">
DELETE FROM tb_zip_file_detail_log
WHERE detail_log_id = #{detailLogId}
</delete>
<!-- 로그 ID로 ZIP 파일 처리 상세 로그 삭제 -->
<delete id="deleteZipFileDetailLogByLogId" parameterType="String">
DELETE FROM tb_zip_file_detail_log
WHERE log_id = #{logId}
</delete>
<!-- ZIP 파일 처리 상세 로그 조회 -->
<select id="selectZipFileDetailLog" parameterType="String" resultType="go.kr.project.batch.model.ZipFileDetailLogVO">
SELECT
detail_log_id AS detailLogId,
log_id AS logId,
file_name AS fileName,
file_path AS filePath,
file_size AS fileSize,
file_type AS fileType,
is_image AS isImage,
is_corrupted AS isCorrupted,
process_status AS processStatus,
error_message AS errorMessage,
process_time AS processTime,
reg_date AS regDate,
reg_user_id AS regUserId
FROM tb_zip_file_detail_log
WHERE detail_log_id = #{detailLogId}
</select>
<!-- 로그 ID로 ZIP 파일 처리 상세 로그 목록 조회 -->
<select id="selectZipFileDetailLogListByLogId" parameterType="String" resultType="go.kr.project.batch.model.ZipFileDetailLogVO">
SELECT
detail_log_id AS detailLogId,
log_id AS logId,
file_name AS fileName,
file_path AS filePath,
file_size AS fileSize,
file_type AS fileType,
is_image AS isImage,
is_corrupted AS isCorrupted,
process_status AS processStatus,
error_message AS errorMessage,
process_time AS processTime,
reg_date AS regDate,
reg_user_id AS regUserId
FROM tb_zip_file_detail_log
WHERE log_id = #{logId}
ORDER BY process_time ASC
</select>
<!-- ZIP 파일 처리 상세 로그 목록 총 개수 조회 -->
<select id="selectZipFileDetailLogListTotalCount" parameterType="go.kr.project.batch.model.ZipFileDetailLogVO" resultType="int">
SELECT COUNT(*)
FROM tb_zip_file_detail_log
WHERE 1=1
<if test="logId != null and logId != ''">
AND log_id = #{logId}
</if>
<if test="fileName != null and fileName != ''">
AND file_name LIKE CONCAT('%', #{fileName}, '%')
</if>
<if test="processStatus != null and processStatus != ''">
AND process_status = #{processStatus}
</if>
<if test="isImage != null and isImage != ''">
AND is_image = #{isImage}
</if>
<if test="isCorrupted != null and isCorrupted != ''">
AND is_corrupted = #{isCorrupted}
</if>
</select>
<!-- ZIP 파일 처리 상세 로그 목록 조회 -->
<select id="selectZipFileDetailLogList" parameterType="go.kr.project.batch.model.ZipFileDetailLogVO" resultType="go.kr.project.batch.model.ZipFileDetailLogVO">
SELECT
detail_log_id AS detailLogId,
log_id AS logId,
file_name AS fileName,
file_path AS filePath,
file_size AS fileSize,
file_type AS fileType,
is_image AS isImage,
is_corrupted AS isCorrupted,
process_status AS processStatus,
error_message AS errorMessage,
process_time AS processTime,
reg_date AS regDate,
reg_user_id AS regUserId
FROM tb_zip_file_detail_log
WHERE 1=1
<if test="logId != null and logId != ''">
AND log_id = #{logId}
</if>
<if test="fileName != null and fileName != ''">
AND file_name LIKE CONCAT('%', #{fileName}, '%')
</if>
<if test="processStatus != null and processStatus != ''">
AND process_status = #{processStatus}
</if>
<if test="isImage != null and isImage != ''">
AND is_image = #{isImage}
</if>
<if test="isCorrupted != null and isCorrupted != ''">
AND is_corrupted = #{isCorrupted}
</if>
ORDER BY process_time ASC
<if test="pagingYn == 'Y'">
LIMIT #{startIndex}, #{recordCountPerPage}
</if>
</select>
</mapper>

@ -0,0 +1,186 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="go.kr.project.batch.mapper.ZipFileProcessLogMapper">
<!-- ZIP 파일 처리 로그 ID 생성 -->
<select id="generateZipFileProcessLogId" resultType="String">
SELECT CONCAT('ZPFL', LPAD(NEXTVAL(seq_zip_file_process_log_id), 8, '0'))
</select>
<!-- ZIP 파일 처리 로그 등록 -->
<insert id="insertZipFileProcessLog" parameterType="go.kr.project.batch.model.ZipFileProcessLogVO">
INSERT INTO tb_zip_file_process_log (
log_id,
zip_file_name,
zip_file_path,
extract_path,
archive_path,
total_files,
success_files,
error_files,
image_files,
non_image_files,
corrupted_files,
process_status,
error_message,
start_time,
end_time,
reg_date,
reg_user_id,
mod_date,
mod_user_id
) VALUES (
#{logId},
#{zipFileName},
#{zipFilePath},
#{extractPath},
#{archivePath},
#{totalFiles},
#{successFiles},
#{errorFiles},
#{imageFiles},
#{nonImageFiles},
#{corruptedFiles},
#{processStatus},
#{errorMessage},
#{startTime},
#{endTime},
NOW(),
#{regUserId},
NOW(),
#{modUserId}
)
</insert>
<!-- ZIP 파일 처리 로그 수정 -->
<update id="updateZipFileProcessLog" parameterType="go.kr.project.batch.model.ZipFileProcessLogVO">
UPDATE tb_zip_file_process_log
SET
extract_path = #{extractPath},
archive_path = #{archivePath},
total_files = #{totalFiles},
success_files = #{successFiles},
error_files = #{errorFiles},
image_files = #{imageFiles},
non_image_files = #{nonImageFiles},
corrupted_files = #{corruptedFiles},
process_status = #{processStatus},
error_message = #{errorMessage},
end_time = #{endTime},
mod_date = NOW(),
mod_user_id = #{modUserId}
WHERE log_id = #{logId}
</update>
<!-- ZIP 파일 처리 로그 삭제 -->
<delete id="deleteZipFileProcessLog" parameterType="String">
DELETE FROM tb_zip_file_process_log
WHERE log_id = #{logId}
</delete>
<!-- ZIP 파일 처리 로그 조회 -->
<select id="selectZipFileProcessLog" parameterType="String" resultType="go.kr.project.batch.model.ZipFileProcessLogVO">
SELECT
log_id AS logId,
zip_file_name AS zipFileName,
zip_file_path AS zipFilePath,
extract_path AS extractPath,
archive_path AS archivePath,
total_files AS totalFiles,
success_files AS successFiles,
error_files AS errorFiles,
image_files AS imageFiles,
non_image_files AS nonImageFiles,
corrupted_files AS corruptedFiles,
process_status AS processStatus,
error_message AS errorMessage,
start_time AS startTime,
end_time AS endTime,
reg_date AS regDate,
reg_user_id AS regUserId,
mod_date AS modDate,
mod_user_id AS modUserId
FROM tb_zip_file_process_log
WHERE log_id = #{logId}
</select>
<!-- ZIP 파일 처리 로그 목록 총 개수 조회 -->
<select id="selectZipFileProcessLogListTotalCount" parameterType="go.kr.project.batch.model.ZipFileProcessLogVO" resultType="int">
SELECT COUNT(*)
FROM tb_zip_file_process_log
WHERE 1=1
<if test="zipFileName != null and zipFileName != ''">
AND zip_file_name LIKE CONCAT('%', #{zipFileName}, '%')
</if>
<if test="processStatus != null and processStatus != ''">
AND process_status = #{processStatus}
</if>
</select>
<!-- ZIP 파일 처리 로그 목록 조회 -->
<select id="selectZipFileProcessLogList" parameterType="go.kr.project.batch.model.ZipFileProcessLogVO" resultType="go.kr.project.batch.model.ZipFileProcessLogVO">
SELECT
log_id AS logId,
zip_file_name AS zipFileName,
zip_file_path AS zipFilePath,
extract_path AS extractPath,
archive_path AS archivePath,
total_files AS totalFiles,
success_files AS successFiles,
error_files AS errorFiles,
image_files AS imageFiles,
non_image_files AS nonImageFiles,
corrupted_files AS corruptedFiles,
process_status AS processStatus,
error_message AS errorMessage,
start_time AS startTime,
end_time AS endTime,
reg_date AS regDate,
reg_user_id AS regUserId,
mod_date AS modDate,
mod_user_id AS modUserId
FROM tb_zip_file_process_log
WHERE 1=1
<if test="zipFileName != null and zipFileName != ''">
AND zip_file_name LIKE CONCAT('%', #{zipFileName}, '%')
</if>
<if test="processStatus != null and processStatus != ''">
AND process_status = #{processStatus}
</if>
ORDER BY start_time DESC
<if test="pagingYn == 'Y'">
LIMIT #{startIndex}, #{recordCountPerPage}
</if>
</select>
<!-- 처리 중인 ZIP 파일 로그 조회 -->
<select id="selectProcessingZipFileLog" parameterType="String" resultType="go.kr.project.batch.model.ZipFileProcessLogVO">
SELECT
log_id AS logId,
zip_file_name AS zipFileName,
zip_file_path AS zipFilePath,
extract_path AS extractPath,
archive_path AS archivePath,
total_files AS totalFiles,
success_files AS successFiles,
error_files AS errorFiles,
image_files AS imageFiles,
non_image_files AS nonImageFiles,
corrupted_files AS corruptedFiles,
process_status AS processStatus,
error_message AS errorMessage,
start_time AS startTime,
end_time AS endTime,
reg_date AS regDate,
reg_user_id AS regUserId,
mod_date AS modDate,
mod_user_id AS modUserId
FROM tb_zip_file_process_log
WHERE zip_file_name = #{zipFileName}
AND process_status = 'PROCESSING'
ORDER BY start_time DESC
LIMIT 1
</select>
</mapper>
Loading…
Cancel
Save