Spring Projcect/[갠플] Online-mall

[2주차] DB 연결 & 회원가입 구현

계란💕 2022. 9. 22. 11:24

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을 붙여도 오류가 난다.
      • 전에는 애너테이션을 붙이니까 해결됐는데 이번에는 어느 부분이 문제인지 알아보기!