스프링부트 너 뭐 돼?🤷‍♀️(9) - 회원가입, 로그인 페이지

joyfulwave·2022년 12월 7일
0

피할 수 없다면 즐기자! 스프링부트 너.. 뭐 돼?




📚 간단한 회원가입, 로그인 페이지를 만들어보자

📌 프로젝트 구성

📌 src/main/java

⚫ HomeController.java

  • 로그인 유무를 확인하여 home을 보여주는 파일
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";
	}
	
}

⚫ MemberController.java

  • 회원 추가 파일
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:/";
	}
	
}

⚫ TestDataInit

  • 테스트 데이터 추가 파일
코드를 입력하세요




⚫ LogFilter.java

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);
	}

}

⚫ LoginCheckFilter

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);
	}
}

⚫ Webconfig.java

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;
	}
	
	
}




⚫ LoginCheckInterceptor

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;
	}
}

⚫ LogInterceptor

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");
		
	}
}




⚫ Item.java

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;
	}
	
	
	
}

⚫ ItemController.java

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}";
	}
	
}

⚫ ItemRepository.java

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());
	}
	
}




⚫ LoginController.java

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:/";
	}
	
}

⚫ LoginForm.java

package com.koreatit.mylogin.loginweb.login;

import lombok.Data;

@Data
public class LoginForm {
	private String loginId;
	private String password;
}

⚫ LoginService.java

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;	
		}
		
	}
	
}




⚫ Member.java

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
}

⚫ MemberRepository.java

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;
	}
}




⚫ SessionConst.java

package com.koreatit.mylogin.loginweb.session;

public class SessionConst {
	public static final String LOGIN_MEMBER = "loginMember";
	
	
}




📌 src/main/resources

⚫ home.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>
		<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>

⚫ loginHome.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>




⚫ addForm.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>

⚫ editForm.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>

⚫ item.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>

⚫ items.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>




⚫ login.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>

⚫ addMemberForm.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

0개의 댓글