[게시판 프로젝트] 게시글 리스트

J_Eddy·2021년 12월 28일
1

📌 Board List

카테고리 설정 및 분기

게시판의 컨셉은 중고거래 컨셉이므로 각 카테고리가 존재한다. 해당 카테고리에 쉽게 접근할 수 있도록 사이드바 형식으로 카테고리를 설정하였다.

먼제 카테고리 html을 fragment로 만들었다. 해당 sideCategory.html을 boardList.html에서 include형식으로 다음과 같이 호출하였다. sideCategory에서는 thymeleaf문법인 th:each를 사용하여 해당 list를 돌렸다. th:each는 java의 for문과 같다고 생각한다.
boardList.html

 <th:block th:include="fragments/sidecategory :: sidecategory(${categoryList})"></th:block>

sideCategory.html

<div th:fragment="sidecategory(categoryList)">
    <div id="sidebar-wrapper">
        <ul class="sidebar-nav">
            <li class="sidebar-brand">
                <a>게시판</a>
            </li>
            <li th:each="category : ${categoryList}">
                <a th:text="${category.name}" th:href="@{'/board/list?category='+${category.id}}"></a>
            </li>
        </ul>
    </div>
</div>

위의 boardList.html에 들어가는 sidecategory(${categoryList})는 아래의 서비스 로직을 통해 컨트롤러 단에서 카테고리 리스트를 모델에 담아 전송하였다.

 public List<BoardCategoryDto> getCategoryList() {
        List<BoardCategoryEntity> boardCategoryRepositoryAll = boardCategoryRepository.findAll();
        List<BoardCategoryDto> boardCategoryDtoList = new ArrayList<>();

        for(BoardCategoryEntity category: boardCategoryRepositoryAll){
            boardCategoryDtoList.add(category.toDto());
        }
        return boardCategoryDtoList;
    }

사실 sideCategory를 하면서 가장 까다로웠던 부분은 CSS를 설정하는 부분이었다. 해당 사이드바가 차지하는 공간과 body가 들어가는 공간이 겹쳐지면서 이 부분을 분리하는 것이 까다로웠다. 아래는 CSS의 전문이다. CSS는 아직 공부해야할 부분이 많다.

#sidebar-wrapper {
    position: fixed;
    width: 13%;
    height: 100%;
    background: #EEEEEE;
    overflow-x: hidden;
    overflow-y: auto;
    z-index: 1000;
    top: 0;
    left: 0;
}
.sidebar-nav {
    width: 250px;
    margin: 0;
    padding: 0;
    list-style: none;
}
.sidebar-nav li {
    text-indent: 1.5em;
    line-height: 2.8em;
}
.sidebar-nav li a {
    display: block;
    text-decoration: none;
    color: #343A40;
}
.sidebar-nav li a:hover {
    color: #090708;
    background: rgba(255, 255, 255, 0.2);
}
.sidebar-nav > .sidebar-brand {
    font-size: 1.3em;
    line-height: 3em;
}

게시물 리스트(Toast Grid)

게시물 리스트를 구현할 때 toastGrid를 사용하였다. 사실 처음부터 toastGrid를 사용한것은 아니고, table을 이용해서 구현하였는데 이 부분을 toastGrid로 바꾸어 설정하면 더욱 편리하고 실무에서도 많이 쓰인다고 팀장님 께서 말씀해주셔서 바꾸게 되었다. ToastGrid의 자세한 설명은 다른 포스트를 통해 작성하도록 하겠다.

먼저 toastgrid는 javaScript를 이용해서 표가 그려지게 된다. 처음에는 아래와 같은 방식으로 컬럼을 설정하고 ajax를 통해 표에 집어넣어! 하는 식으로 작성하였다.
하지만 이렇게 하면 toast Grid에서 제공하는 페이징 기능을 사용할 수 없을 뿐더러 만약 100만건의 데이터가 있다고 한다면 전체 리스트를 받아와 뿌려주기 때문에 문제가 생긴다. 즉 서버에서 페이징을 하지않고 프런트에서 페이징을 하니 과부하가 발생 할 여자가 충분하다.


수정 후 dataSource를 이용하여 페이징까지 한번에 구현하였다. toastGrid공식 문서에 나온 페이징 처리의 json 리턴 양식을 보면 다음과 같다.

이에 맞춰 dataSource 를 만들었고 readData라는 메소드를 통해 ajax로 데이터를 가져왔다. 이 때 보내질 값은 해당 게시판의 카테고리 id와 게시글, 작성자, 내용 등의 searchType, 검색어인 keyWord를 param값으로 전송하였다.

const datasource = {
        api: {
            readData: {
                url: '/api/board/list/table',
                method: 'GET',
                initParams: {categoryId: categoryId, searchType: $("#searchType").val(), keyword: $("#keyword").val()}
            }
        }
    }
...
pageOptions: {
            perPage: 5
        }

이후 서비스 로직에서 해당 보드 리스트를 페이지에 따라 요청하였으며 이 과정에서 JPAQuerydsl을 사용하였다. 처음에는 진짜 "Querydsl을 왜쓰지..? 편한 JPA가 있는데;" 라고 생각했지만 사용해 보니Querydsl너무 편하다..

보드 리스트 서비스

/*한페이지 출력 리스트*/
    public List<BoardDto> getBoardList(Long categoryId, int page, int perPage, String searchType, String keyword) {
        BoardCategoryEntity boardCategoryEntity = boardCategoryRepository.findById(categoryId).orElse(null);
        List<BoardEntity> boardEntityList = boardQueryRepository.getBoardList(boardCategoryEntity, page, perPage, searchType, keyword);
        List<BoardDto> boardDtoList = new ArrayList<>();

        for (BoardEntity boardEntity : boardEntityList) {
            boardDtoList.add(boardEntity.toDto());
        }
        return boardDtoList;
    }

한페이지 출력 리스트 Querydsl
DB를 oracle로 사용하여 페이징을 하는과정에서 offset, limit를 사용하였다. offset은 시작부분을, limit는 얼만큼의 갯수를 꺼낼건지 정하는 함수로서 각각 start와 perPage를 인자값으로 넣어주었다.
이 부분에서 눈여겨 봐야할 부분은 where절이다.

사용자가 검색을 할때 검색 serachType이 바뀔수 있다. 예를 들어 만약 사용자가 title로 검색을 한다면 where조건절에는 where(QBoardEntity.boardEntity.title.contains(keyword));처럼 title이 들어와야 하고 작성자로 검색할 때에는 title자리에 writer이 들어와야 한다.

위와 같은 이유 때문에 똑같은 쿼리를 만들 수 없어 아래 where절에 eqSearchType이라는 메소드를 만들어 유동적으로 조건문이 바뀌게 하였다. 해당 메소드는 switch case로 동작하게 설정하였고 인자값으로는 searchTypekeyword가 들어가게 설정하였다.

/*한페이지 출력 리스트*/
    public List<BoardEntity> getBoardList(BoardCategoryEntity boardCategoryEntity, int page, int perPage, String searchType, String keyword){
        int start = (page * perPage) - perPage;
        return queryFactory.selectFrom(QBoardEntity.boardEntity)
                .where(QBoardEntity.boardEntity.category.eq(boardCategoryEntity)
                    .and(eqSearchType(searchType,keyword))
                )
                .orderBy(QBoardEntity.boardEntity.id.desc())
                .offset(start)
                .limit(perPage)
                .fetch();
    }
/*검색조건 분기 함수*/
    private BooleanExpression eqSearchType(String searchType, String keyword){
        if(!keyword.equals("")){
            switch (searchType) {
                case "title":
                    return QBoardEntity.boardEntity.title.contains(keyword);
                case "content":
                    return QBoardEntity.boardEntity.content.contains(keyword);
                case "writer":
                    return QBoardEntity.boardEntity.writer.nickname.eq(keyword);
            }
        }
        return null;
    }

이후 해당 값들을 위에 언급한 리턴 조건에 맞추어 다음과 같이 리턴하였다.

 objectMap.put("result", true);
        objectMap.put("data", dataMap);
        dataMap.put("contents", boardDtoList);
        dataMap.put("pagination", paginationMap);
        paginationMap.put("page", page);
        paginationMap.put("totalCount", total);
        return objectMap;

Toast Grid 표 만들어주는 설정

columns: [
            {header: '번호', name: 'id', width: 'auto',align: 'center'},
            {header: '상태', name: 'status', width: 60, align: 'center',renderer : {type: ColumnConverter}},
            {header: '제목', name: 'title', width: 600},
            {header: '작성자', name: 'writer', width : 'auto', align: 'center'},
            {header: '카테고리', name: 'categoryName', width: 75,align: 'center'},
            {header: '작성일', name: 'createdDate', align: 'center', width: 125},
            {header: '조회수', name: 'hit', align: 'center', width: 60}
        ],

하지만 한가지 문제점이 더 발생하였다. ToastGrid는 Dto를 그대로 리턴하여 해당 column에 뿌려준다. 이때 우리는 status 컬럼은 enum타입으로 sell, soldOut, old와 같이 영어로 저장하였다. 이렇게 되면 표에서는 판매중이 아닌 sell로 표시가 되어 문제가 발생한다.

이를 해결하고자 replace를 이용하여converter 함수를 구현하였다. 위 컬럼 목록 중 상태컬럼을 보면 뒤에 renderer: typel:ColumnConverter라고 써있는게 보일것이다. 이는 아래 메소드를 이용하였다.

여기서 처음에 헷갈렸던 부분은 rendererformatter이다. 어떤것을 사용해야할지 몰랐는데 공식 문서에서 "TOAST UI Grid는 셀 UI를 사용자가 커스터마이징할 수 있도록 커스텀 렌더러를 제공한다. 커스텀 렌더러는 셀 렌더링 작업을 조작하는 데 formatter보다 훨씬 유용하다." 라고 나와있다. 자세한 내용은 아래 링크를 통해 확인 할 수 있다.

Toast Grid Render 공식문서

class ColumnConverter {
    constructor(props) {
        const el = document.createElement('div');

        this.el = el;
        this.render(props);
    }
    render(props) {
        this.el.innerText = converter(props.formattedValue);
    }getElement() {
        return this.el;
    }
}
/*문자 변환 함수*/
function converter(value){
    let result;
    if(value=="sell"){
        result=value.replace(/sell/gi,"[판매중]");
    }else if(value=="soldOut"){
        result=value.replace(/soldOut/gi,"[판매완료]");
    }else
        result=value.replace(/old/gi,"[판매중]");

    return result;
}

최종 결과는 다음과 같다

profile
논리적으로 사고하고 해결하는 것을 좋아하는 개발자입니다.

0개의 댓글