스프링수업 9일차

한 일

  • pages테이블 수정 / 삭제
  • pages 정렬기능 추가
  • category 설정
  • category CRUD

pages테이블 수정 / 삭제

수정하기

- AdminPageController -

@GetMapping("/edit/{id}")
public String edit(@PathVariable int id, Model model) {
	Page page = pageRepo.getById(id);	// 테이블에서 id로 page검색
	model.addAttribute("page", page);	// 수정페이지에 page정보 객체를 전달
	return "admin/pages/edit";	// 수정 페이지로 보냄
}

- edit.html -

<div class="container">
  <div class="display-2">페이지 수정</div>
  <a th:href="@{/admin/pages}" class="btn btn-primary my-3">돌아가기</a>

  <form method="post" th:object="${page}" th:action="@{/admin/pages/edit}">
    <div th:if="${#fields.hasErrors('*')}" class="alert alert-danger">에러 발생</div>
    <div th:if="${message}" th:class="${'alert ' + alertClass}" th:text="${message}"></div>
    <input type="hidden" th:field="*{id}" />
    <input type="hidden" th:field="*{sorting}" />

    <div class="form-group">
      <label for="">제 목</label>
      <input type="text" class="form-control" th:field="*{title}" placeholder="제목" />
      <span class="error" th:if="${#fields.hasErrors('title')}" th:errors="*{title}"></span>
    </div>
    <div class="form-group">
      <label for="">슬러그</label>
      <input type="text" class="form-control" th:field="*{slug}" placeholder="슬러그" />
    </div>
    <div class="form-group">
      <label for="">내 용</label>
      <textarea class="form-control" th:field="*{content}" cols="30" rows="10" placeholder="컨텐트"></textarea>
      <span class="error" th:if="${#fields.hasErrors('content')}" th:errors="*{content}"></span>
    </div>
    <button type="submit" class="btn btn-danger">수 정</button>
  </form>
</div>

form의 action값 수정(add > edit), input:hidden타입으로 id, sorting 추가


수정버튼 클릭 시 정보가 제대로 출력됨을 확인.

- AdminPageController -

// 수정하기를 통해 작성한 값을 검사 후 업데이트
@PostMapping("/edit")
public String edit(@Valid Page page, BindingResult bindingResult, RedirectAttributes attr) {
	//유효성검사 결과 에러가 있으면 다시 돌아감
	if (bindingResult.hasErrors()) return "admin/pages/edit";
	
	attr.addFlashAttribute("message", "성공적으로 수정됨");
	attr.addFlashAttribute("alertClass", "alert-success");	// 부트스트랩 경고창(color: succeess)
	
	// 슬러그 검사
	String slug = page.getSlug() == "" ? page.getTitle().toLowerCase().replace(" ", "-") : page.getSlug();
	Page slugExist = pageRepo.findBySlugAndIdNot(slug, page.getId());	// 슬러그로 DB검색 후 있으면 Page로 리턴 - 단, 현재 id의 slug는 중복에서 제외
	
	if (slugExist != null) {	// 동일한 slug가 존재하면 저장x
		attr.addFlashAttribute("message", "입력한 slug가 이미 존재합니다.");
		attr.addFlashAttribute("alertClass", "alert-danger");
		attr.addFlashAttribute("page", page);
		
	} else {
		page.setSlug(slug);	// 소문자, '-' 수정 된 slug를 업데이트
		page.setSorting(100);
		
		pageRepo.save(page);
	}
	return "redirect:/admin/pages/edit/" + page.getId();	// /edit/{id} 로 주소가 작성되어있으므로 id값도 같이 넘겨야함
}

=> add의 유효성검사 > 저장 과 비슷하지만 저장 후 넘겨줄 페이지와 slug 중복검사부분이 다름.

- PageRepository.interface -

Page findBySlugAndIdNot(String slug, int id);	// slug를 찾되 현재 id의 slug는 제외




수정하기 테스트

삭제하기

- AdminPageController -

@GetMapping("/delete/{id}")
public String delete(@PathVariable int id, RedirectAttributes attr) {
	pageRepo.deleteById(id);
	
	attr.addFlashAttribute("message", "성공적으로 삭제되었습니다.");
	attr.addFlashAttribute("alertClass", "alert-success");
	
	return "redirect:/admin/pages";		// 인덱스 페이지로 돌아감
}

- index.html -
삭제버튼 a태그에 class추가, message를 띄울 div태그 추가

<div th:if="${message}" th:class="${'alert ' + alertClass}" th:text="${message}"></div>
	...생략...
<td><a th:href="@{'/admin/pages/delete/' + ${page.id}}" class="deleteConfirm">삭제</a></td>

- app.js -
삭제버튼 클릭 시 확인용 alert창 띄우기

$(function () {
  $(
    'a.deleteConfirm'.click(function () {
      if (!confirm('삭제하시겠습니까?')) return false; // 취소 시 삭제안함
    })
  );
});

=> a태그에 클래스명이 deleteConfirm인 태그를 클릭하면 alert창을 띄워 사용자에게 삭제여부를 물음.

footer.html <script th:src="@{/js/app.js}"></script> 추가하여 js파일을 링크.



확인 클릭 시 page삭제. 취소 클릭 시 동작없음.

home페이지 삭제불가능하도록 수정

- index.html -

<td><a th:if="${page.slug != 'home'}" class="deleteConfirm" th:href="@{'/admin/pages/delete/' + ${page.id}}">삭제</a></td>

반복문 th:each태그 내의 삭제a태그에 조건검사를 추가하여 slug가 home인 경우 a태그를 무효화함.


제이쿼리 참고

$(document).ready(function(){
	...
})

$(function(){
  	...
})

위의 두 문장은 같은 역할을 함
=> 화면로딩을 완료한 후 작성한 코드를 실행함.

참고링크


정렬기능 추가


처음에 추가한 슬림버전에는 없는 기능이므로 제이쿼리 링크를 변경함.
아래의 footer.html을 복사하여 사용해도 무관.

- footer.html -

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.0/jquery-ui.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script th:src="@{/js/app.js}"></script>

- index.html -

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head th:replace="/fragments/head :: head-admin"> </head>

  <body>
    <nav th:replace="/fragments/nav :: nav-admin"></nav>

    <main role="main" class="container">
      <div class="display-2">Pages</div>
      <a th:href="@{/admin/pages/add}" class="btn btn-primary my-3">추가하기</a>
      <div th:if="${message}" th:class="${'alert ' + alertClass}" th:text="${message}"></div>

      <div th:if="${!pages.empty}">
        <table class="table sorting" id="pages">
          <tr class="home">
            <th>제 목</th>
            <th>슬러그</th>
            <th>수 정</th>
            <th>삭 제</th>
          </tr>
          <tr th:each="page : ${pages}" th:id="'id_' + ${page.id}" th:class="${page.slug}">
            <td th:text="${page.title}"></td>
            <td th:text="${page.slug}"></td>
            <td><a th:href="@{'/admin/pages/edit/' + ${page.id}}">수정</a></td>
            <td><a th:if="${page.slug != 'home'}" class="deleteConfirm" th:href="@{'/admin/pages/delete/' + ${page.id}}">삭제</a></td>
          </tr>
        </table>
      </div>
      <div th:if="${pages.empty}">
        <div class="display-4">현재 페이지가 없습니다...</div>
      </div>
    </main>

    <footer th:replace="/fragments/footer :: footer"></footer>
    <script>
      // 테이블 중 id가 pages인 객체를 찾음
      $('table#pages').sortable({
        items: 'tr:not(.home)', // home클래스를 제외한 tr행을 sorting 가능하도록
      });
    </script>
  </body>
</html>


이제부터 index페이지에서 home페이지를 제외한 페이지들의 title을 드래그하여 순서를 변경할 수 있음.

sorting 설정하기

1) 드래그로 순서를 바꿀 수 있게하기
2) 바뀐 순서를 DB에 저장하기
3) DB에 저장된 sorting값에 맞춰 순서대로 출력하기

위의 순서대로 sorting을 설정해준다.

1) 드래그로 순서를 바꿀 수 있게하기
- index.html -
script 수정

<script>
  $('table#pages').sortable({
    items: 'tr:not(.home)',
    placeholder: 'ui-state-highlight',
    update: function () {
      //순서가 바뀔때 이벤트 발생
      let ids = $('table#pages').sortable('serialize'); //id를 문자열로 순서대로 시리얼라이즈

      console.log(ids);
      let url = '/admin/pages/reorder';
    },
  });
</script>

let ids = $('table#pages').sortable('serialize')
=> id들을 순서대로 문자열로 바꾸어 serialize해줌.


순서를변경했을때 콘솔에 출력되는 문구를 확인.

- style.css -
css에 추가

table.sorting tr:not(.home) {
  cursor: pointer;
  /* 옮길 수 있는 행만 마우스포인터 표시 */
}
.ui-state-highlight {
  border: 1px dashed #ccc;
  /* 위치옮길 때 변경될 위치를 점선으로 미리보기할 수 있는 기능 */
}

- index.html -
script에 추가

<script>
  $('table#pages').sortable({
    items: 'tr:not(.home)',
    placeholder: 'ui-state-highlight',
    update: function () {
      //순서가 바뀔때 이벤트 발생
      let ids = $('table#pages').sortable('serialize'); //id를 문자열로 순서대로 시리얼라이즈

      console.log(ids);
      let url = '/admin/pages/reorder';

      $.post(url, ids, function (data) {
        //AJAX post로 ids를 서버에 전송 후 결과를 data로 받음
        console.log(data); //콘솔확인
      });
    },
  });
</script>

2) 바뀐 순서를 DB에 저장하기
- AdminPageController -

@PostMapping("/reorder")
public @ResponseBody String reorder(@RequestParam("id[]") int[] id) {
    
    int count = 1;
    Page page;

    for (int pageId : id) {
        page = pageRepo.getById(pageId);	// DB에서 id로 page객체 검색
        page.setSorting(count);				// setSorting에 count값을 넣어줌
        pageRepo.save(page); 	//sorting 값을 순서대로 저장
        count++;
    }
    return "ok";	// view페이지가 아니라 ok문자열로 리턴
}

@RequestParam("id[]") int[] id: 앞에서 console.log(ids)로 출력해봤던 배열을 받아오기 위함
@ResponseBody: view가 아닌 일반 문자열("ok")로 리턴하기 위해 추가

=> 여기까지 한 후 테스트해보면 드래그로 순서를 바꿨을때 DB의 sorting값도 변경됨을 확인가능.
sorting값에 따라 page객체의 순서를 맞춰 가져오는 기능은 아직 추가하지 않았으므로, view를 새로고침해도 출력은 초기순서 그대로 됨.
고로 다음은 DB에 저장된 sorting순서에 맞춰 pages를 가져오도록 수정해야 함.

3) DB에 저장된 sorting값에 맞춰 순서대로 출력하기
- AdminPageController -
index메서드 수정

@GetMapping
public String index(Model model) {
	List<Page> pages = pageRepo.findAllByOrderBySortingAsc();
	model.addAttribute("pages", pages);
	return "admin/pages/index";
}

=> 단순히 모든 page객체를 찾아오는 findAll()대신 findAllByOrderBySortingAsc()로 변경 후 인터페이스에서 메서드를 생성해줌.

- PageRepository.interface -

List<Page> findAllByOrderBySortingAsc();



새로시작하여 테스트해보면 DB의 sorting값에 따라 순서대로 출력됨을 확인가능.


category 설정


참고용 폴더구조

CREATE TABLE IF NOT EXISTS categories (
	id int not null auto_increment,
    name VARCHAR(45) not null,
    slug VARCHAR(45) not null,
    sorting int not null,
	PRIMARY KEY (id)
);

categories 테이블 생성


category 클래스 생성

- Category -

@Entity
@Table(name="category")
@Data
public class category {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;
	
	@NotBlank(message = "이름을 입력해주세요")
	@Size(min = 2, message = "이름은 2자 이상")
	private String name;
	
	private String slug;
	private int sorting;
}


새 인터페이스 생성

- CategoryRepository.intergace -

public interface CategoryRepository extends JpaRepository<Category, Integer>{
}


새 컨트롤러 클래스 생성

- AdminCategoryController -

@Controller
@RequestMapping("/admin/categories")
public class AdminCategoryController {

	@Autowired
	private CategoryRepository categoryRepo;
	
	@GetMapping
	private String index(Model model) {
		List<Category> catetories = categoryRepo.findAll();
		model.addAttribute(catetories);
		return "admin/categories/index";
	}
}



복사해온 admin/categories/index.html의 page -> category, pages -> categories 로 변경

- index.html -

<main role="main" class="container">
  <div class="display-2">Categories</div>
  <a th:href="@{/admin/categories/add}" class="btn btn-primary my-3">추가하기</a>
  <div th:if="${message}" th:class="${'alert ' + alertClass}" th:text="${message}"></div>

  <div th:if="${!categories.empty}">
    <table class="table sorting" id="categories">
      <tr class="home">
        <th>제 목</th>
        <th>수 정</th>
        <th>삭 제</th>
      </tr>
      <tr th:each="category : ${categories}" th:id="'id_' + ${category.id}" th:class="${category.slug}">
        <td th:text="${category.name}"></td>
        <td><a th:href="@{'/admin/categories/edit/' + ${category.id}}">수정</a></td>
        <td><a th:if="${category.slug != 'home'}" class="deleteConfirm" th:href="@{'/admin/categories/delete/' + ${category.id}}">삭제</a></td>
      </tr>
    </table>
  </div>
  <div th:if="${categories.empty}">
    <div class="display-4">현재 페이지가 없습니다...</div>
  </div>
</main>

<footer th:replace="/fragments/footer :: footer"></footer>
<script>
  $('table#categories').sortable({
  items: 'tr:not(.home)',
  placeholder: 'ui-state-highlight',
  update: function () {
  //순서가 바뀔때 이벤트 발생
  let ids = $('table#categories').sortable('serialize'); //id를 문자열로 순서대로 시리얼라이즈

  console.log(ids);
  let url = '/admin/categories/reorder';

  // $.post(url, ids, function (data) {
  //   //AJAX post로 ids 전송 후 결과를 data로 받음
  //   console.log(data); //콘솔확인
  // });
  },
  });
</script>


http://localhost:8080/admin/categories 에 맨 처음 접근 시 나타나는 화면.(데이터 없음)

insert into categories(id, name, slug, sorting)
values (1, 'TEST', 'test', 1);

DB의 categories테이블에 테스트용 데이터를 넣어 확인.


데이터가 있을 때


카테고리 CRUD



admin/categories/add.html 경로로 복사 후 수정

- add.html -

<div class="container">
  <div class="display-2">카테고리 추가</div>
  <a th:href="@{/admin/categories}" class="btn btn-primary my-3">돌아가기</a>

  <form method="post" th:object="${category}" th:action="@{/admin/categories/add}">
    <div th:if="${#fields.hasErrors('*')}" class="alert alert-danger">에러 발생</div>
    <div th:if="${message}" th:class="${'alert ' + alertClass}" th:text="${message}"></div>

    <div class="form-group">
      <label for="">이 름</label>
      <input type="text" class="form-control" th:field="*{name}" placeholder="이름" />
      <span class="error" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span>
    </div>
    <button type="submit" class="btn btn-danger">추 가</button>
  </form>
</div>

1. 추가하기

- AdminCategoryController -

@GetMapping("/add")	// 	/admin/categories/add
public String add(@ModelAttribute Category category) {
	return "admin/categories/add";
}

추가하기 버튼을 누르면 add페이지로 이동

- AdminCategoryController -

@PostMapping("/add") 
public String add(@Valid Category category, BindingResult bindingResult, RedirectAttributes attr) {
	//유효성검사 결과 에러가 있으면 다시 돌아감
	if (bindingResult.hasErrors()) return "admin/categories/add";
	// 유효성 검사 통과 시
	attr.addFlashAttribute("message", "성공적으로 페이지 추가됨");
	attr.addFlashAttribute("alertClass", "alert-success");	// 부트스트랩 경고창(color: succeess)
	
	String slug = category.getName().toLowerCase().replace(" ", "-");
	
	Category nameExist = categoryRepo.findByName(category.getName());	// DB의 name을 검색하여 있으면 Category에 저장
	
	if (nameExist != null) {	// 동일한 이름의 category가 이미 있을 경우
		attr.addFlashAttribute("message", "입력한 category가 이미 존재합니다.");
		attr.addFlashAttribute("alertClass", "alert-danger");
		attr.addFlashAttribute("category", category);	// 입력된 데이터가 저장된 페이지객체를 그대로 유지함
		
	} else {
		category.setSlug(slug);	// 소문자, '-' 수정 된 slug를 업데이트
		category.setSorting(100);	// 기본 sorting값
		
		categoryRepo.save(category);
	}
	return "redirect:/admin/categories/add";	// post-redirect-get
}

- CategoryRepository.interface -

Category findByName(String name);



유효성 검사와 name중복검사에 걸리지 않으면 DB에 정상적으로 입력됨.


name중복검사에 통과하지 못하면 입력값을 그대로 가지고 다시 add페이지로 돌아와 에러message가 출력됨.

2. 수정하기

- AdminCategoryController -

@GetMapping("/edit/{id}")
public String edit(@PathVariable int id, Model model) {
	Category category = categoryRepo.getById(id);	// 테이블에서 id로 category검색
	model.addAttribute("category", category);
	return "admin/categories/edit";	// 수정 페이지로 보냄
}

admion/category/edit.html의 pages -> categories, page -> category, title -> name 으로 모두변경
- edit.html -

<div class="container">
  <div class="display-2">카테고리 수정</div>
  <a th:href="@{/admin/categories}" class="btn btn-primary my-3">돌아가기</a>

  <form method="post" th:object="${category}" th:action="@{/admin/categories/edit}">
    <div th:if="${#fields.hasErrors('*')}" class="alert alert-danger">에러 발생</div>
    <div th:if="${message}" th:class="${'alert ' + alertClass}" th:text="${message}"></div>
    <input type="hidden" th:field="*{id}" />
    <input type="hidden" th:field="*{sorting}" />

    <div class="form-group">
      <label for="">제 목</label>
      <input type="text" class="form-control" th:field="*{name}" placeholder="제목" />
      <span class="error" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span>
    </div>
    <button type="submit" class="btn btn-danger">수 정</button>
  </form>
</div>


DB에서 id로 찾은 데이터를 객체에 담아 출력까지 확인완료.
다음으로 수정입력한 데이터를 업데이트하는 기능을 추가.

- AdminCategoryController -

@PostMapping("/edit")
public String edit(@Valid Category category, BindingResult bindingResult, RedirectAttributes attr) {
	//유효성검사 결과 에러가 있으면 다시 돌아감
	if (bindingResult.hasErrors()) return "admin/categories/edit";
	
	attr.addFlashAttribute("message", "성공적으로 category 수정됨");
	attr.addFlashAttribute("alertClass", "alert-success");	// 부트스트랩 경고창(color: succeess)
	
	String slug = category.getName().toLowerCase().replace(" ", "-");
	Category nameExist = categoryRepo.findByName(category.getName());

	if (nameExist != null) {	// 동일한 slug가 존재하면 저장x
		attr.addFlashAttribute("message", "입력한 category가 이미 존재합니다.");
		attr.addFlashAttribute("alertClass", "alert-danger");
		attr.addFlashAttribute("category", category);
		
	} else {
		category.setSlug(slug);	// 소문자, '-' 수정 된 slug를 업데이트
		category.setSorting(100);
		
		categoryRepo.save(category);
	}
	return "redirect:/admin/categories/edit/" + category.getId();	// /edit/{id} 로 주소가 작성되어있으므로 id값도 같이 넘겨야함
}




DB까지 수정됨을 확인.

삭제하기

- AdminCategoryController -

@GetMapping("/delete/{id}")
public String delete(@PathVariable int id, RedirectAttributes attr) {
	categoryRepo.deleteById(id);

	attr.addFlashAttribute("message", "성공적으로 삭제되었습니다.");
	attr.addFlashAttribute("alertClass", "alert-success");
	
	return "redirect:/admin/categories";
}



좋은 웹페이지 즐겨찾기