2020년 01월 06일
업데이트:
게시글
게시글 상세조회
boardList.jsp
한 행의 어느 곳을 눌러도 게시글 상세조회가 진행된다.
<script>
// 게시글 상세보기 기능 (jquery를 통해 작업)
$("#list-table td").on("click",function(){
// 게시글 번호 얻어오기
var boardNo = $(this).parent().children().eq(0).text();
//console.log(boardNo);
// 클릭이 되는지 테스트
var url = "${contextPath}/board/view.do?cp=${pInfo.currentPage}&no="+boardNo;
location.href = url;
});
</script>
게시글 상세조회 초기 화면
게시글 상세조회 화면 구성 후
게시글 조회수 증가
BoardController
// 게시글 상세조회 Controller *********************************************
else if(command.contentEquals("/view.do")) {
errorMsg = "게시글 상세 조회 과정에서 오류 발생";
int boardNo = Integer.parseInt(request.getParameter("no"));
// 상세조회 비즈니스 로직 수행 후 결과 반환 받기
Board board = service.selectBoard(boardNo);
if(board!=null) { // 상세조회 성공 시
path = "/WEB-INF/views/board/boardView.jsp";
request.setAttribute("board", board);
view = request.getRequestDispatcher(path);
view.forward(request, response);
}else {
request.getSession().setAttribute("swalIcon", "error");
request.getSession().setAttribute("swalTitle", "게시글 상세 조회 실패");
response.sendRedirect("list.do");
}
}
BoardService.java
상세 조회에 성공할 때마다 조회 수가 1씩 증가해야 되기 때문에 같이 작성해 준다.
/** 게시글 상세조회 service
* @param boardNo
* @return board
* @throws Exception
*/
public Board selectBoard(int boardNo) throws Exception {
Connection conn = getConnection();
Board board = dao.selectBoard(conn,boardNo);
if(board != null) { // DB에서 조회 성공 시
// 조회수 증가
int result = dao.increaseReadCount(conn,boardNo);
if(result>0) {
commit(conn);
// 반환되는 Board 데이터에는 조회수가 증가되어 있지 않기 때문에
// 조회수를 1 증가 시켜줌
board.setReadCount(board.getReadCount() + 1);
}
else rollback(conn);
}
close(conn);
return board;
}
BoardDao.java
/** 게시글 상세조회 DAO
* @param conn
* @param boardNo
* @return board
* @throws Exception
*/
public Board selectBoard(Connection conn, int boardNo) throws Exception {
Board board = null;
String query = prop.getProperty("selectBoard");
try {
pstmt = conn.prepareStatement(query);
pstmt.setInt(1,boardNo);
rset = pstmt.executeQuery();
if(rset.next()) {
board = new Board();
board.setBoardNo(rset.getInt("BOARD_NO"));
board.setBoardTitle(rset.getString("BOARD_TITLE"));
board.setBoardContent(rset.getString("BOARD_CONTENT"));
board.setMemberId(rset.getString("MEMBER_ID"));
board.setReadCount(rset.getInt("READ_COUNT"));
board.setBoardCreateDate(rset.getTimestamp("BOARD_CREATE_DT"));
board.setBoardModifyDate(rset.getTimestamp("BOARD_MODIFY_DT"));
board.setCategoryName(rset.getString("CATEGORY_NM"));
}
}finally {
close(rset);
close(pstmt);
}
return board;
}
/** 조회수 증가 DAO
* @param conn
* @param boardNo
* @return result
* @throws Exception
*/
public int increaseReadCount(Connection conn, int boardNo) throws Exception {
int result =0;
String query = prop.getProperty("increaseReadCount");
try {
pstmt = conn.prepareStatement(query);
pstmt.setInt(1,boardNo);
result = pstmt.executeUpdate();
}finally {
close(pstmt);
}
return result;
}
SQL
<!-- 게시글 상세 조회 -->
<entry key="selectBoard">
SELECT * FROM V_BOARD
WHERE BOARD_NO=?
AND BOARD_STATUS='Y'
</entry>
<!-- 조회수 증가 -->
<entry key="increaseReadCount">
UPDATE BOARD SET
READ_COUNT = READ_COUNT+1
WHERE BOARD_NO =?
</entry>
본인이 작성한 글에만 수정/삭제 버튼이 노출된다.
로그인 안 했을때
글 작성자로 로그인을 했을 때
boardView.jsp
<%-- 로그인된 회원과 해당 글 작성자가 같은 경우--%>
<c:if test="${!empty loginMember && (board.memberId == loginMember.memberId) }">
<button id="deleteBtn" class="btn btn-primary float-right">삭제</button>
<a href="#" class="btn btn-primary float-right ml-1 mr-1">수정</a>
</c:if>
게시글 검색
boardList.jsp
<div class="my-5">
<form action="${contextPath }/search.do" method="GET" class="text-center" id="searchForm">
<select name="sk" class="form-control" style="width: 100px; display: inline-block;">
<option value="title">글제목</option>
<option value="content">내용</option>
<option value="titcont">제목+내용</option>
<option value="writer">작성자</option>
</select>
<input type="text" name="sv" class="form-control" style="width: 25%; display: inline-block;">
<button class="form-control btn btn-primary" style="width: 100px; display: inline-block;">검색</button>
</form>
</div>
목록으로 버튼을 누르면 기존 페이지가 유지된다.
검색과 관련된 기능들을 모아서 관리 할 Controller생성.
SearchController.java
package com.kh.wsp.search.controller;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.kh.wsp.board.model.vo.Board;
import com.kh.wsp.board.model.vo.PageInfo;
import com.kh.wsp.search.model.service.SearchService;
@WebServlet("/search.do")
public class SearchController extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String searchKey = request.getParameter("sk");
String searchValue = request.getParameter("sv");
String cp = request.getParameter("cp");
try {
SearchService service = new SearchService();
Map<String, Object> map = new HashMap<String, Object>();
map.put("searchKey", searchKey);
map.put("searchValue", searchValue);
map.put("currentPage", cp);
검색 내용이 포함된 페이징 처리
SearchController.java
// 페이징 처리를 위한 데이터를 계산하고 저장하는 객체 PageInfo 얻어오기
PageInfo pInfo = service.getPageInfo(map);
SearchService.java
package com.kh.wsp.search.model.service;
import static com.kh.wsp.common.JDBCTemplate.*;
import java.sql.Connection;
import java.util.List;
import java.util.Map;
import com.kh.wsp.board.model.vo.Board;
import com.kh.wsp.board.model.vo.PageInfo;
import com.kh.wsp.search.model.dao.SearchDAO;
public class SearchService {
private SearchDAO dao = new SearchDAO();
/** 검색 내용이 포함된 페이징 처리 정보 생성 service
* @param map
* @return pInfo
* @throws Exception
*/
public PageInfo getPageInfo(Map<String, Object> map) throws Exception {
Connection conn = getConnection();
// 얻어온 파라미트 cp 가 null이면 1, 아니면 int형으로 파싱
map.put("currentPage", (map.get("currentPage") == null )? 1 : Integer.parseInt((String)map.get("currentPage")) );
// 검색 조건에 따른 SQL 조건문을 조합하는 메소드 호출
String condition = createCondition(map);
// SQL 조건문을 map에 추가
map.put("condition", condition);
// DB에서 조건을 만족하는 게시글의 수를 조회하기
int listCount = dao.getListCount(conn, condition);
close(conn);
// PageInfo 객체를 생성하여 반환
return new PageInfo((int)map.get("currentPage"), listCount) ;
}
SeachDAO.java
package com.kh.wsp.search.model.dao;
import static com.kh.wsp.common.JDBCTemplate.*;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import com.kh.wsp.board.model.vo.Board;
import com.kh.wsp.board.model.vo.PageInfo;
public class SearchDAO {
private Statement stmt = null;
private PreparedStatement pstmt = null;
private ResultSet rset = null;
/** 조건을 만족하는 게시글 수 조회 DAO
* @param conn
* @param condition
* @return listCount
* @throws Exception
*/
public int getListCount(Connection conn, String condition) throws Exception {
int listCount = 0;
String query = "SELECT COUNT(*) FROM V_BOARD WHERE BOARD_STATUS = 'Y' AND " + condition;
try {
stmt = conn.createStatement();
rset = stmt.executeQuery(query);
if(rset.next()) {
listCount = rset.getInt(1);
}
}finally {
close(rset);
close(stmt);
}
return listCount;
}
검색 전/후 주소 조건으로 나눠주기
검색을 하지 않았을 때
검색을 했을 때
boardList.jsp
<%---------------------- Pagination ----------------------%>
<%-- 페이징 처리 주소를 쉽게 사용할 수 있도록 미리 변수에 저장 --%>
<c:choose>
<%-- 검색 내용이 파라미터에 존재할 때 == 검색을 통해 만들어진 페이지인가? --%>
<c:when test="${!empty param.sk && !empty param.sv }">
<c:url var="pageUrl" value="/search.do"/>
<%-- 쿼리스트링으로 사용할 내용을 변수에 저장함. --%>
<c:set var="searchStr" value="&sk=${param.sk }&sv=${param.sv }"/>
</c:when>
<c:otherwise>
<c:url var="pageUrl" value="/board/list.do"/>
</c:otherwise>
</c:choose>
<!-- 화살표에 들어갈 주소를 변수로 생성 -->
<%-- 검색을 안 했을 때 : /board/list.do?cp=1
검색을 했을 때 : /search.do?cp=1&sk=title&sc=49 --%>
<c:set var="firstPage" value="${pageUrl}?cp=1${searchStr }"/>
<c:set var="lastPage" value="${pageUrl}?cp=${pInfo.maxPage}${searchStr }"/>
<%-- EL을 이용한 숫자 연산의 단점 : 연산이 자료형에 영향을 받지 않는다. --%>
<%-- <fmt:parseNumber> : 숫자 형태를 지정하여 변수 선언
integerOnly="true" : 정수로만 숫자 표현 (소수점 버림)
--%>
<fmt:parseNumber var="c1" value="${(pInfo.currentPage - 1)/10}" integerOnly="true"/>
<fmt:parseNumber var="prev" value="${c1 * 10}" integerOnly="true"/>
<c:set var="prevPage" value="${pageUrl}?cp=${prev}${searchStr }"/> <!-- /board/list/do?cp=10 -->
<fmt:parseNumber var="c2" value="${(pInfo.currentPage + 9)/10}" integerOnly="true"/>
<fmt:parseNumber var="next" value="${c2 * 10 + 1}" integerOnly="true"/>
<c:set var="nextPage" value="${pageUrl}?cp=${next}${searchStr }"/>
<div class="my-5">
<ul class="pagination">
<%-- 현재 페이지가 10페이지 초과인 경우 --%>
<c:if test="${pInfo.currentPage>10}">
<li><!-- 첫 페이지로 이동(<<) -->
<a class="page-link" href="${firstPage}"><<</a>
</li>
<li> <!-- 이전 페이지로 이동(<) -->
<a class="page-link" href="${prevPage}"><</a>
</li>
</c:if>
<!-- 페이지 목록 -->
<c:forEach var="page" begin="${pInfo.startPage}" end="${pInfo.endPage}">
<!-- 1부터 10까지 1씩(step설정이 안되어있다면) 순차적으로 증가해라 -->
<c:choose>
<c:when test="${pInfo.currentPage == page}">
<li>
<a class="page-link">${page}</a> <!-- a태그의 href를 없애서 클릭이 안되게 한다. -->
</li>
</c:when>
<c:otherwise>
<li>
<a class="page-link" href="${pageUrl}?cp=${page}${searchStr }">${page}</a>
</li>
</c:otherwise>
</c:choose>
</c:forEach>
<%-- 현재 페이지가 마지막 페이지 미만인 경우 --%>
<c:if test="${next < pInfo.maxPage}">
<li> <!-- 다음 페이지로 이동(>) -->
<a class="page-link" href="${nextPage}">></a>
</li>
<li><!-- 마지막 페이지로 이동(>>) -->
<a class="page-link" href="${lastPage}">>></a>
</li>
</c:if>
</ul>
</div>
검색 내용이 있을 경우 검색창에 해당 내용을 작성해두는 기능
검색어 유지
boardList.jsp
<script>
// 검색 내용이 있을 경우 검색창에 해당 내용을 작성해두는 기능
(function(){
var searchKey = "${param.sk}";
// 파라미터 중 sk가 있을 경우 ex ) "49"
// 파라미터 중 sk가 없을 경우 ex ) 빈문자열
var searchValue = "${param.sv}";
// 검색창 select의 option을 반복 접근
$("select[name=sk]>option").each(function(index,item){
// index : 현재 접근중인 요소의 인덱스
// item : 현재 접근중인 요소
if($(item).val() == searchKey){
$(item).prop("selected",true);
}
});
// 검색어 입력창에 searchValue 값 출력
$("input[name=sv]").val(searchValue);
})();
</script>
게시글 상세보기 기능과 목록으로 버튼에 검색여부 조건 추가해주기
boardList.jsp
게시글 상세보기 기능에도 쿼리스트링 변수를 추가해 준다.
<script>
// 게시글 상세보기 기능 (jquery를 통해 작업)
$("#list-table td").on("click",function(){
// 게시글 번호 얻어오기
var boardNo = $(this).parent().children().eq(0).text();
//console.log(boardNo);
// 클릭이 되는지 테스트
var url =
"${contextPath}/board/view.do?cp=${pInfo.currentPage}&no="+boardNo + "${searchStr}";
// 검색
location.href = url;
});
</script>
boardView.jsp
<c:choose>
<c:when test="${!empty param.sk && !empty param.sv }">
<c:url var="goToList" value="../search.do">
<%-- .. == 상위 주소로 이동 --%>
<c:param name="cp">${param.cp }</c:param>
<c:param name="sk">${param.sk }</c:param>
<c:param name="sv">${param.sv }</c:param>
</c:url>
</c:when>
<c:otherwise>
<c:url var="goToList" value="list.do">
<!-- url를 사용하면 쿼리스트링을 만들 수 있다. -->
<c:param name="cp">${param.cp }</c:param>
</c:url>
<!-- 이부분이 원래 바깥에 있었음 -->
</c:otherwise>
</c:choose>
<a href="${goToList}" class="btn btn-primary float-right">목록으로</a>
로그인을 했을 때에만 글쓰기 버튼이 보임
로그인 안 했을 때
로그인 했을 때
boardList.jsp
<%-- 로그인이 되어있는 경우 --%>
<c:if test="${!empty loginMember }">
<button type="button" class="btn btn-primary float-right" id="insertBtn"
onclick="location.href = '${contextPath}/board/insertForm.do'">글쓰기</button>
</c:if>
글쓰기
글쓰기 화면 구성
글쓰기 초기 화면
boardInsert.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시판</title>
<style>
.insert-label {
display: inline-block;
width: 80px;
line-height: 40px
}
.boardImg{
cursor : pointer;
}
</style>
</head>
<body>
<jsp:include page="../common/header.jsp"></jsp:include>
<div class="container my-5">
<h3>게시글 등록</h3>
<hr>
<!-- 파일 업로드를 위한 라이브러리 cos.jar 라이브러리 다운로드(http://www.servlets.com/) -->
<!--
- enctype : form 태그 데이터가 서버로 제출 될 때 인코딩 되는 방법을 지정. (POST 방식일 때만 사용 가능)
- application/x-www-form-urlencoded : 모든 문자를 서버로 전송하기 전에 인코딩 (form태그 기본값)
- multipart/form-data : 모든 문자를 인코딩 하지 않음.(원본 데이터가 유지되어 이미지, 파일등을 서버로 전송 할 수 있음.)
-->
<form action="${contextPath }/board/insert.do" method="post"
enctype="multipart/form-data" role="form" onsubmit="return boardValidate();">
<div class="mb-2">
<label class="input-group-addon mr-3 insert-label">카테고리</label>
<select class="custom-select" id="categoryCode" name="categoryCode" style="width: 150px;">
<option value="10">운동</option>
<option value="20">영화</option>
<option value="30">음악</option>
<option value="40">요리</option>
<option value="50">게임</option>
<option value="60">기타</option>
</select>
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">제목</label>
<input type="text" class="form-control" id="boardTitle" name="boardTitle" size="70">
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">작성자</label>
<h5 class="my-0" id="writer">${loginMember.memberId }</h5>
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">작성일</label>
<h5 class="my-0" id="today"></h5>
</div>
<hr>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">썸네일</label>
<div class="boardImg" id="titleImgArea">
<img id="titleImg" width="200" height="200">
</div>
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">업로드<br>이미지</label>
<div class="mr-2 boardImg" id="contentImgArea1">
<img id="contentImg1" width="150" height="150">
</div>
<div class="mr-2 boardImg" id="contentImgArea2">
<img id="contentImg2" width="150" height="150">
</div>
<div class="mr-2 boardImg" id="contentImgArea3">
<img id="contentImg3" width="150" height="150">
</div>
</div>
<!-- 파일 업로드 하는 부분 -->
<div id="fileArea">
<input type="file" id="img0" name="img0" onchange="LoadImg(this,0)" multiple="multiple">
<!-- multiple 속성 = 사진 여러개 선택 가능 -->
<input type="file" id="img1" name="img1" onchange="LoadImg(this,1)">
<input type="file" id="img2" name="img2" onchange="LoadImg(this,2)">
<input type="file" id="img3" name="img3" onchange="LoadImg(this,3)">
</div>
<div class="form-group">
<div>
<label for="content">내용</label>
</div>
<textarea class="form-control" id="boardContent" name="boardContent" rows="15" style="resize: none;"></textarea>
</div>
<hr class="mb-4">
<div class="text-center">
<button type="submit" class="btn btn-primary">등록</button>
<button type="button" class="btn btn-primary">목록으로</button>
</div>
</form>
</div>
<jsp:include page="../common/footer.jsp"></jsp:include>
<script>
(function printToday(){
// 오늘 날짜 출력
var today = new Date();
var month = (today.getMonth()+1);
var date = today.getDate();
var str = today.getFullYear() + "-"
+ (month < 10 ? "0"+month : month) + "-"
+ (date < 10 ? "0"+date : date);
$("#today").html(str);
})();
// 유효성 검사
function boardValidate() {
if ($("#boardTitle").val().trim().length == 0) {
alert("제목을 입력해 주세요.");
$("#title").focus();
return false;
}
if ($("#content").val().trim().length == 0) {
alert("내용을 입력해 주세요.");
$("#content").focus();
return false;
}
}
</script>
</body>
</html>
파일 업로드
-
COS 코스 다운로드 후, 라이브러리 추가하기
라이브러리 추가
-
이미지 업로드와 관련된 스크립트 작성
boardInsert.jsp
<script>
// 이미지 영역을 클릭할 때 파일 첨부 창이 뜨도록 설정하는 함수
// 페이지 로딩이 끝나고나면 #fileArea 요소를 숨김.
$(function(){
$("#fileArea").hide();
// 파일 선택 버튼이 사라진다.
$(".boardImg").on("click",function(){// 이미지 영역이 클릭 되었을 때
// 클릭한 이미지 영역 인덱스 얻어오기
var index = $(".boardImg").index(this);
// 클릭된 요소가 .boardImg 중 몇 번째 인덱스인지 반환
// console.log(index);
// 클릭된 영역 인덱스에 맞는 input file 태그 클릭
$("#img" + index).click();
});
});
// 각각의 영역에 파일을 첨부 했을 경우 미리 보기가 가능하도록 하는 함수
function LoadImg(value, num) {
// value.files == input태그에 파일이 업로드되어 있으면 true
// value.files[0] : 여러 파일 중 첫번째 파일이 업로드 되어있으면 true
if(value.files && value.files[0] ){ // 해당 요소에 업로드된 파일이 있을 경우
var reader = new FileReader();
// 자바스크립트 FileReader
// 웹 애플리케이션이 비동기적으로 데이터를 읽기 위하여
// 읽을 파일을 가리키는 File 혹은 Blob객체를 이용해 파일의 내용을 읽고
// 사용자의 컴퓨터에 저장하는 것을 가능하게 해주는 객체
reader.readAsDataURL(value.files[0]);
// FileReader.readAsDataURL()
// 지정된의 내용을 읽기 시작합니다.
// Blob완료되면 result속성 data:에 파일 데이터를 나타내는 URL이 포함 됨.
reader.onload = function(e){
// FileReader.onload
// load 이벤트의 핸들러.
// 이 이벤트는 읽기 동작이 성공적으로 완료 되었을 때마다 발생함.
// 읽어들인 내용(이미지 파일)을 화면에 출력
$(".boardImg").eq(num).children("img").attr("src", e.target.result);
// e.target.result : 이벤트가 발생한 요소의 결과 , 파일 읽기 동작을 성공한 요소가 읽어들인 파일 내용
}
}
}
</script>
전체 boardView.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시글</title>
<style>
#board-area{ height: 700px; }
#board-content{ padding-bottom:150px;}
.boardImgArea{
height: 300px;
}
.boardImg{
width : 100%;
height: 100%;
max-width : 300px;
max-height: 300px;
margin : auto;
}
/* 이미지 화살표 색 조정
-> fill='%23000' 부분의 000을
RGB 16진수 값을 작성하여 변경 가능
*/
.carousel-control-prev-icon {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23000' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E") !important;
}
.carousel-control-next-icon {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23000' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E") !important;
}
.replyWrite > table{
width: 90%;
align: center;
}
#replyContentArea{ width: 90%; }
#replyContentArea > textarea{
resize: none;
width: 100%;
}
#replyBtnArea{
width: 100px;
text-align: center;
}
.rWriter{ margin-right: 30px;}
.rDate{
font-size: 0.7em;
color : gray;
}
#replyListArea{
list-style-type: none;
}
.board-dateArea{
font-size: 14px;
}
</style>
</head>
<body>
<jsp:include page="../common/header.jsp"></jsp:include>
<div class="container my-5">
<div>
<div id="board-area">
<!-- Category -->
<h6 class="mt-4">카테고리 : [${board.categoryName}]</h6>
<!-- Title -->
<h3 class="mt-4">${board.boardTitle}</h3>
<!-- Writer -->
<p class="lead">
작성자 : ${board.memberId}
</p>
<hr>
<!-- Date -->
<p>
<span class="board-dateArea">
작성일 : <fmt:formatDate value="${board.boardCreateDate}" pattern="yyyy년 MM월 DD일 HH:mm:ss"/>
<br>
마지막 수정일 : <fmt:formatDate value="${board.boardModifyDate}" pattern="yyyy년 MM월 DD일 HH:mm:ss"/>
</span>
<span class="float-right">조회수 ${board.readCount} </span>
</p>
<hr>
<!-- 이미지 출력 -->
<!-- Content -->
<div id="board-content">${board.boardContent}</div>
<hr>
<div>
<%-- 로그인된 회원과 해당 글 작성자가 같은 경우--%>
<c:if test="${!empty loginMember && (board.memberId == loginMember.memberId) }">
<button id="deleteBtn" class="btn btn-primary float-right">삭제</button>
<a href="#" class="btn btn-primary float-right ml-1 mr-1">수정</a>
</c:if>
<%--
상대 경로 작성법
- 앞에 아무것도 없음 : 현재 위치(주소 제일 마지막 /뒷부분)
- ../ : 현재 위치에서 한단계 상위 (주소 제일 마지막 /보다 왼쪽으로 한칸 앞 /)
--%>
<c:choose>
<c:when test="${!empty param.sk && !empty param.sv }">
<c:url var="goToList" value="../search.do">
<%-- .. == 상위 주소로 이동 --%>
<c:param name="cp">${param.cp }</c:param>
<c:param name="sk">${param.sk }</c:param>
<c:param name="sv">${param.sv }</c:param>
</c:url>
</c:when>
<c:otherwise>
<c:url var="goToList" value="list.do">
<!-- url를 사용하면 쿼리스트링을 만들 수 있다. -->
<c:param name="cp">${param.cp }</c:param>
</c:url>
<!-- 이부분이 원래 바깥에 있었음 -->
</c:otherwise>
</c:choose>
<a href="${goToList}" class="btn btn-primary float-right">목록으로</a>
</div>
</div>
</div>
</div>
<jsp:include page="../common/footer.jsp"></jsp:include>
<script>
</script>
</body>
</html>
전체 boardList.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시판</title>
<style>
.pagination {
justify-content: center;
}
#searchForm {
position: relative;
}
#searchForm>* {
top: 0;
}
.boardTitle>img {
width: 50px;
height: 50px;
}
#list-table th {
text-align: center;
}
#list-table td:not(:nth-of-type(3)) {
text-align: center;
}
.list-wrapper{
height: 540px;
}
#list-table td:hover{
cursor : pointer;
}
</style>
</head>
<body>
<jsp:include page="../common/header.jsp"></jsp:include>
<div class="container my-5">
<h1>게시판</h1>
<div class="list-wrapper">
<table class="table table-hover table-striped my-5" id="list-table">
<thead>
<tr>
<th>글번호</th>
<th>카테고리</th>
<th>제목</th>
<th>작성자</th>
<th>조회수</th>
<th>작성일</th>
</tr>
</thead>
<%-- 게시글 목록 출력 --%>
<tbody>
<c:choose>
<c:when test="${empty bList }">
<!-- bList가 비어있을 때 : 게시글 목록 조회에서 조회되지 않았을 때 -->
<tr>
<td colspan="6">존재하는 게시글이 없습니다.</td>
</tr>
</c:when>
<c:otherwise> <!-- 조회된 게시글 목록이 있을 때 -->
<c:forEach var="board" items="${bList }"> <!-- 이때 bList에는 최소 1개, 최대 10개의 데이터가 들어있다. -->
<!-- for문으로 반복접근해서 모든 데이터를 출력 해준다.
한 바퀴마다 bList에서 하나씩 꺼내와 board에 담는다 -->
<tr>
<td>${board.boardNo }</td> <!--글번호 -->
<td>${board.categoryName }</td> <!--카테고리명 -->
<td class="boardTitle">
${board.boardTitle }
</td> <!--제목 -->
<td>${board.memberId }</td> <!--작성자 -->
<td>${board.readCount }</td> <!-- 조회수 -->
<td>
<%-- 날짜 출력 모양 지정 --%>
<fmt:formatDate var="createDate" value="${board.boardCreateDate}" pattern="yyyy-MM-dd"/>
<fmt:formatDate var="today" value="<%= new java.util.Date() %>" pattern="yyyy-MM-dd"/>
<c:choose>
<%-- 글 작성일이 오늘이 아닐 경우 --%>
<c:when test="${createDate != today}">
${createDate }
</c:when>
<%-- 글 작성일이 오늘일 경우 --%>
<c:otherwise>
<fmt:formatDate value="${board.boardCreateDate}" pattern="HH:mm"/>
</c:otherwise>
</c:choose>
</td> <!-- 날짜 출력 -->
</tr>
</c:forEach>
</c:otherwise>
</c:choose>
</tbody>
</table>
</div>
<%-- 로그인이 되어있는 경우 --%>
<c:if test="${!empty loginMember }">
<button type="button" class="btn btn-primary float-right" id="insertBtn"
onclick="location.href = '${contextPath}/board/insertForm.do'">글쓰기</button>
</c:if>
<%---------------------- Pagination ----------------------%>
<%-- 페이징 처리 주소를 쉽게 사용할 수 있도록 미리 변수에 저장 --%>
<c:choose>
<%-- 검색 내용이 파라미터에 존재할 때 == 검색을 통해 만들어진 페이지인가? --%>
<c:when test="${!empty param.sk && !empty param.sv }">
<c:url var="pageUrl" value="/search.do"/>
<%-- 쿼리스트링으로 사용할 내용을 변수에 저장함. --%>
<c:set var="searchStr" value="&sk=${param.sk }&sv=${param.sv }"/>
</c:when>
<c:otherwise>
<c:url var="pageUrl" value="/board/list.do"/>
</c:otherwise>
</c:choose>
<!-- 화살표에 들어갈 주소를 변수로 생성 -->
<%-- 검색을 안 했을 때 : /board/list.do?cp=1
검색을 했을 때 : /search.do?cp=1&sk=title&sc=49 --%>
<c:set var="firstPage" value="${pageUrl}?cp=1${searchStr }"/>
<c:set var="lastPage" value="${pageUrl}?cp=${pInfo.maxPage}${searchStr }"/>
<%-- EL을 이용한 숫자 연산의 단점 : 연산이 자료형에 영향을 받지 않는다. --%>
<%-- <fmt:parseNumber> : 숫자 형태를 지정하여 변수 선언
integerOnly="true" : 정수로만 숫자 표현 (소수점 버림)
--%>
<fmt:parseNumber var="c1" value="${(pInfo.currentPage - 1)/10}" integerOnly="true"/>
<fmt:parseNumber var="prev" value="${c1 * 10}" integerOnly="true"/>
<c:set var="prevPage" value="${pageUrl}?cp=${prev}${searchStr }"/> <!-- /board/list/do?cp=10 -->
<fmt:parseNumber var="c2" value="${(pInfo.currentPage + 9)/10}" integerOnly="true"/>
<fmt:parseNumber var="next" value="${c2 * 10 + 1}" integerOnly="true"/>
<c:set var="nextPage" value="${pageUrl}?cp=${next}${searchStr }"/>
<div class="my-5">
<ul class="pagination">
<%-- 현재 페이지가 10페이지 초과인 경우 --%>
<c:if test="${pInfo.currentPage>10}">
<li><!-- 첫 페이지로 이동(<<) -->
<a class="page-link" href="${firstPage}"><<</a>
</li>
<li> <!-- 이전 페이지로 이동(<) -->
<a class="page-link" href="${prevPage}"><</a>
</li>
</c:if>
<!-- 페이지 목록 -->
<c:forEach var="page" begin="${pInfo.startPage}" end="${pInfo.endPage}">
<!-- 1부터 10까지 1씩(step설정이 안되어있다면) 순차적으로 증가해라 -->
<c:choose>
<c:when test="${pInfo.currentPage == page}">
<li>
<a class="page-link">${page}</a>
<!-- a태그의 href를 없애서 클릭이 안되게 한다. -->
</li>
</c:when>
<c:otherwise>
<li>
<a class="page-link" href="${pageUrl}?cp=${page}${searchStr }">${page}</a>
</li>
</c:otherwise>
</c:choose>
</c:forEach>
<%-- 현재 페이지가 마지막 페이지 미만인 경우 --%>
<c:if test="${next < pInfo.maxPage}">
<li> <!-- 다음 페이지로 이동(>) -->
<a class="page-link" href="${nextPage}">></a>
</li>
<li><!-- 마지막 페이지로 이동(>>) -->
<a class="page-link" href="${lastPage}">>></a>
</li>
</c:if>
</ul>
</div>
<!-- 검색창 -->
<div class="my-5">
<form action="${contextPath }/search.do" method="GET" class="text-center" id="searchForm">
<select name="sk" class="form-control" style="width: 100px; display: inline-block;">
<option value="title">글제목</option>
<option value="content">내용</option>
<option value="titcont">제목+내용</option>
<option value="writer">작성자</option>
</select>
<input type="text" name="sv" class="form-control" style="width: 25%; display: inline-block;">
<button class="form-control btn btn-primary" style="width: 100px; display: inline-block;">검색</button>
</form>
</div>
</div>
<jsp:include page="../common/footer.jsp"></jsp:include>
<script>
// 게시글 상세보기 기능 (jquery를 통해 작업)
$("#list-table td").on("click",function(){
// 게시글 번호 얻어오기
var boardNo = $(this).parent().children().eq(0).text();
//console.log(boardNo);
// 클릭이 되는지 테스트
var url = "${contextPath}/board/view.do?cp=${pInfo.currentPage}&no="+boardNo + "${searchStr}";
// 검색
location.href = url;
});
// 검색 내용이 있을 경우 검색창에 해당 내용을 작성해두는 기능
(function(){
var searchKey = "${param.sk}";
// 파라미터 중 sk가 있을 경우 ex ) "49"
// 파라미터 중 sk가 없을 경우 ex ) 빈문자열
var searchValue = "${param.sv}";
// 검색창 select의 option을 반복 접근
$("select[name=sk]>option").each(function(index,item){
// index : 현재 접근중인 요소의 인덱스
// item : 현재 접근중인 요소
if($(item).val() == searchKey){
$(item).prop("selected",true);
}
});
// 검색어 입력창에 searchValue 값 출력
$("input[name=sv]").val(searchValue);
})();
</script>
</body>
</html>
전체 SearchController.java
package com.kh.wsp.search.controller;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.kh.wsp.board.model.vo.Board;
import com.kh.wsp.board.model.vo.PageInfo;
import com.kh.wsp.search.model.service.SearchService;
@WebServlet("/search.do")
public class SearchController extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String searchKey = request.getParameter("sk");
String searchValue = request.getParameter("sv");
String cp = request.getParameter("cp");
try {
SearchService service = new SearchService();
Map<String, Object> map = new HashMap<String, Object>();
map.put("searchKey", searchKey);
map.put("searchValue", searchValue);
map.put("currentPage", cp);
// 페이징 처리를 위한 데이터를 계산하고 저장하는 객체 PageInfo 얻어오기
PageInfo pInfo = service.getPageInfo(map);
// 검색 게시글 목록 조회
List<Board> bList = service.searchBoardList(map, pInfo);
// 결과 확인
// System.out.println(pInfo);
// for(Board b:bList) {
// System.out.println(b);
// }
// 조회된 내용과 PageInfo 객체를 request객체에 담아서 요청 위임
String path = "/WEB-INF/views/board/boardList.jsp";
request.setAttribute("bList", bList);
request.setAttribute("pInfo", pInfo);
RequestDispatcher view = request.getRequestDispatcher(path);
view.forward(request, response);
}catch(Exception e) {
e.printStackTrace();
String path = "/WEB-INF/views/common/errorPage.jsp";
request.setAttribute("errorMsg", "검색 과정에서 오류 발생");
RequestDispatcher view = request.getRequestDispatcher(path);
view.forward(request, response);
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
전체 SearchService.java
package com.kh.wsp.search.model.service;
import static com.kh.wsp.common.JDBCTemplate.*;
import java.sql.Connection;
import java.util.List;
import java.util.Map;
import com.kh.wsp.board.model.vo.Board;
import com.kh.wsp.board.model.vo.PageInfo;
import com.kh.wsp.search.model.dao.SearchDAO;
public class SearchService {
private SearchDAO dao = new SearchDAO();
/** 검색 내용이 포함된 페이징 처리 정보 생성 service
* @param map
* @return pInfo
* @throws Exception
*/
public PageInfo getPageInfo(Map<String, Object> map) throws Exception {
Connection conn = getConnection();
// 얻어온 파라미트 cp 가 null이면 1, 아니면 int형으로 파싱
map.put("currentPage", (map.get("currentPage") == null )? 1 : Integer.parseInt((String)map.get("currentPage")) );
// 검색 조건에 따른 SQL 조건문을 조합하는 메소드 호출
String condition = createCondition(map);
// SQL 조건문을 map에 추가
map.put("condition", condition);
// DB에서 조건을 만족하는 게시글의 수를 조회하기
int listCount = dao.getListCount(conn, condition);
close(conn);
// PageInfo 객체를 생성하여 반환
return new PageInfo((int)map.get("currentPage"), listCount) ;
}
/** 검색 조건에 따라 SQL에 사용될 조건문을 조합하는 메소드
* @param map
* @return
*/
private String createCondition(Map<String, Object> map) {
String condition = null;
String searchKey = (String)map.get("searchKey");
String searchValue = (String)map.get("searchValue");
// 검색 조건(searchKey)에 따라 SQL 조합
switch(searchKey) {
// "BOARD_TITLE LIKE '%' || '49' || '%'"
case "title" : condition = " BOARD_TITLE LIKE '%' || '" + searchValue + "' || '%' "; break;
case "content" : condition = " BOARD_CONTENT LIKE '%' || '" + searchValue + "' || '%' "; break;
case "titcont" : condition = " (BOARD_TITLE LIKE '%' || '" + searchValue + "' || '%' "
+ "OR BOARD_CONTENT LIKE '%' || ' " + searchValue + "' || '%') "; break;
case "writer" : condition = " MEMBER_ID LIKE '%' || '" + searchValue + "' || '%' "; break;
}
return condition;
}
/** 검색 게시글 목록 리스트 조회 Service
* @param map
* @param pInfo
* @return bList
* @throws Exception
*/
public List<Board> searchBoardList(Map<String, Object> map, PageInfo pInfo) throws Exception {
Connection conn = getConnection();
String condition = createCondition(map);
List<Board> bList = dao.searchBoardList(conn, pInfo, condition);
close(conn);
return bList;
}
}
전체 SearchDAO.java
package com.kh.wsp.search.model.dao;
import static com.kh.wsp.common.JDBCTemplate.*;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import com.kh.wsp.board.model.vo.Board;
import com.kh.wsp.board.model.vo.PageInfo;
public class SearchDAO {
private Statement stmt = null;
private PreparedStatement pstmt = null;
private ResultSet rset = null;
/** 조건을 만족하는 게시글 수 조회 DAO
* @param conn
* @param condition
* @return listCount
* @throws Exception
*/
public int getListCount(Connection conn, String condition) throws Exception {
int listCount = 0;
String query = "SELECT COUNT(*) FROM V_BOARD WHERE BOARD_STATUS = 'Y' AND " + condition;
try {
stmt = conn.createStatement();
rset = stmt.executeQuery(query);
if(rset.next()) {
listCount = rset.getInt(1);
}
}finally {
close(rset);
close(stmt);
}
return listCount;
}
/** 검색 게시글 목록 조회 DAO
* @param conn
* @param pInfo
* @param condition
* @return
* @throws Exception
*/
public List<Board> searchBoardList(Connection conn, PageInfo pInfo, String condition) throws Exception {
List<Board> bList = null;
String query = "SELECT * FROM" +
" (SELECT ROWNUM RNUM , V.*" +
" FROM" +
" (SELECT * FROM V_BOARD " +
" WHERE " + condition +
" AND BOARD_STATUS = 'Y' ORDER BY BOARD_NO DESC) V )" +
"WHERE RNUM BETWEEN ? AND ?";
try {
// SQL 구문 조건절에 대입할 변수 생성
int startRow = (pInfo.getCurrentPage()-1) * pInfo.getLimit() +1;
int endRow = startRow + pInfo.getLimit()-1;
// 500개의 글 중에서 1페이지에 해당하는 글을 가져옴 : 500~491번째 글만 가져오게 됨.
pstmt = conn.prepareStatement(query);
pstmt.setInt(1, startRow);
pstmt.setInt(2, endRow);
rset = pstmt.executeQuery();
bList = new ArrayList<Board>();
while(rset.next()) {
Board board = new Board(rset.getInt("BOARD_NO"),
rset.getString("BOARD_TITLE"), rset.getString("MEMBER_ID"),
rset.getInt("READ_COUNT"),
rset.getString("CATEGORY_NM"), rset.getTimestamp("BOARD_CREATE_DT"));
bList.add(board);
}
}finally {
close(rset);
close(pstmt);
}
return bList;
}
}
전체 boardInsert.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시판</title>
<style>
.insert-label {
display: inline-block;
width: 80px;
line-height: 40px
}
.boardImg{
cursor : pointer;
}
</style>
</head>
<body>
<jsp:include page="../common/header.jsp"></jsp:include>
<div class="container my-5">
<h3>게시글 등록</h3>
<hr>
<!-- 파일 업로드를 위한 라이브러리 cos.jar 라이브러리 다운로드(http://www.servlets.com/) -->
<!--
- enctype : form 태그 데이터가 서버로 제출 될 때 인코딩 되는 방법을 지정. (POST 방식일 때만 사용 가능)
- application/x-www-form-urlencoded : 모든 문자를 서버로 전송하기 전에 인코딩 (form태그 기본값)
- multipart/form-data : 모든 문자를 인코딩 하지 않음.(원본 데이터가 유지되어 이미지, 파일등을 서버로 전송 할 수 있음.)
-->
<form action="${contextPath }/board/insert.do" method="post"
enctype="multipart/form-data" role="form" onsubmit="return boardValidate();">
<div class="mb-2">
<label class="input-group-addon mr-3 insert-label">카테고리</label>
<select class="custom-select" id="categoryCode" name="categoryCode" style="width: 150px;">
<option value="10">운동</option>
<option value="20">영화</option>
<option value="30">음악</option>
<option value="40">요리</option>
<option value="50">게임</option>
<option value="60">기타</option>
</select>
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">제목</label>
<input type="text" class="form-control" id="boardTitle" name="boardTitle" size="70">
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">작성자</label>
<h5 class="my-0" id="writer">${loginMember.memberId }</h5>
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">작성일</label>
<h5 class="my-0" id="today"></h5>
</div>
<hr>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">썸네일</label>
<div class="boardImg" id="titleImgArea">
<img id="titleImg" width="200" height="200">
</div>
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">업로드<br>이미지</label>
<div class="mr-2 boardImg" id="contentImgArea1">
<img id="contentImg1" width="150" height="150">
</div>
<div class="mr-2 boardImg" id="contentImgArea2">
<img id="contentImg2" width="150" height="150">
</div>
<div class="mr-2 boardImg" id="contentImgArea3">
<img id="contentImg3" width="150" height="150">
</div>
</div>
<!-- 파일 업로드 하는 부분 -->
<div id="fileArea">
<input type="file" id="img0" name="img0" onchange="LoadImg(this,0)" multiple="multiple">
<!-- multiple 속성 = 사진 여러개 선택 가능 -->
<input type="file" id="img1" name="img1" onchange="LoadImg(this,1)">
<input type="file" id="img2" name="img2" onchange="LoadImg(this,2)">
<input type="file" id="img3" name="img3" onchange="LoadImg(this,3)">
</div>
<div class="form-group">
<div>
<label for="content">내용</label>
</div>
<textarea class="form-control" id="boardContent" name="boardContent" rows="15" style="resize: none;"></textarea>
</div>
<hr class="mb-4">
<div class="text-center">
<button type="submit" class="btn btn-primary">등록</button>
<button type="button" class="btn btn-primary">목록으로</button>
</div>
</form>
</div>
<jsp:include page="../common/footer.jsp"></jsp:include>
<script>
(function printToday(){
// 오늘 날짜 출력
var today = new Date();
var month = (today.getMonth()+1);
var date = today.getDate();
var str = today.getFullYear() + "-"
+ (month < 10 ? "0"+month : month) + "-"
+ (date < 10 ? "0"+date : date);
$("#today").html(str);
})();
// 유효성 검사
function boardValidate() {
if ($("#boardTitle").val().trim().length == 0) {
alert("제목을 입력해 주세요.");
$("#title").focus();
return false;
}
if ($("#content").val().trim().length == 0) {
alert("내용을 입력해 주세요.");
$("#content").focus();
return false;
}
}
// 이미지 영역을 클릭할 때 파일 첨부 창이 뜨도록 설정하는 함수
// 페이지 로딩이 끝나고나면 #fileArea 요소를 숨김.
$(function(){
$("#fileArea").hide();
$(".boardImg").on("click",function(){// 이미지 영역이 클릭 되었을 때
// 클릭한 이미지 영역 인덱스 얻어오기
var index = $(".boardImg").index(this);
// 클릭된 요소가 .boardImg 중 몇 번째 인덱스인지 반환
// console.log(index);
// 클릭된 영역 인덱스에 맞는 input file 태그 클릭
$("#img" + index).click();
});
});
// 각각의 영역에 파일을 첨부 했을 경우 미리 보기가 가능하도록 하는 함수
function LoadImg(value, num) {
// value.files == input태그에 파일이 업로드되어 있으면 true
// value.files[0] : 여러 파일 중 첫번째 파일이 업로드 되어있으면 true
if(value.files && value.files[0] ){ // 해당 요소에 업로드된 파일이 있을 경우
var reader = new FileReader();
// 자바스크립트 FileReader
// 웹 애플리케이션이 비동기적으로 데이터를 읽기 위하여
// 읽을 파일을 가리키는 File 혹은 Blob객체를 이용해 파일의 내용을 읽고
// 사용자의 컴퓨터에 저장하는 것을 가능하게 해주는 객체
reader.readAsDataURL(value.files[0]);
// FileReader.readAsDataURL()
// 지정된의 내용을 읽기 시작합니다.
// Blob완료되면 result속성 data:에 파일 데이터를 나타내는 URL이 포함 됨.
reader.onload = function(e){
// FileReader.onload
// load 이벤트의 핸들러.
// 이 이벤트는 읽기 동작이 성공적으로 완료 되었을 때마다 발생함.
// 읽어들인 내용(이미지 파일)을 화면에 출력
$(".boardImg").eq(num).children("img").attr("src", e.target.result);
// e.target.result : 이벤트가 발생한 요소의 결과 , 파일 읽기 동작을 성공한 요소가 읽어들인 파일 내용
}
}
}
</script>
</body>
</html>
공유하기
Twitter Google+ LinkedIn
댓글남기기