개발자입니다
[비트캠프] 83일차(18주차1일) - Spring Framework(Thymeleaf), myapp-62 본문
[비트캠프] 83일차(18주차1일) - Spring Framework(Thymeleaf), myapp-62
끈기JK 2023. 3. 6. 19:18
62. Thymeleaf 기술 사용하기
/app/board/list 요청이 《Front Controller》Dispatcher Servlet 으로 오면 여기서 list() 실행을 BoardController 로 지시한다. 리턴은 없으므로 요청 주소에서 "board/list" 를 따와서 ThymeleafViewResolver로 JSP 주소 요청한다. 여기서 《ISpringTemplateEngine》SpringTemplateEngine 을 aggregation(집합, 약결합)한다. 여기서 《ITemplateResolver》SpringResourceTemplateResolver 를 aggregation(집합, 약결합)한다.
리턴 받아서 실행을 ThymeleafView 로 한다. 여기서 list.html 로 parsing 해서 리턴 받는다.
Thymeleaf 사이트에서 문서 참고한다. https://www.thymeleaf.org/
central.sonatype.com 에 'thymeleaf-spring5' 검색해서 아래 코드 복사한다.
build.gradle 에 복붙해서 $ gradle eclipse 한다.
62. Thymeleaf
<html xmlns:th="http://www.thymeleaf.org"> 에서 밑줄 부분을 tag library(package) name : tag 나 attribute 가 정의된 파일의 이름. namespace 라 한다.
ns : namespace
th : 실제 namespace 이름 대신 사용할 별명(alias)
↓
<p th:text="~">예제 텍스트</p>
text : library에 정의된 속성명
62. Thymeleaf - HTML5 - ish (HTML5 문법에 맞게)
<p data-th-text="~">예제 텍스트</p>
↑
<p th:text="~">예제 텍스트</p>
Eclipse > Window > Preferences > General > Workspace 에서 Refresh using native hooks or poling 체크하면 VSCode 에서 수정해도 브라우저 새로고침시 적용된다.
### 62. Thymeleaf 뷰 기술 사용하기
- JSP 대신에 Thymeleaf 뷰 기술을 사용하는 방법
Thymeleaf 템플릿 정보 설정 및 실행 엔진을 만든다. (Thymeleaf spring 3.1 문서 참고)
templateResolver.setCacheable(false) 에서 false 설정으로 템플릿 파일을 캐시하지 않고 요청마다 매번 템플릿 파일을 컴파일 한다. 개발시 사용한다. true 설정시 최초 템플릿 컴파일하여 캐시한다. 서버 가동 중간에 JSP 수정해서 장난치는걸 막는다.
package bitcamp.myapp.config;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ITemplateResolver;
@ComponentScan(
value = "bitcamp.myapp",
excludeFilters = {
@Filter(
type = FilterType.REGEX,
pattern = {"bitcamp.myapp.controller.*"})
})
@PropertySource("classpath:/bitcamp/myapp/config/jdbc.properties")
@MapperScan("bitcamp.myapp.dao")
@EnableTransactionManagement
public class RootConfig {
{
System.out.println("RootConfig 생성됨!");
}
@Bean
public DataSource dataSource(
@Value("${jdbc.driver}") String jdbcDriver,
@Value("${jdbc.url}") String url,
@Value("${jdbc.username}") String username,
@Value("${jdbc.password}") String password) {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(jdbcDriver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
return ds;
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) throws Exception {
System.out.println("PlatformTransactionManager 객체 생성! ");
return new DataSourceTransactionManager(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource, ApplicationContext appCtx) throws Exception {
System.out.println("SqlSessionFactory 객체 생성!");
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setTypeAliasesPackage("bitcamp.myapp.vo");
factoryBean.setMapperLocations(appCtx.getResources("classpath*:bitcamp/myapp/mapper/*Mapper.xml"));
return factoryBean.getObject();
}
@Bean
public TilesConfigurer tilesConfigurer() {
TilesConfigurer configurer = new TilesConfigurer();
configurer.setDefinitions("/WEB-INF/defs/app-tiles.xml", "/WEB-INF/defs/admin-tiles.xml");
return configurer;
}
// Thymeleaf 템플릿에 관한 정보를 설정한다.
@Bean
public SpringResourceTemplateResolver templateResolver(ApplicationContext applicationContext){
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(applicationContext);
templateResolver.setPrefix("/WEB-INF/thymeleaf/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCacheable(false);
return templateResolver;
}
// Thymeleaf 템플릿을 실행할 엔진을 만든다.
@Bean
public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver){
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
templateEngine.setEnableSpringELCompiler(true);
return templateEngine;
}
}
ThymeleafViewResolver 객체 Bean 등록하여 실행할 Thymeleaf 템플릿을 결정하는 일을 한다. 우선순위 1로 한다.
tiles 는 우선순위 2로 한다. jsp 는 우선순위 3으로 한다.
package bitcamp.myapp.config;
import java.nio.charset.StandardCharsets;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesView;
import org.thymeleaf.spring5.ISpringTemplateEngine;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
import bitcamp.myapp.controller.StudentController;
import bitcamp.myapp.controller.TeacherController;
import bitcamp.myapp.web.interceptor.AuthInterceptor;
//@Configuration
@ComponentScan(
value = "bitcamp.myapp.controller",
excludeFilters = {
@Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {StudentController.class, TeacherController.class})
})
// WebMVC 관련 설정을 처리하고 싶다면 다음 애노테이션을 추가하라!
// => WebMVC 관련 설정을 수행하는 클래스를 정의했으니,
// WebMvcConfigurer 구현체를 찾아
// 해당 인터페이스에 정의된대로 메서드를 호출하여
// 설정을 수행하라는 의미다!
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {
{
System.out.println("AppConfig 생성됨!");
}
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
@Bean
public ViewResolver viewResolver() {
// 페이지 컨트롤러가 jsp 경로를 리턴하면
// viewResolver가 그 경로를 가지고 최종 jsp 경로를 계산한 다음에
// JstlView를 통해 실행한다.
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
viewResolver.setOrder(3);
return viewResolver;
}
@Bean
public ViewResolver tilesViewResolver() {
UrlBasedViewResolver vr = new UrlBasedViewResolver();
// Tiles 설정에 따라 템플릿을 실행할 뷰 처리기를 등록한다.
vr.setViewClass(TilesView.class);
// request handler가 리턴한 view name 앞에 일반 페이지임을 표시하기 위해 접두사를 붙인다.
vr.setPrefix("app/");
// 뷰리졸버의 우선 순위를 InternalResourceViewResolver 보다 우선하게 한다.
vr.setOrder(2);
return vr;
}
// 실행할 Thymeleaf 템플릿을 결정하는 일을 한다.
@Bean
public ThymeleafViewResolver viewResolver(ISpringTemplateEngine templateEngine){
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine);
// Content-Type을 설정한다.
// => 만약 설정하지 않는다면 자바의 Uncode2(UTF-16)을 ISO-8859-1로 변환시킨다.
// 즉 영어는 제대로 변환되지만 한글을 '?'로 변환된다.
viewResolver.setCharacterEncoding(StandardCharsets.UTF_8.name());
viewResolver.setOrder(1);
// 페이지 컨트롤러의 request handler가 무엇을 리턴하든지 간에
// Thymeleaf 템플릿 엔진을 사용하겠다는 의미다!
viewResolver.setViewNames(new String[] {"*"});
//viewResolver.setViewNames(new String[] {".html", ".xhtml"});
return viewResolver;
}
// WebMvcConfigurer 규칙에 맞춰 인터셉터를 등록한다.
@Override
public void addInterceptors(InterceptorRegistry registry) {
System.out.println("AppConfig.addInterceptors() 호출됨!");
registry.addInterceptor(
new AuthInterceptor()).addPathPatterns("/**/*insert", "/**/*update", "/**/*delete");
}
}
Thymeleaf 에서 cookie 사용이 안되어 AuthController 에서 클라이언트가 보낸 쿠키 @CookieValue 로 꺼낸다.
/auth/login 에서 loginfail 시 redirect 되도록 수정한다.
package bitcamp.myapp.controller;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestMapping;
import bitcamp.myapp.service.StudentService;
import bitcamp.myapp.service.TeacherService;
import bitcamp.myapp.vo.Member;
@Controller
public class AuthController {
{
System.out.println("AuthController 생성됨!");
}
@Autowired private StudentService studentService;
@Autowired private TeacherService teacherService;
@RequestMapping("/auth/form")
public void form(@CookieValue(required = false) String email,
Model model,
HttpSession session) {
model.addAttribute("email", email);
if (session.getAttribute("error") != null) {
model.addAttribute("error", session.getAttribute("error"));
}
}
@RequestMapping("/auth/login")
public String login(
String usertype,
String email,
String password,
String saveEmail,
HttpServletResponse response,
HttpSession session,
Model model) {
if (saveEmail != null) {
Cookie cookie = new Cookie("email", email);
cookie.setMaxAge(60 * 60 * 24 * 30); // 30일 동안 유지
response.addCookie(cookie);
} else {
Cookie cookie = new Cookie("email", "");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
Member member = null;
switch (usertype) {
case "student":
member = studentService.get(email, password);
break;
case "teacher":
member = teacherService.get(email, password);
break;
}
if (member != null) {
session.setAttribute("loginUser", member);
session.removeAttribute("error");
return "redirect:../../";
} else {
session.setAttribute("error", "loginfail");
return "redirect:form";
}
}
@RequestMapping("/auth/logout")
public String logout(HttpSession session) {
session.invalidate();
return "redirect:../../";
}
@RequestMapping("/auth/fail")
public void fail() {
}
}
Thymeleaf 적용한 HTML 은 JSP 사용 태그 제거한다.
data-th-if 처럼 Thymeleaf 문법 표현한다.
<!-- auth/form.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>로그인</h1>
<p data-th-if="${error == 'loginfail'}">이메일 또는 암호가 맞지 않습니다!</p>
<form action="login" method="post">
<table border="1">
<tr>
<th>회원 유형</th>
<td>
<input type="radio" name="usertype" value="student" checked> 학생
<input type="radio" name="usertype" value="teacher"> 강사
</td>
</tr>
<tr>
<th>이메일</th>
<td><input type="email" name="email"
value="user@test.com"
data-th-value="${email}"></td>
</tr>
<tr>
<th>암호</th>
<td><input type="password" name="password"></td>
</tr>
</table>
<div>
<input type="checkbox" name="saveEmail"> 이메일 저장<br>
<button>로그인</button>
</div>
</form>
</body>
</html>
<!-- auth/fail.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>권한 오류!</h1>
<p>실행 권한이 없습니다!</p>
</body>
</html>
개발시 보기 좋도록 기본데이터 넣는다.
<!-- board/list.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>게시판(Thymeleaf)</h1>
<div><a href='form'>새 글</a></div>
<table border='1'>
<tr>
<th>번호</th> <th>제목</th> <th>작성자</th> <th>작성일</th> <th>조회수</th>
</tr>
<tr data-th-each="b : ${boards}">
<td data-th-text="${b.no}">1</td>
<td><a href='view?no=1'
data-th-href="@{view(no=${b.no})}"
data-th-text="${b.title}">제목입니다.</a></td>
<td data-th-text="${b.writer.name}">홍길동</td>
<td data-th-text="${b.createdDate}">2023-01-01</td>
<td data-th-text="${b.viewCount}">77</td>
</tr>
</table>
<form action='list' method='get'>
<input type='text' name='keyword' value="검색어" data-th-value='${param.keyword}'>
<button>검색</button>
</form>
</body>
</html>
<!-- board/form.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>게시판(Thymeleaf)</h1>
<form action='insert' method='post' enctype="multipart/form-data">
<table border='1'>
<tr>
<th>제목</th>
<td><input type='text' name='title'></td>
</tr>
<tr>
<th>내용</th>
<td><textarea name='content' rows='10' cols='60'></textarea></td>
</tr>
<tr>
<th>첨부파일</th>
<td><input type="file" name='files' multiple></td>
</tr>
</table>
<div>
<button>등록</button>
<button id='btn-cancel' type='button'>취소</button>
</div>
</form>
<script>
document.querySelector('#btn-cancel').onclick = function() {
location.href = 'list';
}
</script>
</body>
</html>
<!-- board/insert.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<meta http-equiv='Refresh' content='1;url=list'>
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>게시판(Thymeleaf)</h1>
<p data-th-unless="${error}">입력 했습니다.</p>
<p data-th-if="${error}">입력 실패입니다!</p>
</body>
</html>
<!-- board/view.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>게시판(Thymeleaf)</h1>
<div data-th-unless="${board}">
<p>해당 번호의 게시글 없습니다.</p>
<div>
<button id='btn-list' type='button'>목록</button>
</div>
</div>
<div data-th-if="${board}">
<form id='board-form' action='update' method='post' enctype="multipart/form-data">
<table border='1'>
<tr>
<th>번호</th>
<td><input type='text' name='no'
value='1'
data-th-value='${board.no}'
readonly></td>
</tr>
<tr>
<th>제목</th>
<td><input type='text' name='title'
value='제목입니다.'
data-th-value='${board.title}'></td>
</tr>
<tr>
<th>내용</th>
<td><textarea name='content' rows='10' cols='60'
data-th-text="${board.content}">내용입니다.</textarea></td>
</tr>
<tr>
<th>작성자</th>
<td data-th-text="${board.writer.name}">홍길동</td>
</tr>
<tr>
<th>등록일</th>
<td data-th-text="${board.createdDate}">2023-01-01</td>
</tr>
<tr>
<th>조회수</th>
<td data-th-text="${board.viewCount}">99</td>
</tr>
<tr>
<th>첨부파일</th>
<td>
<input type="file" name='files' multiple>
<ul>
<li data-th-each="boardFile : ${board.attachedFiles}">
<a href="../download/boardfile?fileNo=1"
data-th-href="@{../download/boardfile(fileNo=${boardFile.no})}"
data-th-text="${boardFile.originalFilename}">test.gif</a>
[<a href="filedelete?boardNo=1&fileNo=1"
data-th-href="@{filedelete(boardNo=${board.no},fileNo=${boardFile.no})}">삭제</a>]
</li>
</ul>
</td>
</tr>
</table>
<div>
<button id='btn-list' type='button'>목록</button>
<button data-th-if="${session.loginUser.no == board.writer.no}">변경</button>
<button data-th-if="${session.loginUser.no == board.writer.no}" id='btn-delete' type='button'>삭제</button>
</div>
</form>
</div>
<script>
document.querySelector('#btn-list').onclick = function() {
location.href = 'list';
}
document.querySelector('#btn-delete').onclick = function() {
var form = document.querySelector('#board-form');
form.action = 'delete';
form.submit();
}
</script>
</body>
</html>
<!-- board/update.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<meta http-equiv='Refresh' content='1;url=list'>
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>게시판(Thymeleaf)</h1>
<p data-th-unless="${error}">변경했습니다.</p>
<p data-th-if="${error}">변경 실패입니다!</p>
</body>
</html>
<!-- board/delete.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<meta http-equiv='Refresh' content='1;url=list'>
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>게시판(Thymeleaf)</h1>
<p data-th-unless="${error}">삭제했습니다.</p>
<p data-th-if="${error}">삭제 실패입니다.</p>
</body>
</html>
조언
*
과제
Thymeleaf 로 변경하기
teacher, student