스프링 부트와 AWS로 혼자 구현하는 웹 서비스 4장
Chapter 04
머스테치로 화면 구성하기
머스테치란?
- 수많은 언어를 지원하는 가장 심플한 템플릿 엔진
템플릿 엔진이란?
- 지정된 템플릿 양식과 데이터가 합쳐서 HTML문서를 출력하는 소프트웨어
머스테치 플러그인 설치
- JSP나 Thymeleaf등은 유료버전에서만 지원 가능
- Plugins으로 mustache 검색 후 설치
기본 페이지 만들기
-build.gradle에 의존성 등록
compile('org.springframework.boot:spring-boot-starter-mustache')
-src/main/resources에 templates 파일을 만들고 index.mustache 생성
-web 패키지 안에 IndexController 생성
테스트 코드로 검증
- test 패키지에 IndexControllerTest 생성
- 테스트가 통과했으므로 검증완료
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
package com.qwon.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("스프링 부트로 시작하는 웹 서비스");
}
}
|
-Application.java의 main 메소드 실행 후, 브라우저에서 http://localhost:8080 접속
게시글 등록 화면 만들기
화면 개발을 위한 오픈소스 사용하기
프론트엔드 라이브러리 이용방법(부트스트랩, 제이쿼리 등)
1. 외부 CDN 사용 <--- 사용할 예정
2. 직접 라이브러리 받아서 사용
- templates 디렉토리에 layout 디렉토리 추가
- footer.mustache, header.mustache 파일 생성
- 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>
</body>
</html>
- header.mustache
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<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>
</body>
</html>
- index.mustache에 글 등록 버튼 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
{{>layout/header}}
<h1>스프링 부트로 시작하는 웹 서비스</h1>
{{>layout/footer}}
{{>layout/header}}
<h1>스프링 부트로 시작하는 웹 서비스 Ver.2</h1>
<div class="col-md-12">
<div class="row">
<div class="col-md-6">
<a href="/post/save" role="button"
class="btn btn-primary">글 등록</a>
</div>
</div>
</div>
{{>layout/footer}}
|
- IndexController에 /posts/save에 해당하는 컨트롤러 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package com.qwon.springboot.web;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@RequiredArgsConstructor
@Controller
public class IndexController {
@GetMapping("/")
public String index(){
return "index";
}
@GetMapping("/posts/save")
public String postsSave(){
return "posts-save";
}
}
|
- index.mustache와 같은 위치에 posts-save.mustache 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
{{>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}}
|
- 서버 실행 후, http://localhost:8080/posts/save로 이동
게시글 등록 버튼에 대한 기능 추가
- src/main/resource에 static/js/app 디렉토리 생성
- index.js 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
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='/';
}).fail(function(error){
alert(JSON.stringify(error));
});
}
};
main.init();
|
index.js에서 var main={}이라는 코드를 쓰는 이유?
- 여러 사람이 참여하는 프로젝트에서는 중복된 함수 이름이 자주 발생할 수 있는데, 이런 문제를 피하려고 유효범위(scope)를 만들어 사용
- 이렇게 하면 index 객체 안에서만 function이 유효하기 때문에 다른 js와 겹칠 위험이 사라짐
이제, index.js를 머스테치 파일이 쓸 수 있게, footer.mustache에 추가
- 스프링 부트는 기본적으로 src/main/resources/static으로 설정되어 있기에 절대경로로 바로 시작하면 된다.
이제, 브라우저를 실행 후, 게시글을 등록한다.
localhost:8080/h2-console로 접속해서 실제 DB에 데이터가 등록되었는지 확인
전체 조회 화면 만들기
- index.mustache 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
{{>layout/header}}
<h1>스프링 부트로 시작하는 웹 서비스</h1>
{{>layout/footer}}
{{>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}}
|
- Controller, Service, Repository 코드 작성하기
- PostRepository 인터페이스에 쿼리 추가
- PostService에 코드 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
package com.qwon.springboot.service.posts;
import com.qwon.springboot.domain.posts.Posts;
import com.qwon.springboot.domain.posts.PostsRepository;
import com.qwon.springboot.web.dto.PostsListResponseDto;
import com.qwon.springboot.web.dto.PostsResponseDto;
import com.qwon.springboot.web.dto.PostsSaveRequestDto;
import com.qwon.springboot.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional
public Long save(PostsSaveRequestDto requestDto){
return postsRepository.save(requestDto.toEntity()).getId();
}
@Transactional
public Long update(Long id, PostsUpdateRequestDto requestDto){
Posts posts=postsRepository.findById(id)
.orElseThrow(()->new
IllegalArgumentException("해당 게시글이 없습니다. id="+id));
posts.update(requestDto.getTitle(),requestDto.getContent());
return id;
}
public PostsResponseDto findById(Long id){
Posts entity=postsRepository.findById(id)
.orElseThrow(()->new
IllegalArgumentException("해당 게시글이 없습니다. id="+id));
return new PostsResponseDto(entity);
}
@Transactional(readOnly=true)
public List<PostsListResponseDto> findAllDesc() {
return postsRepository.findAllDesc().stream()
.map(PostsListResponseDto::new)
.collect(Collectors.toList());
}
}
|
- web/dto 아래에 PostsListResponseDto 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package com.qwon.springboot.web.dto;
import com.qwon.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 변경
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package com.qwon.springboot.web;
import com.qwon.springboot.service.posts.PostsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@RequiredArgsConstructor
@Controller
public class IndexController {
private final PostsService postsService;
@GetMapping("/posts/save")
public String postsSave(){
return "posts-save";
}
@GetMapping("/")
public String index(Model model){
model.addAttribute("posts",postsService.findAllDesc());
return "index";
}
}
|
테스트 결과
- localhost:8080으로 접속한 후, 등록 화면을 이용해 데이터를 등록해보기
게시글 수정, 삭제 화면 만들기
- 게시글 수정화면 머스테치 파일 생성
- src/main/resources/templates/posts-update.mustache
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
{{>layout/header}}
<h1>게시글 수정</h1>
<div class="col-md-12">
<div class="col-md-4">
<form>
<div class="form-group">
<label for="id">글 번호</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에 update 함수 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
var main = {
init : function () {
var _this = this;
$('#btn-save').on('click', function () {
_this.save();
});
$('#btn-update').on('click',function(){
_this.update();
});
},
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));
});
}
};
main.init();
|
- 전체목록에서 수정페이지로 이동할 수 있도록 index.mustache 코드 수정
- IndexController에 메소드 추가
테스트 결과
- 실행시 타이틀 항목에 링크 표시 확인
- 링크 클릭시 제목, 내용 수정가능
- 테스트2로 바꿔보기
게시글 삭제
- posts-update.mustache에 삭제 버튼 추가
- index.js에 btn-delete 코드 추가
- 서비스 메소드(PostsService)
- 컨트롤러 추가(PostsApiController)
테스트 결과
- 수정화면에서 삭제 버튼 클릭
- 삭제 완료