My Project/amigo

[3일차] Admin Page - 게시글 관리

미로910 2024. 11. 25. 09:36
 

[2일차] Admin Page - 회원 관리

CRUD(1) xml(2) model, DTO(3) Interfaces -> Repository(4) Service(5) Controller(6) 화면 구성관리자 페이지에서 어떤 목적과 어떤 기능을 만들건지 잘 설계하고 들어가기!!회원 관리 기능-  회원 조회-  회원 생성- 

maze910.tistory.com

(관리자 페이지 및 설계 방법을 모르시면 회원 관리를 보시길 추천드립니다)


게시글 관리 기능

  • -  게시 조회
  • -  게시글 생성
  • -  게시글 삭제
  • -  게시글 수정

(추가적 기능으로 게시글 부분)

  • - 댓글 조회
  • - 댓글 삭제

에서 조회, 삭제 // 댓글 조회, 댓글 삭제 할 예정 (생성은 게시글 페이지에서 생성합니다)

 

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 &nbsp;
        return contentLocation.replaceAll("<(/?p[^>]*)>", "").replaceAll("&nbsp;", "");
    }


}

 

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