본문으로 바로가기
728x90

 

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)

 

 

 

테스트 결과

 

- 수정화면에서 삭제 버튼 클릭

 

 

- 삭제 완료

 

728x90