spring boot

47일차 수업 빼먹은 날(웹보드 보며 혼자 보충)

비루블 2022. 8. 31. 02:56

웹보드 스크립트 확인하며 혼자 공부후 블로그 작성

 

오늘은 무엇을 했냐면,

boardone(이전다음글,  답글기능, 삭제, 조회수 증가(세션x)), boarupdate,  member join(idcheck)

 

0. boardone 설명

boardone.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>
    
    <p th:text="${obj.no}"></p>    
    <p th:text="${obj.title}"></p>    
    <p th:text="${obj.content}"></p>    
    <p th:text="${obj.writer}"></p>    
    <p th:text="${obj.hit}"></p>    
    <p th:text="${obj.regdate}"></p>

    <div style="display: inline-block;">
        <a th:href="@{/board/boardlist.do}"><button>목록</button></a>
    </div>

    <div style="display: inline-block;">
        <a th:href="@{/board/boardupdate.do(no=${obj.no})}"><button>수정</button></a>
    </div>

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

    <div style="display: inline-block;">
        <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>

    <hr />
    <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:text="${tmp.content}"></td>
            <td th:text="${tmp.writer}"></td>
            <td th:text="${tmp.regdate}"></td>
            <td><button th:onclick="|javascript:deleteReplyAction('${tmp.no}', '${obj.no}')|">삭제</button></td>
        </tr>
        <tr th:if="${#lists.isEmpty(rlist)}">
            <td colspan="5">답글이 없습니다.</td>
        </tr>
    </table>

    <script type="text/javascript">
        const deleteAction = () => {
            const ret = confirm('삭제할까요?');
            if(ret === true){
                document.getElementById("form").submit();
            }
        }

        const deleteReplyAction = ( no, boardno ) => {
            alert(no);
            alert(boardno);
            if( confirm('댓글 삭제할까요?') ){
                const form = document.createElement("form");
                form.setAttribute("action", "/ROOT/board/deleteboardreply.do");

                const input1 = document.createElement("input");
                input1.setAttribute("type", "text");
                input1.setAttribute("name", "no");
                input1.setAttribute("value", no);

                const input2 = document.createElement("input");
                input2.setAttribute("type", "text");
                input2.setAttribute("name", "boardno");
                input2.setAttribute("value", boardno);

                form.appendChild(input1);
                form.appendChild(input2);

                form.setAttribute("method", "post");
                document.body.appendChild(form);
                form.submit();
            }
        }
    </script>

</body>
</html>

p태그로 데이터 표기

목록, 수정,  삭제(script) 버튼

이전글, 다음글

답글내용, 작성자 기입란 생성(BoardReplyRepository사용할 것임.)

달급내용, 작성자 바로 보이게끔 table 생성(th:if 처음 썻음 if에서 not문도 처음 씀)

답글 없을때 colspan을 tr에 줘봤음

삭제에만 onclick을 줬음

 

html부분에서 script를 반응시키기 위한 방법

삭제에서 confirm을 씀.

document이용하여 getElementById를 찾음

삭제부분 action을 보면 id를 form으로 부여 해놨음.

confirm true시 제출

 

1. boardone> 이전다음글(원리 역순 설명)

boardone.html

<div style="display: inline-block;">
    <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>

if문 사용 mongodb document사용하여 다음 데이터가 없을 경우 0으로 출력하게끔 만들어 놓음

0이 나오면 버튼이 사라짐

BoardController> selectone

// /ROOT/board/boardone.do?no=14
@GetMapping(value = "/boardone.do")
public String boardOneGET(
    Model model,
    @RequestParam(name = "no", defaultValue = "0") long no ){
    // 0. 오류처리
    if(no == 0) { 
        return "redirect:/board/boardlist.do";
    }

    // 0 조회수증가 (세션으로 처리)
    bService.boardUpdateHit(no);

    // 1. 게시물 1개 정보
    Board board = bService.selectBoardOne(no);
    System.out.println( board.toString() );

    // 1.1 이전글
    long prevNo = bService.prevBoard(no);

    // 1.2 다음글
    long nextNo = bService.nextBoard(no);

    // 1.3 답글목록
    List<BoardReply> rlist = bService.selectBoardReplyList(no);

    // 2. view로 값을 전달
    model.addAttribute("obj",  board);
    model.addAttribute("prev", prevNo);
    model.addAttribute("next", nextNo);
    model.addAttribute("rlist", rlist);

    // 3. view표시
    return "board/boardone";
}

1.1과 1.2만 일단 먼저 보자

// 1.1 이전글
long prevNo = bService.prevBoard(no);

// 1.2 다음글
long nextNo = bService.nextBoard(no);

현재 게시물의 no를 서비스의 prev,next에 넣어줬다.

모델에 넣는건 아니까 생략

서비스로 한번 가보자

 

BoardService

public long prevBoard(long no ){
    try {
        Board board = bRepository.selectBoardPrev(no);
        return board.getNo();
    }
    catch(Exception e){
        e.printStackTrace();
        return 0L;
    }
}

public long nextBoard(long no ){
    try {
        return bRepository.selectBoardNext(no);
    }
    catch(Exception e){
        e.printStackTrace();
        return 0L;
    }
}

Repository의 selectBoardPrev와 selectBoardNext를 사용하고 있다.(return에 적혀진게 다른 것은 아래 코드때문)

BoardRepository

// 이전글
@Aggregation(pipeline = {
    "{ '$match' : { '_id' : { $lt : ?0 } } }",
    "{ '$sort'  : { '_id' : -1 }           }",
    "{ '$limit' : 1                        }"
})
public Board selectBoardPrev(long no);

// 다음글
@Aggregation(pipeline = {
    "{ '$match'   : { '_id' : { $gt : ?0 } } }",
    "{ '$project' : { '_id' :  1 }           }",
    "{ '$sort'    : { '_id' :  1 }           }",
    "{ '$limit'   : 1                        }"
})
public long selectBoardNext(long no);

@Aggregation사용

pipline으로 찾아올 값 설정

이전글 > 이전글은 정렬을 하고, lt(미만)를 사용하여 넣어준 no보다 작은 게시물 번호를 하나 들고오는 것(Board타입)

다음글>  다음글은 정렬을 하고, gt(초과)를 사용하여 넣어준 no보다 큰 게시물 번호를 하나 들고 오는 것(long타입)

 

2. boardone> 답글 기능 탑재 삭제 및 표기 포함

BoardReply.java entity

package com.example.entity;

import java.util.Date;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
@NoArgsConstructor
@Document(collection = "boot_board_reply")
public class BoardReply {
    
    @Id
    private long no; //답글번호(시퀀스), 기본키

    private long boardno = 0L; //원본게시글번호, 외래키(게시물존재하는것만)
    private String content; // 답글내용
    private String writer;  // 답글작성자
    private Date regdate;
}

 

답글적기

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

답글 적기는 하던 그대로 th:action사용(설명 생략 글쓰기, 회원가입이랑 같음)

 

답글내용 불러오기

<table border="1">
    <tr th:if="${not #lists.isEmpty(rlist)}" th:each = "tmp, idx : ${rlist}">
        <td th:text="${idx.count}"></td>
        <td th:text="${tmp.content}"></td>
        <td th:text="${tmp.writer}"></td>
        <td th:text="${tmp.regdate}"></td>
        <td><button th:onclick="|javascript:deleteReplyAction('${tmp.no}', '${obj.no}')|">삭제</button></td>
    </tr>
    <tr th:if="${#lists.isEmpty(rlist)}">
        <td colspan="5">답글이 없습니다.</td>
    </tr>
</table>

역시 그대로 데이터 받아와서 출력해주기(if문 사용하여 데이터가 없을시 "답글이 없습니다" 출력)

lists.isEmpty 사용으로 true or false로 데이터 값 있는지 없는지 확인 가능

답글 삭제 (script 연동 및 script에 데이터 주기)

목적 : 답글 삭제시 게시글 넘버와 답글 넘버가 일치하여야함

<td><button th:onclick="|javascript:deleteReplyAction('${tmp.no}', '${obj.no}')|">삭제</button></td>
const deleteReplyAction = ( no, boardno ) => {
    alert(no);
    alert(boardno);
    if( confirm('댓글 삭제할까요?') ){
        const form = document.createElement("form");
        form.setAttribute("action", "/ROOT/board/deleteboardreply.do");

        const input1 = document.createElement("input");
        input1.setAttribute("type", "text");
        input1.setAttribute("name", "no");
        input1.setAttribute("value", no);

        const input2 = document.createElement("input");
        input2.setAttribute("type", "text");
        input2.setAttribute("name", "boardno");
        input2.setAttribute("value", boardno);

        form.appendChild(input1);
        form.appendChild(input2);

        form.setAttribute("method", "post");
        document.body.appendChild(form);
        form.submit();
    }
}

alert를 이용하여 데이터가 잘오는지 확인

confirm사용으로 삭제확인

form을 script에서 만들어 주기

const form = document.createElement("form");
form.setAttribute("action", "/ROOT/board/deleteboardreply.do");

form에 넣어줄 데이터들 설정해줌

form에 넣어줄 데이터는 답글 번호와 게시글 번호

const input1 = document.createElement("input");
input1.setAttribute("type", "text");
input1.setAttribute("name", "no");
input1.setAttribute("value", no);

const input2 = document.createElement("input");
input2.setAttribute("type", "text");
input2.setAttribute("name", "boardno");
input2.setAttribute("value", boardno);

form에 element 담아주기

form.appendChild(input1);
form.appendChild(input2);

form의 method 셋팅을 post로 설정

form.setAttribute("method", "post");

제출

document.body.appendChild(form);
form.submit();

 

 

3. boardone> 조회수 증가(세션 없어서 새로고침시 계속 올라감)

html 없음

controller에서 boardOneGet할때 불러옴

bService.boardUpdateHit(no);

보드 서비스

// 게시물 번호가 오면 조회수 1증가시키기
public int boardUpdateHit( long no ){
    try {
        // 1. 게시물 꺼내기
        Board board = bRepository.findById(no).orElse(null);
        board.setHit(  board.getHit() + 1L  );

        //2.저장하기
        if(bRepository.save(board) != null){
            return 1;
        }
        return 0;
    }
    catch(Exception e){
        e.printStackTrace();
        return -1;
    }
}

boardrepository를 불러와서 findById사용하여 해당 넘버의 게시물 정보 받아오기

set으로 hit + 1하고 save

 

4. boardupdate

목적 : 해당 넘버의 게시글 데이터 불러와서 미리 input에 기입

input 값 수정시 th:action으로 post(게시글 수정controller)

상세페이지로 다시 돌아갈때 받아온 obj.no 사용하여 돌아감

원리 : 

+ 수정 페이지에서 상세페이지로 넘어가기

+ th:value로 받아온 데이터 넣기

+ readyonly(input에 적어주면 화면에서 데이터 수정 불가능)

+ 게시글 상세페이지로 다시 돌아갈때는 받아온 obj.no로 돌아감

+ controller return처리

수정 완료 및 실패시 다시 해당 넘버의 상세페이지로 이동

게시글 정보 불러올때 실패시 list로 이동,

성공시 model사용하여 obj에 board내용 html로 넣어주고 board/boardupdate로 이동

 

html

readyonly(input에 적어주면 화면에서 데이터 수정 불가능)

다시 상세페이지 돌아갈때는 boardone.do(no=${obj.no}) 사용.

 

boardupdate.do 이용하여 해당 페이지 data값 받음.(no는 boardupate에서 param으로 넘겨줌)

// boardupdate.html
// 다음 페이지로 param 주는 방법
<div style="display: inline-block;">
    <a th:href="@{/board/boardupdate.do(no=${obj.no})}"><button>수정</button></a>
</div>

 

 

controller(update)

// /ROOT/board/boardupdate.do
@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();
}

// /ROOT/board/boardupdate.do?no=10
@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";
    }
    // 1. 서비스 호출
    Board board = bService.selectBoardOne(no);
    System.out.println( board.toString() );
    // 2. view로 값을 전달
    model.addAttribute("obj", board);
    // 3. view표시
    return "board/boardupdate";
}

post

수정 버튼 눌렀을때 반응하는 form method

board model 정보를 받아와서

수정 성공시 다시 상세 게시글 페이지로 돌아와줌. 실패시도 마찬가지

return "redirect:/board/boardupdate.do?no=" + board.getNo();

 

get

param으로 정보를 받음. no정보가 없을시(0) boardlist로 이동

서비스 이용하여 상세 정보 불러오기

model로 담아서 board/boardupdate로 전당

 

controller(상세페이지 돌아가기)

// /ROOT/board/boardone.do?no=14
@GetMapping(value = "/boardone.do")
public String boardOneGET(
    Model model,
    @RequestParam(name = "no", defaultValue = "0") long no ){
    // 0. 오류처리
    if(no == 0) { 
        return "redirect:/board/boardlist.do";
    }

    // 0 조회수증가 (세션으로 처리)
    bService.boardUpdateHit(no);

    // 1. 게시물 1개 정보
    Board board = bService.selectBoardOne(no);
    System.out.println( board.toString() );

    // 1.1 이전글
    long prevNo = bService.prevBoard(no);

    // 1.2 다음글
    long nextNo = bService.nextBoard(no);

    // 1.3 답글목록
    List<BoardReply> rlist = bService.selectBoardReplyList(no);

    // 2. view로 값을 전달
    model.addAttribute("obj",  board);
    model.addAttribute("prev", prevNo);
    model.addAttribute("next", nextNo);
    model.addAttribute("rlist", rlist);

    // 3. view표시
    return "board/boardone";
}

설명 생략(prev, next, rlist는 따로 설명)

 

service

updateBoard(수정)

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

수정이니까 save사용(tmi>insert도 save사용함)

 

repository에서 몽고db함수(findById)이용하여 바로 데이터를 가져와줌.(기본키 이용)

- orElse 사용하여 바로 오류처리

 

html상 board를 받아온 것을 다시 넣어줘야하니까

// 1. 기본키를 이용하여 기존데이터를 꺼냄
Board board1 = bRepository.findById(board.getNo()).orElse(null);

// 2. board1에 board의 값으로 변경
board1.setTitle( board.getTitle() );
board1.setWriter(board.getWriter());
board1.setContent( board.getContent() );

※ 이렇게 하는 이유는 무엇일까 생각해봄

수정 페이지에서 readyonly를 썻지만, 이렇게 하는 이유는 사용자의 악용을 막을 수 있을 듯.

board1이라는 것을 다시 만들고 html상 title, writer, content만 딱 수정하게 할 수 있게끔 해놓은거라 생각

 

selectBoardOne(현재 게시글 상세 정보 불러오기)

public Board selectBoardOne( long no ){
    try {
        // Optional<Board>   .orElse(없을경우처리방법)
        Board board = bRepository.findById(no).orElse(null);
        return board;
    }
    catch(Exception e){
        e.printStackTrace();
        return null;
    }
}

설명 생략

 

BoardRepository.java

package com.example.repository;

import java.util.List;

import org.springframework.data.domain.PageRequest;
import org.springframework.data.mongodb.repository.Aggregation;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;

import com.example.entity.Board;

public interface BoardRepository
    extends MongoRepository<Board, Long> {

    // 검색어, 페이지네이션, 정렬
    @Query( value = "{ title : { $regex : ?0 } }", sort="{_id : -1}")
    public List<Board> selectBoardList(String text, 
                PageRequest pageable);        
    
    // 검색어 포함 개수
    @Query ( value = "{ title : { $regex : ?0 } }", count = true )                
    public long countBoardList(String text);

    // 이전글
    @Aggregation(pipeline = {
        "{ '$match' : { '_id' : { $lt : ?0 } } }",
        "{ '$sort'  : { '_id' : -1 }           }",
        "{ '$limit' : 1                        }"
    })
    public Board selectBoardPrev(long no);

    // 다음글
    @Aggregation(pipeline = {
        "{ '$match'   : { '_id' : { $gt : ?0 } } }",
        "{ '$project' : { '_id' :  1 }           }",
        "{ '$sort'    : { '_id' :  1 }           }",
        "{ '$limit'   : 1                        }"
    })
    public long selectBoardNext(long no);
}

여기서 깨우친게 있음.

repository에 몽고db가 상속되어 있어서, findbyid를 작성하지 않아도 바로 사용할 수 있음.

(사용하려면 아래 코드처럼 만들어 줘야함<Board, Long> -> Board는 entity, Long은 @id의 type)

findbyid는 몽고db document 함수

 

 

5. idcheck(member>join)

idcheck.html

<!DOCTYPE html>
<html lang="en">
<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페이지 이동시 ret으로 중복확인 값을 받아옴.(idcheck는 join.html에 중복확인 버튼을 통해 접속)

 

join.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>
    <h3>회원가입</h3>

    <form th:action="@{/member/join.do}" method="post">
        아이디 : <input type="text" name="id" />
            <input type="button" value="중복확인" th:onclick="|javascript:idcheckAction()|" >
        <br />
        암호 : <input type="password" name="password" /><br />
        암호확인 : <input type="password" /><br />
        이름 : <input type="text" name="name" /><br />
        연락처 : <input type="text" name="phone" /><br />
        <input type="submit" value="가입" />
    </form>

    <script type="text/javascript">
        const idcheckAction = () => {
            // 1. 사용자가 입력한 아이디를 받아옴
            const id = document.getElementsByName("id")[0];

            // 2. GET으로 전송하기
            location.href="/ROOT/member/idcheck.do?id=" + id.value;
        }
    </script>
</body>
</html>

해당 부분에서

아이디 부분과 script만 보면됨

아이디 : <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];

        // 2. GET으로 전송하기
        location.href="/ROOT/member/idcheck.do?id=" + id.value;
    }
</script>

body에서 script부분을 반응하게끔 th:onclick 사용

<input type="button" value="중복확인" th:onclick="|javascript:idcheckAction()|" >

클릭시 script 반응

문법은 html script 문법(axios할때 많이 썻던 기억이 있음.)

 

script는 화면에 표기된(사용자가 입력한 아이디) 정보를 끌어오기 위해 아래코드 사용

// 1. 사용자가 입력한 아이디를 받아옴
const id = document.getElementsByName("id")[0];

membercontroller호출하여 idcheck페이지로 이동

// 2. GET으로 전송하기
location.href="/ROOT/member/idcheck.do?id=" + id.value;

그러면 idcheck페이지에서 ret으로 중복확인이 됨.

 

 

실습문제

답글 삭제(이 문제는 게시글 번호와 답글 번호가 일치하여야만 삭제할 수 있게 만들어야함.)

const deleteReplyAction = ( no, boardno ) => {
    alert(no);
    alert(boardno);
    if( confirm('댓글 삭제할까요?') ){
        const form = document.createElement("form");
        form.setAttribute("action", "/ROOT/board/deleteboardreply.do");

        const input1 = document.createElement("input");
        input1.setAttribute("type", "text");
        input1.setAttribute("name", "no");
        input1.setAttribute("value", no);

        const input2 = document.createElement("input");
        input2.setAttribute("type", "text");
        input2.setAttribute("name", "boardno");
        input2.setAttribute("value", boardno);

        form.appendChild(input1);
        form.appendChild(input2);

        form.setAttribute("method", "post");
        document.body.appendChild(form);
        form.submit();
    }
}

html에서 no와 boardno를 model형태로 controller에 보냄("/ROOT/board/deleteboardreply.do")

 

@PostMapping(value="deleteboardreply.do")
public String deleteboardreplyPOST(@ModelAttribute BoardReply boardReply){
    System.out.println(boardReply.toString());

    // 서비스를 호출해서 삭제하기
    bService.deleteBoardReply(boardReply);

    return "redirect:/board/boardone.do?no=" + boardReply.getBoardno();
}

BoardReply형태로 html에서 받아옴.

 

서비스에서 삭제를 끌어옴

서비스

// 답글 삭제
// deleteBoardReply
public int deleteBoardReply(BoardReply boardReply){
    try {
        brRepository.delete(boardReply);
        return 1;
    }
    catch(Exception e){
        e.printStackTrace();
        return 0;
    }
}

컨트롤러에서 받은 BoardReply타입의 boardReply를 delete이용하여 게시물 넘버, 답글 넘버 일치하는 것 삭제