[Springboot] 답변형게시판 #5 (검색 + 대댓글 + 좋아요)

728x90

 

TODO : 검색 로직, 대 댓글 만들기(231106)
개발 환경 : 인텔리제이 --> SpringbootMybatisMini

 


 

#1 여러 서비스를 실행시키기 위한 사전작업인 mapperInter 입니다

 

ReboardMapperInter

package boot.data.mapper;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.Mapper;

import boot.data.dto.ReBoardDto;

@Mapper
public interface ReBoardMapperInter {
		public  int getMaxNum();
		public int getTotalCount(Map<String,String> map);
		public List<ReBoardDto> getPagingList(Map<String,Object>map);
		public void insertReboard(ReBoardDto dto);
		public void updateRestep(Map<String,Integer> map);
		public void updateReadCount(int num);
		public ReBoardDto getData(int num);
		public void updateReboard(ReBoardDto dto);
		public void deleteReboard(int num);
		public void updateLikes(int num);
}
  • 위 코드는 로직을 처리해줄 코드를 미리 나열 해 준 것이라 따로 설명은 안하겠습니다.
    • 자세한 설명은 아래에서 진행하겠습니다. 

아래 내용을 설명하기전 대,댓글 로직을 간단하게 설명해보겠습니다.

 

#1) 답변형 댓글 을 작성하려면

1) regroup : 새글 + 예전글

2) restep : 출력

3) relevel : 들여쓰기 

위 3가지의 column이 필요하다. 

답변형 로직 그림 설명

1) regroup은 원글 a에 해당하는 답글들을 묶어주는 즉 그룹핑 하는 column이다

ex) 글이 한개도 없을땐, MaxNum 값이 자동으로 regroup에 들어간다. 이때 num값은 없으니

regroup 0 , restep 0 , relevel 0 이다. 

그러면 a의 답글들은 전부 regroup이 1 이고

이후 b라는 새 글이 작성되면,  원글 a + a답글들의 최대 num 값이 regroup에 들어간다. 

a원글 + a 답글들의 최대 num 값이 6이면, 새로운 글인 b는 regroup 7이 된다.

 

2) restep은 a글에 해당하는 답글들의 순서를 나열하기 위한 column이다

a글의 댓글을 달면은 a에 붙어 있어야한다.

num 1 -> 원글 a 면 regroup 1 restep 0  relevel 0

num 2 -> a의 답글1 regroup 1 restep 1  relevel 1

num 3 -> a의 답글2 regroup 1 restep 2  relevel 1 -> num 2의 restep은 2로 변경된다.

num 4 -> num2(a의 답글1)의 답글1 regroup 1 restep 3 relevel 2 -> num2를 기준으로 restep 2+1 relevel은 답글의 답글이니까 1칸 들여쓰기해서 2가 되어야함
num 5 -> a의 답글3 regroup 1 restep 1 relvel 1 -> num1을 기준으로 num5의 regroup이 +1되고 num 234의 regroup +1씩 해서 번호 밀려남
num 6 -> num2(a의 답글1)의 답글2 regroup 1 restep 4 relevel 1
-> num2를 기준 현재 num 2의 regroup은 3이니까 num 6의 regroup은 +1 해서 4
-> num2를 기준으로 num 2의 relevel은 1이니까 num 6의 relevel은 +1 해서 2

 

 

 

#2 다음으로 mapper 인터페이스를 직접 구현해줄 sql쿼리를 작성하는 Mapper입니다.

<?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="boot.data.mapper.ReBoardMapperInter">
    <select id="getMaxNum" resultType="int">
        select ifnull(max(num),0) from reminiboard
    </select>

    <select id="getTotalCount" parameterType="Map" resultType="int">
        select count(*) from reminiboard
        <if test="searchcolumn!=null and searchword!=null">
            where ${searchcolumn} like concat('%',#{searchword},'%')
        </if>
    </select>

    <select id="getPagingList" parameterType="Map" resultType="reboard">
     select * from reminiboard
        <if test="searchcolumn!=null and searchword!=null">
              where ${searchcolumn} like concat ('%',#{searchword},'%')
        </if>
    order by regroup desc, restep asc limit #{start},#{perpage}
    </select>

    <insert id="insertReboard" parameterType="reboard">
        insert INTO reminiboard values (null,#{id},#{name},#{subject},#{content},#{photo},0,0,#{regroup},#{restep},#{relevel},now())
    </insert>
    
    <update id="updateRestep" parameterType="Map">
        update reminiboard set restep=restep+1 where regroup=#{regroup} and restep > #{restep}
    </update>

    <update id="updateReadCount" parameterType="int">
        update reminiboard set readcount = readcount+1 where num=#{num}
    </update>

    <select id="getData" parameterType="int" resultType="reboard">
        select * from reminiboard where num=#{num}
    </select>

    <update id="updateReboard" parameterType="reboard">
        update reminiboard set subject=#{subject},content=#{content}
        <if test="photo!=null">
            ,photo=#{photo}
        </if>
        where num=#{num}
    </update>

    <delete id="deleteReboard" parameterType="int">
        delete from reminiboard where num=#{num}
    </delete>

    <update id="updateLikes" parameterType="int">
        update reminiboard set likes=likes+1 where num=#{num}
    </update>
</mapper>
  • 위 내용은 MapperInter를 SQL 쿼리로 작성한 것이며 하나하나씩 설명을 해보겠습니다. 

 

  • 1️⃣ getMaxNum() -> 댓글을 달고 바로 글에 들어갔을 때 방금 쓴 글이 num이 제일 최대 값이기 때문에, 이 로직을 사용해서 페이지를 바로 넘긴다. 
    • ifnull을 하는 이유는 맨 처음에 글이 없을 때 0으로 넣어주지 않으면 null이 나오기 때문에 0으로 설정하고 시작.

 

  • 2️⃣ getTotalCount -> 검색 로직 으로 reminiboard테이블에서 검색결과를 나타내줌 ex) 검색 결과 몇개가 나오는지를 보여줌 
    • where 조건에서 # 이 아니라 ${searchcolumn}을 쓴 이유는 searchcolumn 이란 column은 DB에 존재하지 않고 $를 쓰면은 form에 name값을 불러온다는 뜻으로 이해하면 편하다. select box에서 option으로 id,name 등등 을 넣어놨고,     검색을 할 때, id나 name등을 선택을 하면 그 value가 {searchcolumn} 안에 들어간다고 보면된다. id컬럼이랑, name 컬럼은 테이블에 존재하닌까.  (mybatis에서 column명을 표시할 때는 $ 를 사용한다)
      • MyBatis에서 like문에 파라미터를 사용하기 위해서는 문자열 합치기 함수를 사용 해야한다
        • like concat ('%',#{searchword},'%'} 이 쿼리를 통해 {searchword}안에는 내가 검색한 단어가 들어간다. 
        • 그리고 검색시 아래 페이징으로 원글 기준 내림차순으로 정렬,  그리고 댓글은 최신 글부터 나오게 정렬하며, 검색시 페이지를 제한한다, 각 페이지(perpage) 당, 처음시작글(start) 글이 무엇인지 제한한다. 

 

  • 3️⃣ getPagingList -> 페이징 퀴리로, Map으로 여러 파라미터 값을 받아주고, 반환값은 reboardDto를 반환한다
      • where 조건에서 # 이 아니라 ${searchcolumn}을 쓴 이유는 searchcolumn 이란 column은 DB에 존재하지 않고 $를 쓰면은 form에 name값을 불러온다는 뜻으로 이해하면 편하다. select box에서 option으로 id,name 등등 을 넣어놨고,     검색을 할 때, id나 name등을 선택을 하면 그 value가 {searchcolumn} 안에 들어간다고 보면된다. id컬럼이랑, name 컬럼은 테이블에 존재하닌까.  (mybatis에서 column명을 표시할 때는 $ 를 사용한다)(위 내용이랑 같음)
        • like concat ('%',#{searchword},'%'} 이 쿼리를 통해 {searchword}안에는 내가 검색한 단어가 들어간다. 
        • 그리고 검색시 아래 페이징으로 원글 기준 내림차순으로 정렬,  그리고 댓글은 최신 글부터 나오게 정렬하며, 검색시 페이지를 제한한다, 각 페이지(perpage) 당, 처음시작글(start) 글이 무엇인지 제한한다. 

 

  • 4️⃣ updateReadCount -> 조회수를 증가시키는 쿼리

 

  • 5️⃣ updateLikes -> 좋아요 증가시키는 쿼리

 

  • insertReboad -> 댓글 insert 하는 것
  • updateRestep -> restep : 출력을 업데이트 하는 것.
    • 업데이트시 restep+1을 한다, 조건에 만족시 어떤조건?
      • 업데이트를 하는 것이 regroup 컬럼 이고, 기존 restep (들여쓰기)이 restep기존보다 클 때 업데이트 한다. 
  • getData -> num에따른 dto값들을 가져오는 쿼리
  • updateReboard -> 댓글에 등록한 값들을 update시켜주는 쿼리
  • deleteReboard -> 댓글 삭제시키는 로직.

 

 

그리고 Mapper인터페이스 에서는 좀더 추상적으로 Map을 쓰지만

이 것을 비즈니스 로직으로 처라하기 위한 서비스에서느 구체적으로 어떤 것을 처리할지를 적어줘야한다

ex)
<mapper인터페이스>
public int Logic1(Map<String,String> map);
-> -> ->
<service 인터페이스>
public int getTotalCount(String searchcolumn, String searchword, int startnum, int perpage);
이런식으로 바꿔줘야한다

 

다음으로는 비즈니스 로직을 처리해주기 위한 처리 과정입니다.

@Repository
public interface ReBoardServiceRepo {
	public  int getMaxNum();
	public int getTotalCount(String searchcolumn, String searchword);
	public List<ReBoardDto> getPagingList(String searchcolumn, String searchword,int startnum, int perpage);
	public void insertReboard(ReBoardDto dto);
	public void updateRestep(int regroup, int restep);
	public void updateReadCount(int num);
	public ReBoardDto getData(int num);
	public void updateReboard(ReBoardDto dto);
	public void deleteReboard(int num);
	public void updateLikes(int num);
}

 

 

#3 다음은 위 인터페이스를 구현해주는 , 즉 비즈니스 로직을 작성해주는 클래스 입니다

package boot.data.service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import boot.data.dto.ReBoardDto;
import boot.data.mapper.ReBoardMapperInter;

@Service
public class ReBoardService implements ReBoardServiceRepo{
	@Autowired
	ReBoardMapperInter reBoardMapperInter;
	@Override
	public int getMaxNum() {
		return reBoardMapperInter.getMaxNum();
	}

	@Override
	public int getTotalCount(String searchcolumn, String searchword) {
		Map<String,String> map = new HashMap<>();
		map.put("searchword",searchword);
		map.put("searchcolumn",searchcolumn);

		return reBoardMapperInter.getTotalCount(map);
	}

	@Override
	public List<ReBoardDto> getPagingList(String searchcolumn, String searchword,int startnum, int perpage) {
		Map<String,Object> map = new HashMap<>();
		map.put("searchword",searchword);
		map.put("searchcolumn",searchcolumn);
		map.put("startnum",startnum);
		map.put("perpage",perpage);
		return reBoardMapperInter.getPagingList(map);
	}

	@Override
	public void insertReboard(ReBoardDto dto) {
		int num = dto.getNum();
		int regroup = dto.getRegroup();
		int restep = dto.getRestep();
		int relevel = dto.getRelevel();

		if(num==0) { //num=0 이면 새 글 이다.
			regroup = this.getMaxNum()+1;
			restep = 0;
			relevel = 0;
		} else { //답글

			//같은 그룹 중 전달받은 restep 보다 큰 값들은 모두 일괄적으로 +1
			this.updateRestep(regroup,restep);
			//그리고 나서 잔달받은 값보다 1크게 DB에 저장
			restep++;
			relevel++;
		}
		//변경된 값들을 다시 DTO에 저장
		dto.setRegroup(regroup);
		dto.setRestep(restep);
		dto.setRelevel(relevel);

		reBoardMapperInter.insertReboard(dto);
	}

	@Override
	public void updateRestep(int regroup, int restep) {
		Map<String,Integer> map = new HashMap<>();
		map.put("regroup",regroup);
		map.put("restep",restep);

		reBoardMapperInter.updateRestep(map);
	}

	@Override
	public void updateReadCount(int num) {
			reBoardMapperInter.updateReadCount(num);
	}

	@Override
	public ReBoardDto getData(int num) {
		return reBoardMapperInter.getData(num);
	}

	@Override
	public void updateReboard(ReBoardDto dto) {
		reBoardMapperInter.updateReboard(dto);
	}

	@Override
	public void deleteReboard(int num) {
		reBoardMapperInter.deleteReboard(num);
	}

	@Override
	public void updateLikes(int num) {
		reBoardMapperInter.updateLikes(num);
	}
}
  • getTotalCount() 에서, parameter 값으로 seachcolumn이랑, searchword를 값으로 담아서 보내주는 것이다
    • 어디다 담냐? Map이라는 컬렉션 프레임워크에 담는 것이다. map을 선언해주고, map에 put메소드를 사용해서 파라미터 값들을 담아준다. 그리고 return 할 때 메소드에 담아서 보내주는 것 이다.

 

  • getPagingList() 에도 위 와같이 parameter 값을 map에 담아서 return할 때 메소드에 담아서 보내주는 것이다. 
    • 여기서 한번 더 순서를 말하자면, 스프링부트에서 데이터가 움직이는 과정은
      • 1) form에서 클라이언트가 데이터를 요청하고
      • 2) 컨트롤러에서 데이터를 처리하기 위해서 호출된 메서드를 불러온다
      • 3) 컨트롤러 -> 서비스 -> mapper -> sql 식으로 다 거쳐가고 모든게 문제가 없다면, 다시
      • 4) sql -> mapper -> 서비스 -> 컨트롤러로 돌아온다 그리고 클라이언트가 요청한 데이터를 처리해준다.

 

  • insertBoard() 에는 parametery 값은 dto값을 담아서 보내준다
    • 답변게시판에서 게시글 작성은 추가적인 로직이 필요하다
    • 기본적으로 게시판에서는 regroup,  restep, relevel, num 까지 4가지 가 필요하다 num은 
      • num은 새로운 글을 작성하면DB에 auto_increment되는 primary key 이다. 
      • 나머지 3가지는 위에 설명했으므로 생략.
      • 같은 그룹 중 전달받은 restep 보다 큰 값들은 모두 일괄적으로 +1  
        • ,ex) this.updateRestep(regroup,restep);  restep++; relevel++;
        • 그 이후 다시 dto에 값들을 담아준다. 
  • 나머지 로직은 위에 Mapper에서 설명했던 내용과 변동이 없습니다

 

#4) 컨트롤러

package boot.data.controller;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpSession;

import org.apache.tiles.request.attribute.Addable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

import boot.data.dto.MemBoardDto;
import boot.data.dto.ReBoardDto;
import boot.data.service.ReBoardService;
import lombok.Data;

@Controller
@RequestMapping("/reboard")
public class ReboardController {
	@Autowired
	ReBoardService reBoardService;
	@GetMapping("/list")
	public ModelAndView relist(
		@RequestParam(value="currentPage",defaultValue = "1") int currentPage,
		@RequestParam(value="searchcolumn", required = false) String sc ,
		@RequestParam(value="searchword",required = false) String sw) {

		ModelAndView mv= new ModelAndView();

		int totalCount = reBoardService.getTotalCount(sc,sw);
		int totalPage; //총 페이지수
		int startPage; //각블럭에서 보여질 시작페이지
		int endPage; //각블럭에서 보여질 끝페이지
		int start; //db에서 가져올 글의 시작번호(mysql은 첫글이 0,오라클은 1)
		int perPage=10; //한페이지당 보여질 글의 갯수
		int perBlock=5; //한블럭당 보여질 페이지 개수

		totalPage=totalCount/perPage+(totalCount%perPage==0?0:1);

		startPage=(currentPage-1)/perBlock*perBlock+1;

		endPage=startPage+perBlock-1;

		if(endPage>totalPage)
			endPage=totalPage;

		start=(currentPage-1)*perPage;

		List<ReBoardDto> list = reBoardService.getPagingList(sc,sw,start,perPage);

		//list의 각 글에 댓글 개수 표시를 해야할 때
		/*for(MemBoardDto m:list) {
			m.setAcount(adao.getAnswerList(d.getNum()).size());
		}*/

		//각 페이지에 출력할 시작번호
		int no = totalCount-(currentPage-1)*perPage;

		mv.addObject("totalCount",totalCount);
		mv.addObject("list",list);
		mv.addObject("startPage",startPage);
		mv.addObject("totalPage",totalPage);
		mv.addObject("endPage",endPage);
		mv.addObject("no",no);
		mv.addObject("currentPage",currentPage);

		mv.setViewName("/reboard/boardlist");

		return mv;
	}

	@GetMapping("/form")
	public String form(
		@RequestParam(defaultValue = "0") int num,
		@RequestParam(defaultValue = "0") int regroup,
		@RequestParam(defaultValue = "0") int relevel,
		@RequestParam(defaultValue = "0") int restep,
		@RequestParam(defaultValue = "1") int currentPage , Model model) {

		//답글이 있을 경우 넘어오는 값들이다.
		//새글일 경우는 모두 null이므로, defaultValue만 값으로 전달 한다.
		model.addAttribute("num",num);
		model.addAttribute("regroup",regroup);
		model.addAttribute("relevel",relevel);
		model.addAttribute("restep",restep);
		model.addAttribute("currentPage",currentPage);

		// 새글일 경우는 null , 답글일 경우에는 원글 제목 가져오기
		String subject = "";

		//새글일 경우
		if(num==0) {
		}

		//답글일 경우
		if(num>0) {
			subject = reBoardService.getData(num).getSubject();
		}
		model.addAttribute("subject",subject);

		return "/reboard/addform";
	}

	@PostMapping("/insert")
	public String insertReboard(
		@ModelAttribute ReBoardDto dto , HttpSession httpSession, ArrayList<MultipartFile> upload, @RequestParam(defaultValue = "1") int currentPage) {

		String path = httpSession.getServletContext().getRealPath("/rephoto");
		String uploadname="";

		if(upload.get(0).getOriginalFilename().equals("null")) {
			dto.setPhoto("no");
		} else {
			for(MultipartFile m :upload) {
				SimpleDateFormat sdf= new SimpleDateFormat("yyyyMMddHHmm");
				String mName = sdf.format(new Date()) + "_" + m.getOriginalFilename();
				uploadname += mName +",";

				try {
					m.transferTo(new File(path+"\\"+mName));
				} catch (IOException e) {
					throw new RuntimeException(e);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
			uploadname = uploadname.substring(0,uploadname.length()-1);
		}
		dto.setPhoto(uploadname);

		String id=(String)httpSession.getAttribute("myid");
		dto.setId(id);

		reBoardService.insertReboard(dto);

		return "redirect:list?currentPage="+currentPage;
	}

	@GetMapping("/content")
	public String detail(int num, int currentPage, Model model) {
		//조회수 증가
		reBoardService.updateReadCount(num);

		//dto
		ReBoardDto reBoardDto =reBoardService.getData(num);
		model.addAttribute("dto",reBoardDto);
		model.addAttribute("currentPage",currentPage);


		return "/reboard/content";
	}

	@GetMapping("/likes")
	@ResponseBody
	public Map<String,Integer> likes(int num) {
		reBoardService.updateLikes(num);
		int likes = reBoardService.getData(num).getLikes();

		Map<String,Integer> map = new HashMap<>();
		map.put("likes",likes);
		return map;
	}

}

 

  • 1️⃣ relist() 메소드에서 ReqeustParam 으로 currentPage , searchcolumn, searchword를 다 불러와준다, 
    • 그리고 각자 값에 맞춰, currentPage는 시작을 1로 시작하게 하고, 나머지 두개는 require=false로 바꿔둔다

 

  • 2️⃣ form()

 

  • 3️⃣ insertReboard()

 

  • 4️⃣ detail() -> 제목 클릭시 댓글만 보이는 디테일 페이지로 들어가게 해주는 로직이다
    • 디테일 페이지로 들어가기 위해서는 그에 해당하는 num, currentPage가 필요하므로 파라미터값으로 불러와서 Model에 담은 다음,  content 페이지로 넘겨준다

 

  • 5️⃣ likes() -> 비동기방식(ajax)로 처리하기 위해서는 @responsebody 어노테이션을 필수로 해줘야 합니다. 
728x90