jnk1m
Foliage IT
jnk1m
전체 방문자
오늘
어제
  • 분류 전체보기 (209)
    • Today I Learned (34)
    • Java (47)
    • Database (15)
    • [NHN Academy] (27)
    • Spring (47)
    • HTML + CSS + JavaScript (11)
    • JSP (3)
    • Node.js (10)
    • React Native (2)
    • 기타 (8)
    • 스크랩 (5)

인기 글

최근 글

티스토리

hELLO · Designed By 정상우.
글쓰기 / 관리자
jnk1m

Foliage IT

Spring

[SpringBoot] 2022/05/24 Memo

2022. 6. 4. 00:44

** 하나의 테이블을 가지고 CRUD 작업을 수행하는 Application
1.프로젝트 생성
=>build type: gradle

=>의존성
spring-devtools
spring-jpa
mysql 또는 oracle
thymeleaf
spring-web
lombok

2.application.properties 파일에 기본 설정을 추가
#server 의 port 설정
server.port = 80

#연결할 데이터베이스 설정 - MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/adam?useUnicode=yes&characterEncoding=UTF-8&serverTimezon=UTC
spring.datasource.username=adam
spring.datasource.password=wnddkd

#연결할 데이터베이스 설정 - Oracle
#spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
#spring.datasource.url=jdbc:oracle:thin:@192.168.10.4:1521:xe
#spring.datasource.username=system
#spring.datasource.password=oracle

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=update
logging.level.org.hibernate.type.descriptor.sql=trace

spring.devtools.livereload.enabled=true
spring.thymeleaf.cache=false

3.부트스트랩의 사이드 바 적용
1)부트스트랩 사이드 바 다운로드
https://startbootstrap.com/template/simple-sidebar

2)다운로드 받은 파일의 압축을 해제

3)압축을 해제한 assets, css, js 디렉토리를 프로젝트의 resources/static 디렉토리에 복사

4)templates 디렉토리에 공통된 레이아웃 파일을 위한 디렉토리를 생성 - layout

5)templates/layout 디렉토리에 공통된 디자인을 위한 html 파일을 생성 - basic.html

6)basic.html 파일에 사이드 바의 index.html 파일의 내용을 복사해서 붙여넣고 수정
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<th:block th:fragment="setContent(content)">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="description" content="" />
    <meta name="author" content="" />
    <title>Simple Sidebar - Start Bootstrap Template</title>
    <!-- Favicon-->
    <link rel="icon" type="image/x-icon" th:href="@{assets/favicon.ico}" />
    <!-- Core theme CSS (includes Bootstrap)-->
    <link th:href="@{/css/styles.css}" rel="stylesheet" />
    <!-- Bootstrap core JS-->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js"></script>
    <!-- Core theme JS-->
    <script th:src="@{/js/scripts.js}"></script>

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>

</head>

<body>
<div class="d-flex" id="wrapper">
    <div class="border-end bg-white" id="sidebar-wrapper">
        <div class="sidebar-heading border-bottom bg-light">Start Bootstrap</div>
        <div class="list-group list-group-flush">
            <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Dashboard</a>
            <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Shortcuts</a>
            <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Overview</a>
            <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Events</a>
            <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Profile</a>
            <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Status</a>
        </div>
    </div>
        <!-- Page content wrapper-->
    <div id="page-content-wrapper">
        <!-- Top navigation-->
        <nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
            <div class="container-fluid">
                <button class="btn btn-primary" id="sidebarToggle">Toggle Menu</button>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>
                    <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <ul class="navbar-nav ms-auto mt-2 mt-lg-0">
                        <li class="nav-item active"><a class="nav-link" href="#!">Home</a></li>
                        <li class="nav-item"><a class="nav-link" href="#!">Link</a></li>
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" id="navbarDropdown" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a>
                            <div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
                                <a class="dropdown-item" href="#!">Action</a>
                                <a class="dropdown-item" href="#!">Another action</a>
                                <div class="dropdown-divider"></div>
                                <a class="dropdown-item" href="#!">Something else here</a>

                                </div>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
        <!-- Page content-->
        <div class="container-fluid">
            <th:block th:replace = "${content}"></th:block>
        </div>
    </div>
</div>
</th:block>
</body>
</html>
    
4.공통 디자인 확인
1)HTML 출력을 처리할 Controller를 생성하고 시작 요청을 처리하는 메서드를 추가
@Controller
public class MemoPageController {

@GetMapping("/")
public String main() {
return "main";
}

}
    
2)templates 디렉토리에 main.html 파일을 추가하고 작성
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{layout/basic::setContent(~{this::content})}">
<th:block th:fragment="content">
<h1>Memo Application</h1>
</th:block>
</th:block>

3)application 실행


4)브라우저에서 localhost:포트번호 를 입력하고 확인


5.프로젝트 구조
1)기본 구조
Thymeleaf Page <-> Controller(MemoPageController) <-> MemoService(Template Method Pattern 적용) <-> MemoRepository(영속성 작업)

2)화면 구성
작업 URL Method 수행할 기능 Redirect URL
목록보기 /memo/list GET 목록보기 - 페이징, 검색

삽입 /memo/register GET 입력 화면
/memo/register POST 실제 삽입 /memo/list

상세보기 /memo/read GET 하나의 데이터 조회

수정 /memo/modify GET 수정/삭제 화면
/memo/modify POST 실제 수정 /memo/read

삭제 /memo/remove POST 실제 삭제 /memo/list

3)DTO 와 Entity
=>Entity: 데이터베이스 와 연동하기 위한 클래스, 데이터베이스에 꼭 필요한 것 들만으로 구성되어야 합니다.
이 클래스는 Repository에서만 사용해야 합니다.

=>Entity는 사용자의 요청 과 반드시 일치하지는 않는 문제가 발생하는데 이런 경우에는 DTO 클래스를 별도로 만들어서 해결을 합니다.
Repository에서 넘겨준 데이터를 가지고 가공을 해서 Controller에게 전달하거나 전달받고 Controller는 DTO를 View에게 전송해서 출력하거나 사용을 합니다.
이렇게 Service에서 Controller로 데이터가 전달된다고 해서 이러한 클래스를 Data Transfer Object 라고 합니다.
안드로이드 같은 곳에서 이러한 DTO 패턴으로 만들어진 클래스만 화면 사이에 전달이 됩니다.
이 때 자바는 Serializable 인터페이스를 구현한 것을 DTO로 인식하빈다.

프로그램에서 만든 데이터는 근본적으로 Main Memory에 존재하는 데이터입니다.
이 Main Memory의 데이터를 반영구적으로 저장하기 위해서 File에 기록을 할려면 Main Memory에서 File(Auxiliary Memory)로 옮겨야 합니다. 
이 때 이동이 가능한 인스턴스는 Serializable 인터페이스가 구현된 인스턴스만 File에 기록이 가능합니다.
이렇게 저장한 데이터는 반드시 Serializable 인터페이스가 구현된 인스턴스로 읽어야 합니다.


=>Entity의 생성 시간 과 수정 시간
생성 시간 과 수정 시간을 사용하고자 하는 경우 매번 현재 시간을 가져오는 것은 아주 많이 사용하는 공통된 로직입니다.
정적인 로직을 매번 수행하는 것은 자원의 낭비입니다.
JPA에서는 이를 Annotation으로 처리할 수 있도록 해주는데 Application 의 Entry Point 가 되는 클래스에 @EnableJpaAuditing 이라는 어노테이션을 추가하고 Entity에 @CreatedDate 와 @LastModifiedDate 를 추가한 속성을 만들면 됩니다.

=>Entity를 테이블로 생성하지 않도록 하고자 하는 경우에는 @MappedSuperclass 를 추가

6.자동으로 처리되는 날짜/시간을 위한 작업
1)Entry Point(애플리케이션이 시작되는 지점 - main 메서드를 소유한 클래스) 가 되는 클래스에 annotation 추가

//JPA 관련된 작업을 감시
@EnableJpaAuditing

@SpringBootApplication
public class MemoApplication {

public static void main(String[] args) {
SpringApplication.run(MemoApplication.class, args);
}

}

2)Entity 들이 공통으로 가져야 하는 속성을 가진 Entity 클래스 생성 - model.BaseEntity
import java.time.LocalDateTime;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;

import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import lombok.Getter;

//공통된 속성을 가진 Entity

//테이블로 생성할 필요가 없음
@MappedSuperclass
//데이터베이스 작업을 감시
@EntityListeners(value = {AuditingEntityListener.class})
@Getter
//abstract: 인스턴스를 생성할 수 없도록 해주는 클래스로 상속을 통해서만 사용이 가능
abstract public class BaseEntity {
//생성한 시간을 저장하는데 컬럼 이름은 regdate 이고 수정할 수 없도록 생성
@CreatedDate
@Column(name="regdate", updatable=false)
private LocalDateTime regDate;

//수정한 시간을 저장하는데 컬럼 이름은 moddate 이고 수정할 수 없도록 생성
@LastModifiedDate
@Column(name="moddate", updatable=false)
private LocalDateTime modDate;
}

3)애플리케이션에서 사용할 Memo Entity 생성 - model.Memo
=>BaseEntity를 상속받도록 생성

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity
@Table(name="memo")

@Getter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Memo extends BaseEntity{
//시스템이 생성해주는 랜덤한 문자열
//@Id
//@GeneratedValue(generator="system-uuid")
//@GenericGenerator(name="system-uuid", strategy="uuid")
//private String id;

//gno 값을 데이터베이스의 auto_increment 나 sequence를 이용해서 생성
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
//메모 번호
private Long gno;

@Column(length=100, nullable=false)
//메모 제목
private String title;

@Column(length=1500, nullable=false)
//메모 내용
private String content;

@Column(length=100, nullable=false)
//메모 작성자
private String writer;

//title을 변경해주는 메서드
public void changeTitle(String title) {
this.title = title;
}

//content를 변경해주는 메서드
public void changeContent(String content) {
this.content = content;
}

}

4)애플리케이션을 실행해서 테이블이 만들어지는지 확인

7.Persistency 작업
1)Memo Entity를 사용하기 위한 Repository 클래스를 생성 - persistency.MemoRepository

2)querydsl 을 사용하기 위한 설정 - build.gradle(2.6x 버전)에서 수행
plugins {
id 'org.springframework.boot' version '2.5.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'

id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'

}

group = 'com.adamsoft'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.oracle.database.jdbc:ojdbc8'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-java8time'

testImplementation 'org.springframework.boot:spring-boot-starter-test'

// QueryDSL
    implementation 'com.querydsl:querydsl-jpa'
    implementation 'com.querydsl:querydsl-apt'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.1'
}

// querydsl 추가 시작
def querydslDir = "$buildDir/generated/querydsl"

querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}
sourceSets {
    main.java.srcDir querydslDir
}
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    querydsl.extendsFrom compileClasspath
}

compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}


tasks.named('test') {
useJUnitPlatform()
}

3)MemoRepository에 QuerydslPredicateExecutor 인터페이스를 extends
public interface MemoRepository extends JpaRepository<Memo, Long>,
QuerydslPredicateExecutor<Memo> {

}

4)src/test/java 의 기본 패키지에 테스트용 클래스를 생성 - MemoRepositorytest
import java.util.List;

import javax.transaction.Transactional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.query.Param;

import com.adamsoft.memo.model.Memo;

public interface MemoRepository extends JpaRepository<Memo, Long>,
QuerydslPredicateExecutor<Memo> {

//title이 일치하는 데이터를 조회 - 이름을 이용해서 생성한 쿼리
List<Memo> findByTitle(String title);

//직접 쿼리를 작성하는 방법
@Transactional
@Modifying
@Query("update Memo m set m.title = :title, m.content = :content where m.gno = :gno")
int updateMemo(@Param("title") String title, 
@Param("content") String content, @Param("gno") Long gno);

}



5)MemoRepositoryTest 클래스에 데이터 삽입을 테스트하기 위한 메서드를 생성하고 실행한 후 데이터베이스 확인

@SpringBootTest
public class MemoRepositoryTest {
@Autowired
private MemoRepository memoRepository;

//데이터 삽입 테스트
//@Test
public void insertMemo() {
//300개의 정수 모임을 생성하고 순회
IntStream.rangeClosed(1, 300).forEach(i -> {
//데이터 생성
Memo memo = Memo.builder()
.title("title..." + i)
.content("content__" + i)
.writer("user" + (i % 10))
.build();
//데이터 삽입
memoRepository.save(memo);
});

//확인은 데이터베이스에서 아래 구문 수행
/*
select *
from memo;
 */
}

//데이터 수정 테스트
//@Test
public void updateMemo() {
//수정할 데이터 가져오기
Optional<Memo> result = memoRepository.findById(300L);

if(result.isPresent()) {
Memo memo = result.get();
memo.changeTitle("제목 변경");
memo.changeContent("내용 변경");

memoRepository.save(memo);
}else {
System.out.println("데이터가 존재하지 않습니다.");
}
//확인은 데이터베이스에서 아래 구문 수행
/*
select *
from memo 
where gno = 300;
 */
}

//데이터 삭제 테스트
//@Test
public void deleteMemo() {
//삭제 데이터 가져오기
Optional<Memo> result = memoRepository.findById(300L);

if(result.isPresent()) {
Memo memo = result.get();
memoRepository.delete(memo);
}else {
System.out.println("데이터가 존재하지 않습니다.");
}
//확인은 데이터베이스에서 아래 구문 수행
/*
select *
from memo 
where gno = 300;
 */
}

//title이 title...1인 데이터 조회
//@Test
public void findByTitle() {
List<Memo> list = memoRepository.findByTitle("title...1");
System.out.println(list);
}

//@Test
public void modifyMemo() {
int result = memoRepository.updateMemo("제목", "내용", 299L);
System.out.println(result);
}


//title에 1이 포함된 데이터를 조회
//@Test
public void testQuery1() {
//gno의 내림차순으로 정렬해서 0번 페이지의 데이터 중 10개를 가져오는 Pageable
Pageable pageable = PageRequest.of(0,  10, Sort.by("gno").descending());

//querydsl 이 만들어준 클래스를 이용해서 쿼리 생성
//querydsl은 Entity에 명령을 직접 수행하지 않고 Q로 시작하는 별도의 메모리 공간에 명령을 수행
QMemo qMemo = QMemo.memo;

//검색할 조건을 가지는 JPQL(SQL)생성기
BooleanBuilder builder = new BooleanBuilder();

//title에 1이 포함된 쿼리를 생성
String title = "1";
//title에 1이 포함된 데이터를 조회하는 쿼리
BooleanExpression expression = qMemo.title.contains(title);
//실제 쿼리로 생성  where title like "%1%"
builder.and(expression);

//쿼리 수행
Page<Memo> result = memoRepository.findAll(builder, pageable);

for(Memo memo : result) {
System.out.println(memo);
}

}

//title 또는 content에 1이 포함되어 있고 gno 가 0보다 큰 데이터 조회
@Test
public void testQuery2() {
//gno의 내림차순으로 정렬해서 0번 페이지의 데이터 중 10개를 가져오는 Pageable
Pageable pageable = PageRequest.of(0,  10, Sort.by("gno").descending());

//querydsl 이 만들어준 클래스를 이용해서 쿼리 생성
//querydsl은 Entity에 명령을 직접 수행하지 않고 Q로 시작하는 별도의 메모리 공간에 명령을 수행
QMemo qMemo = QMemo.memo;

//검색할 조건을 가지는 JPQL(SQL)생성기
BooleanBuilder builder = new BooleanBuilder();

//조건을 생성
String keyword = "1";

//title에 포함된 것
BooleanExpression expTitle = qMemo.title.contains(keyword);
//content에 포함된 것
BooleanExpression expContent = qMemo.content.contains(keyword);

//2개의 조건을 or로 묶어주기
BooleanExpression exAll = expTitle.or(expContent);

builder.and(exAll);
//gno 가 0보다 큰 조건을 and로 묶어주기
//제가 임의로 0보다 큰 이라고 설정한 것입니다.
//다른 조건을 만들어도 됩니다.
//작은 조건은 lt 입니다.
builder.and(qMemo.gno.gt(0L));

//쿼리 수행
Page<Memo> result = memoRepository.findAll(builder, pageable);

for(Memo memo : result) {
System.out.println(memo);
}
}
}

8.Service 작업
=>Entity 는 데이터베이스 관련 작업을 위한 클래스입니다.
Entity는 우리가 관리하는 클래스가 아니고 Spring JPA 의 Entity Manager 가 관리하는 객체입니다.
수명주기(인스턴스가 만들어지는 시점 과 소멸되는 시점)를 우리가 알 수 없습니다.

=>화면에 출력하고 클라이언트에서 넘겨준 파라미터를 사용하는 경우 이 데이터는 일시적으로 필요한 데이터입니다.
이러한 데이터를 Entity 클래스를 이용해서 관리하면 자원의 낭비가 발생할 수 있습니다.
Service 와 Controller 와 View에서 사용하는 별도의 데이터 클래스가 필요

=>별도의 데이터 클래스(DTO)를 만들면 유사한 코드를 중복으로 개발하게 되고 Entity 와 DTO 사이의 변환을 위한 메서드가 필요하게 됩니다.

1)dto 패키지에 MemoDTO 클래스를 생성하고 작성
//Controller 와 Service 사이의 데이터 전달을 위한 클래스
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class MemoDTO {
private Long gno;
private String title;
private String content;
private String writer;
private LocalDateTime regDate;
private LocalDateTime modDate;
}

2)사용자의 요청을 처리하는 메서드의 원형을 갖는 Service 인터페이스 생성 - service.MemoService
public interface MemoService {
//DTO 객체를 Entity 로 변환해주는 메서드
//이 메서드는 클라이언트 정보를 가지고 데이터베이스에 변환을 수행하기 위해서 사용
default Memo dtoToEntity(MemoDTO dto) {
Memo memo = Memo.builder()
.gno(dto.getGno())
.title(dto.getTitle())
.content(dto.getContent())
.writer(dto.getWriter())
.build();
return memo;
}

//Entity 객체를 DTO 객체로 변환해주는 메서드
//조회를 할 때 사용
default MemoDTO entityToDto(Memo memo) {
MemoDTO dto = MemoDTO.builder()
.gno(memo.getGno())
.title(memo.getTitle())
.content(memo.getContent())
.writer(memo.getWriter())
.regDate(memo.getRegDate())
.modDate(memo.getModDate())
.build();
return dto;
}
}

3)Service 인터페이스의 메서드를 구현한 ServiceImpl 클래스를 생성 - service.MemoServiceImpl
//로그 기록을 위한 어노테이션 - log.레벨(메시지)를 이용해서 로그를 출력 하는 것이 가능
@Log4j2
@Service
//생성자를 이용해서 주입받기 위한 어노테이션
//Autowired를 이용해서 주입받으면 주입받는 시점을 예측하기 어렵기 때문에 비추천
@RequiredArgsConstructor
public class MemoServiceImpl implements MemoService {
//주입받기 위한 Repository - 생성자에서 주입받기 위해서는 final로 만들어져야 합니다.
private final MemoRepository memoRepository;

}


9.데이터 삽입
1)MemoService 인터페이스에 데이터 삽입을 위한 메서드를 선언
//데이터 삽입을 위한 메서드
//삽입된 메모의 gno 값을 리턴
public Long insertMemo(MemoDTO dto);

2)MemoServiceImpl 클래스에 데이터 삽입을 위한 메서드를 구현
@Override
public Long insertMemo(MemoDTO dto) {
log.info("=========데이터 삽입=================");
log.info(dto);

//DTO를 Entity로 변환
Memo memo = dtoToEntity(dto);

//데이터 저장하고 저장한 내용을 memo에 다시 기록
memoRepository.save(memo);

return memo.getGno();
} 

3)데이터 삽입 테스트
=>Service를 테스트하기 위한 테스트 클래스를 만들고 테스트
@SpringBootTest
public class MemoServiceTest {
@Autowired
private MemoService memoService;

@Test
public void testInsert() {
MemoDTO dto = MemoDTO.builder().title("데이터 삽입 테스트")
.content("삽입 성공?").writer("아담").build();
System.out.println(memoService.insertMemo(dto));
//데이터베이스에 가서 확인
// select * from memo order by gno desc
}

}

10.목록 보기 구현
=>고려 사항
- 페이징 처리 여부
- 페이지 방식 처리
페이지 번호 이용(개수가 고정 이거나 select 를 이용해서 설정) 
스크롤 이용(화면의 크기를 알아야 하고 화면의 크기를 가지고 처음에 가져올 데이터 개수를 설정할 수 있어야 합니다.)

=>페이징 처리를 하게되면 요청을 할 때 필요한 데이터가 존재
페이지 번호 와 데이터 개수가 필요

=>검색을 구현하고자 하는 경우에 필요한 데이터
검색 항목 과 키워드 가 필요

=>목록 보기를 하기 위해서는 별도의 DTO 클래스가 필요

1)요청 관련 DTO - 페이징 적용
@Builder
@Data
@AllArgsConstructor
public class PageRequestDTO {
//페이지 번호
private int page;
//한 페이지에 출력될 데이터 개수
private int size;

//생성자 - 인스턴스 변수를 초기화
public PageRequestDTO() {
this.page = 1;
this.size = 10;
}

//Pageable(Spring Boot JPA에서 Page 단위 검색을 위한 클래스) 객체를 생성해주는 메서드
public Pageable getPageable(Sort sort) {
return PageRequest.of(page-1, size, sort);
}

}


2)응답 관련 DTO - Page 객체를 받아서 List로 변환해서 저장
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.data.domain.Page;

import lombok.Data;

//재활용을 위해서 템플릿을 적용
//DTO는 변환할 DTO 클래스가 대입되고
//EN은 Entity 와 DTO 사이의 변환을 수행해 줄 메서드 
//Java에서는 Generic 이라고 하고 객체 지향에서 템플릿 프로그래밍 이라고 합니다.
//알고리즘을 구현할 때 프레임워크를 만들 때 중요
@Data
public class PageResponseDTO <DTO, EN> {
//DTO의 List
private List<DTO> dtoList;

//Page 객체 와 함수를 적용해서 List로 변환해주는 메서드
public PageResponseDTO(Page<EN> result, Function<EN, DTO> fn) {
dtoList = result.stream().map(fn).collect(Collectors.toList());
}
}


3)Service 인터페이스에 목록보기를 위한 메서드를 선언
//목록 보기를 위한 메서드
public PageResponseDTO<MemoDTO, Memo> getList (PageRequestDTO requestDTO);

4)ServiceImpl 클래스에 목록보기 메서드를 구현
@Override
public PageResponseDTO<MemoDTO, Memo> getList(PageRequestDTO requestDTO) {
//정렬 조건을 적용해서 페이징 객체를 생성
Pageable pageable = requestDTO.getPageable(Sort.by("gno").descending());
Page<Memo> result = memoRepository.findAll(pageable);

//Entity를 DTO로 변환해주는 함수 설정
Function<Memo, MemoDTO> fn = (entity -> entityToDto(entity));
//결과 리턴
return new PageResponseDTO <>(result, fn);
}

5)Test 클래스에서 메서드 테스트
//목록 보기 테스트
@Test
public void testList() {
PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(1).size(10).build();
PageResponseDTO <MemoDTO, Memo> resultDTO = memoService.getList(pageRequestDTO);
for(MemoDTO memoDto : resultDTO.getDtoList()) {
System.out.println(memoDto);
}
}

6)페이징 처리 시 필요한 요소
=>페이지 번호 목록을 화면에 출력하기 위해서는 시작하는 페이지 번호, 끝나는 페이지 번호, 이전 페이지 여부, 다음 페이지 여부, 현재 페이지 번호가 필요

끝나는 페이지 번호 = (int)(Math.ceil(현재 페이지 번호 / (double)페이지 번호 출력 개수)) * 페이지 번호 출력 개수

현재 페이지 번호가 5 이고 페이지 번호 출력 개수가 10이라면

(int)(Math.ceil(5 / (double) 10)) * 10 = 10

시작하는 페이지 번호 = 끝나는페이지번호 - (페이지 번호 출력 개수 - 1);

=>PageResponseDTO 클래스에 페이지 번호 출력을 위한 코드를 추가
package com.adamsoft.memo.dto;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import lombok.Data;

//재활용을 위해서 템플릿을 적용
//DTO는 변환할 DTO 클래스가 대입되고
//EN은 Entity 와 DTO 사이의 변환을 수행해 줄 메서드 
//Java에서는 Generic 이라고 하고 객체 지향에서 템플릿 프로그래밍 이라고 합니다.
//알고리즘을 구현할 때 프레임워크를 만들 때 중요
@Data
public class PageResponseDTO <DTO, EN> {
//DTO의 List
private List<DTO> dtoList;

//전체 페이지 개수
private int totalPage;

//현재 페이지 번호
private int page;

//출력할 페이지 번호 개수
private int size;

//이전 페이지 목록 여부
private boolean prev;
//다음 페이지 목록 여부
private boolean next;

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


//출력할 페이지 번호 목록
private List<Integer> pageList;

//출력할 페이지 번호를 계산하는 메서드
private void makePageList(Pageable pageable) {
this.page = pageable.getPageNumber() + 1;
this.size = pageable.getPageSize();

int tempEnd = (int)(Math.ceil(page / 10.0)) * 10;
start = tempEnd - 9;
prev = start > 1;
end = totalPage > tempEnd ? tempEnd: totalPage;
next = totalPage > tempEnd;
pageList = IntStream.rangeClosed(start, end).boxed().collect(Collectors.toList());
}


//Page 객체 와 함수를 적용해서 List로 변환해주는 메서드
public PageResponseDTO(Page<EN> result, Function<EN, DTO> fn) {
//출력할 데이터 목록 생성
dtoList = result.stream().map(fn).collect(Collectors.toList());
//페이지 번호 목록 생성
totalPage = result.getTotalPages();
makePageList(result.getPageable());
}
}

7)테스트 클래스에서 메서드를 생성해서 테스트
//목록 보기 테스트 - 목록 과 페이지 번호 테스트
@Test
public void testPageList() {
PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(21).size(10).build();
PageResponseDTO <MemoDTO, Memo> resultDTO = memoService.getList(pageRequestDTO);
for(MemoDTO memoDto : resultDTO.getDtoList()) {
System.out.println(memoDto);
}
//이전 과 다음 페이지 존재 여부
System.out.println("이전:" + resultDTO.isPrev());
System.out.println("다음:" + resultDTO.isNext());
//전체 페이지 개수
System.out.println("전체 페이지 개수:" + resultDTO.getTotalPage());
//페이지 번호 목록
System.out.println(resultDTO.getPageList());

}

8)MemoPageController 클래스를 수정해서 목록 보기를 구현
@Controller
//로그 기록을 편리하게 할 수 있도록 해주는 어노테이션
@Log4j2
//인스턴스 변수의 주입을 생성자에서 자동으로 처리하도록 해주는 어노테이션
@RequiredArgsConstructor
public class MemoPageController {
private final MemoService memoService;

// / 요청이 오면 templates 디렉토리에 있는 main.html을 출력
@GetMapping("/")
public String main() {
//redirect 할 때 는 View의 이름을 적는 것이 아니고 요청을 적어야 합니다.
return "redirect:/memo/list";
}

//목록보기 요청을 처리
@GetMapping("/memo/list")
public void list(PageRequestDTO pageRequestDTO, Model model) {
log.info("목록 보기...");
model.addAttribute("result", memoService.getList(pageRequestDTO));
}

}

9)templates 디렉토리 안에 memo 디렉토리를 생성하고 그 안에 list.html 파일을 만들어서 작성
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic::setContent(~{this::content})}">
<th:block th:fragment="content">
<h1>Memo Application</h1>

<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">제목</th>
<th scope="col">내용</th>
<th scope="col">작성일</th>
</tr>
</thead>

<tbody>
<tr th:each="dto:${result.dtoList}">
<th scope = "row">[[${dto.gno}]]</th>
<td>[[${dto.title}]]</td>
<td>[[${dto.writer}]]</td>
<td>[[${#temporals.format(dto.regDate, 'yyyy/MM/dd')}]]</td>
</tr>
</tbody>
</table>

<ul class="pagination h-100 justify-content-center align-items-center">
<li class="page-item" th:if="${result.prev}">
<a class="page-link" th:href="@{/memo/list(page=${result.start-1})}" 
tabindex="-1">이전</a>
</li>

<li th:class=" 'page-item ' + ${result.page == page?'active':''} " 
th:each="page:${result.pageList}">
<a class="page-link" th:href="@{/memo/list(page=${page})}" >[[${page}]]</a>

<li class="page-item" th:if="${result.next}">
<a class="page-link" th:href="@{/memo/list(page=${result.end+1})}" >다음</a>
</li>
</ul>
</th:block>
</th:block>


11.데이터 삽입 처리
=>데이터 삽입 과정: 데이터 삽입 요청을 하면 데이터를 입력할 수 있는 화면으로 이동하고 데이터 입력이 끝난 후 데이터 삽입 요청을 하면 데이터 삽입을 수행
단순한 화면 이동이나 데이터 출력은 GET 방식으로 이루어지고 나머지 작업은 대부분 POST(PUT, DELETE) 방식으로 처리합니다.
삽입, 삭제, 수정 작업이 완료되면 웹 의 경우 페이지 이동은 redirect를 이용합니다.

1)MemoPageController을 삽입 처리를 위한 메서드를 생성
//삽입 화면으로 이동하는 요청을 처리
@GetMapping("/memo/register")
public void register() {
log.info("데이터 삽입 화면으로 이동");
}

//데이터 삽입 처리
@PostMapping("/memo/register")
public String register(MemoDTO dto, Model model, HttpSession session, 
RedirectAttributes rAttr) {
//여기가 제대로 출력이 안되면 요청 URL 과 View 이름을 확인하고
//form의 경우라면 입력 요소의 name을 확인
log.info("파라미터:", dto);

//삽입
Long gno = memoService.insertMemo(dto);

//model에 msg를 저장
//model.addAttribute("msg", gno + " 삽입 성공");

//session.setAttribute("msg", gno + " 삽입 성공");

//리다이렉트 할 때 데이터를 전달
rAttr.addFlashAttribute("msg", gno + " 삽입 성공");

return "redirect:/memo/list";

}


2)templates/memo 디렉토리 안에 register.html 파일을 만들고 데이터 입력 화면을 생성
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic::setContent(~{this::content})}">
<th:block th:fragment="content">

<h1 class="mt4">메모 등록</h1>

<form th:action="@{/memo/register}" th:method="post">
<div class="form-group">
<label>제목</label>
<input type="text" class="form-control" name="title" placeholder="제목 입력" />
</div>
<div class="form-group">
<label>내용</label>
<textarea class="form-control" name="content"></textarea>
</div>
<div class="form-group">
<label>작성자</label>
<input type="text" class="form-control" name="writer" />
</div>
<button type="submit" class="btn btn-primary">저장</button>

</form>


</th:block>
</th:block>

3)list.html 파일에 데이터 삽입 요청을 생성
<h1>Memo Application
<span>
<a th:href="@{/memo/register}">
<button type="button" class="btn btn-outline-warning">메모 작성</button>
</a>
</span>

</h1>

<div th:if="${msg != null}" th:text="${msg}"></div>


4)애플리케이션을 실행하고 메모 작성을 해서 메시지가 출력되는지까지 확인


12.상세보기
=>처리 과정: 목록에서 제목이나 글번호 같은 것을 클릭하면 하나의 데이터를 찾아올 수 있는 기본키 값 과 함께 서버에 요청하고 서버는 기본키 값을 이용해서 데이터를 읽어와서 리턴

1)list.html 파일에서 제목을 출력하는 부분을 수정
<td><a th:href="@{/memo/read(gno=${dto.gno}, page=${result.page})}">[[${dto.title}]]</a></td>

2)MemoService 인터페이스에 상세보기 처리를 위한 메서드를 선언
//상세 보기를 위한 메서드
public MemoDTO read(Long gno);

3)MemoServiceImpl 클래스에 상세보기 처리를 위한 메서드를 구현
@Override
public MemoDTO read(Long gno) {
//기본키를 이용해서 데이터 찾아오기
Optional <Memo> memo = memoRepository.findById(gno);
return memo.isPresent() ? entityToDto(memo.get()) : null;
}

4)MemoController 클래스에 상세보기 요청을 처리하기 위한 메서드 작성
//상세보기 처리를 위한 메서드
@GetMapping("/memo/read")
public void read(long gno, @ModelAttribute("requestDTO") PageRequestDTO requestDTO, Model model) {

MemoDTO memo = memoService.read(gno);
model.addAttribute("dto", memo);

}

5)memo 디렉토리에 read.html 파일을 생성하고 작성
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic::setContent(~{this::content})}">
<th:block th:fragment="content">
<h1 class="mt4">메모 상세 보기</h1>

<div class="form-gruop">
<label>GNO</label>
<input type="text" class="form-control" name="gno" 
th:value="${dto.gno}" readOnly></input>
</div>
<div class="form-gruop">
<label>제목</label>
<input type="text" class="form-control" name="title" 
th:value="${dto.title}" readOnly></input>
</div>
<div class="form-gruop">
<label>내용</label>
<textarea class="form-control" rows="5" name="content" 
 readOnly>[[${dto.content}]]</textarea>
</div>
<div class="form-gruop">
<label>작성자</label>
<input type="text" class="form-control" name="writer" 
th:value="${dto.writer}" readOnly></input>
</div>
<div class="form-gruop">
<label>작성 시간</label>
<input type="text" class="form-control" name="regDate" 
th:value="${#temporals.format(dto.regDate, 'yyyy/MM/dd HH:mm:ss')}" readOnly></input>
</div>
<div class="form-gruop">
<label>수정 시간</label>
<input type="text" class="form-control" name="modDate" 
th:value="${#temporals.format(dto.modDate, 'yyyy/MM/dd HH:mm:ss')}" readOnly></input>
</div>

<a th:href="@{/memo/list(page=${requestDTO.page})}">
<button type="button" class="btn btn-info">목록</button>
</a>
</th:block>
</th:block>

16.데이터 수정
=>상세보기에 수정 링크를 생성해서 수정 링크를 누르면 수정 화면으로 이동을 하고 수정을 한 후 수정 버튼을 누르면 수정되도록 작성

1)read.html 파일에 수정 링크를 생성
<a th:href="@{/memo/modify(gno=${dto.gno}, page=${requestDTO.page})}">
<button type="button" class="btn btn-info">주정</button>                                               </a>

2)상세보기에서 수정을 눌렀을 때 처리를 위한 메서드를 MemoPageController에 생성하는데 상세보기 처리 위에 URL 만 추가

//상세보기 와 수정 보기 처리를 위한 메서드
@GetMapping({"/memo/read", "/memo/modify"})
public void read(long gno, @ModelAttribute("requestDTO") PageRequestDTO requestDTO, 
Model model) {

MemoDTO memo = memoService.read(gno);
model.addAttribute("dto", memo);

}

3)memo 디렉토리에 modify.html 파일을 만들어서 수정 화면을 생성
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic::setContent(~{this::content})}">
<th:block th:fragment="content">
<h1 class="mt-4">메모 수정</h1>

<form action="/memo/modify" method="post">
 <input type="hidden" name="page" th:value="${requestDTO.page}" />
 
<div class="form-group">
<label>글번호</label>
<input type="text" class="form-control" name="gno" 
th:value="${dto.gno}" readonly="readonly" />
</div>

<div class="form-group">
<label>제목</label>
<input type="text" class="form-control" name="title" 
th:value="${dto.title}" />
</div>
<div class="form-group">
<label>내용</label>
<textarea  class="form-control" name="content">[[${dto.content}]]</textarea> 
</div>
<div class="form-group">
<label>작성자</label>
<input type="text" class="form-control" name="writer" 
th:value="${dto.writer}" readonly="readonly" />
</div>
<div class="form-group">
<label>작성일</label>
<input type="text" class="form-control" 
th:value="${#temporals.format(dto.regDate, 'yyyy/MM/dd HH:mm:ss')}" 
readonly="readonly" />
</div>
<div class="form-group">
<label>수정일</label>
<input type="text" class="form-control" 
th:value="${#temporals.format(dto.modDate, 'yyyy/MM/dd HH:mm:ss')}" 
readonly="readonly" />
</div>
</form>
<button type="button" class="btn btn-primary listBtn">목록</button>
<button type="button" class="btn btn-primary modifyBtn">수정</button>

<script th:inline="javascript">
//form 객체 찾아오기
var actionForm = $("form")

//목록 버튼을 눌렀을 때
$(".listBtn").click(function(){
var page = $("input[name='page']");
actionForm.empty();
actionForm.append(page);
actionForm.attr('action', '/memo/list').attr('method', 'get');
actionForm.submit();
});

//수정 버튼을 눌렀을 때
$(".modifyBtn").click(function(){
if(!confirm('수정하시겠습니까?')){
return;
}

actionForm.attr('action', '/memo/modify').attr('method', 'post');
actionForm.submit();
});
</script>
</th:block>
</th:block>

4)MemoService 인터페이스에서 수정을 처리하는 메서드를 선언
//수정을 처리하는 메서드
public void modify(MemoDTO dto);

5)MemoServiceImpl 클래스에 수정을 처리하는 메서드 구현
@Override
public void modify(MemoDTO dto) {
//데이터 찾아오기
Optional<Memo> result = memoRepository.findById(dto.getGno());
if(result.isPresent()) {
Memo memo = result.get();
memo.changeTitle(dto.getTitle());
memo.changeContent(dto.getContent());
memoRepository.save(memo);
}

}

6)MemoPageController 클래스에 수정 요청을 처리하기 위한 메서드를 구현
@PostMapping("/memo/modify")
public String modify(MemoDTO memo, @ModelAttribute("requestDTO") PageRequestDTO requestDTO,
RedirectAttributes rattr) {
log.info("dto:" + memo);

memoService.modify(memo);

//상세보기로 이동할 때 필요한 gno 값 과 page 값을 설정
rattr.addAttribute("gno", memo.getGno());
rattr.addAttribute("page", requestDTO.getPage());

return "redirect:/memo/read";
}


17.데이터 삭제
=>삭제는 기본키를 받아서 삭제하던지 아니면 DTO를 받아서 처리
1)MemoService 인터페이스에 삭제를 위한 메서드 선언
//삭제를 처리하는 메서드
public void remove(Long gno);

2)MemoServiceImpl 클래스에 삭제를 위한 메서드 구현
@Override
public void remove(Long gno) {
//데이터 찾아오기
Optional<Memo> result = memoRepository.findById(gno);
if(result.isPresent()) {
memoRepository.deleteById(gno);
}
}


3)MemoPageController 클래스에 삭제 요청을 처리하는 요청 처리 메서드를 작성
@PostMapping("/memo/remove")
public String remove(long gno, RedirectAttributes rattr) {
log.info("gno:" + gno);

memoService.remove(gno);

//상세보기로 이동할 때 필요한 gno 값 과 page 값을 설정
rattr.addAttribute("msg", gno + " 삭제");

return "redirect:/memo/list";
}

4)read.html 파일을 수정
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic::setContent(~{this::content})}">
<th:block th:fragment="content">
<h1 class="mt4">메모 상세 보기</h1>

<div class="form-gruop">
<label>GNO</label>
<input type="text" class="form-control" name="gno" 
th:value="${dto.gno}" id="gno" readOnly></input>
</div>
<div class="form-gruop">
<label>제목</label>
<input type="text" class="form-control" name="title" 
th:value="${dto.title}" readOnly></input>
</div>
<div class="form-gruop">
<label>내용</label>
<textarea class="form-control" rows="5" name="content" 
 readOnly>[[${dto.content}]]</textarea>
</div>
<div class="form-gruop">
<label>작성자</label>
<input type="text" class="form-control" name="writer" 
th:value="${dto.writer}" readOnly></input>
</div>
<div class="form-gruop">
<label>작성 시간</label>
<input type="text" class="form-control" name="regDate" 
th:value="${#temporals.format(dto.regDate, 'yyyy/MM/dd HH:mm:ss')}" readOnly></input>
</div>
<div class="form-gruop">
<label>수정 시간</label>
<input type="text" class="form-control" name="modDate" 
th:value="${#temporals.format(dto.modDate, 'yyyy/MM/dd HH:mm:ss')}" readOnly></input>
</div>

<a th:href="@{/memo/list(page=${requestDTO.page})}">
<button type="button" class="btn btn-info">목록</button>                                                                                               
</a>
<a th:href="@{/memo/modify(gno=${dto.gno}, page=${requestDTO.page})}">
<button type="button" class="btn btn-primary">수정</button>                                                                                               
</a>

<button type="button" class="btn btn-warning">삭제</button>                                                                                               
<script>
$('.btn-warning').click(function(){
if(!confirm("정말로 삭제?")){
return;
}

var request = new XMLHttpRequest();
var url = '/memo/remove';
request.open('POST', url, true);
var params='gno=' + document.getElementById('gno').value;
request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

request.send(params);

request.addEventListener('load', () => {
alert("삭제 완료");
location.href = '/memo/list';
})

});
</script>
</th:block>
</th:block>

18.검색 조건 활용
=>어떤 종류의 검색 조건이 있는지 결정
제목 - t
내용 - c
작성자 - w
제목 + 내용 - tc
제목 + 내용 + 작성자 - tcw

1)조회를 할 때 사용하는 PageRequestDTO 클래스에 검색 조건 과 값을 나타낼 인스턴스 변수를 추가
//검색 항목을 저장할 변수
private String type;
//검색 값을 저장할 변수
private String keyword;

2)MemoServiceImpl 클래스에 검색 조건을 만들어주는 메서드 생성
//검색 조건을 만들어 주는 메서드
private BooleanBuilder getSearch(PageRequestDTO requestDTO) {
//querydsl에서 사용할 검색 조건을 만드는 객체 생성
BooleanBuilder booleanBuilder = new BooleanBuilder();

//검색 항목을 읽음
String type = requestDTO.getType();

//querydsl 이 Entity에 적용할 검색 Entity를 제공하는데 그 객체를 가져옴
QMemo qMemo = QMemo.memo;

//검색에 사용할 값
String keyword = requestDTO.getKeyword();

//gno 값이 0보다 큰 데이터만 조회
BooleanExpression expression = qMemo.gno.gt(0L);
booleanBuilder.and(expression);

//검색 조건이 없을 때
if(type == null || type.trim().length() == 0) {
return booleanBuilder;
}

//검색 조건이 있는 경우
BooleanBuilder conditionBuilder = new BooleanBuilder();
if(type.contains("t")) {
conditionBuilder.or(qMemo.title.contains(keyword));
}
if(type.contains("c")) {
conditionBuilder.or(qMemo.content.contains(keyword));
}
if(type.contains("w")) {
conditionBuilder.or(qMemo.writer.contains(keyword));
}
//모든 조건 통합
booleanBuilder.and(conditionBuilder);

return booleanBuilder;
}

3)MemoServiceImpl 클래스의 목록을 조회하는 메서드를 수정
@Override
public PageResponseDTO<MemoDTO, Memo> getList(PageRequestDTO requestDTO) {
//정렬 조건을 적용해서 페이징 객체를 생성
Pageable pageable = requestDTO.getPageable(Sort.by("gno").descending());
//검색 조건을 생성
BooleanBuilder booleanBuilder = getSearch(requestDTO);
//검색 조건을 적용해서 조회
Page<Memo> result = memoRepository.findAll(booleanBuilder, pageable);

//Entity를 DTO로 변환해주는 함수 설정
Function<Memo, MemoDTO> fn = (entity -> entityToDto(entity));
//결과 리턴
return new PageResponseDTO <>(result, fn);
}

4)Test 클래스에서 생성한 메서드 테스트
@Test
public void testListSearch() {
PageRequestDTO pageRequestDTO = 
PageRequestDTO.builder().page(1).size(10).type("tc").keyword("강진").build();
PageResponseDTO<MemoDTO, Memo> result = memoService.getList(pageRequestDTO);
System.out.println(result.getDtoList());
}

5)list.html 파일에 검색 폼 추가
<form action="/memo/list" method="get" id="searchform">
<div class="input_group">
<input type="hidden" name="page" value="1"/>
<div class="input-group-prepend">
<select class="custom-select" name="type">
<option th:selected="${pageRequestDTO.type == null}">---</option>
<option value="t" 
th:selected="${pageRequestDTO.type == 't'}">제목</option>
<option value="c" 
th:selected="${pageRequestDTO.type == 'c'}">내용</option>
<option value="w" 
th:selected="${pageRequestDTO.type == 'w'}">작성자</option>
<option value="tc" 
th:selected="${pageRequestDTO.type == 'tc'}">제목 또는 내용</option>
<option value="tcw" 
th:selected="${pageRequestDTO.type == 'tcw'}">제목 또는 내용 또는 작성자</option>
</select>

<input class="form-control" name="keyword" 
th:value="${pageRequestDTO.keyword}" />
</div>

<div class="input-group-append" id="button-addon4">
<button class="btn btn-outline-secondary btn-search" type="button">
검색</button>
<button class="btn btn-outline-secondary btn-clear" type="button">
초기화</button>
</div>
</div>
</form>

6)list.html 파일에 스크립트 코드 추가
<script>
var searchForm = $("#searchform");

$('.btn-search').click(function(){
searchForm.submit();
});

$('.btn-clear').click(function(){
searchForm.empty().submit();
});
</script>


7)list.html 파일에서 페이지 링크 부분을 수정- 페이지 번호를 눌렀을 때 검색 폼의 내용이 사라지는 현상을 제거
<ul class="pagination h-100 justify-content-center align-items-center">
<li class="page-item" th:if="${result.prev}">
<a class="page-link" th:href="@{/memo/list(page=${result.start-1}, type=${pageRequestDTO.type}, keyword=${pageRequestDTO.keyword})}" 
tabindex="-1">이전</a>
</li>

<li th:class=" 'page-item ' + ${result.page == page?'active':''} " 
th:each="page:${result.pageList}">
<a class="page-link" th:href="@{/memo/list(page=${page}, type=${pageRequestDTO.type}, keyword=${pageRequestDTO.keyword})}" >[[${page}]]</a>

<li class="page-item" th:if="${result.next}">
<a class="page-link" th:href="@{/memo/list(page=${result.end+1}, type=${pageRequestDTO.type}, keyword=${pageRequestDTO.keyword})}" >다음</a>
</li>
</ul>

8)list.html 파일의 상세보기 링크 부분도 수정
<td><a th:href="@{/memo/read(gno=${dto.gno}, page=${result.page} , type=${pageRequestDTO.type}, keyword=${pageRequestDTO.keyword})}">
[[${dto.title}]]</a></td>

9)read.html 파일에서 수정 과 목록 보기 요청에도 동일한 파라미터를 추가
<a th:href="@{/memo/list(page=${requestDTO.page}, type=${requestDTO.type}, keyword=${requestDTO.keyword})}">
<button type="button" class="btn btn-info">목록</button>                                                                                               
</a>
<a th:href="@{/memo/modify(gno=${dto.gno}, page=${requestDTO.page}, type=${requestDTO.type}, keyword=${requestDTO.keyword})}">
<button type="button" class="btn btn-primary">수정</button>                                                                                               
</a>

10)modify.html 파일에서 form에 type 과 keyword를 같이 전송할 수 있도록 hidden을 추가
<input type="hidden" name="keyword" th:value="${requestDTO.keyword}" />
<input type="hidden" name="type" th:value="${requestDTO.type}" />

11)modify.html 파일에서 목록보기를 눌렀을 때를 처리하는 자바스크립트를 수정
//목록 버튼을 눌렀을 때
$(".listBtn").click(function(){
var page = $("input[name='page']");
var type = $("input[name='type']");
var keyword = $("input[name='keyword']");

actionForm.empty();

actionForm.append(page);
actionForm.append(type);
actionForm.append(keyword);

actionForm.attr('action', '/memo/list').attr('method', 'get');
actionForm.submit();
});


12)MemoController 에서 수정을 처리하는 메서드를 수정
@PostMapping("/memo/modify")
public String modify(MemoDTO memo, @ModelAttribute("requestDTO") PageRequestDTO requestDTO,
RedirectAttributes rattr) {
log.info("dto:" + memo);

memoService.modify(memo);

//상세보기로 이동할 때 필요한 gno 값 과 page 값을 설정
rattr.addAttribute("gno", memo.getGno());
rattr.addAttribute("page", requestDTO.getPage());

rattr.addAttribute("type", requestDTO.getType());
rattr.addAttribute("keyword", requestDTO.getKeyword());

return "redirect:/memo/read";
}




    'Spring' 카테고리의 다른 글
    • [SpringBoot] 2022/05/30 OneToMany
    • [SpringBoot] 2022/05/24 ToDo (REST API)
    • [SpringBoot] 2022/05/23 SingleTable CRUD
    • [SpringBoot]2022/05/19: thymeleaf, Single table CRUD

    티스토리툴바