[AJAX + SPA게시판 클라이언트단 글 읽기, 글 수정 추가, 글 작성, 글 삭제 수정]
1. index.html
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath }/CSS/common.css"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://kit.fontawesome.com/a076d05399.js"></script>
<script src="${pageContext.request.contextPath }/JS/board.js"></script>
<title>SPA 게시판</title>
</head>
<body>
<h2>게시판 - SPA</h2>
<%-- 글목록 --%>
<div id="list">
<div class="d01">
<div class="left" id="pageinfo"></div>
<div class="right" id="pageRows"></div>
</div>
<div class="clear"></div>
<form id="frmList" name="frmList">
<table>
<thead>
<th>#</th>
<th>UID</th>
<th>제목</th>
<th>작성자</th>
<th>조회수</th>
<th>작성일</th>
</thead>
<tbody>
</tbody>
</table>
</form>
<%--버튼 --%>
<div class="d01">
<div class="left">
<button type="button" id="btnDel" class="btn danger">글삭제</button>
</div>
<div class="right">
<button type="button" id="btnWrite" class="btn success">글작성</button>
</div>
</div>
</div>
<br>
<%-- 페이징 --%>
<div class="center">
<ul class="pagination" id="pagination">
</ul>
</div>
<%-- 글작성/보기/수정 대화상자 --%>
<div id="dlg_write" class="modal">
<form class="modal-content animate" id="frmWrite" name="frmWrite" method="post">
<div class="container">
<h3 class="title">새글 작성</h3>
<span class="close" title="Close Modal">×</span>
<input type="hidden" name="uid"> <%-- 삭제나 수정 위해 필요 --%>
<div class="d01 btn_group_header">
<div class="left">
<p id="viewcnt"></p>
</div>
<div class="right">
<p id="regdate"></p>
</div>
<div class="clear"></div>
</div>
<label for="subject"><b>글제목</b></label>
<input type="text" placeholder="글제목(필수)" name="subject" required>
<label for="name"><b>작성자</b></label>
<input type="text" placeholder="작성자(필수)" name="name" required>
<label for="content"><b>내용</b></label>
<textarea placeholder="글내용" name="content"></textarea>
<div class="d01 btn_group_write">
<button type="submit" class="btn success">작성</button>
</div>
<div class="d01 btn_group_view">
<div class="left">
<button type='button' class="btn danger" id="viewDelete">삭제</button>
</div>
<div class="right">
<button type='button' class="btn info" id="viewUpdate">수정</button>
</div>
<div class="clear"></div>
</div>
<div class="d01 btn_group_update">
<div>
<button type="button" class="btn info fullbtn" id="updateOk">수정완료</button>
</div>
</div>
</div>
</form>
</div>
</body>
</html>
2. board.js
var page = 1 // 현재 페이지
var pageRows = 10 // 한 페이지에 보여지는 게시글 개수
$(document).ready(function(){
// 게시판 목록 1페이지 로딩
loadPage(page);
// 글작성 버튼 누르면 팝업
$("#btnWrite").click(function() {
setPopup("write");
$("#dlg_write").show();
});
// 모달 대화상자에서 close 버튼 누르면 닫기
$(".modal .close").click(function(){
$(this).parents(".modal").hide();
});
// 글 작성 submit 되면
$("#frmWrite").submit(function(){
$(this).parents(".modal").hide();
return chkWrite();
});
// 글 삭제 버튼 누르면
$("#btnDel").click(function(){
chkDelete();
});
// 글 읽기(view) 대화상자에서 삭제 버튼 누르면 해당 글(uid) 삭제 진행
$("#viewDelete").click(function(){
var uid = viewItem.uid;
if(deleteUid(uid)){ // 해당 글 삭제
$(this).parents(".modal").hide(); // 삭제 성공하면 대화상자 닫기
}
});
// 글 읽기(view) 대화상자에서 수정버튼 누르면
$("#viewUpdate").click(function() {
setPopup("update");
});
// 글 수정 완료 버튼 누르면
$("#updateOk").click(function(){
chkUpdate();
});
});
// page 번째 페이지 로딩
function loadPage(page){
$.ajax({
url : "list.ajax?page=" + page + "&pageRows=" + pageRows
, type : "GET"
, cache : false
, success : function(data, status){
if(status == "success"){
//alert("AJAX 성공: 받아쮸~");
if(updateList(data)){
// 업데이트된 list에 필요한 이벤트 가동
addViewEvent();
// ★ 만약 이 코드를 $(document).ready()에 두면 동작 안할 것이다!! ★
}
}
}
});
} // end loadPage()
//
function updateList(jsonObj){
result = "";
if(jsonObj.status == "OK"){
var count = jsonObj.count;
// 전역변수 업데이트!
window.page = jsonObj.page;
window.pageRows = jsonObj.pagerows;
var i;
var items = jsonObj.data; // 배열
for(i = 0; i < count; i++){
result += "<tr>\n";
result += "<td><input type='checkbox' name='uid' value='" + items[i].uid + "'></td>\n";
result += "<td>" + items[i].uid + "</td>\n";
result += "<td><span class='subject' data-uid='" + items[i].uid + "'>" + items[i].subject + "</span></td>\n";
result += "<td>" + items[i].name + "</td>\n";
result += "<td><span data-viewCnt='" + items[i].uid + "'>" + items[i].viewcnt + "</span></td>\n";
result += "<td>" + items[i].regdate + "</td>\n";
result += "</tr>\n";
} // end for
$("#list tbody").html(result); // 테이블 업데이트!
// 페이지 정보 업데이트
$("#pageinfo").text(jsonObj.page + "/" + jsonObj.totalpage + "페이지, " + jsonObj.totalcnt + "개의 글");
// pageRows
var txt = "<select id='rows' onchange='changePageRows()'>\n";
txt += "<option " + ((window.pageRows == 10)?"selected":"") + " value='10'>10개씩</option>\n";
txt += "<option " + ((window.pageRows == 20)?"selected":"") + " value='20'>20개씩</option>\n";
txt += "<option " + ((window.pageRows == 50)?"selected":"") + " value='50'>50개씩</option>\n";
txt += "<option " + ((window.pageRows == 100)?"selected":"") + " value='100'>100개씩</option>\n";
txt += "</select>\n";
$("#pageRows").html(txt);
// 페이징 업데이트
var pagination = buildPagination(jsonObj.writepages, jsonObj.totalpage, jsonObj.page, jsonObj.pagerows);
$("#pagination").html(pagination);
return true;
} else {
alert(jsonObj.message);
return false;
}
return false;
} // end updateList()
function buildPagination(writePages, totalPage, curPage, pageRows){
var str = ""; // 최종적으로 페이징에 나타날 HTML 문자열 <li> 태그로 구성
// 페이징에 보여질 숫자들 (시작숫자 start_page ~ 끝숫자 end_page)
var start_page = ( (parseInt( (curPage - 1 ) / writePages ) ) * writePages ) + 1;
var end_page = start_page + writePages - 1;
if (end_page >= totalPage){
end_page = totalPage;
}
//■ << 표시 여부
if(curPage > 1){
str += "<li><a onclick='loadPage(1)' class='tooltip-top' title='처음'><i class='fas fa-angle-double-left'></i></a></li>\n";
}
//■ < 표시 여부
if (start_page > 1)
str += "<li><a onclick='loadPage(" + (start_page -1) + ")' class='tooltip-top' title='이전'><i class='fas fa-angle-left'></i></a></li>\n";
//■ 페이징 안의 '숫자' 표시
if (totalPage > 1) {
for (var k = start_page; k <= end_page; k++) {
if (curPage != k)
str += "<li><a onclick='loadPage(" + k + ")'>" + k + "</a></li>\n";
else
str += "<li><a class='active tooltip-top' title='현재페이지'>" + k + "</a></li>\n";
}
}
//■ > 표시
if (totalPage > end_page){
str += "<li><a onclick='loadPage("+ (end_page + 1 ) + ")' class='tooltip-top' title='다음'><i class='fas fa-angle-right'></i></a></li>\n";
}
//■ >> 표시
if (curPage < totalPage) {
str += "<li><a onclick='(" + totalPage + ")' class='tooltip-top' title='맨끝'><i class='fas fa-angle-double-right'></i></a></li>\n";
}
return str;
} // end buildPagination()
function changePageRows(){
window.pageRows = $("#rows").val();
loadPage(window.page);
}
// 새글 등록 처리
function chkWrite() {
var data = $("#frmWrite").serialize(); // 해당 폼 안의 name이 있는 것들을 끌어 들어옴
// 리턴값은 Object
//alert(data + "--" + typeof data);
//subject=aaa&name=bbb&content=ccc--string
// ajax request
$.ajax({
url : "writeOk.ajax",
type : "POST",
cache : false,
data : data, // POST 로 ajax request 하는 경우 parameter 담기
success : function(data, status){
if(status == "success"){
if(data.status = "OK") {
alert("INSERT 성공" + data.count + "개 : " + data.status);
loadPage(1); // 첫 페이지 리로딩
} else {
alert("INSERT 실패" + data.status + " : " + data.message);
}
}
}
});
// request 후, form 에 입력된것 reset()
$("#frmWrite")[0].reset();
// type이 submit일때 비록 action이 없더라도
// 자기 페이지로 리로딩된다...!!
// 페이지 리로딩을 원치 않는다면 리턴 값을 false 값을 주면 된다!
return false; // 페이지 리로딩은 안 할 것이다
} // end chkWrite();
// check된 uid의 게시글들만 삭제하기
function chkDelete() {
var uids = []; // 빈 배열 준비
// .each는 $("#list tbody input[name=uid") 각각에
// each function()을 실행시키라는 의미!
$("#list tbody input[name=uid]").each(function() {
//$(this)는 checkbox
if($(this).is(":checked")) { // jQuery에서 check 여부 확인 방법
uids.push($(this).val()); // 배열에 uid 값 추가
}
});
//alert(uids);
if(uids.length == 0) {
alert("삭제할 글을 체크해 주세요");
} else {
if(!confirm(uids.length + "개의 글을 삭제하시겠습니까?")) return false;
var data = $("#frmList").serialize();
// uid=1010&uid=1011&uid=1012
$.ajax({
url : "deleteOk.ajax",
type : "POST",
data : data,
cache : false,
success : function(data, status){
if(status == "success"){
if(data.status == "OK"){
alert("DELETE 성공 " + data.count + "개");
// 현재 페이지 리로딩
loadPage(window.page);
} else {
alert("DELETE 실패 " + data.message);
}
}
}
});
}
} // end chkDelete()
// 현재 글 목록 list에 대해 이벤트 등록
// - 제목(subject) 클릭하면 view 팝업 화면 뜰 수 있게 하기
function addViewEvent() {
$("#list .subject").click(function(){
// data-uid 는 내가 만튼 커스텀 애트리뷰트 데이터임
//alert($(this).text() + " : " + $(this).attr('data-uid'));
// 읽어오기
$.ajax({
url : "view.ajax?uid=" + $(this).attr('data-uid'),
type : "GET",
cache : false,
success : function(data, status) {
if(status = "successs") {
if(data.status == "OK") {
// 읽어온 view 데이터를 전역변수에 세팅
viewItem = data.data[0];
// 팝업에 보여주기
setPopup("view");
$("#dlg_write").show();
// 리스트상의 조회수 증가시키기
$("#list [data-viewcnt='" + viewItem.uid + "']").text(viewItem.viewcnt);
} else {
alert("VIEW 실패" + data.message);
}
}
}
});
});
} // end addViewEvent()
// 대화상자 셋업
function setPopup(mode) {
// 글작성
if(mode == 'write'){
$('#frmWrite')[0].reset(); // form 안의 기존 내용 reset
$("#dlg_write .title").text("새글 작성");
$("#dlg_write .btn_group_header").hide();
$("#dlg_write .btn_group_write").show();
$("#dlg_write .btn_group_view").hide();
$("#dlg_write .btn_group_update").hide();
$("#dlg_write input[name='subject']").attr("readonly", false);
$("#dlg_write input[name='subject']").css("border", "1px solid #ccc");
$("#dlg_write input[name='name']").attr("readonly", false);
$("#dlg_write input[name='name']").css("border", "1px solid #ccc");
$("#dlg_write textarea[name='content']").attr("readonly", false);
$("#dlg_write textarea[name='content']").css("border", "1px solid #ccc");
}
// 글읽기
if(mode == 'view'){
$("#dlg_write .title").text("글 읽기");
$("#dlg_write .btn_group_header").show();
$("#dlg_write .btn_group_write").hide();
$("#dlg_write .btn_group_view").show();
$("#dlg_write .btn_group_update").hide();
$("#dlg_write #viewcnt").text("#" + viewItem.uid + " - 조회수: " + viewItem.viewcnt);
$("#dlg_write #regdate").text(viewItem.regdate);
$("#dlg_write input[name='uid']").val(viewItem.uid); // 나중에 삭제/수정을 위해 필요
$("#dlg_write input[name='subject']").val(viewItem.subject);
$("#dlg_write input[name='subject']").attr("readonly", true);
$("#dlg_write input[name='subject']").css("border", "none");
$("#dlg_write input[name='name']").val(viewItem.name);
$("#dlg_write input[name='name']").attr("readonly", true);
$("#dlg_write input[name='name']").css("border", "none");
$("#dlg_write textarea[name='content']").val(viewItem.content);
$("#dlg_write textarea[name='content']").attr("readonly", true);
$("#dlg_write textarea[name='content']").css("border", "none");
}
// 글수정
if(mode == "update"){
$("#dlg_write .title").text("글 수정");
$("#dlg_write .btn_group_header").show();
$("#dlg_write .btn_group_write").hide();
$("#dlg_write .btn_group_view").hide();
$("#dlg_write .btn_group_update").show();
$("#dlg_write input[name='subject']").attr("readonly", false);
$("#dlg_write input[name='subject']").css("border", "1px solid #ccc");
$("#dlg_write input[name='name']").attr("readonly", true);
$("#dlg_write textarea[name='content']").attr("readonly", false);
$("#dlg_write textarea[name='content']").css("border", "1px solid #ccc");
}
} // end setPopup()
// 특정 uid 의 글 삭제하기
function deleteUid(uid) {
if(!confirm(uid + "글을 삭제하시겠습니까?")) {return false;}
// POST 방식
$.ajax({
url : "deleteOk.ajax",
type : "POST",
data : "uid=" + uid,
cache : false,
success : function(data, status) {
if(status = "success") {
if(data.status = "OK") {
alert('DELETE 성공' + data.count + "개");
loadPage(window.page); // 현재 페이지 리로딩
// 콕 찝어서 전역변수임을 알려주고 싶을때는 window.를 앞에 명시해주면 된다
} else {
alert("DELETE 실패 " + data.message);
return false;
}
}
}
});
return true;
} // end deleteUid(uid)
// 글 수정
function chkUpdate() {
// serialize의 리턴은 String -> ex) name=aaa&subject=bbb&content=ccc&uid=1230
var data = $("#frmWrite").serialize();
$.ajax({
url : "updateOk.ajax",
type : "POST",
cache : false,
data : data,
success : function(data, status){
if(status == "success"){
if(data.status == "OK"){
alert("UPDATE 성공 " + data.count + "개:" + data.status);
loadPage(window.page); // 현재 페이지 리로딩
} else {
alert("UPDATE 실패 " + data.status + " : " + data.message);
}
$("#dlg_write").hide(); // 현재 팝업 닫기
}
}
});
} // end chkUpdate()
3. common.css
@charset "UTF-8";
/* 기본 버튼 */
.btn {
border: none;
color: white;
padding: 14px 28px;
font-size: 16px;
cursor: pointer;
}
.success {background-color: #4CAF50;} /* Green */
.success:hover {background-color: #46a049;}
.info {background-color: #2196F3;} /* Blue */
.info:hover {background: #0b7dda;}
.warning {background-color: #ff9800;} /* Orange */
.warning:hover {background: #e68a00;}
.danger {background-color: #f44336;} /* Red */
.danger:hover {background: #da190b;}
.default {background-color: #e7e7e7; color: black;} /* Gray */
.default:hover {background: #ddd;}
/* 글 목록 */
#list table {width: 100%;}
#list table, #list th, #list td {
border: 1px solid black;
border-collapse: collapse;
}
#list th, #list td {
padding: 10px;
}
#list .subject:hover { /* 글 제목 위에 커서를 올린 경우*/
text-decoration: underline;
color: orange;
cursor: pointer;
}
.clear { clear: both; }
.left {
float: left;
}
.right {
float: right;
}
/* 페이징 */
.center {
text-align: center;
}
ul.pagination{
list-style-type:none
}
ul.pagination li{
display: inline-block;
}
ul.pagination a {
color: black;
float: left;
padding: 4px 8px;
text-decoration: none;
transition: background-color .3s;
/* border: 1px solid #ddd; */
/* margin: 0 4px; */
margin: 0px;
}
ul.pagination a.active {
background-color: #4CAF50;
color: white;
border: 1px solid #4CAF50;
}
ul.pagination a:hover:not(.active) {background-color: #ddd;}
/* 버튼 그룹 */
.d01 {
margin: 5px 0;
}
/* 모달 팝업 */
/* 모달 팝업 */
.modal { /* 모달 전체 적용 */
background-color: rgba(0, 0, 0, 0.4);
width: 100%;
height: 100%;
position : fixed;
top: 0;
left: 0;
z-index: 1;
padding-top: 40px; /* 내부여백 */
overflow: auto;
display : none; /* 기본적으로 안보이기 */
}
.modal .modal-content {
background-color: #fefefe; /* 배경은 흰색 */
width: 80%; /* 화면대비 80% */
margin: 5% auto 15% auto; /* 위에서 5%, 아래에서 15%, 좌우 중앙정렬 */
border: 1px solid #888; /* 테두리 */
}
.modal .container {
padding: 16px;
position: relative; /* 이래야 안에 있는 absolute 들이 동작 */
}
.modal .close { /* close 버튼 */
font-size: 35px;
font-weight: bold;
color: #000;
position: absolute;
right: 25px;
top: 0px;
}
.modal .close:hover,
.modal .cloas:focus {
color: red;
cursor: pointer;
}
.modal input[type=text] {
width: 100%;
border: 1px solid #ccc;
margin: 8px 0;
padding: 12px 20px;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
}
.modal textarea {
width: 100%;
margin: 8px 0;
}
.modal .fullbtn {
width: 100%;
cursor: pointer;
}
[AJAX와 JSON을 이용하여 다단계 Category 구현]
1. ERD > test_category.erm, test_category.sql
DROP TABLE test_category CASCADE CONSTRAINTS;
CREATE TABLE test_category
(
ca_uid number NOT NULL,
ca_name varchar2(30) NOT NULL,
ca_depth number DEFAULT 1,
ca_parent number,
ca_order number DEFAULT 1,
PRIMARY KEY (ca_uid)
);
-- 시퀀스
DROP SEQUENCE test_category_seq;
CREATE SEQUENCE test_category_seq;
-- 샘플 데이터 name, depth, parent, order
INSERT INTO test_category VALUES(test_category_seq.nextval, '의류', 1, null, 1);
INSERT INTO test_category VALUES(test_category_seq.nextval, '식품', 1, null, 3);
INSERT INTO test_category VALUES(test_category_seq.nextval, '스포츠', 1, null, 2);
-- [1.의류][xxx]
INSERT INTO test_category VALUES(test_category_seq.nextval, '남성복', 2, 1, 3);
INSERT INTO test_category VALUES(test_category_seq.nextval, '여성복', 2, 1, 1);
INSERT INTO test_category VALUES(test_category_seq.nextval, '아동복', 2, 1, 2);
-- [1.의류][4.남성복][xxx]
INSERT INTO test_category VALUES(test_category_seq.nextval, '양복', 3, 4, 3);
INSERT INTO test_category VALUES(test_category_seq.nextval, '조끼', 3, 4, 1);
INSERT INTO test_category VALUES(test_category_seq.nextval, '점퍼', 3, 4, 2);
-- [1.의류][5.여성복][xxx]
INSERT INTO test_category VALUES(test_category_seq.nextval, '가디건', 3, 5, 3);
INSERT INTO test_category VALUES(test_category_seq.nextval, '블라우스', 3, 5, 2);
INSERT INTO test_category VALUES(test_category_seq.nextval, '원피스', 3, 5, 1);
-- [1.의류][6. 아동복][xxx]
INSERT INTO test_category VALUES(test_category_seq.nextval, '운동복', 3, 6, 3);
INSERT INTO test_category VALUES(test_category_seq.nextval, '교복', 3, 6, 2);
INSERT INTO test_category VALUES(test_category_seq.nextval, '저고리', 3, 6, 1);
-- [2.식품][xxx]
INSERT INTO test_category VALUES(test_category_seq.nextval, '반찬', 2, 2, 3);
INSERT INTO test_category VALUES(test_category_seq.nextval, '인스턴트', 2, 2, 1);
-- [2.식품][16.반찬][xxx]
INSERT INTO test_category VALUES(test_category_seq.nextval, '김치', 3, 16, 1);
INSERT INTO test_category VALUES(test_category_seq.nextval, '명란젓', 3, 16, 2);
-- [2.식품][17.인스턴트][xxx]
INSERT INTO test_category VALUES(test_category_seq.nextval, '라면', 3, 17, 1);
INSERT INTO test_category VALUES(test_category_seq.nextval, '과자', 3, 17, 4);
INSERT INTO test_category VALUES(test_category_seq.nextval, '빵', 3, 17, 3);
INSERT INTO test_category VALUES(test_category_seq.nextval, '피자', 3, 17, 2);
-- [3.스포츠][xxx]
INSERT INTO test_category VALUES(test_category_seq.nextval, '야구', 2, 3, 1);
-- [3.스포츠][24. 야구]
INSERT INTO test_category VALUES(test_category_seq.nextval, '야구배트', 3, 24, 1);
INSERT INTO test_category VALUES(test_category_seq.nextval, '글러브', 3, 24, 2);
SELECT * FROM test_category;
-- depth 2 이면서 부모 uid 가 1 인 카테고리 읽기 (order 오름차순 순서로)
SELECT ca_uid, ca_name name, ca_depth, ca_parent, ca_order
FROM test_category
WHERE ca_depth = 1 AND ca_parent IS NULL
ORDER BY ca_order ASC;
SELECT *
FROM test_category
WHERE ca_depth = 2
ORDER BY ca_order ASC;
2. 기능 요구 사항
id - 필요기능 - 기능 설명
A - 카테고리 목록 - 특정 parant 밑에 있는 child category의 목록데이터 가져오기
3. request 기획
id - 필요기능 - URL - method - parameter(요청인자) - 예외상황
A - 카테고리 목록 - /cate_list.ajax - POST
- [depth] depth 값, [parent] parent uid 값, parent 값이 없거나 0이면 depth의 값이 1일때 목록 가져오기 -
- 없는 parent uid 값, 잘못된 depth 값
4. response 기획
출력명 - 출력 설명 - 타입
count - 결과 데이터 개수 - 정수
status - [처리 결과] “OK”, “FAIL” .. - 문자열
message - 처리 메세지
data - 리스트 - 배열
└ 1 - uid - 카테고리 uid - 정수
└ 2 - name - 카테고리 이름 - 문자열
└ 3 - depth - 카테고리 depth - 정수
└ 4 - parent - 카테고리의 parent uid - 정수
└ 5 - order - 카테고리의 순서 - 정수
* 다단계 Category 구현!! 오늘이나 내일 오전 중으로 스스로 해보기 과제!!! *
'웹_프론트_백엔드 > JAVA프레임윅기반_풀스택' 카테고리의 다른 글
2020.06.18 (0) | 2020.06.18 |
---|---|
2020.06.17 (0) | 2020.06.17 |
JSP Team Project(2020.05.25 ~ 2020.06.15) (0) | 2020.06.16 |
2020.06.10 (0) | 2020.06.10 |
2020.06.09 (0) | 2020.06.09 |