회원 등록 부분의 컨트롤러, 문서를 개발할 것이다.
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@GetMapping("/members/new")
public String createForm(Model model){
model.addAttribute("memberForm", new MemberForm());
return "members/createMemberForm";
}
}
보통 컨트롤러에서 service를 쓰니 일단 주입받는다. (@RequiredArgsCons~~ 저걸로 final 인자로 있는 생성자 생성해주고, 그럼 알아서 빈에서 주입됨.)
/members/new 라는 url로 왔을 때,
템플릿 중
members/createMeberForm.html을 찾아서 거기다 렌더링 해 준다.
그런데 모델에 memberForm이라는 것에 값을 MemberForm()
@Getter @Setter
public class MemberForm {
@NotEmpty(message = "회원 이름은 필수 입니다")
private String name;
private String city;
private String street;
private String zipcode;
}
이렇게 name만 비어있으면 안되는 Form용 빈 객체를 생성해서 넣어준다.
이렇게 빈 객체로 넣는 이유가 아마 저
createMemberForm.html이 값을 읽게 하려는 속셈 같다.
왜냐하면 뭔가 validation 하는 데, 조건에 불만족하는 값을 하나 넣었다고 모든 걸 초기화 시킬 수도 있으니,
그것 빼고 담아서 저기에 다시 보내주려는 속셈 같다.
여튼 모델에 넘겼으니, 저 모델은 디스패처서블릿(이 컨트롤러를 호출한 컨트롤러)에서 넣어주는 거므로, 저 Model의 주소는 DispatcherServlet이 가지고 있기에 랜더링 할 때 같이 뷰에다가 넘겨준다. 그래서 뷰도 읽을 수 있다.
<!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-controlfieldError' : '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>
이렇게 모델을 통해 "memberForm"이라는 이름으로 넘겨받은 값, 이번 경우 객체를 사용할 수 있다. (GetMapping 에서는 그냥 빈 객체이므로 memberForm 내부의 필드값 자체는 다 비어있다.)
${모델에있는값 즉 키} 이렇게 사용할 수 있고,
저 *{변수명}
이거는 저렇게 th:object = ${모델로부터넘겨받은것의키} 이렇게 오브젝트 지정해 주면 저 오브젝트를 계속 사용하겠다는 것이다.
그래서 저렇게 * 하면 저 오브젝트의 변수를 가리키는 것이다.
th:field는 저거 input 태그의 id, name을 써주는 거다. 변수명으로 맞춰주는 듯. id랑 name은 관례상 같음.
저 form 보면 /members/new 로 가게끔 한다.
action해서 가는 게 /members/new인데, 보면 method = post로 되어 있다.
즉, PostMapping("members/new") 를 만들고, 거기에 인증해서 조건에 만족하지 않으면 다시 /members/new 로 보내는 데 그 때는 사용자가 입력한 값들을 MemberForm에 담고 그걸 넘겨서 다시 입력해야 하지 않게끔 하려는 의도 같다.
만약 validation을 통과하면 저 받은 MemberForm을 가지고 DB에 등록 후, redirect 하겠지.
@PostMapping("/members/new")
public String joinForm(@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:/";
}
이렇게 PostMapping 했다.
MemberForm으로 받아오고, 저기서 @Vaild 할 시 직역 그대로 검증인데, 오류가 나면 BindingResult 말 그대로 바인딩에 대한 결과인데, 검증오류 시 저 result에 담긴다.
뭘 검증하냐,
@Getter @Setter
public class MemberForm {
@NotEmpty(message = "회원 이름은 필수 입니다")
private String name;
private String city;
private String street;
private String zipcode;
}
여기 보면 @NotEmpty 애노테이션이 있다.
jakarta(javax)의 기능인데 말 그대로 비어있으면 안된다는 거다.
뭐 많다.
여튼 여기서는
@Getter @Setter
public class MemberForm {
@NotEmpty(message = "회원 이름은 필수 입니다")
private String name;
private String city;
private String street;
private String zipcode;
}
이름이 비어있으면 안된다.
비어 있을 시,
@PostMapping("/members/new")
public String joinForm(@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:/";
}
result에 에러가 들어가고,
저렇게 에러 있는지 없는 지 검사해서 들어갈 수 있다.
MemberForm 저렇게 인자로 받는 건 Model에 넣어준다. 저게 @ModelAttribute 생략됐던 거였나?
여튼 그래서 저렇게 BindingResult를 가지면 코드를 그대로 진행 해 주는데,
타임리프에서,
<input type="text" th:field="*{name}" class="form-control"
placeholder="이름을 입력하세요"
th:class="${#fields.hasErrors('name')}? 'form-controlfieldError' : 'form-control'">
<p th:if="${#fields.hasErrors('name')}"
th:errors="*{name}">Incorrect date</p>
이 부분.
th:class는 타임리프 클래스,
# 샾 붙은 게 지정된 변수? 같은 것 들인데,
그 중 fields가 BindingResult인 듯.
거기에 'name'이라는 에러 있으면, 이거는 아마 기본값은 필드명으로 들어가는 듯.
저거는 삼항연산자로, th:class니까 클래스 지정
그리고 마찬가지로 th:if만약 저 조건이 참이면,
저 p를 보이는 듯. 거짓이면 안 보이고.
근데 보이는 데, th:errors 이거는 에러내용을 뽑아오는 건데, 저 *은 아까 말했듯이
<form role="form" action="/members/new" th:object="${memberForm}"
저 object 저거임.
그 에러 내용은 어디서 뽑아온거냐,
@Getter @Setter
public class MemberForm {
@NotEmpty(message = "회원 이름은 필수 입니다")
private String name;
private String city;
private String street;
private String zipcode;
}
여기 message.
보통 이렇게 폼은 따로 클래스를 만듦.
엔티티에 그대로 받아오는 건 스펙상 안 맞는 경우가 많음. DB에 더 연관되어 있기 때문에. 그냥 Form용 클래스 따로 만들어서 필요한 것만 뽑아서 엔티티에 맞추는 게 훨씬 좋음.
'스프링데이터 + JPA > 웹 애플리케이션 개발' 카테고리의 다른 글
23. 상품 등록 (0) | 2023.11.10 |
---|---|
22. 회원 목록 조회 (0) | 2023.11.10 |
20. 웹 계층 개발 (0) | 2023.11.09 |
19. 주문 검색 기능 개발 (0) | 2023.11.09 |
18. 주문 기능 테스트 (0) | 2023.11.08 |