Spring

[Spring / 게시판-댓글 #1] 코드 복사 = 테이블/ 객체/ mapper/ xml/ 서비스 /테스트 22.07.28 [12일차]

양빵빵 2022. 7. 28. 15:29

댓글 기능 구현

 

- 테이블 만들기

 

CREATE SEQUENCE seq_tbl_reply;

CREATE TABLE tbl_reply (
    reply_no NUMBER(10),
    reply_text VARCHAR2(1000) NOT NULL,
    reply_writer VARCHAR2(50) NOT NULL,
    reply_date DATE default SYSDATE,
    board_no NUMBER(10),
    CONSTRAINT pk_reply PRIMARY KEY (reply_no),
    CONSTRAINT fk_reply_board
    FOREIGN KEY (board_no)
    REFERENCES tbl_board (board_no)
);

 

- 데이터 베이스 값을 받을 객체 생성

package com.project.web_prj.reply.domain;

import lombok.*;

import java.util.Date;

@Setter @Getter @ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Reply {

    private Long replyNo; //댓글번호
    private String replyText; //댓글내용
    private String replyWriter; //댓글작성자
    private Date replyDate; //작성일자
    private Long boardNo; //원본 글번호
}

 

- 마이바티스 Mapper 생성

package com.project.web_prj.reply.repository;

import com.project.web_prj.common.paging.Page;
import com.project.web_prj.reply.domain.Reply;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface ReplyMapper {

    //댓글 입력
    boolean save(Reply reply);

    //댓글 수정
    boolean modify(Reply reply);

    //댓글 삭제
    boolean remove(Long replyNo);

    // 댓글 전체 삭제
    boolean removeAll(Long boardNo); // 게시물에 달린 모든 댓글 삭제

    //댓글 개별 조회
    Reply findOne(Long replyNo);

    //댓글 목록 조회
    List<Reply> findAll(@Param("boardNo") Long boardNo
            , @Param("page") Page page);
    // mybatis는 값을 여러개 보내면 인식불가능, 그래서 @Param을 적어준다

    // 댓글 수 조회
    int getReplyCount(Long boardNo);
}

 

- 마이바티스 xml 생성

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.project.web_prj.reply.repository.ReplyMapper">

    <resultMap id="replyMap" type="com.project.web_prj.reply.domain.Reply">
        <result property="replyNo" column="reply_no" />
        <result property="replyText" column="reply_text" />
        <result property="replyWriter" column="reply_writer" />
        <result property="replyDate" column="reply_date" />
        <result property="boardNo" column="board_no" />
    </resultMap>

    <insert id="save">
        INSERT INTO tbl_reply
        (reply_no, reply_text, reply_writer, board_no)
        VALUES
        (seq_tbl_reply.nextval, #{replyText}, #{replyWriter}, #{boardNo})
    </insert>

    <!--  댓글 수정  -->
    <update id="modify">
        UPDATE tbl_reply
        SET reply_text = #{replyText}
        WHERE reply_no = #{replyNo}
    </update>

    <!--  댓글 삭제  -->
    <delete id="remove">
        DELETE FROM tbl_reply
        WHERE reply_no = #{replyNo}
    </delete>

    <!--  댓글 전체 삭제  -->
    <delete id="removeAll">
        DELETE FROM tbl_reply
        WHERE board_no = #{boardNo}
    </delete>

    <!--  댓글 개별조회  -->
    <select id="findOne" resultMap="replyMap">
        SELECT * FROM tbl_reply
        WHERE reply_no = #{replyNo}
    </select>

    <!--  댓글 목록 조회  -->
    <select id="findAll" resultMap="replyMap">
        SELECT  *
            FROM (
                SELECT ROWNUM rn, v_reply.*
                FROM (
                    SELECT *
                    FROM tbl_reply
                    WHERE board_no = #{boardNo}
                    ORDER BY board_no DESC
                ) v_reply
            )
        WHERE rn BETWEEN (#{page.pageNum} - 1) * #{page.amount} + 1 AND (#{page.pageNum} * #{page.amount})
        <!--
                List<Reply> findAll(@Param("boardNo") Long boardNo, @Param("page") Page page);
                -> @Param("page")로 설정했으므로 #{page.pageNum} 이라고 적어야한다
        -->
    </select>

    <select id="getReplyCount" resultType="int">
        SELECT COUNT(*)
        FROM tbl_reply
        WHERE board_no=#{boardNo}
    </select>

</mapper>

 

- 테스트하기

package com.project.web_prj.reply.repository;

import com.project.web_prj.common.paging.Page;
import com.project.web_prj.reply.domain.Reply;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class ReplyMapperTest {

    @Autowired ReplyMapper replyMapper;

    @Test
    @DisplayName("댓글 1000개를 무작위 게시물에 등록해야 한다.")
    void saveTest() {

        for (int i = 1; i <= 1000; i++) {
            long bno = (long) (Math.random() * 300 + 1);

            Reply reply = new Reply();
            reply.setBoardNo(bno);
            reply.setReplyText("댓글" + i);
            reply.setReplyWriter("메롱이"+ i);

            replyMapper.save(reply);
        }
    }

    @Test
    @DisplayName("댓글 내용을 수정, 개별조회해야 한다.")
    void modifyTest() {

        Long rno = 1000L;

        Reply reply = new Reply();
        reply.setReplyText("수정된 댓글");
        reply.setReplyNo(rno);

        replyMapper.modify(reply);

        Reply one = replyMapper.findOne(rno);

        assertEquals("수정된 댓글", one.getReplyText());

    }

    @Test
    @DisplayName("특정 게시물의 댓글목록을 조회해야 한다.")
    void findAllTest() {
        List<Reply> replyList = replyMapper.findAll(1L, new Page());

        replyList.forEach(System.out::println);

        assertEquals(2, replyList.size());

    }

}

 

 

- board 서비스 클래스

package com.project.web_prj.board.service;

import com.project.web_prj.board.domain.Board;
import com.project.web_prj.board.repository.BoardMapper;
import com.project.web_prj.common.paging.Page;
import com.project.web_prj.common.search.Search;
import com.project.web_prj.reply.repository.ReplyMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.util.WebUtils;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
@Log4j2
@RequiredArgsConstructor
public class BoardMapperService {

    private final BoardMapper boardMapper;
    private final ReplyMapper replyMapper;

    // 게시물 등록 요청 중간 처리
    public boolean saveService(Board board) {
        log.info("save service start - {}", board);
        return boardMapper.save(board);
    }

    // 게시물 전체 조회 요청 중간 처리
    public List<Board> findAllService() {
        log.info("findAll service start");
//        return repository.findAll();

        List<Board> boardList = boardMapper.findAll();

        // 목록 중간 데이터 처리
        processConverting(boardList);

        // 글제목 줄임 처리 ctrl + alt + m
//        subStringTitle(boardList);

        // 시간 포맷팅 처리 ctrl + alt + m
//        convertDateFormat(boardList);

        return boardList;
    }

    private void processConverting(List<Board> boardList) {
        for (Board b : boardList) {
            convertDateFormat(b);
            substringTitle(b);
            checkNewArticle(b);
            setReplyCount(b);
        }
    }

    private void setReplyCount(Board b) {
        b.setReplyCount(replyMapper.getReplyCount(b.getBoardNo()));
    }


    private void checkNewArticle(Board b) {

        // 게시물의 작성일자와 현재 시간을 대조

        // 게시물의 작성일자 가져오기 16억 5초
        long regDateTime = b.getRegDate().getTime();

        // 현재 시간 얻기 (밀리초) 16억 10초
        long nowTime = System.currentTimeMillis();

        // 현재 시간 - 작성 시간
        long diff = nowTime - regDateTime;

        // 신규 게시물 제한 시간
        long limitTime = 60 * 5 * 1000; // 5분

        if (diff < limitTime) {
            b.setNewArticle(true);
        }
    }

    private void convertDateFormat(Board b) {
        Date date = b.getRegDate();

        SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd a hh:mm"); // pattern을 쓰면 원하는 형태로 바뀐다
        // hh : 1 - 12시간 , HH : 24시간으로 표기
        b.setPrettierDate(sdf.format(date));
    }

    private void substringTitle(Board b) {
        // 만약에 글제목이 5글자 이상이라면 5글자만 보여주고 나머지는 ...처리
        String title = b.getTitle();

        if (title.length() > 5) {
            String subStr = title.substring(0, 5);
            b.setShortTitle(subStr + "...");
        } else {
            b.setShortTitle(title);
        }
    }

    /*private void convertDateFormat(List<Board> boardList) {
        for (Board b : boardList) {
            Date date = b.getRegDate();

            SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd a hh:mm");// pattern을 쓰면 원하는 형태로 바뀐다
            // hh : 1 - 12시간 , HH : 24시간으로 표기

            b.setPrettierDate(sdf.format(date));
        }
    }
    private void subStringTitle(List<Board> boardList) {
        for (Board b : boardList) {
            // 만약에 글제목이 5글자 이상이라면 5글자만 보여주고 나머지는 ... 처리
            String title = b.getTitle();
            if (title.length() > 5) {
                String subStr = title.substring(0, 5);
//                b.setTitle(subStr + "...");
                b.setShortTitle(subStr + "...");
            } else {
                b.setShortTitle(title);
            }
        }
    }*/

    // 게시물 전체 조회 요청 중간 처리 with paging
    public Map<String,Object> findAllWithPagingService(Page page) {
        log.info("findAll service start");
//        return repository.findAll();

        Map<String, Object> findDataMap = new HashMap<>();

        List<Board> boardList = boardMapper.findAllWithPaging(page);

        // 목록 중간 데이터 처리
        processConverting(boardList);

        // 글제목 줄임 처리 ctrl + alt + m
//        subStringTitle(boardList);

        // 시간 포맷팅 처리 ctrl + alt + m
//        convertDateFormat(boardList);

        findDataMap.put("bList", boardList);
        findDataMap.put("tc", boardMapper.getTotalCount());

        return findDataMap;
    }

    // 게시물 전체 조회 요청 중간 처리 with searching
    public Map<String,Object> findAllWithSearchService(Search search) {
        log.info("findAll service start");
//        return repository.findAll();

        Map<String, Object> findDataMap = new HashMap<>();

        List<Board> boardList = boardMapper.findAllWithSearch(search);

        // 목록 중간 데이터 처리
        processConverting(boardList);

        // 글제목 줄임 처리 ctrl + alt + m
//        subStringTitle(boardList);

        // 시간 포맷팅 처리 ctrl + alt + m
//        convertDateFormat(boardList);

        findDataMap.put("bList", boardList);
        findDataMap.put("tc", boardMapper.getTotalCountWithSearch(search));

        return findDataMap;
    }

    // 게시물 상세 조회 요청 중간 처리
    @Transactional
    public Board findOneService(Long boardNo, HttpServletResponse response, HttpServletRequest request) {
        // HttpServletResponse - 쿠키를 만들어서 클라이언트에 전송하기위해 필요
        log.info("findOne service start - {}", boardNo);

        // 트랜잭션 처리 - 동시에 일어나야 한다
        // 상세보기는 뜨지 않는데 조회수가 올라가면 안된다, 반대의 경우도 마찬가지
        Board board = boardMapper.findOne(boardNo);

        // 해당 게시물 번호에 해당하는 쿠키가 있는지 확인
        // 쿠키가 없으면 조회수를 상승시켜주고, 쿠키를 만들어서 클라이언트에 전송
        makeViewCount(boardNo, response, request);

        return board;
    }

    private void makeViewCount(Long boardNo, HttpServletResponse response, HttpServletRequest request) {

        // 쿠키를 조회 - 해당 이름의 쿠키가 있으면 쿠키가 들어오고 없으면 null이 들어옴
        Cookie foundCookie = WebUtils.getCookie(request, "b" + boardNo);

        if (foundCookie == null) {
            boardMapper.upViewCount(boardNo);

//        new Cookie("쿠키이름", "쿠키값"); // 쿠키 생성
            Cookie cookie = new Cookie("b" + boardNo, String.valueOf(boardNo)); // 쿠키 생성
            cookie.setMaxAge(60);                                                     // 쿠키 수명 설정 (60초)
            cookie.setPath("/board/content");                                         // 쿠키 작동 범위

            response.addCookie(cookie);                                               // 클라이언트에 쿠키 전송
        }
    }

    @Transactional
    // 게시물 삭제 요청 중간 처리
    public boolean removeService(Long boardNo) {
        log.info("remove service start - {}", boardNo);

        // 댓글 먼저 모두 삭제
        replyMapper.removeAll(boardNo);
        // 원본게시물 삭제
        boolean remove = boardMapper.remove(boardNo);
        return remove;
    }

    // 게시물 수정 요청 중간 처리
    public boolean modifyService(Board board) {
        log.info("modify service start - {}", board);
        return boardMapper.modify(board);
    }
}

 

- reply 서비스 클래스

 

package com.project.web_prj.reply.service;

import com.project.web_prj.common.paging.Page;
import com.project.web_prj.common.paging.PageMaker;
import com.project.web_prj.reply.domain.Reply;
import com.project.web_prj.reply.repository.ReplyMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
@RequiredArgsConstructor
public class ReplyService {

    private final ReplyMapper replyMapper;

    //댓글 목록 조회
    public Map<String, Object> getList(Long boardNo, Page page) {
        PageMaker maker
                = new PageMaker(page, getCount(boardNo));

        Map<String, Object> replyMap = new HashMap<>();
        replyMap.put("replyList", replyMapper.findAll(boardNo, page));
        replyMap.put("maker", maker);

        return replyMap;
    }
    //댓글 총 개수 조회
    public int getCount(Long boardNo) {
        return replyMapper.getReplyCount(boardNo);
    }
    //댓글 개별 조회
    public Reply get(Long replyNo) {
        return replyMapper.findOne(replyNo);
    }

    //댓글 쓰기 중간처리
    public boolean write(Reply reply) {
        return replyMapper.save(reply);
    }

    //댓글 수정 중간처리
    public boolean modify(Reply reply) {
        return replyMapper.modify(reply);
    }
    //댓글 삭제 중간처리
    public boolean remove(Long replyNo) {
        return replyMapper.remove(replyNo);
    }
}