This commit is contained in:
mindol1004
2024-11-15 17:58:46 +09:00
parent fc98b4ffc0
commit b01e150c9a
5 changed files with 2381 additions and 115 deletions

View File

@@ -8,7 +8,6 @@ import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;
import com.spring.domain.email.service.EmailSendService;
import com.spring.domain.post.repository.PostRepository;
import com.spring.infra.batch.AbstractBatchTask;
import com.spring.infra.batch.BatchJobInfo;
@@ -26,12 +25,10 @@ import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor
public class EmailSendBatch extends AbstractBatchTask {
private final PostRepository postRepository;
private final EmailSendService emailSendService;
@Override
protected List<Step> createSteps() {
// log.info("EmailSendBatch -> createSteps");
return List.of(
addStep("emailSendJobStep1", createTasklet()),
addStep("emailSendJobStep2", createSendTasklet())
@@ -43,13 +40,11 @@ public class EmailSendBatch extends AbstractBatchTask {
log.info("EmailSendBatch -> createTasklet");
return ((contribution, chunkContext) -> {
emailSendService.sendEmail();
// postRepository.findAll();
return RepeatStatus.FINISHED;
});
}
private Tasklet createSendTasklet() {
// log.info("EmailSendBatch -> createSendTasklet");
return ((contribution, chunkContext) -> RepeatStatus.FINISHED);
}

View File

@@ -16,31 +16,31 @@ import com.spring.infra.db.orm.jpa.SecondaryJpaConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
// @Slf4j
// @Component
// @BatchJobInfo(
// group = "${batch-info.post-batch.group}",
// jobName = "${batch-info.post-batch.job-name}",
// cronExpression = "${batch-info.post-batch.cron-expression}",
// description = "${batch-info.post-batch.description}"
// )
// @RequiredArgsConstructor
public class PostCreateBatch { //extends AbstractBatchTask {
@Slf4j
@Component
@BatchJobInfo(
group = "${batch-info.post-batch.group}",
jobName = "${batch-info.post-batch.job-name}",
cronExpression = "${batch-info.post-batch.cron-expression}",
description = "${batch-info.post-batch.description}"
)
@RequiredArgsConstructor
public class PostCreateBatch extends AbstractBatchTask {
// private final PostMapper postMapper;
private final PostMapper postMapper;
// @Autowired
// @Override
// public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
// super.setTransactionManager(transactionManager);
// }
@Autowired
@Override
public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
super.setTransactionManager(transactionManager);
}
// @Override
// protected Tasklet createTasklet() {
// return ((contribution, chunkContext) -> {
// postMapper.save(Post.builder().title("testTitle").content("testPost").build());
// return RepeatStatus.FINISHED;
// });
// }
@Override
protected Tasklet createTasklet() {
return ((contribution, chunkContext) -> {
postMapper.save(Post.builder().title("testTitle").content("testPost").build());
return RepeatStatus.FINISHED;
});
}
}

View File

@@ -36,106 +36,105 @@ import com.spring.infra.db.orm.jpa.SecondaryJpaConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
// @Slf4j
// @Component
// @BatchJobInfo(
// group = "${batch-info.post-create-batch.group}",
// jobName = "${batch-info.post-create-batch.job-name}",
// cronExpression = "${batch-info.post-create-batch.cron-expression}",
// description = "${batch-info.post-create-batch.description}"
// )
// @RequiredArgsConstructor
public class PostCreateBatchChunk { //extends AbstractBatchChunk {
@Slf4j
@Component
@BatchJobInfo(
group = "${batch-info.post-create-batch.group}",
jobName = "${batch-info.post-create-batch.job-name}",
cronExpression = "${batch-info.post-create-batch.cron-expression}",
description = "${batch-info.post-create-batch.description}"
)
@RequiredArgsConstructor
public class PostCreateBatchChunk extends AbstractBatchChunk {
// @Autowired
// @Override
// public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
// super.setTransactionManager(transactionManager);
// }
@Autowired
@Override
public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
super.setTransactionManager(transactionManager);
}
// @Qualifier(SecondaryJpaConfig.ENTITY_MANAGER_FACTORY)
// private final EntityManagerFactory entityManagerFactory;
@Qualifier(SecondaryJpaConfig.ENTITY_MANAGER_FACTORY)
private final EntityManagerFactory entityManagerFactory;
// private final PostRepository postRepository;
// private final PostBackUpRepository postBackUpRepository;
private final PostRepository postRepository;
private final PostBackUpRepository postBackUpRepository;
// private List<Post> list = new ArrayList<>();
private List<Post> list = new ArrayList<>();
// @Override
// public Job createJob() {
// return new JobBuilder(batchJobInfoData.getJobName())
// .repository(jobRepository)
// .incrementer(new RunIdIncrementer())
// .start(readListStep())
// .next(decider())
// .from(decider()).on("PROCESS").to(processStep())
// .from(decider()).on("TERMINATE").to(terminateStep())
// .end()
// .build();
// }
@Override
public Job createJob() {
return new JobBuilder(batchJobInfoData.getJobName())
.repository(jobRepository)
// .incrementer(new RunIdIncrementer())
.start(processStep())
// .next(decider())
// .from(decider()).on("PROCESS").to(processStep())
// .from(decider()).on("TERMINATE").to(terminateStep())
.build();
}
// private JobExecutionDecider decider() {
// return (JobExecution jobExecution, StepExecution stepExecution) ->
// !list.isEmpty() ? new FlowExecutionStatus("PROCESS") : new FlowExecutionStatus("TERMINATE");
// }
private JobExecutionDecider decider() {
return (JobExecution jobExecution, StepExecution stepExecution) ->
!list.isEmpty() ? new FlowExecutionStatus("PROCESS") : new FlowExecutionStatus("TERMINATE");
}
// private Step readListStep() {
// return new StepBuilder("readListStep")
// .repository(jobRepository)
// .transactionManager(transactionManager)
// .tasklet(readListTasklet())
// .build();
// }
private Step readListStep() {
return new StepBuilder("readListStep")
.repository(jobRepository)
.transactionManager(transactionManager)
.tasklet(readListTasklet())
.build();
}
// private Tasklet readListTasklet() {
// return (contribution, chunkContext) -> {
// list = postRepository.findAll();
// return RepeatStatus.FINISHED;
// };
// }
private Tasklet readListTasklet() {
return (contribution, chunkContext) -> {
list = postRepository.findAll();
return RepeatStatus.FINISHED;
};
}
// private Step processStep() {
// return new StepBuilder("processStep")
// .repository(jobRepository)
// .transactionManager(transactionManager)
// .<Post, PostBackUp>chunk(5)
// .reader(testReader())
// .processor(testProcessor())
// .writer(testWriter())
// .build();
// }
private Step processStep() {
return new StepBuilder("processStep")
.repository(jobRepository)
.transactionManager(transactionManager)
.<Post, PostBackUp>chunk(5)
.reader(testReader())
.processor(testProcessor())
.writer(testWriter())
.build();
}
// private JpaPagingItemReader<Post> testReader() {
// return new JpaPagingItemReaderBuilder<Post>()
// .name("testReader")
// .entityManagerFactory(entityManagerFactory)
// .pageSize(5)
// .queryString("select p from Post p")
// .build();
// }
private JpaPagingItemReader<Post> testReader() {
return new JpaPagingItemReaderBuilder<Post>()
.name("testReader")
.entityManagerFactory(entityManagerFactory)
.pageSize(5)
.queryString("select p from Post p")
.build();
}
// private ItemProcessor<Post, PostBackUp> testProcessor() {
// return post ->
// PostBackUp.builder().postId(post.getPostId()).content(post.getContent()).title(post.getTitle()).build();
// }
private ItemProcessor<Post, PostBackUp> testProcessor() {
return post ->
PostBackUp.builder().postId(post.getPostId()).content(post.getContent()).title(post.getTitle()).build();
}
// private ItemWriter<PostBackUp> testWriter() {
// return postBackUpRepository::saveAll;
// }
private ItemWriter<PostBackUp> testWriter() {
return postBackUpRepository::saveAll;
}
// private Step terminateStep() {
// return new StepBuilder("terminateStep")
// .repository(jobRepository)
// .transactionManager(transactionManager)
// .tasklet(terminateTasklet())
// .build();
// }
private Step terminateStep() {
return new StepBuilder("terminateStep")
.repository(jobRepository)
.transactionManager(transactionManager)
.tasklet(terminateTasklet())
.build();
}
// private Tasklet terminateTasklet() {
// return (contribution, chunkContext) -> {
// log.error("List Read Error : List is null");
// return RepeatStatus.FINISHED;
// };
// }
private Tasklet terminateTasklet() {
return (contribution, chunkContext) -> {
log.error("List Read Error : List is null");
return RepeatStatus.FINISHED;
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,432 @@
## Thymeleaf 사용 가이드
### 1. 기본 설정
#### 1.1 의존성 추가
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
```
#### 1.2 application.yml 설정
```yaml
spring:
thymeleaf:
cache: false # 개발 환경에서는 캐시 비활성화
prefix: classpath:/templates/ # 템플릿 위치
suffix: .html # 템플릿 확장자
mode: HTML # 템플릿 모드
encoding: UTF-8 # 인코딩
```
### 2. 레이아웃 구성
#### 2.1 기본 레이아웃 (layout.html)
```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8">
<title layout:title-pattern="$CONTENT_TITLE - 배치 모니터링">기본 타이틀</title>
<!-- 공통 CSS -->
<link rel="stylesheet" th:href="@{/css/common.css}">
<link rel="stylesheet" th:href="@{/css/layout.css}">
<!-- 페이지별 CSS 영역 -->
<th:block layout:fragment="css"></th:block>
</head>
<body>
<!-- 헤더 영역 -->
<header th:replace="fragments/header :: header"></header>
<!-- 사이드바 영역 -->
<aside th:replace="fragments/sidebar :: sidebar"></aside>
<!-- 본문 영역 -->
<main layout:fragment="content">
<!-- 각 페이지의 내용이 여기에 들어감 -->
</main>
<!-- 푸터 영역 -->
<footer th:replace="fragments/footer :: footer"></footer>
<!-- 공통 JavaScript -->
<script th:src="@{/js/common.js}"></script>
<!-- 페이지별 JavaScript 영역 -->
<th:block layout:fragment="script"></th:block>
</body>
</html>
```
#### 2.2 페이지 구현 예시 (batch-jobs.html)
```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout}">
<head>
<title>배치 작업 목록</title>
<!-- 페이지별 CSS -->
<th:block layout:fragment="css">
<link rel="stylesheet" th:href="@{/css/batch-jobs.css}">
</th:block>
</head>
<body>
<!-- 본문 영역 -->
<main layout:fragment="content">
<h2>배치 작업 목록</h2>
<div class="job-list">
<table>
<thead>
<tr>
<th>작업명</th>
<th>상태</th>
<th>마지막 실행</th>
<th>다음 실행</th>
<th>작업</th>
</tr>
</thead>
<tbody>
<tr th:each="job : ${jobs}">
<td th:text="${job.name}">작업명</td>
<td th:text="${job.status}">상태</td>
<td th:text="${#temporals.format(job.lastExecuted, 'yyyy-MM-dd HH:mm:ss')}">
마지막 실행
</td>
<td th:text="${#temporals.format(job.nextExecute, 'yyyy-MM-dd HH:mm:ss')}">
다음 실행
</td>
<td>
<button th:onclick="'executeJob(\'' + ${job.id} + '\')'">실행</button>
</td>
</tr>
</tbody>
</table>
</div>
</main>
<!-- 페이지별 JavaScript -->
<th:block layout:fragment="script">
<script th:src="@{/js/batch-jobs.js}"></script>
</th:block>
</body>
</html>
```
### 3. Thymeleaf 문법 가이드
#### 3.1 기본 표현식
```html
<!-- 텍스트 출력 -->
<span th:text="${message}">기본 메시지</span>
<!-- HTML 출력 -->
<div th:utext="${htmlContent}">기본 HTML</div>
<!-- 변수 표현식 -->
<span th:text="${user.name}">사용자명</span>
<!-- URL 표현식 -->
<a th:href="@{/jobs/{id}(id=${job.id})}">상세보기</a>
<!-- 조건부 표현식 -->
<div th:if="${not #lists.isEmpty(jobs)}">
작업이 있습니다.
</div>
<div th:unless="${not #lists.isEmpty(jobs)}">
작업이 없습니다.
</div>
<!-- 반복문 -->
<tr th:each="job, stat : ${jobs}">
<td th:text="${stat.count}">1</td>
<td th:text="${job.name}">작업명</td>
</tr>
<!-- Switch-Case -->
<div th:switch="${job.status}">
<p th:case="'RUNNING'">실행 중</p>
<p th:case="'COMPLETED'">완료</p>
<p th:case="*">기타</p>
</div>
```
#### 3.2 유틸리티 객체
```html
<!-- 날짜 포맷팅 -->
<span th:text="${#temporals.format(job.startTime, 'yyyy-MM-dd HH:mm:ss')}">
2024-01-01 12:00:00
</span>
<!-- 숫자 포맷팅 -->
<span th:text="${#numbers.formatDecimal(amount, 1, 'COMMA', 2, 'POINT')}">
1,234.56
</span>
<!-- 문자열 처리 -->
<span th:text="${#strings.abbreviate(description, 100)}">
긴 설명...
</span>
<!-- 컬렉션 처리 -->
<div th:if="${#lists.isEmpty(jobs)}">
작업이 없습니다.
</div>
```
#### 3.3 폼 처리
```html
<!-- 폼 바인딩 -->
<form th:action="@{/jobs}" th:object="${jobForm}" method="post">
<div>
<label>작업명:</label>
<input type="text" th:field="*{name}">
<span th:if="${#fields.hasErrors('name')}"
th:errors="*{name}"
class="error">
이름 오류
</span>
</div>
<div>
<label>Cron 표현식:</label>
<input type="text" th:field="*{cronExpression}">
<span th:if="${#fields.hasErrors('cronExpression')}"
th:errors="*{cronExpression}"
class="error">
Cron 표현식 오류
</span>
</div>
<button type="submit">저장</button>
</form>
```
### 4. 공통 컴포넌트
#### 4.1 페이지네이션 (fragments/pagination.html)
```html
<div th:fragment="pagination (page)">
<div class="pagination"
th:if="${page.totalPages > 0}">
<!-- 이전 페이지 -->
<a th:href="@{${#httpServletRequest.requestURI}(page=${page.number - 1})}"
th:class="${page.first} ? 'disabled' : ''"
th:if="${not page.first}">
이전
</a>
<!-- 페이지 번호 -->
<span th:each="pageNum : ${#numbers.sequence(0, page.totalPages - 1)}">
<a th:href="@{${#httpServletRequest.requestURI}(page=${pageNum})}"
th:text="${pageNum + 1}"
th:class="${pageNum == page.number} ? 'active' : ''">
1
</a>
</span>
<!-- 다음 페이지 -->
<a th:href="@{${#httpServletRequest.requestURI}(page=${page.number + 1})}"
th:class="${page.last} ? 'disabled' : ''"
th:if="${not page.last}">
다음
</a>
</div>
</div>
```
#### 4.2 알림 메시지 (fragments/alert.html)
```html
<div th:fragment="alert (type, message)">
<div th:if="${message != null}"
th:class="'alert alert-' + ${type}">
<span th:text="${message}">알림 메시지</span>
<button type="button" class="close" data-dismiss="alert">&times;</button>
</div>
</div>
```
### 5. JavaScript 연동
#### 5.1 타임리프와 JavaScript 데이터 전달
```html
<!-- 단일 값 전달 -->
<script th:inline="javascript">
const message = /*[[${message}]]*/ '기본 메시지';
// 객체 전달
const job = /*[[${job}]]*/ {};
// 배열 전달
const jobs = /*[[${jobs}]]*/ [];
</script>
<!-- AJAX 호출 예시 -->
<script th:inline="javascript">
function executeJob(jobId) {
const csrfToken = /*[[${_csrf.token}]]*/ '';
const csrfHeader = /*[[${_csrf.headerName}]]*/ 'X-CSRF-TOKEN';
fetch(`/api/jobs/${jobId}/execute`, {
method: 'POST',
headers: {
[csrfHeader]: csrfToken
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('작업이 실행되었습니다.');
location.reload();
} else {
alert('작업 실행에 실패했습니다.');
}
});
}
</script>
```
### 6. 보안 처리
#### 6.1 Spring Security 통합
```html
<!-- 인증 여부 확인 -->
<div th:if="${#authorization.expression('isAuthenticated()')}">
<span th:text="${#authentication.name}">사용자명</span>
</div>
<!-- 권한 기반 표시 -->
<div th:if="${#authorization.expression('hasRole(''ADMIN'')')}">
관리자 메뉴
</div>
<!-- CSRF 토큰 -->
<form th:action="@{/jobs}" method="post">
<input type="hidden"
th:name="${_csrf.parameterName}"
th:value="${_csrf.token}" />
<!-- 폼 내용 -->
</form>
```
### 7. 에러 페이지
#### 7.1 에러 페이지 구현 (error.html)
```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout}">
<head>
<title>에러 발생</title>
</head>
<body>
<main layout:fragment="content">
<div class="error-container">
<h2>오류가 발생했습니다</h2>
<div class="error-details">
<p>
<strong>상태:</strong>
<span th:text="${status}">500</span>
</p>
<p>
<strong>메시지:</strong>
<span th:text="${error}">Internal Server Error</span>
</p>
<div th:if="${trace}" class="error-trace">
<pre th:text="${trace}">스택 트레이스</pre>
</div>
</div>
<a th:href="@{/}" class="btn btn-primary">홈으로 돌아가기</a>
</div>
</main>
</body>
</html>
```
### 8. 성능 최적화
#### 8.1 캐시 설정
```yaml
spring:
thymeleaf:
cache: true # 운영 환경에서는 캐시 활성화
```
#### 8.2 프래그먼트 캐싱
```html
<!-- 캐시 키 지정 -->
<div th:fragment="userInfo (user)"
th:cache="${user.id}">
<!-- 사용자 정보 표시 -->
</div>
```
### 9. 국제화 (i18n)
#### 9.1 메시지 설정
```yaml
spring:
messages:
basename: messages
encoding: UTF-8
```
#### 9.2 메시지 사용
```html
<!-- 메시지 출력 -->
<h1 th:text="#{page.title}">제목</h1>
<!-- 파라미터가 있는 메시지 -->
<p th:text="#{message.welcome(${user.name})}">환영합니다</p>
<!-- 언어 선택 -->
<div class="language-selector">
<a th:href="@{''(lang=ko)}">한국어</a>
<a th:href="@{''(lang=en)}">English</a>
</div>
```
### 10. 모범 사례
#### 10.1 코드 구조화
```text
templates/
├── layouts/
│ └── layout.html
├── fragments/
│ ├── header.html
│ ├── footer.html
│ ├── sidebar.html
│ ├── pagination.html
│ └── alert.html
├── batch/
│ ├── list.html
│ ├── detail.html
│ └── form.html
├── error/
│ ├── 404.html
│ ├── 500.html
│ └── error.html
└── index.html
```
#### 10.2 네이밍 컨벤션
- 레이아웃 템플릿: layout.html
- 페이지 템플릿: 기능명/동작.html (예: batch/list.html)
- 프래그먼트: fragments/컴포넌트명.html
- 에러 페이지: error/에러코드.html
#### 10.3 주석 처리
```html
<!-- /* 타임리프 주석 - 클라이언트에 전송되지 않음 */ -->
<!--/* 여러 줄
타임리프 주석 */-->
<!-- 일반 HTML 주석 - 클라이언트에 전송됨 -->
```