Post View

블로그에 댓글 기능이 추가되었습니다.

드디어 블로그에 댓글 기능이 추가되었습니다!
github(https://github.com/kurien92/kreBlog/commit/3a99def019f3542bebcdb7c40d635a376954bc6a)에 올린걸 보니 5월 2 ~ 3일 정도 부터 작업을 시작해서 5월 16일에 어느정도 마무리 되어 서버에 반영하게 되었네요.

현재 댓글은 작성자명, 비밀번호, 댓글내용으로 구성되어있으며 댓글, 답글, 수정, 삭제 기능을 제작하였습니다.
답글에는 추가적인 코멘트를 달지 못하도록 2depth 까지만 구현해두었으며, 삭제 시에는 작성자명과 작성시간은 그대로 보이고 내용만 숨겨지도록 설정해두었습니다.

View 화면에서 Comment에 추가된 스크립트(86~703번 줄)를 보시면 해당 부분은 jQuery의 ajax 기능을 통해서 처리할 수 있도록 작업해두었습니다.
코드가 굉장히 복잡해보이죠? 코딩을 하다보니 코드 양이 많아 귀찮아져서... 설계 없이 급하게 만들었더니 이렇게 되어버렸습니다... ㅋㅋㅋ...

해당 스크립트는 추후에 jQuery Plugin 형태로 바꿔서 외부 script 호출하도록 변환시킬 예정이구요, 더 나아가서는 블로그 자체를 React와 같은 SPA의 형태로 만들 생각도 있습니다.
이처럼 ajax를 통해 작성을 하다보니 Spring의 CommentController는 RestController 어노테이션이 적용되어있습니다.

package net.kurien.blog.controller;

import java.text.SimpleDateFormat;
import java.util.List;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

import net.kurien.blog.exception.InvalidRequestException;
import net.kurien.blog.module.comment.dto.CommentDto;
import net.kurien.blog.module.comment.service.CommentService;
import net.kurien.blog.module.comment.vo.Comment;
import net.kurien.blog.module.token.vo.Token;
import net.kurien.blog.util.HtmlUtil;
import net.kurien.blog.util.RequestUtil;
import net.kurien.blog.util.TokenUtil;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/comment")
public class CommentController {
    private static final Logger logger = LoggerFactory.getLogger(CommentController.class);
    
    @Inject
    private CommentService commentService;
    
    @RequestMapping(value = "/list/{postNo}", method = RequestMethod.GET)
    public JsonObject list(@PathVariable int postNo) {
        JsonObject json = new JsonObject();

        List comments = commentService.getList(postNo);
        
        JsonArray commentJsonArray = new JsonArray();
        
        for(int i = 0; i < comments.size(); i++) {
            commentJsonArray.add(getJsonFromComment(comments.get(i)));
        }

        json.addProperty("result", "success");
        json.add("value", commentJsonArray);
        json.addProperty("message", "");
        
        return json;
    }
    
    @RequestMapping(value = "/write/{postNo}", method = RequestMethod.POST)
    public JsonObject write(@PathVariable int postNo, CommentDto commentDto, HttpServletRequest request, HttpServletResponse response, Model model) throws Exception {
        JsonObject json = new JsonObject();
        
        try {
            validInput(commentDto);
        } catch(InvalidRequestException ire) {
            json.addProperty("result", "success");
            json.add("value", new JsonObject());
            json.addProperty("message", "");
            return json;
        }
        
        Comment comment = new Comment();
        
        comment.setPostNo(postNo);
        comment.setAuthor(HtmlUtil.escapeHtml(commentDto.getName()));
        comment.setPassword(commentDto.getPassword());
        comment.setComment(HtmlUtil.escapeHtml(commentDto.getText()));
        comment.setWriteIp(RequestUtil.getRemoteAddr(request));
        
        commentService.write(comment);
        
        json.addProperty("result", "success");
        json.add("value", getJsonFromComment(comment));
        json.addProperty("message", "");
        
        return json;
    }

    @RequestMapping(value = "/reply/{no}", method = RequestMethod.POST)
    public JsonObject reply(@PathVariable int no, CommentDto commentDto, HttpServletRequest request, HttpServletResponse response, Model model) throws Exception {
        JsonObject json = new JsonObject();
        
        try {
            validInput(commentDto);
        } catch(InvalidRequestException ire) {
            json.addProperty("result", "success");
            json.add("value", new JsonObject());
            json.addProperty("message", "");
            return json;
        }
        
        Comment comment = new Comment();
        
        comment.setAuthor(HtmlUtil.escapeHtml(commentDto.getName()));
        comment.setPassword(commentDto.getPassword());
        comment.setComment(HtmlUtil.escapeHtml(commentDto.getText()));
        comment.setWriteIp(RequestUtil.getRemoteAddr(request));

        try {
            commentService.reply(no, comment);
        } catch(InvalidRequestException ire) {
            json.addProperty("result", "fail");
            json.add("value", new JsonObject());
            json.addProperty("message", "잘못된 요청입니다.");
            
            return json;
        }
        
        json.addProperty("result", "success");
        json.add("value", getJsonFromComment(comment));
        json.addProperty("message", "");
        
        return json;
    }
    
    @RequestMapping(value = "/modify/{no}", method = RequestMethod.POST)
    public JsonObject modify(@PathVariable int no, CommentDto commentDto, String token, HttpServletRequest request, HttpServletResponse response, Model model) throws Exception {
        JsonObject json = new JsonObject();
        
        if(TokenUtil.checkToken(request, "comment", token) == false) {
            json.addProperty("result", "fail");
            json.add("value", new JsonObject());
            json.addProperty("message", "수정 가능한 시간이 만료되었습니다.");
            
            return json;
        }
        
        try {
            validInput(commentDto);
        } catch(InvalidRequestException ire) {
            json.addProperty("result", "success");
            json.add("value", new JsonObject());
            json.addProperty("message", "");
            return json;
        }

        Comment comment = new Comment();
        
        comment.setCommentNo(no);
        comment.setAuthor(HtmlUtil.escapeHtml(commentDto.getName()));
        comment.setPassword(commentDto.getPassword());
        comment.setComment(HtmlUtil.escapeHtml(commentDto.getText()));
        comment.setWriteIp(RequestUtil.getRemoteAddr(request));
        
        commentService.modify(comment);
        
        comment = commentService.get(no);
        
        json.addProperty("result", "success");
        json.add("value", getJsonFromComment(comment));
        json.addProperty("message", "");
        
        return json;
    }
    
    @RequestMapping(value = "/delete/{no}", method = RequestMethod.POST)
    public JsonObject delete(@PathVariable int no, String token, HttpServletRequest request, HttpServletResponse response, Model model) throws Exception {
        JsonObject json = new JsonObject();

        if(TokenUtil.checkToken(request, "comment", token) == false) {
            json.addProperty("result", "fail");
            json.add("value", new JsonObject());
            json.addProperty("message", "삭제 가능한 시간이 만료되었습니다.");
            
            return json;
        }
        
        commentService.delete(no);
        
        json.addProperty("result", "success");
        json.addProperty("value", "");
        json.addProperty("message", "");
        
        return json;
    }
    
    @RequestMapping(value = "/passwordCheck/{no}", method = RequestMethod.POST)
    public JsonObject passwordCheck(@PathVariable int no, String password, HttpServletRequest request, HttpServletResponse response, Model model) throws Exception {
        boolean checkedPassword = commentService.checkPassword(no, password);
        
        JsonObject json = new JsonObject();
        
        if(checkedPassword == false) {
            json.addProperty("result", "fail");
            json.add("value", new JsonObject());
            json.addProperty("message", "비밀번호가 일치하지 않습니다.");
            
            return json;
        }
        
        Comment comment = commentService.get(no);
        
        JsonObject valueObject = getJsonFromComment(comment);

        valueObject.addProperty("token", TokenUtil.createToken(request, "comment", 21600000));

        json.addProperty("result", "success");
        json.add("value", valueObject);
        json.addProperty("message", "");

        return json;
    }

    private JsonObject getJsonFromComment(Comment comment) {
        JsonObject commentJson = new JsonObject();
        
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
        
        String text = comment.getComment();
        
        if(comment.getDeleteYn().equals("Y")) {
            text = "";
        }

        commentJson.addProperty("no", comment.getCommentNo());
        commentJson.addProperty("name", comment.getAuthor());
        commentJson.addProperty("time", sdf.format(comment.getWriteTime()));
        commentJson.addProperty("text", text);
        commentJson.addProperty("depth", comment.getCommentDepth());
        commentJson.addProperty("delYn", comment.getDeleteYn());
        
        return commentJson;
    }

    
    private boolean validInput(CommentDto commentDto) throws InvalidRequestException {
        // TODO Auto-generated method stub
        if(commentDto.getName().length() < 2) {
            throw new InvalidRequestException("작성자명은 2자 이상으로 입력해주세요.");
        }
        
        if(commentDto.getName().length() > 30) {
            throw new InvalidRequestException("작성자명은 30자 미만으로 입력해주세요.");
        }
        
        if(commentDto.getPassword().length() == 0) {
            throw new InvalidRequestException("비밀번호를 입력해주세요.");
        }
        
        if(commentDto.getPassword().length() > 30) {
            throw new InvalidRequestException("비밀번호는 30자 미만으로 입력해주세요.");
        }
        
        if(commentDto.getPassword().length() == 0) {
            throw new InvalidRequestException("댓글을 입력해주세요.");
        }
        
        if(commentDto.getPassword().length() > 30) {
            throw new InvalidRequestException("댓글은 10000자 미만으로 입력해주세요.");
        }

        return true;
    }
}

포스트를 불러온 후 CommentController의 list 메서드를 통해 코멘트 목록을 보여주고, write, reply, modify, delete와 같은 메서드를 통해 댓글과 답글을 작성, 수정, 삭제합니다.
서비스의 경우에는 크게 어려운 부분은 없었기 때문에 본 글에는 별도로 올리지 않았으니 github에 올린 코드를 참고해주세요!

passwordCheck 메서드는 댓글을 수정하거나 삭제하기 전에 해당 댓글의 비밀번호를 확인하기 위해 만들었습니다.
비밀번호를 확인한 뒤 실제 댓글을 수정하는 경우 인증 확인을 위해 비밀번호 확인 후 token을 발행하여 클라이언트에게 건네주게 됩니다.

그리고 댓글을 수정하는 경우에는 해당 token이 유효한지 확인하고 수정하게 되어있습니다.
이러한 처리를 한 이유는 token이 없는 경우 외부에서 modify 메서드에 전송되는 형태로 서버에 수정 요청을 하게 되면 modify 메서드에는 비밀번호를 검증하는 로직이 없으니 정상 요청인지 아닌지 구분할 수 없기 때문입니다.
token이 있다면 해당 token이 유효한 값인지 여부에 따라 정상 요청이라는 것을 구분할 수 있게 되죠.

그리고 댓글을 입력하거나 수정하는 부분에는 "Java에서 HTML 문자 escape 하기"에서 적용했던 escape를 적용하였습니다.
이 부분은 서버 배포할 때까지 생각을 못하고 있다가 서버 배포 후에 갑자기 생각나서 급하게 적용했습니다.

이러한 처리를 하지 않으면 누구나 댓글을 통해 html 코드를 입력할 수 있는데, script도 사용이 가능해지게 되어 XSS(사이트 간 스크립팅)에 공격 당할 수 있는데, Post 기능 제작시에는 관리자만 사용할 수 있도록 로그인이 존재하므로 보안을 크게 신경쓰지 않았지만 댓글의 경우 누구나 입력할 수 있기 때문에 조금 더 보안을 신경썼습니다.
물론 신경쓴다고 썼지만 여전히 취약한 부분이 있을 것으로 예상되며, 혹시라도 취약점을 발견해주시면 kurien92@gmail.com으로 메일 보내주시면 감사하겠습니다.

현재 작성중인 Blog의 코드는 https://github.com/kurien92/kreBlog에서 확인하실 수 있으며 질문이나 의견 등은 댓글 혹은 메일로 보내주시기 바랍니다!

Comments