From 0cf6ea1f9d716cb4c9091ca50f640c3e3de7bbe0 Mon Sep 17 00:00:00 2001 From: Chanhae Lee Date: Mon, 16 Feb 2026 22:29:20 +0900 Subject: [PATCH 1/6] =?UTF-8?q?ci(global):=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=20OAuth=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20CI/CD=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit application-*.yml local/dev/prod에 카카오 OAuth 설정 추가, GitHub Actions dev/prod 스크립트에 카카오 환경 변수 추가, KakaoProperties OAuth 설정 바인딩 클래스 추가 --- .github/workflows/deploy-to-dev-ec2.yml | 4 ++++ .github/workflows/deploy-to-prod-ec2.yml | 4 ++++ .../global/core/properties/KakaoProperties.java | 17 +++++++++++++++++ src/main/resources/application-dev.yml | 5 +++++ src/main/resources/application-local.yml | 5 +++++ src/main/resources/application-prod.yml | 5 +++++ 6 files changed, 40 insertions(+) create mode 100644 src/main/java/com/devkor/ifive/nadab/global/core/properties/KakaoProperties.java diff --git a/.github/workflows/deploy-to-dev-ec2.yml b/.github/workflows/deploy-to-dev-ec2.yml index 42ae5b76..e2985be6 100644 --- a/.github/workflows/deploy-to-dev-ec2.yml +++ b/.github/workflows/deploy-to-dev-ec2.yml @@ -94,6 +94,10 @@ jobs: GOOGLE_ANDROID_CLIENT_ID="${{ secrets.GOOGLE_ANDROID_CLIENT_ID }}" \ GOOGLE_CLIENT_SECRET="${{ secrets.GOOGLE_CLIENT_SECRET }}" \ GOOGLE_REDIRECT_URI="${{ secrets.GOOGLE_REDIRECT_URI }}" \ + KAKAO_CLIENT_ID="${{ secrets.KAKAO_CLIENT_ID }}" \ + KAKAO_CLIENT_SECRET="${{ secrets.KAKAO_CLIENT_SECRET }}" \ + KAKAO_REDIRECT_URI="${{ secrets.KAKAO_REDIRECT_URI }}" \ + KAKAO_APP_ID="${{ secrets.KAKAO_APP_ID }}" \ IAM_ACCESS_KEY="${{ secrets.IAM_ACCESS_KEY }}" \ IAM_SECRET_KEY="${{ secrets.IAM_SECRET_KEY }}" \ BUCKET_NAME="${{ secrets.BUCKET_NAME }}" \ diff --git a/.github/workflows/deploy-to-prod-ec2.yml b/.github/workflows/deploy-to-prod-ec2.yml index 14068e8b..28002640 100644 --- a/.github/workflows/deploy-to-prod-ec2.yml +++ b/.github/workflows/deploy-to-prod-ec2.yml @@ -92,6 +92,10 @@ jobs: GOOGLE_ANDROID_CLIENT_ID="${{ secrets.GOOGLE_ANDROID_CLIENT_ID }}" \ GOOGLE_CLIENT_SECRET="${{ secrets.GOOGLE_CLIENT_SECRET }}" \ GOOGLE_REDIRECT_URI="${{ secrets.GOOGLE_REDIRECT_URI }}" \ + KAKAO_CLIENT_ID="${{ secrets.KAKAO_CLIENT_ID }}" \ + KAKAO_CLIENT_SECRET="${{ secrets.KAKAO_CLIENT_SECRET }}" \ + KAKAO_REDIRECT_URI="${{ secrets.KAKAO_REDIRECT_URI }}" \ + KAKAO_APP_ID="${{ secrets.KAKAO_APP_ID }}" \ IAM_ACCESS_KEY="${{ secrets.IAM_ACCESS_KEY }}" \ IAM_SECRET_KEY="${{ secrets.IAM_SECRET_KEY }}" \ BUCKET_NAME="${{ secrets.BUCKET_NAME }}" \ diff --git a/src/main/java/com/devkor/ifive/nadab/global/core/properties/KakaoProperties.java b/src/main/java/com/devkor/ifive/nadab/global/core/properties/KakaoProperties.java new file mode 100644 index 00000000..399e2c3e --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/global/core/properties/KakaoProperties.java @@ -0,0 +1,17 @@ +package com.devkor.ifive.nadab.global.core.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@Getter +@Setter +@ConfigurationProperties(prefix = "oauth.kakao") +public class KakaoProperties { + private String clientId; + private String clientSecret; + private String redirectUri; + private Integer appId; +} \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 5af5b1a2..8d2588bd 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -31,6 +31,11 @@ oauth: android-client-id: ${GOOGLE_ANDROID_CLIENT_ID} client-secret: ${GOOGLE_CLIENT_SECRET} redirect-uri: ${GOOGLE_REDIRECT_URI} + kakao: + client-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_CLIENT_SECRET} + redirect-uri: ${KAKAO_REDIRECT_URI} + app-id: ${KAKAO_APP_ID} profile-image: env: dev diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index c38e8b47..a9be8010 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -32,6 +32,11 @@ oauth: android-client-id: ${GOOGLE_ANDROID_CLIENT_ID} client-secret: ${GOOGLE_CLIENT_SECRET} redirect-uri: ${GOOGLE_REDIRECT_URI} + kakao: + client-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_CLIENT_SECRET} + redirect-uri: ${KAKAO_REDIRECT_URI} + app-id: ${KAKAO_APP_ID} profile-image: env: local diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 480314a2..f87b5395 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -31,6 +31,11 @@ oauth: android-client-id: ${GOOGLE_ANDROID_CLIENT_ID} client-secret: ${GOOGLE_CLIENT_SECRET} redirect-uri: ${GOOGLE_REDIRECT_URI} + kakao: + client-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_CLIENT_SECRET} + redirect-uri: ${KAKAO_REDIRECT_URI} + app-id: ${KAKAO_APP_ID} profile-image: env: prod From f1238da02435eb9c843b37c3a73daa9b2ddd4326 Mon Sep 17 00:00:00 2001 From: Chanhae Lee Date: Mon, 16 Feb 2026 22:30:27 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat(auth):=20OAuth2Provider=EC=99=80=20Pro?= =?UTF-8?q?viderType=EC=97=90=20KAKAO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 추가적으로 enum 순서 알파벳순으로 통일 --- .../ifive/nadab/domain/auth/core/entity/ProviderType.java | 1 + .../ifive/nadab/domain/auth/infra/oauth/OAuth2Provider.java | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/devkor/ifive/nadab/domain/auth/core/entity/ProviderType.java b/src/main/java/com/devkor/ifive/nadab/domain/auth/core/entity/ProviderType.java index 72c23050..f64b3911 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/auth/core/entity/ProviderType.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/auth/core/entity/ProviderType.java @@ -2,5 +2,6 @@ public enum ProviderType { GOOGLE, + KAKAO, NAVER } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/OAuth2Provider.java b/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/OAuth2Provider.java index 59639d8f..bd97fc6e 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/OAuth2Provider.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/OAuth2Provider.java @@ -8,8 +8,9 @@ * - 지원되는 소셜 로그인 제공자를 정의 */ public enum OAuth2Provider { - NAVER, - GOOGLE; + GOOGLE, + KAKAO, + NAVER; public static OAuth2Provider fromString(String provider) { for (OAuth2Provider p : values()) { From 0500fc574dd05301e021692a2b16801500bd9ec6 Mon Sep 17 00:00:00 2001 From: Chanhae Lee Date: Mon, 16 Feb 2026 22:34:54 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat(auth):=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=20OAuth2Client=20=EB=B0=8F=20DTO=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KakaoOAuth2Client에 Authorization URL 생성, Access Token 발급, 사용자 정보 조회, 앱 키 검증 (Native SDK용) 관련 로직 구현, 관련 3가지 DTO 추가 --- .../infra/oauth/client/KakaoOAuth2Client.java | 200 ++++++++++++++++++ .../dto/KakaoAccessTokenInfoResponse.java | 26 +++ .../client/dto/KakaoProfileResponse.java | 47 ++++ .../oauth/client/dto/KakaoTokenResponse.java | 25 +++ 4 files changed, 298 insertions(+) create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/KakaoOAuth2Client.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/dto/KakaoAccessTokenInfoResponse.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/dto/KakaoProfileResponse.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/dto/KakaoTokenResponse.java diff --git a/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/KakaoOAuth2Client.java b/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/KakaoOAuth2Client.java new file mode 100644 index 00000000..35578950 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/KakaoOAuth2Client.java @@ -0,0 +1,200 @@ +package com.devkor.ifive.nadab.domain.auth.infra.oauth.client; + +import com.devkor.ifive.nadab.domain.auth.infra.oauth.OAuth2UserInfo; +import com.devkor.ifive.nadab.domain.auth.infra.oauth.client.dto.KakaoTokenResponse; +import com.devkor.ifive.nadab.domain.auth.infra.oauth.client.dto.KakaoProfileResponse; +import com.devkor.ifive.nadab.domain.auth.infra.oauth.client.dto.KakaoAccessTokenInfoResponse; +import com.devkor.ifive.nadab.global.core.properties.KakaoProperties; +import com.devkor.ifive.nadab.global.core.response.ErrorCode; +import com.devkor.ifive.nadab.global.exception.OAuth2Exception; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * 카카오 OAuth2 인증 플로우 처리 + * - Authorization URL 생성, Access Token 발급, 사용자 정보 조회 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class KakaoOAuth2Client { + private static final String AUTHORIZATION_URI = "https://kauth.kakao.com/oauth/authorize"; + private static final String TOKEN_URI = "https://kauth.kakao.com/oauth/token"; + private static final String USER_INFO_URI = "https://kapi.kakao.com/v2/user/me"; + + private final KakaoProperties kakaoProperties; + private final WebClient webClient; + + // Authorization URL 생성 (프론트 전달용) + public String buildAuthorizationUrl(String state) { + return AUTHORIZATION_URI + + "?client_id=" + kakaoProperties.getClientId() + + "&redirect_uri=" + kakaoProperties.getRedirectUri() + + "&response_type=code" + + "&state=" + state + + "&prompt=select_account"; // 매번 계정 선택 화면 표시 + } + + // Access Token 발급 + public String fetchAccessToken(String code) { + KakaoTokenResponse response = webClient.post() + .uri(TOKEN_URI) + .header("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") + .body(BodyInserters.fromFormData("grant_type", "authorization_code") + .with("client_id", kakaoProperties.getClientId()) + .with("client_secret", kakaoProperties.getClientSecret()) + .with("code", code) + .with("redirect_uri", kakaoProperties.getRedirectUri())) + .retrieve() + .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(), + clientResponse -> clientResponse.bodyToMono(KakaoTokenResponse.class) + .map(errorResponse -> { + String errorMessage = errorResponse.getErrorDescription() != null + ? errorResponse.getErrorDescription() + : "HTTP " + clientResponse.statusCode(); + log.warn("카카오 토큰 발급 실패: {}", errorMessage); + return new OAuth2Exception(ErrorCode.AUTH_OAUTH2_TOKEN_FAILED); + })) + .bodyToMono(KakaoTokenResponse.class) + .block(); + + // 응답 없음 + if (response == null) { + log.warn("카카오 토큰 발급 실패: 응답 없음"); + throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_TOKEN_FAILED); + } + + // 에러 응답 체크 (HTTP 200이지만 error 필드가 있는 경우) + if (response.getError() != null) { + String errorMessage = response.getErrorDescription() != null + ? response.getErrorDescription() + : "알 수 없는 오류"; + log.warn("카카오 토큰 발급 실패: {}", errorMessage); + throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_TOKEN_FAILED); + } + + // Access Token 체크 (error가 null인 정상 응답인데 access_token이 없는 예외 상황 방어) + if (response.getAccessToken() == null) { + log.warn("카카오 토큰 발급 실패: access_token 필드 없음"); + throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_TOKEN_FAILED); + } + + return response.getAccessToken(); + } + + // 사용자 정보 조회 + public OAuth2UserInfo fetchUserInfo(String accessToken) { + KakaoProfileResponse response = webClient.get() + .uri(USER_INFO_URI) + .header("Authorization", "Bearer " + accessToken) + .retrieve() + .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(), + clientResponse -> clientResponse.bodyToMono(KakaoProfileResponse.class) + .map(errorResponse -> { + String errorMessage = errorResponse.getMsg() != null + ? errorResponse.getMsg() + : "HTTP " + clientResponse.statusCode(); + log.warn("카카오 사용자 정보 조회 실패: {}", errorMessage); + return new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); + })) + .bodyToMono(KakaoProfileResponse.class) + .block(); + + // 응답 없음 + if (response == null) { + log.warn("카카오 사용자 정보 조회 실패: 응답 없음"); + throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); + } + + // 필수 필드 체크 + if (response.getId() == null) { + log.warn("카카오 사용자 정보 조회 실패: id 필드 없음"); + throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); + } + + // kakao_account 체크 + if (response.getKakaoAccount() == null) { + log.warn("카카오 사용자 정보 조회 실패: kakao_account 정보 없음"); + throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); + } + + // 이메일 동의 여부 체크 + if (Boolean.TRUE.equals(response.getKakaoAccount().getEmailNeedsAgreement())) { + log.warn("카카오 사용자 정보 조회 실패: 이메일 제공 동의 필요"); + throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); + } + + // 이메일 존재 여부 체크 + if (response.getKakaoAccount().getEmail() == null) { + log.warn("카카오 사용자 정보 조회 실패: 이메일 정보 없음"); + throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); + } + + // 이메일 유효성 체크 + if (Boolean.FALSE.equals(response.getKakaoAccount().getIsEmailValid())) { + log.warn("카카오 사용자 정보 조회 실패: 유효하지 않은 이메일 (다른 계정에 이미 연결됨)"); + throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); + } + + // 이메일 인증 여부 체크 + if (Boolean.FALSE.equals(response.getKakaoAccount().getIsEmailVerified())) { + log.warn("카카오 사용자 정보 조회 실패: 인증되지 않은 이메일"); + throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); + } + + return new OAuth2UserInfo( + String.valueOf(response.getId()), + response.getKakaoAccount().getEmail() + ); + } + + // Access Token 앱 키 검증 (Native SDK 로그인용, 다른 앱의 토큰 사용 방지) + public void validateAccessTokenAppId(String accessToken) { + KakaoAccessTokenInfoResponse response = webClient.get() + .uri("https://kapi.kakao.com/v1/user/access_token_info") + .header("Authorization", "Bearer " + accessToken) + .retrieve() + .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(), + clientResponse -> clientResponse.bodyToMono(KakaoAccessTokenInfoResponse.class) + .map(errorResponse -> { + String errorMessage = errorResponse.getMsg() != null + ? errorResponse.getMsg() + : "HTTP " + clientResponse.statusCode(); + log.warn("카카오 Access Token 검증 실패: {}", errorMessage); + return new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); + })) + .bodyToMono(KakaoAccessTokenInfoResponse.class) + .block(); + + // 응답 없음 + if (response == null) { + log.warn("카카오 Access Token 검증 실패: 응답 없음"); + throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); + } + + // 에러 응답 체크 + if (response.getMsg() != null) { + String errorMessage = response.getMsg(); + log.warn("카카오 Access Token 검증 실패: {}", errorMessage); + throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); + } + + // 앱 ID 체크 + if (response.getAppId() == null) { + log.warn("카카오 Access Token 검증 실패: app_id 필드 없음"); + throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); + } + + // 앱 ID 검증 (우리 앱의 토큰인지 확인) + if (!kakaoProperties.getAppId().equals(response.getAppId())) { + log.warn("카카오 Access Token 검증 실패: 다른 앱의 토큰 (기대: {}, 실제: {})", + kakaoProperties.getAppId(), response.getAppId()); + throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); + } + + log.debug("카카오 Access Token 검증 성공: app_id={}", response.getAppId()); + } +} \ No newline at end of file diff --git a/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/dto/KakaoAccessTokenInfoResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/dto/KakaoAccessTokenInfoResponse.java new file mode 100644 index 00000000..3af8614b --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/dto/KakaoAccessTokenInfoResponse.java @@ -0,0 +1,26 @@ +package com.devkor.ifive.nadab.domain.auth.infra.oauth.client.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 카카오 Access Token 정보 조회 API 응답 + * - 카카오 토큰 정보 보기 API (/v1/user/access_token_info) 응답 형식 + * - Access Token의 유효성 및 앱 ID 검증에 사용 + */ +@Getter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class KakaoAccessTokenInfoResponse { + + @JsonProperty("app_id") + private Integer appId; // 토큰이 발급된 앱 ID + + @JsonProperty("msg") + private String msg; // 에러 메시지 + + @JsonProperty("code") + private String code; // 에러 코드 +} \ No newline at end of file diff --git a/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/dto/KakaoProfileResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/dto/KakaoProfileResponse.java new file mode 100644 index 00000000..d47ce22e --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/dto/KakaoProfileResponse.java @@ -0,0 +1,47 @@ +package com.devkor.ifive.nadab.domain.auth.infra.oauth.client.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 카카오 사용자 정보 API 응답 + * - 카카오 사용자 정보 요청 API (/v2/user/me) 응답 형식 + * - 성공: id, kakao_account 필드 포함 + * - 실패: msg, code 필드 포함 + */ +@Getter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class KakaoProfileResponse { + + @JsonProperty("id") + private Long id; // providerId + + @JsonProperty("kakao_account") + private KakaoAccount kakaoAccount; + + @JsonProperty("msg") + private String msg; // 에러 메시지 + + @JsonProperty("code") + private String code; // 에러 코드 + + @Getter + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public static class KakaoAccount { + @JsonProperty("email_needs_agreement") + private Boolean emailNeedsAgreement; // 이메일 제공 동의 필요 여부 + + @JsonProperty("email") + private String email; + + @JsonProperty("is_email_valid") + private Boolean isEmailValid; // 이메일 유효성 + + @JsonProperty("is_email_verified") + private Boolean isEmailVerified; // 이메일 인증 여부 + } +} \ No newline at end of file diff --git a/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/dto/KakaoTokenResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/dto/KakaoTokenResponse.java new file mode 100644 index 00000000..3f72421a --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/dto/KakaoTokenResponse.java @@ -0,0 +1,25 @@ +package com.devkor.ifive.nadab.domain.auth.infra.oauth.client.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 카카오 Access Token 응답 DTO (카카오 Refresh Token은 사용X) + * - 성공: access_token + * - 실패: error, error_description + */ +@Getter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class KakaoTokenResponse { + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("error") + private String error; + + @JsonProperty("error_description") + private String errorDescription; +} \ No newline at end of file From 7de70d7a23088bd358cb66dc9f798fac0b0b32b0 Mon Sep 17 00:00:00 2001 From: Chanhae Lee Date: Mon, 16 Feb 2026 22:37:12 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat(auth):=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20Service=20=EB=B0=8F=20API=20?= =?UTF-8?q?=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NativeAuthService의 executeKakaoLogin 메서드로 Native SDK 로그인 처리, 웹 소셜 로그인의 경우 네이버와 구글 방식과 동일하므로 switch문으로 분기, GET /api/v1/auth/kakao/url, POST /api/v1/auth/kakao/login, POST /api/v1/auth/kakao/native-login 엔드포인트 추가 --- .../nadab/domain/auth/api/AuthController.java | 65 +++++++++++++++++-- .../dto/request/KakaoNativeLoginRequest.java | 16 +++++ .../auth/application/NativeAuthService.java | 23 ++++++- .../auth/application/SocialAuthService.java | 5 ++ 4 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/auth/api/dto/request/KakaoNativeLoginRequest.java diff --git a/src/main/java/com/devkor/ifive/nadab/domain/auth/api/AuthController.java b/src/main/java/com/devkor/ifive/nadab/domain/auth/api/AuthController.java index 9b2e0df5..430867f2 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/auth/api/AuthController.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/auth/api/AuthController.java @@ -7,6 +7,7 @@ import com.devkor.ifive.nadab.domain.auth.api.dto.request.SignupRequest; import com.devkor.ifive.nadab.domain.auth.api.dto.request.NaverNativeLoginRequest; import com.devkor.ifive.nadab.domain.auth.api.dto.request.GoogleNativeLoginRequest; +import com.devkor.ifive.nadab.domain.auth.api.dto.request.KakaoNativeLoginRequest; import com.devkor.ifive.nadab.domain.auth.api.dto.response.AuthorizationUrlResponse; import com.devkor.ifive.nadab.domain.auth.api.dto.request.SocialLoginRequest; import com.devkor.ifive.nadab.domain.auth.api.dto.response.TokenResponse; @@ -77,12 +78,12 @@ public class AuthController { ), @ApiResponse( responseCode = "400", - description = "ErrorCode: AUTH_UNSUPPORTED_OAUTH2_PROVIDER - provider가 'naver' 또는 'google'이 아닌 경우", + description = "ErrorCode: AUTH_UNSUPPORTED_OAUTH2_PROVIDER - provider가 'naver', 'google', 'kakao'가 아닌 경우", content = @Content) } ) public ResponseEntity> getAuthorizationUrl( - @Parameter(description = "OAuth2 제공자 (naver, google)", example = "naver") + @Parameter(description = "OAuth2 제공자 (naver, google, kakao)", example = "naver") @PathVariable("provider") String provider ) { // Provider 검증 및 변환 @@ -236,7 +237,7 @@ public ResponseEntity> login( responseCode = "400", description = """ 잘못된 요청 - - ErrorCode: AUTH_UNSUPPORTED_OAUTH2_PROVIDER - provider가 'naver' 또는 'google'이 아닌 경우 + - ErrorCode: AUTH_UNSUPPORTED_OAUTH2_PROVIDER - provider가 'naver', 'google', 'kakao'가 아닌 경우 - ErrorCode: VALIDATION_FAILED - code 또는 state 값이 누락된 경우 """, content = @Content @@ -259,7 +260,7 @@ public ResponseEntity> login( } ) public ResponseEntity> oauth2Login( - @Parameter(description = "OAuth2 제공자 (naver, google)", example = "naver") + @Parameter(description = "OAuth2 제공자 (naver, google, kakao)", example = "naver") @PathVariable("provider") String provider, @RequestBody @Valid SocialLoginRequest request, HttpServletResponse response @@ -390,6 +391,60 @@ public ResponseEntity> googleNativeLogin( ); } + @PostMapping("/kakao/native-login") + @PermitAll + @Operation( + summary = "카카오 Native SDK 로그인", + description = """ + Android/iOS 앱에서 카카오 SDK로 받은 Access Token을 사용하여 로그인을 완료합니다.
+ Access Token과 signupStatus는 응답 바디(JSON)로 반환되며, Refresh Token은 HttpOnly 쿠키로 자동 설정됩니다.
+ 기존 회원은 바로 로그인 처리되며, 신규 사용자는 자동으로 회원가입 후 로그인됩니다.
+
+ 신규 가입자(signupStatus: PROFILE_INCOMPLETE)는 온보딩 과정에서 약관 동의(POST /terms/consent) 후 닉네임을 입력해야 합니다.
+
+ **signupStatus:**
+ - PROFILE_INCOMPLETE: 프로필 입력 필요 (신규 가입자, 약관 동의 + 닉네임 입력 필요)
+ - COMPLETED: 가입 완료 (모든 필수 정보 입력 완료)
+ - WITHDRAWN: 회원 탈퇴 (14일 내 복구 가능) + """, + responses = { + @ApiResponse( + responseCode = "200", + description = "로그인 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class), mediaType = "application/json") + ), + @ApiResponse( + responseCode = "400", + description = "ErrorCode: VALIDATION_FAILED - Access Token이 누락된 경우", + content = @Content + ), + @ApiResponse( + responseCode = "401", + description = "ErrorCode: AUTH_OAUTH2_USERINFO_FAILED - 카카오로부터 사용자 정보 조회 실패 (유효하지 않은 토큰)", + content = @Content + ), + @ApiResponse( + responseCode = "409", + description = "ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_DIFFERENT_METHOD - 해당 이메일이 다른 방법으로 이미 가입된 경우", + content = @Content + ) + } + ) + public ResponseEntity> kakaoNativeLogin( + @RequestBody @Valid KakaoNativeLoginRequest request, + HttpServletResponse response + ) { + // 카카오 Access Token으로 사용자 정보 조회 및 로그인 + TokenBundle tokenBundle = nativeAuthService.executeKakaoLogin(request.kakaoAccessToken()); + + // Refresh Token을 HttpOnly 쿠키에 저장 + cookieManager.addRefreshTokenCookie(response, tokenBundle.refreshToken()); + + return ApiResponseEntity.ok( + new TokenResponse(tokenBundle.accessToken(), tokenBundle.signupStatus()) + ); + } + @PostMapping("/refresh") @PermitAll @Operation( @@ -625,7 +680,7 @@ public ResponseEntity> withdrawUser(
**소셜 로그인 계정의 경우:**
- 별도 복구 API가 필요 없습니다.
- - 소셜 로그인(POST /naver/login 또는 POST /google/login)을 시도하면 자동으로 복구됩니다. + - 소셜 로그인(POST /{provider}/login)을 시도하면 자동으로 복구됩니다. """, responses = { @ApiResponse( diff --git a/src/main/java/com/devkor/ifive/nadab/domain/auth/api/dto/request/KakaoNativeLoginRequest.java b/src/main/java/com/devkor/ifive/nadab/domain/auth/api/dto/request/KakaoNativeLoginRequest.java new file mode 100644 index 00000000..dca521c0 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/auth/api/dto/request/KakaoNativeLoginRequest.java @@ -0,0 +1,16 @@ +package com.devkor.ifive.nadab.domain.auth.api.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +/** + * 카카오 Native SDK 로그인 요청 DTO + * - Android/iOS 앱에서 카카오 SDK로 받은 Access Token 전달 + */ +@Schema(description = "카카오 Native SDK 로그인 요청") +public record KakaoNativeLoginRequest( + @Schema(description = "카카오 SDK로부터 받은 Access Token", example = "AAAANv1...") + @NotBlank(message = "카카오 Access Token은 필수입니다") + String kakaoAccessToken +) { +} \ No newline at end of file diff --git a/src/main/java/com/devkor/ifive/nadab/domain/auth/application/NativeAuthService.java b/src/main/java/com/devkor/ifive/nadab/domain/auth/application/NativeAuthService.java index bb887a88..ea91b583 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/auth/application/NativeAuthService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/auth/application/NativeAuthService.java @@ -4,6 +4,7 @@ import com.devkor.ifive.nadab.domain.auth.infra.oauth.OAuth2Provider; import com.devkor.ifive.nadab.domain.auth.infra.oauth.OAuth2UserInfo; import com.devkor.ifive.nadab.domain.auth.infra.oauth.client.GoogleOAuth2Client; +import com.devkor.ifive.nadab.domain.auth.infra.oauth.client.KakaoOAuth2Client; import com.devkor.ifive.nadab.domain.auth.infra.oauth.client.NaverOAuth2Client; import com.devkor.ifive.nadab.domain.user.core.entity.User; import lombok.RequiredArgsConstructor; @@ -14,7 +15,7 @@ /** * Native SDK 소셜 로그인 서비스 * - Android/iOS 앱에서 각 SDK로 받은 토큰으로 로그인 처리 - * - 네이버, 구글, Apple 등 모든 Native SDK 로그인을 통합 관리 + * - 네이버, 구글, 카카오, Apple 등 모든 Native SDK 로그인을 통합 관리 */ @Slf4j @Service @@ -24,6 +25,7 @@ public class NativeAuthService { private final NaverOAuth2Client naverOAuth2Client; private final GoogleOAuth2Client googleOAuth2Client; + private final KakaoOAuth2Client kakaoOAuth2Client; private final SocialAccountService socialAccountService; private final TokenService tokenService; @@ -58,4 +60,23 @@ public TokenBundle executeGoogleLogin(String googleIdToken) { // 3. JWT 토큰 발급 return tokenService.issueTokens(user.getId()); } + + // 카카오 Native SDK 로그인 처리 + public TokenBundle executeKakaoLogin(String kakaoAccessToken) { + // 1. 앱 키 검증 (다른 앱의 토큰 사용 방지) + kakaoOAuth2Client.validateAccessTokenAppId(kakaoAccessToken); + + // 2. 카카오 API로 사용자 정보 조회 + OAuth2UserInfo userInfo = kakaoOAuth2Client.fetchUserInfo(kakaoAccessToken); + + // 3. 사용자 조회 또는 생성 + User user = socialAccountService.getOrCreateUser( + OAuth2Provider.KAKAO, + userInfo.providerId(), + userInfo.email() + ); + + // 4. JWT 토큰 발급 + return tokenService.issueTokens(user.getId()); + } } \ No newline at end of file diff --git a/src/main/java/com/devkor/ifive/nadab/domain/auth/application/SocialAuthService.java b/src/main/java/com/devkor/ifive/nadab/domain/auth/application/SocialAuthService.java index efc73430..a9043aa7 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/auth/application/SocialAuthService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/auth/application/SocialAuthService.java @@ -4,6 +4,7 @@ import com.devkor.ifive.nadab.domain.auth.infra.oauth.OAuth2UserInfo; import com.devkor.ifive.nadab.domain.auth.infra.oauth.OAuth2Provider; import com.devkor.ifive.nadab.domain.auth.infra.oauth.client.GoogleOAuth2Client; +import com.devkor.ifive.nadab.domain.auth.infra.oauth.client.KakaoOAuth2Client; import com.devkor.ifive.nadab.domain.auth.infra.oauth.client.NaverOAuth2Client; import com.devkor.ifive.nadab.domain.auth.infra.oauth.state.StateManager; import com.devkor.ifive.nadab.domain.user.core.entity.User; @@ -27,6 +28,7 @@ public class SocialAuthService { private final NaverOAuth2Client naverOAuth2Client; private final GoogleOAuth2Client googleOAuth2Client; + private final KakaoOAuth2Client kakaoOAuth2Client; private final StateManager stateManager; private final SocialAccountService socialAccountService; private final TokenService tokenService; @@ -40,6 +42,7 @@ public String getAuthorizationUrl(OAuth2Provider provider) { return switch (provider) { case NAVER -> naverOAuth2Client.buildAuthorizationUrl(state); case GOOGLE -> googleOAuth2Client.buildAuthorizationUrl(state); + case KAKAO -> kakaoOAuth2Client.buildAuthorizationUrl(state); }; } @@ -56,12 +59,14 @@ public TokenBundle executeOAuth2Login(OAuth2Provider provider, String code, Stri String accessToken = switch (provider) { case NAVER -> naverOAuth2Client.fetchAccessToken(code, state); case GOOGLE -> googleOAuth2Client.fetchAccessToken(code); + case KAKAO -> kakaoOAuth2Client.fetchAccessToken(code); }; // 3. 사용자 정보 조회 OAuth2UserInfo userInfo = switch (provider) { case NAVER -> naverOAuth2Client.fetchUserInfo(accessToken); case GOOGLE -> googleOAuth2Client.fetchUserInfo(accessToken); + case KAKAO -> kakaoOAuth2Client.fetchUserInfo(accessToken); }; // 4. 사용자 조회 또는 생성 From 16f62eaaa1a68eadc2fba9e044c531f60fefef58 Mon Sep 17 00:00:00 2001 From: Chanhae Lee Date: Mon, 16 Feb 2026 22:44:34 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix(auth):=20=EA=B5=AC=EA=B8=80=20ID=20Toke?= =?UTF-8?q?n=20audience=20=EA=B2=80=EC=A6=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aud 검증 시 Web Client ID 사용 --- .../domain/auth/infra/oauth/client/GoogleOAuth2Client.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/GoogleOAuth2Client.java b/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/GoogleOAuth2Client.java index fcc8420d..70fe8504 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/GoogleOAuth2Client.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/auth/infra/oauth/client/GoogleOAuth2Client.java @@ -197,13 +197,13 @@ private void validateIdTokenInfo(GoogleIdTokenInfoResponse tokenInfo) { throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); } - // audience 검증 (우리 앱의 Android Client ID인지) - if (!googleProperties.getAndroidClientId().equals(tokenInfo.getAud())) { + // audience 검증 (우리 앱의 Web Client ID인지) + if (!googleProperties.getClientId().equals(tokenInfo.getAud())) { log.warn("구글 ID Token 검증 실패: 유효하지 않은 audience - {}", tokenInfo.getAud()); throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); } - // azp (Authorized party) 검증 (azp가 있으면 우리 앱의 Client ID인지 확인) + // azp (Authorized party) 검증 (azp가 있으면 우리 앱의 Android Client ID인지 확인) if (tokenInfo.getAzp() != null && !googleProperties.getAndroidClientId().equals(tokenInfo.getAzp())) { log.warn("구글 ID Token 검증 실패: 유효하지 않은 authorized party - {}", tokenInfo.getAzp()); throw new OAuth2Exception(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED); From 6b77d3bfb8e1ebe8020780fbb72fe6406db7ae77 Mon Sep 17 00:00:00 2001 From: Chanhae Lee Date: Mon, 16 Feb 2026 23:28:17 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor(auth):=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=8B=9C=20=EC=A0=9C=EA=B3=B5=EC=9E=90?= =?UTF-8?q?=EB=B3=84=20=EC=97=90=EB=9F=AC=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 다른 로그인 방식을 사용해주세요에서 이제는 네이버 계정으로 가입된 이메일입니다. 네이버로 로그인해주세요로 변경 --- .../nadab/domain/auth/api/AuthController.java | 32 ++++++++++++++++--- .../application/SocialAccountService.java | 22 +++++++++++-- .../repository/SocialAccountRepository.java | 3 ++ .../nadab/global/core/response/ErrorCode.java | 5 ++- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/devkor/ifive/nadab/domain/auth/api/AuthController.java b/src/main/java/com/devkor/ifive/nadab/domain/auth/api/AuthController.java index 430867f2..b2b26dc2 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/auth/api/AuthController.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/auth/api/AuthController.java @@ -254,7 +254,13 @@ public ResponseEntity> login( ), @ApiResponse( responseCode = "409", - description = "ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_DIFFERENT_METHOD - 해당 이메일이 다른 방법으로 이미 가입된 경우", + description = """ + 이메일 중복 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_NAVER - 네이버 계정으로 이미 가입됨 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_GOOGLE - 구글 계정으로 이미 가입됨 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_KAKAO - 카카오 계정으로 이미 가입됨 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_BASIC - 일반 계정으로 이미 가입됨 + """, content = @Content ) } @@ -313,7 +319,13 @@ public ResponseEntity> oauth2Login( ), @ApiResponse( responseCode = "409", - description = "ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_DIFFERENT_METHOD - 해당 이메일이 다른 방법으로 이미 가입된 경우", + description = """ + 이메일 중복 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_NAVER - 네이버 계정으로 이미 가입됨 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_GOOGLE - 구글 계정으로 이미 가입됨 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_KAKAO - 카카오 계정으로 이미 가입됨 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_BASIC - 일반 계정으로 이미 가입됨 + """, content = @Content ) } @@ -371,7 +383,13 @@ public ResponseEntity> naverNativeLogin( ), @ApiResponse( responseCode = "409", - description = "ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_DIFFERENT_METHOD - 해당 이메일이 다른 방법으로 이미 가입된 경우", + description = """ + 이메일 중복 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_NAVER - 네이버 계정으로 이미 가입됨 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_GOOGLE - 구글 계정으로 이미 가입됨 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_KAKAO - 카카오 계정으로 이미 가입됨 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_BASIC - 일반 계정으로 이미 가입됨 + """, content = @Content ) } @@ -425,7 +443,13 @@ public ResponseEntity> googleNativeLogin( ), @ApiResponse( responseCode = "409", - description = "ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_DIFFERENT_METHOD - 해당 이메일이 다른 방법으로 이미 가입된 경우", + description = """ + 이메일 중복 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_NAVER - 네이버 계정으로 이미 가입됨 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_GOOGLE - 구글 계정으로 이미 가입됨 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_KAKAO - 카카오 계정으로 이미 가입됨 + - ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_BASIC - 일반 계정으로 이미 가입됨 + """, content = @Content ) } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/auth/application/SocialAccountService.java b/src/main/java/com/devkor/ifive/nadab/domain/auth/application/SocialAccountService.java index 3e549bf7..ed66d3c1 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/auth/application/SocialAccountService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/auth/application/SocialAccountService.java @@ -52,11 +52,27 @@ public User getOrCreateUser(OAuth2Provider provider, String providerId, String e // User 조회 실패시 신규 소셜 로그인 사용자 생성 private User saveNewSocialUser(String email, OAuth2Provider provider, String providerId) { // 이메일 중복 체크 및 탈퇴 계정 확인 - userRepository.findByEmail(email).ifPresent(user -> { - if (user.getDeletedAt() != null) { + userRepository.findByEmail(email).ifPresent(existingUser -> { + if (existingUser.getDeletedAt() != null) { throw new BadRequestException(ErrorCode.AUTH_WITHDRAWN_ACCOUNT_RESTORE_REQUIRED); } - throw new ConflictException(ErrorCode.AUTH_EMAIL_ALREADY_REGISTERED_WITH_DIFFERENT_METHOD); + + // 기존 제공자 확인 + SocialAccount existingSocialAccount = socialAccountRepository.findByUser(existingUser) + .orElse(null); + + if (existingSocialAccount != null) { + // 소셜 로그인 계정 + ErrorCode errorCode = switch (existingSocialAccount.getProviderType()) { + case GOOGLE -> ErrorCode.AUTH_EMAIL_ALREADY_REGISTERED_WITH_GOOGLE; + case KAKAO -> ErrorCode.AUTH_EMAIL_ALREADY_REGISTERED_WITH_KAKAO; + case NAVER -> ErrorCode.AUTH_EMAIL_ALREADY_REGISTERED_WITH_NAVER; + }; + throw new ConflictException(errorCode); + } else { + // 일반 계정 + throw new ConflictException(ErrorCode.AUTH_EMAIL_ALREADY_REGISTERED_WITH_BASIC); + } }); // User 생성 및 저장 diff --git a/src/main/java/com/devkor/ifive/nadab/domain/auth/core/repository/SocialAccountRepository.java b/src/main/java/com/devkor/ifive/nadab/domain/auth/core/repository/SocialAccountRepository.java index a8694491..6d94a34c 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/auth/core/repository/SocialAccountRepository.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/auth/core/repository/SocialAccountRepository.java @@ -15,6 +15,9 @@ Optional findByProviderTypeAndProviderUserId( String providerUserId ); + // 사용자로 소셜 계정 조회 + Optional findByUser(User user); + // 사용자로 소셜 계정 존재 여부 확인 boolean existsByUser(User user); } \ No newline at end of file diff --git a/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java b/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java index 9da79fca..88358330 100644 --- a/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java +++ b/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java @@ -49,7 +49,10 @@ public enum ErrorCode { AUTH_DUMMY_USER_NOT_FOUND(HttpStatus.NOT_FOUND, "더미 유저(ID: 11111)가 존재하지 않습니다"), // 409 Conflict - AUTH_EMAIL_ALREADY_REGISTERED_WITH_DIFFERENT_METHOD(HttpStatus.CONFLICT, "이미 가입된 이메일입니다. 다른 로그인 방식을 사용해주세요."), + AUTH_EMAIL_ALREADY_REGISTERED_WITH_NAVER(HttpStatus.CONFLICT, "이미 네이버 계정으로 가입된 이메일입니다. 네이버로 로그인해주세요."), + AUTH_EMAIL_ALREADY_REGISTERED_WITH_GOOGLE(HttpStatus.CONFLICT, "이미 구글 계정으로 가입된 이메일입니다. 구글로 로그인해주세요."), + AUTH_EMAIL_ALREADY_REGISTERED_WITH_KAKAO(HttpStatus.CONFLICT, "이미 카카오 계정으로 가입된 이메일입니다. 카카오로 로그인해주세요."), + AUTH_EMAIL_ALREADY_REGISTERED_WITH_BASIC(HttpStatus.CONFLICT, "이미 일반 계정으로 가입된 이메일입니다. 이메일/비밀번호로 로그인해주세요."), // ==================== EMAIL (이메일 인증) ==================== // 400 Bad Request