commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
1840
batch-quartz/src/main/resources/developer-guide.md
Normal file
1840
batch-quartz/src/main/resources/developer-guide.md
Normal file
File diff suppressed because it is too large
Load Diff
432
batch-quartz/src/main/resources/thymeleaf-guide.md
Normal file
432
batch-quartz/src/main/resources/thymeleaf-guide.md
Normal 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">×</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 주석 - 클라이언트에 전송됨 -->
|
||||
```
|
||||
Reference in New Issue
Block a user