처음부터 따라할 수 있도록! 기록해두기
프로젝트 생성




필기
hdd에 영구히 저장할 수 있도록 도와주는기술 jpa -> db에
{{> layout/header}} layout 폴더에 있는 header : 부분 템플릿
@PathVariable
주소에 들어오는 숫자값을 annotation으로 int id에 넣어줄 수 있음 (받을 수 있음)
괄호 안의 변수와 중괄호 안의 변수가 같아야 한다.
bean = new된 객체
파일이 날라가는게 아니니까 확장자를 쓰지X -> 버퍼에 심어줘야
header의 content-type에 mime 타입
content-type는 header의 key, mime 타입은 key의 value의 형식
담긴 내용의 형식이 어떤건지 mime 타입을 지켜서 content-type로 지정해줘야한다.
- 쿼리스트링 -> 주소 (DB에서 where절에 쓴다 - 구체적 질의 ?key=value&key=value
- 바디(Post,Put) - xwform ( insert 하려고 key=value&key=value
post를 insert,update,delete로 쓸 예정이라서 save-form에서 "/board"가 아닌 action="/board/save"로 한다.
-> 주소에 delete같은 동사 붙이면 안되지만 post로만 할거라서 붙인다!
form 태그는 get,post 밖에 안됨
수행해달라(->끝나면 redirection 어디 페이지로 갈지) = action
<정리>
GET /emp/1
select * from emp where empno = 1;
pk나 unique는 주소에 /를 바로
GET /emp?job=manager
select * from emp where job = "MANAGER"'
pk,unique가 아니면 모두 queryString
GET /emp?job=manager&sal=1000
select * from emp where job = "MANAGER"' and sal = 1000; -> int sal로 받으면 안되고 integer로 받아야 null처리 가능
annotation
spring이 제어권 가지고 new -> 제어의 역전 ioc
EntityManager JDBC connection 객체
@Controller @Repository -> 내부에 @Component 잇음
~
설정 파일의 db연결 코드 읽으면서 ioc에 EntityManager 생김(?
~
@entity 찾아서 query로 바꿔서 실행
~
DB에 board_tb 생김(IoC에 뜬게 아님)
~
IoC자기자신을 순회하면서 EntityManager type으로 찾음 잇네? -> 주입 해줌 (의존성 주입)
repository는 entityManager 의존한다.
service는 repository 의존
controller는 service 의존
게시글 쓰기 완료 후 list로 가면 모델에 데이터 안담고 감 -> list에 안뿌려짐 (reudirection 해야하는 이유)
getRequestDispatcher
model은 request라고 생각
class Model{
HttpServlet request;
void setAttribute(key,value)
}
dataTransferObject DTO
write -> 다른 사람 wait 하도록 만듦
delete -> 없는 게시글 굳이 삭제하겠다고 고립시킬 필요 없음 -> 조회해서 있는지 확인
없으면 실패하는 시간동안 고립됨
update는 model에서 조회해서 뿌리고 수정해야됨
application.properties
# utf-8 한글 인코딩
server.servlet.encoding.charset=utf-8
server.servlet.encoding.force=true
# DB 연결 코드 (EntityManager 만들어냄)
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:test
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
# JPA @Entity 스캔해서 테이블 생성
spring.jpa.hibernate.ddl-auto=create
# 콘솔에 쿼리 표시
spring.jpa.show-sql=true
# 더미데이터 sql문 실행
spring.sql.init.data-locations=classpath:db/data.sql
# ddl-auto가 실행된 후에 sql문 실행하는 법
spring.jpa.defer-datasource-initialization=true
# mustache에서 request 객체 접근하게 설정하는 법
spring.mustache.servlet.expose-request-attributes=truepackage com.metacoding.blogv1.board;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.sql.Timestamp;
// runtime 때 만들어줌
@Getter // (Lombok)
@AllArgsConstructor // full 생성자
@NoArgsConstructor // default 생성자 만들어줌 (Lombok)
@Table(name = "board_tb") // table명 설정
@Entity // jpa가 관리할 수 있게 설정
public class Board {
@Id // pk 설정
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment 설정
private Integer id;
private String title;
private String content;
private Timestamp createdAt; // now
}
package com.metacoding.blogv1.board;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
// 책임 : 요청 잘 받고 응답 잘 하고
@Controller // 컴퍼넌트 스캔 -> DS가 활용
public class BoardController {
// DI
private BoardService boardService;
public BoardController(BoardService boardService) {
this.boardService = boardService;
}
@PostMapping("/board/{id}/update")
public String update(@PathVariable("id") int id, String title, String content) {
// update board_tb set title(body로 받음)=?, content(body로 받음)=? where id(주소로 받음) =?
// 주소로 받는 데이터는 전부 where에 걸린다. (pk,unique는 /1로, queryString
return "redirect:/board/" + id;
}
@PostMapping("/board/{id}/delete") // 원래는 DeleteMapping이지만 JS를 안배워서 PostMapping으로
public String delete(@PathVariable("id") int id) {
// System.out.println("id : " + id); // id로 db 가서 삭제하면 됨 -> 이건 controller의 책임이 아님
boardService.게시글삭제(id); // return 받을게 없음
return "redirect:/";
}
@PostMapping("/board/save")
public String save(String title, String content) { // input 태그의 name값과 같아야 한다. // -> req.getParameter()와 같다
// return "list"; -> 안되는 이유) db 연결 후 데이터 뿌릴때 문제 생김(db 데이터가 안들어감) - 모델이 데이터 안담고 간다
boardService.게시글쓰기(title, content);
return "redirect:/"; // page로 가는 주소가 만들어져 있으면 무조건 redirection (재활용 하세용)
}
// c - model - v
@GetMapping("/")
public String list(HttpServletRequest request) {
// DB 조회해서 화면에 뿌려줘야 됨 -> service에게 요청
List<Board> boardList = boardService.게시글목록(); // boardList는 model이다
request.setAttribute("models", boardList); // request에 담기
return "list"; // forward하기 // = req.getRequestDispatcher("list").forward(req,resp)
}
// model을 request에 담아야지 ->
@GetMapping("/board/{id}") // 패턴 매칭 /board/1, /board/2
public String detail(@PathVariable("id") int id, HttpServletRequest request) {
Board board = boardService.게시글상세보기(id); // board는 model이다
request.setAttribute("model", board);
return "detail";
}
// c - v
@GetMapping("/board/save-form") // 주소 (하이픈(-) 사용)
public String saveForm() { // 자바는 카멜 표기법ㅇ, url에는 _,카멜 안씀
return "save-form"; // 확장자 없어도 되는 이유 - 무조건 viewResolver를 탄다!
}
// ex)게시글 3번 수정하기
@GetMapping("/board/{id}/update-form") // URI
public String updateForm(@PathVariable("id") int id) {
return "update-form"; // 파일명
}
}
package com.metacoding.blogv1.board;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
// 책임: 트랜잭션 처리, 비지니스 로직 처리 (ex)송금 중 잔액 검사)
@Service // IoC
public class BoardService {
private BoardRepository boardRepository;
// DI : 의존성 주입 -> IoC로부터 들고옴 (BR는 반드시 IoC에 떠있어야 함)
public BoardService(BoardRepository boardRepository) {
this.boardRepository = boardRepository;
}
// boardRepository는 ioc에 떠있으니까 DI받아야됨
@Transactional // 트랜잭션 시작 -> 함수 내부가 다 수행되면 commit, 실패 rollback
public void 게시글쓰기(String title, String content) {
boardRepository.insert(title, content);
}
// write(insert,update,delete)만 commit/rollBack -> 트랜잭셔널 없어도 됨
public List<Board> 게시글목록() {
List<Board> boardList = boardRepository.findAll();
return boardList; // 그냥 boardRepository.findAll() 바로 리턴해도 됨 할 일 없으니까 바로 위임 가능
}
public Board 게시글상세보기(int id) {
return boardRepository.findById(id); // 할 일 없으니까 바로 위임
}
@Transactional
public void 게시글삭제(int id) {
// 1. 게시글이 존재하는지 확인
Board board = boardRepository.findById(id);
// 2. 삭제
if (board == null) {
throw new RuntimeException("게시글이 없는데 왜 삭제를 ㅠㅠ");
}
boardRepository.deleteById(id);
} // commit (exception 터지면 rollback)
}
package com.metacoding.blogv1.board;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
// 책임 : DB와 소통하는 친구
@Repository // IoC 컬렉션에 뜬다. (BoardController도 IoC에 뜬다
public class BoardRepository {
private EntityManager em;
// DI -> IoC 순회해서 EntityManager 타입으로 찾아서 전달해준다.
public BoardRepository(EntityManager em) {
System.out.println("BoardRepository new 됨");
this.em = em;
}
public void insert(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(); // insert,update,delete
}
public List<Board> findAll() {
// Board.class 추가 -> board class로 자동매핑
Query query = em.createNativeQuery("select * from board_tb order by id desc", Board.class);
List<Board> boardList = query.getResultList(); // 조회할 때
return boardList;
}
public Board findById(int id) {
Query query = em.createNativeQuery("select * from board_tb where id = ?", Board.class);
query.setParameter(1, id);
try {
Board board = (Board) query.getSingleResult(); // getSingleResult의 return 타입이 Object이므로 다운캐스팅해야됨
return board;
} catch (Exception e) {
return null;
}
}
public void deleteById(int id) {
Query query = em.createNativeQuery("delete from board_tb where id = ?"); // 조회하는 게 아니므로 매핑 필요 없음
query.setParameter(1, id);
query.executeUpdate();
}
}
data.sql
insert into board_tb(title, content, created_at)
values ('제목1', '내용1', now());
insert into board_tb(title, content, created_at)
values ('제목2', '내용2', now());
insert into board_tb(title, content, created_at)
values ('제목3', '내용3', now());
insert into board_tb(title, content, created_at)
values ('제목4', '내용4', now());
insert into board_tb(title, content, created_at)
values ('제목5', '내용5', now());detail.mustache
{{> layout/header}}
<section>
<a href="/board/{{model.id}}/update-form">수정화면으로 가기</a>
<form action="/board/{{model.id}}/delete" method="post">
<button type="submit">삭제</button>
</form>
<div>
번호 : {{model.id}} <br>
제목 : {{model.title}} <br>
내용 : {{model.content}} <br>
작성일 : {{model.createdAt}} <br>
</div>
</section>
</body>
</html>list.mustache
{{> layout/header}}
<section>
<table border="1">
<tr>
<th>번호</th>
<th>제목</th>
<th></th>
</tr>
{{#models}}
<tr>
<td>{{id}}</td>
<td>{{title}}</td>
<td><a href="/board/{{id}}">상세보기</a></td>
</tr>
{{/models}}
</table>
</section>
</body>
</html>save-form.mustache
{{> layout/header}}
<section>
<!-- http body : title=제목6&content=내용6
http header : application/x-www-form-urlencoded
key값은 input태그의 name, value값은 input태그에 사용자가 입력하는 값-->
<form action="/board/save" method="post" enctype="application/x-www-form-urlencoded">
<input type="text" name="title" placeholder="제목"><br>
<input type="text" name="content" placeholder="내용"><br>
<button type="submit">글쓰기</button>
</form>
</section>
</body>
</html>update-form.mustache
{{> layout/header}}
<section>
<form action="/board/1/update" method="post" enctype="application/x-www-form-urlencoded">
<input type="text" name="title" value="제목1"><br>
<input type="text" name="content" value="내용1"><br>
<button type="submit">글수정</button>
</form>
</section>
</body>
</html>Share article