diff --git a/src/main/java/com/somemore/SomemoreApplication.java b/src/main/java/com/somemore/SomemoreApplication.java index 87d5b8963..dec2413b5 100644 --- a/src/main/java/com/somemore/SomemoreApplication.java +++ b/src/main/java/com/somemore/SomemoreApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class SomemoreApplication { public static void main(String[] args) { diff --git a/src/main/java/com/somemore/auth/UserRole.java b/src/main/java/com/somemore/auth/UserRole.java new file mode 100644 index 000000000..0e4d9ed8e --- /dev/null +++ b/src/main/java/com/somemore/auth/UserRole.java @@ -0,0 +1,7 @@ +package com.somemore.auth; + +public enum UserRole { + VOLUNTEER, + CENTER, + ADMIN +} diff --git a/src/main/java/com/somemore/auth/cookie/SetCookieService.java b/src/main/java/com/somemore/auth/cookie/SetCookieService.java new file mode 100644 index 000000000..f728dac31 --- /dev/null +++ b/src/main/java/com/somemore/auth/cookie/SetCookieService.java @@ -0,0 +1,30 @@ +package com.somemore.auth.cookie; + +import com.somemore.auth.jwt.domain.TokenType; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class SetCookieService implements SetCookieUseCase { + + @Override + public void setToken(HttpServletResponse response, String value, TokenType tokenType) { + ResponseCookie cookie = generateCookie(tokenType.name(), value, tokenType.getPeriodInSeconds()); + response.addHeader("Set-Cookie", cookie.toString()); + } + + private static ResponseCookie generateCookie(String name, String value, int time) { + return ResponseCookie.from(name, value) + .httpOnly(true) + .secure(true) + .path("/") + .maxAge(time) + .sameSite("Lax") + .build(); + } +} diff --git a/src/main/java/com/somemore/auth/cookie/SetCookieUseCase.java b/src/main/java/com/somemore/auth/cookie/SetCookieUseCase.java new file mode 100644 index 000000000..148e2e013 --- /dev/null +++ b/src/main/java/com/somemore/auth/cookie/SetCookieUseCase.java @@ -0,0 +1,8 @@ +package com.somemore.auth.cookie; + +import com.somemore.auth.jwt.domain.TokenType; +import jakarta.servlet.http.HttpServletResponse; + +public interface SetCookieUseCase { + void setToken(HttpServletResponse response, String value, TokenType tokenType); +} diff --git a/src/main/java/com/somemore/auth/oauth/OAuthProvider.java b/src/main/java/com/somemore/auth/oauth/OAuthProvider.java new file mode 100644 index 000000000..bf0a73914 --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/OAuthProvider.java @@ -0,0 +1,24 @@ +package com.somemore.auth.oauth; + +import lombok.Getter; + +@Getter +public enum OAuthProvider { + NAVER("naver"); + + private final String providerName; + + OAuthProvider(String providerName) { + this.providerName = providerName; + } + + public static OAuthProvider from(String providerName) { + for (OAuthProvider provider : values()) { + if (provider.providerName.equals(providerName)) { + return provider; + } + } + + throw new IllegalArgumentException("올바르지 않은 OAuth 제공자: " + providerName); + } +} diff --git a/src/main/java/com/somemore/auth/oauth/handler/failure/CustomOAuthFailureHandler.java b/src/main/java/com/somemore/auth/oauth/handler/failure/CustomOAuthFailureHandler.java new file mode 100644 index 000000000..d7ba81196 --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/handler/failure/CustomOAuthFailureHandler.java @@ -0,0 +1,21 @@ +package com.somemore.auth.oauth.handler.failure; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class CustomOAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) { + // TODO 프론트엔드와 협의 + log.error("안녕 난 말하는 감자야"); + } +} diff --git a/src/main/java/com/somemore/auth/oauth/handler/success/CustomOAuthSuccessHandler.java b/src/main/java/com/somemore/auth/oauth/handler/success/CustomOAuthSuccessHandler.java new file mode 100644 index 000000000..5f389367b --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/handler/success/CustomOAuthSuccessHandler.java @@ -0,0 +1,62 @@ +package com.somemore.auth.oauth.handler.success; + +import com.somemore.auth.cookie.SetCookieUseCase; +import com.somemore.auth.jwt.domain.EncodedToken; +import com.somemore.auth.jwt.domain.TokenType; +import com.somemore.auth.jwt.usecase.command.GenerateTokensOnLoginUseCase; +import com.somemore.auth.oauth.OAuthProvider; +import com.somemore.auth.oauth.naver.service.query.ProcessNaverOAuthUserService; +import com.somemore.auth.redirect.RedirectUseCase; +import com.somemore.volunteer.usecase.query.FindVolunteerIdUseCase; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +@Slf4j +public class CustomOAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final ProcessNaverOAuthUserService processNaverOAuthService; + private final FindVolunteerIdUseCase findVolunteerIdUseCase; + private final GenerateTokensOnLoginUseCase generateTokensOnLoginUseCase; + private final SetCookieUseCase setCookieUseCase; + private final RedirectUseCase redirectUseCase; + + @Value("${frontend.url}") + private String frontendRootUrl; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + String oAuthId; + switch (getOAuthProvider(authentication)) { + case NAVER -> oAuthId = processNaverOAuthService.processOAuthUser(authentication); + default -> { + log.error("지원하지 않는 OAuth 제공자입니다."); + throw new IllegalArgumentException(); + } + } + + UUID volunteerId = findVolunteerIdUseCase.findVolunteerIdByOAuthId(oAuthId); + EncodedToken accessToken = generateTokensOnLoginUseCase.saveRefreshTokenAndReturnAccessToken(volunteerId); + + setCookieUseCase.setToken(response, accessToken.value(), TokenType.ACCESS); + redirectUseCase.redirect(request, response, frontendRootUrl); + } + + private static OAuthProvider getOAuthProvider(Authentication authentication) { + if (authentication instanceof OAuth2AuthenticationToken token) { + return OAuthProvider.from(token.getAuthorizedClientRegistrationId()); + } + throw new IllegalArgumentException(); + } +} diff --git a/src/main/java/com/somemore/auth/oauth/naver/domain/NaverUser.java b/src/main/java/com/somemore/auth/oauth/naver/domain/NaverUser.java new file mode 100644 index 000000000..c99b56519 --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/naver/domain/NaverUser.java @@ -0,0 +1,23 @@ +package com.somemore.auth.oauth.naver.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Table(name = "naver_user") +public class NaverUser { + @Id + private String oauthId; + + private NaverUser(String oauthId) { + this.oauthId = oauthId; + } + + public static NaverUser from(String oauthId) { + return new NaverUser(oauthId); + } +} diff --git a/src/main/java/com/somemore/auth/oauth/naver/dto/response/NaverUserProfileResponseDto.java b/src/main/java/com/somemore/auth/oauth/naver/dto/response/NaverUserProfileResponseDto.java new file mode 100644 index 000000000..0d03ca360 --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/naver/dto/response/NaverUserProfileResponseDto.java @@ -0,0 +1,38 @@ +package com.somemore.auth.oauth.naver.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.somemore.auth.oauth.OAuthProvider; +import com.somemore.volunteer.dto.request.VolunteerRegisterRequestDto; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record NaverUserProfileResponseDto( + String resultcode, // 결과 코드 + String message, // 결과 메시지 + Response response // 응답 데이터 +) { + @JsonIgnoreProperties(ignoreUnknown = true) + public record Response( + String id, // 일련 번호 + String name, // 이름 + String email, // 이메일 + String gender, // 성별 (F, M, U) + String birthday, // 생일 (MM-DD) + String birthyear, // 출생 연도 + String mobile // 휴대 전화 번호 + ) {} + + public VolunteerRegisterRequestDto toVolunteerRegisterRequestDto() { + return new VolunteerRegisterRequestDto( + OAuthProvider.NAVER, + this.response.id(), + this.response.name(), + this.response.email(), + this.response.gender(), + this.response.birthday(), + this.response.birthyear(), + this.response.mobile() + ); + } +} diff --git a/src/main/java/com/somemore/auth/oauth/naver/repository/NaverUserRepository.java b/src/main/java/com/somemore/auth/oauth/naver/repository/NaverUserRepository.java new file mode 100644 index 000000000..5d0128d7f --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/naver/repository/NaverUserRepository.java @@ -0,0 +1,9 @@ +package com.somemore.auth.oauth.naver.repository; + +import com.somemore.auth.oauth.naver.domain.NaverUser; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface NaverUserRepository extends JpaRepository { +} diff --git a/src/main/java/com/somemore/auth/oauth/naver/service/command/NaverOAuth2UserInfoService.java b/src/main/java/com/somemore/auth/oauth/naver/service/command/NaverOAuth2UserInfoService.java new file mode 100644 index 000000000..496dcf1f3 --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/naver/service/command/NaverOAuth2UserInfoService.java @@ -0,0 +1,43 @@ +package com.somemore.auth.oauth.naver.service.command; + +import com.somemore.auth.oauth.naver.dto.response.NaverUserProfileResponseDto; +import com.somemore.auth.oauth.naver.usecase.query.CheckNaverUserUseCase; +import com.somemore.auth.oauth.naver.usecase.command.RegisterNaverUserUseCase; +import com.somemore.auth.oauth.naver.util.OAuthResponseConverter; +import com.somemore.volunteer.usecase.command.RegisterVolunteerUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Service +@Transactional +public class NaverOAuth2UserInfoService { + + private final CheckNaverUserUseCase checkNaverUserUseCase; + private final RegisterNaverUserUseCase registerNaverUserUseCase; + private final RegisterVolunteerUseCase registerVolunteerUseCase; + + public OAuth2User processOAuth2User(OAuth2User oAuth2User) { + NaverUserProfileResponseDto dto = OAuthResponseConverter.convertToNaverUserProfileResponseDto(oAuth2User); + String naverOauthId = dto.response().id(); + + if (isNewUser(naverOauthId)) { + registerUser(dto); + } + + return oAuth2User; + } + + private boolean isNewUser(String id) { + return !checkNaverUserUseCase.isNaverUserExists(id); + } + + private void registerUser(NaverUserProfileResponseDto dto) { + registerNaverUserUseCase.registerNaverUser(dto.response().id()); + registerVolunteerUseCase.registerVolunteer(dto.toVolunteerRegisterRequestDto()); + } +} diff --git a/src/main/java/com/somemore/auth/oauth/naver/service/command/RegisterNaverUserService.java b/src/main/java/com/somemore/auth/oauth/naver/service/command/RegisterNaverUserService.java new file mode 100644 index 000000000..29ac3b291 --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/naver/service/command/RegisterNaverUserService.java @@ -0,0 +1,25 @@ +package com.somemore.auth.oauth.naver.service.command; + +import com.somemore.auth.oauth.naver.domain.NaverUser; +import com.somemore.auth.oauth.naver.repository.NaverUserRepository; +import com.somemore.auth.oauth.naver.usecase.command.RegisterNaverUserUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class RegisterNaverUserService implements RegisterNaverUserUseCase { + + private final NaverUserRepository naverUserRepository; + + @Override + public void registerNaverUser(String oAuthId) { + NaverUser naverUser = NaverUser.from(oAuthId); + + naverUserRepository.save(naverUser); + } +} diff --git a/src/main/java/com/somemore/auth/oauth/naver/service/query/CheckNaverUserService.java b/src/main/java/com/somemore/auth/oauth/naver/service/query/CheckNaverUserService.java new file mode 100644 index 000000000..58e1e99a5 --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/naver/service/query/CheckNaverUserService.java @@ -0,0 +1,22 @@ +package com.somemore.auth.oauth.naver.service.query; + +import com.somemore.auth.oauth.naver.repository.NaverUserRepository; +import com.somemore.auth.oauth.naver.usecase.query.CheckNaverUserUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CheckNaverUserService implements CheckNaverUserUseCase { + + private final NaverUserRepository naverUserRepository; + + @Override + public boolean isNaverUserExists(String id) { + return naverUserRepository.existsById(id); + } +} diff --git a/src/main/java/com/somemore/auth/oauth/naver/service/query/ProcessNaverOAuthUserService.java b/src/main/java/com/somemore/auth/oauth/naver/service/query/ProcessNaverOAuthUserService.java new file mode 100644 index 000000000..f6d27d6e3 --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/naver/service/query/ProcessNaverOAuthUserService.java @@ -0,0 +1,43 @@ +package com.somemore.auth.oauth.naver.service.query; + +import com.somemore.auth.oauth.OAuthProvider; +import com.somemore.auth.oauth.naver.dto.response.NaverUserProfileResponseDto; +import com.somemore.auth.oauth.naver.usecase.query.CheckNaverUserUseCase; +import com.somemore.auth.oauth.usecase.ProcessOAuthUserUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.somemore.auth.oauth.naver.util.OAuthResponseConverter.convertToNaverUserProfileResponseDto; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ProcessNaverOAuthUserService implements ProcessOAuthUserUseCase { + + private final CheckNaverUserUseCase checkNaverUserUseCase; + + @Override + public String processOAuthUser(Authentication authentication) { + OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); + return processUserInformation(oAuth2User); + } + + private String processUserInformation(OAuth2User oAuth2User) { + NaverUserProfileResponseDto dto = convertToNaverUserProfileResponseDto(oAuth2User); + String oAuthId = dto.response().id(); + + if (checkNaverUserUseCase.isNaverUserExists(oAuthId)) { + return oAuthId; + } + + log.error("유저가 회원 가입을 진행했으나, 존재하지 않는 상태입니다. OAuth Provider: {}, OAuth ID: {}", + OAuthProvider.NAVER, + oAuthId); + throw new IllegalStateException(); + } +} diff --git a/src/main/java/com/somemore/auth/oauth/naver/usecase/command/RegisterNaverUserUseCase.java b/src/main/java/com/somemore/auth/oauth/naver/usecase/command/RegisterNaverUserUseCase.java new file mode 100644 index 000000000..b87a0e797 --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/naver/usecase/command/RegisterNaverUserUseCase.java @@ -0,0 +1,5 @@ +package com.somemore.auth.oauth.naver.usecase.command; + +public interface RegisterNaverUserUseCase { + void registerNaverUser(String oAuthId); +} diff --git a/src/main/java/com/somemore/auth/oauth/naver/usecase/query/CheckNaverUserUseCase.java b/src/main/java/com/somemore/auth/oauth/naver/usecase/query/CheckNaverUserUseCase.java new file mode 100644 index 000000000..d9cf1e6c2 --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/naver/usecase/query/CheckNaverUserUseCase.java @@ -0,0 +1,5 @@ +package com.somemore.auth.oauth.naver.usecase.query; + +public interface CheckNaverUserUseCase { + boolean isNaverUserExists(String id); +} diff --git a/src/main/java/com/somemore/auth/oauth/naver/util/OAuthResponseConverter.java b/src/main/java/com/somemore/auth/oauth/naver/util/OAuthResponseConverter.java new file mode 100644 index 000000000..13fe95c44 --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/naver/util/OAuthResponseConverter.java @@ -0,0 +1,20 @@ +package com.somemore.auth.oauth.naver.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.somemore.auth.oauth.naver.dto.response.NaverUserProfileResponseDto; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Map; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class OAuthResponseConverter { + + public static NaverUserProfileResponseDto convertToNaverUserProfileResponseDto(OAuth2User oAuth2User) { + ObjectMapper objectMapper = new ObjectMapper(); + + Map attributes = oAuth2User.getAttributes(); + return objectMapper.convertValue(attributes, NaverUserProfileResponseDto.class); + } +} diff --git a/src/main/java/com/somemore/auth/oauth/service/CustomOAuth2UserService.java b/src/main/java/com/somemore/auth/oauth/service/CustomOAuth2UserService.java new file mode 100644 index 000000000..dc8035035 --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/service/CustomOAuth2UserService.java @@ -0,0 +1,39 @@ +package com.somemore.auth.oauth.service; + +import com.somemore.auth.oauth.OAuthProvider; +import com.somemore.auth.oauth.naver.service.command.NaverOAuth2UserInfoService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class CustomOAuth2UserService implements OAuth2UserService { + + private final NaverOAuth2UserInfoService naverOAuth2UserInfoService; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + try { + OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService(); + OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); + + return switch (OAuthProvider.from(getRegistrationId(userRequest))) { + case NAVER -> naverOAuth2UserInfoService.processOAuth2User(oAuth2User); + }; + } catch (OAuth2AuthenticationException e) { + log.error("OAuth 사용자 정보를 로드하는 중 문제가 발생했습니다: {}", e.getMessage(), e); + throw e; + } + } + + private static String getRegistrationId(OAuth2UserRequest userRequest) { + return userRequest.getClientRegistration().getRegistrationId(); + } +} diff --git a/src/main/java/com/somemore/auth/oauth/usecase/ProcessOAuthUserUseCase.java b/src/main/java/com/somemore/auth/oauth/usecase/ProcessOAuthUserUseCase.java new file mode 100644 index 000000000..5bbf14924 --- /dev/null +++ b/src/main/java/com/somemore/auth/oauth/usecase/ProcessOAuthUserUseCase.java @@ -0,0 +1,10 @@ +package com.somemore.auth.oauth.usecase; + +import jakarta.servlet.ServletException; +import org.springframework.security.core.Authentication; + +import java.io.IOException; + +public interface ProcessOAuthUserUseCase { + String processOAuthUser(Authentication authentication) throws IOException, ServletException; +} diff --git a/src/main/java/com/somemore/auth/redirect/RedirectConfig.java b/src/main/java/com/somemore/auth/redirect/RedirectConfig.java new file mode 100644 index 000000000..6a1aeb791 --- /dev/null +++ b/src/main/java/com/somemore/auth/redirect/RedirectConfig.java @@ -0,0 +1,15 @@ +package com.somemore.auth.redirect; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.web.DefaultRedirectStrategy; +import org.springframework.security.web.RedirectStrategy; + +@Configuration +public class RedirectConfig { + + @Bean + public RedirectStrategy redirectStrategy() { + return new DefaultRedirectStrategy(); + } +} diff --git a/src/main/java/com/somemore/auth/redirect/RedirectService.java b/src/main/java/com/somemore/auth/redirect/RedirectService.java new file mode 100644 index 000000000..aa2c25a4c --- /dev/null +++ b/src/main/java/com/somemore/auth/redirect/RedirectService.java @@ -0,0 +1,23 @@ +package com.somemore.auth.redirect; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.stereotype.Service; + +import java.io.IOException; + +@Service +@RequiredArgsConstructor +@Slf4j +public class RedirectService implements RedirectUseCase { + + private final RedirectStrategy redirectStrategy; + + @Override + public void redirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException { + redirectStrategy.sendRedirect(request, response, url); + } +} diff --git a/src/main/java/com/somemore/auth/redirect/RedirectUseCase.java b/src/main/java/com/somemore/auth/redirect/RedirectUseCase.java new file mode 100644 index 000000000..d215c4ce8 --- /dev/null +++ b/src/main/java/com/somemore/auth/redirect/RedirectUseCase.java @@ -0,0 +1,9 @@ +package com.somemore.auth.redirect; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +public interface RedirectUseCase { + void redirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException; +} diff --git a/src/main/java/com/somemore/domains/Volunteer.java b/src/main/java/com/somemore/domains/Volunteer.java deleted file mode 100644 index 9377ebcc1..000000000 --- a/src/main/java/com/somemore/domains/Volunteer.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.somemore.domains; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Lob; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -@Entity -public class Volunteer { - @Id - @Column(name = "id", nullable = false, length = 16) - private String id; - - @Column(name = "oauth_provider", nullable = false) - private String oauthProvider; - - @Column(name = "oauth_id", nullable = false) - private String oauthId; - - @Column(name = "name", nullable = false) - private String name; - - @Column(name = "nickname", nullable = false) - private String nickname; - - @Column(name = "img_url", nullable = false) - private String imgUrl; - - @Lob - @Column(name = "introduce", nullable = false) - private String introduce; - - @Column(name = "tier", nullable = false, length = 20) - private String tier; - - @Column(name = "total_volunteer_hours", nullable = false) - private Integer totalVolunteerHours; - - @Column(name = "total_volunteer_count", nullable = false) - private Integer totalVolunteerCount; - -} \ No newline at end of file diff --git a/src/main/java/com/somemore/global/configure/RedisConfig.java b/src/main/java/com/somemore/global/configure/RedisConfig.java new file mode 100644 index 000000000..a4fa30013 --- /dev/null +++ b/src/main/java/com/somemore/global/configure/RedisConfig.java @@ -0,0 +1,44 @@ +package com.somemore.global.configure; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@EnableRedisRepositories +@RequiredArgsConstructor +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Value("${spring.data.redis.password}") + private String password; + + @Bean + public LettuceConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port); + config.setPassword(password); + + return new LettuceConnectionFactory(config); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + + return redisTemplate; + } +} diff --git a/src/main/java/com/somemore/global/configure/SecurityConfig.java b/src/main/java/com/somemore/global/configure/SecurityConfig.java index c897da912..bc7edb98c 100644 --- a/src/main/java/com/somemore/global/configure/SecurityConfig.java +++ b/src/main/java/com/somemore/global/configure/SecurityConfig.java @@ -1,4 +1,69 @@ package com.somemore.global.configure; +import com.somemore.auth.oauth.handler.failure.CustomOAuthFailureHandler; +import com.somemore.auth.oauth.handler.success.CustomOAuthSuccessHandler; +import com.somemore.auth.oauth.service.CustomOAuth2UserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; + +@RequiredArgsConstructor +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@Slf4j public class SecurityConfig { + + private final CustomOAuth2UserService customOAuth2UserService; + private final CustomOAuthSuccessHandler customOAuthSuccessHandler; + private final CustomOAuthFailureHandler customOAuthFailureHandler; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + return httpSecurity + .csrf(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable) + .sessionManagement(sessionManagement -> + sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + + .authorizeHttpRequests(request -> + request + .requestMatchers( + "/**" +// "/login", +// "/oauth2/**", +// "/api/auth/**", +// "/v3/api-docs/**", +// "/swagger/**", +// "/swagger-ui.html", +// "/swagger-ui/**" + ) + .permitAll() + .anyRequest().authenticated() + ) + + .oauth2Login(oauth2 -> + oauth2 + .userInfoEndpoint(userInfoEndpointConfig -> + userInfoEndpointConfig.userService(customOAuth2UserService)) + .failureHandler(customOAuthFailureHandler) + .successHandler(customOAuthSuccessHandler) + ).build(); + + +// TODO JWT 인증 필터가 인증 요청 처리, JWT 인증 필터를 UsernamePasswordAuthenticationFilter 앞에 추가 +// return httpSecurity +// .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) +// .addFilterBefore(jwtExceptionFilter, JwtAuthFilter.class) +// .build(); + } } diff --git a/src/main/java/com/somemore/volunteer/domain/Gender.java b/src/main/java/com/somemore/volunteer/domain/Gender.java new file mode 100644 index 000000000..aaadb3a39 --- /dev/null +++ b/src/main/java/com/somemore/volunteer/domain/Gender.java @@ -0,0 +1,25 @@ +package com.somemore.volunteer.domain; + +import lombok.Getter; + +@Getter +public enum Gender { + MALE("M"), + FEMALE("F"), + UNDEFINED("U"); + + private final String code; + + Gender(String code) { + this.code = code; + } + + public static Gender from(String code) { + for (Gender gender : Gender.values()) { + if (gender.code.equalsIgnoreCase(code)) { + return gender; + } + } + return UNDEFINED; + } +} diff --git a/src/main/java/com/somemore/volunteer/domain/Tier.java b/src/main/java/com/somemore/volunteer/domain/Tier.java new file mode 100644 index 000000000..9a41b5111 --- /dev/null +++ b/src/main/java/com/somemore/volunteer/domain/Tier.java @@ -0,0 +1,15 @@ +package com.somemore.volunteer.domain; + +import lombok.Getter; + +@Getter +public enum Tier { + RED, + ORANGE, + YELLOW, + GREEN, + BLUE, + INDIGO, + VIOLET, + RAINBOW +} diff --git a/src/main/java/com/somemore/volunteer/domain/Volunteer.java b/src/main/java/com/somemore/volunteer/domain/Volunteer.java new file mode 100644 index 000000000..a8125c9c7 --- /dev/null +++ b/src/main/java/com/somemore/volunteer/domain/Volunteer.java @@ -0,0 +1,81 @@ +package com.somemore.volunteer.domain; + +import com.somemore.auth.oauth.OAuthProvider; +import com.somemore.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.util.UUID; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Table(name = "volunteer") +public class Volunteer extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column(name = "id", nullable = false, columnDefinition = "BINARY(16)") + private UUID id; + + @Enumerated(EnumType.STRING) + @Column(name = "oauth_provider", nullable = false) + private OAuthProvider oauthProvider; + + @Column(name = "oauth_id", nullable = false) + private String oauthId; + + @Column(name = "nickname", nullable = false) + private String nickname; + + @Column(name = "img_url", nullable = false) + private String imgUrl; + + @Lob + @Column(name = "introduce", nullable = false) + private String introduce; + + @Enumerated(EnumType.STRING) + @Column(name = "tier", nullable = false, length = 20) + private Tier tier; + + @Column(name = "total_volunteer_hours", nullable = false) + private Integer totalVolunteerHours; + + @Column(name = "total_volunteer_count", nullable = false) + private Integer totalVolunteerCount; + + + public static Volunteer createDefault(OAuthProvider oauthProvider, String oauthId) { + return Volunteer.builder() + .oauthProvider(oauthProvider) + .oauthId(oauthId) + .nickname(UUID.randomUUID().toString().substring(0, 8)) + .imgUrl("") + .introduce("") + .tier(Tier.RED) + .totalVolunteerHours(0) + .totalVolunteerCount(0) + .build(); + } + + @Builder + private Volunteer( + OAuthProvider oauthProvider, + String oauthId, + String nickname, + String imgUrl, + String introduce, + Tier tier, + Integer totalVolunteerHours, + Integer totalVolunteerCount + ) { + this.oauthProvider = oauthProvider; + this.oauthId = oauthId; + this.nickname = nickname; + this.imgUrl = imgUrl; + this.introduce = introduce; + this.tier = tier; + this.totalVolunteerHours = totalVolunteerHours; + this.totalVolunteerCount = totalVolunteerCount; + } +} diff --git a/src/main/java/com/somemore/volunteer/domain/VolunteerDetail.java b/src/main/java/com/somemore/volunteer/domain/VolunteerDetail.java new file mode 100644 index 000000000..9f9321a29 --- /dev/null +++ b/src/main/java/com/somemore/volunteer/domain/VolunteerDetail.java @@ -0,0 +1,70 @@ +package com.somemore.volunteer.domain; + +import com.somemore.volunteer.dto.request.VolunteerRegisterRequestDto; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Table(name = "volunteer_detail") +public class VolunteerDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "volunteer_id", nullable = false, columnDefinition = "BINARY(16)") + private UUID volunteerId; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "email", nullable = false) + private String email; + + @Enumerated(EnumType.STRING) + @Column(name = "gender", nullable = false, length = 10) + private Gender gender; + + @Column(name = "birth_date", nullable = false, length = 10) + private String birthDate; + + @Column(name = "contact_number", nullable = false) + private String contactNumber; + + + public static VolunteerDetail of(VolunteerRegisterRequestDto dto, UUID volunteerId) { + return VolunteerDetail.builder() + .volunteerId(volunteerId) + .name(dto.name()) + .email(dto.email()) + .gender(Gender.from(dto.gender())) + .birthDate(String.format("%s-%s", dto.birthyear(), dto.birthday())) + .contactNumber(dto.mobile()) + .build(); + } + + @Builder + private VolunteerDetail( + UUID volunteerId, + String name, + String email, + Gender gender, + String birthDate, + String contactNumber + ) { + this.volunteerId = volunteerId; + this.name = name; + this.email = email; + this.gender = gender; + this.birthDate = birthDate; + this.contactNumber = contactNumber; + } +} diff --git a/src/main/java/com/somemore/volunteer/dto/request/VolunteerRegisterRequestDto.java b/src/main/java/com/somemore/volunteer/dto/request/VolunteerRegisterRequestDto.java new file mode 100644 index 000000000..67dcf7a96 --- /dev/null +++ b/src/main/java/com/somemore/volunteer/dto/request/VolunteerRegisterRequestDto.java @@ -0,0 +1,15 @@ +package com.somemore.volunteer.dto.request; + +import com.somemore.auth.oauth.OAuthProvider; + +public record VolunteerRegisterRequestDto( + OAuthProvider oAuthProvider, + String oauthId, + String name, + String email, + String gender, + String birthday, + String birthyear, + String mobile +) { +} diff --git a/src/main/java/com/somemore/volunteer/repository/VolunteerDetailRepository.java b/src/main/java/com/somemore/volunteer/repository/VolunteerDetailRepository.java new file mode 100644 index 000000000..7dcb9f3c1 --- /dev/null +++ b/src/main/java/com/somemore/volunteer/repository/VolunteerDetailRepository.java @@ -0,0 +1,12 @@ +package com.somemore.volunteer.repository; + +import com.somemore.volunteer.domain.VolunteerDetail; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface VolunteerDetailRepository extends JpaRepository { + + Optional findByVolunteerId(UUID volunteerId); +} diff --git a/src/main/java/com/somemore/volunteer/repository/VolunteerRepository.java b/src/main/java/com/somemore/volunteer/repository/VolunteerRepository.java new file mode 100644 index 000000000..25e15dde8 --- /dev/null +++ b/src/main/java/com/somemore/volunteer/repository/VolunteerRepository.java @@ -0,0 +1,14 @@ +package com.somemore.volunteer.repository; + +import com.somemore.volunteer.domain.Volunteer; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface VolunteerRepository extends JpaRepository { + + Optional findByOauthId(String oauthId); +} diff --git a/src/main/java/com/somemore/volunteer/service/command/RegisterVolunteerService.java b/src/main/java/com/somemore/volunteer/service/command/RegisterVolunteerService.java new file mode 100644 index 000000000..8ea14dea0 --- /dev/null +++ b/src/main/java/com/somemore/volunteer/service/command/RegisterVolunteerService.java @@ -0,0 +1,33 @@ +package com.somemore.volunteer.service.command; + +import com.somemore.volunteer.domain.Volunteer; +import com.somemore.volunteer.domain.VolunteerDetail; +import com.somemore.volunteer.dto.request.VolunteerRegisterRequestDto; +import com.somemore.volunteer.repository.VolunteerDetailRepository; +import com.somemore.volunteer.repository.VolunteerRepository; +import com.somemore.volunteer.usecase.command.RegisterVolunteerUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.somemore.volunteer.domain.Volunteer.createDefault; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class RegisterVolunteerService implements RegisterVolunteerUseCase { + + private final VolunteerRepository volunteerRepository; + private final VolunteerDetailRepository volunteerDetailRepository; + + @Override + public void registerVolunteer(VolunteerRegisterRequestDto dto) { + Volunteer volunteer = createDefault(dto.oAuthProvider(), dto.oauthId()); + volunteerRepository.save(volunteer); + + VolunteerDetail volunteerDetail = VolunteerDetail.of(dto, volunteer.getId()); + volunteerDetailRepository.save(volunteerDetail); + } +} diff --git a/src/main/java/com/somemore/volunteer/service/query/FindVolunteerIdService.java b/src/main/java/com/somemore/volunteer/service/query/FindVolunteerIdService.java new file mode 100644 index 000000000..b6c7e6ee3 --- /dev/null +++ b/src/main/java/com/somemore/volunteer/service/query/FindVolunteerIdService.java @@ -0,0 +1,27 @@ +package com.somemore.volunteer.service.query; + +import com.somemore.volunteer.repository.VolunteerRepository; +import com.somemore.volunteer.usecase.query.FindVolunteerIdUseCase; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FindVolunteerIdService implements FindVolunteerIdUseCase { + + private final VolunteerRepository volunteerRepository; + + @Override + public UUID findVolunteerIdByOAuthId(String oAuthId) { + return volunteerRepository.findByOauthId(oAuthId) + .orElseThrow(EntityNotFoundException::new) + .getId(); + } +} diff --git a/src/main/java/com/somemore/volunteer/usecase/command/RegisterVolunteerUseCase.java b/src/main/java/com/somemore/volunteer/usecase/command/RegisterVolunteerUseCase.java new file mode 100644 index 000000000..45e06dcf4 --- /dev/null +++ b/src/main/java/com/somemore/volunteer/usecase/command/RegisterVolunteerUseCase.java @@ -0,0 +1,7 @@ +package com.somemore.volunteer.usecase.command; + +import com.somemore.volunteer.dto.request.VolunteerRegisterRequestDto; + +public interface RegisterVolunteerUseCase { + void registerVolunteer(VolunteerRegisterRequestDto dto); +} diff --git a/src/main/java/com/somemore/volunteer/usecase/query/FindVolunteerIdUseCase.java b/src/main/java/com/somemore/volunteer/usecase/query/FindVolunteerIdUseCase.java new file mode 100644 index 000000000..6427cb5db --- /dev/null +++ b/src/main/java/com/somemore/volunteer/usecase/query/FindVolunteerIdUseCase.java @@ -0,0 +1,7 @@ +package com.somemore.volunteer.usecase.query; + +import java.util.UUID; + +public interface FindVolunteerIdUseCase { + UUID findVolunteerIdByOAuthId(String oAuthId); +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a8c91bcd8..e6943650b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,6 +15,29 @@ spring: generate_statistics: true show-sql: true + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} + password: ${REDIS_PASSWORD} + + security: + oauth2: + client: + registration: + naver: + client-id: ${NAVER_CLIENT_ID} + client-secret: ${NAVER_CLIENT_SECRET} + redirect-uri: "http://localhost:8080/login/oauth2/code/naver" + authorization-grant-type: authorization_code + scope: ${NAVER_SCOPE} + + provider: + naver: + authorization-uri: https://nid.naver.com/oauth2.0/authorize + token-uri: https://nid.naver.com/oauth2.0/token + user-info-uri: https://openapi.naver.com/v1/nid/me + user-name-attribute: response # 네이버 API가 사용자 정보를 "response" 객체 안에 넣어 반환 #swagger springdoc: @@ -35,5 +58,12 @@ springdoc: paths-to-match: - /api/** +frontend: + url: localhost + jwt: secret: ${JWT_SECRET} + +logging: + level: + org.springframework.security: DEBUG diff --git a/src/test/java/com/somemore/IntegrationTestSupport.java b/src/test/java/com/somemore/IntegrationTestSupport.java index 3a6c6934d..cba699570 100644 --- a/src/test/java/com/somemore/IntegrationTestSupport.java +++ b/src/test/java/com/somemore/IntegrationTestSupport.java @@ -1,11 +1,9 @@ package com.somemore; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.test.context.ActiveProfiles; @ActiveProfiles("test") -@EnableJpaAuditing @SpringBootTest public abstract class IntegrationTestSupport { diff --git a/src/test/java/com/somemore/volunteer/service/command/RegisterVolunteerServiceTest.java b/src/test/java/com/somemore/volunteer/service/command/RegisterVolunteerServiceTest.java new file mode 100644 index 000000000..8454a470d --- /dev/null +++ b/src/test/java/com/somemore/volunteer/service/command/RegisterVolunteerServiceTest.java @@ -0,0 +1,79 @@ +package com.somemore.volunteer.service.command; + +import com.somemore.IntegrationTestSupport; +import com.somemore.auth.oauth.OAuthProvider; +import com.somemore.volunteer.domain.Gender; +import com.somemore.volunteer.domain.Tier; +import com.somemore.volunteer.domain.Volunteer; +import com.somemore.volunteer.domain.VolunteerDetail; +import com.somemore.volunteer.dto.request.VolunteerRegisterRequestDto; +import com.somemore.volunteer.repository.VolunteerDetailRepository; +import com.somemore.volunteer.repository.VolunteerRepository; +import jakarta.persistence.EntityNotFoundException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.assertj.core.api.Assertions.assertThat; + +class RegisterVolunteerServiceTest extends IntegrationTestSupport { + + @Autowired + private RegisterVolunteerService registerVolunteerService; + + @Autowired + private VolunteerRepository volunteerRepository; + + @Autowired + private VolunteerDetailRepository volunteerDetailRepository; + + @AfterEach + void tearDown() { + volunteerRepository.deleteAllInBatch(); + volunteerDetailRepository.deleteAllInBatch(); + } + + @DisplayName("봉사자와 상세 정보를 저장한다") + @Test + void registerVolunteer() { + // given + VolunteerRegisterRequestDto dto = new VolunteerRegisterRequestDto( + OAuthProvider.NAVER, + "oauth-id-example", + "making", + "making@example.com", + "M", + "06-08", + "1998", + "010-1234-5678" + ); + + // when + registerVolunteerService.registerVolunteer(dto); + + // then + Volunteer volunteer = volunteerRepository.findByOauthId("oauth-id-example") + .orElseThrow(EntityNotFoundException::new); + VolunteerDetail volunteerDetail = volunteerDetailRepository.findByVolunteerId(volunteer.getId()) + .orElseThrow(EntityNotFoundException::new); + + // Volunteer + assertThat(volunteer.getOauthProvider()).isEqualTo(OAuthProvider.NAVER); + assertThat(volunteer.getOauthId()).isEqualTo("oauth-id-example"); + assertThat(volunteer.getNickname()).hasSize(8); // 8자리 default UUID + assertThat(volunteer.getImgUrl()).isEqualTo(""); // default + assertThat(volunteer.getIntroduce()).isEqualTo(""); // default + assertThat(volunteer.getTier()).isEqualTo(Tier.RED); // default + assertThat(volunteer.getTotalVolunteerHours()).isEqualTo(0); // default + assertThat(volunteer.getTotalVolunteerCount()).isEqualTo(0); // default + + // VolunteerDetail + assertThat(volunteerDetail.getVolunteerId()).isEqualTo(volunteer.getId()); + assertThat(volunteerDetail.getName()).isEqualTo("making"); + assertThat(volunteerDetail.getEmail()).isEqualTo("making@example.com"); + assertThat(volunteerDetail.getGender()).isEqualTo(Gender.MALE); + assertThat(volunteerDetail.getBirthDate()).isEqualTo("1998-06-08"); + assertThat(volunteerDetail.getContactNumber()).isEqualTo("010-1234-5678"); + } +} diff --git a/src/test/java/com/somemore/volunteer/service/query/FindVolunteerIdServiceTest.java b/src/test/java/com/somemore/volunteer/service/query/FindVolunteerIdServiceTest.java new file mode 100644 index 000000000..29aed5992 --- /dev/null +++ b/src/test/java/com/somemore/volunteer/service/query/FindVolunteerIdServiceTest.java @@ -0,0 +1,60 @@ +package com.somemore.volunteer.service.query; + +import com.somemore.IntegrationTestSupport; +import com.somemore.auth.oauth.OAuthProvider; +import com.somemore.volunteer.domain.Volunteer; +import com.somemore.volunteer.repository.VolunteerRepository; +import jakarta.persistence.EntityNotFoundException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class FindVolunteerIdServiceTest extends IntegrationTestSupport { + + @Autowired + private FindVolunteerIdService findVolunteerIdService; + + @Autowired + private VolunteerRepository volunteerRepository; + + @AfterEach + void tearDown() { + volunteerRepository.deleteAllInBatch(); + } + + @DisplayName("존재하는 OAuth ID로 봉사자 ID를 조회한다") + @Test + void findVolunteerId() { + // given + String oAuthId = "example-oauth-id"; + Volunteer volunteer = Volunteer.createDefault(OAuthProvider.NAVER, oAuthId); + + volunteerRepository.save(volunteer); + + // when + UUID actualId = findVolunteerIdService.findVolunteerIdByOAuthId(oAuthId); + + // then + assertThat(actualId) + .isNotNull() + .isEqualTo(volunteer.getId()); + } + + @DisplayName("존재하지 않는 OAuth ID로 조회 시 예외를 던진다") + @Test + void throwExceptionWhenVolunteerNotFound() { + // given + String oAuthId = "non-existing-oauth-id"; + + // when + // then + assertThatThrownBy(() -> findVolunteerIdService.findVolunteerIdByOAuthId(oAuthId)) + .isInstanceOf(EntityNotFoundException.class); + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 4b984f3e6..a80bba3cc 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -21,4 +21,15 @@ spring: init: mode: never -JWT_SECRET: 965b005bea929b5da041a3c8cf24ce0130880eb438942c159039f210aac07cc6857394c1253db2f4673f81e9485415764a086971620149072329179b800ea8e7 \ No newline at end of file + data: + redis: + host: localhost # 로컬 Redis 사용 + port: 6379 + password: # 테스트에서는 비밀번호 없이 연결 + +frontend: + url: http://localhost:3000 # 테스트용 프론트엔드 주소 + +jwt: + secret: 63bf2c80266cd25072e53b3482e318c30d1cd18d8c98d0f5d278530a94fe28d9fbbec531e5ccb58c725c125738182357786b71f43a7172c5d0c94a17f0da44f2 # 테스트용 JWT 시크릿 키 +