오잉 별것도 안했는데 벌써 7번째 포스팅이라니 이게 무슨 일이오
아무튼 이번에는 드디어ㅠㅠ 게시판을 붙여보는 시간이다.
되도록이면 한방에 포스팅을 정리하고 싶어서 페이징 처리까지 끝낸 뒤에 글을 쓰려다보니 조금 시간이 걸렸다.
참 신기하게도 분명 내가 예전에 써서 잘 돌아갔던 기능들인데 다시 이 프로젝트를 진행하면서 재활용하려니까 안 먹는 것들이 있다.
물론 내가 능력이 부족해서 개발환경 세팅이라던가 뭐 그런데서 실수했겠지 ㅠㅠ 하면서도 짜증나는 이 기분
덕분에 실무에서는 하루만에 개발 완료할 것을 엄청나게 질질 끌면서 개발했다 흑흑
1. 게시판용 테이블 생성
먼저 DB가 있어야 게시물을 읽고 쓸테니 DB 생성부터
CREATE TABLE `myhome`.`note` (
`sn` VARCHAR(45) NOT NULL,
`title` VARCHAR(100) NOT NULL,
`contents` VARCHAR(6000) NOT NULL,
`insert_dt` VARCHAR(45) NOT NULL,
`update_dt` VARCHAR(45) NULL,
`delete_at` VARCHAR(45) NOT NULL DEFAULT 'N;,
PRIMARY KEY (`sn`),
UNIQUE INDEX `sn_UNIQUE` (`sn` ASC));
글 삭제 시에는 물리 삭제를 하지 않고 논리 삭제만 하도록 'delete_at' 이라는 컬럼을 넣고 기본값은 'N'을 줬다.
글 작성일자, 수정일자는 데이터 변환 시에 오류가 발생하지 않도록 처리하기 귀찮으니 그냥 VARCHAR 타입으로 지정했다.
'sn' 이 글번호로써 이 테이블의 주요키 역할을 수행하게 되는데,
보통 트리거를 써서 처리하지만 나는 insert 할 때 select 서브쿼리를 써서 처리할 생각이므로 트리거는 따로 생성을 안 했다.
2. 패키지 및 소스 파일 생성
이제 구조를 잡아서 소스 파일들을 차례로 만들어줘야지!
처음에는 상세 게시물 조회, 수정, 신규 게시물 작성을 하나의 화면에서 모두 처리하고
insert와 update 역시 merge into 쿼리를 써서 한방에 처리하려고 했다.
그런데 원인을 알 수 없는 파라미터 처리 문제 ㅠㅠㅠ로 인해 마구 삽질하다가
그냥 나의 정신건강을 위해 기능별로 별도의 jsp 를 생성해서 구현함...머저리 같은 짓을 했다.
혹시라도 이 글을 보고 그대로 따라하시는 분이 있다면 부디 저보다 고차원적으로 구현하시길
일단 자바 소스가 들어갈 패키지 note를 하나 만들어 주고 그 안에 컨트롤러, 서비스, DAO, VO를 차곡차곡 넣었다.
common 폴더 밑에는 페이징 처리에 쓰일 VO를 더 만들어줬다.
실제로 내가 쓴 건 PagingVO.java 였고, PagingUtil.java 는 다른 분들이 공개해놓은 소스 공부하느라 만들어본 것...
그래서 자바 소스 패키지 구조는 아래와 같이 변동되었다.
쿼리를 작성해야 하니 note-mapper.xml 도 만들어서 매퍼 폴더에 넣어주고
js, css, jsp 파일도 게시판 용으로 전부 생성했다.
3. 개발 환경 수정
개발하던 중에 내가 jstl 설정을 제대로 안했다는 것을 깨달았다.
그래서 급하게 다시 검색.....머리가 나쁘면 손가락이 고생합니다.
pom.xml 을 열어서 아래 dependency를 추가해주고
<!-- taglibs -->
<dependency>
<groupId>taglibs</groupId>
<artifactId>datetime</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>string</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.1.2</version>
</dependency>
jstl.jar 와 standard.jar 를 다운 받아서 라이브러리에 추가한당
(다운로드 링크 → http://archive.apache.org/dist/jakarta/taglibs/standard/binaries/)
4. 화면 소스 작성
DB에서 가져온 게시물 목록을 뿌려줄 화면을 먼저 만들었다.
소스는 아래와 같다.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>My Home</title>
<script type="text/javascript" src="/home/resources/js/jquery-3.2.1.min.js"></script>
<script src="/home/resources/js/note/noteList.js"></script>
<!-- 부트스트랩 -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="/home/resources/css/noteList.css">
</head>
<body>
<jsp:include page="/WEB-INF/views/header.jsp" />
<div id="location" class="jumbotron">
<h2>MAKING NOTES</h2>
<h4> - How to make this website</h4>
</div>
<div id="main">
<div id="tablePanel" class="panel panel-default">
<div id="loading">
<img src="/home/resources/img/loader.gif" />
</div>
<table class="table">
<thead>
<tr id="head">
<th>#</th>
<th>Title</th>
<th>Date</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<br>
<c:if test="${admin ne null }">
<div id="btn">
<button id="toWrite" class="btn btn-default">Write</button>
</div>
</c:if>
<br>
<div id="paging">
<nav>
<ul class="pagination">
<li id="prevBtn" class="active"><a href="#" aria-label="Previous"><span aria-hidden="true">«</span></a></li>
<li style="display:none;"><a href="#">1</a></li>
<li style="display:none;"><a href="#">2</a></li>
<li style="display:none;"><a href="#">3</a></li>
<li style="display:none;"><a href="#">4</a></li>
<li style="display:none;"><a href="#">5</a></li>
<li id="nextBtn" class="active"><a href="#" aria-label="Next"><span aria-hidden="true">»</span></a></li>
</ul>
</nav>
</div>
</div>
<input type="hidden" id="curBlock" />
<input type="hidden" id="curPage" />
</body>
</html>
앞서 만들어둔 메뉴바가 있으니 그걸 그대로 가져와서 목록 조회 화면에도 넣어주고,
이 게시판의 글은 관리자만이 쓸 수 있게 할 예정이므로 c 태그를 써서 관리자일 경우에만 쓰기 버튼이 보이도록 처리했다.
div id="paging" 영역에 들어간 태그들과 가장 하단에 들어간 hidden 타입의 인풋 박스 두개가 페이징 처리에 필요하다.
jsp는 이 정도에서 구현을 끝내고 가볍게 css를 쪼금씩 수정
@charset "UTF-8";
#location {
font-weight : bold;
padding : 5%;
}
#main {
text-align : center;
}
#loadiing img{
z-index : 99999;
display : none;
position : absolute;
}
#tablePanel {
display : inline-block;
margin : 0 auto;
height : 410px;
width : 80%;
}
#tablePanel th {
text-align : center;
}
#tablePanel td:first-child, th:first-child{
width : 10%;
}
#tablePanel th:first-child + th, td:first-child + td {
text-align : left;
margin-left : 2em;
}
#tablePanel td:nth-child(3) {
width : 15%;
}
#paging {
display : inline-block;
margin : 0 auto;
}
아유 조잡해.....css 겁나게 못하는 사람의 소스가 바로 이런 것
검색하다보니 예전엔 몰랐던 신기한 셀렉터가 있어서 써봤다.
th:first-child + th 라고 선언하면 <th> 태그 중 두번째 순서에 있는 개체가 선택된다.
td:first-child + td 도 마찬가지로 2열을 선택한다.
css 만지는건 재밌지만 내가 잘 모르고 못하니까 금방 짜증이 난다.
더군다나 부트스트랩을 쓰니 살짝만 만져도 제법 괜찮은 화면이 나온다.
부트스트랩님은 천재시고 저는 똥멍충이 입니다.
이제 js 파일을 만들어준다.
$(function(){
getNoteList(1);
$("#toWrite").click(function(){
location.href="/home/note/writeNoteView";
});
//페이지 네비게이션 바 클릭 시
$("li").on("click", function(e){
var page = $(this).text().trim();
var curBlock = parseInt($("#curBlock").val());
var curPage = parseInt($("#curPage").val());
var prevClass = $("#prevBtn").hasClass("disabled");
var nextClass = $("#nextBtn").hasClass("disabled");
var mod = curPage % 5;
if(mod == 0) mod = 5;
if(page == "«" && prevClass != true){
page = curPage - mod;
}else if(page == "«" && prevClass == true){
return;
}else if(page == "»" && nextClass != true){
page = (curPage + 5 - mod) + 1;
}else if(page == "»" && nextClass == true){
return;
}
getNoteList(page);
});
});
//테이블 목록 생성
function getNoteList(page){
var param = {"curPage" : page};
$.ajax({
url : "getNoteList",
type : "get",
data : param,
dataType : "json",
beforeSend : function(){
$("#loading img").css("display", "inline-block");
},
success : function(data){
console.log("getNoteList : success");
var result = "";
if(data.length > 0){
for(var i in data){
result += "<tr><td>" + data[i].sn + "</td>";
result += "<td><a href='readNote?sn=" + data[i].sn + "'>" + data[i].title + "</a></td>";
result += "<td>" + data[i].insert_dt + "</td></tr>";
}
}else{
result = "<tr><td colspan='3'>불러올 글이 없습니다.</td></tr>";
}
$("tbody").html(result);
},
error : function(e){
console.log("getNoteList :error");
},
complete : function(e){
$("#loading img").css("display", "none");
setPagingNav(page);
}
});
}
//하단 페이징 네비게이션 세팅
function setPagingNav(page){
var param = {"curPage" : page};
$.ajax({
url : "setPaging",
type : "get",
data : param,
dataType : "json",
success : function(data){
console.log("setPaging : success");
$("#curBlock").val(data.curBlock);
$("#curPage").val(data.curPage);
var index = data.lastPage - data.firstPage + 1;
for(var i = 1; i < 6; i++) {
var pageNo = data.firstPage;
$("li:eq(" + i + ")").removeClass("active");
for(var j = 1; j <= index; j++){
if(i == j) {
$("li:eq(" + j + ")").attr("style", "dislpay:inline-block");
$("li:eq(" + j + ")").html('<a href="#">' + pageNo + '</a>');
}
if(data.curPage == $("li:eq(" + j + ")").text().trim())
$("li:eq(" + j + ")").attr("class", "active");
++pageNo;
}
}
if(data.curBlock <= 1) $("#prevBtn").attr("class", "disabled");
if(data.totalBlockNum <= data.curBlock) $("#nextBtn").attr("class", "disabled");
},
error : function(e){
console.log("setPaging :error");
}
});
}
ㅠㅠㅠㅠ 내가 상상하던 js 소스는 이게 아니었는데....
내 예상대로라면 잘 돌아갔어야 하는 기능들이 갑자기 안 먹다보니 짜증 나서
계획 없이 마구잡이로 고치기 시작했더니 이렇게 돌아올 수 없는 강을 건넌 소스가 나왔다.
처음 화면에 들어와서 문서를 읽고 나면 바로 getNoteList 함수를 실행한다.
이때 어차피 1페이지를 읽게 될 것이므로 그냥 파라미터도 1을 써서 바로 보내버렸다.
자세한 설명은 아래에
//테이블 목록 생성
function getNoteList(page){
var param = {"curPage" : page}; //자바 컨트롤러에 보낼 파라미터를 세팅해줌. 파라미터의 이름은 "curPage" 로 정해줬다.
//page는 선택한 페이지 번호 값으로, 처음에는 무조건 1페이지가 보이도록 1을 던져줌
$.ajax({
url : "getNoteList",
type : "get",
data : param,
dataType : "json",
beforeSend : function(){
$("#loading img").css("display", "inline-block"); //데이터를 불러오는 동안 화면에는 로딩 마스크를 띄워놓고
},
success : function(data){
console.log("getNoteList : success");
var result = "";
//데이터 로딩에 성공하면 for in 문을 돌리면서 테이블에 붙일 하위 태그들을 만들어주고
if(data.length > 0){
for(var i in data){
result += "<tr><td>" + data[i].sn + "</td>";
result += "<td><a href='readNote?sn=" + data[i].sn + "'>" + data[i].title + "</a></td>";
result += "<td>" + data[i].insert_dt + "</td></tr>";
}
}else{
result = "<tr><td colspan='3'>불러올 글이 없습니다.</td></tr>";
}
$("tbody").html(result); //결과물을 tbody에 넣는다
},
error : function(e){
console.log("getNoteList :error");
},
complete : function(e){
$("#loading img").css("display", "none"); //처리가 종료되면 로딩 마스크는 다시 안 보이게 고이 접어주고
setPagingNav(page); //페이징 처리하는 함수를 실행시킴
}
});
}
만약 ajax의 리턴값으로 List 형태의 게시물 목록을 받게 구현하지 않고
model.addAttribute() 를 써서 컨트롤러에서 화면단으로 바로 게시물 목록과 페이징 객체를 추가하여 보낸다면 이렇게 안 짜도 될텐데
굳이~~~ajax의 리턴값으로 게시물 목록과 페이징 객체를 따로 보내다보니 이런 식으로 구현이 되었다.
이제 페이징 처리 함수를 자세히 보면 아래와 같다.
//하단 페이징 네비게이션 세팅
function setPagingNav(page){
var param = {"curPage" : page}; //위와 마찬가지로 선택한 페이지 번호를 파라미터로 세팅
$.ajax({
url : "setPaging",
type : "get",
data : param,
dataType : "json",
success : function(data){
console.log("setPaging : success");
$("#curBlock").val(data.curBlock); //추후 페이징 처리를 위해 hidden 속성의 인풋 박스에 값 세팅
$("#curPage").val(data.curPage);
var index = data.lastPage - data.firstPage + 1; //한 화면에 5개의 페이지만 보이도록 페이징 객체를 설정해둔 상태이나,
//페이지 갯수가 5개 미만일 수 있으므로 index 변수를 생성
//기본적인 페이징 처리 태그는 << 1 2 3 4 5 >> 와 같이 보이도록 jsp에 구현해둠
//따라서 <<, >> 버튼을 제외한 나머지 목록태그에 대해서만 반복문이 돌도록 설정
for(var i = 1; i < 6; i++) {
var pageNo = data.firstPage; //가장 첫 페이지부터 시작하도록 pageNo 변수에 데이터값 담아줌
$("li:eq(" + i + ")").removeClass("active"); //우선 페이징 처리용 목록 태그 전체에서 active 클래스 제거
//실제 페이지 갯수만큼 반복문이 돌도록 설정
for(var j = 1; j <= index; j++){
//페이지 블록의 순서와 실제 데이터의 페이지 번호 순서가 일치할 경우
//(예: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 와 같은 모양으로 나열되므로 두번째 블록에 7번 페이지가 오도록)
if(i == j) {
$("li:eq(" + j + ")").attr("style", "dislpay:inline-block"); //해당 페이지 블록이 보이도록 스타일 적용
$("li:eq(" + j + ")").html('<a href="#">' + pageNo + '</a>'); //해당 페이지 블록의 번호 역시 변경
}
//현재 페이지가 어떤 것인지 표시할 수 있도록 현재 페이지 번호가 들어간 블록에 active 클래스를 적용
if(data.curPage == $("li:eq(" + j + ")").text().trim())
$("li:eq(" + j + ")").attr("class", "active");
++pageNo; //페이지 번호를 증가시켜줌
}
}
//첫번째 페이지블록에 있다면 더이상 불러올 이전 페이지가 없으므로 << 버튼 disabled
if(data.curBlock <= 1) $("#prevBtn").attr("class", "disabled");
//마지막 페이지블록에 있다면 더이상 불러올 다음 페이지가 없으므로 >> 버튼 disabled
if(data.totalBlockNum <= data.curBlock) $("#nextBtn").attr("class", "disabled");
},
error : function(e){
console.log("setPaging :error");
}
});
}
그리고 별도의 펑션을 만들어서 뺄까 말까 고민하다가 그냥 냅둔 부분인데
페이지 블록 클릭 시의 기능은 아래와 같이 처리했다....
//페이지 네비게이션 바 클릭 시
$("li").on("click", function(e){
var page = $(this).text().trim(); //선택한 페이지 블록의 번호를 가져옴
var curBlock = parseInt($("#curBlock").val()); //히든값으로 박아넣은 현재블록번호 순서값을 가져옴
var curPage = parseInt($("#curPage").val()); //히든값으로 박아넣은 현재 페이지 번호값을 가져옴
var prevClass = $("#prevBtn").hasClass("disabled"); //<<버튼에 disabled가 적용된 상태인지 boolean 값으로 가져와서 변수에 저장
var nextClass = $("#nextBtn").hasClass("disabled"); //>>버튼에 disabled가 적용된 상태인지 boolean 값으로 가져와서 변수에 저장
var mod = curPage % 5; //한번에 다섯개의 페이지블록을 생성하므로 현재 페이지 번호를 5로 나눈 나머지값을 가지고 계산
if(mod == 0) mod = 5; //만약 현재 페이지가 5, 10, 15 와 같이 5의 배수라면 현재 화면에서 가장 마지막 페이지일 것임
//따라서 나머지값이 0이라면 해당 변수를 5로 다시 세팅해줌
//예를 들어 현재 페이지가 7이라면 나머지값은 2, 현재 블록순서는 2일 것임
if(page == "«" && prevClass != true){
page = curPage - mod; //<<버튼 클릭 시 7 - 2 = 5. 조회할 페이지값을 5로 세팅
}else if(page == "«" && prevClass == true){
return; //더이상 조회할 이전 페이지가 없는 상태라면 더 이상 진행하지 않고 리턴
}else if(page == "»" && nextClass != true){
page = (curPage + 5 - mod) + 1; //>>버튼 클릭 시 7 + 5 - 2 + 1 = 11. 조회할 페이지값을 11로 세팅
}else if(page == "»" && nextClass == true){
return; //더이상 조회할 다음 페이지가 없는 상태라면 더 진행하지 않고 리턴
}
getNoteList(page); //조회할 페이지 값을 가지고 getNoteList 함수 실행
});
자바 소스는 아래와 같다.
처음에 생각했던 로직을 그대로 구현할 수 없게 되면서 이상하게 방향을 틀다보니 이렇게 비효율적인 소스가 등장했다...ㅠㅠㅠㅠ
너무 아쉽지만 리팩토링을 하기에는 내가 너무 지쳐섴ㅋㅋㅋ그리고 이제 편하게 이런거 짤 시간이 없어서 그냥 마무리했음ㅠㅠ
package com.blog.home.note.ctr;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.blog.home.note.svc.NoteSvc;
import com.blog.home.note.vo.NoteVO;
import com.mysql.jdbc.StringUtils;
import common.vo.PagingVO;
@RequestMapping("/note")
@Controller("noteCtr")
public class NoteCtr {
@Autowired
private NoteSvc svc;
@RequestMapping("/noteListView")
public String noteListView() {
return "/note/noteList";
}
//페이징
@RequestMapping("/setPaging")
public @ResponseBody PagingVO setPagingVO(@RequestParam Map<String, Integer> param) {
Object obj = param.get("curPage");
int page = Integer.valueOf((String) obj);
int totalCount = svc.getTotalCount();
PagingVO vo = new PagingVO(totalCount, page);
return vo;
}
//게시물 목록
@RequestMapping("/getNoteList")
public @ResponseBody List<NoteVO> getNoteList(@RequestParam Map<String, Integer> param) throws Exception {
PagingVO vo = this.setPagingVO(param);
List<NoteVO> list = svc.getNoteList(vo);
return list;
}
}
package com.blog.home.note.vo;
import java.io.Serializable;
public class NoteVO implements Serializable {
private String sn;
private String title;
private String contents;
private String insert_dt;
private String update_dt;
private String delete_at;
public String getSn() {
return sn;
}
public void setSn(String sn) {
this.sn = sn;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContents() {
return contents;
}
public void setContents(String contents) {
this.contents = contents;
}
public String getInsert_dt() {
return insert_dt;
}
public void setInsert_dt(String insert_dt) {
this.insert_dt = insert_dt;
}
public String getUpdate_dt() {
return update_dt;
}
public void setUpdate_dt(String update_dt) {
this.update_dt = update_dt;
}
public String getDelete_at() {
return delete_at;
}
public void setDelete_at(String delete_at) {
this.delete_at = delete_at;
}
public NoteVO() {}
public NoteVO(String sn, String title, String contents, String insert_dt, String update_dt, String delete_at) {
super();
this.sn = sn;
this.title = title;
this.contents = contents;
this.insert_dt = insert_dt;
this.update_dt = update_dt;
this.delete_at = delete_at;
}
}
서비스, DAO는 특별히 다른 처리를 하는 게 없어서 패스...
쿼리는 아래와 같이 썼다.
<!-- 총 게시물 수 조회 -->
<select id="getTotalCount" resultType="int">
SELECT COUNT(*)
FROM NOTE
WHERE DELETE_AT = 'N'
</select>
<!-- 게시물 조회 -->
<select id="getNoteList" parameterType="paging" resultType="note">
<![CDATA[
SELECT TB.SN,
TB.TITLE,
TB.INSERT_DT
FROM (SELECT SN,
TITLE,
SUBSTRING(INSERT_DT, 1, 10) AS INSERT_DT,
ROW_NUMBER() OVER() AS ROWNO
FROM NOTE
WHERE DELETE_AT = 'N'
ORDER BY SN ASC) AS TB
WHERE TB.ROWNO >= #{firstNote}
AND TB.ROWNO <= #{lastNote}
]]>
</select>
해당 페이지의 첫번째 게시글 번호와 마지막 게시글 번호는 PagingVO 객체 생성 자 안에서 계산해서 만들도록 세팅해뒀다.
너무 길어지니 페이징 객체와 페이징 처리 로직은 다음 글에 이어서.....
완성된 게시판의 모습은 이렇다