스프링부트 : admin 페이지 구현

조정우·2025년 4월 28일
0

오늘은 : admin 페이지 계정 승인 및 삭제 , 게시물 삭제 를 구현해 볼생각이다


1. admin ROLE_ADMIN 인것을 확인한다 그리고 admin 일경우 전체 유저 및 게시물 정보를 보여준다
2. 승인된 유저 승인되지 않은 유저를 나눠 준다
3. 게시물을 따로 보여준다

1. Controller AdminPage

  • 현재 로그인 정보를 Authentication 으로 가져온다 이휴 role 값을 가져와서 anyMatch로 만약 로그인 정보가 ROLE_ADMIN 일경우에 true를 반납
  • memberList -> 현재 가입한 모든 유저들의 정보를 가져온다
  • boardList -> 현재 까지 모든 게시물 정보를 가져온다 최신순으로
  • CommentMap -> 게시글에 달린 댓글
  • commentMemberMap -> 댓글을 작성한 유저의 정보
  • commentImagesMap -> 댓글에 작성된 댓글 이미지 split "," 끊어서 가져오기
  • connectUserCount ,InProgress,InCompleted -> 전체 유저의 수 , 승인된 승인되지 않은
@GetMapping("/mypage")
    public String mypage(Principal principal, Model model) {

        // 헌재 로그인한 유저의 인증정보를 가져온다
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user = (User) authentication.getPrincipal();
        Collection<? extends GrantedAuthority> authorities = user.getAuthorities(); // roles()말고 authorities()

        // 로그인 유저의 role 상태를 체크해서 ADMIN 이면 true 를 반납 anyMatch -> 있는경우 true
        boolean isAdmin = authorities.stream()
                .anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN")); // "ROLE_ADMIN"으로 체크

        Map<Long, Member> boardMembersMap = new HashMap<>(); // 게시물 작성자 정보를 가져오기
        Map<Long, List<String>> boardImagesMap = new HashMap<>(); // 게시물 이미지 정보 "," 로 가져오기

        // 로그인 한 유저가 admin 인지 체크한다
        if (isAdmin) {
            String username = principal.getName();
            Member member = memberRepository.findByUserId(username)
                    .orElseThrow(() -> new RuntimeException("해당 유저를 찾을 수 없습니다"));

            // 모든 유저들의 정보를 가져오기
            List<Member> memberList = memberRepository.findAllByOrderByUserAtDesc();
            
            // 게시물에 대한 댓글 가져오기
            Map<Long,List<Comment>> CommentMap = new HashMap<>();

            // 댓글 에 대한 유저 정보 가져오기
            Map<Long,Member> commentMemberMap = new HashMap<>();

            // 댓글에 대한 이미지 정보 가져오기
            Map<Long,List<String>> commentImagesMap = new HashMap<>();
            
            // 현재 로그인 중인 유저들의 숫자만 가져오기
            Long connectUserCount = memberList.stream()
                    .filter(memberC -> "connect".equals(memberC.getUserStatus()))
                    .count();
            
            // 현재 승인 되지 않은 유저들의 수
            Long InProgress = memberList.stream()
                    .filter(memberA -> "In-progress".equals(memberA.getUserAction()))
                    .count();
            // 현재 승인 된 유저들의 수
            Long InCompleted = memberList.stream()
                    .filter(memberA -> "In-completed".equals(memberA.getUserAction()))
                    .count();


            // 모든 유저들의 게시물 가져오기
            List<Board> boardList = boardRepository.findAllByOrderByBoardIdxDesc();
            for (Board board : boardList) {
                Member boardMemberList = memberRepository.findByIdx(board.getBoardUseridx()).orElseThrow(() -> new RuntimeException("해당유저를 찾을수없습니다"));
                boardMembersMap.put(board.getBoardIdx(), boardMemberList);

                List<String> boardImageList = new ArrayList<>();
                if (board.getBoardImages() != null && !board.getBoardImages().isEmpty()) {
                    boardImageList = Arrays.asList(board.getBoardImages().split(","));
                }
                boardImagesMap.put(board.getBoardIdx(), boardImageList);

                // 게시판에 달린 댓글
                List<Comment> commentList = commentRepository.findByCommentBoardidx(board.getBoardIdx());
                CommentMap.put(board.getBoardIdx(), commentList);
                for (Comment comment : commentList) {
                    // 댓글에 달린유저 정보
                    Member commentMember = memberRepository.findByIdx(comment.getCommentUseridx()).orElseThrow(() -> new RuntimeException("해당유저를 찾을수없습니다"));
                    commentMemberMap.put(comment.getCommentIdx(), commentMember);
                    
                    // 댓글에 달린 이미지
                    List<String> commentImageList = new ArrayList<>();
                    if (comment.getCommentImages() != null && !comment.getCommentImages().isEmpty()) {
                        commentImageList = Arrays.asList(comment.getCommentImages().split(","));
                    }
                    commentImagesMap.put(comment.getCommentIdx(), commentImageList);
                }
            }


            model.addAttribute("memberList", memberList); // 모든 유저의 정보
            model.addAttribute("boardList", boardList); // 게시물 정보
            model.addAttribute("writers", boardMembersMap); // 게시물 작성자 정보
            model.addAttribute("boardImages", boardImagesMap); // 게시물 이미지 정보
            model.addAttribute("connectMember", connectUserCount); // 현재 로그인 중인 유저 수
            model.addAttribute("InProgress", InProgress); // 현재 유저의 수를 가져온다
            model.addAttribute("InCompleted", InCompleted); // 현재 유저의 수를 가져온다
            model.addAttribute("commentList",CommentMap); // 댓글 정보
            model.addAttribute("commentwirtes",commentMemberMap); // 댓글을 작성한 유저의 정보
            model.addAttribute("commentImages",commentImagesMap); // 댓글에 작성된 이미지정보

        }

        return "mypage";
    }

2. 유저 승인 하기

  • approve-btn 승인되지 않은 유저들의 버튼 클릭시 각 해당하는 idx 값을 가져옴
  • fetch , application/json -> JSON 형태로 파싱해준다 (데이터를 원하는 형태로 변환 하는 과정)
  • /admin/approveUser -> POST로 데이터를 변환해서 받아줄 주소
  • body JSON.stringify, XMLHttpRequest 를 이용해 서버로 데이터를 전송 Controller 에서 Map 형태로 받는다
document.querySelectorAll(".approve-btn").forEach(btn => {
        btn.addEventListener("click", function () {
            const setIdx = this.dataset.idx;
            const currentBtn = this; // 버튼 참조 저장

            const isConfirmed = confirm("정말 이 유저를 승인하시겠니까?");
            if (!isConfirmed) {
                return;
            }

            fetch(`/admin/approveUser`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({userIdx: setIdx}) // userIdx 값을 Controller 보내줌
            })
                .then(response => response.json()) // 서버 응답을 JSON 으로 받기
                .then(data => {
                    console.log("서버 응답 :", data);
                    // 버튼 텍스트를 "승인 완료"로 변경
                    currentBtn.textContent = "승인 완료";
                    currentBtn.classList.add("un-approve-btn");
                    // 버튼 비활성화 (원하는 경우)
                    currentBtn.disabled = true;

                    // 실시간 승인 유저 업데이트
                    if (data.InProgressCount !== undefined) {
                        document.getElementById('InProgress').textContent = data.InProgressCount;
                    }
                    if (data.InCompletedCount !== undefined) {
                        document.getElementById('InCompleted').textContent = data.InCompletedCount;
                    }
                    // 기존 In-progress에서 삭제
                    const memberElement = btn.closest('.In-progress');
                    if (memberElement) {
                        memberElement.remove();
                    }

                    // In-completed 영역에 새로 추가
                    const completedContainer = document.querySelector('.In-completed');
                    if (completedContainer) {
                        const newMemberDiv = document.createElement('div');
                        newMemberDiv.className = 'In-completed'; // 스타일 클래스 이름

                        newMemberDiv.innerHTML = `
                    <div class="In-completed" th:each="member : ${memberList}"
                         th:if="${member.userAction == 'In-completed'}">
                        <a th:href="@{${'/profile/' + member.idx}}">
                            <div class="user-image">
                                <img th:src="@{${'/profile-images/' + member.userImage}}" alt="" width="50px"
                                     style="border-radius: 50%">
                            </div>
                        </a>
                        <div class="user-info">
                            <div class="user-info-inner">
                                <span class="user-name" th:text="${member.userName}"></span>
                                <span class="user-id" th:text="${'@'+member.userId}"></span>
                                <a th:href="@{${'/message/' + member.idx}}">
                                    <img src="/local-images/conversation.png" alt="" width="20px">
                                </a>
                            </div>
                            <div class="user-action">
                                <button class="un-approve-btn" disabled>승인완료</button>
                                <button class="delete-btn" th:data-idx="${member.idx}">계정삭제 하기</button>
                            </div>
                        </div>
                    </div>
                `;
                        completedContainer.appendChild(newMemberDiv);
                    }
                });
        });
    });

3. Controller PostMapping 승인API

-@RequestBody Map<String, Object> requestData -> 클라이언트에서 보는 JSON 데이터를 Map 형태로 받겠다는 의미이다 그래서 받으면 Map 객체로 변환 된다

  • Map<String,Object> Strign(Key) , Value(Object) 키 값 쌍을 이루는 컬렉션 useridx 는 Key 가 되고 값이 Object로 저장이 된다 useridx : 1이런식으로
  • 그래서 받은 useridx 값으로 해당하는 memberreposiotory 에서 userAction 찾아서 In-completed 로 변경이후 저장
  • InProgress , InCompleted는 실시간으로 승인되지 않은 유저들의 수를 처리해주기위해서
@PostMapping("/admin/approveUser")
    public ResponseEntity<Map<String, Object>> approveUser(@RequestBody Map<String, Object> requestData) {
        Map<String, Object> map = new HashMap<>();

        Long useridx = Long.valueOf(requestData.get("userIdx").toString());

        Member member = memberRepository.findByIdx(useridx).
                orElseThrow(() -> new RuntimeException("해당 유저를 찾을수없습니다"));

        member.setUserAction("In-completed");
        memberRepository.save(member);

        // 현재 승인 되지 않은 유저
        Long InProgress = memberRepository.countByUserAction("In-progress");
        map.put("InProgressCount", InProgress);

        // 현재 승인 된 유저 수
        Long InCompleted = memberRepository.countByUserAction("In-completed");
        map.put("InCompletedCount", InCompleted);

        return ResponseEntity.ok(map);
    }

4. HTML 구현 부

전체적인 부부은 앞에서 작성한 main 이랑 같다 아니 거기서 대부분 글을 들고왔다
이미지 슬라이드 부터 댓글 팝업 댓글 이미지 슬라이드 , 해당 유저 클릭시 및 이미지 클릭시 해당 페이지로 이동

<div class="admin-container">
        <!-- 상단 정보 -->
        <div class="admin-info">
            <div>전체 회원 수: <span id="total-users" th:text="${memberList.size()}"></span></div>
            <div>전체 게시물 수: <span id="total-posts" th:text="${boardList.size()}"></span></div>
            <div>현재 로그인 중인 회원 수: <span id="online-users" th:text="${connectMember}"></span></div>
        </div>

        <!-- 왼쪽과 오른쪽 div로 나누기 -->
        <div class="admin-content">
            <!-- 왼쪽: 유저 승인 정보 -->
            <div class="user-info" id="user-info">
                <div class="unapproved-users">
                    <h3>
                        <span>승인되지 않은 유저 :</span>
                        <span th:text="${InProgress}" class="action-count" id="InProgress"></span>
                    </h3>
                    <div class="In-progress" th:each="member : ${memberList}"
                         th:if="${member.userAction == 'In-progress'}">
                        <div class="user-image">
                            <img th:src="@{${'/profile-images/' + member.userImage}}" alt="" width="50px"
                                 style="border-radius: 50%">
                        </div>
                        <div class="user-info">
                            <div class="user-info-inner">
                                <span class="user-name" th:text="${member.userName}"></span>
                                <span class="user-id" th:text="${'@'+member.userId}"></span>
                            </div>
                            <div class="user-action">
                                <button class="approve-btn" th:data-idx="${member.idx}">승인하기</button>
                                <button class="delete-btn" th:data-idx="${member.idx}">계정삭제 하기</button>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="approved-users">
                    <h3>
                        <span>승인된 유저 :</span>
                        <span th:text="${InCompleted}" id="InCompleted" class="action-count"></span>
                    </h3>
                    <!-- 승인된 유저 목록 -->
                    <div class="In-completed" th:each="member : ${memberList}"
                         th:if="${member.userAction == 'In-completed'}">
                        <a th:href="@{${'/profile/' + member.idx}}">
                            <div class="user-image">
                                <img th:src="@{${'/profile-images/' + member.userImage}}" alt="" width="50px"
                                     style="border-radius: 50%">
                            </div>
                        </a>
                        <div class="user-info">
                            <div class="user-info-inner">
                                <span class="user-name" th:text="${member.userName}"></span>
                                <span class="user-id" th:text="${'@'+member.userId}"></span>
                                <a th:href="@{${'/message/' + member.idx}}">
                                    <img src="/local-images/conversation.png" alt="" width="20px">
                                </a>
                            </div>
                            <div class="user-action">
                                <button class="un-approve-btn" disabled>승인완료</button>
                                <button class="delete-btn" th:data-idx="${member.idx}">계정삭제 하기</button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>

            <!-- 오른쪽: 게시물 정보 -->
            <div class="post-info" id="post-info">
                <h3>게시물 정보</h3>
                <!-- 게시물 목록 (스크롤 가능) -->
                <div class="post-list">
                    <div class="post" th:each="boards : ${boardList}" style="position: relative">
                        <!-- 삭제 버튼 -->
                        <div class="delete-section">
                            <div class="delete-section-inner" onclick="toggleDeleteForm(this)">
                                <span class="delete-span">...</span>
                            </div>
                        </div>

                        <!-- 삭제 폼 (처음엔 숨김) -->
                        <div class="delete-form" th:data-idx="${boards.boardIdx}">
                            <span>해당 게시물 삭제</span>
                        </div>

                        <div class="post-header">
                            <a th:href="@{'/profile/' + ${writers[boards.boardIdx].idx}}">
                                <img class="profile-img"
                                     th:src="@{'/profile-images/' + ${writers[boards.boardIdx].userImage}}"
                                     alt="프로필">
                            </a>
                            <div class="board-user-info">
                                <span class="user-name" th:text="${writers[boards.boardIdx].userName}"></span>
                                <span class="user-id" th:text="${'@' +writers[boards.boardIdx].userId}"></span>
                            </div>
                            <span class="time" th:data-time="${boards.boardAt}"></span>
                        </div>

                        <a class="boardDetail" th:href="@{${'/board/' + boards.boardIdx}}">
                            <div class="post-content" th:text="${boards.boardContent}">
                            </div>
                            <div class="board-img-list">
                                <div class="board-img-list-inner">
                                    <button class="board-slide-left">&#10094;</button>
                                    <div class="img-list-inner board-list"
                                         th:each="imgUrl : ${boardImages[boards.boardIdx]}">
                                        <img th:src="@{${'/board-images/' + imgUrl}}" alt="게시물 이미지">
                                    </div>
                                    <button class="board-slide-right">&#10095;</button>
                                </div>
                            </div>
                        </a>
                        <div class="post-actions">
                            <span class="toggle-comments">💬 <span th:text="${boards.boardComent}"></span></span>
                            <span>
                                <span class="boardLike-btn" th:data-board-idx="${boards.boardIdx}">❤️</span>
                                <span class="board-like-count" th:text="${boards.boardLike}"></span>
                            </span>
                            <span>👀 <span th:text="${boards.boardView}"></span></span>
                        </div>

                        <!--     댓글창 팝업     -->
                        <div class="comment-section" th:attr="data-board-id=${boards.boardIdx}"
                             style="border-top: none;">
                            <!--  댓글 목록 가져오기   -->
                            <div th:each="comment : ${commentList[boards.boardIdx]}" class="comment-bar">
                                <div>
                                    <div class="post-header">
                                        <a th:href="@{${'/profile/' + commentwirtes[comment.commentIdx].idx}}">
                                            <img class="profile-img"
                                                 th:src="@{${'/profile-images/' + commentwirtes[comment.commentIdx].userImage}}"
                                                 alt="프로필">
                                        </a>
                                        <div class="user-info">
                                            <span class="user-name"
                                                  th:text="${commentwirtes[comment.commentIdx].userName}"></span>
                                            <span class="user-id"
                                                  th:text="${'@' +commentwirtes[comment.commentIdx].userId}"></span>
                                        </div>
                                    </div>
                                    <div class="comment-main">
                                        <div class="post-content comment-content" th:text="${comment.commentContent}">
                                        </div>
                                        <div class="board-img-list comment-img-list">
                                            <div class="board-img-list-inner comment-img-list-inner">
                                                <button class="board-slide-left">&#10094;</button>
                                                <div class="comment-img-list-inner-container">
                                                    <div class="img-list-inner"
                                                         th:each="imgUrl : ${commentImages.get(comment.commentIdx)}">
                                                        <img th:src="@{${'/comment-images/' + imgUrl}}" alt="게시물 이미지">
                                                    </div>
                                                </div>
                                                <button class="board-slide-right">&#10095;</button>
                                            </div>
                                        </div>
                                        <div class="post-actions">
                                            <span class="toggle-comments">💬 <span
                                                    th:text="${comment.commentRelay}"></span></span>
                                            <span>
                                <span class="comment-like-btn" th:data-comment-idx="${comment.commentIdx}">❤️</span>
                                <span class="comment-like-count" th:text="${comment.commentLike}"></span>
                            </span>
                                            <span>👀 <span th:text="${comment.commentView}"></span></span>
                                        </div>
                                    </div>
                                </div>
                            </div>

                            <div class="board-detail-src">
                                <a th:href="@{${'/board/' + boards.boardIdx}}">댓글 더보기</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

!따로 게시물 삭제 및 유저 삭제는 코드로 보여주지 않았다 유저 승인이랑 너무 유사해서..

전체적인 이미지 -

게시물 삭제 이미지 :

승인 하기 및 : 유저 삭제 하기 :

profile
)개발( 마구잡이로 글쓰기

0개의 댓글