본문 바로가기

국비교육/국비교육

day42 - 0923

테이블 조인(Table Join)

- 둘 이상의 테이블에서 공통 필드를 기반으로 데이터 또는 행을 결합하는 것

- 두 테이블은 기본키(Primary Key, PK)와 외래키(Foreign Key, FK) 관계로 연결되어있어야 한다

-- 테이블 조인
select [열 목록] from [첫 번째 테이블] [JOIN의 종류] [두 번째 테이블] ON [조인 조건] [WHERE 검색 조건];

내부 조인(Inner Join)

- 조인 조건을 만족하는 행(교집합)만 조회

- null을 갖는 행(조인 조건을 만족하지 못하는 행)이 나올 수 없다

- INNER JOIN을 JOIN이라고 써도 INNER JOIN으로 인식한다

외부 조인(Outer Join)

 

1) Left Outer Join

- 조인 조건을 만족하는 행 뿐만 아니라 조인 조건을 만족하지 못하는 A 테이블의 모든 행도 함께 조회

 

2) Full Outer Join

- 조인 조건을 만족하는 행 뿐만 아니라 조인 조건을 만족하지 못하는 A 테이블과 B 테이블의 모든 행도 함께 조회

 

3) Right Outer Join

- 조인 조건을 만족하는 행 뿐만 아니라 조인 조건을 만족하지 못하는 B 테이블의 모든 행도 함께 조회

 

테이블 조인의 예시

- pupil 테이블의 teacher_no는 외래키로, teacher 테이블의 기본키인 teacher_no를 참조하고 있다

 

Inner Join

- 조인 조건을 만족하는 행만 조회

select * from teacher T inner join pupil P on T.teacher_no = P.teacher_no;

 

Left Outer Join

- 조인 조건을 만족하는 행 뿐만 아니라 조인 조건을 만족하지 못하는 teacher 테이블의 모든 행도 함께 조회

- teacher 테이블의 '홍길동' 행은 pupil 테이블과의 조인이 없으므로 pupil 테이블 항목의 값이 null로 출력

테이블 조인과 댓글에 회원 정보 출력

select * from teacher T left outer join pupil P on T.teacher_no = P.teacher_no;

 

Full Outer Join

- 조인 조건을 만족하는 행 뿐만 아니라 조인 조건을 만족하지 못하는 teacher와 pupil 테이블의 모든 행도 함께 조회

- teacher 테이블의 '홍길동' 행은 pupil 테이블과의 조인이 없으므로 pupil 테이블 항목의 값이 null로 출력

- pupil 테이블의 모든 행은 teacher 테이블과 조인되어 있다

select * from teacher T full outer join pupil P on T.teacher_no = P.teacher_no;

 

Right Outer Join

- 조인 조건을 만족하는 행 뿐만 아니라 조인 조건을 만족하지 못하는 pupil 테이블의 모든 행도 함께 조회

- pupil 테이블의 모든 행은 teacher 테이블과 조인되어 있다

select * from teacher T right outer join pupil P on T.teacher_no = P.teacher_no;

 


 

게시글 옆에 댓글의 갯수 출력

- 댓글(reply) 테이블의 원본글 번호(reply_origin)는 외래키로, 게시판(board) 테이블의 게시글 번호(board_no)를 참조

- 게시판(board) 테이블과 댓글(reply) 테이블의 조인 조건을 board_no = reply_origin로 한다

 

SQL문 - 게시판(board) 테이블에 댓글(reply) 테이블을 조인한 후 조회 

- 게시글에 달린 댓글의 갯수를 표시해야 한다

- 게시글에 달린 댓글이 0일 수도 있다 (= 조인 조건을 만족하지 못하는 경우)

- 따라서 게시판(board) 테이블에 댓글(reply) 테이블을 Left Outer Join해야 한다

  (조인 조건을 만족하는 행 뿐만 아니라 조인 조건을 만족하지 못하는 게시판(board) 테이블의 모든 행도 함께 조회)

 

** 게시글별 댓글의 갯수 조회 (확인용)

- 댓글(reply) 테이블에서

  댓글의 원본글 번호(reply_origin)와 

  댓글의 원본글 번호별 댓글의 갯수(count(*) over(partition by reply_origin) reply_count)를

  중복인 행을 제거하여(distinct) 조회

select distinct reply_origin, count(*) over(partition by reply_origin) reply_count from reply;

 

게시판(board) 테이블에 댓글(reply) 테이블 조인

- 게시판(board) 테이블에 댓글(reply) 테이블을 board_no = reply_origin 조건으로 Left Outer Join

  (댓글이 없는 게시글도 함께 조회하기 위해)

- 게시판 테이블의 모든 행(B.*) 뿐만 아니라

  게시글별 댓글의 갯수(count(R.reply) over(partition by B.board_no) reply_count)도 함께 조회

- 중복 제거(distinct) 

select distinct
    B.*, count(R.reply_no) over(partition by B.board_no) reply_count
from board B left outer join reply R on B.board_no = R.reply_origin;

 

BoardListVO

- 게시글에 달린 댓글의 갯수를 표시하기 위해 replyCount 필드 추가

@Data 
@Builder 
@NoArgsConstructor 
@AllArgsConstructor
public class BoardListVO {
	
	// 필드
	private int boardNo;
	private String boardWriter;
	private String boardTitle;
	private String boardContent;
	private Date boardWritetime;
	private Date boardUpdatetime;
	private int boardRead;
	private int boardLike;
	private String boardHead;	
	
	// 계층형 게시판을 위한 필드 추가
	private int boardGroup; 
	private int boardParent; 
	private int boardDepth;
	
	// 게시글에 달린 댓글의 갯수를 위한 필드 추가
	private int replyCount;
}

 

BoardDao

- 통합 조회의 반환형을 List<BoardDto>에서 List<BoardListVO>로 변경

public interface BoardDao {

	// 3) BoardListSearchVO를 이용한 통합 조회
	List<BoardListVO> selectList(BoardListSearchVO vo);
	// - 전체 조회
	List<BoardListVO> list(BoardListSearchVO vo);
	// - 검색 조회
	List<BoardListVO> search(BoardListSearchVO vo);
}

 

BoardDaoImpl

- BoardListVO에 대한 RowMapper 추가

- 통합 조회의 반환형을 List<BoardDto>에서 List<BoardListVO>로 변경

- Top N Query 안에 들어갈 SQL문을 테이블 조인 조회 구문으로 수정

@Repository
public class BoardDaoImpl implements BoardDao {

	// 의존성 주입
	@Autowired
	JdbcTemplate jdbcTemplate;
	
	// BoardListVO에 대한 RowMapper
	private RowMapper<BoardListVO> listMapper = new RowMapper<BoardListVO>() {
		@Override
		public BoardListVO mapRow(ResultSet rs, int rowNum) throws SQLException {
			return BoardListVO.builder()
						.boardNo(rs.getInt("board_no"))
						.boardTitle(rs.getString("board_title"))
						.boardContent(rs.getString("board_content"))
						.boardWriter(rs.getString("board_writer"))
						.boardHead(rs.getString("board_head"))
						.boardRead(rs.getInt("board_read"))
						.boardLike(rs.getInt("board_like"))
						.boardWritetime(rs.getDate("board_writetime"))
						.boardUpdatetime(rs.getDate("board_updatetime"))
						.boardGroup(rs.getInt("board_group"))
						.boardParent(rs.getInt("board_parent"))
						.boardDepth(rs.getInt("board_depth"))
						// 게시글에 달린 댓글의 갯수
						.replyCount(rs.getInt("reply_count"))
					.build();
		}
	};
	
	// 3) BoardListSearchVO를 이용한 통합 조회
	@Override
	public List<BoardListVO> selectList(BoardListSearchVO vo) {
		// 조회 유형
		if(vo.isSearch()) {	// 검색 조회일 경우
			return search(vo);
		}
		else {	// 그렇지 않다면
			return list(vo);
		}
	}
	
	// - 전체 조회
	@Override
	public List<BoardListVO> list(BoardListSearchVO vo) {
		String sql = "select * from ("
					+ "select rownum rn, TMP.* from ("
						+ "select * from ("
						+ "select distinct B.*, "
						+ "count(R.reply_no) over(partition by B.board_no) reply_count "
						+ "from board B left outer join reply R on B.board_no = R.reply_origin "
						+ ")"
					+ "connect by prior board_no=board_parent "
					+ "start with board_parent is null "
					+ "order siblings by board_group desc, board_no asc "
					+ ")TMP"
				+ ") where rn between ? and ?";
		Object[] param = new Object[] {vo.startRow(), vo.endRow()};
		return jdbcTemplate.query(sql, listMapper, param);
	}
	
	// - 검색 조회
	@Override
	public List<BoardListVO> search(BoardListSearchVO vo) {
		String sql = "select * from ("
				+ "select rownum rn, TMP.* from ("
					+ "select * from ("
						+ "select distinct B.*, "
						+ "count(R.reply_no) over(partition by B.board_no) reply_count "
						+ "from board B left outer join reply R on B.board_no = R.reply_origin "
					+ ")"
					+ "where instr(#1, ?) > 0 "
					+ "connect by prior board_no=board_parent "
					+ "start with board_parent is null "
					+ "order siblings by board_group desc, board_no asc "
					+ ")TMP"
				+ ") where rn between ? and ?";
		sql = sql.replace("#1", vo.getType());
		Object[] param = new Object[] {vo.getKeyword(), vo.startRow(), vo.endRow()};
		return jdbcTemplate.query(sql, listMapper, param);
	}
}

 

board 폴더의 list.jsp

- 게시글 제목 옆에 댓글의 갯수를 출력

<table border = "1" width = "900">
<thead>
	<%-- 로그인 상태에서만 글쓰기 항목이 보이도록 설정 --%>
	<c:if test = "${loginId != null}">
	<tr>
		<td align = "right" colspan = "5">
			<a href = "write">글쓰기</a>
		</td>
	</tr>
	</c:if>
	
	<tr>
		<th>번호</th>
		<th width = "45%">제목</th>
		<th>작성자</th>
		<th>작성일</th>
		<th>조회수</th>
		<th>그룹</th>
		<th>부모</th>
		<th>차수</th>
	</tr>
</thead>

<tbody align = "center">
	<c:forEach var = "boardDto" items = "${list}">
	<tr>
		<td>${boardDto.boardNo}</td>
		<td align = "left">
			<%-- 차수만큼 띄어쓰기 반복 --%>
			<c:forEach var="i" begin="1" end="${boardDto.boardDepth}" step="1">
				&nbsp;&nbsp;
			</c:forEach>
		
			<%-- 말머리가 있을 경우에만 출력 --%>
			<c:if test = "${boardDto.boardHead != null}">
				[${boardDto.boardHead}]
			</c:if>
			
			<%-- 제목을 누르면 해당 게시글의 상세 페이지로 이동하도록 --%>
			<a href = "detail?boardNo=${boardDto.boardNo}">
				${boardDto.boardTitle}
			</a>
			
			<%-- 해당 게시글에 달린 댓글의 갯수 출력 --%>
			<c:if test="${boardDto.replyCount > 0}">
				[${boardDto.replyCount}]
			</c:if>
		</td>
		<td>${boardDto.boardWriter}</td>
		<td>
			<%-- 작성 날짜를 current라는 변수로 만든다 --%>
			<c:set var = "current">
				<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "yyyy-MM-dd"/>
			</c:set>
			
			<c:choose>
				<%-- today(오늘 날짜)와 current(작성 날짜)가 같다면 시간과 분만 표시--%>
				<c:when test = "${today == current}">	
					<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "HH:mm"/>
				</c:when>
				<%-- 그렇지 않다면 년도-월-일로 표시 --%>
				<c:otherwise>
					<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "yyyy-MM-dd"/>
				</c:otherwise>
			</c:choose>
		</td>
		<td>${boardDto.boardRead}</td>
		<%-- 그룹, 상위글, 차수 추가 --%>
		<td>${boardDto.boardGroup}</td>
		<td>${boardDto.boardParent}</td>
		<td>${boardDto.boardDepth}</td>
	</tr>
	</c:forEach>
</tbody>

<c:if test = "${loginId != null}">
<tfoot>
	<tr>
		<td align = "right" colspan = "8">
			<a href = "write">글쓰기</a>
		</td>
	</tr>
</tfoot>
</c:if>

</table>

 

댓글에 해당 댓글 작성자의 닉네임과 등급 출력

- 댓글(reply) 테이블의 댓글 작성자(reply_writer)는 외래키로, 회원(member) 테이블의 회원 아이디(member_id)를 참조

- 회원(member) 테이블과 댓글(reply) 테이블의 조인 조건을 member_id = reply_writer로 한다

 

SQL문 - 댓글(reply) 테이블에 회원(member) 테이블을 조인한 후 조회 

- 댓글에 해당 댓글 작성자의 닉네임과 등급을 표시해야 한다

- 회원 탈퇴시 댓글 작성자는 null일 수도 있다 (= 조인 조건을 만족하지 못하는 경우)

- 따라서 댓글(reply) 테이블에 회원(member) 테이블을 Left Outer Join해야 한다

  (조인 조건을 만족하는 행 뿐만 아니라 조인 조건을 만족하지 못하는 댓글(reply) 테이블의 모든 행도 함께 조회)

 

댓글(reply) 테이블에 회원(member) 테이블 조인

- 댓글(reply) 테이블에 회원(member) 테이블을 member_id = reply_writer 조건으로 Left Outer Join

  (작성자가 없는 댓글도 함께 조회하기 위해)

- 댓글 테이블의 모든 행(R.*) 뿐만 아니라

  회원 테이블의 회원 닉네임(M.member_nick), 회원 등급(M.member_grade)을 함께 조회

select 
    R.*, M.member_nick, M.member_grade 
from reply R left outer join member M on R.reply_writer = M.member_id;

 

ReplyListVO

- 댓글 작성자의 닉네임, 회원 등급를 표시하기 위해 memberNick, memberGrade 추가

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ReplyListVO {

	// 필드
	private int replyNo;
	private String replyWriter;
	private int replyOrigin;
	private Date replyWritetime;
	private String replyContent;
	
	// 댓글 블라인드 상태 처리를 위한 필드
	// - 블라인드 컬럼에 저장된 char(1)의 값을 논리로 변환
	private boolean replyBlind;
	
	// 댓글 목록에 댓글 작성자의 닉네임과 등급을 표시하기 위한 필드 추가
	private String memberNick;
	private String memberGrade;
}

 

ReplyDao

- 댓글 조회의 반환형을 List<ReplyDto>에서 List<ReplyListVO>로 변경

public interface ReplyDao {

	// 추상 메소드 - 댓글 목록
	List<ReplyListVO> replyList(int replyOrigin);
}

 

ReplyDaoImpl

- ReplyListVO에 대한 RowMapper 추가

- 통합 조회의 반환형을 List<ReplyDto>에서 List<ReplyListVO>로 변경

- Top N Query 안에 들어갈 SQL문을 테이블 조인 조회 구문으로 수정

@Repository
public class ReplyDaoImpl implements ReplyDao {

	// 의존성 주입
	@Autowired
	private JdbcTemplate jdbcTemplate;
	
	// ReplyListVO에 대한 RowMapper
	private RowMapper<ReplyListVO> listMapper = new RowMapper<ReplyListVO>() {
		@Override
		public ReplyListVO mapRow(ResultSet rs, int rowNum) throws SQLException {
			return ReplyListVO.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)
						// 댓글 작성자 닉네임
						.memberNick(rs.getString("member_nick"))
						// 댓글 작성자 회원 등급
						.memberGrade(rs.getString("member_grade"))
					.build();
		}
	};

	// 추상 메소드 오버라이딩 - 댓글 목록
	@Override
	public List<ReplyListVO> replyList(int replyOrigin) {
		// 추상 메소드 오버라이딩 - 댓글 목록
	@Override
	public List<ReplyListVO> replyList(int replyOrigin) {
		String sql = "select R.*, M.member_nick, M.member_grade "
						+ "from reply R left outer join member M "
							+ "on R.reply_writer = M.member_id "
						+ "where reply_origin = ? "
						+ "order by reply_no asc";
		Object[] param = {replyOrigin};
		return jdbcTemplate.query(sql, listMapper, param);
	}
}

 

board 폴더의 detail.jsp

<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>
			<%-- 댓글 작성자의 닉네임--%>
			[${replyDto.getMemberNick()}]
			<%-- 댓글 작성자의 등급 --%>
			[${replyDto.getMemberGrade()}]
			
			<%-- 댓글 내용 --%>
			<%-- 블라인드 여부에 따라 다르게 보이도록 --%>
			<c:choose>
				<c:when test = "${replyDto.replyBlind}">
					<pre>블라인드 처리된 댓글입니다</pre>
				</c:when>
				<c:otherwise>
					<pre>${replyDto.replyContent}</pre>
				</c:otherwise>
			</c:choose>
			
			<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}">		
				<%-- 댓글의 현재 블라인드 상태에 따라 댓글 블라인드 메뉴가 다르게 보이도록 설정 --%>
				<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"> <%-- 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>

 

게시글 좋아요 

- 하나의 회원이 여러 게시글에 좋아요를 누를 수 있다

- 하나의 게시글에 여러 회원이 좋아요를 누를 수 있다

- 회원과 게시글의 관계는 N:N 관계이다

- 어떤 회원이 어떤 게시글에 좋아요를 눌렀는지를 DB에 저장해야 한다

 

테이블 생성

게시글 좋아요(member_board_like)
- 회원 아이디(member_id) : 회원(member) 테이블의 회원 아이디(member_id) 참조, 회원 탈퇴시 좋아요 기록 삭제, 반드시 입력
- 게시글 번호(board_no) : 게시판(board) 테이블의 게시글 번호(board_no) 참조, 게시글 삭제시 좋아요 기록 삭제, 반드시 입력
- 게시글 좋아요 시간(like_date) : 날짜, 기본값은 현재 시간으로, 

** 회원 아이디(member_id)와 게시글 번호(board_no)를 복합키로 설정
- 회원은 하나의 게시물에 한 번만 좋아요를 누를 수 있다
- 회원 아이디(member_id)와 게시글 번호(board_no)를 복합키로 설정하여 unique 특성을 부여한다
-- member_board_like 테이블 생성
create table member_board_like (
member_id references member(member_id) on delete cascade not null,
board_no references board(board_no) on delete cascade not null,
like_time date default sysdate not null,
primary key(member_id, board_no) -- 복합키 설정
);

 

게시글 상세 페이지에 게시글 좋아요 표시

MemberBoardLikeDto

- 회원 아이디(memberId), 게시글 번호(boardNo), 좋아요 시간(likeTime) 필드 생성

@Data 
@NoArgsConstructor 
@AllArgsConstructor 
@Builder
public class MemberBoardLikeDto {

	// 필드
	private String memberId;
	private int boardNo;
	private Date likeTime;
}

 

MemberBoardLikeDao

- 좋아요(member_board_like) 테이블에 게시글 좋아요 기록을 등록 / 삭제

- 게시글에 좋아요를 누른 상태인지 확인

public interface MemberBoardLikeDao {

	// 추상 메소드 - 게시글 좋아요 기록 등록
	void insert(MemberBoardLikeDto dto);
	
	// 추상 메소드 - 게시글 좋아요 기록 삭제
	void delete(MemberBoardLikeDto dto);
	
	// 추상 메소드 - 게시글 좋아요 여부 조회
	boolean check(MemberBoardLikeDto dto);
}

 

MemberBoardLikeDaoImpl

@Repository
public class MemberBoardLikeDaoImpl implements MemberBoardLikeDao {

	// 의존성 주입
	@Autowired
	private JdbcTemplate jdbcTemplate;
	
	// 추상 메소드 오버라이딩 - 게시글 좋아요 기록 등록
	@Override
	public void insert(MemberBoardLikeDto dto) {
		String sql = "insert into member_board_like(member_id, board_no) values(?, ?)";
		Object[] param = new Object[] {dto.getMemberId(), dto.getBoardNo()};
		jdbcTemplate.update(sql, param);
	}

	// 추상 메소드 오버라이딩 - 게시글 좋아요 기록 삭제
	@Override
	public void delete(MemberBoardLikeDto dto) {
		String sql = "delete member_board_like where member_id = ? and board_no = ?";
		Object[] param = new Object[] {dto.getMemberId(), dto.getBoardNo()};
		jdbcTemplate.update(sql, param);
	}

	// 추상 메소드 오버라이딩 - 게시글 좋아요 여부 조회
	@Override
	public boolean check(MemberBoardLikeDto dto) {
		String sql = "select count(*) from member_board_like where member_id = ? and board_no = ?";
		Object[] param = new Object[] {dto.getMemberId(), dto.getBoardNo()};
		int count = jdbcTemplate.queryForObject(sql, int.class, param);
		// 좋아요가 이미 되어 있으면 1 아니면 0 -> 좋아요가 되어 있는지(boolean)를 반환
		return count == 1;
	}
}

 

BoardController

- 로그인 상태이면 해당 게시글 좋아요 여부를 조회하여 model에 첨부

- 회원의 게시글 좋아요 기록 등록 및 삭제 Mapping 추가

@Controller
@RequestMapping("/board")
public class BoardController {

	// 의존성 주입
	@Autowired
	private MemberBoardLikeDao memberBoardLikeDao;
	
	// 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));
		
		// 2. 게시글 좋아요 여부
		// - 로그인 상태이면 해당 게시글 좋아요 여부를 조회하여 model에 첨부
		// HttpSession에서 로그인 중인 회원 아이디를 반환
		String memberId = (String) session.getAttribute("loginId");
		if(memberId != null) { // 로그인 중이라면(memberId의 값이 null이 아니라면)
			// DTO의 인스턴스 생성
			MemberBoardLikeDto memberBoardLikeDto = new MemberBoardLikeDto();
			// DTO의 회원 아이디(memberId)를 HttpSession에서 반환한 회원 아이디(memberId)로 설정 
			memberBoardLikeDto.setMemberId(memberId);
			// DTO의 게시글 번호(boardNo)를 RequestParam에서 입력받은 게시글 번호(boardNo)로 설정
			memberBoardLikeDto.setBoardNo(boardNo);
			// 설정이 끝난 memberBoardLikeDto로 게시글 좋아요 여부를 조회한 결과를 반환
			model.addAttribute("isLike", memberBoardLikeDao.check(memberBoardLikeDto));
		}
		
		// 게시글 상세 페이지(detail.jsp)로 연결
		return "board/detail";
	}
	
	// 여기서부터는 게시글 좋아요 관련
	// 1. 게시글 좋아요 기록 등록 및 삭제
	@GetMapping("/like")
	public String boardLike(@RequestParam int boardNo, HttpSession session, RedirectAttributes attr) {
		
		// HttpSession에서 로그인 중인 회원 아이디를 반환
		String memberId = (String) session.getAttribute("loginId");
		
		// DTO의 인스턴스 생성
		MemberBoardLikeDto memberBoardLikeDto = new MemberBoardLikeDto();
		// DTO의 회원 아이디(memberId)를 HttpSession에서 반환한 회원 아이디(memberId)로 설정 
		memberBoardLikeDto.setMemberId(memberId);
		// DTO의 게시글 번호(boardNo)를 RequestParam에서 입력받은 게시글 번호(boardNo)로 설정
		memberBoardLikeDto.setBoardNo(boardNo);
		
		// 게시글 좋아요 증가 / 감소
		if(memberBoardLikeDao.check(memberBoardLikeDto)) {	// 게시글에 좋아요를 누른 적이 있다면
			// 게시글 좋아요 기록 삭제
			memberBoardLikeDao.delete(memberBoardLikeDto);
		}
		else {	// 게시글에 좋아요를 누른 적이 없다면
			// 게시글 좋아요 기록 등록
			memberBoardLikeDao.insert(memberBoardLikeDto);
		}
		
		// 게시글 좋아요 처리 후 해당 게시글 상세 Mapping으로 강제 이동(redirect)
		attr.addAttribute("boardNo", boardNo);
		return "redirect:/board/detail";
	}
}

 

board 폴더의 detail.jsp

- 로그인 중인 회원의 해당 게시글에 대한 게시글 좋아요 여부(isLike)에 따라 표시되는 하트가 달라지도록 설정

- 게시글 좋아요를 누를 때 하이퍼링크로 해당 게시글 번호(boardNo)가 전달되도록 설정 (RequestParam)

<table border = "1" width = "900">
<tbody>
	<tr>
		<th width = "25%">번호</th>
		<td>${boardDto.boardNo}</td>
	</tr>
	<tr>
		<th>말머리</th>
		<td>${boardDto.boardHead}</td>
	</tr>
	<tr>
		<th>제목</th>
		<td>
			${boardDto.boardTitle}
			
			<%-- 게시글 좋아요 여부에 따라 표시되는 하트 모양 --%>
			<c:choose>
				<c:when test = "${isLike}">
					<a href = "like?boardNo=${boardDto.boardNo}">♥</a>
				</c:when>
				<c:otherwise>
					<a href = "like?boardNo=${boardDto.boardNo}">♡</a>
				</c:otherwise>
			</c:choose>
		</td>
	</tr>
	<tr>
		<th>작성자</th>
		<td>${boardDto.boardWriter}</td>
	</tr>
	<tr height = "200" valign = "top">
		<th>내용</th>
		<td>
			<%-- pre 태그는 엔터, 띄어쓰기, 탭키 등을 있는 그대로 표시하는 영역 --%>
			<pre>${boardDto.boardContent}</pre>
		</td>
	</tr>
	<tr>
		<th>조회수</th>
		<td>${boardDto.boardRead}</td>
	</tr>
	<tr>
		<th>작성일</th>
		<td>
			<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "y년 M월 d일 E요일 a h시 m분 s초"/>
		</td>
	</tr>
	<%-- 한 번이라도 수정한 적이 있다면(수정 시간이 null이 아니라면) 수정 시간을 표시 --%>
	<c:if test = "${boardDto.boardUpdatetime != null}">
		<tr>
			<th>수정일</th>
			<td>
				<fmt:formatDate value = "${boardDto.boardUpdatetime}" pattern = "y년 M월 d일 E요일 a h시 m분 s초"/>
			</td>
		</tr>
	</c:if>
</tbody>
<tfoot>
	<tr>
		<td colspan = "2" align = "right">
			<%-- 로그인 상태에서만 게시글 및 답글 작성이 가능하도록 설정 --%>
			<c:if test = "${loginId != null}">
				<a href = "write">글쓰기</a>
				<%-- 답글 작성시 하이퍼링크를 통해 boardParent(key)와 boardNo(value)를 전달 --%>
				<a href = "write?boardParent=${boardDto.boardNo}">답글쓰기</a>
			</c:if>
			
			<%-- 로그인한 회원이 게시글 작성자인지에 대한 변수(boolean) 생성 --%>
			<c:set var = "owner" value = "${loginId == boardDto.boardWriter}"></c:set>
			
			<%-- 로그인한 회원의 등급이 관리자인지에 대한 변수(boolean) 생성 --%>
			<c:set var = "admin" value = "${mg == '관리자'}"></c:set>
			
			<%-- 만약 로그인한 회원이 게시글 작성자라면 게시글을 수정할 수 있도록 설정--%>
			<c:if test = "${owner}">	
				<a href = "edit?boardNo=${boardDto.boardNo}">수정</a>
			</c:if>
			
			<%-- 만약 로그인한 회원이 게시글 작성자이거나 등급이 관리자라면 게시글을 삭제할 수 있도록 설정--%>
			<c:if test = "${owner || admin}">	
				<a href = "delete?boardNo=${boardDto.boardNo}">삭제</a>
			</c:if>
			
			<a href = "list">목록으로</a>
		</td>
	</tr>
</tfoot>
</table>

 

게시글 목록 페이지에 각각의 게시물의 좋아요 갯수 출력

MemberBoardLikeDao

- 게시글의 좋아요 갯수 반환

- 게시글의 좋아요 총 갯수를 갱신

public interface MemberBoardLikeDao {

	// 추상 메소드 - 게시글 좋아요 갯수
	int count(int boardNo);
	
	// 추상 메소드 - 게시글 좋아요 총 갯수 갱신
	void refresh(int boardNo);
}

 

MemberBoardLikeDaoImpl

- 게시글 좋아요 갯수 :

  게시글 좋아요(member_board_like) 테이블에서 특정 게시글 번호(board_no) 갯수 조회

- 게시글 좋아요 총 갯수 갱신

  게시판(board) 테이블의 좋아요(board_like)의 값을

  게시글 좋아요(member_board_like) 테이블의 특정 게시글 번호(board_no) 갯수로 설정

@Repository
public class MemberBoardLikeDaoImpl implements MemberBoardLikeDao {

	// 의존성 주입
	@Autowired
	private JdbcTemplate jdbcTemplate;
	
	// 추상 메소드 오버라이딩 - 게시글 좋아요 갯수
	@Override
	public int count(int boardNo) {
		String sql = "select count(*) from member_board_like where board_no = ?";
		Object[] param = new Object[] {boardNo};
		return jdbcTemplate.queryForObject(sql, int.class, param);
	}

	// 추상 메소드 오버라이딩 - 게시글 좋아요 총 갯수 갱신
	@Override
	public void refresh(int boardNo) {
		String sql = "update board "
				+ "set board_like = (select count(*) from member_board_like where board_no = ?) "
				+ "where board_no = ?";
		Object[] param = new Object[] {boardNo, boardNo};
		jdbcTemplate.update(sql, param);
	}
}

 

BoardController

- 해당 게시글 좋아요 총 갯수를 조회하여 model에 첨부

- 게시글 좋아요 기록 등록 및 삭제를 할 때마다 게시글 좋아요 총 갯수를 갱신

@Controller
@RequestMapping("/board")
public class BoardController {

	// 의존성 주입
	@Autowired
	private MemberBoardLikeDao memberBoardLikeDao;
	
	// 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));
		
		// 2. 게시글 좋아요 여부
		// - 로그인 상태이면 해당 게시글 좋아요 여부를 조회하여 model에 첨부
		// HttpSession에서 로그인 중인 회원 아이디를 반환
		String memberId = (String) session.getAttribute("loginId");
		if(memberId != null) { // 로그인 중이라면(memberId의 값이 null이 아니라면)
			// DTO의 인스턴스 생성
			MemberBoardLikeDto memberBoardLikeDto = new MemberBoardLikeDto();
			// DTO의 회원 아이디(memberId)를 HttpSession에서 반환한 회원 아이디(memberId)로 설정 
			memberBoardLikeDto.setMemberId(memberId);
			// DTO의 게시글 번호(boardNo)를 RequestParam에서 입력받은 게시글 번호(boardNo)로 설정
			memberBoardLikeDto.setBoardNo(boardNo);
			// 설정이 끝난 memberBoardLikeDto로 게시글 좋아요 여부를 조회한 결과를 반환
			model.addAttribute("isLike", memberBoardLikeDao.check(memberBoardLikeDto));
		}
		
		// 3. 게시글 총 좋아요 갯수 첨부
		model.addAttribute("likeCount", memberBoardLikeDao.count(boardNo));
		
		// 게시글 상세 페이지(detail.jsp)로 연결
		return "board/detail";
	}

	// 여기서부터는 게시글 좋아요 관련
	// 1. 게시글 좋아요 기록 등록 및 삭제
	@GetMapping("/like")
	public String boardLike(@RequestParam int boardNo, HttpSession session, RedirectAttributes attr) {
		
		// HttpSession에서 로그인 중인 회원 아이디를 반환
		String memberId = (String) session.getAttribute("loginId");
		
		// DTO의 인스턴스 생성
		MemberBoardLikeDto memberBoardLikeDto = new MemberBoardLikeDto();
		// DTO의 회원 아이디(memberId)를 HttpSession에서 반환한 회원 아이디(memberId)로 설정 
		memberBoardLikeDto.setMemberId(memberId);
		// DTO의 게시글 번호(boardNo)를 RequestParam에서 입력받은 게시글 번호(boardNo)로 설정
		memberBoardLikeDto.setBoardNo(boardNo);
		
		// 게시글 좋아요 증가 / 감소
		if(memberBoardLikeDao.check(memberBoardLikeDto)) {	// 게시글에 좋아요를 누른 적이 있다면
			// 게시글 좋아요 기록 삭제
			memberBoardLikeDao.delete(memberBoardLikeDto);
		}
		else {	// 게시글에 좋아요를 누른 적이 없다면
			// 게시글 좋아요 기록 등록
			memberBoardLikeDao.insert(memberBoardLikeDto);
		}
		
		// 4. 게시글 좋아요 총 갯수 갱신
		memberBoardLikeDao.refresh(boardNo);
		
		// 게시글 좋아요 처리 후 해당 게시글 상세 Mapping으로 강제 이동(redirect)
		attr.addAttribute("boardNo", boardNo);
		return "redirect:/board/detail";
	}
}

 

board 폴더의 list.jsp

- 게시글 목록에서 각각의 게시글마다 좋아요 하트와 그 갯수가 보이도록 설정

<table border = "1" width = "900">
<thead>
	<%-- 로그인 상태에서만 글쓰기 항목이 보이도록 설정 --%>
	<c:if test = "${loginId != null}">
	<tr>
		<td align = "right" colspan = "5">
			<a href = "write">글쓰기</a>
		</td>
	</tr>
	</c:if>
	
	<tr>
		<th>번호</th>
		<th width = "45%">제목</th>
		<th>작성자</th>
		<th>작성일</th>
		<th>조회수</th>
		<th>그룹</th>
		<th>부모</th>
		<th>차수</th>
	</tr>
</thead>

<tbody align = "center">
	<c:forEach var = "boardDto" items = "${list}">
	<tr>
		<td>${boardDto.boardNo}</td>
		<td align = "left">
			<%-- 차수만큼 띄어쓰기 반복 --%>
			<c:forEach var="i" begin="1" end="${boardDto.boardDepth}" step="1">
				&nbsp;&nbsp;
			</c:forEach>
		
			<%-- 말머리가 있을 경우에만 출력 --%>
			<c:if test = "${boardDto.boardHead != null}">
				[${boardDto.boardHead}]
			</c:if>
			
			<%-- 제목을 누르면 해당 게시글의 상세 페이지로 이동하도록 --%>
			<a href = "detail?boardNo=${boardDto.boardNo}">
				${boardDto.boardTitle}
			</a>
			
			<%-- 해당 게시글에 달린 댓글의 갯수 출력 --%>
			<c:if test="${boardDto.replyCount > 0}">
				[${boardDto.replyCount}]
			</c:if>
			
			<%-- 해당 게시글의 좋아요 갯수 출력 --%>
			<c:if test = "${boardDto.getBoardLike() > 0}">
				♥ ${boardDto.getBoardLike()}
			</c:if>
		</td>
		<td>${boardDto.boardWriter}</td>
		<td>
			<%-- 작성 날짜를 current라는 변수로 만든다 --%>
			<c:set var = "current">
				<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "yyyy-MM-dd"/>
			</c:set>
			
			<c:choose>
				<%-- today(오늘 날짜)와 current(작성 날짜)가 같다면 시간과 분만 표시--%>
				<c:when test = "${today == current}">	
					<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "HH:mm"/>
				</c:when>
				<%-- 그렇지 않다면 년도-월-일로 표시 --%>
				<c:otherwise>
					<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "yyyy-MM-dd"/>
				</c:otherwise>
			</c:choose>
		</td>
		<td>${boardDto.boardRead}</td>
		<%-- 그룹, 상위글, 차수 추가 --%>
		<td>${boardDto.boardGroup}</td>
		<td>${boardDto.boardParent}</td>
		<td>${boardDto.boardDepth}</td>
	</tr>
	</c:forEach>
</tbody>

<c:if test = "${loginId != null}">
<tfoot>
	<tr>
		<td align = "right" colspan = "8">
			<a href = "write">글쓰기</a>
		</td>
	</tr>
</tfoot>
</c:if>

</table>

'국비교육 > 국비교육' 카테고리의 다른 글

day44 - 0927  (0) 2022.09.27
day43 - 0926  (0) 2022.09.26
day41 - 0922  (0) 2022.09.22
day40 - 0921  (0) 2022.09.22
day39 - 0920  (0) 2022.09.20