댓글 생성시 해당 댓글이 어떤 게시글의 댓글인지 알 수 있도록
게시글의 번호를 외래키로 지정해줘야한다
- 외래키 지정시 객체 자체 = entity를 통으로 넣는다
외래키 = 다른 엔티티의 객체
- 댓글 N개 ↔ 게시글 1개의 관계가 설정된다
@OneToMany
➡️ 게시글 1개 ↔ 댓글 N개
@JsonBackReference(value = "board1")
@OneToMany(mappedBy = "board")
@OrderBy(value = "no desc") // 정렬하기 no를 기준으로 내림차순
List<BoardReply> reply;
@ManyToOne
➡️ N개의 댓글 ↔ 1개의 게시글
@ToString.Exclude
@ManyToOne
@JoinColumn(name = "brdno")
Board board; // 엔티티 만들때는 객체로 잡고 있지만 DB에는 Board엔티티의 기본키값인 no만 들어감
글제목 누르면 게시글 상세페이지로 이동
<!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>
<a th:href="@{/home.do}">홈으로</a>
<a th:href="@{/board/insert.do}">게시글 등록</a>
<table border="1">
<tr>
<th>글번호</th>
<th>글제목</th>
<th>작성자</th>
<th>글내용</th>
<th>조회수</th>
<th>등록일</th>
<th>버튼</th>
</tr>
<tr th:each="obj, idx : ${list}">
<td th:text="${obj.no}"></td>
<td>
<a th:href="@{/board/selectone.do(no=${obj.no})}" th:text="${obj.title}"></a>
</td>
<td th:text="${obj.writer}"></td>
<td th:text="${obj.content}"></td>
<td th:text="${obj.hit}"></td>
<td th:text="${obj.regdate}"></td>
<td>
<form th:action="@{/board/delete.do}" method="post">
<input type="hidden" name="no" th:value="${obj.no}" />
<input type="submit" value="삭제" />
</form>
<form th:action="@{/board/update.do}" method="get">
<input type="hidden" name="no" th:value="${obj.no}" />
<input type="submit" value="수정" />
</form>
</td>
</tr>
</table>
</body>
</html>
- 게시글 상세페이지 이동시 Param으로 받아온 no로 게시글을 조회한다
- ModelAndView 로 조회한 게시글을 리턴해준다
➡️ ModelAndView 리턴시setViewName
과addObject
를 따로 담아 리턴도 가능하다
@GetMapping(value = "/selectone.do")
public ModelAndView selectOneGET(@RequestParam(name = "no") Long no){
Board board = bRepository.findById(no).orElse(null);
ModelAndView mav = new ModelAndView();
mav.setViewName("board_selectone");
mav.addObject("brd", board);
return mav;
}
댓글 저장시
➡️BoardReply
에서 Borad를 객체로 잡고 있지만,
DB에 저장될때는Board
entity 객체 전체가 저장되는게 아니라
Board
entity의 기본키값인no
만 DB에 저장된다
➡️Board
no가 외래키로 들어갔기 때문에name="board.no"
를 반드시 넘겨줘야 한다
<!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>
<a th:href="@{/home.do}">홈으로</a>
<a th:href="@{/board/select.do}">글목록</a>
<hr />
게시판글쓰기
<br />
<a th:href="@{/board/select.do}" th:text="글목록"></a>
<p th:text="${brd.no}"></p>
<p th:text="${brd.title}"></p>
<p th:text="${brd.content}"></p>
<p th:text="${brd.writer}"></p>
<p th:text="${brd.hit}"></p>
<p th:text="${brd.regdate}"></p>
<hr />
<form th:action="@{/reply/insert.do}" method="post">
<!-- board no가 외래키로 들어갔기 때문에 name="board.no"를 반드시 넘겨줘야 한다 -->
<input type="text" name="board.no" th:value="${brd.no}" readonly /><br />
<input type="text" name="content" placeholder="댓글내용" /><br />
<input type="text" name="writer" placeholder="작성자" /><br />
<input type="submit" placeholder="댓글달기" /><br />
</form>
<hr />
</body>
</html>
댓글 작성페이지로 이동
@GetMapping(value = "/insert.do")
public ModelAndView insertGET(@RequestParam(name = "no") Long no){
Board board = new Board();
board.setNo(no);
BoardReply breply = new BoardReply();
breply.setBoard(board);
return new ModelAndView("reply_insert", "breply", breply);
}
댓글 등록페이지 생성
<!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>
<a th:href="@{/home.do}">홈으로</a>
<a th:href="@{/board.do}">게시판으로</a>
<hr />
댓글쓰기
<hr />
<form th:action="@{/reply/insert.do}" method="post">
<input type="text" th:field="${breply.board.no}" /><br />
<input type="text" th:field="${breply.content}" name="content" placeholder="내용" /><br />
<input type="text" th:field="${breply.writer}" name="writer" placeholder="작성자" /><br />
<br />
<hr />
<input type="submit" value="댓글작성" />
</form>
</body>
</html>
댓글 등록하기
@PostMapping(value="/insert.do")
public String insertPOST(@ModelAttribute BoardReply breply) {
System.out.println("===============breply=================");
System.out.println(breply.toString());
brRepository.save(breply);
return "redirect:/board/selectone.do?no=" + breply.getBoard().getNo();
}
댓글을 출력할 테이블 생성
<table border="1">
<tr th:each="obj, idx : ${brd.reply}">
<td th:text="${obj.no}"></td>
<td th:text="${obj.content}"></td>
<td th:text="${obj.writer}"></td>
<td th:text="${obj.regdate}"></td>
</tr>
</table>
Board entity에는 댓글 목록이 포함되어 있다!
Board entity에 댓글 목록이 포함된것을 확인할 수 있다
@OneToMany(mappedBy = "board") List<BoardReply> reply;
BoardReply entity에서는 Board 엔티티의 no를 필요로 하기 때문에
Board를 포함하고 있다@ManyToOne @JoinColumn(name = "brdno") Board board;
게시글 조회시 Board entity는 이미 댓글의 리스트를 갖고있는 상태이다
➡️ 출력만 해주면 된다
// 게시글 상세페이지로 이동
@GetMapping(value = "/selectone.do")
public ModelAndView selectOneGET(@RequestParam(name = "no") Long no){
Board board = bRepository.findById(no).orElse(null);
// 댓글목록 확인용 출력 => stackoverflow
System.out.println(board.getReply().toString());
ModelAndView mav = new ModelAndView();
mav.setViewName("board_selectone");
// 게시글에 해당하는 댓글 목록 list도 board는 이미 갖고 있다
mav.addObject("brd", board);
return mav;
}
Board
와BoardReply
사이에 1:N 양방향 매핑이 되어있는데
Board
에서List<BoardReply>
를 조회하는 경우
조회된BoardReply
는Board
를 다시 조회하고,
다시 조회된 Board는List<BoardReply>
를 또 다시 조회하면서
Board
와BoardReply
사이의 사이클이 생성되며 무한루프에 빠지게 된다
링크참조
1. 양방향 관계에서 직렬화 방향을 설정해주어 순환참조를 해결할 수 있도록 설계된 annotation을 @JsonManagedReference
또는 @JsonBackReference
를 사용한다!
@JsonManagedReference
➡️ 연관관계의 주인의 반대 entity에 선언한다
직렬화를 정상적으로 수행하도록 한다@JsonBackReference
➡️ 연관관계의 주인 entity에 선언한다
직렬화가 진행되지 않도록 수행한다
해당데이터를 포함시키지 않는 어노테이션 @JsomIgnore
을 사용한다
이 어노테이션을 사용하면 JSON 데이터에 해당 프로퍼티는 null로 들어가게 된다
DTO를 사용
순환참조의 상황이 발생한 주 원인은 양방향 매핑
이기도 하지만, 더 정확하게는 entity를 직접 반환한것이 순환참조 발생의 큰 원인중 하나이다
➡️ entity 자체를 return 하는것이 아니라, DTO객체를 만들어 필요한 데이터만 반환하면 순환참조 관련 문제를 사전에 방지 할 수 있다
@JsonBackReference(value = "board1")
추가
@JsonBackReference(value = "board1")
추가
외래키 확보 후 실행되는 작업들에 대해서 깊게 고민해봐야 한다
게시글 이미지 등록은 게시글의 번호를 확보한 후 진행!
-- 이미지 보관용 테이블
CREATE TABLE BOARDIMAGETBL1(
NO NUMBER CONSTRAINT PK_BOARDIMAGE1_NO PRIMARY KEY,
BRDNO NUMBER CONSTRAINT FK_BOARD1_NO REFERENCES BOARDTBL1(NO),
IMAGENAME VARCHAR2(200),
IMAGESIZE NUMBER,
IMAGETYPE VARCHAR2(30),
IMAGEDATA BLOB,
REGDATE TIMESTAMP DEFAULT CURRENT_DATE
);
CREATE SEQUENCE SEQ_BOARDIMAGETBL1_NO START WITH 1 INCREMENT BY 1 NOCAHCE NOMAXVALUE;
이미지 보관용 테이블 생성
...
@Getter
@Setter
@ToString
@NoArgsConstructor
@Entity
@Table(name="BOARDTBL1")
@SequenceGenerator(name = "SEQ3", sequenceName = "SEQ_BOARDTIMAGEBL1_NO", initialValue = 1, allocationSize = 1)
public class BoardImage {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ3")
private Long no;
// 변수명이 같으니 컬럼명 따로 지정하지 않음
@Column(length = 200) //길이만 지정
String imagename;
Long imagesize;
@Column(length = 30)
String imagetype;
@Lob
byte[] imagedata;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm.ss.SSS")
@CreationTimestamp
// updatable => 수정시에도 날짜 갱신/변경여부
@Column(name = "REGDATE", updatable = false)
private Date regdate = null;
@ToString.Exclude
@JsonBackReference(value = "board2")
@ManyToOne
@JoinColumn(name = "BRDNO")
Board board; // 엔티티 만들때는 객체로 잡고 있지만 DB에는 Board엔티티의 기본키값인 no만 들어감
// 테이블 컬럼과 상관없는 임시변수
// 이미지 url용 임시변수
@Transient
String imageurl;
}
업로드 할 이미지의 용량설정
# 이미지 용량 설정
spring.servlet.multipart.max-file-size:50MB
spring.servlet.multipart.max-request-size:50MB
- 이미지 등록 페이지 접속시
@RequestParam(name = "no") Long no
으로 게시글의 번호를 받아온후
model.addAttribute("no", no);
에 no를 담아 보내준다- 이미지 등록시
@RequestParam(name = "file") MultipartFile file
을 이용해 받아온 file정보 4개는 따로 추가해주고,Repository
를 이용해서 저장save
한다
@Controller
@RequestMapping(value = "/boardimage")
public class BoardImageController {
@Autowired
BoardImageRepository biRepository;
// 이미지 등록하기
// 1번 게시물에 대한 이미지 등록 페이지
// 127.0.0.1:8080/ROOT/boardimage/insert.do?no=1
@GetMapping(value = "/insert.do")
public String insertGET(Model model,
@RequestParam(name = "no") Long no
){
model.addAttribute("no", no);
return "boardimage_insert";
}
// 이미지 등록하기
@PostMapping(value = "/insert.do")
public String insertPOST(
@RequestParam(name = "file") MultipartFile file,
@ModelAttribute BoardImage image) throws IOException{
// System.out.println(file.getOriginalFilename());
// System.out.println(image.toString());
image.setImagename(file.getOriginalFilename());
image.setImagedata(file.getBytes());
image.setImagesize(file.getSize());
image.setImagetype(file.getContentType());
System.out.println("======image.toString()========");
System.out.println(image.getImagename());
System.out.println(image.getImagesize());
System.out.println(image.getImagetype());
System.out.println(image.getBoard());
biRepository.save(image);
// file정보 4개 추가, 저장소를 이용해서 save
return "redirect:/board/select.do";
}
}
<!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>
<a th:href="@{/home.do}">홈으로</a>
<hr />
게시글 이미지등록
<hr />
<!-- 이미지 첨부되었기 때문에 enctype="multipart/form-data"추가 -->
<form th:action="@{/boardimage/insert.do}" method="post"
enctype="multipart/form-data">
<!-- name="board.no" 로 잡아주면 board가 갖고있는 no 에 한번에 들어간다 -->
<input type="text" name="board.no" th:value="${no}" placeholder="제목" /><br />
<input type="file" name="file" /><br />
<br />
<hr />
<input type="submit" value="일괄추가" />
</form>
</body>
</html>
이미지는 출력시 실제 이미지 파일을 가져오려면 용량도 크고, 속도도 느리다
이미지 url을 생성하여 조회된 결과를 출력하면 된다
@OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE)
@OrderBy(value = "no desc") // 정렬하기 no를 기준으로 내림차순
List<BoardImage> image;
게시글 조회시 Board entity는 이미 이미지 리스트를 갖고있는 상태이다
➡️ List<BoardImage>
를 전부 가져오면 byte 배열형태의 실제 이미지 파일을 가져오게 되는데 용량도 크고 로딩되는 시간이 느리다
이미지 url을 생성하여 해당 이미지를 조회해보기!
이미지는 이미지 번호만 알면 이미지 url을 만들수 있기 때문에 모든 정보를 다 쓸 필요가 없다 ➡️ BoardImage enttity의 항목중에서 필요한 일부 컬럼만 가져오기
public interface BoardImageProject {
// Long no;
Long getNo();
// Board board;
Board getBoard();
}
@Repository
public interface BoardImageRepository extends JpaRepository<BoardImage, Long>{
// 엔티티 폴더에서 보드이미지프로젝트 생성
List<BoardImageProject> findByBoard_noOrderByNoAsc(long no);
}
html에서 생성될 이미지 url을 입력시 이미지 데이터 불러오기 위한 메서드 생성
코드1
// 이미지 번호를 전달하면 해당하는 이미지의 URL을 전송
// 아이템 이미지 불러오기
// 127.0.0.1:8080/BOOT/boardimage/image?no=이미지번호
@GetMapping(value = "/image")
public ResponseEntity<byte[]> imageGET(
@RequestParam(name = "no") Long no) throws IOException {
System.out.println(no);
// 아이템 이미지 번호가 존재하는 경우
if (no > 0L) {
BoardImage image = biRepository.findById(no).orElse(null);
// System.out.println(item.toString());
if (image.getImagesize() > 0L) { // 이미지 파일이 존재하는 경우
// 타입설정 png인지 jpg인지 gif인지
HttpHeaders headers = new HttpHeaders();
headers.setContentType(
MediaType.parseMediaType(image.getImagetype()));
// 실제이미지데이터, 타입이포함된 header, status 200
ResponseEntity<byte[]> response = new ResponseEntity<>(
image.getImagedata(), headers, HttpStatus.OK);
return response;
} else { // 이미지 파일이 존재하지 않는경우 = default이미지 설정
InputStream is = resourceLoader.getResource(DEFAULT_IMAGE)
.getInputStream();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_JPEG);
// 실제이미지데이터, 타입이포함된 header, status 200
ResponseEntity<byte[]> response = new ResponseEntity<>(
is.readAllBytes(), headers, HttpStatus.OK);
return response;
}
} else { // 아이템 이미지 번호가 존재하지 않는경우 = default이미지 설정
InputStream is = resourceLoader.getResource(DEFAULT_IMAGE)
.getInputStream();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_JPEG);
// 실제이미지데이터, 타입이포함된 header, status 200
ResponseEntity<byte[]> response = new ResponseEntity<>(
is.readAllBytes(), headers, HttpStatus.OK);
return response;
}
}
코드2
@GetMapping(value = "/image")
public ResponseEntity<byte[]> selectOneImageGET(@RequestParam(name="no") Long no ) throws IOException{
BoardImage image = biRepository.findById(no).orElse(null);
HttpHeaders headers = new HttpHeaders();
ResponseEntity<byte[]> response = null;
if(image.getImagesize() > 0L) { // DB에 파일존재O
headers.setContentType(MediaType.parseMediaType(image.getImagetype()));
response = new ResponseEntity<>(image.getImagedata(), headers, HttpStatus.OK);
}
else {
InputStream stream = resourceLoader.getResource("classpath:/static/image/default_image.png").getInputStream();
headers.setContentType(MediaType.IMAGE_PNG);
response = new ResponseEntity<>(stream.readAllBytes(),
headers, HttpStatus.OK);
}
return response;
}
List<BoardImageProject>
를 게시글 번호로 조회하고ModelAndView
리턴시 조회된List<BoardImageProject>
를 담아준다
// 게시글 상세페이지로 이동
@GetMapping(value = "/selectone.do")
public ModelAndView selectOneGET(@RequestParam(name = "no") Long no) {
Board board = bRepository.findById(no).orElse(null);
// 댓글목록 확인용 출력 => stackoverflow
System.out.println("=======board.getReply======");
System.out.println(board.getReply().toString());
// 이미지 확인용
System.out.println("=======board.getImage======");
System.out.println(board.getImage().toString());
// 페이지네이션 가져오기
PageRequest pageRequest = PageRequest.of(0, 5);
List<BoardReply> reply = brRepository.findByBoard_noOrderByNoAsc(no, pageRequest);
System.out.println("===============reply==================");
System.out.println(reply);
List<BoardImageProject> image = biRepository.findByBoard_noOrderByNoAsc(no);
ModelAndView mav = new ModelAndView();
mav.setViewName("board_selectone");
// 게시글에 해당하는 댓글 목록 list도 board는 이미 갖고 있다
mav.addObject("brd", board);
mav.addObject("reply", reply);
mav.addObject("image", image);
return mav;
}
ModelAndView에서 전달해준 image값으로 이미지 url을 생성해준다
...
<table border="1">
<tr th:each="obj, idx : ${image}">
<!-- 원본 게시물 번호 확인용 -->
<p th:text="${obj.board.no}"></p>
<!-- 127.0.0.1:8080/BOOT/boardimage/image?no=이미지번호 -->
<td><img th:src="@{/boardimage/image(no=${obj.no})}" style="width:50px;"></td>
</tr>
</table>
...
삭제버튼 생성
➡️ 삭제 후 다시 해당게시물 위치에 돌아오려면 실제 게시물 번호 no를 알아야 한다
삭제시 삭제할 이미지 번호 + 게시물 번호를 넘겨줘야 한다
<table border="1">
<tr th:each="obj, idx : ${reply}">
<td th:text="${obj.no}"></td>
<td th:text="${obj.content}"></td>
<td th:text="${obj.writer}"></td>
<td th:text="${obj.regdate}"></td>
<td>
<form th:action="@{/reply/delete.do}" method="post">
<input type="text" th:value="${obj.no}" name="no" />
<input type="text" th:value="${brd.no}" name="board.no"/>
<input type="submit" value="삭제" />
</form>
</td>
</tr>
</table>
// 댓글 삭제하기
@PostMapping(value="/delete.do")
public String deletePOST(@ModelAttribute BoardReply breply){
brRepository.deleteById(breply.getNo());
return "redirect:/board/selectone.do?no="
+ breply.getBoard().getNo();
}