카테고리 없음
과제 - 소통게시판 - 4
parkrams
2023. 6. 29. 20:13
728x90
AJAX와 JSON
REST 방식의 서비스
@Configuration
@EnableWebMvc
public class CustomServletConfig implements WebMvcConfigurer {
// 부트스트랩과 swagger-ui/index.html 을 모두 쓰기 위한 설정
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/js/**")
.addResourceLocations("classpath:/static/js/");
registry.addResourceHandler("/fonts/**")
.addResourceLocations("classpath:/static/fonts/");
registry.addResourceHandler("/css/**")
.addResourceLocations("classpath:/static/css/");
registry.addResourceHandler("/assets/**").
addResourceLocations("classpath:/static/assets/");
}
}
REST 방식의 댓글 처리 준비
- 단계
- URL 설계와 데이터 포맷 결정
- 컨트롤러의 JSON / XML 처리
- 동작 확인
- 자바스크립트를 통한 화면 처리
URL 설계와 DTO 설계
댓글 등록 @Valid
- 실제 동작 여부 확인 하기 위해 ReplyController 의 register() 다음과 같이 수정
@RestController
@RequestMapping("replies")
@Log4j2
public class ReplyController {
@ApiOperation(value = "Replies POST", notes = "POST 방식으로 댓글 등록")
@PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
// public ResponseEntity<Map<String, Long>> register (@RequestBody ReplyDto replyDto){
public Map<String, Long> register (@Valid @RequestBody ReplyDto replyDto,
BindingResult bindingResult) throws BindException{
log.info(replyDto + "리플라이 디티오");
if(bindingResult.hasErrors()){
throw new BindException(bindingResult);
}
Map<String, Long> resultMap = Map.of("rno" , 111L);
return resultMap;
}
}
- 수정된 사항
- ReplyDto를 수집할 때 @Valid를 적용
- BindingResult 를 파라미터로 추가하고 문제가 있을 때는 BindException을 throw 하도록 수정
- 메소드 선언부에 BindException을 throws 하도록 수정
- 메소드 리턴값에 문제가 있다면 @RestControllerAdvice가 처리할 것이므로 정상적 결과만 리턴
JPA 다대일(Many ToOne) 연관관계 실습
- 데이터베이스 상에서 PK/FK로 처리되는 관계를 JPA에서 어떻게 처리하는지 학습하고 이를 실습
연결관계를 결정하는 방법
- 데이터 베이스 에서 PK와 FK를 이용해서 엔티티 간의 관계를 표현
- 데이터베이스의 테이블을 설계하는 경우 PK를 가진 테이블을 먼저 설계 하고 이를 FK로 사용하는 테이블을 설계하는 방식이 일반적
- JPA는 객체지향 이므로 방향성을 결정하는 것이 어렵다
- JPA 연관 관계의 판단 기준을 결정할 때
- 연관 관계의 기준은 항상 변화가 많은 쪽을 기준으로 결정
- ERD의 FK를 기준으로 결정
변화가 많은 쪽을 기준
댓글 조회 / 수정 / 삭제
게시물 목록과 Projection
조인 처리
- 최종적으로 검색 조건까지 적용하고 applyPagination() 까지 적용
// 조인 처리 JPQLQery 의 leftJoin 을 이용할 때는 on 을 이용해 조인 조건 지정
// 조인 후 게시물당 처리가 필요하므로 groupBy 적용
@Override
public Page<BoardListReplyCountDto> searchWithReplyCount(String[] types, String keyword, Pageable pageable) {
QBoard board = QBoard.board;
QReply reply = QReply.reply;
JPQLQuery<Board> query = from(board);
query.leftJoin(reply).on(reply.board.eq(board));
query.groupBy(board);
// 최종
if ( (types != null && types.length > 0) && keyword != null){
BooleanBuilder booleanBuilder = new BooleanBuilder(); // (
for (String type: types){
switch (type){
case "t" :
booleanBuilder.or(board.title.contains(keyword));
break;
case "c" :
booleanBuilder.or(board.content.contains(keyword));
break;
case "w" :
booleanBuilder.or(board.writer.contains(keyword));
break;
}
} // end for
query.where(booleanBuilder);
}
//bno > 0
query.where(board.bno.gt(0L));
JPQLQuery<BoardListReplyCountDto> dtoQuery = query.select(Projections.bean(BoardListReplyCountDto.class,
board.bno,
board.title,
board.writer,
board.regDate,
reply.count().as("replyCount")
));
this.getQuerydsl().applyPagination(pageable, dtoQuery);
List<BoardListReplyCountDto> dtoList = dtoQuery.fetch();
long count = dtoQuery.fetchCount();
return new PageImpl<>(dtoList, pageable, count);
}
- 테스트 코드 작성
@Test
public void testSearchReplyCount() {
String[] types = {"t","c","w"};
String keyword = "1";
Pageable pageable = PageRequest.of(0,10, Sort.by("bno").descending());
Page<BoardListReplyCountDto> result = boardRepository.searchWithReplyCount(types, keyword, pageable );
//total pages
log.info(result.getTotalPages() + "토탈페이지");
//page size
log.info(result.getSize() + " 페이지 사이즈");
// pageNumber
log.info(result.getNumber() + " 페이지 넘버 ");
//prev next
log.info(result.hasPrevious() + " : " + result.hasNext() + "프리브넥스트");
result.getContent().forEach(board -> log.info(board));
}
모든 패키지가 있는데 dto가 있는데 오류가 생긴다?
- gradle 에 Other -> compileJava 실행 하면 오류 해결 된다.
게시물 목록 화면 처리
---------- 23-6-30----------=
댓글 서비스 계층의 구현
- Service 패키지에 ReplyService 인터페이스와 ReplyServiceImpl 클래스 추가
댓글 등록 처리
// 댓글 등록 테스트
@Test
public void testRegister(){
ReplyDto replyDto = ReplyDto.builder()
.replyText("ReplyDto Text")
.replier("replier")
.bno(100L)
.build();
// 댓글 수 출력
log.info(replyService.register(replyDto) + "댓글 등록");
}
댓글 조회 / 수정 / 삭제 / 목록
- 댓글 수정하는 경우에는 Reply 객체에서 replyText만 수정할 수 있으므로 Reply를 수정
//댓글 수정 하기 위해
public void changeText(String text){
this.replyText = text;
}
- ReplyService 인터페이스에 CRUD 기능 선언
// 등록
Long register(ReplyDto replyDto);
// 조회
ReplyDto read(Long rno);
// 수정
void modify(ReplyDto replyDto);
//삭제
void remove(Long rno);
- ReplyServiceImpl
@Override
public Long register(ReplyDto replyDto) { // 작성
Reply reply = modelMapper.map(replyDto, Reply.class);
Long rno = replyRepository.save(reply).getRno();
return rno;
}
// 댓글 읽기
@Override
public ReplyDto read(Long rno) {
Optional<Reply> replyOptional = replyRepository.findById(rno);
Reply reply = replyOptional.orElseThrow();
return modelMapper.map(reply, ReplyDto.class);
}
// 댓글 수정
@Override
public void modify(ReplyDto replyDto) {
Optional<Reply> replyOptional = replyRepository.findById(replyDto.getRno());
Reply reply = replyOptional.orElseThrow();
reply.changeText(replyDto.getReplyText()); // 댓글의 내용만 수정 가능
}
//댓글 삭제
@Override
public void remove(Long rno) {
replyRepository.deleteById(rno);
}
특정 게시물의 댓글 목록 처리
- 실제 반환 타입은 Reply가 아니라 ReplyDto 타입이므로 ReplyServiceImpl에서 이를 변환
@Override
public PageResponseDto<ReplyDto> getListOfBoard(Long bno, PageRequestDto pageRequestDto) {
Pageable pageable = PageRequest.of(pageRequestDto.getPage() <= 0 ? 0: pageRequestDto.getPage() -1,
pageRequestDto.getSize(),
Sort.by("rno").ascending());
Page<Reply> result = replyRepository.listOfBoard(bno, pageable);
List<ReplyDto> dtoList = result.getContent().stream().map(reply -> modelMapper.map(reply, ReplyDto.class))
.collect(Collectors.toList());
return PageResponseDto.<ReplyDto>withAll()
.pageRequestDto(pageRequestDto)
.DtoList(dtoList)
.total((int)result.getTotalElements())
.build();
}
컨트롤러 계층 구현
- Swagger UI를 이용해서 테스트와 함께 필요한 기능 개발
- ReplyController는 ReplyService를 주입 받도록 설계
@RequiredArgsConstructor // 의존성 주입을 위한
public class ReplyController {
private final ReplyService replyService;
등록 기능 확인
- ReplyController의 등록 기능은 이미 개발된 코드에 JSON 처리를 위해서 추가 코드 필요
- swagger-ui/index.html 들어가서 작성 테스트~
{
"bno": 100,
"replier": "마늘빵",
"replyText": "우라",
"rno": 0
}
잘못 된 상황에 대한 처리
- 클라이언트에 서버의 문제가 아니라 데이터의 문제가 있다고 전송하기 위해 @RestControllerAdvice를 이용하는 CustomRestAdvice에 DataIntegrityViolationException를 만들어서 사용자에게 예외 메시지를 전송하도록 구성
- CustomRestAdvice 에 추가
// 서버가 아니라 데이터가 문제라 클라이언트에게 알리기 위한 예외 메시지
@ExceptionHandler(DataIntegrityViolationException.class)
@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
public ResponseEntity<Map<String, String>> handleFKException(Exception e ){
// 에러로그
log.error(e);
Map<String, String> errorMap = new HashMap<>();
errorMap.put("time", " " + System.currentTimeMillis());
errorMap.put("msg", "constraint fails");
return ResponseEntity.badRequest().body(errorMap);
}
- 추가한 handleFKException()는 DataIntegrityViolationException이 발생ㅎ면 'constraint fails' 메시지를 클라이언트로 전송
- 이전 에러메시지와 다르게 400 에러 출력 ~
특정 게시물의 댓글 목록
//특정 게시물의 댓글 목록
@ApiOperation(value = "Replies of Board", notes = "GET 방식으로 특정 게시물의 댓글 목록 ")
@GetMapping(value = "/list/{bno}")
public PageResponseDto<ReplyDto> getList(@PathVariable("bno") Long bno,
PageRequestDto pageRequestDto){
PageResponseDto<ReplyDto> responseDto = replyService.getListOfBoard(bno, pageRequestDto);
return responseDto;
}
- getList()에서 bno 값은 경로에 있는 값을 취해서 사용할 것이므로 @PathVariable을 이용하고, 페이지와 관련된 정보는 일반 쿼리 스트링을 이용 ( 결과가 달라질 수 있는 부분은 일반 쿼리 스트링을 쓰고 고정 값은 URL로 고정하는 방식)
- 레포지토리 수정
- 레포지토리 listOfBoard 에서 bno 를 @Param으로 안 받아 오면 오류 생기니 반드시 확인
@Query("select r from Reply r where r.board.bno = :bno") Page<Reply> listOfBoard(@Param("bno") Long bno, Pageable pageable);
- 댓글 목록
@ApiOperation(value = "Replies of Board", notes = "GET 방식으로 특정 게시물의 댓글 목록") @GetMapping(value = "/list/{bno}") public PageResponseDto<ReplyDto> getList(@PathVariable("bno") Long bno, PageRequestDto pageRequestDto){ PageResponseDto<ReplyDto> responseDto = replyService.getListOfBoard(bno, pageRequestDto); return responseDto; }
특정 댓글 조회
- controller
@ApiOperation(value = "Read Reply", notes = "GET 방식으로 특정 댓글 조회")
@GetMapping("/{rno}")
public ReplyDto getReplyDto( @PathVariable("rno") Long rno ){
ReplyDto replyDto = replyService.read(rno);
return replyDto;
}
- swagger ui 에서 정상 적이면 ReplyDto 가 Json으로 출력
{
"rno": 3,
"bno": 1,
"replyText": "333333333",
"replier": "33333333",
"regDate": "2023-06-30 11:20:13"
}
특정 댓글 조회 - 데이터가 존재하지 않을 경우 처리
- 서비스 계층에서 조회 시 Optional<T>을 이용해 orElseThrow를 이용했기 때문에 컨트롤러에게 예외가 전달
- 이를 해결하기 위해 CustomRestAdvice를 이용 해서 예외처리 추가
특정 댓글 삭제
- 일반적으로 REST 방식에서 삭제 작업은 GET/POST가 아닌 DELETE 방식을 이용해 처리
- ReplyController에 remove() 추가
// 특정 댓글 조회 시 예외 처리
@ExceptionHandler(NoSuchElementException.class)
@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
public ResponseEntity<Map<String, String>> handleNoSuchElement (Exception e){
Map<String, String> errorMap = new HashMap<>();
errorMap.put("time", " " + System.currentTimeMillis());
errorMap.put("msg", "No Such Element Exception ");
return ResponseEntity.badRequest().body(errorMap);
}
@ApiOperation(value = "Delete Reply", notes = "DELETE 방식으로 특정 댓글 삭제")
@DeleteMapping("/{rno}")
public Map<String,Long> remove( @PathVariable("rno") Long rno ){
replyService.remove(rno);
Map<String, Long> resultMap = new HashMap<>();
resultMap.put("rno", rno);
return resultMap;
}
존재하지 않는 번호의 삭제 예외
@ExceptionHandler({NoSuchElementException.class,
EmptyResultDataAccessException.class}) // 추가
이 책의 예제는 Swagger UI를 이용했지만 REST 방식의 개발은 Postman 같은 도구도 사용 되고
크롬 확장 프로그램인 'Advanced Rest Client' 등도 무료로 사용 가능
------------------------- 23-7-1 ------------------
댓글의 자바스크립트 처리
- 자바스크립트 에서
는 Promise라는 개념을 도입해 '비동기화 된 호출을 동기화된 방식' 으로 작성할 수 있는 문법적인 장치를 만들었는데 Axios는 이를 활용하는 라이브러리 - Axios란
- node.js 와 브라우저를 위한 Promise 기반 HTTP 클라이언트
- 동형 ( 동일한 코드베이스로 브라우저와 node.js에서 실행할 수 있다
- 서버 사이드에서는 네이티브 node.js의 http 모듈을 사용하고, 클라이언트(브라우저) 에서는 XMLHttpRequests를 사용
Axios 를 위한 준비
- 자바 스크립트의 경우 read.html 에서는 주로 이벤트 관련 된 부분을 처리
- 별도의 js 파일을 작성해 Axios 를 이용한 통신을 처리하도록 구성
- static 폴더에 있는 js 폴더에 reply. js 파일을 추가
Axios 호출해 보기
- reply.js에 간단하게 Axios를 이용하는 코드를 추가
- Axios를 이용할 때 async/await를 같이 이용하면 비기 처리를 동기화 된 코드처럼 작성 가능
- async는 함수 선언 시에 사용하는데 해당 함수가 비동리를 위한 함수라는 것을 명시하기 위해 사용
- await는 함 async 함수 내에 비동기 호출 하는 부분에 사용
- reply.js
async function get1(bno) { const result = await axios.get(`/replies/list/${bno}`) console.log(result) return result; }
- read.html get1() 호출 코드
<script layout:fragment="script" th:inline="javascript">
const bno = [[${dto.bno}]]
get1(bno)
</script>
비동기 함수의 반환
- 화면에서 결과가 필요하다면 Axios의 호출 결과를 반환받아야 하기 때문에 reply.js 에서는 다음과 같이 작성
- reply.js
return result.data;
- read.html
console.log(get1(bno))
- 코드를 실행하면 예상과 달리 Promise 가 반환
- 실행결과는 console.log(get1(bno)) 이후에 실행
- 이것은 get1()이 비동기 함수이므로 get1()을 호출한 시점에선 반환할 것이 없지만 나중엔 무언가를 반환할 것이므로 반환하기로 한 '약속' 만을 반환하기 때문 ( 금융에서 약속 어음고 비슷한 개념 )
- 만일 비도기 처리되는 결과를 반환해서 처리 한다면 then()과 catch() 등을 이용해서 작성
- reply.js에서 결과를 반환하도록 구성 ;; return 에 result.data 에서 .data 제거
- read.html 에서 then 과 catch 메서드 이용
const bno = [[${dto.bno}]]
get1(bno).then(data => {
console.log(data)
}). catch(e=>{
console.error(e)
})
비동기 처리 방식의 결정
- 비동기 처리 할 때는 앞선 방법처럼 일반적인 함수와 동작 방식이 다르므로 이를 어떻게 사용해 일관성 있게 처리할 것인지를 결정
- 비동기 함수를 이용해 결과 데이터를 처리하는 방식은 크게 다음과 같다
- 비동기 함수에서는 순수하게 비동기 통신만 처리하고 호출한 쪽에서 then()이나 catch()등을 이용해서 처리
- 비동기 함수를 호출할 때 나중에 처리해야하는 내용을 같이 별도의 함수로 구성해서 파라미터로 전송
- 현재 예제 에서 비동기 통신은 reply.js가 담당하고 화면은 read.html 에서 처리하도록 구성
- reply.js는 Axios를 이용해 Ajax통신하는 부분이므로 코드의 양이 많지는 않지만 통신하는 영역과 이벤트나 화면 처리 영역을 분리하기 위해서 사용 ( 이러한 방식은 vue나 react에서도 많이 사용하는 방식이므로 미리 연습해두면 좋다, )
댓글 처리와 자바스크립트
댓글 목록 처리
- 필요한 기능 설계
- bno : 현재 게시물 번호
- page : 페이지 번호
- size : 페이지당 사이즈
- goLast : 마지막 페이지 호출 여부
- goLast를 이용해 강제적으로 마지막 댓글 페이지 호출 하도록 설정
728x90