앞선 포스팅에서 todo delete 기능을 만들었다면, 이번 포스팅에서는 update 기능을 만들 것.
update 버튼을 만들고, update 버튼을 누르면 업데이트된 todo 화면을 보여줘야 되고, 실제로 todo 리스트에 업데이트된 todo가 추가되어야 한다.
update 버튼을 1개 더 만들었다.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<meta charset="UTF-8">
<link href = "webjars/bootstrap/5.1.3/css/bootstrap.min.css" rel = "stylesheet">
<title>List Todos Page</title>
</head>
<body>
<div class = "container">
<h1>Your Todos</h1>
<table class = "table">
<thead>
<tr>
<th>id</th>
<th>Description</th>
<th>Deadline</th>
<th>is Done?</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<c:forEach items = "${todos}" var = "todo">
<tr>
<td>${todo.id}</td>
<td>${todo.description}</td>
<td>${todo.deadline}</td>
<td>${todo.done}</td>
<td> <a href = "delete-todo?id=${todo.id}" class = "btn btn-warning">DELETE</a> <td>
<td><a href = "update-todo?id=${todo.id}" class = "btn btn-success">UPDATE</a></td>
</tr>
</c:forEach>
</tbody>
</table>
<a href = "add-todo" class = "btn btn-success">Add Todo</a>
<script src="webjars/bootstrap/5.1.3/js/bootstrap.min.js"></script>
<script src="webjars/jquery/3.6.0/jquery.min.js"></script>
</div>
</body>
</html>
/list-todos URL 실행결과,
하지만 이 상태에서 UPDATE 버튼을 누르면 에러가 나므로, /update-todo URL에 대응되는 todo 컨트롤러를 만들 것.
이때 UPDATE 버튼과 Add Todo 기능의 차이를 말하자면, Add Todo는 기존에 존재하던 todo 리스트에 id값을 1개 더한 후, 그 다음 todo 리스트를 추가하는 것. 반면, UPDATE 버튼은 "특정" id값을 가진 todo의 내용을 변경한다. 즉, 본래의 todo 리스트 개수에는 별 영향을 끼치지 않는다.
// update - todo
@RequestMapping("/update-todo")
public String showUpdateTodoPage(@RequestParam int id, ModelMap model) {
Todo todo = todoService.findById(id);
model.put("todo", todo);
return "addTodo";
}
리턴값 : addTodo
model.put("todo", todo) 가 필요한 이유
<form:form>
태그를 이용해 modelAttribut = "todo"가 있다. 근데 해당 addTodo.jsp로 mapping된 컨트롤러에 todo 이름의 모델이 없다면, 에러가 나기 때문!TodoService클래스에 findById 메서드 추가
public Todo findById(int id) {
Predicate<? super Todo> predicate = todo -> todo.getId() == id;
// predicate은 todo 리스트에서 우리가 찾는 id를 매칭하게끔 도와준다.
Todo todo = todos.stream().filter(predicate).findFirst().get();
return todo;
}
자바의 함수형 프로그래밍 이용.
조건(predicate)을 정의하고, Todo 스트림으로부터의 그 predicate를 filtering하고, 단 하나의 원소(element)를 찾아, get했다!
/list-todo URL에서 id가 2인 UPDATE 버튼을 누른 결과,
id가 2인 todo list의 description이 화면에 띄워진 것을 확인할 수 있었다.
해당 화면의 URL은 http://localhost:8080/update-todo?id=2 이었다!
하지만 이 과정까지 하면, update될 todo가 화면에 보일 뿐, 실제로 update할 내용을 누르면 해당 페이지에 변동없이 머무른다.
이유는 간단하다. /update-todo URL에서 submit을 누른 순간, GET이 아닌, POST method가 작동하는데, TodoController의 showUpdateTodoPage()에서는 해당 2가지의 메서드를 둘다 적용시키기 때문!
업데이트를 Todo 리스트에 저장하는 작업을 해보자.
showNewTodoPage() - addNewTodoPage() 와 비슷하게 showUpdateTodoPage() - updateNewtodoPage() 메서드를 만들어보자.
// update-todo - GET
@RequestMapping(value = "/update-todo", method = RequestMethod.GET)
public String showUpdateTodoPage(@RequestParam int id, ModelMap model) {
Todo todo = todoService.findById(id);
model.put("todo", todo);
return "addTodo";
}
// update-todo - POST
@RequestMapping(value = "/update-todo", method = RequestMethod.POST)
public String updateNewTodoPage(ModelMap model, @Valid Todo todo, BindingResult result) {
if (result.hasErrors()) {
return "addTodo";
}
String username = (String) model.get("name");
todoService.updateTodo(todo);
return "redirect:list-todos";
}
public void updateTodo(@Valid Todo todo) {
deleteById(todo.getId()); // showUpdateTodoPage()에서 이미 쿼리파라미터로 특정 id의 todo 기능을 만족하고,
// updateNewTodoPage()에서는 command bean을 통해 바인딩이 된 특정 todo 인스턴스기에 가능
todos.add(todo);
}
양뱡향 바인딩을 정리한 해당 시리즈의 14 포스팅의 본문과 15 포스팅의 상단부 정리를 다시 보면 이해가 쉽다.
하지만 실행결과, id 순서도 바뀌고 + deadline도 공란인 것을 확인할 수 있었다!
deadline값이 없어진 이유는, 그 값을 <form:form>
태그에서 채우고 있지 않기 때문이다. (addTodo.jsp에서)
상세 설명은 아래를 참고하자.
showUpdateTodoPage()에서 특정 id의 todo를 update해줄 addTodo.jsp를 보여줬다. 해당 뷰에서는 특정 id todo에 description만 입력한 model을 (modelAttribute = “todo”) TodoController의 /update-todo URL의 POST method(submit 버튼을 눌렀으니)로 보내줘, 바인딩해준다.
updateNewTodoPage()메서드에서는 해당 특정 id + description이 추가된, todo를 update한다. 하지만 이때 addTodo.jsp 뷰에서 보내준 todo에는 description만 input받았기에, deadline이 공란인 것이다! (기타 값이 존재하는 이유는, TodoService에서 정적 메서드값 3개를 하드코딩값으로 입력한 것이 존재하기에)
<body>
<div class = "container">
<h1>Enter Todo Details</h1>
<form:form method = "post" modelAttribute="todo">
Description : <form:input type = "text" path="description" name = "description" required = "required" />
<form:errors path = "description" cssClass = "text-warning"/> <br>
<form:input type = "hidden" path="id"/> <br>
<form:input type = "hidden" path="done"/> <br>
<input type = "submit" class = "btn btn-success"/>
</form:form>
<script src="webjars/bootstrap/5.1.3/js/bootstrap.min.js"></script>
<script src="webjars/jquery/3.6.0/jquery.min.js"></script>
</div>
</body>
아래 TodoController - updateNewTodoPage() : username을 todo 안에 설정!
// update-todo - POST
@RequestMapping(value = "/update-todo", method = RequestMethod.POST)
public String updateNewTodoPage(ModelMap model, @Valid Todo todo, BindingResult result) {
if (result.hasErrors()) {
return "addTodo";
}
String username = (String) model.get("name");
todo.setUsername(username);
todoService.updateTodo(todo);
return "redirect:list-todos";
}
deadline 설정은 다음 포스팅에서 알아본다.
update 기능을 만들기 위해, 수정된 최종 코드는 다음과 같다.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<meta charset="UTF-8">
<link href = "webjars/bootstrap/5.1.3/css/bootstrap.min.css" rel = "stylesheet">
<title>List Todos Page</title>
</head>
<body>
<div class = "container">
<h1>Your Todos</h1>
<table class = "table">
<thead>
<tr>
<th>id</th>
<th>Description</th>
<th>Deadline</th>
<th>is Done?</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<c:forEach items = "${todos}" var = "todo">
<tr>
<td>${todo.id}</td>
<td>${todo.description}</td>
<td>${todo.deadline}</td>
<td>${todo.done}</td>
<td> <a href = "delete-todo?id=${todo.id}" class = "btn btn-warning">DELETE</a> <td>
<td><a href = "update-todo?id=${todo.id}" class = "btn btn-success">UPDATE</a></td>
</tr>
</c:forEach>
</tbody>
</table>
<a href = "add-todo" class = "btn btn-success">Add Todo</a>
<script src="webjars/bootstrap/5.1.3/js/bootstrap.min.js"></script>
<script src="webjars/jquery/3.6.0/jquery.min.js"></script>
</div>
</body>
</html>
@Service
public class TodoService {
private static List<Todo> todos = new ArrayList<>();
private static int todosCount = 0;
// static 변수 초기화를 위해서는, static 블럭이 필요.
static {
todos.add(new Todo(++todosCount, "minjiki2", "Learn springboot",
LocalDate.now().plusYears(1), false));
todos.add(new Todo(++todosCount, "minjiki2", "Learn db",
LocalDate.now().plusYears(1), false));
todos.add(new Todo(++todosCount, "minjiki2", "Learn algorithm",
LocalDate.now().plusYears(2), false));
}
public List<Todo> findByUsername(String userName) {
return todos;
}
public void AddTodo(String username, String description, LocalDate deadline, boolean done) {
Todo todo = new Todo(++todosCount, username, description, deadline, done);
todos.add(todo);
}
public void deleteById(int id) {
// 람다 함수 x -> y : todo -> todo.getId() == id
Predicate<? super Todo> predicate = todo -> todo.getId() == id;
todos.removeIf(predicate); // 모든 todo bean에 대해, 해당 조건을 만족하면 remove된다.
}
public Todo findById(int id) {
Predicate<? super Todo> predicate = todo -> todo.getId() == id;
// predicate은 todo 리스트에서 우리가 찾는 id를 매칭하게끔 도와준다.
Todo todo = todos.stream().filter(predicate).findFirst().get();
return todo;
}
public void updateTodo(@Valid Todo todo) {
// TODO Auto-generated method stub
deleteById(todo.getId());
todos.add(todo);
}
}
@Controller
@SessionAttributes("name")
public class TodoController {
private TodoService todoService;
@Autowired
public TodoController(TodoService todoService) {
super();
this.todoService = todoService;
}
// list-todos
@RequestMapping("/list-todos")
public String listAllTodos(ModelMap model) {
List<Todo> todos = todoService.findByUsername("minjiki2");
model.addAttribute("todos", todos);
return "listTodos";
}
// add-todo - GET
@RequestMapping(value = "/add-todo", method = RequestMethod.GET)
public String showNewTodoPage(ModelMap model) {
String username = (String) model.get("name");
Todo todo = new Todo(0, username, "", LocalDate.now().plusYears(1), false); // 새로운 Todo를 생성할 때마다의 디폴트 todo
model.put("todo", todo); // model scope == request URL scope이기에 /add-todo에만 해당 모델값이 유지된다.
return "addTodo";
}
// add-todo - POST
@RequestMapping(value = "/add-todo", method = RequestMethod.POST)
public String addNewTodoPage(ModelMap model, @Valid Todo todo, BindingResult result) {
if (result.hasErrors()) {
return "addTodo";
}
String username = (String) model.get("name");
todoService.AddTodo(username, todo.getDescription(), LocalDate.now().plusYears(1), false);
return "redirect:list-todos";
}
// delete-todo
@RequestMapping("/delete-todo")
public String deleteTodo(@RequestParam int id) {
// Delete todo
todoService.deleteById(id);
// 삭제된 todo를 보여주는 redirect:list-todos
return "redirect:list-todos";
}
// update-todo - GET
@RequestMapping(value = "/update-todo", method = RequestMethod.GET)
public String showUpdateTodoPage(@RequestParam int id, ModelMap model) {
Todo todo = todoService.findById(id);
model.put("todo", todo);
return "addTodo";
}
// update-todo - POST
@RequestMapping(value = "/update-todo", method = RequestMethod.POST)
public String updateNewTodoPage(ModelMap model, @Valid Todo todo, BindingResult result) {
if (result.hasErrors()) {
return "addTodo";
}
String username = (String) model.get("name");
todo.setUsername(username);
todoService.updateTodo(todo);
return "redirect:list-todos";
}
}
이 시리즈는 Udemy 강의의 내용을 정리한 것입니다.
https://www.udemy.com/course/spring-boot-and-spring-framework-korean/