게시판을 운영하다 보면,

사용자가 글 내용을 입력할 때 텍스트박스(TextArea)로 입력하는 게 아니라 웹 에디터를 이용할 때가 많다.

즉, HTML 태그를 사용자가 입력하는 것이다.

하지만 현재의 코드에서는 사용자가 입력한 HTML 코드가 실행되지 않는다. 

글쓰기에서 글 내용에 “<B>테스트</B>”라고 입력한 후 저장해 보자.

글 읽기에서 확인해보면 입력한 내용이 그대로 출력되는 것을 알 수 있다.



사용자가 원하는 것은 굵게 표시된 글자일 것이다.

이렇게 출력되는 것은 Spring에서 HTML 실행을 막아놨기 때문이다.

다음과 같이 출력(out) 테그에 escapeXml="false"를 넣어주면 문제가 해결된다.

<tr>
    <td>내용</td>
    <td><c:out value="${boardInfo.brdmemo}" escapeXml="false"/></td>
</tr>

boardRead.jsp

하지만 이렇게 처리할 경우 악의적인 사용자에 의해 잘못된 스크립트가 삽입되어 실행되는 문제가 발생할 수 있다.

예로, 글쓰기에서 새로운 게시글을 하나 만들어 글 내용에 다음과 같이 입력해 보자.

<script>
    alert(“이게 실행되면 안 되는데”);
</script>

글 읽기로 확인해 보면 자바 스크립트가 실행되어 메시지 창이 나오는 것을 알 수 있다.



HTML은 실행하고 자바 스크립트는 실행하지 못하게 해야 한다.

따라서 <script>를 &lt;script>로 바꾸어 사용자가 입력한 프로그램 코드가 실행되지 않게 만들어 주면 된다.

개발자에 따라 사용자가 입력한 내용을 DB에 저장할 때 처리하기도 하고 보여 줄 때 처리하기도 한다. 

개인적 성향으로 다음과 같이 데이터를 불러와서 보여줄 때 처리하도록 했다.

public class boardVO {
    private String brdno, brdtitle, brdwriter, brdmemo, brddate, brdhit, brddeleteflag;
 ~~ 생략 ~~
    public void setBrdwriter(String brdwriter) {
        this.brdwriter = brdwriter;
    }

    public String getBrdmemo() {
        return brdmemo.replaceAll("(?i)<script", "&lt;script");
    }

boardVO.java






사용자가 게시글을 작성할 때 작성자명, 제목, 내용을 항상 입력해야 한다.

필수 입력이 되어야 하는 것이다.

모든 게시판들이 그런 것은 아니지만 본 예제는 모두 필수 입력이다.

즉, 사용자가 입력한 내용을 확인해서 내용이 있으면 서버로 보내서 저장하고,

그렇지 않으면 안내 메시지를 출력하고 입력하도록 해야 한다.



다음 코드와 같이 fn_formSubmit() 함수를 추가해 준다. 

3개의 필드를 필수로 했기 때문에 3개의 IF문이 사용되었다. 

form1은 폼테그의 이름(name)이고 폼 테그 내의 각 이름을 이용하여 값(value) 있는지 없는지 확인하고 있다.

focus메소드는 커서가 해당 입력상자에서 대기하도록 한다.

~~ 생략 ~~
<head>
<title>board3</title>
<script>
function fn_formSubmit(){
    var form1 = document.form1;
   
    if (form1.brdwriter.value=="") {
        alert("작성자를 입력해주세요.");
        form1.brdwriter.focus();
        return;
    }
    if (form1.brdtitle.value=="") {
        alert("글 제목을 입력해주세요.");
        form1.brdtitle.focus();
        return;
    }
    if (form1.brdmemo.value=="") {
        alert("글 내용을 입력해주세요.");
        form1.brdmemo.focus();
        return;
    }
    form1.submit();   
}
</script>
</head>
<body>
    ~~ 생략 ~~
        <a href="#" onclick="fn_formSubmit()">저장</a>
    </form>   
</body>
</html>

boardForm.jsp

저장 버튼도 기존에 바로 전송하도록(form1.submit()) 되어 있던 것을

코드와 같이 해당 함수에서 확인 후 처리 하도록 바꾸어 준다.


실제 사용에서는 사용자가 공백만 넣을 수도 있다.

따라서 길이를 비교하기 전에 공백을 제거하는 Trim 함수를 사용하면 좋다.

다만, 자바 스크립트의 기본 함수에는 없기 때문에 만들어 써야 한다. 

인터넷을 뒤져서 찾아보거나 패턴으로 만들어 보길 바라고,

차후 다룰 예정인 Jquery는 기본 함수로 제공되기 때문에 여기서는 넘어간다.


다음으로 글쓰기와 관련해서 수정해볼 내용은 board2에서 진행한 글쓰기와 수정을 하나의 컨트롤,

두 개의 서비스로 구현한 것을 다시 하나의 서비스로 구현해 보는 것이다. 

유사한 기능을 하는 서비스를 굳이 두 개로 사용하기 보다는

하나로 사용하는 것이 효율적이고 서비스 개념에도 맞는다고 생각한다.

다음과 같은 기존 코드에서 IF문을 서비스로 옮기면 된다.

@RequestMapping(value = "/board3Save")
public String boardSave(boardVO boardInfo) throws Exception {
    if (param.getBrdno()==null || "".equals(param.getBrdno()))
           boardSvc.insertBoard(boardInfo);
    else boardSvc.updateBoard(boardInfo);

    return "redirect:/board3List";
}

board3Ctr.java

public void insertBoard(boardVO param) throws Exception {
    if (param.getBrdno()==null || "".equals(param.getBrdno()))
           sqlSession.insert("insertBoard3", param);
    else sqlSession.update("updateBoard3", param);
}

board3Svc.java

마지막으로 위와 같이 수정했다면 updateBoard 서비스는 삭제해 준다.





현재 게시판에서는

게시물의 제목이 긴 경우 두 줄 이상으로 출력된다.

이 경우 지저분해 보일 수 있기 때문에

다음 그림과 같이 지정된 글자수로 한 줄로 잘라서 보여주는 경우가 많다.


화면에 보여줄 때 제목의 글자수(길이)가 지정된 글자수보다 많은지 확인하고

많으면 자르는 방식으로 구현하면 된다.

다만, 자르려고 하는 글자가 한글일 때 문제가 발생하게 된다.

영어와 숫자를 제외한 문자는 2바이트(EUC-KR)내지 3바이트(UTF-8)로 처리된다.

따라서 별도의 처리가 필요하지만 본 예제에서는 utiletc.java 파일에 getShortString 함수로 구현되어 있다.

몇 년 전 다른 사람 것을 가져다 사용 중 인데(출처가 기억안나서…) 그냥 사용하길 바란다.

본 예제에서는 데이터를 뿌려줄 때 (boardList.jsp) 글자를 자르는 메소드(getShortString)를 호출해서 사용하도록 했다. 

화면에 맞추어 최대 35만 보이도록 표현했다.

<c:forEach var="listview" items="${listview}" varStatus="status">   
    <c:url var="link" value="board3Read">
        <c:param name="brdno" value="${listview.brdno}" />
    </c:url>       
                                           
    <tr>
        <td>
            <c:out value="${searchVO.totRow-((searchVO.page-1)*searchVO.displayRowCount + status.index)}"/>                   
        </td>
        <td><a href="${link}"><c:out value="${listview.getShortTitle(35)}"/></a></td>
        <td><c:out value="${listview.brdwriter}"/></td>
        <td><c:out value="${listview.brddate}"/></td>
        <td><c:out value="${listview.brdhit}"/></td>
    </tr>
</c:forEach>

boardList.jsp

JSP에서는 boardVO에 있는 함수를 호출(getShortTitle)하고, getShortTitle는 실제 글자를 자르는 함수를 호출한다.

글자를 자르기 위해서는 getShortString 함수를 호출한다는 것만 기억하자.

public class boardVO {
    private String brdno, brdtitle, brdwriter, brdmemo, brddate, brdhit, brddeleteflag;


    public String getShortTitle(Integer len){
        return utiletc.getShortString(brdtitle, len);
    }
  ~~ 생략 ~~
}

boardVO.java


getShortString 함수는 UTF-8로 한글을 자른다.

따라서 영어나 숫자가 있을 때와 없을 때(한글이 많을 때) 출력된 모습이 다른 걸 알 수 있다.

한글이 많을수록 짧게 표현된다. 

UTF-8은 3바이트로 처리하고 화면에 출력되는 것은 2 바이트(EUC-KR)로 처리되어서 그런 것으로 추측되는데

EUC-KR처리하면 바꿔야 하는 다른 것들이 많다(UTF-8이 거의 표준).


이것을 프로그램으로 처리하면 이상과 같이 다소 어려움이 있다.

하지만, 다음과 같이 디자인에서 사용하는 CSS를 이용하면 아주 쉽게 처리할 수 있다.

<td style="border: 1px solid black; max-width: 100px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;">

<a href="${link}"><c:out value="${listview.brdtitle}"/></a>

</td>

이 코드는 글자를 출력하다 지정된 너비(width)를 넘어가면 줄여서 표시하게된다.

자세한 내용은 찾아보길 바란다.


실행후 F12를 눌러서 DOM 탐색기로 보면

다음과 같이 글 제목의 전체 내용이 다 있지만

브라우저에서는 적절하게 잘려서 출력되는 걸 볼 수 있다.

IE 7 이하에서는 적용되지 않지만

대부분 IE 9 이상을 사용하기 때문에 CSS로 사용하는 것이 더 좋을 것 같다.







그림과 같이 사용자가 게시물 중 원하는 게시물을 빨리 찾기 위한 검색 기능을 구현해 본다.


검색의 기본 개념은 데이터 베이스에 저장된 많은 데이터 중에서

특정 필드를 대상으로 어떤 값을 가진 데이터를 조회하도록 DBMS에게 시키는 것이다.

예로, 게시물 중에서 제목이나 내용에 “게시판”이라는 글자가 있는 데이터를 모두 찾는 것이다.

이 처리를 위해서는 


1.    리스트에서 찾고자 하는 필드(제목, 내용)와 찾고자 하는 검색어(게시판)를 입력할 수 있도록 해줘야 한다.

2.    그리고 이 값을 SQL로 전달해서 실행하면 된다.

3.    실제로 리스트를 가져오는 SELECT문에 WHERE만 추가해주면 끝이다.


위 3가지만 수정하면 되지만 선행 작업이 필요하다. 

먼저, 다음 코드와 같이 PageVO를 상속 받아서 SearchVO를 생성한다.

위 그림에서 검색하고자 하는 필드(제목, 내용)을 저장하는 searchType,

사용자가 찾으려는 검색어를 저장하는 searchKeyword를 추가해서 생성한다.

PageVO에 이 두 필드를 추가해서 사용해도 되지만

상속받아서 별도의 클래스를 만드는 이유는

실제 사이트 개발시 많이 사용되기 때문에

페이징과 관계된 기능을 PageVO에 두고 상속받아 사용하는 것이 편리하다.

따라서 PageVO와 pageVO를 사용한 것들을 모두 수정해 줘야 한다.

public class SearchVO extends  PageVO  {

            private String searchKeyword = "";           // 검색 키워드

           private String searchType = "";                // 검색 필드: 제목, 내용 

           private String[] searchTypeArr;                // 검색 필드를 배열로 변환

          

           public String getSearchKeyword() {

                     return searchKeyword;

           }

           public void setSearchKeyword(String searchKeyword) {

                     this.searchKeyword = searchKeyword;

           }

           public String getSearchType() {

                     return searchType;

           }

           public void setSearchType(String searchType) {

                     this.searchType = searchType;

           }

           public String[] getSearchTypeArr() {

                     return searchType.split(",");

           }

 }

/common/SearchVO.java


다음 코드 중 Form 테그의 내용은 검색 기능을 위해 추가된 내용이다.

<%@ page language="java" contentType="text/html; charset=UTF-8"    pageEncoding="UTF-8"%>
<%@ taglib prefix="c"  uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>

~~ 생략 ~~
<c:forEach var="listview" items="${listview}" varStatus="status">   
<c:url var="link" value="board3Read">
    <c:param name="brdno" value="${listview.brdno}" />
</c:url>       
<tr>
    <td>
    <c:out value="${searchVO.totRow-((searchVO.page-1)*searchVO.displayRowCount + status.index)}"/>    </td>
    <td><a href="${link}"><c:out value="${listview.brdtitle}"/></a></td>
    <td><c:out value="${listview.brdwriter}"/></td>
    <td><c:out value="${listview.brddate}"/></td>
    <td><c:out value="${listview.brdhit}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
<c:if test="${searchVO.totPage>1}">
<div class="paging">
    <c:forEach var="i" begin="${searchVO.pageStart}" end="${searchVO.pageEnd}" step="1">
<c:url var="pageLink" value="board3List">
        <c:param name="page" value="${i}" />
        </c:url>
        <c:choose>
            <c:when test="${i eq searchVO.page}">
            <c:out value="${i}"/>
            </c:when>
            <c:otherwise>
                 <a href="${pageLink}"><c:out value="${i}"/></a>
            </c:otherwise>
        </c:choose>
    </c:forEach>
</div>
<br/>
</c:if>
<form id="form1" name="form1"  method="post">
<div>
    <input type="checkbox" name="searchType" value="brdtitle" <c:if test="${fn:indexOf(searchVO.searchType, 'brdtitle')!=-1}">checked="checked"</c:if>/>
    <label class="chkselect" for="searchType1">제목</label>
    <input type="checkbox" name="searchType" value="brdmemo" <c:if test="${fn:indexOf(searchVO.searchType, 'brdmemo')!=-1}">checked="checked"</c:if>/>
    <label class="chkselect" for="searchType2">내용</label>
    <input type="text" name="searchKeyword" style="width:150px;" maxlength="50" value='<c:out value="${searchVO.searchKeyword}"/>' onkeydown="if(event.keyCode == 13) { fn_formSubmit();}">
    <input name="btn_search" value="검색" class="btn_sch" type="button" onclick="fn_formSubmit()" />
</div>
/form>   
</body>
</html>


board3List.jsp

체크 박스(searchType)를 이용해 사용자가 검색하고자 하는 필드를 선택할 수 있도록 했다.

제목(brdtitle)과 내용(brdmemo)이 동일한 이름(searchType)을 가지기 때문에

서버로 전달될 때 searchType 변수에 콤마(,)로 구분되어 전달된다.

즉, 사용자가 제목을 체크하면 searchType에 “brdtitle”,

둘 다 체크하면 “brdtitle, brdmemo”이 저장되어 전달 된다.

폼 테그의 action에 url이 지정되지 않았기 때문에 자기 자신(boardList)을 호출한다.


JSP 파일 시작 부분에 테그 라이브러리 선언(<%@ taglib prefix="fn" uri="~~"%>)을 넣어 주고,

fn:indexOf를 사용하였다.

검색 값을 넣고 검색하면 선택된 필드가 계속 선택된 상태로 보여야 한다.

사용자가 선택한 필드는 searchType에 넣어져 서버로 전송되고,

서버에서 처리 후 다시 JSP 파일로 전달 된다.

JSP에서 HTML 생성시 searchType에 각 체크 박스의 값이 있는지 (IndexOF) 확인하여,

있으면 체크(checked="checked")하여 값이 유지되도록 처리한다.


사용자가 선택한 필드와 검색어가 컨트롤과 서비스를 통해 다음과 같이 전달된다.

@RequestMapping(value = "/board3List")
public String boardList(SearchVO searchVO, ModelMap modelMap) throws Exception {
        searchVO.pageCalculate( boardSvc.selectBoardCount(searchVO) );

        List<?> listview   = boardSvc.selectBoardList(searchVO);
       
        modelMap.addAttribute("listview", listview);
        modelMap.addAttribute("searchVO", searchVO);

        return "board3/boardList";
}

board3Ctr.java

public Integer selectBoardCount(SearchVO param) throws Exception {
    return sqlSession.selectOne("selectBoard3Count", param);
}
public List<?> selectBoardList(SearchVO param) throws Exception {
    return sqlSession.selectList("selectBoard3List", param);
}

board3Svc.java

넘겨 받은 SearchVO의 값을 가지고 WHERE 절을 생성한다.

다만, 데이터 개수를 세는 selectBoard3Count, selectBoard3List 에 동일한 조건식이 적용되어야 한다.

동일한 코드를 두 번 입력하면 귀찮고 유지보수 문제가 있어서

다음과 같이 공통으로 빼고 식별자(includeBoard)를 부여한 뒤

이 것을 각 SQL문에서 include하여 사용하는 것이 좋다.

<sql id="includeBoard">
    WHERE BRDDELETEFLAG='N'
    <if test="searchKeyword!=null and searchKeyword!=''">
        <foreach item="item" index="index" collection="searchTypeArr">
            AND ${item} LIKE CONCAT('%', #{searchKeyword},'%' )
         </foreach>
    </if>
</sql>

<select id="selectBoard3Count" resultType="Integer" parameterType="gu.common.SearchVO">
    SELECT COUNT(*)
      FROM TBL_BOARD
     <include refid="includeBoard"/>
</select>

<select id="selectBoard3List" resultType="gu.board3.boardVO" parameterType="gu.common.SearchVO">
    SELECT BRDNO, BRDTITLE, ~~ 생략 ~~
      FROM TBL_BOARD
     <include refid="includeBoard"/>
     ORDER BY BRDNO DESC
     LIMIT ${rowStart-1}, 10
</select>

baord3.xml

검색어가 없으면 조건을 사용하지 않고

전체 리스트를 보여주기 위해 IF문이 사용되었고,

검색어가 있으면 검색 대상 필드의 개수만큼 돌면서(foreach)서 조건을 생성하도록 했다. 

searchTypeArr는 searchType을 콤마(,)를 기준으로 잘라서 배열로 반환하는 것으로

foreach문은 배열 개수만큼 반복하여 다음과 같은 코드가 생성한다.

AND brdtitle LIKE CONCAT('%', '게시판','%')
AND brdmemo LIKE CONCAT('%', '게시판','%')

위의 조건절은 다소 잘 못 된 것으로 두 개의 필드가 선택된 경우

AND가 아닌 OR가 되어야 한다.

즉, 제목과(AND) 내용에 해당 검색어가 있는 것이 아니라

제목이나(OR) 내용에 해당 검색어가 있는 데이터를 찾는 것이다.

즉 다음과 같이 생성되어야 한다.

Mybatis의 foreach문은 보다 더 다양한 기능을 가지고 있으니

잘 활용해서 다음과 같이 작성해 보길 바란다.

AND (
        brdtitle LIKE CONCAT('%', '게시판','%')
   OR brdmemo LIKE CONCAT('%', '게시판','%')
)

검색 필드로 작성자도 추가해 보자.

페이징 처리는 리스트 형식으로 데이터를 보여주는 모든 페이지에 사용되는 기능이다.

그래서, 자바에서는 PageVO로 만들어서 상속받아서 사용하도록 처리했다.

HTML에서도 공통처리하고 가져다가 사용하면 개발 시간을 단축하는 효과를 볼 수 있다.

공통으로 처리하기 위해서

공통부분을 별도의 JSP 파일로 빼고 해당 파일을 jsp:include로 가져오도록 한다.


하지만 앞서 검색 기능을 추가하였다.

현재는 검색 후 결과에 대하여 페이징이 되지 않는다.

이 문제를 해결하는 방법은

검색 폼의 내용과 페이지 번호가 같이 서버로 전송되어야 한다는 것이다.

즉, 기존에 처리한 페이징의 링크는 URL에 파라메터로 페이지 번호가 넘어가고(board3List?page=1)

검색은 폼의 입력 값들을 따로 만들어서 전송하기 때문에 같이 전송되지 않는다.

따라서, 페이징을 위해

현재 보는 페이지 번호가 폼과 같이 전송되게 하는 것이 가장 쉽다 (반대로 하면 처리가 복잡하다). 

Hidden 필드로 페이지(name=page)를 추가하고

링크가 아닌 자바 스크립트로 호출하게 처리하는 것이다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<c:if test="${searchVO.totPage>1}">
    <div class="paging">
        <c:if test="${searchVO.page>1}">
            <a href="javascript:fnSubmitForm(1);">[처음]</a>
            <a href="javascript:fnSubmitForm(${searchVO.page-1});">[이전]</a>
        </c:if>
               
        <c:forEach var="i" begin="${searchVO.pageStart}" end="${searchVO.pageEnd}" step="1">
            <c:choose>
                <c:when test="${i eq searchVO.page}">
                    <c:out value="${i}"/>
                </c:when>
                <c:otherwise>
                    <a href="javascript:fnSubmitForm(${i});"><c:out value="${i}"/></a>
                </c:otherwise>
            </c:choose>
            <c:if test="${not status.last}">|</c:if>
        </c:forEach>
       
    <c:if test="${searchVO.totPage > searchVO.page}">
        <a href="javascript:fnSubmitForm(${searchVO.page+1});">[다음]</a>
        <a href="javascript:fnSubmitForm(${searchVO.totPage});">[마지막]</a>
    </c:if>
    </div>
    <br/>
       
    <input type="hidden" name="page" id="page" value="" />
       
    <script type="text/javascript">
    function fnSubmitForm(page){
        document.form1.page.value=page;
        document.form1.submit();
    }
    </script>
</c:if>

common/pagingforSubmit.jsp

코드에서 확인 할 수 있듯이

페이징 관련 모든 링크는 fnSubmitForm 함수를 가고자 하는 페이지 번호와 함께 호출한다.

이 함수는 hidden 테그로 생성한 page에 값을 넣고(document.form1.page.value=page)

서버로 전송한다 (submit).

이때 폼 이름이 form1으로 검색에서 사용되는 폼 이름과 같다.


설명이 길었지만 코드는 위와 같이 간단하다.

페이징 처리에 추가된 코드는 이전(page-1), 다음(page+1), 처음으로(1),

마지막(totPage)으로 기능을 넣은 것이다.

화면에 보이는 페이지는 10개의 페이지 이다.

다음 10개 페이지(pageEnd+1), 이전 10개 페이지(pageStart-1)도 만들어 보면 좋을 것이다.


기존 JSP(boardList.jsp)파일에서는 페이징 관련 코드들을 지우고,

폼 테그 안 쪽에 페이징 파일을 가져(include) 오는 코딩을 해 준다.

<form id="form1" name="form1"  method="post">
    <jsp:include page="/WEB-INF/jsp/common/pagingforSubmit.jsp" />
    <div>
        <input type="checkbox" name="searchType" value="brdtitle" <c:if test="${fn:indexOf(searchVO.searchType, 'brdtitle')!=-1}">checked="checked"</c:if>/>
          ~~ 생략 ~~
    </div>
</form> 

boardList.jsp

이상의 내용과 게시판 2에서 정리한 내용을 합하여 정리하면

컨트롤에서 pageVO.pageCalculate를 호출하고

JSP에서 pagingforSubmit.jsp만 호출하면

간단하게 페이징 처리가 된다는 것이다.

페이징 처리는 리스트 기능에 거의 사용되는 것이니 알고 있는 것이 좋다.


+ Recent posts