From f72b1f9cdade145b970137d72517bc2d5fea891f Mon Sep 17 00:00:00 2001 From: banjjoknim Date: Sun, 22 Nov 2020 23:05:23 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8A=A4=ED=94=84=EB=A7=81=20=EB=B6=80?= =?UTF-8?q?=ED=8A=B8=EC=99=80=20AWS=EB=A1=9C=20=ED=98=BC=EC=9E=90=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=98=EB=8A=94=20=EC=9B=B9=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20Chapter4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WebServiceBySpringBootAndAWS.md | 1 + WebServiceBySpringBootAndAWS/build.gradle | 1 + .../src/ChapterDescription/Chapter4.md | 713 ++++++++++++++++++ .../domain/posts/PostsRepository.java | 6 + .../book/springboot/service/PostsService.java | 19 + .../book/springboot/web/IndexController.java | 36 + .../springboot/web/PostsApiController.java | 6 + .../web/dto/PostsListResponseDto.java | 22 + .../src/main/resources/static/js/app/index.js | 74 ++ .../main/resources/templates/index.mustache | 34 + .../templates/layout/footer.mustache | 7 + .../templates/layout/header.mustache | 9 + .../resources/templates/posts-save.mustache | 26 + .../resources/templates/posts-update.mustache | 31 + .../springboot/web/IndexControllerTest.java | 28 + 15 files changed, 1013 insertions(+) create mode 100644 WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter4.md create mode 100644 WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/IndexController.java create mode 100644 WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/dto/PostsListResponseDto.java create mode 100644 WebServiceBySpringBootAndAWS/src/main/resources/static/js/app/index.js create mode 100644 WebServiceBySpringBootAndAWS/src/main/resources/templates/index.mustache create mode 100644 WebServiceBySpringBootAndAWS/src/main/resources/templates/layout/footer.mustache create mode 100644 WebServiceBySpringBootAndAWS/src/main/resources/templates/layout/header.mustache create mode 100644 WebServiceBySpringBootAndAWS/src/main/resources/templates/posts-save.mustache create mode 100644 WebServiceBySpringBootAndAWS/src/main/resources/templates/posts-update.mustache create mode 100644 WebServiceBySpringBootAndAWS/src/test/java/com/banjjoknim/book/springboot/web/IndexControllerTest.java diff --git a/WebServiceBySpringBootAndAWS/WebServiceBySpringBootAndAWS.md b/WebServiceBySpringBootAndAWS/WebServiceBySpringBootAndAWS.md index 01dc067..b9c2db7 100644 --- a/WebServiceBySpringBootAndAWS/WebServiceBySpringBootAndAWS.md +++ b/WebServiceBySpringBootAndAWS/WebServiceBySpringBootAndAWS.md @@ -3,3 +3,4 @@ - [Chapter1. 인텔리제이로 스프링 부트 시작하기](https://github.com/banjjoknim/TIL/blob/master/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter1.md) - [Chapter2. 스프링 부트에서 테스트 코드를 작성하자](https://github.com/banjjoknim/TIL/blob/master/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter2.md) - [Chapter3. 스프링 부트에서 JPA로 데이터베이스 다뤄보자](https://github.com/banjjoknim/TIL/blob/master/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter3.md) +- [Chapter4. 머스테치로 화면 구성하기](https://github.com/banjjoknim/TIL/blob/master/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter4.md) \ No newline at end of file diff --git a/WebServiceBySpringBootAndAWS/build.gradle b/WebServiceBySpringBootAndAWS/build.gradle index a541128..3100e32 100644 --- a/WebServiceBySpringBootAndAWS/build.gradle +++ b/WebServiceBySpringBootAndAWS/build.gradle @@ -30,4 +30,5 @@ dependencies { compile('org.springframework.boot:spring-boot-starter-data-jpa') compile('com.h2database:h2') testCompile('org.springframework.boot:spring-boot-starter-test') + compile('org.springframework.boot:spring-boot-starter-mustache') } diff --git a/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter4.md b/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter4.md new file mode 100644 index 0000000..c0291b8 --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter4.md @@ -0,0 +1,713 @@ +# Chapter4. 머스테치로 화면 구성하기 + +--- + +## 4.1 서버 템플릿 엔진과 머스테치 소개 + +일반적으로 웹 개발에 있어 템플릿 엔진이란, **지정된 템플릿 양식과 데이터**가 합쳐져 HTML 문서를 출력하는 소프트웨어를 이야기합니다. + +- 서버 템플릿 엔진 : `JSP`, `Freemarker` ... +- 클라이언트 템플릿 엔진 : `리액트(React)`, `뷰(Vue)`의 `View` 파일 ... + +```java + + + + +``` + +**페이지 로딩속도를 높이기 위해** `css`는 `header`에, `js`는 `footer`에 두었습니다. HTML은 위에서부터 코드가 실행되기 때문에 **head가 다 실행되고서야 body가 실행**됩니다. 즉, `head`가 다 불러지지 않으면 사용자 쪽에선 백지 화면만 노출됩니다. 특히 `js`의 용량이 크면 클수록 `body` 부분의 실행이 늦어지기 때문에 `js`는 `body` 하단에 두어 화면이 다 그려진 뒤에 호출하는 것이 좋습니다. + +반면 `css`는 화면을 그리는 역할이므로 `head`에서 불러오는 것이 좋습니다. 그렇지 않으면 `css`가 적용되지 않은 깨진 화면을 사용자가 볼 수 있기 때문입니다. 추가로, `bootstrap.js`의 경우 **제이쿼리가 꼭 있어야**만 하기 때문에 부트스트랩보다 먼저 호출되도록 코드를 작성해야 합니다. 보통 이런 경우를 `bootstrap.js`가 **제이쿼리에 의존**한다고 합니다. + +라이브러리를 비롯해 기타 HTML 태그들이 모두 레이아웃에 추가되었으므로 `index.mustache`의 코드는 다음과 같이 변경됩니다. + +```java +{{>layout/header}} // 1. + +

스프링 부트로 시작하는 웹 서비스

+ +{{>layout/footer}} +``` + +**1. {{>layout>header}}** +- `{{>}}`는 현재 머스테치 파일(`index.mustache`)을 기준으로 다른 파일을 가져옵니다. + +레이아웃으로 파일을 분리했으니 `index.mustache`에 글 등록 버튼을 추가합니다. + +```java +{{>layout/header}} + +

스프링 부트로 시작하는 웹 서비스

+
+
+ +
+
+ +{{>layout/footer}} +``` +여기서는 `` 태그를 이용해 글 등록 페이지로 이동하는 글 등록 버튼이 생성되었습니다. 이동할 페이지의 주소는 `/posts/save`입니다. + +이 주소에 해당하는 컨트롤러를 생성합니다. + +```java +@RequiredArgsConstructor +@Controller +public class IndexController { + ... + + @GetMapping("/posts/save") + public String postsSave() { + return "posts-sava"; + } +} +``` +`index.mustache`와 마찬가지로 `/posts/save`를 호출하면 `posts-save.mustache`를 호출하는 메소드가 추가되었습니다. `posts-save.mustache` 파일을 생성합니다. 파일의 위치는 `index.mustache`와 같습니다. + +**posts-save.mustache** +```java +{{>layout/header}} + +

게시글 등록

+ +
+ +
+ +{{>layout/footer}} +``` +아직 게시글 등록 화면에 **등록 버튼은 기능이 없습니다.** API를 호출하는 `JS`가 전혀 없기 때문입니다. 그래서 `src/main/resources`에 `static/js/app` 디렉토리를 생성합니다. + +**index.js** +```java +var main = { + init : function () { + var _this = this; + $('#btn-save').on('click', function () { + _this.save(); + }); + }, + save : function () { + var data = { + title: $('#title').val(), + author: $('#author').val(), + content: $('#content').val() + }; + + $.ajax({ + type: 'POST', + url: '/api/v1/posts', + dataType: 'json', + contentType: 'application/json; charset=utf-8', + data: JSON.stringify(data) + }).done(function () { + alert('글이 등록되었습니다.'); + window.location.href = '/'; // 1. + }).fail(function (error) { + alert(JSON.stringify(error)); + }); + } +}; + +main.init(); +``` +**1. window.location.href = '/'** +- 글 등록이 성공하면 메인페이지(`/`)로 이동합니다. + +`index.js`의 첫 문장에 `var main = { ... }`라는 코드를 선언했습니다. 굳이 `main`라는 변수의 속성으로 `function`을 추가한 이유는, 예를 들면 `index.js`가 다음과 같이 `function`을 작성한 상황이라고 가정하겠습니다. + +```java +var init = function() { + ... +}; + +var save = function() { + ... +}; + +init(); +``` +`index.mustache`에서 `a.js`가 추가되어 `a.js`도 **a.js만의 init과 save function이 있다**면? +브라우저의 스코프는 **공용 공간**으로 쓰이기 때문에 나중에 로딩된 `js`의 `init`, `save`가 먼저 로딩된 `js`의 `function`을 **덮어쓰게 됩니다.** +여러 사람이 참여하는 프로젝트에서는 **중복된 함수 이름**은 자주 발생할 수 있습니다. 모든 `function` 이름을 확인하면서 만들 수는 없습니다. 그래서 이런 문제를 피하려고 `index.js`만의 유효범위를 만들어 사용합니다. + +방법은 `var index`이란 객체를 만들어 해당 객체에서 필요한 모든 `function`을 선언하는 것입니다. 이렇게 하면 **index 객체 안에서만 function이 유효**하기 때문에 다른 `JS`와 겹칠 위험이 사라집니다. + +생성된 `index.js`를 머스테치 파일이 쓸 수 있게 `footer.mustache`에 추가합니다. + +```java + + + + + + + +``` +`index.js` 호출 코드를 보면 **절대 경로**(`/`)로 바로 시작합니다. 스프링 부트는 기본적으로 `src/main/resources/static`에 위치한 자바스크립트, CSS, 이미지 등 정적 파일들은 URL에서 `/`로 설정됩니다. + +그래서 다음과 같이 파일이 위치하면 위치에 맞게 호출이 가능합니다. + +- `src/main/resources/static/js/...(http://도메인/js/...)` +- `src/main/resources/static/css/...(http://도메인/css/...)` +- `src/main/resources/static/image/...(http://도메인/image/...)` + +등록 기능이 완성되었으므로 직접 브라우저에서 테스트하고 `h2` DB에 데이터가 등록되었는지도 확인해봅니다. + +--- + +## 4.4 전체 조회 화면 만들기 +전체 조회를 위해 `index.mustache`의 UI를 변경합니다. + +**index.mustache** +```java +{{>layout/header}} + +

스프링 부트로 시작하는 웹 서비스 Ver.2

+
+
+ +
+
+ + + + + + + + + + + + {{#posts}} // 1. + + // 2. + + + + + {{/posts}} + +
게시글번호제목작성자최종수정일
{{id}}{{title}}{{author}}{{modifiedDate}}
+
+ +{{>layout/footer}} +``` +**1. {{#posts}}** +- `posts`라는 `List`를 순회합니다. +- `Java`의 `for`문과 동일하게 생각하면 됩니다. + +**2. {{id}} 등의 {{변수명}}** +- `List`에서 뽑아낸 객체의 필드를 사용합니다. + +다음으로 `Controller`, `Service`, `Repository` 코드를 작성합니다. +기존에 있던 `PostsRepository` 인터페이스에 쿼리가 추가됩니다. + +```java +package com.banjjoknim.book.springboot.domain.posts; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface PostsRepository extends JpaRepository { + + @Query("SELECT p FROM Posts p ORDER BY p.id DESC ") + List findAllDesc(); +} +``` +`SpringDataJpa`에서 제공하지 않는 메소드는 위처럼 `@Query`를 사용하여 쿼리로 작성해도 됩니다. + +`Repository` 다음으로 `PostsService`에 코드를 추가합니다. + +```java +@RequiredArgsConstructor +@Service +public class PostsService { + private final PostsRepository postsRepository; + + ... + + @Transactional(readOnly = true) + public List findAllDesc() { + return postsRepository.findAllDesc().stream() + .map(PostsListResponseDto::new) + .collect(toList()); + } +} +``` +`findAllDesc` 메소드의 트랙잭션 어노테이션(`@Transaction`)에 옵션이 하나 추가되었습니다. `(readOnly = true)`를 주면 **트랜잭션 범위는 유지**하되, 조회 기능만 남겨두어 **조회 속도가 개선**되기 때문에 등록, 수정, 삭제 기능이 전혀 없는 서비스 메소드에서 사용하는 것을 추천합니다. + +아직 `PostsListResponseDto` 클래스가 없기 때문에 이 클래스 역시 생성합니다. + +```java +package com.banjjoknim.book.springboot.web.dto; + +import com.banjjoknim.book.springboot.domain.posts.Posts; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class PostsListResponseDto { + private Long id; + private String title; + private String author; + private LocalDateTime modifiedDate; + + public PostsListResponseDto(Posts entity) { + this.id = entity.getId(); + this.title = entity.getTitle(); + this.author = entity.getAuthor(); + this.modifiedDate = entity.getModifiedDate(); + } +} +``` +마지막으로 `Controller`를 변경합니다. + +```java +package com.banjjoknim.book.springboot.web; + + +import org.springframework.ui.Model; + + +@RequiredArgsConstructor +@Controller +public class IndexController { + + private final PostsService postsService; + + @GetMapping("/") + public String index(Model model) { // 1. + model.addAttribute("posts", postsService.findAllDesc()); + return "index"; + } + + @GetMapping("/posts/save") + public String postsSave() { + return "posts-save"; + } + +} +``` +**1. Model** +- 서버 템플릿 엔진에서 사용할 수 있는 객체를 저장할 수 있습니다. +- 여기서는 `postsService.findAllDesc()`로 가져온 결과를 `posts`로 `index.mustache`에 전달합니다. + +`Controller`까지 모두 완성되었으므로, `http://localhost:8080/`로 접속한 뒤 등록 화면을 이용해 정상적으로 기능이 동작하는지 확인합니다. + +--- + +## 4.5 게시글 수정, 삭제 화면 만들기 + +게시글 수정 API는 이미 만들어둔 `PostsApiController`의 `update` 메소드를 이용합니다. + +#### 게시글 수정 + +게시글 수정 화면 머스테치 파일을 생성합니다. + +```java +{{>layout/header}} + +

게시글 수정

+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ 취소 + + +
+
+ +{{>layout/footer}} +``` +**1. {{post.id}}** +- 머스테치는 객체의 필드 접근 시 점(`Dot`)으로 구분합니다. +- 즉, `Post` 클래스의 `id`에 대한 접근은 `post.id`로 사용할 수 있습니다. + +**2. readonly** +- `input` 태그에 읽기 기능만 허용하는 속성입니다. +- `id`와 `author`는 수정할 수 없도록 읽기만 허용하도록 추가합니다. + +그리고 `btn-update` 버튼을 클릭하면 `update` 기능을 호출할 수 있게 `index.js` 파일에도 `update function`을 하나 추가합니다. + +```java +var main = { + init: function () { + var _this = this; + ... + + $('#btn-update').on('click', function () { // 1. + _this.update(); + }) + }, + save: function () { + ... + }, + update: function () { // 2. + var data = { + title: $('#title').val(), + content: $('#content').val() + }; + + var id = $('#id').val(); + + $.ajax({ + type: 'PUT', // 3. + url: '/api/v1/posts/' + id, // 4. + dataType: 'json', + contentType: 'application/json; charset=utf-8', + data: JSON.stringify(data) + }).done(function () { + alert('글이 수정되었습니다.'); + window.location.href = '/'; + }).fail(function (error) { + alert(JSON.stringify(error)); + }); + } +}; + +main.init(); +``` +**1. $('#btn-update').on('click')** +- `btn-update`란 `id`를 가진 HTML 엘리먼트에 `click` 이벤트가 발생할 때 `update function`을 실행하도록 이벤트를 등록합니다. + +**2. update: function ()** +- 신규로 추가될 `update function`입니다. + +**3. type: 'PUT'** +- 여러 `HTTP Method` 중 PUT 메소드를 선택합니다. +- `PostApiController`에 있는 API에서 이미 `@PutMapping`으로 선언했기 때문에 `PUT`을 사용해야 합니다. 참고로 이는 `REST` 규약에 맞게 설정된 것입니다. +- `REST`에서 `CRUD`는 다음과 같이 `HTTP Method`에 매핑됩니다. + - 생성 (Create) - POST + - 읽기 (Read) - GET + - 수정 (Update) - PUT + - 삭제 (Delete) - DELETE + +**4. url: '/api/v1/posts/' + id** +- 어느 게시글을 수정할지 `URL Path`로 구분하기 위해 `Path`에 `id`를 추가합니다. + +마지막으로 전체 목록에서 **수정 페이지로 이동할 수 있게** 페이지 이동 기능을 추가해 보겠습니다. `index.mustache` 코드를 '살짝' 수정합니다. + +**index.mustache** +```java + + {{#posts}} + + {{id}} + {{title}} // 1. + {{author}} + {{modifiedDate}} + + {{/posts}} + +``` +**1. < a href="/posts/update/{{id}}">** +- 타이틀(`title`)에 `a tag`를 추가합니다. +- 타이틀을 클릭하면 해당 게시글의 수정 화면으로 이동합니다. + +`IndexController`에 다음과 같이 메소드를 추가합니다. + +```java +@RequiredArgsConstructor +@Controller +public class IndexController { + + private final PostsService postsService; + + ... + + @GetMapping("/posts/update/{id}") + public String postsUpdate(@PathVariable Long id, Model model) { + PostsResponseDto dto = postsService.findById(id); + model.addAttribute("post", dto); + + return "posts-update"; + } +} +``` +브라우저에서 수정 기능이 제대로 동작하는지 확인합니다. + +#### 게시글 삭제 +삭제 버튼은 본문을 확인하고 진행해야 하므로, 수정 화면에 추가합니다. + +**posts-update.mustache** +```java +
+
+ ... + 취소 + + +
+
+``` +**1. btn-delete** +- 삭제 버튼을 수정 완료 버튼 옆에 추가합니다. +- 해당 버튼 클릭시 `JS`에서 이벤트를 수신할 예정입니다. + +삭제 이벤트를 진행할 `JS` 코드도 추가합니다. + +**index.js** +```java +var main = { + init: function () { + var _this = this; + ... + + $('#btn-delete').on('click', function () { + _this.delete(); + }); + }, + ... + delete: function () { + var id = $('#id').val(); + + $.ajax({ + type: 'DELETE', + url: '/api/v1/posts/' + id, + dataType: 'json', + contentType: 'application/json; charset=utf-8' + }).done(function () { + alert('글이 삭제되었습니다.'); + window.location.href = '/'; + }).fail(function (error) { + alert(JSON.stringify(error)); + }) + } +}; + +main.init(); +``` +`type`은 `DELETE`를 제외하고는 `update function`과 크게 차이 나진 않습니다. 다음으로는 삭제 API를 만듭니다. 먼저 서비스 메소드입니다. + +**PostsService** +```java +@RequiredArgsConstructor +@Service +public class PostsService { + private final PostsRepository postsRepository; + + ... + + @Transactional + public void delete(Long id) { + Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id)); + + postsRepository.delete(posts); // 1. + } +} +``` +**1. postsRepository.delete(posts);** +- `JpaRepository`에서 이미 `delete` 메소드를 지원하고 있으니 이를 활용합니다. +- 엔티티를 파라미터로 삭제할 수도 있고, `deleteById` 메소드를 이용하면 `id`로 삭제할 수도 있습니다. +- 존재하는 `Posts`인지 확인을 위해 엔티티 조회 후 그대로 삭제합니다. + +서비스에서 만든 `delete` 메소드를 컨트롤러가 사용하도록 코드를 추가합니다. + +**PostApiController** +```java +@RequiredArgsConstructor +@RestController +public class PostsApiController { + + private final PostsService postsService; + + ... + + @DeleteMapping("/api/v1/posts/{id}") + public Long delete(@PathVariable Long id) { + postsService.delete(id); + return id; + } +} +``` +컨트롤러까지 생성되었으니 브라우저에서 기능이 잘 동작하는지 테스트를 해봅니다. + +--- \ No newline at end of file diff --git a/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/domain/posts/PostsRepository.java b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/domain/posts/PostsRepository.java index 1b148b4..f434274 100644 --- a/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/domain/posts/PostsRepository.java +++ b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/domain/posts/PostsRepository.java @@ -1,6 +1,12 @@ package com.banjjoknim.book.springboot.domain.posts; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; public interface PostsRepository extends JpaRepository { + + @Query("SELECT p FROM Posts p ORDER BY p.id DESC") + List findAllDesc(); } diff --git a/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/service/PostsService.java b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/service/PostsService.java index 7899d2a..6e1c8d3 100644 --- a/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/service/PostsService.java +++ b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/service/PostsService.java @@ -2,6 +2,7 @@ package com.banjjoknim.book.springboot.service; import com.banjjoknim.book.springboot.domain.posts.Posts; import com.banjjoknim.book.springboot.domain.posts.PostsRepository; +import com.banjjoknim.book.springboot.web.dto.PostsListResponseDto; import com.banjjoknim.book.springboot.web.dto.PostsResponseDto; import com.banjjoknim.book.springboot.web.dto.PostsSaveRequestDto; import com.banjjoknim.book.springboot.web.dto.PostsUpdateRequestDto; @@ -9,6 +10,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + +import static java.util.stream.Collectors.toList; + @RequiredArgsConstructor @Service public class PostsService { @@ -33,4 +38,18 @@ public class PostsService { return new PostsResponseDto(entity); } + + @Transactional(readOnly = true) + public List findAllDesc() { + return postsRepository.findAllDesc().stream() + .map(PostsListResponseDto::new) + .collect(toList()); + } + + @Transactional + public void delete(Long id) { + Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id)); + + postsRepository.delete(posts); + } } diff --git a/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/IndexController.java b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/IndexController.java new file mode 100644 index 0000000..47cde0d --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/IndexController.java @@ -0,0 +1,36 @@ +package com.banjjoknim.book.springboot.web; + +import com.banjjoknim.book.springboot.service.PostsService; +import com.banjjoknim.book.springboot.web.dto.PostsResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@RequiredArgsConstructor +@Controller +public class IndexController { + + private final PostsService postsService; + + @GetMapping("/") + public String index(Model model) { + model.addAttribute("posts", postsService.findAllDesc()); + return "index"; + } + + @GetMapping("/posts/save") + public String postsSave() { + return "posts-save"; + } + + @GetMapping("/posts/update/{id}") + public String postsUpdate(@PathVariable Long id, Model model) { + PostsResponseDto dto = postsService.findById(id); + model.addAttribute("post", dto); + + return "posts-update"; + } + +} diff --git a/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/PostsApiController.java b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/PostsApiController.java index 85da0d5..62d8099 100644 --- a/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/PostsApiController.java +++ b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/PostsApiController.java @@ -27,4 +27,10 @@ public class PostsApiController { public PostsResponseDto findById(@PathVariable Long id) { return postsService.findById(id); } + + @DeleteMapping("/api/v1/posts/{id}") + public Long delete(@PathVariable Long id) { + postsService.delete(id); + return id; + } } diff --git a/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/dto/PostsListResponseDto.java b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/dto/PostsListResponseDto.java new file mode 100644 index 0000000..9e2f0fe --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/dto/PostsListResponseDto.java @@ -0,0 +1,22 @@ +package com.banjjoknim.book.springboot.web.dto; + +import com.banjjoknim.book.springboot.domain.posts.Posts; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class PostsListResponseDto { + + private Long id; + private String title; + private String author; + private LocalDateTime modifiedDate; + + public PostsListResponseDto(Posts entity) { + this.id = entity.getId(); + this.title = entity.getTitle(); + this.author = entity.getAuthor(); + this.modifiedDate = entity.getModifiedDate(); + } +} diff --git a/WebServiceBySpringBootAndAWS/src/main/resources/static/js/app/index.js b/WebServiceBySpringBootAndAWS/src/main/resources/static/js/app/index.js new file mode 100644 index 0000000..3c4a18f --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/main/resources/static/js/app/index.js @@ -0,0 +1,74 @@ +var main = { + init: function () { + var _this = this; + $('#btn-save').on('click', function () { + _this.save(); + }); + + $('#btn-update').on('click', function () { + _this.update(); + }); + + $('#btn-delete').on('click', function () { + _this.delete(); + }); + }, + save: function () { + var data = { + title: $('#title').val(), + author: $('#author').val(), + content: $('#content').val() + }; + + $.ajax({ + type: 'POST', + url: '/api/v1/posts', + dataType: 'json', + contentType: 'application/json; charset=utf-8', + data: JSON.stringify(data) + }).done(function () { + alert('글이 등록되었습니다.'); + window.location.href = '/'; + }).fail(function (error) { + alert(JSON.stringify(error)); + }); + }, + update: function () { + var data = { + title: $('#title').val(), + content: $('#content').val() + }; + + var id = $('#id').val(); + + $.ajax({ + type: 'PUT', + url: '/api/v1/posts/' + id, + dataType: 'json', + contentType: 'application/json; charset=utf-8', + data: JSON.stringify(data) + }).done(function () { + alert('글이 수정되었습니다.'); + window.location.href = '/'; + }).fail(function (error) { + alert(JSON.stringify(error)); + }); + }, + delete: function () { + var id = $('#id').val(); + + $.ajax({ + type: 'DELETE', + url: '/api/v1/posts/' + id, + dataType: 'json', + contentType: 'application/json; charset=utf-8' + }).done(function () { + alert('글이 삭제되었습니다.'); + window.location.href = '/'; + }).fail(function (error) { + alert(JSON.stringify(error)); + }) + } +}; + +main.init(); \ No newline at end of file diff --git a/WebServiceBySpringBootAndAWS/src/main/resources/templates/index.mustache b/WebServiceBySpringBootAndAWS/src/main/resources/templates/index.mustache new file mode 100644 index 0000000..a8dd805 --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/main/resources/templates/index.mustache @@ -0,0 +1,34 @@ +{{>layout/header}} + +

스프링 부트로 시작하는 웹 서비스 Ver.2

+
+
+ +
+
+ + + + + + + + + + + + {{#posts}} + + + + + + + {{/posts}} + +
게시글번호제목작성자최종수정일
{{id}}{{title}}{{author}}{{modifiedDate}}
+
+ +{{>layout/footer}} \ No newline at end of file diff --git a/WebServiceBySpringBootAndAWS/src/main/resources/templates/layout/footer.mustache b/WebServiceBySpringBootAndAWS/src/main/resources/templates/layout/footer.mustache new file mode 100644 index 0000000..168de24 --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/main/resources/templates/layout/footer.mustache @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/WebServiceBySpringBootAndAWS/src/main/resources/templates/layout/header.mustache b/WebServiceBySpringBootAndAWS/src/main/resources/templates/layout/header.mustache new file mode 100644 index 0000000..c00872c --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/main/resources/templates/layout/header.mustache @@ -0,0 +1,9 @@ + + + + 스프링 부트 웹 서비스 + + + + + diff --git a/WebServiceBySpringBootAndAWS/src/main/resources/templates/posts-save.mustache b/WebServiceBySpringBootAndAWS/src/main/resources/templates/posts-save.mustache new file mode 100644 index 0000000..1d66f0a --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/main/resources/templates/posts-save.mustache @@ -0,0 +1,26 @@ +{{>layout/header}} + +

게시글 등록

+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ 취소 + +
+
+ +{{>layout/footer}} \ No newline at end of file diff --git a/WebServiceBySpringBootAndAWS/src/main/resources/templates/posts-update.mustache b/WebServiceBySpringBootAndAWS/src/main/resources/templates/posts-update.mustache new file mode 100644 index 0000000..5cc99c9 --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/main/resources/templates/posts-update.mustache @@ -0,0 +1,31 @@ +{{>layout/header}} + +

게시글 수정

+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ 취소 + + +
+
+ +{{>layout/footer}} \ No newline at end of file diff --git a/WebServiceBySpringBootAndAWS/src/test/java/com/banjjoknim/book/springboot/web/IndexControllerTest.java b/WebServiceBySpringBootAndAWS/src/test/java/com/banjjoknim/book/springboot/web/IndexControllerTest.java new file mode 100644 index 0000000..5074083 --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/test/java/com/banjjoknim/book/springboot/web/IndexControllerTest.java @@ -0,0 +1,28 @@ +package com.banjjoknim.book.springboot.web; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +public class IndexControllerTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void 메인페이지_로딩() { + // when + String body = this.restTemplate.getForObject("/", String.class); + + // then + assertThat(body).contains("스프링 부트로 시작하는 웹 서비스"); + } +} \ No newline at end of file