create table security_board(
num number primary key,
writer varchar2(100),
subject varchar2(500),
content varchar2(4000),
regdate date
);
create sequence security_board_seq;
@Data
public class SecurityBoard {
private int num;
private String writer;//작성자(아이디)
private String subject;
private String content;
private String regdate;
private String name;//작성자(이름)
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xyz.itwill.mapper.SecurityBoardMapper">
<!-- 삽입 -->
<insert id="insertSecurityBoard">
<selectKey resultType="int" keyProperty="num" order="BEFORE">
select security_board_seq.nextval from dual
</selectKey>
insert into security_board values(#{num}, #{writer}, #{subject}, #{content}, sysdate)
</insert>
<!-- num이라는 조건에 맞게 업데이트 처리 -->
<update id="updateSecurityBoard">
update security_board set subject=#{subject}, content=#{content} where num=#{num}
</update>
<!-- 해당 번호로 삭제 -->
<delete id="deleteSecurityBoard">
delete from security_board where num=#{num}
</delete>
<!-- 검색한 컬럼을 resultType를 사용해서 해당 객체로 만들어줌
security_board 테이블과 security_user에 있는 컬럼을 join => inner join(해당 컬럼에 같은 값을 가진 필드만 join 처리)-->
<select id="selectSecurityBoardByNum" resultType="SecurityBoard">
select num, writer, subject, content, regdate, name from security_board
join security_user on writer=userid where num=#{num}
</select>
<!-- 리스트의 전체 갯수를 가져오는 sql -->
<!-- 조건을 걸어 검색필드에 값이 있다면 해당 조건과 키워드에 맞게 갯수를 반환 -->
<select id="selectSecurityBoardCount" resultType="int">
select count(*) from security_board join security_user on writer=userid
<if test="keyword != null and keyword != ''">
<bind name="word" value="'%'+keyword+'%'"/>
where ${column} like #{word}
</if>
</select>
<!-- 페이징 처리된 게시판 리스트를 가져오는 sql -->
<!-- 키워드가 있다면 검색된 키워드의 게시판 리스트를 반환 -->
<select id="selectSecurityBoardList" resultType="SecurityBoard">
select * from (select rownum rn, board.* from (select num, writer, subject, content
, regdate, name from security_board join security_user on writer=userid
<if test="keyword != null and keyword != ''">
<bind name="word" value="'%'+keyword+'%'"/>
where ${column} like #{word}
</if>
order by num desc) board) where rn between #{startRow} and #{endRow}
</select>
</mapper>
public interface SecurityBoardMapper {
int insertSecurityBoard(SecurityBoard board);
int updateSecurityBoard(SecurityBoard board);
int deleteSecurityBoard(int num);
SecurityBoard selectSecurityBoardByNum(int num);
int selectSecurityBoardCount(Map<String, Object> map);
List<SecurityBoard> selectSecurityBoardList(Map<String, Object> map);
}
selectSecurityBoardList 메소드의 매개변수는 전달값이 2개 이상이면 DTO 객체나 Map 객체를 사용해야하는데 DTO객체가 없다면 무조건 Map 객체 사용
Map 객체는 <String, Object>가 국롤
public interface SecurityBoardDAO {
int insertSecurityBoard(SecurityBoard board);
int updateSecurityBoard(SecurityBoard board);
int deleteSecurityBoard(int num);
SecurityBoard selectSecurityBoardByNum(int num);
int selectSecurityBoardCount(Map<String, Object> map);
List<SecurityBoard> selectSecurityBoardList(Map<String, Object> map);
}
@Repository
@RequiredArgsConstructor
public class SecurityBoardDAOImpl implements SecurityBoardDAO {
private final SqlSession sqlSession;
@Override
public int insertSecurityBoard(SecurityBoard board) {
return sqlSession.getMapper(SecurityBoardMapper.class).insertSecurityBoard(board);
}
@Override
public int updateSecurityBoard(SecurityBoard board) {
return sqlSession.getMapper(SecurityBoardMapper.class).updateSecurityBoard(board);
}
@Override
public int deleteSecurityBoard(int num) {
return sqlSession.getMapper(SecurityBoardMapper.class).deleteSecurityBoard(num);
}
@Override
public SecurityBoard selectSecurityBoardByNum(int num) {
return sqlSession.getMapper(SecurityBoardMapper.class).selectSecurityBoardByNum(num);
}
@Override
public int selectSecurityBoardCount(Map<String, Object> map) {
return sqlSession.getMapper(SecurityBoardMapper.class).selectSecurityBoardCount(map);
}
@Override
public List<SecurityBoard> selectSecurityBoardList(Map<String, Object> map) {
return sqlSession.getMapper(SecurityBoardMapper.class).selectSecurityBoardList(map);
}
}
public interface SecurityBoardService {
void addSecurityBoard(SecurityBoard board);
void modifySecurityBoard(SecurityBoard board);
void removeSecurityBoard(int num);
SecurityBoard getSecurityBoardByNum(int num);
Map<String, Object> getSecurityBoardList(Map<String, Object> map);
}
@Service
@RequiredArgsConstructor
public class SecurityBoardServiceImpl implements SecurityBoardService {
private final SecurityUserDAO securityUserDAO;
private final SecurityBoardDAO securityBoardDAO;
@Transactional
@Override
public void addSecurityBoard(SecurityBoard board) {
if(securityUserDAO.selectSecurityUserByUserid(board.getWriter()) == null) {
throw new RuntimeException("게시글 작성자를 찾을 수 없습니다.");
}
securityBoardDAO.insertSecurityBoard(board);
}
@Transactional
@Override
public void modifySecurityBoard(SecurityBoard board) {
if(securityUserDAO.selectSecurityUserByUserid(board.getWriter()) == null) {
throw new RuntimeException("게시글 작성자를 찾을 수 없습니다.");
}
if(securityBoardDAO.selectSecurityBoardByNum(board.getNum()) == null) {
throw new RuntimeException("변경하고자 하는 게시글을 찾을 수 없습니다.");
}
securityBoardDAO.updateSecurityBoard(board);
}
@Transactional
@Override
public void removeSecurityBoard(int num) {
if(securityBoardDAO.selectSecurityBoardByNum(num) == null) {
throw new RuntimeException("삭제 하고자 하는 게시글을 찾을 수 없습니다.");
}
securityBoardDAO.deleteSecurityBoard(num);
}
@Override
public SecurityBoard getSecurityBoardByNum(int num) {
SecurityBoard board=securityBoardDAO.selectSecurityBoardByNum(num);
if(board == null) {
throw new RuntimeException("게시글을 찾을 수 없습니다.");
}
return board;
}
@Override
public Map<String, Object> getSecurityBoardList(Map<String, Object> map) {
int pageNum=1;
if(map.get("pageNum") != null && !map.get("pageNUm").equals("")) {
pageNum=Integer.parseInt((String)map.get("pageNum"));
}
int pageSize=5;
if(map.get("pageSize") != null && !map.get("pageSize").equals("")) {
pageSize=Integer.parseInt((String)map.get("pageSize"));
}
int totalBoard=securityBoardDAO.selectSecurityBoardCount(map);
int blockSize=5;
Pager pager=new Pager(pageNum, pageSize, totalBoard, blockSize);
map.put("startRow", pager.getStartRow());
map.put("endRow", pager.getEndRow());
List<SecurityBoard> boardList=securityBoardDAO.selectSecurityBoardList(map);
Map<String, Object> result=new HashMap<String, Object>();
result.put("pager", pager);
result.put("securityBoardList", boardList);
return result;
}
}
@Controller
@RequestMapping("/board")
@RequiredArgsConstructor
public class SecurityBoardController {
private final SecurityBoardService securityBoardService;
//페이지 요청시 모든 전달값을 Map 객체로 제공받아 사용
// ex) Map 객체로 전달받는 값: /board/list?pageNum=2&pageSize=5&column=subject&keyword=Spring
@RequestMapping("/list")
public String list(@RequestParam Map<String, Object> map, Model model) {
model.addAttribute("resultMap", securityBoardService.getSecurityBoardList(map));
model.addAttribute("searchMap", map);
return "board/board_list";
}
@RequestMapping(value= "/register", method = RequestMethod.GET)
public String register() {
return "board/board_register";
}
@RequestMapping(value= "/register", method = RequestMethod.POST)
public String register(@ModelAttribute SecurityBoard board) {
board.setSubject(HtmlUtils.htmlEscape(board.getSubject()));
board.setContent(HtmlUtils.htmlEscape(board.getContent()));
securityBoardService.addSecurityBoard(board);
return "redirect:/board/list";
}
}
<body>
<h1>게시글 목록</h1>
<hr>
<div id="container">
<%-- 로그인된 사용자만 글쓰기 가능 --%>
<sec:authorize access="isAuthenticated()">
<div style="text-align: right; margin-bottom: 10px;">
<button type="button" onclick="location.href='<c:url value="/board/register"/>';">글쓰기</button>
</div>
</sec:authorize>
<table>
<tr>
<th class="num">글번호</th>
<th class="writer">작성자</th>
<th class="subject">제목</th>
<th class="regdate">작성일</th>
</tr>
<c:choose>
<c:when test="${empty resultMap.securityBoardList }">
<tr>
<td colspan="4">검색된 게시글이 없습니다.</td>
</tr>
</c:when>
<c:otherwise>
<c:forEach var="securityBoard" items="${resultMap.securityBoardList }">
<tr>
<td>${securityBoard.num }</td>
<td>${securityBoard.name }</td>
<td style="text-align: left;">
${securityBoard.subject }
</td>
<td>${securityBoard.regdate }</td>
</tr>
</c:forEach>
</c:otherwise>
</c:choose>
</table>
<div style="text-align: center;">
<c:choose>
<c:when test="${resultMap.pager.startPage > resultMap.pager.blockSize }">
<a href="<c:url value="/board/list"/>?pageNum=${resultMap.pager.prevPage}&pageSize=5&column=${searchMap.column}&keyword=${searchMap.keyword}">
[이전]
</a>
</c:when>
<c:otherwise>
[이전]
</c:otherwise>
</c:choose>
<c:forEach var="i" begin="${resultMap.pager.startPage }" end="${resultMap.pager.endPage }" step="1">
<c:choose>
<c:when test="${resultMap.pager.pageNum != i }">
<a href="<c:url value="/board/list"/>?pageNum=${i}&pageSize=5&column=${searchMap.column}&keyword=${searchMap.keyword}">
[${i }]
</a>
</c:when>
<c:otherwise>
[${i }]
</c:otherwise>
</c:choose>
</c:forEach>
<c:choose>
<c:when test="${resultMap.pager.endPage != resultMap.pager.totalPage }">
<a href="<c:url value="/board/list"/>?pageNum=${resultMap.pager.nextPage}&pageSize=5&column=${searchMap.column}&keyword=${searchMap.keyword}">
[다음]
</a>
</c:when>
<c:otherwise>
[다음]
</c:otherwise>
</c:choose>
</div>
<div style="text-align: center;">
<form action="<c:url value="/board/list"/>" method="post">
<select name="column">
<option value="name">작성자</option>
<option value="subject">제목</option>
<option value="content">내용</option>
</select>
<input type="text" name="keyword">
<sec:csrfInput/>
<button type="submit">검색</button>
</form>
</div>
</div>
</body>
global-method-security: Controller 클래스의 요청 처리 메소드에 권한 관련 어노테이션을 제공하기 위한 엘리먼트
security 네임스페이스를 추가하여 spring-securit.xsd 파일의 엘리먼트를 사용할 수 있도록 설정
pre-post-annotations 속성: disabled(기본값) 또는 enabled 중 하나를 속성값으로 설정
=> 속성값을 [enabled]로 설정하면 @PreAuthorize 어노테이션 또는 @PostAuthorize 어노테이션을 사용할 수 제공
secured-annotations 속성: disabled(기본값) 또는 enabled 중 하나를 속성값으로 설정
=> 속성값을 [enabled]로 설정하면 @Secured 어노테이션을 사용할 수 있도록 제공
<security:global-method-security pre-post-annotations="disabled" secured-annotations="disabled"/>
=> 설정 해주는거
로그인 사용자만 요청 처리 메소드를 호출할 수 있도록 권한 설정
@PreAuthorize: 요청 처리 메소드가 실행되기 전에 권한을 설정하기 위한 어노테이션(권한에 따라 요청 메서드 실행 여부 결정)
value 속성: 권한(ROLE)을 속성값으로 설정 - SpEL 사용 가능
=> value 속성외에 다른 속성이 없는 경우 속성값만 설정 가능
@PostAuthorize: 요청 처리 메소드가 실행된 후에 권한을 설정하기 위한 어노테이션
@Secured: 권한(ROLE)을 속성값으로 설정 - SpEL 사용 불가능
// 로그인 된 사용자만 해당 요청 메서드 이용 가능
@PreAuthorize("isAuthenticated()")
@RequestMapping(value= "/register", method = RequestMethod.GET)
public String register() {
return "board/board_register";
}
@PreAuthorize("isAuthenticated()")
@RequestMapping(value= "/register", method = RequestMethod.POST)
public String register(@ModelAttribute SecurityBoard board) {
board.setSubject(HtmlUtils.htmlEscape(board.getSubject()));
board.setContent(HtmlUtils.htmlEscape(board.getContent()));
securityBoardService.addSecurityBoard(board);
return "redirect:/board/list";
}
<sec:authorize access="isAuthenticated()">
<div style="text-align: right; margin-bottom: 10px;">
<button type="button" onclick="location.href='<c:url value="/board/register"/>';">글쓰기</button>
</div>
</sec:authorize>
<div style="margin-top: 10px;">
<form method="get" id="linkForm">
<input type="hidden" name="num" value="${securityBoard.num }">
<%-- 권한 설정을 위해 게시글 작성자 전달 --%>
<input type="hidden" name="writer" value="${securityBoard.writer }">
<input type="hidden" name="pageNum" value="${searchMap.pageNum }">
<input type="hidden" name="pageSize" value="${searchMap.pageSize }">
<input type="hidden" name="column" value="${searchMap.column }">
<input type="hidden" name="keyword" value="${searchMap.keyword }">
<button type="button" id="listBtn">글목록</button>
<sec:authorize access="isAuthenticated()">
<%-- authorize 태그의 access 속성값으로 설정된 권한이 있는 경우 var 속성값으로
작성된 Scope 속성명에 [true] 저장 --%>
<sec:authorize access="hasRole('ROLE_ADMIN')" var="adminRole"/>
<%-- authentication 태그로 인증된 사용자 정보를 제공받아 var 속성값으로 작성된
Scope 속성명에 인증된 사용자 정보 저장 --%>
<sec:authentication property="principal" var="pinfo"/>
<%-- 해당 조건을 만족하면 보여줌 --%>
<c:if test="${adminRole || pinfo.userid eq securityBoard.writer }">
<button type="button" id="modifyBtn">글변경</button>
<button type="button" id="removeBtn">글삭제</button>
</c:if>
</sec:authorize>
</form>
</div>
<script type="text/javascript">
$("#listBtn").click(function() {
$("#linkForm").attr("action","<c:url value="/board/list"/>").submit();
});
$("#modifyBtn").click(function() {
$("#linkForm").attr("action","<c:url value="/board/modify"/>").submit();
});
$("#removeBtn").click(function() {
$("#linkForm").attr("action","<c:url value="/board/remove"/>").submit();
});
게시글 수정 => 로그인 사용자 중 관리자 또는 게시글 작성자인 경우에만 요청 처리 메소드를 호출할 수 있도록 권한 설정
SpEL를 사용해 권한 설정할 경우 EL 연산자 사용 가능
=> # 표현식을 사용하여 요청 처리 메소드의 전달값이 저장된 매개변수 사용 가능
/modify를 GET 방식으로 요청할 때 map.get("num")으로 가져오는데 board_detail.jsp에서 hidden으로 name과 value 설정해놓은 값이 map객체에 저장되어 url로 넘어갈 때 원하는 값을 꺼내서 사용할 수있다.
@PreAuthorize("hasRole('ROLE_ADMIN') or principal.userid eq #map['writer']")
@RequestMapping(value= "/modify", method = RequestMethod.GET)
public String modify(@RequestParam Map<String, Object> map, Model model) {
int num=Integer.parseInt((String)map.get("num"));
model.addAttribute("securityBoard", securityBoardService.getSecurityBoardByNum(num));
model.addAttribute("searchMap", map);
return "board/board_modify";
}
@PreAuthorize("hasRole('ROLE_ADMIN') or principal.userid eq #map['writer'] ")
@RequestMapping(value= "/modify", method = RequestMethod.POST)
public String modify(@ModelAttribute SecurityBoard board,
@RequestParam Map<String, Object> map, Model model) throws UnsupportedEncodingException {
board.setSubject(HtmlUtils.htmlEscape(board.getSubject()));
board.setContent(HtmlUtils.htmlEscape(board.getContent()));
securityBoardService.modifySecurityBoard(board);
String pageNum=(String)map.get("pageNum");
String pageSize=(String)map.get("pageSize");
String column=(String)map.get("column");
// URLEncoder로 감싸줘야하는 이유: url은 한글을 못 받아서 utf-8이라고 명시를 해줘야 받을 수 있음
String keyword=URLEncoder.encode((String)map.get("keyword"), "utf-8");
// 새로운 페이지를 요청할 때 이전 페이지로 돌아가기 위해
// => 쉽게 말해 2페이지에서 상품의 상세페이지를 눌렀을 때 상품이 보여지고
// 글목록 버튼을 누르면 2페이지로 돌아가기 위해 필요(키워드로 검색했다면 키워드 목록까지)
return "redirect:/board/detail?num="+board.getNum()+"&pageNum="+pageNum
+"&pageSize="+pageSize+"&column="+column+"&keyword="+keyword;
}
삭제
로그인 사용자 중 관리자 또는 게시글 작성자인 경우에만 요청 처리 메소드를 호출할 수 있도록 권한 설정
관리자, 작성자만 삭제 가능 => 번호로 삭제
@PreAuthorize("hasRole('ROLE_ADMIN') or principal.userid eq #writer ")
@RequestMapping("/remove")
public String remove(@RequestParam int num, @RequestParam String writer) {
securityBoardService.removeSecurityBoard(num);
return "redirect:/board/list";
}
create table security_reply(
num number primary key,
writer varchar2(100),
content varchar2(1000),
regdate date,
board_num number,
constraint reply_board_num_fk foreign key(board_num)
references security_board(num) on delete cascade);
create sequence security_reply_seq;
@Data
public class SecurityReply {
private int num;
private String writer;//작성자(아이디)
@NotEmpty(message = "내용을 입력해 주세요.")
private String content;
private String regdate;
private int boardNum;
private String name;//작성자(이름)
}
<mapper namespace="xyz.itwill.mapper.SecurityReplyMapper">
<insert id="insertSecurityReply">
<selectKey resultType="int" keyProperty="num" order="BEFORE">
select security_reply_seq.nextval from dual
</selectKey>
insert into security_reply values(#{num}, #{writer}, #{content}, sysdate, #{boardNum})
</insert>
<select id="selectSecurityReplyList" resultType="SecurityReply">
select num, writer, content,regdate, board_num, name from security_reply join
security_user on writer=userid where board_num=#{boardNum} order by num desc
</select>
</mapper>
public interface SecurityReplyMapper {
int insertSecurityReply(SecurityReply reply);
List<SecurityReply> selectSecurityReplyList(int boardNum);
}
public interface SecurityReplyDAO {
int insertSecurityReply(SecurityReply reply);
List<SecurityReply> selectSecurityReplyList(int boardNum);
}
@Repository
@RequiredArgsConstructor
public class SecurityReplyDAOImpl implements SecurityReplyDAO {
private final SqlSession sqlSession;
@Override
public int insertSecurityReply(SecurityReply reply) {
return sqlSession.getMapper(SecurityReplyMapper.class).insertSecurityReply(reply);
}
@Override
public List<SecurityReply> selectSecurityReplyList(int boardNum) {
return sqlSession.getMapper(SecurityReplyMapper.class).selectSecurityReplyList(boardNum);
}
}
public interface SecurityReplyService {
void addSecurityReply(SecurityReply reply);
List<SecurityReply> getSecurityReplyList(int boardNum);
}
@Service
@RequiredArgsConstructor
public class SecurityReplyServiceImpl implements SecurityReplyService {
private final SecurityReplyDAO securityReplyDAO;
private final SecurityUserDAO securityUserDAO;
private final SecurityBoardDAO securityBoardDAO;
@Override
public void addSecurityReply(SecurityReply reply) {
if(securityUserDAO.selectSecurityUserByUserid(reply.getWriter()) == null) {
throw new RuntimeException("작성자를 찾을 수 없습니다.");
}
securityReplyDAO.insertSecurityReply(reply);
}
@Override
public List<SecurityReply> getSecurityReplyList(int boardNum) {
if(securityBoardDAO.selectSecurityBoardByNum(boardNum) == null) {
throw new RuntimeException("게시글을 찾을 수 없습니다.");
}
return securityReplyDAO.selectSecurityReplyList(boardNum);
}
}
// Spring MVC에서 RESTful 웹 서비스를 구현하는 컨트롤러를 정의할 때 사용한다.
// JSON 또는 XML 형식으로 데이터를 반환하는 컨트롤러임을 나타내고,
// 모든 메서드의 반환값이 HTTP 응답 본문으로 직접 쓰여진다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/reply")
public class SecurityReplyController {
private final SecurityReplyService securityReplyService;
//BindingResult 객체 : @Valid 어노테이션을 사용해 객체 필드값에 대한 검증시 문제가 발생될
//경우 관련 에러를 저장하기 위한 객체 - Errors 객체의 자식 객체
@PreAuthorize("isAuthenticated()")
// PostMapping: 주로 데이터를 서버로 전송하거나 등록할 때 사용. 여기서는 새로운 댓글을 등록하기 위해 사용
@PostMapping("/register")
public String register(@RequestBody @Valid SecurityReply reply, BindingResult bindingResult)
throws BindException {
if(bindingResult.hasErrors()) {
throw new BindException(bindingResult);
}
securityReplyService.addSecurityReply(reply);
return "success";
}
@GetMapping("/list/{boardNum}")
public List<SecurityReply> list(@PathVariable int boardNum) {
return securityReplyService.getSecurityReplyList(boardNum);
}
}
<%-- 댓글을 입력받거나 댓글 목록을 출력하는 태그 - 로그인 사용자에게만 제공 --%>
<sec:authorize access="isAuthenticated()">
<input type="hidden" id="writer" value="<sec:authentication property="principal.userid"/>">
<div>
<textarea rows="3" cols="60" id="content"></textarea>
<button type="button" id="addBtn">댓글쓰기</button>
</div>
</sec:authorize>
<%-- 댓글 --%>
<div id="replyList"></div>
// 화면에 출력하는 함수(처음 페이지가 로드 되었을 때 무조건 띄워질 수 있게)
function replyListDisplay() {
$.ajax({
type: "get",
url: "<c:url value="/reply/list"/>/"+${securityBoard.num},
dataType: "json",
success: function(result) {
if(result.length == 0) {
var html="<div style='width: 600px; border-bottom: 1px solid black;'>";
html+="댓글이 하나도 없습니다.";
html+="</div>";
$("#replyList").html(html);
return;
}
var html="";
$(result).each(function() {
html+="<div style='width: 600px; border-bottom: 1px solid black;'>";
html+="["+this.num+"]"+this.name+"<br>";
html+="<pre>"+this.content+"</pre>("+this.regdate+")";
html+="</div>";
});
$("#replyList").html(html);
},
error: function(xhr) {
alert("에러코드 = "+xhr.status);
}
});
}
replyListDisplay();
// 글쓰기 버튼을 눌렀을 때 hidden으로 처리되어있는 작성자랑 내용 작성 부분 가져옴
//beforeSend 속성 : 페이지 요청전에 실행될 명령이 작성된 함수를 속성값으로 설정
// => XMLHttpRequest 객체를 함수의 매개변수로 제공받아 사용 가능
beforeSend: function(xhr) {
xhr.setRequestHeader("${_csrf.headerName}","${_csrf.token}");
},
//ajaxSend() 메소드를 호출하여 페이지를 Ajax 기능으로 요청할 경우 무조건 CSRF 토큰 전달$(document).ajaxSend(function(event, xhr) {
xhr.setRequestHeader("${_csrf.headerName}","${_csrf.token}");
});