[Java+SpringBoot+JPA] 이미지 삽입을 활용한 게시판 만들기

2024. 1. 9. 22:04Pratice/CRUD

반응형

 

 

 


 

 

🟢 Constant

▪️ Role을 이용해 책 카테고리 분류 (한글로 불러올 예정)

public enum BookRole {

  ALL("전체"),
  CULTURE("교양"),
  COMPUTER("컴퓨터"),
  NOVEL("소설"),
  STUDY("학습"),
  ETC("기타");

  private String description;

  BookRole(String description) {
    this.description = description;
  }

  public String getDescription() {
    return description;
  }

}

 

 

🟢 Entity

▪️ Role을 이용해 책 카테고리 분류 (한글로 불러올 예정)

@Entity
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Table(name="book")
@SequenceGenerator(name = "book_SEQ",
                    sequenceName = "book_SEQ",
                    allocationSize =1,
                    initialValue = 1)
public class BookEntity extends BaseEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_SEQ")
  @Column(name = "bookId")
  private Integer bookId;          //도서 번호

  @Enumerated(EnumType.STRING)
  private BookRole bookRole;

  @Column(name = "bookTitle", length = 50, nullable = false)
  private String bookTitle;     //도서 제목

  @Lob
  @Column(name = "bookInfo", length = 1000, nullable = false)
  private String bookInfo;      //도서 정보

  @Column(name = "bookAuthor", length = 20)
  private String bookAuthor;      //저자

  @Column(name = "bookCompany", length = 20)
  private String bookCompany;   //출판사

  @Column(name = "bookDate")
  private String bookDate;      //발매일

  @Column(name = "bookPrice", nullable = false)
  private int bookPrice;        //가격

  @Column(name = "bookImg")
  private String bookImg;    // 이미지 파일의 경로나 URL을 저장

}
더보기

▪️ BaseEntity

@Getter
@Setter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

  @Column(name = "credate", nullable = false, updatable = false)
  @CreatedDate
  private LocalDate credate;

  @Column(name = "modidate")
  @LastModifiedDate
  private LocalDateTime modidate;

}

 

🟢 DTO

▪️ Role을 이용해 책 카테고리 분류 (한글로 불러올 예정)

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class BookDTO {

  private Integer bookId;          //도서 번호

  private BookRole bookRole;

  @NotEmpty (message = "도서 제목을 입력해주세요.")
  private String bookTitle;     //도서 제목

  @NotEmpty (message = "도서 정보를 입력해주세요.")
  private String bookInfo;      //도서 정보

  private String bookAuthor;      //저자

  private String bookCompany;   //출판사

  private String bookDate;      //발매일

  @NotNull (message = "가격을 입력해주세요.")
  private int bookPrice;        //가격

  private String bookImg;       // 이미지 파일의 경로나 URL을 저장

  private LocalDate credate;    //등록날짜

  private LocalDateTime modidate;   //수정날짜
  
}

 

 

🟢 Repository

▪️ 검색 조건 : 제목, 저자, 제목+저자, 카테고리

@Repository
public interface BookRepository extends JpaRepository <BookEntity, Integer> {

  //검색처리(도서제목, 저자, 도서제목+저자)

  //도서제목
  @Query("SELECT u FROM BookEntity u WHERE u.bookTitle like %:keyword%")
  Page<BookEntity> findByBookTitle(String keyword, Pageable pageable);

  //저자
  @Query("SELECT u FROM BookEntity u WHERE u.bookAuthor like %:keyword%")
  Page<BookEntity> findByBookAuthor(String keyword, Pageable pageable);

  //도서 제목 + 저자
  @Query("SELECT u FROM BookEntity u WHERE u.bookTitle like %:keyword% OR u.bookInfo like %:keyword%")
  Page<BookEntity> findByBookTitleOrBookInfo(String keyword, Pageable pageable);

  //카테고리
  @Query("SELECT u FROM BookEntity u WHERE u.bookRole = :bookRole")
  Page<BookEntity> findByBookRole(@Param("bookRole") BookRole bookRole, Pageable pageable);

}

 

더보기

▪️ BookRepository TEST

@SpringBootTest
@RequiredArgsConstructor
public class BookRepositoryTest {
  
  @Autowired
  private BookRepository bookRepository;


  //삽입
  @Test
  public void insert() throws Exception{
    BookEntity bookEntity = BookEntity.builder()
        .bookRole(BookRole.ALL)
        .bookTitle("TEST")
        .bookInfo("TEST")
        .bookAuthor("TEST")
        .bookCompany("TEST")
        .bookPrice(25000)
        .build();
    bookRepository.save(bookEntity);
  }
  
  //대량 삽입
  @Test
  public void insert2() throws Exception{
    for(int i=1; i<50; i++) {
      BookEntity bookEntity = BookEntity.builder()
          .bookRole(BookRole.ALL)
          .bookTitle("TEST"+i)
          .bookInfo("TEST"+i)
          .bookAuthor("TEST"+i)
          .bookCompany("TEST"+i)
          .bookPrice(i)
          .build();
      bookRepository.save(bookEntity);
    }
  }


  //전체 조회
  @Test
  public void findeAll() throws Exception{
    List<BookEntity> bookEntityList = bookRepository.findAll();
    for(BookEntity data : bookEntityList) {
      System.out.println(data.toString());
    }
  }

  //개별조회
  @Test
  public void findeDne() throws Exception{
    Integer bookId = 1;
    Optional<BookEntity> bookEntity = bookRepository.findById(bookId);
    System.out.println(bookEntity.toString());
  }


  //수정
  @Test
  public void update() throws Exception{
    Integer bookId = 1;
    Optional<BookEntity> bookEntity = bookRepository.findById(bookId);
    BookEntity result = bookEntity.orElseThrow();

    result.setBookAuthor("test");
    result.setBookCompany("test");
    result.setBookInfo("test");
    bookRepository.save(result);

    if(result == null) {
      System.out.println("수정 실패 하였습니다.");
    } else {
      System.out.println("수정 성공 하였습니다.");
    }

  }


  //삭제
  @Test
  public void delete() throws Exception{
    int bonum = 50;
    bookRepository.deleteById(bonum);
    boolean check = bookRepository.existsById(bonum);

    if (check) {
      System.out.println("삭제 실패하였습니다.");
    } else {
      System.out.println("삭제 성공하였습니다.");
    }
  }

  
  //검색 조회 (도서제목)
  @Test
  public void search() throws Exception {
    Page<BookEntity> bookEntityList = bookRepository.findByBookTitleOrBookInfo("테스트", PageRequest.of(1, 10, Sort.by(Sort.DEFAULT_DIRECTION, "bonum")));
    System.out.println("검색 결과: "+bookEntityList.toString());
  }

}

 

 

🟢 application.properties

#파일전송 사용
spring.servlet.multipart.enabled=true

#1개 파일의 최대크기
spring.servlet.multipart.max-file-size=20MB

#전체 파일의 최대크기
spring.servlet.multipart.max-request-size=100MB

#상품의 이미지를 등록할 위치, AWS 이용(사용자 변수)
itemImgLocation=C:/img/item/

#경로
uploadPath=file:///C:/img/

 

 

 

🟢 Config

▪️ WebMVCConfig

▪️ MVC : Model + View + Controller

@Configuration
public class WebMvcCofig implements WebMvcConfigurer {

  @Value(("${uploadPath}"))
  String uploadPath;

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/images/**")
        .addResourceLocations(uploadPath);    //자원위치 = 물리적위치
  }

}

 

 

🟢 FileService

▪️ 이미지 저장 (경로, 파일명, 데이터 값)

▪️ 이미지 삭제

@Service
public class FileService {

  //저장할 경로, 파일명, 데이터 값
  public String uploadFile(String uploadPath, String originalFileName, byte[] filedata) throws Exception {

    UUID uuid = UUID.randomUUID(); //문자열 생성
    String extendsion = originalFileName.substring(originalFileName.lastIndexOf("."));    //문자열 분리

    String saveFileName = uuid.toString()+extendsion;    //새로운 파일명

    //실질적 저장
    String uploadFullurl = uploadPath+saveFileName;      //저장위치 및 파일명

    FileOutputStream fos = new FileOutputStream(uploadFullurl);
    fos.write(filedata);
    fos.close();

    return saveFileName;   //데이터베이스에 저장할 파일명
  }


  //파일 삭제 (상품을 수정시 기존 파일을 삭제하고 새로운 파일을 저장)
  //삭제는 보낼 필요가 없으니 void
  public void deleteFile(String uploadPath, String fileName) throws Exception {
    String deleteFileName = uploadPath+fileName;

    File deleteFile = new File(deleteFileName);
    if(deleteFile.exists()) {    //파일이 존재하면
      deleteFile.delete();
    }
  }

}

 

 

 

🟢 Service

▪️ 이미지 경로 선언

▪️ MultipartFile imgFile 매개변수로 추가

▪️ 게시글 등록, 수정, 삭제 할 때 이미지 조건 설정 (게시글 수정 할 때, 이미지는 반드시 수정 될 필요 없음!)

 

▪️ 검색조건 : t-제목, ti-제목 및 내용, a-저자, ca-카테고리

@Service
@RequiredArgsConstructor
@Transactional
public class BookService {

  @Value("${itemImgLocation}")
  private String itemImgLocation;

  private final FileService fileService;
  private final BookRepository bookRepository;
  private final ModelMapper modelMapper = new ModelMapper();


  //삽입
  public void insert(BookDTO bookDTO, MultipartFile imgFile) throws Exception {

    //이미지 삽입
    String originalFileName = imgFile.getOriginalFilename();
    String newFileName = "";

    if(originalFileName != null) {
      newFileName = fileService.uploadFile(itemImgLocation, originalFileName, imgFile.getBytes());
    }
    bookDTO.setBookImg(newFileName);

    BookEntity bookEntity = modelMapper.map(bookDTO, BookEntity.class);
    bookRepository.save(bookEntity);
  }


  //검색 조회
  public Page<BookDTO> search(String type, String keyword, String bookRole, Pageable page) throws Exception {

    int currentPage = page.getPageNumber()-1;
    int pageLimit = 10;    //페이지 리스트 수

    //int page 대신에 Pageable을 사용하면 page-1이 아니고 page로 사용
    Pageable pageable = PageRequest.of(currentPage, pageLimit, Sort.by(Sort.Direction.DESC, "bookId"));

    Page<BookEntity> bookEntityPage;

    if (type.equals("t") && keyword != null) {
      bookEntityPage = bookRepository.findByBookTitle(keyword, pageable);
    } else if (type.equals("a") && keyword != null) {
      bookEntityPage = bookRepository.findByBookAuthor(keyword, pageable);
    } else if (type.equals("ti") && keyword != null) {
      bookEntityPage = bookRepository.findByBookTitleOrBookInfo(keyword, pageable);
    } else if (type.equals("ca") && keyword != null) {
      bookEntityPage = bookRepository.findByBookRole(BookRole.valueOf(bookRole), pageable);
    } else {
      bookEntityPage = bookRepository.findAll(pageable);
    }

    List<BookDTO> bookDTOList = bookEntityPage.stream()
        .map(bookEntity -> modelMapper.map(bookEntity, BookDTO.class))
        .collect(Collectors.toList());

    return new PageImpl<>(bookDTOList, pageable, bookEntityPage.getTotalElements());

  }


  //개별 조회
  public BookDTO findeOne(Integer bonum) throws Exception {

    Optional<BookEntity> bookEntity = bookRepository.findById(bonum);
    BookDTO bookDTO = modelMapper.map(bookEntity, BookDTO.class);

    return bookDTO;

  }


  //수정 (삽입과 거의 동일. 상단에 기존 파일 삭제 추가할 것)
  public void update(BookDTO bookDTO, MultipartFile imgFile) throws Exception {
    // 수정 할 것 개별조회
    BookEntity bookEntity = bookRepository.findById(bookDTO.getBookId()).orElseThrow();
    String deleteFile = bookEntity.getBookImg();

    if (imgFile != null && !imgFile.isEmpty()) { // 이미지 파일이 전송되었을 때만 처리
      String originalFileName = imgFile.getOriginalFilename(); // 저장 할 파일명
      String newFileName = "";

      // 기존 파일 삭제
      if (deleteFile != null) {
        fileService.deleteFile(itemImgLocation, deleteFile);
      }

      // 새로운 파일 업로드
      newFileName = fileService.uploadFile(
          itemImgLocation,
          originalFileName,
          imgFile.getBytes()
      );
      bookDTO.setBookImg(newFileName); // 새로운 파일명을 재등록
    }

    // 나머지 정보 업데이트
    bookDTO.setBookId(bookEntity.getBookId());

    // 변환
    BookEntity data = modelMapper.map(bookDTO, BookEntity.class);
    bookRepository.save(data); // 저장
  }




  //삭제
  public void delete(Integer bookId) throws Exception {

    BookEntity bookEntity = bookRepository.findById(bookId).orElseThrow();

    fileService.deleteFile(itemImgLocation, bookEntity.getBookImg());

    bookRepository.deleteById(bookId);
  }


}

 

 

🟢 Controller

▪️ 검증처리를 할 때 순서 : @Valid - DTO - BindingResult bindingResult, ~

▪️ MultipartFile imgFile 매개변수로 추가

@Controller
@RequiredArgsConstructor
public class BookController {

  private final BookService bookService;

  //목록 + 검색폼 + 페이지
  @GetMapping("/booklist")
  public String listForm(@RequestParam(value = "type", defaultValue = "") String type,
                         @RequestParam(value = "keyword", defaultValue = "") String keyword,
                         @RequestParam(value = "bookRole", defaultValue = "") String bookRole,
                         @PageableDefault(page=1) Pageable pageable,
                         Model model) throws Exception {

    Page<BookDTO> bookDTOS = bookService.search(type, keyword, bookRole, pageable);

    int blockLimit = 5;
    int startPage = (((int)(Math.ceil((double) pageable.getPageNumber() / blockLimit))) - 1) * blockLimit + 1;
    int endPage = Math.min(startPage + blockLimit - 1, bookDTOS.getTotalPages());

    int prevPage = bookDTOS.getNumber();
    int currentPage = bookDTOS.getNumber() + 1;
    int nextPage = bookDTOS.getNumber() + 2;
    int lastPage = bookDTOS.getTotalPages();

    bookDTOS.getTotalElements();  //전체 게시글 조회

    model.addAttribute("bookDTOS", bookDTOS);

    model.addAttribute("startPage", startPage);
    model.addAttribute("endPage", endPage);
    model.addAttribute("prevPage", prevPage);
    model.addAttribute("currentPage", currentPage);
    model.addAttribute("nextPage", nextPage);
    model.addAttribute("lastPage", lastPage);

    model.addAttribute("type", type);
    model.addAttribute("keyword", keyword);

    return "/book/list";
  }


  //등록폼
  @GetMapping("/bookinsert")
  public String insertForm(Model model) throws Exception {
    BookDTO bookDTO = new BookDTO();
    model.addAttribute("bookDTO", bookDTO);
    model.addAttribute("bookRole", BookRole.values());
    return "/book/insert";
  }
  //등록처리
  @PostMapping("/bookinsert")
  public String insertProc(@Valid BookDTO bookDTO, BindingResult bindingResult, MultipartFile imgFile, Model model) throws Exception {

    if(bindingResult.hasErrors()) {
      model.addAttribute("bookRole", BookRole.values());
      return "/book/insert";
    } bookService.insert(bookDTO, imgFile);

    return "redirect:/booklist";

  }


  //상세
  @GetMapping("/bookdetail")
  public String detailForm(int bookId, Model model) throws Exception {
    BookDTO bookDTO = bookService.findeOne(bookId);
    model.addAttribute("bookDTO", bookDTO);
    return "/book/detail";
  }


  //수정폼
  @GetMapping("/bookupdate")
  public String updateForm(Integer bookId, Model model) throws Exception {

    BookDTO bookDTO = bookService.findeOne(bookId);

    model.addAttribute("bookDTO", bookDTO);
    model.addAttribute("bookRole", BookRole.values());

    return "/book/update";
  }
  //수정처리
  @PostMapping("/bookupdate")
  public String updateProc(@Valid BookDTO bookDTO, BindingResult bindingResult, MultipartFile imgFile, Model model) throws Exception {

    if(bindingResult.hasErrors()) {
      model.addAttribute("bookRole", BookRole.values());
      return "/book/update";
    } bookService.update(bookDTO, imgFile);

    return "redirect:/booklist";
  }


  //삭제
  @GetMapping("/bookdelete")
  public String deleteForm(Integer bookId) throws Exception {
    bookService.delete(bookId);
    return "redirect:/booklist";
  }


}

 

 


 

🟢 list.html

▪️ 총 게시글 수 / 테이블 / 다중 검색영역 / 페이지네이션

<body>
<div layout:fragment="content">
  <div class="row mt-5">
    <div class="col-lg-3"></div>  <!-- 좌측 여백 -->
    <div class="col-lg-6">    <!-- 본문 영역 -->
      <div class="container mt-3">
        <h2>Book List</h2>
        <br>
        <b>* 총 게시글 수 : <span th:text="${bookDTOS.getTotalElements}"></span>건</b>
        <hr>
        <table class="table table-striped mt-3">    <!-- 테이블 시작 -->
          <thead>
          <tr>
            <th>이미지</th>
            <th>도서 제목</th>
            <th>분류 코드</th>
            <th>가격</th>
            <th>저자</th>
            <th>출판사</th>
            <th>발매일</th>
          </tr>
          </thead>
          <tbody>
            <tr th:each="data:${bookDTOS}" style="cursor:pointer" th:onclick="|location.href='@{/bookdetail(bookId=${data.bookId})}'|">
              <input type="hidden" name="bookId" th:value="${bookId}">
              <td><img th:src="|/images/item/@{${data.bookImg}}|" width="100" height="100"></td>
              <td>[[${data.bookTitle}]]</td>
              <td>
                <div th:if="${data.bookRole != null}">
                  <div th:utext="${#strings.replace(data.bookRole.description, '\n', '&#10;')}"></div>
                </div>
              </td>
              <td>[[${data.bookPrice}]]원</td>
              <td>[[${data.bookAuthor}]]</td>
              <td>[[${data.bookCompany}]]</td>
              <td>[[${data.bookDate}]]</td>
            </tr>
          </tbody>
        </table>      <!-- 테이블 끝 -->

        <div class="row">   <!-- 검색 영역 시작 -->
          <div class="col-sm-2"></div>
          <div class="col-sm-8">
            <form th:action="@{/booklist}" method="get">
              <input type="hidden" name="page" value="1">
              <div class="input-group mb-3 mt-5">
                <select class="form-select" name="type" id="searchType">
                  <option value="" th:selected="${type == ''}">== 선택 ==</option>
                  <option value="t" th:selected="${type == 't'}">제목</option>
                  <option value="ti" th:selected="${type == 'ti'}">제목+내용</option>
                  <option value="a" th:selected="${type == 'a'}">저자</option>
                  <option value="ca" th:selected="${type == 'ca'}">카테고리</option>
                </select>

                <!-- 중분류 (검색창) -->
                <input type="text" class="form-control" name="keyword" id="keyword" th:value="${keyword}">

                <!-- 중분류 (카테고리 유형) -->
                <select class="form-select" name="bookRole" id="bookRole">
                  <option value="" th:selected="${bookRole == ''}"> == 선택 == </option>
                  <option value="CULTURE" th:selected="${bookRole == 'CULTURE'}"> 교양 </option>
                  <option value="COMPUTER" th:selected="${bookRole == 'COMPUTER'}"> 컴퓨터 </option>
                  <option value="NOVEL" th:selected="${bookRole == 'NOVEL'}"> 소설 </option>
                  <option value="STUDY" th:selected="${bookRole == 'STUDY'}"> 학습 </option>
                  <option value="ETC" th:selected="${bookRole == 'ETC'}"> 기타 </option>
                </select>

                <button type="submit" class="btn btn-primary" name="searchButton">검색</button>

                <button type="reset" class="btn btn-primary" name="resetButton" th:onclick="|location.href='@{/booklist}'|">다시</button>

              </div>
            </form>

            <!-- JavaScript 코드 -->
            <script>

                // resetSearchForm : 검색 유형 드롭다운이 변경될 때 호출되며, 선택한 검색 유형에 따라 스타일을 업데이트
                document.getElementById('searchType').addEventListener('change', function () {
                    resetSearchForm();
                });

                function resetSearchForm() {
                    // 검색 폼 값 초기화
                    document.getElementById('keyword').value = '';
                    document.getElementById('bookRole').value = '';

                    // localStorage에서 이전 선택 값을 제거
                    localStorage.removeItem('selectedBookRole');

                    // applyStylesAfterSearch : 선택한 검색 유형 및 카테고리 유형에 따라 스타일을 적용
                    applyStylesAfterSearch();
                }

                function applyStylesAfterSearch() {
                    var selectedSearchType = document.getElementById('searchType').value;

                    //toggleElementDisplay : 조건에 따라 요소의 표시 여부를 전환
                    //'ca'가 선택되면 카테고리 유형을 선택하는 드롭다운 출력
                    toggleElementDisplay('bookRole', selectedSearchType == 'ca');

                    //'t', 'ti', 'a'이 선택되면 상품명을 입력하는 텍스트 필드 출력
                    toggleElementDisplay('keyword', selectedSearchType == 't' || selectedSearchType == 'ti' || selectedSearchType == 'a');

                    //localStorage : 검색 값 유지
                    if (selectedSearchType == 'ca') {
                        //초기화
                        var bookRoleElement = document.getElementById('bookRole');

                        if (bookRoleElement.value == '') {
                            // localStorage에서 이전 선택 값을 가져옴
                            bookRoleElement.value = localStorage.getItem('selectedBookRole') || '';
                        }
                    }
                }


                //검색창 또는 선택창 출력 (elementId에 해당하는 요소의 표시 여부를 condition에 따라 조절)
                function toggleElementDisplay(elementId, condition) {
                    var element = document.getElementById(elementId);
                    element.style.display = condition ? 'block' : 'none';
                }

                // 페이지 로드 시 초기 스타일 적용
                applyStylesAfterSearch();

                // 페이지 로드 시 localStorage에서 이전 선택 값을 가져와 적용
                window.onload = function () {
                    applyStylesAfterSearch();
                }

                // 페이지 언로드 시 localStorage에 현재 선택 값을 저장
                window.onbeforeunload = function () {
                    localStorage.setItem('selectedBookRole', document.getElementById('bookRole').value);
                }

            </script>
          </div>
          <div class="col-sm-2"></div>
        </div>    <!-- 검색 영역 끝 -->

        <!-- 페이지 영역 -->
        <div class="d-flex justify-content-center" th:if="${lastPage > 1}">
          <ul class="pagination">
            <li class="page-item" th:unless="${startPage==1}">
              <a class="page-link" th:href="@{/booklist(type=${type}, keyword=${keyword}, page=1)}">처음</a>
            </li>

            <li class="page-item" th:unless="${currentPage==1}">
              <a class="page-link" th:href="@{/booklist(type=${type}, keyword=${keyword}, page=${prevPage})}">이전</a>
            </li>

            <span th:each="page:${#numbers.sequence(startPage, endPage)}">
            <li class="page-item" th:unless="${page == currentPage}">
              <!-- 다른 페이지 -->
              <a class="page-link" th:href="@{/booklist(type=${type}, keyword=${keyword}, page=${page})}">[[${page}]]</a>
            </li>
            <li class="page-item active" th:if="${page == currentPage}">
              <!-- 활성화(현재 위치) -->
              <a class="page-link" href="#">[[${page}]]</a>
            </li>
          </span>

            <li class="page-item" th:unless="${currentPage==lastPage}">
              <a class="page-link" th:href="@{/booklist(type=${type}, keyword=${keyword}, page=${nextPage})}">다음</a>
            </li>

            <li class="page-item" th:unless="${endPage==lastPage}">
              <a class="page-link" th:href="@{/booklist(type=${type}, keyword=${keyword}, page=${lastPage})}">끝</a>
            </li>
          </ul>
        </div>
        <!-- 페이지 영역 끝 -->

      </div>
    </div>
    <div class="col-lg-3"></div>  <!-- 우측 여백 -->
  </div>
</div>
</body>

 

 

🟢 insert.html

<body>
  <div layout:fragment="content">
    <div class="row mt-5">
      <div class="col-lg-3"></div>  <!-- 좌측 여백 -->
      <div class="col-lg-6">    <!-- 본문 영역 -->
        <h4 class="card-title">도서 등록 : </h4>
        <p class="card-text">도서 정보를 등록해주세요.</p>
        <form th:action="@{/bookinsert}" method="post" enctype="multipart/form-data" th:object="${bookDTO}">
          <hr>
          <div class="row">
            <div class="col-sm-2">
              도서 제목 :
            </div>
            <div class="col-sm-10">
              <input type="text" class="form-control" name="bookTitle">
              <p class="text-danger" th:if="${#fields.hasErrors('bookTitle')}" th:errors="*{bookTitle}"></p>
            </div>
          </div>

          <br>
          <div class="row">
            <div class="col-sm-2">
              도서정보 :
            </div>
            <div class="col-sm-10">
              <textarea class="form-control" rows="5" id="bookInfo" name="bookInfo"></textarea>
              <p class="text-danger" th:if="${#fields.hasErrors('bookInfo')}" th:errors="*{bookInfo}"></p>
            </div>
          </div>

          <br>
          <div class="row">
            <div class="col-sm-2">
              분류코드 :
            </div>
            <div class="col-sm-10">
              <select class="form-control" id="bookRole" name="bookRole" required>
                <option th:each="type:${bookRole}"
                        th:unless="${type.name() == 'ALL'}"
                        th:value="${type.name()}"
                        th:text="${type.getDescription()}"
                        th:selected="${type.name() eq data?.bookRole?.name()}">
                </option>
              </select>
            </div>
          </div>

          <br>
          <div class="row">
            <div class="col-sm-2">
              저자 :
            </div>
            <div class="col-sm-10">
              <input type="text" class="form-control" name="bookAuthor">
            </div>
          </div>

          <br>
          <div class="row">
            <div class="col-sm-2">
              출판사 :
            </div>
            <div class="col-sm-10">
              <input type="text" class="form-control" name="bookCompany">
            </div>
          </div>

          <br>
          <div class="row">
            <div class="col-sm-2">
              발매일 :
            </div>
            <div class="col-sm-10">
              <input type="date" class="form-control" name="bookDate">
            </div>
          </div>

          <br>
          <div class="row">
            <div class="col-sm-2">
              가격 :
            </div>
            <div class="col-sm-10">
              <input type="number" class="form-control" name="bookPrice">
              <p class="text-danger" th:if="${#fields.hasErrors('bookPrice')}" th:errors="*{bookPrice}"></p>
            </div>
          </div>

          <br>
          <div class="row">
            <div class="col-sm-2">
              이미지:
            </div>
            <div class="col-sm-10">
              <input type="file" class="form-control" name="imgFile" id="imgFile" onchange="previewImage()">
              <br>
            </div>
          </div>
          <br>
          <hr>
          <!-- 입력 칸 끝 -->
          <!--  버튼 시작 -->
          <div class="container mb-3 mt-3">
            <button type="submit" class="btn btn-primary">등록</button>
            <button type="reset" class="btn btn-secondary">다시</button>
            <button type="button" class="btn btn-secondary" onclick="location.href='/booklist'">목록</button>
          </div>
          <!--  버튼 끝 -->
        </form>
        </div>
      </div>    <!-- 본문 영역 끝 -->
    <div class="col-lg-3"></div>  <!-- 우측 여백 -->
  </div>
</body>

 

 

🟢 detail.html

<body>
<div layout:fragment="content">
    <div class="row mt-5">
      <div class="col-lg-3"></div>  <!-- 좌측 여백 -->
      <div class="col-lg-6">    <!-- 본문 영역 -->
        <div class="container mt-3">
          <div class="card" style="width:700px">
            <div class="card-body">
              <br>
              <h2>도서 상세 정보</h2>
              <br>
              <table class="table">
                <thead>
                <input type="hidden" name="bookId" th:value="${bookDTO.bookId}">
                <tr>
                  <span class="input-group-text" id="image">
                    <img th:src="|/images/item/@{${bookDTO.bookImg}}|" id="bookImg" width="300" height="500">
                  </span>
                </tr>
                <tr>
                  <th>도서제목</th>
                  <td>[[${bookDTO.bookTitle}]]</td>
                </tr>
                <tr>
                  <th>도서정보</th>
                  <td>[[${bookDTO.bookInfo}]]</td>
                </tr>
                <tr>
                  <th>분류코드</th>
                  <td>[[${bookDTO.bookRole.getDescription()}]]</td>
                </tr>
                <tr>
                  <th>저자</th>
                  <td>[[${bookDTO.bookAuthor}]]</td>
                </tr>
                <tr>
                  <th>가격</th>
                  <td>[[${bookDTO.bookPrice}]]원</td>
                </tr>
                <tr>
                  <th>출판사</th>
                  <td>[[${bookDTO.bookCompany}]]</td>
                </tr>
                <tr>
                  <th>발매일</th>
                  <td>[[${bookDTO.bookDate}]]</td>
                </tr>
                <tr>
                  <th>등록일</th>
                  <td th:text="${#temporals.format(bookDTO.credate, 'yyyy-MM-dd')}"></td>
                </tr>
                <tr>
                  <th>수정일</th>
                  <td th:text="${#temporals.format(bookDTO.modidate, 'yyyy-MM-dd')}">수정일</td>
                </tr>
                </thead>
              </table>
            </div>
          </div>
          <!--  버튼 시작 -->
          <div class="container mb-3 mt-3">
            <button type="button" class="btn btn-primary" onclick="location.href='/booklist'">목록</button>
            <button type="button" class="btn btn-primary" th:onclick="|location.href='@{/bookupdate(bookId=${bookDTO.bookId})}'|">수정</button>
            <button type="button" class="btn btn-danger" th:onclick="|location.href='@{/bookdelete(bookId=${bookDTO.bookId})}'|">삭제</button>
          </div>
          <!--  버튼 끝 -->
        </div>
      </div>
      <div class="col-lg-3"></div>  <!-- 우측 여백 -->
    </div>
  </div>
</div>
</body>

 

 

🟢 update.html

<body>
  <div layout:fragment="content">
    <div class="row mt-5">
      <div class="col-lg-3"></div>  <!-- 좌측 여백 -->
      <div class="col-lg-6">    <!-- 본문 영역 -->
        <h4 class="card-title">도서 수정 : </h4>
        <p class="card-text">도서 정보를 수정해주세요.</p>
        <hr>
        <form th:action="@{/bookupdate}" method="post" enctype="multipart/form-data" th:object="${bookDTO}">

          <!-- hidden : 도서 번호, 등록일, 수정일 -->
          <input type="hidden" name="bookId" th:field="*{bookId}" >
          <input type="hidden" name="credate" th:field="*{credate}" >
          <input type="hidden" name="modidate" th:field="*{modidate}">
          <input type="hidden" name="bookImg" th:field="*{bookImg}">

          <!-- 입력 칸 시작 -->
          <div class="row">
            <div class="col-sm-2">
              이미지 :
            </div>
            <div class="col-sm-10">
              <img th:src="|/images/item/@{${bookDTO.bookImg}}|" width="200" height="300"> <br>
              <input type="file" class="form-control" id="bookImg" name="imgFile">
            </div>
          </div>

          <br>
          <div class="row">
            <div class="col-sm-2">
              도서 제목 :
            </div>
            <div class="col-sm-10">
              <input type="text" class="form-control" name="bookTitle" th:field="*{bookTitle}">
              <p class="text-danger" th:if="${#fields.hasErrors('bookTitle')}" th:errors="*{bookTitle}"></p>  <!-- 검증 문구 -->
            </div>
          </div>

          <br>
          <div class="row">
            <div class="col-sm-2">
              도서정보 :
            </div>
            <div class="col-sm-10">
              <textarea class="form-control" rows="5" id="bookInfo" name="bookInfo" th:field="*{bookInfo}"></textarea>
              <p class="text-danger" th:if="${#fields.hasErrors('bookInfo')}" th:errors="*{bookInfo}"></p>  <!-- 검증 문구 -->
            </div>
          </div>

          <br>
          <div class="row">
            <div class="col-sm-2">
              분류코드 :
            </div>
            <div class="col-sm-10">
              <select class="form-control" id="bookRole" name="bookRole" required>
                <option th:each="type:${bookRole}"
                        th:unless="${type.name() == 'ALL'}"
                        th:value="${type.name()}"
                        th:text="${type.getDescription()}"
                        th:selected="${type.name() eq data?.bookRole?.name()}">
                </option>
              </select>
            </div>
          </div>
          <br>
          <div class="row">
            <div class="col-sm-2">
              저자 :
            </div>
            <div class="col-sm-10">
              <input type="text" class="form-control" th:field="*{bookAuthor}" name="bookAuthor">
            </div>
          </div>

          <br>
          <div class="row">
            <div class="col-sm-2">
              가격 :
            </div>
            <div class="col-sm-10">
              <input type="number" class="form-control" th:field="*{bookPrice}" name="bookPrice">
              <p class="text-danger" th:if="${#fields.hasErrors('bookPrice')}" th:errors="*{bookPrice}"></p>  <!-- 검증 문구 -->
            </div>
          </div>

          <br>
          <div class="row">
            <div class="col-sm-2">
              출판사 :
            </div>
            <div class="col-sm-10">
              <input type="text" class="form-control" th:field="*{bookCompany}" name="bookCompany">
              <p class="text-danger" th:if="${#fields.hasErrors('bookCompany')}" th:errors="*{bookCompany}"></p>  <!-- 검증 문구 -->
            </div>
          </div>

          <br>
          <div class="row">
            <div class="col-sm-2">
              발매일 :
            </div>
            <div class="col-sm-10">
              <input type="date" class="form-control" th:field="*{bookDate}" name="bookDate">
            </div>
          </div>
          <hr>
          <!-- 입력 칸 끝 -->
          <!--  버튼 시작 -->
          <div class="container mb-3 mt-3">
            <button type="submit" class="btn btn-primary">수정</button>
            <button type="reset" class="btn btn-primary">다시</button>
            <button type="button" class="btn btn-secondary" onclick="location.href='/booklist'">목록</button>
          </div>
          <!--  버튼 끝 -->
        </form>
      </div>
    </div>    <!-- 본문 영역 끝 -->
      <div class="col-lg-3"></div>  <!-- 우측 여백 -->
    </div>
  </div>
</body>

 

반응형