로그인, 회원가입 기능을 수정하고 로그아웃 기능을 추가해보자!
[개발목표]
- 로그인 기능 수정 및 구현
- 로그아웃 기능 구현
- 회원가입 수정
1. 로그인 기능 수정 및 구현
[구현화면]
[요구사항]
- 아이디와 비밀번호를 통해서 로그인을 한다.
- 아이디와 비밀번호 둘 중에 하나라도 빈 값을 입력할 시 로그인 할 수 없고 화면에 안내문구를 띄운다.
- '비밀번호보기' 기능 : 눈 아이콘을 클릭하면 입력한 비밀번호가 그대로 보여지고, 다시 클릭하면 다시 패스워스 형태로 돌아간다.
- '아이디 기억하기' 기능 : '아이디 기억하기' 기능을 체크하고 로그인 하면 다음 로그인 시 아이디가 입력된 상태로 보여진다.
LoginForm.java
package seulgi.bookRentalSystem.domain.login;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Data
public class LoginForm {
@NotEmpty
private String loginId;
@NotEmpty
private String password;
}
- 로그인을 위한 객체를 하나 생성한다.
- 각 항목마다 @NotEmpty 어노테이션을 사용했는데, '아이디와 비밀번호 둘 중에 하나라도 빈 값을 입력할 시 로그인 할 수 없고 화면에 안내문구를 띄운다.' <- 이 요구사항을 충족하기 위해 Bean Valication 을 사용하고자 사용한 어노테이션이다.
LoginService.java
package seulgi.bookRentalSystem.domain.login;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import seulgi.bookRentalSystem.domain.member.Member;
import seulgi.bookRentalSystem.domain.member.MemberMapper;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class LoginService {
private final MemberMapper memberMapper;
/**
* 로그인 (null 이면 로그인 실패)
*
* @param loginId
* @param password
* @return
*/
public Member login (String loginId, String password){
return memberMapper.findByLoginId(loginId)
.filter(m -> m.getPassword().equals(password))
.orElse(null);
}
}
로그인을 위한 Service를 하나 생성. Null이면 로그인이 실패인 로직이다.
LoginController.java
package seulgi.bookRentalSystem.web.login;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import seulgi.bookRentalSystem.domain.login.LoginForm;
import seulgi.bookRentalSystem.domain.login.LoginService;
import seulgi.bookRentalSystem.domain.member.Member;
import seulgi.bookRentalSystem.domain.member.MemberService;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
private final MemberService memberService;
/**
* 로그인 화면 호출
* @param loginForm
* @return
*/
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm")LoginForm loginForm){
return "login/loginForm";
}
/**
* 로그인
* @param form
* @param bindingResult
* @param redirectURL
* @param request
* @return
*/
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form
, BindingResult bindingResult
, @RequestParam(defaultValue = "/") String redirectURL
, HttpServletRequest request){
if (bindingResult.hasErrors()){
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null){
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
String memberName = loginMember.getMemberName();
HttpSession session = request.getSession();
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
session.setAttribute("loginId", form.getLoginId());
session.setAttribute("loginName", memberName);
return "redirect:" + redirectURL;
}
/**
* 로그아웃
* @param request
* @return
*/
@GetMapping("/logout")
public String logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/book";
}
@PostMapping("/{memberId}/idCheck")
public ResponseEntity<Boolean> idCheck(@PathVariable String memberId){
String idCheck = memberService.idCheck(memberId);
if (idCheck == null){
return ResponseEntity.ok(true);
} else {
return ResponseEntity.ok(false);
}
}
}
로그인에 필요한 컨트롤러를 따로 만들었고 톧아보기로 한다.
1) 로그인 화면 호출 / 로그인
/**
* 로그인 화면 호출
* @param loginForm
* @return
*/
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm")LoginForm loginForm){
return "login/loginForm";
}
/**
* 로그인
* @param form
* @param bindingResult
* @param redirectURL
* @param request
* @return
*/
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form
, BindingResult bindingResult
, @RequestParam(defaultValue = "/") String redirectURL
, HttpServletRequest request){
if (bindingResult.hasErrors()){
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null){
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
String memberName = loginMember.getMemberName();
HttpSession session = request.getSession();
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
session.setAttribute("loginId", form.getLoginId());
session.setAttribute("loginName", memberName);
return "redirect:" + redirectURL;
}
- 로그인 화면 호출은 get방식으로 화면을 호출하는 아주 간단한 방식으로 구현하였고,
- 로그인 처리는 @Valid 를 사용하여 Validation을 진행하였다.
- 로그인이 된 후에는 그 로그인 정보를 계속 활용하기 위해 세션을 하나 만들어 거기에 로그인 정보를 저장해 두었다.
2) 로그아웃 기능 구현
/**
* 로그아웃
* @param request
* @return
*/
@GetMapping("/logout")
public String logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/book";
}
로그아웃 기능은 간단하다. 세션을 가져와서 세션에 로그인 정보가 담겨 있을 경우 그 세션을 무효화 하는 방법으로 로그아웃을 진행하였다.
loginForm.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}" href="../css/bootstrap.min.css" rel="stylesheet">
<script th:src="@{/js/login/loginForm.js}"></script>
<style>
.container {
max-width: 560px;
}
.field-error {
border-color: #dc3545;
color: #dc3545;
}
.is-invalid {
border-color: #dc3545;
color: #dc3545;
}
.form-floating{
margin-top: 20px;
}
div.is-invalid{
font-size: 0.87em;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>로그인</h2>
</div>
<form action="item.html" id="loginForm" th:action th:object="${loginForm}" method="post">
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}"
th:text="${err}">전체 오류 메시지</p>
<p>
</div>
<div class="form-floating">
<input type="text" id="loginId" th:field="*{loginId}" class="form-control"
placeholder="아이디를 입력하세요." th:errorclass="is-invalid">
<label for="loginId">ID</label>
</div>
<div class="form-floating input-group mb-3">
<input type="password" id="password" th:field="*{password}" class="form-control"
placeholder="비밀번호를 입력하세요." th:errorclass="is-invalid">
<span class="input-group-text" id="inputGroup-sizing-lg" style="background-color: white;">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" id="passwordShow" fill="currentColor"
class="bi bi-eye" viewBox="0 0 16 16" style="color: lightgrey;">
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8M1.173 8a13 13 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5s3.879 1.168 5.168 2.457A13 13 0 0 1 14.828 8q-.086.13-.195.288c-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5s-3.879-1.168-5.168-2.457A13 13 0 0 1 1.172 8z"/>
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5M4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0"/>
</svg>
</span>
</div>
<div class="form-check text-start my-3">
<input class="form-check-input" type="checkbox" value="remember-me" id="flexCheckDefault">
<label class="form-check-label" for="flexCheckDefault">ID 기억하기</label>
</div>
<div class="row">
<div class="col">
<button class="w-100 btn btn-secondary btn-lg" type="button"
th:onclick="|location.href='@{/member/join}'|">
회원 가입
</button>
</div>
<div class="col">
<button class="w-100 btn btn-primary btn-lg" id="loginBtn" type="submit">로그인</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
복잡해 보이지만 역시나 부트스트랩을 사용해서 그렇다.
loginForm.js
function setCookie(cname, cvalue, exdays) {
let d = new Date();
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
let expires = "expires=" + d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function rememberId() {
let loginId = document.getElementById("loginId").value;
// 체크박스가 체크되어 있으면, 쿠키에 아이디를 저장합니다.
if (document.getElementById("flexCheckDefault").checked) {
setCookie("rememberedId", loginId, 30); // 쿠키는 30일 동안 유효합니다.
} else {
// 체크박스가 체크되어 있지 않으면, 쿠키를 삭제합니다.
document.cookie = "rememberedId=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
}
function togglePasswordVisibility() {
let passwordInput = document.getElementById("password");
if (passwordInput.type === "password") {
passwordInput.type = "text";
} else {
passwordInput.type = "password";
}
}
document.addEventListener('DOMContentLoaded', function () {
let rememberedId = getCookie("rememberedId");
if (rememberedId !== "") {
document.getElementById("loginId").value = rememberedId;
}
document.getElementById('loginBtn').addEventListener('click', function(event) {
event.preventDefault();
rememberId();
document.getElementById('loginForm').submit();
});
document.getElementById('passwordShow').addEventListener('click', function () { togglePasswordVisibility();})
document.getElementById('flexCheckDefault').addEventListener('change', function() { rememberId(); });
});
로그인 화면에서 '아이디 기억하기' 기능과 '비밀번호 보이기/숨기기' 기능 구현을 위해 자바스크립트를 사용했다. 내용이 생각보다 많으니 톧아 보기로 한다.
1) 아이디 저장하기 기능 구현
실행 순서대로 톧아볼 예정
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('flexCheckDefault').addEventListener('change', function() { rememberId(); });
});
아이디 저장하기 체크박스에 변화가 감지되면 rememberId() 함수가 실행된다.
rememberId() 함수 실행
function rememberId() {
let loginId = document.getElementById("loginId").value;
// 체크박스가 체크되어 있으면, 쿠키에 아이디를 저장합니다.
if (document.getElementById("flexCheckDefault").checked) {
setCookie("rememberedId", loginId, 30); // 쿠키는 30일 동안 유효합니다.
} else {
// 체크박스가 체크되어 있지 않으면, 쿠키를 삭제합니다.
document.cookie = "rememberedId=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
}
체크박스가 체크되었다면 쿠키를 생성하고 그 기간을 30일 동안 유지되며, 체크되지 않았다면 쿠키를 삭제해준다.
setCookie()함수 호출
function setCookie(cname, cvalue, exdays) {
let d = new Date();
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
let expires = "expires=" + d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}
쿠키를 생성해주는 실질적인 함수. GPT의 도움을 받았다.
화면이 처음 로드될 때
document.addEventListener('DOMContentLoaded', function () {
let rememberedId = getCookie("rememberedId");
if (rememberedId !== "") {
document.getElementById("loginId").value = rememberedId;
}
});
쿠키를 가져와서 그 쿠키가 존재한다면 쿠키에 담긴 로그인 아이디를 아이디 인풋에 넣어주는 방식으로 아이디 기억하기 기능이 구현된다.
getCookie()
쿠키를 실질적으로 가져오는 부분
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
2) 비밀번호 보이기/숨기기 기능 구현
document.getElementById('passwordShow').addEventListener('click', function () { togglePasswordVisibility();})
function togglePasswordVisibility() {
let passwordInput = document.getElementById("password");
if (passwordInput.type === "password") {
passwordInput.type = "text";
} else {
passwordInput.type = "password";
}
}
비밀번호 숨기기/보이기 버튼을 클릭하면 input의 타입을 password에서 Text로 토글 전환 함으로 구현한다.
3) 로그인 기능 구현
document.getElementById('loginBtn').addEventListener('click', function(event) {
event.preventDefault();
rememberId();
document.getElementById('loginForm').submit();
});
로그인 버튼 클릭시 아이디 기억하기 기능을 체크한 후 submit하여 컨트롤러에 접근한다.
2. 로그아웃 기능 구현
LoginController.java
/**
* 로그아웃
* @param request
* @return
*/
@GetMapping("/logout")
public String logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/book";
}
로그아웃 기능은 로그인 기능에 비해 무척이나 간단한다. 로그아웃을 할경우 세션을 무효화 시켜주기만 하면 된다.
3. 회원가입 수정 및 구현
[요구사항]
- 회원아이디, 닉네임, 비밀번호, 비밀번호 확인 모두 필수값이며 하나도 입력하지 않았을 경우 회원가입이 불가능하다.
- 회원아이디, 닉네임, 비밀번호 모두 각각의 유효성체크를 필요로 한다.
- 회원아이디의 경우 중복검사를 반드시 진행해야 하며 진행하지 않았을 경우 회원가입이 불가능하다.
- 비밀번호 입력후 비밀번호를 한번 더 입력하지 않을 경우 회원가입이 불가능하다.
joinForm.js
이전 개발일지에 회원가입에 대한 기본적인 컨트롤러, 서비스, 메퍼들은 생성하고 소개했으니 수정 /추가한 부분에 대해서만 언급할 예정이다.
function idValidate(){
let idInput = document.getElementById('memberId');
let idError = document.getElementById('idError');
if (idInput.value.trim() === '') {
idInput.classList.add('is-invalid');
idError.style.display = '';
idError.textContent = 'ID는 필수입니다.';
} else {
idInput.classList.remove('is-invalid');
idError.style.display = 'none';
}
let idRegExp = /^[a-zA-Z0-9!_.-]{4,12}$/;
if( !idRegExp.test(idInput.value) ) {
idInput.classList.add('is-invalid');
idError.style.display = '';
idError.textContent = '4~12글자, 영어나 숫자만 가능합니다. (입력불가특수문자 : @\#$%&=/)';
} else {
idInput.classList.remove('is-invalid');
idError.style.display = 'none';
}
}
function nameValidate(){
let memberNameInput = document.getElementById('memberName');
let memberNameError = document.getElementById('memberNameError');
if (memberNameInput.value.trim() === '') {
memberNameInput.classList.add('is-invalid');
memberNameError.style.display = '';
memberNameError.textContent = '닉네임은 필수입니다.';
} else {
memberNameInput.classList.remove('is-invalid');
memberNameError.textContent = 'none';
}
let nicknameRegExp = /^[a-zA-Z0-9가-힣]{1,20}$/;
if( !nicknameRegExp.test(memberNameInput.value) ) {
memberNameInput.classList.add('is-invalid');
memberNameError.style.display = '';
memberNameError.textContent = '1~20글자, 한글, 영어, 숫자만 가능합니다.';
} else {
memberNameInput.classList.remove('is-invalid');
memberNameError.style.display = 'none';
}
}
function passwordValidate() {
let passwordInput = document.getElementById('password');
let passwordError = document.getElementById('passwordError');
if (passwordInput.value.trim() === '') {
passwordInput.classList.add('is-invalid');
passwordError.textContent = '비밀번호는 필수 입니다.';
} else {
passwordInput.classList.remove('is-invalid');
passwordError.textContent = '';
}
let passwordRegExp = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()\-_=+{};:,<.>]).{8,}$/;
if (!passwordRegExp.test(passwordInput.value)) {
passwordInput.classList.add('is-invalid');
passwordError.style.display = '';
passwordError.textContent = '최소 8자 이상 영문, 한글, 숫자, 특수문자가 포함되어야 합니다.';
} else {
passwordInput.classList.remove('is-invalid');
passwordError.style.display = 'none';
}
}
function checkPasswordValidate(){
let passwordInput = document.getElementById('password');
let checkPasswordInput = document.getElementById('checkPassword');
let checkPasswordSuccess = document.getElementById('checkPasswordSuccess');
let checkPasswordError = document.getElementById('checkPasswordError');
if ( passwordInput.value.trim() !== '' ) {
if (passwordInput.value === checkPasswordInput.value) {
checkPasswordSuccess.style.display = '';
checkPasswordError.style.display = 'none';
checkPasswordInput.classList.remove('is-invalid');
checkPasswordInput.classList.add('is-valid');
checkPasswordSuccess.textContent = '비밀번호가 일치합니다.';
} else {
checkPasswordSuccess.style.display = 'none';
checkPasswordError.style.display = '';
checkPasswordInput.classList.add('is-invalid');
checkPasswordInput.classList.remove('is-valid');
checkPasswordError.textContent = '비밀번호가 일치하지 않습니다.';
}
}
}
function idCheck() {
let idInput = document.getElementById('memberId');
let idError = document.getElementById('idError');
let idNotError = document.getElementById('idNotError');
fetch("/" + idInput.value + "/idCheck", {
method : "POST"
})
.then(response => {
if(response.ok){
console.log("아이디 사용 가능");
idInput.classList.remove('is-invalid');
idInput.classList.add('is-valid');
idError.style.display = 'none';
idNotError.textContent = '사용 가능한 아이디 입니다.';
idNotError.style.display = '';
} else {
console.log("이미 사용 중인 아이디 입니다.");
idInput.classList.remove('is-valid');
idInput.classList.add('is-invalid');
idError.style.display = '';
idError.textContent = '이미 사용 중인 아이디 입니다.';
idError.style.display = '';
idNotError.style.display = 'none';
}
})
.catch(error => {
console.error("error", error);
})
}
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('saveBtn').addEventListener('click', function(event) {
event.preventDefault();
idValidate();
idCheck();
nameValidate();
passwordValidate();
checkPasswordValidate();
if(!document.getElementById('memberId').classList.contains('is-valid')){
alert("아이디 중복검사는 필수 입니다.");
return;
}
if ( document.getElementById('memberId').classList.contains('is-valid')
&& !document.getElementById('memberName').classList.contains('is-invalid')
&& !document.getElementById('password').classList.contains('is-invalid')
&& document.getElementById('memberId').classList.contains('is-valid') ) {
document.getElementById('joinForm').submit();
}
});
document.getElementById('memberId').addEventListener('keyup', function() { idValidate(); });
document.getElementById('memberName').addEventListener('keyup', function() { nameValidate(); });
document.getElementById('password').addEventListener('keyup', function() { passwordValidate(); });
document.getElementById('checkPassword').addEventListener('keyup', function() { checkPasswordValidate(); });
document.getElementById('idCheck').addEventListener('click', idCheck);
});
아이디 (아이디 유효성검사, 중복검사), 닉네임, 비밀번호, 비밀번호 확인에 대한 유효성검사를 진행하고 모든 항목의 유효성 검사에 문제가 없을 때, 회원가입이 진행되도록 한다.
'💻 뚝딱뚝딱 > 팀내도서대여시스템(OBRS)' 카테고리의 다른 글
[개발일지#011] 회원정보수정 수정하기 (0) | 2024.04.11 |
---|---|
[개발일지#010] 페이지네이션 적용하기 (회원목록 / 나의책 / 빌린책 / 모든책) (0) | 2024.04.11 |
[개발일지#008] 나의책 / 빌린책 기능 구현 (0) | 2024.04.09 |
[개발일지#007] 책 수정 / 삭제 기능 수정 및 구현 (2) | 2024.04.08 |
[개발일지#006] 책 대여 / 반납 기능 구현 (0) | 2024.04.07 |