🗝️ 오류발생시 컨트롤러에서 출력sysout
하여 확인하는 습관들이기
board 제목 누르면 다음페이지 내용나오게 구현
public Board selectOneBoard(long no){
➡️entity
로 받아도 된다CrudRepository.findById(Long id) : Optional<Board>
+.orElse
➡️ 해당 no가 없을경우 처리방법
Optional
: 조회할 게시글이 있다면no
에 해당하는 Board 가져오고 없으면 null을 넣음
= if문 처리 기능과 같다.orElse
: 해당 no가 없을경우 처리Optional<T>
: 저장된 값이 존재하면 그 값을 반환하고, 값이 존재하지 않으면 인수로 전달된 값을 반환함.
public Board selectOneBoard( long no ){
try {
// 내가 작성한 코드
// Query query = new Query();
🗝️ 오류발생시 컨트롤러에서 출력`sysout`하여 확인하는 습관들이기
---
## 게시글 상세페이지
> board 제목 누르면 다음페이지 내용나오게 구현
![](https://velog.velcdn.com/images/yeoonnii/post/dedd740c-59c3-4647-a454-efd19c637429/image.png)
### 📁 BoardService.java
> * `public Board selectOneBoard(long no){` ➡️ `entity`로 받아도 된다
* `CrudRepository.findById(Long id) : Optional<Board>`
\+ `.orElse` ➡️ 해당 no가 없을경우 처리방법
>>* `Optional` : 조회할 게시글이 있다면 `no`에 해당하는 Board 가져오고 없으면 null을 넣음
= if문 처리 기능과 같다
>>* `.orElse` : 해당 no가 없을경우 처리
>>* `Optional<T>` : 저장된 값이 존재하면 그 값을 반환하고, 값이 존재하지 않으면 인수로 전달된 값을 반환함.
```java
public Board selectOneBoard( long no ){
try {
// 내가 작성한 코드
// Query query = new Query();
// query.addCriteria(Criteria.where("_id").is(no));
// return mongoTemplate.findOne(query, Board.class);
// Optional방식
Board board = bRepository.findById(no).orElse(null);
return board;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
defaultvalue = 0
➡️ 해당 게시글이 없는경우 목록으로 가게한다(오류처리)
// 상세페이지 1개 조회 @GetMapping(value = "/boardone.do") public String boardOneGET(Model model,
나가기
임시저장수정하기
20220830 [Spring Boot]
// query.addCriteria(Criteria.where("_id").is(no));
// return mongoTemplate.findOne(query, Board.class);
// Optional방식
Board board = bRepository.findById(no).orElse(null);
return board;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
### 📁 BoardController.java
> `defaultvalue = 0`
➡️ 해당 게시글이 없는경우 목록으로 가게한다(오류처리)
```java
// 상세페이지 1개 조회
@GetMapping(value = "/boardone.do")
public String boardOneGET(Model model,
@RequestParam(name = "no", defaultValue = "0") long no) {
if (no == 0) { // 번호가 없는경우, 오류처리
return "redirect:/board/boardlist.do";
}
// 1.서비스에서 가져오기
Board retboard = bService.selectOneBoard(no);
System.out.println(retboard.toString());
// 2.VIEW에서 html로 값 전달
// 화면나오기전에 html로 데이터 보내기 => 목록
model.addAttribute("obj", retboard );
// view표시
return "boardone";
}
받아오는 데이터는
obj
하나니까 반복문 돌리지 않고 바로obj
로 받으면 된다
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>상세페이지</title>
</head>
<body>
<div style="border: 1px double #0000006e; padding: 20px;">
<h3>상세페이지</h3>
<hr />
번호<span th:text="${obj.no}"></span><br />
제목<span th:text="${obj.title}"></span><br />
내용<span th:text="${obj.content}"></span><br />
작성자<span th:text="${obj.writer}"></span><br />
조회수<span th:text="${obj.hit}"></span><br />
작성일<span th:text="${obj.regdate}"></span> <br />
</div>
</body>
</html>
수정은 화면 이동과 같다 ➡️
get
이니 a태그 사용할 수 있다
삭제는post
로 보내기 떄문에 a태그를 사용할 수 없다
- 수정버튼 클릭되면 게시판 수정 전 기존데이터 가져오기
- 수정할 데이터 입력 받기
content
내용이 길기 때문에get
이 아닌post
사용한다- 변경버튼 생성시
<a>
+<button>
태그가 아니라<input>
태그로 바꿔줘야<form>
태그 영향을 받지 않는다 ➡️<a>
+<button>
은 폼태그와 별도로 실행- form내부에서 데이터 입력받을때
th:value="${obj.데이터입력될변수명}"
를 지정하면 해당obj
에 사용자가 입력한 데이터가 들어간다name="no"
는 입력데이터는 아니지만, 변경조건(=변경될 글 번호)이기 때문에name
을 지정해준다
name
➡️ entity 변수명
value
➡️ 실제 입력되는값
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>게시판변경</title>
</head>
<body>
<form th:action="@{/board/boardupdate.do}" method="post">
글번호<input type="text" th:value="${obj.no}" name="no" readonly/><br />
제목<input type="text" th:value="${obj.title}" name="title"/><br />
내용<input type="text" th:value="${obj.content}" name="content"/><br />
작성자<input type="text" th:value="${obj.writer}" name="writer"/><br />
조회수<input type="text" th:value="${obj.hit}" readonly/><br />
작성일<input type="text" th:value="${obj.regdate}" readonly/><br />
<input type="submit" value="변경" />
<a th:href="@{/board/boardlist.do}"><input type="button" value="목록" /></a>
<a th:href="@{/board/boardone.do(no=${obj.no})}"><input type="button" value="상세" /></a>
</form>
</body>
</html>
- 수정할 게시글 가져오기
boardUpdateGET
➡️ 변경버튼 누르고 수정페이지로 이동할때 기존데이터 가져온다- DB 수정하기
boardUpdatePOST
- html에서 href작성하고 해당주소 보면서 Controller 작성하면 편하다
// http://127.0.0.1:8080/ROOT/board/boardupdate.do?no=21
// 수정할 게시글 가져오기
@GetMapping(value = "/boardupdate.do")
public String boardUpdateGET(
Model model,
@RequestParam(name = "no", defaultValue = "0") long no) {
// 0. 오류처리
if (no == 0) { // 번호가 없는경우, 오류처리
return "redirect:/board/boardlist.do";
}
// 서비스에서 가져오기
Board retboard = bService.selectOneBoard(no);
System.out.println(retboard.toString());
// 2.VIEW에서 html로 값 전달
model.addAttribute("obj", retboard);
// view표시
return "boardupdate";
}
// ROOT/board/boardupdate.do?no=10
// 수정하기
@PostMapping(value = "/boardupdate.do")
public String boardUpdatePOST(@ModelAttribute Board board) {
int ret = bService.updateBoard(board);
if(ret == 1){
return "redirect:/board/boardone.do?no=" + board.getNo();
}
return "redirect:/board/boardupdate.do?no=" + board.getNo();
}
DB수정용 Service 생성
➡️ 기존데이터를 불러오고 update하는 항목만 덮어쓴다✔️ 반드시 기존 데이터를 꺼낸 후 수정해야 한다!
기존 데이터를 꺼내지 않은 상태에서 수정하면 불러오지 않은 기존 데이터hit
,regdate
는 빈칸으로 업데이트 될지도 모르기 때문이다
// 수정하기
public int updateBoard (Board board){
try {
// 1. 기본키를 이용하여 기존데이터를 꺼냄
Board board1 = bRepository.findById((board.getNo())).orElse(null);
// 2. board1에 board의 값으로 변경
board1.setTitle(board.getTitle());
board1.setWriter(board.getWriter());
board1.setContent(board.getContent());
// 3. 변경된 board1을 save
if( bRepository.save(board1) != null ){
return 1;
}
return 0;
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
- 게시글 삭제시 만약
get
을 사용한경우 누군가 주소창에 입력만 해도 데이터 삭제가 실행될 수 있으니 사용하면 안된다
➡️<form>
태그 내부에 삭제 코드를 작성한다<div style="display: inline-block;">
➡️ 디자인적 요소<a>
+<button>
⇒<input type="button">
으로 변경해준다
➡️<a>
+<button>
은 폼태그와 별도로 실행된다th:onclick="|javascript:deleteAction()|”
➡️ 클릭시 작성한 자바스크립트를 호출<script>
태그 생성 +type="text/javascript"
설정 ➡️ 삭제 버튼 클릭시 알림창 띄워주는 함수 생성
<div style="display: inline-block;">
<form th:action="@{/board/boarddelete.do}" method="post" id="form">
<input type="hidden" th:value="${obj.no}" name="no" />
<input type="button" value="삭제" th:onclick="|javascript:deleteAction()|">
</form>
</div>
<script type="text/javascript">
const deleteAction = () => {
const ret = confirm('삭제할까요?');
if (ret === true) {
document.getElementById("form").submit(); // id = form 을 찾아 submit
} //취소하면 아무일도 일어나지 않음
}
...
// http://127.0.0.1:8080/ROOT/board/boarddelete.do
// 삭제하기
@PostMapping(value = "/boarddelete.do")
public String boardDeletePost(
@RequestParam(name = "no") long no){
int ret = bService.deleteBoard(no);
if(ret == 1){
return "redirect:/board/boardlist.do";
}
// http://127.0.0.1:8080/ROOT/board/boardone.do?no=24
return "/board/boardone.do?no=" + no;
}
// 삭제하기
public int deleteBoard(long no) {
try {
bRepository.deleteById(no);
return 1;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
이전글/다음글 이동시 바로 한 페이지전으로 이동한다고 생각하면 안된다
해당 페이지보다 작은/큰 페이지중 가장 큰/작은 페이지를 가져와야 한다
- _id가 전송되고 있는 no 보다 더 작아야 함
- 1.에서 찾은 작은수들 중 내림차순 정렬
- 내림차순으로 정렬된 수 중 1개만 가져오기
- _id가 전송되고 있는 no보다 더 커야 함
- 1.에서 찾은 큰수들 중 오름차순 정렬
- 오름차순으로 정렬된 수 중 1개만 가져오기
gte
는 이상gt
는 초과lt
미만lte
이하@Aggregation
➡️ 통계낼때 사용 ,쿼리보다 복잡한 조건을 줄 수 있다$project
사용시 service에서 _id만 꺼내어 쓰지 않아도 된다! 바로 리턴 가능
// 이전글
@Aggregation(pipeline = {
// _id가 전송되고 있는 no 보다 더 작아야 함
"{ '$match' : { '_id' : {$lt : ?0} } }",
// 작은것 중 내림차순 정렬
"{ '$sort' : { '_id' : -1 } }",
// 1개만 가져오기
"{ '$limit' : 1 }"
})
// long no => 현재 글 번호
public Board selectBoardPrev( long no );
// 다음글
@Aggregation(pipeline = {
// _id가 전송되고 있는 no 보다 더 커야 함
"{ '$match' : { '_id' : {$gt : ?0} } }",
// 아예 가져올때 번호만 가져온다
"{ '$project' : { '_id' : 1 } }",
// 큰것 중 오름차순 정렬
"{ '$sort' : { '_id' : 1 } }",
// 1개만 가져오기
"{ '$limit' : 1 }"
})
// long no => 현재 글 번호
public long selectBoardNext( long no );
// 이전글 조회 => 컨트롤러 boardone 에 추가
public long prevBoard(long no) {
try {
// Boardrepository 에서 projection 사용안한경우 아래 코드 사용
Board board = bRepository.selectBoardPrev(no);
return board.getNo(); //가져온 board에서 no를 꺼내주어야 함
} catch (Exception e) {
e.printStackTrace();
return 0L;
}
}
// 다음글 조회 => 컨트롤러 boardone 에 추가
public long nextBoard(long no) {
try {
// Boardrepository 에서 project 사용한경우 selectBoardNext자체를 리턴
return bRepository.selectBoardNext(no);
} catch (Exception e) {
e.printStackTrace();
return 0L;
}
}
// 상세페이지 1개 조회
@GetMapping(value = "/boardone.do")
public String boardOneGET(Model model,
@RequestParam(name = "no", defaultValue = "0") long no) {
...
// 1.1 이전글
long prevNo = bService.prevBoard(no);
// 1.2 다음글
long nextNo = bService.nextBoard(no);
...
model.addAttribute("prev", prevNo); // 이전글 번호 정보
model.addAttribute("next", nextNo); // 다음글 번호 정보
...
return "boardone";
<input type="button">
➡️<form>
내부에 작성된<submit>
에 반응<a>
+<button>
➡️<form>
내부에 있어도 반응하지 않음<a th:if="${prev != 0}" th:href="@{/board/boardone.do(no=${prev})}"> <button>이전글</button> </a>
<div style="display: inline-block;">
<!-- http://127.0.0.1:8080/ROOT/board/boardone.do?no=23 -->
<!-- 이전글이 없는경우 no=0인경우는 반응하면 안된다! =>if문 사용하기 -->
<a th:if="${prev != 0}" th:href="@{/board/boardone.do(no=${prev})}">
<button>이전글</button>
</a>
</div>
<div style="display: inline-block;">
<a th:if="${next != 0}" th:href="@{/board/boardone.do(no=${next})}">
<button>다음글</button>
</a>
</div>
💡 "{ '$projection' : { '_id' : 1 } }"
으로 잘못 작성하여 생긴 오류였다
"{ '$project' : { '_id' : 1 } }"
로 수정해주니 오류없이 작동한다
<table>
생성 반복문 돌려서 출력
➡️ 댓글이 있는경우/없는경우 두가지로 나누어 출력한다
➡️ thymeleaf if list empty 검색하여 if 사용법 참고- 원본게시글 번호를 html
<form>
태그안에hidden
으로 잡아둔다- 사용자가 입력하는건 댓글 내용과 작성자
- 원래는 댓글 등록시 임시비밀번호를 지정하게 하고 삭제시 비밀번호 일치하면 지울 수 있게끔 해야한다
- 원본 게시글 번호는
obj.no
이다
입력되는 데이터를 BoardReply에 맞게name
을 지정해준다
...
<form th:action="@{/board/boardreplyinsert.do}" method="post">
<input type="hidden" th:value="${obj.no}" name="boardno" />
<textarea rows="4" placeholder="답글내용" name="content"></textarea>
<input type="text" placeholder="작성자" name="writer">
<input type="submit" value="답글" />
</form>
<table border="1">
<!-- 댓글이 있는경우 -->
<tr th:if="${not #lists.isEmpty(rlist)}" th:each="tmp, idx : ${rlist}">
<td th:text="${idx.count}"></td>
<!-- <td th:test="${tmp.no}"></td> //삭제시에는 no가 필요 -->
<td th:text="${tmp.content}"></td>
<td th:text="${tmp.writer}"></td>
<td th:text="${tmp.regdate}"></td>
<td><button th:onclick="|javascript:deleteAction('${tmp.no}')|">삭제</button></td>
</tr>
<!-- 댓글이 없는경우 -->
<tr th:if="${#lists.isEmpty(rlist)}">
<td colspan="5"> 댓글이 없습니다</td>
</tr>
</table>
...
// form 만들어도 된다. script에서도 폼 생성 가능
const deleteReplyAction = (no) => {
if (confirm('댓글을 삭제할까요?')) {
// 스크립트가 <form> 생성
const form = document.createElement("form");
// <form action="aaaaa">
form.setAttribute("action", "")
// 메소드 생성<form action="aaaaa" method="post">
form.setAttribute("method", "post");
document.body.appendChild(form);
form.submit();
}
- 원본게시글이 있어야 하위에 댓글 작성 가능함
➡️ entity / BoardReply.java 에외래키
로 원본게시글 번호가 있어야한다- 댓글과 게시판 기능은 컨트롤러, 서비스는 같은파일에 코드 작성할 수는 있지만
Repository
(저장소)는 같은 파일에 쓸 수 없다entity
생성되면Repository
는 따로 생성해야 한다.
@Getter
@Setter
@ToString
@NoArgsConstructor
@Document(collection = "boot_board_reply") // DB컬렉션 생성
public class BoardReply {
@Id
private long no; // 답글번호(시퀀스), 기본키
private long boardno = 0L; //원본 게시글 번호, 외래키(게시물 존재하는 것)
private String content; // 답글내용
private String writer; // 답글작성자
private Date regdate;
}
상세페이지 1개가 조회되는 시점에 댓글 목록도 같이 호출되어 있어야 한다
// 상세페이지 1개 조회
@GetMapping(value = "/boardone.do")
public String boardOneGET(Model model,
@RequestParam(name = "no", defaultValue = "0") long no) {
...
//1.3 댓글 목록
List<BoardReply> rlist = bService.selectBoardReplyList(no);
// VIEW에서 html로 값 전달
model.addAttribute("rlist", rlist); // 답글목록
return "boardone";
...
// 댓글 등록하기
public int insertBoardReply(BoardReply boardReply){
try {
// 시퀀스 사용하기
long seq = cService.generateCounter("SEQ_BOARD_REPLY_NO");
// 기본키 no 와 regdate안만들어진 상태
boardReply.setNo(seq);
boardReply.setRegdate(new Date());
// 저장소에 저장
if( brRepository.save(boardReply) != null){
return 1;
}
return 0;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// 해당 게시글에 등록된 댓글 목록 가져오기
// 게시판 원본 글 번호
public List<BoardReply> selectBoardReplyList( long boardno ){
try {
// 댓글을 _id기준으로 내림차순 정렬
Sort sort = Sort.by(Direction.DESC, "_id");
return brRepository.findByBoardno(boardno, sort);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
...
- 기본적인 CRUD(추가, 수정, 삭제, 조회) 기능이 포함되어 있다
boardno
에 해당하는 댓글들을 list로 가져오기Query
말고findby변수명
사용
➡️ 쿼리는 db에 맞춰져있어서 db변경되면 같이 변경해야함 되도록이면 쿼리를 안쓰는게 좋다
import com.example.entity.BoardReply;
@Repository
public interface BoardReplyRepository extends MongoRepository<BoardReply, Long>{
public List<BoardReply> findByBoardno(long boardno, Sort sort);
}
별도의 저장소 필요없음!
save
사용하여 변경사항 바로 저장
일종의 수정과 같다!
1. 게시물 조회 하기
2. 조회한 게시물의 기존 조회수hit
를 꺼내기
3.hit
+ 1증가
// 게시물 클릭시 조회수 1 증가
public int boardUpdateHit( long no ){
try {
// 1. 게시물 꺼내기
Board board = bRepository.findById(no).orElse(null);
// 기존조회수 꺼내기 + 1
board.setHit( board.getHit() + 1L );
// 2. 저장하기
if(bRepository.save(board) != null){
return 1;
}
return 0;
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
게시글 클릭하여 조회된 게시글을 확인하는 시점에
해당 개시글은 조회수가 이미 증가된 상태여야 하니
조회수 증가 코드는 상세페이지 조회 상단에 위치해야 한다
- 📢 해결해야할 문제점
해당 글을 새로고침하는 경우 조회수 계속 증가한다
이 문제는 나중에 세션으로 처리하기!
// 상세페이지 1개 조회
@GetMapping(value = "/boardone.do")
public String boardOneGET(Model model,
@RequestParam(name = "no", defaultValue = "0") long no) {
if (no == 0) { // 번호가 없는경우, 오류처리
return "redirect:/board/boardlist.do";
}
// 0. 조회수 증가
// 나중에 세션으로 처리하기
bService.boardUpdateHit(no);
...
- 아이디 중복확인용 저장소 생성
- 해당아이디의 존재여부 확인하는
@Query
를 작성 ➡️findbyId
도 이용가능- MongoRepository <Member, string>
➡️ MongoRepository <entity, entity id의 타입>
@Repository
public interface MemberRepository extends MongoRepository<Member, String>{
// 아이디 중복확인용
// 해당아이디의 존재여부 확인
@Query(value="{_id : ?0}", exists = true)
public boolean selectIdCheck(String id);
}
- 아이디 중복확인시 일치하는 아이디 1개 조회니까 string으로 보낸다
➡️ 조회할 항목이 2개 이상 이었다면 entity를 보내는데,
조회할 항목이 1개인 경우 entity로 보내면 다시 꺼내어 써야하니 더 번거롭다selectIdCheck
또는findById
사용하여 조회if(mRepository.findById(id).orElse(null) != null){
public int selectIdCheck( String id ){
try {
// if(mRepository.findById(id).orElse(null) != null){
if(mRepository.selectIdCheck(id)){
return 1;
}
return 0;
} catch (Exception e) {
e.printStackTrace();;
return -1;
}
}
주소가 변경되면
idcheck
수행하고 return
➡️ idcheck 수행되는 시점은 중복확인버튼 클릭시이며, 이때 새로운 주소로 이동하여 결과 알려주는 방식으로 진행
* return 되는idcheck
는idcheck.html
을 의미한다
// 아이디 중복확인
// 127.0.0.1:8080/ROOT/member/idcheck.do?id=aaa
@GetMapping(value = "/idcheck.do")
public String idCheckGET(
Model model,
@RequestParam(name="id", defaultValue = "") String id){
int ret = mService.selectIdCheck(id);
model.addAttribute("ret", "사용가능");
if( ret == 1 ){
model.addAttribute("ret", "사용불가");
}
return "idcheck";
}
<a>
는 입력되어 받아오는 값이 아닌 정해진 값을 사용한다.
사용자 에게서 입력받을 id값을 넣어야 하는데 입력받을 값을 모르기 때문에 스크립트를 사용하여 사용자가 입력한 id를 받아와야 한다- join.html에서 중복확인 버튼 클릭시 MemberController.java에서
ret
+사용가능/사용불가
를 결과 값으로 받으며 , return시idcheck.html
로 이동한다... <form th:action="@{/member/join.do}" method="post"> <label style="display: inline-block; width: 100px;"> 아이디 </label><input type="text" name="id"/> <input type="button" value="중복확인" th:onclick="|javascript:idcheckAction()|" /> <br /> ... <script type="text/javascript"> const idcheckAction = () => { // 1. 사용자가 입력한 아이디 받아오기 const id = document.getElementsByName("id")[0]; // getElementsByName => 복수형이라 배열로 받는다. console.log(id.value); // 2.get으로 전송 // 스크립트가 <form> 생성 location.href = "/ROOT/member/idcheck.do?id=" + id.value; } </script> ...
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>아이디중복확인</title>
</head>
<body>
<div th:text="${ret}"></div>
</body>
</html>
💡 idcheck.html
을 생성하지 않아 발생한 오류였다
idcheck.html
을 생성 후 실행하니 오류없이 작동한다