application.yml
spring:
profiles:
active:
- dev #활성화할 프로필 설정
application-dev.yml
server:
servlet:
encoding:
charset: utf-8
force: true
port: 8080
spring:
mustache:
servlet:
expose-session-attributes: true # Mustache 템플릿에서 세션 속성에 접근할 수 있도록 허용
expose-request-attributes: true # Mustache 템플릿에서 요청 속성에 접근할 수 있도록 허용
datasource:
driver-class-name: org.h2.Driver # 데이터베이스 드라이버로 H2 DB를 사용
url: jdbc:h2:mem:test;MODE=MySQL # H2 인메모리 데이터베이스를 MySQL 호환 모드로 사용 (테스트용)
username: sa # 데이터베이스 연결 시 기본 사용자 이름
password: # 데이터베이스 기본 비밀번호 (비어 있음)
h2:
console:
enabled: true # H2 데이터베이스 콘솔을 활성화하여 브라우저에서 데이터베이스를 관리할 수 있도록 함
sql:
init:
data-locations:
- classpath:db/data.sql # 애플리케이션 초기화 시 실행할 데이터 삽입 SQL 파일의 경로 (data.sql)
jpa:
hibernate:
ddl-auto: create # 애플리케이션이 시작될 때 데이터베이스 테이블을 자동으로 생성
show-sql: true # Hibernate가 실행하는 SQL 쿼리를 콘솔에 출력
properties:
hibernate:
format_sql: true # 출력되는 SQL 쿼리를 포맷팅하여 읽기 쉽게 출력
defer-datasource-initialization: true # 데이터베이스 초기화가 지연되도록 설정하여 JPA 설정 후에 데이터 초기화
output:
ansi:
enabled: always # 콘솔 출력 시 ANSI 색상을 항상 사용하도록 설정 (색상을 통해 로그를 더 쉽게 구분 가능)
엔티티 클래스 만들기 (Board.java)
package com.tenco.blog_v1.board;
import jakarta.persistence.*;
import lombok.Data;
import java.sql.Timestamp;
@Entity
@Table(name = "board_tb")
@Data
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 기본키 전략 db 위임
private Integer id;
private String title;
private String content;
// created_at 컬럼과 매핑하며, 이 필드는 데이터 저장시 자동으로 설정 됨
@Column(name = "created_at", insertable = false, updatable = false)
private Timestamp createdAt;
}
레포지토리 클래스 작성 (BoardNativeRepository.java) - Native쿼리 연습
- @Repository 애노테이션으로 스프링에게 이 클래스가 레포지토리임을 알린다.
- EntityManager를 주입받아 Native Query를 사용하여 CRUD 메서드를 구현한다.
- @Transactional 애노테이션을 사용하여 데이터 변경 메서드에 트랜잭션을 관리한다.
package com.tenco.blog_v1.board;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@RequiredArgsConstructor
@Repository // IoC
public class BoardNativeRepository {
// DI 처리
private final EntityManager em;
/**
* 새로운 게시를 생성
*
* @param title
* @param content
*/
@Transactional
public void save(String title, String content) {
Query query = em.createNativeQuery(
"INSERT INTO board_tb(title, content, created_at) VALUES (?, ?, NOW())"
);
query.setParameter(1, title);
query.setParameter(2, content);
// 실행
query.executeUpdate();
}
/**
* 특정 ID의 게시글을 조회 합니다.
*
* @param id
* @return
*/
public Board findById(int id) {
Query query = em.createNativeQuery("SELECT * FROM board_tb WHERE id = ? ", Board.class);
query.setParameter(1, id);
return (Board) query.getSingleResult();
}
/**
* 모든 게시글 조회
*
* @return
*/
public List<Board> findAll() {
Query query = em.createNativeQuery("SELECT * FROM board_tb ORDER By id DESC ");
return query.getResultList();
}
/**
* 특정 ID로 게시글을 수정하는 기능
*
* @param id
* @param title
* @param content
*/
@Transactional
public void updateById(int id, String title, String content) {
Query query = em.createNativeQuery("UPDATE board_tb SET title = ?, content = ? WHERE id = ?");
query.setParameter(1, title);
query.setParameter(2, content);
query.setParameter(3, id);
query.executeUpdate();
}
/**
* 특정 ID의 게시글을 삭제 합니다.
* @param id
*/
@Transactional
public void deleteById(int id) {
Query query
= em.createNativeQuery("DELETE FROM board_tb WHERE id = ?");
query.setParameter(1, id);
query.executeUpdate();
}
}
컨트롤러 클래스 작성 (BoardController.java)
- @Controller 애노테이션으로 스프링에게 이 클래스가 컨트롤러임을 알린다.
- @RequiredArgsConstructor를 사용하여 레포지토리를 주입받는다.
- 각 HTTP 요청에 대해 적절한 핸들러 메서드를 작성한다.
- GET 요청: 데이터 조회 및 뷰 반환
- POST 요청: 데이터 변경 및 리다이렉트
index.mustache
{{> layout/header}}
<div class="container p-5">
<!-- 게시글 목록을 반복 출력 (boardList가 null이 아니고 비어 있지 않다면 출력) -->
{{#boardList}}
<div class="card mb-3">
<div class="card-body">
<h4 class="card-title mb-3">{{title}}</h4>
<a href="/board/{{id}}" class="btn btn-primary">상세보기</a>
</div>
</div>
{{/boardList}} <!-- 반드시 섹션을 닫는 태그가 필요 -->
<!-- 게시글이 없을 경우 출력할 내용 -->
{{^boardList}}
<p>게시글이 없습니다.</p>
{{/boardList}}
<ul class="pagination d-flex justify-content-center">
<li class="page-item disabled"><a class="page-link" href="#">Previous</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</div>
{{> layout/footer}}
static/styles.css
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.content {
flex: 1;
}
footer {
position: relative;
bottom: 0;
width: 100%;
background-color: #f8f9fa;
text-align: center;
padding: 20px;
}
save-form.mustache
{{> layout/header}} {{! Partial 태그 (부분 템플릿 태그) }}
<main class="container p-5 content">
<article>
<div class="card">
<div class="card-header"><b>글쓰기 화면입니다</b></div>
<div class="card-body">
<form action="/board/save" method="post">
<div class="mb-3">
<input type="text" class="form-control" placeholder="Enter title" name="title">
</div>
<div class="mb-3">
<textarea class="form-control" rows="5" name="content"></textarea>
</div>
<button class="btn btn-primary form-control">글쓰기완료</button>
</form>
</div>
</div>
</article>
</main>
{{> layout/footer}}
BoardController
package com.tenco.blog_v1.board;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
@Controller
public class BoardController {
private final BoardNativeRepository boardNativeRepository;
@GetMapping("/")
public String index(Model model) {
List<Board> boardList = boardNativeRepository.findAll();
model.addAttribute("boardList", boardList);
log.warn("여기까지 오니");
return "index";
}
// 주소설계 - http://localhost:8080/board/save-form
// 게시글 작성 화면
@GetMapping("/board/save-form")
public String saveForm() {
return "board/save-form";
}
// 게시글 저장
// 주소설계 - http://localhost:8080/board/save
@PostMapping("/board/save")
public String save(@RequestParam(name = "title") String title, @RequestParam(name = "content") String content) {
// 파라미터가 올바르게 전달 되었는지 확인
log.warn("save 실행: 제목={}, 내용={}", title, content);
boardNativeRepository.save(title, content);
return "redirect:/";
}
// 특정 게시글 요청 화면
// 주소설계 - http://localhost:8080/board/10
@GetMapping("/board/{id}")
public String detail(@PathVariable(name = "id") Integer id, HttpServletRequest request) {
Board board = boardNativeRepository.findById(id);
request.setAttribute("board", board);
return "board/detail";
}
// 주소설계 - http://localhost:8080/board/10/delete ( form 활용이기 때문에 delete 선언)
// form 태크에서는 GET, POST 방식만 지원하기 때문이다.
@PostMapping("/board/{id}/delete")
public String delete(@PathVariable(name = "id") Integer id) {
boardNativeRepository.deleteById(id);
return "redirect:/";
}
// 게시글 수정 화면 요청
// board/id/update
@GetMapping("/board/{id}/update-form")
public String updateForm(@PathVariable(name = "id") Integer id, HttpServletRequest request) {
Board board = boardNativeRepository.findById(id);
request.setAttribute("board", board);
return "board/update-form"; // src/main/resources/templates/board/update-form.mustache
}
// 게시글 수정 요청 기능
// board/{id}/update
@PostMapping("/board/{id}/update")
public String update(@PathVariable(name = "id") Integer id, @RequestParam(name = "title") String title, @RequestParam(name = "content") String content) {
boardNativeRepository.updateById(id, title, content);
return "redirect:/board/" + id;
}
}
Native Query와 JPQL란 뭘까?
▶ Native Query 소개
Native Query는 데이터베이스의 고유한 SQL 문법을 사용하여 쿼리를 작성한다. 복잡한 쿼리나 JPQL로 표현하기 어려운 특정 기능을 사용할 때 유용하다.
▶ JPQL(Java Persistence Query Language)란? (앞으로 배워야 하는 부분)
JPQL은 객체 지향 쿼리 언어로, 엔티티 객체를 대상으로 쿼리를 작성한다. JPQL은 데이터베이스 독립적이며, JPA의 장점을 최대한 활용할 수 있게 해준다.
템플릿 작성 (Mustache)
- src/main/resources/templates/ 디렉토리에 Mustache 템플릿 파일을 생성합니다.
- 각 컨트롤러 메서드가 반환하는 뷰 이름에 맞춰 템플릿 파일을 작성합니다.
- index.mustache
- board/save-form.mustache
- board/detail.mustache
- board/update-form.mustache
index.mustache
{{> layout/header}}
<div class="container p-5">
<!-- 게시글 목록을 반복 출력 (boardList가 null이 아니고 비어 있지 않다면 출력) -->
{{#boardList}}
<div class="card mb-3">
<div class="card-body">
<h4 class="card-title mb-3">{{title}}</h4>
<a href="/board/{{id}}" class="btn btn-primary">상세보기</a>
</div>
</div>
{{/boardList}} <!-- 반드시 섹션을 닫는 태그가 필요 -->
<!-- 게시글이 없을 경우 출력할 내용 -->
{{^boardList}}
<p>게시글이 없습니다.</p>
{{/boardList}}
<ul class="pagination d-flex justify-content-center">
<li class="page-item disabled"><a class="page-link" href="#">Previous</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</div>
{{> layout/footer}}
Mustache 태그 설명
- {{> layout/header}}: Partial(부분 템플릿) 태그이다. header.mustache 파일을 포함해 헤더를 재사용할 수 있다. 공통 레이아웃을 유지하는 데 유용하다.
- {{#boardList}}: Section(섹션) 태그입니다. 조건부 반복 블록으로 boardList 데이터가 존재할 때만 내부 블록을 반복 처리한다. 주로 리스트 데이터 렌더링에 사용된다.
- {{title}} / {{id}}: Variable(변수) 태그이다. Mustache에서 데이터 바인딩을 처리하는 구문으로, 해당 변수 값이 템플릿에서 렌더링된다. 이 경우 각 게시글의 title과 id 값이 출력된다.
728x90
'Spring boot > Blog 프로젝트 만들기(JPA)' 카테고리의 다른 글
완성 코드 STEP 02 - 연관 관계 매핑: User와 Board 엔티티 ( JPA API , JPQL 쿼리 사용, 인증(세션 로직 추가 ) (0) | 2024.10.07 |
---|---|
완성 코드 STEP 02 - 사용자 관리 기본 코드 추가 및 설정( JPA API , JPQL 쿼리 사용, 인증(세션 로직 추가 ) (0) | 2024.10.07 |
완성 코드 STEP 01 - 프로젝트 생성 (0) | 2024.10.07 |
Mustache 란 뭘까? (2) | 2024.10.07 |
템플릿 엔진이란 뭘까? (2) | 2024.10.07 |