Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
implementation("io.jsonwebtoken:jjwt-api:0.12.5")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.5")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.5")

// webflux (for WebClient)
implementation 'org.springframework.boot:spring-boot-starter-webflux'

// Lombok
compileOnly 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
import com.moongeul.backend.api.member.dto.LoginResponseDTO;
import com.moongeul.backend.api.member.dto.LoginRequestDTO;
import com.moongeul.backend.api.member.dto.UserInfoDTO;
import com.moongeul.backend.api.member.entity.Member;
import com.moongeul.backend.api.member.service.MemeberService;
import com.moongeul.backend.common.exception.BadRequestException;
import com.moongeul.backend.api.member.service.MemberService;
import com.moongeul.backend.common.response.ApiResponse;
import com.moongeul.backend.common.response.ErrorStatus;
import com.moongeul.backend.common.response.SuccessStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand All @@ -24,7 +22,7 @@
@RequestMapping("/api/v2/member")
public class MemberController {

private final MemeberService memberService;
private final MemberService memberService;

@Operation(
summary = "로그인 API",
Expand All @@ -36,11 +34,7 @@ public class MemberController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "유효하지 않은 엑세스토큰 입니다.")
})
@PostMapping("/login")
public ResponseEntity<ApiResponse<LoginResponseDTO>> loginWithGoogle(@RequestBody LoginRequestDTO loginRequestDTO) {
// 엑세스토큰이 입력되지 않았을 경우 예외 처리
if (loginRequestDTO == null || loginRequestDTO.getCode() == null || loginRequestDTO.getCode().isEmpty()) {
throw new BadRequestException(ErrorStatus.MISSING_GOOGLE_ACCESSTOKEN.getMessage());
}
public ResponseEntity<ApiResponse<LoginResponseDTO>> loginWithGoogle(@Valid @RequestBody LoginRequestDTO loginRequestDTO) {

LoginResponseDTO response = memberService.loginWithGoogle(loginRequestDTO.getCode());
return ApiResponse.success(SuccessStatus.SEND_LOGIN_SUCCESS, response);
Expand All @@ -56,8 +50,7 @@ public ResponseEntity<ApiResponse<LoginResponseDTO>> loginWithGoogle(@RequestBod
})
@GetMapping("/user-info")
public ResponseEntity<ApiResponse<UserInfoDTO>> getUserInfo(@AuthenticationPrincipal UserDetails userDetails){
Member member = memberService.getMemberByEmail(userDetails.getUsername());
UserInfoDTO response = memberService.getUserInfo(member);
UserInfoDTO response = memberService.getUserInfo(userDetails.getUsername());
return ApiResponse.success(SuccessStatus.GET_USERINFO_SUCCESS, response);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.moongeul.backend.api.member.dto;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class GoogleInfoResponseDTO {

private String id;
private String email;
private String name;
private String picture;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.moongeul.backend.api.member.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class GoogleTokenResponseDTO {

// JSON의 access_token 필드를 이 변수에 매핑
@JsonProperty("access_token")
private String accessToken;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.moongeul.backend.api.member.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.Getter;

@Getter
public class LoginRequestDTO {

@NotBlank(message = "구글 엑세스토큰이 입력되지 않았습니다.")
private String code; // 인가코드
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
chain.doFilter(request, response); // 필터 체인(Filter Chain) 내의 다음 단계로 요청을 넘기는 것
}

// Request Header에서 토큰 정보 추출
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.moongeul.backend.api.member.service;

import com.moongeul.backend.api.member.dto.GoogleInfoResponseDTO;
import com.moongeul.backend.api.member.dto.GoogleTokenResponseDTO;
import com.moongeul.backend.api.member.dto.UserInfoDTO;
import com.moongeul.backend.api.member.entity.Member;
import com.moongeul.backend.api.member.entity.Role;
import com.moongeul.backend.api.member.jwt.dto.JwtTokenDTO;
import com.moongeul.backend.api.member.dto.LoginResponseDTO;
import com.moongeul.backend.api.member.repository.MemberRepository;
import com.moongeul.backend.common.config.jwt.JwtTokenProvider;
import com.moongeul.backend.common.exception.NotFoundException;
import com.moongeul.backend.common.response.ErrorStatus;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class MemberService {

private final MemberRepository memberRepository;
private final JwtTokenProvider jwtTokenProvider;
private final OAuthService oAuthService;

// 인가코드 받아 JWT로 교환 및 회원가입/로그인 처리
@Transactional
public LoginResponseDTO loginWithGoogle(String code){

// 1. 인가 코드로 Google Access Token 및 사용자 정보 획득
GoogleTokenResponseDTO tokenResponse = oAuthService.getGoogleToken(code);
GoogleInfoResponseDTO userInfo = oAuthService.getGoogleUserInfo(tokenResponse.getAccessToken());

// 2. 사용자 정보 추출
String socialId = userInfo.getId();
String email = userInfo.getEmail();
String name = userInfo.getName();
String picture = userInfo.getPicture();

// 3. DB 처리 (회원가입 또는 로그인)
Member member = memberRepository.findBySocialId(socialId)
.map(entity -> entity.update(name, picture)) // 이미 있으면 정보 업데이트
.orElseGet(() -> signUp(socialId, email, name, picture)); // 없으면 신규 회원가입

// 4. 자체 JWT 토큰 생성 및 반환
JwtTokenDTO jwtToken = jwtTokenProvider.generateToken(member);
member.updateRefreshToken(jwtToken.getRefreshToken()); // 생성된 refreshToken DB 저장

return LoginResponseDTO.builder()
.role(member.getAuthorityKey())
.accessToken(jwtToken.getAccessToken())
.refreshToken(jwtToken.getRefreshToken())
.build();
}

// 신규 회원가입 처리 로직 (DB 저장)
private Member signUp(String socialId, String email, String name, String picture) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

회원 생성은 여러 DB 쓰기를 하나의 작업으로 묶어 예외 발생 시 전체 롤백 및 데이터 정합성을 보장해야 하므로, 회원가입 메서드에 @transactional을 추가해주세요!

Copy link
Contributor Author

@gogori6565 gogori6565 Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signUp 메서드는 부모 메서드(loginWithGoogle)가 제공하는 트랜잭션 범위 내에서 실행되고 있기 때문에 추가적인 @transactional을 넣지 않았습니다!
피드백 감사합니다 :)

Member newUser = Member.builder()
.email(email)
.name(name)
.profileImage(picture)
.password("OAuth Password") // 임시 패스워드
.socialId(socialId) // 예시 사용자명 생성
.socialType("google")
.role(Role.GUEST) // 이후 필요 정보 모두 입력 시 USER 로 승격
.build();
return memberRepository.save(newUser);
}

// 사용자 정보 조회
@Transactional(readOnly = true)
public UserInfoDTO getUserInfo(String email){

Member member = getMemberByEmail(email);

return UserInfoDTO.builder()
.id(member.getId())
.name(member.getName())
.profileImage(member.getProfileImage())
.nickname(member.getNickname())
.build();
}

private Member getMemberByEmail(String email) {
return memberRepository.findByEmail(email)
.orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage()));
}
}

This file was deleted.

Loading