티스토리 뷰
관리자가 등록한 게시글 리스트를 확인하는 화면으로
로그인하지 않은 사용자, 로그인 사용자 모두 접속이 가능한 화면으로 구성
해당 화면에서는 아래와 같은 기능이 포함되어 있음
- 게시글 리스트 조회
- 게시글 목록 선택 시 상세 내용 조회
- 첨부파일이 포함된 게시글인 경우 첨부파일 다운로드 가능
* 파일 다운로드의 경우 "[프로젝트] 10. Util 클래스 생성 - 세션 / 파일 업로드 경로 (Week 3)" 블로그에서 작성한 서비스 클래스를 호출하여 사용함
[ dataTables api 사용 ] - header.html 파일 내용 수정
표 형태의 데이터 조회를 위해 선택한 api로
클라이언트 (브라우저) 내에서 페이징 기능을 제공하며 디폴트 스타일로 표 형태를 깔끔하게 그려줌
아래 파일을 src\main\resources\static\css 폴더 아래에 복사하여 넣기
경로 )
src\main\resources\static\common\header.html
<!-- dataTables -->
<link href="static/css/dataTables.css" rel="stylesheet">
<!-- 중간 생략 -->
<!-- datatables -->
<script type="text/javascript" src="//cdn.datatables.net/1.10.21/js/jquery.dataTables.min.js"></script>
[ BoardController.java ] - 게시글 조회 화면 컨트롤러
경로 )
src\main\java\com\info\fastboard\board\controller\BoardController.java
package com.info.fastboard.board.controller;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.info.fastboard.board.persistence.BoardMapper;
import com.info.fastboard.main.util.StorageService;
@Controller
public class BoardController {
@Autowired
private StorageService storageService;
@Autowired
private BoardMapper boardMapper;
@GetMapping("/board")
public String main() {
return "board";
}
/**
* 게시글 리스트 조회
* @return
*/
@ResponseBody
@PostMapping("/board/findBoardList")
public List<String[]> findBoardList(@RequestBody Map<String, Object> map) {
List<Map<String, Object>> list = boardMapper.findBoardList(map);
List<String[]> newList = new ArrayList<String[]>();
int idx = 1;
for(Map<String, Object> inMap : list) {
String[] output = new String[inMap.size()];
output[0] = (idx++) + "";
output[1] = (String) inMap.get("CODE_NAME");
output[2] = (String) inMap.get("BOARD_TITLE");
output[3] = (String) inMap.get("USER_NM");
output[4] = (String) inMap.get("INPUT_DATETIME");
output[5] = (String) (inMap.get("BOARD_IDX") + "");
newList.add(output);
}
return newList;
}
/**
* 게시글 상세 조회
* @return
*/
@ResponseBody
@PostMapping("/board/findBoardDetail")
public List<Map<String, Object>> findBoardDetail(@RequestBody Map<String, Object> map) {
List<Map<String, Object>> list = boardMapper.findBoardDetail(map);
return list;
}
/**
* 첨부 파일 다운로드
* @return
* @throws UnsupportedEncodingException
*/
@GetMapping("/board/download")
public ResponseEntity<Resource> serveFile(@RequestParam(value="fileName") String fileName, @RequestParam(value="orgFileName") String orgFileName) throws UnsupportedEncodingException {
Resource file = storageService.loadAsResource(fileName);
return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + URLEncoder.encode(orgFileName, "UTF-8") + "\"").body(file);
}
}
[ BoardMapper.java ]
경로 )
src\main\java\com\info\fastboard\board\persistence\BoardMapper.java
package com.info.fastboard.board.persistence;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BoardMapper {
// 게시글 리스트 조회
List<Map<String, Object>> findBoardList(Map<String, Object> map);
// 게시글 상세 데이터 조회
List<Map<String, Object>> findBoardDetail(Map<String, Object> map);
}
[ BoardMapper.xml ]
경로 )
src\main\resources\mapper\BoardMapper.xml
<?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.info.fastboard.board.persistence.BoardMapper">
<select id="findBoardList" parameterType="hashmap" resultType="hashmap">
SELECT A.BOARD_IDX
, B.CODE_NAME
, A.BOARD_TITLE
, C.USER_NM
, A.BOARD_DESC
, A.USE_YN
, DECODE(A.DUE_DATE, NULL, '', TO_CHAR(A.DUE_DATE, 'yyyy-mm-dd hh24:mi:ss')) AS DUE_DATE
, A.FIXED_YN
, A.INPUT_USER_ID
, DECODE(A.INPUT_DATETIME, NULL, '', TO_CHAR(A.INPUT_DATETIME, 'yyyy-mm-dd hh24:mi:ss')) AS INPUT_DATETIME
, A.MODIFY_USER_ID
, DECODE(A.MODIFY_DATETIME, NULL, '', TO_CHAR(A.MODIFY_DATETIME, 'yyyy-mm-dd hh24:mi:ss')) AS MODIFY_DATETIME
FROM INFO.CRT_BOARD_INFO A
LEFT OUTER JOIN INFO.CRT_CODE_DETAIL B
ON A.BOARD_TYPE = B.DETAIL_ID
AND B.CODE_ID = 'BOARD_TYPE'
LEFT OUTER JOIN INFO.CRT_USER_INFO C
ON A.INPUT_USER_ID = C.USER_ID
WHERE 1=1
AND A.USE_YN = 'Y'
AND NVL(TO_CHAR(A.DUE_DATE, 'RRRR-MM-DD'), '99991231') > TO_CHAR(SYSDATE, 'YYYYMMDD')
<if test="!''.equals(searchType)">
AND A.BOARD_TYPE = #{searchType}
</if>
<if test="!''.equals(searchText)">
AND A.BOARD_TITLE LIKE '%' || #{searchText} || '%'
</if>
ORDER BY A.INPUT_DATETIME DESC
</select>
<select id="findBoardDetail" parameterType="hashmap" resultType="hashmap">
SELECT A.BOARD_IDX
, B.CODE_NAME
, A.BOARD_TITLE
, C.USER_NM
, A.BOARD_DESC
, A.USE_YN
, DECODE(A.DUE_DATE, NULL, '', TO_CHAR(A.DUE_DATE, 'yyyy-mm-dd hh24:mi:ss')) AS DUE_DATE
, A.FIXED_YN
, A.INPUT_USER_ID
, DECODE(A.INPUT_DATETIME, NULL, '', TO_CHAR(A.INPUT_DATETIME, 'yyyy-mm-dd hh24:mi:ss')) AS INPUT_DATETIME
, A.MODIFY_USER_ID
, DECODE(A.MODIFY_DATETIME, NULL, '', TO_CHAR(A.MODIFY_DATETIME, 'yyyy-mm-dd hh24:mi:ss')) AS MODIFY_DATETIME
, D.FILE_NAME
, D.ORG_FILE_NAME
FROM INFO.CRT_BOARD_INFO A
LEFT OUTER JOIN INFO.CRT_CODE_DETAIL B
ON A.BOARD_TYPE = B.DETAIL_ID
AND B.CODE_ID = 'BOARD_TYPE'
LEFT OUTER JOIN INFO.CRT_USER_INFO C
ON A.INPUT_USER_ID = C.USER_ID
LEFT OUTER JOIN INFO.CRT_BOARD_FILE D
ON A.BOARD_IDX = D.BOARD_IDX
WHERE 1=1
AND A.BOARD_IDX = #{boardIdx}
</select>
</mapper>
백엔드 구성이 완료되었으면
프론트엔드 구성을 진행
앞서 진행한 메뉴들과 동일하게 뷰 단의 html 파일과 스크립트 처리를 위한 js 파일을 생성
[ board.html ]
경로 )
src\main\resources\templates\board.html
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org" lang="ko" class="h-100">
<head>
<meta charset="utf-8">
<!-- 반응형 -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<th:bloc th:replace="../static/common/header::header"></th:bloc>
</head>
<body class="d-flex flex-column h-100">
<!-- 상단 메뉴바 -->
<th:bloc th:replace="../static/common/menu::menu"></th:bloc>
<!-- Begin page content -->
<main class="flex-shrink-0">
<div class="container">
<h1 class="mt-5">FAST BOARD 게시판</h1>
<p class="lead">
게시글 등록 및 게시글 조회를 위한 화면입니다.
</p>
<!-- 조회조건 영역 -->
<div id="panel1" class="row p-5 bg-light border rounded-3 mb-5">
<div class="col-sm-4 row">
<label for="txtTitle" class="col-sm-6 col-form-label">제목</label>
<div class="col-sm-6">
<input type="text" class="form-control" id="txtTitle" placeholder="제목으로 검색">
</div>
</div>
<div class="col-sm-4 row">
<label for="txtTitle" class="col-sm-6 col-form-label">카테고리</label>
<div class="col-sm-6">
<select id="cmbBoardType" class="form-control"></select>
</div>
</div>
<div class="col-sm-4 text-end">
<button type="button" class="btn btn-primary" id="btnSearch">조회</button>
</div>
<div style="clear: both;"></div>
<br/>
<div class="com-sm-12">
<table id="tblMain" class="table table-bordered" summary="게시물 목록">
<thead>
<tr>
<th width="8%">NO</th>
<th width="18%">카테고리</th>
<th>제목</th>
<th width="18%">작성자</th>
<th width="18%">등록일자</th>
</tr>
</thead>
</table>
</div>
</div>
<!-- 데이터 영역 -->
<div id="panel2" class="row p-5 bg-light border rounded-3 mb-5" style="display: none;">
<div class="com-sm-12">
<table id="tblSub" class="table table-bordered">
<tbody>
<tr>
<th width="18%">카테고리</th>
<td id="tdBoardType"></td>
<th width="18%">작성자</th>
<td id="tdInputUser"></td>
<th width="18%">등록일자</th>
<td id="tdInputDate"></td>
</tr>
<tr>
<th width="18%">제목</th>
<td id="tdBoardTitle" colspan="5"></td>
</tr>
<tr>
<th width="18%">내용</th>
<td colspan="5" style="height: 640px;">
<pre id="tdBoardDesc" style="height: 100%; overflow-y: auto;"></pre>
</td>
</tr>
<tr>
<th width="18%">첨부파일</th>
<td id="tdBoardFiles" colspan="5"></td>
</tr>
</tbody>
</table>
<br/>
<div class="btn-group">
<button type="button" id="btnBack" value="목록" title="목록" class="btn_b_s">목록</button>
</div>
</div>
</div>
</div>
</main>
<!-- 하단 푸터 영역 -->
<th:bloc th:replace="../static/common/footer::footer"></th:bloc>
<!-- Page Script -->
<script src="static/views/board.js"></script>
</body>
</html>
[ board.js ]
경로 )
src\main\resources\static\views\board.js
/*******************************************************************************
*
* board.js
*
* @author THKIM
* @since 2022
* @DESC 게시판 화면 스크립트
*
******************************************************************************/
(function() {
function Board() {
/*
* private variables
*/
// Korean
var dataTables = null;
var lang_kor = {
"decimal" : "",
"emptyTable" : "데이터가 없습니다.",
"info" : "_START_ - _END_ (총 _TOTAL_ 건)",
"infoEmpty" : "0건",
"infoFiltered" : "(전체 _MAX_ 건 중 검색결과)",
"infoPostFix" : "",
"thousands" : ",",
"lengthMenu" : "_MENU_ 개씩 보기",
"loadingRecords" : "로딩중...",
"processing" : "처리중...",
"search" : "검색 : ",
"zeroRecords" : "검색된 데이터가 없습니다.",
"paginate" : {
"first" : "첫 페이지",
"last" : "마지막 페이지",
"next" : "다음",
"previous" : "이전"
},
"aria" : {
"sortAscending" : " : 오름차순 정렬",
"sortDescending" : " : 내림차순 정렬"
}
};
/*
* 초기화 메소드
*/
function _init() {
// 이벤트
bindEvent();
// 게시글 카테고리 콤보박스 데이터 조회
findCodeList();
// 게시글 리스트 조회
findBoardList();
}
function bindEvent() {
// 조회 버튼 클릭 이벤트
$("#btnSearch").on("click", function() {
toastr.success('데이터 조회중입니다.', '당근게시판');
findBoardList();
});
// 목록 버튼 클릭 이벤트
$("#btnBack").on("click", function() {
$("#panel2").hide();
$("#panel1").show();
});
}
/*
* 코드 정보 리스트 조회
*/
function findCodeList() {
var obj = {
codeId: 'BOARD_TYPE'
}
var html = "";
cfFind("/findCodeList", obj, function(data) {
$.each(data, function(idx, node) {
html += "<option id='" + node.DETAIL_ID + "'>" + node.CODE_NAME + "</option>";
});
$("#cmbBoardType").html("<option id=''>전체</option>" + html);
}, true, "POST");
}
/*
* 게시글 리스트 조회
*/
function findBoardList() {
var obj = {
searchText: $("#txtTitle").val(),
searchType: $("#cmbBoardType option:selected")[0].id
}
cfFind("/board/findBoardList", obj, function(data) {
if(dataTables != null) {
$("#tblMain").DataTable().destroy();
}
dataTables = $("#tblMain").DataTable({
data: data,
columns: [
{ title: "NO", width: "8%" },
{ title: "카테고리", width: "18%" },
{ title: "제목" },
{ title: "작성자", width: "18%" },
{ title: "등록일자", width: "18%" }
],
language: lang_kor,
filter: false,
ordering: false,
lengthChange: false
});
// row 클릭 이벤트
$("#tblMain tbody").on('click', 'td', function () {
var rowIdx = $(this)[0].parentNode.childNodes[0].innerText;
var rowNode = dataTables.row(Number(rowIdx)-1).data();
var boardIdx = rowNode[5];
findBoardDetail(boardIdx);
});
}, true, "POST");
}
/*
* 게시글 상세 조회
*/
function findBoardDetail(boardIdx) {
var obj = {
boardIdx: boardIdx
}
cfFind("/board/findBoardDetail", obj, function(data) {
if(data.length > 0) {
var fileList = "";
$.each(data, function(idx, node) {
if(idx == 0) {
$("#tdBoardType").html(node.CODE_NAME); // 카테고리
$("#tdInputUser").html(node.USER_NM); // 작성자
$("#tdInputDate").html(node.INPUT_DATETIME); // 등록일자
$("#tdBoardTitle").html(node.BOARD_TITLE); // 제목
$("#tdBoardDesc").html(node.BOARD_DESC); // 내용
}
if(node.FILE_NAME != "" && node.FILE_NAME != null) {
fileList += "<a style='display: flex; margin: 5px;' href='/board/download?fileName=" + node.FILE_NAME + "&orgFileName=" + encodeURI(node.ORG_FILE_NAME) + "'>";
fileList += "<img src='static/images/sub/file.jpg' />";
fileList += node.ORG_FILE_NAME + "</a>";
}
});
$("#tdBoardFiles").html(fileList);
$("#panel1").hide();
$("#panel2").show();
}
}, true, "POST");
}
function _finalize() {
}
return {
init : _init,
finalize : _finalize
};
};
var board = new Board();
board.init();
})();
//# sourceURL=board.js
상단 메뉴바에 로그인 여부, 관리자로 로그인 했을 경우에만 보여지는 메뉴들을 설정하기 위해
menu.html 파일을 수정하고 시큐리티 설정을 적용함
[ menu.html ]
경로 )
src\main\resources\static\common\menu.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<meta charset="UTF-8">
<!-- 메뉴 영역을 block으로 묶어서 사용 -->
<th:block th:fragment="menu">
<header>
<!-- Fixed navbar -->
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">FAST BOARD</a>
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
aria-controls="navbarCollapse" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item"><a class="nav-link" href="/">메인</a></li>
<li class="nav-item"><a class="nav-link" href="/board">게시판</a></li>
<li class="nav-item"><a class="nav-link" href="/community">커뮤니티 게시판</a></li>
<li class="nav-item"><a class="nav-link" href="/code" sec:authorize="hasRole('ADMIN')">코드 관리</a></li>
<li class="nav-item"><a class="nav-link" href="/user" sec:authorize="hasRole('ADMIN')">사용자 관리</a></li>
<li class="nav-item"><a class="nav-link" href="/admin" sec:authorize="hasRole('ADMIN')">게시글 관리</a></li>
</ul>
<form class="d-flex" role="search" style="color: white;">
<div sec:authorize="isAuthenticated()">
<span sec:authentication="name"></span>님께서 로그인하셨습니다.
</div>
<a class="btn btn-outline-success" href="/login" sec:authorize="isAnonymous()">로그인</a>
<a class="btn btn-outline-success" href="/logout" sec:authorize="isAuthenticated()">로그아웃</a>
</form>
</div>
</div>
</nav>
</header>
</th:block>
</html>
'SpringBoot 프로젝트' 카테고리의 다른 글
[프로젝트] 13. 커뮤니티 게시판 - 등록, 수정, 댓글 (Week 4) (0) | 2022.12.19 |
---|---|
[프로젝트] 11. 관리자 화면 - 게시글 관리 구현 (Week 4) (0) | 2022.11.30 |
[프로젝트] 참고. gradle build (이클립스) (0) | 2022.11.17 |
- Total
- Today
- Yesterday
- gitlab삭제
- 리액트스터디
- 신입사원태복
- 스프링프로젝트
- 수정사항
- 게시판구현
- BCryptPasswordEncoder
- 프로젝트
- 신입
- 처음시작은어색할지도몰라
- 스터디
- 깃헙배포
- 리액트
- Oracle
- 신입사원태복이
- 스프링
- 프로젝트연습
- 장바구니
- 권한분리
- springboot
- 스프링시큐리티
- 리액트오류
- 공통스크립트
- 태복
- 메이븐설정
- java
- 1주차끝
- centOS7
- 로그인
- 세션
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |