오늘은 화면에 책 리스트를 띄워볼 예정이다.
URL을 localhost/list 로 들어가면 데이터베이스에 담겨있는 책들의 정보가 리스트로 출력되도록.
그리고 detail, edit, delete 그리고 search 까지 해볼 예정이다.
BookController
일단 어제 만든 BookController에서 코드를 추가해보자. 완성본은 아니고 매핑이 되는지 확인용도
/*
요청URI : /list?keyword=알탄 or /list or /list?keyword=
요청파라미터 : keyword=알탄
요청방식 : get
required=false : 선택사항. 파라미터가 없어도 무관
*/
@RequestMapping(value="/list", method=RequestMethod.GET)
public ModelAndView list(ModelAndView mav) {
log.info("list에서 왔다 : " + mav);
// Model : 데이터
// View : jsp
// forwarding
mav.setViewName("book/list");
return mav;
}
어제와 다른 점은 ModelAndView가 파라미터에서 선언되었다는 점이다.
또 하나 setViewName은 jsp의 경로를 적어넣어야 하는데 list 뒤에 .jsp를 붙히지 않았고, book 앞에 경로를 설정하지 않았다.
이게 가능한 이유는
이 부분 덕분에 book/list 앞 뒤로 있어야 할 경로가 자동으로 들어가기 때문이다.
list.jsp
그 다음 list.jsp를 만들어볼껀데 JSP 템플릿을 JSTL이 포함되어있는 템플릿을 하나 만들도록 하자
위 경로와 같이 jsp를 만들어주고 finish가 아닌 Next를 눌러준다.
오른쪽 하단에 JSP Template을 눌러주고
New 버튼 클릭
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
${cursor}
</body>
</html>
그럼 위와 같은 기본적으로 JSTL이 담긴 JSP 템플릿이 만들어진다. 그대로 Finish를 눌러주자
다행히 매핑이 잘 되서 jsp를 불러오고 있는 모습이니 Controller로 다시 넘어가 이어서 작성해보자
Controller 이어서 작성
@RequestMapping(value="/list", method=RequestMethod.GET)
public ModelAndView list(ModelAndView mav) {
log.info("list에서 왔다 : " + mav);
// 도서 목록
List<BookVO> bookVOList = this.bookService.list();
log.info("list->bookVOList : " + bookVOList);
// Model : 데이터
// View : jsp
// forwarding
mav.setViewName("book/list");
return mav;
}
.list()에 오류가 난다고 뜰테지만 bookService에 list메소드가 없기 때문이니
어제와 마찬가지로 f2룰 눌러 BookService 인터페이스에 메소드를 만들어주자
이어서 쭉 Impl과 Dao까지 설정을 해주자
책 리스트를 SQL에서 Select문으로 가져올것이고, 1개 이상이기 때문에 SelectList로 연결되어야 한다.
"book.selectList"는 book이라는 namespase에 select태그의 id값이 list라는 태그로 연결한다는 뜻
이제 book_sql.xml로 넘어가준다.
book_SQL.xml
<select id="list" resultType="bookVO">
select book_id title, category, price, insert_date
from book
order by book_id desc
</select>
위 코드는 SQL의 book 테이블에서 book_id, title, categort, price, insert_data를 "역순(최신순)"으로 뽑아 내 bookVO에 담아 리턴해준다.
이제 최종적으로 Controller를 완성하자
@RequestMapping(value="/list", method=RequestMethod.GET)
public ModelAndView list(ModelAndView mav) {
log.info("list에서 왔다 : " + mav);
List<BookVO> bookVOList = this.bookService.list();
log.info("list->bookVOList : " + bookVOList);
// Model : 데이터
mav.addObject("bookVOList", bookVOList);
// View : jsp
// forwarding
mav.setViewName("book/list");
return mav;
}
JSP도 마저 설정을 해줘야 한다. 값을 불러와야 하기 때문
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<title>도서 목록</title>
</head>
<body>
<h3>도서 목록</h3>
<!-- mav.addObject("bookVOList", bookVOList); -->
<p>${bookVOList}</p>
</body>
</html>
SQL과 JSP에서 출력되는 정보가 동일하게 출력된다.
SQL에 책 정보를 대량으로 한번에 넣는 코드
/
DECLARE
BEGIN
for i in 1..127 loop
insert into book(book_id, title, category, price, insert_date)
values (
(select nvl(max(book_id),0)+1 from book), -- 마지막 번호에서 +1
'제목'||i, -- 제목 + i(반복)
'소설',
trunc(dbms_random.value(10000,50000)), -- 만원~5만원
sysdate
);
end loop;
END;
/
1~127번 반복되는 SQL/PL 코드이다. 여기선 카테고리명을 소설로 한정했다.
마지막엔 항상 commit을 해주자.
그럼 이제 뭘 해야 할까?
저 많은 책 정보를 JSP에 출력해봐야한다. 이걸 쉽게 하기 위해 JSTL을 넣은거니까
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<title>도서 목록</title>
</head>
<body>
<h3>도서 목록</h3>
<!-- mav.addObject("bookVOList", bookVOList); -->
<%-- <p>${bookVOList}</p> --%>
<table border="1">
<thead>
<tr>
<th>번호</th>
<th>제목</th>
<th>카테고리</th>
<th>가격</th>
<th>등록일</th>
</tr>
</thead>
<tbody>
<!--
forEach 태그? 배열(String[], int[][]), Collection(List, Set) 또는
Map(HashTable, HashMap, SortedMap)에 저장되어 있는 값들을
순차적으로 처리할 때 사용함. 자바의 for, do~while을 대신해서 사용함
var : 변수
items : 아이템(배열, Collection, Map)
varStatus : 루프 정보를 담은 객체 활용
- index : 루프 실행 시 현재 인덱스(0부터 시작)
- count : 실행 회수(1부터 시작. 보통 행번호 출력)
-->
<!-- data : mav.addObject("bookVOList", bookVOList); -->
<!-- row : bookVO 1행 -->
<c:forEach var="bookVO" items="${bookVOList}" varStatus="stat">
<tr>
<td>${stat.count}</td>
<td>${bookVO.title}</td>
<td>${bookVO.category}</td>
<td>${bookVO.price}</td>
<td>${bookVO.insertDate}</td>
</tr>
</c:forEach>
</tbody>
</table>
</body>
</html>
근데 이제보니 가격쪽이 좀 불편하다. 천단위 컴마가 안찍혀 나온다.
코드수정을 좀 해보자
우선 JSTL을 하나 더 추가해준다.
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
그 다음 book.price 부분을 바꿔주도록 하자
<td><fmt:formatNumber value="${bookVO.price}" pattern="#,###" />원</td>
이러면 천단위 컴마는 찍히게 된다.
도서 등록
도서 등록을 위한 a태그를 만들고 누르면 이전에 만든 create.jsp로 넘어가게 된다.
어제 만들어놓은 createPost의 redirect 부분을 /list로 변경해 도서등록이 완료되면 list로 넘어가도록 해주자.
SELECT문 SQL에 DESC를 걸어주었기 때문에 역순으로 출력이 되므로 가장 최근에 넣은 정보가 먼저 나온다.
이제 제목을 누르면 상세정보가 뜨도록 해보자
상세 정보
결국 위 했던 내용대로 하면 된다. controller랑 jsp 매핑하고 Service, Dao 호출해서 연결하고 SQL문 짜고... 의 반복인 것
일단 링크부터 연결해주자. jsp에서 title출력부분을 수정해주자
<td><a href="/detail?bookId=${bookVO.bookId}">${bookVO.title}</a></td>
그리고 detail.jsp는 create.jsp와 모양이 비슷하니까 복사해서 이름만 바꿔주자
//책 상세보기
//요청된 URI 주소 : http://localhost/detail?bookId=3
//요청파라미터, 쿼리 스트링(Query String) : bookId=3
//요청방식 : get
//매개변수 : bookVO => {"bookId":"3","title":"null","category":"null","price":0(null),"insertDate":"null"}
@RequestMapping(value="/detail")
public ModelAndView detail(BookVO bookVO, ModelAndView mav) {
log.info("detail -> bookVO : " + bookVO);
// bookVO = this.bookService.detail(bookVO);
// Model : 데이터
mav.addObject("title", "도서 상세");
// mav.addObject("bookVO", bookVO);
// View : jsp
mav.setViewName("book/detail");
return mav;
}
Controller에 코드를 추가해주고 /detail로 들어가보면
create를 복사해오고 수정을 안했기 때문에 create와 똑같은 페이지가 출력된다.
(제목에 걸려있는 링크를 타고 들어가면 오류가 나니 주의하자)
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>Insert title here</title>
</head>
<body>
<h1>책 등록</h1>
<!-- mav.addObject("title","도서 생성") -->
<h5>${title}</h5>
<p>${bookVO}</p>
<!--
요청URI : /crate
요청파라미터 : {title=개똥이의 모험, category=소설, price=12000}
요청방식 : post
-->
<form action="/create" method="post">
<p>제목 : <input type="text" name="title" required placeholder="타이틀"></p>
<p>카테고리 : <input type="text" name="category" required placeholder="카테고리"></p>
<p>가격 : <input type="text" name="price" required placeholder="가격"></p>
<p>
<input type="button" id="edit" value="수정">
<input type="button" id="delete" value="삭제">
<input type="button" id="list" value="목록">
</p>
</form>
</body>
</html>
이렇게 수정해 준 뒤 버튼마다 각각 이벤트를 달아주자
jQuery를 사용할것이기 때문에
이 두 파일을 각각 압축을 푼 뒤
위 사진과 같은 경로에 넣어준다(js폴더에 빨간 X표시는 무시해도 된다. 돌아가는데 문제 없음)
이제 jsp 상단 스크립트 영역에
<script type="text/javascript" src="/resources/js/jquery.min.js"></script>
를 추가해주면 jQuery를 사용할 수 있다.
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<!-- static folder설정(정적 폴더 설정)=>css, images, upload, js
서버에서 앞서 처리될 필요가 없는 정적 리소스 파일을 처리하는 역할 수행
웹 애플리케이션의 물리적 경로 이름을 설정하고 이 경로에 정적 리소스 파일들을 저장하면
소스 코드나 웹 브라우저의 주소창에서 해당 리소스의 경로를 사용하여 직접 접속할 수 있음
정적 리소스란 클라이언트에서 요청이 들어왔을 때 요청 리소스가 이미 만들어져 있어 그대로 응답하는 것
mapping : 웹 요청 경로 패턴을 설정. 컨텍스트 경로를 제외한 나머지 부분의 경로와 매핑
location : 웹 애플리케이션 내에서 실제 요청 경로의 패턴에 해당하는 자원 위치를 설정. 위치가 여러 곳이면 각 위치를 쉼표로 구분
-->
<resources mapping="/resources/**" location="/resources/" />
servlet-context.xml
<script type="text/javascript">
// document 내의 모든 요소들이 로딩된 후에 실행
// 1개의 jsp에 1번만 사용할 것
$(function(){
// id=list인 값을 찾아 클릭할 때 함수가 실행된다
$('#list').on('click', function(){
// 해당 경로로 이동
location.href="/list";
})
});
</script>
detail에 스크립트를 추가하고 목록 버튼을 누르면 /list로 이동된다.
이제 controller의 코드에서 주석처리되었던
// bookVO = this.bookService.detail(bookVO);
// mav.addObject("bookVO", bookVO);
의 주석을 풀어주고
Service, Dao를 연결해주자
(...스킵...)
Dao쪽에서 1개의 정보만을 가져올 것이기 때문에 selectOne으로 만들어줘야 한다.
public BookVO detail(BookVO bookVO) {
return this.sqlSessionTemplate.selectOne("book.detail", bookVO);
}
이제 xml로 넘어가 쿼리문을 작성하자
<select id="detail" resultType="bookVO" parameterType="bookVO">
select book_id, title, category, price, insert_date
from book
where book_id = ${bookId}
</select>
이제 서버를 재가동 하고 list에 있는 제목 링크를 타고 들어가게 되면
book_id가 127번인 책의 detail 페이지에 데이터가 잘 담겨가게 됀다.
detail.jsp의 p태그 안에 있는 input의 value값들을 넣어주고 readonly를 넣으면 수정이 불가능하게 만들어준다.
(넣는 김에 class명도 하나씩 넣어주자)
<p>제목 : <input type="text" name="title" value="${bookVO.title}"
class="formData" required placeholder="타이틀" readonly></p>
<p>카테고리 : <input type="text" name="category" value="${bookVO.category}"
class="formData" required placeholder="카테고리" readonly></p>
<p>가격 : <input type="text" name="price" value="${bookVO.price}"
class="formData" required placeholder="가격" readonly></p>
그럼 이제 수정버튼을 누르면 readonly가 지워지면서 수정이 가능하도록 스크립트를 작성해보자.
// 수정모드로 전환
// id=edit인 버튼을 찾아 누르면 함수가 실행된다.
$('#edit').on('click', function(){
// class="formData"의 readonly 속성을 제거한다.
$('.formData').removeAttr("readonly");
})
$('.formData')로 class명이 formData인 요소들을 모두 불러온 뒤 readonly속성을 제거해줬다. 이제 수정이 가능한 상태로 변경된다.
그렇다면 수정모드로 전환되었을 때 버튼이 변경되어야 한다.
수정모드로 들어갔을때 수정, 삭제, 목록 버튼이 아닌 확인, 취소 버튼이 뜨도록 해보려고 한다.
jsp를 수정해보도록 하자.
<!-- 일반모드 시작 -->
<p id="p1">
<input type="button" id="edit" value="수정">
<input type="button" id="delete" value="삭제">
<input type="button" id="list" value="목록">
</p>
<!-- 일반모드 끝 -->
<!-- 수정모드 시작 -->
<p id="p2" style="displat:none;">
<input type="button" id="confirm" value="확인">
<input type="button" id="cancel" value="취소">
</p>
<!-- 수정모드 끝 -->
평소에는 p1의 버튼들만 보이다가 수정버튼이 눌리면 p1은 사라지고 p2의 버튼들이 보이도록 만들어보려고 한다
스크립트를 수정해보자
let title = "${bookVO.title}"
let category = "${bookVO.category}"
let price = "${bookVO.price}"
// document 내의 모든 요소들이 로딩된 후에 실행
// 1개의 jsp에 1번만 사용할 것
$(function(){
// id=list인 값을 찾아 클릭할 때 함수가 실행된다
$('#list').on('click', function(){
// 해당 경로로 이동
location.href="/list";
})
// 수정모드로 전환
// id=edit인 버튼을 찾아 누르면 함수가 실행된다.
$('#edit').on('click', function(){
$('#p1').css("display", "none")
$('#p2').css("display", "block")
// class="formData"의 readonly 속성을 제거한다.
$('.formData').removeAttr("readonly");
})
//취소버튼 클릭
$("#cancel").on("click",function(){
$("#p1").css("display","block");
$("#p2").css("display","none");
//readonly 속성 추가
$(".formdata").attr("readonly",true);
//입력란 초기화
//태그로 접근하면 태그만 슨다
$("input[name='title']").val(title);
$("input[name='category']").val(category);
$("input[name='price']").val(price);
console.log("title:"+title+",category:"+category+",price"+price);
});
});
위 코드로 인해 수정 버튼을 누르면 수정이 가능하고, 취소 버튼을 누를 시 readonly 속성이 다시 부여되고, 원래 데이터가 들어가게 된다.
이제 수정버튼을 누른 후 확인을 누르면 수정이 되도록 만들어야 한다.
Edit(Update)
우선 jsp를 조금 변경해주자
<form id="frm" name="frm" action="/updatePost" method="post">
<input type="hidden" name="bookId" value="${bookVO.bookId}">
form에 id와 name값을 넣어주고 action값을 변경해주자
form안에 name속성이 있는 모든 데이터들의 값이 updatePost를 받는 controller로 넘어가게 될 것이다.
수정하려면 쿼리문 안에 bookId의 값이 들어있어야 하기 때문에 input type="hidden"으로 보이지 않게 만들어 준 뒤 name값을 부여해 form이 전송될 때 같이 parameter로 넘어가게 된다.
// 요청URI : /updatePost
// 요청파라미터 : {bookId=127, title=개똥이의 모험2, category=소설2, price=12002}
// 요청방식 : post
@RequestMapping(value="/updatePost", method=RequestMethod.POST)
public ModelAndView updatePost(BookVO bookVO, ModelAndView mav) {
log.info("updatePost->bookVO : " + bookVO);
int result = this.bookService.updateVO(bookVO);
// redirect -> 새로운 URI를 재요청 /detail?bookId=127
mav.setViewName("redirect:/detail?bookId=" + bookVO.getBookId());
return mav;
}
}
컨트롤러에서 받아준다.
insert, update, delete의 반환값은 int값이기 때문에 int result로 선언해주어야 한다.
이제 또 다시 Service, Dao, SQL문을 작성하면 된다.
Delete
아.. 귀찮다...
우선 detail.jsp로 가서 delete버튼을 눌렀을 때 form의 action이 delestPost로 변경되게 해야한다.
스크립트를 수정해보자
// 삭제 버튼 클릭
$('#delete').on('click', function(){
$('#frm').attr('action','/deletePost');
let result = confirm("삭제하시겠습니까?")
// console.log("result : " + result)
// result : true(확인) / false(취소)
if(result > 0){ // true
$('#frm').submit();
alert("삭제되었습니다.");
}else{ // false
alert("삭제가 취소되었습니다.");
return;
}
})
삭제버튼을 클릭 시 form의 action 속성이 /deletePost로 변경되고, confirm을 통해 한 번 더 확인한다.
확인은 true, 취소는 false를 반환하며
if문을 통해 true(확인)일때 submit()을 하여 deletePost에 데이터를 전송하고,
false(취소)일 때 return을 하여 함수를 종료한다.
이제 controller에서 데이터를 받아주자
@RequestMapping(value="/deletePost", method=RequestMethod.POST)
public ModelAndView deletePost(BookVO bookVO, ModelAndView mav) {
log.info("deletePost->bookVO : " + bookVO);
int result = this.bookService.delete(bookVO);
log.info("deletePost->result : " + result);
mav.setViewName("redirect:/list");
return mav;
}
아까 말했다싶이 insert, update, delete는 반환값이 int값이기 때문에 int타입으로 선언해준다.
이제 또 반복이다. Service, Dao, SQL.xml을 연결해주고 결과를 실행해보자
(...스킵...)
취소버튼을 누르면 아무일도 일어나지 않는다.
잘 삭제되었다.
마지막으로 검색기능을 넣어주자
search
우선 jsp에 검색어를 입력할 input과 전송할 버튼을 만들어주자
<!-- action속성 및 값이 생략 시, 현재 URI(/list)를 재요청.
method는 GET(form 태그의 기본 HTTP 메소드는 GET임)
param : keyword=알탄
요청URI : /list?keyword=알탄 or /list or /list?keyword=
요청파라미터 : keyword=알탄
요청방식 : get
-->
<form>
<input type="text" name="keyword" value="" placeholder="검색어를 입력하세요">
<button type="button" id="btnButton">검색</button><br>
</form>
도서 등록이 들어있는 p태그 안에 넣어주자
이후 스크립트 태그 안에 아래 코드를 작성해주자.
$(function() {
$('#btnSearch').on('click',function(){
let keyword = $("input[name='keyword']").val();
// json오브젝트
let data = {
"keyword":keyword
};
console.log("data :", data);
$(this).parent().submit();
});
});
input요소 중 name값이 keyword인 요소의 value를 가져와서 data로 JSON화 시킨 후 submit을 시켜준 것.
이제 컨트롤러에서 작업을 해야하는데 조금 중요한 내용이 들어있다.
@RequestMapping(value="/list", method=RequestMethod.GET)
public ModelAndView list(ModelAndView mav,
@RequestParam(value="keyword", required=false, defaultValue="") String keyword) {
log.info("list에서 왔다 : " + mav);
log.info("list에서 왔다 : " + keyword);
// map("keyword":"알탄")
Map<String, Object> map = new HashMap<String, Object>();
map.put("keyword", keyword);
List<BookVO> bookVOList = this.bookService.list(map);
log.info("list->bookVOList : " + bookVOList);
// Model : 데이터
mav.addObject("bookVOList", bookVOList);
// View : jsp
// forwarding
mav.setViewName("book/list");
return mav;
}
우리가 작성한 list 메소드에 파라미터를 잘 살펴보면
@RequestParam(value="keyword", required=false, defaultValue="") String keyword)
이러한 어노테이션으로 들어가 있다.
required=false : 파라미터가 없어도 무관
defaultValue="" : 파라미터가 없을 시 null이 아닌 공백으로 처리
하라는 어노테이션이다.
받아온 keyword값을 map에 저장하게 되면 ("keyword":"알탄") 이런 형식으로 저장될 것이다. (key:value)
이제 list에 파라미터를 map으로 넣어주면 .list가 오류가 날 텐데
두번째 change 어쩌구를 눌러 service 인터페이스의 메소드를 변경하고 나머지 Impl과 Dao도 변경해주자.
SQL 작성하는 xml도 바꿔줘야 한다.
<!-- where 1 = 1은 늘 참임.
조건이 2개 이상일 때 WHERE + AND
조건이 1개일 때 WHERE이어야 함.
WHERE(생략)
AND => 오류 발생
==>
WHERE 1 = 1
AND(생략)
AND => 정상
True and True = True
True and False = False
keyword : null(/list)
keyword : "" (/list?keyword=) -->
<select id="list" resultType="bookVO" parameterType="hashMap">
SELECT BOOK_ID, TITLE, CATEGORY, PRICE, INSERT_DATE
FROM BOOK
WHERE 1 = 1
<if test="keyword != null and keyword != ''">
AND (TITLE LIKE '%'|| #{keyword} ||'%'
OR CATEGORY LIKE '%'|| #{keyword} ||'%'
OR PRICE LIKE '%'|| #{keyword} ||'%')
</if>
ORDER BY BOOK_ID DESC
</select>
parameterType을 추가해줘야 한다. map은 HashMap으로 선언했기 때문에 넣어주지 않으면 오류가 난다.
WHERE 1 = 1 조건문을 유연하게 추가하기 위한 절이다.
뒤에 이어지는 동적인 조건을 추가할 때 이 부분에 추가 조건을 쉽게 붙일 수 있다.
참고 : https://hyjykelly.tistory.com/5
WHERE 1 = 1 무조건 True 값이 나오는 문을 WHERE절 앞에 둘 경우 AND 절은 동적으로 이용이 가능해져 Param 값이 NULL일 경우 처리되지 않도록 할 수 있다.
MyBatis의 OGNL(Object-Graph Navigation Language) 표현식
<if test=" 조건문 ">
</if>
3일자 종료!
'Spring 실습' 카테고리의 다른 글
Spring 실습 6일차(ajax를 활용한 Create, Update, Delete) (0) | 2024.04.29 |
---|---|
Spring 실습 5일차(Tiles) (1) | 2024.04.26 |
Spring 2024-04-23 실습(2일차) (0) | 2024.04.23 |
Spring 2024-04-22 실습 (0) | 2024.04.22 |
Spring 개발 환경 설정 (4) | 2024.04.22 |