02-회원제 게시판 만들기_SpringBoot와 JPA
02-회원(member) 파트 - SpringBoot
DB와의 연동과 Validation을 위해 build.gradle 파일에 내용을 추가해준다.
plugins {
id 'org.springframework.boot' version '2.6.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.ex'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
}
test {
useJUnitPlatform()
}
위에서 추가된 내용은 추려보았다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
runtimeOnly 'mysql:mysql-connector-java'
}
DB 설정
Mysql에 db와 user, 비밀번호를 생성하고 관련 권한을 부여한다.(Ctrl+Enter)
create database 디비이름;
create user 사용할 아이디@localhost identified by '사용할 비밀번호';
grant all privileges on 디비이름.* to 사용할 아이디@localhost;
test01 커넥션을 열고 아래와 같이 입력 후 Ctrl + Enter를 눌러준다.
use test01;
정상적으로 db 사용이 가능함을 확인한다.
application.yml 파일을 열어 DB 접속 추가정보를 작성해준다.
# 서버포트, DB 연결정보
server:
port: 8096
#포트번호는 각자 세팅이 다르기 때문에 꼭 확인해보세요.
#DB 접속 정보
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test01?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: 디비아이디
password: 디비비밀번호
thymeleaf:
cache: false
#JPA 관련 설정(Spring에 속해있는 설정으로 맨 앞칸에서 쓰면 안되고 위의 datasource와 같은 수준의 칸에서 써야 함.
jpa:
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
show-sql: true
hibernate:
ddl-auto: create
# 위의 ddl-auto: create은 시작할때 마다 다시 시작하는 의미
# DB에 데이터를 한번 만든 경우 create을 update로 변경하여 사용
# 데이터를 계속 추가하고자 하는 경우 update라는 명령어로 코딩
# ddl: data definition language
# Entity를 수정하는 경우에는 create으로 db를 다시 시작하는게 좋음
패키지를 아래와 같이 모두 생성해준다.(dto, service, entity, repository)
회원가입
index 페이지를 아래와 같이 작성한다.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>index.html</title>
</head>
<body>
<div th:align="center">
<h2>index.html</h2><br><br>
<a href="member/save">회원가입</a><br><br>
</body>
</html>
화면에 회원가입 링크가 생성되었다.
회원가입 폼을 아래와 같이 구성해본다.(비밀번호 재입력 확인란과 Email 중북여부 기능을 포함한다.)
위의 폼에 맞게 template 밑 member 폴더 밑에 save.html을 만들어준다.
[save.html]
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:font-size="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>save.html</title>
<script src="https://code.jquery.com/jquery-latest.min.js"></script>
<style>
.field-error {
color:red;
}
</style>
</head>
<body>
<div th:align="center">
<span style=" font-size:1.5em; color: black;" th:align="center" >회원 가입</span><br><br>
<form action="/member/save" method="post" enctype="multipart/form-data" th:object="${member}">
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err: ${#fields.globalErrors()}" th:text="${err}">글로벌오류</p>
</div>
이름<br>
<input type="text" th:field="*{memberName}" placeholder="이름: 2~50자로 입력해주세요">
<p th:if="${#fields.hasErrors('memberName')}" th:errors="*{memberName}" th:errorclass="field-error"></p><br><br>
비밀번호<br>
<input type="password" th:field="*{memberPw}" id="pwd1" placeholder="비밀번호: 5~20자로 입력해주세요." required>
<p th:if="${#fields.hasErrors('memberPw')}" th:errors="*{memberPw}" th:errorclass="field-error"></p><br>
비밀번호 확인<br>
<input type="password" id="pwd2" placeholder="비밀번호 확인: 비밀번호를 다시 입력해주세요." required><br><br>
<div class="alert alert-success" style="color: green" id="alert-success">비밀번호가 일치합니다.</div><br>
<div class="alert alert-danger" style="color: red" id="alert-danger">비밀번호가 일치하지 않습니다.</div>
이메일<br>
<input type="email" th:field="*{memberEmail}" onblur="emailDp()" placeholder="이메일: 5~50자로 입력해주세요"><br>
<span id="emailCheck"></span>
<p th:if="${#fields.hasErrors('memberEmail')}" th:errors="*{memberEmail}" th:errorclass="field-error"></p><br><br>
주소<br>
<input type="text" th:field="*{memberAddr}" placeholder="주소">
<p th:if="${#fields.hasErrors('memberAddr')}" th:errors="*{memberAddr}" th:errorclass="field-error"></p><br><br>
핸드폰 번호<br>
<input type="text" th:field="*{memberPhone}" placeholder="핸드폰번호: 10~11자로 숫자만 입력해주세요">
<p th:if="${#fields.hasErrors('memberPhone')}" th:errors="*{memberPhone}" th:errorclass="field-error"></p><br><br>
생년월일<br>
<input type="date" th:field="*{memberDate}" placeholder="생년월일"><br><br>
<span style=" font-size:0.8em; color: black;" th:align="center" >프로필 사진</span><br>
<input type="file" th:field="*{memberFile}" placeholder="프로필 사진"><br><br>
<input type="submit" value="회원가입">
</form>
</div>
</body>
</html>
controller 내 MemberController를 생성한 후 아래와 같이 작성한다.
package com.ex.test01.controller;
import com.ex.test01.dto.*;
import com.ex.test01.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@Controller
@RequiredArgsConstructor
@RequestMapping("/member")
public class MemberController {
private final MemberService ms;
// 회원가입 화면 보여주기
@GetMapping("/save")
public String saveForm(Model model) {
model.addAttribute("member", new MemberSaveDTO());
return "member/save";
}
}
@RequestMapping는 /member의 주소로 오는 모든 요청(get, post, put, update, delete등)을 받아줄 수 있는 어노테이션으로 이 이후 컨트롤러에서 작성되어지는 브라우저 주소 부분(만일 작성해야 할 주소가 "/member/save"라면)은 member를 생략한 하단의 주소("/save")만 기입하면 된다.
dto 패키지 內 MemberSaveDTO를 class형식으로 만들어준다.
MemberSaveDTO에 코드를 작성해준다.
package com.ex.test01.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.constraints.NotBlank;
import java.sql.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MemberSaveDTO {
@NotBlank(message = "이름은 필수입니다.")
@Length(min = 2, max = 50, message = "2~50자로 입력해주세요")
private String memberName;
@NotBlank
@Length(min = 5, max = 20, message = "5~20자로 입력해주세요")
private String memberPw;
@NotBlank(message = "Email은 필수입니다.")
@Length(min = 5, max = 50, message = "5~50자로 입력해주세요")
private String memberEmail;
// @NotBlank(message = "주소는 필수입니다.")
// @Length(min = 10, max = 50, message = "10~50자로 입력해주세요")
private String memberAddr;
@NotBlank(message = "핸드폰 번호는 필수입니다.")
@Length(min = 10, max = 11, message = "10~11자로 숫자만 입력해주세요")
private String memberPhone;
private Date memberDate;
private MultipartFile memberFile;
private String memberFilename;
}
여기까지 작성이 되면 회원가입 화면(saveForm)이 정상적으로 뜨게 된다.
이제는 화면가입에 기입된 정보가 정상적으로 DB에까지 저장되는 프로세스를 구축해본다.
MemberController에 관련 코드를 추가해준다.
// 회원가입 저장
@PostMapping("/save")
public String save(@Validated @ModelAttribute("member") MemberSaveDTO memberSaveDTO, BindingResult bindingResult) {
System.out.println("MemberController.save =" + memberSaveDTO);
if (bindingResult.hasErrors()) {
return "member/save";
}
try {
Long memberId = ms.save(memberSaveDTO);
} catch (IllegalStateException | IOException e) {
bindingResult.reject("emailCheck", e.getMessage());
return "member/save";
}
return "redirect:/";
}
service package 內 MemberService를 interface 형식으로 만들어준다.
service package 內 MemberServiceImpl을 class 형식으로 만들어준다.
MemberService에 코드를 추가해준다.
package com.ex.test01.service;
import com.ex.test01.dto.*;
import java.io.IOException;
public interface MemberService {
Long save(MemberSaveDTO memberSaveDTO) throws IOException;
}
MemberServiceImpl에 코드를 추가해준다.
package com.ex.test01.service;
import com.ex.test01.dto.*;
import com.ex.test01.entity.MemberEntity;
import com.ex.test01.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
private final MemberRepository mr;
// 회원 가입 저장
@Override
public Long save(MemberSaveDTO memberSaveDTO) throws IOException {
MultipartFile memberFile = memberSaveDTO.getMemberFile();
String memberFilename = memberFile.getOriginalFilename();
memberFilename = System.currentTimeMillis() + "-" + memberFilename;
String savePath = "F:\\Development_F\\source\\springboot\\test01\\src\\main\\resources\\templates\\img\\" + memberFilename;
if (!memberFile.isEmpty()) {
memberFile.transferTo(new File(savePath));
}
memberSaveDTO.setMemberFilename(memberFilename);
MemberEntity memberEntity = MemberEntity.saveMember(memberSaveDTO);
System.out.println("MemberServiceImpl.save");
return mr.save(memberEntity).getMemberId();
}
}
상기 코드 중 이 부분(String savePath = “F:\~~~~~”)은 각자의 PC에 저장되는 폴더의 full 주소에 역슬래쉬(\) 두개를 추가한 후 + memberFilename을 붙이는 형식으로 작성하면 된다.
entity package 內 MemberEntity를 class 형식으로 생성한다.
MemberEntity를 작성해준다.
package com.ex.test01.entity;
import com.ex.test01.dto.MemberSaveDTO;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.sql.Date;
@Entity
@Getter
@Setter
@Table(name = "member_table")
public class MemberEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="member_id")
private Long memberId;
@Column
private String memberName;
@Column
private String memberPw;
@Column
private String memberEmail;
@Column
private String memberAddr;
@Column
private String memberPhone;
@Column
private Date memberDate;
@Column
private String memberFilename;
public static MemberEntity saveMember(MemberSaveDTO memberSaveDTO) {
MemberEntity memberEntity = new MemberEntity();
memberEntity.setMemberName(memberSaveDTO.getMemberName());
memberEntity.setMemberPw(memberSaveDTO.getMemberPw());
memberEntity.setMemberEmail(memberSaveDTO.getMemberEmail());
memberEntity.setMemberAddr(memberSaveDTO.getMemberAddr());
memberEntity.setMemberPhone(memberSaveDTO.getMemberPhone());
memberEntity.setMemberDate(memberSaveDTO.getMemberDate());
memberEntity.setMemberFilename(memberSaveDTO.getMemberFilename());
return memberEntity;
}
}
repository package 內 MemberRepository를 interface형식으로 생성한다.
MemberRepository에 코드를 작성한다.
package com.ex.test01.repository;
import com.ex.test01.entity.MemberEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<MemberEntity, Long> {
}
여기까지 작성 완료 후 서버를 가동한 후 회원가입을 실제 진행해본다. mysql db에서 select * from member_table;로 가입한 회원정보가 정상적으로 들어왔다면 회원가입은 정상적으로 이뤄진 것이다.
사용자에게 비밀번호를 재입력받아 비밀번호 일치 여부를 확인해주는 코드를 작성한다. save.html 內 header 영역에 아래와 같이 scrpit를 작성한다.
<script type="text/javascript">
$(function(){
$("#alert-success").hide();
$("#alert-danger").hide();
$("input").keyup(function(){
var pwd1=$("#pwd1").val();
var pwd2=$("#pwd2").val();
if(pwd1 != "" || pwd2 != ""){
if(pwd1 == pwd2){
$("#alert-success").show();
$("#alert-danger").hide();
$("#submit").removeAttr("disabled");
}else{
$("#alert-success").hide();
$("#alert-danger").show();
$("#submit").attr("disabled", "disabled");
}
}
});
});
</script>
save.html 본문 內 비밀번호 관련 파트는 아래와 같이 작성한다.
비밀번호<br>
<input type="password" th:field="*{memberPw}" id="pwd1" placeholder="비밀번호: 5~20자로 입력해주세요." required>
<p th:if="${#fields.hasErrors('memberPw')}" th:errors="*{memberPw}" th:errorclass="field-error"></p><br>
비밀번호 확인<br>
<input type="password" id="pwd2" placeholder="비밀번호 확인: 비밀번호를 다시 입력해주세요." required><br><br>
<div class="alert alert-success" style="color: green" id="alert-success">비밀번호가 일치합니다.</div><br>
<div class="alert alert-danger" style="color: red" id="alert-danger">비밀번호가 일치하지 않습니다.</div>
Email 중복 여부 체크
Email 중복 여부 체크 기능 구현을 위해 save.html 內 header 영역에 아래와 같이 작성한다.
<script>
/* 아이디 입력을 하는 동안에 idDuplicate() 함수를 호출하고 입력된 값을 콘솔에 출력 */
function emailDp() {
const id = document.getElementById('memberEmail').value;
console.log(id);
const checkResult = document.getElementById('emailCheck');
$.ajax({
type: 'post', // 전송방식(get, post, put 등)
url: '/member/emailDp', // 요청주소(controller로 요청하는 주소)
data: {'memberEmail': id}, // 전송할 데이터
dataType: 'text', // 요청 후 리턴받을 때의 데이터 형식
success: function (result) { // 요청이 성공적으로 처리됐을때 실행 할 함수
console.log('ajax 성공');
console.log(result); // MemberController에서 넘어온 result값 찍어보기(ok or no)
if (result == "ok") {
checkResult.style.color = 'green';
checkResult.innerHTML = '멋진 아이디네요!!';
} else {
checkResult.style.color = 'red';
checkResult.innerHTML = '이미 사용중인 아이디입니다.';
}
},
error: function () { // 요청이 실패했을때 실행 할 함수
console.log('오타 찾으세요.');
}
});
}
</script>
Email 중복 여부 체크 기능 구현을 위해 save.html 內 본문 영역에 아래와 같이 작성한다.
이메일<br>
<input type="email" th:field="*{memberEmail}" onblur="emailDp()" placeholder="이메일: 5~50자로 입력해주세요"><br>
<span id="emailCheck"></span>
<p th:if="${#fields.hasErrors('memberEmail')}" th:errors="*{memberEmail}" th:errorclass="field-error"></p><br><br>
여기까지 작성 후 MemberController에 아래와 같이 작성한다.
// 이메일 중복 체크
@PostMapping("/emailDp")
@ResponseBody
public String emailDp(@RequestParam("memberEmail") String memberEmail) {
System.out.println("MemberController.emailDp :" + memberEmail);
String result = ms.emailDp(memberEmail);
return result;
}
위와 같이 작성을 하면 emailDp 부분이 빨간색으로 뜨고 그 부분을 클릭하면 MemberService에 아래와 같이 메서드가 자동으로 작성되어진다.
public interface MemberService {
String emailDp(String memberEmail);
}
위 항목에서 또 emailDp 부분이 빨간색으로 뜨고 그 부분을 클릭하면 MemberServiceImpl에 아래와 같이 메서드가 자동으로 작성되어지고 거기에 코드를 추가해준다.
// 이메일 중복
@Override
public String emailDp(String memberEmail) {
MemberEntity result = mr.findByMemberEmail(memberEmail);
if (result == null)
return "ok";
else
return "no";
}
위 항목에서 mr.findByMemberEmail 부분이 빨간색으로 뜨고 그 부분을 클릭하면 MemberRepository에 아래와 같이 메서드가 자동으로 작성되어진다. 여기까지 했으면 email 중복 체크 여부 기능이 회원가입 시 Email을 기입하자 마자 나타나는 결과메세지로 정상적으로 작동되는지 확인할 수 있다.
public interface MemberRepository extends JpaRepository<MemberEntity, Long> {
MemberEntity findByMemberEmail(String memberEmail);
}
아래는 위의 내용이 모두 포함된 save.html의 전체 코드이다.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:font-size="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>save.html</title>
<script src="https://code.jquery.com/jquery-latest.min.js"></script>
<style>
.field-error {
color:red;
}
</style>
<script>
/* 아이디 입력을 하는 동안에 idDuplicate() 함수를 호출하고 입력된 값을 콘솔에 출력 */
function emailDp() {
const id = document.getElementById('memberEmail').value;
console.log(id);
const checkResult = document.getElementById('emailCheck');
$.ajax({
type: 'post', // 전송방식(get, post, put 등)
url: '/member/emailDp', // 요청주소(controller로 요청하는 주소)
data: {'memberEmail': id}, // 전송할 데이터
dataType: 'text', // 요청 후 리턴받을 때의 데이터 형식
success: function (result) { // 요청이 성공적으로 처리됐을때 실행 할 함수
console.log('ajax 성공');
console.log(result); // MemberController에서 넘어온 result값 찍어보기(ok or no)
if (result == "ok") {
checkResult.style.color = 'green';
checkResult.innerHTML = '멋진 아이디네요!!';
} else {
checkResult.style.color = 'red';
checkResult.innerHTML = '이미 사용중인 아이디입니다.';
}
},
error: function () { // 요청이 실패했을때 실행 할 함수
console.log('오타 찾으세요.');
}
});
}
</script>
<script type="text/javascript">
$(function(){
$("#alert-success").hide();
$("#alert-danger").hide();
$("input").keyup(function(){
var pwd1=$("#pwd1").val();
var pwd2=$("#pwd2").val();
if(pwd1 != "" || pwd2 != ""){
if(pwd1 == pwd2){
$("#alert-success").show();
$("#alert-danger").hide();
$("#submit").removeAttr("disabled");
}else{
$("#alert-success").hide();
$("#alert-danger").show();
$("#submit").attr("disabled", "disabled");
}
}
});
});
</script>
</head>
<body>
<div th:align="center">
<span style=" font-size:1.5em; color: black;" th:align="center" >회원 가입</span><br><br>
<form action="/member/save" method="post" enctype="multipart/form-data" th:object="${member}">
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err: ${#fields.globalErrors()}" th:text="${err}">글로벌오류</p>
</div>
이름<br>
<input type="text" th:field="*{memberName}" placeholder="이름: 2~50자로 입력해주세요">
<p th:if="${#fields.hasErrors('memberName')}" th:errors="*{memberName}" th:errorclass="field-error"></p><br><br>
비밀번호<br>
<input type="password" th:field="*{memberPw}" id="pwd1" placeholder="비밀번호: 5~20자로 입력해주세요." required>
<p th:if="${#fields.hasErrors('memberPw')}" th:errors="*{memberPw}" th:errorclass="field-error"></p><br>
비밀번호 확인<br>
<input type="password" id="pwd2" placeholder="비밀번호 확인: 비밀번호를 다시 입력해주세요." required><br><br>
<div class="alert alert-success" style="color: green" id="alert-success">비밀번호가 일치합니다.</div><br>
<div class="alert alert-danger" style="color: red" id="alert-danger">비밀번호가 일치하지 않습니다.</div>
이메일<br>
<input type="email" th:field="*{memberEmail}" onblur="emailDp()" placeholder="이메일: 5~50자로 입력해주세요"><br>
<span id="emailCheck"></span>
<p th:if="${#fields.hasErrors('memberEmail')}" th:errors="*{memberEmail}" th:errorclass="field-error"></p><br><br>
주소<br>
<input type="text" th:field="*{memberAddr}" placeholder="주소">
<p th:if="${#fields.hasErrors('memberAddr')}" th:errors="*{memberAddr}" th:errorclass="field-error"></p><br><br>
핸드폰 번호<br>
<input type="text" th:field="*{memberPhone}" placeholder="핸드폰번호: 10~11자로 숫자만 입력해주세요">
<p th:if="${#fields.hasErrors('memberPhone')}" th:errors="*{memberPhone}" th:errorclass="field-error"></p><br><br>
생년월일<br>
<input type="date" th:field="*{memberDate}" placeholder="생년월일"><br><br>
<span style=" font-size:0.8em; color: black;" th:align="center" >프로필 사진</span><br>
<input type="file" th:field="*{memberFile}" placeholder="프로필 사진"><br><br>
<input type="submit" value="회원가입">
</form>
</div>
</body>
</html>
위 내용을 보면 알 수 있듯이 thymeleaf와 ajax 사용을 위해 jquery도 선언을 하였다.
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:font-size="http://www.w3.org/1999/xhtml">
<script src="https://code.jquery.com/jquery-latest.min.js"></script>
댓글남기기