댓글 기능 구현
- 테이블 만들기
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);
}
}
'Spring' 카테고리의 다른 글
[Spring / 게시판-댓글 #3] 비동기_RESTful 이론_22.07.28 [12일차] (0) | 2022.07.28 |
---|---|
[Spring / 게시판-댓글 #2] 테이블/ 객체/ mapper/ xml/ 서비스 /테스트 22.07.28 [12일차] (0) | 2022.07.28 |
[@MyBatis] 최신글 표시 기능 22.07.20 - #1# [11일차] (0) | 2022.07.21 |
[@MyBatis] 검색 기능 만들기 22.07.20 - #2# [10일차] (0) | 2022.07.21 |
[@MyBatis] 검색 기능 만들기 22.07.20 - #1# [10일차] (0) | 2022.07.20 |