우리는 아래와 같은 코드를 객체리터럴(Object literal)이라고 알고 있습니다.
var healthObj = {
name : "달리기",
lastTime : "PM10:12",
showHealth : function() {
console.log(this.name + "님, 오늘은 " + this.lastTime + "에 운동을 하셨네요");
}
}
healthObj.showHealth();
그런데 healthObj의 형태를 가진 여러개의 객체가 필요하다면 어떻게 할까요?
healthObj2, healthObj3...
그렇게 구현해도 되지만, 비슷한 객체를 계속 중복해서 만들어 두는 건 이상해보이죠.
더군다나 각 객체마다 showHealth라는 메서드는 중복으로 들어가 있을 겁니다.
따라서 이를 좀 더 개선할 필요가 있어보이네요.
먼저 객체를 동적으로 생성하는 방법을 알아보겠습니다.
방법은 간단한데 아래처럼 함수를 이용하는 것입니다.
먼저, 아래 코드를 실행해보세요.
function Health(name, lastTime) {
this.name = name;
this.lastTime = lastTime;
this.showHealth = function(){...}
}
const h = new Health("달리기", "10:12");
내부적으로 안에 return this가 생략되어있다고 생각하면 좋다
h는 객체입니다.
h안을 들여다보면 어떻게 구성되어 있는지 알 수 있습니다.
Health함수를 한 번 더 불러서 h2객체를 만듭니다.
h2 = new Health("걷기", "20:11");
계속 이런식으로 객체를 만들어낼 수 있습니다.
Health함수는 new키워드로 불리면서, 객체를 생성하는 함수 역할을 합니다.
그래서 Health를 생성자(constructor)라고 합니다.
하지만 아직 문제가 하나 존재합니다.
아직 h와 h2를 열어보면 showHealth메서드가 여전히 중복해서 존재합니다.
이처럼 동일한 메서드가 여기저기 메모리 공간을 차지하는 것은 분명 자원 낭비입니다.
자바스크립트는 prototype이라는 공간을 통해서 객체간의 재사용 되는 것을 바라보게 할 수 있습니다.
개념적으로는 이렇습니다.
prototype타입이라는 것은 신기합니다.
객체지향 언어를 배운 분들은 '이것이 상속(inheritance)인가?'싶을 겁니다.
실제로 비슷한 개념이라 할 수도 있습니다.
위 그림에서는 각 인스턴스(h, h2, h3)가 prototype이라는 같은 참조객체를 바라보고 있는 것입니다.
따라서 prototype의 어떤 속성을 변경하면 모든 인스턴스에게 공유됩니다.
prototype에 어떠한 속성을 추가하면서 실제 코드로 확인해보겠습니다.
아래 코드를 보면 Health 함수 아래 prototype 객체가 존재하고, 이것에 showHealth 메서드를 속성으로 추가했습니다.
이런 식으로 prototype객체 안에 여러 가지 속성을 추가할 수 있습니다.
function Health(name, lastTime) {
this.name = name;
this.lastTime = lastTime;
}
Health.prototype.showHealth = function() {
console.log(this.name + "," + this.lastTime);
}
const h = new Health("달리기", "10:12");
console.log(h); //크롬개발자도구를 열고 이 부분이 어떻게 출력되는지 확인해보세요
h.showHealth();
그럼 아래처럼 여러 인스턴스를 만들어도 prototype안의 showHealth는 같은 참조점을 바라보고 있는 것을 알 수 있습니다.
const h = new Health("달리기", "10:12");
const h2 = new Health("걷기", "14:20");
console.log(h.showHealth === h2.showHealth); //true
TabUI 동작 코드를 생성자패턴으로 구현해볼 수 있습니다.
이렇게 구현한 코드는 UI Component라고 말하곤 합니다.
이전에 구현했던 코드는 아래와 같습니다.
function makeTemplate(data, clickedName) {
var html = document.getElementById("tabcontent").innerHTML;
var resultHTML = "";
for (var i = 0; i < data.length; i++) {
if (data[i].name === clickedName) {
resultHTML = html.replace("{name}", data[i].name)
.replace("{favorites}", data[i].favorites.join(" "));
break;
}
}
document.querySelector(".content").innerHTML = resultHTML;
}
function sendAjax(url, clickedName) {
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", function () {
var data = JSON.parse(oReq.responseText);
makeTemplate(data, clickedName);
});
oReq.open("GET", url);
oReq.send();
}
var tabmenu = document.querySelector(".tabmenu");
tabmenu.addEventListener("click", function (evt) {
sendAjax("./json.txt", evt.target.innerText);
});
전역공간에 구현된 코드를 prototype기반의 클래스로 구현해볼 겁니다.
prototype기반 코드는 하나의 클래스(모듈)로 만드는 것으로, 기존코드의 큰 수정 없이 변경할 수 있습니다.
우리는 지금처럼 비슷한 기능 덩어리를 하나의 객체, 즉 클래스 형태로 만들 수가 있습니다.
영상에 노출된 코드는 아래에서도 확인할 수 있습니다.
<html>
<header>
<link rel="stylesheet" href="tabui.css">
<style>
h2 {
text-align: center
}
h2,
h4 {
margin: 0px;
}
.tab {
width: 600px;
margin: 0px auto;
}
.tabmenu {
background-color: bisque;
}
.tabmenu>div {
display: inline-block;
width: 146px;
height: 50px;
line-height: 50px;
text-align: center;
cursor: pointer;
}
.content {
padding: 5%;
background-color: antiquewhite;
}
</style>
</header>
<body>
<h2> TAB UI TEST</h2>
<div class="tab">
<div class="tabmenu">
<div>crong</div>
<div>jk</div>
<div>pobi</div>
<div>honux</div>
</div>
<section class="content">
<h4>hello jk</h4>
<p>golf, facebook</p>
</section>
</div>
<script>
function Tab(tabElement) {
this.tabmenu = tabElement;
this.registerEvents();
}
Tab.prototype = {
registerEvents : function() {
this.tabmenu.addEventListener("click", function (evt) {
this.sendAjax("./json.txt", evt.target.innerText);
}.bind(this));
},
sendAjax : function(url, clickedName) {
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", function () {
var data = JSON.parse(oReq.responseText);
this.makeTemplate(data, clickedName);
}.bind(this));
oReq.open("GET", url);
oReq.send();
},
makeTemplate : function(data, clickedName) {
var html = document.getElementById("tabcontent").innerHTML;
var resultHTML = "";
for (var i = 0; i < data.length; i++) {
if (data[i].name === clickedName) {
resultHTML = html.replace("{name}", data[i].name)
.replace("{favorites}", data[i].favorites.join(" "));
break;
}
}
document.querySelector(".content").innerHTML = resultHTML
}
}
var tabmenu = document.querySelector(".tabmenu");
var o = new Tab(tabmenu);
/*
function makeTemplate(data, clickedName) {
var html = document.getElementById("tabcontent").innerHTML;
var resultHTML = "";
for (var i = 0; i < data.length; i++) {
if (data[i].name === clickedName) {
resultHTML = html.replace("{name}", data[i].name)
.replace("{favorites}", data[i].favorites.join(" "));
break;
}
}
document.querySelector(".content").innerHTML = resultHTML;
}
function sendAjax(url, clickedName) {
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", function () {
var data = JSON.parse(oReq.responseText);
makeTemplate(data, clickedName);
});
oReq.open("GET", url);
oReq.send();
}
var tabmenu = document.querySelector(".tabmenu");
tabmenu.addEventListener("click", function (evt) {
sendAjax("./json.txt", evt.target.innerText);
});
*/
</script>
<script id="tabcontent" type="my-template">
<h4>hello {name}</h4>
<p>{favorites}</p>
</script>
</body>
</html>
숫자 하나 찾기
ar result = "abc3zzz".match(/\d/)[0];
console.log(result);
숫자 두 개 찾기
var result = "abc32zzz".match(/\d\d/)[0];
console.log(result);
우편번호 구/신
var result1 = "19323".match(/(\d{3}-\d{3}|\d{5})/)[0];
console.log (result1);
var result2 = "193-123".match(/(\d{3}-\d{3}|\d{5})/)[0];
console.log (result2);
"92405".match(/(\d{3}-\d{3}|[0-46-8]\d{4}/)[0];
대쉬로 나뉘거나 안나뉜 우편번호를 나타내되, 안나뉜 경우 뒤에 첫 숫자는 5와 9는 안되는 경우
핸드폰 전화번호 규칙
var result = "010-9021-0011".match("/01[01789]-\d{3,4}-\d{4}/")
개발도구에서의 함수 선택
\(?function\s+[a-zA-Z_$]+
치환
var result = "011-021-0011".replace(/(\d{2})\d/, "$10");;
console.log(result);
>"010-021-0011"
$1은괄호를 나타내고, 그 뒤에 숫자를 0으로 바꿔라라는 의미다.
탐욕적(greedy), 게으른(lazy) 수량자
뒤에서부터 찾거나, 앞에서부터 찾거나 (lazy를 써서 앞에서부터 찾도록 합니다.)
greedy : , +, {n,}
lazy : ?, +?, {n,}?
탐욕적으로 최대한 길게 잡는다
물음표를 붙이면 게으르게 하나만 찾는다
<form action="/join" method="post">
<div class="inputWrap">
<div class="email">
<span> Email </span> <input type="text" name="email"><br/>
</div>
<div class="password">
<span> Password </span> <input type="password" name="password"><br/>
</div>
</div>
<input class="sendbtn" type="submit">
</form>
name이 서버에 보낼 때 키 값이 되는 것이다.
다음의 form 코드에서 email 정보가 올바른지 유효성검증을 하고 싶습니다.
<form action="/join" method="post">
<div class="inputWrap">
<div class="email">
<span> Email </span> <input type="text" name="email"><br/>
</div>
<div class="password">
<span> Password </span> <input type="password" name="password"><br/>
</div>
</div>
<input class="sendbtn" type="submit">
</form>
이 부분처리를 서버에서 한다면 사용자는 꽤 답답할 겁니다.
왜냐하면, 서버에 갈 때까지 email 정보가 틀렸는지 알 수가 없기 때문입니다.
예를 들어 다른 값을 모두 다 넣고 확인을 눌러서 서버로 데이터를 보냈는데, email 정보가 틀렸다고 메시지가 뒤늦게 나온다면 사용자는 당황할 겁니다.
좀 더 좋은 UX를 제공하기 위해서는 에러 메시지를 더 빨리 사용자에게 노출해주는 것이 좋습니다. form 검증방법은 아래와 같이 구현할 수가 있습니다.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title> Join !</title>
<link rel="stylesheet" href="/css/ui.css">
</head>
<body>
<h1>Join my website!</h1>
<div class="formWrap">
<form action="/join" method="post" id="myform">
<div class="inputWrap">
<div class="email">
<span> Email </span> <input type="text" name="email"><br/>
</div>
<div class="password">
<span> Password </span> <input type="password" name="password"><br/>
</div>
</div>
<input class="sendbtn" type="submit">
</form>
</div>
<section class="result"></section>
<script>
var btn = document.querySelector(".sendbtn");
var result = document.querySelector(".result");
btn.addEventListener("click", function(evt) {
evt.preventDefault();
var emailValue = document.querySelector("[name='email']").value;
var bValid = (/^[\w+_]\w+@\w+\.\w+$/).test(emailValue);
if(!bValid) {
result.innerHTML = "올바르지 않은 이메일입니다";
} else {
result.innerHTML = "이메일정보가 좋아요~";
document.querySelector("#myform").submit();
}
});
</script>
</body>
</html>
preventDefault()를 이용해서 클릭하자마자 서버로 전송되는 것을 막았다.
정규식을 통해서 이메일 형식을 정의한다. 시작은 문자 또는 언더바, 그리고 끝은 무자열이게 정의했다.
위 코드에서는 addEventListener에서 click 이벤트를 사용했습니다. 다른 방법도 있습니다.
'submit'이벤트를 통해서 역시 동일하게 form체크와 데이터 전송을할 수있습니다.
아래 방법은 위 방법보다 좀더 의미적으로 와닿을 겁니다.
document.querySelector("#myform").addEventListener("submit", function(evt) {
console.log(evt.target);
});
HTTP프로토콜은 상태 유지가 안되는 프로토콜입니다.
상태유지가 안되는 프로토콜은 무상태 프로토콜(stateless protocol)이라 하고 HTTP, UDP, DNS가 있습니다.
상태를 유지하는 프로토콜은 상태 프로토콜(stateful protocol)이라 하고 FTP, Telnet이 있습니다.
서버에서 쿠키 생성, Reponse의 addCookie메소드를 이용해 클라이언트에게 전송
Cookie cookie = new Cookie(이름, 값);
response.addCookie(cookie);
클라이언트가 보낸 쿠키 정보 읽기
Cookie[] cookies = request.getCookies();
클라이언트에게 쿠키 삭제 요청
Cookie cookie = new Cookie("이름", null);
cookie.setMaxAge(0);
response.addCookie(cookie);
같은 이름을 가진 두 쿠키가 존재할 수 없기 때문에 이렇게 보내면 삭제 된다.
쿠키의 유효기간 설정
@CookieValue 애노테이션 사용
컨트롤러메소드(@CookieValue(value="쿠키이름", required=false, defaultValue="기본값") String 변수명)
package kr.or.connect.guestbook.controller;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.CookieValue;
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 kr.or.connect.guestbook.dto.Guestbook;
import kr.or.connect.guestbook.service.GuestbookService;
@Controller
public class GuestbookController {
@Autowired
GuestbookService guestbookService;
@GetMapping(path="/list")
public String list(@RequestParam(name="start", required=false, defaultValue="0") int start,
ModelMap model,
HttpServletRequest request,
HttpServletResponse response) {
String value = null;
boolean find = false;
Cookie[] cookies = request.getCookies();
if(cookies != null) {
for(Cookie cookie : cookies) {
if("count".equals(cookie.getName())) {
find = true;
value = cookie.getValue();
}
}
}
if(!find) {
value = "1";
}else { // 쿠키를 찾았다면.
try {
int i = Integer.parseInt(value);
value = Integer.toString(++i);
}catch(Exception ex) {
value = "1";
}
}
Cookie cookie = new Cookie("count", value);
cookie.setMaxAge(60 * 60 * 24 * 365); // 1년 동안 유지.
cookie.setPath("/"); // / 경로 이하에 모두 쿠키 적용.
response.addCookie(cookie);
List<Guestbook> list = guestbookService.getGuestbooks(start);
int count = guestbookService.getCount();
int pageCount = count / GuestbookService.LIMIT;
if(count % GuestbookService.LIMIT > 0)
pageCount++;
List<Integer> pageStartList = new ArrayList<>();
for(int i = 0; i < pageCount; i++) {
pageStartList.add(i * GuestbookService.LIMIT);
}
model.addAttribute("list", list);
model.addAttribute("count", count);
model.addAttribute("pageStartList", pageStartList);
model.addAttribute("cookieCount", value); // jsp에게 전달하기 위해서 쿠키 값을 model에 담아 전송한다.
return "list";
}
@PostMapping(path="/write")
public String write(@ModelAttribute Guestbook guestbook,
HttpServletRequest request) {
String clientIp = request.getRemoteAddr();
System.out.println("clientIp : " + clientIp);
guestbookService.addGuestbook(guestbook, clientIp);
return "redirect:list";
}
list.jsp 에서 방명록 전체 수 옆에 방문한 수를 출력하는 el 코드를 추가합니다.
package kr.or.connect.guestbook.controller;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.CookieValue;
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 kr.or.connect.guestbook.dto.Guestbook;
import kr.or.connect.guestbook.service.GuestbookService;
@Controller
public class GuestbookController {
@Autowired
GuestbookService guestbookService;
@GetMapping(path="/list")
public String list(@RequestParam(name="start", required=false, defaultValue="0") int start,
ModelMap model, @CookieValue(value="count", defaultValue="1", required=true) String value,
HttpServletResponse response) {
// 쿠키 값을 1증가 시킨다.
try {
int i = Integer.parseInt(value);
value = Integer.toString(++i);
}catch(Exception ex){
value = "1";
}
// 쿠키를 전송한다.
Cookie cookie = new Cookie("count", value);
cookie.setMaxAge(60 * 60 * 24 * 365); // 1년 동안 유지.
cookie.setPath("/"); // / 경로 이하에 모두 쿠키 적용.
response.addCookie(cookie);
List<Guestbook> list = guestbookService.getGuestbooks(start);
int count = guestbookService.getCount();
int pageCount = count / GuestbookService.LIMIT;
if(count % GuestbookService.LIMIT > 0)
pageCount++;
List<Integer> pageStartList = new ArrayList<>();
for(int i = 0; i < pageCount; i++) {
pageStartList.add(i * GuestbookService.LIMIT);
}
model.addAttribute("list", list);
model.addAttribute("count", count);
model.addAttribute("pageStartList", pageStartList);
model.addAttribute("cookieCount", value); // 쿠키를 추가한다.
return "list";
}
@PostMapping(path="/write")
public String write(@ModelAttribute Guestbook guestbook,
HttpServletRequest request) {
String clientIp = request.getRemoteAddr();
System.out.println("clientIp : " + clientIp);
guestbookService.addGuestbook(guestbook, clientIp);
return "redirect:list";
}
}
정의
이용 방법
HttpSession session = request.getSession();
HttpSession session = request.getSession(true);
HttpSession session = request.getSession(false);
setAttribute(String name, Object value)
session.setAttribute(이름, 값)
getAttribute(String name) 메소드
String value = (String) session.getAttribute("id");
세션은 클라이언트가 서버에 접속하는 순간 생성
<session-config>
<session-timeout>30</session-timeout>
</session-config>
실습코드
GuessNumberController
package kr.or.connect.guestbook.controller;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class GuessNumberController {
@GetMapping("/guess")
public String guess(@RequestParam(name="number", required=false) Integer number,
HttpSession session,
ModelMap model) {
String message = null;
// get방식으로 /guess 를 요청하는데 파라미터 number가 없을 경우에는 session에 count를 0으로 randomNumber엔 1부터 100사이의 값을 저장합니다.
if(number == null) {
session.setAttribute("count", 0);
session.setAttribute("randomNumber", (int)(Math.random() * 100) + 1); // 1 ~ 100사이의 random값
message = "내가 생각한 숫자를 맞춰보세요.";
}else {
// number파라미터가 있을 경우 세션에서 값을 읽어들인 후, number와 세션에 저장된 값을 비교합니다.
// 값을 비교해서 작거나 크다면 카운트를 1증가시켜주고
// 값이 같다면 세션 정보를 삭제합니다.
// 각 상황에 맞는 메시지를 message변수에 저장을 한 후 jsp에게 전달하기 위해서 ModelMap의 addAttribute메소드를 통해 전달하게 됩니다.
int count = (Integer)session.getAttribute("count");
int randomNumber = (Integer)session.getAttribute("randomNumber");
if(number < randomNumber) {
message = "입력한 값은 내가 생각하고 있는 숫자보다 작습니다.";
session.setAttribute("count", ++count);
}else if(number > randomNumber) {
message = "입력한 값은 내가 생각하고 있는 숫자보다 큽니다.";
session.setAttribute("count", ++count);
}else {
message = "OK " + ++count + " 번째 맞췄습니다. 내가 생각한 숫자는 " + number + " 입니다.";
session.removeAttribute("count");
session.removeAttribute("randomNumber");
}
}
model.addAttribute("message", message);
return "guess";
}
}
guess.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>숫자 맞추기 게임</title>
</head>
<body>
<h1> 숫자 맞추기 게임.</h1>
<hr>
<h3>${message }</h3>
<c:if test="${sessionScope.count != null}">
<form method="get" action="guess">
1부터 100사이의 숫자로 맞춰주세요.<br>
<input type="text" name="number"><br>
<input type="submit" value="확인">
</form>
</c:if>
<a href="guess">게임 다시 시작하기.</a>
</body>
</html>
@SessionAttributes 파라미터로 지정된 이름과 같은 이름이 @ModelAttribute에 지정되어 있을 경우 메소드가 반환되는 값은 세션에 저장됩니다.
아래의 예제는 세션에 값을 초기화하는 목적으로 사용되었습니다.
@SessionAttributes("user")
public class LoginController {
@ModelAttribute("user")
public User setUpUserForm() {
return new User();
}
}
@SessionAttributes의 파라미터와 같은 이름이 @ModelAttribute에 있을 경우 세션에 있는 객체를 가져온 후, 클라이언트로 전송받은 값을 설정합니다.
@Controller
@SessionAttributes("user")
public class LoginController {
......
@PostMapping("/dologin")
public String doLogin(@ModelAttribute("user") User user, Model model) {
......
}
}
메소드에 @SessionAttribute가 있을 경우 파라미터로 지정된 이름으로 등록된 세션 정보를 읽어와서 변수에 할당합니다.
@GetMapping("/info")
public String userInfo(@SessionAttribute("user") User user) {
//...
//...
return "user";
}
SessionStatus 는 컨트롤러 메소드의 파라미터로 사용할 수 있는 스프링 내장 타입입니다.
이 오브젝트를 이용하면 현재 컨트롤러의 @SessionAttributes에 의해 저장된 오브젝트를 제거할 수 있습니다.
@Controller
@SessionAttributes("user")
public class UserController {
......
@RequestMapping(value = "/user/add", method = RequestMethod.POST)
public String submit(@ModelAttribute("user") User user, SessionStatus sessionStatus) {
......
sessionStatus.setComplete();
......
}
}
modelAttribute속성으로 지정된 이름의 객체를 세션에서 읽어와서 form태그로 설정된 태그에 값을 설정합니다.
<form:form action="login" method="post" modelAttribute="user">
Email : <form:input path="email" /><br>
Password : <form:password path="password" /><br>
<button type="submit">Login</button>
</form:form>
GuestbookAdminController.java
package kr.or.connect.guestbook.controller;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
public class GuestbookAdminController {
@GetMapping(path="/loginform")
public String loginform() {
return "loginform";
}
@PostMapping(path="/login")
public String login(@RequestParam(name="passwd", required=true) String passwd,
HttpSession session,
RedirectAttributes redirectAttr) {
if("1234".equals(passwd)) {
session.setAttribute("isAdmin", "true");
}else {
redirectAttr.addFlashAttribute("errorMessage","암호가 틀렸습니다.");
return "redirect:/loginform";
}
return "redirect:/list";
}
@GetMapping(path="/logout")
public String login(HttpSession session) {
session.removeAttribute("isAdmin");
return "redirect:/list";
}
}
loginform.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>loginform</title>
</head>
<body>
<h1>관리자 로그인</h1>
<br><br>
${errorMessage}<br>
<form method="post" action="login">
암호 : <input type="password" name="passwd"><br>
<input type="submit">
</form>
</body>
</html>
기존 코드에서 /delete 삭제 부분을 추가합니다.
세션에 isAdmin이름의 값이 있을 경우에만 삭제 처리를 하도록 합니다.
GuestbookController.java
package kr.or.connect.guestbook.controller;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.CookieValue;
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.bind.annotation.SessionAttribute;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import kr.or.connect.guestbook.dto.Guestbook;
import kr.or.connect.guestbook.service.GuestbookService;
@Controller
public class GuestbookController {
@Autowired
GuestbookService guestbookService;
@GetMapping(path="/list")
public String list(@RequestParam(name="start", required=false, defaultValue="0") int start,
ModelMap model, @CookieValue(value="count", defaultValue="1", required=true) String value,
HttpServletResponse response) {
try {
int i = Integer.parseInt(value);
value = Integer.toString(++i);
}catch(Exception ex){
value = "1";
}
Cookie cookie = new Cookie("count", value);
cookie.setMaxAge(60 * 60 * 24 * 365); // 1년 동안 유지.
cookie.setPath("/"); // / 경로 이하에 모두 쿠키 적용.
response.addCookie(cookie);
List<Guestbook> list = guestbookService.getGuestbooks(start);
int count = guestbookService.getCount();
int pageCount = count / GuestbookService.LIMIT;
if(count % GuestbookService.LIMIT > 0)
pageCount++;
List<Integer> pageStartList = new ArrayList<>();
for(int i = 0; i < pageCount; i++) {
pageStartList.add(i * GuestbookService.LIMIT);
}
model.addAttribute("list", list);
model.addAttribute("count", count);
model.addAttribute("pageStartList", pageStartList);
model.addAttribute("cookieCount", value);
return "list";
}
@PostMapping(path="/write")
public String write(@ModelAttribute Guestbook guestbook,
HttpServletRequest request) {
String clientIp = request.getRemoteAddr();
System.out.println("clientIp : " + clientIp);
guestbookService.addGuestbook(guestbook, clientIp);
return "redirect:list";
}
@GetMapping(path="/delete")
public String delete(@RequestParam(name="id", required=true) Long id,
@SessionAttribute("isAdmin") String isAdmin,
HttpServletRequest request,
RedirectAttributes redirectAttr) {
if(isAdmin == null || !"true".equals(isAdmin)) { // 세션값이 true가 아닐 경우
redirectAttr.addFlashAttribute("errorMessage", "로그인을 하지 않았습니다.");
return "redirect:loginform";
}
String clientIp = request.getRemoteAddr();
guestbookService.deleteGuestbook(id, clientIp);
return "redirect:list";
}
}
기존 list.jsp에서 isAdmin세션값이 있을 경우 삭제 링크를 걸업줍니다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>방명록 목록</title>
</head>
<body>
<h1>방명록</h1>
<br>
방명록 전체 수 : ${count }, 방문한 수 : ${cookieCount }<br><br>
<c:forEach items="${list}" var="guestbook">
${guestbook.id }<br>
${guestbook.name }<br>
${guestbook.content }<br>
${guestbook.regdate }<br>
<c:if test="${sessionScope.isAdmin == 'true'}"><a href="delete?id=${guestbook.id}">삭제</a><br><br></c:if>
</c:forEach>
<br>
<c:forEach items="${pageStartList}" var="pageIndex" varStatus="status">
<a href="list?start=${pageIndex}">${status.index +1 }</a>
</c:forEach>
<br><br>
<form method="post" action="write">
name : <input type="text" name="name"><br>
<textarea name="content" cols="60" rows="6"></textarea><br>
<input type="submit" value="등록">
</form>
</body>
</html>
Interceptor는 Dispatcher servlet
에서 Handler(Controller)
로 요청을 보낼 때, Handler
에서 Dispathcer servlet
으로 응답을 보낼 때 동작합니다.
LogInterceptor.java
package kr.or.connect.guestbook.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
public class LogInterceptor extends HandlerInterceptorAdapter{
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println(handler.toString() + " 가 종료되었습니다. " + modelAndView.getViewName() + "을 view로 사용합니다.");
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println(handler.toString() + " 를 호출했습니다.");
return true;
}
}
WebMvcContextConfiguration 에 addInterceptors()메소드를 추가합니다.
인자로 넘어온 InterceptorRegistry의 addInterceptor에 앞에서 만든 인터셉터 객체를 넣어주면 추가가 됩니다.
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor());
}
이번 시간엔 컨트롤러의 메소드의 인자값으로 사용자가 임의의 값을 전달할 수 있도록 도와주는 아규먼트 리졸버(Argument Resolver)에 대해 알아보도록 하겠습니다.
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="아규먼트리졸버클래스"></bean>
</mvc:argument-resolvers>
</mvc:annotation-driven>
getDefaultArgumentResolvers()메소드를 보면 기본으로 설정되는 아규먼트 리졸버에 어떤 것이 있는지 알 수 있습니다.
Map객체나 Map을 상속받은 객체는 Spring에서 이미 선언한 아규먼트 리졸버가 처리하기 때문에 전달 할 수 없습니다.
Map객체를 전달하려면 Map을 필드로 가지고 있는 별도의 객체를 선언한 후 사용해야 합니다.
HeaderInfo.java
package kr.or.connect.guestbook.argumentresolver;
import java.util.HashMap;
import java.util.Map;
public class HeaderInfo {
private Map<String, String> map;
public HeaderInfo() {
map = new HashMap<>();
}
public void put(String name, String value) {
map.put(name, value);
}
public String get(String name) {
return map.get(name);
}
}
HeaderMapArgumentResolver.java
package kr.or.connect.guestbook.argumentresolver;
import java.util.Iterator;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
public class HeaderMapArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType() == HeaderInfo.class;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HeaderInfo headerInfo = new HeaderInfo();
Iterator<String> headerNames = webRequest.getHeaderNames();
while(headerNames.hasNext()) {
String headerName = headerNames.next();
String headerValue = webRequest.getHeader(headerName);
// System.out.println(headerName + " , " + headerValue);
headerInfo.put(headerName, headerValue);
}
return headerInfo;
}
}
supportsParameter에서 true가 반환되는 경우에만 resolveArgument가 호출이 된다. resolveArgument가 반환한 값은 컨트롤러 메서드의 인자로 전달 된다.
아규먼트 리졸버를 적용하려면 WebMvcContextConfiguration 클래스에 addArgumentResolvers메소드를 오버라이딩 하고, 인자로 넘어온 argumentResolvers에 앞에서 생성한 아규먼트 리졸버를 넘겨줘야 합니다.
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
System.out.println("아규먼트 리졸버 등록..");
argumentResolvers.add(new HeaderMapArgumentResolver());
}
GuestbookController 의 메소드인 list메소드의 인자로 HeaderInfo headerInfo를 추가합니다.
콘솔에 headerInfo의 get메소드에 user-agent를 넘겨서 값이 잘 출력되는지 확인할 수 있도록 코드를 추가합니다.
@GetMapping(path="/list")
public String list(@RequestParam(name="start", required=false, defaultValue="0") int start,
ModelMap model, @CookieValue(value="count", defaultValue="1", required=true) String value,
HttpServletResponse response,
HeaderInfo headerInfo) {
System.out.println("-----------------------------------------------------");
System.out.println(headerInfo.get("user-agent"));
System.out.println("-----------------------------------------------------");