From 0612f167758ae7ccd93eb8218c5dce62f46a7cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EA=B2=BD=EC=88=98?= Date: Wed, 14 Jan 2026 04:06:57 +0900 Subject: [PATCH 1/5] feat(#2): kakao oauth2 login and jwt token issue --- build.gradle | 10 +- .../global/auth/LoginController.java | 25 +++++ .../global/auth/details/CustomOAuth2User.java | 39 ++++++++ .../auth/details/CustomUserDetails.java | 73 ++++++++++++++ .../global/auth/details/SnsPrincipal.java | 69 ++++++++++++++ .../global/auth/jwt/JwtAuthFilter.java | 5 + .../global/auth/jwt/JwtProvider.java | 90 ++++++++++++++++++ .../global/auth/oauth/KakaoOAuthUserInfo.java | 65 +++++++++++++ .../global/auth/oauth/NaverOAuthUserInfo.java | 64 +++++++++++++ .../global/auth/oauth/OAuthAttributes.java | 14 +++ .../global/auth/oauth/OAuthUserInfo.java | 8 ++ .../auth/security/OAuth2SuccessHandler.java | 63 ++++++++++++ .../global/auth/security/SecurityConfig.java | 89 +++++++++++++++++ .../global/auth/security/SwaggerConfig.java | 4 + .../auth/service/CustomOAuth2UserService.java | 74 ++++++++++++++ .../auth/service/UserAccountService.java | 30 ++++++ .../user/application/service/UserService.java | 12 +++ .../application/service/UserServiceImpl.java | 57 +++++++++++ .../RealMatch/user/domain/entity/User.java | 51 ++++++++++ .../domain/repository/UserRepository.java | 12 +++ src/main/resources/static/favicon.ico | 0 src/main/resources/static/img.png | Bin 0 -> 57848 bytes .../RealMatch/test/TestController.java | 13 +++ 23 files changed, 863 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/example/RealMatch/global/auth/LoginController.java create mode 100644 src/main/java/com/example/RealMatch/global/auth/details/CustomOAuth2User.java create mode 100644 src/main/java/com/example/RealMatch/global/auth/details/CustomUserDetails.java create mode 100644 src/main/java/com/example/RealMatch/global/auth/details/SnsPrincipal.java create mode 100644 src/main/java/com/example/RealMatch/global/auth/jwt/JwtAuthFilter.java create mode 100644 src/main/java/com/example/RealMatch/global/auth/jwt/JwtProvider.java create mode 100644 src/main/java/com/example/RealMatch/global/auth/oauth/KakaoOAuthUserInfo.java create mode 100644 src/main/java/com/example/RealMatch/global/auth/oauth/NaverOAuthUserInfo.java create mode 100644 src/main/java/com/example/RealMatch/global/auth/oauth/OAuthAttributes.java create mode 100644 src/main/java/com/example/RealMatch/global/auth/oauth/OAuthUserInfo.java create mode 100644 src/main/java/com/example/RealMatch/global/auth/security/OAuth2SuccessHandler.java create mode 100644 src/main/java/com/example/RealMatch/global/auth/security/SecurityConfig.java create mode 100644 src/main/java/com/example/RealMatch/global/auth/security/SwaggerConfig.java create mode 100644 src/main/java/com/example/RealMatch/global/auth/service/CustomOAuth2UserService.java create mode 100644 src/main/java/com/example/RealMatch/global/auth/service/UserAccountService.java create mode 100644 src/main/java/com/example/RealMatch/user/application/service/UserService.java create mode 100644 src/main/java/com/example/RealMatch/user/application/service/UserServiceImpl.java create mode 100644 src/main/java/com/example/RealMatch/user/domain/entity/User.java create mode 100644 src/main/java/com/example/RealMatch/user/domain/repository/UserRepository.java create mode 100644 src/main/resources/static/favicon.ico create mode 100644 src/main/resources/static/img.png create mode 100644 src/test/java/com/example/RealMatch/test/TestController.java diff --git a/build.gradle b/build.gradle index 6c2b21c4..99f84889 100644 --- a/build.gradle +++ b/build.gradle @@ -32,9 +32,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' @@ -45,10 +47,10 @@ dependencies { testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - //JWT - implementation 'io.jsonwebtoken:jjwt-api:0.12.6' - implementation 'io.jsonwebtoken:jjwt-impl:0.12.6' - implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6' + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' // mySQL runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/java/com/example/RealMatch/global/auth/LoginController.java b/src/main/java/com/example/RealMatch/global/auth/LoginController.java new file mode 100644 index 00000000..f8a3ae47 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/LoginController.java @@ -0,0 +1,25 @@ +package com.example.RealMatch.global.auth; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class LoginController { + + // OAuth2 로그인 성공 후 redirect URL + @GetMapping("/login/success") + public String loginSuccess( + @RequestParam String accessToken, + @RequestParam String refreshToken + ) { + // 간단하게 HTML로 화면에 토큰 출력 + return "" + + "" + + "

로그인 성공!

" + + "

Access Token: " + accessToken + "

" + + "

Refresh Token: " + refreshToken + "

" + + "" + + ""; + } +} diff --git a/src/main/java/com/example/RealMatch/global/auth/details/CustomOAuth2User.java b/src/main/java/com/example/RealMatch/global/auth/details/CustomOAuth2User.java new file mode 100644 index 00000000..a58b954f --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/details/CustomOAuth2User.java @@ -0,0 +1,39 @@ +package com.example.RealMatch.global.auth.details; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import com.example.RealMatch.user.domain.entity.User; + +import lombok.Getter; + +@Getter +public class CustomOAuth2User implements OAuth2User { + + private final User user; + private final Map attributes; + + public CustomOAuth2User(User user, Map attributes) { + this.user = user; + this.attributes = attributes; + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } + + @Override + public String getName() { + return user.getName(); + } +} diff --git a/src/main/java/com/example/RealMatch/global/auth/details/CustomUserDetails.java b/src/main/java/com/example/RealMatch/global/auth/details/CustomUserDetails.java new file mode 100644 index 00000000..6bf6d98c --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/details/CustomUserDetails.java @@ -0,0 +1,73 @@ +package com.example.RealMatch.global.auth.details; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import com.example.RealMatch.user.domain.entity.User; + +public class CustomUserDetails implements UserDetails, OAuth2User { + + private final User user; + private final Map attributes; + + public CustomUserDetails(User user, Map attributes) { + this.user = user; + this.attributes = attributes; + } + + public User getUser() { + return user; + } + + // OAuth2User + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public String getName() { + return user.getId().toString(); + } + + // UserDetails + @Override + public Collection getAuthorities() { + return Collections.emptyList(); // 아직 Role 안 씀 + } + + @Override + public String getPassword() { + return null; // OAuth 로그인은 패스워드 없음 + } + + @Override + public String getUsername() { + return user.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/example/RealMatch/global/auth/details/SnsPrincipal.java b/src/main/java/com/example/RealMatch/global/auth/details/SnsPrincipal.java new file mode 100644 index 00000000..dffbe6fa --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/details/SnsPrincipal.java @@ -0,0 +1,69 @@ +package com.example.RealMatch.global.auth.details; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import com.example.RealMatch.user.domain.entity.User; + +public record SnsPrincipal(User user) implements OAuth2User, UserDetails { + + public static SnsPrincipal fromEntity(User user) { + return new SnsPrincipal(user); + } + + @Override + public Map getAttributes() { + return Map.of( + "id", user.getId(), + "email", user.getEmail(), + "nickname", user.getName(), + "provider", user.getProvider(), + "providerId", user.getProviderId() + ); + } + + @Override + public Collection getAuthorities() { + return Collections.emptyList(); // OAuth는 Role 없음 + } + + @Override + public String getName() { + return user.getName(); + } + + @Override + public String getPassword() { + return ""; // OAuth는 패스워드 사용 X + } + + @Override + public String getUsername() { + return user.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/example/RealMatch/global/auth/jwt/JwtAuthFilter.java b/src/main/java/com/example/RealMatch/global/auth/jwt/JwtAuthFilter.java new file mode 100644 index 00000000..c5dbec8f --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/jwt/JwtAuthFilter.java @@ -0,0 +1,5 @@ +package com.example.RealMatch.global.auth.jwt; + +public class JwtAuthFilter { + +} diff --git a/src/main/java/com/example/RealMatch/global/auth/jwt/JwtProvider.java b/src/main/java/com/example/RealMatch/global/auth/jwt/JwtProvider.java new file mode 100644 index 00000000..a3908aa4 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/jwt/JwtProvider.java @@ -0,0 +1,90 @@ +package com.example.RealMatch.global.auth.jwt; + +import java.nio.charset.StandardCharsets; +import java.util.Date; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import com.example.RealMatch.user.domain.entity.User; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class JwtProvider { + + private final SecretKey key; + private final long accessTokenExpireTime; + private final long refreshTokenExpireTime; + + public JwtProvider( + @Value("${jwt.secret}") String secretKey, + @Value("${jwt.access-token-expire-time}") long accessTokenExpireTime, + @Value("${jwt.refresh-token-expire-time}") long refreshTokenExpireTime + ) { + byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8); + this.key = new SecretKeySpec(keyBytes, SignatureAlgorithm.HS256.getJcaName()); + this.accessTokenExpireTime = accessTokenExpireTime; + this.refreshTokenExpireTime = refreshTokenExpireTime; + log.info("JwtProvider initialized - Access: {}ms, Refresh: {}ms", + accessTokenExpireTime, refreshTokenExpireTime); + } + + public String createAccessToken(User user) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + accessTokenExpireTime); + + String token = Jwts.builder() + .setSubject(String.valueOf(user.getId())) + .claim("email", user.getEmail()) + .claim("name", user.getName()) + .claim("provider", user.getProvider()) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + log.info("Access token created for user ID: {}", user.getId()); + return token; + } + + public String createRefreshToken(User user) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + refreshTokenExpireTime); + + String token = Jwts.builder() + .setSubject(String.valueOf(user.getId())) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + log.info("Refresh token created for user ID: {}", user.getId()); + return token; + } + + public Claims validateToken(String token) { + try { + return Jwts.parser() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (Exception e) { + log.error("Invalid JWT token: {}", e.getMessage()); + throw new IllegalArgumentException("Invalid token", e); + } + } + + public Long getUserIdFromToken(String token) { + Claims claims = validateToken(token); + return Long.parseLong(claims.getSubject()); + } +} diff --git a/src/main/java/com/example/RealMatch/global/auth/oauth/KakaoOAuthUserInfo.java b/src/main/java/com/example/RealMatch/global/auth/oauth/KakaoOAuthUserInfo.java new file mode 100644 index 00000000..8317e14b --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/oauth/KakaoOAuthUserInfo.java @@ -0,0 +1,65 @@ +package com.example.RealMatch.global.auth.oauth; + +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class KakaoOAuthUserInfo implements OAuthUserInfo { + + private final Map attributes; + + public KakaoOAuthUserInfo(Map attributes) { + this.attributes = attributes; + log.debug("=== Kakao OAuth Attributes ==="); + log.debug("Full attributes: {}", attributes); + } + + @Override + public String getProviderId() { + Object id = attributes.get("id"); + log.debug("Kakao ProviderId: {}", id); + return String.valueOf(id); + } + + @Override + public String getProvider() { + return "kakao"; + } + + @Override + public String getEmail() { + Map kakaoAccount = (Map) attributes.get("kakao_account"); + + if (kakaoAccount == null) { + log.warn("kakao_account is null"); + return "no-email@kakao.com"; + } + + String email = (String) kakaoAccount.get("email"); + log.debug("Kakao Email: {}", email); + + return email != null ? email : "no-email@kakao.com"; + } + + @Override + public String getName() { + Map kakaoAccount = (Map) attributes.get("kakao_account"); + + if (kakaoAccount != null) { + Map profile = (Map) kakaoAccount.get("profile"); + + if (profile != null) { + String nickname = (String) profile.get("nickname"); + log.debug("Kakao Nickname: {}", nickname); + + if (nickname != null && !nickname.isEmpty()) { + return nickname; + } + } + } + + log.warn("Kakao nickname not found, using default"); + return "카카오사용자"; + } +} diff --git a/src/main/java/com/example/RealMatch/global/auth/oauth/NaverOAuthUserInfo.java b/src/main/java/com/example/RealMatch/global/auth/oauth/NaverOAuthUserInfo.java new file mode 100644 index 00000000..20bcf8bc --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/oauth/NaverOAuthUserInfo.java @@ -0,0 +1,64 @@ +package com.example.RealMatch.global.auth.oauth; + +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class NaverOAuthUserInfo implements OAuthUserInfo { + + private final Map attributes; + + public NaverOAuthUserInfo(final Map attributes) { + this.attributes = attributes; + log.debug("=== Naver OAuth Attributes ==="); + log.debug("Full attributes: {}", attributes); + } + + @Override + public String getProviderId() { + final Map response = (Map) attributes.get("response"); + + if (response == null) { + log.warn("Naver response is null"); + return null; + } + + final String id = (String) response.get("id"); + log.debug("Naver ProviderId: {}", id); + return id; + } + + @Override + public String getProvider() { + return "naver"; + } + + @Override + public String getEmail() { + final Map response = (Map) attributes.get("response"); + + if (response == null) { + log.warn("Naver response is null"); + return "no-email@naver.com"; + } + + final String email = (String) response.get("email"); + log.debug("Naver Email: {}", email); + return email != null ? email : "no-email@naver.com"; + } + + @Override + public String getName() { + final Map response = (Map) attributes.get("response"); + + if (response == null) { + log.warn("Naver response is null"); + return "네이버사용자"; + } + + final String name = (String) response.get("name"); + log.debug("Naver Name: {}", name); + return name != null ? name : "네이버사용자"; + } +} diff --git a/src/main/java/com/example/RealMatch/global/auth/oauth/OAuthAttributes.java b/src/main/java/com/example/RealMatch/global/auth/oauth/OAuthAttributes.java new file mode 100644 index 00000000..6499a064 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/oauth/OAuthAttributes.java @@ -0,0 +1,14 @@ +package com.example.RealMatch.global.auth.oauth; + +import java.util.Map; + +public class OAuthAttributes { + + public static OAuthUserInfo of(final String provider, final Map attributes) { + return switch (provider) { + case "kakao" -> new KakaoOAuthUserInfo(attributes); + case "naver" -> new NaverOAuthUserInfo(attributes); + default -> throw new IllegalArgumentException("Unsupported provider: " + provider); + }; + } +} diff --git a/src/main/java/com/example/RealMatch/global/auth/oauth/OAuthUserInfo.java b/src/main/java/com/example/RealMatch/global/auth/oauth/OAuthUserInfo.java new file mode 100644 index 00000000..389bd2be --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/oauth/OAuthUserInfo.java @@ -0,0 +1,8 @@ +package com.example.RealMatch.global.auth.oauth; + +public interface OAuthUserInfo { + String getProviderId(); + String getEmail(); + String getName(); + String getProvider(); +} diff --git a/src/main/java/com/example/RealMatch/global/auth/security/OAuth2SuccessHandler.java b/src/main/java/com/example/RealMatch/global/auth/security/OAuth2SuccessHandler.java new file mode 100644 index 00000000..8231a0b8 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/security/OAuth2SuccessHandler.java @@ -0,0 +1,63 @@ +package com.example.RealMatch.global.auth.security; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import com.example.RealMatch.global.auth.details.CustomOAuth2User; +import com.example.RealMatch.global.auth.jwt.JwtProvider; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { + + private final JwtProvider jwtProvider; + + // 테스트용 + @Value("${app.frontend.url:http://localhost:8080}") + private String frontendUrl; + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException { + + try { + CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); + + log.info("OAuth2 authentication successful for user: {}", oAuth2User.getUser().getId()); + + String accessToken = jwtProvider.createAccessToken(oAuth2User.getUser()); + String refreshToken = jwtProvider.createRefreshToken(oAuth2User.getUser()); + + log.debug("Tokens created - Access token length: {}", accessToken.length()); + + String redirectUrl = String.format("%s/login/success?accessToken=%s&refreshToken=%s", + frontendUrl, + URLEncoder.encode(accessToken, StandardCharsets.UTF_8), + URLEncoder.encode(refreshToken, StandardCharsets.UTF_8) + ); + + log.info("Redirecting to: {}", frontendUrl + "/login/success"); + response.sendRedirect(redirectUrl); + + } catch (Exception e) { + log.error("Error during OAuth2 success handling", e); + response.sendRedirect(frontendUrl + "/login/error?message=" + + URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8)); + } + } +} diff --git a/src/main/java/com/example/RealMatch/global/auth/security/SecurityConfig.java b/src/main/java/com/example/RealMatch/global/auth/security/SecurityConfig.java new file mode 100644 index 00000000..10931910 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/security/SecurityConfig.java @@ -0,0 +1,89 @@ +package com.example.RealMatch.global.auth.security; + +import java.util.Arrays; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import com.example.RealMatch.global.auth.service.CustomOAuth2UserService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Configuration +@RequiredArgsConstructor +@EnableWebSecurity +public class SecurityConfig { + + private final CustomOAuth2UserService customOAuth2UserService; + private final OAuth2SuccessHandler oAuth2SuccessHandler; + + /** + * SecurityFilterChain 설정 + * - CORS 설정 적용 + * - CSRF 비활성화 + * - 세션 상태 Stateless + * - 특정 URL 허용 (/login/**, /oauth2/** 등) + * - OAuth2 로그인 성공/실패 핸들러 지정 + */ + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + log.info("Configuring Security Filter Chain"); + + http + // CORS 설정 + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + // CSRF 비활성화 + .csrf(csrf -> csrf.disable()) + // 세션을 사용하지 않음 + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // 요청 권한 설정 + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/", "/error", "/favicon.ico", + "/css/**", "/js/**", "/images/**", + "/login/**", "/oauth2/**" + ).permitAll() + .anyRequest().authenticated() + ) + // OAuth2 로그인 설정 + .oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) + .successHandler(oAuth2SuccessHandler) // 성공 핸들러 + .failureHandler((request, response, exception) -> { // 실패 핸들러 + log.error("OAuth2 login failed", exception); + response.sendRedirect("/login/failure?message=" + exception.getMessage()); + }) + ); + + return http.build(); + } + + /** + * CORS 설정 + * - 프론트엔드(React) 주소 허용 + * - 허용 메서드, 헤더, 인증 허용 + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + + return source; + } +} diff --git a/src/main/java/com/example/RealMatch/global/auth/security/SwaggerConfig.java b/src/main/java/com/example/RealMatch/global/auth/security/SwaggerConfig.java new file mode 100644 index 00000000..25005c48 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/security/SwaggerConfig.java @@ -0,0 +1,4 @@ +package com.example.RealMatch.global.auth.security; + +public class SwaggerConfig { +} diff --git a/src/main/java/com/example/RealMatch/global/auth/service/CustomOAuth2UserService.java b/src/main/java/com/example/RealMatch/global/auth/service/CustomOAuth2UserService.java new file mode 100644 index 00000000..1826f53b --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/service/CustomOAuth2UserService.java @@ -0,0 +1,74 @@ +package com.example.RealMatch.global.auth.service; + +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import com.example.RealMatch.global.auth.details.CustomOAuth2User; +import com.example.RealMatch.global.auth.oauth.OAuthAttributes; +import com.example.RealMatch.global.auth.oauth.OAuthUserInfo; +import com.example.RealMatch.user.domain.entity.User; +import com.example.RealMatch.user.domain.repository.UserRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + private final UserRepository userRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + + OAuth2User oAuth2User = super.loadUser(userRequest); + String provider = userRequest.getClientRegistration().getRegistrationId(); + + log.info("=== OAuth2 Login Start ==="); + log.info("Provider: {}", provider); + log.debug("OAuth2 attributes: {}", oAuth2User.getAttributes()); + + try { + // Provider별 사용자 정보 파싱 + OAuthUserInfo userInfo = OAuthAttributes.of(provider, oAuth2User.getAttributes()); + + log.info("Parsed user info:"); + log.info(" - Provider: {}", provider); + log.info(" - ProviderId: {}", userInfo.getProviderId()); + log.info(" - Email: {}", userInfo.getEmail()); + log.info(" - Name: {}", userInfo.getName()); + + // DB에서 사용자 조회 또는 생성 + User user = userRepository.findByProviderAndProviderId(provider, userInfo.getProviderId()) + .map(existingUser -> { + log.info("Existing user found: {}", existingUser.getId()); + existingUser.updateProfile(userInfo.getEmail(), userInfo.getName()); + return userRepository.save(existingUser); + }) + .orElseGet(() -> { + log.info("Creating new user"); + User newUser = User.createOAuthUser( + userInfo.getEmail(), + userInfo.getName(), + provider, + userInfo.getProviderId() + ); + return userRepository.save(newUser); + }); + + log.info("User authenticated successfully - ID: {}, Name: {}", + user.getId(), user.getName()); + log.info("=== OAuth2 Login End ==="); + + return new CustomOAuth2User(user, oAuth2User.getAttributes()); + + } catch (Exception e) { + log.error("Error during OAuth2 user processing", e); + throw new OAuth2AuthenticationException("OAuth2 processing failed: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/example/RealMatch/global/auth/service/UserAccountService.java b/src/main/java/com/example/RealMatch/global/auth/service/UserAccountService.java new file mode 100644 index 00000000..7abfbfaf --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/auth/service/UserAccountService.java @@ -0,0 +1,30 @@ +package com.example.RealMatch.global.auth.service; + +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import com.example.RealMatch.user.domain.entity.User; +import com.example.RealMatch.user.domain.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class UserAccountService { + + private final UserRepository userRepository; + + public Optional findByProviderAndProviderId(final String provider, + final String providerId) { + return userRepository.findByProviderAndProviderId(provider, providerId); + } + + public User saveOAuthUser(final String email, + final String name, + final String provider, + final String providerId) { + final User user = User.createOAuthUser(email, name, provider, providerId); + return userRepository.save(user); + } +} diff --git a/src/main/java/com/example/RealMatch/user/application/service/UserService.java b/src/main/java/com/example/RealMatch/user/application/service/UserService.java new file mode 100644 index 00000000..ade91fc4 --- /dev/null +++ b/src/main/java/com/example/RealMatch/user/application/service/UserService.java @@ -0,0 +1,12 @@ +package com.example.RealMatch.user.application.service; + +import java.util.Optional; + +import com.example.RealMatch.user.domain.entity.User; + +public interface UserService { + + Optional findByProviderAndProviderId(String provider, String providerId); + + User saveOAuthUser(String email, String nickname, String provider, String providerId); +} diff --git a/src/main/java/com/example/RealMatch/user/application/service/UserServiceImpl.java b/src/main/java/com/example/RealMatch/user/application/service/UserServiceImpl.java new file mode 100644 index 00000000..e5c38364 --- /dev/null +++ b/src/main/java/com/example/RealMatch/user/application/service/UserServiceImpl.java @@ -0,0 +1,57 @@ +package com.example.RealMatch.user.application.service; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Service; + +import com.example.RealMatch.user.domain.entity.User; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class UserServiceImpl implements UserService { + + private final Map memoryStore = new ConcurrentHashMap<>(); + + @Override + public Optional findByProviderAndProviderId(final String provider, final String providerId) { + final String key = provider + ":" + providerId; + final User user = memoryStore.get(key); + + if (user != null) { + log.info("User found in memory store: {} ({})", user.getName(), user.getEmail()); + } else { + log.info("User not found in memory store for key: {}", key); + } + + return Optional.ofNullable(user); + } + + @Override + public User saveOAuthUser(final String email, + final String nickname, + final String provider, + final String providerId) { + final String key = provider + ":" + providerId; + + final User existingUser = memoryStore.get(key); + if (existingUser != null) { + log.info("Updating existing user: {}", existingUser.getId()); + existingUser.updateProfile(email, nickname); + memoryStore.put(key, existingUser); + return existingUser; + } + + final User user = User.createOAuthUser(email, nickname, provider, providerId); + memoryStore.put(key, user); + + log.info("User saved in memory: {} / Email: {} / ID: {}", + user.getName(), user.getEmail(), user.getId()); + log.info("Total users in memory: {}", memoryStore.size()); + + return user; + } +} diff --git a/src/main/java/com/example/RealMatch/user/domain/entity/User.java b/src/main/java/com/example/RealMatch/user/domain/entity/User.java new file mode 100644 index 00000000..d4e85fdb --- /dev/null +++ b/src/main/java/com/example/RealMatch/user/domain/entity/User.java @@ -0,0 +1,51 @@ +package com.example.RealMatch.user.domain.entity; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String email; + private String name; + private String provider; + private String providerId; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public static User createOAuthUser(String email, String name, String provider, String providerId) { + User user = new User(); + user.email = email; + user.name = name; + user.provider = provider; + user.providerId = providerId; + user.createdAt = LocalDateTime.now(); + user.updatedAt = LocalDateTime.now(); + return user; + } + + public void updateProfile(String email, String name) { + this.email = email; + this.name = name; + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/example/RealMatch/user/domain/repository/UserRepository.java b/src/main/java/com/example/RealMatch/user/domain/repository/UserRepository.java new file mode 100644 index 00000000..03275cde --- /dev/null +++ b/src/main/java/com/example/RealMatch/user/domain/repository/UserRepository.java @@ -0,0 +1,12 @@ +package com.example.RealMatch.user.domain.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.example.RealMatch.user.domain.entity.User; + +public interface UserRepository extends JpaRepository { + + Optional findByProviderAndProviderId(String provider, String providerId); +} diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/src/main/resources/static/img.png b/src/main/resources/static/img.png new file mode 100644 index 0000000000000000000000000000000000000000..4b8d1bf58a741ab2e01873cd86d64470b8c63d57 GIT binary patch literal 57848 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 Date: Mon, 19 Jan 2026 14:43:34 +0900 Subject: [PATCH 2/5] feat(#2): kakao oauth2 login --- .../global/auth/LoginController.java | 25 ----- .../auth/details/CustomUserDetails.java | 73 -------------- .../global/auth/details/SnsPrincipal.java | 69 -------------- .../global/auth/jwt/JwtAuthFilter.java | 5 - .../global/auth/jwt/JwtProvider.java | 90 ------------------ .../global/auth/oauth/KakaoOAuthUserInfo.java | 65 ------------- .../global/auth/oauth/NaverOAuthUserInfo.java | 64 ------------- .../global/auth/oauth/OAuthAttributes.java | 14 --- .../global/auth/oauth/OAuthUserInfo.java | 8 -- .../global/auth/security/SecurityConfig.java | 89 ----------------- .../global/auth/security/SwaggerConfig.java | 4 - .../auth/service/CustomOAuth2UserService.java | 74 -------------- .../auth/service/UserAccountService.java | 30 ------ .../global/config/SecurityConfig.java | 79 ++++++++------- .../config/jwt/JwtAuthenticationFilter.java | 26 +++++ .../oauth}/CustomOAuth2User.java | 6 +- .../oauth}/OAuth2SuccessHandler.java | 37 ++++--- .../service/CustomOAuth2UserService.java | 70 ++++++++++++++ .../global/controller/TestController.java | 20 +++- .../RealMatch/match/domain/entity/User.java | 48 ++++++++++ .../domain/repository/UserRepository.java | 11 +++ .../user/application/service/UserService.java | 12 --- .../application/service/UserServiceImpl.java | 57 ----------- .../RealMatch/user/domain/entity/User.java | 51 ---------- .../domain/repository/UserRepository.java | 12 --- src/main/resources/static/img.png | Bin 57848 -> 0 bytes 26 files changed, 236 insertions(+), 803 deletions(-) delete mode 100644 src/main/java/com/example/RealMatch/global/auth/LoginController.java delete mode 100644 src/main/java/com/example/RealMatch/global/auth/details/CustomUserDetails.java delete mode 100644 src/main/java/com/example/RealMatch/global/auth/details/SnsPrincipal.java delete mode 100644 src/main/java/com/example/RealMatch/global/auth/jwt/JwtAuthFilter.java delete mode 100644 src/main/java/com/example/RealMatch/global/auth/jwt/JwtProvider.java delete mode 100644 src/main/java/com/example/RealMatch/global/auth/oauth/KakaoOAuthUserInfo.java delete mode 100644 src/main/java/com/example/RealMatch/global/auth/oauth/NaverOAuthUserInfo.java delete mode 100644 src/main/java/com/example/RealMatch/global/auth/oauth/OAuthAttributes.java delete mode 100644 src/main/java/com/example/RealMatch/global/auth/oauth/OAuthUserInfo.java delete mode 100644 src/main/java/com/example/RealMatch/global/auth/security/SecurityConfig.java delete mode 100644 src/main/java/com/example/RealMatch/global/auth/security/SwaggerConfig.java delete mode 100644 src/main/java/com/example/RealMatch/global/auth/service/CustomOAuth2UserService.java delete mode 100644 src/main/java/com/example/RealMatch/global/auth/service/UserAccountService.java rename src/main/java/com/example/RealMatch/global/{auth/details => config/oauth}/CustomOAuth2User.java (84%) rename src/main/java/com/example/RealMatch/global/{auth/security => config/oauth}/OAuth2SuccessHandler.java (56%) create mode 100644 src/main/java/com/example/RealMatch/global/config/service/CustomOAuth2UserService.java create mode 100644 src/main/java/com/example/RealMatch/match/domain/entity/User.java create mode 100644 src/main/java/com/example/RealMatch/match/domain/repository/UserRepository.java delete mode 100644 src/main/java/com/example/RealMatch/user/application/service/UserService.java delete mode 100644 src/main/java/com/example/RealMatch/user/application/service/UserServiceImpl.java delete mode 100644 src/main/java/com/example/RealMatch/user/domain/entity/User.java delete mode 100644 src/main/java/com/example/RealMatch/user/domain/repository/UserRepository.java delete mode 100644 src/main/resources/static/img.png diff --git a/src/main/java/com/example/RealMatch/global/auth/LoginController.java b/src/main/java/com/example/RealMatch/global/auth/LoginController.java deleted file mode 100644 index f8a3ae47..00000000 --- a/src/main/java/com/example/RealMatch/global/auth/LoginController.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.RealMatch.global.auth; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class LoginController { - - // OAuth2 로그인 성공 후 redirect URL - @GetMapping("/login/success") - public String loginSuccess( - @RequestParam String accessToken, - @RequestParam String refreshToken - ) { - // 간단하게 HTML로 화면에 토큰 출력 - return "" + - "" + - "

로그인 성공!

" + - "

Access Token: " + accessToken + "

" + - "

Refresh Token: " + refreshToken + "

" + - "" + - ""; - } -} diff --git a/src/main/java/com/example/RealMatch/global/auth/details/CustomUserDetails.java b/src/main/java/com/example/RealMatch/global/auth/details/CustomUserDetails.java deleted file mode 100644 index 6bf6d98c..00000000 --- a/src/main/java/com/example/RealMatch/global/auth/details/CustomUserDetails.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.example.RealMatch.global.auth.details; - -import java.util.Collection; -import java.util.Collections; -import java.util.Map; - -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.oauth2.core.user.OAuth2User; - -import com.example.RealMatch.user.domain.entity.User; - -public class CustomUserDetails implements UserDetails, OAuth2User { - - private final User user; - private final Map attributes; - - public CustomUserDetails(User user, Map attributes) { - this.user = user; - this.attributes = attributes; - } - - public User getUser() { - return user; - } - - // OAuth2User - @Override - public Map getAttributes() { - return attributes; - } - - @Override - public String getName() { - return user.getId().toString(); - } - - // UserDetails - @Override - public Collection getAuthorities() { - return Collections.emptyList(); // 아직 Role 안 씀 - } - - @Override - public String getPassword() { - return null; // OAuth 로그인은 패스워드 없음 - } - - @Override - public String getUsername() { - return user.getEmail(); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } -} diff --git a/src/main/java/com/example/RealMatch/global/auth/details/SnsPrincipal.java b/src/main/java/com/example/RealMatch/global/auth/details/SnsPrincipal.java deleted file mode 100644 index dffbe6fa..00000000 --- a/src/main/java/com/example/RealMatch/global/auth/details/SnsPrincipal.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.example.RealMatch.global.auth.details; - -import java.util.Collection; -import java.util.Collections; -import java.util.Map; - -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.oauth2.core.user.OAuth2User; - -import com.example.RealMatch.user.domain.entity.User; - -public record SnsPrincipal(User user) implements OAuth2User, UserDetails { - - public static SnsPrincipal fromEntity(User user) { - return new SnsPrincipal(user); - } - - @Override - public Map getAttributes() { - return Map.of( - "id", user.getId(), - "email", user.getEmail(), - "nickname", user.getName(), - "provider", user.getProvider(), - "providerId", user.getProviderId() - ); - } - - @Override - public Collection getAuthorities() { - return Collections.emptyList(); // OAuth는 Role 없음 - } - - @Override - public String getName() { - return user.getName(); - } - - @Override - public String getPassword() { - return ""; // OAuth는 패스워드 사용 X - } - - @Override - public String getUsername() { - return user.getEmail(); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } -} diff --git a/src/main/java/com/example/RealMatch/global/auth/jwt/JwtAuthFilter.java b/src/main/java/com/example/RealMatch/global/auth/jwt/JwtAuthFilter.java deleted file mode 100644 index c5dbec8f..00000000 --- a/src/main/java/com/example/RealMatch/global/auth/jwt/JwtAuthFilter.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.RealMatch.global.auth.jwt; - -public class JwtAuthFilter { - -} diff --git a/src/main/java/com/example/RealMatch/global/auth/jwt/JwtProvider.java b/src/main/java/com/example/RealMatch/global/auth/jwt/JwtProvider.java deleted file mode 100644 index a3908aa4..00000000 --- a/src/main/java/com/example/RealMatch/global/auth/jwt/JwtProvider.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.RealMatch.global.auth.jwt; - -import java.nio.charset.StandardCharsets; -import java.util.Date; - -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import com.example.RealMatch.user.domain.entity.User; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Component -public class JwtProvider { - - private final SecretKey key; - private final long accessTokenExpireTime; - private final long refreshTokenExpireTime; - - public JwtProvider( - @Value("${jwt.secret}") String secretKey, - @Value("${jwt.access-token-expire-time}") long accessTokenExpireTime, - @Value("${jwt.refresh-token-expire-time}") long refreshTokenExpireTime - ) { - byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8); - this.key = new SecretKeySpec(keyBytes, SignatureAlgorithm.HS256.getJcaName()); - this.accessTokenExpireTime = accessTokenExpireTime; - this.refreshTokenExpireTime = refreshTokenExpireTime; - log.info("JwtProvider initialized - Access: {}ms, Refresh: {}ms", - accessTokenExpireTime, refreshTokenExpireTime); - } - - public String createAccessToken(User user) { - Date now = new Date(); - Date expiryDate = new Date(now.getTime() + accessTokenExpireTime); - - String token = Jwts.builder() - .setSubject(String.valueOf(user.getId())) - .claim("email", user.getEmail()) - .claim("name", user.getName()) - .claim("provider", user.getProvider()) - .setIssuedAt(now) - .setExpiration(expiryDate) - .signWith(key, SignatureAlgorithm.HS256) - .compact(); - - log.info("Access token created for user ID: {}", user.getId()); - return token; - } - - public String createRefreshToken(User user) { - Date now = new Date(); - Date expiryDate = new Date(now.getTime() + refreshTokenExpireTime); - - String token = Jwts.builder() - .setSubject(String.valueOf(user.getId())) - .setIssuedAt(now) - .setExpiration(expiryDate) - .signWith(key, SignatureAlgorithm.HS256) - .compact(); - - log.info("Refresh token created for user ID: {}", user.getId()); - return token; - } - - public Claims validateToken(String token) { - try { - return Jwts.parser() - .setSigningKey(key) - .build() - .parseClaimsJws(token) - .getBody(); - } catch (Exception e) { - log.error("Invalid JWT token: {}", e.getMessage()); - throw new IllegalArgumentException("Invalid token", e); - } - } - - public Long getUserIdFromToken(String token) { - Claims claims = validateToken(token); - return Long.parseLong(claims.getSubject()); - } -} diff --git a/src/main/java/com/example/RealMatch/global/auth/oauth/KakaoOAuthUserInfo.java b/src/main/java/com/example/RealMatch/global/auth/oauth/KakaoOAuthUserInfo.java deleted file mode 100644 index 8317e14b..00000000 --- a/src/main/java/com/example/RealMatch/global/auth/oauth/KakaoOAuthUserInfo.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.example.RealMatch.global.auth.oauth; - -import java.util.Map; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class KakaoOAuthUserInfo implements OAuthUserInfo { - - private final Map attributes; - - public KakaoOAuthUserInfo(Map attributes) { - this.attributes = attributes; - log.debug("=== Kakao OAuth Attributes ==="); - log.debug("Full attributes: {}", attributes); - } - - @Override - public String getProviderId() { - Object id = attributes.get("id"); - log.debug("Kakao ProviderId: {}", id); - return String.valueOf(id); - } - - @Override - public String getProvider() { - return "kakao"; - } - - @Override - public String getEmail() { - Map kakaoAccount = (Map) attributes.get("kakao_account"); - - if (kakaoAccount == null) { - log.warn("kakao_account is null"); - return "no-email@kakao.com"; - } - - String email = (String) kakaoAccount.get("email"); - log.debug("Kakao Email: {}", email); - - return email != null ? email : "no-email@kakao.com"; - } - - @Override - public String getName() { - Map kakaoAccount = (Map) attributes.get("kakao_account"); - - if (kakaoAccount != null) { - Map profile = (Map) kakaoAccount.get("profile"); - - if (profile != null) { - String nickname = (String) profile.get("nickname"); - log.debug("Kakao Nickname: {}", nickname); - - if (nickname != null && !nickname.isEmpty()) { - return nickname; - } - } - } - - log.warn("Kakao nickname not found, using default"); - return "카카오사용자"; - } -} diff --git a/src/main/java/com/example/RealMatch/global/auth/oauth/NaverOAuthUserInfo.java b/src/main/java/com/example/RealMatch/global/auth/oauth/NaverOAuthUserInfo.java deleted file mode 100644 index 20bcf8bc..00000000 --- a/src/main/java/com/example/RealMatch/global/auth/oauth/NaverOAuthUserInfo.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.RealMatch.global.auth.oauth; - -import java.util.Map; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class NaverOAuthUserInfo implements OAuthUserInfo { - - private final Map attributes; - - public NaverOAuthUserInfo(final Map attributes) { - this.attributes = attributes; - log.debug("=== Naver OAuth Attributes ==="); - log.debug("Full attributes: {}", attributes); - } - - @Override - public String getProviderId() { - final Map response = (Map) attributes.get("response"); - - if (response == null) { - log.warn("Naver response is null"); - return null; - } - - final String id = (String) response.get("id"); - log.debug("Naver ProviderId: {}", id); - return id; - } - - @Override - public String getProvider() { - return "naver"; - } - - @Override - public String getEmail() { - final Map response = (Map) attributes.get("response"); - - if (response == null) { - log.warn("Naver response is null"); - return "no-email@naver.com"; - } - - final String email = (String) response.get("email"); - log.debug("Naver Email: {}", email); - return email != null ? email : "no-email@naver.com"; - } - - @Override - public String getName() { - final Map response = (Map) attributes.get("response"); - - if (response == null) { - log.warn("Naver response is null"); - return "네이버사용자"; - } - - final String name = (String) response.get("name"); - log.debug("Naver Name: {}", name); - return name != null ? name : "네이버사용자"; - } -} diff --git a/src/main/java/com/example/RealMatch/global/auth/oauth/OAuthAttributes.java b/src/main/java/com/example/RealMatch/global/auth/oauth/OAuthAttributes.java deleted file mode 100644 index 6499a064..00000000 --- a/src/main/java/com/example/RealMatch/global/auth/oauth/OAuthAttributes.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.RealMatch.global.auth.oauth; - -import java.util.Map; - -public class OAuthAttributes { - - public static OAuthUserInfo of(final String provider, final Map attributes) { - return switch (provider) { - case "kakao" -> new KakaoOAuthUserInfo(attributes); - case "naver" -> new NaverOAuthUserInfo(attributes); - default -> throw new IllegalArgumentException("Unsupported provider: " + provider); - }; - } -} diff --git a/src/main/java/com/example/RealMatch/global/auth/oauth/OAuthUserInfo.java b/src/main/java/com/example/RealMatch/global/auth/oauth/OAuthUserInfo.java deleted file mode 100644 index 389bd2be..00000000 --- a/src/main/java/com/example/RealMatch/global/auth/oauth/OAuthUserInfo.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.RealMatch.global.auth.oauth; - -public interface OAuthUserInfo { - String getProviderId(); - String getEmail(); - String getName(); - String getProvider(); -} diff --git a/src/main/java/com/example/RealMatch/global/auth/security/SecurityConfig.java b/src/main/java/com/example/RealMatch/global/auth/security/SecurityConfig.java deleted file mode 100644 index 10931910..00000000 --- a/src/main/java/com/example/RealMatch/global/auth/security/SecurityConfig.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.example.RealMatch.global.auth.security; - -import java.util.Arrays; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import com.example.RealMatch.global.auth.service.CustomOAuth2UserService; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Configuration -@RequiredArgsConstructor -@EnableWebSecurity -public class SecurityConfig { - - private final CustomOAuth2UserService customOAuth2UserService; - private final OAuth2SuccessHandler oAuth2SuccessHandler; - - /** - * SecurityFilterChain 설정 - * - CORS 설정 적용 - * - CSRF 비활성화 - * - 세션 상태 Stateless - * - 특정 URL 허용 (/login/**, /oauth2/** 등) - * - OAuth2 로그인 성공/실패 핸들러 지정 - */ - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - log.info("Configuring Security Filter Chain"); - - http - // CORS 설정 - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - // CSRF 비활성화 - .csrf(csrf -> csrf.disable()) - // 세션을 사용하지 않음 - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - // 요청 권한 설정 - .authorizeHttpRequests(auth -> auth - .requestMatchers( - "/", "/error", "/favicon.ico", - "/css/**", "/js/**", "/images/**", - "/login/**", "/oauth2/**" - ).permitAll() - .anyRequest().authenticated() - ) - // OAuth2 로그인 설정 - .oauth2Login(oauth2 -> oauth2 - .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) - .successHandler(oAuth2SuccessHandler) // 성공 핸들러 - .failureHandler((request, response, exception) -> { // 실패 핸들러 - log.error("OAuth2 login failed", exception); - response.sendRedirect("/login/failure?message=" + exception.getMessage()); - }) - ); - - return http.build(); - } - - /** - * CORS 설정 - * - 프론트엔드(React) 주소 허용 - * - 허용 메서드, 헤더, 인증 허용 - */ - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000")); - configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); - configuration.setAllowedHeaders(Arrays.asList("*")); - configuration.setAllowCredentials(true); - configuration.setMaxAge(3600L); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - - return source; - } -} diff --git a/src/main/java/com/example/RealMatch/global/auth/security/SwaggerConfig.java b/src/main/java/com/example/RealMatch/global/auth/security/SwaggerConfig.java deleted file mode 100644 index 25005c48..00000000 --- a/src/main/java/com/example/RealMatch/global/auth/security/SwaggerConfig.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.RealMatch.global.auth.security; - -public class SwaggerConfig { -} diff --git a/src/main/java/com/example/RealMatch/global/auth/service/CustomOAuth2UserService.java b/src/main/java/com/example/RealMatch/global/auth/service/CustomOAuth2UserService.java deleted file mode 100644 index 1826f53b..00000000 --- a/src/main/java/com/example/RealMatch/global/auth/service/CustomOAuth2UserService.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.RealMatch.global.auth.service; - -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.stereotype.Service; - -import com.example.RealMatch.global.auth.details.CustomOAuth2User; -import com.example.RealMatch.global.auth.oauth.OAuthAttributes; -import com.example.RealMatch.global.auth.oauth.OAuthUserInfo; -import com.example.RealMatch.user.domain.entity.User; -import com.example.RealMatch.user.domain.repository.UserRepository; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Service -@RequiredArgsConstructor -public class CustomOAuth2UserService extends DefaultOAuth2UserService { - - private final UserRepository userRepository; - - @Override - public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - - OAuth2User oAuth2User = super.loadUser(userRequest); - String provider = userRequest.getClientRegistration().getRegistrationId(); - - log.info("=== OAuth2 Login Start ==="); - log.info("Provider: {}", provider); - log.debug("OAuth2 attributes: {}", oAuth2User.getAttributes()); - - try { - // Provider별 사용자 정보 파싱 - OAuthUserInfo userInfo = OAuthAttributes.of(provider, oAuth2User.getAttributes()); - - log.info("Parsed user info:"); - log.info(" - Provider: {}", provider); - log.info(" - ProviderId: {}", userInfo.getProviderId()); - log.info(" - Email: {}", userInfo.getEmail()); - log.info(" - Name: {}", userInfo.getName()); - - // DB에서 사용자 조회 또는 생성 - User user = userRepository.findByProviderAndProviderId(provider, userInfo.getProviderId()) - .map(existingUser -> { - log.info("Existing user found: {}", existingUser.getId()); - existingUser.updateProfile(userInfo.getEmail(), userInfo.getName()); - return userRepository.save(existingUser); - }) - .orElseGet(() -> { - log.info("Creating new user"); - User newUser = User.createOAuthUser( - userInfo.getEmail(), - userInfo.getName(), - provider, - userInfo.getProviderId() - ); - return userRepository.save(newUser); - }); - - log.info("User authenticated successfully - ID: {}, Name: {}", - user.getId(), user.getName()); - log.info("=== OAuth2 Login End ==="); - - return new CustomOAuth2User(user, oAuth2User.getAttributes()); - - } catch (Exception e) { - log.error("Error during OAuth2 user processing", e); - throw new OAuth2AuthenticationException("OAuth2 processing failed: " + e.getMessage()); - } - } -} diff --git a/src/main/java/com/example/RealMatch/global/auth/service/UserAccountService.java b/src/main/java/com/example/RealMatch/global/auth/service/UserAccountService.java deleted file mode 100644 index 7abfbfaf..00000000 --- a/src/main/java/com/example/RealMatch/global/auth/service/UserAccountService.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.RealMatch.global.auth.service; - -import java.util.Optional; - -import org.springframework.stereotype.Service; - -import com.example.RealMatch.user.domain.entity.User; -import com.example.RealMatch.user.domain.repository.UserRepository; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class UserAccountService { - - private final UserRepository userRepository; - - public Optional findByProviderAndProviderId(final String provider, - final String providerId) { - return userRepository.findByProviderAndProviderId(provider, providerId); - } - - public User saveOAuthUser(final String email, - final String name, - final String provider, - final String providerId) { - final User user = User.createOAuthUser(email, name, provider, providerId); - return userRepository.save(user); - } -} diff --git a/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java b/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java index 1f558a40..b988a085 100644 --- a/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java +++ b/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java @@ -1,12 +1,12 @@ package com.example.RealMatch.global.config; -import java.util.List; +import java.util.Arrays; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; @@ -14,63 +14,72 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import com.example.RealMatch.global.config.jwt.JwtAuthenticationFilter; -import com.example.RealMatch.global.presentation.advice.CustomAccessDeniedHandler; +import com.example.RealMatch.global.config.oauth.OAuth2SuccessHandler; +import com.example.RealMatch.global.config.service.CustomOAuth2UserService; import com.example.RealMatch.global.presentation.advice.CustomAuthEntryPoint; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Configuration -@EnableWebSecurity @RequiredArgsConstructor +@EnableWebSecurity public class SecurityConfig { - private final JwtAuthenticationFilter jwtAuthenticationFilter; - - private static final String[] PERMIT_ALL_URL_ARRAY = { - "/api/v1/test", - "/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", "/swagger-ui.html" - }; - private static final String[] REQUEST_AUTHENTICATED_ARRAY = { - "/api/v1/test-auth" - }; - - @Value("${cors.allowed-origin}") - private String allowedOrigin; - @Value("${swagger.server-url}") - String swaggerUrl; + private final CustomOAuth2UserService customOAuth2UserService; + private final OAuth2SuccessHandler oAuth2SuccessHandler; + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomAuthEntryPoint customAuthEntryPoint; @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomAuthEntryPoint customAuthEntryPoint, CustomAccessDeniedHandler customAccessDeniedHandler) throws Exception { + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + log.info("Configuring Security Filter Chain"); + http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) - - .exceptionHandling(exception -> exception - .authenticationEntryPoint(customAuthEntryPoint) // 401 - .accessDeniedHandler(customAccessDeniedHandler) // 403 - ) - + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling(exception -> exception.authenticationEntryPoint(customAuthEntryPoint)) .authorizeHttpRequests(auth -> auth - .requestMatchers(REQUEST_AUTHENTICATED_ARRAY).authenticated() - .requestMatchers(PERMIT_ALL_URL_ARRAY).permitAll() - .anyRequest().denyAll() + .requestMatchers( + "/", "/error", "/favicon.ico", + "/css/**", "/js/**", "/images/**", + "/login/**", "/oauth2/**", + "/api/test", + "/api/login/success", + // --- Swagger 관련 경로 추가 --- + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html" + ).permitAll() + .anyRequest().authenticated() ) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + + .oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) + .successHandler(oAuth2SuccessHandler) + .failureHandler((request, response, exception) -> { + log.error("OAuth2 login failed", exception); + response.sendRedirect("/api/test?error=" + exception.getMessage()); + }) + ); + return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(List.of(allowedOrigin, "http://localhost:8080", swaggerUrl)); - configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); - configuration.setAllowedHeaders(List.of("*")); - configuration.setAllowCredentials(true); // 쿠키/인증정보 포함 요청 + configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8080")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } } - diff --git a/src/main/java/com/example/RealMatch/global/config/jwt/JwtAuthenticationFilter.java b/src/main/java/com/example/RealMatch/global/config/jwt/JwtAuthenticationFilter.java index 37964ae8..9d47fa01 100644 --- a/src/main/java/com/example/RealMatch/global/config/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/RealMatch/global/config/jwt/JwtAuthenticationFilter.java @@ -1,6 +1,8 @@ package com.example.RealMatch.global.config.jwt; import java.io.IOException; +import java.util.Arrays; +import java.util.List; import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -20,12 +22,32 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtProvider jwtProvider; + // JWT 검증을 건너뛸 경로들 + private static final List EXCLUDED_PATHS = Arrays.asList( + "/login", + "/oauth2/", + "/login/oauth2/", + "/oauth/callback", + "/api/v1/test", + "/v3/api-docs", + "/swagger-ui", + "/swagger-resources" + ); + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String requestURI = request.getRequestURI(); + + // OAuth2 관련 경로는 JWT 검증 건너뛰기 + if (shouldNotFilter(requestURI)) { + filterChain.doFilter(request, response); + return; + } + String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); if (authHeader == null || !authHeader.startsWith("Bearer ")) { @@ -59,4 +81,8 @@ protected void doFilterInternal(HttpServletRequest request, filterChain.doFilter(request, response); } + private boolean shouldNotFilter(String requestURI) { + return EXCLUDED_PATHS.stream() + .anyMatch(requestURI::startsWith); + } } diff --git a/src/main/java/com/example/RealMatch/global/auth/details/CustomOAuth2User.java b/src/main/java/com/example/RealMatch/global/config/oauth/CustomOAuth2User.java similarity index 84% rename from src/main/java/com/example/RealMatch/global/auth/details/CustomOAuth2User.java rename to src/main/java/com/example/RealMatch/global/config/oauth/CustomOAuth2User.java index a58b954f..d3c0c56c 100644 --- a/src/main/java/com/example/RealMatch/global/auth/details/CustomOAuth2User.java +++ b/src/main/java/com/example/RealMatch/global/config/oauth/CustomOAuth2User.java @@ -1,4 +1,4 @@ -package com.example.RealMatch.global.auth.details; +package com.example.RealMatch.global.config.oauth; import java.util.Collection; import java.util.Collections; @@ -7,7 +7,7 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.user.OAuth2User; -import com.example.RealMatch.user.domain.entity.User; +import com.example.RealMatch.match.domain.entity.User; import lombok.Getter; @@ -34,6 +34,6 @@ public Collection getAuthorities() { @Override public String getName() { - return user.getName(); + return user.getProviderId(); } } diff --git a/src/main/java/com/example/RealMatch/global/auth/security/OAuth2SuccessHandler.java b/src/main/java/com/example/RealMatch/global/config/oauth/OAuth2SuccessHandler.java similarity index 56% rename from src/main/java/com/example/RealMatch/global/auth/security/OAuth2SuccessHandler.java rename to src/main/java/com/example/RealMatch/global/config/oauth/OAuth2SuccessHandler.java index 8231a0b8..8ca2992e 100644 --- a/src/main/java/com/example/RealMatch/global/auth/security/OAuth2SuccessHandler.java +++ b/src/main/java/com/example/RealMatch/global/config/oauth/OAuth2SuccessHandler.java @@ -1,16 +1,14 @@ -package com.example.RealMatch.global.auth.security; +package com.example.RealMatch.global.config.oauth; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; -import com.example.RealMatch.global.auth.details.CustomOAuth2User; -import com.example.RealMatch.global.auth.jwt.JwtProvider; +import com.example.RealMatch.global.config.jwt.JwtProvider; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -24,10 +22,6 @@ public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { private final JwtProvider jwtProvider; - // 테스트용 - @Value("${app.frontend.url:http://localhost:8080}") - private String frontendUrl; - @Override public void onAuthenticationSuccess( HttpServletRequest request, @@ -38,26 +32,29 @@ public void onAuthenticationSuccess( try { CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); - log.info("OAuth2 authentication successful for user: {}", oAuth2User.getUser().getId()); - - String accessToken = jwtProvider.createAccessToken(oAuth2User.getUser()); - String refreshToken = jwtProvider.createRefreshToken(oAuth2User.getUser()); - - log.debug("Tokens created - Access token length: {}", accessToken.length()); - - String redirectUrl = String.format("%s/login/success?accessToken=%s&refreshToken=%s", - frontendUrl, + // 토큰 생성 + String accessToken = jwtProvider.createAccessToken( + oAuth2User.getUser().getId(), + oAuth2User.getUser().getProvider(), + oAuth2User.getUser().getRole()); + String refreshToken = jwtProvider.createRefreshToken( + oAuth2User.getUser().getId(), + oAuth2User.getUser().getProvider(), + oAuth2User.getUser().getRole()); + + // 테스트를 위해 백엔드 컨트롤러 주소로 직접 리다이렉트 + // 실제 연동 시 프론트엔드 서버 주소와 토큰을 받을 경로를 지정 + String redirectUrl = String.format("http://localhost:8080/api/login/success?accessToken=%s&refreshToken=%s", URLEncoder.encode(accessToken, StandardCharsets.UTF_8), URLEncoder.encode(refreshToken, StandardCharsets.UTF_8) ); - log.info("Redirecting to: {}", frontendUrl + "/login/success"); + log.info("Redirecting to: {}", redirectUrl); response.sendRedirect(redirectUrl); } catch (Exception e) { log.error("Error during OAuth2 success handling", e); - response.sendRedirect(frontendUrl + "/login/error?message=" + - URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8)); + response.sendRedirect("http://localhost:8080/api/test?error=" + URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8)); } } } diff --git a/src/main/java/com/example/RealMatch/global/config/service/CustomOAuth2UserService.java b/src/main/java/com/example/RealMatch/global/config/service/CustomOAuth2UserService.java new file mode 100644 index 00000000..beb33a26 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/config/service/CustomOAuth2UserService.java @@ -0,0 +1,70 @@ +package com.example.RealMatch.global.config.service; + +import java.util.Map; + +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.RealMatch.global.config.oauth.CustomOAuth2User; +import com.example.RealMatch.match.domain.entity.User; +import com.example.RealMatch.match.domain.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + private final UserRepository userRepository; + + @Override + @Transactional + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + //xs 기본 loadUser 호출 + OAuth2User oAuth2User = super.loadUser(userRequest); + + // 정보 추출 + String provider = userRequest.getClientRegistration().getRegistrationId(); // "kakao" + Map attributes = oAuth2User.getAttributes(); + + // 카카오는 id가 Long으로 오기 때문에 String.valueOf로 안전하게 변환 + String providerId = String.valueOf(attributes.get("id")); + + Map kakaoAccount = (Map) attributes.get("kakao_account"); + String email = (kakaoAccount != null) ? (String) kakaoAccount.get("email") : null; + + String name = null; + if (kakaoAccount != null) { + Map profile = (Map) kakaoAccount.get("profile"); + if (profile != null) { + name = (String) profile.get("nickname"); + } + } + + // DB 저장 및 업데이트 + String finalName = name; + User user = userRepository.findByProviderIdAndProvider(providerId, provider) + .orElseGet(() -> { + User newUser = User.builder() + .providerId(providerId) + .provider(provider) + .email(email) + .name(finalName) + .role("USER") + .build(); + return userRepository.save(newUser); + }); + + if (email != null && !email.equals(user.getEmail())) { + user.updateProfile(email, name); + userRepository.save(user); + } + + // CustomOAuth2User 반환 + return new CustomOAuth2User(user, attributes); + } +} diff --git a/src/main/java/com/example/RealMatch/global/controller/TestController.java b/src/main/java/com/example/RealMatch/global/controller/TestController.java index 3ee96566..c39b492f 100644 --- a/src/main/java/com/example/RealMatch/global/controller/TestController.java +++ b/src/main/java/com/example/RealMatch/global/controller/TestController.java @@ -1,11 +1,15 @@ package com.example.RealMatch.global.controller; +import java.util.Map; + import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.example.RealMatch.global.config.jwt.CustomUserDetails; +import com.example.RealMatch.global.config.jwt.JwtProvider; import com.example.RealMatch.global.presentation.CustomResponse; import com.example.RealMatch.global.presentation.code.GeneralSuccessCode; @@ -19,6 +23,12 @@ @RequestMapping("/api") public class TestController { + private final JwtProvider jwtProvider; + + public TestController(JwtProvider jwtProvider) { + this.jwtProvider = jwtProvider; + } + @Operation(summary = "api 테스트 확인", description = """ 테스트용 api입니다. @@ -52,7 +62,11 @@ public CustomResponse testAuth( String response = "Hello from Spring Boot 👋"; return CustomResponse.onSuccess(GeneralSuccessCode.GOOD_REQUEST, response); } - - + @GetMapping("/login/success") + public CustomResponse> loginSuccess( + @RequestParam("accessToken") String accessToken, + @RequestParam("refreshToken") String refreshToken + ) { + return CustomResponse.onSuccess(GeneralSuccessCode.GOOD_REQUEST, Map.of("accessToken", accessToken, "refreshToken", refreshToken)); + } } - diff --git a/src/main/java/com/example/RealMatch/match/domain/entity/User.java b/src/main/java/com/example/RealMatch/match/domain/entity/User.java new file mode 100644 index 00000000..fbfbc773 --- /dev/null +++ b/src/main/java/com/example/RealMatch/match/domain/entity/User.java @@ -0,0 +1,48 @@ +package com.example.RealMatch.match.domain.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "users") +@Getter +@NoArgsConstructor +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String providerId; // 카카오 고유 ID + + @Column(nullable = false) + private String provider; // kakao + + private String email; + private String name; + + @Column(nullable = false) + private String role = "USER"; + + @Builder + public User(String providerId, String provider, String email, String name, String role) { + this.providerId = providerId; + this.provider = provider; + this.email = email; + this.name = name; + this.role = role != null ? role : "USER"; + } + + public void updateProfile(String email, String name) { + this.email = email; + this.name = name; + } +} diff --git a/src/main/java/com/example/RealMatch/match/domain/repository/UserRepository.java b/src/main/java/com/example/RealMatch/match/domain/repository/UserRepository.java new file mode 100644 index 00000000..d3ab10da --- /dev/null +++ b/src/main/java/com/example/RealMatch/match/domain/repository/UserRepository.java @@ -0,0 +1,11 @@ +package com.example.RealMatch.match.domain.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.example.RealMatch.match.domain.entity.User; + +public interface UserRepository extends JpaRepository { + Optional findByProviderIdAndProvider(String providerId, String provider); +} diff --git a/src/main/java/com/example/RealMatch/user/application/service/UserService.java b/src/main/java/com/example/RealMatch/user/application/service/UserService.java deleted file mode 100644 index ade91fc4..00000000 --- a/src/main/java/com/example/RealMatch/user/application/service/UserService.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.RealMatch.user.application.service; - -import java.util.Optional; - -import com.example.RealMatch.user.domain.entity.User; - -public interface UserService { - - Optional findByProviderAndProviderId(String provider, String providerId); - - User saveOAuthUser(String email, String nickname, String provider, String providerId); -} diff --git a/src/main/java/com/example/RealMatch/user/application/service/UserServiceImpl.java b/src/main/java/com/example/RealMatch/user/application/service/UserServiceImpl.java deleted file mode 100644 index e5c38364..00000000 --- a/src/main/java/com/example/RealMatch/user/application/service/UserServiceImpl.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.RealMatch.user.application.service; - -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -import org.springframework.stereotype.Service; - -import com.example.RealMatch.user.domain.entity.User; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Service -public class UserServiceImpl implements UserService { - - private final Map memoryStore = new ConcurrentHashMap<>(); - - @Override - public Optional findByProviderAndProviderId(final String provider, final String providerId) { - final String key = provider + ":" + providerId; - final User user = memoryStore.get(key); - - if (user != null) { - log.info("User found in memory store: {} ({})", user.getName(), user.getEmail()); - } else { - log.info("User not found in memory store for key: {}", key); - } - - return Optional.ofNullable(user); - } - - @Override - public User saveOAuthUser(final String email, - final String nickname, - final String provider, - final String providerId) { - final String key = provider + ":" + providerId; - - final User existingUser = memoryStore.get(key); - if (existingUser != null) { - log.info("Updating existing user: {}", existingUser.getId()); - existingUser.updateProfile(email, nickname); - memoryStore.put(key, existingUser); - return existingUser; - } - - final User user = User.createOAuthUser(email, nickname, provider, providerId); - memoryStore.put(key, user); - - log.info("User saved in memory: {} / Email: {} / ID: {}", - user.getName(), user.getEmail(), user.getId()); - log.info("Total users in memory: {}", memoryStore.size()); - - return user; - } -} diff --git a/src/main/java/com/example/RealMatch/user/domain/entity/User.java b/src/main/java/com/example/RealMatch/user/domain/entity/User.java deleted file mode 100644 index d4e85fdb..00000000 --- a/src/main/java/com/example/RealMatch/user/domain/entity/User.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.RealMatch.user.domain.entity; - -import java.time.LocalDateTime; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -@Entity -@Table(name = "users") -public class User { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String email; - private String name; - private String provider; - private String providerId; - - @Column(name = "created_at") - private LocalDateTime createdAt; - - @Column(name = "updated_at") - private LocalDateTime updatedAt; - - public static User createOAuthUser(String email, String name, String provider, String providerId) { - User user = new User(); - user.email = email; - user.name = name; - user.provider = provider; - user.providerId = providerId; - user.createdAt = LocalDateTime.now(); - user.updatedAt = LocalDateTime.now(); - return user; - } - - public void updateProfile(String email, String name) { - this.email = email; - this.name = name; - this.updatedAt = LocalDateTime.now(); - } -} diff --git a/src/main/java/com/example/RealMatch/user/domain/repository/UserRepository.java b/src/main/java/com/example/RealMatch/user/domain/repository/UserRepository.java deleted file mode 100644 index 03275cde..00000000 --- a/src/main/java/com/example/RealMatch/user/domain/repository/UserRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.RealMatch.user.domain.repository; - -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; - -import com.example.RealMatch.user.domain.entity.User; - -public interface UserRepository extends JpaRepository { - - Optional findByProviderAndProviderId(String provider, String providerId); -} diff --git a/src/main/resources/static/img.png b/src/main/resources/static/img.png deleted file mode 100644 index 4b8d1bf58a741ab2e01873cd86d64470b8c63d57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57848 zcmcG#cU+Un7dN_!!tN@tiUQKIg2=jrCJ;JNK~OuB>J@9ppME+h`_oScT7DK6)jarPmGaY1|M}^v^~KxKUaJK$&FZ${ zY#tH%>1PV%sLzGfJo!6a0Z|0WynZQ=8B|wSyz5Whs%LIf%a^B?Lo3~{{7gsM+`1ff zxs_*g=gwcw1<17;a00zDJzDMj`Ksl@CQ?D5`TmbYj!NR5ki(}(?X6>;znJ$2(F=zc z3T0_m*PRQWC;kc4`3B`@^J06ruPR1Y-U$P@1H&|a&eLRUUqNl@IbTPfTOjcRkx2;p z0Q^`PWnz5g)r5JRR8fP6oJO~M%(eYWTuTdP3CdnMO?N4FuA3zb64OV)c}L)JQ2}YX zh6C(VLGqoZ)~slM-fMBzm0s_vw3{CvCBPTR@nV0zS$NTXo^=AE5&f>7k=VI zNgXk}|BKQQS||MRR_crSRaSE0E~*+rqdZ>B7Doi92L=A$1%cWS(Tlu0_&(5KZ0ZCM z91=MNT{XGTT+sPp!1EyW!-lS6Jx3VS9_i!jaFhwG0XRvp-*0C_EZk<7cCKGX7`kCj zW58J}A7D#}$+47|tZ&J@ak6u9RC9U|P?@mx9e+NrbPKxtUTIuKrm8%uaOd*>)CnQn z+43;U_@w~9DC62&{NhQ1aybfHdx&vMF_mWMqeMQ&4kRCFJD*e2^)g!)uVh*K^Kl~$ zS;XVgt5LM}L#1t^iQJ575VRkn_$vA)1So#q{e|!<`}nXbwv&4D;#YQ8u_WH0Hs{{L1p%O9A+vPbPE%W|;2_&3mpU+o^Gch4ke`D6V;ovq5m%?Bmd+Mx1B;ZreEM zHL5=Uf~O`VGyoa}Z@5!A>Fh=Gs`}E!XuS*!Un8B{wq0 z@quMnCn{oBcvXU0#abj}*P~xg4V@`k z__jV)PV#uYC;=0odWMRV@W|+0yAkpVzQ{`aRhWqhw24Nm36u5VW-p-WJXDp6{DSXutL+6&aQVX>E+jreWa=a z$5xW8X1e*xCze5F+Ak$uOYhD%$vz&~4T-`8+20;hsGFI*pSzPd`~G0Z`*Xw|-^>WY z#q(2+eZAg<^EJkd(EgD<`BaVqn=uL{`JtXn|o zjyUOK^UvW2<=u_y*qyIiJcGP%HynbE>8aCS%BMxt=5p#<3Rm<7AjHMDm!%#$T!0^s zL)Tn3BaeMp9;@G-o9zZUZq%V&Fbiovj@4$F^eu#cU!t3~$g^b>3GO37?p>sTr0?rg zb{nzqQwE2Gv(haM%8Od;i`^1%@<% z)LqydFO-MXyt7gr?V&uw=-AXG1mjfTV~&b+_HV3PGX|@OcZcoR3|vBXjn?+${4SL& zbSc39rfWimRcaX*{l{E|g*-Q}$4eBFw^3m+#h zM$Q(TG^GB-Ky%!Rx#e4YnJm{h?b3Ml_U;kmv~#?&uHUmu0t&GACcPVk?3m^M(XBjK zD6I_Hl=0Jea@_Tezho_BQ@>g=Irth5t){>_SfxX=d`8zI+ie@JGfpmla-QAkfpSgY zNm+zB?jB-%JFzU!(e_An8*ul6Wy84k#8{WRONEs?dPWV?zkoaJHSnCyZSeiL4=~16 zT!jp|CC~0lh4&yxxdUEf6mx& zk4iCg4schjKEUb?m>6W=ssbxvSu-sy4m?+p z@XqTLLzYM*Qf>Yo{Cl*%uA`-mcY_8$=KZj79U0Vs;>O5)@(R&cRlqQ#2Q|vr-``Iz zTCv__h7dzYt1#WVur_6V_bxf#&^;w4VkUn25HX9H{+Lpt1YUn%tdjFbTs_wBM&fZZ z_35;W-j$x6`H&k`Tisk;)@?U-eFX;^jC$xe@Vs4_wNH;PjgmzAnba`k=$&w<;2c^p z8B@A-?3#(){t__Or^BcM^<`AG?GXPO{+8PuB+oxYJ4{YJ@v3OFfFT%di>>EhY?{XW z;UViAqg=4JM`18*sMR~1oOI5Ok*4^+j&%%`^nfxsxmeXd@S5LibLT zZM!~&CInci<_15M0xoBaGhPRz){Tz-Mw_5}+c%$C-|D&7ml7p9=a0j>f&Iq~wK9h_ z+NP>62j@YpBMrMELPZDbwd#`u1G^ET^^Kw*QTjiL#`257`oZV5LZz?YUvx*m1a0W8 z$6_)|sUUUX%k2CY4bxb8`pbMLKk=_C{w+meyTao@3-UQ$dDmQi%NDsSx@<20xc=)g zn*Lq^4`;k8`S*3iyk2Yq-6*wmLq&C0rD__mFmfC{wJih@D{IVc=-+s3-0f+rJ2Oy= zQ@e?ajy>mJia(5kYbX-bO55ZOnG9<_L!k`AWIhvK?277bOWe@2?l6Xr-IUQ}DhN(z zE|ZsS&B!m%6Pb?Q=?Vy#Dheh!Y>Pnj>?p&tR(CZ3RwvRsqa+RS#WjO<5_`@)274#J z^4v1{kdnFsiIXa&l78Puca9D2a{roHa9pj*2x>p(Y3)?v{vZam%Su~-J!-emc?1rX ze!}&W@J0cSwSv93P}<#X%m1}4hE3|(V@515XJcsQeYb#XeQs9}nS9k;w`(S;%<+#I z^N&swATXfmmg5|L& zJLLGYJR>!+Cqd87>G`iJ|8dD6syc5h@Zn<0$}!I@E&MWN`*zuaYeKM?6?H-cP!I%ql^~FLGV~+;NP_B=JoQ*rCMZMut%UCHGb>*N92G zd9Ocp>Yc%<6tb{Lg^gyJ;I8zoyq5HmWTu*m*6A3#>pYvCr&tv9iV8(rj#pa3-K`^! zJ}Hn%tWf%p-J(DifE%vA`AY?3$RFPplI<0pKEMA;)JOQSx*Pm@O|g9_$cS*Tey?|6 zuP^1Lfx6Hpy^(8d)OOO2fxolX>Bka6S9$M?4Sqatf4d&9@pM|%U77~$e zQr4(7>b8+(P#E@f?b*b(Q1Xi|oBZ{CahXrqWkqs{J$iP|_O;V9CBv8yBB`(6!YJYD zJw)&+@WNm~oM?f^{u%}k7fZ5kl~KZ()nUE2UXBa;Wsv#f)C2=ODM<5AkztM>QIdGBzDW24GyTq+3V zHFl=1axkfzIVbjnr`m`1E{dNu<1F%wdy!5?ak9DEJMjwqS?*z9r)28C>raA zuizHM($Ks`bYP<5(30;H6K#}hI9aCzjSzb0sk48~afFY=(mJ*FzWX2_f#5}VZVW># z$_A3<7IQ!|2s9_+@VhLJ`sCv(ON7E#L|Yl zTCPq8^}DTfE>r65+(^ z4S`S6!&>z=hLifDR&jcIEDvwk|MjkyEYuNGlp*wQb;sCLSu2v>#(2Z_@c?*3z|vk1 zQ!v^o{M3JOAul&BWRhYo`U)-Bjtf1QDeOI!hL%i1-0cGuER`QO0)eoPI2h3bLI`er zpLDq`qFmYkvY%=$)Mvn$-8Im$EXNtQ49?$NAAL?2ft{>u{R|U462})|KL-U3R6J>p zs~0H*f6|wA64y^9?Xrvy{kS25Z3pWLFx+RZ&C~y_O6d4+71CAK7?*AOm^2H@$a{gC zcK9IwtP<*G7CQURZIeVAaHT|~W6GaXy05wg zBB|$%ykz=Mjw*|31Ov+-y&w*-GyFqa&gN?VZ6>~fH!#JGj~g3opapmQ(8Kj{!Y>GT z{)rnjP^jpsjvy4yzxU&1VCR2Z20v7A3x5o~YKUiMBh&`)^qJEmi9Z_>zy3xQ`P8(* zg#8SDdbfhDm!h4nf9v%p6UM#&jQsq+zsKclQ9gLt-90ffy=`ocn66I&$)$E#HuR&m zThEsJ=j&x8YLms%Waw?6A=Fkg9pDF~mL~JYgStgX z0Kke@`jeG(S%xgRH;1noM*qPgM{JBUmXs8fAaG3|=C?7mNTz5p^f)o9HZfvqEjPeP;o1c${L7^pB zgmA_oT>)K8K+;JG(Jm4u;*|!G_)HT z5g?hd_udcsZ<&2IE(HYrx8&mg8K8ZDSxpl@Ja%F`G3}KUkbDyN?}HGH9`3p(8rpwL zc@pRU6j5?Yv=9`^Ai*<4q%o~Z)BElJpC5|U~t)k)a$jyIDpR^&?c^3v_=#{=ZK9%(7#>3vhtb>E8AlY*8AmGu~ zBZM_oSBv25amWF-g^5~`@!h|zLy1PvTG|~q+-jkuU{wa%>YxLDz>AdH#Df?QQ6+%g^#2IH z)@RYV>R^U)ulqr$UisFO*8s4VvUV1+5=}mSt2HZcG&wfC8`POLw*6dtqF!IN`mTr# zh{63{_5_%<{9fK6EtZkNS4}9pYFO`%CBvfG*=VN+1w!z&>Ey`y{_I(7taUXA8K zLen+7OO#nml}e2XyRFxSq;kGv-qK)7_%T(zV`vEmmg(hXSAyWnMB3LOuo=9Y$+V-K zPUAAt>gw>*^^i^oXIO8s6SRDbx`6|+p^bXACooZPSlrvfJ36B2U=%LfAW>&C_S6as3$mRQmQ1OBnRqio#qn{RDn`5|DgR6 zNdY8lzSDu0fm7!2(H3UvDD4j6$fT6h}G1- zWFQWE?;l`Cb4y07ib1ssLft{w7fX2UO#+CCb1rM}BcVq>Y}zL$5M%Jp?JBX=rc%i0 zwnm>BX24bz>PK*Q3H|9s3yNR<7==zu|Nlc`=snEkU$7&Jzz~wMV4NIppPG>W6D=hM zn9WeIt;buIL^r+UaRgJIN;XdhPJpeOOg-C$F#Kly{V`aB`1D4ihX6>SgF*vt- zD$XS*seacMmAsThdn3v@>(PARtQDgEjr7WUW1??MX&TWE66+~u0sV5=FHce_(&@cY zb|^vG*8PmAMG?GZEvt*SIU$o4@@wO_tkvJwb=LOceN`>~Xy09R8_9D2M_?wkVUNy6Yw5EgnP2~bY}J05uCsDh_9A&yF4`l=TSk5AK!kTaLp1fVp>N0 zycy;`Kz?`*X)q$cl6A=xSjEC8uj_M@fEBHI}wgQ~#)!8>JaRH}r{nEwQ_X4Uv>8V&10EGjOduDWj! z%$<*h+)U%ULA<2xlB23>IHLN>vm?3|Ody!;XTKhUiqox8(cs^N-rCT=8+y$Is7!l< zcuaPuIfs_{;$m+m^PiLF0%NM>M_$Wrk#H?4_EyqB$WKvSOsp(t9CMg$tq3b2pS`hL zZQK{egJ=^=(3xLdRp9!0q0lLjFFd%gH9Jv5_78*@x{)8Npnj&fH&wL1wK5BdR0Q8j z#@~V@HtE3K-H1@IY_<%d8qLf#<`BN>w$DQwKV^Vyo(9I?`r%FJNO#Ks+oCZ{U;`6d z;5!;Lp&Gk+T0U&4c>lQ+r(zVbXDvyw@f9q!;c7??2~(Twe((%mN#h2BN9;u&jfwCq zbfBVYPc6d80PHd=_^j zKy%4tR^F2kYWqpFdFp>8G!{vh`J$E}D>7N!eZGT8*~6W`^AsqLm{>+PoXkn3Iwe{xdBZaR{bEa*ZEh z&tI%*s9qV0xkdkD39n2dv^XDq5N-(ST4|Vrf~M|YR|cjYrpci@b0J6BehS1m-n|== z!_XO^NbPl{d}DBEA5JMK4a00Puh_yydYbAipHjCVmqcM*?hZiaH0?BqH-5p(kBkP9 z2GTm-{Sh+|@8L#cW{Yv;%Oigd5s#ip@5rL7RSmmq->SMEE9k{$Itpavon)w%;U*+5 zpH0bH@ioCG>ZD579p*wMf#Q*dZZ%v#UF~ay`hYA(J&1k5j1sK;Aoswu5%l{!w`X}1 zYc~?|!Yi@f|LyJe!<3OOyo1LLPD@+k`g|5cOr$4XVndd>!&MKQw6^4!3 z1JS8eRJz1rQmF>DX$zMiMSUqpQFF6*))xH)Js&_$~-GK!s8voLySjqq)O=iejyE*qM*tmQ+F(-!&#v(e1}B2*E%z4BiS8+{lz zaY`)8Z$DfK(D^A~dtFHc#4_Gf0<`pT+J#0tm|0H3rwl}2!NP1p=y02FPIq(yyreMr z-c)DH60d)(f$;{7IrnXBdugAfT_^l5+2}QF1WCYr4W-HE7?r9( zP2ROAqHiKLgETk*&mMlN6p37k(ci*9rX4}4&eckcZmLqOKt@~X{jc97H#kS%0QvAH} z$a!U^V}A@MUjbe2-?lh;^lfeqsA%`R+X8i&w_&i@KuiuZSDA8vZM2MU6UpFF_*fx} zipXc-rMbS^Z0Ob-#-)`l4R#O0{Km>W@*d_<0eteMPw%j18*kB4403kGJJs{u8N#R2U@jGy1!m_R_57TC6T)5!y&q1{7wER z)AY+)uo7iKaL(2vJn$cPz`uciv3)E6G!l}h8(V55t`9i-ey-X{8e1+~eU+uEtHpST zNVftc&p-Z;z#6Hm%)ZoKT9dSu1hSA)l39^6T(nZ`Tu#=CBgG&@pi5;$ACi6&Hu1t? zmNyv6i&UHiLJHk*XAx=di4YmGo)qdRR_)-nOn=9{>>~m!S)3uk+%&#xU+q2eS{iY* zN}9Yn$|hOdOpUoIa_ya~{T(dP3%1SkV+d}z(^1MHw}mgkdKxRXW2xa4wtyA-N74RZ zhuGr_Iv}$Ar4YzlPuyssq>1whszAy{$@N4LQUs+i-b_~=W65T-o^tuQVu}Dlum$^q_fx78 zR+0;e5+&O}#Q0i33&rHpb!NrR#&pw(;>$q1$g2?__EoV<-ga!B>kNM_zEU5rVrsOK zI`PS+{`xvW;WyD~ief+_&3$suEDLw7(zfc@YdHA;=<9)D7`oO_f%>WO z-i^Ngh}YRaXEcAlis~HsS=2E0zC{_#sWL$#bb;x~w6ZNB%{p!haY4&iDx_Q9@ zcP}&Cr{8O;k3FF!=^kKG&7U8R=bn{;nJv3!DCp|&Kb3K?fXLueTjRBZdd;=84PQl0 z`b%kQvMGJwk9p0}MV@Q_QEbVH!0zaxQvWLO2;N`hCxV#O{%8KSy2hMyAgz$eC57`9 z2e`%32xchnRu=XahNGZNSZM+R$b$n{cz4Y!xU1=oZI0-|qR~DdlWXD5={v4&WWJTM zyA8w=UefPoQ*8S7?e+cJ&7q=!>Dg|-tCai9gPg`nA+U8Lgpbp%5e1Bgib$~<}^vK zIEjXFy*xDP|GL$D#LLZJ!Sd~Z{Qb17SMV@*#>mvpdmEuw| zCb4{tC!cBTDKT72;Tx(6_M156h4uneXwE~ezur@}$A}s$^6{K6^Xu?ZV_NF$kZvpa zjWp|)8NYQu(tRac8sVkLu}`L5IWxBSYMSKZLDW5xe?eU8=~Aodr*f_}@&?{DO0&$F z8ejU(B=T~6VVSI^96>*C0SaPUDU(TpL0e%<^W87KQ>Yz5_)eAg)TCfF(CEH!M`+o! zyWLRtZJ#bYR?BHyN5h&E1!J~RMZCUOQ{|S1-y7+kytIH%`*RvbQD7mje@Z7Y26>;| zC)1oJCnPo}2vI4&M?4E(v)k#fvv4BMzrimgvd`sd zo|8-0LU0H^LeGp4V=0OPjrg(%1oMhcvWP?VtXI3G{2;u^qUnU6qmbU5PzsZ#c^KiG?L-A%aaFlAV^EABW~WCjaoXbhdzof$ zA)mcg4sLDSUAqt9NGVtfmT1k=i9d$cEWS13;%M#z-@JC32+b}nUsu;M%v4sR*V6!# zg8mtETr-CZUfpLUHh+^93fhHVuxG<}rWWppCGI|&GMkC<-N9olCY96U=QUlLlV1UZ zKPYQpp_7zx;iPBNriNSj;XE-N8+zc?wD4o@HDE{m&LKklbp5M9DP%-hZU|Dh)vz5) zCMtCd#YIB&dPUZt_PDe5=InX3Wz2FmnwPDupk^tXe8(uv&F7%!A%4jBuMNipEhZ?qkga z$uQK)qp2g_Ki;oR(AyI4;czE(P)j*eGeUt?GbVm+V@>pnKP+K{37j2|CGBf_z^{c6~ zRz&OVrT8X_vBQ}LgN&-Nces&Ot_siPny^`-*bE!{ zid1WQIBylVp(iqGN)?(~W6Y3qXt6C4WcKO6u?OKm3Bu@2nXu6h?3WXtjJiP*)s-*7 zf9E|N04$X_*OBbQL{LE|@EiWZ0rD-Vx~(q8=%+iiDWdImuG|j%(v6$kIEBu6 zVEC_u)00gx`l<{NQQ!TL=0Lp~!^f#*-d2%I_YJYs)HVfV$_KDCss}_S7Yd@MvL1xe zPW{^0_q}k(Bc6ZZYtkxh?^CA!pJB1XR#WKWg#nAff{yoif=Nfr`evVO$5X=Um7&+5 zZubPDo=~ZbP54^OJ@&+xjQX$)B-vR8aO2-;>Gq3WbQ+83=?zzfBVjFl1DiqmiBl(@ z$gc5jA8ojN+oh@oeQqhcXrgTV)U6t?`|xReKU@R|w&UD5fu@_6ktNErztQDJ zU-5lR9g5;NRgS<;bwETrq+OdiCV+jv_ z)qg}=&6p|=ow)Tv7BSf$i8(DLuK5z$u1M!P}4rG_l+1~ZJiZQtsLj?IjeI#YE zi6Q+eK#4rH%~JmnpXS5Bz~1d2v(UV)Cy=N#Ta}o<VMyDTch@hCISdtbs*Xmy9$yJ015F<~*kPBAK6(T-DEQOCH=9lsizYpwF)6~REH zkBxDw`0_p;*(dg&|No~bpK+H>r+-J zt^Gd^ViDV4CBuvxcDppR@9VHk6Zd=J14ABh+*(v0;1QwH0U|c@l-{SG*a+ z$$i1mad>oQ*7(_BYbJbR7^b)aKukYek|)+2Sjn1j`!Qjjg{|?Z&{;Cq1%;KnZ9q~= zJ6qJ7ZPw1ci|18NrCQ#rjDw$O4{}hW$1(s>!r8>hnO4Kw+P}mgmq`=#n{k$&C53_21>LHJ0Rw!zANI#SeEAEXzfetZl0+fG6UJd5zgY>^=6|B^Cq78CU9fO5qI4S6`?@% zK;USSDUE`h2Z>kV8z%JT^BZfhK>~G2CN&0SK~bGHca4(G6Eszxgij2nnobU8i+993 zvp&8sNX}TeVM)|0NG(?$^V(o>zxnn{kaMuX+0$ypPZo!NtRG43Kh4#1yy1$?vJz}J zB(8ehRd=}DBXb|e0(HezP1oaWs%`)nF8 zE4VC39w7Tl!B|R^#hp{5l6-ULhCzy7g8=+3xf@zonfn~gGgs~$vgM3t6fYb=`ncZ} zvHKS^t9waUe2v7%h!RiMPy}XsLExi^x)m~ejqeL=l#J~2+E{74RP?C4V42{PXwb5C zrjCeJlvz(hmZY~p?AEM!oq)MlpLs0~9d0g+^W;{}3&b!Ji;W|Kze%tb(;O9DhNPjr zhzjWTb8q@dKG2+VyXZ1!AM9@~ZuhHMwU+T2!5W^!bS1 zD}~vac2{;LxsV-$>rFtvnj=J4OHpJ}3(v=`{>+*dIn#!VR7lx%+7DMTijO->3(z?n z+x7=gf>E*XXh{n|wx}t$1!%AEtFw<|8TBH!Ll2xM2G?Ne&-K(vfBpsX`vZl4th^BV zKI+7XC}s@I3$%4xA+}itDAj?frr-v-w6`9E+0Wap&sidd>nFYU-?e$}2mNQyx;cL< zvpjfHAp6b|he$G=oWUy}-V!l>H1#!|{}L2-z*t0h%3tBJ4#i+*ge^-hD8cJ@BLc=fq5BQ6P_{Qb8i0gU6u= z90E27{=X3Mr90YET9yh|ziSzS^fBiz9PiCBsOXNWj|>E6Tj^vO{5(v6=%g-S#-VPDS_liuJn2oz(39_wG=(4d5c9qQQfk%gyC%1#tr#r)+KDY?Wk(y?rWNzr8;iCu~y;|nh}x*1b%QP(+Sv+=5e6ZmUsOK@LM!yepodwZPU z=|5u2gorWZAIw)GFIU}q_Clt^I=VK=3QZpn1|fEbnEyZ!M8SaM7L6;Y5%s| z+7Ij4Gfu(ljOOeDTjI}YaDAIkRwfYDocKF_lX*tRbdD__U(8D$Q0zxs4>3qkk^#8i zdxXXD2czsdD+ae?S7AZ6unVo_*Y}@OiW7OGTgmaf(KN4nWoF5$;E1z3f5x-*U7Ct? zg!?-H*1rxRSg?eiY8UQqt-08Ow<T&AhXVVp!@WO>=s4K}7zJ)Y>Vop-B&MxGinYk~aSMP# zW;>!rsK2$6)F5W3D6{8>%VzeU>CE46UHLHWouCfBb9P6yh1b7OWclpfC*>^H!3v$@wf9wG$CA=6U`a&XpN`pHiUrJwe7th^l_p_+`}I=uLIC2J=yF-L zl_(BmnX))GE_A&iE=s-d6Qo1&oQ`{avWHiEXiUng)*^DJ=#AJBi$UjGvj^rA%F=EQ z5!ycLwG}1ljR$N$4*2z*N-=NT<5fhoo)&Hwjn?6*#I4e&?DFRV-%EedpwHX6NFAK( ze<|zrGx$V%g(gmq^U4?ym~<(wCxYC_VL=oNDW}W4-KgYPTTs#_3zGr zNqPN>f4*cL1octzyxX;=r+#(`26el&Y@{qu=itmU>T(i6UU2%wg^i8v$hqyx9=W{u z%x^2NFAdymS@jo`9xz$?-RN1Fh%*hJI*qm9-5qG#X|-+6yY2nsgMOS@&ogfu!S_@h zf6N||-eKle~H%Ux(5)s{JcF=fAr7t*^5bR&!ibiVuwX)nn$ zYCW{*fY6~l#dPUj&mBL+&vZm#>GfGxwj?o&+t)dmNsQe$j!jAxeFUq@QtjewcpylKhkWb zCv0E4ANEvfa%*w>)!J2A-n{c*IqhaUuYY#xK&FQcm!I)$S&`Wo0Qekzjq`_>5*N?& zF{NjG&ew<3I@?;d0{{lcd@Xnm-R{CPsQ!;OkBeE{Zqlk<(20Qqv*w`Y-HsZ#B=L>5hx*Ut;}e zP?TlBk(yToIl$iM(pC>u-}aQ{Te*4oD5d+`t1?pu9(f$UX4LSpw44>TBmJlM`cuM` zbk!3tMh;v*Lilu&^}y)0jVSZ`T|tuDB#oD@jO}0Wc*q3U)7a$nhm~F>itT0+pPi28W8i?i1OEc z3{C4CddzQTH?fY6cs!<4b$^wPIH#>8ijp#ZZ+Q|BE1h1H{EAlZP@0h5sZn(p5j;IO z@OZ?p#+I`{v!{tT-xKNANv{h|3zcV(bo3O=J1&Z3_xncAV&3$HRjR>9Fvc<}-d%VZLZ5H3=CoueX1# z{?TOn>2Lkdl1&;{H>v~Y1p}AaSERJ((o^~-c4rdtW+m%W-@LwQ=k(;;gteU%1;oTD zQmlKm{y3f=z5=RHW~-bxX1=q(Eo{WCZ*Ai#8|tC0F~}cEPrDAy-f5&O{7yLCl(;#{ zzEsxzeI0l84vtry)csn^E$yUtT1Fj5akgZWYt&W7J2Ch2UH%YOXt18DsWW^1v4P0BG$HC+vvE_Ve*JW~FpFj?+@ojh@TH_DYUkQcn!LZV z<7+)OPZloNFU_!ox6*vjnesLUq2^#NJ4Abs6ZUC77OcIqH2yv8(b0}Z#P@D)pTB&I zx6Ie;3WS&_Y3CQOY+)qIkp9gy!AgPthZ9<%Z=IfO3VPH86HiHN8qf4kZXD)*vT_I@ zEjHn!T01wkjcr&jrgI%uX_xkAMVzHh9mqu|xuIAm`@`TZiKnm; z*W|u`sjvpGp97}e=K8Gkxp|AOA#wXVKI#>Ehwgm8_th$~tsNT26STi=SUp-L+xNor zSJz*wey6paF_d}oIpwC+z3R-p%8cirX$>+}@f~?*&Lq1n;5*y#jJFRlTe>Hlin#<&8wKN%jd!!4Q_F96TXB>N$B zB3^IPye+4v#=cMQZ0iW!R(`4wpBR|?yfI)lPoZlzhoP$4tsl&;f2*Vf==T0`aGTKy&XCI;o_CgD!T=pR1K$cjdR0M57> zh;%y@Qcn_QGWB%iY;?$XcK3$s_RpE)k5e+gLiUdDwr9Oqy7{f!I$m@ESA`UNL=zJ@ z#`RsgW{DZB%ahXd%?FZ507ImYro|ndJr$4Mxd0M|b<@3W{B zAy$z86gxa*H-pQWEo z4q!BmM6c)^|Mq2G`xa|QVK=cW^i~ys$_AzVBgSo2*c0u3y=ZnH z?;Ttso)dZQ;dP%%tCSOP$zOf<`N}c5DHFxpJrQqP&(tu=FLR{c^RYVWCWp1|f18PN z-%o8jxltrgoQbZDTN*C4+uz!(#I2(jrngTQI;c6g_sD1o_O#+sE-*0h>mMcGM*MR1 zna*bs`x>&-hBgd=C^Q1By`;{58+oRGX`>!QzM)FX+A)UT(tfhcK3FJQk0 zd)92dSlkGjwYA*1>rA~VU34U0E=+KUq}In0O%9^0}`@(PTKRpk?_l z>018Vk3(;5|5_KwvG!*&zL=?@HukmRUard{!DH+5xk~D8YrkEb_K{DZY8owgpPV;hqVlt*Exjlpwy)0(QKI6ta8u_59i| z4E1HU+qisA4P#*`HZhbC@k_*?o+?*g(oWd5oVogJ_s_Yh!y85Ad(#q(uJ@edD}@jC zt;)g2@~zxcCB}uLQ;OfQ4b{l@N9MQITNY}NzY!efBqruQU-B|3OjwU(2~A4z_#cZ2 z&L0t9=820tH8Cd747U?UbagYj_4^lXdmz6v2f@Tf0LO3+_1%p<ni%8e_1&J4!LH%X(c4$}$z z`0~@H!{kLNvkZXW+>;b()S~0IZOh4}wqL_`aq+T)CQ8(<2={2zSN;!9m~gyx^QyM+ zb3DAyimYMs9>+M~?qIkwJ;WH#(VB2YrIWUWc+|MG9jQK=UWdeR$n+0YG}TyzTSa=yTR-RI-Q5gZcG}QT=NselN^^Xi2gk4`3Rcu;2OeVHf5nv(wP03~L^b zXvFVxU7D!&>(bGh)7-)%Qxm_gHW;i03G8%B!&)upfzS3H{-Aj=gi;a%y>GbHfm=p5 zKDMfEJQP=LL$v~|w5xhM9_2S)$-4XuA;gTjbiQM^M6F7*t^`k({!q^uGRl*YX4sws zMf-HP*q>{pMt9N?$coA|&IMWLf^kv)R*{0!hdd1ipdq|yIv|DPzvr4f_uP9`blzt* z-_Y4ndfUvWvKGDrGK~UNVprk+2Vrj>4`mzvf8V9jouU<4LsD5L$ub!xOH@cwjC~nP zSu?VXW#*P7S%$JNqmnIzY*{B+24l%KmcbNb>|_RGjG6geeP7S>`@WvPoKMp}Kd=Ro_41DPv!^DV#9H6N2dU++B6c~a1x z{HIp+?|I3mfs~~1H*`vc$6#Pi)g^U8^tKb%pCn#HEK<|zV4s>*5q>6zAPSHZF9i^ zLX*9icKA8YLd$DH`lF6kR@EfrI0*lN?3;uoCPf-CPifSHANJSkW2f%Ur5f7d_C`IY z%|$&WpKX;N#AS~XMQRf=%3jWJwC;)2D%$wvv&&vir`h+@t=QIqwO*$N-(%zR)CAfH zgL^FQd3*P=qAL%}e~!5AOg0@QJy9q{Nmr&v#+MyhL3iQ90__XjVPK#(y%JPswNq`j z9BADVxHd3dOtHj;WZ=)u8&+v^HVKU??z>AKp8|WmrVT4=g_=25xn>XVBL~`pTKtu%3V2ZLcad+*r^MWOb=7#`^rSXikE~W@=;$G4&L<-aXqM`;pTJ z>v6X@JT1?cOeelIgNXtB1ZMEZ3b1qSYbcG^a7?kw^}QYtL;*J+`CsV98v0MkjRFyh z<6~l@m!&8B)f+GT(l+^9&e*fe9kf8neLgMTQpsMS&3OO$a090~*V@ zP=9?|di#^!$7a1e4S&+ zX~8TKXtia3-t(PDNxV~tT3y+BDtC71T@;taIew~tbf{@7 z-@Sp6++|XYX!|m9IzzI+$zS{fPVBjBCBf%~SZ~ie>C*M~S}rLLCt0wNomHszYH4Ds z_QiIc;0K=AI}z7)H13`jis^tnfhVgSOO0MiebI+M?I=kUHnY?-@mhw8z(-4xKq5#F zc#J3auadtWt3LW;*{wFe6?xuwquxGI<<#Ho^ZNT+>!OwX?VSvzsV>@4&l%sO=t zFLtqZ(l7)UhiZlnzmu@+2(kIFQI~!!VOT_F*`p}N&Y7G0881>1FUI0*BrTL=;J;dU z3^@TOPbW^LA0>xrwT%Fnw-(N9QvjoMDi#RPzxUFDk~9 z%-!)FtK$BuR4OHXr1-{sF+<(6R}u~T&4*dl{u5y|JO`O>b+K`Yq34wx zIKzwo#%>vX@@&$OAC-ab7mP*KNLV}0rv_Y3O9WPD)VdwkpnfjFwcoxgBC9F;xaar1 z`z#Ko!Y{>HdNUs~|81)FtVMipS{*(8N-)w*99G>fk0`B@yTSA;G|tV3$>YU7&MnuV zaM*z%N6llgqFgfqTaYMLA=Y~d9kz)<$o~=0Tp@pf)*)dbMECCUx^v9ysfD8UThie4 zBF){sMx5qwPHLK=_x#YmJ5|*9YzNk4&HBR=#Q)|DME7dk);S=CZkMY@Y0Y7Tfj(~O zoTniqH@`LTtYBR^8~Wm$`-Ecp%>O;T!=%MdJkv&sco7!j2B1x}q|Zd774C)b0jbfr zAxK$A_RT${OJFE}CtbTgYO;PGFm_Zyd;F7zHxS~@`?pg`1rIWfR^_}v*nEy|6C zDVd>J=yKzF$P>mq$U%G?l#4(*m$4KnyInpIGitj>lXc-Lx2$yvCh#Jp8Z!eoyPrxrX$n7QE-kXKJuOnJ zoLmw3y;@At-FVE?pVX3W%{c9@z}YKj86Ci}#L={Wf;_gYYCJ@%LXpC$m+U?VkCv>4 zUhzoGRYfxzL4hF55CdGwoGm5!u-Y6ru{iAzL`NpiryFGRMNlkcbQ*&Iyct;o5hxk= zuxw?!@;-0YZ2GeH78|PjW*hOup?+rS)kTxthewTiyn3;dA@64#&jT@{+AhtuxAXg? zCrhU(*gzjc7nizuso?KB5Uq{3t!qEe$BejKe<%?pcGcQHW)jlY&=dZe*p<%*j-Q!* zFTFXGwu70`-v6_Eq(>Ef;PK3ht7#IH|J)!uPw735_30)u?uwMBNksn(4F2xcvpep; zK*?^<2AeE1hRDV(WyE`2J770$) zTk(q-dA{;)-6*8gIl{lfX20ul-V@SfXHpzQOuIMCtP79sbU0IT1j@r)NtQW;@WZR( zr&q|nek!6WwVtng3!}Z{5vfN#y(1{8SZwqhHl=j00FEZ zsnkH&LVbDaA3#HDJ}|4FoH%}V?nHqaNRN^44S3cP7|uVV+5P#+0xZl!(ddv;zA?pR zMbW*~Z)c^|6?-sN-Gsaf2flKmHB8A(ezSQ)Yf{{{>&hN(uOuwnJdq_vd3wJ7(6hPD zpJ}%Iuw?@Sinp&O8Tb<|es$kHQs_%T7A|a)aNxVAy?-EMf1ys({9;#M-Kt|k#N4fq zb2=V#4=@2Wliae}!^FWiPq?T4UZ&OgW3AxvmB4*Al=c0ipw=zVdf761f490EktgVr zQ*@_kSvS2x%-=D*qKVjqO<&n_ z*H)2SI7>02w@J2arDa`?ZXxa;9@&h~W@hWRGBaZ-?+CXM&mvR@T0^(05BaEn(-H?J zmWHG0v1UCBOaWJJ-69_9ljnAKaiBnk*n_n$9TR%-PNinxio$4GTsP_3#Qyse>S9Oe zaI`V}>B8)M+uGHddH)z5J|ycyHt`EPzx%WzZFl7&qY!U0KYdQ4zj%?QxRp+9CeQXR zqGI(XxGa|9WPb!CKpMIdJkv4p8A}B}r3Fe%SrG1{rtEJ%=;$z^@28ovyMYBADc15lOBtjK^*hm8W#@q=%A?CCP-XSyT!+Zs-= zK501Q>1d`p0>xfo$7jRzl4W<$6M@z^vR?nmrQAn&_NDE<)Vz^)u^tZe+-llH0YE*8 zS%$#)eOB;MbMneF*kxy}ub~RCLD`4Kef(nLFALjRAZ3TCx1KHh07SEiBa*diVMW`k zHKG<}Gm)cbHb3-#cJ1cjTY>8HHJ^ZO@3c&Heis9LnoX<^+rSFZ%pd&>cewvtzw4pi zh&Tq`X_{cgSIp~~(`GY+XbFurELL;Fc)j!Ak|@Q#76?%Eu*%e%;Tq{93~$_dvoz*} z%Ldk>gK`STVyli2s?9a3AtIiuai~1W)IrP>t%shBqWIRy`R8E-kkM~h5b_zL&)0tC z{u+k0<9UTOESXcO^dng~eqXxCRxMFQs7%RxD$pimduuoUaa(LvzZ&}K+3O?EyoX|| z?iqK#Lz(I;jaG-Gy|%SZ5`BZOnGd(g9$}+b+tk+4Q$9;cPf#Qrj-6Yn-+z=U-)ywg zI-uIy`J^oNy_{yI`e&O=uxL;4R7l2#m~5$a)9w|`nbT0+)ERgL^p?esOm*L0&$sIl zPndg8SMndTnv(qai8r?@oGF@{*n?ro0B3J}eF*kC%&xzj(@0D}r4cw5QIF~__J$tRc4y#4JO zT2YaKCSnh+WKDE(rd}l@jyFsR_O*r0ht-$80&i{=CpyXXhDYlXCYx-rpdVAasdp5U zLaIB5tK!Ak_K?V>`4EogjNgZ)xn#C{Gsp!b>&zaz2JlXxdxP=oa)UPZx*_x|$;t2~ zBg@8Im?TbP#iI%|c6zVF${)D3O=8Bv845>oBB7%HN*7Cya#w5>D1L0oewRCt7)n&rF|poBDxQppV_*~EA%yiVhL9+P$gXpAE zVse}vjGh?-3Hv$1DUa>JZ*Ot!$(4rAW*G0-xA%d9fzg``iCF8V`$o4VuNsZ5^-aA0 zO)!6&B=cemzU?}3K23>T99s3M>BMR7QPX*=f#m_~slS=~(Gp~CX4lO(Z#(T&P;s|Q z_b8=N`-X~X0+cJl*^yTutO-UCuM3&IzFViLWEb*T^G@h?$$(nuy&oUZe#FSeT<(77 zCn5Jy$G&=lb*-n!Kd+dkEP&8M`bO0YyxCN|A0aXY~<8Shx|Xv}+YD#g3ffV7QxqRPbimckPO4 zk$q=OZ)8XI_)(&5^?HzpHb^O~^WeJ*9Tl6GSNvcDw$=+a2aC-H9zUJ2PIx*6+Gre+ z+_T~*C{+fL*$)_!DjX?n+g~3LYeSS(5;Y1*DXxDg7Mm9N50eHMWdkv*Er93>L~2O1 z!E#Vc+HO9&C9oa#e36coYh187?H7PQnESNOZBuJ!Io5(7O*bt?RE}eLs6+2wS{q0>5HECN zZh9I1aejqzhHGAI@~iok%VpBmx66H5Ho^$KMJQ*kqqMwuaob`~7v?WcC{OO0h)QJ~ z?26As2vP*l!OlTzlRwj%MD_atU1E zqBqTzfdv?RAGcz_#BS#MrR+!gG#vmSomRcp71jz0YuPy!9pB1Y(14{PFn71TZ9;Ok zevy2lau2Bf!qO_6dM$d~&H6{tN`ANwAcS{ftoaJy{E`wGPntN}Yc0>T_JrKybN|Hj z-~HECKse|Q1Lo*0k-GDM!M$t`p+6O+8#=*FnJ{}d`T45L3q9Va$dA7A@SX3?PH2K< zUtSg3_ODPA^u28=3-DUx2SdKxfXUR_oV4N%t_J1OhJQJ*mupr9Hf@J&G=@P&we6xZ zd7nT`qpo+N*Db*QgUb>}nGe~EhM*vKC5rluhd|y{BcvyP;8=oIxrpw*@(@TmA3WQ;=)_|RN1s+57C2w zJ*@WZZnV&v;75m9gMRKdu0H^Ch#(ZY?D=;X_tqz!S-5uZjpw!mAGj#d`xNVnpY;y9 zoqoF&X#~!z|9iCm4!V3TiHH+=7xoMj;XYV9;1qZ1osbc zDzRs_a3r#J2D^~3dryRrAX4&0=S=X_jkkxq0fwoc7xGuL&#tkuJq`=f{e+Q?={Gv$ zdprc6^&@+{{j~3I<0kf+Py_kLoxPJ37_eXBzxdF>sOb|yx>>`BpFf|hy2IuCn;vVWXrAb8jeYn-`<_Bg&V}d-uG~F% zLMfvXyWqN+dICO}OY#bDci>rp>H?J$LIMiJA6NgtGHyx$cI335e&+thPQ@)0J~K{w z-Jb497kU)&Oe%aa^sx0wrp&3Fs-iESQWz#p^c-*0EqN*E$9ze$iGts7QHKEfvM(ij zXQ;~Bn$u96t@BnzX*~K@nIYy~E!Xh%mSW~)BnB)t%fVfI$FK0PGO2rLFW-@w;r*`* zKXEzslpU|DlC>85VPMC9$~mD)?60sh*|ih|CjIBHoU(=D z1K%31+23DKZ>+alW(yXR{THx{DE3)=!y!PPYkFGUH&Laz0U?cI!h;nmz58Vw)=Zqb zrv#09{bK!s!4R^oG@08s127ll3tp*ulsS2536FW8gvTwj%`s07Rs4gY z+fs~7`C*jDWcU7d>}e-@!^i=5lm4!ddr*l>?w%Cwf3q=w`aO{ogk4#$`-X7+MxI@r zkZTY%oG<+eq{6B1AMn5a6o`}>TY#eTS?B$isC+r=oJT-7tjrQ{CDyK!%EV0mOor$Vx_JcHbbQ&{)bE?Ma;BCcQ3@m2>4fyeR;buS?!# z2ga_sL++Gp))W;#Ruok9aQW4*z$=6h!OeSqRz*~O$UdJWCtBf)$zKRMwsigFpY3u7 z!v6mRWI1Ovc>RMpbjd};SHoMw2jU^pdp(xKoaQ^G@4M!&A=}U4hmQH$5i*i2Fd3Jm z({!3LKPohb*A^w2P4fELegq112HlwV!Z9_0xpL>V)R*gY5x=GUE^l`mRCT_B$B%uj z`*;Z*ANVh2q+wq<6~FZd^PG=zwk{jXSq~}?0xkUDB+1ssUh&UfP6YWQf80j7tidH4 zr=}w>bP1F%NeY~yd>ycCLDYnPp0yhA@TclxhrX?RiEfBg*;f=;V$tTI0ci$6p0Q+F zCX1HmRvtP%vA)_(W>07MG++8_R%%o^6GRS2F=iO{iztSE|K+7z9|xWc8?cQO{H>hE zvtIKgWdh_ln-U(nV=+Q0A5?11<^*_^uxlSn^d&1hi;qjJhSeW_4C?aGA#kjz9(Ms& zTY-M8n0bZTNR1Wk-%}=ghesxv*z2Y>;lW*Lb=hCI(P&|AGIPaV4mvf8o><|yD9snY zB>DOV9a%gK3U=JGk?;yzT8(M~n=9$W*xP@TbjI^pctPr`hMcl*Sx;0-Hgl)`h)x-f^RKhTf90_f)IbL4#WlyOiHu3NDVB!5_nB z`TOi!ccDWNdKWmAJ~8|6Q};L1ES(*itrkWmXfVYDKYqk}IrpTBavveb+&5`LkEg)_Wj$@i5f zHuIniKv-1c%@L3*`%AlymPKIjnXJ&p=HD>ovW!U#kg|&k-2m#1_7@~SID9Ni)GUfRJ*GoD6Up93E z4Jlm{TJW@|I|iwJIU4gD=!}5ZFe8PVtB>MPQuJCIoXu|?8m~T`OdpLC7%#7g<|RNQA(=}eMl)g zfs{(-&cD|sY63Q~C58^=y=)(!Xs)|&%xJW0uI>tuc`MUCNTPmZ>j#+KW5UV=)A>lU z`lbR31=sWcsaA~HQqMPyYpr$Fba~!vzHpd+4XC=HN~6<((OnNd#SI2U?OSsH)>wuA z?$SI_QEy&g|4pdS$ znzEGY+PO-P3s>s9OX5atrq4Qjk|h=~F22VPZvj~%D<+Y$BO2NmFZ1Ft;h`g1F1)F1 zaGSsvOS%4CPJ8oN(4;u`KOnRY+I*XqJ<+`8E!2?PpUUl=zY;a1V-Sl>U}s7# zY7$~h=6>j1drj2e@`rsbPc9FbIZZR%19DX3TivgY($Xn^twEvi!l|=YOm!gpf6S-f zohPQbpHTOKjEYwPgT0&moyYX~MnNLsJRp8;v7{(p`%O_aSm3pb4n;$0ka#Ja$QwWZ z;GHV&Pc5~p?n<8oAVtNl#cT&+R&ULe5N#+83#|2i3jT>aYGB7f(2g}CS3h;t(}9JK zso#c?M6TE6p3Z01tloLF5aBNk&Ju%FOM|Y}o;Io$hR2E-nVDxjvFTg+AnmY9J7id5 zsW>LhZ~|Nh?v0{a^8|DLgP)XYvDNX+Gql4BxXCM~0$n!po(@-C%^Egze6A)}-4a#@ zqB4DnO5=@>fKpE~-=vP}DWSVac4;BmR!R}o!4C`E!U~kTJ0)eVP z5VX)3dS08JA>Rl>Gp!k}(p@JCY5AX%Bo7;*kmYUeP*&G^o@8@V=vbptAT(HZ<9%Z8 zn2)Lwn6#M>(t6wc{i@&6L+*>RzfStU`y}*AFCB=^!rM zBgzt?yyxq(%~n7;DY=JYX4mAf<*BLl(xuaq9|hV|zvR230Ocw_5hVz&NdI9S>7(gpD`>26uAh zLlubv*V@$Qyn+qy9!4*MYScp#!smSKG^v0VNx|$4paQEjQ>K@Xw7+|mZI)DZcqT7_ zy;7Gu*}P;TpbE^-GQ4KAWj{dDSc~La0%t*_573^GoTl&JqPE5eV$RDbJS zSgx3Uk$eVf^WHy&_L1z7<*tw_{QaTW!+1~P=_UD(m4BSI$Wj-!yEFK}DIoWQl;pq? z0yoG)ngi+v7z$z1BZ+kNE0`J4?%?);Oe>{CeuLmErLVQ*-3+7yMz3#H{h zr>{(TZ_b4JK2OV`@D5WhJVNb=dwNERrHAj&38*6pZZ1=&;-dg~K7U00c3*tm_sG-w zjK)VNPHk~8Ek~&lx1Bv^j}A_m9iylTk@U-XXR^LF)2)5nhm&stGlvcCb19z zWUr8+X*O&aZ($Cp(9kb!B~GeSo%%{#L4SQl7f7y2Vk>=F5~{vCUQv)`=9eJ5MeOS& z)-?&#H?EFK`{ey9#}TG@(V1s~HGZGey?SDMn^;x}Fy(w$9aGm{zBD}l^8?JREq7Ii%0Ox({piC%?G0s!n(U%d6AQC zd*EPC`*|a?7GP1>klm>J*(LO~Y2OIA1;WJ*ES2O_s7RBFBVLl%Yic~2 zYkDu+{Ycj<-jKZ<@9twljp)b>jy##W;6AdPXWXqR>vn7q_3@@Dkn5U`?D%Cc02rxF zW4qwFG|rLwv;R5#Q+R+0ydvXyGrZ>*4f*DeDff9j3xk$SOQo2Z)O!s=dqRH`&2P{` zwt?BUXI(}TxtnSNwUb#1jVXlO>(a??f|$%aeL)|8v#5P~6d3{#^CcAAiUI6x*>Z8W zd7k3n^|THhF_^+(pZ5};#|LFT?q0Yw}|UkBGXAnmDLV%BBwiByq4G%Xui`F#l;1 zKx=ni=ezeTI$6w|R@!a7T{b$P{ipL1#*(vuo>@gz%}R29>oOd!ahi7T>d#CvQ_5}E zl%B%Wy;2jADZ@L@KDQUwz}AYMHVnYo@UF|Hv}zyD*5~Y3-FV48g9xRKz`d z{zKzpy*db=wA-Cs@%c1Oc@0b56(+Q}8Y}0?g&I)f;P*?TIUL^*N57-h*lHs4M*fs6 z+Q@_Td9=E=9NNDR3Pb{cH7`9lc1B;mimWa_&v5wh6r zC1L$mDYj`NT&FxG9%ECDGcfNuE933#L6F}1yZ%=hGoe+X zc?(a$-x5RpDHrZztHEfL#(%>AgLX-RHMtu4WhvtJ%17%360cDLu=`a6ET*Cr;~MEF z&T|&2;8Ot2O*(m&f_uhfdVwRQ7$>`APlIFq@Z!gkIV?>z&<3MOQv`*%%Su-g3WX6l$oQ)a{Zx*^tNta-2 zH2BdV#PNeuj=tJ|^%W=?6alq3GNQHmy4Jhr=I7$ypH7PVH|R^_Qlz-?1RJ?|&S9qS zV~$Fi`EO4YvS4^)2-o!I-V9y}q)I=t^@*1efazWY$bx9f}in)eEcm%)z~O@#fIT$g_t}RH+uo#C#e6mNTp^n~mr= zUEzGHc6j81via0|-g}Z$ zo;v)ETjyA|3CbYBK>+VIdf=@nSIYzMY*3k`&8v={PVz6%?9iCKr{bI#JLqZxep ztc!i42 z(dB>Z!=%!bZ#WN&FpGm?QpPX<0*3Ms&t{Ntf52(740Jt1S4plL6Y&=&;M0Vl&OZPp zEegeU>qPDEW|W$X<*C{a)Lt6oM0T%etQ1l3yrOi>wVU;)xX$>(Mt{dANI<$4Cd>Vpce~!sMU2l0C6=2anQp%n=SwAA>eu;9ykpI$Ou*I}22H1LTrU}K4 zYT2EcR?BO+Tt;6L^@U(JU>g6z9ZM08l|;&2XxW}e%EHBG3jPrR&Y^dugYIo)sV?Z5 zFRM4(8qG{Gi%sp;QE)xfccb;TM1m~s&@0=wcR*LO=v(pagnXfHIpnokyIMQpfa6Bw^6|h@gR&P2uFIPlYWu4yFWzgT*>3G7qYc=nc1q7?_R3<5L$q$L!Q`! zo<7z7dzq&jEsW(qZG`VY)8Jp&)nBPMvxdi#$7`Ie0igol)+!FmF6b7zPYM=@sG)|H zq3)k9^zQ%PANi9m@d_3q59Y!AT_#L53!UtTgfZczczdqM|NYH<8V7o_!tuqs{txls zhRT$E)~AA6kut*)2n*=>F&@Na{rcGnlu8djNddf%p~n?jAZ(>$jxyL(AMV3)*GX8`nbg*o=?q=&|p zWa@RMQG?$5fcP@H;^7j15?5V1nTjBB1tcxp*Em2yrn-vI~ z%p!5`C#IDRzViJ30DR*K9`#_1gs0o`k~;?n!u7Iu;M!u5DSUH~(rjVEqyoPx`+BaZ ztsB^@mQ8k;*+F~%&jAEh(TV@R0~&bvN*tH2dZLjO;!Y}*$X4bvu>(|1O{eBX;pY#-~o|4a!eoA0W z7*V-51#66>)d&Nt!jU3AiC5?+bfg%Z<&)CfV5QZl=)oc3^NE%geki_(!a^Hp5zecJAq(05A&wdqOW3T4L_hsFm_~ zTAJyY+sf`voMW#2{Tu=RIXN3Qq*mh?QB9tASz#|&+u;d3{Da#ni*PN_##N`JV!lYog>h78hx=c@V?`Qv(aCER=e&)4)Yd5f>qLSeuvQe=m_`dwF(kb9KBa|o`JnmU1?oqOC<*lPX2Gdwlb zsD?5wU*lpL4!b$>5wkRY9bn9PH-|%%3c9-&4wA6VZl$Us0LN9CU-gN*NJ)H!$d+8E zKy6q;Gi3EltF?j_(G_T1o#$43THCNyy`m=dD=h^1%y_Z<03DR#`As5qqiayRc@yZDa+La2 zo1VTOd(dAxBrYVwiJ8&&wNT7LNuKE8*{I4d2z0559lkkY6SyUl&j~0ktbA_aLrADE zZL*gTJG1Bcecxyvm^s7#zX!6o?i(a>fzjvXUC%SBnfj-1m`NSD@$s#`&V@tCFE8rH z)agsB*S~(#db0d=jPx~z)0b;9V;gs8de`+#)~rLz>>BUqqR75(d(l8uP0g5nO-Vm^ zdbF_7w8XE%_9dnYQm)rLtDNwQE$1+!FJrs|R*+Qg-1Ugrj$k`V)F$D@h!)7M%AL;= zt-X=TW>=KGe_lz>XlXtQ zfa%=AE5h1^8?jug-m%x)(lA@UberE3-yS85xr%I=g$KXqF+4W8qD9O7&SAIb% zyeU_2m3f3pu*PdW;RZKABVWW=GqK(*HCjBJ&Gd7MJHYybNeL@|ta_zLTVo(xs}8`b z44`)}ABuG(jh_;X@*DYTIJMKV*!t{K!qc#3VSFHEHli~u42zK$pF4kDoZhPTVw9*M zNcz6;kN==#&9}AYB>c^531akHCq9PaH<6~R(OSj&1B;e}yPR>ggY?8U@6|KxYYEmn zuSqjPTEYn<%JJnwbK?oN5Vds70NpPi4$&)sYmXaLOp4&0MR&yzdVR03WcOd_zNfBZl3yKfo0d5@hzDud5f-)I}Xm$9NA^=7{iq?tIwx8GAshVhw4b@?)j{ zHX~!hhH~A--an275FN$H3!EgW*nk_p+Co-P?atZb8U}GP5AtYch2KsvVR-K^+c(dZ z9HdvS)vqwo+6@C?Wa%_1dgfdRN9Dc2eBOOOQCejJ$#x|G#F7X-k%}He=5Eg3RU?=wcd)+E2Au69vlBMqvGL~yY1br4acvp^+8z=j zT>;IFwtm_37`AOFPFlx?Nh)h0jHA7`44}PPem#~E+wmlo-IiyfHE-N)@!R``)sSAi z+T68%4KEoC_UP)s!Uw?*h;tTfGkk5M9NEyA4BqrNH`c9Acw{@@k5Jo+=-pCKBTU=~ z`)VCZgwDaTmzG#KbmgMdUwFJJIz++zLi0)VLBkSFi8+K1Uvd)7`5_PDqf=# z-sl%MfDp#4<-HFZh2*{Ri<9)z;wo>1a4_KBG!{jo#1R<>ht!JVcNL;H$F-03SoZnj zgSamhj%a+IETD%dbk+?Dk>20~kBrbzcsa4s$4F(9f(}@L+0B%y(Nnpv-Cobkoq;T$ zDm9SQ!}~odU;JR-;PO5NrI^Nk> zW!S0dZR0~8ANk8BC2&E*Tr2F@b;skRcsPW5?L^7>4>#@&{AX=(29Z7&Fh)nWy$Omf z5wlPsVvq`*H&nzs7AIVMLDt^7y)BtFSq9I9GZr!nW3BUY40qK%y9KAUam zsExplm#l2(RZh%Z-HgX!xeAg1HnXx+!fWF;G5X+gx?w^0bKzWXcV?Bg{lo&4NpthU zq#;gf7u-;I1@7bJjktM;Hg4d8f^+w}|EaUe>2c^d`yf^V#z$4LpMU?wuM{+LA=ApfjSZrTyxXtEqD+Rrcl?iuTIGytD1o;$++=8TL`06Q1XGtFF=b;Ak*A%U+tuQem zQ*HrQ(dW!>DTX*IrVj+CL^gD8x2y)Q;Wz4D!6)WQ9K7+WA=fsn#F&Cv-mKc$^Iu`F z?m2iE$I(O187fkpvPZj~H#aZr3-R$8-+G@kRQAgj)GNBOP+~ahPS|c5`U>fUD-A0< zDuuiks6CgQupQ|qNJlQVZl$a9BxGsLs!>bQdrA=T8C#Pl#f{` zH67U%84p&*GONNy=H|c}V|?_^z48U9?h*_t&RurUI-TJ3io55Kn;FsvzZc!2@Cs;v zwb^VtXB9X48m#c@CgtQ}N7(;!Je-$#`A}4P+@;cgi2cun^~8CG-wr&kO1{Z+tpLobyV$$LE3vSJz(Nzc? z_1Paj=WOS882Y$6wSqRPv`>0TG^B}}v%d*rPN0tiUO@*Oa`{SFbPQ=1R_qihPIlNk zEkWi8*8&Y+BY0c<=&MyTMh944*|SL{|L?5`liadjyn#cH3c}g`rHTs9Ew9Q$Er9BP z*PjE4XhDn^x^RPJv;S~`YddhGD^HU8exXwqt=rjq&dsmfDaxhjNs77;+=f}Cc5tqR zf5Zc_`%B%I!-S%*WoH}CZgAP4>89T&1g4u->@d?|TX_Rh`?*~DcJ~>Le#=fLrP#Tt z0h(~SP^|l(@I3p?H^Y@FQHQA#QMr4?(R%1(0D~}3-4yB_%csWc7bf$3zkUIDY?~Np zD4oHX5z@;&&ZXprF`s)`H05<}*!1=}zi9(*#B2TPpBkqemG=^)(AKc^H5jY{>@;)k z8c1;97t5e|_FP(ra1hB+^%v^9cUZgkE8%GFvELti|MQoBz-*BycnXRAz+Zp<5&sVU zxoDb0h`669A0K9eiN)13KS=J;`f!{v%+&rwt`<6khR@s+4({`VcxNr0FZ>R9ob6{g zqCDI;8!9B|Pd0PMoY1aq=Cihfjq`E2`?&)VLN7Xm$DAy6Xa2kbtPMhf8U|Z#aQF0% zzn2@U1IUB9IA*&XHfI(<7uw9mhF_@TU0_wvaSau_zT7^CN?38!fNzc`NyY9}hL51T zBe=`SrWI4!ivM%9t9Q)vx?-&Imo2k9M;j_Lx|M>X3rab%?m4W7#e7f%? zl~%R3qRJAdol{~v z@0|88f5Ny1q6TI~6a^$R4B+G+ACKWMo60;{n!S3^bL5KJitiyi|Ex?z_U!lFP+Siv za#}l{0i0IC0-e7Gj&sWT;Q7l^id|XO?!3u4rAhZL&|@Dj!q?8i!~(|c4-+iIRr#4^ z;ZOwy5!6Z@wuL!4_8Fvrg>S~?b|fDm$xj{Y+}{klH7%^N<|-&?5{VHDls(4E`pt{_ zWmHJmUchoqA)9MFhO{hu9e#6i1N3(OMhD702frqtuV=Zophg?&ncTFBMPTMXA zXx`rB8JvTlAa_wRoJp-ivobbN3P0ZC2TAb+>Jk5DZFH$!V0%M|?E+l1c^rrz{5xiY zOYJM4R0v_p!}7m=Yu`Dd8K}M6T4*X+^?`AGlaN&=nH~B#a#9W$oe@jUTu(l}$1*(2 z6=lI*G8L~fPx$1wS%$VRbpGD=bIfO+jR$4HA{Ga+XW`l zi_#8DS7-3-cIU8mTl1W=sdM;<`ZtExau{WfTi2}4%Alcmf(IwMJe9^Uu{Tj%T4+Uy}F%Da3mVeOvR zR%JtzIoKTcJ5^>rS-%gH6AN3Qt`3cd=$u_`xaNIw{Td#k(6WY+Q* z%q@{!L#bme5|4+kI_RwWdOLk+YLfFAxUm?M{iejbzf4KNzIXwODVZX!@1I(L`ohuL zUuw-_D~rxJ%-G(x_<1Dja?8WpMLH@)!Gu1DFlH|Osqn(e!1FTipgW}?1?`>6HLAtY z#(l4}L@mYwnzO$YGlSh)wUm_Si^s8o=t1JAG-@}pIiT8o;a7znLz_+XDCsng1< zDcV3S=$HpoN-f?ls`eX7x&t+-bzxrwzlGzM8KaZe<4BWZlps4Q<6L9 z*{bfm!mY%&)UWVmNz4tH04+SpuR}qra~s(SARLCog^g>^eCd>G%3aR&X9)_9^U^JD zdHa&UuG}Rj^1DiUX-?1)PtjGhHa0Xtd_{bf84}z*2LtQBs(1@ zFZVseQ&pG&;?B3J1N!NlIzTN}DFok=jU(na`OKAhIS}j6t_9jhvPCZ zx$D|{=Fsg2-v{=4+X~0a_j}G1W}TmK?!$hXIODbIPZ<*u%#e#53gk>hMg7iYSMhd; zgYld-z`uju?#|j^&n#BHV4jQB@CmK_3h(ca2Pgs=SzMX}7P5xVWz`%0&{o^uAAp9? zH#pOC>RdJs8bFUm@74$jiub99$j>AQi2Amf?(47Zvtx3DpLFSk(cTUG7zl@xD_gut zf@cEXRXMO~3hxI!tPy=TfuyqP@FF0zGMGj!#JQ(2*uyTo?bej+0q#157CIu=0JKra0TBTA~iGfTF`W><9sfT}$0}0q!3j zb&U41!ynbv?rF}#?>0#km1^P}^5p3O_@sKv=7%gs^F~Y-j^nv~(ftpcI#*{KDpk$J zR^;|iYA#B$DtS%AAAtr*qarp(Ct{aLWX^bBA(TBjJRk?xI&Px{3_3qZW}Aiw(KoTf z(cg{l>qMhX<4rf*$UM~l@GmG`DUz%8X+{}rv*kzxz|i- zD!xFm$8vAlAuEJVY|Ze6Uat)}Uij7dViYo@Fly&KbExQvxq*}(kDmK`U%3t)oFNb+~+Xw>~GJHIr)5gx)+a|kOykCeIxK1M6 z=>x_;GJMN_3pwU!xx1QYT#GA-J;ds2(?*3)uSh63Tyd#bI2f8&8P53F7lBO!HwC7u z$66|D;@meC{tsnu9?s_ag^li3(T&pXR!qTe?e2D?NYzwRXTM*o#U>IhBGOVrXlPB5 zD5_dP+TE?FAyvDm7!oR|nI_R1QZ=PwXoW-(8X-uK^X~JVbA8vju5-R~&R?#}o2)m< z`#fu{d#!uj53RGlu?zlMrWDsIh3;K2lMD804M&CQ@=f=6v`&+S5hiI zO*4d66;5YoYcmrkD45_(is!ssz)HN$=k<1Rk{Ed@lyarY$@h&Pg{&cbY%)?qqx@*> z3y*T>yR+EYX=ZY={`#W)k`sPr%Luyf3vimr4va4bvvVX=4gU6ABRU5`ZHRuyOh17) z!Ax4PHKGL|8Oza8FLgL21^4#P;eJ2ZH9?bEXi=>iCLzj~e(3K)VKiqMyg&2H8y)pjW;lU zk*EYv{fd2B3q@nqQDPvwC6w%|S-kx5DNIf9kxBc6V!l@~4D2O_ds8d@%eur$8+g9i zzLBC8*HoeUTYL_S(cyGB=h1(`o2ngfg_2ufMqWUPJqRxX zRfNS)`Z&I>i}J$12x$sZR0SZqeymL@JBl&hEA8F1oUbu`m7!dC0LTXIrg#7;NCYXB zOv75H{Y(SI*WPY&n*M_op4IIl$~KZqO+iP9kJ^+Tb8E=IS;}&-rxw|w=wH{Y;{Ym< z>G)1sN}VkSKt<%XmzJAY}@lj7Wb;NH9SW!6&~Hq%0LVr``1;H|RY0Ni29rL1Rk zsLH~J0|>>`1S`6qdX80y`PZMkRJrBW$29E6{$bhS>_(Sl$M@iDHKLtGPW%c<(a6b)&i72+cIR1N1CKsb)Z5_og&=O^^+fu)EQxW- zzJoR}<5#eBqx7-)&S|Rz(VM&kp{br>i{BSQ<@P@VzFUj*mcLM>F9}PLtW=W2$@eK2 z!FdzOovq*)9p(Snn%0~CQomClS#i3a?5dx__MdEp$LXLf+5UrKtr1MNvatm3v?+Y9mv&9YJSyONfn_E2m%aESCwooCzqSB2l-h7aBA zTZHL>ueU`3TFQ%HICq3b%HQcUwbP5&3f=YN(|PW!lXUNUyd3emJl1V<7J*+`efp8K z^&!&q>*io!ecOtWOCetGQJlQ=QCFci*Lr({rcnUs(IE=!FQ%Qan_MkPb(#7`K|EpG z)_%Md_h9=yMp|11XG%I>2~6#-Jz+LnTBEZ-N)MwDiCUrki+X#z&Dq(@i@rZcxxcMT zk=JYDvIQRtB>$~F5w*yU+g{K=jr*DH`(zPjkHxEoATDnYl`fY1EOYv0^W#rab6Am}bOz6r4UmcOQO$nF>22KzA-RH}ccy za~<_g>}=BFe{144$CjMFwVB;Q%+%zm^%dr*L|yC*yL1c1SxPh;xz#HEO(?D(ZQTk| zPGaFbC*&hmX{6RM!WC&o>-K}CgQOVV-7a#f{^XL~ZRMn6r191tL1+SS@PJE!LmN%w z{2~b{wK3u71Br+FSA2l<9Z7LmqYV&J5siPZjgfw*t_8t#7E{JnX~%bZ0}E|42Jz5uSiDj>|g_T!hHC|P8`wV@zBvu$zG*z&&A2_pu+ z4yhb$x9+ck*|`iraa5jpZ7IH;GGU7xekN zEpk%JZd+Dt!S*8Hz0$g4SwLF$W$>9Tb3pe|} z|EyrYU!1f~Xxq@-thxxU-gaJdl<|*Ub2&%z*|v;r`tk`oSzI0CcU2q)#;E{6%UU(# z1??T>w#!J=>|*z_N$9d!J;F1;jq(Up-}GjT8BwyFkdrFeD_yC{_~Ltkn($CxCQQ8s z^M>!+Ck^{vP_<UCojwbyWSy-rXo;5;|K3tHZal3j`@mh&O(901}~*3TVqmGWcyL zA^^>qV?D5sp{k2l<*N4;ZK%KJlecp=d#AB=Mn77OP{gsBw2O{8cU9`u175)4=>|;y zhRT&gj^}xA%m%Bsf6m?3?x@-g&urQM7AxRH@!K%L9MXaWA9t(ZccN8rnZ!!<`B_IJV@))*;vg9a zOuf-(#CA#&n>dQb+bCc0`)GbOMqzVZe*F5r*fs24OT{RMFU;$f41iq~_(4qx~eGzU!N}o^B7>j#!>U^c5Dpa(`h>({##2@*-6|{$qo`EsF_W<>biKtHEXWl2rX%Z?aU@2;OwJ6qMM7Oyd0B#($<$> zz(Ala1uyKA*wE z_6Wn?P;`r8wJYjMSTC_5S{pUXg(gotuGO6yQ10NrRj67ffZ{!F*S*ryhbY>l_nop9 zJ15vJ#ULR%i^dLvWd-*SW(W=cmT7yoHpl8vt~}d#<|uuWhZ8E-?UgP9Q0q>5?IulX z@Ri@95*4H#nM+E-?Yb+??hy?5db1CfXB0*B+{S)pdUr`WkBpcdAvuD`zElcJ+>+|^ z{WL%3E%6LZ>T{R1mO>BB$29BhOgRG&OS{?+9UG+_Q;~RB7Mif!me-GjK@IiQa{{F7 z$1JA@2}>)SG98QI6grT0X~u(#i3qHmyS-<9iac4ZQR}X~Ha4O>XA>&z_peus&{w|q zWG&ffcPy{y6gg+MZ@SpT*U~*FL%vakyEX0tM6UD^JpPddhj~Oyd{E}ZzM4{qe{sS% zf_?PMN99f1sV_^#_nDdwVH6E@>CunXJM-?Ph^~LJUXKC9~ zF5c}Cd)O`v3F%tgNaI%fQZ+R|9Oj;=|LWdTXA1|zv6CTQUhwzCb8Ktdib+$vaG}(3 zx&}DN?vZY)_y}8E>+`%`ra?{EU$<=h7W;(l805cCMGQ%-Lf%6jSXWQRY0R`NSNa@% zi`sZo%0BS}+Nac5^^QU@{p??rq-!)x#7a7M^Y`noBL!=}4hB#!gCT)2|$jKb+dKQc~W%t_K9)+Tu3UWZ#Ttb>a^L#}z4lH2s_QsZ-l-1O$SRT3$kB z<9(U#(j|2scPAfJRo$;4<=mCOp*faaIgp%)pqlv!1Y2i!@b7Q+Crk#kve!3%g&D)K z4=dQv#XegNrc2lZS#IcZ;4Hr+JIUVW%QFL+&{t**^W%G_kxg9HH6HKkyBX9&f9mm!H8NuFYFUpQe_6;*m6TN}z+-}FMZ>28G z-8_U6)71Emqd&H3EI{^ilHT4MPh^`UsqIZ(s}1WDv2Pxjq`&z_qD4{^T>d4_Y%WFq z+?uVNquRocMsxk4OWQX51l_sE0xf^Rj4g;_BuZy%X@* z2j6ZVJ0q^`oqwDV#AY4lCMbk1`w18~vdAhxB0c_I+=9asW3w5H${&D$qQY^o4(>lP=8(zk1p^=Tc8f1Z_+idJhA7fo5K4{To`#|_Em)8!; zK5snL-#odc5=CaU)`d@9aN2&2X-tui(E4FFMq5Wv=P1O2$t~VyQ$SPVvu(}^o)viQ z$Y$ZXUF(9$8~!T<^($YtlX8(0jUkb&0tdn!B{^heLKg9y$}nF8Pfub^oz78=h?nUT zpyA+z*9_w*hp*2k_odtJ#^Tm_y8FhF8x3mF7<#Ag15O`-*ANXBkT09=yE8R4(JbyLb9j1{^9Q%P5+8I`hF)}b*(-H zz;&cRED$Z`P#2aSA8Un58kqRUYn!Zk60$h_VR~< zX?nM{6=1S{e|?c?e}JQ$xP7pEoBv>V)k4D{S)z7tA^fnz;PBI*8lk$qJ7kU)C; zHwnA?pu_6nZKDHC=R=we<{ybbAP%YLOZEx3YCptSo$O~Sy&LESavW$(B7XJz;JzZy zW)+DohyyMF@`f@zv$Ow-^tJNHUGTf9vCiGNhSG0Q8lb(haa?2L4Y?ljNZDO>Ard+T z@q&ANi|+N!&rA64##pA~URZ1Mrfr*Oc+h2GF~M>Sl{`V13S&R8Q4+g7!hJcgfTSc` zsN8z|Y~tiN{_2maXnXvDtPT0@#PU-Lw#$owPv<(! z@P~d`@1iOb`bO~u&=#ZDxPLy#rp_0=dTbqzERxzKAiu5m=z*U$F;PeP`FNAZ*6X$! zpsd8$zN5$fnYJ19n8vG=1)?fZvuPCQqkT`xlymK%xs7&uym;)XUso zq{-a0hiYTLP$KC5Gzx|FN>F%knDlV$ndj_>{>dy)ynDE&?D&by==U~4tQBeJu0X*l zs`&64L4KGuVQhTJAE&iAKGsJ0g!`gSdLAg_+?T!oKokLJN7@)P%Nkg8yyhQ<-{DkN z45T8rp#NF50RmbaC#&}3wvUSbmY+2I`J8zKB!C}{$AO7T5T2#2E$p<=wp)g}cYyovYtH1)ILt$2I+K6qlgAxIDut2n-a}w^Cd+ z7rxX}*vj@AA%4vBM;TAPuWUyYlhQi(((kuW(p&LD7k z+;Hl^ZHg^oWm)eon`!L$L#~e1D0rMQ(P5svO=PAzZY_TQIiiQC{T>iEzB@+(yw)j7 z?08HQ88vJ098dSRivm(SEwOGkKOy!R7f`{MC^KO<9*qQF2g2268Oki7x5tm!gbP>1 zX|85lS$SxHXzW2g-Coiew*KIZe#&Ue(%EC#bh(v++MY1HiJ%=Jaz-yU*7AcKAc ze1<#YYEwA+LQwuwVcvE9`YaH&+$legl!*O{l;@9?CpVTc#f@;G`*pg0Do~XZTx_2d zCM=>@e=na2I^9$Wa8vvfKsRL__ZwBj)#c)hc&Vsn2^p!y9|Ro|``>cu7Z`wh>W}3? z96MWQi8%AsE1#$d4}8#XfU}##4oV;Hi{90dm6f-upUfdr`t75UZXfCpQnr2>be_r` z2YF=Ci@%knq)ZeH;WKU$v~T#aiwj^;S^YZ;@9`rUlj5U-thSo^pX{8R^dz_Jh3*(vY;O0m?Be5& z)E&FFT6;0sCoy(PT4=rfVvk7R^S4hFt5qc!(f!^|K~zuPpF;#vGfxfmha4|>8Oq2} zR<29{{GD#BOb@7hPt>mT=-c`(5_zlYiTk3CF0SL3zYI>R+aJoj4&1ys22fmb*p8bO zgeR=>jwP%VX=qq4$Bm*oIFrAzt6_$S=~W7GWwECsO8rx7^eu|fFU$CKR4FmTb;^%` z#TF3>1PRWVW*g39K45Mt6C~6eZvHqpzQ{fCqpg20R%8~vdO$y=;_k!ip>0E$r5N6( zQS>CZP4k19IMB-E`sTCWv7H>>Zm)^;YcLJ`W;W?~LPD3H%U56;;(Y0`quA;Crek9V z=XItwK7FVEE~+v51K>v(l#)I(;#x(Wgilk?C;Png#UZ&axOsA^a5FkRs_myHm!yeb z#_p=QFjEM-22(Y&>Iy|7P1!lMiojpWja<%Vl0+%=+9KBD;YXLKeS(kl4WCj;XA**{ z`3-TKg@9N5M05*f-H!_;5!4-l8>xBPxi-}yk%>pzM5y$xyIJ|ho{7u+Kg%{?rygPO zCim!odx{tQ6}SdFBHq1Kxc>H-m4hYrDNxJ-lvL!gFeXSND*D_Fl_G~^pB4%$OYq-u zkad*p{71+7lRu0=>;2yFyQJ>EPm#bLrnVLL%YfcoR7#PATG){pZ5uEt%ye>DPoiw2 zl^2JjxA(;%sTsmrQss8(N0ngUD>=rH$FJ~?zm+8jEx?TwHl=S$U}CIY&RjLjT$$h& zM;s!Bhzn)YY4o%M{^7p#^Fo#E&xHi`=$Ja84(JZxgOKaw?kbfS_Q8L)(<6I`YucXG z5Ax;6($WM;QQk4qjQ(S`EqT84`|Teo^hTJm_QrTfx@0XSMO>cDFKD8~%sdT$9u|)K zV=jfR&@_GT0`5y^C$REdE{89`>rIgfGX~u3LLvgLX(G}kXiYmEH7n*Hpsg6*r*qsaOLzQS?2 z8y;+1)`o6|gJ;v>Uz&_&&|hnm2_jb?IM4~I;RdRYbN_A64U%@o3buCTBE`!k;+B2b z0o(QRiwO9S*tg5}Z0Lh4rY4KF`fY1BTvBh;Ap($8K9Jp!q-qt{#HXVGRkrGRElXmG z5s2M-W$_K2KNJ<}Jy134WX~MyOu{kGZR$*+#Qm)+xIM}CXUF(s92eItnK;rydc0#N zZg*gwkp&366K?uXrH zq)do;M&2hNb}ts3;t3K3wo_cIiyrWEj-JjJo81M;Z9EY$}1A07>rKIPc)#s|( zwD{V@-3cI+=lSAe1)(!BDw318}0rh8YcGNI+G1(g3xX7~?w%;mTsaDeaINJ;omck!K%e1~RNOtSGl zsl%YnRPrZf!d|RNOwSFV^J0?eb^A-w7>FNb_2<3*YExrgWB`yG#FRNq3v2cHart?y zJ3q4`sl#t6lx~Hm^f2XNZP!$Rdj2yG;G&a;+Z*;pwLJ<(C`dnyZK(c*5Vx)hGih!%=Y&T|E@-mJ)$Ul1qNPXVoiqwv~W;2>TShJY;D40E46s zN7yP8zyJZIS@gTy)Ut^E5XeqcZ~V%!jp87T2wt|Wy|V4)glW2rNsZ=}p}tO0zjA4$ zP)=;#S_|>W@Cs*5h}(^q7M#3H(*&199!rM|DdD>7NxRjxPp&;|_N`n(y=n z5~ki|O=e7P%0+cP4}08nhKkE$4@N&fh*DNoMxRV&eR+oH@SSRjWyZ3fRkh@dA%0`+ zC*7@Fj+2S6y${PzY1IP6z$HFL0&_p9_?`VO_Q6}Tk7fT-8@^}jiq|X+}tWA5X078Nd*-2w*P3RN3JTqCL5 z5RjPpX?X~qIB(ho)BvLam$L~8i_~j#=|ajPLPT4%di%I{^=T4(@(FE$gJzU7fcCbx zwH7CThs&McQH;V|3*8kid+l$!F|Xl7+n}X1>7EA)SLRkk$EE82i!Paht@^xoUt|qz zR%2i7O6{Lg{NLM+c|Nx~P>7By(6@23_<8q5oAX`$j;nr@>miZu1Nq5Uq_CoS*DYhq z^Cw_iu>TAnCltud-uRYv={vDKD#EEXGs-dLQTRThgy&eEFe9LKOls5{1k_6cDDvr> zV+WsaC(i&fo?G98frT1QWEEHwzHG}c{!;&L>s?WXoIVCy(NTEk#=fXodY8k&`ilh; z76I)$X3*aQa*ss8?ESAj_&C+hgr?{ku@T^|`VSdT>}s@Y;1?VjLbeVOqE` z&@&I;c=FYOwjI{?u7=x_iCrDwCnaooMod~b`sQVPsoKq^@)l}RQipF5iZ{Y5u#~lM zW&RCPZMSC zULawNghr=KETYHOdsFB(lvj6=(Tbi2xnas=k;ky-J!e*g4nPi0b=@*LFx=>OngQ{+ zm)$v!!QJq)fGp7{r(K|ED09`g9%Ib7yO-Yfi(7$m2?~53TCa8!Ni9A9ysIJ7Z7siG zj}g4j3MWT6T(1GGE+NkoJ12Kca#OWKr%W4q-icTHA~d5-$_ssAeQy8j7wSg7-&fOY z_WMLackp{n9aM_r?k~*&5IQQa=#|YcPHQmXo7*ok`%E1S%NnLl<%jh%!%f-o;BVNV zn->1*8Prbw#86*=!bI(J9RNQrmXJ3*PF-d4H1yRyyoK3G^J>g`jZg>iW*ShO{c0ke zOcouJZFwv3m<=#(Pre^CA+Ny$oxM`|zh7I}lzr};2UpC8AIvTnSR+>4{*yevCl>fp zWkb+`aaIFq-m&I6FQu?G10r978sEOpDI<@iem+bTNaeHM8NQHet-EhKjYQbXOjCpP zFQ?kkteP*&A0Y~Y7ysj3PBOp|SKsX$ztzIFq8+H+@U7UWy1o3%@aZho*iabCY5LD? z@OSJ#qv)?D3-nfMMh<=5pB{Y?n_V{Y<}QHzh(}?@<&R2C)fTlz(X-$buc_J%^fWEx z3ug+=7xdNT+s8gKuv>EazrWnFxVa1hdEe0*4?twUFD!WWy_RjX56kxtpWMFhDSgd& zvKc@v#^m9Aei;?XBG|SXSkaPVb9}!{t_rRhaItUNY1&fy0_k!B8?mHqbZpNc;Nu>a zhdnHgugYP~8smQptF6`&57ivwR{?~3#Fs~rv7b7pZ~GOsW>Aez5B;1ePzU%T&x*}N zFX-xGuzOjTr9a5&c*``Rs$IDB1y+6GZDWT`r&vimv-H6&C$?`|37h%H5K;$1F!&v7R*z0yaB$RjVtpIHL&u?QuyI}A9jUjmv8Q5d zdE0M>bsne8J?zU*r$mYl^b#=u&!G@@!P~zOW)EA-zVP~aTEN3{Cur%NFo&c~$db2r zrzq{wm0nSK;1D5&t}^hSt`ILhu1N}KaUrdzQ+$*|O26^gz;n&%Ta~2J?==k=(x4x` zV=r4)6^%ui7@Itf9f(ei46(J6dFKzxY#O0&9m0yMwaP6#?!kucII{SSbp?3|CjH-K z`QwIFd|8iteKX~U?YBAI&Mjb{^&Z4gD_w_mGlpZUZy)Oau(rvURP2^^iP6CQTJ}}w zr5gF+(`_|)k6YQEW%HSg`DWNDCH{G|&YwAJR)neFfzNUa^LWSf+hgl@L>xXP zg}$Ahb+-DpR{Tz*ulo2UeW!2v*H5hH$IH*Y*(e^6G*<5TK8ELV)K7G+ZH-!&jsQC@ z;sO#Wy!=Ab(vzY4_=*&P(eotzTK+Cu$v2Gqp`G!u!2lB^GwKK4KpY1HcTf_m3qN;V zLFb#+#p#4KEpe8@{L<;Q1pmTH!j;J9@1k$;PS0kO3kJvth2c<=9b zL2dqf@=O^mLzHM0Ll+_c>o0}TDID#d3X<1W+JVqk&GkRKCLv{ZbW-w_m`U}p5aSz; zz&imKQ~99)>7t8`2xxe~zasCHzP*>I6;@1yDJ^9;j=M7p^R-OAZT{JD0Oi<;ddQjM z#;7P0_@t@(NAzJ}hm&DERy@FY-!r&e9mx4&Ou|4c^^75<^ySyS4FcR4EgV&NZuF}) zc4DsfvnMCb9rc~f`w(KLzc!a)b6TUZwc0;4JrH+mt(dW1Yd?)YH&mE^7f&=&)z`8T z1-2^HS|@#9A1&tRVIWcVvM;phA^7Pkz9YkFd}mWOJyk-KBRreoUQPBcfw{&pAh;0o z)^MWjz4-UKdUZ-{{XrC(8L!^2v9x;|*Cx**k)q7diOUg5F#Q)W5E$RuAGC|X#IF>NS82=7#*yL0^QZ)pz!=Lj;HZUl#JXzY~L{JY=v;^9{;6?!jD^3C*DU5};D-w{Au&0xE&|GMD2=Xj8B5S47xZcnmS?3*fRs^kNPFp^Q)*{ z(D0dV$$MG4oA*+UQ7;hwHgJ09kx(h;`C1Bk25XmjI502h8;uJCIyJ z=Y2xkR#`Ft&3>pzF84j$^P$i=06fz5R?}U3Ic2BY;E>~>pW_!xIRX{TI1JCn7(d)$ zSN+7y5T;lED(d(5<=!(^tk1o+me{BXyB=EWfHijb;*pbYTDgc<*8j zUq%3CV$PYLLTWQ=ojeaMC|fxB5lgFcM<-1U`WT5EpDxpj8QbsU(+`iL7HdznRXVQU z+^Lm2DAID2VERTzL+wDsut4 z&Nf`vNPC828d^Mw4Syt6?T~~uj|KcVEq?zZF*Y58Yo-1lmaF&)=@3>oQib{8^bAgK z&>V50E&v3BtfKB!+@ZaKVUyJw+E7@XI!u(Yz}2#~qwF81|?Fm`YJLj@1T z=nfts#%5WrICDAOmA{hnFiLaWM9ff;MpBTSmH6PgeQ>}TA5ZfzR5NpWm+x9Y?A+6! zcP@E4zREpFwtKcVzgG@tm-?r~%{gSImrAb&@k#vK)`_sj*C=-)PBsRtF7bpb(dd=oyIbfWNVA zqZSbyBGoqQzpHe$_^!2Tx_P<{s<&zz%r8LSAPeedTjK*QPds zUCp^npaL2-E9|Ls7#eh5jXr=#dHEkLvx#((UK-0fy_Q)>I2ID$yLDi0+1`vXR_N5n zSUdQS1jBMWSG5+>AF_1D{{cy{)pM6KGWvCbr|byWyZJ)6vykEKWoqP&pX_+ zzU6GiZBCswSLH5J`M#`#afSobL@u*v-d2wHsxHw#JMwKb~Xlun4H zoJdPq+qAmOAEMZzwRQH?pykz(Duy92cxF*^T`Ltc6VMJGiRJ{vzbZb7pF-yev&2B06f)Kt z)3k>SJ-E)L)>-Cc^}4QTV|YzCs+SMUSClycBdu~Cv9GKoy(>?dG``Uq1@gxU(FaKx z<9gorr(MlThBoR~6=<@+HNy=4mce|^WaMS0e{qSV^HJ>6K+H_N6Rt6T;hswUfEtQ@ z$$maZjU$_5HV*_oC)&~M5pPm6{u`9*GU8-0GUiMK^PsLD-rPP(*>)tug6#PEZp_s} z_tPL9i|PuIWBj3{BgU%x%G>Qif#HLSEA$_3m>kld~3hO?L@XMY_|PBzL+@q>y^aZ`Hq!> z?za_{0OrEv|3oh$?$#k_J8|J8=*`KD^~u~!V1zL4Lgs8Oc#o(nJ{{`ThkQ5UIFe{% zTyi(pldgqW>$Tfo8zbM_wJzW;<_on~J85YUT?WSCP^yDcsBthj1BLT^8f!I)4LnT~ zaOMx#JZDAh?^P2)JTF?Le!nh*EAl8gpKo1P4~!o+7oF*yRyR|dswbANQ(ax-EY2`u zCz*MQys}*$@lOI9+5HikuB=Hd$Z~Hn_M+~}T)`|WX67X2db1jt%BNrHFU#v#FPKHD zaJp;X9r73-1$G?AthE+N%po|Go%MNbpZuAut;vtIRP7al35S<)|EEzP7#8b?Ay~40 zbj)VHY&GrTb8DJ3F){i5IP|4rNZ_-_r)LI67hD}f4A|aZQ3^wTK;Cba@i}9S5gqu* z;Wf6oTfxWXoqc#!{NyrI` z-wrj-vP|lecL+GM4**;-gU2V}%fLJ`%eiOT!IJ##5#CU!4%BBbGHii29MD41o^iSg z2u93q#EZ))oA2emtf$zZme`jvKKrNJXM$yR8Jp>+eH@cE1JLOZ-=O_vUDxAvVpluC zz~sAnSCqnHAgIgo{Ke$!;>?V`t{mG!_q{V%gd5q6+!69b-(plm0tAW~4^PYU^6DUr znAc}ts)h*BGXu(zIU2!G6^<5#(^>s*_xvRn z+WB0?Fmu4G2988ObZ)Zpd~E(URw#O)J!GWI0H|u5sOGzmeiU9#Za*Cmn-!Zwug}rV z-u^{mTpAP~A0pybDJ_#Fm;QIF75cdNP)i3-t2keltj_v;o`$1K+1ZxQ$n+rBCNzST z03IFTF@4l0VQU$2Gjp9uM=6;@U%i~Ps@v_1^n9EC;)`mVFQ>-}&h}JGUNY*bVe0P3 zD#ilPbTTj&OS$yiJkyDJl9$5@sf zi8KrK?+sDAjvrREXwA4sW&_#&89k0>9zF2%3?i5>`_3=H0%!TCY67tS-}>MRjKAxc z`Y}+Qv115IMcSi>$*UuGH|LfTk#?fnaYM(|hKhk9YbRRYYVAfh{g^{O)l$RyckwDfoo}5hKVzC+3{p(B)v)(KPnMok z01@imH>4FQb|OqrMC{kdTs1B{o?hCQNK)Rz%`IFbwrhr9!$;J5&ijGw-AEk^m^Q9u z*CamB6%WsQ(WUfBnOjDu0#D;~yom@2CRA^{wfPa5=w~-ryK_Jjgfi%$!c|YN$qy1Z+a9q%+I@W@_~~pUHY9-kUL~-a>nnLdkj4nL`2F z0T2aXiR5)w=*_zIEm-)l52Uol05rYzUDju!@hg4vu=e~cJ3vpNvTxpfb1$hBC~qKY zx4hC5fq12DR`nz(EAV-8EEGWR@z{szm7`K71_+L*d?9<(I01z6i<_Z598pzPM*Iq5 z-Bs~Q5As|syXZ^jSY}qg>f6#vMbEsgcX$KW6Vbg%rPI!YW}>++H%pB%kXrZD2?&}f z=e8dAcu%82BeN6S{g;a+PO2R9FiT51%ip>7mD7n8!$}$@ULDP20h$G+Ex?4eXzh&Y zs)yiT+!Ht+8k!bP)sPwOxk|}+oZFaYBy)zH^)#B_GD!aHcS8tQCe)&b{G5kVglSRG z?C(_1sLxBnKn~e&&A+$)>G|Vr&wG3|U3;vj7^hEGhI0)D&H5ev+lx-$0^h2Wy1m-t*WG@VWi;VTw#^-edauyCtoV~q$k_!pF?$?LR3j3sb6zUtaf_h-9 z%84rAf$31^OB*7m*#T!VsyVE@(Qh__d)-AKDbf!@Uz1E@tVh5iXnIDfuqC|e7JBte z22pCcJF?bcKwso)Yi#VX`+Uh7#9}`K=^**liqID`LvehKLeFTW78*vExB>U3I}Lpk ztKjYMH;^EwedKIRc?OvA)cuExbx5q}v}VraI*E>H`dlbf$pwWVEzDI#1B#7ap$vo6 z?LTxw3|Q8y9*yQ1W0(9)9A2r`2mas+IRJtUhPR}<{~Pu5JAq%a`z=Uk=Rvx9r8Te` z_m(g!vOesV8mu7!8yx03OVmtnpcDhkz+9p1ek+$qiK&_rRF<)!B zqLTY?^X5HBOiRKAH9FO4{F0eHV}Cag=NbbPvKIJ8gMQvY(BAdPQ=ajG*TKb#!EU6w zr-9w4dXJF_9ShqAamO?5C4z6Zpj@rrYpfYRo}y8Cn}67gJEqka`%pi8KLSUcMHu6O z{&QKcITwhgp{+wv@~vuF1iO(G%#08i(NYaFA}M_`4>9VO_O6(>qQ&h@75?PuW50)sl{wk^k@Z)`8u=ZzE^+rRkud z?^?33qR)CLuK(BY?-McUMY~?qUNbi%LH({4lJ5sme-B0JF*I|WF6$=?AVdbw%$ zh++gDZ2kL)X|UNtaPoXIIk z@Pjmn8cLW6PXO84&$FgMfTQkM(=ws=MrfoS@7KVh{#z_PmaGfh;F& zkFqG4PA9*=9$#{{VlFFz8=sHKofN6xyq-?QQ?9jT#I5>+MQ4Elm!!t@^S#rTQ_&A| zdEJs65ML4HCH>Fa(-bwJVr+V74evcap#;B{r=Jc9Qy8kn zyrJ#xPyW~=IjxA-+cJpoCF&V7?0`+S^3hs7e499aWnn8pSiIlVpJB+jt75JT$`;m& zjURH)T(xuvHHra(BG=54D}L{_Z|@8>17hJhRkeF?6Wc^?y63nxEYt4~|HXD`z-Q)^VoH6h%>AdmzXVQ^n>@n5C)@pA+TgM1~^5D|N? zts62CDm~t7M@`6g1Ov+0Y4#5Ayxk*&xX|i3&1zn7$wkBk5jhxen%AFOks$BpId(fR(u77smpC4OEZoC2JMjGk)kja>@7bAhh}6jkxOFBKCb4M(bVd%bhvE5TwszOo;xyPNiKk74tk7zQ zUbXTAvex~C{g21(z1_`d^_MYSf3GZZ&U}-&1g(~ljEvy#nJ0E2a6m;XC^|iSvTmR;4(ytHtb&J{v4*AW_?mtJXAT)oq-Hh zi&mwSCo!?eUJID z7W0-DAq?QxyB^9w=3#&uD*qWh=OLycqip_A4VZ@}4(HpuMkb<%%+wTCg=bDod+#G0 z23*zfNu)nz~ptBVvZ%qPZn9{q@7oeq2X3fH}6}-j77s{c< z4`Vfbw^jl!F51j!!4f~Z+nw7-n8H1e<_bM@ndOdSKklOk3WO@{~ZTVYy!A??dG9=d}`uM$vS6cuIK~wCRQUAk~^H z{-FY`oQe*er2Y%1dYR^xKUMS0#@$KBjk`dqZ}5RgDZoP=c`lrB~MDF=uiwaxWJ zkil{i1Nc>PyTrU21Ut>Byg$3FWk6QrXt~+~Ig!Y)%D*s#EA;J!p%F^uj)>}Nb+>PB zU5pIK6d)VsdV0iEQZsJIyuY*8bI_2o(@UmjWnzr{HnEKEc>vQ7^^c{An$yTR{IQs3 z4H1+Bbd_173n3RzIm9!Q>L8xLIG#hT^=`$H)j5brIHttBZ*jbAWAcT$nVO}OW(cRO zLwvxmkenZP;kbib&zlISM`W$A&ssguZjjY%*uL{A5ae{KMUht&r~tIu|loY*obkfuao+RwmeIDk8NJ}fPieCW!aU}>qe>{kuP5LeE6l15&0E;-^ zS%XrV7SMB=pY1%?p{CSlmzs>A15L~_heN~{^O{C4a{HNgY=N3B7& zigTrs4#2}_(p`&J)AQ(n0r{SuU@-(#Jq{>aL(C8vM25Csq<0754~lsLKc z$U2Yzg|>2T+c|qzB(<^udR&dpx)11L0m%~(grR*l($c?*gPlx5Y6Z^AHh|(>0DM?q ze#DfU`KUIw5N%{S#Ja0x28g`fNLg{+Qn7|aQ9drmWoL`$@Xj2|FQ$s^%Yk+cq z$gh)X7yP8@1f^9A_Sj&VnfWhK0Q@TG@8`mFpw29$mS4m45-=*p0bnkm1pl;b#BuOf z0GnNI`(pQpLk}9fQg(KU)-DO~KAPFAZ_79Y`0hs~Zr2&W*D+=?%TLoGKz2#V!K6Fw zi*R~%UFMu@M>}A&9U%HA<~e|>#4kLR!>P{6n2HbeSuTT=4V(OPC4K&yFbmd8o*c)y z=ae%EAY#v9w!AXDRPF)c_LepSDEMfm+SI-X?3S>mz1q^4@Qc|&W||d3m+Vh7SZwz>vi8wleqVln{8)t8kDsBgW3Aauw zyslnhS9P-F;{31-Ba$J|RgICKD-!3DG*0uz4wcU|{Jfwa97~Xux`bMr_9(7z^Xv8BH^e`;P8_V)OlAoRF6zEo zh<{qZw|E&nLrnnd@Lu_fP>z*8xy1u6LCi7dsTOE|jt29i-LwPDA(z2=R&CMmw49BN z^8s;afuct!!rI0-^>@!a{k+e2Vtjh^8>yChL@s^DmXu+@`_`({xO7nTscXGMnyJZ5 z!A%#B#*w;FGHc7;i!D4R{(93%r()9Q%(~oGEIA)ynwb^*9#<8AGJ|vs5t2`wG+keP zXG}x0cn6rE1z{uVYK5hvd{?ak^>M=g5KF91@XT=|{T(4WiG4WzgQWGR zgiYvsaK&L>Z#(Y>Hdav9v6+KSZ;wys`$;g~q55B=#_QQjU&ppKF1`%6RQw-CEBo7) zjLt(x0ou#DxF?eGFvnajFI+HNSFFHCEkSmRAce`_nHVbWJ_j@9-_+GGsICn9@%XxV zgieD4BR5msKH_xvOmV=TYp&y$5+?Pe!h1vcMGFV*nX+NQBSuCt^h)>Srt!?kA9%f9pQ;? zguoh<%43~tF*ud$2e-#ITwul#-+s^ZF65$GK9v#mAk{%z_uYD04*gvvG}IgT-9ap9 z@h>RT$9?J{?$d9X%IT5Z#^|5-@x%g>S)tD3O`;ReGn%kJG)FNf$SJHYxQ4zCZNOj| zXgpzf zj+XJOc$CRPF&(FJ&r2{6K1hXNmiz1BmP<<=C~sz61;{}S(xOh?+DBmQrt zU29a5>AtUV%6*#B+>LjHDfdh@nZ_83%4KFs*)vTSkL4{xg&8eC4M`If&D^O8!n9{H z^Ma{olF17)1&Sf&C}87-92H1OCowQd4G|IXa^B9~AI>`GeAsK9FYj8s>wW(3dRfnU z*7N*sf8q#39ht?*@<{bSl)e(Rwe;>>JMIfMN~^k#&iqhA>{y7TFVkVVwe8^Hb#TT#1Bo2$;{p-Hs zqt5XKVANHw-vn78J}Pmkcztg~SE)bv=){T9S<}qZR3S6#Q8|u5Jphp&toGBwJ5_`D zj%0;Ycn+JhHgSlMjqSVXwrvT&syh9?7MEos@MpfBDm!v|ucAwUlw~YLX!ezxRkjr; z&{B>mAgZIdI=)XYRuw)N)ersfD%Hb$5higlo2F=0V|rEI}?xiM*6nK(Y#!P<6c`wwUGGX==^Xj7m2Vl7O7$ar}* znlTX2Xs`LY6$xaP#`N=jBwgs71U_ zvOr?dUDr4fUlYfxE2}nXOf$nm7yOfQ2WaW246fCs5h}J)9J26aM8}-04)QPDvARKG%S4oWN1K#bhn1bL7&t~^S)$Nb7OIL4 z-QMWxfRCav@Htpue|W^1=kFvN9?uc=ck6K=S_Z>nN@^ZP>{Eb^ZO)p6nE+laVq!!f zUtl*OXGPofRs113QRFw9CgMLwL{?~4b#s^^zn&~Xo4Dvd;j;r{V=Qh@+T2aL`_$?N3^sAMR-uAOvOJPlTy7;dWPC7Nzl+a691AKxf ztEjzME<6$UMs9{J_YsQt;ZLk(rcr9Ak18h~ANq&IHM^s6Su{z_MQADF$1O8GP7KN0 zo7y?oDQ0w^4hze%yIr4g<0Lozfh7?d`6m-0al4UsI{e||~J1~hMqfsAe8r%Sp8 zzspviePE6nzL&hc$seT{UOu;Q;uVYPF!X~FVC52vD343!9fUOyAcWgdf%UNMwa{*Y zXXrveGGa2c(Ewr?r6|O<$;-E=`CjZW@Yn)wX6&cA{q!PsC2PDfDFaAqg1Ki>JmL-_}gczL#>p++@%L4EFDK67qT~DT+R^X6<>p}3Yn|B09V7)+#3v!eQoLB z3jE=#AVlAHF>IwC0+Eqxe6;r>2KRCWKmrrdQ`RK~X~3GwU#LKwE4$9x?3uYThNFeb zzgJ4NW%94GYx|e#5=RuzvfVNn)+47b>39K(R2dZ~Wb_%Ov=PYYns3t5ZvoSz%RArU z>u^;WUX8B|^8>_C&nQBmv!cotojj>~3mM>0li= zsB|Qq=-FB`i=IDP3gF)Car?iO%D_WmCJ&h2#h3w!+SGa6%Z#lpkZGx6)_gwfAq`9+ zt{c+LuKHQn>q;oA3+L6m4ll!eY@bFW)rE}an-bFwuv8H4-pq@e(`UDM1;8sTKNY&6=v zaA(?f%j%TcyaEsJ9MBT%{5BjPWyeO07u*Q`kP)W_(XLCs%zzYO z!x-m~aDp%<%r#$9Hn{Mo{-5tz_lc58)@W@}JG66FGCQ^5`v>B|hT~+{RF%ehuvVU1 z`h$EA!2-PfGzpWV2C}FT?a9)CAswNwdam1{Jo+Q_JY;wFDPB0^V5dukvC#(|&ZrB|(*tYSNg=j|wQYUP zQ*-;1>-v@^YOH30JlI0TussK_A|rqle^w@&&c}UYk*F#YX%-$1#NQFqI^{_G&9sB& zHKT*La*sO16ebN=-ONl%!9dU=h%D9%!Eg6}JM@?I5zF0>OSk&23Hc(Cy~OuqaKkll z8W$e>>7YG`f#3L(!d8=DEar&^Om|tulueaw-??bH1n~ls|7xb)<(cnng)OSqeo?iI z;5rbW8gXL9fy1e;M_Nnom}@fb|9sK$S>3#&?u5i*$S3zhx>n=VBrH_XDnuDW$@36v zobYSCu&myCu?2L&*(NiDW_*HK`T{)8*1>Rxsn~vP@Ig+@c3`R_S=%dI zN*;LEPGFknpiQA*hW;&&(?=opB917Q(TlEU=t+Z;O3uxzurTz3M`6_hW4UNh)^JV6hb}H+%2p1mN)G+K8YOP?0pxD7vM~( z{p44D890FCIVlZVL?vpKV}>E341vw{^OoY9x;rayzxWi%JlnkOj=vuL3+8PVM5ve- z&DQCbGnV@Dz&kTfHLU-~<;ssq0v3o&zKrGq8?qNtE6KWF zv!+%)Yccrsz^|KYQd`q6#pea#)t4T|ynpJi>AMfSDZxyb2llt|M7b1vos7$wn!c_s{rXeDz!MvpTl3O&`u%29QyI1n&v;%Hu8Ua1~F zWisC+C;E}3%C>$_decRhjLGvsi@I9NfQ~;Z0hk3Qyj1MbaY&Wgp0f1BPfK{Aw#Td8 zTk8_dVZ73M0M#}aT)5+P#ua$J;%9Lr0>JkEG|XIm4oc)X?y_0PB4RdTdT8~l7t?sF zEmfY(pHeUjI|P;U?#PxBVYI)w+r=!Mn|Z!|w(-@^@NQBzLaJY$L7)cjrKyxtzfOTh ztWP{{0#*@{dbS5f%GA=!cT#h}0C?>SvS15LXHlpulw#OmiNq0(171iHP7cy{Ac7JX)YQ9ZA zOLZGXfD+#fw?ds9G(cwe&QN+%Wzaxy4+uOQsKDGlHd&bnDg-|!!@Ym1pr~vG=TA-R zBcCRX0nhdWcl`;c0{oJc@>CfuTt3XvzJ%ke0ef1Jiws0cEQ%%pUh?XSkl~%T?fB^< z58;}9--HcOJC+k{BZ!*mx!3-w43_QRxp|jd0JnrLpZZy{jr#`zs5C7Qf{zB6uwotg zr*ms=0&$zOB7m3@7sH&%pZg|gHmZ{BkO_wSH7x?TC|3c-Be6Eavtu;0ICdI(n)>*iQ!HKV;Cr8gvBTtgo>#l z^8)gqdPc;dlew41dlMsv2A|Me{1^P`2Z2*k9F>f%E6AfBWRo2^nc?eYY_g|ZQ#4es z`!tuW43PTEdtZ3>%! z#Ir{oRHvJ`gp;7a2m?r+s8L0_72=uT7djAQf(Pz#VwE>yNe{ERC95&M!-raU7r77S z)AOwM8rRF<3e2>$^0^=+cu(H?$m#<{PYuOU-bM3cmi$D9YiEXNgIzNo`Y%WZ&Lt{9 z&=L~Z-G0L(I9D84aF@ibhnZz*ztYL0l@WI`$kofj5AAAo6 z;Df5cN1og%Yl~k(PidH`7YB77x7O$mr)n~O3{Of_Z^_le#4)zcJV$___V>NO1!A{0aDVq7)}&+9&e4w{ z|Kjkbg}Rm&xGj%EzGnFrluw~W*f)D6^Ss3N8J8_p1K^JEc+_A(8AkfD%ayNEeC|^~ znaVz8j@RCn|C=qdv;3+Tt{U&r8agQy<2OWu{}>8n3^=g%XD>zHu)uY3U{`;10^ zpuxfNN5(K~K;IXKZRoFJ7){rx++$g!jX*Keg3yTL$BD4-#-Ii3-=%?VU}DW5=Dqs5 zU(ac;OKx?i!vwZ>tHOp*85mf?&iYgW8qZs zq2`s%ImO4_|yTGu Date: Mon, 19 Jan 2026 15:48:19 +0900 Subject: [PATCH 3/5] feat(#2): kakao oauth2 login --- docker-compose.yaml | 2 +- .../global/config/SecurityConfig.java | 57 +++++++++++-------- src/main/resources/application-prod.yml | 18 ++++++ src/main/resources/application-test.yml | 18 ++++++ 4 files changed, 69 insertions(+), 26 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index a2d2f9df..8de22e70 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,7 +11,7 @@ services: MYSQL_PASSWORD: ${MYSQL_PASSWORD} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} ports: - - "3306:3306" + - "3308:3306" volumes: - ./mysql_data:/var/lib/mysql command: diff --git a/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java b/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java index b988a085..ed9f887b 100644 --- a/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java +++ b/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java @@ -1,7 +1,8 @@ package com.example.RealMatch.global.config; -import java.util.Arrays; +import java.util.List; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -16,6 +17,7 @@ import com.example.RealMatch.global.config.jwt.JwtAuthenticationFilter; import com.example.RealMatch.global.config.oauth.OAuth2SuccessHandler; import com.example.RealMatch.global.config.service.CustomOAuth2UserService; +import com.example.RealMatch.global.presentation.advice.CustomAccessDeniedHandler; import com.example.RealMatch.global.presentation.advice.CustomAuthEntryPoint; import lombok.RequiredArgsConstructor; @@ -23,48 +25,53 @@ @Slf4j @Configuration -@RequiredArgsConstructor @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; private final OAuth2SuccessHandler oAuth2SuccessHandler; private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomAuthEntryPoint customAuthEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + + private static final String[] PERMIT_ALL_URL_ARRAY = { + "/", "/error", "/favicon.ico", + "/css/**", "/js/**", "/images/**", + "/login/**", "/oauth2/**", + "/api/test", + "/api/login/success", + "/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", "/swagger-ui.html" + + }; + + @Value("${cors.allowed-origins:http://localhost:3000,http://localhost:8080}") + private List allowedOrigins; @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { log.info("Configuring Security Filter Chain"); http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .exceptionHandling(exception -> exception.authenticationEntryPoint(customAuthEntryPoint)) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(customAuthEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler)) .authorizeHttpRequests(auth -> auth - .requestMatchers( - "/", "/error", "/favicon.ico", - "/css/**", "/js/**", "/images/**", - "/login/**", "/oauth2/**", - "/api/test", - "/api/login/success", - // --- Swagger 관련 경로 추가 --- - "/v3/api-docs/**", - "/swagger-ui/**", - "/swagger-ui.html" - ).permitAll() - .anyRequest().authenticated() - ) + .requestMatchers(PERMIT_ALL_URL_ARRAY).permitAll() + .anyRequest().authenticated()) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .oauth2Login(oauth2 -> oauth2 - .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuth2UserService)) .successHandler(oAuth2SuccessHandler) .failureHandler((request, response, exception) -> { log.error("OAuth2 login failed", exception); response.sendRedirect("/api/test?error=" + exception.getMessage()); - }) - ); + })); return http.build(); } @@ -72,9 +79,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8080")); - configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); - configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowedOrigins(allowedOrigins); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("*")); configuration.setAllowCredentials(true); configuration.setMaxAge(3600L); diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 83a0b6ef..fdc292c9 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -36,6 +36,24 @@ spring: security: refresh-token: expire-days: 14 + oauth2: + client: + registration: + kakao: + client-id: ${KAKAO_ID} + client-secret: ${KAKAO_SECRET} + redirect-uri: "http://localhost:8080/login/oauth2/code/kakao" + client-authentication-method: client_secret_post + authorization-grant-type: authorization_code + scope: + - profile_nickname + - account_email + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id logging: level: diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 41ce166c..e4e9cae9 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -35,6 +35,24 @@ spring: security: refresh-token: expire-days: 14 + oauth2: + client: + registration: + kakao: + client-id: ${KAKAO_ID} + client-secret: ${KAKAO_SECRET} + redirect-uri: "http://localhost:8080/login/oauth2/code/kakao" + client-authentication-method: client_secret_post + authorization-grant-type: authorization_code + scope: + - profile_nickname + - account_email + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id jwt: secret: ${JWT_SECRET} From 8267e9c094347db63f1efe0d8364dbdeb341ae7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EA=B2=BD=EC=88=98?= Date: Mon, 19 Jan 2026 16:19:29 +0900 Subject: [PATCH 4/5] feat(#2): kakao oauth2 login --- .../RealMatch/global/config/SecurityConfig.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java b/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java index 709b7a6c..186e7a1d 100644 --- a/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java +++ b/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java @@ -41,12 +41,12 @@ public class SecurityConfig { "/login/**", "/oauth2/**", "/api/test", "/api/login/success", - "/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", "/swagger-ui.html" + "/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", "/swagger-ui.html"}; private static final String[] REQUEST_AUTHENTICATED_ARRAY = { "/api/test-auth" }; - + @Value("${swagger.server-url}") private String swaggerUrl; @@ -60,19 +60,19 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) - + .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - + .exceptionHandling(exception -> exception .authenticationEntryPoint(customAuthEntryPoint) .accessDeniedHandler(customAccessDeniedHandler)) - + .authorizeHttpRequests(auth -> auth .requestMatchers(REQUEST_AUTHENTICATED_ARRAY).authenticated() .requestMatchers(PERMIT_ALL_URL_ARRAY).permitAll() .anyRequest().authenticated()) - + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo From d9c540bfb966834cd8d3fa8d6b4dff5129412bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EA=B2=BD=EC=88=98?= Date: Mon, 19 Jan 2026 16:21:43 +0900 Subject: [PATCH 5/5] feat(#2): kakao oauth2 login --- src/main/resources/application-prod.yml | 2 +- src/main/resources/application-test.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 22c2e547..0d442b41 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -42,7 +42,7 @@ spring: kakao: client-id: ${KAKAO_ID} client-secret: ${KAKAO_SECRET} - redirect-uri: "http://localhost:8080/login/oauth2/code/kakao" + redirect-uri: ${KAKAO_REDIRECT_URI} client-authentication-method: client_secret_post authorization-grant-type: authorization_code scope: diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index d7618bf3..04018fe8 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -39,9 +39,9 @@ spring: client: registration: kakao: - client-id: ${KAKAO_ID} - client-secret: ${KAKAO_SECRET} - redirect-uri: "http://localhost:8080/login/oauth2/code/kakao" + client-id: client + client-secret: secret + redirect-uri: http://redirect-uri-test client-authentication-method: client_secret_post authorization-grant-type: authorization_code scope: