| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- jdk
- jstl
- Egov
- Oracle
- custom tag
- 한글타이핑
- APR
- tomcat
- type-hangul
- vmware
- jdk설치
- PCRE
- apache2
- jdk1.8
- thymeleaf egov custom tag
- 달력 라이브러리
- 달력 api
- 캘린더 라이브러리
- Custom
- linux
- CentOS 7
- springboot
- CSS애니메이션
- Apache
- 캘린더 api
- AJP
- fullcalendar
- thymeleaf
- spring boot 3
- httpd
- Today
- Total
SSG
[Thymeleaf 3] Egov 페이징 태그 구현 본문
작업 환경
Spring Boot 3.0.4
JDK 17
Thymeleaf 3
mybatis 3.0.1
기존의 EgovFramework를 사용하고 있던 터라 Thymeleaf 확장 태그 구현도 연습할 겸 구현을 시도해보았다.
페이징 계산은 EgovFramework 4.0 버전의 PaginationInfo를 사용하였다.
아쉽지만 type과 jsFunction의 경우 시간이 그렇게 많지 않아 생략
paginationInfo.java
/**
* PaginationInfo.java
* <p/><b>NOTE:</b><pre>
* 페이징 처리를 위한 데이터가 담기는 빈.
* 페이징 처리에 필요한 데이터를 Required Fields, Not Required Fields 로 나누었다.
*
* Required Fields
* : 사용자가 입력해야 하는 필드값이다.
* currentPageNo : 현재 페이지 번호.
* recordCountPerPage : 한 페이지당 게시되는 게시물 건 수.
* pageSize : 페이지 리스트에 게시되는 페이지 건수.
* totalRecordCount : 전체 게시물 건 수.
*
* Not Required Fields
* : 사용자가 입력한 Required Fields 값을 바탕으로 계산하여 정해지는 값이다.
* totalPageCount: 페이지 개수.
* firstPageNoOnPageList : 페이지 리스트의 첫 페이지 번호.
* lastPageNoOnPageList : 페이지 리스트의 마지막 페이지 번호.
* firstRecordIndex : 페이징 SQL의 조건절에 사용되는 시작 rownum.
* lastRecordIndex : 페이징 SQL의 조건절에 사용되는 마지막 rownum.
*
* 페이징 Custom 태그인 <ui:pagination> 사용시에 paginationInfo 필드에 PaginationInfo 객체를 값으로 주어야 한다.
* </pre>
*<pre class="code">
*<th:block th:pagination = "${paginationInfo}">
*</pre>
*
*/
public class PaginationInfo {
/**
* Required Fields
* - 이 필드들은 페이징 계산을 위해 반드시 입력되어야 하는 필드 값들이다.
*
* currentPageNo : 현재 페이지 번호
* recordCountPerPage : 한 페이지당 게시되는 게시물 건 수
* pageSize : 페이지 리스트에 게시되는 페이지 건수,
* totalRecordCount : 전체 게시물 건 수.
*/
private int currentPageNo;
private int recordCountPerPage;
private int pageSize;
private int totalRecordCount;
public int getRecordCountPerPage() {
return recordCountPerPage;
}
public void setRecordCountPerPage(int recordCountPerPage) {
this.recordCountPerPage = recordCountPerPage;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public int getCurrentPageNo() {
return currentPageNo;
}
public void setCurrentPageNo(int currentPageNo) {
this.currentPageNo = currentPageNo;
}
public void setTotalRecordCount(int totalRecordCount) {
this.totalRecordCount = totalRecordCount;
}
public int getTotalRecordCount() {
return totalRecordCount;
}
/**
* Not Required Fields
* - 이 필드들은 Required Fields 값을 바탕으로 계산해서 정해지는 필드 값이다.
*
* totalPageCount: 페이지 개수
* firstPageNoOnPageList : 페이지 리스트의 첫 페이지 번호
* lastPageNoOnPageList : 페이지 리스트의 마지막 페이지 번호
* firstRecordIndex : 페이징 SQL의 조건절에 사용되는 시작 rownum.
* lastRecordIndex : 페이징 SQL의 조건절에 사용되는 마지막 rownum.
*/
private int totalPageCount;
private int firstPageNoOnPageList;
private int lastPageNoOnPageList;
private int firstRecordIndex;
private int lastRecordIndex;
public int getTotalPageCount() {
totalPageCount = ((getTotalRecordCount() - 1) / getRecordCountPerPage()) + 1;
return totalPageCount;
}
public int getFirstPageNo() {
return 1;
}
public int getLastPageNo() {
return getTotalPageCount();
}
public int getFirstPageNoOnPageList() {
firstPageNoOnPageList = ((getCurrentPageNo() - 1) / getPageSize()) * getPageSize() + 1;
return firstPageNoOnPageList;
}
public int getLastPageNoOnPageList() {
lastPageNoOnPageList = getFirstPageNoOnPageList() + getPageSize() - 1;
if (lastPageNoOnPageList > getTotalPageCount()) {
lastPageNoOnPageList = getTotalPageCount();
}
return lastPageNoOnPageList;
}
public int getFirstRecordIndex() {
firstRecordIndex = (getCurrentPageNo() - 1) * getRecordCountPerPage();
return firstRecordIndex;
}
public int getLastRecordIndex() {
lastRecordIndex = getCurrentPageNo() * getRecordCountPerPage();
return lastRecordIndex;
}
}
dialect 클래스 생성
dialect 클래스를 생성하여 타임리프에서 새롭게 만든 커스텀 태그를 사용할 수 있도록 해준다. 사용할 접두사는 th 이고, 속성명은 pagination가 되어 화면에서 호출시에는 hello:sayto와 같이 사용한다.
PROCESSOR_PRECEDENCE는 dialect 우선순위를 결정해준다.
PaginationTag.java
package com.pagination;
import org.thymeleaf.dialect.AbstractProcessorDialect;
import org.thymeleaf.processor.IProcessor;
import org.thymeleaf.templatemode.TemplateMode;
import java.util.HashSet;
import java.util.Set;
public class PaginationDialect extends AbstractProcessorDialect {
public static final String NAME = "Pagination";
public static final String DEFAULT_PREFIX = "th";
public static final int PROCESSOR_PRECEDENCE = 800;
private String charset;
public PaginationDialect(String charset) {
super(NAME, DEFAULT_PREFIX, PROCESSOR_PRECEDENCE);
this.charset = charset;
}
@Override
public Set<IProcessor> getProcessors(String dialectPrefix) {
final Set<IProcessor> processors = new HashSet<>();
processors.add(new PaginationProcessor(TemplateMode.HTML,
dialectPrefix, this.charset));
return processors;
}
}
AbstractAttributeTagProcessor 클래스를 확장하여 화면에 태그 표시를 담당하는 처리기를 작성한다.
PaginationProcessor.java
package com.pagination;
import lombok.extern.slf4j.Slf4j;
import org.thymeleaf.IEngineConfiguration;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.engine.AttributeName;
import org.thymeleaf.model.AttributeValueQuotes;
import org.thymeleaf.model.IModel;
import org.thymeleaf.model.IModelFactory;
import org.thymeleaf.model.IProcessableElementTag;
import org.thymeleaf.processor.element.AbstractAttributeTagProcessor;
import org.thymeleaf.processor.element.IElementTagStructureHandler;
import org.thymeleaf.standard.expression.IStandardExpression;
import org.thymeleaf.standard.expression.IStandardExpressionParser;
import org.thymeleaf.standard.expression.StandardExpressions;
import org.thymeleaf.templatemode.TemplateMode;
import org.unbescape.html.HtmlEscape;
import java.util.HashMap;
@Slf4j
public class PaginationProcessor extends AbstractAttributeTagProcessor {
public static final int ATTR_PRECEDENCE = 1300;
public static final String ATTR_NAME = "pagination";
private PaginationInfo paginationInfo;
private String jsFunction = "fnLinkPage";
private String charset;
public PaginationProcessor(TemplateMode templateMode, String dialectPrefix, String charset) {
super(templateMode, dialectPrefix, null, false, ATTR_NAME, true, ATTR_PRECEDENCE, true);
this.charset = charset;
}
@Override
protected void doProcess(ITemplateContext context,
IProcessableElementTag tag, AttributeName attributeName,
String attributeValue, IElementTagStructureHandler structureHandler) {
// thymeleaf로 받은 값을 파싱 후 조회
final IEngineConfiguration configuration = context.getConfiguration();
final IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);
final IStandardExpression expression = parser.parseExpression(context, attributeValue);
paginationInfo = (PaginationInfo) expression.execute(context);
int firstPageNo = paginationInfo.getFirstPageNo();
int firstPageNoOnPageList = paginationInfo.getFirstPageNoOnPageList();
int totalPageCount = paginationInfo.getTotalPageCount();
int pageSize = paginationInfo.getPageSize();
int lastPageNoOnPageList = paginationInfo.getLastPageNoOnPageList();
int currentPageNo = paginationInfo.getCurrentPageNo();
int lastPageNo = paginationInfo.getLastPageNo();
// context에서 modelFactory를 가져와 요소를 만드는 객체 선언
final IModelFactory modelFactory = context.getModelFactory();
final IModel model = modelFactory.createModel();
model.add(modelFactory.createOpenElementTag("div"));
if (totalPageCount > pageSize) {
if (firstPageNoOnPageList > pageSize) {
structureHandler.insertImmediatelyAfter(pageLabel(structureHandler, modelFactory, model, String.valueOf(firstPageNo), "classNamePrevFirst", "처음으로", "[처음]"), true);
structureHandler.insertImmediatelyAfter(pageLabel(structureHandler, modelFactory, model, String.valueOf(firstPageNoOnPageList - 1), "classNamePrev", "이전페이지", "[이전]"), true);
} else {
structureHandler.insertImmediatelyAfter(pageLabel(structureHandler, modelFactory, model, String.valueOf(firstPageNo), "classNamePrevFirst", "처음으로", "[처음]"), true);
structureHandler.insertImmediatelyAfter(pageLabel(structureHandler, modelFactory, model, String.valueOf(firstPageNo), "classNamePrev", "이전페이지", "[이전]"), true);
}
}
for (int i = firstPageNoOnPageList; i <= lastPageNoOnPageList; i++) {
if (i == currentPageNo) {
structureHandler.insertImmediatelyAfter(pageLabel(structureHandler, modelFactory, model, String.valueOf(i), "classNameNow", "현재페이지", String.valueOf(i)), true);
} else {
structureHandler.insertImmediatelyAfter(pageLabel(structureHandler, modelFactory, model, String.valueOf(i), "className", "", String.valueOf(i)), true);
}
}
if (totalPageCount > pageSize) {
if (lastPageNoOnPageList < totalPageCount) {
structureHandler.insertImmediatelyAfter(pageLabel(structureHandler, modelFactory, model, String.valueOf(firstPageNoOnPageList + pageSize), "classNameNext", "다음페이지", "[다음]"), true);
structureHandler.insertImmediatelyAfter(pageLabel(structureHandler, modelFactory, model, String.valueOf(lastPageNo), "classNameLast", "마지막페이지로", "[마지막]"), true);
} else {
structureHandler.insertImmediatelyAfter(pageLabel(structureHandler, modelFactory, model, String.valueOf(lastPageNo), "classNameNext", "다음페이지", "[다음]"), true);
structureHandler.insertImmediatelyAfter(pageLabel(structureHandler, modelFactory, model, String.valueOf(lastPageNo), "classNameLast", "마지막페이지로", "[마지막]"), true);
}
}
model.add(modelFactory.createCloseElementTag("div"));
structureHandler.replaceWith(model, false);
}
private IModel pageLabel(IElementTagStructureHandler structureHandler, IModelFactory modelFactory, IModel model, String onclickValue, String className, String title, String tagValue){
HashMap<String, String> attr = new HashMap<>();
if (!className.isEmpty()){
attr.put("class", className);
attr.put("title", title);
}
attr.put("onclick", jsFunction + "(" + onclickValue + "); return false;");
attr.put("href", "#");
model.add(modelFactory.createOpenElementTag("a", attr, AttributeValueQuotes.DOUBLE, false));
model.add(modelFactory.createText(HtmlEscape.escapeHtml5(tagValue)));
model.add(modelFactory.createCloseElementTag("a"));
return model;
}
}
jsFunction의 경우 따로 구현을 하지 못해서
fnLinkPage 라는 값을 선언하여 사용하였다.
결국 나오는 구현은 요런느낌?
<div>
<a href="#" onclick="fnLinkPage(1); return false;" class="classNamePrev1" title="처음페이지">[처음]</a>
<a href="#" onclick="fnLinkPage(13); return false;" class="classNamePrev14" title="이전페이지">[이전]</a>
<a href="#" onclick="fnLinkPage(11); return false;" class="className">11</a>
<a href="#" onclick="fnLinkPage(12); return false;" class="className">12</a>
<a href="#" onclick="fnLinkPage(13); return false;" class="className">13</a>
<a href="#" onclick="fnLinkPage(14); return false;" class="className">14</a>
<a href="#" class="classNameNow">15</a>
<a href="#" onclick="fnLinkPage(16); return false;" class="className">16</a>
<a href="#" onclick="fnLinkPage(17); return false;" class="className">17</a>
<a href="#" onclick="fnLinkPage(18); return false;" class="className">18</a>
<a href="#" onclick="fnLinkPage(19); return false;" class="className">19</a>
<a href="#" onclick="fnLinkPage(20); return false;" class="className">20</a>
<a href="#" onclick="fnLinkPage(16); return false;" class="classNameNext" title="다음페이지">[다음]</a>
<a href="#" onclick="fnLinkPage(21); return false;" class="classNameLast" title="마지막페이지">[마지막]</a>
</div>
Demo1Application.java
package com.example.demo;
import com.pagination.PaginationDialect;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class Demo1Application {
public static void main(String[] args) {
SpringApplication.run(Demo1Application.class, args);
}
@Bean
public PaginationDialect paginationDialect() {
return new PaginationDialect("UTF-8");
}
}
나도 공식 문서와 다른 분들의 블로그를 보고 겨우겨우 구현에 성공했다.
내가 부족해서 완벽하게 구현하는것은 실패했지만 그래도 좋은 경험이었다..
만약에 더 좋은걸 구현하셨다면 공유 부탁드립니다.ㅠ
참조 :
https://www.thymeleaf.org/doc/tutorials/3.0/extendingthymeleaf.html
Tutorial: Extending Thymeleaf
1 Some Reasons to Extend Thymeleaf Thymeleaf is an extremely extensible library. The key to it is that most of its user-oriented features are not directly built into its core, but rather just packaged and componentized into feature sets called dialects. Th
www.thymeleaf.org
https://appaga.github.io/posts/thymeleaf-extend-001/
Creating Thymeleaf Custom Tags
타임리프 커스텀 태그 만들기
appaga.github.io
https://appaga.github.io/posts/thymeleaf-extend-002/
Extending Thymeleaf dialect : processing attribute value
thymeleaf dialect 확장하여 태그 속성값을 변환하여 출력하기
appaga.github.io
https://blog.hkwon.me/thymeleaf-extend-dialect/
Thymeleaf 확장으로 새로운 dialect 추가해보기
최근에 몇몇 프로젝트를 Thymeleaf를 템플릿 엔진으로 선정해서 진행을 하고 있다. 일단 기존의 개발자들이 JSP & JSTL을 사용하는 것에 너무 익숙하다보니 도입을 하는게 쉽진 않았으나 Spring Framework
blog.hkwon.me
'Spring Boot' 카테고리의 다른 글
| [Spring boot 3] jstl 사용 오류 (0) | 2023.03.10 |
|---|