카테고리 없음

과제 - 소통게시판 - 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