Spring 추가 강의 2
SQL Mapping Framework
Java 코드로부터 SQL문을 분리해서 관리.
매개변수 설정과 쿼리 결과를 읽어오는 코드를 제거.
작성할 코드가 줄어 생산성이 향상되며 유지 보수가 편리하다.
1) board 테이블 생성
CREATE TABLE user_info(
id VARCHAR(30) NOT NULL,
pwd VARCHAR(50) NULL,
name VARCHAR(30) NULL,
email VARCHAR(30) NULL,
birth DATE NULL,
sns VARCHAR(30) NULL,
reg_date DATETIME NULL,
PRIMARY KEY(id)
);
CREATE TABLE board(
bno INT AUTO_INCREMENT,
title VARCHAR(100) NOT NULL,
content TEXT NOT NULL,
writer VARCHAR(30) NOT NULL,
view_cnt INT DEFAULT 0 NULL,
comment_cnt INT DEFAULT 0 NULL,
reg_date DATETIME DEFAULT NOW() NULL,
up_date DATETIME DEFAULT NOW() NULL,
CONSTRAINT board_pk PRIMARY KEY (bno)
);
INSERT INTO board (title, content, writer)
VALUES
('제목2', '내용내용내용내용내용내용내용내용22222', '김둘리');
2) Mapper XML & DTO 작성
getter setter가 있어야 MyBatis가 자동으로 값을 읽어오거나 채워줄 수 있음.
public class BoardDto {
private Integer bno;
private String title;
private String content;
private String writer;
private int view_cnt;
private int comment_cnt;
private Date reg_date;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BoardDto boardDto = (BoardDto) o;
return Objects.equals(bno, boardDto.bno) && Objects.equals(title, boardDto.title) && Objects.equals(content, boardDto.content) && Objects.equals(writer, boardDto.writer);
}
@Override
public int hashCode() {
return Objects.hash(bno, title, content, writer);
}
public BoardDto() { this("","",""); }
public BoardDto(String title, String content, String writer){
this.title = title;
this.content = content;
this.writer = writer;
}
// getter setter toString 생략
}
3) DAO 인터페이스 작성
public interface BoardDao {
BoardDto select(Integer bno) throws Exception;
}
4) DAO 인터페이스 구현 & 테스트
@Repository
public class BoardDaoImpl implements BoardDao {
@Autowired
private SqlSession session;
private static String namespace = "com.fastcampus.ch4.dao.BoardMapper.";
public BoardDto select(Integer bno) throws Exception {
return session.selectOne(namespace + "select", bno);
} // T selectOne(String statement, Object parameter)
}
테스트 클래스 생성
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/root-context.xml"})
public class BoardDaoImplTest {
@Autowired
BoardDao boardDao;
@Test
public void select() throws Exception {
assertTrue(boardDao!=null);
System.out.println("boardDao = " + boardDao);
BoardDto boardDto = boardDao.select(1);
System.out.println("boardDto = " + boardDto);
assertTrue(boardDto.getBno().equals(1));
}
}
정상동작시 콘솔창에 bno=1인 게시글에 대한 정보가 출력됨.
계층간의 데이터를 주고 받기 위해 사용되는 객체
CREATE TABLE user_info(
id VARCHAR(30) NOT NULL,
pwd VARCHAR(50) NULL,
name VARCHAR(30) NULL,
email VARCHAR(30) NULL,
birth DATE NULL,
sns VARCHAR(30) NULL,
reg_date DATETIME NULL,
PRIMARY KEY(id)
);
CREATE TABLE board(
bno INT AUTO_INCREMENT,
title VARCHAR(100) NOT NULL,
content TEXT NOT NULL,
writer VARCHAR(30) NOT NULL,
view_cnt INT DEFAULT 0 NULL,
comment_cnt INT DEFAULT 0 NULL,
reg_date DATETIME DEFAULT NOW() NULL,
up_date DATETIME DEFAULT NOW() NULL,
CONSTRAINT board_pk PRIMARY KEY (bno)
);
SELECT * FROM user_info;
SELECT * FROM board
ORDER BY reg_date DESC, bno desc;
INSERT INTO user_info
VALUES
('hong', '1234', '홍길동', 'hong@aaa.com', '2020-12-01', 'facebook', NOW());
INSERT INTO board
(title, content, writer)
VALUES
('제목2', '내용내용내용내용내용내용내용내용22222', '김둘리');
user_info 테이블과 board 테이블, 테스트용 데이터
@Controller
@Service
@Repository
예외처리를 어디서 수행할지는 앞으로 생각해볼것.
참고
VO(Value Object)라는 용어도 쓰이지만 DAO는 계속 값이 변하는 객체이므로 잘 맞는 용어는 아님.
#{}
는 PreparedStatement
를 사용하므로 값에 대해서만 사용할 수 있음. 따옴표('')도 생략가능.
값에만 사용할 수 있다는 제약이 있어 SQL injection
등을 막아줌.
*참고 : SQL injection
이란 해커에 의해 SQL 삽입공격을 받는 것을 의미한다.
${}
는 일반 Statement
를 사용하므로 따옴표('')가 필요함. SQL을 구현할때 더 유연한 사용이 가능.
값에 대한 것뿐만 아니라 테이블 이름 등에도 사용할 수 있음.
보안을 위해 내부에서 사용하는 것을 권장.
XML내에 특수문자 (<, >, & ...)는 < >로 변환이 필요함.
혹은 특수문자가 포함된 쿼리를 <![CDATA[
와 ]]>
로 감싸야 함.
보통은 가독성면에서 유리한 <![CDATA[
와 ]]>
를 사용하는 편.
1) 게시물 목록 페이징
beginPage(네비게이션의 첫번째 페이지) = (page(현재 페이지)-1) / 10 * 10 + 1
=> 예를 들어 현재 페이지가 10일 경우 1이 나와야한다.
endPage = Math.min(beginPage + naviSize -1, totalPage)
=> 총 페이지 수가 네비게이션바의 마지막 페이지숫자보다 작다면 총 페이지 수를 네비게이션바의 마지막 페이지로 한다.
public class PageHandler {
private int totalCnt; // 총 게시글 갯수
private int pageSize; // 한 페이지의 크기
private int naviSize = 10; // 페이지 네비게이션의 크기
private int totalPage; // 전체 페이지의 갯수
private int page; // 현재 페이지
private int beginPage; // 네비게이션의 첫 페이지
private int endPage; // 네비게이션의 마지막 페이지
private boolean showPrev; // 이전 페이지로 이동하는 링크를 보여줄 것인지의 여부
private boolean showNext; // 다음 페이지로 이동하는 링크를 보여줄 것인지의 여부
public PageHandler(int totalCnt, int page) {
this(totalCnt, page, 10);
}
public PageHandler(int totalCnt, int page, int pageSize){
this.totalCnt = totalCnt;
this.page = page;
this.pageSize = pageSize;
totalPage = (int)Math.ceil(totalCnt / (double)pageSize); // 나눈 후 남는 게시글이 있는 경우 페이지가 하나 더 필요하므로 올림처리
beginPage = (page-1) / naviSize * naviSize + 1;
endPage = Math.min(beginPage + naviSize -1, totalPage); // 최대 페이지가 네비게이션의 크기보다 작을 시 최대페이지 수가 endPage가 됨
showPrev = beginPage != 1;
showNext = endPage != totalPage;
}
void print(){
System.out.println("page = " + page);
System.out.print(showPrev ? "[PREV] " : "");
for (int i = beginPage; i<=endPage; i++){
System.out.print(i+" ");
}
System.out.println(showNext ? " [NEXT]" : "");
}
// getter, setter, toString 생략
public class PageHandlerTest {
@Test
public void test(){
PageHandler ph = new PageHandler(250, 1);
ph.print();
System.out.println("ph = " + ph);
assertTrue(ph.getBeginPage() ==1);
assertTrue(ph.getEndPage() ==10);
}
@Test
public void test2(){
PageHandler ph = new PageHandler(250, 11);
ph.print();
System.out.println("ph = " + ph);
assertTrue(ph.getBeginPage() ==11);
assertTrue(ph.getEndPage() ==20);
}
@Test
public void test3(){
PageHandler ph = new PageHandler(250, 10);
ph.print();
System.out.println("ph = " + ph);
assertTrue(ph.getBeginPage() ==1);
assertTrue(ph.getEndPage() ==10);
}
}
위는 테스트 결과.
PageHandler(255, 25);
에는 총 게시글의 숫자와 현재 페이지 숫자를,
ph.beginPage
에는 첫 페이지로 나와야 하는 숫자를,
ph.endPage
에는 마지막 페이지로 나와야 하는 숫자를 직접 계산한 후 테스트를 돌려 맞는지 확인해본다.
페이지 계산은 실수하기 쉬운 부분이므로 위의 사진처럼 페이지와 PREV, NEXT 버튼의 출력이 정확한지 확인 한 후 다음으로 진행한다.
- BoardController -
로그인 시 접속하게 되는 /list의 경로에 페이지네이션 바와 페이징을 적용하기 위해 @GetMapping("/list")
에 코드를 추가.
@Autowired
BoardService boardService;
@GetMapping("/list")
public String list(Integer page, Integer pageSize, Model m,HttpServletRequest request) {
if(!loginCheck(request))
return "redirect:/login/login?toURL="+request.getRequestURL(); // 로그인을 안했으면 로그인 화면으로 이동
if (page==null) page=1;
if (pageSize==null) pageSize=10;
try {
int totalCnt = boardService.getCount();
PageHandler pageHandler = new PageHandler(totalCnt, page, pageSize);
Map map = new HashMap();
map.put("offset", (page-1)*pageSize);
map.put("pageSize", pageSize);
List<BoardDto> list = boardService.getPage(map);
m.addAttribute("list", list);
m.addAttribute("ph", pageHandler);
} catch (Exception e){
e.printStackTrace();
}
return "boardList"; // 로그인을 한 상태이면, 게시판 화면으로 이동
}
페이징 결과 (총 글 220개, 각 1, 11, 22페이지)
기능별 URI 정의
작업 | URI | HTTP메서드 | 설명 |
---|---|---|---|
읽기 | /board/read?bno=번호 | GET | 지정된 번호의 게시물을 보여준다 |
삭제 | /board/remove | POST | 게시물을 삭제한다 |
쓰기 | /board/write | GET | 게시물을 작성하기 위한 화면을 보여준다 |
쓰기 | /board/write | POST | 작성한 게시물을 저장한다 |
수정 | /board/modify?bno=번호 | GET | 게시물을 수정하기 위해 읽어온다 |
수정 | /board/modify | POST | 수정된 게시물을 저장한다 |
- BoardController -
@GetMapping("/read")
public String read(Integer bno, Integer page, Integer pageSize, Model m){
try {
BoardDto boardDto = boardService.read(bno);
m.addAttribute(boardDto);
m.addAttribute("page", page);
m.addAttribute("pageSize", pageSize);
} catch (Exception e) {
e.printStackTrace();
}
return "board";
}
@GetMapping("/list")
public String list(Integer page, Integer pageSize, Model m, HttpServletRequest request) {
if (!loginCheck(request))
return "redirect:/login/login?toURL=" + request.getRequestURL(); // 로그인을 안했으면 로그인 화면으로 이동
if (page == null) page = 1;
if (pageSize == null) pageSize = 10;
try {
int totalCnt = boardService.getCount();
PageHandler pageHandler = new PageHandler(totalCnt, page, pageSize);
Map map = new HashMap();
map.put("offset", (page - 1) * pageSize);
map.put("pageSize", pageSize);
List<BoardDto> list = boardService.getPage(map);
m.addAttribute("list", list);
m.addAttribute("ph", pageHandler);
m.addAttribute("page", page);
m.addAttribute("pageSize", pageSize);
} catch (Exception e) {
e.printStackTrace();
}
return "boardList"; // 로그인을 한 상태이면, 게시판 화면으로 이동
}
각 게시글의 상세페이지에서 목록으로 돌아갈때 1페이지로 돌아가지않고 원래 보던 목록페이지로 돌아가기 위해 /list
,/read
의 model객체에 page, pageSize를 추가한다.
jsp페이지 만들때 ${}
형식 및 경로설정 실수하지 않도록 주의.
- boardList.jsp -
<div style="text-align:center">
<table border="1">
<tr>
<th>번호</th>
<th>제목</th>
<th>이름</th>
<th>등록일</th>
<th>조회수</th>
</tr>
<c:forEach var = "boardDto" items="${list}">
<tr>
<td>${boardDto.bno}</td>
<td><a href="<c:url value='/board/read?bno=${boardDto.bno}&page=${page}&pageSize=${pageSize}'/>">${boardDto.title}</a></td>
<td>${boardDto.writer}</td>
<td>${boardDto.reg_date}</td>
<td>${boardDto.view_cnt}</td>
</tr>
</c:forEach>
</table>
<br>
<div>
<c:if test="${ph.showPrev}">
<a href="<c:url value='/board/list?page=${ph.beginPage-1}&pageSize=${ph.pageSize}'/>"><</a>
</c:if>
<c:forEach var="i" begin="${ph.beginPage}" end="${ph.endPage}">
<a href="<c:url value='/board/list?page=${i}&pageSize=${ph.pageSize}'/>">${i}</a>
</c:forEach>
<c:if test="${ph.showNext}">
<a href="<c:url value='/board/list?page=${ph.endPage+1}&pageSize=${ph.pageSize}'/>">></a>
</c:if>
</div>
</div>
/list
로 이동시 보여줄 목록페이지.
if문으로 prev버튼과 next버튼의 출력여부를 결정, forEach문을 이용해 페이지네이션바의 페이지번호를 출력한다.
게시글의 제목을 클릭하면 boardDto객체에서 bno, page, pageSize를 넘겨주어 게시글 상세페이지로 이동한다.
- board.jsp -
<div style="text-align:center">
<h2>게시물 읽기</h2>
<form action="" id="form">
<input type="text" name="bno" value="${boardDto.bno}" readonly="readonly">
<input type="text" name="title" value="${boardDto.title}" readonly="readonly">
<textarea name="content" id="" cols="30" rows="10" readonly="readonly">${boardDto.content}</textarea>
<button type="button" id="writeBtn" class="btn">등록</button>
<button type="button" id="modifyBtn" class="btn">수정</button>
<button type="button" id="removeBtn" class="btn">삭제</button>
<button type="button" id="listBtn" class="btn">목록</button>
</form>
</div>
<script>
$(document).ready(function(){ // main()
$('#listBtn').on("click", function(){
location.href = "<c:url value='/board/list'/>?page=${page}&pageSize=${pageSize}";
})
})
</script>
/read
로 이동시 보여줄 board.jsp
에 목록 버튼 클릭 시 가지고있는 page값과 pageSize값을 이용해 원래 보고있던 목록페이지로 돌아가도록 jQuery를 이용하여 클릭 이벤트를 작성한다.
- BoardController -
@PostMapping("/remove")
public String remove(Integer bno, Integer page, Integer pageSize, Model m, HttpSession session, RedirectAttributes rattr) {
String writer = (String) session.getAttribute("id");
try {
m.addAttribute("page", page);
m.addAttribute("pageSize", pageSize);
int rowCnt = boardService.remove(bno, writer);
if(rowCnt == 1){
rattr.addFlashAttribute("msg", "DEL_OK");
return "redirect:/board/list";
}
} catch (Exception e) {
e.printStackTrace();
}
return "redirect:/board/list";
}
RedirectAttributes
로 메시지를 전달하면 Model
로 전달할때와는 달리 메시지가 URL을 통해 전달될때 최초 한 번만 전달됨.
=> 새로고침 시 삭제알림 메시지를 최초 한번만 띄우도록 변경하기 위해 RedirectAttributes
를 사용.
session에 잠깐 저장했다가 바로 삭제하게 됨.
- board.jsp -
script부분에 removeBtn
버튼 클릭액션 추가
$(document).ready(function(){ // main()
$('#listBtn').on("click", function(){
location.href = "<c:url value='/board/list'/>?page=${page}&pageSize=${pageSize}";
});
$('#removeBtn').on("click", function(){
if(!confirm("정말로 삭제하시겠습니까?")) return;
let form = $('#form')
form.attr("action", "<c:url value='/board/remove'/>?page=${page}&pageSize=${pageSize}");
form.attr("method", "post");
form.submit();
})
})
- boardList.jsp -
<script>
let msg = "${msg}";
if(msg == "DEL_OK") alert("성공적으로 삭제되었습니다.");
</script>
넘어온 메시지로 삭제 성공여부를 확인.
msg객체를 parameter에 저장하여 보내는것이 아니므로 받을때에도 ${param.msg}
가 아니라 ${msg}
로 바로 접근해야 함.
삭제 클릭 시 confirm
창으로 삭제여부 확인.
2-1 클릭 시 해당 게시글 삭제, 2-2 클릭 시 아무 동작없이 confirm
창 닫음
- BoardController -
@PostMapping("/write")
public String write(BoardDto boardDto, Model m, HttpSession session, RedirectAttributes ratter) {
String writer = (String) session.getAttribute("id");
boardDto.setWriter(writer);
try {
int rowCnt = boardService.write(boardDto);
if (rowCnt!=1){
throw new Exception("Write failed");
}
ratter.addFlashAttribute("msg", "WRT_OK");
return "redirect:/board/list";
} catch (Exception e) {
e.printStackTrace();
m.addAttribute(boardDto);
ratter.addFlashAttribute("msg", "WRT_ERR");
return "board"; // 읽기와 쓰기에 사용. 쓰기에 사용할떄는 mode = new
}
}
@GetMapping("/write")
public String write(Model m) {
m.addAttribute("mode", "new");
return "board"; // 읽기와 쓰기에 사용. 쓰기에 사용할떄는 mode = new
}
get으로
- boardList.jsp -
boardList.jsp페이지에 글쓰기 버튼 추가
글쓰기버튼을 클릭하면 get으로 write요청을 보냄.
<button id="writeBtn" class="btn-write"token tag"><c:url value="/board/write"/>'"><i class="fa fa-pencil"></i> 글쓰기</button>
- board.jsp -
게시글 등록 에러발생 시 alert창을 띄우며 다시 board.jsp로 돌아옴.
이때 controller에서 redirct할때 model객체에 boardDto를 넣어서 보내주어 에러가 발생해도 작성한 글의 내용도 유지됨.
<script>
let msg = "${msg}";
if(msg == "WRT_ERR") alert("게시물 등록에 실패했습니다. 다시 시도해주세요.");
</script>
// 이하 listBtn, removeBtn 함수 사이에 writeBtn함수 추가
$('#writeBtn').on("click", function(){
let form = $('#form')
form.attr("action", "<c:url value='/board/write'/>");
form.attr("method", "post");
form.submit();
})
작성 후 등록을 위해 writeBtn
버튼을 클릭하면 post요청을 /borad/write로 보냄.
- BoardController -
@PostMapping("/modify")
public String modify(BoardDto boardDto, Model m, HttpSession session, RedirectAttributes ratter) {
String writer = (String) session.getAttribute("id");
boardDto.setWriter(writer);
try {
int rowCnt = boardService.modify(boardDto); // update
if (rowCnt!=1){
throw new Exception("Modify failed");
}
ratter.addFlashAttribute("msg", "MOD_OK");
return "redirect:/board/list";
} catch (Exception e) {
e.printStackTrace();
m.addAttribute(boardDto);
ratter.addFlashAttribute("msg", "MOD_ERR");
return "board"; // 읽기와 쓰기에 사용. 쓰기에 사용할떄는 mode = new
}
}
- board.jsp -
$('#modifyBtn').on("click", function(){
// 1. 읽기 상태이면 수정 상태로 변경
let form = $('#form')
let isReadOnly = $("input[name=title]").attr('readonly');
if(isReadOnly=='readonly'){
$("input[name=title]").attr('readonly', false); // title
$("textarea").attr('readonly', false); // content
$("#modifyBtn").html("등록");
$("h2").html("게시물 수정");
return;
}
// 2. 수정 상태이면 수정된 내용을 서버로 전송
form.attr("action", "<c:url value='/board/modify'/>");
form.attr("method", "post");
form.submit();
});
수정후에도 이전에 있던 목록페이지가 유지되도록 수정해보기.
- board.jsp -
URL에 page와 pageSize를 같이 넘겨줌
form.attr("action", "<c:url value='/board/remove'/>?page=${page}&pageSize=${pageSize}");
- BoardController -
board.jsp
에서 URL로 넘겨준 값을 매개변수로 받아와 model객체에 넣어보냄.
@PostMapping("/modify")
public String modify(BoardDto boardDto, Integer page, Integer pageSize, Model m, HttpSession session, RedirectAttributes ratter) {
String writer = (String) session.getAttribute("id");
boardDto.setWriter(writer);
try {
int rowCnt = boardService.modify(boardDto); // update
m.addAttribute("page", page);
m.addAttribute("pageSize", pageSize);
if (rowCnt!=1){
throw new Exception("Modify failed");
}
ratter.addFlashAttribute("msg", "MOD_OK");
return "redirect:/board/list";
} catch (Exception e) {
e.printStackTrace();
m.addAttribute(boardDto);
ratter.addFlashAttribute("msg", "MOD_ERR");
return "board"; // 읽기와 쓰기에 사용. 쓰기에 사용할떄는 mode = new
}
}
<sql>
과 <include>
공통 부분을 <sql>
로 정의하고 <include>
로 포함시켜 재사용.
코드의 중복을 줄일 수 있어 용이함.
<sql id="selectFromBoard">
SELECT bno, title, content, writer, view_cnt, comment_cnt, reg_date
FROM board
</sql>
<select id="select" parameterType="int" resultType="BoardDto">
<include refid="selectFromBoard"/>
WHERE bno = #{bno}
</select>
<if>
조건절의 결과에 따라 sql문에 추가구문을 더할 수 있다.
단순 if문의 성격을 띄므로 if-else 문과는 다르다.
아래의 예시는 각 조건절 중 하나만 해당될 수 있는 구조이지만 실제로는 여러개의 if문 조건에 해당될 수 있다.
<sql id="searchCondition">
<choose>
<when test='option=="T"'>
AND title LIKE concat('%', #{keyword}, '%')
</when>
<when test='option=="W"'>
AND writer LIKE concat('%', #{keyword}, '%')
</when>
<otherwise>
AND (title LIKE concat('%', #{keyword}, '%')
OR content LIKE concat('%', #{keyword}, '%'))
</otherwise>
</choose>
</sql>
<choose>
<when>
if-else 문과 비슷한 기능을 가짐.
먼저 나온 <when>
절의 조건에 부합하면 아래의 다른 <when>
절은 조건을 보지않고 패스함.
<select id="searchResultCnt" parameterType="SearchCondition" resultType="int">
SELECT count(*)
FROM board
WHERE true
<choose>
<when test='option=="T"'>
AND title LIKE concat ('%', #{keyword}, '%')
</when>
<when test='option=="W"'>
AND writer LIKE concat ('%', #{keyword}, '%')
</when>
<otherwise>
AND (title LIKE concat('%', #{keyword}, '%'))
OR (content LIKE concat('%', #{keyword}, '%'))
</otherwise>
</choose>
</select>
<foreach>
sql문의 in
처럼 조건이 하나가 아닐 경우 사용하되, 조건의 갯수가 정해진 in
과는 달리 갯수가 정해지지 않은 경우에 사용한다.
IN
사용 시SELECT bno, title
FROM board
WHERE bno IN (1,2,3)
<foreach>
사용 시<select id="searchResultCnt" parameterType="SearchCondition" resultType="int">
SELECT bno, title, content, writer
FROM board
WHERE bno IN
<foreach collection="array" item="bno" opne="(" close=")" separator=",">
#{bno}
</foreach>
ORDER BY reg_date DESC, bno DESC
</select>
쿼리문의 와일드카드
%
: 0 ~ n개의 자리를 뜻함. 0개도 해당되므로 없어도 무관함._
: 한 개의 자리를 뜻함. 무조건 하나는 있어야 함.참고로 와일드카드 사용 시 조건을 등호
=
가 아닌LIKE
로 비교해야 한다.
~생략~
=> 검색 페이징에 약간 문제가 있는 것 같음. 추후 확인.
input태그나 textarea태그에 html태그를 입력해 페이지를 임의로 변경하는 경우를 방지함.
예를 들어 위 사진처럼 입력 시 alert태그가 동작하며 화면의 문구들이 깨지게 됨.
이때 사용되는 것이 out태그.
<input name="title" type="text" value="${boardDto.title}" placeholder=" 제목을 입력해 주세요." ${mode=="new" ? "" : "readonly='readonly'"}>
<input name="title" type="text" value="<c:out value='${boardDto.title}'>" placeholder=" 제목을 입력해 주세요." ${mode=="new" ? "" : "readonly='readonly'"}><br>
상단의 input태그의 value값에 사용자가 임의의 태그를 넣지 못하도록 out태그를 적용한 것이 2행의 코드.
적용 시 out태그의 value로 받은 값은 모두 text값으로 인식하게 된다.
Java Script Object Notation. 자바 스크립트 객체 표기법.
{속성명1:속성값1, 속성명2:속성값2, 속성명3:속성값3}
JS객체를 서버로 전송하려면 직렬화(문자열로 변환)가 필요.
서버가 보낸 데이터(JSON문자열)를 JS객체로 변환할때, 역직렬화가 필요.
stringify()
: 객체를 JSON문자열로 변환
parse()
: JSON문자열을 객체로 변환
비동기 통신으로 데이터를 주고받기 위한 기술.
요즘은 대개 JSON을 사용함.
웹 페이지 전체(data+UI)가 아닌 일부(data)만 업데이트 가능.
예제
- SimpleRestController -
@Controller
public class SimpleRestController {
@GetMapping("/ajax")
public String ajax() {
return "ajax";
}
@PostMapping("/send")
@ResponseBody
public Person test(@RequestBody Person p) {
System.out.println("p = " + p);
p.setName("ABC");
p.setAge(p.getAge() + 10);
return p;
}
}
public class Person {
private String name;
private int age;
// 생성자, 기본생성자, getter setter, toString 생략
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
<script src="https://code.jquery.com/jquery-1.11.3.js"></script>
</head>
<body>
<h2>{name:"abc", age:10}</h2>
<button id="sendBtn" type="button">SEND</button>
<h2>Data From Server :</h2>
<div id="data"></div>
<script>
$(document).ready(function(){
let person = {name:"abc", age:10};
let person2 = {};
$("#sendBtn").click(function(){
$.ajax({
type:'POST', // 요청 메서드
url: '/ch4/send', // 요청 URI
headers : { "content-type": "application/json"}, // 요청 헤더
dataType : 'text', // 전송받을 데이터의 타입
data : JSON.stringify(person), // 서버로 전송할 데이터. stringify()로 직렬화 필요.
success : function(result){
person2 = JSON.parse(result); // 서버로부터 응답이 도착하면 호출될 함수
alert("received="+result); // result는 서버가 전송한 데이터
$("#data").html("name="+person2.name+", age="+person2.age);
},
error : function(){ alert("error") } // 에러가 발생했을 때, 호출될 함수
}); // $.ajax()
alert("the request is sent")
});
});
</script>
</body>
</html>
maven 디펜던시 추가 https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind/2.15.2
send버튼 클릭 시 ajax를 통해 data만 갱신되어 화면에 출력됨.
json객체를 주고받기위해 보내기 위한 @RequestBody
와 받기 위한 @ResponseBody
가 필요함.
메서드마다 @ResponseBody
를 붙이는 대신 클래스에 @RestController
사용가능.
모든 메서드에 @ResponseBody
어노테이션을 붙인 것과 같은 효과를 가진다.
웹서비스 디자인 아키텍쳐 접근 방식.
프로토콜에 독립적이며, 주로 HTTP를 사용하여 구현.
리소스 중싱의 API 디자인.
HTTP 메서드로 수행할 작업을 정의.
API를 POST, GET, PUT DELETE 등으로 구별하되 리소스는 간결하게 사용하는 방식.
덧글 저장용 comment 테이블 생성
CREATE TABLE comment(
cno INT NOT null AUTO_INCREMENT,
bno INT NOT NULL,
pcno INT NULL,
COMMENT VARCHAR(3000) NULL,
commenter VARCHAR(30) NULL,
reg_date DATETIME NULL DEFAULT NOW() ,
up_date DATETIME NULL DEFAULT NOW() ,
PRIMARY KEY(cno)
);
INSERT INTO comment (bno, comment, commenter)
VALUES (1, 'hello', 'hong');
SELECT * FROM comment;
컬럼설명
cno : 덧글 및 답글을 통틀어 매기는 고유번호
bno : 덧글을 단 게시글의 번호
pcno : 답글일 경우 어느 덧글에 대한 답글인지 (pcno값이 없을 경우 덧글에 해당됨)
comment : 덧글 및 답글의 내용
commenter : 덧글 및 답글의 작성자
reg_date : 덧글 및 답글의 최초 게시일자
up_date : 덧글 및 답글의 수정일자. 미수정시 reg_date와 같은 값이 들어감.
- commentMapper.xml -
<delete id="deleteAll" parameterType="int">
DELETE FROM comment
WHERE bno = #{bno}
</delete>
<select id="count" parameterType="int" resultType="int">
SELECT count(*) FROM comment
WHERE bno = #{bno}
</select>
<delete id="delete" parameterType="map">
DELETE FROM comment WHERE cno = #{cno} AND commenter = #{commenter}
</delete>
<insert id="insert" parameterType="CommentDto">
INSERT INTO comment
(bno, pcno, comment, commenter, reg_date, up_date)
VALUES
(#{bno}, #{pcno}, #{comment}, #{commenter}, now(), now())
</insert>
<select id="selectAll" parameterType="int" resultType="CommentDto">
SELECT cno, bno, pcno, comment, commenter, reg_date, up_date
FROM comment
WHERE bno = #{bno}
ORDER BY reg_date ASC, cno ASC
</select>
<select id="select" parameterType="int" resultType="CommentDto">
SELECT cno, bno, pcno, comment, commenter, reg_date, up_date
FROM comment
WHERE cno = #{cno}
</select>
<update id="update" parameterType="CommentDto">
UPDATE comment
SET comment = #{comment}
, up_date = now()
WHERE cno = #{cno} and commenter = #{commenter}
</update>
- mybatis-config.xml -
<typeAliases>
<typeAlias alias="BoardDto" type="com.fastcampus.ch4.domain.BoardDto"/>
<typeAlias alias="CommentDto" type="com.fastcampus.ch4.domain.CommentDto"/>
<typeAlias alias="SearchCondition" type="com.fastcampus.ch4.domain.SearchCondition"/>
</typeAliases>
Mapper파일에서 alias를 사용하였으므로 mybatis-config
에 typeAlias태그를 추가한다.
- CommentDto -
public class CommentDto {
private Integer cno;
private Integer bno;
private Integer pcno;
private String comment;
private String commenter;
private Date reg_date;
private Date up_date;
public CommentDto() {}
public CommentDto(Integer bno, Integer pcno, String comment, String commenter) {
this.bno = bno;
this.pcno = pcno;
this.comment = comment;
this.commenter = commenter;
}
// getter setter toString 생략
}
comment 객체를 담을 CommentDto클래스 생성.
BoardDao처럼 메서드 선언만 되어있으므로 CommentDao 인터페이스는 생략.
DAO는 mapper와 직접 연결되어 sql문을 DB에 날리도록 하는 역할을 한다.
- CommentDaoImpl -
@Repository
public class CommentDaoImpl implements CommentDao{
@Autowired
private SqlSession session;
private static String namespace = "com.fastcampus.ch4.dao.CommentMapper.";
@Override
public int count(Integer bno) throws Exception {
return session.selectOne(namespace+"count", bno);
} // T selectOne(String statement)
@Override
public int deleteAll(Integer bno) {
return session.delete(namespace+"deleteAll", bno);
} // int delete(String statement)
@Override
public int delete(Integer cno, String commenter) throws Exception {
Map map = new HashMap();
map.put("cno", cno);
map.put("commenter", commenter);
return session.delete(namespace+"delete", map);
} // int delete(String statement, Object parameter)
@Override
public int insert(CommentDto dto) throws Exception {
return session.insert(namespace+"insert", dto);
} // int insert(String statement, Object parameter)
@Override
public List<CommentDto> selectAll(Integer bno) throws Exception {
return session.selectList(namespace+"selectAll", bno);
} // List<E> selectList(String statement)
@Override
public CommentDto select(Integer cno) throws Exception {
return session.selectOne(namespace + "select", cno);
} // T selectOne(String statement, Object parameter)
@Override
public int update(CommentDto dto) throws Exception {
return session.update(namespace+"update", dto);
} // int update(String statement, Object parameter)
}
service에서는 비즈니스 로직을 처리한다.
여러 개의 동작을 하나의 트랜잭션으로 만들어야 할 경우 이러한 작업은 service계층에서 해준다.
마찬가지로 CommentService인터페이스에는 메서드 선언만 되어있으므로 생략.
- CommentServiceImpl -
@Repository
public class CommentDaoImpl implements CommentDao{
@Autowired
private SqlSession session;
private static String namespace = "com.fastcampus.ch4.dao.CommentMapper.";
@Override
public int count(Integer bno) throws Exception {
return session.selectOne(namespace+"count", bno);
} // T selectOne(String statement)
@Override
public int deleteAll(Integer bno) {
return session.delete(namespace+"deleteAll", bno);
} // int delete(String statement)
@Override
public int delete(Integer cno, String commenter) throws Exception {
Map map = new HashMap();
map.put("cno", cno);
map.put("commenter", commenter);
return session.delete(namespace+"delete", map);
} // int delete(String statement, Object parameter)
@Override
public int insert(CommentDto dto) throws Exception {
return session.insert(namespace+"insert", dto);
} // int insert(String statement, Object parameter)
@Override
public List<CommentDto> selectAll(Integer bno) throws Exception {
return session.selectList(namespace+"selectAll", bno);
} // List<E> selectList(String statement)
@Override
public CommentDto select(Integer cno) throws Exception {
return session.selectOne(namespace + "select", cno);
} // T selectOne(String statement, Object parameter)
@Override
public int update(CommentDto dto) throws Exception {
return session.update(namespace+"update", dto);
} // int update(String statement, Object parameter)
}
각 과정의 순서와 역할을 기억해두자.
보통 인터페이스를 주입해야할 경우 필드에 @Autowired
어노테이션을 선언하게 되는데, 이럴 경우 주입에 필요한 어노테이션을 빼먹는 실수를 할 수 있다.(@Service나 @Repository 등의 어노테이션)
이때 생성자를 통해 주입하면 주입에 필요한 어노테이션을 빼먹을 경우 IDE에서 빨간줄로 오류를 알려주므로 실수를 줄일 수 있다.
// 1. 필드 주입
// @Autowired
BoardDao boardDao;
// @Autowired
CommentDao commentDao;
// 2. 생성자 주입
// @Autowired
public CommentServiceImpl(CommentDao commentDao, BoardDao boardDao) {
this.commentDao = commentDao;
this.boardDao = boardDao;
}
매개변수로 인터페이스를 받아오므로 받아올 수 있는 상태인지 아닌지 실행 전 바로 확인이 가능.
- CommentController -
@RestController
public class CommentController {
@Autowired
CommentService commentService;
// 지정된 게시글의 댓글을 모두 가져오는 메서드
@RequestMapping("/comments")
public ResponseEntity<List<CommentDto>> list(Integer bno) {
List<CommentDto> list = null;
try {
list = commentService.getList(bno);
return new ResponseEntity<List<CommentDto>>(list, HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
return new ResponseEntity<List<CommentDto>>(HttpStatus.BAD_REQUEST);
}
}
}
=> 이때 4xx, 5xx 등의 예외발생 시 어떤 유형의 예외이며 예외가 발생했음을 사용자가 알 수 있어야 한다.
이때 ResponseEntity
를 사용하면 예외가 발생했을때에도 200번의 정상응답을 반환하던 코드를 오류코드로 변경해줄 수 있다.
catch
블록의 return문에 list객체가 없는 이유는 예외가 발생하여 넘겨줄 값이 없거나 의미가 없기 때문, HttpStatus.BAD_REQUEST
는 대체로 예외의 경우 사용자에 의해 발생하므로 잘못된 리퀘스트임을 알려주기 위해 추가했다.
차례로 C, U, D에 해당하는 메서드도 추가.
// 덧글 수정 메서드
@PatchMapping("/comments/{cno}") // /ch4/comments/15 PATCH
public ResponseEntity<String> modify(@PathVariable Integer cno, @RequestBody CommentDto dto){
// String commenter = (String)session.getAttribute("id");
dto.setCno(cno);
System.out.println("dto = " + dto);
try {
if(commentService.modify(dto) != 1) throw new Exception("Write Failed");
return new ResponseEntity<>("MOD_OK", HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
return new ResponseEntity<>("MOD_ERR", HttpStatus.BAD_REQUEST);
}
}
// 덧글 저장 메서드
@PostMapping("/comments") // /ch4/comments?bno=1549 POST
public ResponseEntity<String> wriete(@RequestBody CommentDto dto, Integer bno, HttpSession session){
// String commenter = (String)session.getAttribute("id");
String commenter = "hong";
dto.setCommenter(commenter);
dto.setBno(bno);
System.out.println("dto = " + dto);
try {
if(commentService.write(dto) != 1) throw new Exception("Write Failed");
return new ResponseEntity<>("WRT_OK", HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
return new ResponseEntity<>("WRT_ERR", HttpStatus.BAD_REQUEST);
}
}
// 지정된 게시글의 덧글을 삭제하는 메서드
@DeleteMapping("/comments/{cno}") // /comments/1?bno=1549 <--삭제할 덧글번호
public ResponseEntity<String> remove(@PathVariable Integer cno, Integer bno, HttpSession session){
// String commenter = (String)session.getAttribute("id");
String commenter = "hong";
try {
int rowCnt = commentService.remove(cno, bno, commenter);
if(rowCnt != 1) throw new Exception("Delete Failed");
return new ResponseEntity<>("DEL_OK", HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
return new ResponseEntity<>("DEL_ERR", HttpStatus.BAD_REQUEST);
}
}
commenter에 들어갈 id값은 이 뒤에 로그인 파트 작성 후에 수정함.
로그인한 회원만 글이나 덧글에 대한 이용권한을 부여하도록 변경예정.
덧글 및 답글(대댓글)은 Javascript로 페이지 내에서 동적으로 동작하도록 작성해야 함.
덧글을 불러오는 쿼리 작성 시 pcno로 정렬하되 pcno의 값이 없는 경우 cno로 정렬하도록 쿼리를 짜야 함.
또한 답글끼리의 정렬을 위해 pcno로 정렬 후 cno로 정렬을 한번 더 해주어야 함.
pcno는 답글을 단 원본덧글의 cno이며, cno는 덧글답글 전체를 통합한 일련번호임.
SELECT cno, bno, ifnull(pcno, cno) AS pcno, comment, commenter, reg_date, up_date
FROM comment
WHERE bno = 1549
ORDER BY pcno ASC, cno ASC;
강의완강으로 게시판 완성이 안되었으므로 앞으로 해야 할 것을 정리.
아이디어나 사용기술 등은 평범한 것으로 정하되 기획서, ERD 등의 문서작업은 면접관들이 보기 편하도록 제대로 해놓을 것.
w3등 유용한 사이트 참고.
git init
: 해당 디렉토리 경로에 .git파일 생성
git config --global user.email "유저이메일@sample.com"
: 유저 이메일 설정
git config --global user.name
"닉네임 혹은 이름" : 유저 이름 설정
git config --global --list
: 유저정보 확인
// Staging Area 에 추가
git add 파일명
git add 파일명1 파일명2
git add 폴더이름 ← 지정된 폴더 내의 모든 파일 추가
git add * ← 현재 폴더에 있는 모든 파일 또는 폴더 추가(빈 폴더 제외)
git add -u ← 커밋한 적이 있는 모든 파일 추가(해당 파일이 커밋 이력에 있어야함)
// Staging Area 에서 삭제
git rm --cached 파일명
git reset 파일명
git reset ← 현재 폴더의 모든 폴더와 파일을 Staging Area에서 삭제
// 커밋 생성하기
git commit -m "커밋설명"
git show HEAD
git log
// 커밋 수정하기
git commit --amend ← 최근 커밋의 내역을 볼 수 있음.(커밋 메시지 수정가능)
git commit --amend -sm "변경된 메시지" ← 커밋 메시지만 수정하고 싶을 때
// 취소 커밋 생성하기
git revert HEAD ← 최근 커밋을 취소하는 새로운 커밋을 생성.
주의점
- 파일을 삭제할 때, working directory에서 직접 삭제하지 말고 git을 통해 삭제해야 이력이 관리된다.
- 또한 커밋의 단위를 너무 잘게 쪼개거나 반대로 너무 클 경우 관리가 불편할 수 있으므로 생각해볼 것.
git commit --amend
는 아무것도 변경하지 않아도 커밋ID가 달라진다.
git reflog
로 확인가능.
git log 자세한 로그. 커밋 아이디가 길게 나옴.
git log --oneline 간략한 로그. 커밋 아이디가 짧게 나옴(6자리)
git shortlog 아주 간략한 로그. 커밋 아이디 안나옴.
git show 커밋ID 특정 커밋의 상세정보(브랜치 지정가능)
gitk GUI화면으로 커밋정보를 보여줌
git reflog HEAD가 가리켰던(ref) 커밋의 이력(log)을 보여줌
git relog 브랜치명 브랜치별로 reflog를 보여줌
git log -g reflog를 상세히 보여줌
git reflog
HEAD가 이동한 history를 보여줌
파일이름 대신 object id로 파일을 관리.
커밋 당시의 실제파일 대신 object id의 목록만 저장(파일은 목록형태로 별도로 저장).
Working Directory의 정보(파일목록)는 각 커밋 내에 저장됨.
커밋 시 HEAD와 함께 움직이는 포인터라고 보면 됨.
branch를 하나 더 만들어 HEAD를 옮겨 작업하는것도 가능.
merge는 두 branch가 가리키고 있는 커밋을 합치는 것.
fast-forward merge은 두 커밋을 합치며 한발 전진하는 느낌으로, 새로운 커밋이 만들어지지 않음.
반대로 no fast-forward merge는 무조건 새로운 커밋을 생성하며 이동함.
실습을 개인컴퓨터로 하지 않아 미리 설치되어있던 mariaDB로 진행했으나 DB연결이 되는지 잘 모르겠어
참고링크1 - JDBC 연결 테스트 (MySQL DB 연결)를 보고 확인.
servlet-context.xml에 들어갈 내용은 참고링크2를 보고 참고했다.
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!-- mariadb와 mysql은 커넥터를 같은 것을 사용할 수 있다. -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<!-- 마리아가 버젼이 높아지면서 servertimezone=UTC를 맞춰줘야 한다. -->
<property name="url" value="jdbc:mysql://localhost:3306/springbasic?serverTimezone=UTC"/>
<property name="username" value="DB사용자명"/>
<property name="password" value="DB사용자비밀번호"/>
</bean>
또는 이렇게 작성해도 연결에는 문제가 없었다.
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="org.mariadb.jdbc.Driver"/>
<property name="url"
value="jdbc:mariadb://localhost:3306/springbasic?useUnicode=true&characterEncoding=utf8"></property>
<property name="username" value="DB사용자명"/>
<property name="password" value="DB사용자비밀번호"/>
</bean>
참고링크1을 따라해보니 DB연결은 잘 되는것 같다.
문제는 BoardDaoImplTest
클래스로 select
메서드 테스트를 돌리니 아래 사진처럼 java.lang.IllegalStateException: Failed to load ApplicationContext
에러가 떴다.
아무래도 mapper연결쪽에 문제가 있는것같아 구글링을 하던 중 참고링크3을 참고하여 오류를 해결할 수 있었다.
문제는 root-context.xml의 sqlSessionFactory
bean에서 아래처럼 mapper와 연결해주는 부분이 빠져있었기 때문이었다.
<property name="mapperLocations" value="classpath:mapper/*Mapper.xml"/>
수정 후 다시 테스트를 돌리니 정상적으로 테스트 동작을 마쳤다.
추가그런데 property는 크게 상관없는듯 하다. jdbc를 mysql로 고치치 않고 DB를 변경했는데 문제없이 연결이 되었다. 결국 해결은 되었지만 어느부분이 문제였는지 잘 모르겠다...
이 외에도 Mapper.xml에 쿼리문을 작성할때 오타나 문법실수 등을 주의하여 작성하자. 여러번 틀려서 오류를 많이 냈다.
=> 검색 페이징에 약간 문제가 있는 것 같다.
제목을 title1 ~ title20으로 넣었을 때 title1을 검색하면 총 2페이지, 11개가 나와야하는데 1페이지만 정상출력되고 페이징이 안된다.(URL에 page값을 넣어서 이동해보면 또 잘됨.)
- BoardDaoImplTest -
@Test
public void searchResultCntTest() throws Exception {
boardDao.deleteAll();
for (int i = 1; i<=20; i++){
BoardDto boardDto = new BoardDto("title"+i, "asdfasdfasdf", "hong");
boardDao.insert(boardDto);
}
SearchCondition sc = new SearchCondition(1, 10, "title2", "T");
int cnt = boardDao.searchResultCnt(sc);
System.out.println("cnt = " + cnt);
assertTrue(cnt==2); // 1~20, title2, title20
}
@Test
public void searchSelectPageTest() throws Exception {
boardDao.deleteAll();
for (int i = 1; i<=20; i++){
BoardDto boardDto = new BoardDto("title"+i, "asdfasdfasdf", "hong");
boardDao.insert(boardDto);
}
SearchCondition sc = new SearchCondition(1, 10, "title2", "T");
List<BoardDto> list = boardDao.searchSelectPage(sc);
System.out.println("list = " + list);
assertTrue(list.size()==2); // 1~20, title2, title20
}
boardDao.searchResultCnt
는 검색결과 게시글의 총 갯수를 찾아오는 sql문.
boardDao.searchSelectPage
는 검색결과 현재 페이지에서 무슨 게시글들을 리스트로 보여줄지정하는 sql문.
해결
pom파일에 https://mvnrepository.com/artifact/org.bgee.log4jdbc-log4j2/log4jdbc-log4j2-jdbc4.1 디펜던시를 추가하여 테스트 클래스에서 실제 돌아가는 쿼리를 확인했다.
SELECT count(*) FROM board WHERE true AND (title LIKE concat('%', 'T', '%') OR content LIKE concat('%', 'T', '%'))
아래는 boardMapper.xml에 작성한 sql.
<sql id="searchCondition">
<choose>
<when test='option=="T"'>
AND title LIKE concat('%', #{keyword}, '%')
</when>
<when test='option=="W"'>
AND writer LIKE concat('%', #{keyword}, '%')
</when>
<otherwise>
AND (title LIKE concat('%', #{keyword}, '%')
OR content LIKE concat('%', #{keyword}, '%'))
</otherwise>
</choose>
</sql>
<select id="searchSelectPage" parameterType="SearchCondition" resultType="BoardDto">
SELECT bno, title, content, writer, view_cnt, comment_cnt, reg_date
FROM board
WHERE true
<include refid="searchCondition"/>
ORDER BY reg_date DESC, bno DESC
LIMIT #{offset}, #{pageSize}
</select>
<select id="searchResultCnt" parameterType="SearchCondition" resultType="int">
SELECT count(*)
FROM board
WHERE true
<include refid="searchCondition"/>
</select>
#{keyword}
는 검색어가 들어가야 할 자리인데 title을 검색하는지 content를 검색하는지 구분하는 option의 값이 넘어가고 있었다.
알고보니 생성자의 매개변수 순서와 함수를 호출할때 입력한 인자의 값 순서가 달라서 발생한 문제였다.
그렇게 다 고친줄 알았으나...이번엔 맨 마지막 페이지만 안나오는 문제가 생겼다.
32개를 찾았으면 총 4페이지에 마지막 페이지에 2개의 게시글이 나와야 하는데 3페이지까지만 출력되었다.
마찬가지로 URL에 직접 page값을 수정하면 정상적으로 조회할 수는 있었다.
- PageHandler -
public void doPaging(int totalCnt, SearchCondition sc){
this.totalCnt = totalCnt;
this.sc = sc;
totalPage = (int)Math.ceil(totalCnt / sc.getPageSize()); // 나눈 후 남는 게시글이 있는 경우 페이지가 하나 더 필요하므로 올림처리
beginPage = (sc.getPage()-1) / NAV_SIZE * NAV_SIZE + 1;
endPage = Math.min(beginPage + NAV_SIZE -1, totalPage); // 최대 페이지가 네비게이션의 크기보다 작을 시 최대페이지 수가 endPage가 됨
showPrev = beginPage != 1;
showNext = endPage != totalPage;
}
위는 변경 전, 아래는 변경 후.
public void doPaging(int totalCnt, SearchCondition sc){
this.totalPage = totalCnt / sc.getPageSize() + (totalCnt % sc.getPageSize()==0? 0:1);
this.sc.setPage(Math.min(sc.getPage(), totalPage)); // page가 totalPage보다 크지 않게
totalPage = totalCnt / sc.getPageSize() + (totalCnt % sc.getPageSize()==0? 0:1); // 나눈 후 남는 게시글이 있는 경우 페이지가 하나 더 필요하므로 올림처리
beginPage = (this.sc.getPage() -1) / NAV_SIZE * NAV_SIZE + 1;
endPage = Math.min(beginPage + NAV_SIZE - 1, totalPage); // 최대 페이지가 네비게이션의 크기보다 작을 시 최대페이지 수가 endPage가 됨
showPrev = beginPage != 1;
showNext = endPage != totalPage;
}
PageHandler
클래스에서 페이지 계산이 틀렸었다.
페이지 계산은 실수하기 쉬운 부분이므로 반드시 테스트를 하며 진행하자.
회원가입 기능을 만들때 sns(페북, 인스타, 트위터 중 택1)는 라디오 버튼으로, birth는 input date로 입력을 받아 넘기려는데 400에러가 발생해서 일단 이 두가지는 sql에서 하드코딩하도록 해두었다.
같은 기능을 사용하는 로그인페이지는 괜찮은데 왜 오류가 날까?
덧글 Ajax로 불러오는게 어려움.