본문 바로가기

프로젝트

[SpringBoot] 경매사이트 만들기 - 페이징(JPA), 좋아요 수정했음

※수정사항

  1.  ActionBoardController의 actionDetailPost 메소드에서 전에는 게시물 번호를 String으로 매개변수로 받아서 long으로 변환해줬는데, 굳이 그럴필요가 없어서 처음부터 long으로 받도록했다.
  2. 게시물 보는사람이 회원인지 비회원인지를 구분하는 부분을 Controller에서 작성했었는데 Service 계층에서 작성하는게 관리가 쉽고, 보기 편할듯해서 ActinoBoardService에 checkMember 메소드를 만들어줬다.
  3. 이전에 게시판 작성할때 파일 업로드를 하려고 Photo 테이블을 추가했는데 수정된 ERD를 안올렸었다.. 깜빡ㅠ
  4. 좋아요 만들었을때 추가와 삭제를 모두 POST방식으로 해서 하나의 컨트롤러로 요청이 드가게 했는데, 이부분을 추가할땐 POST 요청, 삭제할땐 DELETE 요청으로 나눠서 처리했다.

1. ActionBoardController.java

package org.my.toyproject.controller;

import ...

@Controller
public class ActionBoardController {
	
	@Autowired
	private ActionBoardService actionBoardService;
	
	@Autowired
	private ActionDetailPostService actionDetailPostService;
	
	@Autowired
	private PagingService pagingService;
	
	...
	
	//게시판 상세보기 페이지 반환 컨트롤러
	@SuppressWarnings("unused")
	@GetMapping("actionDetailPost")
	public String actionDetailPost(@RequestParam long actionBoardNo, Model model) {
		ActionBoard actionBoard = actionBoardService.findPostByActionBoardNo(actionBoardNo);//요청게시물 찾기
		List<MyPick> myPickList = actionBoard.getMemberPickList();//요청게시물의 좋아요 정보
		boolean flag = actionDetailPostService.checkMember(myPickList);//회원이 보는건지, 비회원이 보는건지 확인

		//게시글이 존재하지 않을때. 
		if(actionBoard==null) return "actionboard/actionDetailPostNotExist.tiles";		
		
		model.addAttribute("actionPost",actionBoard);
		model.addAttribute("photoList", actionBoard.getPhotoList());
		model.addAttribute("totalPickCount", myPickList.size());
		model.addAttribute("myPickList", myPickList);
		model.addAttribute("memberPick",flag);
		
		return "actionboard/actionDetailPostForm.tiles";
	}
	
}

 

 2. ActionBoardService.java

package org.my.toyproject.service;

import ...

@Service
public class ActionBoardService {
	
	@Autowired
	private FileHandler fileHandler;
	
	@Autowired
	private ActionBoardRepository actionBoardRepository;
	
	@Autowired
	private PhotoRepository photoRepository;
	
	...
    
	//게시물 보는사람이 회원이지 아닌지 판단함-> 좋아요 하트를 채워줄지 말지를 결정하기 위함.
	public boolean checkMember(List<MyPick> myPickList) {
		Authentication auth = SecurityContextHolder.getContext().getAuthentication();//시큐리티 인증객체를 갖고온다.
		boolean flag = false;
		
		if(!auth.getPrincipal().equals("anonymousUser")) {
			PrincipalDetail principal = (PrincipalDetail) auth.getPrincipal();
			for(MyPick m:myPickList) {
				if(m.getMember().getMemberNo()==principal.getMemberNo()) {
					flag = true;
					break;
				}
			}
		}
		
		return flag;
	}
}

3. ERD - File 테이블 추가


4. 좋아요 추가 · 삭제 요청 나누기.

actionBoard.js

function pick(flag){
	let data={
			actionBoardNo:$("#postNo").val(),
			};
	
	if(flag==true){
		$.ajax({
				type: "DELETE",
				url: "deletePick?actionBoardNo="+$("#postNo").val(),
				//어차피 DELETE는 body사용안함.
				//contentType: "application/json; charset=utf-8",//body데이터가 어떤 타입인지(MIME)
				//dataType:"json",//응답이 왔을때의 데이터가 json모양이면 js객체로 변환.
				success: function(result) {
					//alert(result);
					document.getElementById("pickView").innerHTML='<input type="image" id="no-pick" src="bootstrap/img/kdg/no-heart.png" onclick="pick(false)">';
					document.getElementById("pickResult").innerHTML = result;
				},
				error: function(error, jqXHR, textStatus, errorThrown) {
					console.log(error);
					console.log(jqXHR);
					console.log(textStatus);
					console.log(errorThrown);
					alert("에러났어요. 다시 시도해주세요.");
				}
			});//ajax
		}else{
			$.ajax({
				type: "POST",
				url: "insertPick",
				data:JSON.stringify(data),//위의 data는 js객체라 이렇게 json으로 변경해줘야함.(글고 http body데이터임)
				contentType: "application/json; charset=utf-8",//body데이터가 어떤 타입인지(MIME)
				//dataType:"json",//응답이 왔을때의 데이터가 json모양이면 js객체로 변환.
				success: function(result) {
					//alert(result);
					document.getElementById("pickView").innerHTML='<input type="image" id="yes-pick" src="bootstrap/img/kdg/yes-heart.png" onclick="pick(true)">';
					document.getElementById("pickResult").innerHTML = result;
				},
				error: function(error, jqXHR, textStatus, errorThrown) {
					console.log(error);
					console.log(jqXHR);
					console.log(textStatus);
					console.log(errorThrown);
					alert("에러났어요. 다시 시도해주세요.");
				}
			});//ajax
		}//if문
}//pick(flag) 함수

DELETE 요청의 경우 bodydata를 사용하지 않는다. 이말은 json을 사용하지 않는다는 말과 같다. 이게 무슨 말이냐면 json을 통한 데이터 송신은 key:value 형태로 해서 body에 데이터를 담고 서버로 보내게 된다. 따라서, 서버쪽에선 @RequestBody 를 가지고 객체를 만들어서 받거나 Map형태로 받게된다. 하지만 DELETE 요청은 @RequestBody로 데이터를 받을수 없고 @Pathvariable 또는 @RequestParam 형태로 요청값을 받는다. 그렇기 때문에 ajax로 데이터를 보내줄때 json 형식이 아니라 쿼리스트링을 통해 값을 넘겨준다.

 

MyPickApiController.java

package org.my.toyproject.controller.api;

import ...

@RestController
public class MyPickApiController {
	
	@Autowired
	private MyPickService myPickService;
	
	
	//좋아요 추가하는 컨트롤러, 반환값은 좋아요 총개수
	@Secured("ROLE_MEMBER")
	@PostMapping("insertPick")
	public ResponseEntity<Integer> updatePick(@AuthenticationPrincipal PrincipalDetail principal,
			@RequestBody MyPickUpdateVO myPickUpdateVO) {
		
		myPickService.savePick(myPickUpdateVO,principal);
		
		//지금 게시물의 좋아요 정보
		List<MyPick> pickList = myPickService.actionBoardNoByPickcnt(myPickUpdateVO.getActionBoardNo());
		int cnt = pickList.size();//총개수
		
		return new ResponseEntity<Integer>(cnt,HttpStatus.OK);
	}
	
	//좋아요 삭제하는 컨트롤러, 반환값은 좋아요 총개수
	@Secured("ROLE_MEMBER")
	@DeleteMapping("deletePick")
	public ResponseEntity<Integer> deletePick(@AuthenticationPrincipal PrincipalDetail principal,
			@RequestParam long actionBoardNo) {
		
		myPickService.deletePick(actionBoardNo,principal);
		
		//지금 게시물의 좋아요 정보
		List<MyPick> pickList = myPickService.actionBoardNoByPickcnt(actionBoardNo);
		int cnt = pickList.size();//총개수
		
		return new ResponseEntity<Integer>(cnt,HttpStatus.OK);
	}
}

 

MyPickUpdateVO.java

package org.my.toyproject.vo;

import ...

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MyPickUpdateVO {
	private long actionBoardNo;
}

 

JPA를 이용해서 페이징하기!

JPA를 사용하지 않을때 Paging을 하기 위해선 Paging클래스를 만들고 하나부터 열까지 모든 로직을 직접 작성해야 할수 있었다. 하지만 JPA에선 이런부분을 손쉽게 해결할 수 있는데 @PageableDefault 라는걸 사용하면 된다.

먼저 @PageableDefault를 사용하기전 최소한의 정보를 알고 드가자~

 


@PageableDefault

@PageableDefault은 JPA로 페이징할때 손쉽게 해주기 위한 어노테이션으로 실제로 사용할땐 매개변수 부분에 추가하고, 변수는 Pageable이란 인터페이스로 받아줘야한다. 기본적으로 @PageableDefault에 설정할수 있는 값들을 보면 아래와 같다.

  • size : 한페이당 최대 게시물 수
  • sort : 페이징할때 기준이되는 column
  • direction : ORDER BY의 역할( asc, desc )

나는 이거 3개만 설정하고 나머지는 pageable을 이용한 객체를 만들어서 진행했고, 추가적인 부분은 PagingVO를 만들어가지고 페이징을 해줬다~~

그리고 pageable을 이용한 객체는 repository를 이용한 findAll() 메소드로 만들수 있다.

	@Transactional
	public Page<ActionBoard> pageList(Pageable pageable) {
		return actionBoardRepository.findAll(pageable);
	}

쉽게 말해서 코드를 보면 위와같은데 매우 쉽지않나요? jpa를 처음 사용했을때 객체를가지고 테이블을 조작할때 진짜 좋다라고 느꼈는데, 페이징 부분에서 다시한번 파워풀한걸 느낍니다 ㄷㄷ..

 

결과

설명하자면

한페이지당 게시물수는 6개로 설정

페이지그룹은 5페이지가 하나의 그룹이 되도록했음.(여기선 게시물이 총7개밖에 없어서 2페이지까지 밖에 안나옴...)

만약 총페이지 개수가 5개가 넘어가면 화살표도 나옴. 즉, 총 7페이지라면 1 2 3 4 5 6 7 이 아니라 1 2 3 4 5 > 이렇게 나옴.

 

그럼 코드를 하나씩 살펴보자.


1. ActionBoardController

	//경매게시판 리스트 페이지 반환 컨트롤러
	@GetMapping("actionBoardListForm")
	public String actionBoardListForm(@PageableDefault(size = 6, sort = "actionBoardNo", direction = Direction.DESC) Pageable pageable,
			Model model) {//@PageableDefault -> size: 한페이지에 게시물수 / sort: 정렬 기준 / direction: 정렬 방법
		Page<ActionBoard> pageList = actionBoardService.pageList(pageable);//경매게시글에 대한 페이징 객체
		List<ActionBoard> postList = pageList.getContent();//페이징 객체에 있는 내용물들
		//게시판 리스트에서 페이지 숫자 및 화살표 표시해주기 위한 정보를 담은 객체
		PagingVO paging = pagingService.pagingInfo(pageList);
		
		model.addAttribute("postList", postList);
		model.addAttribute("paging", paging);
		model.addAttribute("preview", paging.getPreviousPageGroupOfPage());
		model.addAttribute("next", paging.getNextPageGroupOfPage());

		return "actionboard/actionBoardList.tiles";
	}

 

 

2. ActionBoardService

	@Transactional
	public Page<ActionBoard> pageList(Pageable pageable) {
		return actionBoardRepository.findAll(pageable);//페이징 객체만들어서 반환
	}

 

 

3. PagingVO

package org.my.toyproject.vo;

import ...

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PagingVO {
	private int pageGroupPerPageCnt; //페이지 그룹당 페이지 개수
	private int nowPage; //현재 페이지
	private int nowPageGroup; //현재 페이지 그룹
	private int nowPageGroupStartPage; //현재 페이지 그룹에서 시작 페이지
	private int nowPageGroupEndPage; //현재 페이지 그룹에서 마지막 페이지
	private boolean hasPrevious; //이전 페이지 그룹이 있나?
	private boolean hasNext; //다음 페이지 그룹이 있나?
	private int previousPageGroupOfPage; //이전 페이지 그룹에서 첫번째 페이지
	private int nextPageGroupOfPage; //다음 페이지 그룹에서 마지막 페이지
	
}

 

 

4. PagingService -> 게시판에서 아래 번호와 화살표를 표시하기 위한 로직

package org.my.toyproject.service;

import ...
/**
 * - 페이징에대한 정보를 반환해주는 서비스 -
 * 페이지 그룹당 페이지 수: pageGroupPerPageCnt
 * 현재 페이지: nowPage
 * 현재 페이지 그룹: nowPageGroup
 * 현재 페이지 그룹에서 시작 페이지: nowPageGroupStartPage
 * 현재 페이지 그룹에서 마지막 페이지: nowPageGroupEndPage
 * @author USER
 *
 */
@Service
public class PagingService {
	
	public PagingVO pagingInfo(Page<ActionBoard> pageList) {
		PagingVO pagingVO = null;
		
		//페이지 그룹당 페이지 수
		int pageGroupPerPageCnt = 5; 
		
		//현재 페이지(jpa는 페이지를 0부터 시작하기 때문에 +1을 해서 보여줌)
		int nowPage = pageList.getNumber()+1;
		
		//현재 페이지 그룹
		int nowPageGroup = 0; 
		if(nowPage%pageGroupPerPageCnt==0) nowPageGroup = nowPage/pageGroupPerPageCnt;
		else nowPageGroup = nowPage/pageGroupPerPageCnt+1;
		
		//현재페이지 그룹에서 시작 페이지
		int nowPageGroupStartPage = (nowPageGroup-1)*pageGroupPerPageCnt+1;
		
		//현재페이지 그룹에서 마지막 페이지
		int nowPageGroupEndPage = nowPageGroup*pageGroupPerPageCnt; 
		if(pageList.getTotalPages()<nowPageGroupEndPage)
			nowPageGroupEndPage = pageList.getTotalPages();
		
		//현재 페이지그룹에서 이전 페이지 그룹 있으면 true
		boolean hasPrevious = true;
		if(nowPageGroupStartPage==1) hasPrevious = false;
		
		//현재 페이지그룹에서 다음 페이지 그룹 있으면 true 
		boolean hasNext = true;
		if(nowPageGroupEndPage==pageList.getTotalPages()) hasNext = false;
		
		//마지막 페이지그룹
		int endPageGroup = 0;
		if(pageList.getTotalPages()%pageGroupPerPageCnt==0) 
			endPageGroup = pageList.getTotalPages()/pageGroupPerPageCnt;
		else endPageGroup = pageList.getTotalPages()/pageGroupPerPageCnt+1;
		
		//이전페이지 그룹의 첫번째 페이지
		int previousPageGroupOfPage = 0;
		if(nowPageGroup!=1) previousPageGroupOfPage = (nowPageGroup-2)*pageGroupPerPageCnt;
		
		//다음페이지 그룹의 첫번째 페이지
		int nextPageGroupOfPage = 1;
		if(nowPageGroup!=endPageGroup) nextPageGroupOfPage = nowPageGroup*pageGroupPerPageCnt;
		
		//페이징 객체 생성
		pagingVO = new PagingVO(
				pageGroupPerPageCnt, nowPage, nowPageGroup, nowPageGroupStartPage, 
				nowPageGroupEndPage, hasPrevious, hasNext,
				previousPageGroupOfPage, nextPageGroupOfPage);
		return pagingVO;
	}
}

 

 

5. actionBoardList.jsp -> 경매게시판에서 게시물들의 리스트를 보여줌.

       <c:set value="${requestScope.paging }" var="paging"></c:set>
        <div class="row">
        	<!-- 게시글 하나하나를 다뽑아서 보여주기 -->
	        <c:forEach var="postList" items="${requestScope.postList}">
	          <div class="col-lg-4 col-md-6 d-flex align-items-stretch" data-aos="zoom-in" data-aos-delay="100">
	            <div class="img-box">
	              <a href="${pageContext.request.contextPath}/actionDetailPost?actionBoardNo=${postList.actionBoardNo }">
	              	<img src="/bootstrap/img/kdg/JoinImg.jpeg" alt="Imagin Error" class="img-thumbnail">
	              </a>
	              <h4>
	              	<a href="${pageContext.request.contextPath}/actionDetailPost?actionBoardNo=${postList.actionBoardNo }">
	              		${postList.actionBoardTitle }
	              	</a>
	              </h4>
	              <p>${postList.actionBoardcontent }</p>
	            </div>
	          </div>
	        </c:forEach>
	        
	        <!-- 글쓰기 버튼 -->
	        <div class="row">
		        <div class="text-center my-3">
					<button class="kdg-write-button-css" onclick="location.href='actionBoardWriteForm'">Write Post</button>
				</div>
			</div>
			
			<!-- 페이징 처리 -->
			<div class="text-center">
				<!-- 이전 페이지 그룹 있나? -->
				<c:if test="${paging.hasPrevious==true }">
					<a href="?page=${preview }"><i class="fas fa-lg fa-angle-double-left"></i></a>
					&nbsp;
				</c:if>
				<!-- 페이지 번호 표시 -->
				<c:forEach var="pagingNumber" begin="${paging.nowPageGroupStartPage }" end="${paging.nowPageGroupEndPage }">
					<c:choose>
						<c:when test="${pagingNumber==paging.nowPage }">
							<i class="fas fa-lg fa-sort-numeric-up-alt">
								<a href="?page=${pagingNumber-1 }" class="board-number-change-color abncc">${pagingNumber }</a>
							</i>
							&nbsp;
						</c:when>
						<c:otherwise>
							<i class="fas fa-lg fa-sort-numeric-up-alt">
								<a href="?page=${pagingNumber-1 }" class="board-number-change-color">${pagingNumber }</a>
							</i>
							&nbsp;
						</c:otherwise>
					</c:choose>
				</c:forEach>
				<!-- 다음 페이지 그룹 있나? -->
				<c:if test="${paging.hasNext==true }">
					<a href="?page=${requestScope.next }"><i class="fas fa-lg fa-angle-double-right"></i></a>
				</c:if>
			</div>
		</div>

참고로 JPA를 사용할때 주의할점은 JPA는 page를 0페이지부터 시작한다는걸 알아야한다. 이부분은 직접 해보면서 URL부분을 보면 쉽게 알수 있다. 

1페이지 일때 URL은 아래와 같다.

page=0 부터 시작하는걸 볼수 있다.

이부분만 주의한다면 누구나 쉽게 페이징 할수 있을듯!

 

 

★이슈사항

1. 문제

좋아요 수정할때 DELETE 요청이 들어왔을때도 서버에서 json형태로 데이터를 받으려고 했는데 에러남.. 이유는 처음에 설명한것과 같음.

 

해결

DELETE 요청 데이터를 @RequestParam으로 받아서 해결.

 

2. 문제

서버에서 클라이언트 쪽으로 좋아요 총개수를 반환하는 부분에서 Integer로 반환했는데, 이걸 클라이언트 쪽에서 json모양 객체로 받으려하니깐 STATUSTEXT PARSERERROR 라는 에러가 발생했음. 사실 이부분은 당연함.

응답데이터가 key:value 형태가 아니라 cnt하나만 넘겨줬기 때문에 json으로 받아질리가 없었음...ㅋㅋㅋ큐

 

해결

받는 데이터 타입을 정해주는 dataType을 없애버림.

 

 

★다음에 할꺼

결제 서비스 만들기!

--> 결제 서비스를 즉시구매에 대한 부분부터 만드려고함!!