day41 - 0922
댓글(Reply)
- 게시판(Board)과 같은 controller를 사용한다
테이블 생성
댓글(Reply) - 댓글 번호(reply_no) : 숫자, 기본키, 시퀀스로 부여 - 댓글 내용(reply_content) : 문자, 3000byte, 반드시 입력 - 댓글 작성자(reply_writer) : 문자, 회원(Member) 테이블의 회원 아이디(member_id)가 되도록, 회원 탈퇴시 null - 댓글 작성시간(reply_writetime) : 날짜, 기본값이 현재 시간이 되도록, 반드시 입력 - 댓글 원본글 번호(reply_origin) : 게시판(Board) 테이블의 게시글 번호(board_no)가 되도록, 게시글 삭제시 댓글 삭제 |
-- REPLY 테이블 생성
create table reply (
reply_no number primary key,
reply_writer varchar2(20) references member(member_id) on delete set null,
reply_origin number references board(board_no) on delete cascade,
reply_writetime date default sysdate not null,
reply_content varchar2(3000) not null
);
ReplyDto 생성
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ReplyDto {
// 필드
private int replyNo;
private String replyWriter;
private int replyOrigin;
private Date replyWritetime;
private String replyContent;
}
댓글 작성
replyDao
public interface ReplyDao {
// 추상 메소드 - 댓글 작성
void replyWrite(ReplyDto replyDto);
}
replyDaoImpl
@Repository
public class ReplyDaoImpl implements ReplyDao {
// 의존성 주입
@Autowired
private JdbcTemplate jdbcTemplate;
// 추상 메소드 오버라이딩 - 댓글 작성
@Override
public void replyWrite(ReplyDto replyDto) {
String sql = "insert into reply("
+ "reply_no, "
+ "reply_content,"
+ "reply_writer, "
+ "reply_origin"
+ ") values(reply_seq.nextval, ?, ?, ?)";
Object[] param = {replyDto.getReplyContent(),
replyDto.getReplyWriter(),
replyDto.getReplyOrigin()
};
jdbcTemplate.update(sql, param);
}
}
BoardController
@Controller
@RequestMapping("/board")
public class BoardController {
// 의존성 주입
@Autowired
ReplyDao replyDao;
// 1. 댓글 작성
// 댓글 작성 Mapping
@PostMapping("/reply/write")
public String replyWrite(@ModelAttribute ReplyDto replyDto, RedirectAttributes attr, HttpSession session) {
// HttpSession에서 로그인 중인 회원의 아이디(loginId) 반환
String replyWriter = (String) session.getAttribute("loginId");
// 반환한 회원 아이디(replyWriter)를 View에서 입력받은 replyDto의 작성자가 되도록 설정
replyDto.setReplyWriter(replyWriter);
// DB에서 등록(INSERT) 실행
replyDao.replyWrite(replyDto);
// 댓글 작성 후 답글이 달릴 원본 게시글의 상세 Mapping으로 강제 이동(redirect)
attr.addAttribute("boardNo", replyDto.getReplyOrigin());
// return "redirect:../detail" // 상대 경로
return "redirect:/board/detail"; // 절대 경로
}
}
board 폴더의 detail.jsp
- 로그인 여부에 따라 댓글 작성창이 다르게 보이도록 설정
<%-- 댓글 작성 --%>
<c:choose>
<%-- 로그인 상태라면 (loginId가 비어있지 않다면) --%>
<c:when test = "${loginId != null}">
<form action = "reply/write" method = "post">
<%-- 댓글의 원본글 번호(replyOrigin)이 해당 게시글의 게시글 번호(boardNo)가 되도록 --%>
<input type = "hidden" name = "replyOrigin" value = "${boardDto.boardNo}">
<table width = "500">
<tbody>
<tr>
<th>
<textarea name = "replyContent" rows = "5" cols = "55"
placeholder = "댓글 내용" required>
</textarea>
</th>
<th>
<button type = "submit">등록</button>
</th>
</tr>
</tbody>
</table>
</form>
</c:when>
<c:otherwise>
<table width = "500">
<tbody>
<tr>
<th>
<textarea name = "replyContent" rows = "5" cols = "55"
placeholder = "로그인 후 댓글 작성이 가능합니다" disabled>
</textarea>
</th>
<th>
<button type="submit" disabled>등록</button>
</th>
</tr>
</tbody>
</table>
</c:otherwise>
</c:choose>
댓글 목록
- 해당 게시글 번호(boardNo)와 같은 원본글 번호(replyOrigin)을 갖는 댓글의 전체 조회 결과를 표시한다
ReplyDao
import com.kh.springhomeSelfStudy.entity.ReplyDto;
public interface ReplyDao {
// 추상 메소드 - 댓글 목록
List<ReplyDto> replyList(int replyOrigin);
}
ReplyDaoImpl
@Repository
public class ReplyDaoImpl implements ReplyDao {
// 의존성 주입
@Autowired
private JdbcTemplate jdbcTemplate;
// ReplyDto에 대한 RowMapper
private RowMapper<ReplyDto> mapper = new RowMapper<ReplyDto>() {
@Override
public ReplyDto mapRow(ResultSet rs, int rowNum) throws SQLException {
return ReplyDto.builder()
.replyNo(rs.getInt("reply_no"))
.replyWriter(rs.getString("reply_writer"))
.replyContent(rs.getString("reply_content"))
.replyOrigin(rs.getInt("reply_origin"))
.replyWritetime(rs.getDate("reply_writetime"))
.build();
}
};
// 추상 메소드 오버라이딩 - 댓글 목록
@Override
public List<ReplyDto> replyList(int replyOrigin) {
String sql = "select * from reply "
+ "where reply_origin = ? "
+ "order by reply_no asc";
Object[] param = {replyOrigin};
return jdbcTemplate.query(sql, mapper, param);
}
}
BoardController
- 댓글 목록은 해당 게시글의 상세 페이지에 표시되도록 해야 한다
- BoardController의 게시글 상세 Mapping에서 댓글 전체 조회 결과를 model에 첨부한다
@Controller
@RequestMapping("/board")
public class BoardController {
// 의존성 주입
@Autowired
ReplyDao replyDao;
// 3. 게시글 상세
// 게시글 상세 Mapping
@GetMapping("/detail")
public String detail(Model model, @RequestParam int boardNo, HttpSession session) {
// HttpSession에 읽은 게시글 번호를 저장할 수 있는 Set 생성
Set<Integer> history = (Set<Integer>) session.getAttribute("history");
// 만약 history가 없다면 새로 생성
if(history == null) {
history = new HashSet<>();
}
// 현재 게시글 번호를 저장한 적이 있는지 판정
// - HashSet의 .add(E e) 명령은 저장할 수 있는지(boolean)을 반환
if(history.add(boardNo)) { // 저장할 수 있다면 (처음 들어온 게시글)
// 조회수 증가 + 게시글 상세의 결과를 model에 첨부
BoardDto boardDto = boardDao.read(boardNo);
model.addAttribute("boardDto", boardDto);
}
else { // 저장할 수 없다면 (이미 들어온 적이 있는 게시물)
// 게시글 상세의 결과를 model에 첨부
BoardDto boardDto = boardDao.selectOne(boardNo);
model.addAttribute("boardDto", boardDto);
}
// 갱신된 history를 다시 HttpSession에 저장 (해당 게시글을 다시 볼 때 조회수 증가 방지)
session.setAttribute("history", history);
// 2. 댓글 목록
// - 댓글 목록은 해당 게시글의 상세 페이지에 표시된다
// - 댓글 전체 조회의 결과를 model에 첨부
model.addAttribute("replyList", replyDao.replyList(boardNo));
// 게시글 상세 페이지(detail.jsp)로 연결
return "board/detail";
}
}
board 폴더의 detail.jsp
- Model에 첨부된 댓글 전체 조회의 결과를 반복문을 이용하여 표시한다
- 댓글 작성자의 닉네임과 등급을 표시하기 위해서는 댓글 테이블과 회원 테이블의 테이블 조인의 결과가 필요하다
- 로그인 중인 회원이 댓글 작성자일 경우에만 댓글 수정 및 삭제 메뉴를 표시한다
- 관리자일 경우에만 댓글 블라인드 메뉴를 표시한다
<table border = "1" width = "500">
<%-- 댓글 목록 --%>
<tbody>
<c:forEach var = "replyDto" items = "${replyList}">
<%-- 댓글 목록창 --%>
<tr>
<td width = "90%">
<%-- 댓글 작성자명 --%>
${replyDto.replyWriter}
<%-- 댓글 작성자가 게시글 작성자일 경우 [작성자]로 표시되도록 --%>
<c:if test = "${boardDto.getBoardWriter() == replyDto.getReplyWriter()}">
[작성자]
</c:if>
<%-- 댓글 작성자의 닉네임과 등급(테이블 조인 필요) --%>
[닉네임][등급]
<%-- 댓글 내용 --%>
<pre>${replyDto.replyContent}</pre>
<br><br>
<%-- 댓글 작성일이 'yyyy-MM-dd HH:mm'의 형태로 표시되도록 --%>
<fmt:formatDate value = "${replyDto.replyWritetime}" pattern = "yyyy-MM-dd HH:mm"/>
</td>
<th>
<%-- 로그인 중인 회원이 댓글 작성자일 때만 댓글 수정 및 삭제 메뉴가 보이도록 --%>
<c:if test = "${loginId == replyDto.replyWriter}">
수정
<br>
삭제
</c:if>
<%-- 관리자일 때만 댓글 블라인드 메뉴가 보이도록 --%>
<c:if test =" ${admin}">
<a href = "#">블라인드</a>
</c:if>
</th>
</tr>
</c:forEach>
</tbody>
</table>
댓글 삭제
- 댓글 삭제 후 해당 댓글이 달린 게시글의 상세페이지로 강제 이동하기 위해 게시글 번호(replyOrigin)가 필요하다
- 댓글 삭제를 위해 해당 댓글 번호(replyNo)가 필요하다
ReplyDao
import com.kh.springhomeSelfStudy.entity.ReplyDto;
public interface ReplyDao {
// 추상 메소드 - 댓글 삭제
boolean replyDelete(int replyNo);
}
ReplyDaoImpl
- 댓글 번호(replyNo)를 매개변수로 하여 댓글 삭제를 실행한다
@Repository
public class ReplyDaoImpl implements ReplyDao {
// 의존성 주입
@Autowired
private JdbcTemplate jdbcTemplate;
// 추상 메소드 오버라이딩 - 댓글 삭제
@Override
public boolean replyDelete(int replyNo) {
String sql = "delete reply where reply_no = ?";
Object[] param = new Object[] {replyNo};
return jdbcTemplate.update(sql, param) > 0;
}
}
BoardController
- 댓글 번호(replyNo)는 댓글 삭제를 위해, 댓글이 달린 게시글 번호(replyOrigin)는 강제 이동(redirect)를 위해 필요하다
@Controller
@RequestMapping("/board")
public class BoardController {
// 의존성 주입
@Autowired
ReplyDao replyDao;
// 4. 댓글 삭제
@GetMapping("/reply/delete")
public String replyDelete(RedirectAttributes attr, @RequestParam int replyNo, @RequestParam int replyOrigin) {
// DB에서 삭제(DELETE) 실행
replyDao.replyDelete(replyNo);
// 댓글 삭제 후 답글이 달릴 원본 게시글의 상세 Mapping으로 강제 이동(redirect)
attr.addAttribute("boardNo", replyOrigin);
return "redirect:/board/detail";
}
}
board 폴더의 detail.jsp
- 댓글 삭제 및 강제 이동을 위해 댓글 삭제시 하이퍼링크로 댓글 번호(replyNo)와 댓글 원본글 번호(replyOrigin) 전달
<%-- 댓글 목록 --%>
<tbody>
<c:forEach var = "replyDto" items = "${replyList}">
<%-- 댓글 목록창 --%>
<tr>
<td width = "90%">
<%-- 댓글 작성자명 --%>
${replyDto.replyWriter}
<%-- 댓글 작성자가 게시글 작성자일 경우 [작성자]로 표시되도록 --%>
<c:if test = "${boardDto.getBoardWriter() == replyDto.getReplyWriter()}">
[작성자]
</c:if>
<%-- 댓글 작성자의 닉네임과 등급(테이블 조인 필요) --%>
[닉네임][등급]
<%-- 댓글 내용 --%>
<pre>${replyDto.replyContent}</pre>
<br><br>
<%-- 댓글 작성일이 'yyyy-MM-dd HH:mm'의 형태로 표시되도록 --%>
<fmt:formatDate value = "${replyDto.replyWritetime}" pattern = "yyyy-MM-dd HH:mm"/>
</td>
<th>
<%-- 로그인 중인 회원이 댓글 작성자일 때만 댓글 수정 및 삭제 메뉴가 보이도록 --%>
<c:if test = "${loginId == replyDto.replyWriter}">
수정
<br>
<%-- 댓글 삭제 및 강제 이동을 위해 하이퍼링크로 replyNo와 replyOrigin 전달 --%>
<a href = "reply/delete?replyNo=${replyDto.replyNo}&replyOrigin=${replyDto.replyOrigin}">삭제</a>
</c:if>
<%-- 관리자일 때만 댓글 블라인드 메뉴가 보이도록 --%>
<c:if test =" ${admin}">
<a href = "#">블라인드</a>
</c:if>
</th>
</tr>
</c:forEach>
</tbody>
</table>
댓글 수정
- 댓글 수정 후 해당 댓글이 달린 게시글의 상세페이지로 강제 이동하기 위해 게시글 번호(replyOrigin)가 필요하다
- 댓글 수정을 위해 해당 댓글 번호(replyNo)가 필요하다
ReplyDao
public interface ReplyDao {
// 추상 메소드 - 댓글 수정
boolean replyUpdate(ReplyDto replyDto);
}
ReplyDaoImpl
- 댓글 번호(replyNo)를 매개변수로 하여 댓글 수정를 실행한다
@Repository
public class ReplyDaoImpl implements ReplyDao {
// 의존성 주입
@Autowired
private JdbcTemplate jdbcTemplate;
// 추상 메소드 오버라이딩 - 댓글 수정
@Override
public boolean replyUpdate(ReplyDto replyDto) {
String sql = "update reply set reply_content = ? where reply_no = ?";
Object[] param = new Object[] {replyDto.getReplyContent(), replyDto.getReplyNo()};
return jdbcTemplate.update(sql, param) > 0;
}
}
BoardController
- 댓글 번호(replyNo)는 댓글 수정를 위해, 댓글이 달린 게시글 번호(replyOrigin)는 강제 이동(redirect)를 위해 필요하다
@Controller
@RequestMapping("/board")
public class BoardController {
// 의존성 주입
@Autowired
BoardDao boardDao;
// 의존성 주입
@Autowired
ReplyDao replyDao;
// 3. 댓글 수정
@PostMapping("/reply/edit")
public String replyEdit(RedirectAttributes attr, @ModelAttribute ReplyDto replyDto) {
// DB에서 수정(UPDATE) 실행
replyDao.replyUpdate(replyDto);
// 댓글 수정 후 답글이 달릴 게시글 상세 Mapping으로 강제 이동(redirect)
attr.addAttribute("boardNo", replyDto.getReplyOrigin());
return "redirect:/board/detail";
}
}
board 폴더의 detail.jsp
- 댓글창에서 수정을 누를 때 해당 댓글칸이 수정창으로 바뀌도록 하기 위해서는 JQuery를 사용해야 한다
1) 처음에는 댓글칸(view 클래스)만 보여준다
2) 수정(edit-btn 클래스)을 누르면 댓글칸(view 클래스)를 숨기고 댓글 수정창(editor 클래스)를 보여준다
3) 취소(cancel-btn 클래스)를 누르면 다시 댓글칸(view 클래스)를 보여주고 댓글 수정창(editor 클래스)를 숨긴다
- 아직 JQuery에 대해서 배우지 않았으므로 흐름만 파악한다
<%-- 댓글 수정을 누를 때 댓글 수정창이 나오도록 설정(JQuery 사용) --%>
<script src="https://code.jquery.com/jquery-3.6.0.js"></script>
<script>
$(function(){
//1. edit-btn을 누르면 view를 숨기고 editor를 보여준다
$(".edit-btn").click(function(){
$(this).parents(".view").hide();
$(this).parents(".view").next(".editor").show();
});
//2. cancel-btn을 누르면 editor를 숨기고 view를 보여준다
$(".cancel-btn").click(function(){
$(this).parents(".editor").hide();
$(this).parents(".editor").prev(".view").show();
});
//3. 처음에는 view만 보여준다
$(".editor").hide();
});
</script>
<table border = "1" width = "500">
<%-- 댓글 목록 --%>
<tbody>
<c:forEach var = "replyDto" items = "${replyList}">
<%-- 댓글 목록창 --%>
<tr class = "view"> <%-- class 이름을 view로 변경 --%>
<td width = "90%">
<%-- 댓글 작성자명 --%>
${replyDto.replyWriter}
<%-- 댓글 작성자가 게시글 작성자일 경우 [작성자]로 표시되도록 --%>
<c:if test = "${boardDto.getBoardWriter() == replyDto.getReplyWriter()}">
[작성자]
</c:if>
<%-- 댓글 작성자의 닉네임과 등급(테이블 조인 필요) --%>
[닉네임][등급]
<%-- 댓글 내용 --%>
<pre>${replyDto.replyContent}</pre>
<br><br>
<%-- 댓글 작성일이 'yyyy-MM-dd HH:mm'의 형태로 표시되도록 --%>
<fmt:formatDate value = "${replyDto.replyWritetime}" pattern = "yyyy-MM-dd HH:mm"/>
</td>
<th>
<%-- 로그인 중인 회원이 댓글 작성자일 때만 댓글 수정 및 삭제 메뉴가 보이도록 --%>
<c:if test = "${loginId == replyDto.replyWriter}">
<%-- 댓글 수정을 누르면 --%>
<a class = "edit-btn">수정</a>
<br>
<%-- 댓글 삭제 및 강제 이동을 위해 하이퍼링크로 replyNo와 replyOrigin 전달 --%>
<a href = "reply/delete?replyNo=${replyDto.replyNo}&replyOrigin=${replyDto.replyOrigin}">삭제</a>
</c:if>
<%-- 관리자일 때만 댓글 블라인드 메뉴가 보이도록 --%>
<c:if test =" ${admin}">
<a href = "#">블라인드</a>
</c:if>
</th>
</tr>
<%-- 댓글 수정창 --%>
<%-- 로그인 중인 회원이 댓글 작성자일 때만 수정창이 보이도록 --%>
<c:if test = "${loginId == replyDto.replyWriter}">
<tr class = "editor"> <%-- class 이름을 editor로 변경 --%>
<th colspan = "2">
<form action = "reply/edit" method = "post">
<input type = "hidden" name = "replyNo" value = "${replyDto.replyNo}">
<input type = "hidden" name = "replyOrigin" value = "${replyDto.replyOrigin}">
<textarea name = "replyContent" rows = "5" cols = "50" required>${replyDto.replyContent}</textarea>
<button type = "submit">변경</button>
<a class = "cancel-btn">취소</a>
</form>
</th>
</tr>
</c:if>
</c:forEach>
</tbody>
</table>
댓글에 대한 Interceptor - 댓글 작성자 본인만 댓글 수정 및 삭제가 가능하도록
** HTTP Referer
- HTTP header에 담겨있는 정보 중 하나로 이전 페이지에 대한 url 정보를 담고있다
- HTTP Referer의 유무에 따라 이전 페이지에서 접속했는지, 해당 페이지의 url을 직접 입력하고 들어왔는지를 알 수 있다
ReplyDao
- 댓글 번호(replyNo)를 매개변수로 하는 단일 조회 메소드 추가
public interface ReplyDao {
// 추상 메소드 - 댓글 정보(Interceptor 설정을 위해 필요)
ReplyDto replyDetail(int replyNo);
}
ReplyDaoImpl
- 댓글 번호(replyNo)를 매개변수로 하는 단일 조회 메소드 오버라이딩
@Repository
public class ReplyDaoImpl implements ReplyDao {
// 의존성 주입
@Autowired
private JdbcTemplate jdbcTemplate;
// 추상 메소드 오버라이딩 - 댓글 정보(Interceptor 설정을 위해 필요)
@Override
public ReplyDto replyDetail(int replyNo) {
String sql = "select * from reply where reply_no = ?";
Object[] param = new Object[] {replyNo};
return jdbcTemplate.query(sql, extractor, param);
}
}
MemberReplyOwnerCheckInterceptor
@Component
public class MemberReplyOwnerCheckInterceptor implements HandlerInterceptor {
// 의존성 주입
@Autowired
private ReplyDao replyDao;
// preHandler 메소드 오버라이딩
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// HttpSession에서 로그인 중인 회원 아이디 반환
// 1) HttpSession 반환
HttpSession session = request.getSession();
// 2) HttpSession에서 로그인 중인 회원 아이디(loginId) 반환
String loginId = (String) session.getAttribute("loginId");
// RequestParam인 댓글 번호(replyNo)로 단일 조회하여 해당 댓글의 작성자 반환
// 1) 하이퍼링크를 통해 전달받는 댓글 번호의 정보를 반환 (String 형태로 반환되므로 int로 형 변환해야 한다)
int replyNo = Integer.parseInt(request.getParameter("replyNo"));
// 2) 전달받은 댓글 번호(replyNo)로 단일 조회 실행
ReplyDto replyDto = replyDao.replyDetail(replyNo);
// 3) 단일 조회의 결과에서 해당 댓글의 작성자(replyWriter) 반환
String replyWriter = replyDto.getReplyWriter();
// Http Header에서 Referer 정보(이전 페이지의 URL 정보) 반환
String referer = request.getHeader("Referer");
// 1. 현재 로그인 중인 회원이 해당 댓글의 작성자인지 판정
if(loginId.equals(replyWriter)) { // 현재 로그인 중인 회원이 해당 댓글의 작성자라면
// Referer이 존재하며(null이 아님) referer에 게시글 상세 페이지의 jsp파일 경로가 포함되어 있다면
if(referer != null && referer.contains("/board/detail")) {
return true; // 해당 회원은 작성자이면서 이전 페이지를 통해 수정/삭제 경로에 도달했으므로 통과(true)
}
}
// 2. 그 외의 경우는 모두 차단(false)
response.sendError(403);
return false;
}
}
InterceptorConfiguration
- 댓글 작성자가 아닐 경우 해당 댓글 수정(/board/reply/edit)과 삭제(/board/reply/delete)에 대한 접근을 제한한다
** 관리자는 모든 댓글을 삭제하는 것이 아닌 블라인드 처리하도록 해야한다
@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {
// 의존성 주입
@Autowired
private MemberReplyOwnerCheckInterceptor memberReplyOwnerCheckInterceptor;
// 추상 메소드 오버라이딩 - addInterceptors(InterceptorRegistry registry)
@Override
public void addInterceptors(InterceptorRegistry registry) {
// MemberReplyOwnerCheckInterceptor에 대한 설정
registry.addInterceptor(memberReplyOwnerCheckInterceptor)
.addPathPatterns(
"/board/reply/edit", // 댓글 수정
"/board/reply/delete" // 댓글 삭제
)
.excludePathPatterns(
);
}
}
댓글 블라인드
- 관리자는 회원의 댓글을 삭제하는 대신 블라인드 처리한다
- 댓글 테이블에 댓글의 블라인드 상태를 나타내는 컬럼을 추가한다
- 댓글 블라인드 컬럼의 값에 따라 해당 댓글의 표시 여부가 결정된다
댓글 테이블 (수정)
- 댓글 테이블에 댓글 블라인드(reply_blind) 컬럼을 추가한다
- 댓글 블라인드(reply_blind)의 자료형은 char(1)로 하며 블라인드가 설정되면 'Y', 해제되면 null이 입력되도록 한다
댓글(Reply) - 댓글 번호(reply_no) : 숫자, 기본키, 시퀀스로 부여 - 댓글 내용(reply_content) : 문자, 3000byte, 반드시 입력 - 댓글 작성자(reply_writer) : 문자, 회원(Member) 테이블의 회원 아이디(member_id)가 되도록, 회원 탈퇴시 null - 댓글 작성시간(reply_writetime) : 날짜, 기본값이 현재 시간이 되도록, 반드시 입력 - 댓글 원본글 번호(reply_origin) : 게시판(Board) 테이블의 게시글 번호(board_no)가 되도록, 게시글 삭제시 댓글 삭제 - 댓글 블라인드(reply_blind) : 문자(char(1)), 블라인드 상태에 따라 'Y' 또는 null이 들어가도록 설정 |
-- reply 테이블 생성
create table reply (
reply_no number primary key,
reply_writer varchar2(20) references member(member_id) on delete set null,
reply_origin number references board(board_no) on delete cascade,
reply_writetime date default sysdate not null,
reply_content varchar2(3000) not null,
reply_blind char(1) check(reply_blind = 'Y');
);
ReplyDto (수정)
- reply 테이블의 댓글 블라인드(reply_blind) 컬럼을 위해 DTO에 replyBlind 필드를 추가한다
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ReplyDto {
// 필드
private int replyNo;
private String replyWriter;
private int replyOrigin;
private Date replyWritetime;
private String replyContent;
// 댓글 블라인드 상태 처리를 위한 필드
// - 블라인드 컬럼에 저장된 char(1)의 값을 논리로 변환
private boolean replyBlind;
}
ReplyDao
- 댓글 블라인드 상태를 수정하는 추상 메소드 추가
public interface ReplyDao {
// 추상 메소드 - 댓글 블라인드 상태 수정
boolean updateBlind(int replyNo, boolean blind);
}
ReplyDaoImpl
- DTO에 replyBlind 필드가 추가되었으므로 RowMapper와 ResultSetExtractor를 수정한다
- 댓글 블라인드 상태를 수정하는 추상 메소드 오버라이딩
3항 연산을 통해
DTO의 replyBlind 필드값이 true이면 DB에서 댓글 테이블의 reply_blind 컬럼값을 문자열 Y로,
DTO의 replyBlind 필드값이 false이면 DB에서 댓글 테이블의 reply_blind 컬럼 값을 null로 수정
@Repository
public class ReplyDaoImpl implements ReplyDao {
// 의존성 주입
@Autowired
private JdbcTemplate jdbcTemplate;
// ReplyDto에 대한 RowMapper
private RowMapper<ReplyDto> mapper = new RowMapper<ReplyDto>() {
@Override
public ReplyDto mapRow(ResultSet rs, int rowNum) throws SQLException {
return ReplyDto.builder()
.replyNo(rs.getInt("reply_no"))
.replyWriter(rs.getString("reply_writer"))
.replyContent(rs.getString("reply_content"))
.replyOrigin(rs.getInt("reply_origin"))
.replyWritetime(rs.getDate("reply_writetime"))
// reply 테이블의 reply_blind 값을 논리로 변환
// - reply_blind 테이블이 null이 아니면 true 반환
// - reply_blind 테이블이 null이면 false를 반환
.replyBlind(rs.getString("reply_blind") != null)
.build();
}
};
// ReplyDto에 대한 ResultSetExtractor
private ResultSetExtractor<ReplyDto> extractor = new ResultSetExtractor<>() {
@Override
public ReplyDto extractData(ResultSet rs) throws SQLException, DataAccessException {
if(rs.next()) {
return ReplyDto.builder()
.replyNo(rs.getInt("reply_no"))
.replyWriter(rs.getString("reply_writer"))
.replyContent(rs.getString("reply_content"))
.replyOrigin(rs.getInt("reply_origin"))
.replyWritetime(rs.getDate("reply_writetime"))
// reply 테이블의 reply_blind 값을 논리로 변환
// - reply_blind 테이블이 null이 아니면 true 반환
// - reply_blind 테이블이 null이면 false를 반환
.replyBlind(rs.getString("reply_blind") != null)
}
else {
return null;
}
}
};
// 추상 메소드 오버라이딩 - 댓글 블라인드 상태 수정
@Override
public boolean updateBlind(int replyNo, boolean replyBlind) {
String sql = "update reply set reply_blind = ? where reply_no = ?";
// 3항 연산
// - 댓글이 블라인드 상태이면(replyBlind가 true) setReplyBlind의 값은 "Y"
// - 댓글이 블라인드 상태가 아니라면(replyBlind가 false) setReplyBlind의 값은 null
String setReplyBlind = replyBlind ? "Y" : null;
Object[] param = new Object[] {setReplyBlind, replyNo};
return jdbcTemplate.update(sql, param) > 0;
}
}
BoardController
-
@Controller
@RequestMapping("/board")
public class BoardController {
// 의존성 주입
@Autowired
ReplyDao replyDao;
// 5. 댓글 블라인드 상태 수정
@GetMapping("/reply/blind")
public String replyBlind(RedirectAttributes attr, @RequestParam int replyNo, @RequestParam int replyOrigin) {
// 하이퍼링크를 통해 전달받은 replyNo를 이용하여 단일 조회 실행 후 그 결과를 replyDto에 저장
ReplyDto replyDto = replyDao.replyDetail(replyNo);
// replyDto에서 해당 댓글의 블라인드 상태(replyBlind) 반환
// - boolean에 대한 getter 메소드의 이름은 'is'로 시작한다
boolean isReplyBlind = replyDto.isReplyBlind();
// 해당 댓글의 현재 블라인드 상태(isReplyBlind)와 반대인 값을 새로운 블라인드 상태로 수정
replyDao.updateBlind(replyNo, !isReplyBlind);
// 블라인드 상태 수정 후 해당 댓글이 달린 게시글 상세 Mapping으로 강제 이동(redirect)
attr.addAttribute("boardNo", replyOrigin);
return "redirect:/board/detail";
}
}
** 댓글 블라인드 현재 상태 판정 및 상태 수정
- 논리 부정 연산자(!)로 if ~ else 문을 간결하게 쓸 수 있다
// replyDto에서 해당 댓글의 블라인드 상태(replyBlind) 반환
// - boolean에 대한 getter 메소드의 이름은 'is'로 시작한다
boolean isReplyBlind = replyDto.isReplyBlind();
// 1. if ~ else문 사용
// 댓글 블라인드 현재 상태 판정 및 상태 수정
if(isReplyBlind) { // 해당 댓글이 블라인드 상태이면
replyDao.updateBlind(replyNo, false); // 블라인드 해제
}
else { // 그렇지 않다면(해당 댓글이 블라인드 상태가 아니라면)
replyDao.updateBlind(replyNo, true); // 블라인드 설정
}
// 2. 논리 부정 연산자(!) 사용
// 해당 댓글의 현재 블라인드 상태(isReplyBlind)와 반대인 값을 새로운 블라인드 상태로 수정
replyDao.updateBlind(replyNo, !isReplyBlind);
board 폴더의 detail.jsp
- 관리자일 때만 댓글 블라인드 메뉴가 보이도록 설정
- 댓글의 현재 블라인드 상태에 따라 댓글 블라인드 메뉴가 다르게 보이도록(설정/해제) 설정
- 댓글 블라인드시 하이퍼링크로 해당 댓글 번호(replyNo)와 원본글 번호(replyOrigin)가 전달되도록 하여
Controller에서 RequestParam으로 입력받을 수 있도록 설정
<table border = "1" width = "500">
<%-- 댓글 목록 --%>
<tbody>
<c:forEach var = "replyDto" items = "${replyList}">
<%-- 댓글 목록창 --%>
<tr class = "view">
<td width = "90%">
${replyDto.replyWriter}
<c:if test = "${boardDto.getBoardWriter() == replyDto.getReplyWriter()}">
[작성자]
</c:if>
[닉네임][등급]
<%-- 댓글 내용 - 블라인드 여부에 따라 다르게 보이도록 --%>
<c:choose>
<c:when test = "${replyDto.replyBlind}">
<pre>블라인드 처리된 댓글입니다</pre>
</c:when>
<c:otherwise>
<pre>${replyDto.replyContent}</pre>
</c:otherwise>
</c:choose>
<br><br>
<fmt:formatDate value = "${replyDto.replyWritetime}" pattern = "yyyy-MM-dd HH:mm"/>
</td>
<th>
<c:if test = "${loginId == replyDto.replyWriter}">
<a class = "edit-btn">수정</a>
<br>
<a href = "reply/delete?replyNo=${replyDto.replyNo}&replyOrigin=${replyDto.replyOrigin}">삭제</a>
</c:if>
<%-- 관리자일 때만 댓글 블라인드 메뉴가 보이도록 --%>
<c:if test="${admin}">
<%-- 댓글의 현재 블라인드 상태에 따라 댓글 블라인드 메뉴가 다르게 보이도록 설정 --%>
<c:choose>
<c:when test = "${replyDto.replyBlind}">
<a href="reply/blind?replyNo=${replyDto.replyNo}&replyOrigin=${replyDto.replyOrigin}">블라인드<br>해제</a>
</c:when>
<c:otherwise>
<a href="reply/blind?replyNo=${replyDto.replyNo}&replyOrigin=${replyDto.replyOrigin}">블라인드<br>설정</a>
</c:otherwise>
</c:choose>
</c:if>
</th>
</tr>
<%-- 댓글 수정창 --%>
<c:if test = "${loginId == replyDto.replyWriter}">
<tr class = "editor">
<th colspan = "2">
<form action = "reply/edit" method = "post">
<input type = "hidden" name = "replyNo" value = "${replyDto.replyNo}">
<input type = "hidden" name = "replyOrigin" value = "${replyDto.replyOrigin}">
<textarea name = "replyContent" rows = "5" cols = "50" required>${replyDto.replyContent}</textarea>
<button type = "submit">변경</button>
<a class = "cancel-btn">취소</a>
</form>
</th>
</tr>
</c:if>
</c:forEach>
</tbody>
</table>