Springboot 게시물 리스트 및 이미지 슬라이드

조정우·2025년 4월 4일
0

오늘의 할일 : 게시물 리스트 가져오기 및 이미지 슬라이드 -> Controller 에서 가져와야 할것 (게시물 리스트 , 현재 로그인 유저 아이디 , 게시물을 작성한 유저의 정보, 각 게시물에 해당하는 이미지)

1. MainController - index.html 을 나타냄

member = memberRepository.findByUserId(username) - 현재 로그인한 유저의 정보를 가져옴
List boards = boardRepository.findAll() - 현재 등록된 모든 게실물 가져오기
Map<Long,Member> boardMembers = new HashMap<>() - 게시물을 작성한 유저의 정보를 가져옴
Map<Long, List> boardImages = new HashMap<>() - 게시물에 해당하는 이미지 값을 가져옴

Map으로 원하는 정보를 put으로 내보내줘서 Model로 만들어준다
Member writer = memberRepository.findByIdx(useridx) 일단 member DB에서 board안에 저장된 useridx 값을 가져와서 비교 해서 있으면 -> boardMembers.put(board.getBoardIdx(), writer) 그 정보값을 put으로 model로 넣어준다

  • 나머지 Map도 같은 형식으로 넣어줬다
  • Repository에서 Long 값으로 하나더 선언 해준다 기존 findById는 Integer값이여서 새로 선언 해줬다 idx 값이 다르면 하나더 만들어주는게 좋다
// 메인페이지에서 게실물 작성하기
@GetMapping("/")
public String index(Model model,Principal principal) {

    Member member = null;

    if (principal != null) {  // 사용자가 로그인한 경우
        String username = principal.getName();
        member = memberRepository.findByUserId(username)
                .orElseThrow(() -> new RuntimeException("유저 정보를 찾을 수 없습니다"));
    }

    List<Board> boards = boardRepository.findAll();

    // 게시물을 작성한 유저의 정보를 가져오기
    Map<Long,Member> boardMembers = new HashMap<>();
    Map<Long, List<String>> boardImages = new HashMap<>();

    for (Board board : boards){
        Long useridx = board.getBoardUseridx();
        Member writer = memberRepository.findByIdx(useridx).orElseThrow(() -> new RuntimeException("게지물 작성자를 찾을 수 없습니다"));
        boardMembers.put(board.getBoardIdx(), writer);

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

    model.addAttribute("member", member); // 현재 로그인한 유저의 정보
    model.addAttribute("boardList", boards); // 모든 게실물
    model.addAttribute("writers", boardMembers); // 게시물 작성자의 정보
    model.addAttribute("boardImages", boardImages);
    model.addAttribute("board", new Board()); // 새 게시물 작서용 객체
    return "index";
}

2. HTML 구현

th:if="{#authorization.expression('isAuthenticated()')}" , isAnonymous() 로 로그인 유무를 체크 th:each ="boards : ${boardList}" 로 게시물 반복을 해준다 <br> "{writers[boards.boardIdx].userName" 으로 게시물을 작성한 유저의 정보를 가저온다

<div class="post-create">
      <div  th:if="${#authorization.expression('isAuthenticated()')}">
          <form th:action="@{/board}" method="post" th:object="${board}" enctype="multipart/form-data">
              <div style="display: flex; align-items: center;">
                  <img class="profile-img" th:src="@{${'/profile-images/' + member.userImage}}" alt="프로필">
              </div>
              <div>
                  <input type="text" th:field="*{boardContent}" placeholder="무엇을 작성하시겠습니까?">
              </div>

              <!-- 이미지 미리보기 슬라이드 -->
              <div class="image-slider">
                  <button type="button" class="slide-btn left">&#10094;</button>
                  <div class="preview-container" id="preview-container"></div>
                  <button type="button" class="slide-btn right">&#10095;</button>
              </div>

              <div class="buttons">
                  <input type="file" class="upload-btn" name="files"  onchange="preView(this);" multiple accept="image/*" />
                  <button class="file-btn" onclick="ImgUpload(event)">이미지 선택</button>
                  <button class="submit-btn">작성 완료</button>
              </div>
          </form>
      </div>
      <div th:if="${#authorization.expression('isAnonymous()')}" class="login-message">
          <div style="display: flex; align-items: center;">
              <img class="profile-img" th:src="@{'/profile-images/' + basic.png}" alt="프로필">
          </div>
          <div>
              <input type="text"  placeholder="로그인 후 이용이 가능합니다">
          </div>
          <a th:href="@{/login}">로그인 페이지로 이동</a>
      </div>
  </div>

  <!-- 게시글 목록 리스트 -->
  <div class="post" th:each="boards : ${boardList}">
      <div class="post-header">
          <img class="profile-img" th:src="@{${'/profile-images/' + writers[boards.boardIdx].userImage}}" alt="프로필">
          <div class="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>
      <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" th:each="imgUrl : ${boardImages.get(boards.boardIdx)}">
                  <img th:src="@{${'/board-images/' + imgUrl}}" alt="게시물 이미지">
              </div>
              <button class="board-slide-right">&#10095;</button>
          </div>
      </div>
      <div class="post-actions">
          <span class="toggle-comments">💬 <span th:text="${boards.boardComent}"></span></span>
          <span>❤️ <span 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}">
          <div class="comment-input" th:if="${#authorization.expression('isAuthenticated()')}">
              <img class="profile-img" th:src="@{${'/profile-images/' + member.userImage}}" alt="프로필">
          </div>

          <div class="comment-font">
              <input type="text" placeholder="무엇을 작성하시겠습니까?">
          </div>

          <!-- 이미지 미리보기 슬라이드 -->
          <div class="image-slider comment-slider">
              <button type="button" class="slide-btn left comment-left" >&#10094;</button>
              <div class="preview-container comment-container" id="coment-container"></div>
              <button type="button" class="slide-btn right comment-right">&#10095;</button>
          </div>

          <div class="comment-btn">
              <input type="file" class="upload-btn commentupload-btn"  name="commentfiles" onchange="preView(this);" multiple accept="image/*" />
              <button class="file-btn" th:attr="data-board-id=${boards.boardIdx}" onclick="CommentImgUpload(event)">이미지 선택</button>
              <button class="comment-submit">작성</button>
          </div>
      </div>
  </div>

3. JS 이미지 프리뷰 및 슬라이드

댓글 이미지 선택 댓글 이미지 선택을 클릭하면 ( 버튼값이다)가실행된다 이후 fileInput에 강제로 th:attr="data-board-id=${boards.boardIdx}" 이값을 저장한다 댓글에서 이미지를 넣었을 경우에만 해당

function CommentImgUpload(event) {
        event.preventDefault();
        const button = event.currentTarget; // 이미지 선택 버튼을 클릭
        const boardId = button.dataset.boardId; // 클릭한 버튼의 data 값을 가져옴

        // 더 안전하게: 버튼 기준으로 가장 가까운 comment-section 찾기
        const commentSection = button.closest(".comment-section");// 버튼 기준으로 가장 가까운 .comment-section 찾기

        const fileInput = commentSection.querySelector(".commentupload-btn"); // 해당 섹션 안의 input[type="file"] 찾기

        fileInput.dataset.boardId = boardId; // fileInput에 data 값을 강제로 넣어줌
        fileInput.click(); // input 클릭해서 파일 선택창 열기
    }
  

아래의 HTML DIV에서 댓글 preView 이미지 선택을 누르면

<input type="file" class="upload-btn commentupload-btn"  name="commentfiles" onchange="preView(this);" multiple accept="image/*" />
<button class="file-btn" th:attr="data-board-id=${boards.boardIdx}" onclick="CommentImgUpload(event)">이미지 선택</button>

CommentImgUpload 에서 받아온 boardId 값을 -> 아래의 preView
input.dataset.boardId(여기다가 저장) 해주고
document.querySelector(`.comment-section[data-board-id="${boardId}"] 해당 하는 값을 찾아준다

let postImages = [];  // 게시물 이미지 배열
    let commentImagesMap = {};  // key: boardId, value: 이미지 배열

    // 게시물 및 댓글 이미지 프리뷰
    function preView(input) {
        const isComment = input.classList.contains("commentupload-btn");
        const boardId = input.dataset.boardId;

        let previewContainer;
        if (isComment) {
            previewContainer = document.querySelector(`.comment-section[data-board-id="${boardId}"] .comment-container`);
            if (!commentImagesMap[boardId]) {
                commentImagesMap[boardId] = [];  // 초기화
            }
        } else {
            previewContainer = document.getElementById("preview-container");
        }

        let images = isComment ? commentImagesMap[boardId] : postImages;

        if (input.files.length + images.length > 10) {
            alert("최대 10장까지만 업로드할 수 있습니다.");
            return;
        } -> 이미지는 10장 까지만 

        Array.from(input.files).forEach((file) => {
            if (!file.type.startsWith("image/")) return;

            images.push(file);

            const reader = new FileReader();
            reader.onload = function (e) {
                const imgWrapper = document.createElement("div");
                imgWrapper.classList.add("preview-wrapper");

                const img = document.createElement("img");
                img.src = e.target.result;
                img.classList.add("preview-img");

                const closeBtn = document.createElement("span");
                closeBtn.classList.add("preview-close");
                closeBtn.textContent = "X";
                closeBtn.onclick = function () {
                    const currentIndex = images.indexOf(file);
                    removeImage(currentIndex, isComment, boardId); // ✨ boardId 넘기기
                };

                imgWrapper.appendChild(img);
                imgWrapper.appendChild(closeBtn);
                previewContainer.appendChild(imgWrapper);
                if(isComment){
                    initComentSlider();
                }else{
                    initBoardSlider();
                }
            };
            reader.readAsDataURL(file);
        });

        updateFileInput(isComment, boardId); // ✨ boardId 넘기기
    }

그리고 파일을 하나씩 추가해도 전에 파일이미지가 남아있게 해준다

     //선택한 이미지를 input에 넣어줌 기존 파일을 유지해줌
    function updateFileInput(isComment, boardId = null) {
        const dataTransfer = new DataTransfer();
        let images;

        if (isComment) {
            images = commentImagesMap[boardId] || [];
            const container = document.querySelector(`.comment-section[data-board-id="${boardId}"]`);
            const fileInput = container.querySelector(".commentupload-btn");
            images.forEach(file => dataTransfer.items.add(file));
            fileInput.files = dataTransfer.files;
        } else {
            images = postImages;
            const fileInput = document.querySelector(".upload-btn");
            images.forEach(file => dataTransfer.items.add(file));
            fileInput.files = dataTransfer.files;
        }
    }      

전체적인 게시물 이미지 슬라이드에 대한 구조 설명

만약 이미지가 5장이라면 해당하는 이미지 하나씩 div 크기를 정해준다 , 그리고 그 위에 div를 하나더 감싸서 실제로 보여지는 container 구조를 작성한다 그리고 안에 있는 이미지는 inline-flef로 일렬로 정렬해준다 당연 크기는 400px 으로 정해준후 overflow를 줘서 넘어가면 안보이게 이후 아래 그림은 400px , 400px 이라면 버튼에 따라서 left를 누르면 해당 container의 translate +400만큼 증가시켜주고 , right -400만큼 시켜줘서 밀어주면서 overflow:hidden 이기 때문에 보여지는 이미지가 밀리면서 보인다

게시물 이미지 슬라이드

게시물은 각 해당하는 하는 값들만 이미지 슬라이드를 해야 하기때문에 forEach로
전체 div board-img-list-innder 안에 있는 값을 하나씩 img-list-inner을 하나씩 찾아서
각 해당하는 값에 div를 translateX - 275씩 만큼 움직여준다
나머지 슬라이드도 비슷하게 작성

document.addEventListener("DOMContentLoaded", function () {
       const sliders = document.querySelectorAll(".board-img-list-inner");

       sliders.forEach(slider => {
           const imgContainer = slider.querySelectorAll(".img-list-inner");
           const images = Array.from(imgContainer);
           const leftBtn = slider.querySelector(".board-slide-left");
           const rightBtn = slider.querySelector(".board-slide-right");
           let currentIndex = 0;
           const imgWidth = 275; // 이미지 크기 (275px + 여백 25px 고려)
           const maxIndex = images.length - 2; // ✅ 최대 이동 가능 인덱스

           // 슬라이드 이동 함수
           function updateSlide() {
               const offset = -(currentIndex * imgWidth);
               imgContainer.forEach(img => img.style.transform = `translateX(${offset}px)`);

               // 왼쪽 버튼 제어
               leftBtn.style.display = currentIndex > 0 ? 'block' : 'none';

               // 오른쪽 버튼 제어
               rightBtn.style.display = currentIndex >= maxIndex ? 'none' : 'block';
           }

           // ⬅️ 왼쪽 이동
           leftBtn.addEventListener("click", function () {
               if (currentIndex > 0) {
                   currentIndex--;
                   updateSlide();
               }
           });

           // ➡️ 오른쪽 이동
           rightBtn.addEventListener("click", function () {
               if (currentIndex < maxIndex) {
                   currentIndex++;
                   updateSlide();
               }
           });

           // 초기 슬라이드 상태 업데이트
           updateSlide();
       });
   });
                                           

4 구현 이미지 보여 주기

  • 기존 게시물 이미지 슬라이드

  • 게시물 작성 이미지 슬라이드

전체적인 구조 및 html

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

0개의 댓글