ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Camel][Spring] 게시판 만들기 #6. 게시물 검색 기능 구현
    Spring/게시판 만들기 2020. 6. 27. 03:53

    [Camel][Spring] 게시판 만들기 #6. 게시물 검색 기능 구현

     

    본 게시판 만들기 프로젝트는 더블에스 Devlog Spring-MVC 를 참조하여 작성했음을 알려드립니다. 또한 개인적인 학습을 목적으로한 포스팅이기 때문에 완벽하지 않을 수 있음을 알려드립니다. 문제점이나 궁금한점은 댓글로 남겨주시면 감사하겠습니다. 프로젝트 생성에 앞서 이번 게시판 만들기 프로젝트는 이클립스를 사용하여 구현하였습니다. 

     

    이번 포스팅에서는 게시물 검색 기능을 구현하도록 하겠습니다. 검색 기능은 만들기 프로젝트는 더블에스 Devlog Spring-MVC 를 참조하여 작성했음을 알려드립니다. 또한 개인적인 학습을 목적으로한 포스팅이기 때문에 완벽하지 않을 수 있음을 알려드립니다. 문제점이나 궁금한점은 댓글로 남겨주시면 감사하겠습니다. 프로젝트 생성에 앞서 이번 게시판 만들기 프로젝트는 이클립스를 사용하여 구현하였습니다. 

     

     

     

    이번 포스팅에서는 게시물 검색 기능을 구현하도록 하겠습니다. 검색기능은 검색조건에 부합하는 결과를 목록으로 출력하도록 구현하겠습니다.

     

     페이징 처리와 함께 검색기능을 구현하기 위해 필요한 데이터로는 현재 페이지 번호를 나타내는 page, 페이지 당 출력할 게시물의 수를 나타내는 perPageNum, 그리고 추가적으로 검색 조건과 검색키워드가 필요합니다. page와 perPageNum의 경우에는 Criteria 클래스에 멤버변수로 선언되어 있지만, 검색조건과 검색키워드에 해당하는 변수는 선언한 적이 없기때문에 추가적으로 선언이 필요합니다. 

     

     이러한 문제를 해결하기 위해 Criteria 클래스에 변수를 추가하는 방법도 있습니다. 하지만 이번 포스팅에서는 기존 코드를 변경하지 않고 새로운 클래스를 작성함으로써 기존 코드의 수정을 줄이고, 기존 코드와의 차이점을 확인해보도록 하겠습니다. 

     

    1. SearchCriteria 클래스 생성

    src/main/java/기본패키지/commons/paging 패키지에 SearchCriteria 클래스를 생성하고, 아래와 같이 검색조건과 검색키워드를 멤버변수로 선언해줍니다. 아래처럼 SearchCriteria 클래스에서 새로운 멤버변수를 선언하고 Criteria 클래스를 상속함으로써 기존 코드의 수정을 최소화 할 수 있습니다. 

    package com.cameldev.mypage.commons.paging;
    
    public class SearchCriteria extends Criteria {
    
        	private String searchType;
        	private String keyword;
        
    	public String getSearchType() {
    		return searchType;
    	}
    	public void setSearchType(String searchType) {
    		this.searchType = searchType;
    	}
    	public String getKeyword() {
    		return keyword;
    	}
    	public void setKeyword(String keyword) {
    		this.keyword = keyword;
    	}
    
    	@Override
    	public String toString() {
    		return "SearchCriteria [searchType=" + searchType + ", keyword=" + keyword + "]";
    	}
    }

     

    2. ArticlePagingSearchController 컨트롤러 생성

    페이징 처리를 구현했을 때와 같은 방식으로 기존 컨트롤러에 새로운 메소드를 추가하는 방식이 아니라, 새로운 컨트롤러를 추가해줌으로써 기존 코드와의 차이를 쉽게 확인할 수 있습니다. 컨트롤러는 다른 컨트롤러와 같은 패키지에 생성하도록 합니다. 

    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.ModelAttribute;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    import com.cameldev.mypage.commons.paging.PageMaker;
    import com.cameldev.mypage.commons.paging.SearchCriteria;
    import com.cameldev.mypage.service.ArticleService;
    
    @Controller
    @RequestMapping("/article/paging/search")
    public class ArticlePagingSearchController {
    
        private static final Logger logger = LoggerFactory.getLogger(ArticlePagingSearchController.class);
    
        private final ArticleService articleService;
    
        @Inject
        public ArticlePagingSearchController(ArticleService articleService) {
            this.articleService = articleService;
        }
    
        @RequestMapping(value = "/list", method = RequestMethod.GET)
        public String list(@ModelAttribute("searchCriteria") SearchCriteria searchCriteria,
                           Model model) throws Exception {
    
            logger.info("search list() called ...");
    
            PageMaker pageMaker = new PageMaker();
            pageMaker.setCriteria(searchCriteria);
            pageMaker.setTotalCount(articleService.countArticles(searchCriteria));
    
            model.addAttribute("articles", articleService.listCriteria(searchCriteria));
            model.addAttribute("pageMaker", pageMaker);
    
            return "article/search/list";
        }
    
    }

     

    3. list.jsp 페이지 작성

    /WEB-INF/views/article/search디렉토리를 생성하고, /WEB-INF/views/article/paging 디렉토리 list.jsp를 복사하여 붙어 넣어줍니다. 그리고 페이지 번호를 출력하는 영역의 하단에 아래의 코드를 추가함으로써 검색을 위한 영역을 추가하도록 하겠습니다.

    <div class="card-footer">
      <div class="row">
        <div class="form-group col-sm-2">
          <select class="form-control" name="searchType" id="searchType">
            <option value="n" <c:out value="${searchCriteria.searchType == null ? 'selected' : ''}"/>>:::::: 선택 ::::::</option>
            <option value="t" <c:out value="${searchCriteria.searchType eq 't' ? 'selected' : ''}"/>>제목</option>
            <option value="c" <c:out value="${searchCriteria.searchType eq 'c' ? 'selected' : ''}"/>>내용</option>
            <option value="w" <c:out value="${searchCriteria.searchType eq 'w' ? 'selected' : ''}"/>>작성자</option>
            <option value="tc" <c:out value="${searchCriteria.searchType eq 'tc' ? 'selected' : ''}"/>>제목+내용</option>
            <option value="cw" <c:out value="${searchCriteria.searchType eq 'cw' ? 'selected' : ''}"/>>내용+작성자</option>
            <option value="tcw" <c:out value="${searchCriteria.searchType eq 'tcw' ? 'selected' : ''}"/>>제목+내용+작성자</option>
          </select>
        </div>
          <div class="form-group col-sm-10">
            <div class="input-group">
            <input type="text" class="form-control" name="keyword" id="keywordInput" value="${searchCriteria.keyword}" placeholder="검색어">
            <span class="input-group-btn">
              <button type="button" class="btn btn-primary btn-flat" id="searchBtn">
              	<i class="fa fa-search"></i> 검색
              </button>
            </span>
            </div>
          </div>
      </div>
      <div class="float-right">
        <button type="button" class="btn btn-success btn-flat" id="writeBtn">
        	<i class="fa fa-pencil"></i> 글쓰기
        </button>
      </div>
    </div>

     

     그리고 검색된 목록에서 사용자가 특정게시글을 조회하거나 수정/삭제한 뒤 다시 검색한 게시글 목록으로 돌아가기 위해서는 검색한 목록의 정보(검색조건, 검색키워드)를 유지할 필요가 있습니다. 그래서 정보를 list.jsp에 다시 세팅해주도록 jstl의 <c:out>을 이용했습니다.

     

     

    4. 페이지 URI 이동 수정

     기존의 게시글 목록 페이지 번호와 게시글 제목에는 URI링크가 걸려있는데 검색한 게시글의 목록 정보를 유지하기 위해서는 &searchType=값&keyword=값과 같은 GET방식으로 URI에 붙이는 작업이 필요하게 됩니다. 그렇기 때문에 우리는 PageMaker 클래스에 URI 자동생성 메소드를 추가하게되면 GET방식으로 URI에 붙이는 작업을 수행할 수 있습니다.

     

    4-1. PageMaker 클래스에 메소드 추가

    public String makeSearch(int page) {
    
        UriComponents uriComponents = UriComponentsBuilder.newInstance()
                .queryParam("page", page)
                .queryParam("pagePageNum", criteria.getPerPageNum())
                .queryParam("searchType", ((SearchCriteria) criteria).getSearchType())
                .queryParam("keyword", encoding(((SearchCriteria) criteria).getKeyword()))
                .build();
    
        return uriComponents.toUriString();
    }
    
    private String encoding(String keyword) {
        if (keyword == null || keyword.trim().length() == 0) {
            return "";
        }
    
        try {
            return URLEncoder.encode(keyword, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            return "";
        }
    }

     makeSearch 메소드는 검색조건과 검색키워드에 해당하는 URI 작업에 해당하는 메소드이고, encoding 메소드는 검색키워드의 인코딩 처리를 위한 메소드입니다. 

     

    4-2. list.jsp 페이지 수정

    4-1에서 새로 추가한 메서드를 호출하여 목록 페이지의 번호 링크에 검색조건과 검색키워드가 추가된 URI가 생성되도록 아래와 같이 수정해줍니다. 

    <div class="card-footer">
        <div class="text-center">
            <ul class="pagination">
                <c:if test="${pageMaker.prev}">
                    <li><a href="${path}/article/paging/search/list${pageMaker.makeSearch(pageMaker.startPage - 1)}">이전</a></li>
                </c:if>
                <c:forEach begin="${pageMaker.startPage}" end="${pageMaker.endPage}" var="idx">
                    <li <c:out value="${pageMaker.criteria.page == idx ? 'class=active' : ''}"/>>
                        <a href="${path}/article/paging/search/list${pageMaker.makeSearch(idx)}">${idx}</a>
                    </li>
                </c:forEach>
                <c:if test="${pageMaker.next && pageMaker.endPage > 0}">
                    <li><a href="${path}/article/paging/search/list?${pageMaker.makeSearch(pageMaker.endPage + 1)}">다음</a></li>
                </c:if>
            </ul>
        </div>
    </div>
    <td>
      <a href="${path}/article/paging/search/read${pageMaker.makeSearch(pageMaker.criteria.page)}&articleNo=${article.articleNo}">
        ${article.title}
      </a>
    </td>
    <script>
    $(document).ready(function () {
    	var result = "${msg}";
    	if (result == "regSuccess") {
    	    alert("게시글 등록이 완료되었습니다.");
    	} else if (result == "modSuccess") {
    	    alert("게시글 수정이 완료되었습니다.");
    	} else if (result == "delSuccess") {
    	    alert("게시글 삭제가 완료되었습니다.");
    	}
    	
        $("#searchBtn").on("click", function (event) {
            self.location =
                "${path}/article/paging/search/list${pageMaker.makeQuery(1)}"
                + "&searchType=" + $("select option:selected").val()
                + "&keyword=" + encodeURIComponent($("#keywordInput").val());
        });
    });
    </script>

     

    5. 검색 처리를 위한 영속 계층 구현 

    5-1. ArticleDAO 인터페이스

    아래의 코드를  ArticleDAO 인터페이스에 추가함으로써 검색 결과와 검색결과의 수를 반환하는 추상 메소드를 추가해줍니다. 

    List<ArticleVO> listSearch(SearchCriteria searchCriteria) throws Exception;
    
    int countSearchedArticles(SearchCriteria searchCriteria) throws Exception;

    5-2. ArticleDAOImpl 클래스

    ArticleDAO 인터페이스에 추가한 추상메소드를 오버라이딩하여 구현해줍니다. 

    @Override
    public List<ArticleVO> listSearch(SearchCriteria searchCriteria) throws Exception {
        return sqlSession.selectList(NAMESPACE + ".listSearch", searchCriteria);
    }
    
    @Override
    public int countSearchedArticles(SearchCriteria searchCriteria) throws Exception {
        return sqlSession.selectOne(NAMESPACE + ".countSearchedArticles", searchCriteria);
    }

    5-3. articleMapper.xml

    <select id="listSearch" resultMap="ArticleResultMap">
        <![CDATA[
        SELECT
            article_no,
            title,
            content,
            writer,
            regdate,
            viewcnt
        FROM tbl_article
        WHERE article_no > 0
        ]]>
          <include refid="search"/>
        <![CDATA[
        ORDER BY article_no DESC, regdate DESC
        LIMIT #{pageStart}, #{perPageNum}
        ]]>
    </select>
    
    <select id="countSearchedArticles" resultType="int">
        <![CDATA[
        SELECT
            COUNT(article_no)
        FROM tbl_article
        WHERE article_no > 0
        ]]>
        <include refid="search"/>
    </select>
    
    <sql id="search">
        <if test="searchType != null">
            <if test="searchType == 't'.toString()">
                AND title LIKE CONCAT('%', #{keyword}, '%')
            </if>
            <if test="searchType == 'c'.toString()">
                AND content LIKE CONCAT('%', #{keyword}, '%')
            </if>
            <if test="searchType == 'w'.toString()">
                AND writer LIKE CONCAT('%', #{keyword}, '%')
            </if>
            <if test="searchType == 'tc'.toString()">
                AND (
                    title LIKE CONCAT('%', #{keyword}, '%')
                    OR content LIKE CONCAT('%', #{keyword}, '%')
                )
            </if>
            <if test="searchType == 'cw'.toString()">
                AND (
                    content LIKE CONCAT('%', #{keyword}, '%')
                    OR writer LIKE CONCAT('%', #{keyword}, '%')
                )
            </if>
            <if test="searchType == 'tcw'.toString()">
                AND (
                    title LIKE CONCAT('%', #{keyword}, '%')
                    OR content LIKE CONCAT('%', #{keyword}, '%')
                    OR writer LIKE CONCAT('%', #{keyword}, '%')
                )
            </if>
        </if>
    </sql>

    if문을 사용함으로써 SQL문을 동적으로 상황에 맞게 사용할 수 있도록 하였습니다. 그리고 <include>를 사용함으로써 SQL문을 간결하게 할 수 있습니다. 

     

     

    6. 검색 처리를 위한 Service 계층 구현 

    6-1 ArticleService 인터페이스

    아래의 코드를 ArticleService 인터페이스에 추가해줍니다.

    List<ArticleVO> listSearch(SearchCriteria searchCriteria) throws Exception;
    
    int countSearchedArticles(SearchCriteria searchCriteria) throws Exception;

    6-2 ArticleServiceImpl 클래스

    아래의 코드를 ArticleServiceImpl 클래스에 추가해줍니다.

    @Override
    public List<ArticleVO> listSearch(SearchCriteria searchCriteria) throws Exception {
        return articleDAO.listSearch(searchCriteria);
    }
    
    @Override
    public int countSearchedArticles(SearchCriteria searchCriteria) throws Exception {
        return articleDAO.countSearchedArticles(searchCriteria);
    }

     

     

    7. 검색 처리를 위한 ArticlePagingSearchController 작성

    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.ModelAttribute;
    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.commons.paging.PageMaker;
    import com.cameldev.mypage.commons.paging.SearchCriteria;
    import com.cameldev.mypage.domain.ArticleVO;
    import com.cameldev.mypage.service.ArticleService;
    
    @Controller
    @RequestMapping("/article/paging/search")
    public class ArticlePagingSearchController {
    
        private static final Logger logger = LoggerFactory.getLogger(ArticlePagingSearchController.class);
    
        private final ArticleService articleService;
    	
        @Inject
        public ArticlePagingSearchController(ArticleService articleService) {
            this.articleService = articleService;
        }
        
        @RequestMapping(value = "/write", method = RequestMethod.GET)
        public String writeGET() {
    
            logger.info("search writeGET() called...");
    
            return "article/search/write";
        }
    
        @RequestMapping(value = "/write", method = RequestMethod.POST)
        public String writePOST(ArticleVO articleVO,
                                RedirectAttributes redirectAttributes) throws Exception {
    
            logger.info("search writePOST() called...");
    
            articleService.create(articleVO);
            redirectAttributes.addFlashAttribute("msg", "regSuccess");
    
            return "redirect:/article/paging/search/list";
        }
        
        
        
        @RequestMapping(value = "/list", method = RequestMethod.GET)
        public String list(@ModelAttribute("searchCriteria") SearchCriteria searchCriteria,
                           Model model) throws Exception {
    
            logger.info("search list() called ...");
    
            PageMaker pageMaker = new PageMaker();
            pageMaker.setCriteria(searchCriteria);
            //pageMaker.setTotalCount(articleService.countArticles(searchCriteria));
            pageMaker.setTotalCount(articleService.countSearchedArticles(searchCriteria));
    
            //model.addAttribute("articles", articleService.listCriteria(searchCriteria));
            model.addAttribute("articles", articleService.listSearch(searchCriteria));
            model.addAttribute("pageMaker", pageMaker);
    
            return "article/search/list";
        }
        
     // 조화 페이지
        @RequestMapping(value = "/read", method = RequestMethod.GET)
        public String read(@RequestParam("article_no") int article_no,
                           @ModelAttribute("searchCriteria") SearchCriteria searchCriteria,
                           Model model) throws Exception {
    
            logger.info("search read() called ...");
            model.addAttribute("article", articleService.read(article_no));
    
            return "article/search/read";
        }
        
     // 수정 페이지
        @RequestMapping(value = "/modify", method = RequestMethod.GET)
        public String modifyGET(@RequestParam("article_no") int article_no,
                                @ModelAttribute("searchCriteria") SearchCriteria searchCriteria,
                                Model model) throws Exception {
    
            logger.info("search modifyGet() called ...");
            logger.info(searchCriteria.toString());
            model.addAttribute("article", articleService.read(article_no));
    
            return "article/search/modify";
        }
        
     // 수정 처리
        @RequestMapping(value = "/modify", method = RequestMethod.POST)
        public String modifyPOST(ArticleVO articleVO,
                                 SearchCriteria searchCriteria,
                                 RedirectAttributes redirectAttributes) throws Exception {
    
            logger.info("search modifyPOST() called ...");
            articleService.update(articleVO);
            redirectAttributes.addAttribute("page", searchCriteria.getPage());
            redirectAttributes.addAttribute("perPageNum", searchCriteria.getPerPageNum());
            redirectAttributes.addAttribute("searchType", searchCriteria.getSearchType());
            redirectAttributes.addAttribute("keyword", searchCriteria.getKeyword());
            redirectAttributes.addFlashAttribute("msg", "modSuccess");
    
            return "redirect:/article/paging/search/list";
        }
        
     // 삭제 처리
        @RequestMapping(value = "/remove", method = RequestMethod.POST)
        public String remove(@RequestParam("article_no") int article_no,
                             SearchCriteria searchCriteria,
                             RedirectAttributes redirectAttributes) throws Exception {
    
            logger.info("search remove() called ...");
            articleService.delete(article_no);
            redirectAttributes.addAttribute("page", searchCriteria.getPage());
            redirectAttributes.addAttribute("perPageNum", searchCriteria.getPerPageNum());
            redirectAttributes.addAttribute("searchType", searchCriteria.getSearchType());
            redirectAttributes.addAttribute("keyword", searchCriteria.getKeyword());
            redirectAttributes.addFlashAttribute("msg", "delSuccess");
    
            return "redirect:/article/paging/search/list";
        }
    }

    위의 컨트롤러를 보면 게시글의 검색정보가 유지되도록 메서드들의 매개변수 타입을 SearchCriteria로 변경한것을 확인할 수 있습니다. 

     

    8. 검색 처리를 위한 JSP 파일 수정

    8-1. read.jsp (Main Content부분)

    read.jsp - 변경 전
    read.jsp - 변경 후

     

    8-2. read.jsp (script부분)

    read.jsp의 script부분을 아래와 같이 수정해줍니다. 

    $(document).ready(function () {
    
        var formObj = $("form[role='form']");
        console.log(formObj);
    
        $(".modBtn").on("click", function () {
            formObj.attr("action", "${path}/article/paging/search/modify");
            formObj.attr("method", "get");
            formObj.submit();
        });
    
        $(".delBtn").on("click", function () {
            formObj.attr("action", "${path}/article/paging/search/remove");
            formObj.submit();
        });
    
        $(".listBtn").on("click", function () {
            formObj.attr("action", "${path}/article/paging/search/list");
            formObj.attr("method", "get");
            formObj.submit();
        });
    
    });

     

    8-3. modify.jsp (Main Content부분)

    read.jsp 페이지와 마찬가지로 modify.jsp 페이지에서도 <input>타입의 hidden속성으로 검색조건과 검색키워드를 추가해줍니다. read.jsp 페이지와 거의 동일하게 때문에 수정이 필요한 부분만 아래에 코드로 작성하겠습니다.

    <div class="card-body">
        <input type="hidden" name="article_no" value="${article.article_no}">
        <input type="hidden" name="page" value="${searchCriteria.page}">
        <input type="hidden" name="perPageNum" value="${searchCriteria.perPageNum}">
        <input type="hidden" name="searchType" value="${searchCriteria.searchType}">
        <input type="hidden" name="keyword" value="${searchCriteria.keyword}">
        <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>

     

    8-4. modify.jsp (script부분)

    modify.jsp의 script부분을 아래와 같이 수정해줍니다. 

    $(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/paging/search/list?page=${searchCriteria.page}"
                + "&perPageNum=${searchCriteria.perPageNum}"
                + "&searchType=${searchCriteria.searchType}"
                + "&keyword=${searchCriteria.keyword}";
        });
    
    });

     

    8-5. left_column.jsp 수정

    페이징 처리와 검색기능이 구현된 목록으로 이동하는 버튼을 생성해주기 위해 left_column.jsp 페이지에 아래의 코드를 추가해줍니다. 

    <li class="nav-item has-treeview menu-open">
      <a href="#" class="nav-link">
        <i class="nav-icon fas fa-tachometer-alt"></i>
        <p>
          Paging Search Board
          <i class="right fas fa-angle-left"></i>
        </p>
       </a>
       <ul class="nav nav-treeview">
        <li class="nav-item">
          <a href="${path}/article/paging/search/write" class="nav-link">
            <i class="far fa-circle nav-icon"></i>
            <p>Write Page</p>
          </a>
      	</li>
        <!-- List Paging 목록 버튼 추가 -->
        <li class="nav-item">
          <a href="${path}/article/paging/search/list" class="nav-link">
            <i class="far fa-circle nav-icon"></i>
            <p>List Paging Search Page</p>
          </a>
        </li>
      </ul>
    </li>

     

    9. 최종 구현 모습

    검색 가능 페이징처리 게시판목록

     

     

    검색 결과 목록

     

    10. 포스팅을 마치며

    이번 포스팅에서는 게시물 검색 기능을 구현해보았습니다. 다음 포스팅에서는 댓글 처리에 대해 알아보겠습니다. 

     

    다음포스팅

    2020/06/27 - [Spring/게시판 만들기] - [Camel][Spring] 게시판 만들기 #7-1. 댓글처리 구현

     

    [Camel][Spring] 게시판 만들기 #7-1. 댓글처리 구현

    [Camel][Spring] 게시판 만들기 #7-1. 댓글처리 구현 본 게시판 만들기 프로젝트는 더블에스 Devlog Spring-MVC 를 참조하여 작성했음을 알려드립니다. 또한 개인적인 학습을 목적으로한 포스팅이기 때문

    cameldev.tistory.com

     

    글을 읽으시면서 잘못된 부분이나 궁금하신 사항은 댓글 달아주시면 빠른 시일내에 수정 및 답변하도록 하겠습니다. 

    댓글

Camel`s Tistory.