검색 조건으로 조회하고 페이지 링크를 거는 목록, 상세, 등록, 수정, 삭제 등을 다루는 평범한 게시판
* 파일 구조
/src/main/java/도메인/controller/Board2Controller.java, request/response 교통정리만 처리
/src/main/java/도메인/domain/Board2.java, 기본 데이타 구조 (DB 연계). Board 라고 해도 됨
/src/main/java/도메인/dto/PageRequestDTO.java, 목록 검색 조건과 페이지 정보
/src/main/java/도메인/repository/Board2Repository.java, DB 관리 (JPA 핵심이자 기술할게 없기도..)
/src/main/java/도메인/service/Board2Service.java, 인터페이스
/src/main/java/도메인/service/Board2ServiceImpl.java, 실제적인 비즈니스 로직 (DB & File)
/src/main/java/도메인/vo/CodeValueVO.java, 코드값 세팅용
/src/main/java/도메인/vo/PaginationVO.java, 목록 페이지 링크 세팅용
/src/main/resources/application.yml, 서비스 포트, DB datasource, logging 등등
/src/main/resources/templates/, Thymeleaf View
/src/main/resources/templates/board2/list.html, 목록 (검색 조건, 페이지 링크)
/src/main/resources/templates/board2/read.html
/src/main/resources/templates/board2/modify.html
/src/main/resources/templates/board2/register.html
/src/main/resources/templates/layouts/default.html, 기본 레이아웃, header/footer/container 구분
/src/main/resources/static/, 일반적인 파일 경로(css, js, 이미지)
/src/main/resources/static/test.html, 서버 작동 확인용
/src/main/resources/static/css/default.css, 기본 CSS
/src/main/resources/static/js/default.js, 기본 자바스크립트
/src/main/resources/static/js/jquery.min.css, 아직은 jquery 가 쓸만..
* build.gradle
plugins {
id 'org.springframework.boot' version '2.7.5'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
id 'java'
}
group = '도메인'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc' // JdbcTemplate 이지만, 혹시나
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // JPA
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2' // Mybatis 이지만, 혹시나
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation' // 유효성 검증, @Entity 멤버변수에 유효성 설정
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' // Layout
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.oracle.database.jdbc:ojdbc8' // JDK 1.8 용 ojdbc
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
* application.yml (들여쓰기 제대로 안 하면 에러) .xml 보다 상위(?)의 .yml
server.port : 8086 # 80 과 8080 은 다른 서비스가 사용하고 있어서 8086 으로..
spring :
datasource :
driver-class-name : oracle.jdbc.driver.OracleDriver
url : jdbc:oracle:thin:@아이피:포트/서비스명
username : 아이디
password : 비밀번호
jpa :
generate-ddl : 'true' # true 데이터베이스 고유 기능 사용
database : oracle
database-platform : org.hibernate.dialect.Oracle10gDialect
hibernate :
ddl-auto : update # update 변경 부분만 반영, create 기존 테이블 삭제후 재생성, none 사용하지 않음
show-sql : 'true' # true 로그에 쿼리 인쇄
mybatis :
mapper-locations : classpath:mappers/**/*.xml
config-location : classpath:mybatis-config.xml
logging :
level :
org :
hibernate : info # dd
* Board2.java
@Entity // javax.persistence.*, JPA 사용 명시. JPA 에서 DB 와 매칭
@Getter @Setter @ToString // lombok.*. 엔터티 마다 매번 기술해야 하는 코드를 줄이는 힘
@Table(name="web_test_board") // javax.persistence.*, 실제 DB 테이블명
//@EqualsAndHashCode(of="...") 를 반드시 기술할 필요는 없음
public class Board2{
@Id // PK 세팅. javax.persistence.*. 여러 칼럼 보다는 하나의 칼럼으로 id 세팅 요망
@SequenceGenerator(name="BOARD_SEQUENCE_GEN", // javax.persistence.*
sequenceName="seq_web_test_board", initialValue=1, allocationSize=1) // 실제 DB 시퀀스명
@GeneratedValue(strategy=GenerationType.SEQUENCE, // javax.persistence.*
generator="BOARD_SEQUENCE_GEN")
//@Column(name="board_no") // javax.persistence.*. 실제 DB 칼럼명. Camel 표기방식을 따르면 생략 가능
private Integer boardNo; // int 나 long 등등 primitive type 불가, Integer 나 Long 은 가능
// Long 으로 쌓이는 데이타 보다는 Integer 가 적당할듯.. 작은 데이타가 빠르다
@NotBlank // javax.validation.constraints.*, .html 타임리프에 입력값에 대한 검증 메시지 표시
@Column(length=200, nullable=false) // DB 제한 가능
private String title;
@Lob // javax.persistence.*
private String content1;
@Lob
private String content2; // 하나의 DB Table 에 Clob 2개를 입력해 봄
// JdbcTemplate 에서는 select 로 시퀀스 잡고, 기본 데이타 insert ~ for update 후에 update 로 clob 을 넣는데..
// JPA 에선 repository.save() 가 DB 호출 1번에 insert 하고 insert 된 시퀀스 포함 전체 데이타를 리턴해 줌
private String writer;
@CreationTimestamp // insert 시 저장. org.hibernate.annotations.*. DB 에선 Date type 이어도 됨
private LocalDateTime regDate;
@UpdateTimestamp // update 시 저장. org.hibernate.annotations.*
private LocalDateTime updDate;
}
* CodeValueVO.java
@Getter // lombok.*. 생성자로 세팅하므로 @Setter 불필요
public class CodeValueVO{ // Value Object
private String code;
private String value;
public CodeValueVO() {
super();
}
public CodeValueVO(String code, String value) {
this.code = code;
this.value = value;
}
}
* PaginationVO.java
@Getter // lombok.*
// 추출된 데이타에 대한 페이지 링크 처리
public class PaginationVO<T>{ // Value Object
private Page<T> page; // org.springframework.data.domain.*
private Pageable prevPageable; // org.springframework.data.domain.*
private Pageable nextPageable;
private Pageable currentPageable;
private List<Pageable> pageableList; // java.util.*
private int currentPageNumber;
private int totalPageCount;
public PaginationVO(Page<T> page) {
this.page = page;
this.currentPageable = page.getPageable();
this.currentPageNumber = this.currentPageable.getPageNumber() +1;
this.totalPageCount = page.getTotalPages();
this.pageableList = new ArrayList<>();
calcPages();
}
private void calcPages() {
int endPageNumber = (int)(Math.ceil(this.currentPageNumber /10.0) *10);
int startPageNumber = endPageNumber -9;
Pageable startPageable = this.currentPageable;
for(int i = startPageNumber; i<this.currentPageNumber; i++) {
startPageable = startPageable.previousOrFirst();
}
this.prevPageable = startPageable.getPageNumber()<=0 ? null : startPageable.previousOrFirst();
if(this.totalPageCount < endPageNumber) {
endPageNumber = this.totalPageCount;
this.nextPageable = null;
}
for(int i=startPageNumber; i<=endPageNumber; i++) {
pageableList.add(startPageable);
startPageable = startPageable.next();
}
this.nextPageable = ((startPageable.getPageNumber() + 1) < totalPageCount) ? startPageable : null;
}
}
* Board2Repository.java (인터페이스이긴 한데, 구현하는 .java 를 만들 필요는 없음)
public interface Board2Repository extends JpaRepository<Board2, Integer>{ // org.springframework.data.jpa.repository.*
// Pageable 을 사용함으로써, 페이지 내비게이션이 가능해짐
Page<Board2> findAll(Pageable pageable); // org.springframework.data.domain.*
// ..Containing 가 특수문자가 입력되는 경우에도, DB 쿼리 생성시 escape 키워드를 사용하면서 잘 찾아 줌
public Page<Board2> findByTitleContaining(String title,Pageable pageable);
public Page<Board2> findByContent1Containing(String content1,Pageable pageable);
public Page<Board2> findByContent2Containing(String content2,Pageable pageable);
public Page<Board2> findByWriterContaining(String writer,Pageable pageable);
public Page<Board2> findByTitleContainingOrContent1Containing(String title, String content1,Pageable pageable);
public Page<Board2> findByContent1ContainingOrWriterContaining(String content1, String writer,Pageable pageable);
public Page<Board2> findByTitleContainingOrContent1ContainingOrWriterContaining(String title, String content1, String writer,Pageable pageable);
}
* PageRequestDTO.java
// 목록에서 페이지 내비게이션 담당
@Getter @Setter @ToString
public class PageRequestDTO{ // Data Transfer Object
private int page;
private int sizePerPage;
private String searchType;
private String keyword;
public PageRequestDTO() {
this.page = 1;
this.sizePerPage = 1; // 사용자 화면에서 받아올 수도 있어서 set method 세팅. 통상 화면당 10개의 레코드 표시
}
public void setPage(int page) { // @Setter 와 중복시 사용자가 기술한 내용이 적용됨
this.page = page<=0 ? 1 : page; // 마이너스 페이지 불허
}
public void setSizePerPage(int size) { // @Setter 와 중복시 사용자가 기술한 내용이 적용됨
this.sizePerPage = (size<=0 || size>100) ? 10 : size; // 하나의 페이지에 마이너스 라인이나 100 라인 이상 불허
}
public int getPageStart() {
return (this.page -1) * this.sizePerPage;
}
}
* Board2Controller.java
@Controller // org.springframework.stereotype.*, 애너테이션이 있어야 컨트롤 타워가 됨
@RequestMapping("/board2") // org.springframework.web.bind.annotation.*. 기본 상위 경로
@RequiredArgsConstructor // lombok.*. final 멤버변수 인젝션용
@Slf4j
public class Board2Controller{
private final Board2Service service; // 비즈니스 로직 처리용
private final List<CodeValueVO> searchTypes = new ArrayList<CodeValueVO>(); list() 에서 매번 세팅하지 않고 한 번 만 적용
// 신규 등록한 다음엔 첫 페이지로 가기 때문에 @ModelAttribute("pr") 설정하지 않음
// Model 에 담으면 View 에서 Thymeleaf 가 활용
@GetMapping("/register") // class 에 기술된 애너테이션과 합쳐져 '/board2/register' 경로가 됨
public void registerForm(Model model) throws Exception{
model.addAttribute(new Board2()); // TemplateInputException 방지. 타임리프 form 문제 id 를 board2 로 세팅하기 때문
}
// 목록과의 검색조건과 페이지 연계를 위해 @ModelAttribute 이용 (목록 -> 상세 -> 수정 -> (DB) -> 목록)
// @ModelAttribute() 로 form 데이타를 받아서 Control 에서 사용하고 (Model 에 다시 담지 않고) View 에 전달
// form 태그의 id 속성과 동일하면(여기선 'pr') 상하관계를 따져서 세팅하고,
// 상이하면(pr 이 아닌 'board2' 같은..) input 등등의 name 기준으로 파라미터를 담아준다
@GetMapping("/modify")
public void modifyForm(Integer boardNo, @ModelAttribute("pr") PageRequestDTO pr, Model model) throws Exception{
model.addAttribute(service.read(boardNo)); // 수정할 상세 정보 추출
}
// 목록과의 연계를 위해 @ModelAttribute 이용 (목록 -> 목록)
// @ModelAttribute("pr") PageRequestDTO pr 이 아니라 PageRequestDTO pr 였으면 pr 에 해당하는 object 가 없을 때 에러.
// @ModelAttribute("pr") 는 list() 매핑시 request 에서 PageRequestDTO 에 해당하는 값을 선별해서 담아주고 view 에서도 그대로 사용
@GetMapping("/list")
public void list(@ModelAttribute("pr") PageRequestDTO pr, Model model) throws Exception{
model.addAttribute("board2", new Board2());// 목록에선 체크된 게시글이 없으므로 빈 내용으로 생성. 상세 조회시 사용하므로 추가
Page<Board2> pg = service.list(pr); // {"page":6,"sizePerPage":1,"pageStart":5}
model.addAttribute("pgn", new PaginationVO(pg));
if(searchTypes.size()==0) { // 데이타가 없는 경우 1회만 설정. 목록 검색 조건 기본값 설정
searchTypes.add(new CodeValueVO("","선택해 주세요"));
searchTypes.add(new CodeValueVO("t","제목"));
searchTypes.add(new CodeValueVO("c1","내용1")); // Lob 이 여럿일 때 어떻게 작용하는지 확인하기 위해 1과 2로 구분
searchTypes.add(new CodeValueVO("w","저자"));
searchTypes.add(new CodeValueVO("tc1","제목 OR 내용1")); // 많이 사용
searchTypes.add(new CodeValueVO("c1w","내용1 OR 저자"));
searchTypes.add(new CodeValueVO("tcw","제목 OR 내용1 OR 저자"));
}
model.addAttribute("searchTypes",searchTypes);
}
// 목록과의 연계를 위해 @ModelAttribute 이용 (목록 -> 상세 -> 목록)
// View 에 데이타 전달하기 위해 Model 이용
@GetMapping("/read")
public void read(Integer boardNo, @ModelAttribute("pr") PageRequestDTO pr, Model model) throws Exception{
model.addAttribute(service.read(boardNo));
}
// 등록한 다음엔 첫 페이지로 가기 때문에 @ModelAttribute("pr") 설정하지 않음 (필요시 세팅 가능)
// redirect return 하기 때문에 Model 대신 RedirectAttributes 이용
// @Validated, org.springframework.validation.annotation.*
// BindingResult, org.springframework.validation.*
// RedirectAttributes, org.springframework.web.servlet.mvc.support.*
@PostMapping("/register")
public String register(@Validated Board2 board, BindingResult br, RedirectAttributes ra) throws Exception{
if(br.hasErrors()) return "board2/register" ; // @Entity @NotNull @Column 등으로 설정한 오류시 get register 로 이동하여 메시징
Board2 result= service.register(board); // 등록 성공시 true. regDate 세팅하지 않아도 @CreateTimestamp 에 의해 자동 설정
// DB 서버와 웹서버의 시차가 있는 경우, 간혹 웹서버의 시각을 세팅하기도 하는데..
// 가급적이면 두 서버의 시간은 동기화 하고, DB 서버 중심으로 돌아가는 것이 좋다
// 여러 테이블을 다루는 경우 각 테이블에 동일 시각을 넣기 위해서는 작업 개시 시각을 select 한 다음 재사용
// addFlashAttribute() 로 세팅하면, 해당 화면을 새로고침 했을 때 다음 화면에선 값이 소멸되고 없음. 1회용 메시징
ra.addFlashAttribute("msg", result.getBoardNo()!=null ? "등록되었습니다." : "등록되지 않았습니다.");
return "redirect:/board2/list"; // .html 에서와 달리 절대경로 사용.
// 작업 성공 url 을 한번 더 보여주고 사용자가 어디로 이동할 지 구성하는 화면도 가능하긴 하다
// 이런 경우 RedirectAttributes 가 아니라 Model 을 사용하면 된다
// Model 사용시 return "board2/list" 와 같이 기술. '/board2/list' 라 해도 되긴 하지만 주소창에 '//board2/list' 라고 표시된다. ㅠ.
}
// 목록과의 연계를 위해 @ModelAttribute 이용 (목록 -> 상세 -> 수정 -> (DB) -> 목록)
// redirect return 하기 때문에 Model 대신 RedirectAttributes 이용
@PostMapping("/modify")
public String modify(@Validated Board2 board, @ModelAttribute("pr") PageRequestDTO pr, BindingResult br, RedirectAttributes ra) throws Exception{
if(br.hasErrors()) return "board2/modify" ; // @Entity @NotNull @Column 등으로 설정한 오류시 get modify 로 이동하여 메시징
boolean result = service.modify(board); // 수정 성공시 true
ra.addAttribute("page", pr.getPage()); // redirect 하기 때문에 @ModelAttribute() 로 전달받았어도 RedirectAttributes() 에 담는다
ra.addAttribute("sizePerPage", pr.getSizePerPage());
ra.addAttribute("searchType", pr.getSearchType());
ra.addAttribute("keyword", pr.getKeyword());
ra.addFlashAttribute("msg", result ? "수정되었습니다. " : "수정되지 않았습니다. ");
return "redirect:/board2/list"; // 수정 후 조회 화면 또는 수정 화면으로 갈 수도 있지만 여기선 목록 화면으로 이동
}
// 목록과의 연계를 위해 @ModelAttribute 이용 (목록 -> 상세 -> 삭제(DB) -> 목록)
// redirect return 하기 때문에 Model 대신 RedirectAttributes 이용
@PostMapping("/remove")
public String remove(Integer boardNo, @ModelAttribute("pr") PageRequestDTO pr, RedirectAttributes ra) throws Exception{
boolean result = service.remove(boardNo); // 삭제 성공시 true
ra.addAttribute("page", 1); // 삭제한 다음에는 첫 페이지로 이동
ra.addAttribute("sizePerPage", pr.getSizePerPage()); // redirect 하기 때문에 @ModelAttribute() 로 전달받았어도
// RedirectAttributes() 에 담는다
ra.addAttribute("searchType", pr.getSearchType());
ra.addAttribute("keyword", pr.getKeyword());
ra.addFlashAttribute("msg", result ? "삭제되었습니다. " : "삭제되지 않았습니다. ");
return "redirect:/board2/list";
}
}
* Board2Service.java
public interface Board2Service{
public Board2 register(Board2 board) throws Exception;
public boolean modify(Board2 board) throws Exception;
public Board2 read(Integer boardNo) throws Exception; // 파라미터를 Board2 board 로 해도 되지만, 가벼울수록 좋다
public boolean remove(Integer boardNo) throws Exception; // 파라미터를 Board2 board 로 해도 되지만, 가벼울수록 좋다
public Page<Board2> list(PageRequestDTO pr) throws Exception;
}
* Board2ServiceImpl.java
@Service // org.springframework.stereotype.*
@RequiredArgsConstructor // lombok.*
@Slf4j // lombok.extern.slf4j.*
public class Board2ServiceImpl implements Board2Service{
private final Board2Repository repository; // @RequiredArgsConstructor 가 interface 에 자동 주입
@Override // java.lang.*
public Board2 register(Board2 board) throws Exception{
return repository.save(board); // DB 저장. select 1회 insert(또는 update) 1회, DB 호출
}
@Override
public boolean modify(Board2 board){
boolean result = false;
try {
Board2 bd2 = this.read(board.getBoardNo()); // select 1회, DB 호출
if(bd2.getRegDate()==null) return result; // 수정 대상이 없음
bd2.setTitle(board.getTitle()); // 전달받은 데이타를 DB 데이타에 옮겨 담음
bd2.setContent1(board.getContent1());
bd2.setContent2(board.getContent2());
bd2.setWriter(board.getWriter());
repository.save(bd2); // DB 저장. 에러시 예외처리. select 1회 update 1회, DB 호출
result = true;
}catch(Exception e) {
String msg = e.getMessage();
if(msg.indexOf("EntityNotFoundException")>-1) ; // 수정 대상 없음
else log.info("\n-.Board2ServiceImpl.modify.Exception: "+msg); // 추가적인 예외 발생하는지 체크
}finally {
}
return result;
}
@Override
public Board2 read(Integer boardNo) throws Exception{
Board2 result = new Board2(); // 조회 내역이 없어도 화면에 표시하려고 생성자 할당
if(boardNo==null) return result;
result = repository.getReferenceById(boardNo); // getOne() 은 Deprecated. select 1회, DB 호출
return result;
}
@Override
public boolean remove(Integer boardNo){
boolean result = false;
try {
repository.deleteById(boardNo); // return 없음. Exception 으로 처리, select 1회 delete 1회, DB 호출
result = true; // deleteById() 실행시 예외발생하면 result = true; 실행되지 않으므로 return false 됨. 그래서 try 문 사용
}catch(Exception e) {
String msg = e.getMessage();
if(msg.indexOf("EmptyResultDataAccessException")>-1) ; // 삭제 대상 없음
else log.info("\n-.Board2ServiceImpl.remove.Exception: "+msg); // 추가적인 예외 발생하는지 체크
}finally {
}
return result;
}
@Override
public Page<Board2> list(PageRequestDTO pr) throws Exception{
int pn = pr.getPage() -1; // 사용자 화면에선 1페이지, java 에선 0번째
int spp = pr.getSizePerPage(); // 페이지 당 레코드수. 한 화면에 보여지는 라인수
// 대체로 화면당 10 건 표시
String st = pr.getSearchType(); // 검색 유형
String kw = pr.getKeyword(); // 검색 단어
Pageable p = PageRequest.of(pn, spp, Sort.Direction.DESC, "boardNo");
Page<Board2> pg = null;
// 검색 유형에 맞게 처리
if(st==null || kw==null || st.length()==0 || kw.length()==0) pg = repository.findAll(p);
else {
switch(st) { // String 도 switch 가능, 현재 JDK1.8 이용 중
case "n" : pg = repository.findAll(p); break;
case "t" : pg = repository.findByTitleContaining(kw, p); break;
case "c1" : pg = repository.findByContent1Containing(kw,p); break;
case "w" : pg = repository.findByWriterContaining(kw,p); break;
case "tc1" : pg = repository.findByTitleContainingOrContent1Containing(kw,kw,p); break;
case "c1w" : pg = repository.findByContent1ContainingOrWriterContaining(kw,kw,p); break;
case "tcw" : pg = repository.findByTitleContainingOrContent1ContainingOrWriterContaining(kw,kw,kw,p); break;
}
}
return pg;
}
}
* default.html 기본 레이어
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" /><!--/* 요즘 대세 UTF-8 */-->
<link rel="stylesheet" type="text/css" href="/css/default.css"/><!-/* /static/ 경로에 위치 *-->
<script src="/js/jquery-1.10.2.min.js"></script><!-/* /static/ 경로에 위치 *-->
<script src="/js/default.js"></script><!-/* /static/ 경로에 위치 *-->
</head>
<body>
<div class="header">페이지 헤더</div><!-/* header 를 파편 처리하기도 한다 *-->
<div layout:fragment="content" class="content"></div><!-/* default.html 레이어의 파편 *-->
<div class="footer">페이지 푸터</div><!-/* footer 를 파편 처리하기도 한다 *-->
</body>
</html>
* default.css
.header, .footer {width:100%; padding:10px; background:#ddd}
.header {margin-bottom:20px}
.footer {margin-top:20px}
div.content {width:700px; margin:0 auto}
.btn {padding:10px}
.btn a {cursor:pointer; border:1px #ccc solid;border-radius:5px; padding:5px 10px; text-decoration:none}
table.df {width:600px; border-spacing:0}
table.df th, table.df td {text-align:center; padding:5px; border:1px #ccc solid}
table.df th.l, table.df td.l {text-align:left}
table.df th.r, table.df td.r {text-align:right}
div.pgn {width:600px; margin:0 auto; text-align:center}
* default.js
function act(method,action){
frmObj // 화면용 .html 에서 정의. 타임리프의 주석처리 방식이 적용되지 않음
.attr('method',method)
.attr('action',action)
.submit();
}
function goRead(bn){
$('#boardNo').val(bn);
act('get','read'); // read 는 get
}
function goList(pn){
$('#page').val(pn);
act('get','list') // list 는 get
}
function validForm1(){ // 목록 검색시 필수요소 점검
let $kw = $('#keyword');
if($('#searchType option:selected').val()==''){ alert('검색 유형을 선택해 주세요. '); return false; }// 검색 유형을 선택하지 않았으면, 검색어 리셋
else if($kw.val()==''){ alert('검색 단어를 입력해 주세요. '); return false; }
return true;
}
function validForm2(){ // 등록/수정시 필수요소 점검
if($('#title').val()==''){ alert('제목을 입력해 주세요. '); return false; }
if($('#content1').val()==''){ alert('내용1 을 입력해 주세요. '); return false; }
if($('#content2').val()==''){ alert('내용2 를 입력해 주세요. '); return false; }
if($('#writer').val()==''){ alert('작가를 입력해 주세요. '); return false; }
return true;
}
* list.html
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{/layouts/default.html}">
<head>
<title>[JPA] Board, List</title>
</head>
<body>
<div layout:fragment="content"><!--/* default.html 의 <div layout:fragment="content"></div> 대체 */-->
<!--/* form 태그의 method 와 action 속성은 자바스크립트에서 처리하므로 생략 */-->
<form id="pr" th:object="${pr}"><!--/* @ModelAttribute("pr") PageRequestDTO 에 담는다 */-->
<input type="hidden" th:field="${board2.boardNo}">
<input type="hidden" th:field="*{page}">
<input type="hidden" th:field="*{sizePerPage}">
<h2>List</h2>
<div class="btn">
<select th:field="*{searchType}"><!--/* *{searchType} 과 ${e.code} 가 동일할 때 해당 option selected */-->
<option th:each="e : ${searchTypes}" th:value="${e.code}" th:text="${e.value}"></option>
</select>
<input type="text" th:field="*{keyword}">
<a id="btnList">검색</a>
<a th:href="@{register}">등록</a>
</div>
<table class="df" th:with="list = ${pgn.page.content}">
<col width="80">
<col width="320">
<col width="100">
<col width="200">
<tr>
<th>No</th>
<th>Title</th>
<th>Writer</th>
<th>RegDate</th>
</tr>
<tr th:if="${#lists.isEmpty(list)}">
<td colspan="4">List is empty</td>
</tr>
<tr th:each="board : ${list}">
<td th:text="${board.boardNo}"></td>
<td><a th:href="|javascript:goRead(${board.boardNo})|" th:text="${board.title}"></a></td>
<td th:text="${board.writer}"></td>
<td th:text="${#temporals.format(board.regDate,'yyyy-MM-dd HH:mm:ss')}"></td>
</tr>
</table>
<div class="pgn">
<th:block th:if="${pgn.prevPageable}" th:with="prevNum = ${pgn.prevPageable.pageNumber} +1">
<a th:href="|javascript:goList(${prevNum})|">Prev [[${prevNum}]]</a>
</th:block><!--/* 이전 */-->
<th:block th:each="p : ${pgn.pageableList}" th:with="pNum = ${p.pageNumber} +1">
<a th:href="|javascript:goList(${pNum})|">[[${pNum}]]</a>
</th:block><!--/* 페이지 번호 목록 */-->
<th:block th:if="${pgn.nextPageable}" th:with="nextNum = ${pgn.nextPageable.pageNumber} +1">
<a th:href="|javascript:goList(${nextNum})|">Next [[${nextNum}]]</a>
</th:block><!--/*이후 */-->
</div>
</form>
<script th:inline="javascript">
var frmObj;
$(document).ready(function(){
frmObj = $('#pr');// 여기서 사용된 form 태그. 목록 검색용과 상세 조회용 별도로 구현할 수 있지만 여기서는 하나로..
$('#btnList').on('click',function(){
if(!validForm1()) return;// 검색 버튼을 누를 때만 검색 유형과 검색 단어를 점검
goList(1); // 검색 버튼 누르면 항상 첫 페이지 선택
});
});
$(window).load(function(){// 목록 화면이 표시되고 alert 되기를 바랐건만, 흰 바탕에 alert 된 다음 view 가 표시됨
let msg = [[${msg}]]; if(msg) alert(msg);
});
</script>
</div>
</body>
</html>
* read.html
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{/layouts/default.html}">
<head>
<title>[JPA] Board, Read</title>
</head>
<body>
<div layout:fragment="content"><!--/* default.html 의 <div layout:fragment="content"></div> 대체 */-->
<!--/* form 태그의 method 와 action 속성은 자바스크립트에서 처리하므로 생략 */-->
<form id="board2" th:object="${board2}"><!--/* @ModelAttribute("pr") PageRequestDTO 에는 form control 중 name 기준으로 담는다 */-->
<input type="hidden" th:field="*{boardNo}"><!--/* Board2Controller.read() 의 파라미터의 boardNo 가 아니라, model.addAttribute() 에 담긴 service.read(boardNo) 결과값의 boardNo */-->
<input type="hidden" th:field="${pr.page}">
<input type="hidden" th:field="${pr.sizePerPage}">
<input type="hidden" th:field="${pr.searchType}">
<input type="hidden" th:field="${pr.keyword}">
<h2>Read</h2>
<table class="df">
<col width="80">
<col width="320">
<tr>
<th class="r">BoardNo</th>
<td class="l" th:text="*{boardNo}"></td>
</tr>
<tr>
<th class="r">Title</th>
<td class="l" th:text="*{title}"></td>
</tr>
<tr>
<th class="r">content1</th>
<td class="l" th:text="*{content1}"></td>
</tr>
<tr>
<th class="r">content2</th>
<td class="l" th:text="*{content2}"></td>
</tr>
<tr>
<th class="r">writer</th>
<td class="l" th:text="*{writer}"></td>
</tr>
</table>
<div class="btn">
<a id="btnModify">수정</a>
<a id="btnRemove">삭제</a>
<a id="btnList">목록</a>
</div>
</form>
<script>
var frmObj;
$(document).ready(function(){
frmObj = $('#board2');
$('#btnModify').on('click',function(){
act('get','modify'); // 수정 양식으로 이동
});
$('#btnRemove').on('click',function(){
act('post','remove'); // remove 는 post
});
$('#btnList').on('click',function(){
act('get','list') // list 는 get. 페이지나 목록 검색 조건 전달
});
});
</script>
</div>
</body>
</html>
* modify.html
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{/layouts/default.html}">
<head>
<title>[JPA] Board, Modify</title>
</head>
<body>
<div layout:fragment="content"><!--/* default.html 의 <div layout:fragment="content"></div> 대체 */-->
<!--/* form 태그의 method 와 action 속성은 자바스크립트에서 처리하므로 생략 */-->
<form id="pr" th:object="${pr}"><!--/* 목록으로 돌아갈 때 수정화면에 입력된 내용이 섞이지 않도록 form 을 2중화 */-->
<input type="hidden" th:field="${board2.boardNo}"><!-- Board2Controller.read() 의 파라미터의 boardNo 가 아니라, model.addAttribute() 에 담긴 service.read(boardNo) 결과값의 boardNo -->
<input type="hidden" th:field="*{page}">
<input type="hidden" th:field="*{sizePerPage}">
<input type="hidden" th:field="*{searchType}">
<input type="hidden" th:field="*{keyword}">
</form>
<form id="board2" th:object="${board2}"><!--/* DB 처리하고 RedirectAttribute 에 재설정하면서 사용자 입력 정보 리셋 */-->
<input type="hidden" th:field="*{boardNo}">
<input type="hidden" th:field="${pr.page}">
<input type="hidden" th:field="${pr.sizePerPage}">
<input type="hidden" th:field="${pr.searchType}">
<input type="hidden" th:field="${pr.keyword}">
<h2>Read</h2>
<table class="df">
<col width="80">
<col width="320">
<tr>
<th class="r">BoardNo</th>
<td class="l" th:text="*{boardNo}"></td>
</tr>
<tr>
<th class="r">Title</th>
<td class="l">
<input type="text" th:field="*{title}">
<p th:if="${#fields.hasErrors('title')}" th:errors="*{title}"></p><!--/* BindingResult 로 세팅된 오류 메시징 */-->
</td>
</tr>
<tr>
<th class="r">content1</th>
<td class="l">
<textarea th:field="*{content1}"></textarea>
<p th:if="${#fields.hasErrors('content1')}" th:errors="*{content1}"></p><!--/* BindingResult 로 세팅된 오류 메시징 */-->
</td>
</tr>
<tr>
<th class="r">content2</th>
<td class="l">
<textarea th:field="*{content2}"></textarea>
<p th:if="${#fields.hasErrors('content2')}" th:errors="*{content2}"></p><!--/* BindingResult 로 세팅된 오류 메시징 */-->
</td>
</tr>
<tr>
<th class="r">writer</th>
<td class="l">
<input type="text" th:field="*{writer}">
<p th:if="${#fields.hasErrors('writer')}" th:errors="*{writer}"></p><!--/* BindingResult 로 세팅된 오류 메시징 */-->
</td>
</tr>
</table>
<div class="btn">
<a id="btnModify">저장</a>
<a id="btnList">목록</a>
</div>
</form>
<script>
var frmObj;
$(document).ready(function(){
$('#btnModify').on('click',function(){
frmObj = $('#board2');
if(!validForm2()) return;
act('post','modify')
});
$('#btnList').on('click',function(){
frmObj = $('#pr');
act('get','list');// act('get','list') 로 하면 input textarea 값들이 웹브라우저 주소창에 뜨지만, 여기에서는 페이지나 목록 검색 조건 고려. top.location.href= '/board2/list' 또는 'list'
});
});
</script>
</div>
</body>
</html>
* register.html
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{/layouts/default.html}">
<head>
<title>[JPA] Board, Register</title>
</head>
<body>
<div layout:fragment="content"><!--/* default.html 의 <div layout:fragment="content"></div> 대체 */-->
<!--/* form 태그의 method 와 action 속성은 자바스크립트에서 처리하므로 생략 */-->
<form id="board2" th:object="${board2}"><!--/* @ModelAttribute("pr") PageRequestDTO 에 담는다 */-->
<h2>Register</h2>
<table>
<tr>
<th>Title</th>
<td>
<input type="text" th:field="*{title}">
<p th:if="${#fields.hasErrors('title')}" th:errors="*{title}"></p><!--/* BindingResult 로 세팅된 오류 메시징 */-->
</td>
</tr>
<tr>
<th>content1</th>
<td>
<textarea th:field="*{content1}"></textarea>
<p th:if="${#fields.hasErrors('content1')}" th:errors="*{content1}"></p><!--/* BindingResult 로 세팅된 오류 메시징 */-->
</td>
</tr>
<tr>
<th>content2</th>
<td>
<textarea th:field="*{content2}"></textarea>
<p th:if="${#fields.hasErrors('content2')}" th:errors="*{content2}"></p><!--/* BindingResult 로 세팅된 오류 메시징 */-->
</td>
</tr>
<tr>
<th>writer</th>
<td>
<input type="text" th:field="*{writer}">
<p th:if="${#fields.hasErrors('writer')}" th:errors="*{writer}"></p><!--/* BindingResult 로 세팅된 오류 메시징 */-->
</td>
</tr>
</table>
<div class="btn">
<a id="btnRegister">저장</a>
<a id="btnList">목록</a>
</div>
</form>
<script>
var frmObj;
$(document).ready(function(){
frmObj = $('#board2');
$('#btnRegister').on('click',function(){
if(!validForm2()) return;
act('post','register');// '/board2/register' 라고 해도 된다, 현재의 경로가 '/board2/' 이기 때문
});
$('#btnList').on('click',function(){
top.location.href = 'list';// act('get','list') 로 하면 input textarea 값들이 웹브라우저 주소창에 뜬다
// top.location.href= '/board2/list' 라고 해도 된다.
// self.location 대신에 top 을 쓰는 이유는, 간혹 iframe 과 연계된 무언가의 영향을 받지 않기 위해..
});
});
</script>
</div>
</body>
</html>
'Server Oriented > Spring' 카테고리의 다른 글
스프링 파일 업로드 MultipartFile #1/2 (0) | 2023.02.03 |
---|---|
JUnit5 JPA DB 테이블 1개 목록/상세/등록/수정/삭제 (0) | 2022.12.05 |
스프링, 세션과 쿠키 (0) | 2022.11.02 |
스프링 뷰, 타임리프 form 과 결과 (0) | 2022.10.14 |
스프링 build.gradle 수정후 반드시 실행 (0) | 2022.10.13 |