<strong sec:authentication="name"></strong>
session.setAttribute("userName", userName);
UserDetails ud = new User(dto.getUserName(), dto.getPassword(), authList);
th:block : 묶을 수 있는 Thymeleaf 블럭<th:block th:fragment="bootstrap">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js" integrity="sha384-ndDqU0Gzau9qJ1lfW4pNLlhNTkCfHzAVBReH9diLvGRem5+R9g2FzA8ZGN954O5Q" crossorigin="anonymous"></script>
</th:block>
<th:block th:insert="/include/resource :: bootstrap"></th:block>
index 전달<th:block th:insert="/include/navbar :: nav('index')"></th:block>
<nav th:fragment="nav(thisPage)" class="navbar navbar-expand-md bg-primary-subtle">
<div class="container">
<a class="navbar-brand" th:href="@{/}">Acorn</a>
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link"
th:classappend="${thisPage == 'member' ? 'active' : null}"
th:href="@{/member/list}">Member</a>
</li>
<li class="nav-item">
<a class="nav-link"
th:classappend="${thisPage == 'book' ? 'active' : null}"
th:href="@{/book/list}">Book</a>
</li>
<li class="nav-item">
<a class="nav-link"
th:classappend="${thisPage == 'board' ? 'active' : null}"
th:href="@{/board/list}">Board</a>
</li>
<li class="nav-item">
<a class="nav-link"
th:classappend="${thisPage == 'gallery' ? 'active' : null}"
th:href="@{/gallery/list}">Gallery</a>
</li>
</ul>
</div>
</div>
</nav>
<ul class="navbar-nav">
</ul>
<th:block sec:authorize="isAuthenticated">
<li class="nav-item me-2">
<a class="nav-link p-0" th:href="@{/user/info}">
<strong sec:authentication="name"></strong>
</a>
</li>
<li class="nav-item me-2">
<span class="navbar-text">Signed in</span>
</li>
<li class="nav-item">
<a class="btn btn-danger btn-sm"
th:href="@{/user/logout}">로그아웃</a>
</li>
</th:block>
<th:block sec:authorize="!isAuthenticated">
<li class="nav-item">
<a class="btn btn-outline-light btn-sm me-2"
th:href="@{/user/loginform}">로그인</a>
</li>
<li class="nav-item">
<a class="btn btn-warning btn-sm"
th:href="@{/user/signup-form}">회원가입</a>
</li>
</th:block>
userInfo 메소드 생성@GetMapping("/user/info")
public String userInfo() {
// 로그인된 userName
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
// 서비스 객체를 이용해서 사용자 정보를 얻어와서
// Model 객체에 담은 다음
// Thyemleaf 템플릿 페이지에서 응답
return "user/info";
}
getUserByName 메소드 추가public UserDto getUser(String userName);
@Override
public UserDto getUser(String userName) {
return dao.getByUserName(userName);
}
userInfo 메소드 코드 완성@GetMapping("/user/info")
public String userInfo(Model model) {
// 로그인된 userName
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
// 서비스 객체를 이용해서 사용자 정보를 얻어와서
UserDto dto = service.getUser(userName);
// Model 객체에 담은 다음
model.addAttribute("dto", dto);
// Thyemleaf 템플릿 페이지에서 응답
return "user/info";
}
<div class="container">
<h1>회원 가입 정보</h1>
<table class="table table-bordered table-striped">
<tr>
<th>아이디</th>
<td th:text="${dto.userName}"></td>
</tr>
<tr>
<th>비밀번호</th>
<td>
<a th:href="@{/user/edit-password}">수정하기</a>
</td>
</tr>
<tr>
<th>이메일</th>
<td th:text="${dto.email}"></td>
</tr>
<tr>
<th>프로필 이미지</th>
<td>
<i th:if="${dto.profileImage == null}" style="font-size:100px;" class="bi bi-person-circle"></i>
<img th:unless="${dto.profileImage == null}"
th:src="@{/upload/{name}(name=${dto.profileImage})}"
style="width:100px;height:100px;border-radius:50%;"/>
</td>
</tr>
<tr>
<th>최종 수정 날짜</th>
<td th:text="${dto.updatedAt}"></td>
</tr>
<tr>
<th>가입 날짜</th>
<td th:text="${dto.createdAt}"></td>
</tr>
</table>
<a th:href="@{/user/edit}">개인 정보 수정(이메일, 프로필 사진)</a>
</div>
@{/upload/aaa.jpg}
@{/upload/bbb.jpg}
@{/upload/ccc.jpg}
@{/upload/{경로변수명}(변수명=넣을값)}
@{/upload/{name}(name=${dto.profileImage})}
or
@{'/upload/' + ${dto.profileImage}}
# file save location
file.location=C:/playground/upload
@PropertySource(value="classpath:custom.properties")
@Controller
public class ImageController {
@Value("${file.location}")
private String fileLocation;
@GetMapping("/upload/{saveFileName}")
public ResponseEntity<InputStreamResource> image(@PathVariable("saveFileName") String name) throws IOException{
String filePath = fileLocation + File.separator + name;
File file = new File(filePath);
if(!file.exists()) {
throw new RuntimeException("file not found!");
}
String mimeType = Files.probeContentType(file.toPath());
InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(mimeType))
.contentLength(file.length())
.body(isr);
}
}
<div class="container">
<h1>비밀번호 수정 양식</h1>
<form th:action="@{/user/update-password}" method="post" id="editForm">
<div class="mb-2">
<label class="form-label" for="password">기존 비밀번호</label>
<input class="form-control" type="text" name="password" id="password" />
</div>
<div class="mb-2">
<label class="form-label" for="newPassword">새 비밀번호</label>
<input class="form-control" type="text" name="newPassword" id="newPassword" />
</div>
<div class="mb-2">
<label class="form-label" for="newPassword2">새 비밀번호 확인</label>
<input class="form-control" type="text" id="newPassword2" />
</div>
<button class="btn btn-sm bg-primary-subtle text-white" type="submit">수정하기</button>
</form>
</div>
<script>
const editor = new toastui.Editor({
el:document.querySelector('#editor'),
height:'500px',
initialEditType:'wysiwyg',
previewStyle:'vertical',
language:'ko',
initialValue:`<%=dto.getContent()%>`
});
document.querySelector("#editForm").addEventListener("submit", (e)=>{
const content = editor.getHTML();
console.log(content);
document.querySelector("#hiddenContent").value=content;
});
</script>
spring06.exception 패키지 생성
PasswordException 클래스 생성
비밀 번호 수정시 입력한 비밀 번호가 DB 에 저장된 비밀번호와 일치하지 않을 때 발생시키는 Exception type
public class PasswordException extends RuntimeException{
// 생성자
public PasswordException(String message) {
super(message); // 생성자의 매개 변수에 전달된 예외 메세지를 부모 생성자에 전달하면
// 이 객체의 .getMessage() 메소드를 호출하면 해당 메세지가 리턴한다
}
}
ExceptionController 클래스 생성
예외 컨트롤러 @ControllerAdvice 어노테이션을 붙여서 bean 으로 만든다
@ControllerAdvice
public class ExceptionController {
public String password(PasswordException pe) {
return "redirect:/user/edit-password";
}
}
updatePassword 메소드 생성@PostMapping("/user/update-password")
public String updatePassword() {
return "user/update-password";
}
@Data
public class PwdChangeRequest {
private String userName;
private String password;
private String newPassword;
}
updatePassword 안에 코드 추가@PostMapping("/user/update-password")
public String updatePassword(PwdChangeRequest pcr) {
// pcr 객체에는 기존 비밀번호와 새 비밀번호가 들어 있다
return "user/update-password";
}
updatePassword 메소드 생성public void updatePassword(PwdChangeRequest pcr);
@Override
public void updatePassword(PwdChangeRequest pcr) {
// 로그인된 userName
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
// DB 에 저장된 암호화된 비밀번호를 읽어온다
UserDto dto = dao.getByUserName(userName);
String encodedPwd = dao.getByUserName(userName).getPassword();
// 암호화된 비밀번호와 입력한 비밀번호를 비교해서 일치하는지 확인
boolean isValid = BCrypt.checkpw(pcr.getPassword(), encodedPwd);
// 만일 일치하지 않으면 에외 발생시키기
if(!isValid) {
throw new PasswordException("기존 비밀번호가 일치하지 않습니다");
}
// 일치하면 새 비밀번호를 암호화해서 UserDto 객체에 담은 다음 DB 에 수정 반영
dto.setPassword(encoder.encode(pcr.getNewPassword())); dao.updatePassword(dto);
}
updatePassword 메소드 완성// 비밀번호 수정 반영 요청 처리
@PostMapping("/user/update-password")
public String updatePassword(PwdChangeRequest pcr, HttpSession session,
HttpServletRequest req, HttpServletResponse res) {
// pcr 객체에는 기존 비밀번호와 새 비밀번호가 들어 있다
service.updatePassword(pcr);
// 세션을 초기화ㅎ서 로그아웃 처리를 한다
session.invalidate();
// Security Logout Handler 객체를 이용해서 강제 로그아웃
new SecurityContextLogoutHandler()
.logout(req, res, SecurityContextHolder.getContext().getAuthentication());
return "user/update-password";
}
<div class="container">
<p class="alert alert-success">
비밀번호 수정 완료
로그아웃 되었습니다
<a class="alert-link" th:href="@{/user/loginform}">다시 로그인</a>
</p>
</div>
ExceptionHandler 또는 다른 일반 Controller 에서 리다이렉트 이동하면서 일회성 데이터를 전달하고 싶을 때는 컨트롤러의 매개변수에 RedirectAttributes 를 선언exception 이라는 키값으로 PasswordException 객체를 담는다Model 객체에 자동으로 담긴다@ExceptionHandler(PasswordException.class)
public String password(PasswordException pe, RedirectAttributes ra) {
ra.addFlashAttribute("exception", pe);
return "redirect:/user/edit-password";
}
⭐ UserController 의 `"/user/edit-password"`
요청을 처리하는 메소드가 호출되는 경우 2가지
1. 비밀번호 수정 링크를 눌렀을 때
2. 비밀번호 수정 정보를 입력하고 전송했지만 기존 비밀번호가 일치하지 않아서
PasswordException 이 발생했을 때
edit-password.html 에 코드 추가
만일 에러 객체가 존재한다면 아래의 알림 출력
<p class="alert alert-danger" th:if="${exception}" th:text="${exception.message}"></p>
String[] whiteList = {"/", "/user/loginform", "/user/login-fail",
"/user/expired", "/user/signup-form", "/user/signup", "/test/**"};
@ResponseBody 어노테이션을 붙인 컨트롤러 메소드에서 Map 을 리턴하면{ } 형식의 json 문자열이 응답된다{ } 안에는 Map 에 담긴 key : value 데이터가 들어있다@Controller
public class TestController {
@GetMapping("/test/json01")
@ResponseBody
public Map<String, Object> json01(){
Map<String, Object> map = Map.of(
"num",1,
"name","유재석",
"isMan",true
);
return map;
}
// json 문자열 응답 테스트를 위한 클래스 (dto 클래스)
@Data
public class MemberResponse{
private int num;
private String name;
private boolean isMan;
}
@GetMapping("/test/json02")
@ResponseBody
public MemberResponse json02() {
MemberResponse m = new MemberResponse();
m.setNum(2);
m.setName("박명수");
m.setMan(false);
return m;
}
@GetMapping("/test/json03")
@ResponseBody
public List<String> json03(){
List<String> list = List.of("유재석", "박명수", "정준하");
return list;
}
@GetMapping("/test/json04")
@ResponseBody
public List<MemberResponse> json04(){
MemberResponse m1 =new MemberResponse();
m1.setNum(1);
m1.setName("유재석");
m1.setMan(true);
MemberResponse m2 =new MemberResponse();
m2.setNum(2);
m2.setName("박명수");
m2.setMan(false);
List<MemberResponse> list = List.of(m1, m2);
return list;
}
}
{
"isMan": true,
"num": 1,
"name": "유재석"
}
{
"num": 2,
"name": "박명수",
"man": false
}
[
"유재석",
"박명수",
"정준하"
]
{
"num": 1,
"name": "유재석",
"man": true
},
{
"num": 2,
"name": "박명수",
"man": false
}
Map or Dto ➜ { } 형식의 json
List ➜ [ ] 형식의 json
List<String> ➜ ["xxx", "yyy", "zzz"]
List<Map> ➜ [{ }, { }, { }]
List<Dto> ➜ [{ }, { }, { }]
UserController 에 checkId 메소드 생성
사용 가능한 아이디 인자 여부를 json 문자열로 리턴하는 메소드
@GetMapping("/user/check-id")
@ResponseBody
public Map<String, Object> checkId(String inputId){
return null;
}
<div class="container">
<h1>회원가입 양식</h1>
<form th:action="@{/user/signup}" method="post" novalidate>
<div class="mb-2">
<label class="form-label" for="userName">아이디</label>
<input class="form-control" type="text" name="userName" id="userName"/>
<small class="form-text">영문자 소문자로 시작하고 5글자~10글자 이내로 입력하세요</small>
<div class="valid-feedback">사용 가능한 아이디 입니다.</div>
<div class="invalid-feedback">사용할 수 없는 아이디 입니다.</div>
</div>
<div class="mb-2">
<label class="form-label" for="pwd">비밀번호</label>
<input class="form-control" type="password" name="password" id="pwd"/>
<small class="form-text">특수 문자를 하나 이상 조합하세요.</small>
<div class="invalid-feedback">비밀 번호를 확인 하세요</div>
</div>
<div class="mb-2">
<label class="form-label" for="pwd2">비밀번호 확인</label>
<input class="form-control" type="password" id="pwd2"/>
</div>
<div class="mb-2">
<label class="form-label" for="email">이메일</label>
<input class="form-control" type="email" name="email" id="email"/>
<div class="invalid-feedback">이메일 형식에 맞게 입력하세요.</div>
</div>
<button class="btn btn-primary btn-sm" type="submit" disabled>가입</button>
</form>
</div>
<script>
// 아이디 유효성 여부를 관리할 변수
let isIdValid = false;
// 비밀번호 유효성 여부를 관리할 변수
let isPwdValid = false;
// 이메일 유효성 여부를 관리할 변수
let isEmailValid = false;
const checkForm = () => {
// 만일 아이디도 유효하고 그리고 비밀번호도 유효하고 그리고 이메일도 유효하다면
if(isIdValid && isPwdValid && isEmailValid){
// 전송 버튼에 disabled 속성을 제거하고
document.querySelector("[type=submit]").removeAttribute("disabled");
}else{
// 전송 버튼에 disabled 속성을 추가한다
document.querySelector("[type=submit]").setAttribute("disabled", "");
}
};
// blur 는 포커스를 잃었을 때 발생하는 이벤트
document.querySelector("#userName").addEventListener("blur", (e)=>{
// 현재까지 입력한 아이디를 읽어온다.
const inputId = e.target.value;
// "영문자 소문자로 시작하고 5글자~10글자 이내로 입력" 이 검증조건을 만족했는지 여부를 알아낸다
const reg = /^[a-z].{4,9}$/;
// 만일 정규 표현식을 통과 하지 못했다면
if(!reg.test(inputId)){
e.target.classList.remove("is-valid");
e.target.classList.add("is-invalid");
//사용할수 없는 아이디라는 의미에서 false 를 넣어준다.
isIdValid=false;
checkForm();
return; //함수를 여기서 종료하기
}
// fetch() 함수를 이용해서 get 방식으로 입력한 아이디를 보내고 사용가능 여부를 json 으로 응답받는다.
fetch("[[@{/}]]user/check-id?inputId="+inputId)
.then(res=>res.json())
.then(data=>{
// 일단 is-valid , is-invalid 클래스를 제거하고
e.target.classList.remove("is-valid", "is-invalid");
// data 는 {canUse:true} 또는 {canUse:false} 구조의 object 이다
if(data.canUse){ //사용가능 하다면
e.target.classList.add("is-valid");
// 사용할수 있는 아이디라는 의미에서 true 를 넣어준다
isIdValid = true;
}else{//사용가능 하지 않다면
e.target.classList.add("is-invalid");
//사용할수 없는 아이디라는 의미에서 false 를 넣어준다
isIdValid = false;
}
checkForm();
});
});
// 함수를 미리 만들어서
const checkPwd = ()=>{
// 비밀 번호를 검증할 정규 표현식(특수문자 포함여부)
const reg_pwd=/[\W]/;
// 양쪽에 입력한 비밀번호를 읽어와서
let pwd=document.querySelector("#pwd").value;
let pwd2=document.querySelector("#pwd2").value;
// 일단 is-valid 와 is-invalid 클래스를 제거를 먼저하고
document.querySelector("#pwd").classList.remove("is-valid", "is-invalid");
// 일단 정규표현식을 만족하는지 확인해서 만족하지 않으면 함수를 여기서 종료
// 만일 첫번째 비밀번호가 정규표현식을 통과하지 못하거나 또는 두번째 비밀번호가 정규표현식을 통과하지 못한다면
if( !reg_pwd.test(pwd) || !reg_pwd.test(pwd2) ){
document.querySelector("#pwd").classList.add("is-invalid");
isPwdValid = false;
checkForm();
return;
}
if(pwd == pwd2){
document.querySelector("#pwd").classList.add("is-valid");
// 비밀번호가 유효하다는 의미에서 true 를 넣어준다
isPwdValid = true;
}else{
document.querySelector("#pwd").classList.add("is-invalid");
// 비밀번호가 유효하지 않다는 의미에서 false 를 넣어준다
isPwdValid = false;
}
checkForm();
};
// 비밀번호 혹은 비밀번호 확인란에 입력 했을때 동일한 함수가 호출되도록 한다
document.querySelector("#pwd").addEventListener("input", checkPwd);
document.querySelector("#pwd2").addEventListener("input", checkPwd);
document.querySelector("#email").addEventListener("input", (e)=>{
//이메일을 검증할 정규 표현식
const reg_email=/@/;
//입력한 문자열 읽어오기 (e.target 은 이벤트가 발생한 바로 그 요소의 참조값이다)
const email=e.target.value;
//일단 is-valid 와 is-invalid 클래스를 제거를 먼저하고
document.querySelector("#email").classList.remove("is-valid", "is-invalid");
if(reg_email.test(email)){
document.querySelector("#email").classList.add("is-valid");
isEmailValid = true;
}else{
document.querySelector("#email").classList.add("is-invalid");
isEmailValid = false;
}
checkForm();
});
</script>
canUseId 메소드 생성public Map<String, Object> canUseId(String id);
@Override
public Map<String, Object> canUseId(String id) {
// id 를 이용해서 DB 에 해당하는 아이디로 가입된 정보가 있는지 읽어와 본다 (없으면 null)
UserDto dto = dao.getByUserName(id);
// id 가 사용 가능한지 여부 (dto 가 null 이면 사용가능한 아이디)
boolean canUse = dto == null ? true : false;
// Map 에 담아서 리턴한다
return Map.of("canUse", canUse);
}
checkId 메소드 완성// 사용 가능한 아이디 인자 여부를 json 문자열로 리턴하는 메소드
@GetMapping("/user/check-id")
@ResponseBody
public Map<String, Object> checkId(String inputId){
return service.canUseId(inputId);
}
String[] whiteList = {"/", "/user/loginform", "/user/login-fail",
"/user/expired", "/user/signup-form", "/user/signup",
"/test/**", "/user/check-id"};