DB에 회원 데이터를 관리할 테이블을 생성한 후 회원가입 기능을 구현해보자
- 회원 테이블
CREATE TABLE `tb_member` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '회원 번호 (PK)',
`login_id` varchar(20) NOT NULL COMMENT '로그인 ID',
`password` varchar(60) NOT NULL COMMENT '비밀번호',
`name` varchar(20) NOT NULL COMMENT '이름',
`gender` enum('M','F') NOT NULL COMMENT '성별',
`birthday` date NOT NULL comment '생년월일',
`delete_yn` tinyint(1) NOT NULL COMMENT '삭제 여부',
`created_date` datetime NOT NULL DEFAULT current_timestamp() COMMENT '생성일시',
`modified_date` datetime DEFAULT NULL COMMENT '최종 수정일시',
PRIMARY KEY (`id`),
UNIQUE KEY uix_member_login_id (`login_id`)
) COMMENT '회원'
-성별 처리용 Enum클래스 생성 (enum은 상수의 개념)
package com.study.domain.member;
public enum Gender {
M,F
}
-회원 요청(request) 클래스 생성
회원가입과 회원정보 수정에 사용될 요청 클래스이다.
비동기(Ajax)통신, json포맷으로 데이터를 주고 받기 때문에 @Setter는 필요하지 않다
package com.study.domain.member;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.thymeleaf.util.StringUtils;
import java.time.LocalDate;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberRequest {
private Long id; // 회원 번호 (PK)
private String loginId; // 로그인 ID
private String password; // 비밀번호
private String name; // 이름
private Gender gender; // 성별
private LocalDate birthday; // 생년월일
public void encodingPassword(PasswordEncoder passwordEncoder) {
if (StringUtils.isEmpty(password)) {
return;
}
password = passwordEncoder.encode(password);
}
}
-회원응답 response 클래스 생성
회원 데이터 조회에 사용될 응답 클래스이다.
package com.study.domain.member;
import lombok.Getter;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Getter
public class MemberResponse {
private Long id; // 회원 번호 (PK)
private String loginId; // 로그인 ID
private String password; // 비밀번호
private String name; // 이름
private Gender gender; // 성별
private LocalDate birthday; // 생년월일
private Boolean deleteYn; // 삭제 여부
private LocalDateTime createdDate; // 생성일시
private LocalDateTime modifiedDate; // 최종 수정일시
public void clearPassword(){
this.password ="";
}
}
-회원 Mapper인터페이스 생성하기
package com.study.domain.member;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MemberMapper {
/**
* 회원 정보 저장 (회원가입)
* @param params - 회원 정보
*/
void save(MemberRequest params);
/**
* 회원 상세정보 조회
* @param loginId - UK
* @return 회원 상세정보
*/
MemberResponse findByLoginId(String loginId);
/**
* 회원 정보 수정
* @param params - 회원 정보
*/
void update(MemberRequest params);
/**
* 회원 정보 삭제 (회원 탈퇴)
* @param id - PK
*/
void deleteById(Long id);
/**
* 회원 수 카운팅 (ID 중복 체크)
* @param loginId - UK
* @return 회원 수
*/
int countByLoginId(String loginId);
}
DB와 통신할 MyBatis의 Mapper인터페이스이다. 지금까지 구현해온 게시글 , 댓글 CRUD 메서드와 유사한 구조를 가지고 있지만 상세정보를 조회하는 findBy- - -()메서드에서 PK(id)가 아닌 UK(loginid) 를 기준으로 쿼리가 실행된다는 차이가 있다.
- 회원 XML Mapper 생성
MembetrMapper 인터페이스와 연결할 MyBatis XML Mapper이다.
MemberMapper.xml을 추가 후 아래 SQL쿼리를 작성한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.study.domain.member.MemberMapper">
<!-- tb_member 테이블 전체 컬럼 -->
<sql id="memberColumns">
id
, login_id
, password
, name
, gender
, birthday
, delete_yn
, created_date
, modified_date
</sql>
<!-- 회원 정보 저장 (회원가입) -->
<insert id="save" parameterType="com.study.domain.member.MemberRequest" useGeneratedKeys="true" keyProperty="id">
INSERT INTO tb_member (
<include refid="memberColumns" />
) VALUES (
#{id}
, #{loginId}
, #{password}
, #{name}
, #{gender}
, #{birthday}
, 0
, NOW()
, NULL
)
</insert>
<!-- 회원 상세정보 조회 -->
<select id="findByLoginId" parameterType="string" resultType="com.study.domain.member.MemberResponse">
SELECT
<include refid="memberColumns" />
FROM
tb_member
WHERE
delete_yn = 0
AND login_id = #{value}
</select>
<!-- 회원 정보 수정 -->
<update id="update" parameterType="com.study.domain.member.MemberRequest">
UPDATE tb_member
SET
modified_date = NOW()
, name = #{name}
, gender = #{gender}
, birthday = #{birthday}
<if test="password != null and password != ''">
, password = #{password}
</if>
WHERE
id = #{id}
</update>
<!-- 회원 정보 삭제 (회원 탈퇴) -->
<delete id="deleteById" parameterType="long">
UPDATE tb_member
SET
delete_yn = 1
WHERE
id = #{id}
</delete>
<!-- 회원 수 카운팅 (ID 중복 체크) -->
<select id="countByLoginId" parameterType="string" resultType="int">
SELECT
COUNT(*)
FROM
tb_member
WHERE
login_id = #{value}
</select>
</mapper>
-회원 서비스 클래스 생성하기
비즈니스 로직을 담당한다.
package com.study.domain.member;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberMapper memberMapper;
private final PasswordEncoder passwordEncoder;
/**
* 회원 정보 저장 (회원가입)
* @param params - 회원 정보
* @return PK
*/
@Transactional
public Long saveMember(final MemberRequest params) {
params.encodingPassword(passwordEncoder);
memberMapper.save(params);
return params.getId();
}
/**
* 회원 상세정보 조회
* @param loginId - UK
* @return 회원 상세정보
*/
public MemberResponse findMemberByLoginId(final String loginId) {
return memberMapper.findByLoginId(loginId);
}
/**
* 회원 정보 수정
* @param params - 회원 정보
* @return PK
*/
@Transactional
public Long updateMember(final MemberRequest params) {
params.encodingPassword(passwordEncoder);
memberMapper.update(params);
return params.getId();
}
/**
* 회원 정보 삭제 (회원 탈퇴)
* @param id - PK
* @return PK
*/
@Transactional
public Long deleteMemberById(final Long id) {
memberMapper.deleteById(id);
return id;
}
/**
* 회원 수 카운팅 (ID 중복 체크)
* @param loginId - UK
* @return 회원 수
*/
public int countMemberByLoginId(final String loginId) {
return memberMapper.countByLoginId(loginId);
}
/**
* 로그인
* @param loginId - 로그인 ID
* @param password - 비밀번호
* @return 회원 상세정보
*/
public MemberResponse login(final String loginId, final String password) {
// 1. 회원 정보 및 비밀번호 조회
MemberResponse member = findMemberByLoginId(loginId);
String encodedPassword = (member == null) ? "" : member.getPassword();
// 2. 회원 정보 및 비밀번호 체크
if (member == null || passwordEncoder.matches(password, encodedPassword) == false) {
return null;
}
// 3. 회원 응답 객체에서 비밀번호를 제거한 후 회원 정보 리턴
member.clearPassword();
return member;
}
}
passwordEncoder: PasswordEncoder 빈이다
saveMember() : 회원정보를 저장한다. MemberRequest의 encodingPassword()를 호출해 비밀 번호를 암호화한다.
PasswordEncoder의 encode()는 파라미터로 전달한 값을 60자리의 난수로 리턴해 준다. 회원 테이블의 password를 varchar(60)자리로 선언한 이유도 이와 같다
findMemberByLoginId(): 로그인 id 를 기본으로 회원 상세정보를 조회한다.
updateMember() : 회원정보를 수정한다. saveMember()와 실행되는 쿼리에만 차이가 있다.
deleteMemberById() : Pk를 기준으로 회원 정보를 삭제한다. 게시글, 댓글과 마찬가지로 논리 삭제 방식을 이용한다
countMemberByLoginId() : 로그인 ID를 기준우로 회원 수를 카운팅한다. 아이디 중복을 체크하는 용도로 사용 된다
-회원 컨트롤러 클래스 생성
회원 관련 페이지와 데이터의 CRUD 를 처리할 컨트롤러이다. 댓글과는 달리 회원가입 페이지가 필요하기 때문에 @RestController가 아닌 @Controller와 @ResponseBody가 사용된다.
package com.study.domain.member;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
// 로그인 페이지
@GetMapping("/login.do")
public String openLogin() {
return "member/login";
}
// 회원 정보 저장 (회원가입)
@PostMapping("/members")
@ResponseBody
public Long saveMember(@RequestBody final MemberRequest params) {
return memberService.saveMember(params);
}
// 회원 상세정보 조회
@GetMapping("/members/{loginId}")
@ResponseBody
public MemberResponse findMemberByLoginId(@PathVariable final String loginId) {
return memberService.findMemberByLoginId(loginId);
}
// 회원 정보 수정
@PatchMapping("/members/{id}")
@ResponseBody
public Long updateMember(@PathVariable final Long id, @RequestBody final MemberRequest params) {
return memberService.updateMember(params);
}
// 회원 정보 삭제 (회원 탈퇴)
@DeleteMapping("/members/{id}")
@ResponseBody
public Long deleteMemberById(final Long id) {
return memberService.deleteMemberById(id);
}
// 회원 수 카운팅 (ID 중복 체크)
@GetMapping("/member-count")
@ResponseBody
public int countMemberByLoginId(@RequestParam final String loginId) {
return memberService.countMemberByLoginId(loginId);
}
// 로그인
@PostMapping("/login")
@ResponseBody
public MemberResponse login(HttpServletRequest request) {
// 1. 회원 정보 조회
String loginId = request.getParameter("loginId"); //이 정보를 받아오기
String password = request.getParameter("password");
MemberResponse member = memberService.login(loginId, password);
// 2. 세션에 회원 정보 저장 & 세션 유지 시간 설정
if (member != null) {
HttpSession session = request.getSession();
session.setAttribute("loginMember", member); //사용자 정보 세션에 저장
session.setMaxInactiveInterval(60 * 30);
}
return member;
}
// 로그아웃
@PostMapping("/logout")
public String logout(HttpSession session) {
session.invalidate();
return "redirect:/login.do";
}
}
-로그인 HTMl페이지 처리하기
member 폴더에 login.html 을 추가한다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<title>로그인</title>
<link rel="stylesheet" th:href="@{/css/default.css}" />
<link rel="stylesheet" th:href="@{/css/common.css}" />
<link rel="stylesheet" th:href="@{/css/content.css}" />
<link rel="stylesheet" th:href="@{/css/button.css}" />
<style>
#login_box .signup_btn {background:#42d870; border:0; border-bottom:solid 3px #4ed177; border-radius:50px; width:100%; height:52px; line-height:52px; font-size:16px; color:#fff; text-align:center; margin:20px 0 15px;}
</style>
</head>
<body>
<div id="login_wrap">
<div id="login_box">
<div class="login_con">
<div class="login_tit">
<h2>대림 게시판 </h2>
<p>LOG<i>IN</i></p>
</div>
<div class="login_input">
<form id="loginForm" onsubmit="return false;" autocomplete="off">
<ul>
<li>
<label for="loginId" class="skip_info">아이디</label>
<input type="text" id="loginId" name="loginId" placeholder="아이디" title="아이디" />
</li>
<li>
<label for="password" class="skip_info">비밀번호</label>
<input type="password" id="password" name="password" title="비밀번호" placeholder="비밀번호" />
</li>
</ul>
<button type="button" onclick="login();" class="login_btn">로그인</button>
<button type="button" onclick="openSignupPopup();" class="signup_btn">회원가입</button>
</form>
</div>
</div>
</div>
</div>
<!--/* 회원가입 popup */-->
<div id="signupPopup" class="popLayer">
<h3>회원가입</h3>
<div class="pop_container">
<form id="signupForm" onsubmit="return false;" autocomplete="off">
<table class="tb tb_row tl">
<colgroup>
<col style="width:30%;" /><col style="width:70%;" />
</colgroup>
<tbody>
<tr>
<th scope="row">아이디<span class="es">필수 입력</span></th>
<td>
<input type="text" name="loginId" placeholder="아이디" maxlength="20" style="width: 80%;" />
<button type="button" id="idCheckBtn" class="btns btn_st5" onclick="checkLoginId();" style="width: 20%; float: right;">중복 확인</button>
</td>
</tr>
<tr>
<th scope="row">비밀번호<span class="es">필수 입력</span></th>
<td><input type="password" name="password" placeholder="비밀번호" maxlength="30" /></td>
</tr>
<tr>
<th scope="row">비밀번호 재입력<span class="es">필수 입력</span></th>
<td><input type="password" name="passwordCheck" placeholder="비밀번호 재입력" maxlength="30" /></td>
</tr>
<tr>
<th scope="row">이름<span class="es">필수 입력</span></th>
<td><input type="text" name="name" placeholder="이름" maxlength="10" /></td>
</tr>
<tr>
<th scope="row">성별<span class="es">필수 입력</span></th>
<td>
<div class="radio_group">
<p class="radios">
<input type="radio" id="male" name="gender" value="M" checked />
<label for="male">남</label><span class="check"></span>
</p>
<p class="radios">
<input type="radio" id="female" name="gender" value="F" />
<label for="female">여</label><span class="check"></span>
</p>
</div>
</td>
</tr>
<tr>
<th scope="row">생년월일<span class="es">필수 입력</span></th>
<td><input type="number" name="birthday" placeholder="숫자 8자리 입력" /></td>
</tr>
</tbody>
</table>
</form>
<p class="btn_set">
<button type="button" class="btns btn_st2" onclick="saveMember();">가입</button>
<button type="button" class="btns btn_bdr2" onclick="closeSignupPopup();">취소</button>
</p>
</div>
<button type="button" class="btn_close" onclick="closeSignupPopup();"><span><i class="far fa-times-circle"></i></span></button>
</div>
<script th:src="@{/js/function.js}"></script>
<script th:src="@{/js/jquery-3.6.0.min.js}"></script>
<script th:src="@{/js/common.js}"></script>
<script src="https://kit.fontawesome.com/79613ae794.js" crossorigin="anonymous"></script>
<script>
// 회원가입 팝업 open
function openSignupPopup() {
layerPop('signupPopup')
}
// 회원가입 팝업 close
function closeSignupPopup() {
const form = document.getElementById('signupForm');
form.loginId.readOnly = false;
form.querySelector('#idCheckBtn').disabled = false;
form.reset();
layerPopClose();
}
// 아이디 중복 체크
function checkLoginId() {
const loginId = document.querySelector('#signupForm input[name="loginId"]');
isValid(loginId, '아이디');
const count = getJson(`/member-count`, { loginId : loginId.value });
if (count > 0) {
alert('이미 가입된 아이디가 있습니다.');
loginId.focus();
return false;
}
if (confirm('사용 가능한 아이디입니다.\n입력하신 아이디로 결정하시겠어요?')) {
loginId.readOnly = true;
document.getElementById('idCheckBtn').disabled = true;
}
}
// 회원 정보 유효성 검사
function validationMemberInfo(form) {
const fields = form.querySelectorAll('input:not([type="radio"])');
const fieldNames = ['아이디', '비밀번호', '빕밀번호 재입력', '이름', '생년월일'];
for (let i = 0, len = fields.length; i < len; i++) {
isValid(fields[i], fieldNames[i]);
}
if (form.loginId.readOnly === false) {
alert('아이디 중복 체크를 완료해 주세요.');
throw new Error();
}
if (form.password.value !== form.passwordCheck.value) {
alert('비밀번호가 일치하지 않습니다.');
form.passwordCheck.focus();
throw new Error();
}
}
// 회원 정보 저장 (회원가입)
function saveMember() {
// 1. 필드 유효성 검사
const form = document.getElementById('signupForm');
validationMemberInfo(form);
// 2. 파라미터 세팅
const params = {}
new FormData(form).forEach((value, key) => params[key] = value.trim());
params.birthday = params.birthday.replace(/(\d{4})(\d{2})(\d{2})/g, '$1-$2-$3');
// 3. Save API 호출
callApi('/members', 'post', params);
alert('가입을 축하드립니다!\n로그인 후 서비스를 이용해 주세요.');
closeSignupPopup();
}
// Enter 로그인 이벤트 바인딩
window.onload = () => {
document.querySelectorAll('#loginId, #password').forEach(element => {
element.addEventListener('keyup', (e) => {
if (e.keyCode === 13) {
login();
}
})
})
}
// 로그인
function login() {
const form = document.getElementById('loginForm');
if ( !form.loginId.value || !form.password.value ) {
alert('아이디와 비밀번호를 모두 입력해 주세요.');
form.loginId.focus();
return false;
}
$.ajax({
url : '/login',
type : 'POST',
dataType : 'json',
data : {
loginId: form.loginId.value,
password: form.password.value
},
async : false,
success : function (response) {
location.href = '/post/list.do';
},
error : function (request, status, error) {
alert('아이디와 비밀번호를 확인해 주세요.');
}
})
}
</script>
</body>
</html>
signupPopup: 회원가입 페이지를 따로 구성하지 않고 레이어 팝업을 이용해 회원가입을 처리한다. 댓글 처리에 사용된 레이어 팝업과 동일하며 , 필드 개수에만 차이가 있다
openSignupPopup() : 회원 가입 팝업을 띄우는 함수이다. common.js의 layerPop()을 호출하는 게 전부이다.
closeSignupPopup(): signupForm 안에 있는 로그인 id의 읽기모드 (read only) , 중복 확인 버튼의 비활성화(disabled) 속성, 그리고 모든 필드의 값을 초기화한 후 common.js 의 layerPopClose()를 호출해 팝업을 닫는 함수이다
checkLoginId(): 로그인 ID의 중복을 체크하는 함수이다. MemberController의 countMemberByLoginId()를 호출해 사용자가 입력한 id와 동일한 아이디가 있으면 로직을 종료하고, 없는 경우에는 아이디 결정 여부를 물어 확인 (true),이 되면 로그인 id와 중복 확인 버튼을 각각 읽기모드 (readonly) , 비활성화(disabled) 처리한다.
closeSignUpPopup() 에서 로그인 id와 중복 확인 버튼을 초기화하는 이유는 이 때문이다.
validationMemberInfo(): 회원정보의 유효성을 검사하는 필드. 변수 fields는 파라미터로 전달받은 signupForm의 모든 필드 중 성별(radio)버튼을 제외하고 모든 필드를 의미하고, fieldnames는 fields에 해당되는 필드명을 의미한다.
savemember() : MemberController의 savemember() 를 호출해 DB에 회원 정보를 저장하는 함수이다.
Form Data의 생성자에 form 앨리먼트를 전달하면 form 안에 있는 모든 필드의 name과 value가 FormData객체에 key-value형식으로 저장된다. 이 FormData객체를 forEach() 로 순환해 리터럴 객체인 params에 key- value 를 저장하고 생년월일을 yyyy-mm--dd 형식으로 치환한 후 Save API를 호출한다.
'JAVA > SpringBoot' 카테고리의 다른 글
게시판 프로젝트 - 다중 첨부 파일 업로드, 다운로드 구현 (0) | 2023.11.15 |
---|---|
게시판 프로젝트 - 로그인/ 로그아웃/ 로그인 세션 체크 기능 (0) | 2023.11.15 |
게시판 프로젝트 - Ajax(비동기) 페이징 새로고침시 페이지 번호 유지하기 (0) | 2023.11.15 |
게시판 프로젝트 - 댓글 페이징 처리 (0) | 2023.11.15 |
게시판 프로젝트 - 댓글 삭제 기능 구 (0) | 2023.11.15 |