diff --git a/src/main/java/com/mycom/backenddaengplace/auth/config/CorsMvcConfig.java b/src/main/java/com/mycom/backenddaengplace/auth/config/CorsMvcConfig.java index 6f1831c..e6dd99a 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/config/CorsMvcConfig.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/config/CorsMvcConfig.java @@ -9,9 +9,11 @@ public class CorsMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry corsRegistry) { - - corsRegistry.addMapping("/**") + corsRegistry.addMapping("/public/**") // Spring Security에서 제외된 경로만 관리 + .allowedOrigins("http://localhost:3000") + .allowedMethods("GET", "POST", "OPTIONS") + .allowedHeaders("*") .exposedHeaders("Set-Cookie") - .allowedOrigins("http://localhost:3000"); + .allowCredentials(true); } -} \ No newline at end of file +} diff --git a/src/main/java/com/mycom/backenddaengplace/auth/config/SecurityConfig.java b/src/main/java/com/mycom/backenddaengplace/auth/config/SecurityConfig.java index 29b900b..13212ae 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/config/SecurityConfig.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/config/SecurityConfig.java @@ -15,8 +15,8 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.web.cors.CorsConfiguration; - -import java.util.Collections; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @Configuration @EnableWebSecurity @@ -40,7 +40,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/oauth2/**", "/auth/**", "/reissue").permitAll() - // "/" 경로 제외 .anyRequest().authenticated()) .oauth2Login(oauth2 -> oauth2 @@ -48,10 +47,25 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { userInfo.userService(customOAuth2UserService)) .successHandler(customSuccessHandler)) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) // JWT 필터 추가 - .addFilterBefore(customLogoutFilter, LogoutFilter.class); - + .addFilterBefore(customLogoutFilter, LogoutFilter.class) + // CORS 설정을 람다 형식으로 추가 + .cors(cors -> { + CorsConfigurationSource source = corsConfigurationSource(); + cors.configurationSource(source); + }); return http.build(); } -} + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.addAllowedOrigin("http://localhost:3000"); // 허용할 클라이언트 주소 + configuration.addAllowedMethod("*"); // 모든 HTTP 메서드 허용 + configuration.addAllowedHeader("*"); // 모든 헤더 허용 + configuration.setAllowCredentials(true); // 쿠키 허용 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); // 모든 경로에 대해 설정 적용 + return source; + } +} diff --git a/src/main/java/com/mycom/backenddaengplace/auth/config/WebConfig.java b/src/main/java/com/mycom/backenddaengplace/auth/config/WebConfig.java index 303bad6..52622a0 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/config/WebConfig.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/config/WebConfig.java @@ -18,9 +18,9 @@ public void addInterceptors(InterceptorRegistry registry) { * // (...) * .anyRequest().authenticated()) // 그 외의 요청은 인증 필요 */ - registry.addInterceptor(authorizationInterceptor) - .addPathPatterns("/**") // 모든 경로에 적용, '/' 제외 필요 없음 - .excludePathPatterns("/error", "/logout", "/api/login"); // 에러 페이지, 로그아웃, 로그인 API는 제외 + registry.addInterceptor(authorizationInterceptor) + .addPathPatterns("/**") // 모든 경로에 적용, '/' 제외 필요 없음 + .excludePathPatterns("/error", "/logout", "/api/login"); // 에러 페이지, 로그아웃, 로그인 API는 제외 } } diff --git a/src/main/java/com/mycom/backenddaengplace/auth/controller/ReissueController.java b/src/main/java/com/mycom/backenddaengplace/auth/controller/ReissueController.java index ead285a..0a54fd4 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/controller/ReissueController.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/controller/ReissueController.java @@ -76,7 +76,7 @@ public ResponseEntity reissue(HttpServletRequest request, HttpServletResponse String role = jwtUtil.getRole(refresh); //make new JWT - String newAccess = jwtUtil.createJwt("access", username, role, 600000L); + String newAccess = jwtUtil.createJwt("access", username, role, 60000000L); String newRefresh = jwtUtil.createJwt("refresh", username, role, 86400000L); //Refresh 토큰 저장 DB에 기존의 Refresh 토큰 삭제 후 새 Refresh 토큰 저장 diff --git a/src/main/java/com/mycom/backenddaengplace/auth/dto/CustomOAuth2User.java b/src/main/java/com/mycom/backenddaengplace/auth/dto/CustomOAuth2User.java index 267f5b9..8c9e11e 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/dto/CustomOAuth2User.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/dto/CustomOAuth2User.java @@ -5,44 +5,78 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.user.OAuth2User; -import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; @Getter public class CustomOAuth2User implements OAuth2User { - private final UserDTO userDTO; - private final Member member; // Member 추가 + private final UserDTO userDTO; // 소셜 로그인 정보 저장 + private final Member member; // Member 객체와 연동 - public CustomOAuth2User(UserDTO userDTO, Member member) { // 생성자 수정 + public CustomOAuth2User(UserDTO userDTO, Member member) { this.userDTO = userDTO; this.member = member; } + /** + * OAuth2 사용자 속성 반환 + * - 현재는 attributes가 null로 반환되어 있으므로, 필요한 경우 이를 수정해야 합니다. + */ @Override public Map getAttributes() { - return null; + // userDTO를 기반으로 속성을 반환하거나 필요한 데이터를 채울 수 있습니다. + Map attributes = new HashMap<>(); + attributes.put("email", userDTO.getEmail()); + attributes.put("nickname", userDTO.getNickname()); + attributes.put("provider", userDTO.getProvider()); + attributes.put("providerId", userDTO.getProviderId()); + attributes.put("profileImage", userDTO.getProfileImage()); + return attributes; } + /** + * 사용자 권한 반환 + * - 현재는 "ROLE_USER" 권한만 부여하도록 설정 + */ @Override public Collection getAuthorities() { - Collection collection = new ArrayList<>(); - collection.add(() -> "ROLE_USER"); // 람다식으로 단순화 - return collection; + return Collections.singleton(() -> "ROLE_USER"); // 단일 권한 ROLE_USER } + /** + * OAuth2 사용자의 이름 반환 + * - 닉네임이 존재하면 닉네임 반환, 없으면 "Unknown User" 반환 + */ @Override public String getName() { return userDTO.getNickname() != null ? userDTO.getNickname() : "Unknown User"; } + /** + * OAuth2 사용자의 고유 username 생성 + * - "provider_providerId" 형식으로 반환 + */ public String getUsername() { return userDTO.getProvider() + "_" + userDTO.getProviderId(); } - // Member 객체를 반환하는 메소드 추가 + /** + * Member 엔티티 반환 + * - 소셜 로그인 이후 Member와 연동된 데이터를 반환 + */ public Member getMember() { return member; } -} \ No newline at end of file + public String getProvider() { + return userDTO.getProvider(); + } + + public String getProviderId() { + return userDTO.getProviderId(); + } + + +} diff --git a/src/main/java/com/mycom/backenddaengplace/auth/handler/CustomSuccessHandler.java b/src/main/java/com/mycom/backenddaengplace/auth/handler/CustomSuccessHandler.java index 97fefaf..173bfdf 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/handler/CustomSuccessHandler.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/handler/CustomSuccessHandler.java @@ -1,9 +1,11 @@ package com.mycom.backenddaengplace.auth.handler; -import com.mycom.backenddaengplace.auth.dto.CustomOAuth2User; import com.mycom.backenddaengplace.auth.domain.RefreshEntity; +import com.mycom.backenddaengplace.auth.dto.CustomOAuth2User; import com.mycom.backenddaengplace.auth.jwt.JWTUtil; import com.mycom.backenddaengplace.auth.repository.RefreshRepository; +import com.mycom.backenddaengplace.member.domain.Member; +import com.mycom.backenddaengplace.member.repository.MemberRepository; import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; @@ -28,60 +30,74 @@ @Slf4j @Component @RequiredArgsConstructor -// public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { public class CustomSuccessHandler implements AuthenticationSuccessHandler { private final JWTUtil jwtUtil; private final RefreshRepository refreshRepository; + private final RequestCache requestCache = new HttpSessionRequestCache(); // RequestCache 추가 + private final MemberRepository memberRepository; // DB 조회를 위한 MemberRepository 추가 @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { - // OAuth2User 정보를 가져옴 + // **OAuth2User 정보를 가져옴** CustomOAuth2User customUserDetails = (CustomOAuth2User) authentication.getPrincipal(); String username = customUserDetails.getUsername(); + String provider = customUserDetails.getProvider(); // 소셜 로그인 제공자 (e.g., google, kakao) + String providerId = customUserDetails.getProviderId(); // 제공자의 고유 ID Collection authorities = authentication.getAuthorities(); String role = authorities.iterator().next().getAuthority(); - // Access와 Refresh 토큰 생성 - String accessToken = jwtUtil.createJwt("access", username, role, 600000L); // 10분 - String refreshToken = jwtUtil.createJwt("refresh", username, role, 2592000000L); // 30일 - - // Refresh 토큰 저장 - addRefreshEntity(username, refreshToken, 2592000000L); - log.info("accessToken: {}", accessToken); - log.info("refreshToken: {}", refreshToken); - - // 응답 설정: Access는 헤더에, Refresh는 쿠키에 저장 - // FIXME: 문자열 하드코딩보다는 상수로 관리하는 것이 좋음 - // response.setHeader("Authorization", accessToken); - response.setHeader(HttpHeaders.AUTHORIZATION, accessToken); - response.addCookie(createCookie("refresh", refreshToken)); - response.setStatus(HttpStatus.OK.value()); - - /* - * TODO: - * 예를 들어 인덱스 페이지 접근 (미인증 상태) -> 로그인 시도 -> 로그인 성공 -> 인덱스 페이지로 이동 - * 쇼핑몰에서 마이페이지 URL 을 알고 있을 때 바로 접근 (미인증 상태) -> 인증 성공 -> 요청했던 마이페이지 URL 로 이동 - */ - RequestCache requestCache = new HttpSessionRequestCache(); - String redirectUrl = Optional.ofNullable(requestCache.getRequest(request, response)) - .map(SavedRequest::getRedirectUrl) - .orElse("/"); - - /* - * FIXME #1: access Token 을 로그인 인증 성공하면 FE 에 전달하는 방법 -> 이후 요청 시 헤더에 access Token 을 넣어서 요청 - * Next.js FE -> /places/1 API 를 호출할 때, Authorization 헤더에 인증 토큰을 세팅해서 요청해주세요. - * FE -> 인증이 필요한 API -> 헤더에 인증 정보를 세팅해야 한다. - * - * FIXME #2: 프론트가 /places/1 API, 쿠키에 저장되어 있는 refresh Token 을 API 전달 - * 헤더에 refreshToken 을 꺼내서 만료 안됐다 -> 만료 기간 갱신도 새로 해주고, 새로운 access Token 발급해줄 수 있음 (정하기 나름) - * API 에서는 refresh 토큰을 DB 로 조회해서 만료 여부 확인 - */ - - response.sendRedirect(redirectUrl); + // **DB에서 회원 여부 확인** + Optional member = memberRepository.findByProviderAndProviderId(provider, providerId); + + if (member.isPresent()) { + // **회원이 존재하는 경우: JWT 생성 및 원래 요청으로 리다이렉트** + log.info("Existing member found: {}", member.get().getEmail()); + + // Access와 Refresh 토큰 생성 + String accessToken = jwtUtil.createJwt("access", username, role, 60000000L); // 1000분 + String refreshToken = jwtUtil.createJwt("refresh", username, role, 2592000000L); // 30일 + + // Refresh 토큰 저장 + addRefreshEntity(username, refreshToken, 2592000000L); + log.info("accessToken: {}", accessToken); + log.info("refreshToken: {}", refreshToken); + + // 응답 설정: Access는 헤더에, Refresh는 쿠키에 저장 + response.setHeader(HttpHeaders.AUTHORIZATION, accessToken); + response.addCookie(createCookie("refresh", refreshToken)); + response.setStatus(HttpStatus.OK.value()); + /* + * TODO: + * 예를 들어 인덱스 페이지 접근 (미인증 상태) -> 로그인 시도 -> 로그인 성공 -> 인덱스 페이지로 이동 + * 쇼핑몰에서 마이페이지 URL 을 알고 있을 때 바로 접근 (미인증 상태) -> 인증 성공 -> 요청했던 마이페이지 URL 로 이동 + */ + // **RequestCache에서 리다이렉션 URL 가져오기** + SavedRequest savedRequest = requestCache.getRequest(request, response); + String redirectUrl = "/home"; // 기본값으로 /home 설정 + if (savedRequest != null) { + redirectUrl = savedRequest.getRedirectUrl(); + } + + log.info("Login successful. Redirecting to: {}", redirectUrl); + response.sendRedirect(redirectUrl); // 원래 요청으로 리다이렉션 + } else { + // **회원이 존재하지 않는 경우: /members로 리다이렉트** + log.info("No member found. Redirecting to /members for registration."); + response.sendRedirect("/members"); + } } + /* + * FIXME #1: access Token 을 로그인 인증 성공하면 FE 에 전달하는 방법 -> 이후 요청 시 헤더에 access Token 을 넣어서 요청 + * Next.js FE -> /places/1 API 를 호출할 때, Authorization 헤더에 인증 토큰을 세팅해서 요청해주세요. + * FE -> 인증이 필요한 API -> 헤더에 인증 정보를 세팅해야 한다. + * + * FIXME #2: 프론트가 /places/1 API, 쿠키에 저장되어 있는 refresh Token 을 API 전달 + * 헤더에 refreshToken 을 꺼내서 만료 안됐다 -> 만료 기간 갱신도 새로 해주고, 새로운 access Token 발급해줄 수 있음 (정하기 나름) + * API 에서는 refresh 토큰을 DB 로 조회해서 만료 여부 확인 + */ private void addRefreshEntity(String username, String refreshToken, Long expiredMs) { // Access Token, Refresh Token @@ -97,9 +113,6 @@ private Cookie createCookie(String key, String value) { cookie.setMaxAge(2592000); // 30일 cookie.setHttpOnly(true); cookie.setPath("/"); - return cookie; } - - } diff --git a/src/main/java/com/mycom/backenddaengplace/auth/jwt/JWTFilter.java b/src/main/java/com/mycom/backenddaengplace/auth/jwt/JWTFilter.java index f699a55..914bb09 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/jwt/JWTFilter.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/jwt/JWTFilter.java @@ -1,7 +1,6 @@ package com.mycom.backenddaengplace.auth.jwt; import com.mycom.backenddaengplace.auth.dto.CustomOAuth2User; -import com.mycom.backenddaengplace.auth.dto.CustomUserDetails; import com.mycom.backenddaengplace.auth.dto.UserDTO; import com.mycom.backenddaengplace.auth.repository.AuthMemberRepository; import com.mycom.backenddaengplace.member.domain.Member; @@ -20,8 +19,109 @@ import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -import java.io.PrintWriter; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JWTFilter extends OncePerRequestFilter { + + private final JWTUtil jwtUtil; + private final AuthMemberRepository authMemberRepository; // 추가 + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // JWT 헤더 확인 및 로깅 + String accessToken = request.getHeader(HttpHeaders.AUTHORIZATION); + log.info("{}{} {}{}Authorization: {}", System.lineSeparator(), request.getMethod(), request.getRequestURI(), + System.lineSeparator(), accessToken); + + // **수정됨**: 헤더에 토큰이 없으면 다음 필터로 바로 진행 + if (accessToken == null || !accessToken.startsWith("Bearer ")) { + log.info("No Authorization header or invalid format."); + filterChain.doFilter(request, response); + return; + } + + // "Bearer " 제거 + accessToken = accessToken.substring(7); + + try { + // **수정됨**: JWT 만료 여부 확인 (예외 처리) + jwtUtil.isExpired(accessToken); + } catch (ExpiredJwtException e) { + log.warn("Access token expired: {}", e.getMessage()); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("access token expired"); // 클라이언트에 메시지 전송 + return; + } + + // 토큰 카테고리 확인 + String category = jwtUtil.getCategory(accessToken); + if (!category.equals("access")) { + log.warn("Invalid access token category: {}", category); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("invalid access token"); + return; + } + + // 사용자 정보 추출 + String username = jwtUtil.getUsername(accessToken); + String role = jwtUtil.getRole(accessToken); + + // **수정됨**: username 파싱 로직 예외 처리 추가 + String[] parts = username.split("_"); + if (parts.length != 2) { + log.error("Invalid username format in token: {}", username); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("invalid username format"); + return; + } + + String provider = parts[0]; + String providerId = parts[1]; + + // **수정됨**: 사용자 정보 조회 실패 시 로그 추가 + Member member = authMemberRepository.findByProviderAndProviderId(provider, providerId); + if (member == null) { + log.warn("No member found for provider: {}, providerId: {}", provider, providerId); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("user not found"); + return; + } + + // UserDTO 생성 + UserDTO userDTO = new UserDTO(); + userDTO.setProvider(provider); + userDTO.setProviderId(providerId); + userDTO.setEmail(member.getEmail()); + userDTO.setNickname(member.getNickname()); + userDTO.setProfileImage(member.getProfileImageUrl()); + + // CustomOAuth2User 생성 + CustomOAuth2User customOAuth2User = new CustomOAuth2User(userDTO, member); + + // **수정됨**: SecurityContext에 인증 정보 설정 + Authentication authToken = new UsernamePasswordAuthenticationToken( + customOAuth2User, + null, + customOAuth2User.getAuthorities() + ); + + SecurityContextHolder.getContext().setAuthentication(authToken); + log.info("Authentication set for user: {}", username); + + // **수정 없음**: 다음 필터로 진행 + filterChain.doFilter(request, response); + } +} + + + + + + +/* @Slf4j @Component @RequiredArgsConstructor @@ -96,4 +196,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse filterChain.doFilter(request, response); } -} \ No newline at end of file +} + + */ \ No newline at end of file diff --git a/src/main/java/com/mycom/backenddaengplace/auth/service/CustomOAuth2UserService.java b/src/main/java/com/mycom/backenddaengplace/auth/service/CustomOAuth2UserService.java index 8a21bef..cee28cb 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/service/CustomOAuth2UserService.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/service/CustomOAuth2UserService.java @@ -28,46 +28,54 @@ public CustomOAuth2UserService(AuthMemberRepository authMemberRepository, AuthMe @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - OAuth2User oAuth2User = super.loadUser(userRequest); - String registrationId = userRequest.getClientRegistration().getRegistrationId(); - OAuth2Response oAuth2Response = getOAuth2Response(registrationId, oAuth2User); + OAuth2User oAuth2User = super.loadUser(userRequest); // 소셜 로그인 유저 데이터 가져오기 + String registrationId = userRequest.getClientRegistration().getRegistrationId(); // 제공자 구분 + OAuth2Response oAuth2Response = getOAuth2Response(registrationId, oAuth2User); // OAuth2Response로 매핑 - // Member 저장 또는 업데이트 + // **회원 정보 저장 또는 업데이트** Member member = saveOrUpdateUser(oAuth2Response); - // UserDTO 생성 + // **UserDTO 생성** UserDTO userDTO = createUserDTO(member); - // CustomOAuth2User 생성 시 Member도 함께 전달 + // **CustomOAuth2User 반환** return new CustomOAuth2User(userDTO, member); } + /** + * 제공자별 OAuth2Response 매핑 로직 + */ private OAuth2Response getOAuth2Response(String registrationId, OAuth2User oAuth2User) { if ("kakao".equals(registrationId)) { return new KakaoResponse(oAuth2User.getAttributes()); } else if ("google".equals(registrationId)) { return new GoogleResponse(oAuth2User.getAttributes()); } else { + log.error("Unsupported provider: {}", registrationId); throw new OAuth2AuthenticationException("Unsupported provider: " + registrationId); } } + /** + * 회원 정보를 저장하거나 업데이트 + */ private Member saveOrUpdateUser(OAuth2Response oAuth2Response) { String provider = oAuth2Response.getProvider(); String providerId = oAuth2Response.getProviderId(); String email = oAuth2Response.getEmail(); - // 기본 이메일 처리 + // **기본 이메일 처리** if (email == null || email.isEmpty()) { email = provider + "-" + providerId + "@noemail.com"; } log.info("Social login request: provider={}, providerId={}, email={}", provider, providerId, email); + // **DB에서 회원 조회** Member member = authMemberRepository.findByProviderAndProviderId(provider, providerId); if (member == null) { - // 새 사용자 저장 + // **새 사용자 저장** member = Member.builder() .provider(provider) .providerId(providerId) @@ -76,21 +84,24 @@ private Member saveOrUpdateUser(OAuth2Response oAuth2Response) { .profileImageUrl(oAuth2Response.getProfileImage()) .build(); authMemberRepository.save(member); - log.info("New user registered: {}", member); // 새 사용자 등록 로그 + log.info("New user registered: {}", member); } else { - // 기존 사용자 업데이트 + // **기존 사용자 업데이트** authMemberService.updateMember(member, email, oAuth2Response.getName(), oAuth2Response.getProfileImage(), provider, providerId); - log.info("Existing user updated: {}", member); // 기존 사용자 업데이트 로그 + log.info("Existing user updated: {}", member); } return member; } + /** + * UserDTO 객체 생성 + */ private UserDTO createUserDTO(Member member) { UserDTO userDTO = new UserDTO(); userDTO.setProvider(member.getProvider()); diff --git a/src/main/java/com/mycom/backenddaengplace/member/repository/MemberRepository.java b/src/main/java/com/mycom/backenddaengplace/member/repository/MemberRepository.java index beeeff4..40a0766 100644 --- a/src/main/java/com/mycom/backenddaengplace/member/repository/MemberRepository.java +++ b/src/main/java/com/mycom/backenddaengplace/member/repository/MemberRepository.java @@ -9,5 +9,6 @@ public interface MemberRepository extends JpaRepository { boolean existsByEmail(String email); + Optional findByProviderAndProviderId(String provider, String providerId); }