From 819a31a8600ad151a3cead490670727cd36365c0 Mon Sep 17 00:00:00 2001 From: jinhee0 Date: Sun, 3 Aug 2025 13:55:22 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20separate?= =?UTF-8?q?=20AuthResult=20and=20TokenPair=20from=20AuthService=20interfac?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract AuthResult nested record to AuthResultDto --- .github/workflows/dev.yml | 75 ------------------- .../user/controller/AuthController.java | 31 ++++---- .../user/dto/response/AuthResultDto.java | 8 ++ .../user/dto/response/TokenPairDto.java | 8 ++ .../domain/user/service/AuthService.java | 18 ++--- .../domain/user/service/AuthServiceImpl.java | 28 +++---- .../user/service/PasswordResetService.java | 1 - 7 files changed, 54 insertions(+), 115 deletions(-) delete mode 100644 .github/workflows/dev.yml create mode 100644 src/main/java/com/yourmode/yourmodebackend/domain/user/dto/response/AuthResultDto.java create mode 100644 src/main/java/com/yourmode/yourmodebackend/domain/user/dto/response/TokenPairDto.java diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml deleted file mode 100644 index d662029..0000000 --- a/.github/workflows/dev.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Build and Deploy on Develop - -on: - pull_request: - branches: [ develop ] - types: [ closed ] - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - - steps: - # 1. GitHub 레포지토리 체크아웃 - - name: Checkout repository - uses: actions/checkout@v3 - - # 2. JDK 17 설정 - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - # 3. application-dev.properties 생성 - - name: Create application-dev.properties - run: | - cd src/main/resources - echo "${{ secrets.APPLICATION_DEV }}" > application-dev.yml - - # 4. gradlew 실행 권한 부여 - - name: Grant execute permission to gradlew - run: chmod +x ./gradlew - - # 5. Gradle 빌드 (테스트 제외, dev 프로필로) - - name: Build with Gradle using 'dev' profile - run: ./gradlew build -x test -Dspring.profiles.active=dev - - - # 6. 빌드 결과물 확인 - - name: List build artifacts - run: ls -al build/libs - - # 7. AWS 인증 설정 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v2 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-northeast-2 - - # 8. 배포용 디렉토리 준비 - - name: Prepare deployment package - run: | - mkdir -p deploy/scripts - cp build/libs/*.jar deploy/ - cp appspec.yml deploy/ - cp -r scripts/* deploy/scripts/ - - # 9. 배포용 zip 압축 생성 - - name: Create deployment zip file - run: zip -r your-mode-deploy.zip . -i "deploy/*" - - # 10. zip 파일을 S3에 업로드 - - name: Upload deployment zip to S3 - run: aws s3 cp your-mode-deploy.zip s3://${{ secrets.S3_BUCKET }}/your-mode/deploy/your-mode-deploy.zip --sse AES256 - - # 11. CodeDeploy를 트리거 - - name: Trigger CodeDeploy deployment - run: | - aws deploy create-deployment \ - --application-name ${{ secrets.CODEDEPLOY_APP_NAME }} \ - --deployment-group-name ${{ secrets.CODEDEPLOY_DEPLOYMENT_GROUP }} \ - --s3-location bucket=${{ secrets.S3_BUCKET }},key=your-mode/deploy/your-mode-deploy.zip,bundleType=zip \ - --deployment-config-name CodeDeployDefault.AllAtOnce \ - --file-exists-behavior OVERWRITE diff --git a/src/main/java/com/yourmode/yourmodebackend/domain/user/controller/AuthController.java b/src/main/java/com/yourmode/yourmodebackend/domain/user/controller/AuthController.java index 1cfb836..88c4f06 100644 --- a/src/main/java/com/yourmode/yourmodebackend/domain/user/controller/AuthController.java +++ b/src/main/java/com/yourmode/yourmodebackend/domain/user/controller/AuthController.java @@ -1,6 +1,7 @@ package com.yourmode.yourmodebackend.domain.user.controller; import com.yourmode.yourmodebackend.domain.user.dto.request.*; +import com.yourmode.yourmodebackend.domain.user.dto.response.AuthResultDto; import com.yourmode.yourmodebackend.domain.user.dto.response.AuthResponseDto; import com.yourmode.yourmodebackend.domain.user.dto.response.UserIdResponseDto; import com.yourmode.yourmodebackend.domain.user.service.AuthService; @@ -17,13 +18,13 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import jakarta.validation.Valid; @Slf4j @RestController @@ -104,10 +105,11 @@ private void setTokenCookies(HttpServletResponse response, String accessToken, S "message": "요청에 성공하였습니다.", "result": { "user": { - "name": "홍길동", - "role": "USER" - }, - "additionalInfoNeeded": null + "name": "string", + "email": "test1234@example.com", + "role": "USER", + "isNewUser": false + } } } """ @@ -244,7 +246,7 @@ public ResponseEntity> signUp( @Valid @RequestBody LocalSignupRequestDto request, HttpServletResponse response ) { - AuthService.AuthResult authResult = authService.signUp(request); + AuthResultDto authResult = authService.signUp(request); // 토큰을 쿠키로 설정 setTokenCookies(response, authResult.tokenPair().accessToken(), authResult.tokenPair().refreshToken()); @@ -279,10 +281,11 @@ public ResponseEntity> signUp( "message": "요청에 성공하였습니다.", "result": { "user": { - "name": "홍길동", - "role": "USER" - }, - "additionalInfoNeeded": null + "name": "string", + "email": "test1234@example.com", + "role": "USER", + "isNewUser": false + } } } """) @@ -326,7 +329,7 @@ public ResponseEntity> login( @Valid @RequestBody LocalLoginRequestDto request, HttpServletResponse response ) { - AuthService.AuthResult authResult = authService.login(request); + AuthResultDto authResult = authService.login(request); // 토큰을 쿠키로 설정 setTokenCookies(response, authResult.tokenPair().accessToken(), authResult.tokenPair().refreshToken()); @@ -500,7 +503,7 @@ public ResponseEntity> loginWithKakao( @Valid @RequestBody KakaoLoginRequestDto request, HttpServletResponse response ) { - AuthService.AuthResult authResult = authService.processKakaoLogin(request); + AuthResultDto authResult = authService.processKakaoLogin(request); // 토큰이 있으면 쿠키로 설정 (기존 회원) if (authResult.tokenPair().accessToken() != null && authResult.tokenPair().refreshToken() != null) { @@ -664,7 +667,7 @@ public ResponseEntity> completeSignupWithKakao( @Valid @RequestBody KakaoSignupRequestDto request, HttpServletResponse response ) { - AuthService.AuthResult authResult = authService.completeSignupWithKakao(request); + AuthResultDto authResult = authService.completeSignupWithKakao(request); // 토큰을 쿠키로 설정 setTokenCookies(response, authResult.tokenPair().accessToken(), authResult.tokenPair().refreshToken()); @@ -755,7 +758,7 @@ public ResponseEntity> refreshAccessToken( HttpServletRequest request, HttpServletResponse response ) { - AuthService.AuthResult authResult = authService.refreshAccessToken(request); + AuthResultDto authResult = authService.refreshAccessToken(request); // 토큰을 쿠키로 설정 setTokenCookies(response, authResult.tokenPair().accessToken(), authResult.tokenPair().refreshToken()); diff --git a/src/main/java/com/yourmode/yourmodebackend/domain/user/dto/response/AuthResultDto.java b/src/main/java/com/yourmode/yourmodebackend/domain/user/dto/response/AuthResultDto.java new file mode 100644 index 0000000..5d3904b --- /dev/null +++ b/src/main/java/com/yourmode/yourmodebackend/domain/user/dto/response/AuthResultDto.java @@ -0,0 +1,8 @@ +package com.yourmode.yourmodebackend.domain.user.dto.response; + +/** + * 인증 결과를 담는 DTO + * @param tokenPair 토큰 쌍 (access token, refresh token) + * @param userInfo 사용자 정보 + */ +public record AuthResultDto(TokenPairDto tokenPair, UserInfoDto userInfo) {} \ No newline at end of file diff --git a/src/main/java/com/yourmode/yourmodebackend/domain/user/dto/response/TokenPairDto.java b/src/main/java/com/yourmode/yourmodebackend/domain/user/dto/response/TokenPairDto.java new file mode 100644 index 0000000..f2de4fa --- /dev/null +++ b/src/main/java/com/yourmode/yourmodebackend/domain/user/dto/response/TokenPairDto.java @@ -0,0 +1,8 @@ +package com.yourmode.yourmodebackend.domain.user.dto.response; + +/** + * 토큰 쌍을 담는 DTO + * @param accessToken 액세스 토큰 + * @param refreshToken 리프레시 토큰 + */ +public record TokenPairDto(String accessToken, String refreshToken) {} \ No newline at end of file diff --git a/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthService.java b/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthService.java index e88d4ea..bc19b4d 100644 --- a/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthService.java +++ b/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthService.java @@ -1,22 +1,16 @@ package com.yourmode.yourmodebackend.domain.user.service; import com.yourmode.yourmodebackend.domain.user.dto.request.*; +import com.yourmode.yourmodebackend.domain.user.dto.response.AuthResultDto; import com.yourmode.yourmodebackend.domain.user.dto.response.UserIdResponseDto; -import com.yourmode.yourmodebackend.domain.user.dto.response.UserInfoDto; import com.yourmode.yourmodebackend.global.config.security.auth.PrincipalDetails; import jakarta.servlet.http.HttpServletRequest; public interface AuthService { - AuthResult signUp(LocalSignupRequestDto request); - AuthResult login(LocalLoginRequestDto request); - AuthResult processKakaoLogin(KakaoLoginRequestDto request); - AuthResult completeSignupWithKakao(KakaoSignupRequestDto request); - AuthResult refreshAccessToken(HttpServletRequest request); + AuthResultDto signUp(LocalSignupRequestDto request); + AuthResultDto login(LocalLoginRequestDto request); + AuthResultDto processKakaoLogin(KakaoLoginRequestDto request); + AuthResultDto completeSignupWithKakao(KakaoSignupRequestDto request); + AuthResultDto refreshAccessToken(HttpServletRequest request); UserIdResponseDto logout(PrincipalDetails principal); - - // 토큰 쌍을 위한 간단한 레코드 - record TokenPair(String accessToken, String refreshToken) {} - - // 인증 결과를 위한 레코드 (토큰과 사용자 정보 포함) - record AuthResult(TokenPair tokenPair, UserInfoDto userInfo) {} } diff --git a/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthServiceImpl.java b/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthServiceImpl.java index 9323d31..6b8733f 100644 --- a/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthServiceImpl.java +++ b/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthServiceImpl.java @@ -2,7 +2,9 @@ import com.yourmode.yourmodebackend.domain.user.entity.*; import com.yourmode.yourmodebackend.domain.user.dto.request.*; +import com.yourmode.yourmodebackend.domain.user.dto.response.AuthResultDto; import com.yourmode.yourmodebackend.domain.user.dto.response.AuthResponseDto; +import com.yourmode.yourmodebackend.domain.user.dto.response.TokenPairDto; import com.yourmode.yourmodebackend.domain.user.dto.response.UserIdResponseDto; import com.yourmode.yourmodebackend.domain.user.dto.response.UserInfoDto; import com.yourmode.yourmodebackend.domain.user.enums.OAuthProvider; @@ -80,7 +82,7 @@ public class AuthServiceImpl implements AuthService{ * - 인증 실패 시 AUTHENTICATION_FAILED 상태로 예외 발생 */ @Transactional - public AuthResult signUp(LocalSignupRequestDto request) { + public AuthResultDto signUp(LocalSignupRequestDto request) { // 이메일 중복 체크 후 중복되면 예외 발생 validateDuplicateEmail(request.getEmail()); @@ -113,7 +115,7 @@ public AuthResult signUp(LocalSignupRequestDto request) { UserInfoDto userInfo = buildUserInfoDto(user); // 토큰 쌍과 사용자 정보 반환 - return new AuthResult(new TokenPair(access.token(), refresh.token()), userInfo); + return new AuthResultDto(new TokenPairDto(access.token(), refresh.token()), userInfo); } catch (AuthenticationException ex) { throw new RestApiException(UserErrorStatus.AUTHENTICATION_FAILED); @@ -283,7 +285,7 @@ private void saveUserToken(User user, String refreshToken, LocalDateTime expired * @throws RestApiException 인증 실패 시 AUTHENTICATION_FAILED 상태로 예외 발생 */ @Transactional - public AuthResult login(LocalLoginRequestDto request) { + public AuthResultDto login(LocalLoginRequestDto request) { try { // 인증 토큰 생성: 이메일, 비밀번호 정보 포함 UsernamePasswordAuthenticationToken authenticationToken = @@ -311,7 +313,7 @@ public AuthResult login(LocalLoginRequestDto request) { UserInfoDto userInfo = buildUserInfoDto(user); // 토큰 쌍과 사용자 정보 반환 - return new AuthResult(new TokenPair(access.token(), refresh.token()), userInfo); + return new AuthResultDto(new TokenPairDto(access.token(), refresh.token()), userInfo); } catch (BadCredentialsException e) { throw new RestApiException(UserErrorStatus.INVALID_CREDENTIALS); @@ -395,7 +397,7 @@ public Map requestUserInfoWithKakao(String accessToken) { * @throws RestApiException 토큰 발급 실패, 이메일 누락, 회원 미존재 등 예외 상황 */ @Transactional - public AuthResult processKakaoLogin(KakaoLoginRequestDto request) { + public AuthResultDto processKakaoLogin(KakaoLoginRequestDto request) { // 카카오 토큰 발급 Map tokenInfo = requestTokenWithKakao(request.getAuthorizationCode()); String accessToken = (String) tokenInfo.get("access_token"); @@ -427,7 +429,7 @@ public AuthResult processKakaoLogin(KakaoLoginRequestDto request) { .isNewUser(true) .build(); - return new AuthResult(new TokenPair(null, null), userInfo); + return new AuthResultDto(new TokenPairDto(null, null), userInfo); } User user = userRepository.findByEmailWithProfile(email) @@ -454,7 +456,7 @@ public AuthResult processKakaoLogin(KakaoLoginRequestDto request) { .isNewUser(false) .build(); - return new AuthResult(new TokenPair(access.token(), refresh.token()), userInfo); + return new AuthResultDto(new TokenPairDto(access.token(), refresh.token()), userInfo); } /** @@ -467,10 +469,10 @@ public AuthResult processKakaoLogin(KakaoLoginRequestDto request) { * 6) 로그인 완료 응답 반환 * * @param request 카카오 회원가입 요청 DTO (추가 정보 포함) - * @return 로그인 응답 DTO (JWT 토큰, 사용자 정보 등) + * @return JWT 토큰과 유저 정보가 포함된 응답 DTO */ @Transactional - public AuthResult completeSignupWithKakao(KakaoSignupRequestDto request) { + public AuthResultDto completeSignupWithKakao(KakaoSignupRequestDto request) { // 이메일 중복 체크 및 회원 생성, 저장 validateDuplicateEmail(request.getEmail()); User user = createAndSaveUser(request); @@ -497,18 +499,18 @@ public AuthResult completeSignupWithKakao(KakaoSignupRequestDto request) { UserInfoDto userInfo = buildUserInfoDto(user); // 토큰 쌍과 사용자 정보 반환 - return new AuthResult(new TokenPair(access.token(), refresh.token()), userInfo); + return new AuthResultDto(new TokenPairDto(access.token(), refresh.token()), userInfo); } /** * 리프레시 토큰을 사용하여 액세스 토큰을 재발급하는 메서드 * * @param request HttpServletRequest - 쿠키에서 리프레시 토큰을 추출 - * @return AuthResponseDto 사용자 정보가 포함된 응답 DTO (토큰은 쿠키로 설정됨) + * @return JWT 토큰과 유저 정보가 포함된 응답 DTO * @throws RestApiException 토큰 유효성 실패 혹은 사용자 정보 미발견 시 예외 발생 */ @Transactional - public AuthResult refreshAccessToken(HttpServletRequest request) { + public AuthResultDto refreshAccessToken(HttpServletRequest request) { // 쿠키에서 리프레시 토큰 추출 String refreshToken = null; if (request.getCookies() != null) { @@ -554,7 +556,7 @@ public AuthResult refreshAccessToken(HttpServletRequest request) { UserInfoDto userInfo = buildUserInfoDto(user); // 토큰 쌍과 사용자 정보 반환 - return new AuthResult(new TokenPair(newAccess.token(), newRefresh.token()), userInfo); + return new AuthResultDto(new TokenPairDto(newAccess.token(), newRefresh.token()), userInfo); } diff --git a/src/main/java/com/yourmode/yourmodebackend/domain/user/service/PasswordResetService.java b/src/main/java/com/yourmode/yourmodebackend/domain/user/service/PasswordResetService.java index cddbd16..475d998 100644 --- a/src/main/java/com/yourmode/yourmodebackend/domain/user/service/PasswordResetService.java +++ b/src/main/java/com/yourmode/yourmodebackend/domain/user/service/PasswordResetService.java @@ -8,5 +8,4 @@ public interface PasswordResetService { void sendSMS(SmsSendRequestDto request); void verifyCode(SmsVerifyRequestDto request); void changePassword(PasswordChangeRequestDto request); - } From 2071d91a4254a92f85679e187b454d369a9c8969 Mon Sep 17 00:00:00 2001 From: jinhee0 Date: Sun, 3 Aug 2025 14:26:15 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=EC=9D=84=20=EB=B0=B1?= =?UTF-8?q?=EC=97=94=EB=93=9C=20=EC=BD=9C=EB=B0=B1=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/auth/oauth2/kakao/callback 엔드포인트 추가 --- .../user/controller/AuthController.java | 131 ++++++++---------- .../dto/request/KakaoLoginRequestDto.java | 18 --- .../domain/user/service/AuthService.java | 2 +- .../domain/user/service/AuthServiceImpl.java | 105 +++++++------- .../domain/user/status/UserErrorStatus.java | 2 + 5 files changed, 110 insertions(+), 148 deletions(-) delete mode 100644 src/main/java/com/yourmode/yourmodebackend/domain/user/dto/request/KakaoLoginRequestDto.java diff --git a/src/main/java/com/yourmode/yourmodebackend/domain/user/controller/AuthController.java b/src/main/java/com/yourmode/yourmodebackend/domain/user/controller/AuthController.java index 88c4f06..80ef5fc 100644 --- a/src/main/java/com/yourmode/yourmodebackend/domain/user/controller/AuthController.java +++ b/src/main/java/com/yourmode/yourmodebackend/domain/user/controller/AuthController.java @@ -5,7 +5,9 @@ import com.yourmode.yourmodebackend.domain.user.dto.response.AuthResponseDto; import com.yourmode.yourmodebackend.domain.user.dto.response.UserIdResponseDto; import com.yourmode.yourmodebackend.domain.user.service.AuthService; +import com.yourmode.yourmodebackend.domain.user.status.UserErrorStatus; import com.yourmode.yourmodebackend.global.common.base.BaseResponse; +import com.yourmode.yourmodebackend.global.common.exception.RestApiException; import com.yourmode.yourmodebackend.global.config.security.auth.CurrentUser; import com.yourmode.yourmodebackend.global.config.security.auth.PrincipalDetails; import io.swagger.v3.oas.annotations.Operation; @@ -357,8 +359,8 @@ public ResponseEntity> login( * @return 카카오 인증 URL */ @Operation( - summary = "카카오 로그인 인증 요청(임시)", - description = "카카오 인증 URL을 반환합니다." + summary = "카카오 로그인 인증 요청", + description = "카카오 인증 URL을 반환합니다. 프론트엔드에서 이 URL로 리다이렉트하여 카카오 로그인을 진행합니다." ) @CrossOrigin(origins = "*", allowedHeaders = "*") @GetMapping("/oauth2/kakao/authorize") @@ -371,28 +373,21 @@ public ResponseEntity> getKakaoAuthUrl() { return ResponseEntity.ok(BaseResponse.onSuccess(kakaoAuthUrl)); } - /** - * 카카오 로그인 요청 처리. - * - * @param request KakaoLoginRequestDto - 카카오 인가 코드를 담은 데이터 - * @param response 쿠키 설정을 위한 HttpServletResponse - * @return 로그인 성공 시 유저 정보가 포함된 응답을 반환 - */ @Operation( - summary = "카카오 로그인 요청 처리", - description = "인가 코드(code)를 받아 카카오 로그인 처리를 수행합니다. 신규 회원일 시 \"additionalInfoNeeded\"에 사용자 정보를 담아 반환합니다." + summary = "카카오 로그인 콜백 처리", + description = "카카오 인증 후 리다이렉트되는 콜백 URL입니다. 인가 코드를 받아 로그인 처리를 수행합니다. 신규 사용자인 경우 추가 정보 입력이 필요하며, 기존 사용자인 경우 바로 로그인됩니다." ) @ApiResponses({ @ApiResponse( responseCode = "200", - description = "로그인 성공 (추가 정보 필요 여부에 따라 결과가 다름)", + description = "로그인 성공 또는 추가 정보 필요", content = @Content( mediaType = "application/json", schema = @Schema(implementation = BaseResponse.class), examples = { @ExampleObject( - name = "추가 정보가 필요 없는 로그인 성공", - summary = "기존 회원 로그인 성공, 추가 정보 필요 없음", + name = "기존 사용자 로그인 성공", + summary = "기존 사용자 카카오 로그인 성공 (토큰 설정됨)", value = """ { "timestamp": "2025-06-29T12:34:56.789", @@ -401,26 +396,28 @@ public ResponseEntity> getKakaoAuthUrl() { "result": { "user": { "name": "홍길동", + "email": "user@example.com", "role": "USER", - }, - "additionalInfoNeeded": null + "isNewUser": false + } } } """ ), @ExampleObject( - name = "추가 정보가 필요한 로그인 성공", - summary = "신규 회원 로그인 성공, 추가 정보 필요", + name = "신규 사용자 추가 정보 필요", + summary = "신규 사용자 카카오 로그인 (추가 정보 입력 필요)", value = """ { "timestamp": "2025-06-29T12:35:00.123", "code": "COMMON200", "message": "요청에 성공하였습니다.", "result": { - "user": null, - "additionalInfoNeeded": { + "user": { + "name": "신규 사용자", "email": "newuser@example.com", - "name": "신규 회원" + "role": "USER", + "isNewUser": true } } } @@ -430,82 +427,64 @@ public ResponseEntity> getKakaoAuthUrl() { ) ), @ApiResponse( - responseCode = "404", - description = "사용자 정보가 존재하지 않는 경우", + responseCode = "400", + description = "인가 코드 누락 또는 잘못된 요청", content = @Content( mediaType = "application/json", schema = @Schema(implementation = BaseResponse.class), examples = @ExampleObject( - name = "사용자 미존재", - summary = "이메일은 존재하지만 사용자 정보가 없을 때 발생", + name = "인가 코드 누락", + summary = "인가 코드가 없는 경우", value = """ - { - "timestamp": "2025-06-29T12:45:00.000", - "code": "USER-404-001", - "message": "사용자를 찾을 수 없습니다." - } - """ + { + "timestamp": "2025-06-29T12:45:00.000", + "code": "AUTH-400-001", + "message": "인가 코드가 필요합니다." + } + """ ) ) ), @ApiResponse( responseCode = "502", - description = "카카오 토큰/사용자 정보 요청 실패 관련 에러들", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = BaseResponse.class), - examples = { - @ExampleObject( - name = "카카오 토큰 요청 실패", - summary = "", - value = """ - { - "timestamp": "2025-06-29T12:40:00.000", - "code": "KAKAO-502-001", - "message": "카카오 토큰 요청에 실패했습니다." - } - """), - @ExampleObject( - name = "카카오 사용자 정보 요청 실패", - summary = "", - value = """ - { - "timestamp": "2025-06-29T12:40:01.000", - "code": "KAKAO-502-002", - "message": "카카오 사용자 정보 요청에 실패했습니다." - } - """) - } - ) - ), - @ApiResponse( - responseCode = "503", - description = "카카오 서버와의 통신 불가능", + description = "카카오 API 호출 실패", content = @Content( mediaType = "application/json", schema = @Schema(implementation = BaseResponse.class), examples = @ExampleObject( - name = "카카오 서버와 통신 불가능", - summary = "", + name = "카카오 API 실패", + summary = "카카오 토큰 또는 사용자 정보 요청 실패", value = """ - { - "timestamp": "2025-06-29T12:40:00.000", - "code": "KAKAO-503-001", - "message": "카카오 서버와의 통신이 불가능합니다." - } - """) + { + "timestamp": "2025-06-29T12:45:00.000", + "code": "KAKAO-502-001", + "message": "카카오 토큰 요청에 실패했습니다." + } + """ + ) ) ) }) - @CrossOrigin(origins = "*", allowedHeaders = "*") - @PostMapping("/oauth2/kakao/login") - public ResponseEntity> loginWithKakao( - @Valid @RequestBody KakaoLoginRequestDto request, + @GetMapping("/oauth2/kakao/callback") + public ResponseEntity> handleKakaoCallback( + @RequestParam(value = "code", required = false) String code, + @RequestParam(value = "error", required = false) String error, HttpServletResponse response ) { - AuthResultDto authResult = authService.processKakaoLogin(request); + // 에러 파라미터가 있으면 에러 처리 + if (error != null) { + throw new RestApiException(UserErrorStatus.KAKAO_AUTH_DENIED); + } + + // 인가 코드가 없으면 에러 + if (code == null || code.isEmpty()) { + throw new RestApiException(UserErrorStatus.KAKAO_AUTH_CODE_MISSING); + } + + // 카카오 로그인 처리 + AuthResultDto authResult = authService.handleKakaoCallback(code); - // 토큰이 있으면 쿠키로 설정 (기존 회원) + // 기존 사용자인 경우 토큰을 쿠키에 설정 if (authResult.tokenPair().accessToken() != null && authResult.tokenPair().refreshToken() != null) { setTokenCookies(response, authResult.tokenPair().accessToken(), authResult.tokenPair().refreshToken()); } diff --git a/src/main/java/com/yourmode/yourmodebackend/domain/user/dto/request/KakaoLoginRequestDto.java b/src/main/java/com/yourmode/yourmodebackend/domain/user/dto/request/KakaoLoginRequestDto.java deleted file mode 100644 index 1e92967..0000000 --- a/src/main/java/com/yourmode/yourmodebackend/domain/user/dto/request/KakaoLoginRequestDto.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.yourmode.yourmodebackend.domain.user.dto.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "카카오 로그인 요청 DTO") -public class KakaoLoginRequestDto { - - @Schema(description = "카카오에서 받은 인증 코드", example = "AQABAAE...코드내용...") - @NotBlank(message = "authorizationCode는 필수입니다.") - private String authorizationCode; -} diff --git a/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthService.java b/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthService.java index bc19b4d..3b08bc5 100644 --- a/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthService.java +++ b/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthService.java @@ -9,7 +9,7 @@ public interface AuthService { AuthResultDto signUp(LocalSignupRequestDto request); AuthResultDto login(LocalLoginRequestDto request); - AuthResultDto processKakaoLogin(KakaoLoginRequestDto request); + AuthResultDto handleKakaoCallback(String code); AuthResultDto completeSignupWithKakao(KakaoSignupRequestDto request); AuthResultDto refreshAccessToken(HttpServletRequest request); UserIdResponseDto logout(PrincipalDetails principal); diff --git a/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthServiceImpl.java b/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthServiceImpl.java index 6b8733f..065d54e 100644 --- a/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthServiceImpl.java +++ b/src/main/java/com/yourmode/yourmodebackend/domain/user/service/AuthServiceImpl.java @@ -385,21 +385,63 @@ public Map requestUserInfoWithKakao(String accessToken) { } } + + + /** + * 카카오 회원가입 완료 처리 메서드 + * 1) 이메일 중복 여부 확인 + * 2) User 엔티티 생성 및 저장 + * 3) UserCredential 저장 (비밀번호는 null, Kakao OAuth 정보 포함) + * 4) UserProfile 저장 (키, 몸무게, 성별, 체형 등 프로필 정보) + * 5) JWT 토큰 생성 및 저장 + * 6) 로그인 완료 응답 반환 + * + * @param request 카카오 회원가입 요청 DTO (추가 정보 포함) + * @return JWT 토큰과 유저 정보가 포함된 응답 DTO + */ + @Transactional + public AuthResultDto completeSignupWithKakao(KakaoSignupRequestDto request) { + // 이메일 중복 체크 및 회원 생성, 저장 + validateDuplicateEmail(request.getEmail()); + User user = createAndSaveUser(request); + saveUserCredential(user, null, OAuthProvider.KAKAO, null); + saveUserProfile(user, request); + + // PrincipalDetails 생성 (password는 null or "") + PrincipalDetails principalDetails = new PrincipalDetails(user, ""); + + // 인증 토큰 생성 (인증매니저가 처리할 authentication) + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities()); + + // 인증 성공 시 SecurityContext에 저장 (로그인 상태 유지) + SecurityContextHolder.getContext().setAuthentication(authentication); + + // JWT 토큰 생성 및 저장 + Integer userId = user.getId(); + JwtProvider.JwtWithExpiry access = jwtProvider.generateAccessToken(userId, user.getEmail()); + JwtProvider.JwtWithExpiry refresh = jwtProvider.generateRefreshToken(userId, user.getEmail()); + saveUserToken(user, refresh.token(), refresh.expiry()); + + // 사용자 정보 생성 + UserInfoDto userInfo = buildUserInfoDto(user); + + // 토큰 쌍과 사용자 정보 반환 + return new AuthResultDto(new TokenPairDto(access.token(), refresh.token()), userInfo); + } + /** - * 카카오 인가 코드로 로그인 처리하는 메서드 - * 1) 인가 코드로 토큰 발급 - * 2) 토큰으로 사용자 정보 조회 - * 3) 이메일로 회원가입 여부 확인 - * 4) 신규회원이면 null 토큰과 사용자 정보를 반환, 기존 회원이면 JWT 발급하여 로그인 처리 + * 카카오 콜백 처리를 위한 메서드 + * 신규 사용자인 경우 토큰을 null로 설정하고, 기존 사용자인 경우 토큰을 설정합니다. * - * @param request authorizationCode 카카오 인가 코드 - * @return 인증 결과 (토큰 쌍과 사용자 정보) - * @throws RestApiException 토큰 발급 실패, 이메일 누락, 회원 미존재 등 예외 상황 + * @param code 카카오 인가 코드 + * @return JWT 토큰과 유저 정보가 포함된 응답 DTO (신규 사용자는 토큰이 null) + * @throws RestApiException 카카오 API 호출 실패 시 예외 발생 */ @Transactional - public AuthResultDto processKakaoLogin(KakaoLoginRequestDto request) { + public AuthResultDto handleKakaoCallback(String code) { // 카카오 토큰 발급 - Map tokenInfo = requestTokenWithKakao(request.getAuthorizationCode()); + Map tokenInfo = requestTokenWithKakao(code); String accessToken = (String) tokenInfo.get("access_token"); // 카카오 사용자 정보 조회 @@ -459,49 +501,6 @@ public AuthResultDto processKakaoLogin(KakaoLoginRequestDto request) { return new AuthResultDto(new TokenPairDto(access.token(), refresh.token()), userInfo); } - /** - * 카카오 회원가입 완료 처리 메서드 - * 1) 이메일 중복 여부 확인 - * 2) User 엔티티 생성 및 저장 - * 3) UserCredential 저장 (비밀번호는 null, Kakao OAuth 정보 포함) - * 4) UserProfile 저장 (키, 몸무게, 성별, 체형 등 프로필 정보) - * 5) JWT 토큰 생성 및 저장 - * 6) 로그인 완료 응답 반환 - * - * @param request 카카오 회원가입 요청 DTO (추가 정보 포함) - * @return JWT 토큰과 유저 정보가 포함된 응답 DTO - */ - @Transactional - public AuthResultDto completeSignupWithKakao(KakaoSignupRequestDto request) { - // 이메일 중복 체크 및 회원 생성, 저장 - validateDuplicateEmail(request.getEmail()); - User user = createAndSaveUser(request); - saveUserCredential(user, null, OAuthProvider.KAKAO, null); - saveUserProfile(user, request); - - // PrincipalDetails 생성 (password는 null or "") - PrincipalDetails principalDetails = new PrincipalDetails(user, ""); - - // 인증 토큰 생성 (인증매니저가 처리할 authentication) - UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities()); - - // 인증 성공 시 SecurityContext에 저장 (로그인 상태 유지) - SecurityContextHolder.getContext().setAuthentication(authentication); - - // JWT 토큰 생성 및 저장 - Integer userId = user.getId(); - JwtProvider.JwtWithExpiry access = jwtProvider.generateAccessToken(userId, user.getEmail()); - JwtProvider.JwtWithExpiry refresh = jwtProvider.generateRefreshToken(userId, user.getEmail()); - saveUserToken(user, refresh.token(), refresh.expiry()); - - // 사용자 정보 생성 - UserInfoDto userInfo = buildUserInfoDto(user); - - // 토큰 쌍과 사용자 정보 반환 - return new AuthResultDto(new TokenPairDto(access.token(), refresh.token()), userInfo); - } - /** * 리프레시 토큰을 사용하여 액세스 토큰을 재발급하는 메서드 * diff --git a/src/main/java/com/yourmode/yourmodebackend/domain/user/status/UserErrorStatus.java b/src/main/java/com/yourmode/yourmodebackend/domain/user/status/UserErrorStatus.java index a52a845..a8a3039 100644 --- a/src/main/java/com/yourmode/yourmodebackend/domain/user/status/UserErrorStatus.java +++ b/src/main/java/com/yourmode/yourmodebackend/domain/user/status/UserErrorStatus.java @@ -25,6 +25,8 @@ public enum UserErrorStatus implements BaseCodeInterface { KAKAO_USERINFO_REQUEST_FAILED(HttpStatus.BAD_GATEWAY, "KAKAO-502-002", "카카오 사용자 정보 요청에 실패했습니다."), KAKAO_API_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "KAKAO-503-001", "카카오 서버와의 통신이 불가능합니다."), KAKAO_EMAIL_NOT_FOUND(HttpStatus.BAD_REQUEST, "KAKAO-400-001", "카카오 계정에서 이메일을 받아올 수 없습니다."), + KAKAO_AUTH_DENIED(HttpStatus.BAD_REQUEST, "AUTH-400-004", "카카오 인증이 거부되었습니다."), + KAKAO_AUTH_CODE_MISSING(HttpStatus.BAD_REQUEST, "AUTH-400-005", "인가 코드가 필요합니다."), // 로그인 AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "AUTH-401-001", "인증에 실패했습니다."), // 기타 인증 실패