2023.04.26 64일차 Project
64일차
간단한 프로젝트만들기
1. 기본 준비
일단 테이블에 더미데이터를 넣어주자.
INSERT INTO Board (title, body, writer) VALUES ('샘플 제목', '샘플 본문', 'user00');
우리가 아는것
Clinet -> Controller -> Mapper
javaBeans -> Model -> View(jsp)
컨트롤러 매퍼 자바빈 뷰를 만들어야한다.
컨트롤러 패키지, domain 자바빈 패키지(DTO), 매퍼패키지, 뷰 폴더 필요하다.
1.1 컨트롤러
이제 의미있는 이름을 붙여줘야한다.
게시물관련이니 BoardController로 만들자.
@Controller를 붙이는 것만으로 스프링이 객체를 만들어서 관리해준다.
목록을 보여주는 메소드를 만들것이다.
경로 : http://localhost:8080 / http://localhost:8080/list 어디로 오든 해준다.
@RequestMapping어노테이션에 배열로 넣어주면 여러 경로로 받을 수 있다.
@Controller
@RequestMapping("/")
public class BoardController {
@Autowired
private BoardMapper mapper;
//게시물 목록
@GetMapping({"/", "list"})
public String list(Model model) {
List<Board> list = mapper.selectAll();
model.addAttribute("boardList", list);
return "list";
}
}
1.2 Board DTO
테이블과 같은 이름으로 만들어주고 getter setter추가해주기
@Data
public class Board {
private Integer id;
private String title;
private String body;
private LocalDateTime inserted;
private String writer;
}
1.3 Mapper
좀더 명확한 기능을 하도록 이름을 지어주고 컨트롤러에는 @Autowired어노테이션으로 Injection해준다.
추상메소드를 만들어 놓는다.
게시글 목록은 최신글부터 받아와야하니 id를 기준으로 내려쓰기 해주자.
@Mapper
public interface BoardMapper {
@Select("SELECT id, title, writer, inserted FROM Board ORDER BY id DESC")
public List<Board> selectAll();
}
1.4 view
특정 view로 이동하도록 application.properties에 view resolver를 설정해줘야한다.
spring.mvc.view.prefix= /WEB-INF/views/
spring.mvc.view.suffix= .jsp
jsp를 위해 필요한 라이브러리들을 다운받아줘야한다.
<!-- jsp 설정 -->
<!-- https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-jasper -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/jakarta.servlet.jsp.jstl/jakarta.servlet.jsp.jstl-api -->
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.glassfish.web/jakarta.servlet.jsp.jstl -->
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
</dependency>
view는 테이블을 만들어서 값을 받아주면된다.
날짜의경우 LocalDateTime타입인데 date타입으로 바꾸어서 포맷하기 위해 parseDate를해줘야한다.
<div class="container-lg">
<h1>게시물 목록 보기</h1>
<!-- table.table>thead>tr>th*4^^tbody -->
<table class="table">
<thead>
<tr>
<th>글번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
<c:forEach items="${boardList}" var="board">
<tr>
<td>${board.id}</td>
<td>${board.title}</td>
<td>${board.writer}</td>
<td>
<fmt:parseDate value="${board.inserted}" pattern="yyyy-MM-dd'T'HH:mm" var="parsedDateTime" type="both" />
<fmt:formatDate value="${parsedDateTime}" pattern="yyyy/MM/dd"/>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
Mapper로 DB에서 값을 가져온 후
컨트롤러에서 boardList라는 이름으로
모델에 담아 view에 전달했다.
2. 프로젝트 구조
우리프로젝트는 크게 두개의 계층으로 나누어진다.
controller model view가 일하는 부분
mapper가 일하는 부분
더 큰 프로젝트는 세개의 계층으로 나누어진다.
3 tier architecture
presentation Tier(layer) / Business Tier(layer) / Data(persistance) Tier(layer)
1.presentation Tier(layer)
controller model view가 일하는 부분이다.
2.Business(service) Tier(layer)
Service라는 이름을 가지고 있다.
실제 서비스 하는 메소드 등을 가지고 있다.
3.Data(persistance) Tier(layer)
DB와 관련한 일을 작업하는 층이다.
DAO 인터페이스 DAO직접 구현한 것으로 나눈다.
마이바티스는 인터페이스와 구현 모두 해줘서 Mapper 인터페이스 하나만 만들면된다.
이 세개 층이 서로 서로 일을 시키고 데이터를 주고 받는다.
사이사이에 데이터를 주고받을때 쓰는게 JavaBean(DTO)이다.
3. Service layer
서비스 레이어를 추가할 건데 우리한테 그렇게 필요하다고 생각되지 않을 것이다.
그러나 언젠간 큰 프로젝트에서 사용하기 때문에 간단하게 추가할 것이다.
각 계층은 원래 인터페이스 - 구현객체로 만들지만 간단하게 만들 것이다.
서비스도 @Component를 붙이면 알아서 Spring Bean이 되는데
더 특화된 @Service를 붙였다.
의미가 있도록 BoardService로 만들어주자
실무에서 인터페이스를 만들고 꽃듯이 만들어 보았다.
서비스가 일하는 것은 매퍼에게 일을 시키는 것이다.
public interface BoardService {
public List<Board> listBoard();
public Board getBoard(Integer id);
public boolean update(Board board);
public boolean remove(Integer id);
public boolean create(Board board);
}
@Service
public class BoardServiceImpl implements BoardService{
@Autowired
private BoardMapper mapper;
public List<Board> listBoard(){
List<Board> list = mapper.selectAll();
return list;
}
}
컨트롤러는 매퍼에게 일을 바로 시키는게 아니라 서비스에게 일을 시키게 되는 것이다.
@GetMapping({"/", "list"})
public String list(Model model) {
List<Board> list = boardService.listBoard();
model.addAttribute("boardList", list);
return "list";
}
필요없는 것을 수정할때 controller javabean service mapper view를 수정해야한다.
이 5가지를 다 만들어야 원하는 기능이 작동하게 된다.
4. detail
제목을 누르면 detail로 가게 해준다.
파라미터로 전달해도되고 이번에는 @PathVariable를 사용해보자.
<td>
<a href="/id/${board.id}">${board.title}</a>
</td>
4.1 detail controller
강사님은 board메소드 get.jsp로포워드 나는 detail페이지이니 detail로 작성했다.
@GetMapping("/id/{id}")
public String detail(@PathVariable("id") Integer id, Model model) {
// 1. request param 수집가공
// 2. business logic 처리
Board board = boardService.getBoard();
// 3. add attribute
model.addAttribute("board", board);
// 4. fowrard/redirect
return "detail";
}
4.2 서비스
id를 받고 게시글 하나를 받아오는 메소드 작성
public Board getBoard(Integer id) {
return mapper.selectById(id);
}
4.3 BoardMapper
id로 받아온 값을 SELECT문에 넣어주면된다.
@Select("SELECT * FROM Board WHERE id = #{id}")
Board selectById(Integer id);
4.4 view
<div class="container-lg">
<h1>${board.id}번 게시물 보기</h1>
<div>
제목 : ${board.title}
</div>
<div>
본문 : <div>${board.body}</div>
</div>
<div>
작성자 : ${board.writer}
</div>
<div>
작성일시 : ${board.inserted}
</div>
</div>
5. update
수정은 조회후 수정을 해야한다. 수정폼화면으로 먼저 이동해야한다.
bootstrap을이용해서 링크를 버튼모양으로 바꿔주는 기능을 이용하자.
<div>
<a class="btn btn-secondary" href="/update/${board.id}">수정하기</a>
</div>
5.1 view
읽어온 데이터를 기반으로 수정폼을 만들어준다.
액션태그를 적지않으면 같은 경로로 가게하고 대신 post요청을 받게 하면된다.
<div class="container-lg">
<h1>${board.id} 게시물 수정</h1>
<form method="post">
<input type="hidden" name="id" value="${board.id }" />
<div>
제목 : <input type="text" name="title" value="${board.title}"/>
</div>
<div>
본문 : <textarea name="body">${board.body }</textarea>
</div>
<div>
작성자 : <input type="text" name="title" value="${board.writer}"/>
</div>
<div>
작성일시 : <input type="text" name="title" value="${board.inserted}" readonly/>
</div>
<input type="submit" value="수정" />
</form>
</div>
5.2 컨트롤러 - 2
먼저 컨트롤러에서는 update 폼으로 가지게 하면된다.
링크에서 받아온 id를 기준으로 데이터를 불러와서 수정폼으로 가지게 해준다.
@GetMapping("/update/{id}")
public String update(@PathVariable("id") Integer id, Model model) {
model.addAttribute("board", boardService.getBoard(id));
return "update";
}
이후 post로 온 데이터를 처리하기 위해 @PostMapping어노테이션으로 메소드를 만들면된다.
//@RequestMapping(value = "/update/{id}", method = RequestMethod.POST)
@PostMapping("/update/{id}")
public String updateProcess(Board board) {
boardService.update(board);
return "null";
}
5.4 service
public boolean update(Board board) {
int cnt = mapper.update(board);
return cnt == 1;
}
5.5 mapper
@Update("UPDATE Board SET title = #{title}, body = #{body}, writer=#{writer} "
+ "WHERE id = #{id}")
int update(Board board);
서비스의 메소드가 mapper에게 일을해서 mapper가 변경된 것을 리턴하게 한다.
만약 1개라면 잘된것이다.
변경후에는 다시 리스트나 게시물 보기로 가게 해야한다.
원하는 것을 선택하면된다.
"redirect/list"; or "redirect/id/" + board.getId();
//@RequestMapping(value = "/update/{id}", method = RequestMethod.POST)
@PostMapping("/update/{id}")
public String updateProcess(Board board) {
boolean ok = boardService.update(board);
if (ok) {
//해당게시물 보기로 리디렉션
return "redirect:/id/" + board.getId();
} else {
//수정폼으로 리디렉션
return "redirect:/update/" + board.getId();
}
}
redirect일때 RedirectAttributes객체를 통해서 값을 전달할 수 있엇다.
이 객체의 addAttribute메소드를 사용하면 쿼리스트링에 붙어서가고
addFlashAttribute은 모델에 붙여서 리다이렉트한다.
파라미터에 성공했다는 값을 받는다면
js로 게시물이 수정되었다는 update페이지에서 alert를 띄워주자.
<c:if test="${not empty param.success}" >
<script>
alert("게시물이 수정되었습니다.")
</script>
</c:if>
만약 실패라면 돌아간 페이지에서 실패햇다고 알려주면된다.
<c:if test="${not empty param.fail}" >
<script>
alert("게시물이 수정되지 않았습니다.")
</script>
</c:if>
파라미터로 가면 새로고침하면 계속 같은 파라미터가 잇기때문에 계속 alert이 신경쓰인다.
addFlashAttribute로 값을 보내주었다.
@PostMapping("/update/{id}")
public String updateProcess(Board board, RedirectAttributes rttr) {
boolean ok = boardService.update(board);
if (ok) {
//해당게시물 보기로 리디렉션
rttr.addFlashAttribute("success", "success");
return "redirect:/id/" + board.getId();
} else {
//수정폼으로 리디렉션
rttr.addFlashAttribute("fail", "fail");
return "redirect:/update/" + board.getId();
}
}
6. delete
6.1 view
여러가지 방법이 있다. 위에처럼 delete링크로 직접이동해도된다.
여기선 버튼을 누르면 숨겨진 removeForm으로 submit이 되게할 수 있다.
form바깥에 submit버튼이 있다면 form에 id를 주고 button에 form을주면된다.
<button class="btn btn-danger" form="removeForm" type="submit">삭제하기</button>
<div class="d-none">
<form action="/remove" method="post" id="removeForm">
<input type="text" name="id" value="${board.id }" />
</form>
</div>
6.2 BoardService
remove메소드를 작성해주면된다.
public boolean remove(Integer id) {
int cnt = mapper.deleteById(id);
return cnt == 1;
}
6.3 BoardMapper
DELTE문을 매핑해주면된다.
@Delete("DELETE FROM Board WHERE id = #{id}")
int deleteById(Integer id);
6.4 Controller
성공하면 list로 실패하면 다시 그 페이지로 가게 해준다.
@PostMapping("remove")
public String update(Integer id) {
boolean ok = boardService.remove(id);
if (ok) {
return "redirect:/list";
} else {
return "redirect:/id/" + id;
}
}
}
Service에서 boolean값으로 받는 이유는
컨트롤러에서 성공햇을때와 아닐때의 처리를 쉽게 하기 위해
boolean값으로 받아온다.
int값으로 받아와도 if구분을 해서 할 수 있다.
6.5 alert
바로 삭제가 되지 않도록 확인창을 띄우고 확인을 누르면 삭제를 하게 하자.
<script>
$("#removeButton").click(function(e) {
// submit 진행 이벤트 막기
e.preventDefault();
const res = confirm("삭제 하시겠습니까?");
if (res) {
// submit 실행
$("#removeForm").submit();
}
});
</script>
addFlashAttribute시 키 값을 매핑을 해줌으로써
alert내용들도 구분해줄 수 있다.
<c:if test="${success eq 'removeScucess'}" >
<script>
alert("게시물이 삭제되었습니다.")
</script>
</c:if>
7. create
글 삽입하기 혼자 작성하기
insert 컨트롤러 get -> insert form -> insert 컨트롤러 post
-> service -> mapper -> list
7.1 view
insert from과 list에 insert from로 가는 버튼, 게시물쓰는 버튼을 추가해줘야한다
<div class="container-lg">
<h1>게시물 쓰기</h1>
<form method="post">
<div>
제목 : <input type="text" name="title" />
</div>
<div>
내용 : <textarea name="body"></textarea>
</div>
<div>
작성자 : <input type="text" name="writer" />
</div>
</form>
</div>
<div>
<h1>게시물 목록 보기</h1>
<div align="right">
<a class="btn btn-secondary" href="/add">글쓰기</a>
</div>
</div>
7.2 컨트롤러
get방식으로 addForm을 가져오고 post처리로 받은 데이터를 db에 넘겨주어야한다.
post방식으로 받은 것을 board에 매핑될 수 있도록 그냥 넣어준다.
등록처리되었음을 알리기 위해 redirect하면서 model을 보내주었다.
@GetMapping("add")
public String addForm(Model model) {
//게시물 작성 form(view)로 포워드
return "add";
}
@PostMapping("add")
public String addProcess(Board board, RedirectAttributes rttr) {
//새 게시물 db에 추가
boolean ok = boardService.create(board);
if (ok) {
rttr.addFlashAttribute("success", "insertScucess");
return "redirect:/list";
} else {
rttr.addFlashAttribute("fail", "insertFail");
return "redirect:/list";
}
}
7.3 BoardService
서비스는 mapper에게 insert메소드를 작동하게 한다.
public boolean create(Board board) {
int cnt = mapper.insert(board);
return cnt == 1;
}
7.4 BoardMapper
mapper는 넘어온 값을 쿼리에 집어 넣는다.
여러 객체가 오면 board.프로퍼티로 해야하지만 하나뿐이라 그냥 넣어주면된다.
@Insert("INSERT INTO Board (title, body, writer) "
+ "VALUES (#{title}, #{body}, #{writer})")
int insert(Board board);
2023.04.26
오늘의 오류
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
SpringApplication cannot be resolved
at com.example.demo.Prj120230425Application.main(Prj120230425Application.java:10)
https://stackoverflow.com/questions/33301073/spring-boot-application-cant-resolve-the-org-springframework-boot-package
검색해보니 spring-boot-starter-parent에 대한 종속성 문제라고 나온다.
Right button on project -> Maven -> Update Project
then check "Force update of Snapshots/Releases"
프로젝트 업데이트로 해결했다.
return에는 view resolver가 보는 링크로 가지는 것이다. 위 매핑이 있으니 자꾸 헷갈리게 된다. 그곳으로 가는것이 아니라 reutrn의 view resolver로 간다는것을 이해하자