Day48

2020. 12. 8. 10:53교육과정/KOSMO

키워드 : 방명록 답글 기능 만들기 / mybatis

 

 

****

 

 

※. DB 연동방식 : ① JDBC

                       ② mybatis (ibatis)

                       ③ jpa

 

 

0. 답글 달기의 구조 : 순서번호의 역순 정렬을 사용한다. 

 

(1) 글 번호 순으로 정렬했을 경우

(2) 순서번호의 역순으로 정렬했을 경우

    ▶ 순서번호의 답글에 관한 숫자는 6자리 중 2자리씩 할당되므로,

        999999 여섯자리 숫자 구조에서는 답글의 답글이 3번째까지 가능

    ▶ 한 글의 답글은 2자리 숫자가 99에서부터 역순으로 전개되므로 99개까지 가능

※ 답글은 현재는 사용하는 경우가 많지 않으나, 관리자가 답글을 달아주는 경우에 사용된다. 

 

 

1. 게시글 보기(BoardView.jsp) 에서 "답변하기" 텍스트 클릭하면 답변 작성 페이지로 이동

 

   (1) <a> 태그를 사용하여 하이퍼 링크를 생성

<a href="BoardReplyForm.jsp">답변하기</a>

 

   (2) 기존 글이 부모글이 되므로, 부모글의 게시글번호(ID)를 파라미터로 가져가기

<a href="BoardReplyForm.jsp?parentId=<%= rec.getArticleId() %>">답변하기</a>

 

   (3) 이전 화면(BoardView.jsp)에서 넘어오는 부모 게시글의 번호를

       답변 작성 페이지(BoardReplyForm.jsp)에서 받아서 저장

<%
   String parentId = request.getParameter("parentId");
%>

 

   (4) "작성" 버튼을 클릭했을 때 다음화면으로 넘어갈 <form> 의 자식 요소들에 name을 부여

작성자 : <input type='text' name='writerName'><br/><br/>
제  목 : <input type='text' name='title'><br/><br/>
내  용 : <textarea name='content' rows='10' cols='40'></textarea><br/><br/>
패스워드(수정/삭제시 필요) :
         <input type='password' name='password'><br/><br/>

 

   (5) <form> 에 action 속성 부여하여 사용자 입력값을 "BoardReply.jsp"로 전송

<form name='frm' action='BoardReply.jsp' method='post'>

 

   (6) 부모 게시글의 번호를 <form> 의 자식요소로 넣어 다음 화면(BoardReply.jsp)에 함께 전달

<input type='hidden' name='parentId' value='<%= parentId %>' />

 


2. 답변 작성 페이지(BoardReplyForm.jsp)에서 넘어온 입력값을

   서비스 페이지(ReplyArticleService.java)로 전송

 

   (1) 답변 작성 페이지(BoardReplyForm.jsp)에서 BoardReply.jsp로 넘어오는

        파라미터 (부모 게시물의 글 번호)를 넘겨받아 저장

<% 
   String pId = request.getParameter("parentId");
%>

 

   (2) 테스트를 위해 이전 화면에서 넘어온 데이터를 화면에 출력 (임시)

<%= pId %> / <%= rec.getTitle() %>

 

 

   (3) ReplyArticleService.java의 reply( ) 메소드 호출

<%
   BoardRec reRec = ReplyArticleService.getInstance().reply(pId, rec);
%>

 


3. 사용자 입력값을 DB에 등록하기

 

   (1) ReplyArticleService.java의 reply( ) 메소드는 인자 2개를 받아 수행

public BoardRec reply( String pId,  BoardRec rec ) throws BoardException {

 

   (2) reply( ) 메소드에서는 부모게시글의 레코드를 바탕으로 답변글의 순서번호를 구한 뒤,

       그룹번호는 부모의 그룹번호와 동일하게 부여한다. 

       등록일은 새로운 날짜가 들어가게 한다. (기작성된 내용으로 분석X 자유ㅋ)

BoardDao dao = BoardDao.getInstance();
BoardRec parent = dao.selectById(parentId);         // 부모게시글의 레코드를 얻어옴

checkParent(parent, parentId);                      // 부모게시글을 체크

String maxSeqNum = parent.getSequenceNo();
String minSeqNum = getSearchMinSeqNum( parent );
		
String lastChildSeq = dao.selectLastSequenceNumber( maxSeqNum, minSeqNum );
		
String sequenceNumber = getSequenceNumber( parent,lastChildSeq);
		
		
rec.setGroupId(parent.getGroupId());               // 부모의 그룹번호와 동일하게 지정
rec.setSequenceNo(sequenceNumber);                 // 위에서 구한 답변글의 순서번호 지정
rec.setPostingDate( (new Date()).toString());      // 등록일

 

   (3) BoardDao.java의 insert( ) 메소드를 호출하여 답글의 입력값을 DB에 등록

int articleId = dao.insert(rec);
rec.setArticleId(articleId);

return rec;

 

(결과) 답변이 부모글의 아래에 자리하게 된다.

 

< ReplyArticleService.java의 reply( ) 메소드 전체 소스 코드 >

더보기
public BoardRec reply( String pId,  BoardRec rec ) throws BoardException{
		
    int parentId = 0;
    if( pId != null ) parentId = Integer.parseInt(pId);


    BoardDao dao = BoardDao.getInstance();
    // 부모게시글의 레코드를 얻어옴
    BoardRec parent = dao.selectById(parentId);
		
    // 부모게시글을 체크
    checkParent(parent, parentId);
		
    // 답변글에 순서번호 구하기
    String maxSeqNum = parent.getSequenceNo();
    String minSeqNum = getSearchMinSeqNum( parent );
		
    String lastChildSeq = dao.selectLastSequenceNumber( maxSeqNum, minSeqNum );
		
    String sequenceNumber = getSequenceNumber( parent,lastChildSeq);
		
		
    rec.setGroupId(parent.getGroupId()); // 부모의 그룹번호와 동일하게 지정
    rec.setSequenceNo(sequenceNumber);	 // 위에서 구한 답변글의 순서번호 지정
    rec.setPostingDate( (new Date()).toString());	 // 등록일
		
    int articleId = dao.insert(rec);
    rec.setArticleId(articleId);
		
    return rec;
		
}

 


4. 목록에서 답변에 해당되는 글 제목 앞에 이미지 삽입하기

 

   (1) BoardList.jsp의 글 제목이 자리하는 <td> 태그 안에 부모글이 있을 경우 (getLevel( ) != 0) 제목 앞에 이미지 출력

<td>
    <a href="BoardView.jsp?id=<%= rec.getArticleId() %>"><%= rec.getTitle() %></a>
</td>

 

<td>
    <% if(rec.getLevel() != 0) { %>
        <img alt='답글표시' src='imgs/board_re.gif'>
    <% } %>
    <a href="BoardView.jsp?id=<%= rec.getArticleId() %>"><%= rec.getTitle() %></a>
</td>

 

 

 

   (2) 부모글이 얼마나 존재하느냐에 따라 이미지 앞에 공백을 넣기 위해 for문 작성

<td>
    <% if(rec.getLevel() != 0) { %>
        <img alt='답글표시' src='imgs/board_re.gif'>
    <% } %>
    <a href="BoardView.jsp?id=<%= rec.getArticleId() %>"><%= rec.getTitle() %></a>
</td>

 

<td>
    <% for(int i=0; i<rec.getLevel(); i++) { %>
        &nbsp;
    <% } %>
    <% if(rec.getLevel() != 0) { %>
        <img alt='답글표시' src='imgs/board_re.gif'>
    <% } %>
    <a href="BoardView.jsp?id=<%= rec.getArticleId() %>"><%= rec.getTitle() %></a>
</td>

 

 

 

 

< BoardList.jsp 전체 소스 코드 >

더보기
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="board.model.*, board.service.*" %>
<%@ page import="java.util.List" %>

<%  //웹브라우저가 게시글 목록을 캐싱할 경우 새로운 글이 추가되더라도 새글이 목록에 안 보일 수 있기 때문에 설정
	response.setHeader("Pragma","No-cache");		// HTTP 1.0 version
	response.setHeader("Cache-Control","no-cache");	// HTTP 1.1 version
	response.setHeader("Cache-Control","no-store"); // 일부 파이어폭스 버스 관련
	response.setDateHeader("Expires", 1L);			// 현재 시간 이전으로 만료일을 지정함으로써 응답결과가 캐쉬되지 않도록 설정
%>

<%
String pNum = request.getParameter("page");
int pageNo = 1;
if (pNum != null) 
    pageNo = Integer.parseInt(pNum);

// Service에 getArticleList()함수를 호출하여 전체 메세지 레코드 검색
ListArticleService service = ListArticleService.getInstance();
int pageTotalCount = service.getTotalPage();
List <BoardRec> mList = service.getArticleList(pageNo); 


%>

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title> 게시글 목록 </title>
</head>

<BODY>

	<h3> 게시판 목록 </h3>
	
	<table border="1" bordercolor="darkblue">
	<tr>
		<td> 글번호 </td>
		<td> 제 목 </td>
		<td> 작성자 </td>
		<td> 작성일 </td>
		<td> 조회수 </td>
	</tr>
	

	<% if( mList.isEmpty() ) { %>
		<tr><td colspan="5"> 등록된 게시물이 없습니다. </td></tr>
	<% } else { %>
	
		<% for(BoardRec rec : mList) { %>
		<tr>
			<td><%= rec.getArticleId() %></td>
			<td>
				<% for(int i=0; i<rec.getLevel(); i++) { %>
					&nbsp;
				<% } %>
				<% if(rec.getLevel() != 0) { %>
					<img alt='답글표시' src='imgs/board_re.gif'>
				<% } %>
				<a href="BoardView.jsp?id=<%= rec.getArticleId() %>"><%= rec.getTitle() %></a>
			</td>
			<td><%= rec.getWriterName() %></td>		
			<td><%= rec.getPostingDate() %></td>
			<td><%= rec.getReadCount() %></td>
		</tr>
		<% } // end of for %>

	<% } // end else %>
		<tr>
			<td colspan="5">
				<a href="BoardInputForm.jsp">글쓰기</a>
			</td>
		</tr>
	</table>
	
	<a href="BoardList.jsp?page=1">[◀◀]</a>
	<a href="BoardList.jsp?page=<%= pageNo-1 %>">[◀]</a>
	<% for (int i=1; i<=pageTotalCount; i++) { %>
    <a href='BoardList.jsp?page=<%= i %>'>[ <%= i %>]</a>
	<% } %>
	<a href="BoardList.jsp?page=<%= pageNo+1 %>">[▶]</a>
	<a href="BoardList.jsp?page=<%= pageTotalCount %>">[▶▶]</a>
	
	
</BODY>
</HTML>

< BoardView.jsp 전체 소스 코드 >

더보기
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="board.service.*, board.model.*" %>
<%
	// 1. 해당 게시물의 게시글번호값을 얻어온다
	String id = request.getParameter("id");
	// 2. Service에 getArticleById() 호출하여 그 게시글번호를 갖는 레코드를 검색한다.
	ViewArticleService service = ViewArticleService.getInstance();
	BoardRec rec = service.getArticleById(id);
	
%>    
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title> 게시글 보기 </title>
</head>
<body>

	<h4> 게시판 글 보기 </h4><br/>
	<table border="1" bordercolor="red">
	<tr>
		<td> 제  목 : </td>
		<td><%= rec.getTitle() %></td>
	</tr>
	<tr>
		<td> 작성자 : </td>
		<td><%= rec.getWriterName() %></td>
	</tr>
	<tr>
		<td> 작성일자 : </td>
		<td><%= rec.getPostingDate() %></td>
	</tr>
	<tr>
		<td> 내  용 : </td>
		<td><%= rec.getContent() %></td>
	</tr>
	<tr>
		<td colspan="2">
			<a href="BoardList.jsp">목록보기</a>
			<a href="BoardReplyForm.jsp?parentId=<%= rec.getArticleId() %>">답변하기</a>
			<a href="BoardModifyForm.jsp?id=<%= rec.getArticleId() %>">수정하기</a>
			<a href="BoardDeleteForm.jsp?id=<%= rec.getArticleId() %>">삭제하기</a> 	
		</td>
	</tr>
	</table>


</body>
</html>

< BoardReplyForm.jsp 전체 소스 코드 >

더보기
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>

<%
	// 답변글의 부모 게시글의 번호를 넘겨받기
	String parentId = request.getParameter("parentId");
%>
    
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title> 답변 글쓰기 </title>
</head>
 <body>
	<h4> 답변 글 쓰기 </h4><br/>
	
	<form name='frm' action='BoardReply.jsp' method='post'>
	<input type='hidden' name='parentId' value='<%= parentId %>' />
	작성자 : <input type='text' name='writerName'><br/><br/>
	제  목 : <input type='text' name='title'><br/><br/>
	내  용 : <textarea name='content' rows='10' cols='40'></textarea><br/><br/>
	패스워드(수정/삭제시 필요) :
			 <input type='password' name='password'><br/><br/>
	<input type='submit' value='작성'>
	<input type='reset' value='취소'>
	</form>

</body>
</html>

< BoardReply.jsp 전체 소스 코드 >

더보기
<%@ page import="board.service.ReplyArticleService, board.model.BoardRec"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>

<%
	request.setCharacterEncoding("UTF-8");
%>

<jsp:useBean id="rec" class="board.model.BoardRec">
	<jsp:setProperty name="rec" property="*"/>
</jsp:useBean>

<%
	// 1. 부모게시물의 게시번호를 넘겨받기
	String pId = request.getParameter("parentId");
	// 2. Service에 reply() 호출하여 답변글 등록하기
	BoardRec reRec = ReplyArticleService.getInstance().reply(pId, rec);

%>
    
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title> 답변 글 저장하기 </title>
</head>
<body>
<%= pId %> / <%= rec.getTitle() %>
답변글을 등록하였습니다. <br/><br/>

<a href="BoardList.jsp"> 목록보기 </a> &nbsp;
<a href="BoardView.jsp?id=<%=rec.getArticleId()%>"> 게시글 읽기 </a>

</body>
</html>

< ReplyArticleService.java 전체 소스 코드 >

더보기
package board.service;

import java.text.DecimalFormat;
import java.util.Date;

import board.model.BoardDao;
import board.model.BoardException;
import board.model.BoardRec;


public class ReplyArticleService {
	
	private static ReplyArticleService instance;
	public static ReplyArticleService getInstance()  throws BoardException{
		if( instance == null )
		{
			instance = new ReplyArticleService();
		}
		return instance;
	}
	
	public BoardRec reply( String pId,  BoardRec rec ) throws BoardException{
		
		int parentId = 0;
		if( pId != null ) parentId = Integer.parseInt(pId);


		BoardDao dao = BoardDao.getInstance();
		// 부모게시글의 레코드를 얻어옴
		BoardRec parent = dao.selectById(parentId);
		
		// 부모게시글을 체크
		checkParent(parent, parentId);
		
		// 답변글에 순서번호 구하기
		String maxSeqNum = parent.getSequenceNo();
		String minSeqNum = getSearchMinSeqNum( parent );
		
		String lastChildSeq = dao.selectLastSequenceNumber( maxSeqNum, minSeqNum );
		
		String sequenceNumber = getSequenceNumber( parent,lastChildSeq);
		
		
		rec.setGroupId(parent.getGroupId()); // 부모의 그룹번호와 동일하게 지정
		rec.setSequenceNo(sequenceNumber);	 // 위에서 구한 답변글의 순서번호 지정
		rec.setPostingDate( (new Date()).toString());	 // 등록일
		
		int articleId = dao.insert(rec);
		rec.setArticleId(articleId);
		
		return rec;
		
	}
	
	
	/*
	 * 부모글이 존재하는지 부모글이 마지막 3단계인지 확인하는 함수
	 */
	private void checkParent( BoardRec parent, int pId ) throws BoardException
	{
		
		if( parent == null ) throw new BoardException("부모글이 존재하지 않음 : " + pId );
		
		int parentLevel = parent.getLevel();
		if( parentLevel == 3 ) throw new BoardException("3단계 마지막 레벨 글에는 답변을 달 수 없습니다.");
	
	}
	
	
	private String getSearchMinSeqNum( BoardRec parent )
	{
		String parentSeqNum = parent.getSequenceNo();
		DecimalFormat dFormat = new DecimalFormat("0000000000000000");
		long parentSeqLongValue = Long.parseLong(parentSeqNum);
		long searchMinLongValue = 0;
		
		switch( parent.getLevel())
		{
		case 0 : searchMinLongValue = parentSeqLongValue / 1000000L * 1000000L; break;
		case 1 : searchMinLongValue = parentSeqLongValue / 10000L * 10000L; break;
		case 2 : searchMinLongValue = parentSeqLongValue / 100L * 100L; break;
		}
		return dFormat.format(searchMinLongValue);
	}
	
	
	private String getSequenceNumber( BoardRec parent, String lastChildSeq ) throws BoardException
	{
		long parentSeqLong	= Long.parseLong( parent.getSequenceNo());
		int  parentLevel	= parent.getLevel();
		
		long decUnit = 0;
		if		( parentLevel == 0 ){	decUnit = 10000L;	}
		else if	( parentLevel == 1 ){	decUnit = 100L;		}
		else if ( parentLevel == 2 ){	decUnit = 1L;		}
		
		String sequenceNumber = null;
		
		DecimalFormat dFormat = new DecimalFormat("0000000000000000");
		if( lastChildSeq == null ){			// 답변글이 없다면
			sequenceNumber = dFormat.format(parentSeqLong-decUnit);
		} else {							// 답변글이 있다면, 마지막 답변글인지 확인
			String orderOfLastChildSeq = null;
			if( parentLevel == 0 ){
				orderOfLastChildSeq = lastChildSeq.substring(10,12);
				sequenceNumber = lastChildSeq.substring(0, 12) + "9999";
			}else if( parentLevel == 1 ){
				orderOfLastChildSeq = lastChildSeq.substring(12,14);
				sequenceNumber = lastChildSeq.substring(0, 14) + "99";				
			}else if( parentLevel == 2 ){
				orderOfLastChildSeq = lastChildSeq.substring(14,16);
				sequenceNumber = lastChildSeq;			
			}
			
			if( orderOfLastChildSeq.equals("00")){
				throw new BoardException("마지막 자식 글이 이미 존재합니다.");
			}
			
			long seq = Long.parseLong(sequenceNumber) - decUnit;
			sequenceNumber = dFormat.format(seq);
			
			return sequenceNumber; 
		}
		return sequenceNumber;
		
	}
}

< BoardRec.java 전체 소스 코드 >

더보기
package board.model;


import java.sql.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class BoardDao
{
	
	// Single Pattern 
	private static BoardDao instance;
	
	// DB 연결시  관한 변수 
	private static final String 	dbDriver	=	"oracle.jdbc.driver.OracleDriver";
	private static final String		dbUrl		=	"jdbc:oracle:thin:@192.168.0.17:1521:orcl";
	private static final String		dbUser		=	"scott";
	private static final String		dbPass		=	"tiger";
	
	
	private Connection	 		con;	
	
	//--------------------------------------------
	//#####	 객체 생성하는 메소드  : 싱글톤 사용 - 다수의 사용자가 접속할 때마다 드라이버를 생성하지 않도록 하기 위함
	public static BoardDao	getInstance() throws BoardException
	{
		if( instance == null )
		{
			instance = new BoardDao();
		}
		return instance;
	}
	
	private BoardDao() throws BoardException
	{
	
		try{
			
			/********************************************
				1. 오라클 드라이버를 로딩
					( DBCP 연결하면 삭제할 부분 )
			*/
			Class.forName( dbDriver );	
		}catch( Exception ex ){
			throw new BoardException("DB 연결시 오류  : " + ex.toString() );	
		}
		
	}
	
	
	//--------------------------------------------
	//#####	 게시글 입력전에 그 글의 그룹번호를 얻어온다
	public int getGroupId() throws BoardException
	{
		PreparedStatement ps = null;
		ResultSet rs = null;
		int groupId=1;
		try{
			con	= DriverManager.getConnection( dbUrl, dbUser, dbPass );
			String sql = "SELECT SEQ_GROUP_ID_ARTICLE.nextVal FROM dual";
			ps	= con.prepareStatement( sql );
			rs = ps.executeQuery();
			
			while(rs.next()) { 
				groupId = rs.getInt("nextval");
			}
			
			return groupId;
		}catch( Exception ex ){
			throw new BoardException("게시판 ) 게시글 입력 전에 그룹번호 얻어올 때  : " + ex.toString() );	
		} finally{
			if( ps   != null ) { try{ ps.close();  } catch(SQLException ex){} }
			if( con  != null ) { try{ con.close(); } catch(SQLException ex){} }
		}		
	}

	//--------------------------------------------
	//#####	 게시판에 글을 입력시 DB에 저장하는 메소드 
	public int insert( BoardRec rec ) throws BoardException
	{
		
		/************************************************
		*/
		ResultSet rs = null;
		Statement stmt	= null;
		PreparedStatement ps = null;
		try{
			con	= DriverManager.getConnection( dbUrl, dbUser, dbPass );
			
			String sql = "INSERT INTO article(article_id, group_id, sequence_no, posting_date, read_count, writer_name, title, content, password) "
								+ "VALUES (SEQ_ARTICLE_ID_ARTICLE.nextVal, ?, ?, sysdate, ?,?,?,?,?)";
			
			
			ps = con.prepareStatement(sql);
			ps.setInt(1, rec.getGroupId());
			ps.setString(2, rec.getSequenceNo());
			ps.setInt(3, rec.getReadCount());
			ps.setString(4, rec.getWriterName());
			ps.setString(5, rec.getTitle());
			ps.setString(6, rec.getContent());
			ps.setString(7, rec.getPassword());
			// 
			int result = ps.executeUpdate();
			System.out.println(result + "행 입력 성공");

			////////////////////////////////////////////////////////////////
			stmt = con.createStatement();
			String sqlSeq = "SELECT SEQ_ARTICLE_ID_ARTICLE.currval as articleId FROM dual"; // currval : 현재값 가져오기
			
			rs = stmt.executeQuery(sqlSeq);
			if(rs.next()) {
				System.out.println("BoardDao 113line : " + rs.getInt("articleId"));
				return rs.getInt("articleId");
			} 
			///////////////////////////////////////////////////////////////
			
			return -1;
		
		}catch( Exception ex ){
			throw new BoardException("게시판 ) DB에 입력시 오류  : " + ex.toString() );	
		} finally{
			if( rs   != null ) { try{ rs.close();  } catch(SQLException ex){} }
			if( stmt != null ) { try{ stmt.close(); } catch(SQLException ex){} }
			if( ps   != null ) { try{ ps.close();  } catch(SQLException ex){} }
			if( con  != null ) { try{ con.close(); } catch(SQLException ex){} }
		}
		
	}

	//--------------------------------------------
	//#####	 전체 레코드를 검색하는 함수
	// 리스트에 보여줄거나 필요한 컬럼 : 게시글번호, 그룹번호, 순서번호, 게시글등록일시, 조회수, 작성자이름,  제목
	//							( 내용, 비밀번호  제외 )
	// 순서번호(sequence_no)로 역순정렬
	public List<BoardRec> selectList(int firstRow, int endRow) throws BoardException
	{
		PreparedStatement ps = null;
		ResultSet rs = null;
		List<BoardRec> mList = new ArrayList<BoardRec>();
		boolean isEmpty = true;
		
		try{
			con	= DriverManager.getConnection( dbUrl, dbUser, dbPass );
			
//			String sql = "SELECT * FROM article ORDER BY sequence_no DESC";
			String sql = "SELECT * FROM article WHERE article_id IN "
							+ "(SELECT article_id FROM"
							+ "(SELECT rownum as num, article_id FROM"
							+ "(SELECT article_id FROM article ORDER BY article_id DESC))"
							+ "WHERE (num >=? AND num <=?))" 
//							+ "ORDER BY article_id DESC";
							+ "ORDER BY sequence_no DESC";
			ps = con.prepareStatement(sql);
			ps.setInt(1, firstRow);
			ps.setInt(2, endRow);
			rs = ps.executeQuery();
			
			
			while(rs.next()) {
				BoardRec rec = new BoardRec();
				rec.setArticleId(rs.getInt("article_id"));
				rec.setGroupId(rs.getInt("group_id"));
				rec.setSequenceNo(rs.getString("sequence_no"));
				rec.setPostingDate(rs.getString("posting_date"));
				rec.setReadCount(rs.getInt("read_count"));
				rec.setWriterName(rs.getString("writer_name"));
				rec.setTitle(rs.getString("title"));

				mList.add(rec);
				isEmpty = false;
			}
			
			if( isEmpty ) return Collections.emptyList();
			
			return mList;
		}catch( Exception ex ){
			throw new BoardException("게시판 ) DB에 목록 검색시 오류  : " + ex.toString() );	
		} finally{
			if( rs   != null ) { try{ rs.close();  } catch(SQLException ex){} }
			if( ps   != null ) { try{ ps.close();  } catch(SQLException ex){} }
			if( con  != null ) { try{ con.close(); } catch(SQLException ex){} }
		}		
	}
	
	//--------------------------------------------
	//#####	 게시글번호에 의한 레코드 검색하는 함수
	// 			비밀번호 제외하고 모든 컬럼 검색
	public BoardRec selectById(int id) throws BoardException
	{
		PreparedStatement ps = null;
		ResultSet rs = null;
		
		BoardRec rec = new BoardRec();
		
		try{
			con	= DriverManager.getConnection( dbUrl, dbUser, dbPass );
			
			String sql = "SELECT * FROM article WHERE article_id=?";
			ps = con.prepareStatement(sql);
			ps.setInt(1, id);
			rs = ps.executeQuery();
			
			while(rs.next()) {
				rec.setArticleId(rs.getInt("article_id"));
				rec.setGroupId(rs.getInt("group_id"));
				rec.setSequenceNo(rs.getString("sequence_no"));
				rec.setPostingDate(rs.getString("posting_date"));
				rec.setReadCount(rs.getInt("read_count"));
				rec.setWriterName(rs.getString("writer_name"));
				rec.setTitle(rs.getString("title"));
				rec.setContent(rs.getString("content"));
			}
			
			return rec;
		}catch( Exception ex ){
			throw new BoardException("게시판 ) DB에 글번호에 의한 레코드 검색시 오류  : " + ex.toString() );	
		} finally{
			if( rs   != null ) { try{ rs.close();  } catch(SQLException ex){} }
			if( ps   != null ) { try{ ps.close();  } catch(SQLException ex){} }
			if( con  != null ) { try{ con.close(); } catch(SQLException ex){} }
		}		
	}

	//##### 전체 레코드 수를 검색
	public int getTotalCount() throws BoardException {
		PreparedStatement ps = null;
		ResultSet rs = null;
		int count = 0;
		try{
			con	= DriverManager.getConnection( dbUrl, dbUser, dbPass );
			String sql = "SELECT count(*) cnt FROM article";
			ps = con.prepareStatement(sql);
			rs = ps.executeQuery();
			
			while(rs.next()) {
				count = rs.getInt("cnt");
            }

			return  count;
		}catch( Exception ex ){
			throw new BoardException("게시판 ) 전체 레코드 수 검색시 오류  : " + ex.toString() );	
		} finally{
			if( ps   != null ) { try{ ps.close();  } catch(SQLException ex){} }
			if( con  != null ) { try{ con.close(); } catch(SQLException ex){} }
		}
		
	}
	
	
	
	//--------------------------------------------
	//#####	 게시글 보여줄 때 조회수 1 증가
	public void increaseReadCount( int article_id ) throws BoardException
	{

		PreparedStatement ps = null;
		try{
			con	= DriverManager.getConnection( dbUrl, dbUser, dbPass );
			String sql = "UPDATE article SET read_count=(read_count+1) WHERE article_id=?";
			ps = con.prepareStatement(sql);
			ps.setInt(1, article_id);
			ps.executeUpdate();
			

		
		}catch( Exception ex ){
			throw new BoardException("게시판 ) 게시글 볼 때 조회수 증가시 오류  : " + ex.toString() );	
		} finally{
			if( ps   != null ) { try{ ps.close();  } catch(SQLException ex){} }
			if( con  != null ) { try{ con.close(); } catch(SQLException ex){} }
		}
		
	}
	
	//--------------------------------------------
	//#####	 게시글 수정할 때
	//		( 게시글번호와 패스워드에 의해 수정 )
	public int update( BoardRec rec ) throws BoardException
	{

		PreparedStatement ps = null;
		try{
			con	= DriverManager.getConnection( dbUrl, dbUser, dbPass );
			String sql = "UPDATE article SET title=?, content=? WHERE article_id=? AND password=?";
			ps = con.prepareStatement(sql);
			ps.setString(1, rec.getTitle());
			ps.setString(2, rec.getContent());
			ps.setInt(3, rec.getArticleId());
			ps.setString(4, rec.getPassword());
			int result = ps.executeUpdate();
			
			return result; // 나중에 수정된 수를 리턴하시오.
		
		}catch( Exception ex ){
			throw new BoardException("게시판 ) 게시글 수정시 오류  : " + ex.toString() );	
		} finally{
			if( ps   != null ) { try{ ps.close();  } catch(SQLException ex){} }
			if( con  != null ) { try{ con.close(); } catch(SQLException ex){} }
		}
		
	}
	
	
	//--------------------------------------------
	//#####	 게시글 삭제할 때
	//		( 게시글번호와 패스워드에 의해 삭제 )
	public int delete( int article_id, String password ) throws BoardException
	{

		PreparedStatement ps = null;
		try{
			con	= DriverManager.getConnection( dbUrl, dbUser, dbPass );
			String sql = "DELETE FROM article WHERE article_id=? AND password=?";
			ps = con.prepareStatement(sql);
			ps.setInt(1, article_id);
			ps.setString(2, password);
			int result = ps.executeUpdate();

			return result; // 나중에 수정된 수를 리턴하시오.
		
		}catch( Exception ex ){
			throw new BoardException("게시판 ) 게시글 수정시 오류  : " + ex.toString() );	
		} finally{
			if( ps   != null ) { try{ ps.close();  } catch(SQLException ex){} }
			if( con  != null ) { try{ con.close(); } catch(SQLException ex){} }
		}
		
	}
	
	
	//----------------------------------------------------
	//#####  부모레코드의 자식레코드 중 마지막 레코드의 순서번호를 검색
	//       ( 제일 작은 번호값이 마지막값임)
	public String selectLastSequenceNumber( String maxSeqNum, String minSeqNum ) throws BoardException
	{
		PreparedStatement ps = null;
		ResultSet rs = null;

		try{
			con	= DriverManager.getConnection( dbUrl, dbUser, dbPass );
			String sql = "SELECT min(sequence_no) as minseq FROM article WHERE sequence_no < ? AND sequence_no >= ?";  
			ps = con.prepareStatement( sql );
			ps.setString(1, maxSeqNum);
			ps.setString(2, minSeqNum);
			rs = ps.executeQuery();
			if( !rs.next()) {				
				return null;
			}
			
			return rs.getString("minseq");
		}catch( Exception ex ){
			throw new BoardException("게시판 ) 부모와 연관된 자식 레코드 중 마지막 순서번호 얻어오기  : " + ex.toString() );	
		} finally{
			if( rs   != null ) { try{ rs.close();  } catch(SQLException ex){} }
			if( ps   != null ) { try{ ps.close();  } catch(SQLException ex){} }
			if( con  != null ) { try{ con.close(); } catch(SQLException ex){} }
		}			
	}
}

 



5. mybatis 개념

 

※ JDBC의 단점 :

     (1) 매번 DB와 연결하는 코드와 연결 해제하는 코드(close 메소드)를 반복적으로 작성해야 한다.

     (2) 컬럼이 20라면 조회 결과에서 값을 가져올 때 매 번 컬럼의 이름을 적어야 한다. 

     (3) DB에서 컬럼이 하나라도 추가되면 java 파일과 jsp 파일을 수정해야 하고, 그 과정에서 서버를 닫아야 한다. 

 

※ 마이바티스란?

    : JDBC에서 SQL을 별도의 XML로 분리하여 관리하도록 돕는 프레임워크

 

※ 마이바티스 구조

    : 각 테이블별로 CRUD를 담당하는 mapper가 다로 존재한다.

 

 


6. 마이바티스에서 DB 연결 후 목록 가져오기

 

(1) DB 설계

CREATE TABLE comment_tab(
  comment_no         number(10),     -- 글 번호 (PK)
  user_id            varchar2(30),   -- 사용자
  comment_content    varchar2(500),  -- 글 내용
  reg_date           date,           -- 작성 날짜
  CONSTRAINT pk_comment_tab_comment_no PRIMARY KEY (comment_no)
);

 

(2) DB에 서너개의 데이터를 입력해둔다. 

 

(3) DB와 연결하는 xml을 작성한다. 

    ① src- mybatis.guest.model - Comment.java 파일 생성 후 변수 선언 및 Getter / Setter 생성하고, 직렬화 한다. 

        (직렬화 : 어떤 데이터를 찾을 수 있도록 가상 통로를 만들어주는 것)

public class Comment implements java.io.Serializable {
	
	private long commentNo;
	private String userId;
	private String commentContent;
	private String regDate;

 

    ② Mybatis-3-User-Guide_ko.pdf 파일의 5페이지 스크립트를 복사하여,

        src - mybatis-config.xml 파일에 붙여넣기 한다. 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
 PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
 <environments default="development">
 <environment id="development">
 <transactionManager type="JDBC"/>
 <dataSource type="POOLED">
 <property name="driver" value="${driver}"/>
 <property name="url" value="${url}"/>
 <property name="username" value="${username}"/>
 <property name="password" value="${password}"/>
 </dataSource>
 </environment>
 </environments>
 <mappers>
 <mapper resource="org/mybatis/example/BlogMapper.xml"/>
 </mappers>
</configuration>

 

    <dataSource> 태그 내에서 사용자 정보를 변경한다. 

 <property name="driver" value="oracle.jdbc.driver.OracleDriver"/>
 <property name="url" value="jdbc:oracle:thin:@192.168.0.17:1521:orcl"/>
 <property name="username" value="scott"/>
 <property name="password" value="tiger"/>

 

    Mapper를 연결한다. (mapper가 여러개일 경우에도 등록 가능)

<mapper resource="mybatis/guest/mapper/CommentMapper.xml"/>

 

(4) src - mybatis - guest - mapper의 CommentMapper.xml 작성하기

 

   PDF의 7페이지 스크립트를 복사하여 CommentMapper.xml에 붙여넣기 한다. 

<?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="org.mybatis.example.BlogMapper">
 <select id="selectBlog" parameterType="int" resultType="Blog">
 select * from Blog where id = #{id}
 </select>
</mapper>

 

    ② id와 데이터가 저장될 resultType을 작성하고, <mapper> 태그의 속성 중 namespace를 xml 파일명으로 바꾼다.

        SQL문에서는 세미콜론( ; )을 사용하지 않는다. 

<mapper namespace="CommentMapper">
<select id="selectComment" resultType="mybatis.guest.model.Comment">
   select * from comment_tab
</select>

 

(5) CommentService.java 파일 작성

    ① CommentRepository.java 클래스의 객체를 생성한다. 

private CommentRepository repo = new CommentRepository();

    ② CommentRepository.java 클래스의 selectComment( ) 메소드를 호출하고 그 결과값을 리턴한다. 

        (해당 메소드 작성 전이라 에러 발생할 수 있음)

public List<Comment> selectComment() {
    return repo.selectComment();
}

 

(6) CommentRepository.java 파일 작성 - DB와 연결

 

※ JDBC : 연결담당 - Connection Class가 수행

※ Mybatis : 연결담당 - mybatis-config.xml의 값에 따라 SqlSession이 수행

 

   ① getSqlSessionFactory( ) 메소드에서는 Builder 메소드가 Factory를 만든다. 

private SqlSessionFactory getSqlSessionFactory() {
		
    InputStream inputStream;
    try {
        inputStream = Resources.getResourceAsStream("mybatis-config.xml");
    } catch (IOException e) {
        throw new IllegalArgumentException(e);
    }
    SqlSessionFactory sessFactory =  new SqlSessionFactoryBuilder().build(inputStream);
    // builder가 공장을 만들고, 거기서 세션이 만들어져서 리턴된다. 
    return sessFactory;
}

 

    ② selectComment( ) 메소드 작성

public List<Comment> selectComment() {
    // get~메소드의 결과인 공장으로 openSession() 메소드를 호출하면 세션을 얻을 수 있다. 
    SqlSession sqlSess = getSqlSessionFactory().openSession();
		
    try {
        return sqlSess.selectList("CommentMapper.selectComment");
    }finally {
        sqlSess.close();
    // 실제적으로 연결을 닫는 것이 아니라 반납하는 것임
    // 마이바티스는 미리 연결객체(Connection)을 몇 개 열어놓고 ConnectionPool을 관리함
    }
}

 

(7) DB의 컬럼명과 java 파일의 변수명이 다르기 때문에, SQL에서 별칭을 부여하여 둘을 매핑 시켜준다. 

    → 이러한 별칭 부여는 번거롭기 때문에 (8)에서 보다 효율적인 방법으로 변경하게 됨

 select comment_no as commentNo,
 		user_id as userId,
 		comment_content as commentContent,
 		reg_date as regDate 
 from comment_tab

 

( 결과 )

 

 

(8) SQL문에서 컬럼명을 모두 기술하면서 별칭 부여 하려면 컬럼 수가 많을수록 번거롭기 때문에, 

    xml에서 환경설정을 통해 자동으로 바뀌게끔 한다. 

 

    ① CommentMapper.xml에서 SQL을 다시 * 로 수정한다. 

 select * from comment_tab

 

    ② mybatis-config.xml 파일의 <configuration> 태그 바로 아래에 <settings> 태그를 작성해준다. 

<!-- 테이블의 컬럼명과 VO의 멤버변수명이 다른 경우 (이름 규칙을 맞추었다면) -->
<settings>
    <setting name="mapUnderscoreToCamelCase" value="true" />
</settings>

 

(결과) 목록이 정상적으로 출력됨

 

 

(9) 클래스에 별칭 부여하기

    ① CommentMapper.xml에서 데이터가 저장되는 resultType의 이름이 너무 길 경우 별칭을 부여하여 사용할 수 있다.

<select id="selectComment" resultType="mybatis.guest.model.Comment">

 

    ② mybatis-config.xml 환경설정 파일에서 <configuration> 태그 안에 <typeAliases> 태그로 별칭 부여

<!-- 클래스에 별칭 부여 -->
<typeAliases>
    <typeAlias type="mybatis.guest.model.Comment" alias='comment'/>
</typeAliases>

 

    ③ CommentMapper.xml 에서 resultType의 이름을 별칭으로 변경

<select id="selectComment" resultType="comment">

 

(결과) 별칭 부여 후에도 데이터가 정상적으로 출력됨

 

 

(10) 드라이버 연결하는 정보에 아무나 접근하지 못하게 하면서 오탈자에 대한 대비를 하기 위해 properties 파일을 작성

      → DB관리자에게 해당 properties 파일만 제공하고, 계정 정보 변경이 필요할 경우 properties파일만으로 수정 가능

    ① src - 우클릭 - others - general - file - dbconnect.properties 파일 생성 (확장자명이 properties )

 

    ② properties 파일 내에 드라이버 연결을 위한 정보를 기술

# dbconnect.properties

dbcon.driver=oracle.jdbc.driver.OracleDriver
dbcon.url=jdbc:oracle:thin:@192.168.0.17:1521:orcl
dbcon.user=scott
dbcon.pass=tiger

 

    ③ mybatis-config.xml 파일에서 드라이버 연결에 관한 부분을 properties의 변수명으로 수정

<property name="driver" value="${dbcon.driver}"/>
<property name="url" value="${dbcon.url}"/>
<property name="username" value="${dbcon.user}"/>
<property name="password" value="${dbcon.pass}"/>

 

    ④ mybatis-config.xml 파일에서 <configuration> 태그 바로 아래에 위에서 작성한 파일과 연결하기 위한

        <properties> 태그를 작성하고 속성의 resource값으로 properties 파일명을 적는다. (태그 작성 위치 중요함!)

<properties resource="dbconnect.properties" />

 

(결과) DB와 정상적으로 연결되어 목록이 화면에 출력됨

 

 


7. 마이바티스를 사용하여 데이터 입력하기

 

(1) insertCommentForm.jsp의 <form>의 자식 요소들은 Comment.java 파일의 변수들과 같은 이름을 name 속성의 값으로 가지므로, 사용자 입력값을 다음 페이지(insertCommentSave.jsp)로 전달할 수 있다. 

private long commentNo;
private String userId;
private String commentContent;
private String regDate;

 

<form name="frm" action="insertCommentSave.jsp" >
<table>
    <tr><td>글번호</td><td><input type="text" name="commentNo" size="3"/></td></tr>
    <tr><td>사용자</td><td><input type="text" name="userId" size="15"/></td></tr>
    <tr><td>메세지</td><td><textarea name="commentContent" rows="10" columns="40"></textarea></td></tr>
    <tr><td><input type="submit" value="입력"/></td></tr>
</table>
</form>

 

(2) insertCommentSave.jsp 에서는 이전 화면에서 넘어오는 데이터를 Comment.java 클래스의 멤버변수로 지정한다.

 <jsp:useBean id="comment" class="mybatis.guest.model.Comment">
     <jsp:setProperty name="comment" property="*"/>
 </jsp:useBean> 

 

(3) insertCommentSave.jsp 에서 데이터가 들어있는 객체 comment를 인자로 하여

    CommentService.java의 insertComment( ) 메소드를 호출한다. 

<%
    CommentService.getInstance().insertComment(comment);
%>

 

(4) CommentService.java 파일에서 리턴 타입이 없고 인자를 하나 받는 메소드 insertComment( ) 를 작성한다.

    해당 메소드는 CommentRepository.java의 insertComment( ) 메소드를 호출하며

    기존 인자를 그대로 넘겨주고 결과(int값이 올 예정)를 리턴 받는다.

public Integer insertComment(Comment c) {
    return repo.insertComment(c);
}

 

(5) CommentRepository.java 에서 insertComment( ) 메소드를 작성

    → sql세션이 insert( ) 메소드를 수행하면

       CommentMapper.xml 에서 id가 insertComment인 SQL을 찾고,

       메소드의 인자로 넘어온 Comment c를 사용하여 DB에 사용자 입력값을 입력한다. 

public Integer insertComment(Comment c) {
    SqlSession sqlSess = getSqlSessionFactory().openSession();
    try {
        int result = sqlSess.insert("CommentMapper.insertComment", c);
        if (result > 0) sqlSess.commit();
        else sqlSess.rollback();
        return result;
    }finally {
        sqlSess.close();
    }
}

 

(6) CommentMapper.xml 에서 <insert> 태그를 사용하여

    데이터 입력을 위해 insert( ) 메소드에서 호출할 SQL을 작성한다.

<insert id="insertComment" parameterType="comment">
    INSERT INTO comment_tab(comment_no, user_id, comment_content, reg_date) 
    VALUES ( #{commentNO}, #{userID}, #{commentContent}, sysdate )
</insert>

 

 

(결과)

 

 


8. 마이바티스에서 작성한 글 확인하기

 

(1) listComment.jsp에서 작성자명을 클릭하면 해당 글 번호를 다음 페이지인 viewComment.jsp로 넘긴다.

<a href="viewComment.jsp?cId=<%=comment.getCommentNo()%>"><%= comment.getUserId()%> 님이 쓴 글</a>

 

(2) viewComment.jsp에서는 이전 페이지에서 넘어오는 게시물 번호를 넘겨받아 변수에 저장하고,

    게시글 번호를 인자로 가져가는 CommentService.java의 selectCommentByPrimaryKey( ) 메소드를 호출하여

    게시글 번호에 해당되는 데이터를 DB에서 가져와 Comment 객체에 담는다. 

<!-- 키에 해당하는 글번호를 넘겨받아 서비스의 메소드 호출  -->
<% 
  long commentNo = Integer.parseInt( request.getParameter("cId"));
  Comment comment = CommentService.getInstance().selectCommentByPrimaryKey(commentNo);
%>

 

(3) CommentService.java 에서 selectCommentByPrimaryKey( ) 메소드를 작성하여 

    CommentRepository.java의 selectCommentByPrimaryKey( ) 메소드를 호출한다 .

public Comment selecCommentByPrimaryKey(long commentNo) {
    return repo.selectCommentByPrimaryKey(commentNo);
}

 

(4) CommentRepository.java 에서 selectCommentByPrimaryKey( ) 메소드를 작성한다. 

    인자로 받은 long타입의 데이터 commentId를 HashMap의 객체 담은 뒤,

    sql세션이 HashMap의 값을 사용하여 selectOne( ) 메소드를 수행하게 한다. 

    selectOne( ) 메소드는 CommentMapper.xml에서 id가 selectCommentByPK인 SQL문을 찾아

    hashmap을 인자로 하여 조건문을 수행하고 결과값을 Comment 타입으로 돌려준다. 

public Comment selectCommentByPrimaryKey(long commentId) {
    SqlSession sqlSess = getSqlSessionFactory().openSession();
    try {
        HashMap map = new HashMap();
        map.put("commentId", commentId);
        return sqlSess.selectOne("CommentMapper.selectCommentByPK", map);
    }finally {
        sqlSess.close();
    }
}

 

(5) CommentMapper.xml 에서 <select> 태그에 id를 부여한 뒤,

      인자로 사용하는 parameterType과 데이터를 받을 resultType, SQL문을 작성한다. 

<select id="selectCommentByPK" parameterType="long" resultType="comment">
SELECT * comment_tab WHERE commnet_no=#{commentNo} {}안의 변수명은 무엇이든 가능하나 보기 편하려고 동일이름 부여
 	SELECT * from comment_tab WHERE comment_no=#{commentNo}
 </select>

 

(6) <where> 태그와 <if> 태그를 사용하여 select문이 경우에 따라 조건식을 갖게 하기

     commentId가 없을 경우에는 조건문 WHERE 절이 수행되지 않는다. 

<select id="selectCommentByPK" parameterType="hashmap" resultType="comment">
SELECT * from comment_tab 
<where>
    <if test="commentId != null">
        comment_no=#{commentId}
    </if>
</where>
</select>

 

(결과)

 

 

 

 

 

반응형

'교육과정 > KOSMO' 카테고리의 다른 글

Day50  (0) 2020.12.10
Day49  (0) 2020.12.09
Day47  (0) 2020.12.07
Day46  (0) 2020.12.04
Day45  (0) 2020.12.03