[회원관리] 회원가입

2022. 12. 30. 21:25프로젝트/라이어게임

728x90
SMALL

회원가입 API

기능 Method URL Request Response 비고
회원가입 POST /lier/signup {
”email”:””,
”password”:””,
”passwordConfirm”:””,
”nickname”:””
}
data{
”회원가입 성공 ” }
- 회원가입 성공 시, nickname 뒤에 난수 키워드가 붙어서 저장됨
  1.  post 형식
  2. Request 요청은 json형태의 DTO 객체를 이용해 요청하였고, 필요한 정보는 이메일, 비밀번호, 재확인용 비밀번호, 닉네임 이렇게 네가지이다.
  3. Response 응답 값은 json 형태의 data 에 "회원가입 성공" 이라는 성공 문구가 나오면 정상완료되었다는 뜻이다.
  4. 회원가입 성공을 하게되면 각 유저의 구별을 위해 닉네임 뒤에 난수값을 붙여서 생성되게 된다.

회원가입 시 저장될 Member Entity 부터 생성하도록 한다.

 

Member

package com.example.finalproject.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import org.hibernate.Hibernate;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.persistence.*;
import java.util.List;
import java.util.Objects;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Member extends Timestamped{

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

    @Column(nullable = false)
    private String email;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String nickname;

    @Column(nullable = false)
    private Long winNum;

    @Column(nullable = false)
    private Long winLIER;

    @Column(nullable = false)
    private Long winCITIZEN;

    @Column(nullable = false)
    private Long lossNum;

    @Column(nullable = false)
    private Long lossLIER;

    @Column(nullable = false)
    private Long lossCITIZEN;

    @JsonIgnore
    @OneToMany(mappedBy = "member",fetch = FetchType.LAZY,cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Post> posts;

    @JsonIgnore
    @OneToMany(mappedBy = "member",fetch = FetchType.LAZY,cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Comment> comments;

    @JsonIgnore
    @JoinColumn(name="gameroommember_id")
    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private GameRoomMember gameRoomMember;

    @JsonIgnore
    @OneToOne(mappedBy = "member",cascade = CascadeType.ALL, orphanRemoval = true)
    private MemberActive memberActive;

    @JsonIgnore
    @JoinColumn(name="memberreward_id")
    @OneToOne
    private MemberReward memberReward;

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) {
            return false;
        }
        Member member = (Member) o;
        return memberId != null && Objects.equals(memberId, member.memberId);
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }

    public boolean validatePassword(PasswordEncoder passwordEncoder, String password) {
        return passwordEncoder.matches(password, this.password);
    }

}
  • 유저의 정보에 들어갈 주요 내용으로는 
    • memberId : 유저 고유 ID
    • email : 유저 이메일
    • password : 계정 비밀번호
    • nickname : 닉네임
    • winNum : 총 승리 전적 (초기값 0)
    • winLIER : 라이어로써 승리한 전적 (초기값 0)
    • winCITIZEN : 시민으로써 승리한 전적 (초기값 0)
    • lossNum : 총 패배 전적 (초기값 0)
    • lossLIER : 라이어로써 패배한 전적 (초기값 0)
    • lossCITIZEN : 시민으로써 패배한 전적 (초기값 0)
  • Post, Comment, GameRoomMember, MemberActive, MemberReward 는 연관관계를 맺은 DB들이다.
  • MemberActive는 회원가입으로 계정이 새로 생성이 된다면 같이 생성이 되는 유저의 활동 이력을 저장하는 DB 이다.

 

 

MemberRepository

package com.example.finalproject.repository;

import com.example.finalproject.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {

    Optional<Member> findByEmail(String email);

}
  • Member 전용 Repository를 생성한다.
  • QueryDSL만 사용하는 것이 아니라 왜 JPA를 추가로 사용하느냐? 
    • JPA가 단순 save()명령어로 저장을 쉽게 할 수 있기 때문이다. 이후 모든 entity에 저장을 하게 될 경우 JPA save()명령어를 사용하게 될것이다.

 

 

MemberActive

package com.example.finalproject.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDateTime;

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Builder
@Entity
public class MemberActive {

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

    @Column(nullable = false)
    private Long createNum;

    @Column(nullable = false)
    private Long ownerNum;

    @Column(nullable = false)
    private Long enterNum;

    @Column(nullable = false)
    private Long exitNum;

    @Column(nullable = false)
    private Long gamestartNum;

    @Column(nullable = false)
    private Long gamereadyNum;

    @Column(nullable = false)
    private Long voteNum;

    @Column(nullable = false)
    private Long correctanswerNum;

    @Column
    private LocalDateTime starttime;

    @Column
    private LocalDateTime endplaytime;

    @Column
    private Long playhour;

    @Column
    private Long playminute;

    @OneToOne(fetch = FetchType.LAZY)
    private Member member;
}
  • 유저 활동 이력으로써 들어갈 주요 내용은
    • memberactiveId : 유저 활동이력 고유 ID
    • createNum : 유저가 방을 만든 활동이력 (초기값 0)
    • ownerNum : 유저가 방장이 된 활동이력 (초기값 0)
    • enterNum : 유저가 방에 입장한 횟수 (초기값 0)
    • exitNum : 유저가 방을 나간 횟수 (초기값 0)
    • gamestartNum : 방장만이 게임시작을 할 수 있는데, 따라서 방장으로써 게임시작을 진행한 횟수 (초기값 0)
    • gamereadyNum : 게임준비 한 횟수 (초기값 0)
    • voteNum : 투표한 횟수 (초기값 0)
    • correctanswerNum : 라이어로써 정답을 맞춘 횟수 (초기값 0)
    • starttime : 유저가 게임을 플레이할 때마다 시작한 시간대를 저장
    • endplaytime : 게임종료 시 게임이 종료된 시간대를 저장
    • playhour : 유저가 플레이한 시간 (단위 - 시간)
    • playMinute : 유저가 플레이한 시간 (단위 - 분)
  • Member 한명 당 하나의 MemberActive가 생성이 되야하므로 연관관계 형식은 OneToOne 으로써 맺는다.

 

 

MemberActiveRepository

package com.example.finalproject.repository;

import com.example.finalproject.domain.MemberActive;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MemberActiveRepository extends JpaRepository<MemberActive, Long> {
}
  • MemberActive 전용 Repository를 생성한다. (저장용으로만 사용)

 

 

Timestamped

package com.example.finalproject.domain;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {

  @JsonFormat(timezone = "Asia/Seoul")
  @CreatedDate
  private LocalDateTime createdAt;

  @JsonFormat(timezone = "Asia/Seoul")
  @LastModifiedDate
  private LocalDateTime modifiedAt;

}

유저가 생성이 되면 언제 생성이 되었는지에 대한 시간을 알기 위해 Timestamped를 생성하여 자동으로 생성시간이 저장되게 해주었다.

  • @EntityListeners(AuditingEntityListener.class) : 해당 클래스에 Auditing 기능을 포함시켜준다. Auditing 기능은 자동으로 반영되어 저장할 수 있게끔하는 기능이다.
  • @MappedSuperclass : JPA Entity 클래스들이 해당 추상 클래스를 상속할 경우 createDate, modifiedDate를 컬럼으로 인식할 수 있게 한다.
  • @CreatedDate : Entity가 생성되어 저장될 때 생성시간이 자동 저장된다.
  • @LastModifiedDate : 조회한 Entity의 값을 변경할 때 수정시간이 자동 저장된다.
  • @JsonFormat(timezone = "Asia/Seoul") : JSON 응답값의 형식을 지정할 때 사용한다. 날짜 형식을 아시아 서울 시간대로 맞춘다는 뜻이다.

Auditing 기능을 사용하여 자동으로 반영되게 하려면 추가적으로 application 실행 객체에서 @EnableJpaAuditing을 설정해주어야 한다.

 

application

package com.example.finalproject;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.socket.config.annotation.EnableWebSocket;

import javax.annotation.PostConstruct;
import java.util.TimeZone;

@EnableJpaAuditing
@SpringBootApplication
@EnableWebSocket
public class FinalprojectApplication {


    //@PostConstruct는 Bean이 완전히 초기화 된 후,단 한번만 호출되는 메서드 이다.
    //애플리케이션이 처음 구동될때 한번 실행된다
    @PostConstruct
    public void started() {
        // timezone 세팅
        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
    }

    public static void main(String[] args) {

        SpringApplication.run(FinalprojectApplication.class, args);
        System.out.println("실행 확인~~~~~~~");
    }

}

 

이로써 유저 정보가 저장될 entity 생성은 완료했다.

이제 본격적으로 회원가입에 대한 api를 구성해보도록 하자.

 


 

MemberController

package com.example.finalproject.controller;

import com.example.finalproject.configuration.SwaggerAnnotation;
import com.example.finalproject.controller.request.LoginRequestDto;
import com.example.finalproject.controller.request.MemberRequestDto;
import com.example.finalproject.exception.PrivateResponseBody;
import com.example.finalproject.service.MemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


@Slf4j
@RequiredArgsConstructor
@RequestMapping("/lier")
@Controller
public class MemberController {

    private final MemberService memberService;

    // 회원가입
    @PostMapping("/signup")
    public ResponseEntity<PrivateResponseBody> signup(
            @RequestBody MemberRequestDto memberRequestDto){
        // 회원가입 진행시 재확인용 비밀번호는 입력하지 않는지 확인

        log.info("회원가입 - 이메일 : {}, 닉네임 : {}, 패스워드 : {}", memberRequestDto.getEmail(), memberRequestDto.getNickname(), memberRequestDto.getPassword());
        return memberService.signup(memberRequestDto);
    }
}

회원가입을 구현 MemberController이다.

앞으로 회원가입 뿐만이 아니라 로그인, 로그아웃과 같은 회원관리 api 들의 이벤트들을 관리할 컨트롤러 단이다.

 

  • @Slf4j : 로그 형식으로 출력값을 확인하기 위한 어노테이션이다.
  • @RequiredArgsConstructor : 초기화 되지않은 final 필드나, @NonNull 이 붙은 필드에 대해 생성자를 생성해준다. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용되곤 한다.
  • @RequestMapping : 컨트롤러에 있는 api들의 공통 호출 주소를 기입함으로써, 각 api 들이 호출될때 일일히 해당 공통 주소부분을 기입하지 않아도 되도록 해준다.
  • @Controller : 해당 클래스가 컨트롤러라는 것을 명시하는 어노테이션이다. 이제 동작에 대한 이벤트가 발생하게 된다면 해당 컨트롤러로 넘어가서 동작을 관리하게 될 것이다.
  • @PostMapping : post형식으로 기입한 url주소로 호출된다는 것을 뜻한다.
  • @RequestBody : 단순 값으로 요청하는 것이 아니라 DTO와 같은 객체를 통해 객체에 존재하는 Body(변수값)으로 요청하고자하는 어노테이션이다.실질적으로 로직이 동작되는 Service 단에 DTO 객체를 넘겨준다.
  • 반환값 타입은 ResponseEntity를 사용하였다. ResponseEntity는 제네릭을 사용함으로써 일일히 반환되어야하는 변수타입을 지정하지 않아도 된다.

 

MemberService

package com.example.finalproject.service;

import com.example.finalproject.controller.request.LoginRequestDto;
import com.example.finalproject.controller.request.MemberRequestDto;
import com.example.finalproject.controller.request.TokenDto;
import com.example.finalproject.domain.Member;
import com.example.finalproject.domain.MemberActive;
import com.example.finalproject.domain.MemberReward;
import com.example.finalproject.exception.PrivateResponseBody;
import com.example.finalproject.exception.StatusCode;
import com.example.finalproject.jwt.TokenProvider;
import com.example.finalproject.repository.MemberActiveRepository;
import com.example.finalproject.repository.MemberRepository;
import com.example.finalproject.repository.MemberRewardRepository;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Optional;

import static com.example.finalproject.domain.QMember.member;
import static com.example.finalproject.domain.QMemberActive.memberActive;
import static com.example.finalproject.domain.QMemberReward.memberReward;

@Slf4j
@RequiredArgsConstructor
@Service
public class MemberService {

    private final TokenProvider tokenProvider;
    private final PasswordEncoder passwordEncoder;
    private final JPAQueryFactory jpaQueryFactory;
    private final MemberRepository memberRepository;
    private final MemberActiveRepository memberActiveRepository;


    // 회원가입
    public ResponseEntity<PrivateResponseBody> signup(MemberRequestDto memberRequestDto) {

        // 아이디 중복 확인
        if (null != isPresentMember(memberRequestDto.getEmail())) {
            return new ResponseEntity<>(new PrivateResponseBody
                    (StatusCode.DUPLICATED_EMAIL, null), HttpStatus.BAD_REQUEST);
        }

        // 비밀번호 중복 확인
        if (!memberRequestDto.getPassword().equals(memberRequestDto.getPasswordConfirm())) { // 비밀번호 encode 하기 이전에 비교
            return new ResponseEntity<>(new PrivateResponseBody
                    (StatusCode.DUPLICATED_PASSWORD, null), HttpStatus.BAD_REQUEST);
        }

        // 닉네임 중복 확인
        if (!(jpaQueryFactory
                .selectFrom(member)
                .where(member.nickname.eq(memberRequestDto.getNickname()))
                .fetchOne() == null)) {
            return new ResponseEntity<>(new PrivateResponseBody
                    (StatusCode.DUPLICATED_NICKNAME, null), HttpStatus.BAD_REQUEST);
        }

        // 회원 정보 저장
        Member member = Member.builder()
                .email(memberRequestDto.getEmail()) // 이메일
                .password(passwordEncoder.encode(memberRequestDto.getPassword())) // 비밀번호 인코딩하여 저장
                .nickname(memberRequestDto.getNickname() + "#" + (int) (Math.random() * 9999)) // 닉네임 + 고유 숫자값 부여
                .winNum(0L) // 전체 승리 횟수
                .lossNum(0L) // 전체 패배 횟수
                .winCITIZEN(0L) // 시민으로써 승리한 횟수
                .winLIER(0L) // 라이어로써 승리한 횟수
                .lossCITIZEN(0L) // 시민으로써 패배한 횟수
                .lossLIER(0L) // 라이어로써 패배한 횟수
                .build();

        memberRepository.save(member);

        // 유저 활동 기록 초기화 (업적용)
        MemberActive memberActive = MemberActive.builder()
                .createNum(0L) // 방 생성 횟수
                .ownerNum(0L) // 방장이 된 횟수
                .enterNum(0L) // 방에 들어간 횟수
                .exitNum(0L) // 방을 나간 횟수
                .gamereadyNum(0L) // 게임준비 한 횟수
                .gamestartNum(0L) // 게임시작 한 횟수
                .voteNum(0L) // 투표한 횟수
                .correctanswerNum(0L) // 정답을 맞춘 횟수
                .starttime(null)
                .endplaytime(null)
                .playhour(0L)
                .playminute(0L)
                .member(member)
                .build();

        // 활동 기록 초기화 저장
        memberActiveRepository.save(memberActive);

        return new ResponseEntity<>(new PrivateResponseBody
                (StatusCode.OK, "회원가입 성공"), HttpStatus.OK);
    }
}
  • isPresentMember 에 DTO로 전달받은 유저의 이메일을 넣어 이미 존재하는 계정이 있다면 중복된 유저로 판단하고 오류 처리를 한다.
  • DTO로 전달받은 password와 passwordconfirm의 값이 동일하다면 정상, 동일하지 않다면 잘못된 정보입력으로 판단하여 오류 처리를 한다.
  • 초기에 QueryDSL을 세팅을 하였고, JPAQueryFactory 의존성 주입하여 Query문을 구성하여 실행한다.
jpaQueryFactory
        .selectFrom(member) // member 테이블에 대해서 조회를 한다.
        .where(member.nickname.eq(memberRequestDto.getNickname())) // member 닉네임 컬럼 속성에서 DTO로 전달받은 닉네임값과 일치하는 데이터가 있는지에 대한 조건
        .fetchOne() // fetchone은 하나의 데이터만을 조회한다.
  • 위의 조건대로 이미 동일한 닉네임을 가지고 있는지 확인하여 중복된다면 오류 처리를 한다.
  • 이메일 중복, 비밀번호 재확인, 닉네임 중복의 조건들을 무사히 통과하게 된다면 builder 로 Member에 유저 정보를 저장하고, 동시에 각 유저마다 가지고 있는 활동이력 정보(MemberActive)도 builder를 통해 생성하여 저장한다.
  • 정상적으로 회원가입이 완료가 되었다면 "회원가입 성공" 문구 반환한다.

 

회원가입 성공!

다음 시간은 로그인 구현을 정리해보자

728x90
반응형
LIST

'프로젝트 > 라이어게임' 카테고리의 다른 글

[회원관리] 로그인  (0) 2023.01.03
프로젝트 세팅  (0) 2022.12.30
[회원관리] Jwt를 활용한 회원관리 기능 세팅  (0) 2022.12.28
서비스 아키텍쳐  (0) 2022.12.15