[Java+SpringBoot+JPA] 이미지 삽입을 활용한 게시판 만들기
2024. 1. 9. 22:04ㆍPratice/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', ' ')}"></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>
반응형
'Pratice > CRUD' 카테고리의 다른 글
[Java+SpringBoot+JPA] 테이블 조인을 활용한 댓글 기능+좋아요/싫어요(2) (1) | 2024.01.11 |
---|---|
[Java+SpringBoot+JPA] 테이블 조인을 활용한 댓글 기능(1) (1) | 2024.01.11 |
[Java+SpringBoot+JPA] To do List 게시판 만들기 (시작일/마감일 설정) (1) | 2024.01.09 |
[Java+SpringBoot+JPA] 기본 CRUD 구현하기 (6) 조회수 증가 기능 (0) | 2024.01.02 |
[Java+SpringBoot+JPA] 기본 CRUD 구현하기 (5) 검색 & 페이지 기능 (0) | 2023.12.24 |