본문으로 바로가기

게시판 만들기는 CRUD에 적합한 예제여서 좋다. 게시판만 만들 수 있다면 스프링이 어떻게 굴러가는 지 이해할 수 있다. 솔직히 목록보기, 작성하기를 손쉽게 했다면 상세보기, 수정, 삭제는 껌이다. 정말 쉽게 따라 올 수 있다. 왜냐하면 흐름은 반복되기 때문이다. 개인적인 생각으로는 스프링은 흐름 파악이 되면 반이상은 다 된거라고 본다. 그래야 어디가 잘 못 되었고, 어디에 가서 기능을 추가해야 되는 지 한번에 알 수 있기 때문이다. 이런 반복되는 흐름 속에서 내가 원하는 기능을 만들면 되는 거다.



1. 상세보기 버튼

boardList.jsp

1
<td><a href='<c:url value='/board/boardDetail?idx=${bList.IDX }'/>' class="text-dark">${bList.TITLE }</a></td>             
cs

글 목록에서 제목을 클릭하면 제목의 상세보기로 넘어가게 할 것이다. url에 idx(글번호) 파라미터를 같이 넘겨줄 것이다. 이렇게 적게되면, 주소창에도 파라미터 값이 찍히게 나올 것이다. 주소창에 찍히길 원치 않는 다면, hidden으로 숨기는 방법도 있다. 보통 카페글이나 블로그글들의 내용을 보기위해 클릭하게 되면, 주소창에 파라미터가 날라가는 게 보인다. 그래서 나도 따로 숨기진 않을 것이다.


2. BoardController.java ("/board/boardDetail")

1
2
3
4
5
6
7
8
9
    @RequestMapping(value="/board/boardDetail")
    public ModelAndView boardDetail(CommandMap commandMap) throws Exception {
        
        ModelAndView mv = new ModelAndView("/board/boardDetail");
        Map<String, Object> detail = boardService.viewBoardDetail(commandMap.getMap());
        mv.addObject("detail",detail);
        
        return mv;
    }
cs

- 2행: 주소창에 파라미터를 날린 것을 CommandMap 형으로 받았다. (CommandMap에 관한 내용은 HandlerMethodArgument 참고)

- 4행: 상세페이지로 갈 주소이다.

- 5행: 글의 상세 조회를 위한 서비스를 호출한다. 글 목록은 리스트형태지만, 글 상세정보는 한 줄만 가져오면 되기때문에 map 형식이다.

- 6행: 상세페이지로 갈 때, 가져갈 글 목록 리스트이다. "detail" 이란 이름으로 담았다.



3. Service 영역

컨트롤러에서 service를 가져와 만들었으니, 다음은 service영역을 만져야 한다. 여기서 중요한 것은 ServiceImpl에서 2개의 DAO를 호출한다. 글의 상세페이지를 조회할 때, 우리는 두 가지를 수행해야 한다. 첫번째로 글의 상세정보를 가져와야 하고, 두번째로 글의 조회수를 1 증가시켜야 한다. 이 두 가지의 동작을 하나의 트랜젝션에서 수행해야한다. ServiceImpl은 하나의 페이지를 호출할 때 필요한 비즈니스 로직을 묶어서 처리하는 곳이다. 따라서 두 가지 동작을 serviceImpl에 처리하면 된다. 


물론 컨트롤러에서 상세정보를 불러오는 service와 조회수를 증가시키는 sevice를 따로 만들어서 호출해도 상관은 없다. 이렇게 서비스를 따로 만든다면 '수정'을 할 때 서비스를 새로 만들지 않고 상세정보를 불러오는 service를 그대로 사용하면 된다. 하지만 나는 비즈니스 로직을 묶어서 처리해야 된다는 가르침(?)을 받았기 때문에 serviceImpl에서 비즈니스 로직을 묶어서 처리하겠다.


BoardService.java

1
    Map<String, Object> viewBoardDetail(Map<String, Object> map);
cs



BoardServiceImpl.java

1
2
3
4
5
6
7
    @Override
    public Map<String, Object> viewBoardDetail(Map<String, Object> map) {
        // TODO Auto-generated method stub
        boardDAO.updateHitBoard(map);
        Map<String, Object> detail = boardDAO.detailBoard(map);
        return detail;
    }
cs

- 4행: 조회수를 1 증가시키는 동작을 한다.

- 5행: 글의 상세정보를 가져오는 동작을 한다.



4. BoardDAO.java

1
2
3
4
5
6
7
8
9
10
    @SuppressWarnings("unchecked")
    public Map<String, Object> detailBoard(Map<String, Object> map) {
        // TODO Auto-generated method stub
        return (Map<String, Object>) selectOne("board.detailBoard", map);
    }
 
    public void updateHitBoard(Map<String, Object> map) {
        // TODO Auto-generated method stub
        update("board.updateHitBoard", map);
    }
cs

- 1~5행: 글의 상세정보를 가져오는 쿼리를 호출한다.

- 7~10행: 글 조회수를 올려주는 쿼리를 호출한다.



5. boad_sql.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    <select id="detailBoard" resultType="hashmap">
        <![CDATA[
            SELECT
                IDX,
                TITLE,
                CONTENTS,
                HIT_CNT,
                CREA_ID,
                IF(
                    DATE_FORMAT(CREA_DATE, '%Y%m%d') < DATE_FORMAT(now(),'%Y%m%d'),
                    DATE_FORMAT(CREA_DATE, '%Y.%m.%d'),
                    DATE_FORMAT(CREA_DATE, '%H:%i')
                ) as CREA_DATE
            FROM
                TB_BOARD
            WHERE
                IDX = #{ idx}
        ]]>
    </select>
    
    <update id="updateHitBoard">
        <![CDATA[
            UPDATE
                TB_BOARD
            SET
                HIT_CNT = HIT_CNT + 1
            WHERE
                IDX = #{ idx}
        ]]>
    </update>
cs

- 9~13행: 이 부분은 그냥 CREA_DATE만 써서 작성 시간을 가져와도 된다. 나는 글 작성 시간이 오늘이면 시간으로, 이전 시간의 글들은 날짜로 표기하고 싶었다. 그래서 조금은 복잡한 쿼리문을 넣었다. 무슨 말인지 모르겠으면 실행한 부분을 보면 이해가 될 것이다. 굳이 이렇게 안해도 된다면, db에 저장된 CREA_DATE를 가져오면 된다.

- 26행: 조회수를 1씩 올리는 부분이다. 



6. boardDetail.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="container col-md-6">
    <div class="card">
        <div class="card-body">
            <h4 class="card-title mb-3">${detail.TITLE }</h4>
            <h6 class="card-subtitle text-muted mb-4">
                <i class="far fa-user"></i> ${detail.CREA_ID }
                ·
                <i class="far fa-clock"></i> ${detail.CREA_DATE }
                ·
                <i class="fas fa-align-justify"></i> ${detail.HIT_CNT }
            </h6>
            <p class="card-text">${detail.CONTENTS }</p>
        </div>
        <div class="card-body">
            <a href="#" class="btn btn-outline-secondary btn-sm" role="button">수정</a>
            <a href="#" class="btn btn-outline-secondary btn-sm " role="button">삭제</a>
        </div>
        <div class="card-body">
            <a href='<c:url value='/board/boardList'/>' class="btn btn-info" role="button">목록으로</a>
        </div>
    </div>
</div>
cs

- 4~12행: 컨트롤러에서 ModelAndView로 글 상세정보를 담아 detail이라는 이름으로 보냈다. 글 목록과는 조금 다른 것을 확인할 수 있다. 글 목록은 map을 list형태로 가져왔지만, 상세정보는 그냥 map 하나만 가져왔기 때문에 detail을 바로 사용해도 된다. 여기서 주의할 것은 map에 담긴 이름을 정확히 써줘야 한다. 나는 CommandMap이기 때문에 db의 이름과 똑같다. VO를 쓰신 분이라면 VO에 적힌 이름으로 사용하면 된다.

- 14~17행: 아직 구현은 안됐지만 수정과 삭제부분을 미리 만들어 놨다.

- 19행: 목록으로 돌아가는 버튼을 만들었다. 게시글 목록을 불러오는 것과 같기때문에 게시글 불러오는 주소를 적어줬다.



6. 실행



게시판의 목록이다. sql문을 작성할 때 언급한 작성일을 보자. 오늘(2018.10.16) 쓰여진 글들은 시간으로 표시되었고, 그 전의 글들은 날짜로 표시된 것을 확인할 수 있다. 사실 별로 상관없지만, 이렇게 한번 해보고 싶어서 넣어봤다. ㅎㅎ 이제 상세페이지 테스트를 해보자. 나는 조회수가 0인 7번글('fdas')을 눌러볼 것이다.


글의 상세보기에 들어와졌다! 주소창에 보면 해당 글의 글번호가 파라미터로 날라간 것이 보인다. 그리고 조회수도 1 올라간게 보인다. 상세보기를 완성했다. 글 목록을 불러오는 것과 비슷하다. 수정과 삭제를 해도 비슷하게 느껴질 것이다. 마지막으로 '목록으로' 버튼을 눌러서 목록으로 가보자. 


목록에도 7번글의 조회수가 1 올라간 것을 확인할 수 있다.


컨트롤러에서 상세정보를 불러오기전에 조회수를 먼저 업데이트 시켜야 된다고 했다. 콘솔을 보면 그 이유를 알 수 있다. 해당 글의 조회수를 올리고 나서 상세정보를 불러야 지금 본 조회도 포함이 된다. 만약에 정보를 불러오고 조회수를 업데이트 시킨다면, 상세 페이지에서는 조회수가 올라가지 않았을 테고 목록으로 버튼을 눌렀을 때 글 목록에서 보면 조회수가 올라간게 보일 것이다.


끝! 나름의 게시판이 만들어졌다. 마지막단계인 수정과 삭제만 만들면 되는데 이제 수정과 삭제는 더 쉽게 느껴질 것이다.


댓글을 달아 주세요

  1. chris 2019.04.03 13:34

    ineterceptor bean도 등록도 되어 있고 Interceptor 설정하는 페이지 다시 점검해봣는데..
    님처럼 요청 들어오기전에 interceptor가 가로채지 않는건 어떤 설정에서의 문제일까요...?

  2. 익명 2019.05.29 13:40

    비밀댓글입니다