본문 바로가기

SpringBoot

SpringBoot 게시판 기능구현 3

1. 게시글 읽기(Read) 및 댓글 기능 구현

  • 게시글을 읽는데 가장 중요한 조건 몇가지가 있다.
    1.  로그인된 회원만 볼 수 있다.
    2. Board Table에 있는BoardId와 게시판 도메인이 일치하는경우에 의해서만 게시글이 존재한다.
    3. 게시판이 있을 경우 게시글을 띄울때 그 게시글의 인덱스 번호를 활용해 어디 게시판의 몇번째 게시글인지 알아야 한다.

이를 줄여서 aid(ArticleId)라고 하고 bbs/write?aid=index값으로 도메인에 표시하려고 한다. 다음은 aid값을 구하는 과정을 기능구현한 것이다.

 

<select id="selectArticleIndex"
        resultType="dev.babsang.studymemberbbs.vos.bbs.ArticleReadVo">
    SELECT `index`           AS `index`,
           `user_email`      AS `userEmail`,
           `board_id`        AS `boardId`,
           `title`           AS `title`,
           `content`         AS `content`,
           `view`            AS `view`,
           `written_on`      AS `writtenOn`,
           `modified_on`     AS `modifiedOn`,
           `user`.`nickname` AS `userNickname`
    FROM `study_bbs`.`articles`
             LEFT JOIN `study_member`.`users` AS `user` ON `articles`.`user_email` = `user`.`email`
    WHERE BINARY `index` = #{index}
    ORDER BY `index` DESC
    LIMIT 1
</select>
  • 게시글의 인덱스번호를 Select하는 쿼리문(xml)이다. LEFT JOIN테이블에 대해서는 아래에서 자세히 다룰것이다. 우선 WHERE절에서 index의 값이 일치하냐에 대한 것으로 쿼리가 시작된다. 일치할 경우 SELECT를 통해 ArticleReadVoEntity의 모든 값을 Search할 수 있게 된다.
IBbsMapper
ArticleReadVo selectArticleIndex(@Param(value = "index") int index);
  • 위와 같이 ArticleReadVo 타입의 selectArticleIndex 메서드를 만들어 Param값으로 index를 받는다. 이때 주의 할 점은 WHERE절 #{}안의 값과 일치하는 값을 변수명으로 지정해야 한다. 아니면 Parameter를 찾지 못한다는 오류가 발생한다.
BbsService getArticle()구현
public ArticleReadVo getArticle(int index) {
    return this.bbsMapper.selectArticleIndex(index);
}
  • ArticleReadVo타입의 getArticle() 메서드는 Mapper.xml, IBbsMapper와 Controller를 이어주는 징검다리 역할을 한다.
BbsController getRead() 요청방식
@RequestMapping(value = "read", method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE)
//bbs/read 도메인을 띄우기 위한 RequestMapping이다.
public ModelAndView getRead(@RequestParam(value = "aid", required = true) int aid) {
    // 이메일은 login메서드를 통해 가져오지만 nickname은 가져오지 않기 때문에 null이뜬다.
    ModelAndView modelAndView = new ModelAndView("bbs/read");
    if (aid < 0 || this.bbsService.getArticle(aid) == null) {
        modelAndView.addObject("result", CommonResult.FAILURE.name());
    } else {
        ArticleReadVo articleId = this.bbsService.getArticle(aid);
        BoardEntity board = this.bbsService.getBoard(articleId.getBoardId());
        modelAndView.addObject("result", CommonResult.SUCCESS.name());
        modelAndView.addObject("article", this.bbsService.getArticle(aid));
        //위에서 받아온 값은 그냥 String이다. 보내줘야 하는것 boardEntity타입의 변수이다.
        modelAndView.addObject("aid", articleId.getIndex());
        modelAndView.addObject("board", this.bbsService.getBoard(board.getId()));
    }
    return modelAndView;
}
  • IBbsMapper, BbsService에서 index의 값을 int타입으로 받았기 때문에 @RequestParam을 통해 가져온 데이터의 값을 int aid값에 대입한다.
  • 이때 @RequestParam의 value는 게시판을 통해 게시글 페이지가 클라이언트에게 보여졌을 경우 도메인에 있는 aid를 나타낸다. (JS에서 responseObject['aid']를 사용하기 위한 변수 어노테이션이다.)
  • modelAndView에 addObject객체를 통한 키와 값지정을 통해 HTML에서 th:text를 통해 값을 동적으로 받아온다.
  • "article"키는 getArticle(어떠한 인덱스)를 통해 그 인덱스에 관한 값들을 SELECT 하는 역할을 한다. 예를 들어 getArticle(31번 인데스)일 경우 31번 인덱스가 xml에서 where절에서 조건의 일치가 통과가 되면 모든 값들을 SELECT해오는 방식이다.
Js기능 구현
Cover.show('게시글을 등록 중입니다\n잠시만 기다려주세요.');
    const xhr = new XMLHttpRequest();
    const formData = new FormData();
    formData.append('title', form['title'].value);
    formData.append('content', editor.getData());
    formData.append('bid', form['bid'].value);
    xhr.open('POST', './write'); // http://localhost:8080/bbs/write
    // formData.append('content',editor.getData());
    // formData.append('bid', window.location.href);
    // xhr.open('POST', window.location.href); http://localhost:8080/bbs/write?bid=free
    xhr.onreadystatechange = () => {
        Cover.hide();
        if (xhr.readyState === XMLHttpRequest.DONE) {
            if (xhr.status >= 200 && xhr.status < 300) {
                const responseObject = JSON.parse(xhr.responseText);
                switch (responseObject['result']) {
                    case 'not_allowed':
                        Warning.show('게시글을 작성할 수 있는 권한이 없거나 로그아웃 되었습니다. 확인 후 다시 시도해 주세요.');
                        break;
                    case 'success':
                        const aid = responseObject['aid'];
                        window.location.href = `read?aid=${aid}`;
                        break;
                    default:
                        Warning.show('알 수 없는 이유로 게시글을 작성하지 못하였습니다. 잠시 후 다시 시도해 주세요.');
                        break;
                }
            } else {
                Warning.show('서버와 통신하지 못하였습니다. 잠시후 다시 시도해 주세요.');
            }
        }
    }
    xhr.send(formData);
};
  • JS는 작성할때 마다 새롭다. 방금 전 formData.append()에 대해서 확실하게 알게 되었다. formData.append()를 통해 DB에서 index(AutoIncrement), user_email(session.user)등 값을 가져올 구실이 없는 친구들을 append를 통해 같이 전송해주는 것이다. 즉 화면에서 input에 name이 있는 값들에 대해서 처리를 해준다. bid같은 경우는 히든필드를 사용하여 name이 bid임을 가지고 있으나 클라이언트에게 공개할 필요는 없기에 hidden을 통해 숨겨주었을 뿐 값을 같이 넘겨 주어야 한다. 
  • JSON이 가진 result의 값이 success일 경우 작성하기 버튼을 눌렀을시 read페이지 이나 read?aid=${aid(컨트롤러에서 작성한)}을 통해 클라이언트에게 보여진다. 여기서 window.location.href는 localhost:8080/bbs/를 나타낸다. 즉 localhost:8080/bbs/read?aid=(index번호값)이 된다.  
postRead Controller 기능 구현(댓글 작성 버튼 관련)
@RequestMapping(value = "read", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String postRead(@SessionAttribute(value = "user", required = false) UserEntity user,
                       CommentEntity comment) {
    JSONObject responseObject = new JSONObject();
    System.out.println("여기");
    if (user == null) {
        responseObject.put("result", CommonResult.FAILURE.name().toLowerCase());
    } else {
        comment.setUserEmail(user.getEmail());
        Enum<?> result = this.bbsService.writeComment(comment); // 여기서 성공/실패를 나누고 그결과를 밑에 put()함
        System.out.println("여기는?");
        responseObject.put("result", result.name().toLowerCase());
    }
    return responseObject.toString();
}
  • user는 session을 통해 로그인 인증이 된 회원의 정보를 가져온다.
  • comment.setUserEmail()메서드를 통해 로그인된 클라이언트의 이메일로 업데이트 한다.
  • 컨트롤러에서 서비스관련 기능을 구현하는 이유는 Service에서 부터 매개변수를 많이 받으면 컨트롤러에서 똑같이 받아야 하기 때문에 굳이 그럴필요없이 효율성을 높이기 위해 컨트롤러에서 구현할 수 있는 부분은 구현해 준것이다.
writeComment Service 기능 구현
public Enum<? extends IResult> writeComment(CommentEntity comment) {
    ArticleEntity article = this.bbsMapper.selectArticleIndex(comment.getArticleIndex());
    if(article == null) {
        return CommonResult.FAILURE;
    }
    return this.bbsMapper.insertComment(comment) != 0
            ? CommonResult.SUCCESS
            : CommonResult.FAILURE;
}
  • writeComment는 댓글 작성관련 서비스 기능 구현을 한것이다.
  • ArticleEntity타입의 article은 selectArticleIndex메서드를 통해 comment.getArticleIndex()의 값을 담고있다.

2. 댓글 기능 구현

comment.xml
CommentEntity[] selectCommentsByArticleIndex(@Param(value = "articleIndex")int articleIndex);
  • Entity배열로 받는 이유는 하나의 게시글에는 많은 댓글이 생길 수 있기 때문에 배열로 받아야 한다.
comment mapper
<select id="selectCommentsByArticleIndex"
        resultType="dev.babsang.studymemberbbs.entities.bbs.CommentEntity">
    SELECT `index`         AS `index`,
           `comment_index` AS `commentIndex`,
           `user_email`    AS `userEmail`,
           `article_index` AS `articleIndex`,
           `content`       AS `content`,
           `written_on`    AS `writtenOn`
    FROM `study_bbs`.`comments`
    WHERE `article_index` = #{articleIndex}
    ORDER BY `index`
</select>
  • 다른 select쿼리와 다른점은 없다. 항상 말하든 주의 할점은 WHERE절 변수명과 이름 일치 시키기.
comment Service
public CommentEntity[] getComments(int articleIndex) {
    return this.bbsMapper.selectCommentsByArticleIndex(articleIndex);
}
  • CommentEntity 배열타입을 받아 Mapper와 Controller를 연결해주는 역할을 한다.
comment Controller(GET방식)
@RequestMapping(value = "comment", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String getComment(@RequestParam(value = "aid")int articleIndex) {
    JSONArray responseArray = new JSONArray();
    CommentEntity[] comments = this.bbsService.getComments(articleIndex);
    for (CommentEntity comment : comments) {
        JSONObject commentObject = new JSONObject();
        commentObject.put("index", comment.getIndex());
        commentObject.put("commentIndex", comment.getCommentIndex());
        commentObject.put("userEmail", comment.getUserEmail());
        commentObject.put("articleIndex", comment.getArticleIndex());
        commentObject.put("content", comment.getContent());
        commentObject.put("writtenOn", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(comment.getWrittenOn()));
        // new SimpleDateFormat("형식").format([Date 타입 객체]) : [Date 타입 객체]가 가진 일시를 원하는 형식의 문자열로 만들어 반환한다.
        responseArray.put(commentObject);
    }
    return responseArray.toString();
}
  • 댓글은 많은 정보를 담아서 한꺼번에 담아 와야 함으로 JSON객체 방식으로 값을 받아온다.
  • JSONArray는 Object를 배열로 담을수 있는 JSON 배열 방식이다.
  • CommentEntity배열 타입의 comments에 위에서 RequestParam으로 받아온 articleIndex값을 대입한다.
  • 이후 향상된 for문을 통해 반복을 시켜주는 값을 JSONObject 객체에 하나씩 담아준다.
  • 그후 마지막에 각자 담긴 commentObjce객체 값을 다시 responseArray객체에 담아준다.
  • 아래와 같이 배열에 object방식으로 키와 값들이 들어간 배열이 생성되었다. (개발자 도구 Response값임).

JSONArray에 담긴 comment의 value값들

Comment JS 기능 구현
if (commentForm != null) {
    commentForm.onsubmit = e => {
        e.preventDefault();
        if (commentForm['content'].value === '') {
            alert('댓글을 입력해 주세요.')
            commentForm['content'].focus();
            return false;
        }
        Cover.show('댓글을 작성하고 있습니다. \n잠시만 기다려 주세요.');
        const xhr = new XMLHttpRequest();
        const formData = new FormData();
        formData.append('articleIndex', commentForm['aid'].value);
        formData.append('content', commentForm['content'].value);
        // formData.append는 값이 넘겨져야 할것들만 넘겨준다. index, commentIndex, userEmail, writtenOn은 각자 자신의 값이 따로 넘겨진다. index= autoIncrement, commentIndex는 null값이 들어감, userEmail은 컨트롤러에서 로그인된 session.user의 값으로 들어간다. writtenOn은 default now(xml에서 처리)로 저 두개의 값만 넘겨주면 된다.
        xhr.open('POST', './read');
        xhr.onreadystatechange = () => {
            if (xhr.readyState === XMLHttpRequest.DONE) {
                Cover.hide();
                if (xhr.status >= 200 && xhr.status < 300) {
                    const responseObject = JSON.parse(xhr.responseText);
                    switch (responseObject['result']) {
                        case 'success':
                            alert('성공임');
                            break;
                        default:
                            alert('알 수 없는 이유로 댓글을 작성하지 못하였습니다.\n\n잠시후 다시 시도해 주세요.');
                    }
                } else {
                    alert('서버와 통신하지 못하였습니다. \n\n잠시후 다시 시도해 주세요.')
                }
            }
        };
        xhr.send(formData);
    }
}

  • 가장 처음 if조건문을 살펴보면 commentForm이 null이 아닐경우를 기준으로 xhr를 구현한다. null이 아닌경우는 html에서 바로 위와 같이 session.user가 null이 아닐때 즉 로그인된 회원들을 기준으로 form태그를 사용할 수 있게 됨으로 로그인이 안되어있다면 form태그도 클라이언트 브라우저에서는 사라지게 된다.