8.  수정 - 제목, 내용만

  • DAO에 수정하는 updateOne() 메서드 작성
  • 테스트 코드 작성 후 테스트
  • Service 작업
  • 컨트롤러에 modify 추가
  • modify.jsp 작성
public interface BoardMapper {

    void insert(BoardVO boardVO); // DB 저장하는 메소드

    List<BoardVO> selectAll();

    BoardVO selectOne(Integer no);

    void updateHit(Integer no);

    void updateOne(BoardVO boardVO);
}
    <update id="updateOne">
        UPDATE tbl_board SET title = #{title}, content= #{content} WHERE no = #{no}
    </update>
    @Test
    public void updateOne() {
        Integer no = 8;
        BoardVO boardVO = BoardVO.builder()
                .no(no)
                .title("수정")
                .content("수정된 내용...")
                .build();

        boardMapper.updateOne(boardVO);
        log.info(boardMapper.selectOne(no));
    }

public interface BoardService {

    void register(BoardDTO boardDTO);

    List<BoardDTO> getAll();

    BoardDTO getOne(Integer no);

    void updateOne(BoardDTO boardDTO);
}
    @Override
    public void updateOne(BoardDTO boardDTO) {
        log.info("update...");

        BoardVO boardVO = modelMapper.map(boardDTO, BoardVO.class);
        log.info(boardVO);
        boardMapper.updateOne(boardVO);

    }
    @PostMapping("/modify")
    public String modify(@Valid BoardDTO boardDTO,
                         BindingResult bindingResult,
                         RedirectAttributes redirectAttributes) {

        if (bindingResult.hasErrors()) {
            // 유효성 검사 결과 에러가 있으면 수정 페이지로 돌아감
            log.info("has error");

            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            redirectAttributes.addAttribute("no", boardDTO.getNo());
            return "redirect:/board/modify";
        }
        boardService.updateOne(boardDTO);
        return "redirect:/board/read?no=" + boardDTO.getNo();
    }
<div class="card-body">
    <form action="/board/modify" method="post">
    <input type="hidden" name="no" value="${dto.no}">
    <br>
    <div class="input-group">
        <span class="input-group-text">Title</span>
            <input type="text" name="title" class="form-control" value="${dto.title}">
    </div><br>
    <div class="input-group">
        <span class="input-group-text">Content</span>
        <textarea name="content" class="form-control">${dto.content}</textarea>
    </div><br>
    <div class="input-group">
        <span class="input-group-text">Writer</span>
        <input type="text" name="writer" class="form-control" value="${dto.writer}" readonly>
    </div><br>
    <div class="input-group">
        <span class="input-group-text">Date</span>
        <input type="date" name="dueDate" class="form-control" value="${dto.addDate}" readonly>
    </div><br>
    <div class="my-4">
        <div class="float-end">
            <button type="button" class="btn btn-danger">Remove</button>
            <button type="button" class="btn btn-primary">Modify</button>
            <button type="button" class="btn btn-secondary">List</button>
        </div>
    </div>
    </form>
    
    <script>
        const serverValidResult = {};
        <c:forEach items="${errors}" var="error">
            serverValidResult['${error.getField()}'] = '${error.defaultMessage}';
        </c:forEach>
        console.log(serverValidResult);
    </script>
                    
    <script>
        const frmModify = document.querySelector('form');
        document.querySelector('.btn-danger').addEventListener('click', function () {
            frmModify.action = '/board/remove';
            frmModify.method = 'post';
            frmModify.submit();
        });
        document.querySelector('.btn-primary').addEventListener('click', function () {
            frmModify.action = '/board/modify';
            frmModify.method = 'post';
            frmModify.submit();
        });
        document.querySelector('.btn-secondary').addEventListener('click', function (e) {
            self.location = `/board/list?${boardDTO.link}`;
        });
    </script>
</div>

 


 

9.  삭제

  • DAO에 삭제하는 deleteOne() 메서드 작성
  • 테스트 코드 작성 후 테스트
  • Service 작업
  • 컨트롤러에 remove 추가
public interface BoardMapper {

    void insert(BoardVO boardVO); // DB 저장하는 메소드

    List<BoardVO> selectAll();

    BoardVO selectOne(Integer no);

    void updateHit(Integer no);

    void updateOne(BoardVO boardVO);

    void deleteOne(Integer no);
}
    <delete id="deleteOne">
        DELETE FROM tbl_board WHERE no = #{no}
    </delete>
    @Test
    public void deleteOne() {
        Integer no = 8;
        BoardVO boardVO = boardMapper.selectOne(no);
        log.info(boardVO);

        boardMapper.deleteOne(no); // 삭제

        boardVO = boardMapper.selectOne(no);
        log.info(boardVO);
    }

public interface BoardService {

    void register(BoardDTO boardDTO);

    List<BoardDTO> getAll();

    BoardDTO getOne(Integer no);

    void updateOne(BoardDTO boardDTO);

    void deleteOne(Integer no);

}
    @Override
    public void deleteOne(Integer no) {
        boardMapper.deleteOne(no);
    }
    @PostMapping("/remove")
    public String remove(Integer no, RedirectAttributes redirectAttributes) {
        log.info("-----remove----");
        log.info("no: " + no);

        boardService.deleteOne(no);
        return "redirect:/board/list";
    }

 

 


 

10.  비밀번호 확인

  • PasswdVO 작성
  • BoardMapper 인터페이스 작성
  • BoardMapperxml 작성
  • BoardMapperTest 클래스 작성
  • PasswdDTO 작성
  • BoardService 작성
  • BoardServiceImpl 작성
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PasswdVO {
    private int no;
    private String passwd;
}
public interface BoardMapper {

    ...

    BoardVO selectOneByPasswd(PasswdVO passwdVO);
}
    <select id="selectOneByPasswd" resultType="com.example.spring_ex_01_2404.domain.BoardVO">
        SELECT * FROM tbl_board WHERE no = #{no} AND passwd = SHA2(#{passwd}, 256)
    </select>
    @Test
    public void selectOneByPasswd() {
        PasswdVO passwdVO = PasswdVO.builder()
                .no(12)
                .passwd("1234").build();
        BoardVO boardVO = boardMapper.selectOneByPasswd(passwdVO);

        log.info(boardVO);
    }

@ToString
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PasswdDTO {
    private Integer no;
    private String passwd;
}
public interface BoardService {
    ...
    
    boolean isCurrentPasswd(PasswdDTO passwdDTO);
}
 @Override
    public boolean isCurrentPasswd(PasswdDTO passwdDTO) {
        PasswdVO passwdVO = modelMapper.map(passwdDTO, PasswdVO.class);

        return boardMapper.selectOneByPasswd(passwdVO) != null;
    }
    @Test
    public void isCurrentPasswd() {
        PasswdDTO passwdDTO = PasswdDTO.builder()
                .no(12)
                .passwd("1234").build();
        log.info(boardService.isCurrentPasswd(passwdDTO));
    }

 

JSP 코드 - 수정 or 삭제에 따라 버튼 내용 및 문구 타이틀 변경 추가
 <div class="card-body">
    <h3>${reason == "incorrect" ? "틀린 비밀번호 입니다." : "비밀번호를 입력해주세요!"}</h3>
    <form action="/board/passwd" method="post">
        <input type="hidden" name="no" value="${no}">
        <input type="hidden" name="mode" value="${mode}">
        <div class="input-group">
            <span class="input-group-text">Password</span>
            <input type="password" name="passwd" placeholder="비밀번호">
        </div><br>
        <div class="my-4">
            <div class="float-end">
                <button type="submit" class="btn btn-danger">${mode == "remove" ? "삭제" : "수정"}</button>
            </div>
        </div>
    </form>
</div>

 

Controller - 수정, 삭제 하기 전 비밀번호 확인 코드
    @GetMapping("/modify")
    public String modify(Integer no, Model model,
                       HttpServletRequest request,
                       RedirectAttributes redirectAttributes) {

        // 1. 세션에 저장된 비밀번호를 불러옴.
        HttpSession session = request.getSession();
        String passwd = (String) session.getAttribute("passwd");

        // 2. 비밀번호가 없으면 비밀번호 입력 페이지로 리다이렉트
        if (passwd == null || passwd.isEmpty()) {
            redirectAttributes.addAttribute("no", no);
            redirectAttributes.addAttribute("mode", "modify");
            return "redirect:/board/passwd";
        }

        // 3. 비밀번호가 틀리면 비밀번호 입력 페이지로 리다이렉트
        if (!boardService.isCurrentPasswd(PasswdDTO.builder().no(no).passwd(passwd).build())) {
            session.removeAttribute("passwd");
            redirectAttributes.addAttribute("no", no);
            redirectAttributes.addAttribute("mode", "modify");
            redirectAttributes.addFlashAttribute("reason", "incorrect");
            return "redirect:/board/passwd";
        }

        BoardDTO boardDTO = boardService.getOne(no);
        model.addAttribute("dto", boardDTO);
        
        return "/board/modify";
    }

    @RequestMapping("/remove")
    public String remove(Integer no, HttpServletRequest request, RedirectAttributes redirectAttributes) {

        // 1. 세션에 저장된 비밀번호를 불러옴.
        HttpSession session = request.getSession();
        String passwd = (String) session.getAttribute("passwd");

        // 2. 비밀번호가 없으면 비밀번호 입력 페이지로 리다이렉트
        if (passwd == null || passwd.isEmpty()) {
            redirectAttributes.addAttribute("no", no);
            redirectAttributes.addAttribute("mode", "remove");
            return "redirect:/board/passwd";
        }

        // 3. 비밀번호가 틀리면 비밀번호 입력 페이지로 리다이렉트
        if (!boardService.isCurrentPasswd(PasswdDTO.builder().no(no).passwd(passwd).build())) {
            session.removeAttribute("passwd");
            redirectAttributes.addAttribute("no", no);
            redirectAttributes.addAttribute("mode", "remove");
            redirectAttributes.addFlashAttribute("reason", "incorrect");
            
            return "redirect:/board/passwd";

        }

        // 4. 게시물 삭제
        log.info("-----remove----");
        boardService.deleteOne(no);

        // 5. 삭제 후 비밀번호 삭제
        session.removeAttribute("passwd");

        return "redirect:/board/list";
    }

    // 비밀번호 입력 페이지
    @GetMapping("/passwd")
    public void passwd(Integer no, String mode, Model model) {
        model.addAttribute("no", no);
        model.addAttribute("mode", mode);
    }

    @PostMapping("/passwd")
    public String passwdPost(Integer no, String mode, String passwd, HttpServletRequest request,
                             RedirectAttributes redirectAttributes) {
        log.info("...passwdPost()");
        log.info(mode);

        // 1. 전달받은 비밀번호를 세션에 저장
        HttpSession session = request.getSession();
        session.setAttribute("passwd", passwd);

        // 2. 해당 처리 페이지로 리다이렉트
        redirectAttributes.addAttribute("no", no);
        return "redirect:/board/" + mode;
    }

 

 

 

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]

 


 

💡  DTO : 컨트롤러에서 서비스로, 혹은 서비스에서 컨트롤러로 데이터를 주고 받을 때 사용
       VO : 서비스는 DAO와 데이터 주고 받을 때는 VO를 사용하기 때문에 서비스에서 DTO ↔ VO 작업이 필요
  • DTO : 컨트롤러 ↔ 서비스
  • VO : 서비스 ↔ DAO

1.  콘솔창에서 데이터베이스 작업 

CREATE TABLE `tbl_board` (
    `no` int auto_increment primary key,  
    `title` varchar(100) not null,     // 제목
    `content` text not null ,          // 내용
    `writer` varchar(50) not null ,    // 작성자
    `passwd` varchar(100) not null ,   // 비밀번호
    `addDate` datetime,                // 작성날짜
    `hit` int default 0                // 조회수
);

 


 

2.  BoardVO 작성 

테이블에 저장할 데이터를 담거나, 테이블에서 들고온 데이터를 담는 용도

주로 DAO (데이터베이스 처리하는 파트) 에서 사용

@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class BoardVO {

    private Integer no;
    private String title;      // 제목
    private String content;    // 내용
    private String writer;     // 작성자
    private String passwd;     // 비밀번호
    private LocalDate addDate; // 작성일
    private Integer hit;       // 조회수

}

 


3.  VO를 이용해서 데이터베이스에 저장하는 테스트 코드 작성

 작성 순서

 

  1)  BoardMapper 인터페이스 작성 - 객체 받아 데이터베이스에 저장, 수정, 삭제를 실행하는 메소드 작성

  2) BoardMapperxml 작성 - 데이터베이스 쿼리문 작성

  3)  BoardMapperTest 클래스 작성 - 실행 테스트

 

 

public interface BoardMapper {
    void insert(BoardVO boardVO); // DB 저장하는 메소드
}

 

<mapper namespace="com.example.spring_ex_01_2404.mapper.BoardMapper">
    <insert id="insert">
        INSERT INTO tbl_board (title, content, writer, passwd, addDate)
        VALUES (#{title}, #{content}, #{writer}, SHA2(#{passwd},256), now())
    </insert>
</mapper>

 

  ✓  mysql로 비밀번호 암호화 할 경우 SHA2(입력값, 256) 함수 사용

  ✓ addDate는 글 작성 시간이 들어가니 now() 함수를 사용

  ✓ hit는 작성시에는 0이니 컬럼의 기본값을 사용

@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
class BoardMapperTest {
    @Autowired(required = false)
    private BoardMapper boardMapper;

    @Test
    public void testInsert() {
        BoardVO boardVO = BoardVO.builder() // 빌더를 이용해서 BoardVO 객체를 생성
                .title("스프링 테스트")
                .content("스프링 테스트 중...")
                .writer("river")
                .passwd("1234")
                .addDate(now())
                .build();
        boardMapper.insert(boardVO);
    }
}
02:34:06 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.insert] ==> Preparing: INSERT INTO tbl_board (title, content, writer, passwd, addDate) VALUES (?, ?, ?, SHA2(?,256), now())
02:34:06 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.insert] ==> Parameters: 스프링 테스트(String), 스프링 테스트 중...(String), river(String), 1234(String)
02:34:06 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.insert] <== Updates: 1

 


4.  Service 작업

BoardDTO → BoardService → BoardServiceImpl → 테스트 코드
@ToString
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BoardDTO {
    private Integer no;
    private String title;
    private String content;
    private String writer;
    private String passwd;
    private LocalDate addDate;
    private Integer hit;
}

 

public interface BoardService {

    void register(BoardDTO boardDTO);
}

 

@Log4j2
@Service
@RequiredArgsConstructor // 생성자 객체 주입. private final로 선언된 참조변수에 객체를 저장하는 생성자 작성.
public class BoardServiceImpl implements BoardService{
    private final BoardMapper boardMapper;
    private final ModelMapper modelMapper;

    @Override
    public void register(BoardDTO boardDTO) {
        log.info(boardDTO);
        BoardVO boardVO = modelMapper.map(boardDTO, BoardVO.class);
        log.info(boardVO);
        boardMapper.insert(boardVO);
    }
}

 

@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
class BoardServiceImplTest {
    @Autowired
    private BoardService boardService;

    @Test
    public void testRegister() {
        BoardDTO boardDTO = BoardDTO.builder()
                .title("service test")
                .content("service test...")
                .writer("user")
                .passwd("12345")
                .addDate(LocalDate.now())
                .build();
        boardService.register(boardDTO);
    }
}
03:23:33 INFO [com.example.spring_ex_01_2404.service.BoardServiceImpl] BoardDTO(no=null, title=service test, content=service test..., writer=user, passwd=12345, addDate=2024-05-01, hit=null)
03:23:33 INFO [com.example.spring_ex_01_2404.service.BoardServiceImpl] BoardVO(no=null, title=service test, content=service test..., writer=user, passwd=12345, addDate=2024-05-01, hit=null)
03:23:33 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.insert] ==> Preparing: INSERT INTO tbl_board (title, content, writer, passwd, addDate) VALUES (?, ?, ?, SHA2(?,256), now())
03:23:33 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.insert] ==> Parameters: service test(String), service test...(String), user(String), 12345(String)
03:23:33 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.insert] <== Updates: 1

 


 

5.  등록페이지 작성

  • /board/add로 접근할 경우 등록 페이지 출력
  • 등록페이지에서 입력 후 submit을 하면 데이터베이스에 글이 등록
작업순서 
  • BoardController 작성
  • get으로 /board/add에 접근할 경우 실행할 메서드 작성
  • /board/add로 정상적으로 접근되는지 확인
  • webapp에 add.jsp 코딩
  • controller에 post로 /board/add에 접근할 경우 실행할 메서드 작성
  • 테스트 후 입력한 데이터가 제대로 전달되는지 확인
  • addPost()를 수정해서 서비스로 연결
  • 데이터베이스에 저장 확인
  • addPost()를 수정해서 /board/list 이동 확인
@Log4j2
@Controller
@RequestMapping("/board")
@RequiredArgsConstructor
public class BoardController {
    private final BoardService boardService;

    @GetMapping("/add")
    public void add() {
        log.info("add board...");
    }

    @PostMapping("/add")
    public String addPost(@Valid BoardDTO boardDTO, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
        log.info("board addPost()...");

        if (bindingResult.hasErrors()) {
            log.info("has error...");
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            return "redirect:/board/add";
        }

        log.info(boardDTO);
        boardService.register(boardDTO);

        return "redirect:/board/list";

    }
 }

 

 <form  method="post">
    <div class="input mb-3">
        <span class="input-group-text">Title</span><br>
        <input type="text" name="title" placeholder="Title"><br><br>
    </div>
    <div class="input mb-3">
        <span class="input-group-text">Content</span><br>
        <textarea name="content" cols="60" rows="18"></textarea><br><br>
    </div>
    <div class="input mb-3">
        <span class="input-group-text">Writer</span><br>
        <input type="text" name="writer" placeholder="Writer"><br><br>
    </div>
    <div class="input mb-3">
        <span class="input-group-text">Password</span><br>
        <input type="password" name="passwd" placeholder="비밀번호"><br><br>
    </div>
    <div class="float-end">
        <button type="submit" name="submitBtn">Submit</button>
    </div>
</form>

 

 


 

6.  목록페이지

  • DAO에 목록을 불러오는 selectAll() 메서드 작성
  • 테스트 코드 작성 후 테스트
  • Service 작업
  • 테스트 코드 작성 후 테스트
  • 컨트롤러에 /board/list 경로와 매핑을 한 list() 작성
  • list.jsp 작성
public interface BoardMapper {

    void insert(BoardVO boardVO); // DB 저장하는 메소드

    List<BoardVO> selectAll(); // DB에 저장되어 있는 리스트 목록
}
    <select id="selectAll" resultType="com.example.spring_ex_01_2404.domain.BoardVO">
        SELECT * FROM tbl_board ORDER BY no DESC
    </select>
    @Test
    public void selectAll() {
        List<BoardVO> boardVOList = boardMapper.selectAll();
        for(BoardVO boardVO : boardVOList) {
            log.info(boardVO);
        }
    }
public interface BoardService {

    void register(BoardDTO boardDTO);

    List<BoardDTO> getAll();

}
    @Override
    public List<BoardDTO> getAll() {
        List<BoardVO> voList = boardMapper.selectAll(); // dao에서 데이터베이스에서 들고온 VO리스트를 리턴
        List<BoardDTO> dtoList = new ArrayList<>();
        for (BoardVO boardVO: voList) {
            // 개별 VO를 DTO로 변환.
            BoardDTO boardDTO = modelMapper.map(boardVO, BoardDTO.class);
            dtoList.add(boardDTO); // DTO리스트에 저장.
        }
        return dtoList;
    }
  @Test
    public void getAll() {
        List<BoardDTO> boardDTOList = boardService.getAll();
        for (BoardDTO boardDTO : boardDTOList) {
            log.info(boardDTO);
        }
    }
  @RequestMapping("/list")
    public void list(Model model) {
        log.info("todo list...");
        model.addAttribute("dtoList", boardService.getAll());
    }
<div class="card">
    <div class="card-header">Board List</div>

        <div class="card-body">
            <table class="table">
                <thead>
                    <tr>
                        <th scope="col">No</th>
                        <th scope="col">Title</th>
                        <th scope="col">Writer</th>
                        <th scope="col">Date</th>
                    </tr>
                </thead>

                <tbody>
                <c:forEach var="dto" items="${dtoList}">
                    <tr>
                         <th scope="row">${dto.no}</th>
                         <td>${dto.title}</td>
                         <td>${dto.writer}</td>
                         <td>${dto.addDate}</td>
                    </tr>
                </c:forEach>
                </tbody>
            </table>
        </div>
    </div>
</div>

 


7.  조회

리스트에서 클릭을 하면 해당 글을 보여주는 페이지 구현

  • DAO에 게시물을 불러오는 selectOne() 메서드 작성
  • 테스트 코드 작성  후 테스트
  • Service 작업
  • 컨트롤러에 /board/read 경로와 매핑을 한 read() 작성
  • read.jsp 작성
  • updatehit 추가 (조회수 증가)
public interface BoardMapper {

    void insert(BoardVO boardVO); // DB 저장하는 메소드

    List<BoardVO> selectAll();

    BoardVO selectOne(Integer no); // 조회
    
    void updateHit(Integer no); // 조회수 증가
 
}
    <select id="selectOne" resultType="com.example.spring_ex_01_2404.domain.BoardVO">
        SELECT * FROM tbl_board WHERE no = #{no}
    </select>
    
    <update id="updateHit">
        UPDATE tbl_board SET hit = hit + 1 where no = #{no}
    </update>
    @Test
    public void selectOne() {
        BoardVO boardVO = boardMapper.selectOne(1);
        log.info(boardVO);
    }
11:46:50 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.selectOne] ==> Preparing: SELECT * FROM tbl_board WHERE no = ?
11:46:50 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.selectOne] ==> Parameters: 1(Integer)
11:46:50 TRACE [com.example.spring_ex_01_2404.mapper.BoardMapper.selectOne] <== Columns: no, title, content, writer, passwd, addDate, hit
11:46:50 TRACE [com.example.spring_ex_01_2404.mapper.BoardMapper.selectOne] <== Row: 1, 스프링 테스트, <<BLOB>>, river, 1234, 2024-05-01 00:00:00, 1
11:46:50 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.selectOne] <== Total: 1

 

public interface BoardService {

    void register(BoardDTO boardDTO);

    List<BoardDTO> getAll();

    BoardDTO getOne(Integer no);
}
    @Override
    public BoardDTO getOne(Integer no) {
        boardMapper.updateHit(no);
        BoardVO boardVO = boardMapper.selectOne(no);
        BoardDTO boardDTO = modelMapper.map(boardVO, BoardDTO.class);
        return boardDTO;
    }

 

 @GetMapping("/read")
    public void read(Integer no, Model model) {
        BoardDTO boardDTO = boardService.getOne(no);
        log.info(boardDTO);
        model.addAttribute("dto", boardDTO);
    }

 

<div class="card-body">
    <div class="input-group">
        <span class="input-group-text">No</span>
        <input type="text" name="tno" class="form-control" value="${dto.no}" readonly>
    </div> <br>
    <div class="input-group">
        <span class="input-group-text">Title</span>
        <input type="text" name="title" class="form-control" value="${dto.title}" readonly>
    </div><br>
    <div class="input-group">
       <span class="input-group-text">Content</span>
       <textarea name="title" class="form-control" readonly>${dto.content}</textarea>
    </div><br>
    <div class="input-group">
        <span class="input-group-text">Writer</span>
        <input type="text" name="writer" class="form-control" value="${dto.writer}" readonly>
    </div><br>
    <div class="input-group">
        <span class="input-group-text">Date</span>
        <input type="date" name="dueDate" class="form-control" value="${dto.addDate}" readonly>
    </div><br>
    <div class="input-group">
        <span class="input-group-text">Hit</span>
        <input type="text" name="writer" class="form-control" value="${dto.hit}" readonly>
    </div><br>
    <div class="my-4">
        <div class="float-end">
            <button type="submit" class="btn btn-primary">Modify</button>
            <button type="reset" class="btn btn-secondary">List</button>
        </div>
    </div>

    <script>
        document.querySelector('.btn-primary').addEventListener('click', function (e) {
            self.location = `/board/modify?no=${dto.no}&${boardDTO.link}`;
        }, false);
        document.querySelector('.btn-secondary').addEventListener('click', function (e) {
            self.location = "/board/list?${boardDTO.link}";
        }, false);
    </script>
</div>

 

 

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  검색 / 필터링 조건의 정의

📍  대부분의 서비스에서는 검색 기능을 제공. 단순히 제목이나 내용 등을 검색하는 경우도 있고, 복잡한 검색 조건을 필터링 filtering 하는 경우도 있음
📍  검색 search는 'A 혹은 B 혹은 C'와 같이 찾고자 하는 경우 검색 조건들은 주로 OR 조건으로 연결되는 경우가 많음
        ➡️  예를 들어 '제목 or 내용 or 작성자'가 xxx인 경우와 같은 데이터가 있을수도 있고, 없을 수도 있음
📍  필터링 filtering'A인 동시에 B에도 해당'한다는 개념. 필터링은 주로 특정한 범위나 범주의 값으로 걸러내는 방식
        ➡️  예를 들어 '완료된 일 중에서 특정 날짜까지 끝난 Todo'는 'A & B'와 같이 AND라는 개념의 필터링이 적용

 

다음과 같은 검색과 필터링 조건을 구성하고 처리

 

  • 제목 title과 작성자 writer 는 키워드 keyword를 이용하는 검색 처리
  • 완료 여부를 필터링 처리
  • 특정한 기간을 지정 from, to 한 필터링 처리
  • 검색과 필터링에 필요한 데이터는 다음과 같이 구분
    * 제목, 작성자 검색에 사용하는 문자열 - keyword
    * 완료 여부에 사용되는 boolean 타입 - finished
    * 특정 기간 검색을 위한 LocalDate 변수 2개 - from, to

 


2.  검색 / 필터링 조건의 결정

검색 기능을 개발할 때는 우선 검색 기능의 경우의 수를 구분하는 작업이 필요

  • 검색 / 필터링의 종류가 '완료 여부, 제목, 작성자, 기간'들의 조합으로 구성
  • 검색 종류를 types라고 지정해서 '제목(t), 작성자(w)'로 구분해서 검색의 실제 값은 검색 종류에 따라 키워드 keyword를 이용
  • 검색은 목록 기능에 사용하는 PageRequestDTO에 필요한 변수들을 추가해서 구성
PageRequestDTO에 types, keyword, finished, from, to 변수를 새롭게 추가
    private String[] types; // 검색 경우의 수 1) title 2) writer 3) title, writer
    private String keyword; // 검색어
    private boolean finished;
    private LocalDate from;
    private LocalDate to;

 


3.  types에 따른 동적 쿼리

Mybatis에는 실행 시에 쿼리를 만들 수 있는 여러 태그들을 제공

 

1) foreach, if

 

🤓  <foreach>는 반복 처리를 위해 제공
🤓  <foreach>의 대상은 배열이나 List, Map, Set과 같은 컬렉션 계열이나 배열을 이용

 

실습을 위해 {"t", "w"}와 같은 types를 PageRequestDTO에 설정하고 테스트를 진행
기존의 TodoMapperTests 클래스에 새로운 메서드를 추가
@Test
    public void testSelectSearch() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .page(1)
                .size(10)
                .types(new String[]{"t", "w"})
                .keyword("AAAA")
                .build();
        List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
        voList.forEach(vo -> log.info(vo));
    }

 

📍 TodoMapper의 selectList()는 PageRequestDTO를 파라미터로 받고 있기 때문에 변경 없이 바로 사용 가능하므로 TodoMapper.xml만 수정하면 됨

 

TodoMapper.xml에서는 <select id="selectList"> 태그에 MyBatis의 <foreach>를 적용
<select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
    select * from tbl_todo 
    <foreach collection="types" item="type">
        #{type}
    </foreach>    
    order by tno desc limit #{skip}, #{size}
</select>

 

  👾  현재 PageRequestDTO의 types는 {"t", "w"}이므로 테스트 코드를 실행하면 다음과 같은 코드가 만들어지는 것을 확인
        (쿼리문이 아직 완성된 상태가 아니라서 에러가 발생)

 

  👾  sql 부분만 보면 다음과 같이 sql 이 실행
        select * from tbl_todo ? ? order by tno desc limit ?, ?
        ✓  't'와 'w'가 전달되었기 때문에 'from tbl_todo' 뒤에 두 개의 "?"가 생성된 것을 확인.

 

<if> 적용
<select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
    select * from tbl_todo 
    <foreach collection="types" item="type" open="(" close=")" separator=" OR">
        <if test="type == 't'.toString()">
            title like concat('%', #{keyword}, '%')
        </if>
        <if test="type =='w'.toString()">
            writer like concat('%', #{keyword}, '%')
        </if>
    </foreach>    
    order by tno desc limit #{skip}, #{size}
</select>

 

  👾  open과 close를 이용해서 '()'와 배열을 처리하면서 중간에는 OR을 추가해서 다음과 같은 쿼리가 생성
       select * from tbl_todo (title like concat('%', ?, '%') OR

           writer like concat('%', ?, '%'))

       order by tno desc limit ?, ?


2)  <where>


📍 쿼리에 where 키워드가 빠져있음. 이것은 만일에 types가 없는 경우에는 쿼리문에 where를 생성하지 않기 위함.
📍 <where>는 태그 안쪽에서 문자열이 생성되어야만 where 키워드를 추가.
📍 이를 이용해서 types가 null이 아닌 경우를 같이 적용하면 다음과 같이 작성.

<select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
    select * from tbl_todo 
    <where>
        <if test="types != null and types.length > 0">
            <foreach collection="types" item="type" open="(" close=")" separator=" OR">
                <if test="type == 't'.toString()">
                    title like concat('%', #{keyword}, '%')
                </if>
                <if test="type =='w'.toString()">
                    writer like concat('%', #{keyword}, '%')
                </if>
            </foreach>
        </if>
    </where>
    order by tno desc limit #{skip}, #{size}
</select>

 


3) <trim>과 완료 여부 / 만료일 필터링


완료 여부 (f)와 만료 기간 (d)에 대한 처리
  📍  완료 여부는 PageRequestDTO의 finished 변수 값이 true인 경우에만 'finished = 1'과 같은 문자열이 만들어지도록 구성
         ✓  주의점은 앞의 검색 조건(제목, 작성자)이 있는 경우에는 'and finished = 1'의 형태로 만들어져야 하고,
              그렇지 않은 경우에는 바로 'finished = 1'이 되어야 함
  📍  MyBatis의 <trim>이 이런 경우에 사용. <where>과 유사하게 동작하면서 필요한 문자열을 생성하거나 제거할 수 있음

        <if test = "finished">
            <trim prefix="and">
                finished = 1
            </trim>
        </if>
    </where>    
    order by tno desc limit #{skip}, #{size}
</select>

 

  👾  finished 값을 이용해서 SQL 문을 생성해 내는데 <trim>을 적용. prefix를 적용하게 되면 상황에 따라서 'and'가 추가.

 

테스트 코드 수정해서 확인
@Test
    public void testSelectSearch() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .page(1)
                .size(10)
                .types(new String[]{"t", "w"})
                .keyword("스프링")
                .finished(true)
                .build();
        List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
        voList.forEach(vo -> log.info(vo));
    }

 

같은 방식으로 만료일 dueDate 을 처리하면 다음과 같음
<if test="from != null and to != null">
    <trim prefix="and">
        dueDate between #{from} and #{to}
    </trim>
</if>

 


4) <sql> 과 <include>


MyBatis의 동적 쿼리 적용은 단순히 목록 데이터를 가져오는 부분과 전체 개수를 가져오는 부분에도 적용되어야 함
  📍전체 개수를 가져오는 TodoMapper의 getCount()에 파라미터에 PageRequestDTO 타입을 지정한 이유 역시 동적 쿼리를 적용하기 위함
  📍 MyBatis에는 <sql> 태그를 이용해서 동일한 SQL 조각을 재사용할 수 있는 방법을 제공
        ➡️  동적 쿼리 부분을 <sql>로 분리하고 동일하게 동적 쿼리가 적용될 부분은 <include>를 이용해서 작성

 

TodoMapper.xml에서 <sql id='search'>로 동적 쿼리 부분을 분리
<sql id="search">
    <where>
        <if test="types != null and types.length > 0">
            <foreach collection="types" item="type" open="(" close=")" separator=" OR">
                <if test="type == 't'.toString()">
                    title like concat('%', #{keyword}, '%')
                </if>
                <if test="type =='w'.toString()">
                    writer like concat('%', #{keyword}, '%')
                </if>
            </foreach>
        </if>
        <if test = "finished">
            <trim prefix="and">
                finished = 1
            </trim>
        </if>
        <if test="from != null and to != null">
            <trim prefix="and">
                dueDate between #{from} and #{to}
            </trim>
        </if>
    </where>
</sql>
    <select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
        select * from tbl_todo 
        <include refid="search"></include>
        order by tno desc limit #{skip}, #{size}
    </select>

    <select id="getCount" resultType="int">
        select COUNT(tno) FROM tbl_todo
        <include refid="search"></include>
    </select>

 

테스트 코드에서는 TodoMapper의 selectList()와 getCount()를 호출해서 결과를 확인
    @Test
    public void testSelectSearch() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .page(1)
                .size(10)
                .types(new String[]{"t", "w"})
                .keyword("스프링")
                //.finished(true)
                .from(LocalDate.parse("2022-04-28"))
                .to(LocalDate.parse("2024-04-30"))
                .build();
        List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
        voList.forEach(vo -> log.info(vo));

        log.info(todoMapper.getCount(pageRequestDTO));
    }

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  목록 데이터를 위한 DTO와 서비스 계층

🚀  TodoMapper에서 TodoVO의 목록과 전체 데이터의 수를 가져온다면 이를 서비스 계층에서 한 번에 담아서 처리하도록 DTO를 구성하는 것이 좋음
🚀  작성하려는 DTO는 PageResponseDTO라는 이름을 생성하고 다음과 같은 데이터와 가능을 가지도록 구성

  •  TodoDTO의 목록
  •  전체 데이터의 수
  •  페이지 번호 처리를 위한 데이터들 (시작 페이지 번호 / 끝 페이지 번호)

🚀  화면상에서 페이지 번호들을 출력하려면 현재 페이지 번호 page 와 페이지당 데이터의 수 size를 이용해서 계산할 필요가 있음
        ➡️  이 때문에 작성하려는 PageResponseDTO는 생성자를 통해서 필요한 page나 size 등을 전달받도록 구성

 

PageResponseDTO가 가져야 하는 데이터를 정리해서 클래스를 구성
public class PageResponseDTO<E> {

    private int page;
    private int size;
    private int total;

    // 시작 페이지 번호
    private int start;
    // 끝 페이지 번호
    private int end;

    // 이전 페이지의 존재 여부
    private boolean prev;
    // 다음 페이지의 존재 여부
    private boolean next;

    private List<E> dtoList;
}

 

  ✓  제네릭을 이용하는 이유는 나중에 다른 종류의 객체를 이용해서 PageResponseDTO를 구성할 수 있도록 하기 위함
       예를 들어 게시판이나 회원 정보 등도 페이징 처리가 필요할 수 있기 때문에 공통적인 처리를 위해서 제네릭으로 구성
 

   ✓  PageResponseDTO는 여러 정보를 생성자를 이용해서 받아서 처리하는 것이 안전
        예를 들어 PageRequestDTO 에 있는 page, size 값이 필요하고, TodoDTO 목록 데이터와 전체 데이터의 개수도 필요
 

PageResponseDTO의 생성자는 Lombok의 @Builder를 적용
public class PageResponseDTO<E> {
    @Builder(builderMethodName = "withAll")
    public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total) {
        log.info(pageRequestDTO);
        this.page = pageRequestDTO.getPage();
        this.size = pageRequestDTO.getSize();

        this.total = total;
        this.dtoList = dtoList;
}

 


1) 페이지 번호 계산

페이지 번호를 계산하려면 우선 현재 페이지의 번호 page 가 필요.
화면에 10개의 페이지 번호를 출력한다고 했을 때 다음과 같은 경우들이 생길수 있음.

  •  현재 page가 1인 경우 : 시작 페이지 start는 1, 마지막 페이지 end는 10
  •  현재 page가 10인 경우 : 시작 페이지 start는 1, 마지막 페이지 end는 10
  •  현재 page가 11인 경우 : 시작 페이지 start는 11, 마지막 페이지 end는 20

 

A.  마지막 페이지 / 시작 페이지 번호의 계산


📍 end는 현재 페이지 번호를 기준으로 계산

this.end = (int)(Math.ceil(this.page/10.0)) * 10;

 

  👾  page를 10으로 나눈 값을 올림처리 한 후 * 10
         1 / 10 => 0.1 => 1 => 10
         11 / 10.0 => 1.1 => 2 => 20
         10 / 10 => 1.0 => 1 => 10

 

📍 시작 페이지 start의 경우 계산한 마지막 페이지에서 9를 빼면 됨

this.start = this.end - 9;

 

시작 페이지의 구성은 끝났지만 마지막 페이지의 경우 다시 전체 개수 total를 고려해야 함.
만일 10개씩 (size) 보여주는 경우 전체 개수 total가 75라면 마지막 페이지는 10이 아니라 8이 되어야 함.

int last = (int)(Math.ceil(total/(double)size));

 

     123 / 10.0 => 12.3 => 13
     100 / 10.0 => 10.0 => 10
     75 / 10.0 => 7.5 => 8

 

📍 마지막 페이지 end는 앞에서 구한 last 값보다 큰 경우에 last 값이 end가 되어야만 함

this.end = end > last ? last : end;

B. 이전 prev / 다음 next 의 계산


이전 prev 페이지의 존재 여부는 시작 페이지 start가 1이 아니라면 무조건 true
다음 next은 마지막 페이지 end 와 페이지당 개수 size를 곱한 값보다 전체 개수 total가 더 많은지 보고 판단

this.prev = this.start > 1;
this.next = total > this.end * this.size;

 

PageResponseDTO는 최종적으로 Lombok의 @Getter를 적용


2)  TodoService / TodoServiceImpl

TodoService와 TodoServiceImpl에서는 작성된 PageResponseDTO를 반환타입으로 지정해서 getList()를 구성

public interface TodoService {
    void register(TodoDTO todoDTO);

    //List<TodoDTO> getAll();

    PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO);

    TodoDTO getOne(Long tno);

    void remove(Long tno);

    void modify(TodoDTO todoDTO);
}
 @Override
    public PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO) {
        List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
        List<TodoDTO> dtoList = new ArrayList<>();
        for(TodoVO todoVO : voList) {
            dtoList.add(modelMapper.map(todoVO, TodoDTO.class));
        }

        int total = todoMapper.getCount(pageRequestDTO);

        PageResponseDTO<TodoDTO> pageResponseDTO = PageResponseDTO.<TodoDTO>withAll()
                .dtoList(dtoList)
                .total(total)
                .pageRequestDTO(pageRequestDTO)
                .build();
        return pageResponseDTO;
    }

 

TodoService의 getList()는 테스트를 통해서 결과를 확인
  @Test
    public void pageingTest() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(1).size(10).build();
        PageResponseDTO<TodoDTO> pageResponseDTO = todoService.getList(pageRequestDTO);
        log.info(pageResponseDTO);
        for (TodoDTO todoDTO : pageResponseDTO.getDtoList()) {
            log.info(todoDTO);
        }
    }

 


3)  TodoController와 JSP처리

TodoController의 list()에서는 PageRequestDTO를 파라미터로 처리하고,

Model에 PageResponseDTO의 데이터를 담을 수 있도록 변경

@RequestMapping("/list")
    public void list(@Valid PageRequestDTO pageRequestDTO, BindingResult bindingResult, Model model) {
        log.info(pageRequestDTO);

        if(bindingResult.hasErrors()) {
            pageRequestDTO = pageRequestDTO.builder().build();
        }
        model.addAttribute("responseDTO", todoService.getList(pageRequestDTO));
    }

 

  ⚡️  list()는 @Valid를 이용해서 잘못된 파라미터 값들이 들어오는 경우 page는 1, size는 10으로 고정된 값을 처리하도록 구성
  ⚡️  기존과 달리 Model에 responseDTO라는 이름으로 PageResponseDTO를 담아 주었기 때문에 list.jsp를 수정

<c:forEach var="dto" items="${responseDTO.dtoList}">
    <tr>
        <th scope="row">${dto.tno}</th>
            <td><a href="/todo/read?tno=${dto.tno}" class="text-decoration-none">
                <c:out value="${dto.title}"/>
            </a></td>
            <td>${dto.writer}</td>
            <td>${dto.dueDate}</td>
            <td>${dto.finished}</td>
    </tr>
</c:forEach>

 

  ✓  프로젝트를 실행하고 브라우저에서 "/todo/list"를 호출했을 때 1페이지에 해당하는 데이터들이 출력되는 것을 확인


4)  페이지 이동 확인

화면을 추가로 개발하기 전에 "/todo/list?page=xx&size=xx"를 호출해서 결과가 정상적으로 처리되는지 확인.
page의 경우는 음수가 될 수 없고, size는 100을 넘을 수 없음.


A.  화면에 페이지 이동을 위한 번호 출력


브라우저를 통해서 페이지의 이동에 문제가 없다는 것을 확인했다면 화면 아래쪽에 페이지 번호들을 출력하도록 구성.
페이지 번호는 부트스트랩의 pagination이라는 컴포넌트를 적용.

 

list.jsp에 <table>태그가 끝난 후에 <div>를 구성해서 화면에 적용
<div class="float-end">
    <ul class="pagination flex-wrap">
        <c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
            <li class="page-item"><a class="page-link" href="#">${num}</a></li>
        </c:forEach>
    </ul>    
</div>

 

  ✓  브라우저에서 page를 변경시켜 페이지 번호들이 변경되는지 확인

 


B.  화면에서 prev / next / 현재 페이지


페이지 번호들이 정상적으로 출력된다면 "이전 / 다음"을 처리

<div class="float-end">
    <ul class="pagination flex-wrap">
        <c:if test="${responseDTO.prev}">
            <li class="page-item"><a class="page-link">Previous</a></li>
        </c:if>
        
        <c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
            <li class="page-item"><a class="page-link" href="#">${num}</a></li>
        </c:forEach>
                                
        <c:if test="${responseDTO.next}">
            <li class="page-item"><a class="page-link">Next</a></li>
        </c:if>
    </ul>
</div>

 

  ⚡️  11페이지 이상되었을 때 Previous 버튼이 보임

  ⚡️  1 ~ 10 페이지의 경우 Previous 버튼이 보이지 않음

📍 현재 페이지의 번호는 class 속성에 "active"라는 속성값이 추가되어야 함. 삼항 연산자를 이용

<c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
    <li class="page-item" ${responseDTO.page == num ? "active" : ""}>
    <a class="page-link" href="#">${num}</a></li>
</c:forEach>

 

 


C.  페이지의 이벤트 처리


👾  화면에서 페이지 번호를 누르면 이동하는 처리는 자바스크립트를 이용해서 처리
👾  화면의 페이지 번호를 의미하는 <a> 태그에 직접 "onclick"을 적용할 수도 있지만, 한 번에 <ul> 태그에 이벤트를 이용해서 처리
👾  우선은 각 페이지 번호에 적절한 페이지 번호를 가지도록 구성. 이때는 "data-"속성을 이용해서 필요한 속성을 추가해주는 방식을 사용

 

 "data-num"이라는 속성을 추가해서 페이지 번호를 보관하도록 구성
<c:if test="${responseDTO.prev}">
    <li class="page-item">
        <a class="page-link" data-num="${reponseDTO.start-1}">Previous</a>
    </li>
</c:if>

<c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
    <li class="page-item" ${responseDTO.page == num ? "active" : ""}>
    <a class="page-link" data-num="${num}">${num}</a></li>
</c:forEach>

<c:if test="${responseDTO.next}">
    <li class="page-item">
    <a class="page-link" data-num="${responseDTO.end+1}">Next</a></li>
</c:if>

 

<ul> 태그가 끝난 부분에 이벤트 처리 추가
 <script>
    document.querySelector(`.pagination`).addEventListener('click', function (e) {
        e.preventDefault();
        e.stopPropagation();

        const target = e.target;
        if(target.tagName !== 'A') {
            return;
        }
        const num = target.getAttribute('data-num');
        self.location = `/todo/list?page=\${num}`; 
    });
</script>

 

  ⚡️  자바 스크립트의 이벤트 처리는 <ul> 태그에 이벤트를 등록하고 <a> 태그를 클릭했을 때만 data-num 속성값을 읽어와서 현재 주소(self.location)를 변경하는 방식으로 작성
  ⚡️  자바 스크립트에서 백틱(`)을 이용하면 문자열 결합에 '+'를 이용해야 하는 불편함을 줄일 수 있음. 대신에 JSP의 EL이 아니라는 것을 표시하기 위하여 \${}로 처리
  ⚡️  자바 스크립트 처리가 완료되면 화면상의 페이지 번호를 클릭해서 페이지 이동이 가능한지 확인

 


D.  조회 페이지로 이동


목록 페이지는 특정한 Todo의 제목 title을 눌러서 조회 페이지로 이동하는 기능이 존재
기존에는 단순히 tno만을 전달해서 '/todo/read?tno=33'과 같은 방식으로 이동했지만, 페이지 번호가 붙을 때는 page와 size 등을 같이 전달해 주어야만 조회 페이지에서 다시 목록으로 이동할 때 기존 페이지을 볼 수 있게 됨
이를 위해 list.jsp에는 각 Todo의 링크 처리 부분을 수정할 필요가 있음

 

페이지 이동 정보는 PageRequestDTO 안에 있으므로 PageRequestDTO 내부에 간단한 메서드를 작성해서 필요한 링크를 생성해서 사용. (파라미터로 전달되는 PageRequestDTO는 Model로 자동 전달되기 때문에 별도의 처리가 필요하지 않음)

 

PageRequestDTO에 link라는 속성을 추가하고 getLink()를 추가해서 GET 방식으로 페이지 이동에 필요한 링크들을 생성
private String link;

public String getLink() {
        if (link == null) {
            StringBuilder builder = new StringBuilder();
            builder.append("page=" + this.page);
            builder.append("&size=" + this.size);
            link = builder.toString();
        }
        return link;
}

 

list.jsp 수정
<c:forEach var="dto" items="${responseDTO.dtoList}">
    <tr>
        <th scope="row">${dto.tno}</th>
        <td><a href="/todo/read?tno=${dto.tno}&${pageRequestDTO.link}" 
            class="text-decoration-none">
            <c:out value="${dto.title}"/>
        </a></td>
        <td>${dto.writer}</td>
        <td>${dto.dueDate}</td>
        <td>${dto.finished}</td>
    </tr>
</c:forEach>

 

  ⚡️ 기존의 tno=xx 뒤에 &를 추가하고 PageRequestDTO의 getLink()의 결과인 문자열을 생성하게 되면 다음과 같이 기존 링크에
page와 size가 추가된 형태가 됨

<a href="/todo/read?tno-3579&page=1&size=10" class="text-decoration-none" data-tno=357>

 


E. 조회에서 목록으로

 

조회 화면에서는 기존과 달리 PageRequestDTO를 추가로 이용하도록 TodoController를 수정

 

TodoController의 read 메소드는 PageRequestDTO 파라미터를 추가해서 수정
 @GetMapping({"/read", "/modify"})
    public void read(Long tno, PageRequestDTO pageRequestDTO, Model model) {
        TodoDTO todoDTO = todoService.getOne(tno);
        log.info(todoDTO);
        model.addAttribute("dto", todoDTO);
    }

 

read.jsp에서는 List 버튼의 링크를 다시 처리
document.querySelector('.btn-secondary').addEventListener('click', function (e) {
    self.location = "/todo/list?${pageRequestDTO.link}";
}, false);

 

  👾  브라우저는 특정한 페이지에서 조회 페이지로 이동해서 List 버튼을 눌렀을 때 정상적으로 이동하는지 확인

 


F.  조회에서 수정으로


조회 화면에서 수정 화면으로 이동할 때도 현재 페이지 정보를 유지해야 하기 때문에 read.jsp에서는 링크 처리 부분을 수정

document.querySelector('.btn-primary').addEventListener('click', function (e) {
    self.location = `/todo/modify?tno=${dto.tno}&${pageRequestDTO.link}`;
}, false);

 

  👾  화면에서 Modify 버튼을 누르면 아래와 같이 동작

 


G.  수정화면에서의 링크 처리


수정 화면에서도 다시 목록으로 가는 링크가 필요. TodoController의 read() 메서드는 GET 방식으로 동작하는 '/todo/modify'에도 동일하게 처리되므로 JSP에서 PageRequestDTO를 사용할 수 있음.

 

modify.jsp 의 List 버튼을 누르는 자바 스크립트의 이벤트 부분을 변경
document.querySelector('.btn-secondary').addEventListener('click', function (e) {
    self.location = `/todo/list?${pageRequestDTO.link}`;
});

 


H.  수정 / 삭제 처리 후 페이지 이동


실제 수정 / 삭제 작업은 POST 방식으로 처리되고 삭제 처리가 된 후에는 다시 목록으로 이동할 필요가 있음.
그렇기 때문에 수정 화면에서 <form> 태그로 데이터를 전송할 때 페이지와 관련된 정보를 같이 추가해서 전달해야 함.

 

modify.jsp에 <input type="hidden">을 이용해서 추가
<form action="/todo/modify" method="post">
    <input type="hidden" name="page" value="${pageRequestDTO.page}">
    <input type="hidden" name="size" value="${pageRequestDTO.size}">

 

 

TodoController에서 POST 방식으로 이루어지는 삭제 처리에도 PageRequestDTO를 이용해서 <form> 태그로 전송되는 태그들을 수집하고 수정 후에 목록 페이지로 이동할 때 page는 무조건 1페이지로 이동해서 size 정보를 활용

@PostMapping("/remove")
    public String remove(Long tno, PageRequestDTO pageRequestDTO , RedirectAttributes redirectAttributes) {
        log.info("-----remove----");
        log.info("tno: " + tno);

        todoService.remove(tno);
        redirectAttributes.addAttribute("page", 1);
        redirectAttributes.addAttribute("size", pageRequestDTO.getSize());
        return "redirect:/todo/list";
    }

 

  👾  브라우저는 목록에서 특정한 Todo를 조회 -> 수정 / 삭제 화면 -> 삭제 후 이동이 정상적으로 이루어지는지 확인

 


I.  수정 처리 후 이동


Todo를 수정한 후에 목록으로 이동할 때는 페이지 정보를 이용해야 하므로 TodoController의 modify() 에서는 PageRequestDTO를 받아서 처리하도록 변경.

    @PostMapping("/modify")
    public String midify(PageRequestDTO pageRequestDTO,
                         @Valid TodoDTO todoDTO,
                         BindingResult bindingResult,
                         RedirectAttributes redirectAttributes) {
        if (bindingResult.hasErrors()) {
            log.info("has error");
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            redirectAttributes.addAttribute("tno", todoDTO.getTno());
            return "redirect:/todo/mofidy";
        }
        log.info(todoDTO);
        todoService.modify(todoDTO);
        
        redirectAttributes.addAttribute("page", pageRequestDTO.getPage());
        redirectAttributes.addAttribute("size", pageRequestDTO.getSize());
        
        return "redirect:/todo/list";
    }

 

  👾  정상적으로 수정된 후에는 '/todo/list'로 이동할 때 필요한 page나 size를 유지할 수 있도록 구성

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  페이징 처리를 위한 TodoMapper

👩🏻‍💻  Todo 데이터의 수가 많아진다면 데이터베이스에서 많은 시간이 걸릴 수 있고, 화면에서 이를 출력하는 데도 많은 시간과 자원이 소모
👩🏻‍💻  일반적으로는 많은 데이터를 보여주는 작업은 페이징 처리를 해서 최소한의 데이터들을 보여주는 방식을 선호
        ⚡️  페이징 처리를 하게 되면 데이터베이스에서 필요한 만큼의 최소한의 데이터를 가져오고 이를 출력하기 때문에 성능 개선에도 많은 도움이 됨

 

1)  페이징을 위한 SQL 연습

개발 전에 데이터베이스 상에서 원하는 동작을 미리 구현.


A. 더미 데이터 추가하기

 

많은 양의 데이터를 만드는 방법. 2배씩 늘어나기 때문에 몇번만 실행하면 많은 데이터를 생성함.
더미 데이터 추가를 위해 아래의 SQL을 여러번 처리.

insert into tbl_todo(title,dueDate,writer) (select title,dueDate,writer from tbl_todo);

 

  📍 최종적으로 데이터의 수를 확인

select count(tno) from tbl_todo;

B.  limit 실습


페이징 처리를 위해서는 select의 마지막 부분에는 limit 처리를 사용.
일반적으로 웹에서는 가장 최근에 등록된 데이터를 우선으로 보여주므로 가장 마지막에 등록된 데이터가 순차적으로 보이도록 쿼리를 작성

select * from tbl_todo order by tno desc;

 

📍  limit 뒤의 값이 한 개만 존재하는 경우 가져와야 하는 데이터의 수를 의미. 가장 최근 데이터 10개를 가져옴

select*from tbl_todo order by tno desc limit 10;
select*from tbl_todo order by tno desc limit 0, 10;

 


C.  count()의 필요성


페이징 처리를 하기 위해서는 페이지 번호를 구성할 때 필요한 전체 데이터의 개수가 필요함.
예를 들어 전체 데이터가 30개이면 3 페이지까지만 출력해야 하는 작업에서 사용

select count(tno) from tbl_todo;

 


2)  페이지 처리를 위한 DTO

💡  페이지 처리는 현재 페이지 번호 page, 한 페이지당 보여주는 데이터의 수 size가 기본적으로 필요

      ➡️  2개의 숫자를 매번 전달할 수도 있겠지만 나중에 확장 여부를 고려해서라도 별도의 DTO를 만들어 두는 것이 좋음

 

dto 패키지에 PageRequestDTO 클래스를 정의
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {
    @Builder.Default
    @Min(value = 1)
    @Positive
    private int page = 1;

    @Builder.Default
    @Min(value=10)
    @Max(value=100)
    @Positive
    private int size = 10;

    public int getSkip() {
        return (page - 1) * size;
    }

}

 

  ✏️  페이지 번호 page와 한 페이지당 개수 size를 보관하는 용도 외에도 limit에서 사용하는 건너뛰기 skip 의 수를 getSkip() 만들어서 사용
  ✏️  page나 size는 기본값을 가지기 위해서 Lombok의 @Builder.Default를 이용
  ✏️  @Min, @Max를 이용해서 외부에서 조작하는 것에 대해서도 대비하도록 구성

 


3)  TodoMapper의 목록 처리

TodoMapper 인터페이스는 PageRequestDTO를 파라미터로 처리하는 selectList()를 추가

 List<TodoVO> selectList(PageRequestDTO pageRequestDTO);

 

TodoMapper.xml 내에서 selectList는 다음과 같이 구현
    <select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
        select * from tbl_todo order by tno desc limit #{skip}, #{size}
    </select>

 

  ✏️  MyBatis는 기본적으로 getXXX, setXXX를 통해서 동작하기 때문에 #{skip}의 경우는 getSkip()을 호출
 

테스트 코드를 이용해서 TodoMapper의 selectList()가 정상적으로 동작하는지 확인
 @Test
    public void testSelectList() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .page(5)
                .build();
        List<TodoVO> todoVOList = todoMapper.selectList(pageRequestDTO);
        for (TodoVO todoVO : todoVOList) {
            log.info(todoVO);
        }

        todoVOList.forEach(item -> log.info(item));

    }

 


4)  TodoMapper의 count 처리

화면에 페이지 번호들을 구성하기 위해서는 전체 데이터의 수를 알아야만 가능
  ✓ 예를 들어 마지막 페이지가 7에서 끝나야 하는 상황이 생긴다면 화면상에서도 페이지 번호를 조정해야 하기 때문

 

TodoMapper에 getCount()를 추가


  👾  getCount()는 나중에 검색에 대비해서 PageRequestDTO를 파라미터로 받로록 설계

 int getCount(PageRequestDTO pageRequestDTO);

 

TodoMapper.xml은 우선은 전체 개수를 반환하도록 구성
<select id="getCount" resultType="int">
    select COUNT(*) FROM tbl_todo
</select>

 

test 코드 실행 확인
 @Test
    public void testGetCount() {
        log.info(todoMapper.getCount(PageRequestDTO.builder().build()));
    }

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  Todo의 삭제 기능 개발

수정과 삭제는 GET 방식으로 조회한 후에 POST 방식으로 처리
사실상 GET 방식의 내용은 조회 화면과 같지만 스프링 MVC에는 여러 개의 경로를 배열과 같은 표기법을 이용해서 하나의 @GetMapping으로 처리할 수 있기 때문에 read() 기능을 수정해서 수정과 삭제에도 같은 메서드를 이용하도록 작업

 

TodoController의 read()를 다음과 같이 수정해서 "/todo/modify?tno=xxx"의 경로를 처리하도록 수정
 @GetMapping({"/read", "/modify"})
    public void read(Long tno, Model model) {
        // 1) request로 전달 받은 tno를 서비스에 전달해서 2)TodoDTO를 반환받아서 3)View 에 전달
        TodoDTO todoDTO = todoService.getOne(tno);
        log.info(todoDTO);
        model.addAttribute("dto", todoDTO);
    }

 

 

/WEB-INF/view/todo 폴더에 있는 read.jsp를 그대로 복사해서 modify.jsp를 구성


  👾  modify.jsp에서는 수정과 삭제 작업이 POST 방식으로 처리될 예정이므로 이를 위한 <form> 태그를 구성하고 수정이 가능한 항목들은 편집이 가능하도록 함

<form action="/todo/modify" method="post">
    <div class="input-group mb-3">
        <span class="input-group-text">Tno</span>
        <input type="text" name="tno" class="form-control" value="${dto.tno}" readonly>
    </div>
    <div class="input-group mb-3">
        <span class="input-group-text">Title</span>
        <input type="text" name="title" class="form-control" value="${dto.title}">
    </div>
    <div class="input-group mb-3">
        <span class="input-group-text">DueDate</span>
        <input type="date" name="dueDate" class="form-control" value="${dto.dueDate}">
    </div>
    <div class="input-group mb-3">
        <span class="input-group-text">Writer</span>
        <input type="text" name="writer" class="form-control" value="${dto.writer}" readonly>
    </div>
    <div class="form-check">
        <label class="form-check-label">
            Finished &nbsp;
        </label>
        <input type="checkbox" name="finished"
            class="form-check-input" ${dto.finished ? "checked" : ""}>
    </div>
    <div class="my-4">
        <div class="float-end">
            <button type="button" class="btn btn-danger">Remove</button>
            <button type="button" class="btn btn-primary">Modify</button>
            <button type="button" class="btn btn-secondary">List</button>
        </div>
    </div>
</form>

 

 

 

 ✓  제목 / 만료일 / 완료는 수정이 가능하도록 수정
  ✓  화면 아래 버튼은 삭제, 수정, 목록 버튼이 추가
  ✓  브라우저를 통해 "/todo/modify?tno=xx"와 같은 경로로 화면이 나오는 것 확인

 

 

 

 

 

 

 


 

1)  Remove 버튼의 처리


Remove 버튼의 클릭은 자바 스크립트를 이용해서 <form> 태그의 action을 조정하는 방식으로 동작하게 구성

<script>
    const frmModify = document.querySelector('form');
    
    document.querySelector('.btn-danger').addEventListener('click', function () {
        frmModify.action = '/todo/remove';
        frmModify.method = 'post';
        frmModify.submit();
    });
</script>

 

  ✓  Remove 버튼은 class 속성이 "btn-danger"이므로 이를 이용해서 클릭 이벤트를 처리

 

TodoController에는 POST 방식으로 동작하는 remove() 메서드를 설계
    @PostMapping("/remove")
    public String remove(Long tno, RedirectAttributes redirectAttributes) {
        log.info("-----remove----");
        log.info("tno: " + tno);

        todoService.remove(tno);
        return "redirect:/todo/list";
    }

 

  ✓  우선 tno 파라미터가 정상적으로 전달되는지 확인하고 목록으로 이동하도록 구성

  ✓  Remove 버튼을 누르면 다음과 같은 로그 출력 확인


 

2) TodoMapper와 TodoService 처리

TodoMapper에는 delete() 메서드를 추가하고 TodoMapper.xml에는 sql을 추가
public interface TodoMapper {
    String getTime();

    void insert(TodoVO todoVO);

    List<TodoVO> selectAll();

    TodoVO selectOne(Long tno);

    void delete(Long tno);
}
    <delete id="delete">
        DELETE FROM tbl_todo WHERE tno = #{tno}
    </delete>

 

test 코드 작성
  @Test
    public void testDelete() {
        // 1) tno로 데이터를 반환해서 정상 출력 확인 2) 삭제 3) 다시 tno로 데이터를 반환해서 삭제 확인.
        Long tno = 2L;
        TodoVO todoVO = todoMapper.selectOne(tno);
        log.info(todoVO);

        todoMapper.delete(tno);

        todoVO = todoMapper.selectOne(tno);
        log.info(todoVO);

    }

 

TodoServce / TodoServiceImpl에는 remove() 메서드를 작성
public interface TodoService {
    void register(TodoDTO todoDTO);

    List<TodoDTO> getAll();

    TodoDTO getOne(Long tno);

    void remove(Long tno);
}
    @Override
    public void remove(Long tno) {
        todoMapper.delete(tno);
    }

 

 

TodoController 에서 TodoService의 remove()를 호출하는 코드를 추가
    @PostMapping("/remove")
    public String remove(Long tno, RedirectAttributes redirectAttributes) {
        log.info("-----remove----");
        log.info("tno: " + tno);

        todoService.remove(tno);
        return "redirect:/todo/list";
    }

 

  ✓  브라우저를 통해서 특정한 번호를 가진 게시물이 삭제 되는지 확인

 


2.  Todo의 수정 기능 개발

Todo의 수정 기능은 수정이 가능한 항목들만 변경되어야 하므로 SQL이 복잡해짐

 

TodoMapper에 메서드 추가
public interface TodoMapper {
    String getTime();

    void insert(TodoVO todoVO);

    List<TodoVO> selectAll();

    TodoVO selectOne(Long tno);

    void delete(Long tno);

    void update(TodoVO todoVO);
}

 

TodoMapper.xml 코드 추가
<update id="update">
    UPDATE tbl_todo SET title = #{title}, dueDate = #{dueDate}, 
        finished = #{finished} WHERE tno = #{tno}
</update>

 

test 코드 추가
@Test
    public void testUpdate() {
        Long tno = 12L;
        TodoVO todoVO = TodoVO.builder()
                .tno(tno)
                .title("수정")
                .dueDate(LocalDate.parse("2024-4-26"))
                .finished(true)
                .build();

        todoMapper.update(todoVO);
        log.info(todoMapper.selectOne(tno));
    }

 

 

TodoService / TodoServiceImpl 에서는 TodoDTO를 TodoVO로 변환해서 처리
public interface TodoService {
    void register(TodoDTO todoDTO);

    List<TodoDTO> getAll();

    TodoDTO getOne(Long tno);

    void remove(Long tno);

    void modify(TodoDTO todoDTO);
}
 @Override
    public void modify(TodoDTO todoDTO) {
        log.info("...modify()...");

        TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
        log.info(todoVO);
        todoMapper.update(todoVO);
    }

 


1)  checkbox를 위한 Formatter


👾  수정 작업에서는 화면에서 체크박스를 이용해서 완료여부 finished (boolean) 를 처리하게 됨
👾  문제는 브라우저가 체크박스가 클릭된 상태일때 전송되는 값은 "on"이라는 값을 전달
👾  TodoDTO로 데이터를 수집할 때에는 문자열 "on"을 boolean 타입으로 처리할 수 있어야 하므로 컨트롤러에서 데이터를 수집할 때 타입을 변경해 주기 위한 CheckboxFormatter 를 formatter 패키지에 추가해서 개발

public class CheckBoxFormatter implements Formatter<Boolean> {
    @Override
    public Boolean parse(String text, Locale locale) throws ParseException {
       if (text == null) return  false;
       return text.equals("on");
    }

    @Override
    public String print(Boolean object, Locale locale) {
        return object.toString();
    }
}

 

servlet-context.xml에 format 라이브러리 추가
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
        <set>
            <bean class="com.example.spring_ex_01_2404.controller.formatter.LocalDateFormatter"/>
            <bean class="com.example.spring_ex_01_2404.controller.formatter.CheckBoxFormatter"/>
        </set>
    </property>
</bean>

 


2)  TodoController의 modify()

@PostMapping("/modify")
    public String midify(@Valid TodoDTO todoDTO,
                         BindingResult bindingResult,
                         RedirectAttributes redirectAttributes) {
        if (bindingResult.hasErrors()) {
        // 유효성 검사 결과 에러가 있으면 수정 페이지로 돌아감
            log.info("has error");
            
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            redirectAttributes.addAttribute("tno", todoDTO.getTno());
            return "redirect:/todo/mofidy";
        }
        log.info(todoDTO);
        todoService.modify(todoDTO);
        return "redirect:/todo/list";
    }

 

  ✓  @Valid를 이용해서 필요한 내용들을 검증하고 문제가 있는 경우에는 다시 "/todo/modify"로 이동시키는 방식을 사용
  ✓  "/todo/modify"로 이동할 때는 tno 파라미터가 필요하기 때문에 RedirectAttributes의 addAttribute을 이용

 

/WEB-INF/view/todo/modify.jsp 에 검증된 정보를 처리하는 코드를 추가


  👾  <form> 태그가 끝난 후에는 <script> 태그를 이용

<script>
    const serverValidResult = {};
    <c:forEach items="${errors}" var="error">
    serverValidResult['${error.getField()}'] = '${error.defaultMessage}';
    </c:forEach>
    console.log(serverValidResult);
</script>
<script>
    const frmModify = document.querySelector('form');
                             
    document.querySelector('.btn-danger').addEventListener('click', function () {
        frmModify.action = '/todo/remove';
        frmModify.method = 'post';
        frmModify.submit();
    });
                            
    document.querySelector('.btn-primary').addEventListener('click', function () {
        frmModify.action = '/todo/modify';
        frmModify.method = 'post';
        frmModify.submit();
    });
                           
    document.querySelector('.btn-secondary').addEventListener('click', function () {
        self.location = '/todo/list';
    });
</script>

 

 

 

[ 내용 참고 : IT 학원 강의 ]

+ Recent posts