20221005 [Spring Boot, JPA]

Yeoonnii·2022년 10월 8일
0

TIL

목록 보기
41/52
post-thumbnail

게시글 상세페이지 댓글등록

댓글 생성시 해당 댓글이 어떤 게시글의 댓글인지 알 수 있도록
게시글의 번호를 외래키로 지정해줘야한다

  • 외래키 지정시 객체 자체 = entity를 통으로 넣는다
    외래키 = 다른 엔티티의 객체
  • 댓글 N개 ↔ 게시글 1개의 관계가 설정된다

entity/Board.java

@OneToMany ➡️ 게시글 1개 ↔ 댓글 N개

    @JsonBackReference(value = "board1")
    @OneToMany(mappedBy = "board") 
    @OrderBy(value = "no desc") // 정렬하기 no를 기준으로 내림차순
    List<BoardReply> reply;

entity/BoardReply.java

@ManyToOne ➡️ N개의 댓글 ↔ 1개의 게시글

    @ToString.Exclude
    @ManyToOne
    @JoinColumn(name = "brdno")
    Board board; // 엔티티 만들때는 객체로 잡고 있지만 DB에는 Board엔티티의 기본키값인 no만 들어감

board_select.html

글제목 누르면 게시글 상세페이지로 이동

<!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>

BoardController.java

  • 게시글 상세페이지 이동시 Param으로 받아온 no로 게시글을 조회한다
  • ModelAndView 로 조회한 게시글을 리턴해준다
    ➡️ ModelAndView 리턴시 setViewNameaddObject를 따로 담아 리턴도 가능하다
@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;
    }

board_selectone.html

댓글 저장시
➡️ 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>

BoardReplyController.java

댓글 작성페이지로 이동

    @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);
    }

reply_insert.html

댓글 등록페이지 생성

<!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>

BoardReplyController.java

댓글 등록하기

    @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();
    }

게시글 상세페이지에서 댓글 조회

board_selectone.html

댓글을 출력할 테이블 생성


    <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>

BoardController.java

Board entity에는 댓글 목록이 포함되어 있다!

entity/Board.java

Board entity에 댓글 목록이 포함된것을 확인할 수 있다

@OneToMany(mappedBy = "board")
    List<BoardReply> reply;

entity/BoardReply.java

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;
    }

🤯 stackoverflow Error

BoardBoardReply사이에 1:N 양방향 매핑이 되어있는데
Board에서 List<BoardReply>를 조회하는 경우
조회된 BoardReplyBoard를 다시 조회하고,
다시 조회된 Board는 List<BoardReply>를 또 다시 조회하면서
BoardBoardReply사이의 사이클이 생성되며 무한루프에 빠지게 된다

💡 해결방법

링크참조
1. 양방향 관계에서 직렬화 방향을 설정해주어 순환참조를 해결할 수 있도록 설계된 annotation을 @JsonManagedReference 또는 @JsonBackReference를 사용한다!

  • @JsonManagedReference
    ➡️ 연관관계의 주인의 반대 entity에 선언한다
    직렬화를 정상적으로 수행하도록 한다
  • @JsonBackReference
    ➡️ 연관관계의 주인 entity에 선언한다
    직렬화가 진행되지 않도록 수행한다
  1. 해당데이터를 포함시키지 않는 어노테이션 @JsomIgnore 을 사용한다
    이 어노테이션을 사용하면 JSON 데이터에 해당 프로퍼티는 null로 들어가게 된다

  2. DTO를 사용
    순환참조의 상황이 발생한 주 원인은 양방향 매핑이기도 하지만, 더 정확하게는 entity를 직접 반환한것이 순환참조 발생의 큰 원인중 하나이다
    ➡️ entity 자체를 return 하는것이 아니라, DTO객체를 만들어 필요한 데이터만 반환하면 순환참조 관련 문제를 사전에 방지 할 수 있다

entity/BoardReply.java

@JsonBackReference(value = "board1") 추가

entity/Board.java

@JsonBackReference(value = "board1") 추가


게시글 이미지 등록

외래키 확보 후 실행되는 작업들에 대해서 깊게 고민해봐야 한다

게시글 이미지 등록은 게시글의 번호를 확보한 후 진행!

이미지 보관용 테이블 생성(SQL)

-- 이미지 보관용 테이블
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;

entity / BoardImage.java

이미지 보관용 테이블 생성

...
@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;

}

application.properties

업로드 할 이미지의 용량설정

# 이미지 용량 설정
spring.servlet.multipart.max-file-size:50MB
spring.servlet.multipart.max-request-size:50MB

BoardImageController.java

  • 이미지 등록 페이지 접속시
    @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";
        }
}

boardimage_insert.html

<!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을 생성하여 조회된 결과를 출력하면 된다

entity/Board.java

	@OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE) 
    @OrderBy(value = "no desc") // 정렬하기 no를 기준으로 내림차순
    List<BoardImage> image;

게시글 조회시 Board entity는 이미 이미지 리스트를 갖고있는 상태이다
➡️ List<BoardImage>를 전부 가져오면 byte 배열형태의 실제 이미지 파일을 가져오게 되는데 용량도 크고 로딩되는 시간이 느리다

이미지 url을 생성하여 해당 이미지를 조회해보기!

entity/BoardImageProject.java

이미지는 이미지 번호만 알면 이미지 url을 만들수 있기 때문에 모든 정보를 다 쓸 필요가 없다 ➡️ BoardImage enttity의 항목중에서 필요한 일부 컬럼만 가져오기

public interface BoardImageProject {
    
    //  Long no;
    Long getNo();

    //  Board board;
    Board getBoard();
}

BoardImageRepository.java

@Repository
public interface BoardImageRepository extends JpaRepository<BoardImage, Long>{
    
    // 엔티티 폴더에서 보드이미지프로젝트 생성
    List<BoardImageProject> findByBoard_noOrderByNoAsc(long no);
}

BoardImageController.java

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;
    }

BoardController.java

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;
    }

board_selectone.html

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>
...

게시글 이미지 삭제

board_selectone.html

삭제버튼 생성
➡️ 삭제 후 다시 해당게시물 위치에 돌아오려면 실제 게시물 번호 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>

BoardReplyController.java

    // 댓글 삭제하기
    @PostMapping(value="/delete.do")
    public String deletePOST(@ModelAttribute BoardReply breply){
        brRepository.deleteById(breply.getNo());
        return "redirect:/board/selectone.do?no="
            + breply.getBoard().getNo();
    }

Board ↔ BoardImage/BoardReply

0개의 댓글