본문 바로가기

Spring Boot 수업 정리

Spring boot 3일차

● 리스트에서 제목을 누르면 상세페이지로 이동할 수 있도록 list에 title부분을 수정

<td> <a th:href="@{/board/detail(bno=${bvo.bno})}">[[${bvo.title }]]</a></td>

 

  detail 페이지를 구성해줄건데 이번에는 modfiy.html을 만들어서 수정화면으로 넘어가서 수정하는 것이아닌 상세페이지에서 버튼을 누르면 detail 페이지의 상태를 modify 페이지의 상태로 만들어보도록 함.

수정을 위해서는 form에서 post할 필요가 있으므로 form안에 내용들을 넣어주도록함.

<div layout:fragment="content">
   <div>
       <form action="/board/modify" method="post" id="modForm">
       <input type="hidden" name="bno" th:value="${bvo.bno}">
       <div class="mb-3">
           <label for="n" class="form-label">no.</label>
           <input type="text"
                  class="form-control" id="n" name="bno" th:value="${bvo.bno }"
                  readonly="readonly">
       </div>
       <div class="mb-3">
           <label for="t" class="form-label">title</label>
           <input type="text"
                  class="form-control" id="t" name="title" th:value="${bvo.title }"
                  readonly="readonly">
       </div>
       <div class="mb-3">
           <label for="w" class="form-label">writer</label>
           <input type="text"
                  class="form-control" id="w" name="writer" th:value="${bvo.writer }"
                  readonly="readonly"> <span class="badge text-bg-info">[[${bvo.regDate }]]</span>
       </div>
       <div class="mb-3">
           <label for="c" class="form-label">content</label>
           <textarea class="form-control" id="c" rows="3" name="content"
                     readonly="readonly">[[${bvo.content }]]</textarea>
       </div>
       <button type="button" id="listBtn" class="btn btn-info">List</button>
       <button type="button" id="modBtn" class="btn btn-primary">Modify</button>
       <button type="button" id="delBtn" class="btn btn-danger">Delete</button>
       </form>
    </div>
 </div>

 

● 컨트롤러에서 list에서 보낸 bno를 받아서 getDetail로 BoardVO 객체를 가져와서 상세페이지에 addAttribute를 사용해 bvo로 추가해주도록 해줌.

    @GetMapping("/detail")
    public void detail(Model m, @RequestParam("bno") int bno){

        BoardVO boardVO = bsv.getDetail(bno);

        log.info(" bvo >> {} ", boardVO);

        m.addAttribute("bvo", boardVO);
    }

 

● js파일을 하나만들어주고 list 버튼을 누르면 list로 이동할 수 있도록 만들어줌.

document.getElementById('listBtn').addEventListener('click', () => {
    // list로 이동 
    location.href="/board/list";
});

 

● 수정버튼을 누르면 수정할 부분의 readOnly를 false로 수정이 가능하도록 바꿔줘야하고,

수정버튼과 삭제버튼을 지우고 submit 버튼으로 수정을 완료하는 버튼을 추가할 수 있도록함.

document.getElementById('modBtn').addEventListener('click', () => {
    // title, content 의 readonly를 해지 readOnly = true = false
    document.getElementById('t').readOnly = false;
    document.getElementById('c').readOnly = false;

    // modBtn delBtn 삭제
    document.getElementById('modBtn').remove();
    document.getElementById('delBtn').remove();

    // modBtn => submit 버튼으로 변경 추가
    let modBtn = document.createElement('button'); // <button></button>
    modBtn.setAttribute('type','submit'); // <button type="submit"></button>
    modBtn.classList.add('btn', 'btn-outline-primary');
    modBtn.innerText="submit";  // <button type="submit" class="btn btn-outline-primary">submit</button>

    // form 태그의 자식 요소로 추가 - form 가장 마지막에 추가됨.
    document.getElementById('modForm').appendChild(modBtn);
});

 

form 태그의 id를 하나넣어주고 appendChild로 만들어준 수정버튼을 form태그안의 자식요소로 마지막에 추가해주도록함.

 

● 컨트롤러에서 post로 전송된 수정한 정보를 update 할 수 있도록 구현해줌. 

 @PostMapping("/modify")
  public String modify(BoardVO boardVO){
      int isOk = bsv.modify(boardVO);
      return "redirect:/board/detail?bno=" + boardVO.getBno();
  }

 

● 삭제 버튼을 눌렀을 때 bno를 넣어서 링크로 이동 할 수 있도록 함.

document.getElementById('delBtn').addEventListener('click', () => {
    let bnoVal = document.getElementById('n').value;
    location.href=`/board/delete?bno=${bnoVal}`;
});

 

● 받아온 bno의 BoardVO 객체를 컨트롤러에서 delete 할 수 있도록 구현함.

    @GetMapping("/delete")
    public  String delete(@RequestParam("bno") int bno){
        int isOk = bsv.delete(bno);
        return "redirect:/board/list";
    }

 


 

페이지네이션 

pagingVO(DB처리에 필요한 값을 전달하기 위한 객체)  / pagingHandler
pagingHandler (화면 처리에 필요한 값을 전달하기 위한 객체) 

 

●  PagingVO

select * from board 조건 limit 번지, 개수
번지 : 0번지부터 => 현재페이지번호 한페이지의 개수에 따라 계산되어야 함.
개수 : 한페이지에 출력할 리스트의 개수 => 10 

limit 0,10 / limit 10,10 / limit 20,10
1 2 3 4 5 => 하단 페이지 번호 
번지를 구하는 공식 : (페이지번호 -1) * 개수 

 

● 부트스트랩을 이용해서 list 화면 아래에 페이지네이션 라인을 추가

<nav aria-label="Page navigation example">
      <ul class="pagination">
           <li class="page-item">
                <a class="page-link" href="#" aria-label="Previous">
                    <span aria-hidden="true">&laquo;</span>
                </a>
          </li>
          <li class="page-item"><a class="page-link" th:href="@{/board/list(pageNo=1,qty=10)}">1</a></li>
          <li class="page-item"><a class="page-link" th:href="@{/board/list(pageNo=2,qty=10)}">2</a></li>
          <li class="page-item">
               <a class="page-link" href="#" aria-label="Next">
                   <span aria-hidden="true">&raquo;</span>
               </a>
         </li>
     </ul>
</nav>

 

● PagingVO 생성하고 현재 페이지와 게시글 개수를 이용해서 limit의 번지수를 구하기.

@ToString
@AllArgsConstructor
@Setter
@Getter
public class PagingVO {
    private int pageNo; // 현재 페이지번호
    private int qty; // 한페이지에 출력되는 게시글 개수

    public PagingVO(){
        pageNo = 1;
        qty = 10;
    }

    public int getStartIndex(){
        return (this.pageNo-1)*this.qty;
    }
    
}

 

●  PagingHandler

 

- 화면에서 보이는 페이지네이션의 시작값 : startPage
- 화면에서 보이는 페이지네이션의 끝값 : endPage
- 페이지네이션의 숫자 개수 10 
- 정말 마지막 페이지 값 : realEndPage
- prev, next의 존재여부 : prev, next 
- 전체 리스트 개수 : totalCount => DB에서 가져와야 하는 값 ( 파라미터로 전달 )
- pgvo 값 : 파라미터로 전달 

@Getter
@Setter
@ToString
public class PagingHandler {
    private int startPage;
    private int endPage;
    private int realEndPage;
    private boolean prev, next;

    private int totalCount;
    private PagingVO pgvo;

    public PagingHandler(PagingVO pgvo, int totalCount){
        this.pgvo = pgvo;
        this.totalCount = totalCount;

        // 1~10 => 10 / 11~20 => 20 / 21~30 => 30 ...
        // (현재 페이지번호 / 10) 올림 => * 10
        this.endPage = (int)Math.ceil(pgvo.getPageNo() / (double)pgvo.getQty()) * 10;
        this.startPage = endPage - (pgvo.getQty()-1);

        this.realEndPage = (int)Math.ceil(totalCount / (double)pgvo.getQty());

        if(realEndPage < endPage){
            this.endPage = realEndPage;
        }
        this.prev = this.startPage > 1;  // 1 11 21
        this.next = this.endPage < realEndPage;
    }
}

 

prev,next는 true / false로 false 면 이전, 다음버튼을 disabled 시켜줌.

 

●  컨트롤러 list부분에 pgvo를 받아오고 게시글의 총 total 개수를 구해온다음 페이징 핸들러를 안에 pgvo와 토탈게시글 수를 넣어 생성해준 후 addAttribute 해줌

    @GetMapping("/list")
    public String list(Model m, PagingVO pgvo){

        int totalCount = bsv.getTotal();

        PagingHandler ph = new PagingHandler(pgvo,totalCount);

        m.addAttribute("list", bsv.getList(pgvo));
        m.addAttribute("ph",ph);

        return "/board/list";
    }

 

●  ${#numbers.sequence(from,to)} 를 이용해서 시작페이지부터 끝페이지까지 나타낼 수 있도록해줌

th:classappend를 이용해서 현재페이지가 i일때 active가 클래스에 추가되도록해서 현재 페이지를 나타낼 수 있도록해줌.

<th:block th:each="i:${#numbers.sequence(ph.startPage, ph.endPage)}">
       <li class="page-item" th:classappend="${ph.pgvo.pageNo eq i ? 'active' : ''}"><a class="page-link" th:href="@{/board/list(pageNo=${i},qty=10)}">[[${i}]]</a></li>
</th:block>

 

●  이전, 다음버튼에 prev,next가 false 면 disabled하게 classappend해주고 누르면 페이지는 각자 시작페이지 -1

끝페이지 +1를 해주어서 이동할 수 있도록 해줌.

 <li class="page-item" th:classappend="${ph.prev eq false ? 'disabled' : ''}">
       <a class="page-link" th:href="@{/board/list(pageNo=${ph.startPage-1},qty=10)}" aria-label="Previous">
          <span aria-hidden="true">&laquo;</span>
       </a>
</li>
<li class="page-item"  th:classappend="${ph.next eq false ? 'disabled' : ''}">
       <a class="page-link" th:href="@{/board/list(pageNo=${ph.endPage+1},qty=10)}" aria-label="Next">
            <span aria-hidden="true">&raquo;</span>
       </a>
</li>

 


검색기능 구현

 

● PagingVO에  type과 keyword를 추가하고 타입과 키워드가 입력되지않는 현재페이지와 게시글 양만 가지고있는 생성자를 만들어줌.

    private String type;
    private String keyword;
    
    public PagingVO(int pageNo, int qty) {
    this.pageNo = pageNo;
    this.qty = qty;
    }

 

타입을 받아와서 한글자씩 잘라서 배열로 리턴해주는 getter 메서드를 생성

    public String[] getTypeToArray() {
        return this.type == null ? new String[]{} : this.type.split("");
    }

 

● 컨트롤러 list에서 총 개수를 구하는부분에 pgvo를 추가로 넣어주고 

 int totalCount = bsv.getTotal(pgvo);

 

● 매퍼에서 sql 구문으로 getTypeToarray를 collection으로 받아와서 배열 안의 요소 하나를 type으로 설정하고

type이 t,w,c 와 같다면 keyword를 포함하고있는 애들을 추출하는 구문을 만들어줌

    <sql id="search">
        <if test="type != null">
            <trim prefix="where (" suffix=")" suffixOverrides="or">
                <foreach collection="typeToArray" item="type">
                    <trim suffix="or">
                        <choose>
                            <when test="type=='t'.toString()">
                                title like concat('%',#{keyword},'%')
                            </when>
                            <when test="type=='w'.toString()">
                                writer like concat('%',#{keyword},'%')
                            </when>
                            <when test="type=='c'.toString()">
                                content like concat('%',#{keyword},'%')
                            </when>
                        </choose>
                    </trim>
                </foreach>
            </trim>
        </if>
    </sql>

 

● getList와 getTotal 에 추가 

<include refid="search"></include>

 

● list 윗부분에 검색 화면을 구성해줌 

<div class ="container-fluid">
    <form action="/board/list" class="d-flex" role="search" >
       <select class="form-select form-select-sm" id="inputGroupSelect01" name="type" style="width: 70%; margin-right: 20px">
           <option th:selected="${ph.pgvo.type == null ? true : false}" >Choose...</option>
           <option th:value="t" th:selected="${ph.pgvo.type == 't' ? true : false}" >title</option>
           <option th:value="w" th:selected="${ph.pgvo.type == 'w' ? true : false}">writer</option>
           <option th:value="c" th:selected="${ph.pgvo.type == 'c' ? true : false}">content</option>
           <option th:value="tw" th:selected="${ph.pgvo.type == 'tw' ? true : false}" >title + writer</option>
           <option th:value="wc" th:selected="${ph.pgvo.type == 'wc' ? true : false}">writer + content</option>
           <option th:value="tc" th:selected="${ph.pgvo.type == 'tc' ? true : false}">title + content</option>
           <option th:value="twc" th:selected="${ph.pgvo.type == 'twc' ? true : false}">all</option>
        </select>

       <input class="form-control me-2" name="keyword" type="search" placeholder="Search..." aria-label="Search" th:value="${ph.pgvo.keyword }" >
       <input type="hidden" name="pageNo" value="1">
       <input type="hidden" name="qty" th:value="${ph.pgvo.qty }">
       <button type="submit" class="btn btn-success position-relative">
                  Search
            <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
            [[${ph.totalCount }]]
                  span class="visually-hidden">unread messages</span>
            </span>
        </button>

     </form>
</div>

 


파일기능 구현

 

● 라이브러리 추가

	implementation 'org.apache.tika:tika-core:2.4.1'
	implementation 'org.apache.tika:tika-parsers:2.4.1'
	implementation 'net.coobird:thumbnailator:0.4.17'

 

● file DB 구성

create table file(
uuid varchar(256),
save_dir varchar(256) not null,
file_name varchar(200) not null,
file_type int(1) default 0,
bno bigint not null,
file_size bigint,
reg_date datetime default now(),
primary key(uuid));

 

● FileVO 생성

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class FileVO {
    private String uuid;
    private String saveDir;
    private String fileName;
    private int fileType;
    private long bno;
    private long fileSize;
    private String regDate;
}

 

● BoardDTO 생성

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class BoardDTO {
    private BoardVO bvo;
    private List<FileVO> flist;
}

 

● register.html에 파일 추가, 출력라인 추가

 <!--/* file 추가 라인 */-->
 <div class="mb-3">
     <label for="file" class="form-label">File</label>
     <input type="file" class="form-control" id="file" name="files" multiple style="display:none">
     <button type="button" id="trigger" class="btn btn-primary">File Upload</button>
 </div>

 <!--/* file 출력 라인 */-->
<div class="mb-3" id="fileZone"></div>
<button type="submit" class="btn btn-primary" id="regBtn">register</button>

 

● boardRegister.js로 자바스크립트 파일하나 생성해주고 스크립트 추가

<script th:src="@{/js/boardRegister.js}"></script>

 

● 트리거 버튼이누르면 hidden으로 숨겨놨던 파일타입의 input이 클릭되도록 해주고 정규식을 이용해서 실행파일 막아주고 사이즈를 정해준다음 업로드가 불가능한지 가능한지 알 수 있게 구별해주는 메서드를 하나 만듬.

document.getElementById('trigger').addEventListener('click', () => {
    document.getElementById('file').click();
})

// 실행파일 막기 / 20MB 이상 막기 
const regExp = new RegExp("\.(exe|sh|bat|jar|dll|msi)$");
const maxSize = 1024*1024*20;

function fileValidation(fileName, fileSize){
    if(regExp.test(fileName)){
        return 0;
    } else if(fileSize > maxSize){
        return 0;
    } else {
        return 1;
    }
}

 

● 변화가있는 요소를 가져올 수 있게하고 file에 변화가 생기면 files를 가져와서 파일오브젝트에 저장해줌

출력 표시라인인 fileZone에 파일오브젝트안의 요소들을 for문으로 풀어서 목록그룹을 구성해서 출력할 수 있도록 함.

document.addEventListener('change', (e) => {
    if(e.target.id == 'file'){
        const fileObject = document.getElementById('file').files;
        console.log(fileObject);
        document.getElementById('regBtn').disabled = false;

        const fileZone = document.getElementById('fileZone');
        // 이전 추가 파일 삭제
        fileZone.innerHTML="";

        let ul = ` <ul class="list-group list-group-flush">`
        let isOk = 1; // 여러 파일에 대한 값을 확인하기위해 하나라도 검열에 걸리면 0 
        for(let file of fileObject){
            let valid = fileValidation(file.name, file.size)
            isOk *=  valid
            ul += `<li class="list-group-item">`;
            ul += `<div class="ms-2 me-auto">`;
            ul += `${valid ? '<div class="fw-bold text-success-emphasis">업로드 가능</div>' : '<div class="fw-bold text-danger-emphasis">업로드 불가능</div>'}`;
            ul += `${file.name}</div>`;
            ul += `<span class="badge text-bg-${valid ? 'success' : 'danger'} rounded-pill">${file.size}Bytes</span>`
            ul += `</li>`;
        }
        ul += `</li>`;
        fileZone.innerHTML = ul;
        if(isOk == 0){     
            document.getElementById('regBtn').disabled = true;
        }
    }
})

 

업로드가 불가능한 파일이라고 메서드에서 0으로 걸러지면 register버튼을 disabled 시켜준다.

스스로 다시 disabled가 false로 변하지않으므로 위에 다시 변화가 생길 때 false로 돌아가도록 추가해준다.

'Spring Boot 수업 정리' 카테고리의 다른 글

Spring boot 6일차  (1) 2024.11.19
Spring Boot 5일차  (1) 2024.11.18
Spring Boot 4일차  (0) 2024.11.15
Spring Boot 2일차  (1) 2024.11.13
Spring Boot 1일차  (0) 2024.11.13