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) 글이 무엇인지 제한한다.
- MyBatis에서 like문에 파라미터를 사용하기 위해서는 문자열 합치기 함수를 사용 해야한다
- where 조건에서 # 이 아니라 ${searchcolumn}을 쓴 이유는 searchcolumn 이란 column은 DB에 존재하지 않고 $를 쓰면은 form에 name값을 불러온다는 뜻으로 이해하면 편하다. select box에서 option으로 id,name 등등 을 넣어놨고, 검색을 할 때, id나 name등을 선택을 하면 그 value가 {searchcolumn} 안에 들어간다고 보면된다. id컬럼이랑, name 컬럼은 테이블에 존재하닌까. (mybatis에서 column명을 표시할 때는 $ 를 사용한다)
- 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) 글이 무엇인지 제한한다.
- where 조건에서 # 이 아니라 ${searchcolumn}을 쓴 이유는 searchcolumn 이란 column은 DB에 존재하지 않고 $를 쓰면은 form에 name값을 불러온다는 뜻으로 이해하면 편하다. select box에서 option으로 id,name 등등 을 넣어놨고, 검색을 할 때, id나 name등을 선택을 하면 그 value가 {searchcolumn} 안에 들어간다고 보면된다. id컬럼이랑, name 컬럼은 테이블에 존재하닌까. (mybatis에서 column명을 표시할 때는 $ 를 사용한다)(위 내용이랑 같음)
-
- 4️⃣ updateReadCount -> 조회수를 증가시키는 쿼리
- 5️⃣ updateLikes -> 좋아요 증가시키는 쿼리
- insertReboad -> 댓글 insert 하는 것
- updateRestep -> restep : 출력을 업데이트 하는 것.
- 업데이트시 restep+1을 한다, 조건에 만족시 어떤조건?
- 업데이트를 하는 것이 regroup 컬럼 이고, 기존 restep (들여쓰기)이 restep기존보다 클 때 업데이트 한다.
- 업데이트시 restep+1을 한다, 조건에 만족시 어떤조건?
- 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 어노테이션을 필수로 해줘야 합니다.