일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- json
- jsp
- 자바
- 프로그래머스
- 대분류/중분류/소분류
- Oracle
- MVC
- jsp 프로젝트
- 일단_해보는거야
- 고객센터 구현
- 로그인과 장바구니 구현
- 교보문고 따라하기
- Sts
- 코딩
- jakarta.mail
- js
- MySQL
- 스프링
- SESSION
- jquery
- ajax
- java
- 이메일로 인증코드 전송 구현
- Level 1
- Spring MVC
- 오라클
- 인증코드로 비밀번호 변경 구현
- 세션
- 다중 카테고리 구현
- Spring
감 잃지말고 개발하기
[JSP][MVC][MySQL] 도서 사이트에서 다중 카테고리 구현하기 #6. 데이터 수정하기 (1) 본문
지난 포스팅 "도서 사이트에서 다중 카테고리 구현하기 #5. 데이터 출력하기 (2)"에서 서버에서 가져온 데이터를 도서 상세 페이지에 출력시키는 클라 딴 로직을 정리해 보았다.
다중 카테고리 구현 #6부턴 총 3개의 포스팅에 걸쳐 도서 수정 페이지에서 관리자가 수정한 모든 도서 정보(카테고리 데이터 포함)를 DB까지 안전하게 저장시키는 서버 딴 로직을 정리해 보도록 하겠다.
이번 구현에서의 핵심은 관리자의 액션 한 번으로 2개 이상의 테이블 데이터를 건들면서 2개 이상의 DML 구문을 사용하는 DAO 클래스의 트랜잭션 연산에 있으므로 DAO 클래스 및 Service 클래스를 중점적으로 살펴볼 것이다.
♠ 해당 구현에 필요한 테이블 생성은 아래 포스팅을 참고하세요 ♠
2023.06.12 - [JSP/MVC] - [JSP][MVC][MySQL] 도서 사이트에서 다중 카테고리 구현하기 #1. 구현 틀 잡기
[JSP][MVC][MySQL] 도서 사이트에서 다중 카테고리 구현하기 #1. 구현 틀 잡기
"도서 등록 페이지 카테고리 구현하기" 포스팅에서는 클라 딴 페이지에서 카테고리 1개를 구현하는 로직을 정리해 보았다(아래 포스팅 참고). 이번 포스팅에서는 이것을 조금 더 응용해 다중 카
persimmon-ary-stepbystep.tistory.com
목표
♠ 트랜잭션의 ACID 원칙을 준수하여 데이터베이스 작업을 수행할 수 있다.
♠ 중복된 코드를 최소화하고 가독성 있는 코드를 지향한다.
로직 흐름
- 관리자가 도서 수정 페이지에서 정보를 수정한 후 "수정" 버튼을 클릭한다.
- 컨트롤러에서 해당 URL을 매핑해 해당 Action 클래스를 호출한다.
- Action 클래스에선 form 태그로 넘어온 도서 정보를 받은 후 DB 관련 비즈니스 로직을 처리하는 Service 클래스를 호출한다.
- Service 클래스에선 실제 DB와의 작업을 연계하는 DAO 클래스를 호출한다.
- DAO 클래스에 필요한 트랜잭션을 수행한다.
- DB 처리가 완료되면 페이지 관련 처리가 이루어진다.
로직 코드 및 실행 화면
기본적인 MVC 패턴의 흐름 과정은 생략한다.
1. 관리자 수정 페이지(bookView.jsp)
포스팅 주제에 집중하기 위해 대/중/소분류 select 옵션을 가져오는 JS 코드 및 분류 추가 / 삭제 버튼 JS 코드, 유효성 검사 코드는 생략하도록 한다.
1-1. ▼ 도서 정보 영역 HTML
<form action="/manager/bookManage/bookModifyPro.ma" method="post"
name="bookForm" enctype="multipart/form-data">
<div class="form-row">
<input type="hidden" name="b_id" value="${book.b_id }" >
<div class="form-group col-md-4">
<label>도서 이미지</label>
<div id="uploadResult" class="my-2">
<!-- 도서 이미지가 들어가는 곳 -->
<img id="bookImage"
src="${request.getSession().getServletContext().getRealPath('/')}/bookImage/${book.b_image}"
class="img-fluid rounded mx-auto d-block" width="230px;" />
<!-- 도서 이미지가 들어가는 곳 -->
</div>
<div class="custom-file my-4">
<input type="file" class="custom-file-input" name="b_image" id="b_image" accept=".jpg,.png"
onchange="setThumbnail(event);">
<label class="custom-file-label" for="b_image">파일 선택</label>
</div>
</div>
<div class="form-row col-md-8 form_row_8_div">
<div class="col-12">
<div class="form-group">
<label for="b_name">도서명</label>
<input type="text" class="form-control" placeholder="도서명을 입력하세요"
name="b_name" id="b_name" required="required" value="${book.b_name }">
</div>
</div>
<div class="col-4">
<div class="form-group">
<label for="b_price">정가</label>
<div class="input-group input-group-merge">
<input type="text" class="form-control" placeholder="정가를 입력하세요"
aria-label="Amount (to the nearest dollar)" name="b_price" id="b_price"
required="required" value="${book.b_price }"/>
<span class="input-group-text">
<i class="fa fa-krw" aria-hidden="true" style="font-size: medium;"></i>
</span>
</div>
</div>
</div>
<div class="col-4">
<div class="form-group">
<label>재고 현황</label>
<select id="inputState" class="form-control">
<option selected>선택하세요</option>
<option>미입고</option>
<option>입고</option>
<option>품절</option>
<option>절판</option>
</select>
</div>
</div>
<div class="col-4">
<div class="form-group">
<label for="b_page">쪽수</label>
<div class="input-group input-group-merge">
<input type="text" class="form-control" name="b_page" id="b_page"
required="required" value="${book.b_page }">
<span class="input-group-text">쪽</span>
</div>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="b_writer">저자</label>
<input type="text" class="form-control" placeholder="저자가 2명이상인 경우 ,를 써주세요"
name="b_writer" id="b_writer" required="required" value="${book.b_writer }">
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="b_translator">옮긴이</label>
<input type=text" class="form-control" placeholder="옮긴이를 적어주세요"
name="b_translator" id="b_translator" value="${book.b_translator }">
</div>
</div>
<div class="col-4">
<div class="form-group">
<label for="b_publisher">출판사</label>
<input type="text" class="form-control" name="b_publisher" id="b_publisher"
required="required" value="${book.b_publisher }">
</div>
</div>
<div class="col-4">
<div class="form-group">
<label for="b_publish_date">출판일</label>
<input type="date" class="form-control" value="${book.b_publish_date }"
name="b_publish_date" id="b_publish_date" required="required"/>
</div>
</div>
<div class="row col-12 mb-3 wrapper">
<div class="col-md-6 col-xl-3">
<div class="form-group">
<label for="b_main_catgy">카테고리 대분류</label>
<select class="form-control main_select" name="b_main_catgy">
<option value="">전체</option>
</select>
</div>
</div>
<div class="col-md-6 col-xl-3">
<div class="form-group">
<label for="b_middle_catgy">카테고리 중분류</label>
<select class="form-control middle_select" name="b_middle_catgy">
<option value="">--</option>
</select>
</div>
</div>
<div class="col-md-6 col-xl-3">
<div class="form-group">
<label for="b_sub_catgy">카테고리 소분류</label>
<select class="form-control sub_select" name="b_sub_catgy">
<option value="">--</option>
</select>
</div>
</div>
<div class="col-md-6 col-xl-3">
<div class="form-group mt-4">
<button type="button" class="btn btn-secondary add_catgy_btn">분류 추가
<span class="btn-icon-right"><i class="fa fa-plus"></i></span>
</button>
</div>
</div>
</div>
<!-- 서브 카테고리 박스 -->
<div class="col border rounded border-secondary mx-2 p-3 add_div"
style="box-shadow: 5px 10px #716a8f;">
<c:if test="${book.bookSubCatgyList != null }">
<c:forEach items="${book.bookSubCatgyList}" var="book_sub_catgy" varStatus="status">
<div class="row col-12 wrapper wrapper_div${status.count }">
<div class="col-md-6 col-xl-3 mb-2">
<label for="b_main_catgy">카테고리 대분류</label>
<select class="form-control main_select" name="b_main_catgy">
</select>
</div>
<div class="col-md-6 col-xl-3 mb-2">
<label for="b_middle_catgy">카테고리 중분류</label>
<select class="form-control middle_select" name="b_middle_catgy">
</select>
</div>
<div class="col-md-6 col-xl-3 mb-2">
<label for="b_sub_catgy">카테고리 소분류</label>
<select class="form-control sub_select" name="b_sub_catgy">
</select>
</div>
<div class="col-md-6 col-xl-3 mb-2 mt-4">
<button type="button" class="btn btn-warning delete_catgy_btn">삭제
<span class="btn-icon-right"><i class="fa fa-close"></i></span>
</button>
</div>
</div>
</c:forEach>
</c:if>
</div>
<!-- 서브 카테고리 박스 -->
</div>
</div>
<div class="form-row">
<div class="form-group col-xxl-12">
<label for="b_content">책 설명</label>
<textarea class="form-control" rows="10" wrap="off" id="b_content"
name="b_content" required="required">${book.b_content }</textarea>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox">
<label class="form-check-label">Check me out</label>
</div>
</div>
</div>
<button type="reset" class="btn btn-outline-warning btn-lg">초기화</button>
<button type="button" class="btn btn-outline-info btn-lg"
onClick="window.location.href='/manager/bookManage/bookList.ma'">
등록된 도서 목록
</button>
<button type="submit" class="btn btn-outline-primary btn-lg regist_btn">수정</button>
<button type="button" class="btn btn-outline-danger btn-lg"
onclick="window.location.href='/manager/bookManage/bookDeletePro.ma?b_id=${book.b_id}'">
삭제
</button>
</form>
2. 컨트롤러
해당 Action 클래스를 호출한다.
@WebServlet("*.ma")
public class ManagerFrontController extends HttpServlet{
private static final long serialVersionUID = 1L;
protected void doProcess(HttpServletRequest req, HttpServletResponse resp)
throws ServerException, IOException, ServletException {
req.setCharacterEncoding("UTF-8");
/* 0. 관리자가 아니면 접근 불가 */
HttpSession session = req.getSession(false);
boolean s_isLogin = (boolean)session.getAttribute("isLogin");
String s_userId = (String)session.getAttribute("userId");
if(s_isLogin != true || !s_userId.equals("manager")) {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("<script>");
out.println("alert('접근 불가한 계정입니다.')");
out.println("location.href='/bookShopMain.ok';");
out.println("</script>");
}
String requestURI = req.getRequestURI();
String contextPath = req.getContextPath();
String command = requestURI.substring(contextPath.length());
/* 2. 각 요청 주소의 매핑 처리 */
ActionForward forward = null;
Action action = null;
// 관리자 도서 정보 수정 처리 요청
if (command.equals("/manager/bookManage/bookModifyPro.ma")) {
action = new ManageBookModifyProAction();
try {
forward = action.execute(req, resp);
} catch (Exception e) {
e.printStackTrace();
}
}
/* 3. 포워딩 처리 */
if(forward != null) {
if(forward.isRedirect()) {
// redirect 방식
resp.sendRedirect(forward.getPath());
} else {
// forward 방식
RequestDispatcher dispatcher = req.getRequestDispatcher(forward.getPath());
dispatcher.forward(req, resp);
}
}
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServerException, IOException, ServletException {
doProcess(req, resp);
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServerException, IOException, ServletException {
doProcess(req, resp);
}
}
3. Action 클래스(ManageBookModifyProAction.java)
특정 도서의 정보 수정 요청을 처리하는 클래스이다.
☞ Action 클래스에서의 처리
Action 클래스에서 필요한 처리는 다음과 같다.
- MultipartRequest 객체를 사용해 브라우저에서 전송된 파라미터를 Book 타입의 객체에 저장한다.
- 이미지 파일이 변경되지 않은 경우에 대한 처리를 한다.
- DB 처리를 위해 Service 클래스를 호출한다.
- DB 처리 결과에 따른 페이지 이동에 대해 처리한다.
☞ 서브 카테고리를 저장할 int형 배열 생성
도서 수정 페이지에서 모든 소분류 값은 name="b_sub_catgy"으로 저장된다(1-1. HTML 코드 참고).
때문에 이 파라미터 값들을 배열로 받았을 때, [0]의 값은 메인 카테고리의 값이 된다.
서브 카테고리만 받아오기 위해 아래와 같이 처리한다.
String[] catgy_arr = multi.getParameterValues("b_sub_catgy");
int[] sub_catgy_arr = new int[catgy_arr.length-1]; // 서브 분류 데이터만 넣을 새로운 배열 생성
for (int i = 0; i < sub_catgy_arr.length; i++) {
// catgy_arr[1]부터 저장되도록 처리
sub_catgy_arr[i] = Integer.parseInt(catgy_arr[i+1]);
}
☞ 이미지 파일이 변경되지 않은 경우에 대한 처리
관리자가 도서 수정 페이지에서 도서 이미지를 변경하지 않을 수도 있다.
아래 코드는 기존 DB 테이블에 저장되어 있는 도서 이미지 파일명을 받아와 브라우저에서 건너온 데이터와 비교한 후 알맞게 처리하는 작업이다.
String oldFileName = service.getBookImage(book.getB_id());
// 이전 파일명과 변경 파일명이 동일한지 비교
boolean isSameFile = oldFileName.equals(book.getB_image());
if(book.getB_image() == null) {
// 사진이 변경되지 않은 경우, multi.getFilesystemName("b_image")의 값은 null이다.
book.setB_image(oldFileName);
}
☞ deleteFile(String path, String fileName);
해당 메서드는 첫 번째 인자에 해당하는 서버 경로를 찾아 두 번째 인자에 해당하는 파일을 삭제하는 역할을 수행한다.
이 메서드를 이용해 이미지 파일이 변경되었다는 전제 하에
도서 정보 수정 실패 시 관리자가 변경한 파일명이, 도서 정보 수정 성공 시 이전 파일명(기존 DB 테이블에 저장된 이미지 파일명)이 삭제되도록 처리할 것이다.
private void deleteFile(String path, String fileName) {
File deleteFile = new File(path+"/"+fileName);
if(deleteFile.exists()) {
deleteFile.delete();
System.out.println(" M.BookModifyPro.A : 파일 삭제 완료");
}
}
▼ 전체 코드
package action;
import ...
public class ManageBookModifyProAction implements Action {
@Override
public ActionForward execute(HttpServletRequest req, HttpServletResponse resp) throws Exception {
String realFolder = ""; // 파일이 업로드될 서버 상의 실제 경로 저장
String saveFolder = "/bookImage"; // 파일을 업로드할 디렉토리명을 지정
int fileSize = 5*1024*1024; // 한번에 업로드할 수 있는 파일 크기(5Mbyte)
/* 파일이 업로드될 서버 상의 물리적인 경로 얻어오기 */
ServletContext context = req.getServletContext();
realFolder = context.getRealPath(saveFolder);
/* MultipartRequest 클래스 생성자를 사용하여 객체를 생성하는 순간 바로 서버 상에 파일 업로드.*/
MultipartRequest multi = new MultipartRequest(req,
realFolder,
fileSize,
"UTF-8",
new DefaultFileRenamePolicy());
/* 카테고리 데이터 저장(서브 분류 데이터만 저장할 것)
* [0]의 값은 메인 분류 데이터 값이다!
* 1. String형 배열을 int형 배열로 전환
* 2. 배열의 값들을 순회하며 처리 */
String[] catgy_arr = multi.getParameterValues("b_sub_catgy");
int[] sub_catgy_arr = new int[catgy_arr.length-1];
for (int i = 0; i < sub_catgy_arr.length; i++) {
sub_catgy_arr[i] = Integer.parseInt(catgy_arr[i+1]);
}
Book book = new Book();
book.setB_id(Integer.parseInt(multi.getParameter("b_id")));
book.setB_name(multi.getParameter("b_name"));
book.setB_writer(multi.getParameter("b_writer"));
book.setB_translator(multi.getParameter("b_translator"));
book.setB_publisher(multi.getParameter("b_publisher"));
book.setB_bc_code(Integer.parseInt(catgy_arr[0]));
book.setB_price(Integer.parseInt(multi.getParameter("b_price")));
book.setB_image(multi.getFilesystemName("b_image"));
book.setB_page(Integer.parseInt(multi.getParameter("b_page")));
book.setB_publish_date(multi.getParameter("b_publish_date"));
book.setB_content(multi.getParameter("b_content"));
System.out.println(" M.BookModifyPro.A : 새로 저장될 책 정보 - "+book.toString());
/* DB 처리
* 1. 이미지 처리를 위한 이미지 정보를 가져오는 메서드 호출
* 1. 사진이 변경되지 않은 경우 DB에 저장되어 있는 파일명을 book 객체의 b_image값으로 세팅
* 2. 변경할 도서 정보 저장하는 메서드 호출
*/
ManageBookModifyProService service = new ManageBookModifyProService();
String oldFileName = service.getBookImage(book.getB_id());
boolean isSameFile = oldFileName.equals(book.getB_image());
if(book.getB_image() == null) {
book.setB_image(oldFileName);
}
// 2.
boolean isModifySuccess = service.modifyBook(book, sub_catgy_arr);
/* DB 처리 결과에 따른 페이지 이동 처리
* 성공시 /bookList.ok로 이동
* .ok -> .ok : redirect */
ActionForward forward = null;
if(!isModifySuccess) {
// isRegistSuccess가 false일 때(도서 수정 실패)
// 이전 파일명과 새로운 파일명이 다른 경우 서버에 새로운 파일명 삭제 처리
if(!isSameFile) {
deleteFile(realFolder, book.getB_image());
}
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("<script>");
out.println("alert('수정 실패')");
out.println("history.back(-1)");
out.println("</script>");
} else {
// 도서 수정 성공
// 이전 파일명과 새로운 파일명이 다른 경우 서버에 저장된 이전 파일명 삭제 처리
if(!isSameFile) {
deleteFile(realFolder, oldFileName);
}
forward = new ActionForward();
forward.setRedirect(true);
forward.setPath("/manager/bookManage/bookView.ma?b_id="+book.getB_id());
}
return forward;
}
private void deleteFile(String path, String fileName) {
File deleteFile = new File(path+"/"+fileName);
if(deleteFile.exists()) {
deleteFile.delete();
System.out.println(" M.BookModifyPro.A : 파일 삭제 완료");
}
}
}
4. Service 클래스(ManageBookModifyProService.java)
특정 도서 정보 수정을 처리하는 비즈니스 로직을 구현한 클래스이다.
4-1. modifyBook()
특정 도서 한 권의 정보를 수정하는 메서드이다.
☞ 처리 흐름
해당 메서드에서의 처리 흐름은 다음과 같다.
- boolean형 변수 isModifySuccess를 선언하고 초기값을 false로 설정한다. 해당 변수는 도서 수정이 성공했는지 여부를 나타낸다.
- BookDAO 객체를 생성하고 커넥션 풀에서 Connection 객체를 얻어와 BookDAO 객체에 Connection 객체를 주입한다.
- 도서 정보를 수정하는 updateBook() 메서드를 호출하고, 결과 업데이트된 행의 개수인 updateCount를 반환한다.
- updateCount가 0보다 큰 경우, 즉 도서가 성공적으로 업데이트되었을 때 트랜잭션을 COMMIT 한다. 그렇지 않은 경우 ROLLBACK 한다.
- 예외 처리가 발생한 경우 트랜잭션을 ROLLBACK 한다.
- 모든 DB 작업이 끝나면 사용한 리소스를 해제한다.
- isModifySuccess 값을 리턴한다.
▼ 전체 코드
package svc;
import static db.JdbcUtil.close;
import static db.JdbcUtil.commit;
import static db.JdbcUtil.getConnection;
import static db.JdbcUtil.rollback;
import java.sql.Connection;
import java.sql.SQLException;
import dao.BookDAO;
import vo.Book;
public class ManageBookModifyProService {
/** b_id에 따른 도서 이미지 파일명을 가져오는 메서드 */
public String getBookImage(int b_id) throws SQLException {
String imageFileName = null;
/* DB 처리 */
BookDAO bookDAO = BookDAO.getInstance();
Connection conn = getConnection();
bookDAO.setConnection(conn);
imageFileName = bookDAO.getBookImage(b_id);
close(conn);
return imageFileName;
}
/** 수정 처리 메서드
* @throws SQLException */
public boolean modifyBook(Book book, int[] sub_catgy_arr) {
boolean isModifySuccess = false;
/* DB 처리 */
BookDAO bookDAO = BookDAO.getInstance();
Connection conn = getConnection();
int updateCount = 0;
try {
bookDAO.setConnection(conn);
updateCount = bookDAO.updateBook(book, sub_catgy_arr);
// 예외 발생은 안했지만 updateCount이 0보다 작을 때를 위해 한 번 더 처리
if(updateCount > 0) {
commit(conn);
isModifySuccess = true;
} else {
rollback(conn);
}
} catch (SQLException e) {
System.out.println(" SQLException 예외 발생 ");
rollback(conn);
} catch (Exception e) {
System.out.println(" Exception 예외 발생 ");
System.out.println(e);
rollback(conn);
} finally {
close(conn);
}
System.out.println(" M.BookModifyPro.S updateCount : " +updateCount);
return isModifySuccess;
}
}
5. DAO 클래스(BookDAO.java)
BookDAO 클래스에서 도서 이미지 파일명을 가져오는 메서드는 다음과 같다.
도서 정보를 실제적으로 수정하는 updateBook() 메서드는 다음 포스팅에서 자세히 다루도록 하겠다.
public class BookDAO {
/** BookDAO 객체에 Connection 객체를 주입하는 메서드
* @throws SQLException */
public void setConnection(Connection conn) throws SQLException {
this.conn = conn;
conn.setAutoCommit(false);
}
public String getBookImage(int b_id) {
String imageFileName = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
String sql = "select b_image from jspbookshop.book where b_id=?";
try {
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, b_id);
rs = pstmt.executeQuery();
if(rs.next()) {
imageFileName = rs.getString(1);
}
} catch (Exception e) {
System.out.println(" getBookImage ERROR : "+e);
} finally {
close(pstmt);
}
return imageFileName;
}
}
#6. 데이터 수정하기 (1) 끝.
'JSP > MVC' 카테고리의 다른 글
[JSP][MVC][MySQL] 도서 목록 페이지에서 필터링 구현하기 #1. 구현 틀 잡기 (0) | 2023.08.02 |
---|---|
[JSP][MVC][MySQL] 도서 사이트에서 다중 카테고리 구현하기 #8. 데이터 수정하기 (3) (마지막) (0) | 2023.06.30 |
[JSP][MVC][MySQL] 도서 사이트에서 다중 카테고리 구현하기 #7. 데이터 수정하기 (2) (0) | 2023.06.29 |
[JSP][MVC][MySQL] 도서 사이트에서 다중 카테고리 구현하기 #5. 데이터 출력하기 (2) (0) | 2023.06.13 |
[JSP][MVC][MySQL] 도서 사이트에서 다중 카테고리 구현하기 #4. 데이터 출력하기 (1) (0) | 2023.06.12 |
[JSP][MVC][MySQL] 도서 사이트에서 다중 카테고리 구현하기 #1. 구현 틀 잡기 (2) | 2023.06.12 |