일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 검증 애노테이션
- 불변 객체
- QueryDSL
- 값 타입 컬렉션
- 임베디드 타입
- JPA 활용 2
- 로그인
- JPQL
- 벌크 연산
- JPA
- 스프링MVC
- 스프링
- 김영한
- 실무활용
- 타임리프
- Bean Validation
- 페이징
- 예제 도메인 모델
- 트위터
- 컬렉션 조회 최적화
- 일론머스크
- API 개발 고급
- jpa 활용
- 스프링 mvc
- 기본문법
- Spring Data JPA
- 타임리프 문법
- 스프링 데이터 JPA
- JPA 활용2
- 프로젝트 환경설정
- Today
- Total
RE-Heat 개발자 일지
[JPA 활용1] [5] 웹 계층 개발(상편) 본문
인프런 김영한 님의 강의를 듣고 작성한 글입니다.
[1] 홈 화면과 레이아웃
■ 홈 컨트롤러 등록
HomeController
@Controller
@Slf4j
public class HomeController {
@RequestMapping("/")
public String home() {
log.info("home controller");
return "home";
}
}
- resources/templates/home.html로 연결
home.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header">
<title>Hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<div class="jumbotron">
<h1>HELLO SHOP</h1>
<p class="lead">회원 기능</p>
<p>
<a class="btn btn-lg btn-secondary" href="/members/new">회원 가입</a>
<a class="btn btn-lg btn-secondary" href="/members">회원 목록</a>
</p>
<p class="lead">상품 기능</p>
<p>
<a class="btn btn-lg btn-dark" href="/items/new">상품 등록</a>
<a class="btn btn-lg btn-dark" href="/items">상품 목록</a>
</p>
<p class="lead">주문 기능</p>
<p>
<a class="btn btn-lg btn-info" href="/order">상품 주문</a>
<a class="btn btn-lg btn-info" href="/orders">주문 내역</a>
</p>
</div>
<div th:replace="fragments/footer :: footer"/>
</div> <!-- /container --></body>
</html>
- th:replace로 템플릿 조각을 가져옴
- 스프링 MVC 2편 - [1] 타임리프 - 기본기능(하편) [16] 템플릿 조각 참조
fragments/header.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="header">
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous">
<!-- Custom styles for this template -->
<link href="/css/jumbotron-narrow.css" rel="stylesheet">
<title>Hello, world!</title>
</head>
fragments/bodyHeader.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<div class="header" th:fragment="bodyHeader">
<ul class="nav nav-pills pull-right">
<li><a href="/">Home</a></li>
</ul>
<a href="/"><h3 class="text-muted">HELLO SHOP</h3></a>
</div>
fragments/footer.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<div class="footer" th:fragment="footer">
<p>© Hello Shop V2</p>
</div>
참고]
1. 부트스트랩 4.31 버전을 다운로드하고 css·js를 템플릿에 추가
- jumbotron은 부트스트랩 5버전 이후부턴 사용 X
2. 부트스트랩이 적용이 안 되면 resources를 우클릭 후 Reload From Disk를 해주면 해결된다.
3. jumbotron을 적용하려면 resources/static/css/jumbotron-narrow.css를 추가해줘야 한다.
참고] 뷰템플릿 변경사항을 서버 재시작 없이 즉시 반영하기
1. spring-boot-devtools 추가
2. html 파일 build-> Recompile
■ 결과화면
[2] 회원 등록
MemberForm
@Getter @Setter
public class MemberForm {
@NotEmpty(message = "회원 이름은 필수입니다.")
private String name;
private String city;
private String street;
private String zipcode;
}
- 실무에선 엔티티는 핵심 비즈니스 로직만 가지게 하고, 화면에 맞는 폼객체나 DTO를 따로 만든다. 그 이유는 엔티티가 화면에 종속적으로 변하는 것을 피하기 위해서다.
- Bean Validation 등록 필요
- @NotEmpty로 회원 이름을 필수 입력하도록 만듦.
참고] BeanValidation 관련 내용은
스프링 MVC 2편 - [5] 검증2- Bean Validation(상편)
스프링 MVC 2편 - [5] 검증2- Bean Validation(하편) 확인
MemberController
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@GetMapping("/members/new")
public String createForm(Model model) {
model.addAttribute("memberForm", new MemberForm());//validation 용
return "members/createMemberForm";
}
@PostMapping("members/new")
public String create(@Valid MemberForm form, BindingResult result){
if (result.hasErrors()){
return "members/createMemberForm";
}
Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode());
Member member = new Member();
member.setName(form.getName());
member.setAddress(address);
memberService.join(member);
return "redirect:/";
}
}
① GET /members/new
- model에 validation용 MemberForm의 빈 객체를 담아 넘긴다.
- 참고로 타임리프에서 form태그에 작성하는 th:object는 Model 객체에서 넘어온 객체의 필드값을 참조하게 한다.
② POST /members/new
- @Valid는 @NotEmpty 같은 애노테이션을 확인 후 검증을 수행한다. 검증 오류가 발생하면 Error를 생성해 BindingResult에 담아준다. 그리고 그 값을 createMemberForm.html로 넘긴다.
- 모든 작업이 완료되면 홈 화면으로 redirect한다.
members/createMemberForm.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header"/>
<style>
.fieldError {
border-color: #bd2130;
}
</style>
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<form role="form" action="/members/new" th:object="${memberForm}"
method="post">
<div class="form-group">
<label th:for="name">이름</label>
<input type="text" th:field="*{name}" class="form-control"
placeholder="이름을 입력하세요"
th:class="${#fields.hasErrors('name')}? 'form-control fieldError' : 'form-control'">
<p th:if="${#fields.hasErrors('name')}"
th:errors="*{name}">Incorrect date</p>
</div>
<div class="form-group">
<label th:for="city">도시</label>
<input type="text" th:field="*{city}" class="form-control" placeholder="도시를 입력하세요">
</div>
<div class="form-group">
<label th:for="street">거리</label>
<input type="text" th:field="*{street}" class="form-control"
placeholder="거리를 입력하세요">
</div>
<div class="form-group">
<label th:for="zipcode">우편번호</label>
<input type="text" th:field="*{zipcode}" class="form-control"
placeholder="우편번호를 입력하세요">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<br/>
<div th:replace="fragments/footer :: footer"/>
</div> <!-- /container -->
</body>
</html>
① 회원 이름을 빼먹으면 @NotEmpty의 message에 담은 "회원 이름은 필수입니다"라는 에러 메시지가 넘어가게 세팅
② th:filed *{name}처럼 *이 붙으면 th:object를 참조한다.
■ 에러 발생 화면
[3] 회원 목록 조회
MemberController
@GetMapping("/members")
public String list(Model model){
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
- member를 전체 조회한 뒤, model에 담아 넘김
memberlist.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header"/>
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<div>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>이름</th>
<th>도시</th>
<th>주소</th>
<th>우편번호</th>
</tr>
</thead>
<tbody>
<tr th:each="member : ${members}">
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
<td th:text="${member.address?.city}"></td>
<td th:text="${member.address?.street}"></td>
<td th:text="${member.address?.zipcode}"></td>
</tr>
</tbody>
</table>
</div>
<div th:replace="fragments/footer :: footer"/>
</div> <!-- /container -->
</body>
</html>
- th:each로 members 객체에서 member를 하나씩 꺼내온 후 id, name, city, street, zipcode를 출력
실행화면
■ 폼 객체 vs 엔티티 직접 사용앞서 언급했듯 요구사항이 정말 단순하면 폼 객체(MemberForm) 없이 엔티티를 직접 써도 된다.(List<Member>)하지만 화면 요구사항이 복잡해지면 엔티티에 화면을 처리하는 기능이 점점 증가해 결국 유지보수하기 어렵게 된다. 따라서 실무에선 엔티티는 핵심 비즈니스 로직만 가지고 있고 화면을 위한 로직을 가지게 해선 안 된다.
다시 말해 화면이나 API에 맞는 폼 객체나 DTO를 쓰는 게 바람직하다.
[4] 상품 등록
ItemController - 일부 발췌
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping("/items/new")
public String createForm(Model model) {
model.addAttribute("form", new BookForm());
return "items/createItemForm";
}
@PostMapping("/items/new")
public String create(BookForm form) {
Book book = new Book();
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setStockQuantity(form.getStockQuantity());
book.setAuthor(form.getAuthor());
book.setIsbn(form.getIsbn());
itemService.saveItem(book);
return "redirect:/items";
}
- 어설프게 엔티티를 생성하지 말고, creatBook() 같은 생성자 메서드를 써서 넘기는 게 바람직하나 예제라 setter 사용
createItemForm.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header"/>
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<form th:action="@{/items/new}" th:object="${form}" method="post">
<div class="form-group">
<label th:for="name">상품명</label>
<input type="text" th:field="*{name}" class="form-control"
placeholder="이름을 입력하세요">
</div>
<div class="form-group">
<label th:for="price">가격</label>
<input type="number" th:field="*{price}" class="form-control"
placeholder="가격을 입력하세요">
</div>
<div class="form-group">
<label th:for="stockQuantity">수량</label>
<input type="number" th:field="*{stockQuantity}" class="form-
control" placeholder="수량을 입력하세요">
</div>
<div class="form-group"><label th:for="author">저자</label>
<input type="text" th:field="*{author}" class="form-control"
placeholder="저자를 입력하세요">
</div>
<div class="form-group">
<label th:for="isbn">ISBN</label>
<input type="text" th:field="*{isbn}" class="form-control"
placeholder="ISBN을 입력하세요">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<br/>
<div th:replace="fragments/footer :: footer"/>
</div> <!-- /container -->
</body>
</html>
실행화면 & DB 확인
[5] 상품 목록
ItemController
@GetMapping("/items")
public String itemList(Model model) {
List<Item> items = itemService.findItems();
model.addAttribute("items", items);
return "items/itemList";
}
- 상품 목록을 조회 후 model에 담아 itemList.html로 넘김
itemList.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header"/>
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<div>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>상품명</th>
<th>가격</th>
<th>재고수량</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${items}">
<td th:text="${item.id}"></td>
<td th:text="${item.name}"></td>
<td th:text="${item.price}"></td>
<td th:text="${item.stockQuantity}"></td>
<td>
<a href="#" th:href="@{/items/{id}/edit (id=${item.id})}"
class="btn btn-primary" role="button">수정</a>
</td>
</tr>
</tbody>
</table>
</div>
<div th:replace="fragments/footer :: footer"/>
</div> <!-- /container -->
</body>
</html>
실행화면
'백엔드 > JPA' 카테고리의 다른 글
[JPA 활용2] [1] API 개발 기본 (0) | 2023.08.30 |
---|---|
[JPA 활용1] [5] 웹 계층 개발(하편) (0) | 2023.08.27 |
[JPA 활용1] [4] 상품·주문 도메인 개발 (0) | 2023.08.25 |
[JPA 활용1] [3] 앱 구현 준비 및 회원 도메인 개발 (0) | 2023.08.24 |
[JPA 활용1] [2] 도메인 분석 설계 (0) | 2023.08.20 |