본문 바로가기
기초 및 언어/▶ Spring

17. Spring_BookSite만들기-1

by 류딩이 2025. 10. 14.

기본 준비

server.port = 8080
    spring.datasource.url=jdbc:oracle:thin:@localhost:1521:orcl
    spring.datasource.username=sqlid
    spring.datasource.password=sqlpw
    spring.datasource.driver-class-name=oracle.jdbc.OracleDriver

mybatis.mapper-locations=classpath:mapper/*.xml

spring.jpa.hibernate.ddl-auto=create 
spring.jpa.hibernate.ddl-auto=update 
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

 

pom.xml 추가

properties 아래 추가
<repositories>
        <repository>
            <id>oracle</id>
            <url>https://repo.maven.apache.org/maven2</url>
        </repository>
</repositories>

<dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.5</version>
</dependency>
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
            <groupId>nz.net.ultraq.thymeleaf</groupId>
            <artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>

 

새로 추가 (layout, ModelMapper)

<dependency>
        <groupId>nz.net.ultraq.thymeleaf</groupId>
        <artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
<dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>2.3.9</version>
</dependency>

 

Controller → Service → Repository → Entity → View(Thymeleaf)

 


📁common

 

header.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

    <meta charset="UTF-8">
    <div th:fragment="top">
        <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
            <div class="navbar-brand">
                <a th:href="'/'" style="color:white">도서 정보 페이지</a>
            </div>
        </nav>
    </div>
</html>

 

footer.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <meta charset="UTF-8">

    <div class="footer" th:fragment="bottom">
        <footer class="page-footer font-small cyan darken-3">
            <div class="footer-copyright text-center py-3">
                Book WebSite
            </div>
        </footer>
    </div>
</html>

 

mylayout.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
>
<head>
    <meta charset="UTF-8">
    <title>mylayout</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link th:href="@{/css/mylayout.css}" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>

    <th:block layout:fragment="script"></th:block>
    <th:block layout:fragment="css"></th:block>
</head>
<body>
<div th:replace="~{common/header::top}"></div>
<div layout:fragment="content"></div>
<div th:replace="~{common/footer::bottom}"></div>
</body>
</html>

📁templates/book

1. 기본 구조

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{common/mylayout}">

 

  • xmlns:th : Thymeleaf 네임스페이스 (th:속성 사용 가능)
  • xmlns:layout : Layout Dialect 사용을 위한 네임스페이스
  • layout:decorate : common/mylayout.html 템플릿을 기반으로 현재 페이지 내용을 삽입
    → 즉, 이 페이지는 공통 레이아웃을 상속받는 자식 페이지

 


select.html

더보기
<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{common/mylayout}">
>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div class="content" layout:fragment="content">
        <h2>도서 목록 페이지</h2>
        <form th:action="@{/blist}" method="get" class="form-inline mb-3 justify-content-center">
            <div class="form-group mx-sm-3 mb-2">
                <input type="text" name="keyword" th:value="${keyword}" class="form-control" placeholder="제목 또는 저자입력">
                <button type="submit" class="btn btn-primary mb-2">검색</button>
            </div>
        </form>
        <table class="table">
            <thead>
                <tr>
                    <th>번호</th>
                    <th>제목</th>
                    <th>저자</th>
                    <th>출판사</th>
                    <th>가격</th>
                    <th>입고일</th>
                    <th>배송비</th>
                    <th>구매가능서점</th>
                    <th>보유수량</th>
                    <th>수정</th>
                    <th>삭제</th>
                </tr>
            </thead>

            <tbody>
                <tr th:each="book : ${bookList}">
                    <td th:text="${book.no}"></td>
                    <td th:text="${book.title}"></td>
                    <td th:text="${book.author}"></td>
                    <td th:text="${book.publisher}"></td>
                    <td th:text="${book.price}"></td>
                    <td th:text="${book.buy}"></td>
                    <td th:text="${book.kind}"></td>
                    <td th:text="${book.bookstore}"></td>
                    <td th:text="${book.count}"></td>
                    <td>수정</td>
                    <td>삭제</td>
                </tr>
            </tbody>
        </table>

        <div style="text-align:center">
            <button type="button" class="btn btn-primary"
                    th:onclick="|location.href='@{/book/insert}'|">추가</button>
            <button type="button" class="btn btn-danger">선택항목 삭제</button>
        </div>
        <br>
        <!--페이지 설정-->
        <!--0 이상일때-->
        <div th:if="${bookList.totalPages > 0}">
            <nav>
                <ul class="pagination justify-content-center">
                    <li class="page-item" th:classappend="${bookList.hasPrevious()}?'':'disabled'">
                        <a class="page-link" th:href="@{/blist(page=${bookList.number-1}, size=${bookList.size}, keyword=${keyword})}">Previous</a>
                    </li>

                    <li class="page-item"
                        th:each="i : ${#numbers.sequence(0, bookList.totalPages-1)}" th:classappend="${i == bookList.number} ? 'active' : ''">
                        <a class="page-link"
                           th:href="@{/blist(page=${i}, size=${bookList.size}, keyword=${keyword})}" th:text="${i + 1}">
                        </a>
                    </li>

                    <li class="page-item" th:classappend="${bookList.hasNext()}?'':'disabled'">
                        <a class="page-link" th:href="@{/blist(page=${bookList.number+1}, size=${bookList.size}, keyword=${keyword})}">Next</a>
                    </li>

                </ul>
            </nav>
        </div>
    </div>
</body>
</html>

 

 


 

📁BookBean

public class BookBean {
    private int no;

    @NotBlank(message = "제목 필수 입력")
    private String title;
    @NotBlank(message = "저자 필수 입력")
    private String author;
    @NotBlank(message = "출판사 필수 입력")
    private String publisher;

    @NotNull(message = "가격을 입력하세요")
    @Min(value = 1, message = "가격은 1원 이상이어야 합니다.")
    private int price;
    @NotEmpty(message = "입고일 필수 입력")
    private String buy;
    @NotEmpty(message = "배송비 필수 입력")
    private String kind;
    @NotEmpty(message = "구입가능 서점 하나 이상 선택")
    private String bookstore;
    @Min(value = 1, message = "보유수량은 1개 이상이어야 합니다.")
    private int count;

📁Entity

BookEntity

@Entity
@Table(name = "book")
public class BookEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_seq_gen")
    @SequenceGenerator(
            name = "book_seq_gen",  // 시퀀스 제너레이터 이름
            sequenceName = "BOOK_SEQ",  // 실제 DB 시퀀스 이름
            allocationSize = 1  // 증가 단위 (보통 1)
    )
    private int no;
    private String title;
    private String author;
    private String publisher;
    private int price;
    private String buy;
    private String kind;
    private String bookstore;
    private int count;

 

  • @Entity : JPA가 관리하는 엔티티 클래스
  • @Table(name="book") : 실제 DB 테이블 이름 명시
  • @Id : 기본키 지정
  • @GeneratedValue + @SequenceGenerator : Oracle 시퀀스로 PK 자동 증가

📁Controller

BookController

더보기
@Controller
public class BookController {

    // (1) BookService를 주입받음 (비즈니스 로직 처리 담당)
    @Autowired
    BookService bookService;

    // [1️⃣ 도서 목록 조회 + 검색 + 페이징]
    @GetMapping(value = {"/", "blist"}) // (2) URL의 쿼리 파라미터를 받음
    public String getAllBooks(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "3") int size,
            @RequestParam(required = false) String keyword,
            Model model
    ){

        // (3) DB의 전체 도서 수 조회
        long totalCount = bookService.count();
        System.out.println("totalCount : " + totalCount);


        // (4) Service를 통해 현재 페이지에 표시할 도서 목록을 가져옴 :  Page Class
        Page<BookEntity> bookList = bookService.getBookEntity(page,size,keyword); // 현재 페이지에 출력할 3가지

        // (5) 디버깅용 로그 출력 (페이징 정보)
        System.out.println("bookList.getTotalElements() :" +bookList.getTotalElements()); //전체 데이터 개수
        System.out.println("bookList.getTotalPages() :" +bookList.getTotalPages()); // 전체 페이지 수
        System.out.println("bookList.getNumber() :" + bookList.getNumber()); // 현재 페이지 번호(0부터 시작)
        System.out.println("bookList.getNumberOfElements()" + bookList.getNumberOfElements());

        // (6) 화면(view)에 전달할 데이터 등록
        model.addAttribute("bookList", bookList);
        model.addAttribute("page", page);
        model.addAttribute("size", size);
        model.addAttribute("keyword", keyword);
        model.addAttribute("totalCount", totalCount);

        return "book/select";
    }

    // [2️⃣ 도서 등록 폼으로 이동]
    @GetMapping(value = "book/insert")
    public String insertBook(Model model){
        model.addAttribute("bookBean", new BookBean());
        return "book/insert";
    }

    // [3️⃣ 도서 등록 처리]
    @PostMapping(value = "/book/insert")
    public String insertBookProc(@Valid@ModelAttribute("bookBean") BookBean bookBean, BindingResult br, Model model){

        if(br.hasErrors()){
            return "/book/insert";
        }
        //bookBean을 Entity로 바꾸는 작업
        BookEntity bookEntity = bookService.beanToEntity(bookBean);
        bookService.saveBook(bookEntity);
        return "redirect:/blist";
    }
}

🔽bookBean을 Entity로 바꾸는 작업을 하는 이유 ==> Service에서 작업

BookEntity bookEntity = bookService.beanToEntity(bookBean);
bookService.saveBook(bookEntity);

1️⃣ 배경: Bean과 Entity의 역할이 다름

  • BookBean 은 폼과 화면용
  • BookEntity 는 데이터베이스 저장용

2️⃣ save() 메서드는 BookEntity만 저장할 수 있음

👉 따라서 Bean → Entity 변환 과정이 필요


📁Service

BookService

 📘 [BookService 클래스 설명]
 Controller ↔ Service ↔ Repository 사이에서 데이터를 주고받는 중간 다리 역할
 - Controller가 요청을 보내면 Service가 로직을 처리하고
 - Repository를 통해 DB와 실제로 데이터를 주고받는다.
 

ModelMapper 객체 생성 - Bean ↔ Entity 간 자동 변환을 도와주는 라이브러리

더보기
package com.example.Ex02.service;

/*controller - BookService - repository :중간 연결다리*/

import com.example.Ex02.bean.BookBean;
import com.example.Ex02.entity.BookEntity;
import com.example.Ex02.repository.BookRepository;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

@Service
public class BookService {
    @Autowired
    BookRepository bookRepository;

    // ModelMapper : Bean ↔ Entity 자동 변환 도구
    private static ModelMapper modelMapper = new ModelMapper();

    // 1️⃣ 도서 총 개수 조회
    public long count() {
        return bookRepository.count();
    }

    // 2️⃣ 도서 목록 조회 (검색 + 페이징)
    // Pageable : 페이징 정보와 정렬 기준 지정 (no 기준 내림차순)

    public Page<BookEntity> getBookEntity(int page, int size, String keyword) {
        Pageable pageable = PageRequest.of(page,size, Sort.by("no").descending());

        // 검색어(keyword)가 없으면 전체 목록 반환
        if (keyword == null || keyword.isEmpty()){
            return bookRepository.findAll(pageable);
        } 
        // 검색어가 있으면 제목 또는 저자에 keyword가 포함된 결과만 반환
        else{
            return bookRepository.findByTitleContainingOrAuthorContaining(keyword, keyword, pageable);
        }
    }

    // 3️⃣ Bean → Entity 변환
    public BookEntity beanToEntity(BookBean bookBean) {
        BookEntity bookEntity= modelMapper.map(bookBean, BookEntity.class);
        return bookEntity;
    }
    // 4️⃣ 도서 저장 (등록 또는 수정)
    public void saveBook(BookEntity  bookEntity){
        bookRepository.save(bookEntity);
    }

}

 

📁Repository (인터페이스)

Controller  →  Service  →  Repository(interface)  →  DB

public interface BookRepository extends JpaRepository<BookEntity, Integer> {

    Page<BookEntity> findByTitleContainingOrAuthorContaining(String keyword, String keyword1, Pageable pageable);
}