테이블 조인(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">
</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">
</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 |