오늘이라도

[Spring] 14. 웹사이트 만들기 ⑬ : 답글 처리, 검색 기능 본문

취업성공패키지 SW 개발자 교육/Spring

[Spring] 14. 웹사이트 만들기 ⑬ : 답글 처리, 검색 기능

upcake_ 2020. 7. 17. 09:48
반응형

https://github.com/upcake/Class_Examples

교육 중에 작성한 예제들은 깃허브에 올려두고 있습니다. 

gif 파일은 클릭해서 보는 것이 정확합니다.


 - 웹사이트 만들기 ⑬ : 답글 처리, 검색 기능 -

▲작동 화면

 

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>list JSP</title>
</head>
<body>
<h3>공지사항</h3>
<form method="post" action="list.no" id="list">
	<input type="hidden" name="curPage" value="1"/>
	
	<div id="list-top">
		<div>
			<ul>
				<li>
					<select name="search" class="w-px80">
						<option value="all" ${page.search eq 'all' ? 'selected' : '' }>전체</option>
						<option value="title" ${page.search eq 'title' ? 'selected' : '' }>제목</option>
						<option value="content" ${page.search eq 'content' ? 'selected' : '' }>내용</option>
						<option value="writer" ${page.search eq 'writer' ? 'selected' : '' }>작성자</option>
					</select>
				</li>
				<li><input value="${page.keyword }" type="text" name="keyword" class="w-px300" /></li>
				<li><a class="btn-fill" onclick="$('form').submit()">검색</a></li>
			</ul>
			<ul>
				<core:if test="${login_info.admin eq 'Y' }">
					<li><a class="btn-fill" href="new.no">글쓰기</a></li>
				</core:if>
			</ul>
		</div>
	</div>
</form>

<table>
	<tr>
		<th class="w-px60">번호</th>
		<th>제목</th>
		<th class="w-px100">작성자</th>
		<th class="w-px120">작성일자</th>
		<th class="w-px60">첨부파일</th>
	</tr>
	<core:forEach items="${page.list }" var="vo">
		<tr>
			<td>${vo.no }</td>
			<td class="left">
				<core:forEach var="i" begin="1" end="${vo.indent }">
					${i eq vo.indent ? "<img src = 'img/re.gif' />" : "&nbsp;&nbsp;" }	
				</core:forEach>
				<a href="detail.no?id=${vo.id }&curPage=${page.curPage }" >${vo.title }</a>
			</td>
			<td>${vo.name }</td>
			<td>${vo.writedate }</td>
			<td>
				<core:if test="${!empty vo.filename }">
					<a href="download.no?id=${vo.id }">
						<img title="${vo.filename }" class="file-img" src="img/attach.png" />
					</a>
				</core:if>
			</td>
		</tr>
	</core:forEach>
</table>
<div class="btnSet">
	<jsp:include page="/WEB-INF/views/include/page.jsp"/>
</div>
</body>
</html>

▲list.jsp

 

@charset "UTF-8";
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR&display=swap');

body {
	margin: 0 auto;
	text-align: center;
	font-size: 16px;
	font-family: 'Noto Sans KR', sans-serif;
}

a:link, a:visited {
	text-decoration: none;
	color: #000;
}

#content {
	padding: 20px 0;
	min-width: 1024px;	/* 창의 최소 크기 지정 */
}

img {
	vertical-align: middle;	/* 세로축 가운데 정렬 */
}

table {
	width: 80%;
	margin: 0 auto;
	border: 1px solid;
	border-collapse: collapse;	/* 테두리 겹침 설정 collapse: 겹치지 않게 처리 */
}

table th, table td {
	border: 1px solid;
	padding: 5px 10px;
}

table td a:hover { font-weight: bold; }

.btnSet { margin-top: 20px;	}

a.btn-fill, a.btn-empty {
	text-align: center;
	padding: 3px 10px;
	border:1px solid #3367d6;
	border-radius: 3px;
	box-shadow: 2px 2px 3px #022d72;
	/* 오른쪽, 아래쪽, 번진 정도 */
}

a.btn-fill { 
	background-color: #3367d6;
	color: #fff;
}

a.btn-empty { 
	background-color: #fff;
	color: #3367d6
}

a.btn-fill-s, a.btn-empty-s {
	text-align: center;
	padding: 1px 10px;
	border:1px solid #c4dafc
	border-radius: 3px;
	box-shadow: 2px 2px 3px #022d72;
	color: #0000cd;
	font-size: 13px;
}

a.btn-fill-s { 
	background-color: #bacdfa;
}

a.btn-empty-s { 
	background-color: #fff;
}

.btnSet a:not(:first-child) {
	margin-left: 3px;
}

a:hover { cursor:pointer; }

input {
	height: 22px;
	padding: 3px 5px;
	font-size: 15px;
}

input[type=radio] {
	width: 18px;
	margin: 0 5px 3px;
	vertical-align: middle;
}

table tr td label:not(:last-child) {
	margin-right: 20px;	
}

.w-pct60 { width: 60% }
.w-pct70 { width: 70% }
.w-pct80 { width: 80% }
.w-px40 { width: 40px }
.w-px60 { width: 60px }
.w-px80 { width: 80px }
.w-px100 { width: 100px }
.w-px120 { width: 120px }
.w-px140 { width: 140px }
.w-px160 { width: 160px }
.w-px180 { width: 180px }
.w-px200 { width: 200px }
.w-px300 { width: 300px }

.left { text-align: left }
.right { text-align: right }

.font-img { cursor: pointer; }

ul { list-style: none; padding: 0; }
#list-top { width: 80%; padding: 20px 10%;}
#list-top ul { margin:0; display:flex; }
#list-top ul:last-child { float: right; }
#list-top ul:first-child { float: left; }
#list-top ul li:not(:first-child) { margin-left:2px }
#list-top div { width: 100%; height: 32px;}
#list-top ul li * { vertical-align:middle; }

input[name=title] { width:calc(100% - 14px) }
textarea[name=content] { width:calc(100% - 6px); height: 150px; resize: none;}

/* 파일 첨부 */
.file-img { width: 18px; height:18px; cursor:poinrter; }
#attach-file, #delete-file { display:none; }

select { height: 32px }

▲common.css

 

package common;

public class PageVO {
	private int pageList = 10; //페이지당 목록수
	private int blockPage = 10; //블럭당 페이지수
	private int totalList; //총목록수
	private int totalPage; //총페이지수
	//157 페이지 = 총목록수/페이지당 목록수 + 나머지가 있으면+1
	private int totalBlock; //총블럭수
	//16 블럭 = 총페이지수/블럭당 페이지수 + 나머지가 있으면+1
	private int curPage; //현재페이지번호
	private int beginList, endList; //현재페이지의 시작/끝 목록번호
	private int beginPage, endPage; //현재블럭의 시작/끝 페이지번호
	
	private String search, keyword; //검색기준, 검색어
	
	public String getSearch() {
		return search;
	}
	public void setSearch(String search) {
		this.search = search;
	}
	public String getKeyword() {
		return keyword;
	}
	public void setKeyword(String keyword) {
		this.keyword = keyword;
	}
	public int getPageList() {
		return pageList;
	}
	public void setPageList(int pageList) {
		this.pageList = pageList;
	}
	public int getBlockPage() {
		return blockPage;
	}
	public void setBlockPage(int blockPage) {
		this.blockPage = blockPage;
	}
	public int getTotalList() {
		return totalList;
	}
	public void setTotalList(int totalList) {
		this.totalList = totalList;
		
		//총페이지수=총목록수/페이지당보여질목록수
		//576/10 --> 57 ..6 -> 58페이지
		totalPage = totalList / pageList;
		if( totalList % pageList >0 ) ++totalPage;
		
		//총블럭수=총페이지수/블럭당보여질페이지수
		//58/10 --> 5..8 -> 6블럭
		totalBlock = totalPage / blockPage;
		if( totalPage % blockPage > 0 ) ++totalBlock;
		
		//시작/끝 목록번호
		//끝목록번호: 576, 566, 556, 
		endList = totalList - (curPage-1) * pageList;
		//시작목록번호: 567, 557, 547, 
		//= 끝목록번호 - (페이지당보여질목록수-1) 
		beginList = endList - (pageList-1);
		
		//현재 블럭번호 
		curBlock = curPage / blockPage;
		if( curPage % blockPage > 0 ) ++curBlock;
		//시작/끝 페이지번호
		//끝페이지번호: 10, 20, 30, ...
		endPage = curBlock * blockPage;
		//시작페이지번호 : 1, 11, 21, ...
		beginPage = endPage - (blockPage-1);
		
		//2048건 ▶ 1페이지 : 2048 ~ 2039, 1 ~ 10
		//			205페이지 : 8 ~ 1, 51 ~ 58
		//끝 페이지 번호가 총 페이지 번호보다 크면 총 페이지 번호가 끝 페이지 번호이다.
		if(endPage > totalPage) {endPage = totalPage; }
	}
	private int curBlock; //현재블럭번호 
	
	public int getCurBlock() {
		return curBlock;
	}
	public void setCurBlock(int curBlock) {
		this.curBlock = curBlock;
	}
	public int getTotalPage() {
		return totalPage;
	}
	public void setTotalPage(int totalPage) {
		this.totalPage = totalPage;
	}
	public int getTotalBlock() {
		return totalBlock;
	}
	public void setTotalBlock(int totalBlock) {
		this.totalBlock = totalBlock;
	}
	public int getCurPage() {
		return curPage;
	}
	public void setCurPage(int curPage) {
		this.curPage = curPage;
	}
	public int getBeginList() {
		return beginList;
	}
	public void setBeginList(int beginList) {
		this.beginList = beginList;
	}
	public int getEndList() {
		return endList;
	}
	public void setEndList(int endList) {
		this.endList = endList;
	}
	public int getBeginPage() {
		return beginPage;
	}
	public void setBeginPage(int beginPage) {
		this.beginPage = beginPage;
	}
	public int getEndPage() {
		return endPage;
	}
	public void setEndPage(int endPage) {
		this.endPage = endPage;
	}
}

▲PageVO.java

 

package com.hanul.iot;

import java.io.File;
import java.util.HashMap;

import javax.servlet.http.HttpServletResponse;
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 org.springframework.web.multipart.MultipartFile;

import common.CommonService;
import member.MemberServiceImpl;
import member.MemberVO;
import notice.NoticePage;
import notice.NoticeServiceImpl;
import notice.NoticeVO;

@Controller
public class NoticeController {
	@Autowired private NoticeServiceImpl service;
	@Autowired private MemberServiceImpl member;
	@Autowired private CommonService common;
	@Autowired private NoticePage page;
	
	//공지사항 목록화면 요청//////////////////////////////////////////////////////
	@RequestMapping("/list.no")
	public String list(Model model, HttpSession session, @RequestParam(defaultValue = "1") int curPage, String search, String keyword) {
		//공지사항 클릭 하면 admin으로 자동 로그인
		HashMap<String, String> map = new HashMap<String, String>();
		//HashMap : 데이터를 담을 자료 구조
		map.put("id", "admin");
		map.put("pw", "1234");
		session.setAttribute("login_info", member.member_login(map));
		session.setAttribute("category", "no");
		//DB에서 공지 글 목록을 조회해와 목록 화면에 출력
		page.setCurPage(curPage);
		page.setSearch(search);
		page.setKeyword(keyword);
		model.addAttribute("page", service.notice_list(page));
		
		return "notice/list";
	}
	
	//신규 공지 글 작성 화면 요청//////////////////////////////////////////////////////
	@RequestMapping("/new.no")
	public String notice() {
		return "notice/new";
	}
	
	//신규 공지 글 저장 처리 요청//////////////////////////////////////////////////////
	@RequestMapping("/insert.no")
	public String insert(MultipartFile file, NoticeVO vo, HttpSession session) {
		//첨부한 파일을 서버 시스템에 업로드하는 처리
		if( !file.isEmpty() ) {
			vo.setFilepath(common.upload("notice", file, session));
			vo.setFilename(file.getOriginalFilename());
		}
		
		vo.setWriter( ((MemberVO) session.getAttribute("login_info")).getId() );
		//화면에서 입력한 정보를 DB에 저장한 후
		service.notice_insert(vo);
		//목록 화면으로 연결
		return "redirect:list.no";
	}
	
	//공지글 상세 화면 요청//////////////////////////////////////////////////////
	@RequestMapping("/detail.no")
	public String detail(int id, Model model) {
		//선택한 공지글에 대한 조회수 증가 처리
		service.notice_read(id);
		
		//선택한 공지글 정보를 DB에서 조회해와 상세 화면에 출력
		model.addAttribute("vo", service.notice_detail(id));
		model.addAttribute("crlf", "\r\n");
		model.addAttribute("page", page);
		
		return "notice/detail";
	} //detail()
	
	//첨부파일 다운로드 요청//////////////////////////////////////////////////////
	@ResponseBody @RequestMapping("/download.no")
	public void download(int id, HttpSession session, HttpServletResponse response) {
		NoticeVO vo = service.notice_detail(id);
		common.download(vo.getFilename(), vo.getFilepath(), session, response);
	} //download()
	
	//공지글 삭제 처리 요청//////////////////////////////////////////////////////
	@RequestMapping("/delete.no")
	public String delete(int id, HttpSession session) {
		//선택한 공지글에 첨부된 파일이 있다면 서버의 물리적 영역에서 해당 파일도 삭제한다
		NoticeVO vo = service.notice_detail(id);
		if(vo.getFilepath() != null) {
			File file = new File(session.getServletContext().getRealPath("resources") + vo.getFilepath());
			if( file.exists() ) { file.delete(); }
		}
		
		//선택한 공지글을 DB에서 삭제한 후 목록 화면으로 연결
		service.notice_delete(id);
		
		return "redirect:list.no";
	} //delete()
	
	//공지글 수정 화면 요청//////////////////////////////////////////////////////
	@RequestMapping("/modify.no")
	public String modify(int id, Model model) {
		//선택한 공지글 정보를 DB에서 조회해와 수정화면에 출력
		model.addAttribute("vo", service.notice_detail(id));
		return "notice/modify";
	} //modify()
	
	//공지글 수정 처리 요청//////////////////////////////////////////////////////
	@RequestMapping("/update.no")
	public String update(NoticeVO vo, MultipartFile file, HttpSession session, String attach) {
		//원래 공지글의 첨부 파일 관련 정보를 조회
		NoticeVO notice = service.notice_detail(vo.getId());
		String uuid = session.getServletContext().getRealPath("resources") + notice.getFilepath();
		
		//파일을 첨부한 경우 - 없었는데 첨부 / 있던 파일을 바꿔서 첨부
		if(!file.isEmpty()) {
			vo.setFilename(file.getOriginalFilename());
			vo.setFilepath(common.upload("notice", file, session));
			
			//원래 있던 첨부 파일은 서버에서 삭제
			if( notice.getFilename() != null ) {
				File f = new File(uuid);
				if ( f.exists() ) { f.delete(); }
			}
			
		} else {
			//원래 있던 첨부 파일을 삭제됐거나 원래부터 첨부 파일이 없었던 경우
			if(attach.isEmpty()) {
				//원래 있던 첨부 파일은 서버에서 삭제
				if( notice.getFilename() != null ) {
					File f = new File(uuid);
					if ( f.exists() ) { f.delete(); }
				}
				
			//원래 있던 첨부 파일을 그대로 사용하는 경우
			} else {
				vo.setFilename(notice.getFilename());
				vo.setFilepath(notice.getFilepath());
			}
			
		}
		
		//화면에서 변경한 정보를 DB에 저장한 후 상세 화면으로 연결
		service.notice_update(vo);
		
		return "redirect:detail.no?id=" + vo.getId();
	} //update()
	
	//공지글 답글 쓰기 화면 요청=============================================================================================
	@RequestMapping("/reply.no")
	public String reply(Model model, int id) {
		//원글의 정보를 답글 쓰기 화면에서 알 수 있도록 한다.
		model.addAttribute("vo", service.notice_detail(id));
		
		return "notice/reply";
	} //reply()

	//공지글 신규 답글 저장 처리 요청=============================================================================================
	@RequestMapping("/reply_insert.no")
	public String reply_insert(NoticeVO vo, HttpSession session, MultipartFile file) {
		if(!file.isEmpty()) {
			vo.setFilename(file.getOriginalFilename());
			vo.setFilepath(common.upload("notice", file, session));
		}
		vo.setWriter( ((MemberVO)session.getAttribute("login_info")).getId() );
		
		//화면에서 입력한 정보를 DB에 저장한 후 목록화면으로 연결
		service.notice_reply_insert(vo);
		return "redirect:list.no";
	} //reply_insert()
} //class

▲NoticeController.java

 

package notice;

import java.util.List;

import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class NoticeDAO implements NoticeService {
	@Autowired private SqlSession sql;
	
	@Override
	public void notice_insert(NoticeVO vo) {
		sql.insert("notice.mapper.insert", vo);
	}

	@Override
	public List<NoticeVO> notice_list() {
		return sql.selectList("notice.mapper.list");
	}

	@Override
	public NoticeVO notice_detail(int id) {
		return sql.selectOne("notice.mapper.detail",id);
	}

	@Override
	public void notice_update(NoticeVO vo) {
		sql.update("notice.mapper.update", vo);
	}

	@Override
	public void notice_delete(int id) {
		sql.delete("notice.mapper.delete", id);
	}

	@Override
	public void notice_read(int id) {
		sql.update("notice.mapper.read", id);
	}

	@Override
	public NoticePage notice_list(NoticePage page) {
		page.setTotalList((Integer) sql.selectOne("notice.mapper.totalList", page));
		page.setList(sql.selectList("notice.mapper.list", page));
		
		return page;
	}

	@Override
	public void notice_reply_insert(NoticeVO vo) {
		sql.insert("notice.mapper.reply_insert", vo);
	}
}

▲NoticeDAO.java

 

<?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="notice.mapper" >
	<sql id="search_where">
		<if test="search == 'title' or search == 'content'">
			WHERE ${search } LIKE '%' || #{keyword } || '%'
		</if>
		<if test="search == 'writer'">
			WHERE <include refid="search_writer" />
		</if>
		<if test="search == 'all'">
			WHERE title LIKE '%' || #{keyword } || '%'
			OR content LIKE '%' || #{keyword } || '%'
			OR <include refid="search_writer" />
		</if>
	</sql>
	
	<sql id="search_writer">
		writer IN (SELECT id FROM member WHERE name LIKE '%' || #{keyword } || '%')
	</sql>
	
	<select id="list" resultType="notice.NoticeVO">
		SELECT * 
		FROM (SELECT ROWNUM no, n.*, (SELECT name FROM member m WHERE m.id = writer) name
				FROM (SELECT * FROM notice <include refid="search_where" /> ORDER BY root, step DESC) n
				ORDER BY no DESC) n 
		WHERE no BETWEEN #{beginList} and #{endList}				
		
	</select>

	<select id="totalList" resultType="integer">
		SELECT COUNT(*) FROM notice <include refid="search_where" />
	</select>
	
	<insert id="insert">	
		INSERT INTO notice (id, root, title, content, writer, filename, filepath)
		VALUES (seq_notice.nextval, seq_notice.currval, #{title }, #{content }, #{writer }, #{filename, jdbcType=VARCHAR }, #{filepath, jdbcType=VARCHAR }) 
	</insert>
	
	<select id="detail" resultType="notice.NoticeVO">
		SELECT n.*, (SELECT name FROM member m WHERE m.id = n.writer) name FROM notice n WHERE id=#{id }
	</select>
	<!-- jdbcType=VARCHAR 속성을 넣으면 null값이 허용된다. -->
	
	<update id="read">
		UPDATE notice SET readcnt = readcnt + 1 WHERE id=#{id}
	</update>
	
	<delete id="delete">
		DELETE FROM notice WHERE id=#{id }
	</delete>
	
	<update id="update">
		UPDATE notice set title = #{title }, content = #{content }, filename = #{filename, jdbcType=VARCHAR }, filepath = #{filepath, jdbcType=VARCHAR }
		WHERE id=#{id }
	</update>
	
	<insert id="reply_insert">
		<!-- 원글의 step보다 더 큰 step을 가진 글이 있다면 그 글들의 step을 먼저 +1 한다. -->
		<![CDATA[
			{CALL DECLARE BEGIN
				UPDATE notice SET step = step + 1
				WHERE root = #{root } AND step > #{step };
				
				INSERT INTO notice (id, root, title, content , writer, step, indent, filename, filepath)
				VALUES (seq_notice.NEXTVAL, #{root }, #{title }, #{content }, #{writer }, #{step } + 1, #{indent } + 1, #{filename, jdbcType=VARCHAR }, #{filepath, jdbcType=VARCHAR });
			END}
		]]>
	</insert>

</mapper>

▲notice-mapper.xml

 

--테이블 생성
CREATE TABLE customer (
	id		NUMBER CONSTRAINT customer_id_pk PRIMARY KEY,
	name	VARCHAR2(50) NOT NULL,
	gender	VARCHAR2(3) NOT NULL,
	email	VARCHAR2(50),
	phone	VARCHAR2(13)
);

--시퀀스 생성
CREATE SEQUENCE seq_customer START WITH 1 INCREMENT BY 1;

--레코드 삽입
INSERT INTO customer(id, name, gender)
VALUES (seq_customer.NEXTVAL, '홍길동', '남');

INSERT INTO customer(name, gender)
VALUES ('심청', '여');

--트리거(trigger) 설정
CREATE OR REPLACE TRIGGER trg_customer
    BEFORE INSERT ON customer   --커스터머 테이블에 인서트가 되기전에
    FOR EACH ROW    --모든 행에 대하여
BEGIN   --시작한다
    SELECT seq_customer.NEXTVAL INTO :new.id FROM dual;  --시퀀스의 데이터를 담고있는 테이블은 없으므로 더미 테이블(dual)에서 조회한다.
END;    
/       --끝 슬래쉬까지 써줘야한다


drop trigger trg_customer;
CREATE OR REPLACE TRIGGER trg_customer
    BEFORE INSERT ON customer 
    FOR EACH ROW 
BEGIN 
    SELECT seq_customer.NEXTVAL INTO :new.id FROM dual; 
END;    
/

--조회
SELECT * FROM customer;

--커밋
COMMIT;



--20/07/02==================================================================
--회원 관리 테이블
CREATE TABLE member(
    irum    VARCHAR2(20)    NOT NULL,
    id      VARCHAR2(20)    CONSTRAINT member_id_pk PRIMARY KEY,
    pw      VARCHAR2(20)    NOT NULL,
    age     NUMBER,
    gender  VARCHAR2(3)     NOT NULL,
    birth   DATE,
    post    VARCHAR2(7),
    addr    VARCHAR2(50),
    email   VARCHAR2(50)    NOT NULL,   --유니크가 들어간다 생각하고 NOT NULL만 지정
    tel     VARCHAR2(20),
    admin   VARCHAR2(1)     DEFAULT 'N'
);

--기존에 있던 member 테이블 수정
ALTER TABLE member
ADD(
    gender  VARCHAR2(3) DEFAULT '남' NOT NULL,
    birth   DATE,
    post    VARCHAR2(7),
    email   VARCHAR2(50),
    admin   VARCHAR2(1) default 'N'
);

UPDATE MEMBER SET email = id || '@naver.com';

ALTER TABLE member
MODIFY (irum NOT NULL, pw NOT NULL, email NOT NULL);

ALTER TABLE member RENAME COLUMN irum TO name;

ALTER TABLE member ADD CONSTRAINT member_id_pk PRIMARY KEY(id);

--관리자 회원 정보 저장
INSERT INTO member(name, id, pw, age, gender, email, admin)
VALUES ('관리자', 'admin', '1234', 25, '남', 'admin@admin.com', 'Y');

SELECT * FROM member;

--20/07/10======================================================================
CREATE TABLE notice(
    id          NUMBER CONSTRAINT notice_id_pk PRIMARY KEY,
    title       VARCHAR2(300) NOT NULL,
    content     VARCHAR2(4000) NOT NULL,
    writer      VARCHAR2(20) NOT NULL,
    writedate   DATE DEFAULT SYSDATE,
    readcnt     NUMBER DEFAULT 0,
    filename    VARCHAR2(300),
    filepath    VARCHAR2(300)
);

CREATE SEQUENCE seq_notice
START WITH 1 INCREMENT BY 1;

CREATE OR REPLACE TRIGGER trg_notice
    BEFORE INSERT ON notice
    FOR EACH ROW
BEGIN
    SELECT seq_notice.NEXTVAL INTO:NEW.id FROM dual;
END;
/

SELECT * FROM notice;

INSERT INTO notice(title, content, writer)
VALUES ('공지 글 테스트', '테스트 공지 글 입니다.', 'admin');

COMMIT;

--20/07/15================================================
--테이블에 공지글 항목 추가
INSERT INTO notice(title, content ,writer, writedate, filepath, filename)
SELECT title, content, writer, writedate, filepath, filename FROM notice;

COMMIT;

--20/07/16================================================
--notice 테이블에 칼럼 추가
CREATE TABLE notice(
    id          NUMBER CONSTRAINT notice_id_pk PRIMARY KEY,
    title       VARCHAR2(300) NOT NULL,
    content     VARCHAR2(4000) NOT NULL,
    writer      VARCHAR2(20) NOT NULL,
    writedate   DATE DEFAULT SYSDATE,
    readcnt     NUMBER DEFAULT 0,
    filename    VARCHAR2(300),
    filepath    VARCHAR2(300)
    root        NUMBER,
    step        NUMBER default 0,
    indent      NUMBER default 0 
);

ALTER TABLE notice
ADD(root NUMBER, step NUMBER DEFAULT 0, indent NUMBER DEFAULT 0);

UPDATE notice SET root = id;

--root : 원글의 root
--step : 원글 step + 1 / 원글의 step보다 더 큰 step을 가진 글이 있다면 그 글들의 step을 먼저 +1 한다.
--indent : 원글 indent + 1

ALTER TRIGGER trg_notice DISABLE;

SELECT * FROM notice;

COMMIT;

--20/07/17======================================================================================================================
INSERT INTO member(id, pw, gender, email, name, admin)
VALUES ('admin2', '1234', '남', 'admin2@admin@.com', '운영자', 'Y');

UPDATE notice SET writer='admin2'
WHERE mod(id, 3) = 0;

COMMIT;

▲table.sql

 

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="core" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>detail JSP</title>
</head>
<body>
<h3>공지글 안내</h3>
<table>
	<tr>
		<th class="w-px160">제목</th>
		<td colspan="5" class="left">${vo.title }</td>
	</tr>
	<tr>
		<th>작성자</th>
		<td>${vo.name }</td>
		<th class="w-px120">작성일자</th>
		<td class="w-px120">${vo.writedate }</td>
		<th class="w-px80">조회수</th>
		<td class="w-px80">${vo.readcnt }</td>
	</tr>
	<tr>
		<th>내용</th>
		<td colspan="5" class="left">${fn:replace(vo.content, crlf, '<br>') }</td>
	</tr>
	<tr>
		<th>첨부 파일</th>
		<td colspan="5" class="left">
			${vo.filename }
			<core:if test="${!empty vo.filename }">
				<a href="download.no?id=${vo.id }" style='margin-left: 15px'><i class="fas fa-download font-img"></i></a>
			</core:if>
		</td>
	</tr>
</table>

<div class="btnSet">
	<a class="btn-fill" href="list.no?curPage=${page.curPage }&search=${page.search }&keyword=${page.keyword }">목록으로</a>
	<!-- 관리자인 경우 수정/삭제 가능 -->
	<core:if test="${login_info.admin eq 'Y' }">
		<a class="btn-fill" href='modify.no?id=${vo.id }'>수정</a>
		<a class="btn-fill" onclick="if(confirm('정말 삭제하시겠습니까?')) {href='delete.no?id=${vo.id }' }">삭제</a>
	</core:if>
	<!-- 로그인이 된 경우 답글 쓰기 가능 -->
	<core:if test="${!empty login_info }">
		<a class="btn-fill" href="reply.no?id=${vo.id }">답글 쓰기</a>
	</core:if>
</div>
</body>
</html>

▲detail.jsp

 

 

 

반응형