Java EE 정리 19
25 Mar 2019
Reading time ~11 minutes
Java EE 정리 19 - 일정관리 만들기(4)
일정관리 만들기(4)
-
게시판 만드는 방법
// 1. 총 게시물의 수를 구함
int totalCnt = " SELECT COUNT(*) cnt FROM diary ";
// 또는
totalCnt = " SELECT num cnt FROM diary "; // PK로 조회
// 검색할 값이 존재하면
// 검색 조건(제목, 내용, 작성자)에 따라 동적 쿼리를 생성해야 한다.
totalCnt += " WHERE 컬럼명 LIKE '%'||?||'%' "
// 2. 한 화면에 보여질 게시물 수 설정
int pageScale = 10;
// 3. 게시물을 나눠서 보여주기 위한 총 페이지 수를 구한다
int totalPage = totalCnt/pageScale;
if (totalCnt%pageScale != 0) { // 딱 나눠 떨어지지 않으면
totalPage++; // +1
}
// 4. 시작 게시글 번호 구하기
// 사용자가 클릭한 index list 번호를 가지고 시작 게시글 번호를 얻는다.
String currentPage = request.getParameter("currentPage");
int startNum = 1;
if (currentPage != null) {
int tempPage = Integer.parseInt(currentPage);
// 시작번호 = 선택된 페이지 * 스케일 - 스케일 + 1
startNum = tempPage*pageScale - pageScale + 1;
}
// 5. 끝 게시글 번호 구하기
int endNum = 0;
endNum = startNum + pageScale - 1;
- 검색 조건에 따라 동적 쿼리 생성
- 총 게시물의 수를 구하는 메소드
// DiaryDAO
...
public int selectEvtCnt(SearchDataVO sdvo) throws SQLException {
int cnt = 0;
System.out.println(sdvo);
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConn();
StringBuilder selectEvtCnt = new StringBuilder();
selectEvtCnt
.append(" SELECT count(*) cnt ")
.append(" FROM diary ");
// 검색조건에 따라 Count할 총 게시글의 수가 달라진다(Dynamic Query)
if (sdvo != null) {
selectEvtCnt
.append(" WHERE ").append(sdvo.getFieldName()).append(" LIKE '%'||?||'%' ");
pstmt = con.prepareStatement(selectEvtCnt.toString());
pstmt.setString(1, sdvo.getKeyword());
} else {
pstmt = con.prepareStatement(selectEvtCnt.toString());
}
rs = pstmt.executeQuery();
if(rs.next()) {
cnt = rs.getInt("cnt");
}
} finally {
if (rs != null) { rs.close(); }
if (pstmt != null) { pstmt.close(); }
if (con != null) { con.close(); }
}
return cnt;
}
...
- 게시판 만들기 위한 정보 얻기
<!-- list.jsp -->
...
<%
DiaryDAO d_dao = DiaryDAO.getInstance();
SearchDataVO sdvo = null;
// 1. 전체 페이지 수를 얻기
int totalCnt = d_dao.selectEvtCnt(sdvo);
// 2. 한 화면에 보여질 게시물의 수
int pageScale = 10;
// 3. 총 페이지 수 구하기
// int totalPage = (int)Math.ceil(totalCnt/(double)pageScale);
int totalPage = totalCnt/pageScale;
if (totalCnt%pageScale != 0) {
totalPage++;
}
// 4. 시작 게시글 번호 구하기
// current_page에 따라 시작번호는 달라진다.
// 1->1, 2->11, 3->21, 4->31
String currentPage = request.getParameter("current_page");
int startNum = 1;
if (currentPage != null) {
int tempPage = Integer.parseInt(currentPage);
startNum = tempPage*pageScale - pageScale + 1;
}
// 5. 끝 게시글 번호 구하기
int endNum = startNum + pageScale - 1;
%>
전체 페이지 수 : <%= totalCnt %><br/>
한 화면에 보여질 게시물의 수 : <%= pageScale %><br/>
총 페이지 수 : <%= totalPage %><br/>
현재 페이지 번호 : <%= currentPage %><br/>
시작 번호 : <%=startNum %><br/>
끝 번호 : <%=endNum %><br/>
...
<div id="diaryIndexList">
<%
for(int i=1; i<=totalPage; i++) {
%>
[<a href="list.jsp?current_page=<%=i %>"><%=i %></a>]
<%
}
%>
</div>
...
- pageScale만큼의 수를 가져오는 SQL 쿼리
SELECT num, subject, writer, e_year, e_month, e_date, w_date, ip
FROM (SELECT rownum r, num, subject, writer, e_year, e_month, e_date, w_date, ip
FROM (SELECT num, subject, writer, e_year, e_month, e_date, w_date, ip
FROM diary
ORDER BY w_date DESC))
WHERE r BETWEEN 1 AND 10;
-- 랭크함수 ROW_NUMBER() OVER()를 사용하면 짧게 줄일 수 있다.
SELECT num, subject, writer, e_year, e_month, e_date, w_date, ip
FROM (SELECT num, subject, writer, e_year, e_month, e_date, w_date, ip, ROW_NUMBER() OVER(ORDER BY w_date DESC) r_num
FROM diary)
WHERE r_num BETWEEN 1 AND 10;
- 시작, 끝 번호를 담는 VO 생성
public class ListRangeVO {
private int startNum, endNum;
// 기본생성자, setters, getters, toString 생성
...
- 게시글 목록을 가져오는 메소드
// DiraryDAO
public List<DiaryListVO> selectList(SearchDataVO sdvo, ListRangeVO lrvo) throws SQLException{
List<DiaryListVO> list = new ArrayList<DiaryListVO>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConn();
StringBuilder selectList = new StringBuilder();
selectList
.append(" SELECT num, subject, writer, e_year, e_month, e_date, w_date")
.append(" FROM (SELECT num, subject, writer, e_year, e_month, ")
.append(" e_date, w_date, ROW_NUMBER() OVER(ORDER BY w_date DESC) r_num ")
.append(" FROM diary) ")
.append(" WHERE r_num BETWEEN ? AND ? ");
pstmt = con.prepareStatement(selectList.toString());
pstmt.setInt(1, lrvo.getStartNum());
pstmt.setInt(2, lrvo.getEndNum());
rs = pstmt.executeQuery();
DiaryListVO dlvo = null;
while(rs.next()) {
dlvo = new DiaryListVO(rs.getShort("num"),
rs.getString("subject"), rs.getString("writer"),
rs.getString("e_year"), rs.getString("e_month"),
rs.getString("e_date"), rs.getString("w_date"));
list.add(dlvo);
}
} finally {
if (rs != null) { rs.close(); }
if (pstmt != null) { pstmt.close(); }
if (con != null) { con.close(); }
}
return list;
}
- selectEvtCnt, selectList메소드 호출 예외처리
- JSTL catch 사용
<c:catch var="e">
<%
DiaryDAO d_dao = DiaryDAO.getInstance();
SearchDataVO sdvo = null;
// 1. 전체 페이지 수를 얻기
int totalCnt = d_dao.selectEvtCnt(sdvo);
// 2. 한 화면에 보여질 게시물의 수
int pageScale = 10;
// 3. 총 페이지 수 구하기
// int totalPage = (int)Math.ceil(totalCnt/(double)pageScale);
int totalPage = totalCnt/pageScale;
if (totalCnt%pageScale != 0) {
totalPage++;
}
// 4. 시작 게시글 번호 구하기
// current_page에 따라 시작번호는 달라진다.
// 1->1, 2->11, 3->21, 4->31
String currentPage = request.getParameter("current_page");
int startNum = 1;
if (currentPage != null) {
int tempPage = Integer.parseInt(currentPage);
startNum = tempPage*pageScale - pageScale + 1;
}
// 5. 끝 게시글 번호 구하기
int endNum = startNum + pageScale - 1;
ListRangeVO lrvo = new ListRangeVO(startNum, endNum);
List<DiaryListVO> list = d_dao.selectList(sdvo, lrvo);
pageContext.setAttribute("list", list);
pageContext.setAttribute("totalPage", totalPage);
%>
</c:catch>
...
<c:if test="${ not empty e }">
<tr>
<td colspan="5">서비스가 월활하지 못한점 죄송합니다.</td>
</tr>
</c:if>
<c:if test="${ empty e and empty list }">
<tr>
<td colspan="5">
이벤트가 존재하지 않습니다.<br/>
<a href="diary.jsp">이벤트 작성</a>
</td>
</tr>
</c:if>
<c:forEach var="data" items="${ list }">
<c:set var="i" value="${ i+1 }"/>
<tr>
<td><c:out value="${ i }"/></td>
<td><c:out value="${ data.subject }"/></td>
<td><c:out value="${ data.writer }"/></td>
<td><c:out value="${ data.e_year }-${ data.e_month }-${ data.e_date }"/></td>
<td><c:out value="${ data.w_date }"/></td>
</tr>
</c:forEach>
...
<div id="diaryIndexList">
<c:forEach var="i" begin="1" end="${ totalPage }" step="1">
[<a href="list.jsp?current_page=${ i }">${ i }</a>]
</c:forEach>
...
- 게시글을 조회하는 걸 클래스로 빼서 만들자
- 업무 로직을 처리하는 service 패키지
- jsp에서는 가급적이면 업무로직을 처리하지 않는다.
// kr.co.sist.diary.service.ListService
// 게시판 리스트에 관한 업무 처리 클래스
public class ListService {
private static ListService ls; // 싱글톤 패턴
private ListService() {}
public static ListService getInstance() {
if (ls == null) {
ls = new ListService();
}
return ls;
}
public int totalCount(SearchDataVO sdvo) {
int totalCnt = 0;
DiaryDAO d_dao = DiaryDAO.getInstance();
// 1. 전체 페이지 수를 얻기
try {
totalCnt = d_dao.selectEvtCnt(sdvo);
} catch (SQLException e) {
e.printStackTrace();
}
return totalCnt;
}
public int pageScale() {
int pageScale = 10;
return pageScale;
}
public int totalPage(int totalCnt) {
int totalPage = totalCnt/pageScale();
if (totalCnt%pageScale() != 0) {
totalPage++;
}
return totalPage;
}
public int startNum(String currentPage) {
int startNum = 1;
if (currentPage != null) {
int tempPage = Integer.parseInt(currentPage);
startNum = tempPage*pageScale() - pageScale() + 1;
}
return startNum;
}
public int endNum(int startNum) {
int endNum = startNum + pageScale() - 1;
return endNum;
}
public List<DiaryListVO> searchList(SearchDataVO sdvo, ListRangeVO lrvo) {
List<DiaryListVO> list = new ArrayList<DiaryListVO>();
DiaryDAO d_dao = DiaryDAO.getInstance();
try {
list = d_dao.selectList(sdvo, lrvo);
DiaryListVO dlvo = null;
String subject = "";
// 글의 제목은 24자 까지만 보여준다.
for(int i=0; i<list.size(); i++) {
dlvo = list.get(i);
subject= dlvo.getSubject();
if( subject.length() > 25 ) {
subject = subject.substring(0, 24)+"...";
dlvo.setSubject(subject);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
}
<!-- list.jsp -->
...
<%
// 업무 로직을 클래스로 빼서 JSP가 깔끔해졌다.
ListService ls = ListService.getInstance();
request.setCharacterEncoding("UTF-8"); // POST 한글처리
// 1. 전체 페이지 수를 얻기
int totalCnt = ls.totalCount(null);
// 2. 한 화면에 보여질 게시물의 수
int pageScale = ls.pageScale();
// 3. 총 페이지 수 구하기
int totalPage = ls.totalPage(totalCnt);
// 4. 시작 게시글 번호 구하기
String currentPage = request.getParameter("current_page");
int startNum = ls.startNum(currentPage);
// 5. 끝 게시글 번호 구하기
int endNum = ls.endNum(startNum);
ListRangeVO lrvo = new ListRangeVO(startNum, endNum);
List<DiaryListVO> list = ls.searchList(null, lrvo);
pageContext.setAttribute("list", list);
pageContext.setAttribute("totalPage", totalPage);
%>
...
- 인덱스리스트도 메소드로 구현
- c:out은 EL을 태그로 인식안해서 EL만 써줘야 함
// ListService
public String indexList(String url, int totalPage) {
StringBuilder indexList = new StringBuilder();
for(int i=1; i<= totalPage; i++) {
indexList.append("[ ").append("<a href='")
.append(url).append("?current_page=")
.append(i).append("'>").append(i).append("</a> ]");
}
return indexList.toString();
}
<!-- list.jsp -->
...
String indexList = ls.indexList("list.jsp", totalPage);
pageContext.setAttribute("indexList", indexList);
%>
</c:catch>
...
<div id="diaryIndexList">
${ indexList }
</div>
- 검색 기능 추가
- 제목, 내용, 작성자(fieldName) 선택
- 버튼 이벤트 처리
$(function() {
$("#keyword").keydown(function(evt) {
if(evt.which == 13) { // 엔터가 입력되면
if($("#keyword").val() == "") {
$("#keyword").focus();
alert("검색할 키워드를 입력해주세요.");
return;
}
}
});
$("#searchBtn").click(function() {
if($("#keyword").val() == "") {
alert("검색할 키워드를 입력해주세요.");
$("#keyword").focus();
return;
}
$("#searchFrm").submit();
});
});
<% // list.jsp
ListService ls = ListService.getInstance();
String keyword = request.getParameter("keyword");
String fieldName = request.getParameter("fieldName");
SearchDataVO sdvo = null;
if (keyword != null && !"".equals(keyword)) { // 사용자가 검색값을 넣었을 때
sdvo = new SearchDataVO(fieldName, keyword);
}
// 1. 전체 페이지 수를 얻기/ 검색에 맞는 게시물 수 얻기
int totalCnt = ls.totalCount(sdvo); // 검색 시 sdvo가 null이 아님
...
- 검색 조건에 따라 동적 쿼리를 수행
// DiaryDAO
...
public int selectEvtCnt(SearchDataVO sdvo) throws SQLException {
int cnt = 0;
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConn();
StringBuilder selectEvtCnt = new StringBuilder();
selectEvtCnt
.append(" SELECT COUNT(*) cnt ")
.append(" FROM diary ");
// 검색조건에 따라 Count할 총 게시글의 수가 달라진다(Dynamic Query)
if (sdvo != null) {
selectEvtCnt
.append(" WHERE ").append(sdvo.getFieldName()).append(" LIKE '%'||?||'%'" );
pstmt = con.prepareStatement(selectEvtCnt.toString());
pstmt.setString(1, sdvo.getKeyword());
} else {
pstmt = con.prepareStatement(selectEvtCnt.toString());
}
...
public List<DiaryListVO> selectList(SearchDataVO sdvo, ListRangeVO lrvo) throws SQLException{
List<DiaryListVO> list = new ArrayList<DiaryListVO>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConn();
StringBuilder selectList = new StringBuilder();
selectList
.append(" SELECT num, subject, writer, e_year, e_month, e_date, to_char(w_date, 'yyyy-mm-dd hh24:mi') w_date ")
.append(" FROM (SELECT num, subject, writer, e_year, e_month, ")
.append(" e_date, w_date, ROW_NUMBER() OVER(ORDER BY w_date DESC) r_num ")
.append(" FROM diary ");
if (sdvo != null) {
selectList.append(" WHERE ").append(sdvo.getFieldName())
.append(" LIKE '%'||?||'%' ");
}
selectList.append(") WHERE r_num BETWEEN ? AND ? ");
pstmt = con.prepareStatement(selectList.toString());
int bindIdx = 1; // 바인드 idx 변수로 중복코드를 줄일 수 있다.
if (sdvo != null) {
pstmt.setString(bindIdx++, sdvo.getKeyword());
}
pstmt.setInt(bindIdx++, lrvo.getStartNum());
pstmt.setInt(bindIdx++, lrvo.getEndNum());
rs = pstmt.executeQuery();
...
- ‘작성자’, ‘내용’ 검색을 수행하면 ‘제목’으로 초기화됨
- 유지가 되도록 변경
<form action="list.jsp" method="post" id="searchFrm" name="searchFrm">
<select name="fieldName" class="inputBox" id="fieldName">
<option value="subject"${ param.fieldName eq 'subject' ?" selected='selected'" : "" }>제목</option>
<option value="content"${ param.fieldName eq 'content' ?" selected='selected'" : "" }>내용</option>
<option value="writer"${ param.fieldName eq 'writer' ?" selected='selected'" : "" }>작성자</option>
</select>
<input type="text" name="keyword" class="inputBox" style="width:150px" id="keyword" value="${ param.keyword }"/>
<input type="button" value="검색" id="searchBtn" class="btn" style="width:30px;"/>
</form>
- 검색 후 인덱스 리스트를 누르면 초기화 되버린다.
- 인덱스리스트 클릭 시 검색 정보도 같이 요청되어야 됨
<!-- list.jsp -->
...
// indexList를 만들 때 sdvo도 전달
String indexList = ls.indexList("list.jsp", sdvo, totalPage);
pageContext.setAttribute("indexList", indexList);
%>
// ListService
public String indexList(String url, SearchDataVO sdvo, int totalPage) {
StringBuilder indexList = new StringBuilder();
for(int i=1; i<= totalPage; i++) {
indexList.append("[ ").append("<a href='")
.append(url).append("?current_page=")
.append(i);
if (sdvo != null) {
indexList.append("&fieldName=").append(sdvo.getFieldName())
.append("&keyword=").append(sdvo.getKeyword());
}
indexList.append("'>").append(i).append("</a> ]");
}
return indexList.toString();
}
- ‘전체글’ 버튼을 추가해서 검색 결과에서 초기화
...
<div id="diary">
<div id="diaryHeader">
<sapn style="float:left;"><a href="list.jsp"><img src="images/btn_all.png"/></a></sapn>
이벤트 목록
</div>
...
- 게시글 번호를 역순으로 나타내기
pageContext.setAttribute("bbsIdx", totalCnt-(Integer.parseInt(currentPage)-1)*pageScale);
%>
...
<c:forEach var="data" items="${ list }">
<c:set var="bbsIdx" value="${ bbsIdx }"/>
<tr>
<td><c:out value="${ bbsIdx }"/></td>
<td><c:out value="${ data.subject }"/></td>
<td><c:out value="${ data.writer }"/></td>
<td><c:out value="${ data.e_year }-${ data.e_month }-${ data.e_date }"/></td>
<td><c:out value="${ data.w_date }"/></td>
</tr>
<c:set var="bbsIdx" value="${ bbsIdx-1 }"/>
</c:forEach>
</table>
- indexList를 c:out으로 출력하고 싶을 때 “escapeXml”속성을 false로 준다.
<div id="diaryIndexList">
<c:out value="${ indexList }" escapeXml="false"/>
</div>
- 게시글 읽기
<td><a href="read.jsp?num=${ data.num }"><c:out value="${ data.subject }"/></a></td>
- read.jsp는 read.form 그대로 복 붙, form action속성만 변경
...
<form action="list.jsp" method="post" name="readFrm">
...
<!-- x버튼 삭제 -->
...
<td style="width:400px">
<%= ddvo.getContent() %>
</td>
...
<!-- 비밀번호, 이벤트일 삭제 -->
...
<td colspan="2" align="center">
<a href="#void" onclick="history.back();">글읽기</a>
</td>
- 예외처리
<%
DiaryDAO d_dao = DiaryDAO.getInstance();
try{
int num = Integer.parseInt(request.getParameter("num"));
DiaryDetailVO ddvo = d_dao.selectDetailEvent(num);
if (ddvo == null) {
throw new NullPointerException();
}
%>
...
<td style="width:400px">
<div id="subject" ><strong><%= ddvo.getSubject() %></strong></div>
</td>
...
<%
} catch (NumberFormatException nfe) {
%>
유효하지 않은 파라미터가 입력되었습니다.<br/>
<%
} catch (NullPointerException npe) {
%>
해당 글을 찾을 수 없습니다.<br/>
<%
} catch (SQLException se) {
%>
<img src="images/construction.jpg" title="죄송합니다."/>
<%
}
%>
</div>
- 일정정보를 조회할 때 CLOB 데이터형을 getString으로 가져오는건 틀린 방법
- CLOB은 getClob을 사용해서 처리한다.
public DiaryDetailVO selectDetailEvent(int num) throws SQLException, IOException{
DiaryDetailVO ddvo = null;
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
BufferedReader br = null;
try {
con = getConn();
StringBuilder selectOneEvt = new StringBuilder();
selectOneEvt
.append(" SELECT WRITER, SUBJECT, CONTENT, TO_CHAR(W_DATE, 'YYYY-MM-DD DY HH24:MI') W_DATE, IP ")
.append(" FROM DIARY ")
.append(" WHERE NUM=? ");
pstmt = con.prepareStatement(selectOneEvt.toString());
pstmt.setInt(1, num);
rs = pstmt.executeQuery();
if(rs.next()) {
// CLOB(Character Large Object) 처리
Clob clob = rs.getClob("content");
// CLOB처리를 위해 별도의 스트림을 연결
br = new BufferedReader(clob.getCharacterStream());
String temp = "";
StringBuilder content = new StringBuilder();
while((temp = br.readLine()) != null) {
content.append(temp);
}
ddvo = new DiaryDetailVO(rs.getString("writer"),
rs.getString("subject"), content.toString(),
rs.getString("w_date"), rs.getString("ip"));
}
} finally {
if (br != null) { br.close(); }
if (rs != null) { rs.close(); }
if (pstmt != null) { pstmt.close(); }
if (con != null) { con.close(); }
}
return ddvo;
}
<!-- read_form.jsp -->
...
<div id="readFrm">
<%
DiaryDAO d_dao = DiaryDAO.getInstance();
try{
int num = Integer.parseInt(request.getParameter("num"));
DiaryDetailVO ddvo = d_dao.selectDetailEvent(num);
%>
...
</form>
<%
} catch (IOException ie) {
%>
<img src="images/construction.jpg" title="죄송합니다."/>
<%
} catch (SQLException se) {
%>
<img src="images/construction.jpg" title="죄송합니다."/>
<%
}
%>
</div>
<!-- read.jsp -->
...
<%
DiaryDAO d_dao = DiaryDAO.getInstance();
try{
int num = Integer.parseInt(request.getParameter("num"));
DiaryDetailVO ddvo = d_dao.selectDetailEvent(num);
if (ddvo == null) {
throw new NullPointerException();
}
%>
...
<%
} catch (IOException ie) {
%>
글의 내용을 읽어들이지 못했습니다.<br/>
<%
} catch (NumberFormatException nfe) {
%>
유효하지 않은 파라미터가 입력되었습니다.<br/>
<%
} catch (NullPointerException npe) {
%>
해당 글을 찾을 수 없습니다.<br/>
<%
} catch (SQLException se) {
%>
<img src="images/construction.jpg" title="죄송합니다."/>
<%
}
%>
</div>