부트스트랩, 제이쿼리 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>
- css 불러옴 (화면 그리는 역할이라 head에서 불러야 함)
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.js 추가-->
<script src="/js/app/index.js"></script>
</body>
</html>
- js 불러옴 (js 용량 크면 body 부분 실행 늦춰져서 하단에 두는게 좋음 - 화면 다 그려진 후 호출)
- bootstrap.js는 제이쿼리에 의존해서 제이쿼리가 먼저 호출되게 코드 작성
index.mustache
{{>layout/header}}
<!--: 현재 머스테치 파일을 기준으로 다른 파일 가져옴-->
<h1>스프링 부트로 시작하는 웹 서비스</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}}
<!-- post라는 List 순회 (for문)-->
<tr>
<!-- List에서 뽑아낸 객체 필드 사용-->
<td>{{id}}</td>
<td>{{title}}</td>
<td>{{author}}</td>
<td>{{modifiedDate}}</td>
</tr>
{{/posts}}
</tbody>
</table>
</div>
{{>layout/footer}}
- {{>}}
- 현재 mustache 파일을 기준으로 다른파일 가져옴
- {{#posts}}
- posts 라는 List 순회 (for문과 동일)
- {{변수명}}
- List에서 뽑아낸 객체의 필드 사용
index.js
API 호출하는 js (게시글 등록 화면의 등록 버튼에 기능 넣어주기 위한)
src/main/resources/static/js/app에 생성하기
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();
- var main={...} 으로 선언한 (index 변수 속성으로 function 추가) 이유
- 브라우저의 scope(유효 범위)는 공용 공간으로 쓰여서 나중에 로딩된 fucntion이 덮어씀
- 중복된 함수 이름은 자주 발생하는데 이때마다 이름을 확인하면서 만들 수 없음
- index.js(해당 객체)만의 scope를 만들어서 사용함
-> index 객체 안에서만 function이 유효해서 다른 js와 겹칠 위험 없어짐
@Query
PostRepository
package com.springAWS.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<Posts, Long> {
@Query("SELECT p FROM Posts p ORDER BY p.id DESC") //SpringDataJpa에서 제공하지 않는 메소드는 쿼리로 작성해도 됨
List<Posts> findAllDesc();
}
- SpringDataJpa에서 제공하지 않는 메소드는 query로 작성해도 됨 (훨씬 가독성 좋음)
- 규모 있는 프로젝트에서는 조회용 프레임워크로 데이터 조회 (querydsl, jooq, MyBatis 등)
등록/수정/삭제는 SpringDataJpa로 진행
PostsService에 추가
@RequiredArgsConstructor //final이 선언된 모든 필드를 인자값으로 하는 생성자 생성해줌
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional(readOnly = true)
public List<PostsListResponseDto> findAllDesc(){
return postsRepository.findAllDesc().stream()
.map(PostsListResponseDto::new)
.collect(Collectors.toList());
}
}
- @Transactional
- readOnly=true
- 트랜잭션 범위 유지하고 조회 기능만 남겨둠
- 조회 속도가 개선됨
- readOnly=true
- .map(PostsListResponseDto::new)
- .map(Posts -> new PostsListResponseDto(posts)) 와 같음
- postsRepository 결과로 넘어온 Posts의 Stream을 map을 통해 PostsListResponseDto로 변환해 List로 반환하는 메소드
PostsListResponseDto
package com.springAWS.web.dto;
import com.springAWS.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();
}
}
IndexController
package com.springAWS.web;
import com.springAWS.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("/")
public String index(Model model) {
model.addAttribute("posts",postsService.findAllDesc());
return "index";
//머스테치 스타터 덕에 컨트롤러에서 문자열 반환 시 앞의 경로와 뒤의 파일 확장자 자동 지정됨
//src/main/resources/templates/index.mustache로 전환돼 View Resolver 처리하게 됨
}
@GetMapping("/posts/save")
public String postsSave(){
return "posts-save";
}
}
- Model
- 서버 템플릿 엔진에서 사용 가능한 객체 저장할 수 있음
- postsService.findAllDesc()로 가져온 결과를 posts로 index.mustache에 전달함