1. 프로젝트와 DB 연결
- 로컬로 연결
- MySQL 워크벤치 이용
- 워크벤치에서 먼저 데이터베이스 "malldb"를 생성한다.
- 인텔리제이의 데이터 소스 및 드라이버에 들어간다.
- 아래 창의 데이터베이스에 워크벤치에서 생성한 DB의 이름을 넣어서 연결해준다.
- 워크벤치에서 이용한 아이디, 비밀번호를 입력해서 연결한다.
Note) 실행 결과 - 워크벤치에서 테스트 용도로 만든 테이블이 인텔리제이에서도 조회된다.
- application.yml
- 데이터베이스 연결 위한 정보를 표시한다.
- 아래와 같이 꼭 들여쓰기 한다.
<hide/>
spring:
datasource:
url : jdbc:mariadb://127.0.0.1:3306/malldb
driver-class-name: org.mariadb.jdbc.Driver
username: root
password: 비밀번호
jpa:
generate-ddl: true
hibernate:
ddl-auto: update
show-sql: true
2. 회원 가입 구현하기
- Member
<hide/>
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member implements MemberCode{
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE) // IDENTITY로 하면 오류나는 이유??
private Long id;
private String userId;
private String userName;
private String phone;
private String password;
private String rePassword;
private Long point; // 포인트 잔액
private LocalDateTime regDt; // 회원 가입일
private boolean adminYn; // 관리자 여부 - false 기본값으로 넣기
private String userStatus; // 이용 가능 / 정지
@ElementCollection(fetch = FetchType.EAGER) // 컬렉션이니까 꼭 필요!!
private List<Coupon> couponList;
// private LocalDateTime udtDt; // 회원 정보 수정일
private boolean emailAuthYn;
private LocalDateTime emailAuthDt;
private String emailAuthKey;
private String resetPasswordKey;
private LocalDateTime resetPasswordLimitDt; //초기화 ???? 가능한 날짜
// 주소
private String zipcode; // 우편 번호
private String addr; //
private String addrDetail; //
}
- MemberInput
- Member 엔티티에는 모든 정보가 있어야하고 input에는 딱 필요한 정보만 넣기 위해서 새로운 클래스를 만든다.
<hide/>
@Data
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberInput {
private String userId;
private String userName;
private String phone;
private String password;
private String rePassword;
private String userStatus;
private String zipcode; // 우편 번호
private String addr; //
private String addrDetail; //
}
- Repository
- 메서드 이름은 엔티티에 있는 멤버변수의 이름으로 구성된다.
<hide/>
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
// Optional<Member> findByEmailAuthKey(String emailAuthKey);
Optional<Member> findByUserIdAndUserName(String userId, String userName);
Optional<Member> findByResetPasswordKey(String resetPasswordKey);
Optional<Member> findByUserId(String userId);
}
- Service
- ServiceImpl: 인터페이스 Service가 아닌 실제 구현체 Impl에 @Service 붙인다.
- register() 구현하기
- 메일 보내기는 다음에 html 이용해서 구현한다.
<hide/>
@Override
public boolean register(MemberInput parameter) {
// 같은 아이디가 존재하는 경우
Optional<Member> optionalMember = memberRepository.findByUserId(parameter.getUserId());
if(optionalMember.isPresent()){ // apa
return false;
}
String encPassword = BCrypt.hashpw(parameter.getPassword(), BCrypt.gensalt());
String uuid = UUID.randomUUID().toString();
Member member = Member.builder()
.userId(parameter.getUserId())
.userName(parameter.getUserName())
.phone(parameter.getPhone())
.password(encPassword)
.regDt(LocalDateTime.now())
// .emailAuthYn(false)
// .emailAuthKey(uuid)
.userStatus(Member.MEMBER_STATUS_REQ)
.build();
memberRepository.save(member);
// 메일 보내기
// String email = parameter.getUserId();
// String subject = "fastlms 사이트 가입을 축하드립니다.";
// String text = "<p>fastlms 사이트 가입을 축하드립니다.</p>" +
// "<p>아래 링크를 클릭하셔서 가입을 완료하세요.</p>" +
// "<div><a target='_blank' href='http://localhost:8080/member/email-auth?id=" +
// uuid +
// "'>가입 완료</a></div>";
//
// mailComponents.sendMail(email, subject, text);
return true;
}
- Controller
- 다시 보니까 model.add() 부분은 딱히 필요 없다.
- 다만 주소를 검색해서 저장할 수 있도록 구현한다.
<hide/>
@Controller
@RequiredArgsConstructor
class MemberController {
private final MemberService memberService;
@GetMapping("/member/register")
public String register(){
return "member/register";
}
@PostMapping("/member/register")
public String submit(Model model
, MemberInput parameter){
boolean result = memberService.register(parameter);
// 이미 계정이 있는 경우
if(!result){
return "error/existsId";
}
model.addAttribute( "result", result); // model에 넣으면 어떻게 html로 넘어가지??
return "member/register_complete";
}
@GetMapping("/member/login")
public String login(){
return "member/login";
}
}
- HTML
- 메인
<hide/>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<h1>Online mall</h1>
<div>
<a href="/member/register">회원가입</a>|
<a href="/member/login">로그인</a>|
<a href="/member/logout">로그아웃</a>
</div>
<hr/>
</body>
</html>
- 회원가입
- Daum 주소 검색을 이용해서 구현할 계획
<hide/>
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>회원가입 페이지</title>
</head>
<body>
<h1>회원 가입</h1>
<div>
<form id = "registerForm" method="post" >
<table>
<tbody>
<tr>
<th>아이디</th>
<td> <input type = "email" name="userId" placeholder="아이디(이메일)입력" required/></td>
</tr>
<tr>
<th>이름</th>
<td><input type = "text" name="userName" placeholder="이름 입력" required/>
</td>
</tr>
<tr>
<th>연락처</th>
<td><input type = "tel" name="phone" placeholder="전화번호 입력" required/>
</td>
</tr>
<tr>
<th>비밀번호</th>
<td><input type = "password" name="password" placeholder="비밀번호 입력" required/>
</td>
</tr>
<tr>
<th>비밀번호 확인</th>
<td><input type = "password" name="password" placeholder="비밀번호 입력" required/>
</td>
</tr>
<!-- <tr>-->
<!-- <th>주소</th>-->
<!-- <td>-->
<!-- <div>-->
<!-- <input type="text" id="zipcode" name="zipcode" th:text="${member}" _readonly placeholder="우편번호 입력"/>-->
<!-- <button onclick="execDaumPostcode()" type="button">우편 번호 찾기</button>-->
<!-- </div>-->
<!-- <div>-->
<!-- <input type="text" id="addr" name="addr" th:text="${member.addr}" _readonly placeholder="주소 입력">-->
<!-- <input type="text" id="addrDetail" name="addrDetail" th:text="${member.addrDetail}" placeholder="상세 주소 입력">-->
<!-- </div>-->
<!-- </td>-->
<!-- </tr>-->
</tbody>
</table>
<button>회원가입</button>
</form>
</div>
<div id="layer" style="display:none;position:fixed;overflow:hidden;z-index:1;-webkit-overflow-scrolling:touch;">
<img src="//t1.daumcdn.net/postcode/resource/images/close.png" id="btnCloseLayer" style="cursor:pointer;position:absolute;right:-3px;top:-3px;z-index:1" onclick="closeDaumPostcode()" alt="닫기 버튼">
</div>
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<script>
// 우편번호 찾기 화면을 넣을 element
var element_layer = document.getElementById('layer');
function closeDaumPostcode() {
// iframe을 넣은 element를 안보이게 한다.
element_layer.style.display = 'none';
}
function execDaumPostcode() {
new daum.Postcode({
oncomplete: function(data) {
// 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분.
// 각 주소의 노출 규칙에 따라 주소를 조합한다.
// 내려오는 변수가 값이 없는 경우엔 공백('')값을 가지므로, 이를 참고하여 분기 한다.
var addr = ''; // 주소 변수
var extraAddr = ''; // 참고항목 변수
//사용자가 선택한 주소 타입에 따라 해당 주소 값을 가져온다.
if (data.userSelectedType === 'R') { // 사용자가 도로명 주소를 선택했을 경우
addr = data.roadAddress;
} else { // 사용자가 지번 주소를 선택했을 경우(J)
addr = data.jibunAddress;
}
// 사용자가 선택한 주소가 도로명 타입일때 참고항목을 조합한다.
if(data.userSelectedType === 'R'){
// 법정동명이 있을 경우 추가한다. (법정리는 제외)
// 법정동의 경우 마지막 문자가 "동/로/가"로 끝난다.
if(data.bname !== '' && /[동|로|가]$/g.test(data.bname)){
extraAddr += data.bname;
}
// 건물명이 있고, 공동주택일 경우 추가한다.
if(data.buildingName !== '' && data.apartment === 'Y'){
extraAddr += (extraAddr !== '' ? ', ' + data.buildingName : data.buildingName);
}
// 표시할 참고항목이 있을 경우, 괄호까지 추가한 최종 문자열을 만든다.
if(extraAddr !== ''){
extraAddr = ' (' + extraAddr + ')';
}
// 조합된 참고항목을 해당 필드에 넣는다.
// document.getElementById("sample2_extraAddress").value = extraAddr;
} else {
// document.getElementById("sample2_extraAddress").value = '';
}
// 우편번호와 주소 정보를 해당 필드에 넣는다.
document.getElementById('zipcode').value = data.zonecode;
document.getElementById("addr").value = addr;
// 커서를 상세주소 필드로 이동한다.
document.getElementById("addrDetail").focus();
// iframe을 넣은 element를 안보이게 한다.
// (autoClose:false 기능을 이용한다면, 아래 코드를 제거해야 화면에서 사라지지 않는다.)
element_layer.style.display = 'none';
},
width : '100%',
height : '100%',
maxSuggestItems : 5
}).embed(element_layer);
// iframe을 넣은 element를 보이게 한다.
element_layer.style.display = 'block';
// iframe을 넣은 element의 위치를 화면의 가운데로 이동시킨다.
initLayerPosition();
}
// 브라우저의 크기 변경에 따라 레이어를 가운데로 이동시키고자 하실때에는
// resize이벤트나, orientationchange이벤트를 이용하여 값이 변경될때마다 아래 함수를 실행 시켜 주시거나,
// 직접 element_layer의 top,left값을 수정해 주시면 됩니다.
function initLayerPosition(){
var width = 300; //우편번호서비스가 들어갈 element의 width
var height = 400; //우편번호서비스가 들어갈 element의 height
var borderWidth = 5; //샘플에서 사용하는 border의 두께
// 위에서 선언한 값들을 실제 element에 넣는다.
element_layer.style.width = width + 'px';
element_layer.style.height = height + 'px';
element_layer.style.border = borderWidth + 'px solid';
// 실행되는 순간의 화면 너비와 높이 값을 가져와서 중앙에 뜰 수 있도록 위치를 계산한다.
element_layer.style.left = (((window.innerWidth || document.documentElement.clientWidth) - width)/2 - borderWidth) + 'px';
element_layer.style.top = (((window.innerHeight || document.documentElement.clientHeight) - height)/2 - borderWidth) + 'px';
}
</script>
</body>
</html>
- 회원 가입 완료
<hide/>
<!DOCTYPE html>
<!--<html lang="en">-->
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>회원가입 완료</title>
</head>
<body>
<div th:if="${result}">
<h1>회원 가입 완료</h1>
<p>회원 가입이 완료됐습니다.</p>
</div>
<div th:if="${result eq false}">
<h1>회원 가입 결과</h1>
<p>회원 가입이 실패했습니다.</p>
</div>
</body>
</html>
Note) 실행 화면
- 메인 페인지
- 회원 가입
- 가입 실패 - 중복된 id 있는 경우
- 정상 가입
3. ERD 정리
https://www.erdcloud.com/d/fmCSy7ivx4eoWKTMR
- 점검 사항
- Member엔티티의 ID 생성 전략을 항상 쓰던 IDENTITY로 하면 오류가 나는데 SEQUENCE로 설정하면 오류가 안난다.
- 원인은?
- 주소, 메일 보내기 구현하기
- 회원 인증을 통해 회원이 되도록 구현한다.
- 재고 테이블은 크게 필요가 없
- List<Coupon> CouponList에 문제가 있는데 (일대다) @ElementCollection을 붙여도 오류가 난다.
- 전에는 애너테이션을 붙이니까 해결됐는데 이번에는 어느 부분이 문제인지 알아보기!
- Member엔티티의 ID 생성 전략을 항상 쓰던 IDENTITY로 하면 오류가 나는데 SEQUENCE로 설정하면 오류가 안난다.
'Spring Projcect > [갠플] Online-mall' 카테고리의 다른 글
[6일차] 관리자 로그인, 상품 등록 (0) | 2022.09.28 |
---|---|
[5일차] 연관 관계 매핑 - 장바구니, 쿠폰 (0) | 2022.09.27 |
[4일차] 오류 해결, 로그인 히스토리 (0) | 2022.09.24 |
[3주차] 회원 가입, 로그인 (0) | 2022.09.23 |
[1주차] 프로젝트 주제와 생성 과정 (0) | 2022.09.11 |