스프링수업 8일차

하파타카·2022년 4월 5일
0

SpringBoot수업

목록 보기
8/23

한 일

pma 프로젝트에 rest api적용하기 -2-

  • put / pacth로 직원정보 수정하기
  • Delete 직원 삭제하기
  • 페이지 사이즈로 데이터 가져오기 (페이징)

쇼핑몰 프로젝트

  • home 페이지 생성
  • DB, 테이블 생성 (lombok설치, DB매핑)
  • admin(관리자) 페이지

put / pacth로 직원정보 수정하기

REST api에서 POST와 PUT의 차이

put 전체정보 업데이트

- EmployeeApiController -

// 전체 업데이트 http put 메서드
@PutMapping(consumes = "application/json")
@ResponseStatus(HttpStatus.OK)	// http 상태 메시지
public Employee update(@RequestBody @Valid Employee employee) {
	return empRepo.save(employee);	// save는 id가 있으면 업데이트 없으면 새로 생성
}

업데이트하기위한 employee객체를 입력받음(이름, 성, 이메일).
consumes 는 서버에서 받을 데이터타입을 지정.
여기서는 json타입으로 받음.


포스트맨으로 확인.
직원 중 한명의 정보를 모두 수정 한 후 send시 수정된 정보를 리턴받아 화면에 출력함.


put메서드를 사용했을때는 업데이트할 모든 속성의 데이터가 필요.
=> 하나라도 데이터가 없으면 업데이트가 안됨.

pacth 부분정보 업데이트

- EmployeeApiController -

// 부분 업데이트시 업데이트할 직원의 id가 필요
@PutMapping(path = "/{id}", consumes = "application/json")
public Employee update(@PathVariable long id, @RequestBody Employee employee) {
	Employee emp = empRepo.findByEmployeeId(id);	// 업데이트 전 원래 데이터를 불러옴
	
	if(employee.getEmail() != null) {
		emp.setEmail(employee.getEmail());
	}
	if(employee.getFirstName() != null) {
		emp.setFirstName(employee.getFirstName());
	}
	if(employee.getLastName() != null) {
		emp.setLastName(employee.getLastName());
	}
	return empRepo.save(emp);	// 업데이트된 emp를 저장
}

각 속성의 데이터가 null인지 체크하여 null값이 아닐 경우에만 해당속성을 업데이트.

http://localhost:8080/app-api/employees/9
id를 9로 넣어 9번 직원 '김유신'의 정보를 put으로 부분업데이트.

수정한 정보만 업데이트됨.


Delete 직원 삭제하기

- EmployeeApiController -

@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable long id) {
	// 예외가 발생하여 데이터가 삭제되지 않았을 경우를 알기위해 try/catch문 사용
	try {
		empRepo.deleteById(id);
	} catch (EmptyResultDataAccessException e) {
		System.out.println("삭제 안됨");	
	}
}


http://localhost:8080/app-api/employees/11
11번 직원인 최창수를 삭제. 삭제 성공 시 데이터를 불러오지 않음.


페이지 사이즈로 데이터 가져오기 (페이징)

page: 가져올 페이지의 수
size: 한 페이지에 들어갈 데이터 수
일때 요청주소(parameter)는 employees?page=1&size=5 의 형식을 띄며, 1개의 페이지에 표시될 5개의 데이터만 리턴하게됨.

페이지 계산을 해주는 @PagingAndSortingRepository 을 사용.
=> 전체 정보를 가져오는것이 아닌 원하는 수(page, size로 지정)의 데이터만 객체에 담아 리턴.

Pageable: 페이징한 데이터를 저장해 리턴하는 객체
Sort: 정렬한 데이터를 저장해 리턴하는 객체

- EmployeeApiController -

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
	...생략...
//	@GetMapping		// 직원을 모두 가져오는 findAll은 주석처리
//	public Iterable<Employee> getEmployees(){
//		return empRepo.findAll();
//	}

// 페이징을 적용한 직원 리스트
@GetMapping
@ResponseStatus(HttpStatus.OK)
public Iterable<Employee> findPaginatedEmployees(@RequestParam(value = "page", defaultValue = "1") int page,
												@RequestParam(value = "size", defaultValue = "5") int size) {
	// 페이지설정 리파지토리를 변경(상속받는 클래스를 변경)
	Pageable pageable = PageRequest.of(page, size);
	return empRepo.findAll(pageable);
}

매개변수로 page와 size를 넘겨받음.

@GetMapping(params = {"page", "size"})의 params는 필요없음.
=> page와 size속성을 무조건 paramter로 입력받고싶을 경우는 남겨둬야하나, 여기서는 기본데이터를 지정해주었으므로(page=0, size=5) params를 선언하지 않아야 http://localhost:8080/app-api/employees 로 요청을 보냈을 때 기본데이터대로 처리됨.

- EmployeeRepository -

public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
	...생략...
}

상속받는 클래스를 PagingAndSortingRepository로 변경.
PagingAndSortingRepository는 CrudRepository를 포함하므로 CrudRepository는 삭제해도 무관.

http://localhost:8080/app-api/employees?page=0&size=3

페이지는 0부터 시작이므로 첫 페이지는 0페이지가 됨.


쇼핑몰 프로젝트



새 프로젝트 생성 후 http://localhost:8080/ 로 접속해 Whitelabel Error Page가 나오면 완료.

home 페이지 생성

부트스트랩 4.6버전 사용.
부트스트랩 최신버전인 5버전은 제이쿼리를 사용하지 않아(순수자바스크립트사용) 여기서는 4.6버전을 사용.

bootstrap v4.6 템플릿 페이지소스 복사하여 home.html 생성
html태그로 보기: ctrl + u


새 클래스 생성

- WebConfig -

@Configuration
public class WebConfig implements WebMvcConfigurer {
	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		// controller 대신 view를 매핑함
		registry.addViewController("/").setViewName("home");
	}
}

앞선 프로젝트들과 달리 이번엔 controller대신 @Configuration을 사용하여 뷰를 매핑함.
기본주소(/)일때 home페이지를 찾아 매핑함.(확장자 관계없이 이름으로 찾음)


적용시 화면.

header, body, footer로 분리

thymeleaf태그를 사용하여 분리

자주 사용하는 Thymeleaf 문법


폴더구조

- home.html -

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

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

    <main role="main" class="container"></main>

    <footer th:replace="/fragments/footer :: footer"></footer>
  </body>
</html>

- head.html -

<head th:fragment="head-front">
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />

  <title>쇼핑몰 홈</title>

  <!-- Bootstrap core CSS -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" />
  <!-- 커스텀 CSS -->
  <link rel="stylesheet" th:href="@{/css/style.css}" />
</head>

<head th:fragment="head-admin">
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />

  <title>쇼핑몰 관리자홈</title>

  <!-- Bootstrap core CSS -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" />
  <!-- 커스텀 CSS -->
  <link rel="stylesheet" th:href="@{/css/style.css}" />
</head>

head태그를 두개 생성하여 하나는 head(고객용), 하나는 head-admin(관리자용)으로 명명

- nav.html -

<nav th:fragment="nav-front" class="navbar navbar-expand-md navbar-dark bg-dark">
  <a class="navbar-brand" href="#">쇼핑몰🎁</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarShop" aria-controls="navbarShop" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarShop">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" href="#">Home</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#">Link</a>
      </li>
    </ul>
  </div>
</nav>

<nav th:fragment="nav-admin" class="navbar navbar-expand-md navbar-dark bg-dark">
  <a class="navbar-brand" th:href="@{/}">관리자⛑</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarShop" aria-controls="navbarShop" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarShop">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" th:href="@{/admin/pages}">Pages</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#">Link</a>
      </li>
    </ul>
  </div>
</nav>

마찬가지로 nav역시 nav-front와 nav-admin(관리자)으로 명명.

- footer.html -

<footer>
  <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js"></script>
</footer>


기본페이지 http://localhost:8080/ 접근 시 아래 사진처럼 나와야함.(background는 style.css에서 지정해둠)


DB, 테이블 생성

shoppingmall스키마 생성

테이블 생성


pages 테이블 생성

CREATE TABLE `shoppingmall`.`pages` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(45) NOT NULL,
  `slug` VARCHAR(45) NOT NULL,
  `content` MEDIUMTEXT NOT NULL,
  `sorting` INT NOT NULL,
  PRIMARY KEY (`id`));

JPA, Lombok 추가

Lombok사용을 위한 설치과정
설치 전 STS종료 => 설치 완료 후 STS다시 켜서 확인

C:\Users\admin.m2\repository\org\projectlombok\lombok\1.18.22 경로에서

위의 파일 더블클릭하여 실행
=> 안될 시 https://projectlombok.org/download 에서 다운로드, SpringWorkspace폴더에서 git bash로 실행(압축해제x 실행o)하여 java -jar lombok.jar 로 jar파일을 실행하면 설치화면이 나옴


설치는 STS에 해줌. 인스톨 클릭하면 완료됨.

lombok 설치과정

DB 테이블과 매칭


새 패키지, 클래스 생성

- Page -

// 실제 테이블과 매핑
@Entity
@Table(name = "pages")
@Data	// lombok에 의해 get, set, 생성자, toString 자동생성됨
public class Page {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;
	private String  title;
	private String slug;
	private String content;
	private int sorting;
}

=> lombok설치 후 @Data 사용시 자동으로 get, set, 생성자, toString가 생성됨.

application DB설정

- application.properties -

# DB setting
spring.datasource.url=jdbc:mysql://localhost:3306/shoppingmall?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=1234

username과 password, url은 현재 사용환경에 맞게 입력.


pom.xml에 MySQL Driver 추가.


admin(관리자) 페이지

관리자페이지 만들기

- 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"></main>

    <footer th:replace="/fragments/footer :: footer"></footer>
  </body>
</html>


새 클래스 생성

- AdminPageController -

@Controller
@RequestMapping("/admin/pages")
public class AdminPageController {
	@GetMapping
	public String index() {
		return "admin/pages/index";
	}
}


기본주소 localhost:8080 으로 접근 시 쇼핑몰페이지(home.html) 출력.


http://localhost:8080/admin/pages 으로 접근 시 관리자페이지(index.html) 출력.
관리자⛑ 클릭 시 쇼핑몰페이지(home.html)로 이동함.

관리자 페이지 DB연동 체크


새 인터페이스 생성
Repository는 인터페이스임

- PageRepository -

public interface PageRepository extends JpaRepository<Page, Integer> {
	// JpaRepository상속 시 List<PAGE>로 리턴되는 findAll 등 여러 메서드가 추가됨.
}

JpaRepository를 상속받을 경우
@Override default List<Page> findAll();가 이미 생성되어있으므로 @Override로 재정의하지 않아도 사용할 수 있음.

- AdminPageController -

@Controller
@RequestMapping("/admin/pages")
public class AdminPageController {
	
	@Autowired
	private PageRepository pageRepo;
	
	@GetMapping
	public String index(Model model) {
		List<Page> pages = pageRepo.findAll();
		model.addAttribute("pages", pages);
		return "admin/pages/index";
	}
}

view(admin/pages/index)로 Page List를 넘김.

- index.html -

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

controller에서 model에 담아 넘긴 페이지정보를 th:if에서 pages.empty로 검사하여 페이지가 있는지 구별함.

insert into pages(id, title, slug, content, sorting)
values (1, 'Home', 'home', '홈페이지', 0);
insert into pages(id, title, slug, content, sorting)
values (2, 'Service', 'service', '서비스페이지', 1);

DB에 테스트용데이터 입력

http://localhost:8080/admin/pages 로 관리자페이지에 접근하여 확인.

DB의 pages테이블의 데이터가 출력됨.


DB의 pages테이블에 에 저장된 데이터가 없을 때 출력되는 화면.

페이지 추가

- AdminPageController -

@GetMapping("/add")
public String add(Model model) {
	model.addAttribute("page", new Page());
	return "admin/pages/add";
}

혹은

@GetMapping("/add")
public String add(@ModelAttribute Page page) {
	return "admin/pages/add";
}

로 작성해도 같음.

- add.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/add}">
    <div class="form-group">
      <label for="">제 목</label>
      <input type="text" class="form-control" th:field="*{title}" placeholder="제목" />
    </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>
    </div>
    <button type="submit" class="btn btn-danger">추 가</button>
  </form>
</div>


유효성 검사


pom.xml에 추가

- Page -

	...생략...
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;

@NotBlank(message = "제목을 입력해주세요")
@Size(min = 2, message = "제목은 2자 이상")
private String  title;
private String slug;	// title을 소문자로 띄워쓰기 특스문자등을 - 로 바꿈

@Size(min = 5, message = "내용은 5자 이상")
private String content;
private int sorting;	// 정렬
    ...생략...

- AdminPageController -
@GetMapping("/add") 아래에 새로 추가.

@PostMapping("/add")
public String add(@Valid Page page, BindingResult bindingResult) {
	//유효성검사 결과 에러가 있으면 다시 돌아감
	if (bindingResult.hasErrors()) return "admin/pages/add";
	
	return "redirect:admin/pages/add";
}

- add.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/add}">
    <div th:if="${#fields.hasErrors('*')}" class="alert alert-danger">에러 발생</div>
    <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>

각 input태그 아래에 span태그를 만들어 에러메시지가 출력되도록 작성.

- style.css -

span.error {
  color: coral !important;
  /* 에러 메시지를 표시할 색 */
}


유효성검사에 통과하지 못하면 에러메시지가 출력되며 다시 admin/add 페이지로 돌아감.
post방식으로 넘어가므로 주소창에는 변화가 없음.

유효성 검사 경고창 출력

- AdminPageController -

// model은 요청이 그대로 들어갈때만 사용할 수 있으므로 여기서는 RedirectAttributes 사용
@PostMapping("/add") 
public String add(@Valid Page page, BindingResult bindingResult, RedirectAttributes attr) {
	//유효성검사 결과 에러가 있으면 다시 돌아감
	if (bindingResult.hasErrors()) return "admin/pages/add";
	// 유효성 검사 통과 시
	attr.addFlashAttribute("message", "성공적으로 페이지 추가됨");
	attr.addFlashAttribute("alertClass", "alert-success");	// 부트스트랩 경고창(color: succeess)
	
	// 슬러그 검사 (슬러그 미입력시 title의 소문자로 하고, 공백은 - 로 대체), 입력시 소문자공백은 -로 대체
	String slug = page.getSlug() == "" ? page.getTitle().toLowerCase().replace(" ", "-") : page.getSlug();
	Page slugExist = pageRepo.findBySlug(slug);	// 슬러그는 유일한 값이어야하므로 유일한지 검사. 슬러그로 DB검색 후 있으면 Page로 리턴
	
	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);	// 기본 sorting값
		
		pageRepo.save(page);	// 위의 과정은 page를 저장하기 전 유효성을 검사한 것이므로 저장되는건 page객체이다
	}
	return "redirect:/admin/pages/add";	// post-redirect-get(새로고침시 다시 반복된요청이 가는걸 방지)
}
String slug = page.getSlug() == "" ? page.getTitle().toLowerCase().replace(" ", "-") : page.getSlug();
=> 참일때 String slug = page.getTitle().toLowerCase().replace(" ", "-");
거짓일때 String slug = page.getSlug();

- PageRepository -

Page findBySlug(String slug);	// 실제 구현은 jpa 하이버네이트가 함

=> jpa를 사용하지 않으면 직접 쿼리를 작성하여야 함.

- add.html -
유효성검사에 대한 경고를 보여주도록 뷰를 수정.

<form method="post" th:object="${page}" th:action="@{/admin/pages/add}">
  <div th:if="${#fields.hasErrors('*')}" class="alert alert-danger">에러 발생</div>
  <div th:if="${message}" th:class="${'alert' + alertClass}" th:text="${message}"></div>
    ...생략...
  <button type="submit" class="btn btn-danger">추 가</button>
</form>

th:text="{message}": 넘어온 message가 있으면 출력
th:class="${'alert' + alertClass}": 유효성검사에서 넘겨준 alertClass는 경고창의 color값이므로 alertdanger, alertsuccess 등의 값이 됨


중복된 슬러그 입력 시 에러 (데이터가 입력된 page객체를 다시 넘겨주어 입력한 값을 유지)

profile
천 리 길도 가나다라부터

0개의 댓글

관련 채용 정보