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
22 changes: 21 additions & 1 deletion docker-compose.yml
Comment thread
nuj1min marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,25 @@ services:
timeout: 5s
retries: 10

redis:
image: redis:7.4-alpine
container_name: yeogido-redis
restart: unless-stopped

ports:
- "6379:6379"

command: ["redis-server", "--appendonly", "yes"]

volumes:
- redis-data:/data

healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10

volumes:
mysql-data:
mysql-data:
redis-data:
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.yeogido.backend.domain.auth.client;

import com.yeogido.backend.domain.auth.dto.SocialUserInfo;
import com.yeogido.backend.domain.auth.exception.AuthErrorCode;
import com.yeogido.backend.global.exception.GeneralException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;

@Component
@RequiredArgsConstructor
public class KakaoUserInfoClient {

private final RestClient.Builder restClientBuilder;

@Value("${app.oauth.kakao.user-info-uri}")
private String userInfoUri;

public SocialUserInfo getUserInfo(String accessToken) {
try {
KakaoUserInfoResponse response = restClientBuilder.build()
.get()
.uri(userInfoUri)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.retrieve()
.body(KakaoUserInfoResponse.class);

if (response == null || response.id() == null) {
throw new GeneralException(AuthErrorCode.INVALID_SOCIAL_ACCESS_TOKEN);
}

return response.toSocialUserInfo();
} catch (RestClientException e) {
throw new GeneralException(AuthErrorCode.INVALID_SOCIAL_ACCESS_TOKEN);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.yeogido.backend.domain.auth.client;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.yeogido.backend.domain.auth.dto.SocialUserInfo;
import com.yeogido.backend.domain.auth.enums.SocialProvider;

public record KakaoUserInfoResponse(
Long id,
@JsonProperty("kakao_account")
KakaoAccount kakaoAccount,
Properties properties
) {

public SocialUserInfo toSocialUserInfo() {
String email = kakaoAccount == null ? null : kakaoAccount.email();
String name = kakaoAccount == null ? null : kakaoAccount.name();
if (name == null && properties != null) {
name = properties.nickname();
}
String profileImageUrl = properties == null ? null : properties.profileImage();

return new SocialUserInfo(
SocialProvider.KAKAO,
String.valueOf(id),
email,
name,
profileImageUrl
);
}

public record KakaoAccount(
String email,
String name
) {
}

public record Properties(
String nickname,
@JsonProperty("profile_image")
String profileImage
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.yeogido.backend.domain.auth.client;

import com.yeogido.backend.domain.auth.dto.SocialUserInfo;
import com.yeogido.backend.domain.auth.exception.AuthErrorCode;
import com.yeogido.backend.global.exception.GeneralException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;

@Component
@RequiredArgsConstructor
public class NaverUserInfoClient {

private final RestClient.Builder restClientBuilder;

@Value("${app.oauth.naver.user-info-uri}")
private String userInfoUri;

public SocialUserInfo getUserInfo(String accessToken) {
try {
NaverUserInfoResponse response = restClientBuilder.build()
.get()
.uri(userInfoUri)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.retrieve()
.body(NaverUserInfoResponse.class);

if (response == null || response.response() == null || response.response().id() == null) {
throw new GeneralException(AuthErrorCode.INVALID_SOCIAL_ACCESS_TOKEN);
}

return response.toSocialUserInfo();
} catch (RestClientException e) {
throw new GeneralException(AuthErrorCode.INVALID_SOCIAL_ACCESS_TOKEN);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.yeogido.backend.domain.auth.client;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.yeogido.backend.domain.auth.dto.SocialUserInfo;
import com.yeogido.backend.domain.auth.enums.SocialProvider;

public record NaverUserInfoResponse(
String resultcode,
String message,
Response response
) {

public SocialUserInfo toSocialUserInfo() {
return new SocialUserInfo(
SocialProvider.NAVER,
response.id(),
response.email(),
response.name(),
response.profileImageUrl()
);
}

public record Response(
String id,
String email,
String name,
@JsonProperty("profile_image")
String profileImageUrl
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,17 @@ public ApiResponse<AuthResDTO.Token> login(@Valid @RequestBody AuthReqDTO.Login
return ApiResponse.onSuccess(SuccessCode.OK, response);
}

@Operation(summary = "소셜 로그인 API", description = "소셜 제공자와 제공자 식별자를 이용하여 로그인 또는 자동 회원가입을 진행합니다.")
@Operation(summary = "소셜 로그인 API", description = "프론트에서 전달받은 소셜 accessToken을 검증하고, 신규 사용자는 프로필 작성 단계로 안내합니다.")
@PostMapping("/social-login")
public ApiResponse<AuthResDTO.Token> socialLogin(@Valid @RequestBody AuthReqDTO.SocialLogin request) {
AuthResDTO.Token response = authService.socialLogin(request);
public ApiResponse<AuthResDTO.SocialLogin> socialLogin(@Valid @RequestBody AuthReqDTO.SocialLogin request) {
AuthResDTO.SocialLogin response = authService.socialLogin(request);
return ApiResponse.onSuccess(SuccessCode.OK, response);
}

@Operation(summary = "소셜 로그인 추가 프로필 작성 완료 API", description = "신규 소셜 사용자의 추가 프로필 정보를 저장하고 회원가입을 완료합니다.")
@PostMapping("/social-signup/complete")
public ApiResponse<AuthResDTO.Token> completeSocialSignup(@Valid @RequestBody AuthReqDTO.SocialSignupComplete request) {
AuthResDTO.Token response = authService.completeSocialSignup(request);
return ApiResponse.onSuccess(SuccessCode.OK, response);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.yeogido.backend.domain.auth.converter;

import com.yeogido.backend.domain.auth.dto.AuthResDTO;
import com.yeogido.backend.domain.auth.dto.SocialUserInfo;

public class AuthConverter {

private AuthConverter() {
}

public static AuthResDTO.SocialLogin toExistingSocialLoginResponse(AuthResDTO.Token token) {
return new AuthResDTO.SocialLogin(
false,
token.userId(),
token.accessToken(),
token.refreshToken(),
null,
null,
null
);
}

public static AuthResDTO.SocialLogin toNewSocialLoginResponse(
String temporaryToken,
SocialUserInfo socialUserInfo
) {
return new AuthResDTO.SocialLogin(
true,
null,
null,
null,
temporaryToken,
socialUserInfo.email(),
socialUserInfo.name()
);
}
}
36 changes: 28 additions & 8 deletions src/main/java/com/yeogido/backend/domain/auth/dto/AuthReqDTO.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.yeogido.backend.domain.auth.dto;

import com.yeogido.backend.domain.auth.enums.SocialProvider;
import com.yeogido.backend.domain.user.enums.Gender;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
Expand Down Expand Up @@ -51,18 +52,37 @@ public record Login(

@Schema(name = "AuthSocialLoginReq", description = "소셜 로그인 요청")
public record SocialLogin(
@Schema(description = "소셜 로그인 제공자", example = "KAKAO")
@Schema(description = "소셜 로그인 제공자", example = "KAKAO", allowableValues = {"KAKAO", "NAVER"})
@NotNull(message = "소셜 로그인 제공자는 필수 입력값입니다.")
SocialProvider provider,

@Schema(description = "소셜 제공자 사용자 식별자", example = "1234567890")
@NotBlank(message = "소셜 제공자 사용자 식별자는 필수 입력값입니다.")
String providerId,
@Schema(description = "소셜 accessToken", example = "kakao_access_token")
@NotBlank(message = "소셜 accessToken은 필수 입력값입니다.")
String accessToken
) {}

@Schema(description = "소셜 계정 이메일", example = "kakao_user@example.com")
@NotBlank(message = "이메일은 필수 입력값입니다.")
@Email(message = "올바른 이메일 형식이어야 합니다.")
String email
@Schema(name = "AuthSocialSignupCompleteReq", description = "소셜 로그인 추가 프로필 작성 완료 요청")
public record SocialSignupComplete(
@Schema(description = "소셜 회원가입 임시 토큰", example = "temp_social_signup_token")
@NotBlank(message = "소셜 회원가입 임시 토큰은 필수 입력값입니다.")
String temporaryToken,

@Schema(description = "사용자 이름. 소셜 계정 이름을 기본값으로 사용하며 프로필에서 수정 가능합니다.", example = "홍길동")
@NotBlank(message = "이름은 필수 입력값입니다.")
String name,

@Schema(description = "성별", example = "MALE")
@NotNull(message = "성별은 필수 입력값입니다.")
Gender gender,

@Schema(description = "출생연도", example = "2001")
@NotBlank(message = "출생연도는 필수 입력값입니다.")
@Pattern(regexp = "^\\d{4}$", message = "출생연도는 4자리 숫자여야 합니다.")
String birthYear,

@Schema(description = "지역 ID", example = "1")
@NotNull(message = "지역 ID는 필수 입력값입니다.")
Long regionId
) {}

@Schema(name = "AuthPasswordSendCodeReq", description = "비밀번호 찾기 인증번호 발송 요청")
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/yeogido/backend/domain/auth/dto/AuthResDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,30 @@ public record Token(
String refreshToken
) {}

@Schema(name = "AuthSocialLoginRes", description = "소셜 로그인 응답")
public record SocialLogin(
@Schema(description = "신규 소셜 사용자 여부", example = "true")
boolean isNewUser,

@Schema(description = "사용자 ID. 기존 사용자 로그인 성공 시 반환", example = "2")
Long userId,

@Schema(description = "액세스 토큰. 기존 사용자 로그인 성공 시 반환", example = "eyJhbGciOiJIUzI1NiJ9...")
String accessToken,

@Schema(description = "리프레시 토큰. 기존 사용자 로그인 성공 시 반환", example = "eyJhbGciOiJIUzI1NiJ9...")
String refreshToken,

@Schema(description = "프로필 작성용 임시 토큰. 신규 소셜 사용자일 때 반환", example = "temp_social_signup_token")
String temporaryToken,

@Schema(description = "소셜 Provider에서 조회한 이메일. 신규 소셜 사용자일 때 반환", example = "kakao_user@example.com")
String email,

@Schema(description = "소셜 Provider에서 조회한 이름. 신규 소셜 사용자일 때 반환", example = "홍길동")
String name
) {}

@Schema(name = "AuthEmailCheckRes", description = "이메일 중복 확인 응답")
public record EmailCheck(
@Schema(description = "이메일 사용 가능 여부", example = "true")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.yeogido.backend.domain.auth.dto;

import com.yeogido.backend.domain.auth.enums.SocialProvider;

public record SocialSignupTokenPayload(
SocialProvider provider,
String providerId,
String email,
String name,
String profileImageUrl
) {

public static SocialSignupTokenPayload from(SocialUserInfo socialUserInfo) {
return new SocialSignupTokenPayload(
socialUserInfo.provider(),
socialUserInfo.providerId(),
socialUserInfo.email(),
socialUserInfo.name(),
socialUserInfo.profileImageUrl()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.yeogido.backend.domain.auth.dto;

import com.yeogido.backend.domain.auth.enums.SocialProvider;

public record SocialUserInfo(
SocialProvider provider,
String providerId,
String email,
String name,
String profileImageUrl
) {
}
Loading
Loading