스프링 부트 와 AWS 웹서비스 만들기 #4

xicodey·2022년 4월 13일
0

이글은 공부한 내용을 정리한 글입니다.
정리해두면 나중에 보기 좋으니...

머테치로 화면 구성하기

머스테치란

수많ㅡㄴ 언어를 지원하는 가장 심플한 템플릿 엔진

JSP,Freemarker은 서버 템플릿 엔진이고, 리액트, 뷰는 클라이언트 템플릿 엔진이다.
머스테치는 서버 템플릿 엔진 언어도 지원하고 클라이언트 템플릿 엔진으로 모두 사용 할 수 있다

물론. 템플릿 엔진은 JSP, Velocity, Fremarker, Thymeleaf등 있지만, 그둘과 다르게
머스테치의 장점이라 하면

문법이 다른 템플릿 엔진보다 심플하다.
로직 코드를 사용할 수 없어 View의 역활과 서버의 역활이 명확하게 분리된다.
Mustache.js와 Mustache.java 2가지가 다 있어, 하나의 문법으로 클라이언트/ 서버 템플릿 모두 사용 가능하다.

머스테치 설치

Markgetpalce에서 검색해서 설치하자.

기본페이지 만들기

가장 먼저 스프링 부트 프로젝트에 build.gradle에 머스테치 스터디 의존성을 추가합시다.

implementation('org.springframework.boot:spring-boot-starter-mustache')

머슽테치의 파일 위치 src/main/resources/templates

index.mustache 생성

<!DOCTYPE HTML>
<html>
<head>
    <title>스프링부트 웹서비스</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>
	<h1>스프링 부트로 시작하는 웹 서비스</h1>
</body>
</html>

이 머스테치에 URL을 매핑하기 위해 Controoler를 생성합시다.

IndexController

@Controller
public class IndexController {
    private final PostsService postsService;

    @GetMapping("/")
    public String index(Model model) {
        return "index";
    }
}    

이제 코드가 완성되었으니 테스트 코드를 작성합시다.

IndexControllerTest

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class IndexControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void 메인페이지_로딩() throws Exception{
        //when
        String body = this.restTemplate.getForObject("/", String.class);

        //then
        assertThat(body).contains("스프링 부트로 시작하는 웹 서비스");
    }
}

이 테스트는 실제로 URL 호출 시 페이지의 내용이 제대로 호출되는지에 대한 테스트입니다.

게시글 등록 화면 만들기

공통 영역을 별로의 파일로 분리하여 관리하기

src/main/resource/templates/layout 디펙토리에 footer.mustache, header.mustache를 추가합시다.

header.mustache

<!DOCTYPE HTML>
<html>
<head>
    <title>스프링부트 웹서비스</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>
{{>layout/footer}}

footer.mustache

<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
```

자주 쓰는 부분은 공통의 영역으로 관리하면, index.mustache는 이렇게 깔끔해진다.

{{>layout/header}}  // 헤더부분
<h1>스프링 부트로 시작하는 웹 서비스</h1>
{{>layout/footer}}  // 풋터부분

레이아웃으로 파일을 분리했으니 index.mustache에 글 등록 버튼을 추가 합시다.

{{>layout/header}}

<h1>스프링부트로 시작하는 웹 서비스 Ver.2</h1>
<div class="col-md-12">
    <div class="row">
        <div class="col-md-6">
            <a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
        </div>
    </div>
    <br>
</div>

{{>layout/footer}}
**<a> 태그를 이용해서 글 등록 페이지로 이동하는 글등록 버튼이 생성되습니다.**

이동할 페이지의 주소는 /posts/save에 페이지에 관련된 컨틀롤러는 모두 indexController를 사용합니다.

IndexController에 추가

@GetMapping("/posts/save")
public String postsSave() {
    return "posts-save"; 
}

index.mustache와 마찬가지로 /posts/save를 호출하면 posts-save.mustache를 호출하는 메소드를 추가했습니다.
컨트롤러 코드가 생성되었으니 posts-save.mustache 생성합시다.

posts-save.mustache

{{>layout/header}}

<h1>게시글 등록</h1>

<div class="col-md-12">
  <div class="col-md-4">
      <form>
          <div class="form-group">
              <label for="title">제목</label>
              <input type="text" class="form-control" id="title" placeholder="제목을 입력하세요">
          </div>
          <div class="form-group">
              <label for="author"> 작성자 </label>
              <input type="text" class="form-control" id="author" placeholder="작성자를 입력하세요">
          </div>
          <div class="form-group">
              <label for="content"> 내용 </label>
              <textarea class="form-control" id="content" placeholder="내용을 입력하세요"></textarea>
          </div>
      </form>
      <a href="/" role="button" class="btn btn-secondary">취소</a>
      <button type="button" class="btn btn-primary" id="btn-save">등록</button>
  </div>
</div>

{{>layout/footer}}

등록 버튼은 기능이 없으니 src/main/resources에 static/js/app 디렉토리를 생성합시다.
그리고 index.js를 만듭시다.

index.js

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));
      });
  }
};

main.init();

footer.mustache에 추가합시다.

<!--index.js 추가-->
<script src="/js/app/index.js"></script>

index.js 호출 코드를 보면 절대 경로(/)로 시작한다.

전체 조회 화면 만들기

index.mustache에 UI를 변경합시다.

{{>layout/header}}

<h1>스프링부트로 시작하는 웹 서비스 Ver.2</h1>
<div class="col-md-12">
  <div class="row">
      <div class="col-md-6">
          <a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
      </div>
  </div>
  <br>
  <!-- 목록 출력 영역 -->
  <table class="table table-horizontal table-bordered">
      <thead class="thead-strong">
      <tr>
          <th>게시글번호</th>
          <th>제목</th>
          <th>작성자</th>
          <th>최종수정일</th>
      </tr>
      </thead>
      <tbody id="tbody">
      {{#posts}}
          <tr>
              <td>{{id}}</td>
              <td>{{title}}</td>
              <td>{{author}}</td>
              <td>{{modifiedDate}}</td>
          </tr>
      {{/posts}}
      </tbody>
  </table>
</div>

{{>layout/footer}}

{{#posts}}

posts 라는 List를 순회합니다.
Java의 for문과 동일함

{{변수명}}

List에서 뽑아낸 객체의 필드를 사용함

이제 만들었으면 PostsRepository 인터페이스에 쿼리 추가합시다.

PostsRepository

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface PostsRepository extends JpaRepository<Posts, Long> {

  @Query("SELECT p FROM Posts p ORDER BY p.id DESC ")
  List<Posts> findAllDesc();
}

그다음은 PostsService에 추가합시다.

PostsService

@Transactional(readOnly = true)
  public List<PostsListResponseDto> findAllDesc() {
      return postsRepository.findAllDesc().stream()
              .map(PostsListResponseDto::new)
              .collect(Collectors.toList());
  }

@Transactional(readOnly = true는 트랜잭션 범위에 유지하되, 조회 기능만 남겨두어 조회 속도가 개선된다.
그래서 등록, 수정, 삭제 기능에 전혀 없는 서비스 메소드에서 사용하는것이 좋다

PostsListResponseDto를 만듭시다.

PostsListResponseDto

@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.getModifienDate();
  }
}

마지막으로 Controller를 변경합시다.

@RequiredArgsConstructor
@Controller
public class IndexController {
  private final PostsService postsService;

  @GetMapping("/")
  public String index(Model model) {
      model.addAttribute("posts", postsService.findAllDesc());
      return "index";
  }
}

Model

서버 템플릿 엔진에서 사용할 수 있는 객체를 저장할 수 있습니다.

게시글 수정, 삭제 화면 만들기

posts-update.mustache

{{>layout/header}}

<h1>게시글 수정</h1>

<div class="col-md-12">
  <div class="col-md-4">
      <form>
          <div class="form-group">
              <label for="title">글 번호</label>
              <input type="text" class="form-control" id="id" value="{{post.id}}" readonly>
          </div>
          <div class="form-group">
              <label for="title">제목</label>
              <input type="text" class="form-control" id="title" value="{{post.title}}">
          </div>
          <div class="form-group">
              <label for="author"> 작성자 </label>
              <input type="text" class="form-control" id="author" value="{{post.author}}" readonly>
          </div>
          <div class="form-group">
              <label for="content"> 내용 </label>
              <textarea class="form-control" id="content">{{post.content}}</textarea>
          </div>
      </form>
      <a href="/" role="button" class="btn btn-secondary">취소</a>
      <button type="button" class="btn btn-primary" id="btn-update">수정 완료</button>
  </div>
</div>

{{>layout/footer}}

index.js파일에 updata function을 하나 추가합시다.

 var _this = this;
      $('#btn-save').on('click', function () {
          _this.save();
      });

      $('#btn-update').on('click', function () {
          _this.update();
      });
 ],

.....
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));
      });
  }

index.mustache에 수정 페이지로 이동할 수 있게 코드 추가

{{>layout/header}}

<h1>스프링부트로 시작하는 웹 서비스 Ver.2</h1>
<div class="col-md-12">
  <div class="row">
      <div class="col-md-6">
          <a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
      </div>
  </div>
  <br>
  <!-- 목록 출력 영역 -->
  <table class="table table-horizontal table-bordered">
      <thead class="thead-strong">
      <tr>
          <th>게시글번호</th>
          <th>제목</th>
          <th>작성자</th>
          <th>최종수정일</th>
      </tr>
      </thead>
      <tbody id="tbody">
      {{#posts}}
          <tr>
              <td>{{id}}</td>
              <td><a href="/posts/update/{{id}}">{{title}}</a></td> // a태그 추가
              <td>{{author}}</td>
              <td>{{modifiedDate}}</td>
          </tr>
      {{/posts}}
      </tbody>
  </table>
</div>

{{>layout/footer}}

indexController 메소드 추가

@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에 삭제 버튼을 추가합시다.

{{>layout/header}}

<h1>게시글 수정</h1>

<div class="col-md-12">
  <div class="col-md-4">
      <form>
          <div class="form-group">
              <label for="title">글 번호</label>
              <input type="text" class="form-control" id="id" value="{{post.id}}" readonly>
          </div>
          <div class="form-group">
              <label for="title">제목</label>
              <input type="text" class="form-control" id="title" value="{{post.title}}">
          </div>
          <div class="form-group">
              <label for="author"> 작성자 </label>
              <input type="text" class="form-control" id="author" value="{{post.author}}" readonly>
          </div>
          <div class="form-group">
              <label for="content"> 내용 </label>
              <textarea class="form-control" id="content">{{post.content}}</textarea>
          </div>
      </form>
      <a href="/" role="button" class="btn btn-secondary">취소</a>
      <button type="button" class="btn btn-primary" id="btn-update">수정 완료</button>
      <button type="button" class="btn btn-danger" id="btn-delete">삭제</button> // 삭제 부분
  </div>
</div>

{{>layout/footer}}

index.js에 삭제 함수를 추가합시다.

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));
      });
  }

마지막으로 PostsService에 삭제 API를 만들어 봅시다.

@Transactional
  public void delete (Long id) {
      Posts posts = postsRepository.findById(id)
              .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));

      postsRepository.delete(posts);
  }

postsRepository.delete(posts)

엔티티를 파라미터로 삭제할 수 있고, deleteById 메소드를 이용하면 id를 삭제할 수 있다.
존재하는 Posts인지 확인을 위해 엔티티 조회 후 그대로 삭제한다.

결과물들

글등록 화면

성공시

글 등록 후

수정 화면

수정 성공시

수정 된 화면

글 삭제

잘되는것을 알 수 있다.

0개의 댓글

관련 채용 정보