JS

[Next JS] Commerce Project #6

상도동 카르마 2023. 4. 15. 20:05

What to do?

Google OAuth

회원가입 기능(Sign Up) 만들기


데모

 

  • 로그인 버튼 클릭

 

  • 구글 로그인 팝업창이 뜨면, 로그인

  • 로그인 성공

 

 

  • 새로고침하면 로그인된걸 볼 수 있음

 

  • 데이터베이스 실제 정보가 들어간걸 확인 할 수 있음

my sql


Flow

  1. Google Login
    • Google OAuth를 사용해 credential을 가져옴
  2. View에서 Next JS API 요청
    • End Point : /api/auth/google-sign-in
    • method : GET
    • parameter : credential
  3. Nest JS에서 Spring 서버로 POST request
    • End Point : localhost:8080/api/user/signIn
    • payload : 회원정보 (username/email/imgUrl)
  4. Spring 서버에서 받은 회원정보로 SignIn 처리
    • 이미 존재하는 이메일 - 이메일로 회원정보 조회 후, 회원정보 return
    • 존재하지 않는 이메일 - DB에 회원정보 저장(회원가입 처리) 후, 회원정보 return
  5. Spring 서버에서 Next JS로 회원정보 return

 


Back End

 

  • 유저 권한
    • 향후 유저 권한을 정의할 수 있도록 만든 필드
@Getter
@AllArgsConstructor
public enum UserRole {
    USER("ROLE_USER"),
    MANAGER("ROLE_MANGER"),
    ADMIN("ROLE_ADMIN");
    private final String name;
}

 

  • 유저 상태
@Getter
@AllArgsConstructor
public enum UserStatus {
    ACTIVE("활동중인 유저"),
    BLOCKED("차단된 유저"),
    DEACTIVATED("비활성화된 유저"),
    REMOVED("회원탈퇴한 유저");
    private final String description;
}

 

 

  • 유저 Entity
    • id
    • username
    • imgUrl : 프로필 사진
    • password
    • user role
    • user status
@Entity
@Getter
@Table(name = "USER_ACCOUNT")
@SQLDelete(sql = "UPDATE USER_ACCOUNT SET removed_at = NOW() WHERE id=?")
@Where(clause = "removed_at is NULL")
@EntityListeners(AuditingEntityListener.class)
public class UserAccountEntity {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true) @Setter
    private String username;
    @Column(unique = true) @Setter
    private String email;
    @Column(name = "img_url") @Setter
    private String imgUrl;
    @Column @Setter
    private String password;
    @Enumerated(EnumType.STRING) @Setter
    private UserRole userRole = UserRole.USER;
    @Enumerated(EnumType.STRING) @Setter
    private UserStatus userStatus = UserStatus.ACTIVE;

    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) @CreatedDate
    @Column(updatable = false, name = "created_at")
    private LocalDateTime createdAt;
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) @LastModifiedDate
    @Column(name = "modified_at")
    private LocalDateTime modifiedAt;
    @Column(name = "removed_at") @Setter
    private LocalDateTime removedAt;

    private UserAccountEntity(String username, String email, String imgUrl, String password, UserRole userRole, UserStatus userStatus) {
        this.username = username;
        this.email = email;
        this.imgUrl = imgUrl;
        this.userRole = userRole;
        this.userStatus = userStatus;
    }

    protected UserAccountEntity(){}

    public static UserAccountEntity of(String username, String email, String imgUrl, String password, UserRole userRole, UserStatus userStatus){
        return new UserAccountEntity(username, email, imgUrl, password, userRole, userStatus);
    }
}

 

  • User Dto
public record UserAccountDto(
        Long id,
        String username,
        String email,
        String imgUrl,
        String password,
        UserRole userRole,
        UserStatus userStatus,
        LocalDateTime createdAt,
        LocalDateTime modifiedAt,
        LocalDateTime removedAt
) {
    public static UserAccountDto of(
            String username,
            String email,
            String imgUrl,
            String password,
            UserRole userRole,
            UserStatus userStatus
    ) {
        return new UserAccountDto(
                null,
                username,
                email,
                imgUrl,
                password,
                userRole,
                userStatus,
                null,
                null,
                null
        );
    }

    public static UserAccountDto from(UserAccountEntity entity) {
        return new UserAccountDto(
                entity.getId(),
                entity.getUsername(),
                entity.getEmail(),
                entity.getImgUrl(),
                entity.getPassword(),
                entity.getUserRole(),
                entity.getUserStatus(),
                entity.getCreatedAt(),
                entity.getModifiedAt(),
                entity.getRemovedAt()
        );
    }
}

 

  • Repository
    • findByEmail : Email로 해당 유저가 있는지 조회
@Repository
public interface UserAccountRepository extends JpaRepository<UserAccountEntity, Long> {

    Optional<UserAccountEntity> findByEmail(String email);

}

 

  • Service
    • Email로 이미 존재하는 회원인지 확인
      • 이미 존재하는 이메일 → DB에서 해당 회원 정보 return
      • 존재하지 않는 이메일  → DB에 회원정보 저장
    • 아직 비밀번호로 해당 유저가 맞는지 판단하는 기능은 구현하지 않음
@Service
@RequiredArgsConstructor
public class UserAccountService {
    private final UserAccountRepository userAccountRepository;

    public UserAccountDto signUp(String username, String email, String imgUrl) {
        return UserAccountDto.from(userAccountRepository.findByEmail(email)
                .orElseGet(() -> userAccountRepository.save(
                        UserAccountEntity.of(username, email, imgUrl, null, UserRole.USER, UserStatus.ACTIVE)
                )));
    }
}

 

  • Request
@Data
public class SignUpRequest {
    private String username;
    private String email;
    private String imgUrl;
    private String password;
}

 

  • Controller
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/user")
public class UserAccountController {
    private final UserAccountService userAccountService;

    @PostMapping("/signUp")
    public UserAccountDto signUp(@RequestBody SignUpRequest req){
        return userAccountService.signUp(req.getUsername(), req.getEmail(), req.getImgUrl());
    }
}

Front End

 

  • MyGoogleSignUp.ts
    • Sign Up 버튼
export default function MyGoogleSignUp() {
  const handleSuccess = (credentialResponse: CredentialResponse): void => {
    const endPoint = `/api/auth/google-sign-up?credential=${credentialResponse.credential}`
    fetch(endPoint)
      .then(res => res.json())
      .then((data) => {
        console.log(data)
      })
  }
  const handleError = () => {
    console.error('error')
  }

  return (
    <GoogleLogin onSuccess={handleSuccess} onError={handleError}></GoogleLogin>
  )
}

 

  • api/auth/sign-in.ts
    • Request
      • End point : localhost:8080/api/user/signIn(Spring 서버)
      • mehtod : POST
      • payload : username, email, imgUrl
    • Response : Spring 서버로부터 받은 회원정보
import type { NextApiRequest, NextApiResponse } from 'next'
import axios from 'axios'
import jwtDecode from 'jwt-decode'

type Data = {
  message : String,
}

type DecodedData = {
  aud:String,
  azp:String,
  email:String,
  email_verified:Boolean,
  exp: Number,
  family_name:String,
  given_name:String,
  iat:Number,
  iss:String,
  jti:String,
  name:String,
  nbf:Number,
  picture:String,
  sub:String
}

async function signUp(credential:string) {

    const decoded : DecodedData = jwtDecode(credential)
    const data = {
      username : `GOOGLE_${decoded.name}`,
      email :decoded.email,
      imgUrl : decoded.picture,
      password : null
    }

    try {
      await axios.post("http://localhost:8080/api/user/signUp", data)
      .then(console.log)
      
    } catch (err) {
      console.log(err);
    }
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
    const {credential} = req.query
  try {
    await signUp(String(credential))
    res.status(200).json({ message : 'Sign-in success'})
  } catch (e) {     
    console.error(e)
    return res.status(400).json({ message: 'Fail to login'})
  }
}