Luver Duck 2022. 9. 22. 21:48

댓글(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>