68일차 (1) - JavaScript (게시판 프로그램 -> 무한스크롤, fetchAPI - DELETE, PUT, PATCH)

Yohan·2024년 5월 29일
0

코딩기록

목록 보기
103/156
post-custom-banner

무한스크롤 추가

  • isFetching: true인 동안에는 (데이터가 요청 중) 데이터 요청이 진행 중이므로 함수를 바로 종료하여 중복 요청을 방지
// =============== 무한 스크롤 전용 함수 ============= //

// 전역 변수
let currentPage = 1; // 현재 무한스크롤시 진행되고 있는 페이지 번호
let isFetching = false; // 데이터를 불러오는 중에는 더 가져오지 않게 제어하는 변수
let totalReplies = 0; // 총 댓글 수
let loadedReplies = 0; // 로딩된 댓글 수


function appendReplies({ replies }) {

    // 댓글 목록 렌더링
    let tag = '';
    if (replies && replies.length > 0) {
        replies.forEach(({ rno, writer, text, createAt }) => {
            tag += `
        <div id='replyContent' class='card-body' data-reply-id='${rno}'>
            <div class='row user-block'>
                <span class='col-md-3'>
                    <b>${writer}</b>
                </span>
                <span class='offset-md-6 col-md-3 text-right'><b>${getRelativeTime(createAt)}</b></span>
            </div><br>
            <div class='row'>
                <div class='col-md-9'>${text}</div>
                <div class='col-md-3 text-right'>
                    <a id='replyModBtn' class='btn btn-sm btn-outline-dark' data-bs-toggle='modal' data-bs-target='#replyModifyModal'>수정</a>&nbsp;
                    <a id='replyDelBtn' class='btn btn-sm btn-outline-dark' href='${rno}'>삭제</a>
                </div>
            </div>
        </div>
        `;
        });



    } else {
        tag = `<div id='replyContent' class='card-body'>댓글이 아직 없습니다! ㅠㅠ</div>`;
    }


    // += 으로 무한스크롤 시 댓글이 계속 뜨게함
    document.getElementById('replyData').innerHTML += tag;

    // 로드된 댓글 수 업데이트
    loadedReplies += replies.length;

    // 페이지 태그 렌더링
    // renderPage(pageInfo);
}



// 서버에서 댓글 데이터를 페칭 (페칭: 비동기로 뜨게함)
// pageNo = 1이 기본값
export async function fetchInfScrollReplies(pageNo=1) {

    // isFetching : false -> 서버에서 데이터를 가져오지 않는 상태
    if (isFetching) return; // 서버에서 데이터를 가져오는 중이면 return

    isFetching = true; // 데이터를 가져오는 중


    const bno = document.getElementById('wrap').dataset.bno; // 게시물 글번호
    const res = await fetch(`${BASE_URL}/${bno}/page/${pageNo}`);
    const replyResponse = await res.json();

    if (pageNo === 1) {
        // 총 댓글 수 전역 변수 값 세팅
        totalReplies = replyResponse.pageInfo.totalCount;
        loadedReplies = 0; // 댓글 입력, 삭제시 다시 1페이지 로딩시 초기값으로 만든다.
        // 댓글 수 렌더링
        document.getElementById('replyCnt').textContent = totalReplies;
        // 초기 댓글 reset
        document.getElementById('replyData').innerHTML = '';

        // 무한 스크롤 생성
        setupInfiniteScroll(); 
    }

    // 댓글 목록 렌더링
    // console.log(replyResponse)
    appendReplies(replyResponse);
    currentPage = pageNo;
    isFetching = false;
    hideSpinner();

    // 댓글을 전부 가져올 시 스크롤 이벤트 제거하기
    if (loadedReplies >= totalReplies) {
        window.removeEventListener('scroll', scrollHandler);
    }
}


// 스크롤 이벤트 핸들러 함수
async function scrollHandler(e) {

    // 스크롤이 최하단부로 내려갔을 때만 이벤트 발생시켜야 함
    //  현재창에 보이는 세로길이 + 스크롤을 내린 길이 >= 브라우저 전체 세로길이
    if (
        window.innerHeight + window.scrollY >= document.body.offsetHeight + 100
        && !isFetching
    ) {
        // console.log(e);

        // 서버에서 데이터를 비동기로 불러와야 함
        // 2초의 대기열이 생성되면 다음 대기열 생성까지 2초를 기다려야 함.
        // async, await 으로 대기열 무한 생성을 막는다.
        showSpinner();
        await new Promise(resolve => setTimeout(resolve, 500));
        fetchInfScrollReplies(currentPage + 1);
    }
}

// 무한 스크롤 이벤트 생성 함수
export function setupInfiniteScroll() {
    window.addEventListener('scroll', scrollHandler);
}

fetchAPI PUT (수정요청)

  • 댓글 번호를 수정하는 부분에서는 구할 수 없으므로 댓글에서 미리 구해서 수정 모달에 넣어줌
import { BASE_URL } from "./reply.js";
import { fetchInfScrollReplies } from "./getReply.js";
// 수정 이벤트 등록 함수
export function modifyReplyClickEvent() {

  // 버블링 (replyData에 event)
  document.getElementById('replyData').addEventListener('click', e => {
    e.preventDefault();

    if (!e.target.matches('#replyModBtn')) return;

    // 수정 전 텍스트 읽기
    const text = e.target.closest('.row').querySelector('.col-md-9').textContent;
    
    // 모달의 textArea에 넣기
    document.getElementById('modReplyText').value = text;

    // 댓글번호 구하기
    const rno = e.target.closest('#replyContent').dataset.replyId;

    // 모달에 클릭한 댓글번호 달아놓기
    document.querySelector('.modal').dataset.rno = rno;
});
}

// 수정 요청 처리 이벤트
document.getElementById('replyModBtn').addEventListener('click', e => {

  fetchReplyModify();
});


// 댓글 수정 비동기 요청 처리 함수
async function fetchReplyModify() {

  const payload = {
    // 댓글번호 구해서 모달에 달아놓아야 rno 가져올 수 있음
    rno: document.querySelector('.modal').dataset.rno,
    newText: document.getElementById('modReplyText').value,
    bno: document.getElementById('wrap').dataset.bno
  };

  const res = await fetch(BASE_URL, {
    method: 'PUT',
    headers: {
      'content-type': 'application/json'
    },
    body: JSON.stringify(payload)
  });

  if(!res.ok) {
    alert('수정 실패!');
  }
  
  // 모달 닫기
  document.getElementById('modal-close').click();

  window.scrollTo(0, 0); // 삭제 후 페이지 상단으로 이동
  await fetchInfScrollReplies();
}

ReplyService

  • 댓글 수정할 때 필요한 입력값들을 포장하는 dto객체 필요!

ReplyModifyDto

  • rno, newText를 받고 bno는 수정완료 후에 새로운 목록을 갱신하기 위해 추가

ReplyApiController

// 댓글 수정 요청
    // PUT, PATCH매핑 둘 다 받을 수 있게
    // @RequestBody : JSON 데이터 -> 자바 객체로 파싱
    @RequestMapping(method = {RequestMethod.PUT, RequestMethod.PATCH})
    public ResponseEntity<?> modify(
            @Validated @RequestBody ReplyModifyDto dto
            , BindingResult result
    ) {

        log.info("/api/v1/replies : PUT, PATCH");
        log.debug("parameter: {}", dto);

        if (result.hasErrors()) {
            Map<String, String> errors = makeValidationMessageMap(result);

            return ResponseEntity
                    .badRequest()
                    .body(errors);
        }

        ReplyListDto replyListDto = replyService.modify(dto);

        return ResponseEntity.ok().body(replyListDto);

    }

fetchAPI DELETE

import { BASE_URL } from "./reply.js";
import { fetchInfScrollReplies } from "./getReply.js";

// 댓글 삭제 비동기 요청 처리 함수
const fetchDeleteReply = async (rno) => {
    const res = await fetch(`${BASE_URL}/${rno}`, {
        method: 'DELETE'
    });

    if (res.status !== 200) {
        alert("삭제에 실패했습니다!")
        return;
    }

    fetchInfScrollReplies();
    window.scrollTo(0, 0); // 삭제 후 페이지 상단으로 이동
};



// 댓글 삭제 처리 이벤트 등록 함수
export function removeReplyClickEvent() {
    // 버블링
    document.getElementById('replyData').addEventListener('click', e => {

        e.preventDefault(); // a태그 클릭 못하게 막음
        if (!e.target.matches('#replyDelBtn')) return;

        if (!confirm('정말 삭제할까요??')) return;

        // 댓글번호
        const rno = e.target.closest('#replyContent').dataset.replyId;
        fetchDeleteReply(rno);

    });

}
profile
백엔드 개발자
post-custom-banner

0개의 댓글