Spring Projcect/[갠플] Online-mall

[3주차] 회원 가입, 로그인

계란💕 2022. 9. 23. 11:34

1.  회원 가입 

  • 주소
    • impl클래스에 Member.build()하면 데이터베이스에 내용이 추가된다.
    • 컨트롤러에 "detail"이라는 이름으로 model을 넣어준다.
    • html 작업
  • 컨트롤러
    • POST에는 model이 필요하지 않을 수도 있다.
<hide/>
@GetMapping("/member/register")
public String register(){
    return "member/register";
}

@PostMapping("/member/register")
public String submit(Model model
                    , MemberInput parameter
                    , Principal principal){
    boolean result = memberService.register(parameter);

    // 이미 계정이 있는 경우
    if(!result){
        return "error/existsId";
    }

    model.addAttribute( "result", result);  // model에 넣으면 어떻게 html로 넘어가지??
//        model.addAttribute( "detail", parameter);  // model에 넣으면 어떻게 html로 넘어가지??
    return "member/register_complete";
}

 

  • html
    • 우편번호 찾기를 누르고 선택하면 자동으로 zipcode, addr에 자동으로 값이 세팅된다.
    • addrDetail에는 사용자가 상세 주소를 입력하도록 한다. 
    • 테이블 안의 변수들에 대해 Member 엔티티와 동일하게 변수명을 넣어주고 form이 제출되면 자동으로 MemberServiceImpl이 실행되면서 자동으로 repository에 멤버 엔티티가 저장된다.
<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>ID</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" readonly placeholder="우편번호 입력"/>
              <button onclick="execDaumPostcode()" type="button">우편 번호 찾기</button>
            </div>
            <div>
              <input type="text" id="addr" name="addr"  readonly placeholder="주소 입력">
              <input type="text" id="addrDetail" name="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>

 

  Note) 실행 결과

 

  • 비밀번호 확인 - equals()로 단순하게 매칭한다.

 

 

 

 

2.  로그인 

 

  • 오류  - 스프링 빈 충돌
    • 원인: 양방향 참조 SecurityConfig <=> memberServiceImpl
<hide/>

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-09-23 21:14:49.551 ERROR 8612 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  securityConfiguration defined in file [C:\Users\Ran\Desktop\R\zerobase\remote_mall\build\classes\java\main\com\example\mall\configuration\SecurityConfiguration.class]
↑     ↓
|  memberServiceImpl defined in file [C:\Users\Ran\Desktop\R\zerobase\remote_mall\build\classes\java\main\com\example\mall\service\MemberServiceImpl.class]
└─────┘


Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

 

 

 

3. 장바구니, 쿠폰 구현

 

  • Cart
    • Member의 userId와 매핑
    • List<Product>를 멤버 변수로 가지기 때문에 "cart_product_list"라는 새로운 테이블이 생성된다.
    • 고객이 상품 정보에서 장바구니에 추가 버튼을 누르면 이 리스트에 추가될 예정이다.
    • 상품 리스트 매핑 구현하기
<hide/>
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Cart {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long cartNo;

    @OneToOne(mappedBy = "cart")
    private Member member;

//    @OneToMany
//    private List<Product> productList;  // 카트 안에 있는 상품 리스트 - 크기가 상품 개수
    // 상품 총 가격은?
    // 상품이 같은 상품이 여러 개 있을 수도 있으니 Map으로 해야되는지도 확인

//    @OneToMany
//    private Map<Product, Integer> productIntegerMap;
    private String userId;
}

 

  •  Coupon
    • Member와 매핑

 

  • Category 
<hide/>
public enum Category {
    CATEGORY_OUTER,
    CATEGORY_TOP,
    CATEGORY_PANTS,
    CATEGORY_SKIRT,
    CATEGORY_SHOES,
    CATEGORY_BAG,
    CATEGORY_HEADWEAR,
    CATEGORY_UNDERWEAR,
    CATEGORY_ACCESSORY
    ;
}

 

 

 

 

  Ex) 

  • 로그인 관련해서 시큐리티컨피그 <=> 서비스 임플 클래스 사이에 의존성 주입이 cycle을 만들어서 오류가 생겼다.
  • 그래서 관련 부분을 모두 주석 처리했는데도 다음과 같은 오류가 발생
  • 오류
    • 1) CommandAcceptanceException
    • 2) SQLException:  product 테이블 제약조건에 대한 index가 빠졌다. 
    • 원인: 
    • 해결
<hide/>
org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL "alter table cart_product_list add constraint FKpjx3nast9fhr1j2i27gfrnnpw foreign key (product_list_product_id) references product (product_id)" via JDBC Statement
	at org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:67) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.applySqlString(AbstractSchemaMigrator.java:581) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.applySqlStrings(AbstractSchemaMigrator.java:526) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.applyForeignKeys(AbstractSchemaMigrator.java:452) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.performMigration(AbstractSchemaMigrator.java:263) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.doMigration(AbstractSchemaMigrator.java:123) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.performDatabaseAction(SchemaManagementToolCoordinator.java:196) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.process(SchemaManagementToolCoordinator.java:85) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:335) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:471) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1498) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:58) ~[spring-orm-5.3.22.jar:5.3.22]
	at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:365) ~[spring-orm-5.3.22.jar:5.3.22]
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409) ~[spring-orm-5.3.22.jar:5.3.22]
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396) ~[spring-orm-5.3.22.jar:5.3.22]
	at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:341) ~[spring-orm-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1154) ~[spring-context-5.3.22.jar:5.3.22]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:908) ~[spring-context-5.3.22.jar:5.3.22]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.22.jar:5.3.22]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.3.jar:2.7.3]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734) ~[spring-boot-2.7.3.jar:2.7.3]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) ~[spring-boot-2.7.3.jar:2.7.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) ~[spring-boot-2.7.3.jar:2.7.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) ~[spring-boot-2.7.3.jar:2.7.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) ~[spring-boot-2.7.3.jar:2.7.3]
	at com.example.mall.MallApplication.main(MallApplication.java:12) ~[main/:na]
Caused by: java.sql.SQLException: (conn=2164) Failed to add the foreign key constraint. Missing index for constraint 'FKpjx3nast9fhr1j2i27gfrnnpw' in the referenced table 'product'
	at org.mariadb.jdbc.export.ExceptionFactory.createException(ExceptionFactory.java:297) ~[mariadb-java-client-3.0.7.jar:na]
	at org.mariadb.jdbc.export.ExceptionFactory.create(ExceptionFactory.java:368) ~[mariadb-java-client-3.0.7.jar:na]
	at org.mariadb.jdbc.message.ClientMessage.readPacket(ClientMessage.java:137) ~[mariadb-java-client-3.0.7.jar:na]
	at org.mariadb.jdbc.client.impl.StandardClient.readPacket(StandardClient.java:833) ~[mariadb-java-client-3.0.7.jar:na]
	at org.mariadb.jdbc.client.impl.StandardClient.readResults(StandardClient.java:772) ~[mariadb-java-client-3.0.7.jar:na]
	at org.mariadb.jdbc.client.impl.StandardClient.readResponse(StandardClient.java:691) ~[mariadb-java-client-3.0.7.jar:na]
	at org.mariadb.jdbc.client.impl.StandardClient.execute(StandardClient.java:634) ~[mariadb-java-client-3.0.7.jar:na]
	at org.mariadb.jdbc.Statement.executeInternal(Statement.java:935) ~[mariadb-java-client-3.0.7.jar:na]
	at org.mariadb.jdbc.Statement.execute(Statement.java:1061) ~[mariadb-java-client-3.0.7.jar:na]
	at org.mariadb.jdbc.Statement.execute(Statement.java:452) ~[mariadb-java-client-3.0.7.jar:na]
	at com.zaxxer.hikari.pool.ProxyStatement.execute(ProxyStatement.java:94) ~[HikariCP-4.0.3.jar:na]
	at com.zaxxer.hikari.pool.HikariProxyStatement.execute(HikariProxyStatement.java) ~[HikariCP-4.0.3.jar:na]
	at org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:54) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	... 33 common frames omitted

 

 

 

 

 

 확인 사항

  • 깃에 커밋하기 전에 보안 관련 사항을 항상 체크한다. smtp관련 내용을 올리지 않도록 주의한다.
  • 장바구니에 여러 상품을 담는 경우 List<Product> 에 같은 상품이 여러 개 있으면 이를 Map으로 바꾸는 부분을 Impl에서 구현해야될 것 같다.
    • 그럼  MemberServiceImpl ? 
    • Cart도  repositoy 가 필요 ?
    • 상품 총 가격을 어떻게 나타낼 지도 생각하기
  • 재고 관리 기능
    • 품절된 상품은 매달 1일에 재입고 된다고 가정?
  • smtp 관련 이메일 전송 오류 해결하기
  • 회원 가입 시 login 시간, cartNo 들어가도록 한다.
  • 쿠폰 종류는 딱히 생각이 안 나서 알파벳으로 넣었다.
    • 회원 가입시 회원 가입 기념 쿠폰이 자동으로 지급 되도록 설정한다.
  • 로그인 히스토리 내용이 남도록 구현한다.
  • RDBMS는 정규화된 테이블 2개로 다대다 관계를 표현 불가하다고 JPA 강의에서 영한쌤이 알려주심
    • 따라서, 다대다 매핑 금지
    • 연결 테이블을 엔티티로 만들어서 쓸 수는 있다.
    • ex) Member - Product 
<hide/>
@Entity
public class MemberProduct {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name="MEMBER_ID")
    private Member member;

    @ManyToOne
    @JoinColumn(name="PRODUCT_ID")
    private Product product;
}

 

  • 배송, 결제 테이블도 필요할 것으로 보인다.
    • pay <= 1:1 => order <= 1:1 => delivery
    • 일대일 매핑
    •  

 

 

 

 

 

현재 테이블 목록