-
[Camel][Spring] 게시판 만들기 #3. 게시판 글 쓰기,수정 삭제Spring/게시판 만들기 2020. 6. 25. 21:49
[Camel][Spring] 게시판 만들기 #3. 게시판 글 쓰기,수정 삭제
본 게시판 만들기 프로젝트는 더블에스 Devlog Spring-MVC 를 참조하여 작성했음을 알려드립니다. 또한 개인적인 학습을 목적으로한 포스팅이기 때문에 완벽하지 않을 수 있음을 알려드립니다. 문제점이나 궁금한점은 댓글로 남겨주시면 감사하겠습니다. 프로젝트 생성에 앞서 이번 게시판 만들기 프로젝트는 이클립스를 사용하여 구현하였습니다.
1. Article 테이블 생성
기본적인 CRUD(Create(생성), Read(읽기), Update(갱신), Delete(삭제))를 구현하기 위해서 스키마를 생성하고 아래와 같이 테이블을 생성하겠습니다. 이 과정에서 본인은 MySQL Workbench 8.0을 사용했으며 설치 방법 및 사용방법은 따로 설명하지 않겠습니다. 혹시 설치 및 사용법이 궁금하신분은 디벨로펀프로젝트-mariadb, mySql Workbench 설치 및 샘플 DB 구축을 참고하시면 도움이 될것이라고 생각합니다.
Workbench를 사용한 테이블 생성과정은 아래와 같습니다.
2. ArticleVO 작성
앞서 생성한 테이블의 구조를 객체화 시키기 위해 ArticleVO 클래스를 src/main/java/기본패키지/domain 경로에 생성한 후 아래와 같이 작성해 줍니다.
package com.cameldev.mypage.domain; import java.sql.Date; import java.util.Arrays; public class ArticleVO { private Integer article_no; private String title; private String content; private String writer; private Date regDate; private int viewCnt; public Integer getArticle_no() { return article_no; } public void setArticle_no(Integer article_no) { this.article_no = article_no; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getWriter() { return writer; } public void setWriter(String writer) { this.writer = writer; } public Date getRegDate() { return regDate; } public void setRegDate(Date regDate) { this.regDate = regDate; } public int getViewCnt() { return viewCnt; } public void setViewCnt(int viewCnt) { this.viewCnt = viewCnt; } @Override public String toString() { return "ArticleVO [article_no=" + article_no + ", title="+ title + ", content="+ content + ", writer="+ writer + ", regDate="+ regDate + ", viewCnt="+ viewCnt + "]" ; } }
3. DAO 계층 구현
3-1. ArticleDAO 인터페이스 작성
ArticleVO 클래스를 작성했다면 이제는 src/main/java/기본패키지/persistence 경로에 ArticleDAO 인터페이스를 생성한 후 아래와 같이 작성해 줍니다.
package com.cameldev.mypage.persistence; import java.util.List; import com.cameldev.mypage.domain.ArticleVO; public interface ArticleDAO { void create(ArticleVO articleVO) throws Exception; ArticleVO read(Integer article_no) throws Exception; void update(ArticleVO articleVO) throws Exception; void delete(Integer article_no) throws Exception; List<ArticleVO> listAll() throws Exception; }
3-2. ArticleDAOImpl 클래스 작성
위에서 ArticleDAO 인터페이스를 작성했다면, 이제는 ArticleDAO 인터페이스를 구현한 ArticleDAOImpl 클래스를 작성해줍니다.
package com.cameldev.mypage.persistence; import java.util.List; import javax.inject.Inject; import org.apache.ibatis.session.SqlSession; import org.springframework.stereotype.Repository; import com.cameldev.mypage.domain.ArticleVO; @Repository public class ArticleDAOImpl implements ArticleDAO { private static final String NAMESPACE = "com.cameldev.mypage.mappers.article.ArticleMapper"; private final SqlSession sqlSession; @Inject public ArticleDAOImpl(SqlSession sqlSession) { this.sqlSession = sqlSession; } @Override public void create(ArticleVO articleVO) throws Exception { sqlSession.insert(NAMESPACE + ".create", articleVO); } @Override public ArticleVO read(Integer article_no) throws Exception { return sqlSession.selectOne(NAMESPACE + ".read", article_no); } @Override public void update(ArticleVO articleVO) throws Exception { sqlSession.update(NAMESPACE + ".update", articleVO); } @Override public void delete(Integer article_no) throws Exception { sqlSession.delete(NAMESPACE + ".delete", article_no); } @Override public List<ArticleVO> listAll() throws Exception { return sqlSession.selectList(NAMESPACE + ".listAll"); } }
3-3. articleMapper.xml 파일 작성
다음으로는 src/main/resources/mappers/article/경로에 articleMapper.xml을 생성하고, 아래와 같이 작성해준다.
단 이때 주의해야 할 점이 있다면 ArticleDAOImpl 클래스의 NAMESPACE 변수에 정의된 String값과 mapper 파일의 namespace 값은 일치해야한다는 점입니다.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.cameldev.mypage.mappers.article.ArticleMapper"> <insert id="create"> INSERT INTO tb_article ( article_no , title , content , writer , regdate , viewcnt ) VALUES ( #{article_no} , #{title} , #{content} , #{writer} , #{regDate} , #{viewCnt} ) </insert> <select id="read" resultMap="ArticleResultMap"> SELECT article_no , title , content , writer , regdate , viewcnt FROM tb_article WHERE article_no = #{article_no} </select> <update id="update"> UPDATE tb_article SET title = #{title} , content = #{content} WHERE article_no = #{article_no} </update> <delete id="delete"> DELETE FROM tb_article WHERE article_no = #{article_no} </delete> <select id="listAll" resultType="ArticleVO"> <![CDATA[ SELECT article_no, title, content, writer, regdate, viewcnt FROM tb_article WHERE article_no > 0 ORDER BY article_no DESC, regdate DESC ]]> </select> <resultMap id="ArticleResultMap" type="ArticleVO"> <id property="article_no" column="article_no"/> <result property="title" column="title" /> <result property="content" column="content" /> <result property="writer" column="writer" /> <result property="regDate" column="regdate" /> <result property="viewCnt" column="viewcnt" /> </resultMap> </mapper>
위의 mapper파일에서 주의깊게 봐야할 부분은 resultMap 속성입니다. resultMap을 사용하는 경우는 Java 객체의 변수명과 DB Column명이 다를 경우입니다. Java 객체의 변수명과 DB Column명이 일치하지 않을 경우 select 쿼리의 경우 원하는 결과값을 가져올 수 없습니다. 이런 경우 SQL Aliases를 사용해서 해결가능은 하겠지만, 불일치하는 번수가 많을수록 매번 SQL Aliases를 사용하기에는 번거롭지 않을 수 없습니다.그래서 이러한 번거로움을 없애기 위해 resultMap을 사용해서 Java 변수명과 DB Column명을 일치시켜주는 작업을 해주고 select 쿼리의 resultType속성 대신 resultMap 속성을 사용합니다.
4. Service 계층 구현
4-1. ArticleService 인터페이스 작성
Service 계층을 구현하기 위해 src/main/java/기본패키지/service패키지에 ArticleService인터페이스를 생성하고, 아래와 같이 메서드를 정의해줍니다.
package com.cameldev.mypage.service; import java.util.List; import com.cameldev.mypage.domain.ArticleVO; public interface ArticleService { void create(ArticleVO articleVO) throws Exception; ArticleVO read(Integer article_no) throws Exception; void update(ArticleVO articleVO) throws Exception; void delete(Integer article_no) throws Exception; List<ArticleVO> listAll() throws Exception; }
4-2. ArticleServiceImpl 클래스 작성
위에서 ArticleService 인터페이스를 작성했다면, 이제는 ArticleService 인터페이스를 구현한 ArticleServiceImpl 클래스를 작성해줍니다.
package com.cameldev.mypage.service; import java.util.List; import javax.inject.Inject; import org.springframework.stereotype.Service; import com.cameldev.mypage.domain.ArticleVO; import com.cameldev.mypage.persistence.ArticleDAO; @Service public class ArticleServiceImpl implements ArticleService { private final ArticleDAO articleDAO; @Inject public ArticleServiceImpl(ArticleDAO articleDAO) { this.articleDAO = articleDAO; } @Override public void create(ArticleVO articleVO) throws Exception { articleDAO.create(articleVO); } @Override public ArticleVO read(Integer article_no) throws Exception { return articleDAO.read(article_no); } @Override public void update(ArticleVO articleVO) throws Exception { articleDAO.update(articleVO); } @Override public void delete(Integer article_no) throws Exception { articleDAO.delete(article_no); } @Override public List<ArticleVO> listAll() throws Exception { return articleDAO.listAll(); } }
5. ArticleController 작성
Controller 클래스 선언부에 @Controller 애너테이션과 @RequestMapping("/article")을 추가했는데 이때@RequestMapping("/article")을 추가함으로써 공통의 경로를 /article로 인식하도록 설정하였다. 보통 Controller는 하나의 기능을 가진 모듈의 대표적인 경로를 갖도록 하는 것이 좋습니다.
src/main/java/기본패키지/controller 패키지에 ArticleController를 생성하고, 각각의 요청에 응답할 메서드들을 아래와 같이 작성해줍니다.
package com.cameldev.mypage.controller; import javax.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import com.cameldev.mypage.domain.ArticleVO; import com.cameldev.mypage.service.ArticleService; @Controller @RequestMapping("/article") public class ArticleController { private static final Logger logger = LoggerFactory.getLogger(ArticleController.class); private final ArticleService articleService; @Inject public ArticleController(ArticleService articleService) { this.articleService = articleService; } // 등록 페이지 이동 @RequestMapping(value = "/write", method = RequestMethod.GET) public String writeGET() { logger.info("write GET..."); return "/article/write"; } // 등록 처리 @RequestMapping(value = "/write", method = RequestMethod.POST) public String writePOST(ArticleVO articleVO, RedirectAttributes redirectAttributes) throws Exception { logger.info("write POST..."); logger.info(articleVO.toString()); articleService.create(articleVO);; redirectAttributes.addFlashAttribute("msg", "regSuccess"); return "redirect:/article/list"; } // 목록 페이지 이동 @RequestMapping(value = "/list", method = RequestMethod.GET) public String list(Model model) throws Exception { logger.info("list ..."); model.addAttribute("articles", articleService.listAll()); return "/article/list"; } // 조회 페이지 이동 @RequestMapping(value = "/read", method = RequestMethod.GET) public String read(@RequestParam("article_no") int article_no, Model model) throws Exception { logger.info("read ..."); model.addAttribute("article", articleService.read(article_no)); return "/article/read"; } // 수정 페이지 이동 @RequestMapping(value = "/modify", method = RequestMethod.GET) public String modifyGET(@RequestParam("article_no") int article_no, Model model) throws Exception { logger.info("modifyGet ..."); model.addAttribute("article", articleService.read(article_no)); return "/article/modify"; } // 수정 처리 @RequestMapping(value = "/modify", method = RequestMethod.POST) public String modifyPOST(ArticleVO articleVO, RedirectAttributes redirectAttributes) throws Exception { logger.info("modifyPOST ..."); articleService.update(articleVO); redirectAttributes.addFlashAttribute("msg", "modSuccess"); return "redirect:/article/list"; } // 삭제 처리 @RequestMapping(value = "/remove", method = RequestMethod.POST) public String remove(@RequestParam("article_no") int article_no, RedirectAttributes redirectAttributes) throws Exception { logger.info("remove ..."); articleService.delete(article_no); redirectAttributes.addFlashAttribute("msg", "delSuccess"); return "redirect:/article/list"; } }
6. JSP 파일 작성
6-1. 기능별 JSP 파일 작성 전 세팅
JSP 파일을 생성하기 위해서 WEB-INF/views 디렉터리에 article 폴더를 생성하고 그 안에 등록(write.jsp), 조회(read.jsp), 수정(modify.jsp), 목록(list.jsp)을 위한 jsp 파일을 생성합니다. 이때 생성한 모든 jsp파일에는 home.jsp 파일의 내용을 그대로 복사해서 붙여넣습니다.
그 후 등록, 조회, 목록, 수정을 위한 jsp파일 내의 <%@ include> 태그의 file 속성 값을 아래와 같이 수정해줍니다.
아래의 코드는 등록을 위한 write.jsp 파일을 수정한 결과입니다.
그리고 head.jsp 파일의 상단에는 JSTL과 EL을 사용하기 위해서 코드를 추가하여 아래와 같이 수정해줍니다.
<%@ page contentType="text/html; charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <c:set var="path" value="${pageContext.request.contextPath}"/> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <title>AdminLTE 3 | Starter</title> <!-- Font Awesome Icons --> <link rel="stylesheet" href="${path}/plugins/fontawesome-free/css/all.min.css"> <!-- Theme style --> <link rel="stylesheet" href="${path}/dist/css/adminlte.min.css"> <!-- Google Font: Source Sans Pro --> <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700" rel="stylesheet"> </head>
추가적으로 Side bar를 사용하기위해 left_column.jsp를 아래와 같이 수정해줍니다.
<aside class="main-sidebar sidebar-dark-primary elevation-4"> <!-- Brand Logo --> <a href="index3.html" class="brand-link"> <img src="${path}/dist/img/AdminLTELogo.png" alt="AdminLTE Logo" class="brand-image img-circle elevation-3" style="opacity: .8"> <span class="brand-text font-weight-light">AdminLTE 3</span> </a> <!-- Sidebar --> <div class="sidebar"> <!-- Sidebar user panel (optional) --> <div class="user-panel mt-3 pb-3 mb-3 d-flex"> <div class="image"> <img src="${path}/dist/img/user2-160x160.jpg" class="img-circle elevation-2" alt="User Image"> </div> <div class="info"> <a href="#" class="d-block">Alexander Pierce</a> </div> </div> <!-- Sidebar Menu --> <nav class="mt-2"> <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false"> <!-- Add icons to the links using the .nav-icon class with font-awesome or any other icon font library --> <li class="nav-item has-treeview menu-open"> <a href="#" class="nav-link active"> <i class="nav-icon fas fa-tachometer-alt"></i> <p> Starter Pages <i class="right fas fa-angle-left"></i> </p> </a> <ul class="nav nav-treeview"> <li class="nav-item"> <a href="${path}/article/write" class="nav-link active"> <i class="far fa-circle nav-icon"></i> <p>Write Page</p> </a> </li> <li class="nav-item"> <a href="${path}/article/list" class="nav-link"> <i class="far fa-circle nav-icon"></i> <p>List Page</p> </a> </li> </ul> </li> <li class="nav-item"> <a href="#" class="nav-link"> <i class="nav-icon fas fa-th"></i> <p> Simple Link <span class="right badge badge-danger">New</span> </p> </a> </li> </ul> </nav> <!-- /.sidebar-menu --> </div> <!-- /.sidebar --> </aside>
6-2. 기능별 JSP 파일 작성
각각의 페이지의 내용은 <!-- Main Content --> 주석 아래의 <div class="container-fluid">태그 내에 작성하면 됩니다.
write.jsp
<div class="col-lg-12"> <form role="form" id="writeForm" method="post" action="${path}/article/write"> <div class="card"> <div class="card-header with-border"> <h3 class="card-title">게시글 작성</h3> </div> <div class="card-body"> <div class="form-group"> <label for="title">제목</label> <input class="form-control" id="title" name="title" placeholder="제목을 입력해주세요"> </div> <div class="form-group"> <label for="content">내용</label> <textarea class="form-control" id="content" name="content" rows="30" placeholder="내용을 입력해주세요" style="resize: none;"></textarea> </div> <div class="form-group"> <label for="writer">작성자</label> <input class="form-control" id="writer" name="writer"> </div> </div> <div class="card-footer"> <button type="button" class="btn btn-primary"><i class="fa fa-list"></i> 목록</button> <div class="float-right"> <button type="reset" class="btn btn-warning"><i class="fa fa-reply"></i> 초기화</button> <button type="submit" class="btn btn-success"><i class="fa fa-save"></i> 저장</button> </div> </div> </div> </form> </div>
list.jsp
<div class="col-lg-12"> <div class="card"> <div class="card-header"> <h3 class="card-title">게시글 목록</h3> </div> <div class="card-body"> <table class="table table-bordered"> <tbody> <tr> <th style="width: 30px">#</th> <th>제목</th> <th style="width: 100px">작성자</th> <th style="width: 150px">작성시간</th> <th style="width: 60px">조회</th> </tr> <c:forEach items="${articles}" var="article"> <tr> <td>${article.article_no}</td> <td><a href="${path}/article/read?article_no=${article.article_no}">${article.title}</a></td> <td>${article.writer}</td> <td><fmt:formatDate value="${article.regDate}" pattern="yyyy-MM-dd a HH:mm"/></td> <td><span class="badge bg-red">${article.viewCnt}</span></td> </tr> </c:forEach> </tbody> </table> </div> <div class="card-footer"> <div class="float-right"> <button type="button" class="btn btn-success btn-flat" id="writeBtn"> <i class="fa fa-pencil"></i> 글쓰기 </button> </div> </div> </div> </div>
추가적으로 redirect되면서 redirectAttributes.addFlashAttribute()를 통해 저장된 데이터를 가지고 요청이 정상적으로 처리되었는지 알려주기 위해 아래와 같이 js코드를 작성해줍니다.
var result = "${msg}"; if (result == "regSuccess") { alert("게시글 등록이 완료되었습니다."); } else if (result == "modSuccess") { alert("게시글 수정이 완료되었습니다."); } else if (result == "delSuccess") { alert("게시글 삭제가 완료되었습니다."); }
read.jsp
<div class="col-lg-12"> <div class="card"> <div class="card-header"> <h3 class="card-title">글제목 : ${article.title}</h3> </div> <div class="card-body" style="height: 700px"> ${article.content} </div> <div class="card-footer"> <div class="user-block"> <img class="img-circle img-bordered-sm" src="${path}/dist/img/user1-128x128.jpg" alt="user image"> <span class="username"> <a href="#">${article.writer}</a> </span> <span class="description"><fmt:formatDate pattern="yyyy-MM-dd" value="${article.regDate}"/></span> </div> </div> <div class="card-footer"> <form role="form" method="post"> <input type="hidden" name="article_no" value="${article.article_no}"> </form> <button type="submit" class="btn btn-primary listBtn"><i class="fa fa-list"></i> 목록</button> <div class="float-right"> <button type="submit" class="btn btn-warning modBtn"><i class="fa fa-edit"></i> 수정</button> <button type="submit" class="btn btn-danger delBtn"><i class="fa fa-trash"></i> 삭제</button> </div> </div> </div> </div>
게시글 조회 페이지의 하단 버튼(목록, 수정, 삭제)를 제어하기 위해 아래와 같이 jQuery코드를 작성해줍니다.
$(document).ready(function () { var formObj = $("form[role='form']"); console.log(formObj); $(".modBtn").on("click", function () { formObj.attr("action", "${path}/article/modify"); formObj.attr("method", "get"); formObj.submit(); }); $(".delBtn").on("click", function () { formObj.attr("action", "${path}/article/remove"); formObj.submit(); }); $(".listBtn").on("click", function () { self.location = "${path}/article/list" }); });
modify.jsp
<div class="col-lg-12"> <form role="form" id="writeForm" method="post" action="${path}/article/modify"> <div class="card"> <div class="card-header"> <h3 class="card-title">게시글 작성</h3> </div> <div class="card-body"> <input type="hidden" name="article_no" value="${article.article_no}"> <div class="form-group"> <label for="title">제목</label> <input class="form-control" id="title" name="title" placeholder="제목을 입력해주세요" value="${article.title}"> </div> <div class="form-group"> <label for="content">내용</label> <textarea class="form-control" id="content" name="content" rows="30" placeholder="내용을 입력해주세요" style="resize: none;">${article.content}</textarea> </div> <div class="form-group"> <label for="writer">작성자</label> <input class="form-control" id="writer" name="writer" value="${article.writer}" readonly> </div> </div> <div class="card-footer"> <button type="button" class="btn btn-primary"><i class="fa fa-list"></i> 목록</button> <div class="float-right"> <button type="button" class="btn btn-warning cancelBtn"><i class="fa fa-trash"></i> 취소</button> <button type="submit" class="btn btn-success modBtn"><i class="fa fa-save"></i> 수정 저장</button> </div> </div> </div> </form> </div>
수정 페이지의 하단 버튼을 제어하기 위해 아래와 같이 jQuery코드를 작성해줍니다.
$(document).ready(function () { var formObj = $("form[role='form']"); console.log(formObj); $(".modBtn").on("click", function () { formObj.submit(); }); $(".cancelBtn").on("click", function () { history.go(-1); }); $(".listBtn").on("click", function () { self.location = "${path}/article/list" }); });
7. 글쓰기, 수정, ,목록, 조회, 삭제 구현 완료
페이지가 다소 부족해 보일 수 있지만 버튼의 모든 기능은 구현했으며 정상적으로 작동하는 것을 확인할 수 있습니다. 그림에서처럼 글을 작성하면 목록 페이지에 리스트되며, DB에도 정상적으로 데이터가 저장되는 것을 확인할 수 있습니다.
8. 포스팅을 마치며
이번 포스팅에서는 게시판의 글쓰기, 수정, 삭제, 조회 기능을 구현해 보았습니다. 다음 포스팅에서는 예외 처리를 다루도록 하겠습니다.
다음포스팅
2020/06/26 - [Spring/게시판 만들기] - [Camel][Spring] 게시판 만들기 #4. 예외처리 (Exception)
글을 읽으시면서 잘못된 부분이나 궁금하신 사항은 댓글 달아주시면 빠른 시일내에 수정 및 답변하도록 하겠습니다.
'Spring > 게시판 만들기' 카테고리의 다른 글
[Camel][Spring] 게시판 만들기 #5-2. 페이징 처리 (Paging) 추가 사항 (0) 2020.06.26 [Camel][Spring] 게시판 만들기 #5-1. 페이징 처리 (Paging) (0) 2020.06.26 [Camel][Spring] 게시판 만들기 #4. 예외처리 (Exception) (0) 2020.06.26 [Camel][Spring] 게시판 만들기 #2. BootStrap 템플릿 적용 (2) 2020.06.25 [Camel][Spring] 게시판 만들기 #1. 프로젝트 생성 & 세팅 (2) 2020.06.25