(관리자 페이지 및 설계 방법을 모르시면 회원 관리를 보시길 추천드립니다)
게시글 관리 기능
- - 게시 조회
- - 게시글 생성
- - 게시글 삭제
- - 게시글 수정
(추가적 기능으로 게시글 부분)
- - 댓글 조회
- - 댓글 삭제
에서 조회, 삭제 // 댓글 조회, 댓글 삭제 할 예정 (생성은 게시글 페이지에서 생성합니다)
sql
-- 유저 테이블
create table user_tb (
id int primary key auto_increment,
user_id varchar(50) not null,
name varchar(20) null,
password varchar(1000) not null,
nickname varchar(20) null,
UNIQUE (nickname),
phone_number varchar(20) null,
gender varchar(10) null,
profile Blob,
birth int null,
point int default 0,
user_role int default 0,
online_status boolean default false,
active_status varchar(7) default '활동중',
created_at timestamp default CURRENT_TIMESTAMP
);
-- 게시글 테이블 (board_tb) - 참조되므로 먼저 생성
create table board_tb (
id int primary key auto_increment,
school_id int,
title varchar(50),
content_location varchar(255),
image_location blob,
user_id int,
view_count int default 0,
likes int default 0,
created_at timestamp default CURRENT_TIMESTAMP,
foreign key (school_id) references school_tb(id),
foreign key (user_id) references user_tb(id)
);
-- 댓글 테이블
create table comment_tb (
id int primary key auto_increment, -- pk
board_id int, --게시글 id
user_id int, --유저 id
parent_id int, --대댓글의 부모 게시글 id , 일반 댓글은 null
content_location varchar(255), -- 댓글, 대댓글 내용
created_at timestamp default CURRENT_TIMESTAMP,
foreign key (board_id) references board_tb(id) ON DELETE CASCADE,
foreign key (user_id) references user_tb(id)
);
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!-- mapper DTD 선언 -->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.amigo_project.repository.interfaces.AdminRepository">
<!-- 유저 게시글수 받아오기 -->
<select id="findBoardCount" resultType="com.example.amigo_project.dto.AdminDTO">
SELECT u.*, COUNT(b.id) AS board_count
FROM user_tb u LEFT JOIN
board_tb b ON u.id = b.user_id WHERE
u.id = #{id}
</select>
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!-- mapper DTD 선언 -->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.amigo_project.repository.interfaces.BoardRepository">
<!-- 특정 학교의 게시글 조회 -->
<select id="findBoardsBySchoolId" resultType="com.example.amigo_project.dto.BoardDTO">
SELECT b.id, b.school_id AS schoolId, u.nickname as nickname, b.title, b.content_location AS contentLocation,
b.view_count AS viewCount, b.likes, b.created_at AS createdAt
FROM board_tb as b
JOIN user_tb as u
ON b.user_id = u.id
WHERE school_id = #{schoolId}
ORDER BY b.created_at DESC, b.id DESC
</select>
<!-- 게시글에 대한 상세내용을 가져오면서 user_tb에 있는 nickname도 같이 들고오는 쿼리 -->
<select id="BoardById" parameterType="int" resultType="com.example.amigo_project.dto.BoardDTO">
SELECT
b.id,
b.school_id AS schoolId,
b.user_id AS userId,
b.title,
b.content_location AS contentLocation,
b.image_location AS imageLocation,
b.view_count AS viewCount,
b.likes,
b.created_at AS createdAt,
u.nickname AS nickname
FROM
board_tb b
LEFT JOIN
user_tb u ON b.user_id = u.id
WHERE
b.id = #{boardId};
</select>
<!-- 게시판Id를 기준으로 게시글을 삭제하는 쿼리 -->
<delete id="deleteBoard">
DELETE FROM board_tb WHERE id = #{boardId}
</delete>
<!-- 게시판Id를 기준으로 게시글을 찾는 쿼리 -->
<select id="findBoardId" resultType="com.example.amigo_project.dto.BoardDTO">
SELECT *
FROM board_tb
WHERE id = #{boardId}
</select>
<!-- 댓글을 삭제하면 작동하는 쿼리 -->
<delete id="deleteCommentById" parameterType="int">
DELETE FROM comment_tb WHERE id = #{id}
</delete>
</mapper>
BoardRepository
// 학교id를 기준으로 게시글을 리스트로 가져온다.
public List<byte[]> findImageSearch(int schoolId);
// 게시글에 대한 상세내용을 가져오면서 user_tb에 있는 nickname도 가져온다.
BoardDTO BoardById(@Param("boardId") int boardId);
// 게시글id를 기준으로 게시글을 삭제하는 쿼리
public void deleteBoard(@Param("boardId") int boardId);
// 댓글을 삭제
public void deleteCommentById(@Param("id") int id);
BoardService
/**
* 학교 번호를 기준으로 게시글 리스트를 불러온다.
* @param schoolId
* @return
*/
@Transactional(readOnly = true)
public List<BoardDTO> getBoardsBySchoolId(int schoolId) {
return boardRepository.findBoardsBySchoolId(schoolId);
}
/**
* 게시글을 가져오기 위해 게시글의 id를 기준으로 찾는다.
* @param boardId
* @return
*/
@Transactional(readOnly = true)
public BoardDTO getBoardById(int boardId) {
return boardRepository.BoardById(boardId);
}
/**
* 게시글 삭제 기능
* @param boardId
*/
public void deleteBoard(int boardId) {
boardRepository.deleteBoard(boardId);
}
/**
* 댓글 삭제 하는 기능
* @param id
*/
public void deleteCommentById(int id) {
boardRepository.deleteCommentById(id);
}
BoardDTO
package com.example.amigo_project.dto;
import com.example.amigo_project.repository.model.Board;
import lombok.*;
import java.sql.Blob;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Base64;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class BoardDTO {
private int id;
private int schoolId;
private int userId;
private String nickname;
private String title;
private String contentLocation; // 게시글 내용은 텍스트이므로 String으로 변경
private byte[] imageLocation; // BLOB 필드 (이미지)
private int viewCount;
private int likes;
private Timestamp createdAt;
private String createdAtFormat;
private String image;
// 날짜 포맷
public void getFormattedCreatedAt() {
SimpleDateFormat formatter = new SimpleDateFormat("MM/dd HH:mm");
this.createdAtFormat = formatter.format(createdAt);
}
// 이미지 포맷
public void getFormattedImage() {
// image = Base64.getEncoder().encodeToString(imageLocation);
if (this.imageLocation != null && this.imageLocation.length > 0) {
this.image = Base64.getEncoder().encodeToString(this.imageLocation);
} else {
this.image = null;
}
}
// HTML 태그를 제거하는 메서드
public String removeHtmlTags(String contentLocation) {
if (contentLocation == null) {
return null;
}
// HTML 태그를 모두 제거 (기본적으로 모든 HTML 태그 제거) and remove
return contentLocation.replaceAll("<(/?p[^>]*)>", "").replaceAll(" ", "");
}
}
AdminController
/**
* 게시글 관리
* @param model
* @return
*/
// 게시글 관리
@GetMapping("/board-list")
public String boardPage(Model model){
int schoolId = 1;
List<BoardDTO> boardList = boardService.getBoardsBySchoolId(schoolId);
// 각 BoardDTO에 대해 createdAt 값을 포맷팅 (getFormattedCreatedAt() 호출하는 반복문)
for (BoardDTO board : boardList) {
board.getFormattedCreatedAt();
}
model.addAttribute("boardList", boardList);
return "views/admins/board";
}
// 게시글 상세보기 (댓글 포함)
@GetMapping("/board-list/detail/{id}")
public String boardDetail(Model model, @PathVariable(name = "id") int boardId) {
// 게시글 정보 조회
BoardDTO board = boardService.getBoardById(boardId);
// 각 BoardDTO에 대해 createdAt 값을 포맷팅
board.getFormattedCreatedAt();
model.addAttribute("board", board);
// 해당 게시글의 댓글 조회
List<CommentDTO> commentList = boardService.findCommentsByBoardId(boardId);
// 각 댓글에 대해 createdAt 값 포맷팅
for (CommentDTO comment : commentList) {
comment.getFormattedCreatedAt();
}
model.addAttribute("commentList", commentList);
return "views/admins/boardDetail";
}
// 게시글 삭제하기
@PostMapping("/deleteBoard/{id}")
public String deleteBoard(@PathVariable(name = "id") Integer boardId){
System.out.println("여기로 옵니다.");
boardService.deleteBoard(boardId);
return "redirect:/admin/board-list"; // 게시글 목록 페이지로 이동
// TODO 나중에 오류 페이지 만들기
}
// 댓글 삭제하기
@DeleteMapping("/deleteComment/{id}")
@ResponseBody
public ResponseEntity<?> deleteComment(@PathVariable("id") int id) {
try {
// 댓글 삭제 처리
boardService.deleteCommentById(id);
return ResponseEntity.ok().body("{\"success\": true}");
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("{\"success\": false}");
}
}
화면 구성 (mustache 사용)
[게시글 관리 - 목록]
<body id="page-top">
{{>layout/admin/adminSidebar}}
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">
<div id="content">
<!-- Main Content -->
<div class="container-fluid">
<h1 class="h3 mb-2 text-gray-800">게시글 관리</h1><br><br>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-secondary">게시글 목록</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="boardTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>게시글 번호</th>
<th>게시글 제목</th>
<th>게시글 내용</th>
<th>게시글 작성자</th>
<th>게시글 작성일</th>
<th>게시글 상세보기</th>
</tr>
</thead>
<tbody>
{{#boardList}}
<tr>
<td>{{id}}</td>
<td><a href="/admin/board-list/detail/{{id}}">{{title}}</a></td>
<td class="truncate-content">{{contentLocation}}</td>
<td>{{nickname}}</td>
<td>{{createdAtFormat}}</td>
</td>
<td><a href="/admin/board-list/detail/{{id}}" class="btn btn-primary btn-sm">상세보기</a></td>
</tr>
</tr>
{{/boardList}}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- End of Main Content -->
</div>
</div>
<!-- End of Content Wrapper -->
</div>
<!-- Bootstrap core JavaScript-->
<script src="/vendor/jquery/jquery.min.js"></script>
<script src="/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- Core plugin JavaScript-->
<script src="/vendor/jquery-easing/jquery.easing.min.js"></script>
<!-- Custom scripts for all pages-->
<script src="/vendor/js/sb-admin-2.min.js"></script>
<!-- DataTables JavaScript -->
<script src="https://cdn.datatables.net/1.10.25/js/jquery.dataTables.min.js"></script>
<script>
$(document).ready(function () {
$('#boardTable').DataTable({
"language": {
"search": "검색:",
"lengthMenu": "_MENU_ 개씩 보기",
"info": "총 _TOTAL_개의 게시글",
"paginate": {
"previous": "이전",
"next": "다음"
}
}
});
});
// 내용 10글자만 보이게 하기
document.addEventListener('DOMContentLoaded', function () {
const elements = document.querySelectorAll('.truncate-content');
elements.forEach(function (el) {
const content = el.textContent;
if (content.length > 10) {
el.textContent = content.substring(0, 10) + '...';
}
});
// 제목 8글자만 보이게 하기
const titleElements = document.querySelectorAll('td a');
titleElements.forEach(function (el) {
const title = el.textContent;
if (title.length > 8) {
el.textContent = title.substring(0, 8) + '...';
}
});
});
</script>
</body>
</html>
[게시글 관리 - 상세보기]
<!-- Custom styles for comments -->
<style>
.comment-box {
background-color: #ffecec;
padding: 15px;
margin-bottom: 10px;
border-radius: 5px;
border: 1px solid #f5c6cb;
}
.comment-header {
font-weight: bold;
margin-bottom: 5px;
}
.comment-content {
margin-bottom: 10px;
}
.comment-delete-btn {
background-color: #f5c6cb;
border: none;
padding: 5px 10px;
cursor: pointer;
border-radius: 3px;
}
.comment-delete-btn:hover {
background-color: #e57373;
}
</style>
</head>
<body id="page-top">
{{>layout/admin/adminSidebar}}
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">
<div id="content">
<!-- Main Content -->
<div class="container-fluid">
<h1 class="h3 mb-2 text-gray-800">게시글 관리</h1><br><br>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-secondary">게시글 정보</h6>
</div>
<div class="card-body">
<table class="table table-bordered">
<tbody>
<tr>
<th>작성자</th>
<td>{{board.nickname}}</td>
<th>등록일시</th>
<td>{{board.createdAtFormat}}</td>
<th>조회수</th>
<td>{{board.viewCount}}</td>
</tr>
</tbody>
</table>
<div class="mt-4">
<div class="form-group">
<label for="title">제목</label>
<input type="text" id="title" class="form-control" value="{{board.title}}" readonly>
</div>
<div class="form-group">
<label for="contentLocation">내용</label>
<textarea id="contentLocation" class="form-control" rows="10" readonly>{{board.contentLocation}}</textarea>
</div>
</div>
</div>
</div>
<!-- 댓글 관리 -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-secondary">댓글 관리</h6>
</div>
<div class="comment-body">
{{#commentList}}
<div class="comment-box">
<div class="comment-header">
{{nickname}} ({{createdAtFormat}})
<button class="comment-delete-btn" onclick="deleteComment({{id}})">삭제</button>
</div>
<div class="comment-content">
{{content}}
</div>
</div>
{{/commentList}}
{{^commentList}}
<p>댓글이 없습니다.</p>
{{/commentList}}
</div>
</div>
</div>
<div class="text-center mt-4">
<button class="btn btn-secondary" onclick="deletePost({{board.id}})">삭제하기</button>
<a href="/admin/board-list" class="btn btn-secondary">리스트</a>
</div>
</div>
<!-- End of Main Content -->
</div>
<!-- End of Content Wrapper -->
</div>
<!-- Bootstrap core JavaScript-->
<script src="/vendor/jquery/jquery.min.js"></script>
<script src="/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- Core plugin JavaScript-->
<script src="/vendor/jquery-easing/jquery.easing.min.js"></script>
<!-- Custom scripts for all pages-->
<script src="/vendor/js/sb-admin-2.min.js"></script>
<!-- DataTables JavaScript -->
<script src="https://cdn.datatables.net/1.10.25/js/jquery.dataTables.min.js"></script>
<script>
$(document).ready(function () {
$('#commentTable').DataTable();
});
function deletePost(postId) {
if (confirm('정말로 이 게시글을 삭제하시겠습니까?')) {
$.ajax({
url: '/admin/deleteBoard/' + postId,
type: 'POST',
success: function (result) {
alert('게시글이 삭제되었습니다.');
window.location.href = '/admin/board-list'; // 삭제 성공 후 게시글 목록으로 이동
},
error: function (err) {
alert('삭제에 실패했습니다. 다시 시도해주세요.');
}
});
}
}
function deleteComment(commentId) {
if (confirm('정말로 이 댓글을 삭제하시겠습니까?')) {
fetch('/admin/deleteComment/' + commentId, {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
alert('댓글이 삭제되었습니다.');
window.location.reload(); // 삭제 후 페이지 새로고침
}
})
.catch(err => {
alert('댓글 삭제에 실패했습니다. 다시 시도해주세요.');
});
}
}
</script>
</body>
</html>
게시글 제목, 상세보기 버튼을 누르면 상세보기 페이지로 이동한다.
DataTables 플러그인을 적용시켰습니다.
$(document).ready(function () {
$('#commentTable').DataTable();
});
- 정렬(Sorting): 열을 클릭하면 데이터를 오름차순/내림차순으로 정렬 가능.
- 검색(Search): 테이블 상단에 검색 입력창이 생겨 실시간으로 필터링 가능.
- 페이지네이션(Pagination): 긴 테이블 데이터를 페이지 단위로 나눠 표시.
- 기타: 사용자 정의 데이터 형식, 열 숨기기 등 다양한 옵션 제공.
'My Project > amigo' 카테고리의 다른 글
[2일차] Admin Page - 회원 관리 (2) | 2024.11.06 |
---|---|
[1일차] Admin Page Bootstrap 사용 (2) | 2024.10.18 |