ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,83 @@
+import React, { useState } from "react";
+import { useQuery } from "@tanstack/react-query";
+import Pagination from "@/components/Common/Pagination";
+import ReviewModal from "@/components/Modal/ReviewModal";
+import Tab from "@/components/Tab";
+import WriteReviewCard from "@/components/Cards/writeReviewCard";
+
+const fetchReviews = async () => {
+  return [
+    {
+      id: 1,
+      name: "김코드 기사님",
+      date: "2024.07.01",
+      price: "210,000원",
+    },
+    {
+      id: 2,
+      name: "이코드 기사님",
+      date: "2024.07.03",
+      price: "250,000원",
+    },
+  ];
+};
+
+export default function AwaitingReview() {
+  const [reviews, setReviews] = useState([]);
+  const [currentPage, setCurrentPage] = useState(1);
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [selectedReview, setSelectedReview] = useState(null);
+
+  const ITEMS_PER_PAGE = 6;
+
+  const { data } = useQuery(
+    ["reviews", currentPage],
+    () => fetchReviews(currentPage, ITEMS_PER_PAGE),
+    {
+      keepPreviousData: true,
+    }
+  );
+
+
+
+  const handlePageChange = (page: number) => {
+    setCurrentPage(page);
+  };
+
+  const handleWriteReview = (review: any) => {
+    setSelectedReview(review);
+    setIsModalOpen(true);
+  };
+
+  return (
+    <div className="p-6">
+      <div>
+        <Tab />
+      </div>
+
+      <div className="grid gap-6">
+        {data.reviews.map((review: any) => (
+          <div key={review.id} className="p-4 border rounded-lg shadow-sm">
+            <WriteReviewCard
+              item={review}
+              onClick={() => handleWriteReview(review)}
+            />
+          </div>
+        ))}
+      </div>
+
+      <Pagination
+        currentPage={currentPage}
+        totalPages={Math.ceil(data.total / ITEMS_PER_PAGE)}
+        onPageChange={handlePageChange}
+      />
+
+      {isModalOpen && (
+        <ReviewModal
+          review={selectedReview}
+          closeModal={() => setIsModalOpen(false)}
+        />
+      )}
+    </div>
+  );
+}
\ No newline at end of file

리뷰: 오우 감사합니다 다 바꿔야겠네요
@@ -1,5 +1,5 @@
 import { DataForReviewRequestCode } from '@/apis/group';
-import { Button } from '@/components';
+import { Button, BlinkLoader } from '@/components';
 import { HOM_EVENT_NAME } from '@/constants';
 import { debounce, trackEventInAmplitude } from '@/utils';
 
@@ -42,7 +42,7 @@ const URLGeneratorButton = ({
       onClick={handleURLCreationButtonClick}
       disabled={!isFormValid && !mutation.isPending}
     >
-      {mutation.isPending ? '리뷰 링크 생성 중...' : '리뷰 링크 생성하기'}
+      {mutation.isPending ? <BlinkLoader>리뷰 링크 생성 중...</BlinkLoader> : '리뷰 링크 생성하기'}
     </Button>
   );
 };

리뷰: children으로 pending일 때 보여줄 문구를 받는 게 조금 더 자연스러워 보이는데 content를 받는 이유는 애니메이션 효과를 넣기 위함이었겠죠?!
@@ -1,5 +1,5 @@
 import { DataForReviewRequestCode } from '@/apis/group';
-import { Button } from '@/components';
+import { Button, BlinkLoader } from '@/components';
 import { HOM_EVENT_NAME } from '@/constants';
 import { debounce, trackEventInAmplitude } from '@/utils';
 
@@ -42,7 +42,7 @@ const URLGeneratorButton = ({
       onClick={handleURLCreationButtonClick}
       disabled={!isFormValid && !mutation.isPending}
     >
-      {mutation.isPending ? '리뷰 링크 생성 중...' : '리뷰 링크 생성하기'}
+      {mutation.isPending ? <BlinkLoader>리뷰 링크 생성 중...</BlinkLoader> : '리뷰 링크 생성하기'}
     </Button>
   );
 };

리뷰: 처음에는 적용범위를 '텍스트'로 한정해서, before의 content에 넣었어요. 
올리가 로딩을 구현하고 있다고 들으니 깜박이는 로딩 적용 범위를 텍스트 이외의 Node로 넓혀도 괜찮겠다는 생각이 들었어요.
현재 피그마 상에서는 깜박이는 로딩이 텍스트외에서 사용하는 곳이 없지만, 애니메이션 자체가 텍스트이외에 모든 요소에도 적용되는 상황이여서, children을 받도록 props 변경하고 컴포넌트명도 BlinkLoader로 변경했어요.
@@ -1,5 +1,5 @@
 import { DataForReviewRequestCode } from '@/apis/group';
-import { Button } from '@/components';
+import { Button, BlinkLoader } from '@/components';
 import { HOM_EVENT_NAME } from '@/constants';
 import { debounce, trackEventInAmplitude } from '@/utils';
 
@@ -42,7 +42,7 @@ const URLGeneratorButton = ({
       onClick={handleURLCreationButtonClick}
       disabled={!isFormValid && !mutation.isPending}
     >
-      {mutation.isPending ? '리뷰 링크 생성 중...' : '리뷰 링크 생성하기'}
+      {mutation.isPending ? <BlinkLoader>리뷰 링크 생성 중...</BlinkLoader> : '리뷰 링크 생성하기'}
     </Button>
   );
 };

리뷰: 굿굿👍 저도 loading 새로 파서 넣어야겠네요.
children을 받으니 이미지가 포함된 로딩 컴포넌트도 쉽게 사용할 수 있을 것 같아요!
@@ -0,0 +1,13 @@
+import React from 'react';
+
+import { EssentialPropsWithChildren } from '@/types';
+
+import * as S from './style';
+
+interface BlinkLoaderProps extends EssentialPropsWithChildren, S.LoaderProps {}
+
+const BlinkLoader = ({ children, $animationDurationTime }: BlinkLoaderProps) => {
+  return <S.Loader $animationDurationTime={$animationDurationTime}>{children}</S.Loader>;
+};
+
+export default BlinkLoader;

리뷰: 컴포넌트에서 받는 Props명은 `$`를 안 붙이고, style props명에만 `$`를 붙이기로 했던 것 같아요. 확정지었는지 기억이 확실치 않아서 코멘트 남겨요😅
@@ -0,0 +1,13 @@
+import React from 'react';
+
+import { EssentialPropsWithChildren } from '@/types';
+
+import * as S from './style';
+
+interface BlinkLoaderProps extends EssentialPropsWithChildren, S.LoaderProps {}
+
+const BlinkLoader = ({ children, $animationDurationTime }: BlinkLoaderProps) => {
+  return <S.Loader $animationDurationTime={$animationDurationTime}>{children}</S.Loader>;
+};
+
+export default BlinkLoader;

리뷰: children으로 더 많은 요소들이 올 수 있게 확장되어 다양하게 사용될 수 있을 것 같아요👍🏻
@@ -0,0 +1,13 @@
+import React from 'react';
+
+import { EssentialPropsWithChildren } from '@/types';
+
+import * as S from './style';
+
+interface BlinkLoaderProps extends EssentialPropsWithChildren, S.LoaderProps {}
+
+const BlinkLoader = ({ children, $animationDurationTime }: BlinkLoaderProps) => {
+  return <S.Loader $animationDurationTime={$animationDurationTime}>{children}</S.Loader>;
+};
+
+export default BlinkLoader;

리뷰: > 컴포넌트에서 받는 Props명은 `$`를 안 붙이고, style props명에만 `$`를 붙이기로 했던 것 같아요. 확정지었는지 기억이 확실치 않아서 코멘트 남겨요😅

그런 논의가 있었는지 저도 잘 기억이 나지 않네요. 다만, 저번에 진행한 회원용 공통 UI 작업에서 스타일 컴포넌트의 props를 확장 받아서 사용하는 코드들이 있었고 스타일 Props의 별다를 props를 받는게 아니라서 스타일 컴포넌트 props를 확장받는 것으로 했어요. 

만약 이 부분에 대한 코드 컨벤션 필요가 느껴진다면, 프론트 회의에서 논의해보죠
@@ -6,8 +6,14 @@ import { checkIsValidImage, checkIsValidScore } from '../../validChecker';
 
 const reviewRouter = Router();
 
-reviewRouter.get('/', checkRequiredFieldsQuery(['userId', 'page']), async (req, res) => {
-  const { userId, page, pageSize: pageSize = 10 } = req.query;
+  interface QueryType {
+  userId?: string;  
+  travelId?: string;
+  }
+
+
+reviewRouter.get('/', checkRequiredFieldsQuery(['page']), async (req, res) => {
+  const { userId, travelId, page, pageSize: pageSize = 10 } = req.query;
   const page_ = Number(page);
   const pageSize_ = Number(pageSize);
 
@@ -16,12 +22,17 @@ reviewRouter.get('/', checkRequiredFieldsQuery(['userId', 'page']), async (req,
     return;
   }
 
+
+  const findQuery:QueryType = {};
+  if (userId) findQuery.userId = String(userId);
+  if(travelId) findQuery.travelId = String(travelId);
+
   try {
     const skip = (page_ - 1) * pageSize_;
-    const totalReviews = await Review.countDocuments({ userId });
+    const totalReviews = await Review.countDocuments(findQuery);
     const totalPages = Math.ceil(totalReviews / pageSize_);
 
-    const reviews = await Review.find({ userId })
+    const reviews = await Review.find(findQuery)
       .sort({ createdDate: -1 })
       .skip(skip)
       .limit(pageSize_)
@@ -30,14 +41,21 @@ reviewRouter.get('/', checkRequiredFieldsQuery(['userId', 'page']), async (req,
     const reviewsWithTravelInfo = await Promise.all(
       reviews.map(async (review) => {
         const travel = await Travel.findOne({ id: review.travelId }).lean();
+        const user = await User.findById(review.userId).lean();
         return {
           id: review._id,
           travelId: review.travelId,
           reviewImg: review.reviewImg,
           content: review.content,
           travelScore: review.travelScore,
           createdDate: review.createdDate,
-          travelTitle: travel?.travelTitle || '',
+          travelTitle: travel?.travelTitle || '',      
+          userName: user?.userName,
+          socialName: user?.socialName,
+          userProfileImage: user?.userProfileImage,
+          userEmail: user?.userEmail,
+          isVerifiedUser: user?.isVerifiedUser,
+          mbti: user?.mbti,
         };
       }),
     );

리뷰: _:warning: Potential issue_

**[민감 정보 반환 가능성]**  
`userEmail`을 그대로 응답에 포함하면 사용자 이메일이 노출될 수 있어, PII(개인정보) 보호 측면에서 고려가 필요합니다.  

사용자 이메일이 외부 API 응답에 포함되어도 괜찮은지를 다시 한 번 검토해 보세요. 필요하다면 마스킹 처리나 반환 제한을 권장합니다.

<!-- This is an auto-generated comment by CodeRabbit -->
@@ -0,0 +1,32 @@
+package nextstep.security.config;
+
+import nextstep.security.UserDetails;
+
+import java.security.Principal;
+
+//  Implementations which use this class should be immutable.
+public abstract class AbstractAuthenticationToken implements Authentication{
+
+    private Object details;
+
+    @Override
+    public String getName() {
+        if (this.getPrincipal() instanceof UserDetails userDetails) {
+            return userDetails.getUsername();
+        }
+        if (this.getPrincipal() instanceof Principal principal) {
+            return principal.getName();
+        }
+        return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString();
+    }
+
+    public void setDetails(Object details) {
+        this.details = details;
+    }
+
+    @Override
+    public Object getDetails() {
+        return this.details;
+    }
+
+}

리뷰: principal이 추가될 때 마다 해당 클래스를 수정해야 하는 것은 문제가 있어 보이는데요.
생성 시점에 principal에서 필요한 값만 추출하여 넘겨 받는다면 해당 문제를 해결할 수 있을 것 같아요!
@@ -0,0 +1,42 @@
+package nextstep.security.config;
+
+import nextstep.security.AuthenticationException;
+import nextstep.security.UserDetailService;
+import nextstep.security.UserDetails;
+
+public class DaoAuthenticationProvider implements AuthenticationProvider{
+
+    private final UserDetailService userDetailService;
+    private final PasswordEncoder passwordEncoder;
+
+    public DaoAuthenticationProvider(UserDetailService userDetailService, PasswordEncoder passwordEncoder) {
+        this.userDetailService = userDetailService;
+        this.passwordEncoder = passwordEncoder;
+    }
+
+    public DaoAuthenticationProvider(UserDetailService userDetailService) {
+        this.userDetailService = userDetailService;
+        this.passwordEncoder = null;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+        if (!supports(authentication.getClass())) {
+            return null;
+        }
+        String username = (String) authentication.getPrincipal();
+        String password = (String) authentication.getCredentials();
+
+        UserDetails user = userDetailService.getUserByUsername(username);
+        if (!password.equals(user.getPassword())) {
+            throw new AuthenticationException("Passwords do not match");
+        }
+        return new UsernamePasswordAuthenticationToken(username, password, user);
+    }
+
+    @Override
+    public boolean supports(Class<?> authentication) { // todo 왜 instanceOf가 아닌 이렇게 클래스로 비교를 하는걸까?
+        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
+    }
+
+}

리뷰: 해당 클래스를 사용하고 있지 않군요!
@@ -0,0 +1,45 @@
+package nextstep.security.config;
+
+import nextstep.security.ProviderNotFoundException;
+
+import java.util.Collections;
+import java.util.List;
+
+public class ProviderManager implements AuthenticationManager {
+
+    private List<AuthenticationProvider> providers = Collections.emptyList();
+
+    public ProviderManager(List<AuthenticationProvider> providers) {
+        this.providers = providers;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) {
+        Class<? extends Authentication> authenticationClassToTest = authentication.getClass();
+        Authentication result = null;
+        for (AuthenticationProvider provider : this.providers) {
+            if (!provider.supports(authenticationClassToTest)) {
+                continue;
+            }
+            result = provider.authenticate(authentication);
+            if (result != null) { // 인증된 객제가 있다면
+                copyDetails(authentication, result);
+                result.setAuthenticated(true); // todo 책임 확인 한번 필요
+                break;
+            }
+        }
+        if (result == null) {
+            throw new ProviderNotFoundException("지원하는 provider가 없습니다!");
+        }
+        return result;
+    }
+
+    // 인증된 토큰에 detail 값이 없다면 detail 넣어주기
+    private void copyDetails(Authentication source, Authentication dest) {
+        if ((dest instanceof AbstractAuthenticationToken token) && (dest.getDetails() == null)) {
+            token.setDetails(source.getDetails());
+        }
+    }
+
+}
+

리뷰: 지원하는 provider를 찾아도 authenticate 시 null이 응답된다면 해당 예외 메시지가 출력되겠네요.
의도한 내용이 맞을까요?
@@ -0,0 +1,45 @@
+package nextstep.security.config;
+
+import nextstep.security.ProviderNotFoundException;
+
+import java.util.Collections;
+import java.util.List;
+
+public class ProviderManager implements AuthenticationManager {
+
+    private List<AuthenticationProvider> providers = Collections.emptyList();
+
+    public ProviderManager(List<AuthenticationProvider> providers) {
+        this.providers = providers;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) {
+        Class<? extends Authentication> authenticationClassToTest = authentication.getClass();
+        Authentication result = null;
+        for (AuthenticationProvider provider : this.providers) {
+            if (!provider.supports(authenticationClassToTest)) {
+                continue;
+            }
+            result = provider.authenticate(authentication);
+            if (result != null) { // 인증된 객제가 있다면
+                copyDetails(authentication, result);
+                result.setAuthenticated(true); // todo 책임 확인 한번 필요
+                break;
+            }
+        }
+        if (result == null) {
+            throw new ProviderNotFoundException("지원하는 provider가 없습니다!");
+        }
+        return result;
+    }
+
+    // 인증된 토큰에 detail 값이 없다면 detail 넣어주기
+    private void copyDetails(Authentication source, Authentication dest) {
+        if ((dest instanceof AbstractAuthenticationToken token) && (dest.getDetails() == null)) {
+            token.setDetails(source.getDetails());
+        }
+    }
+
+}
+

리뷰: ProviderManager는 여러 개의 AuthenticationProvider를 관리하고, 올바른 Provider를 선택하여 인증을 수행하는 역할을 담당하게 되는데요.
이 역할은 ProviderManager이 아닌 AuthenticationProvider의 역할에 더 적절해 보이는데 어떻게 생각하시나요?
@@ -0,0 +1,49 @@
+package nextstep.security.interceptor;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.AuthenticationException;
+import nextstep.security.UserDetailService;
+import nextstep.security.UserDetails;
+import nextstep.security.util.Base64Convertor;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+public class BasicAuthInterceptor implements HandlerInterceptor {
+
+    private final UserDetailService userDetailService;
+
+    public BasicAuthInterceptor(UserDetailService userDetailService) {
+        this.userDetailService = userDetailService;
+    }
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        try {
+            String authorizationHeader = request.getHeader("Authorization");
+            String authType = authorizationHeader.split(" ")[0];
+            String credentials = authorizationHeader.split(" ")[1];
+            String decodedString = Base64Convertor.decode(credentials);
+            checkAuthType(authType);
+
+            String[] usernameAndPassword = decodedString.split(":");
+            String username = usernameAndPassword[0];
+            String password = usernameAndPassword[1];
+
+            UserDetails userDetail = userDetailService.getUserByUsername(username);
+            if (!userDetail.getPassword().equals(password)) {
+                throw new AuthenticationException();
+            }
+            return true;
+        } catch (Exception e) {
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            return false;
+        }
+    }
+
+    private void checkAuthType(String authType) {
+        if (!authType.equalsIgnoreCase(HttpServletRequest.BASIC_AUTH)) {
+            throw new AuthenticationException();
+        }
+    }
+
+}

리뷰: BasicAuthInterceptor와 FormLoginInterceptor는 사용되지 않는 것 같은데, 어떤 이유로 추가하셨을까요?
@@ -0,0 +1,42 @@
+package nextstep.security.config;
+
+import nextstep.security.AuthenticationException;
+import nextstep.security.UserDetailService;
+import nextstep.security.UserDetails;
+
+public class DaoAuthenticationProvider implements AuthenticationProvider{
+
+    private final UserDetailService userDetailService;
+    private final PasswordEncoder passwordEncoder;
+
+    public DaoAuthenticationProvider(UserDetailService userDetailService, PasswordEncoder passwordEncoder) {
+        this.userDetailService = userDetailService;
+        this.passwordEncoder = passwordEncoder;
+    }
+
+    public DaoAuthenticationProvider(UserDetailService userDetailService) {
+        this.userDetailService = userDetailService;
+        this.passwordEncoder = null;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+        if (!supports(authentication.getClass())) {
+            return null;
+        }
+        String username = (String) authentication.getPrincipal();
+        String password = (String) authentication.getCredentials();
+
+        UserDetails user = userDetailService.getUserByUsername(username);
+        if (!password.equals(user.getPassword())) {
+            throw new AuthenticationException("Passwords do not match");
+        }
+        return new UsernamePasswordAuthenticationToken(username, password, user);
+    }
+
+    @Override
+    public boolean supports(Class<?> authentication) { // todo 왜 instanceOf가 아닌 이렇게 클래스로 비교를 하는걸까?
+        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
+    }
+
+}

리뷰: 네 passwordencoder는 추후 미션 진행하면서 조금 더 뜯어볼 예정입니다!
@@ -0,0 +1,45 @@
+package nextstep.security.config;
+
+import nextstep.security.ProviderNotFoundException;
+
+import java.util.Collections;
+import java.util.List;
+
+public class ProviderManager implements AuthenticationManager {
+
+    private List<AuthenticationProvider> providers = Collections.emptyList();
+
+    public ProviderManager(List<AuthenticationProvider> providers) {
+        this.providers = providers;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) {
+        Class<? extends Authentication> authenticationClassToTest = authentication.getClass();
+        Authentication result = null;
+        for (AuthenticationProvider provider : this.providers) {
+            if (!provider.supports(authenticationClassToTest)) {
+                continue;
+            }
+            result = provider.authenticate(authentication);
+            if (result != null) { // 인증된 객제가 있다면
+                copyDetails(authentication, result);
+                result.setAuthenticated(true); // todo 책임 확인 한번 필요
+                break;
+            }
+        }
+        if (result == null) {
+            throw new ProviderNotFoundException("지원하는 provider가 없습니다!");
+        }
+        return result;
+    }
+
+    // 인증된 토큰에 detail 값이 없다면 detail 넣어주기
+    private void copyDetails(Authentication source, Authentication dest) {
+        if ((dest instanceof AbstractAuthenticationToken token) && (dest.getDetails() == null)) {
+            token.setDetails(source.getDetails());
+        }
+    }
+
+}
+

리뷰: > 지원하는 provider를 찾아도 authenticate 시 null이 응답된다면 해당 예외 메시지가 출력되겠네요. 의도한 내용이 맞을까요?

네 맞습니다!
`AuthenticationManager`에서 인증을 support하는 `AuthenticationProvider`가 없다면 해당 예외를 던지게 만들어 놓았습니다!
`ProviderManager` 코드를 참고했는데요, `ProviderManager`에서는 `parent` `AuthenticationManager`가 존재한다면 부모 `AuthenticationManager`가 `authenticate()` 를 호출하는데, 결국은 그 부모도 support하는 `AuthenticationProvider`가 없다면 `ProviderNotFoundException`을 던지길래 참고했습니다.!

`ProviderManager`의 `authenticate() ` 일부
``` java
if (result == null && this.parent != null) {
	// Allow the parent to try.
	try {
		parentResult = this.parent.authenticate(authentication);
		result = parentResult;
	}
	catch (ProviderNotFoundException ex) {
		// ignore as we will throw below if no other exception occurred prior to
		// calling parent and the parent
		// may throw ProviderNotFound even though a provider in the child already
		// handled the request
	}
	catch (AuthenticationException ex) {
		parentException = ex;
		lastException = ex;
	}
    ....
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
	lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
			new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
....
}
```
@@ -0,0 +1,45 @@
+package nextstep.security.config;
+
+import nextstep.security.ProviderNotFoundException;
+
+import java.util.Collections;
+import java.util.List;
+
+public class ProviderManager implements AuthenticationManager {
+
+    private List<AuthenticationProvider> providers = Collections.emptyList();
+
+    public ProviderManager(List<AuthenticationProvider> providers) {
+        this.providers = providers;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) {
+        Class<? extends Authentication> authenticationClassToTest = authentication.getClass();
+        Authentication result = null;
+        for (AuthenticationProvider provider : this.providers) {
+            if (!provider.supports(authenticationClassToTest)) {
+                continue;
+            }
+            result = provider.authenticate(authentication);
+            if (result != null) { // 인증된 객제가 있다면
+                copyDetails(authentication, result);
+                result.setAuthenticated(true); // todo 책임 확인 한번 필요
+                break;
+            }
+        }
+        if (result == null) {
+            throw new ProviderNotFoundException("지원하는 provider가 없습니다!");
+        }
+        return result;
+    }
+
+    // 인증된 토큰에 detail 값이 없다면 detail 넣어주기
+    private void copyDetails(Authentication source, Authentication dest) {
+        if ((dest instanceof AbstractAuthenticationToken token) && (dest.getDetails() == null)) {
+            token.setDetails(source.getDetails());
+        }
+    }
+
+}
+

리뷰: 그런데 궁금한점은 `ProviderManager` 는 내부에  `parent`  프로퍼티로 `AuthenticationManager` 가져서 `AuthenticationManager` 를 계속해서 이을수 있다는것을 확인했는데요,
왜 이렇게 만들었을지 재연님은 어떻게 생각하시나요?
왜 parent를 프로퍼티로 두었을까요? 
여러 AuthenticationManager  간의 인증 절차를 유연하게 설정하기 위해 관계 설정을 했던것일것 같긴한데 궁금합니다 :)