ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -1,4 +1,4 @@
-import axios, {AxiosResponse} from 'axios';
+import axios, {AxiosResponse, InternalAxiosRequestConfig} from 'axios';
 import {API_BASE_URL} from '@/config/ApiEndPoints';
 import Cookies from 'js-cookie';
 
@@ -9,15 +9,48 @@ const apiClient = axios.create({
   },
 });
 
+apiClient.interceptors.request.use(
+  (config: InternalAxiosRequestConfig) => {
+    const token = Cookies.get('jwt');
+    if (token) {
+      config.headers.Authorization = `Bearer ${token}`;
+    }
+    return config;
+  },
+  error => {
+    return Promise.reject(error);
+  },
+);
+
+apiClient.interceptors.response.use(
+  (response: AxiosResponse) => {
+    return response;
+  },
+  error => {
+    if (error.response?.status === 401) {
+      // TODO: 토큰 만료 처리
+    }
+    return Promise.reject(error);
+  },
+);
+
 export default apiClient;
 
-export const postRequest = async (url: string, data: any) => {
-  const token = Cookies.get('jwt');
-  const response = await apiClient.post(url, data, {
-    headers: {
-      Authorization: `Bearer ${token}`,
-    },
-  });
+interface RequestConfig {
+  params?: Record<string, any>;
+  headers?: Record<string, any>;
+}
+
+export const postRequest = async <T>({
+  url,
+  data,
+  config = {},
+}: {
+  url: string;
+  data: any;
+  config?: RequestConfig;
+}) => {
+  const response = await apiClient.post<T>(url, data, config);
   return response;
 };
 
@@ -27,17 +60,14 @@ export const formDataEntries = (event: React.FormEvent<HTMLFormElement>) => {
   return data;
 };
 
-export const getRequest = async <T>(
-  url: string,
-  params: Record<string, any> = {},
-  token: string = '',
-): Promise<AxiosResponse<T>> => {
-  const response = await apiClient.get<T>(url, {
-    headers: {
-      Authorization: token ? `Bearer ${token}` : '',
-    },
-    params,
-  });
+export const getRequest = async <T>({
+  url,
+  config = {},
+}: {
+  url: string;
+  config?: RequestConfig;
+}): Promise<AxiosResponse<T>> => {
+  const response = await apiClient.get<T>(url, config);
   return response;
 };
 

리뷰: 모든 인증을 한군데서 처리하면서
서버에서 온 요청(SSR 등)과 클라이언트에서 온 요청마다 
쿠키 파싱을 다르게 할 수 있습니다(못한다거나)
그래서 `typeof window`로 서버/클라이언트 환경을 체크하고 다르게 처리해야 합니다

그리고 클라이언트(브라우저) 환경인 경우 `withCredentials: true`(https://inpa.tistory.com/entry/AXIOS-%F0%9F%93%9A-CORS-%EC%BF%A0%ED%82%A4-%EC%A0%84%EC%86%A1withCredentials-%EC%98%B5%EC%85%98) 설정으로 자동으로 쿠키를 실어서 보낼 수 있습니다.
아주 자주 사용하는 속성으로 반드시 알아두세요
@@ -1,4 +1,4 @@
-import axios, {AxiosResponse} from 'axios';
+import axios, {AxiosResponse, InternalAxiosRequestConfig} from 'axios';
 import {API_BASE_URL} from '@/config/ApiEndPoints';
 import Cookies from 'js-cookie';
 
@@ -9,15 +9,48 @@ const apiClient = axios.create({
   },
 });
 
+apiClient.interceptors.request.use(
+  (config: InternalAxiosRequestConfig) => {
+    const token = Cookies.get('jwt');
+    if (token) {
+      config.headers.Authorization = `Bearer ${token}`;
+    }
+    return config;
+  },
+  error => {
+    return Promise.reject(error);
+  },
+);
+
+apiClient.interceptors.response.use(
+  (response: AxiosResponse) => {
+    return response;
+  },
+  error => {
+    if (error.response?.status === 401) {
+      // TODO: 토큰 만료 처리
+    }
+    return Promise.reject(error);
+  },
+);
+
 export default apiClient;
 
-export const postRequest = async (url: string, data: any) => {
-  const token = Cookies.get('jwt');
-  const response = await apiClient.post(url, data, {
-    headers: {
-      Authorization: `Bearer ${token}`,
-    },
-  });
+interface RequestConfig {
+  params?: Record<string, any>;
+  headers?: Record<string, any>;
+}
+
+export const postRequest = async <T>({
+  url,
+  data,
+  config = {},
+}: {
+  url: string;
+  data: any;
+  config?: RequestConfig;
+}) => {
+  const response = await apiClient.post<T>(url, data, config);
   return response;
 };
 
@@ -27,17 +60,14 @@ export const formDataEntries = (event: React.FormEvent<HTMLFormElement>) => {
   return data;
 };
 
-export const getRequest = async <T>(
-  url: string,
-  params: Record<string, any> = {},
-  token: string = '',
-): Promise<AxiosResponse<T>> => {
-  const response = await apiClient.get<T>(url, {
-    headers: {
-      Authorization: token ? `Bearer ${token}` : '',
-    },
-    params,
-  });
+export const getRequest = async <T>({
+  url,
+  config = {},
+}: {
+  url: string;
+  config?: RequestConfig;
+}): Promise<AxiosResponse<T>> => {
+  const response = await apiClient.get<T>(url, config);
   return response;
 };
 

리뷰: 넵, https://github.com/wjdals0304/ecommerce-project/pull/47 
해당 pr 에서 코멘트할게요.
@@ -19,7 +19,8 @@
     "dev": "next dev",
     "build": "next build",
     "start": "next start",
-    "lint": "next lint"
+    "lint": "next lint",
+    "check": "npm run lint && tsc --noEmit && npm run build"
   },
   "eslintConfig": {
     "extends": [

리뷰: 궁금한게, 공통적으로 사용하는 apiClient.get 파라미터를 변경하였는데 
사용하고 있는곳에서 컴파일 버그가 안 잡히는 현상이 있었어요. 해당 파일 접근할때만 빨간색이 생기더라구요. 
@uu29 자바스크립트에서는 컴파일할때 버그 잡는게 없나요?
@@ -19,7 +19,8 @@
     "dev": "next dev",
     "build": "next build",
     "start": "next start",
-    "lint": "next lint"
+    "lint": "next lint",
+    "check": "npm run lint && tsc --noEmit && npm run build"
   },
   "eslintConfig": {
     "extends": [

리뷰: 있어요. 보통 타입스크립트 컴파일러에서 자동으로 해주는데...  타입스크립트 재시작(cursor의 restart typescript)해도 안되던가요?
@@ -19,7 +19,8 @@
     "dev": "next dev",
     "build": "next build",
     "start": "next start",
-    "lint": "next lint"
+    "lint": "next lint",
+    "check": "npm run lint && tsc --noEmit && npm run build"
   },
   "eslintConfig": {
     "extends": [

리뷰: gpt 물어보니 
1. TypeScript 도입
2. ESLint & JSDoc 활용
3. APIClient의 메서드 인터페이스를 고정
4. 정적 분석 도구 (TypeScript, ESLint) 도입

이렇게 하라고 하네요. 
흠 리스타트해도 안되는듯하네요?..
@@ -19,7 +19,8 @@
     "dev": "next dev",
     "build": "next build",
     "start": "next start",
-    "lint": "next lint"
+    "lint": "next lint",
+    "check": "npm run lint && tsc --noEmit && npm run build"
   },
   "eslintConfig": {
     "extends": [

리뷰: eslint를 설치해보는건 어떨까요? 흠
@@ -64,7 +64,7 @@ const BookFeedTab = ({
         />
         <Box sx={bookReviewTabStyles.sectionHeader}>
           <Typography variant="h6" fontWeight="bold">
-            한줄평 ({bookPostCount?.review_count})
+            한줄평 ({bookPostCount?.review_count || 0})
           </Typography>
           <Button
             size="small"
@@ -75,7 +75,7 @@ const BookFeedTab = ({
             더보기
           </Button>
         </Box>
-        {latestReviews ? (
+        {Array.isArray(latestReviews) && latestReviews.length > 0 ? (
           <Grid container spacing={2}>
             {latestReviews.map((review, index) => (
               <Grid
@@ -115,7 +115,7 @@ const BookFeedTab = ({
       <Box>
         <Box sx={bookReviewTabStyles.sectionHeader}>
           <Typography variant="h6" fontWeight="bold">
-            포스팅 ({bookPostCount?.posting_count})
+            포스팅 ({bookPostCount?.posting_count || 0})
           </Typography>
           <Button
             size="small"
@@ -126,7 +126,7 @@ const BookFeedTab = ({
             더보기
           </Button>
         </Box>
-        {latestPostings ? (
+        {Array.isArray(latestPostings) && latestPostings.length > 0 ? (
           <Grid container spacing={2}>
             {latestPostings.map((posting, index) => (
               <Grid

리뷰: 이 부분은 지워주시면 좋을 것 같습니다!
@@ -64,7 +64,7 @@ const BookFeedTab = ({
         />
         <Box sx={bookReviewTabStyles.sectionHeader}>
           <Typography variant="h6" fontWeight="bold">
-            한줄평 ({bookPostCount?.review_count})
+            한줄평 ({bookPostCount?.review_count || 0})
           </Typography>
           <Button
             size="small"
@@ -75,7 +75,7 @@ const BookFeedTab = ({
             더보기
           </Button>
         </Box>
-        {latestReviews ? (
+        {Array.isArray(latestReviews) && latestReviews.length > 0 ? (
           <Grid container spacing={2}>
             {latestReviews.map((review, index) => (
               <Grid
@@ -115,7 +115,7 @@ const BookFeedTab = ({
       <Box>
         <Box sx={bookReviewTabStyles.sectionHeader}>
           <Typography variant="h6" fontWeight="bold">
-            포스팅 ({bookPostCount?.posting_count})
+            포스팅 ({bookPostCount?.posting_count || 0})
           </Typography>
           <Button
             size="small"
@@ -126,7 +126,7 @@ const BookFeedTab = ({
             더보기
           </Button>
         </Box>
-        {latestPostings ? (
+        {Array.isArray(latestPostings) && latestPostings.length > 0 ? (
           <Grid container spacing={2}>
             {latestPostings.map((posting, index) => (
               <Grid

리뷰: 앗, 넵!
@@ -0,0 +1,33 @@
+package com.hansarangdelivery.config;
+
+import com.hansarangdelivery.entity.PageType;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.List;
+
+@Configuration
+public class PageableConfig implements WebMvcConfigurer {
+    @Override
+    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
+        PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver();
+        resolver.setFallbackPageable(PageRequest.of(0, 10)); // 기본값 설정
+        resolver.setMaxPageSize(50); // 최대 페이지 크기 제한
+        resolvers.add(resolver);
+    }
+
+    public static void validatePageSize(Pageable pageable) {
+        if(pageable.getPageSize() != PageType.TEN.getSize()
+            && pageable.getPageSize() != PageType.THIRTY.getSize()
+            && pageable.getPageSize() != PageType.FIFTY.getSize()
+        ) {
+            throw new IllegalArgumentException("페이지 사이즈는 10 / 30 / 50 중에서 입력 가능합니다.");
+        }
+    }
+}
+

리뷰: 이 부분에서 size를 10,30,50 적용시켜야 할것 같아요
@@ -0,0 +1,47 @@
+package nextstep.security.authentication;
+
+import java.util.List;
+import java.util.Map;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "spring.security.oauth2.client")
+public class OAuth2ClientProperties {
+
+    private Map<String, Registration> registrations;
+    private Map<String, Provider> providers;
+
+    public Map<String, Registration> getRegistrations() {
+        return registrations;
+    }
+
+    public void setRegistrations(Map<String, Registration> registrations) {
+        this.registrations = registrations;
+    }
+
+    public Map<String, Provider> getProviders() {
+        return providers;
+    }
+
+    public void setProviders(Map<String, Provider> providers) {
+        this.providers = providers;
+    }
+
+    public record Registration(
+            String provider,
+            String clientId,
+            String clientSecret,
+            String redirectUri,
+            String authorizationGrantType,
+            List<String> scope
+    ) {
+    }
+
+    public record Provider(
+            String name,
+            String authorizationUri,
+            String tokenUri,
+            String userInfoUri,
+            String accessTokenUri
+    ) {
+    }
+}

리뷰: 단순한 설정 파일은 record를 활용하면 코드가 깔끔해질 것 같아요~
@@ -0,0 +1,33 @@
+spring:
+  security:
+    oauth2:
+      client:
+        registrations:
+          github:
+            provider: "github"
+            client-id: "Ov23lijfjg9lkGyYVDXN"
+            client-secret: "YOUR_GITHUB_CLIENT_SECRET"
+            scope:
+              - read:user
+            redirect-uri: "http://localhost:8080/login/oauth2/code/github"
+            authorization-grant-type: authorization_code
+          google:
+            provider: "google"
+            client-id: "349246449409-h7hgk8kms3k8d7tgal8nesbh24h34t0d.apps.googleusercontent.com"
+            client-secret: "YOUR_GOOGLE_CLIENT_SECRET"
+            scope:
+              - email
+              - profile
+            redirect-uri: "http://localhost:8080/login/oauth2/code/google"
+            authorization-grant-type: authorization_code
+        providers:
+          github:
+            authorization-uri: "https://github.com/login/oauth/authorize?"
+            token-uri: "https://github.com/login/oauth/access_token"
+            access-token-uri: "http://localhost:8089/login/oauth/access_token"
+            user-info-uri: "https://api.github.com/user"
+          google:
+            authorization-uri: "https://accounts.google.com/o/oauth2/v2/auth?"
+            token-uri: "https://oauth2.googleapis.com/token"
+            access-token-uri: "http://localhost:8089/login/oauth/access_token"
+            user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo"

리뷰: 소소하지만.. IntelliJ를 사용하실 경우 자동으로 개행을 추가할 수 있습니다!

https://velog.io/@d-h-k/intellij-%ED%8C%8C%EC%9D%BC%EB%81%9D%EC%97%90-%EA%B0%9C%ED%96%89%EC%9D%84-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EC%B6%94%EA%B0%80%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95
@@ -0,0 +1,71 @@
+package nextstep.security.authentication;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Map;
+import nextstep.security.authentication.OAuth2ClientProperties.Provider;
+import nextstep.security.authentication.OAuth2ClientProperties.Registration;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.filter.GenericFilterBean;
+import org.springframework.web.util.UriComponentsBuilder;
+
+public class OAuth2LoginRedirectFilter extends GenericFilterBean {
+
+  private static final String REQUEST_URI = "/oauth2/authorization";
+
+  private final Map<String, Registration> registrations;
+  private final Map<String, Provider> providers;
+
+  public OAuth2LoginRedirectFilter(OAuth2ClientProperties properties) {
+    this.registrations = properties.getRegistrations();
+    this.providers = properties.getProviders();
+  }
+
+  @Override
+  public void doFilter(ServletRequest request,
+      ServletResponse response,
+      FilterChain chain) throws IOException, ServletException {
+    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
+
+    String requestURI = httpServletRequest.getRequestURI();
+    String method = httpServletRequest.getMethod();
+
+    String registrationSource = null;
+    if (requestURI.startsWith(REQUEST_URI)) {
+      registrationSource = getRegistrationId(requestURI);
+    }
+
+    if (registrationSource != null && method.equals(HttpMethod.GET.name())) {
+      Registration registration = registrations.get(registrationSource);
+      Provider provider = providers.get(registrationSource);
+
+      String redirectUri = UriComponentsBuilder.fromHttpUrl(provider.authorizationUri())
+          .queryParam("client_id", registration.clientId())
+          .queryParam("scope", String.join(" ", registration.scope()))
+          .queryParam("response_type", "code")
+          .queryParam("redirect_uri", registration.redirectUri())
+          .build()
+          .toUriString();
+
+      httpServletResponse.sendRedirect(redirectUri);
+      return;
+    }
+
+    chain.doFilter(request, response);
+  }
+
+  private String getRegistrationId(String requestURI) {
+    for (String registrationId : registrations.keySet()) {
+      if (requestURI.contains(registrationId)) {
+        return registrationId;
+      }
+    }
+    return null;
+  }
+}

리뷰: OAuth2 스펙에서는 scope를 공백으로 구분하는 것이 표준입니다.
지금은 쉼표로 구분하고 있는데, 표준에 맞게 수정해보면 어떨까요?

참고: https://datatracker.ietf.org/doc/html/rfc6749#section-3.3

![image](https://github.com/user-attachments/assets/6d590f38-dc07-46df-9b83-9cb916389362)
@@ -0,0 +1,71 @@
+package nextstep.security.authentication;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Map;
+import nextstep.security.authentication.OAuth2ClientProperties.Provider;
+import nextstep.security.authentication.OAuth2ClientProperties.Registration;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.filter.GenericFilterBean;
+import org.springframework.web.util.UriComponentsBuilder;
+
+public class OAuth2LoginRedirectFilter extends GenericFilterBean {
+
+  private static final String REQUEST_URI = "/oauth2/authorization";
+
+  private final Map<String, Registration> registrations;
+  private final Map<String, Provider> providers;
+
+  public OAuth2LoginRedirectFilter(OAuth2ClientProperties properties) {
+    this.registrations = properties.getRegistrations();
+    this.providers = properties.getProviders();
+  }
+
+  @Override
+  public void doFilter(ServletRequest request,
+      ServletResponse response,
+      FilterChain chain) throws IOException, ServletException {
+    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
+
+    String requestURI = httpServletRequest.getRequestURI();
+    String method = httpServletRequest.getMethod();
+
+    String registrationSource = null;
+    if (requestURI.startsWith(REQUEST_URI)) {
+      registrationSource = getRegistrationId(requestURI);
+    }
+
+    if (registrationSource != null && method.equals(HttpMethod.GET.name())) {
+      Registration registration = registrations.get(registrationSource);
+      Provider provider = providers.get(registrationSource);
+
+      String redirectUri = UriComponentsBuilder.fromHttpUrl(provider.authorizationUri())
+          .queryParam("client_id", registration.clientId())
+          .queryParam("scope", String.join(" ", registration.scope()))
+          .queryParam("response_type", "code")
+          .queryParam("redirect_uri", registration.redirectUri())
+          .build()
+          .toUriString();
+
+      httpServletResponse.sendRedirect(redirectUri);
+      return;
+    }
+
+    chain.doFilter(request, response);
+  }
+
+  private String getRegistrationId(String requestURI) {
+    for (String registrationId : registrations.keySet()) {
+      if (requestURI.contains(registrationId)) {
+        return registrationId;
+      }
+    }
+    return null;
+  }
+}

리뷰: authorization_grant_type 값을 추가해주신 이유가 있을까요?
@@ -0,0 +1,71 @@
+package nextstep.security.authentication;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Map;
+import nextstep.security.authentication.OAuth2ClientProperties.Provider;
+import nextstep.security.authentication.OAuth2ClientProperties.Registration;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.filter.GenericFilterBean;
+import org.springframework.web.util.UriComponentsBuilder;
+
+public class OAuth2LoginRedirectFilter extends GenericFilterBean {
+
+  private static final String REQUEST_URI = "/oauth2/authorization";
+
+  private final Map<String, Registration> registrations;
+  private final Map<String, Provider> providers;
+
+  public OAuth2LoginRedirectFilter(OAuth2ClientProperties properties) {
+    this.registrations = properties.getRegistrations();
+    this.providers = properties.getProviders();
+  }
+
+  @Override
+  public void doFilter(ServletRequest request,
+      ServletResponse response,
+      FilterChain chain) throws IOException, ServletException {
+    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
+
+    String requestURI = httpServletRequest.getRequestURI();
+    String method = httpServletRequest.getMethod();
+
+    String registrationSource = null;
+    if (requestURI.startsWith(REQUEST_URI)) {
+      registrationSource = getRegistrationId(requestURI);
+    }
+
+    if (registrationSource != null && method.equals(HttpMethod.GET.name())) {
+      Registration registration = registrations.get(registrationSource);
+      Provider provider = providers.get(registrationSource);
+
+      String redirectUri = UriComponentsBuilder.fromHttpUrl(provider.authorizationUri())
+          .queryParam("client_id", registration.clientId())
+          .queryParam("scope", String.join(" ", registration.scope()))
+          .queryParam("response_type", "code")
+          .queryParam("redirect_uri", registration.redirectUri())
+          .build()
+          .toUriString();
+
+      httpServletResponse.sendRedirect(redirectUri);
+      return;
+    }
+
+    chain.doFilter(request, response);
+  }
+
+  private String getRegistrationId(String requestURI) {
+    for (String registrationId : registrations.keySet()) {
+      if (requestURI.contains(registrationId)) {
+        return registrationId;
+      }
+    }
+    return null;
+  }
+}

리뷰: provider.getAuthorizationUri()에 대한 값도 UriComponentsBuilder에서 같이 생성해보면 좋겠습니다~
@@ -0,0 +1,27 @@
+package nextstep.security.authentication;
+
+import java.util.Map;
+import nextstep.security.authentication.OAuth2ClientProperties.Provider;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+public class OAuth2UserInfoClient {
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    public OAuth2UserInfo getUserInfo(String accessToken, Provider provider) {
+        HttpHeaders httpHeaders = new HttpHeaders();
+        httpHeaders.add("Authorization", "Bearer " + accessToken);
+
+        HttpEntity<Void> request = new HttpEntity<>(httpHeaders);
+        ResponseEntity<Map> response = restTemplate.exchange(provider.userInfoUri(), HttpMethod.GET,
+                request, Map.class);
+
+        Map<String, Object> attributes = response.getBody();
+
+        return OAuth2UserInfoFactory.getOAuth2UserInfo(provider.name(), attributes);
+    }
+}

리뷰: 지금은 Map의 형태로 응답하고 있는데, 이러한 코드는 타입 안정성이 부족하고 가독성이 떨어지게 됩니다.
OAuth 제공자마다 다른 응답 정보가 있을 수 있는 상황에서 안정성과 유연성을 어떻게 같이 가져갈 수 있을지 고민해보면 재미있겠네요 ㅎㅎ
@@ -0,0 +1,47 @@
+package nextstep.security.authentication;
+
+import java.util.List;
+import java.util.Map;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "spring.security.oauth2.client")
+public class OAuth2ClientProperties {
+
+    private Map<String, Registration> registrations;
+    private Map<String, Provider> providers;
+
+    public Map<String, Registration> getRegistrations() {
+        return registrations;
+    }
+
+    public void setRegistrations(Map<String, Registration> registrations) {
+        this.registrations = registrations;
+    }
+
+    public Map<String, Provider> getProviders() {
+        return providers;
+    }
+
+    public void setProviders(Map<String, Provider> providers) {
+        this.providers = providers;
+    }
+
+    public record Registration(
+            String provider,
+            String clientId,
+            String clientSecret,
+            String redirectUri,
+            String authorizationGrantType,
+            List<String> scope
+    ) {
+    }
+
+    public record Provider(
+            String name,
+            String authorizationUri,
+            String tokenUri,
+            String userInfoUri,
+            String accessTokenUri
+    ) {
+    }
+}

리뷰: 확실히 엄청 깔끔해지네요!