diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..7c8df7b --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,67 @@ +name: CI/CD Pipeline with Docker + +on: + push: + branches: [ develop ] # develop 브랜치에 push 발생 시 실행 + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + cache: 'gradle' + + - name: Create application.yml + run: | + mkdir -p src/main/resources + cat < src/main/resources/application.yml + ${{ secrets.APPLICATION_YML }} + EOF + shell: bash + + - name: Build with Gradle + run: | + chmod +x gradlew + ./gradlew bootJar + + - name: Login to DockerHub + run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + + - name: Build and push Docker image + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ vars.MY_APP }}:latest . + docker push ${{ secrets.DOCKER_USERNAME }}/${{ vars.MY_APP }}:latest + + deploy: + needs: build + runs-on: ubuntu-latest + + steps: + - name: Deploy to EC2 + env: + EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }} + EC2_USERNAME: ${{ secrets.EC2_USERNAME }} + EC2_HOST: ${{ secrets.EC2_HOST }} + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + MY_APP: ${{ vars.MY_APP }} + run: | + echo "$EC2_SSH_KEY" > private_key.pem + chmod 600 private_key.pem + + ssh -i private_key.pem -o StrictHostKeyChecking=no $EC2_USERNAME@$EC2_HOST " + docker pull $DOCKER_USERNAME/$MY_APP:latest + docker stop $MY_APP || true + docker rm $MY_APP || true + docker run -d -p 8080:8080 --name $MY_APP \ + $DOCKER_USERNAME/$MY_APP:latest + " + + rm -f private_key.pem diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..96ecb44 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Build stage +FROM bellsoft/liberica-openjdk-alpine:17 AS builder + +WORKDIR /app + +COPY gradlew build.gradle settings.gradle /app/ +COPY gradle /app/gradle +RUN chmod +x gradlew +RUN ./gradlew dependencies --no-daemon + +COPY . . +RUN ./gradlew clean build -x test --no-daemon + +# Run stage +FROM bellsoft/liberica-openjdk-alpine:17 + +WORKDIR /app + +# JAR 파일 복사 +COPY --from=builder /app/build/libs/*.jar app.jar + +# 필요한 리소스 파일 복사 +COPY src/main/resources/application.yml /app/config/application.yml + +EXPOSE 8080 + +# Spring Boot가 설정 파일을 읽도록 환경 변수 설정 +ENTRYPOINT ["java", "-jar", "app.jar"] +CMD ["--spring.config.location=file:/app/config/application.yml"] diff --git a/src/main/java/com/memesphere/binance/service/BinanceQueryService.java b/src/main/java/com/memesphere/binance/service/BinanceQueryService.java deleted file mode 100644 index 0110274..0000000 --- a/src/main/java/com/memesphere/binance/service/BinanceQueryService.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.memesphere.binance.service; - -import com.memesphere.binance.dto.response.BinanceTickerResponse; - -public interface BinanceQueryService { - BinanceTickerResponse getTickerData(String symbol); -} diff --git a/src/main/java/com/memesphere/chat/repository/ChatCustomRepository.java b/src/main/java/com/memesphere/chat/repository/ChatCustomRepository.java deleted file mode 100644 index 73fedf6..0000000 --- a/src/main/java/com/memesphere/chat/repository/ChatCustomRepository.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.memesphere.chat.repository; - -public interface ChatCustomRepository { - -} diff --git a/src/main/java/com/memesphere/binance/dto/response/BinanceTickerResponse.java b/src/main/java/com/memesphere/domain/binance/dto/response/BinanceTickerResponse.java similarity index 85% rename from src/main/java/com/memesphere/binance/dto/response/BinanceTickerResponse.java rename to src/main/java/com/memesphere/domain/binance/dto/response/BinanceTickerResponse.java index 292b10a..4d6fa43 100644 --- a/src/main/java/com/memesphere/binance/dto/response/BinanceTickerResponse.java +++ b/src/main/java/com/memesphere/domain/binance/dto/response/BinanceTickerResponse.java @@ -1,4 +1,4 @@ -package com.memesphere.binance.dto.response; +package com.memesphere.domain.binance.dto.response; import lombok.Data; diff --git a/src/main/java/com/memesphere/domain/binance/service/BinanceQueryService.java b/src/main/java/com/memesphere/domain/binance/service/BinanceQueryService.java new file mode 100644 index 0000000..3f3cc3d --- /dev/null +++ b/src/main/java/com/memesphere/domain/binance/service/BinanceQueryService.java @@ -0,0 +1,7 @@ +package com.memesphere.domain.binance.service; + +import com.memesphere.domain.binance.dto.response.BinanceTickerResponse; + +public interface BinanceQueryService { + BinanceTickerResponse getTickerData(String symbol); +} diff --git a/src/main/java/com/memesphere/binance/service/BinanceQueryServiceImpl.java b/src/main/java/com/memesphere/domain/binance/service/BinanceQueryServiceImpl.java similarity index 85% rename from src/main/java/com/memesphere/binance/service/BinanceQueryServiceImpl.java rename to src/main/java/com/memesphere/domain/binance/service/BinanceQueryServiceImpl.java index 5704d7f..fbb8984 100644 --- a/src/main/java/com/memesphere/binance/service/BinanceQueryServiceImpl.java +++ b/src/main/java/com/memesphere/domain/binance/service/BinanceQueryServiceImpl.java @@ -1,6 +1,6 @@ -package com.memesphere.binance.service; +package com.memesphere.domain.binance.service; -import com.memesphere.binance.dto.response.BinanceTickerResponse; +import com.memesphere.domain.binance.dto.response.BinanceTickerResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/com/memesphere/domain/chartdata/converter/ChartDataConverter.java b/src/main/java/com/memesphere/domain/chartdata/converter/ChartDataConverter.java index bc79457..a83f6db 100644 --- a/src/main/java/com/memesphere/domain/chartdata/converter/ChartDataConverter.java +++ b/src/main/java/com/memesphere/domain/chartdata/converter/ChartDataConverter.java @@ -1,6 +1,6 @@ package com.memesphere.domain.chartdata.converter; -import com.memesphere.binance.dto.response.BinanceTickerResponse; +import com.memesphere.domain.binance.dto.response.BinanceTickerResponse; import com.memesphere.domain.chartdata.entity.ChartData; import com.memesphere.domain.memecoin.entity.MemeCoin; diff --git a/src/main/java/com/memesphere/domain/chartdata/scheduler/ChartDataScheduler.java b/src/main/java/com/memesphere/domain/chartdata/scheduler/ChartDataScheduler.java index d3eaba3..e0c8e73 100644 --- a/src/main/java/com/memesphere/domain/chartdata/scheduler/ChartDataScheduler.java +++ b/src/main/java/com/memesphere/domain/chartdata/scheduler/ChartDataScheduler.java @@ -1,7 +1,7 @@ package com.memesphere.domain.chartdata.scheduler; -import com.memesphere.binance.dto.response.BinanceTickerResponse; -import com.memesphere.binance.service.BinanceQueryService; +import com.memesphere.domain.binance.dto.response.BinanceTickerResponse; +import com.memesphere.domain.binance.service.BinanceQueryService; import com.memesphere.domain.chartdata.entity.ChartData; import com.memesphere.global.apipayload.code.status.ErrorStatus; import com.memesphere.global.apipayload.exception.GeneralException; diff --git a/src/main/java/com/memesphere/domain/memecoin/MemeCoinInitializer.java b/src/main/java/com/memesphere/domain/memecoin/MemeCoinInitializer.java deleted file mode 100644 index dd03e04..0000000 --- a/src/main/java/com/memesphere/domain/memecoin/MemeCoinInitializer.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.memesphere.domain.memecoin; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.memesphere.domain.memecoin.entity.MemeCoin; -import com.memesphere.domain.memecoin.repository.MemeCoinRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.boot.CommandLineRunner; -import org.springframework.stereotype.Component; - -import java.io.File; -import java.util.List; - -@Component -@RequiredArgsConstructor -public class MemeCoinInitializer implements CommandLineRunner { - private final MemeCoinRepository memeCoinRepository; - private final ObjectMapper objectMapper; - - @Override - public void run(String... args) throws Exception { - if (memeCoinRepository.count() == 0) { - String filePath = "src/main/resources/memecoin-storage/memecoin.json"; - - List memeCoins = objectMapper.readValue( - new File(filePath), - new TypeReference>() {} - ); - - memeCoinRepository.saveAll(memeCoins); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/memesphere/domain/memecoin/entity/MemeCoinInitializer.java b/src/main/java/com/memesphere/domain/memecoin/entity/MemeCoinInitializer.java new file mode 100644 index 0000000..860a0e0 --- /dev/null +++ b/src/main/java/com/memesphere/domain/memecoin/entity/MemeCoinInitializer.java @@ -0,0 +1,46 @@ +package com.memesphere.domain.memecoin.entity; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.memesphere.domain.memecoin.repository.MemeCoinRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class MemeCoinInitializer implements CommandLineRunner { + private final MemeCoinRepository memeCoinRepository; + private final ObjectMapper objectMapper; + + @Override + public void run(String... args) throws Exception { + if (memeCoinRepository.count() == 0) { + // JAR 내부 classpath에서 JSON 파일 로드 + try (InputStream inputStream = getClass().getClassLoader() + .getResourceAsStream("memecoin-storage/memecoin.json")) { + + if (inputStream == null) { + throw new IOException("'memecoin-storage/memecoin.json' 파일을 찾을 수 없습니다."); + } + + // JSON 파일을 Java 객체(List)로 변환 + List memeCoins = objectMapper.readValue( + inputStream, + new TypeReference>() {} + ); + + // DB에 저장 + memeCoinRepository.saveAll(memeCoins); + + } catch (IOException e) { + throw new RuntimeException("JSON 파일 로드 중 오류 발생: " + e.getMessage(), e); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/memesphere/domain/user/controller/TestLoginController.java b/src/main/java/com/memesphere/domain/user/controller/TestLoginController.java index 58284b6..a12464a 100644 --- a/src/main/java/com/memesphere/domain/user/controller/TestLoginController.java +++ b/src/main/java/com/memesphere/domain/user/controller/TestLoginController.java @@ -7,13 +7,17 @@ @org.springframework.stereotype.Controller public class TestLoginController { - @Value("${testLoginUrl}") - private String testLoginUrl; + @Value("${testKakaoLoginUrl}") + private String testKakaoLoginUrl; + + @Value("${testGoogleLoginUrl}") + private String testGoogleLoginUrl; @GetMapping("/login") public String login(Model model) { - model.addAttribute("testLoginUrl", testLoginUrl); + model.addAttribute("testKakaoLoginUrl", testKakaoLoginUrl); + model.addAttribute("testGoogleLoginUrl", testGoogleLoginUrl); return "login"; } } diff --git a/src/main/java/com/memesphere/domain/user/controller/UserController.java b/src/main/java/com/memesphere/domain/user/controller/UserController.java index 9cf046f..9ece014 100644 --- a/src/main/java/com/memesphere/domain/user/controller/UserController.java +++ b/src/main/java/com/memesphere/domain/user/controller/UserController.java @@ -1,13 +1,17 @@ package com.memesphere.domain.user.controller; +import com.memesphere.domain.user.dto.request.NicknameRequest; import com.memesphere.domain.user.dto.request.SignInRequest; import com.memesphere.domain.user.dto.request.SignUpRequest; +import com.memesphere.domain.user.dto.response.GoogleUserInfoResponse; import com.memesphere.domain.user.dto.response.TokenResponse; import com.memesphere.domain.user.dto.response.KakaoUserInfoResponse; import com.memesphere.domain.user.service.AuthServiceImpl; +import com.memesphere.domain.user.service.GoogleServiceImpl; import com.memesphere.domain.user.service.KakaoServiceImpl; import com.memesphere.global.apipayload.ApiResponse; import com.memesphere.domain.user.dto.response.LoginResponse; +import com.memesphere.global.apipayload.code.status.ErrorStatus; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -23,11 +27,12 @@ public class UserController { private final KakaoServiceImpl kakaoServiceImpl; + private final GoogleServiceImpl googleServiceImpl; private final AuthServiceImpl authServiceImpl; @PostMapping("/login/oauth2/kakao") @Operation(summary = "카카오 로그인/회원가입 API") - public ApiResponse callback(@RequestParam("code") String code) throws IOException { + public ApiResponse kakaoLogin(@RequestParam("code") String code) throws IOException { TokenResponse kakaoTokenResponse = kakaoServiceImpl.getAccessTokenFromKakao(code); KakaoUserInfoResponse kakaoUserInfoResponse = kakaoServiceImpl.getUserInfo(kakaoTokenResponse.getAccessToken()); LoginResponse loginResponse = kakaoServiceImpl.handleUserLogin(kakaoUserInfoResponse); @@ -35,6 +40,16 @@ public ApiResponse callback(@RequestParam("code") String code) th return ApiResponse.onSuccess(loginResponse); } + @PostMapping("/login/oauth2/google") + @Operation(summary = "구글 로그인/회원가입 API") + public ApiResponse googleLogin(@RequestParam("code") String code) throws IOException { + TokenResponse googleTokenResponse = googleServiceImpl.getAccessTokenFromGoogle(code); + GoogleUserInfoResponse googleUserInfoResponse = googleServiceImpl.getUserInfo(googleTokenResponse.getAccessToken()); + LoginResponse loginResponse = googleServiceImpl.handleUserLogin(googleUserInfoResponse); + + return ApiResponse.onSuccess(loginResponse); + } + @PostMapping("/signup") @Operation(summary = "일반 회원가입 API") public ApiResponse signUp(@Valid @RequestBody SignUpRequest signUpRequest) { @@ -48,4 +63,16 @@ public ApiResponse login(@Valid @RequestBody SignInRequest signIn LoginResponse loginResponse = authServiceImpl.handleUserLogin(signInRequest); return ApiResponse.onSuccess(loginResponse); } + + @PostMapping("/signup/nickname/validate") + @Operation(summary = "닉네임 중복 확인 API") + public ApiResponse isNicknameValidate(@RequestBody NicknameRequest nicknameRequest) { + boolean isDuplicate = authServiceImpl.checkNicknameDuplicate(nicknameRequest.getNickname()); + + if (isDuplicate) { + return ApiResponse.onSuccess("이미 사용 중인 닉네임입니다."); + } else { + return ApiResponse.onSuccess("사용 가능한 닉네임입니다."); + } + } } diff --git a/src/main/java/com/memesphere/domain/user/converter/UserConverter.java b/src/main/java/com/memesphere/domain/user/converter/UserConverter.java index c7cb605..0c58e2f 100644 --- a/src/main/java/com/memesphere/domain/user/converter/UserConverter.java +++ b/src/main/java/com/memesphere/domain/user/converter/UserConverter.java @@ -1,6 +1,7 @@ package com.memesphere.domain.user.converter; import com.memesphere.domain.user.dto.request.SignUpRequest; +import com.memesphere.domain.user.dto.response.GoogleUserInfoResponse; import com.memesphere.domain.user.entity.SocialType; import com.memesphere.domain.user.entity.User; import com.memesphere.domain.user.dto.response.TokenResponse; @@ -35,6 +36,31 @@ public static User toUpdatedKakaoUser(KakaoUserInfoResponse kakaoUserInfoRespons .build(); } + // 구글 로그인 유저 + public static User toGoogleUser(GoogleUserInfoResponse googleUserInfoResponse) { + return User.builder() + .loginId(UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE) + .nickname(googleUserInfoResponse.getName()) + .email(googleUserInfoResponse.getEmail()) + .profileImage(googleUserInfoResponse.getPicture()) + .socialType(SocialType.GOOGLE) + .userRole(UserRole.USER) + .build(); + } + + public static User toUpdatedGoogleUser(GoogleUserInfoResponse googleUserInfoResponse, TokenResponse tokenResponse) { + return User.builder() + .loginId(UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE) + .nickname(googleUserInfoResponse.getName()) + .email(googleUserInfoResponse.getEmail()) + .profileImage(googleUserInfoResponse.getPicture()) + .accessToken(tokenResponse.getAccessToken()) + .refreshToken(tokenResponse.getRefreshToken()) + .socialType(SocialType.GOOGLE) + .userRole(UserRole.USER) + .build(); + } + // 일반 로그인 유저 public static User toAuthUser(SignUpRequest signUpRequest, PasswordEncoder passwordEncoder) { return User.builder() diff --git a/src/main/java/com/memesphere/domain/user/dto/request/NicknameRequest.java b/src/main/java/com/memesphere/domain/user/dto/request/NicknameRequest.java new file mode 100644 index 0000000..b75f2c4 --- /dev/null +++ b/src/main/java/com/memesphere/domain/user/dto/request/NicknameRequest.java @@ -0,0 +1,19 @@ +package com.memesphere.domain.user.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class NicknameRequest { + + @NotEmpty + @Schema(description = "닉네임", example = "홍길동") + private String nickname; +} diff --git a/src/main/java/com/memesphere/domain/user/dto/response/GoogleUserInfoResponse.java b/src/main/java/com/memesphere/domain/user/dto/response/GoogleUserInfoResponse.java new file mode 100644 index 0000000..f2e9d60 --- /dev/null +++ b/src/main/java/com/memesphere/domain/user/dto/response/GoogleUserInfoResponse.java @@ -0,0 +1,21 @@ +package com.memesphere.domain.user.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class GoogleUserInfoResponse { + + @Schema(description = "계정 유저 이름", example = "홍길동") + private String name; + + @Schema(description = "계정 프로필 이미지", example = "https://lh3.googleusercontent.com/a/ACg8ocL1gRaTq2dfArGVEQC_fcEMdc101SbmOGE2u_-68LosJmIPOg=s96-c") + private String picture; + + @Schema(description = "계정 이메일", example = "abc123@gmail.com") + private String email; +} diff --git a/src/main/java/com/memesphere/domain/user/entity/User.java b/src/main/java/com/memesphere/domain/user/entity/User.java index 1184cb3..f32d4a9 100644 --- a/src/main/java/com/memesphere/domain/user/entity/User.java +++ b/src/main/java/com/memesphere/domain/user/entity/User.java @@ -36,27 +36,25 @@ public class User extends BaseEntity { @Column(unique = true) private String email; - @Setter private String password; - @Setter - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private SocialType socialType; + @Column(length = 8) + private String birth; + + private String profileImage; private String wallet; - @Setter private String accessToken; - @Setter private String refreshToken; @Enumerated(EnumType.STRING) - private UserRole userRole; + @Column(nullable = false) + private SocialType socialType; - @Column(length = 8) - private String birth; + @Enumerated(EnumType.STRING) + private UserRole userRole; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) @Builder.Default @@ -73,4 +71,12 @@ public class User extends BaseEntity { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) @Builder.Default private List chatLikeList = new ArrayList<>(); + + public void saveAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public void saveRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } } diff --git a/src/main/java/com/memesphere/domain/user/service/AuthServiceImpl.java b/src/main/java/com/memesphere/domain/user/service/AuthServiceImpl.java index c7bb054..926c256 100644 --- a/src/main/java/com/memesphere/domain/user/service/AuthServiceImpl.java +++ b/src/main/java/com/memesphere/domain/user/service/AuthServiceImpl.java @@ -49,8 +49,8 @@ public LoginResponse handleUserLogin(SignInRequest signInRequest) { accessToken = tokenProvider.createAccessToken(existingUser.getEmail(), existingUser.getLoginId()); String refreshToken = tokenProvider.createRefreshToken(existingUser.getEmail()); - existingUser.setAccessToken(accessToken); - existingUser.setRefreshToken(refreshToken); + existingUser.saveAccessToken(accessToken); + existingUser.saveRefreshToken(refreshToken); userRepository.save(existingUser); return new LoginResponse(accessToken, refreshToken); } else { diff --git a/src/main/java/com/memesphere/domain/user/service/GoogleService.java b/src/main/java/com/memesphere/domain/user/service/GoogleService.java new file mode 100644 index 0000000..71f5442 --- /dev/null +++ b/src/main/java/com/memesphere/domain/user/service/GoogleService.java @@ -0,0 +1,12 @@ +package com.memesphere.domain.user.service; + +import com.memesphere.domain.user.dto.response.GoogleUserInfoResponse; +import com.memesphere.domain.user.dto.response.LoginResponse; +import com.memesphere.domain.user.dto.response.TokenResponse; + +public interface GoogleService { + TokenResponse getAccessTokenFromGoogle(String code); + GoogleUserInfoResponse getUserInfo(String accessToken); + void handleUserRegistration(GoogleUserInfoResponse userInfo, TokenResponse kakaoTokenResponse); + LoginResponse handleUserLogin(GoogleUserInfoResponse userInfo); +} diff --git a/src/main/java/com/memesphere/domain/user/service/GoogleServiceImpl.java b/src/main/java/com/memesphere/domain/user/service/GoogleServiceImpl.java new file mode 100644 index 0000000..81e25ed --- /dev/null +++ b/src/main/java/com/memesphere/domain/user/service/GoogleServiceImpl.java @@ -0,0 +1,131 @@ +package com.memesphere.domain.user.service; + +import com.memesphere.domain.user.converter.UserConverter; +import com.memesphere.domain.user.dto.response.GoogleUserInfoResponse; +import com.memesphere.domain.user.dto.response.KakaoUserInfoResponse; +import com.memesphere.domain.user.dto.response.LoginResponse; +import com.memesphere.domain.user.dto.response.TokenResponse; +import com.memesphere.domain.user.entity.SocialType; +import com.memesphere.domain.user.entity.User; +import com.memesphere.domain.user.repository.UserRepository; +import com.memesphere.global.apipayload.exception.GeneralException; +import com.memesphere.global.jwt.TokenProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; + +@Slf4j +@Service +@RequiredArgsConstructor +public class GoogleServiceImpl implements GoogleService{ + + private final TokenProvider tokenProvider; + private final UserServiceImpl userServiceImpl; + private final UserRepository userRepository; + + @Value("${security.oauth2.client.registration.google.client-id}") + private String clientId; + + @Value("${security.oauth2.client.registration.google.client-secret}") + private String clientSecret; + + @Value("${security.oauth2.client.registration.google.redirect-uri}") + private String redirectUri; + + @Value("${security.oauth2.client.registration.google.authorization-grant-type}") + private String authorizationCode; + + @Value("${security.oauth2.client.provider.google.token-uri}") + private String tokenUri; + + @Value("${security.oauth2.client.provider.google.user-info-uri}") + private String userInfoUri; + + + public TokenResponse getAccessTokenFromGoogle(String code) { + try { + String decodedCode = URLDecoder.decode(code, StandardCharsets.UTF_8); + + RestTemplate restTemplate = new RestTemplate(); + String uri = UriComponentsBuilder.fromUriString(tokenUri) + .queryParam("grant_type", authorizationCode) + .queryParam("client_id", clientId) + .queryParam("redirect_uri", redirectUri) + .queryParam("code", decodedCode) + .queryParam("client_secret", clientSecret) + .toUriString(); + + ResponseEntity responseEntity = restTemplate.postForEntity(uri, null, TokenResponse.class); + return responseEntity.getBody(); + } catch (Exception e) { + log.error("Error occurred while getting access token from Google: ", e); + throw new RuntimeException("Failed to retrieve access token from Google", e); + } + } + + public GoogleUserInfoResponse getUserInfo(String accessToken) { + try { + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + accessToken); + HttpEntity entity = new HttpEntity<>(headers); + + String uri = UriComponentsBuilder.fromUriString(userInfoUri).toUriString(); + + ResponseEntity responseEntity = restTemplate.exchange(uri, HttpMethod.GET, entity, GoogleUserInfoResponse.class); + return responseEntity.getBody(); + } catch (Exception e) { + log.error("Error occurred while getting user info from Google: ", e); + throw new RuntimeException("Failed to retrieve user info from Google", e); + } + } + + public void handleUserRegistration(GoogleUserInfoResponse googleUserInfoResponse, TokenResponse tokenResponse) { + String email = googleUserInfoResponse.getEmail(); + User existingUser = userRepository.findByEmail(email).orElse(null); + + if (existingUser == null) { + User newUser = UserConverter.toGoogleUser(googleUserInfoResponse); + userServiceImpl.save(newUser); + } else { + User updatedUser = UserConverter.toUpdatedGoogleUser(googleUserInfoResponse, tokenResponse); + userServiceImpl.save(updatedUser); + } + } + + public LoginResponse handleUserLogin(GoogleUserInfoResponse googleUserInfoResponse) { + User existingUser = userRepository.findByEmail(googleUserInfoResponse.getEmail()).orElse(null); + String accessToken; + + if (existingUser != null) { + + accessToken = tokenProvider.createAccessToken(existingUser.getEmail(), existingUser.getLoginId()); + String refreshToken = tokenProvider.createRefreshToken(existingUser.getEmail()); + + existingUser.saveAccessToken(accessToken); + existingUser.saveRefreshToken(refreshToken); + userRepository.save(existingUser); + + return new LoginResponse(accessToken, refreshToken); + } else { + User newUser = UserConverter.toGoogleUser(googleUserInfoResponse); + newUser = userRepository.save(newUser); + + accessToken = tokenProvider.createAccessToken(newUser.getEmail(), newUser.getLoginId()); + String refreshToken = tokenProvider.createRefreshToken(newUser.getEmail()); + + newUser.saveAccessToken(accessToken); + newUser.saveRefreshToken(refreshToken); + userRepository.save(newUser); + + return new LoginResponse(accessToken, refreshToken); + } + } +} diff --git a/src/main/java/com/memesphere/domain/user/service/KakaoServiceImpl.java b/src/main/java/com/memesphere/domain/user/service/KakaoServiceImpl.java index bf1be13..b7c2d60 100644 --- a/src/main/java/com/memesphere/domain/user/service/KakaoServiceImpl.java +++ b/src/main/java/com/memesphere/domain/user/service/KakaoServiceImpl.java @@ -77,7 +77,6 @@ public void handleUserRegistration(KakaoUserInfoResponse kakaoUserInfoResponse, userServiceImpl.save(newUser); } else { User updatedUser = UserConverter.toUpdatedKakaoUser(kakaoUserInfoResponse, tokenResponse); // 기존 유저 업데이트 - existingUser.setSocialType(SocialType.KAKAO); userServiceImpl.save(updatedUser); } } @@ -91,8 +90,8 @@ public LoginResponse handleUserLogin(KakaoUserInfoResponse kakaoUserInfoResponse accessToken = tokenProvider.createAccessToken(existingUser.getEmail(), existingUser.getLoginId()); String refreshToken = tokenProvider.createRefreshToken(existingUser.getEmail()); - existingUser.setAccessToken(accessToken); - existingUser.setRefreshToken(refreshToken); + existingUser.saveAccessToken(accessToken); + existingUser.saveRefreshToken(refreshToken); userRepository.save(existingUser); return new LoginResponse(accessToken, refreshToken); @@ -103,8 +102,8 @@ public LoginResponse handleUserLogin(KakaoUserInfoResponse kakaoUserInfoResponse accessToken = tokenProvider.createAccessToken(newUser.getEmail(), newUser.getLoginId()); String refreshToken = tokenProvider.createRefreshToken(newUser.getEmail()); - newUser.setAccessToken(accessToken); - newUser.setRefreshToken(refreshToken); + newUser.saveAccessToken(accessToken); + newUser.saveRefreshToken(refreshToken); userRepository.save(newUser); return new LoginResponse(accessToken, refreshToken); diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index 5c681e9..42c0bcb 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -5,6 +5,7 @@ login - login + kakaoLogin + googleLogin \ No newline at end of file