● 리스트에서 제목을 누르면 상세페이지로 이동할 수 있도록 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">«</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">»</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">«</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">»</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 |