Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public enum SuccessCode implements BaseCode {
USER_SOCIAL_SIGNIN_SUCCESS(HttpStatus.CREATED, "USER_2011", "소셜 회원가입이 완료되었습니다."),
USER_SOCIAL_LOGIN_SUCCESS(HttpStatus.CREATED, "USER_2012", "소셜 로그인이 완료되었습니다."),
USER_KEY_LOGIN_SUCCESS(HttpStatus.CREATED, "USER_2013", "KEY 로그인이 완료되었습니다."),
USER_KAKAO_LOGIN_SUCCESS(HttpStatus.CREATED, "USER_2014", "카카오 로그인이 완료되었습니다."),
USER_LOGOUT_SUCCESS(HttpStatus.OK, "USER_2001", "로그아웃 되었습니다."),
USER_REISSUE_SUCCESS(HttpStatus.OK, "USER_2002", "토큰 재발급이 완료되었습니다."),
USER_DELETE_SUCCESS(HttpStatus.OK, "USER_2003", "회원탈퇴가 완료되었습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
import com.example.silverbridgeX_user.user.converter.UserConverter;
import com.example.silverbridgeX_user.user.domain.User;
import com.example.silverbridgeX_user.user.dto.JwtDto;
import com.example.silverbridgeX_user.user.dto.KakaoDto;
import com.example.silverbridgeX_user.user.dto.UserRequestDto;
import com.example.silverbridgeX_user.user.dto.UserRequestDto.UserAddressReqDto;
import com.example.silverbridgeX_user.user.dto.UserRequestDto.UserNicknameReqDto;
import com.example.silverbridgeX_user.user.dto.UserResponseDto.GuardianMyPageResDto;
import com.example.silverbridgeX_user.user.dto.UserResponseDto.OlderMyPageResDto;
import com.example.silverbridgeX_user.user.jwt.CustomUserDetails;
import com.example.silverbridgeX_user.user.service.KakaoService;
import com.example.silverbridgeX_user.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
Expand All @@ -34,6 +38,7 @@
@RequestMapping("/members")
public class UserController {
private final UserService userService;
private final KakaoService kakaoService;

@Operation(summary = "소셜 회원가입", description = "프론트에게 유저 정보 받아 소셜 회원가입 후, 토큰 반환하는 메서드입니다.")
@ApiResponses({
Expand Down Expand Up @@ -75,6 +80,23 @@ public ApiResponse<JwtDto> socialLogin(
UserConverter.jwtDto(accessToken, refreshToken, user.getUsername()));
}

@GetMapping("/code/kakao")
@Operation(summary = "카카오 로그인 API", description = "카카오 로그인 API입니다.")
@Parameters({
@Parameter(name = "code", description = "카카오 API에 대한 response code, query parameter 입니다!")
})
public ApiResponse<?> kakaoLogin(@RequestParam("code") String code) {
String accessToken = kakaoService.getAccessTokenFromKakao(code);

KakaoDto.KakaoUserInfoResponseDTO userInfo = kakaoService.getUserInfo(accessToken);

String email = userInfo.getKakaoAccount().getEmail();

boolean isUser = userService.existByEmail(email);

return ApiResponse.onSuccess(SuccessCode.USER_KAKAO_LOGIN_SUCCESS, UserConverter.toSocialLoginResponseDTO(isUser, email));
}

@Operation(summary = "key 로그인", description = "노인의 key 로그인 후, 프론트에게 유저 정보 받아 토큰 반환하는 메서드입니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "USER_2013", description = "회원가입 & 로그인 성공"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.example.silverbridgeX_user.user.domain.User;
import com.example.silverbridgeX_user.user.dto.JwtDto;
import com.example.silverbridgeX_user.user.dto.KakaoDto;
import com.example.silverbridgeX_user.user.dto.UserRequestDto;
import com.example.silverbridgeX_user.user.dto.UserResponseDto.GuardianMyPageResDto;
import com.example.silverbridgeX_user.user.dto.UserResponseDto.OlderInfoDto;
Expand Down Expand Up @@ -54,4 +55,11 @@ public static GuardianMyPageResDto guardianMyPageResDto(User user, List<User> ol
.build();
}

public static KakaoDto.SocialLoginResponseDTO toSocialLoginResponseDTO(boolean isUser, String email) {

return KakaoDto.SocialLoginResponseDTO.builder()
.isUser(isUser)
.email(email)
.build();
}
}
223 changes: 223 additions & 0 deletions src/main/java/com/example/silverbridgeX_user/user/dto/KakaoDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package com.example.silverbridgeX_user.user.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.Email;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.Date;
import java.util.HashMap;

public class KakaoDto {
@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public static class KakaoTokenResponseDTO {
@JsonProperty("token_type")
public String tokenType;
@JsonProperty("access_token")
public String accessToken;
@JsonProperty("id_token")
public String idToken;
@JsonProperty("expires_in")
public Integer expiresIn;
@JsonProperty("refresh_token")
public String refreshToken;
@JsonProperty("refresh_token_expires_in")
public Integer refreshTokenExpiresIn;
@JsonProperty("scope")
public String scope;
}

@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public static class KakaoUserInfoResponseDTO {
@JsonProperty("id")
public Long id;

//자동 연결 설정을 비활성화한 경우만 존재.
//true : 연결 상태, false : 연결 대기 상태
@JsonProperty("has_signed_up")
public Boolean hasSignedUp;

//서비스에 연결 완료된 시각. UTC
@JsonProperty("connected_at")
public Date connectedAt;

//카카오싱크 간편가입을 통해 로그인한 시각. UTC
@JsonProperty("synched_at")
public Date synchedAt;

//사용자 프로퍼티
@JsonProperty("properties")
public HashMap<String, String> properties;

//카카오 계정 정보
@JsonProperty("kakao_account")
public KakaoAccount kakaoAccount;

//uuid 등 추가 정보
@JsonProperty("for_partner")
public Partner partner;

@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class KakaoAccount {

//프로필 정보 제공 동의 여부
@JsonProperty("profile_needs_agreement")
public Boolean isProfileAgree;

//닉네임 제공 동의 여부
@JsonProperty("profile_nickname_needs_agreement")
public Boolean isNickNameAgree;

//프로필 사진 제공 동의 여부
@JsonProperty("profile_image_needs_agreement")
public Boolean isProfileImageAgree;

//사용자 프로필 정보
@JsonProperty("profile")
public Profile profile;

//이름 제공 동의 여부
@JsonProperty("name_needs_agreement")
public Boolean isNameAgree;

//카카오계정 이름
@JsonProperty("name")
public String name;

//이메일 제공 동의 여부
@JsonProperty("email_needs_agreement")
public Boolean isEmailAgree;

//이메일이 유효 여부
// true : 유효한 이메일, false : 이메일이 다른 카카오 계정에 사용돼 만료
@JsonProperty("is_email_valid")
public Boolean isEmailValid;

//이메일이 인증 여부
//true : 인증된 이메일, false : 인증되지 않은 이메일
@JsonProperty("is_email_verified")
public Boolean isEmailVerified;

//카카오계정 대표 이메일
@JsonProperty("email")
public String email;

//연령대 제공 동의 여부
@JsonProperty("age_range_needs_agreement")
public Boolean isAgeAgree;

//연령대
//참고 https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info
@JsonProperty("age_range")
public String ageRange;

//출생 연도 제공 동의 여부
@JsonProperty("birthyear_needs_agreement")
public Boolean isBirthYearAgree;

//출생 연도 (YYYY 형식)
@JsonProperty("birthyear")
public String birthYear;

//생일 제공 동의 여부
@JsonProperty("birthday_needs_agreement")
public Boolean isBirthDayAgree;

//생일 (MMDD 형식)
@JsonProperty("birthday")
public String birthDay;

//생일 타입
// SOLAR(양력) 혹은 LUNAR(음력)
@JsonProperty("birthday_type")
public String birthDayType;

//성별 제공 동의 여부
@JsonProperty("gender_needs_agreement")
public Boolean isGenderAgree;

//성별
@JsonProperty("gender")
public String gender;

//전화번호 제공 동의 여부
@JsonProperty("phone_number_needs_agreement")
public Boolean isPhoneNumberAgree;

//전화번호
//국내 번호인 경우 +82 00-0000-0000 형식
@JsonProperty("phone_number")
public String phoneNumber;

//CI 동의 여부
@JsonProperty("ci_needs_agreement")
public Boolean isCIAgree;

//CI, 연계 정보
@JsonProperty("ci")
public String ci;

//CI 발급 시각, UTC
@JsonProperty("ci_authenticated_at")
public Date ciCreatedAt;

@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Profile {

//닉네임
@JsonProperty("nickname")
public String nickName;

//프로필 미리보기 이미지 URL
@JsonProperty("thumbnail_image_url")
public String thumbnailImageUrl;

//프로필 사진 URL
@JsonProperty("profile_image_url")
public String profileImageUrl;

//프로필 사진 URL 기본 프로필인지 여부
//true : 기본 프로필, false : 사용자 등록
@JsonProperty("is_default_image")
public String isDefaultImage;

//닉네임이 기본 닉네임인지 여부
//true : 기본 닉네임, false : 사용자 등록
@JsonProperty("is_default_nickname")
public Boolean isDefaultNickName;

}
}

@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Partner {
//고유 ID
@JsonProperty("uuid")
public String uuid;
}
}

@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class SocialLoginResponseDTO {
private boolean isUser;
@Email
private String email;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.example.silverbridgeX_user.user.service;

import com.example.silverbridgeX_user.user.dto.KakaoDto;
import io.netty.handler.codec.http.HttpHeaderValues;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Slf4j
@RequiredArgsConstructor
@Service
public class KakaoService {

private String clientId;
private final String KAUTH_TOKEN_URL_HOST ;
private final String KAUTH_USER_URL_HOST;

@Autowired
public KakaoService(@Value("${kakao.client_id}") String clientId) {
this.clientId = clientId;
KAUTH_TOKEN_URL_HOST ="https://kauth.kakao.com";
KAUTH_USER_URL_HOST = "https://kapi.kakao.com";
}

public String getAccessTokenFromKakao(String code) {

KakaoDto.KakaoTokenResponseDTO kakaoTokenResponseDto = WebClient.create(KAUTH_TOKEN_URL_HOST).post()
.uri(uriBuilder -> uriBuilder
.scheme("https")
.path("/oauth/token")
.queryParam("grant_type", "authorization_code")
.queryParam("client_id", clientId)
.queryParam("code", code)
.build(true))
.header(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString())
.retrieve()
//TODO : Custom Exception
.onStatus(HttpStatusCode::is4xxClientError, clientResponse -> Mono.error(new RuntimeException("Invalid Parameter")))
.onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new RuntimeException("Internal Server Error")))
.bodyToMono(KakaoDto.KakaoTokenResponseDTO.class)
.block();


log.info(" [Kakao Service] Access Token ------> {}", kakaoTokenResponseDto.getAccessToken());
log.info(" [Kakao Service] Refresh Token ------> {}", kakaoTokenResponseDto.getRefreshToken());
//제공 조건: OpenID Connect가 활성화 된 앱의 토큰 발급 요청인 경우 또는 scope에 openid를 포함한 추가 항목 동의 받기 요청을 거친 토큰 발급 요청인 경우
log.info(" [Kakao Service] Id Token ------> {}", kakaoTokenResponseDto.getIdToken());
log.info(" [Kakao Service] Scope ------> {}", kakaoTokenResponseDto.getScope());

return kakaoTokenResponseDto.getAccessToken();
}

public KakaoDto.KakaoUserInfoResponseDTO getUserInfo(String accessToken) {

KakaoDto.KakaoUserInfoResponseDTO userInfo = WebClient.create(KAUTH_USER_URL_HOST).get()
.uri(uriBuilder -> uriBuilder
.scheme("https")
.path("/v2/user/me")
.build(true))
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) // access token 인가
.header(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString())
.retrieve()
//TODO : Custom Exception
.onStatus(HttpStatusCode::is4xxClientError, clientResponse -> Mono.error(new RuntimeException("Invalid Parameter")))
.onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new RuntimeException("Internal Server Error")))
.bodyToMono(KakaoDto.KakaoUserInfoResponseDTO.class)
.block();

log.info("[ Kakao Service ] Auth ID ---> {} ", userInfo.getId());
log.info("[ Kakao Service ] NickName ---> {} ", userInfo.getKakaoAccount().getProfile().getNickName());
log.info("[ Kakao Service ] Email ---> {} ", userInfo.getKakaoAccount().getEmail());

return userInfo;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ public User findByEmail(String email) {
.orElseThrow(() -> new GeneralException(ErrorCode.USER_NOT_FOUND_BY_EMAIL));
}

@Transactional
public boolean existByEmail(String email) {
return userRepository.existsByEmail(email);
}


@Transactional
public Boolean checkMemberByEmail(String email) {
return userRepository.existsByEmail(email);
Expand Down
Loading