Searching on Board

Prerequisites

  • 검색할 속성을 지정 가능 (제목, 주소, 운전면허 여부 등)
  • 앞의 프로젝트와 연결 (1013)

View_Search Word

<body>
	<div>
		<form action="list" class="d-inline-flex">
			<select name="title" class="form-control">
				<option value="name" ${title=="name"?"selected":"" }>이름</option>
				<option value="addr" ${title=="addr"?"selected":"" }>주소</option>
				<option value="driver" ${title=="driver"?"selected":"" }>운전면허</option>
			</select>
			<input type="text" name="search" class="form-control" placeholder="검색" value="${search }">
			<button type="submit" class="btn btn-success">검색</button>
		</form>
	</div>
</body>
  • 검색 후 초기 값 설정을 위해 세팅한 값 존재
  • 이들이 아무것도 검색하지 않은 최초의 상태에 null 값으로 등장하며 오류 발생 → 추후 해결

Mapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="friday">
	<!-- 검색 리스트 -->
	<select id="selectAllInfo" resultType="idto" parameterType="Map">
		select * from myinfo
		<if test="search!=null">
			where ${title} like concat('%',#{search},'%') <!-- where {검색기준이 될 필드명} like concat('%',?,'%'); -->
		</if> <!-- ${컬럼자체},#{데이터} -->
		order by num desc
	</select>
</mapper>
  • Spring Framework의 Mapper(xml)에서 조건문은 <if test=”조건”>태그를 이용 (조건은 문자열 형태로 입력)
  • MySQL에서 문자열 비교 검색 조건은 where ${title} like concat('%',#{search},'%')
    • where {검색 기준이 될 필드명} like concat(’%’,?,’%’)
    • Mapper에서 ${ }컬럼 자체 값을 의미하며, #{ }데이터 값을 의미

DAO (Interface Overriding)

@Repository
public class InfoDao implements InfoInter {
	
	@Autowired
	SqlSession session;

	@Override
	public List<InfoDto> getAllInfo(Map<String, String> map) {
		return session.selectList("selectAllInfo", map);
	}
}
  • Spring Framework의 특성상 두 가지 이상의 변수를 전달하기 위해서는 일반적으로 Map, DTO 등 정해진 방법 사용

Controller

@Controller
public class InfoController {
	
	@Autowired
	InfoInter inter;

	//getTotalCount(),getAllInfo() 생략..1013참조
	
	@GetMapping("/info/list")
	public String list(Model model,
			@RequestParam(defaultValue = "name") String title,
			@RequestParam(required = false) String search) {
		
		int count=inter.getTotalCount();
		
		//List<InfoDto> list=inter.getAllInfo();
		System.out.println(title+","+search);
		
		Map<String, String> map=new HashMap<String, String>();
		map.put("search", search);
		map.put("title", title);
		
		List<InfoDto> list=inter.getAllInfo(map);
		
		model.addAttribute("count", count);
		model.addAttribute("list", list);
		
		model.addAttribute("search", search);
		model.addAttribute("title", title);
		
		return "info/infoList";
	}
}
  • 초기 변수가 null 값으로 등장하는 문제 해결을 위해 defaultValue 지정 및 required=false 선언
  • Mapper, DAO에서 필요한 값을 Map에 담아 전달

Posts and Comments on Board

Pre-Settings

Ready Settings

  • 파일 업로드, JDBC 등 필요한 설정 (1011 참고)
<!--servlet-context.xml-->
<beans>
	<context:component-scan base-package="spring.mvc.*,board.data.controller" />
</beans>
  • DispatcherServlet Context의 Controller 스캔 범위를 확장하려면, servlet-context.xml 파일에서 <context:component-scan>태그의 base-package 속성을 ,(comma)로 나열하거나, 와일드카드(*)를 이용하여 범위 특정을 제거

DB Settings

create table reboard(num smallint primary key auto_increment,
writer varchar(20),
pass varchar(20),
subject varchar(200),
content varchar(2000),
photo varchar(500),
regroup smallint,
restep smallint,
relevel smallint,
viewcount smallint default 0,
writeday datetime);
  • reboard 테이블을 생성하고 num을 primary key로 지정
  • viewcount 속성은 default 값을 지정하여, null 입력 시 자동으로 0으로 대체
  • regroup, restep, relevel 세 속성은 댓글 및 대댓글을 위한 속성

DTO Settings

public class BoardDto {

	private int num;
	
	private int regroup;
	private int restep;
	private int relevel;
	private int viewcount;
	
	private String writer;
	private String pass;
	private String subject;
	private String content;
	private String photo;
	
	private Timestamp writeday;

	//getter,setter 생략
}
  • num 속성을 int로 지정하는지, String으로 지정하는지에 따라 추후 약간의 차이 발생

MyBatis Alias Settings

<!--mybatis-->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
  
<configuration>
	<typeAliases>
		<typeAlias alias="bdto" type="spring.mvc.reboard.BoardDto"/>
	</typeAliases>
</configuration>
  • src/main/resources>mybatis-config.xml
  • Mapper에서 BoardDto 대신 사용할 alias 지정 (해당 파일 혹은 클래스까지의 경로 명시)

Interface for DAO Settings

public interface BoardDaoInter {

	public int getTotalCount();
	public int getMaxNum();
	public void updateRestep(int regroup,int restep); //댓글 출력 제어용
	public void insertReboard(BoardDto dto);
	public List<BoardDto> getPagingList(int start,int perPage);
	public BoardDto	getContent(int num);
}
  • 이번 게시판은 Pagination 기능을 추가할 것 (전체 데이터 출력 메서드에 start, perPage 변수 필요)
  • 댓글 및 대댓글 출력 제어를 위한 메서드 추가

Forethought for Ordering Posts

Prerequisites

  • 뿌리 글(첨부할 대상이 없는 게시물), 댓글, 대댓글 등의 모든 게시물은 최신순 정렬 (나중에 게시한 것이 앞에 위치)
  • 뿌리 글, 댓글 등의 게시물에 첨부하는 댓글은 첨부되는 게시물 아래에 위치
  • 첨부하는 댓글의 level(계층)은 첨부되는 댓글의 level보다 한 계층 아래

Logics

  • 답변형 게시 글을 위해 필요한 변수 4가지
    • num(numbering) : 정렬되는 모든 게시물에 부여하는 식별자 (primary key)
    • regroup(grouping sort) : 최종 뿌리 글로부터 연결되는 모든 게시물에 동일하게 부여하는 식별자 → g
    • restep(list order) : grouping sort 내에서 각 게시물이 출력될 순서를 지정하는 식별자 → s
    • relevel(hierarchy) : 게시물의 계층을 구분하는 식별자 → l
  • 뿌리 글이 생성될 경우 g = max(num)+1 (절대 중복되지 않음 : num은 primary key이므로), g-s-l(g-0-0) 부여
--mysql in spring
select ifnull(max(num),0) from reboard --null값 처리..ifnull()
  • 댓글의 경우 첨부하는 대상 글의 g-s-l 값을 받아서 변경
    • g = g (대상 글과 동일한 group)

    • s’ = s+1 그리고 해당 group에 속하는 게시물의 s 값 중 s’ 이상은 모두 s = s+1 (첨부 대상 글보다 순서 값 증가하되, 다른 게시물 사이에 생성될 경우 이후의 게시물 순서 차례로 밀어내기)

      --mysql in spring
      update reboard set restep=restep+1 where regroup=#{regroup} and restep>#{restep}
    • l = l+1 (첨부 대상 글보다 1 계층 아래)

Implementation

Mapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="board">
	<select id="getTotalCountOfReboard" resultType="int">
		select count(*) from reboard
	</select>
	
	<!-- num의 max값,null일경우 0 -->
	<select id="maxNumOfReboard" resultType="int">
		select ifnull(max(num),0) from reboard
	</select>
	
	<!-- 같은 그룹중에서 step보다 큰데이터는 모두 +1 -->
	<update id="updateStepOfReboard" parameterType="HashMap">
		update reboard set restep=restep+1 where regroup=#{regroup} and restep>#{restep}
	</update>
	
	<!-- insert: 새글,답글 모두 해당 -->
	<insert id="insertOfReboard" parameterType="bdto">
		insert into reboard (writer,pass,subject,content,photo,regroup,restep,relevel,writeday)
		values (#{writer},#{pass},#{subject},#{content},#{photo},#{regroup},#{restep},#{relevel},now())
	</insert> <!--viewcount는 default 0이므로 null-->
	
	<!-- 페이징처리 전체리스트 -->
	<select id="selectPagingOfReboard" resultType="bdto" parameterType="HashMap">
		select * from reboard order by regroup desc,restep asc limit #{start},#{perPage}
	</select>
	
	<select id="selectContent" resultType="bdto" parameterType="int">
		select * from reboard where num=#{num}
	</select>
</mapper>
  • 여기서는 다중 전달 값을 HashMap으로 전달
  • maxNumOfReboard 그리고 updateStepOfReboard는 위의 Forethought for Ordering Posts에서 구한 sql문이므로 설명 생략
  • 전체 조회문(selectPagingOfReboard) 일부 제어
    • Pagination을 위해 limit 제어
    • 게시물 정렬을 위해 order by 제어 (regroup, restep은 특성에 맞게 오름,내림차순 설정)

DAO_Ordering Posts

@Repository
public class BoardDao implements BoardDaoInter {

	@Autowired
	private SqlSession session;
	
	//getTotalCount(),getMaxNum() 생략

	@Override
	public void updateRestep(int regroup, int restep) {
		
		Map<String, Integer> map=new HashMap<String, Integer>();
		map.put("regroup", regroup);
		map.put("restep", restep);
		
		session.update("updateStepOfReboard", map);
	}
}
  • 게시물 정렬을 위한 regroup, restep, relevel을 조작하기 위한 메서드

DAO_Insert

@Repository
public class BoardDao implements BoardDaoInter {

	//sqlSession 생성 및 연결 생략

	@Override
	public void insertReboard(BoardDto dto) {
		
		int num=dto.getNum(); //String이면 null이냐 아니냐,int면 0이냐 아니냐
		int regroup=dto.getRegroup();
		int restep=dto.getRestep();
		int relevel=dto.getRelevel();
		
		if(num==0) {
			regroup=getMaxNum()+1;
			restep=0;
			relevel=0;
		}
		else {
			//같은 그룹중 잔달받은 restep보다 큰글들은 모두 +1
			this.updateRestep(regroup, restep);
			
			//전달받은 step과 level 모두 +1
			restep++;
			relevel++;
		}
		//바뀐값들을 다시 dto에 담는다
		dto.setRegroup(regroup);
		dto.setRestep(restep);
		dto.setRelevel(relevel);
		
		session.insert("insertOfReboard", dto);
	}
}
  • insert하는 게시물도 정렬을 위해 일부 변수 조작 필요 (regroup, restep, relevel) → DTO로부터 필요한 변수 get (get의 반환 값은 첨부 대상 글g-s-l 값임)
  • if(num==0) 조건은 뿌리 글 생성 시 getNum()의 반환 값이 없으므로 해당 자료형의 default 값으로 발생 (String : null, int : 0 등) → g-s-l 값을 1-0-0으로 지정
  • else 조건은 댓글 및 대댓글 생성g-s-l 값 조작
  • 조작한 g-s-l 값을 DTO에 담아 insert할 게시물의 g-s-l 값으로 지정
@Repository
public class BoardDao implements BoardDaoInter {

	@Override
	public List<BoardDto> getPagingList(int start, int perPage) {
		
		Map<String, Integer> map=new HashMap<String, Integer>();
		map.put("start", start);
		map.put("perPage", perPage);
		
		return session.selectList("selectPagingOfReboard", map);
	}
}
  • Pagination을 위해 필요한 변수를 전달 받아 sql문에서 사용
@Repository
public class BoardDao implements BoardDaoInter {

	@Override
	public BoardDto getContent(int num) {
		return session.selectOne("board.selectContent", num);
	}
}
  • 디테일 페이지를 위한 메서드

Controller_for View_List

@Controller
public class BoardListController {

	@Autowired
	BoardDaoInter dao;
	
	@GetMapping("board/list")
	public String list(Model model,
			@RequestParam(value = "currentPage",defaultValue = "1") int currentPage) {
		
		//페이징처리에 필요한 변수선언
		int totalCount=dao.getTotalCount();
		int totalPage; //총 페이지수
		int startPage; //각블럭에서 보여질 시작페이지
		int endPage; //각블럭에서 보여질 끝페이지
		int startNum; //db에서 가져올 글의 시작번호(mysql은 첫글이 0,오라클은 1)
		int perPage=3; //한페이지당 보여질 글의 갯수
		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;

		startNum=(currentPage-1)*perPage;

		//각페이지에서 필요한 게시글 가져오기
		List<BoardDto> list=dao.getPagingList(startNum, perPage);
		
		//각 페이지에 출력할 시작번호
		int no=totalCount-startNum;
		
		model.addAttribute("totalCount", totalCount);
		model.addAttribute("list", list);
		model.addAttribute("startPage", startPage);
		model.addAttribute("endPage", endPage);
		model.addAttribute("totalPage", totalPage);
		model.addAttribute("no", no);
		model.addAttribute("currentPage", currentPage);
		
		return "reboard/boardList";
	}
}
  • Pagination을 위한 변수 선언 및 연산
  • 필요한 값은 매핑 주소로 전달

Controller_for Post_g-s-l Setting

@Controller
public class BoardWriteController {
	
	@Autowired
	BoardDaoInter dao;

	@GetMapping("/board/writeForm")
	public String writeform(Model model,@RequestParam Map<String, String> map) {
		
		//요 5개는 답글일 경우에만 넘어온다(새글일경우 안넘어옴)
		String currentPage=map.get("currentPage");
		String num=map.get("num");
		String regroup=map.get("regroup");
		String restep=map.get("restep");
		String relevel=map.get("relevel");
		
		System.out.println(currentPage+","+num);
		
		//입력폼에 hidden으로 넣어줘야함..답글일때 대비
		model.addAttribute("currentPage", currentPage==null?"1":currentPage);
		model.addAttribute("num", num==null?"0":num);
		model.addAttribute("regroup", regroup==null?"0":regroup);
		model.addAttribute("restep", restep==null?"0":restep);
		model.addAttribute("relevel", relevel==null?"0":relevel);

		//dao설정에 따라 디폴트값 0으로..0으로 넣어야 dao에서 새글로 인식
		//폼이 답글,새글 공용이므로
		
		return "reboard/writeForm";
	}
}
  • 게시물 작성 시 g-s-l 초기 값 지정

Controller_Insert

@Controller
public class BoardWriteController {
	
	@Autowired
	BoardDaoInter dao;

	@PostMapping("/board/insert")
	public String insert(@ModelAttribute BoardDto dto,
			@RequestParam ArrayList<MultipartFile> uimage, //업로드 파일 여러개라서
			HttpSession session) {
		
		String path=session.getServletContext().getRealPath("/WEB-INF/photo");
		System.out.println(path);
		
		SimpleDateFormat sdf=new SimpleDateFormat("yyyyMMddHHmmss");
		
		String photo="";
		if(uimage.get(0).getOriginalFilename().equals(""))
			photo="no";
		else {
			
			for(MultipartFile f:uimage) {
				
				String fName=sdf.format(new Date())+"_"+f.getOriginalFilename();
				photo+=fName+",";
				
				try {
					f.transferTo(new File(path+"\\"+fName));
				} catch (IllegalStateException e) {
					e.printStackTrace();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			//photo에서 마지막 컴마 제거
			photo.substring(0, photo.length()-1);
		}
		//dto의 photo에 넣기
		dto.setPhoto(photo);
		
		//insert
		dao.insertReboard(dto);
		
		return "redirect:list"; //content일단 없으니까 목록으로
	}
}
  • 업로드할 파일이 여러 개라면 MultipartFile 객체를 List에 담아 호출
    • 업로드한 파일이 있는지 여부 확인하기 위해 MultipartFile의 get().getOriginalFilename() 메서드를 사용하지만 get(index number) 객체의 인자로 첫 번째 파일을 의미하는 0 입력
    • 여러 개의 파일을 ,(comma)로 연결하기 위해 누적 (photo+=fName+”,”) → 마지막 comma는 substring으로 제거
    • DB에 저장되는 데이터는 파일명이 comma로 연결된 문자열 형태
  • 실제 파일 업로드는 DB 저장과 무관하게 transferTo() 객체를 통해 실행

View_Ordering Posts

<body>
	<div>
		<table>
			<caption align="top"><b>스프링 답변형 게시판</b>
				<span style="float: right">
					<button type="button" class="btn btn-outline-info" onclick="location.href='writeForm'">글쓰기</button>
				</span>
			</caption>
			<tr>
				<th>번호</th>
				<th>제목</th>
				<th>작성자</th>
				<th>작성일</th>
				<th>조회</th>
			</tr>
			<c:if test="${totalCount==0 }">
				<tr>
					<td colspan="5" align="center"><b>등록된 게시글이 없습니다</b></td>
				</tr>
			</c:if>
			<c:if test="${totalCount>0 }">
				<c:forEach var="dto" items="${list }">
					<tr>
						<td align="center">${no }</td>
						<c:set var="no" value="${no-1 }"></c:set> <!-- 출력후 감소(증감연산자 없음) -->
						
						<td> <!-- 제목 -->
							<!-- relevel만큼 공백 -->
							<c:forEach var="s" begin="1" end="${dto.relevel }">
								&nbsp;&nbsp;
							</c:forEach>

							<!-- 답글인 경우에만 re.png이미지출력 -->
							<c:if test="${dto.relevel>0 }">
								<img src="../upload/re.png">
							</c:if>

							<!-- 제목..여기누르면 내용보기로 -->
							<a href="content?num=${dto.num }&currentPage=${currentPage}">${dto.subject }</a>
						
							<!-- 사진이 있을경우 아이콘 표시 -->
							<c:if test="${dto.photo!='no' }">
								<i class="bi bi-images"></i>
							</c:if>
						</td>
						<td align="center">${dto.writer }</td>
						<td align="center"><fmt:formatDate value="${dto.writeday }" pattern="yyyy-MM-dd"/></td>
						<td align="center">${dto.viewcount }</td>
					</tr>
				</c:forEach>
			</c:if>
		</table>

		<!--페이징 처리 생략-->
	</div>
</body>
  • JSTL에는 증감연산자가 존재하지 않음 → <c:set>을 이용하여 증감 연산
    • <c:set var="no" value="${no-1 }"></c:set>
  • relevel(g-s-ll 값)을 통해 각 게시물을 계층화하여 정렬
    • <c:forEach var="s" begin="1" end="${dto.relevel }">    </c:forEach>
    • <c:if test="${dto.relevel>0 }"> <img src="ㄴ이미지"> </c:if>

View_Pagination

<!-- 페이징 -->
		<c:if test="${totalCount>0 }">
			<div>
				<ul class="pagination justify-content-center">
					<!-- 이전 -->
					<c:if test="${startPage>1 }">
						<li class="page-item"><a href="list?currentPage=${startPage-1 }">이전</a></li>
					</c:if>
					
					<c:forEach var="pp" begin="${startPage }" end="${endPage }">
						<c:if test="${pp==currentPage }">
							<li class="page-item active">
								<a class="page-link" href="list?currentPage=${pp }">${pp }</a>
							</li>
						</c:if>
						<c:if test="${pp!=currentPage }">
							<li class="page-item">
								<a class="page-link" href="list?currentPage=${pp }">${pp }</a>
							</li>
						</c:if>
					</c:forEach>
					
					<!-- 다음 -->
					<c:if test="${endPage<totalPage }">
						<li class="page-item"><a href="list?currentPage=${endPage+1 }">다음</a></li>
					</c:if>
				</ul>
			</div>
		</c:if>
  • Pagination은 이전에 했던 방식과 동일하되 Spring Framework, JSTL에 맞게 형식만 변경

1017에 이어서 to be continued

profile
초보개발자

0개의 댓글