이번엔 게시판에서, 회원마다 읽은 글을 목록에 표시해주는 기능 을 구현했다.
join을 이용해서 구현했는데, 마침 조인쿼리는 경험이 적어서 많이 헤맸다.
특히, 익숙하지 않은 조인쿼리를 mybatis에서,
그것도 페이징 , 검색 , 조건부 정렬, 카테고리구분 의 기능이 적용되어있는 복잡한(내 기준 ㅜ)
게시글 목록 select문에 적용시키느라 쉽지 않았다.
적용시키려고 뷰,컨트롤러,VO,Mapper 등등 여기저기 다 건드리고,
디벨로퍼에서 수십번씩 쿼리를 써보고,
수많은 시행착오 끝에 결과적으로 조인쿼리에 대해 많이 알게됐다. 역시 직접 구르는게 최ㄱ..
Join은 실제로 많이 사용하는 쿼리라고 하니 참 좋은 경험이였던 것 같다.
코드설명에 앞서, 먼저 로직을 간단하게 설명해보면
- 회원의 읽은 목록을 저장할 테이블 생성(MP_BOARD_CHECK)
- 게시글목록(list)에서 제목을 클릭해서 게시글내용(readView)으로 이동(readView 컨트롤러 호출)
- 게시글내용 컨트롤러에 insert문을 넣어서 목록의 PR과 회원의 PR을 저장
- 목록은 List로 select문의 결과를 받아오는데, 조인쿼리를 추가해서 게시판테이블(MP_BOARD)과 저장테이블의 값을 받음
- 뷰단에서 if(목록의 PR == 저장테이블에 저장된 목록PR && 저장테이블의 회원PR == 로그인한 회원의PR) 으로 읽은 목록을 구분해서 표시
이다.
select쿼리를 이용해서 , 비로그인과 로그인시를 구분했다.
테이블 생성
create table MP_BOARD_CHECK ( CNO NUMBER NOT NULL,
MEMBER_ID VARCHAR2(30) NOT NULL,
BNO NUMBER NOT NULL,
CONSTRAINT MP_BOARD_CHECK_PR PRIMARY KEY (CNO),
CONSTRAINT MEMBER_ID_FK FOREIGN KEY(MEMBER_ID)
REFERENCES MP_MEMBER(MEMBER_ID) ON DELETE CASCADE,
CONSTRAINT BNO_FK FOREIGN KEY(BNO)
REFERENCES MP_BOARD(BNO) ON DELETE CASCADE
);
commit;
어느 회원이, 무슨 글을 읽었는지 구분을 위한,
회원ID (PR) , 게시글고유번호 (PR) 를 저장할 테이블을 생성해준다.
외래키와 delete cascade 제약조건을 추가했다.
로그인을 한 회원 (세션값에 ID가 있음)이 목록페이지(list)에서 제목을 눌러서 ,
게시글 내용페이지(readView)로 이동하기 위해 컨트롤러를 호출할때 ,
insert문을 통해 생성한 테이블에 값을 넣어준다.
readView 컨트롤러
@RequestMapping(value="/board/readView", method=RequestMethod.GET)
public String read(HttpSession session,@ModelAttribute("scri") SearchCriteria scri,BoardVO boardVO, Model model) throws Exception{
logger.info("read");
if(session.getAttribute("login") != null) {
MemberVO memberVO =(MemberVO) session.getAttribute("login");
if(service.boardCheck(boardVO.getBno(),memberVO.getMemberId())==0) {
service.insertBoardCheck(boardVO.getBno(), memberVO.getMemberId());
}
}
/*model.addAttribute("read", service.read(boardVO.getBno()));
model.addAttribute("scri", scri);
model.addAttribute("replyVO",new ReplyVO());
List<Map<String, Object>> fileList = service.selectFileList(boardVO.getBno());
model.addAttribute("file", fileList);
model.addAttribute("move", service.movePage(boardVO));
logger.info("fileList=" + fileList); */
return "/board/readView";
}
(관련없는 부분은 주석처리 했다.)
HttpSession을 매개변수로 선언해주고,
로그인 시에 부여되는 세션의 key값으로 getAttribute를 통해 null이 아니면 (로그인이 되어있으면)
회원테이블에 사용되는 VO에 세션값을 담아준다.
그 다음, 저장테이블에 중복 된 값이 있는지 확인한 후 (count가 0)
없다면 저장테이블에 값을 저장한다.
매개변수로 뷰단에서 게시글PR (bno) 과 로그인한 회원의 ID ( memberId )를 넣어준다.
Mapper.xml
<insert id="insertBoardCheck">
insert into MP_BOARD_CHECK(CNO , BNO , MEMBER_ID)
values((SELECT NVL(MAX(CNO), 0) + 1 FROM MP_BOARD_CHECK) ,#{bno} ,#{memberId})
</insert>
<select id="boardCheck" resultType="int">
select count(*) from MP_BOARD_CHECK
where MEMBER_ID = #{memberId} AND BNO = #{bno}
</select>
컨트롤러에서 매개변수로 받은 bno와 memberId를 통해,
select문으로 중복저장을 방지하고 (컨트롤러에서 count값이 0이 나와야 insert문 실행)
insert문으로 읽은 게시글의 번호와 회원의 ID를 저장한다.
DAO,DAOImpl
//조회한 게시글 표시
public void insertBoardCheck(int bno,String memberId)throws Exception;
public int boardCheck(int bno,String memberId)throws Exception;
@Override
public void insertBoardCheck(int bno,String memberId)throws Exception{
Map<String,Object> map = new HashMap<String, Object>();
map.put("memberId", memberId);
map.put("bno", bno);
sqlsession.insert("boardMapper.insertBoardCheck", map);
}
public int boardCheck(int bno,String memberId)throws Exception{
Map<String,Object> map = new HashMap<String, Object>();
map.put("memberId", memberId);
map.put("bno", bno);
return sqlsession.selectOne("boardMapper.boardCheck", map);
}
간단하다. 파라미터를 map에 담아서 넣어준다.
Service, ServiceImpl
//조회한 게시글 표시
public void insertBoardCheck(int bno,String memberId)throws Exception;
public int boardCheck(int bno,String memberId)throws Exception;
@Override
public void insertBoardCheck(int bno,String memberId)throws Exception{
dao.insertBoardCheck(bno, memberId);
}
@Override
public int boardCheck(int bno,String memberId)throws Exception{
return dao.boardCheck(bno,memberId);
}
컨트롤러에서 받은 매개변수를 dao로 전달
이제 저장테이블에 정보가 입력되었으니, 그 정보를 이용해 회원이 읽은 게시글을 구분해서 표시해주도록 하자.
저장테이블의 값을 받을 VO를 생성해준다
public class BoardCheckVO {
private int bno;
private String memberId;
private int cno;
// getter&setter 생성
}
조인을 위해 BoardVO에 생성한 VO를 추가
public class BoardVO {
private int bno;
private String title;
private String content;
private String writer;
private Date regdate;
private int hit;
private int replyhit;
private int next;
private int last;
private String nexttitle;
private String lasttitle;
private int bgnoinsert;
private int bgno;
private String id;
private int likehit;
private int hatehit;
private int idpoint;
private BoardCheckVO boardCheckVO;
//getter&setter
}
<resultMap type="kr.co.vo.BoardCheckVO" id="BoardCheckVO">
<result column="CNO" property="cno"/>
<result column="BNO" property="bno"/>
<result column="MEMBER_ID" property="memberId"/>
</resultMap>
<resultMap type="kr.co.vo.BoardVO" id="BoardVO">
<result column="BNO" property="bno"/>
<result column="TITLE" property="title"/>
<result column="WRITER" property="writer"/>
<result column="CONTENT" property="content"/>
<result column="HIT" property="hit"/>
<result column="REPLYHIT" property="replyhit"/>
<result column="REGDATE" property="regdate"/>
<result column="BGNO" property="bgno"/>
<result column="LIKEHIT" property="likehit"/>
<result column="HATEHIT" property="hatehit"/>
<result column="IDPOINT" property="idpoint"/>
<result column="ID" property="id"/>
<collection property="boardCheckVO" resultMap="BoardCheckVO"></collection>
</resultMap>
<select id="listPage" resultMap="BoardVO"
parameterType="kr.co.vo.SearchCriteria">
SELECT a.BNO,
a.TITLE,
a.CONTENT,
a.WRITER,
a.REGDATE,
a.HIT,
a.REPLYHIT,
a.BGNO,
a.LIKEHIT,
a.HATEHIT,
a.IDPOINT,
a.ID,
b.BNO,
b.MEMBER_ID
FROM (
SELECT BNO,
TITLE,
CONTENT,
WRITER,
REGDATE,
HIT ,
REPLYHIT,
BGNO,
LIKEHIT,
HATEHIT,
IDPOINT,
ID,
ROW_NUMBER() OVER(
<include refid="sort" />) AS RNUM
FROM MP_BOARD
WHERE 1=1
<include refid="search" />
<if test="bgno != 0">
and BGNO=#{bgno}
</if>
)
a left outer JOIN MP_BOARD_CHECK b
on a.bno = b.bno
<if test="memberId != null">
AND b.member_id = #{memberId}
</if>
WHERE RNUM BETWEEN #{rowStart} AND #{rowEnd}
Join 사용에 대한 포스팅은 따로 작성을 예정이다.
mybatis에서 join사용법에 대한 부분은 따로 검색을 통해 알아보도록 하자.
include 부분은 다른 기능에 관한 것이니 신경쓸 필요가 없고,
결론적으로 from 뒤에 , where 사이에 조인을 추가해주고, on으로 조인 할 범위를 잡아주면 된다.
서브쿼리로 게시판 테이블의 내용을 정렬하고, 그 값에 a태그를 붙여준다.
저장테이블엔 b태그를 붙여주고,
비 로그인시
on a.bno = b.bno (이 쿼리 자체로는 select문에 변화는 없다)
로그인시 (memberId != null)
저장테이블의 id와 로그인한 회원의 id 값이 같으면 저장테이블에서 값을 가져온다.
즉 ,
MP_BOARD a Left Outer Join MP_BOARD_CHECK b
게시판 테이블의 모든 값과
게시판 테이블에 공통되는(ON) 저장테이블의 값을 select
ON a.bno = b.bno AND b.memberId = #{memberId}
게시판 테이블의 bno와 저장테이블의 bno가 같고,
저장테이블의 ID가 로그인한 회원의 ID와 같다면 값을 출력
두개의 조건을 모두 만족하지 않으면 게시판 테이블의 모든값을 출력하지만 저장테이블은 null을 출력
즉 ,
뷰단에서 게시판 테이블의 select값과 저장테이블의 select값을 비교해야하는데 ,
저장테이블은 null값이 나와서 if문에서 빠져나오게 된다.
(저장테이블 값이 없다 = 읽지 않았다 = 목록에 표시 x)
mybatis 에서 , select문은 항상 resultType 또는 resultMap을 선언해줘야 한다.
조인을 위해 resultMap을 작성, boardVO에 조인VO를 넣어줌으로 써, boardVO resultMap으로 select결과값을 받는다.
Controller
@RequestMapping(value = "board/list", method = RequestMethod.GET)
public String list(HttpSession session,Model model, @ModelAttribute("scri") SearchCriteria scri) throws Exception{
logger.info("list");
if(session.getAttribute("login") != null) {
MemberVO memberVO =(MemberVO) session.getAttribute("login");
logger.info("memberId"+memberVO.getMemberId());
scri.setMemberId(memberVO.getMemberId());
}
model.addAttribute("list", service.list(scri));
/*model.addAttribute("notice", service.notice());
PageMaker pageMaker = new PageMaker();
pageMaker.setCri(scri);
pageMaker.setTotalCount(service.listCount(scri));
model.addAttribute("pageMaker", pageMaker);
*/
return "board/list";
}
필요없는 부분은 주석처리 하였다.
조인 쿼리에서, 회원구분을 위해 memberId 파라미터를 넣어줘야 한다
비로그인, 로그인을 구분해서
HttpSession으로 memberId를 가져오고 목록의 필요한 parameter를 전달하는 VO에
set으로 memberId를 넣어준다.
현재 내 프로젝트는 위에서 말했듯이 정렬 , 검색 , 카테고리구분 등의 기능 역할을 하는 scri를(VO)
parameterType으로 사용하기 때문에 , 따로 설명하진 않겠다.
자신의 프로젝트에 상황에 따라 세션에서 가져온 memberId를 model까지 전달해주도록 하자.
그 후 , list라는 key로 뷰로 데이터를 보낸다.
list.jsp
<c:forEach items="${list}" var="list">
<c:choose >
<c:when test="${not empty login and list.bno == list.boardCheckVO.bno and list.boardCheckVO.memberId == login.memberId}">
<tr class="table-active">
</c:when>
<c:otherwise>
<tr>
</c:otherwise>
</c:choose>
부트스트랩을 사용중인데, tr class="table-active" 는 표 테이블의 한 행을 마우스를 올렸을때 색으로 변경해준다.
if문을 사용해서 ,
not empty login --> ${login} 이 비어있지 않으면 true ( 세션값 )
list.bno == list.boardCheckVO.bno --> 게시판테이블의 bno와 저장테이블의 bno가 같으면 true
list.boardCheckVO.memberId == login.memberId --> 저장테이블의 ID 와 로그인한 회원의 ID가 같으면 true
3개의 값이 모두 참이여야 표테이블의 행의 색을 바꾼다.
즉 로그인상태, 어느 회원인지 , 회원이 읽은 게시글번호가 무엇인지를 비교한다.
결과
- '3333' 회원으로 로그인 , 게시글목록 (list)
- #1174 번 게시글을 클릭해서 내용으로 이동 (readView)
- 저장테이블에 로그인한 회원의 ID와 읽은 글의 BNO가 저장되었음
- 총 3개의 게시글을 읽고 목록으로 나온 모습
- 카테고리를 옮겨도, 같은 테이블에서 카테고리를 나눴기 때문에 읽은 게시글은 그대로 표시됨
- '11111' 회원으로 로그인 한 직후의 모습 ( 테스트하며 다 1167빼고 다 읽은 상태)
이 기능을 마무리하면서 ,
게시글내용 (readView) 페이지에 구현해놓은 이전글 , 다음글 기능에도 표시를 하는 방법이 생각났다.
[스프링]게시판 이전글 다음글 구현 :: 간편 웹프로그래밍 (tistory.com)
다음글 쿼리는, 해당 글의 카테고리의 다음글 bno를 산출한다.
컨트롤러에서, 다음글 bno를 따로 하나의 객체에 담아주고 ,
select count(*) from mp_board_check where membre_id = #{memberId} AND bno = {다음글bno}
로그인 한 회원의 ID와 다음글 BNO에 맞는 열이 저장테이블에 있다면 (글을 읽었다면 저장테이블에 insert) 1 ,
없다면 0을 리턴한다.
그 값을 resultType = int 형으로 받아서 컨트롤러에서 model에 담아서 뷰단으로 보내준다.
(model.addAttribute("nextCheck" , [결과값객체생성자]) )
<if test="${nextCheck == 0}">
<if test =${nextCheck != 0}">
이렇게 if문으로 값을 비교해서 0이 아닐때, 표시를 바꿔주면 되겠다
짜여진 코드를 보고 변형해서 사용하는 것이 아닌 , 내가 코드를 직접 짜고 , 힘들게 구현한 만큼
배운 것도 많고 더 보람찬 개발이였다.
다 좋은데 불안한 점은 개발자 커뮤니티에서 본 글이 생각난다는 것이다.
"개발을 할 때, 내가 짠 코드는 신과 나만이 알고 있고
개발을 완료하고 시간이 지나면, 내가 짠 코드는 신만이 알고 있다."
직접 구현한 만큼 잊어버려도 작성한 글을 보면 바로 생각날 것 같지만,
커뮤니티 프로젝트를 한달넘게 하면서 많은 기능들을 추가했더니 정말 가물가물해진다... 분명 내가 짯는데...
복습만이 살길...
'SPRING > IceWater Community' 카테고리의 다른 글
[스프링] 댓글 좋아요/싫어요 구현 - (Ajax) (3) | 2021.10.06 |
---|---|
[스프링] 게시판 이전글 다음글 읽은 글 표시 (1) | 2021.10.01 |
[스프링] 공지사항 구현 (0) | 2021.09.30 |
[스프링] 추천기능 구현 (OKKY사이트 추천기능 참고) (4) | 2021.09.28 |
[스프링] 회원정보수정 - 회원탈퇴 (ajax) (0) | 2021.09.25 |