피할 수 없다면 즐기자! 스프링부트 너.. 뭐 돼?
package com.koreatit.mylogin.loginweb;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.SessionAttribute;
import com.koreatit.mylogin.loginweb.member.Member;
import com.koreatit.mylogin.loginweb.member.MemberRepository;
import com.koreatit.mylogin.loginweb.session.SessionConst;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
public class HomeController {
// localhost:9090 -> home.html
private final MemberRepository memberRepository;
// @GetMapping
public String home() {
return "home";
}
// @GetMapping
public String homev2(@CookieValue(name = "memberId", required = false)Long memberId,
Model model) {
// 로그인한 사용자가 아니라면 home으로 보낸다.
if( memberId == null ) {
return "home";
}
// db조회
Member loginMember = memberRepository.findById(memberId);
// 사용자가 없으면 null 처리 필요
if( loginMember == null ) {
return "home";
}
// loginHome : 로그인에 성공한 사람만이 볼 수 있는 화면
model.addAttribute("member", loginMember);
return "loginHome";
}
// @GetMapping
public String homev3(HttpServletRequest request, Model model) {
HttpSession session = request.getSession(false);
// 로그인한 사용자가 아니라면 home으로 보낸다.
if( session == null ) {
return "home";
}
Member loginMember = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);
// 사용자가 없으면 null 처리 필요
if( loginMember == null ) {
return "home";
}
// loginHome : 로그인에 성공한 사람만이 볼 수 있는 화면
model.addAttribute("member", loginMember);
return "loginHome";
}
@GetMapping
public String homev4(
// session attribute를 뒤져서 member에 값을 넣어준다.
@SessionAttribute(name=SessionConst.LOGIN_MEMBER, required=false)Member loginMember,
Model model) {
// 사용자가 없으면 null 처리 필요
if( loginMember == null ) {
return "home";
}
// loginHome : 로그인에 성공한 사람만이 볼 수 있는 화면
model.addAttribute("member", loginMember);
return "loginHome";
}
}
package com.koreatit.mylogin.loginweb;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.koreatit.mylogin.loginweb.member.Member;
import com.koreatit.mylogin.loginweb.member.MemberRepository;
import lombok.RequiredArgsConstructor;
@Controller
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("add")
public String addForm(@ModelAttribute("member")Member member) {
// model.addAttribute("member", new Member());
return "/members/addMemberForm";
}
@PostMapping("add")
public String save(@ModelAttribute Member member) {
memberRepository.save(member);
return "redirect:/";
}
}
코드를 입력하세요
package com.koreatit.mylogin.loginweb.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
public class LogFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 명시적 형변환
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
String requestURI = httpServletRequest.getRequestURI();
System.out.println("requestURI : " + requestURI);
chain.doFilter(request, response);
System.out.println("responseURI : " + requestURI);
}
}
package com.koreatit.mylogin.loginweb.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.util.PatternMatchUtils;
import com.koreatit.mylogin.loginweb.session.SessionConst;
public class LoginCheckFilter implements Filter {
private static final String[] whitelist = {"/", "/members/add", "/login", "/logout", "/css/*"};
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest)request;
String requestURI = httpRequest.getRequestURI();
HttpServletResponse httpResponse = (HttpServletResponse)response;
System.out.println("인증 체크 필터 시작 ");
if(isLoginCheckpath(requestURI)) {
System.out.println("인증 체크 로직 실행 : " + requestURI);
HttpSession session = httpRequest.getSession(false);
if(session == null
|| session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
System.out.println("미 인증 사용자 요청");
// 로그인으로 redirect
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
// 미인증 사용자는 다음으로 진행하지 않고 끝낸다.
return;
}
}
// 다음 단계로 넘어간다.
chain.doFilter(request, response);
}
/*
* 화이트 리스트의 경우 인증 체크 x
* simpleMatch : 파라미터 문자여링 특정 패턴에 매칭되는지를 검사함.
*/
private boolean isLoginCheckpath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
}
package com.koreatit.mylogin.loginweb.filter;
import javax.servlet.Filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.koreatit.mylogin.loginweb.interceptor.LogInCheckInterceptor;
import com.koreatit.mylogin.loginweb.interceptor.LogInterceptor;
@Component
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/error");
registry.addInterceptor(new LogInCheckInterceptor())
.order(1)
.addPathPatterns("/**") // 모든 경로 전체
.excludePathPatterns("/", "/members/add", "/login", "/logout", "/css/**");
}
// @Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean
= new FilterRegistrationBean<Filter>();
// LogFilter 등록
filterRegistrationBean.setFilter(new LogFilter());
// 필터 순서
filterRegistrationBean.setOrder(1);
// 모든 url 다 적용
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
// @Bean
public FilterRegistrationBean logCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean
= new FilterRegistrationBean<Filter>();
// LogCheckFilter 등록
filterRegistrationBean.setFilter(new LoginCheckFilter());
// 필터 순서
filterRegistrationBean.setOrder(2);
// 모든 url 다 적용
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
package com.koreatit.mylogin.loginweb.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.servlet.HandlerInterceptor;
import com.koreatit.mylogin.loginweb.session.SessionConst;
public class LogInCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String requestURI = request.getRequestURI();
System.out.println("[interceptor] : " + requestURI);
HttpSession session = request.getSession(false);
if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER)== null) {
System.out.println("[미인증 사용자 요청]");
// 로그인으로 redirect
response.sendRedirect("/login?redirectURL" + requestURI);
return false;
}
return true;
}
}
package com.koreatit.mylogin.loginweb.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String requestURI = request.getRequestURI();
System.out.println("[interceptor] requestURI" + requestURI);
return true; // false -> 진행 x
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("[interceptor] postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println("[interceptor] afterCompletion");
}
}
package com.koreatit.mylogin.loginweb.item;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
super();
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
package com.koreatit.mylogin.loginweb.item;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import lombok.RequiredArgsConstructor;
@Controller
@RequestMapping("/items")
@RequiredArgsConstructor
public class ItemController {
private final ItemRepository itemRepository;
@GetMapping
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "items/items";
}
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute(item);
return "items/item";
}
@GetMapping("/add")
public String addItem(Model model) {
model.addAttribute("item", new Item());
return "items/addForm";
}
@PostMapping("/add")
public String saveV9(Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
if(!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false,
new String[] {"required.item.itemName", "required.default"}, null, null));
}
if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.addError(new FieldError("item", "price", item.getPrice(), false,
new String[] {"range.item.price"}, new Object[] {1000, 10000}, null));
}
if(item.getQuantity() == null || item.getQuantity() > 10000) {
bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false,
new String[] {"max.item.quantity"}, new Object[] {9999}, null));
}
if(bindingResult.hasErrors()) {
return "items/addForm";
}
Item saveItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", saveItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/items/{itemId}";
}
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "items/editForm";
}
@PostMapping("/{itemId}/edit")
public String update(@PathVariable Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId, item);
// 상세페이지
return "redirect:/items/{itemId}";
}
}
package com.koreatit.mylogin.loginweb.item;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Repository;
@Repository
public class ItemRepository {
// static 사용
private static final Map<Long, Item> store = new HashMap<Long, Item>();
private static long sequence = 0L;
// 저장
public Item save(Item item) {
item.setId(++sequence);
store.put(item.getId(), item);
return item;
}
// id로 찾기
public Item findById(Long Id) {
return store.get(Id);
}
// 전체 찾기
public List<Item> findAll(){
return new ArrayList<Item>(store.values());
}
// 수정
public void update(Long itemId, Item updateParam) {
// item을 먼저 찾는다
Item findItem = findById(itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
}
package com.koreatit.mylogin.loginweb.login;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.koreatit.mylogin.loginweb.member.Member;
import com.koreatit.mylogin.loginweb.session.SessionConst;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm")LoginForm loginForm) {
return "login/loginForm";
}
// @PostMapping("/login")
public String login(@ModelAttribute LoginForm form, Model model,
RedirectAttributes redirectAttributes, HttpServletResponse response) {
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if( loginMember == null ) {
// 로그인 실패
model.addAttribute("msg", "로그인실패");
return "login/loginForm";
}
/*
* - addAttribute : url 뒤에 붙는다.
* - addFlashAttribute : url뒤에 붙지 않는다.
*/
// 로그인 성공
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()) );
response.addCookie(idCookie);
redirectAttributes.addFlashAttribute("msg", "로그인 성공");
return "redirect:/";
}
// @PostMapping("/login")
public String loginv2(@ModelAttribute LoginForm form, Model model,
RedirectAttributes redirectAttributes, HttpServletRequest request) {
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if( loginMember == null ) {
// 로그인 실패
model.addAttribute("msg", "로그인실패");
return "login/loginForm";
}
// 로그인 성공
HttpSession session = request.getSession();
// 세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
redirectAttributes.addFlashAttribute("msg", "로그인 성공");
return "redirect:/";
}
@PostMapping("/login")
public String loginv3(@ModelAttribute LoginForm form, Model model,
RedirectAttributes redirectAttributes, HttpServletRequest request,
@RequestParam(defaultValue="/")String redirectURL) {
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if( loginMember == null ) {
// 로그인 실패
model.addAttribute("msg", "로그인실패");
return "login/loginForm";
}
// 로그인 성공
HttpSession session = request.getSession();
// 세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
redirectAttributes.addFlashAttribute("msg", "로그인 성공");
return "redirect:" + redirectURL;
}
// @PostMapping("/logout")
public String logout(HttpServletResponse response) {
Cookie cookie = new Cookie("memberId", null);
cookie.setMaxAge(0);
response.addCookie(cookie);
return "redirect:/";
}
@PostMapping("/logout")
public String logoutv2(HttpServletRequest request) {
// 세션을 삭제
/*
* request.getSession(true)
* - 세션이 있으면 기존 세션을 반환한다.
* - 세션이 없으면 새로운 세션을 생성해서 반환한다.
* request.getSession(false)
* - 세션이 있으면 기존 세션을 반환한다.
* - 세션이 없으면 새로운 세션을 생성하지 않고, null을 반환
*/
HttpSession session = request.getSession(false);
if( session != null ) {
session.invalidate();
}
return "redirect:/";
}
}
package com.koreatit.mylogin.loginweb.login;
import lombok.Data;
@Data
public class LoginForm {
private String loginId;
private String password;
}
package com.koreatit.mylogin.loginweb.login;
import org.springframework.stereotype.Service;
import com.koreatit.mylogin.loginweb.member.Member;
import com.koreatit.mylogin.loginweb.member.MemberRepository;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class LoginService {
private final MemberRepository memberRepository;
public Member login(String loginId, String password) {
Member member = memberRepository.findByLoginId(loginId);
if(member != null && member.getLoginId().equals(loginId)
&& member.getPassword().equals(password)) {
// 로그인 성공
return member;
}else {
return null;
}
}
}
package com.koreatit.mylogin.loginweb.member;
import lombok.Data;
@Data
public class Member {
private Long id;
private String name; // 사용자 이름
private String loginId; // 로그인 ID
private String password; // 사용자 PW
}
package com.koreatit.mylogin.loginweb.member;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Repository;
@Repository
public class MemberRepository {
private static Map<Long, Member> store = new HashMap<Long, Member>();
private static long sequence = 0L;
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
public Member findById(Long id) {
return store.get(id);
}
public List<Member> findAll(){
return new ArrayList<Member>(store.values());
}
public Member findByLoginId(String loginId) {
List<Member> all = findAll();
for(Member m : all) {
if(m.getLoginId().equals(loginId)) {
return m;
}
}
return null;
}
}
package com.koreatit.mylogin.loginweb.session;
public class SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link th:href="@{/css/bootstrap.min.css}"
href="css/bootstrap.min.css" rel="stylesheet">
<title>Insert title here</title>
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>홈 화면</h2>
</div>
<div class="row">
<div class="col">
<button class="w-100 btn btn-secondary btn-lg" type="button"
th:onclick="|location.href='@{/members/add}'|">
회원 가입
</button>
</div>
<div class="col">
<button class="w-100 btn btn-dark btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/login}'|" type="button">
로그인
</button>
</div>
</div>
<hr class="my-4">
</div>
<!-- /container -->
</body>
<script>
let message = "[[${msg}]]";
if(message != ""){
alert(message);
}
</script>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
<title>Insert title here</title>
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>홈 화면</h2>
</div>
<h4 class="mb-3" th:text="|로그인 : ${member.name}|" >로그인 사용자 이름</h4>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-secondary btn-lg" type="button"
th:onclick="|location.href='@{/items}'|">상품 관리</button>
</div>
<div class="col">
<form method="post" th:action="@{/logout}">
<button class="w-100 btn btn-dark btn-lg"
onclick="location.href='items.html'" type="submit">
로그아웃
</button>
</form>
</div>
</div>
<hr class="my-4">
</div>
<!-- /container -->
</body>
</html>
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link href="./css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}"
rel="stylesheet">
<style>
.container {
max-width: 560px;
}
.field-error {
border-color: #dc3545;
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 등록 폼</h2>
</div>
<h4 class="mb-3">상품 입력</h4>
<!-- 같은 url에 전송하고 방법만 달라지는 경우는 url을 생략해도 된다. -->
<form action="item.html" th:object="${item}" th:action method="post">
<div>
<!--
- 타임리프는 스프링의 BindingResult를 활용해서 편리한 오류 표현기능 제공
- field : BindingResult가 제공하는 오류에 접근 할 수 있다.
- th:errorclass : th:field에서 지정한 필드에 오류가 있으면 class를 추가
- th:errors : 해당 필드에 오류가 있는 경우에 태그를 출력한다. th:if의 편의 기능
-->
<label for="itemName">상품명</label>
<!-- th:field를 사용하면 id 와 name, value을 생략할 수 있다. 그러나 label로 묶여있는 경우에는 id속성을 남겨놔야한다. -->
<!-- <input type="text" id="itemName" name="itemName" th:field="${item.itemName}" class="form-control" placeholder="이름을 입력하세요"> -->
<input type="text" id="itemName" th:field="*{itemName}" th:errorclass="field-error" class="form-control" placeholder="이름을 입력하세요">
<div class="field-error" th:errors="*{itemName}">상품명 오류</div>
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" th:field="*{price}" th:errorclass="field-error" class="form-control" placeholder="가격을 입력하세요">
<div class="field-error" th:errors="*{price}">상품 가격 오류</div>
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" th:field="*{quantity}" th:errorclass="field-error" class="form-control" placeholder="수량을 입력하세요">
<div class="field-error" th:errors="*{quantity}">상품 수량 오류</div>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">
상품등록
</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/items}'|"
type="button">
취소
</button>
</div>
</div>
</form>
</div>
<!-- /container -->
</body>
</html>
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link href="./css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}"
rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 수정 폼</h2>
</div>
<form action="item.html" th:object="${item}" th:action method="post">
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" th:field="*{id}" class="form-control" value="1" th:value="${item.id}" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}" class="form-control" value="상품A" th:value="${item.itemName}">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" th:field="*{price}" class="form-control" value="10000" th:value="${item.price}">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" th:field="*{quantity}" class="form-control" value="10" th:value="${item.quantity}">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">
저장
</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='item.html'"
th:onclick="|location.href='@{/items/{itemId}(itemId=${item.id})}'|"
type="button">
취소
</button>
</div>
</div>
</form>
</div>
<!-- /container -->
</body>
</html>
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link href="./css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}"
rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 상세</h2>
</div>
<!-- 추가 -->
<h2 th:if="${param.status}" th:text="'저장완료!'"></h2>
<div>
<label for="itemId">상품 ID</label>
<input type="text" id="itemId" name="itemId" class="form-control" th:value="${item.id}" value="1" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control" th:value="${item.itemName}" value="상품A" readonly>
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control" th:value="${item.price}" value="10000" readonly>
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control" th:value="${item.quantity}" value="10" readonly>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<!-- /basic/items/아이템Id/edit -->
<button class="w-100 btn btn-primary btn-lg"
onclick="location.href='editForm.html'"
th:onclick="|location.href='@{/items/{itemId}/edit(itemId=${item.id})}'|"
type="button">
상품수정
</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/items}'|"
type="button">
목록으로
</button>
</div>
</div>
</div>
<!-- /container -->
<script th:inline="javascript">
if([[${param.status}]]){
alert("저장완료!");
}
</script>
</body>
</html>
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link href="./css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}"
rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>상품 목록</h2>
</div>
<div class="row">
<div class="col">
<button class="btn btn-primary float-end"
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/items/add}'|"
type="button">
상품 등록
</button>
</div>
</div>
<hr class="my-4">
<div>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>상품명</th>
<th>가격</th>
<th>수량</th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${items}">
<td>
<!-- /basic/items/아이템의ID -->
<a href="item.html"
th:href="@{/items/{itemId}(itemId=${item.id})}"
th:text="${item.id}">1</a>
</td>
<td>
<!-- /basic/items/아이템의ID -->
<a href="item.html"
th:href="@{|/items/${item.id}|}"
th:text="${item.itemName}">itemName</a>
</td>
<td th:text="${item.price}">10000</td>
<td th:text="${item.quantity}">10</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- /container -->
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
.field-error {
border-color: #dc3545;
color: #dc3545;
}
</style>
<title>Insert title here</title>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>로그인</h2>
</div>
<form action="" th:action th:object="${loginForm}" method="post">
<div>
<label for="loginId">로그인 ID</label>
<input type="text" id="loginId" th:field="*{loginId}" class="form-control">
</div>
<div>
<label for="password">비밀번호</label>
<input type="password" id="password" th:field="*{password}"class="form-control">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">
로그인
</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/}'|"
type="button">취소</button>
</div>
</div>
</form>
</div>
<!-- /container -->
</body>
<script>
let message = "[[${msg}]]";
if(message != ""){
alert(message);
}
</script>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link th:href="@{/css/bootstrap.min.css}"
href="/css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
.field-error {
border-color: #dc3545;
color: #dc3545;
}
</style>
<title>Insert title here</title>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>회원 가입</h2>
</div>
<h4 class="mb-3">회원 정보 입력</h4>
<form action="" id="myForm" th:action th:object="${member}" method="post">
<div>
<label for="loginId">로그인 ID</label>
<input type="text" id="loginId" th:field="*{loginId}" class="form-control" >
</div>
<div>
<label for="password">비밀번호</label>
<input type="password" id="password" th:field="*{password}" class="form-control" >
</div>
<div>
<label for="name">이름</label>
<input type="text" id="name" th:field="*{name}" class="form-control" >
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg"
type="submit" >
회원가입
</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/}'|"
type="button">
취소
</button>
</div>
</div>
</form>
</div>
<!-- /container -->
</body>
</html>
출처
https://media.giphy.com/media/kyUIknbbDNvID5XzU4/giphy.gif
https://media.giphy.com/media/A6aHBCFqlE0Rq/giphy.gif