diff --git a/api/build.gradle b/api/build.gradle index 86858757..11a3dcb2 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -20,6 +20,8 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.12.6' implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + runtimeOnly 'com.mysql:mysql-connector-j' runtimeOnly 'com.h2database:h2' diff --git a/api/src/main/java/com/pinback/api/config/filter/JwtAuthenticationFilter.java b/api/src/main/java/com/pinback/api/config/filter/JwtAuthenticationFilter.java index ae912097..8d88accb 100644 --- a/api/src/main/java/com/pinback/api/config/filter/JwtAuthenticationFilter.java +++ b/api/src/main/java/com/pinback/api/config/filter/JwtAuthenticationFilter.java @@ -96,7 +96,11 @@ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletExce path.startsWith("/v3/api-docs") || path.startsWith("/docs") || path.startsWith("/api/v1/test/push") || - path.startsWith("/api/v1/test/health") + path.startsWith("/api/v1/test/health") || + path.startsWith("/api/v1/auth/google") || + path.startsWith("/oauth/callback") || + path.startsWith("/login/google") || + path.startsWith("/login/oauth2/code/google") ; } } diff --git a/api/src/main/java/com/pinback/api/config/security/SecurityConfig.java b/api/src/main/java/com/pinback/api/config/security/SecurityConfig.java index b29e0a78..514a5986 100644 --- a/api/src/main/java/com/pinback/api/config/security/SecurityConfig.java +++ b/api/src/main/java/com/pinback/api/config/security/SecurityConfig.java @@ -52,13 +52,20 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ).permitAll() .requestMatchers( - "/api/v1/auth/token" + "/api/v1/auth/token", + "/api/v1/auth/google" ).permitAll() .requestMatchers( "/api/v1/test/*" ).permitAll() + .requestMatchers( + "/login/google", + "/oauth/callback", + "/login/oauth2/code/google" + ).permitAll() + .anyRequest().authenticated() ) .formLogin(AbstractHttpConfigurer::disable) diff --git a/api/src/main/java/com/pinback/api/google/controller/GoogleLonginController.java b/api/src/main/java/com/pinback/api/google/controller/GoogleLonginController.java new file mode 100644 index 00000000..028a9037 --- /dev/null +++ b/api/src/main/java/com/pinback/api/google/controller/GoogleLonginController.java @@ -0,0 +1,44 @@ +package com.pinback.api.google.controller; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.pinback.api.google.dto.request.GoogleLoginRequest; +import com.pinback.application.auth.usecase.AuthUsecase; +import com.pinback.application.google.dto.response.GoogleLoginResponse; +import com.pinback.application.google.usecase.GoogleUsecase; +import com.pinback.shared.dto.ResponseDto; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + +@Slf4j +@RestController +@RequestMapping("/api/v1/auth/google") +@RequiredArgsConstructor +@Tag(name = "Google", description = "구글 소셜 로그인 API") +public class GoogleLonginController { + + private final GoogleUsecase googleUsecase; + private final AuthUsecase authUsecase; + + @Operation(summary = "구글 소셜 로그인", description = "구글을 통한 소셜 로그인을 진행합니다") + @PostMapping() + public Mono> googleLogin( + @Valid @RequestBody GoogleLoginRequest request + ) { + return googleUsecase.getUserInfo(request.toCommand()) + .flatMap(googleResponse -> { + return authUsecase.getInfoAndToken(googleResponse.email()) + .map(loginResponse -> { + return ResponseDto.ok(loginResponse); + }); + }); + } +} diff --git a/api/src/main/java/com/pinback/api/google/dto/request/GoogleLoginRequest.java b/api/src/main/java/com/pinback/api/google/dto/request/GoogleLoginRequest.java new file mode 100644 index 00000000..78e69a33 --- /dev/null +++ b/api/src/main/java/com/pinback/api/google/dto/request/GoogleLoginRequest.java @@ -0,0 +1,14 @@ +package com.pinback.api.google.dto.request; + +import com.pinback.application.google.dto.GoogleLoginCommand; + +import jakarta.validation.constraints.NotNull; + +public record GoogleLoginRequest( + @NotNull(message = "인가 코드(code)는 비어있을 수 없습니다.") + String code +) { + public GoogleLoginCommand toCommand() { + return new GoogleLoginCommand(code); + } +} diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index 23a9d1ea..49f2a9d2 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -27,3 +27,10 @@ jwt: issuer: ${ISSUER} fcm: ${FCM_JSON} + +google: + client-id: ${CLIENT_ID} + client-secret: ${CLIENT_SECRET} + redirect-uri: ${REDIRECT_URI} + token-uri: ${TOKEN_URI} + user-info-uri: ${USER_INFO_URI} diff --git a/application/build.gradle b/application/build.gradle index 4c676534..6379c853 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -4,13 +4,15 @@ dependencies { // 도메인 모듈 (핵심 비즈니스 로직) api project(':domain') - + // Spring Boot (애플리케이션 서비스를 위한 기본 스프링 기능) implementation 'org.springframework.boot:spring-boot-starter' - + // 트랜잭션 관리와 페이징을 위한 JPA implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - + // Test용 H2 데이터베이스 testImplementation 'com.h2database:h2' + + implementation 'org.springframework.boot:spring-boot-starter-webflux' } diff --git a/application/src/main/java/com/pinback/application/auth/usecase/AuthUsecase.java b/application/src/main/java/com/pinback/application/auth/usecase/AuthUsecase.java index 223cea0f..b4609254 100644 --- a/application/src/main/java/com/pinback/application/auth/usecase/AuthUsecase.java +++ b/application/src/main/java/com/pinback/application/auth/usecase/AuthUsecase.java @@ -7,6 +7,7 @@ import com.pinback.application.auth.dto.SignUpResponse; import com.pinback.application.auth.dto.TokenResponse; import com.pinback.application.auth.service.JwtProvider; +import com.pinback.application.google.dto.response.GoogleLoginResponse; import com.pinback.application.notification.port.in.SavePushSubscriptionPort; import com.pinback.application.user.port.out.UserGetServicePort; import com.pinback.application.user.port.out.UserSaveServicePort; @@ -14,7 +15,10 @@ import com.pinback.domain.user.entity.User; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; +@Slf4j @Service @RequiredArgsConstructor public class AuthUsecase { @@ -46,4 +50,28 @@ public TokenResponse getToken(String email) { return new TokenResponse(accessToken); } + + @Transactional(readOnly = true) + public Mono getInfoAndToken(String email) { + return userGetServicePort.findUserByEmail(email) + .flatMap(existingUser -> { + log.info("기존 사용자 로그인 성공: User ID {}", existingUser.getId()); + + //Access Token 발급 + String accessToken = jwtProvider.createAccessToken(existingUser.getId()); + + return Mono.just(GoogleLoginResponse.loggedIn( + existingUser.getId(), existingUser.getEmail(), accessToken + )); + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("신규 유저 - 임시 유저 생성"); + User tempUser = User.createTempUser(email); + + return userSaveServicePort.saveUser(tempUser) + .flatMap(savedUser -> { + return Mono.just(GoogleLoginResponse.tempLogin(savedUser.getId(), savedUser.getEmail())); + }); + })); + } } diff --git a/application/src/main/java/com/pinback/application/common/exception/GoogleApiException.java b/application/src/main/java/com/pinback/application/common/exception/GoogleApiException.java new file mode 100644 index 00000000..8154649e --- /dev/null +++ b/application/src/main/java/com/pinback/application/common/exception/GoogleApiException.java @@ -0,0 +1,10 @@ +package com.pinback.application.common.exception; + +import com.pinback.shared.constant.ExceptionCode; +import com.pinback.shared.exception.ApplicationException; + +public class GoogleApiException extends ApplicationException { + public GoogleApiException() { + super(ExceptionCode.GOOGLE_API_ERROR); + } +} diff --git a/application/src/main/java/com/pinback/application/common/exception/GoogleEmailMissingException.java b/application/src/main/java/com/pinback/application/common/exception/GoogleEmailMissingException.java new file mode 100644 index 00000000..9aa4ceda --- /dev/null +++ b/application/src/main/java/com/pinback/application/common/exception/GoogleEmailMissingException.java @@ -0,0 +1,10 @@ +package com.pinback.application.common.exception; + +import com.pinback.shared.constant.ExceptionCode; +import com.pinback.shared.exception.ApplicationException; + +public class GoogleEmailMissingException extends ApplicationException { + public GoogleEmailMissingException() { + super(ExceptionCode.GOOGLE_EMAIL_MISSING); + } +} diff --git a/application/src/main/java/com/pinback/application/common/exception/GoogleTokenMissingException.java b/application/src/main/java/com/pinback/application/common/exception/GoogleTokenMissingException.java new file mode 100644 index 00000000..f99c2347 --- /dev/null +++ b/application/src/main/java/com/pinback/application/common/exception/GoogleTokenMissingException.java @@ -0,0 +1,10 @@ +package com.pinback.application.common.exception; + +import com.pinback.shared.constant.ExceptionCode; +import com.pinback.shared.exception.ApplicationException; + +public class GoogleTokenMissingException extends ApplicationException { + public GoogleTokenMissingException() { + super(ExceptionCode.GOOGLE_TOKEN_MISSING); + } +} diff --git a/application/src/main/java/com/pinback/application/google/dto/GoogleLoginCommand.java b/application/src/main/java/com/pinback/application/google/dto/GoogleLoginCommand.java new file mode 100644 index 00000000..e5f20392 --- /dev/null +++ b/application/src/main/java/com/pinback/application/google/dto/GoogleLoginCommand.java @@ -0,0 +1,6 @@ +package com.pinback.application.google.dto; + +public record GoogleLoginCommand( + String code +) { +} diff --git a/application/src/main/java/com/pinback/application/google/dto/response/GoogleApiResponse.java b/application/src/main/java/com/pinback/application/google/dto/response/GoogleApiResponse.java new file mode 100644 index 00000000..b9911297 --- /dev/null +++ b/application/src/main/java/com/pinback/application/google/dto/response/GoogleApiResponse.java @@ -0,0 +1,14 @@ +package com.pinback.application.google.dto.response; + +import org.springframework.lang.Nullable; + +public record GoogleApiResponse( + String id, + String email, + Boolean verifiedEmail, + String name, + @Nullable String givenName, + @Nullable String familyName, + String picture +) { +} diff --git a/application/src/main/java/com/pinback/application/google/dto/response/GoogleLoginResponse.java b/application/src/main/java/com/pinback/application/google/dto/response/GoogleLoginResponse.java new file mode 100644 index 00000000..63d0c565 --- /dev/null +++ b/application/src/main/java/com/pinback/application/google/dto/response/GoogleLoginResponse.java @@ -0,0 +1,18 @@ +package com.pinback.application.google.dto.response; + +import java.util.UUID; + +public record GoogleLoginResponse( + boolean isUser, + UUID userId, + String email, + String accessToken +) { + public static GoogleLoginResponse loggedIn(UUID userId, String email, String accessToken) { + return new GoogleLoginResponse(true, userId, email, accessToken); + } + + public static GoogleLoginResponse tempLogin(UUID userId, String email) { + return new GoogleLoginResponse(false, userId, email, null); + } +} diff --git a/application/src/main/java/com/pinback/application/google/dto/response/GoogleTokenResponse.java b/application/src/main/java/com/pinback/application/google/dto/response/GoogleTokenResponse.java new file mode 100644 index 00000000..3bbddfbb --- /dev/null +++ b/application/src/main/java/com/pinback/application/google/dto/response/GoogleTokenResponse.java @@ -0,0 +1,14 @@ +package com.pinback.application.google.dto.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record GoogleTokenResponse( + String accessToken, + String expiresIn, + String tokenType, + String scope, + String idToken +) { +} diff --git a/application/src/main/java/com/pinback/application/google/dto/response/GoogleUserInfoResponse.java b/application/src/main/java/com/pinback/application/google/dto/response/GoogleUserInfoResponse.java new file mode 100644 index 00000000..943c3f33 --- /dev/null +++ b/application/src/main/java/com/pinback/application/google/dto/response/GoogleUserInfoResponse.java @@ -0,0 +1,9 @@ +package com.pinback.application.google.dto.response; + +public record GoogleUserInfoResponse( + String email +) { + public static GoogleUserInfoResponse from(String email) { + return new GoogleUserInfoResponse(email); + } +} diff --git a/application/src/main/java/com/pinback/application/google/port/out/GoogleOAuthPort.java b/application/src/main/java/com/pinback/application/google/port/out/GoogleOAuthPort.java new file mode 100644 index 00000000..13d3fc06 --- /dev/null +++ b/application/src/main/java/com/pinback/application/google/port/out/GoogleOAuthPort.java @@ -0,0 +1,9 @@ +package com.pinback.application.google.port.out; + +import com.pinback.application.google.dto.response.GoogleUserInfoResponse; + +import reactor.core.publisher.Mono; + +public interface GoogleOAuthPort { + Mono fetchUserInfo(String code); +} diff --git a/application/src/main/java/com/pinback/application/google/service/GoogleOAuthClient.java b/application/src/main/java/com/pinback/application/google/service/GoogleOAuthClient.java new file mode 100644 index 00000000..86efaa91 --- /dev/null +++ b/application/src/main/java/com/pinback/application/google/service/GoogleOAuthClient.java @@ -0,0 +1,117 @@ +package com.pinback.application.google.service; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +import com.pinback.application.common.exception.GoogleApiException; +import com.pinback.application.common.exception.GoogleEmailMissingException; +import com.pinback.application.common.exception.GoogleTokenMissingException; +import com.pinback.application.google.dto.response.GoogleApiResponse; +import com.pinback.application.google.dto.response.GoogleTokenResponse; +import com.pinback.application.google.dto.response.GoogleUserInfoResponse; +import com.pinback.application.google.port.out.GoogleOAuthPort; + +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + +@Slf4j +@Service +public class GoogleOAuthClient implements GoogleOAuthPort { + private final WebClient googleWebClient; + private final String googleClientId; + private final String googleClientSecret; + private final String googleRedirectUri; + private final String googleTokenUri; + private final String googleUserInfoUri; + + public GoogleOAuthClient( + WebClient googleWebClient, + @Qualifier("googleClientId") String googleClientId, + @Qualifier("googleClientSecret") String googleClientSecret, + @Qualifier("googleRedirectUri") String googleRedirectUri, + @Qualifier("googleTokenUri") String googleTokenUri, + @Qualifier("googleUserInfoUri") String googleUserInfoUri) { + this.googleWebClient = googleWebClient; + this.googleClientId = googleClientId; + this.googleClientSecret = googleClientSecret; + this.googleRedirectUri = googleRedirectUri; + this.googleTokenUri = googleTokenUri; + this.googleUserInfoUri = googleUserInfoUri; + } + + @Override + public Mono fetchUserInfo(String code) { + + return requestAccessToken(code) + // 토큰 응답을 UserInfo 요청으로 변환하여 연결 + .flatMap(tokenResponse -> { + + // Access Token 유효성 검증 + if (tokenResponse == null || tokenResponse.accessToken() == null) { + log.info("tokenResponse: {}", tokenResponse); + log.error("Google Access Token 획득 실패: 응답 본문에 토큰이 없습니다. Code: {}", code); + return Mono.error(new GoogleTokenMissingException()); + } + // Access Token으로 사용자 정보 요청 + return getUserInfo(tokenResponse.accessToken()); + }); + } + + private Mono requestAccessToken(String code) { + log.info("redirect: {}", googleRedirectUri); + String requestBody = "code=" + code + + "&client_id=" + googleClientId + + "&client_secret=" + googleClientSecret + + "&redirect_uri=" + googleRedirectUri + + "&grant_type=authorization_code"; + + return googleWebClient.post() + .uri(googleTokenUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .bodyValue(requestBody) + .retrieve() + // HTTP 오류 발생 시 + .onStatus(status -> status.isError(), clientResponse -> + clientResponse.bodyToMono(String.class) + .flatMap(body -> { + String errorLog = String.format( + "[GoogleOAuth API 에러] HTTP Status: %s, Detail: %s", + clientResponse.statusCode(), body + ); + log.error(errorLog); + return Mono.error(new GoogleApiException()); + }) + ) + .bodyToMono(GoogleTokenResponse.class); + } + + private Mono getUserInfo(String accessToken) { + return googleWebClient.get() + .uri(googleUserInfoUri) + .header("Authorization", "Bearer " + accessToken) + .retrieve() + .onStatus(status -> status.isError(), clientResponse -> + clientResponse.bodyToMono(String.class) + .flatMap(body -> { + String errorLog = String.format( + "[Google UserInfo API 에러] HTTP Status: %s, Detail: %s", + clientResponse.statusCode(), body + ); + log.error(errorLog); + return Mono.error(new GoogleApiException()); + }) + ) + .bodyToMono(GoogleApiResponse.class) + .map(apiData -> { + // 이메일 필드 유효성 검사 + if (apiData.email() == null || apiData.email().isBlank()) { + log.error("Google UserInfo 응답에 이메일 필드가 누락되었습니다."); + throw new GoogleEmailMissingException(); + } + // 최종 DTO로 변환하여 반환 + return GoogleUserInfoResponse.from(apiData.email()); + }); + } +} diff --git a/application/src/main/java/com/pinback/application/google/usecase/GoogleUsecase.java b/application/src/main/java/com/pinback/application/google/usecase/GoogleUsecase.java new file mode 100644 index 00000000..d064d450 --- /dev/null +++ b/application/src/main/java/com/pinback/application/google/usecase/GoogleUsecase.java @@ -0,0 +1,23 @@ +package com.pinback.application.google.usecase; + +import org.springframework.stereotype.Service; + +import com.pinback.application.google.dto.GoogleLoginCommand; +import com.pinback.application.google.dto.response.GoogleUserInfoResponse; +import com.pinback.application.google.port.out.GoogleOAuthPort; + +import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Mono; + +@Service +@RequiredArgsConstructor +public class GoogleUsecase { + private final GoogleOAuthPort googleOAuthPort; + + public Mono getUserInfo(GoogleLoginCommand command) { + + String code = command.code(); + + return googleOAuthPort.fetchUserInfo(code); + } +} diff --git a/application/src/main/java/com/pinback/application/user/port/out/UserGetServicePort.java b/application/src/main/java/com/pinback/application/user/port/out/UserGetServicePort.java index 382edd97..e5d0fc53 100644 --- a/application/src/main/java/com/pinback/application/user/port/out/UserGetServicePort.java +++ b/application/src/main/java/com/pinback/application/user/port/out/UserGetServicePort.java @@ -4,8 +4,12 @@ import com.pinback.domain.user.entity.User; +import reactor.core.publisher.Mono; + public interface UserGetServicePort { User findByEmail(String email); User findById(UUID id); + + Mono findUserByEmail(String email); } diff --git a/application/src/main/java/com/pinback/application/user/port/out/UserSaveServicePort.java b/application/src/main/java/com/pinback/application/user/port/out/UserSaveServicePort.java index b28af141..a57daf1d 100644 --- a/application/src/main/java/com/pinback/application/user/port/out/UserSaveServicePort.java +++ b/application/src/main/java/com/pinback/application/user/port/out/UserSaveServicePort.java @@ -2,6 +2,10 @@ import com.pinback.domain.user.entity.User; +import reactor.core.publisher.Mono; + public interface UserSaveServicePort { User save(User user); + + Mono saveUser(User user); } diff --git a/build.gradle b/build.gradle index d29cfbfb..0ac8f8a5 100644 --- a/build.gradle +++ b/build.gradle @@ -55,13 +55,13 @@ subprojects { useJUnitPlatform() inputs.property("java.vendor", System.getProperty("java.vendor")) inputs.property("java.version", System.getProperty("java.version")) - + // 병렬 테스트 실행 maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 - + // 테스트 성능 최적화 jvmArgs = ['-XX:+UseG1GC', '-Xmx1g'] - + // 테스트 보고서 설정 testLogging { events "passed", "skipped", "failed" @@ -70,7 +70,7 @@ subprojects { showCauses = true showExceptions = true } - + // 테스트 완료 후 결과 출력 afterSuite { desc, result -> if (!desc.parent) { diff --git a/domain/src/main/java/com/pinback/domain/user/entity/User.java b/domain/src/main/java/com/pinback/domain/user/entity/User.java index ce4d105f..444b694c 100644 --- a/domain/src/main/java/com/pinback/domain/user/entity/User.java +++ b/domain/src/main/java/com/pinback/domain/user/entity/User.java @@ -25,6 +25,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class User extends BaseEntity { + private static final LocalTime TEMP_REMIND_DEFAULT_MARKER = null; + private static final Long DEFAULT_ACORN_COUNT = 0L; @Id @GeneratedValue(strategy = GenerationType.UUID) @@ -34,7 +36,7 @@ public class User extends BaseEntity { @Column(name = "email", nullable = false, unique = true) private String email; - @Column(name = "remind_default", nullable = false) + @Column(name = "remind_default", nullable = true) private LocalTime remindDefault; @Column(name = "acorn_count", nullable = false) @@ -44,7 +46,15 @@ public static User create(String email, LocalTime remindDefault) { return User.builder() .email(email) .remindDefault(remindDefault) - .acornCount(0L) + .acornCount(DEFAULT_ACORN_COUNT) + .build(); + } + + public static User createTempUser(String email) { + return User.builder() + .email(email) + .remindDefault(TEMP_REMIND_DEFAULT_MARKER) + .acornCount(DEFAULT_ACORN_COUNT) .build(); } diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle index 5aa64692..c35546f1 100644 --- a/infrastructure/build.gradle +++ b/infrastructure/build.gradle @@ -18,6 +18,9 @@ dependencies { implementation 'com.google.firebase:firebase-admin:9.2.0' implementation 'com.auth0:java-jwt:4.5.0' + + implementation 'org.springframework.boot:spring-boot-starter-webflux' + } def generated = layout.buildDirectory.dir("generated/querydsl").get().asFile diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/config/google/GoogleConfig.java b/infrastructure/src/main/java/com/pinback/infrastructure/config/google/GoogleConfig.java new file mode 100644 index 00000000..116474be --- /dev/null +++ b/infrastructure/src/main/java/com/pinback/infrastructure/config/google/GoogleConfig.java @@ -0,0 +1,55 @@ +package com.pinback.infrastructure.config.google; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class GoogleConfig { + + @Value("${google.client-id}") + private String clientId; + + @Value("${google.client-secret}") + private String clientSecret; + + @Value("${google.redirect-uri}") + private String redirectUri; + + @Value("${google.token-uri}") + private String tokenUri; + + @Value("${google.user-info-uri}") + private String userInfoUri; + + @Bean + public WebClient googleWebClient() { + return WebClient.builder().build(); + } + + @Bean + public String googleClientId() { + return clientId; + } + + @Bean + public String googleClientSecret() { + return clientSecret; + } + + @Bean + public String googleRedirectUri() { + return redirectUri; + } + + @Bean + public String googleTokenUri() { + return tokenUri; + } + + @Bean + public String googleUserInfoUri() { + return userInfoUri; + } +} diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/user/service/UserGetService.java b/infrastructure/src/main/java/com/pinback/infrastructure/user/service/UserGetService.java index b0008f9d..1ef78040 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/user/service/UserGetService.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/user/service/UserGetService.java @@ -11,6 +11,7 @@ import com.pinback.infrastructure.user.repository.UserRepository; import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Mono; @Service @RequiredArgsConstructor @@ -28,4 +29,9 @@ public User findByEmail(String email) { public User findById(UUID id) { return userRepository.findById(id).orElseThrow(UserNotFoundException::new); } + + @Override + public Mono findUserByEmail(String email) { + return Mono.justOrEmpty(userRepository.findByEmail(email)); + } } diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/user/service/UserSaveService.java b/infrastructure/src/main/java/com/pinback/infrastructure/user/service/UserSaveService.java index 552dfd42..630e412c 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/user/service/UserSaveService.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/user/service/UserSaveService.java @@ -8,6 +8,8 @@ import com.pinback.infrastructure.user.repository.UserRepository; import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; @Service @RequiredArgsConstructor @@ -20,4 +22,12 @@ public class UserSaveService implements UserSaveServicePort { public User save(User user) { return userRepository.save(user); } + + @Override + public Mono saveUser(User user) { + return Mono.fromCallable(() -> { + return userRepository.save(user); + }) + .subscribeOn(Schedulers.boundedElastic()); + } } diff --git a/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java b/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java index 99cc87b7..f107cb1b 100644 --- a/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java +++ b/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java @@ -40,8 +40,11 @@ public enum ExceptionCode { CATEGORY_ALREADY_EXIST(HttpStatus.CONFLICT, "c40903", "이미 존재하는 카테고리입니다."), //500 - INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "s50000", "서버 내부 오류가 발생했습니다."); - + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "s50000", "서버 내부 오류가 발생했습니다."), + GOOGLE_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "s50001", "Google API 처리 중 오류가 발생했습니다."), + EXTERNAL_SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "s50300", "외부 서비스(Google)와 통신할 수 없습니다."), + GOOGLE_TOKEN_MISSING(HttpStatus.INTERNAL_SERVER_ERROR, "s50002", "Google API에서 필수 Access Token을 받지 못했습니다."), + GOOGLE_EMAIL_MISSING(HttpStatus.INTERNAL_SERVER_ERROR, "s50003", "Google API에서 email을 받지 못했습니다."); private final HttpStatus status; private final String code; private final String message; diff --git a/src/main/resources/static/login.html b/src/main/resources/static/login.html new file mode 100644 index 00000000..388d449a --- /dev/null +++ b/src/main/resources/static/login.html @@ -0,0 +1,16 @@ + + + + + 로그인 + + + + + + + + \ No newline at end of file